pax_global_header00006660000000000000000000000064152731110430014507gustar00rootroot0000000000000052 comment=f9af73ed5ba1a191aec1d97e0f1945b28a6f47bf statistics-release-2.1.2/000077500000000000000000000000001527311104300153215ustar00rootroot00000000000000statistics-release-2.1.2/CONTRIBUTING.md000066400000000000000000000405511527311104300175570ustar00rootroot00000000000000# Contribution Guidelines Thank you for considering a contribution to the **statistics** package! These guidelines describe how to write code that blends into the package so your submission can be reviewed and merged quickly. When in doubt, copy the conventions of an existing neighbouring file rather than introducing new ones. ## 1. License Every source file in the **statistics** package is licensed under the [GNU General Public License, version 3 or later](https://www.gnu.org/licenses/gpl-3.0.en.html) (GPLv3+) — there are no other licenses in the package. If you are submitting a new function, it must carry the following header (use the appropriate year and your own name and email): ``` ## Copyright (C) 2026 Your Name ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more ## details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ``` Use the current year for a brand-new file (e.g. `2026`); when editing an existing file, extend its year into a range (e.g. `2022-2026`). ## 2. Documentation New functions should be properly documented with the embedded help text in [Texinfo](https://www.gnu.org/software/texinfo/) format. The documentation block goes **after** the License header and **before** the function body. The Texinfo manual is [here](https://www.gnu.org/software/texinfo/manual/texinfo/), but reading through existing function files should do the trick. ``` ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} anova1 (@var{x}) ## @deftypefnx {statistics} {@var{p} =} anova1 (@var{x}, @var{group}) ## ## Help text goes here. ## ## @seealso{anova2, anovan} ## @end deftypefn ``` A few conventions for the Texinfo block: - Open every block with `## -*- texinfo -*-`. - Standalone functions use `@deftypefn {statistics} ... @end deftypefn`; additional signatures use `@deftypefnx`. - Mark arguments and variables with `@var{X}`, inline code with `@code{...}`, and quoted literals or option names with `@qcode{'name'}`. - Add cross-references with `@seealso{a, b, c}` as the last line before `@end deftypefn`. - Put **two spaces after a sentence-ending period** inside the block, matching GNU Texinfo style. For **classdef classes**, the documentation is split across several blocks that use `@deftp` (define-type) rather than `@deftypefn`: - The **class** itself is documented with a `@deftp {statistics} ... @end deftp` block, placed right after the `classdef` line. - Each public **property** carries its own `@deftp {} {property} ... @end deftp` block, immediately above the property in its `properties` section. - Each public **method**, by contrast, is documented exactly like a standalone function — with a `@deftypefn {} ... @end deftypefn` block above the method definition. The scope name in the braces is the class name, and the method's error messages are prefixed `.:` (see §4). In short: **classes and properties use `@deftp`; methods use `@deftypefn`**, just as free functions do. - Keep every body line **within 80 columns**. The only lines allowed to run over are the `@deftypefn` / `@deftypefnx` header lines, whose signatures may be longer. **Note:** the Texinfo source is not printed verbatim on the command window. Type e.g. `help anova1` to render and review the documentation as the user sees it. ## 3. Demos Although examples can live in the help documentation, it is always useful to embed runnable examples as demos, which the user invokes with the `demo` command: ``` >> demo anova1 ``` Demos go at the **end of the file**, after the function and any local helper functions. A small demo looks like this: ``` %!demo %! x = meshgrid (1:6); %! x = x + normrnd (0, 1, 6, 6); %! anova1 (x, [], 'off'); ``` ### Demo comments and online documentation The online reference pages for the package are generated with [pkg-octave-doc](https://github.com/gnu-octave/pkg-octave-doc), which renders each `%!demo` block as an interleaved **notebook**: the block is split into cells so that every statement's console output and any figures appear immediately beneath the code that produced them. To make a demo read well both in the terminal (`demo funcname`) and online, its comment lines support a **small subset of Markdown** instead of Texinfo. The supported constructs are: - inline `` `code` ``, `**bold**`, `*italic*`, and `[text](url)` links; - unordered lists (lines starting with `-` or `*`) and ordered lists (lines starting with `1.`); - paragraphs, separated by a blank comment line. Underscore emphasis (`_text_`) and `#` headings are intentionally **not** supported — they would clash with identifier names and the `#` comment marker. Do **not** use Texinfo tags inside demo comments. ``` %!demo %! ## Subtract 30.92 from x to simulate a 3-parameter Weibull with %! ## `gamma = 30.92`, then plot the result. %! x = [46 64 83 105 123 150 150]; %! c = [0 0 0 0 0 0 1]; %! f = [1 1 1 1 1 1 4]; %! wblplot (x - 30.92, c, f, 0.05); ``` Consecutive statements that print nothing are merged into a single input box, so muted setup code reads as one block; a statement prints (and gets an output box) as soon as it is left unterminated by a semicolon or calls `disp`, `printf`, and the like. ## 4. BISTs (testing suite) It is **very important** that function files ship a built-in self-test (BIST) suite that checks for correct output and properly catches error conditions. BISTs go at the bottom of the file, using `%!` line prefixes. ``` %!test %! x = [1 2 1 3 2 4 3 2 4 3 2 2]; %! [h, p, stats] = chi2gof (x); %! assert_equal (h, 0); %! assert_equal (p, NaN); %! assert_equal (stats.chi2stat, 0.1205375022748029, 1e-14); %!error chi2gof () %!error chi2gof ([2, 3; 3, 4]) ``` A few points on layout and content: - **Put positive tests first, then a blank line, then the `%!error` tests.** - The package requires **Octave >= 11**, so use **`assert_equal`** for value comparisons — it gives clearer failure messages than the older `assert`. - In a `%!error` test, match the **full** error message; this line is exempt from the 80-column limit. Escape regex metacharacters such as parentheses (`\(`, `\)`). - **Only test errors that this function itself emits.** Do not re-test errors raised by core Octave functions — those are covered in core Octave. - Use `%!shared var1, var2` to declare fixtures shared across following tests. Append tests as you develop; it saves debugging time and catches marginal errors that would otherwise come back as bug reports. It is equally important to add tests when fixing a bug or extending an existing function. There is no such thing as too many tests. :metal: ## 5. Coding style The package has its own coding style. It is close in spirit to the conventions used in GNU Octave itself, but it is **not** identical — there are deliberate differences (Allman-style block terminators, a specific quoting policy, naming rules, and so on) that are spelled out below. Follow these rules for any new code, and when in doubt copy the layout of an existing neighbouring file. The overriding goal is consistency: a reader should not be able to tell which contributor wrote a given file. ### Layout and whitespace - Keep lines **within 80 columns**. Wrap long calls and strings with `...` continuation, aligning the continuation under the first argument. - Use `LF` (unix) line endings, **never** `CRLF` (windows). - **Indent with 2 spaces. Never use tabs** — anywhere, including compiled sources. - Use `##` for comments. Do **not** use `%` or `%%` as in MATLAB. (`%!` is reserved for BIST/demo blocks; see §3 and §4.) - When **calling or defining** a function, put a space after each comma and **before the opening parenthesis**. This is the single most visible Octave-vs-MATLAB tell — do not omit it: ``` x = max (sin (y + 3), 2); function out = foo (a, b) ``` - The exception is **matrix and cell constructors**, where a space before the paren would be parsed as a separator and split the element in two: ``` a = [sin(x), cos(x)]; b = {sin(x), cos(x)}; ``` - For an **indexing** expression, do **not** put a space after the identifier — this distinguishes indexing from a function call. The space after a comma is optional for simple indices but recommended for complex ones: ``` A(:,i,j) A([1:i-1; i+1:n]) ``` ### Operators and control flow - Use `!` for logical NOT (not `~`) and `!=` for not-equal (not `~=`): ``` a != 0; b(! isnan (a)) = []; ``` - Enclose `if`, `while`, `until`, and `switch` conditions in parentheses: ``` if (isvector (a)) s = sum (a); endif ``` Do **not** parenthesise the iteration counter of a `for` statement: ``` for ii = 1:numel (a) b(ii) = sum (a(:,ii)); endfor ``` - Always close a block with its **specific typed keyword** — `endif`, `endfor`, `endwhile`, `endswitch`, `endfunction`, `endclassdef`, `endproperties`, `endmethods` — never the generic `end`. (`end` still means the last index inside an indexing expression, e.g. `x(end)`.) ### Quoting - **Single quotes are the default** for every character vector and option string: `'Format'`, `'off'`, `anova2 (x, reps, 'off')`, `set (gca, 'XLim', xl)`. - **Double quotes** are used **only** for the string literals passed to the message functions `error`, `warning`, `printf`, `fprintf`, and `sprintf` — always, whether or not the string contains an escape (`\n`) or a format specifier (`%d`). Keeping these double-quoted lets apostrophes read naturally: ``` error ("anova1: X must be numeric."); % yes error ("Prediction must be 'curve' or 'observation'."); % yes error ('Prediction must be ''curve'' or ''observation''.'); % no ``` - **Cell arrays of character vectors** always use single quotes: `{'red', 'green', 'blue'}`, never `{"red", "green", "blue"}`. - In a `switch`/`case`, a single-char-vector label uses single quotes — `case 'first'`, and `case {'first', 'last'}` for several. ### Naming | Kind | Convention | Examples | |--------------------------|--------------|-----------------------------------| | Class / type | lowercase | `cvpartition`, `LinearModel`\* | | Public property | PascalCase | `Formula`, `NumObservations` | | Local variable | camelCase | `optNames`, `inputFormat` | | Loop index | `ii` (or `i`)| `for ii = 1:n` | | Object handle in methods | `this` | `function disp (this)` | | Private helper (`private/`) | `__name__.m` | `__disp__`, `__paramci__` | | Compiled oct-file (`src/`) | `__name__.cc`| `__editdist__.cc` | Double-underscore wrapping (`__foo__`) marks a function as **internal / not user-facing**. (\*Established statistics classes such as `LinearModel` keep their historical MATLAB-compatible casing; new lower-level types follow the lowercase rule.) ### Error messages Error messages follow the format **`": ."`**: - Prefix with the function name (`"anova1: ..."`) — or, inside a class method, `".: ..."` (e.g. `"LinearModel.predict: ..."`). - Start lower-case and **end with a period**. - Refer to arguments by their UPPERCASE documentation name (`"... X must be numeric."`). - Build long messages with `strcat (...)` across `...`-continued lines, aligned under the first fragment — **never** with `[...]` bracket concatenation: ``` error (strcat ("anova1: GROUP must be a vector of the same", ... " length as X.")); ``` - When forwarding a computed message, use `error ("scope: %s", errmsg)`. ### Function and file structure A function file is laid out in this order: 1. GPL header (§1). 2. Texinfo documentation block (§2). 3. The `function` line, then **input validation first**, under an `## Input validation` comment: guard `nargin` / `nargout`, then type- and shape-check each argument, erroring early. 4. The body, with `##` full-line comments marking the logical steps. 5. `endfunction`. 6. Any local helper functions. 7. The `%!demo` blocks (§3), then the `%!test` / `%!error` blocks (§4). ``` function p = anova1 (x, group, displayopt) ## Input validation if (nargin < 1) error ("anova1: too few input arguments."); endif ... ## Compute the group means ... endfunction ``` Default argument values may be set in the signature, e.g. `function [err, days] = hms2days (H, MI, S, MS = 0)`. ### classdef classes Inside a class file, order the sections as: the `classdef` line and class `@deftp` block; `properties` blocks (public first, then attributed blocks such as `properties (SetAccess = private, Hidden)`), each documented property carrying its own `@deftp` block; `methods (Hidden)` for `disp`/`display` and internals; one or more `methods (Access = public)` blocks grouped by theme; `methods (Access = private)`; `endclassdef`; and finally any local helper functions after `endclassdef`. See §2 for the `@deftp`-vs-`@deftypefn` documentation split. ### Compiled sources (`src/`) Oct-files follow the same spirit with a few C++ specifics: - GPL header in a `/* ... */` block (same text as §1). - `#include `; define functions with `DEFUN_DLD`. - **2-space indentation, no tabs**; opening brace of a free function on its own line. - `//` line comments; built via `src/Makefile`. ### Keeping metadata in sync When you add a user-facing function or class, update **`INDEX`** — add the new name under the right category heading (this drives the function index and the online docs). Do **not** touch `DESCRIPTION`: its `Version` and `Date` are bumped only at release time, by the maintainer. The package also ships a set of `doc-cache` files (one per function directory). These feed `lookfor` and let `pkg install` skip a slow regeneration step, so keep them current. Do this whenever you: - add a new function or class, or - change the docstring of a function, of a class, or of one of a class's methods or properties. Regeneration is done by the **`pkg-octave-doc`** package, version 0.8.0 or newer, which caches a class's documented members under their qualified names alongside the class itself. Build the oct-files first with `make -C src`: a compiled function's help text lives inside its `.oct` file, and a missing or stale one stops the run and asks for a build. ``` pkg load pkg-octave-doc # At the package root, for every cache below it: package_texi2cache ('-auto') # only what git reports as changed (seconds) package_texi2cache () # the whole package (slow, ~minutes) package_texi2cache ('-check') # writes nothing, reports what would change # Standing in one function directory, e.g. inst/Distribution_Functions: folder_texi2cache ('-auto') # that directory's cache alone # Standing in the directory that holds the file, for a single name: function_texi2cache ('anova1') classdef_texi2cache ('prob.NormalDistribution') ``` `-auto` is the fast path and takes the work from `git`: everything differing from `HEAD`, staged and unstaged, plus untracked files, so a function written but not yet committed still counts. `-check` writes nothing and reports what would change, which is how a tree is tested for a stale cache; since every docstring is linted as it is parsed, it is also how the package is checked for broken texinfo without touching anything. Naming a class caches the class and all of its documented members together, so a renamed or newly documented method needs the class named once and no more. **`INDEX` decides what is cached** at the package and directory scopes: only the names it lists are written, so add yours there first. The two per-name functions cache whatever you name and only warn when it is unlisted. --- **That's about it!** Keep more or less consistent with the other function files in the package and you should be fine. :smile: If anything here is unclear, open an issue or ask in your pull request — we are happy to help. statistics-release-2.1.2/COPYING000066400000000000000000001045121527311104300163570ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read .statistics-release-2.1.2/DESCRIPTION000066400000000000000000000006041527311104300170270ustar00rootroot00000000000000Name: statistics Version: 2.1.2 Date: 2026-11-05 Author: various authors Maintainer: Andreas Bertsatos Title: Statistics Description: The Statistics package for GNU Octave. Categories: Statistics Depends: octave (>= 11.1.0), datatypes(>=1.5.0) License: GPLv3+ Url: https://github.com/gnu-octave/statistics Tracker: https://github.com/gnu-octave/statistics/issues statistics-release-2.1.2/INDEX000066400000000000000000000201301527311104300161070ustar00rootroot00000000000000statistics >> Statistics Descriptive Statistics cdfcalc cl_multinom cohenKappa corr dcov ecdf fleissKappa geomean grpstats harmmean jackknife ksdensity limitsOfAgreement mvksdensity nancov nanmax nanmean nanmedian nanmin nanstd nansum nanvar partialcorr partialcorri tabulate trimmean Data Manipulation combnk crosstab datasample dummyvar fillmissing grp2idx ismissing isoutlier multiway normalise_distribution randsample rmmissing standardizeMissing tiedrank Hypothesis Testing adtest anova anova1 anova2 anovan ansaribradley barttest binotest chi2gof chi2test correlation_test detectdrift dwtest fishertest friedman hotelling_t2test hotelling_t2test2 jbtest knntest kruskalwallis kstest kstest2 lillietest manova1 mcnemar_test meanEffectSize mmdtest multcompare ranksum regression_ftest regression_ttest rjtest runstest signrank signtest stats.drift.DriftDiagnostics swtest ttest ttest2 vartest vartest2 vartestn ztest ztest2 Experimental Design ff2n fullfact parseWilkinsonFormula sampsizepwr sigma_pts x2fx Regression CompactLinearModel CoxModel coxphfit fitcox fitglm fitglme fitlm fitlme fitlmematrix fitmnr fitnlm fitrm GeneralizedLinearMixedModel GeneralizedLinearModel glmfit glmval invpred lasso lassoglm LinearFormula LinearMixedModel LinearModel logistic_regression mnrfit mnrval monotone_smooth MultinomialRegression mvregress mvregresslike nlinfit nlparci nlpredci NonLinearModel plsregress regress regress_gp RepeatedMeasuresModel ridge robustfit stepwisefit stepwiseglm stepwiselm Machine Learning ClassificationBaggedEnsemble ClassificationDiscriminant ClassificationECOC ClassificationEnsemble ClassificationGAM ClassificationKernel ClassificationKNN ClassificationLinear ClassificationNaiveBayes ClassificationNeuralNetwork ClassificationPartitionedECOC ClassificationPartitionedEnsemble ClassificationPartitionedKernel ClassificationPartitionedLinear ClassificationPartitionedModel ClassificationSVM ClassificationTree CompactClassificationDiscriminant CompactClassificationECOC CompactClassificationEnsemble CompactClassificationGAM CompactClassificationNaiveBayes CompactClassificationNeuralNetwork CompactClassificationSVM CompactClassificationTree CompactRegressionGAM CompactRegressionEnsemble CompactRegressionGP CompactRegressionNeuralNetwork CompactRegressionSVM CompactRegressionTree CompactTreeBagger designecoc fcnnpredict fcnntrain fitcdiscr fitcecoc fitcensemble fitcgam fitckernel fitcknn fitclinear fitcnb fitcnet fitcsvm fitctree fitrensemble fitrgam fitrgp fitrkernel fitrlinear fitrnet fitrsvm fitrtree fitSVMPosterior fscchi2 fsrftest gamboostinter gamboostpairs gamboostpredict gamboosttrain gampredict gamtrain partialDependence plotPartialDependence PredictiveModel RegressionBaggedEnsemble RegressionEnsemble RegressionGAM RegressionGP RegressionKernel RegressionLinear RegressionNeuralNetwork RegressionPartitionedEnsemble RegressionPartitionedKernel RegressionPartitionedLinear RegressionPartitionedModel RegressionSVM RegressionTree lime shapley svmpredict svmtrain templateDiscriminant templateEnsemble templateKernel templateKNN templateLinear templateNaiveBayes templateSVM templateTree TreeBagger treepredict treetrain Clustering CalinskiHarabaszEvaluation cluster ClusterCriterion clusterdata cophenet DaviesBouldinEvaluation dbscan evalclusters fitgmdist GapEvaluation gmdistribution inconsistent kmeans kmedoids linkage optimalleaforder SilhouetteEvaluation spectralcluster Nearest Neighbors createns editDistance ExhaustiveSearcher hnswSearcher KDTreeSearcher knnsearch mahal nomdist nomdist2 pdist pdist2 rangesearch squareform Anomaly Detection iforest IsolationForest LocalOutlierFactor lof ocsvm OneClassSVM robustcov Dimensionality Reduction canoncorr cmdscale factoran mdscale nnmf pca pcacov pcares ppca princomp procrustes ReconstructionICA relieff rica rotatefactors sequentialfs sparsefilt SparseFiltering tsne Model Evaluation confusionchart confusionmat ConfusionMatrixChart crossval cvpartition perfcurve rocmetrics Markov Models hmmdecode hmmestimate hmmgenerate hmmtrain hmmviterbi Random Sampling johnsrnd mhsample pearsrnd qrandn slicesample Distribution Classes paretotails prob.ProbabilityDistribution prob.BetaDistribution prob.BinomialDistribution prob.BirnbaumSaundersDistribution prob.BurrDistribution prob.ExponentialDistribution prob.ExtremeValueDistribution prob.GammaDistribution prob.GeneralizedExtremeValueDistribution prob.GeneralizedParetoDistribution prob.HalfNormalDistribution prob.InverseGaussianDistribution prob.KernelDistribution prob.LogisticDistribution prob.LoglogisticDistribution prob.LognormalDistribution prob.LoguniformDistribution prob.MultinomialDistribution prob.NakagamiDistribution prob.NegativeBinomialDistribution prob.NormalDistribution prob.PiecewiseLinearDistribution prob.PoissonDistribution prob.RayleighDistribution prob.RicianDistribution prob.StableDistribution prob.tLocationScaleDistribution prob.TriangularDistribution prob.UniformDistribution prob.WeibullDistribution Distribution Fitting betafit betalike binofit binolike bisafit bisalike burrfit burrlike copulafit evfit evlike expfit explike gamfit gamlike geofit gevfit gevlike gevfit_lmom gpfit gplike gumbelfit gumbellike hnfit hnlike invgfit invglike logifit logilike loglfit logllike lognfit lognlike nakafit nakalike nbinfit nbinlike normfit normlike poissfit poisslike raylfit rayllike ricefit ricelike stblfit stbllike tlsfit tlslike unidfit unifit wblfit wbllike Distribution Functions betacdf betainv betapdf betarnd binocdf binoinv binopdf binornd bisacdf bisainv bisapdf bisarnd burrcdf burrinv burrpdf burrrnd bvncdf bvtcdf cauchycdf cauchyinv cauchypdf cauchyrnd chi2cdf chi2inv chi2pdf chi2rnd copulacdf copulapdf copularnd evcdf evinv evpdf evrnd expcdf expinv exppdf exprnd fcdf finv fpdf frnd gamcdf gaminv gampdf gamrnd geocdf geoinv geopdf geornd gevcdf gevinv gevpdf gevrnd gpcdf gpinv gppdf gprnd gumbelcdf gumbelinv gumbelpdf gumbelrnd hncdf hninv hnpdf hnrnd hygecdf hygeinv hygepdf hygernd invgcdf invginv invgpdf invgrnd iwishpdf iwishrnd jsucdf jsupdf laplacecdf laplaceinv laplacepdf laplacernd logicdf logiinv logipdf logirnd loglcdf loglinv loglpdf loglrnd logncdf logninv lognpdf lognrnd mnpdf mnrnd mvncdf mvnpdf mvnrnd mvtcdf mvtpdf mvtrnd nakacdf nakainv nakapdf nakarnd nbincdf nbininv nbinpdf nbinrnd ncfcdf ncfinv ncfpdf ncfrnd nctcdf nctinv nctpdf nctrnd ncx2cdf ncx2inv ncx2pdf ncx2rnd normcdf norminv normpdf normrnd plcdf plinv plpdf plrnd poisscdf poissinv poisspdf poissrnd raylcdf raylinv raylpdf raylrnd ricecdf riceinv ricepdf ricernd stblcdf stblinv stblpdf stblrnd stdrcdf stdrinv stdrpdf stdrrnd tcdf tinv tpdf trnd tlscdf tlsinv tlspdf tlsrnd tricdf triinv tripdf trirnd unidcdf unidinv unidpdf unidrnd unifcdf unifinv unifpdf unifrnd vmcdf vminv vmpdf vmrnd wblcdf wblinv wblpdf wblrnd wienrnd wishpdf wishrnd Distribution Statistics betastat binostat bisastat burrstat chi2stat copulaparam copulastat evstat expstat fstat gamstat geostat gevstat gpstat gumbelstat hnstat hygestat invgstat logistat loglstat lognstat nakastat nbinstat ncfstat nctstat ncx2stat normstat plstat poisstat raylstat ricestat stdrstat tlsstat tristat tstat unidstat unifstat wblstat Distribution Wrappers cdf fitdist icdf makedist mle mlecov pdf random Plotting andrewsplot bar3 bar3h biplot blandAltmanPlot boxchart boxplot cdfplot dendrogram ecdfhist einstein gardnerAltmanPlot glyphplot gplotmatrix gscatter heatmap hist3 histfit manovacluster normplot parallelcoords parallelplot ppplot probplot qqplot scatterhist scatterhistogram silhouette stats.chart.BoxChart stats.chart.HeatmapChart stats.chart.ParallelCoordinatesPlot stats.chart.ScatterHistogramChart swarmchart violin wblplot I/O libsvmread libsvmwrite loadmodel Utilities cholcov logit makima probit statget statset statistics-release-2.1.2/NEWS000066400000000000000000000361261527311104300160300ustar00rootroot00000000000000 Summary of important user-visible changes for statistics 2.1.0: ------------------------------------------------------------------- Backwards incompatible improvements: ==================================== ** createns: with no 'NSMethod', builds a KDTreeSearcher for data with at most 10 columns, not sparse, under the euclidean, cityblock, chebychev or minkowski distance, as MATLAB does, where it always built an ExhaustiveSearcher. ** mnrfit: the raw and Pearson residuals of ordinal and hierarchical models have K-1 columns, as in MATLAB, where they had one per category; the 'display' option is removed, MATLAB having none. New functions and methods: ========================== ** fitmnr, MultinomialRegression: multinomial regression of nominal, ordinal and hierarchical responses from a matrix, a table or a formula, with predict, feval, random, coefCI, coefTest, testDeviance, plotResiduals, plotSlice and partial dependence, as in MATLAB. Bug fixes: ========== ** cmdscale: eigenvalues that are numerically zero no longer add columns of noise to Y; the cutoff follows the precision of D, as in MATLAB (#530). ** ExhaustiveSearcher, hnswSearcher, KDTreeSearcher: a 'P' without the minkowski distance, and a 'Scale' or 'Cov' without its distance, raise an error, as in MATLAB, where they were ignored. ** fitcknn, ClassificationKNN: 'Exponent' takes any positive scalar, where one that was not an integer raised an error. ** knnsearch, rangesearch, createns, fitcknn, ExhaustiveSearcher, hnswSearcher, KDTreeSearcher: a Minkowski exponent of Inf gives the Chebychev distance on every search method, where it raised an error and a fitcknn model fitted with it failed in predict (#533). ** knnsearch, rangesearch: the kd-tree search lost a row whose NaN fell on a split, so knnsearch failed once K reached it, measured a row holding a NaN by its other coordinates under the Chebychev distance, and skipped rows tied at the worst distance when the query and a split were the same infinity; 'IncludeTies' returned nothing when the K-th distance was NaN; a 'P' without the minkowski distance raises an error, where it was ignored. ** mnrfit: a character matrix response takes each row as one label (#529); a matrix response row holding NaN leaves its observation out, and one holding several 1s counts that many observations, as in MATLAB; an observation left out keeps a NaN row in the residuals; a single category gives an empty fit, as in MATLAB, where it raised an error. ** pdist, pdist2: a Minkowski exponent of Inf gives the Chebychev distance, where every distance was 1 (#532); the Chebychev distance is NaN for a pair where either row holds a NaN, as in MATLAB, and where a coordinate difference is Inf - Inf, as for every other metric (MATLAB passes over it). ** ridge: a predictor with no variation is taken as a constant term, where every coefficient was NaN (#531); fewer than two rows left after removing NaN raise an error. Improvements: ============= ** mnrfit: string and categorical responses are accepted, and responses may be row vectors (#529); a matrix response holds counts, one column per category, with new options 'interactions', 'weights', 'iterationlimit' and 'tolerance', as in MATLAB. Summary of important user-visible changes for statistics 2.1.1: ------------------------------------------------------------------- Important Notice: lassoglm divides its loss by the total number of binomial trials, as MATLAB does, so a fit with more than one trial per row gives different coefficients at a given Lambda. Backwards incompatible improvements: ==================================== ** anovan: stats.varnames is a column, as in MATLAB, where it kept the shape the names were given in and the default names were a row. ** friedman: KendallsWCI bounds the concordance beyond chance, which is 0 where the columns do not differ, where it bounded the expected value of Kendall's W and could exclude W itself; KendallsW and the bootstrap interval are unchanged. ** lasso, lassoglm: 'Weights' has one element per row of X, as given; a vector already shortened to the rows without NaN, and integer, logical, complex or infinite weights, raise an error (#534). ** lassoglm: the loss is divided by the total number of binomial trials, as in MATLAB, so a fit with more than one trial per row gives different coefficients at a given Lambda; 'Size' is renamed 'BinomialSize', as in fitglm, and 'Size' stays as an alias. New functions and methods: ========================== ** cohenKappa, fleissKappa: agreement between two raters, unweighted or weighted, with a confidence interval and a test, and among several raters. MATLAB has neither. ** fitSVMPosterior, fitPosterior: fit the transformation from a two-class SVM's scores to posterior probabilities, a sigmoid, a step function for separated classes or a constant, as in MATLAB; predict then returns posterior probabilities. ** limitsOfAgreement, blandAltmanPlot: the Bland-Altman limits of agreement between two measurement methods, as numbers and as a plot. MATLAB has neither. ** relieff: ranks predictors by importance with ReliefF for classification and RReliefF for regression, as in MATLAB. ** rjtest: the Ryan-Joiner test of normality, as in MATLAB R2026b, with p-values from Ryan and Joiner's approximation or by Monte Carlo simulation. ** sequentialfs: selects predictors by forward or backward sequential search on a cross-validated criterion, as in MATLAB; statset ('sequentialfs') gives its default options. Bug fixes: ========== ** fitcknn, fitcnb: a class with prior 0 could be predicted by fitcknn, its observations still counted as neighbours, and made fitcnb fail, saying its data had zero variance. ** fitgmdist: on convergence, returned the parameters of one step beyond those whose likelihood it reported, and a 'MaxIter' of 0 raised an error; it returns the parameters of the reported likelihood, as MATLAB does. ** glmfit: ran one iteration fewer than MATLAB for a given 'MaxIter', which changed the coefficients wherever the limit was reached; a 'MaxIter' of 0, returning the first weighted fit, is accepted. ** isoutlier: a 'ThresholdFactor' of 0 is accepted, as documented (#536); an integer factor gave thresholds rounded to integers and a single one single thresholds; a NaN, logical, text or complex factor raises an error, as in MATLAB, where it was taken. ** kmeans: stopped by 'MaxIter', returned centroids that were not the means of the returned clusters, and the 'final' and 'iter' displays counted one iteration too many. ** kmedoids: with 'Algorithm', 'pam', took one swap per iteration where MATLAB gives every medoid in turn its best swap, so it needed more iterations and labelled the clusters in another order; a 'MaxIter' of 0 returns the starting medoids, where it made one pass. ** lasso, lassoglm: a full weight vector raised an error whenever X or Y held NaN; the weights of dropped rows are dropped with them, and lassoglm takes a NaN weight as a missing observation, as MATLAB does (#534). ** lassoglm: hung when every response was at the edge of its range, all failures or all successes for 'binomial' and all zeros for 'poisson'; B is zero, as in MATLAB. A 'Size' vector of any length was taken, and the wrong number of trials used. ** nnmf, tsne: a 'MaxIter' of 0 raised an error; they return the starting point with its residual or loss. Improvements: ============= ** anovan: 'varnames' is taken as a character matrix with one name per row, as in MATLAB. ** crosstab: 'IncludeMissingGroups' counts missing values as a group of their own, as in MATLAB (#535); a single input gives a column of counts. ** fitcsvm, fitctree, fitcknn, fitcnb, fitcnet, fitcgam, fitcensemble, fitcecoc, fitclinear, fitckernel: a class with prior 0 is removed from the data before fitting, with a warning, as in MATLAB, and in fitcsvm, fitctree, fitclinear and fitckernel also one whose misclassification costs are all 0; NumObservations, RowsUsed and cross-validation count the rows kept. fitcdiscr keeps such a class, as MATLAB does. ** fitcsvm, fitcensemble, fitcecoc, fitclinear, fitckernel: 'Cost' is taken as a structure with fields ClassNames and ClassificationCosts, as in MATLAB and as the other classifiers already did. ** kmeans: 'Options' is taken as in MATLAB, where it was ignored with a warning; a 'MaxIter' of 0 returns the starting centroids. ** lasso, lassoglm: a negative or non-integer 'MaxIter' raises an error, where it was taken; a 'MaxIter' of 0 returns the starting point, every coefficient 0. ** lassoglm: a binomial Y is taken as a two-column matrix of successes and trials, a logical vector or a two-category categorical vector, as in MATLAB, and a logical or two-category categorical Y for 'normal' and 'poisson' too. ** statset: a 'TolFun', 'MaxIter' or 'MaxFunEvals' of 0 is accepted, as in MATLAB; a non-integer count raises an error. Summary of important user-visible changes for statistics 2.1.2: ------------------------------------------------------------------- Important Notice: a row missing only some predictors is now kept by fitcsvm, fitcnb, fitcecoc, fitcensemble and fitrensemble, as in MATLAB, so fits on data with missing values change. Backwards incompatible improvements: ==================================== ** dummyvar: a single grouping variable gives single dummy variables, as in MATLAB (#538). ** ismissing: a numeric or logical indicator applies to data of its own class, and a double on either side to every numeric and logical class, as in MATLAB; other pairings raise an error. ** nancov: input must be real floating point; integer and logical input raise an error, where integers failed or gave wrong values under 'pairwise'. ** nanmedian: the result takes the class of the input, as for median; single input returned double, and integer medians could saturate. ** nanvar, nanstd: input must be floating point, as for var and std; integer and logical input raise an error, where integers gave wrong values. New functions and methods: ========================== ** fscchi2: rank predictors for classification by chi-square tests of independence, from a matrix or a table. 'UseMissing' tests missing values as one more category or bin, as MATLAB documents; MATLAB itself ignores the option. ** fsrftest: rank predictors for regression by one-way F-tests, from a matrix or a table. 'UseMissing' tests missing values as one more category or bin, as MATLAB documents; MATLAB itself ignores the option. Bug fixes: ========== ** Classification and regression models: fitted from a table, an ordinal predictor is numeric in trees, ensembles and generalized additive models, as in MATLAB; empty text and missing strings are missing, not a category; 'Weights' may name a table variable. predict from a table takes a missing value and a level the model was not fitted on as missing, where it refused them. ** ClassificationDiscriminant, CompactClassificationDiscriminant: a query with a missing value has NaN scores and costs and takes the class of largest prior, as in MATLAB, where it had zero scores and the first class; margin returned a single NaN for it. loss, margin and edge of the compact class failed on numeric or logical labels. ** ClassificationEnsemble: under 'Subspace', a learner with no score for a query does not vote on it, and each learner fits its rows under the ensemble's weights, as in MATLAB, where a learner that dropped a row kept the ensemble's prior. ** ClassificationEnsemble, CompactClassificationEnsemble, ClassificationPartitionedEnsemble: loss, edge, kfoldLoss and kfoldEdge: a row no learner can score for its missing values counts as the class of greatest prior under 'classiferror', 'classifcost' and 'mincost' and makes any other loss and the edge NaN, as in MATLAB, where it was left out, so a loss could contradict predict. ** fitcdiscr: a row missing every predictor is dropped, as in MATLAB, where it was counted in NumObservations and the empirical prior, which changed the covariance and every posterior; a class left with no rows raises an error. ** fitcecoc, fitcensemble, fitrensemble: a row missing some predictors is kept, as in MATLAB, where it was dropped; a row is dropped only when its response or every predictor is missing or its weight is zero. ** fitcknn, fitctree, fitrtree: a row missing every predictor, and for trees one of weight zero, was counted in NumObservations, RowsUsed, X and the empirical prior; it is now dropped, as in MATLAB. ** fitcnb: a row missing some predictors is kept, each predictor fitted on the rows it is known for, where it was dropped; a row with a missing class is dropped, where it was a class of its own; X and Y hold the rows used, which fixes cross-validation after a row was dropped. Under 'mn', a row missing some counts counts in the prior only, and a query missing a count has NaN posterior and cost, as in MATLAB. ** fitcsvm: a row missing every predictor is dropped and the prior counts every row kept, as in MATLAB, where a fit with partly missing rows gave another model. ** hygeinv: P = 0 and P = 1 give the least and greatest possible count, a sample of size 0 gives 0, and scalar input with no marked items no longer fails. ** hygepdf, hygecdf: accurate to about 1e-15 as the population grows, where the relative error reached 1e-9 at a million. ** hygepdf, hygernd, hygestat: a sample of size 0 gives probability 1 at x = 0, random values 0, and mean and variance 0, as in MATLAB, where it gave NaN. ** ismissing: a logical indicator was refused for double data (#537). ** mvksdensity: a row with a NaN in X or in the weights is dropped, as in MATLAB, where every result was NaN or wrong; a single observation is valid (#542); weights with no positive value in the rows kept raise an error. ** nancov: single input gives a single result under 'pairwise', for empty input and with no complete rows, as in MATLAB (#541). ** nanmean: a sparse X with DIM past the last dimension no longer fails. ** tiedrank: TIEFLAG and BIDIR are read as a condition is, as in MATLAB, so [] keeps the default (#539); an empty TOL takes the default. ** trimmean: an empty X with DIM gives the right size and checks FLAG and DIM, and a VECDIM past the last dimension returns X (#540); integer, logical and char X is computed and returned in double, where 'weighted' rounded to the integer class, as MATLAB does. Improvements: ============= ** nansum: takes a char array, as sum does. statistics-release-2.1.2/ONEWS-1.1.x000066400000000000000000000051151527311104300167040ustar00rootroot00000000000000Summary of important user-visible changes for statistics 1.1.3: ------------------------------------------------------------------- ** The following functions are new in 1.1.3: copularnd mvtrnd ** The functions mnpdf and mnrnd are now also usable for greater numbers of categories for which the rows do not exactly sum to 1. Summary of important user-visible changes for statistics 1.1.2: ------------------------------------------------------------------- ** The following functions are new in 1.1.2: mnpdf mnrnd ** The package is now dependent on the io package (version 1.0.18 or later) since the functions that it depended of from miscellaneous package have been moved to io. ** The function `kmeans' now accepts the 'emptyaction' property with the 'singleton' value. This allows for the kmeans algorithm to handle empty cluster better. It also throws an error if the user does not request an empty cluster handling, and there is an empty cluster. Plus, the returned items are now a closer match to Matlab. Summary of important user-visible changes for statistics 1.1.1: ------------------------------------------------------------------- ** The following functions are new in 1.1.1: monotone_smooth kmeans jackknife ** Bug fixes on the functions: normalise_distribution combnk repanova ** The following functions were removed since equivalents are now part of GNU octave core: zscore ** boxplot.m now returns a structure with handles to the plot elemenets. Summary of important user-visible changes for statistics 1.1.0: ------------------------------------------------------------------- ** IMPORTANT note about `fstat' shadowing core library function: GNU octave's 3.2 release added a new function `fstat' to return information of a file. Statistics' `fstat' computes F mean and variance. Since MatLab's `fstat' is the equivalent to statistics' `fstat' (not to core's `fstat'), and to avoid problems with the statistics package, `fstat' has been deprecated in octave 3.4 and will be removed in Octave 3.8. In the mean time, please ignore this warning when installing the package. ** The following functions are new in 1.1.0: normalise_distribution repanova combnk ** The following functions were removed since equivalents are now part of GNU octave core: prctile ** The __tbl_delim__ function is now private. ** The function `boxplot' now accepts named arguments. ** Bug fixes on the functions: harmmean nanmax nanmin regress ** Small improvements on help text. statistics-release-2.1.2/ONEWS-1.2.x000066400000000000000000000047501527311104300167110ustar00rootroot00000000000000Summary of important user-visible changes for statistics 1.2.4: ------------------------------------------------------------------- ** Made princomp work with nargout < 2. ** Renamed dendogram to dendrogram. ** Added isempty check to kmeans. ** Transposed output of hist3. ** Converted calculation in hmmviterbi to log space. ** Bug fixes for stepwisefit wishrnd. ** Rewrite of cmdscale for improved compatibility. ** Fix in squareform for improved compatibility. ** New cvpartition class, with methods: display repartition test training ** New sample data file fisheriris.txt for tests ** The following functions are new: cdf crossval dcov pdist2 qrandn randsample signtest ttest ttest2 vartest vartest2 ztest Summary of important user-visible changes for statistics 1.2.3: ------------------------------------------------------------------- ** Made sure that output of nanstd is real. ** Fixed second output of nanmax and nanmin. ** Corrected handle for outliers in boxplot. ** Bug fix and enhanced functionality for mvnrnd. ** The following functions are new: wishrnd iwishrnd wishpdf iwishpdf cmdscale Summary of important user-visible changes for statistics 1.2.2: ------------------------------------------------------------------- ** Fixed documentation of dendogram and hist3 to work with TexInfo 5. Summary of important user-visible changes for statistics 1.2.1: ------------------------------------------------------------------- ** The following functions are new: pcares pcacov runstest stepwisefit hist3 ** dendogram now returns the leaf node numbers and order that the nodes were displayed in. ** New faster implementation of princomp. Summary of important user-visible changes for statistics 1.2.0: ------------------------------------------------------------------- ** The following functions are new: regress_gp dendogram plsregress ** New functions for the generalized extreme value (GEV) distribution: gevcdf gevfit gevfit_lmom gevinv gevlike gevpdf gevrnd gevstat ** The interface of the following functions has been modified: mvnrnd ** `kmeans' has been fixed to deal with clusters that contain only one element. ** `normplot' has been fixed to avoid use of functions that have been removed from Octave core. Also, the plot produced should now display some aesthetic elements and appropriate legends. ** The help text of `mvtrnd' has been improved. ** Package is no longer autoloaded. statistics-release-2.1.2/ONEWS-1.3.0000066400000000000000000000012641527311104300165770ustar00rootroot00000000000000Summary of important user-visible changes for statistics 1.3.0: ------------------------------------------------------------------- ** The following functions are new: bbscdf bbsinv bbspdf bbsrnd binotest burrcdf burrinv burrpdf burrrnd gpcdf gpinv gppdf gprnd grp2idx mahal mvtpdf nakacdf nakainv nakapdf nakarnd pdf tricdf triinv tripdf trirnd violin ** Other functions that have been changed for smaller bugfixes, increased Matlab compatibility, or performance: betastat binostat cdf combnk gevfit hist3 kmeans linkage randsample squareform ttest statistics-release-2.1.2/ONEWS-1.4.x000066400000000000000000000141701527311104300167100ustar00rootroot00000000000000Summary of important user-visible changes for statistics 1.4.3: ------------------------------------------------------------------- New functions: ============== ** anova1 (patch #10127) kruskalwallis ** cluster (patch #10009) ** clusterdata (patch #10012) ** confusionchart (patch #9985) ** confusionmat (patch #9971) ** cophenet (patch #10040) ** datasample (patch #10050) ** evalclusters (patch #10052) ** expfit (patch #10092) explike ** gscatter (patch #10043) ** ismissing (patch #10102) ** inconsistent (patch #10008) ** mhsample.m (patch #10016) ** ncx2pdf (patch #9711) ** optimalleaforder.m (patch #10034) ** pca (patch #10104) ** rmmissing (patch #10102) ** silhouette (patch #9743) ** slicesample (patch #10019) ** wblplot (patch #8579) Improvements: ============= ** anovan.m: use double instead of toascii (bug #60514) ** binocdf: new option "upper" (bug #43721) ** boxplot: better Matlab compatibility; several Matlab-compatible plot options added (OutlierTags, Sample_IDs, BoxWidth, Widths, BoxStyle, Positions, Labels, Colors) and an Octave-specific one (CapWidhts); demos added; texinfo improved (patch #9930) ** auto MPG (carbig) sample dataset added from https://archive.ics.uci.edu/ml/datasets/Auto+MPG (patch #10045) ** crosstab.m: make n-dimensional (patch #10014) ** dendrogram.m: many improvements (patch #10036) ** fitgmdist.m: fix typo in ComponentProportion (bug #59386) ** gevfit: change orientation of results for Matlab compatibility (bug #47369) ** hygepdf: avoid overflow for certain inputs (bug #35827) ** kmeans: efficiency and compatibility tweaks (patch #10042) ** pdist: option for squared Euclidean distance (patch #10051) ** stepwisefit.m: give another option to select predictors (patch #8584) ** tricdf, triinv: fixes (bug #60113) Summary of important user-visible changes for statistics 1.4.2: ------------------------------------------------------------------- ** canoncorr: allow more variables than observations ** fitgmdist: return fitgmdist parameters (Bug #57917) ** gamfit: invert parameter per docs (Bug #57849) ** geoXXX: update docs 'number of failures (X-1)' => 'number of failures (X)' (Bug #57606) ** kolmogorov_smirnov_test.m: update function handle usage from octave6+ (Bug #57351) ** linkage.m: fix octave6+ parse error (Bug #57348) ** unifrnd: changed unifrnd(a,a) to return a 0 rather than NaN (Bug #56342) ** updates for usage of depreciated octave functions Summary of important user-visible changes for statistics 1.4.1: ------------------------------------------------------------------- ** update install scripts for octave 5.0 depreciated functions ** bug fixes to the following functions: pdist2.m: use max in distEucSq (Bug #50377) normpdf: use eps tolerance in tests (Bug #51963) fitgmdist: fix an output bug in fitgmdist t_test: Set tolerance on t_test BISTS (Bug #54557) gpXXXXX: change order of inputs to match matlab (Bug #54009) bartlett_test: df = k-1 (Bug #45894) gppdf: apply scale factor (Bug #54009) gmdistribution: updates for bug #54278, ##54279 wishrnd: Bug #55860 Summary of important user-visible changes for statistics 1.4.0: ------------------------------------------------------------------- ** The following functions are new: canoncorr fitgmdist gmdistribution sigma_pts ** The following functions have been moved from the statistics package but are conditionally installed: mad ** The following functions have been moved from octave to be conditionally installed: BASE cloglog logit prctile probit qqplot table (renamed to crosstab) DISTRIBUTIONS betacdf betainv betapdf betarnd binocdf binoinv binopdf binornd cauchy_cdf cauchy_inv cauchy_pdf cauchy_rnd chi2cdf chi2inv chi2pdf chi2rnd expcdf expinv exppdf exprnd fcdf finv fpdf frnd gamcdf gaminv gampdf gamrnd geocdf geoinv geopdf geornd hygecdf hygeinv hygepdf hygernd kolmogorov_smirnov_cdf laplace_cdf laplace_inv laplace_pdf laplace_rnd logistic_cdf logistic_inv logistic_pdf logistic_rnd logncdf logninv lognpdf lognrnd nbincdf nbininv nbinpdf nbinrnd normcdf norminv normpdf normrnd poisscdf poissinv poisspdf poissrnd stdnormal_cdf stdnormal_inv stdnormal_pdf stdnormal_rnd tcdf tinv tpdf trnd unidcdf unidinv unidpdf unidrnd unifcdf unifinv unifpdf unifrnd wblcdf wblinv wblpdf wblrnd wienrnd MODELS logistic_regression TESTS anova bartlett_test chisquare_test_homogeneity chisquare_test_independence cor_test f_test_regression hotelling_test hotelling_test_2 kolmogorov_smirnov_test kolmogorov_smirnov_test_2 kruskal_wallis_test manova mcnemar_test prop_test_2 run_test sign_test t_test t_test_2 t_test_regression u_test var_test welch_test wilcoxon_test z_test z_test_2 ** Functions marked with known test failures: grp2idx: bug #51928 gevfir_lmom: bug #31070 ** Other functions that have been changed for smaller bugfixes, increased Matlab compatibility, or performance: dcov: returned dcov instead of dcor. added demo. violin: can be used with subplots. violin quality improved. princomp: Fix expected values of tsquare in unit tests fitgmdist: test number inputs to function hist3: fix removal of rows with NaN values ** added the packages test data to install statistics-release-2.1.2/ONEWS-1.5.x000066400000000000000000000300501527311104300167040ustar00rootroot00000000000000Summary of important user-visible changes for statistics 1.5.0: ------------------------------------------------------------------- Important Notice: 1) dependency change to Octave>=6.1.0 2) `mean` shadows core Octave's respective function 3) removed dependency on `io` package 4) incompatibility with the `nan` package New functions: ============== ** anova2 (fully Matlab compatible) ** bvncdf ** cdfcalc, cdfplot ** chi2gof (fully Matlab compatible. bug #46764) ** chi2test (bug #58838) ** cholcov (fully Matlab compatible) ** ecdf (fully Matlab compatible) ** evfit (fully Matlab compatible) ** evlike (fully Matlab compatible) ** fitlm (mostly Matlab compatible) ** fillmissing (patch #10102) ** friedman (fully Matlab compatible) ** grpstats (complementary to manova1) ** kruskalwallis (fully Matlab compatible) ** kstest (fully Matlab compatible) ** kstest2 (fully Matlab compatible. bug #56572) ** libsvmread, libsvmwrite (I/O functions for LIBSVM data files) ** manova1 (fully Matlab compatible) ** manovacluster (fully Matlab compatible) ** mean (fully Matlab compatible, it shadows mean from core Octave) ** multcompare (fully Matlab compatible) ** mvtcdfqmc ** ranksum (fully Matlab compatible. bug #42079) ** standardizeMissing (patch #10102) ** svmpredict, svmtrain (wrappers for LIBSVM 3.25) ** tiedrank (complementary to ranksum) ** x2fx (missing function: bug #48146) Improvements: ============= ** anova1: added extra feature for performing Welch's ANOVA (PR #15) ** anovan: mostly Matlab compatible, extra features. (patch #10123, PR #1-42) ** binopdf: implement high accuracy Loader algorithm for m>=10 (bug #34362) ** cdf: extended to include all available distributions ** crosstab: can handle char arrays, fixed ordering of groups ** gaminv: fixed accuracy for small 1st argument (bug #56453) ** geomean: fully Matlab compatible. (patch #59410) ** gevlike: fully Matlab compatible. ACOV output is the inverse Fisher Inf Mat ** grp2idx (fully Matlab compatible, indexes in order of appeearance) ** harmmean: fully Matlab compatible. ** hygepdf: added optional parameter "vectorexpand" to facilitate vectorization of other hyge functions. Allows different inputs lengths for x and t,m,n parameters, with broadcast expanded output (bug #34363) ** hygecdf: improved vectorization for non-scalar inputs. hygeinv hygernd ** ismissing: corrects handling of n-D arrays, NaN indicators, and improves matlab compatibility for different data types. (patch #10102) ** kmeans: improved help file, evaluate efficiency (bug #8959) ** laplace_cdf: allow for parameters mu and scale (bug #58688) laplace_inv laplace_pdf logistic_cdf logistic_inv logistic_pdf ** logistic_regression: fixed incorrect results (bug #60348) ** mvncdf: improved performance and accuracy (bug #44130) ** normplot: fixed ploting error (bug #62394), updated features ** pdf: extended to include all available distributions ** pdist: updated the 'cosine' metric to be more efficient (bug #62495) ** rmmissing: corrects cellstr array handling and improves matlab compatibility for different data types. (patch #10102) ** signtest: fix erroneous results, fully Matlab compatible (bug #49961) ** ttest2: can handle NaN values as missing data (bug #58697) ** violin: fix parsing color vector affecting Octave>=6.1.0 (bug #62805) ** wblplot: fixed coding style and help texinfo. (patch #8579) Removed Functions: ================== ** anova (replaced by anova1) ** caseread, casewrite (do not belong here) ** chisquare_test_homogeneity (replaced by chi2test) ** chisquare_test_independence (replaced by chi2test) ** kolmogorov_smirnov_test (replaced by kstest) ** kolmogorov_smirnov_test_2 (replaced by kstest2) ** kruskal_wallis_test (replaced by kruskalwallis) ** manova (replaced by manova1) ** repanova (replaced by anova2) ** sign_test (replaced by updated signtest) ** tblread, tblwrite (belong to `io` package when tables are implemented) ** t_test, t_test_2 (deprecated: use ttest & ttest2) ** wilcoxon_test (replaced by ranksum) Available Data Sets: ==================== ** acetylene Chemical reaction data with correlated predictors ** arrhythmia Cardiac arrhythmia data from the UCI machine learning repository ** carbig Measurements of cars, 1970–1982 ** carsmall Subset of carbig. Measurements of cars, 1970, 1976, 1982 ** cereal Breakfast cereal ingredients ** examgrades Exam grades on a scale of 0–100 ** fisheriris Fisher's 1936 iris data ** hald Heat of cement vs. mix of ingredients ** heart_scale.dat Used for SVM testing ** kmeansdata Four-dimensional clustered data ** mileage Mileage data for three car models from two factories ** morse Recognition of Morse code distinctions by non-coders ** popcorn Popcorn yield by popper type and brand ** stockreturns Simulated stock returns ** weather Daily high temperatures in the same month in two consecutive years Summary of important user-visible changes for statistics 1.5.1: ------------------------------------------------------------------- Important Notice: 1) `mean` shadows core Octave's respective function 2) incompatibility with the `nan` package New functions: ============== ** barttest (fully Matlab compatible) ** evcdf (fully Matlab compatible) ** evinv (fully Matlab compatible) ** evpdf (fully Matlab compatible) ** evrnd (fully Matlab compatible) ** evstat (fully Matlab compatible) ** gpfit (fully Matlab compatible) ** gplike (fully Matlab compatible) ** gpstat (fully Matlab compatible) ** levene_test (options for testtypes, handling NaNs and GROUPS like anova1) ** ncfcdf (fully Matlab compatible) ** ncfinv (fully Matlab compatible) ** ncfpdf (fully Matlab compatible) ** ncfrnd (fully Matlab compatible) ** ncfstat (fully Matlab compatible) ** nctcdf (fully Matlab compatible) ** nctinv (fully Matlab compatible) ** nctpdf (fully Matlab compatible) ** nctrnd (fully Matlab compatible) ** nctstat (fully Matlab compatible) ** ncx2cdf (fully Matlab compatible) ** ncx2inv (fully Matlab compatible) ** ncx2rnd (fully Matlab compatible) ** ncx2stat (fully Matlab compatible) ** normlike (fully Matlab compatible) ** sampsizepwr (fully Matlab compatible with extra functionality) ** vartestn (fully Matlab compatible) Improvements: ============= ** bartlett_test: improved functionality, hanlding NaNs and GROUPS like anova1 ** chi2cdf: added "upper" option and confidence bounds ** chi2test: improved functionality, handles multi-way tables ** crosstab: returns chi-square and p-value for multiway tables ** evfit: fixed bug that caused an error when x is a row vector ** fcdf: added "upper" option and confidence bounds ** gamcdf: added "upper" option and confidence bounds ** gpcdf: added "upper" option and confidence bounds ** mvnpdf: fixed MATLAB compatibility ** mvnrnd: fixed MATLAB compatibility ** ncx2pdf: reimplemented to be fully MATLAB compatible ** normcdf: added "upper" option and confidence bounds ** tcdf: added "upper" option ** vartest: fixed MATLAB compatibility ** vartest2: fixed MATLAB compatibility ** ztest: fixed MATLAB compatibility Removed Functions: ================== ** cloglog ** nanmean (replaced by mean) ** kolmogorov_smirnov_cdf (unused by new kstest, kstest2 functions) ** u_test (replaced by ranksum) ** var_test (replaced by vartest) ** z_test (replaced by ztest) Summary of important user-visible changes for statistics 1.5.2: ------------------------------------------------------------------- Important Notice: 1) `mean`, `median`, `std`, and `var` functions shadow core Octave's respective functions 2) incompatibility with the `nan` package New functions: ============== ** median (fully Matlab compatible) ** std (fully Matlab compatible) ** var (fully Matlab compatible) Improvements: ============= ** mean: fixed MATLAB compatibility ** multcompare: fixed erroneous results for Welch ANOVA, updated features ** tcdf: fixed erroneous results ** ttest: added support for NaN values and matrix inputs ** ttest2: added support for matrices and multiple t-tests Removed Functions: ================== ** nanmedian (replaced by median) ** nanstd (replaced by std) ** nanvar (replaced by var) Summary of important user-visible changes for statistics 1.5.3: ------------------------------------------------------------------- Important Notice: 1) `mean`, `median`, `std`, and `var` functions shadow core Octave's respective functions 2) incompatibility with the `nan` package New functions: ============== ** adtest (fully Matlab compatible) ** hotelling_t2test (new functionality, replacing old hotelling_test) ** hotelling_t2test2 (new functionality, replacing old hotelling_test_2) ** regression_ftest (new functionality, replacing old f_test_regression) ** regression_ttest (replacing old t_test_regression) ** vmcdf (von Mises cummulative distribution function) Improvements: ============= ** betacdf: added "upper" option ** binocdf: added "upper" option ** expcdf: added "upper" option and confidence bounds ** geocdf: added "upper" option ** gevcdf: added "upper" option ** hygecdf: added "upper" option ** laplace_cdf: updated functionality ** laplace_inv: updated functionality ** laplace_pdf: updated functionality ** laplace_rnd: updated functionality ** logistic_cdf: updated functionality ** logistic_inv: updated functionality ** logistic_pdf: updated functionality ** logistic_rnd: updated functionality ** logncdf: added "upper" option and confidence bounds ** mean: fixed MATLAB compatibility ** median: fixed MATLAB compatibility ** multcompare: print PostHoc Test table ** nbincdf: added "upper" option ** poisscdf: added "upper" option ** raylcdf: added "upper" option ** std: fixed MATLAB compatibility ** unidcdf: added "upper" option ** unifcdf: added "upper" option ** var: fixed MATLAB compatibility ** vmpdf: updated functionality ** vmrnd: updated functionality ** wblcdf: added "upper" option and confidence bounds Removed Functions: ================== ** anderson_darling_cdf (replaced by adtest) ** anderson_darling_test (replaced by adtest) ** hotelling_test (replaced by hotelling_t2test) ** hotelling_test_2 (replaced by hotelling_t2test2) ** f_test_regression (replaced by regression_ftest) ** t_test_regression (replaced by regression_ttest) Summary of important user-visible changes for statistics 1.5.4: ------------------------------------------------------------------- Important Notice: 1) `mean`, `median`, `std`, and `var` functions shadow core Octave's respective functions 2) incompatibility with the `nan` package New functions: ============== ** bvtcdf ** correlation_test (new functionality, replacing old cor_test) ** icdf (wrapper for all available *inv distribution functions) ** fishertest (fully Matlab compatible) ** procrustes (fully Matlab compatible) ** ztest2 (new functionality, replacing old prop_test_2) Improvements: ============= ** cdf: updated wrapper for all available *cdf distribution functions ** dcov: handles missing values and multivariate samples ** geomean: fixed MATLAB compatibility ** harmmean: fixed MATLAB compatibility ** mean: fixed MATLAB compatibility ** median: fixed MATLAB compatibility ** mvtcdf: improved speed, fixed Matlab compatibility ** pdf: updated wrapper for all available *pdf distribution functions ** random: updated wrapper for all available *rnd distribution functions ** regression_ttest: new functionality ** std: fixed MATLAB compatibility ** var: fixed MATLAB compatibility Removed Functions: ================== ** cor_test (replaced by correlation_test) ** prop_test_2 (replaced by ztest2) statistics-release-2.1.2/ONEWS-1.6.x000066400000000000000000000302701527311104300167110ustar00rootroot00000000000000 Summary of important user-visible changes for statistics 1.6.0: ------------------------------------------------------------------- Important Notice: 1) dependency changed to Octave>=7.2.0 2) various distribution functions have been renamed, deprecated, or modified extensively so that backwards compatibility is broken 3) `mad`, `mean`, `median`, `std`, `var` functions shadow core Octave's respective functions 4) incompatibility with the `nan` package New functions: ============== ** betafit (fully Matlab compatible) ** betalike (fully Matlab compatible) ** binofit (fully Matlab compatible) ** binolike (similar functionality to MATLAB's negloglik) ** bisafit (similar functionality to MATLAB's mle) ** bisalike (similar functionality to MATLAB's negloglik and mlecov) ** burrfit (similar functionality to MATLAB's mle) ** burrlike (similar functionality to MATLAB's negloglik and mlecov) ** einstein (plotting function for the einstein tile) ** geofit (similar functionality to MATLAB's mle) ** gumbelcdf ** gumbelfit (similar functionality to MATLAB's mle) ** gumbelinv ** gumbellike (similar functionality to MATLAB's negloglik and mlecov) ** gumbelpdf ** gumbelrnd ** hncdf ** hnfit (similar functionality to MATLAB's mle) ** hninv ** hnlike (similar functionality to MATLAB's negloglik and mlecov) ** hnpdf ** hnrnd ** invgcdf ** invgfit (similar functionality to MATLAB's mle) ** invginv ** invglike (similar functionality to MATLAB's negloglik and mlecov) ** invgpdf ** invgrnd ** isoutlier (fully Matlab compatible) ** logifit (similar functionality to MATLAB's mle) ** logilike (similar functionality to MATLAB's negloglik and mlecov) ** loglcdf ** loglfit (similar functionality to MATLAB's mle) ** loglinv ** logllike (similar functionality to MATLAB's negloglik and mlecov) ** loglpdf ** loglrnd ** lognfit (fully Matlab compatible) ** lognlike (fully Matlab compatible) ** nakafit (similar functionality to MATLAB's mle) ** nakalike (similar functionality to MATLAB's negloglik and mlecov) ** nbinfit (similar functionality to MATLAB's mle) ** nbinlike (similar functionality to MATLAB's negloglik and mlecov) ** mad (fully Matlab compatible) ** normfit (fully Matlab compatible) ** poissfit (fully Matlab compatible, extra functionality) ** poisslike (similar functionality to MATLAB's negloglik) ** raylfit (fully Matlab compatible, extra functionality) ** rayllike (similar functionality to MATLAB's negloglik) ** ridge (fully Matlab compatible) ** unidfit (similar functionality to MATLAB's mle) ** unifit (similar functionality to MATLAB's mle) ** vminv (quantile function for the von Mises distribution) ** wblfit (fully Matlab compatible) ** wbllike (fully Matlab compatible) Improvements: ============= ** bisacdf: supports "upper" option ** burrcdf: supports "upper" option ** cauchycdf: supports "upper" option ** cdf: update support for all univariate cumulative distribution functions ** gamfit: fixed MATLAB compatibility ** gamlike: fixed MATLAB compatibility ** icdf: update support for all univariate quantile functions ** laplacecdf: supports "upper" option ** logicdf: supports "upper" option ** mcnemar_test: updated functionality ** nakacdf: supports "upper" option ** pcacov: fixed MATLAB compatibility ** pdf: update support for all univariate probability density functions ** plsregress: fixed MATLAB compatibility ** random: update support for all univariate functions ** runstest: fixed MATLAB compatibility ** tabulate: fixed MATLAB compatibility ** tricdf: supports "upper" option ** trimmean: fixed MATLAB compatibility Removed Functions: ================== ** run_test (replaced by runstest) ** stdnormal_cdf (replaced by normcdf) ** stdnormal_inv (replaced by norminv) ** stdnormal_pdf (replaced by normpdf) ** stdnormal_rnd (replaced by normrnd) Renamed Functions: ================== ** bisacdf (replacing bbscdf) ** bisainv (replacing bbsinv) ** bisapdf (replacing bbspdf) ** bisarnd (replacing bbsrnd) ** cauchycdf (replacing cauchy_cdf) ** cauchyinv (replacing cauchy_inv) ** cauchypdf (replacing cauchy_pdf) ** cauchyrnd (replacing cauchy_rnd) ** laplacecdf (replacing laplace_cdf) ** laplaceinv (replacing laplace_inv) ** laplacepdf (replacing laplace_pdf) ** laplacernd (replacing laplace_rnd) ** logicdf (replacing logistic_cdf) ** logiinv (replacing logistic_inv) ** logipdf (replacing logistic_pdf) ** logirnd (replacing logistic_rnd) Summary of important user-visible changes for statistics 1.6.1: ------------------------------------------------------------------- Important Notice: 1) `mad`, `mean`, `median`, `std`, `var` functions shadow core Octave's respective functions prior to Octave v9.1 2) incompatibility with the `nan` package New functions: ============== ** ClassificationKNN (new classdef) ** predict (for ClassificationKNN classdef) ** fitcknn ** fitrgam ** knnsearch (fully Matlab compatible) ** mnrfit ** rangesearch (fully Matlab compatible) ** RegressionGAM (new classdef) ** predict (for RegressionGAM classdef) Improvements: ============= ** anovan: new features ** friedman: bug fixes ** pdist: updated functionality, fully MATLAB compatible ** pdist2: updated functionality, fully MATLAB compatible ** regress_gp: updated functionality with RBF kernel ** ridge: bug fixes Summary of important user-visible changes for statistics 1.6.2: ------------------------------------------------------------------- Important Notice: 1) `mad`, `mean`, `median`, `std`, `var` functions shadow core Octave's respective functions prior to Octave v9.1 2) incompatibility with the `nan` package New functions: ============== ** ricecdf ** riceinv ** ricepdf ** ricernd Improvements: ============= ** changed ClassificationKNN legacy class to classdef ** changed RegressionGAM legacy class to classdef ** update figure handling in BISTs, bug fix when calling 'pkg test statistics' Summary of important user-visible changes for statistics 1.6.3: ------------------------------------------------------------------- Important Notice: 1) `mad`, `mean`, `median`, `std`, `var` functions shadow core Octave's respective functions prior to Octave v9.1 2) incompatibility with the `nan` package New functions: ============== ** ricefit ** ricelike ** ricestat ** tlscdf ** tlsinv ** tlspdf ** tlsrnd ** tlsstat Improvements: ============= ** cdf: add support for Rician and location-scale T distributions ** gevlike: deprecate gradient as a second output argument (undocumented) ** icdf: add support for Rician and location-scale T distributions ** pdf: add support for Rician and location-scale T distributions ** random: add support for Rician and location-scale T distributions ** ricernd: fixed bug that produced erroneous results ** updated input validation and documentation to distribution *stat functions Summary of important user-visible changes for statistics 1.6.4: ------------------------------------------------------------------- Important Notice: 1) `mad`, `mean`, `median`, `std`, `var` functions shadow core Octave's respective functions prior to Octave v9.1 2) incompatibility with the `nan` package New functions: ============== ** bisastat ** burrstat ** fitdist ** glmfit (needs a lot of work yet) ** hnstat ** invgstat ** logistat ** loglstat ** makedist ** mle ** nakastat ** plcdf ** plinv ** plpdf ** plrnd ** plstat ** tlsfit ** tlslike ** tristat New classdefs: ============== ** BetaDistribution ** BinomialDistribution (methods on truncated distributions do not work yet) ** BirnbaumSaundersDistribution ** BurrDistribution ** ExponentialDistribution ** ExtremeValueDistribution ** GammaDistribution ** GeneralizedExtremeValueDistribution ** GeneralizedParetoDistribution ** HalfNormalDistribution ** InverseGaussianDistribution ** LogisticDistribution ** LoglogisticDistribution ** LognormalDistribution ** LoguniformDistribution ** MultinomialDistribution ** NakagamiDistribution ** NegativeBinomialDistribution (methods on truncated distributions do not work yet) ** NormalDistribution ** PiecewiseLinearDistribution ('mean', 'std', and 'var' methods fail for truncated distributions) ** PoissonDistribution (methods on truncated distributions do not work yet) ** RayleighDistribution ** RicianDistribution ** tLocationScaleDistribution ** TriangularDistribution ** UniformDistribution ** WeibullDistribution Improvements: ============= ** binolike: accepts frequency vector as third input argument ** gevfit: accepts frequency vector as third input argument ** gevlike: accepts frequency vector as third input argument ** gpfit: accepts frequency vector and requires fixed parameter 'theta' ** gplike: accepts frequency vector as third input argument ** logl* distribution functions: changed parameters for MATLAB compatibility ** hnfit: accepts frequency vector as third input argument ** hnlike: accepts frequency vector as third input argument ** nbinfit: accepts frequency vector as third input argument ** nbinlike: accepts frequency vector as third input argument ** tri* distribution functions: swapped the order of b and c input arguments ** unidfit: accepts frequency vector as third input argument ** unifit: accepts frequency vector as third input argument Summary of important user-visible changes for statistics 1.6.5: ------------------------------------------------------------------- Important Notice: 1) `mad`, `mean`, `median`, `std`, `var` functions shadow core Octave's respective functions prior to Octave v9.1 2) incompatibility with the `nan` package Improvements: ============= ** mad/median: correct mad handling of vectors with multiple Infs ** PiecewiseLinearDistribution: fix truncated `mean`, `std`, and `var` methods ** various fixes to avoid errors when testing the package functions Summary of important user-visible changes for statistics 1.6.6: ------------------------------------------------------------------- Important Notice: 1) `mad`, `mean`, `median`, `std`, `var` functions shadow core Octave's respective functions prior to Octave v9.1 2) incompatibility with the `nan` package New functions: ============== ** editDistance Improvements: ============= ** fillmissing: combining `movmedian` method and `missinglocation` option no longer ignores non-NaN values. ** gumbelinv: fix incorrect return value due to misplaced minus sign ** glmfit: fix input validation, parameter parsing, and update documentation ** histfit: updated support for other distributions, fixed MATLAB compatibility ** plot method for probability distribution objects partially implemented to allow plotting PDF and superimposing it over histograms of fitted data. Summary of important user-visible changes for statistics 1.6.7: ------------------------------------------------------------------- Important Notice: 1) `mad`, `mean`, `median`, `std`, `var` functions shadow core Octave's respective functions prior to Octave v9.1 2) incompatibility with the `nan` package New functions: ============== ** bar3 ** bar3h Improvements: ============= ** ClassificationKNN: bug fixes in computing Prior and when parsing explicit ClassNanes (github issue #148) ** nanmax: update functionality to support VECDIM and 'all' options, fix error when requesting a mutual comparison (savannah bug #65802) ** nanmin: update functionality to support VECDIM and 'all' options, fix error when requesting a mutual comparison (savannah bug #65802) statistics-release-2.1.2/ONEWS-1.7.x000066400000000000000000000177661527311104300167310ustar00rootroot00000000000000 Summary of important user-visible changes for statistics 1.7.0: ------------------------------------------------------------------- Important Notice: 1) `mad`, `mean`, `median`, `std`, `var` functions shadow core Octave's respective functions prior to Octave v9.1 2) incompatibility with the `nan` package New functions: ============== ** fcnnpredict ** fcnntrain ** fitcdiscr ** fitcgam ** fitcnet ** fitcsvm ** loadmodel ** signrank New classdefs: ============== ** ClassificationDiscriminant ** ClassificationGAM ** ClassificationNeuralNetwork ** ClassificationPartitionedModel ** ClassificationSVM ** CompactClassificationDiscriminant ** CompactClassificationGAM ** CompactClassificationNeuralNetwork ** CompactClassificationSVM Improvements: ============= ** BinomialDistribution: fix computations on truncated distribution methods (github issue #128) ** ClassificationKNN: new methods and various bug fixes ** finv: fix erratic output for very large DF2 (savannah bug #66034) ** fitcknn: support for cross-validation ** NegativeBinomialDistribution: fixed computations on truncated distribution methods (github issue #128) ** plot method for probability distribution objects updated to support 'cdf' and 'probability' PlotType options (github issue #129) ** PoissonDistribution: fixed computations on truncated distribution methods (github issue #128) Summary of important user-visible changes for statistics 1.7.1: ------------------------------------------------------------------- Important Notice: 1) `mad`, `mean`, `median`, `std`, `var` functions shadow core Octave's respective functions prior to Octave v9.1 2) incompatibility with the `nan` package Improvements: ============= ** ClassificationPartitionedModel: add input validation for cvpartition object (github issue #160) ** ClassificationNeuralNetwork: use named variable for timing training (github issue #169) ** kstest: add support for probability distribution objects, fix previous regression related to failing test (github issues #164, #165, #167), add more BISTs and a DEMO ** RegressionGAM: add iteration limit to the private fitGAM function to avoid infinite loop with Netlib BLAS/LAPACK (github issue #160) Summary of important user-visible changes for statistics 1.7.2: ------------------------------------------------------------------- Important Notice: 1) `mad`, `mean`, `median`, `std`, `var` functions shadow core Octave's respective functions prior to Octave v9.1 2) incompatibility with the `nan` package Improvements: ============= ** fcnnpredict, fcnntrain: various bug fixes, allow MacOS users to enable support for OpenMP (github issues #168, #171, #172) ** editDistance: use correct index types to avoid compiler warnings (github issue #171) Summary of important user-visible changes for statistics 1.7.3: ------------------------------------------------------------------- Important Notice: 1) `mad`, `mean`, `median`, `std`, `var` functions shadow core Octave's respective functions prior to Octave v9.1 2) incompatibility with the `nan` package New functions: ============== ** glmval Improvements: ============= ** glmfit: fixed output and updated functionality and compatibility Summary of important user-visible changes for statistics 1.7.4: ------------------------------------------------------------------- Important Notice: 1) `mad`, `mean`, `median`, `std`, `var` functions shadow core Octave's respective functions prior to Octave v9.1 2) incompatibility with the `nan` package Improvements: ============= ** ClassificationDiscriminant, ClassificationKNN, ClassificationNeuralNetwork: fix type of returning labels and ClassNames to be the same as input Y, add custom display and subsref/subsassgn methods ** confusionchart: fix empty figures in demos (github issue #178) ** ConfusionMatrixChart: properly update cdata in figure (github issue #178) ** fitdist: fix occasionally failing BISTs (github issue #174) ** kmeans: fix empty figures in demos (github issue #179) Summary of important user-visible changes for statistics 1.7.5: ------------------------------------------------------------------- Important Notice: 1) `mad`, `mean`, `median`, `std`, `var` functions shadow core Octave's respective functions prior to Octave v9.1 2) incompatibility with the `nan` package New functions: ============== ** createns ** multiway New classdefs: ============== ** cvpartition ** ExhaustiveSearcher ** hnswSearcher ** KDTreeSearcher Improvements: ============= ** chi2pdf, gampdf: fix INF handling (#203) ** crosstab: allow row vectors as input, fix numeric input ordering (#184) ** cvpartition: old style class has been replaced by classdef implementation. 'K-fold' partitioning is now randomized, and 'repartition' method works for both 'k-fold' and 'holdout' partition types. ** fitdist: fix NaN handling as missing values (#203) ** fpdf, finv: fix output when DoF tend to infinity (#203) ** fullfact: fix MATLAB compatibility (#212) ** grpstats: fix MATLAB compatibility (#215) ** knnsearch, rangesearch: fix slow kd-tree implementation (#151) ** vartest, vartest2: fix computation of right tail p-value (#183) ** x2fx: fix variable shadowing in nested loops (#204) ** ztest: fix second output for one-tailed tests (#199, #200) Summary of important user-visible changes for statistics 1.7.6: ------------------------------------------------------------------- Important Notice: 1) `mad`, `mean`, `median`, `std`, `var` functions shadow core Octave's respective functions prior to Octave v9.1 2) incompatibility with the `nan` package New functions: ============== ** factoran (#257) ** nanmean (#239) Improvements: ============= ** Update documentation add demos to all distrbution objects. Several minor bug fixes. ** ClassificationDiscriminant: improve documentation, allow changing 'Cost' and 'Prior' properties in existing objects ** ClassificationKNN: 'predict' can use either exhaustive or kdtree method ** crosstab: fix NaN input handling, improve efficiency (#214, #219) ** crossval: fix MATLAB compatibility (#217, #223, #229) ** cvpartition: fix MATLAB compatibility (#227) ** multiway: simplified function signature, improved efficiency (#258, #259) ** nansum: fix MATLAB compatibility to handle 'vecdim' and 'all' options ** pdist2: fix returning the smallest/largest K values for single output ** tcdf: improve speed and fix edge case in processing small DF (#232) ** ztest2: improve input validation (#226) Summary of important user-visible changes for statistics 1.7.7: ------------------------------------------------------------------- Important Notice: 1) `mad`, `mean`, `median`, `std`, `var` functions shadow core Octave's respective functions prior to Octave v9.1 2) incompatibility with the `nan` package Improvements: ============= ** Update documentation to all classification objects. Minor bug fixes (#263) ** Update documentation to ConfusionMatrixChart class (#285) ** cvpartition: fix error in stratified input vector (#268) fix handling of missing observations (#269) ** pca: fix erroneous Hotelling's T^2 statistics when applying weights (#279) statistics-release-2.1.2/ONEWS-1.8.x000066400000000000000000000140761527311104300167210ustar00rootroot00000000000000 Summary of important user-visible changes for statistics 1.8.0: ------------------------------------------------------------------- Important Notice: 1) Minimum Octave version required is 9.1.0 2) Add dependency to the datatypes package 3) Incompatibility with the `nan` package 4) Update 'libsvm' library to version 3.36 5) Using `tablicious` as a drop in replacement for `datatypes` might cause issues. Future development of statistical functions to enable support for tables and categorical arrays will be based on the implementations in `datatypes` New functions: ============== ** makima Improvements: ============= ** fillmissing: add support for the "makima" interpolation method ** svmpredict: add support for one class posterior probablity Removed functions: (available in core Octave) ================== ** mad ** mean ** median ** std ** var Summary of important user-visible changes for statistics 1.8.1: ------------------------------------------------------------------- Important Notice: 1) Soon after Octave 11.1 is officially released the 'statistics' package will update its dependency to Octave 11, so that further development can rely on advancements to core statistics and data functions that will be available with Octave 11. 2) Incompatibility with the `nan` package 3) Using `tablicious` as a drop in replacement for `datatypes` might cause issues. New functions: ============== ** dummyvar ** parseWilkinsonFormula Improvements: ============= All random generator distribution functions now accept empty size vectors as third input argument for MATLAB compatibility. ** confusionmat: add support for new data types ** crosstab: add support for new data types ** cvpartition: implement 'summary' method ** datasample: improve input validation ** fillmissing: char arrays do not have standard missing values ** friedman: return ANOVA results in a table ** grp2idx: fix MATLAB compatibility, add support for new data types ** grpstats: add support for tables and categorical arrays, add ploting functionality, fix remaining MATLAB compatiblity issue ** hnswSearcher: improve performance in building index and query operations ** ismissing: fix MATLAB compatibility, char arrays do not have standard missing values ** KDTreeSearcher: improve query speed ** knnsearch: improve input validation and avoid infinite recursion ** pcacov: improve input validation ** pdist, pdist2: improve performance, add fast euclidean algorithms ** rangesearch: improve input validation and avoid infinite recursion ** rmmissing: fix MATLAB compatibility, char arrays do not have standard missing values ** standardizeMissing: fix MATLAB compatibility, char arrays do not have standard missing values ** stepwisefit: fix MATLAB compatibility, replace legacy Draper-Smith algorithm, results may differ from previous implementation ** tabulate: add support for tables and categorical arrays ** wishrnd: allow for non-integral DF under certain constrains Summary of important user-visible changes for statistics 1.8.2: ------------------------------------------------------------------- Important Notice: 1) Update dependency to datatypes 1.2.0 2) Incompatibility with the `nan` package 3) Using `tablicious` as a drop in replacement for `datatypes` might cause issues. Improvements: ============= ** anova1: properly handle NaN values as group labels ** boxplot: add support for multi grouping variables ** grp2idx: improve integration with datatypes class objects ** makima: improve MATLAB compatibility ** parseWilkinsonFormula: add 'equations' mode ** sampsizepwr: fix computations for single-tailed distributions ** signtest: properly handle NaN values Summary of important user-visible changes for statistics 1.8.3: ------------------------------------------------------------------- Important Notice: 1) Minimum Octave version required is 11.1.0 2) Update dependency to datatypes 1.2.3 3) Incompatibility with the `nan` package 4) Using `tablicious` as a drop in replacement for `datatypes` might cause issues. Improvements: ============= ** ClusterCriterion: improve speed ** cmdscale: add support for second argument ** DaviesBouldinEvaluation: fix index ** harmmean, trimmean: improve MATLAB compatibility ** linkage: improve speed and fix unused variable ** pca, princomp: improve speed ** randsample: fix edge cases ** stepwisefit: improve speed ** squareform, tabulate: improve MATLAB compatibility ** RegressionGAM.predict: correct ySD computation using residual variance Summary of important user-visible changes for statistics 1.8.4: ------------------------------------------------------------------- Important Notice: 1) Update dependency to datatypes 1.2.6 2) Incompatibility with the `nan` package 3) Using `tablicious` as a drop in replacement for `datatypes` might cause issues. New functions: ============== ** LinearModel: object-oriented linear regression model class, returned by `fitlm` ** anova: object-oriented interface for analysis of variance, built on `anova1`, `anova2`, and `anovan` with a MATLAB-compatible object API Improvements: ============= ** anova1: add support for categorical arrays as grouping variables ** fitlm: now returns a LinearModel object; the previous `[TAB, STATS] = fitlm (...)` output form is no longer supported ** glmfit: fix a variable error (GitHub PR #440) ** parseWilkinsonFormula: fix power-term resolution from a base table column statistics-release-2.1.2/ONEWS-1.9.x000066400000000000000000003170341527311104300167220ustar00rootroot00000000000000 Summary of important user-visible changes for statistics 1.9.0: ------------------------------------------------------------------- Important Notice: 1) Update dependency to datatypes 1.3.3. 2) The 29 probability distribution classes have moved into the `prob` namespace. The flat class names are gone; this is a rename, not an alias. 3) The generalized additive model classes now fit boosted trees by default, as MATLAB does. Every fitted value, score and loss moves. The spline engine remains available as 'FitMethod', 'splines'. 4) `fitcnet` and `fitrnet` now default to the LBFGS solver, and `fitcnet` to rectified hidden layers and a softmax output. Every default fit returns different weights. 5) Models of the compact classes saved by an earlier version no longer load and must be re-saved from the model they were compacted from. 6) Incompatibility with the `nan` package. 7) Using `tablicious` as a drop in replacement for `datatypes` might cause issues. Breaking changes: ================= This release changes behaviour that working code may depend on. Each item below says what breaks and what to write instead. ** anova2: NaN values are refused again, as they always were and as MATLAB does. ** anovan: the returned table has nine columns where it had seven; 'Singular?' is inserted at column 4, moving 'Mean Sq.' to column 5, and 'Eta Sq.' and 'Part. Eta Sq.' are appended at 8 and 9. A model with any random factor keeps the interactions that factor appears in, so `p` and the table are longer and the table gains eight further columns; p-values change for every random effects model. An interaction is named 'X1:X2', and the random factor marker is no longer carried in the names returned in `atab`, `stats.varnames` or the expected mean square expressions. Reading the table by name from `atab(1,:)` is unaffected. ** boxplot: the quartiles are taken from `quantile` at its own default method, the one `prctile` and MATLAB use, rather than from core's `statistics`. Boxes, whisker fences, the returned statistics matrix and what counts as an outlier all move. ** ClassificationGAM, RegressionGAM: the classes fit boosted trees by default; pass 'FitMethod', 'splines' for the previous engine. Eleven spline options now raise unless that engine is selected, and a second output from `RegressionGAM.predict` raises under the default engine. The boosted classifier fits by local scoring, so every multi-predictor classification GAM fits to different numbers. A tree leaf now holds at least five observations. `Interactions` reports the K-by-2 matrix of fitted predictor pairs, not the request that was passed; use `IntMatrix` to recover the argument. `ScoreTransform` defaults to 'logit' and the raw score is the log-odds pair MATLAB reports. `RegressionGAM.fitBoosted` is no longer a public method. ** ClassificationKNN: `DistanceWeight` holds the name of the weight rather than a function handle, and now applies: nothing read it before, so 'inverse' and 'squaredinverse' changed no prediction. The documented spelling 'squaredinverse' is accepted. ** ClassificationSVM: `loss` returns different numbers, having scaled every loss by the class labels so that only the classification error was right. `margin` takes Y in the form the class labels are in, as documented, where it required the +1/-1 coding. `Alpha` is populated for every kernel and holds unsigned magnitudes, `Beta` is the primal coefficient vector for a linear kernel only, `SupportVectorLabels` is s-by-1 of +/-1, and `IsSupportVector` is logical. No classification result changes. ** cluster, clusterdata: 'MaxClust' yields exactly N clusters however the merge heights tie, and a 'Cutoff' on the inconsistency coefficient requires every node below to be under the cutoff. Both returned too few clusters. ** ClusterCriterion, CalinskiHarabaszEvaluation, DaviesBouldinEvaluation, GapEvaluation, SilhouetteEvaluation: the cluster evaluation classes are value classes, not handles, so `e2 = e1` copies; assign results back, as `e = addK (e, 4)`. `ClusterCriterion` is abstract and can no longer be constructed directly. ** CompactClassificationSVM: the class drops `IsSupportVector`, which was sized to the training set and which MATLAB's compact class does not carry. ** cvpartition: `test` and `training` return one column per requested set; given a vector of set indices they returned one column per set per pass. ** evalclusters: a matrix of clustering solutions with no `KList` is numbered by the clusters each column holds, not by column position, so criterion values and `OptimalK` change. `SilhouetteEvaluation.ClusterSilhouettes` holds the mean silhouette of each cluster rather than every observation's value. ** factoran: the function is rewritten and its output signature is now MATLAB's, `[lambda, psi, T, stats, F]`. The third output was the factor scores and is now the rotation matrix; scores moved to the fifth, so code taking the third output as scores silently receives an m-by-m matrix. The fit is maximum likelihood by default where it was principal axis factoring, which remains available as 'Extraction', 'paf'. ** fcnntrain, fcnnpredict: both take activation names in two arguments, `Activations` and `OutputLayerActivation`, rather than numeric codes in a single vector. `fcnnpredict` takes the layer weights and biases, not the model structure `fcnntrain` returns. The `Alpha` argument and field are removed. The 'elu' activation saturates at -1, as it is defined to, where its negative arm carried the leaky rectifier's 0.01 scale. ** fitcnet: 'Solver' defaults to 'lbfgs', MATLAB's solver, where it defaulted to 'sgd', so every default fit returns different weights. 'LearningRate' belongs to the epoch loop and now raises unless a solver is named, while 'GradientTolerance', 'LossTolerance' and 'StepTolerance' are now accepted by default; `TrainingHistory` and `ConvergenceInfo` carry the fields of the solver that ran. `fitrnet` is new in this release. `fitcnet` additionally defaults 'Activations' to 'relu', 'OutputLayerActivation' to 'softmax' and 'LearningRate' to 0.003. Initial weights are drawn from Octave's generator, so repeated fits no longer return an identical model and `rand ('seed', s)` governs a fit. `ConvergenceInfo` reports the value the fit ended at as a scalar and carries the series beside it as `History`. `ModelParameters` reports the fit rather than the network; read `Mdl.LayerWeights` and `Mdl.LayerBiases`. ** fitdist: the 'theta' option defaults to 0 for the generalized Pareto, as MATLAB assumes it, where it defaulted to 1; `mle` changes with it. ** fpdf: the density at x = 0 is 1 at df1 = 2 and Inf below it, where it was zero whatever the numerator degrees of freedom. ** friedman: the second output is a cell array with a header row, as MATLAB's is and as `anova1` and `anova2` already were; read `tbl{2,2}` rather than `tbl.SS(1)`. The table is displayed by default. ** gpfit: the second argument is `alpha`, the confidence level, not `theta`, a known location; `paramhat` is 1-by-2 and `paramci` 2-by-2. Fit a known location by shifting the data. Note `gpfit (x, 0.05)` was a fit at location 0.05 and is now a 95% confidence level. ** gplike: `params` is the two element vector [k, sigma] and `acov` is 2-by-2, where it was 3-by-3 with a zero row and column. ** hmmviterbi, hmmestimate: `hmmviterbi` returns the true maximum probability path, having scored a spurious transition out of the last state, and `hmmestimate` no longer counts a phantom transition out of an assumed initial state. Both return different values. ** LinearModel: a weighted fit's `LogLikelihood` carries the 0.5 * sum (log (w)) term and counts only nonzero weight observations, following R rather than MATLAB; `AIC`, `AICc`, `BIC` and `CAIC` move with it. Unweighted and robust fits are unchanged. ** LinearModel: `Formula` is a `LinearFormula` object rather than a structure or a character vector. Term order follows the variable order of the data rather than the alphabetical order of their names, and no longer depends on the order the formula names them, so coefficient names move with it. The omitted reference level of a character or string grouping column is the level the data presents first, not the alphabetically first one, so coefficients change name and meaning; a categorical column is unaffected. A model with no intercept and a categorical predictor gives the first categorical an indicator for every level by both routes, so its coefficients are the group means; this is a deliberate deviation from MATLAB, which drops the reference level. ** mle: the 'normal' and 'lognormal' distributions return the maximum likelihood estimate, `std (x, 1)`, where they returned the unbiased `std (x, 0)`. ** nbinfit: the confidence bounds are no longer clamped to the parameter space, a bound outside it being what tells the caller the normal approximation has broken down. ** negloglik: the distribution object method returns the negative log likelihood, as documented and as MATLAB returns it; it returned the log likelihood, the wrong sign. ** pca: `tsquared` is 0 where the fit has one degree of freedom or fewer, and NaN for dropped rows, returned at the full observation count. ** prob distribution objects: the 29 classes are now `prob.NormalDistribution` and so on; the flat names are gone, so `class (pd)` and `isa (pd, ...)` take the qualified name. `makedist`, `fitdist`, `cdf`, `pdf`, `icdf`, `random` and `mle` take the distribution's name and are unaffected. `DistributionName` holds the distribution's name ('Normal'), not the class name. `prob.WeibullDistribution` renames `lambda` and `k` to `A` and `B` and `prob.RayleighDistribution` renames `sigma` to `B`. `ParameterValues` is a cell for the piecewise linear and multinomial classes, `x` and `Fx` of the piecewise linear class are rows, and the 29 classes now share an abstract base, `prob.ProbabilityDistribution`, as MATLAB's do. ** proflik: the distribution object method computes a true profile likelihood, re- maximizing over the remaining parameters at each point, where it held them fixed at their fitted values and so gave wrong results for every multi-parameter distribution. The default grid is 101 values over the 98% confidence interval when the selected parameter is the only one estimated, and 21 otherwise. ** regress_gp: the prediction intervals were wrong in both branches and are now correct, and both branches return the lower bound in column 1; code reading Yint(:,1) as an upper bound must be changed. ** signrank: two differences agreeing to within eps (x) + eps (y) rank as tied and one smaller than that is dropped, as MATLAB does, so `signedrank`, `zval` and the p-value move on such data. `stats` carries `signedrank` where it carried `sign`, which belongs to `signtest`, and `zval` is empty for the exact test. ** signtest: the test decision `h` is returned as a logical, as documented and as MATLAB returns it, where it was a double. ** tabulate: the percentage column is count / total * 100 taken literally, so data with no observations gives NaN where it gave zero. ** The compact classes: `CompactClassificationSVM` drops `ModelParameters` and `IsSupportVector`, `CompactClassificationNeuralNetwork` drops six properties describing the fit, and `CompactClassificationGAM` drops seven, MATLAB's own compact classes carrying none of them. Read any of them off the model that was compacted from. A compact SVM, neural network or GAM model saved by an earlier version therefore no longer loads, and `loadmodel` raises 'invalid model'. The compact regression and Gaussian process classes are new in this release and never carried them. ** The distribution functions: the 130 *cdf, *pdf and *inv functions validate the class of their numeric arguments. Continuous distributions and every *inv accept double and single only; the discrete *cdf and *pdf additionally accept the integer types and promote to double, so the result is always a probability. Logical and character arrays are rejected. This fixes silently wrong answers: `geocdf (int32 (0:4), 0.3)` returned [0 1 1 1 1], `nbincdf` returned zeros and `binopdf` returned Inf. ** The random generators: the 39 generators and the distribution objects' `random` method treat a negative dimension as zero and return an empty array, as core Octave and MATLAB do, instead of raising. ** The supervised learner classes: `ScoreTransform` and `ResponseTransform` hold the name of the transform rather than a function handle, so `strcmp (Mdl.ScoreTransform, 'none')` works and `predict` applies the transform for you. `Prior` is a 1-by-K row in `ClassNames` order. `W` weighs each class by its prior rather than being uniform. An observation is dropped only for a missing response, so `size (X, 1)`, `numel (W)` and `NumObservations` now agree and `RowsUsed` is empty when nothing was dropped. The `Standardize` property is gone from eleven classes; test `! isempty (Mdl.Mu)` instead. `Cost` refuses anything that is not a square matrix. `BinEdges` is a cell on the cross-validated classifier, and a column cell on the GAM classes. `RowsUsed` is a logical mask on `ClassificationGAM`, `ClassificationSVM` and `RegressionGAM`, where it was a double. `edge` normalizes its 'Weights' within each class to that class's prior. The cross-validated models are read only apart from `Cost`, `Prior` and the transforms, either assignment now reaching every fold, and their transforms are applied where they used to be inert. `CrossValidatedModel` holds the learner's short name, not the class name. An observation that no fold held out gets a missing label with NaN scores, a deliberate deviation from MATLAB, which names the first class. ** tiedrank: an array is ranked along its first dimension, as MATLAB does, instead of being refused; `tieadj` carries one entry per column. ** trimmean: a non-scalar percentage is refused, as in MATLAB; it was accepted and returned an untrimmed mean behind a warning. ** ttest, ttest2: `Alpha` is validated; a negative, zero, unit or vector value was accepted, a negative one returning a NaN confidence interval together with 'do not reject'. ** unifit: the function returns MATLAB's four outputs [ahat, bhat, aci, bci], where it packed both estimates into one vector and both intervals into one matrix. X may now be a matrix, negative data is accepted, and the upper endpoint's interval was inverted. New functions: ============== ** andrewsplot: Andrews plot of multivariate data, with grouping, four standardizations and a quantile mode. ** ansaribradley: Ansari-Bradley test for equal dispersions, with an exact permutation p-value or a tie corrected normal approximation. ** biplot: biplot of principal component coefficients or factor loadings. ** ClassificationDiscriminant.cvshrink: cross-validate the discriminant over a grid of `Gamma` and optionally `Delta`. ** ClassificationDiscriminant.mahal, ClassificationDiscriminant.logp, ClassificationDiscriminant.nLinearCoeffs: squared Mahalanobis distance to each class mean, the log of the density summed over the classes, and the number of predictors kept at a regularization threshold; also on the compact class. ** ClassificationGAM.addInteractions, RegressionGAM.addInteractions: fit interaction terms onto a model that already carries its univariate ones. ** ClassificationGAM.resume, RegressionGAM.resume: add trees to a fitted model, continuing in the phase that ran last. ** ClassificationPartitionedLinear, ClassificationPartitionedKernel, RegressionPartitionedLinear, RegressionPartitionedKernel: the cross-validated counterparts of the linear and kernel learners. ** ClassificationPartitionedModel.kfoldLoss, ClassificationPartitionedModel.kfoldMargin, ClassificationPartitionedModel.kfoldEdge: the class had none of the three; `kfoldLoss` takes 'classiferror', 'classifcost', 'mincost' or a function handle, the margin based losses not being offered. ** CompactLinearModel: a fitted linear model without its training data, returned by `LinearModel`'s new `compact` method. ** CompactRegressionGAM: the compact counterpart of `RegressionGAM`. ** copulastat, copulaparam, copulafit: complete the copula family, which already shipped `copulacdf`, `copulapdf` and `copularnd`. ** coxphfit: Cox proportional hazards regression, opening survival analysis, with stratified models, the counting process form and seven residual types. ** dbscan: density-based spatial clustering of applications with noise. ** discardSupportVectors: empty the support vectors of a linear kernel model and leave `Beta` and `Bias` to predict, on the four SVM classes; the engine's own copy is collapsed, so the memory is freed. ** dwtest: Durbin-Watson test for autocorrelation in regression residuals, with an exact p-value by Imhof's method. ** ecdfhist: histogram built from the output of `ecdf`. ** fitckernel, ClassificationKernel, fitrkernel, RegressionKernel: Gaussian kernel models for large data, fitted linearly in a randomized feature space, with `resume`. ** fitclinear, ClassificationLinear, fitrlinear, RegressionLinear: linear models for data with many predictors, with ridge or lasso penalties and six solvers; a vector 'Lambda' fits one model per strength inside a single object, narrowed afterwards by `selectModels`. ** fitcnb, ClassificationNaiveBayes, CompactClassificationNaiveBayes: naive Bayes classification with the 'normal', 'kernel', 'mvmn' and 'mn' predictor distributions, mixable one per predictor. ** fitcox, CoxModel: the Cox proportional hazards model as an object, with `survival`, `hazardratio`, `coefci`, `linhyptest`, `plotSurvival` and `discardResiduals`. ** fitglm, GeneralizedLinearModel: generalized linear regression as a fitted model object over five families, the GLM counterpart of `fitlm` and `LinearModel`. A binomial response given with 'BinomialSize' is the number of successes, as MATLAB reads it, and `predict` returns the probability of success. ** fitglme, GeneralizedLinearMixedModel: generalized linear mixed effects models by penalized quasi-likelihood, with four fit methods. ** fitlme, fitlmematrix, LinearMixedModel: linear mixed effects models from a Wilkinson formula extended with random effects terms or from design matrices, by maximum likelihood or REML. ** fitnlm, NonLinearModel: nonlinear regression by iterative least squares as a fitted model object, with an error model and robust fitting. ** fitrgp, RegressionGP, CompactRegressionGP: Gaussian process regression with ten covariance functions, four explicit bases, and `postFitStatistics`. ** fitrnet, RegressionNeuralNetwork, CompactRegressionNeuralNetwork: neural network regression against the mean squared error with an identity output layer. ** fitrsvm, RegressionSVM, CompactRegressionSVM: support vector machine regression by epsilon-insensitive loss, with 'SVMtype', 'nu_svr' as an Octave extension. ** gamtrain, gampredict, gamboosttrain, gamboostpredict, gamboostpairs, gamboostinter, __lbfgs__, __bhtsne__, __knnselect__, __knnbrute__: new compiled functions: the spline and boosted-tree GAM engines, the limited-memory BFGS solver, the Barnes-Hut t-SNE summation and the nearest-neighbour kernels. They are not meant to be called directly. ** glyphplot: star glyph plot of multivariate data; Chernoff faces are not supported. ** gplotmatrix: matrix of grouped scatter plots, with grouped histograms on the diagonal. ** hmmdecode: posterior state probabilities of a hidden Markov model by a scaled forward-backward recursion. ** hmmtrain: maximum likelihood estimation of hidden Markov model parameters by Baum- Welch or Viterbi training. Baum-Welch is verified bit-for-bit against MATLAB; Viterbi training carries two documented deviations. ** iforest, IsolationForest: isolation forest anomaly detection, with an `isanomaly` method. ** invpred: inverse prediction from a simple linear regression, with Fieller confidence bounds that need not be finite. ** jbtest: Jarque-Bera hypothesis test of composite normality. ** johnsrnd: random arrays from the Johnson system of distributions. ** KernelDistribution: nonparametric kernel smoothing distribution object, created by `fitdist (x, 'Kernel')`. ** kfoldfun: apply a function across the folds of a cross-validated model, on `ClassificationPartitionedModel` and `RegressionPartitionedModel`. ** kmedoids: k-medoids clustering, with the 'pam' and 'small' algorithms. ** ksdensity: kernel smoothing density estimate, with four kernels, five output functions and bounded support; 'Censoring' is not yet implemented. ** lasso, lassoglm: lasso and elastic net regularized regression by cyclic coordinate descent, for least squares and for the five GLM families, with cross- validation and the 1-SE rule. ** lillietest: Lilliefors composite goodness of fit test for the normal, exponential and extreme value families. ** LinearFormula: model formula object, the class of the `Formula` property of a `LinearModel` and of a `GeneralizedLinearModel`. ** LinearModel.plot, LinearModel.plotInteraction, LinearModel.anova, LinearModel.step, LinearModel.compact: five new methods. ** lof, LocalOutlierFactor: local outlier factor anomaly detection, with an `isanomaly` method. ** mdscale: nonclassical metric and nonmetric multidimensional scaling over five stress criteria. ** mlecov: asymptotic covariance matrix of maximum likelihood estimators, from the observed Fisher information. ** mnrval: predict category probabilities and confidence bounds from a multinomial logistic regression model, the companion to `mnrfit`. ** mvksdensity: multivariate product-kernel smoothing density estimate. ** mvregress, mvregresslike: multivariate linear regression by maximum likelihood, with three algorithms for missing responses. ** nanvar, nanstd, nanmedian, nancov: complete the nan* family alongside the existing `nanmean`, `nansum`, `nanmax` and `nanmin`. ** nlinfit, nlparci, nlpredci: Levenberg-Marquardt nonlinear least squares with a numeric Jacobian, and confidence and prediction intervals for its coefficients and predictions. ** nnmf: nonnegative matrix factorization, by alternating least squares or multiplicative updates. ** ocsvm, OneClassSVM: one-class support vector machine anomaly detection, with an `isanomaly` method. ** parallelcoords: parallel coordinates plot of multivariate data. ** paretotails: piecewise distribution object with generalized Pareto tails and an empirical middle; the 'kernel' middle segment is not supported. ** partialcorr, partialcorri: linear and rank partial correlation coefficients, with p-values. ** pearsrnd: random arrays from the Pearson system of distributions. ** perfcurve: ROC and other classifier performance curves, returning the curve, thresholds, AUC and optimal operating point, with bootstrap bounds. ** ppca: probabilistic principal component analysis, handling missing data by expectation-maximization. ** probplot: probability plot of a sample against eight reference distributions, with censoring and frequency weights. ** RegressionPartitionedModel: the regression counterpart of `ClassificationPartitionedModel`, returned by `crossval` on a `RegressionGAM`, `RegressionGP`, `RegressionNeuralNetwork` or `RegressionSVM`. ** rica, ReconstructionICA: reconstruction independent component analysis for feature extraction, with a `transform` method. `Mu` and `Sigma` are columns and `FitInfo` carries the whole minimisation history, as in MATLAB. ** robustcov: robust multivariate covariance and mean estimate, with the 'fmcd' and 'ogk' methods; 'olivehawkins' is not implemented. ** robustfit: robust M-estimator linear regression by iteratively reweighted least squares, with nine weight functions. ** rocmetrics: receiver operating characteristic metrics object, with per-class AUC and the `addMetrics`, `average` and `plot` methods. ** rotatefactors: rotate a factor loading matrix by the orthomax family, promax, or procrustes rotation, the last defaulting to 'Type', 'oblique' as in MATLAB. ** scatterhist: scatter plot with marginal histograms or kernel densities. As in MATLAB, the marginal bars point toward the scatter by default and 'Location' names the corner the histograms occupy, not the scatter. ** sparsefilt, SparseFiltering: sparse filtering for feature extraction, with a `transform` method. ** spectralcluster: spectral clustering using the eigenvectors of a similarity graph Laplacian. ** StableDistribution, stblpdf, stblcdf, stblinv, stblrnd, stblfit, stbllike: the stable distribution in the Nolan S0 parameterization, by numerical inversion of the characteristic function; `makedist ('Stable')` was previously an unsupported stub. ** statset, statget: the options structure pair used across the package's iterative algorithms; seven functions documented an 'Options' argument and nothing could build one. ** stepwiseglm: fit a `GeneralizedLinearModel` by stepwise term selection, with the selection trace in its `Steps` property. ** stepwiselm: stepwise linear regression over five criteria, with term hierarchy enforced on both addition and removal. ** The classification margin and edge surface: fifteen new methods complete it: every full classifier now has `margin`, `edge`, `resubPredict`, `resubMargin`, `resubEdge` and `resubLoss`, and every compact classifier has `margin` and `edge`. A compact classifier gets no resubstitution method, as in MATLAB. ** tsne: t-distributed stochastic neighbor embedding, with the 'exact' and 'barneshut' algorithms. Improvements: ============= ** anova: the `anova1`, `anova2` and `anovan` backends all run silently, so fitting never prints a table or opens a figure. `varianceComponent` no longer returns an interval for a negative variance estimate, and a saturated model reports its error term as zero on zero degrees of freedom rather than producing no F-ratios at all. ** anova1: `p` and `F` are NaN for a design with no variation at all and `p` is 0 where the fit is exact, both having been the wrong way round. A categorical level that no observation uses leaves every field of `stats`, so they can be indexed together; this is a deliberate deviation from MATLAB, whose own fields disagree in length there. The `vartype` argument is documented as an Octave extension. ** anovan: the table reports a 'Singular?' flag and the degrees of freedom that can actually be estimated for each term. A design that is not of full column rank now takes the minimum-norm least squares solution, where the coefficients came from a triangular solve of a singular system and the sums of squares were nonsense. A continuous factor raised to a power is squared from the raw predictor rather than the centred one, which could put Type II and Type III out by orders of magnitude. `stats.termcols` counts one column per level, and 'sstype' accepts 'H' beside 'h'. ** boxplot: a single observation, an entirely missing variable and an empty input are plotted rather than failing inside the function, integer observations are accepted, and a grouped plot with a one-observation group no longer fails. ** canoncorr: rank deficient input was solved against a singular triangular factor, so the coefficients ran away and the canonical correlations came back wrong with nothing said. The factorisation now pivots, deficient columns are dropped, and a warning is raised as MATLAB raises one. ** ClassificationDiscriminant, CompactClassificationDiscriminant: all six discriminant types are available where only 'linear' was. `DiscrimType`, `Gamma` and `Delta` may be assigned after fitting, re- deriving `Sigma`, `LogDetSigma` and `Coeffs` without refitting, and `Delta` now eliminates predictors, the new `DeltaPredictor` reporting the value at which each drops out. `predict` applies the `ScoreTransform` the model holds, where it ignored it entirely. Assigning `Prior` or `Cost` no longer destroys the model, `LogDetSigma` is accurate on nearly collinear predictors, and the class can be loaded at all, `load_model` having raised 'invalid model' whatever the model held. ** ClassificationGAM, CompactClassificationGAM: 'Interactions' given as 'all' or as a count built no interaction term at all. Assigning `Cost` was refused on every model. `ClassNames` keeps the type of Y, where it was a cell of character vectors whatever the response was. A numeric response coded 1 and 2 gave a NaN intercept and every score with it. `predict` applies the `ScoreTransform`, which was documented, settable and read by nothing. ** ClassificationKNN: `NumNeighbors`, `Distance`, `DistanceWeight`, `BreakTies`, `IncludeTies` and `DistParameter` may be assigned after fitting and are live. `predict` applies the score transform once rather than once per query row, and its third output honours the cost matrix where it returned 1 - posterior. A model fitted with any metric a kd-tree cannot search could not be reloaded; eight of the twelve metrics were affected. 'CacheSize' is carried for compatibility. ** ClassificationNeuralNetwork: the trained parameters are reachable through `LayerWeights` and `LayerBiases`; before this a network could be trained with no way to see what it had learned. `loss` accepts every loss MATLAB documents for the class and defaults to 'mincost'. A saved model came back carrying another model's numbers, `savemodel` having written 21 of 29 properties into a placeholder object. ** ClassificationPartitionedModel: an explicit `Prior` reached no fold on the discriminant, SVM and neural network backings, and a non-default `Cost` reached no fold on the SVM backing, which enters it into the box constraint. A standardized `ClassificationKNN` refitted every fold on the raw scale. `kfoldPredict` discarded the predictions of a 'Holdout' partition, raised for a GAM backing and refused a cost for a network backing. The class gains `W` and a `disp` of its own, and takes a `Prior` on a naive Bayes backing. ** ClassificationSVM, CompactClassificationSVM: `predict` and `resubPredict` return the expected cost as a third output, and `kfoldPredict` does for an SVM-backed cross-validated model, where it raised. MATLAB returns the cost matrix read transposed; we return the row, which is documented. `resubLoss` returned the wrong number or none, and `resubPredict` selected the training rows with a bare mask, so a three-predictor model was asked about a single column. The classes report `Prior`, `Cost`, `W`, `CategoricalPredictors` and `ExpandedPredictorNames`. ** crossval: the partition covers only the observations actually trained on; with any missing value present each fold trained on rows its own fit then discarded. A function handle over a predictor and a response is no longer rejected, the last data variable having been dropped, which is why `plsregress`'s 'CV' never worked. ** cvpartition: 'Stratify' and 'GroupingVariables' can be combined, and 'GroupingVariables' applies to 'Holdout' and 'Leaveout' as well as 'KFold', where all three accepted it and silently ignored it, giving the caller the leakage the option exists to prevent. A scalar SVAL to `repartition` never worked, and seeding is now confined to the call. ** evalclusters: the returned object has MATLAB's shape and reports MATLAB's names. The silhouette criterion honours the 'Distance' it is given, where it always used 'sqeuclidean' and every metric returned the same numbers. ** ExhaustiveSearcher, KDTreeSearcher, hnswSearcher, knnsearch, rangesearch: the exhaustive search no longer forms the whole distance matrix for the four Minkowski-family metrics, and the searches take their K smallest by partial selection: a 4000-point self-search at K = 5 falls from 4.5 s to 1.9 s and searches that could not allocate now run. The kd-tree search is about five times faster, resolving the metric once per query. `knnsearch` and `rangesearch` accept a per-call metric. Asking for more neighbours than the data holds is answered with all of them. Single data stays single and every other class converts to double, which fixes integer data silently rounding each coordinate difference. Raising `SearchSetSize` on `hnswSearcher` returned the same neighbour for every query past a threshold. ** fcnntrain, fcnnpredict: every network the package trained descended the wrong gradient: the mean squared error layer differentiated sum (y - t)^2 as 2y - t rather than 2 (y - t), so training settled at 2y = t and the network converged on half its target. The backward pass of a hidden activation layer recovered the incoming gradient by dividing by its own input, read past the end of the array for any widening network, and applied the wrong local derivative for six of the eight activations. The `Accuracy` and `Loss` vectors described no network that ever existed and were then stored behind as many zeros as the epoch count. Training visited the samples in a fixed order, so with sorted labels the weights swung between the classes instead of settling. Weights are drawn on a range set by the fan-in rather than always on [-1, 1]. ** fitcgam, fitrgam: nine new boosted-tree options matching MATLAB in name and default, and an interaction asked for by count or 'all' is chosen by testing each candidate pair against the predictor phase's residuals and ranking by p-value, with 'MaxPValue' setting the cut. The GAM classes report `FitMethod`, `TreeModel`, `ReasonForTermination`, `BinEdges` and `PairDetectionBinEdges`, and `ModelParameters` reports MATLAB's thirteen fields under the boosted-tree engine. ** fitcnet: 'Prior' and 'Cost' are accepted, having been rejected as unknown parameters, so there had been no way to weigh the classes at all. 'Activations' accepts 'none', which the documentation listed and the code rejected. A model fitted with 'Standardize' set to true was trained on the raw predictors while `predict` standardized its own, so resubstitution accuracy on a badly scaled problem was 0.275 and is now 0.925. ** fitcsvm: a model fitted with 'Standardize' set to true was trained on the raw predictors while `predict` standardized its own, so resubstitution accuracy on a badly scaled problem was 0.500, chance, and is now 0.912. ** glmfit: the returned coefficient vector was clobbered by the binomial Anscombe residual computation, returning garbage coefficients whenever the `stats` output was requested and taking `glmval`'s confidence intervals with it. ** glmval: accept a numeric 0 or 1 for the 'simultaneous' option and 'BinomialSize' as an alias for 'size'. ** gpfit: the boundary warnings say which boundary was reached and that the confidence intervals are withheld. NaN and Inf observations propagate into the estimates rather than being dropped with a warning. ** hygecdf, poisstat, wblpdf, ClassificationNeuralNetwork: error messages and help text corrected: `hygecdf` named its third argument `k` where `hygepdf` names it `M`, `poisstat` named SIGMA, `wblpdf`'s three signature lines named `wblinv`, and the `Solver` property read 'Gradient Descend'. ** LinearModel, fitlm: a robust fit reported the wrong `LogLikelihood` and information criteria, and a robust fit given weights scored the wrong residuals, so a precisely measured observation was less likely to be downweighted. A variable the model does not use is no longer counted as one of its predictors. `Steps` is populated by `stepwiselm` and `step`, where it was always empty. `removeTerms` could not name a term on a table model whose formula uses only some of the table's variables, `step` refused any model holding a power term, and `stepwiselm` silently dropped one from a starting model or bound. Thresholds letting a term enter and leave at once are refused, where the search never returned. `VariableNames` and `PredictorNames` are column cell arrays. ** mle: the custom distribution interface is implemented, having been a stub that printed a message and silently fitted a normal. The 'bernoulli' and 'unif' families could not be fitted at all, and an empty 'Frequency' raised instead of meaning no frequencies. ** mnrfit: rewritten for MATLAB compatibility: nominal models with any number of categories, having errored for more than two, plus ordinal and hierarchical models, four links, the full `stats` structure and the `EstDisp` option. ** ModelParameters: the property reaches its final shape on every learner that has one. `ClassificationKNN` and `ClassificationDiscriminant` gain it, having been the only learners without one; the neural network pair reports the fit as it was asked for rather than the network that came out of it; `RegressionGP` reports its starting values; and the six partitioned classes report the learner's parameters under their own tags. ** optimalleaforder: a custom 'Transformation' is reachable, having been documented and implemented but refused before the code that uses it ran, and a two-leaf tree no longer crashes. ** paramci, proflik: `paramci` reports one column per parameter on `BinomialDistribution`, holding the fixed N at its own value, which also unblocks `proflik` there, and a fixed parameter is addressable by name again on every distribution. `proflik` returns a third output, defaults `pnum` to the first free parameter, keeps the profiled-out parameters inside their own range, and refuses a parameter with no confidence interval. On `BurrDistribution` it profiled a censored likelihood, the frequencies having landed in the censoring slot. ** pca: three defects in the handling of missing data and weights, contributed by Swayam Shah (GitHub PR #451). With 'Rows' set to 'all' the guard that refuses NaN input fired only when every column held a NaN. ** pdist, pdist2: no longer return a negative distance: 'cosine', 'correlation' and 'spearman' put a row at -2.2e-16 from itself. ** prob.PiecewiseLinearDistribution: `mean`, `var` and `std` of a truncated distribution are computed exactly segment by segment, where quadrature integrated across the density's jumps and lost four to five digits. ** randsample: added to the package INDEX, so the function is listed in `pkg describe` and the online documentation; it shipped but was undiscoverable there. ** regress: the coefficient standard errors and confidence intervals are computed from the QR factor directly rather than by inverting R' * R, which squared the condition number of the design. On the Longley benchmark they now match the NIST certified values to 6e-13 relative, previously 2e-8, with no spurious singularity warning. ** regression_ftest, regression_ttest: `regression_ttest` had no working call form, `cov` of two vectors returning a matrix, and its fitted values were evaluated at mean (x) instead of at x, so the residual sum of squares was really the total. `regression_ftest` refused the commonest test of all, dropping a single predictor. Positive tests have been added to both. ** RegressionGAM: `predict` computes ySD and yInt from the model's own terms rather than the stored predictors, so a model built with 'Interactions' or a 'Formula' no longer reports a standard deviation several times too large, or raises. A single Inf passed validation, and a scalar 'Knots', 'Order' or 'DoF' was never expanded. `savemodel` wrote a text file where every other class writes a binary one. ** RegressionGAM, ClassificationGAM: the spline fitting is compiled and each predictor's design is factorised once for the whole fit: fitting a classifier is four times faster and cross-validating one between five and nine times. A rank deficient design no longer divides by a singular value that is rounding noise. ** sampsizepwr: the 'r' test type for the correlation coefficient was unreachable by any route, and the sample size came from Fisher's transformation of the difference between the two correlations, which is right only when the null correlation is zero, the single value the function refused. ** slicesample: a log density supplied through 'logpdf' was logged a second time, so the chain sampled an entirely different distribution without complaint; drawing from a standard normal returned values with a mean around -8000. Three missing input checks are added. ** svmpredict: scoring a single observation corrupted the heap and aborted the interpreter; two or more rows were fine. The result pointers referred to a temporary that was freed before the prediction loop wrote to it. ** The copula family: `copulapdf` gains the Gaussian and Student's t families and `copularnd` the Frank and Gumbel-Hougaard families, so every family MATLAB supports now works in every copula function; only Clayton previously worked in all five. The Ali-Mikhail-Haq and Farlie-Gumbel-Morgenstern Octave extensions work throughout as well. ** The distribution name wrappers: `cdf`, `pdf`, `icdf`, `random`, `makedist`, `fitdist` and `mle` accept the same set of distribution names, matched ignoring case, spaces and hyphens; they previously accepted disjoint spellings, so `makedist ('Extreme Value')` and `cdf ('ExtremeValue', ...)` were both errors while their opposites worked. ** The distribution objects: `prob.KernelDistribution` hides `NumParameters`, `ParameterNames` and `ParameterDescription`, MATLAB carrying none of the three, and `prob.StableDistribution` no longer lists `ParameterCI`; all remain readable by name. The classes list `Truncation` and `IsTruncated` where MATLAB lists them. `MultinomialDistribution.random` and `LoguniformDistribution.random` were broken in every call form, and `KernelDistribution`'s `pdf`, `cdf` and `icdf` errored on an empty query. ** The learner classes: each reaches the property and method surface MATLAB gives it. `Intercept`, `CategoricalPredictors`, `ExpandedPredictorNames`, `W`, `BetweenSigma`, `KernelParameters`, `BoxConstraints`, `OutlierFraction`, `Nu` and `BinEdges` are added where each was missing, and the ten classes with a MATLAB counterpart declare `HyperparameterOptimizationResults`, read-only and always empty. Every classification learner accepts a response given as a character matrix, which the documentation always listed and which had never worked. Every classifier with a settable `Cost` validates the matrix as MATLAB does and accepts a struct of `ClassNames` and `ClassificationCosts`; `Prior` accepts the matching struct form. The seventeen supervised classes enforce access with property attributes rather than hand-written `subsref` and `subsasgn` whitelists. Setting `ScoreTransform` to 'none' or 'identity' made `predict` raise on four classes, and assigning it raised on five others, so the property could not be set at all. 'ismax' and 'symmetricismax' mark the largest score of each observation, where the maximum was taken down the column. ** The paired argument parser: `crossval`, `grpstats`, `rmmissing`, `stepwisefit` and `scatterhist` parse Name-Value arguments through `parsePairedArguments` from the `datatypes` package, and the statistics package's own private `pairedArgs` is gone. ** tiedrank: accepts MATLAB's fourth argument, a tolerance, which defaults to exact comparison so every existing call is unaffected. ** violin: a row vector is read as one variable, a cell of row vectors works, integer and logical observations are accepted, and a single observation, an empty input and a call with no arguments report what is wrong instead of failing inside the function. ** wblplot: a non-finite observation is rejected; `wblplot ([1, Inf, 2, 3])` never returned, creating nine graphics objects per iteration for as long as it was left alone. Removed functions: ================== ** mvtcdfqmc: the quasi-Monte-Carlo integrator behind `mvtcdf` and `mvncdf` is now the private helper `__mvtcdfqmc__` and can no longer be called directly. MATLAB ships no such function, so nothing portable depended on it. Call `mvtcdf` or `mvncdf` instead. Summary of important user-visible changes for statistics 1.9.1: ------------------------------------------------------------------- Improvements: ============= ** ClassificationKNN: a custom distance function handle works again. The constructor probed the handle against the response instead of the predictors and sized the result against the number of response rows, so a correctly written `distfun` was rejected and no custom metric could be used at all. A handle assigned to `Distance` after fitting was accepted without being probed and failed later inside `predict`, and `disp` could not print one. The `D2 = distfun (ZI, ZJ)` signature is now documented in the property's help text. ** documentation: `doc/statistics.qch` is rebuilt with pkg-octave-doc 0.7.7. Double- clicking the package in the GUI Documentation tab opens an overview listing every documented name under its category, rather than whichever category came first, and the one formula that reached the page as raw TeX now renders as the plain text form of its docstring. The online pages for `NonLinearModel` and `gmdistribution` were published as plain function pages, listing none of their properties or methods, because a member documented outside texinfo made the class renderer fail and fall back silently; both are rendered as classes again, restoring 105 and 72 documented members. Summary of important user-visible changes for statistics 1.9.2: ------------------------------------------------------------------- New functions and methods: ========================== ** rica, ReconstructionICA: a `'NonGaussianityIndicator'` name-value pair, and the read-only property of the same name, giving one sign per learned feature: `+1` seeks a super-Gaussian feature and `-1` a sub-Gaussian one. It sets the sign that feature's contrast term carries in the objective, so it moves the fit. The default is all `+1`, which is the fit as it stood. Improvements: ============= ** binoinv: an exactly attained probability is reached rather than stepped past, so the median of a symmetric binomial is right: `binoinv (0.5, 5, 0.5)` returned 3 and `binoinv (0.5, 2001, 0.5)` returned 1001. A `p` of 1 returns `n` rather than wherever the CDF first saturates, which was 206 for 500 trials at 0.25. An answer above 500 no longer errors on a row vector, on a vector `n`, or with a NaN among the probabilities. ** prob.BinomialDistribution: `median` and `icdf` at 0.5 follow the `binoinv` fix, so `median (prob.BinomialDistribution (5, 0.5))` is 2 rather than 3 on platforms whose `betainc` is less accurate. ** nbinpdf: a density is returned wherever one exists. The three factors of the formula left the range of a double before their product did, so `nbinpdf (1000, 1001, 0.5)` was `Inf` where the density peaks at 0.0089, and `NaN` past the mode. ** nbincdf: follows the `nbinpdf` fix. The lower tail sums the density, so it read 1 from 308 failures on for 1001 successes, where the value is 4.2e-39, `NaN` past the mode, and was not monotone. ** nbininv: an exactly attained probability is reached rather than stepped past. For `r` successes at a probability of 0.5 the median is exactly `r-1`, and `nbininv (0.5, r, 0.5)` returned `r` for 188 of the 399 values of `r` from 2 to 400, `nbininv (0.5, 101, 0.5)` among them. ** prob.NegativeBinomialDistribution: `median` and `icdf` follow the `nbininv` fix, so `median (prob.NegativeBinomialDistribution (101, 0.5))` is 100 rather than 101. ** documentation: `disp` and `display` are hidden on every class that defines one, which was already true of 141 of the 147 and is now true of all of them. They no longer appear in `methods (CLASS)` or in the online pages for `anova`, `paretotails`, `NonLinearModel`, `ConfusionMatrixChart`, `gmdistribution` and `rocmetrics`. Calling them is unaffected, and `NonLinearModel`'s `subsref` is hidden with them. `gmdistribution`'s methods are documented: `cdf`, `cluster`, `mahal`, `pdf`, `posterior`, `random` and the static `fit` carried no help text at all and now answer `help gmdistribution.METHOD`. `help` headings name the package rather than a category that does not exist: `copularnd`, `createns` and `normplot` were headed `Function File`, and `loadmodel` was headed `ClassificationSVM`, a class it is not part of. `NonLinearModel` and `LinearMixedModel` document their properties: all 23 and all 17 carried no help text at all and now answer `help CLASS.PROPERTY`. `NonLinearModel`'s `ErrorModelInfo` is hidden, MATLAB not carrying it in its public interface, so `properties ('NonLinearModel')` lists 23 names rather than 24; reading `MDL.ErrorModelInfo` is unaffected. `CoxModel`, `GeneralizedLinearMixedModel` and `gmdistribution` document their properties: all 16, all 14 and all 14 carried no help text and now answer `help CLASS.PROPERTY`, and `gmdistribution`'s constructor is documented. `gmdistribution`'s `NlogL` is hidden, MATLAB hiding the same alias, so `properties ('gmdistribution')` lists 14 names rather than 15; reading `OBJ.NlogL` is unaffected and `NegativeLogLikelihood` carries the same value. `ConfusionMatrixChart`'s fifteen property set methods move into a hidden block, so `set.XLabel` and its fourteen siblings no longer appear as methods in the online pages. Assigning the properties is unaffected. `ReconstructionICA` and `SparseFiltering` document their properties: the same eight on each carried no help text and now answer `help CLASS.PROPERTY`. `FitInfo` states that the fitting trajectory is this implementation's own, so its length and its iteration counts differ from MATLAB's under either solver. Six more property set methods move into hidden blocks, in `ClassificationKernel`, `ClassificationLinear`, `RegressionKernel`, `RegressionLinear`, `RegressionGP` and `CompactRegressionGP`, so `set.ScoreTransform` and `set.ResponseTransform` no longer appear as methods in the online pages. Assigning the properties is unaffected. `RegressionGP` and `RegressionGAM` document their constructors, and every compact class answers `help CLASS.CLASS` with real text where eight of the ten had returned a one-word source comment. All ten hide the constructor, as MATLAB does, so `methods` no longer lists it; `compact` is named in every one of their class descriptions as the way to create one. Every demo that draws random numbers seeds each generator stream it uses, so `demo` gives the same result twice and a rebuild of the online pages diffs only where something really changed. Octave seeds `rand`/`randn`, `randg`, `rande` and `randp` separately, and the legacy `('seed', N)` form selects a different generator, so many demos that looked seeded were not. The 29 `+prob` distribution class constructors answer `help`: each carried no help text at all and now gives its call signature, names its parameters and states their defaults. They stay hidden, `makedist` being the usual way to create a distribution object. ** johnsrnd: `quantiles` may be given as a 2-by-4 matrix, the first row four strictly increasing, evenly spaced standard normal quantiles and the second row the data quantiles at them; a four-element vector still means the fixed `[-1.5, -0.5, 0.5, 1.5]` points. The `SL` family returned a curve of the wrong sign whenever `delta` was negative, `lambda` having been fixed at 1 rather than taken as `sign (delta)`. A `NaN` among the quantiles is refused instead of being carried into the fit. ** fitclinear, fitrlinear: a fit that stops because the line search cannot improve the objective says so, reporting `TerminationCode` -11 and `'Unable to find a step decreasing the objective.'` as MATLAB does. Every exit but three was reported as `'Iteration limit exceeded.'`, so a fit that gave up after 59 iterations of a limit of 1000 claimed to have run out of iterations. ** partialcorr: a coefficient is NaN where the controlling variables explain either of the two variables completely, as MATLAB returns. The residual variance was tested against exact zero, which floating-point arithmetic does not produce, so the coefficient was computed from rounding error and was not reproducible between builds: `partialcorr ([1 2; 3 4; 5 5], [2; 4; 6])` gave an off-diagonal of -0.13 here and -0.089 elsewhere, where MATLAB gives NaN. ** lasso: `'Standardize'` is forced to false, with a warning, when `'Intercept'` is false. The two were accepted together and the predictors were standardised anyway. ** mixed effects models: `fitlme`, `fitlmematrix`, `fitglme`, `LinearMixedModel` and `GeneralizedLinearMixedModel` fit markedly more accurately. Both profiled deviances are now optimised with their closed form gradients rather than by finite differences, every matrix is symmetrised before it is factored, and the fitting path forms no explicit inverse. On a balanced one-way fixture, where REML must equal the ANOVA estimate exactly, the variance component was 6.9e-07 out and is now 1.4e-09. Summary of important user-visible changes for statistics 1.9.3: ------------------------------------------------------------------- Important Notice: Update dependency to datatypes 1.4.2. New functions and methods: ========================== ** fitctree, ClassificationTree, CompactClassificationTree: a CART binary decision tree for classification, over the new compiled `treetrain` and `treepredict` engine. It grows on a numeric predictor matrix with a numeric, logical, character or cellstring response, honours `Prior`, `Cost` and `Weights`, merges leaves and estimates the cost complexity pruning sequence, and reports the property surface MATLAB reports. Its methods are `predict`, `prune`, `predictorImportance`, `nodeVariableRange`, `view`, `margin`, `edge`, `loss`, the four `resub` forms, `compact`, `crossval`, `cvloss` and `savemodel`. `compact` returns a `CompactClassificationTree`, the tree without the observations it was fitted on, carrying the 33 properties MATLAB reports. `crossval` returns a `ClassificationPartitionedModel` over compact folds, taking `KFold`, `Holdout`, `Leaveout` or `CVPartition`, each fold grown with the parent's class names, prior, cost and observation weights. `cvloss` returns the cross-validated loss of the tree and of its subtrees, with the standard error, the leaf count of each subtree and the pruning level chosen by `'TreeSize'`; its standard error is the standard error over the folds, which is not the quantity MATLAB reports under that name. Categorical predictors are taken with `'CategoricalPredictors'`, `'MaxNumCategories'` and `'AlgorithmForCategorical'`. Such a predictor is split into two sets of levels, reported in `CutType`, `CutCategories` and `CategoricalSplit` and shown by `view` and `nodeVariableRange`; a level a node did not see stops an observation there. Two classes order the levels, more classes search every partition up to `MaxNumCategories` levels and otherwise take the best of the PullLeft, PCA and OVAbyClass heuristics. Splits match MATLAB R2024a but for ties and, on nodes with more than `MaxNumCategories` levels, the heuristic splits and their sides. Not implemented, and refused rather than ignored: surrogate splits, predictor subsampling, the `'twoing'` split criterion, the `'impurity'` pruning criterion, and binning. ** fitrtree, RegressionTree, CompactRegressionTree: a CART binary decision tree for regression, over the same compiled engine. It grows on a numeric predictor matrix and a numeric response, honours `Weights` and `QuadraticErrorTolerance`, merges leaves and estimates the cost complexity pruning sequence, and reports the 36 properties MATLAB reports. Its methods are `predict`, `prune`, `predictorImportance`, `nodeVariableRange`, `view`, `loss`, `resubPredict`, `resubLoss`, `compact`, `crossval`, `cvloss` and `savemodel`. `compact` returns a `CompactRegressionTree`, carrying the 28 properties MATLAB reports; `crossval` returns a `RegressionPartitionedModel` over compact folds, each grown with the parent's growth parameters and a slice of its observation weights; `cvloss` returns the cross-validated mean squared error of the tree and of its subtrees, with the same standard error caveat as the classification tree. Categorical predictors are taken with `'CategoricalPredictors'` and `'MaxNumCategories'`, the levels being ordered as for two classes. Splits match MATLAB R2024a but for ties and the sides a split's levels take. Not implemented, and refused rather than ignored: surrogate splits, predictor subsampling and binning. ** fitcensemble, ClassificationEnsemble, ClassificationBaggedEnsemble, CompactClassificationEnsemble: ensembles of decision trees for classification, boosted with AdaBoostM1, AdaBoostM2, GentleBoost, LogitBoost, LPBoost, TotalBoost or RUSBoost, or bagged. `predict`, `loss`, `edge`, `margin` with `Learners`, `Mode` and `UseObsForLearner`, the `resub*` methods, `compact`, `resume`, `removeLearners` and `predictorImportance`; `oobPredict`, `oobLoss`, `oobEdge`, `oobMargin` and `oobPermutedPredictorImportance` on the bagged ensemble. The `Subspace` method fits nearest neighbour or discriminant learners on random predictor subsets, or on every combination with `AllPredictorCombinations`, and averages their scores; weights that are not uniform are refused, the learners taking none. `RUSBoost` grows each tree on a weighted, class-balanced sample sized by `RatioToSmallest`. A boosting method resamples its observations when `'Resample'` is `'on'` or `'FResample'` or `'Replace'` is given, returning the bagged ensemble, which records the rows each learner drew and estimates out-of-bag error. `'CategoricalPredictors'` is passed on to every tree, which splits those predictors into sets of levels, and is carried to compact models and cross-validation folds; the `Subspace` method refuses them, its learners taking none here, where MATLAB passes them to nearest neighbour learners and drops them from discriminant ones. `LPBoost` grows each tree on the dual weights of a linear program over the trees, whose solution gives the learner weights, and stops when the smallest edge is within `'MarginPrecision'` of the program's value. `TotalBoost` takes one quadratic step towards the least relative entropy under edge constraints, stops when no tree can improve the margin, and maximises the smallest margin with GLPK. Both follow MATLAB R2024a; where a program has several solutions the trees grown next, or the weights chosen, can differ. MATLAB R2024a stops as if `'MarginPrecision'` were at least 0.01, where a smaller value is taken as given here. A perfect weak learner is kept and ends the fit, where MATLAB drops it; GentleBoost applies `LearnRate` to the fit; `resume` continues AdaBoostM2 exactly. ** fitrensemble, RegressionEnsemble, RegressionBaggedEnsemble, CompactRegressionEnsemble: ensembles of regression trees, boosted with LSBoost or bagged. `predict` and `loss` with `Learners`, `Mode` and `UseObsForLearner`, `resubPredict`, `resubLoss`, `compact`, `resume`, `removeLearners` and `predictorImportance`; `oobPredict`, `oobLoss` and `oobPermutedPredictorImportance` on the bagged ensemble, which a boosting method returns when `'Resample'` is `'on'` or `'FResample'` or `'Replace'` is given. `'CategoricalPredictors'` is passed on to every tree and carried to compact models and folds. `regularize` finds non-negative lasso weights for the trees over a path of `Lambda` values, `shrink` returns a compact ensemble of the trees a column of those weights keeps, and `cvshrink` cross-validates that choice; all three are on the bagged ensemble too. `regularize` returns the minimum of the lasso objective, where MATLAB's solver can stop short of it at small `Lambda`. ** ClassificationPartitionedEnsemble, RegressionPartitionedEnsemble: cross-validated ensembles, from `fitcensemble` or `fitrensemble` given `CrossVal`, `KFold`, `Holdout`, `Leaveout` or `CVPartition`, or from the `crossval` method of an ensemble. `kfoldPredict`, `kfoldLoss` with `Folds` and the modes `average`, `individual` and `cumulative`, `kfoldfun` and `resume`, and for classification `kfoldEdge` and `kfoldMargin`. A regression `ResponseTransform` is applied once, where MATLAB applies it twice. ** fitcecoc, ClassificationECOC, CompactClassificationECOC: multiclass classification from binary learners. A coding matrix says which classes each binary learner calls +1, which it calls -1 and which sit it out; a new observation goes to every learner and takes the class whose row of that matrix its scores match most closely. Seven learners are accepted, `svm`, `tree`, `knn`, `naivebayes`, `discriminant`, `linear` and `kernel`, either by name or as a template carrying the options to fit them with. Seven coding designs, seven binary losses and both decoding schemes. A linear or kernel learner keeps no training data, so the fit returns the compact model for those two, as MATLAB does. `discardSupportVectors` empties the support vectors of every binary learner that is a support vector machine on a linear kernel, whose linear model stands in for them exactly, and warns when none of them is; `selectModels` narrows every binary learner to a subset of its fitted regularization strengths. `'CategoricalPredictors'` is passed as given to every binary learner, which codes the predictors its own way; the model reports the indices. ** ClassificationPartitionedECOC: cross-validated ECOC models, from `crossval` or from `fitcecoc` given `KFold`, `Holdout`, `Leaveout` or `CVPartition`. It carries the coding matrix, the binary loss and what each observation was to each learner, which the general cross-validated class has nowhere to put and without which a fold's scores cannot be decoded. `kfoldPredict`, `kfoldMargin`, `kfoldEdge`, `kfoldLoss` and `kfoldfun`. ** TreeBagger, CompactTreeBagger: random forests of bagged decision trees, for classification and regression. Each tree is grown on a bootstrap sample and chooses every split from a random subset of the predictors. `predict`, `error`, `margin` and `meanMargin`, their out-of-bag counterparts `oobPredict`, `oobError`, `oobMargin` and `oobMeanMargin`, and `compact`, `growTrees`, `append`, `combine` and `setDefaultYfit`. Out-of-bag predictor importance by permutation, the split criterion contributions and split counts of each predictor, quantile regression with `quantilePredict`, `quantileError`, `oobQuantilePredict` and `oobQuantileError`, and the proximity matrix with `fillprox`, `mdsprox`, `proximity` and `outlierMeasure`. `'CategoricalPredictors'`, `'MaxNumCategories'` and `'AlgorithmForCategorical'` are passed to its trees. Labels come back in the type of the response, where MATLAB returns a cell array of character vectors. `oobQuantileError` in `individual` mode judges each tree on its own out-of-bag observations, where MATLAB R2024a fails, and `growTrees` empties a filled proximity matrix, where MATLAB keeps the one computed for the earlier trees. ** templateSVM, templateTree, templateKNN, templateNaiveBayes, templateDiscriminant, templateLinear, templateKernel, templateEnsemble: learner templates, which name a learner and the options it is to be fitted with without fitting anything. `fitcecoc` takes one as its `Learners`; `'Learners', 'ensemble'` asks for a LogitBoost ensemble of 100 trees. The default binary loss follows the method, as in MATLAB. A RUSBoost learner's scores, which MATLAB can read with no binary loss and so cannot predict with, are divided by the total weight of its trees, as R's ebmc and adabag and scikit-learn scale a boosted ensemble's votes, and read with `quadratic`. ** designecoc: the coding design matrix on its own, over seven designs. ** treetrain, treepredict: the compiled engine for decision trees, shared by the classification and the regression learner. `treetrain` grows a binary tree by recursive partitioning, merges the leaf pairs that lower no misclassification and computes the cost complexity pruning sequence; `treepredict` evaluates one, and both take the level sets of a categorical predictor. A row missing the split predictor descends to neither child and is stopped at that node when predicting. Neither is meant to be called directly. ** swtest: the Shapiro-Wilk test of normality, by Royston's algorithm AS R94, for 3 to 5000 values; `'Method', 'shapiro-francia'` gives the Shapiro-Francia test instead, for 5 to 5000 values. MATLAB has no such test. ** corr: correlation coefficients with their p-values, as MATLAB computes them: `'Type'` Pearson, Kendall or Spearman, `'Rows'`, `'Tail'` and `'Weights'`. It shadows core Octave's `corr`, which offers the Pearson coefficient alone and no p-value. Two deviations: a character array is refused, where MATLAB correlates the character codes, and an array of more than two dimensions is refused, where MATLAB and core Octave answer `NaN` for some of them. ** fitcknn, ClassificationKNN: `'CategoricalPredictors'`, empty or `'all'` as in MATLAB: with every predictor categorical the default distance is `'hamming'`. ** fitcsvm, fitrsvm, fitcnet, fitrnet (and their classes): `'CategoricalPredictors'`, as indices, a logical vector or `'all'`. Each categorical predictor is dummy coded in place, one column per level seen in training, named as in `'x1 == 2'` in `ExpandedPredictorNames`, and left unstandardized; `X` keeps the predictors as given, and compact models, saved models and cross-validation folds carry the coding. A level not seen in training gives no score or prediction. Bug fixes: ========== ** kmeans: an empty `'Replicates'` or `'MaxIter'` raised "invalid number of replicates" and "invalid MaxIter", where MATLAB takes an empty value as a request for the default. Both now use the default, 1 replicate and 100 iterations, and an empty `'Replicates'` beside a three-dimensional `'Start'` takes the replicate count from its third dimension. ** ClassificationGAM, RegressionGAM, fitcgam, fitrgam: the bin grids of a boosted-tree fit, `BinEdges` for a predictor with more distinct values than cuts and `PairDetectionBinEdges`, placed some cuts between different observations from MATLAB's whenever the bin position was fractional, and handled repeated values differently. Each cut is now the midpoint of the two observations MATLAB uses, moved up to the next distinct value where they are equal, so such fits now agree with MATLAB. ** ClassificationGAM, RegressionGAM, fitcgam, fitrgam: in an interaction term a missing value added nothing, and a training row missing one of the pair's predictors took no part in its trees. As in MATLAB, such a row now stops at the node splitting on its missing predictor and takes that node's value, in training and in prediction. One case is known to differ: a regression fitted with missing values can start its second interaction tree with a different split from MATLAB's. ** ClassificationGAM, RegressionGAM, fitcgam, fitrgam: interaction terms were fitted on the coarse grid used to detect them, so their surfaces differed from MATLAB's. Pair trees are now fitted to the rows, as in MATLAB: a node is cut between two values it holds, a leaf keeps at least five rows, trees grow a layer at a time within `'MaxNumSplitsPerInteraction'`, and each pair's curvature-weighted mean moves into the intercept. `PairDetectionBinEdges` reports only the detection grid. With `'MaxNumSplitsPerInteraction'` 1 no interaction is selected, and a warning says so. ** ClassificationGAM, RegressionGAM, fitcgam, fitrgam: a training row missing a predictor was left out of the boosted-tree fit. It is now fitted, as in MATLAB: it takes no part in that predictor's splits and is fitted with the value of the tree's root, and a predictor with missing values is held to zero at a missing value, so the intercept is the one MATLAB reports. The spline fit still uses complete rows. ** ClassificationGAM, RegressionGAM, fitcgam, fitrgam: `'Prior'` did not reach the boosted-tree fit, so it changed only the reported prior, and neither learner took a fit-time `'Weights'`. The fit now weighs each class by its prior, spread over its observations in proportion to `'Weights'`, as MATLAB does; an empirical prior is each class's share of the weight, and `W` reports the weights the fit used. Cross-validation folds keep the weights of the rows they hold. `'Weights'` is refused with `'FitMethod'` `'splines'`. ** stepwisefit: data with no complete observation, empty or all missing, failed with "operator +: nonconformant arguments"; it now returns the result MATLAB does, with `NaN` coefficients, `stats.df0` -1, `stats.dfe` 0 and an empty `history`. X and Y with different numbers of rows now get their own message (#484). ** ClassificationGAM, RegressionGAM and their compact classes: `predict` dropped every row holding a missing value, so it returned fewer rows than it was given, and `loss`, `margin` and `edge` failed on such data. Every row is now kept. Under boosted trees a missing value adds nothing from its term; under splines the row has no score (`NaN`), and a classifier gives it the class of largest `Prior`. ** fitcecoc, ClassificationECOC: a model whose binary learners take no observation weights (SVM, KNN, naive Bayes, discriminant) failed with "invalid parameter name" on classes of unequal size, and its cross-validation failed whenever a fold's classes were unequal: the class weights, equal or not, were passed to learners that cannot take them. Such a learner is now given the prior its two sides hold, as MATLAB's learners report it; `'Weights'` that vary within a class are refused for them with a message that says why. ** ClassificationSVM, CompactClassificationSVM: `margin`, `loss`, `edge` and `resubLoss` scored the data without the standardization the model was fitted with, so on a model fitted with `'Standardize'` they gave wrong margins and losses. ** ClassificationNeuralNetwork, RegressionNeuralNetwork and their compact classes: a row missing a predictor was given a score or a prediction with the default ReLU activation, the missing value being rectified to zero; its scores are now `NaN`. ** ClassificationLinear, ClassificationKernel, ClassificationSVM, ClassificationNeuralNetwork, ClassificationKNN, ClassificationECOC, their compact classes, and the matching regression models: a row missing a predictor was labelled with the first or second class, or given finite KNN scores and zero ECOC losses, or a `NaN` prediction. As in MATLAB, a classifier now gives it `NaN` scores (and `NaN` cost) and the class of largest `Prior`, and a regression model predicts the weighted lower median of the training response. `fitrgp` keeps such rows in `X` and `NumObservations`, as MATLAB does, but leaves them out of the fit. ** fitclinear, fitckernel, fitrlinear, fitrkernel, fitrgp: `'CategoricalPredictors'` was stored but ignored, so a categorical predictor was fitted as a number. Each categorical predictor is now dummy coded in place, one column per level seen in training, named as in `'x1 == 2'` in `ExpandedPredictorNames`, and left unstandardized; new data is coded the same way, a level not seen in training giving `NaN`. The option also takes `'all'`, and indices beyond the predictors are refused. ** ClassificationECOC, ClassificationEnsemble, ClassificationKernel, ClassificationLinear, ClassificationSVM, and the fitc functions that build them: classes given as text were laid out in the order they first appear in `Y` instead of sorted, so `ClassNames`, the score columns and per-class options such as `Prior`, `Cost` and `RatioToSmallest` disagreed with MATLAB and with the other classifiers. They are now sorted. ** ClassificationDiscriminant, ClassificationECOC, ClassificationEnsemble, ClassificationGAM, ClassificationKernel, ClassificationKNN, ClassificationLinear, ClassificationNeuralNetwork, ClassificationSVM, ClassificationTree, and the fitc functions that build them: `'ClassNames'` chose which classes to fit but not their order; the model now keeps the order given, as MATLAB and ClassificationNaiveBayes do. ** ClassificationDiscriminant, ClassificationECOC, ClassificationEnsemble, ClassificationGAM, ClassificationKernel, ClassificationKNN, ClassificationLinear, ClassificationNaiveBayes, ClassificationNeuralNetwork, ClassificationSVM, ClassificationTree, TreeBagger, and the fitc functions that build them: `'ClassNames'` whose type differed from the labels' failed with an internal error or was matched wrongly. Numeric names for text labels now raise "not all 'ClassNames' are present in Y." in every classifier; text names match numeric labels by their text, in naive Bayes and TreeBagger too, and logical labels as `'true'` and `'false'`; the ensembles, ECOC, kernel and linear classifiers now refuse a name absent from the labels like the others. ** ClassificationTree, RegressionTree, fitctree, fitrtree: when `MaxNumSplits` ran out partway through a layer of the tree, the nodes left unsplit were the last ones of that layer, whatever their splits were worth. They are now the ones whose splits gain the least, as MATLAB documents, so such a tree grows the splits MATLAB grows: `fitrtree (meas(:,2:4), meas(:,1), 'MaxNumSplits', 10, 'MinLeafSize', 5)` has a resubstitution MSE of 0.0791 where it had 0.0837. ** partialcorr: a controlling variable shifted by a large constant or scaled by a small one, or two variables of very different scales, gave a wrong coefficient or `NaN`: `partialcorr (X, [1e8 + t, t.^2])` differed from `partialcorr (X, [t, t.^2])` by half. The data are now centered and rescaled before the regression, so the coefficient no longer changes. MATLAB's does. Patch by Dmitri Sergatskov (#483). ** ClassificationSVM, fitcsvm: a two-class problem whose labels are the numbers -1 and +1 was fitted with every class the other way round, so `predict` returned the wrong label for every observation and `resubLoss` was 1 where it should have been 0. Any other labels were unaffected, numeric or textual, which is why nothing caught it. The solver is now given the class indices rather than the labels themselves. ** anova (LinearMixedModel), DFMethod satterthwaite: a model whose random-effects terms are confounded, such as `(1|g) + (1|g:g2)` where `g2` is a function of `g`, reported DF2 of 0 and a meaningless p-value. It now reports the degrees of freedom of the equivalent reduced model. ** nanmean, nansum, nanvar, nanstd, nanmedian: an empty input that is not 0 by 0 keeps its shape instead of collapsing to a scalar. `nansum (zeros (0,3))` gives `[0 0 0]`, `nanmean ([], 2)` a 0 by 1 empty and `nanvar (ones (2,0,3,2), 0, 2)` a 2 by 1 by 3 by 2, as MATLAB returns; each gave a scalar. A 0 by 0 input with no dimension given still gives the scalar. ** nanmax, nanmin: an empty input gives an empty result. The all-NaN mask was assigned into an empty array, which grew it, so `nanmax ([])` answered `NaN` and `nanmax (zeros (0,3))` a 1 by 3 of `NaN`, values that were never in the data. A dimension vector applied to an empty input no longer errors. ** nancov: an N-D input is refused with `Inputs must be 2-D.` instead of failing on an out-of-bound index. An explicitly empty `Y`, as in `nancov (x, [])`, is taken as given and its length checked, where it was discarded and the variance of `X` returned. `nancov (zeros (0,3), zeros (0,3))` gives a 2 by 2 of `NaN`, the two arguments being paired into two columns, not a 3 by 3. `nancov ([])` gives `NaN`, not an empty. ** ranksum: `stats` always carries a `zval` field, empty for the exact method, so reading it no longer errors on the method that computes no z-statistic. Its fields are ordered `ranksum` then `zval`, as MATLAB orders them; `zval` came first on every method. ** lillietest, jbtest: the Monte-Carlo p-value requested through `MCTol` is no longer smoothed by an added one, so it reaches exactly 0 and exactly 1 as MATLAB's does. The smallest value either could return was `1 / (reps + 1)`. ** jbtest: a sample with zero variance no longer aborts with an internal indexing error. Skewness and kurtosis are undefined there, so `jbstat` is NaN, and `p` is 0 and `h` is 1. ** datasample: `k` may be 0, returning an empty of the sampled orientation, as MATLAB returns; it was refused as not positive. ** makima: sample points where `x` or `y` is NaN are dropped in pairs before the fit, with a warning, where a single NaN made every returned value NaN. A NaN in one series of a matrix or N-D `y` drops that sample point from all of them, as MATLAB does. ** ztest: a matrix or N-D array is tested along the operating dimension instead of being collapsed to a single test, and a scalar is tested rather than answering an empty. `ztest ([1 2; 3 4; 5 6], 3, 1)` gives `[0 0]` and `ztest (5, 0, 1)` gives 1, where the first gave the scalar 1 and the second a 1 by 0 empty; with `dim` given and a NaN present the call raised `nonconformant arguments`. An empty input keeps its shape and a group with no data gives NaN, where every empty gave the scalar 0. ** grpstats: an empty X keeps its columns and has no groups, where every empty gave a 0 by 0 whatever the statistic asked for: `grpstats (zeros (0,3), zeros (0,1))` gives a 0 by 3, `'meanci'` a 0 by 3 by 2 and `'gname'` an empty cell column, as MATLAB returns. A grouping variable whose values are all missing now leaves no groups instead of raising `'group_mean' undefined`, and a row missing in any of several grouping variables is dropped rather than indexing the group names with NaN. `'gname'` with no grouping variable returns `{'1'}` where it raised `'g_names' undefined`. ** grpstats: `'sem'` on a matrix divides each column by its own count instead of solving a least squares system, which collapsed every column onto one value: `grpstats ([1 10; 2 20; 3 30], [1 1 1], 'sem')` gives `[0.5774 5.7735]` where it gave `[3.1754 3.1754]`. Columns of equal spread hid it. The table route was already right. ** grpstats: `'VarNames'` renames the variables of the output table instead of raising `table.renamevars: too few input arguments.` ** grpstats: a function handle in WHICHSTATS works on a matrix, where it raised `binary operator '>' not implemented for 'function handle' by 'scalar'` before any statistic was computed. It is applied to each column of each group on the values that are not NaN, as MATLAB applies it, and a table names the new variable after the function rather than after `@mean`; the table route was passing the NaNs through, so `@mean` answered NaN for a group holding one and `@numel` counted them. ** dwtest, runstest: an empty input gives a result instead of raising. `dwtest` returns `p = 0` and `d = NaN`, `runstest` returns `h = 0`, `pval = 1` and a `stats` of `nruns` and `z` both NaN with `n1` and `n0` both 0, as MATLAB returns. `dwtest (zeros (0,1), zeros (0,2))` died with `A(0,_): subscripts must be either integers 1 to (2^63)-1 or logicals`, a zero-length vector clearing the vector check and reaching the body; the other empty shapes raised a validation error. Two deliberate differences from MATLAB: `d` is NaN for every empty input, where MATLAB gives 0 for some of them and NaN for others, and a character array is refused, where MATLAB accepts one and returns the empty result. ** ClassificationKNN, fitcknn: `predict` on a model fitted to a categorical response raised `cat: categorical/vertcat method failed` instead of returning labels. The labels now come back as a categorical array, in the type of the response, as they already did for every other response type. ** fitcdiscr, fitcgam, fitcknn, fitcnb, fitcsvm: a `Prior` given as a structure was refused for a response held in a character matrix or a categorical array, the first with "'ClassNames' in the 'Prior' structure must name every class of the model" and the second with an error from `categorical.eq`. The structure's class names are now matched by text for both, and may themselves be a character matrix. ** crossval, kfoldPredict: a classifier fitted to a categorical or string response could not be cross-validated: `crossval` passed the model's own class names back to the learner, whose constructor accepted 'ClassNames' only as a cell array of character vectors, a logical or numeric vector or a character array. All eight classifier constructors now accept categorical and string 'ClassNames', and `kfoldPredict` returns labels in the type of the response, missing ones as `` or ``. ** savemodel, loadmodel (classification models): a classifier fitted to a categorical or string response did not survive a save. `savemodel` warned that classdef objects cannot be saved and wrote its class names and response as structures, after which the SVM, linear and kernel classifiers loaded with `ClassNames` a structure and predicted structures, and the KNN, naive Bayes, discriminant, GAM and neural network classifiers refused the file. Both types now save without a warning and load back as they were, and files saved by 1.9.2 load too. ** fitcsvm, fitcgam, fitckernel, fitclinear, fitcecoc: a categorical response carrying a category with no observations, such as a subset of a categorical array, counted that category as a class. The four binary learners refused the fit and `fitcecoc` built a coding matrix for a class with no data. Unused categories are no longer classes, as in the other classifiers. ** fitcnb, ClassificationNaiveBayes: 'ClassNames' given as a cell array of character vectors was refused for a categorical or string response with "no observation belongs to the named classes". Class names and labels of any textual type are now matched by their text. ** grp2idx: a categorical grouping variable was numbered over the values it held, sorted by name, while the names returned listed every category, so the numbers and the names disagreed whenever a category was unused or the categories were not in alphabetical order, and the levels lost their category order and ordinality. It is now numbered by category code, over every category and in category order, as MATLAB does. ** anova: the fitted values of a one-way model grouped by a categorical array with an unused category, or with categories out of alphabetical order, were taken from the wrong group's mean. Each observation now takes the mean of its own group. ** grpstats: grouped by a categorical array with an unused category, or with categories out of alphabetical order, the statistics were reported against the wrong group names. Each group now carries its own statistics, one per category in category order, as MATLAB reports. ** crosstab: the rows and columns of a categorical variable are laid out in category order, as MATLAB lays them out, where they were sorted by name. Improvements: ============= ** fitcdiscr, ClassificationDiscriminant: `'CategoricalPredictors'` is refused with a message saying that discriminant analysis cannot use categorical predictors, instead of as an invalid parameter name. ** @seealso, package wide: a class member named in an `@seealso` is now written qualified, `Class.member`, where 122 of them were bare. `help margin` finds nothing while `help ClassificationTree.margin` prints the method, so a bare name told the reader to type something that does not work; the online pages link a qualified member to its own page or to the class page and the collapsible holding it. A reference to a function keeps its bare name. ** ClassificationPartitionedModel: accepts a `ClassificationTree`, reporting `CrossValidatedModel` as `Tree` and storing a `CompactClassificationTree` per fold. `Cost` cannot be assigned on one, as it cannot on a cross-validated `ClassificationSVM`, the cost being consumed while the tree is fitted. ** RegressionPartitionedModel: accepts a `RegressionTree`, reporting `CrossValidatedModel` as `Tree` and storing a `CompactRegressionTree` per fold. ** fitlme, fitlmematrix, fitglme: the fits no longer form a matrix whose size is the number of observations. At 12800 observations and 20 random effects `fitlme` takes 0.10 seconds and 0.17 GiB where it took 114 seconds and 3.8 GiB; at 6400 observations `fitglme` takes 0.15 seconds and 0.18 GiB where it took 134 seconds and 1.4 GiB. Peak memory no longer grows with the number of observations, so fits above about 25000 observations are possible where they previously were not. ** cmdscale: points that are all coincident give a single column of zeros rather than an empty matrix, whose shape varied with how the input was given: a 4 by 0 for `cmdscale (zeros (4,4))` and a 0 by 2 for `cmdscale (0)`. With `p` given, the single column is kept and `e` keeps its length of `p`. ** grpstats: `grpstats (tbl)` summarises a table with no grouping variable, every variable being a data variable and the single group named `All`, as MATLAB does; it raised `table.removevars: too few input arguments.` ** grpstats: `[means, sem, counts, gname] = grpstats (x, group)` returns the four default statistics without naming any, as MATLAB does; up to four outputs were an error unless WHICHSTATS listed exactly that many. ** dwtest: a design matrix of full rank leaves no residual degrees of freedom and the statistic has no null distribution. The p-value was 0.5 by the exact method, from an empty coefficient set, and the approximate method reported 0 for an undefined value. It is now 0, 1 or 0 by tail for the exact method and NaN for the approximate one. ** regress: a row vector `y` is taken as a column, as in `robustfit`, `lasso`, `fitlm`, `stepwiselm` and `stepwisefit`; it was refused. ** fitcgam, fitrgam, ClassificationGAM, RegressionGAM: accept `'CategoricalPredictors'` under the boosted-tree engine, as indices, a logical vector or `'all'`. A tree splits a categorical predictor into two sets of levels, a level not seen in training predicts as a missing value, and its `BinEdges` and `PairDetectionBinEdges` are empty, as in MATLAB. `gamboosttrain`, `gamboostinter` and `gamboostpairs` take a matching logical argument flagging columns of level codes. ** fitcgam, fitrgam, ClassificationGAM, RegressionGAM: under the boosted-tree engine an interaction tree that splits on only one of its pair's predictors adds nothing, and a pair left with no tree is dropped with a warning, as in MATLAB. Such trees were kept, so `'Interactions', 'all'` fits moved away from MATLAB's after the first round. ** fitclinear, fitrlinear, fitckernel, fitrkernel: the 'bfgs' solver keeps the full inverse Hessian and both quasi-Newton solvers take MATLAB's weak Wolfe line search, so fits reach MATLAB's iterations and coefficients; they differed in the fourth significant digit. `ModelParameters.LineSearch` reports 'weakwolfe'. The coefficient tolerance is tested before the gradient tolerance, as in MATLAB. ** fitclinear, fitrlinear (lasso, 'sparsa'): the solver tests the gradient tolerance, a tolerance of 0 switches its test off, and a line search that finds no decrease reports code -11. With both tolerances at 0 it reported the coefficient tolerance satisfied. ** fitcdiscr, ClassificationDiscriminant: accept `'Weights'`. The class means and covariances are weighted, an empirical prior sums the weights per class, a row of zero weight is left out, and `W` holds the weights over their sum, as in MATLAB. Cross-validation folds keep the weights of their rows. ** fitcknn, ClassificationKNN: accept `'Weights'`. Each class carries its prior spread over its rows in proportion to their weights, and a neighbour votes with that weight; an empirical prior sums the weights, standardization is weighted, and a row of zero weight is left out, as in MATLAB. Scores under a prior other than the empirical one now follow it: a neighbour always voted as one. ** knnsearch, rangesearch, KDTreeSearcher, fitcknn: the kd-tree search returns neighbours at equal distance in row order and keeps the earlier row as the K-th neighbour, as MATLAB does. It returned them in the order it met them and could return a later row instead, so a KNN classifier could vote with a different neighbour. ** isoutlier: the `'percentiles'` method works on a matrix. It took both thresholds from one `quantile` call and indexed the result linearly, which only holds for a vector, so every column was tested against the first column's pair and the outlier mask was wrong without saying so. An option that needs a value now says which one is missing; `'movmean'`, `'movmedian'`, `'percentiles'`, `'SamplePoints'`, `'ThresholdFactor'` and `'MaxNumOutliers'` all failed on an internal subscript error instead. ** isoutlier: the `'quartiles'` method works. It passed the operating dimension to `quantile` where the probability belongs, so every call raised `Q(3): out of bound 2`. `'ThresholdFactor'` applies to it, and the help text no longer says otherwise. ** dummyvar: a `NaN` in a numeric grouping variable is missing data, filling that variable's own block of columns with `NaN` and leaving the others alone, as MATLAB does; it raised. A column that is entirely `NaN` has no levels, so it produces no columns and a warning names it. ** anova1, kruskalwallis, vartestn, levene_test, bartlett_test: a vector `X` without `GROUP` is a single group, whichever way it lies, as MATLAB reads it; all five raised instead. With one group there is nothing to compare, so `p` is `NaN`; `kruskalwallis` and `vartestn` returned 0. An empty `X` gives `anova1` and `kruskalwallis` an answer with no degrees of freedom below zero, where MATLAB reports negative ones, and the three variance tests refuse it by name, as MATLAB's `vartestn` does. ** knnsearch: `'K'` above the number of observations returns all of them, as MATLAB does; it raised a nonconformant error on `'kdtree'` and named a range on `'exhaustive'`. This also settles `'K', Inf`, which ran out of memory, and a single-row `X`, which returned that one point K times. An empty `X` or `Y` returns empty rather than raising. ** LIBSVM: the bundled `svm.cpp` and `svm.h` move to the instance-weights variant 3.37. Nothing in the two files changed from 3.36 but the version constant. ** fitcsvm, ClassificationSVM, svmtrain: accept observation weights. `svmtrain` takes a fourth argument of instance weights, each scaling its instance's box constraint, from LIBSVM's instance-weights variant. `fitcsvm` gives each observation the box constraint n * C * w, w its weight scaled to the cost-adjusted prior; an empirical prior sums the weights, standardization is weighted, and a row of zero weight is left out, as in MATLAB. Prior and Cost now enter the fit the same way with or without weights, and `loss` scales the weights of each class to its prior, as MATLAB does; it counted every row alike. ** fitcsvm, ClassificationSVM, CompactClassificationSVM (one-class): `predict` labels every row with the model's one class and returns one score column, as MATLAB does; it failed with an index error whenever a row was an outlier. A one-class fit ignores observation weights, and its classification error is 0, as in MATLAB. Its bias puts the least supported support vector on the boundary, or with `'OutlierFraction'` leaves that fraction of the training rows below it while keeping `Nu`, as MATLAB does; it was LIBSVM's average over the free support vectors, and `'OutlierFraction'` refitted the model with that `Nu`. ** crosstab: a categorical group gives an unused category its zero row or column, as MATLAB does, so the table lines up with `labels`; the table dropped the row while `labels` kept its name, so every later row was mislabelled. The chi-square statistic and p-value are computed over the rows and columns holding observations, where MATLAB returns NaN when any is empty. ** Classifiers with a categorical response: `ClassNames`, and the labels `predict` returns, carry only the categories the response holds, as MATLAB reports them; an unused category stayed in their category list. ** ClassificationKNN, ClassificationDiscriminant, CompactClassificationDiscriminant: `loss`, `margin` and `edge` match the true labels to the classes by name, whatever their type, as every other classifier does and as MATLAB does; cellstr, string or categorical labels of another type than `ClassNames` were refused. ** ClassificationSVM, CompactClassificationSVM (one-class): `margin`, and so `edge` and `resubEdge`, return NaN, as MATLAB does; they returned twice the score. `resubLoss` with `'classifcost'` returns 0, as MATLAB does; it failed with an index error when a training row was an outlier. ** fitlme, fitlmematrix, fitglme, LinearMixedModel, GeneralizedLinearMixedModel: the random-effects design is held as a sparse matrix, and the fit factorises its covariance in sparse form unless crossed terms make it fill in, so large fits with many groups use far less memory and run much faster; `designMatrix` returns it sparse for `'Random'`, as MATLAB does. ** LinearMixedModel: `residuals` with `'ResidualType', 'Standardized'` no longer builds n-by-n matrices; it took 35 s and 7.7 GB at 12000 observations and could not run on large data sets, and now takes milliseconds. `anova` with `'DFMethod', 'Satterthwaite'` runs 10 to 250 times faster with many groups. Summary of important user-visible changes for statistics 1.9.4: ------------------------------------------------------------------- Important Notice: Observation weights given to the machine learning models must be single or double; logical and integer weights are refused. Backwards incompatible improvements: ==================================== ** Observation weights: `'Weights'` must be single or double at fitting and in `loss`, `edge`, `resubLoss`, `resume` and the `TreeBagger` error methods, as in MATLAB. Integer weights fitted a different model or failed; logical weights worked only in `fitcecoc`, `fitcensemble` and `fitrensemble`. New functions and methods: ========================== ** boxchart, stats.chart.BoxChart: box charts from vectors or a table, with notches, either orientation, jittered outliers and colour groups. The returned object redraws when its properties are set. ** gumbelstat: the mean and variance of the Gumbel distribution. ** lime: local interpretable model-agnostic explanations for any model that predicts and for a function handle, with a linear or a tree simple model, and a `plot` method. `'Distance'` also takes a function handle, which MATLAB does not. ** partialDependence, plotPartialDependence: partial dependence of a fitted model on one or two predictors, drawn as a line, a surface or individual conditional curves. ** PredictiveModel: an abstract superclass of every model with a `predict` method, so that `isa (Mdl, 'PredictiveModel')` tells whether a model predicts. ** shapley: Shapley values for any model that predicts and for a function handle, exact or estimated, interventional or conditional, and exact at any size for linear models and trees. `plot`, `boxchart`, `swarmchart` and `plotDependence` draw them. ** swarmchart: swarm charts. `XData` holds where each point was drawn, where MATLAB leaves it unspread. ** Table input: every learner takes a table at fitting, and so do `predict`, `loss`, `margin`, `edge` and the other methods that take observations, matched to the predictors by name. A variable holding levels is a categorical predictor. `shapley` and `lime` take tables too. Bug fixes: ========== ** anova1: the table for `'unequal'` variances carried a sum of squares column headed `F`; it now has the five columns the function prints. ** anova2: an empty `X` failed on an internal variable; it is refused with a clear error, where MATLAB returns NaN p-values. ** burrinv: every quantile of a Burr distribution with a scale other than 1 was wrong, which reached `icdf` and `median` of `BurrDistribution`. ** ClassificationDiscriminant: `mahal` with `'ClassLabels'` failed for a categorical response and refused categorical and string labels. ** ClassificationDiscriminant, ClassificationKNN: `resubLoss` failed when given both `'LossFun'` and `'Weights'`. ** ClassificationGAM, RegressionGAM: with `'FitMethod', 'splines'`, `'Knots'` and `'DoF'` given together kept only the second. ** ClassificationKNN: `partialDependence` failed for class labels given as text. ** ClassificationSVM: `'Nu'` on two classes trained a one-class model, `'OutlierFraction'` overwrote a given `'Nu'`, and `loss` and `margin` failed for a categorical or string response. ** ClassificationTree, ClassificationNaiveBayes, ClassificationLinear, ClassificationKernel: `edge` on data missing a class was scaled by the prior of the classes present. ** combnk: `combnk (DATA, 0)` returned 0-by-0 instead of the 1-by-0 empty combination. ** datasample: NaN and all-zero weights were sampled from, and an `Inf` weight drew the last element, repeatedly even without replacement. ** dummyvar: a grouping variable holding `Inf` ran out of memory, and an empty one with several columns failed. ** ff2n: `ff2n (0)` was refused; it returns 1-by-0. The error message printed texinfo markup. ** fitcecoc: weights varying within a class were refused for SVM, KNN, naive Bayes and discriminant learners. ** fitcknn, fitcsvm: `'Leaveout', 'on'` was refused. ** fitcnet: a prior other than the empirical one did not affect training, and `W` was misaligned when a row missed a predictor. ** fitcsvm, fitcknn, fitcnet: standardization scaled a constant predictor by about 1e16. ** fitrgp: `W` held the weights as given instead of summing to one. ** gpcdf, gppdf, gpinv, gprnd, gpfit: the help text gave the wrong condition for the Pareto special case. ** grp2idx: empty grouping variables returned wrongly shaped outputs or failed, and a character array without rows counted as one observation. ** iforest: fewer than 3 observations were refused with an error about an option the caller had not given. ** libsvmwrite: column indices were written in the wrong format on Windows. ** LinearModel: `anova` reported a rounding residue, or an infinite F, for a term that adds no rank, and failed on a model holding only an intercept. ** mvncdf, mvtcdf: an options structure missing fields, as `statset` makes, was refused. ** randsample: `randsample (Inf, 1)` returned a huge number and an infinite `k` ran out of memory. ** ranksum, vartestn: the help text described a rule and fields the functions do not have. ** RegressionSVM: `IsSupportVector` marked the wrong observations when a row missed a predictor. ** rmmissing: empty input accepted invalid options and returned a wrongly shaped `TF`; `'MinNumMissing'` may be 0, and on a vector it counts per element. ** Single observation weights: `fitclinear` and `fitrlinear` fitted a different model from single weights. `W` keeps the class of the weights; `Prior` and the predictions stay double where MATLAB returns single. ** standardizeMissing: an empty or matrix indicator was refused. ** svmtrain, svmpredict: an options string of 2048 characters or more was read past its end. Improvements: ============= ** Observation weights: `fitcnb`, `fitrsvm`, `fitcnet` and `fitrnet` take `'Weights'`, and their `resubLoss` and cross-validation weigh by them. ** CategoricalPredictors: every learner that codes categorical predictors takes them by name as well as by index. ** TreeBagger, CompactTreeBagger: a read-only `CategoricalPredictors` property. ** pdist: inputs of 1000 observations or more are computed much faster, 0.02 s instead of 46 s for `'correlation'` on 1200 rows. ** Machine_Learning: `inst/Supervised_Learning` is renamed `inst/Machine_Learning`, and its `INDEX` group "Machine Learning". ** Compiler warnings: the compiled sources build without warnings under `-Wall -Wextra`. statistics-release-2.1.2/ONEWS-2.0.x000066400000000000000000000344561527311104300167160ustar00rootroot00000000000000 Summary of important user-visible changes for statistics 2.0.0: ------------------------------------------------------------------- Important Notice: 1) Update dependency to datatypes 1.5.0. 2) Incompatibility with the `nan` and `tablicious` packages. Backwards incompatible improvements: ==================================== ** bartlett_test, levene_test: removed; `vartestn` runs both tests, with 'TestType' 'Bartlett', 'LeveneAbsolute', 'LeveneQuadratic' or 'BrownForsythe'. `levene_test`'s 'quadratic' option computed the absolute test. ** chi2test: returns `[h, p, stats]` where it returned `[p, chisq, df, E]`, so `p = chi2test (x)` still runs but returns `h`: every call must be updated. The statistic, its degrees of freedom and the expected table are in `stats` (`chi2stat`, `df`, `E`); 'Alpha' sets the level of `h`. ** correlation_test: the statistic in `stats` is `tstat` (Pearson, Spearman, with `df`) or `zval` (Kendall) instead of `stat`, and the coefficient is `CorrCoef` instead of `corrcoef`. ** fitcsvm, fitrsvm, ClassificationSVM, RegressionSVM: 'KernelScale' divides every predictor before the kernel is applied, and the polynomial kernel is (1 + u'v)^q, as in MATLAB; 'KernelOffset' now changes the sigmoid kernel alone. Every Gaussian, polynomial and sigmoid fit changes; models saved before predict as they did. `SupportVectors` is a full matrix. ** hotelling_t2test, hotelling_t2test2: the statistic in `stats` is `t2stat`, no longer `Tsq`. ** jackknife: the result has one row per jackknife sample, as in MATLAB, so a scalar estimator returns a column where it returned a row (#511). ** lime: with predictors holding levels, coefficients are read against the query point's level, the kernel scale is the square root of the number of predictors, and 'goodall3' counts the query point into the level frequencies, reproducing MATLAB's coefficients to 3e-5. 'ofd' is one less the mean OF similarity. On data mixing numbers and levels, a numeric predictor scores its difference over its range, where MATLAB's undocumented rule differs. ** mcnemar_test: returns `[h, p, stats]`, with the chi-squared statistic in `stats.chi2stat`; the significance level is the 'Alpha' option, no longer a second argument. ** regression_ftest: fits both models itself, `regression_ftest (y, X, keep)` taking the columns the reduced model keeps, instead of two coefficient vectors; with no `keep` it tests every column together. Its former F statistic and degrees of freedom were wrong (21636 on (2, 10) where R gives 4554 on (2, 9)); it now agrees with R and with `coefTest` of `fitlm`. ** ztest2: returns `[h, p, ci, stats]`: the third output is now the confidence interval, and the statistic is `stats.zval`. New functions and methods: ========================== ** detectdrift, stats.drift.DriftDiagnostics: drift between a baseline and a target data set, variable by variable, with nine metrics, permutation p-values with Clopper-Pearson intervals, Bonferroni or false discovery rate correction, `summary`, `ecdf`, `histcounts` and four plots. 'MaxNumPermutations' is never exceeded, where MATLAB can pass it. ** fitrm, RepeatedMeasuresModel: repeated measures models, with `ranova`, `epsilon`, `mauchly`, `anova`, `manova`, `coeftest`, `margmean`, `grpstats`, `multcompare`, `predict`, `random`, `plot` and `plotprofile`, and the Greenhouse-Geisser, Huynh-Feldt and lower bound corrections. Hotelling-Lawley uses McKeon's F with McKeon's degrees of freedom, as SAS does, and Tukey-Kramer p-values are computed where MATLAB floors them. ** gardnerAltmanPlot: two samples, or paired samples, beside their effect size from `meanEffectSize` and its interval on a second vertical axis. ** heatmap, stats.chart.HeatmapChart: heatmap of a matrix, or of a table counted or aggregated over two of its variables, with cell labels, a colour bar, colour scaling, missing-data colour, and `sortx`, `sorty`, `xlim` and `ylim`. ** knntest: two-sample test on nearest neighbours, for numeric matrices and tables of numeric, categorical, logical and text variables. ** meanEffectSize: mean difference, Cohen's d, Glass's delta, Cliff's delta, median difference, robust Cohen's d and the Kolmogorov-Smirnov statistic, for one, two or paired samples, with exact or BCa bootstrap intervals that reproduce MATLAB's. Octave extensions: the 'akpcohen' effect, as the R package WRS2 computes it, and 'Resampling', 'stratified'. ** mmdtest: two-sample test on the maximum mean discrepancy with a Gaussian kernel and a permutation p-value, following MATLAB's kernel, scale and p-value rules. ** nomdist, nomdist2: dissimilarity between rows of nominal data, within one sample and between two, by the 16 measures of the R package `nomclust`, Goodall 3 by default. MATLAB has no counterpart. ** parallelplot, stats.chart.ParallelCoordinatesPlot: parallel coordinates plot of a matrix or a table, optionally grouped, with the six normalizations MATLAB offers. 'LineAlpha' blends each line's colour toward the background, since Octave's lines carry no transparency. ** scatterhistogram, stats.chart.ScatterHistogramChart: scatter plot of two samples, from vectors or a table and optionally grouped, with a histogram or kernel density of each along its sides. ** stdrcdf, stdrinv, stdrpdf, stdrrnd, stdrstat: the studentized range distribution, with each tail computed directly. MATLAB has no public counterpart. Bug fixes: ========== ** betainv: quantiles are resolved to the last digits in both tails; the lower tail was off by up to nine orders of magnitude. `tinv`, `finv`, `icdf` and `prob.BetaDistribution` inherit the fix: `tinv (1e-6, 1)` was -44721 where the quantile is -318310. ** binotest: the two-sided p-value counts every outcome as likely as the one observed; `binotest (1, 10, 0.5)` gave 0.0117 for 0.0215. The one-sided p-values were swapped. 'alpha' is validated. ** chi2test: 'homogeneous' fits to convergence, 'marginal' tests the collapsed table as documented, `E` keeps the layout of `X` for 'joint' and 'conditional', and 'mutual' and 'homogeneous' no longer need an empty value. Every model agrees with R. ** correlation_test: 'kendall' and 'spearman' no longer fail. Spearman's test uses the t approximation, as R's `cor.test` does, where reversing a sample changed the p-value; Kendall's variance allows for ties. ** finv: small `P` keeps its digits, where `finv (1e-10, 1, 1)` was 0, and degrees of freedom above 1.5e6 are no longer capped. ** fitcsvm, fitrsvm, ClassificationSVM, RegressionSVM: 'BoxConstraint', 'Nu', 'CacheSize' and 'Tolerance' reach the solver at full precision, where a 'BoxConstraint' or 'Nu' below 5e-7 failed. A fit stopped at the solver's iteration limit raises an Octave warning. A one-class model labels its support vectors +1, so its `Beta` no longer has the wrong sign. ** fitglme, GeneralizedLinearMixedModel: a normal response is fitted against its estimated dispersion, so standard errors, p-values and random effects match `fitlme`; they were off by up to a factor of 53. ** fitrsvm, RegressionSVM: the default 'BoxConstraint' of a Gaussian kernel is `iqr (Y) / 1.349`, as in MATLAB; it was 1, so such fits change. ** GeneralizedLinearModel: `devianceTest` reports an F test where the dispersion is estimated, as MATLAB does; it gave a chi-square with a wrong p-value. ** geomean, harmmean: an `Inf` in `DIM` or `VECDIM` is refused, where it was ignored (#510). ** glmfit, fitglm: the canonical 'inverse gaussian' fit converges, where its deviance could become `Inf`. ** jackknife: each row of a matrix sample is an observation, as in MATLAB; it left out single elements and returned wrong values silently. 'Options' is accepted. ** jsucdf, jsupdf: no longer fail on every call. ** ksdensity, paretotails: the lower tail of the CDF keeps its digits. ** lime: a predictor holding one level no longer makes `fit` fail. ** linkage: an `Inf` or `NaN` distance no longer corrupts the tree; they merge at height `Inf` and `NaN`, in MATLAB's order. ** mcnemar_test: the exact and mid-p tests returned p-values above 1 when the larger discordant count came first. ** nancov: two empty inputs return a 2-by-2 matrix of NaN, as in MATLAB, where they returned a scalar NaN. ** ranksum: an unknown option name is refused, so a misspelt 'tail' no longer runs a two-sided test without notice. ** robustcov: 'qn' no longer fails, and 'ogk' matches MATLAB on moderately contaminated data. ** tcdf, tinv: the lower tail of `tcdf` keeps its digits (`tcdf (-17.138, 147)` was -1.1e-16 where it is 3.6e-37), and `tinv` is resolved near `p = 0.5`. ** vartestn: group labels given as text no longer fail. ** P-values below 1e-16: resolved, where they were 0, in `anova1`, `anova2`, `anovan`, `barttest`, `binotest`, `canoncorr`, `chi2gof`, `chi2test`, `correlation_test`, `coxphfit`, `factoran`, `friedman`, `hotelling_t2test`, `hotelling_t2test2`, `jbtest`, `kruskalwallis`, `manova1`, `mcnemar_test`, `multcompare`, `regress`, `regression_ftest`, `regression_ttest`, `stepwisefit`, `ttest`, `ttest2`, `vartest2`, `vartestn`, `ztest2`, `ClassificationGAM`, `CoxModel`, `GeneralizedLinearMixedModel`, `GeneralizedLinearModel`, `LinearMixedModel`, `NonLinearModel` and `RegressionGAM`. Improvements: ============= ** binotest: a fourth output `stats` carries the estimate and Cohen's h. ** chi2test: `stats` carries Cohen's w, and Cramer's V for a two-way table, each corrected for bias and with a confidence interval. ** correlation_test: `stats` carries a confidence interval for the coefficient. ** hotelling_t2test, hotelling_t2test2: `stats` carries the F statistic `fstat` and the Mahalanobis distance with its confidence interval. ** jackknife: several samples reach an estimator taking more than one input as separate arguments, as in MATLAB. ** kruskalwallis, friedman, ranksum, signrank: `stats` carries an effect size with its confidence interval: the rank eta squared, Kendall's W and the rank-biserial correlation; 'ConfidenceIntervalType', 'bootstrap' gives a BCa bootstrap interval. `kruskalwallis` and `friedman` take 'Alpha', 'ConfidenceIntervalType' and 'NumBootstraps' after `displayopt`. ** mcnemar_test: `stats` carries the odds ratio of the discordant pairs and Cohen's g, each with an exact confidence interval. ** regression_ftest: `stats` carries both models' coefficients and residual sums of squares, and Cohen's f² with its confidence interval. ** statset: an empty value such as `[]` stands for no options structure, and any number of structures may lead the arguments, as in MATLAB (#509). ** ztest2: a confidence interval for the difference in proportions, and `stats` with Cohen's h and its interval. Summary of important user-visible changes for statistics 2.0.1: ------------------------------------------------------------------- Backwards incompatible improvements: ==================================== ** nanmax, nanmean, nanmedian, nanmin, nanstd, nansum, nanvar: DIM is validated alike in all seven: a logical, non-integer, infinite, empty or matrix DIM and a VECDIM with repeated or infinite dimensions raise an error, where some were accepted or failed with an unrelated message. ** robustcov: MAH and OUTLIERS have one row per row of X, rows with missing data padded with NaN and false, where those rows were dropped silently (#520). Bug fixes: ========== ** confusionmat: logical input always gives a 2-by-2 matrix with order [false; true], as MATLAB since R2024b (#523); a numeric Order for logical input is taken as logical, where it raised an error; a duplicated Order raises an error, where it gave extra rows and columns. ** crosstab: numeric labels are written without padding, '1' beside '10' where it gave ' 1'; an empty numeric input gives no labels, where it gave one empty name; an integer-typed input beside other numbers raised an error. ** fitctree, ClassificationTree: on i386, merging leaves left some leaf pairs unmerged, so a tree kept extra nodes (#514). ** fitrgam, fitcgam, RegressionGAM, ClassificationGAM: on i386, boosting interactions could accept a round in which no pair grew a tree, so a pair whose trees all split one predictor was kept (#514). ** makima: complex y with more than one series raised an error, or returned wrong values when the number of series equalled the number of sample points (#524); the pp form of an N-dimensional y returned its series in the wrong order. ** nanmax, nanmean, nanmin, nansum: logical input is accepted, as in the rest of the family; `nanmax` and `nanmin` return a logical result, as in MATLAB (#516). ** plsregress: a component that cannot be formed, as with a constant X, is NaN, as in MATLAB, where X with several responses raised an error. ** robustcov: X with fewer rows than twice its columns raises an error, where 'fmcd' and 'ogk' failed inside chol (#520); small samples in 'fmcd' no longer give a covariance that is not positive definite, its small-sample factors now following the R package robustbase; 'BiasCorrection' false drops both factors, as in MATLAB. ** shapley: the 'conditional' method counts distances equal to within rounding as tied, so the neighbours kept no longer depend on the platform (#514). ** squareform: 'tomatrix' with a matrix input raises an error, where it returned a distance matrix built from the matrix's elements (#521). Improvements: ============= ** crosstab, tabulate: datetime and duration inputs are accepted, NaT and NaN durations counted as missing (#525). ** grp2idx: datetime grouping variables are accepted, numbered in sorted order, with NaT as missing (#522). ** plsregress: new 'Intercept' option fits without centering the data (#526). ** robustcov: new 'olivehawkins' method (#520). statistics-release-2.1.2/doc/000077500000000000000000000000001527311104300160665ustar00rootroot00000000000000statistics-release-2.1.2/doc/statistics.png000066400000000000000000000006421527311104300207700ustar00rootroot00000000000000‰PNG  IHDRddÿ€tIMEß Ó@Ÿ…tEXtCommentCreated with GIMPW�1IDATxÚíÜAƒ Ph¼ÿ•颫&mé(Âû«vaŒO%æRJ’º<À‚ ,X`Á‚ ,°`ÁºQ¶c›åüöw‘ˆ±^:9§¥ž³†°`Á‚K`Á‚k�vgØ„6­³a…6­Ûï3Óz&+fžaw<­² <,X뾤œËp™ +§ÈåËî†j–ÔMJï>u<±À·¬ÕÞŽÛHw^à�›¼†~êpÕeû Z�w7„ ,é]ù÷úaå߈3 Ã,X\àÏè¢cßìíü^X±oöömªY°`Á‚%°`ÁÒî´O¤3¬Êö¬t)G´J†!,X°`ÁX°`Á‚K`Á‚uy¦ý®CÄ’Ä9¿ë`ªY°` ,X°`Á‚%°`Áº>Odœ1ë'ø£#IEND®B`‚statistics-release-2.1.2/doc/statistics.qch000066400000000000000000060500001527311104300207560ustar00rootroot00000000000000SQLite format 3@ ….zq  o–«A ž $ § › 3 ½ ? É oX''qtableMetaDataTableMetaDataTableCREATE TABLE MetaDataTable(Name Text, Value BLOB )t ##�/tableFolderTableFolderTableCREATE TABLE FolderTable(Id INTEGER PRIMARY KEY, Name Text, NamespaceID INTEGER )| ''�7tableFileNameTableFileNameTable CREATE TABLE FileNameTable (FolderId INTEGER, Name TEXT, FileId INTEGER, Title TEXT )t ++�tableFileFilterTableFileFilterTable CREATE TABLE FileFilterTable (FilterAttributeId INTEGER, FileId INTEGER )f ''� tableFileDataTableFileDataTable CREATE TABLE FileDataTable (Id INTEGER PRIMARY KEY, Data BLOB )� 77�#tableFileAttributeSetTableFileAttributeSetTable CREATE TABLE FileAttributeSetTable (Id INTEGER, FilterAttributeId INTEGER )�33�/tableContentsFilterTableContentsFilterTable CREATE TABLE ContentsFilterTable (FilterAttributeId INTEGER, ContentsId INTEGER ){''�5tableContentsTableContentsTableCREATE TABLE ContentsTable (Id INTEGER PRIMARY KEY, NamespaceId INTEGER, Data BLOB )x--�#tableIndexFilterTableIndexFilterTableCREATE TABLE IndexFilterTable (FilterAttributeId INTEGER, IndexId INTEGER )� !!‚ tableIndexTableIndexTableCREATE TABLE IndexTable (Id INTEGER PRIMARY KEY, Name TEXT, Identifier TEXT, NamespaceId INTEGER, FileId INTEGER, Anchor TEXT )h##�tableFilterTableFilterTableCREATE TABLE FilterTable (NameId INTEGER, FilterAttributeId INTEGER )l++�tableFilterNameTableFilterNameTableCREATE TABLE FilterNameTable (Id INTEGER PRIMARY KEY, Name TEXT ){55�tableFilterAttributeTableFilterAttributeTableCREATE TABLE FilterAttributeTable (Id INTEGER PRIMARY KEY, Name TEXT )h))� tableNamespaceTableNamespaceTableCREATE TABLE NamespaceTable (Id INTEGER PRIMARY KEY,Name TEXT ) ààCoctave.community.statistics   =’…úôîèâÜÖÐÊľ¸²¬¦ š”Žˆ‚|vpjd^XRLF@:4.(" þøòìæàÚÔÎȼ¶°ª¤ž˜’„š|ƒšV‚š'�™s€™B™~˜_}˜/|˜{—Sz—#y–ux–Bw–v•mu•Ct•s”cr”7q“up“;o’n’Bm’ l‘?k‘j�Vi�)h�rg�3fŽzeŽKdŽc�hb�7a� `Œ^_Œ1^Œ]‹U\‹[ŠlZŠCYŠX‰mW‰<V‰UˆKTˆS‡_R‡1Q‡P†IO† N…KM…L„aK„0Jƒ+I‚H�  ‚¡P „à index.htmlstatistics6Descriptive_Statistics.html,Descriptive Statistics,Data_Manipulation.html"Data Manipulation.Hypothesis_Testing.html$Hypothesis Testinganova.html anova*anova_properties.htmlProperties$anova_?  Æ «7ûöñìçâÝØÓÎÉÄ¿ºµ°«¦¡œ—’�ˆƒ~ytoje`[VQLGB=83.)$ ü÷òíèãÞÙÔÏÊÅÀ»¶±¬§¢�˜“މ„zuoic]WQKE?93-'!  ý ÷ ñ ë å ß Ù Ó Í Ç Á » µ ¯ © £ � — ‘ ‹ …  y s m g a [ U O I C = 7 1 + %      û õ ï é ã Ý × Ñ Ë Å ¿ ¹ ³ ­ § ¡ › • � ‰ ƒ } w q k e _ Y S M G A ; 5 / ) #     ÿ ù ó í ç á Û Õ Ï É Ã ½ · ± «6‚e5‚d4‚b3‚a2‚_1‚^0‚\/‚[.‚Z-‚X,‚U+‚R*‚Q)‚P(‚N'‚L&‚K%‚I$‚H#‚F"‚E!‚C ‚A‚?‚=‚<‚:‚9‚7‚5‚2‚0‚.‚-‚+‚*‚(‚&‚$‚"‚! ‚ ‚ ‚ ‚ ‚‚‚‚‚‚‚‚‚ ‚ ÿ‚ þ‚ ý‚ü‚û‚ú‚ø�÷�{ö�xõ�tô�pó�lò�hñ�eð�bï�_î�\í�Zì�Wë�Sê�Oè�Lç�Kæ�Iå�Gä�Eâ�Cá�Bß�AÞ�?Ý�=Ü�;Û�8Ú�5Ø�2×�0Ö�/Õ�.Ô�-Ó�+Ñ�)Ð�'Î�%Í�$Ì�#Ë�"Ê� É�È�Ç�Æ�Å�Ä�Ã�Â�Á�À� ¿� ¾�½�¼�»�º�¹�¸·~¶{µy´x³w²u±s°q¯p­o«mªl©k¦j¥h¤g£f¢e¡c aŸ_ž\›YšV™U˜T—S–R•Q’O‘M�L�KŒI‹GŠF‰EˆC‡A„@ƒ>‚=€;8~6}5|4{3z2t1s.r,q*o'n&m#l"k ihgfec][QLKJ I C @ =1( Þ>ûöðêäÞ=‚X<‚';�r:�29z8F ÷÷ doc îî!qchVersion1.0;�gªä¦¯Ú¡›+vÉŠq"`Ñ»ÚzÎQÛ¦sš(�žh!›0.²$¼¢òlü(ŸæA 2Ë(ôŠ|ýñ·ßÈé·¾Sôï*9¨ìI¿~ÿmЬ£ÇŸÂu„0õ· \%�hßc, Fì´|u•ƒÝú}§�¸ÂK¨¹�†n š¹ŸýF» ˆÁÌüÛ|¢Þ¨w–¨öã¨a^‚jw%¸dÆ ´¢ÍêV©4IR1™[¿ �ßmÙ{òãù$�uv~“.ÛÇÒ‰kÊi_‚fù€}�öÝ�–cƒ„NPž‹ÓTocÀD =Ô&JŒÜ&‚`¾hÊlÞPÞ³¿@nõ Üîü4È’9”ÿþüâ!­ÆÚT¾W%çv_²Õ&u`ûø>NÀFÕÆÁT@¯QßÜ úhæíAÇï�ÿºay�ÿ:½ÈÀ5®!lvYÝ]³Ä¦çüªÌú˜ùü±Ý#iS”¸ä—(Ì¡ –Œ=ŸæW*»€ëož?¹6þPÿkjoñ§ðÿÏ>]¯nž%ùµ>™à3–hù;høy’¡ûô™ŸŒÍ �Áü�?-ÐyAn×ïñÿûr¶3/CG¬)(ˆÄúÖÈ(4¶ÉÉ+Ó_fW¯Ÿ—ó_5·1ûý2 £„n¯É_cƒqY7â2.íÈ`ø øJ7¯ÐO ÿm­“×Cnöå|a5Ë< ¢ìÃñ{�¹PFCÏu¶)îT€ÝÅf}]¿} —î>¶ÄMç<£¨S¼”S�hB£É‰b�¿VÞ>׎›òËè“:•f-ê¦HruÓ†)uš<`\�,ȥŇIÇRœ.ØFþ]Š éJ²wMpî½ú2þb6þþõ�~Pƒz4±¼®E M�ùøjó8[àËr`�õ¸š²ÂßK¤ÕñF\ß[3©ï¯0 ÉVÉõ„ÜcºÛ4Ç”y>'˜Þ%�5½’¹óÿô³uÞ `ad p^¯ÿ0 Û0" ÛÐ8Üe 8š�û§}„xrîJgoô,Xó�0Îb®gvXcä©Z›@QX…¯ÈiÈ„-}\±p‚Dû2Ô™ýKÆKYh ùæ•—PÇ~*J%0{¸w1Ž)nÒß�„‹Íùšžf½PEqFxàjGRߢÀ�`Ç"?qŸÅIì´5eœgÖ“e¦REsü²Ö5?9?h±¤EùÚ74µ^®eÞÒä´Ì~H™ÃÕ˜9w9:^™§AmHN@ÄìÕf !L>aïfŽtd%pDy�9Ñ©ý€àÆ2ÉlAã´¾`K Ò€�5�U�YBCϘ …L¨æÀP—¢©Ó YEfºdK&æ]V3) û„™Ö,€šÐ?›†´5"¾SP†œO–bÈ—Ú_·£³ÁIÀ®ObuÇëÞWó�`²Þã&%²XÞÓ¿‹w•�hãàÁOÁ„zÄ…óä?Æ@ö¬Ë8I?¬"��;àÂ\é/äÑuþ×Ú%2׫EŸì èœõ OÜ«0Ø%É�hb+DüJ &Þš Þ¨-Ï·C%Ï«œëR  â ö±3¤Ö Ä…:Î8„Që*Àl‰8²¯Q9íWÎÖk÷kßiÈ õ 4¢Fa«º·°Ò0/%½¢û7¬Ò‹¶©¯ñº}� òo?}þúËmæ}ºÚ±&ˆ°,-ÿÖÆñ Á¼˜(tz•®�Ld8ýw¾¿ªÜÍÜQ1Ê'ï¯Þß½A?—ïý«§t÷ÉźۦeÉ€6/ÙjæG·~ì�ª;:?ɔ΂õ×ÏáŘ+¾˜&WY²Üçi€’4fE/�Oy¡Rœd¦Lk7}€NfÐÓÌïU¯¬ëÉM9#™Uµ›IÃu^À'ÁbYá7��u„§Œœ™%®ÚûúXúÊ:˜>–Æ(CUÔ>Á%é>Ÿ-ºÏlq‹tk)ÛH� �eö�=™ty+4È0æÇJv¤À8<¯ëI0[ô¼4/+ä×Áq"Îð&CÃ…º0úðÛ �@}2»9+q”3ÀÆ£Ù¢qkìÌêÞrîèÃrÇ”^U0_[º?¤¶qXÃ�jmÔå‘ üåÔ&ú¹÷´U ñÕla|l5I,.¨ÁÒt“ÄÆ4ŸÍÍ{£Õoµ®ù&n¦Ïâeªá1¾Â]“ �-+Ò�z°ŒóÝ]—ñóÝ�—Ì)´?¥¼PÌ*YázM›Õöš÷t«M„È4®„»Ñò’m”ì‹t_xø…áÞ:3ÂÍ*ŽeT0[ÔnŒ‰K+ëCq]á4#®…�cÞàFíq_ËU<$Ùý$hEæã†b%iÙm—‰œ FP¬(~’'fˆ´Û1¢HÞcªZãÂ{£gâ©­00d§ÃÄš0,Yš:â,¢ÜëÞtÜp({ÂXÛ²Iœ«h*A#wVD Ô’„‚|Ô*à0¾L‰G©ÉªIж>;Á‘°³¨Þ…¿Y£µ~P¼9¸wýã›Ï²Zœ9æïêÈø˜î�¨Gm²d �²I’8±±™1†ûy{¿x÷RÍ] ƒaúè¸Á-+â%g·Ä‡“�òÖRgh×±*�êñy5œ{ùAv·ûÞ¯mª{OçhP70¤ìxD:űJÉÚ.©íêa˨ï&!ÑÊ-›®ÜF�z¥ÈèŠji²”ýO§8míj:5«S�©)²l "sš¿V#Y¯‰Y�Ä¢Fœ"Ð*$ëU!¹¨ X�D…8ERª�ìDõá¦yÜþúùkB‡´$,õ6T¼¿¥ì©æW³Æð[/‡q�¢ ªT¦¶ÇC0E½ä+Ü=P2azQxÿNÕ㹺º–=áÕ& ¼òg°LV´hj ´šT,^ñr:P¼Ëã[q¦e¶hÜwÁ·béâJ¥j#5\Gó2£özíGE߬PÑ­1šÅÈS´r 5aè‘(Êê>‚L�:[ú¦œeÀYwaùº;ã� À¦�¡ëhRçªQÉ´q;ÌÏ~FduÞõ°íƒÞ¹©®û~êf³bª5‰GÕþö9î54�O2Áø¾œÁ/÷n®õm7 '(G…X „IñÕÇôØ(ÿ�Ѐ‡·ú‹×Á�öiL3©Ç´Ê4S:íRÒ*ÚÅ_Û_J+Žœä*ô—*,}E/£ ýÏê§”[€–Án�fRÅËÓdìã#�Íè’ˆÚÐ)Tk1Ex«/+"%Ë­hËâð +zÏŒ¨m¼£gèAÀÖöθÛújG ¥²€ÂãÛœ;¹Ô…ŠRœˆ¯»ªf ÓSc#ÑûBëHȺîøòÛŽ¯ ãr�χrèÕ~2fm@&ޯɸ~–AGó~ ëdRñJ&?ya§·�_í±�ÆÕ“ÌÐâÍM ïÍͰ��B\Öñ/2njÞ��'£©/i1`­V9 ßk í z‚ÈT!®ž�bªÁ§ä€� ¡*dp3’yh6Òw�ÕïÆ€2�ÑN‡?.{¹Ú̓Ê8NtaJᇤ;*Iãœs–<Øp,Ÿ`˜³Úg9ÊS}wð!q�¡Û[»Uk­b%¡'BjkQŠqócKQïÍLehö�öw&üï{{¼Óð~x÷å� '>oãÃg&\oº³>Ë}§Îý´rí‘›,ôJ� �Î*?¾ùl �ÛPáѱ8�ÖÐe2¶Là´]azj„xl’ˆøúÙvðá­¬onŒ8јè<2!üyÞ2²©Í…í*mI˜žöÄ�˜™vªùÐ̼2 L€ëÆ„í�1BÃ:5¹ƒõ¬¹ b=Aeý¥9€¼ámž†õ«îÚ ‹±¨ÌV’6ÞÖk³"å³o ïÖgrFÅZÔ£c ÉúŠÖ;Dj†É®ò:‰zJÖ7Ìj–ËÅ칆]uõ¦oœÓ,Ì„iokîõPÓÞ4=%°‡¥¶ `š‹†2&¸4Üé<2:½‡L]�E…ƒ,òtŸ­¡asØ qòQkøX²2äyœm¬# û½B“ SÓû¶×ðN gÞ 0Ùtä ÖôA=ÃcÎ{Më¾$¶’õ©{©’ºšPþ}Y"wrV™Û–·øã[#ï™?p»c õ'²âˆHÊb�":¼Áz;šÎV„c¶\YЉ6�)óJ¹[ÞZ@Ûü[Õ•Ê7e]Óü¶×ÐV˜ Ó4wxÞn£�7%ÚÆÓÛf}1õh˜F�Ø}m¦Ž]¬VyÔ]í§u,ý¼�eY£�¼U®u_FûiÝQ/ªÔ©NÔÞNÌÚ{�è¶n&‡ÁV™Y�x½Iêp8ï¦4Z‹Ÿ®'�üéþ9W`gƒ[®çå—œ+Ø�õ‘¶÷Ѓ½p“À–6YË;j…Ý: ÷6Y_m³ë¸½eõ¶�±³BüoÔ� îgJ�´i¦øDJ*î¼O“×÷V÷ƒVöVÉúŠïg !Œ1jÃ^J¡ƒcð‡IV Ù2»¨ „%BÒ�e± ÂδDÆa„]t¿e~ŠgP—ÁŒXÝü}Pbx°§Þ<ƒŽ$²,ìÐPËÀ$Ý)Z�¢©¾Ø[ÞD‰ÓA‹°yxŒõ2_áùu|5×X&ÅÓü-…pÞ¦>ÝVßAA|“¦¡ø\dWù›Ã3¯ß¿½}÷æfȉ#m`xÊ®–—Ù¢~×µªpÉ9½È#Þô…€R ©—¤E€Äûh©(¼_ÿ°¸=k„{ëóN­DXc�ÂÈ…#y1Êõ¾:|2%#Ç¿´Ba_\y §ð0ìIo½T ”ÅÃkeÃpÚÆí…ÇÉå@Az­ÏŠ<¡ÄA¼‚º†b*œ ©ßãZÚÕSãÀNÚ1b«ãm:L*áêT�pOg*Ì|íÛÀÈoÔÜÏbX5D‹òZEìqüë,òq3¾›Æ7óÿú¥Š:–»]q'!_'Áš±eD¯-žÆèûÖ¤%WZWû†ßÏåãvkãu9«ôfßn„¦ÊåMâu–l3ÿÒ®CåCM®±_j?9& Z3à<ú…Ty q9Ù<©£Âý…‹¬Ôn`•4¸¬hý鳇 ó8ÎüÌ_úùŸJ½*®3ëûÙÂþβ¥žëäfu>œËïxO±iëélÑ~b‰wééT€yï•¿Möá:ˆÛܳ¼œ-,/Œ]4%�KÚZoWU_+AÉËqà?úinãÑlѸ5;fÒ:ÏxŽæ,Ü©ê³kd•UãÙlѼïõé®·ñÑã(nƒp—ìŒÚŒ1½™-LO�ã�2��[G {Ù‰âNïD‘³}ßÞ "dæw>&f+ZØ‹jª×kG6z¹Bý‰×ÐÖc¢ÖAñ>à¥?`ÖîŽéwLë]ÿ¢~W¨ ~FRà×Ï6D¡ˆ·tß:Yíé¾Ó±ÞÇ1—›Ž¤eÒl‚qzo¥ñRã&G݃$‘¿£ÍŒñbÞÒ^øq² r9—Ò{¯)k&�ž{œˆ‘,„%|<¢·¹.žÊúh.Ä“—Ö�<"³Õ«–qe·Ê“FS;UÞ¸”–ÚØõ�w|ä#:[BìÇOŠÍÝX'"z%�zϧÆê¢]*;ȈUŠ÷f¯×„Ö•ùñVéöU»±¶0<©¼j[8ìæTç<”h�µ:n@8²ó®¤Mø>¡lµÃB”Ì*ñÈp�å3ñòFÃΩÙAˆþ–[QÅŒ±û|¶è>3z¤ÊTvu×.ΚˆêOf‹ú� ÅOuÓü ¢-¹oÑò÷~K²pí]ÿtsûÛ³ž>ièÞ}?¿G]móÙlѼ7Æý¿Ÿ“guÓŒþÐ1~øt÷žÎ ôñ?òØ+êð;“�þ÷ÓÀ1>fþ¿–(»2pm.x¥)3ì¸' Œ0ÙàA›áÙ£Jdçò—}âš…�tºtý_>>ó"Uìío”ðvP‰m3( OèHv¥³OW´$jÈ/±¢è·rþ¨ö`¶¨Ý˜ð|9"Nƒ±Ìûj^¬ž»oj½¯é™æ[¯|;HÝØ>-à £—Æš4{ à%û�j.qQ=dlÞ#Zçðq`hÊ‚‚éX>lSõÉ|üMé=¼AA?(>¤�öš²aê]–Pg)ÒÃ�¨¯’%× h³`EjêŠïžY95`GÐSà ´XŸØ¡7•å3ÿêZêÏ>]á�+ó‰Žì]FÏ{ ÃËU�’ÜŠ�¿v@i¬]÷Ž$ðP·À×>Ç÷1\µ~BŸ]6Z†1ã|µ8å„ H@ûºÈx¸ët–qEÚjRo(FóÓ炇ϡ&‹ÖÍã׺xyw2Yeè§Âš,i çäm¹ ðäÜAïj³ÁS?ˆèÝý,ˆ°—$ nnéU�ßè+Ñ÷óMÓc&EŽ€ P°ð¿y8�“cEÆ¿À½Œds£ÂT›×ß>½{ÛUV»¡Ã7$;‚�U¬ñ°˜ú­Ù®,ØéÇiæaâÓ>Ž”X®þØCé?Üà‰¯µ;£Cµ|_²fÌñG Ð!¾› Ä} JÚƒÞzøš=Þgª«"�qÿøoBQÌK7·Âs„“ÕŽ•«†œ*Zª5ÕÖu1¿½ù0dƾ)« ‰:Þ¨õx¶è;àöxš´4àö™IJñTxÊÔˆÈ$5öëo_Þ=#;Oànl•r°W¼ò(‹AQ0º>S¼CñЫ)ºof ÓSs-·ê–ÏPÃ`Â8b@¯å®?R·Õ¨Þꦺ�¬&ÂQ v�}´×º¼Ç®UÓSjV>ÑàþŠÇ züì>9Ü‘æúä|Äc›5l|DܸQ´V˜j¶(I#—Ëízs²#›öêð‹5ôžÀ¾² “ön*}ctoó;jÑúì\jíl™ ÜÓ„Œ× 2}côýð±':CjÚ‚ŽXx®ÉF\šêáϵB»á¢;óp0§"®Õ…¶Í© âC9–�ÚóW~ÁìÄ'c«ùF›ÞÝâ¼_QË=ýpPËi}ˆ‹ó{²‹ó,^Ïš²Œm0;?Ëü'#ïÿR›?AÊhû’Z°Ã U_&LÑŽ'9g M­†ÌÈyÆ1–IQ‰IÒS¤zT\”S�À¢q‹qç5”ü…t½ÑgHr ÓgPuIšÀø{þ“O¨Ž‹[3,U`Ƴ55‚Ù_ñ'î™ýnv ޝö à\V‡€šþ* ËlÅh½ÒÐ[��p!�+„Ù½a—K ÁkykÖ(“ºC_d*R¿âR)+|s’¿ùµ¹”–Wf¹*Â�~ÙTIç�€î<7Ç›B"g(+í?€çRë’Oyl=º:K­Tpgw¼¤ÝtÝ2vÓ+�tµŽŠõ“nnÐÛbkìæRóKcD¤œÇ”ÔôOñ³ÒƤO*)DO sT%olþâ¢8¼ÛRŠîKß}aŒîâUÛŽ�þ’lÉÕcÁjz-hM¯Ì�-[=›é qx´)E…ÛôÖ}º‡÷µQãû z÷�¸Ûö ÜǨÙaT¸ /m—¤® ×Nw¶`·¤ð–·Æ ÷ÚAÒŽÐßø÷þÖ� tÓkÁmzeŒ\‘tÎKÌ‘!J_2]‚ž$æ�E$géºú¬µCÖ™Þ8m§åŸ·Ó°�Åí©ôxÜž¢ßµ ›s¸*Näyb3 o5üîóÌ%s…ö›ÿâ¼·®éµà5½2û9�3ÄÁÊnmu_j´�F¬”ÊÒ[rŒY�v_ Òî ã4;oâi�‘¼ø»[ c´ ¶'äö¶àa|7§ÔÞm±ÇðYh•{ß]•í;†èm÷¡U³˜H™Ì/Må©RºBþ·^ëÅðV0ÞXf–¡çß'ûÜsl¾ü¦àw¡­ó1¼à†7Æ-J8Ù(´Ç½Ýƒ¢('­Þny30’»ûI.ûRþ&(f !z§ kS…8‘‰ŸêG›(î3ÉȃL>Ú‘3ý0Ãà^1N¤zÍ–‡�( vI²>&°�˜uL cÝqΕ6—S¤Â=¡NáÞ9på"|Lôr�¼»¶‹úÊ-\ÍÆüt!ì8ÑÝÜgó“ üDzñ”*ïïŸ>¹E*¬ê$VžÜ*I¡Ã$F–¤}« N‚[9 "³g¬”)]œ)â†ß×akP®@¾žR}M�´;`�)³‘®Ú€ªMȸD)ücâ4žØÖ�ˆ}|u&†<Íâ%³OˆSØç’bs€¯®¬øš,Ïrô @�*šmw²éJÚöõ�~}æ´Rù§+Ú³�.ß…Qi–Ó�}ïƒr]/®„æ ¬†\ãÝ3¥q¤ MeO{cšÎk®vã×øõ_æ€pàŽ*=EgE¼=ª‡O3žõsØ©mE)oOÖɆøwèp#Ä�yªI'|ý‘¾æ¨°²¤ÿ?²v1q‘.Î$sWÍæ:É]Ì|äë)<¬ár+ˆ[â¡쬠=‡ì,sQS§ðñ|øpj”˜)„f–³­NA25u Ï‚+ÔÌ;ª3o~ %OÖˆgÃk&:UŠa9�îkÉÇ“µâ€Åþ=» „p ‰åœ²S�Ì=M�½3àZ,�B¸@Åí¹l‡}3B^ u /; g¦2á¬Y;o:ð=á¤P'qÒ1ª§µ‰šr%•zÂÜ-Tæ`IžÂBçÀ2ÿ‰»g!\q°œÁwŠ”9¨©Sx\ÁJ1™pÖˆ%°À)Pa P'1Ð1ª¼X² áŒ}9G;8ÊìÓÔ)ìs�ªYòÕóÊ@‹Ühqå}wZf«§Y‹}p]¢ÝÇÁš.„+}¹Æ­!©«phá±KYuBAb~¥ðàV'è(—8YxNW GA ˆC‘ƒwLë.Ëw'Ä¢èŸVa«õ†£âS�j<ùê -ÀbÄúëéÑ7-(0Zbaa¢wíåerc�abˆÀ èD� »TóÌ&HƘï³äÄÂÄÄar2TnÃÉEû01l ì…$+UÄÄ É�Ô�ˆå¢«˜8a!ñyd-ÅÅÄ(Ys±ÕµÏ2:&¬ ™Ë8§ËdÉÇÄ�‰Ò‰°Až©æžMظÎ/h� cÍ]<[ Ó!f£«¹Eê}ÆÅ‡v);‚«8Š«=ä*.qåïW»'BY’ƸuzyILÔ°¯sìG›S­šÿ >‰g¾¹ÌŒÊÈI:NÇóõy:rŸU³î²Ö”Îj¹’ãÀªøî‰øÏ÷ÔÀùj®|ºýÛeZ6 I‘¥úkˆÎÞNB? ý÷¶mBõ ¿½ “ iÀŠ ‡£t¡ûZZ㞥N5®KU(5ÙŠ¡þ4R‡Jׯ³„1ad�^?ù‚ÜX¸˜ Yáp€ND ²K5ÏlBeܾÿ,R9±H11BžÜlŒÐ�j�²¹0jG!\L¦B-S}��#.&W¡–+›£ÎºKˆV¨Ek¤#¬át+]±–.›?£:„âR²kÙèÑG t$X±,›OÃrtÇ™Ä*Öb5Ò«áh’&žˆ™ÙXc=^ã̉˜5ÑHÆD5Œ.Y#¡ð‘5ž˜ó�ˆ†H©@ë«·´ó×ßA*ðp¤jU$Y» [p�Ve!Ui[=Ðà×?h “Z-†Ôjå*¸� .¦Tp¹ÃºÆ½Š¨²…0.ƒÖ{]¤^õ’B è%GtÑI"°TóÌ"{æ3‹Î!e‚ÄLˆ]�›Í¦Zp–wBô®«_^t1"bÉbb€du·�º�ˆA~©æ¢MÄú“:‹¬AV,kLŒ�5Ç›qµp­8B–¯Ff%ñ ”hùþCÃe!‹àj`Èl츑\qü,_Oäñù%tÅá´|#ŸÇÁ;ØBÖfbõ1³¸¨À"°Å`��OGîFb ‘X›…8€É�ØB$öˆ•8Fb‹3Hìã+ÙGÛ²Ê;Ç­^tRÙ�¯DP®°ŒOƒ{åJ:_¥š¡Çåse[xiæ¨Á||%’ù8rõ¥…³.ÅF½,ŽL˜¹w9Â`ñcb�ø]ᇙ¥š_Vi»Œã!°`11J°\;;RžƒHm3�ƒC/"N)OB¤§ Ò6È2'”ò„Cj›nè?`õb”òŒC:r¾¡—WS!á&¯,OBY%[Á^F’03&¡ÈÓp„ND sKK®ÙÌtîYä 3bÑj„t9Ùæ·‰7Í%™ÂŒF6ǽˆL!)!HÔ€.D �¥šg�2U|‰B$PBŒ�'7»·à+$„o|q $HHˆ!âÔÝËØ‘à $DoÎ/6�„ 1Flìõ܃['“Ða#ÇT_Dh �„& Ï… !¬TóË"H¦c½Ï!H�Iˆ‚äb×ë6˜u&‚´¶mÌ'S^´~*óãíÅdj�‰L­nΗŸ€Ô�x­³TsÑ*^ml�´u&’¶¹I_—“…޽­6_kw×ñ‹ˆ;VºUOÁèBÐØ…js vQ�]ÄØ[:ÒWêx_ù¢�Ç||5‡LÙåþ2òò@�¯ƒ¢Ï&Âvä*À©0Ø“véùÅ2ä1#_Gª�ó„vÛán¾š@~ÏÀnÜâ&m’ÉŒC¸ù:@&‹1�_Æ!Ü|=³ h¿@ôßÈîß ž¥A%„ ÐûÖ—,D’%ÄÑZ�êBÀ`ªyh±Ï.h†$Mˆ¢Öá£[�Ûh�³Åz¼ëœor1‘Ûh‘ÝÑ=ŠårB·ÑBg‹ç0ðñb·Ñb72‚ÃÀK‡‚wàéÆƒm²ñW`Àç WØrÿÀ3�‡�óŒ‡ÙeDëÀ�Û4ãE9E²t9ËXqÌ¡=ˆÿìÁê>ÓÇ ]DY=ˆûìa¨÷l8:Bô Þ³«óLã9»=ˆóìa¬ïÌÉ™U-,�âL!z×qçÕúSnFDÃŒr+΃Ÿ-à÷x™¯ƒ9—Ä~èýàæEÞ×,Y©<¿òà'¼ê›�äòþ÷_žó� þñÿ™\$YLïp-½iiút”ÂøÉ è­ çyÝüUmá|ôñÃÄn ¨b\Hg:M¬z9þ8±ê·Ì <¤*‡g|\R}µ–—¿¶ç5V†:€‚8@B�ä$(¹ *×\j$—œž�ÓA·Ï2A'ÔHtÎUi#ÃÝþ™¦Æ!srÖÁÅ»êcã,·ØÇ3:Úµ¾vŽdŠ¡®§@I܃͚e*ôÝ¡¾•´¥»jäLçúÍ�ö§¢QFÂ×qõçnoöªÇT`11—›Í½Û¨6Œi3Ñ9"ÕÛè¶~Äø„‡púÆÂŽ¥ðžkÄ‚­P0 5R:Ý·âT4^z‚¾sºß†‘Û M¨qà\D~·1a¼0cÒÔ8LN¢‡; ‚•˜ š jzljF›1$M�ƒä6ö¬�®E°„Û™ž1>¤5+IˆqP¿»‰1è€Hã�—Y83ÎÐ45ÝÐx87%μ Ã'©§"}XŠºbΣó-Ç=è¿e~ $“ÿ\¿ï=׿?ÐiYYÀ3t0îû�X.Y®wjL(tÐìŽ_ð„Ì#OÈŒ­„&¼MP`6x¤Æ*ȱ.CN–¿«U1 ªÙÂÔjŒÒÃÆ–º`Xäß+昦þQ,‹B BÕ×j"ÿ1ˆöP÷* vI²ö  ÀJV£wiîä¾J®& oò§(-Ú&3¡*XÏ’AÕÙ±%ã·�n‚ãÕ…Geéë?¨Ññ„!Zyx8ÅìË6ÛI¬æÏ½â!�ÿ4´È”šWã´á×0)@ÙoYê»A:O'æ"ÞL=ä)<œ-j7=�ŒÂ7œÐÔZ­76C[û…M~,ï›Ø–~öo³þ7.4ż}ïßÞ{�ÂÛBEíNÊaÇYìzòØ%Yð'ô®0Æš–]À|æk‹=NRuíj³{Œ¤¼d+>ÌOemuý&,"?þÊ�šLØÞb’9§iÔ¼¿QŽ›·TŃY)÷"åçûŒŸƒˆï’õåЂ™<®pð eŒºÌüdYòèQšS²‘ŠaÂ( Y²Þ¯”䃩Fg�$g#„%‚4 ý'ld*Jº Ø{b4°¶×Y²¥ÒŠîiU"V¼»@e~¶Ú¼eûÙ“· ÷9j²¤a4"\ຣîYS½Í0œ2\#ÙÐu"<Έ—Ì9 úá ÊÆ›œ£¶ÁTˆ·e”‚ŸcßÉì ³WêÀ›p¶~¶ŽUVo¯�G&€?r¢N›…ÖÉÑ.9ÈÙš'M70l¼ÆÖU+Þ©h<Öð)ݱ˜—doUÂ!ó®)í³3w([ü<28ÁVÞôâ« Ù(à)Õû6Kö)XË'œ”‡´Û„[+A†x<<ɰ Õ£íê`$*I!«F罃ÒF~:[aÊù'~Å€ÙB¦ŒÔW–<äüNð&ûM0ŒZØB±%ƒ,�aÀ‰ $ €�š]ã›*0èÒ§²—¥]¿zÿ¬¦;aŽ©JOã/†„U�VYRMåPOY¥IÞr${ð‘Ñ( iƒ™.ê‡7µ[£d‹ž&yÁn:ν}Žú‹úƒ¨ÒÑ@hÏ"jùšê«ÃþÍõÖolðž÷iÓxâ Â®JadxUît+íµü(:ö•¿ÚAõ‡û(†�Þø7<“ËÔ•༘Z�fZóa¨ÂU’dëF2�û^-£“z”:ÔB�Û¼ÖYr•ÕïŒÕÖ0YæuJ�˜¤)Õ&´í4X©�ÊÈÇå{_¿Î Èu[–†„·²…/HÆBõÛ‚ƒ¤÷IdÕßúhh§bÙê¯$Ê—Œž“ªò�?4_û¹õŸÿ)ÜúcïÇh�œÈ*逨h«Ýôwöõ^+iµe´E #eùÙ6ÀˆçR‰Ž—¦<±zÛOŒ«¦[`ëvA£H�Rµ£�‡jÅХصåÁúÑσp—ìd 0KúX¼u•R«2À#½€Äà|—<�e‡ãþi™üÁÏ"˜UtŸ±‚‰Nœ‚$DKœ¯ýÍñWJÓm‰“ô$®m Ö%DÏ0‰{Ù(^Z5ä/âL 7­‡ ¼Üc¸é•ÆR¦ò|ØÐ=ìù fîÛäñ2ßÖ}ƒsoݧF?`5¢~Žq±œm5çÎu 5œ{ƒÌ–zÆbKZÀÖß6×ߘA‹[ƒ\š´“ëð]Õÿa­wÁ[6ËaId.’Õ8¨²ÙO*©è½Ÿ´f³T’1Y³”Æ$æ2vÇ3¾A·jƒ¢­d‘W‰®jïá†Ñ»úéî ;V?½ü2ȧ ?��¬`™"èíð0œ’6®F|U 9¦b5ô1®²—Süwž&ñµ….Å+u'ƒRq0ÿòéíí¯ŸÅºÅ�=¨ •³ª×–¼ÝÝÖTæÊ+8¨ðé…÷}§r€à“+ ö– ”úÔÑ |<Ñdö‰µ›ãŒÍýƒ:�±ªjb%ò#Ø<Ð'vác”“rLÒfb�.2 ú=h•¹ž;�”o�ëNM<·~Ä~�ìQÞqÇ(Ž%þ:‚W! iޑÇ õÞas%‹Ï2ï[Ùyôc-kXúã-èoè+%3´åí ÖT¦ÖëL’�¦¬„0ZBðJå÷Osœ”òÖj•àè:(—Mt&²ð²C8%.µwÂ[‡¡Õ@“£Äµ¹+Ó™£Dþ}ù4 ×c~˜¿÷>'kÝ7ø‹Z\Öj¿ VÞO*‹ ñšÁ4 a8 ¯ÄÆíZN}<Ñê dteñ—дIœãˆÅ¶(°•vB[%½æ Eý$†ŠIYèÙ{ì7BRÓÂ`m¿*öÙ�6h€’k(¹ŠŒ�@^"¬2rÿw²&y‚úÊx& †ÒÑ÷XìZ#£²e¾\&ë'¼" ÅÕÿJÊî1R½×ŵ‡wO�ÆžYM*«Œ»·/FrQÇ£ >üLwe Ÿ&Šv z3$ðÓ­cÉ8ÿ:µ#ýëÔ¼@Âu;§Œ�v3‡E¤Ç|ÊÄ¿ç½Ã‹Íaß�†DGÎÍã�ª�sBÈ<>Jd-ƒ'fYr‰F”-÷?3 Øz”¯|ºÕZì™ ,èd ظ!K7øýÚVNÐ§ŽØ\"èaXMMWÌx'ç�Á=Ð\’CN|•Z,Ä×C�¸fõŒŽoš¯géŒM?ÎÔsMÇAÐ[pŸY&Ux¢ *D´)¯“j }|ÚL”êU2N=�™W9|53çÍC:ÈÖU¿„�|Yx ƒï*Ë0Éœ™oÎÊ”lN°†çè&‡ Úaâa{¥R¬IåuMp@Bm(®i ¾=ËW‹d'ª¡ì�ÍQ3u]�¾iÒj¼$ß��VÅÜ ·¦»ç£“ñ„��lë<�–%~x—¹Ï� Ø®]¹Ò\[›ñ}ü$Êw`šœœ°`�à>Šã$‚)‰�M¶MP�Ä Q u œEÔ¾Ìδїy ë˜5:ó/®àÂ5 >zt{ @¿*þpDiòc[F¶CaEpçžY뺫õ´!ïRFKÓ|V¿&݇ôF â»�‹j…:ó¹!ÿdÖ&3ð ÛfĜމ-‡é ¶ƒ¦¨wNÑB0EHvKxÇo f `[ீâægK„q›§uó3 ì�¼¼Bݯ©Û`«°ÉÎ|°]©Ø1# ;—]0ƒ&{ˆ>ÿY¼:4Î%vBðƒæ(vÍÑUâ�Û°÷u(ZÉ㶬d×è® ’�@båHìïV¯ a@révAcUj½¥È¬w±Œ¿*êY3¡˜è”I޶ ÈPe�–H«›Ô¥IMoD¬ìˆ"‹á@ABÉSÏ.BÆš5T’†(® �#¤©gŽ+Y– ü,Kbï‰g-CƒˆF?¥lNÜBLt¢QÅß<{õõ³OãEB%h›F'§î˜øÔÈòP:Eö�†€¿M›äÊ Á³ŽU¼¬#æŽx:–ÎÊtƒdâ³8I2j‰ž“œäŠVNvþÅl=MÔ [ö¢ÞL¥X·âoxõ8±·q|z2šœ=|ôPŠzñôƒ�ˆGž¹î$_Í>óap…}„*¯²Z¼?3ÃûDGŸ"¦ @‡ËðÁþt�êdÃÅ�NFœ�N Ô§§'öã6Ø_Å* è"Ä]¿¢‚�¿ÝaßÂv<Øá´âÃ5K¨­=x˱›´Wç’l@™Üv²“Þ£¥÷E”Sw¸Èƒp<S&»¿o\ #ŠگڳéŒäþ/1‚Ö�…³j^ôÛ×oh_ÇPÖÝYGÂEŸh ë&=Së÷ `!ui‚Ú"'‰Œ±óÎ|¦‡¿˜ð¸¿¦ }å¹±â“H*_ѾIÖ‡ä�•·^®`ì¥�ª£YõKó\ƒ�¶Ão¡¶îÉ—×ÆÝªJF•3žÍ¾8+q÷d�I)ˆµöDƒŽ‚þÜ¢�/ðÊ·œÀ¤}áãê7.çl^SB2í MRíŸÚ CÖêìþ°5]–W.( EÎî.ÍYldR3´²l3´àFÍÎ9ZTÄ&A²—"O¾fëFCžKë¿“ÕÀHÒÃÀÓz•dùN ¦Ô‚’‹Í ­ßuV{–š*e2+�Ú•9RÔ�»>&v‹zØ=Ëæ¤>6¼xŒÐ5^Ø÷+ÔNýe ·d³Óòý7 Ùu/À3]ˆ3¡\ãñ]¢:H.ýš�Ð=‘,/²Ëu¹Æ¬•‹‹*½Êl�x$;ÁûõÊq Æ5…@×}”¦Çò¼]2gyh»U¾õšDÜ8Fº„‘V¸Xc¬¸Åk“GB(‰ÜkÆÁèŽ\-qã°S—˜›ÇÏ»÷5Çã]42|´€ÔÛÞCêTMž-1G)Ÿãˆ~…ÉaFkœš¬Ö-q@÷-«•>Q¶K Ƽ³óN[;㛜Ä/›õ à³üÈOÊ"«J÷➊#n–¢~M´g0­4ÿ<ñÕ9eXì\“îS'aAN�ׇ[-¯æ¬Ê×ÈŠuÖlDžšü-Ê=«×hú²?�Tƒ¾Èîþ´)ÄoÐS;œ¿ئO‘ Ìà\Ç£$Góu³XÆÏ^Æ�ŸD²µä˜«t‡íú[HG¡³‘ÌÝÁ|Øô*)ê9î-okoru<Á145Ñ)t³èAžcco[«½í•Ý<{‘»�WAÝ£øæX$�k¸"ðÍȨüH1Xæq¡……¤o´ÂꚌïŒtÍ›)K¢<9ØA2)¶À« çK�3Ç!HÏ%“GL ”QÄ”m3S ½¦‘x"eä�.º¾…A!ŽNhA•v'*Yôá«bnRC(¦/ÑóòŒ-�hD "yªŽï×¾e†6½þyö:%Q‘® l�K�¯q�Â…%5Ÿ-ùlM7>v«ÀQ‹a,’#?l5WM&2/"´€¿¸[’ĉ®ãTƒmBïu“äc_ýSîæHâG�%AÈ ëçñü@Ñ38èšë1ô@+ßÖðªáÚKq•Sþ0FšŠ��RF€!LE)áf6)9žsNßKCTD¦gãÜÇkô£tÕ�)‚Í~s…Õ;‚–B‘CzÞç ´+Ë�^’oŽâÿ:Rõ�·H¯Õ¹Cš{Ëæ(†úO³r™ÂV¼NÔ—ððd49<=8{<óü“‡èKxèû>?B‚!ƒðçGJ qzŸžIÎ3F7 Ú 5±+”ÏÔ!Æg²ݲ*ì«ù`ô}5ê¬yøè ‚úèÃýü¸+x¿Æ1|¼n×G è�?;�ªá¹<8×Oòêk>GøT˶„}3€×ñ àŸ7¬$3û Ó49¾¸ò¿ø.©þÙùæ{Ï Îìå8�?Ïj¦ †‘�i�ã´ðȳõþï°•ÕxfÎY`Ô®þÀH"Îã Id¿ÒÇÕŽ·Õ¨6öG4+k™õ0üI§°»©3€ï¼÷ål¼Â+77ýy²à­ D/1Z7¢¤zzÓ*ãöU‡h�Äöpwû[²P Xèxp›[‘RþÅ&ã­At ä„XfMÓª‘bÞÝu�éofi5œÄŠ�c?sö¯òW^ýô�­è7ñeÇ•@uÅÌÂç½®]˜-ʶ€¡õþ5^¶Ñ9&peƒE‡éQ:�Íz„öU2šûÕw<êÂÃêrC�â–yä¯yî›æëÕ=Àá 4$kç MbÖrÓ1з.w€Ëu[yÓvP’àmº dçñ¦”Ùè¯É*OŠÃoÒ,­î°²Y/ŽãòÂHÕ®kã‚fëò˜€Ú�EÎ`¢ƒ1›Á°HŠ*“‘[Ém6W_‘á.([‹%Ù3˜dë“óà“‘ëMÚ¹;Iœâ�\U¨Ùîî¼åÕݲ½~å°¼D#RaY� Ü‹r]ÌjãkáMöR›Ÿ�Ù’ Q¼&Îö_ ×e93ò-*òðŒXOÉ*Ô€¦�IR¤åºîNÛÙ^QǼ©vZøg…5´ö ŠÝ³5ÕRˆ±t—)MãŸzx–Ô…D…¯)A›Ç Œzþ›TL�IW¼{B7ºÌlFÕ“f³L¿L¥#âVÇlÞ`çä£ k‚1NÍwÕQY pT^íí¨”dÖJ4àðŸ=Œ‹²Ì±¼‘¥6•sù�¢|ÏF' v/z2$ýFA‹hX>äY‘ À}àì·ðãjŠ´¾ÜÀÏMÐ?\åÜÑ“ôñn^µ&ùm¶nÂÖÅ;¥iв8´^Œä¿$º‚ù$âÀùŽPs6iãw‘Зˆ�6» /hLØjäOêûüp1Næ­ڛ毣¾¸H*wT.ŠyÅÉ«N ‡¹žÖˆªâœÄGÿB¯ûa¼$×û2þ3þ Ó¹­Ã½ * õQk·èÞ»Uà^¤pUi®OcË2´J8à`Ò¶ˆÃ‹EIvGL$¡D]77‡Äž—S`GFg\ecRƒù7’ft‘R½­ïêF„Ù"2r–8OS^¦(<5þ©vöŽx‘DÚtrUæ.¼:�¤c;E2þ0zÀÑ%ò›Ä}7o?ºe⾓<Ý8qß>ÆIúÑÀ,ýmIú‚~gÓ£O?.ZλnU-‚äȃú¾@Ýq@eŒ9ˆ¢�ý Å @B¬žžÆ’Ónì!n{ è¦}¢ �ÑäÉã“ 2â:ACêÔ�]&™’ÆÉ±_ ,ZÕªY…Õd‘¿=Ũ ,cë9r6.$@¶³ñ8¤dàJâ.ùÏ%Æä–Éø-)Ѿ¶‡þqÉ©?Dry—ÙþþÓÉÛ ÚQ_†¶-qðœk>Å_ÿ0ÌqòäÑ“ƒ·œwõ)Çqk/þÀ9åL<†.RD�´<,ÛœÌi»¦Ü'…È/ß#TmŸ|çà•s¾v¼^˜ #åJÙFWòÈBÄÅ×H†D Mô,iŒÂ&Ú—†a¢ŽË‹(Õ¾e°kÃû¹0wxÃE¥ƒ^4¶z,Ô�ez yTJéÖ{‰Ú¹­$hc±p’ÌO€�o/PíQ¡@Y¸Ð"=9é„ì9°DAܪ)�j¥Ý­JpÛª&A!‡79œÀ/U0ØR5÷™îQ¦û£Ç ö˜êï>šìÈË?Âx>eeðÅ·GÂÐÚ©ùW6Hsò¥\’6±ñ8uÊÒ´ë:àGmøüO} ¡€�1KõàvÄÅP@Ei½•mËq™h_Ë~5ÑÓ(;Q%“Kœö€Õ�êñè뜚ú�ªÇ'÷R>À­}ìêÆmÃñ›”�Ÿ^¥µ5!Ï�¦Ïoœë§~òeZ¢uÄ1Ë7û™Ž—®M׌p')ÿÆVOG¢…´ïu.xË–oaYlðN§ƒfÛÙMªÖNaƒbž'—=”Ÿqž ]{¥AÑA{Ï„"*æl“‘óU·ÕXbºÕ56!HàÝYt ÀDA ìv¬�R®*k�“Õ+±¾ÌÎß+.ê•qn�¼ªŒê•jS‚Xàjá:ìñ‹oÿöùO™7¶¨ûèÔÇÏpg*Wª3üš) G˜w$ÄçbeÕvSÅ%Ön” wU¼ƒ�Úóáíf�sÿíN8µTÜÆF¡í tšZt�d;tH³MLg’;õš‡@ ã ;”` ʈ¢scšl�Šu7Ö>€Å&È…ŽâWÑ>*žwJ�°œ•H4o4h©“ýÒ|/EóBŒ»�†VÛ£ÎnÇ�M6-üüÙW2àª7Ú6�éÄÓ„�“Èm°›£Ñãâv`z$þ€�ŠMPmòÚ HPvg"Zh ¬Ð8±´jnÉÃðí·ÊuÞzHâ‰ðrK4€h*²îÎ ¥.„üŒ�a¡ñ)j—ñ_ ¼,ãø‡I|úc÷†GñÇ™8ƒ _P¸=’ÚHIí "häÓâR˽¸©¬+lGÙ=aE[w;`»f銻‚ãs6´S8†ÖÏ>Žô—u-¼=iñߨïRÒÐÆÁO½ep—˜(¦ž1À¯uX.¢ìÚì²(ý~ƒþ™ aÔ6ÍèÉ$ÇQÏ“çÆ¤n\5Nß‘±DRšŸlV¸!(HÕ.¤eLMþ¾f^ca½�[õÃ3?ݨ5l8¤ÞkA!0Úƒ™_“¼¥‘ÒÆj¤D|2^¡}b0Æÿ•·P™eLýgéΕ90l’¢,³ÆŒz·±íH–XäÖO"ÙãÇ!½Z‘ÅÙ•Òå«ýÄt×xÎ" æNdõ=Æô½¬J ˜½YX=îZíçÓ¥÷·éÜYmHÉ�o P~Ò u Ïo ‘|C.ç ~7€š1^açq'¦ïFHÙ\äCð{‹“¸[nx`w8õ¡ÐýC¤]ŽÛAaß ëÉi¹ÄË�:ùâ¦s'çñËõà œFt¥ÑCÍ,4,V˜ú?4`¨»3¨/™²5L½§€_F¶ý‚›Ô«ãA+¢IŸô`,,/¿iD÷È%™Šdm‘ZöZð&Þºƒ¥Ö¼}S"­Œ„fC.™$eyÄ„“´ùñáK’vaÁ‡êáW()moÈoÑ6éé-§u¡ãÎóo’NÝSz±•´CB—õ\P˜Ÿ£ˆw !¼ ,÷gT&òD …2Ë�¸Ÿ«†Öù‹ØºRJÛ´¦� Uj#SÝYÙØ´O¼fSÄ`‹KÖÁ"yQ«~�¨X8¢d¥aru¾°~ ý«\Ù¢»ƒöNÙ$”áéb yd£›pF²é=?ÍfoLì„&˜aZ½)”ƒ‰#töÊTNp½Ø» Kî­í‚+Z`ÔVÆEëHà¬é€%�9ÀÛ‘™0�nu7÷�B,%½÷‡5»WÕAÒkÚ§…‰„hb,}’2£NÍ*Ô?5€™U%68à pb§2‰fªpƒï|c^ÎÓäŠzk(ž`Jž÷$§Ý…¬w4|I…JŒMæ ¨§gîêØ\`)qÕ »ðp® ¦¯tbzZÍf^:\Š6“ÁÅ÷‚9+¼mór]‰]„ S�RMñ^˜òr�lÜ.•ë!…IcãßÄÐÁ!À5ûè¶ÐŠcBB0ö‚œ|ö´²©¤à°ðužj…³.,‘SÉ„ñ£1ö’ã‡Ri­xÛ''¶©w@<ð¨�cpD‡8[‰AG\"aÚ6Û9"ìã}-]Ž…ê;uÚIŠdÅ¥’s>Fª ¼§ ûQdE¥-RKôéac]ºÖW^�Öá UÊ*2 ~n¤ï¬Då�»ÈлC_¢jMØ¢±I—ÔKñ'§¸Ð2];ñÅrî ‡ùg9ŒÉiÙž¿¶q«…#Š8~ïGþÝ$ ·&1yÃÐâ¶q8ÚÆÏ›ä0¶ƒ'!Å"Æl±l×Ù ³_d×cè\köóBVj§®ë‘³ÃRÞ¢ÕÓÆþtöÐeóÎ4ñÕQ|±Î°ƒ²[1…öH;y]¶–àûÈlÙÅžÕ™ÓRg‹IšãÚ¬Œ\S¸ ¿wK¶ˆ ¢x ÞF¼wo°¯»þvàÏ£á¹o\7#ð0âe:ËvŽ��Üht5¶ý²—á[9~¯ùšÙ°ùL ²-ëÊ Nó�S gÿÉvÐ�åz©.·â3$o¶Ï�¼¹å ÔmëŸKå‡p".€Eb l€Ní5+RþÞjÀ4ësÃä�ƒiDâïÁÍ6ušm�ñ³@0�ò8|}àL·c¦%Ǿ&g¦„ ¯9YÖ§$`ëŒÿeåë¡ónm+S:Âç^v^·¬q´nG?S£{µòßÖ9º-z[@â;}Ä&yŽG“Ãpÿù'§èbëG+ƒEÚM°•‡Ž™W¨zRñ •$I³¶HÜ“Ϲ³"‹Rë*˜§Dr§çŽSS¬9ã«Ì˜è±!Æ«WĤö3]Ñ;Û W+pKÇh ·’4«�;í�²øöŽ]È$º  äù(m1ÏþbE=G°#Ó‚/Ý8¶Ëwqx’–É¥? »›²êJl¨{bdé´D‰øz›•[@9±”’œ’:,û¶ŒC Ùƒ—i³(¥j 9èIÇ¥s±Æ /Ý.¥G—G‚?ð Ú«løEx�5«×ˆþ-ð %ꉢÂFÒZ ÛDôò–TB²ÇDV�ñ6‚2y�¢/JYˆz~†Ìç}kÀP%ÆØ0,òmÁl»g�sK’Œ<[ÖÞ˜}'÷kü²;1æ’ 'yû¢¶Å±µ“‚œfNQ··ì=wÛ41G¼]ß–×èÒ© O`¥_[Ä®‹ 4q?ð@k èÌ"LÔÔmYÞ7°­ØßÙBK‘�Á­rwq¬ +Ý3-o~-ketr¨xdÙ]Þ[Tþ.håN«Œ‚ÛÒR ÕfÓE¸(¸]/°ç{{‡8Ä£vjQÙ뤂rG{íÚ‹M½]¼„Žñ0ˆmŠ56ŒÛ^ŽÎl²F™ˆ •K·5ð­Le­Øn�&°ÑÅ€²Ê.)rß,N)1„)ÊD·ŒGKZ"Ñè*•v¦Ð"\¬Ø,×Á•/}¦nÒ°¨û¸Ç[Çî ª‘\·õt$Œ ,à ü­³�ÙÃë§8‹öEžÞ}òš±.(m½YÍ4î³€3£M}£ùµ´Æ»IeòÍ!¯ltß}[ž=“h¹’Ú[è@9ÖM”uÇÚ}ƒOX´ˆze ½µ]†Ã²œà@uí¾òÝdv«x”>“¬M mOù3lÕW:²°Öà¡x»¬Çü°#ýâÉr\ëJÞÜ•/^Š].”¡�5Å)rA¥õlð5©mäR9#«b¼ÈØáà*ÜþÜØG‡“­Ÿ½­þdu¿æ£6a˜piÌÂŽÞ¦5¸^¡¦#ÛHѬ…×¹„¯YnË9´wƒûÔ³Ú`Ÿòllž ØÒ;w•ï"–áwdš…+H ‘gï.ÐüK>[-‘׋BB �q#¿¾Aœa¸Ê·¸sX¿n_ú� F4‘…6*ñÆ @vˆ»ÏêûR€¶MFÝaÐιî' ÈŸöÆY@í`û{KRxdu`ºÏ$ …«›ô<>Ž_þý›n"п'ïzаÝîIêìÿ}çm…ö�O B?, ¨³Óo= ¨ø0‘Üû»�´m•;³€Z üæI@=‹¹—, î1¼›I@a*þe™Cû=¦™ÝíÏŠ\î0HWj�~ó< “yoÒ°wdýœL_¿.²yêëæ»ý¤u| ¿tAg(#€~áQ¸ÛÈð·šñöm†ÂÐqTrkÒºåªë̹،mj#r%D§,|“¼n{f¨ ‹TÛ@‚VÛÇwuÂ~»,®O%�~á¸��²÷ŽQ¼QÆ¡‚(7Vë%NÍ lólžpJ²½ñç�B!Ž]ÌNœqã¹±”\¡<„Ì豯pÛÒ¹À JG· ÄðÛæ{ WŠ~¥qƃb‡sw®Np19¢Cf«LA¡-½�Ä]Óø´=�º�¯-2�–ÈKÝÖÐæH˜×’Å¢§‚«�J®ÎlNÿUiºôÑãYôéu+×ë‹ýÆ‚fvV 85,Çéa“|™I§îÖÁò1àKÐÃ³ç©  ý@®á :|ŸÚHJÍR·c( lÕì7µÛšYºaÃñ|m�yûjì8o8ê47-+n )êôBñc°òì'þ�õ¤ƒÑ“3Ã&cãþ29K‹Ù�0̶ÂÔý,]¥…T\nW…ê8¾ßq‹lx »"nSºª2ñ-ÛÝv#ŠýC¯"ùvYÄËáhv|é´I–GΧ))×TªÖZ¨ÅÅ'{j¨˜ ýu0 Ì4�U£;–Ϙe|××á–”�þ³nð>#6�&' 'ÙÂÀ^�"X¸ÖùÖmE?$¯Ü»÷\»‹›uJ09 Z5G³ÎR@!,†k|ã €e…áHU:M‰ �z-kLˆ>I�»Êc‘Î1±¶œ®œrÝ)¾¤®™Hq¹-4´Æ@ñïåÔK•0 |´¯•¿›�4rÜ»H©Œ+8óÕã\°ÝG8z3Ž7€'p†o°Â2<�6ãÈ?X>çÂôŽ«õÌ=V>7*�Ìm@¤�+9ÝÙÁgÏÎ2Κâ°}õhîäJ®Õ=¦¾¬øû Û6b\”SÔVeˆAÇ£†w¼Âz¹¾¶Êeíæ(òWx]ááRk•ñ˜œŽe‘«õ¼N§X?“6 ê[9<…â–€k»&ï  Foþwòø|:ý¿[#BXzÔK^÷ž©;æ«�=1´%;9ŽÒÆýÆm– vhQÂFýL7O)-‹®¨á{co_¥B·¶½¢2Èâ&I ºËŽnó›�.KTSì šà` ›6‰oÚ­¼�=Õ°+ØRH¨½3�¨ß—ê©AY;ØŒÃ�VXw_ÿ³Ç)[&×CÕåa¬Ýÿ9Š­±ÃG £x»íƒÐ\hêõ²þet{˜B*½Þ:,ßš¨;Vw „ÖZÄÈøÐ=ƒcl:?rWi­‹r�Ï$z‘áÐomNÌÙ…èpëéšÃ‡Y¹t0ì~M¶]D3i Ç‘cÈ�ªS®â‹,Áʇ_®+2`ÌRlç[kØÑ;ÈMQÁ££,ÚšŒÛ½ºö9uèÚÚ�ß®AVh°ßñ7Yžczê–Ž¢R²Fz}ÛþÆX:ûÑ@;>šŒ&g“Ãɇ°�Y™�MNŽ&'O_гÇMŽà®ðÐ�Ðݳï$ühö³T¨o@ž?ÒfÅG8íG�‘„}‡2{?|2Ž%0útøÑ£ÃŸ@N''å²>žL=úèÉã&�ýª†-;Äëz†ì Ù8vóÝ~vˆ¹½Ýîwâ6:¸P¦Õíªf´zŠTþcpö÷ÉÜ2“üyq}Û‰÷éŽÙ3üþ%Îã¿¢¤B²dËÈÆr0¸1Ž=w^_¶¼F'N{ðMgæÈé¶¶â ²¶mÞ ’×ZCÈ)ï;ÇX™dŠ%{ËŒÜ1ÞÙ±äOˆ¿9¹Î‰±¦ ÁT‹v®Ø«EOÑ¢LñiÙp¼¥5¯Ôãˆm8¨ `¢lHŽóÌÆ”¢Â U¨ä8ò¬A¤r`4M0æmŸ3í»Äæ@jâÄ­e�‘+ Ìà<õi ‹-…•¡µ÷žÿ6»gL€É—b33r ) dÓÙÜ ‰gÑh;‹\šê5³'àÛÆ�PLý‘,<Úšö“gÇ»½ LØs¾‰!§Ò­¥½ßgŠz¨{‰yß61=Þ¿(ß8æoþ®Aä2O;?€öV¤ÓE‘½.¯ì�dAìÄÄò+ÿ9úµ}YÉë$€jœ4žmîÝÌt"zú«Ü¾þ¸Hwç>U ݹy‰AiÔˆV%@…O6žhDc«� ëJZéµ¾¥dm %â¥ò!èRÇ�Wþ%m¨Î.—x'†ßxôê Íß®\~Àµgá�¤²µ#¼»£¡�ý­íFÏaӚ碶ûmšØ»ùÆ%�F)�Æ`ïÊÒÍÈÁ]ýÙ!«ã­5_jƒLe͘®—ÜÓÄð[â?Rl3ÛßIé´ÕÐÕmÙ *½Ì#4°=r}í-¿ø+'±Z^6eIˆß „4…$ì<Ljr|Æá6qÒÊ•}¦LuþÛ|˜LìóWI®¤ÎE™‹¹Þ΄Å-.\l»9Žé?S¿æú6�.6ì| ãû.ól RÓßæÏ´Ì¿£Du~ÛO™úº|æj<�Ñzúk~ %ë¶“Þ{‹O`oƒÏ˜{Û|~š <|–7K�×ùe²À:�‚8ì�SÒÉŽ¾—¤¸ÁŠŒ»· ‡œ$› ©�Þu™¤Ý‹Š­7é­‡øé_§0JaõBBFZ¹Ñ&ÌÚ²WÝ•è"`ÈS^ÈE“q\ÀËC½l‹Ò&Š={tŠ‹À ÆCu-Ó¼¾žðÏAÙæ±#bô*³jóÑ�&ŸÑ%_$“™œ* „Ħä³r�ÈŠ¹gˆQÒ‘øúDÏDõòÊ‘:ù‹ý|@SWÐÔî$šnÐÈ"ÿmîcžvÂ+]Oiñ|;GÑ^ó²îÕ#y»1`­¬ DhÁ¤à$E6÷u°n�d{¢¼Ü8œ.jIPbÛÍ‘Æte Wy4ig⩜çÖIoKkno •Ù.RléáÔæñAìV%ˆ:…@ÔNtåÔ`FE>Ø1ÂVû¨Mƒp„Ùö­ #öXës¸¢®1ønƒ/'8èKm¹= è:ù³øÂ¦è“ Ç=rJû?ížüÐ…¿i~ÁyuÉ£Ø7’pùj ]Ñ¢§¯I‹œŽx8Ñ8–!…RœnÎ º®´.!—˜ŒåîùÃvÃö5DIâ0Ý ¸umlŽÜÅ1T3͉äg9O�Ë�4)ÿeGI:“È&Œ–íŠÔ$=öHÏ•æËä]èÀѩ̂U ìæA(ê‹F¸t·r”�>°f8G,•BXºƒÚ-©OôCZ%s•FÊœKê~ÿ³]¢$Oo­dZŠeæI–‹ÔJùé2£‚BYÓqSö ­¸ä7�çyÉD˜”þ®,-u#S·)Ë�6<8¨á4@^îÁ9ÿw?ë¼S73zþ»÷»\A>lœ–NÉ>œ–T–oÁéªÅþi€;œ�<Žøñ¶%Å~p§ÓäàÙ/ÖÅ�ÿ¦Y{¯Š5…ÄÖÀ}+ûxgåÒN¼Eò=�¿ÌÔ�"­X¤*/7”¯\þ‚­vË‚:yGâ£TÀˆ‰ºkj‰r�¢Qžtf|xK·ÁDä _=Chí1¯¸ƒ±%ð€ ý�#õ× ³2�!/M«‚EË‚ñ¼Èklî€)�à„õ¸fKwó�ͰÍœ­`~­ Ó>(,hlG„m”%Tͬ3j½9}f˜OûÑ@É*1=SµKê}äg&RY;$l’µ�Mz²¶�=Ý•­NZ5!`%©lŠØ ½j\TvJLÛî“íX83¹'÷ê3Eë-~À(9(oi~‰,òhÐKŠ{èÔrÑ„cÝhŒ…ÛQ{p<çJ¶*^œÙ q«+Öj ¬&Òð0 HkÊK®{ņ`©WâªÛ[o¿+·³(œÙê+$ÀÞܯ\«[Xš©CËæ_§Ì!Ž­ ¯ÚY^¦æƒ´ðv�éRrbB= Ø�~ZT—_ªM-8t¯•ÿ)jÞJ´ëÏÝ‚xQ€7l/ˆ×s–÷QÏáÄ|'îº7ã¸I%¼hënôS�A¥ð¢=káõ”ÂÛ¢ÜÌû)™èY±¿r³^²r³^nWOü ËF£ÙGAñš©›îDC6ô� òƒ§¬ íá÷šr‹âà[x)éö‚|í1“n á ÛTYn1ã^ƒì`)Þ1ö8"wˆ®î’Þ©ý.- Ú"¼[’D'×+½’à •Üù=—"Ó Šï‘énb„÷mR|Ocôí'=\>³U~£;ÎNùÂYXW:»ábÿ¸:ɰ[B¼“jÁoY<ÔµÿÑÄÃÀ)åÃèÖ¢ ´}g¥Ã½ØÛCÌ‚ƒˆ2¬Â¦¦Ô˜W¯(+Ãè–Ú36°�p†®ب�Hõ‘رH t¿ª+ÓgŠ}.0vzÄ( øk‘÷Ét;xcéûy‰»ƒP'CqÕÒ%YZNäAÎ�sIääªÄîB0׋9•XѳÚÄÀî/•óÁ$}`mwcíþ`V¶®SÇ´æî¸­+ js£ý“ÜzÂñúÖkjñ^§CÛ àT·‘Ößš–@Î µ9nÕ° s[OÀïnÑŠÅãî{±lüδ…=&¼;}aÏI÷ШÓöíu†¥/[ШC´mô}¿æUëw)p}+»¿Ã øïÈìTz�ÿ¼ó;lÿÈ#¹œ�S`mñ¨'Ï¥$I@Dé*W—VŽá€óú¢`vâÕ:ŠâÚÛÔRºïÔS†,ùÓT¸‚o¤%|oµ¬úäïÌ@­cÞH ÉΑ+<ß^vÞïj½+ÆUûvA÷caÍÚ‚S¶§ÔÔñògw%2 ùm€èüw²eUw# î»�÷žØµ7l ÉŠ{ É €2ùý€ÈDw‚ÞeHvw’àÐÙ“{i#¿@tWnâ¢òï'¤  ï†N¼Í�6È¿›�-Wn·x­[ý;“¯ÿ¨ �³¼—ˉßew7Þ}%ÛSAñͳ÷¢žP¼«žP8ümÂ?h€{‰þŽ,ÔìúíÎso!'Ãæ¬Œí2|þÁ¦å@É ;M¡nI;UŒÝ‚Qö,5¼]{s ðÖ0åe—í™üVê†CWM‚–m„Q] ` ¤:êz§¥�œp>¿©c"ògVU¨®£k„ÏhA<#ÿjšS'É…k½B¶"VŸ¦ ºGÔN}Š¿a¢æç4S#ïÅ`cx§‡¦3Fûæ3OÞMrôT‘kGÙ3Õ1ö—Žùo¤EÁy&ö>QV;Í_Ê»ˆ°£N²Ç)ql’ø"ò ;µ\ÀÀîúàÓñÊ@à‘ˆ[cÌPu*êkHxÍ*²DòïuJŠõr¥zÆî’lÑxcGc¾"öú‹�¤Ðvúî]ìO�…¬ޤz/V‡ÈxNî/ÄkØnìTW"Y$ü릺á3¹‘m›KUÕMaŸ¶žÜÁëàÞ²Äíùï]⧆œo)Ÿõí¸3è9O_Àn¥/à÷£/„F¾?ɽg¶{”Õ·Ì8\:¿‹¿On`Èâx¯µÿ] Ç®Š·Š÷lütr½vî{ ¯[V~OÀÂIÚG;Øp+ÇýöB.: îZéP™á÷穾þÝR‚˜ŒÿþÛ›Œÿ�3ø;Ï„ËôÎE“�ꊀ$­Þ&¢…¸—ˆ–àÈw`2¾Á<÷m2lî»Co>ÿ`¡ôJË5Ýq, | ™^9õÛâÉ»ެ ßQ@ò•mô[…¯lÕ®�)سGwi îX‚£ )ØnÓNSp¿%x;‚Þ§%ØÝÞýÊÙZÜ©ý—Í¿Ñ=Ø]óotKû¯1ÿFÖþköt Õw‡Ñwänôu ÜÑü½¦xkSïKïž›ä|Kï vc§§Ý™}ךw£»³ïzâyHG¼oñÞþíjÞ½}w•TM–䨮9¾óí~b~µ(]QÑçN¤ý}&ÔÿõíN·¹‹Éðg×V&@dù‹{àE¿µM©°k‚›´‚ù:+R Vp¹«¤xËØl*ÈÙ·4-Óù<›fn°ãNyß=$à!iî¿‘3`ònÔ ‘é{ÄÝ(€E\™–‹LR·îC+Kç§Õ–Ž1(BWenR’)"Øô±å²£6›ú¢§n­Ù¥VåZ9p‘Y­3ˆ±«Ž3·äsø3 ð¿ÙÏ ^cÏY YË»µ:”&‹²B•ñìs@òf¹LÌïÙÔÝû™~2Æh¥](Â\wš…¯½ž&¢ lCbö!ê`Ð^ôK�~U8ÏMzÿ ö)€µÓã §dûí7o+õÅÚ.è“-Ë:õ¶ »•„Vèv§uïÙ¾‡# „®·1Iï5Þ©�¾þ^µé…»wHö#Q�:ߌ lº¡'mb;¿óÐfG“ÃÖëccN¡S?tÒ'îöF§±0MàXm si¹Lo€\ŸVE"e¶±Zºh¬,Ó¾J2#Ô:k~ær•Po)‰¸b ´çjiEúf•'¦•ªIA‹}HK¯ç§>Hð`ùÐ�äc6ö¸áì —OCéÔ«Ô7}�{e8 ( 5eP-]‹©Œûl½ÊAuâB'ÚX�{Ýbí÷:mÌwô|½¢Lû¦¿O”/}·…§aëZN¿¨œ[‘O›OÈ”)f͹9¶Í*µMdàøuf~}²Bm»ß8½kŸ|YÂÝe¯ýÎË<µLì(ómbÑÈ—/MCÒ×µq¾`éêõËcÿ5-f®6…„£(¹÷¢£ˆQ³³$£æ`í|î�kPÊósVXAÿ¡¿Ž`;fþfaë�þÍrá°ÅíÈ´B~•^&Õ kÍàŠ¤…°šîF õt.Ȇ–ºpYüƒ§ �ÞM^ùF³¬jã[“'Óª¬RÜ4êÒdî—™AؼœbìfgÉé9×~lfï�9m£àd5t‘®ÊüJ•qê’`Æ œ�K…œzÕ¢_õ3 Ä’PÓ· ÛtŽtŒF¶_ÓªôH¥mOU¡AÀ¾å)éøÚÁØiü„”Æ>‹x9Oê�g¹tGzÕOÕÌÎM®°×6ï0�JˆfFÖ"»#nüÄ•,*$+Ò½AfUi &ÊfkàIâ,–£}øÚ]Ã;¼^oªªf„#!å">ŒOáŸ×}íÄ{›‘¼o£ž•îJêfâ÷&Ä:SH—±e2ª8"·k¬yîÖv´l …ÌjÃíi-sÂÞš±4 6ÛåªÜ›–Ê}‹yÄÜb¶6 éöû+Ûƒ­o›ÝÊö^Ö· vçJP/“äÛ÷Tú ð&þÌ›Ø,Ž¢ os”gåÚ˼´u{ìKÉl–q^¾1ÒPm¶îVöºj‹½Ž[G'ðÆ;Qj,fC{ÎK¢%ªV¢[e²ß¹Uæ• ×ö ã�¢;vî�7ÞNÃD$e#|Ûx-Šþ>’0�|ž+F`ÿ}Ékü½›ÒZ“\໩#©éWÃ¥´ ç�}œ ;Ãòš‹ÜÅÏrF 8Ü¿9¶ÁN‹é†ûžã [|åÔr}ý-#ÿ6™BÁ‰Go =˜eõ*O6uxæFÕC/È ãáze‚½=~„5Š)òGK#ÛØ; Ü0ß�šU�BˆÕ(MœÝ4Ís7ÐÎ<ÂßÕô ¬è²¬ÜáPÅÂfò¨)™)a)öH7_QøÚ u̬Ô=mQÞ³GÌnH9óUl$tú7š–ë‚;hÎÄn�v‰¢‘€kb(rX´÷ñ+¢¦&º›kÌp€œ ²—1{=.Ôý42-—Kœú:+fåu˜7»Xjqb§ÙÂ-9jF#²eÛ´5n•8¢h2yFÂå )2b¦‰æ;ú@¥ŒÁ½JÛ6J£äÖ„EÔÝD–Ä/³eZ8Zâ�‰Ýé\xhE€²é,Îs‘"š>£3¿Ò-t58¨O[¦åæîz¯(‚ò+Þ2øæÉšË¹‹äxK[¸M¯æI)0pÍü§¬fieŒjé¶Þ±æšAÅq*§Î(-Í–ËkT+à�}|Ú:H”x(i¨'â�à4½R¥Dm-é"FzŠi¾ž¥^IÓZáPß6ÂûÄÙòÞ¶ˆ"÷ÍJ “JVèè\W»&L ¡KàÒô€J&u‚LfICÛ ˜y±Î7-«u•Ò{z²Š6]Òbeµž7�ÇMÙÀzþONNôü±²phTr®$¬°oÔtùVG i÷ ¨ß„œlnm;4-ŸáV 4MI0À¤«ªD!ŠõsŠsèíYw?œ n,b§Ýg.ÚºY 6kÿ3ú"ÇÓ Éh€³á~�lï šÜ¢Ö$?ñSÃ�ÆîýlÝlÊ{ÂUæ0`Ž»)‰… åP¡Òu¸Ð)QBØ,TMÉÑŽ J¨gïx"6iáæR0-âšIJ2ž†.ÓYÀؽŵ¯A#H×Ã=`…hâ.x$˜JžL_«¦‘'inÐS(ˬœcäÍ›‘XzaÛ(Ø0‹c‚aR†¿ þI?ø|�œw6X=¬šS ~ugËñ’¢.8Pd¿ HFÂÓ¦ì‘~«lÙj¦_Ý¢ €¢'‚nu ëä�f’¿°ŒßýLíØ;nÑ{œ ¹‡[çœðãÌH×Ý®N¼j=Ýáà(™ ÉL¶´$Kß 34>ºEv¹)—8t^^ãÇ>/C+ø´"õ@…eV³G®äûÚÆiW¹(ܸÜüçÔÚéÄU‡¿viŽEµÎ~MÃB¶= =�!Wkg·Nu,ÉÍ3”rZB�6ÞlÊûòuÿʳ ˜Ý.FÊšä>KÒ&§/™Õ¤Bǧ.â-+EÈà~ U‡¼ÐÀ>úûÕݘý• Ú`ß4,ˆ0¯�»¾”t‚M_¾Í¨>+rÖ^HeÁú–cÚypH…ÕÎåjoõ8 È’ÖZâëu'ª£˜I6!ûª—¨ˆ³Ú06v Š)0Ö‡ö.XTà±$Kµ7ê´„ŠiÆ®ñvâ 9¼]]E:vðPØ�£& ÕchšSÀ`zcòÓËo�$·¹�#O�!LœÇqv’ÒÇv$‚\‹±V²-HÔ+aAPó‘ÍtIÜX5'ôŒò¬Ë²H;'(“ȾX0s`YíEÒæÂÐ_¤›?ë%½»ÀHaopn¶>ÝTðÏÄÿY³øäÁéÉŸ¨•¹Y¸¿<ú~1ÆÔ²àß�ñõcš'±fÞX·ƒac‡š™û ý->ÑË8Q{Â3ù«î>˵‘r7 ":â ¶m0Íó²¬nÓ ¥zØ›†ãÐ¦à ƒ£}3‚  îÀ×$‹ÿÏÝüÊîc™‰ÐAÕJ¯k0gn�çH5èî �€ïL®"w‰E•QkpuÆMâÃx~`5’ “k„ ˜¨›/ ÖÓ2û�&ú±™¯�1K¦˜u®�tÜÂKï0ŽéEe"z{b�+iF¢Mâ×DÝÑ*تC"öT8…‹¬ËZðÈã[¥WY¹®ãzZÝ›´§²÷ŽåûÞÅ0�ú˜n›[z„0éXÈ$Ÿ]Š+Q~Ÿ¾àØÎ4ÅÓe >D-�È;§ë¢ükB¥°êðʉ“òî¥_ºA†¦&ˆ“ øÏÖ¾aª¢¢‰†Õⲕ>–ô@vgI 8ï�‰œ-ü½e2ç­kÒ îRúGmRºeÞûe]6Oadþ`³#¢y í•<³ÐñE9ÛàÑ�zý Š—Ò ®û®ú:�ôv¤ÇxœÍ}[“Û¸’æ{ý ®&b¢ŠðµÛÓí²ÇåÓ}v7v+( ’ØÅ[“”ªªýæ ¼)B:ûPÅ$ ‰D"‘^ÿ—÷_Þ}ÿŸ_?x»" W¯õEùk¸Dªð½ÕÎÏrUüu¶/6óÿ>{ Ï‹ Õ"/ü"È‹`•¿~ÉO®^¿”_.“õ~ç‡Z*ïÕ‹^¼‚$?À›tñê/¯þÛü‡æù¯¯_¦ôäûNy·UòÔ_Ýû[åm’ÌûñæoÞ—UáÔ N½{µx—Ä…Š Èn®^ïx,^ûÞ.S›¿Îþé½ÊWY�ÁAÝUß�-jÏkù½~é/^¿„´¿âþÝg?Ò}i“>�¼ú#ÛozJ“b§ò ¿û® —x;[TÏò¢†¬24&âR ì³þ7áúPJø5ÔoèÇóÏ*PÙ³s ü&TAž‹xÕnL?Òk0³|ùQo½šCE9ƒ„mU)úF!Ìí J÷ 8%®­7~Ãú>6j¶(É>XèôL :^áßCWè%`-‚Öˆ’ *²†V‰VŠÊ¯�°¸Ÿ¤-Ë1GDA‘Ù¼ÑÍj¶è<2áz jc=Ô-ÿ‚jÔþ ù>ãçÀ½]²žÊ¾èPc`íÆõ3ö“¤sAîÏÌÏôö |=ÖDj}ä��ÞÃ.±éÄk4Œ½ÿÆC“\Meà‰üGÂW£Æ @o"(xDûH Àà,ÉœC!E"Ä1&‘ú8+˜uPÂAò8 LuVH�à Œ`ªª âsWU^¬ \�ñ­�µŸ­¡M¶hÎ l1°½Åb®;/‹  ¸CR6÷sÁ�ª€ǵ›6²¤>—­\4 &l_ ` Qþjçe*Oa¬L Š?J3h“«"Éž{þú÷=ù¤ÈëL¿‚D~€îŸ*ÝÔÒÀ€”FŒ³…¦ŒUŸ)Uz›Lý±‡áÛ“G#Y,Ê>à‘ÔºÄÞA!0—Vô(b³iê˜|rº5éÜFæ/õ¼1œïúi$ßõ-Ä·?¦‡yÑ2¾Ç�^Mø¿©bŸÅž¢´DË ¦_ç¥ÅyϼôTHñš^¬!Çá|¶ÀË’ôÞ Õ+”h."ƒ2}½JbÖJ6ž1¸BPo²$ò©ÈÇ Á5ců°1;[T´‘¥äŸ ŸÄö¥ü'ä ƒ’'R’šêá%a= ̘PÚM†Œ!ÉÓU»1w¨ <òž™ 2âg™ÿ4ŒÓ^ëG°!aÊøGU@VkõHÊŠÆiAÿỗ¼,{IZM ×%òd_„�Ê0s!­™Ë{j–Ü"OÌ•üa>„2Ž�“ð fœ¤òâ}´„ uIø9î(àt: c�ü0ÈÕ]Ý•v‚ñ¹ éwè•s‹ôFrúŒ*(Op$Z$0Äòn®ÿòü‡g•·T·¿‚>s�e€$`¦kå_¡à7oÂ¥¿öÉñô-U™sꘋøÖZŸT´‘é¬FJ LJMk´ ¢RÈKÒÜCDÉA5Åœ!LtmñªÏI÷™Rú`æÀ“V›Ø˧ʌÖx‡†f”E Öhá� ” QÝî€1àŠ—®"´}Ñ /<ï��^�“éˆR¢3½› Ë‹+I™‘zN«Ç·Õ/VI¸�b,0SÏI*¹ï Xqšj|å­ Ã�61ˆQ\dO^ M–ù?ª|«la �{¡Š·`"þ€¹¬ÕƇÿü óo`€«¸Ìmú[ÉBcc0øÙ]°ÝANåws(:ê�$G+îÊn®-&Ól*V†iFu€ÍÔý׸¿.àv¶à«©¶ß@§“A럿÷3ò4l“d+0N’Í|Þ®B„ß }V0qrð0t5û*=Õ;G•ìÇ~ø„yƒÒ+ÇL×on¾üúæu•`é�¾&-'.J[<$W•û»Ó¼Ô›{,2%�÷ÐÝ× ¸ó öàò/°†¡�<€Ü±ùUÓÔ9�:hX UÊ–²÷òDOzL‚ òéç ; C/“ Û‚=4Ì!QRäϽ=H³�¸ G Ì �ý©Ü~%Ü~ÕÃí™EÌÞøØÆq´uMö©Z?£ï�.ŸWYHŠç5¦\1W„]]>ž�]ñ¬ünŸp’Áà]ß<ëˆ(XñÏZbŠ|sóŽXT€P*žYgÓÕ Wñ—uwó ^ož™!Ç‘Á>Ùçµa«w¼P/®ÈhLü‹‹ÊX†j‹q�QýŒ+h)Šåuòì…÷GÈ~ˆ;� à5t=þ½™ãù>àl£Ú•}e¨R2ÞVªØ˜C�〜†süö¼o‚Gµ~á}xdo<òNOKd ²YAµD`ÐÐQáC9U©!€ Èוd¹‹ÀkäûÂ{ã-C�¹] › O•Uì ºu®Ïõ·ðWa$ŸyŽˆðƒ�¸6­xÆd`>¯}YGaW½Õ•—ü.ãòLê@/3ª'”íÚ­¹3À󷜂ô‚ üPõSU«?öØxƒ# Î4Ûà-ÁÐæþISÆxª¢('™ ÔBžB‡¬ÐiÏ=Aé!š‚)ˆÁ$”Ñ$×ìÀá�¿ (؆,€TÌšTZêiymŸŠjµ ^m“Íl!„qܼ æÀ!ì³Ú}øÄŽ3ežhª_?®J$k®¬kdU¥©V¤êvI”lU¬€uϦ€«ªýN@¶žkör2p9­q–/ÂØ1° 4q&«œ lXË¥áú$Hç’éÊB2¦¤‰6̧4Ÿ5…‰­³`…®ÝCèµGïSr<’Tعb¤û !ß+ÀØ�ð�+„¯FXû ÚÈü7PôÀþRÉøû"©×K€S‹ä£ÎÊØ?TÍÁ”Ñn‚j”QV´ÙE�oK¤¡KšÚ®60’YGhuhªßLC‹5‰eêà >ÊÏšª„–­b¥ÇVlf�gÅ¿*ͽг†q ·Â ý¯BÓµ×LæÏ_•ßÇ,±¿.�bë£iÆ!4¤˜£Jôžè' gõÏ®èwxº�b¯zWˆÎj?és}ÿ¤Ó–ýþ_y×ÿ tñïßµRãÛg5yõp|*í&M2€Tv–è ï:õø‚t«ùÅ×.׫nQ�fÿIJ¢Ò¨�R’ ù}ÉÅWäÿð³?öjþVe~{<Š-J{’J9ÕŠ§ pîã˜ñaìÿ+Ë(ªG¢¥�–3h¨Xb�cŽÞòÙ[aeûüÞ ¡9€«ßö÷p?sRв˜–0>×Êd^Ó&õJæ‹ Â¯zGáÏ� Ã[£ð«)ÃpîJµv¹ªõ£`nãÀ¦ü‘@À_’Å:…ñ¹ˆCn“†ÛFãÿ9 Á°I²ä0¿�‚ ÆÞõÏóÛggô¬0´W£±Ù×DÖ€ð|ØÂ¤N1 +Ú< �o78¸=œH{¡"»ê‹8ŸMÛÜ>s#˜h«ÈÏÄv­ßõ·êÏ«LÙÅw0ŽßÕ„j)Y¢ã¤ÇŽý.ÇŽ“´õ¥±·+o’‘ ˆ˜”Ä<}¾û»¯s (É©ÏY]«�%˜Ëž™=ûúÛŸÿ׳WOOÿùúËxQ¯–�£ÏõŸ4™Á?«´Nâé")«´~tgSÏÿ|ç.<¯³z™>þz».êEZeU|šVu–Ÿ~—ÿ}~WZ8+f[lïäqU'uoM«qèKx%ú<‰ód•>º“Ìjx~çñçwüzò˜À[ø}O—IU=º³¸Ï+xkZÌR¯ƒÏ/’òñâó»øOü(þüÌ4pö8Ñ_¯ø¯Ÿß¥¯?¿»¾å–ÇüîK˜�÷àûd¹I¯×ùnïÒÜú"YòƒÛãùh‘.—Ùúa_Gð¿CøµÞ¿Cy�ÌðïÑôbß!=ŽŸä³´¬ŠüðYR.aKÄçE1ËÓª:,æ‡ó¬Žv×`sG:Ë.t>«êðþn�çÒX-GxÄeZoʼŠj=ž¥Ó¬ÊŠ<že ÝÆùf¹Œ¼A,’šþ2Kà˜dy|‘NkxÙm=†÷æe±‚V×Åz³ÒB“—Y½€'yQ®hsä.³³ þmo*œ<¶Û¢M;ŽOáoɲNËš»HÝ1eUÄãºã�Æ$̘xœí}k�ÜF’àwþ žcw/ªÕ²loKj\[’½Z[’!É3X‚Á*fuqÄ"Ë$«^,pãþÞý’‹g>ÈdõC’1³0vÖêª"3###ã‘�þ×ÓWOÞþçÏÏÒe·*O’Gú�Érøgeº,�/³¦5Ýã{›n±÷í½ø¾+ºÒœ<Íà×YU¬7eÖuõè€Hȳ:¿Âá¦'m�´]1o�ÓÈ‹ðDò(K«leߛ׫YõáÞÉ£ƒ _>:á/à©#ø¼NçeÖ¶�ï-¤‹þ·;xz^ç&˜çÑyÖœÌà?éãôÑÌ4;Iwè×@á&üøþ´û耆{t°Æ)OÒצÛ4Uš•eŠƒÞ¦õ"x/5¥Y™ªkÓ¢J{3ì'Öÿ¤É þ³L›$ž.’ªN›O¬›ùáãÇð}“5yzþyZO«lÕdWiü²Iš¬n²iýñ1ÿ}|,£\”³ Ž99¯ÍSgqßÛðXôqÉ2ýäÁt6Ÿ&ùôÁùÇÇ qz.ßÀs§ðÅ*žæI]ò`ña<¯áÿáÏŸ–³Ô›î‡�¯’ê|óÙç_~|ŒŸÆ1}ñ¦ýEáý•.ëKÿ‹lÆÿ|aÁ¹8�G<"ÿ~ðñ1Áðññ á<�?ƒÇÖyÒ¤qRÄér•U¼O×Küwa°VÙźÉÊ"ž¯‹)~8Šx„Yv¥k]Ö‡>àAy�;׆À ¤a(ã©BW{àE;á‹G0ËA\Îãf‘ÆåE�VW þPÇYAßÍ@¢:Y®ò4НÒiSV± ÃQìý/“M|»Wåu ÏNË|½,äÍ1€7‹«tU¥uZ4mWðU¹”)Šy…¿•Ë4^³´Ê7Yqé-æÿ[C6몈Ú;g5-.Žìz•äkܯ&¾^dÓ=/º§UšÔi}ÅÏšøŸðÝ(;ÓïfÛÌC à÷üPÜ”òñ/“=úáçìãðîSOòº”•×Q ÿiÕ¼¥uökz[`](�6.¡iU~,ÓºN.SB þ&›ÅYë¬à‹²Al ÄÇæqÖzM’ø»žh k¯``ŠçÉswå€5eqyþ2M ˜³øãcù*,NâE•Î?yð"/›Ðì©éÿƒE­à‹çò�I y ·Ú¥vùOpÙàÍréR<ûm�ê…¨­ý³¯eñL©Üa µú§wϽ¿.‚”l0ŽKä/àG‹rÖG`ËbžÍÒbšÆyz•æx_u¶ (íª¬ˆ" ¡¨ Œ|ŠwÓ]Ó$f“Vp£RØŠ%ÕYœÔñÇéÒÌ—4‹ç+Ø”ÆÇq½^Žþ ËI—çñuÖ,b n³tžðž3[V4HÕòÈÛ ^_fEleU®ˆR#q©ù®ÀÛ5¼ Ã9Hüj?:ä~¼ñfIÎÄ�c’k34˜¾nªu‹A\Ë@�ÐUJ@¹KkÊ<Ò=®0 ,óàˆ!¼^áJâYy�0שrœf³">š¾I«iV#dÞë4¾NÀ2¾LÎÀDÒ´s¶RüÉ¥=`…¡‡±�?]ßÀñ–d:MWM<ÊSà[5p94ÍS:‡ú€· [$WÈýà*³¤š9[o|Pä2TrÄ™I^…‚Å(=º<ò°ã““£“Óø@Y±,�×u [ÿóºnÌ.½.€å øP¯+‚{ t²%ÜBÈJ½TqRÁ`‚ )¢»NY4Yå!ç(K�ø§7+X<ŒCd— ‚¡ ÙÕHQ ‡Y`}¹È7f†¬H �âu�Ð9X­Ø ¬‡ÏªËõECb구×(²Â9>87�Í}xyä‚6þ°…÷ë*EÒÁ‡á>ócˆ+„“þº(+º�úã’ÞÅYýÁWTµ› þ\ÎfÍâ“““?=8ÿ.©2| DíEçÇW›Uç‡G�á#p—ÿ~ŒC7"¥Ó<3o(—:óÎp…„Ÿ¶¦E|˜Wé/k¸TœåpÚ#~§ïŸöùŽik¸ôIxÚ×Pr‘‹•Ô‚Pp�.6±»6 ¡_Í…ò?�ßË›§H“}î¤ìf ˆ=g‚[¬— 'À©¦Ì­êb˪@Ì·íf€t!Ü"G(ÌÔ‹r�Ïø¶²Üf¤VY‡Ê;H¬BÄpÄ$M¦ �üÓ<¯iÞë1}O_\ð/"TæÊó%†Ù.€µ­¸“¿¦U¡¸u8apé9øáÄgñÀ ‰X!›à*_Z8]”q¤ÈRN×®�öËŒ†¥Í�%pôÆ�²0"-ý€Kv‘ÃÒ]˜ÎÖ}œbл2J𜑠—Á#~Ä�.(’x!@ A†gF Édž0øÙU’åL„h“„$3V¿JFr¹ÈŠ÷UžÁ� EPLä™i9]C~ß`:Rdþ-¹¬�wüÓæò³·ÇDµŒDKrÆó²Iÿ#&žö"à«xž¾I¦�RyPû˜S5(lÌ�jâY0³¾Hs`PÀfQ HfÈÄp¯”!ë»°åUº�|ËbÝvf )+›Û¦øâ áÜö)ä!wНQ#²ãÄ£N=�?xø4ž<�&?ŽãÉ üß/·‘QOŽ>zÒ‘e¶o‚lïÎ}ÐçέEyêÏ{¿¬Ëæ©Á3þÓCUÕR‹vxæ ÈÍâ’%…$‰ižžÑâÿ'�ÛØÄ_ÅŸê�c™ã%_á—HÝ>GAl™<Þ”¯”’}a„�ñâGFQ•ÉE–g ð$tLˆúl™¢Ý£°© þˆ¯Ê<þàl÷“'�ЮVñéÉÃÃÓ“�dSáß°î>È]êÛwíÞº¿-�¥4ñ+~A²*«DýŒî>ï.ß®‘Öî¬1Š-ܽÄq<Ž¿ƒE><=‚E>z<Žq‘€]‡““ÓÞEºzï•>;ŠŸñûDg>=‚/>î•›¯@¾|CB7�’6�jåïU5<ò”È…|ÆõG–�9’.²¤¶¸{C"Ü�£øÃS:uØÜ�É“ÃÉéGÞvã èjòå"-þš¬V‰«È›/÷Óã_ó+Ží Ô¶; SÝìÖÝo9ß«ûœá½°ælõTmH°©ÞòHˆ¸?´!�!§™oØí�è¦Ö�]ÓÊŸøJ}@�%Áƒrê˜fb+^Ó”Ø#Ü¡ëuÝë’4Æ ¯Õ¦,P°BM|ˆi$p€Ä!¸hØT`vð®%‹�LvIf8�1ý "`�àž¾,+ æcÖQ +.�†>Ï*PsE% Ø ‰©ð�NqGàazÐå‘Xè¸b¤‰Š·yy‰†ˆ±�…þÀÕ¤oš³˜ý(@¯Ò Õx�ß°±…R5^}«Ê뱈¦@/+˜�1úªÀ^ÓÀ°[,Kòƒ|Øvï”0ê²pÏ^gÉŠºy¸¿µ™ÿbbuFÆOÉî2ƒ�/.UH|ž(¯É‚YªÊ°ZÜ%}•DiV¥›jÓCÀ' fùÄâ Ù]+<&F „5±›Ïo á çÂЮ–žeÈZÇÅü%"D$dUÄ*)FùÀ¬F¹|—Hì*\}P‘y€ì)eÈb„5#ùŠ|na‚¥«ÁQ°œìwf Ë[ÄÎ^Ö Õ‘¤©„3%#”׉]'eªòнðvû°#öä�ì–m êû�|•€©•¥ËÆ,ßC³õWßlÝGkù_ΚõQ‘_Þ;‘?Ðú™¤Á‹¦�ƒž€²â×7ö�óÛ¢´‚š¡±õ³°P*´Ùj]ÏÆr_ÞÎʺò��ÛÚÆŸ{:ù”«Ï5ýÝm�›Aô¡Èç¡Ù5ÖÐç¼^�jÀO³¶°Õ~ïEàcðÙBÚ„Cæl õiÿ8gUÂŽ –Ì)ÉÉUòý’D/0 QfHÁë¿O¬X‡W“´Úƒ�FÕŠÄ©XVYÛ§›„O×–ƒ•>S?(»7*Yúój¡È» õ3•1ê�“Àù3Î2:†‰.ðˆ‡‡¬�âIºÒì§س'ȇ¦�Žév€ÈCܰ,>˜òÊ:&ÕNò&�¢Fó qųël‡UÉá /ÅJÛ¦~ _eÿ)äßµiV=ý"®˜àê.¸¿·¨›Õê ¶ÏW.ä«»¹ÞŸB×e%>©ˆÛ}{ô–Uò¯ñÒÉé §ÖyÇçôtM‘DÁ}Û©�¶žY§GŸ¡YØK¡�i<Àøå3<…d“TýYöAåÄÐí‰ô¤vš‘aI1 ;O›ˆX4xÕÜ·ÏÌÌ ÿÖ Hìøª<õå·ú {aȘØ*!3gBÎ;!2¿¬GÐ:?/¬–àÃOÓÄ^d‹gpC³Ÿj84q”‹¢5=¿á`³¢(éҮͼX°Ùð�ã»Îù˜¨üT‘·Á€ZY CWÞ¬�åðÀý†\Ö¢±,ÀqÀ‚f3ª›©DbT?#œê3xÓãUv9Nä¸fb Ù:"`ŸPa°“Ö#�$‚8u Øõ±d°¾`U8)¯,ëâ›q×5ÇÝÏ0 uY€PÌ7¨4‰e¿>.k 0PË ¿hÀP¯Âq«/€.Êzþ_îó†Þ9›¨HJ3Øßyçœ%d³' ùÐ�{p€vgð T/Eì6 ¬" FÏEU º'i[£5nW¿ XZGL…&ñ ¢âbY ee-k¬ÇðáÅÍMÈÕ'î‹q¥ìйYœ=³Ë÷~ÝFOÜc„Hþâá%º#w¯Ðnœ³ -ܰuéX¥Cëþ{:ÛµÎU˜­˜›\<Û)ÔGYt›À¢ Y'�xt¤ØÀµÇ¾‡b,ñ`Ö2�2Ìf5ðá}°ú—u'z9’RT 23ó<— Tˆ&ñíU]ÛÁæ&JÔG¹bn¤j}œ^BÝ©¥×é¯q%mQ”¥¸Ø<=Íûöv^ ï|-Åji§6H ¯{ä£TÉùG¸fn}«rYçèŒ@Ú¿úÓÊ÷ ïóúâW´’þHpð™_›jι30@=oÙÇùån5ÕÏM ÆOw5pÐéŸÌQ÷]0<hÏQ7 çÀŠùž´qÒDQù ¯Õu;2í»À™IH¡KÍyN5é¨ =�xyOœ«·Ä+Ñ#³bñ¦ê…ãèLâȽE¿Ñðòc®n G"¥ÕÏ`àƒ‰‹šEW¾–¹­exöOb°IÊÑ®¼²B;³z7`m•gM>³šZ4=ýÖ® ðYY~ x÷Ø]8´:&1cÅú‘$Þè(ªÜ©-¥A�Ÿ†Ïx4£"} €‘8èu¯�„hý¡8T{“�¢ê‹?TDIï¿/)`÷ßÑ�újL€šîèOû ¿^EyFäbCzÉ Òä‡zò†üÅ?×pâØýÝA0£,p¦!´ü=y‚šu-y™Ê@/û¬Fž3ÂÝÆoÑ�n†ã¯ 37èÍ%`†,’žûøùÖóe±N§”ItL�æÜÓŒîM2|©5C€Pá;>>¨ø›.çd¹æ~ÈAƾ áÂp+“Uc´I?çÅà@�Æ{“�Qv ?™nZŽ‹ ˆ=J‹Z-ûÉÈ–Ÿ ;ó<™Âß™ú) Ý]ÔÊâ÷œ ªá°*»Yí¯f�Þ_I¸ë½rZ@M»iØ÷K\žxÛ¦Bÿ‰M—�›”4Ä–¹« ºsƒÄBÉ)A"�¦ªQ"ç±ÏŸ$äšæ*zëøPÏI.³raE♵ -¨çêÈQR"pž ðçP‚žj 'N˜¥èFûËäUβù‡ Ðü,Vy-iA­&Q²#½ÁGÝ#æRÁ(å—»§XYšì2æúªu"ÜCdÒGb€Ã€0ìJ\Òš‰œíDb�¸ì†Nw(ËŠVë¡cw+'ˆx€ÌápÀÜÌ!zŽ:gn-ôcõ¤5�Æ[â38¢{†ŽY6Ÿ›5…ÿ�Í~g¼KµÏ-%ÂaÜÀ¦ek}éôÞC¢ýW²{šÔly‹é{R}µQf¬Æi¡Áðí�˜Ošî±—÷ü¢˜qÆd£«L¬Â‹{ÎÖ4J=flCiÉk•&¶B§éÄ`ÎõEôMBÙœPX:¬Ç¾$²>Aþç¶œ-õÁ ¯ÐǨπˆ3T†Ÿgªšõ!Ll¶Íí ÔS ;GµÁJî­¯ë °�ÙЧœ U~E�¥…·´¤]çcÜÆæº$6ß°…�04ŽÓ !¶ÊIÊgA)ÞÄúŠgxrÑ©>qà“H»ÄcI´BHß™îJ’mF)Ý릞›¶•sŸh¢«¬t’‚5±Ô J|Å¢€SkÝë¨�Ÿ9eBˆ“bçh×?l Nb¢m¾åÔaìÉ1yxšémç3à–€¢�%Œ6‚\Ÿ‡‹ªßg‡.WkOž½".®Ù) G bNa��’âãD8PCl™~¶oïJFp]™=\ÝLFT‰C«>¸ª†w‡#*Æ3zÎMë¸V'BßOe(Þm™«ÔUµg]$ÏD¡á�Á#R;2Ý2:§”\Í„&9½¢I!}Ú³¯â˜{äªt` £� ¥š€œŒÍ¹2¢ÙÀº¬¡‡áSIcdÊ ²—Ñ…nLèÓ:D —KpBƒ5K–<ȬIqT`#�¡ ÚÊ é°€s“U`SÚ9¸d )�¢¨YhjœUtüM²B’¤Äµ‰‹X®–Ñα‹ŠÏSÕ“;"�œº�$žŽ%" �'Õ9:Y ËE«¬è˜‡ò=Uï¦Li¼Oÿ}`ésáÔsq§;øøËì%½@ÿ>x?Iÿ7&Û/怳û7øe×pÁPX“&‘•+Ó�(:y ¨ž$ø®^•kÉÙO_}¢™^c0Ó s5<3³ÌÐöŽü•Žpc樑ä”U>Â-†a„½±(ç<ÞÀnŠÈ.OxŽïY$ÞAË�†.4_!õ¿á]îe—0”¼áq†¾ß„UèMå”âÄWž¶…¦bÜË2¥qÞMÓcÎ Ü9�àÙÚ}ïKÊÄ÷ï�!†}Fœ¡B�V³f Ù`<§3Ó0œ1qnœv>3Ý…ÃÃ×Cz žh-câlqu…¦³r­V $pÎ8£÷cà(YÆÆÁêF¥iX 'oªðfJ}ó¬Êÿ*j_Ÿ°NAºòÓµH¹GhÄ øzÁ–(p(+RÃФ!]ˆ¢øTÜ4N°ì>ö¼g˜ˆÂá¾—º§u±ëDT)qÊ…þÊa$²q€§ì¥'!ƒ2å#/jø&¨&M¶vïþÒzI¤ã䛯y$¯ ìÍA5>ÑiEB²È#C”EÛ©þ&›¿½,¶ ®ï]á‚4Ç�=‹OÜ o—5¬KÏnÉÙÀ�‰Š ‹ —ÀÛÁR/\Ò'C©_üI-•[°9ç…üÃ#yÆÖÇ빇ûèC…ZCV•ø�xÔÁî%qƒ½»#êÄÌH7ÍØ|‘]þ�­‡®Mþþ¶ì¼³ñAê?pêÕfEû¡àBo¼ÝÌ[‡Ëîp@Â’«†°:À™¨zR´¾Pƒ³’Q8§„º�¹ —Œëó #·),ÎVb1¦(0\œUu£©Xê«°�3ã;Ñ£±Ý˜Ä…óžø^(òP ¼ûIú’÷#+E—=s5}­+÷°¡jõëØ’ö*˧40Œ!ú1HÖ I·™Çm¸ÇF�º�Q—ô"m¨Å‘€'Q�ÁÉ †èd…‰ªƒ£I±.žæÂRj•FÊ·…øI½õØù÷<¹%/•=^ Á"‹¬vX±m¬;!qLív¢ÈrqZ Fy(–Ê€™ªŸPâaÛi–õõâX®ÝÆä"´ XeϹï3á™ ŒgŸÇEz0ö£vo�°5ðè€?YƒiÔÿ'ߥD 9Ò¢Aƒú`ƒc?�JÕ¼P!à*áú�ΰàÞ&Û Tñ£lW¼¯æ’…¨@Ö�=ÿ" åRßUhèTuo¨~5 ½ÉÇ)¹4lŠîbSRýz˜FÓòpœ ÊOç%€÷±;!¿)‚š¦)Ed{—s1Ï’qìŠ[˜™y¶u"Ôý�ÿ3UrY,Èœû»žË¡ðÜ mº3QèQ ÁI[Éù˜ݽؗ9³^oÓ)�”£ ÌN^ ­�Ï{ø¸ ×ÑœU¡¬ìËöÏ¿ÜJÊ:Êð»ñlI›—i3%m‚æm²$Ïü,5;‚ÍÎkï–÷Î[üAgU�iv§¹®Oo‹Ï©Ë;�p’þ`¸øþ’DÆ0iý¦µ;Û2w|^¯lGÅE�rNåø\‡`ÍÒ`|‚ÕU\†àÄkO‰* Ë|P4NÙzÖÑX~´ãƒN¨ Rü®âwröÆ5 =þ“ö7ÎÃp¿hbØptÛ”gð‹-µìÿ h~ð˜Ž�ghAÀ¦"ço!n¶£½§}¸Ñ[QI®kˆÁWªÌy­‘\Ód‹v}ÝAzžH‘,¥ÞkM l¶8UèfrQ€Ž`Ÿ8mÂêÑ` 0¢–ä4q;E¯ˆA=nÒïÑÇÁ [^xý²Âílä#³Ð’ÅèMèb¼ð›®5 V;8âA²‡©Wú³¢šJ«ì£kCÙ+À¸ç¦G#5šã\¢M,Œ X†C’Ò¥åi.“ðt£ŽèFëA%§[j‰ÌÊ2¤¬[Þ;y7=þñý£³:‘ÊÏá#?Ò϶Üå„EøHÈ1\~�-ÛY Êr�"‰äüÃ˲Ðk�‹`š¢z‰¸GsRTõFeFç9[²“²Ôp èÕ«©QûkŽ\)6‹gÛð'å-¡²[¡7"ï¶c{±iˆä,“žÄ ÆGo´ÎK(qKV£qH=#IÎ=ʳžPtá£�îeöv"ì€ü£øõ`7Ð!!íÝd1žo Üwk9+ˆt ÚcM¶ï§‰�¬²&èæ5lùvZI *à!6Ð1¾ïCH+‡ØsªÜ|K?šCn£Ù'zÊ+e€|~ÙSNû2ÎVRuWo–ép7z,%záj4ˆCbšÖ33ïÍ�·ò‹«8´Å=.8û0no*<þØØ§eÞÏ Ï°mS¿t^—ð9eõ$Á]O¸}—žOThå=Öxð¬ŸÛ˜^N8(ÔÞ ýúž»jõ·›E«ZØ1Y7#êü÷E•ß©v%ž oŠ/Ù#ýÀG< §!jÏ;jí顇�ewßlU“¬›K|BXh,M½ÓÖŸŒƒ¾|*< Ä#Ö?+Oþ¬<ù'©ý|z„7GŽ{CäQR3)ñþz7ˆ?~¸ÆÜéeöÙÚ ÊWh6&±�®uØ‚‹wçžâÙ+�dvuŸ_ºØe¡µ(°õ"¹‰É§ñ`4KQIa_í÷Ï3Œý‰ü¿Nº™Ø·ý&t”Ùá�¨é®åU�8ŸxSvE¼¥ðõ€xM„9¿ÜUôÐ)F&w^¾(‚ûL‡ŠÙwöŠ”Io/5‡QÛº‘YG)"\‡GI€Z2*ˬaŸFs^HJß΋ӧ»N0ó£Ç ^gÚѾ)VcùWþuÆÞ9Ý“§»».Åßw¬ÍïMvÚßšnçh÷_M³˜ÕùÎýƒ£h—‹mgb‹Hð2Ê2?µ™)Ÿ,4=›ZîbÃí6ÃC£Úžøëâ—éý¿Ü;y!ëï{ùö~²>mºcíš«^h0‘u¼yc—»Ðد”§ù­ï2¯œ¥À$$A ²‘ù’lƒêÓä»w<àn Þ¾¨w‡/ác;ÝÀ™‚:4G�[É Gø@»†³`ßùæA*J u¬ž™o"¡"hêþ“GîI‰¶é逿6­YlJ›»©U2`’Ȫ*ôaΰ›b†·KÜggÍf6»ÂD\qÁøôúÿû?ÿ—¯ï "$=ÌT�C W…w ΊNSëfßë�.¯ÖhµÔ#Ý`0þ¬‡—a-¥�©à…q’Ü)¦ÍïŒkÖ¢£SŽ1OH+}ÓmÈKú»QJ7k!İ#çÜ8šHl]ª»ž(ܾ µ½Dß�&$Î Æï-5| ‰Õ5Þ»b•¢UÖ~ …E7§×^Pt�<¸£Æ.øl=à,}d©.µï¥õ æý.t�Ö53&ê0ˆZF2J'éôð�“JlA&*¶A…˜Ý’3Í=©»¥HnÀ±ÅŠ:@â1•ØûÃéÙ+¿Û\-to&ª|ôç»­T¿±�ÊzʯM¢2ÿO‘®ZDôÉ$KXÌ)+£²Ÿ•’.$²µžZáâŽVØvAL‘ª \5õ‰5Y×6ýa°Òþ:méÍ-Öe‡ý7¼´ÉAËn?¬âó*›ýš ÞiƒkX‚‰øN,Xa±Ð¼LXižK~›¤ú¹dwÛ²†bMl{ö{žÐ•T\ìj³ï¥S=ý^‡$× ŒÊV͹©Än ÀH¶ÁÁu¥Ü2Ëo ’8:‡ØŒðùj6I«L“=0"Xš GÂþ˜aèugXÔR–:x›ý̓!²Þ®tº7‚Å4¿*UO["©¥míwÔ"5ª×§ëÂðÚH×y‰;Ú» ‹¹¯¡â]éz”RA;f¤úHÕÛù.´¤_dÛ«‘ëu6t=#Âj¥Hã> V�F½“ •úAÛ! Û™gòßÚí �)ʰ5ÅЧ3r!ª�×z·ò .4Ðüç¥;è;é $á³Ú†¡ïþÙFCÑ)Çêß[‰ ù—åj�8Ú\‘ýþ.“q)eòýV6œ×S9½Ç+°wÙÈXhŸøäM0•äqxpàÔŸÝ�ZðŽç÷ np*üÎ GÔh¡ç:Õ”àÐpTÍE|œz‰¡6põLÿYohc°¤iSéÍ7횯Lݶ�°N±äµâtð=Ù)­†Ç�µ·d$Z/e4UΆÑÓ/ߪ>ú}†8º†ˆà„Ø ’ä,r¤L§ŠzD¼ èPqbV#êf]99I™ÞÛ�=ö˜(]¸S’¼¾÷Ã{‘=Ð’4â@v¸ú`%}¸†JÎí MEDßܽ>r€H_�C$£i�ßAª):N í‹µ¬\/Y²íÙro)’�ø"þeÛ°¶ÃýÃÃåQ’ƈßGÖ.e7@v×1—� ›núE·PxQ¶/zqá>Ò½<œ+ˆ”Wk�^öÕo¹áÓ–¸ˆ¾�—ø7^ÕT ð7®8<±(KÁÔÝ$bihjöîZS�z�èÛFRt¡I»ªtx¿I„w¼È._nVj XÜrgv“9ÇŽ¢(D*ûŸ\/ûU…é°ú{ÍÆì¾x÷•\3(3„š%Ég×>¯²”éá_ô@Dsá@„&a‡¤@�`™:]Ь8°�c`ö•¦É´á�’Ø}y½njÀz¯ØÇæl,Êÿú,[nq¤ã¨>x¡WCº•ZÌ:¯ a-;·¹Rú I$H ŽÔϲpþˆP£çéÇeoÖ¸�_i™»ëuTº�?x¨wÁX¹_Òmˆätô{²Pß��´Kú)’=SŽ Ò}ºZÚ&Ó9v)TìbsA—vRç¾÷Ò½«!:†eÖ†À'Vkµ~ÛŠ›£º¼p¯�Á˜‚o[þøM4¦¬ÍÇÜW ù�`dî· Å¿„(f×ÝŒâ_þ P< ãõ(~¢Xš¯²ñ‡à÷É?~Gaâ—5¨¨R=Ù¢QG4Ý ]®¯ëª :좽7.û ¥§5hàÕóœ™-;"Í› o¦ÌŽ�p •ýžcº§gFBªi&QBÕ>¡AB°ž«±)ïqÊ“›:¹^ÇÝ6±(Gè×<ýæ:t½uüQÝ&â™q‡EÀKŒÓ¯nÙã� ›sã:Â<šycÙü-¹~7ôðݯ؄äŽ� >9Ÿ »î](§*\w”÷1XGR©nÛªâFL<R[÷ÛÃu’¾©K Èë ªXÚÙ¤Aî¬tyO×·³î«ÿÖE tì95�¤sÏ$%m8˜°Dß°™á•ÐÁ=¦ItJÎá¶ArPô]·0„JƒÁþÀG­xêPR€‰Ôz¨Ëps޾°iAp¯¢�){i"µ¢ƒëH9. öçœÚôRÙp踪ÜÃSŸµñȃ­#³_Ï·#´Œ> iØŒž¡h§hÓÁõ.ÎN›ï=ÜÒþŽ™+ŸŽúT6ES³‚˜€ÈŽË4ºåÊéFÁr»¦­Úž¡+mÆìÁå©6­L€¢·-+ý1ó±ù�5{ð×:q3O\ÞK�>lëY¶¶¦‡žy\‰‰hïÀ‰œ—ÝÇ6僉»Y tÿ•Ž{‚ÀcW!hâsC¿üв9Üõ1LÀÀ®s8‡´4gef¾EåÖзMÛ)ú’!³êÀÎ=Hw‡žÅü�?º×žÄwc8Ž£PçWèï±ß=Z—Ï;ÅñÚ¾mœô-ãaÀèA8NO½oµ]hˆp‰þUà–�kÚ!ïÆ”kb Ü]‘Àc†-ß"2  [þlQNãÝÜl¼Æ'!s¡î¾ÜSE› ÚI‡ðô¥icžLðä�1Ømþ³KräèFq \ÿ'_T³výPƒû'�Ö�ÍÆ0—r›½�ìqúî« žÀ¯'é7“ôÛ÷‰¡0fðb,øüÎíüÄA5IÝòÞLQÆž¸�v"¤�¦H&‡ïo£ðÎÏRHÕõ¾¸wâ}WwÙ]œæWßcæ)¿ñn§ ¿|õúÅéOÏß<{êk¢c#[½ôééÛô1ø|³ŠìzúüÍÛ×Ï¿ûåíóW/?®´à3ƒexñìå›1`�U6YÕR&žÕùÈ+Š]:T'y¹s8™îúiŠa£ÌõÎMFWužÃ‡Ór–åô|]Ï@º ߸>Ë …À?æËœ³ÍË¡éuÆå¤˜p½—ôjIç›ÕF"ò>–Çãwž<ý~Y·©HÏ ¶²—ØiðšM|CѰ^[&¬Ï²¢q'‡AqÂ�zóø~º‘ZwŸ¶ÝJvá¤79P*êE,>”wï58)ãÚ"{ω ‰Ël qÍj]pãœA[É¢s¿R5ƒî0¸†ÐÞ…rßÁt5(…(Ѷ>öŽ@*µŒ‘Dì®!+bœq]ì$áü�þs\'¯¿z²L:�JAªæƒøWGpo)ŒÑ ­ â?£cA¤4g+©Ñ”—%ât7“à€öÞõþðîÄ~9t­žv u!E¼/ÐÓcï(Ábݧ¥ ÚŸêôsæ7'“|‡ ÎlMúPTÔZÉÞc H’öЫ—m�ùÒ§‘­°{suEw-�–+\&ÁžÁVewî@ÛÆ¤+\›n÷ÅÑPÎ5ÂL >¨0l�¿šfµé‚ô ¾’G›ëuQwU ˜�Çùóλizÿw?ýêý®�œ~Ñœ6ÍÃtoºômŠ¡âC÷ßGiŠß'ɵƒ!1=<”¯Gæ¿iüí Æ«¦8+¨}C½iæ 9¥oöÿcßcå“ôçýÓ}Ë̹¬ýmHvt{ô¬bl®錕•æêØÃXgàágÏ~à0ûèè†nÞ(‹Œ;øòÜIú,ݺH_šMS·ó‚�¿P—ê_ñœ™túõt’>oÛ�I�€º§ÒÓuS”éÑá!(u?gX-qtø`ïh:…c÷æÍËôpúõƒ½Ã£o(­z þ¾ÿ÷ÊlÐüÙ‡÷¾ÞŸN¹_a Þe×­�...ö‚¼hàXïƒ-s ß ê5/ÍÁº(Þàð8úáׇ‡¾þúÛݸ2ÈÍ[¡;«€¹/o§t]ùê…?ˆU)εáÃG¨ZwœK>y7Kò÷�©ßÌGiYŸ¢‰ø%7ññ»÷ï‡N?Îâ².¹«†•�ëîh÷ZÍ @˜é‘ÃññR%Òú€Œ9ùFßš$6Ö­Ø4t_¯t“Çl]Íô»0øÂ7üP’=µgÓ§B7ÕD@‘¥L�ý·jjD¢->yŸW —`æºPèˆüО{Ê‹¾x*Ò7�šÌ%äHÓH5 ¨Œ—¿l‚ªwœR.> 1“úˆ¤àN [afgBþ`a|¤Í ñ«Y¬Ü+Ïæ-�s]2…Þíϼꗪ,>ƒÌº2›ÙëVå $f¶"5§°™œ9b„üH8_!—*'J:ÔdÐއ•×D³Ú¡äÒ[�„D½³ûÉz1 „W¾éþ–f 8j #,w5ìÊçµc¸ Ã}°œÕ�šò}ôàŸ¤ÛdXþë`XMDy¿¨7~Mu~a×Aæ 7è;ø0/*�çæO{n[LÓ@»ioœ¿Üâ2I-Ð×u,¬Ô)9˜22ÚóÊk å_A‚ºxïàVª s/¦ï÷ÞIls­Á‹hžÈ˽[‹È@j½$l¾PcRÏ�¸mô?; þÙñ³t@ôˆXU‘ˆöìÆ/ás…N„=n�p*�t{ŒäNìÃq�äfßv—v¡´‹¥¡oèzn ? °JÿœÛ¢5áMx¶–j$‹:éA¦qªÞ�ëSåpØöCò{e9Ç7!b ôûQÿ9YÜÖ`f»w’p³”v_Rgc#hc.ÿu£GXåÈk «F¿y?ÈZ¯«¥A4Uº¨^nVÒ�j¬¶’?Ýf&0%ý ¶½9yô¤C÷å‚réõväêJ¢àx�YÃÝ«{#ÛTÖM äMn;Õº›K{AÚë~§»šÇ~(÷½Ò ¤|Ÿ,��‡Ò›a:ÑëIåÎVïd¹V–‘zðà&t`qtOO]ÍÍ–J×pOÂkЮٖ°sP߆IFÔŒàú“àB¸aOM~�/ F .�ð[ln_•Ï6õ„Ç­¾®‡½Îv]í+µ~%p̬m�=l¶´­QtâêÈ, ÀßxÅdÀ›ÛU—Ý —)úŽ;ʱôLXjgáUã;ú¢[Ñ*r�öÎ>–X_sôÃw»Ì6ô?N#½�ºMêcéÅÞ·†dl ˆEy­3ë;ß‹ zÇŽì¯ÙzO[ ê"§dv5v€nœ¤æ@Â{Xý»1¼‘r9hÁ\CgK•‡w_tK3Îzèqv{á}°Ê‡%øý†–âqý›ãÈë'›-cw‘¸#�J<3à‰´BþÓbúÓbºÎbê5!—#!Æ“$£�˜O‰ÅË� …6Ê ÆÍ¨«Š—K>[m“Ý‹¾W|Wtä´EGSÓÃgs°�˜Ê `Ž.;ÍìÝ’¥ Lÿš!«ÿƒ´ß”‘«Ù�Éhëd¯Q3’gµ-‹¼Q‹úćeK#io¢°N”ØõØú}‡…À‚Þ7$JYŸ£�O‡É(äÙ(¶Çy6Jüz»ëÛ]�õ„'þ­Í¡ñlq\Cõh0“eÇ·{‰œá•ÃFƶÞ3‡}þéÇýqpý^ÿɰÙ?òx¯�¾�Èœâw2Ut£¯XÝν6 YgßL.é)Åà;Úü•øí¢;Æ¢eÕ}çðå°ÅÄXóÚï × [ŠJÃÉ Eg[Ÿ*üt×ÖE½)sjÐeت ö"Còu=´p96… §w-_»,Èg/*Ç4°ï(_¿,$ãôïMÏó�4xí陨º#«d7%Icc 3ab‡ù¢­�õÌŽëæ0·&«>x¹~uãÈÇ"š@U…A²üïA<Â�û±]¯?zB÷΢ÌÎþ�€�ïfE^Ä ’[ˆ>;xúT]ŽØOþ�z¯»Ÿ÷—UöÞÑÓÊ]�†0¡Ø¥Œ+Ìm”«4]FåbZ”}CÌôÙ•ü5q{þò)Ì’XÆGN�ª¶6jê»y½’Ot!HNøÔï/D!ßû\1DŰ�m�aï¡öoÒ¢JÒºõ¼Â½Cqƒò WDÒúSXߦÕ;0MlärÆít¡D1Ò>[j0ÙTÄçZ+i� É�ô»úŲ®úž&%B÷¸b·�%NQàü™Á4ÓrI|FŠ2†>¨`×¹»c‰[@*¢œ×TÔ Ðš¥q*5˜neU@P³=°|㌰±Yùv›- fàm*żr•UVVÆÉÚ,û×ÿ|ìÖMÒéöÝK\PÂfFw0ŽÒ D¡1èþ´(n#(¶=ÓŒüˆvmY:å%ÛØ†écë:´ë*¼òi«­o§äOŠáÃíH�3UªœØ—«óÆ-˜Ð‡ôjÊþ-»�ÉÙ4ßTlÍ%�mô ûèÑ$5Ýœ�D«»Œ§kÓZì'ÀoÛ¬)ö¾k2P£¯ˆÄû ÛB&f9©'DãÊòðæ™…3u³œ®ÉE îôqÂH`ŽÈ “[¬P}ySÉÜ-K½ƒ4ðóùüÝ45ó`áâåýÓV]écÖŒ}×óÝ»‘llß½÷vñ“’GD˜&7–¦z=f «ZÞÒT¶âµoC®9ìd€É¿¥á+§5?Š=`S-©ÑfP 6í (Ê%€ð-Õ•ª‡Íƒ»ó«©à&ZF¯b¿QÿYq®]Š×|±Ô RÑ܉/Ëßî6œÍ ]m%×#îвtk~ªØÕÑ­ÖJ„‡9ìψö4åÍEňZ”L$”‹…i0UVì=Ú!ªø…Gx¯™s¯°:[/]Mp&å+Ú³:¿ÂÑn8Iþ?n±ržó¢ÞÕ»×ù5]¦ÕfYGþV‡ÖNâlNã"ú”é/0ÝÊ�Æ…Ðåþâ*;ϳy6MòiÅËô"]Žc ÑqŒï–—Y•ê:î³h{œ­ÆºFí… ’Y¨4›eH S±Æª¸ZÃ÷ó,�ÅgÛ(.ò§³*Ê4ÆÑÒâu’•qRžoVi^W°dÏá¥ô*Y­—é8Þ›("Mmq-ho¹KÁôÁµÌe¥ˆnUºªÇðÐÝÀ§Å 'AÃË)­´l¢õáiW›)Léð¢Èë4~ ³-`�V²/œuÀ]0/–Ëâ[Y'%Ì z«hÔg8=¸fØX÷œÛ§³NΖé!ì®5І�'•žy/ÄüRb }ðogE kd~­Vô-Þ1~ãë2[%åXY]Âÿ°Š³zñèÎäþîÈn©î>Ã?Ô¸ºv7úç—¶dR©«/ ¸Ä‹lšÂâ_$Ù’ÈŒ»×‡â�Î.÷žRV“(Œ?À’>Ž¢Ï7((,³ÇQü ³»kPlˆ±Ùü.&ndþi,OÒ«uãÁ…ÿû²8Ïý'—)Œt¹”‡Md–γ{M396iüQYÁ+ùðÈ/c} ýÂÑÍë,ô§ºL�ÆtlÌq| #<‡òü­Œ…óé8OVÙr;Fv ‹„çt¹Å•Îat¥*7ïŠHwEržd9p¡×{]TYí±ye]áå—®#`3z­LÝdeó-ç\Ã>‰Q[%´%uwÉÅyÔ HžÀ5�CsqñìÃÆàÍñ$žÂ…=”ÉÖ®Sâ.ÏËEü‰öPVÂÌé«)p*$DÌbh$Š™sÝ:¾[ d*ψ/"¼DžôÂ8rYbçÄ^<=-–=£ øP.à†–”³8-K˜–NM¯í¨-tÓŽäÀ¬6°­àJF ¸JL }­Áp8ÍÉz]WxØ]1HzDª ÏÙÔÄ/J`]ÈÅX*u9¢C5Œ,Oªíj] ZôÑÆÈ2]¢ŒÇ(,¸�…0"ªè…PnHP›'Ë*Ef‹º´$šG$”©@&ü?2€ˆ¡Ê%+oDVf_f« Ü”ææ=¢¦Ñ¸h(¡‘аu(‘ŽEéÓÌY‚ÂtŠR"ˆ®Ûš& Åõa´"Yñ7øü¼L¡7O&Çco.þT”¸Q–Ù;¸ÕðÈœA[$'Óé¦Ä?S[tI­ˆ±R‡Ô_c²ôz4PØh( Ý||?lòwyqéÝs²×‹ÓbGÙì {ñz/7´ÐƒqÄÒFp‡‹Ö`N)­£w=Êt6°+‰k -ͺöX—ô~ýaÈÅcÔ¯ÝFÕÈšª}öܧIÕ2!dT�éH[òÒºÈ�·5 Öòñ›4¥ý÷ þü®<Šâøó$^”éüÑkfýY̬Ghßý~9cë*ÿ‹6Öq<ä»wÇÿÿn™Á‘Iù[ûóðï«Kþ–ÿe›0‘‚ok5΋‹äĵÓƒý¬Æk϶+ ÜŠÕxPËÂÎËb³þýû’'°Û×Ëd ·Æ3y ÿ¯·ë™Ïý~à,žù¶êΑގ1½«{y€T7ÐãøuZ¢´Â(Á‡— x“ˆ2 0dø8#�fôäå«ïŸ°¬ ÒNRª»J“œÕÞË"2NZ%zL,lƒNÐþk,ãò^pÅ\ærm»Ü´²Ê<°;îúˆDTXÿ<>\M+d¼wÑÆ9Ë’ó¼ [m½,êjLòSœÈ•ëíŒZ7h¡ý¾¦0i uò$­5_’eÚ’;HCVmØéÄ^aø<ÞYMm‹*BÊš¸ �² �âêˆ$Á¦õ ‘¿’8 $¨×ËÍ*§k†„öÛt«ÅÜZHÞÕw;ÆÑï(e-å¾ÐS¼yqÇÀ+À¨‘´ù2­ª¨uP±…sÐ7󇼂h ç†jºjÙÿ°•š÷š¡‹Èçð¯ÜËä¥,›'!·úõˆŒœŸåz¢Ï>ÞMÌP»�5rìV°åÅ”‰?²1*™¢ÆDË=Ž$(‹Ëx�–ºNżi<‰ÿ2Ék$L:+èºÉÆ1ü›†A‚Qoe£~<èšš.,í“Ö̬‘€ß‹T.£·LóózAî…D l2í²ƒfÁa¶‘¥~ºLq�I{#f).lv¡Ñ ‘iÆ� ²` SØ÷Yn¿�]ßÜ6Í[„ˆ•‹:²™Q ÙŠÌo+æ%Û %ÚÌààç$>+®�ñªàJÂyÔÓ%êz‘«uøl«–'~Ÿ màˆY$ZGybí[Y+ô‘×Í�\Ù Íœ ;ƒåFv`ßUÆ^Ìç:ÝwތѮœ�oÊ´MGïÚí&"4k«:E~¤þ –’”m3À‡aÃÏ„=ˆWÃ:*e›¥¿nHAâKë(~˧—…—p{e%ÓGës$�6À·‘Ó5‰ðí( D€—¥:àwõ·É[=F®ÏÕïý²�Ðeñ6]N²NUL74krhd&êÚ; U�®åy5­Ñ]”^ÕiŽîÎñ‹'§{ò…iÚ¹.O̰áúC¶�gzS¢Tx»¨fEÊNe¶U¢]+Wmûd:M×pQã�9tš¤øûXTd=\tj½{€´*üé¹<À#&o„wžógï�0Ô§ðo|YâqÌ�í�7¸j²¢�çq¹Éé`3uøîš“´“Šw›5x-ª ø%™Diwæ…#�ËB£Å;Øþ0Ž—ÛÁj—اY”ÍŠs´`4ˆNôŽàÚ™D†G„ÇK”‰ˆcANï[8‰0Š Za[Xä –åÇ‘€x™<¨ØÔC„ çBo[kÃ2"™ ²ÄI~(\Ì6 ²’2ßxÎÅä¸pj¿P1‡6æu9IhWD騮ÄÏ„÷õ¢XÎd­ùÖFÏ^tb, 2æòtÐu‡ý9ŠC>y+:MõûøMëÑln)wûøEûµç¦:/üðuYœ}t^?|n_— œ�‘˜ùŠ¡�O°Ú¡ê¢š{“mr4žnxZQkt¡iÙGé>“ðÄ g(¼ÂÞ÷ù%qJ³ È¬Ô /$ÈŠ°ÇuQ,á`Á™ pw­¶GìŽÎt™Ê–^¥ s[:bpIkNgé9œ’çðì qd°óž»æA²´'5 !(TÒ¥ [mz躧-[9Œ‰�iä3E¿ «è@e¥+SÙ³b]ªu’S´€ºÜLáÔ§îI²š+Þ:óÍ’#X³$ÔpÎ2´P‰öX±§ƒÏ²Që^|÷·Ó§¯^¼~òí—†ÿ¶�ÏObÎ 6šµEXòÃA+Å'Ö°8«Òò‚ïYj´{C±.â3ÔK�ö…ÊpF!&êU žëµú¾Q ÚH—3+…:d#Ì#oìnš{{ÿœ“˜gmÆ aAŸw‚ž_Ž…½íʶde›±Np»¤W$;œ“påY‚Y„I õ%´M:á,½ÈÄÙ‡Óã»D-¸8s’69‰D4QÔCƒµ=~—¦kºå³¼AgÎ>)ˆgÒ¢ø·´,èÕ„v ¹©¢³¶¨R^‡ŠokX“sÖËEoaAÃðÇŸÂÿðÏB¿ÑÕ8þáÇqü1¨GãóÖpÖ` äõ¡wFWðÆV¢‰M�”ÆGyíÁ«xtFõÉA|ôÇø‡ÃÉ8æ1Þÿ±cüððÐ(S�âÿ#^??Nê ÿ×§þ¯ÏøŸÿë‚›ìŸíÍý,ß1âðP?›öîßÐïœ}Ç?ݽž„IÓ“0¹©'aÒiþRݪ­}HWï×ÚƒÈãô´|ߦþÉmFÊ_ϸß?cίP$=OЊ—�œ4š–E·ö[ïr 7 Xû#ÏÜ–,ñá ePÒ+¶¸oróÜv!oŒ==—Ìîb�oêoh�(ËÂH4håEÙqLR ë—3– X§¾®ÝÞZNÙxL ~ ¢JX+¢6K!ðTT›ÒŒ=�ù‡,%h-d5¦wÎIÔ\°‡�&C&΀ˆ²Ê*ФpŠ˜SÓrË”œ«��æ ¶‰åA#Z=¿j+öœ«M4&I‘ˆð·%ã=°å3Ô�$`Š· ÜI·lÛu„œ1Å\ç]VÛ‘iKT›5\Œ• ÷’fC-kk{ècë5¦j�¹äð›Í|(K³Íí�»¡ùBGaL´H�ý)åOs5m'õ@cýâ³· ‚8õrʧ›õIM«îd$D;ËÓ¤äŸûÚ³ò©ÛžÚWý mŸÃ8Ol@ ЩÚdµ‰]Oì‘(a¤ ucÌÆÙ²˜¾“3bLÅ�½øèŠX¦á³�Ϩž¼ øÛtÍžµ°ÿ7xGß¾88¤=#ÒûW ƒä0›*=ü*…3FŸƒ%qóŠØ,Æ$ÙÇms@„/’uÏ ¦,Î�ãÀó‹¬P38Z/Ö kÍêm›þÌô•þädñm?Ü7ì¹pô°&¶Ž¥|é7šWà$BeóWVPMˆ¦�q´·¢¯äw„›5Ys™ÿÝæÇÿÕ1'ÿ·l6ïäôÑ{V(—E‚fÂ5ê\U¤I<’'ãøÞ­k„¤vb7ðí˜Öº2¨×³jpxÁШ "‰à¥ßEs;¹cœ ¨æ–75·ü¦š[î(3ÿôdú¯¾}õÝë[Uœõ%OòfäÅM³‹‡èLùp�é¶u¥=º¾MuMž £`ïA¹ñXÄŸãÑ˃VTÖhZ\ø‘Y(º=yù””¶º‰WÃÂYªÂ/ÝT׆NQ]ŠaÒ³ ¯îŠãÛ#²"¯Ó£XKD?Ã%¸?&ïì¿æu­Yq¼ÐÆq†_-WÆ:�¹‰šê 0»÷Ðiy�LßáÀQV_$%ådèÿ¥ùô«ÙÏ(ŒÅ só×…ñ°Ì –F7õ™öˆ¿¹3Ä–ÀÛlŒã²ÝÐ6ô4ßÃÉY�ˆ£F‚¶�KëŽùÈÏ¶ÎæJeob�â&÷Æ“ÏÆ÷Nàßÿ†�áÿ“¶{‡—Ae˜Ã/¢ÞO gRK‹5©oXÖm ] ÇÏHp"½_d5ÎòAõMôKŠ’ä÷_H¡Ü 8h®ˆGèE3Œä •iáCP™8+ÖÖtiñ"‰spIÊ]�T”ï¾#Y@²ÌÌ$ s¢…yî_˜aý¢ù¶xGŽ]qèH䥩’ƒM4µ ÷Æt`¢½Ü«”ð ¬È ò 3ó2AK@ÄéVø–sØ×ÙATÊE¶6¹‘nšƒÓ®†(û·Câ Ó™>týub Sý¬¶Ó£!Ëã¾ù»/�èÆjéà5¡%‰øÖÑ?qÐÖ7|òø �V Æ&•£Nx%Ùöx]C–PsÏê\Ô{­.`ä ¹Áò[.9¤6ü¾JÊwD¹÷ÄçP+‰ÔÍ'ÚY‘/·œ\ÖÛ�ØEG˜ŠÌCG :‚ÿЧc‰_@C 6W˜.ŸÎQi=ä° 6¯ºÁðÙá� Ná®Ù¢Ûg06󆯶0°„V¨áí�°Îo9åe:Rý…¾Y%î^cÊ”å�yº]§š¿*|®qo¹¶ÏÞÍÚ)80øù¢X^pvŸ¿Æ@°­u2‡fráuÅÏq£eY<€Íº¡n©G,t/+YÙß.dE­¾âÈgÏzí8¯K޲dQ‡ä8"©l 7ôÒXy{�ý˜�RV JÐžÞØoçÛ±í�õc¾ïz[ˆ ½>ŒÚÄÑÈ/ÏøìLªf�qß­ÓåÁØÝ�ë²h¿ó¬uZ=½<<9à÷Ñ‘Ê,ÖómTíÑ8N; º†L™¯6Û~,FgŽ`s{K¶_?‡�õw40¼†5++¹ÊV›•wwPp˜ß�õTÒbÌîþ³`ô d•2j½PœÔ¢D… Ж‘GÇenŽŠŽS‘t*T¬åGO€Óbðs8QÙŒDâË"f ¦k•îEÛÛÉAêU¸†­õðP� FÝêlt‘è,KâKø€ƒŠxŸ gêäÇ[ëí³×Ë£Æ?z4„¸oŠÇñåáÙöð¥�ÂÐÜ(VÇ_¶¶²ZGD0DÓ€….·®fív2ÊÆ¿˜KXYŽx]T~¥ 1öJ!Æ�<þ­(ò3}‚Øj^b‰(¸§ÐŒ© /IKM=ê´wU!Üä¹LÿÅÄ+¼]¦ªj#'ª9±Ê·Ý7¯Á´Oàœ£”gë× Ã\a4A)ŠPãqÞ‰ù⎞¦K¼çã8þ2Ù™öÓ �d·CÙœ.q°Ã7hÙMòˆ¸´àb pÿ…a¶‹ N`9]Ðò ìß2ŠÆfÑ�.%X‰¦ oIU` ‘­,çY…Y5Jõœ)@¬–R[X"F!T#)hÉ0<Ë- —…¤Šn¨µ•ðê%Z?0 –�å�‹ì)‡á��îyW¯tlÖkLÞZØÏ”BF‡§<Ïò6í†8eo°ñU¯ó¶¾>²ùýw�œžTD/ûÐEô2l…´h'ÒÄKÎô¦AûÁ&xÊmª©·„žp‹Â>´¶èx”Íi!-<¢1v©“ô`ž%î6¦Ø?N„ròëÄÿõžÒ5­§šWR«mNË*Òàkœó{`u‰…ñRO >²Ö΋ÎB»VM/ Šba7ãäøX…½“ÃÖPþÀZK70™%%j7dT€—@†Ë¦ƒÍ´Ì.ÉéÕÊNÆÃ&ÿûþœÎÞéÅ3b9Ãä÷^m f‰“9oIý&žÈ8 95/ÓbY¹aQ&Ϙ٫ P�Åð…Z6=·–àq38Ëɬiäü³Eóù¡¸¡ÌžC'ÎûCí•–íÉë'ðî–²¿N‹8¹(2|}‘LßÁ^;¹À¸�‹ÓÕ"Ã&çñ%è¡i€ FUšò½ó>VÝôÓ6gÓã¡Ölçe]üý�'š.ÞJåv>=ÛdËú0Ëò,3º¬‰@u“1›ákD£vüÚTÔàVí~À±Å#Ü&t�GŠ!¸^§ u¡Ö½¦�×ÁX ® ØEôýA[hÁ;_ã¯^Ûû¿1f3Ð’ Ð @ér•–µ6÷5ÿÚlë�3«¤l’®7ž'cž†gmÎD®-40V_uÏLö�øœÛ¤í lÀ�gI+éò�þ+˜XsùñÙ\‡ôf3�"£¾piPí"RÔE%±p Ž=¶GEö~¼Äu§úÀv<Â@¿Íjµ=@¥èš�X_•ºÀ(UqØW�� ´T@�xß ÆåÉ�qÇ•¡â)ÜÌŪµ�«é"5q”€Åæ? ÄÄ©�P026Xøý¶C¨¦¾§2Å=ˆ'iòKãÖÐÏfÚî «$WÊTIîP�$àb6Þ’ƒé¦ò+Í®²|CܳIhªãôCbø|Ϙès_½ÕØMÊ’kqѨ± è\6ÜÛ¸w‹Ö6 -3Æpâ#øRÔxc=2ŽŠ,AÔú\m7žk¢ãj±3O†÷az,¯R›‘1t ¹#Ö©aé‰q4c'vd±˜Í ¸øÎ¶îýÅð”6[S@…�5TKI©óºå[oYÜ%8ŠŸº#´€Ä�»Ôñû*Ù±ÆV˜½7Û³VO¶”gC ïÕf”»uÍÊkƒA�CK?‡ËAìž‘¢p¿„AÉÂP'ÍèíßÞ`GÜî.m|$þØ¡p6³Uö6-Qa›•PÄñ5n¼Êf›d©��~Š':󇶊¢dÁ‚%å¶9¬Wî°Fö0'`àÞÄWºäÄvH…‡Ð€é-�Ò~Ii¨¡å.³µì ©HÍ~#U¡Ù¸ÒµÙC8 oË$¢fpµFïBqpüË´� '›$Vg㇣ 6…c17Üm‘õ}±72\ËerÎ/{î¼ »@€'} €È–k0%˜T$‚�õ>ˆ±*�zdIéa �û\®8&úõrᛎpŒ«ÞÔd K8È»$Ÿ¹|EU.Z@ƒ‰B<ºÎT£3[µFЄÎ7Éyú�©Ú:3ó8 _rÌ1¼~™³ÿ u÷ع0a¡Ö&8ˆÓP3µé0ÇsLþJ-"5¹ c@&ʱãÁm  ]¨SúÀ-öYãN_û¸éM§96•ÁLm8ðÕŒ½çîëU#njŒM®¶ ‹9v¼&žÓ•} hù;Rݱ) CÀã,qg\–¦ÏÝ<ÜÛõ»›Þfu67)A™´ŒG|"éúI¬ñv™%æB‘Œ†ZHlb2xL Ú½·Àk‚«Z‰‡ÂMaÿ¡ß�áKj†‚ÎÒi‚Ή¤#NäJfp xƒiO¤Š\´ãG¦Íå_Äã',e Óú©íGÝâ†Îâ‚™§—ÒŠx�YrêÕ‚ÕLN¤|Ï€0Ô·‚Ö¦/W ’+ስg݇yŒMU AC)rÏièú]äfy ½iÔÏRbkÇÍQð#×ËÐüiLê(Ûã9À½t.�(J�¨q{èòoò ú�Í [œ¢^4û%«  S#¤¶ëÒp,]*óÊ€5ÜIŠX+A iÿ»:†¿ûöŽÂÉKN%%D:Ë§Ë ]6´ŠCñ°1àóîi°ŽÝsçà=F†íN Ãõ…‘‚VȾ3RŸe›nT¤�P§Nð`'Ƙ†‰ß¡†pX‡%öu §“§z µ9�…!ª±8�È‘¹FÿC¿õ› ßbòöظ·?òº+âYQ9`Ô 3FŽË”æ.Z›¹µá5»<ä(é4˜ î¡ÝñU#mRÚ–E†‘¤‰kjõ}.W��X8UÍßM«s6%³º�¸h8—œô¯.ø—ŸœpöH×YòÑä%ï·Éq{`à7J麈G_J—ß·ÆÃ_o^®î<¦úÀ*8Þge2[¦[/Ìy~“B’~;Ûd„]¿»÷–Ö¨�ÐÈ€j�ð½”�Ü™6tXjß=ü‚_¦ø�Ú§[“näDwH“î‰Û§Ü¤;¤!ÛD“yÉXâ2r.ô4ˆµÍÀ6?«’,'a ¬Ð+޳">ÐÍ­±�*kH·¥Kd½dc/nØ­Oh­´}ÍGŽ}Ñ%|ìT¥$Áݧò±–¥l¦x£ŒÞZF…Ô"YéÖÒÂå*mµÊ¨Ù%UÂä°úVg rVÛÌ…PX�Ê#N¡Ëw¨Eyºä>)„I{†‚¿jæüA#Ȇáe¥t$¨= àÊOµF¨îÝ)dUtv‹>;–.já·Ï²„ú ¢t,«ŠRÄÈBÆ;8`×*Æ€Ù…bˆd¼°ÁJš�Jh»r™N1 íkéÑùÙ,ªÍYM|pv½4­Àg¤> -Ö ‚4ªª¨Îc,^†áËä¥5Jë™Ò:=R˜õ%0,êœv±q6Žž’5®èàq–Niܱs•• ì¨.Äž¯â,9Ëp›Œ»Qý©dkžZã,4ªÍ ûXrRµ ¢ÑɸN óõ ™w©$Ãhái&fÏÚ‚• 3ø Íæo½Ÿ_íøz¾5÷T .›n‚‡XMV{Ôñz5¥Ígqàñp¸Û¸§¢›3bè°ÜkÔ¶ô—2§xôÛ�xo°.·Fpì! ÙU¿¢o«¨)6v×ÉRü=vËU�ûªÜJ•Æ�õíê~L:äÇ»Ö7P%1ŽŸù5û :ÖI¶ìíÏ(W¥rîp9ìa�²ƒo`·«Þ,HÈyZ‡MLá }—�Å…/L2ý%¾•|ª¹GÚ‚3;§(Ô´,¯Ì‹Q‘[ü28Bc¼$Lh¯l|2¹“=²bKAÔ7 [á{ס>5)vÞвu&�8�$wêïηOîÛ„XW½ûÇ@ŽEÓÊ}8itÚkTS÷¾u{%;ÒÀn›%9¨x‹[ôÑ•èõ]`FÔ“ŒrààÐØuƒ±ákÌ&Xš�±I­=®àÍq3;‰ÊÛwÉ{¨ÍC¾ÈÍ·{@àp±µ‰V]Û£Wô3€pç±üÐm39ƒQq‰5c.ÑGûYJòY¶r­¶•[© 6¼ùî@äý ¶Ç`%Ì[Ãn{HÏX´rä"«~½­Ñ=Ž¿€�—i]ûèú�Ô+Y ßJòM­u 8Dt·5ëŠVÌ5n y¬^{Š ›.Òé;Ñþ¹Æ”ÈÌ4I¶&èÏ8?O?Á€4¼\ÈcÞqR7až^rŸbŠx•uá#ûÔD�µÒKÇ*c;‰£à ¥0r„“€c·6”vŠ:èNJóŠÌbŠ«‚L0+ö�“zOó�º ûZ¤Èí©¥lµO•n˜¡›ÅS5�ˆ83<ÌñÂXMŒŒA÷ÀÕz©�vy‘‹Ó™¨¤•àÍèyÍ›«Ú†ö¹é!)´õ’†5€dQ¢³Di‚š¢�(®ß)âÖ\÷>¥–âjËz³kB£“ÍШTì„î˜ÅA5ý’¢m¸y8°Å4£¼håOíB÷‘3ôŽ›$Ê5ny4ø&T®wší°Ûn ç[¾?æ¥ßÅñm0éÛ°õ�Mu÷ûNÛ?Èݺ- �gqáÈ´‹}²�Å[¨3×^Ý4Íúñh�b*’“§ÖP¼p�5»“w¢› y$Lé€N:'´YâÛbG»î@„ÜMÙŒ�ÏÖâk‹CúpëK¬·ÍŒ"ÏN6rƒCY‡ÃÁݪù”¢qÔ2ê¤ÆñMQau˜²&¿™«7\ú h0N®�Y;á!Ø´cã'›5²ÈÈë1sø_�ü%6ˆi§®�¨ÉÉ0lh†®…%5"ÈÍlf”o³g36da83FºÕ¢>¨g¹�L¡zc‚TÑC®¬ÆN°GÝLÜä¸ÝV¡`º‰øÉáå“ôð3Š(û¶Qg�ÎПlE�Iˆ Àjî¶Ä(ÄvÁ<å/R³*¯Æ®Å håÐrø˜¿XG‡Ñä$§6 Ú�»{Ž‚]/víV:ñ¼/xN‚<ïvªÎþxtbÞ�/«û‡÷`.X7­Ä?»;)þäøØÆZÄO—XD¨<|�&X‰=u1W÷€ƒW¬½X¤${OXÚ2W´¿“·¢�Ú@Ž>L[7¬ÞeX´ -ž‡ÂÝVæÄÞõ®e/ƒ €O �øª¯ûFC¯ØÝtÏ-¤Æ|ÎÉ"Ž›”Ó ³jy+vò¶Ú™ç€0ª~-áŽ@ćqãññ£˜ì5»§ßôÍ/£ÒF­ÍÚZßãhd†9, Àâãj©(ìã=Ïw˜%Hœ“óbîÈ“òä!¦…›%vGÜrW¾#Ìï¸-K^«ó^×u»ôu0@í)_þt‘)îÛyQÌr¸Ì1žc3]m_$†Y3®õ𾯵XÄxrJ¾'ýÆs|K\‚ÞÒ‹)„yjjD°&Ž �@’0 ùò‚F‰GcÇ©Xšæ.aÌà\äp0À´vJ¾.ðï^ŒF¯¿<8úirt÷Ë.v/¾Rç¯c„¡þp _ꃡ)‹(èx&(è}þŽoÏ“çÖ0îío¨j£q� i€¼qî–:û"ãÄ/q« hƒÈFɈ%~:»¼¡. ³á’€¥uìbÓ¿Ù©ôa ÊÆ¯Eb²(Ú�6~0 þñ<Ã,-Þ’´æ(D¹4cZ é‚¶FÇòcGå&5š¾¤+“¿1/6ç‹`ÆQ]†*–ö°1BM5åeòòæá:{Ð5 ”GùCxî¡´-Ð0;®& ø²ö§¤KØ3Œzél{6ïiõYë¨ jS–¿7™ xÉè’ ‡7,P~P_¯z:yåI!æ*†Þ5ýeOÓ_ú2BGÓc�‚»÷ZÒnäÀ♼hlânØL‰"`nPí¦G0•€Hü¿)É»|‚ ì©ë}⛋ãaüP¤“ã��Hд.ûû|â$I;ò×ðè íÕ…~4°�©­eÞÑ<&$nÔŸéfµS¦/c™B ¿¬¿Áe4[¦N��¼ò œ1 YñÙ2’wM°DAب”"/KP2ük!w¥Ü Ä3™vǃ$O:Ó�[¶AJˆ+î”� ­à@xi< sþ\…ëa‹jÊÛ+eÖ,ÚGÍè¶�N¨LlàÈ8ú|ŸCäXß iªÓ!’¤Lñ:´R÷ΡSÂ;�ÿóc–gµ3:jëÃ()i-—f•å;gOè›îCÄÕ·„gã°Ô̸$Ø–5D_3]$�7¿µÚ •€IV|aÑ«¥R WOM…mþ�hnÜÎ1E”ä•Ðë×2¹i44ÚrNXzùt;˜™(­½^y‡­×`z°•\p“ÀA8ôÝ‘©O´�ãþ—ŒYÌ„6¨ž#×+@ÅçúüN»¶`½½j<ï‡;†Gäæ ¨C"È?­ö¯þE?¼ÔañN|h“y{ÀÒ#Ά‰Sј-‚ˆäðO„zHÒDë«R¶áˆ&¦~ŠM]`Ü)GɘŒmÎ…†=î¹L�µÁ¤ZckM‹褎¦yãlQjoÚèÒ¯¿šæXxÇì•(Äé4ÚBí€Rô(�òc#>BÝР};avÃ,Úッ^“ö-õ°e÷FÊ]7«r_ƒº„“ÉÀ›õ‰ËõðÙ` kˆ™I¡ ›d¯)%=,ŠU� Y½=è·²c…­ �¦³yüœ˜<1°Ê5vºï �p&ÁÃ)Ëä¬8h\bɇŸ1b9�Ìm• ¡zÙo&BOê¨Ò_hr‘‚�jÑn5ŽÔ2±çIí$VÑØF’åQm'Â`~N®{N…µ(Æ…|í4Yn¼Æ´;‰mê§H ¦Ï]k9–]F’VVh2ˆs<¥¢SE�Ýgî�[f û%´Àc÷çÑäÕ%óF›«©´F‘ü©¸ð}ÊŽ#ƒuž’‘„}¦£“¬Šw,ÈœÂJÝe] nÎ ò Ù_ÍÂŒ1¨“!ûÙ&‹ú�c•%SÇ èµÖêÅØâY™&ï(#ž—9›EÞ¦Ò*ü” 5r7ªê*åF/ ü©Z(è ÑXǾHÎ@-qé`±VÕƒPðÀ%Ø„ä~&ƒ%5âð—ÌP3’æ%+ŸâÖ»À;8?”_Ý"n\‰;xx‡–¦âq´«€xlBW+»…Ž!  ýa�˜8æÐÉxé€HÂ::š\‚=šÚôÞ…rÒc$U1/RyÛîÞq4Ç6­eìå5쾌Úh­•-Cæ‡c¾täj5_îŠÌh,”53L€ŸÍûºè¨n4¨åW»Æ.´¢h¶±Ñ¥è$%xóÇ/ùE|Xë†p‹ÉVÑ),i²®š*ΰ |¹3,ÆS,+'*CBô G1xâ͆”½ );âGzSSwDÇ\:Ñ1šÏþ›F†r­P–�= ð_Vß÷ΖÞéžî÷2]6ø{¹AÍX „÷Õ/E–×þ³uÍú6"­u¥ñ7s›§ûÍöæ ©®$‰xèM&ÏaªjâY– Ôð–Ü�MBæ zà‚��Ø— :sdÔ*¹ŠGÌšãÙ|Äwãü@$Xê{øúÞâÞI}(Žh½üNF”ûæ¼Î9ÑÖëæv—ÿ¡ 3Ô¹î,œÒvÑ¢·*Há]ÇÕï�JßbÄ–[‰šÐqÛÁv)Å,*›¸¾7¹Y‰ÀD™LJCA¼7Ϭm³¾LJÄŽÂ’MÍ�gsüF>ÉO“d}¥ l¦Šá¢kM !8DP§¸¢®/Já†VßËdu6Kd™æ�æP<£Sà’ymo` �‰¤¿sŠ› Ó„;pˆB÷EË#ŽB‚:‰ü®¯å%lFgØ/šº¦ˆàIË)ë+ŠÓÈérŠF¼­AO•÷ cñí¹KþÙf ¿ŸYk†"± 5Q £æ»£¬—“®²¯^~§°¡a •\ÇЈà?ïÙcWY7<ÚD)$ ¯<”UG$`“Ã…£±çÚÏGâxÚãTˆ€#ÈVã.ça;·Êø¾QnŽlÚjSoHLvô dU(T MÁ SÓlèõrË6ô\ev‘½É0x`m»GÔiøk•YÍQó%[RH?@°„Æ›þÊóŽ3å–Só˜ÜïNnÜ�®M¹Ôi‘ÃÞkêý‡[—2>~ÑÞ´Ü�LfYχÔHÚÞt1ŒãÑ“q §ëéItž‘}–fû�œˆQmj»;!ö»$ŒÀõ˜Wú Îð/øis‚§ž½å ê³2܉êå»ÈÊÎR戗�W[¨ >µ¯p·assу{tP¡éAÄh pûÑã…b�î"‰OŠŒ,è6äSE¾�*‘Ú©ZT1”`OŸ€Ñ ]&D—/Zs ¹DD/ÂúdRJ� d:Î&©½À 鑃/wúŠLµ…½åŸÚz‰µ´8¤Ë# úS8q;ÉÎAnýd§JŠdN;¢£at­}ªÖ,§ðæpÆôµýØXå¬mPÇÐòƒ¸˜ÍªC‚¶¶ØbÇU0`?5‡Hñ±¶ôìá_»ØWH{!SXÍp D½EÇÄpŸzÂ3 ,ÊYPë²@‘„×FËÀÃi:Û°G }ê0ƒçdE{¯�î#1é,­kÉxWçÕÓbº(“üðEBÕòðëvÉo ��rJÐ_�ñÐàßqÎ×="°ÈH(ïÚr‰2Ó€ÐíIžõ‰7UÄ¢6ÉÔNè%USOÝP2¢7DüÌœ6Z²kËkÀæéʆH#OCã#…™P‚êäjâhLŽï=r<Ðnp8:æN¹ØýÆCME8`Q)½ô&ŒÕ Ç£�£m¸Ñ¼ ×'l~�i„æƒ"É/�Aß@X\h¦Ç<«à®v(²?ô볤N~†­’©±›ñA¦eQU0±;�õ§áØ"vTw¼F��æÀÃËŸ¹÷·n”%â禵ñ§›øR[MÝ.Äëk÷|œ×ßÓïz‹cÚË!ºg¿×É‚w¼¤‰ÆÍ:ú¼49V'�Ÿ9YUl/|ÅYÔu f›ô²&âTÏHÔ‰&«×Ðuæy».„«I•ˆZ lÁÌ ºµ�Ó©3Ó©jER�Î¥ 8í’è'bíÉMÀû@Épõ‰Á¹ÅƤÌéý`ð+÷T²ŸãÀ‚Zóþ¡ÀŒÕ×e®>VÖF¦ÒmáóŒWäEî ²—×}®1Ó·ë¿Ä.$Ü8 p›½Á*u«$·5#\ìÿzÐŽ;=h】ó‹Éh7¨×ß(ý½Ÿ¿5)ðW�$A¬5pæ:lªYµ“ìNe7/ÓhïEN–;û ƒMîßÕSØžO�»úëØÆ-�Ú^�v;}tÿ 4õZoä$Ž÷ÃìÒ¾†œïÈÄšüjÒ=[£¯�û—âçѶGïͧüi"9õTN¦ûŒ„+à>`É<°Âå¶‘þ‰oÙ3Ù~Y·Úªûí“hšÖ³2›»¦Içé~VÉgϲä8h…¬6rí¹§È/>=Œ$˜nuÞ™eÞKþ”拺|·©hù¯+˜Éö_ðþñêcþy‹�?þ×nÔLgÈd°�k³Ü{ÔãC*ã*Šgüë�Ò< sø {àèÏ€CÖp•ÚvK†ÏàpYœÃÀƒ£8¢úâZËÏEˆ.Ly€Þ}€ãKÎ*Þ!­†ûŒq]eפ“EˆQŒï…!4W�Ô!ÆDØ5 ô×ýȉQ2·°3�¡;XÒØž÷íÑ€—iÝ#)ü*ÓôÈE"…é¸VïÂRnX¼[9O¤�LÀa¢±™äpEƒbhDâwÇG÷ †Ê±“!¶ÃÜÔÈ[&Ów(»%èò§Ü ‘9ÄÜíüM<ÉÎL–˜«•Ì+1ðK£è ¥›­€ƒûOPúž%†Fc�šWäkU!f9NîÆ%h$$á ý²Rø?Å×UñßÿC¸”¸L§‰m-¨D V¢ð^Ãú&•Ÿ Cê…Ÿ Ã(0ƒ+-ÑΕ“@ß�‚V_áò:(ùnãoh‡ZP›ÉYa2 >~Ë�fíVÝšgÜš¼ê7m+ŸÅ]bW–€Ë„R‹‘„Ðyr||ÛÄ �_$W/7+÷;‹w1gÐ1gý΃*¾S‘QŒ9ƒMËç†Z; §> t=ª)K¯´§”é9Ëæ"qäéUÍ�JvõÎðî!óÒo�™‹{ŽþŠÚ¸% }+Éí¯CÖd,âkr »¯U6µµ¿Ü28¤Ñ©ZRB|ZY ÚTºw²¡ÜxH>ßzl —øö?z¼òlìä~ÁáÔ“Î BÁäVz2Z$ë¦,\Í“eE²Acoãfí7Å_¤ù/ â~]LÐÚ¹@/aì÷hjvœ':7ÑŽƒ³{wûÍôž•�‡Ðž” öìn‹Ló6ĤI °ïF3Å>>$�ñžI@yÙ™¤R?ý³~÷}3q¥O…¼ç˜ø¿— ‹ï¥~Qß§¾‘… øÆ&} ®·Èhü¦LW¡ä°ðÙ]¹Å–ºÅ"wÐŽ CÇÓ±‹l#‚DÕiK²È]‰� S<á ÊuœW&ÐY-ÃBb”ltâÆñÂEzS¥1ÞVm!K›&Æ…Ç,.¯­ß26v,{%áv:dh?¼µGÔ!’)ÁîqÜØæœŠQŽE›]“ð¥T4ñ“8ÛYL71ôžƒºÃÈ5x‘» %cÉâ�g°w¬ Â<²š¾ ½åéºûïۛͧ¥¶vÌ&¨ š¿ZeÍq„“Ê0ú¤®Rˆ&Ÿ£(~ ŒdÌ¥SL‚ ´Ⱦ½xe3²(¹K´³Óæ§Ÿ9‡Ò².•TïØ…¿Ó~ÀyÝ´àN�Zè†& +HFÏݺQEŽBØi¹Z¤\ ÒåQ_¡œ*Ô2"ÕïWÉ!~Âv»Çæ4§(óOŽ�OòÝ”žêÎy}Žùã¥ø�!:ÀxÍYäìM#SVyq™­È±ÅzJ¥2]†íÝk9~Oc±lE‰S{8ªŽ(ŒÉêÓÂìŸhÔç.dÿzßwv¸WLÎsvÝË{d%­f’�Ä?ôøú/Hü`?ÿÚs¥_6ò[ÊÛÈõ¹n?ïó0�p4ó’�Ü!v z¶)ϲüð-ì˜"·¨ <ꆟ`"_–§IÛ@ôt3²ÚàÐKBÜ�5²€‡x:ãÅ q¼ ЛÛ(‹ÕíÁ“‹ =ž›bÉ>ïˆýØ‚wÊ0k)ùßTZ/rd„l !!Λ$p”šK—«�k •˜Ð,!;r¼ÜüŸ±[(Ò/©_³QÈ]0JƒõhÀ{[6ã—‘‘åá ÛŽY5‰Ú<;>ÍŸ3µk§àE½&ï4ÌNþÊZP´ðÌl!6»#—ö·ÑÁA|ô:Ó+%è/&4f©&¦Í1aÍMfpQ5NÀX}ƒuA˜=ñ±)mµßå:ÞûŸ@'ç¼r�×Ã6›}ŽÊlƆCGTTªÅlom�^µÅ^-ª¯[LáÊ 81¶šnÏŸ�h/{Ή£¼è Ùü³b ¿«“ÅWæ Ä=(a8u’¼ð… 7ça„U’àOø—_7É ™2†Á8N±AN$÷ÍjQÌ�|Äñ¦®ÞâsÇŠ‰ÓI0q"ŠÏÈÑ#˜ízQͫע…éÝ P¹)g¨p�!×W÷È­´[EpWó´'¶œÖò´3ŽÜX°ÜðíÈ)úËúW^ä¿¥e€�„žå›Æ.«}u1˜�‘£ÔʉBÙP°¼ºçqr·À�Ûd 6­^7x‹R,§‰ÖM±Ýƒ²C^­÷§æE.²¸”˜3—ÚÌéAÔ›'®Q:.j]gÇn_ýœíé•‚Æç'.l�¿|&\�ÙS×òÿ2yÙz×Ùÿ‘|BöNo¸èÿÅ6A›Fç ”4¨|ðŽ…ÆË³"‚¾XvKÈ'¤]Ûê¸MgãP±“0š[äcÎÚ»�i\aN4c�D=Ù„[<âXÄ�{êG£«�øZ Šl‹�vƒÅ®>Ç#{(éÉb°}ØœUÔn’”\ Q%».�Ü¿¶}^~–âu‹È£óŒS˜# ˜TË}‚ðm†Z|Ò{…á¿Õ¨,snH¾­ÐT�ä�T¸–aÐO4jS#JqFû+°ß1Û 7QàmU²ùÅ<«—+ÄÄ€ö;߀¤ÌáæòS·êéBoõÓÅà¸6ô…ÛÈMáõ÷k]Ï#"Èœ´%Aó¨C¼ä…©äýcx€C:Û­èvuÚ µ1p�›©œ6¥ô8¶Ó]Õ=;{EÁ* Ä;M„ËIm«^€XÓ² 6{Äg©Ä&w2g…2€Yaj¼¹X²<¨&§ 7Y»&‡’ü†‚Ô # õØÉ£×(qMJvèÜ;ªwzp¢Ï9ªv ªc¤@uÏNp�Ò¢‘-ê÷}ÒÕ÷î"£‘WeÔ•‹ˆ9K¶�ã ©#SÎ6ÃÔ:Ò+ÊÉէž@´bQKÚ»ŠŒŸMN‹�‘âÁ…Fì­Î]-IŠÕ&ãv V¯:uÔÜ'´ éåRÆ4«’ˆfð»›3© …rÛ°›óÚyz•!JéÊn´EOR_»\iä×+ݧ\é>ÓƒYµCCÒÔñ¬ØœÕ*\6“±ï ãz/~ìR¶¥?8T�þg@ý;^¦W³Yõ-²�¾ô¾�òYR-ÆqúX�äÅzj¿‘À³¡%ÕĽÅÌ Š�æˆÚq¹[*H¡Ùñ�eóÚ­Úá°­ëÒ´¢ô<îJ�©®…°Wõèy�?~“ÏA›ÿhu,­�j¡ï¸ƒ8�Ð`8ÛQrô?�7†ø0Àu�ìÑåÒÔj.9«øA!WŽlã¥|ZÍA�¢Õ· àŒÔKÒ?ñsô©ó[ p:õð N<|‚vŸ7H®~‘¶Ö�Åô¼|Èamü†¯Oî<æ{j¸%f«$wÕiyt®mâVTé�mË5P¦ëêßЙÚ^ù¶+ÖRϯ1ìŸr>ШàË“õYæà üv, =v†ŒÁÀ` 0d‘ œ/ÓžK F?R@Lƒ8À‰\ËŒEFWbË#qý!«=›áˆÓÆC£ ‚Qró‘iŸüÆ’žMåÔ_s�«™Jøñg\Doˆ¡¤9\�5f j³f#?&Lp-Ò–“q³„ÿ,³Ç xPSTÕ&) zmÓÏÕ‚a!…—Ÿd³àI1;Çê@'NGöD´Ë&Ãß–¢ã\‚Fù˜yB�Ëçe±Yc´Ø7ó˜‹r¡oñ^˜fhEª ¦>k¥yÅ-oªÞÛš«8y[ežÖÞ{òòÕ÷OD‚³:ÎYLò†Á^q\ZT.p³^“©V–~œ"ÓÄ\]�Ö„]RÉ=¦¤ ÚcúÌ<;g<Î|V\>d-~�LßaØÕÀÈ3£¯˜iR8olÃô’¼¸H&.\œÒñ.î Þ ÉZ¨»›uù:6c¨Cð8âù¡M!¥4qáI|k/»áLl¾sËU¢xBú s4WÐÒ«}]¼µ$¦qÖ3w˜pŸ3 8áöË›7c, Å/à6ÃÐîx]gQÎ�Æ¢ )î¶Œµw*jåAüTÐ(¢øÒn²Jý8Ž¿$(}õâõ“o¿4VºÀnŽCvÍ:Êá–­ÅÒ«šK"4à›Áê…†³¥XZuÐNêŒÎ`˜4HI‘Žã/L<¸Ÿ_É¥Ì4Ð+f¦§º(ÓEé)�¢‰FYˆ_¤RNK¸Qs*¼AØ"’Žü¤�8xƒ À-%™ò¾d?¿Tb£™ž+�`¶ü•Í¿¡4xSHÍî;cÙóK¶õy3#¬6Ï& ëF˜±��#¢Qm­¯léºJ—s –äé$H\¼>#[¿ÁX´aò°ÊÉ9E�µ6¦º�ýC%#móˆ»grœÛ»t¼kVl4ÕÂ÷ÆAÖ¹V¬6;>œb¼¼�  RŠoa�©`ME/³Ö¿¨a%kÛo‰Àš{ĦïIX‘>†\íÐÞ8‚@o�È^cÄþò¦óaY+ïMv‡÷}úéõÒ5'Ö¨ô¤Ï†IíŸm˜¶Ê•"7#Î<×%R“ÇÆœŒã=r¯œ¡µ=§ÛuÚ;ÖPœ�)Dé“6bI¶ V†oc¹ÊC{-QÞÊQrÎìÞu‘«a—heyj‚xÝÀ©¾Ü¬¾Ð6;ówÌjX…Ì"÷à'‘‰¨Soc6TÈþ÷ ”±@=¸ˆw-*‘(0ÄOTH'­'¬”i�® ®ã‚œ]±[V*�ZÈúø£ü¬Z›šS�?_—攦üá�Çñ²H¨t¬mþ0²3ɳq|ïà!6_*ýå 1�®Õçk¼ñ@MƒoÇ$÷Täž ÷Mú%<ëÆÍÑ/Yq¼cÈÁY2å¦z—,/a?dÕ�ÇÞ¯{ØiaOˆèÎcç—nk뢨9GõçzÒbjþi°õõš‘6íþnjªm–¯Ù݃ nõ;÷wÝâE7œß ­°ûTÚ—×Á$}*�ñ_k_†ž¢ZÃP§"ôÏø×'WIÁWZ¨(9HÅÔø•ºaÓÙ¡ü~Ðo¸dt&iÔ³:RPLÂJ=9Ñ(e„bå=m€ÔúMþ./0!ׄÊO‹ –gãäX,ÁÖ"#MbþÁŠ=òv†Él±ámLŽ5ü`›f=¢î9úø“•7K5gfI�Þ<Ûág]óáÙ;o8íãö\ùóö– 9:«øeò’q~\lQѸþf¥¢¿V’Ä×Ô<,]Ùä˜FVŒFu<)Üá(V›Ä•�L8¸ç;o5Áîë8ì>w-<‘Ç߃Æ�1Pmì~?D~/¬ì¤+IãØÆ´ XðÓ2X½éa2æ;[ôª±ÓÚLj6…Ï1œ>Ë)Ú‹fÜåÖw!êžíSmQwßÓ$Á±ÈvA}ÐKäö±OB–U ü<2x,v; àºfÁ+y朅áLk’žÚŒ¸¥ƒg]Û7_™Ù™3¯®'øgþ™M�Ï�Ê(ðÿiî¾eꥷKÅw îÙt\I“³ù ÿ@íé³ ÿ§ï;c®i,cÀÀ7Ê×t#Ý…‹~S–VË|¶í^~ÐCg}¶_¬yOƒkPêªa³.@ð…ƒ¦c @-·�íèZ•ÅÕñ"Y€Š›gYe0%Û£x&fX'lXï¨q¤»Çéã™î­í3°¸RÚ^èáé7²9Ø,ã›ÐtãE^zj(kí™�‡ê#cÒ «A,L3Éúk}L6#ØÛó“x{U+,ñ�ð”mGd Û䨙ÛzQc¼gé¼(Î}>e·$pŽÐÓ»¢ir�ÖÃÆICs{R��òÙ]s‚ Ø^«Ÿîû–rºþòé|jʲ�ŠŠpÉtº)“év¬FÔVJÑÞI*]ºXS|¥°õh¸^6éQÌ&¿·f6¹‰"s­agÇÿµÅ·Ê¸pÄÝ:AgMO*v)•€ÞÇJÜ0‰‰ú`¬�BÁñ~»ålç^Û¡J8�;b§”=¹=1ÛFSˆ¼è,“w+ýG¿¾^uËÃ{ŠÃÑyx 8q¹ á^Cû�ìü¡ÈÎ×·G�òŸ¯þ”ÿ¼=\ƒtíÈÚòtrp°Ã ¬¢ø�%q;Žq‡>îmzÛ€êf&‡ruWÝŵ)qæG ™ã=ŠêxÝÑå3ŽÞ“xÞQeóÚây<&°>1¬U°„ÿ„¿üGdÿß#²·%ön�ý—³†û„Ü ÿ[¸ýÜï®–EXvj¶ÿž}©obZË›„€7ÔO“qGó^ʽß~9ãåΣi™ÕÍL÷ƒzÿ%)ݤ‡_¤eâ™»% }èœV]!«·×È;ç‘ìÊ9ÇÃïÇÉï°Ç;ד°~Abna0ø$è#h»<È qà9²� Å8 ]ɽ Éãm©9“°š: ¤á ùp¾7Kô̆�#¯@A;S›¢Ï¹£Å ÚôÃ6tˆ¤ìt%iP\sBµ'‚M©n]ãªo-h9í/_À&åp'}zÿ1U Yý)½£2Ð$e}¢ÛeuŠ5žâH-ci|÷]z™§Ug8ã»ÐGï6e]`rÜ-qjܹMyM–Ëm0RP‚6ÄYÐ�ÃPlý笛ùú'�¾Ôÿ€Ùf«‰FÉÙ£ª$ÃôñøÄDSž:øÄAÛ$*—üò­Ìµ›ùkro…ŠИÒçI¹,|81å?6�”Ôd†�INYKÊbd²¥ øÎfE¦§Îé\7�âѺý6+­'š®QN2„‡ »‚†TѨ¥¨Õ"ø>‚õL1:ÝÏ[» !žT{B¿ñÂí¶l «ªËMXÁ¢àLŽö^©0C©ÛËû¬ï1ŘÝÿ1¼Ì;.[]öÝ0_B¯F„ ðØ†‚„±®ÖlP©9DhK{;ÀZó¡dI&¡Ö‹˜bÒâKÆ''BVÕ»Ø%"ÑÒ©j‹R�®êYî ç{¼£‘P�È*éµÊ–d�"\áÄlà2]¥pïÐá;…2„v“4ñÚx¬’-ß" ê¡u!¾²¨%¢oÁí¸ ƒ‚ÖÉ- jŸ•ßCðéÚ Q{‡4î£VF�ÁNl5†(Ð3rÎLSõ–a,²æ'ÚËóF„É+�îÛ*ËKÐ0t�þpqMŽ´ÐåoáÍ–ìÅ”(2:j0>\ie{ú¥–È¡@Á‘1Ž;A»©nÕ (bi”¢R:nq)o(Á0�\É\—AsRVIVfÛB~ŸMéª ÌŒT©Àiz¼™™¬_OK¢É�­n¸K¹Q‘†Œµ™~¦^+Ð8¢¨xâujZÅz�,SF¦ÊeÝ“ÚB¤Ð …µ N}�êò¦¡bš‘¼^&S.A‚ÆC‘HéWѽ'˜Ÿ�84OJLœ:ÃGx™óddFâíst`!Žª¢;ÆçhƒåÇÕêa:Þ9Jé „Œ±¨xu©Ô’VE¡ w f>$E6ãhð"¯R�J8P—îE™�g9ì:Å…)l"qËŒ"˜•¾ÙPªWëÂç°Y†±É^/ZÕ©g’À9�e„˜x•ƒ[^ñ‘´˜�J±læWñT�›Èï ÀOC&�‡dQ;�°ÿ2qŒŒ®èz÷¤% Ü0\VÞð>0\VðùÞUû}·Ìà®Iù[ûóðï+š¯.û­AôÞ˜ƒ úý>ö øÈYx2˜vŒAåÞ=ûÏn®ßÝ{Ëç÷†4éx#¼›NO§cïŒì1в1o‘/IÏûDŒÕ€!#„8>ÈS`£p”ÃQñ›Ë'òMaÕåãÌŸ8æ ¹?/1©»J1ƒßT†¡w±–ªKDÄó%¯?¹vÀ!b¾Ë®E¥/èÒU€W °)È~PBdS^±à¢½¯q…iˆÊØ ÞÞ*ÑÆÁ—Dq²-aöž±� ú„}™Ë*(.'~;Ä6åÛ0§Ú�,à„mG„Rý™×¨"îuèB{1 s,{UãÈ÷åÔµ0ŽzÁšKùkðŠ»&ÐqK"pr&O¬"ê PûgHå dÊ… ¢®žSŽÛ–+¡|;‚ê5dç«DÄšÊ1ذ¥wÜáÈ]õú[Wêoåùü–„c›i£þyª%XmV㎢˰- wø?zWâA|rnЋïÒ(ôOôÝÿCÑè�\áÿl~t‡¥”èôÓ›üšù$þõ§‰|þî Çº×yÓ\£þ§-ÿ}èõ?'ŸîN Õÿülr­„ÒɧJƒ%Æ'�I¦ŽEØä”Fû'•ºÃ{¹Y½ä˯ܫP𥵓¶oèNÿØ)rà¦dºã×b¼&¯ƒá¦Õ2B%lIêôjš¦3æª òLüþ×"† Ðåq‡õ.ø4ⲿóýu‹Çš9·ðÐâ±c®óܬX:Ž œb°Ì)Õ\h•9•J´Fær‚Í÷߬7-K ÊÊÒúë%É(;ËËâ XK*Òæä•Ó ¥÷b´¶N`ò6œª)™R¥›é2›¥‰)ìél%õ^úÇÉõ`jÝš–É zlŽÀ§‘G·(ÇÇçE�@7÷ßë¡B™x•]Y�íršKM)¿Ç…ˆ§ñYZMa@ÈÔÜ…NZkÝZìòKýÒ°Ø>–átÙÞ+7è“ÅOR]íî€E%L#Ø‚´eÒõgé…@®2 �£uˆhÿÐëìxàĦY½%L…½çõ”ÂÌñgObNáÊÎ÷»±ÃW9žƒ|£è6؈žî„wåºd>÷Î<0SëèÆ£³5¨Üè:£š'UÝÚ~†÷à_o°;O�aB±²(xí“æ²­v¯cÄ­Õ‚N±Á@÷½Ú`\KAb@††  É‚÷ÁWüq|iXe« $kŽ#Õ¸™¼X!y ™ˆ°$6‡u*þ&CH∴ˆFN«’”0Íó2CãuÀ”²Ã�ªæmgý´v79 ù„u‰•ìâ í 2z"8°#9+¦”5+=ÚþrG¤¦ú´˜ËÂŶx'怓2]—Ål3µ4#–‘uÓæ)WÁ©Æñ«<2çj‰ýÉ}ç-¾M…¡<ˆ_ÅoÑ¿‡EÓG“ã“ãhèëâ2FªÐ2mѯ1zý¿ñwùY–çx2=Ç ¾ŠÚÃb~8Ïj�FZ-`ebôçžoãõb[a=í8þ 4H5Pá |C”Aâ‰ãÿþèø¿Æñëãÿ>>þdÿéK6ŠülŒ,ìŽ^}Ç^0MGfÿ‡“;»ßÏ÷:QçkOkÔÈúB}t£kƒº7Ú¹}d÷ˆQ“xÿm]¾7”÷ÛÉ¿ ë}çðÿ=ðï×–A„NýWnãð-5"˜¼’¸sèÅ»!ð$óD x4ö&Ë-újTn¦¨‚�;`{“ ‹?”³†E Õ09 ÄØ7�qÕ& €?!þ;+“˼  ².Ö&V*;J�ìG!€ën¯¸GeºUÑþû>â±í4#ó–¨_jó¨/DY ±‰ðsò�²©— ÜOŒ;—ÉÐÕœW9âÍ@Y1éèêFaQîxò2�ÅʵÎ1¶@õĦ—¬hÜPÍñÊ ˆ9¼èÛ�G¡\-Ûí×#2™8m 7Ï>ÞMÌP»�5ò¬—b�®Dnök¡ŽEßA(긬SᄇBÐ8§j ßÖ”¥›lŒØãØ4&ÝS UÄ臺¦¦ Kû¤5³f‚Häø$ÑÍ�•gÇl²mÝÃD0 ³�,õÓ%Aï&í�Xd]?ß pT”ÖT(®cg^´ìú[-b \H¶Ê}Ò#l†ÏŠ«õ’ Ô°*¸’OÑQÙÀw7Íå°Ðæ¸ [—7piÖ Iq‚€9¬µ‚È`3 ?7Ã$�V�³ï¼c&FhCÀ·%Çü/¨_@ëhì¨$êûè�},Ðÿ¡ÀóY'oÉÄòå:é°ûæ¤+;ùx�¾Žãw„ O©Ïø›†hŒ¬ßlækÉ[`WG˜–Ê ÁÄwiÀ«Þn"SaY”°!ò�Ñ|zíeÊæƒ9òY5“¬½¢–|¿cU‚÷ m¯Ë9{wmhû¯oÚž² šÎuÐÉ£Ö)`tò¬þ&ù0Éo“\ ‘Žˆú?šÜ—n Ÿ\ÔÎ!°ä®CG˜Ä»èÒ¿!¹¯)�Ý.¶9FAn�F[`Ò>E4q|Hˆpó—˜bƒÑ¹ãøSøþ™°É¡/ ÷ti:ººutJ�¦ßÑ}Ä'ñÑã'㘇tÿÇŽáÂüü�E°GñÿaT”' ÀÂÿ~áÿúÔÿUÐZþ/4ãoŸ(dÔð~"˜ë'à ™¶Ò¤-â5ô["6Àéå‡÷‰Ñ®É.Ö~ZÝZD¸}h‘®–TH¾}´w0ïíã]ÕNz½Øw<�ã7ù_‹åª8/ÊââðÍ*+a‡Ç£¿¾9h9'˜ ×ÿàÑíÿHº‡$e(ÑÍ7å Ã32´©ÀÁÏú” ƒ(¤¹ˆæV¡€¨ÑÁrp‰bIFmT¯ki�mT}mnããø¥ï>c`;Že¶F÷°êe³åiùÛk6ÙÐ †"r½¥UšsÎoƒöDÞ mÀÛø* [o8cõ¢¡?¨ª?ý V/ûÁ{ö#·ŒÏùüå ?ï¨z00IžÝ€Iž,2N·µkö¸ºKï`Üq€ÔÔ‚nµf|_Œªf7Ö�y©ø®1™‘"âËq`)Ìà�âœ8rw™X·a=ÆêŸ=ñÝÉW‡Éƪë^ÞšD��¾åwi·2�Q-›¦:íæ¬/3„�)ðV@Þ‰‘óÊâ0oD8ªS=CÐéªÀÌ{[ ™L_rÇЕçHõÎ.°5�,Q˜JVM¨”2ë‚Ë£;Pö oïXÆÅË ³ô`0b¨c<œXß úˆ‚eá vÉØ÷p$KÍÈ—QŽxµd2Õº�7ÙyÃQTOò�„aD´Ít *4zò¬¤¸Qê¢OĸXEqð£gÂgÆÈ»œDeÙðÞC€çÛFc=g-²m6°¡šh¾2# Ô¤\ò£cÐv5pB ˜;—¿*=ýÞÃzg­6B¡¯ÝVæÂÁe•�ƒc¾Á-9dS$®ã"ެ“ÓÔ½¥B(7à3IEú),Ò®·ˆSXlx¹aœ&Fyk¸£Ûû°5¡ÛÃø Å‹ú¦Ò×Ë¢¶ïƒÒƒwËÃM«×ERìfâýàЄ”‚ ððÂ/]†Z¯ Ê‰'\Mn×&< «£‘xòûY‰'¾¨5p~P@-~_öàɰÝaÃ�¹­¡„˜˜P ºÒÔa¯; B¢=2^�¨2‰9Ê¿aÂ’ä ôP�ID—’4ëßîTeÈè ;Žº†W B™n�L·i9eÚÁTdyݱî{Õ¨wo$VF‚0ÛvÙn³,LT£–6b ì‡h€�ïùóñµBA&Öêà\óÆ&GjtPQ{l³roß.ë Ûj„ÿÓMdΤ<;F+Åô3×RtÒ0�Á£IÀ>ÿ ÿqU]›ÉÎþÉ8æõ;Ä6æôæYbvw·Ö]WƦ Ê#6‚0¶b9`Xï@.ªW³$ÐÊ$6úÕ×<ºÀ,PÉi˺ä è?û#†“$W£=?]¡éèùþý—�Í—¼Bþ«FÒ–âwfŽð®l9Ò˜kÌÀpmÝWtF©õ¾vôÞrš¥Ák̵ÍšLë�Q65𲟨ÃiyˆÕÔ�®C³$¹Gq#¸+£}o"ï�¸ïÍ ““²GºÇÛ onœ 0º nÏ@8 Yw ‡ON×µÞ2eLvÙ £.Ãakan[íÆšÝ­½†µ:W{4Š�«F^[·s¹ýŸ¾Öÿ-\s@¿‡7¤®ÏÀq=ŽÿF/ÎQù¹}5ÎŽ¢#´‡­eÁ`™…©3r�â=ݵ{¢kï1yéŽV¹¤sKE Q£xºFu SéV�ðÊq:÷ 9wð<ûÎG«YR-º²H´Sq<{»âsŒ9ÑÞ(°ûî/é=/ºaž¬²åVŒË6~ éYl˜õ¿H§É¦b—‰óùÙÌ—c»ÍLßÖí!ÐŽP)ƒê¦€.Õ­Ö{kf!&57Qk¿šP7Ð.e³fÜô+_à}+‰ š.È~á0¼aLÊÑÇÌLèýH‚D# ˆQzTʵÄ�˳òü½.SMt fN[{«oS>pÊÅðµÔ5©¡*ô²>tFÈ•e¡5©98–©ó²3\?vq‡’4$UÃмÙ´oˆøâéia*%�ëÀ›nÌa×[£Ò�³—¢«)=“o-¾r¨òTìÇÙ0¢@¶”-=5ŒšÞ ‚Ô|ÑSæIª<TëA°g•xÄ2SN¶£<‹ñ²u×-q+Wùþ51g4jW CG¼‘Æ•$Tí i¡Ø‹@ÿ¦òpÐ�éŽËîóðtbëFE…£†ƒÞ Q˜¿æ–†^*\*zõ Âå ÔS¸_=¨Oõ�ëúë®[5d¨Ÿn¥™(F�[iNÊ>ºÜÌÕ¶L ï |æº�ݼVìnÿAÅÎÎíhuÁî:ÁZ† àqüJÒÛ<ð¨0ÂÊ †X¬ÆÙÕ‘¡\YœŽÈdäñx:_ŒG.ˆûBJ†7eŠ”jT�Àt8ð§©ÑHXðZt"x¤‡‹”ÒøòBÐÌšCh ›$&à\¡5\dŒq?4F¤ñ“nN&—6©š �7[—µ�n *µ»uÂÐÚ¨ØÓWß¾úîus~®ƒFî9”¾C¡UGŒ“Úhræ�«]5m–­8�Z8o�ÌdþD—Ÿù„ÑôûÁ«`ò 3jJ¨É’`d—¦ñ'n›–mŠN8P*öü3˜É•À^¢p‘ä�üžîA$ÈŒëlÿ„ÛÜ»1*w™¶¬¢¼j �c–·]¹;xªa"�q�¯Ç^±a‘>*†‚©ùRìµµ9‡ZЕēÊÎpYAÜ6¯G'ÖQ!B�8ˆe›Ø}áõhrÀ6yûÖ ­§û%Ìùw“)‰ßœ>9}9@!&ôÎZæYºœU¿‡ñ ËÝö¶ÃцÁꜺ~ȇ—0Sç‚þÁ±s áXW¶¹÷¾èéOlY·ÛáiO‡uQc…£Ûéh6ï£e ¤%îÝ·oÞ¼Þ¯“>vtòÅÞ�ô­£“Ó=;eælÖc@ý˜;ím¶|gbEù315xZâ0÷òt‘U¿öm Pó*”‚LÑhé‘"(;pS†÷<›ïOYúpPivžæ�±؇¾²á~F¸:<Ábí¸Möè&�îìÆòøf?dÙP+Öã ÑÎè.pݹhöZìBÕþ-_Ѥ6S(_—Yù�>OÉÈ?l�°Óžie|àÿxúG�·zéÿñ”àjþÁÌ_+ØM å�ý’Õæ¬ÆŒ=¹6³’E€AÃ%`Öžá¾H æåÅYVYtf²z’”Lý'4Âaçƒú>¿Nç•ñWð0𪘻}›ø9A§õtþ•XXWØ6”u®aiYoY†y:z0¶’g:à^�e¹Wj¾áþâµ§&‰f"1õN1�Ü€í…Ý�$Óó l!– ©žñYJå±ÊzQœS(&Üó˜ùÞòQ‡�b𧫤ü¹átvßÀíì7sûŽçþö�þˆáï¯S§ÆYÀZ¿Ÿ]äfnè€Á¢s´=p²/¦/ñ#[ïšäíœÎoŠ©»« 7š«Þ‡ Á†8ïMä|ɶ¬““�@½L«‡\ôN‚e�ÏN·’â®r¤³lº(êb…zpجs%´9]`Qlòâ¡àÇÇ Oκ}ZUuHƒààQ„Gs„“f™*ã ÝÇSϰ›í Qw·]Z/‘ͳθÄxQrþ &qŒ£ J…¸�IyNÄ_ÀL±„4æiã²§ŽÆ…¶}1ò{²žÅ¦&„�†"k"§JŠBp™ö¬š‚²‘PBɆ�žZkw†ÛÎYžÑ 0³ž"ŠÓæû“ümµ¶ï¼¤Ð±®xµQÓ^ÎÚ`ì`AWÆù§xJÑ‹�â ¾VÞ*Oœ5ÒÈi<'‚‰çˆsêâ‘FÆ•D¥ôŒƒ×]’³Ùák.›¢çÈœÄmšh|>Gˆ¸Ø 0'ñ9�8C :Š[ÌûOJ˜î­D\}·†ïd.Í6Îó(Š¿§Ä| Óžk´ˆ?ê&ލ©šfÂ׌çÕz‡ƒàÑÆ¬› ¹ÂmH7ÓÄ—³Ë¤œUŠ”ªi£"ºQ„>ž.ù�N�zhsXR–× $à‘xàÚ_áÒZà¹S³ØÞTFô–ßÔA öÅWCï&¨iŽcG½Ö<´¬ÑÐr­MSc#4ÆqÚ[îGÆáš¯Ýh6ßBN’Éx@C@4ÿàÔªh‚rVj£u®Ñ¨ ÿú�Yu¤pß"“HÉ K›5]œ´ÃÂÌQ[Wv÷qsÊ<µR-WEy';ÜÙ¼o ¨�´Uc®ã�ba˜}»|5›UßâÍ»‹D–%"AÛ IÖk�dˆq á,¾O÷A0Ã1 ±�¤lÔa}<-à„W_õµO¯t—H?—¢ªÁéŽè Űhõ&�%Ê5:+mV ÷Õ¬’brhŒó½&ssr…TÂ/°ú¸á�:ØÌÑŸ.‹5脇¯Aù«((CÒ5êÌ ¥ï#—–©­8zÑv ÁŠoÙÃîÐ,É•J¤;Ï•òW*!ªcGi.„±/’ÂóM‰ü>°6¯ä ƒ%p$Aq]pŠ8o?Áè!õŽ/êz]=¸{7Í�.³wȼ²ä¨(Ïïâow¥±?L>«D½Üï}Ç«><°àˆó?¿HòLQH$å{Xx�7å§á¾~ä­<~ýi@ɬjó×öçžx�4É¿¤%~+ì*äÞöSÉù;O…m´f4çÜ@IÞ¿QZÿy3­x�ίQŒÜǺ'OãúB�¹¨{¡aklD…¬t¸RäˆC�þ�ÙùS /�xåÖsˆÉ3çÕ¬�ݶ$²‰ñz`ülÿ––…­yŒæÛŃƒ9¦{L,¼Yü,—Ž8i[«ÆÌ³jOÔmÞÎ%Ú5™¾fˆ»žOÌR™úm„ ¹ä± œˆújƒ…°¦8¯QmǦ¨$¬¸®±[yL—„­,“åšl½a�ãb¹þ‰òW23“ï;³9_oÙH>fAÖ­â” qª$Õ;Ò›ú\,�þ0–‚Ð z†+ ‹¤Œ\ !pŠÓ ð¾ ‚÷�)‚ùýQMžÖ·mxñÊ�„>©(Ë,Þà¥q\\¥tì6Àù j.g•õ”©S³ÑÜ–Æù ê}ܾ¹‡Ÿškp¹ˆ û«†^#Êú–ýçݱîÁ Ó׌ÿ³ f"ïUV#³=¹Mj$mj®²@qÊó4«µJ¬�±Ð�@¢[1H*Jz‚žÐÙÒPÏj�±£¹˜`ó-bγ+2!FRi ²Œ›ª,2£AL&[rE;þÈìr܋ȅÔûä|‡È/ö-Z¬$^S’’++ŒÚˆT`äWHê2Ű ‘ÇÇ�ê=dpŒZËZñd—¼,LIŠa|Mlxï)Ó&~½ûÂa>5ê¡H>diÓZLžëØŸy:¢°m«Íóp“/ »¾&cÃkÃ# Ï×Ü»”c ž¶¨9'e¥†Òò{qûö‡ØIQ\ÂM¾ö‡IYI\Ï„�«h„vq[1º˜–N•‰Úeä$·¿ì#&êè0_n:»\¶+HÈÞü’Þc×—sbá!¢Žr R™Î(‰ F«&دPªÇÉñ1™ôh×?ñ|jn‰œa³Þ¯ÊH'‚•GÌRµ+�ØýáÝõ^YWr¢»9 ›œ™!eìbNÔþláò¬Ù®5ŽÈÿØù@² *UÒ»iv”/!â½Ê—tÖŒôK3{�÷‰5þÒumŒë¨ûXu´Jm5w¬³ÖŠQtÛÔC*€�+ql¬�±%UÃ`î›@07\¿F]Ltú•‰„crÿˆé®»hU¾5kö¦ÖLÆq¸+gxº ›¼Ôð:.8ØLMŽWýãC �Ê.QÇ÷³3¶‚.PæaV}Ö“ÐRâs¦Ÿþ©6ßP"–óîÆÌ‹œ«\tß[‡]ö¦Þ ãÕÚjÍ_Ä|PXGAÁÊ0Þ·érº0Ÿá;"¥¸/‘I!o�*œºûðÌ�Õ™¢ý~ÀÒ°�ÿYßšøN‹™_ /’Æ�ß?è4Û;'³ö’„Ò‚¡mŒNéaòi¸×0òœcCÈó¾W=K—uÒG"zÍ’_y­þ2ʾš~ñ¨µk‘U?_YÏNÙ¢6ÝêEʧ¶ž×º,–!‹#"î…¤qp{„~«‘‚ZŒy›½’¯·1—ÃxÆS|o7åéµåEÓÑ”ìÎiç<_ýœ�ãíÏ¿ÈMÂYÔÍ·à%ÆÜ‚ù=‰Ñ"—œ”þné|›ñÔ„¦èGwÀ å0Ð… ­²ÙOÿ×£Øôpjµã]¾ZŸ•ϲ=˜9¾<˜�÷X‰±�0CÇ¿èá Ÿ SWe[g™!ÐîŒËD\oòeq*òP~ù-½=€k>‰¹á(nðOj5å˜PT] lØÀ“wë¡£~ò×׆L’2 º9f*ýd‰@ãø¯i•.W‚ó:Í)þH‡"Û¹ÒaÔ!vO’3…LÑ‚�öÈõYíÜ ýø—NÜÛœËsN¨“xo�5[ˆ˜ ºKe`ߺ=!>‡ˆïðnì�–¨6ŽCR‚yê]Ž^$ŠË»#¶*ù’RÅ�:”Ä<ͪ-4ⶇ¹±ÕŽŒŸ™¦ke連ø •@n\ªÿ>ÅYHyປG±/’uÝÝŠóŸ·Ðü¤û_K�ƒ¾À¬ŸÇöÝ-bèÎBÃuµÇð‰µÚƒÝâNÐžÔøhÉ£Rf�bì[1*?I´° Ìc¸£“i™jàî2MÎ7iõ`€‹ÐŠ0AB%  ‘J6‚uüiį~ÞþDªÚDïƒ]²a×2\@ÛFñ¢Â~Gð#h…毸�~ú$þSd¿¤?9p6YÖ±lÝ«FÌÕ×l8l(¼å¢YÚ‰ ÈRÒ¹ûØðF†ŸÿÉ;8éœÏäÅ“¯Ì3üÓÁÒdCŸ|Ul–i}ø:…­Pgi)67‚' /+­¢~bíK+Y(šQ"Y€!µEJ—)}Ó ˆÿz"aèˆC¦/¡)I§Õ‚e³a ^RUk"3–„qá-ÂRb‡Î†…G³Ÿ³ØÑvËòÃ_ðáÙß�cppQayx�twwØb ýžxó™ŠÍÆÅí‡ø#ˆLäe: © ÔÛ/Ȳóx®•àƒxcìD¬p2j›hÔð!/7ü2»³+ÀúOwã$ßâÊÑ.š‹Œ¿çn`©½h2—Ã*fwŠ‹”èâ62Õy)5ç‚a"[ûÛ tçîîn¡IÈb«~ù  õé'“˜¬k£S¾OÍÕxÙ¿“Nu© Ìk™­È«èøØ'Ç.R/Îߪâ“cuF�_ ÍùF*QË�ÛåÒ^Ãݺ³dr‚à³^›è쬄Q`F¶ügüIzÎÈú*—º)¤â¸,ƒØAF"È*®ö&ç¨ä„ßNÌŽ“8ÁÚ·Å�—ˆž‹@n¬�€òy,\Ÿ/35…´©OU{ã@÷5H ÛRü6ÍL”UGo"á„l›ïÕOÆÄ+ñ›o�8Ów@àøí·o&® ö ”.³c‡æ+‹¶å¤k[¶>›/‹¢a1�\ÈÅ›oçnVzlmbü.³¼*JÂÔ,ƒde‚lˆ_Sј7ecÜà=ã ^JÎ&—ò°cûî˜hR7u¨ˆÃ%`د¬®Àñ5=ë© Â¥#ì4U/¥‘=Ë®q¶Õ8‰PŽôß¹Á)–�Å"ôV(Ò‡ü"9MAq¬1š;˜Í…÷ä¿?1›’X"aPoê•I\ôædÜ¿_v”~nl"»»­c™ã¦õ`Z�Ò–!ĉ›ã¢p¥ H;Ï=*A’ãÑyRXbXøW±cX@óÀmh\•îxê¹Úš¢/®Ê¡5€ÓÉDïã]„ð`ôÅÓäÀ¹œ€5#˜÷‘žV–C%ÿü p§wy6olhâ�ü3Ò(+]õíöµ¶1`±$È�Å\8m&ÖåÝöV"ò\eó‡žAÇ!¹hT}CÕšv„y£8§0�13Ãwiº&`>'¾Rw'Býæ,µÖÍf¡XÇþ¨î]mÊÓ«-\6æ‹Ú­É ”ÜjCÉ/¸ûšAœh´rfϓ咊Øa›5Ü0óHj{[²´ýªihºª•ç4áâÄ¡@$’¹BvŽ<¥—øë<»â{t–¢8LvÅ,§rqélADå©)ÍÒþÎ"Ñ¡0l4†³¦«u½uã^�#óÄÉÀ µ´¤3 ©9†_pØ´-A¢EÏÓ˨±1JŠ‚ ¨f¥cÂ2m(®I? GgT²¦H6x@ƒóÔOê”Ì€!/ÍâE ~¾Š,&¡5¬´=vGç9ä1üa,ï%¾c±'{äÛT/›ñ_ŽŒ€óõþæ‰9ßÅÏŽŒ°�&ÇÇ÷±@ü„ír¡¼‹VnO�dmÇbãxç`Øž´ à¯(r­è\EXCÉ×-g)(TÒxů«ívHqNöÒ”[aØÉèÞÁ8¾wòÙá½ÉŸ±|Í‘o`ˆ`ö‡O�Zvœõä„f ÓÚÌ8j3�vH‹é‘.¹5� 9QÀ>X <<å’“Ž*Î\Ä‹â︖³ši+3à<^3¹-Ìè³bCÜ?9üì>fž©ÉlŒsû½!ü˜ÂÛƒ3™(?«ìÌÞ.¶Më�ª—b:9ì´+Rå¥ÍòÿoïÛŸœ8ÎvŸ¿b>ªœ£M$±,|L;Ÿq|Ûñ—JR³Òìîi´ÖH Ë©ó¿Ÿ~oÝo÷ôŒF—âP•­4Ó÷Ë{}ôv€¦Ø=ߨ²p~”éÇãåߊ+óñû!m¬´7zðàΑ™Á§V]× "„>cFŠ¿5J>ìùé·5l Œu–Á“5Φ9ÍF£»¸î>¸;89~p8ÐÓå­¶K-Z1àª�B±é=sG¼®Öó[�øÃ¶`©m´�6vã*ÿ×K'¿S{Yw¾eµè–4¨ù4$™O·Ç"1/YX`N ’bvOE:|mÛg#uDRÕ-ŠÃ©êNµ¹Î¾—оE <¢C TpIV '‡ï ³óKs\wN² æ�[¶mÃÊM“Æy~‡®ÙHèˆXQŠ:÷$xœÌ!L;¬Ša7­¨Ö|©9`”A–®½}Þ¥Y‘C ¼“$<›F,¯sNW�ˆ,sR+U’þÝÖl‰¬N—A,{Ì|úÚYÉ^)¢�Ù 'ê�¹@œŠW{ÿùó§ÿƒÁÖÌLï5Å\¼ýׯhJ™›_éØûíšëÍÓò\-üãµþ±ûì6ya¨…ò,š—A‰·Z™/|Ýd‘ÃWW GF„×b¸r5€›†1!¢…cèý)hâ³Yð°‘LGù5OÛÉ™áxu¦š™³Dí".IƒÄþÊÎ<ðg#˜W¤Ög¸·î§ i´†´7˜¢×«²¡U_à¯h&®öB‰�•‹D²Ì2q ƒ¿8£!_ðØ1mQN€ÑF}7iã É®rû_É­Fà¨ÛäyÍ|¡ÇO†ãè+¯ì±Û� �/ë3€*C• æ‘�FéblŒ7 d±�HÔu³û¤Äj��.q×7Ý­:‹­eX·ƒ‹c‡fä‘íM¶’ª…8պͭbî´hLÂf3^óõŠiÉqÚ6ÜቿÅ}Ü;›Í à,2×Vù.è,‘¼BhI{ï&Ýïi‰ ðµd£—#-™gÚo-µé–•‡…ÏÓìÌÜF²‡ØæM.1*¾ðfÚÚ %Ž.˜±Š/A�x ²Ùfæê[)Kª&uå€8Ÿm¯(LÀ¨O¹ÄF DÔ­h>‚ñL¹§Tѵ*O¬µ^÷P@‘Y•M˜�§‚6 -;䀺Ðä£Ï½·cèx·Ð{û$J�’(Ֆסš÷ýzþƒZÑ;5Ô%çèÍ�Wã^I9ª™?ñ…Ó·[#¯4ßѦ\ÒkbeV’D ÊÆm»\Û$šl "KQXøTµhëáøÊ$ç ŒÓ“‘ÙwTX’HX”xh=3E¨µbð˜iAAÿ©X=ï½&UöÁ=Ç8L,R!˜2Œ¦.Úöu‘FжNDÈ’ƒ€a¬Ü8Ø+‹82›iåýÉ�®'TWlûeÁê §~ÜÞµ¦È€C0ŠõÀ70ºÍ¶%†Yãó'u—|³±Ÿo†÷H†™1`­2ýœÍòß¡¸Š‘Â<PRžÍ+ve= 480ÃyÑ=Ðårr»¤ºgS4§5?LÓ?›F­@°ùï!˜Ÿ,–篷êÃ_¦/²jbîû'Ãôåäb‘Ï^/.ÏHn2/¿œ/f˜Gc2˜²ª ãÉø—…é9™ÍŸg�§ßåd‰OL‹*óªÝé§Ÿ�ï >ÿüΉ_Ë’ù‚èÃ~¶³Û—968A]Û¿Ü·ÛÙÀ¾ò Rºk”Bâ‚= _ÛUÑg"dè­° ã3{3Â~åI8Ï};SجápxÀZø¯o>\�üן¿üüë—oI©Ùô×ÍSÝ�zj#šÓV5É‚šºŽ·¢¾zõË_7À9 ر‘hŒÈ0!»"û�õ¨bƒ–ÄoúëºZ!@¡`"IDq9µŠ ÁŸöžýüõ‹#(×Í\«×òóÓÝÉŸÜNS#Ò¼‹ÁS¬ ÓæfiX€º�$�nqºÊˆ}•@sÕ0µoIîÊGÄ”ü‚‡)1§ôéß1ÿ næåºz ‰ÏFOá„óe‘OçY Y`˜$ —œ&úûJéEw”`�ÁCM êœÅ[k<ÇYHGƒ±�eùG“ÍÛˆ4ù^È0±ïßœ�”±‚ ¨¯˜ò,xsê1bX±Ô†!í«eˆ¡ŒP Ó'Ÿ5@”RÒ{ƒû Rq‘ËìÀ«¥U ˆ×ÌÕã ˜ «WÅt�ÍÔ09Øc¥{[‚àXT÷3QTõšÁåàÍ*b†È¤ö9÷OjEc3~ïtBKs¥ÔW�ó×ø·å™óË‹ýä‚SÔ9¼y)rŠ:FcPÊ@~šJˆÏjbÁIU¦‰¹È¯ì·«ZÕi†ÀÏÚ�_AÕÑ¢àWšYR¨FàIszíík†—@<‘÷Œ¯z‡"ÕP� Å¥+q¤Á›aм¸$ÊnŒ¢rj¦71"ücÓ�#G0âìzGðãï~øæq€5åC<'µ ÿ·Á®e:ÄôS‰�—/R,Ñ-) ßÇÓ¢-‰ÃnPaÆGÔ-D F¿ýco4¨uæ³aÚ{ÊXø½�¨áiîc4ï:œ_Ñý‚ÜÞ¨¾øúY—1µ�‰é bÞ†i¸Ä¾dlVÌ‹•·CW‹!œÄ ¿Èþƒ8‡˜1˜÷™œ7މ«7=bL·ÄÚ…Ÿ~kE=k0âp Ï——¨XþßéÿëÓ·G)“bxm7{ÈN¤ùAØP?-†¦!ÿGÊÀ’�UŽ×³êbP{×Éê$~ÄçI=(3…H�.âI#lì¦ß–& �tåÄhy*@—pñQ{±Àk-JYjØ©Yå ›”…4”þ5Єָ_ίVþFžÓ'EØ�Ø'æÀ9÷_9›.å 3 LUMéÃ�è×ÍÑcd XL„;_Ù٠ĪDÉUl½ jᤀªAæb¿Ð‚ä“ôI^þjê)‹Á7Ü s)å—³/.—‹I>5’Б4ŽRª§ö(íÙ^‚Õ€>ÁÍÑ´Z.R€K3ëSI�ØHeOGZnª‡ºÈV±h¤±èÑ…&r03{à¶ÈÄÛæaP•äBDÖåkoþÚ2¢û¹½ßgˆ‹ íÌʨ<Š’/ï¡u#ˬã¹a¥¡+‰ð/}ˆý©*N™×ÜhG Ø¥mÆSð8áa„­ÜToÃ2̈ rc Œè?fü0ŒL”±¡ç10éee݆ÑIáL�i˜Ú6ªBô–3·¸­Wë×ùµ ²êqræŸáÈ5[á�9�°ƒ}½FØí:!tˆêŸÍA}O×e™¯VÀô0r ²ÜJó»‚F^ƒð"û.u>wìÞ#SÖda„tþ Å.k&±Î^]“õZ¹ËÒ,‘p…!Ñ,†£‚¿¢â�,r�†ª°Ä„×Sëq‡©tÝ…-¬\£-¬7‡30гÈ5Ï·Ÿ£Pðú5h’]Òø=cëêbN['#z£ä =爵iÓ(ÿÓñ=kS$Ý訟^/Ö(rBt/Sœ2`,ŠNg3Ð!aÝV³âü8BÑïc¾À+¢Z6›X�=¬4’ú „zBΜå+¡ i_ôY‰¬¬äžf#)gÚ “»€$†ÙŒ¢æ�—MÁ(Ú([#ŸJ¶^-5}Bà\6Ð^ »¸Íè.U‚!²¸X±Jmu3é¬�W¨œ¤,�J�±Ïw”Žï±¼~0F¨ YzyÚIÄ|ª–D ˆ®¡åA$Šç�¨Øaj*^â;ÿšrÖ¢n7Új† A�䢒8€PBÞ=¸ Å+ßb÷ࣟذé4û5�ŽàÙî÷Þ,ÒSˆò‡™ÎŒœA¼:ËÅ[¢I~‰(©«‹7Y±Êub ð<Ó|‘YÄ.6#ç²ñ&Ÿ²™æM%›¨:œza„sÆEß0åò™aØU§bqv-TAŠz©X}É£¤r‚\º‰%¶RŒÈ Œç·d˜þHqÅfï™Ã»Äù”ÒLƒH‚ÍûßGéÖ,Ó³Ž6hj@âl&¢?’Yé¸*–@›O Z©Ô‚©ÕIùëœjTŠR‘Pe�°j>¶hL ¤›rÌÖè‚ôÚ¹`(#8`3VÀíÓ~”±u ³3@Â8ܺ’!õ××/_}ûüñ«Nú�ÿ¬%à�µ,3©ª’µb‘}/ ÐÈùÄb²tš�QÎ&9;Ǭ�5Øw5æ/$BÑÊ"—4‡écyT§S¬ù™ÃZgM xô9qª”¬x^'£+Õi¹xÃW‚zìžÌ´þ-‹�s3¶3Ò!1:ˆg ë+Hai%uÑXrÐÎ¥|X@¸ÑêmÆ áÎ'�Ý®¬˜�`ð´.1 ¥ZÏ wM ,UPjÇ!·êr–]Ü˾{üK·CN=Š·‡žAáŒ%Zb™À‡Ð'®×BâÊ9_f—ÖJ®W`/± ‚I‚z$ÎõF#T¢U>SÓ;°Y �ÿòõ,W3¿8ã«{°˜@ª6~†é_é~Æ¿úp`¾Fc"Ǩ¤œfKÐÞg¸màÿˆ±´¡UKÆÖ:šÆŠW¬ �>KÓÁ–K•ËÒçµòò믟vY(î9>§´àƒá°ÀÒZµ2›Aê nZò, Y<×Ä&Öt€P"$¾òÓ×ñc3¥Ø¢£²§/ÑÈ0¸2qYoÓiÁR 9ŠÌÁ¤ëò½7hm˜Ô½2m¢�¥=²‘º¸8* &Í], 4Iè�©”4ž�%¡Ã !%¦�Ä· l‘`Ž&šj0ȱgžÿ³78ž™n w›¯²2‘º#U��–ú®SQms”)fÙ¥ãôSGJÁ§ ¤C¢¬äìå+P&” :»öøô‚ãfHd–5s=¾ûtƒ¢t 1�SÒ£ã‹zàý;ûŸÕìòÂÁ÷�^-f`f’Mkîœâ|½$KCV”" ð!U_¼T†Š yJ’ôØbËdj.ê®’º×º~êŽq'×ÒŠvÛ‡ÎÇÄ�½ ø#Ö Aß,fó+Ó)Ð<±Ö9g¹”iˆÙTµIu�NmeW=O¢]oë¹…ÈvC t¥a}MVr1’×NÙ³¨«?4Oh2ƒF�ä 6r{[ö€c`d»ÊÙqÑÆ×Vë[ÐñDÂî-ROGÝã�Hæä÷¶ÈN$9û–U‡;ÇWi¿ó­GÞŸ[е±¯úÖ#ùT÷Ø-ªÊRË�«ÙØÝÌ?ÍQ]6ëÒFtÙôËm¢¹.íVüß§®„âóë=bºv©h;¢ó®ú’*÷mÝá<Û2+;·í0yŸÑ¦4²ÒïÛ¸GéÏÅl²xkNC( “òP³�çç+à¯Âœm~,Ì *>é’ôyV–ƒŸ/ŠU™_§?Ršy×x,·¸C]ö¡—-ª�²DÅ#Ä»G©œáД—„ åÀÒ0°yU"¢ÿ†—ï/}•ª¤ÓêüŒP²Üë‘íS"§ÖhP‡ E ¥"r ²hoC̵$N™Û‘\-è´‘GËóÕ…•ü9V‰, ŽöžªP=¯I4vÙ xOnfú}ö½ IˆÎDxi�â†uóe®+'¡@pýY9eª‚¾^g�¢!ù­q—o³èê¹o¤-Š`«ç ¶ŠJ€òR’¾ìJÆ<ƒ¸¹ŒBÎ9%MÐCc‹“µ±ŠâY�pQÇ'ñÜL©‹µà8Ú‚³¬˜‘´Ë�iióNÕ«Ýf â§Û¶“’øÙãÖ–ÍŒ”Q9ÐWBÍâüv%Z sú! Kt( H¥ÖzÇN ÑÓE¾»r ñòÞ œŽ¬m³‚ö±²n}¥Žzîý¾rÂêÆW¶-`g'&rÆ3Ì”ˆÐÄÒÞ�QÃô1׃,Êh‡Lc†1ÖÍ`<)ˆ“GçÎà( NùíêW±ˆöO¢nœ QŒú*HQ÷ØRn÷1¿ÙžyQÕ^v9["ì])Òwáî÷9»úˆ‹eÞ[Šõ±˜'äº{dì׈qÅÆ¥ñ¨Òã¾¾†â¨Ãý|¨¸åOàÝo¿H�X�®5R™šöºX"‹W…Äêq+ÖéC_,÷óܵJfÙüt Ž&S[Ú3'`ú§Ô4æ¿L^EšÕå ‹�9ä|‚:®mö6í=K¡”‚Ä‹·Súh¾í§ÇÂN¶.ŒVb ½Æ'H&uÊíAŸÎ ÉÜÅfmÏFÜ"F:—fËKÌM ½µdA³è\B€"9Àx#‰ñF‘ÿyâSd„ÁäLA̘<6™¬—ÙäšáxÛXÐccïÚ@ð¿ÊýŠŽX‚Î?äÅÜhOÃ8r�§É ã¯Ökñ©å€Koi·C¤.PF›+Xh(Ô· ¬±ïZb–mÞé A_+åÉrŵ¨7J&´ ‰»Aû4íWY1ƒ’†œ1÷æjeAã¾Ã’žCícÿb¹XŸ_àæbd9Sê+Ôäè 24Îæ¡4F†ó¿ÙIÆoØW{Vk[̪±:œUcuV�¨o±OëŠûh1)>Þ6u1yìÝÊVsŒ¶ïPïö–"‡õ•Eì2«í"¾Ü’Þe°‚Э$f�DçË<7Hû#Ëq >3´ïÖ{„–s#nspû0‹*Ü´Ç�D3Õ8² ¥¤“Côä³h¬N\ü¥Ñþ› Ç|îPÿ7%SNÄ^‰�ZŒ,bü�06áLÄõ†ýäÜ@ŒäCkhÓ°¿ÅÆS³l¸½Dùþ�»7�´ýc¨Ç"#0Q®eVcu˶�Û1Ï�¤-´e©ôÕJŽÂ"&ï÷D;·�ªuª®ky�ûd‹õDYGôƒâÔÏË)§ÆÂ¢éGZžÔ†aXï —`æià‚Û/ØÉ¹í\Íç^퀬Ýex†E0ë '`¦ÒµZµ£¡°Û4ç(0¿y Àn�Jé̹Aå³{# ·m³¥ðᎢ4øçZ«kÊPŽÇŶJÍïO­9qé¿©ƒ÷ð¹ö£ .Œcí©uNÚ–V ý> p>Öcϧڱzt÷îS¿çO>öýÇG›´¤Ãig5íã즴~ð_ç—ö%ó¹Eù5Ô>~ÝWçø5ç1m(y‹£{/ùÝ—ÍÝè„< ;tù+Ý¥Z{X«šªµ¹Q�Ò×Y9øËÂȨËZôƒõ’  u«ît]JÿUÉß¡Äm!pá0ÖÍ ù]„q&»Õ™¾8êðGЬ¥(6÷ݺ|]B‚¦%#º²£Mi½koàˆÅÖY^¥ªÐî§~o”ÐW°E»Nòw<�ĉä �ù!‹LT@·ÞI3Ê(‡sR'Á…­Ø{ Ì¥²?$û0«/jFD)�‘òŒ‘`åEÊ%�/®<´ÔWuù·I°í“²ƬåÝ�Ò°iD‚¼Ê…”ºÆY}~2[Ìû.øB8Ç ð´÷ö(þ1=EÊïßà ü­÷ˆÀa…˜ކÿ¥¤·NáYÔýÄ3Çžš¢UÖÑ=,�OüÎíûÈ[V‚¥ûö]ûò0bç}Á�ãaDÔ#Ñ�û£¨ qŠ!äɈ1Dq�}r%~Yçn¯§‘7µOàB³ùÔýùYuççåEvY,ƒgK³Z‹Œ5:ÁÁ©Þ¬œývÊÂeŒa¼@lïöéŽø\œÇ™fÛ6¯ßy>`—%kÒ®Ö—„hüqÉëw,QN3Áνû;‘éPÑléXþ«¨ G˜Z… ‘"ò†3‹à«Äº¸| ëñ�†®ÈZnÎZÄò³èl< T9¸¿ Ø%á§Ä†Wv£(*!ögóÑ¥àC€Ú|±Ú"Œ¯—“Ík)Îky<és/ £ÍP‚ð‚ï’Ó÷¢ÚO�Í1:²× pU¦§@œçÿé"©ì£ÏÓÞ!’x¥].Õ14:�Çä>øËéqüëQüëqüë;›Ï,|�&×ßÅüÃ÷îD˜—Æ£økƒÑðîèø^ô·;ÃÑçF͇Ž4è$úöhx|ÜÜ Ñøþý¦ßî�FñßFÃ;'R[Kƒ S¢¡AŸ7Œ‚yéNóowîÝ7 úçŸß?Þ´÷¯HL�nßÈnGXP#�_.‘“¤B4�b6„Àü‚шä`“»,òINÀâëSg]½œ\—ÎdŠÉ†S,Óœ~!ÌF X°õ¬ûÖ›i­º( ·½�Y*�;ac¤ù ±0 D¶Š d‚¨²Ú¢Ž"ù(´1"[¾B_×IŒÅ¨’ÔÜÕ鱺Þä˜ñFôÇ ±ãá#xf8:Â>õFpÂI°¾ež¦ñ0OZÆšGî«>€+eoÌýÃñ&6ÅK…b²Û˜¤çk#Ó"<÷Ü?¬ÑË1+N—™àn«‹•°öGÇlj¢B› ’­�Û‡‹j|CÔV˜Ø’t¹Ã°ºõäÂ¥ýsÆ Ü¤±òÒi"ÙÎ�¶Ñ´72«J!¿¥óF�u߃ßöXJçV`Û+–rT›(Œf­½J�ݦw[Ø>š!©�B vÖÛn�½¥˜DŹØÞ«…ÈŠ(UãŒU·7´Þ"Ì«W_×tËJ%ÖU¥™ÃTRÄE† ÷Ë5 ÈeÐß…Ìs}¤¦¸\*ÓÒMCX7‹ÆXƒìíç�_}÷ø‰ìÐP `)›'¸QpNWÀ…7²’#F¨`’­„°è$“£0ÈÞbA‰N¾3ôi�‚ŠÒ&Ï=¤˜§Š²×¬L µ«¹°D|Öí³°yW¯†À¯g…Ç'Ãô»!‹�˜‡‚¿Þ‡—³Åª²±üá£i�ýU>¹(qµO?}éìOs •Ÿc>7Œ×yYV׳«¬49ýrcÀ×T_O—S]Od×dk3>S4¡/Ò3s»b½ŒÅ”Ëï˜ †˜­ H43âKð˜YÿÕÄ\(»®šF–ô£bŸ5›åváN@~“xÀÆ÷"Í£eIüt NPÓ ¬ˆÈ÷,î|,CôCàNâvîw�Û©0‹Ö¸,tÿ¸]Åy·Vعã�4¦Œ Kü J½Õ -aóùP±Ü\ÒÎȼ æK ^€®ÅQò@9gcíÍ(vé'‰çψU�—'ìD\´·ñ dúUùe}yÏÂN¡Œ¬4Ý}ëÇÚX¸EÓ¶“cñR¬H€¬•èb]�~ÖqGsrŒv>™ŸFó(¹Ô™h÷¢ èm6µ ãrj k]LsبcØ nOÅÑÛà5êà…E« ­n%¶Æla‘:k“­øÛúZä\ñPb:®‰¶(GØx•KÄ×a»™K£‚­QGçz'_aBN-B1ÉŽÔw–wöØ{ýX%d£y 0Öœ�EØ� V�øPV6‰¥+‹ÙÝ¡Q"�\dHª´ddõ¬ª‹´¢–eQƒ� çC!ö¦0|3‘‹IŠP~8 ¼ÁÊ>jVƒ]=V vnùì*K®”ƒhÂÝ‹çE?ß Vz×ÚnB9TO}e¿�‚Jn¨²Q?îÔ‰ Ïö° «0òPwhc=SÒ€¥X›¼kþ�KЏßs6yØè˜4•¹×‹‘«‡áAr­€0µª³Ê­€8×Ä*:S@$žV×s#YÀqE¨¬ø¯xÈ7„Ç©³Mx¿r£µN•·4ÍL?:8vÙ«lñÜñ`g¡½ ðg±š¨‘®¦Ç#>$¦ÓkÃÀ¯@Ö4 && i§¹‹R§ƒB“–}CžÞºÚç@mO8žÓ›ë� ô¿�+ldc¼šãÀPîÝ>ŒŠîª>ö‰•lÀQ,Í:Ñ:Ùjãý-äüÇJò‰3 *‰_o§&â-ž0—‡ 2?˜v°e^O|îƒ|èi^MLõ°‰º.�„Ó¦œ`%À;� ê,Ý:»š>ÓCz‰ …¸ª8£ -©m÷‰"Z¬_-tv% ¶UX¼qÄ”&Í0«ÚŠak[åüµéyÒÄ·k‡ö'º±öwL’jÓð>žùqÆ‹CM ]#725œœÈSã7½ã¬´jÉÏ´xvŸ»MSk}{XêÇqz¿§Û½ƒ vóuê��5­ëF�ãÓu»ûuëN£Ç×h¿Ö ã<¸›ç$‘±ê:Ánt‚9ÖùpkaDÓª«À¥`Q¡M ºV2ïx8F ÈÑžþˆ FQÌêÝ–dcïQ�Àcdt’lÑÿ–îG3NUD\› ¤®hŃþ,ä¦xè7‹u. ‚$ A€†DfÕ¸:›X䧺²ªÙ�ê°„¾5±»le�é8" gD4ò¹x }r—«â´î¦î ÒÀÉÖÕ|[ü¤}oðŠRÙ» bª sÍæªjÛúmk?gYm 88q¨�»¶ÅÝã»I<¾Ÿî¦S‡#\6ÑŽC5¦5“‹|: t5�ì&ñÙnв_Ã>Ä!x=…” c+Kes÷l”#®unF—§›Ÿ“ ÍÜãUß<{…VD¡ÏÅ:Œ?]]02a¹qéíE‰r¥ª‚ÎâÆ¼Ç¿  ü›RÀ†·¼ø[Í¥ÓB¥#öȹtôêÓV�NÀm`ÇFLGvhato�> àŽs{¼9í÷‚ÝXTà'_©XNŸE DlÉ*K8­¬6�òÔ+9Õõ(í�ùÃíô§Àöì7c(Y‘D‰’žˆ/–ée6y��ËšÇ%Ï;¿²™shã�'‹uÃë,ýöØìç:¸4Ú¼±1 O �»‹ OQÁD "®@�Ñ[ f{ÐxÒ>”ÅÊÌ îQaˆ£ 9´Ä:¹X ¨¬œK×eÕ){õÞÞÙ«qz§Æ„Ï{ ’µÇbä+6IJ=yÁð4@‹p§„x Œˆ¹ ˜¶£‰ZZ¾' a�C½c^«Ç*ÕÚÁW^žmDŠÏ0î Ò]ÔqÍ;2fsš+Þ­Íòk77ðS£úÿëyV—œíJžÜUARÆ­Gòi_/p·w]p´‹yíü®P[¿›¦Ãp=õ|ØÛ‡rÇܾŒæî^üMù°;Õö!}؇�ÙɇÝ!Æû% ‡óZ·„rïîµF•c/uÔ:šìã¥ö±ßJ,é'ßò�|Ë» ò-\ÖçO¾åOÆîO¾å�ÄMöÉ·üÉ·üÉ·üÉ·üÉ·üÑM؆o™¼Êä^=>þt/c¯I£øH¼Ë] eþ³¼ËFä“wyGïòv~åߣK¹Ž•£ø45®¬K; _¶húy# Å8IDÝ줾$ô.é]7àˆÛõ¬êoB›ú›=ÁQ¤€ÃC£4•üA`Ðucn½zÓ ½C£Yd⟋Ùë›ÃA§¦tÁA÷Úó ý¡·â›'{œK>ðÏfa¸xˆã‚>~tôO‡m@qÆa Þä ëG2•¼¼œ„׋ Fƒ¨‘Ÿ‘ È'vÑ FÝa‰Ã×14õÌ¡ {hÅh”ÅŒ†ÐŠÊøÏày$dkÄóŸëˆçÀóßP¸Ó®æw&Ý´ñjq p6îkXð† ?(.¸nþóÍV »‰Íöá³ú¡…¹¨ø¼~cÎk+ËYø1"ž~ç`¯¶z¼À&9BÞu<Ú_,®«ÕB1þZd“ôñËôŃ»F DS¿X³³ønÂð~Bp!™ ÞzfàÜe`ö )ÜÓ…]cÒåô:}¡¸‰IB º‡Š)€¶¿‘mó@A¹Þ!Ô£QÓÁ@FÄXD™9„ïέ‡“‹v�šâØ4�ñ[<à;0âg†ªìc;ð'Óv{xí¬t„oÊaA*º³UÀ VzÄj‹¬ÉjÒØad™v¼gzºh=\%ôÜ=ÂÁm+㤾2^5STT)‰Dæ#- ³÷BñÎÖ3‰Æ‹ÈlŽPcfª_¼^/YÕÉM®(•”Zå3[¯0Zæxœ]‘hK(n^`Ö\æ¿™N4›8¥vI?4`°æ ÜEKh„Ž€ÁA §_ûz ›î¼ÞÞŽ0Ã}3ò`^Âs4 ˆÉ‘6"‚MÕf;c¬)¥×xSÖŽJ¬0n\ý•WÚe­ Õ›�ÜØ ÓKw†¸D u�$Ø)HBÌG¢?öÔl…GNä¨S�Ølv>€ŠÑX -Ÿ…"WÖ¬ BõÀÊõÆa„ œÚ¡…±É|ðjBÄÐ óòšG!æ!Ž_ñJpx> c¨8[AÏÌf×H2~fõMdfåó$í�´3ËW¢ðVG Dü¤X ÀÇkQwNƽ;ƒ»GOŒþ`Ô’êâ‹{#ÈxpïÄÃ3®7î~ÃW·O]¢�mMüÆrÃþ²0§‡‹¨Œ IfÑhääÇUµ0u‘³ZŸ¤÷>ïݹóùÑÃñè„›?ÝC8æ±×ü†²¼�ýör1y�¯‚A [+VIàLô’cér`Ûÿé/).ÁU(æmR`ع‡þŒ÷õsþQº0÷ÆGG÷eüG÷ïBÜiîÀ‹Üh1¯Y}€þô…©Ý“UM¡”·òó@·ÞùǬðºVòÒ¸{· ãîçܰ“\N>ý{‘yê·#§? #zHGþAéÙ#ÆoÓvŒ´~�£ö¡ñ°3ÀÛ÷âQúÊEYu˜Ö<ƒ­ÜEí–éHCàAhYÖ² Y½í•~ùeâÍ�;¬ åÙY› Á^gs–>Y“" HTíLC.Ò);@n°žæåÊJa«˜IÚwÈ[€È¹òSwŒÙŽRgh1̧ƒ•6ë'>UßûªaãƒÑC¬ÌÄQiH³@è¤VU8‚×d ¿È®r'¤£Ù^�sJ%ôt0=ǽ°†#Œ¼A êSU�Ø*/„qÉ_£½R’fâŒ(pU€ÈEKµbΣ<3ºÄd1[Ï˾° ìøà[¨†A$|m"½vùQT‰i4, HK¿¥Ç$ÅvñÞ¡º0œÈÖ†­3ð0¸ý—‹ÙŒ \9$!jvø8Z@.×+±ÖŸ­Ë n^¦>�„Í,Ö•£ú˜’¯¢J¿Ï¾¯´¿B@°Iæ¾¹*ÌÙ¼ƒ`uéã/›ÉZá±r+?t8ÀÚ>\{“¢½‚ºmdH{õ¨ŽîFE ÁÝ×µ%ÔP0°pøM»1ŠþKÉÇTµTdû££‘•¡×¤ì;„¢•ÏÌf¨$^�Í;þ¿¾Œ�´fXM» !íÅyŠYˆ³a:U•¤8Í;8×�- ü臸˜3¤ ;s†mþÛ÷е³Nõom@BÖþaûV¶xº¼µýæìj\y¸8PTŽ8Jõ%‹…3ö뼂^àXQ8Å]I倘A�BãÊ<ò/�ºÇsCèR�~2à°Ï“ÖW"nÔõºñÊkŒÃb\ñ°Ifü ŽÓ܆ØÈÊ#‹Xœ0¸ÐLÿ "oZç›8áN#g=˜?ËÚ©‰ðèø¸7PÎýgѼ}¿_È ¯7²E6"ÒDpÕ[«ÄZê+g1“ðéü|™“9éÌ|ztz&ÛB<94ÏvÌ\Ò½�øÕŠã³ŽÊ|±X�i´<ÿ×jLš_øMwUï¥ñ-ºŸÆÝß k×1Þ °ŽC�uü!TÖñ>ºØ{Òf6¶qu¦¦z˜Ó•…vndÄ ˆçËÅúÿÂ~ß'�áW,ï1�‚úhan{+éA�yZ…G>ôXŽN•dÛ"çŽ? º‡tÇûKºÏP-Il£‰èkÓ{¹¸ÊÆõFÀˆ9a佊»¯(Ä}A <8‰Ë=�EàH™‰>?¬0ÜÔB-®ï-·IÇÉ~ⱟ,º¿xìàæU¾€y~¥°O4ù øÊ¼¡;ûÓD [ í;¹Öz?ãç0 ¹ÌÆ*×e­Ò$¨¹_¸f|8ìW\ü�Z9äÖAÙZ'°EM ­a—tU’˜rЮ �Zuƒã&ÝÀeE¢ Žâ7¤Î�è([×DëdѺ&q$»‹Ô1‰:ÙM¤Ž“Q¯¤Ñ-ºšF‡¥·èo‰½©I‡ãÀ÷ñ7{ÄÕÛnš´cEï+ê>Œq÷š×%ò~oía׊o^}éÒ¤Gé_ƒŒgåÊß!°ŸëìÄ”ie~àf*Ô×è p+œÆ¼j îßÛ/} Vmk\¿oĉ$DêÉ�€W/濨’pŸ Ïix×õþðÛz±úÂ6Þ|õ}u5H±A¯!)   7�š*&©9ÑEKÉïÓà ™Ñ;>ĉÉóeúýà)1qqB{¨!ÚåÛ¬"ÆÄްQaL¬ÆH-°-jhCúf±|-63‰�üvHÎ…P¯ºœ Šû®©sAí»ës-!‚Þjwo-2ÿˆ«8sÌ�ñŽh�@jyn\x«‰acù[Ó¯yÞÇè¼É-ëø"4ÂQ�«wëÃzs'³s�ÿH˜¥X€ Ák�÷$¤Y;Awkmüdol¢Ö¸$­™çCäÕ6¥˜ó¾ú�V¾�"™æäÞNù é¶×’Uu³Çe*¢óÓEê§ò´�LKç@k“ ÅÞ¾²^É�,]§"ÆÊ‹ãÿS7V^¦ -½ÆNê~¬¼º>¥Gks§½Ñ�á~àë¡~Ù%|Ûu«· z +þ=Ó·bµ�‚�wwö¹k?=¹k­b5äW¯`;V»H×ëéèª�>®™b�6Pûéè¡FÛM~Go½Û7b³U×èîN¬FÅv/—ÔakúÀ:ôøƒ)Ñ[ÔüÞ´èö6=èqQ£Ÿ¹p}²ËÆ®vW¨Ç]–¤Ò¨KiE›&]”Óü2/!ÁjÕâÀÛàí;‰i¾ì[ô;¿A·†¸»DéîÍÅÖõéêp 5&EC»›µêdCªý{ЪëM¥f[#¿§Ñ†Jvâë˜ãÚÚ¨e{JuqÃnPªÇ­ZuÒä#p�߯¿Ká›P…"ÝÈçla�ÇtFûÞ”ðñ„ÞÖË,’ž zùVfºª/š´sJ2ܨ¢/Á½�ζÞߎàÕÞ/G7¨®G:úo¤¯Ÿ½]}´©Šr=Þ3�ÝÕö–úÆ›�åb^”[Ö¸�¡`üÉRðÉRðÉRðÉRð;°ìÌJÚþXÔþ-Þäó³¼%BZ¹ÑfPÖmew›Á©zgo»¶ÂøØÇ(οͳ‹…ãÒj;�UïÈÓM?ߢ®”1;Õ¹ýЪ¼ÆÀR�f“åÂ(ôY€Q�"zw7 „At5}ï‰yΨZ*ÁkšK9šCeEÒàk,py¾âø¸¬#zŸ*´Á­ßhHœ¾·…Õ¡Vr²£Õ^»œe×ð•bH: ]sÐ!gtÇ79⾘'OoÓËÙÂŽŽ?Ù¡.Zv=, §U4‹EJ�¶š%µ÷] Áb„†¹du`"«ô ò$™Ó†Þ±K£Cbçàoê8üÎC‰0=‹7´¶À À<‚ùO‚b—„‹aI’¢“ŒM<+!‘N¹w–Mhzä…„"6MÛëUíÅ~|ô8` Tc»tQ’¾1/ˆ¯Ó‚ÚÐ ¼‘Õ·œCº mDŸ¤6JÔ�.6®ˆCY'‚£Ü-¸vSD‹Í!éft›C#àQJ\N`»áä4ð„Ö4}Âê.M€%tºXŸ®Gƒ£mgÅ”[©· )»ƒÊß6l¡Æò%}x•¿“‹žQCüÕøMrH¿½®ö jù.sºÅußYEJ¼øÑ*îØÐÅÕä˵™’]”. ~!·2]³xËâqLb ò[ggúµÅ¼@ù Ê¥„ÊYjuÕÕÍZµ0¯b/þcç�o Ì'¢ã Jçs�óãÐâÇJ«•S¡Q±×øÑaiÚFYUõ�¹‚Ëüÿ¬³)y'­5Ò³~}ÆŒá< „‚¶¸Êøâ°”6RÞÊÁèJã ÅJóe±dAƒRû¶êÈãÓÊÈÄ-´ú¡º‘øýȸôÎÙ¡OŒ¸W>[,«kSBû€'òèÓ±e7¨Â(Õµ}²,ò²µå)À'ítÌS42 9Ll¶H ¨##~y<Œ ø6ñ ã»(¥Ç÷€×�’t œ1½è’€Í.„ÊÝ e63Jê4õhfM‰5 DçÈG”·„œ ×àË39@5_½ê§9^Ží —câd‰m3Ò¸‘Ÿ—âIÄØ�Ð ¨%}ùêñ«—ðdM:�Š@nˆÔ.Sá‚Ð^ •`ãˆ5,yIÊ‚£ð ¢¾b™°â "&V�Ñ—Xztac×…híÎ…ÃåÙ\I¾É•ÄoiaßRnÐÝ3râ–¹wA.Ì»=3aÞµ¦§ø@ZUq>Ï¢–™÷Wñ�Q-oˆòQÍ}Ÿ!>[V{èøžwJ˜îÜ"/Iæ”ݸµ!îÝ.‰1ÿã!“9µ4Ù'&J?˜•I°R;çÂtåÓй0˜×«ë«YÓBx’¦,Ñüù¼5!&¹“ìºó>óaÞ}ðl݂嚜�áQ'£M�¡ô˜º)ÆkÏÍæ¼ûýçÆlêâþ™1gd¯0›ð$ohzâMOKˆJ½Q:V#(½Ã¥íâ4v³}X�B®ôC+�£äÜÞ D‡:.6úx“Tƒ9¼ZªêÎÑ-šƒª_}K¥Û…r|8.×zlu B9Zºç_ä6”ƒ/ úu§€zB1:×ÄsmØ"ªƒ^ÁPŒÎ•׃:‚ú?þ�ݵNá%bÄ›É!yf�¼Û7D púßÈ»JÿÏ·cÿ×ñAÕнÛñ¾´Òº&vi'Ò›Ì8Ù"V¸[s0ÙDJ`YCCg�Ôâ �B‘»´ß0úÜd±.‰áªZOŒRåÌ¢µ,Ìñº¦_Ç—gòǵ¢å›’“PXAD›"úl3l�á&(ä§:¯ôˆYí êÚ�cGy‡*$xþ»*œ��?h¡M\Ë$äP%†n÷Ödˆ²%°¢ÿ ¯Œˆm˜±Ð–ì ªhZó¹y¼wš˜$…ÿ>-²ór�z?¿Þöˆ*õ6¶áöébz ÿ»�’ÿ·M—TŽ<\ç°a�zŒ•E½Zmà÷¸m†»3±0]ˆŸK.·UKf_˜Jµ„)Àa¯óºT.]Meã>w�~“pL†üàA}�%Ï"óNœ…gfk¥b ô ³�¨; `Dem'ùÛÍW.� äðm¶-•ìT¬ÝzÙ±.•ávðìi—]AQ¾*›ìöVE:?D‹{¯`{ˆW»’J�KäÛæ1A6ïÄÂ^¦…3_µA}K�pS$·>À*Jæ×p?„C[à%.&�Æ=b�‹t°i4ä1Ó^«zXø–ëW¸´!ô&Ï{¼e¨mŠm€KÖœ©1”¾ÆÐw1t/õ98DŸôºñ+wV ͈H¶a§MH\Örc§G"*~;dï¨;q0nRqJ? ë“ù€ñ_\ßç}3Ä·¸ÂßãÊþÙŒïº bÿÆqcΗˆ¼(úö¬§^F �à múQXÞ‰Ò:6ªÛRlX/Ÿº šJ¶¨ Óp1ETk”éC#fâúT’!HÝä©m€ïi»ñÉ*i“ò ÖÉUfµÖE—l�¸oEXwѰà UaLãC;ñš —Os5ÀÚ¨%­U¯ÓÕ¾Ó&˜•ìˆïXœ[à­Dcnc8 ¼ä½v¡‰Š}ÁÓ´ÄO–†Û$þ>,wS¹w;›~zI*¿ƒèäQlt÷©}·)¬[z¸¬Àœë»I¶[r~Ò§{] úRé{°mxLÆtêp�8éâyê7‹ÿ¶'ï&rÛ>±¤Æ8<ð�c,ÃvúÛ#EAnÐí7üáNÇšÕ¢u×+-8 α}MêHùÍF-å&a¶•z«VG�´-ÁSZ\ÖÀŸ9-ìÌËn!õ¡¦wÉoÅ€ˆüí¬ÔÅ%çÁļý»¯(Áj¼U…‘€t¼�VͱcŒ¶³Šu–¢[çêI‰ëÆå�ÙÂ|Ó‰¹Áך‰ÿÍùsí©½f«;Ѷ: OÏ÷xm‚ K,âÝ$°¿7ÛúÀ6jßxKÊ`�užFê»±:sj…ºFêÙX½KÐX�›þ¯Z„+|i:ƒ*6¤¼�§^ÏÏߥçg¬N)ŒÑÑ–†¬‘šNGêá7ÓÇß|ã)ˆWF1íjIkøa#`°Á¤WúÄ®Í($;"¡ÝˆlÚ4nhŸ…â+zñs‹69RÊ1]‡¨ïf`Ç:Ë£ì1EågÛ‘6l™Qò¾4Ý,yBÆ5EìKl}¡>¢o1+/)~G C$A€bt´iùUf(¶a,@aý£mƒ–êd¹[öH<ì[ê$8 ÆIr@„âÄ”# i â DcÓ�í¬iîÎyA¹B,i �{Îà,­àè”�ĺƒ(dyÕv¶-É9a� ÇÍÁò5§#7À0h«ßµw»L>êõUáu¹zOo×Ûšù}’�Ylwd¥ËªÚ¬u£J‡åL“¤0�&÷è×üð0_±•á˜yïZì~üiÔXûݰ›|Ô,.Þjï‡wgͽȦ�Žámt%‡=u¾Ñ§Ýå¾nÝMîNì> 6éêµS7P"fGð9‡ÜˆéšÔIÕܯHjp¹Ycž #"½Ï‰ŒÆ’�:Ç^X—o5€G •<¿0¥ åˆÖÆó2g8Âo!3ÝMÆ…½è«\’Oc:y y…‘Úä5jÊÀ`ÒFôæouRp‹b>Ck0a»Zcã)SG2õàa1Ä%òöçÞÞêÆmi1�’÷T£&* ¶~æ–”nE`f®“Á#•–á%&õ„2pògá,Á¾Q>Êòßà>º³)àbzT³Ñ£O´n[é4Øø›c7 �Hj{vvòf€A'Ä6*¸‡>��¿2î|¢º>à k“KÎòvM³©w“V±á Ù >1ãæœi.fR?¬›y ñ �hçxk«CfÌ3ã|5¸›ÒàÖÏ`Jyˆ}í=Tè&‚íø@oK&·èLrž:‚%üm'­á,É ¨Çs­† =BÞ¶w-7Šç»OD„¾]÷‰@Y> "×ÂéÛa”IßKÛkåÅm @q»…wµ£Áo°€²ZgíÜFV ð$7Z»(ц=ó·¬<¿*ÎÁ‹ øv-ϬcÛX ¾‹²—»hl8z%M2ÖK)Õ-åéëwߟŽÔûÓ‹Wo‡Æxó}&Ô‚mÏç€ãbnÖ÷v™¿\Èý!tìnåÁ‰|ÀŠÿHíófÅïUwxk*¯M÷ï#CûØ í¹‘ÿòRÜ”’ßİ=Šç'òa÷U_`ÛVá/ëªô̳۵=Ð &Þ™ù³Üp-å§žÞzþ'“o»_Õü¢ ßÒUïgRß©Ÿ>ðçüb¢¢¢#Ô§×pœc¼[®”Ã=5ðs ªkT²Ò?UgtÏÚ�"ÌJ:³Úe`JUš)Ià)¤ Ÿyd}•�²N2��_æ}ÎÊQíWDé+Î�ε½^ ^Dó#fæ]¨(–¦›;ù ŒÙ"¿ÑmÎ/Rtnˆ²`ºŠ8¯‡# Çê£.rÊks?s‚×W ÞQ~i½r;&´Ipß•hí­W‚CÐ&:àì@ê [Œ ³j™—Ú_:Ÿ�ó>s‘7ÕPÙð«p㊒ˆò�“Aä,*Zç:ÓllÃ(ªa?l~{4K•ÅýòQ6u%=›6®ôY—Û}ûš)èÈIXïj5à†Róðéøk HѼ�é+õ2A“j³@ Xú—LJ_N�W-–?,ÓU˜­ÿSÏç…Þ`;æ7_ Ç2×ó ¨á$RoµzZ')õ÷"¼Î�˜ÉGbçGŽ:!´ãC�/s¼Á€4>O4Vï–ãçceòä?d°ÅE)Ýo¯�xñŽî·ëéüÚÓþøuoÅÏÙ¿¸©‡rº£Á{BU…EµõTgq£{‰�[w}Ÿ>©?«í‰ÿ¯à)O¨Uóžý]ÿ2+pOz/Ö}Æ×úÇþè6¯/{ÿK°­}F´÷esC"�ñæFp�P‡�¨ks¬•LYG߸>§S‹öFsW#¸áè & ì7¹w¦¢\æu£MEû™`‚kN½²ÕÖÙYA¥Õ&’¯å.G¾±½\ê“/òÔ´õ6oŠôžnL6Û&Îy÷·�Íås~O H·½]j9KAyÏ�xb!ºâ³6ª;Ž;è@fÖ‘/“C¯z“‹Ÿ- îÈÐsf‘h0Œ#DÎËÀ«ô¤£<`Þ‰È= Žw)ù§)å^ß= BcÈÝ žKz¾©ßðºÆ¿»ï…×±¿jJvä«Ì`¨ì[YnJž/ä|Ñv ,êæÐ`Ù¾‡‰54EÀg–#[ìâÁþ€tÌ&£éh<�²áÖ[û®˜C¾JJ-EV¾"|‡>q@ðŠFÓáÿ<ÒåFôO¦Þ£ìpÒ…HcOÔ q:cw\ú-%8øêZºx;Te=oŸSrT¦¤dŸ·pÊðÔeV°·èÍÛç/^[ýb”–w;ù:O7RUw¡F1KÀÉ¥#rœãUÆk–ÚÌ*J›sT[r.„z‚¼»ÇÛ\ÚÀA…û��òZŒF‚Ö—ûø¼"¢Ä1Z $RéÊË�SN;­…j¶'Û¢`�ò¨“‘ú0eûðP“�nbÿñXMÔô'€úJüЍ!¤•„®pða2þùx8~0ø0ÿ<¡Ç?Oé·¹ß=„î²sj¯Å5lŠ’9P$=Ôtv¥».x;ÿ`·SÆW”–ZܱÈÜ—tᯠ©¥E«=UûNë  bÛÆež^ÒŽv ´m|Aƒyn‘2DŒ�³ ž7rô/‰ÂF#oYÖ«µ ÌñÂÿ2¡™�=m$s¤ºÂ‹5�ô¯Ú‘HP2™4¡sqˆCº¸·ÇÕù„ûÙï¢î~éö.úÃ�Ju¶¨øº‚-@NÝÒNÔú×€¬1:özKùØ ,‚�òߦ ý‡äŸm°'=éâmÎ LÀ¹ ZUê6¢€Ëõ Ùš›, Ó|:ÁînйƒëÉ?ØsÄÿÐÿd?s °°¶Eíh!xœÝ=ksÛF’ßñ+ætu éP”H'Ù�c«J~%¾µe—¥ìú*•¤@`Hâ ’èºÊo¿~Í Q’“Ý»ªÝˆÓ==ýîžñã{þöÙŽ{¡–Õ*= ›?:ŒáÏJW¡Š–aQêêÉA]ÍÿzpÏ«¤JõÉ‹ëµ.’•Ϊ0UÏu™,²ÇGüSðøH¦˜åñ'œœ”UX%e•Då#Õù*Œ ‡* WúÉÁ|>ÍN…øòô¿Âˆ)|[«( ËòÉÁò+5/á‡ðµ‚±Që”Ç—aq¿|9}|„ŸÔõx&3ÍNÔ€~Îø·áã#zýñÑAœ¨‹«ü0Õ—:Uó:…ÿ„Q• `¶ã€GÆÉ¥AgU~uÀ/3*Ûàv7\µH.u)`�»J²E©ðm5‡'¡ª BA7Fê*©–*“çåXm-?)Õêp¶9ÌFêj© ­Vø¨Zê@eõj¦ •ÏUUè°Â�)U’á�ípkýJ½£¥*ò+|«*PQ^º\çY r@¿„õ¤ÚMo&ˆò´^eðX!É·|Zy`ÞeF¼ÐË0­�`ûX…Y¬&4ßB)"Ti6 wÄ£¹ZÕe¥f°Êòì0Ó `™K ë­ô¨PFaH@ÚÞ´c»S…®ê0Ä&HÎcµ «"¹u¬R6%÷vå4Szµ®6M”`#Â,Pº(h…¸=a©Þœ^¼>}ê šW ¿¼²*òlqr®aUi™ïÉ£Öªe¡çO|±û…ÅnŒÿï¸ÇˆâÁ‰ùÄ’Gó“û’iÇ:é´/ÝEBOâi§²"Ê›¹CN_ÞK:h·¦Û” d$ÑXFÀ{Nœèd ø4Ü!·åZGÉ<ѱš SøÀG0ß2Añ˜V€_jR05ÀIÕ:/“‹ˆ:d-à'p¬JX:Ùg¸ø ‡Ã<@¿$®Cƒ´Çp[Âo(y£à“´jOôqTKÆAL^ÂÊfI›n²�T]jOBe�>¥L�â¹wÉÂMã?’ôWP]y8¯ê4ô£ó÷N)Ù)•.V¥/!;æ´â27O:äe?�°�uZÝæˆgYÁÜwCâGzÿCc¶MãR_ˆòÓ�0üì·:¯¾EÓ_Xˆù‰Ì‡Õ}x~Ú fɵŽùc}ºõ³¥NÓdýí�X˜�²v°¾o‹X;î;œˆ…øz�,L�M"¨’iQ£d,d0†8>¿AG_,Á"Îë,¡*Y­SÍŠ'+tT%¨ºCÐêZ]á°*ñ­4_$Q€ú"*’ëW‡Çgá hó>MYªÁ䛿<’ á -Mö+PZiš_‰³TnV³<,š;¢õ�ŠÏÂtS&¤B�xI˜ELªB/@¨+žu„.Rv© tt€„ù; ¹sx¾Z×L#@¢Œ–z®`&ö‚Ø5Ò±2”êõ:/€<@÷*9$س)…ª¸ÔmTÙ³{­çÕá÷ˆßyk5xýýù0�MKaK jf Ýp¥ˆvðÊ0Šô^¼ø3O °]IËRa±¨Ù ¸½,S%:lbCJ”›†û�¶ãØ34xbÙi”™È™s6yžãNšòF¥I,›Z>Ú¶ï“ŲA¦7¸ƒCõìFˆN�gV.ÂûïÏ-þ̯`HõZÃ`Ùfk€çÖ…Žr ‡Ä$äúVE�—‚›_蔈R.“u ¦¿ºÒš6n5‚ Çh\=£I2]"K� ‹@$•�^€oÂŒÂq gq(ÁK0�VŒÌqÐ�ø¢NÙ _Á΂½N“j3V§qœ0§›qI*ƒœÝî@ ôx1 O=œu3D�X¸¸�êåø˰2aÒS„sÂÜ`XN;ÔJ…ô>Q­�*†ž5(†l³Fˆôm–£&´_˽‹¡msò5x઀® øÿЉ«å“ƒÉWÿqpb‚ã¥ÿãCüñ9i§5R£ýûWÇðû «Î^\‡¨yÔÂ9ª$È& q(Sô «½«ØB Í&©Áü¶|žçT}¡žé¶Ü”«Ó>Æ×¡Dô!ò¬ÈKT:½<؉ÀH�>Ú‹Ã>,žëTË6ìÄâÁSuÈÐîNŠ£>$ÎXBz)q´‹û’áQ¯œFéÅâQ7{¢ðs ïò+Ðæƒ×É*©ú¸rpúÅÓáÏÓ{³ÅäFz 9ëAd£~W§À)®rÌç$Í—�#Ò6˜h›¶I~/|·¹£ æ@ƒª+¶—¨¬¯ß >¹¼<5:O2±KVi S‘QB$Ý´ý çNlÀX\;·àÿ‹¶ý¡ úNšö÷>÷Èl!×ÇK×�ÉnàÞQä×èäìv2R›éma÷*Ô÷è9ZÖë�}1Ù¹x¸xŸ¸¼+òH“íPè•ù²Ñã¦Þ*hU³& [âýÏÍ^²9 K`Xq¨K0!Jak.ãUÖ˜[OãCÎësi‚  �ïm®É¸„Ú$I¬YžHëxƒâà_È�3�ÁMc(kC_\Â5‚à¼ò"Ü”%€—Z4©ŽG ×�œB”xXAc¿ÕäÍÙuªït†»®$T¡Ñë„™Œ? q †6äF‚‹*“æI…„¤eŒÕ³\Ï!zI( ífƒ1 †³€Â KCš:šŽÔx<6¹!õjΊlí…F<Ÿ 8F±ø)^ÁžD™1Á ñ¯¡õ¾<ö4Ì9¡ÛÖm_“'i(ü¶® »Y»}Ù§”ܰ¯Ý0NXlÔMÀcŒ¦$­r£Nù²mL�»w+xè¨FÁMƒ?_>ínðÏ£ež§ài=ÃèCDíÂD¦ø"@|hyt7Œ®­Ÿs;ú3=àí;€EµL6ᢨnþ£@ŸxàáE“W“_§ð+ˆ$ûiu¿­o8_çiOWR¶¤Se¯œOLÖž\!ÒF ØÔJccmJ-ÏkÊc±‡‰˜0]䙫$Rkkv:Ô¡ŸTô�|Š�Ð*MÖ�’øœqVo¥�Ÿù¤Ï� Ø#ùÐ¡Š“l¨¾y‘¯`‘€Û%¸Ž±Â&ë”$É.v��Òká•:�²X9'O8@ƒ7zz~1tÄ혶ÊÕYòq×ÌV‹Ò¸’5·ï¶¦úZ]4Åt¨7és�Y²œ´·¢Á@h$®±Ï¾±,€ŽùÊáì†140^À0e1="i¦ÿaC¾(òzÝò (q’T`næž³|³.fç°ð`•7„ã A@´µ¶îŠšé4¿2 ?B?¡áçPBÖ` ¶�³"Ök÷Ýží.fI„Ž%½h üm~ FùÜ÷#€Â@^Ì.�€ç˜`Ó—º€ýãÜ›$ò øÈ9+†sÛ;ÓTÌÎÔ<Ä7”NKÍ&&€'¥ìe7N‰‰MÈ’ì)!øÜåÏ’ ÐJ×Ëp¦9EvôÛ+òæX™Ê6½`™6$PW'™0DzÕYò[m}¬º”ð©LjªÍW0‘IÇ®×)¦ý¨‚{‰¹·�VmáÛÔ,•0FRÜs;ÔÒ°ü O±Œlæ��ˆ%ZòzÄ F}¼Š¯…hN€ïq®´…¼ ¯Þ„s›ç¢”`{KŒL“ª£ÔöUF¯)ªÕÁgE j¾YeáGD“¡Ç¶ÿXørAŸ*ŇXkoj\fY*¥�S…!ÕÎßFUxiæ&îh �{.x=F­�ÜõwÃx�œÔ>s¾3ÒÉ¥ñ¹ü‹Ãr/úÊÅÕMëŠÃÀ+ò@H“‘ün],°P€'*œ†Id'±ËDíå^ÈÇ¿ú¯á#ަö¹Û¢WVT6W%ZÀV¬KÐ[o×Ç9ÚÚˆS ðú6p„„ã¦à0ÇH°BŽè€±¶°$È„ï¡ó×®ŸGç‘(× É ±2S¬/ÑhvЊ ŽÁUúo-аà^ ÿêÁ1Ñì!èˆ\Ø÷� ^‡}™ŽÀH`�¼3$ü"Ûñ~Ã/¢U³¼îáA æ1Š>ñ�h«õ ~8Û±?œ?˜ãL½aÏ�Çš2îò€UxãÙ]úøð%›I4JþÑ(Ê&vH4Ê‹ò¶*?3wË»—h�v5 ‰;ËDjzoqgI— >&–jŸu?Ò‘IO�SpøR³IaL{$ZÃDk-cK«ã:¾bÎ(èjpjZf `•%[6²/HäÑ™`ûh9ö>aš1t·Y"ÓC/WKZŽP5ˆsmÖ×Õµ½¢å¤Q3Èú‚_€´¦Uf6öj»‚žb¾´Z4Ý¿1pn°{YåÓ#eçØgytÙ¤n3ÊÊ9ÀœÚkè¼{¨6t^‚eô~†Nl¢Çй}PC瀞ø0ž¼Çþ­eàý¶~øŒî`ȵ}ßß�{YÖFœY`V\(F?}(®å*#N™ 'Û°¢f­3|¯êÆ$Òƒ *½QS{­´YB¥fÓ¦?•”eõ�n /p}vÖkÑ«5Æ:;˜Sò2I£ÛXÒ4–Á/@r*ÓÐ[àMm”7È%2ÚǺšÍÊzdFýÙÖÊfÞÓVP{\7ëe±xì¥ åÈù*;¾ãWp�‚½ Þ®êéb3S‰ø²è¶ao8°vÖÁŒÕ=ñ‹x<ŠIU7ËJ�ß0Á HÚ£—ëfÈá™UQ²¼ÝLÁØ ¦RÐøìÀœcZD—ªäµ±¼2Ù²åžÆò�å�¡ ;°¹�´bž…tk§Û:käžâè§s/zg �Ýÿ=¹4=%†€ÙBLàœ”V²¡­º�Ø ™œäkN¢«–L·«ÕÄ<¤ ßÚj^í­©øxFf²dƒó!_5­BW^Bn‰¶…̇ߨKÓ‡E›©-Ê0ƒ2àÌfæ s([Ev`ôn1v×ZᆵïÌ4$K±Y˜¶›wTž\žÈŽoóÿÌoÏòóÛón?’=�Må]¹½1j»9àô¨XÓ‡¢Ócâ�9ë1ž#‚Þý¸Ú€j_ „6�›Œróãùf±PCEý—�)êÿ`ûòéCØ—O­Zôø ÅlïR…¼# YÔÓ¬‡ŸZUgÜÇê14UÛº82¸,–Ë"EUm˜½Ée±1,©°¯îÒÕ´Í}÷nït�ýçóM=å˜Ö/h>­X‹§EÝÔh’ƒ'íůç‚é¨E6â2y´òYŒ¹L�^–“�Znwp2Ö¥DörÞÓÜÀù~O;§¬-UfbbóÌyè1°×oçs£3 ëÔ\�än*ÌMp 'ÄhYib0Ê„;p©û,9Œùw2{!£»õÈw™¬Êõ±^:šçè"q¥‡ Ú;O¹L?¿*êKJ8;ùÃÉ©Õ-Ÿú B¸°Y¦ëªÙ´‹-$Ñ6ÄÒéÙa// ]W+ð;Ñà3ï/õãs¤nG³-ïÊr…6pðj°ùZ$©=Ta úèûVÂûÓ­�Íû‡«¼Ãœ?3(Í|‘ iàèÀôhÎ¥™ñW‹âò ! šŠñxf›� ßµ9ðàÌ7ãó’ŸŽ“1”#¹MgòÀܬ¸}bàD”+æ»b½ûܧ}/žÃíȰ¢ÐÅ"¬ÉzabŸ Y-‡ŽùË[ܼ�cõŒñf´%½ã4Ù°{“Y–¶Ç;tTfùÐŽ¼‡ö™��„uÆÞRíÞ7GÿH­?I*o¡/Üò}™�ÖìÎÂÚÄ55Øë÷5wó�âú¹$…Ô«MÇŒÐwæ:µ×›ç^cæó´‡n>ÓlK§{X 0½¹‘² Ð1 Ž/H0Bžo´/²„Ðrì£c«s¡’Bû˜®U°ŠZ�w8Uì«K�À±µ¢(Û´ü-²Ã.×ÍfE.õö%Šùf�ò~z0F\ß¶j1`gm0p¾yŒšT]EaYS…*@�‚ý8XÞ6×c'j87Ÿñ.0CÆ3g4›P}f¿™ø<8-t^uak8[à„`ìÿu=+Ú« –º«uYSwø¹É™àXœ²TN#GÃ�– ö\æÇp&•òŠ£ŸxÈkÙÚÈìS«�ùŽNÎÝY.ŽJÌ.dé.r+REüáõ_Ï�Ÿ ªåjQ‚;™�´Ö ÖuŒr°ËÃíÑi+™Ó¿=êµ3»ûiˆYÁxÀóæ.ÆçÑP¿ïyØ€�Öò×>óvÙ·Ëà ÜôFh-w˜fY{ÃìξÓì.Çá¦Ù%òdžVÑdAÌÕRV7OoˆÑ–Ç{È:9·V<º,9<±‘^5, IÒP‰ªýFVèU[ZÅÐX~ü~7méüsy»5Œá¤4»4¸“øá)F\Ð-<%rîÍs…Á¬F1AÄ“™8d]˜/š¥}­)Â,iÁÍE,ñ‡ÿ/0ÁÇÄo `_~hZšŒttfÞ¿…Zoµ•ÁÁâXÖ´xYþ÷MÑV ÿì½€9-0ÎðzmÅ?€gÝ3o.Újx‘Í£æù�)m0ñš"£I�“Ø­4h¼6¦„¾äÙ,1°ƒìš€gñ.fòq …wÑhÃGü�FQ±¥Åæãƒ-¸öÖ¶ož†E%}þv²^Ë«Á~pÖN¾jËͬ9ÖñÎ�m°]¯¿ 2¾¡3XËͯÁ/ŠÕ¢˜–»Wì¹ÍO.SÏK¬™Ó)Á ó–Ä%NÁ�E‘GêZà¬æÚ^�˜ˆ>h‘$üއp첦u®X_VuX(›ód¶`¡ÿÛz±Mlpk(’mjÒ4¬*ÓC†#=ݬ�II}+y¡Èrí>ÉÈ>��Ï$#•�,Oðü% Uû$˜Ð?÷ü?Ì;ÿ^�ã;}ãJ9vžƒDÇïgAïýzþïàøïÅ9³Çí’&Z8O·•Ávx·wÊ�t…3î 1µ ˆ­‰r´6f~¨Ö!‚ XJŽ5Ž rŘ)cHÿo�†Uå+Ž@³û´EÓìë—o¾ç_¹ÆÇyÙMOFÎìòiá›YU\ÖMØÇ˜SH •©l%¶ ,ÄWNÈd]8¸é!6êµtúo`iÞº�*y§˜ÍÞ‚€éÅ?ÒOɫū‰í7P,¶m¥ß Ωl¦ÇñŽ?3Rãôø JÌ–ý}$ Ún&à#3bã£gødNOÆø‘f— 8,b¤‡pã€ÏžÏ ‡3ê ‘{&À<ÿwŒ€°xÆ×U‘iwFdtÏ›¥„K(9X-&a‘¿×H-Ö€3"·Œh<%üÎÁMº¼ o˜¼;(À°¨že~¢JÕ&sö¾ù5FX^¼ò“M_b�Nä'â�S³û0ÕÖ!Û=32LFqüÇÄ¢Ôb8‰�±(vp²-øíYdc½=w_Yvhýœ·¯¬Ó,“Y�Ä MCƈ,a -~û#w¿áÀ¶Žéò™Àç&͇'Lp EH–ê`ã$ Î÷©ËÛÎæB8.äYH{Ö ô=^âx¶ŠNRdàÖ gòÚ2Ml(˜,³Õ�ìhÒækn”L7*Z›£è�ÅúìB�¼©nÃM~²Y g2qˆÕnÆB)…™—SØÊ'O¢3eñ©iKsÆýîü)*UÀ\’Í™Bƒ6‘©H±ÛÄÍݰ$¨&'÷¥DMåùwöŰ€�qvNÆÑÈcå�¡¤ž«âP 1w’œUB9üæ2›®ä§)Æ(£/ŒÆ”þºÉÛ¿o0ƒ [Û} '“Œ’|ÐÙxÃ8ª‡‘õÃȈv"Ù�f¢#IïDû0hâ–à™7LSÛ0 I.l´aFB))4Ç7[b)Vx¡�i¦§Z�˜c$:–å·œYj]9ñrí] <›\†çñÞúšX©û9zõáÙ(…*cs“ãW=(k‡#MâɽJ¿’¥ÈFýT^ËÉœÞBÁ]ßÂZmÔíß¼nm ¦!Š”p½ïxu]T-Šä‰¹0Ñ>ÿÀPfYëÁzÑ, ¬Í“ð5‰M`•¹raîc®‘â"ð�úkj+£�ŒŽÓ�·òølÀ‰²÷öVaŠv Д)¤gÂ÷PÐöGÛòm !óm¬@*sì–³‹ýX�Î…ÜÌÐ ˜×—PÁd’¯((<�ݤb—¬…â´:ö°Õ%d(€kÌÅÒÍ6Ëåv¤m‡žÑέ٠¯m\¤™C¦ÃÜ; ÊŸrrÅ�c~Îb‹÷#GDzÐD�öØy¤e·m‡º«=ç0çiæL;ÊâÃþ–�<í�Æ�sô͠™eÖ°ßJ0‹ ¤+ŽHºdR,1ˆ`A>Òæ¦þ¥ZzÜ÷z"kÐPÏÞ�Å>ÍIE|`¿$ºà uNyq r×U*ºÝå±¹Lh•‚‚ñWà^]ÙëÎþèg—­&/b‘�M×(>ƒ�c£G\à'7§ !§ö´«•S vK µ$Aûþ• ®§ª‘Fæµ¹·øÄ2BÁ®b7Ø“OÇ;è ƒ¼º,•X7¦’-ÊÚ­žÓ)"s,(|ÚÈTS�V�6µÙ! y{SÛfÌ!ªj`¹~{¦·�k³'�'X›Ø…G ‹xï�| ‡µW)¿ÇV/ÎÕ n€ën¤VAÆ-‹ö�‹¬ejôHÀz@ñÊž@ôGsIG“Á|04BU.öeE’Бp]©®2²†�ß)àPÊ}CeŒmuåõÿÖuüu=o,_ƒ¾×mSd¥ �•ꬊw�ÅÑ+ e`’Ì{‹V•Љ˜öÙ�µJìNA$˜ B 2› k&iö«â0JBìB¿ÈH Méº-C‹Í ²ÒX;ž¼ DÝf,Å ’müöñ™€÷ŠLn–Ës'[¸=ŒÀ´&VàÎÞ­#gÝð¬8o 088ÿÅPOzJûs‹$©ZTû$ì"¹r×{=»Ü ÔàÈziamXðz†!3x ì}7�p¿Û“(6,�j¢¡âÍ(»°'‰ÎìØÂ��qª$À »]¹`Íç ìáÿ¡@á[†¦3Â"}>÷?sD÷ÿ§ã¯ŒÞœù~ùoÓ’�Æ÷ªG¡&`s3ÃGG„÷&�%³Šjt°³�Œ×:êÁÖN¶´(Ÿ:>c`ÐŽ‹ËQQ;W§£êöÅx £¥¹—}&QG> prÛKÜ�uÎâM³YÌ2ŠÍ@'Ì. sÐAHs(<þhP†ícM3tä-#¶ÉDÕ{ž/º1v˜UÚ´e{¢)]L»Ú äm³�uÏB«—ìߌÐI3Ù´�Ñ÷Ü“ŒÓ#ÅÁ}¹Æ–t¸\ìàwg-‰ì çî›ù<ög=Ý8ćé‘q¿Ü™Žñ°¨ ô¾CdB™“”Y¯‘fÅíErŒ}6³·fü‹æ°lä+Âp,9�tjÆc®©¸›1Ão°êåU‘é2*�šöåûËî«M›¥Þnê2ÐÄ·Âòãn-.f·ÁPÑèÌÆ¼ö/£|°¯ ÛÕ\ÀÛüui%ø¸oxË�¾~ýâÍ.=YyÔAE�6«^“ }$‡j Î QbŸ™á*—àÕ#YYjFÈ„%q豙ߺ¼‰ÐÃ�w xZl¦WÛè…yQÅxCW#¬Gß‚@×&1†šE<’d‡¸‘£–N¯Ô]2�WBË–Ÿ÷n§)Î7=_ÀÝØ]-w�ìc‘ IÛÒŒ·`ã{~$´1 ø�ÑÌ€ÐÔwù–ÃBWÕuñ¿6ó‚<8„³ ¦‰…:1QY+šã°Þ'ð>K%)!¸��Na!�I .Rl“ÙÝI¹m¼™ôdQ¹1�ÝBñx¦;+Ê.“RKÞ±ÂÍ’,ûáhÁ­á²F%æé[­¸"¡ˆ®ÇXÓ\Yn‡Œ®Æ}ÀÓ‰LÈû ³&–¶c•óº’ĆP×f…0ÂÁTžÜu‰‘Bæ—EqÉh2»´E,m–áÂ9©€”ÃMÍ’5e8D¸e3 js(ŠvGN†üznÏh|íQ¤øÿ/õ¥_Tc¾ˆ©óLÁôåî.‚Bж�¢Þaþ {Î<Ö)Ö€qЬ þâO±ÅËÅV-Ñ¿À.þvñ/°‹ØE„páÁZX;ò( ˆƒq b(† ÄŽžï�ñ¾pî ûÐ? l(Ôƒ§ó¹„‰p4Ið‡û >¤}*f qÀ†¼£I�ú˜ƒ‡Ì3°«y'<Êʎ¦¡þSfF&Q­_°bPXH }6è“”8¤#?šF¤ð‚ÏOƒÏuR�UÀ¬7eÈ™¹£ý‹qt�SP¼lw€ÀÈ(ê‚sQÐb»0³äªwª5lƉóB%,¨.ȈV‡£ôxëÙú.Ïž^JØ¡ŸK‰fWä9‚j½fK’:Z�q`,°F�تJÖd—[ÑökÈ¿“Ì*g/Ù!Çð 'œÚ˜I-^1Yüó0@öBJÀ€í6Êð³Âh\ðÀ&ú°&ú@&"¼�×_Fm(”‰¸¹QŒÃpØí@!Äøw= =M $ù~ R_bAp.?}�²½fxP[Z^úñÞGÏ’_ï“dà�L 3¸·d *Oð¿¼O|_2ƒÓ{•P ךüÔ…ßì—X÷¿ç²…E<ÊÉÛŸæ?|0)»Âܧÿ냉ùgd ¢~™çæ�0™ >ï(z5 f7MBSPóâ�y¿´†OLă–B'óŠ€ìHŒÖõÃÊ}˜Qç¤Ec²`£¯ÚŠšÍò#¿’ɨ²Ñ&¹3PJHÆ–äáFûÄÊ| k±/ýèç?Ÿ» 2Ÿ?ˆo‹¿þÑ GuiFué¢X&ßj”œ@^¨†êÂAæNæÅ%=¥Ô*‹œíé0r7ê .™è�f('Ÿ+ ’,¬Të¶×”C;³ïº__Ùý÷öüÙ‹å^_«= ÞóqÑèzÐØ¶Äؼð¡ç®¶+°zÖESŒÏ©í‡MÓS€MËöžõƒ¢Œ¿¬×¾to¾¸/v6Ñ#Ñÿø Ø_~OoÚ?²¯÷ ¦¶»ç¯Ì‚º£çûëAK(Ä@0ŸVÙÌ o‘YŽäz`ª~”è—aÍ¾é¯ å#<é¬ÝÁÍ^º‘;Ú\óç��†¢ùÂAÇ”'”P‘©ºQ°VXÇ7P&~T7^ ÇÊNü,�…p®ÌäKÛ‚šE¨i<m-Ï< INÁ?7œcÊážEã«‹œ¬Œ‡lÞŸ½Äbˆ—Fq;ÖdÉKtT�ÿ6r»¢üä05W_²tÑ‘| “ºÑv¨:úÁvôÔ[;sT��Y3ÝxÈ-�Ò.š,{‡¦Œ�‰�Šò*¼õ¼ þ¡Ccˆîð$»ÓpwcÙÁgAÏzèV|ëâèÀ£»‘…OĤŽó»�€ùPØU<¬u£@Ï{ÇÙ'/ž5…sIdQªI¢…q€”ØSU Ì𮤓^ÆBÊj0îžáb›fÓQÁ˜N€ú"8M½ˆftvÃà,;W|-@ÑÞCuåøÖúÅHû¨¯@Úo(�ö‘JÂDõívÕ›Ïò[Wdú1Ü=uºšEòÂ¼Š‚*,•*5`ëJ ‚øaÓX) EÕL�,b¦Á P"ºÚ!œ«˜p_dí¼š.…àBÅë^UåºXO¯€†TxœêÀ`q ^»w`^ÌáïÌ Òk¹«êÚo¥Þ¶9އC‚rXI�9õP…úu©ÅçßayÜök0í˜WÍå§L-’ÿ }äm–*Ù$?X„~žâlzY¾¦¾Ï�¾¿€^¤(Neî�c¢qpÏT>‡(’ëÑfݪ¢R¨r²g’�³“­„}H;l1ŽÊÝë`‘"^TUÕ^Ñbqåמ1Øm®1R)\hÉuÖOI­ø?åj]˜&®þ—lVpµ¿_w1M°]ζD>_3{®@ê£Ã*CXYb5\™xŒ¦BáªlŒÀ‹k©z0älàL÷$-âôžï‘]IeDæ±äÊrÎ,¶© Ö Ó;kÀÊR  Ã¦¥ÒÈœio/›IW=ó®Õ§^®˜­sæˆ%S63�èîËïks|›œN¤(*ÕÀE°3ß™˜dgOVÀlÊh¸s |Oª\{EŠ�HœÅPôa %�IXe�øÂpO"†ƒ+ �3O¸6"‹ÂÒ Z™PÕµ7…Kp™88`_O^ Îô>cçÿ¦~`û/Ïb}i˜‘̪¤�Ö ËzvzzøQ{Û,Lkõtÿ2J‚flÉ[â8EÆK¨ $ «€?³¬,Ù@DlÇ©)²gåñoSû±ü´`ÐëñúA(4ãJ�Jã˜&€1{É'nHqQ›NÍõpCjÒ�A&ÍäO�úLû쪤 ?²:ñ€ç±Äýû Å)ê°:E}ÿòõþú%u¾©½/Á…ú 6ÊzgŇû âfËúåzÌ–FèA¡¾«Ñ²Þ ®àŠe.€£¨z4[·Vq?� :òzþ!´(4A. AèÀŒ³àB¹«î··­È¶ÇFjh`m3¾*Iƒ¥Èœ!E³[)Y-cÚÜú ÜJýY‰…CGeãQsÿóHÇŸô¬(�Á宣“PÖw˜ ¯îŤJMÚ>PÇæ²T¸ð…Uó-AhnãÈKNÞ�•µ+žÊþWÉû�¯6²#øÃ+ûzîóÌÏ\ ¼ç�'qˆI¢ ÄÐJÄÿœî_‚ªŠ|gÇè ‡G�¯Å­ìÛfB`ë,mŸá‚ãÚÞåÔ"A™ é)èݽ“ùßæˆÑr¹Ôà,/kC°p¶‚Œæ£¶^WŒs2 ÝaFÛ³Ãp³·%ÔÚ‚õ˜‘¼\޲@Ùâç—è„—Å-è�ßå1«/8p&¿¬zÓÿ§W÷~¨Êé{5§�ê‹/�&à÷(öi|¢¹£µÿ�U±žVðþ1è ³[ü þõÐ%�ЇÖ�:´>P‚ç=qí©±àó@UàîÑïÝÃ&Ö加~–eÑnþÙPåC G|È*9ÕBF‘¹a˜;~–JÓ65°F€]­Ý è<æ`ŠLàY�ùÑR’+ç è6C™ô’‡³OPßé—%Ü´™[<­©HåÜâòÔ ~þ†õÔ—}[ÊJº,=ßø¯×íß7ÍE«¬…¡×õìxû[í'3ò²È9)~v¼=o?òÄ pý:¯3CÍÇà ¥rç…Ö(MA7*í®%Ë\~p«û¶ ï êU%{Q½ôj$Uœ˜~ïФÕ°iWŽòT6;½Z ÄBe 4¨�Ê÷ØÅã9_‡"K¿{²åEnd¨AÔ;çýŠÖÃPNs]"G¹,j«…‹rƒX¾@*ó ”ìÞLÕ~&GN7@ò§G}s ï“?ÙàÏŸ ®"dÀa«8çs#ªà¿Ãß[â g�žñý7’¹®ðúµW}qØ�4ÑWƒ40È>§‹<ßÃ"÷�8ÜZ¶wŒÿ‘°€ÌÊkúü—xÔ=¥R_Ø;ûãϨn:‚gù«r�7Â¥«…l“¼�ƒœ{¯h·uÔÿ½ˆF_•Ú('� 6¤³‹‰·eßUbR:RRÇg¨[M0ÃÄòÇ1“JC¨Â,0’¥'®©á×3Ó°Üf2å)²8r­�UªX‹_bÓÝœœ¶^Æ¥6÷Ú2[UüÑ_°=£>óGí,!n pû ê‘­ÒKNë[¼k%A‚¥Ž ûVø£¿UÍ8!Q™ÌLPÔ8Ï'Y¹rß|T€ÜŽÉ¤F¦0™™*Á³�Ñ‚ÏvÞÄ@Z/[gŽnËic+£ö½gú2×`´a19öÀ�hÞ²B©o=·¥Ä,昿{ª:äcÊ\ŒA¿d'#£Äª©ÚÖØï/ #D*©ÒÉ/¸8[o)6æäµ6Æö–÷ŒùØÜ/º`_/¬;[é“PîN\ûo‰ÁþÙéaÐÝŠN«Ú¬6Pر�ÏIÞqyØðýbm…MúƦ>9Ý¢+†B*)˜€UóÛ.Ÿ”ÿ±�…øð–PJZSKj’½µúª¤P~p>×OEo;ˆÜz  C( ^«+›vrÐ<÷L.ÃÒ=ÞËM�×ooª/i6pÌý– ·57o2óÒÀ˜}�µö|ÉÂBþzÕmc Ž!¹aÞ¢;WŠ YõɆЕ×L"¢Ök("ÞtK=±·~t­ÂýŒbr½«wmvô8MÄZ;ç\Š’ýÑqPkj?ºÿlÐÉAG¢î}eD�cFD#¶À¸´>±Ðí±ip4›kuV® Ž_ƒ%p±·O¼â�¹­R`]]£wÈâæ<²K ,§Ì0³…r4á‰~ÃüÎuÎø¸âù{saßá¿9ùÂvœØà7'_Ó\Ø5ÂècÓ8–¨Y.ME)Q:XÓcŸUµl-d‰™eMîÕ<$_y j®ŽVbí¯6N ñ Z£6äyßÎçm¹ï2�õ�J!*ûíkUšµ�Ô ]zµÝÅ;”ÝéÂîP7DÉžAU?Ž�L„”·]t;#–½ùïvˆòD(ÍL.JèQ(Ïw|j¬fçæ£xêa[îœëÇ.ül´—etßÔB2³`g p…Ìœ ø«…XÅ §ÈY˜g‚$E£�ÀUb¡+Æxêé01‹ÞÇ�Œ§¬æFh¿×¥,,6ÁWƒ (hm„çC–Ϲ‹¦ï¥“<=î+1[÷”˜õ,�EÆ—ç3.dD³ÂŒ˜#ÃVÏ,àÌ^*>¶×ê*Š®†ûÂì¸Ûc�·®+Ž´ÍäÜê®K;ãº2~7X)èÚâ(—­1,,Á ê @{;çõ‚ž/›fVs‰ ¨¬©$@«fÔEÊÆþÖbÓV’•K™ì|]¦«a(vÛ•«œJí¸$L¶Ò©R:†WKQ„YÈÕ® =ÝåÀöÇ–wRõB¼ $¹ù�««Îz™”>Cì¤1LñªX\Ëé;·.€¿EU ̘ 2À«¹™VÐbÆ.'š6³Þ›Bbä(]Ö(#VÒbq ¦dÜÀ`j¼Q…Yø1M7WM›  Øâ€žz€€C¬GgO?:9©ÍI®Bư²­ò£Ó“³üƒ|3ÊO>ØÉóÓ“sóE U��6##;Æ‹kI�›-Ö¬"ûüüäüô·¿ñì_Ì͈f ÎNþðÑÇEN£;ÂÊ,™TC‚0ì™×0ÕÚéíìjö²ˆÙxq–Jƒ9Î?OÚ±õ|µõßšÍK÷þ`ئd‘¹ùðÂI‚½¢7[•Y}œi[Rútü²{ó­Jv‚ÎkÈ<ϹæVT²&iCp,]£‰ëÌ5�þ5°!Ë¢ÑN›ëIjÍB àç�›"^AµoßÙK1L;cþ•4Xe»;Àw›_Óך²4šzð8�D×2#Ž+òßÛÜjGsä]8vÖºÝMáý–ZD‹Â“~e•z畹rZ´Þùn²?ËÒö¼\¤^~^·+¿¬,ÒC=Ù~“áz˜BÿîtãsÚ]Œ/â.Þšë+pØbÖa›ŒçO Ô:ïî…u�‹oñ§E|qU…ŽÑ»Œ/¾_îìÏ•Ë(1 û$yp8§²y§� ‡]yÛKC½Ó¾ú—-‹xÖžî°Îú<Ñ,4Y-"ëñýúC+RnÑ6±u rR¶8Ó¤¸ †b®Ô_ì_%«Há!²˜„ĉîFDŠ`.cÅøæM²ò�©ãXüÊ�ÉCTþ`E–ÙL‡yŽ&¿:¥ÄÐ:»KR.;ô( ¥Ä öÂXËúÃÇ¿‰möÚ)ƒ‰›U^@D„¶ˆä!éÙ˜6uÛ±(hËyfÑÀœ<)ÛÎ:¬ÑiV.óp÷BG~©ä„HÖ¯Û Ò¸‹ºl6ŒB‡Õ‚r©°*•A7k¬mN%wA/oü©Ñþj†%Ûö/è?Üê6˜­�Î7ǧæ§Hg‘‰H‘¡Ð8PûäÙ6ø/÷fÏŒõÉX/\åp®‡ÂéM]’öGÖzšX¢ÔNQ-5*¬žÑÚ–ð©-UÎ 8J~´j Ë�âR£ÃèLé­ã¬¼v7½Åm`'ªµX¢¹‰°iÒ†ž�Ú‡™Á’’¸È2KR†À?¼Qó@ógâ‘™Ý\@ûmó~¨¤�„¤�Ÿr�ôIúU} ·�õù›ÃdýÛS-éÛ&¬(}»#Îo{Ç$íÿðz³Ïm [Ìn¥þÆ_Þ×à‘é½Þï8É66K!'o(E6–ìÕfòpï°P.¯‚ƓŲ§T…Tê~tVÄØ�o ˆ8Ïü®Ž„(A¶Y§*«±Ä³ÀŠ�le.:çb“´ =o¤ân‘ºÕhbpaAÊ‹—±ï«Q?¶Ðº0·Ä�l/vtõ=ëú¤VÑV*D«© ’ë „ië–Ï»¢õT2jì¬Hz¥Ôb®Ø’ÃP"þa‡ÐÕNã+y@ð:d‘HÄŠáé¢;@Ê�G Õ¡ k_³Ó÷‡¨äøíFóq>Ç·˜ôC×,{ó¯Ì�¹(×VQèÐ0º$œVuMë &ùv»4÷\w…i¦Ëü¾©‡­5à{w°MiáÃáÐÙ·×tk{³úÐÆ6µ®ºò)B‘¼J cc'—µÌ�bÚa‰9–¼* !Õðþn(Ÿ|:XÇ�œ1¼àÖßL‹Š-3:6pÇ`áo€;nÍ ä‘6ðëz®s€ R&¥F‚*Á“ãt¤b {�, …¸P�›3RFÆVC)s{›˜ôÜП„æÑìÖ¢¢³qH lIq �N9ü‚DÅbuUø*aäB V%°N7Úg°«�¨†n“ ëz†w&…@%KwÂéyÄdð7ñQÅÑò2üÊÕ%LH´§'§¾ÑÜaé�¦àϾå¢Ü7ј‰œ¡*§ò¢±¾àø²×šW bxäL¡ºMÎ;ö!Qx[Ǿ¯ìA ^P‡:t}à�º½ œn½)õ½ìÝ‹r£ÆÜ™D±^£ouRN‹ Œ·²åJvÙLêXOêš�"‰ ë» º<ìGÏø�;åü®šÅ�Iˆ°Q2~>L<þLKÇüþî¼¥» ßyM}UuT XI¿÷îüaWuvûõ h†ø4ô\M�ëüÇ—ÃhÈ aÇRí÷"¯Û ó�ÅÀuCÛ ×ê�H M�ÊhÂtW˜I”‚‚ Úïú ;67.»Í}¹Ó„ΜRt¡©År2+,ÑóϸäG„îÎŒ÷…NÑl�ÞyGÏû�¥5G§8™@MóE;-©À8Zéì ìÍ/‹äÞØ€ €Sƃ D¹±5<[ŠL©)2‡¦ß#ï9�Lk±lm -csÝ‚H¯=)M†&b8÷8Û•¢/åÞzN7©÷ä_Ùø\aÁs(ùéP“‘Žÿ|ùæK�¥Æèh²{D‰ó®*�UÙû׌´ÃÌV. àþe" 2 ŸßÛ$Œ»Ùk\;gœGÍO—Õ-,¬cÈJ¨Ëvö´b!�½ÀeÑ‘(}U’î¶y eéJ þoèÈ{]Í.Kyh  ‘ˆeçDŸû†6ð3`“™*"ä ¢³´aÍL3]Â7›¥? [µÍÐ÷6vÉ@4ñVBjQ ž­Í`0¢ÔÞ2ʳà?ƒÁÛ›òk´íÙ35‡-\½hè½”âŽtFUùW/ÈÞ.•CÖJÈôH0»€‰âÝö¦Þ;bŒÔµ–Yw©LÈ¥Yb6ì>8dåñop„v•øYÒ;­’ݯÚR|aðŒsP]¨74´Ë õ½õ§Ø8#°Dy’‡Ð!0rò‹lŒk%ù ¦oˆ£¦‚­ aCWó"‹°[;KCͺºDôpà~ Èdb`r³6>öY‚㸅 õ×—”�0k6èÐ*6°*±› 67m&µ�EarŒD™âTv¡䨿´c�N°A—×q¢”g„²(lîf ÿìÆU×+ùº\¼mzAãp!?O‚;'!áÁwXrŠF£Gh(�shÿξŒøÝ Ží \Pÿ8È °‰»Æý1b—Ó±û¥CòDå{Làüž<|*qî,Cáù¬HI¼Ì«œÚŒõâRùÖ �ôš~ñÕ²¸ÝËWã´ˆ¤@xÑsPäsƒDsU!˃¤ýrVÎÓŠµC9FÑNâ°X¥Ç¦†�ch;óƒÎ©z ÿ«YId�'Øu®wÀ>G¥¬¯Ôàâî²€;6³°+†Ûtœ<ÑÝ4¾�¾)º€C]ÙŠªÁýˆ*T©Ç`Tíáú,©©«âZr¦ð”Ó-£4³ïU µ#CI6K)¿ôð‰ÓEOð6ð=˜ˆV‹�°9ûŽ¯Î¾¬ê—ÑO_׳òÖÿ%óÞ:S¯8ý_Ó?‘ç�2ÈX‰�âÈrH 3ˆ#·pd•Ÿ~±BCêMg ç.7äX]ÖŽ×rIP6§—F. Cµ sÿ�òº™m¦ÕdÁ°`n?'UM-ETC¼-ïR14²Å‚÷èþ³ß{¸åVn¸Gê®ë lpÏú†ÛËCqécÛíå~\ú;Á[Ý©' –ôÏïûž¨ZûÑ®Z¸/Cpü;•{#Žïo_¾�ŸØS+V’3ëµ|û¦„Ý!ß:=pˆyM‚Hi4Ú`ÃŒŽË¦Íg#D�Ôk,žÜ¿þúà¡öÂèG8W ³U„R÷F“¤�àWã8„Ððã,Ö¾æÎ}Ž8*{(ÜÑph?ZÅH�Z1};(€4Ž%`Ð�1zíLJ“FuM9q°¼-ÐÇ Bƒ«RGNN† ²m±é@‚Ôm¤Ômâ".:L.š¼óEžY—4!_¹lÑ»<{Hðœ‰,³™• œËy:ú�¹è%^ûx!r‰Ç!->V >�jÚ²/£nøýŒ‡Pt”c¦˜ Ãàwû »ž¦É3ýÁrõ•ôÔ¸]ïKÈ•›^èI˜0K\oœeÈ„[‰yQ¥BpÄpX(ø3uø{ÓÕRÌÊs�õfµ¿Hgµg¡Kï(iþ‰jÓ½ì™ËâËb�CÎ9lÂg,Ú¢5´ª­k.Sîº]/Ýц—êî|^…’RµéU5@_Ò:c –—Ñ�þÜ„K©/>ôêõ€Þ÷(°»hSƒ¡j1ã=À±÷¼Š½î5¬]„ã#b×ð ñkÿg‘ŒØaFgNÜ n¬Kµ¿†…6¬Ev5dÿX×U šgg6NOÝ7�™‘ Ò–Ú– òyD†¼ $bP �’{Ûf¾ÀhŸæûý�ªzÚj�þ—QŠÑd¾_Þ&ýf Óp¤Æ'M2—Qdȶ'´L$ž8ê’5¨ÔFo[V5oÃŵí@¤LG#9.´˜ôLp8Îä±oT•DX&Ö­èdÌ£èjè3xwW‡—Púe ”�‡>Ë�>+×u³1’²ªÛ1"(pŒš¡³‚ˆ@t^Ñ£ê‹fáÙ¬8^™f‡”‡ZÞÿ±_¡·�jî‚‚‹ q¸2„…nµY›Ó@¯íß4¶&};í ÀÃøþQ•epÞÞNªÇkv œZ.ëë­\3Y¥]ãõU,«ÅÖ¹�}­Ö�ï:H¬+ë¶kš™«7è§�•'—'V;÷:Ec÷ >G>¦jykh¨’,ŠVkh�´��ˆÌH|“ÚØ%yâYP|Œ"oã¢wW¢|Bذ#ð‰8[%c÷•³¡…ÑvGEàðÓ¥“û£"ö¼'Þ»ÞXÆ6XÔà páð¸0ß1ªÀ¤ œÏÎXƒžÙ¤Ô¡�Ýí�8ôò• 7³u 5ZkïðJvAÁÀûèC È{|žYàôd—g™¨KKjI§¨õ‰-2íx6w†%$Võîa gï1a¸}Ht6G& LÈ‹Lؘ�í�Lxî\Zœ»UΨ0è¸\¥¬f¾¦/ŒÆ�Õ1 Hy¡¬ù»Ü˜Üiž†!yO†¶ð “ÞêFÒÛ503w ?ܤM>C&³ZS–ÒĈ6žé¢ZV�7UWÍòµ¾X^,(¼XΥƩµ –6«Uñ=´jƒ(…vÓ³Óó“�áÍeqif¸™•÷�1àl,ŽˆÛ1à ì}>HóŸW,‹^P_<òZo`‚¹b@ËþÉi²:F!þ5®°ßS^9;±:ݘÝë{™ Ì™_U{vî~å7•—JNÀ†ønO`µ6Cö¾Ù;…ÞºU$e-rÍ`Ÿ=Âþ‡pJ…t\ó].öíó„õ(mD᫟*6Áº$&”N?Ý@9BÔ“Aæ.&Õ¢ê¶dóÒÝ›‹ÉEì$akŠ×c¥F@ “ÅÌkˆÎîÄËq«C’k^º;ø^{öëzÒ®.äòzöÉjmí\åm�ž ¥,tÉÄt°‹F0¨µÜžlÖ¢Ë ÷Å¢I .z÷`I„ü(Þœ�¯ú"ÿ š–dÿóÿ©2œ¼ï?Z½Ä+•¿;>Û3Q?€QèXÓÙ;Ħ´®j¢w«Þâѹ¬@"IJ€÷vÁ‚]Ýn»ü;àMæçã8qÎ\t…¹&å± Ï4FóKê°±×Vg4Þº&Íæ]#Ôñ.Ÿo0 � æÊz§®‹?"q�)�Œ¯¢l‚�B…2ÿ, 0’œ&…0!šÈ¦™ÍZì5LKƒa#ß©Ú÷~¸îɶONN²<ɼ<±)®M�ùÏ@þ¾ç°X€F�·?ÏBÁYA K;¨ï¹HœÏcAâ!—¡áÌß””ÕÎHöL¼IPËv †ü�¾c©,Hg ŽÅ Y»:9w�¨Æ3n][$¢É*ýÓ]¹4;nÈHœ™7Ðx@o¯ÊhÕ’'ŒÜC!Ÿ¼¤ä ª”åW_ìk%j$8b6=H�Ø#ÀÆÏeUo̾©¦Ä A/ªwæ_5Ílgƒ­ˆêȹýŠQpœM¯àÌ1D (Ê>®8‹ÍÌö‡ÄAT £@™^@¹ÃòèóÍ+á¯9IµÅU ‚~‚ƒpéímº/ f™ˆ¬ ÆÃDwÞ¸4ô90 B­°s§�O•r¡Í>Àþ‹Ë#ïŒÍÞwðy§/|òÈ‚˜ ´ÝŽzÚ»žHs÷< î÷�ŸúeÜniyšFíÆçÒKãÖeœxòÕŸåÍŸCTïþwVæPÉk+�Þ­_aè�@=ZÖk¿R1}q�˜mi '–ùÇ‡ŠšÖÏáXowÂþëëÁ=LÝážþzÃ#x–…©ZlÅV•”P9¨ú›f‰‚3ão¨:�Œ-¢Ý1‹Öۨ׎ øfâ±îrû¬pÍ…E·9‰øXSDfcÑ=AfK,9]8ìfƒØÐÊ\áëéÚ¢¹~‰é:Ä£<&k.=dÇxé�TScr»]5u³YK‡x~âPÇŒb_ÌÐÃÅ£ ]pSñfv �>ÉjöîPЖ9u¹ºH$DÒ§²~`orEí£æs5MM¹=­Töroaç�‘ëß0hÊ+ŽO âÔ1ÉYÛj©%.ÐÜ*ú 7À‘ïÈ.OÖo¦Ñ­d©‘È´e�¼ÞÚYíY$ (ʶyÿB�…ñ~¶”ûªÌbxóÙ‚g‹ŒËÊS2F$²ú²E߃ÉÎmx¨’d€Wçm.nÑ_·5®—ŠfoCÌšL´¦Þs Yw€Pдžœk\n®ˆ§c Â0Š@–Kv¼c�þìç\¨BØNüÅÚg혥[�Èqž‚pÞãUr¶Ž§š­ðH�ªñßFÊKЇå¿ìNF½ý�zck¹¦w6óeº÷lïOGÖc.@vEs$-…*ÏéFR~TMŽœL�¿ËoÌÍÒz,&kf´¾‚Ašé²iö¤Ø 3*rD¨áÔMÌÐÆ mAÈ—…û·\œÍæòŠÐä6ˆÊ—Él^º0ÏW�™Q8Â8£®NÝeFñ@g6¨F\©È“Âá5erí–Ñ�‚§ £šªÝî]áë’�jf0-•~E3/Æï4„Ô�b,tôãªbß­¢¸Z•ƈ'ƵÚÃ9dlSãä¦FÜHD|™!¦HuY7k ÅYƒ¯+Çû!2—� ½ËÕ7¢ qöS£dƒ¤"��0Œ¦†Æ´8 [€E û…Ñ‚3<Ù9ïU–Ø!ÉÂÓ�Ú®�À¼ %?!ïœÙJ’º{ˆdbàÈ1dÁj�Xʃ¸YæÞ7ÚÀù£“ j›µ †uŸI¨rXOuU�€é]Q…ˆ5¡üy�‚’¤;¢²Qqù”O]®ºmº/vÕ_/º ~ú×— TÒy‡á ›Õ¥žqD˜p¡ô‘½¢ˆ|ÔÔ5 $±+ Hea°®@S�Á±¸¢jâZ<0[ df)ã_eH"x\Ê�€±uÛÒ¬5†ãÁ°™†x:âê#)Æ«í¤êrL¡Vµ¥·œ¦m ôº° }‚÷œ²�Öµ‰Ý/Yìˆ µ‹ãdž äÈ…f�}ªb9ÅÞ÷–pO•jW¡?þO‡Ø†­ ZV‹éþBѲ¿.áÙ�:(Ê^”~S“¸Õ­ÊAâÒÒy{…p5“­Ä7Ú »³ ñlû¥É :E;½MAMëQ’¼½”=L1ýV%‡ƒ”ý‡úÄÒôc/¯V3ÜtvêÑ«ÏF¬Çýù Fsý[\˜¹3Ì‹Iœ¤·}×…JÓÓgƒ&aþá«QŸÉ ­Xósj/É%�ûž¥ßbË“MåmOزB·¦dìÊdY� šA•±êœ¯yÐP‹Ecáñmuà�çëæŽµº{m2^�nOƤžØœ‚Wìj¡ŠIm¸*Kc¡ 5.D¤³r˜úuÖ§~¤*Y°0˜ÒàP–$þSŒ ÛϲY§H–‰—ˆN’E#ƒVÌ@ÎNOÓ—ïa:$Ý“’³°lüSùQ¶Õ^ì5è µ™:pÎ%Õ)jÌå„ò…Mˇ 'PÒvyVÿV.˜asçÁ‡_" RÕF®Eœé,7˜¾#Z7ÙÐo†5ÍI�‡Fé� “¾¸ûh—>é\)×Ë깬TY2ñçvªÄ²ªªœJ’¦#|P5ûƒ|;"Û(¥�È'Žû 5»¿ê¶+¿!þoÑ%,·ì\æá‘“ôìFxÁ©=ØZ{½¶Êª{p­üt=Ñ�…ŽÙÌ=ÈN\÷4å¹ÎâÝ)»Z@�‰̸ö²° ?fw‘e½ž³dénûp‰•PJ\Ù�|]W–4Õ±¨©ôJ\EYZÁ/¢N©–³’'T¿T«Ù ²A0H¯’쬂hFak±U�ø]¤²as¹˜@¥JOdïÒ…hÛ;š�¥7×t*ªT: 4x„f²oº¹,Sf’>ÐxUˆ…6ºs¥Æ·‚vñšãUáðl’k�è>7+/«mAAVÈ8œÞeÏ-èØ=bQ¯­oWÅÑò*fÑžJ¾®fd?dø áwœ)ÏÕ�/À›!)§$àÀÊ¢—•œØì~….&ìh˜Í™ä~Œ¨ÃñKiÊ-”¡—NûX3;û ¥7I©Ù V°Ï–/Ø:š…Î%Ï"_Ñ­“ñ¯Õ~=ÑǔȼhIý—†ß)i®b{ª5_2�/T&™‡€�‰rÊ¥ªÂõ4á(;DŒòŒþäÆ˜à}Ö›�mfá›.“qQX†�dD®*¡3p·°•”Ò–,Þ~‚†Ìp—9A4Q‰,ÎÜX|kiŠÓFÖ?=ÄÌHl±¾·Œh£WçˆÝxû|Àáy‰ Íã®[=œØ†’®5‡Bò[s<åý³VŒŒ„y%ôï¤ËT’H2µ¢/ÍDzÔqTØõ!qT«Jy§ [’>!)þpŸ&Õ™�iÊê2áƒGÓ_ü X¯í�–B:‡r�÷Yœ¨ˆúî�Þa«uH5(=¢ûT>åŒY]ÎþABÆ”ã§{ø‚÷²§]y¹"²@$‹ú"7Ùž©�ŸÂ×y¼‘•°2wÚܰ+ )ÑoöRãœzlm*ìÈFéåÙ3,OØî�›ú³†ØÓ—IÂT1j£•˜HÄ * Éþ•„&†EìýM`*'q7Vò¶†¤='ñBÕÒðÌ:e-ª™g �B#�˜«Ö6�F5µ•Cm,œ:¬AQbÍøŽÓ{¡ù0 ÷Ç×*yé¡öŠ£¢¼Z�DÜhmšã#3Mèš°@i{Çâl¨‘6 gHùea¯ý¢œåŽ%ö¹È•§×ù}'ø>g÷?ÄU[ÕX–ÙòÀc©˜ú¢Ì.-ÜË%(Ìej"…­k?fnzHXïÏ]KO†[o Çê¿Ýוò³Þ×ÍÊö¥·égëN_«ì/ùZÿWaã*¢Ü”Cb,€}ì]¡wU"[{a>uN >_ÑzŒ÷Šå.lW‹:Šk9˵k}ˆ|—ÅÛšéÆtK¨NÞiö£R&�{1ÔøÄkƒ²-—%Ƭ^U+²-½ ù�™ViÓ ¶C S9‰cˆkXd+*ènst­¬¡r|­lá��²ó°tR¨ ¼*ã <=ùÃÇý”w¶Àa^¡ˆ|÷E‚y»ÆÈYF·ü©]6 ¬‰3Ýù¿ CšÍÖê5`Œ ›´v"?½ßOý¿ê5Éž¸‡¨¹\š‡k]lŒ@®·•K:/X½™YÎ fV®ÊIr·¹æyþ®4ÌÉp̲»jfåbš¿4Zo9½ª«wÍ5 FÀ/À�·× Ol�nwÖaP›Ã¶Fë üˆ‚?IªñŒ …Ó÷Ëüµ Ì>#'ý¬4š©‘F<‰ž]>~.!£v¿?’ɶ› ˆ‘¦§GÏìcfó?‚“àR§õ®…q3~]pu¦ÅílZ׉NàÕ[¾»©¸¡±u¯€¯TT„ôÉÑ‹3�h™õ�ç‰ÜFë®huF,%=¢bÅD¶˜ô27º„ÑXhÇ9*ýFº@=©MvÛm?k7¶²¨Ã(*|˺QÕnã5¹òuJnzbX#rTFñ< •ðyPžÊ ¼‡” d¨Ý¬}攃ªE‹Í¦£b\*Iº�z^ã Í®ûÝD�ò¤{¨h»õçévǹ¶b•`‡uÁ®›Ê—„²gò”Là8+·†ž>==9{zvjþÖ|<ºÍOþšŸ�òó�ò°>6¼at‘ÿÐÀ@8\llއn´ [ØqH3$CœFr3�þÇöèldøËÙÓ#CÕF<ýÅt°5ÿ3¿œ?5ßÁ¯ðÏ_FO>‚n¦k£"Yª¼‚÷æ)úÚH©€â6Ù@8RµXô Ë f¬ðÑíØ §³Å„’ÔfšëüÖ-‹¦ÃÇÇùã�s|“þÙîÄdé§Sf�{‰Tž“ n,…þ©Y@1Ö/ÊÊö~St…aáù7¥á‹Û’*íý¹Øæ_�䯪ÅÏùÑùééo�@÷Üð"4¢™%âÈL-âP0"oŒ7ª°”‘]eré¾êÊg>Ù\„ðÙí³ó§¿ùíŽó‡SÍ<Œ”ðAo¦f3¡�¬Óï̘^篊é;ðG²�:5—è”?Žóï8Öæìäôø· #‹ÜqÕu«§OžL �ž¬�Í�Š€™ÍúòÉM9y²¢fÛ'Ò ýãd5›?zvŸ·aßò£· ×Ýå‚úÁù¶€³¼êø|sr6Œ‹'MÿáÚÖtò�|w˜÷BÀ<™Ç5ó° öôvw�×�¸ nÜò›êréó¥÷éóæÚ7Ý©õ…ïKLO%_‚–zMÈsùGÈX3^9@fu² v”�~c¨ž]~:;¦AðÜK‘êZ©áØÐ Ú#jkÓ úÃÛkE©„¶FÊÇCŠâÀ9ZÎC˜ÎQ v1ÌØV¨\Ìå¶&‡½âÄ/fA³¦[@bã;[Ö5Ä4L�0QU`‰åWª�{� ZºÕh%iH\dÎ7ÖS‘ÕÄǧ’²/JiÐöŸ…üy&FÊ9vv œIZ¾[ó—œ*éð"Ž¢0¡B¬JÓ"Ô‡å•Y'ÐqŠémN—ˆ¯ ’«+lˆîjÙoS÷¹Æ“�i°q#ÅÇÞÚÕäœvŸ>â?à(Í:9K$„Ú1ÐG{¸Ísæá™ÿðòºöãã ßñK õÖ¦ŸÊšúÚ¬j§ÅÚˆQÖ¶F=”ÓeÐUy xY”‰¥Œ-ÇÈI¸DšoÑá[ï,ÌH™zeéfÑ9Ï1…ýAåÌ3s¹äæØE#s` Yt¬‹ÉÂBëS¯õQŸí^a,¤à'Ï|°,§ÈßÖ:ݳ~˜‚ ¹×'fsg JHŒV‘Á[á�áÉ“S­Üb¼&óýK-Ë@9W YVÝ–6‰TWB“Sm‡�†óì›PIW®û¦÷rоâ>=õuÐ5 `4^ãð‰fò·¾|žÊ×°^O?À±ç7L 2BKMgåñï½ðïð[;�'³…!U‰°„¬ØÚ|lfãYÕ¾„BGÆ7t¹ŒÆA”¡QøÕž›x4–¨ ¿s'ÑxðfáMëð°}X¾,ƒÌI=úžÊgÑIà ¸òòγf™j$leÎAÍAÇ£Ž58£štñ ꉺ§jkíâÅ;|ò„>Yhn²+ˆ-Evþ#Ú|/=E »Ó�’`mW÷°ºŠ”«ûè™÷ñ}T7r”üÆvXZýQÅz ï]£vÔÈÌÔÖ P¸¢»¡§‡PÔp…0¿ýî³´Ðï&Ð+ø#ð3¾¥ŠÍÉà.1yÞPÞÓ:ú’yÝ3¡h¯™¿$„Ÿ,š*‰ N�ôîr0[ïÔ:D|GŒG àoFãDL ^‰¢Nòë,Z¢=üºO!Œ„V)@TRêPU%Û£«ÜEUÉRÀ.åäp­„cõî©•D:I¸Þ–t¹ú  ¢ÜD2õ¡zJ&ŠÊÓ=ÂwçdÖQ$kÆ‚x°}\Õ™ï®Ö*…G¨X“Y^7>]VxíëOQNâÜ#2³JAe²tú(5'\8¤Àômÿ¾·�Št;úšš`G ø/£Q¼Õ¥8]é„¶öKZR¿€ MJ™ìp…¾ªZ¬JäQw�YÝ]{™xJÈÉvÅ“Ý$`e ÖzÙÒrô˜í‘x EÁ¡à]Å ë”]T�or ‡7Åh�Šêú¦/„«¶ÂóÄ@x�2?­¦TêÇZm‚ê][vÇm·]”‰‹!S ¾ãÂÓ}ù%«~ñ*Mw%ÆÚÊC­1™C�ê—À~~º?€ýw¿¿Sû!E¡¡‡çià 4nVÙÛMà.;'H³¢n7PÆ+Ïl6ãú�l±—@±�ÅÝuºs¦ä¬Y/åâ!hdU­!#÷î (óOý¸Ôårž„ïGye¡;vÎæëô‰ÅIóõéP@†Ïm&›¶ûþ²ûjSï]K,gŒ�k|Íz˜<2Qo2Y—71ɤ¢ �#³ C(Ûèk(ÓPdôíÕfR®£g%®3ú¡ƒ¸íuj×nÊE;½²e•ÁX`¨yÒ •ð`¨Ú÷7 ñÚâ‹ìäko‰– ró*Ãnf„A»ò267¼€LŒùéÄW;”1Âó7`Ñe¶]•í�ñvS÷WYBè6x¸åÜXZL“ÅÐ2Ôþšôð.ÿfq·á�QS\—9¤›·Íâ«MP‡ùò‡$ÎP&èMW®\°ÔÈA†5ì%Û usvòP;z@ú”ýÓOáP½GÕ°`w‚‘½É�IÀTËúû¥²ÄÚ7j™;¤»�”~Skðq#ûMKŠF·èä—ènüšÄQ¥FŽ¡�L §|¥YjÚɬ¹Üä”ÕÔ Õèè1‹¬�ccñÔH¢•EMòGÔÈ ¶51:—Á¥Óu_±HáNûZXÄÞ�ù·è_ƒ²»@;âã�äëJŠQ²ÏqÆp¬J �‚A3µGæî ÂÇ÷P¤|µ×ºÉD é‘ä�q]—ÒÒ&/R+Z£VpíøÐWÈí*Û»ÛP ·Æb¼¢ÚáÕ9vv·”–Nò³£BÞ ›jÅ’:‰ž*Ø…ŽŸ‹Z·'¨\“`K�Î÷\Ìk½$œÀµ[—ÜP¼–Ö&� 7—äìXìrÓräTLFú¤*°¸Ü†@ù;Òp+�/ÕÞ~ìRþè›×L’Lk�¥èç÷ò�GŠŽ@Z@?Ê›>«ŠKTZ,Á$Q2!¸UšUjߺg%h3�äDÍŠ\˜dS›sI`où�°¦¡àü”̃žb¬ ]½²1L‚ÉԌ�H³0jd(„�Í쎱>ž˜GR‹d£«½„ä,(sx¡L·IáY¼oÜ*Õ QÝ ¬ØÔåÌú°c‰ë "Ç3Ba¾—äÐÙ5cÖ!¤ªLÆmåîT+Ú¦$æQ"JKaiW˜É1Rç¿€…âÊ�‹ÙßLoÚ •‘·GžpegíÆØC:g0Ì+È—™W·åLTfšWÖwb)Â�HÿMg(·;þÜÜ©æÛã×FÙ.×�2Ôs¼GKÒ.Ã:ØP+êÉ™–žöò‚ö¯çæl§Î–<ñiÜÄ€°Y«"�÷�á~V °À¢¬gL0²„ǶªžníOö eÆd¢¤¯Ü³Š�¯:´y„ƒ‡%ø4?Z™8ƒ1ÇÍçóÚ~'mÿõ|”?É�jó£yeÄÓd¨0* jº%"KŽ*D\¤i° kÊŸ´aâÇsþŸ ø¼ÙýÍŠ²Éæå�‹¡”+F1\AþqÝ�ÍèŒÒ'dÂsöÖl�=Tdøm3å¨è6t× ß‚{‡(ª:_-ŠiiÑt…ÓÿÒÑ Æ¡ Ç�…\!ëþ^$3êšã_j €Ù.S/Œ?­ÌÍ@ zì¿‚Åï|ÓÔ/ÐèŽW½ç÷è™ÿy@(�ìÊ#½Aýþ1ž—ò�ññ�M+ß9ÄMLz%Yäœö1×\k‹@ÀŒîä¶z�qý�%®ÇI±û®­}ãSN56ß,VWE0ü.9 Ž”ŒeASI›”Y5v v`©yð$î¾ñ‘º‘Ä ‹Á›OOó£3ѧôÂÄè8v=L4‹0»ƒú×iÌÖv¼L¬4ª]NjÚ6R«O; rbõëqTW”J¬®uß_4è-»9Ù®zP�!'üé„q¨`s7‘£Wxàñié�:P|`ÐNxË£yÓ±ìŠ�É�¦íõ� €ð#&1Ž»ó ÿ‹�Òçw¦ö‡á>µ»Gí�cªÝýD˜�³’) Ù©(ˆöÞÔ—FLÍÂ(i5 r�FuËî36gé°¶ ¸]„“ê“i×\–è�°ÊøU±˜£ÓH�.Ôâö–#º“L‘*) ÚÞ®Öå8A¬TG/}Ø÷RFB�ˆ»wÿÉdd‡‘³›ý›z ³’Ç Ã,‘¤ä=uC}íà2µì2Æ*„û¥NÀ Á°|eúJÄ\|_Q ìPœ¦âÅIÄ“`z¦rþ[†Ž¼|óå^¿oì=é§‘±ÅÜô¶ÕßPB}Sfß#ÇiGîœ ô õ·«ÞêP¿Óp\›µ†4ó ¦ÀAm¦Ÿ(o𾇋è&áMÎÍ'h¬¡;NòMµ8Çdµ"Tc†‚…~+Ëôdü9xÀ¤G€›1Ÿ—‡oÏs-â÷Ò¤oHda•Š v0Lë )n|8+· £áBsrúñh·ó6”IïãÙûüîÆ |eúâú4Ÿæ˜Ã·îò£ë‘Ïø,C¥w¯íuá•ó ¢u]€[Llí†}÷Úw�·Í>`ýíãL»Üäùj§Ž{ÄA‚×(Œ¡Kp‡G0[0»pNi3¹ZF*ÝHMv [’5l¸kQg»¾¥ÕbÆ…ýгÈgÕ;Ç…÷¦†\ã”2(虩ÒÍÉA3Lw5l»îªEIXPÑ¡Iœ—'l“±¢U¯W“´rËV«³¹âÌ5¼™1¿3Û9ÇMN�aœ¿3„63üf¤YRá3%×]Àü; 0ïÒÈÈþ£ÕbÓ¢kÎþn¯À õ1ú'éç:þ‘ÚùjÑÆAîË5ôÛESøÚÑÖ}Ã:ŸnüaqƒÞÇDýüÛ—¯ÞÛ¨ø›ÛvÚ¬}•wdï}±švßkï«eÛ[~ä=¯À~»À/}%îe…(SÏ-!œC~ý8•5CA=o¾~ùêÅ›¯f—!B˜Øª$¥DÍQ˜Ö*kª1¨vZ]­’8=�Še“±®!ºm>—"Æ B uبí܆s^¥Ñ¨75•²×ë×X­£Y跹ḥ„þÔÜ@L"WJþ è¼èLff¯€ÐÔØåþ5ÆÛOò")lF×yþÌ´E£h~L9‡|�pµ¼Åê©ú°—¹„¶Å¡^è—Ù;gsµûÚø°Y äê,Ê 2‚�„kèAdnж§Bá3‚#Å.+£þazK0�30£Ÿ�1õ%øéÜJ’J¯ ›*ëU³–L{Êq†•4ãwå;Ôj» Ò,%“0F âÙŸtµ¿RO:+Ë…N67mèÙ‚b7 Á‰&.‰YÑ£¶Å(IèÇa£}I£ ðÕ¢Öô¦Ñë_qƒL.}K [â\æJýˆp®»‚ºPÏ×—ã($¤œÌcÁw°Œ�£a‹$ú”Jï88äÒ‡¥µãËv¬è6XƒÄª¾Üµyö¯ÁöŸ°,Z—"öX˜‰ÂOС[Àeâ@¢Âé! 0££k(±¡ªšzX |œ/!J ®—òvZ–œŒ/ N17ª°}þ~W &»«sÿ� K+k¬ä{¶O“lSÏr7Rç¦YwW £EJÔ€(ªê¸bãæ$U'å ¡Z%‰~] ¦:"®V¼¥ø°L›bÔ»ù´fÓ –‡_w”ãåé_å–ènç%ÍôšÆûzÉH‚*ê,B»–/ë]¿ƒ–Ï�%\^úQ(²^@2‘ÔÏ5`,QӪƊW”ÖL wæ‡Ê¶Ñ1ßsÁzs ã;D3úC‹–IóŒžÎ{/¯`8Æ“rw¦¯òvµ òp*¯`˜¼µ ÍüVU;Òô,<›Ðޜމwð)Œ ÷µ·+Û“’áý4ñ¶´”G¸–…ÃÑ8?hO™ï�!5Ì�«Ô(gËéÓ�]km€Ïª¬|Ö»ô©}áZ³GߥkÖú;ö*©¨.+IGíhïêçtþŽ£3{=Q"ÝÓðZR�Ðjž|¯£¢paƒ8Ùa×p¹§·ç�‘7êì-‡µ»BÕ­*`†X·šèüë(Öó¬Ð8zrôábÖöwÿã¡Ý§y¶"�¨wWûdW]"WËEØtÆyˆä’)(600c¶ØÚbE˜ÂߘÂß…˜Âÿ]ÔßÛEýùw½ÞÃ�­‡ëÎ` ‹›® x2àP.j¸¶x°Cy¿¯\íx§)-UÜŠ¢_Í^›u]7m 5Bªy¡dM�“(‡قCÙ7P&î¶È��èÚ>`…Ö~(wL_-9@†„·ÁÌt31§ÛxN/n†W€gr0�JÎ ÝþxI4ö¢ë»ÆNš‰‡ý™µ1�µ+QÏáS[êEÏ (âË~ùùërÕŸ«þ1æªG„TyuZtrñ)7ÔKs%–�dµ%jUÒÞKOì�škÖ?Ù3g}î×òä\µÈÕÎpõÍ8盹U‰ºTRߟ »q¢+Ÿ¼Ô€Çx\Wͦ…:*†º (P/©2p2ÊÊ¥6Ç]e2¦œ¿ô´PUŸX¹Ãv9Ð{6 'ÀMKƒq:’Bô2Éé^ÀúHf¥ñtºƒÁ:[¬³ÉI‹fLKËÂ8OÜý‚Àž·øÖl¼çRq%e‡w–|¥mXûRÝh"vâŒäU¦V=•ÿÕf�ó÷€'¢¼øJ˜T‘¦$Kôiër,íÓ'OÊúä¦z÷\UP=óéÉ+2~ÿ„ÆïŸØøíUˆ»ßûÊMûG¤è9hžæÏ¡F�Ù�š|ÐpâŸá¤û,ü'YþùU¹l¨ð)×@íFf¹ñÂ,û ñló7Û¶+Íeqtö‡?|´»NLìÞåÂçÉWC|Þ¾…kþ{ébg¥�rgm:ÿ†!ü%]WJ‘‡<zå^àÒ¿á¥?k¼M;U2§œè8Z¨õCþAžù‡¹=L;�|2Ä3¬mÂ~¶žÃ~9‡ eaK9³[—]Ã÷~ðÞ׿k\¸§Cuž;™W ¨ºAe�=ÎE·ÏYÜ…µî¯b þ(n¨ÜÓŠ9Í’åŒøL,˜ˆÊ-«ÞTQØ‚¥®Ý¾Ž- Ö¦˜ßÈ0l*7!ÿУ²w?¤:ú!ÕQôªuÀ×(ÜÎ ÏÖÝ]¥K¦·áÁtWŒ;�ùQ[–ù¯žä¼ã�%CÙF÷�Ðuß9MÉÜ_Ï_¼úÓs/«ô¢ýñšíߣIjé< Ûµzjš6Ê-Š wÅZ¹³¨ÏÆ– fÉó�Þ;¬ûuO÷‘mD 3ÌhkèU¯ÿzî, J‡‡ß¾êýeÅçÚf>n‹pqñAgáòåöèëz9S”Ç"ãQZ f=m ˜žR\®«vƒ®úÌK±S„ÍS”ùÐz€¬9×Î7Å7¶veís;²”ص6—/0ÙI9o”G¡/ >]{ÿ(3fŒ?]®l±ów ö§í ”¶jz Q_zï›ö~šw%ÜÓá7wj«‹Ú꤭tt™žG(Xà„Š.ûãíÔ¥ÿc(7,½ONIØí ˆ§lww ùú§ ×áê¡|c¡NÞÓ,~lg>òõd~çak#à{YH~îÍêŽÔñ üåŒ$Ú9~È0•0uö;@þf@Q˜_àp÷Â=üHó³ùwÞè�ºî?nˆ°<"=ö�ÅÆ|4’Ž5¿”íðD×{Ü^n‚Èv/�Ë�bÌÖ¢,!PÑtD×VƒÉ¬ßÞA)xv³O=yÎæ£,=«‡Hç…&\;\iñ³ß¡ƒ#ª„ì•`,  ”«ô·È¸ÇÃv&)°ZY)$ê*ª^�IIYŠÇÎ]öyé¤ê#™oH~RÞvëB>�° £-áHšÏu©Éç$óL“*¨…öZ�­Ö*>¶=›$ÂzëYØå–óúé©Ùùr[¦ÃV±rù™ÊÔzžO7m×,©Ï,1 __”wéR‚iŤ�rAx&„ôáÃZ/N»êl'厱�¹ÍKP_Pu­ªm¥Í^p CzІÐošS„Ú!MËúW¤µ^jÇ+Œ’Ó€5ܦY팳qB†râí“ûþèéøLEA³-Û!Ê`J5Àå‘ñ»§‰sÛ¼©h0 sî§("µGÓKZ{„šÂΉDúSœp}{õÄ껲Ô]uÄfÓü5“Z¯‹Ø¥?+ŒŠ˜%ÐÃG¯?ûj”¿+ Ep(�DS*“žÞuÙ‰}Ü“TiŸ8=9=S|k}Ù2´+aÈ@½¹„¼ÈT›vãˆ"âTQ—zMjÆm² iqäìÌ ê8DtÍÉí²Va ¥�³€3óÛ P£¹bu<}7¸’m�¡±Ó½ãvÃÆŠŽAd]žŸð-�ŽëŸËuÃAK°5…«/5+¯ù$CœdíCon`¥0>ßy …�ÿ¸�ðF2<Õ—˜ pþ©Yù ˆ(Gk‹# C÷³Hdf^1ÜÅ«’XÊ5 kf'ëËî SJx[xH®PpykN×ñì¯çù“^áµçbŸÑ5‹·j+evqRv7%à}Ý4Åõ êDrÐÝM5-UÀA•¨1Ç].8’!¤tÿ|0çâ¯çŠY›â[L)¦^Ü,wÓ¸*x…=«#R‰3<&ôL¦Y8vó)ï/ÛJ ¿gV‚SGãÚ†œ¶ ½ÕTT¬-LôlÅí:‹¦ªë‚Ùª÷‘ß”P+Ô9ô±F�:útWÐMm#"—½ÛhæÕnÖÄ ð²©«.*¹c³`ëË…j…†Ê:qÐhÅ[Ä�]x{. húÀ â ca`{—ˆ]Vðî‘\f.èqx* aÔsÅXqA%M>ˆFª£¸?¢dNÊl^À[6V…ƒÊ5\Ó�¯K*Y‰œO!®óÙcF·.êwÈ�)ë¶k™“CÒLK%ͧ‹¦%"¨¸æ ­céƒV[<^.ˆXA «¶ÜÌA%ÙfN`£H•Paj³<®hÑ^‘YP•½˜7U�bx Ⱥ`§Ùëër¾i!“y²T,¥§º0<¡Ä›Ëjí»L¨aÙ\–ˆ\$ ‘˜•Ë&×J¦5ƒƒ£¦¼-–«EyûóKÃýͼ~za$íÚZ~Í`Ö`¹¥wå-ÿñ½¡¿G¸ûôÕ‰ñÖîjv©ëãçK:5u"ïïóxã§w÷3d¯ÂÎn‚^Ÿgó°÷+Onç颸Ä3¼½ Ki9søž…òVdÀ‚Yd=ƒ…Õ�(©7¶€,‡8øÞ$(Ôr±©0‹QÜÅ·”Jцô®r¼a#.%†dÃÚÜG𹪭YŽytnme´‹41Y3^OA>¯všóÇ›ÙRægÛS©-“bg'ÿjl}Ocñ××ååÂè¿�9N^q‘‚uõ ÃøÙÂ*lfÍÕÄ@Rã]â<_£a>Do™‘Q�C¨`¸æòÃqý̨ºOžB”ÈB·�ð~#y¬6“EÕ^Á~sÛm_³Ç+®Uù‘5Ú÷)4 TðV„&ÃÚ ±¦¨ÖÚóï ói�òÿZ�·…r‹jBGfo,Ò2—¿i6 Bÿ°6²ªæFÍäJxž¤“5…°¤€:Y_ÛŠÈ!P;/T-—Ík+˜¡Óuë"�ÜÖ?XÃN÷–ao õ6­Wzº«Õ,=%v¨n9ñ_{ßêäu6Ô�#·ª¯x­Œ¸à}1 Fúô]ÆüM]Þv-ÖUW_^U`ÛÞqšüÉü¯X_ViÝùYþŠñ>¥á„Dñ®S#µ´VÇ$·¹(Š!zœ7r:7s8T)•‹‚ïº&ôî9…4°ÐOäk7p6­ißNòšc23–ê1¿¨h1N!¿²îË|’-þa)ã þ9³–ºçÕ©~ÞuÇÚ³ƒ›5€Þ«s먨[Ùr´aFò ÚÉ_þêÄ�q|Ì×”ûÇz,à:-¡¢­ì9ð:¨;äNf*`Sµ ¢ií•k8ÎŒzD¹ôˆÔ¬ÁÉã&8’Útµš YfÁ�oq¦^R5¯Øá� xǸ oЙ˜|í*4f44¯JsÌÌUÐU µÆæðM*ˆËj„�ñ×2‘hw®%O–ÙMÏõyBFŽŠØ}ÃÀ�AcK sãffVRó@Rä·÷žõž»Ì"År\ÀONÄ™;w?Ûw¾³{õ¯¦¹¸¯.™Q]ÕÈ¢XSÜÉ-4£‚F×5Óï7ß�-ÓówÀËä sÚ‹ÆC˃GÕÑ¡Dp§ÉU*e‡¤(…0 ‚¦Ì—å·Œ3VæEƒêÍ å-Žÿ¾ÆBرic™û  BIdT™LúZ!ÕÔôq l–€sç? ò=ËS¸hµ“™’WõõËüò¾‰ù�òKg¤‘n'/–é½Üžõ/,Dn�¾ãÒ3‰c*ësb €Û1p'/ªùYŒðœEºä< 0ב¨ŒJ�P²«ú¦c@$©h)Ö�ÀÇé°¿]uκ¾ù?ì'UÃrä~šÄþ@‰…Ñ•”g¿fßÀåÞ téÝÃEï…N„ tA�2á ·=Ú#‚çV@Ú+$ðQž,$PiËò$ðd¡»3Õ&@JZËÂZ D 1-4S{•ñ¥„My{k¼±ýU;ëVØoºE2íÅæµºNI€­/A| bÙE!ÝCãÜž—Óp‚š5‚UÛu¾Š?… ew‰ìu8ª25‰*O¥QY`¯ê—UᬠÕ)¥~Á×¼ƒ0DhÁò8z‘(€e[¨=éõ ½àHYv{jit ÝŸèñâ Ði?B£9šN7A7âÿÊUk–õè&œN¼¬¢�¾ Ë6òç2Œïϳ3ì«Y®7_ÍÜ<·Ál뜣Û¹çÍÝ?'²°h 9ï»ûùú“}âöcuX�uà�”äö.îCîJü>_T¾#Z^1žŠ°1£˜„p³¹¨ÖÜ%òгܜK¢\áÎ�’uâÆï¶§x€åRP°"_CÉâñ†§Á™½SŸ¸W�Ø}†Ñ—û°JÂÄEð?ÃAá1ÖW�2ZíŠAÀÔÀ;7_êap}�ÐÈM�ìŽKñÁ€f¾oP¤H‹èŽTp¡XZõñO!r²H�u¾ªŒàº Öâé„õ/úŽ©»É©/XõAÑf¹ÇÕU †Âžœ‚aƒT&™€*V]ÐM.΄!“Ýj±h¶A1¼ÜîòüÕÏpo:yÃ)îbªìIvý²Zp`|»Û\®kP±ÑÆOìÊDLÃOК“; Š^AR#§�œ¯f §’̱¤yÁY°@ì!8=¯·K,J±à<…À°7úÍP¦Ã(Øwô€0Ó}Ó8 çqÏ:¿Š_›Z�;òö¬d�de‡ØÒKK6ð;¤Ë¯` µVX™„y‚M7Oþ>˜Ñ3¿Æ¹u,² ‰ œ´™48¼Sg0v>~Ä)déO¡�9�LâQ:æÏs@qtó¾É.œ�÷G–»Ë-G?Ü¥¸ÄáÄžýÜìæm7¼¼}³!d÷ánµFÏA•½9ŽÂòç‡2E_&Eq5ç†:JÖ}L¹4ØW§Å£�%ëéïÊm»YÕªõxM2­ÄÜ]ž½”§Œq‚òýOÛMSråpÔ`¹õçÜú;g/X§&(÷¸ -á¡ú”À 10ó;…bÍV�Ñ� sß&ÕÄ»v‚øÿV@ÆÆX7‘~RhšŽ20Xî¡à\ •`sL‡DØ$Ñ5•~ö1ÒúB8e{Aù”®°m™ÃRaRúÝz]„€Å�€úÀýëÍ †Ogïn¶ûúz_ˆO–ôd^HKß—WXF•ç¤Ývwpʵ/šíƒò蟤g×ìËø.PUœ>ç^âé ó9„óåSÊ~äcþ ìSM´L$ä4 *ë;‹H šL‹²üY˜{1¢r€ôVµ¥ëÑ[ @FÒ¼�ч°ž¶¬^›#˜ºÅ²NX÷7\[¶•úßͬH´•¬Da¼!ÁÞIßÏ®ÏÇŒ7’ç*À�öÈSÃ_cËuLf?YÄór§ñ £§òGVn´<1 ;‹Cj´„¶j´âƒ‡üf÷¨�øLG€dÞcbq¤ŽF�•ñRÍ›(o‚hu0R`†Ƶ%Å—çQîÀ‡Rô¢>”�ä wÓ³1CZÍÜfèù~ˆ~îÙsôJ‹n¢¤&pL%5!²a¬»m²šcàÁ� ä«Ä{‰‡yîÞÏ�kX6“XÑH p8m7óÕ–�Çî¼½ÚR¤W–/í—0˜Ù¶ äCs”\NœÑ\÷^ĸʼ!‘¢Œ÷½˜¤*…„Ž¢úâÙÅà;bJ-T…»B®ï>í6)Œ„b<˜ì˜Q³þDïïÑ7àbŒ4ÑØ üÍ#yèÛ¦;uŸ§�ƒtÈL_ i�1D±×?„[Ã×Í@^˜À!|&Þà·.ÄÌ—T£¾=†´òLnsàrøbí0Bß”Aý¯‘hPÀLÊs“&fNìNïtÛNU$ïÙÄ`®(däÅ€ˆ ˆ ¬¶§û\BiQ€šÍØÚ¡a$,w o³Y|äG~0މ½ú8=âÚ£»,†Qçî2’‹v}³zÿaÂ8P"Ããy'©,°ò%g Q­HÑlð’ë(ªôù÷Áó]9Q4ÄêzT‚>G‘@P Á7[¬–Î�FÅ“tà‚¸õ“†È_0–ÇG¡—÷ªµùøAI#]Gs uéæ\U„æ:E<ÆMŽæ0ùÖ);¿³™û…rm)Y2hF $oýزÚXmv1�Ó9ÿ1TsPyäàÍ_€Ô.%õ­ )O–¼Ê+t‹œƒj½É¿Rä÷¿Ç«"�h©ížAŠYQÉ-!¯"ÆKÙt�”qý!sÜ|öÍ*ÐK”"ðb“‚™í˜áh4›±EM°ì=+Ûº²§à-¯r—¿SßãÔÔZZGîuTô¸¸ºG¨Å鱞>h’w¥=p-k¨Q-¤ñiÈÐ/.f¯ö˜‘Ázœ Ó¤'<†%3:?ŒhõüÐÒÛoç!´ê»Éú…–”t¤3Ü31uÈs((ÈIðu0qÁ窤úÙé×QEvªv: þÆoèSZ¶žª?ºIƲ�ÕlÍÀG„û”åë-OH00®Çràk¦±”ÏE-ëÅûøg'î^øBÌ8Xþ3­ñàÔ“È‹C)Ú;ͪGéGôm€›‚ÿ’"Z!t_|O5bо>Ž�ïê+ÚÁû’ œ{£®ÃÅjç®/° – pU‹ ßÙ�PÌõžaô8n…•-‡�صØ& ÙU^ñ¢×?-i¶ºÛYÏŠð“æ±ÜöÈ`•õÕG™o¤[&üP€¢3{hâ”ÃÉÓ9åh@ß´È×>:­G�&N«/Ùw§™Í._2µÒì¬ç…»Oï0Мõdî¶íCžfÄ›£{±þÐT…Ue¶íV°s­R™\n׫͊’³Ý)Ða¢I:º$653 ]né2˜j¦E3…%æ©ðDo½Vˆ|mµ0M§ ^E¾¬C·ÿÀ2ǰ¡Ò&�ÆšêïUJáïI°$ý*ï¥Âòuížòã1ê¢_X¡ ¶@? ¢$jVnAçÖÛAØz~ÁÒé6¿E‚—�öÁ¯{œv“5f]qE¯æ«ãf)GÁsÛ”1;þ‘$ Ò(W±+ÏÛ+§»¢³§Å(m�é‹�Ù U#:FqU¾ÊøÜñÕŽ³ f†<(ré¡óÈØ0�3±7ÉõmTyÿ±nÆÀUlšPµÀ¦ö0u¡ÏÞCßA�¢>(ú†ÖC M›ñ½ßcìHTc°jKÝ: \ì,Qô�zeE½F¯$r�¥@òFa�^ô5„Cü•q€‡OËÕrÛb*ÞÕˆ¥˜˜†B6TöxïwS§vš£Œ‹žIŽhíY + ”¬qïw1¿†§²6ÍÇþÄG‰#=6q/þšg‹*ByQÛ>¡’5¦u«µ–íÞ;5ç"öë\Å„lô¹²BÛÏaŒW«}x›b[è BÊ]y°ç€>„yÀ+ÈUàŠ» jÕc)ïR%…æú¢Þ»f»%oÏŽÄÿ'ÐÈÑÙwýsö³M&–%7¸‚v¬’Ê´u· –xk‘�)„ÊhXNg§ƒÀ©t�L-·;‚#0L�ßîCfЀ¦ÁÌ¡þ p7�¥� 3Þv†–Q,5*®kvÐÄ)È8”¦º¶ü° ÆDZ ¥Dü㬑þ�SôµÏ"•”ô‚P.4Ó¦jñ¶æ²p\Û |(930–r¾²»í€Bæ‚î”!(%’¢üî‡uðÐ>†Zl= :˜çzǮ͢|ÅÏyyYÄÍ,ýpV=t{\‚…õ "l‹$âŒ`c}êðXŽÂ—L¼/™x_2ñ¾dâýŸgâ½{÷ݧÍY;~y{ɾ~žùÛÛ>™;&ïãK› Dþa¹ Ìž�<6̺O[û²ÖŒ¢ª³}^M)‘ä72T2q«ŽÍؤ¢f¹Ún)³jÛ\�7Tr_äñüuœ—™Û·¸~ÑZiñʲ²²ˆ§Å2;£;ƒ½ZPêã5hÏûóÍp¸M3·ãŒ7#$ïCÇÎ ¼gjH6#øÝ¸’ŽAšg0+4ý¦UÛ¢ñb‹ðEïéãü³p²,ƒVC.”ör;Ɇ`Pke;là÷‘ʬٰ�‚9 0.`²yê5à ]‹ümà ¿|æ´@„ûíö¶rZäð§¯]µpn‚¯e t�”(¸Ï„†àÔM¡ì>ÊV¦̺¥Ùé�õÀçMÃN±Dþ§[ˆü¼hM‚ÈYºžÍðþ§A1oºæÎè <”Œ"a}\O¨Z\”m,š\šSYJ"¦ÅäX9¼iöçíÂGæÓ¶DéÔú7âzD€™Ã&µ󌟎2j¶²g\Ð4ž÷‰é£IÎh�(zÛìÐ�ŠVç�Ÿ�€0FntH²”Ü%mï„J\Ä=‚8Hü4�xû¸…ëð½ë¡§Ó,X |° <1¹ÍÁæZ lkn ²·ÞK†2±ÿi§Ç{úêr½gÚ¤hK|õ,û[O;?´Ûôóá{Þ|ËÉ‚¯¨*`gÈþfÚÁ„�¿?œ·‹ø^zVüÔ±‡ ‚›;„¶x˜>xœì½ywǵ/úŠ~º+aƒIÇD[ëÊSâÉÖ“t<Üs¯Ð ;ºtƒ$üÞ;ŸýÕžªv 4H*ñ¹'YIDÝ5îÚµÇßþäÿúâÛÏßþøêËüª[.žeŸÈ?e13ÿ,ˮȧWź-»Omºùñï=1ßwU·(Ÿ½./×eÛVMýÉú&ûä ¿9if[hçìYÛ]ÕvÕ´}šë7ÌOÙ'E^ËòÓGÓævu5¯ºGÏ>yRÀ{çÏä+óä¹ùf•OEÛ~úèêã|Þšç¦Í¬ôÿäºX?›|òþÉ?Í?™¨&&Ïò#üýú}LO¿¥O£Ož`kŸ¤gùWU—ùçÍm¾Z7«fÝú-ùUñs±žµùÚu¾4ï-N2zsV]Ët–íñÇ�¨1šJ@A2�!Djúë6ëºÍ»«2ÿ¤\Ú>ŠîêѳÕ'OÊå³ãÉöø,¿.§]³Îòfn:(çójZ•u×úäk~„†`v8úÌN;jûêèö§jœw#3⫟N�Ìåí*‡oç“öì^7ÓéÊYÞ5é�Öv =sÈ͇uu \­ËYsá±óúäE=ƒÆ³]­Ë2@;åµ™ÞU˲õ–ö$÷>šž·yU·�aeù¤4Ýììá\FzsÕ´e¾nnÚü²º6ïÅo"\wã¼íšÕ_hšUÝ•ksþÍ3XϦݬË1.Ú´ÙÔ]U_ÝM ‰åóf½›7LWÕôÊ ,o&-¼ $™ÿ­©˜,ÖUû.Ë ÓΛz±Í‹¹é"¯ÌÖc÷¸0çhƒqSyXÔÒ¤hËEU›5&rs׿çE9ïòMÝ®Êi5¯ÊÙS³¢´b]æ¥9±¦yC “mf–ê¶ZV?ä„ìVfH•™ÿ¢zgººjÛɬ1U7�™ñu³0+Zu'Š>ÛnÝÔ—Ï<’XnÚ_™6uW˜•2÷W³Ø,kØÿ¦.Û“Ožð{yþÖŒ‰ÞL:Êà-³D@$%¬t[ÕSØÿ­ûÁL¼˜´Ízb&d¶®ñV‰ɬísÕÀ¼9+;CŒ¥™àºF‚‹b†xS¬k³$c¤h ÷ÏåGd×SÍý5™™Q?»È ɯùõß7MwñÕºüû¦¬§[ú(ÌäªYÌ`3èÑoŠoä³u™å²¹†A–†òJ<Òæa½øðý™±c€!Îòb6«ˆåRÖÌ�iɌϒPsyìÈ(2 E�²mÓÍr³0‡èZv02lKaÏz35Ý—Ñðìª�ÊÝU…ïšû¡šmŠE«—Nõ€T•w7Í1ÓŠÇQæÕºµTFÓŸAõ´#¶–1_ƒîèµ¶48S/¸‰Çó- Éã[ØÊجˆ$ø0þT:&œ£°Ò2�úÕdö/©Geæ<|m¸Î#)Ü…fæØr~SuWpd£ñZ‡ópð<³êemN££Q8¬Ô p€u5ÙtÈD [GMiÞžÅwž¿—ðQ³W•é™×}Z¬×[a[ØÀfI\z²h¦ïÌó«UY.kÆT´fTx¬øAsgåšè¨, gÃw -ÌÖàœ�9775NWȬºù.Ã/Ãeþb›–l¦Ï»v¹À[ �èœÛÞÅÄÉ.†>�9ΛŢ¹Aþà‹­OB©ÕA3HdøÅtZ® á<�å ®˜,ÊcsV†7šÝ™¿óÈé13Áj~šàÚ�íßmÃo|µ64¾Þ™±[›ÿ]™ κ«O�ýþW�̼aÝ•þá÷çæ‡ïh&ðËxïIÇz 62óÙKîæí™~ {€åôn*>EfŸé²KݶpN¦ÍrµÁ;£4[eîq³§ÓbaH­�¿Ïv‰P$úœÐM7+çÅfa ¾eYÔá’ß”ÕåßK®”ÀŠw†™ë¨;s磥ûÑͦ»26�š®‹ër]\–žc~!1ß¿È Y±‡r*�ÎqâÜæºdFÁWq³®.«O®0±)”¬†9wæ1ÃØ´fŠÐŽ92ë­ôÇ å‰0³ʼnY¹2G:Žcæiv媩¦FLCùÊã ´œ )í%¡Ï‘�™3¶Ÿ†žÃWMAV\ç§O´h»(ëKCÓ=bªáç7†tÊüÌÜ,k`V¾ä¸†M?¶¬”x�YÚ)º™ðœ·p{2Ñ�åú¾Õâ~ÓŠêÖñQÛ2©C(Mz= 4tEà‰º©�ëòÉ'S·špùz³œ”Ø–šMK X´‘á +*ÓY¤Ãs’éã.Ë ƒgÿ¶*ÛaÜeYšoÊ+s,8‹Jœïuæ!™w¶'f†Íû'ûˆÇ? Érn¤gÿz?`:Ÿ�› **0:Ĭ¥VF,Ç]ë5Íz÷šØ Ïÿ`C˜¡œœžå'OL_3_ñÝabß®�:†Ð¨“#‘Ýñ”@]ƒ›M2ÊÈdS™¡‚Æäl ÏQ"/Ó§‘“³ðÖ®Jù�ÇÁ…¬‡ù²¸ýÚôè sì“ÃÛfñCÌåõ_TíjQlïLo€Ý}œE BAÆHÚù—pPå[�ßÌQ²‚V ²áå7PŸa;ZTDe–ÖlÛ«¹åÔ°>óÍbÁdºÄÛÇÉËxç‰. ŒÆðG¾.AZu#¬á¦™hAÝœµ¥/°šM¿05Tœ¥•Hœ‰Q¢��[övæ JIð‡ËžeNÒ#šq@ë6f’¿,Qîü7†6¾‚‡²Üï~k~ùÜÈ¿°GûÅ9lHæ´¹žì$Kìà­¯K5f•ª Îb¥â'#À¤Üs.ì45"Ç Í­wî¾Ý-ÈÚÎÁ®1ƒSS®×`•cö¬ÇtH·?ë5:û?ÓÙwÊö8°m^;³Ž7£2¶Õ°±�înt|³ü}&Y–/x©zæ’©É2Êi»Þ#&Ø‘~ÞÜ¿©KíœU„fåõà®Ì³t0îÔ“9ûÝà®–$.‹;vÖN¯÷õÆÿe=kûÇš5ª)Ê@¡„H¾¨Q3ÿ% ÍÈ%æ °þÖ�6ˆŽð6¸ C‘ði¾© GÝ#¦�En�YYtWÉÞÐûâùg¿^·æ§ 1Í<Å~ï§ XÇAžÎ’fÜ&´£²™Í3ËîY¤ºYówbæ³–ýN|cËà€ñj��œþæ£ßÑ*‡~lÔßœý†„$+lKË�.Æ3|óT­æ×u¿ë0\À‹¶6œlfæ0:È´c‚“E›N×g{‰NÈèl²é¢]Š—1³ ßa¥ä �–SÞ8ºˆ–0Iºg�&ö|€ñ€"rƒÚkÖäŸà€âY5ã·MTÔëÞ!Vóy¹&J ×l¦á&þïÅr2+Èñ;Êÿ7y̳»Ü’„Í~u[´Î¢Yi}]Îs|ÀxÍ)3DQuÊ•‚|Ý8°pš5•¤fèo¾\ãqŘ *ð`UÊÒ¤VfÌG‰ ª–[È´ ‰-6K´XV ¤)ØQz½#4,Ü?¸ÞùZkÓu6ÄvMŒ¤�Š�Yj¹¿$+&r#J¬@špŒ\ػҲ˜^�ÁìÙ´jÉ4…‘üЯKc“+¶èöS1l1EÈ Î¼#œð¿1GÃPCó4·×J–矹‘YæŸ>ú÷®2šÙ¥ˆíúlHz$%ˆ·çú…/ÊvjÔu`Ž?½qÚ¾]NgóGÏàÿã÷\p=;¯º…QíñŠê‘ӕj#¿ÌÏÓæVÅ}чE}-g �%M¼‡¨¯}=ußë óïþw;íëY|æÜç7atÙwÎO÷ nÒÃ¥± nš‹2Ž(³CzØ0¥»Ç'ÕF\WÓ`œÒ˜ƒ (¢àå6+õ–Ä^QÍäo¦©½AMw‹iJLbwPÓИ¦lHPÓž˜¦]TÑÛ40´)ÛÛtPhS–ŒmÊïÓdùþ8S:ÎUqÍqwŠjõ]V(ŠøÙ}Ä1Aôs$KÑ$6à2“å8liÓÊÊ¢½�d–èB4&¶NÃ\gb‹ +Ý6QÀ9²ç ÷4Cs——æö6*¯ ؾ¶w#O43RΜJFÛî@�L? ÓéÂ,ãÚÉhã]þ~öRìÃø&Ãøî¿PÆùÇ=a¿ûxx6rpöpPïJö>Â0˜ e6 ÝÊr§8çp|{1Zž É Ã) srë^ÇÑ*ØáU_pE¶7ºbo)3M�kíWÄJ‰QvÙÔÅóØnþ4lÿ›d\ÍEç´×šõÚŸÇ~áý™R1ZZç³¥a"óT‰C®ÓV‰I&×éÏ¡ ·˜kKšíœaÇÌ–ò%²tQñ…ž–É %I4ÎWMé0ÛÔ:ÖZÿ`=íum˜›#�8M§Ä‹Ý¡œÁgåùl7ÐìZ©îÀå|¸½á]Ñ`fÅ&¯�UìNÜvÝ•Ïb-¡Â3@¢ÚÌP™BíÎ1øŠZbàÁqÃÁqoT�8�û¨ ™CöÛ]¬Ý"ºçňåtqjÂ1XPàgßå¾W&ù,õAô_\�¾gïêtÝ|f,z†Ë³(¯QLæ[³ÉQn &î|9væLy$T¢î/’Í%½J"&Êú Mô®Â5Q9.>ë!Õf…ÿÁ–\P!¾i V}n©y‘ãÅ´JK‘Ç'†äŒA7À1$¸Ï¿ÄíTNˆhÑ_ÓU˜‚Kÿ¤ao™°Ð≩¡ £+-ôÓ¸e¢àM œg*˜‰Wp6:°ä„K…ÌØ¦¯D•Fw¬¥1Z\vöÑå‹ß‹RHVU¨å�Zc ùÑí®ßuH„�ÿ¸øÞC 9K Þ{�aCô‚ë´æ2” ZÆ}Þôß•d5¨¼¼"Æy8.Ï�Ì`+6 A{|xlñ¹³¨úzæ·9¶¦póÁñ+r@¸0›ÀökvBÀXæY5+­B»™·2세³~Ggï§Ë »ïE,û!)ߌÊ×¹ÍÎQÙÙ,“ûítÄïì»mH�>aD~ 6Ú“�ô5:xãWlÕøÛÌVYŽÆ—çy²¼®@˜›$½k’\uØcщ�£‹üð^}ùüaç Ûò �€ÅpŠ N\ Ÿ,抅SÀŒiߊ[ÎË-�7=.xù7�¢Kê°wÿ®Î@Ò{ö–�Ï*Lxëw8ZÑÄ0Ò8j¸E‚Z¯J�÷¤()®c«­±Wÿ£>ërîȨ�C[}É¢Z·©±¢Q#¼ 1Ïæ‡;„$ÓTW+ Õ’þ‘íL¤E�üµhÙÕªF»ÞbLv6¶×Dä„ Øîªµï�t1â@ìq†ý ‰†0«§l#‰)$¥‚µÁ¸Z§36ÏN¯u“‘5þÀ]Šx�)PΗ¨!]¨/_6ƒ²Ý¬_cLJoamWèe¨†Ã±éíØ¿­‘Ø¿Œü¶ãO0�‚Ì”nošÉlŸé;?øíÚžÑÆM³îûVÉ{©úr4¨£çÛˆ8’½½v2291Ä(ã;IÜi~Š•ø jÔ,>…2F=1àÍÙŸˆb§©bÎrÛ$›6ïpR;pdRÆêÃ,läxHlÛ¾äoìú¼3ÿgF^¹g2_˜Äµà´òµªÂ½Žú`83iámý5úf“ªÍBuC~t ¸ïÕ€ÌÙ)Q{ôYźÎ3ÔJ4·ÄÛ%È6r%ߢ²eKõ6ëÜPÖd"cGD£®)4n™ëg²*¢øóPï4·€þZ©Ô_|o¤g�Ô±¤$@´ë,¯'ñu±¡;Gfàþ·$£:^¨"ùÃÌ«»@žµ†“�rÄí’Æ½ÂØ5m¥0>@ž _öuÔNó¢97uå%þ«ãÆWE06ÅòÚïq¶&Ï�3̇&n«ÄtÙÇíó¦yÜ>¶Ž7 ¥|zï mã @©;5�7úºWËø–¾ïÄ0¾¥Ï;²‹wbuíiç³ÌÊ 3A · ¦ª½Má’kØîÝí~@¬ƒÊÛ[fk’I9NÅTÐà¶_ЭšC 熬•n–ŒßÅž}£'7²äEç.¦ÿ‚‹�D�‰‰l[a\ŸÜ$"Ö°k[:ÆØäŠ�Cè%ä7…Þâ�«Œ¢ Œv~‘^Ú(†v£§Á˜:Ú>¶€ÆtãÁ:\± W‡רÄPh¼¿¼î°òÛí²ñ_�´î‡fø–aº¯u±Â{µÝÚ‰&Á’_“4õ™<OD!”:RÑ�b>ý¢éSÿeXmOFXmOð•Æoÿ±Ÿ%÷Ĉ7¯„?ôI8Òmðu‹rx'± ‰ð•èU�êbÕ¡í¸Ðï2M[ÊÌyž·U˜<�m™&×iÕ¡üø;ºõ0T¸|{ç) {Ù!Ö"^É 6Ì·”įŠÓ¬þ¢@ÍW“xstÅ8…ŠbÃ’¸NA¶ÁH]âÂÜ þöî‡×Ö B'6›bDåF¥ï~%�¸V…¤ ¨pK!qÎG†Bñ•0ZšÅ$5ecIVb )È ÷"�™EdlP†NÕÂV6Îqrž1üÊrs?‘2%æ–ÀhK4VéJÁ¥¢z O‰�™3XJo3`Scfä}AgCƒöZ'R,gI88ÚÃ0ÆœU±œGáT>:$•t}¢�[4îŒâçó„øõ1®Ý7ðÖ2 )Ú-<Œþ{A:IØNdOe®PþŽ3I*~‰?ÎŽ£‘:,_‡/óÅä�ð?ùJðaDm‹HG“íÿ~4At'h ŽÉÓäôÆ,©ÚL«u2#óñ÷y×8ÞL5ö¤lõÌ ŸEwC…¹6)`x¯°'„\¸ê°RŠîYËȪïŒmœpF‚Q–?Šâ/öñ¢œŒ4´öÎ·ÃøúíÕ·Í´¹–•5"«c²�å8>ŒÓë4>øÖ£ø»2ì$á@¢e{% �<JŒ8×ÊÝÌWØS)Æžn? ñy”ƒSÐ]ĤêÔtµÀ�\îA§Uïz÷z{: � gŽðXÍ’�¯z?E„¢³O9Ì6çˆcàWÅ…¢�Õš‘;Mh¤ðÝP’á(,´vŒ\¡-.£š´¡Ð}.%aÔZ#Ê·‰St„Û%ÚæwÙÁíu±Í�<,ÁÁ/�“ Ø�óyÿ¾�‹éìZÂdQ J€×Èæ«Á2Æz)£28´j[Uœ�F2AäÔ«–Ôª&Øy*Z£èËäšõµq8,ïôlZlJ/[qBY~Ûcx`²Ó1ç’N�W¡ÄÈÌytÖ2 SO¯ðÒj$×yfIJñ:™Ï%T}šf]®!E.õ™Hc&z‚ü’y�*¨wWtÇz0$š¶Ù�„9«b;öòC2pde¡"J,y䯄w ï¾1vïPà{òHe9ïËPë·”…œ¹+&ò™?,Tö]�AðVó«sÁb³eÌ!.µ(ÌÎÆŒ>S–;y;S”¥TÉe²YÝk¸ó9%†]‘·-Ögsg�GÄ{4ÉupÊh—‹& \Ý,õ[R{ó%· ð{rÚd)º^AËÁݧh’Àwäpª‘Ñö˜}žNf¦nF•§é\€”t‘ ØÉ\½Vå˜ÀëÔ"X>ϽœëTVdADÓ›ÙΦ°Ôsr˜í •Td)>�·뗧q«›ñtÈߢ ù€ŸãÒŠ0IÈ‹´p¹];£VÜ niQûš‘÷/�í‘+‘ÔEÍfŠŒ‚¸ÉÏs â€Á×·Å`«Â†à ý™3‘6yN¦�š7Ý —5á¸1ÀÐ�¦kávp†SêççAoÙW ‰$é\’|Uï”ä>åËèRn»œ\m«É*ÕT£øû†Ô“NW³¶®’ÌhÔs³ ]Ͳó¬>¤³ ÉvJˆªÖê™O-([M“%ª±sþ¸1¥ºÖǰO›l‰*.žZJzÃ9ã»]“v+}QÔ©ÏSC" ò¦éއêe†Lrƒ—1ÎÜ+í'FKs³²Ø|“s[kïE¦}T�†R€'¡ü‡â¶Ä3 Š?ùWAŽA�Dzž[‚N€…6†5G"Œ â9Jެn¯/èå%ĽàË0ù[ºYÒT°‹1™ÐŒ ¨U-¾jsTäKÆ(fN ¯˜ IÇ»íÚ-¼ ”h.'þ54qóïŽ|süßFºPß]*Íö=~0t �Ëe²Õv¬ÅoÜׯtú ´£îöý{ ßþµÅ•+ÔØtç:"½©K×5tOnÝv÷ëÚÝÞÿݸw·÷{W.Þ ç»yóÀí€|…e,¼ø}…wB÷÷÷j/c|¾\®ãøèH/ƨ9²‚}—ý^E«“>Ûr·ÂÖQëÆM½…ã‡ÏÍJ˜ u‰°ÓR8BÞGpþÕÈD¯IöÁO½äìh_ø6²¡ ò-YnY˜¦«„@“#S“-Ó5¨/Hæj­Q»)„˜H¬Cµ/ÚYŒ êâE<¶²Dbf¡âÞê3Q»ž³H�‘aâ­ .ë#êÊ]iG²Ê8C91¤À-(‡+âçIÞÇp55›á:È™ûã±))áªêaºG‘ ÌJ s“I×6.^%D¸#Ô¦À�S 17˲:…Ë6mŠ #½Æ´ïdIzÉdö¥uâG…€@®¼Ö’l‚°ÄŸ=�àYŸ×Nqõ›72Àcxgëé l ÉJ$hÁêãÏ›R¬Ô3þëÌÝüñ813«C¹²càH§ô²¬¿$¡¦ru¤Â«:NEPu¢Nâ? ÉÆä x?æÿ9ñÇ÷½=…’êFÁ*l7™ª—^­ãƒCéáñuüèñ™ 2žÏ{–ö¤ci_“rO8|Á"?w²&JÆëÚ'�Xš£RW Êœb6vA5©è;=[ æ<Ã@ȧ(ÙCÒ|ƒÕ&l2æuä{°JiÖ=¡KÇú½×Êø0›ºz˜–µòBš+«êYÉ ?÷Ê�-•DA󜄂ÁŒ�t9Ô¥ \s��D´ 6·ó‰äA•Å&zPe¬|–#\„ {Y5³vLDвLƒ3@_•Íü]å‘s7x£ì÷Hï‹wÒ–¿l°UqóÙ"dÀ°|�.?¢Ž2qx&�SüòYlÄ7È"½ä[£pCCQ%R²0y1Ú•¢˜Tk²5ÀS]� õuºì öJ ‘ؤ9ê]'�t\ƒIcµGoäjp`.ŒÏþIHQ‡ð�s—zÐIý­CrBçó°V’R"�þæ4N:Në·;†=ø¸ã®TBò4“Tìt~ššùÄïñV£‘Øô ¸Ó2YyŒ#ß#Žåä Z^ð&|óêí·c¯â°-jã¶Êlºaõ[ì\„ïYžâéÁA=óôêN 6#SÁJ9¯“Ë^Ó¬Þ–"£z{Ö´@ðÚz⪫††ÞÙ^�‘#¶±`v– {`‹¯@0¶’ÈbK¼ÞU¶’o"?p^qIj` '¨aÑUݹcŠêh.êܤ¿4ãY‚Q‡°ÐÈ‹åF©¯ …¶d4 ÏÎ <%ól±€æˆe¤ÕÓ®‰kgר=M°’À¤ÉªÂj§•{ᆪ‚ñr)�Ö=Ò·¸9X®Šζ{±%¼‹–±ÁòŸ&k‚â[¢'‚7<)]-ÙF(”JNï#m2Ÿ üéY«È“i=Mõ?vœf¿Cwv ŸÙÃLW[Þ =¢p`x0xO¿Ò© "‚‘™ºé>LrâÛ^TÓ#ÉLV8KäåÅ_i­—tÎ&îŽ6¦ÃBói<ˈZ<#�ÂAý!é¤W3çK øGij¦…àø‡zƒx…S“þÊñõ�ˤœ[^W¦K:PÔE‰qðÙîD±$B³8K—h«¯)Ä·ƒý�½¿¯äè6/Lr«5ÙU·j²fî‘rwº!ˆbû¼9—¦*ÖU@…4ö‘3ïR^Õä¨ft‰ŽŠ3�Ñ7mJÝ!7‹¶³³Ö=n˜QÇ6Atã´*ë“s,&h•Ò®›°©Ó®¿x±z"�eL^òÅÃæB?ž/MÙG !�(FwK‰y‰&l¤+š%.šye.S¸œPX%5úã5@ã Íȹè|ô+‰ÖçôŠ•Ñ1È4„Þm�aBŒ‡”¦œ_Kœ•LîÍf¥ý׳¥‰@ 3Ÿ/}(Ò ßr0¼(àB�mGO «c ·ñ÷…fÑÈ%Ø­½Æ2”¿T)Žä뢔àzïÕEˆÀ RQ:LGUô¬c}œ´*0@µ£tR\ab9Ÿøw c pOªáµlºJ•Ü\UkŒô.‡*Í�² Š%•N~d•u^qM? r ‘O:̼VCf Ð Üþ£§›gc\ÉüÂåƒ Ûr¡f¸ ¼­8}‘—�ã&ÛÍ{ ælË�¨{s �VLÖ—ÃOlbS‹ÃirÊûǃÁ]Ûà±rµ‡…GD‹œ­[’ú\ˆí„DsPºÍz4 Ø�è–3܆k`Ý�¡±õâ™SÎlþíM5È$8Ä¡vAØšà°¦�±O ý;jØ¥ðÔÐSk‚ m>“fŒDKB‰]�Çüáç�lΚÓ3SÞÛ�.�›`÷ÈÒm=ú¨eæPë†,%ʤµ$�åJf²‡4£@°I}þ™®Öõ5ëÊЬM<á�Ūñ¡§vA¹“X¢y¢@R|žšî8FÏ•ÑÉ7 -ûòQg�%¤*R m ðˆÆaHÃãÍyŒ¹§m?ëo3‘0tˆîš�ÐvNù¤„¶weÒç˜ èe¾Á¶Š2é±°Núm5“.g¯¹÷›By+úv`8�~ìÞÕpní‘ǽõAÇÍ‚_b8õˆ“NŒ« :k$3à@u]ÆÊíp#‡ë†ácgju–[˜¥) Ó°Ezq~Òïéu³n‡ža¬FkŠ@-îºãÇMxïlÞ¯;vÆäàvå u§ÜnÍ�ýöÍ›¶­C¨�3wmS†ýÐbNOêœã¾œØCÅ€%iŶ¤¾Ø¥–ÏSö߸«†[{�\Áz7—Gú?>R^ñ:Ëß&hO²5Nlb¼µ°ÏÙiÕ´j�n\—�Œ¡¤l†ÕÕññÑ3:vÀ¤@<_ÂÚ’'–Æ>‰‡<®�¬Å ÅÛ4X-¥ú©¬ãƒ·cкZ4�e“öÉêâ*"�H<=Z4¡ãvzþô…ný§°Ç9»g6Ï!HÅ‹|.žËÁ›Žfoeu’¶hˆÎà¬Y"¡?ºc80ÁûÁ±h¦ª.;¬J�!z÷ݤs}De¶&ñÖ¢Gd$öäÔ+ɤåJ7y¡± 'Ô‰ÔM‚æ´«GOûm/ˆsÜ›å;nxô ÁÏ:ìÁ4°Ãâýœ wU;ç®+Ô@“O¤ða;'Í´`ì 6ÄÆ Â¡Ì ôõš�øñÚsoNΉ±F�ªÐ’º~Û9v-ÃÔN¤R�8ß >°WªÎ>Èe˜lC¨ ÝA’R’I�Îb7:Ç/0¿íª—"P7þᢠì�5c² ­a¼@-xhÎÑM'½ç®·§LÛÉ~Ö�¨³n‡·úaÇ$h¡Äñ° Àgùîè–i !C$ûȉö–­æOW{�ý›‚Wȼ޾ÉbÚϸȆÙà£e’Írj/´ì8p~œ‡H¢Â»Hƒ)ƒ€›Åiç \¬Ÿ_VD·×à7q+iþƒå# &³–{hb¹À¸3<½"úÃuúñ|å§o¿ý©mMý3ü ·VÛ2›&©y`¤°?¼Å`©:o"Ó?oGW¸eà VThG~ZP6—¬U¯uÐ3xdµŽ‰.$'­tÝ!n8C©:tÊYPRú+¦ &4 ÐÓ,çžP;Ųè>MW+AÞ�œa}™z8Ƨ·bº„(Û¿L“óȤ 0´ô!(dTíPôÐ_”û'¡ ¶«5æÑ3è¬ÎŒ 1+¾,±ìIÀ3h β©LÐwslÚM‚Ö¢¨)‚–ß!S�oáp¯¥à8ßÍjlK¯€%VÎVQDEA›”•µ¿`¼ž‡y­µSó 8ÝT’è8‚.p€1Dõ¹Pv‚Vɵ,]Šáá©xðaN5àG×\üP4ôCÑЖ)þ×S4ôC]ÁuoHXIä@îæÔAeòIGvž…ׄ¯.À¤ ЇFË®‘}ayVþÖa}¤¡2`>Öþëg‹‘DFh¤æôÃPMóê‰$èŠ|î‹¬Í IËÌ&š^Tæ¾ezVtXá 1†¦Wä,~Ú%Ùãÿ<ÁÿùþÏñSþåäéÕ“Fù9.‹ö>©YˆÓ‰@ô‚q˜ºp¡6J-SwnCê>ì]:ášF¯2²v†A…1P'ÇU}x’óUÿ¤œþ }D:Ü”“°f¯)üŠ©ÔÀ%VÒÓuVÑù'ågµ+û¬+‹“Tœg§='$  @1£Cìè vœ~wuÇé'.5#OMÍÂÔö¤Ø­@¿mp ñ ÀÖb‘!.™éÂëÉ c7Šð]t¬M¡B|øzÆÐxRóAYœŒµNJEqkÍŸ%—í7*ݵçÉÍØ˜eÖAÜ ¦àr}Z¼7òÓC:béáçf=ü4JÎÅ2Ñäüã�ʵ™û‰)oõv¯aå[{¿“Èò­½ÞáïW>:¶ýïÍÈmúûë¬~™/ þí¿†×Ñžlà�$¹ÖŠm¯;Ú7p]Ü)v½;å÷[™Ž0Uöòz4áv°zp¿FîÊqˆ]>Ä" •’è ë€âˆ¿ÞòXQ3b¨%�Þ5uè³²aÝ«‹¹O€/ç_qt »r‡u« 3šÆk¹¡gÎ[‹bP \TèÀfy´n2(¯¸Ðª'— 6Ä¢©²ŽW†¾B5†Z~™­8æ²ãºâžß!²`;;1YMçIëç/Š«ÐÝM>sÑ‘üþ†Ñ'p»þÛ`–4vläáìÚG·IxãÙsà³åç)†êMî¯I*d�Ž©�hYRüËQ|Z&óŒÐdkªÒå÷£.– �p„ZMhœ\á&Ô8»€Áío¾%73 ¥Èâ’KLz²[o-‰Õæ´³XÉjÈëF“ Su.Q.²\w[´‡ äî¥(Ú§�8p97–x=»ä¥oEöé>ÊÙ�¯(’««U~Òl•›Pˆä¶”vü¿·HryÞãòüæ2¶qO\ÐÕýJoƒ]ß�è6Øå%ºNožx~øFÒ™ßp:s³Ög¨êì.HåùŽ€íE�CºqB`ÔÊü� ¸K& Ž¶ä|Šž ÅpE£F¨ycžíh´`‹$X– …Š™tÂ5@½}nK†G³a H> 5Û'õ6R·€¦¶�<¤÷›éû�éæ_Ð!“÷m§ˆ¹@ñ‹erÊ®�„‹61&Zä͈̈́Êê¬Ø,që8‘–{›‹Å‘<jyWN�¯“½ ÔÎx'Ô�b\ÚCœb>5® œÈÛØµ--�%+„æñ¦<ëãÁb]¨ �!7ä‘Xó Ñ"J_ªQs)q‡p ÉX‹€\ò…Ï“ �ˆ’ùFfXc„?¡^òKH\kG¥qh½¸RÂ¥ cµYI®Å4Õ˜UâŠñ–È:aÔkRê Î@Ú²`Ü{bM&úU&¶�¿yã,= m’ÏÝFB›ttë mƒ@sê§ÁZÔ›e£’�½³ˆÂAª¸HyZ>œ¨³ß `5ÛØ³ ÷ì˹[ïß8ÿx=&ÑM¼}êzv(KQ|Q „–D®&Z³­yì� e/¨´qý,ëÈÜŠüe¨D'ýêŽ)ïó‘¾ô%‹Iç«�pYæV¨Ñ÷²î«y¾›•-Obá<ñÒ1œýtL3€,¿Ø£wø$>¸’!îÚeµJ®û{—ÖŠBâƒòD–>4‹C‰µ–—/ +,øœÅ'LÜc�ƒú|4nÀ·°=»­�=3ãú»bv�þäÓ­òpäžPU·úú~{­²ÓÞëî¹xÆUü»eý,>š€ðɆ¸‡§øp‡¯®W+ Pží6sèàïÓá-†Èž†ýÃáñŽCßãXœ´�EçÂýBï†{2—} Äùûl‹/NñM Ò\¢4WŸ­b…%’{‹–…!ìo{‘2AmŸ¤ŸNs6ÒDäˆrë„ Ÿ¬p5V) : 0W UsŒZocf®Û€*ì,¹@øöIò{ešÍΰ�Çjަ™Ò¯AZ¿/޼™�§TÌmk\çÙj³2|�,aÂ3)t¸L4DÍ�‘ý[I:•ãåý¨Í”,ýbØsÆ™‡!«$(6öeá�hÔ [l–\|©@2 ˆ¸õ@ܘ>Ǩ馄æùÜ–Ÿ£¾ºm湦«�ÞÚ„�k] šsLy1Â+ЖÌS W)’„ë±¹X_]%§ã³y$áü`Smõ•¬'4¯‡-÷Äë,?/.«óÌŸøTº#Zciã(BISËíþ5™†W»WÚ�É‹ñ+"ëq‚FÝÜõïã³^æ Õð9% {}q–N¯ggé…ëv´¡$é/�ä³·l�¾~ ¹”"¶¡ñ*Ž¿¯63X4ô%ºÑŠˆ[eÃÈ�´ô'Ø;eÆÛ¡ {¶*2{5v«ªT‡j þsÍ §6þtc`‚_Ã(MÀ-\š}ªF8k>Ôº3hfÒNè˄îyêæ§ ù$‘_uØÑðŒIŸRˆ´†‚"{¶ÌÈ¾Š¢ŸG{UÑöh€šæN´+d˜¬åÆ%c4½O®Orç8PÆ -b¨ÊÂE‘áV\0:),~®›)CÇ„hA„4¨NâŽ$ÂÆRÛh‹òÅ6·Pa™ªQ4¬he º¢Ékf….Œ¶¸ øj4«;d¯ä ˜W^L‘ûGo�ö5ÄvËÚµHé_ aœˆ¨ƒ{�f^~L;^A_J›ƒË¬m‡Äe‚EÎóœïg ‚J0DäƒZÑE[¬¬SíàÍ#mãÚß~0¼˜áDŠ}2ó¹Õµ‘4™�†d3§äsЬ 2òF©fÎ㌠çä«ÞÕæ=T€Há‚Ý“‹])¬uwÜÆ8ÀiÝîäŽÜ‹YV_O—Åì|p+^À[1½¶ë^ÏTÜn¿%”Ä*‚ûí�Ìþ>ÙK‚êì¿-Î5RvU¸ŠXàêÝ/}Ïm˜K&íØ«h×Íêà¨ÝÓñï™x"G†¤‹:®ƒÖ]ÃS��%ù)χ©LÜ둃'lLÜÜ–7!Sékú zegúÄS¼L†`B¨õï i ¥�ÃìCÛ™ ©š6"Çw‚€�ʤ$�LRò,‚‚�%æ -+«-l³s¤ïäKçÑ/“ü<²#¿Ó�7"Þ:Çýg~Ǭsˆ’ç3!0¯“SŽ�ò\¡â� >ƒ#Çó¯d6î¼Ã:þßüô›.Ù,£3®Œ#”øj÷(Þ ÿ ×b±ñwxÕƒl·‰c› ¯2%ÈèÃÈ?U´ �šì±qâœí2�…ÿ c'Q�Í÷Ò¤óµíÅÇa,y».Kâ€L|">·šâ&çš§– tt‚D ³7¯0I; <¡JrÓ¬.1MÓËðͰ 'æï0BÐïV}Ü0}0”îp5  ®‚VÏ]€¶R¬£šÎ³n¡OÉ:–ô|ò´3ââ*Hß“©Q5sÛ¢.ºmàø)82‰Œ¶¢¸x˜z¢G€K¥ëÚ 17#奚æ®ÈëÞ—R·=Ö²ãù‰�µ³¥� ‡¯‰§* �y¦YyÄD5’ŽŸ~!žy¤Û‹´6¬Ò�oQÚÔÔÁ*Žëøé¹XiŸïÖQä-Nýㄺ´’ðV+uËpÉ–œ~Ì»‡Œr{6½®‘1™ØX«÷ž'‹'á½j^g¾z-Xø’ëˆõ>9[’a‹kA#à�k¤ä²#ŸR<$M\¤ é…mÿ¦LVŽ žËĺ¢2¥A´6qNo!»”?L„ôŒ U0U‰ <Ò .cgZ‚jƒYäB§èÇ,µå®‘§’ Xö9ÞðŠ 8:Zþ%£ãlu7²'06�(RÄûªƒ‚µ)+IrZiz‚ÇćOr‹“T=>ÜoÿQŸ©—mô!äô"YV©[˜Ëì¬(¤öh#6w«w1¸�üèuË"#é �XCg8RƒDÞ;P�,BÜrÙá�àiÃ3ˆ‰y`ýtŠfÈZš~SÅú„¥GV›l·H͵ôªo=˜�Q«‚L¨±Ÿw# Xijc‰¾'Ž\&3Úøök�î×ø:íº…~RÊø §ðÄy1cÎTÛámjr…=4C˜�œ¡·Ã^ÝaÎB§ãšãàNÈñѤõ&A±Ž“CEˆŒÈ^•>Ë•ÇE8GÝõQÓeñ纼mßi¨ó$êX%3Ϋ³T1 Ì­xŒ°cò�,…E¡1!€y±©ì¶‡ÖITŽû÷õtû¸$4¬‡ž7z¹Œ˜-c%T‰�¿<  ¡÷Thvf ÍDN ÀtPD€å9üŽ£‹†:髨ËW¿ÉÄÎRÎ}ðxaQ6pÅH<ñÜ<ña©Ã®b±0'ãå¢ãåF5`‡ I ˆG»eúÎ(+­"¹n-8O¼VÿuB”§Kf“'fþ”Bb߬9û*•d(¼–ÙgÜ©ùdåF–òXe  Ò#6ò³‹µ¾ß@nâè,_ŠBŸ0Y{sz¾êˆg¤�ŠwÞ à${‡ R‘µi·�°û5a#jvê�]¼ÎÔ%yÏ/¹œp‘íG®sš¦s�¿V”]ÙeÒA\:ä·`eœW9@/qN‘i½%öýýhrü_¬6²X…‡Ã…�9O¬±?Òº¢ Í›mòzõí ô± ™¤žu£Ä2¼ÑðèzíBJQçùìÞ´Ø‘>h]#ø*©ëi^mJP”ÊSîÕf8�‡õ ~ j-Ç’Q|ÔIDoœ<…¼�F·©14°Œ\ñõ^ØàǪy²¡MÏ=íÑ„~#hÉ{‚{)Ï¥Ëf…_öJhhˆò810$½vW‡Ç¶3ÌŒE—Q¼•7D·B™„é1Ñ,Œ'Kÿft»dÙØê¤Lzéæ{ˆE»[WÞ÷QHˆ·yß m:²Ò�iGΤz©ÄèÝón3}2°ITxµ�b@‡ÏýœCvGù;óbÛ#Ú|ÈùÈ�##8*Ѩ³Ò„è9Ñ~'£9ö­XqcOÆ H¿7°Ú›7�¨joÞ´ ÕÞ¼Ù†§ÖœTWËÍwšÝ´¦Ù§ áüÓEØr7ÎPòÀÚLF~L„~nþÝÀu¸“Ÿó‡AÞ.ÀySt¸B½i3÷ƒöÖêí^ß¶ö~'˜o[{½Ã5þÅÁõv ®kN=n¥W¼”^Åu t½ßJ¯.æ/¥7ج[Aéý>=.Q È6^>X,/|koÕJ4/]ƒ7ßmnÏMÁw/Q\Î0Ë]µ§Dç\‚e�°N �Ÿ¹+(] ºK…ÁÐ<—ÕRàƒj—Vb¯a/­ A“À�æhÉ^À/ûûHwÚPe#-ÝÍö§H_‡Sª¢ÁuÈÒ˜ÿÂi¿ÅH¤ßоm¤ß€? ýÞé÷¶0yï?¸éWk0Xû!!0Ÿ6À|zcᛸÁ/èé^…¾ÁžïDàìñŽ0~µÏ›Cüæ Ú¸¾H®álß*¶o>ÝÚ׌äö1}GBú¶Â#¶ÃúÚÞe ¾v5¶„Cî‹êù0öÛBö…P�}ƒZ]�ò[ã~#ƒðÛ‘C6ä÷yp’øÖ\ª\ò›<£-Àô M­¯½Ö­3:h<~¡H¯hq®Bš¦Íp1Ì+‚DL¥æóZ¤ìÚA0 ž!£¾ÌÎÓevV`` ‰Ô‰lôJ†%例ùa2 .‰¸NËC³�4­,•1Ó Xí—àöXô«0h…–Gz�Ë fD: Dî˜úƉpÈbnžÌ{›6&yP�‰v€IΧP’ýKc�éwŠŠ›[ÐÖV=ZN†vé®Ü®¯î×LÆïKŠçR¸eØ[ž÷sÃÞ*êmxõÀÞŽƒ¼5Ñ‹;@ÞBC’±™¢g­1f š�8¢¨%pAt�K²†Ð%ßû/B× èrMJ{ÿh4¥"àölExÑddè”U‡ÀêÔN�ÿ�ë—œ§fÃ%çAÑ}bZ�{Ô"Hâ—xÕ) *}k#�u^¿HÞH©’ØÞ[8¨�P5òQ°‡ƒlò/“ˆ"òã•K×_8˨-RK Ìġ8¦=¯üK)lN§‹³“hŽ,9¥K•¤qÊÝB'ÝØ(Ú›áˆæzØ)-3cÅu"‹…üÔ˜u‹ç9aA³‘"!!KäŽ4Á]ú] ²?:*µ+Sèp>L™M78žQcϹhQm̘º0ü“Qâv¤-+Z´ûæÿn³–„ˆÄÑ0̾\u± oübJËöµºXyÃ×™ï¬i¬aòUøM]œ§˜J¾¡¸ñŽ—¼5 w8ÊÒH ‘.g‚%Äg TTa¦EŽ&1W~xAù<\T¬AcdL°�[üž’ŠÕŠS­‚Ø.UÀ» Ó÷DO™4Œ”½;£/L‹«¶©2]'y:;˳s“©ìãjÌo>µ(+¬ÀÐòQ´BHvçBÍšÔ4’‹0Åm_:~OW,Xº˜8WÌ\NS„XJç«£AE¿Ù:†Ý@¿##X ääñ)-7Ó¿âO[ç9¥ßúgÊÛÁ5¼5*aÖ.9a½qyæQÕãBrêšµõ :%–/&Î9½Yó^Hµ¾Ìo ß&àõÃâï…?B¿2Ë@ÃéˆyC•5�?50Yµö§€dÓ“�ŠAx?‚Œë“Ñ’KWÌ %�È,\˜Øœ‡ßÑð± »DÝ0>+5·Œuìnã·ÄM·FáØrô”Æûë�R}V›Ó3ŒS[ ºpó¢A硸— zëVè>×øÙŽƒeêÒÄe‚ì‹ÑE]]¢æ�éªîØæl=¼ÆœföTÇ>› =ÝWul×uSäð¢|ä˲! Â¬10¯¹Éx�nêazƒ6!²uùƒüÓ¦OÈ?ÙâÙIë¦k'­oîÛÁ6îɹtu¿Þ�Á®ïƽ3Øå]ùw´Ó›;xÞ¤›¸Æ›„Œòü–}t„Ëéâ´êðëØ‘Kʤ±˜v}¬Ppé¿"ò'"@hŠ‚G$þ¶Èߥ¸¡¾6àÂKøÈýp\v÷‹¯¿ygKp#®㲑á5ÂÝ ˆl"{VÈ•}Æ�?’ÞÀq—ñð"z©zð[Zš<8b8j‹ôÒÓ‰›*ê3´óºL\�Œ°útà ùöar𺠸 6 àµ.1¯âCŸmÖ…ôòRÇù WÛ‹y8ªêfŽ\&Uˆæ#ü‘ÿP >~ôŸ�Æò•odHïuvñãAò+µ ¬îï, 8‰s8d99ۮɞ/@C!Î ³§½�³,íO›„Š*‘ÕiEÙçEÛOÀS:N?ö'CxB´SNÍX¶;}7Kˆ4´×ò1ÉÝÎò DÌ�-ß+8Óc—8ò½nfÙ¢P ~ѵTŠÁ�VLã"2‹*].裶X°žÌ8âWnBõ±™ÿ¡)[ƒ*LÂ¥Qa¤Îî‹.@[w±èÏQ–aޏë}fÕWŽÖ™×¼©#¬8£*µ) éÞ^v¶±îüÕþæ> Þ4¹fASÜ-ZvÌc$a|G Mr²§‘ýUâ¨�5Ø� ‰AiAeœáý¦ívÊhRÙkµƒÚû„ò¾mù‰œÊ¬ÙK ›T5M¬‚xíeÕâ8ã )ñÅÉ)þ$�%dö)Ö×Ï"²Ö³V$xþ=OòyŠÑ&*à¿qZ–ƒ±:v—»åŒm:…"öˆnÑ…ÊÕ.>çx±—Áz¨6"œ_ؘ¿˜$ä@ðb<3 ™hÂéQ·È,ò¶éyÜò|™Uk HL¼¼™ašnÐÊIûtʼn�¾åqO¾lÞ ¬vä¨-Á8Ìͼñ4DzÌ$\F>F™Ìùµ"lК채4LüYp®²Ê¡È˜»cS4âg¢š'¼Š ؉t]x›/YqÜ„Èôž×©TéÄLéãq–¨à{W†Y±ªÝîÔer=‘ôkYL ã‰n=އ…Á[‹ã‘¢›Çñ ˆÔ7ŠHæ’0«qL×Ð_$Êl`{Ä872ïæx¹HáŠx\ ;7A òm`Üñ�®Pg* ßl¦oÃÇŒK\u`¶3ÿBÁˆŸÜ9ñƒËdºG�ðãF�pSdýáÃ-%Öí²üb�Þá“øàJ†p¸k—7/æÎÌ’k¹Gûs?7à[ØžÝVÈž™qý]qðÆA^ø“OóáÈ=ÁÔQ� ï·WPm†k-µz<<¦Ý¼Š·¬ŸÅG“øHþþ ÿpOñá ^]¯óy·Ñ˜C§àxŸoá0Dö4ìr§¡ïq,NÚÇ¢sẂgßû8_—©Æ á•Ée®5 äèWv;�%ËÅ! ޤ)4Ó½—ˆZçD¯·yW5á-5i3l3Š»8§Ê«|CZú½;êR¼»ôî‡EŒãü³ÀÝvž¨~*ëøàãøq¼HòfùC){0Mg ¤ÖÎT�V\.ÍöÎr�¿&]¨¶~•OãûfY”°JÛrÀ#jÀ€èøPåŠí°¥4?Mfç—õ¾&·±J¶g®25o¶˜mæ‡dœ%l87ûcSÜ£xºÉ@t¡›e Cö»"„WÅeZ»„N9#‚œ—’¾Ä”n&DÑy™ƒZš,³Sü MŠä\†œ_ˆËÚ`Ùø+± Ü0Æ™ âAï s¶/´"�íÃá`çêbÕv†_nìLmÜO°sØÕ½;w}'ÁÎÃ]ÞQ°³ëôæÁÎ’ÛÿÀ –À1ÝnÐ3 wÇ çpTÑà°ö ~¶)‚ŸéÁÏCð6k£ø6Ù |8Þn¸6ÝáÑÑ­„GÓ!¸axtÏaýâ¢a6ÿ£â¢ï ‡åWþûý!.ú×}oPH¿ì¸è;DJÚ+.ú.�’vŠ‹>óf$ZR´G4ô ºQt+áÐÁÝòÃkÚàò½ë,´_¶K6Dƽ*³ðºmeñ<öÀ¤Õ22aöÁPF©Q¥¹ÈN@Nøãx®ÍÕFñ:ˆy’ºMóQ3á|}S®JVXBpš‚`—Q5Ø9¹Û¥*1µé}H"6=ýx]î·ä‹¾ûáÅ å’K÷rx› - ˆÞ­±!­Ö:©Î%Â4ÉV•«I.VÙ‰à vº„‹;—�ç*Åí®ñqéz” Q´Böo‰’m–»ŠâèUoi¢ûŽ 2Q2JY·§.]Gx¨é”ŲRÆ´"rmƒ�‘ð1@‚*\ÒIÂj�^ÔY„1ߌÜÓ„Ù¹â$´¶˜F2\‡ù†jÒÚkê�ÿßÿ«.�ÍC’.òx“°ZÔ­¿ð…Í"]4Z7çâ¾o�Wõ°òe�ãº,.ˆQ¬Ðþ/éC||¬K£1_$4ÆžsD 2ÔÅfI,U¶eÖÜ` Çe-*Ýä0þ—7ÙÕ<õa\Ë,-µì[ûcCýHîŸ4, „×#(ÁÝI^ÀÁD¾_§¼æ$<ç°xËÚþ,ÇÛœn=¼žÑKINÊ)à8Vs). ‚àBãɉϲӳÃy¶B\ÒuŬ�5\gé#.q*ÕqU©‚Õ KÀ€Íư�%™‚ו,#›|Sm(†¼†Í�„A Õ‘?�[¶êͦww°}U":c´#p²-i”‘îÞ¶ÀE3tQU?”“TQH¹„:�³üÚ‡ºÑPOs¬ØôkK¸'­¾çHö*ÇéÕZŠ#úX'NM" Œ:…ü[*Û5Ëïµä5_M‘bœÆ®ó áúø’Mû¾ ÜÙ Ðík‰ë¹™)»@¶àk »L?¹ô¾�ŠÜ UfÔž¨·ª¾Çdœ?éªIWNm_gÕìy],¯Mª3€¸õ CüšpßÒCQTRM ¤ ^H!eÂŒ§¦^¶U_�¶,Œlìz0…±Ê¹›ßwxòw _eÑPuœHî ɼôk‡§å<œs_¸õxð³¬>ÔCzÊýyéÚ/Rú­Ó’u±ÔmtÿŽÿ®9ƒÀ•3²µà%×™½¬ ÞHŒõÙL£BZjv´4õ†Ï43‡æÃ ~HDƒ£IçÖVHJ9õ/§§Õöï¹�IGÓñßÿ½‰ã‹ÿþc¼õ&S@Sÿñ“kû-L ˆà5xyö°·Û7Á.¦`‹¾[,€z"ú1Mt§ [ë�>l¤!Mľ˜£Ä^÷O@åzæ!² U`¶_ Š *?'ó9e×xÀSµØ¶ù¦¶h`ɬ7 UqBΜè K*ÑŸ$—°Ñ@(%§ö¢¥ÈípØGmí÷Š©þª�èbi¦ÅUÄû…÷Ýé‚“"¢ä‡¢û¢¸zá>rqõô‚‡œ!ÃD!éÜ�çÁÈmnŠi‰�‘­¶|ÍZÏp¨¸ëÖŒGÆÄÍ«·©vo4aÖ—økŽ­kÁÞ†{ APýâNÒh„O0_{ÈýbÐIMi4z•Ë– ÀJ8A…ž½-�øMz\ÞÙó¯-ëŒäó’!}¹Ä—· #DMx„{'%Lr·nM8r»Ð…H¾`8ñcqvÞg ˆ, Œ%·8iïã ‚|$‘ÂÖv#h�)—øÉjcjKG}ä^ìsG¹*”;™s-õ«ä”4B…]“²ûn±:01¾èËN�ÚäY«tTÇãÚËdrŠÈLƒéi�õr´ñ²eÕëe[ª6Ù{�k¾‹dä-,ãvj²EQ>pŠrÔ±©£mš]»5 :�ܯ¿ˆ¹—½˜hkÜ)£<#ûú •*Y^&×C:SEÊMˆ 4<¯étÎ�–›)]µcT�J[¶.«uée†‚Ìàw ›ˆBñn{Ƙs.¨X#+ÅZp€ÉLXß\W<04•Ì�7üý! :5d_‰ù±¹˜ÊŒŠ;ä]LJœ~<Ö9y‹€¦ñ‘îIŸe‰ 0®ôó¤þMÉñ‘lS/žà«áµtÜ7Þa›e3¾‰Ê‹b”ö„U8üŽ]ƒÃqNôU쿊å«ßd¼-ÆE2\uá…- JNBŒy²ÖNÃ); hp]ûå¢ã嬲ÏÛá¢XæìlÓ7K(cϤ^t ,¦^-¶ùŽ"Ÿ"¨lÍ…"7+œ{š-A³æì…ESÈ‚ tHE‰:Hvô1|�ཱL°]Ù¶‡�𳋵¾ïö€ {w¥‹ëíÕšž»7-v¤OZ× ZàNœæÕ¦L½ù$ò¯b¦ ®U^‡8ˆÞë“Ôér,5­»c£œ·y[¥w§FG…eª# žoÐQ˦ÎuŠ%552ðX�‘žlhÓ½“”ÈDÉhí7‚–¼I�ç,9SΞKÀ¿ˆ½JhhˆòØ@’^»«ÃcÛª­£è’M ÛyCt+”ÙA˜î È`¶G´ùP*N OxT¢Qg%L �9÷Œh¿“Ñ{t['c[ì^Ù~ ÃvåøÁÏÍÌ> †lµžTWËÍwšÝ´¦Ù§$ƒüS!E¹Ðch»|®ÿÚáKqÌÑ·áb(w±.Ó´‘¼ˆ?Ý8{‘¹ŸôÅF_÷š¿¸¥ï;I`ÜÒçe0ú^ožÂˆ%—J4LÏ2ÊĶ9Ž#8B{§/R{còO9I¬s@‘†(62݂ԗ bÃÊÃ8v_'}s m]¬ô=­°ÕÓ:mçCiŠW#'ûd(Â3SÌ1ÈElåæõ�ï8 'Ì%ê(|y¯w˜Â2¦NçDSp¢æ¶9°…àZne5¾?³�c²æŠö} êÑÁ:˜dú|§ DCÇ;q5�JÀS{žmSakOZ“Ö6 ¦oõ˜>ØÙªXyò Jú²‡Ñžîd¥~ˆ+žÎj©¼#>_šÌ�÷ôÇøÇWLruâmÞlð># ¥+‰¦T´Z©nÊ‹š#ícÌ4h:kŠg&Çër>KÊùÞ³k2&�ua«˜ÂNFf+;™mwˆñï¸ïsù°¹�’ß$“I¡!ã×Jtr¦b‘�i“grˆÍï�CÜqL£®sJdÓYØ–vÕö³rœÖptj×`$Å¿°ï‘Rê`ê n-“™ÄGhƒ�ـƇgÓ³|žÁ*mœ?‰€�”ʘ01¤ †a Ë’Jð¥ýSçJLd‰©DÞ§Ó5ÃDAÜaSƒWÔÞoKaž";ï-wÄðlVˆ E¨ŠDç>j$zr>¸ƒb=0¬ÔŠ5Ù­„|é>O×5«÷mÄåj~/ü¥rŠ ‡¬Øá:UÍΓÀÆÍéɸ“§¸² ¨?WjÊ ¥‰æîŸÕCQ|Í5gŸ¨[+JrL¸,ÙÆöI+w[}]”æ¨÷æC`�&hÉž+ºîfÔRœ‰šUªõCz¸ÏSX„y­¿›åã˜Ú¸G³¤Ë4r‘L¯©gc!ù"È,Œ8…ØTR ¢x‚Õ»]ò͈o/`+Œ9‘Ö_'Wo6+]Ÿ©i2º1Ø¡Öt$lmJðZ¸&··=L3c31ÓÐ^ 4þõv›1U„�”ß·q¸]#3cvJ|'Šx‰ï‘1’…‰ïï6˜P°åõ§P!,œMÆOØ »kÍ !�„¢kE¨\“É«4Õ*̨~sùä™lM\v´¡,AËwÔX u¬-lµ&Ì�rðÑ|Ÿ2ŒÝ�í„c#XßRý¥`æÑo4çfô›#ëR.=�_X§šªú£Ò};@‡#ãŒíµ+(n׈·Qt�!�ÑmÄ»µB˜•ºø»oY…B¼õeš¼ÆG-�%k'—¡p@ŧ]� `è™ Ç2Â>¶ß³ç6êDœH±V#î¨(½7§¿8é‚‚È5ÛQg°äR{6ïèVÞw& iB™¦AsF“2r 2C¼^Ûaêx (œËáQ›Æ¬©dì´RùY†gÕö•¸˜à‹dŒ²#GÐÕAÑû¬›PúuFêì%\}©ŸÞ7Lލl§ÖCé™0à9;äfBê䦯Î9ËaÆ‹¡@.ÙɦW“·k !I†–)œÛÂi;»em4F÷–fsÛ㣃ÃHU¨ŽÁÒ!ÖtÇ`8�ôˆ!‘¦[p”½KÖZ%WñA0ÞI|ÿ!6 ìXиÉÃûAáÅÞ猪Ò^k`N…­Ø=ÌRM%¸¢¤OÓ4Y€ºr†,[‘pÍXnj Mþ0egÂå¬à- oü|f0Z)ŠìF†.úœS=ûD5ÍžYVEäÙ—ÄŽu2¨C£#—é‚ür$x�ÛŠq€7² [oLÍ3µß6S[Æk ‹Á²ª¡,·�Í4<&aéÇmÙ Qw:Ã^Ù ýÕ�-cT2C´S­Ç�“ ٠hïwwh|^NXÃô™ez…ÅTÖÐ'Çý´I1ÖX^¶Jj ¿3öo;ƒ�¾Êªº½ôèùr}–„Â]ÆÞ‰Ì7ô&ÇîSFüÒÒËäú™³ÁÓÐ\àfJ�¡n©ò» ~—u~¡¡µ£N†ÒÛRŽ ôñ¹Qœá’"Y�–ÞÇ@)S(|¨xO�ª;>øÇ)Àv\ã DlYâ¡ÕÉ ‰™¬×š˜Þ"Ð7ÊBô ãý%¦‚ÔN;}:Ϻrµ(v<Ë3»¡r€ptH�ž˜sGyq&(|âþ´üÐÈqoI�ì�ŽûÒ$•éF×5òL9‘±I{Öy$r�¤=Ï[LÖžš”½)]ômžãØL¾®¨”w8Ó§ˆ�¢öAéK¶Äf,/¢Ž;{µãˆ9®€s ¯õñÃ++Dë –ÌRÔ²|âÅwù-�2� 5óíúTFgr©ºH~Y!س"ú¤ŠžÌ5Á²¼§E1÷QlÜ1‹EÆRkÀ ·k ËFAž!¢ÄyÉæ²Ãnˆ Žnö®€ž÷ Óå’~ƒ—ŒDî¸îò­‹Ý1 ï€ä_¥+ Ô0€Çý|Ó ÓнòtõwŸÁRñ=N. ; '¯ÌÄ:•n,›œ‘ Yže¿˜QóãïOŽNþ”¨mܱ¹lôAU;S™‚†$á Žb˜ëÁZ¤ì»Q5]J!nêÃbq8"ãÄoO&OÍ¥¨=ýeŒH†¤†ÍÀƒý$O>±f{Ë”›š–tj_FQ“3×iމ:Xû�þdÕ¯T |q=[ö {Ç7š˼ 2¥.Ãõ C'Ì@)ó´ß5¿mq Ý­$Æ„š%¹¨ü…þh"l'bÓÓH“âéf‚êb²g®Z‘qGé Ž�4Ì#°ÉûdžI½>ÿèãE ÇßäK¬Œ?¢c 6<h52½v™÷õEËU߯�ã'c\ÍýüÖwª}�­v$õÞõ×È'±µ<;±‘èS ì€[WÕè¿g�mé<¶�§'ຠ^DƒóœÖ7ÛgÅ:>‘îšp÷öÈY7.á]OÝx#ò$òhN&ÃiÝÈ:Ï�iF{ÌsÏÍmϤ›?û•˜¸‰¶„)à–CÇòò·åžÍ¾ƒ7‰¢‘Ëe5OuŠêGø²$F(È8YÔNv±oŠ67n6[,“Cœa¤µ’�gz‹ÆÊ¨bl?cå-X%ûyœÃp`­ï؉¸c‹AîK‘c{”®èÄV§Å¶Óf] ]NÎôjÝFŠ-N­†ÔrhLÅÓ)s†n¼šuç„æD…f[äužÄ‹$[RÖ†X=8ÔÏ9\ª­Æo�C×ÄƯš$,_”Ÿ‚äg¿.�d}÷z37ÙÜ/àžµ©,¾š8�Úß“·äÒST·óñÏòDû$ÄTµ™°õ£©Œ™äΦéÄ«Q_d¹¢cH5_ï‹$(~¢–ê÷ DÛ]a0 vB‹wSÉr6¨¸*£û+²(µ‰åŸ}'µ8Ùux…a®~‡ÐX›ì8ºM¸Òë̸c§ˆOų(gç 8ñ9ßcXOÕÕævý Bdô"wx1zvï}4Ú=bô· m®ÙþJS)ù[ÿ7ÜîßÃŽ•Ó¤QÑÙÍ�RLÿÕ4¶SwàIÙÚÕÝ9Qvîúæþ“�»¼ ×É~»¹‚ô7iž–TNy?§‘ s=ƃož¿fœ:ïßíY±ó�IìVç5W)ÊÚ#©Øk™Øí»\&�ò¢�¬j†�â(¬Ú¨J fù¦ØT^úã"{§Jo«èJuÁï¶šk8ØË ¤ñ©àyhýG)yþÚWöNºÞV¶l×Xæ`m›¥_Ån¾­ì@�ÖÈêY§58Ú#@øÒdÒGsBS™„ó`‘wªÒ"gº²-Ñë�³’4’ˆ5MÖæ{a¶3¥V…S¾§)E;ˆÀ¦Š<ïH¬îOÅÝ1%6r9±7O‰�œX•5oœ± ;s²“YÝ o°¯.î7+ò N–ÞbQÞ_ˆeLš¦+D·wš&\gÃê£«Ø !Ù);ºL“sñ¹¢cB"?¥�)ÞÒ9Y¥:GT9òÔ¦W“�­ÒÕŽê-ôŒs@ª3ÎÑK(#ÊŸG (çàEÐÔœ}Ãø½®Ö›vƒ¡Í«¢¨Ï(Ò–Þ µ0…5ð‰Èñw³¹kzUs�-‡ýtÉȦêžú<Âè3î¯�†i‚δ8'± ‚y&åEÄõHÄ\þZ–.ÍOqülLJù)ò™‘‘�ë£ê¤ lj₊­“*)—Ð;­^X*Ž2�vHù}K8rƒ*°zµJÉ#œY±’ ¾‹SD ¦[ü”¹ÚBÛÚá)#9¸:žÀõ1‰=zÿÈërh’x×ì~Ü"ê-˜Eã2*Ãö«ˆÜ¡½ˆ¼bŒåL¾…à»gL±Ü¼ƒƒ=�“ÁôûÐÝÚZ¼�­+| '•ä 0¿¤Õ@'öG$eGŽþøÑ_%_ËimÔœ‹F?ëª5¬µ*UИ%¹ü\±bŽb—XI÷mZ®…]±æSzE>ˆ§]" þÏüŸ?áÿ?å_Nž^=1±«Š KIØ\ŠW'rà¯ÎÂYÚÞ"ŒO›C~E¨;#&èø¨û°÷k—‹†Î [¥1P'ÇU}x’3|RÎÿ„Žn ÞˆŠˆa±Kj³Ü¨�@ÀV±V§ˆwS�§ÚJɰ !ÁeT‘L·¢z{ZœØ&²Ù™GדÝe^ÑÇ©¸á)"øKñÝ ‡ÃÐ:Ð°Ãæ"�ëŽe·¢J…G©‰¬ +Á­w”-p«A6Êöĸ‡—yýš”%Ç´0Ç£†²œ¸RX #û£hZ-EOæ WŽF4ZIùÆwä(<]>4Û�Ïo$÷±ß ² o<ÕRIm<ÖÈU2�“#\¨#­ 'I›¯‹LÀXwóÚsáv­a�úá©W¯8«…n:Ò„÷¦WISH¼LµŽ) úÓu¿RXoÞ­Ë�‰<üŠv¹ÕW7Kó¹âÞ �È]ÎÎJìÉ×÷3ÕÒz®Ïð¤Ø­ð5°åâA�­­2âò<9ŸälåƒéX]jl¢�è|.(«‚Ë{®“Rƒ.›Ð¾g"��€¢%?+›†`æ 01�Yvwƒ)Â0Û¼7òC¿O@)wÂ"FžØó¼Hé–€›|pF*ô�§|FzNW¶µ2i AÅúÄ–0¼¤¼º€·‚ʉûïÑÀé bFÝÕ:ÑE¶ƒk*/NÌúhŸÑ%`¿äµ)p| _êDþöðaü‡ø#ä~,6(zv•v,‡×<ÇðÓe¸bØœŒË‰ÎR,Âû §gäñ°ÅÕŠÈilñã¡"“÷´ÏA]£ÆûÝtƒŠËZN¢�wøIk‡íG[wØÌ�çÜÞl#øÞþfÏ‹Å]lµ.S´}«çhJY²…Ì‹ÕÏt¾?Ùý|²×ùî™ñ=ñºèGñêÞõVÑŸ‚ÈüáJå,XÈ/Ƙo¸Ì�»‰=ì–îSßpœ>éÒb»&Þ«5„Ь°×9Ë-Áà+-Bäƒè�½l÷X® ¸A�sfù(;Ö!Ù¥ÄúÂy(ÔWgW¶¯¨Q:ý—eǼ§²8o6+tWoÓÒ)pcKQuÄS“ûŒÖœRH°äœ /ó¼u â^ÊL‰LÿÓÍü´UI¾™M^?ÅØŸ¬ã=A}e«uYÞ!‚ �åëLŠ>cÑa }2ºª•Y”—^PÜkYø – ª‰ÁJŸº;�ÛÁÚ°í›îbÔÀ"#¢f³‹ÉZuÁ Eñ±Ú˜W”ýês}Ð̶Y­÷›ßØ yïÌ�uNKŸ(ü5¦®x!ƒøÜÚ†?�äË3Yº€ñ¯‹²í‰êÙ‰Ž²&GlnSÐÃc¡¥:]Óg 96•f¥%µ¤ºm¸ ⬂öHÚ-ŸŸTŒ]RÍxñž%Ë…ëM à»èÜ�ç)c,´uÅxµû——îXÆ—#Lá†/4‘oÌ1O„o‰ÇŽÚK­XÉqñÇ\RþI1zDwM!Ápe5#/nCG2@HÕåé_“çûP€•ò‚O±C Ê:ÛNŽ–*ìää«ÎÙx8ri~HËiQ�(xØD0à yKõ˜5q¨vŸ3_l¬ Rj~>{ÞËϨœs­ÆJ�âøVO�µ˜�¤XçrZ®]‹€oÄ]Aã‚‘;`�ºƒa Ûcܬ:y<ŒÓý©/·ìÌf& r[4eÔNÙ6ÝÐQ K`Œ6Ú*p ‚Jö/§ (¦ øF4Õ—N ®Ly–÷r‡ÿ-„/Ó:¥Ã×z ÅZ,otݦHŒ›þ ¢�Þv5ûÕ³W·HVš¤Xôg€.W :Ð%Ø·æGD]%Øo!?¢K í7�ô¹FÈq?d'U“*žA 憕|+[òE¸âgµ©>ÑË@ö3ÌÖÅ ý\ËéJÃèéx³ŽåL›¦’Uá}�µÓ"Ʊ òÇXR°m6qˆ“”¹µd�¦Ü4dø¸³}�9ÑÒˆF†Ÿl2ü9ÙÌ_KØãƒÏ壿øçéÚ}ÿÞ¼nF¯oŒ§‚MÜ’JÐÓ½b¨ ö|'è)ƒ=ÞnŠöyóRHß$ Ô„p fˆ.kPS‚·3jÊéz·ÐÞV’Ë7o=×cw­˜ùºã?»ë"µÃuÃxUÅ�¾æˆ¦ÑE�ˆÑÙ`SµÚZrÈJhêÇï FÝZ×�–{Ÿàĵ Œ£ ËÞÛ¡ØÂJ_Ã=Þ+ø¼˜m8y(Ù9uÙªûéKo�íxÅxÜ(¾_&Ô@b,’ߦež.¿–¼²VÆÖI•U½Oßaˆnw�ÇÓõ×Õ»ìtÕY9 4hv€<™ddqZ3+­'ù¬/÷Ì�g·ä3‡aB5nlQjWªµQÒX %]g­§Œ¢HiQ�ô%’²¸ú‘‰``÷�næ·­‹ùíƒålMÒF:t¤u5Ù¿Íᵋ/-åÐ=NäݸëùÇß÷ÚÌýÜù­ÞîõÞßÚû�Üý[{½Ã5Þÿî-~ü=‡4ìP†ð�ÿÜ6©ƒ¿¹=¡CÚÛIðhßTßÊL„ ï!|D½ÉB·%|DÒÇÍ„�héC×} „Wìv$�ˆE�á}ÝI È ÖeþպʖFðp¼Ù¬¾BW9vû%\�ô�ª-"ЈÞÍ’e‡€ò*YMç�ÊÅÕ hŽtäºCJá×Fß®ýs›ƒ÷·=*òk˜•ÙTUt›RþT iþÅA°èÌ.ѯ$Ál h–É<£ 5a÷ù½pþrW�Ž�ÚnB…³ ÜØ,�.~`®ô€„"—qÉ/ÒÝÅÂÝ1 lFU§\9p»ç à)Á­{Çå$ÅÆG†׸E%l2­Mh£Ë*»‰ØÇkÙ”­¾‰¨}lûåµW"Þl‰µ-ñ/cÓ¾‘¤–Ò†ÿ÷°<§ïòœ~xCyN›¹y®ÕÛ½Ês[{¿ynk¯w¸Æ¿8y®kp=òœ¤hÜš'íÝPŒÓÓþAŒ+Æéºÿ²Å¸`_ïDŒó¸±Ð’æ¥Ú»byá;º‰hÖÜ“›ŠfZÓMCñ,°  Ð"Zói!‡øF ìÂÐ9rò£ƒV*Þ, ÊH¥pš7W ŽbÂÍ]�øÇ_Ç·¡ÈÍ"cGñ*˳Ã)yA9Ž «šã_8 í·)ö�ñmÊ� ~�o$ÞTz»=‰²_T;�5t É�yZ7„@øåÆ µq?â_ØÕ½Ê~Ã]߉à7Üåyñ\§7wãåé¦Äêoi}Y”ç·'ˆAƒ;zñ‘D­¡LâæÕGÂK#Gm¥'�’úä³.xÜVîü8Èž<�6ZP²�÷€ìéÇéÙº³‹Ì¨ 2A¹fÄ…€;Û�vú v\ª Añ·ƒ±?JÌþ�†.”åN¼iYpLa¶\.xΡ-ŒF�ah›!“–É5vÊp–,{Àe´R_³€bQôY^ßä0mØÊÞ‘eê *ÂÿR99NÆŒ/?oq°ŽœR�H`R€ C¦1Œ“eèÞuå˜-Ü¡;£‰·†9Ó¸É1vøÖ…¹«ò‰Ònµ™bRP߃ÏxÌ¿ ×褓_FÈ““í ²W7- EÛ•ÝŒÙû˜Ë¬i¨d#QlhB‰;ê<ÊquVl–¸yñ #ÈíÓóŽÔa æŽ<˜kâN“ ÈN¯…r\M)u-7‚2ål�Èè²±ûCíÒìkûñ'íX[†â—dGÒ]kuG`¥Zyó¼7«(J¶620BãøI‰ð•j5_�¤êbW_ÛŒ­�H+û�ίŽãÏ>‹O¼~àŠ¯}%9}�pZKƒ#tiª‚[yºœcla„‚ê%£‘ÀŸÍ‘}D�ólNí`jy^ŽüŒÏµD=Sûã‘ExÔ¤-V]4àŠ$mžómk²É€êMl¤aP7�1xÛ÷”æl Z�·£nä›>~2 ú§kÄ[Á¹û†-Xå -ÍZ¨n§Ó«8Òz`€»M��îÖÄMÀm&ww7Á¼uçIº‘ôw{È¡��cˆ§oGñï±eOí’¿BA K‡l£s}ÞL©à‹,²É9K‡3Û, 9ClZ…³ ØB΢bWò;§È«`ûˆý‚äå4ç>ÆNVl CK>n8—«œGDib&K¼Y LóŠÑ«~V4ôp½‹/›YG�ŽŽž �(Ò|$jcÂç%vÎô8Eœn.€fL…M† ê}@— Ìø”Cª;"ÃqåëD"åŒQ'Xç™�¢ÒmY°Ð®jÈ®:�ïHuaS;žnoë¤ý�xõÊΪ‰¸œ.N;ÍÈÛG]»ŒMX®êv‰1€Å…ÀЀL³LM=¦’'˜¹†¥R¯c)òs¸JWìî_óŽnPIs„ó—5×Dål5KeÙ3AôçR¯ÆyOâ¿ÖD×?)"ð[jð�hÄp2饧7ÖŸ%—Ÿ¯â¦bQ[Ñ›¹z…4JIÔ+dx[¯ ®ìÄGQÇšÇY¥˜ýZˆŽX¨F6²\c®É¼¸Ì 0&Û“JrÅÈ׸žDÈ”âŒÀIeïÆ’í•¿;784ç:VIîíÄÚ8e°3â0‚gˆÚpÍàÆé Æ? S@¹NÇqî9ÇÈ�é–¶‰û ·'bsgVÉU¶Ú¬ŒŽã8¤—Œú¨ìøèèä ¿˜ÈŠ@" Ât©DP™„ = 9V„`Óñ¸•ù2«Ö@?è\¾¹=SñðVu6Ⱦ’Ó}nblÜ’7Ý%ôæPï•6Ýã)D0¥;’›rØÙ«‰1Ó%®hpƒà¶Lrñ£ î6µÙvª¹ã}™\kÖ¿  –T·Psû¥Hž-ؽ½^Ošw´O¼®4ïhLžwo¿1@\®À3« ¢9S¬Õ‰–ÚäÒ$ó±I#ÌË!‘V¤x`´¥‰0_·fž[{ƒ­ ¿ƒ&�‚Úô±Íü©WÏé†7Œþ ëvÜåŒ:.ÂI×%ø�KçŸSžsGÒdõSYÇÇ�AìËÿ™!0yº‚§é,ÙP±r§6¢„E¨8…â²!L|ŸÇ-󮚞RÌ—E‰xq[rÀ#‰ÿHC~¨ƒÛÇL¸§^–ÓdvŽå¿h¤úw âEåè9æÍj^°a�ʼ¢ÇÐï«\»áª2 FrAPæ?ï7cñ�Zsqôœˆ±XXŸ·‡Äánð“ý›¼m| -dªFQçÖ(Æ­·ŽÙë „Þ�4ù†«Ý<´A>Áƒb}f»ã|¼#¿›å9±bøRÿ¹Ë·Â”økg‡Š0ª.šEÂà—GQ÷avu¯FÃ]ßI„Ñp—waä:½y„QµY#ÛPáeÅ”|{‘F0Ô#�Â}ˆ4úi´o¤‘Ë–—k Cš“K9Dœ!Ü2,f"Êš'»§dë瀺©.jq³k LÈl•ιÊ §öÜîW„b2Ù’ÔWo¦uKU¬ÂPqŒ’ Q¢ÓÏ�¹¥è.b”èìÞ0Fi|$Ò` R´C$ ûC$Ò‡H¤‘H"‘>D"}ˆDú‰ô3G"õG?ìSÔHéë·Ó6<¸ìYð†7'©Z¡Ű¾}Î~ÒÌUÕ]âÇññ“GúOÏÈàÐOá4Gq Ü ãm(´¦†…Òݲ8Ÿ28èò"[_à÷ˆl•ÇgNèKhpd�Eˆmòƒ QŠJ 4åÜ£G,Ä�\æN$Š‘áLäwÑ3…Y�¨ ¤‹Eöÿ·÷­Ím$W–ßóWTh#F’ Ê$eÏ:¨ ÅJê–GÛ-µ¦Ew�Ãa+ @�¬!€B£’ptìoß¼¯Ì›YY¨ êaóK·²òyóÜ×¹#4¨,ëkŠ;bh­àpË2˜­ëðä©[¶xGd4¸Ÿ�ÂÉ38{tÑÏ€ E?ýêkñ„·¢ÛZÃê‹SðBâfŽwé>ã¨yõê;>· õi˜ëÂA‘Œõ µÓyñ¬áŸXc�¸1 ׃GóÔi·C§×Á|vÙ~®ƒö“æ¿»A´ïn¥§ÆÎtŒä�åÃIӘϧ¦iú÷L®©`·Ðß‚¸&ØV€©Å�:P@B„¹¢›åÜX§§®ô×™‚Öû'?ÁôÓSLZå²D1Éìïž|›Õ �EL}ãµ�8š�Ý_9; §{M§›ì^ñ——|ôa‚ 7À׫¨ÊÝî"0Þlû!50H4mñEöèà×_×ÙAvùë¯?oJãÉGP@Ô6õøï¿¸¾6Ÿ‚Žì‘}Ì>(éZ™ÿ±¾l“‡^ŽÍ×ú1?© Pu–¼­fDìȳt‹mUµûMÓ»uÇ 9‹L☲£¿Q¹þ¶I÷q"¸‰TÁ€j4 Ó¤ZpC+þ‡ÞF•Ñyñ¡ÛÚÉÍ�’KVïA¸�½�göœ 7«"Ulãpk5ݧDÓ'Û“jÈ�õ-¹¤g6ZBò-”_”ÙBî«rÅEl¨ã^�⬲Dõ„{º‚?ƒ'XXëvÿÞÞûKÕÔµ #á5}µýöÅN‚ÃÐ ~ÊP ÞÎ:hˆ[;ÙÒûRá§ÎŒÍ㎌ÄXÏ\é�îq¬°è–Àe*í:ؤHòÝ›±‡Uü­ü¸g‡Suöÿ²kз®�=Ú#ÆLs�Q‰Àâ EJ5^�  8f}¡�j:åËñ�GrC¨ÔQXE£–±q�q™u{ÊËý‚/#ûáÅÔÊëD€ª–¥Œ¾ô;îÔç^_‰¼?Γbš®”m™d].\åj½bý–\UÕ¼I [½•¨º ø:ªû¹,À1Hçn5“^;•E¬=0> bVDçŽÍyìúÌ0}LUU´rî‚Ý·\™¹]h1êÑø"ºQŠ3øò`…i×ÕÌÅóÐzë�úÉSëö”í%ÛÕv $xyc©oœ$û|��—Hvç½�x‘aÙAt»ðMçPýÑ>ðg«©©´£°¯Ò�`�¨¨6kžÀ´Rº0çDP1ÐéóÆiƒ§«kÔ-­TË‘ ‰xà‚©¯( µþ^±$túoLâ„� *_%°Æg Ò8þ}w�ÆÿvÏì¤AMß ß÷ #¨òy*áÜÛJ�•T²zÕ^h�³àŠAJ(�18 Ðñ‹ç©LBÆ*M¶`¤Z¡$¥³Á*¶¥åÓ«|S£—€Ed9fÑUü²¶£Ú‘f5– Š5@!ø¾àùtó/] 7*Õ –íÚ_€{4t�9ó>y_RÐJGzÂæ5nàH Få‹~ȵ M´«&¥ÈÅ!d›Òæçk!yë>-‹ï�]&‚ (ßd°ïAYh\¾!X™Îq{+q»l•ª�w …À¸*mR©Õ‡¾ÃŽYä.Ý>*~\˜>â^=Vf×jâ­> ßÃ�æ@� ukbÎ _Xµ{€·ÎõÊ'V’œZ%-æŒ®Äæ}èO% ‘\¦^\ˆœÑ ›ê}9hË‚l»…ÅA4à|4²°ÖW üŽQAÝe1AgÞ–ý–¢_\/Bg\™òt‹M#Ž~èÛ­õ|û–Ø]Te(ª Ê*ÔAÓâö�DÒò˺§€�Q‰ ³©É¡~¯nÙŽ~_Ö ¾&üêÅtqž‡Ê~_Ž.„õ^_P0§¸Ã,¸yæT&ûm²!ùbˆ��’)>Hîü.óüJ}&|VGéߨÏ8_ ¥5ºìÞyÞ÷žú¯u>† æÑ·eÔé߉eƒc±x�GÚð ò“žçD_Qpv9d(§){•¢X!¨‰òì–°4 YZü{ÏQö ‚”·%–ÄEì™[„:¦úÕêØÑ¹\ÅÃC0Ħ=â�bãÐÅ[Ç!ê‘! ûr¶yGZ`¶ ‰šg=OA‚öȧ¾Ißf÷ð«–¤og}÷‡µ=Õg¼É:žD¢— Ø©†…×<l¶º’(¼–!¶PîN­íý². «WIèÑm²gÁ ;LOÑ1;*ú»Lɱ(ÿêûKx–½ŸîŸ»üÖù?Õ[ ~zûÞ^µpn¢šLú›ÝÜ¡¬®YØŸsqÏÀ�¶ê|xáÓ7rÞú­·,+tçÛŸy_=º;Oó]õð‹šµÛ©Jtg :êp}õ·-]ÝZºŠ³€tÂ:#l3ÒU� žQi|ZÃ|XZi_uo'vzn£Þn•îj¢P¡«ê€nEè'þ‹¨6-Ð=Ý6NlêQü|ÅØè DÐì“Wðjn�Ž{¸R±_!1c°¨.}jµ/' ×þÊ¢ âé7d�IùoTDÞŤšJÞXߟ,}30nJ[VwàÌD¢OÖë!à*;zb’“]úâÚ¯xYF8Ý’$?Ñà¸*uÛ“NS­µ”ë6áXÏa� |y±Ô^,¼Âj\{…8N››ÇçJC?”È>ˆíqV/fÔ I*º"O¢8ˆìuë`õÔñ«pï6ÆZ®ÀÕ{;ÁbM´búÂå¡SÈŽtS“þª¬ ñ°¥'-ÓNÆo µÚðA5Áð±²ÛÈy7òsñ\z—g'KQ×W×ýú²2濫UÊ%­^QGá*¸¸,Ð\ê¾£Žî_q‡Ó[ÍŸõß·"`G&X"cQ�ù|ö$}É7TÒ ÃKRÌÖ˜¨ÞWc�ê¥Î xÁ'3Â…î±ö[ݨ$Äp«e£™pŸó±@õ³d!ëTÉwù;g¶Y>3%ŒyùB\–.g|‹Ëòðf.Ë?ª€y]­¡˜²‹Ò{Wà”aî�ã˳ڌ•ðKC*ÔQÒê¼EMV}‰Š°µö&6ßà>?G¢$$Ÿ2*Ò 'È핊¹>zÜÄ«ûƒKv°i§„ =$V†ØßÇ7(“&Le‹î×ÃDIºd'_HÄÌ¥�aÄœ#„Žü bÝ�pI—ÒþSü­Pû{AË]º0‰µ•¦¶_MÌ/‹À[ã+Ó½Öª¯� ð…Kâ 7ׄ`a†Ì‹ì7YmÿxùØAB¹²Q°4 Ëp;1ѯ’þü+Ä5Sϸ=ÍÂ�·ôbK¶ÍK~‹'NrÊiyQLËóª«(€ø5çD[xæ ŽÚ9|ǦèUȈâ ç̯¶@ìÖ–¥¸‘8HAòaõ-úi˜½É/žŸº°ûl:êæ8É5P°{YÜ<)—`Ó'Ø|”åÃJhiÐY?N0{`”Ñ>µ›ßÃØ<À¶½X]°=iWÚ‡L$Á%kÈW£¤ óó¢Ê cr+™Ý(>ÑaÁ0óÅVôžO­ o0j¬š·e;ÛÅ ¹;ÖUšÏÆ8éd‰vÀ@ƒt¢ð´y×Öÿyôá1Ÿ¬G//7Sñ»-ìßC$ÉKÈküy±�¾g;?ÿ÷~ö\[Ú…­ÉØ¿ó/l ïtÚ sTÆÅµìg±6¼ÌÕ±�ÀÌü^Â᛫PŒ¹òÒ‚T™£À‚4²qyF\÷9ÔdÍÇ.œèÆ•-´>ðÄús¨#ŒSÛÛcLa„K{$r• ˆlK”“È …W4¥¶§ �‚™w ¿o´¨4K�€Gµ”ÝÞ›ßRuwwÓB¤©›®b¼ ö9öÖ#WÕyÉ3„tŠ2ôÓÍÂyØTI‹˜»×Üåó7­ü/Ulç�†à·�Óva—okÐoÿø�øÓ�ð7¶¥¦»U–“õ‚d( § ˜´c›.æÃ­è�P�Š“ìÿ>ÉÞOóD¸wãMCÌÏÄÄ™æ¯ã¤rp X8_W ¿”²Ïv—ù{,³Ëy^�±.æÍ=1¾¤jv…¿‡KÁ^8öö¯WÎ&Ss:ëÛ7§¶wÜÇHÁ¼«A¿�<>JÐ>Þøú‰ÝµÊø½ýû¦¾MÇ‹ÞÚòTüò>�í/eKaIú‰úàAˆ4[�õȾûX; è“Ýœåø:°¥JŸ �içWß>›içWÞÆ`Ýù²ýy¶½êæéR;ZÄ]'¸aº÷Bûwªc »÷�ùü¢‘3Q-�yÄ0¤²mî€b¨ÜHo“·ž2îV¯´-*j‚aT*d87Š} í-ô68Aã̸Xö?Eã,o„§¹ñu”ÑÄ3(çwÙ™j¼S`( W« ‹Ë-ÀÀDâk•q×]дëÖ Â€ÊÜKˆÎ|0c««×«gúí�8áŬMpø1ØýÐÈ£¯m\£^A¹. ‰ô⇠’üü.zưÀ·À¥Láo¯aà ·±Ïœ@ˆ å†xa6>ˆrYÔ§ÛC¿@‡ ›9¨£!�,X27¡}ß$ÇL‡!ÀR:6;µ9;RÐÌÍrÐ86_% ™Ö ´l×™0;e µ$ 9˜¸SZjô™¥ª°Y7>³ sñ<ÍÄs¼øVC¦ë�ç¦úZ# Q¶‰Þ AòÄœñYè#dU¨âz…G��‡[j1ùuO¦‘=ÃÀ•¥ÝBÈÍ¥�c„‰Y‡, ÆÏ³˜’KJëºÓÜtvë[øSãj,èËC‹ã`qC-sê˜$'HPÔ½zöû±ÒÚ è¨Y!K¹V8l&&eÛ‚ÆåÀ®\öhÑJz³ˆí‹*å÷ÝRÑõ»+èHïŸåË‹tFn½Ú¥ŠuSµ¯Ð.�è;÷ É<² ‰í \w"ePЀŒ7UQBÔætit¬Ø$³so"$g¿3r@0öÜUëŒ}™/4‡w�fD¼a9Ÿcd~j§@»˜ƒ³OçFÕœKF‹5(ÊY1>ÃE¦8V.ÜÌ­$ÓQ�®\„ï¨(§,J.XÄü†a&ÿù;úóÝzöÒJÐpO -5VœÎY?V6F`g9“#ØŠëQQHŽÁ [ÚùÇ™%ôÁ©¯1 Z€€O Ò�9Ý„f”�ic­ø#õÆpuŽ1Ä”»*7ÔjÅ1 ;®¦ÅèÏà’ÐSöþäõ…ÕÞ'Å䦳dIþF‚‹ú½ÁŠcQ]¹÷vdµbÍ‹@#ÎF@¥:¦]GhÇ%Bøãw½°o?ƒz¢«ðÔÀÅl±ÚÐj¯!ïïoá‡;¬¨W®>PDS�£¢ÝL3uJ)Obb‡‘1_ÜÚRD—‹ð‰y ) %Ïè‚,eÊÒÌ>ðæ@2‚úùF¨W�ƒoEžÓ éˆÄÎA)‹YVˈ¥‰�•›lÚ8°8ª÷ù¬B‰ Û_¿!Átåfƒ‹¨`‚­Iäj¬Òó ÞO(Á`Y¢‹IÇÄ’¥ÎyUÈœUŒO¾¨ìÖ§‡wæ*~ª\š»e·Š³6)j,‡ä•5->Áµ5»•Äj"·5™Ró3�Þ•D lp)W7~Å´JôÄŒªîhŽ�à&ˆ”À<Ѧ£LéþDÚÏ6r(=±.ª«{6Ù{H”PÎë€Z¥h~Ž_!û!rÝ*ïÝ¡îˆøÄ÷¤‘!¥dÝW¨ó=e¾¦oÁ:L4//Îå¡)`À·xÏ©â;w‡€óè I`´rĹf;†0èþÒÚ»ØXz±8?}M/‘ºIv´;¸t„$âæLqœÜÐ^§”�£6 g÷è úT+BË‹0ß�Ù_9l"/ï¹ú]6rò’äÍyp2°BtÜNÛmvþ(aÚ¿9uï!¯—`Àjb¨Ã’ÓsU!!L £tî|¯Rˆ¼t\z†Óe�6Às«Ãº Ï­ƒ´X2ïga/bVÒUëµ®Êâ9ˆí¸çù%rãT.Ø�ŠTø�‘Ýüs-Ϊz9�#÷à¹ü«é®ú¦œ»g0ùã�Ð&9ßÕ²˜–Ådòà9ÿc×ßSZ&”ó‡þ+ÑR¾Ê?¾Íçå‚ùQÙ}¾{»¢žË¿¶9ÎdÀÊs&#¿•ëLù¾³Îwß�ó¬ó�{ui5Þv‡î3ý®/Ë–êYoš lC±ûúà6>3êÊ�;Íø‚€xÐ+Ð+¬ÙÔ”€×FX0ýkÄeº‚rU )SJgËj½H¥˜JÔĸÞÃí–µ¹ÝÌî~·6·[à;ø¼~7•¿!óÍÍö…ªî[[qˆ.°`OîÓróµú¿ºCöÛý_ý(MÖ˶u"¶xÀLØN0ÓéÛÉfº]`�0šs[ Ç„ L–Ðììër‚™ž^°N0s^0Ùn�n0•Ì·Õ É‹„Æ­�©¯±¯˜ ¼bŸÀ)fø.æHÖ›¸ÅL‹_lg·^tÞA…—£#Å›A-üÍÍ¡aº¿C,º¨i“¢£íÞvï »�+ŒÎéü 9µ!â4]ø |G|F�ÊÛ@žÁRîíÏu•vÉ$Zïý1÷þ˜¯Åó¥;îMÝŸ×Ô-öŠ{[÷½­ûæ¶î0Ê…Ø´ˆ§ƒ§ß6Åå34Ó­qÎ^<Ëù*&Ùc¼¯‰_ ÷tÇŒ¹Iªjܯ1¢¦È"ƒ’ÀŸÚñzÑâé]lg`V{ÎþpΤ/×fÿŸ›Ø1ê²þx œ‘2†|^]æGžÓÿÛ ögùlXUðC‹È”Õ>øüVƒ¢–¶[˜ù¯×‡ÁŸo1é(øè=`¯à{Y�Yø!.ì�vÓ†í)²å[X¶»™·¾�%ûV/å¿VKvæùKh y)5œ«çAž'ÂÝ€ñv«q[Y t§ƒ'ŒJQ%|" ‹óœ/XsVÌíÇä�·ˆ�$)±úÈå‹{bDrϨò ¾üõH�£RV¹à’RS1P©‰—âøÓ‹€€ñ,{þkB�åJ(>Á`¾"Æd3‚|ñ±•™Vè"?|DÐ00'´ê>Å‚ê¦Qf¥½°�gÍtÙÕ²”Lå™ôf:/=8֢﷿,µh±y‡Öí‰9Mî{p–+¶%–+®KYSÝ`æØ•"ÏöŸø3,Î,~nt[‹>ŒšJ¨ŒÎn{ä¦< Bz o5~_ë¢ØÆû•¬k[ñ·×Ç4q©²P”TŽÇ D í`ÇØŽëDb»Ç_�ÛÑøT.€ŒtÓ¾Xôº†ÔÙú”I0áFXPOIÍŠ‡ÑG@ûMٔш¢`�6Ûõ ”‚áz|V@¨E”Pà Ðd†ÄÜÓrV®}sB²œHëÙâ-ê¡jPæQ�]ãÁnÏC‚�©H0Ô·ÑÓ†×}žLó3^4f- a±ìŽéìʼĬ�,³ÌrZ7jtpi—æ WggËrÌÎRLð�«DtQ E^¥Í»@/g–¡�¯¦€ÄQÀ™±ºó·v/Ô^ê&êgЍÄ<3³Åª í}d¸Œu€ƒÉ’ˆ®7¤kG_u™(»qõºÎõèÙ[Õ³tYÛèô‰ß7G5Zƒaµœ¯ÈÒGW<ÞgQò+ŒºÛíò\ù€î MËÕ•]_ÛÞŽç™^á–EvlF;UÀÅp›¢�sE>VÛê%8B’¬p»ª÷áƒÙ4ß@À8Ô„Âò,¯L‹„ØbáÍfÝZGÆä#`¼vÒ�]Î(>ÐÒyU­¡®�=V¥À²õàe†\Üâö•yÿÈWì[á"Kb�¦39gõ”æÊ‰2¬ßG§†JÑ ’nÿOá@qm¨ø M¸#C~#Ü!Î –{ÊuõhŒ¼m0ÃRFk ŒʺL¥6ÌÚNã¸ØÒ{-‰;lîjB÷ÚøíÞè ¬ƒ¢Ngºl–êÝ…Aòl�¤ÏÖÓ°ëˆÈ¿&%¿"»žÁ˜W5<"zÖÖSsÆÍ4f ßû 4Ñ”:¤6pE/Q ˜�®ßZª é »@ÓTUfR\EQKP¯lA2çŒlʓ̪Пš�Ó*ó¡Jw€¶adˆáX'ê÷aÊ DèìõQS˜~óú¸ç@ñðåví¯ÇżBh%w³³Í—ߨºÀ7�RÇÑï­b¸r·¥Ù9‡·"ªá×Tì˜ösÊ"1ðÂï²Èâ×ÌÛ8\5:ª—Õå?Š/¡nA—±— Âi†kåÇ×LùÊÔupE=à‹ªA~ߨ)Ž=ûqL«‡—w'Ä•î7A®Œc˜û—$Þ”–âô›U�¸çù¢]]}B=¥¼ÝØSÖÝËÈiõ}9¿ø<ïSþÅæŒiãFÚy+åaçn7½ �ñ#¥>p„9Fò¬Ý›†áDäÜ [á="?áfãª�¢ÑÇ'&LoÑ^Mqê‡P„u€ ¥A¸ØwN'Öu10t‚)xM ýò4Ò 4³4úN‰ #æèA1Íeê‹%V+�çL¢ QnÕÅzÑZ7D•ÎvÌó�EÖÚ@Ä ‰¼$dRt°TšÞHî)¸öƒÈ)nW´…³Šÿ¬–0F�Çfî 3 ¬?ÚÉgâú*N‡­Yhö¤À%Šø!3Xé(‡¬²6B ©á°ðõŠÑº0𣮽Ò%`½ <£«á»E ®VsÕ� kXl*öw`=ÞâÚî7«EÀú6Ö4¯‚ÐÃòxÞTf{ÝhB /¼Ge›#âb­2eQëê|Y­ÏÎO"ÐåN‡�ßÿ × pÅ(ßÍ´:;¨Æ•© YòĶm Z¢hy ¨’†(âä®:ô³úÓ7ÉRu‡·ÊþB_©eÊ1 Úå¦áž„õJùÿ8\£!€Øj  <’0�æJ¨gh㞗˱§‡š/êøù0zA› Ù»£=°Õa§³ù¥a[÷ðŽ’}ë„ájÆUÎF(ïÜÍ-Ú¸–úÎ ¸î)ŽÌ³�HŽhTq–öP`D غÎØý3W¶�-Ì:$¸2²ëË¿÷¶BÞ[B×°±À,×�ÄF"ÃUöåh _žŠ=™qK½‚ÌQe_L ÙçñS±Vu·û~iÏi2XïŽÆAÞ(4°GHÝ]¿»§á~/a~í}ë^®X€ ˆûR:£ý°ã;of³¿SIC!ºb„�è7uò!¾f¦;ò~Ç^†h–ì8“¡sÐeNda¦¼-DøÝžs#Ï@öÉ¢µÀ8›»#zÂIœ»Áø"íóåJPvŽñ€&¸LÑGù¢G¤T *;Ùæ�ÖŠuWL—ŒØ›@%êü|:ñ&ÍŽ`)aZà?ŠeUsý¨âf±„'½b  + o{±ôq:ƒ¬Oœ!Wfò¼Æ5л'ûÛçáI±@��lûà¨éì :!9‡ê….RBOÈ]Èá-yPZÔSA"ƒ)@ÿ€ÜJ4FöaVdÕTŒ¢àBî6Eòéæ8Ñ|ÅñkÙn“¼œ¢¡ Š%Õ.Z¡'É>ÁO®QvS•5o4 Aä¼V{ªñ€°×NOs{4£EÇá²S~Î¥ìø|ãgz–o d«¹ }·‡ØD±ž5Í ÑÕ*$•Ò£%�=ª80Uƒþ ³à‚M(šÉ$î{€fbUÌO”jãe*æÝ�å9ñkC[Šu8‘†ÓÓJênF:ಘR&í“‚³¨ z¡‚r»Š­ ´pÕÃÒ–ëi�e‘j—¢åGÍ/[Cf®q6!H�Ä8viå™=`óøC9áT)͇<}tYB%°�»z$ÄŠi�F 6Ô;�'ª‚’>„…j ߨ5´Ú[¸&ÝŠâH]l£P-­0¿›Â!4徕ÍÓ©af_˜*a©qAïÎlð³xsíoQ`Þ,Ø·”ºz©|XÊÖ‚rPÞ�wö¬pöŠ7 ³qº(8˜˜¿„µpûÚµ½!펣Æ&<±Uз$øË¹: CæG 3–()öävÇarÑ1°”�Õ3Ù×T�“ÑÀbxÇ1Ç]an€_‰Ö¡ õø‰¡úÃúV_©\}Uâ-"ÇØÎÿ˜yšqC‹õMµË¶Cr&'~íº zŠÓŒÆNã,jd„Y/ˆr�^ê‚ö`B‰GÜâÞm_³áXÂ=9V•÷`G;'Ü«Än‘å‰àìÀå°Cb§C¡I‰¿/&äÈq`o"ÇÓ7*\˜‰B[y7^� ¬%1–Fàc µe¹Ýî9�ïÛŽB¢s*¬wk¬äÕùÆ�>¼NS-D¡”’Îé8|ƒNLÝJu!ޏÜ-”¥õ{¶óõwP·¶ãM…-VÂ/ÖΗtoßȳ½y]®"HÓ¯ýÞÕYÜ�»Ó.ƒ‘)wÒ­.Ï÷óoÙGØa´™góâŠ<*÷´×3ídz‡)6˜RmKáN Æâà2º½/4_‰æª3ös« ÒÀ7 À˶³‹m¦Ô¦ÙÙ6Ïv—÷Ü£W#kQÚkùª´÷í¢šn ¨)Ÿ’ŠæKÓKÖ•û»áýþ’ýѸ÷Ñ‚† Ž 48+:ÂÜë3ÀÊP––ÉàEA:DMPó‰‰\ß¾ømÓqmŸ�ÞFÀOX8±£%$¢Nì¾°ü�íago50Ú+ײq)Ïâ¹Æ� œ]ÜzßY!^¹?@üp—ëï yÑÕ'ï�Â?�oîªgÛÜÁø8¿±K¨¯7¨cšÖø›N±NÀ�´ž——�2²*xŒ”§—võp e ×�ØØî]CÞ5‘òu8†ha\‚¹º9¢¨³²y/Á¾!µÛ»`wÂ^ªr�…~;;xù×¾d°b‰T§iÇ×Q„< rÄÓhb²¾^O&êO*Ÿà;jžºÊQøÐn3ëö†»‹m¯öÿ£–&³ßfüðXX–zºáô¹¬GçŬhxâØ‰Ì×;" 3ÎÖ|\‚¹u\Ô`c×ZoßXÑ…÷Æ`}r;“.•®á¨ó­¥�[Ú”êÅÆoÅ�rBi\l2§´µæ6V~“Ún„‚#ÓêÌé—7ûÒí33®{­]H8Så5Ä…~HÐrN„qdr³o‡Í0jaâé¾-“-�ùÊ�wK“Áá Cï�>7ªó¼¹™änQ��Çò Q–ðvòží+t‚³¦·E|¬„›’ âl2,¨)®È)ŠÎª? ý ›â&|$V@ ¾vjΰX᪚Ú¡)Žžå×ål=3ê´Mñ€Œ6£é×aQoòx»‡}·¯†ítçÊv*¡oA¯×3ÉÝxCón[L·~‰·[m÷f2õ›*ñš‚ño¾ô­±­‡¢ê–-y„Ø&Ù?·v¨²„î0•W°/)X€DYó-æxÿêÕW˜ùe¨X|µ}ãj¤*]«ñÝnæâÅØ)V×J4æ”.'�$r.s§/Û/Ö0¾ë¾D�ƒPõé“ ²žoþ§èÉ'+8´5Ò°ÆÃ†þú[¯®6*½¢lýL€¢/9Ü»Ø�š«ÆÛ{“°ôD³7 ß³ûÛYÒ“#ÐÀ�šÕ‰mQ=ŽwNnL~¦"UÂ0�°iØPç¢áÛc¾^¼¡}VÁÛ^ï‹ ×H/n?ƒŸˆÅšÆZ¬‰G`Ösˆ)0k µáAH—å¹Õêm‘5†:íŒþ“ �.$¡Ú¤ywäDAÝü…‰M¿0 ù“ "ë<Šq¿¬Ë%U10’½Á&´ÀL�’C¨°°Ï�gîbʰ‘9÷¦^;°1•V¡¨AÌLÁ.Ë Ý{0�ŒŒ3Wés£hî±y},AÅ(Qò•jï{)esçnãzUnVÚ¬wþw` ˜ÄÑ´¶‘ G~jw:Í""ëÙ_TÓ<è‰7.°‚b˸Åv#Ž¢"ûe�¼8ø¡±(îíô­b’[L6H>B!^×ç, B'.ÌT³Öˆe$5D5B\œ–GÔLÄËsí‡\dTê5P¹ÑV  )ª+OSîHä@XJIƒŸñ«�x#(‰àuUOѸ�>c9Œã?v—Ã8†GöS"ƒ^‡âáÁ½Çõi£Ëç7·j¦¥Ö<ÿV3c«HƒÇ&¼¶ìÒüiâ\¹\Ëï‚¥R.t& žÉ´ªOÆ™J ¼w%0ˆãäÝ´˜Ÿ­Î¹ÊSPUâÐdÙ+Ïn14â�=o$lÓB¿o�‹{&C²rÞ»²5ù¬â[(–Ø#*'�à á­‹)e\Z¡ÖzK½´ïÖ3]Þü´ú€4;/ó^_A�ÄÊ^ÊËüŠJ5‚EkYà�2C3#Û»¡úD0Þ¦“1Ç(5®„Ê{6*`Ä/ÇsRÕ#Šlæ^­èüK) ãÚdØgÉ· ²:>ï9�;‘Ù~VZj»5£n6`ü»ýø…-Òvy˸±^ [øuM»Sx€Ñ| bIM¿Îi?ð“~Cÿa½²ªÝ+bʺÁ¸A4;š!¢Ûò;Õ* 0$¬öU>'×D-µ–ð2ï™®žä/£×<½O§¤³tН,fZ{¸+¿ÉÃ÷Îãg�ç•ãÝ즔 ã©à Ê:�/DaÐZØ  ñmì‚‹iµzß´v%?nÚÙd|—Å[T°ÉèÃÏ£¶˜ÝÒ�ñ¦·t¯úšß†m Ü…Éí6/Û‡™mëû÷gZëæ]šÓvä]˜Ð°�<¨Þ£C CÙû)ÐÁ4íR8`åÜ%¹'@3Âr“ößÅõÂ^ççè2¦%{Õ߀‰#R©“0Ÿm±‘õ1‘�XA¼hwä`ÅÇI3gÊe)ˆAg !-üÆš`\ßÓU:ãñ7W$¸%˜XÔ§¸"$(-~›­§Åø$ž6´Q]å›X_v‡qH~]ð‹‘ÿý^)Èí÷ý  FŽ�E0­'®Ê"pÀ¸+øCƒÇÍmFäÒ÷…‡µ.+ß ýØî„õò’ü¦ait,á[,yó’" æ§šyË% ï±*†'à «�Œqy©òg¾?Îëû°yfiÿ!÷?jÔÃú¼œp­Œ³€  K�<ÅPKçbvõJ iÙõ�ÛË5í¤|®'´´bÊåF ^l:BÍ yO>Ê%Ϊ é}«Š°Ã¨¸ˆ32¡úU3ÍòvICehX=�¸b�)‹‹«­Þ¡²{ãòY–"Ï‚;.Þ¢ù¸Íå¶ÉÜ-åj`¼#]¨°ï•/²Ý6\ñ¹`͘ëòÌ`À̼.f\ß|H¹�žF¡Kë3¬hoÊX‘h’¹ç gZ|((é§…; L‘l`å�JEùaqð{@î0Wq§ÑŠÛ\Bç}î3L„¡Ìçù†Ë‰ÅN£íf^$=?’4�_Ì öRÑXc/PX·�—’‹p¡¸¯¸ îMKHhŸ\;¨/Y[þÃÝðô£büq »FpºI‡b§êúgœ4v¸•?˜Qÿoî¾Û> ²ûe]­žMË¡mî#þ¬é³]ûû×öó‹ó‘=gùhv�ÁÇKÅS÷·Ï?Änò]‡ é�¬‰+Ÿs¤öj„8L1p¸L H¿£· É*ÈJŒ[bƒÇ:l ÆìÃOo�ÉFÈÃÁ~AžÂË cëIÖ—•UODv’½°°´…#Ь©éé'Ù›‰Œ®30Ä#½%×õ«0C¢“½=7PݯK+-í°i+>ÉamWqwS×Çq¥_Õµ¹òUÌ“Èæ«y6±s´^Â«Þ Íql`¼ÁÉÇÁ±Ü«zêO¨Š/Úã¼¾;[ýfL\¸�äùˆ‡^a‘fžOü炉'”^!}9&-Q©Iöý›—v“4#.·½.Vy0ôýQ5ü±‚"DlBu%Á‚jîɨ;w©SÈöè0XñÃÃøÓ?§›bò»S””vÝþª£m¯JO×VT/Ó/~™¬R\¬½Ø>Ï+Þ˜õãx7œ:²- ÃÍ AÌÑyr3ŒçÉelÁÒ`ËÅÉÄ# ©a—K„6Æ9qô†‰®["Ó;ºNø©ìÀ½Pz< p3Y@ã�FÕ’È€*à·(&ö§Ž­ù�…ÇRøøT¥ Š`ÊáÚVÑ~‰’\×ìQ9q‡+“tö}`Øü¶•Màµoy "Ð`Ö‡Š:¨¸Ò’\»Qÿ>_š=5ûtáÞd¤ÂÅ£‹ƒ£Ç¿ã¤Ça9Ï—~·R‡cÔ£Å$š±Wñ‘JNã[Žá=ßýÍ~{ÄtZ”–ìrUbY?À�Mœ8sŽV �Ã= Ò2Õ<ðbÖ­3Î+‹~�\Êÿ«¼’\ÕÆ=Ž#ódûò®ZþT÷O-#¹š•ýÀ9Y¾§#&éh�€Í÷AãnR)eN±Ã{Žï¶“K7EöôÉÓÏþ¼#šý®´²Ž‹ãïœàýÃGVúÃcf‘ºÊêõ‚*H¤÷?,Ýóâà•ì ¿è8Ô™¤¹<&î«Ë !õÙgè&�‘Èý֥ǼÌÁ;(Ú¼ÊõM;‘ªË4-ú¨ÇaõÝ­5»uÁ†Äsnê›ÚTœEêAÃ.š”^p�·qþh¸ÝŸ^r÷=⇙Ók¸ªÇO$ße« ZÕí`P·âvw !'ð7[˜�¼{I ¯¾ {+ú²Dxëw�ÿ¿ôþI ·o°I[ær^Íi�[3WZR€äáHƒÌŽ$V×”é[m•½Â4'R8¨o]²èè-©œZAÛgôVª…šwø¼L´ö:„Ú~àÝ –ãÃÇ_‰þðêàÃO¯²G3Ûé’aQL÷§@Ì×wò²ãäË ¼ƒy7iöi²ÙbQ—ÓjnòcöH�á÷í³ÔÞx§rµòõ¢°ðsšØ«ô…ûnß-ûÜùò$[³.ò›ËýíSŸüxÒD×�ÎòÙ,Ïþ Êv×ϲõC÷ÏËì·¨—>ÎþÎùÞO÷·§!ØêöVwv¢�ÅõâÑAØÏ_×—¿Ú~?¾qŸÒ¢.ÏfU9Nô ïó>³uã¥Ï�½ì]ÙOÞç�øÿN%rÆG¹d>Ò¥¾ÛQû£FëM§Œ9ìkZÏÙÓæ-pæ[ÃY£Æh·µuô;«U¼‘hx鯧†ñŸ[>L´5ÒmA=n+ä§ìcºÑñ^±êª—t¯‹ts�O¼cîß1_§Þ0_#YÓ;IŸ~Ëá“?$Þ³ðïá~ú—ñø]ÕuÀuÜþ©¡ÌÔtñ0Ô äé‡?³Y1«€Á×~ ¯zûRÍÌajþ‹¶>»4wƒŽËRe«®¦:{® ι–PÍ*°˜‚%Q¥|kÕþõ’´“AvÈöhûÒîd %6�<Ð`›†é&7ç¤Í\ÕìDj¯|'R¶îbÚV%t†žüÍ+²O1lÚ6ÔKµ‹Ÿeóƒ $µŒ–°™¬‚ÒÊÑümÒ@Úiu{t»Mߥ7æ[,0¢¡‘ââÒÐ&küR)TLÌãÌQ)„“, ªŒ°-Üg�i™/?â‚hñ™ŸYÕ—j{øô;/ ì”4Z]U«|úá'ß(~�ý¯?5]žWþ±Æ}› o¬“8¦Ìû£úqóGW׿¥Ÿ5^ØÍNHq 3Ÿ±½°u¸V6š­/?r(´oÛß‹=­ž@6¸¹ëj qGËõ\8¯3äò¬=7K6-gåJøégEáÜ †h:(�l‚qqp(ïMü_œ‰eÏЦ~SÖ5‚œëЩÔ×»™þOµ‘=ݘªo]ß󻆸?Ï^aÌ F½P£É=VmÛ9ΧÐÎ=EZoî ßóT·ý´(üæº" Á (°TŒÁ.Ì#äà g…Ãn²¾U"�È�Ë!Iç¬Ì”‡<Œw'Ã'u {‘”ƒÊ3¿a–â •ŠÊ�¤ÁÌLŠ„pRžZåjdü‘öT Ü Ø¼æ0—|½®ï÷ˆÞ�4ƒB×ì÷Þ0-ItrœJFcå9#ï¼'?S!Â>%Zýpq‡L˜˜NK%I%К^BnXò)Ë8ÉŠÁV•çÿ6Ö‹gb9yþÊ˶,®1\øÁóì´uòŸ§›Eñp�=´wÓ.…ÑÃÇÏLöv<µ¿”%Êý÷ û‹}è{Z‚ÚþàÔ>f»µ,”ÅÇZ|ªÃ©¿«4ÏšÛæ49€Å3CBÏtÀ3͓߃ì®ì‡Ë„Ù&°¾üæ¢Þ(¤Ö‰Å§2yõzÈU“:/F�}ŒõU²¾2 ²$ø$NË'YÉ/ε–kh?ûž#XÚ€,3E�Ò\8€30äƒ20‰–D¥aÚï÷\-Çë�’…ÛiÙô¤¤øÙ Û„ö'\C¡õ@˯¶8m™äo9Ä?9ä«=À ß”O¬LšÅ…Ù•,Äö�Ø£c7OFlE**‰‚ïÄ[@émÏ)4Ûæ0hâŸ-!à@²êCN¸ˆß)ƒÔÕ‰à5Ñ,µ€#èçÖàÇè'?Û ™(»¹©@²¨þ'ž/è€/iη„ˆØ·Åø ðÈ2ú‚Ø‘L (K'!q͉c�g~%ÖUÏ­Ž_"%cHè’Ù ÈgC!,w;‰ab)_Ÿ¢—.؆ÏLHOë“«DʽÂÞ¦øÒq3ç ¸iŸß¾ˆÔºˆ£�é^pic„SüЕ)}�ó–ˆÔOÃÃ?ÙnM‹—@lÁå™^Xû€..®ßBí¸àßZ¯û}tG’«áÆtÉ>Ÿnû£Ëb<�ñ‹U ÐZj3#(»=ÎÆåÒÞGSU]uçÃäsÒÂEðˆæo|ãHÁ w;S¨‰¡Ö'ò›—óÔf@9º¦,¾›<1ñB»+ nFXaÀЃì ݹA*ï½{˜Á-„�užê2QbäÝõw¸W0×z¥ CæÆg–Æ­N´EuUÔzà´tŸîàPÅÇš0ªÞ¥%, Ñ»ÔS�ËOmUוTs¦vThCJ€¤ŠBØ¡™`£*Jƒ�Q wª L×2"ø!ÎÕtz|ùùì)Ë@À—CMÇs€vC %ôlê‚ö¶ÅU°c¸`¥;¥ðC¤MˆwzSYciÞ^sÙœ‘�[»¹—yÛ–‰@�œõi!Ö+ÜòÛFP/Çâ"HÈá�’ˆUQƒážUüó«|c@AÂf�ôßÊ}_J§§Þ=P‚„.q~àYªF ‹m¨œf�_ ôz®PŸ˜– ÷y¹hEö…mÜT=г¶ SÄuVÔÏx¢Óà‹k» 1®n^F þú9øËÂÌô­ýüO°"-w§=5Vè®—ÀOµ’A¨Ãõ¼IõÜöÐy&ÙÈ�\ñÉsà®F]Þ¸yIêk±ÄzJ±3H‹èàR Âr#þE)¨óv¹­jƒ0]2ÑËâu†ï?:ù.`™t–u)M,0×E]L'Ñ/êË`‰©›)XÓè¦.Ýë*—ˆ—��´Sê´Ï7vÍËjM�ßHêû»õ —­Áåð¶œSájÇá>‹Cüt~Mv‰èsüð•0|ÇMé§mìÒ;”í$Ø^§ïizþ¿¾=öNÏÕÑþ Ï$AEŽÎÆ%ýÃÆß�Áj0áCØ‘xèŸÍêÔ/‚®3ý4+êöj™Úe®Í°X]¢ñ^U1¶°ËòS¾, ¹*ÙÊÊV`ÕS¸^ÍÂuNÔñ‚â´¨‘&• ˜G\;ŸØÖÎ�F•ÌuðÂ<ÇE1VgJ‘gË &–C$ Š·õñߟ1«íïYÀ ¥Î´Áê?ð%‹¹•TBã,ܸ ÁøqY2æT"¼¥ñ˜²še?³É³s2y¼ ÙN@Ax¼Gh)¹Šx¼J¯v¢ e�mˆIËT&(\ gâsPλS¦ýQmQÐBÁ w§zC.-Œ·†�7Z'L@,)HkE½dG‡f¸‹:͈cíEÛù€ÕEòš÷ô‹)´¾:Ÿ½®–j!½RŠõ$8ïùzU…Þ•1¡F_×ùh<9Z¬§SÀ—Ûå±È ^Q]æÃ Îuð˜ß£§šñÙ_4ã¼��"t )øªfð#¯iÆ7VRš|S¿.æów¨5?ƒ�Àª�øøg¼Õ�aÜ1‡¯ý\L×µ†&�j+æ«jí IŒ+ÁïæÞðÖž•f+ðÍ·À€Ýv<1Ø=ÔÁé8Û›ãòâ¢ÓÌ ³ÀE°×¾Õ3@=.Fظñ²…k åN°bÝ´¸†ºX®™øþ—uÁE3` ×åt%vÜl;Z¯¦ ôUA¡i0ÉŠ~–ÁEF";h�þñ#é¹k«Y|Œ‚FÙÒ«ì G•õÉCyÀÏ”×PÆ ›}j6ÎþØÙbíÆª-¡ #… Z¬»/ó³³b,dOø¶Ø¨>¯DÒøm­6h0»¸.;[üöÕ¯’-ÁýZØ6Æ�F÷§o“�ØÏ{ý^¥ÿ·gúw·ÂÙ‚-¹��¿W©íY�­DqƒÛû[³÷[>}g!eµ¼H7¨ŸèÕæ{¹°ínÛDÑ3;·»mk%žÛµý-¦ñÔ®moÙ�§vm½x]MãC½Z–h”–Ø“Îß;OV›Û*Õ‚½†,êo]ÏíÒ¾Þ§mßïÔ^´?·=³K»J¶|½KkMQ²ý©�ÚN –ÎwyƒÚœ-_ïÒšÞªmßooÏÛ§«|Õ·•ôj~ß»Wï[;õ¾oµå¡¾-7V3øªo+ÍUŒ­†ÛÚ�'{-Ã6üç-¿gíÆž-€£O[~ë;˜D†m_w¶ÖÚNïü6î³­Çó�Wnüq篃K5þ¸óשm½Û~öO·£”­ÏìÒnËTõE&É_´L`_D’ü…> ít¶èEC™�½¤À´œÉóðÏ–§êóܪÉ~�ÿjy¶!P’’k�ýÇï†Õxÿ‡'Ÿ›ÿ†®¦ø åå‚÷g…ïPá©xœì½é’ÜÖµ.øO�fGÛE;‹¬*Z::¤¤¸5˜-’b‹´l…ãØ�ÌDVÁ• ¤d Ž÷Íúú™z�{¯�)‘Yƒ¤#Þ¸Çb%€=®½ö¿õéÿñåw/Þÿøö«ø¬^-?�>Õÿ¤Éþ³Jë$ž�%e•ÖŸ=ØÔ‹ÃO<†ßë¬^¦Ÿ¿NfgYžÆ¯Ò¤Ì³üôÓÇü{ôécù~Z̯±µãÏ«:©³ªÎfÕÓ¸ý¼}šÄy²J?{0O«ì4OgÅìÁçŸ>Nðû“Ïý�ðö ü¶Žgˤª>{pöQ¼¨àÍY1Oƒn>½HÊÏ_úÿ: ™~Ðßò~‡À?<üô15ùéãu«;øÿ‡ðg½_·¿;K—Ëlý º¤¯ÿf³z_fɲú½ô©ƒéÉçñ‹bËs»ñ*©Ëì*^eœäqZ–ð�YQ–鬯·ŠM½ÞÔ1¶PÅ+øß壈[šg:«UuøÑnœÇÔ˜…ŸÂÈU‹Ë´Þ”y×g)öíÇÑHm#<63�§×ÍFÅáªfUœÄŸ¦+7ú¤>{ðù·W¯>}œ®>×)Q|x<‰�`Yæñ�ãˬ>‹ ¼²¸Œ×iÉŸÓSüuV,7«œL³<)¯ã%hZ>�“Hÿ�}×eä;�ë"®a'yŠ2…URž§Ô[–ÇY]i«‹²XÁ‹E•FúŽ ûNdòóQ\'ç¸E¼NÊšÛÑ-Ãý±+·ÚTuÁ)‹§mZ¨“é2=,Ój]äUv‘"]ÐoÁ 1¿VÁR¯q-è¯iQÎÓÒýY­è[daãë2ƒÙ^Ãñ©Kø¿3Øšy}öÙƒ“'ÿ׃Ͽ$2frfÿv³bµ^¦õŽûqòÚ�ÃãþÁEcºL®iªõ2«‰CÚó §NõeÇ/?LWëú:>-‹ÍfIìß]À~ÔüE'Kh§=rް«ûOòàÉ?¾=Ä™þñøá‡©�&ÉlÍd{'_žep¬�q\à{I~í®�GQü²Æ7/áòÖò4>ù䣣c!† YN›Žàâ8>¢!Ž¥×.ŠdWrÚsæí±ý‹¯Š* '�î©Hv¶ïe]áåp�¤U=¡ëLáIˆ“2�…Žœ÷<Í«´„sS¬çžLa+Úã;>Z§ÿ<‰î÷Ô8E<��ë/>²ËtQÓ�lµFyð£ýhüháöŽäúž¸‹RÇKW�²#:�벘&Ó hýºÝïÑ£�Ú´ø˜.$üÜ€ŸGã$ }¸x^&—À3쳘RÚgiä‚ÿÇg…«Ù,]×(ŠÁ"Ï Fê3ä HëÌ/R7dGôð–ôkå•.¶ÀŒ+>…fx¢´¾tw /ëß�ŸÅÇÿ¥‹‡ÓÃk…Þç¾aöxä Ï�¨›/¯yÉ€ë9éì²pÇ“–LÇÖÖ7¿L/²%ˆC<㇯Ÿ¿õü §…<ÑyT›)JÐ%Šò ³þt”'BL¸Ð8„Æ6Ï’WkŠL¨v¾™Ár›ˆò4£ ^¦‰‚A§öï5Èw”´ ãïOŽNþ”<åM)7Û¹q|š\·ëg>ñ“O&îÿþÄ4üÇ1{d!Ü þè?&ñGáÿѧýiWí[¾YMYHÕÆàd)ŠiZ_¦ âÐ๬ÈÐér!K3d¡Â�Øýô*™"2H¥E~úù»$ãeU€RöX~Š@ Nâ³2]|ö@ôЪú•ßÿs‘Õ3V>õ_¸ý“Ø~ù6[d3Úù¯^|÷‚¿mÿ7Pë7&',sGŠ‹Yž¯aW³Ymt_óënÊ/~ôÏ­*4åTàWÉuZþ5ÍNÏêJõºØ?ù"Kàð ßsÐ-/hBáïß‘²I_ùW‚7þöb»z}ƒ Ì_CÖ ›–Ì«›©õo @:¨€·êêüWÏ*똚šük`Ö±¼‰kÉ ¿N6+r`Kȇߤ›2Y€“”ç�â!ÞÜ^�å2ÃØø{Úwà{?m²RØÿ¢X.‹K–OÑZ‘”§›Uš×•žê Z¥–Ùçqϸã§ñóx†ü•û X6¸©ÎŠåœï…T�¨µ,ñë ¨^³³¶M ‡¥ž ˆ®5z·ËDÀã„K†•sZcxvõ¶Žn /ÛÖeˆ0èÝ»}³æáá­ìŠ]à/ðßÖÈZû#CÆš¸ß�ör"F^‚4>ËæpCpû ±�&A ƒð!t+3JÖëå5iTðÝ¥´°Šñ}+€xšÕkÅ @·°ùj³^%>öÆ<~ ~Øä WË݃ôL¶!©Öl¨rû¼Ån&;10íV·Jê¸ëªÇ›«×�� Žy “PMtžÔ è%D[SÇpêë"‡úÁv£¢¢ œ‡†²Ne9pˆl¾ÖQ%¨o²x©7o«‘×]�,Ò¤Þà9=˜gp$+¤�‡¸Q×k4 5¦¬Ö¥y±A#‘Z3üµïš£7îÈ>¦ë¯V6g²Ä5~Œ|Às¹€Ñ2[Y‰î|Ä ˆ0�ó‡5Í®à¿!™ E…èמ;U�uQe5Š´š”ÎÜv‘,7ðÈÒ0Úª1Ùš[‘­ÜT8˜³ÅñŒBwÜ!Ÿ2XݯõHW<|Ø;´sÕ…KU=ãU£„øÔÿ1Þ}‹f�µžà–É4]já͇ûš¡öU�€„Ì‘‹.+*ÈßS<³°Æ5oIhÖ�Çe U¬tës¾îr¾î*m¶ªu㺻pýùËîëå’òMQ§O�¹ @˜ U‘,–¨,Î`U™£Sâ»wÜ#Xò3��ñ›U’Ãá�«8™Ï]È[¿[§ùë·Èµà‰î£¿ÄËä”V�WüÅׯžóN×N~ÿÛß‚'ë2+J>¸4mØË!0úÙyrš>u³I%­Óü�Ÿ6EýìÅÛ·Ô.ÿ5‰ù¿‡/ëúñY±J§ezI,³)�âã,Ÿ-7ó4>üðèôpQÀWkþÐ]Ù·%a+ß~`YxSÆî—Î�.X8‡l“ÍYrRÁ©CH^hJëÁÃa±]&í2³�üUóeS¼•Vœ´þ·@®ú±C`—ýû6ä¶>fÈɶ|Jyðà«u1; ßý2«ÖÀÌ^æ‹âÂÿðÒx±_ÆWTÕ×r¥ß†Cï†�ËJ¼+–iùÝÚìDS'x�ÍÞž`îFÝÖHn$Žÿíå½öÑÒN§°Ó’ܲ,Ç:ûE†n¬ßÁ�LD®TX4X3‘—å%NŽSÀû“䘊“]Dh©â•R’Bç1,7 `�…FUU¶Ê@æÁ{°’Kœý´øþï–ÜÏNËõI6¿’¿œ4ý(\¹»W.ŽÿBr}™ž–x%9tš.™ðÕÜ>TñIÈQìÀ«ý�Iç’ßðÇâæLE™ˆ0¸B"*çèÉ.«S•ôT¤µúv\Óe:¬;ˆí{»×°ˆ ±H Ce©½êÝJ¥áì¤SÂgi™Í¬Z M²iŲ©“% …NÕÎ _Â`â—|ö½x'/¢:–lêf–ÍH<šƒøQ®2C¥êše¥�ê¯M/m¨ (Гœˆ¯a露ÛQõk³O-e;*{p£: j¡Jîïs㚆UÁ©gÐE�Õ×vÅ›�®Šl®ï·_(Ó寴FA"‹ …}êôë¬nU'ù™ùê Žt9-–@xðäÓ1’bQ¯’«þ‘,ƒ¡øù®C\¦Éùu÷@'âÉÓÓ„u³%È­|x¨"G�ŽŽÛ½‡}¤WÀp2?Æ(|¤±žs*Õ¤ÁÈ$ý�˜OcþÑÏ�_ ô¾ 4Nê_ AÈ?ð<¼”’Ž0ÑÄÑýê3º]<î0³4í˜ú"¹ÍTý´\åiVì0¯÷Þ®“Î’åS=O–»Nvò¼\©¦Q;Ìse^Ú4Ýqš‹ ø�= Ö9ò¡÷D)Zf ,ʸ,ŸI€i¿î•g‹e³>›¸K®UC®<�Ñb–«ÆþlS‚^+vU«Ú5þ²o{üáîËèH@qÖ¤ ùëx꜕ïÝÔ¹@Ye㕃ŽÝ�œf �ÔâÕDm V7�®½¾ ž-]~ÏdÖdzÙΨëˆ;v�%‰à�µ¥/¹Èd$v`q¢°þ2²g£Zè�KÃíß�â›3¨v•jÉ€Æ#QoŒK¤Ó%íJ0ã\Ÿ8ï8ÇÔ#°BDH´¾r‰qåU•¬q¶,ˆP(9äpc$QšM§°4ÔeÅ�O¾ïZìMkv�hLüoŽ$ŠóìFåÅHØ—îº;)c×áMCÂÓñ9/¸h6ŠGü=kß`‚ã™$5(ÿ<—ãxr\¯‘ö½êce2mç �jTm íè·"îM{›c¨Øß2¡cK©ËªƒI¥.ÁÕ`±ÇþŒØ{G¨eSÉ­”t�á&ØÀzÄÓ§÷¢çX·eµŽ‡mçð„{‚G’£…•b0ìêLæ†ñ Ž|Ÿz:¥ÍÑ¥#‡7z¾F³–ôŒ›¢¼+ÚoÛ 1à´£þË_-_oK7Û¤—ã¥×Aö�PØ™0wÉW;Ј rDr­¢ â®ãHg³g0ë…)�Y3UGRÿDކ^•éJÏý‹ñY§«®)Ñ<)|ÄTgh_jø=9>Cߦã÷ žÿŽ}›Á·?°gÓæk½DŠŸ)ÛB�sô¬2ïØM}T%¾�W°?Õ€‹AV|iwœŒ³Î�ªMy‰UÄ�žÜ›v±Ì* ’t¡Ûd “_Înç›ã¾/™† ¦…=µÉ­ª@å´‰þsy’çÚ•²œ6|:féšDl=.X2ǶՖnÞ½§ƒ;f™z¥H‰™°V Zj픳é8µû”£owàNDöZ|±•ß Š­+cC1x–æò1}þ^�ŸiPmÕØ”ø–7ø˜²ÊBS{1É– ‹ò´×”)à§êÂ]'¬ñD<¼|90 ¹ i\¨F¿(/¥?ÞdU¡: ‚¥Ùb™+š*}ûtE¾Ä”¥?¸=’È�«d_®‰»‹·‰ ŸµÝAHp¿Â¢?y>N“¿HuoŸ?s Óæ˜´ÉTy6³%ªí¨�¡{±„XUUirt¥@« V¬Jñ¢6#ê°k¢ l.ƒÖCx¼·Øbk�¾t2q­Ç4TB[Å·Hvíõ€±ÿºÿ÷½AïYèïñxñ$=?îõûÉÂá=/ê}µúÁºÜW¿¾ñf8N}ÇñceQÅóü®u¥;Ï>;y›ãÆûDA»‡í„OóUoæ· ü%ñódæ'ðƒEv�zñÊÅ8÷ìZË—A" ÁIy†Š &riÛËþÊTO¢:A lè™c­q‚ºø ‡ö/µ¹ä¤a8æË ›4ä™f³ŸΫFdzó+2Ì5Í~Õå[{h+½*˜b!* Ž$½¢E|Þf“×É ¼H>F-Œ€}�À¯$—’D @ CºÃ.�%˜¬Áj;¤aþðŽ‹ ØŠï@xŠ㕪pz©Ï£×]¾CûûðÁ“#‹‰,ÙÃ3m Ü¡I£»-k�Ö‰€è�â´S~Áb5~ˆkº1%pE5Ô¥4!ï&Ès0Dà¿å„¨¦Ëq1Šuø•ó3îüBl3.Éò$Ÿ¬ò„Æ¢…%MkÕÀbW< fáæFXæyª�ªÅ*0'®²Ù"q¬ã�Ñ�¼Bs´rT2 Á1Ò �o�¢ÓAp£.¢9IH #ñ .#ƒ°n$á£îT£T4Y¥¼Ì®M‡ÖIk�Ý¥¥ƒ¢ƒIœdÉ„ÚAÒ¯ŸÈa%‡!o‹ó”²ž±zbt“$Mmfcš%3b!°ªJ¬ª ��`Uê1‚m?r~ï:Æ£-Ït@j±œCÙŽâë �)ѹ{¸Xß4õ9Å7˜7—®&*8…S*b³<² ¯…ôz½ì‚ð~Ùg«@ eÔÃ)§«<ïà‘g¨kÁ+㬩’jÝÊŸèF¯Þ43 œ�‰»gDKc»»n¢9nÂãNÍ"§ö¨Ëg¸e-U’¹D/ Ï~ï”l¿h•:?{£GÎqÀ¬`žod­Ü&iRtÛŠÔbÎ&È2  ëûÖ×g×={ <0Éòd_—ù·+㸽6%â`¡�X#'qêx¬TµÈ 4Ÿ½ˆ�ÕáŸÚãƒUºÌ“µ‘T[ÉÝr:mÓ£XÆBã g:(gš9dc_ â²²ñ"õÚ‘™Ú5¢·_ΘºW“•(È›NrÍîëÏÙ1[0YœLdZÄšÍC�TÚG²oH:zÏ™Vµ´c¶êϱ=qSœ¹�œŒ¬H�!UmÄ81ÏÞ¤±ì­ãuúvËò,Ž[p�ó9ñ “38£Ú@õË*:@Π`cŠ$ y�ñl²ˆ'å2CnÄJˆ¼�ÆÖÒJGñì³e‰åy¤cÀywÑ÷˜pϺlÔ¥Ìj¿ˆ)=Õ«d§62k)y¥9Ès@Ý„€×ë^œ˜…h´p×Çe'”‰ÒÌ VE¶¢Õ"úŠãØBbm*;�­˜èüH^aÝ$Œµ(D[SÞ�¹L޽yµ•$}Û3:1QËÈ“«ËU••«Ú•uIJ~UUi¦ò©~£Ø�a³è·×Šsï^éßm*úU ÆZôËAwIû»k#úÄe÷ï”ØµÃ<~7€ÁòþþVM:˜Jí’úKò£O¦¹˜�Ívh©õ–k�Ìbuy³iI ±á67uœbÐNX¨‹êC8"9¡”T¤¸èˆûMy%áZs¿©° üDÇØX%¶E©£X°Mã×̦i=T]ðˆ¡£¹�Ê,•.´&ÝB Ϭå$ø»ñ.èjÁZ¯Z#�¸ º7/ýÃKŠÓÙ±á�È…qLt@ëGâJML�€¹È+]…Žˆ�lËZ¡˜ŒDž1U·™×qÀ�X3 8 Ä>o¯é)ÓO2ž@ãØwxÞI �±øú(¾X�Q œifÏI NuíÙúñ�Ôûè l#н^ŠªQ|ùãøUE’J~9Š~ýÕW£ÿ·nÿõi×ßkÙc¤‰BïP¯‡Øèjܵb½§ Ã&•_.þ[‚ ßÕ«Eúé¤å¬qp µ¿x׉Þѽ}/‚]ü翌³ìÊuk<ãwåâJ�o½ìŸu}ª:¬Ï°ºp «¶}C'9[�Óµ ÒÉuÚ�nVæºïÉšB~˜çJ­tê@ä¯Yå¾Ã±«e¬ƒ¸° …½üœ¢XäèWï|©kÍ!ë¬Ü9ÛºþŸ]×=5²¨Š)›Dz ‘Çè%C´§Â¶'N"É€CEJf™ïè^j{ú¿øäÈ9š*yr¥á´U™¥n{„žW·Z†ewÛ®áæ-w à^›eÀ´­nQÐÖ<“ ë£ì�ŽwÁ�åð;“ì�ÇW‘ÐSÑ ¸‰Rö“ÐŒ¼~nc¡«šìÚsŸ:ñ=a]Ë<™(ôB‚¢»æŽ:Žh¶ÍCx$6ðÐ *É mª-ÏæôÊO¬‰‚35ƒì/à‡ UO`6vYl,¿þÒ©uåE‹Èpèk%ÒR'ó„Ø›lr=¼›Ÿ¿†5Ib¬�È?¹3"¿¶ˆtÃn!idʺ5ñ[ãujÝ¿ÏõKpfª½¸_ç\gÀÆÆ(°Qœ´Ø$ŸNÝÜÿ£¥!ñ©®F/Hqtnâ‰Cž0ÄÁpƒ°c'¼V™Zf`·yúS!éHÛÿu‡�ÜYÏ ,Ïüà§ ‡jNFu»UrW„Á|¶ÛLâϽö‚:�‰Z®J·%“)yêvBéò.Pªz—:‚¿,Ë fÆ1ÑInSÐ"vÙàá|‘4y2æÜí&£„ÑõFLál~+ªn $„Ö+…}Å•Á`dƒR¼w²ÒiwýQµRé` hÒ]Þfù·rµ í@‡MCÿ¼ “Ò‹Ûó>*eZ–‰^áeœWp¨²s_^aÚj 2�½òI¦cz|° 3´ÏzÕÑ$…0yŒ~­[VL©Ú'ÈSÕÁúxßëbB°÷FôCùiJ ìñ íåD„Ec¿ áña„ðÆ(Z;9õÜìÕ¸%M:ºi�Z®i'%f0þ[BƒÜü¥Õô¡WP³÷-íènqvòœW"gk+ð µ5¦â”µ=�,_£=²!.Œ-JO\®s•*“çNÚõSÕÞjŒ ˆQBÆ;j´6»ýZ‚§”ŒÁêîd¡%JsÚò\|ºZa^wíuEîòØR܇-núq ØãÚJáþëCÛ£Çɘês¶À®‹Üh ä.Tšµp¢TŠŽ0ŽÍo!zð=…Û"ÉkÎÓ‰¡—…Bea¥û‰à Üx�ÞÏrÛWWzäí7Rªv ÁõÁ‘äV¶žX±¤›ñJã+SOB©Ø�n�&çX£9i[ò�&ÔØ³IàÖÈhµ$Ú#b{:/GCx¨Õ/+&X-+_&áÔGŠs4©›�Æ»^(n‡9~C(£ˆ6�ŒÌšØŽ"m‰Á =Sï‰s›Ç˜øºñú±ûƒÑª£v5|ä¶âTæ0EC§)Ž,0½E”ÔQürÒ$·Š2:ïjJ!‘<¸–}ò»b]šÂŸ¿~æt$èÝm|ü 'PYZJsOo—0õDQ Êš“6е—Êù{ÁñP¨ dÃaBÏ'9=ºå)5Í"‡6΀øŠJZœ¨w;ëˆxÐ\CR=8l&W±W¡k¿¸E;éøx…“ÐÁ–@§U¹„÷µZ”·Êc#iù[€î¹vº’Fü”̓UòSu' “ a#ÊF‘’ÈAdÃb¤Ç†ÕÕæþÔ‰‹É+º0³gSµžr‘5(Ì8‘vh��s™\þ.èþƒY!˜�8h„è¤Å$nÔ}ƒÊBø»@ÚbN ê†ól³R;ñÖ?…—¬Ú9‰ÝÙfQY_¶¡Òy£ÛMœmòÛ ÌkYQ«ö|Ý`|ªt3xØ‚Úö˜µ='�'í¬£y¹M|µJmÅL¨%Þ‚R¸¾S¯’"v±nÙ çpjÉNÖ±ªÝqFd��Ä’T¶‰)÷4ÉçAä�¯iohZvÞ-Z]Ž%IZ«©Ð>yQ1žC5âÝ~%Ç×�Ž_£Ü+4kÜ â‡s §dìotÙ´o-sÊíþDöäÿ›4Ò´©ÔB&kÚ Î�NZ±Ž<]�šaPoïG<Ù�RíTás>ƒ‚e²hï8y _99_ñY…·HÆ/´œm9ᵪՠª5²îF­¡c-EêYï’¥nˆHy†b‰‹}pt4«·Ü0ùÊF;×Ä·f»Ç�¿Ø,F§�sƒ…(þ©¬Ê¢Ì õƒä&«uò«×‡,+kOã”ÅDñªh2ì˜S€ š)Kg0K>¦Ó,çXè$À‚vV;{m{›Ò`þ¼ÞjI˜7�3çeYë:xîÂ%Âé‘Ýv·÷×¾¹mÌd�ÛC_�ÇÝPÉæ€·LûvØ"lŸn+Ì3tƒŒ_båÒ'�F3q«Ø¤#×ôfðB[LYÕ¾P!(“D+ôZ 9‡Í�P\'ë‚N·ZöÏ,ÿ¾vxëZ tmK­P–5â•u·‹IõZ”›»%!lÖaÍ¡§÷5mÂ.-DTÒèâ1‡!$és©ît&p@D�ù K/k£™DúFu>°¢ºZ­•.eÏv‡'ßÞÙÆ±–¨/Ø’˜}ï:"[bs“ûZ”0ë¾öTw:±�ñÚãt&½ ô›e˜=Üw©g´þ0@—úïÑz¿îg…éËâõeà}žk�q\Èvñ[¯]¯µÁÖ¿�[q‡¡µß@ù~¨Ùî?øÐw!³Ù¯€«¼›Gõ½&Mn 3Û®Õª¤þrÝmg�Øü¦59˜ÒD ”v-YY¯¾%Xëî`Œ.'�¤ÁvÚûÅáÛÅ1÷ùºè4þ_ò‚B½Øô©ŒÏrXX‘pC•‘�)\!§Ÿ…%/ßJc/øq‡íØs¹Á�o˜­Ò}K âÖ&I‡e•ç¢X”©kn¼0mºPéQid-çbx³7±dÁêˆã·¯w)èŽ 4Ù1¤Û²ä±9Ã䇡î¨ËeL¤<€b[cgžÈq_‹}¦ ùãf¦+¯OÆî5_”¨{*™kì‹?/ò…ÈÍ+v8˜œrQò5'Ôê.J •UÐnaƒY¾¼–SpSʼn¶á”é�ëj~fUÒ³�Çà�÷×Oýb”-¤]:¤ûDI§£¾åÒWî ±ô$/ž4:‚Òj„#V ùüS3ÆdR•uLP3Ä”9¢¯oÁûwL¢µ€jÉÐýòq¨V04ÚŠ‚ÌdÒ½ù ¤H®’1×£pG9•ò§ÀÀ_ê0…÷ˆ“]Àßs¾ºÿLZ ¸ 8nH·çe*é´ºœ;ö’‡ªžc+s4US¿ºË)â Z’‚~æü·up€®K�pê„sóÁņu0ÕH•m×¢MOƒšº‘3â1ÐT˜ýÂ’L–|Nw~êÍÉíI õDõ2)HšPS:çf‡Žë¡šâzúíªj(Ÿ»sç¶\Ã0$rYÊ@~,ôQc�.1|íÒ`c”ÆAö|õüÅÙ�4a[¸…s|ºòÂp¡^o×Âˈ¶\GÈ zòfûe¼ÑËxÓ¢÷3:O‰î.3¾.n¦ð¬ŽL–½Y4 z+ÞϽº+�k‚Ÿžóè‡ç›ˆ(`m›÷î,úð9-ú…vGáZÏM,—6ïW}V×+tEØòG‰Gé4ý`KõúL\xUóceBT#…›¿ ßÒÔž® köõ6›|èm²Ü©k×ûf‹õ¶øMk­æ¢­[ì%�VÉT–’s3òúÈënñîà)Hª�'‘ì4íÒ¥®¦è `kG©Æ`dÿÙÕá ïök1À[ÓƒõúC£z\[8;€æï@Þ,V\…/Ê…´^TM–m‘�Ùu?¡"øÆ-EÃJ‚e•¥+k$Ú;Y†ú("ÂFñGñwð?ל£ƒ$œê%;ò±j.~I÷µ -ö­¹Á^?[ÅWl‡!JXóïÿJ—_�â“/þtxòÇ?bØC{OæM³|t|œÝÕ`ÆáB’"9Ré긾;F=ñ÷ûh™N÷Nwx�*»7nü-›ÊmF¹J¦¤:Ž¡ð§ÝÉ&‚oe£æŒÜ‡§Þ'JN�Iâ··h¶èPO’ëÊöÀêX½×gW›,¶Ã ©ÏF\i†®Y�‹XôklN^‘ý‹…já�3â�°´óhs?bûp«%qÔísСL§é|äÑè#ïîK‹Ô¤¶j�{'ìµm.Ûá;÷Uº�º*,òiÖÇ·¶ŸV5ú)�1}ΗT&ò‰;â'"›’ LÝŠ~YS�NÃæŠ ¡,½›mrbšñ‚u]%EÍ®+›æMà$Î�¿RH#�`Œ1©Â´ð c’µ’O§Ý«Ÿ9PmÃÓÒ“Qw”¦m½2 ¦©|†-¥?kÓBŽb”€Ò¹�,äEú¬vé-û?MªÃ@ /ç£øG£øy6EFñÅÑ_¤9ñIrsSæÉ(¾>º:Š¿ÅÆñ�Sò†ö8U�â§Y‰Y;´K“:¾-s±æàßƯN/WªiTk⮟œQÈñÿäx\¦küß9�þÕçq¥ [[× �®.¿xœí=ksÛF’ßñ+pººDª¥$[Ùì&¶£*YrœdײËRâ}ÔÞH ID À $æÃýöë×<ñ éWœ«­Ú�E˜ééééw÷<ù�‹—ç×õ,ž7‹ü4z¢ÿQI ÿ,T“Ä“yRÕªùfoÕL¿Ú;†ï›¬ÉÕéy¾ªUeÅìÉ1=9–7ÇeºÆqžÖMÒdu“MêG±ûü=Iâ"Y¨oö&üÃÞé“ã_;9•oà¹øbOò¤®¿Ù›OkxlR¦ÊúÉmR�^?9Æâoâ'c;Âø4Þ§ŸÿÁ?�âÏ~Y•ÍãóUSN§ü÷ˆ8ç'žÓOŽ—rýÄ…Z6sï�‹Oºó*Ã�, þv3¤ð¿CøØ¼'ˆ_$÷D]8—�`œÆjš*–A”ª\ÄI'³Y^.T�ܪxžÁ_ÕdžM’\?7•RG�”f·zI‹úðË=ü9¼ Cõ¿»ðÇ3Uà„*�Ç븙ïkž7ÉL äQ<]“;’ß BèSœÒŠj³¤Q¼ªá˜ͼRõ¼ÌÓø6ÉWÊÛ̸)£8KUÑdÓu\¨;‹’ýϪP©)�?Äe'“IY¥8xSÂø‹ä>[¬q±ZŒa�å`©³J¥‘ÌÝ 3²Ù1[cPØEKqVÚM0ëd^–µ"l™§â)ÀFh@Ð,î`æ lïXÅ*ƒ—*¡™¬˜”E Dàoâ}!Yå ­”¿Ká�¤˜(þt¿™Ãöv£ÞqzviR.– lA¾acðØ;ôd ï«é4›d0"Z%“yŒ„òV©�ꄺžhg€â¹Êfsò£ÈãN¸OødŠìËì˜Â!�Êé§ÞE�⬩£Xö@À€ñN\ÚèB,LSw. (Åò²iÌ{plÓªœUÉbïÔþÝ~+œÏ¥Ö½S÷Óæwo*)@È𿛟.¶gÙYøÆ¥Búkþç k\V5¿¸ÄmÜ;¥X' ,Ëm« ŒºPu`¾«úÀ£©ó7-u˜Esü@²npæKX¿÷ÅOxVzÄÝ›*Y.�ëiB§ KÌ€ L;ذÄû~ÚF‡>F#ÕEW#VE¢1!r«r5›;�ÉãZ€À¨Ý’Å,ù}‰€=»º³þM¬¿\�©ÆåûòU'ÓbÁÿä#áñÓ”?ýKX²ºmЧuë=k%j�è4èJå �^Gÿf³×[�á«mp@,‘È7EDsƒ ƒ�鬚;GÜ_(9µ‡@ÃÀÔj’ƒ©l4xPPç2)RÂÀ´PI½ª.G¨³E–' Ê­=ôpú±\%· à±µ"�ŽŸÃ� 8á[e•ͲðƒÜ.^–À­µñß)B4Ù(¯²<5#Ål4sÙ�>‰ˆ@–š4¹.&³m$°d*”Gfи‚C�J)Ú¤“É íCø„G ïñ–Ü™‘‡”®å„–³í42Òª\5@Ê8oWÕVÀÚôdEIr­c÷{–“Š.$–¡¼ãQZ0¥sTœ9…j`"Qœ—w´Ê,)f+Ø8”3 Ø«$ Í8<{†z ÎïJBu$¸ÖÛmí%"M˜d"X] ØÁ¨qÝ ÿ¿åÈ{“ [½¦|�Ûºwjÿî+鸞$…#Tø‹Ýìˆ,½wõy=D�*¯–u–k/‡|·ÈŠeS¿ƒ»o;� ÝŸ_Ýnt)ùXÓ„E]ª{ßp6Wyž-µo¯- Û^ hGEÀÀ ­¼X¯FÅ¿úëp3±hÖ‡O”WK˜X—=xøÏ–Ëx.²µšíšË2«U¼ñôêüìò`X%à%´0Çнµ�^_5�1—ƒœº@…‹>Q TÒÌ÷N/?k²…ª¿zr¬§h×(TÇáÇ*»�]pÐ*�÷%ó å…Û›�kæ ÆJ!çz^‚¦_%i¶ª;Ä6#¬'0«ôÛu{ÁÀÜ_—w5‰>RêeY°Ô÷–�JĤÌW øÛ{.ž‚È_¡C d “%ã\�MÔjÄöÑZëa½£8ŽÞE£®™4õ0¿äMgÆ8øÉ£æL Ù»8ƒîðl[OkÈ.¶`õ «!àDÀ}ì"P@ØÏF?›åèxƒÌuÎØ^ifɺõÛÏ!¿a¶TôΆziò@] H†D¹s^ÎÈ.ÝžÎÐ?ØT+ã=E—™«Ž“èž3—¯¦i&ÈаˆoIìŒ]y™ëYC7ô¡¸žµÚ�Umט£½4xJYdÖ ÈP“¡ï¼b~ xn¶ÄÃJŸøä›�õ‚ÞÅŒðe•-’j ^SÁÿÁ€ÏÒfþÍÞïþkï4pŸ?9næî3_=ÐÏ8u~è¸á¿8Ã�†Ný¡»%¼“ºÏÒÕzŽKÔ#@ [­Ï‹A>äi„ø½Ý@îÞT`ÔªZ¡Qoâ}û¢1Ç5Œøš8ÔÙGB´*O×­Ç-š€pçåä¦Å‚€‡Ž×ðŸÛÖ/ÀËn@åºÉÚÃ-’9¨3E9Îêö€ úmVçø©ÚãÚ0é‹Ö[ód`ÌZßÿ ˆLªÔ~�ïxâÅŠÙ.J“æê€ž#ÇBCš}?ÆÝ5¡€È‘AÀàW 3×!†zÄä„Ä8D„¯¶¡¾z#ìÂÔ=œK Å:º¦Ñ1¯ÑË¥O7ò‘²Èר¶e)«\„•¿R ‰õ™>ÑþÔ� „aO¶\뀷ÕiÃu(ËÙˆGò¦Á.®g¿bÈåYËW5 ttm×Xí9WÅ ˜ˆ ‘:ë(Æb,„ZB¼F£”öÒ-±w^Þnƒ»IIö ÍÍ䩱çœÙÈC™¡ޏ7æMøg2×:U–¢wBÓ áµŒ‡§c!Æ1ô.Þ¸M“—•÷÷|&¾‹£ìdϨQ[¾vSzVóg¿£ :´¹ç¸ËܯwsšÁ›®çÇÈø­Õ ð¿y� h!rBAÀù'²-Þ/a~F(ýB/óhÄ›rR²4,=œôš6²õ@¥£+]4™4Vé{éf)ŸTÛAa�oà„÷r²/sôDzPQÐý.ÉÿÎòŠÀº%™)�J3"hùÆ�Üîu�¸�ø ®xS¦ß±n»ÓQ'épÑ»Öþñýö¦àiwÜ›(œ~Ù›{iŸM/éø½Œ5‹QZª4¿¿O‚ÀΦ{å¥dò®ØúÆ OX3Q»êU¨S#¾Ú©'õœ"_hÏ+z¡Ê�±pöA Ö-€ÉÖˆ½|­ &ÂH€˜ÖjÎ&FÄoôøYeýÆpZ7P›°É¢nÆ*Å©bß›5žMt™›¶om =öµEÒ�ˆÝÊÕX;~Ób6Éaã"]›"jK|˶µ¾Ä{,²E&¨C¶4Ññ-¹à¶ÐÙ›NPIÉÿ²RÅm/ó”Â7¼5´üYr­ê6+Ö}IéW!× „×rïfê"¬È‹$–þðCKcå1êG`+T_Q“ĉä$|nÕ¢îO’¡¿”žÖå­Æ–êîèZ\^"EÖL¨§ pý¿~‰ñ×�µÅ©.*¶*¸)ؤ›ä+,Ô>ÛÕù¹.”Bó´^ú2/S¬Þ@£NMÑt!Ùñ²V}Å»²ô³} oû¬!P‡ækž¡‚ç’vf$á´á„wA5´‚üŸgó[?sE°W`êj‘¸š_ö6ªpw•LJÌË„ùÄÔ³lbÈw òâNMÕ¯ •öC�W�Ø…>ăr|F¼ÉQ-@X’àÁþN3ø¸Å3y–�ñIØ5ÒÅäÀfKæQÒ¬.쪭Ê÷ΡØm¥ª±Ù>½MðôujwpÈ­£ŽÕ~4$ÊW»(h”“6%œ³>Db×BÇÙ€[’ð[–_ÂKí$d¹“bÒXÿ©G„F3;ØC‘�Öƒtô–¹ûá°YÃÜ…ÊEòžNnqtF‰m Ò£ä?©­²±�Y|ÍeõøÔRYã$ê/ï´]Á‡LZ�NôJyQé°ò:l$ñežÏQàLæÉ”Ê4àe¯R"²!Ûm¨‘Iú z&¥S#3 ¼ å´qd¹×òC* &'ýÄY6š¯Æê]êÖÊK¡ÈÌÊæë„d̳Œ?àrµyf£Nz½=±]/_ï"¼:é¯J’qQ+�¥4ÄxôÏ;Os|HÓËCÀá¾C@CÞ×1ðìsx¸©Ú:ð+‡oê“àøR‘ô¨NxjgœN&Š3i2ìB¾OÝ]4A#V'¦¿îÓ8­;‘Ÿ¬Ñ`žv‰„z±¡^ò­ë×õÃÈ–^FEÓÓY¡Çb-—xfŠ<åcó´ýö¢qn*¸§˜ð¦Ç6ÝëÆé«ðø k-‘î¶7û»�æ Üo´8Œ9ïÄÂ(´Âç<9÷2¢àÍ¥ZR¸B…„Yê©‚cÚÌÚGñ›“w¯O^òI(�þ×hLôÅsÃÆ â¡i’ëø¹±…�ù“¸…nÌs–p'pÍD›M@«‡—°]†Žødq¬—†iÄKͰÌÛ%îÜ‘3óx¤Ÿo,\Ød÷a€4kU LËôÈÆ|‚ÒdN‚¢sôo`h ¥Uiâ"Kù "@ÉÄbŒÈÊ?/È3¨è,ÐKp,�s( ÂiÐí¾| Ó-Öˆï ¤úÍOŠò—U~TÆE’]Eò ø,Y`1H'ÜÿÎÏ‚µuJìWÅOÁMUÄ6´‘Nº°�y°'½cÅ·­ŽÏ•–Ö&`Õü‹y-“.7?q0íÐJ5 &Rwæ‡êÎj%/æH�÷°.¬Çßü¥v½Ez kÒ²)Çu4ÉÁ¹úP<ÄÎÆ�"qå2±pߟuGKk�]íÃIH2Ö—iU$ žœ·ã1‹)­À éòaWëýÀµ�.* €Þ¨WÛN3û!Ô0ïû[èòߥWCWÛøû°¨7î[`b‹á–Æ—B#r1ÜÆïRðù^pX¢ØÞ´fä\㮃S‹Ù¢@RÈ�ɯµ(0îš¶£Xù[¯½‰î 0W%ÉÎ ]V Ë!$ûÃÆ“�QG”$_kmk¹ÐJb¼ ½T·¹‡]-eߘ–Ã(€¥.ç½w˜1°x0P�ü_qZ€œ(àz $~°Ëg÷\ÖhA¢úK©ãÃîyÃ^É|ši5[�ûîEáôº\Œ<8¬Ø&}k‚L>ip±²Tú¾åï„'¥Œ ^ú™(ÂBÓI-ö¢QVÎòÕ|lt!Îâ9B¹>RY.ü¤�³fÂì§jfrfÌø¦º1Tr×"'G³<×ÔyŠÝU-`‹UiJ/“E®ÉÁSOz£t«3¡È�µ©_€4å})©%Q ßt3ËEQWÐZéN8Zü™% �õæèªS— ô|Í–fÃë²É´F'$çyér|À"åj™z[óÊ|Q„Hâ ›5—·;MóNÔ5J8¨«÷ÄÚ0<.òiÔ�™¢,Uu= l€ª¸›‘ûŸù¾Ã!ç^§¤—¸±‡[Çüoóµ7 æÔß_+nKÃoMÒj„¸u¬?´Ÿ©¦X€(è¡¿ƒÅ<›ÕãØŠš[ÏÞßRÏ""ÚÅŒ_íÀ¿ÈÖ#ï¬eù–ä'H{ f4˜È4HpYR¤—+öÀÖ™xý¢\•¨cЭ¬FÀ¥·tþN[B§@³ª¼Âq𶇦ý[à첊^¬ü'w¿Ø= G™S­j±±‘¯ÕëëSÚ�ú:Ú Š÷OµÔқѮœð€l/¬85k‰¿ñôöb²g€¨J;ÝmHþ,ññÁdç`{opÎgÂÅt3cšŒ‹„º¼‚©nRdm—Ñ»õòM­œìÒLI® ± Vö�ÀÛ¯˜î}S•>× Èã)7D9Ê‘qöÙ¨â$fÃNa@™†Êr~èwVþùÔ¤#<¾¨W€âSõªPüò—däµÁF�ÒЧ.B‚gŸ Ø�Ö’žÜ�™à—$´„Ýa"ßåjá¤I-Nk5Hd´·üéPk<ªD`ï¶NÍ'ñrnÒlz<ÝHÂÖ1‡ñòïïá·Z›FýP<68ÜdƒìSCë]Õ4i— z“ËÛÍQv€Çеd?ÒŸõ‚¾Ü€–Œz={ƒÁÁ|Þ¦n·ÁKù4~¹n•/&H;ŒiUs¼Ý>m@õà 8oikþ´NB‚)û 7é§ÃOµ�ùxë Ü4Y\ ¶'xÛl@´3X"Æagp qú üèƒIËÒéÌ¥J®J“Fj`«-öw?ï»*6²6_Ô7¸v@§ÚIdy¤ Û9±Ú¥²1NJÌ«ãÍ–sø[.g’ø�×óähî Þ‰5Ô’²6ä³äî@¤ùäá¥ÃÁÞàx�Ød£ùt»}i1¿‹ k“«Ë€@¬Ä< ÂþS§¾jðÉ2m2Ãl¼ØÁú¯þž…OvNméàj�g|ÛDž~Êð`ƒõ$çEuµIVè½fd_ÑZÇñ<Õù°�™ü÷÷Z¡æ¡oéÇ£Hl�H‡½J¬Ÿ%¨«çŽ|hÎ9ßs¼ƒŸïÇ=åâžÐ2âÈßrÕþ¢<¨7i‘µŸ[èGÊ>ý[fM¶÷´;æ[ªm3Ù£çk*‡]j&ÁUcòÈ3¥5±òk”²Çù=QÑžCîƒN/ƒ;•/m“(ãn„Hkg# Åßö}È6B×w ¢ÄN³T':{=@Ë€ÿt¶uV'8ÁM˜C1ò…Ø %Ÿ\§­ ²»h—ÑŠ)ŠFî*…}ª!4…?$( ?òcøúû‘WÙݸÀ~*û»:bÑ@%Ũ\;ù†-,¬1�†Ä®|$|yZ™gÃ|p°mÝ…ñE�¶­çèê?<ÂÖ~»a¡ò»¹";¬{_‡rCÂðvÿÀqèÐqàYúé… ßR*Ö+¶æ¨VШcœÊ.ð•dn〺p…êHL0aé’ö‹!ÈždÄ],`“²Šš#Qx Û™BÉÈÁ]l1£6 PZÊäe­( Š|¬ $5=å*’Ä6Hòv œþ5ìŠlê@$# ûCîÖ…ÚVFëá„÷`©‡u™8@쇕è}íÄö�ZÞ!£Cëä%¸{�ò%¥Ê~g!§'Žƒñ¦ûŸBþK®�Äú¾®f©‡uÀN†³¥¨ð>ê?ภñá�tMþ‘ ‚ðÒãís ܤÂÚ ëó¼¢½ÌD5b3D6¼‹ íyP«ƒï�¦S�N�P0:Ì,-‚ÌÚ,b¹ÑH¸É.J ŠÇÏ]^‰oú)áO ÈÛË�ìãÔåM7§ñ21ȈtœÈgÚÆÜС5˜8RN½2ùe^�—Á0Ñã?Ã&s4þë¸ODD' M^%¡3ãHçQP¨‘ò5¾Ga‹…h˜µzÿo¢$¸ž5àƒÒÝ®Äv-Ó‹:50ÙÇ'…EÄž'À¤ßjžl&uÑb°y‚R(lÊ)&δ”˜¥vX Ý…¤yIŠebŸ¢¼æ™}¿$º¾þÛPªó“—xïSSªƒ§Ú..°ÀŽè’Þ!z#Ç/'û&Ñù®¤˜b­˜>Tã)¶Ý¨�1µq¦³%zÕ˜âÔ•R¥]MÔ¶Óv¥–äê·¢‘lä�qœ ‰BÛgÝoÝȃN$Gí`9}geJóÒ*“E»Øu™´aü×]—M£¿8£†FŠ#ÃW|O•Èâï=gËϨSEªÂ¼*ÕŽÀ(o‹|¤Ô˜;e²Èùƒ×g'oⳬRE–覧yf�Ãeñ+,~y“ò]c‡Ÿ=Ù9üü3yæú†ñ2ø6Ý7Œÿ}W§ü`òS‚ ŒâL´&¥Ê+)B B¹0KÁB+ÀÅ/iÈ$�Aÿ#Ç”'ªN‘À©Â ×0ŒŸ<lãÏ¿ØyvØ~cÏÝjé´Õr¸eªIåp]Ut[¦Ò¡çK® aË+I¨¿~ê�CïéR„-W“ÐV|çVT/:¼õÐáC7ÓºûL¥¶èðCÉŠ´±Y­>`…Ña¯öEÁ‚£š»ìiU¯hö»j-Ö6‡…Þxgšê­’ÜY-c«$�â!�Ÿg§ÍTò™6HmEÁ|„6-¼Ú(WìW/’26RÝŠ§ŒDD‰¬>4êÈ‚³ZF@‘{sZÕôT&¤"@w�9³ÍL@ažÔ†$âbLéÂ¤Žƒ\Ý…I¯ë¦±¬¿`ÃÈMølTÓØÝÂ14Cƒí4Å>l×pÕêÏè0ÍíÂÑe$7ùóÈ߀~\ „a¦*ÃBË(¾ä1Å{âÜK�¹k%—åóé„x.´@瓉."N¦¨é)žbü«¬Bh³ƒ1{´föòô;qÛ«\)ªÕ+™ã?kýÆZýÀºXÚu¿Ôò¿n¤z*µþÊ“¶>댿…ŒØ`Î~Þ¹ü€¯æ-Ûgý¦*�ØðÐ/#Y®kî"ï¾_¢õ'‘{Fƒt ¨C¬:·®Ñ2.÷¢‘ ²v—í¡óvì\¸Þ?2ÛùƒüíÈã¬ía%òL)ŽÉPPƒamH8WîûxبNòH”èÝ34#·)öˆ+fIŒÔáB±@”NûN¢›¯4©JÓ Ñ öÊ Æ»Ý@­¨¼t“ð1,ÁJ–x7…WêkP,ÇG¤§ÕLoî8§ò6l‹AÅœˆsí†LÇ•½¢C×Ù¹&4 d*d6˜aO’™˜Wrˆ‚-&3š `0ñÁvÍÑðÈãýÝ( 6–�õè¸sàÚ"ø!©"ün'V〉ÉÁEz áøM‚g¥@õ9>i1hMÖs ‚…ƒ5„ö-` �ëˆÈ¡¤R…!?ZÌzøéÓàØàðo¹Y^ÛÍÛjˆ*VïA^Y«ÄOÅpÚ\ÚàT«’Y39Mùö_0 Ša[F¦U*Gõ:¬�DfÐq²C÷30Š“µñ^Û1‡6; ”e?ˆo¾åÄþ�Ï·…]åeí�ÍòžGl–ê€MËÀ¦»¬mDþ°>ð&³Š/ßš¶Š-±8M×hò@n”IYÕ{nî /›´ÕžÉ|×ôkWÍžœ¦HŸZ tÔ¨õëòôyW¯æpdÓ¦È]åMw…Xy¼o;Û°ÐöS ·¬wdžE³¼Ú�Û’ÚŽ´®lðËàßZ×ÜÝ“ð—RɽúO»WÒõ‘s9«0�l‡zíBšH¾{ÈÇyº¯qè£Jzp±Ân xÖ)ÝöœYtÜö ~8Ô¥±?š6š±[‘A×ÇÄV?¨ÿGµ´<‰},mo­ZKúH4ÝX]–º µÍʨßHØo‹z4-|úðM %šýž‘hØôáñ°¶ãçÓ;tü”x^×éòéÝ:]JLÚ.ð�» ø‹z#°Þa¾£Á|´Y‡yoÖµD}ú/ÜU⡳—æÓfïC~ÁÈ#Ýc Sôk­AÃV—ì©o¡êìé¶¶gº�Ù‹S»¦¥åÓ�ÒÒRÀ³¦¥eq³¥¥TÙkš˜¶oñoÑÄôI¿^œO{q>öâüçéÅù°ï°]Ó»¬¬‚°kmwo[¼[¹AÍÙ´)²} ê¢AÕáóÛš±R¿6! ÎÕ}K›Ùå‘Cqðu=b(ƒUý¨+¼åÈZ’RålîJž@+ÖŒvwÈ”ÃP�/@knbãJlqfþ�qÜÎFî¬×kîá ˆþ!°ßVµ)®Û 8 �[£›MD¡­¤ŽU«âp?-eMØý>œ‰™-_ ÊX©Uåæ¼©»R[Ü|S•6jTùdæÇt™°‡ ¹D˜ßç+.!7·+q÷î¨�Åá¾up­¶Ž.&óµâ$ò¯bâÃoœŒ`Ù Xׯ‚zؼá¦MìõX[ò¦¬ž/wݦN!zoØ<¢÷ê&ÉDw¯e�¼qz¥Ûî˜MûÁŸ}è‘à�˜Î ›aâ6jLžÕ™üº#©ö'¼ ’/L³¥[÷9äü âqHd£N r;ô›£ÎJ%}©`Ôz« z{xt^/Æ.AŽên�ª_Ke2o5‘Õu€å™íÓÛ$r̦—XG«ýhè´½¯»¤ f––zûÉ›°ñÜr3IÜ$¡fŠX�h;ÔÓ`E:º”j=é„b�ýpør5ºRÕpQTš|¥[µEæ8÷K0§3¶™=sÁ};>á}5UFÜEýå]ãòb½®«1θ!2.ò¢:cpó«ÂËÜêÖ'Î�=Ù ¾q±†Ofxq{¶èiÞÂßz/¿yo|ÌrÓ2~àZt^ŠmÃzÞ«±oð¶i\°6œÅ!{ZqO|› ]Ò«?¶éà`k+˜¥4dÔ<¿yÞ™»øxÕjûT«=Û¼ZM ¿¾ŒéÙÿ¸2&qß»¼äXW4Ù»Þ]œRªHyß{Ÿ:±këtžÝ¡NGŽ¿® åÙ¿pŠÀÃÚz¥gw«Wj¡óœ%œ¸îš‰6%šEnÏ¾È –Š·}Ny=LíšÝÜã¦ÙKcáÂ.»»tV1-sýЦ|²¾èÙo[ $øš¢–vH‹¢øš2¦g¥ŒIÀ³¦Œ)€Ç�YÆdcÝÕ@Ï«�«�þyª�>šE-�Šó‹7ä õq*2ΧÚdZÍŸBCÜÓËÇø�`DŠ>ßË’ùœÁ¢ÖƉuÚ1{…IG�«Èe]ÛK{àJ[R¬þ­ïÞˆŒD ‡�:4Úë§ Î!$ûÃÆ“ÔH{™¥¾‡Ü?,Õˆ•´\5 ô¡(¶Ê@�W¡—êV÷PÔ1„B®ßTæ*˜(€¥.¾w°1°øÜÕYS¿Ôô¹QÀ÷@Hü`—ßî9­ ЂDõ—Òœ³´Ïs‡wÐ…÷bqz68Ø‡Ó äÖü±îáo•µ¬ÍÚšÅ{|Ü«v£o£Oy¬ÜlÈóå›Ôküúͪ’r["q{–Ý­*ƒßî3ů÷šb³UXŽ­rÖ¢†vïÕmt³e Øàh�á˜oÀÑ>˜QÕõ³°|I¤a¶ pæ$˜Ž¥§Ü‚²t=K±§.(¯x`ZñƒÌŸÏÍñ´jËí¡îo©Í—FçI¾,ÊY( αOr…ãt5Ç ä´¬´:Ã"{UØ {®ïˉmäá,…õªG·ñFÔåVZqÿÌÂøB1°ÙIúVá%9ÄøAÚ�KMí™°úU²Î¹Èj›OØæù”®'`P‡ÔÛ©<¼*9>I+щ¼†&¾ET#w°H(…®wG½ç³2›9Ëb}Ñç Ø¶ñ5‚4רaÝFÅšãÓ¿›¿wâƒíx/>´a4‰ò…�ÏGÞë:`R ôJo[’ÂúRùS™ÂL÷.¦ËêVc*jœÓv¤ò ¨3{Zyµ ;—·;QƒS#ûâdwಠÙ�Å ÌÚÏÜsšj # vo•OÚtÜÊ–=™%j8ð2•Œï@Mµ e…!mC%<«Ü%%fz1áM"ÞF*jâÝP2ŒØÖhåj|£Ö;û�+¶”6 …î+M#FäbLsƒ ¥;%1D’'mnüd„÷ßÂhÚTH¬Ô�°ñÀ™e𛼑ì>^W þKÖÄÛf|±á©­ÚmËÉR & ö.@¾5.k}0-åð¾YËØp™4ÝœøCÕ°ú6 ˜tÁ™*µÙaŠõí°_Y‹ï‚Ì!Uи[ÇÍpxª™öG˜eåMí]ùUË[_¿zŽHí=ÿKñ&iƒžÎÇ·ø/>}ý­ñFÆ ÍBͤj ÉXKþxœí\{sÛ6¶ÿŸŸWwnמ‘,ǽév׎gÒ<º�Æv¦É¶{§ÓÛ�HHš$‚”ãÞÙï¾ç€ EIvâÍdïîLY$çõ;ÐÙ<¿zöö^¿˺ÈÏ“3ÿ¡d…ª¥H—²²ª~2jêùäëў׺ÎÕùÓÒ2¿ÏU­ÒZ›òlÊ/’³©`f²[îѹ­e­m­SûG1ÐZ$gR”²POFzn*eëÑùÙTbï“s÷Ú�Àƒ•Hsií“Ñò±˜[h–šLuf8[Ëêü"ËϦø‡x"Îfí³sq@ þ¯Ϧ4ÀÙtu§ÁîŽ>æÙê9ýå“Îæ¾Úgûxà¿ |­ ùb©ò\¯N‡&ðï=¸�×2oÔàüçN „$©ÐÊŠ]/á«ÐÖäåCðTG ÷ÉôÚS]ØÉãÃoì¼#s˜b®k;4•¨�¨—*gªsÉz9:¿<›ªâ|2»�l¼zM¯|©ô{Ï77Kc•¨Ì LW)afVUkšçÏDjò¦(mBo¡‹–³\Ù1½«TÝTÔÎ-ò;OíK·¼3û+2²Ë‚£¤C‰(XÝR®•�µÈl¶ø²CÎ.,ìEŽŒ°ÍÌÊb•+qñôí«§ß…iªVÀ¶Ú˜S÷PÔò6n®nT%f·"SsÙäu—zQ•9Œ§²,M�ˆÄF˜²Ã1¯L�âtT~ÏÃ�>ì™[ãÉLhõ;vý‘ÈÍB§2k`¶©ºó‹y. ].hiÓØƒbËÞiâáãõ3wß ~.tIÃU²\ ‰ìDÕ/ �ª`ânxœí}kwÛF’èwü \͹jC=�ع–£se93«�­øFž™ds’9Ù$‘€’•9»¿ýÖ£Õ@%ÙÉÌjÏNL‘@wuu½«ºúÅÿzõÍé»ïß~ϪÅü8zaþQÉþY¨*‰G³¤(UõåÖªšì|±µßWi5WÇç*)TYÅç*�Î.ó¢|±Ç?D/öô—ùø‡;8.«¤JË*•ÏãÀ‹ðDô"‰³d¡¾Ü*©TVn¿ØKðõÃcóõr5ºRÕEú«v…XI§YlE ’ˆK⛤@):D´Š¯ñƒ�^`1q±’ÌW=p5ÞAŒHT�<ƒ‘/ñ‚cÞ¸“ñ8Eq•Ìòös©å ËX¢Ìaï�í[ø‹Ño—%<7N xa~‹²‰!×r@×Âq”,‚Æ(àHzYe™êOnš(Xj-G�óÑj¡²Šs"µ Xn¥`™óôª•¦,i¼m|s1JæªF ú§ÓÜR�ä;±Ÿ+ä*ÂLì­ÝgÈH­ôر”Þ�˜è„6z „7q=´ð&y¾Z¼–-ߪâ¾ .þZ)áå³p# ¨6ϦÇgÙX줘ÒÚÊç/öôuŠõÄÛNüNÚBC­¦S`�„¥­ZXc-©f[ÇçŸTéB•Go_ì©ÅqœÁt`ÒÄðc‘¾�´((Ðà(ÔØ€Á\ªâšð]Ò �˜p�L@¾¯àónü¸$¾TÀWÅF‰ž¥}õ߬*XþšE; k'>Yk®’ó$ZÇÀÃ.Þ:! E  c¸F’ÙheíXøê}²XÎUsóY´jsäø“ì²\‹ãøì�ÙcÅ#lÇøƒ³ìý•±ø7êT²Îw`ºþp0Œ�â'Ãø³£øóaüôÇ#²À=«ö»í£(Z;EÓ€Œï/«¼:2Xâ¿Ì·,Éüï SûßZ¥ÌÖÌb¹} ߣt��OÄM†¶‡˜ ¡Æ©úÀlµ¾ÿõ[ó'lÂîî.jlÿÿø÷¦2ïÂê�p c–j£–õ³Gq i“y™ƒËiI¬‰$žjòåV“gvÑ“ÿCóû­ %À?r<ŸJx,ÿ»­ÖÇ�ûÈ#Èo¶jlZ[Çþnc<ÄU–±øØ:¶{¿\$ÙT™×Åe`;‰l"‰P`’Ñùõf‰±ŒøÃØhìïú¨Ä½fÍ|�N»g0ޱvš�wð÷»Í÷ƒœPxvâÿ©ý±ž2€Œ’cÁêöd>?Õ5R%ëôa-*ñÁHòÀ[$|qø¡³žÄêÏ _„ç>>Í€.ö�p´xðZ]ƒ"˜U*Ͷ�ö¸TÕ�_ #Ù”¼(c¬‚aÔ>q$»¼�‡ã*¹"'}{þQ‘Ü¢áƒÁÕü©"¾Vè³�ÖÁ᱄æ T2šq3ËI³ØmLjžß�%¶Z\*zcZ$Ë™Z(x¥Ä@Ø�C°8æJD`ÊÕ%lYµb«²P¿¬Ò‚CMàÊ]Ã[z^c³3ÿãà¿,òÎ̓à%ŸMê H]|H?ð°Œ76šF4ϰ ÚÛ!a¡P`gäšjlzû™†íñÁy¬-òxp¾s°½ï�þæñÙÌ6 ^Çù txÙº”LSÀ š%"L’i JpCà¿U±2­Žâƒm 1ð˜•^ñöP èÅ ôX ¾Õ‚·$©Á’05hî+`$øïàù¶Ã5ÿæócV½ ¹*ëðÃ,—ª>Ñ0N'"ðIÜ—=`tQÚ¾ÌÕP_ñ1 æq™Ïd@¤•ö¼Ü·e�÷†ˆ«›Y ,5JJÅÌ¥i_ð'§µªiRŒµgeˆ‹QÕ§À ê£Òs™æÛ°v¥²Îͺ£È ‰ {XÁáÖØ&8 ûI ƒ-)ìľÜè~TH޳Ð&IÁqw©ñ1……þzaÏ÷õëE ãÀ…¨±éi ›ö1¹¬Ž;O¤‹ÕÂb9l1o6ø®Lé<)0Z�PÎòù¸[CJ…�¤;IQ¤ì:ÍWe\Þ‚èxÏ;ƒ?! A·³!FY¸›4Òd•�8¬Š°i^ç—WYúËJU¹¥Hó,q«(„Ÿ ‚E`°U£ ^-qk›¸³ˆE!"wÖ¤R Pk©é ¡¼h,öB\-¦2I )…È"«Ά&ô‹4�Aaš�Õ{k]y³qPNØ9)ËÕ‚¬üÂ<©µˆE–Ù�šnô6§)J ¡%—ùµª-�%UC8¤lø›è9#8�VE¡#HÔzï­/W£“F�Uå Û~xTbf xÁ)ÍÊþÛ#¹�7®Ý4hòÛÿm¾\¨$ÓTN!» ù£Oëa=_bûì “` ÜÌG£Ðqî„ܤ;&”m–ètŒ9 d@¦|zÃýbØxÁq`ØÝy>MG "🤀gÿ†ª£TÉš\¨–0ý /„Íp  ÌÀœ¼~Ý´?Ì�V¼Ýsãø+üƒ€3sœ9Ö6Ù§’Õ"'¿Ìs§BVÐ:8çk 9Ó �#Ìó‘Á×Ö#4¼ÜÌàKO@~9$W6U*1m}ÎH!R¦ée«BÛÜàîáþ×Lðê&Ù׵ќ˜qJ¸Ý˜õ`þ°Þmf£_Ȇ«Ö,lLAºT UÛð=hßÀjÁ¼±[6Ùp„gàøÞ´à™µŸ]¦«õXÀ»Y(+Œ|Ihj€É‰i’AÏÇcñèúHˆ§b©ÀYî }3XM­H¼â] }Òv Z�œèü6qz¢²±_C¥�µÜ(×× ˆÕ™áÚ=•ѾEÉ¢ii�ù`*Þá“ÿ=˜¼øyù‹˜ŠÔï*̵¹6lÈjýA™�¹kUÉ…+x^.àÃÀ3bQ«çu¥«ÞƘi»¨÷+Ø •%݉þ¦´ hƒµó&VÉ*]âfH1Ó¥¬nihêTð²)®"9x­ªWÐ¥T•ÛØVja‡^H†º °^%p‡3S$‘RHd&ÁSƒâU*m*«×¸v•ëºFY4eèl ª¿LÚ§`ŸÇô ˜á1ì"vÆ­Gî¿ùcضZÂ2íÒT5¼B¾Øh—Ÿf™Æ‚ެd®YTy” ÜÔs­2$—7üšL~dÁ¹’ºK¸h UÖvÀî׸„ Èþ  ËE@Ï: 7³`�W0!›™*Qò_mA}1`龪t{®°®àÿ%¸¥¬^>}ùßÿ5b7s¼D>˸ÍW_ù6?¶Ž�Mkþ‹$:ëM,ø‹“û»à1ë,nN³àî—M1ƒ}YèÉ7¨©X »±àËZ- Ý�7Ï�Ž��xT$k9ò»—¸VV­þM‰Æ"ëMB‚ ÂåD}L^زµiľ‹K§êaL\íÙ²_:/�ˆ“Ç_yÛq×%>3e-a,ö¬•¤Ðtïò€D�Ò 4”;´æt(¡¥t PærË"[KÝ.öHˆïÀ+4oCaÎÑ´©o— K“g^‹@e¹õ»F–µ†8Øð&0Ÿ˜8û4È€®¯è3 þðÈ5ò•Áÿª©sŒ}^J Å^]½9ÓàŽ1MÁ;|bà ³{�té©ð¸�è=PG`:€ïOxàÁq=h ÓÏ®��èè�(,«Û¬�,SÅJˆzÓ’â±|Ü!i{Ph²!+VL Ðú¯LŸ.¸•ïÅÁõ$ x`Ç"+€a‡^äÿeÀ×É×ûÁ×ïO>|ñÐí!܆hš.úÚÔaüå¢C9.}–µÎ€Vèœ;ƒØ8õ˜ˆG!M’øŒ'û¸n³MnÌ!Î]o7çz–@T2��Í@�ËÑ9þƒ0§IíºˆP'}ÿ(ÜéFø$Èó“̵{~Àô÷AŸÑðÿ€Ê':Å ‘&l³Z™ªöà¡€S—êƒÀ'Ѽ~î›—ò¶{€ÎC‚Ðģл€Ð«R=Ãyßüxñ Ðç.nþóáÏn=q£H˜ì©’ìΛ ë�ˆ7-DeDÐÌ4e†…0U¦9ÄÁ¼Ž„\ï"‡÷¯"v€}åjÅ!¦/uéß0BSö‘�Dü†ÔBÃÊ@´ÂËIÄ·²±œ±¸VU©räFk.ˆþž¾ÁŽ_ ­ÃƒUN$  S>×…¢?_?·“W +#HßjY`†DºÆ�—˜Áv Ò–s«7:êߤ�4Á»r’hx%‹Y&7dûÿ7üDE¸hó÷×¹Hf”‰d, –8 ‹Q¡¨™<§lj⼃;À™˜`†½´’hõ%Ë‹’Wš²T ɸõaIÖ™{Ô8öÎZÝÆl‹¦ÃZ�·ˆð7qƒ¶f`WžƒýÚI± VÝ1ÏOæ£ú98 ¯i‡`ñ›ºjU�Ê£+Ð�ºR ºHŠƒ&…D.ó)¨„2Wœ‘²ŽVâ×:²’†�º†”MðaèS†a§^Òç�þ]YýçÙCÝ©w“Êk&Rëë `ËþÊ@ÆÒ‘ÚãéQ“{WQ·Q“}UTN‘ éµU¹#6$gÙ¹&®”’éù¢Ô,Ÿ_û¢²ïs +Çè jÐ@â…tý0™,OË/Ò:“¯;GÖ^âj HW°û„&Æ,—:Ým@pR6hÑHõd'¸‘Ô«†£†Ê4‹¥cG®gOظŒ‘ü`vt�¶ï<ÞÀ€Æ{íp˜ŒÝºBëÙÏ‘\6žÙ �Õ �œ+œrœEÇ¥úQ:…èz¤Äà�X=q�À¥ýØòõ’¹œ©ÜöÎ,‚x,}%ƒ$Ñ…�”STtÕƒ@¸µFÿ�¡ÖçX��� '¢£.Îég¢—ÝtH#Àg2*U˜µÊ¼¿qör8»õÀb64i¥okúËo*ÉQgv‡þ~=êì×á8 m%$T@loczxÄc†D`µqç€/{MQéYá LÔu9u‡É}B¯'öDe«Â\_÷µï 8<Çx¢KСã£?üþ±Û”×N•¢ 0'›€ô&¥˜>Þ<�p§ƒ–á=/ UDŸ™ J7Hðâžâ]Aä…Ÿ=é?ÅË4AN1ËÛ·¤­mGRN‡òæ mcð 4ÿDà¹k)Xs“6˜” O¯+ˆÑ£3·Æ*!zKó&ó·J“lƒVÿ*é¶““;»ø°t͸â‚"Ö­ÕSŸprkª1_š{\2/Òl#½3f—ño:]€€ÏZÿñÜ�3Àñ`¹êevÇ i×íˆd®* r�;M æo¡§Ì”Õײœ|¯g–„7‘`oªPË59lÜRÞ\ë6åEãŠ+|3ù¿B'€™žŠ%®¥ ó˜¹M¢eÿ nð@^õèr0¬âM-«ºû.¦ê²)žÅ¾©Õª%/¡m qwÊiÛØg¡(m\ô®bz$¾D»ï`† —!é䌼c²OŠèøèñF’èè1ch*.XUsúï2pÐÁ×qTmÛ´ZW#¾ ²‚rP.Ô¯3´›É€Þ‘i°�oAºóíG—8EÙO”!/Û\ig  %½cD.¿£\Æ$|È*yS¶||L‰¶—ûô€®î%—£&º¹¨Þ52�Ú$Q£–ýCò9å É#òô·I ™£a¢Çß' |ç¾ÑÒ>3U¥öKûÖ ófù0 …/JN„û�w1gÓ‚Éñóº³Æ°Ö›èx#ÛÒÂ] ø@ˆ+<Ÿ1Ôúîr|õí÷ßÕŠ­Ònqîn“‰Œ4uÆpÞ�“íÑ`í ’šÎÒ’-ƒÜqi.=ÚzáýÄÞÕ¹0º ]ùì+.iÊŧàsØ\…`£»J¥)Uæ^Õ²±q}+ië"Qáä]Y«á ¾?¶ú­í,h{ø%î4)ýÅVót¹Yÿ‰ÖYëòþRºÝyÞAT;®3èÐ�îS;PBP…ìM¼¡ƒ‹Ý”ß$¾ì·óLäI§ÐÕ·ž[×áLoŒ QöG§…½r)ðÖä†H¥PÄe30NQ ²ÕRã’çM™2ë–`¬rŸ¿ýùíXU¹åjøIç°ïO˜Ÿ@ÈK“ß÷Oø ý¦Êˆ #ï^ îÑèè¨�„ ¨¼HC!Õ+o¥œ^Óírw3i•ÏÍÑ0~ÚbL™€* ‰0Šð‡Ý·ÐhÖ>Æ[ïu¤ƒ»a¹·f…Rð‡¤ÍÔq­Y+ÜUS÷›)Á­ˆX-³vèµ°v f^bÓý~Fš…ñY ø•IÓ/g8+†6)Ús iÃúýcØN\è:æV_öÖòéÓ×�ŒÔ!'?°óIÔ( j(ý8GËVËr"å¦Z©õy5mÉ÷9ûÝ÷ W\…¨ûfirbG:%~½�l“WÐ8Ræ‚’ü!½O€G`¼µŠö»Ý¦ï1XW:Å<·©XúE k›ö„†+MÃ.õ¸›Žv«Iæð}0Ofè†ÂnÅoª2>, ùPÇO Y�æ�]4–éñGzKw¿oz_6üè«ñzŽgR,Øâ1ËÁ�v¥Ó¹®€Üéȉš ÷¯Œ^ò›_Ãå.Ò ùÑ9}¸’"(ü[Èì¾'¹»eÅå W¬ñPÕüy¬\Ÿï¼:=xUúl ݺ}·œ‰ßv~`„~©x£<ìúL‰î©û=®)ÿÌ×ßئÑPF^O+P5�ìözU¢P+eÚ€Zatƒshº×“´˜Ó¹Õ�•�€º‚d۔ȵjÕ&˜g™I@kÆŸ5»†qU6òÉ»Y.þÞÏ|†äÉôÏ{Æ,»³×"˜œÆz%k�⥡Õ.°å GMsPà2Œbã2„ gÛ¿ôÌ(äÙÌS&½×vÛtGÖu€ZÄTæDG2®áLG5%îÉ;^’ ñôD„e–UmHzôwôg]ÇIä=?rѱRODç펙å“:�\X¢øz%“�¯cY͈0Ô‰À¬ÈoT®aàÖ^^¨%gϧø³2ýd2YWÉ„ÔIQ'–Þ"»¾Z«|T«Þ¥"ădš_€_•Õœxú[r^ÄÛµ]•i±w|xøh0ŒßÀ@Lý¦mÆŸ±?ŒÏÒ RïËiÆ´r†nŸ9’ã볯NÇ_|öùèpôÉçŸ�Ž=:}üâ-<Ò ÜŸ>ÿâó£Ñ_ã½ï 5ÜÚ'uš&¨ ýt 8¶—U-Î(<᪜õ¹¡f ª�Iḡô«Û¹¡òd9ž&®ÇŽÓãf^úRùvž)w>õÈÔ™çÂùã�ÏçJp†—ëŸ@ùæÕ½ƒ»ÕGøÑ"ÍóluœO”¹N�š|ÿ8ò¾>a¹\§æwŒŸ8ì®Ej ×.{b§eÔ…ì(ø5gåÞ}ϬVEåIº4 &ÍâÁÓÓ'éòéh¼u~zK?ÅÓ¤Q'bû€²ÝW•Wlõ•ã:­.%ž�\Zå"þx©1 öx~¾ž© *wM®ßÓÛK�=r&y™ vUïÇ$yw²ZN¨Až>i@Ãh¦O?*ÆõêäÉü‰Ÿ¬0jÁ`€N¿\¡�Ÿ'»¶ì‘àYËç¿`×âÌD0 ᨡp¾„ã€aŠWœ•¾�{×9öÚ:~T¦ã3ºÇ#Ä U<>Œ×…F›@涤 ¬‰ŽƒƒY š©ÁVƒ 9n"(ËSt&€%"pÙ€¢Ú§5#›ž­´-Ô[¦ ¨Ÿ¿¢h] FãÔ‰ÜÆ„`µkñS �¯—B'înïeÃǃxÿŸñ±!Q4fÕFhtÔ]]•_9Ø^fÞÄ)ØZvíLúm7Ñe Z™Œ…Êqn³Ú¼é�ÔAäOá©Xº¨ú°v¥ÜŽ� ¹:4Ù¡Q5@ÿ�b-ù’á³Ïౚn” ŸÞñð¥Aè‡=l„Fþ¦¼êj“ÈäÐ%¢�BÊ®.sC1{Ót–¬óf@$ QP‡QÃê>Li@Ù*ÉA°_¹ò‰Âô€/êÝyVÄßO $i²�ɸyNâ7Ïο{ö›mã�ÎyÓ!þ¥Ì ùR D`'Û˜¯³�?b0mëÞ½_¤¤à»f[¼(AëÐ…#ú6 -ËDŸ±`Ÿ„=r4MIÙðS°·Â}îå:xvå8]dd3X¨4˜{ù.ýeMÑì™jÏžæåè‘Þæ‘qÓ»ÚWõ"Íí^ö.Ë•bBuuú!rrŒÇ$ä¦|8Œ�í³’÷ÙpEüN Üìë7Ê1O+Á´j%*Æä«Ü p†¬¡Ž1Õ5›VŸv·¹B=·®3÷4ìùÎõjûÌ®K~­¹ñH€£TŒW&Ô“±Ê¬Y�Âóèðð–�œ—9Lâ0ì^hÀ|¼LáôQ—kô­.LQ›»¥£Ïƒ@uL–Ž øx+ªÕ#˜bd§Ø® êK-¸T!»M"ûc�º† زçs¼nŒ®„É/šÓä“•…ùE�R 8©y ždÕd½”XcÇpõ,5?:…Óe1 G’­I…_ U\\�|‰DU2¾?žÀ:íy4v@®ýPÓ:H’oÊ5ˆ˜¢iOK‘%l2\5—™bâ¨BW}±±nr4Ñá]¯W(ÉÒ+Ê2¿‘š'kLCØÈ’Øq…Vè ̉ª®/_­ë…cMFÖœ„'Q&­yÊ'—Ôœœ¶DËÌ—Ôäº'ãê²L~©ëPÅd'Q<(6KfÛ‡KËA½ê‚LL :WÙx­ÉŠu½^®¤ˆ„N²fÃÍÝ®ª‘Cl>Ͻl˫ųJ1©v,/eHÈ ?KáðÔgAߊmWüq÷¸Ùi]³ÑÖv µ’TÈˀޠʲNƒ‹ ÃÐ\�ÕJpõãYóÓìœÚæä O8�ÁMKÓNRÏлÞÀl3@×¢º ú�JRŒGål$XqƒUiôh£.â[ΘH–§hÐØõ²–S?I´šòMޱ�æí´ †,½,Àµ÷虨Ál7Q¢e”6 <%ñóÌȧ3´ù]¶Ž§ÒÜf^Š8{"×ÛÓ0®ôî°I<¸akísº§%V³åíï’«<&ûb?>Ýg@Ab" ãgûñKJ„ò|úŠÝÏ4΂ÞñøÌ&\-­D_ >>Œ�ÝÌ…¯Ö°Õ*�@YG_|v„«Š¿¡ZrQ·ñ·ûñ×0çGÀc›“³rÉ:JÓ XÈ­H'¿Mz+Âò¶ÞL@yÀüЋDà‹?9Þû4ÁO?ýdôéçÇÓ_?ó`ú& пهÿê.pÎFÜÚ ³æO*gÍ?Hm5ç0Gñs`'ɼŒß¢²C{ðW�ïÏãþÊ„<;âAÛ¬HÎ[”M’OáÿîðX¨ô*ü{ë·ƒ…NþßlV„íÿéœ�ÉüîTM$›Oõ¯þì”n�Ô‡—HÝCeÑ®E€€K^ÒÂ4»Vf˜ENÂû¾¡ºM&…7݇$Rœ–ÅÄ”[íI†9òH°µùÓ · kçt ‹=÷.[æ!•n9uS­R¥ÈOØu²8@5Ëæk±yüÔÝ`V;W5r‹ö07¶íøöùˆ*nN|1ˉ¼€�di`íHÅ¥,N¡›Âíï ú,9í h¢…<©P¥D~ûÛ°L6(˜)%12YúµÖAÜb&÷aëä<(ˆÁ^,M\' ;aµÌVVƒÞ¬^ö9(0G­ƒy#.ªqQEž¶qÀf3�U§t‹)zÂ'w#sƒ¼Á IäEIfYAõ¿CÐ=hÏeÏPUè¨göx�J60¿ÕÒtÚÙ?Å�J?$ÚÖ”è†|%ñ,CÖ:y¢¯êŒ&ÑeY‚Ú\˜¢�ÕøòëÊpë=H@™)}pN4òŠdwî�™0°¦%(ã—žÕ‹ÜíÀ‰Íh™Ê4§˜#è_>�?�McŽ=×Ý¶Þ ø™²É1=«€µ‘BæŽ=TÉ$ CNx¢�w™‘»ë�s&þ¶Z×Inlnãô› ¡�ކ]êÁº�x�2qö¦@0S0XNy@¹3{Sø{@ô3ŒL(–ýÎîI3²Á»Še–éïŒÓÖjüå8‹8£¢q…ø&x)T�³�ÌŸ Ì7ÀÁ¿ôó†Ql7XŒx€àv‰�Äup”·ß�=œfUŠ5rý€ßùvS@î¬B·4tœ f"„N“~p0’²ª Ü­V¸†0†�䩽#‚Á în5<ÁöÂFÕ+=tjõ­7�`MT\ýf@µÁ’�ï`J6Igò÷˜Þ„è™S;hål�)Ä»âw‡ÖýG­(’Úê‘<ˆÀSúŸW9§E¶VÄw�»´jz6ÃGu‰pT fÑÝè°¥awg62²,v/.B%W«<ÓðC•£À ߥ84úÌz–iã’RìŒy «4a7¤0lVÿ„ó©GµÆÍ£:UVý†‡S7ïÝ•{¬›á@:gH*¦ú¤³:> @þ=9äÂdÓÑÐ'›ÏK8ãÅRªDPÏ i4äDKU§EROæMrýФã:/óWëÂ|IYªúÃ�6•óÞØÿPæÌà$Npéï`‹fZÖ嬈ѺÈ~Y§*@ÑÉ®ÑËÜbÞ»ªU軎�ì™JDGöµ|8*LÅ¥ÉjnaVäTf±Å ¥Y j§“dÍ—u*n+,è ©ìh€vµà•æ1¦¬‹É‚:Ä bNûܪè«ÒeyIê :™–c€‰KùèÛ¥ßñkcaQ‚Ú€3óŒuFö|Pèý¡&["N®ÑªØ3)ŽR¸5²õU¶ ½ÐÄ…(Ãb™LmÝ`ˆŠÅdóH²Œ‹KtDÚÂE‰8*ÚWé†Jò(f  Ní*™1ªȈyã2š¡© l+Q“Û É@2‡ŠyÍJõf%í °Ò+L/¿±x’ú‹ &%ܵõ:Í&›)¦Æ=¯üÂG·ÈTÓOÓ&Érƒ»H̨+V€ÏÐðbæáÞ·0w†yŒvF7FCŽyó/2)8Y Æy•€l…3Ï×”cÀG1ˆ$ü_Gcã„ÂzÎußÅ„¼d¿€n=¹Û¥*ÀòÊj] Nrβ¢vÍF`�£,Ë7‡Å$LÔËÐ¥éI–1¼íˆÚ5h€ž3*Ù§¼ áˆM0cîÚ.¶x:…4Kį1‹1#a6d³.ˆÂ©°¦¸Ä–1—m䎓qyyï-‘l›#«Ç´=…;°íô«Ÿhf¢¦©ßÞ¿YNÀ´iÐsoÿîŽòC“q(L&gI÷Àм>ÇfQ,gŽW?îìÒd'À{ÏâÿÆsçñpÆ—ç·�ûöC<ŒÁY‡žzxO0ÜÆŸhçú@g¢Ñ„ES–|=Ñ"vpÞp6è.â’ =`±œõô�«’Æî$¨”ž¦¯ã»à`½ƒG˜4‘Óºî›ÞÜ·Z€ªG‘ySãU(G±¿G¾‘ó‰JPҤОoÀ ²)v”Ð�¨·Ä Ÿ®.ùco6’èÂíöYrâŸÔVÉèÈ„ )¶Þ“fÌ[Öþ�÷£ÇÙ·#Å1fíP/Ô¿q¾ãV¢´ :v|ÄÕH­Ùɨ!ù@öódÚ“é »þ&Њm�ฉE#‚í±¯�˜„*õÜNÜ*r“Î|$%щUcÒ4Ƕj×ߺ0þMÛ²�Ú͆g5Ns¤o�.‹ÒwÈöPN‡Y—V“�Ë1õ²Ä¶M¼¬2Rš´¨Èä‰Ôl48"–£áaö©…dœ Bõ+;¤4T£�²²Ãƒpù¡Æ½—ç350¶¨<’ä%«Z9ì Ø¼ñXúÁø^»I‘r¶#ŽñâqÏ׫)îªÂ}ãÁÆ­Iÿ²'0ƒŸ1—·…ònª4Ç|„ÍIÏø„4ôd�Z ý$ZZËÚìïC¶zˆùž —™¯ ûüMÚ³âdwÖo‚³öxR oÒoz'ý@¯ŠÏ]È,)Ò+—{ý*b“Ôt{Uþò®Dñë90'I$OZ46É53 n¡Ú>NWÿ·Ûý¿ÔeCVгàù¡Ÿ2ûhd±ÃÈRV™ÓÑ*­§@‹·åº·.°½)w6hHc-ÐØ·É ~Çn�`tóë»Y·úöêøŽöˆ3½†C1…Úû‹Õ‹Æ3N �m…é÷�]¾j$Frw�nnm{/ icÜëUž _ÒûV»ìÿÓømZ¡-N­(�§%?'Û›èºpÛEl·»žÝ0ØÌŒ´­åÜ+BeÀÜz!Ý,:ÝlmîF÷{ò® „ûݰÎdÅ[SûúbN<{œÔ¦Ð“ËYÐp)»dqß¡@£W"­瘞©ÃñÓ¬/5�X;Ÿqïtô":‰°Ï¯ ìû¨íEU‰[›DE—ÜãÇ¡m8õ·Ám"ivëÜIKŸ•yέÚnä¤XØj·1UÐåRœï¼-VP‚ã<¶#�Üå4T샂µ}ê ÉŠ ˜xJ5åÚFSç“ͽ¢A§ÕmèMÞ%7R3°ñ%ЮÛÔ բ̱™a6 hN0>Î1¡gU·q_Ž]Ú'ðÈ A÷?Y4´Öý/A²—ËÍ.PI?ÎQFî,ª�¢ísÇñ7ÜâZ’f�ùh|‚éФóš—¦£±Ë÷˜&´h·H'i]IHÃ`×6t-?kºE]1h›Ú»WìiíÒÐÍ©}Ý Ûð‰Ìƒ~ŸQ|díƒ8]/Ÿ+g¨ƒn5eÃBêî,²ԉÀ}|»�qÏÕßàýôݶ‚[;u¶R\Ú¬²†ÖþÌòÎ’±DV\ušcp°&‹œÔˆ÷$O›n˜CJÞËŦR¯I+ÆkØ"xäVWYí–Áâ^ÁxÙÔ+.íò(§ÀÔaƒ¡à¨ËÍÉ;Aš°L—NKJ 곫ÄMàS\4ÑSè¤\ç¸R¯ÑA¬mOé† ¬Ë€gçóT\˜Àk–¬Óþ„-¢ ìW6ZØq*±GAͤ[ãEfÂT¶C%Ô¢ƒ;Absêe¾Ù¿‰«¶ò) B¹`ÛàzцK£Å$˜ÂËvY7ËLÍ"B²jl‘ÀÜy«ð=–BCê†Ë+‹úÒÕÝÛ^†7ëgQXAóN¦Í’ú'çöCq!¸½} ǻ֌]Ó�>0 u›mƒà ²~.m=x¶ŸîKãçV¯ÿ0±·alCàÛ Ã7eƒê17Xç#M 3Õ(¬æPã0‡Š²"Ú>YËѧj3ÉÜè°fæTh>§!ƒÙŸÎ<¦/O`šDÄè6§Fò8”¡»Ö ±Ø ¸,_8 ¾^SðG\QÔ:8ÿ&i€³[´ÉÆõ-Ã~�cÑJÃ>H /Êi,{žäµü%1àº.'­@Ý>òûs$�Cù0Ôo© Ù~‹úŒ|G“]´IaÅHçKê½Þª‡¾|/¥PƒCôÃøõ~|hüÖàºÑuLÉO«¾Évf=><<¾kYÏíJZnp¶mXmÁê¿´ g‚®lz»â@w¯#Šfq}Q4݇º£hã6ú u×õöÝzÊ»x]>–»@Æ�r£¥ÃÌw­wñ�‡á äþŠz+0‘‰81³êJ/FàÂôH´¨-Ó1Ê©-äÛ½;Ýî¡YÁ¡9*Ç1�¹( +{¢±7ãÖÖýju÷c½Ìú«w‘Þáííß~H7X�fjã�‚¿hvïu×uÎî¾ ýX~ãÖD�½q¶¦#曲ÑKê‚Â> m§ÆöÛºÄB ºYÏîÚ`s¥ô²­¹jFwìq|é�ʺª ’…ym€óÑá!öÕÈ�:OvŽDù†ÚËÍi’ê“Á7¸…n¡ ¬¹1ŽÈ›˜ŒÛ‹ÕíÍ+êè ಚwV ¸0î’œiY˜äièç‡czwmù/}) XàÕÂ:û>œ!ì­ÓVP鸇ه *Ù�Ъ ×Kküºk“C輫ŽXý·–ô¯Ð’ZzÎ]‹­{õ£Ê9½c3Ñmô#�YÖ–:Æf žÐZ1Í>´Ýyhn½\6¥Ž©Iîª-w /“Í'kd®&F+7{n#µ�‹v»{È��˜±@“7FÆT©ÞžÕ$´“^}CX$òt¢kåEïÜT#�²øȸѫa×C~ɰl•‹Î*øÚ(nmE™,r—¶¸Šb¹ÊÞÕç:ŠÃH�ùÅÑmÙµ6€R±;¼%]Ðq1TÅ!³— õh3·Âø[!QÔÂ"gÄÖiÅο\ wÕ™¨ý°v¹NæD iDÂ- ÂdTs㼃‰¼‘â½_ÉëÕÊ.è§5£•;Š)A:2RwœÔT*Ó«„ЮêSž*âçöû*Tš QXþ{�^ýö–ní9¶¾7ø@6ˆÁÀr¿ý/Qþôn�›|+çrqüŒ¿¸ÿž!ŽÏúÿý¡³kÓŒ üÿŸžž,—?)xòñò °òéì^A¿Mõˆ�ëCªGÞVå8A—=8îE‡m9âÌ]¶ƒ{ËaŽU (dÀî½}ûüÙ s �ÚãŽôí½Èäö¾=ÖMºr¡Ž÷‚7ý ãà5?almáþvK^³ €»ŸØ»å²–ÞßÉÎéxco¹UBÊU»rº§É©–8ñ–Iu�Ãi¤ÛÝ™·�^Ikj�L%ì zЃ#Éì’¶Æ6ƒ‰2\&ÒÎNÛø«ŸPÏXµûÀ=ÑÛ+[Þn«^ù]E$4Ñ©zQlÑûƒ9µh§Êƒˆ­ôò[¥Ô÷‡ƒ¹!ˆÍ·ÃÎ3†Ur;1aî®Ú–„©®•V­’‚"c?e/c¼µãwHÞ笣V‹¤­žkZo´-.k^ê«È°Ò£ÿi�×�áº4Ú™ó2xúvS3E½…ªr¶(³:X6Ðó,° ›œïÜæCÔ͇)¥Ù‡6]³™ÀÞ&m.3¬€bã‡~„¯Ýç\Ü¡ÎbôÓ;üÒ{Ìd‡9ùù:æézéåÿ›[Ùû*ËÔ<|÷æì~îd(r pcv/ ×ÄÜNtt«S¼SÐÎîoeÃ{BÊ#¢{ìvjt÷RŽa¼C)Çð¾K9`hf>z1…Í+tÇfQg«kl—]ìâÌûÎuÚ|éq;iÌ•¡JdÆnW*w{i·¼”ºëc:éh!�fs/5ÀÉ Ãú“}L¶ä ålÚ;‘@Û¼AÙYÃÖ²ÜipÅõƒùæ¾kûÿ Þ£äÂßÅÃp�Km‡Të+0­Roã/xþýËW¯Œ€æŠ¦}íéÝl9gÕ°žÿî¥oŽý+çÜ’´ñƒáÉÜh·2S{gÐZk”âRlýAµ7§×o=ëíÇ-k+#ÓÅÖÇ7¥qxV' Ï9Š†Úžgùã’+¿{vþòô|g.I–�+åÃI–?îw&??ûÏž–÷ôJÚ�݉þ{Ÿb‚X{?eTB$þs°µ "fêðŠ(¢;TQÄ„7ô»-—�ûö )�Xµ+#âbt„WÕg¦êÇÞH’�J±y�™ÏË5l 5”‹¶ØÇ܇†‡Ð´åK¼õ#ÏÉ"– ‚$¸OÙÁ+ì\¡i³­øI�mqð§öâwD˜m‚ã1Û ç6blê‹0;HÏÝÝÿ¸_ÿ�¹T{Kªdyä; �€ÍWV3êQþWKˆø°ÞO)hpFešê~8Èé)|ÛÙ»iOÉN· *hù e´QkÏNƾú$Lgl¶Pº®È¦ä~‰ØL¢ŽËXÝ0ý¯ëxݶ4Ø-¯W–¬×f'rå±6é¥62n“^ç6cÂiv™næðE­^› þ×`ªÏQå›TlP”®Q“gM´�ºA›ü³çÔzûùù%jNb;Tî‰úcŠ÷DÄb¹qˆ[È·0kÍ4“£\‘Ý�ýFËÖl#{S"¢³S×tª—ѯàï–f� Uf©V²á;JšÉ‚·‚K¤�)ü…‡ýÚ½\ü–¼¬ÛŸÉm“.ŠckËoR?›¿ Q÷&ÝÏ•ÚòUÝA8ž6µ´“œ#ß+Êž¦Ç]¹rï·ØúWÃÚË´�Ü빟ÚiÑjM‚-·ÎÙ¡Ïoš¢Tt?�se¥×ù5ºå”㛦dÉ4t·ö·ÜreûY ,pÊû>šÜ«(£�ÄÂoé†é%Pá§ì‰©ÀñSž:\Ù^ÙAWw±GÃH‚¨ ¦ –±i_¬Ü%D×dw Ì ,³›÷Œ;T·ïöœ•i¹»%=Gá �ÉØâNª©¶¿voÚÚᾌ�èan†Óv%wêò#�X󽸨£Ù4¶=ÒlÝ;ƒø¿Ïî%i¹Ëà¶rY½r7³ ¹s¹¬ÛèÆ�ˆ„·wè–7@(Šªsc‚¿\§¥ßN ý.‡ôî‡ý§ÔH9Ò��9t#Ÿ0ªN×qoüÓ‹zŽï&,üýšÝ†ÄØ.�¹M`å›ÛÙÕÙôÚòÚ±c'S÷âŒìû˜XSBÛy6—»µ…ºÙô5@ÊÀRzî»佖¸ ÔÉ–Ø›Z›,Èͻ؈ÒDpßÑ€¯;ßéß6€µ«yìxûÅJ¤‹Z“iTíatäû–ú®bã ºŸ²ÁËnº©Äbùí’hõNßmVÊÒºäÏëéÜ„M=�ÑmH�á_éèlJ­�™ÔÒµÛC*#ØM6⯭5¹ã zd�‰º1GØØ=1·cUuŸI×NY߬€“”kªç¦?y*‰çÔŽ`Z¡yç@ß2_ðæÕZz·ó†ÑøfË´ÅŽJ`.¦Øœ:¤.Ÿs¾ÛÑ~…Ý×°ý§ó-�[U!Óœas)¼í7Õ{@¨-÷tˆø)¾!Ãyн6«oqØ¿O`l­“ÏÈ^¾„¹i9^(_àý«¥éІ&r�ÇŒZd¢v+ãuA·kkïNªy·íÖ“MÍã`k€å�é’ø±`Ӝߦa Úf/__3‡ÓyqH9XËÞ!YEñ8G6�üL¯Yz§ƒ̆;Äæé¬Á¼$Åç w§4+Ò¬“ïFöfÇ��ùeCs†4†>y‰;Ì”Ñú¬¤íLòÉ„0D\ï2&¾YaÁ@€0%Ô­VœÊÊÑUMvBMwt7„¼ ¥NR?М�}ky9—)/Wäüa¿?ö’ÄÉsÿ¬²Ín]®ë6_�‚×ó^•�îr–÷tœ1?KJpÔüvýSõs<Нúåçß´^ll¸j¥�ãnœÁf"s3HçFaÇÖÐ vypà—x;_ž;›€Nº$/'fؽŸ\HlS’l²Ú†�®(dëÂô�é¤RIØÁ6 J wÆ9ÌЀ2®9&˜öPî™hT½ “À‡ E›9§ž{ŽÄÂ| §«‰^çQ¹;…|t æùé1py•½·e^’«™¥•;›'€´©u]x/‘°X½ßXX¤ì*Ù`HåüCD©/¸±}.Ã8Y–ìü^RÉtû©ôzï�ö2lÿ�Í—tW™ ŸÓ¶í Ä2¤hƪÆÝW‡JéX= 5v›Êtà©ê…–woQ?²‚V�mhˆxoŸ�¾÷†iË 4zƒ ¢GÄï u„b$­$ð÷+À~ñQéMߢ ÌÒ+ºw‘ÊðãhVÁ'& €ßL§.Š Æ,2éÖMžðh‡7xÒà9M²w¦tPç™ÕO8ÿ½#à[«Ÿ*=ÿîc¼˜qªá”U•••Ídçµ}:oïI÷Ì“é4çÑÁRÚ|Î̱uY:€D~B¤Ï0 Px�£Þì½³ÉTÔ™aº‘Îz>'ÂÅ]•ŽúÜÇÛýóàµÓŸƒSì"z†>VeŠpïYF` uö0h›X7ÄîQT‡£�1ÀîÿÉd½¤Bâi�½ÿ©HøÍ‚X¾;mÉJGQº£çQ� ð ‰=þåïñ¢„½¥|´“ë‘38�QEª_Òôe¡ìYÜ�z½B�Ê6Bý=ý瘽à Þ,trG/Ô×|Lîh¶µ®�Á+Û0“uQËõ=±ÏÙ¬ÀQܹ…ð'ÔÊ�ÈÌß�bä:�i&¬� ãi•`¬F®´ˆL©y•R“›%¹Âq£yg¬®K²¢\ƒ`{(ºŒÐkÍêÍÐ!‹€[Né[ˆ 1äÐ ×ûõ«Zl‹<ßq¼Þ…õ{ᘸ©ð¶^…•ï¯3®{‡L9›õlÇöÑŒaÇe‘õt#fkYqœæ®äM–œxE"Û2^°`è=wþè“q¯‹™#ÚÚÌyÈs�…Ÿç.šåÙEÊDõè�ðÅÊÊý›ͶÔÃñȪÛÕjfÖ}CQ0àdâc€˜du¦W#šD+@AãŸ:_¤þeNk›vbj¨½�e¦”LF’2ÍÖyû¦POÓ]07�‰â”86ÌöÞy2²�Ò5 Z}é; ÚµXîi}…�jvOa7Îk¢Ú¬àÎÌíl/áËtåÕ‹jxÇ ¯5‰øãÔsØ©gåשÆ,qâý3Á O¤³5Ù¡HâÜú�“º*µÑG3Ä! ÃÊ #Õå dG�¶éµªÛôÚ×ûñ·%6m+.Êaür?>Ë–é„–ý;ñŒ‹ìbt–]”ÅÏxÃ÷@ܰ߿Žõå¦\e Y™E/)¬Àãš8�Žw/¿{ýòÕ«}²ž>“Lã×eyS�”Ÿí òÅGÅ4©'� ã£/¾øÌKïG »«aü9Ø�tÆŸ�âä´3ÐMÜ Qc=­Ë›/ÔMãïdM `?údïhtøñ¡Þ¬)OÿöÛO¯þy |Šþó—Øf`ÓSsEª-3 \O¥z£¸OXâ 6^¸J¨VàŽ†ˆºs뱤]P>Á;#P‚…UU�ÖÕq'Ú LAÖÂIòFÿ¡z�æ\ÊN¾d–ôÏe„Wë~¹�;&_k‘òw€M ìú 6‡ëšõVlÞz*çf< ûÞ8›nU>Þ^áŒøÌ¦ FÿŽÍñɘç]¥¬N?¶+Ý"¨�SçÔm÷2êPN¥Õ3aGÖC� *t‰ýWõÝ8¯‡ùOxïðã+ÊË9`é"tµ‹ú‚“î?2cØÚ^/â½cóå¯vÖßQz½zØb‹Á‘G"ûõŸÇñA|¬ƒXM›Øî4w»Åqþ¥£Ï5΄¸,ñ†Iª[¡�rY'clÝ'çîY¿ævmf}§> ¯÷YÐ[B:ö'»6ž–Å×ÉLÉܼÖÛ¦Â:PŒélË�ü9ÚÍ™K›ØÑ'ò5R½aˆmS#”Í é( $Aô>I*m*Íþ&‘D�'­àÔiJÙõ¦éêA< Õ¥£;mS&ëõØ{»J“é—ÓÝ~´çûà Ê×U2ÅúÞsíöÐÉ=kÒUçWç°Îšr L8Œ>—ñLý« K £˜ïÄäÝ�WOb£Ø!ù]ŽA-ŒÑÎHgx-ÃÄóWåÈ–¼ ›xŽr²o­8Í1ÃÇ:ÇÎÊy8ŒæãÙÜP÷p’g~Ycï¨ôªqR†˜5‹;M Ѓø¸Ïò#ÇÏýÌó"Þ{ƒ×SjÜ‹ø/1~:îõT”2|`毙âÒƒ€k, Úú¹Rä-µê™î{!SƒÓîêt£„q~®ë�OâÓ$€«�× öp"éCvS-Z%YÍn5J£-¯RãNz‹¶/¦ÅKœ½Ó…4%è¼ÓÚDç¨�E/âH3*îqX*úX”×a_âj*9í:Ðf•¶[&àô‘¼�~[6h²㱎¿Å�ªLú�$”õºUXÏÆ/‰ÎxÉ-Þ¡%ñ¤é�ÈåìƒåÀ/&3`6´{ƒ£»;Y$˜ÃAº°p4/9sÉAÇî Žñ]š7e:Bº�â²§§‘Fâå¾ ,‡½!Q7)„�éªôÃ…þô[ûÙïÔñ£Ã ÏÚ!æ–ì1£ºÜ€½º×rûâËÉûl3¹.Îé¶ç—uFµM+{9äk÷è¶7øZH9Åԥ̖Áeho÷™c–’úP›5—úu€Ÿ`óI¿O×Ñ�gÖÃÝEè¡m’ã_¤Úh x ‰„ÁÈ�ÝGè\Bà9 %À˜˜„“äe¸—…�=RêÕ½ÈP-›Eýkj¡ªÙSÍÅd§A«Cï¶qð4TrHm$ÐñÔÉîtËUó^=:»š%Ë,ß<6¿¤Ù�â?92”m¼ö*8ì‘¡A}Ô2`�ʬÎÍQeLXO°…�ÿèìï/,@SE³­ptÆaª¡5h*åy†Ï‰²/ňÞ"um-=iõi6é¬`à1=%”Ų+/^‚½Ð”†æ0opC 4Ç?M[çLZLg~2=A…¾BÁ”:[‚©jÈ5‘>d iÇ¢º€õ”c“ÃóM:Ü}·A¸—[|þˆÎÂNÃC·CLŸÇ¨¦´rt¹Í\‡‘ûõíüE�w1�?Žqç̰5w7C>m>À›t·é;}õ‡Óß”Ö\¿_‰Æoí½€_Ëjãy´ú¡ë8´Ị̂ƣ+RäYMÎÞ¸Sß=ü¨JûŽ7 ©;¡ž,’8à�5Œ¥ééÒx»ÁR¬˜&éÒø7 ›"åñò’²UÉôªÊº‘£›üS®9.I¼” â-Ï´sÀ¾M–cËw™]†~™�Òq³quRÔš[ÜZG%!h‚Ú$¢ÈÀÝvƒ_mb“2ÁKQÐñÖ^s_èîG‡£ go±âÜ@¯¼$ìck Óvž�v$¹s7D)ƒ>í>vo2Ž®ñ½‡1huSLæMÈŸ“øYYbßSfÇ’Ä¢ówÏ^Ÿú¸„ýæ[ÈvþòLýq‘>E}Ò]¬Ó éÎÀ¶²ÖÛ”PÓwÔ��oÃl7ÀäÍ*£ÃwR)nKʬ�Á¦I¿³³i�àèŸMÍùœ‹R-™Ô1ªÙ¬XæSËPÓêõûûÙ¬+ð±x™(SÛ?{ ’-³ ÷ð .õ£AêVÕ(ß”íÜéaæÅ„€J0Ä9‚enZ¦gœ²3æd¬’�a/¾#3D G�ÆiÓºWÜ…�í*fª-¡_ŒvŒAé[SƇ–#ð­ÛÒXJûtFVͲF)oš©ci®¤»µ2Ò=ÒÓÃ@¤r"�R4-…8�;Êà:®† ŽÌÕàËHtc焉;ã£Cw�;T|䑱ÙJEþÓ.•Ã+–¦»›o46¿X7îFãe™BÄz™…ùIŒ ΩÀ¾NùÅ¿ _ïXøúüï[+…¾á$_\ìV€™4]¯ì…d=yÞƒ#¢”SÞ×~‰QÎBãQòÒ•ޤt¤cäˆÇã'�\š‡ý,i“«‡qð1ˆëfí÷¬C²¤Ûˆ*¤aí¤}ÑÍ69s(AÄ·¿™¬ ouÓ1a `¾h€}@`L½TXšžÔK xØŽw,^{óü]ººM]ã›Ð.~žTyè×Ô¨PÕ"7 @¤)Ò«â u(%®�v…³ÃtÕÃËqµ�„u/?rU„¶�µ†L.O¢*SmÂÂ+zëA‡˜_£‹vô!2¦~D7‘Q$0èKL‚ÿ­Rªœ¥-ˆHOYR‹•¡R0.娰÷;¢É ½ùh—ºM½è=Å—”Ä‘cP^ÐTš®ai&éùMº2¬`œL.ÜqÝ‘¬�Äñõe‰åsíáv\ð·iºz½}µÏu¶ü [œ©M¢�Ú“ºÛΟkj+•Ÿ�:QÕ[´c^€Ôñ€o±¨ï×ÍN«*P- ,Ê8Ûü¢bb�� å̘ZšŒËf±#x§¯¤šçìH�kà ÖaHF ­s‰ëuó’�ClTi’�™ˆÎe§fõHö 3Uýö39zÿƶsQ%To¥sÈw?¾Óuž¿�¿òˆÐíZ {“ËmpÆ©A˜€;J4˜$gxD�¶µaÒ½¹ø³¥@2J:>‰¼HÃÝ‚ÂaÞ™AãÝ“(á’ÆtùáN~zI#Œ•%è(œÖ‡„Ï*mBWH \;nqç–¿À;=†èú^PFŽqSÕÞ{èšNÛ ÛÍ%äýY½Ê“MÛiïÄ #¹¢ÔTF(ÏÍœˆ#vÏl®sUEØ’Ã:ÒBni¬§¬|¼iwãhÝKHãÙÀ¥D3œ¬Lã–¤Çà±rù9Åöéº{wbÉÀˆ\»#|Ñi €”Œk‡‡éÕhÈÊÉñprRüd ¿;@¨*þ™#uÄÈÜëQÙ: ]eâP7°ZS´ŒÆdÝøöÃ:’f¢mi±¶Õd鉗WƒBGL¶SG—aîO‰¾e� $€äH8¤Ã7A•΢°¿Æ�êA¤Sä*ù(i媠…¬§ŸŽîY0Ýç~:ŒG1ãd¼÷|ü-þ &i | y{’uÛŽ@I;¸›†vø�0¬CH„Û-Ô\íÖd(ඈëÿ”TdÒüêÖ[ëb²À¦cSƒ®nê‘$õQäv3ÛO¾–¬�”hÕèŒ#?÷¬òÒ!æÌèFtˆö‰5DGž9dékÒ !±ÉåV�5e¤-AÑ@ÆÚ“ySÈÀ÷vmOŒL¢þNA†eÚ«ñik»^ºpÜ=Ý#iÀê�°õz3]_éyÝíCôŽÜ�&�Z—0;ê�l•eÑ›º»žIh‚‘€@ÑŽ‰è`gñM Æ&¶Gg(…Û·�ǵ³ZÈ`±¤‹AÈPSÅ@‰Å®r$ˆÌÑ…Y5ÔÅ…µåÓºe³‘`¯‘h§f#¢;xÑ*U¨?q˜>%™r‡6õÌ1JÝËô‚Žžd )àW1ö‚ÿ¨Ã©êeqÆ\f�$®vž©åÀõ÷o.J*ô®æNÒÜ Éø m«'ö‡:Õ[~9C›@Xš=tîl:ó‘û™½6T÷q7— ˜þ*íÈ·cóË]ö‹¾G ÓNì§—¢*›÷ÑßÁËòW÷ÍíAÆ•¬¶>°wR·_xg›Éé Ó"ò%¾¤o‰È;µ"6ï�|@#w˜];ŽÝg½KSŽ3=ÆáÓŠºEÝ_à ùý´ÝˆÂÿâ¶¼c¯tÃZþ³?¦éÆM�2N·_›-îÞïÜ#,ŽµÇˆJÞFFEÑ~mnèÖŽt' ­Sgþ¹møXä'Ä]ü¦pœž+�”mú¬.)’�w¥�÷Cz‰x�;L!¬Ä µO:j]fgWùEküÝq¼J�7—dÂ�BÞ«Ÿ»4­ŒÑf' [,…Þþ5Åpö˽z—èBkBÿ'4ÉøÓ7Nøw�¾ø­œ”é×èÿ»FòÿN5úViú=+õë¶Úw«ªý(X¶ת}‘œ4ˆ[k‡Rû¨·Ö>¾}©}ûRi®µ¿{©}tçZû–ÊÉ–QëË]Åôv¹Ïܶ�›ÿÝwe}S©c�áÇÛYbÿp-"~½m Ýí2vÿò4Ì‚ó2{o?ÕÖ„b÷öDoôVVÕ­L¸f„-•)O˜ÜØ#LhïyIAGã ݽftvúr°³9g÷a ïßS.¿³qõXgx1WÊW£©ÅCdar%Ðöò–ísº^l®]]*t;<& ÒÞÁÆòݦ8ÇÔkì�5k,åaMáŒT’›+ ükÊþ¥�1Í•œg4•E½Ôâ°ky¾òíPgÿ-Htô�޾K‘1!ƒ55!­qÝÛ èbÃFÖ±o‹ã¾Æˆ48w/öÈ3-ŠéW†Òkx- !sfätOG}ª¨rì‰bg|´öJvm¸†} —‹|¿wª÷QX ÓÜ©=+=’YóȪNc”“õÂæI¸HSSBg/€Ž]Ü�¼¥Á\`¸œô\^Y/½ÕõÚpb 3­°U4) ¸ ¹y%Rþ±V ‹ÏwµÎ1c’RÔP3Û˜ù™Í¼@2ç"™®UªåóM†� í0õÛ;•rï8þï�o4EªCð&|Çýå¹€šÅ/³ªÑÞ › J!e Äfï'¬µ $=�• ¨Ë¥ÉÆj‡1ªôîóeüèðнb¦ïð1DÀ7p¹êkÒ3èñ¡Œºo”:„kZ’ö&j�ÐÙf9ɬê�ºMÛŽDÄ`%Âr°Ž±ÑaY7‘�UvÚlÔy7´¦áF‹;p‘¦+\xÅ×`i¸yß(íªÎEÚ„¼´„d yRÜÄœšÓÎüCé»^¤óÄi€YÃópÿßcG·i ÓLÔ1#Ï“•TàRw? ˆŠ0}©}ñmj”äA›‡Õ�&¾Œ‡@KÒB½…k³õS¢îH¢¼«u½Àðgòs,Æ\§§xT` jˆ•á@‡ºñ :GD¸k?]§q!Ñ`!{ Û"õ ƒhé°Â'Uš£šù&ƒ-sÌ ck„àÏœû¸Uy¶¥°�VÆûOtQæ…ìdÖ˜�ËÛÎSý¥úUÚlØYÂ�²ð(Þäu­]á€m™d`]OÀÜHc¼‡|Gž¾Ò µ<Ð᪾:ÓŒºxïí�úhl›ÿíógϵAw„×3“‡Ù¢xb­ò±R™h�—`€é–­’Üvý®ÅÁ¹šª¡9L-åaÙ�{Ï^¾ž¤HqùuÚÕégÅjÏ©¥`¢²Ém�¨(vÜÛ2Ì.|r8`'†F�…uD= $§}­Q^^'ó¹¨"!ÀPh˵!NÞhÀÐ ûí�£ SøS:P.V§‚� ‡�ï@÷lž¹O¡B[¯]ÄZhÓÂCç{öA‹ÿ��6´¡;šê’íGœ£tôiü19c‹-øÓÉtœÄ6#d/ nLx•G—,�`~”…Žœõ~¾›“ÙÓu¾šñ%÷AzF±‘áÉÌàô˜Sàµ(ë…u•Öå*6:Gl4œñ#›LØm„pîñIñz­ ì¥ð˜Ä¹­jõ[)\°ÆY6_kß½=W* pãÑÛx–6}xF-’©òJšN÷ê.ºke}Ïí$è(»C„ß÷ -ŽŸ>ç+ÆÄ´–{Wnh-ÙÓH’nñ߹˂î1y÷€ 8—Ó þ‹�?�þ?ö¶¶ª ÐÐ�Ž ‚œ2&bxœí}ksÛF–èwü \ßÚXÌ�z%ž$–ã*Ç�Ä“XñZÊ8³[wR ’ˆ@€@ILåÇßóìР(Y™d·fjf,’@÷éîó~õ“ÿóâûççÿxû2^4ËüiôDÿI“)ü³L›$ž,’ªN›/¬›Ùèóð}“5yúôE¶L‹:+‹$ÏšMü.�®' ||rÀ¿GOdœq9Ýà¨GOë&i²ºÉ&õã¸ÿ}x0z’ÄE²L¿|0Iв˜”Uõà铃‡9~j¾ƒg�á«U<É“ºþòÁâQ<«á¿#øØÀ “ršz“þ÷“ˤzúìÉþ3ŒéÓWÞ§Êûôƒ÷éïüéÿÅ_ÆOÆ.ã§ñ=ñ£÷ü?øÓàÉòä`…À>�Ÿã›Ù$Éc|=Í\wœÀNlê¬Þ�øÁiv©ëZÖ£GøÝ¯³Ë´ˆÝÉâ½:û5å¯.ø«�éÃR¦‡‘§@¬÷p!ã*mÖUQGñª*NéLâeÒTÙ$­ãrOèÓÙ,›diÑÔ±»³.HKw–©ìO_-ÒJž�/㬎›E×Ë$ÏÓºÁ¹ÜdS ïÓ´³F9Îè&'4Û$tµ‡ ñÞQú¤ª’bžN㬈§é¤J“:+æqYMÓª30¼“%cXÒœpÇe³œ¢N_eð{Y5‹²(«%œ¯—E ó™Å#®×ʸn* ª5ìô¤,š$+Ê*­×9œØ¬*—ñWIÔÚ4Uõ/ëò¤‚ÏFðgïà°ï’Òüô*nà\3àdêl^dpøI1Ic=ŸÀŒe1z–Âëy]>ŽŸÈWQU/ªtöåŸø2Ä¿�èÿ®ª k¹zðTÿbÒ§I€.\þ°œÖ°É©Ëä« wq�6˜¶Í†´_‰ùƱÿÛ\Î<õùȽÍu›u(«›gº™ŸÞmu[!&‰ ý,[³©Å˜$r›ŠkºÞÎ4Ï“  ¾¤ð8H<ÞøŸ§°*Dçx¯¬àÃl >×—YDÜl†@€>h¿±»¶}�gÄEcƒ\ÃÛtîU™HYDä“IV$@mÈ`Ümн}X%íÕ"›,âdìú:ƒáS`?e�Ff=ukAµ³¢ ?ѯµûÝ`?~Ôker´4›§Åe’¯•“ÉIÃb¢˜qÃåÀpþ££ƒãøc~øoòíÇ>ìÿóxÐzD0aÈ2B‡þŒ—nÒ=OZÅ£¸,ÒÚûrè=qà~’‘÷½-ÒâŠõrœVÄÚ„¯š32§*ΘÇM鼃ϭÊöí2… ƒŸÝ�¢½ñÔ –|€Œö1˜r�24©íHe‘oâlgMœ^OÒtZëN¤+8QBò»ÄÿŒ÷>9øTi)nYX¨ÂÁÎQ¦:s.ààÍzš‚¤¨qAIãA.xUÑ:Ó I ú5­JDJtU¥“ qÑlÕ •føàÔ$‰8Ý�_ϼMˆ°Ò¢4‹�܉‡2³¨“¤F¥'½L« SŠ!‹Vw˜À“l ÊÈ0òY!ü¢8FžÊá"À¸’Ú]ç0Y¹j˜´`»`Ñ0‘G|4�9!€#�Ãù�Óæ*øŽ¬.thWëìÓ,“£X&@¦ë¥KɨDøë4­³ ‹Pk:E.ç.e?­Ôñ¦¾ž Ì#Ïak4˜tYÖ-诇&ÎÅÔ�N£~Ò¦!Ç0`| ª`IÈž^'“P!O‹¹ª,+å{gÛ�Z³ÆÃ@ë:�­óxCU(õq…f'júþ2«�Ȳ_Y-ÞK÷çûC�Áè�_Fñ± úÀÕA\Ä�i“fÍ‘ÓÓƒÆðáÊ[/É›LŽòc�ÇŒŽ:µ«Zá®®W+@ 4Ä5Ð]œæé’Tä½<»H™—–ëfµnôÐyº2lt¶.H#Nüs ‡ ˨7K°Ê€IPD@CŨHç á#ÌQeD¸Àþc9M$ÄÙ´Æ7¼Æw@/�}8q³Y¥Ä¢d;Æ)+ˆ@Ù5 —NÛmýØP±CtiF#'FØ }é(ÂEE�ÌC,·ˆ�·ÑÊg‰³è’¼”í\&¨cgÉ 6YR;²bB"9=J`6�‡ë‰ÅÔ‘‰s2¤TPitÃrGp×bϘ֓0Ñ€Ê�$ˆZƒ ±T›/þ¥KõðZWõe\ÿR5{7ˆÑQH/í£9Ú¨—ëIÜ8)·@–!ãQ¹æÉƒ¬vdA�.³Ñ4�I5é~üy?m„#ƒºl¾`îdQ˜˜Hd¸kkOZbd?tæHcf1-Ž‹‹‰Ü�¯½iúfAQc_B.lqêœ�Õš™,a/–`Ί…¬8|^Ž)³ˆ/Šòª5"Rs°”ê ö¹ÃûC[Ô’•éÊiÖ‹ò y[��qyZ«l7#ìÍ:,X¿öþQÞœx°E NN�/ ºb3�xœµ[{oÛ8ÿ_Ÿ‚—Ã]À±“l‹íµŽ�4mw×:Aší¶Š‚–h[WIÔŠTR/îÃßÌð!ê'i»@[Û93Îã7Cuú·—g§—Ï_±µÎ³Y4u‚'ð‘ ÍY¼æ•úx§ÖËý§;שÎÄì­LDÆ^]ó¬æ:•ÅtbÆ£éÄ®_ÈdƒÔgJÃ¥ÓX=cýu0!šrVð\ïIJXÖ Æ‘±Þ™M'iÍÚ`ÕŒ—,θRÇ;ë'l©`v Ô[즋ÞÊÅŒíN¯y5ÓU-Þð…ÈÔt‚¿GŒ†ËJ$i¬E>Û›NˆötR~7ßü/#iw@”þ*éK^‰B;~ÿ\‹,KËç?ˆƒ#ç�B–­��Ù<„3üÙ‡ŸzPÒ”U;¾K¢›{™ª2ãf|D3¹Ä¯Ž˹®Ò¯ãÈÌOÒk'X®öŸì—kÁô�d×"Ö²RHýB(6hŸŒÉ±›u¯#Äj%¦%”—µ6Àª+܈åµÒl!X"–i nR½¦© œ1bKYÁLÆ�¥P"!�2=9˜c•3ŽØL­nR%´ÁB[E²Vº„-6·e1/"”r•^‹ÂiÔ7lë,U (PB‡ÂfôlÄÒ±Ó>2°|@*ÁÕzT@øßæá2­`&¯VuOtÙÎ ¨Yn%^¦«ºNVY…Ïê´ä…ÈìÃQ„d‹Á³°F<cW ”´Pi‚Gg ÿ# d<+1zòŒ¡�ˆJo&d:¬ä 5™@ [*IZÁÊlƒr!çºHD•mÒbe‰*�KŒWc+󣯤±GNh7¼¦¨ß]ÊB£V5[ž¨ôOÿ„ ÛB—�nk•ÐuUŸèø+†!yêTù–4yj|ׇU{WŠ8]¦±SW*”ñØûÒ@•>³rÖ˜%³tæöô2å+øTf²r‹h/19k+¹Ž×ÈØØ@b—�Ðÿx�Ññ_ŒFì`üøñχøùóããO¨ àò<[.Ìv¹ÜÊ÷éâøÓÑ“'øyð¯§CŒ©Òä}ªÒEsì;¹æ)Øx† `ÏìlÌïF‹»–מ³�ÆG�“9Fž,ý“ ÂÝløBÉ É­Ì@u^ì–¬HºvYÉ›�§ö¡–šgýÇrŸŸwužójsõxôEîìÉKzëV*ë…¼ùKý•NÐÅ( \Ô…]’ˆ\²nŠ62AhS²–Ì_y^f­x>Uº’ÅjöæðLIØÕÄEŒö\Wby¼CÐôsMLj‡ÿf¤�N~â°Ÿ�ÂPù 8z-e¨ Í+YéSÌD°Ò]3 "¦­MV ag’´�œIäûâfŒ²§ƒàˆÈx8¸ªdÝÆh4²æú¡ç�b ñ�Zêçgä;ó5œ'qøn·!È«PVK; ûéGêmÆN-’€U€ÓŒÐ˜áÈt1Í�çj+þ¼r™Ì«†¡Jëˆ�7Î"é!ÅüŒu É;D²Áf Ć ˜³ý%^c“Ö ¤*‚Ù&{JW ¨°µ!tœŽ˜‰¡žT³¤+˘غyˆª:¦�/¤^û ãÆºëG“†Å$£þ¨‘¦«Ø+¯™€sCYì»Û2‡RÔùBP†Ïmv_}#]rƒ“ÛçÀv­êÄ×=ÔÎê ÌvÕÀÔ¼“Â*D‘žY– ‡bržR´…¶§F>កŠ$0Œ*ÂX!Þžuñ~×”ŒÅ”5k~œV£/¹P¢º&/Q‘·Fš”p͙ޔy)iFÌáQˆ´Ûˆer—5˜–@ñÀŠ ÄÖ�ilÛß,†zÒþ@qÌS'�p-V²"’fJoU%JX')ƒuMY¥„ÖÈžì~0üÁÉW)æ|cì~ËTé .pe#Ñ$͵¦n?�dèX”ÚÔ5Üéô6áÐÍœ–mšbáX\Š75®}GC}2& mÌqïÇ A%Á¦#–%”H1‰ëÇô­Áý‡þÑ[í[-{I\WÄb…Tg#}‡ôæÞ¤]’  ×�FEwIûž”ë™)�h†gu"(Êì·ºTÊ]å:+IfN6ÖXÎÐt†Dûªõ?¯A£�¼»Áxœ`;ö›e�3±/Vx8+ÛǶS5äL €Æà0".Yàx OÁads¢Í)Ž�OÒãW�â¬Ö÷¼ž)¹�QŸcàZޤ�¨-ŸZ¿²£AçbÂ(JÍ� Ø.«Ë…!“ãƒñáÞs–.Yj�¥T©mà&ÄJTÔ¼l87xÊò5<û,t÷F4V=Ú‡óØ—°ðp�­]"c�¡Ìxl#Æ(yãØíYÌiÌ &¦«B‚ÁŽØ£ Üü£Nal@´sÇq@¶†l‹M[òÚ‹jÍ6û~Aw¿/õûù¤Á{ÔÑ}ØK€Fw`ƒh¡äHǶÜüXÑ‚HJù  ŽPp$~ûâÑŠ…]z»€HÊÀ÷!¿·`lЛ�#®f2OŽhS2QP!EøIQ·²ŠM›…Ų¢€¦!& ¨,�a©º·¥t=*<·¶Ké4‡°Š"Û´$¾$].EEµj¸Ï1;­+Î60 þ¹‘ÕE:‘m{{@l?¬;·ÉIâÚog͵Oû$F·PÝæìÍM@ËF·z` C_ØP¼,P¬ôä‡ÛXߣüŽíÐ%W+³Ã‡#îÀé-ÖnvÚ!á6Ä]JŒëêZ�Û�Ýs_ @×�­T@Ó߬ó­ Ì�Ëp©ˆý·5ó¶Hf]¶~�üÖî¦��_^œ�ž_nÛÌ÷Þ_9ÛÉ[‰æÝ"àý–"àBÄŒ|ˆr*Ã}€Ø³]ØžiÈH¼7õ]<øZš ‚›þ ¤î(ˆùÆàwwO+ˆ¶™A«M;1bú¨lÆÐdXëöxXŒ+°à¶w²=é|«ÆÇQ›Qt…wÖ½îtp™ó #Cý˦…c»:Ïd$¶lZI™(Vz�MGÖË‹ªÁJ UûÌD�Õ`F8i¯»ö&a?/ߺ�_å µ?ÑŒ}œû‚�òû3oú¦…Ó÷ ¢7çI¢%A ‡zê )·ÛÛÜ—å ˜Šm’[æ¾Æ´0ðr×à·&úP¾Oþ1¯m”ÒI =£>0Æl_‘÷•–ö=w†»„RÿÎÔl ൙{ëçÑ¢É"^í’)WûöJØ÷ÜE#nIÜHÅ¢Ðiø‚`sfæåŸž×IV®ù�[Ây?Bê ÂdæMÚèÐÝËGÁÙÙ‰èÆÝ³8dûöô�½3p<½¼>pŽãƒ'fn¸Ã A,ß xø;5&ì’­ÐQ\xÿST² e[wè/2¢¾¹ ’?»2õNˆ÷Ûÿ# ÔöÀ¸EôA®‹½¹‡6WƒµüîR†¡>m«ÙÆ8klÊáÎðÕ„RÀ?…†Ü]$2Ç:JðÜ¿la}0‘ oYé�ööäòÍÉ W²A6Ýøû «N�ÿî·m]àÀüäí«wƒut¯ž±¥LÔiwN¶þ£û"ìBú|©ê…){-(I«ÞKÉ¿ã«Ã(¸{O‚¬5¶v›ëçM�\âµÛ7¼²¥…øüFð »‹¦½µLu¬®¡X°_ú¯¿Ý¾îKQ˜uð¥¿îB€�+åi«,‡¹;3óÙoŸ­�fþe8ꑹr·¾ˆG´‡ß¯Côâa›J·Ã×ëê ¬�˜ +»¤VÐû�Ó‰ý/2óoþ:kÆ èèž¼.YtxœíksÛ6ò;ÎR{†’'NÒ³«×çk3Ó^2©¯�›L?@"$±! • ­(7÷ßo/‚$(Q–íØSg’H"�ž°/.xö·¼»¸úÏûK2/Òdœ™F#øHYAÉdNsÁŠïöÊb:x³w׋¸HØègšâ×äg±Dœª‹ÁÙ¡ž<æÑ A=‰‚±(â‰ø;iL‚»Á%MÙw{ó4�Øîì�Î)Î=Ùk0ö.-È$¡BÀà20ïÕV8»¦ùh�W,€?ÈwälìB�Ⱦ&ØŸ%Ë&L� ÕÜ"§™Xä|\»ÊË¢ºvpv(×=;\ôÂéc) 6á³`¡®ýîÅôÙœ%I¼8½£Eõµi}ȸþSìŠ üÀÏ‹¨°< "§>û³äÅ©X¥cžõC/l�U·¼¢‘÷\,�yN$gJ•Žã$.b&ŸJæq±ÌèjŠº: €(¾6t¥bp²§`^ÐdR&­˜ÄM+à £zÄ£ˆ™æ<í@„�«�kÐ\b� Ï"¸Ä3š´Ñ³8›‘8#ŒNæÎ,¾†Áåœ'Œð±`ù5‹‹°F@"C€e Њ9-ä¬ËXNqE˜á+ÐJzϵ<3€†…¾~¤¯“qY�ˆÌŒ0y’”óŒ³#�„ZÖdpžÏɬÖ®¼02åÍðaod‡�©y1 ‚³Maª‘ØL®Ù¤3–ÍŠ¹Þ_,Óƒà†â(š�²Šy�ùIÒR  ¥`3– ¶g†R‘ Pç)¸¾2áI™f‚ìk£dtYø:87l"�ˆ%9h�uœƒH¢öø´ñ+Ê<\Ó¤d4R %jh‡VÖ=]‹%kíhãÛ‚Ì)H;Áä|I¦0Ò14‹ðQWÝe í‚+xö†å¹FÎËs¿iZU¦DY˜-Hàe5˜(B\y1¸.«lJ-D?²a`�,ܱÓY‹ÚYs¥†4¸H󸘧f5ôË5[^ãÒ^Ǹg)‘š‡6Æžˆ MX„2ZÒ<’ÇtòIþ¨‰(ΙX€éº“UXÁl¨QeÝ$l2¥ø[�RÀ:°¡?1°ÊÚϤÀae÷3°¢y ’Šmœ°Æž‡ùvºÑ™#ˆd<�Yâ2Y§t>#1 '0¦´rV†ÛZ_E¥nŒ»„Fx×k‰ƒíM1ñ­3¡™D�L ¤ÁåtÕí¶.?Ót‘l²f”1’ᣳ"‡ÑèY6 à(|ÅŸg Ð „©‰{#b�:„_�†oBr4<>…ÿ^â·W¿Ÿ*e‡æÞKõZŽÇÿžãÐ�†÷¡vÕUf“’ýã“°Z34°`®¶j‚bÙ¯ày'�°�aÖ•�«��¨Ïïë?/ÔÇÿvÀ5$Ã!¹Í<ê|oDp¸ $hSŽÑ.JûµàPiD‡/š²—ÚèˆÌ8£fÜz¡ßX­ÈOCŒRÁe²/Òœœg30Þª«CØ0qÁF•¥Ãÿ‹Mò#Ìsþ‰,Át’ŸÏ¯~:ÿvN’óÅÖŸv’�ËN^¾œœ€ò]Ìé"…�õ ôù $ÉáŇ 0rÇGGÏk–ô'º”T ^À‚�2¹*ÁªÅ€ ¯k·JS%A¿€ A * ˜7¤@F·¿,—øX:ˤG3´¾Ïù„1d¡�§Þ^^^j¢Ãׯ÷� AÇ'¯Ço^…äŸlœ—4_‘çß¾ùÖÚN.mç!$ ukÁ€}àïI³¹Ú;mþX�a¾/ýµ—«T­Zi}ZíÆ)[§²í ±¾lGŽˆK"£üi¢½{¿HÝ,q½+t z)#^ê^ÄZƒ6£¸eÂßUÅ„X�¨ÒQ¸T‹a°Ò•ïËP—š8¿‘bú‹*ë›a¡Uñå/�¯$QG¤¬Àh/bÜ~+Œ‚¼åLÞ»q‚øÀˆBB™ð2ÃD(�¨: ÂeKèñɤÌ!¶)– V„Ø^°I‰ñ¥Á ƒø–8•P}i[™WcÁ[èˆq¬òO0e`ÜO€ÖDÔHý¯\ü·Íü‘xˆ¬Ç,w…¯¼Š_Ò­¢™)ºˆËxfê�úÈ5D¦0 ‰Ò"¾9I�Ì1�ÌŠÚ©µò-'×mwè© Ì]E­vˆg;•f= x³d 9èU]¹…âJoRûT[6’Õ]z üµ—þ¥7ÍüF»ÖoFr9 ¶Ìë´ØÀe39�-_'&hØ+?1�0i-=ͱ}Ij^Êaˆ¥^Xëf°6JA²bAÑYÂT¸ Á~eL¥�ÃæLÚ V¦W�[0´æ2¥ôV²:öï°Û-c`èœ^³Êt‹ø ³†»ƒñßç`]mJV¼Ô@Zž=Çú$!e–ÄŸ¸xÀQzù5´�×ÍÅ׈Ýú ÏzÙ &8 ­ŸØšÐ¶)V÷Õfšø˜Ra‘óeµÿül1¾Vz)}*6Ci«ƒßô=Õ«�ÕЫ[ÕÓS…î­b>ÿ,‹}t÷¿½ß5 ÷t¤9Ø©ìX[ÑÛ,Øf£ô69vгúê¤+Oõ×Ç^5j_¯¿š«[Ö_7VN«ŠYµ‚­»Ú¾ƒ;ìdj—îêˆÜ]ÑsËŠb/´v)�È;• Ú õT×-ªbÆ Ú‰~0–”šêÑÍÖpuÖ7ã¬ëÅ‘‘ Ó‹¬kÆ©*�9Y�ìîÐ1ão«:WuÞÄÍp±aš5‡ÞŒö•˪x¸± “cy‹�Oý7¹ÿÆêÚ- oØVÛ.A½A¬*îç½Ë©†Ïý›ÃÖÕƒû¡­ÍNU vËÁ»ànõ¼‰ýÎ-$= v€x ŠøÈyBa�ð¦~‘HgÛ~üάºªw(Á쳜�ÛeÚ&}JˆAgÓŒÝݵúÀS†ܸ_®æx�§\í±çj –qVOÔä¥-³4Èß‹¼–SÁÐófЦa÷8n2ƒðB4³4çâ-¤i2þdˆ&3.»V}¹�½yøÜc£ÌF\ ž€Ñù°�›÷‡IJ?ÆPG,¼¼Ñî£»ê º]üvnÚ¥±Çk–�¹ðj×õ4¡³õ�LúØÍÁÎŒuw5apjZgŒ9ªª‹%cøü&åyu(¨+ëožªIó×tðè>^Uç šÙâJ¥’,ö*æT¬Sqä4Î"APÃÓ¢LùÔ(žsÙ†ë<Š%·à ˆÅÐ/OËtðKÀ Y“@8 ·"6¥eR ™¿âÇD®sûz v(´x=…–55‰óWow‚À/•ì5õ©6Á@ÙA=¬Ò„µüF–jJ�ZE>4¤¦Æ°œÃ&‘6�^‚ÊÆQé‚kdÐk›’úõõKEÞpÆÊñÕͧËò–M¡|¥Ÿ u,Ç©öÈéÛ–{‚Ûêº]›ª@½ º›2�Ú·\r"Âv‹Óæj_›õn@¹¢ŸÙëiÿüüŠä 4δŸ©¨¦œFèÓ±1¬„ýŠ9‚0m¥ÒBT&9$ >°1Ð…Æ[Úî:¨}×r»ãµ¯�¶›kHØ�,ÍN…){d£Òg«ö\Uάj&–Þ, d7)p&R•“ÜØ R8;»¿jeÁfÈ^ØiûsXë ±˜²_ê €×ú”ÅSoSÝ«-ã)•ô¦�J—ód–têóY»Oµ—¥ƒxük]S7ÕßLÝI˜sÑ"'Ì©u¾Õ^#ôy› Ù±6çItw^ý)€sšÜÚLºB;Òj¦q3lðJÏ;0'�­ÊJgRKS¤›ÌÁ[1ãô>se¢‚-2æÅ‰ÀͰà¹yÆà êÙX|…uOÔ"|Áœi�:Oí™vVš3¸eâ Ø×%‹gsy܆ÈoåI*M¾ŽfIÅ<Ôj0åenÆb-)Œ•�sí©Ï×âñ|`Ø2sÓVäTåu£1&QBz‚Ê” È(¥œµ´…¡lDTëk)ÒL¸fR=¤Q¤Œ¡Õ‚F_tU @ýEf.0ô³æ%'6åú˜;¾¥@òÏ( °I Zfж_•gR¦q. Ø©Òh¼%¾„Ã"@QÎLMqUX¡ê 4ÉÁN­|ÞY­.µ�©@ ñgÀB“/Ëóíc_0»`^sl—£�¨�\ÉuæøO�$ÿ#‰kevë%ôÅíÞyu�ºÒxã•tﯼê‡Ò×éÛ ·ÝZÆŒ—5Ú ºÃT�=³ œ ‚Ë iínÇB�®eª õ�,>°’ç¶½eZ˜wW ­¿"¥ï  vyÿÓö=>O h§Í(ä-×]CÖÒÊþçzk9^£Çtøðûº¾Â«�‚¾¥íQuzytÌr$¸é{ëýÜ¢j|—î%‘üÿùg'Ž/®>\}»¼ÿ9».VÜ›ú¨z»!,Ò­G†¢]�È“ÈËã³I™Ú™Ø`}E�=5‘ÉÙâÒ@ªò%;ªÊWGãë�ñc8u¡6œù¾ JäiýÞpšâfF†ê Š•ÇåÅä <²c N6žÔ;-U uU&|¯�wÍÉ,] !C¼Þ�À›DA4ë§ô–$Ý~¦zȺžÀˆ®L®û¼m ösˆv�>È^™SýÑ{ѹ—ižë?O΢eåÑD·²ÇÀD”µyFóÞ@ß‹Ë=UZô`è/.™ìø�£Í¶»å€q† 6Âg0Ë;ç{yó„æ¼µÏé‹Äˆ!XI¬`šý z‹âl‡©æšºn[Ü$[.ñ™qe±Zu‰²Àˆ1ZZ¦>Š2ChίÑðH1Íz¿ ƒž$e.W}Ø¥Ú3ŽÕm�SýêBýß>š¶ø›NÉ"ˆe[ÁT»£œ¸;3±fûkƒÔ­$].W}Ô*¥c˜j®9CcÄ=L5O] hÞ‚ì”)Í}ïÓCuÑÈK�'4ç}›gbÍöˆ 2âç�‰wlåöLX¾ž-Ë—Së?Ø@êB™²(4xœÝ[{sܶÿŸŸUgÜ»æ’9ùtÓ$ö¤Ië$¥“vYPRÑ’]ýOä•’Uv´\Ì).>]ú!˜y #5I ªÔÅQ~FÖ æ¥"cÑ‹k*—r1Ç?ä‚,V‰Õ’ŒÌû«†VšLÙyãÅÜÐYÌë¶ÇÄ®*«ÝS埞ä¬(xýìãpòê>¬¼>Ä ü3…G=ÄÓ«�)G]ïj ¤‚­?¯‡¿E!Kï³TJº6$üÖ9#ßâjQµSš•D¬IüH¾j4•š%–@Ư½¥šžYšV†ž.=?‡�”H¦Y)B‰´œ]Ó¢a$“ô¦2ì%†¿�«!žuN5)©Ns¦Ìûµh¤%§`IB†ø˜‘Áa‘#$0e+Y¥É5Kµ�¨ÃS\²,!íB»ã0e•Q™‘JÈ’Á«¦WÓ“ÙÙ„L�ñ¿æ?ðüÚ©cB`qB¸VÄí«HÙ(MVHXòT;�%•Œ*È8ȽY×ßju�%$mä5#5˜ÖÁ+)šMŽS”ST-8îÖ¸÷¬eœ@îÌE†’_|ý–ɹ$—9­¹Þ5І4YÒ¡…(%§ÓÕnú©š’�4h.É·r“ äƒKUŠ’‹"S‰el@î a׬‚!UÓ”etŽŒâ Q,ðÔ’7B¦BJ¦jx�blE2 'GK0”6Òücd-"@µ÷#û°ÔE™·­®Åg¿Úæ4{™jV) qãÝDñ_A¹ Z°ˆÉ¸Â �ë*ÇžâÞ%ÍuZÙS�q1'¦M "(Õà%+W¬�Ü^Ö€\²·µ¨X'Ä“«Fèg—ßÙ¿n»„Œ�–s·09�'ñºEëȨ»ª?ñßý šj%š*cÌ„ÄMþ²Gµ›‰o›­×<å&…ôuI.’È Z–4R|Æ �lyôXÐr•¹¯[ÿµ*Ö`xµ)g ùÂ%ôèt€ìºa“ͳÚu Щè^8gìšÃT线:"T <·ä“}vÉ_ɆŒF±©#hbLæ{º·yú&g’96^~n“ Ï0ôÎ[˜mk¿ÎëRKÞÒëâ2p ƒȺ©RsЭûv�½ð «µ[ ú—u”>5!˜!€ø5+vaô‚¯‹j³¼dÌ€¨¹Jà8¢$—l}qdqÆ/ÏŽÿ¹fÀ $ö¿OH¸ðyL¿ü,i]3©ìr›‡Ž–ö¯[j’Œcoø$@àe®p{ p?4ˆÀïFe ÖFa@Ó”Õ:bÝ-‚BRǹÚΉ�‰Z Vï¢A>wàÜ%yxI ‘&îÉB=J€“ ÓĈ&**w1´²|dk»8qpà%×¢àjúÐ ÁQRl„ä:/cÃoªºÑêÜÍi°$+øÒE[ ‹¸¬P§qw av°AÈËP fÀ\c–#»`¸0šæ yIå[qM !I/`Ðü.@ã…HŠ‚Ökix4«L|Xb©(š²  Ñ `ŽU ô‡ºñ{Öp þqG’{=±€U�ã0îKá(ÏQ…F³—¢@ç\fj³lMB‘”V˜SZàaÌלef@c’�wñ<ú‘]5ˆeÇä{sîíÛÊ&ð û£ç$爔}ÆÊAmk‚ÃΦEbø3*ÈÀN?!j÷¤T.š"#6Âe"¥h€•˜·¬ÁÍæéaæª ê2‡¶³’BFîImGî¦Ýªƒ¤¯„÷ÑAÒW»è çXßÀB p\Ô†ãfÓ˜Èí®„Š€rçÒ úk8˜EF[Ó¦Ðc¦’C^Ü÷,;t§’qžP=üeÊÀÄ0eUjß dªŽº9x óÀõN8”l«]Fp¦Ï`“ä6£èÓ1ÉÙnÁ²ät̶€¬‘¨ø¤µ�µ”Yúp_§Ôt§¯ïÛ¢õæÜ{À‚ûÞdü¹­¬¸>kÍ1_ù ÅZ�’ª™-ÊŒMAÿ®ä&¢Ñ@ã�?�yûþÔZüôÛD á`ïå|¬º|Ò]!ö‹/ÞL¶›Ö»í0*·q©‚�CmØ‹I�öÞ£·‘Oþ`Lp¥ÁØuÎæw�T¾'ÃzÃBæø^Ò¾§UŠ‘f-$¾ D¾Ø�“ó¶‹á …�» gG�çÚr¡%ü›-ŸT+UCÕ?ñqQƒ2]À¶®ü²€Ê£“gȾ"�Gö) €¾à÷ßGÛ1�J™Œ¦³3(Ñ·dö†œb>ª9yCÌà©ù1ÆE6GØ…“ݘœÀ̧î^9ŠOaÕMJFxSE`lL¦H„|B¶0ÿúáÄ)K8_¿ÁäzÒ2<éá ²>:'d6›µLN†Î¦‰çt²—2&ä31Õ¯¹>`{íK,ùPËsÔþÜZ"¹³.Gùã*{°({®Õöèlÿ.ûr±”íËý€«µ/çùyÄÀJîîâÝÚÄë ˆ(6!%£î*ªc¯½¶·ÁíeUÄ12[1¥öY7÷ȺP\õñ]ÀnÄ_ÔŒ*QMÙ2¡·ËÛs8A“N¡³¬w¡ÞŠO6-Ònùzs §ÐÉm-’G4U/¦“;‚ºk8t<ø9�³À;ªˆ‹÷k>%m÷é#5Ÿö5q ù„Ä<&ó.¹ÿˆbh¶a^ÓÇžàA;ò¹wíë�É\JöÀ†OzKz?ž÷u2VAíRQÄ›W …èÆn…}™b:,Ë>B©MÛVFØ»e‘F x…»¼ âÇ!é¿ØØö:3mñ¿½ ÓhÄ7�¼˜°>qk¼"úSÈàúÓ�—oNÇû^öS`v4™©1Îû†¶Ûwq|°ŸÔTÂÒà+¦iÛÌ9õ“º"{hº›ý©ŸmÚZ~ô³¶mˆ5µ…rY2ÜŃԊ$†H�£~cÐ}'Žd÷GOƒú¿h(›‚b*Ž, óH=«î3®î#‹سº2ï<}åÙCдïjâ tß3EÁ'ÁônLz#=d÷¡ì¶ïc4€Gîq§�ëGE>%WÓ¯i£§q®= ’~h#'dÏ|+HS†De¡À­ÔM´C5�×.9Uݹī |×¹²=¡ÞHÁL²LÙyB~Æ;- ]0�ùðÛùb†*Ÿ�ÎÈw \Æ¿‚iŠÂ ¤ÈèôøøéØ_î|mB§0©õK±�¾lŠärßï`vÁ…e~ NO©–X)ë?üú?š™–TÏ�ŸŸ�}~Lr­ëóùœÊ-¿ž ¹™Ó•š÷gÝ/ö\]z ¢’´�‰¨6}œFnÅaDÀ=ÚäÓÊ5RYJær} •G®‘‘—èa´¸G;÷¾ÝÜ“�îm|]µßˆ5P"¾×|ñŽ­÷nÂâÓáìwl«¸àµ-œQ�ØŒõ´8S·÷e�l½;©ð"ײa?e¼gƒö÷ל¼¥i5Ð�½ŸÈ{­Ùä@:’þ>Â'w´ìÞ¯3û‘{i¿³ÎIØ8ñ%d¿kâÓLÔ2é0e?µÜ£crÃ3�‡ ™�@%Ýò²)�rÕ‹vðÀ\Á,¾a¬2=b—c\7=~÷ÎIøÕLœã}7sßFÉÇê“ $ìî4ÞÏÖm â¤ÆÄ€Sñ¨É±×›0ÉÛ¿}w¡û¯ûBn¯�¢šþ�ÄkSÚAl‹(¿¸û½ß–˜»ÿOgnÿçŸÿÀ~¡ËNú[9^¶yðì{zãoŸáà:†oÿ‹}›n¼�Ú‹™‹ƒx sx2½ Ò6OæcïÃ×LÆ·ÞÛ” 3s½ÄwÜ`ʼn¿#gÁfýµŸ›ÿú §~ÉÈ.åsV–íË�†ÁîIeãÕ˜ �ŪL¯ôÑÇîïÜ‹GîUuÙ�Ÿ,ü(3÷€¯#!6Ìžì ÌÇw÷îÒ_yû0›ÑÏØÛæ…0 ëý+Õ[uƒÔ}xýðkGY68£°p`�2ØŒ —†ë®a‡u½ÅÂßeðâ;.~»‡ÖïùKaØUø€¤0nüæ»^ZzsŠew»Oix3^·(Ž®"íᢦN®aYˆ†?_4ÅòC4uð•ÕT§ +7ôW™ëowÙýÌQßä\*º8‚Ö.¨ÅE.†fø÷ǰq;5µ‹o°ÂQÁáKâPÝãÄ@ð_l8½p'A¶Ùºû×*¬ªE¼Ýía‡óZÙ/ð¶QQ3«m¥¾—,6¢ÝÕ>ZÐÖî¤sp{.lªû¼q~^Ý*Ãø€Ó2‡Kv˜ÀA"&ˆo]ßð‘Ý£…S¢ÐÖ5(�»Ð»‡ýò×}œ=‰W+öÀE…Š |}o¼»—ûè½Ðߺ¿øÐ®ì+ܸ¾u[•{‡ÿ…ùWÿ­XêûLkê�ñ€‚±…± " ”ï*pb‰í |z\<œ=º¤fë�ñÁ¿õ¹ê¥ ÁÏ*/b{¥XXÿ>šÇûh ⯕#V.\j;03Xø Ì"Ü/a[¼ó™û›ŸÄ)¾xøB� ¯"7Ý/6ÐXЇûW½pt@e‰�»)nm(÷îÁ‡•ÄVkdãÁŠžûÐw|›¯`�NñPs½Lt.ý5�÷ ª aËwI‚õ¦PBpø»ôæ±<. lþº¿Ü' l%ô†ÇÀƒQÊ*´ ×9®G& çn:a–íÒonnüèú|vþ2ð®ãd}ƒÿuƒ5ý¢Öôà™e˜ä6_ÄÑúÙO¾Oó ¸è†丹P*"ûå%Ÿäôá?ãX,Wžñl`æÚ”¢[VìKïDÛ».m'Ñ’•†‡¦Ò ‰æeqá³ÂøÔPú'©Öç ªÏ*À'efn@5ÖÐ.k¬wY«6x7 ãõkì²j$d¨V ú¡¥mqªÁ?%üÒUšgî[¡$@CWê¦Ô‡‡MHÙZ‚ò+TÅT÷N®( ”¡VTÖýŒýqØÁzñèRGµÃ½xܦÇ ÐÊ�â+®iqÐ- *\1È¥h"¤àÁþ·‹Ó€¦ ÷dÇ�öÛ¹€”êXàsâEkPú+f?‹ÃìGØoaÙOUp'#69 ª×ÛšVlç…ÀÆv;ÎG‰_)œ_&)h*OÒÂ)’à®Ô>›+^ Ì Ìg¢XCªXx˜VÀã\Û›1@ÇÈš°‰Ð¤QÐÊAcóÒ{P cxçA„$ð”´À"­p#½ÀȤ8NŠã¤8ž�âHÌ…|aÆSƒ(ÖHö‰%I’ê ‰¨¥ÂÌõ2ɧ%KIª›¹û·ßÂè>˜�<ŠÉ®ÞlVÇÖ·ìÓÝLë…Iå=i3š&+»cÒ#Ü‹7¯_\JS{ϽyÙ=~Û6håÎ8ÅÞ¸E5ÇD•‰”�tá…^’Š#šdÔ³I—„¶јŸ+–òK8 Õ²áüÕª¹e‡ëÒ®.×Cq <¡CsŠIm§}6 óõ*Û8BiÀsÚl§ÑSż.K’ÂgПç|˜vð•Jºâ¿Eÿ QÿnçÑaÍñFAk"u�Ÿ\p¾WY>^¢ë®úíqJVü�ŒøNÉŠßÓˆï+~#¾¶¸H¯QÖVqµâ‹8 }¦TAw¶¾—î®È¯’ÞÙÔÇ^e4±»(\;µ«–½÷ñÞ=xÐO\(‡chy§PÔÍw3¦ Êï +šø.ÃxÌÐ@] P~Àuß™›&ö-9å‡�9¥UîI´À¸¶æ& 5ütÆh�¨Û¨„S£å6þ:¶V�›}we�×f§š ¯B+L)d–ŠbJ!{°/½mÛ+…Pˆ)…ìÁJ)„"ܚȟlÕB(ÆÕBþT¯êÖDÙj?k¢¬¦§5ÑÊŠ¸ˆoKjÙø6MÕŽaÛh“÷J³-³Ÿò§Ù3¹èãØ3Í;—4T*Ç‚z…{1Ú%µïwR0a‡lm†œá^ŒZ›¾›Â\�-J5̨ÈÎÅQ$¯,×<ËvŸÿÑ ›Nw5X€ ¸1àdv}o±ÉõAÍ8€’¶óáŸ( ïU]”­ÇÒ”9rÎ*¦ * ÐDRÖ,ÚJ /¥ý ÃÓÃ)¬³.ÖR)ħa-m;Ч´–f÷»T›ð^à±´ƒ™”è…b�`ßpA!C�GØb½èžì±¢þ‚m–MkQ[wÊöÕAÌ«“êö™«nÌž'¶ð·TwzfŸ àô,*:¶óñ¹µ;€ÓsI3lÝÿ„Ñ çк•›µ�º<¨XvÎË[É©F·å ‰æÞ~{õ“‡œU’:Ý”íí¾hiMGö`ÖÅ®pgvŒžµ~Ì�¤œ\F#†"ƒpMDfBÌ éÑ•5>.ÕXtC5®½íÖ«UA[úX¶ý1|”M�ŸÂG™÷À1v¡£“²ckßtÝ?ÁKCë� ÕÏÒ—T:׫M@]h$·�‚kè ƒ^³óªJó<ŽCÜ< *sqgjRšôå3É›HYŸãyŠƒC{c¦.è³|¡ª¥Í…E�PŽtâ6Ú;¨=ƒ±düº�ô0sQy9ùì ‚h½L/@㨈f@ãÔOß�¾ëÆ©Ö�¸e/tÇÆ ½Ñ ]¬QK7ôf/tgrCoá†Þà²R¯tǽ¬Ú/^<þâß~ñï_‰ê-�p›ZF@Æ)÷ta¶è4åž.ìÁ¾ôN´Ý§ÜÓ…=Ø!ãT¸Hó'klœ _þT��K¤F:ˆ‹´¬æ´¤Æ˜RЙ-™Ñ´Ú¢Ä>rôçYêw³Î„KzäõWñ&/â<åU9‘—Á›=kÓ™q°â‰q0k}6ŒÃŸÆ†O]gcPÇP“†>!ž)—¢ËÔv 'tt$t4±L>šTãÏY5f¼‘0…�¦/âÝ>ÔnÉÏìTãd«Š±R ©gTˆÁÖ?xû4…££bù>±ï£)KD‡}AK®)\ÚY‹[iP+ mÞ6ï ¥Za£4k­*†e4ÈLf‘ªG°|)$ Ö�:$jšmT`)Mëþ›È�+“ͦąàRFþ%kÏ5´�21eX»I³—ІÝKâC®0ím©  pÆlÙo#ºŽÔ§yè§O`w ñVv‚GÅ#  Kj Ðr,E„½ “ÖmCæ€<„ß Õ]/÷ ŸP>(VËü†A¹æÎº4À,ñ¢Ѝ ¿c“äÓÆK�7ì“éÔ(kP9ô¡¤ x>â¸û n›‰ÓªqÈùñ5¯«¸t3c!÷§l�è¥ê� z÷Y\lבuðo�%ÿ€1BsÛ컫?Æûµç%KYž¥Ž—0üÌUÐW¼ ø‘½0Ç,ò‹�Š��¬HÉÔø1R‹9WcÔÝ]ykMÅÅ­®º77½‘ï%Lc U,äê[:·ÖÂ;ÎTñàCjQþ^G¨1…€PA]Oy7ð3PøN£ Ç^gþªo½{42;ĨÜm¡ñî^+†O}K‡ˆ¾Ç~ÖbSÒL4ŽOU^fŸqÁ {úËx[< aôœ®<5eGâ9jÆ×óží¹Áo"ë�£YH0Xo¯ù< ].㶇 P>�—,ºÛ~·‹“Œå&QÇë~ž,6ÁÖ_úâ]ädV¿¯�ïe�/®zóf…ª?Ëk±ï‹%6H="JÞ4Ê=“<)î 0îUN¶BÀœ•ã�;cî'*_Ja[S>iŽ>kÆyÊMøåUÞK§pS?ã㬣`°D„æßú¡ðJd/×j…37�/ÈÆÃƒøÖO¼u5Ùm&ºù¡/hœ UwHOî�ÈQË“‘J˜Ä%öè½.ó�XÐ@Pߢ%~’ ´LåûçPh7F—ÃN˜\±>»ŠWWp‚/¹º¤Èë~èLGlÒÖÈŸ-á +Hë\Ô½ Þb +ÊÇ.5ìò¬1+F¨O>VC/ÿ¶»è¿û»ëñjŽì4wV­ï¨×º¡Üôú´=š“Þ BÙ¹èÙä¦óï2 ŠÉtãŽJ/õ}q“wÞgâ�÷.×/ôÔ2b’¶P0d{±ú þЉi»Àm�>"üà’ã•Hª,F9>‚pA¼/ÞšK yC³èJ`*,xN÷A†tZL$N*µàÁï_>EA¼™<8¼†,âÞ2¼‹+ÆÁÉ(#â5ZÓ×yøÖÖ­i躒³¬.+zÖPdp‚Ö¿%ÈMìÀ²KÔ,ý±-¹cmZtÿ–À9ý±!dý[æ<ÈþZÚ7ü[f amš• ï�º«í š‡£�¶—£¨ä”Þ�cÊPµçàÙØOŠþ~�Õ�±£_#ëЩ½k¬´¶BÖŽZK7É2,.‚â<¼‚]Gÿ™v�^Ãvº&§Ç²¦<¹<žÜœ?Àùy¡ CvÌ"àPÒ;LˆãóC¤ösšå¨xƒ¡u V"Ž»�FaÒvˆ£•_^É0\¢RýL4¨³ˆçÒà¼eë–ÆD3}„h3–±;@8”¢¬sv¥â‚e�ëÄJ­T&$¤èlWaAIŽÐ܉fB¯ ²,� µ°ÓuŽÉûЧ?i4å„™t"âh'‰w?Ëe?aˆÕW=æxŠÙ�˜×—T³sO‚jÄ)kâ2Ý (£o½·3¦]2‰$š(´*]!A½‚à§ém=£]hACîQ5I¡£ ľE·9õø@VÍLqbpèeìiMÐp‘£gtLR‡‘Ú¾Jâ«q"5�Ojºîó�Àá?ƒ¥À2Ák! )Í,R|èWópß÷¯„�_ý–~gŽ4Œ×ˆ`,€æí'žùªì _~í_«À¯¾>ç( Ñh¾YQS°[&Kî—¬IxRÜÛu6-a¯:«êµE½o¼,ôæ 2ž™Ð:»”ñ ÿ\tü*W¥ø.È®w<Žv°Èƒ¨0$ oÊ..¤1ŒþÂKE|ý ¼ÐÛe°Å¸è/8SËÆý‘{çnáÿüÿúúšMnz Cñ#Tëçv<„Ð8܃"©Ê÷%ýVJô%ì¾~ú亙«D~� Í(¯!îè+rЇyIû�¿“h€ù½ –{ØL §í�Ëe�;D—_#æë­Í‡ÄdU Qa"‘™–NO' K–OPŒþ쬗7 â†�'6œçÊ4?G�hK%mùüV¸¾P©Þ¡[TìÈ“B1ëcþ�ÿËVoÀÏr©¥å†ÿV¼/Z9„?Žw»8Í“ƒ×µôw~ሒZ Ï-¦˜yx‹*@9. ø„ÄJãÅbêK¥Ü´aW4ÅwÝE/ÀÓ»ƒè×¾©²¿#ÁíÞ¸¼ÖÖv”Ê èjLy‘WheQ©*7¼YånÇì*ô×ÒÈq·c–úk]vÇÛµ7®Üí˜u…þZ™WîvœÑe¶&–»·±°‡Z‰ÎÊŠûѲ¢ ªsËQ8YøG·aŒ#@ÙxЪ�qí]E‚Šíi®ÐˆXÖ—VS6 +{dŠ,S´�©Ráè%ê§»²dµàh¼rLŠèn¸Ù‚T\4t¤¸ ÄÛž.-¼Vƒ: W*Û/q¥6$!SÐh�*5Að¯/hÐFÒ80z(Øe"&þó˜Hp‚D˜ ÂAFÁr“ËHaím‹*û ÿ=PQÏ‘¯džY³ÃSªí[á.h¯ÆG» :¬TãÝýf*è(ëóx·@˲j‰ ŽÎ¥Z\Ö4'?Póz[åi0_Ôœ õ®Ç%ÍÖ=Â-MSÛ£9î5Ívc>¡Æ# ÆÎS1áÆ_Ô37ÝÔüLnjþøý»çyqóæù»×Ï¿3ù— L�˜ßó£1H ñÿñ·™û�ÿâà�4ÓTe�¦…�ÿ…Cßu_Eî{�WgâΊ#¬º@©b³`á•„ù²üŸ<¸!“D6.t–Ú.Š%dìÄ×ö ÚlßôÄhi¡·ÛÐ•Ž²·Éb÷_>:ÿRÙhJö4Ys½¦Ï2ó=ip¯ë(ö3ñYÀ.ÜÊä,´w$/«Ÿ�ýȱ*;7òR‰Ám°A’-ƒýµ³)@™/km�€2da­Ë’-ƒýµrìö¶Ü«›=Ôš#4¾RéÅWÊZNy�tT!ʘøn’ö£?ÙsïÉ©ï�–ûã(1Li+JYÔ�ÙÖù�ÔžÕÁ¸âi=œÓOƒ’=]ò<#¬:ÝòüÐê„T&¤2!• ©T"ï†SJ•8Å�5Ú”>°¤MS�3å• D^¦%r#ÕiËsiÐŽ'­E Ëbq4ßÙ@h~‰×’º1‚…ÅÃ:Ó÷èÅ›×/.%'˜§è ˜{ÐEÒFñ‹’NÉE²Š‹k˜/SÿZ2a,Ã@LGÉ!g¼+F<˜s†DX«!¯|7Th1„òêêeœ^Ú«ª¼:Ãh¯…ùwF Rb~õXøno�îs+/÷˜„¤X3]5u”ÞxSîB5˜ñ›úâóW‘‹:÷W1À<«&ÛZ+K’wV˜DuvJ“±ÔðŠ“3ʼnþZ*0~Ì”úk]vÇÛµWºü˜)Nô×Òé 1§3þP§=ÝêÚÓ0ÉÖ§ÉpfÍàt6– V>`§�¬ÎíËV=Âð�ç¬Vhðâ/þr9”ÛÈÙ @Ô£{±}s3Œ›�±±Ê¯Z$EÐî4õrŽ;a SÛ“sÜØ˜@óF#÷I@N¡¶Ôt{là’´gú:Tõ| §‡cå†eã…å¨nXo^+;³ô¦b½Zm¦ù^²ØÈÊUP£o–ÓÖ9+ùƒD[Ñ“,©÷&"¯€Qî—aöÄmÍä‰ÉsøÕ«Ú€ÌÕu¯�u—n‹å Ñõ2ñtM«yÜ©÷¼µ E‹zïÃ/BíF¦*’Á8�Ãä=ÌÃ<¹¬3Ë»t¥¶"~ÉÃS'©Âi>ºi¬ª6aÑDt¦[Ìwœ´ ×Ûp÷`èR�CÕ§öÈ€eöÛzk™¥�Ç—®ëIª�¢Øîx0Lí}.-è¶]•R‚Eqor �ib^P°Ìe·HÂún9anWÏÍc±õ22K—Ê¡¹ÙÅi@O9̱à~Èýôxqé~±!’Ú1µý»â&-´¡�]º7%¥�½HW�ÊE“±¥M.n|qºX_dý¿ðú¡ú-­1mjÁ:sË­3ö�\×<’ëºC(×5�åºîÌuÍ£¹®­Ã¹®Er†u§kÓu�u­´ìèþ{ƒ¤�XŸCˆ¡„h6 ,L7ºê½¤‡1@s°;�$ åS Þ½N‚ýJß:¿»€Ò�æ´ Åd ð–ú£ùâMù”úºùâÙùŸ�q7d<Á˜ ƜƵÓä·v-5P×}ž �TÓzúV.&XÄ~â¶™çßÿøþÃ%¨—dB¡¤ë¬Q¹­b¦‡9Úb˜3Þwe·æc ‡†Ã¶ É|X Ë`�‘”X¨£xÿô­²ØqÖ)€/í‹F@£üc’À^ÁÂzÂFGNLÄ1íä2¸3¹â’n7ûOôÀDIõ£;O`ãÞäç†"…� ] Ž* Y’øµ“G…ÉÒ¦þ‰t1*œLe†>MФÄWlq«’;?òƒÆao€¤*¸ŽEâ ¤--ˆ€™æ‚h�C¼L"ž5„LfA´ôw>üÚB~VÀò Ób‚æ4Ÿ ùÙ@sæqÚ%׊0‡ê ·ñ¶äwAŸúà$7oJbVÄe�ôe€k>º<¼Õ†/µÝ³ ¡j¡E®UMؤ Ò¦˜m~|}2£Ò[l Ã|õœ|©ð¯D±t?ßøjԞɟÁ |…æzAT8%鸚)ƒMrŒ˜3wŠÑß$ÑŸé$¼b¿¯—Šý”W]!™Fæs龺Êá‡fªEAÁvÔ ‡/$¾ ?)í±OŠÛðÏ<2vQo0óñÒ]S??ÊÌz_u,Î”Û ±WRîUµ\çìŽcxsè�£Û)¨Ý1Ž* ¸äáˆSðb±GQÎñ–Z¢¿¼l±f^ܱdeõËEüJ¬Šÿìi![>û2š§»'Ooàÿóé5'V‰Ï >xÆL4„¤´‘<¼~4sáá߇øÏåX* mæîAÎz’˜îÇŸì±oàè`ΰÌÅ£Kö÷1ÿûÕ%Õ·š¹w Rþœê㌕…êfÿ?›¹«KÜÜ£°7Ø»ÖÓÊnKÍMo›ø�ÀX¾jσ0 bèõŸ®Ÿ€2 #Åüýµû% ûˆ>ùÓµû&¸ƒí�m]LË_~Žö¼÷pyþÙOã}‚‘üá‰ÈÙ™ûøá#èúïÿ~óð믿~$VJ,(ÅN'}ýÉWqtiY¾ÖöI¾Œ¾‚CfÝj žS«Çw´ÌÄuo¼>2v¹*béŒ÷'€�au ÿ»ÎéÃZ¹ß•·Ì6œ7ž£Ø°LÕÙ{ã•LÂk5;Ê2öäÃT¥Pµð]¦^…9­�_óÔhxC97 B�c+Ò°[º•ð.ÆS@äÞÂ4FO4Å¥Ò%€föó,ñt‚ÌO…Fù=¼{A�˽B�-,�¢�G(Oª †v€U/Z+¶¹t¬²~ÞQƒ%T3\ûÑÍ—ÆŽ¥8æ¥{¨�é êÕüþªâ+N˜€Â§âÊæBÛÄèðÄ–ÓÁOxn-fta¿Mý^ -þx¿ÛÉ7Ðm27M®•ƒºVZ$Ë2‹N?¾¢Ÿ3˜ŸæÀçL|†r-<ø†\2­"NäV†Z¦�p0òkg”p‹§¬µn�=` wÂCd¼^Åh7Í_`–M’¥þ"~Še +±†ó7ó>ø)#ίL&ómœf:¡h²¬àlKá ƒZÞ¸Ñà‹Û3c±¤i…G´½úËo¾0Ë´—);°-wq Xæáã8�: Æ$e‘ð¡ˆø!œJaŒ)Ôàµf^ ¡‡?ÌÔÈ(¤Dqý _ÿ´/ö¦ ÿMÔ +#²wÎ8þ“{—Ë1y’`¹ö¯Mç=LfB×/‘jJ)á‹ÁOVBÉzbÜj2öK} •–XrF°_0IÚßߥ¥ÄSº�[L@œlOP³Ã¯)fxZó†! °Åò¼'&bX'PÚK2 N>çÛ8/M~#"X �fŽôÚÖ÷Ò=F²a"ã{¡mÁHAp]×IÂɧŠ9¥šIE“ ‰Å Dú+Ÿ%U»6⯢U™›¨bº¦KS•Uíô[� 7`Lìûë>€Iå¹HyªJ%ä¶ãÞKw_ŠR•´®›‚f¦r®e‘I1(‹l€,œ‘*pfæê¬i3qÖ‘fFæÊÀšÕ�h›4òk”]1¯óN�%}Fd è¿R’\�:ÍyƒiUŒÙ¤ï¾5órJfjŽŠ^=2¯ƒNT� Áv¿»œ“k¬ÞÎÜœ¡øð¤˽±¶O½±f™7Öö‰7Ö,ïÆÚ>íÆšeÝXÛ&ÝXólyë.YùÖ<çÆº)冞’o=DF¾µž$î²ÃÈ``²}“ÃzÁšŽ†ŽŽ¯»�ü^¯úø½6™Û¹ÁVE™tÊûsÑ¿µK¤É¢â#ím ‚±´µ�^å(¶¶Ü+UÖÇ13œ±_qóYÙZV¼R¬™ËLÂwuÜí¶rœþ.»3Ô‰;´4‡ßvþ¾N{‡ßÖþ¾"“²Ñá×uKE`Ì=4–p§6�²8‚Ž:„3Z”\†¥M�æä·MF¡³÷*þ|‚ðp<ˆ¾”KÇl¢ha¡P·,n¢8ÄÜ@Ñ £I7„fõÞýþ‡—ÇiŠX…R–è¬(ªÓ�)°ÊùlЙ˜Šzh&'¬#.«÷—œ@ÙʬAsé�±5$#§mÍÍD|6€«I^Õ±?αå’"Ú46F^Äží�—qhÁÆ 8Å.M åÒ"úÚ��Ų»áA7-­»B{ùƒ·OS8ªÛýCùžñN�wÅ£Vä�ã–-ª=®fã †Þv¾ârFû¡áj†±ñébƸ3ZúäÑr�–îs1]Ê8ñ¥ 9uG¾’aÌÇ.Öh¿p®†‹ÎεÅe�z`[¯|tE·¯X­¿ˆZ­ nmáÁq.¾*„tùƒîÄB„vùƒ}é�hÛób!B½üÁ÷bf¡O–è‹1ü+žê°Fä­ö"òjNI Œ+…ƒ�Ãe‚¾rô'$†�zÅ5%zuj^Bï[e„<“§E™™(Â4§ ÓŽFMØð'äúoª±GÑz' t$ ÔÙñ‚JG„J“.<éÂF]˜iÞM†Ã1(rAü“þ\�¬èÈœÌÙµ;<dÑÔ\P¿ÖGã‚k .EDéûs@¼³ãq@FQsîG ‡¢ÃÆ Ü�½w¾‘û©GíÞîö£=ícl|¢}Æ¥}Zú„fŽ€fºÏÅ„eNLûÈ©›hŸ‰ö©…ºf}£+Ä}Ík³‚¶ÆBƒCZì(AZþ`+±AZþ`_z'Ú¶†´Xˆ -°�´X„1.âÉÔ²õ� V<ÕƒZ�ÞÉ[íEïäÕœ’ÞW ¬:z§¯ýé�~è­¸–DoNMëˆ>}BwMì‡v"tÌšÛD蜚��MZîg®å›"mÑcê¸a‰¸&e‹¬èøÊyµ; qsÂT-ýZ“¸9]š–ÖÄÍÕ ä͸9Z*ÅÕœž©UhìŽ]©'‡sº¬*ÆÆ'gtgʨr>fʧòÀ—º©›8œ‰ÃiB·ƒ\©ÇØ‚\sÁ"ÐýñûwÏÿò↥+¦3•Á<`q¾tƒß¼(u/�’í­Òl«Ñ”ó2T­Ä°þD‚\þQÕZÆJ +…} e,$¬öA”±�°RØPÆBÂJa<‹H.®Kö,&ía³�¢ÌÅ ‘½%¯æÄ\Üñ2·X56.wâ¬-ýy‰�;‹”-j¿¬®XyÊáb¸aÅ~4g?rŽÉÅ}¾yU†ä⦼*ge'.îc³Z™ÐÊ„V>´"8Õi^,±JTâT­’µÔqªæ¤#ãs›çÕî(œjÛ¦ÆáT{´>&§:¤XvœªMò´GïÒ»Æ;>*·Z™ê¦x3Î�ꦖ_ý룙ûøg)k®�¥âÈdµãÂÜGK/Y‚ uб:sñ×Ð$ÞKÆ.^”¥fœaP_aš¸j�· —°|ohÇñ Šiâf˜‰ÇÂcÆû åÕ‡÷Êñ�’v@r—ÕŠr'§|[®…q(ßóHàò™Q¾m}ÂÉÇÁÉçbBɧ§|£‰ò�(_\ïqÜ"˜#zAÙ!“ω7¬¬�´òüšIׯ_÷´ùÅq&êÜGóÀÃõ¡5æH«)Z©žÁk‡<¬ ’_œÍTõϘëPMÖgcO2…:¤c¨(6†ƒw$Œö°�0FØgeÀBÂaŸ™ c„mv,"©Ó.骰˜4G4äiÈ+Ø#†¡NO�¸ þ)ÙŽI�Z禉:í%Ç ÔépI³DwàM¯zò¦ç›4kæ6Áb£Hb;DoÂÂ-¼û&HS ¦´¥ð­÷6•Ç,®Žœgn¾B8*K1Â%²Ä§6AgÞøXmM;’õRîOSËöOš¥LJÑDR»zçpNZZÌ^ @«Ç`† Œ™y‡ö*ʇ,¿m&~NbÂ?Œ?!— ¹pnS2C£á–ÈûàiTþIUVtd:óìÚžFµhjµ_ë£Ñ¨‹5ÆÕÔ· âÚÛý¯¥òÎŽG�E8¦¨1h�;©;€AËR…ññÊöC>Cil|b(Çe([ú„_Ž€_Šsñ±À—í>Í$À(ŠãTçÜÖwÛöÓ1$å(çb¢?{ʱ­šµˆ®`UÔfU�…ªøJPåv` Påö¥w¢mk Š…¨ò ŠEë%ž,¡*cPU<ÕCU�bË[íE±åÕœòvâ¸RÀÒ9Pl}åèO±õÃdŵ$zsꛉ¢O•·MË욆§œž:ÚÝDû��î&šõ¶‰‘9'Dó22Ÿ¤™tÖÏ\g%ÆCÚ‹ÇÓXç�vG�2¹"*:6Éqní@®Œ&‰ÕqÑêØ�ö:ýñ°ãݶffFb \¼ tß´îwÜxoÇ#jJ’W06ŽÄ¶wÝJÄM´ßÎaPA€tÚFšú©ãî£,¥«P €iïàxã3'õ—\qkâv˜óÝ.‰çÞ<ƒì^i»ã{‹�Ò¦XyCä’k?yQ>}ek`„Š€h F(^9…‹ b<!T¯hÃHÙ(ÚåýZ=ChÕ®^Æé¦VWhÕ¦PÁÇc †_Ã3§fÛ/ó 6ô‚ÓÈ/t£œf~a¢ÚÜhÚøQáä q�ÄK1ksb ¤Ã?Dñ!rðeÄBòÐ{¼øKœ XŽ[%Ô�º•„0‘%Èê,„Uæãló•¸4��ìžœUäC‹¿†‹ìA´ôw>ü§,ü‹N�.ÆØîW°õ¨õ¹U©úL„Úa……äuhgâL¬9“œÅƒÎúò,¼4¿ÙU?èèFïGêÔç†aç‘7ÊÖ~ =Kôh4¿~:øèÓ«$°‰òž¨“Úþ‚ñÀrØx ¼êð¹‹Ó@qö¤À9L4h&ÚŠ^ç M®³8¼÷´_mÕåÔF,_ §|­ñ¦Å+ÊB_ò„ üÏã Œ@(Qå�2Ò`QpäPÝsr×8ˆË8ËÌÌudàYÄá~ËQÔe¡%ÄQ"Šˆ|“9ͽßíâ-Qxï>f&éE‘m üî´öÂCK?êîi«M@Ž»FróÁܳ¨B ×Á<~l¬¯sRØpI—æ€n=nîk!ñ¢µ!ÅÅÃz&a¦ ¯ïÉK„9Ë9}XŠÃ09ÐäbòZ†¬�>üæÙOŸ�— É|ùµüqÞx.ÎAÁ­�&“ƒ •A¼!Š�)ô£u¶)yñçàà 4CK\N¤¾&4?þãídÐÍGhó€Ö3ô)k}àa^ªÊõp/¬Oq läž•öîÊí™YœZƒ£{ñæõ‹ô’™4yt u6ØOÛyP=„7·H¶†Qè0,v›Õ:QšKÅš¦¼wmf�¸d¼%'¸d´ —‡e¥w)>D›Ñp)ƒG¾::£‰ôhxhÁˆá×dÖÄú>®X$8´ xìèNýŒÈG]|óRÔ9uf\'~c×’ f'm±–Uƒ?ÆØŠnþʈb ¨zÅ€·î“Ës«±ms}»a­G•eâf •}ݦ1q¤�“>*0ÐÐX°ã%Øû*Û "ã0¿6Üô×4¬‹Þ‚ ]£…½òÑ¥{ã¿T¢þ¢ÑI *ŒÛcäûKh_X�ȆçÑZ¦¥¶  .·Ü’DæÀЯԥL)¨]ª_ ˆfÊYpR¡ë8\FiSQ¿œoÊÅ©NÞ¥ŽmeަoÔ+ÛN»Åu¹�=ŠtÕ¯ˆS Ù;ÐýlGÌì)ïg)`oÛAïäz�Unœej‹²·7jMˆ¤»wå[X¯�Ý;»jd¼2+•ÌTfp�Œ–)eâÉN3¢R¤–‰§åw²}kÍŒJ‘j&žlt3*ÃÌròÑR?£r¤Ÿ‰§ýL³*íö2*õH� xâÅ8œ6†8»Vû›Ýzi?Å©“¢·¬‘‚hé�UT§ÊÚT …ȱЈš–Y‡!ûæV …3¨R_¥¦Rj†ìchR0˜ÓZh­†è”ײú¨Õ^�³]´Cç$é®&¥ã3Q:ÈJ“ÃøÑTŽÄ»‹!þ‰�ºAÎÏU}CV3ŒEFm Æ�ÿÌZ*ãðr ÉzOÄqlU™Ð¬›º¦-}ÞÁ£ªŒ�8®¥ªÍh?<©: 6Ù§FµOµódë~½oøõîsQ¹)×_·µj¿ùи_wVåEmVº¼±PQ™'÷5 ¡p-JüP(ÀÞD:$I«äùÓiúÔ@5ô»ïdLo…[÷›çï^?ÿîzPü€k™à°ÓÞ±�þ`_z'Ú¶FXˆ€°Á X„Å“%nÀb 7ˆ§zÜ Y*óV{*ój¤ŠH‡õ¨fJ=ùÐPBØ)mÚìo¢ì§ôgMHÞv¤F²PŠ.µ5Q*rŽo¡,¬0ë;}�gÄjož”­t7O¶ŸÉ8yNÆÉI¥™TšáU²NJ³Ñx M°ð5C(û¤rYÑ‘“Äœ]»ÃÇ<±hj„˜'ýZ-æÉÀb�óäÏÁ”��2Ëð^�—YÆ mÙ¼Z¾IR%²!� ëhL< Tx�-¡®téT3=CJ¡lRÈ0e”Uç3Dzàzvè“Ö¢ýÌ ”6¦ Jš¯ÞˆJf]g »nžúÏÔ…OͰÛnÐ'Ãî1 »�碓a÷œ�A;ƒ+§~ú†Ldœ:ñaÝQ\NüãØdþQã4’Æ?mSüðØ*©SÜRN™ß6÷n�¾:ÁOÂ6¨ �ñ5,.;l­Üï×;¹ìÁhB¡�(mS¡ƒÈìÁ¾4¹ìÁ äBn·çO¶0Šq˜ËŸêa®n·—­ö³ÛËjŽg d¸U Ð:‡8}å€Mè�ä4.�÷eœüdõÈD%`þ�J¨¦r f &rt8:«Ã_My;ÜÒ`ue&¬‡ljâcLzÑz't$Ô9¦Á„�Žˆ�&í·—öÛOí§;÷Ñ~Å#¬Ö£é¾i6×|Ýù'ý)YÑ‘©–³kw(�÷1$±"\N"ZÇbé�”�JY†›a¨!ÞÅñ¨!ƒ´3¡¬¿�CÄL¾ŸJ®@Tü¦F"«vÕwl¨$8ÕCÊ–uWžÄºŠÊ,Ñÿa®,úõÜÏÚÔò•™k¢Ÿ®½­X;,({e-¿×k ãS è×K?Âôb§Ú/Žnì è[Œ}ÿÞ —ŽZ>‰·¦øfí#—{1Ñ\=i®AÆü”NjŸ@bÃ�Þhü˜â4W³)6dŠ£³)o^+Lj$Fªb<¯¸z}m’°-Í¢&áàs$Ö�’j9‹ÏÂPœÛøÑŒÇ² ‚r§,/Éö O˜G"�ßc�{8¶1#³tÑE;ù«d�Ç:jcr&e†Fï¾g áÊJaïJ@ˆ{7…ÉãYÿœüM$Q®(­¢ª]à÷½~.â›ó”(,‚âí¨¤e)ZÉ®LV2�´ÏsÔxð/ž><ÙrÄh�ÌÑ»#S�&;PwPÂ+·“2ÿÝË‹”˜ÅqÇÁ�nÖKÅB¶úùQIÂ8l+�IafC²’ÿùŸW�ÿ3¢^ˆlž°»a”6å3 ªÛïàvÜ$Þ¯7˜_æá—ODŒ¡0—56@”6÷=ºU_=Uê-}²³¥ã'ªËÊÖa(2€µCEý8†Œ†Ov,Eö þ`_z!J[[Z°YZøƒ}i²•ð‡†Òïxƒ`�a…aÙᤅà·&ëX�J9~ÞKr/Ïe¨0Cê®»€}«Ñ® ˜V¢X§c‹$lôÉX,Ÿý¼Õ²| ȵ’ês T_o¦ÏѨ¾º äR#Ï碒ç%ô äRE!WÿëéŠRðâbéh²6êh3µá¹N€H �:C!R“¿u_’JOîhÍR}ºÑ(Rþ|57³ö”…Z”möAšIÔsd¶çÌšþQû–F¸EÔ«ñÑ. +ÕxDÑk®N]1­ë§l¿Ì„Ç‘ã¾ë¦ç3·³Á�? ‹�xgÄ©=3”«Ÿuwж{~©h¬ EMוJÒÒ_'>CXVþ2Þšˆöý¯+µžìn+™ÚžXœq/+µóÉSïžz�§brÓ;ñU%1sG¾©däÕª“§Jb­Ûý%§’Y›ˆµ¶W˜ÂZ}MS}8pvº gª÷‹Çÿšþ’]©uþsQcü%³„×ÝêÜ[^8Â쯒…2ƒÙ_ë²;Þ®5z‡2 Ù_/Q(Áèþ`é% ¥˜“(¨…îï![ìE{ÈZNÉzŒ*D?ÚR#…'[°þ„ŒÕžØ‘ªá݇©¹kGÓè0·êv•tô«¹%*ñ1EèY‹<óÕ¥QVâIíúì8¾ fœ¡¹ë‰ú„.`5Ð2u¬ÌÑsŶ�§ 885km;èhN+Ý;:Õ4˜;Öd±5‡²<Š7ç&p08 ºMP#Aƒ},‹´ÿļÕ8=YÉ0ìÚ[ݾýVcÕΦ=;¯ÙÑjHIšuú.‰Fà�_€žæ» nN7ž°‚x§:qSR3sÜ‹7¯_\Š3Lü’üèÎ@gDeG´~µ=–r…^\?}Æn60#Îé9(t¶ù,Òs´óSÞo™‚0ë*Tí«ÙÙé^ë/¼V+µ¨¶ðຮZR~øƒ�‚…Hýáö¥w¢mk  ‘ Ä,5,ÅTñT«Ë”TkMÆ+k2*¥¤âsùÁÏGnOÚH‚âÊúÅ0òh˪zT :·ØEQjáÕƒ›níã}Ú]WjµŒXOÛkOŽÑ°•ûñ¸¸Ç‹®-w1ô.'Œ[hi!a¤ BÜ+u¼!à°~D¾ð%�\�ò^ }‡øMç3Ìí³Ý>S5€‚ÒæÝ«:›¸¿°Ëk²28FC:²w¡O6O˜�R‰Eî·^®bO ìK¥KöWf~e„=EÙwò�T>K|Ã=0ªèj~UÕFxgÓ¼ZùÊ;JSâ3sK�›ZªÐ{î?µ ¹¬ Wé–bf‡XµÛ‚~"æo‡ö#þ,ì¶r{�œ9{æ¤gˆ A@‡Y\Ãc/ ýÈ¿J}k)í»ù´r?0¥Œb6jÖYÎTyD±y>žÌ?ŠùR&=iz%¾%xE“w€ÖqâE놛á¦É8…—¸{`+ŸœW»1ŸÀÊ1À Ƽ ÷5HàÍ–«) kÂ?.2ï— ï¹ÄÑ~�XšyY»Ba³“ÑÅiÜ•ˆ•J ¼lã4›åï¼"$¶DóÉ.Îäyœ ‡\…÷­�UÓÉÜZ}/ëí®Š�¯V^­ºÀ«•€W«.ðj%àÕª ¼Z xµê¯V^­êáÕA4v(ÎØá4á¾Î¬Ùá/€´oi„ ½íȰR�wä½o,hj\ó8Œ&Ì$ê°)dô;ex"’‡£ïŒc�3fãíê«ÿÐÿþEëYáþ…©íéþŸŠ{»1ŸÜhŽàFÓy*¦û'¾!fnÊ3ݲ¨Ò4º"T^™,5•©ÍÃÊ 4§aõS¾? 1 _¶b[|.¹ Wägå+S(Dbab0³rU|ßù`úÝw†7´?Ð>ðH‡�<ÎÁ¡C˜ƒ�ÏpèžáÀ#$Ú„¶*øpD¼©C«p`:8‡R ›ó‡Zh®]ð8 ×êpa­F¢ŒÏó‚Çà‚õ¿àÑ £—Ú¡)Æ¢ÈÐ4\Ð-Þ©ªK�Ùm$*râèÓp‘ãÃqóátîéB>ýB†ÅTM`ò8`²Ç|L×2ÎçZÆ„&&4ñ±¡ âßmÔÙc‰›y¼¼Ç¿øûgÎÿxÉ   �ÿnƒÿ^õgxœì½éräF–.øßŸ£¶R‘uƒL’*uÕÍ”ÒLkUÞne••²K·M¦–��\ÔÖógžèί1›û =¯4çø¸;€»±�‰¶.¾À—ó}gñãŸý_ÿå«wÿú×o‚Ûj“¼&Ÿ‰?Q¸‚?›¨ ƒåmX”QõùGÛj}öÇ�^À÷U\%Ñë¯ã²*âëmgiðí6]âCùÙ ö¯ä³¼–ëlõˆu^¾.«°‚Bñ²|t•†Ÿ‘Ï 7Ñç]C–«õG¯?{bW¯ù7ð»+ø"–IX–Ÿtûi°.ágËl)Í|v¯óÏ^àŸàóà³ë¦†ë×Á ýçöÏ öãPùtÍ>�~ö‚VýÙ‹¼Õ,üÿ|¬vØ<|¢Uÿv›çQñ[Þ­S¯ƒ/é|m7Ûzp+yŒ×|Œƒ“¯¾þöôœ°R«øN¼Ì¦<ûô#VÑ·YDáò6ˆ’h¥U�­µ¯Ël“o«(¨n£á³5¡?¤ýS~rW·AyæQ�‡Ìy¥2A˜®”±8‚wPWÿÑŠ•!ŽKÖ£l³�Úùo‚¾a6TÿEP.Ã$,‚8…w$õ{”Aÿ•§0Å0(›^äÀ6Kè=k1d�Èà?¼Rÿs�ÿh½xœí}m“ÛF’æwü ¬6ìíž`wKòøv×’!ÛÒŒ"$yb¬íńϒ ‰ÐØìöÆ}º_4ûé"n~ÃÜ_ºÌ¬ …— IIˆ˜QÃ$«*ëUùä“•ùôŸ~øñûwÿãO/ÜM¶ Ÿ9OÅß[Ÿ­Ÿyîbã%©Ÿ}û`Ÿ­®þíÁ |žYè?û!H³$˜ï³ ŽÜ—A–Ñúé ûÎyzÃë˜ÇË{¬ñѳ4ó2(,Òo\sYø‘óÔs#oëû`�¯‚ìÁ³§7Vðøÿ~÷>ع‹ÐKÓol¾vW)ül/ýB#Oo½äÙÎK¼íƃRø_î·îÓy^Ñü™{A¿ºc__>½¡ZžÞìZµðWC3¥ÝEÀ>ûùLÛåe¼p·ñÎMþÉ*ñ=SÑâ.ß´Y:øßügvâl!ö3÷ȵõ2ߥÖýÌOR׋–î"ŽVÁÒ�¾Dðé­B¿âÄÍ6¾ûnKå�¾vX…ËàVŒÄ6½úúkƒ�‚ùõä4¿nâgû$J©Ñ­wl÷[7 >øa°‰ã¥ãú\úÔ�Wô#¥ü“’¬î:¸õ#únéÁwAäÞú‹,NW•áÚ­–ûâ¯�fî㟥˜‹8IütGK5¦ªŸMSŽR 7Œ-›ImaðUá²">þe›U¯è‚`�b9ˆ¹¡½ ýEP…qäEv^ÇI\=Š^wLIÏd­aE+º “-4§ÊhKïýí¶à²Î :l+¼Î¶UG?–a ì³-ÌPÓRᶈְëª*/_¾x¥ç÷ñû8�Vqxž7/ðÓ ¬ég¹¦�^;`ÂFôû N`7[Ü e°Ù–vŒÍã*Û^'‘yØ6\ÄéMýõ+xç*º‰ŠE�d71¬­¡ëEk¸„Q†ê‹ðWEôï|·‚…øÝïþù‹/ƒp¹ŒòŠ.=µ›Ž‡>ÄùŽ+1ͱå"¸¿�a;ÿeY…¸K£$¾Ž (“<« J¦Y}‚^/#C3°«`2øKÕ#ìVì>ÿBt{½-£UPe™¼ØaT³ôæõ÷IÊ $â þ š•Áàç Î�þ·FœÞ}ôš?àì,—Ò9"p/]¤+V†JÇUß”]Ç+ ®e“ø}Ä ãÓ@éïkÌi*@bà“´¨_€Ð×@–°²t¤ ;ˆv:ÚÑšj´Ë�Á·à,³¨[f�Ä#0-7aÚ/[�ðq'ø)V;€�‘ ê jDG¬iA�0DšÔfá> w_á¾âyé#žgá>(Üó–•whPƒÂýQîùHMŠ+-23]Ei‰…å¯+,ííAº?ÎÒ}–î³tß%ñÿ ¤;}IEºÓ·u±�ºT§5ÔRÝÙ"6iCüS‘Ý—‡n™±Ì’ú‹�o£$‰óW£ „~�û‘~,eŸjëß`£g!ŸÖ<Åu?²J檦›¼�6ë¤Kµ-R*†˜Hð)h×H�n7×(…–·Y¥=}´D]Ò»5¤èۉâD³ ŽBYÒ†Ùn#ÄÎ$YLß7 ñÒÇ„1¸^(Ú8%(¯0 Þ Z¶»Š7Hvð}óh¯cŠT­V7Y�4 æ+K£šW4­cUë¸(­î3 ]Jªð=ÔÆß“Í.�oØB 6³d»IK†¡aŠ´‹Q‹¦Ö7ÈÕ*ÆÁ ñ"uÿKyf7á#¡…È^¿˜@h9¸PÎh?šøb»ç��g€×.ºÜÓÄë8Í4ûfŒ?KÔС…¥Ê§ÜDZ^íÛz´ òM "àø¼Z„5)¤üµèé°3%Õ¦d?1ªü,Ö¥)T¼]\üRuó€n°-¤,Mpèì7«g³A[Ñ|Ësæ��ÄÝsÆ–ŸÓÒ›Æuf5ãdhÊ'òŸ™;ã�M¼67|2òVÈ&u|�jº¦Ï¾"B5·RÌßð¢l…º‡™ÝdU‰¹#@-pˆ‹¨Ü&´H˜ÜÓšåUÑèüq‰Ü;ŒŠÜÒ/aŠ Œî@wOÄ ‚�ZmS µ2G—¡h8 ˜/‚½•o¢&ú6@Í Øq!‚›Ú9önѦŠauûÑë‹Ï^DfùÐÿé’þÓÄÖXŒ°G~…?`5€=81(ÂØ{p-ËÙrePŒ³ þÔ®àÏ�9JLXn?¿‹Jµq�e¼,>õ3Í (fi¼PÔÔá´b(m/ =5Ø¿w°y™„XK ¬ÀÉóØ1…­Ölœ~v¸8ªwa¨zY­ÌÌÐtTД·”æ‰\˜¢¦1ʳ¤¦­sB"“ª§5‘¶vêì"í‡!²Sëíwa•„×”ùorXsì _ryøqQ‚¦ùª¶[±î˜LòäW’ÝÓ9N¢�—L5DYÍš¸º¸º¼®U®8¢zÁËñ¾Á’(ó˜-M,)¸0€ŒÓ`•ç\ââˆ,ZŸ¨‚IÝ_¶q`ñÁb>G]tà�ÓdgÀ|Í«-FÊß«-j¨�ÞÝ.>mK»ðkû6½Ƕuï|=Û&Üññn³ŽZ­ÉíÚðáÞîx,õði: ,|æ�½=:§¹X„³Ó|Nó§AŸ­p±B|€´¢Ô“²oF9ßËÞÓœ |qn6áô.xÿ^X;â‹ô:Ünξa»Ò˜­c÷È·»lˆ(A-E”éÃÅD®vÖBgÇ:1Ù.šVU±R2é¹T¶²N‰P“úéf;ÃQÙ`zγGœ‡B çÙƒ{ifg`NHEK`®e9KàO®,Šq–ÀŸúY‚æ"#=ÞE*jêp‘z²…¶»Ô”é* È2·tÞ€¬ƒ-±? ìÈÎ]9#ÍŒ4�Ò~)°Â]�‘Æiò–>:‘—TÔ4­^ªë{Çë-�V:Ü¥ã`…xîuÔóv¬è͸2ãÊapeœþóAàŠæ)oëï)5Ôx2…es×mïƒ:]göãMÑ__Ïj'öúxUYÿ=�Ÿ‡Õ†;´4ÍN�Ôš;´ªìÑI-N¤jVëA-ôŸ¿øî˯])Ù©kV¬ÝÙ5»G×ìÑ·gÀ=\4â§É=îRÍÅJ¿ãaåt(²›­ò±Œovpº´³ ?þ4Ø.Ã×1�s`å°ß¦Iålv³ÑŽzŽ”$‘õm\;÷\?„r?M15D¦-|}b²Ò¬“K²Å z“øµÚ’Pã@Xµè¼ý¸¹X¾ºb9vdØ Öú– ö ”CN¸TÅV¡±¦Áº×\.F1“ï9O8ã+ŸÒI#gÂÂòZTœ(¶�eVQ™tÜ©»uŠ™’ÖA´É«G€îàרÈhõ4 Þ€eÕê§4r¢«­>Ò@Gäýl Ü•2pèf%ø ETŒ~ž•1[Ñ:Nã*Ýz�³6ëßG o¸ˆÞp©Ö-Ê+]”WcEyÕ'Ê‹ÛLùŒ?�Z�{u€|—%ƒþûj»‚U]'»«v)ì«.a?4’½¢¾t…–¹² ÄI&/jÀ Â0aät™Eëu¼ŒQHêïG["�ôçNÒpÄËÃ.½)¢ˆfi[耢£ºxDaH$V]KµZeñ <{‹[žáHG˜‡ºp„ºVe H ” ˜‡*� š2� €‚3rèÿ5Ú¹×á6©ˆ¼šú>.£³?òY=ŸmœÇ#c+.c«^» ·ËÛGUÌÖß��´R²îáBù¸“¬,cza~ømâøãy?ÛÆ¤$[²ý®ñ^12ÌJd´8�IÀ¢¾ÑgcŠSàÎÖ!#ÊÕkÈyý ßqB,¢;'®uÉ2cíîLhꎯ}†Õåd¡1™�kÇc’`uü�¥ÔÙ6, yj™8 j-º^fU“À*´ñ¾õ0 ÐQ�œ  ÁSQÿ‹+!›) ÐÇ«¶;v„o¦íÇ>ßÁyÑA/ [¸‹ÖîVyÉáBL—q õªI<-�ÝðÆ‚¦B7@è(7ë¨G¥£ÂapÈÁ 1(dfÊã{±‹ïOý8¬ùjÄûŽ÷Ôˆš:ü4xÜòÊ,�`ñ€®™) Ñœ„×Âõb–Ný‡ì!Á,pg�ë(p—Bdºë/OIàæ-Åg"7�¨ÉMR�£õËL&]‰‡ßÅBº’½æy™¥ë,]÷*]Ç‘á=JWÍK#Úô÷шj©jcH[÷.|/ö�íÇÏÒÓOŸJ—Tôq¬°ÞuL¹É‰bö¡�áÞ�@0ÍI¢@W�Tþ 1C³¿bþŠ'†ìÏœ‡Qà”åÛD»°·þnŒß£©£–¤ëp'�jˆ–ò VZ5ýµ'üÈ™8z6Ô�A-…Ö°W´•ƒ’Ö¼œ}!DD=CŸˆBåöú6 W ,?z]ÿ &í<+N0s£�<ø�n!w>¾ÚT“<. å–xÀºfÈ¿ýS¸-Ë8L…óˆž4ÊÅ?ðªkWS¥ÿ’3je¾JÂÇ*k·Á¿?'u§Pü&í.ѯÏþœmo°Xé­| 8ù^.Ĭgôký·_|÷çVõ_$ñÙwñûÛ0†6Â_ê"X‘hâOßµŠ}I�ñÎ}b5ÐsñZÒ�k³ñ­ÜæyVT D€CTÇEÙ¯óP».£â.ÄC­TZð¢â&.B¨ºt±œS]Z™cªªΤÒß·w”tŒR>ÉÀŽV/eµ”íZxüužä<'uwµåÒ»&Z3Ï€»ož{Þ’ã°à#„)Û Kà6 ª K³Ôò*¡z-˜ (›5ÔÛ9)ÖR·Òb�æYò˜f›ˆJ³øˆiõQ>Q£K« ueÂ+«Ëœú!ÄŠamF9ÌR¸wtQJõ!ƒ1»ŽÓ°r”«¨h¯õšÍò…nðÞŠ—ï’MçzU�“Ý3×Äa²ÕümŒø%ÁgÌÙ�ì§ÜqÙ�ºç¸ÕÁ€ö‰Zü´\Œ‹UÑ㮤Õ9ø¶ß<„›<zQñ+ñŽf¹xýYUÀÿV¯?N¯ËüÕg/à?~–ÃÚá•D¬àG¯ƒ`?^œ_½dÿûÇW�ôá§WtûU!þèòUp…_䨤‰ŽÙ j.z؇Eð°`åN éláþ÷{¥ø~ÿxYÿçâÓÎiÚ�åT(�¸àÛ¾ÀQxÁF¤cA­aK§ËáÕ$~'†9«—Òß2´)}y¼�Ð9Ïã*zýE¼I�‹¯¶Ì  ‚‘(¨‚þà<ø>/ ö¨¹û6ºþ5+Þ/D³ˆ©G‹àêââ륒͉?ŽÄ¦G§’)/âÑQù¡Å˜mN<é/¹Aér_Xº-š:ö¯¿ôõd§úKŸç£t´´ædÖ]£»°Þ\waÖ*\éÜd�â2-z¨¸E Mn•��BºáB¼‹WÑVèÄLÌó:“8� õ,¤äd4⮡¹,IW£—núPD›0N‘MÕ­áׂd6Y÷Öâû1“gD¿l�ÊCslÊÏ.[ JW¡jhWеŽÃâ:® íÍÚRÖ.ŽoŒ ÷�d÷¢c]�1¯úór”Ý!üw]š¥gitC)1¡56VuT´hÍ~eXÎשA0›rÙ§²(Ö¡í „>†+j¼.©’Ö,z|§Ô(Ô\yc�,]Õ—¨†E÷6üíçÀÒ2ã– •”tP4‰Òô/°ñù·ÕÙª™@š&N¶âSrË÷vŽ‹@Úί¤nCGËà.ÎpwÁ® &{ú²õƬ÷ ¼|›w ¦ƒâ=y¾•"k�G>Hݸ@‰4YÀê�ј~ óP�o*{’¯â©/æÓë‹4›XÄ!tÅü¸uÅÇZuË­tEãïÙö–~~å é峦7kzã4½e£é¹;É<4=-�¢)æJÑÔѧéµuªt²ˆ‘=:Þšg_¿Ìšg·®Ù;LÝÑŠ^'ÛKkÕº|lª'H&2�êÙfÄSõôÑ!ý´TIó$ªg�æ9Ìm ªg‡æI:UÏ ø °˜fXƒõØÁ:€© ^Q&&ëg&§ÝXÓ4ßþþµ¶{ÍÐM;ÿšî^[°Ã`Ô¸ â¨tñ–ñüJfoYÉX Thë)ÑÔ¡¿³CZÙ¶*abñ0ž¦@—o"³@ÙöK„oÔJ®³ú'Úm«lK·–f„$QT2DS]3"nÔ“ù¢×¶”¨ˆ«yíå(”ÔÏ.Á婬E[í5½†ŸÆ(Pƒž7ƒã�¸zÞzÖ i+QMÿSµûšFÆ·1×[µªÏÕørÇ¥(Ûj*§ª@´£j‹Ê©�ö wušÇ¤Yõ{Õî:]ñ@Ÿ›è;UÎõ)nˆß«¨…l‹hbßyt¤N�…41�bæø/ «“œ¡Í{Ê#6øš{ÓO…úyqþ)èžE­KÒpR®KrÄÚ$Ó<_¢íóóફ–J)¾€_ªHûjÐt`^O ÕXk¶l…fÛ(¶ÄF³Õ´žèN�Í£ŸGÄåñòúa/%Ʀbíf�}µãvS‹sË"îÜ*‡Â`S?Ρ扚]?ßòÿiÜ0ˆê–Ù�߸´³ü‹0ÉoGúYG¾†Óx|óP@?8y;ú4‘Ò]¥Ä Õ Þ§Ù}ïΘaÀ™Z¡Hð†Sˆ$þxUw}§ý‰(“Î$‡ÊÝ>ƒ7û˜Ž2;ä/74ihb·×¬Í.j£…j­Ú+ÀWcÏð‡¾#Õ-pÔ�›ê"ˆÏ£sÁî{w Ñ· ßbƒð½Ãt %S ÉÔ2´p^ã)¨ÌÎ=v—*#lPå|)ƒ“BG)‚m¿A[,Þr”2gÒ2,£ºß’ˆ¼¢HLô¼âþWö"wê½E|vIÝúJêù×\ë(Ãu·¿äìØGmB’¤œ°ßâ""â¾¾Ÿ Œ[Ô§@ظr5õâ"ø]prœuŠNô¨-£”š´©€n©óÜŒp=Ót|¹ϰœµ�¦�óÓb¥,aK»zÒ˜…èE¡LW•,�§ÉÓ#ÿZùz¨ˆ�2¾µ³WÛ§å×HS¥QX)܆làe®XµC17áC¼Ùn‚¼�¡†/¨d ú#<â¢Í¶¥fjEa Íb^ Šð\ncʶéþ¤ýÀY¤-„ðûo•o±#@Ú¡Û´a‚î0篈#¤Ã ý¦‘Öç’L£å{$‰ÕåtÉPd>b|LNªèŽžÂ¢ÜœSÑ *¤\KR'ýãr‡ftGoߤÜʱ›7Ù_dzbÑ;)Æþº½h½k?’7p—ÿŽO…¤Éveô±ËäÃËëy|¼ô<›Š§Ðd=Ú¯Éþh ±šªóÐRuüúæ¤^ëgêô šÙJ9*xL5V¹ Þkå®7E½ÌÚ‰£úf Uµ7ô,‚!õm7¹y¯¾um(¢ï¨–úF·–›úV+ñ³úvXõí¡­¾=¸©ob*G¨o–r·´›fÍÚͬÝL¤Ý,™Žâz7k7–Ú ÕY¤ Ü­Ý䚟nÜù3^~?�MÅÚ�Ûñ5»öµ݇r´Ÿ½îÍDH‚žé|Úôzf*3S™™ÊÑ0 îÅTòÆÛ{Ë ‰©Œ;?ÁË×?qŽg[;HU9¢ñ½¤®téŸg*Ën2â‘Ì’ö×zÑô$·$�7„ù�1¢²1?2ÖºlsD¨ŠÀJ:CUFÑ/268Å1�'_®ÚäÎY:»/›ŒƦ‹dÑØ}r¹sF™•Œ½¢³³u_AÂD'E¡«Ülè>*C÷CÎ4&ú×QezÈ™ÎDÿ:—eÖnú×IqzÈ™ÆEÿ:–ä:{pUºr®u±‡^µK‹æ£ë<Ì+ØAôpGÍ#Õ.¯xX»®6f׳�>!º.ºÈAtõ�.í£#2–ØÝÑÚ©}ìÔQ?kŠöá|Ìe„ôñ¬}L¦}Œˆ%î %vÓ>fÊ>Sö))û’ÓnwÿÈLÙ;){®{JFFÄò \<%Ž¡ª-ôSwÕÊ}|q©“š·›ÀOŸkª-&Ù�y»¦3Ã| sF÷Ý�ÝÇóž5ºkA¤ü-ý£Hy­èÅ â6»ªÞEŒ¨u[û íîŽw¨YúúÄÒÎuL·×]æSœ¾{Ìë‚þñ�|næÈ) g.7s¹�q¹§MG OŒŽ¬Õ Ìõ¸�Ìu_ŒÞj}©}¾šÀ 1Aû–y­½û-ݱgG�×Õñ6ΫãÔXê¡’Ï‘yÞäDsN*MN“ °nÖÚ­Öcô }V‰e5 jMcrÜV’]Ü‘wLq;cfL;Ã]öiû1Ȫ›¾xôí™ ©¿žíÇdGXSè];ïšÂîÚÙ;°¦ özØv Ãæš�æz2Õ€šuW8Í t)Á/ë¾X[èlEnøcÖ£8vƒW5\ÇlhAÿ„´…F£UV¤�VÏB*ÿ@ïFM›Í‘|— xË¿‹›¯[•Л^{LQÔöUñr*xÛ*KVÌäWxŸé/N†€Ã ¯¡Ž¼ ê‚ÅUAÕûÚZB‹ Ú¸¼�¯`9÷oÜÓàѾª¯ *õE{S› $^Òüó‹ n²HÍêèÚnåRn#&hÀ…q ÜÞxeKk //þí��,1+˜¹M˜$ }Â:#F³ µ] WŽqƒ®ñFW¶Œ?ùÍK:Eìõ×ôU/Î/®>]—ð¿è�õ{aüÇÅù'WWWX°ªõš±àï?Yð‹®õ™Å²¿ÿÃÅùLކ.P•tí¬ã+]P�ùë.W¾›Ã}Ýçn·§ ]ôø|ñ;Õeû¸�½·~æ³Æö�‹`_MoÇ"Xõ¸®Çù[ת_Ðˬ9mK;ðÆz7½ç¬}ï<}µ6rÒÃo»n¼¶ƒËÆ+kÏ„ðHLøØ›]§{í“;·í::—׳kyr×ò³ %OWmU"?\½ 7ªû�}1Â�(*èPl&»�Ò®þéz­>Ý‹Ï$÷Ë$=l"ï™ü ¯C;þ0¬›mžQÞ†y+a_ØÐ–ѯ•ôïº&-,ÍRРFë"?S¥`}U½MûrµXh¤aKM7½9 ñJ¸ó¼%bÅNœ·¤l/y ç+Kžï�“lEMŸä¤Kòô¬9¢a…m8Ä_¾z÷Åß¿yÁmD†üŠíxÉø6EÏ‹QŒ ƒ3å¿þ×"ø¯ÿ›Ç(Ò̤¦YVê’¶È‚É2MtˆúܺޡR ÍŒqhôμ‡¾ü?¼/°‚YOêÆExNï+(ÅÐäµM <QòÈ…q#ñ‘Mª“Ô±, 'uᜠ¸ás€ Þ‹­ˆSøX`KxS¼JÐàÆæ¤ç�ô5FáªÓÞ,½(¾gåY�Â"–fÁ>�f{ézçÏĪÜÒg¶JÌFØc2‚¼dW¨Ð¿ŽyÍà KjNÿ:—¥öRö×%jJÐxWö×±$‹wåŽê*”b +èUYÕð1º£xDE!<6*e+|Ç–û0Xgï—ųkãƒtÈ×g¢0…™(ÌDa_DaÉÁÞÙ²<…n¢�ë¶íiBwDE#lÏÂ*x´a:ûcÎá9�f€ÑŽH2“‚™̤`&Ï€Œ±<BãcFxE1#Ô &rãUÑ Ï-4b@çÆ¾@&€ç6:�į|Ư©ñkv:ìÂéÀ•'¡�ÌÒ@H®k•EðŠÜ´KY­;Ú ‚Ýâ…Ï-O/f}gÖwf}g·`5FW:z°Ò¼Ü¼§þ^n^A£çLç_îªz¾lë¶öã¾îÇÚ,¸}¼Ö´k“íuAÅ—q‘^‡ÛÍÙ÷!Àþd'6Þ¾‹+6áí7æ36û�§ôÏdi/déiC®ƒnyȽӽŽwc½Žw}NÀ÷ʧ2¾Ù¨±E6wH¹z%'é�µ×2Ef âfDUš)?�ò§oþ~zü¾Lè¥mXV=tæàye,QŠ'Ù’ !«�L¾Oó­èÖ:|«%™XŸ6¥# ëqäZìô�’.ç©1ƒ�q®›üÄ}DísP6!‘8ÞJðqR½ .j¥Sô“#jX|óæ�¶¹TÚØ]÷�±ne Ô†¨¦ým-o#´~ö´ *5_�?ðÖØuß?Dð‹D», S^ð‰Z‰02ÖßûëDÊ"V£È4C¿=“[¿d�‰áéë©”àûòÅ`UjE¤õÎLg9ÍE®o1Üa‹ŽfòæíuM+0и(`¤ãMxS;qXzߤÔGQ˜.¸ÐWGQãnô"®®­Sƒ7p«4Azß³ >.ë×¢Æv¶üy–§z^\ñš¸(Ô{EèÜäY3yÎí˜43ÚUø¦Óíˆ4“ f¯'¦¶§Ê"‘¦FâàEkã³�t&ÞÛ�sv¡£�ƒZ×ÿ3¯ÿgZ¿#)µ©eö—莳ô;–~ÇYú�‡èŽÕîœ�jwœáß¹Æ$Fw<&‘=8kwB;¸Ôô„»©îú<µC€‚'Ç·0Sñ)©øÌ7g¾9óÍ™oÎ|s曓ðMnQ¾ó°(Ï|³‹oêÑJwSE+ÝõE+yóÍ!sïÑÆ8·|œ¹åÌ-gn9sË™[ÎÜræ–£¸å;è³æ–zpáÝØàÂ;5 nŠØ†7½“àÄ©ú²§àEïîz7öë£Cïš�Gۗ鉈$æ�H)u�»êБµg¤ê@Œ¡“&ÕÁSYØÍ çØÍ»9vsòØÍYc›5¶Yc›5¶ç¡±=m¥ÃÁþÔ”Ž\‹®ÎGWçcB™aT#ï Üa—<¬ÿ d¤ÊŽ?¦ú¦Ýç=hiœ�ÚùNBF-7ºŸ4Tj 5®ÌÑ‹ršHë�@Ì7Ç9r;öš¸_^óS‰§éíH¿âTªõvWRôË6†®ànâi£‡´�´ÂóM•³"ͤ:çœ:¢ÔZHr>QDr>& ¹›Z÷Æ$[؃fOK^;®ÚžŒ¼:_Ðwläufc3›ÙØÌÆf6¶76Æl�¹»©sfc*Óvó‰âuó1áº}lÌ€ G¤{üÔË"™áL½fê5S¯™zÍÔëéS¯X¸MÝ0k…Ž=Tôns¼<©†„$+#VyofH¿ù£Á¨ï‹³Öš~!/äŸÄU\hˆMŠk0†¡fΙ(]åth’`¬R;"‘òrKUÚ•)”mKýa{ÑíkŒ'¢Ýk§}¡¿¿‚ªjÿVƒ§³Ëæ�I°M¯qÓÁ@¯MiLÛ]âÅ›U)˺ޙ®Dø�DkVÜ�˜u\ b](WE¸Š—UI‡?»òw›Æë¼�yÉbf b…2Ë!Àß½ŽKöL]qÐH/5Z(0Ýf³ôþ[ké‰Õ†CM_˜ðAü¸(Ùf¯JãËv�pÏ*¨¹ sŠ·4­î&gT“Ih)K.[Ïl'”YÞÈ6œ†N�Zåo–j•«ž� UÏÝbž-#EvÞø.ÂÕ'ìÍ~ÖÇtx‚�uï÷ˆRÏíƒÔ «¯¨N:s÷Z¿…¤>·ä®3¯Öê3П']'f�ÙýÊÚeÈ.Â×ó9z}oÑë³Õb¶ZÌV‹ghµx²äÜÞ[òtÈ9BF¢v‹ïÆw7uè~/Úk[ù§S=Ûâ¯Çi}� >n]>ØÜ��Fždêç-ÿ÷ŸF�†¨q™Ýù Ï¡ûË¿“üväÁƒ ÞÄi)¼þDKÿáÞOå§'”k¾OÑðÆ‰®zà)+ê3n$xcy2ê”Ö¡CºŒ8§ë^m¨÷ zVb£ ]»+BÄóï×Ñ:Äû¨x´=ªj§€¥™:ß_öéS`4º­GÅ"ˆÏ£sqΡw7};ÔÌŒ«d;`¬U9ÈòŽ�/y¥6å’‘Fy‚d�ƒØIýÜBß´ÐqŠ`WoÐÇÁÎpÒ‚˰Œê®K$lL¯>†âQùêŠÙIùô¡Õ9ž�Þð÷¨I¸2¿¤n}%u|A™z„7¨„^bö—œ)vÉc[ˆ ;7=ªJ€\K [¹8¿øtQëy¥t¾öòâO<^ l�Œ¥e„˲5Ð-uª¥ÅdÓñåöÂÖÎûhc–d÷¨çB¥ÌcÒîAÿ‰±°da;Í5ï&)g<3#¯=Ò(ÛÛ Þu´ «„ûÅØ?Üh!ûX‡7áC¼Ùn‚T†è0Ìð»îG²;Àìä ;6‹ 6Ûª7L•Üè`„.•þð\nã*¼N"º5i?pi“!üþ[å[l7\`·i?Â+¥ø¢ŸÓX2B>Á|°I׳eÐ–Ì 3µ@Ódzk~´.7m¯]döKUH � ¦èŠGGe—c ¯xô©�ù³Ä£“úK 1ÕY<º—ç*týìªFÓ‚\•®ŸÝ‚ŸE¯�¢÷)âÚq�fî\qõ(HS‡~ÄK?µ­| Eܳ­ÑŠø�vþ égFýÌ«{Nšñ°^y¸~jÈ­óCÖzé!o.©“oÈ´:)ƒ0�´÷l zæþÃÔI»vÑ·’Q'¥{ÊM'4뤇ÕIÚ:郛N*fr„NúAjm±¢¶5[hV�fÕhBÕhÙ(6îÄY5šN5¢ O�i«O5Ê >Ê®óY–粚:&ñQÚV>åíÀžm©HµÇèh�“=VÞ{ììÙzŠfvsgŒ\ŸÙÍÌnÏnÆ›Žgvó<h� ´�ÝhG$šqó?&ÑÔQ“�iC•ü›ÛÅQˆQíïçðƒ[=�; Â爃è«íÊ1eßçgHwö}GTçGâZg$JåAâšÃ¤óôÃÚFö�¨¿Y«ói‡)O;̼÷9Zõžus3m=SêF Y}w”™ºÝ¦jà<ý<"hž—·³ÝH‡K¦K€îÛË0ä?‡Éú,Åýýb‘o¥Î¶CõF˜ƒ¤qÛUä°ÚÄn B†0:ºŒœ—ÐpœÑíLqvýÓÖ8$‰Åع…XÊ-ÃB ¤×‰¬¢uœŠlæójŸ«�q…ÇÎwöÅHi£:eW¹ÙJtLV¢Û”’úÇ oSJè×’QéJp›R:Aÿ¸•c4‚ýu¤·)£ìoPöøˆŽÎßÌë± Èê&­p«['ü=`ÌÕxìNè!¥¾Ñ'cø6ØKœÀwÆ…&Ä…%“îÎªçŒ .äšb8M&Y^ÏXQUÄŽÖÅ(°ÈûpT 0+3È| 㯶ØÜŒº±Gò…}cu‡=¸’1(Åéê'dšÃW¼ïx—¯¨©ËéëLÌÚÞ_WtHð4HJœÖC€œïÜ=JÔhÏÈWR…5¨ïysžjbDómOI€0z}J4 J³ô¬¾ûi:$µ(ÞŠçKߦIT–êjĹãÉÅ n=U¥r²²ùòra\ÆI§NâÎ:\VDZ'Ê áf'œ]ä·Y²@½ ‚“ UØgÂæÇ>‘qƒÛimñk’÷oçN4íM.Òä‹çx+|4y2vmHfj5{¬vë±v˜Ú^ñlÙMÞ27�K±P×p sSwgZ ´™Ñ7zÈaaÙÚ‘Ž7tbŸôÉ!^âˆéS§ÑÈ`&2Y‰Èh–¤’$2Kêœæ¡‘·`Iý$‰ ØŠèÌj£¤ð ñ–ˆ ªqŽjíšu�S˜²kb¤yfÝ O"t!Úäðm3ÌÒRxÃ6NƳ’ÉžBÚY<ˆ.2Ã]¼ŠV }™<âè…)áÆ®çÂ×\ÅëuTÔ³îIcIp•Nˆ¼$Áé@e|�Â�Ó#j€òó:šºÁ ¨˜ˆ*#ZÆWp �ƒ9óîcÔùŠUTP[¦HħˆÑÙKŒïCL>èîšckÛÎ=gÌ>k ÀQ¢ò×Óºœ~¯Si,¦% šIèlß{Þö½q xœup� X‹©múGU‰špžqœw‡Íî"Öj’~ì'ìʯ«¾XÝ8à„Åúî¾ÂLñXdè.Rí‚8±çkE�«wÒ'juÊ%{'vIbA¹�7¦Ü:ãV'#í¶]º¦‰½7‡¦í14í‰RÑg@h ‚c ôíF� ãߌˆ «k°;…”„›ëÕäy*FtÂ2bLøNÿnË2†Ýpô1c±Þã6.ƒ µŒx…¸þyüv•²Bkc9+´uÅ•û‚š&qÅðüœž½Ãè–¾¢Õ¾Å1²ºýþ«±åN©?µ«þÑñ…+¾Ëµ:VoáÙ…wL.<ÜÚ±ùƒbc!ŠØüÁ½4µ*ð—8s,B#VÔùƒkY¥.ž™cLC<õ3 54ªéÑ¡QuMvù0zG+$ª%UŽ:(j „gß:=„ð“$Æð‚ø1‡¢gH›!í9CÚR€’³ ;Cš¤å-åyšDuM£•hƒ¦z´Q)įIrzÌø5ã׌_Gƒ_㺿Tový¶ÞÞ캆®ä¶æÞ4·ïõ¸ö÷âµv좧·ºWÔ{ø«y¯í×�WÞ ¾1"yÈåð˲W3¸£—¶^k³—vê+!f¦·;¦÷ ÈŠ‹ªþ4ÉÊ}\ÞjÚ6ÿj u»®ªFÉÈyª„?•)»ù™Wøù–PÔÂͯl5bMþ:•žR½Þò Õõú ~äL~U]ex ‘ í\$½ÕÕé"¨(ÂÓŒ åfê1¬!`kÝú?á½×ð´’CÞÕ±nÔ“øWq_¶6LD§àä[~”æŠÊdÖÅE�B{ZTþ)N„à,¸<=?%âÀ: oZ³NïÆ‰ŠøADz¾2»1NõÇé*,o_Áo0œ_Ht—Å«äcT¬zr„#¼/F@×—›Ëïîçy Ê À¥$®ÿBcæµ¥n8È"ê�jù²wÿ¾•ãNŒ DX)ëæ|Ö™�FgF9ÆWþä%µ�­å¡kiÚ¸Ôv�4×tϺ�£4ÿÑ ®¿~ÃõI¼«JЀ@¨A±Ÿ_¶õ!¶ßCË ¤\µU “<ç±ìÔ}Áa'æ±òVŒ|\±,_9;ЖÏC’™Ñl#™O°œ8û;UÁˆ”Ð.ŒÂW·Y•ï�¯CJ|•1®•Iè༠•âº'ÆýTÞ/*80’‰Ñ±bóéO2øÀmœÞj«MJÝ"ÙA5�Òì{­Ï@JzÅàÔ7Ç‹åÎyæ hÌ¥dÀ^úu´ñ<'�§E®ßµ&!�2pð5ô¯ý=סHÍá^qE=¦VQÑΰ²y{¾m›ñfí° È/Š#ìo/ä:ŒË´"*˜¬jž|u‚㋎™ÚÃBlÀ$ãòÑ @€,—¿ßT¾ÿ·+¨ãÒŒ6Á:5`GÄp b‡3`Ì€10–0–}€‘„4[�ª=4_ŽÐ äJ좯£jò}czay¢ïŸYÇ�Ottª;ÏNâóèœRƒ×�5 (Īéci}º�˜ƒ';c'•&zY:+t¿ÌÔgB¦lR)¥Ö/Ý7Îòoš™à­i�éM°8TµIjàÔa³ò]Wý4Ç»W›Å]Ùrw¦ÉnìŽo„¯Ì)°ÀTfŽ=&Ìä;†FT4Ïn6;^Ž*aͳWÔô×<b¸zLN~�Ñ'åäÊìËuby먜=|ð„ÜÐ)‡Ž…ÎÞcs$è�èx´Ð9ƒÆ S€ÆRøÎq|�ܤøM'W6Rl”¬ã³²©re´uÍÊÕŒ3B<]„˜B5qE5ÜN.è}ÚK®¤ëx‘�1pííà¼×ÈìåÀ—k=O|™¦ÇA¯¦·¶ËÇ+#© ª[ðên¿ã]³.ç|êL^ðóÁ³ý¥}”éù¡¾A·ì@}$xªRȾã 5Ø©�ÒE÷:½û`ë„°õã÷&¢§Séªúˆ�¼ º;QŠÚÆnó¤8ÁœÔ±!œS~:¥¿�-pçÅ=�·¯gMY¸û”Mãï3wÈfxmn8c*4ëæG¥›Ã�1xeŽØ …°²÷ÒLg.g¨±=Í\˲ó×âÉñü5ÛZ!o½ÿüu=À �˜È+)j²sIvÓŠ–O2±Çò:%gâ8>#ØŒ`S ØR`�»†9#˜ ‚å-…x"©¨i¬b,©ŸGë!�áj†«®f¸ WãÔµ®4—­x[­¨¡Ë/hi±�¾µ]øjÇ4¿O­S=ý´Fqîã¨e�µ^7^nZ'jaí§ =ŽZÒã©Ujý`¹„«¯V,ùÙQ»?GíÓ`tÏ€”¸hàO•”$-§r2Ú©œ�SÙ³öNe[ôáýÊ=@L“bÍúýw<7ŒTXÀ¸l`°¸«SÖ¡zs3b7+°h�XO½2f7U×ùýšj¥8Ò)£ÖiVTxs3KCŒìÆ�AðNjśt}JÓÚ-ØÜÇed¢Äŵ�Ôµ}vŒîíÉá¹~KWˆ6Taú/_½ûâïß¼àF�mQ‚ŸÀ¸¥la×ãÿÊ^–åµ¼fY!ó0Fâ®/·ÿúß‹àÿû¿øj£'&l¦=®•Ý<6„Q¼‚ž EN\ÀRMX¯ªÌœ:ó¿þKåH[ÔÿñšgÓl¨8ZWvä1¢”SòwŽ’ìþ%»-OÐgÌÐN÷†È¦¢¢χ§•”ÅÀçÁeð"�˪E_lE*½Ùjvp«Y"(fâC1A1'O"¬f‰»Õ,5q'¨IMP?‚šÔ5&¨­0…d²0…dga 2{¨B†î†Nr·èÌ ÍÜ0÷↗?y3ÙÍ|hæC3Ð. v‰�ÁnæCÃ|¨ô’Lô’LôRKýcŽ{Ù;õ™äZÚ™úìÜ,váj3“�™ìÌdg&;{0}d§2•Œ™Jö2eÝÚnB¦ü›ßWÈ”CýC¦&ŒšJÆFM ^cìOLÉ´ASZâÂ�˜©'ÃD:é£4ŽŒV /’j³ ¥J ¡TXjŽ¥j†pÁT3gŸ9ûž8û3 �.º§J;ÓVP\::(.Ýѽm�Ukf»ýµ4“în[ðP«`½ÁÆ~4ø~ó$S?où¿ÿ4v(D…ËìÎolÜ]ùæšqa›£_ÄiP£4^süã)Dlò®º¦�ÙÍ^…Å*XEw±ñü¦2Áô-–YQDež¥+z;Åf¼©Ì–Pð/Cœã>IлöF©¾YóÞ¬› �¡Á8¼N�ô% R\¼Ò]ye ÜæÃ§BYŸër1==IÖµÑLG›NÑ›ÍeÓkqÉßwØW¾m5N³M<Ö¢¼ßÙbs×oú‰(Pn¯o£±ð£×õÏ`>Ápe/Ý>]Ë·´&%ÃMžP„ A¬7oÒ‚öÖ™i1 ¨º…’9‚XÅœäH5«6�JoØQß$¬¨Ò…á2 õQ ÜE¡è3­9$ýÝæ@/ó¹Þú%®#ü§¸*¥÷ÚËš÷ž´:u'IcviUïúùæ�M_ÿÒ¿«¦BsâëϪþ·zýqz]æ¯>{�øñ³È¯$b?z<€ðýñrü~\ýô vß{üââüj\œŠÿù¿~„oiÏ‚“‡üèô!JÙW<\,‚ÿÞQ ðÐsüûÏ;êá\ lÇî¾À×xÁ^ɰ¿ÖQ�dƒ›KüNŒQVﬢtõüóyð]XÀ�~¥ ï‹ô&J‚¿5ßbpd\E¯¿¢Æõ�ó»AIðg(w�eïÙÞa¶/˜d,t|‘çÐÈæoàÃX[ŸþþgŸ~ú‡EðÕm˜o€…} “ñ jI’_ýí+Ì‹‹KEø}�r˜ßÜ…È߯ «B‰9£©_bTbðWØ8Ùò½èsÝKü2(ëþ±>]~òû³ËOþqü'Ñ#´Ýÿšïq¹Ç -FŒØÒ¾]Ô+7ó8U¿0T8«–:X!Üèher|Ë2µ$ÇMVQÃ1w¤d~ØÄhñFÙšÜÓše"ÑX3Q;‹SÔ0a Ñÿ”;zIYËè.* £\’� [‚n£†¬P+§Ðx¨  §<ꕽ]Å¢&ú60 °˜'[í{7=�õ‚°¢²§ÿÓ%ý§iíœf„WÛ;cmTMu›‘†º�fJmèâK”]{BGë «¨.�´b«Ð­©Øjªâ(Ó˜•þ½½U+ý’Pý–™k(ûâÔE£[ ^C]hÒà@å­œuQü—©‚Ò¨<Ê!faîï'””�Dе•ó+‹jˆPªG‘SÎ÷ǑӎÖ9 ,W-%X}mg“f‡�Í›Äw4ÛHm²«‘>¼ÃòVû}×tµ'¤ìbríW,‘œÁXßD)õ­ÎÕ*°7Äp¤�¯†óz€¤6k[ mÓÚ"bE7QòïµÕ'¦¿“âáž�XE(d¡_ìåålñu(¥:"­8¾iV­¨�N¬6b\§ïL„�?,Ö…^…ÎnŠßtÅ0¿”Öksܬ3"­m£Åz>Ä›Fa^àÌrlÜ<†ÞÚ)$ÄV#‘++de„ ˜“ä±¥–ðœ eïÚÝ›z’bàÀE·^RP‚ú\ÒZ/a…‚ßá¢+ƒ“OÁå©G'—/¯Nk­Îê±Rg˜,˜Õ™#Rg&!ÅÔJºfàR|§šg_Œ83/*˜þÈ|WÍ{8ÇÞnúgíÈóÃÏ[ϳ#»#§Æd9_Žc”Ó1LØ2Ü«ÝݳmüÃ̪(°>P1|žÂx¯ ë‘yÕÉz¯Üƒö¡ã¡Jïé#†ùëi Çp ¡jA¡�F1óÞ R0¹»¨x¬'e’CV™bb€-Gr½ ZÓ´:M /¶ U7±Ò¡x�f%Êä8­+Qú¬“ŒùU` õ8€¸/¤¦j…TüH:@rŸÝ›u%¼õVÕ€n1Tϰlµ1"ÆA:áèÔ0\À‘³-{Û{Œò€M&|ï´ B°¤Q0%�«‰69,N>–õ¡s±*¤-ó®cñvˆ+X¼ºkl­�a­ê×S u·P  ýåç›çþÎsIɱ½¨´KÝ"¼_ Æå×@³èªäVÄ2ÚÄgq >{vY…ÔÑH.;�Äêhݽ~4J‘&ó“ÞË¿ÿñ÷“ËS2ò?áaüýäJ|¼‚�çççðÕ×⫯Oÿ³6;j™�†ÅTéocGÑEøq˜ãFTewh•fF[1¤'·�0œg͸º”ÚWaêËøTÜäÊÏÄeVÚ“òsûT¾<GÐèyòØ>F?D$ÄPë ‚§HÑiP OðD«ª~ÇÜ ˆlÈ“–ðöÅv ݤ,4Å^‘%µñf)›+pJ¸8Ž êK æŸ‚ÅÈð¸_RœPÇ v^CÎʨ=Ú„ïiÌ2�Fƒž%+¶½Al KžŠÿ¸ 8 å¨ãe›kQ5ñŒQ�æyýNùAÀ~†¢"Ç¡Ÿ®³b:¢øXnhYªrÃÊ[U·ŸtuñA;û.K@sùmÍ™6.~ù‡Oá—ßq«Gx]‚h†Qb‹¡Ê’¨@ô€â§ÁetöG:î_Ó 2ôÂøö÷âÛ 5û={›ÑÃ*0tø/×­]¹Z/˜õ„‡Çh@aÓGeÀ†…i§!bÈÚjó&4xyqÞèÞ}ƒoƒúhRZ�Dƒ[|5IÑì<ÖKœ£ òéÍáÅí:Úež„�½â¿ øÎ�v¿ºÍâ¥r\ã·ÙzýÛZžðUxZ眨Ó?‰pûò–�GÑ P“å„­ c$.Ê£ŠkÕMHâoAš…IwõôŸõFËwØ´±®˜ôm†›Î=ì|Ë7ð;`*˜ce½M@ ÊÃ+ .\ñžÏEÁ¹ˆknô¸¾s�Ïä`v•;÷£�P†9*ï<•wz„ÛÝT!nwS�*4iöcÏJPý(Cun¥Õ›Z·Véke½ÑÕ Šº^Ø™`¿ý�+óØÁ·g×�g_;ëõY‘g1M”%kíq™ƒ\c‰¨Zñ[)ëÉ¥ä#js¥þ è8÷ªÌ‡ßÐáÿî_ºf@L±žU§6ŽK6ìõEä›�f†�QrzÚ§òBPGÓøZœ‰í2‘ÇH­Ìuˆvm+`w²æùÝßß®BäKâ%•×°ÿ§)áÆ³ma¢‘†‹XÛeú .òÔ[·ß�-k]_k3ÏÖûÿó–a–Ë’ fÁö’@·Aý[5[óyt¾>7!ÀÙp×¹ÆØ$žÄ§¼‘-ÕX»'ñâå©4”M '//¢ I¬ŒæâÆz³�£ó 3óÈÒeçQ =íÅDíMí³ ,'Ý¥"C©qì²=QRÿ¹•jYú+fbžê¯ß:ƒû suKÚO‡‡É©©G˜�Ð&DjcP˨œÀn‡4k"É4ÙÒ.�+e£“ aþ9é´Š.^®‡VEiüiGwg²zP²*ÿœ/áwÊ÷å/ÛìÕï ÿ„Îe³ûäk‘¯Ààn2µ0TuŒñ6ãæßåm´|O�Âöƨeœm¹²¿W'.‹Yky½dýY›µE:Ú°±x¸w�qöI}Cx˜’ž¥Lp­×ñ2¦GÑäºÍ¶7Ì0A“jGÁMÑâgËÇà†zò©šC·¦hJ“3�º?¯@ 9*ö«™j»Øo¥ÇÀUccાHµâVõ(ãO½éè@ÃqjûéÌá¢ÔÚ}è·e_mÑìÈelÅ]¬U]áPC³ÒÏ6U¶^¯ãð-»ï‹´BïëžQ [E :XøÎ@•þvØæˆÁ8'Ǿa·[Qoùõ_+×#(¶Õ*Î"üI˜\·!]³£¿cÁJÝtjÔ™,=ÕÑf'ße_4÷ ¶¾h�Ø×êˆIó*PŽ·¾úVi¥}`оG{FX½"¬Jê<:Ö_Ö̰꺕6N!Q'ÚôlG¢-X`Ùð¯4Ü€›~²"¾‰ÓëqšéFÒ«9„¡�nÛÍ ßM‚Hw¢«w#?ÙxKhÌÿÂøê°Î°3­�„gJ€}–˜;…³åPÝ„5'1U[ÏàõªMö5ÚשӯÑ<�(…h(.n¶Ž�êƒäXé —9Úæ}Y;ƒŽé­ö†ÕJ[>må;PT Úã¢*‰†Ú5ÍÒäJÒ¼Hí¦Å4‹–Û¶h¹ÑE›¬c<ùV‡:zÚâáïn5Ë Š�²„‰[ñµyRÇÖkE[»K¬ÖÄØb |÷Ô³\;|à´aÑÞjÏé¾Xª/X{ôÓùkÇíŸðÿ h3�‘£‹ö‘&é‚" ?®O­N¶‹ ´ý;Ø«!cϪÛEð¦Œ*Ò¨RŽƒƒÎ-oSÊÄ�OdEEI$øb[e˜u|Åø<Š‚ŽÓ÷Y”Ó£çÛ4Æë¾`ß Ž)Q_¾xÀT”çqùxžÄÛó2z¥/ŠêEÝzÁ-{çT†»ýžc‚wJ­™.ì$ð§â®/º` ùzàO56ð§Rb|ü މ±l›ì þ±Œ‚i³×|Y´·Sñ‹|7‰wd _fµ°¬ã¸ï/L MP¢§j‚ê³²M¢¦1‘R’Yëâ_1—�pÿJ™åÐD U¹a?»Æ�Tvä ¡bJù]4/Eè”NŠS˜êÕúÅ Èó«Óàw:Î_=øDë v~t›á?5NÅcl+&´°ðzš}F4¯˜#—Kjó¿aFX£Mš´�(gªMZ-!9£_¶0°ü²¡:¸]�¬w©#c|6JsæadY~‹ë¸*ÂâÑt¬[ß*T‹¥ƒ�¯�¼ ÿnâ›[Úyrõ¤ •©›Cò"H›Ñ×§Eš wX[·”AQá\¯MÏ„Õ鉑τ+y½šÙX!¾Õ‰äÚ¯ e­kgZ’6¦9eX+×9e�AÓ³QI-v½AW2%&ŒŽ ˜yZyP�2FÈ‚yýoµ7N+x좦±‹à²f±Ÿ¼"M©Ž�ƒŸúkùñêU@³EaUW]5]Yæ|Óþ!%}ú>Ül¡Sÿ”U¬�ß×KPÒ¼ AćÿÞŠNö{~h”q�½¢1nJþ§¯ÂÍu¯n¢à_• þ ;Èhýo´{¿ßMÞ§q´Q³2™ic¾Õ�þ͈˜™º»³‰ÙdãLr¢>ÔFhêåfâÅ| Ü„›x’˜·ë‡Z$¬Éú°¢èi;<¤„õ_É(\µn �ÆËt;O®Â¨é.EñfÝwõ¨mì⮞ákqêÐ ãý8õH1O Èyåîbþ-Úù›Ÿ6 ·þÒ¾÷è˜|?8ï…��§V¯èV`�§zôhæ2ž˜â–z–`3èİüè7×QU÷‡øE’ÝI«ú¯±1÷Ç÷Q›Ó6ÆBó¥‹Çd¶Á=C/ðánŒ…(ó÷ÒÔžÃ\®ðÁ"ôúþàZ–]ÿ#ž¯ÿÁbìúñÔO>è+䃎´�£êA'´¦šô&uì&!¸‘üE-a6ŒüñèÏMÐÿË@.ÆPæzV:ÆAŸL…ú­ûXPÿaõ‡oP‡úR>½ õgÀ›o À[ ÈrV9gÀ³¼¼¥mO™Q×4Vë–tÛ±i€ÜôÚáÖÈuÚÇat›uÚÝft{¾è6Nü ÐM�A¨ßÖ;¡®¡+-‡¥õxúÖZ—ظyþÅ2Kê/:ŽIŽÐpë!;Í!.™þÉö 3£8÷HPÂ;k½näã#üHºpuŠüâíd¾LX„m+&Rô1‘N+êL=ör-}½C´%5_K¿ÃkéŸ|ÆE¿šæ:Ö®Qâߌñ‡‹ºM(Ÿò‘¹¦jßÖ.›®Åm|GïO[]îsKª˜K%mœØ4KÑ\ì4X„aáUœ°„•0°1}=¼¸¬²OE67!j7XjŽU^%:AÙPÕÕ-‚x½T¿-pÆÉ¶0^ãæÁ'Aß*jõßšnƒ!߆n`ïÃo?­n7ÞØºªÜnêð½zÖ7!NÈ çœí§�3ì5g±µý^s·uJZ Uï•­ïü‡&LX ÿKãƒEçè Ø€þïÓì"Ÿ¿¿†¸ Ô_²,ÀoVQ¢ï𮋗/á…žH«jnaÚl”ÒÁæˆÐÈ€¿Ž ~Ómœ®"ŒDÃíBx/ö­¢×cd5°I ÔǾ:7¡à…„ÕÑZ!?f觺œØ~Á÷BiÖ>20è ~Òפ 57|ò}eìœiÕ�žþéà£ßpNþ&­œ'¼ý:6½Î ãa… ¨Öت”23ÓòJ#]oÝ,´z�eÉc¨ý O禡«¸µ‹_ü�é(¢‚IøB—îøeKº�Ãå<ÌÐq·EAÌÎÈþÁ7í:ýØLç5¯¨ê¸¦e˜eîXK˜­ÛùùÛ3'„í*8�UÉrͨýëÈ,–kF,è_ç²ÌLÿº2ƒåšöàÖ4c ëeo"k>,2Ÿ˜(T›WÔ©=‚W´ã·§ÀîCtï·%Ø&Sà¶)¾šX¤«t… ¶‰n[Á¶XŸLäÒÓ¯o£û*Kë[F`Tn3zAš¸žR\7AïGuÒ g›Ql[r$r×®Ÿ(ŠQ| Ý‰b¹®OºÈ+š^;¶R9�/Àq§ª¦€,2‘ª©’ITÍ@ÖŒ.3º=ºŒÑ¯†.7Öý—ýðîÉè22=#¯ F•©¬¬{hþ@ÇuÙ7:ÅM°Z‡«4Š2{¯É4dÏB -Ýñ,>Bb�æ–`n¶à¶#K†íÆÍÈgü¶‚obƒß&�SJq(n&,…‰t[Ò[71¤„]ŽP í„DÄß¡Ó`r!:T YhU³¼�ÏX@˪?’à„ÿð´ó—#R=*Ã0gzÜG¦ÇçŸ6ɱW¿'&9”¾Xp«V—®81£n¥Ar°Òü¡�`UºS»ëÔ®úýÇÊG‘wJÿµOû¸ª«gમìcÌÛb¨‡“7šMÛ]óXÓ ©�.JA&´b·œÏU·ó¹cÍMsW¡Ï„ƒ›Yg“Òm “ÜaèÚÏ �°òEÂj¶•±­¤âT ò §•‡%¾â�\ùØJ*A#*wÁ@ºð'Wº?¹šÊŸ\õû“m Ýä:>¦Gê;vRG‰?�vçà"�>b �æ(ˆÐJ«ãpÏ 4ƒ�p]Öýªû§ B^ªwp¥»ƒ«©ÜÁU¿;Ø„,4¸'éù=àt§Ž!“jnÓÎŒ 36=6ŒQn† TêWÎÜJwæVc�¹•îšt¶.NÛÒ¡\´¶½Û�7Ö ½¼±U·7¶wRM‰ÈÇ«”ú�Í1C©–¨ ×¿Úe¥ka'™ ʤ«MïH­\©Û¦¬hMø5*2z2–¡ }¹_ŠŠ•Ü…7µš½©GäM}Jíi³ {íub–Aùƒ¹Ñ» +‰1#ö°;oêÕîN¥ßŒò§òìß] öî„»gU–ÔOÉ›ÚÙ3•u(>k/ªepÖN=©t±¹/´ÉÝ©½3jãBí?—;Ú�ÚÙ?{Ä#åE_Råny/†«˜ÍÇe®x¸âl€>¸G*q>@vçÔ¦W‚O°'wh§€Íú¡]w¯òÑšÀ¿Êk²s°zžÏu�Ur,ŽU?Lݹ3µ•Kc¼m{t^t¡”òµ´ooêŒE3MƒEK�&jíÇ"Š0ü¡‹t/+ß Ü¬¼¦Ñêf§N§x<ÍJÝq8YÇ�™òø¬@d7 Ýž¬3\Ìpq p1Nñ90\äBuð»òŽ*p1ÖóÊkèr9N}lÖ¡¹Cù`�º8Þ;äÐÎÜ®XÜï«ÅLú%o÷BÖå@ôòµöBéÄþVgg$_~³7ò ÞÈçÁgž$;èv�ܪ°4WûfŒ«OÔ ë^^jWõ‘R^-‰=ßlÆÐ„î¶Ä[9 ûÑ`íË“Lý¼åÿþÓØ¡.³;¿±9pwùa’ߎôE�~§e€6 `ôüÎ�P4þI™/ ¯ÈDVaàÝÅ!«PŸÆaŸ²…KYnÕØÄnC­C¼>�;YCq½Oݭσ‹öÆ‹5ú¸WèMn+EÝ_ñytÎ×UÿB%úJÕ—¬ê¯æcž®ñ^P¿¯‘D”ô’evð¾æ;aƒª{¦•¥Ðzy¤öÛ ìÒ‡r ë¯,©{.íí°{uõ1�ÊWWì’9>yÐó*Äûnø‹Ü‰‹-”Ù%uë+©çô†“4ÂëHÂ/ÜûKÎ`òØ/ˆÁ/ÐXÉ«[¹8¿øtQB6®lä//.‚ß'—ÁY§Ì @` �"í©€n©3-]¨$暎/Wä :€–”]³$»GB •²¨…vtÐ�Bâ¡4äëJƒÃQZ�1GItÈŸ¾¥×–CA(‘`ð0>nDªå&ÌtßÜVgØM\hÊ.ȵ·6�z·~Ôú;"W´ÌŠ"*ó,]Õq(böÞ¢£mpAþÝŸïæÝmD|�qf»!~ñ£>™ÀŰ˜|»S­dÀyÕ=èõxSëÓdö6¶î-ƒYeN‘¦2ÞJí¤Ò.PÕÕtÂWD¨·VÊíE „ ó¸É*ªBsGÊLQñlÐ=­Y^�Þ—¸ÆqAÃ8à¾F]ó%U KÔ ¡£|½À@-�¨¨;šª‘‚¾ÐEgÔK j)�‘vUH6)�c烈º¿`§íÑz ÿÓ%ý§‰õS\ üø“#þa)�üÉ£:áxSU‡›Î%Ÿ=¢ðDç45ø¯z3ƒ«ˆ»¿äqöyXŸÐÌÖÓ]¸ÆjE¢aᳬß�¬ÏÛ:ÛDNY»º*wÝ­Q—Žö8ÞÄ‚ÝâÔ�­ž4 öYƒxVKÝÀv#�~$¼.—0ñ!ŽLG @;ìªßò»’ OÂ%vwƒ/ƒ‹;jR ›”·á[�ùÌý§ºìÄÜÁœ3û). yhŸm*þR²ú.õt-Y7ðFRZ€Z�Cv]JŠ�~)OP× âm“ú?½I×ì5£êVT…Â6ϳ¢:Bx« ~@ð®y@ëwöw‚ÖUÔ°>mÊõ�váötlq?þΡNy::MH=ú ¤cªvql�´N=ŽeUÄùpã®� Ú-QöA�~„†óŒ9nà |S”׿láߪL=ËØ‚&ÿñ'›…7ûC÷ç}ú¹ ¿“qàÉ¢?rQÕË¿ᎭkèÐè-3&¸9cµtÅþ jŒâ›Ûã÷ÅÖ=mã!ÊÏ êiÿ&pÆšOúïÑËçxî]ò “NOlÏ�wö‚Jç <±æÖ}q@ÔæÆB³�|2ù;%ä¹î¯•Ñæë–šYPžãÒ…&éF’#èᆲ­Eá{ÈÆhZ½w!^þà†»Ü0òQc!q-MõXþà‚ºX„B6p-Ë[<9â5cx-žúñZõ»Ö#=ÚíZ×ÔáuuÈ¢ë€�ôºŽÂ͉ܮ})rökœŸAe•£•¥€gep+PÉ[Jà4þݺ&gePÒÀŽÖ½;5‚L§zÍ2#ÈŽäI8YËÝ3õ²~¨X>N½ü °\õæÖoëíÌ­k¨1|Ò¤·½ÕïÀ‘ëÖÞ^ܸ]òtâñØÃ‹Ë{×=÷^NÜÑܩߋ«ü›C"Z2Ê{+ç•Ī48o…ã–ÂÊ�Çv‡m½ÄfíþüµOƒÏTb×–ê§I%âe¤ù†Ù7c|â†.sÀ<ž]°õÇË8|'xy?Ûè›féÞ'[„Ôr‚¿À[qO[§LKÉáfïè1w£½CŸŸóžÐ>äyfëÉu-M“ζs›¶†¹29¦'ñKzã v0¬n@§˜Í>Çtb · \öà¸Pˆ.{p/Í4öà¹P„Á5{p-Ëáš?¹Â5ãpÍŸúáZs ‹‘ï5u¹†½Å‰qÈ„¹c@Q â Š½^d29(Ö˜Hvã˜!a†„±¼BÝ]ƒ›!Áò–7‘cWÔ4R“«¦ãuòzÏýΘ0cÂŒ )&hBñ¶þBQCã¾ò±ãMÞØ.œ‡#Zß�+Ñ¥ƒ¾ŽÅ¶÷q+²žÚ®¯û1û°[‚î© ›Æ¥ša³Ï ·s€vuŠE5;÷è<~ŸZÉúÙ†꿉ÓUôœ0�VÞ†¹+§Ä0påûè>�Ê28)£%¦O× µ‡VC+Òk”†I¶dÂFC5i "™ÏÛ,Á›¤é||Á'Fº#1þ•VeáÞ3Y9eåA{ýZõâ)0};˜òäb\ýÄÃKµá Ž,½À�g—‹à’—ÐÇ'ȳ2Æé–giThUÃ�Ó ÀJ-JSZC²Õá–y’¡…òŸ†ƒ¶FAdu¡W„OØ=ǧuh+û÷KñïtÝéRçˆ`?næóä«p»¼}<}%"Å4•ß«àúafY1#z¦J5’J iOâäý)NÎþ%aô+}U�ÓóYé>¥¥9…Cþà¦XˆB1p/MõQþ0Pú‡"D‡8/¼ˆÂö£×â©UÿXÝßÑþ±º¦ÿØÄp¨9ÑJ[t<àYËÈØB22š€‘ŒGÆ60’AŸ 4?`¬W�#ö!! …ÝHH¸ÚÕ�ƒBTpnL?Í0wT0·0ç¬qŽɽÁ\ÞÒú¦ñùÕ5íUë;ZÇà¬íõø(?TmO¬™§‚ivê]—v7£Û3B·'¢Ä©Þ˺Eoïe]C�j“âØôˆ³yýãŸÒÝùH§ïÿ�6/`í_5 ÿ*ËI×4 Z÷�²«ô¯nCÔpö]ˆ-•gßWÛåûfè6[›xoÌc wXº?”~3âc1Gn¡ §4�DšÁ7•xc¥üut ÝÞmßG�ÐBöª n3à¨~e|“ÆkPH1àj¯×ÐYªâ�æP-vöOˆž@gRÅyB jÌï§ZF«-Íj$0i’D4z#LBsyy{�H€Îv1M'Äñ Ófx%Ùø]ßk“ZJ‚vJ2=1ÅÐÉX.œœÓ4åˆå´+1\Cÿ Ê'FU%ø*.¢%¬zßR”0`‰Ô¢ôä}åhiÀÜ\€…Ëå¶—�ºÅHî´'ì-“ ,�‚r̯ edÆ"ÆHp�]~Ò4V±+¡Z#T‹Àz=msüñeô{Æ1Ô€¡A�4üÆø2ú£ôÜü‚;ó{mÆ—0j[èp; b³of°õ‡À Ì£ Ghª-»h8 Ø~¦Õ:…Æõ—>’ãÓÃUÁ)}p%x«‚“Kúà^š›–èƒcÀ–bsâ©]ÁŸs”ze\þü~­‡’œ ñøeó¡Ÿbê1|Ä&ˆ1à5uÄXQMCì€;Ç;àùÜ}ó;û”·çwbÑeœ\g]^¹þ¥R¿l4‘‡â$�‚a~¤Ð#d1wQÒêÆ¾X�ìîЧS'3"‘d\6«HP€£®�ŸþxQ[^øò©×2­hKµÁS¡ t`FÒI— =L%’êa ü�'cà5�1Ú˜Ì#G©°_Ø´¿§ø`Sn~_@ôØ D|<ÅŒÂ/~�ŠŒå6N£† šAŸ¦9”L^ÑOXÑŒ63ÚìKë›mº±B àmŽ à5ÔánÊŸ¶¥fönz/'™í{çyŒ¹_JyyÜi�­Œ×qf?'ÔjãøK{3ñ”¶ã=7òúÿëy5�Uâ d¢`â ¤E ¦É]ïªù(÷”G¹g.µË³â‡&2J÷"S©á ո؄ª/0`úP«æÜ"jKã»ã�ô‰¡˜ª_•³~y¬P¬úO+o穪æõyN‡|¥’¤;Z_éĸK<œ¢F͉5¹§fœÚ…rã+Q}•¢KTÕËX�s1Vªsk:�bGÅ;p Ú¶´agg<݃ýR¯�þFÝ*°iÌ�Ë‚@ÎÑo¦7SöF£U®ŸªñK:x"5G$±œ(Ùw�«sL�[zä@ëqíä“ïo%ø¾ ßNã««zuüÍÈ쨛2çò“e:O¬mÕ&O°NJÍ�F¿ãPãt(?›­ò±}@º�Üç6Q�,ÝrÿÌÓ%œ±Ü OÐK— ½AÅÚ9"ˆ>ºCWH©ĈOïý³˜sªr(Ýè¶çZk•døLŠî!¤«vìŠít"’./¢É‰8´ HïݪVC®wœ©yd¬ÇÑm)3ä"SB×?¨âàçÊ'‚ĹÞYÁ?*?)úÓ¿ŽøŸ”ŒпÎe™ªOÿº\Á%èí ì¯cIvwpe-IÉy {èe.š•�îx?*¯¨Ã“êË`Z®ÖAÙdÛt½N´!%“²w—®ýL·zqNƳ„†$×ëg ›�î€nÉÁÊ]Až�®èr]EŸÈKÉ+šXU·P��Ö¯ùäT_‡C¤Ç¨úΠ6ƒÚóµ1šß³5ÍMÌßÒßQÌ+¨ÁlKó®ÛÞ…»yºÎìÇ#=¢¿žNk'\™Ì‡ícEÔ˜‡¹±#C®q:K|†ô�ãårTËâ¼­›Å ¾ ;5¸zÒùNœ½îÏ—>SÜ'ÏÒÌO�¥±@¿À+èºKùt­²�ÉãF÷Å2"à]‡éÍÅüÑûþ«¦¯&ü½A–ÄPô4ˆ­G+�Â÷Ò‘'ŠSý××2(2ß³±Öe�”®Næ½P4ç�„‡ûñé*ó_aÓxðû¦•´|õ«LÝ9ÛcÁo3ºùϳbÅ`œçêצ‹¦‰¸Ž�LÜbòÀ»5e|Í®Äý[—çÿxþ ¶Ïª®(vsëÍ¢WÅx®f.Í zM5jvª,AU¢üÙ5ÚJÐR’!AâlÉÏ#ã5,� ¿éBmYÜÖØÂÛÃŽÅׇjïñ6äV_Ð+e¢r›pÞrûÉWj¤x0½¡¦—8]Æ«ú½3L˜"^÷æôZ;fži)ºfçŽâ3u�±Ýj<ö´¤eˆR…§P84C`M’ºV¯o¤a3ÍNd§£Øl®›Ì\'ÞÙ·x Qx8oÁuq­ Ûbóß¶*,x%Þˆ Ž€ÈhK_ ÊFôØ+“ð°­€um³mIŒ&�‰¨k3úIÿºžåˆý¤�Ëò“ ±ÇY¼ã�’HöÐK"µ@ þžã5xE]÷‰;“ÉVˆFåÂÜ‹ñtX[Æ!XÛÌMfn2s“™›ìÕÈí)Âòðü�>×ÍEÓ$#Mf6RŒ2G�òt`½? e†õÖgXŸaýaýC59Œ!"cÌ{""zŠxlŠX» }œ§jw­î$�ÅÝØSv ŸžzFu@„GJ|ÖmÞe)hÄ!¾†´Òà�%�„AçÄä‘´ïÁ±ˆ«±tH_D _�sDÍ”53µŸ©ýBíŸ6׳7Xùs½m¯Ô %þ͈(¥º†{ÓÛ©C‘,´Œ7‚‘]ÖÀîJcÜÇuâ/Ôþöd“ÐÆƒJŒ„)@ŽåYº*E2ØMøo¶ªeTÜÑ<È4LXÑáªÇœ§p-K@ ~qºP›ÀªôÑ‹EÚ×›^.Þ²ÛMY_#w%ÇxÙçÆ‚· Í™<Á´ÌrÞõŒ_æÃ{S›ÄÕ=µÖ½ð¸¨‡ýìG¡üûƒOTÿÆp¹-�t†IY¬Äö¸ð$%Z‡lC£~©Uá%t@ž¨Hiþn<=€�Á×¶NLÍ9ÜÒ€+ã>Nà� p¬ë¨É‰\_s%¦éã¤zŇw�ÀÁ‹ˆ!:i­é«û�O?áY‡q·¾ùæ›o‚?|ú{™ÿ”�emÊSkàí]_øÒîg^«÷ö¶²-L6¶RÍHõŠ{›•µí�b—‚ Kx´mZÄÜ‘2SìUPerOkÖ§·qIïãñã€R�_/éb*ÑLÕ·Š‚û h’‹Ýåp4iÝŠ5Y�Ž5Y;§¡™4òĽyË8”¯êXö§‰ÂL­>÷° 5_Š”?÷Z! Á*ZÇ)닼m¢e¯Q¤HúT…(›œü±{~âÃmr£ô—� ožkû͵#ɨïZóÊš£G|¬“æØÅ��49޳OÆÎ> ¤›†ºå‹NÍõ§�â³cd>¤4’�¬¥XûPе kWÎZØmÖîvÁG¼èȺ¦#ëa:ÒŠ¸XOq±vN®2a�cG�1– ¨`@Dz�7$è›Ü½“�gÜy€ÀZ¨¹k5÷C@•VxÀz²ð€µs"½òhÞ„ô¦÷82}r†�BfUjjЧˆ= Ðk9À×£àk‹l“¸¨mÚ�sÜ«å}¹Êí:çé8€?×ùºq�÷®–G:Ñ<éÖ=î7o¬›‡!#½ù"úìÊ>x׉5ûp÷[¯g¿õžï\yð�=ú ¤àn£:{éç®^^ÞîºÕ÷S{z}Z·tôþ6Íw1�ìWwôíÞÕÝ4%Dê¿h�)6 9–Ð«ÂøS>f q´3„�åñ"�YLØÈä­é«ï}â¹%Úsõ ]qN«m/nß<ë¦ÑÒ8·ÛM^ ïÁ@'bªÙBcaò;fçϬ©*ˆÀ µÄ5 èwýÈÍ‚VÇ•y{1õF9¨©¨ñâ´e‰Š˜Ýj÷%~�4ó­~ "]b½×Qu�y‘`D)XR…��k-Yê!ÒtEÄ{G©¢œ®G:E—ÑÙ§µÕÁi Ôf-¦eÓM¤µ¼4IïË X‹7)MŠëußĦŸŽñE{z?Óêœ@Þ\j¶ï“‹ànC‰ ýãFkî6”ÔÐ?®%©ù�þéc$ª¿—wq´·—×Óáëµb&-go³ÝŽÚÇ;–¨œ€ì‚èn\ÒI ºæm�L}(¯�Äz&¼q‘Pi�ö=Ä�ŒÐC�ŠxÍnË[zò(¹É ˜¶ à“Lä-¶O롉íŠh{›S��ÿ$ •P™q¬Ì¹€²›ÞRËšG‚k7m@ ß ¡IC>•š]×R]YCŸ—õi†€€E–S¸l×–ʌӣaEÂÀÏ€9æÔ€¹d°çlEØ=`æš ?�#›×3F•?œÚìà¾>�ÊLÁ044jÇý—M8jÇ»½…b°³€=°€õ×eì¬ê6å?÷všòòµ`u·ŠNÙÎ\¦ž ïÅcjÛ7O‡©YÜyøIi?­HßyãÅ“–8?v˜¬%Ââ’úÛUU²)¿õOk„mIÕ ö×M¯€2T±`�ËRÛûë‡ …X&hÿk’IAŸØAø‚õz• 5HBŒ‹‹¤DGˆ ôð/åÀªfG%È-ØÃ¡Q·ˆ{®ºÿxr0žjŸ;ÙÄS�b7Žˆ¨Î�=Cö Ù=йä°ëlD|ž��ëvË®8 »ø QÁ$vK«š§†lßF [HÞ£�8Ù;>;D“Ìø<ãóŒÏ3>ïR·ÇgåP$” ‡"Ù_Ç’Iü>¢Eñá˜X�[$FÅ;¸HTPÃèÄŽ?߯vf4¦õ½Ä9uÐ3àèÿoïZzÚ‚ð}…ÕJZì$ ^xI-U…*=PTµ‘Á�˜ÚÞ(Ê¡¿½3ûòîÚ�$ЊVœH‚½�ÙÙo¾™�Ým3ck¤‰¦.­2kͰ4íY´–ÅyÏ ´§5¥ˆØÏ<Í‚Ùj·4òŠ”e'õŘYôˆC”ê½$ý½¡€Q¾�¢gKŠVxüï¤(‹K‡‰_ÖÛÔb›dU’¶É3ËÎEÖ·rA„áŒã)‡àBîdG¼Îî$H#`èkxÌ:‡FÀ”8�R\�'ÍDÙ¾¬_¶íÛýýcbU…ó„0Òqdw Ýcü':K%V äÕç káä @6åIq…\r—˜­0È{ÇWÁ±{ŽKFeâ�kÆ¥¨åIK®‘`UQ„˜”Á=_¢D¯þem½‰õ5v\˜ðv—) þ³.»:Üij «¬…ÕÇ}‘R^‹T•��°žøå ö_é’´ªž[Êù9K ›¤áÃfÈŒ¦#Yâ~€·œ2ºdüJ® IátfY¸ûU“ž`QVÒÆ­ýUjšÙGL¶AÎ@1Ó:╟�aÀŒw‰[–&P\‘0GÛ,Ý�™`”‚ÛEq:�Å9 |à,ÖÑO%Û:B‘Ý)ÝkKàu0~ȹ•�&¿Ÿ\§Žoz%Tä”ÍÆ­KÏ÷Ý kÄ„ÄH$y˜6F••Å3u°£JŽi³`�Bî®™âÑž¼QT·܉�î‹ÀÝœ6è /®¤¥¯f8²9Å#XS-¨>A$çi¤¹‡¥Ùj+Õ“¥ñÓ1U.+â-p²WÚUeŒÜkÊoí‚p`ì"fÿÌ_Mm½Â#¼Ž™•ª³9£¼½< ·\¶ôÞœâaRWS ܱÀ¨ƒ˜6L4 ˆÃR—«dÀÆ´Ê#q\GˆWoZ²}Ý”}(Ì�ÊØ…­…E³øQv-k º §†ÆðÂ;ß·¼7�î�²¬ÑœÑõµ�ƒƒþYœé‚2 ‰ÛJYu9ŽÃÌ䫃ú9ÊíBöhç؈‹Ê?­I½ãŠ÷û”ϲ÷9¥E’Å ¶ÎVÐïnzÐKü]†6>}’A8¡i>ðæiGr&ƒÓqòL!FFvz½ù|¤ó*ˆ£ª÷ ü„«IZô.*Þ¤ãÊïo÷ßú€CaîËêTƒ�«Œ Hº±×Ã=¥AÞýÚíÄÞ%�~â_|ã€ü�†�ðž:º-ùîÙí¯Ô PþÎWh­Fc€#¸¾q…¡Šæ®œìàðãmõèqxZ‡| •‚üêwêÅgy¦ å9K* Ҏߤ¥ÐE)”XFΨW£öAQíä‰GÝ¿”£Y=}b ÞpÙ6BÞðäÔöxDõ Ùê4Í]Tó¿”ü/OÝ4„ˇ”?û”[{kr;×ÎäÖÞ˜<¦`6Tþ16úüù–�6É&Wp¹‡~s4"pšÙ½*ÎÑ�ŸB·i¿{êí{¡gt”~Ò¯µX z'1Ø-üõ!(”ûœ½Ûé¡­ìÂO´à{C-™²µ4ÏÒh–~FÏÑ€ ¥ÓÛ pͣж‚½ów¶°·�ú�žýäHW™­q™´ÏíMïÓ~wuuY±ènF¢�„…Nû‰øíqË4i�Ó<´'§îÑ2Ê\qõÒx—¹10ˆïhûÄA>½aB»ç[ÿÍó-í�oý·Î·´s¾õß8ßÒ¾ùÖwÛ|K»æ[ßMó­Ü3ßÙ2ߊ�6Û ©oÎa”hÌ»b}WcÌGpà¦G-îPQ¦RuÅ2:â{ ‡d’0ß +R«;Ìwõ%µN™4ePwÊ?R#´Ü´à›Z½Rë -á¸F¨ö~uHxqò‚•<—¥q-ü¢>í§©\¥uòøÏgí:áÞ2ü¬Ñ´OD:‹Z=ZO7‹<Äó%{©¤ZAçØùã3.­0c”ØxBn~|؇÷³B.+aLóaÿ`Z8¼¥<¡Ålyv0ƒ›K+h:艚ÏòpžK½pí�Ö”iE�¡S‹„~úùç¤üÜœ`u6jó:;@ת±Q›V%tf}ûáâ`C%ÙO%-h#¿=Ã"‘Ϋ(Ø])ÃÿÂáÞú OôØ U4“LUôTÉ‚ŒLUÃ!odð7 ™ªè¥æ™ªèO/M…º|„ƒ}¶J­Û«éÒekìRËΠ3ØŠñ´g‰v,Ùœ¤8ã'·2Hà©Ou Löª‹ ˆê.¼�Oí<Â@x§ÏõÃ}g'�Åüo†^ÀßE7¹çµK슋ÜíR·ââK™S�Þ»K¼Š‹4îR«ââGGpJ×4¼Ûrå­±ˆ˜öAbÍ»EãqsY5õGGpIŠ7Ž¢èl‚’ß_ö�þX�ü­o©Ú«úÐR ø‘¾$¾L”We`‡qa.æêµ)�}°�Vf¨([`ëÌv' 8s ˜&Ü•)\cªïYq…dß{én±®l±¼h?UäÈúr“ÞOë‚�§L>ÄôTêÓSK|™ò€QS³�J.ªqz?¥&²$è�'ëÇhyÛc1³Éf!NðÔ§yö9èêf|(—S†ðòÂFnqŸY£-KQ1Ü õ¾Ë’u©éJ÷X¨›£¯ÛmÐ¥lvcl){m‡ô]4z@g´ý`ðô3!ºI§·"ÐÉŽ ‚§V"²%¨àO�«ºý- "›‚ Ý:ÛZ:^ÝšÁëiXwù=úgB�H@—,øSçªnoèB"]²àãØ ‰p‰dÁ—–Ü"UêM\R´@SÝš4ÖÓ.ßéÃÉ•¼S¯9å=~º¿çtÕ{¼&äîÌ±Èø[ÉÍ �D“#¡XþG4’?4B"�„²àO�«º½‘‰Ê‚š!‰@BYð¥%$T¥^$´·æÕ­y�ðȶüh$œ {¶æß ?àá´x89$ž$#aÑMhBcû-;’GfÐùÙ¿-ÂÿûOýülÛÛhÇ‹ŠT1 „¾£á§+G†1u6JMè8Ú“óeE¿i‹ž¸éGGÅB•l3O²›Ï­•Ng(…ž-̎ЭWýtJ=zŽÒÊè´&i³.7�^K•^óßæ@"¥×ü·:�Hé5ÿí$RzÍwËI”^óÝö@’Z¯ÙúÀ'Úzm®Íà†õäzmžÍ`%ð Z­s3x€Nõ°Æìò”רq –ȤÝúB€Ð)Ð)Y‹„ì=øéÆ)w�QÁ�Ø<î ›Ãaø¬³kʮ쯰¨[3kú X³=ßúëKà‹Ù¦ÆU%ï`YâªSÞžÄ4®ææñ°žÑ¤yßÑÝ�cñð¥?:H&ÇA‹eÁ‹�Hà ,øSçªnoD"�ƒ²àƒeH"pP|i U©W±m`ª[sà`ͺÆÁÂx|D:’:üxgû²Â*¡ <û…–ÙJ¬PšB…È7Ÿ²,•,Z4½ömI3*E¦;áदUœ¨Óà˜#–Q¼x•å9&dµkPÜ:2¨€¿K߃©Ù-éóš2lXœÀЭ?Éxû~"÷‘÷?Â%ÛKÅÖÍ{©§v¨†€� žàºŠÉÀ•ê\Õíì@DÀN/pv*øÒJ`—¥`�ì¯òΰÏójþí^¡>דf© Äþ©ÒÀʶ½A»©÷lšé¶†Àh5ŒVŒö6a¨í× #±¶ ŽÁ@t4ÅýŒýý=0Vc�±š+ ŒÕ`¬$0V#€±’ÀX�ÆJcåŒðªcÀx{ÖBFº5 4*Ö]Ø8�™íâ“òõ�àOމ$ÿpÙó߆�§©øöZŠ“yñúyt.< Ì4æžË³GYÌ€Œ·gEÁ£nÏ$8Š‚?u®ê��·g E¡æ`,Ø0'oÍs5ëiwåçÙ’WÒN½%ÿápöÜX6ÝÎúÏu' ê h¢‚'<Aü©sU·?4A¼ �„©àK+e©s;7Ÿ+ü6·ƒoo�‡ŸÝzCaóˆãëÃ$N9LÀ‰A1×coû¥mÁáÂ< ½0ÐѬj1¨=òSÔAxÍ£*+ZÙ†eb�—>š‡ Ÿ~ù SÖ¤[ø¡L2ù!¦±€'Ej4,_ª6?ã!6‹Ôg¹Í� 0ü"S–émƒ×�ÅT@&JÝÓ•X( +L8 ³ðì¤>Éœb Åþ~ �ß*®—ÄõRý1�ì$œ*sÊ”ûç Ê)OPîŸ#(§ü@¹n œòåI¯Õ–gqiC”¼5HÕ¬k˜y¦ô[ä”M~”T Ø#ÜÀOn¸$�Á/ýtãd_Øp6hôl'f^Ö�‹fú9Ž•Ð4—%ÏY‡T4Ùei}^×ï?둊&¾,ù˜6‚FØEªäMM–Q]ìFž‚ÝÙÆ‘º5ò4¬käç þñx»›y”€Ý §€2œð'uï&Cw‹ÆB�âæ…=N"|°ÇdÂÍ"[=T_2³ ù9¸ ÓSŠ®Ò·›Ås_4µ7øºè8Ñ´yqØ ¸“?´A"v²àO�«º½‘‰ÐÉ‚R!‰€9Yð¥%�S¥Œ‹#;I—º5ÆÕ¬Œ›`Iì÷q4CÒ%ìPA®ˆ9„õ]+ë˜ ÅŽî Ä0ü@Ø&˜ÜúÃAIhHOD"BC*øSçªn4"BC*x!�RÁ—V¢¡,u£aY­ Õ­аaÝñýµÅç…euXãgA0az� ?S£¢z�À!±_húþ…VY¸-²C^jëKv`ˆ¶ú¶à_´ÇÃÐ쵎†YÝ)}ŸÈ;^/œ¹úFeNWÉ¿Ÿ§õšâ4Jà¬Q�bé séiÙú´vâ¶ÜÇæ·ð¦ÊæShf•ð2*¥j�ž¶SÀ+2[Tƒî쑌“ç±è�uÆÅÇZa\5™ø ùžµYò×–à"f«ñÀôN4´7ΓÆe¶±’HÖ‡FåFŒ [%\WXò�=~Ü%ÍÂü°JâFÁ^\Š»¢* %j  VvG&ÎØHžï¶—‚­—ì§ž\#" �( ~Z ‰„F”ê\Õí­‘HhDYèÖKUb/�Ê;3h¥šó€mk•�ÖÿÓ¡ê['d¿Ê1óqQ¾>d�ÊðÕÔjK¶jl‹~BÔ«U2¼ý¥´Ð’qØ× ¹]AAo‹&œîܹÏËð#ù~töÏ¥¾ø‹ß\Vc`Ô›ïä SGà+]ý hºÒÕ›6—õzC+Ðd¥«�»ÂÓ «'%ù²ÐçElÃ9Ý™ÎçÎM¤[EÞ@þ qa{H¦ß}—’ûK=0ܲCrõ-©–$Þƒ�iŸ²©EÉ9»jðû>~“çAëËR:Fò¼“kô®®×¼Ì*NÑâ¡bMΊب¯ûÊJË+ÎS¾°¦æ@€ò‹IáéÃÏ~Š ÄLüU�.ÚŸ~K©N-ÕÐ ècOqj�}Žgg�q’­ð³?Ð}/²»#çQ¼Áo%wôµGr¸°íæ–ÆØh^¤ÐzŸÃ j Ú*`¹ÊÖwxÅç/‚ÿêJê Ïϼ&øPå¿xœí]ï’Ü6rÿΧ`te—t™Õx×9çÎZm•,[¶*'�ë¬:'uçÛÂp03¼å�ÉÙ?—ʧeknÔøÇókV\ìÏ—xY„â¯kúë‡ðqx¾ÒX¯.Âûâf<¾¢¿œ/÷óeŽ\„O³}~¨xØTf›°Úñð ìõÖÖ‡Q­ãk%ù¾<ùÕ=bDR‘T‰9DİàÕ¡HK!Ìž³4Yºᑘ¥ï3¼‰«]XîXÎÜð2*^”¡^!2 ŒJUÛ !¯€iÿUT ·&¼�B<Bèí ï+‘„1Ieû=¢¸À­ ŒSèç�ÛÞ§Uù0 Ÿ„eÄVÈŸ6‡4BùË�Á?à‘BO¥U¸gдۚO "ÎŒ*Ëà?É£ÔšñìPÈ6Y<°gØ*ƒŠÜ½±^O¸ÉÐ>VA÷°pWð ϪÊËÏ—Kž>¼‰¯âœ¯cö0+¶Kük‰œ.uN÷.< h~(¹Ïá‡,Ý^|Ç¡…I™}ž/å­ l„Ò'áå3Õu~�£,Zoî]ÈV°}¨ãôš¨¡àO�«ºó1u隨¡pŒ:®ª8Ý6´›¸"Z(øÒ&ñ'b,i/e ^¶8Íl`“·æ¶šu l©ñx^z#òÜÇ,™ݤ¬ƒälÁ�’ oNY âlpKp3«\á @7“X•ö+€ ¬jNÊ6¹z2/²[ÅI\ÝÁã€D‡(âeù¾A§û�Œ†OÉÍB]DÓÃ(ÔB0JO("‚Q*øSçªn"‚Q*xA!�ŒRÁ—V¨,µ©¿¹ËqØ•qyùŠ—&mÅKY1–ú ¸lÙ–òÖ,\Ú¶%j ƒbËö{æobÆEºb‡ýÉw ¦ Ùäˆ\êöæ±u\j`n›�½¢K0J³A•ÅY ![†jK®÷ ^�tìxœ%¶}uöѯ?ùè7Ÿ*öžÀ;„Ë H\Jƒ– ¾hXJƒ– þÔ¹ª{—Ò ¥‚š–Ò ¥‚/­Bâò˜A{( Må­9дf]£iÂö«µ L‘ñו?²B-auÈò¯ÏŸOŽª² #ÅW¦oa;›Ð‡®¦0›uCl-¢a ÖÂâÀÿmb�ÃÁ@Œý? ö'?˜µ &GÔ-¿€JW?L§tõ¦Íe½ÞX 4Jéꃆ@!€”®ž”£²Ð�¢Ñ.>³PTÝšEÖ5Šbwz¢$p9)_XÁ×Sc¤°C¸�Þ—€„€çpÍ·ç¢m(¬³ý´ˆÕ˜wn¼ ,��‹/O†^0ÓE79Úà`p# ~ó‰àÈ‚?u®êöÆ$ # =ó?Ë ºS‡€æ®\³ŸŸˆDŒEšñ:§zÒoØ>NîŒIX8a`0ö ¬ýãO’8´�pðñëCV=Bs…J²v)ÞïD'ä¬Y'Bµ+M�s­`éèú¢à ŠNŽVhMÜyL%qÊEY!yÙ&•^Ÿ�·ÌÁª]V™0W‘„6ª\šXº$ð+Àæºá_xºfIòq“4{T¶:¬¥ç•Â*˜%à”D!܉–På¤2Ä ®Ú"ÔFš�˜~ãœj‘@¥¿ù¯Ù¡,0�·PVTªŸ%/kÔ8‰ÂïªÃpµn~å$~š°»*3ë jòW'å׸ò›¸ë¦ßN¾É[ÆŠµ¢ Ïð…¸éÅOÎZŸ¼øÆMò$‰O^ÄW;C½ìuPSÃïF­_¿è¨“IÌO¤è/æ9ØÇ¼P­‡w‡jG¸70$ þ.ªØ5ùm ÓZ ¿O^ýöÉá:…šf®}_ÇkNÞÒUg(0nÌ*ʲÂÑñ’ÇBõÁÏÑŽÓ0ºÙeà©@�l…îTOX£ùs)V7¨Áe8¢aÙ“¢bîêÅüñþ¯N~ýË$Û†÷Ï¡ý%ç�†”ƒ”Ë›êFp^y£ª¶ŽËCÁ]š0Ü‚¼©¨¦&°KÀ6ÑKM$Kîc�k¾a‡¤z zÂ1ßZ�ô]jïÿ ¬ª•Ø.³ìò�3§ž½.�XVŠ”pŽÁ¨� :/J²Jh«ËVð=‹S˜,0wXi“¤`ï1ŒP@ŠM‘퇨Xi1¿Ì*þym?7Ø_€}¹çk®4$ŠI>αn^Ð˰ÉÎïvqŽó)"ÿo®îçYT5 €6Uà6Ñ`‚Ë}`ì§ÒŠ€)—g‰úŠ­Ä%ñ9/÷Ðü RÐñ—˜_40vJ€­;ðn6@zì£ÀM‚¼¹°�„tÎ÷µMâÜ»8ýäÏÿ~òOÿq¾ä{Øè„ ïm—¥ d’–ÍÿžQ×ðµ¢ uÑÓûdä9©âyÃÁßdä©â1/È^ѺÏÇ*Úˆµæá°à5\~#¨·òɼŸßÛˆ¾:ß0¼û;…¥Ž´õ—aU»;Äý®Ç›8;ä¤E2ıúà¶|p[>¸-/·Å²ÔY½&¢CC”ñÍ&Žbà$ab—)2,¨O[ºþ�'§‹Ó„ªÇígrñóÉêî¤ã'Cò�XïÍ.Žvš3±kÇšå]µk]�ã +Wv #JÞC?ì ½¤cnO ù=NÄ�÷oª›tç#èó>:ýŒanF0¡Ÿ1ÄÍÐÍb2è5»¿AÐaó!½j�¿^ÄYø'VDeœÞÿHð \†Êc—�”~°‹Ùg³åY‹�«Õ´—¡šÓRäš³är:ÐÄv0í# ÄJ3£;QËõ`QtƒwxjS…†òq×ÉíNè뵺°ÎF­îÍÇAÓ…¥t#áò‚×â çg�àãš%Pk~�ʃ<@!zUÌaæëݼÚAŸÅûœ FzeF°ºh¸¡DÎ'ysB�T[>˜ðÀmwi�E¸mØPwÌ9Xƒ†°ãðŒ‚Þëm Íò ;Þ"«H�'üš'³øurŸÆØÖøàÙõyvüÚòêèÆ {ÛŠqíåíÆÓe¼k ¦bÓJΖ‰w»IäÁâ<ÚÓ-rx‡x•f7© r!ÃClOáŸ"rèy&"/Xfͺ‡ßé‡e�ޤšDÁBfàQ«•oK ÐðˆÍWª?OûÞ Ä�oÅpéÑ5�„å!¦6TÜbÕÑ>Nã={(êßÇE¿Áß[Ú׫�…+~@¬Ñ�Îl É·@ƒšˆ¸�­èñÛfÛp#ßò�f³ð{|3Ì_,6¦º z[Û Â{ÑùÔ£}Ǥú:Ú‡ƒc(¨[§ Ÿ ^^‘’Ƀ&d„Öˆ-Ÿ5"Ò ‚ÎR ª¢¿[8Ö¥DX"l{±ËW ÿÕØílÖ›ˆ®t¬i¤mÕÿø¿ä¢�§b±©éÓæ¸ض »piõ¿p;ñá,Ï3‰áÇjú›\-¡Õ�X«è†Å•1.jˆ5å”*D¦zb»å½ßQ•pœÁ3ŠóŒ…¤\€è¹‹Ùß`Ä�‚Ç*¹†öÎ0ß¿jz�|ÝôH›Ô‹«'îÞæöâêM›Ëzýñþ6'ÀW/ä¾Í òÅÕ“R‚>ºQcaþf&Äß´bpO�§×›3oКIÏhÉsbe°1¢s�ˆ=P7 ÝÝ=µ�ìÖ5«pÞ`¶xÞŸƒcÑ�„ƒºs,*>;ñÁBóéÉp#ðoã�~�}oäÛÜÛx£ÞF`Þ¦w¡dËöúÈ;3àOÍy²¬'_ãÉË© O)æ„YOÚb:Óž8²ž8W/ÞÙ,(¿{úêɾZÊ}Qe»öÚÁS¶º“¶g\8v'~ü¯EøãKÛSØÔñvwËÞ6ú8 Ÿ§á÷ b´0p“ª¿l�JÍäŽ1׃-ÌÈò?Rè ’¤®|ѱÁb4Á ÃMŠ®¡ç<Â.ª· ›Aƒ3ÝL©4#B6”ÖaDÞ‰ûh�Ëw€{ Ð.Ê#ñ@ �h™ûnaËh‹î6«…ÿN¬5ÛYð>kþvD Ü'çoç:Uglõ�œÜƒŒÝÒuô§›Áý¡èp<…;j‘å 9ÜÑèdäSr¸?¡ú¥@4ÈMÁYÜÑ?�'‹;:…á: :O¦òa™ôIæ)ãÀ~ZNø)�xJ î⛈CCä§⃈§â)�xJ žˆ§âx§â)�xJ iÁ”@<%ŸÚÇ)�ø8ê)�xJ ný:íO{ÆÓžñ?ÑžñÇL æ9vQϵÐ4b029� áiÄ Í…ð4b mNÀ]Csá”4b‚¯‹áiÄ Ï…Ð4b‚¥Gp1ƒÒ30�˜ —Áphc"$?ƒÓˆV…sÈÊ’iu�VL¦U8•ØÃÅЮšFLÐôO#fX.„§´-œ”FL4åð4b®€ áiÄ:œêèUqÐá”GÿAÛBx1Cs!8�˜�ã𹉶<öþÚ߇Ë0´îš '¤¸+…§¸-„§3´>�Ë™âáô;–!“ðþ [„Ï+´' ØÂÓˆt8n™G¶áÆ�òs8殉ϫãîÝöôD⦎:øìì¹ÃGq<0]øh¨]BÈûÔ�“ûpY®ëA2‡$¶:#e )ÂÌtiM–��u²i5MøÛMi=Ø—�ˆ~4<¢þN'²úyªþKÑ^G:1_„óX ç[<Ôµ*{³Bf]曑a+qé\ QüPd8Ðo TÓïw`jM1þ“¿fò×LþšM}IÍÓƒÚF¢|E9=HÇYÒZp±‚î§icjþ€úį�V¸È\—®ú[§Yî¡ýÓ4lÚçFÞëMµ�Åà=Øàë,[4i ãµ Ÿ*€»ŸÐ­ àCx#ñè‡×ߘÇõ´ò‚S)a '·Ü&ÞÙI˜'uÚeû «™=Π‰ÑMaÕûÄK®†å-9¬ˆìî© …ØåH=)c¼"a„£étŒétŒ…Ó1¼S0¢æŒ/½l˜Fk½d×rÈÒÄfc¼ï&W½ÚH÷%fêòt�ŽÖ˜ŽÖ˜ŽÖ˜ŽÖø¨Gk wLÎ#«“†ÀX$5æÿ²Î˜®meòYûãµûØU&]þOËy™aîÄàï-.»&‰×Õ†ÌÈ×+…&f‹2Þ»43êµ ƒµ ¦UÓ "«�$P›Öa4؉ŽÓ㟴GY%jë #)Öžs'‰=7¦}‚LWbÓZ_4+tÇ„«­»”Ö*4ôÒ³q‡8ÐdÑCX©sT×Ñ©Æìª}·5ÎaÙ†^¶3Puéó€˜õ�øŒÂC'X²ª˜ƒ>6»ÙÖi²Âë/ ‚6M>{_/=#®¥¥ÀŽ–§*¥jê³'ǵžîç=µõ|îÖóYB# =ä€�q¤˜ïìNwn)½ÿëkñ×ßj·üJo�¢Õ htÆ3Yu] Õ£PIÜÌÁ^½³—0o@/ç£$äà,‰Ä�d[ZK¤Öó¯³iPz.܈(k@,¶ØVEj¤2A´\ý{ëœÎЩÓV6÷ÑóÙ~A=«—ÂÔ¾ÚíT®‹¬ZõÎõhx²÷ÏõÙÞÁèøß×õôïµM܈POâìéÎÞŽz,ìù\QÇò@žC¶1»M^f¥Ž�GñÑ/2Y<î55œ6Td6Ù¸ƒ¯¾Õ—:�b$8�J}dIã,|d”;Øv“@û¶¶Õ�q–òèGÃTÁ:Óš€±Z¬/»7,À+Ñ´K·˜vé¦]ºi—.ä$.Mµˆ ’îÛà r˜k£¿EH�íÙ%<ž# ²µ§GßÔÛzcL… £ì_6 `ÂC¨p©I\è{”fÛc0éFïʵúÅ�ס®Ÿ§9¾Ä Y‘Ýk»ÊY3Té�·ãõ÷ªç´+ÒüÚ¢¿{8OOÄM}N�×Gæ{[ƒÝž?‰z@éì°ýMÂ=¼�Ê]®cZ˜“·™év š�»fQhT¤ö ò¸Ù…hÚ××–ÈsÒc×:þæ.#P {Þ�ü�ÐXežçìàqC¿œ¢óI÷^~ÂV$=ìÄÎ…ÖZ§ö´J<§õ«²ÞÑÄiý\ ¼Ä-Dò“nœxªçá“R‘'ê�O ZB%¯ë �"�K�u Íð¹Qê/`Hð»ˆúͽŒËõ +Yamm|›v<¼ó˜ðˆ+ÿ W»÷\ëÖ_þ+nÍ{÷fí½ÆùPöŒYÅ­û¬VyBBâ…¥.LsdîÑ#ÇЋj*˜‰¸ T:c¾æVa›˜=Oªôy-YÀÌ(ì=Þÿ€ÎÔDî–g1íiµš­çjC±„nMI{.Î¥éâJ‰tЙئNš$>}ÎäZœYk«uêôQ÷"¾u·Î·"ðH¡b…6od}¾E�Œé-bÄ‚³z¯œKõvqý{m{4;œþO µ�ÌBñ„Ë®«´MÕß$¦TT¯›Z`–©dùÿ¾{ròN€ÝhjÖw²^³0Ý9¹CWL­€f–ó�³åF¢ªÙZñVzŽËÅmJìmžéÂn  ·+eK‚çd­™Øý o�jÄþÄC6«xÏ/p$:{8QÿhŒ¤á«3[4²£?Þ¢Å�Ò,Åè š{…^­ËK÷âã¶38Žò@‘b�üö¼Áõê;Øÿ™ï,öBŸPyJÆëV‡î�?œ‹®'ØÀü^ Eƒ/4ÖâÇÀ8[¿s�ùž#ûkÐñ·oK•7îl­’E74¡PVœ-q/G].ôr ÊZ_ JçQ‹Ð˜m|8ñ_��%I‘g°XbͦC­“´82ª‡¤¥ñißyɈ·¡®Ê�ø˜ÚWî¥eï�!všrˆªõ¦Ââb �gsµ–[�Oš)ÝçWõ;mu»}ëTÚCiíÁavðìZ•Q›YÔék?¢–"W«¹^±–2¼x×áE°nûÆ.Hî8"gÑŒ³i<ÍÞ ð˜´ú«=6µªäæ°ˆ¶¦Ö^EÏû ÖS¦ØŒ+®Ë�<Åû–ts¤ìw€jÂk@› �« Á6J¢ö·¬Ͼü0Ñ G±�!‘ý{´ˆ;zÌZCªkï&2�K=^ô1ˆ.7âÑàhƒB„ŒE._¯žRËæ¶Vc�n±ßxSûƒÙ°äë<°E†ö€0è7õ2•$ bbý,j¦Q²KiÓ˜Œµ'±5ݦH×�ç8Y$!aÁ¯¢=J3®€öIãÖÈÑhû¸U ´Â¤¢è.v�>&�­N’Kܶ¡àg /nEvC@Ó0HÈo›jiKÉe·'V¦�Û|Á«OÑœ·n‚ÈC/‚Øù~ßüðíðþ‡Üñ`¬g¼âÌm ÄOá·?t.<=äÏÇ_þ0`ú×?œóÞºö!:ùÞ‡ƒ—¾6ƒ™Ÿâ��[ΆO÷Ö‡D…ã—> M¢3Þùpˆ&¿óá£�©Ê;¢E]N¡7>tÉ�÷Ƈ£‘ÌÖÈóÜøpL}’w>ü4]ù0tdâtåà òéʇéʇ~´Ó•ûئ+4m§+"ž®|8½×Ó•ðNW>LW>ÌãéʇéʇÒéʇñ}œ®|8Žzºòaºò¡õ딜4%'MÉIÿDÉItãCè…'ÞñÐ+hoÚ¯|`h.„_ù`¡Í ¸«¢`h.œråÁ×Åð+ž ¡W>,=‚¯|`Pz^ù@�Ë`8°1 ’ŸÁW>0¬ ç�•%Óê:­˜L«p*±‡‹¡]1ôÊ‚¦Gø• Ë…ð+ÚNºò�*hÊáW>p\¿òÁA‡S½*:œòè!h[¿ò�¡¹|åÇás>XØòØûk |.Ãкgh.œpå_qdKáW>¸-„_ùÀÐú.gЇÓìX†LÂû‹›Ï[„Ï+´' د|pÐáü¹eÙ†skÊÏáØ:§ÿÔáuN ‰°kÎt¼™× ‡˜=$¬î4Tãcé>(âƒ�sçn†K˳H‹ì®�‹g‰ùIJŸKxR¸ã)Mã3·ÍïüËoûA�T _º`8êYRòqJAs3 ›qÐpèÓá¨Æw–Tp(ópø‘�<ª3§8öh¹-Ufuž+6Ø…@ƒåßÍÕ–"ÿÚ 5™Íuø¼^Ú\y X®=¸2½çÊŧv¯¾ÄlϦ�¡Ó/Ó�—˜í§lG—‘‰»ÀòF§Ê¦l`ö�¥3§,´2€0ÞÞ—ãH—ædñÏ\кðá2TšCzƒÑîþ5+Љ½úÉiCîŒÿïdÛôMJh†!ñºO§nÊ÷*u91^ÈF‘±ŸÃʸ,Ó¦ôœ]PWc3”üCÌmGêæ›2DýÇŒ¾†Åωw”jrÇhÑš�<.oðN˜óæÕáx ¹»�y®9ˆ ËëÄ౬û�"]8gRo3§8½)NoŠÓ›âô¦8½)NoŠÓ›âô¦8½)NoŠÓ›âô¦8½)NoŠÓ›âô¦8½)NÏÇ8ÅéMqzSœ^gô§8½O.N�ƒ‚ãÜT)‹tÁzä:�Ðch.„GèYhsîª(š §Dè|] �Ðcx.„Fè,=‚#ô”ž�z¹ †ë’ ù¡Ç°*œCV–L«è´b2­Â©Ä¾-†vÅÐ=‚¦Gx„Ãr!o-«<&$±Ñ@£Ñènt7ÿöõ�¯Þý÷O߈u¹In£÷Pr��*¥ˆ×²0ª|qQ•ËË/.®àûR—‰ºýZ›²ÐóªÔY*~)dž«ÂÜ\ñ�ÑÍ•­dž-vXå³[SÊ`tl¾ÀðVt#E*7êÅE¼X^ÜÞ\I„¾¾…Oðû5|ÈEœHc^\¬?K¯ÄÙBµj¿ÙÊâ6¿¹Â‡x!næ =¿�è'¬Ÿ�ñ»÷­O/ùÓã›+ªùæ*ÿˆXí§¯>�6ØO¯Ž·þ]ÂÇ2°e\«$Ñùs@D`ŸUÈŸYD„·âgUVE*ʵ¯¾þVdK!E•j¨XËR‰…ÏZj+“ ¾]Y¶:ü$âúz뺰1—Ÿ_0 n5�JB@rÇ|*–YAØ=¤qµ©è춃Y¥1L$äVêDÎ%47¾¡ŽÈeü^®Ô!Þ*E?ÆYQ(“géB§«¦š?æUö\ÅòH”™H”´´0z•ꥎ%!ªà—¹,`ÄK˜\B.áÑKƒºÃ�1‚þŸ4aDA#dê!²‹D–ªËºIJÉ�NvÂä*†^À€Íw¢ƒ[ÈtÁõ´ › Û<Ü{l ~VÉÿ!©Ð?�»4‰j.*ï²HÓj½ÝžÛÃTŠ>"™úeŒ#ZÔå$ø+ÔCX)rtE¶n[k*¾ê£bôPf'[g›Òw­˜:—HÒêæOó¬X¨¢þh6‹Za»ò¼ÐYì@g( ø[‹;½(×/.þüôm%ó 1D¹öߺƷ^z£Þ}áOøÂ÷D±ŸêžòKWˆðª´Š*a_´°[Â~zðgµR.ü÷®½÷æÞ£f\ó·ˆ÷ >�f-“Q8áÝóà,Ò¹¬6—oe•¸™qÈÍy:\Å(|Þ#|‡ï•¬âõn Æøà›!}|µÖ— »I˜£¯õõA´ÏÆ¡ýÆ ó¿ár4±Úž£·ß܃Àá4’‹Õ}~ŽÎ~{éË‹1ˆ—çèìwr³% Vrs|*1VèxNuX>Œ$íw*U…Lôï Ѓyju„©FN[¿ ËU9سòì57®—£xyuðÍ�qþ‹L–—iVlÆM¢õ Ç!Ý�± á°õn5È!$þ>ݺ£Äw²2FËQóW§ÛÕ9zýZ扌G1srøÕ ¬ÙŠ£Ð»gÂyˆ÷,Ì 8dz2 = 3¿‘ï%ÝAÂù8Sx÷,8ÕŠmŒ5.=î,ȳ4†e¤¤_ Óøðr8r{mx[V ´Iß�C_ž™¨s½¯s¥ñýa�kt Æò9Έs`ü)ÓÆŒä_=‡Fð³Ü%J¯Öc�ðîYpêxäÂk×YDtæß‘}L2vá^šX‚i=qrxHGÎÜwPwU%r”5²åHAJÅ Ë)ñ×T/Ìu•êÁBë0”g1þ–¥âmÔ({{éð‹‚E %�îæ‹ý9p°°ÐÛÛˆO = –#TD¨ŽcúÖm²2zs Ò¡Aùgh.„B‹› �ÐUQ04 ÙåÉ4sÅÀÖúšá¹­¶K�@ÈûœAé»$Èe0X—ÉÏ@XÅ,ÂÏPØ­… §ÓŠÉ´ §û¶ÚÃjX§M�@ÈÝJ1, ÑQÄ"‡ aÐÖçC4åÀ:`…ã ¸ �8èpª£?ÅA‡S=#m �Ð š �Ð1ÏL~†Â–¶<öþÚ߇Ë0´ëš aÐd¢¸+…Á£µMà¶­-£ÛB4S<œÞ`Á2dÞ_tTl>¯Ð’$`[†^:èpþÜ2�lÃ9Ì4å'ÇH’nÆ–XBi /Œ²6!‚B)~Ü^]EUØðÁ›Ø�Ó"€Ð~ù¦û™ˆ§µéWÛ¨X¶j\¥í� üíÄV[‹içÙ¸Tm‘U9~ù u!|€nÅ«Ba˜e†´k‡ðdó¿ƒ-?&¨³Ë_¶�œ,bj F„zí‰ú„‘g€ ¤¢n°™Ÿõ!¬C�d)Ñ,ÝB�Ù¨ÎSº0,QMŠ1=<(`J–k! m×8ŽÉ4ý��LYJA\ä§à®`K.-v&|.uáE’ÙX-PC³»O+véÙèRk“ó¦˜ôÅS÷ŽGᛣ�I\5O4ŸÚCn;Bó’âædŒÁf–‹˜ú;Ë™î-w¹"GÆÝZÇkäCrdCS° ö>ú�|ÕÊaÿ´mºvBrd‡—’©Òx0Èl¯Ãi–¦n�Y§¥ZÁ ¡ O¿F‡é!*ƒ¾]PZ�ÒF[ÉÚå" TM�Øg¡€LòõÈ æ¢Xщ)nÔ¦^åe¹¾¸}ôtö p«Í-Íö^»¬ˆ4¾U[•x"$]ê…Â�Œ@ƒÄjŸ±>ÀOŸ<ý¼#~`zþççè«p¬�´kâ1*”E—U¡º½ÞE†qÚMı릆OÌ,M8q´ÚÆ}‹^×¢ÃðzMij»�%‹x]»¸­5òäØnW®{h-Q â\«.GÔCZÙ(åmŒîÖJsih³„—l'"Ùb2ª$=Í‚ë�‹ø— Ê’4!0ðiÎc31Þüp—fbo :5q["‘HPªÌÞ8áJÓ Ñj\_XJcþ@¸4è=1S\æ³ì åož¿ýëóÏuEB#j�°Š~×ýøÛa� 8’UÃ2›g9°çì`Zߤ0dBl Ò@Y¤¥ÁÎ�æÃ´/H¿± ømd¦ËKWºÒ=»¸WÁ¥aFóPX}h�cD�Ap¢: BA8Ì(�¡€¦Q=ó âaªCXË—°B€†Ùÿæ£(@uqÅr CDŸå·eN¤@6Ñ ,Â#„Dš1×ÌñMX%·Y¥ÀîüqYÌ[¸újÎ�_á�¥t䨼±n÷3ï‚K·Ã;½–°ÈNùHCù&sųð¤“ÚÛ5B¢f-Õ"fã N[/ãÊè+¸VÓb9C¡!Î äø’ÚkÇLd01”S¤øŽÈ7Lu,Èš3ƒ7%‹� x-�×âÉáØãxU,ãàp+yš+˜E~$’›iMXÎß=ø4"ô¤ßÁý!��¸36a½‰yR˜¸Zåur‹[pƒx¤ÖŽ“ÛL»á–ÊæÐK=3‚w³G‘6ýÊÊ)¸qöлˆHHŠÌÝ&EÉ»e�.pŸ�³£ª“w¨DZñfªœ� Þ"à÷ŒÁÇêQ³oÀ#'ÇŸÒTÍR§ˆ7Q«|¦3çcJˆxà÷Ó±xµ“ŒŸûd\ÊÐç�ƒŒ¿ûêsÁ+4ÿ uÅ–NYLÓ¢vüÉE5¯ÆLfH™A h2Ó'„‘Û¶XÌ2–Ã"`^ñ%€õ RÚ"9‘ô7 9UðN�†ºTf•Œ¤û‘áIx—ãÝÐ ˆšlµPBv���$ S[“MPýÍøª*»œ×Îý£’<:]ºÇW”íN·‘Þœ…G Û¤V{úAÔÚïÿ­Öþ¿¬ÖnÙÿ~½vsÍ÷UlïG¯…Ší{èµ;ôÎÄVÝU¯ù*×QlG¾fû^Ší¨ ÙìA±mlûÐlGÄCîA³y[àóÖwÐlG®j{wÍvô€¤ðߪíQÕöû«¶ÿ«¶ßÿ^ªí÷wTmOÿ­ÛþýtÛÓ+·7(·³ b�ì¤Þ΂кa‘Ž«»«F×t÷С‚�±2JÐlc€`Ì­äpOÆÙ0ó5g£¶ÒØfNÌEË™9ê/`Ÿ­†Ö;�zµ@òE!(tI›;Þ\ùò¹x„B¡ž ‹kû˜ƒTÆ aóY‘ ®­/n‘ä†9tÉ4 b�vS”Ù$Ë­_�Y `æóPZ°¡0ÀµFœ‘2.cÝÐMaâµzc&çgG/å mhˆß(>·m>HTÝŠrêVÆNW⡌6Ѝ¡'5Ð:’ª¹s=Ù¶FˆÌ¡Pé×á(q) ¤Š%# ;u˜b}Š få6¿Lg¤YÑ�9äùrîXÉJ¬v}‘E�ùtjŽX±éó s¥hsOæž6Ê»öր藖|‘�Éåb¡- ÊaÎN„;4ª4¾Ž‚³mˆöÏ`¿$ÓíæåÅúDâhe_²Â‹1»Ü`<å ¦“4éî ó‚—î, �Õ¹Ô_ÞôÏ„½qÓÚ.ÿ†LI9ÊÓÒ%ô�Gk˜„âE'�;�G;±æë+Š™HîN”›Þ’FÞöªþ<{Ç¡úŒ!)±Ïç«®ÕDíæÁtö³Èz0óWÿyÈþoMÿûð�Q›‹Û³Îsàñf­Kk0e/1QDÿ–TõV,–tÝ}q„€ jÞ�êÀ‘³\5Û�è ð£j‘`dJYŽ…þ¶D«÷ÌU q–ô +‹ª"F€ìžÓ¬B)eüáT\Kø80o.¢c£*§ÉlÌ(^‡c›¦µE·L(n€¦£CGL.ƒt²:©ÂHw’ 5N¤Í&aöwr{Œlð‚ "ä‰rÍd¶jtÊ<Ç´˜�¸Q¯øêÄMsÝ|ñ]ÖmÃÇ[Üj¶Ø…@Ô�äÐÿ$�³Y[(¥$3ºh¼Í#£YE^˜mü qCü.MâÀˆ/‰Â²Ãj_x-oŠe9Lßâ{�« ;BÖ Á¯�²ÔîÇ:ÖGwozuÌ÷óóâößkQÙ³¨",ñµ®>lž¬ÈÅ'EV g7@kIr Þý¬¾€·éæ‘Ø·©4“j¥5êòéœ@aXò7VK¥×BÃùRÒ9õ•Õ&MHëÊ:w‹Sh¸¯í³&9rȰð °1¹ŠRr¦ ÿìõwvrö§E�1å0K<ôp·Wì=IË;²êHëÑ)[.vV`9±¥:óò/¢í6š„D*Ì$’„´;×63`mo:&´ž•ÛÕPZðÍ…}³¯eöB›©´-«‡÷­ èb`y¼K´Sï‚ÚØ?àxpÐËÒ{V­æƒÂGÁE‰–ªÞs!¡÷6ùÞs«þõ#J�ßÒÚæp®ó§€c[ÀÏṴ̂;@ª,FËa*L)ö°ž'}nÚ±žEÔÉL]DWZ“<-–Õle”'•:ãFq6_%™¯ÆiT*6�Ò¢f`IºÉÎÙUM:C?Lø‚Ø @ó«ù<­Ëó‡¨>«³1LI]ó=Ö“f¬c¼Ì—Õ’ì 㤔žˆÑ\ÎÞ)ÓIÙ2--xåyóTqÝ’Ž²¼BåU¿%ÿAt—OH­cÝõ(è§ŒÛRï*¹¢Ý½zNú>àÐ0Wì!=Ë)Ä..= Œ1Š|Õp÷`µÎŠ=œ {ŠÄA�œ¡ùå‘�Å8¾¢GÈÊÀñW¤ó‚I`þQ Cfst#‘óZæCà `IÀ$ƒbY;ÒÊ1“ó¶)œ8™c�ÉÀY|N(‘Ep9ºú²~«¿¥Ü§�E£¢Íp]OÍŠGjËÀø<é‡&æìOœ�iïÓÛdXÃ�B#7£™f°ù.ò¶Ü?5¡‹0 ê„m`�V@m#jÌÊ?{¥t¦ç1yxÚmðe:„tÉ¡ZÈ¿|÷üAÜ1ÔTqzü„`®sˆ¨1ÆåßcÀ’¨JuBYÚ79(ttÖ#öÞ¦ÝË¿O·™â«¿}G'G‚éˆÀ$ ;¢�¤¯p'ñ{ºÞÁ¾5À1˜ëOÒîVþíË¿�¾Ãì#o&·w˜ýÂÙPÚà'èŽÎ‘3W]º‘š˜^ô�æ³íüüÔ�y¤Ï¯Qdc/sû�NQìt¦a"bá�PÉÅ�O`Žûº>íZ·ÀXþ´sv¶À_¼Ç4I@AÕôԱʚ‡ í”·°å‘Y®ÿøÉ!GL´ ‚»[Û쾆“݇íƒÄ$ 1“lªå ‚HËMVlµ¢ ˜wsKÎJ%lç rë)amÉ…F}Ÿ�$£j„�A0Œ’é�m‡ ¥¦bÔ·…ø�V‘ºdY—Š*àžú´Z¥öÆeɦ�g?8o†B>ÅÐûÓ>·;û§'@Ás;)<5'üXyrOûm™ºËfHCžÏfÍÜõ³uH:òºÐU;gÛ›—ßeš™ÜYÿ$‰´,˜À�ÑLbùd–UÀÌõÐvÓ�ÉÈ`Dýå€bÖ¥½3äTÁÞMÄÂH°MÛt P³¯7iPª˜Áth”Xzʱ0ŒÇI#&Õ[!›jJOòð…2!ÀÅžŸ=qÔ°¤§ÒŒk¸lô;‡¿¶Œ,!É&Ó{]Ûg;p»@Ù�A î%Öª­+™ÅBG›p>)È—ò˜+j£”ˆ´ÖM#6-Ï0ÈØø**ò7C4©/x¼î%~k VÇ:òä.—¨Ýï¾³(ÕM„ô[iát h=ÓàEsˆp„“5§ùS±°×Z¬—B ò"=ÇI’ãšÝä­›LªMW)Á©…Q”ûÈQŽ Æß*˜Ê�ªí G8r¨ÏáÝ®|â\\Àß#Ù,˜Æ[·Þ7�ûË«—]ËE¾zîÊ¡bhdäP;‘j±±Ñ{ÑlL½…W/ãžÉŽîëPCAá8]M#-Á�Ⱥ˜[D>­Édbqh’•N¼]˸<’OÄñ”MK:å5Ûûyqû⫎Í]8ˆ­qW8`B0Ó»¬Ô`C÷Sõÿ´.î¼ �9ò‹ç‚Ç¢@­�’pÂãì6…3ý†‚½04ÜΕBr»é†^h|߀¼à>¿�Ÿ” „£&À��¡¾«pœáãu ÿ"YÜu\a£U.dü(¸k’\�m °éð‚ÌAm ~ü)™A™W:\ Do0¸¿ !!œç�·dzæI„¤ mÙ54}6kžú—™≄Œ÷ÌPxï*�Ñg‘"¦uÇòZØõÆ±ð•³øÎ¦&aôAœž#79œ¿º™;*±WµÏ°†üªw�" Sg_¨QZ7:Oòœ’d¼E $³€I¥¤ø´ò›@YÃ)y4+ˆ V®ÿO§OÃùŸ×l™DAû…¨µU;�RVûm{�ì#Îcœ(¹@ÁVº�"9àd•m¢6A�(sä¤ÕåŽÅ›Šÿe]Ì•Ò')ìrR‹诅�¬ˆV+5‡%©1â)`ü¤NeNk°¥hì�áÅ„À¦]Ýž;ü +lͰæ_I5IÐ�”>…Ïì¶› $$)+H~Á[d²%s6¾SHÆôæ[-”w™&q׬E¹ŒÉ‚I¥ŽÛލó?N sÆÒJðì0<×[=ÒÔ¨=¶î|ÄÖÐì•Q±½WmÝã‘í¾Âþ1BϦzÀwÁ¢XLo§É’˜ ö¤å<$–¬¹)“‚úÛ®�r‰ôñRcdÞ, Ž)ñ„:×)t„êf@ Ö¿@Jªå›9‡Ï�‚%G'@£whÑ ÄïÝ „aH_—�qÇÝ}�œ ^Xû)†µVXŸ~ŠO¿�[;_λCâéÛ³ÖoOöTÍ»¡ƒóÖPº2ªè›Zè¬×ÓÇ­=�—£m§ò¤}8Po÷jýö¯ä å¸Š³ã똨®iC¯Ÿ´öúßäìµe¯m™d­¯1ÙäShœVS3n<5ûœâgŸj%„”ÑECrÍ�èÙrè~/®;}ceV:š„†Dèæ&Yawè3Ë̉×_‹¹ô§³'ýø“'?ûiVÔ±ds8òíŒùd2ÊŽÙ¡œÝ¦2?¦Pª–)rî¬ÍT!¥ À]\Éöé…ƒ õ®Ú"ÀLâôÞÒ@Ü!`Êbž½O\¥ñu†Q}ÈÚ\Xñã ¬Zq”;³/8—ÓÝ«šRa`@šR%3!e0È'“i-Áœ‡G|õ~:éŸÿ|H^=u�s4ôcnt‹éïøO8ºÓøìgï qúúüÇ$+5^TÛŠ•<ù'£¶ÛÞ?àpGyïÍoxòÇÓ�Oú±<:ÇGŸþñôü ~²¦Xjài3�Oy²›§ÍôÅË/]'ÓÉ.•�×zØ‹¡Éã¸÷¶@æ,omxh{¤L3“ÅÒfDZç×UrU¹>z‚Ô")O¬ W4;¨»d©rÚK´‘ ³ÞØÙÅ�ñ°qVÛ]Çõ`O4ÕfŠ1ô$rm(J%ÆYŠ^Ëbé¼ ª–sÌÇÕr-–±tmÉ"Œ…H~^ì¬z.Ч…?š0/®ñÍ*½Ý¨’}–8>ŽØ—Ts­(~¯Ý aCûÆ€ {éÂ.‰N+yi8Í›“üv+0̳d!–¾„ÝhsÇþ…½F +ÁÙÂhã´ÌÐtÀ{WÌ$Ö·de‘•±ÑXn=T)™*ÅjS¸qû><åU&5y…8~@�Òº«Õ”µ*b¤;½.Û1^³Û°…¨Èt¶Ñ² =:>�Úµ-¹Vä,<P‡ž7:œ*Cl’ Bísë Ä �ˆ,ñØñò<šs?“Qd<›þ+G tß6¸5"ê»”¦©ÁPQ,ܵ¨;y°÷¬aq½¯»Lö¶Q’í„ xìÔÚpà˜r¼k–öNVüR“‰&Fù4d«L“wôÒëK¬\v<÷ yÙŽ›‰]ú ΫÄaeÖoÒÅBñóë$›q®!Úí§š i6Ë(Ƴo뺘¹Ñg鸕¨:rœ“~1M‡ï^`Ì*l‚;mxŒÞÔâLtÃÒ‘{ròÊéÁjJŽoV…K²)¿œ5µååj™*Ú/ÞçZaä¨�ëÞŹØ#@m~ÉV@ûÅW©Ø„úŽéD3ÉÚ hp.Ðh^PÚìÓñå@4pἡTt¤Ø¡ì Tms˜€XƒÑ(f�aóoÉð¿`n°üÂ]ÛÎ4™þŽ/PMc¾bÅïÆ/‹ܹx8ýkó7YNÅuàænpmþV˜¢Ë…_\¥€–ªú—+T˜0Ó§£B}ÏÁ%ýÓ-Ǥ éL©¡‘bôÑÖ2ÌO>Žá¼§j»4¼ûØE¾›`°¿‘å×üÈ<8ÖéîIfýáî�ç uþó(8Ù]ñSÆ›6M©²ÿ‚°p„ &Å:=ï†-×ùnmìë+ë¢�ÊèØí8Nå³}·v½<ü³Å´’�ÉÉêê¬\U½6�˜A|”Œ9þÌŽbzfÄ9=a+À»Õ"âh ‚v)ÅQ;îb`Þ¾ÇñSq.ÌÄ0,–ÀÁšQ˜uÀdÏ:!äAsv³Æ07ÍÆˆÍàUbÒó؉fR’) :¤g×Ä÷ä�ø-บrD>Ò ÏµXÇ÷ í·?¦Ðt÷MÔ¶¿8¡V4@ÓŠøh-»m[ù¦/Aø‰­½†Û;/€ß+röƒAûåË”±CNk¨¿�m#gŽÁur“o|ccM¿�Éæ„™Ž;&óÉY±Þ 5§pŠP®…}5bÔÒ¨h£Œ8F€íŒFceç0{˜°‡æj;¹\¸�/Ϙ<}†³¢Â\)š¨ÞËEŠÓã#ÆKdoµæn‰¤�|ÚËËëƒñh$y°&ÆØAs¯I¬ð¼Õ­ÏW`¬¢Æ&EíX:x:ÿ,wyy´‹Ö~ƒ$îNBòypµ„~Ÿ µÞ ܇áp†“îJ8UŸïøÝ& Ê¬ÿÁ3RÃÙ¿ÒUüsÞ¬ŸÞóÜåÁxO‹aM·–B$/mS¢ʆª…&®CÄ´¤õçòWªjg5¿�}"Â1MÖìŽtňDßÌÉ•Åqïìäìü�8,ΖÍ›(jœÖõ¢zúøqRÞf×ÇE9yœ ªÇgç'çǧ'ŸüùÓƒËM-ÙÏ×»È*óŒÀ¡ð°(ÊÆÿY–.*âýcrBjÉl&™A�ª‡É‘­‡’œ‘×¹Iˤߖc;^³Øc†FIG䛉 é(8ø†6¼ÑeÝÕ%U?8¢,ȲlÑUÕÕë“©/]ìæÞ]½qt䎽�»z¯fã¢ÑëlÂn-2ðûh ö!¶ÇBAûá,wí.8’°¿j×þšçÒPeïÚeópÂ.Ç»véŸ�¯T X¥–4 †gjɺ=óÔòñÞs~¯ã�²~w ù»äýÞ0—-üŠÛ7rß&L]ôSª!̺_“ ÜÉc»NÓ˜ÖöÉ¿›îs*[éÝž ¥UÕâTuן*>¥æg& ÁMØí®1+ÉßF3£A²%ûvÇÒΣø¼o²”›Üâ‰d0%ØaU(!Y$È}é$Už­0þ‰´N%‹ø}Zd§N¯Ó™•ïQ”+­»Ñ�ΖØŒø×jÎE<”åì9‡b1¶�⌠�c( *íÔá­û4JSëƒÎqµÀTÎK4ñ Ï ¡Ò¾ÄÀÇó¬²Z€‚Š«äJÓðŒI¹àŠð¼Ë\Âÿ ›’)IS†;É­yH$z‰Gá&�Rt—–5ztËâfd²¿à\Þq~h£¯´Ûȶñrª‹ð=•äñÆ}À츤‰7/~4z··” “G僸j�m%\~‚`ìæi—aÆhdF%ßMV!“ùzÍ%ÎÊÂú6!˜HGˆz²¿ÖÊ�ñ‘y!9$)ÀØÞÉ\)v›%@·�šž¤+€¾V—`ߘ`Y›QÊ�—h�´yînèÜÌ÷bÁ&L4 ·�Z“KK} @¨Í¶Ï�”µmYzo¬ “³×ß)>�¿9Žÿr¬-ã%üÄÔN$:ôNÿüéŸ0½Æ‹&g\ùDàµñ}£î‹Û`ºZ ŸR!—ƒMò ôûy XWð °8¸y¶ Gà=ûóYïOOÎÎûñ'ÿéè“''»›%¾6CþòV†$Cƒ�EXõ®i•á+=ÌvƒÅd¶ZLƒÈ/ól'©Û~´K¬×î½í­0íF‘w≼þŒ6û@weܣϽjWäUy�¿Vé&ˆOÚFÔÕétÕ0Új°Ô= C‡F´å®´„ëÑ`u$Q¨Ú‹æí–ÅæÅÑú䯕OXÓŠÙw³‘Jž_µ(Þ¥Ž„=”Ɉv�èÉ»d&D3°n'ˆ¤‘’XÄÉ0ªÖ3š pÜt"¼ÖD¢ó%�ÂH¬Ö4=Äj˜ÈAJ¤%ÓUš-à­lžÖ•x;}¨b²_áîvÕðtâܼvNîX{Z•a�ø‹1lnðí À�9 ßø®:ŒŒ?.óÓx$ˇwU }#á6€ººð5°C¬I4õ|(+S§T-ŽžúSo,îÀ&r6€ú¨¼Rž'œ”µ)�ô�`äŽÃW!GzÇî€6A=ú·íøõ‹çþ �«‡©î9å�¥²¯Ý—µƒXžûÇô\Í�œ+ƒ.Aä©AõÏxÏá\L-`IAsB޽˜5Gåø:>=ùOƒÞ8(-¢Ë�5ä�F zš“ ù:«2¬¼Ñ>_ é.œ \ñ‘:ùk|$ûºPél8äêg]¡ä1QãÑ,YKƒ‡åBDa‘Z©éê¥zéšê öpéœmÞ‚cÏJÄëHùvÊuÐЫó,�‰½ l˜Õ˜|ÙBØ#[‘¤cߺfý�LפßÚCäÑõ$Mb&I_õáJ‡ÊY%0ΩM4X¤Y_—×ZÈVÉ=É$-çvîÔïÖ¤¢ëQ,†REßÉd†‡Õ$•ó�ÉŽ#9Ågwg/}¶ú,ӛй;çÇ&v3±Îf錠84ÿw·ï;ç=r¹Iût'~Òý¬ƒ\y¿îSWcãx?ýü!FY¿*õÄ›ù¥’ªÕÜÿ�½ßßü­ØˆÜÄ �ìá:ý^#ª¯bžø›°2övJp ÈM~?F[�e`9d}"Î(�›Õ-†XœlV4'F‚+vµKütÑÂ$o%…ØùÜí‚ÆCšgåN4ògªNÕrÀa_bŢ̅œ`²U’÷%E̘² ÓØÊ‚+c y|‚ú’‘É•0Œžme‰IÇ'¿¹V‹p5‘TgKb·mc8¶èhyoD®qv µŽôEf'h¶„^ü*/*E¨¬º÷´ÿ«9-± :VNÒyÉ^Fîç+óy~î)Ì`Ÿ_KÂ25P‰ûHùÒ0·PÄML¤¬‡5;“StÌÖ‚ï™Lé³b‚ƒö�tn}/ez rhsÇ¢fuEEò(1Ê8j•Ù·¹ëh‰x¦gÀ’#ƒ[Ñ–WívãUKoQµ®ñ zO� ŠÚdåvP£úmb)Ä¡§e\«­Õ¼C® $çd±Há,P‰Dv¢»IY`&Aˆk6–F½éf½ÛF¬öȦË~CJ_Éÿ繩[Õ)CIÕ½¤{Ênyìo9xŒï!UÅñðeZÄ<#³ë#l!D!dIR˘S?Â.®†ÃÈâ)UÀD¡Sÿe”R� ]ejoNåqåsÐ\5BذlZ¢/FþÑ—‚/” ¯på&Ǿ8Çœí¼ÚÇ”ËiYXö>õ—%HÀ`P«ºvÑ–ÓpØg"_d®Þ¶å]:@jZ¨ #Ô±Ž³þ$굌ÖKò¨"�‰¡Ú“�Iº“3Í|É*ر³®‡-�/êÕÓP•b°«…AÏEÜ´NÚð5"øÜ__¸“r�¡F~#w 0èè D�í£¨buΑ� ump?WÒÒèâ¦ÌêTô)B}ØzÉdÏvÜ8Ÿ5Ü«{ZÁݬ|ªU¹’³£Z�9¨7+¡å¨ÉŒKµC¤câpÚšÃäð– =½!U?wjžÀ^²n"äšm5”^¢l¬a™-’Æ`¡úPS¤ª«!ù¦¥)åV„Û>�æõWâÞ’’ý>�Ý–h¾}¼j§‰æ÷;Y^�M›ÞG~6 âHOåÞ2³ùÚ‘÷�`À³��ÀÚ6Ã5«R.�¬‰=ö&Ê¥Îs0pCF#D‘ØBv×{/s+”ð ü$Y.Ê=à—0xˆBûi‚'S,²Ãß~ôáú.õ/ÑpX–iû¼K€Z({t&­Qx˜�­¶ÃÌ`U‡ùf;�ÀÝåÚ; ô!ÔÞÄ6jî(ío4øyY•ÚçÔU?Ý<é²))r+6®•¥ÍxFHâeÍñ�&Ò™…–[Ðy©'E9Ì…æ;Ä`çÔQþÍ�n´³_޽õØ+ßÑ(Éh³´¡fM™MÂò]†Éö>4å™t|.1i¡[›Ò|¨y{Ðt�˜6ÑÀ†÷�yhèp‚R½ùIš–ù1 5Ø}SøChL/÷sböµV–Ÿî"âö|îfŒ\¸)dÐ0ar™´ Ní)™ÄN63FB|yuX²1» 2.L1ǼXN¦š$�Ü7 ÈDk†èw6qÝ8nIý²ñÕ„ Ó(FÂSˆÜZ<èC£…ÜIfƒ syJÄ­,eCKÕÔx©\$}‹ýP€&b§æw6Ç4žEÎ]©ßŠÆœžÄ§'?àêÙÓ_ì,Ó›�ïDvæç¸ôR\²`×�Zð‹œ,e>4BŽä‚¡ùœ4p”Œ~M†¤Ýö‹H…õ`›ë ]§`ŸaÎ 5}4 §¹ª°_ä@3/àñÒC-ÜAm&4ÉEY½Ìq?ôrY¡M/_dé4ìY*b§O­Q¤9ð?NÿÙË¥x—û}ïiÿ”Ÿ·óÇÓC«Omé÷쟽_;ú=ë꿱ýjn+A3˜­nSjì>Z¯,Óó">rܲV}Šž}ä>ŽÚ‚W4A¼  pu=‚° €A€é-`‰Ùʦ׵�à€Ç†»jFe0KlŠ»``ý^‰^âÑ) )ãkòù&E^ߍЅBRüX‚D2^Î\œ�¡Ä”äÑÂàÁ;Ô‘ü‰§d-["U€*�ÅrŽÂ©Æ=i ešôeUb‡ëª8ÂÎqbv€™6È€K^ê�x¨Ýj§6�­±³ºßXžo0àKêàœ[4ŠÀ‘”÷+”‹�LDàZ="¥âð(+S’È-X-Ë1 ˜$çl)\àþJvVäÿgEñn BU4O«i7S?Îꀭ‡'»2öôÉâž;ûÚƒ½eï6Ö¢¶‘Ö{r#vµ»;ÚúT–3co¯%…g- †L7ÉzVo¥Hç¡×—ötFv«�bûžåR“ÅG·ÄCy(¶ÂÊ¿¨2³�æŽËY^äGªx2¨:;ge¹Ýœ/¬m œ\}‹譻Р»[îKÔVSö%ì:Ü$ÙY‡G–m–yïU®½C­K�:aâîK�üµw,=\³å.ÝËùU‹Z–·a¯Ö¤x2ÑÈD¦Ä«ÐûÈ”5kYsíK)r åÇ } xÊW6™:PËÈ:¦‚˜ƒuâ8—“]¬{*Óë¬À æ°Am‰¥BýŸ®¾SÿçfÄWK¸ï¹ê©LÝW½S7j¤é‹éïPË÷gÎ÷QKº“T)ÖYêUQ§Oã¡ØD¼s6Ê}IÈ�[ͰlN~â$„ÙK\?( ²hx9Ïȼ3!µ«I=à�å¶ÚGÊœ¤lB§;)bŽ™Õ¿Z*w:£ÿòC‰îXZì6�ËÝÈÝ*Åy’׉©dxïù.œHð¡!¹N9Þ;�ð-zÕ¢¤‡¾ã@ b~ÏÕìÆ9ìe‘—±”LbÎæ)Èš8þ|ŽÂ›R÷ÚÅ@ï§Dg$“ZÀTqŠŒ3¹æŲ¨‡ûd¹ ‘ ¨°®“ˆ›3û¹S;u�”sM»Ð¢„\' î!éê)&îÄ{YÛä3náP±° H‘Êf9æaá°þd€XÅ·xî­9DK�M"»$Æøå¯S5r¦œœÚeœZ}¬0õGV9œ¥*–ý*ÉŸ`•ä�æ˜ô"ι|áVô-í.½ìôŽŠÁYZoÓ?‡ñí:@r�–°»[ô/-wœ?v6sÆšþ‡F(ÕJvæ&)·"Ë1Ô…™ùQë­Õ©Ûx�à&lÅ�pnÉÃÄñíu¢×èÒaÒ1%Ÿ§{žœ—£0ANÅ)P1Gé–8¥3Ièé^Sr šµqdÊXÐCs4Ž(¸íXô„®’ÍSs�öÁHr�JÂjâÕ‡$%¦~d O“ÀLÌnþßãø[r'ñeFL¿-=eˆk fQ¦Hˆ± éÞ'¬£^Sgû›ÁŒÛË݈)̉cÐS˜g{$~¹Tµ©Ë½åNÜuü‡Túµ º9/Š~ååÜÚ{šFnk•ƒ–«‘Ð˰ÆbX+†±I °¡^>r6D“k�ÖžÓ>ˆY(°éÚŒ ‘Sø€gxÚÇÚÖ+`ÑŽ(‹"Š;ݩȶI²M¾�h‹„!!ýý+…_%eª™NLJ�{äU4!ýÓ-,Ö%H�([áØé¸(SCçžnÈ8`cû�Ô„a‰œÏ~]Þ­öIªHJŸ…Ù)»<ùÃb£e¹’•ƒ�{�ÑõØ&‰"{*j&ÅF"ÅfŠDèæM=jï©­£HS2jà¢= üì8É¿ Uï†ÌŠ } #ƒ à /#zKÎyÜLÙ;é£^0�s§ÉÑO¥®,(8Ò"š>[v©`‘d÷6¹Hlµ†§MŠްºƒT†ˆŒ ¶1ÝÖ§G4¿Fcåuœèø5·aoi�d @cÞITº�ˆ·‹æh§†ñz ®¿O1ÑõIšÁU-šä Ã�îS…ˆÎ؇‚R`Àž>§ÿØ·88:÷þ^ù �ÖN–æ¦ÊÖX¬A@ú廨Pë º&—‹a²™ ÄS÷ñ]¸¿@²¼WWGŸ&Œa]î-T«Ùÿï¯Õ=�¦Ð½Žq +HU)ª¤f»Åd¹“é 7÷�«³/Úz®…a¸–ÉÁV.gÈØ$Îrú^��e"##¤š(.xE®ÓÌ`W»ýùÊÄRs/ø‚êp%b˜è_,�a,�Õ—m« ý›àOÕc=Ïjß–=k€½ÊfÎà :j„ý$•Ï sZ2®S SÀÃ:¢õÊÄ©ô#ž�H¼.Æ€‚Ó¼N‘'³B6™v®±ØG/̤^ºµ†Ó¢Pn%äû�}¯Jƒíò{Ý·Š†¿ca$0®úÖ�Lä=ej0+×¼[nܱæîüû–„)5ãØBšåöö}…E=Úã Ï·ïÄ[ýJPÊÀ©Ä+r|w¤^†M1{™ÀÒÕ$;¥\q«#rֆʤŒ{„«¿’Âõ´"[&Ãõ˜ìÜø‹ë°`! ºlê3À«Lüå6w#°b2ƒ§\†Jï§n—ýÖ­ lòþHÙeiñž~Û6AÕne‚£&³þˆ¶_ÎÒÊF¿™F,"´õϪ©³#ÔTõí„K•¶ ^Ø>œŠ‚LŒ…MXu¯`*ê4²±T<”Êß’ýÄSEv¿º¢©”nZDS½v4Ë;Vµ/¢5ºŠ†ïˆ­Ú0©�Ü;b›:†h†9u4<Øn“Ò{±ÙÝÅÎŒîbmv‘¸+wºmß}s[“ù=}~r•e~Þ,‰¨Ù㬀ÝLKªB’į_1´‡vžÃõLå«qü¥:½xù¥± ¹Úÿ`bLëh^){S7&j=²�FIëP:æD®ÁJg]3.k·aäDÀv·“7O2ÂÞ~{�ްŒðˆÊ&„yy‘Ó­–@>Õ0«ªÄÓu6FðÄGñÉñ“ÃÇîôÉn&cxµ¶,iü¥[‚’ì@ñËâ‹O¥ÀױГ“E%Xï1b«‰/ç+ëljs:ç"ÇhÔaê±èœe—ÖÀ¸¸5�ÒÄU$äl…'gÏÞn©�‡3–‹·÷>#ñN;ä2Ï”=ȃÿûé¬ÿñÏQ (åÝ=ðùƽ[ ñ¤­Àáù/RH¼É±>Ü{Éæƒm’ÅË„— ¥º­˜Ï†£±2ÜÓëת³�w×^C²%º†øWcÇÇŸx�߀7¤¢åS‰¦"SERZ¥O6'-Bà}54¿ýÆä€ÿ]C6BÂ!�v"æƒJWw§ÍΘøƒônCJïî“wŸáäÁ`gŸãïfâÛª_‡7ut�èPà[#ö¨j¿Óñ¢×yC“+Qj4ݪ;ðk;Aÿ6o؈žbzp -›Z Ü!ÅÁ€·Êâ/u�´^“!çF?cK…®�<%K©ÒÇ* Kù´Úè›`/*³tÝ @Pv£eL²]�v� ²¦q»§º‹éš4EaTcøP”40rÕ9¶©°½¾¢¸˜“ßù¡¥³bÒÒ¥ß&½e3R¶Š‚fu Ü +Ötw“Ââgë,“Õ,…í_7êlð~ÝY°¡UkC¯föÚ�ˆ¶˜{hŸëÕ!|\Ï5O ùkfÜÄÌafø>«­Ä”nT8C¬eëjZ³J©k$õ.ùΘPV[F¾-‚;˜˜EÑR¨ÖÓž)ÏdF]9<±‡å’Â�#®(~„�’qÙUM“+#?^Øâ˜wQ™5è©=Ø5ëkz2ªæFžN܇°·øî¿’Å,É�¾I³´Ôò«X > <¡Ù*G/… W›C çÓ~—ñŠŠ†ž„Dš½ˆZ7kŠ ƒ¦Ät›rpuA—U2yÓ}X ÀÌwN�ô¨¡ÛebC ¢`6}�3IþÕ¶oÞ¤31k[ã *ÞeJŒ<[¢¦LL‰�ë(W}/ও/sTþE›V|“T\Èá0�kBÂPQR°Á²BuRC›éS”¯Ñ»Ô•jfŽ,ê�ê4®S©+¥™C�±¥íÇ$wp(—wôÙ[tÁeš&×+é›Þ—h¨å|ÙÉ{,ýpB1VÜô*ûÍ”}&ñ·‹‚ûÅ…;W¢�…«3CÜ€)öýŒ /—\ð¤úu_Qa›(šÍÎxëÚ7…‘u­7»®Ë ŸÂW—)‰t]B�ÎLj<:±-V,ýæ1é•§XÒïë)x÷‘î''Ý{Ü»+ ÷µä=*ƒ1»… £üÛßDhœ‚_õßWdžÈrT±‰Õx;�`Ô¢Ôŵj«}] ŒZô�ñm» ð·–.ÇØçW½@ 5Õ€‡¾N¦S(5·X/f( ûwânÄe(ã·« 1¤ü:›tX`T–*ý\F+àS,t“qñ?·E¤î=†ËjÕîCs)U]Fe·©*½‹è)£vEå¾õ”Œ~G=%Ü_ùÒ¤ÀÇ¿Øç¬†d#ÐbÊåoÓf®Ó{zLL…¶�Ûë·«25eÈ}™5k©¥¦‹¤Köð)Åóºî<ÝE›é}¶_º[ï{‹ØM·Ö˜áf'ýM5N|TFT ä�Iæ—/ÚÆçÊ™Ü6c‚nÃÖ&cCm˜.û™ —8̶J¹^é-9ݳ§‰—Ãí+Â0X‘kɆÜliÐ{Á~DH*P+±vFó%ñ¤­…\˜r_¬Ul”éæ5"òEþÁ4L}¸Ež 9ùupÑKÒ°4ÏïÿNœéQD3`öKô3¢�Ñí‰Ü¢›”ÕJ4P^íÔ�WŸ‡™<ƒBðAJ­ûÐrËú¦œ�ú®'Zбˆ8&�¹ ›âÈOÚ§zšü6ÎW?[E¢ËRXý4z½6ÿ¨¬`âì$×µÀÿJË<�u­pË¾ìΆ‡ÛP]ø]1�ú�@3gÉÊoă¼£ùšÌª:3¹¿;�á…“ìŽ )1°¸Ž­RÆœ#i³úÞM­“w&%À‚ô èx–‘!o€ˆOHÊ�fóúÍÖ_$Aë »lQ¶¶ w‘Ú5FîÜ rÛÛ §·¹ø&FÛXrRÒí±K!» ú�Ù’ÐíÑZ¨­”1|§ÕI»ÉP×ì-7x³&€è-C…-Û\EÒ |„+ìûªÏ%v¸+¯üŠ©~ª¿Í± Š" ¡�ª“|*ñJCeFLD�RÖ­n¬¤¯gÖœ¼¯šú0�š_*�ì©çôa'…òÚ›F]Q×°ÆÁ2›Õ67«RAVÃÙÖ’\E®¿]‰ZeSyŠÒþ�ªèë­&‰“ršõѮDZ~8üý¯Ù܇>Óÿ„˜¿¿Ì|ð¢Ê™XÃYt²ÀÉRTSŽ¢ê»´yH?Ê9ýsüd+\;ϾÍEÖlýccUÚ=g+¯�ôr3-f%€�cS‡ÛÙ÷YzÓ¸ïB°Î>9éÇ6ébûl;M%1¥Ù "^´MR2Í�kcC7o9Þ�Ù V|Ý`À̳È(ƒKddpà›.blA�ÄHóÈ{.�ã#BïÜÍ8•£¦c"ôäfôßtZMn¬�|zGö“ÄnV…µV¹í­Nµ¦J¬£¬Ð¸ÁJ=gó”jåžJJ¹†ƒ'Î_Dg uEÈ¢ ª36m ¿)åu+|"º ã•õ¼6¤Ôt50�®«»!¢�º†tê§´Ø#C/­gu@îeÚ†ÚóüÕmŸ-Â�a»€e_ƒAJú©|d¢8¨ÈÅØ«¹‰/õ®Xxæ{½½ÃšÏÐ-¦RiÈvm=’pÿ¸4Œd3/3Ã#ØP»—¤)AH‰ñì£ÎsÆx�ôXŽ[>Bµ‰Ø#γ¼—�·sç”{üI¤Ù†Xznd?÷>�ÙËÿªvK³7;&^Ã,ПgViu>Êæ|+PÌk®ûM½rjáÀöÎL­IÃ3“>‘›ÃÁ>j°[�¸R¸SœF#©ªù5óKž¶H+¯âÌ d´ÉJÍÜm5 0™9�ƒ»åã¤p�•püâx†9ÑâWZs›[æÍØ+ŠÅ þs[Á9þî«ÏMòã.nª‰~`cýݶ„^þ€Y†ìʰ©«ôtúŒÐÝ‹eŒ¹¤f@í¥?­ÎY}Üøë–Š„3б3¨õûËl$ËŸZ¹RC $õ)ŸCVZÅswYsŠ…I¹ú•¹xV�Z~µf\go|W spiþÜJMIø¸]W)YÖ”oRkñ°Õ× ¼·ØÖö1~—øÖõSi(`ß„ZÍ›Bì[•-'à)Iwgã‘rîªwå¾¶W¾²ûC–[¡Æû¶—úŸÃÓÃ& «(X ó64—Ûê[%¼0—?)•à{^ÊÝîÛm%=›u¡9y<ž-šÜËr«671õÈLÕc­JS1l7¢ºËwœÝ]*:•ÅÎŒ#͵‰< ºs°»}¶.ž—阤w0úÝùLè¶³ÎmÃUýÈ_ üík%9'Øèè®Ò¸Stk䇷†ÑË(¼ù6¡®QëêŠ*&Vn«¶:ƒ`£m¢`·§ÿëášÍm¹kÈfÔ¬�·>dSÈÀ×:tX /r#6aÁsDÿ|Þ·æÓ—Y·ò8U+²œþ±`^cɆ|_oŹ�—s´u4Æ€‡Ä!W.ÿlErÞäöBùo“·e÷¹í¥ºöö£Ú:@3BµµÙÁ6S]ï›ç™Ù=31}gÉàƒ1ÐÙlZ,S×e�ÍÃ�˜fç«®Jv˜Nñ>nlUæõ7õüØö7�ÍQ°;M$@[ߤXNÒûž=/'­sC#Ö|±¬Y|µ½;Éì$Q¥‰€A³…$BCý ЉN–ɾÔÙ/‰@A}áê9w/**"¢‹#·"eÅò—ƒÆÔ–ºnk’“Ui¯dÖt :8‡ãs¹©Ü�€s“Ù[ºôù;A3àtg{˜î¨ä_íÄó?’¤‚óÁi>d£lÖ±Ô<ΊZ�G çJÃ0Mæ~ÛtO‹òGP‰ƒØSNzWóiËþ’§†+ëΨwB«G˜ùß ‹�â$‹â7\©øÈù_¤mæÉm/ÉúƒLÝÄ"{ÔŽ¤wÂ1åûè´o*ð9çIuç‡E‰zÙÈœÞP8S8BüLŠõÙÏ`ûVM|\þ�÷¢qóÕ&1§g¬ ‚·ÎM×S©"s.Îõ;†ÍÁ#åà&�”¾Ob;ØÍXqdRyUÎ8-ŠŒ¯Ù·ñ_ŽãïŠ%¬(]ÞôáNjO*n¿â¢s0Ÿç™Áÿ¯0Ë`¬Z*; Û ¢K62îÑZMã9¬dUep×Oÿüé'ǰÙÙÓÓ“ãÓ“Ó?=>9ÿä“£“�Ï>é}úÉáŸONÏž}rOm®­IqàÕ§™Ø¦²†‰À.+-8¸tmþöUäf•þÝÜ^JN\ÊÍ/®¸˜Ã/Wð9(´<Í‚‹Ó,Ö—¦©nàÎ ™½· ±y¸Cì|µýñÖ�kÌÇû?–rÝ{9Œ‹4Îf³t¤’é~GÀ9¯sÞ×Nî9E¸ |¶ny ã%&1òŠíY$ßJ‰mº¿“úº¡D–x}ß�¸R9þ n�¼?nÔÕdßËÙÈd¡MMñŒÅBRÿb„ ½î›ÂøÓd‘ý½Ä~¢È¤½;™'¤.Ó ^nkد1À:‰ÕÌþ‰S7KdÂ%íܨ`%’§çj‰ýÀ;ÆŸî�ž*Ñ={õw>kJXõÉý«ó�ó÷¿dî tb?ú�ýƤ;jZ^ly;HÉ*a–ÇrUK4g09™JâˆQ[¬n. ‰­Å3�…=›R‚¬ˆÝ{©¡)¢×ù¢Ìà4Vü25%p>þσË@Oùìq=õÚ`“àÙ'æ;G�É�$çpÙ¨¸CŸ1ØÈ=2—ܯ»#£úµxðó¯%ˆÝ �®žñ�}$ª%«V¬§e¼A}shtÅXÄ„6Zß ™ÖUs9w¼89“ðeYqG{+ŇiX7£”ëqD*ºñ]–ÁoÐ�IT÷¨;ày@g#�ÍÑi¿º0kéÐ^63{Z•…Qà»p›ç�È ç1&ä@bfÒñeúp“NƳ4¹feûœ•+ⶦʒsæ?ÞñÌߊa¬/è_T5üÏ[dn9+YÜ ÇIitÈ=Héа’©€d_Ó�b¾|£Þ§|å Oú#æk�P§•Qå˜ä»“daåìÌÏO1ØéLî¶úæÁl*†%^§E‚­_Rú[AÓäh2 °šÐbVsï±Yd³ý~;¬¡W㜠oôË‹¸Z¢š­NBkaÇé†ñFËVb‡U:»F©eŽh Rì œ4›îØx±õJi5��9Ýè¡‹wñ0ªžå­;B m nVI*³4ƒÔ”õƒÞ·44XÇ—N1kPÜŠ„¡mLôÞêfÓ.Á\g  Gzá»H.úŽ 󸆡ÖsÀÄk=öûÅüÝq�Ó‡¬Ô±hú’Õ¼ÖÝ]â07±×÷Ôg3§—‡ÿÖÊo§R^¸ù¡‰Ñ�¯Ð¶0—ø0ñbÆëvÕH%?�œ,òäÛ‰�à½ãž(ÎŒC ­¡ÀDk�ÆÊXã©_…&ÿŽËú�.zÔz×Pº&ŒdtÌ~£Ç!ÐEº§I§ø5ÛÑ©œàá1�ïéÓµc³—‘úJ’0ÆZ¨k«%�¼`Ñdí»•dÖôXö5 6ö1PßïÚ™ƒ±»h¯¾®0"_ô) iS pDšŸ½�x½¤²S²)º£ÉcÕ Üé1dKTÙ!ŒÏ©Ò˜¦¡Ì„³¦À@¨J0^¹ÑDW\NÜöúää�5u9j2]0Í!ez³ÎJMåºï)¹W‰HTVÙMÌÌŒ LÎPÐÛ�Œ©v¬ –α;¢);Já ͼ5½!‡�/TõºKûø�¡ZW³àD¤éÇTí±x¢«Œ“e]Pâ:Üt7ÉG£ž­²XéÇŒC@9HÙFs;w`Ýø¡¦w.ÉᕊV_KhœH2¤JX;€åIšsÍ dØó"«Vp{€+¡]`Ë7÷'‹.Õ<ã@ t®œsT|§ •ˆ07#�†aÎÄ@X?,ÍÕ®‹‚>�çdä˜áxœ&a:?€?ƒ{¨ÆÃØ=ѫ燜Ã+Ãçéj¬ÛÏÕó>]MÕa˜íˆ:ï8¾*üCÉ*7mš yÀG$¹ßœ¥ü.Ó� ü¯J¶²y0¥WWè”Ô£Äù\¤MÒ§Eœ�·Þ0…¡™ó¦�áLF£YjÒè�ºŠá]í¹Ãž2öt÷›KF&peÝ™Rò5Š{]4C5i §«` öu@²É`AFµ!¬&sDߊS”±ëUκ vLÅ&·¼4ãmÙ œ ³‘l ­(˜Ì”Ãö¸dü[J.9t8ʉñKpà ô˜ã¬–¦„\}ÊÇXbTAÏ�Ôl,VãáÒŽÔY�QìýžŒÒø<ž’BÂ/xz?�ôÏÆŸHÛ{“a¢©Jn±^–æ*9}zæ='?K}ùþc’•Z²ëB ñÅJžü‡˜K'°Øû©z§'XÒìOþxúñI?–gçôìÓ?žž?ù§KÄËLçÄÌxzÌ=j;Ô'Ðm2iV6æX¹‹ ÄN3"8îLí2‹É€f„“ m{©Å|ÒRSê¾,>�¸Ëy?Ì`?5FsÓnyNFë&ð; ë$\.î ðp3¹wòç85mSäÀ\ÝËaîw›®6r»š”ÙhNwÿãÆNàÙÙ)¼Ãyï5…OùEƒ™±�Þœ ¸™âšé"€w˜út‚™P�TÏkÜùÜ¥>µÞ&c;C4RŸºÙv›uÔˆ‡8%ÉC\Ù¼ ¡È ž°`x ÜŸ+Ï×Nùiú™ãÞòB¾µ1ŒÉ5H1$H¶-{DÎBdÊÍìÀát‘×0)]"Š@.I=È9MÈé�D7@BZsªš�wµÅl=øq&LÏG’¾•J¹EÖãåCó(X£¸¡�mÛ~@Ý ÀÀ&(#?åw:ÀBå¹O$p6²!.,ÛJÜ*ÈùäyFš[Ø»¿~û\#>ï_}õ5ü8EW°¼Z÷Šk˜òÓ«eëL¤t³þÆ™](]±´œS…ÿºS¡40‰zk*Cª �Е�ŸÌ‚r^mz�Y|L‚½»’) ˆ.ýs¤V«M•³!AWw^ü²ÈTÓèºfRÉ!€Ù8¢?,«4LAfNxi•4L¹°bN üÖ32-gUQep¢äær**ëV ù®¶IÉÞ=L»¸Y©”� [ò ÑZ=²±´‡y-²¿Yæs‚/~6“ìÖUæ%;ë]“UÁ?¹´Èº¸¾ù낊NfnÞlüÔz—Ðl T¨ºV�‹®%Õ²ö† ™.+I˜ ™61Îó8ö U×6,äó‰µzrÕjûáÞH[+{éÖ¥+£Ž´úÀ; ÄJîÔæÞDƒhúÐhŽÓ}¥ ,®\6�wÍ}økñoUoï+¶¥Ê]à„%É|” LÔuïS}IùBAuÔ[z,YB5cc„ÞÎÉ(ê kC�Ø"”ÒXÝ¡´„ì‹Ìó Iƒç]µøl¡ö‘»(UFM¸·š *ù�Þ2“¨�Â[Æ¡vo©¹8O~ß¶ 9Gõ~½cŸô×=~ô;ã4 ;äÔmî9½Yï,ê�}§žŒzòíz{MÓëÚ‘Æ] ´¦K…|HW‰¾;•Ò�}ï÷+6?jØVl¼õ$ý¼p¡÷Á5N±ö½ÒêteóØÌ^·ÖwrŸ¤÷ÁPìpNŸ/û}!Œ.aýÿ¸ø-ôš$�cîÂÔ{8S1G÷�cÎü¶r�2L_Û5Ž�ÝúüË+k”a~Š×OO-ö"çÞGÇP\*.ÐÙÝÁõPn»æ¦¸ý§C�›»m‹’>22 U,L�"ëŦÀø‚¥«fWi¯£åíúcpšïK4ÃPtÀã±Ñ\ Rï¥ßµ.ûIºÿöê÷'•;™¥—œ‚àÙ_ãÙ„c7IŸ Œá€Èšµã*ê@rRR†2wéØ{{¹#d{[ÃuôÆQ“É ©ò§�ÏFx|€D¦87@MÁe|x¥VÖÔAhp¹ìÏ“ãó¨?ÖÃaYÈwžDû6"äHÙ#ûÙ)š‘Œ�‹ Ô{Œê{òÖ(Žàñô0éYâØÖ±�–žªzjç¨�éšD*RÔá*Ö êî3ÒÀN“¦ú"87?ðû]AÓz› Õ¼™¦ž'{ùÏgÐäÈFÔPø% @¢îxþ£¡UEv7û±óØ� ïLY¬*ÖãWE¶/ÿë͇××ÿ÷ñ�lã+u•]¶/àO%�qßÀƒšåŠ;÷r±ù–­ ËM!”/o¹½º¾¼À?ì%»\õVWìŒ~v²¬xòôò‚H\^ÔG‘ÿ{JV«Ã·_¹üs_ýì²O6B)Y¿8f‘ÈÖZñrvÕ+ö¦ w³;WòF°B䦪�ƒ 1š­�e@š[Éu.XŽ•wϲ0·�·-Ï•;ÿvÈ~Gš�,Þ¯†«6^¸¸Æpë\“o˜ßpŸ±1TA2ô‰u_ó‚ >cÓiUã<[ ùKæv˜µ•ù’q]d,¨àˆJžb-µô¢ä|»ž•®Ÿ×NYãBå�qð°[»©ka3æQËe£¸íö„­yî�îYN?·dºZ†ã¢z ¶•~2!K¡o¹jÆ[n¦ -ÙªñLj`Iqd…D‹ ”xœ�VïkÛ0ýî¿âæ}Ù ‰Y¡0†cXÛ1ÊÚ´¬a0Ʋ}޵ÙR�ätùïw’í$þ‘¦b?ݽ{ºw²¾ºº»\~¿ÿ¹)‹È Û?d)ý•h$9SÍܯL6}ï„n Œ®ƒ»0¨/½0hRb™n-Á»Hf¸6<ÑÀ…æ… +qî<Ö›RQ’…³)gѤè3ÂÖ�L빟ŸC¦é7¥[C‰L±SáG¸a**XŒ…{=¥Ì°ø sãN‘8‚7.*ãZauäÛ0pÂ`mEDËœSñJ$†K6UƒÉêrÀDên©êµ)+à‚ô‰•Ì(þ2%K`ps}ñðí¬,°U]¶6’2-IIy0Ø‹ êml-$I,¶3Xæ¨I–E@ŒPiL�i"\W†È%4mÛ”F1.šM‚Tû…µÂ”'¦]j÷<¯îEÊ7­%¥žžûÖyÂÆ>*npà¬C_líγ&ýˆi“aÛ#𴱚mð?Œ%Ã+­<ãÔöx #g00±¬´±f1J¶ç jRм�&Æ–v 2ƒTV1É0Û5¾Ä1ÉÒ’ºRúÕb/wËŠ’ñïFÝœ{ºg9¸¡êÊ¥-K=M˜³ˆfõ+®È mïe{°,ßìø¦‰²;¸÷¤0¦Çy‡’ˆ˜Îä¤×†É¡HH1ゆ¤=é½!ñv=µQR¬¢¤)´¤î äQ_ä ³¹ßUtÅu¢xÉffŸç¯�¯ûÑñ5; 8^çóÇÛ1z‚û¬�"û²XŒ‘Ü'#èÙ+ÅŠšG©þŒÑvú:‹§JÝ3Eo@ºÀôÖŽÐXµ~L¿`ýTMz}Œ•!¸ÏLÐ)²¥Bc³xŸÎbC¾ýðï&¢ùQç¶~ĸ ožCAóÍÔÿ6õñ=�«"‚ÖF€áxœí}ùsÛæ™ðïø+ðigcª¥dq“ZŽf;‡»�âÆÞ¤™L&’ ‰˜”Dwú¿Ïù^xH”³Ýmg7�ïù¼Ï}<û/¿}ñöÇ×_ÄÓz>»Œžé?i2‚æi�ÄÃiRViýÙÁ²}zðž×Y=K/_ÏŠºÎòɳÇü;zöX¾£örzYÕI�Uu6¬žÆ¶=¼ˆž%qžÌÓÏ’|T¦7ÕÞ\>{œà‡g—ÎShñp–TÕgÓ'ñ¸‚¦Ãb”z<øŸ .ã޳뤼¼}öÿ9|ö˜¾yöx±—þú1ýÂex®“ÙÒ<ùhšÎfÙâboƒ'·»t ÿw?ëÖ!°›)÷Ö:bÇ—ñ‹2Mê4Nòø9ãGq1ŽëiÏ—³:ƒn3l3Jв-‹xžBƒFñn7ì6oÜý댠Éåõ‹çÞ0‘�ÖÎi˜-†8¨ºÂÒ«>õtó¦ùnv5zÒ‹PÙóÑ^tìß–I^g³ÎS}N[“”|„Él1Môznºá=¸á‡tÿæ^åU ŒÎA”ñ'€hä^Lï|†¸´L , hÜŒàló§¼  }“U@YÒQÆg1§J»NïôˆæÖhü›ìˆÙKý£²‚º¨dZº��G3Á‹]ô©›4 ©‚U4Nr cz߈ªXà&ÁVË®†O«3¤N×é° ŸÂLp Ò7…8{ô ¨<AD;ëo~)[o÷0s¾^pp€Î‘3ßg€–Š|rùºKfU·ø±<ÚŒ 4þì@ù¿cd2ÿcèn6Kgâ(GÀù¿‘ìÇÝ_Of«Å”YFógó›—Ù<Í+ÚǬ^ýò]:Z‰(ð † ;L˜ç¤Å<~ÄaMIyîð¤øsf”ÛÆíý�¹Ð £•Çmî£[Wéò&ÕÓýw[Õ«Yºÿn‰®í¿ÛulýýÏpÏ\üÂåâ›Kj°ïx›‡œ¿Œ¡1Üÿd1݆%ÇžÛ¡YœÛ!0[%!¤t–Â=¬-wðÞ°'_Žè i@Ó€À�‘Ž—ÏŒèð^wìfš!é†&ƒÙt"Z}AˆˆÚ¢øì¥°õ0Æç«Xø†¾Gôƒ~± @£¹$–ˆ«gj ´`\+¯Gxù°Öv°>^'²‰<Ȉ¹pŒEíRaÀ†‚¤ Æ+½‹K Ì±íÆ ¹ÜX+\mt!u™2‡ *¼a¿ kØJ¼px¢„E»|Ê€83•ô á³ uÅa+*úÐs4T‹’£Æ–1DUû]²Ë×ѯCÌp•ƒT¨J?ÿšýž‰Ú±³Šß±3¾‚O…ä%;#%>…ÿE3O(‡©BÈa¿±Â7V;Ü�t2ÚWÆtS­„Å-m%¨bf«Kk)=)ý öõŒg�–°� PˆM~‰3'ÌåDCC 愎šúíGüù=Oϯçvóq:_²¯'j ˜‰Qóq¶l»‘ÙУfD²GÅ�ºÆ»A°yÚ‹¬µuÈ ¢âÒijGMÏ;5!©/�{Úƒ#e‘;ŽÔœÛ8o�.¯> Øå $ù‹ø(C:qä$§J(SJŸ ú> #ö%^bï.…‘N—xëûrl¿´©4‡2«à°ù!Ì™¡ßìͰ×P/í5£8Çó`®KoáÿÅÕ½rõ @b}½¬Ám"EqÇ«Z‰ÅU$6”äƒÇÇì‚�=? {Š«Z”©_àª�ƒìàvAVælcÅú境€Ïiþ®¶f…ûþâ®-Y:ü 숕�” aýã‡Fçø!ÎEaòb½¸ŠžÏEÅodÅ«NC—;™ÚCK¡3ˆ»A”Ù ¾Ý}yìH·~Ü»ºÄ*Áe�>#ÇU⮕sØPA`ª™¨sÔ%•5ªB]îUJÍÁŠÐÞïÙ~¥.D-à_Úƒšû<0 ²º![lÌ…lq9`ÁÖ(q™Â(´pX[‚3®Hä^K¿Ìˆa°ln©j:Ò¹KŠ”‚Õ!÷©™~I䇜u+‹‰É ÔÅ|œ ‚Y°¹(‡Û ÜlJ¶;ñý3?o²¤m#r¸qq©•i0Ø Çj§¡H‚4ŒÅTˆÑ´md<½ñ Éä8éìMÞÔ>Ø!¦bзeS!ʛ͌HÔ1¤PI »ÂÌ¿ �ÉÀšvdÍë.½vÁhpÚ½Qßn´ÚæƒÐÝk…jrqyÝ—UM)ö£ºÛ±>Ý€ ýôèþíö ž‘©9$�|hP�£$ãj Ó¦ø��j}´>Ò�K7³0X�w3ÂÄ."7÷šÑ‘°<ð÷ å øòÞ<*k±_‡ÊZøh°Þ:ˆÖ?Ò@zУ(â^ ñ‰ì‰VagøÊT È–<ˆ¢k›ø5RUŒ{û4äYª „äJ£ªàH}ß�«c«–ûp1j±éÄ'§!c \©þ;*Ý„âXyø7 ÕG¯Ä({�¥|™n£!ºng=?ñ§$&Ý?Ê8Iuƒ´Bä2úÙÍ‚`e-K×L-‚‰64~iéo±):³‘&D¿F;áÛ¦ë€FèAôÑ‘Cn¤òˆ€û�t¸Mô s²òÓ”jéõ3“l2éSv~c@mΣA R ôTÕ­ZZØ:<:}*ÁØ‹¡¶ËP!¶†ØŠç7 ‰`Dßê6õƒŠßª£ºÎøˆ!ÙE»S)C—¢º¢NÏå|Z^¦¼Jc¥9{…™nÉ ä‚:Ç8#¡¹0‡ì Ó6f;»‹˜ê”rmV{ò¥1þi#|œéòŒ SÒ¹@¾9°YvšF멼@³â;ÂÙ«•·’R-¨Œ&<Ë©­Œ¸™¦$Ž™M \¶óàïî³—Œ¶ßñ»·áSân¼p1q¤¿¡ Øï9E–H:ä± ˆô LuNõØÃþB¢ßs¨)=¯„ߊ°¹Õ Ú¡;žû@’ìŒß�¡¯…À4u,ßB ¶!eÈ»¿°h€…p �u¢1,z«Ø—yÜ$ó¸S3Ï0ätÑßµÑÿA —CDÃ1¸ïq ìqëL‚Uí–¬íÇüvëUÀ¸ô?�'±íŸ=Oì7Ÿ+öÜÂ0¤Ç ûH;ôÁIÖ§šGzl"Ñ©Gº&“;ŒùëyK*œ´±vGça�œv˜ÊrSjù+„Êgp—ƒ"9  1àÓn†È5È•”< í.ã¤öÕ { B"äZ fœòÛ^5€hDýŠ‘»0¿Êظrî>p¯¢ö¸ë4µû˜äØá¬�©W}=JYªž»ȵ ƒkßž­ÌÒ¢f ~‡}ë $g~ÃË�î¡˜Ë©ÜÆÆÐPûÇõvÕù Ø×x !©§¦Ì6õ'M‡ÿøW¿ˆPÁ‰[†Õ ¨`*Á}æ,e¢–®{ˆúO‹½IƒÀÔ) ¢}d-½ö‘zÑû!!Ò½™Së)Û+ªˆ�ÇRbҔ؎Pê!²ž1³©rS˶4 W5TÕ±éØí·26¼ÀŸ t¡]‡Ï„d4R°=rº¹ÕÑ‘‚‡EÇö[“õ[!—áSmôy�Qƒ#uuñ€(5Æ­,°~ÁÎwnj,Á§hlDÍŠõáVãy¸K#ÀÌ3€J@”ÆR>©G“Âu‹S…hð¿\½Þˆüæe†0ã>Ó¡Ã=‰7oe�úþŸ4ÿɬ Œ=IàÔÜ`ÂÒ\ºqˆÝl/¿Œ}n÷:Q¦ìŽ{R.ý8Ÿ.å ‘yY]SJ÷'‘5àD:X�"–.¬ØŽ£V:b%Ãõä=CrÅÒnÏ€5T]û`é¦Y�òèp>žðbsqÏÏՖÊå›ÑžÈý…²cýŒWšÓõš>ð �)š\B ›á5§ïev„Oáèó±¼¼êÖ¡t�åBÅ«|ñ’HuÊÚ¨%ñC~„Ó¼šuÜ9 ÖÄÎ_c�B=‰›Økïzǃ¼þìvÜ×ê¡­­Ûeb?)=hûU0€ɳA—ûø&w[áç¦n_K?Šo¹¹«7Éã6¶æÖ8×߸j‡‹Û)‰ð>×ãÇÒñ1]xZªj:ŠéÓäa6xÞŒ›NÐÚ%¯¦,—9ZÒÀÃÌÐ*½™4þ±žN¸¡S 1mxZY¸Ñ`왹gS%AÓ3LWâ»±î*%è5ÈÉC¥fL¹ºÕôrÀˆð­OžÆ‡z%õTd­fŸêj2ÝØjF†:ç£�µrVЭéñÓ±Öäáµ§‰Ò9í%Wë1ïôXgÛ•šáíOtÙ]aƒñGºNþNÞ‰âüÔûÝæ Ãï3ê[wá'ÜÛ¡;£'8u¼¥@séE&Å¥ÎæN7é K¸ÔK=x‰o&ŠÂM›Çi�ŠIß -±°\´àmE6¡Þ½ºþñÕ÷ñ�S7O™… ÷dñ@)2ßžNÑx]ð£|Ö�òFÄ׺ÂVÈý×ø®drd`–§é.GÂ2Þdv[ìÞ¶÷vBáGÚ‰ ãu¸®÷$WãM£ðm"'¬'¥ðšÇ+ºñÜ1²%î Dü[íÝ�R̼ëó)¸fšýnÑ;à¡!D.ºP9}›¨»Tbþã“\àûõÀ£ѯO›£Ô¢W§ÍG·_Dç?m&:ú"ºûxækáP`S4Œmƒô" ×ãû9ñÅý‹ðßøMãïË œ ¦œŽœsxœÅY]oÛ6}ׯàòÐn€£ ­c HZ`m‚¦hQ { %ÚbC‰*I9ñ¿ß¹$õaÙq´Ý€´±Eò~ž{¯2ûíòêâÓ×ë·¬p¥š'³ö—à9~•Âq–ÜXáÎN·Lÿ<™â¹“N‰9¯ôš³÷Â:·³ix˜Ì¦ñðBçõbnwÒ:™Ù×lt«ÉŒ³Š—âìį�úÿOæ³)§Ó/烧Øÿk–)níÙIñŠ--¶f:�æ¼f³57s½ø6›ÒvÆf‹y<¼˜³ßýò×°øÇlêOΦõÏ�ºä™ÓƆ-ö 5¹…ú´ak]Yñ™›È×¢s©MÙ(þ¸.ü¤øêž¦óY!”’õ›¨­êÜŠÖ\5û�³ #¸4ƒ|‘¹T)*'r<âjc¥ezÉpTò*¬Äauš„ã¹\·Ö—6}u$sÉp÷BW©(k·aUS #3Ö&�­¡�á§äÎÈûÓ„í¢�eºr\V–­ŒnjY­‚A %1œ�RZ©ÐYå¸IØ® Nù}< Úȵ]‘2Œ� 2n ß`[2bOûk¹ÏXŠX)]a¾'A÷$�[ª)+à­]Üæ|^ ":MŽ*¥ïÈ‚6^Ä|F¡ ³„ê¾ÍP›Ž�ÁG~²»BSºHÆ(³èüTˆ>Umo½Ë€Ï ÖÛf&§¨y?’� w…¨°#Ê$g+í:‹Q¹Á#mÈ`22õ7q_Ÿ•³LV™jrÑšôžPœÈPtÕZ†,Æ=7Myµ¼ùÞ yöӦݎ~õ§/Ç®^ÆUØgwΚt9^í$|Œy";gÏU]ðþ©ÇeX¹”¶V|Ó®áþ]ãB÷(ƒÜºÏCÈ�eÀXpE”�3w§Ó;Ü’,_Æ ÏnÔùÌ짆x{[9š‚\vŠ*•`»STýÓ£‹ª’h<ÚwDZO-ÇI�|ì�_¬ÆŸ|ö½ÑîÍ…. À:|í¡áÚãŒy 0ý=´*ŠeüþÏŽ™m¡Ú©B…kLЦzÒhÊ’› ;ÿpõùœõÝQb¤°Ã…è qM¥Ç1÷Œäyh¥ì�(™`Ù@=È%a‚gE(†Ì SN˜0ÆW@ÝiÇøìËXGôh¤ÁÓ»—b�ÓLÉJpÃåh?’\#Và öaFß?ãn*"$\B"oY5º��\X¹B]áÊjœ"¢c ÷0ÕËt)�V7F¤Þè�o‚$êEži}Ú'-ô9 U¦!2IvÑ6%H bc••¡2œƒu° ¾Ù^M¶— #ÆÇáåpK!…á&+ˆn·vúšb2â{#`W®…/! ÍÕJxC£ôÌXœ|C{yž"S-Ž AªÚèZ·ÅRp­@Êr¦‡Ø�nNßðˆû:ÔÕRð*µ^*@Ba÷u(ñýÏ�…ÐòK�¨[o÷•rÚ° Œlý6ª}­Ñ•q°K—¢BÇSq°øHÎå»}k0¯•²gý`ð—…b±[ú¥£kAÜÝSêPÆÓŠÂ¡Îý‰j&{ºÊ‡“„ô- +� ¤mX8Ï”X?Ö±‡6�äh¡òÅ9xâ¯q bÏ›¸žÄ@ pŽZR¯…:ÚrAÉ$Ž"¶.�”é·M¼Õâ�*ç&p'I¨–24k,tSåö úž{·†vá((,æýöŸ—õb˜ñC &ìÁºx­Ð¾öÃ¥&Ρզ¡©cYß@v ü#IGŽKÿ ±ÁZCi›oH}ex]PñkÛ9Œ W™ãk²ÐöRú×”½ûþÈ3뀅ÎQÌ í;¿‡NÌÑ"iõ-¥ûꚀÄUטÄ™MU ‡¹b—FëG"cçÔ/Èzà F'k%Ò¬“Cý„0ÀM¸‚¾D¤<ÿ†š …ÎÃóÐa¬\ Ë¸¥™ ÎÒ¬�«Bá�J¡ŸÈ©ƒ¨V�´…èFÖ0a¶ú<=t†YFMЍÐ+pçóë.ͽ4±Dò¡&ÃæGPÓ¾Ùèú›Üìì8º¼¬‡ Þ#æçAé隀)––î�OßÑõõ…Ðdü@:è;Â{Ïæð´7 „Ý€±“€ÁÚñ•}�-?>ë=Eöµüø6â Þy x-Œoäkm]Z謣’Á� 9ç¸dþÕV˜ÊB+ü„î"Ì]{Û ð"Ò‘�t( M/ƹ\ C-ÁdØ(YJgÃhT‡78ñ•U.–°ÌHç ¢_L D<[ÓBs+6é­A¸Ìö(°ÍÓø‡€iøÛ¿V¢F�ˆW‘2xœ­X]�Ú:}ϯ˜ËCÛ•`Q+UºÚ²Ht»}ê~Еúh’ÉÆmb§¶Ë¿¿ãØ �JC¥]æãÄ9>ž™0ùçËÓÝòûó=$&K§Á¤ú@ÑG††A˜0¥ÑÜ �þŒÉn¸IqÊ„\3È•ÌQŽz2vö`2öù+m-Úû©6Ìpmx¨o �GÁ„�`ÞJ÷õ÷Át2f6ùÃÔ[(îrS¦õí ù±¦¿}5ÊÈ/ê¦BßÂd5¥Ä�•îÉ8·S˜£Î¥Ð3,p戯+ìL�>\äc‘¡â!¨*c�¡‘ ÞÑÆŒâ¯CˆéÚ$RàhöÊ´È„5gWPhŒÀÈbnʨÙãÓ3Èh=é5À2áz·^ºVDÞHŠt{í×5¦…µúÊì*t‹&oïKV�ަ̹~OØ’­R$ „a\pñbY�5S¼´[ª�… Äγ³ONp1;Ä|‘²6;ÎÞ›�*½Í΃]p¹ãp’Ÿyõ �*BS(V'Oi�°á&©7¤%`Ä-[C0¨²aé0‰—!¦R.�©QE\bi«Î0±ð0[~›}~£ô¯B~Òõ½äêIù •=’å˜ÒJßejóGg¤ORº cHÏ©ä¦:� ùYUÅ $'¹ÕÖ9¸1ôíï¨îþ5§ýÀèk1}Ùë„j³x'1ŽyÈQ˜3¨¼Ã4¦U1—ÅŸÐQy^µE¨H-Ù"¦kx ›…õ 1¥ K4 "©_s©‰}òfC�ô¡6œÊ)€YNOÖwö;ôØ‚E‘=Å‹_£Ó³ÜæØâÿ0 /ùmœ6ó3’ñH»(0vŽŒuÈR¦<ƒÖàñ†N¯ùDÅÔ]ø› ÷¼f#÷½�w¿5‰B|['ÑaðŽ„£bŠ ݵòƒ.2m÷ß/ý‚ ™“Dev¬[íyûnÅHG³/|8-ÿg©¹ák. ¾�î¹ ¢Š%j¦£LgrrÇ[*Ëø1bÚ!}ÙéBê(»¨‹yòù`ˆ [˜†p‰–|G´¥®-¥†³·’ö0NL�õ8c—wºžTP×}XYw3¥…! ëÖq�§Tƒ¾­ˆ¦Ö§•Fµ¦¹›îÐbëÀß—°L›3 YÑÓ“.d#òttPÔ™5OMˆ½>ÞìÜ}¤Õh’‡±áì} ÷0ŽÍ��f vÄ£î7ýºz=ñê²ÇÏ�4�P-Ò>� –vñÊsÁ±«Ž£ª�3^=Êé¹:¡Ú÷¹9ÛTÍí�SqD£L¡+ÂÊÑ[_Ù‘ºêšÏ4Wk)êDÅ6»•”}z§@%¥� iœqM“&¥¤º ê¡èøLTc6£r*º@ÒHãDØÞ.oï»YuúÑw ny(ƒúnÖÃâþ`¼™·M‹ËüpîY,–‡Hnh‹j³}…r®Yô£Ð´?!vÊh…Ú¾A¥˜?~]û_DÆîw–ÿDCáë – Ò–ˆ9�vÒxœ½XÛnÛ8}×WÌØ>Ù1 @·u l“-Їf�®ÑE)il�H-IYñßï�ºD²Gné†m‰s!Ï Ï ¹øíáïûõ�Õ_�˜,]‹æYL?Q”Fs7)Ìfö~2§÷†›—Ú0£¯bÅ7æêÁ~?p¶RiÈ•ÌQŽz1¯ÄƒÅ¼6Êxo�\;Ü)|€ÑæH/X0,ûÉ)­«OLcÊN–‹9³oNNº•'7$žC”2­ï&É-l4}fôhÈX$ãÓËÿÐLx‹pùl6¤‰8åÅ<·–°NÂzbfؘ†-ß¡#¡r£ÁÈ8WµþUPYˆù®™d¦g·‹2½ н£ˆð‹0¨v,Õc±Põ Û ‡aÿ¸ýîS™“îl…”¯R@Ôª¯õAnY”@>£§ k0¥œ)Y�1£ø$2�¹Ø‚!³©,QA( k�;úoßÖÍ$,’i‘  çvLq¦d´Zè#{¬§ e‚ §Jû$¤¤U“[Œ=…Ó½ø‡$ŠÑaì¨x_ÏòpØœ·ñ ÝÆæGFCÊÆ‚)ÅöMd‚ßwÿÒ|t^ª¿µ‡ioü_¦)õ$@ªž M’üɸÖÖ·‹]ðÁûŠ´ØhtàjqÏAk­,sÃý8AI„•!Ó…ÂÂýaܼÂóÝá<Œ*�‹Õ˜>‰ÖabCˆ¦DËï]îg"v/ S[4U)Jâ&ØûKå È"5SÍÁ®#Kþûo´Û{òˆÈOPø êŒÉOP»Mm›Ó¶»aDªÒ†d£dæFê4Ô\2ä¸*yDÄ'•/Ó9¤ØÛ ×2ÝxO�Œ 5ªMÀr�Øb†ÂÀ†+m|^\¬…Ú®í¬»‹Vë×ã/”ÍF€u)E­L8m Öv$t.v{ÆúóU}¡q6X‡zžá:6ÿ+€Ñ ¡²÷:hóú6y^]]ÿ©Áñ§Œ+˜ZËxœÍY[oÛ6~ׯàTlO¾ - l«c MZtÍŒ6è°§‚’h‹ƒ$z$e×ÿ~ß!%Y’%'X7'@[yn<ç;—,~¸ùýúîÏÕ;–Ú<[‹úCð¹°œÅ)×FØ«°´ëéÏáëVÚL,�åÖÌ-×vvCo$ßÊX›ÅÜï óŠV¤’Q~îÎI·ëWvž6 Î ž‹«ðÜÖp¹˜s¢þâ©°!XlYœqc®Âô[üLñhA#V‰èˆ·ˆî¡�±;µ˜o‰ò’Ý¥‚¹Ýl­Ê"a‘°{! f÷Š%ö„-Í„qÃ<»DX[w¢"Å´°¥. “vxº‰ÜÕ2çfú*ô¬Þ°£Äã"zš*ú lXª²Ü×J3Áã”í¸–<ÊDÀb•o¹É„Yh€›×2Æ7¥–Å÷l͵{i¹ÞëµI¡H®v"ak­rz°ˆ‘ÉB¸žÞvºãY)˜ZûG¡ó’ì¬`a¬_ç–œ­Ø¶—6…ú†Ä*Ö2E,˜,¬Ð ãÁ®¶±3©_·Poa¹ÐÓ*š í§g*qœ&P„³= $fŒ½Û }`[­ ©=0ip)<™ª";Ì‚ñ›“^ŒDÅe. KÂöü)XŒãꢾRÜ_u{ežs}¨IX•özJZ˜2#ñ{Eœ¬ë�d¿šÂ]c8›­o;¨\È ƒ·ZF%Û´®ùx~›)ëüæ³³JEd´^¿Ë·Á³ë›÷õûÖëà¡6šç-áÚÇWÇûþä5ª7&šïI̼m¬ŠÍò³ŒgF!çÕRÀ!Õb}~8lNi¾ÞÁ‡à¥3‚¯g­{ »—Dá˜ÌP.®®ÉYÎACúrÙÐ?V_+×�Â8¦árÕ,�{I@I±EŸz¹°I0ÿŠÁ³s›go«ˆ —õ7/DMÄ¿M½Ø„ÓS¨ä£c^=óæPâÿ×争÷ߪX¸,Žé÷Ë«Ùu¦¶à8] d. ̆Pä;¬€ – „žjµXÎ0ßVôQej‰Ñ�·Z› E5xde^° ¨ÖrËokwߌ44lOO…²ŒülErQc·‚>\v`ظ}ìì¤2!! ,ƵìUö«qà§¿Ke_;.þkGüâ\8ÔÙÁ�­Ú{>»°êÉ¥1ÄÛY8x2&þè)Ì[}3m•p;Eò0U F&‰} ?‚"_œQkmüÓ=*õýäXµá¨Né­Ú ˆ3$Û�”¾¬¶ÈQr› J,×JkH€Ü½×Ç,7;\Åe}–bÂ}”ðþüX%4r�ì‡@7ŽÞÅ�41�Ñáüº³ÿÑŒÕ�”‘…—�ÒlÒ”™µqu¬Ãa:(ðd@à¶Ì¿T· «´ŸÆLQ”y„$#Ônr,Ý.*úªŽøÕù`oø'YáÃ÷fÄ౯êÎáO¸ôŸcÚ¶QêéÕPµwÝ¢oÆÍuGŽ^ÖAwêsýÔÓ­oϻɸ Uß^ôRõO©È2¹}]Ý}æ1dp±NÌeËN8ÚF¹§©C«c”E"ã!ÖedØÏ¨óÏ•+*•�ÒÀŸV?橽p?&µÚwã:¸ât¢ÐüçÁåÓž 7Чžã ³¾<£)ÿ¤'·t§p¢’�I;i<@4Ó Ç¡Š”ña�hñ¼Z“׃¸B"h_‡Ÿ¶¨È Ë�IÅFÐ,‚­¥6öòíŨ²@Õa4Ï£ØZo@¡ˆ¶ ^#áV¼É0(ˆ�×ËKjSu&-}ú+ß§*O¯£Õ¼!ú§ÿf&Ñ&}ÞXÕP+&Zˆ‘r9ezÉðtByªÖ [:£fRdlòÓiÀ.«W{’ÌVÏhögô³¨}f©ÐE=ŽŽÍ88ìO‡Ïh×õIңɑòì°à²º • c#î3Ú‡#M]Z§"¡]b '„yõµ¹ÿŸÝ?�*ý ððŽ œ xœÍšmoÛ6€¿ëWpÐn€b· ­c mt@_‚5h±O%ÑW‰ÔH*®÷ëww¤dINÅi›mcÓÔ½ñîñÓù/'^�ÿ}öšå®,Ѽù!x?Já8Ksn¬pÇ“Ú-�þ˜Ì`ÝIWˆ…uÜÙifäÒMOðßÉWJ['SËÞ —ëÌÎg~o4Ÿ™‰Î6¨á1=/i÷36N<Í9S¼Ç“}�Lm]–Ül&‹ùŒ£¶'{­m¶ƒ†'°»biÁ­=žäOÙÒ‚ŒTgûÝ}ÆæÉ¢’,ØoóKn:ùg>ÿÏg$c>«väß#xëFêAq.)¼\v|;Å vΓºàN0— FJØR×*›F~K&/ëJ{ôtâŸúœ #XutÉ‹ZX¶Æw,*AR³`xc½c•‘ÊÙŽt¬¶ÓK¿È!¹¸eœ­s]ÆU¯OàõZºœi%˜ÑkV ÃÀIŸÀÖLªUtR€>’à ·±æìÚÚƒÂýú+­–2**' xöL£ap%ZÏ]×&Îð¨Ð¬FÏ»ºp²*Ä9Ä£5¡µõ~ïsÛKzÏß7æ.µa‚"Ž›"&ƒ½SÎÄmüY(íb’îÚ ÙÆ>áŒL‡ÁˆzŸ¶hÓdn�Ñjµø(@ya5$Þ,,EŒA æF,�'o6•µVÚ ô\ž"A~Í„©£cŸ,:o|5’’dÚØji¶[ʸ÷.u|E}y‘?ºª¯ÓÚ$ÞÃO¡&n3?‡¿×Õ>$ˆ(+ 'Î –{�Lj'µbËZ¥ø¢MÓ„[QHåóÓg–Y ·7|^UÆ1�zÆ’ ¤ytȵfƒÇD*Š£Cn|mÝDs* Å“±$« €È—(~º”«TT‚uªìôâepm— §çäeO“[ë^¬¢N°ÖaŒn0ùÙÒè’=j 4Xç$ìѪØ�<Hª« Ð�ž�˺(üv�¦vÈM‚hføº5f«žC˜¬•�/vÉ q) Ërnw #<Í)Ä ùÚÌ¡Óñ†µãaá:C6©L|…s£}¾”ðiÄ2±ä€ÃÛsd_u\”þëß“eoíW…v¯›ur:Ù�Šáöé”Cr¤ðêìXFmŸøÆ¤ê þѼگûnÔBÙzexyŸbLl M*Ô}²ê¥TÛ~¤Û{`x®PÔß1ÀU*áÀ{ÆWUb‡`y ÍG[ΉTPͧP¢Ð¸kPª5ô-vÌ ŽSHQH¶Bü¶î‰úÍi®µ2`ÒàÈ[`‘�D^{£2âHºŽÎ°Fý�Wš±WšJ²��1|û0yh¡QØžI•ѤÓy4}ʦ‰A¬@8®lUŒªàÓˆ•ÔùXZ&‚�åQ0`ð§ à›¦`F °Ý{ ÿPF§ ÁÁcw›½v„}wúå]öÝ^ý‚�Á3>Q·M>Öl˜NÚvS ªÄlÚ,ܶ×X!mÁKß<(ÌûîDA­.\¥ûŠjð? ¼Æ@¶'ÜØ˜i…C£p�H@5Ù�ñb_bmÕ žxQå<ŽzOm2 l]ùâƯò¯mæ,‚»‹øaM•æ=n‹Ê`›mŒ‹Ñºfþ­µ{þ‘$ÿz00úÅÏÜ(€UoG‡à~�Žº¿ŽÙ“˜=Èá+LVÏÛ6Zš€@d¦L¿@ëš­ï lí¥Î¸»œƒaÖo!ÇÓ¬ßKÞg}i·ãÙ7UthëöÍ »kh“²×Ãu̼‹L»¢YÚ�Ùóë›Pj7ZêÒ€ûÑp;Ò5XòS¾Ç€ˆ;¦ÙíCžG�Þ›,éŸ@çڹؖЫ˜?Ã×@ÌB#çfâP4¸çC~´w}{ð ­ Æ[¤–È‹½7^øk£·E“¢Óf�ÇD§ßº+#:¢¾# öjùiè0°òf4ô‡J¬“mפ¿;Ì×ÍR¡Úû° žÊB?F÷)øJZˆf@#S¯íB%‡‰Ê{4¾DK÷rçžÐÒëë¢íÉÜ/aºW?£o|¦Í™0e�¿4Óê/aá,n5kí>}wþ\%ó;‚hœºŸ†H×™{3šüåÄ–ˆ4Åm%ÛÛÐ%vŸíÞõFE?>Ùª ÁÿK'ÆC‘ëšYƒ~û‹¿ƒñsÛ³@O` z�Ì Û¨+‹S²�'à]©­iÕæH€–À‰=¼ŠnÖX^EC]#øüð¾õ§3º â]ø‡¯&Ï~ÈU‘ºëƵˆæµûømÜ,üv~æÿÀÿï ¸ ��”m©^+1xœÍZkoÜ6ý®_ÁUQ fìlŠìf³ãR;. $Mà1¶ØOG¢,¶’8KRž¸¿~Ï%©×Hž™b3Æm∯s߇—^üåêóåÝ¿¿|`¹-‹e´hþ<Å_¥°œ%9×FØ‹¸¶Ùüm|ŽïVÚB,/U¹á‰ý(+Áõ'•ŠbqîG¢ÅyØa­ÒGÚï¯Kc¹•ÆÊļcS+1%ZpVñR\Äã ñrqÎi§×“çâs´Ø°¤àÆ\Äù–ü7Ç?-V&˜4°XOî²Æ!nîâ|Cû-¤¬pÓ˜÷Z#UÅJZù‰©|hŽ.ÍüMì×Þå‚ù£§Îr~3Va[ÆY&­éø´ÈÇ¶Òæª¶Ìbk«¹¬duÏRnùcï¦Ö¿ È"M„­m­+µ~t›ù¥‰_Ú̇ùs•2•™Ÿðä¦3Æ«Ôm P†‰¡mNè*!RœcÃÀÆMÝh‘JÉ´*ÝB¦ë ÙТJ#y ´1cÛ\‚¥Ò$\§´!�¨µúöT8Œ6Ø=ï}Œ°€ßWÊ™+¸=,ÃZ UÞåÒ°’ÿN6‰ŽU«)yQM’:{Ò±¡�ž>YZ¶å†½CM¤’³¨u?r¡�V�ÎYßád@�M¦j±{­ê�y7öXË×…˜ÃÑ6P¦|ä½îÛ`óÓŒÕrCætÿZ+�ÂfÍ?MéÖRè7ßhYrýˆH´ÿçðãÔæñë×ßÇËŸ2GÞùûß0ò¥•ÜŸÓêsR‹Û*lu©D–ÉDŠÊ2ۗܺµi¦ÛºQb;ß4ê{Á½4PvÔm2ƒMágpA&´Vì¼Ë(Þ‹7ó^Ô°S¦4<É™º|9ë\¨=îR=p-9¼=œ;Ïù©Ð´Ãt€s¦¤'d¢j@K®Ë)�\`¶3>‰Ò‰©g^U¤æIõ®ê’ÌÈ:¹É#C¤îQóÕõ‡)?­Ú/ ¶ÛÞ·æ“ùOÍá^­ieB@Êàéoµ� /›é«ñ¦«ÕíΞ«ÕÝΗ�êþ£ü]2W*¨;`¥àºÔ¶Df "öâýÍåŒýH›œ½<¤¼k¤vJ_!©"Û)]ºdµGs·j [PÈ…p*-(Âø¹ò>wgu•ÐvÞSjW„4üÖ²Ú J·¹p )ä8Ø yÛ�Ó+8^˜!Ê�}t�dÖê¿(¬Ì›ÇírHú›jƒ²E•ª—Ïö~ åÔß1ü÷s/óïZÃ_|iA€Ž—þ‹"‘½;Ô®\ë—>1 ÜYØlx[N8N3Üßv §s—hè¤ãeä2þ�"á0ָ“1BéìpˆšB�£ÀT*L¿vc7΄‘iÍ‹]Å\u5´QºQLÏ=ôÌëuQ©Šr)J� ¾oàgäx.›9WDÉýL…?Ñ‚RÓÌçUjÛ… j1Š*à²ðù¹:¾8ÿŸ•½WðóON¨QÝ{‹¡+aœráÂ÷ª šÀ«Z»]o]IõÔ+ÈMÇ‚¢mN†8÷5 „J!VWCžÒò+lï%†¹.‘ à0*I¶Q [ ÇÅ$#»H;BÕ†êU•ÉÔ;Ì!—)pj¡ª{x�wt·º�@‘½/�tòf{ý9i½P­°„Fl¸†ã1DWÞMJN\ß×%UHÔ¸1{ (ß/‰ÙZØ&òÍÀgá9^i êxÒ–@­kD VS(8õ$ðD”=º#l d….Ûƒd¦Î$·ÎÆ+3êk¡R<§R�ÝT¹O§+2.)‹¼¦QlpšàLQOI…JMÇMƒ§ŽÁË*�?`ôŸ�í ˆò¼gÆRÍ·•£Á>?4 ,ð.ÖЦ1‘ â¸Mo '7‘ªË›}r{õ²_x‘N»´#}tï‰}¦æn I® ÜÏÈûJbÀÉQ`MÁ^¤"ã¶e{•ä6�—¯Î^½Yœ‹r¹—a %¹fo qÀ ×Ìüq£ 8#ÉQz*PMºý‚Û'•’6* sìdü• ¢Ó•Ãs‹ë9�¸›WÄè3ÊÀÜé*;ØØ¥õ?„Vÿ$ �†Àô=HMHøÈuõ£4bû�—à³3&³pÕœ1M”hN�1w)Æ;çYÔ†ˆË9ž¬û"¥\²N¾î¨½çA@YAd$ ª)Ñ$Ëðæ8Ól e?dÝìÍZ˜æ`t¾_RÈ ·Ôóù¼ù~Ê ’»x´8ëå’µ°[A9Ýš(\¡iopŒ².� %ÿê~¦>ƒ3YA÷óe.àÿÜᓺ¿‹àǦmRà æ‰íõ~U8B�Jo¬ ãҮݪÌ™«UX×\ºLå<¨ÍžX‰ÄÞOXÆF8gF|ÅÒ�ÛÂUÇiÄ%›i@šä‚ˆ5}õŒ|ˆÄ›’§^áb¸O#ï!ø£ լ˅Y�6äÏ©"v÷¸ìé }R¨r%Ý…i) U+ÊjK-"6Ð<­ÕN ot›¥›°¤3�®i�:kUÈyØA2|éøàñG¡ÄÔ¦éMµ´¢AO+¨ë@_¨×ŽY€‚·,WB8öóŽ-Î爱g9„»ˆ{[�QCõ»AspGÝzbé¯�ö޶8 2 ¶:k¥.©ìé;¦pBwÏw}Òü‡e{àX§¿v Þ=ìÃpØ ëWn]9¸ïŸØö»ñ”³ÉIüTß„/weÙ�¶”5Tá�¦P?¿¹îÚO4t¦pw+?éäð†ÐÂòIðT¸vZWñ¸—5F‡)k$+XØç²d0ÿd8'hñžÖÚ>ä]…ôW×bߌcºqOh©bœ Ï óï6âÆ1a^´3z½Ç¶è� é°�z‚c¬Ÿ‚Ob+- Jnâgó“A\Á¸î:4ä,tëÒÓšôÖÁ¸}Ç­�+ŸL8%îu’'@Í0÷ðªJ3WÙœ8J×ç>Âik5­¬x8\| ©¹�}·|BAÝ£§éc:!˜»Ø7êÇ`î”1¾û·]ø)‡v}sr–QCÿÛã Mñ¸ëŽ?•w2?á9@íôäãq“~_<äºG Y _”N‚¸{&ˆw_ ö¡÷þNÉ4Ú÷ŠxçõbÄX£HÙ¾¼œ êðý$½§LÀ¥�gVhÿ'ÞyÕ™ÊymcÓãr¼üTØúIñγÒÛMÔŒ¯é×A¼�O®ÂÁƒV¼û¾µÇÌÔI:Ò1Û6À�KjÿrÜ<¦üO×ÓÁm{Bxß!ŽÛNñÔu鉮0I;y ´!|76ýŽÑDãè‰Cmà¸×0ž`Ü£±kë†6ñ ‹;eüûK§%$‘¸{ÿ 2~ rO O#>%`ÿÈ·�-ܯydé𺖬_áßJIpR�»§·¸y�;Fß{ý#\á}ó®yŽëwe]²˜ê/ ÿéÚòñ°G?!× !Ðã®»6MžúåÁӆð½�ûí{Ēݼ¾t£žöHºçÊ[®A7}ò±$OöÇ÷˜co×�¿Ázî3ö¿ÒR2ß @@“=¦~0\xœ½[[o·~ß_Áã‡VFt91PàÀQ¤Š‚°�BZE P»”Äf—TI®eõןáeï«]9f qÌËÌp¾¹’›é>~ž¯ûy�ö*‰gÁ4ûAp?¢0 ÷XH¢Þ_¥j;úßÕÆU1™ÍyrÀ¡º§Œ`ñÀ#£ƒà"%r:±‹‚éÄÛðè¤I¿�I…•Іòõ�ÕÁ#†òþª¹v<çd»¥!%LÍù³�\ͦ¬YÝ´ÈØ¾8ÝÀŽ c,åû«ýh+áÏ~U@/„­-Än3aOhº™�¡¼qÌþéä yÌP1‹¬}F|‹Âb7" ˜!2°{"ú”I–ÈÑW–Ì4%I>ŽÕþjv˜NH2mN£3Sˆ¥ 4¬0Î¥yÂqJàÜ\ µ'A»LCtÜAÎ1GTê͚цˆÚÉ$¢,0Ó[ª‰Pb ÇíèahŠ4ºzL“’:¥Óà¡õèçZ§2@ìkÄY|;mM@]›Í#¬‘[ŒYíÛXÑ6;)Ô¯ÏÒg!‰A—Bà“Q;8.H<‘PqÀî¦0e”í,N@X/w€ Aä�³HO[p€B‚d¼8ß䠮¿wBçsÏoó¡ »}¾É†¯[P|%ˆ—è¼^à¬Oõ §ð&&èHÕqF�àGㄯ‚fL¤ã4aòÖÑMuÔŽéÌ)|á|ÕÉ…F¹÷FrF6 ;K»W‹bÄja!"ÎNrj�­j±½Ø­Fy¬7çÁHùHÒä€H¢û¿h‰ ‡‘1‹He‚nïD½®t�0h ¢T°€]TG³/("v]xB{üD‚šÖÐ{ôßšU¯-ƒæ°0þˆëoÏQ™ódõµÀØmø�(êÁö4›æÿ˜‡~AÊ  Ç\R©ò„"; y!‹TŠ+ŸÏ+¥´XÆC ãÔÄ2ÌNÕ PÓhƒø‡Ž6$Ä©$†Bô¤6  j2ÊŒ™¹E/³u—/`ÏïB¸5°¼å3ø‚ä)ŽãSÁœ�Ž $%ÆUÍvWEiqæœ6ê�¿�‘­a"² "f ÿˆx¢#‘.9`\JÊ™'søøiÑ�<,ð²¡ÓÄócëAmÐ)–F€*š|#‰x‚àÍ™D ei¹L Λҭƒ $×ñ4M>— �Ôƒ§'„îùîž~!1ÝsucUYêµÅ&~°`ç+²4]®±{P”!Ž±È»›u+HÆ­,lµ(!LÚ:gµ¼Lí̦xÉæµÖ‰qḠ›T gjž„`f¹;'»D—=y“’KÕh@V«Å„Ù&d i<Ü/‚Ò>g$¾êOó÷\PH`ŠÝ&Q]ëÅ&ê$›Fñà’/ÐTr†B»÷Ù‚)D[á RSon)‰£s忇»yQ²}ø‚ÁJ l5ÜÚK3¾œÝ5aÝ ï"ßi#»GoP1�8u"p K,ïæ¶s µÉèÀ%qr€‚ZÒH;O½å Ü8>‰û9HÞ¼½¾ž Ø(½½nçýcù´?â‘`_¯;oR°3Œç=ÏÁ­À�tã/ç4°¬`¬zТ þ©µËׄòFß V OY‘\ß™dÛ.ÌÄCã÷ 2¬…L9'ø�û«žÌü°ò“™ �'Ö¡QBÈ$º(wGò¶˜‰&È” ÕünÇv=E'¨µ­C*Ävú+I‹S–¥‹-�5”Ùê ¤e/JK_0-Ïà´äP…&ß,ù·PÀ:¬¯Û°¥“¸Gƒn•-æÄZ49Ê4¸ƒp ÎñíêT\¿,ƒ©#ïN`ŸôˆXœŠèZ¾%Ë�ˆna�:Õ—Þ@}ºZ-Á�V«õ™¼ý•BÌŽ FØ�´´‘ÎÞe„!Ou#”_΢žV8+ûªA݇»­ú¼måÉÙVí¾¶j B¯.pk¡ *WéNÜ쓾»r— Ö¤LÆww -© ÀÃÒ+˜D殚ΑÐÝÞÔ)TÉ¡Žª‚‹•ù\Eé)J‚ }°-=Á¶luì¼k®�Ï7ty¢®?Ì.¶ËíœG@2>¢v­•Éð®ÂøÁ» wðÍñ^÷á½ö„÷º ïµ¹¯ûסÎ[Nƒ2(Ý ÐT¯"ü¼ËTiv=ÇTnÙS I¢xóò(Sa�?Ê8N¶ÎÊ6qAw&ŽÃ8–ü(Ý…/•Æ•]=¦!0 ò¬-QWj".ú-ìx5\îm‹” Ì>÷ê· Ó(‘´'üs£îG¿Xê û2Å.äÙ:{㽉{‹­Ëo¡káVÜÀ]„÷KÆ©WßùJ_ê.ìÒv¡ ç_”R…"¬ún×_òÂÝÎ%0l2r ªî6´n )çšB²T!ÛØ©_ÆSf¨¹¯J<YÍÝPÖó†0ë$[àÔ_ç;=ŸZµ}dUt(.ÕëQ_wÃWJç=¥}%ñû(ð+Û.\·–©Âœ¢Oû5}w|ÓV´ƒOy! wPšuë´¼Ò‹N«›:½+½Eá O•‹N—wË×hRkí«;râ&ª—|¦6×¼KíGFßȤ? Å ¤ XÄSØRîŒÜDˆ!¼BL q|¶9[b¶+ug[gI”¥<••™×HLL¡aE)„ø=¡léóŽÖ;Kª� J«è³ÜE®~¥–>¬ÊY-lAÒ˜ï4Ùwª}¢ÛGmé>Y¾Ð´kÔËy!äåä5ÝÒé¾É%sfä$�ÊR¯ÖŸ: ü}E1ðµi¸É£^ ÛS Ô}ù%•ÀÄ}á?±ÿ‰àÿ‡eäLVN ÙàÉÙ¤‡)£Ô±Õ"pyQ™‚r¡Úzÿ$äSÙTBuG>õ»u8E‰ÛΨ.BÓEº6Q×±�}!a îDE²Cm›“áàµP ½Íª#Œöh|òI¯ç–Ä»hŒ®ÇŒúÏÑÉÖ`X̧�ÿQô,VþúÄS‰f µmàOf*�Î~up9°z'm�ù�¿»1‘!4ƒNZó/Si$ÒiŸ4)Áƒ'˜Ý|àmg›Æ‰þëqÝ(TΜø™—ÿ,JNP²a{1§_ËñéòLhv÷úÍ3«/|Ee­&,LE t(ð{l�òÚ&'k¬ÀînÖ›u-M(”r°Vl,S_CØž']s-Í?Ý�<´ú~D¹”Ý„~Oª÷¢_Þ9¹ÄóV �tp%ýÙÝœ°œà Ð-Ƶãe–è�™XƒBFø7ü-覬µÊ«ÒÇ âroàÀ·¶°I0Ñ–�bKoдk2 "Ý´Kžº²pŽFR¤é¦ˆâ¥Oþ»ªš +À).+2S¸=9ï™È—sÚÑ=ãµÂ X”²êôVÕø¡ZQ8½ÌÅʤÝËã´6-Èlþˆ,(º8QF7ëÍžEåvÁmÓ¹iÆøë$ƒêÁGùÉ'LíE‡ã€8X&bƒ>ŽÐ®»´.ÏUVå\5å¾oG‘ûÌPbI°ÿ¡P",ø;™WÅ‚eŽ4Eê,_£sµUã3ÞÞTô/säXXÇP‚7 °ûJ–T¯X»æžªå (Ûdr¬+Ùâ�0ƒÈ•™×3ì1sæ´”i\=ƯùáLåÉ0úÈk–sfÂø”0ÍÎPW”³¯^€ü)·ܽöÁ‰Èä¼ 9±Û˜§q»±”p£4þÔhŠ6`É×`ûõ6àΦ‘ûnãN\l·í–Ñ{ÉïXZ›Mú=¶ÂÝyòÛÓþÇ3÷ÑE�î‡zïìÉÖÚ¨­t™ž3-kÑ€†²žõן™\¡Õî'f›.×§²aÞ#~êÿo$jêRÓéÞŒŸê.‡bKP”HçHæ‚×Ñj-ÏfoKÃN—jö߱ݺɦ•Óï­¼…SfvSuÑ Õ 4RIîÏhßûNžv$ø :ú?¾úÇÈ9ÚC¯Zýp×Ǻn0nÝ¢©Dš`…�âÑÒ`à£k+ rÐb¯û®3Q¤KõÓ$ô�E �+f�Õ£L0ßRD7†4Ç,*b²Áø¨¼é£Û>ç>–�Ç›/!¼}“)ꪥêèݵ,©:@¹¬ d<2¤Ôy#EºM{Ñ.¹|Ê\¨ lSŒ5Gy¤ˆ±²ñl\““½z&ù:¯Þoµè`�C6¯«ëªi'½f7yHZøºêÈù�!Bon<Å{/Ç=FÙ7Ùˆ>µPsÊmb•¼4{›5U·Ê&„2ÃhÙ¸l ð“êÃÃÄP?á9ˆ„¥5æ¸Å”±$L »$ ²[ŒÐÀd’�¡ÊiÕt°Å&¯Ú瀞’Vï¨Ú„�w÷ì‹ËÞã÷Ø9¶‰²¸íY¿ûm™Ïz¨áü;¤~‚‘&©„žBšg|kÏÒ!ÄÉ¿(ÈŒXÛØ ”€uh)#•U�ú ÝXG²�ƒÎqrŽTXpá®ÉÀóëZ¢R<™³¥!z‹VË+›îðq*ÅŽ (üʤ†à<AÐ�Ø�Šºè ›š�ÒY¢ŸÅíÄÏ”ú¨�U9ul`Þ¿";çg€-W·TÝhm“¦'�è­Î“X.$wîåhÑlÓ}›ÊÅ œä0h2ˆ%„#ƒŒƒÖ!€Äb° aôêðöÝG�-Ä·ŸŽøœûϪ´Uj×2`"´÷àc�VHoz 4&mBs4r�pYƒFmù,ä£]G°oºx¦v�â±jz ÅQ]F¹«Îa[:»Ký?×’Œd)W‚>˜<jƒõ Æ.ã’-•Fæ¦î—©:vl§¿ð~ P�°˜v`)SÇ\ÝޏO)é®Àú„Ëê¤À_"�g ’¾Ú†€ÝbŸ kpÏ=êêkœ)Ìd‡Ô±ùò±‰Øe9½Å ö4iv­â¯XØ]�TÜðC�®¡hŸxá�æ*[1p0¿=îG•7Wû!¯3qhêoN�nƒÿ¬3•hë~õ¦îšzº&RM_ƒÈO=7+¤¨ú)°¿£ ñA6 ÓC �T“ä$=# ´¾dJñ&¡b¸ùõ™h…Ç›û ëйnü>â>}TÛÇ�¢%¾ÿf´È)H\ ¬Ûôí“‹“1¿ÚÉKÔ»¼š�Þeêw«Œ˜üíýŒ¿6Ò[C»žƒ§Uò�çb!ézâ“L��w¶ä:†°ézê·<ÀC›ïö-,ˆ.ÈøËÎkQ®oÒEÙZA¨^Ezp7 GRY²Ý8.ü¹X®Gbc~þœ¿8»Ä÷ÅóÛ/êv]O·¶ÜGÁšÞWûþ4;…ªðH F1yÙ 0úy74\~¾ÆÈÊŒÿq]àT= gLEŸ$œ¼•¹,àÏŠ:•SêÆ e†ÞÌX¤Ô},hÆ2w×É4ášk‰ošºV¢>ŸlÄ(Öõhs)dL;·ÖplŠ€‰ÀD˜Ú»Ë°Dwô@Òǿدã,˜M«y¼D¡¤i"–·ö¾gµ‹È9@,E‚,ÄÚ˜ÁÌwüÌÌêà·µ/„ì½¥·àcÊ�F±’u8AH�DÆÈv!ÛiÁTOÃ�!¶¡‡)(�:�­7Sž…žäªå´U¤t¿‚t º)ÙÈ`<Ðþ]¸¢½�][âEÆÍSŒ‚hz´<æ÷œüí†6sfK0¦×ï®XlŸúæUúI[©8oÍü[É…øCç6ó•p³”‘2õé²*0løøNh …°" lAÏð¥Žf¾æ`SüùR)i–.0@ÁxÁ¢ço$o²t˜ ¢rz»‰Ûñü‡êÑ"ˆ›˜œ˜ŽäHhb¸Ñ3SÀqÆo¼û³ð M`cÕ¢ãk•ñ}ÊskÛÌFR ®m)F™É Ã$†w+W˜0mlzv…:ò6S Ü]Qö•=Çlë~§lFolŸðmKôÆöß¶EoìŽáÛ9ÕÍ0ˆÁ)ÈÉ—ú@9‡=šHXwépr赚QÆ"•:iÔݬ"éž}2J”mz—\ôˆ<Ø<Ë„½Ñq5²îøÑ ðœÇ§Œ…ëC… b™F²å§OÍ5!0$ô 3HV×ácé:dòNL‘† ¿ÄFN{N�s¶ʽMbš u±ÆçvOeÙŒP­ÀÑÿûßgÅÿ÷üËûùöÊ  ã}×ÞZ;Û½3“÷�SÄѨC tvE¬!vþ[øºñÎd2t/RàܽLatÇTÖ¦Á5*ö0›-x¸Ä>U›Ã(V+é—õ "têMÁÒ¥3‡œŽž9‹ŸùQ÷ˆHØQÂÃ.‡øq¤qè¯p;µaÒ€[ûžŸÌ·þû‹�t[“5cûD}¡ºŸÚ? ÊêZl�üiÈçue´‹ÜI8-¥{~À¾“¸WààïûEï{‡Õw`�Ùc û ¥×êkùÔz#“—ü ¨ûo¢ÁÞ_»þ6‹À6€ö¹‰ÿ�ï@r- P\ã—Æø“W?þò„‡WhEoH§‰W#…kàþþ<£±3ÞƒêŽ ' °¤I¡Ì¬4'/e9Ý£f óšœví[úÌðŸ¥ViZ4l0½ÙÇ–™ZÛ~C½$ª¨ÍýisÀî%OGœnÔ9`Þ8yNf Ü¡§Æ”^H³ïõøXç6�ž¸µœƒ™:ßça-!|#!ä²°¢jw×ïF`uÒ¾nwO.¨bûx�誊?œf€ÝÇÞh“>b;Ç͆–ÞäcÂÍbêl6TÞTmèjñ&&’glLzgû™â¬Ë…eÔì‹ß’@☿ªÊ<þ•‚Ú²�wñìö3ÆÃpëBáx”^ì0_.ír¾¢²�¿O}3m¼\60j#ý&¾éW]ñc´ó¹7†çS"Ü5ÏÏæa|ð†˜è…Ðß»®øÔì«Ð?æýÏhy}�¿ÊâóYzw:7^ :i¶<¼½Îk]Ü\b%tAù_«RràC°ß[QÀ‚ÔrM�;N«˜%šŸû¯âp9ZÓ¶?ÕOo{¸îÚ°·-ù"œ¬ ùŒ®ˆ°“)=,ci×ðhèü5ˆå#rì=ÀÛaä,L,%D1’ÂXLíraz�ÜBÎ<=O‚õh�€‘RÂæÍ¢2y„7IîNªÌ7àªö¨/µlÚ¤žcƈîžÍèÅ `è ÜïܘæWâÝmdN®}=Ãõè ßÄ \Êsc;bK¦†*ôƒz?]ÔµÓÑSGQBìo÷")S“0™¬ødbWSq�m-ïŠy[Íñå,`1¯•î%Vø–À�ïék¬Çaà`±¨“ȹI[y |äÒ‡±¶š÷®JêO ±� ˆl« ü~ÈQ/”¶ïi5B7dL¯Ï5EÞô¶0’P\šÊãÕ1Ç‹PqþÙÝÚˆ†Çù[L‹„wçƒÑŠ_®á_�ûŽëî<ؼà^Ã1cÖŽí66™0·ÞÉ>UìÐ̱ÙÑmS•«Ë×R’¦Â‘?±_1Ø$8ðØãƒ8ƒ€ohþ÷苃¾%`ŠâGÇü½3—$µ¡´õu†[ßbˆIŽ81rbßñ|bÞ!ýß ¬ÿ¾ ¸¸°= à~Z[xœÍ\yo·’ÿŸŸ‚«Eb ™‘,=ûÖ’8¶õ v‚Ø›—‡818ÝœiÚ}¥Ù-ibø»oUñìcÙÚˆcÏ »X,«~u°/þíÙ�Oßüë§ç>ü} i'àòvC‘7’Üü0‚£>ŽÔC€`HÝ �Üß,Ö¾ø¿›Ó~zúeü³`I^™O¿Oqôu&ó\Õç_Šú¶Ìk?5wçâ’›9x¶®+Ðw�Gâp.vi ôE˜�íúgælã„-%ÌØÀÌ`? vÓÎ’TÅB•­…ö¶Å2Òã�ü×µLHÑ�%E°rœ?G�3&Èð,ŠNÀ‘!&Ào&!7ÜÀ¾8Ð×&k#x£VY;ÏÈ j€¤Hq¥®eÉëžÊMƒÇº‡Q0ñ/xT€…1Ç„ßòÜÈ�_ÍQŒ��)(ŽyOî³ì`X¼Ã™h'P²¼MdÝé’k¦�0þ/ÙTO"QÕsã@2IfìÕ܇j$iÄ�HKd ´-8%+]Ã8ø‚:ëϕʹY‰D0Xü„xÁß.0luƶÉkÓ°žß^"ñX†×2”Åp‰¹,WèÙî /ÆëäE¢X€„ñ0G?8¬jrz‡0®ésƒÄPçݱÑxY8*p„À¢-j@e¬!ÎrÙG›ˆ©Ìñ1R0 6Döª4=üu£ÿìªs xO+Bx¸PïàpæZÒ†œÕ !M¬oBr %ÎpŒ'ÛðÛ Mù‡Å'€ÎVUC©H­hµ¸&ˆUnk`]c”R ›uލ†c¥>ƒ¯’¼S�Ö%†X9 #ˆm÷ýŽiÿgŽÊÖcñÔêçO�þx Ö Ösø‚ÿÂ_ØOG|œŸ~bÃù o¶›—_œß¥–O+øFa¦Àí" hD±é+/x6)mkáÛL5}É5±äDk¶—ô4 ræ`9Rø¡¬ ô°½AnL€Ï? ûÿ*SÙL™ð‰ 2ë\VyŽ ©‘«ÃfÆ{Q=Ãx/q.Ýy·8–�CDŽç½Zˆ…ÊU»�ý3“ƒ£Š� ‚ íz^ºDÇ+ñÊ^¤æ½3M`¢§·¨íÞû =¢B§ ¶ WOàþшµaÛƒ;èób‘5>£vOm#hÿµ”7÷�Ô£©-åµ ã~¿_^öGñ÷Ë“ýs6½/~AÞ�óýÉÐçÆ5b ’Ò›ñýsºý ·«Ù[º>ÑK‹8bžUcºµÃ”ðpä„ó*±q‚‹Ùuç°Â7 \yÙ÷Y1vBçlÓZèŸÃ,ª î¬jÀL1<‘øA%©hÅ Á[±`rq–Em°ócÿ“›ŒRq½¬¡vÖ„²±C3Û ¶ÕP+ÁÁÐæêƒ´ã"mõæi 5òP .<>Â,êuwV‹÷à²yZI´­ID07�—¦ÜnC†,¥ËΘZb¨³É MÁ�–è #àgÚ>°™™}NšWÌa|Ìj Œ"˜2öxœµY]oÛ6}ׯàü°''Æ :ÇÀêf[€¶ ’ À)‰²¹I¤GRNÒ_¿C‘Ô—é8‰2 hZñò’<çòÜ{™åŸþ\ßýu}I¶¦*WÉ2ü`4Ç�ŠJ²-Uš™‹YmгŸg |7Ü”lµ–_dÎJ²SrÇ”áL/n(Y.¼‹Tæ�ÖáO+m¨áÚðL Ñ©°I–”Z±‹Y°8_KV<ãL=[-Ôz{·ŠŽÃÇ; ïHVR­/fÛ÷¤Ðøs†ÿLÎ0£�ù!,þH–éjè&ÅB�ñr±³W¤7NNQQÃ4¡"'f˸"Ýé7'çû°�JŸ½Ÿ97¿CÓ’‘{n¶D F”¼'Øa®—cO,癑Šd²¬+1'ÜèÆÊâ¢íjIkìLÜÈ$þË÷[Àl<¹Ï»o´¬™ÿ~NÈÝ–ë7ü[�æ3)ÊÇsîãñk]]‡³E‰L`räç�J¤@\اHÛIÍ ß3Â…afÉ©…ábÓÞqÖAïv¨²ûÈ)+¤bö‘dˆœ�T<£eo& ô¬FPNžóg¹ùÌÿa%ßJ™Ç`L€yäçfœ•­E¤àÆ r\N€®�Us‡#}àט‰Ý�…d¾r°Ä4àþ ß©Š"æF&@bté•$')Õ8!««º¤M°m›y'PuÅL@È(þ`af{+RpŒ0ä¢Ñ�‚+m‚\�èVJüR„;R…išeRX\A†Q0-xàŽÐ<‡þÁœ« Dp#Ë<ÜŠfF]F³m#`)+¥Ø@Âä$!i/÷W+{1†ˆ;ŠhI#Ê>¶#:p‚¼Œ•@R)úh}ØLK3yÙ³F•º4a1hòD»†Ç0yˆ7Lï¤ÐÌî?a|€C7qøzÊÛ¶à�Ân„–=bˆ½àkNî·áǵ ÉZíùÂa/Çy2!“Ê^žt~hj­‡CÀ\ÕT8ƒ¹%>CT¨j]º/Ó¿�É™ÎOjÀ²f®iAöÑkZ ¥06¦ÎÅ!N×½«ˆÆÑßj¬W;›síp#ÀÔ< ç]¾Od¤FBšÊ8/zÀíÊ8¬vR5î¡Å&!÷ø¹�â &þrǃ«L>n:8FUçó ­0-S᪠«ä›Q÷H��øºƒb7ºÝ¤õ—!#xœ–$o}@ Ã¥ˆñ:´˜ÀîØÑ!ÇC R"áÄæ/ü“¤æ¶j¸ ~¾=t4©%–U;ìÓ’˜t×ÅizÉrR¦ëõkÙì@dÑË5|›f¨ïï©ú%kí‚Y¯£:Uéý[STÇÃR¦»X6Ì}ím•’¶Wê×쉽u¡v€Ó4T"‡Ì\*%Uüªô &Ý”�ŸØEq„Y ý ‚3ž—p]eî¹SRšÖsÎéF äYYDÐ퇔ïJÛ,íu©q?oÂ*ž×´Œ’íà´(ø8d¦|E{tä!A¦š©½S­ƒ�Ň[�Úsô,ð‰í‡¼¸Ï_l»%6´dãWƒÛl+™(X™��¢XþÄ0ºáÞkC2šÖ�Nl¤M¿ÜÆ¢kÂôuó ïŽOj ŽúŒÕ,�±/Q�Å!UN »î]mJØgÞZ_·¡²D‡ì©ÙÎV»å‚U+W* »Œy³+©Hu¶Ò°Ùë·?*€ü ±(lIÙÜa�Z-¨ÜÃUSj7dEÑì¸5lÑ�XË™Õdº¡3| Á;­‘nùÔêå(¿—2¥å‹‚ÂMù?B#x~A€x¤�ß=2 {ÁëʉXä?ƒÛ” "-{�ZÚFªÅ“.q÷ÊtcUí[:~‰�Oyé:î3òêÕ½xÙP:~ú!.j`ôf e²ÚAÝ=Gýôáî{W›Ú{‹J‰*Å™-S{±|ÔgÁ÷OÛEó»ï–C0,º…FJ›Š²@:Ïõöy¢­ /_ +³†—q?§Eœ+Œœ¨«Sôšbé-€¼ú“õ¾$aØ� Ò2Ÿ”}Vê �ÿ? ùèCÀ"�kšÆ¼EZ’j3»ÛL1„øàM �¦ò`ö,ö5ÜèwU—Ú½¯áíÞfÒ".³C“2ð{¡3;ˆ¶K°[mdÊËA*"� Y«­ã4˜ê²©"ˆh AÉJ±C l¸ [Ï¥±î9&ÓÀ‰“åÿr¢×Ç7ՈΞ h$�Á{ÿì Ã� "7¯89oÊ>¹(äFí‘•À‰JÔT[n˜¦[&IKž|ÈgŸL”æì™áÕÆ¦ÅrÙ(ÁF.ôyÉÓ‚x¢Ë^Æ·\‚"<ðJfg†«–Di;%6Aq°§w�S¯ÂÂzayÇ?Ð﮵ÒU¡É>„Ñ—ÌãÇ4£�ˆ�ÌPÕD *šÓºÑÇ/fî��¡.!B383¯h°úI·‡[|?4©uF«íú1òõ¥Þ芽¸|ûüòWlp¤¢ˆ@µ;_-[qÖ @¡‘;j‰] ®Ó’¡RòX5Q{Õ�J2™ç(0m]òTØÈ4{¦ä•vˆ‘-IúÝQ´s ™ÃHŒ°>|üú>n‚‡ž¢€‚[O0žÅw'{ö/»~À¾c×?œt�£X©¯à›Qµsv»ÈKZ#ÊÃ{™�ñpòîÁÉ”NÞuê™O•{jZ�±ê“-Z@µí¨4RÃ9â¥OĶâ“5ãvºµ}Y Έ!Œ4•T•hS¬…uyp˜i•"ô~£’Ü9ѳ•M”"rѦ-¢¦ñ-ÆÃÅ-¨¤�bjT‰N/§ö–Àyøm½<òöØo¤pÓHxz°ßy ²sY°¿’‚áS "Wê€|Êèºm¹Ù‚™PÔB ¶p=AÖîc<ÿÓ ¡ Œò1Ú¦`ȉ™ìzƒ0F Ðh–c-C7ŽTÈ/’×]'|Ng�oCc—t öÛäc™ ZÉÃ9Ï„$p#²çàcn|ÿæÏ¿KÖóãSÝ…#-S[ë,Ñàǃ‡r¡‡ò�âÇu§8¶ùïûÌ/�xö ¤S¤#…ý¢*¹¬=ª|RÙ·ñÅù°£Kú;ÞlH5þÞ÷½^¤1iXßÊ…)KÀúÿØ^6Uô7zœC�  Pä¾ǨLÛÉ)œõg¢ÈHêp T¡']{<Åñä3šäc 3S2aªOyÆm 3<ˆÒ>Š×†t˜�ã<¢(ÖˆÏ1à´ ŸôT?ñ"t QàËýµõ{²ŽÍ-ûêð$<×CûÆ ¯} Ð�Žø±¾=õÒN.†W:"Ùc˜1´ŸÐ9›¬8xœ7âè`þµ`u§ÀdxÆžÂé^~-¢üè©%¹í\5—&3×jtÄi¦]Ñ…“?›|UôáÌuËâ¡ì;98¦Ï½6In»Q™)­}Y §A�zx˜U ŽoGÃýg[~W¹NÖç9t4nªÀM|£7:ŠŒ²j·ÙÞ/^˜„ÆïExº{×7T3ä¶xÞ_)O÷ò”®$>ÿ&ûüÖž‡n¢’îFj®ûTy{5 éÚdG¤‹OèË_s{ìGÂvx±•Ì\uMñ>†áÂeXŒf†LwT|ƒû½d|Ù7E8â°µm!ݼÁ‚Ý ñcë/“ÑÅâLaÿ4pŸ¤ñÕ¯îÚá�frp¿9Üx\?¡ÄoGf;Pö@oî‚£#�eûål¾Ãý–ô´¢ žž–_#­B,IxœÝZ[�Û¸~ׯ`H�]Øã™)‚mÇ@®Ø>$4ƒ6E±”D[ÜH¢–¤<ãüúžsHJ”åëdž $˼œûw.òü/ï~{{ûŸÏïYa«r‘ÌÃÁsøS ËYVpm„}uÑÚåôo3øÞJ[ŠÅ[uÿQå¢d…-Tnæ3÷}2Ÿùó©Ê7xÛõÂXn¥±23/ØølHæœÕ¼¯.Âòeøp±˜Ï8^s³­ÁÙXjXVrc^]ÏÙÒÀ� »Ý/Ø|Íõ¢Êa;~`¯Ø<]ôW¤ öŒv|uë·ÿÖ=ý4ŸÑmóYóx”lZhiaø{œ"ü›Â£}å§…(KÙ¼ô4?�ºLü‹—­ØÉ‚}�–q4k´j”¶RÕ¼dÿÎun˜+ÁÀ—¬"%îd.×�ýÊLŸ_¸Ëë#†;Ç8Ál)­a¶Ž³ 6UG�ÛâbQÏg¢ZLÓÍt´ÔЫÛJh™�`"—™UšÁ²–÷,æ‚ñ:?Jáš­] –L¬Em™••0.Ùàhm˜¬�…ˆa©2)ÜÞVr-ë˜d´ùÄš¶f¬j~wÊÚ ½s!_÷�2­“„´—©¶¶x˜5 ²¥ÒU°Þɦ:É�™åß„³YÎ-OØR«Š-OKÁ¶oj ò…ëð,i "D¾ƒãÆ¿Uµd.0�Þ0 º¿vòÞÔ`�×^ÆŒ[±Rà¼ô 'ý)i˜¨ñË/�5œæhèL•mU›Xa±ÓT­±¬VöÕ–ËHÃ'0PmýÑ‘pu­º` ¡+0¤¬3hÓŸvxj”N�0.ù>K¹¥¬…ʈ§[X]ª²Tw¨Ñ­àŸmÇ>k¸Ô@@Ñ,�ù‹q8“ͦ^ßr-0´�ã δS>Û¯î)U:º{4�Åœ1¼¼Ñ²âzPg5ü/Ø�Ìmñêâæù“ /€-â…_pÁK‚+3<7³>Ñ%ùàgù§¶Ê¾|ãÕçž:ü³y|Š( :f^#IИew…ÌŠ�5o´Y¦ª¦uN˜�䕜p7€�Ÿ¯¡”Cð×[ôó\,y[’#�ÿ^Sps >Æ3 †ïB+ n·§óàÄû!žçìc ?>¡À­A¶¦ó`PnÛêÒéõ|\¿PLœ¦[0ôTÒ+r†2JˆŽigŒ:m$¸VŠz.°GoNa±¾OW—W×ìr´òaº huŽho{µ}Z3§ÉÚiÙP Óœˆ›$¶ÄÁôÏM°YOTD©Vd-§�‰s¤ ò<įæÒPUB±ÐÍà&ÒÝ^(D4@¯³@Fää ª…ôV éx\mÏÒ˜¨¾ÀÆÇµôº“¸šÅ9Ö™~oœ@8ΰkHšú0 ^žÌ„èFLËUa§q‚¸Þa±’Éžò„r÷Ø}xDcÊœ#ö-þl!‘lN{[Î}bÂŽòF=­Å $[‹€G¾FÀàLHÝiÀ¸XÖê'x‰+ïCwÕsX~U‹³Äÿ­�ÊïD? §(À¦Í,Ô,H³ƒ�„AW‚0�q`XÚJ`0ÝxƆÖ¡è%P-fêÞ}î"Üɶ”¢„ ¶5èèå1Çùý?€à€ËÉ`Ç­*¿– mãï¤iJ>4ù9Zë SÞI ëã5¶/£t°�ô¸° *ů×”ðõfku`C°W¡ÒÌ0Ç“Ñ�•To¥x9ã›k¥OK9íEj°]DH‘»ŽÅÁžé*”˜X 5äÐÛŸyù ïPÞ×û�~Fþ€ ¨\$áïx €ŠËä®!€[9g€»ûî8œïºr•þñÃø©$|§T‚ÖË==÷[U/e.°úõgc¢â ›óóšïˆWÇè>=@zü­ýí�šú2Ä·¢X `*H0“°F蘷 +T™‡4ô÷çO°‰Ù–*”®»ûΣ\îW%¢:00ýúêŠ=»fÓ½ûŸt,…6=Úäº9ìÑ´Ð3mêwSA ´ ×/ \®4rUf¡¸tׄúAì¡;å+dÞçþ¹:ÿ§02oÁvÂöž�!qÄo;m:Ç_y4Jì�V ÙÙX*©t8ýP�™Úæè¨¯ö�5y(NVDÕØP!D¢¹s8ØÚn| ÌûÄ…nlÐ̓—'ƒb’€£ þ°†ŠcšÐ@í�5³cÅ'èêUDÂmà0�Gxhœ|HÕ冭E�Bï£BQÀço*…p2A¥�ÕMpF�ã“rL@F·M]É´’�¯£ÇÐe8ª¹vù^´|:ö¶Á%{±òëqp~|jþéË'†#OÆ>*vÅÃî@æÆ9˜»Š¯8ŽÙ‡NSø=Ã9ê’}G*ÀU�“ç~>Üû³+“Žá+0RlàÖî‚×~õ!I½›¦;Љ/;!�¡a…•˜á Á£•ž1g=[§ÔŸ.˜‚È,¨þh\yW(Hw£fï›{‹b�¯;‰�6 ¾#0”¾¹uá±-#nÓ+±¸7°]ÙâZÌîgÑää,ž÷4‹]ýî î„6qr&®%‡��±Tó�Àm •#øª%ñïZ¤“pÎÕq¾a ¡("ß편šÙ�|¼„R/6¶û­!_on|iÕŽ&Õÿï/Bþzµï!W§ÿ"„.¾\©ŽüäÊ¿Rqªïÿ¡:„aÒáÀø]Û8"±Ž~Eæ�ô¿+é|C7¶nTIa‹Ë\—– {'œŸW!šKœúYŠë´ß o5ÿÞB�õþ…¨ƒh§¼ŠÂ ~ÅÎ!R'1™j-N2wAT¤Œƒ†Å…Ž`Ï‹©áDÍ)g8OK¶n»·Þu­¥jÍÖ–­·_µÚÚuظg=ébÙmûÄ?I¼Jü0£^ zá�«Ã\ã¼w-B±­;*x Àì?é5ÝÌÿ¶wæ~1ü?GÀí÷ Ø ûëØˆ&�$æxœ­XmoÛ6þ®_qó‡Áìh P`hƒÛ´,(2ì û@‰'‹(Ej$åÄûõ;в#9¶#£[:Ý[žçîxròÝû_—ހ•r%Û/dœ¾Jt ²‚‹îfR»|þÓ$&¹Nââ#*4LŠÿ�ÿ"2s'ž�ßiŽîКÛ$ªQ·.SÍ7>À›…uÌ ëDfßÁ Wd% +ñfrÂâòijÉ"‰™�u*ýS(�+rPA&™µ7“â-ä–þætëÈ}FJ§|¿ƒdÍÌb%KLb7�¤§,’8]À´±*×Áê"‰›HI\ùl°ÔÊ:Sgr£K`� ç�ÏËDoáù%Ào9‰ŤÜ@º‰ $NF!³æî2z®`äµV™Z�a®@ã…Š>²m`m€ ƒ™“›Ë(äÅÅz‹Siço'¾H6�ÇÜ_~Èsri×5ùv¦þn@O© è³À�ÏÜÉ?�½~Ø]Ï$¿ ë ºÚã9k‚“®D&PÑ S|ºò¸{¢f^_˜:;ƆBèò<�{6#õCÚm†½gâ‰4TJFµÁÏÚi[®\@Á÷†½ÈLS^‚© ï™!mj®¡x2«x++zµË…­z¥{8ø™�ïýìˆ#2lJ¹Qõ P"`¬!áÇÜàñà•G*ÙMޭ٭볛ߛA‰4M ÚŠ†)´[¤º 1´�T´QðšÉÁ�¿Õ?ápèL¯ÛŸ�¾Þ7rvFàY{$~ÿo­Ýõ—V÷aSa�´yµ}åH~šíž7öØsSŽ9«¥»:E»š÷ô_X­úÚ»ÄÇ©‚Ê ™X­öXMäÝuIÙ¹?L‰ÿúCácOð×¾àãNð‚�ûà¿é©~×Ñ’CfÀ™c´#ý.\ÑcbùÜ“}6hùÁ¨ÃàËn�ÃW¥¬Œ®+¡V q�’¦¢¡¸œ#¿Ž VA§}”ûÕ+eÙWp:dËÌJø‰0ím ÐB6ÚHeŠæ÷ÀjhtGª—Ên%´®‡ ÔŸ –�v®ó9Çu8zK%zèoçŽvJH› x¥�Am‰–h×ZÀQé’ànÖZ\ĆƜ.ˆÙ±vÌÐí¼­úh»A¯gnû{BþfïþjomØær¼O?a+ðáÑôlÈ&;(6•¦["±íºO?øÅ›¢ýØÚ�‡ùòóˆ/?�TÞ™èV÷Öõqø˜¬ vüm/T¢TæÍ{ÜšÉcõÝ}¡BZšÅJÝùÍîi �]“‘à|ßE³ïÿ8¦¯Ó;ÉEgúßzÑÞÁ¿›ø!zØuŸ€Yÿö]Kµ’´Ä´§Â´¿ xYÏóA‡¶ò?ÃÐ6ˆ¯“·¿²Äáçœÿjç"|Š %”_xœµYmoÛ6þ®_Áú%ŽÑ¶Â1�%ÎÀiƒ¶°�”t²ˆP¤FRN²_¿£H½;±ÜÈ€á—ÓÝ‘¼ç^éå/7_¯üó°&©Éø*XV@cüÈÀP¥Ti0—³Â$óßf ¤f8¬þŠröÄ&€ª{ö ñ½Œ�“\É”a — Ç,^k(ã»ÆÇ•6Ô0mX¤?“±ÚP,XR"h—³7„ÎoP³baa˜³ÕrA횟ÞÚuGWú„9‰8Õúr–^�DãkŽ? ê‹Pâ-eŸ«]¿�e¸êjq;¥ür‘Û5Väè\ $n1îqÌvÕ&2=¿˜9‰«Pd‘‘ÊÚ„‰-1iW ‘IIS~�3" 1 îB&dÆ(ÿàwuV=È%ÓZŠŠN¨ˆ«GBª¬‘8'äGÊtМ™i\�Æs)øË¹?ÈO2<$<ŽͲN –Ó8ÉÒIRˆè�àpÔS�PÙ“Ë-35A‹¼Ïú,�aõÒ³ÿdæ¿eæL*ã‘ÔüÓÑR;Dc�‰#£¥ƒg%Ó;q ð+5$aÆ@ìÈvKƒp¹Ø "åÛzuCsN#h#Xk¹Êñ¤ÏöÐc›LÌ=¨G•½ÀäI°Ö;ijyJrD !uÒ\jf؈Ž(§ wg¬µÀ¹¢˜ Îh£2YÕ °Œ¤Än’P��ž«Jƒ=¸Ìg¡ŸŽ/Eö5Ô v¥ïꑘô¤¦f |ˆ²„:˜°d‹s,>LØ¢x$ Ì[>èÚŠH¡=Zö †àäp-!IXÄ0waö¶Ôäfï*Ë쥿Α“—FC6rïáuµ¾m\œ: ~‹Óq…ÛŽ‘ªì¢Ë<‚&bŠ4�ì4 9�'fÒ²¬(ùDp¯ÃpF,›µ©FùGI”Ì|%imþ‹e©‹�ßÙQÅìªÚ øÂ ýfãûºW¿ÌwM�Žûl¢Œ‰`èêúDÍR?šŽÇÜá‰àöÚ÷TŠ2=ìùˆbç„*E_¬ª~ˬ›Äe-yë©¡ÙÈí†=g©=˜td&ž»ª÷Œ‹r;ç5GM9^”EõPÐu»ZJr E, ï*~JAøi L.oÚj8¶ØwÿàBË,ëBëÅvåFº~ ·¼D¢zbzòF¹|¿V ýoüìÒšñ¾î!äw"±£Fi®È1ÒQ c¡ŠÈÊ÷ÎS€ÇUѾº»îÕØ?JPWÒŽKö+ê ì|¡hR'4Ök I)kæÔfÝ–¢fé2�ë9çÌùM+Ç×kÖ=”½±òãßäóïízìà{»žxâµ ÷Þö±¸ÀI3†­—¶üË쀷)li»Ÿ¯üa0Ä9€°ï«sÀ`à8Íå†@Á騫#Ç=ñÅQ¥th{w£œ8†c¯ŒbÐÖa}mä'#ë�帺»KÀ,O‚qã[ud[áú¦0"žR¥$ÂRË!«í#xÕͲíFBÒ™—®æýMLÿººž_Þq¿Wéú¬Ž �…ÿocáþGù7⼈Œ$˜¶xœÅYëoÛ6ÿ®¿â¦ƒÓÙq·¢ÀÐ: 7Ù 4mÐuØãË@I”ÅE"]’rê}Øß¾ãK�DQŠ=T ±%òxüïÉóæ‹—o·ï½:‡R×UmÂ%9~ÕTÈJ"Õgq£‹ÕwñÇ5ÓM¾§œJR±?iþšqJä%ûHóK‘Ój³v$ÑfíY¥"?Æß$JÍ”f™z“,�6ÚदgñeœlÖÄðþvÎG›=dQê,.ŸB¡ðo…¯YdH4À¶I§Ù¥¸­]´Yï ã¤/ T–j³`E‹‚fZAmVBÁ´F - 'šœFn}ÎZ­VOcÇò8`“H,ˆôÜ$ÝKª(ÇíHØj×­Ž&�=û,ê›R(†p‹œeZÈÈãûåQjlåkøíQ1žUMNp!kRUGÈñl%KƒIž‹<€À'…¿àÍâñ®;ñŒNÞZÙP"I!“”7L—~й«jè£V#ïKêá3#=žr/⇆(¶ªØ5­X)DŽ{™5Eÿp" h5nŠ8*®¤LBg*9V‘´¢8-E³+ ™‡·ȉeÌ(ÅcŒ@⥥ÊÄ�HFxFaO$š»¦Rµ|�Žæª%ñ¼–UtÂWK´Åò†TÊNByÜ ÜIá) Z�€ôËÏ �–Q»™'ÝËPÎ÷Ⱦ�«7µ},á—"W=EmÐ0ß%?R<ÊJ t¿µŠ0 ”’gñ;ºC@ e<5‘êK¯ø¸³‚Û>°",¥¿ëhÆ|ÜÚñ¹xÄ9�_š d\£s»µ\;3°Ñ¨|’Lìܺøï{)öTjF•E'Wí€Ù¹=1aÙ˜”ù–ÉFç>¤ýþ#’¶´§/ƒã™ÆIÿÍAñ1ÑÀÿwTíÑ"içû–ÐÌ®çÿ ®ãÄ|Ž�3ãP4<›ÔÓ—Öâ¤}ƒwîÂbó3DT+Òª bÿ<²›í‚×|8ß4õÛTQy°Ç¤âäÖÀb$I©Q`ÊìSΈ¹Ÿ,æaŠ˜Â7 ¶)CQdCm}ÐëPÆ}jqªiWºrâ¿) ûœ'�bòv>yw ¬+ů²…Ä b¸Ä®ü=ÝÜ<ÞÕ~Rèâ KjÉí3=F£WÂç�á<ž=Ñm½‡7PÔ…4ƒÜ_ÃüÆBbµ*1VÇÓgNŸ|€6îž»°k.'ÝÛ¸gêFò¶É¯K{#«9œ¥O¶¦Ÿ¢æ’ËÝò[Á¯Hhh´B.¼²ræD³1^�Ì%ÒØM<¾ç~>)à-±z][²¬+‘ÌåÉÙó¤q:{OÆï�ÐåZŠžÔÆ¶E_E¢/rhú̦²¶‰÷û÷Šä |õ¡úù;rãBïb‘Ó‚4•> }Ê+ÜS >¤n·›KT߯‰»~Î]1¯Ü”ÕÈPg9½q}I€Ÿ»›“jÛit(&†MõŽæ®®Mê»æâ†ÃNŠfÏø*z •ïžå9ÍŸGÐpGã§ ´S’]›f©EKäŽ{Z âÞI¯ù5×AÛXú`wùÂ<*¦V¢XåôàÛ¦bs+ׇâe$Œcð¨Õ…ǵ6„u5ŠoÔ�šl¾øçº|q¯ß7r鳆‚Ç·�{mH‡ì×6>ƒÇ¡w;£ÛWqÛ—-C †'ŽZ³Æ�T÷©«Ÿuç3ÛñK“(>ÆIÿíS XôÝÚ^н„Ö•k—’>±¿*4Uù‘%ø#ñî¾O36à<ÊPíÍOL˜$èàøÚJxí=Z»Ÿ§þ ²— ^^�'ºB;¥xœÅ[íoÛ6ÿ®¿‚çáÖ°“¶Ã€CãÈÚtÐvAlw8Üh‰²¹H¢&Jq½ûÛïy!)Ê–ÓîN�¦¶Ä·Ÿwòy<ÿËëŸ^ÝþãúJ¬Û²X$sÿ¡d¥j¥Hײ±ª}9éÚ|ö·É9¼ou[¨Å÷ªR�,ôï*{«+%›w&SÅüœ[“ù¹›ei²-Îùla[ÙjÛêÔ¾‡FC·d.E%Kõr2Þi²˜ŸKœñùA ДÌk‘ÒÚ—“õ·"·ðo�-ŒN¡ÓÌ|yp¦%,Fýçç5ιˆ‘‹‚ºŠF­e­6•(q/|–ð˜Lß{$¥�};ái.ã8´05 ³üU¥­PU*kÛ²UVH±Úƒ�¸…O¾ûîT˜:¢ÚTV @e*øh O£DݨL§­iìTäºmaŽåVèflõ½*¶ Ý(½ZcS¡€fÂþÖI˜ïLˆ[¡­h×JÀJ"5]kÙ´¸(ïh‰�U†ã*Ó”²(¶"m”Äé7º]Ól<ð¬ŠÒo>華¢zBÕn¡kØLH».`¢vݘnµ†}gÀÕF/;)òÆ”4¿úcTÕjYˆ\–·y‹>aTOܲS‡åÉRW¦Ô# µÑÖšjïýJ–¥ìßàŠkÑÕ½-+Ù�˜È0ô”#‘‰wa»b®Ê 0²]O«ù¹*°q]­hC¥’U²ßµì¸ok¨—ÐÀóý¨ã/ÅJœ”Ý) Ý¡vi€ÿ�UyWÀ4¦VM«QϳWFå¹N5×zÎ�HÑÊe¡P((Y‰‚;M(]•É&ªiH÷à´„aÖk'Q¨žÝË¢SöÔÓúµº2¦ÊÛSúµ¶€5q‡97Êꬓ…Ýyÿ†4`çåk-W•¡åGÒF!Ä ûwî5( ¿ûiiUs/qÄ�Un<�•š&³b³ÖétÎl໲zÝ›‚ЦE—á7T^:`~¼ÚϲÑHæ@÷µ)2ÖÎL¶2aA!ð‘V,;]´¤€í L)…—ñ^©V`*!‘î#âU1{–ÛÙ^Ós¼§E&‹ëðW€‰¾ÁÓ?J~6‹y›¹ÓôŸœú«ñngñYb²s²�ýI—…¿¨Ý™Un:hGÇ÷|Ô#½ÃI ?¬‡ûˆp_2 `N˜û×ãö­QX¿³‰þ|wìݼïÊ¡|ì¼%}�“î°ê•£J¶y´åó± °¾Ý¡xÂg�°�ëpiD{è‡=áÀwt°Ñ¡ˆáÆ/ù1‹·™÷fd•�‹ÜßiL¢Û�}¬¾Í«b�]�ŽðÍ€{s5†ë ot ~?ÅÆ.‡/™)�Ž)ÜøL·?#” ­pàjÀ&C(öxè‚ÚÇ0¯zÞÇû‹‹).í±ãñöÑT?¾îšì\~í#ÜÎfƒÎÇ…‰×nwù6kpÃzl8|@�„›ž}Ho¢›�£›ÄpÜžÄ×Lû B#Eö�n\ú›®ÉðÚkéµjfñ‘#‹»YÔÌê­¾S…^,<ŽA…³"ôøV›þÕ`†‡´xø<†¯›’I›rGyt!å�ÔCöNIÛ5ÎÁß%øþ;ìðlWÒ'ÈŽ�çñÜŒkoHB>6¨[u;jzM+‹GÆóSž[giþCÅ-Bfx‘} Sv|—åmµªdÑnÑí¼7}Ô$d]:†ß¡£{rðxª%‡ž�}½Ñ ¡�? j8¦¸Ãúðù“Gõ/pzñy1xðø ^¼DÌ�yÎ I<:dE)½‡NX�NJtf=%9K6Q„kSÝû¿ÇB1{”€·.§{0¥{t«:Ì4NöS�ŸÓ0«ßßþ÷+:5Ú}óIÍ7ƒ ƒ#@ÔºBÂi�¿â¾KjûNGöé­ª�pô16Úr ‹)‡*sîo‹?ãúÙ¥ÏÝÝó;~ú/žãImöP™Ù¡Ò¬ÑãÜhY3íLˆkÊæ|²Òiê«AB"–†ÈSFtUw-×SÌÁZ-Øëâ·szþÏ“ü‚’Êöì”Ï!œs�“>‘:b¸i85c‘&äeV8Ü’û³¦\ÎB=ÿ-®ë %çát’†ûiʉ¡ œ2Õ]gM•N�x4§œdÆì§O}™®EBOyÅmªÝ‚i?Rtªbž; _ÛV¹Î(ßHy¿{Ìx ªªrÕ—E½–Ã2L­»c÷£}:B½½¥Ÿ=}*Nž‰#¥ùëé_99{’©\và$žž=ýöô¸ò@Õ0_ó™²°ÜÂÆ¹ZCÉt1‹�€xYׂ4+R àzB“”XuE÷ ‡œN]~g« P=*ı1ÿÏqõ[§.NŒô�ëÄÉP4§âŸôâã3ÿÌ�Ïýã×kUº¾ø×©/49*¹1-ðêÇÉ‚?Ç8û.wF“8X�›ˆüHUÇÝžÈSèÃâ)÷ų‡å5Ùë+^:áuº3^38,üÔîú~ã ’ƒW¨\ÒÀ5 Ã9 ^¿¢—;>oV&ûo£·Á²€Ã4zãé<Ìz[#N«Gª^~K¤�«ùªöò�gŽÖ?|®y; h«Ú …Y.˜ì¾ 8•X¸"†AFK}¤ŠˆaM ÚëßUcÄIï)°ˆ¶³ÌNX«I[nBÙ¸ •ÊŒòöj¯Ÿè7M�¼¥vD`T—Êå¦<½7ÂÂeÀH~†*XöS<(ËyÈq…%sé˜øiôò1Ü Ì°ºÛ ’ñˆÓ�,ÅבB®$â0™Cf¦’ùxÆÓÚ×ù†j@?ÿ4¢Sâ 5�‹y©6 >ÍÉZˆúT³¸Ò÷¾²s—Cž#n.ZreA‘Žƒ,çú£Wï0<ñrX@ˆÉ½03)]ëçˆÿ®f†"·¦¿i�3¾ÉJæ‚„phªí‰Ì‹nkb� Ó&&�0÷{©žP£üžÛü†õ5TºBZ#m±âÆÌW&º#Å0(Ó@óLÙ&PGŽ"à¨)' þ=mè’~û‚ÛöÇ¢ëC8¬ áBß)ñÅwh[Ð;\ô&¾ ¸0 ý?L;Œ&|ß FaíKÁ[>ËM�p4#&ä‚L2ˆIX—c•9j/L¥„�£Qtˆ$£ Å›.TÀ Úm­z'â*û\�»†m›U#Kè&Áׇzþ'©´Ê4™jö~UÁ¼Û{]ÈÕ*~ýÚ"*Ýôí¡äó‰ßê- î#o�Þù>nvqeÿ#‘¹ Ýú€åh;öƒŸ« âeeSS.CË®ÿrÕküõÃ$ª8r‚ dßv^Š)¸Ã‚áy‡Uƒr¥FCz/ÆÜ‹“Wðøu–×\XR%rc¬v}‘+l ‚�æÎÕsvy‰µ‚að¥©|•çpNqv£i.ðÀ3Åíû¾àbËÁùhêÜ‘¬V**S!kH'Ü`x¤«à�êa7XóJ~”™ƒšˆ*­ù m#ËDÀÁÆC8£RÙí—¦åeökga³>ÁDÝ};F]ßg„#ù(ú±Ê-üE× ”Îh° Y}<}Á8"ú€æ;à99-$wýÄw±Þó÷ž;܇y Pm…ûIÆ®³(·šq·�1ÜèÉ“,HÈõ64ˆ�Ï�몸óƒN4{)ƒ÷¸Ä^ �fB)8Â}Å@¼(Eû³K?,DpSŠßádTÕ|a@AÜŠ…©€µFGc̘¬h�Zå‚�›%K2Pá „b#+ ·Bº}éGß]ú?Š ‘ÙÁÀâ÷·ZVšÄzɉ«Æ8ꩺ²‚?n«ZÉG…/r‰C‘ŸÊJî ¼ž5½‡¬`‡132Y"÷à=½åê Ç�žào¿Òß…¸…ý 0ªIäµ ”FW‹“saO�ë–7²7 « 7ºFû“ ,xA‚ˆ/ pWæZ¨[ß8ĦAJ÷!ÔÑHÖh�çó.¥ÙWƒÀ1{(Tà 8ÂA[e´ä }P59Î笄ˆ³Q+ʶ¸ý“¤;ûÍ2PÞ½¦A.ÜåjÕ”ù;ÿ•`r?㵸é«ú‘“wK°Îþ™wDaïQ’dî!íèr$ÌV.| ¿\‚òFÒsa¶BXÐõê¸�7¢�ëºÎÉH�÷ÉM£xXÙik0*Ä–¤È%Qö«ö½YA/žÙ©QàÉÖ/´;JµáÊø—SBË`ÅtSâgÖçuùëªnqQÐ"XI‘[nÀ%1dpô!°qñf,0w1"�V o( Õ7žû«ýYkèSMÓú¬r�kc#Žlwˆ5FìzÖLµùÉ� OÀ ^0q>Xš®Mñˆ-OþW0 Œ­M¯Þ“-HÉÀBàÉ ·hã…mŽǯEñþ¬ äb!s¡ƒkÉ“]0crÚ.9}OÎ9KrÞ‡T ? Iaô”Šˆö±sm­ìt½H¶cW. Ÿr³Ær)á|õó�/EH-9lòáq_„œ”sn.šjaâÌgK÷[¶ {¦¹›¥N©ˆ°ÆÎª)feÃ#NKÁ«ø›çâ³ÓIûq²e´n–, Ð�æÂd×=èà÷›Ý`ÃQ“�Ìøz7áÕ÷Uöv` ɧXr#-Ovd6pr%D©þèð$¼æ°¾iˆFØTò–|¦]€nÂ�¡<ÌIviÍtŸNÌT—6ç‹Ïå�°sAÁÄnôØq“áÇ“ë#È>ùCõbYm“J#úÛÚ�gþX®œVð®¶yQDÒjŽ€4x­�¤q³³ÁéWÊ» cçÇrÇÀIå���ªžó9x\æ®ÍÐKÔ0 =îãYjª2A$EñR³X€§FÆT"ëÛZÝh—Èè(jE6�Ù¸ýk^uc¢ ½®{âs0ÂE᳉™G,�{aƒü(íÓvp·ŠzD_¹rDò& � ×Å>´ÃÃPCß#”ß ëK 5î�¸âÛ¶·ÀÎGNdÕhÇÖÌã9H»×¨0, ®ùq‡Çïãú@”¬Ä0Aµ„ß:n£H T>‚ȳ‚·½Tþm·¨·à«J/êr.RÖ�â•ÆD´×ò7B'9 �øÆwÇó(w2 �@:Š¢·b›ï‰WÔrUá{Ø:ŸªkGʰFì{[ƒk&îXŠAê½’xÌ¥ur…D4Ó·Ó×’¯ªšÚ&Æ8Êað„~rD³/¬…:ŽÅ*o‡Oj•È�¢êE “ß|Õu¦m=Ø#z+äj�¸·èÄsp]ÎëúFc$ÁÀR‘Õ,k”NŒ9‚[q´ óÉ[CYù‘ ü‰›w´dDœ›!˜Ò¸µ`HæX}ò�·õêmHuŽð¡ãáÓ9Ó)Õ>ßv3²û�ä¥w»(ÐQõßã -SºŒ�Uqáœë-H‰¨4x/!¿E¹Z;_ÒË¢šž£Ôòs¬pL?~¿ú÷3›°úe@úÿ¹erLŸŽŸ ]²}`\J¿:âò áW—ç�v8ødÑùèïÑ (Î9�'“áÚPY’\@§ÛR´[û7§ª´«G˜6zj¾±¾nHòrS éü½Í7Fü¶mث਷͎‹Z)±Hô`õ Ž85”OßÂ]µÖ~„òñ#§sVÁ>²Þ ®$¶Ä°³ŠØºŸiÿAå°¥ÊÇ5‘%y•ÿÖh }螈·=ä¦ÔQ5hØ1}1ìýÌ22Ô¿¦I¤`®Òz”O+…œbQN2$Ø·o¯³ª±eÏ’6à(‚)Åòuªku’€÷Ž)!öLq#Ô …ýx¾=Þ_UÛ0.®°Ý¯1]»1ªgnа’Rò/²lJÛJí.¯Å‹5íØ°þéÓˆ"Ú§OÓшÖ}E´´Âø5•ÔØ K¨å��¥vqw XÐWÀ¨”º+ÕU)ÀÏáà•Üàúƒ5*fg˼Š]‚…·½®žŽl䌯ƜñÕ„g|5l¯`·©,rЃn‹è:®¢gþ,l"+˜«@†2��S/âv°ƒÓõ˜cºžð˜®Ƶéû¿Õ %™=T‘Éá€tþ\¡òé‹…r– l–Jl^ ë2@üâFaç„ÔKŸü‚�`/h&öWFØ=”$~X.µ0»OÙŽ›ì =¹þYÛ'Œçy[¶ëæ¼wݰÓ¶á¿�±E!¨Gh0Ξ%+fý4{èÔo}ÌP7ê³ØFêµ\$œæbÚt.¨]7º‘b ÄïBÕÚùœ•ÏÑ<²»ñ¨õI}�ÝA*DÞùˆî¸¹S.êL™°vÔ£<”~¡GØYXÈ.~î. t.‡Š-¿ -  DB:û9žŠ�S€ïÈa³JÛ›H‹äþFE'$†£u PenVæ}¬$ܮҞä‹X-•÷£§L}GD‡/ýôj*¾5tÏÓlûÌÈ×q‚M½Y%1K: =wŸ¹ U }÷>¥;Im+‹ë:œù†Œ³›‚»`7KÞ¨‚8ß…¹KòéZõâò]ÓzH¹�¼ó•ŽŸìà»dwÜ÷Ú¿gyǵ/ܤ�îåþµ/êS)¤¶ú»¤ª\÷˜HÛç6$ÖÝyú¨ *ýgÏâKa9j‡ƒt–|v›5ò¬“á“u‡ê'�v/ôHLwÆ6;퉦ê™kQO5Œ¿ûÒ¿ØÃÃAÚÉý–�ë%£§l$�ˆ>ÔE>ö¨º-ã³N³¸sÆt{Q­gt]%:¬‘ÂÑ™zEý`³�w2{éàpR‡‰ÖñB‡£'—9Kt¸P"N³U•ÊSšµ�úÒÌ®Ýû„/’N’W´˜Wö$bvŽOÏ]ñ**laØe¶õ±M†·‡V(¤3gG£»"¾ÿGßPe諒^Ú¾–Õˆ8×v©w]%®l]êóvÉAZ®µŸP‹C{?å€ÜCçO¯ï5ǵ?ßôžÐ¼‘…!—lý…‘T³ß«Ä»€ÍúñÙuË�ËÁ�ØwÉ @7Ô„C(œ¤å¼ðãöäêwÎÑ�gB’JŸÿ±{û:ãTOgÂtÑ|—îîJ9¶é6“É5Ï/”¬Iu�Màx%wÀ+×”'múÁkH˜ò4ÞÙ^¦¤�l´¢ ŒXÃ(„Ij柚9eF†fÃ�B¸ËÞÒýïÊÁM¸×Ànß[uXü�t8»3�ÀqF\Êžê %†’Jp®¢Ü¼ÿV…ê’5xt3›.¼ì[.l@:¢sÓœ®uÓÓ¶NñÚA;¶?î d]l>ÿ ÎÀ•=æ>¼‹[fîÎzûÅIÎL´¥æ4%‘Ô¯‡›Ö¢NÌ4¦MŸ^ƒË�<ˆ>ï~ö×áûC¾P&jZ~’±Kô†�%”Ä— –0à…©˜Ø¯Wv.ÖÆ¾RäY÷¾^„¾ýãuÃö“›ꄆMW|°ÔúˆÅxU©Mô�v Ûš·‹Z壸»ÇüYÛ|n@¥€;³©7€n/hüJÜúœDë7%·Úñµ*Ê~ÅüŸÿ R]µºÄo7‚ XñÊWßûï@†÷ØyV` ñ}\Ú,`íó1ÞÞÀ>^ûלBçð§.i.~ÙYJ|£dÉæ˜�‚ÿÖ ¨¹Y¿n#ß vÞŶij U\ÀÈÊFcÑ»Ûj&�ÄÌ{Ìûþ»qlUg7Wt–{°3‡á®ÊïX+¿E“0;?A¬]‡¦�{àr�+�*,Yå„CAjth_HqßoãÛeâ‹Yì;wð$þý¤ì«‹“K]oÉÔ³Â:æ„u¢²¯`2 Ÿfsеü*ß{–ó£ùϧ aO6ï ’ÌÚ«¼y K‹?çØt8©Ò5ß[q^N ”hÚ›Ï:2UÀ{lIX† —À@ú9  ¬¸â†Iñ‰×©×ð•áÖ ­.²`¤ëDÔÚó—y°ûÏÀ÷�.ÿ敃šÛʈ’[p ÇMkÁR8‡K¶Äö 6�¨š ÖÌVÊ8Ú?ƒÚ° ÎQgÐè õo¡Òm‰k‚PN›gÀT� 5-y†_…Z¡7¬¶P²ê˜¥…çÀ["¬ä}#.j§öÝÉ 3ºãÆmýØqÞä6aŒFü²‹îá`�ƒÂŘ”h¼t ÇÕ°4º�D C<ƒŒLAÙ ‰Ñƒ,·)O˜”;ô Æ ×|éÍ•[ÎÆø·ÌñÑ£’¿Tžƒóã 0—¥±hjcˆE�^ss ÿ"®Bã4b�ø]£-&}£1Øo%Ø`†p¥œ;¢I�”ãP!‰%xM˜'cÖeߪ40�že~,WyVbM)§ŽNoB¬Ü0*fŒ@ׄ h1D{­ƒþ‰cU`yqSñÎá +nŠN+»[tUõYd‰Ñ)Ðz£ÌW5Ycröh…Qª®µÂ(ºD³¶®‰"qJñ1\alí®¾S!Ÿ \XJª’}Íë3*õ`ã»zí.·ðžÁ�ðÑÿy:£éËQÕ‡"½5¼•ÓÃâ)Ãn1Y#V�;o(=VÔˆ¯’ç.Ü=“#ÚrŠó³]8‡´àtvË^UNЖg>YcO¤^ ÷ýö‡Á¡=_Rtg¨XA^1ÈXµÎFem^¨È�áˉ"ÿ}D� :'òâvè ­Æù/èp ¼Ó§)殎‚üe¿Ý{zñ!º¿Àí—ãVX.j8-‚¿ÔŸöÅA†Q3d:ØïÑ*-iób¯ùZu)GúÁÖLH¿KH¤Óv>-êP¨‘u¿ý(ìT칩\兩·dá”´¤wtøúX®½êâé™"ÏQ–»ñ‰ñto•?ó"~9†ò‡WÓQÎØ=Ê<3.èù“pýÊìÛt äŸuœ�Ç:US’6<±Ånæ) 'Š�Jø!燩t‡¼.‡Ëîiùî=Ôý1’wc¹ÖuRàY#éÜi�|}Ýô*/Ò·c`7‰ ïš]?œÑñëvÑ=%ZZô7å^ó1H‹u&eºuÑ"îˆ'Éè"jÆâAÑXôm‰Û€ÊÊËÇÓíÏžN„B�“ü¿ÓbØD·k|)ãNA¦ÇÐ,Ý@¾Éà‹m£ët�yZÿñ3¶uTy†ÚÉ[à¡Ÿ×ø&æ8^ Û½÷Ñø¦3~5¹øÚd‰Æáð`8‹3®4˜—GµI‡¿�ð½&‡ÉïP‚â¹øÉ;QWïe9{&“‰�ܨh<òÖ¦2Y’ígm¸ÚˆX_°]Vpx4æ¬ä¼<Ú<øtóë£ÉxÄÉáù–P·ÌCŸç8­bqε~y”=g©Fc1~Übé‚�ç\MŠ'Ó/ì%O·Œ�¦vlÇ'Üp7áÄP «Þ‹‚fè b÷v0Ù(Æ£j-Bü3ÄGóБ>É ÏEõÂGô'&¢âß<¯acxöFÆÙ¬µÎrkžÙu�2v­ ÅLÌE� 3Ë o‡¥u!˶ÈDœE,‘q]@i´�Âã*ƒVEYÕøŽ—Éj�£ÕYÅ…Ò§‘‹1ó°‰…>?"¸â»Ý�«$"6»1æÞTËŒ›n®ƒ ŒšdúŒüSÂb7^v9ÿ¼âÝ_ÆÂ=yØpö…ïç€ÇMþÁêµ3d�E<”¥&SûláºÊîÆâê˜B*F”ŽV×ÍŽMˆ[%bVp£Äáœ>Íaàh@óéb…nKçx±ãÍ€é˜ç€ã? “1³�LÖ†�¿š&ˆ l Eƒnn²£  ŠÉpºžû@"Zb,ËT$PÆ€L2 æ<×èå¦aåÓWy•ñ§�–D!†g‚ca;;b9Ìq£Œ\wýììŒ?cC©µçñ« Š'�ò:7ììôìùà`‚¦€«ØMO;l#9¨ž®’Ô›Ýʉ»gýÇóð¼<§KÌ…Ö¢œ1àqÖÁ!G`Š*®¸ÁB©f¶l"# ",Á8(–y]”l4ep‚vM­J²VIB0gaÙ�¤‡aÄ®¾Ö×I†)å6`?g}ÝŸ÷[ø—°òƒKH/ÿØ 7îþº!�Í|°¶žú;¼÷uè÷˜w¸¶ŠªË Ìg©TžØ�¦"”_@¹†Dª ý ¨ÔDë|¿]+5Tiv”¾^Ú°B�ˆÖÆâF¸¢ákÖªãL%¨9•˜Öh »Ö(XdGô¹¥éãéVœœ¸2/UÁó¾ ‰ÔBhx¬Þ€ÞCO„‘÷ÇkµŠgîqû¹ëÒ’äïE’ö$Å÷bò3ÞnÁó'ž' QjÂÉíõf¶¬(Y³¾†Ž·lJrñe®}ÔŠ=Ÿ»ÏÖ-\ ƒÒ(Ü3χ{²ÆjXc×€Ò‚çy/ w$vmt–ö‰ÄÂ7P’·2eŽ ®µÃ4ºÄÝFþÚOD�¶Æ»“»ÎI±~ŽHEËŠŽ1,í@ ŽÐ¶­¦ÑN£9Üê ˜)MiHñ—DýT̘æ‚cÝÚ�5ÝчŸöfÚëžúÆwòhÂÞ‰[ÈE&e2ÄÝ’J›31è�7uÈpiå3.0™.¯ †²Ä…v*$×KK¶°:-û'�ÄD!3.å èÛj kÓÕ>Ú¦ä‚ÍÄœÔÁ&Hx3$R±8‚óÞVV$O*îBQm¦GôÁU²ÂÕuvùˆ3qþÑÎ>’VÁƵ)ËØL õ× õŠ´eÆ�ô½¢î^4>1@¿7Ý­arîÛÓv !¤7ß‹gÉøÆˆnp¸KW´×´· >!Qg“�N�ó~ "pÏÐ1€Ã›X…I“Ånö¸q´°Z]Â{�¨‘öw¹«K�°�‚aZÝ�¡(ÄÆ*ECc&ËŽânEÌ2Ãux¤=^´¢ÕÖ/¡Ñì¦3²=æjÐèûs‹D<óEaÓ,ô¤ ŸbëhOš¨ñëä?$‡_ŽäÒ|ÀÃ6©Q·íqEÒ~”e½[’¾ÍCÚ#¸ó(£qfYm¾ƒ¸?ÚwÅÒ¹qý|ø~ƒ±ƒ D k™@)o`³ãK||¢PâÈÚÖr«]Ý'ía›xjw°½P·¶� c†0l2„s&óä' ë*M!6{"Ë>\‘¯%¹±¼‡�Âzn£Éö ¤£vâU&/gÔ€õ{MÛœ³\»®½ŒÓ €J[yì ƒ“”5´ÈDî+¬6ˆVëpö eÍëŸ�¹WɵÆõ|ð Ù/…«³!—ë.¶²ÿnIr02lòD¦›»®6Y};öêwŽx)ÊîNÞwrßKýÊôï¤ÞJEÊzÔz×›rb»�œ‹$\OT\Á»gpôà‘@²/&pè£ÁÚ½Wöq&v¦JؾøØo ‚;>M]Ý`o–Ü]éöœ=·€K·äp–üÍ{x§zýáüúß°µ)‹³ìÔÿ#xÿ”Âp¶Xs¥…yyÔ˜åñG'𹑦g?ŠJ(^ÈßEþVV‚«wu. ¶QõF(#…>=±³ÓGp^çwHþé™6ÜHmäB?g#ÁŒì”³Š—âåÑðø'çµX.åBŠÊ裳ÓŽ+=»‡Íd4Ѓ7lQp­_­¿gK ?Çð§R ˜q�çžÍ;v:?K‰Î� šzz²Aòg,zÎnyÑ�Ùg¹¼õ‹—úøû#;ü3|^¶•fÍêJ0Uo,Æ^ålY7øaÑ”•~îH6x�…[·JÀ{ô•)]–ÆÉ$K;��) ù6w^xé¼,Ü-œYLVI&@‹׋‰ÍÕäi‘g©`Ôëü+»&£ %œ[/ªØ:ZZ˜rFÿ²½°¹Ùü™¥|kÈ6ù l“Ãë7YVgi󕑃™G[»h¸Œ éË»Ñ >çž>+zôÿ�n�GfVªF^U¨”lîfœs¸·(<‚€š†l;l­©i0ø†Zx+Ÿ®’¸¨”û>ÔÚ­n'†y×TZCÆÀ¢o­v 4`ÝøCï¸wr1â‹2›Vª’¼&C �•q8‰¤ƒ9*ñ,;Sz”b£�P,†â(ØÀWm2 [q³ Å* B�=ÞXwŠÚ;«[ç¨ÄÁhB5ª­54„ kÔžÑO`ÝŠ¢k9^ʨÌ=‰3´ÔF¦�y$8éÝD\RXR“…ða-±{§A(µú„Ö0rÂÐŒ 5ØøQÊ>Ò¸ÅZH-õkˆ³—ÝV•håË(-Bàm44µk9&·|b¼„&y£wdM¾Ý\Þ«¹º¡ÒºÛcyzŽkeÑ5F;Bc©úëÈ¢™£Ü7D/>mŒ%¾Ã£«ÃZî¢SðÆÊZØ•¹·ô­àQ–¾Z/®o¾]ä3âÔŽ«±Í÷ƒÍ(˜h”úx»v€.§ÐAϯþn�¿ûÄ_ÉSŠÜ—ã•Áá»#]²ˆ9_>d/b%ƒ.H?û IØD=žßUôÆÌ¿È˜jø¯/3e5)²„m« / ‰¡i”䤘 Ù%«„jšÙÐ=h”•鉦IZK  P†Û‚ÚNˆ`À¬¨<¨šC˜ ‰t+ZEeA.ÆŒeI8Ò&¬_ò{n¯­¾$A3nÔê¦õ}ŠTh€ •–2䯯(2ÄíV’Ë)´,,ÑÉ�î›ø (?t4î©þÝEÂz¾ý%½Ðúøb#é;qLnØùq_rÊi(;¾¡:Ï“þzæ5…)œ};áÉ“/%—œÝ¤¥ñÙ±†#{8¯ŽŽÛ¨©¯·¬ß£ »øÀ“a�s,†S—·�N35lþrTøRª-± UE‡ÁXø oá;x ?×Ój€®ÿG áHÀãcô¡ÀdG¸‰.$ç͜楞1–’–¡áŒ…¤¬š0–¶ÆÎ!u(±ÎòÂU�Xž4¯Áa݃–Êee©ÝØ•1Oñæï–`ÅF•½ÎM4Ïê(Ÿ1÷}�Yc‘í†×b!’å¼6:ó]@ÙGœ™6ߺ¯@–£G…BYÛ¶Ý21E I˜¼Î�ãuütºÑ~ºJ§ý4Øj?Ù |º÷õâ’Mi5S°á³ŠÏý z�«Um˹=~¼B•Â>ïr4:¸`än#š:Å3Ǹ‚ëÞå®ÍœÑè¶M�‡qÓ›žñkÑ9Øúþ´p‰o}» È^�^çåÞóðsúÿ†…õ ²‹'.–R xœÅYmkë6þî_q÷KÒ° …í’º¾@¡¹w¤Ù`[N´kK™$§í¿ß‘dÙŽí:Í�Ê ´®|t$=Ïy•g?Ý~»YýýÇlužÍ£™ÿCI‚rª Ä["ÕW£B§“_GS×LgtþÈ8%rÁ^h² Í`'ÅŽJͨšM�H4›–ªÖ"y5Š™+M4SšÅê ª@ÙhF€“œ^�Ú’÷L/¨ÞŠd4ŸM‰Qý¹³£Zµ}F™ÄQêj´½„TáÏÿÕ¨!FñÎô/~;¯0[Ϻָ¤�1›îŒÖ9Üá�r<—à�[¡È½JØÞ/™«ÉåÈI_[PI¬©„=�µ�c Loñ_·“O‹ÇOå� 9yay‘Cƾӌm…H@ÈÈ /ïZâ’*-ªOzfŽ ¢Œo×ó›Åg¢!eÚL±ã>€Õ–©|–HçDðìõ¢<á�8ÄÓ×"ÿ¶VTî-:jˆ­–hÎ:»Ì¡Èq)ˆ†äúvB1Íö×tƒÓcQpíAm*‚B!¤†ó.A�ÎFõFÐ4e1£\Cµ)ÕC�C¨¦FÉ¥ÑÎâTÏ; ã·ÕšEQ" kÃ¥ƒÓäØ{ç„Áÿ ÕCDP?å ¤Z;‚TŠÜŠ'DdçnOåkSºŽ?)1Î* cPÂÄ+3öoA2UFÁŽQÚÑ ô¿—òÀ4£öþo�Âm gêT{„XMÖ…gLA 8)ž·ñ6£c`8`Ä dÌ4ùN¹ãÖ­±ù¯F¤ÜüØïöD2³ªòiÌ›t%醟îZú OÕ»½÷^Ûî/’m]�â™JŸ(ÍFÜðŸ»]5ŒÓ/õÃí?¿]þŒðð”%”ÇÎ=0ôŸè,ó»6\ã�vXOk�M½]ˬߖÁ¸eI•­1K….)ð"§XÎ`-ƒUÍ˸2QÃ`,²"ç]k�Íu+²ÄÇ©:Æ�}Ö^ÄxÔ5å� +ÖEÞIiéN!Ù,Uödó¢ŸÃÒFL³ 0”“W£ª]úª:î ŽPùG±y¬êá!Ø`ÞÒ×&Í"ß#o s›�¹LL2‚í„M¢¦ög¦þÌԢɖ@Ý1D-axÞbdw‡k´@.þ1Õ׃„)qíïÉÐR°�béP2Mm…]ž8–ôeÃ;Aò.f0'Ë"Ö…,Ó­£—f‰Gòúᦕµ~¯G¢*9ØQ;¡ÝÒ}›k>бÐï°ßóe[kºeÕœz݆¢ziAU78§rv׫՚UYbL�%X¸�ºˆ¸ôÒåŠAoö2,¤VÕ—ýêZ81ÇdžÛKù&1™ùÚSž\'ÿJÓ¤]δ§ Æ&âbýåH•f{¸]¯ç©ª»@‹jluPÆõ®ˆìKi3„½S°z£ª¦RâSB7’ºÄ’âC"ò a÷¼—‡w§-¿øéÝÙíWçd£Ë"‰–þ£9ü)™¦$ÛP©˜>�½š/à¹æº`É^1*/ùG–_ŠœË…}-n}*òJû6Qšj®4ÏÔ ²¿&DKJ*Z²Óx8'ËE)ß�ì£eM²‚*uož“•‚ÿæðSú &[/Ó)l`f.5JKBRâ¤9[­X¦)q6Yq­YN´ 9Õô$²kr¾õJ5[1¯ˆE°¿¥yLDú7H&’Õ’)VÁÔË/FDBä¤îÈ)ùýIŠ&ú†üù$…Oæ$GäŽë ƒåÄ`¹¥2ÁùË~;&’V¹(ãv�ä 2ÉÓÁPå÷|;{zL®?òAx•³šÁG¥ “RHÕŸ®øº¤ßù'„¼3džÃJ‘L2Š{¼v¨ (YIaÿ�^U³•¤9a1CSG^ý ëÛ # ¬\iáÁ£Þ0.Iç ¸;¡[Ê š †¥hÖœæÏz&`=Ï8ÆÃÐ8ù%Î"™=qZeŒÔT‚çjçvr¼ŒnÒU;ÇɲR¬ œ…É�o~½Ú“a§œÛ³´‹ñLà69Ï4•c‚nò†ÊÚd³«l 8Ƚ€{Gt’ÝúÖ–n?/hðœVp˜Á³ tt â;¿Ã½»±³×^spö�ÈUÏbKp0Q­“†(”€è\¸G!@ÉV§ñ5["‡@Ù âR%z þ•ÉR玒>Yè—à '\_UÆIûÕnç( 7�ç6àèNõvd8ÌÛ¦|—*&·f#'ƒcÀ`JÊ$+ ¿þÌIÐõcØ  ƒúºU@þY°n¬çž½ö@�ŽF? ÂÏvŽûCx/ló8aOŒö¬eêv÷x 7Š:Gb]�Ÿhbðoáj ÕmžŒºŒ£�Ú7Þˆõþž|#�¾‚ŸcPa¼hgt˜M‚TºŒóÑQšÏ3ÉáªbŠ“ð÷Î×ÕJHǵ™�H§@v­>4�µ€êü·qŸl­ˆ*Ë1ç(yeàM�êææ¦ö¤'Ý\k7È(ûð>Ç<˜®úÜæª6qü‰j_ÜÕïÕðû5ðuUP4PÑ*gS úbyn+s4Lìú“êAŽ7š,?ŰXB¬hQìHº»¿â|I2˜ʧ:"«¦2e”k` ‰O+Wvä\Â5ZìNþƒëÖ¦¹¦]ˆÕK÷k<²t#-¼OɇmL¾ Ž5ÕtìR1ìΨÇ?\PÞÆÃj÷àñR8Ÿï”4UÊ)Ú׵؄˜™zú¨ ¢ Ý…ýwoûÝUc䕊YyS„ ²�)ïõxnÔÓ„K#·´hÀ« ù­ë?}ý¡ú%�Qέ;ØmH6ŒÌPFÎV´)ôQä%¹dE“35âodf¥÷[vNäÑK×®£P<ÂáVBIåR7Óu ×û•Ä£oÿÄýNÐA]f�ªº6TÛ¥œ“Yÿ̽F q½«�G˜c÷MpMïBÕϼÆ�ƒyWp%ªÁ\I·<3§;ßMS$¨2ìI‚8àï¾Ä ¶£2çÿ°ü XÛ0´Ù‘B˜œ›÷J¿¼õÿ lå(î:yûvº²CaA5©ØÝàVpÝUc•ßaص‰£!WôçýÙÍ3‘ß’�ý¹kÿ¸ócã¥ë^{U·>$ÆÉª‹$ÿ˜2FÃ}H&PŒ�Ì6Î1® ’Òì=¶íƒ¸ê0A΀} ØP½G–�:ì+ti†å lY+†'¢Ñu£íav™gÜ5ß•üðü+2øç«¢ÞÐ�HZ�["Õš$g‹Qél³d´z|O4ÍáØ÷ˆ÷½ðl½S\ÍÅjÞ^&åÄ€¹˜›u 6ôK¼W�¢Ïiæ‘âÎv´ÍÔËd<¡øv§Q¡b…•a躦ÆdÍVQ¯ìxr�ã±qòüS»ÕÞ@"¡÷ÜP¸c¤ÞÜQ®Y hŠÁvâã^O~¤ÅŒ uᲦÞ[»ý/–©OÉSds3AºkmäoÊJ¸ÐûÅG¿µ æËíÕÜxaÆj&œH•ÿ0)Ȭ�`o<úâB4eÎ ¤ –ÁîöÚ±­çæúÏXpÛ<´o©`ëª)™4€OFÂpŠs´v7átÌäÓØñìuܾ=­’îóy>Æð¦÷Ñoá Æ„ÉÄšqèç{Ð3Nž>7–€@\²²}ï ¥Qœü½\°2!QäÊÒ|!î˜UwScº˜Š¦Ê-à¼û$=¾‚ííuéÞõ=˜1Ž\–¶f„Ü,LeZo±QoúÜ€¿ú\†PóÞ·� ±ÒL6b.À�5èm?YöŽÚüÚ,/ »§ªñ;“hºm),Ü+ü…ýþ'O; Ä …•ħF1ÏUîxœ½×yDNe)8cì~ ð=Õ¥JX24-‹ìif©5rí—�ÏðRíZv4¹°ÐúD"PÑÞÇù¥È€lUÅŸˆÂ €|©�Ü�b©Ë x$ør ”/4—…, K0.·¼­*¡6e‘âkÀ�³31{˜M,NÏ>ðt)6úüÔ"íß}{á%áÙËoîñyþ—_ðêx|ÚË#£){˜¤ù"l+õš•…`U¹%Õ²¬ Àx‘‹º‚‡Y�ꥅ[£¡Íä•¥í�Õ@‹›z�Là7_F_ßÝ„ïÀÊ)¯R&ªª¬¬Hxh�OõXåðµžz+MçáLoË©’)àöAw¾ßüŠ›)¡èìL Ð~;Ç£·‰Žv…µÀö¤À¶ l—]²Š_Y*̺å[óG‘´¨Æ^³[|wÓó� Ï?óÏmá6çh¼+à-;7 ãk8FÌ;Àº’þÙÛncb ¾Gæ7¥’l9z-vgt©y6ì"¿�˜�'ðt™Õd¡xñÔü"äý¿DU²…XòZ ‚L™BYIšÒ¡�À1'Réa¬ë]>š‡Pw1³×\Œaè+ÁÕ<˞† Br&À«¥n0®_�gIÎil‰ø62ñF**Á ³‚¤eŽöÃx®”,‹±œûîf€Éðf? @—uo{Ïd¬ösÙF¤BZ7W.”¨Áú–…b¹,ê8zK†¥æ¥å`Ž±Î‰M¶‡u£™!ùCQR¦0Ä”°b$sb@]&EgiXúüØ`ƒ‰v¢ 0ÄF¼xœÍ\[�ܶ~ׯ`U´¶�¯ã"m¯pÖv` NŒÝEƒ¾´àHœÆ’8%¥OúÛ{.¤D]æâtÖ-Ç»#Š<<<—ï~ãËß½þéúîïÞˆu]WÉeøKÉþ*U-E¶–Ö©úeÚÔËù7é|^ëºPW?èJIûÞ䪸¼à�’Ë ÿêÂä;œè«+WËZ»Zgî[Ñ{ž%—RT²T/ÓèIzuy!ñÝçý%à÷är#²B:÷2]-–þ›Ã¯5¼’Á ÞZ—‹þë ˜–]^lp¢+/�°je•sÚT¢Ä¡ Èõ}X«tó¯S~çn­¯Õ›œ>áá0ßæSUí„…ñæî_�„ÏÄccgÂljXKÅn–k�«Ÿˆb$ËR×Â,a ø`c*§Ä½´Z. %j#L¥„± °U‰€õr�Õð{âž ñj¿ fñ‹Êj¡Qغ±•ÊÅb'ê5ÌÅïÀâEF/›*C‘…¬r±6EWvW¯uµrašß%‰uE¢m ™©o“ðq dF-—:Ó¨˜=È%˜Îé6*ÓðLÒ*�ƒÑ°Å�5y“)š¤œÑ@zKËUeè�E¥T΃Añ 7º.t½CåùåŸ&í™ãùÁÄek B >¦¦uŒx/™t·xœµYkoÛ6ý®_qg`…ÓÅq À–(Ú´Y 4XÐfX×a(‰²¹J¢BRqÝ_¿Ë‡dQ–± ¨-åò\’çÜéð§7\Ýý}ûf*Ï&AXP’àGN�xF„¤êrP©tôë`ŒïS�|`%â†}£É Oh7TÍx"ñý{ŽNÄ“…F}9‘Š(&‹å9¬�†AH  9½tÍNº/“pL4üéÊ”VlûMKˆ3"åå`v©Ä#|T£Ñ Ê9„DL²œ†cý.!ŒV¬Âq4�¡±dEÊ­éQ86�á¸Ôn'pÅ ©D+ ÐïÍ�€Tð�R¦MF¹Ù! vüIw3&!®¹|¬$MÐLQQ�,[@´pn�%Ø7çâb´5#*€´*bÅx‚¨úm�ÿQÏAÂ�U¶8 ì‚öPïd.GgÍ9¾ÛÄ^ª¿¾MSÄ‘›˜kÛõ²¶…­µÛ¦ËÇk¨jHí0õH�üÓñyl'¡÷@Ú7ÿî5� |¤ª– Š�ƒ"ÁeD- 2„ßXÌh�ÝÅ“"9€—š[­ˆc Å„¡GÀ£@`žï@¤g¸“m;hOÄã; �Åi„Ìx†ªˆÑ[ š0b†¯?üy+�€§ÆÜúÇuào‰&×ãÓ5Å�&B�…F°ëœ ^•ç}À%6žÜÈHD³C¤sœ #ELo‰@L>Ñg¿gb¶|•’yŒå’z|õ;Ü7ø,#­è«ÁÁdטº lfCKK`yAÆ %æXà +¦€Ê�4›–r*YR‘ `Ue_]M6ç]m±Oœ.R?ßZœ¡�ruìàÙ}ÅÕÖÞ„ÙX±/ÜDœjTº]Ö“U˜=þbj¶“ÀJa¨1š’*SGA�ˆõ:Ϊu³ð¸"3þós�Óáøòèú�ÃARŠ˜MÙÑi>ƒˆÄ_Aq?Í/瀑bJR°[M²•c –ƒÐ{Ø®¸LÜK“Âh‚r‡£%Õ ^©²R±Á”=`eûíìgvrÅ«¬œoFX)Å¡»sŒ}Ð1åŸfNIq@V 6m›r‚1Ø'#¨(kç‡ó$¥kWW~Êsm¯YúŠVnþ²&_á¾/$“#žŽšÆT‘(£:s_�*}É””º¿ÕíÒ[£6ŸƒµÖ�pëôZW¶ö,Û²X=N„?ʇ“Ö»w:/=àkï)I_vžO;'ÁöL×½ h#8wNŸ-JŽ�²i³ÞÙ¾ì^Ôº2­8)\ ÔGƒ‚Þú��ðõ�è„^ðbdò\LKåßäèÆè;†��mñÉW¬ X"êBMHoèz·M¦�šrdJkÿUN¸¹”5ÅO÷`E•Sa¢L¿é‰:Sµ\Ôé�Z\öÅÕ�Árõ~[¨\½ß'PšÕhCãe©Tr8Û9»_�Æö÷©ÿº[ýû%œl VYšÆŠ•5ÍÆ};6“5?÷Ç¥ïš }Ö x˜«­Þ ìôÛÂØ\ÙöWWÒ»èKýÉ7V—ÒîÀÞk Öb«ózý2}þüéÕ÷(¸â:~ò—?ÓíŽùñ¾}Q{_¥©òÞT×Ýa º.eMïæñHšš•�wA{�%t“Ì8i•K› e­±h óÎoùœ7ó{Y4p>K°i%³µ¨•-ŸÌ‚yFË]²ù*S~ÝÙxÌ�b\ûØR[$üÜ Ãò«?6åÔ†àŤñÆï(Ÿú”U…jžTïmSâ1Šnß‘µPóë·oÂ6‚$oÉ»{¿QNçàÝYX¹�‰`àÎT3q[79ˆ«UùŒ·?z¦yݹZ;Q�®/Wj&®�ùøG ¡Í¼pà•8C¦X½¶b!.);7 §ì={¶®–pª0J”ð”v©÷·o†{ˆ> ›½á@š·Ò€Ûè 5‰«Êüˆ¥‘ü·£Iooo†sÞÞÞ ý`V?è�ªÐkc†š¥øpm5Ø#쨕ãÕ»ë™øÿ§êì)* ½Çô7÷cS½ó�üíÜ¢Ž £%e"!WRWðLV  e3µ©ç¦*v<àÉ1CCëÀ ™|mr<cK:ŒVvC9©Ýøv­Á­Ê@ÙŽCúVéÕš&î冺©ð³ ÂCÍ‘|»V½Ä[‚œƒyb± ¹/Êyl<0B•àèüòI8cÊ¥"äR˜e»äm­6nZhZ,c³ÕŽ5ŒbFÊ !Ѱ“HÒ­dQ‡£Û×'ÄÍkÁ�àTd±•;PmàØá¼«6�9) nâ½ï\Þ‚¸M!‡ ±è§ÎÉÜÀ`áé‡&žý-†á¬Ñ”ï@Q­Å²b#¯v` VÑYƒõÊ -ðS©ê æƒSd{‰<3š½œùq+qïaÒE7DøpðbØN$òðÑ($ƒ“Aü‚Rþ ÿ*!,ñJbQ+c¹SŸ60­CX¢«a |˜™¢)+Èo�”H ±�Þæ¬Z‚16Òƒ`Áø¨õ|XÏZMàîç¾W­ÁV+ FX—ª(gÑ™á§=₯­  wò&œzæ1Ö\ ù‘dsƒ`ÈåÖ‘5=‹žå$Kh ã}å^À àÕ�Åì°G;)O"‡“gm|TàãŒ!í$CDVVA%wk�yh ¯‚ãZSŠ÷¯î~xõ]P¥)AÊIe£û“L @ejVDÐ9LêñE…~^ZÁœö5D(@÷Æ;™Ø0jB'B5 À_Ê4‹œj©sŠNðÌ‹ËÞ_£AB4—…©VP0°H4W4Ђ1�— æú<²¦:ßw„BDm$X<à5ؾ¤�|¯HÁÒ®š’’Ëã�CÇ��'!7%\Û£�9òIP¾:^ïC| %pC xœÈàÀ„4$68tÕ¡ß5øp¡àdn|ÔiQ j?M•�ÏrSÒå-1* m"ê‘ñ°eÆ]¡Âd>ð,@üÇ!7þÖ» ÍN£¡D*ìu)øùhÐzèfbTd°6èÁ Ð–ÐB$•©æ˜âµ_•5/PC�‰àè#‘Ø#Bl#õCð‚é}�›� ½ô-¹™°˜¨æès -lœO“ÖC(ÖpŸÇ7 Ú¾¨à²¸íŽp�RV°eËð4ÇLɼ%üsÚÑäÛúÈÁ¼nìBWóŸeíð(P…hV„£ç”ÂY@iiUÁØ@–B#+Á ,8mÓàówÊ–îß’mF‚ÞàEßìnMÄ›Å)óý>zöt’Á’î#¶È«�Í]uOÛ.ór: ŒÍú 2Ó%j:Á±™’´ªâA P_˜£‚pú8Ÿ$ÆP:¦�åù‘ª!<7ŽYoü%›d*¥8L‡díꌇ‘÷õÛ7)óœÆR¼õü¸á‹Mé3JÐeÛ´O�KôÓt–>Ÿ0LïJ[š×X„·ýBçì¦ÝcC¥CrÔX0/ÚS´¦óÉÖgc¥#vÖXº÷ÞÕŸY�í°ŒGË3 Õç{¥Ó$°‰ÃÜÏôëFŒUzüg”úüŽnr&”õº\ù¹½­mã¥1kq,CûpCžQRÄÍMܨ}ʇT‡Ÿ7�¸ŽbÌÃò+ð± ÀÐÜ,ç}¬>™nQ#·Ó ¹mÊ�Jï9—¾I™´9e-WÂÅRœuù»” ¢ãå猪x¨•™ƒ™¶\Ì)S$Æ“î´Î³ì ”4Ð*'NþŸòŒù�o+ÒŽü¸/¦‡{�cÀ±LǤËC˜éP;ð¬2vLÏtHü<$ßÄ÷GÎ*VˬL”ÓCBµ¢eé–.{>á,×tL{‹øaÐ�ÁoI=˜T¾ÐóecÑ3Z}>n:âçNœ#>xpÛŠ™Àé€< !_¤ªh乤‰ÉÇ逊<–æ]D?çïP±�?€šzÔçtÈ„>pxØÿÿ"~Eˆƒá!¢ÌSϘš1“]­}Mφôfþ¶öNþK¥þ¨ËW ¶1¯'ä‚«º¢ðR-y†&œa\Z< ºp߇MÍÍÞ‚t¥üE&´˜¾° `£Õ RØJívwñOÙ Œ€LQ|¶3ï@~v‹,P*üÓQ3è\dÁ!Ö²�"Í swœÍ+Ô¼ß!‰Á=-úª^®cŠþÌVf~OöìÉîÛ‰!oË­"Ï!Bê}ƉÎÓø� ë€2;éÊÞ„ØÝ+Pæú?“÷ªñ¤Wb¨mz·Mjò\›j¨:`NÍËq–ÔÁèQËP“‘*qP~U쯾ÒaRbT‚M%R¹ìÙ‡-8JFikº²IsÊ5,w�rG;¶�ý*ÈŒø†q,ì�ìË%Ä#¾B¹4®H�{7 O ­Û®K@Òâ y}BÙä2#ÿ±äU%Þ°‰„2 ˆT$÷R³-W^ü)ÿ‡&L•€P@½"Èé_M”¡Õ‰E©×‰Ýö•Yc>kãL«, V[ßHl; ³êÖmoVGa) …ôw�Ôܚ蜱áðм.ù�f·|ë^§%�ãi ²ü <6 =�Æðÿ›rXÙc;n• ,Á8zPt„øb›�|!�`Ù²Ì7µFOR¬ “F×±(ë"Uc¥�åÃGùUdr]–CºÝX3NÅ[ ºš ¦™_áh\Ü9¡+ãUPoÊÉÑmÖ»…ãZb•L©RuŽÂFÞÎsU¢³¨rÊ`0Ä©ä¢FäȇCœS˜Ý·]¬z$R!ÂpXuŒÇÝÝÍÜ�3„ñéãMÒ�L«Ÿ¡ù¤2ÌVȇ5¢B2Û‘Îþ2/þü=àHÎС�t?Q¿]Ì·ÿ4’Fõ�ÚÙ#²ˆ$õ€ùqöû O¨WàÙsv ØÙöüÜ`D*9`j— `†ÈÖà±�M {в ¼Ò�âe P&lûˆi1¿ðøœ½`?0ÂçÂ>ÚHûbØù9¼ß}€°#1.êrñ�…¡°vÂC�™Qƒ5I&Úì¼VN.ÄKøW¦å&{"a%/a–bÁœ£ÍÀE£#8Õ(祑`- á‚ýWHòL÷•gV&RL#ûôæþ㛟Y™Km>—µT]ÆÍ,é I3Êôé$q¸³µ?M3c¬m^âO=Rs™°ZZ¤(Ú�ho¼•!·UÂŽDØÒbg1ÁàDÖðº’pN8‹CÊEµÌ ](ˆ¯€§WéÊQ«å›LäŠJ¬Ê,ÃðàÖg[f“Œ>Ëò­* ½P+Ô‡Ü ƒ:r¬(˜Í C#¿º™#pª\8e"þÁ6<îBMY*òþô5É`âå“'ŒE |/b#å2oOô¶d; ¤¾Ÿ ,q|û ‘3t 9n‰Žþ¹:¾K½±4Žr»e3·¼šývqÎæá³²¥ëÛµ"1y‚ÅjŠuéÊÞ,�( Æ,Öj¿ø¶±Å)àÖ‚Ää£Çþöaf„Ø–ÁÚ\qv$-MS/¡( T]é)ˆ‡BVOo=ß• j`µèÊ Æ±|©³§I`£ 1Dšê1‰´uhL(Ò†Õ�E>Ùêv’&hÝÒ¬æûB]ÕK�å"õnVRdéP‰æÍ‡ë�¿ùÊÁt�â¢m_:,_b¢Ûaý´!¾Á …yýv\F[~¸6ñ5e?‚ {Ôü—èß?ù!ˆ}nÿ{–ÿðâü|~VLóé‹óþ½ŽOû3EçŸóæñƒ³b`ãë�¯ÁÁAø†Õ xþü�ÎÌVð¬yÐPXxß›" ^ »«I÷ËÜ}v`ý{²3\j7â“f±·'^šCæ÷�뺮À^¢ F;wŽ¿AH‡F¬?‡Ž�K(�‡ÜE2X 0QÓZ ”èý&QÖ‰Cø!„©y×YFí²fÍ•G0im€=Õ”)öaûr+®Eù––q5Í�^£[ØiïÞ©FÇø]Ô1Þ�'”PO]ÛÑ�/Øú‚�Y)¹h«ÿ—ÇÝ}æw�>sø6¢§û¼=UÁR¯! ݯˆ~MÃ0*cÿt7Ôgút7²ÏDzü &µ&"Hê.íÎÁû²]¡¨ÉE�³­¨uƒHNc¤ƒ5ë!k��;w5ÿèÓ±_àJT¤ÛL—Cþ¥ægTLì50‚Ǧö{Ä�Ú†»»2_àÐ*dEØRÙú[I�4?âÀn?y¹èvxìø Ä.iˆ�Ϊ ª�Òu ߇=^á–h\zw’Ê*êuú™T�ñI£*mâ15$o¶C@’ݺ?.é  ¹mz9Òi[}qŽoD5»£×þþ ßÕqB`¶5äÄ(tUf©3ax-�|5}pº�†Ð§CQ÷}]Dµ]SÁ ºÎ7È•U/6RE;•™r›0’6�ÈNšù£ ­x�éöfÔþnÊT»0£²’8·‡ÕQ;â*Z_Êj!�÷ƒEù†/uÄd�觨ú†À+øÃÀµ½0?�ó"Ô ´“* êEo˜öN‹ÍPBMïFÚ@¢'�[©„Ÿû¯Ä²¬R?ÖÁäTAºd„BWG"7å¦Î(•ô-²‡Mòú|WY¼BcäAæp¦Ç8-Æeqñ׊ÿñßþ34k‰Zu ¢Ê‘6•1:$•¦ÖNËG�6%ƒx�©"Lqo^˜¿@Qù?UNßâ­¾&ðMijõlÓü *“êõë“?ýûÉÕ;Dìr®×ñ›¿þo®!4ån|9ÇoçÚÞ$@i�¯TÚ³]§ñ2‚ê”ǘ÷ÏE·ÐÅÁ"@T]¸‡™ŒíÀäc¹ÕhLN¢`9פdU$Qý¹SÓ�)«ö—ÓÃPüÛfs!HxèÅð0$Ú=ó^D +œ’ùÒ„ÆC÷-“SpôÎ\¶ý— ;öí�M­Ë §ãh’§3ØBpå žõ�Û݃ž±Ÿm cgÆîmÆ5Ûï=$٥φ˜‹Ô�s¬�0Šœ©G©y†ß“#w·¨jcã(k‘‡†¶�½5µí€ P¬Ì‚g„R;|Š \�8ƒUqeåñV2UIô³ÑMA?ÑKyg�=0û~qÊmAô/ìmíŽ-é{þž+Sº °cæ§a>qÂ!У0BƒSÜtj³Û“h%Psm- TC8Bá¶c<0|ɱ9&0òncØ®+��nF«ýp”‹�� üÅF?an³¡ÚÚ™ 7ën YÈ\šô÷cö�’;e-í�yá“Ldƒm]C`UÓpÅw¥u+t[`Ü] ¡—ól kF35µ‹¥þæ@p¸Gád\ÃGëÇ\Ì[™#(ͯn‹aÒú%£)AÚEØpl«#²ØÔš.!ñRwÿ.‰½KC⋟†û!“Fe×N©X’ îØèf–ŒãIdR>€›`KkÕ8Ît€u™óETÓ¸ �?§³Ÿ;/>ý§‹å¿çÖ’½‘”´ÄÆœñ€¤¡Â^»ºÕ¸ÑdÍ\© yÑ©�Yƒ5Ö²òA.yöŠé §èW=Æ” ™SgC;Û|2†zÇ.$™3ÚF6ó§°Stià77j8¹Þ¿A�pKËÜx´÷—ðò<™ÿÆ=2|`�Íq;ƒ‘äîú™œö²£©ËàÏäÄw­�ð39}?�:±ëÇbš\#6¹¹ æ’gjìb¹�®¡!$×$ô§Ä¤/ÈÑ û‹Ïª$iNuPI6à>Öö5Úf¶£Þ1¬lÃêñJŽÒÏ÷ö£xº#ØD£ë|�×÷�o‰^öÅ’CìWVèÀ{n·XÀ ¬¬ÐÈ¢.kÕ8ýf”07ž,*‰䲘@züÍu5^Pƒ&¬Ð>³‘^Û¢ê¡iÄfš=ä™\0K“ÍÁË`u�<<ýhDøð¸ÞÜI÷.ëßXseÚoûqñû“’ïõGÌ#ñ Ê·;'i«%( Žh«Áˆu¸Ì@ÁµÑ½P!uI¶ìl£æG¶hÐú9ÄS *‘-TdmMÇ+])áð7ù&§ß.N'³Ù þ±�¿žNÿwxîò»$iŸI‚z¥çƒ—cЈÊÙßH B@Õ™Ö¯AP½½5*‹¿`å¥# ¬GÂtÎ {n“sìÊ�ÿ€lMÃÙ¶Ûñ‹ë]4ð'Ñ—çö§Wçæg]ÿÓzœxVZÁžUÙ|žÏrÒ¹28ð t ½K¯ªq€ÀŠˆIÝ7[ ¬³ e£4[o~˜%íl¹éGzæ`ô^¶`–÷¾E™©·�%ÎBõn’â"érzêli¿&"a÷ Ù±ööön§áå›\h±St“Õ„-ÑYpêøÂ›à˜Ö¦Y’®g¹Rÿ`ë¨O®#:¤¤Ú³Å™1’rˆÑ ž%F/5˜ª Ìt MA~Ý3Eaƒ¯¹Ñs3ŒI0ȳõevq�T�}¦Ü{†_ºSè�ø,çÏÿy³ËÜš�ð%€áLè¥.ˆ`Ībk%Ñ�vñá^97ÁßQYÍh´šôú�ºqá]q(ʲ GÕ,ª™bZ±£ë¨•ý&2+cê‰Fi,Vñ¹¡Ájg»�¢š¤Áš`bS‹ FÑ}‚¬�^,+e†#®Žs¡er5�ÔrëxɉÆÖÓ.ž6.é² DõŸÀµU`ã�¾YÕá¨â¨í3»U­�‡wp+Z?íc¤ÉJ¶µÄØF‹}Xmž-ÍhFm]ž‹ØéŠר —ìú©3c6×A˜1âZÂdCFžxèb‡t8R7ÅÁ*yIAZJ¶îÕ3Zœ“2à!tÔ‘7(îùü’BU�þ›“`²EÉ1ˆ³ÍôÄI<”Ìq-`žUm™*qï EÃZÙ€�ˆ�žqÒNâíFmEÿîeœà�*c ÿû׉׺d 8S“ öýù¬Fe°‚·¸³NËŽ·òþÊ5yrw·kòÇš·¹&yhV2ëe2d·Ðø¯òE™ƒv™Ëßœ2‹þÈàb°¬$gfŸž~¹;9þO¢)9z0êi È&‹@ èøî݃ãé»�?|÷=„ã½ãåÝ=ºûµQ¡ù9û°€†,¼[Œ<OÂ5sk6‘õz³&»Ç,ÌÓYµZ· � õ×¶Us:këóŒÿìZo‡ät’$´.}É;àô™\j¼¸ó:|П]Ï Ë±" å³™yŽ6j‚Î�j-Ù]®iÒb ‰¿Šßmfk•s‘‰ØöÉl¾AN^ááêÎÀ&Xñˆ`¬d52ÜfÃ&X pµ³œ°€Óýºiª8诞ꊟ‘Ì|bV̘Éöhãk™ó)íèHÂx•¯`Ë’2«ÚAûŸHã{q΋dÁîÏ ø1ù8ÈûÊú^Ÿ¼¤¼l»Z�y·Gd„‡hÛŽí¦Í(Lœfo³ËdL<ŒÑíÊ U#Ô!Iã7˜Ð¾û~n³˜ÓFé*v°MŽBéu*OTþ¯ìÔÍFNÁÖ�ÕU’ÎГO† ì>ùÙþþ†«e²Ö‰òLnü�è!ªHŽÕýŽÂß’iØWPhŽ iw:wÅÞþb–Ø©q ¸Ïó—`'S¬E3®›âY%ƶ8=©S –Û0¼j(‰ èÑî’Ž'{±Ô[ÏØ‰GG¹Gˆ®¬ÃØ-ʼd .£Ra‰tÖ§U&¹é� J&Ôa†¤ -|Sµ$à€¡ÄK±ò£Øè8=dg| ä9RŸÂÄê�™‹m–uÕ.–l`Ò¨Ö×ÀD�”I:EèÆ„÷À–6u.3N|îFyA«hgK‹´9è €œ-ûr´¨­èß-ª1úøù€¬æ÷ÖÚ-Ê2S±_i·p;‹ð;²»¤îã�êcÝv&\l œ�X¾F©³^Ç ‹_dб:‘WZi×V� ϲÎ�¯±ê;.mâ®ã ‹¡�+ÞN †á€ Ý·&;}¤±ï$e=×ê ‰ÄÈÞÓW§taB#x2Ê¿˜ÊÕ¥ÑÑ9ý¥ÉØÕuжƒÖØ1Ž­» (’µBŽCö¼ë%eÂÔõ§8¹† �:»äH8«ÈŒî­ý£µèã|ñ’`H3xì¤\x1´ÁZ]Ù~qªrµrøµ)¨•ÙOD-¤4Ìå×"o@+tÀ$½ðÓ‡ÏÚÇ6@ŠÏD;4¸ «<åmRð·Ë� ¥óS†÷56*å0èêè©<Ý?²Ï 5jÛûmR¤âô¼ØÛ³÷âwëUܼÿdŽÄ�“É»É;ü½^}zòìéçÃÏöó«§Ÿà%v™ºêиžë(Äe'·Sh ™aèóTÒ¤Ö$öríÄ–qC9%-³:ç`£Ô¶¨0ÕYŒ0 °‹˜gd“¹zo4L§XtE ½”£d½ 8¹¶n„ ž9ü‘V+¬g)Ñ-ñq L\pÇ™è˜Ä—'¨nxÉNÈöB¾<‹T8Éwy)‚м–Dé>ó�°*pÜ»ÿ¹Ÿºñ:SC¹2úç=UŽuWà1n me¿áEÌ}÷;L&Ÿ_+=gí�õÌË…ñAèÆ±o NpB]ª¿ÿô¥Î'^nÖ˜#¦ðäÂKZñ÷ŽÃV×Ê‘ ™„ÎàoejPœJ = Tâ6Õ¬Bñ $±à„ˆu.ž)ÎëHU+ V©)K@ABÐM�F±ë”1ô#G>®1ûqº$¡¦0:W’ضñˆ,¬ ®=U3¨Î‚€DÕ¹+©c`þ³OëÙÑx?"݈ŠÏ| iXbͧ»Ãh=õ•Þì’ì)®­+…ç±Ϩ‡âqYO™Õ“×HÒgSÓ½ˆ0�j”ꤧ³ªÁÄ[¯4XûºH6WÅM˜ƒ8(è,>ÀÏ¿‹ß�a 'ÍË+Hé1O S5­‹Cí«€%JXåÆÔ?w�:²‘õ[SkÁd°…Uwe9µA4[¥çöŸ¡#„nDñüw0azOyd·€ã—ኟ¨«¨ëŠTçeN^W*7)Ñ\Ÿ ò9£ 95Åb¡¸ôf5B ‹Ù3쪇�}%¥:¾N³øpM W9p¹ Wy ˜kªøÔ•1‚õ‰)³Cn‡CÕ×>–ë.ß]�qŒ‘x|T¶‹ÇºÀG5˜aˆŽàŽg+w{­Tt™L·ÿ�”Õ))/S&Ž+ŸrÆâÇftY¯ýÉ”¥Ne3ì3äÚqϬ&ƉȽ„FB )õÏß½ƒýýD(ùOãLJ¿¾Ãìª ~‰ßÄßɧÃ_?M�?G�Çã;q½�½¼±Úù.²|\�K¬Ø©=è�X‡KÞ£ ¶…Ã7˼ö1W»˜KÞ^¢S«dGZË>•†eµB ë=¤Ÿ±Úð þÊúÏ%çqôXx`ƒx�ÚŠC<;¨'±g„I¥¼gZh‘®¥›kšNÁÀÆó^�%gy‘S0û­mò�ÅRÇÆÀÂDÓ³¡¬z‘Š “¹w¦¹”Ä¥[7Avk~,6#rSd·éE3lZð�×1,ô_Ĭ\·"#ý÷™O¾Bй‘Ì{_íòÖÓô]Èõü'm *ïômÒ`…5 2t)`ÔØß©m*”E¢4|ø#OQÌŽÑò5Tâœ[f³�VÉç� ;¢aFÖãJ> ”N"`;_­@ꢧÄÒ£³ª­)SÅMÔ%š˜¨‹ÏóŠ2 ØG¦S‚fXà§ÚÕZ/=\ÛB¡£>3Ò\ÑÉé¢ãt±ZQwÍY8î“�‹G:Y wÀQrîE¢ÚÅBtXQž-®3ñqÒštílŽ5çh¶d †ACä ™ Nªºxv‘5cÁôOD¼aC(IŒÍ>ˆN2Õ‘²z®3r�ˆÉ蚣mõXò“·@¿ïñï?åßþõS ¢ó ƒO_žfïóÃ_O@jzO|†¯%‹jØYψS¦Î¤FY43,¡ÃnHåT'ázÅéJD¢Õ2%]‡ò‡üéÿÆþôû:wŸƒ½Gu*+}Y€l&z„ ¬g¥šÆ¡a";kÐçYïL$é‡d†É¢¢É¸i£ŽG:P¼À’K¤C„ä lÎ+1]ö!)¨�ûl €n¢™Ë`uޫЗ€•ŽÈ–ÄAÎA“€�rê nSdþ¶cZ¾¶q`1Y)x°‘* rR€’¤fh^ˆåå÷IÑFk¸·HêE& +Z�tY-¸Jš²Á‰eêÔ]<ˆnÆÂ �b`·dzïR‹•½^ãHMfxB.>D)¨™õ …<>·}_Ü’¨Ãgì�÷ûkÑœÆ|zlé4ö ­ÕØ¡îÐdôä ß=©h�f9Ñ0GàYŒQp¬Ì¦èæ2> ¥Ù¢B69ߘÝúý'­{î>Ž¿Š�/qÑŸÑ›~,&’VÄ�öx2+9ÅÒﳡw`qôgšô'’Ú…�6£V´‚Fã  Ë ¸9Õ˜Ü –Óx®ú¬ ûxù;BuOÒô56�PÞõÏWÌ;‘]üà÷¢¶*À ÿPwÆ�Ri…G¬þÊ5-<~¤?¡ÊÊÁÜ.†·Å§ ³ÖÅfeº½ZuÔ:1Œ{¹»j䥨�±JÌ™]œºŸ‘œ]=È?v¦A]³ø+ÍrÊyý d¶�_°g‘)'D"‘VÇ_u®ûP�Ï›×JY·u�KB§r?é ø!7 À=5È©u¢Û À�¸Ö:*pÐûa’ÞµñÔcëºWɆN½æ%^° ÛÏʶ�°�¤¥-Ä ¥oÒ|ÖØæÑÇ÷msiyyð»Èm•è¼ð¿œ�ØÓ”í{gxeÃ…Þ(dÔ¨pfkgDà•—'¿öFUEtÃt×wù ¿zÜ}ÅëI'±¬¦öݨêêX¿Cêõü2À8pPiÿpylgrtŸÈüŒJ;?p@Ê@)�&µ!GSa (è‚)³‰`©ä°Øz¶ÜHÎâëT Ô;Ì5HðFaôZõeG‹£ÈÛ×_�¶á´j^ËÑT½­¡.¶”puz-±ç“àÆÂ§Z!W8‘v�Sây¨Ü$ª{ò¿�©(ôù\Ž–1“œ‡†º`2æ&cˆÌðK¾hWotÿ«*=LèÜ××ùäƒq8wûZ¢÷Ð`”Æþ ¡,…øðSnÜ”˜ü„º#ucJœ®&nç­iêuyÒùb#Ÿï›}|×éMûO§a¬yîØn`‚_Ž;Úsqßyتi¿œt!è�#‘&N‡Í[lr¢æ!g®’V&¾úG~Xh`#{9É:OGñˆ@ˆÚåÎ%“Ê—ñš‹S8ôC èUæ :åGg%ê§Y:˜6æ:ùV‡òò¸–±€arõ1 œJkŽpZqÍ÷+ȈÙ%hkÔ[í"©)Gž Õfù›i"¯=K[‚À)4Ñ3î(ŠMªJļƄuÕ®9ÛMÇ, Ï%°ä§ûS€…„Ÿyœ¦œ·z×ýÀÓÅ’/ºØ¦[:Oܘzé�y% ó%� |ìÆº‚xpý6šæ„Ìáê&ŒVWHï¶eãÕõÍÈ*œÿ�ôMgûþ*§§Ã1p£Oçé2¬wž5© ,´¬^ØS)#ç­®ZIZ% �:a$£ìРǪ…Ñ.½p”ZíÔ ÕB½£F꡸ŠA�ZK|4hè!¹PÐ5 º(³ú€Q:h”pˆ…%¤X°Š¤36¨*Ô\oaÃò”¸@̱n±M¡ÑQ:âKŠŽÆ¨À{“ý‘t`sÖþTƒÿPƒ=Ívxsúåv;4Ûh”j»Ÿfk¥99ñÏH«åÄëQz-XæcôÕ°ºÊ\Œ"�¤Gš)ttdć™Q,BênÔ×wƒ:“áyCJo4Fë%¥wLƒÁ¢j^êÓP›A÷™=ÒÎvÞ¼‰ô†ƒŠÂ‚Ï5›u¸CÞœiÿv}×&¹Ô³\;sšÆ[ºÖF`ÒÁĉŸàY?x|v…Á`¾ïŒÚ†+ š´#L®Røï,ØjQ'«H‡$¼87kæ%—N™Ô¨W3ØS‹DIˆÖ,K÷޾Ž1…�—Ä%æµÝ¹÷Ù; ‡ÁZn�Äç¹j‰MΊ •T·�ÊUƒâ«v…�÷2·"d+é,Q¼ˆ[dÙ\•_ê¤cûãh®\ ¿T’í7Ô2¸öÛZYQÅß;Ûä=8±œÏµ¢le³¶±éÀÆû)=é rÏfk•ßÅÔDZR™/œS:€¤ÿÛbÓò}6eáꞌ\·ÑoŠÜë!µH‹ ÉñUVš‡ö‹©% #¼ážG³Œ˜š' j` é±?”!Ü×€ Á®¿E( Œ¶ñëñz¿_Q¾¬³ÌGzç‘F©ódé¡S6ñöé^oT ;®Ÿ4o6Ë.�{Áé<®¢@¢OTÅ­Ú…}Z}F¸?Ø#Mj˜„¯:•ÇuãÈ£K§Ó?€(a[�Óâ4OävYu®ƒÂ›‚aÁ|ÏF*FŠPDJ±N™3�À�òS¯ƒóx&õ¹p˜h/™h;¨0½½tÇ ’ý¹xí«&•-Vº;‰­7¨¦G/MJ¢SlVÏjD—C9r“Ãî=Þy Ngw¡£w.d‰RVv •é‰m ÔÓw¦ºM ëéÒ•ä0@j³ZeM:T?ÛÄÌw åÅè¤,�F¥)�’ô:/EÝ·tE"oCrp9‰7öÖ4ʦʳPm22�Òãa…IýþÓA~hô� ÀÇÊ¯Ž§ð(¼äŽ!™Wîý½¸b^T˜Œvç$~WÓßrÿfg— ØùÝ ÛÑá:ÑõÊþ ,bä–.‚ø|æ'Ω]�œ²Ý§¦GÍûò+G?Ù­²ôNêß°2c†MÙ¸Á4H�™x‰|ƒ{çxïEnå•´µ5uä£7Œ›ÇðËÅ,1¬2T7fMá´=ðl¨Ëö�e|½Ìg”;N˜Õ-3�U¾—œ7e’Þ:Æ„{}_ĹÅ0Ô k×l],!2r«K@Z£§‹Á×lŸXá¬&Ìfúº4›‚=¿&Ë>!÷ØX„ÕË8æëºòÔö´Ö‹º}yû*+xÛ}ÍU`.—¦ÛªØß®–qÀ\¶ †*è# £g[c’ßo9Œ@†¬Z9Òïȸ‡7Úm;ÏPÒl9%®�v¼~kè-{«ª�!ùcøÞ$h_'·Ôíà*B¡žÈ«c±‹±šÜä^üû(Ç£™”Þà‡Ÿ=vîXLXèÂÜêïOä.ûãx®CY )@ÚìGRN0õœÓL) ǾÆyG 8a€C,$°ÿ)ý�ާ:yŸoC¤a”¦]ôÞ^�`G™€lêåz$�£kÚ»FË�hpÅÛeéƒ�µ¾r|½nc0„m-�Nb ©faa/S+ªä)’œz}R/úPÜòìŒ?‰ïߥÿ§¿Oè½>ô2n¬Úzž`Ë~º²Súº >¾wŒiþ÷îÆ‹:—B"Î�,¦Gƒ¤;˜…Q8ÄøÕ÷•ïÍ Žš¾ ÷ß�¾nÏÎÑKV¸%¢Ø·Dö�)F~Pñ‹Æ=xÿÛD'‘¶Åæ&XÂ&˜è~n¨Q37ñ0$"O5°¸ð™”óBí"œ e8?æÿ»€Û7­¢M¥ý7Œgc¶™RŒx„œpGl³·ËŽ>ô…¼Öc‚TãbQÛBQ=пX(êqú#–~ßøÑˆ0Qœ�0ѾQ‚§\0¹%B Oì0ïÝ\d ?¤Þãg¸‚ƒÜ›x»sœ·Ð)EÕî.¯ëFºÿ;��p’;Þ2и§ ¦œ% £Dö^DÝBÛ€«ÓëRô¸.õuöÂ]g±q%—Q›�¿|˜Äºw$ [—˜–ã"9ËèæAL¸d/A˜Ò½Û@c¨@ÜG^M72$—6á 9¤Þ7» ºûÌÜk¥y<]fdèŠ{K4•öN8t0²Pù‘ð¼—Âò¶¨5ÝG÷Öoú æÀ×S|®2×LSØÎÏUϽ['ë‰ÃQ÷[‹?/²üÄêE#5´.ÀciĪn¥>²Ð“°ÑM:…KK£!-³#Õô=$Îû*Ô »Ý7TÞÇ�t/ÿæoÇqìP"÷gNÝ›ùÚ–ì’zœÁ7•3Êég0ÐæäQØ^£ìh'V§TJCYáhFDÝ­žØäöžn“°?”ºÝF(¶ù Í;µLÚµêvHKý¼¨DºI¿^ã}ŽnǨýçŸ'1ü{‚ÿÚ«Þá›u:âWq- Ü'Ì…· i’A—ï��«TzZ#Âp|ˆwºß)Ýþò%xl¡ƒïyPFÌí¹+倆ÑG!�Ì<—[·t—�×Á£OÎ)ÖcqÙn&Boþ| Þ5y‘‚$¥YèqñîP)ojOÓ� °'³1x«šèåiD{j:‚%Üj�`}»íçññÝ»â/À_J£KiÙ«kÖÂò×\aŽÿ®¦|»HÊ…íØòXm8ø •âûÅé;+ò-ŸìÕäw“dLá¾Îà Øû²î¾È�e®sIvÄrÃÞuϳî`Ü�_ÇÛ%ûPÈßVѸ3÷ÿtboub÷=n¿—ûøOWðŸ®àq�ám[¢¡ø–bÍìwôpôáÞâê8Á¸öqäDGݘŸ¹…ÂõLØê+¥C™íÛå¼áÈô¸q´¨ ¬R›1HöamƒPÝj ÃïW0é­›sî(Rã|¿Õ _Ôvö�ø f3á •ü˜LM¶4SÒ¥y¢±2o*±Ÿ Ù­„4 ‹eUdá.¼öê]sgq|`-r‡Ñ°²€l�ö†A�|ÓÜ»U¹Ÿ±nGÖÆ·ý†¬ïhŒù¸DÖiŠ´“êï[âdT¡§Ž,ßð}¬þPA“>> ôtç‚ 7Ôc’�xã¨eaä7êvñt(˶»®5ì+Xi×]У® w®üXbwò ¯“ºÉ)¹ÜкNŠäPZ¼ÔW‡òrVÓM¬ðš_®íº�ˆ†S™¾Ì%™ãSwGßÎu5&¬BOkÃ̶�Ôƒmñ'k[~âÃÓ÷/lk|ahK�=G&wÕ…0tù«˜Éï7d(oqY|Ú䟵ó;ÚØ4ÐåÎ�.e Ž' Ò™$x9µkÕ¿ÃŽ{¬ÜÈ{@zÖï:ív99ß×Nmè9=Ù¹šbbz«uÏU“ÌNÑ2�©N­Áuj'Ã%lPÌt(‰Û2·Wn÷VÖÒjð ã;ñ»»åÈÇw5~èÜ4bÝ”tH,3µÑRŸ½ë�í#!öТ+;|@ô§àO'ÀŸN€?�û;,GÛÏòÿR¡Ø-æþ=2÷;Ö~´ÍÜ” õQ³ýVGìÄ ûàüV.v²‡¸:Ëtö^Á™2ʸ§æK»|�‹‘Î�-~�½]7é øísüFšÈ¦½•S¢{ãVò˜6ÖT軬”tjv’ù‰6C©8ýKŸè~§® ½L¸~¯ªÎœØ®š8@îg´³é§ãÎc¬ô ^‘ƒ¢ŽÄ^$»Ó‹0¸&ºnÎ ÝùÔëµ\’)ûu€Òi×·‡D66,{LOn¼ÀäÅà¡�Œe5ÛN�“Û&EIX^á¢nKG½>öü€%gá¡Ù0µ¦%ö[˜„—l—¹å§¦ÞÈÉe’ñ ÛõB…ʤ;B ¼[�ܡЦ azÅJT}Y»— `®Žf?ƒ&)90B.Œu� ôi¡D¦ÐŒa²Å&›§øÇá{–0­×4‰7‡Võù“C˜ˆÈ²êP*#•e=öÑOšëÜe déŠâ‰uAbö*møÜ“'ˆÇÆ}ĵëoNô%Óî—÷ûêÇ�ÒñiÜ¡vt´~%òõŽqÄs{eÒeÂyjo½Â{w[âÛq÷‹“ë)!מX77¹~£¢0(_´ýù±üIõA0Øúÿîtº¥GÔÛè wòÑ̯¿û/ƒÎQ¨@R#v.r(a ÉH@$ê²[mOt± f�h‰9­°Å4�p�aÝîu6[Ë”].ó³œr6<9%ç8Ã݃jøµDа–9ñÔ̹}!î½a[³ÝáègLee¦¦ŸKúÝvÅ‚L´]lqÇuµUÀvÄ@â'EMÝTê.±š~V.MFýôS�~{ž\ó�8ßñÝcm Š}�4»@]˜hœó%Z{[�Äà(d&zVâ8bùÃû^=�GÙp]׫�yŽ¢ÚIߣi®Ã�¸W­©%°Ë?´mÔOÎòî'°.^Ýèh¨bKgVG+Å8¬EVg+©tÓ7¼~<ÌïäeÖ$»à¡H™‚-g {¡ñÜ©ñ í½sâ¾Ó“:"Y\d»­x9�uZ ï¢† 'bu˜n�ÕŽÌ¥ö�uÒí0rÞçObã�ÜÂk a›®ˆAóW~½âï3÷n°ÊÌh;}íŽÏZ^B©¨L•Ëõ]Õ}õDCþkÃ)4�>æ·¾Kf)iÐðÖauª"S ôîÜ7¡ü…·WS�‰±»Xˆ <Ž+0Û*L—àêGWŠu.•Os5KêT®J¬ù8;y¤û4tËv B�”»c·Žú;ê‰ Ï£Ÿ/½n�+ôæ673ù—¾óq[&çI^�…VieÆn�Ž>£}ïðپߋ¿÷Zó/zëœæ¯Ïa��_}»�~Ó¹ßÙŒúªÉÖöaËœˆ6ô7êE[.¥8ò«ï÷¿HQ@+7;ž¡yhi�±/( ­æÕÔí ¨5“€ÊTѽ’f½ŒCä­¹¹ñm–/–¦AR¨2F™ÒÖJœ­Ö°Óé©ÂÛEó¬-ûþÉ×­i‹Å–pIQnÌÅÄnšÛØ#†˜;~Âv(8+¬Î¡$sL‡_&%çŒÍ³-n~=âõ¹Q€Š–V«Î—x®?Pþð:S6€p†xi•):"ª]¯«º‘3­4#¡¸¬Òí_x)ˆ_>½y¢„#€�Ù¢{Dokç÷á{šõ/x˲]º>½h,è®dW•‹‡¯²ŒòáÐÞ‘¯"Ðú“x °ùÒgÙ¬Šÿé|qËß­`q_í“�ÐÿþV�tPÂíˆI x@Òo{ø€ñŒ5gž ”!n"Ú¼Ïèb§Ñïã<º»ÅûsÓºkõНºW�»qé%¥¡Ãäפبœ„‹’bK_C ¸wK}Π뮜˜…YR?zñã›GŽÌf³‘œ |©z7Š 'GCö#�Ë̘z¶$Ä�‹Ò¥‡ÉC¼^™p¤ÀƬɂÁ.89ÖðdÍ^µ«Wÿì Íg�/~È’²÷Ô3—§™ð”¤ Z}�å^ „ ;5Š5öæ—ŸÖµ¹_wR$3wG¯LkM ~ÿ©'Ïžv98/ªÿè΀–á9Ù™¶çß3GÈ1àpƽþþÑ·@KÈÕÍuÃØˆ#Éã\™ob›ʼn¶£…‹+B•˜ÏdÚTâj,ùÂò¶hÔ}¶Üª “Ëî/!ÒŽCLì:hŒçyÊ8<–sòa²Ç€Ÿ?vDô. ¥Êk<†é„î$ØKF ¶ìM\‘e Ǿ�Vlbv«O›–‚:»DaLt»Ã!(¼U§—øZ£‹GÌêoèzrLòäOHEü¬Ö§Çó ¼—�~ápÄϰƒõºHf|øèø³3\GBøš;¯Öz°é˜3zŽ� —ÚÎÕmZöþk]mÖ©Æ6¼§Ù1í|nÒå—¢;JÃÅ ˆŠèÜ©lª£¶¹G²³{ü“çÏ~ €²Ü²º§â(~Q•…û+9¨�f‡Gp}ß+óiŽá#yÙV­’/ç`<á¾Æ ²dkÖJ�M&Ÿ»Ü¨Ãzlîµ�Ð*§Äê:YÄSÃRNC(Ü5ÏË�y¢¸såLô]¢žë¬'ô8[þaäâ½å rrñÊeÒ”B˜�Ù''ÌOkQË„â‚9Ým× ÿèxF—f]²2SúJfq·AëH„£ø'4Ã2‡cï\b´‡Ñe<¦XŃíf�•äâTEE{ÎË×ñüs`·�æ ×ÒÍ,�!~Û®½ƒÍ)«ÈØÌõîwÇ�û7%)(™#m¥·ºp Ðåˆ^4­G‰$¦ÌýC½�3ž"òÆ Ý‡ ²ö¨8ý»]�¬.-�"ýHˆCkË0#=EÓ‹nqÏŠ¬£ÜãPNPÆJ�\™4»D¶…/}â¦~çÜ”�/¯!°¸²î ;Zu#~ˆæªmܧñÌG65c8`I%=€—òVGøŠÑ!0`³¯`/ÃøEvñƒïæAö3�w›©ãçÙ¿túaü|µ®+L ÝÕŽëlÃ&R aàð þÅ‘sïsúÁÿxòãã×ÿñÓÓxÙ¬Š‡ÑýO–¤ðÏ*k’x¶Lj•5ßÜj›ùôï·îÀ÷MÞÙÃïó2Kêª4+â²fY¥êÁþ)zpG†8«Ò xüP5I“«&Ÿ©ûqðUx&z�Äe²Ê¾¹å ‡þÁ�Lø• :|p‡Æ|pg}Óó5g…7c�©õû¼\·Í—œú¯Ë¬(òõ©LºÂ_Ô:›ížþ7…�Í�ÍýhÀÃÀ›¤hÍ7úáHãÇu–4YœÄ=0äI†:®Î>d³&®³5`6+›¼\ÀK=Á× øZåUŽ"ž ÍÏõªWjúõ-ž“§ê-Ò¥ðD“6m]ª8‰†�°çy7UÜ,³ £*Uæ�'eŠ¿F1,/ÍgMUÇi'Ùã(Ž. X&Œ±ªÎ³4>Ï“§¿ý¼l²z–­›Û2}Wë�2¡§ˆ^"ųªl’¡Ç?á —M|�ëcøcEPU%@Åxæ/ç[vžÕx±hWe\Í}0÷-r¥" Ü_ã¦,ãÙÊlLÒ,o=|ñ×&_eêô§w²ÕølWY�Ïb˜«¨ù,)bx®Î/q6G“(¾Xf5`¶ºPSÍNáÕ™Êêóq h5 rç±(Èòä p 8þv§ÙDAüŽÇ­E­ý«®dÞŵhŸÖÁì¯]#ÓÃ/Ø¢MµxÆ9úŠ«€Ó‹d;+<4 -u×’ V ƒkyԇⵯ�Âtƒ�üôÕñ¡§•E2ÙDTÓþ«ü¸y«…1¤ˆ©´È+‘óN!qü™ ¨qÖdB~Èibí RQ‹2ß›öƒiv rÆÂý¼Xæ?x5ÂwëH7e- ´âô„V€4ú¯¬®” ,Ng(ê(­Ä{ †̪Úõºþ8‰Q?$¥F–yà’‘¯Åi,²ÂŒÂX÷‹ÐÞæÅG8 @­ÀïWm‘ P[O5ß2†Ä&þ/¦} A4 =ÓÇÇÓ;í>È™S´2 H Òî�6žáS¢Ú�8ñtem¨’Nˆ*ˆ€ó_ ÑþºÊ(i`Ö9ï ªé¡ïA€€:³‡e°�ß!ê9õ̪mb& Ò蘆VU�éÕã,S™ÇA~ :èÛ®]=ê˜wxøËª,³`àHÎ1Åã I…” †'’\¼FîoŸœð™dãÍO~¾*2d|¬¨.¢x¹˜a¬†ðô’vgäZ{~ m)mèȹÞXCÆcÇóºZiçÕ$^zJG õà*/3£t|O{ñŠ÷"Šž¿üþ 8«¼lý¡´žv�’P è>q w ÂÂŽ;3œz"ÛxœÕZmoÛ6þ®_Á©@±IŒ­(0´Ž�,M†`I¤Á†}h‰²¸J¤FRN³_¿;R¢¨;í¹�ÄïD>÷Êã1ËoÞ½?øýî‚ä¦,VѲý`4…�’J’œ*ÍÌi\›ìøÇx㆛‚­nêÂp!KN‹{¶QLk.ÅráˆÑrÑL²–éNùýJj¸6<ÑoÈŽ—�+ZR"hÉNãIžxµ\Pœï‡]€-+’TëÓ8M2 ?ÇðhàåD¦¬‡d¹Þ5Ñ–²ìËE…S®BÔDyNRSá<‰sÊ·-‚R¿ŽÝûgÄ­¿cAK#rý'K Ée‘jBI9½¦Ì�HB ÛHÅKÔ•š¤ øPL1R)–òÄH¥�HÆ�a)Y?5@J¡`¨Yù$"W†pM„T%-Š'’(F‘ÿ‘›œ˜œ5oÁ+ðb‹7«EbÒ !ÀâaäTGdÉJ¯jòxõËrÁÊ•ÎT¦€E¸h¦?Gþ[pÝ,qD¨HíúNÓÑ ˜E0¨#¹Ha"t›@¸d~xªX3ÍÐ&*R€²“Iaø…&Œ&¹WçS'0z .ÚLƒAÚÒ“ìδE=Ë5]ó‚�*BÁ; ”ß%¡$çLQ•äÖpÁ”£CA7” m"»H î�¦7­›�O=XµhCjͲº@Sˆ�PÕZì\²,ã g´J%ßRbèº@=Áö€�à)U)aJY§ÙÐXwQd¥ªŽ·´¨™þî¨YôÛr*’Vý0µ¿´Nèmëï™æiM =¿–›kþ‘<—2Îd-|®8(% ÓLªÿªA í{à¡nüýZ3µ¥èµW"“틊%`2°Ý#&'J>jò„?|“£�bLq@±9 –ù•*Žšôªu±‹VK©¡Qàº�T“uÍ C2%KtÖÆ¤£ëºª¤Op37±;”;c ê±¸ ¾ÊÒ+�‹ Àƒ0# w%�`äÈz“·S"íüj ||ß/®b†öu^@Á÷h‘”UL¤vùf©V&ÇñÎÓƒÙ[4U!ÍݾÀ÷¿ÀŸì;¬ŸÓ­�2´;�²ÉkÅEU;Çqk0¤,3DÖƒ­ F$k–PBo r"GààR�Û`Vl`ÜÒÛÎñ°Mzh™ ºÆt'¦…u#ÂÊÊ<ÙMš&˜¶ S}«…—…y[ƒö2.XúrcÞ´„ôf±€ n�ARçÑ*J½#W[¹yKnήÏ~r"hðk(ˆÔ�O®W�·zû&U%Åfõ;¨D½h†"`HžŠe§q·qž`ÕòÂmJ±ßœ(¹�ßm}±ß?ƒÌjùmð ùfrzÁÿfé5h›*›œÜ»Ó´x5=î «ŽÔ®Ø9‡­Êeo[ßä¯V~åÉjâ�n°âÕ�À`žWXÊaÞÂOµZš´)�¾læ“\'á–6 ÚUV¸(üt=Ú‘lftçr‹ùLf7<�·¥ØÿÔ&‚ Çv»êÁd±‘Ø£‰Í±–0�zf°·uÙ÷ŒÁÀ$Ôº\Cj¬‡ÃÙ%¶¸—äžQ¤¯~»šo^œW")ê”YˆW�"p€q@½‹2%Ão9ôjPºT²= ¢ @3³,�?Æ+ü;…Çýac^ þ䇇ˆ1¤‡f/ôõݼ°Ú‚+Jë1¨–”&3ú¼D—S`.ð µ 0» Éà‹-RgEÄ5p¹vA÷}RYžŠ•+D2ÄɼØ.šM!Ä8Û¤ö\ÑáÆ#E»Ó¤óbwhì Ñ1FGéÒà!öòÞ1ž'‰Üžã`aÒ?¬Æ£Ãë(žNTi*$qŒt^ŒþÔ‡¨12O<˜öÚ£|êǸî4«SIî�žƒøß%©.(„…û2ìÆõxZ†™+¬à„ë*¬p`…%{œ3ã¼ó­I‹²{Ü�± øfFè[;`ÐèÙ‡oÛ±Í oÐLjÇ��ý)…®±�a{‹3z±)®‡#ϖ؇ëý±�Ú~è¡Üô¾Ùlqq›·ýÓ.ˆÃÓʼøÚ¨qN>}¾‡(¤Zp�Á{�ÏÚûÀ|“Ø®°siÐ!žEÛñy¾ƒÅp.Ó¶}tãžþcï(œsW�4}í·ãÎl²Ý}mfOi'ÿÖ@Ÿ�¾ ú¸kÝOT)ŽÔoà…ÞWCm/âö^áÿ€ØÝuÄþÎcŒù·Ws`ýu»–Øß´L5EÆÝ• džÍ lȆ£ÝÜ0ñÎ'n&›1Œ*’?UÀi® ÞáEÓ!±†·PñàNjOcÄBm.Wû9¼—°ŠOXe4¡…lÖ\QHœ¸z�“Q mvǯ¯˜ëχ‚£îý×�¸5Rýµc{hÄfAïâ|ŸDÑ„H~?[4ÿh²pÿÃòZæ‘ —z4¯x9xxœµ]oܸñ]¿‚õCãôvã³Ñ Ç1àsÀ¨ãŽq‡¢( ®ÄÝUN"÷DÊ›}éoïÌðC”´¶ä@ ä.Ž83Î÷ é‹¿¼ÿ|ýð¯/ØÚ”Åeráÿ<ƒ¿Ja8K×¼Ò¼;ªÍrþ�£ønrSˆËOuar©Êœ÷bU ­s%Ù¦RQ™\è‹ —\œ8z •í�úé¥6ÜäÚä©>gÃt!¹àLòR¼;Ú þæZ‰å2Os!�>º¼8á¸ÏÙ~[À@ý `7,-¸ÖïŽÖ¿°¥†?sø§J©Êž8ê¹çqÇ.—mš `�0/N6Hý’Eë쑵Љ]ËòG¿w©ç¿Yð+fø¢l››5SR°JmlÆÒˆ—[ª?u)õ¹#Y£‹ü’YîÃí?lÎȽäFd-ZÄàZ„úõCƒ:“¯2&ªJUL-™Y‹@­‡j¾‚Žì–Á™ó XìXnt‡r�Ò¦s€Íœ˜µpÂ19Ì�¿e÷g?Ÿý�“Ôtæ,¤Ë #+´X¶f+P�…bÈjká\­¡A Q膉m¥0ñ¹=jY­#:HÁndÕ8}ĸ‘iQg‚|÷¦ ýÃñã)Ä©¢ÉÓôû±å÷µ!ULðtݘԚÛÜ¥¶Ò&æ¡ã KG^�ûf«j$¨üäà.3_8,y¡;¨íz€ k!ÎÚs_™@Ø(¶|§±¥Y‚·`Åàñ¦·‚Û\þ1¨qšJ»–V_“ø�-kIºPW7^ûòîêѼ •�û Û/¢Ï^I¯RUnþø5Œ¤ ©h�â_+H9-±Ü7!�¦ÍYç˜^MŸpËÈoƒº �S),"Ø×ÚC”rI,?ª9'Ûžî ¾ÆßAAA}ëBC•®Ñsƒž&—ü{ñ8®o÷€Sɽ¡×»_󢇴4 x©¤+NE¨�r3f¶yj“‘�4”—Âl…�Þ¢çEþ‡(òµR•øYsSWÔ;O ƒ›ˆ\´½ÍÃ^ò–PæŽasìV1 験™©¢À<ŠãÒ|î†ñ˜èÚþøaXÑ8’šHÇHª¯Þ4 ìÏ»žÕ1 Z†æ¤BÁAlÊ|¡JaPê½~A=?¥’ JW„4„’V›ót‚õpìzÎ(7jÃW¶(|[ô/‰ûÄ#ÙLEˆ¹9€.ÃÜfX¥Ñˆh"ÍF÷øo3RÚ@@,Dı-¤÷ÞS_4Zž>¸B¶7í¢Ò*ë‹MÅëKrçWÎw]lÕË&aZ=Ù…RV°â°ß€H ¡]BÔ/‚WNÔ̆գ¦EL3¥Nz#^?˜³ñÄ^©’}ºz¸½úõ¯ð©Þbg¦÷ñÂ2•B[v÷&ñêhümç¾Íd�äöà•R0¾¾:�pŸ§1Æ}”Ÿ®¥mBhfžñ˜óEuôôRµ�Þ $}C8�ô<µ¾ÄìJÓu�¹‚’ýø{×h„Š zI¬^éÖʶ­°d#¦Ü@? ¡çäIkþáÆäÑP&Œ)qä^ˆ%¤ê:NרÎÊN/W‰íUÐk¨Õm¨&†›Žz²î£MtOÒ-yFÖW>*[Næ4lm*8ü;agn·ó}ÕU)¸®qzŒQ/iT�‚‹b  ‹ ‰ ,‘@©ò¿®rHNcâW|Òö#¢ÚWÙ�\â…#Í­S ȇ¯êdꮂ—¹(2ŸV®n®;M|I;Ÿ~ @ñ}ÁuŒ‹Ýemœ>@Çüä3W?X”,\ÍÝ«øzàÒ‚w*ÁsÏñüŒý�µ¸b?1üVî(U–¼(æšC¿Œå]U Û¦;*þ;.ÿô5Øú±dsø4‡ud·wO@NÐ#/¾‰@aÇ…v=Xx/tžÕ¼ž�È©Ì?"Ø·ü°8>N=ñ"dgXÐo0ºhíwÏ·ÝŠ«ùúkb_d䮸¬�íþ8†>o^õiÙ•l°õå¨uɦ d=þ#â�¬9”ؼu%ª“öÈ×Q�K±XêŽ$sC œ&õ;¥·É~Ñ<Ñß>“§-ò¿;œ¹ÛÊx8E{ÀÉŒ=ÐëÛú-êL±{7ÈÆ½÷y.Ð&-U»Nx:…˜u ìöçø†Ô+í*ûVëæ¶!F:¾Åˆ÷º�j_¨ôÂò­ Ëx-ü\fèãýlãêÀößt¾Öl)±†þDiç�8í r‡]|¸t·p“Õîž\ß ¨°@�E€¡;H«£\B°$Ý@rñ ’aÓꆨÙRÏÌê‡îwpBŒœâkJè|k(êCoK³äãÝkö?èóbßégóø»ºüMfÆ\¿Çð^¿·É>wý�’^0 ™®7›¹[Òw"å5^¦ºw|Ô »& ò€±ƒ.zcf#6]eö.xÃ8,�ì¶à¹ô„Hî= Ž?Ö ‚ÔA”üÅ?-¥âzBÇDŸSoxóå6H]åDœ¿y_#Í<¡0#šÏÉ2Ĥˆ²‰cNˆÖqðrzÖ~ÉaßíFe¥L M®‚~”Û‹ÝÜeGQ»ÁAutà§ÒH�ìóÍ*_`”¡WccÙ�ðþBûw‘¯ÖF7•wgo¶%_Ô|²¡®)á«Z¸‚Æ…ÄP—J!2¨eògQ‰ýµ†-Ì>2îv€ëÁ6Òθ‡R]„˜É¸GS/JuÍSgLGÒY™ Žøàã§„Ù6F”`8‚JÃzÿ'±_™\Q!EŒSS|*%u©¨hdºº~y†^�Ý<=¸gÜ»PŽ'3âÀN9"šûEÝ}Ù÷£/ vaŽ°Ø±L,9°7½H}f•Žbà©DÚ¦96�«žÌB½ù%š^º¶Ï§›©PÒ\lÄ4C'¹�nûße.gPhÿOèÂèe£’ •ZÕ�‰'ʶeôðY÷_@÷žê�¤–°�§h¢“<íŒqž»Ñ× åæáÐC\,Ñ+I“x ÍŠ\Ç|Î ÇÅÔôÔ&9.¢Ž5Å�€ŠBÁ~Í>ÀkÌbٵɾªöpÅêø®aò–áÙ~á&ø£œ¿—øÂ�D3Ô°¿4½�¢›åXúö7Biò#b>q¿úxbÁòÿø_±È ::�C5» Füxœí\û�Û¸þ]»Eš¤õÆ›à¶�ÛÍihÑË5H÷À¡8Ðm³‘DIïÆýë;R¢ly­Ý8ip(�ÃZÅ’3¿Žîü7/þùüòÇ7/ÅÒWåEvÿ(YÀŸJy)ò¥´Nù§G+??þóÑî{íKuñzUz]›JËò­ZXåœ6µx­üÒî|Ê�²óièlfŠ5výøÂyéµó:w_‹=�@ëì\ŠZVêéÑ`ÛGƒw�.Χ¥=Vsø-�÷^jD^Jçž-OÅÜAW¹)v ÷kq~%íEU”çSü!žŠóÙpÓóéìB< æ?pã ¿ü#_=<Ÿ’œóió9t¸œ•_ o•kLíÔwÒ~ kþEèôÊØjUÊýºÀ¿c¸ô‡ÕéwKU–º9 ÚÔí´„W²\ OÔ…x¥½�¢ê:¶s² š–�2n\è«8–ÊŸñû—KíüóK%rXoW¹7®¥<ȹöUmƒ`‘˲t!ëB€h/ß+'Ô™ûr�‰ë]oQ»GB¼SjðùœD*!íbU©Ú;w²ýs"©­‹Ã„1�Ã(L½¸@a²tdneBØ,­š?=J°Añ·¬ÑQ«¢ u9…©Û R�U…Îý^X íî D¥œ©žaµ¶æÝZ^˜«juýÑ~öÓ¦ôÐycÍÌñ�}i …;¥¹V¶wgÕ4ñÎ�•‰?EŸÿ1xð†;"g²j…™FðH¬/h=zÂ@®_ٚѦ2ÎgWKÎJÅbÀ‡’ùRXs�Zc;è% �($–�î'B׌~ÝÐhYãçØ%n2.ÈÚx¡O:¡KSº^u¥ìº™‘Û(ƒ»}Ÿ¤]O(èeu.*é­þ ®µ_ S#´–«ª`uY2˜ë¥² 5_ð!V•±ÐôMBT ÖOÂÁ/Z}Ò;kïѲ¤¸áÔ O;O¸.µ× p•C9 Yœô­Ù(ζ<‰zÓ™P‚¶¸‘Ìu¡ê\‰™YÕÈ™[S‘|°6�šÿƒa^½¹)¡gÈH°çéÊsÕxU|½m¼´ ÇÁþõ•BCö�™…¼îǰ5蘯fư*ñÒUô.2Ü~ç�Õ•´kðsoá¿%¬uá—O�žœÞ;º`fã—éƒ?�Àƒïx$ødŠïM}àÎÔIÑë„­íþ³²YÊû­Ÿû"mH�â:½¨õ\ç'½.£ÿÄ€Iw°yKK–¬ªÖÑ¥_]<8™ˆÇ৪º8 ë4—€âäÑÉ©XÀ:ñ—Ó{¡·G¬á&Õß´¶¶¾ϼqá•\zµ0vÛŠA‘‡vñÎ~×8BB‹0Ò­AýƒÆ#BŸ`ïg"ŠY!áó0¬(h’m¸ÇV÷s0ÎàÏÖHY»,ªvŠè4[÷Æ~[€¶ÛÄÁ½ûgF.Çœq˜ãÍ ÖÑH‘$NÄA„Yª©vIã§Ûâ6É/0^Ò™ÿÞ–ü†aä‡"¾¡»Ý„íq¼Ü T?4‡�ôn¯Ù.*ù™ùéù±³}™D ߈N´X,¬¼fXi�PJãÃÍh®�2zQNð=P<Ytaõh Ò`p&0Œh»ÚXhxQ4Æi¤™@»€ )@:ѳ IbònFpݲ¡j�>:Y5¥rCyN¼s€&Z¢]dzõñ�žÛVåä œ°ÕŨ:ÕÙ®¨zÒã¶«Ú»ŽÖ²F7*4LÚ³°Ô!F†KXqš/ž¿9Ø7*HSGïs, ãÑô"…Ú¿ˆÍ�š?ÿû^ˆåfw†Ø\§`;ÛF¹CÁ×Xy dlaØó.L@7CëÚ”2ö �±Æ¬òhàJ”cÍvÍB»ÍËm#oZ#ÒB ØhÖ&\Õ¢³Qö!Iau-6\ŽãBJÈdÄž( #J]a`�ñFìùöäL¢/S:Büaz,`™®Äƒ6bKf[LÅ“‰xñêåCñ{ñîåF*-iˆ€1€=Ifâ6åŽ9œóf j<9Väá=?Ÿ;ãú\*·ÓŸãó»84÷;Â¥YÀ�ºÙô1îív^}PAÁ�ÿö?®ž.ŸŸjO{Wýㆾª‘­mÛ7ºV-×�§qxN/ˆÜÙš°¡w™*ÒÃqF.Õ®‹†Ò>8èrÖ­VÒg‹iœïµÔAFˆ‘¢$ƒ¾hFÈ JeL÷櫲<júþV$…¡=¦ùÛTøóH¨Úw¾“� U*:꜈ÿ(kÄlÓYgtn`V¾§/�4™kí2žoŽ�{{‰£.ã€_õ&S|/ËB´,¶‡äg=K�tÊ6ƒ[JsÌ-ÕÞÞ+^¡1±>[a¦,¶ß9÷MË;KËøÑ£8™Ãˆ>3Ø#˺™\,m‡ôB]iLØïš¶¹íÓïÏ>Ótˆ½¦ßã'a‘C˜‹fÝÚ[êq!bcFZ¹�ºv>‰ éH�ˆž>‚”˜B¿=ú¦Ší$—Œ²€‹±&JÌÁ31SÞÓmYSìCªÁïV;N Q„ŽUÈïjÓåæÜÁµj l‚ ‹�滾�G”èÏ@0)  1«õA[“@ [Ó :Ç®†±$_êc÷Ë ³öV wˆ¶1ÐËà ž ð,,᪷ÌÔÚpÙH²BgYØX4#̨œVž•”G2Í $ò³-#|µÓÒîcƒcÝ“ÉÀ3œÊ¾Phü<¸)� ›Y±­÷WΤ­ï„R³‹�>˃GtkT Ý宜Ü�dÊ› üƒË¹{¡Ê�³Ë^éÓn•¶V mú "¨D�´Ý(ˆÀJ\–õI…@;…;Ë °ß�©î¾M™ÞPÆÐ«IpŠ&áF‰})qÀtO½ªfX%3Fìî°?ÝQ¿¼U'Añ".×8J¹XìQ�“ÖA‡ž ÈE¸ôv¾qòm÷,kmlòÒá5lC#Ę™SöjÄÜÆ†Â-‘�Äj \ÉXk Ö¸q‹ç©OU›0'£„„òÛ$` @ö�‰@<U :A¥ø +lM+zÜݺ‚-ÝÞ<�ö33W*�¿ ˆ!pÈ0®ŒI ,g‰C÷cJE~UÕ\'»ª¹NoQÍ•¬NÜ*n¬‡JJ¨âþ0pÜ“¸÷aoiwG1o”´ÎÔÝYŒìGÚTI²{rŽdœ�ÑÐ¥Að¾q¯@ÐŽl;è“îs‰]“™uižÎ8�•£ƒcÇ~¸eêÿ\&>„� —Þò}êÕ�ƒ›kœV#–zî<ì:KJÁºŒM¨cìS‹óRº°Â"Ò¥VVZˆxr¸ãð:ÆÂ¨ã�R¡prñâÇIÑŒ^âÌpì'®¥­Áó&±|>hʃáÖ#Wø•ÌÕsSÛÙOò¤W“Ø>Álå÷ØÕ�6Á¡ËÇ�Å!ÞÊÚAªêœãSU,¿ÒÖ0‚é0]kwõ±–:8Ôô�_—jëÉkißïÙè/CBu'TwØi^ÑÛÛ„#æ))Œ'»Û[t×ãð1u'? Í3D'¡S®*ÉWÖâŽA{6ÙcÌ14o¤çºÂd2Ù^¢&jvE;ñÀIãVÏAùSwæÊr­4:ر+x:ØxŽSˆš„‘{Ü%H}WêY´¶åG§áýO˜îìú %3ãd:¾�¼Ï0àO}'2¾„È{@�á¨ÛaC0kV‹e¥l”éÜ�gcòg,‰hN(HÌ V�¬ÌÊ%Ÿ]ÙµŒX²^0pó÷t’’EükÛ‡\§l‹¬q §rGæýTÖ—›ªvCçÚL2îß%{1ôÃßo0çÅú¡:ù4¿mI2Í;L”©©Ê†šnŸ±]³´«d)kk–ΰø¨B©vSq­Ý\оFÓ†ºd4~N`¶ôøä^ˆ%´ÿ®Øe¡ež~g0H)ZR�’z±²ªûtg)T3 s¨­ø|€ÌŸcÔÒPn_ú,}±ÝêC6?öWº(8Fàˆ�­8PÌPU’õ¾ñA‡@óâø³�nqgBÐ2’Ä�Kù [—R-[ÜñMEº{ñÕIëŽ4G4ß{gˆ¢dà¬#µw^æïU�ƒØ“;ò TO=ÐÀöÇ×Ï.¿yö×ßY÷ËÊœ9‘˜ ›žzC|ß4!’ªT½bŒšd�ù’Ü»‹ÍâÇ)ñÄhÏÎD{P Og ¸q+šq0]kâ‡ÃÑùQ¤²!Ý€ÒIO8~ui…C$6& ? y{<–XŤKfv\§ISîþÌ2 BÆ~(ŒëÒ|'oH]¥ChA!(Dðúa¨DóëX§ƒ¸µ¦Ÿ&.>ÙŽ‹�GòÛråÍýí™�ph ©’× ~"× ¨œ<úãÃÖë’М>¼c€�LÈKèqd�½óãEÒŠCßš&QuÐ/EÀ‰˜•p%V qŽÓÖžO¶)õŒ§À>H’cJÍ;ÐbîôXa_ü�mÆß):†qmûaˆw&Ž™á.§mŠÃx‹ƒföo•ûξȒç�Â؉aM~þFqÊŠe༑íËò…jTMãèbðöv ? ÿš)ÿOnþ ±7(Î €€’}6¥~%¶xœ½koÛFò;Å‹;[9ËJ®pHd®÷ÄI`íݧÊ\IÛ’\vw)×ýõ7³/¾Vjz  ع�™Ùyï —y÷yýðŸ/ïÉ^—Å*YúFsø)™¦$ÛS©˜¾J½�ÿ3]À¸æº`«O¢úÈ+Få­ÈY±\ØÑd¹p»7"BX¯VJSÍ•æ™zC†»`:YRRÑ’]¥ýÉtµ\P„ð�.J–5É ªÔUºM¶ þæðªaW‹zH—›„ 7ë–‹a­�²Â¬ ’í$SЋД¸Úâ¹LìÒœ<âRÍ_§v÷5±h‡xÌ ›ŸX¦É^¹"pÚª-ךådó”8P0P¥qA´Ø1½g’ó\[j½ovþ¤ïCg�”e ÜÕnuÏ@D…�\.ÜPÊIÉø~•ÞŒ—˜ScS£4¤Htj=zcX›®ÜÃ×ì›Ê8î0_µŽê¶˜§ñžNX¶›zùy/+nýÞHqò”ø\ºŠ�Ûøo$°€ìÛ^ Ö`Û6ë3—ˆûs?�øo-1Ñœ)ƒ<]} Ùæ K“5â/�»KÄW‚ü¦?}ÙÍJÓAŽJÛ‹¢�ÿÎ|Ȇ㠰ݲ'§ó¸ã>­f$F¯™ð“õ÷lä­ƒcìÑØÇ}ß¹Z¿ú'Ðý©)û0ˆ25¸óg¦,0eDbtæ4­ñËÛ3PýÅ^„„´¤¶¯§é«;ë&§éóF1y0ù—¥ª;pš.Ñ[9-eŸS¤#q$ žšH–S›1�i¸…[Hˆ³&ï›û�Awÿ�ö*ô¼DÜ# ÷q¬ú©ÈôøR{wãˆÜN§Ç—Ú»kDÇò±©‰èÝÄÒáÅlL,èÞË\¬è–.&·”Þ5-]ÛÆ4~ˆ\û&· gi§~v:qVåxŸ+áz¯ åSä+S@R[¤Š©–›ü3„wc`›"”޳Èδ%{¡˜œ9bÓ`åÛþÆ�ÎfðjGDý,¡ç³)(0Ä>DÝ¿)9mý‚ɵÄðST£(áíX";*Ê™D®S“R—i÷ß7Ï™L préýà;{¯'é£E§já/U'ng®àí®f·öíÿ½—u��2¶AsiØÕˆï±f�-cÿa œ¢ÏI8m‹þcŠœÒm êÑiKº¦RSæ…«3Q[§«Øc«H‰¯›µÿ†9·øœbq ÂJæëf¦˜f1»$äGìøèGAD£ëF+SIÃZ�¥Y)wÁª¶¥ZØÏ³þ:Ái ù�Y8Ÿ6%xœ½Ymo5þ¾¿Â‚&ÐË%•*Pz9�B•(TmÕU|ðíÎÝZìÚ‹í½ôøõÌØÞï½å’‚Ô*Y{<3žyæÍ™~ñÓï×oÿxõœå¶,fÉ´ù<Ã%XÎÒœköjTÛÅøûÑ×­°Ì~SòW!�ë—*ƒ‚½›«ÌL'~7™N—¹ÊÖÄóbf,·ÂX‘šK¶ë4’%SÎ$/áj�ÅŸ£Ùt‰ñ“Ù^:äùÉ*–ܘ«Qþ”- Nqspò’MW\ÏÊ Ñ/ìŠMç³!»ùŒ�8ºžê±?µŽ¾J¢]Ô2Zœ£AÏýÊétâ˜NªÿF9;/Ùÿü Já¿1~Ú‡*÷uE!ªgAúoèèH�w¼¨a«:3v#,Cl(Y8®LÃRƒ1BIæ®v–xÒL¬ÝK3~:ò§½Þz�x´k“�fd a ³9ìQ9髵)(hFr"u> „ZåD¢”JI [Aj•Žn¸ÌÕÊÖRÜæ£ ƒr6ž¯Ç[•Ûb•†L8ޏ¬Å§H ´F¹¶B.aAcЯ€lÁZ•n9U°XˆT€´‘z=#ž%[� "?SK&Ìñþ ´×:l­%ª—8EP+ Yk,Ó èH¸Ü!ØöÀÄò¿À㤵­qjÔê gù¼€ÀªúÌmQp¢a‹º”d¥>X- S+ÐZdH6_7È{ô:P½ãúQP©g†·d%Uê–|:ˆÙÉ0dYÅ)¯�ñ4… �{¹”îã ‚ƒÔ­Eþ²cƒ «ÐGþk®tºý4¥;K¥&f^iQr½Æle5þÏÙ­Èl~5zrþÕ(\Àæý�ïžâF¸ íLèÜĆ"æ˜d“`½ëÊÄÖw”ú\kå#g¯`/éA%¸LÁgËË´)"Çri6­¹N[ëTZUJ“’¼èì¤t�G9ÇPÎZÔßí¯Õ¼6öýÒÞÔòà-ÈÃd>δ;ìÕ¦0Ì ±\�‚r}žhRU\Âîêâß+’pÈÅÔ•aƒ‡q¶Æ˜Sþ ®ê:ÅTéP)­Æ °=§Ÿxa/ù§¸>@è[UÜPô‹I·úáÈ«¼j#&)³[2ØÖ˺@óTȪ¶�ã¤ç\‡ÑÅ�p꯬#¹ÆÏ¶ýÉu>)Ü¿D’ro ‘Â~;’û7 Füú·iƒ¿DP{<Í{ý>Z–^ƒ�ˆÞñ]÷Ód ÞwÙk#©¬ið~fap8’—æ°)'á/¶ÿ×àí „�7  'xœ½ZÛnÛ8}×Wpý’'F XìºÒ4YhÚÂZì#-Ñ·’è%);î×ïð*êbÙ©ÔŠ&‘†Còœáðp¨ùoï?ß=ýýå¥2ÏÑÜý 8�9‘Å)æ‚È·“Rn®ŸÌ๤2#‹O¬øH ‚ù#KH†vœí—”ˆùÌDó™u´fÉQ¹}µK*$�ŨÇXFsŒ œ“·“ºÝÍ#› �))¤˜,æ3¬<¿^ôX�¿×`´Cq†…x;Iß �€×ð§1´h´ÿÃ çˆæëEÝÙ:ÕMæ³�r»@Á{D`v9–D \$H¦„rTÍ:2mºwƒÉÅõ›‰qs‹$^g¨L+âì€`(®üO•B¿Pàhñ�hÃY!3‘`0Ÿ”‰ìÔ�í1§ªaÜÛ;Cçhuró°Ï´7óx÷g¥k}ƒºŒ©P=GЄ$h}Ôó€ŠóÎ?åÛ»‘v-tû9É=�X¦“tIòü%b„ÑÂY k}¿pD ÙrBD¤šlà·„å0‘§:ôq¿sçkVdÇËã æqQ¤j&.ŠVËÙxk¶£V¿P3VA t.Hc’esŽ�ª±Ê8–¥{KÆE¾ ”Žø-"Ý#ã ¼R¿åzýoÊ"–Ø!Ï;ð¤©6,ë Ùà2“H2¢WëWW� ½Z¿¾ cT0Ë•¼‹ø •E–;«hK÷°Œp„2jGE%;ãFÁÓk¯ˆÉE¡P™�¡×vP¸d�iÎî'bDü[b¼•9á4Fà‘Óç©OjŠ˜˜ee^´ã#eYB‹­ Ò3œ*ÓÒ¢�û\Þ»‰tÙqƤpü&oY�U0í±ž†Ó;ÆUç*fíx¹Ñ K“ºG—Oe~Ù×0"-£Ì×@—Zõ—‡ÂŽ *ayˆ’l5Ûe!�áVL9bu>˜¢CJã4‘Pø¾IFòÐXãÚŸI@(�³*KÔ–û8ùer1W�-Æ í„ã>öÂUõ+x¬ü+a¢ß'XbXŠ·¨œõlЬa²'üXÛ¨áu Û ©ÊâR±F`gÂÛ ÙþÂIBõ†ÖKke6—¡·>wÞniÞM¥ §&›9¥e’ž_jU“ÑQ)ÉÌìã@höÑO€ÿy-ßc%úá Ç  î¯�X"!tÔÂTû´•I�°s ™ÖÍVîÜ?ÇY™�šæñÆjýUG9Boª{¥§äHΚ€æI€IFX4 §Os¥Åö@~´�É¢Ï V þ +�ˆ ²Å!1Ó*¯ÔÁ@«ä/O=�«WÃQÔ>Ú(>\X•dõÏ¥Q-bœA:·º­!ÛZ¢M“!�ka6 «e/XË1ÐZž€k Bå1›ýRIS…’Ó£W˜c¤ÔUN«`Zu£d–¦(s513ÏsÉ2\–5”Œ“ °@4QzݦÒäE½oIÕ¾#¬ÍÕê©Ƨ`|ê‚ñ‰I8¥Œ£Gªæ-�ô¹§v2(«;ø› ¼f¥Ô'&ßZE>úŽÄ¶¬à z…WU’Ô´l¥…e!Õ™mK ÂabZÔyÓ¥3Ýe¥ðOïÃãÜP—},.G`qÙ™2ÔÛ0„äÿM%( “Œõæoø¬¹Ð™Ì./Ç{sÁD?�õG¶ýH¿“Œ¦Œ%§Q¯™ Æ¿á­Í\gÞÂÍÛ‚¤ãø\y# á/Xö8”Õ½bY¯RL•®SÚMÕÇŒ Ëñ3ÍËß�×CJŠ@`«Û*‹v˜ÍV�Ÿ`„NP^ß Ugý;¼î‚U£ed ¿�ƒ¤ÕJ°Œƒ ›^ݪѭ¿måCéн©§ø»|™ážÐ7Ãcßùé(‹hÐ7ÆàÜÅhã*‰”\I$LmS´.i&«ê¸IxÕ¨d[Ïyµ­……qƒ±/ô¹u©�QT±þÔõpïá­ÆØ$gÝ7ÕÍÓ‰Ïá/%&í8 N:¤»^Õ>~uteU³1c½‹hÖtý;¶4˜j¹¹ïnÓ&@Ý<ªÚãÑ. ý�Á™o êvƒ9kº;ó}AÇUÃèŸø>ÌWQë³Gësû«�环œGžt£êÜ­±eØtÖ“:ÍÑž¿ZÜÎÐ\3ÌrÃ[ÉØ‹ZWJ¶R¬i~Af;„w!õ4òbý5³Ÿ©ÍÌWpÿ€hNÏ c Ÿ–cª(;ÔThWxœí]ës9rÿ>¢TÎwW¤õ¸³“H²ª$Ëö¦ÎÚÛ²¼Ù¤R©-�’X̓7%s?äo¿îÆs^ä�¦}ÎÕU­WÖ ¦4Ý¿~¾ü§Û?¿þøß?¼a �gWÉ¥û!x ?r¡9›.x¥„~u´Ò³ñ¿Ãs-u&®>ˆ¥àZ¤w‚«U%Ô]™ŠŒÝ ½(Suyl%—ǖؤL×HúôJi®¥ÒrªÎÙ"Ð:¹ä¬à¹xuÔÙöyçÓ£«Ëc޽�u³û+èï >Z²iÆ•zu´xÁf HMáe7�svùÈ««*¿<Ɵ컜t·¼<ž\±ßRkm�Ì·9¾UK15OwyLÝ]/[C�ÿÆð«þÊC²Oqjy¶�£¾bo¥fœU¶[–Û~‘~ž˜v©|tóËÕøÅ‘ùÔÌ­9‰nAÙ�¡l&µJ˜^ˆŽaÉ‚^h>É‹i²Ò¼™ý$D1V«É/bª36—�¢`“5µ™•U¾ÊxÍNâS-*ö$ÊŠÁQñ(ªµÿ’ͪÒ�w}ê8®KGiýÒ=‹9íPV©¨ˆEežs–�vhe´>­ÿFÜ›”zñœÑt‰vª?ÉìA –Î-I £Å~_à4fìzÁ“B¹N°ûfþûOq×~\§¾í —³b0”©X‚üge!`x×¶ídn^VÚSÊÊ9þ>ÁŸ4ögÅÁsV*äKø”-+‘J’^¤ðgmç¢4- -‹U¹–B׉ߎ×Ì .Ë¥RHŸ{!1Œ¨B‡™˜ÁXVÚK£Ô±�$ŸYV>!!T cRÀýù*…¶ë0E.ˆ´c·Óê�í` áÎ7{/n`„dŒ YÂf5¿MHVü¯*§oÑÆÔ‰/+™ój ªRWðgÁždª¯ŽÎ^üËÑÕ÷¤Êô"~ñ¯øâ?�JÃ7ÇøÝ±¶Ö‹ˆ¤5"V~’z!‹[¡ä¼xæÕ NãöDÙ–R3ÇÖ'ú4è”ǫȭ&¸’¬*ŸØÖد.=¾ðb‚oÍ÷ K´¢¬HŒS6ž–È.R�žÛÛ;U¨%X\ÜØDÓJðG‰|3ZÆl»TÌø*Ó(4­&lQf)Šm<ÅNA+°K‘{ÅÏõâèêáòXäf õ½óúì¹á.®Ïöu!3±uYê atû¹ßâJ,Aqkà/TP:Lß±˜Ma–øŽšÛ¥NÂð/Ù²�3/ žá¶­À®«–ªð�ˆ…Æ [–Ùº(s º@W¢H•QoÜ-™[øÆ¬ÌJ^àÚ6ÕbwÓ¦r¼þý�à…µ]nä –ª’Ÿú¶¶hÇ$ÝøÐ WIdõ71e¬³•‚ÍÏ5ê$�µ*@�+ÃäÆ4ìnC1Tmö˜I‚ß]|}cõ•…3Ä/°ÇL‡vAÉä—E¶†å³Ïi}HA«,uMO^‚æ*‹ùÕ½€ýš©�Ù±}”0°vÔÖÎ�KJ–Ås„ßÿ ]W Õè‡A±DÐðj #_>ò­è×4ÛîêIƒKGÍ£IÛ6aÙCö3bý{ނŸî¯ÿ§>6ÛÁuí·×µßnÍoÿÛžÆo"Ëäò¢…Ÿ?´ *‡=±V’4 iýb*阓¦ÿž¥ŠTN�ˆ:�¹xœ­˜MoÛ8†ïú³¾ô°r„(°(m’ÞÚ]Š=ÒâÈâV"’²›¿ÃÉ’?b'ÀV43ÎÃw†rþÇÃß÷Oÿþó•mêU’÷È8}4hÓíÝ¢³åò¯EFÿ·ÂÖ¸úŽ-2‹ü+2Ói4_ÇZ­ZÔV É³`—äYŒ·VüÙE¿]ˬ0Væ#\ŽCIÎ@²ï'Ío>£Ý!Ê4b#«ã—€“±ïX}| J}ÄS+ãËÍ»‚Þ\ë‹zøò8ôF¿ h OoTígn2�>ŸÖ(‰‹ô|¢ó`ó¡NðÒšZ>Ç�ÆÐÔJúÂUsñµ[vÍ:œ £ÌðîÛªÕ0ügox šd¢Ù×6‹¿�dášÿ£’Š^9•@­xœÅXßoÛ6~×_Á©ÀžìiQ`hm~lÍtŠ=´DYì(R#)þï{GR²lKI¶AÐÆ¢x¼ûx÷éîÈô§«?.ïÿº»&•­E¥Ý£üÔÌR’WTf/âÖ–Ë_âÞ[n˾²†QËŠ[FM«™¹Uiâ'£4 JVªØ¢ÊóÌXj¹±<7ÈÄb�ŠRJ$­ÙE<*giBQßÛ)0¥ É5æ"®Þ“ÒÀ¿% -,ÎAhIºšR´SNêÁ/PzÅì#cr„IZxn•6g„\Ó¼"Z=¢QÔQP €Q§œPYæÄDï ÔLZbéßL¢!n?:#¿/R0“k¾‚½T`A‹DøÙíN}û�à$¿ÓhxáÀàÒGn+¾Ûèæk9b¢w`�}m(†=Hó4'(„0ˆgCš‰HI½ã¢ž0÷Ç&¹beÉsÞO‚L¬­V[°<%柖†Ás%ÚZ’†éñÂ'9,[+Ís`Ã΄K60ö°¶àrýCÖ à>RV÷\¥¶Š³/iÂê¬Ûi®¤_µ¼u,J–äÜ‹{hÆ}±@-Ïô˜›á›4Á§7vŠý‹@zÏKü.½UÄ NXž#~*D`]}ÖéÕ– í>€fÍX¸ÃÖHy¥-lßI5Ë -Æ:WZƒØ*Á@(ª­ûšK­jbˆ<ÄÇn!t†Ì­nånÅ,:P¬1\(Ù¡Zó CT ¨Vp˜£¦_PÓ6¯ÄvoÑÐ*·†‰rÀ¢K�_.|X/O$Ž^¼äV×az 45V+¹Îþd Y™0 ¯"B Wš•˜„×`ÂÀΰ8¼qÚâN)… ¥Û6øánî‘ÖríW¹À½�3ÿûòuµ[pgáÁç~·…²­¯—ÀUü˜Bê¯Þeô#®zh´‚oÉrfœ•8»ë_ Ðó«]¹Bf�°¶•ãßi~3*uöÙg„+—–âlo脲ƒf῟é’ðtÆö <#êP…‡£1ÌŸPÖ¡;˜à�Ë¿Cú1=ÌÁ»1°nâµýû5¤ö€|oø,è¾’Í‹ñ›Ë´q‡£çy{�¥C±<Þ@ÙÁ` í·}|' ¬·¸Ç×£WÏFþ5Üz9haâl8ƒ{½ëožh…æFìºi™3ÄÛ=?�6ïåNü•]Ý\Çü…§µëY *{”ðP¨zˆ(éJ×ó¥Žx•*º:xëGÿ³uNæºÑ£ÝDgsì…'�þHsxû¯Ñypß‚Æ}+z íøŒH%[hm0Z£æÄ:ÒxךŽÐÈOZ|o�­ÝY›ác—ž}h�ã]Ÿ|ŒþÖOý¬áœ¤>×C»ƒqßDω0P`’ŸF<æœ{˜þÂAe^oz°õ$ÚÛáUéƒ�ƒgÝÓÄ/þ¤ê !àþ®cï< ¸GòB4b¨©^ã�ºÚ?=]\PŠƒ·Ý5ǬXëï» PwO½Z®yT`dw=¶+|‡iaµ%k­ÚfVŸ5sUÃñªö`0IãF P†å/�^ÁááÀgáa î�ŸwîÌ ºWÀ\×p íW&™Æd Ù#ñb§…Ø…΃¿£žƒ÷œì/ C‚�$ts%Ìc ƒ'!¿ˆ�}÷–„«óÄßÊÿŽCyûc3‰Ù ~`÷} Úì�ÙsÜ)„N6hD–J �ø6œk/8™£ê&�‡0�,Ž‘ƒÍ4ß dkJ�Ùø±:¢ ël'>UUVDwdÔ ÔÃCËÃûU~ÿ— PôóömãˆIÑjå%Ô«y¶´(Ã6ÄÐT©À¾sÀrlÍåªHr}W+€íÄø�‡ûnµ.ã·"KÍ´“ „Åðèæª¦�^Q¨ð�UhÇ4-Áuµ\ "”z=ª�ìÝ»Æ$ÌãïÜ\°§¤öêýMÄ]#YÅŽzÆU²VËe&8ôâÚ)�{lð È-³É©û ¬œÒ€ôê>ŽsÄ,™þ1xä‹ÂmšYõmÇ‚äZˆi3À€‘À#g Í˜ŸÀ£Ý)ÍÉûHÆ‚A?’¸l$Kذi·iC—O0$\.´0°V3½ ŒX,4–w°0´N�gäyØQ 1÷è[ÓîMØ$+§È[#íÛÆ�hÓX�ŽÒ¼°ºb&+Eè¬T"±"�£êpéí[ Í‘gîïy6…õ³L�“ô¦ ¬€ê¶ja„p±^¢�gµ?©F£ÉT._³öÈßl>¯ W:CÜ–­ò"è-vD}Z! ]“¶Œg×%&nEG¶WRÂ@é^Z —ÄÈK,.F+Æc¿Šªl-ÀFz*«ňôýß½g'gäÔþõ%àX $€\Ë jâXyÌ+5"P»¤@›p£D/zæö¡k"1 òp�W ¶˜ç�:hXxŸ¨ðg(ÞǯØuØ%ÔÓt–mpt/ =0Û Ý1ëŠ8úØ¡À}›ø�G´CW…¤IkÑ�úÛ:˜«nllX_Ëœ5‚Qb{©Ä7fÞµ�;ãÂÈ*̸àÒ í6áõçGrÛÑÏ:ù`—‰¤Ùô¿�Lù2�†N>TàÓrP¹Z‡` '+™iã1uÊŒYöZ°-¡�Ϲë ‹Ã2é£/Û3£ç±À� ?÷^·´cú¢;5ÂÞÄ,Mæzh¾ôÜÄÇlp�Ò�È@£Æ8ìÔæ$GTîƒU %n3Í2QÌkP|<оš|bÈà|k}(†A¥J£šD¾¶ÔN¢îžŸùç.FisŒ‚`PkOHÍÓÊb£¤˜1‡>�k˜0#�X®æà3ÓöVËŠ¥å”‚ëÄ=’ª Õó�÷â ÂyQ›ºË}w»ypƒ0D~P‘¥Üro?»?þ-rÊù�œòºf-ú~ævübŠÉJ;·°3'‡Ù³:ó1Á¥£¼<™sLÖšI'…ŠÌUe;Æš¸Ðí`œ›pê²d*çYöÍbÖi)f¨ú`¤{ÿ7€É»àÖ0�-È5Ìç0Ø5ÐëC•�êí ÅÇöM]Ù¶0å{ØG¼ŠužÉ[6< Î/xvdÉì´bÉŽG‚âY¹)íÁÜØÂEƒïµ'&«Ùž—x7¿¼±_Ì’&¦­Ù”Ä ÔWÖ® ¶oÚ”¨Ã$”Æ8{ÒNïÉ�8Xœôáb2‹Ä£¼–x15|&ïz^‡ÉIøíD¦uTÚƒ:“¾µ°e_IåölǂӽŒudÉë8"”ÖŒ4÷‡¾0,‡t8b‡Ü*xãª�C¼šVhB;ÜöñŒ“oÖÎ|™`E5G?`@¸Â4<\ÀÂÑëS³ðG}áôð!øóu¶\ðFh™ã³¾Š!Øç bw\ãwíC0#Ê0&lïá¤MÒÌfD%KÚCó Á¦kl}çªØ8®u䶸ÕsŠÓ@v^U|íüªÔkýð^·Ûeht³;ÙÄò»?·çíL,<õQj JÞN�” ¹ÇìIÈùB«‘uíqÁÚñ?ð¢¨,ñªå­ |ÛŽ•?$ƒÇq‚“ß8t�È8KfÝáóMü�Ñ9·Ä;«f•²§jM§h±~‰¼(oîh+4¢4雪•¢òª¶mùq¹ eZFjd. i×G�œ°ßž²1ØiÜ>¿3Móáð|†%S›)o „Éoß¾qý·� {®×©ôæ5:þäÅ·«æaÔö€\¯®MØñ¼Z"¼Q}½º÷»Ú˜@w‹� ÆÆz} ~þE Ìàþío¶q§¹jZÉ¥ÆR¢„ö*sPÊóª\-÷²3nàƒ˜Æ–e™©ž 9…)ªí”* µÊÁ”É_]yb·åIÚ¦§ÇòÌ·˜�¤nwö2;­Ÿ7;ƒ”q8tì \aq^—«(ÖJ�ÇU>•I6PøÃÞ@kéí‡ô¡à<(íH+�ÆøƒÖº_@£XÙ½ šõ%Š> (÷åª% žáìŸ5lJÍS Û|7‡Rqž`Ÿöù¯/¬÷†à³”áç¤Î¢Aâ�Õ‹ÝÝ&!IÖ�«ò…i]Y³CvÐ%=7^Òþ�Oõk@:O˜“Ÿ†s/\©�‰ö^ÄWú›åئÓß{�.;‹‘í{—$ÉøT¥ó)Çw8}m28Iã9Çï�0)\\¥\k±17Bæ¯ùr Éu®!›§À % PF€žž;òhzîÅÎe²¥Mž_hƒ¼ÚˆX¿d£Å!_4å,ç¼:ÙÇõìíGÐ<+R8¹˜žsÚòÅ^­ÜåÒ,&òW'«ŸÙBã¿3üÓ ´X&ûxY©¼eÓùEKîU±ÜÓó‚v¸`÷00¹`f…s jmj<^1£X¢øFGŽ%ëJ­LŸý|â¤\²¼Ìæ ØR7øiVŸìˆù2bî¡b)û©V˜ý“ÝþÃkÜQîc³•ÐÍcâï¸Gr&ótûÌ«zŽºŽ5ÖG(R�6•'l¨ZjßL+@)k%N[äØF˜>¸eË 7Œ”Ê%ª‘2�ÿq5AI%°ù–%°àej‚£úIǹ~+Õ{à*5Þ]¾À8÷Å.ÐG=°ÙuΦ�Õ Ü¬N.n¿Ý–ÙLq‘C2=‡ì¢6®+ñÍ›cƒfÆíFWú÷Rþ¢™?+™&:ÂSÕV,¸Õ>�5Óçï°Ë°E>™àNA"b#K¸á{ñž­(J{rmO TÑ{ÃQ%a $Læü¦ä†áÞm'ŒGöûL ¶ù-°æ)Ú·çnG¶âkZ™ä,……a²:3Q~w‚Ñ®¤Y†O™¶ÕZ(™‘Å9ª;±$­gáž‹°I"ï+>°¸Å“V‚~·~B𦰆T“Ch”…;ážÂ ‚vÁŠÔ+Ëï@LHý ãbK­ï÷8Ò—±Žô%˜#}r$ËÈR>G0º‘#ÛëB"w¸o kš-Û�i—b�~!ògQèÜ$7CV2:9yú` 6ûØÒZU>Ð1¥o %²ŒLH±LË £ÚÚ§8ERt]E* wÊŽõa,šÁ`|ÂïC‚5±\™Ã.êéœÇMl6GÏÔÒaß+äÐ:6u¹ ¦ËŒQ@í†7ÔqÎç"f‹8_¢1´©â bNaf�·¹¡M Cºwã!ZÁF+Ù{�äôÁ)yZ•o•²bRUY¾Î²Q¸ãrCáêŽ+¤ÄšS�µó[0«÷ä$ºzµÂƒÍ�“£±XˆM‰Ç¬ŽÓ Xxügœü>ÿÕ –�2©¾¬Jn_Í+ ,ÒuÍT•šÌø%ûMêȵ�yxPóÈ&°j—[+°#Å÷øl–›’L¼�S #þ•òzÆTž¦“Èf£–fqc/,x¡ƒõXëø�ö”¶`žÒ“Û÷”ÛÉÎ ?iÃâöZä×ÉFVÑCª؇×Ðbu–šZjÿ‘J7|‹E~V˜í„ÍEný”‚ßlYD8õXO‘�~ÅÓªŠ* |(ŒÈÄÿ,!¶ÂØ™�†ú  `6±Ó@µà–¬;‚–¬´êˆ;ÆTº¤F­µÿ8£E?| êþäiG[¨ËÌ»bb-{(X iŠ=ˆâ[b¥q�i”³Ë<&_a¼_JEUaýH£±dñ#ÒûH¿ËqÑa7Ø©5ߨÆà gk²¯ªÔ]ž¦Î³‹Gáßp„‹-‘p¶[Áá(á麉ßÇü(•\¨ŠLª„&õÔ�E4ým5m�’ußF™Uæ¿ø µéµž:–¸ 3ü+8ã»°µ[·ãµ«ãó,L¯VÚÐ�²ÛB‹ãÌxigŽ.™ºNŽæ“­ŽÜÎ)-_øªóJZÆ�¤ w¬°>˜7BÇv;‹8„ þ½¤!i5E÷j ãObò[}}ä BçÅÚØÜì¶ÚÚi/…VKU/÷ÇøÂÍî) üf ì$e.q+ŸÙÖ;®�+Ìàvà+û“–au,ìÄ«:˜F;³*G4�n«mç:-Aõ…Ácóƒ£û£'*~Þj�âÅõݦ¾¨õ0ïsè.þšŒsóütgJÒ,½è-ý;‰ìêéîåô½\ Ó]«–>~º^x÷ˆ°™}˜Qn‚³ª¾½/çºà1œv“hÀ¹š•GÛ<Á¡+ô@.ÝÿZ—)#<ãÿ§3,µOÿ¨QGˆi>Uá[“ý÷ö¶¦B¢¨^xƒGTa3šóÜÔOP�s-ó·RÍ@Ñ^D2¾0ïó,Ї„]°n«Q'ÝN]O%þÚj„¹.{ÍhpŒß ó._ÈÑïo8ò`HÖòúàáÖZè—™å?”»]©Ë p3[Zx¬_úÃBi ±“…A)òp:MàœtY e"ÏR‰9±CøÂŽt«h¯£.k<÷Yø(¨Çœò>ghÏìˆî;ik™ìéÄ×Nmÿú òO¨Ûü®—QºŒÁ`�üØ‹(í�¹¥wYšüï“tZ Þn™àK&tí�·QšhQ“Z{hÉ–Viwd«`Ž[`5rs9{ù-iJ•ÓËOq©ã_²9â>§~-'ÜUN-rß-ŽqDGT)aÁ:©Ð0íÁhv42užÛYË Þ¸·æÈ YbãÒ…—yuåvå‡õný±rî{q/ßßšiÏÚ.µØ�¨äVŸ§�÷ã~UâÊ��`x(³9êy$È]®€C�®Ø>È¿ÊM¿‡±/„ÅŽwT«ûàˬû2«£>Eçj81Ù%¼\ƒâKh?MmsOóCôµ\S%;Ö]®`æØ;’Ü"ãî­$æÞ†©#½ƒc'¢{­r0*àìêãâNåsÿ2û¹{sþO]^Ol�W<›2)&xœÍZmoÛ6þ®_Á©ÀÚMŒ­(0tŽ€6M×M,Ò~*(‰¶¸J¢JÒñ¼_¿;¾È’MÛɺIÚˆâ�ϽòEžþðæòôæÓÕ)tU&ÑÔ?ÍáQ1MIVP©˜>‰zvôK<�~ÍuÉ’Ó’*Åg<£š‹ú5�ÏY~V+V¥%›N,O4�8Y©ÈW(ù§Di 4ÏÔK²_0GSJjZ±“xkœL'¥ÿ|0DÓ†dÈt/ÈLÁ¿#xÕ #9ëÁ›¦ä¥0±5�4(9!–ƒ0ÇBÄŒä,ã -ƒù„´Z©‘�ó;�¬RG/b+ð±¸ö1,D¤²L“B”¹Úœ–ѬˆÈ\ŠeM “E«æ’BÏLŠŠè‚3å5¯ç$§à|^“FŠFHm d‰`Åä�ÁA–ŒÏ ­žùD5%×2B1'Ó‚Ï/ÉõUjòäêéS‡7á,ç™R9t&™*Õ„Ý1¹"5ðr®=«"éŠP Ñ9¢äZEVÁ¥úº¿Z "ꔦÐp¦Ž£Ö=§’Q ^©Yr]8x3®3¶aKZçŽúø‚éBä�=²Ly*¸Â“,ÒŒJÉÑâ¨�.& :qÃ}ÉKâÌË‚ƒ9ÁYÖqFE0[¯H*D(È­ 2ð  g<ªžE„ª/„i-ÿî8<Ügžðv“B„ŒÖ£š’f�A’é…¬ÌÊ•3·â%úÂÅ•Ìb MDÔ¢ª  ®ƒâr#jÚ Ä4:Ce׎›*-E=O®Á´T0Ýtâº"B J’ÍNâ �Çköù=£Ãø+Ú£®ã oSШ+!ì"+'L‹wºuK¶¨šé½Sìc�™ö‘·'¼w˜R!­ôõ{œ¬Û¶xKO ¹B*jÍj­\Ñ,ž';LÔ/EŸ±^0iÒ§Œ“«¶çqÏq ¡©Y´L¦:w¥÷_MðhóqÙqÒ6- W¶qrø]`ÌB±ÆìÔ·~úÑ¥²#&€ûÿï2/N\#ü¶€À¤«Ý˜|Ó¦½´Ã*pã�°ÿPì2Uo…4Iˆ¡¶ÙV+ÞNÓ³å�*|Œ“�!�W~½2Ëã�ˆ>Åɧ"3ˆ”4eå°± eÜšCpºVÒ|V™ …Y©– rma† ø6NnC/·73Cº¹å•°ÇÕLª8ÙèFaKõ¶…%mHÐUÇlz£#XR¨S¸—ãC‡Ák^Ÿås˜}+h„!Ñx½Qì»ð6ÞÛ—�æÿÛ0ÂZ¶(5hq�%˜––dœaGàfõ,zâU¶­­`r'ý÷`H!a\�Âib.$Ë/`v‡T8¯�h•ÈÖcFR¢�œdhWÜt¬ßvYß_:^rG%§ö`0Øg5pÈ`ùf…ûHpŒ2=¥Ýµ(82/±£4Â�è4Z·jaÎelàæBbÞÂ#¼yBÿd>"ÚS�w>ø7„õ‚«þ É€sØ��9ÛÆÀ§�òa�çBû ÿMBa¨ qÜM4¯ÇÄë’9N\cçí)Ÿ­HÍ–ÖÊæâ|DÔ%Djœàß=xý™Ù0ûFÄ‹·dq‚ï�ÙFÆ[Q9ç°µÏ{`¶Œc›“.½òÝ}ÛÖÛUdìø6Ðß› o›ßéþ³qOË�Õ939Ð6¿%¾u.\Št^–'ÛQ7nò´§°ó ÊBkü¦è Þ±(Í+Ü4 R¼eE�̉®3œ:B¬+Áºü¶µÐGbv†÷8Í7ü]ȈA˜mp�ȳíJ0(V›Ý®ñ0¬˜æƒbõ©Û6†×åð°¡ÌdµÐ�[ún’î§ß/ä!Ø;y›®Hc$8=MÛÞÄýXqbù‘ÒØ‚ rr‘ >¢HBxœí\oÛ6ýߟ‚ó€vœ+P`hkš’4躮E1”D[\%Ñ#i{Þ§ß‘õË’,ÉRÒ4†5±É»GêÞÝñ™Îì»—¯Ïß~¸¹@¾ ƒùhfÿ!؃B"1r}Ì‘gãµ\œü<žÂë’Ê€ÌÏ,]PKÊ¢x¹$ÞE$Hè]é3O̦fìh6�m:ÌÛ)?Í…„‰BRWÞ�°@e‹)yzÔq"×:•N½ÁR:ü�Íz\û¨¯��’È-æñúäTÏ�’u”¹,Vô[º#Ý(ÄÂvec™á¥êÌÅX¶æÒhŽ^r¶Ò¹PrLuø@Ôb´à,lºotBèÚð30ªrU·X”‡ëþf™,ï±´¬|±[·òØ]Ý�ñ¡½ë¤œ¨‹McÔŒ>6Ç¿ÈÆ¥5¹–qZ½^‡¶ŽžïÜ€ˆAr%¦4ë?ââŸ5{~}Ãi$ã_l-¨ Ò¯œmQÈ81�´C¥~ 9&�"oD¶ÆmŒ ®`ÿF{H¦?yíˆWŒë}&ü^V;îShÎu½Rõ6³£Vœx´yÚ�‡Ë©;$—ﻕÄz˜HÍ|,"ˆ"?¦ë_%¨,µú£òGk²Î]§þ͸ÝiRéb¨O#G“¹¶&$èïëÖR`e“e¨wéîtbi�rSŠª±Çòó2ËMcð1› áxëäùü†ˆœŒÉ;̯òÜ%®ž{€ =ä�Ø+RF• 2lÞ0Ð÷’F&´Pˆw:s GF%VBMñ™o}&‚Ÿ©z¼Cbéúºwé#&!ãF; Ág€¶€sA¥ÒZ`ÍÎNóQË-“ê÷úˆuj»|½/=¡ÖkbŇÇͱE¡³K©1¤¡ ­ µÁ–øß÷•Û–Qº/§½…·”Ù'²¤GõŒó€ô©(“LE0ɺIÐwªÄ[6>©±ÇÖ’Í3Æà�Ô‚ ÝóÝmlÈÑE©Caè„ïP-Pv†¯í}ªz_¾ÐZÐÛ·Z0h-0ɺIÐwª!æK5­fô±õ ̦kòN*B%’Û¯ í6å.ªBG„‡ê‚±t Ç ù>ƒùK-ÍÐ}+ƒ›Á›çMœ›v:ovNïboÞø�”·Ðz÷AU¾yuÿCÇ~tÖj�?Ex?TÖã>ôH£¶Ñ–tgÇe }5™Ð«Èš±:øÍ‘:Ç·!ªò¹G�9rY®(~�1�{x3ºS㢅ܔLèUsÊXœuŽ{¤F¥š245.Ê–¯‘&poFwj\µ:}g¦ôzÏÙœ õ®{¤Hæ`¹ßfõqÚ¬ Œ«ª³æ×ÈÅM6ä˜+&Œÿ®—8r”’©Çr‡†«’‹'yÃI11B\vñ¤%ª9º’†êÊ®¢ Mæ*¾ìú©®QOuz ŽÛÓO]³fÔoñ5-K§RÄû÷#›Ü‘(±�Ô ÊµTwÒäýâc~t+ ¸snaÑÞM�Ž˜Ü‘ÇÊh;!�Ñ»†�5ÝŽ®½+E(Ã_«ò˜ÜúŒ¯FŠü½Oêý[‘7^¯å [œ@Eµ)‚ª/•,oÛ�^˜ü�gŽ |c :f8¬ª¶³Ëä#É&>²�@tÄÖr’ìüý²Ô–Ê Zw±Iæqñ†hùo¾CU7Iú%å±›®Ü ÔiãN£GkËñ˜×ø:q§cÅH®°z#}*;»2³ä²µ�°ëözÅ™ƒP¹ñjì³´«Ð°ä°"݇“ËN¼å£º§¹6rPâ¬G1(±y»RPÎí1­|&�¸9 Z:Ü{XåHÁ-µeù´„ìœ1‹ü�´[B—¾Ô¹Å¦‚?Ó$jGh‡¨ü¥2ÑHAÈа½ZGINQŸÿدùÁÿÓ×Õ5zü©!EÔø.Ü$kù[(oCw]3Aõ+Þ«ö•ؼ]å+çv˜L $°Ã™ á´9Žì¥„X%È‘¿ÀýäÓØ?«ÙUän<.I�÷P8†måAÕ¸î\k'§eö(¦e¬Þ®”VpÜgG_ ^L™ €¹�úKšúu䮿—¾W§'£ýŽX(XD–߇C®2•ºª®t>µ×éÍOòu‚B¯šÜ!gƒ3³žŽàÀ•Œ„§îiØ\1ªc/iS¡n_Ú�"-ö…@]ëVñê�Wæ–Q†åšÜjªâ94­©í;!0¤¢�j¸’"Y¤ïT…u5z¢ÿò…®¸Y¨o *x! W�wþ™ø,ðTvYØU&‹1KͶ œ RT1aϨgPÀ‚FHÀ{˜{È#j€çgLЄ3´õ '©@¬^NÛ‹½6âáö襒{¥¿˜ð¦ñð™š?ô?œ æ HÝH« @Ö[ñxœÍf¦‡îMí–·Äi¯ß…w¡¯þðú‡Wï~÷­XÛ<»N®ü?J¦ðO®¬‹µ¬Œ²/Nj»<ÿד xnµÍÔõ«L£—z!­.‹×Ú,*�ëBVlªr£*«•¹ºàÉÉÕ…:/Ó-nqym,¬4V/Ì31¬J®¤(d®^œì^óøÃÉõÕ…ÄmžîÁóñ€øfmÄ'½8Y-–þ;‡Ÿ`,Êtß‹>óÈmÅÕü Ía[Zsu±A¸×⇹QÕ-­wJ¯ÖÖ$<˜ê[¿mnο>áù/EQçªÒ ±(³:/Ä­ZزwÚ®EY(¡ [ml*ÊtmT*–0o©­ÕÅêY"ìZ ~�GxãG9±Ò·ª˜ ˜®¾Ô2óˆÍ„YÈ ÙR˜:Ç`ÇÇB¼_«^U´TF¥E<Ø$‡ ßY±–†¶¤WårÏþ‰GÀ¦™¶žg¼•6 Uáï Äæ¼,²ícG· Ü8)ø8N >N*‡¤à]¥RM|L¥•# —¶Ò÷ …•ºRIëô¦He•ê߀U›d à·r±N¼ð®ne…Há?@L˜o€u†d©Y| ÃZ Îf  PDUÞ%Ý%‘Ð�â¼Ñ‡µªážQ@Ú@RPLQb«2RXÜyFSJFH· I™&žˆÊŽÐºgÀc/Öe–"¥2u«2ƒ€;ÁžT�¤á7u;à"˜ýYm,ëZÀ÷x¹ûyœÜý<©Üý<$w´XdrÄØ+u?nÔ6aBI‘•+Ø0CÍ4Gd;h&r�¦@V•Ü&4 *ËøŠ`3‡W›†Âh‰[ÌÑž-QâŒ`C’ ‹6Nº`±þÙ‰µfÓò ï°h^�bâ˜\� oÊ‚xƒqÚ1±õy[ç‘ñ7ãd¢³hR éÁîËËÛÀ‰HÏX›Òh èiÕ ¹,³XK"µõv«�< fÛÊÙ8ä5ø͉FÊ?Á)ë¤<ü3Èã"ã˜çgOʵhŸ]8Ö;úpË«ö€{2SÅ ~ËX­:ü*+½l³Ž–¶ ,z‚€¸ùóKK'€G;ñx�ã/ùÀT•oìv挿üÕQf–ˆ»µ*yp’bÉo NG ª(‹sá„5W²0ŒÚ $$¹6è¥ð40\x”¥Àž�J§·ÁMo-š%SÛŠò>KÑœØÛ‰Èøû¶�RoaæHÖµ×LÊ».èæá@‹ÄÁƒ:ÄÄC'{—›~§–o—4Ûqtáæ¡ÇǬû’Tøq·Ö 2M[!AëdµS&ÖÇo”½SªøQ¯r9ާñŠI9ÚÜç§?g¿gQu‹…r>ó–^©<<—v}rýîêBå×çóíùŽ!ï‹{#ÉîUØÕ±›±!Ëš9/kvÚK°Í„f.ô%žoÜ@.¶í‡Ž‚•P,TéymIp>à¡ÕC±øõ3#Ù=·ŸÞ|ž 'GodóëgñB'Åé›óË3Àö ‘‘TiÞöÊØª,V×N8 Ä`îÜÃE‰¬gï0uá”ÂEй…� ? D)`xœÍZ[oÛ6~ׯàÔ‡¾81º¢ÀP¸¶´Û Äk¶dkûTÐeq•H•¤ì¸¿~ç�-9²›îÂh#‰—ÃïÜI/¾yùæâæýÕ+R™¦Î’Eÿ`´€Gà %yE•fæEÚ™òì»t톛še5Õš—<§†Kñ’ë\ñ† *ÌbîF$‹¹§´’Åé>É´�áÚð\?'§(ÀÐdA‰  {‘˜f‹9EÊßžÄÝÉ¢%9y‘VÏH©áß|"…\ll±:Im‹Ú9‹y‹t32ì'TÐz§¹vˉ]ðM�£ÑgÏRGà¦bÄá8µ°àfÞ´5k˜0šÐ„'0EäêO–›ÙV<¯HNi+xnˆbº•B3�ˆT lK Šï4kb0?¾GÆQÉâ<ù*h¡$ˆšÖž ,Æ bdÒ�ß`ÍÔÆÎf…‘0+¹€‘k%»V“Åi@PrÀ޶JsÔSä9!¯ aðÞPü!/-Œ€5™ÒD– ¶²d „ˆ4Н:»^‚¢ Œ¢¤¬°©(zy92®( ¸FPW; /»Ð1²å¦²ÓtCë‘„5–L.µHóB1À º¯U8ãòAq‰Ÿ\r“[éÇ–�Èí¢Àåž›àÕåFª�k~;¶.³kA–T¸ å5]Õl "f+II^s†’G‹®AYT¹Öݬ‡óØ5?öhf}sÁéúrÔeåî{[ͺB^Lm¥¬µr.7Tq*r–+©ŠbNƒš¡ö½%4N+”TÀ�jk*ØybQêhÃxÞï!„ŽIè¿öâšÉþ�1³�yÒ! €i`{y2‚nñ¤ýýI€ª"W‘¢ÞY�ô+hЇ ÜIJƒ_�æÕ„ê`x?Ѧ¡ÁÆJòdfÑ1_ýЉ[’‹ Sƺ:˜bWƒ XF*††AH±Ò™+¿£í­ü(† Cwdú_Xc %x2ø›1@cFÀ}É‚5™eeÄE”�û­ðÊ\Ì¡ÿ¹eØ5 û%¿bÛŠÙHâ…`Û�aÐe9…¤ Ò²œ -H[Nˆ0ƒT >W½bQ;¯csF0ÊÞî dqV@èÚ)×}Í׃n×v)×/™ö ÍìB²²Ô}J@vὄ†"W—b�]3g­%$½¹oJ�Ì[Á¬éb ¸Ü‡Kð:dà+„G! ¤ÃØ‚yØ’ŸCŽó9Y ƒ)Êgàêi(�hZ%Áü Æ\.Í®B®Äžb‚‘Ÿ*[˜Âgñ¿AþÑñ¡çoÓì­[Òç{\þ¿ÙG|²e|]!‹Ø74ÿ-¤wiön Ò•KOÑ1{ÇBó>ÍÞO¡±S$VG“Ì/]3ЋN³ƒ†)œ0d…ÕQ9Jâ±ÿ&·úw(aÒ¬›Âˆ}®@ªĻrD¡Ër"Ýžh;k@ö ¤2�fãïI´Ø1ëò#D–h¨`f˘°!=͆_Sˆ}ÿY_²…¬ ©FñÛX / ±­¥Â~h“ÍSl¼† 9w¢·UÚ~Þÿ`7¯n[È¡¬8´Ÿéö“vd·�,¤öóäl -ÄøÝ"„03øš 5¾?x�Ý€G3*ìò²ß¿Ÿ”w_ÞsW¹©Cü± {ï=ê¶om1zÇkcÁ[vi¶ìŽ'ê†ÑxYÏ©i(V'@aÏ9nÌm\Ëã…ã—¬64x|š�¿§Ð.¹àM×;’Pã·öø`ŸW €&Û™h:çÂî/Ò¬;~CëŽÙýÈo²{7þÙºáè$ƒ½N:ÞøÜ庩©š> ˆÔYø¦í´FN�ï.À¬™Âª/¼~¹ w[þ|ï­Dw+cO»Šh• ¯Š5Fçþm²B�X̰ón>Œfú˜o¯ÂIxÀ¸a ö�®®&²µ[€™ÏÝ;Øß)FÖ°-±àÿlO¤z´oPã}òsW`è‹CŽdwìêõR�hXÆ{"Ñ‚íþˆ'Ÿ÷ÜE?:_6vP�>Š ¡#XÚìãÓÕ¤ølv2°¤®d]„PïÏhó}Ê�Xàõ þ�.´�ÜËãòxÛIŽ€}Lhè€è&WtÅknv·ÑjÏ\*v£ 'Àú ¡£ï)ä¡ÓÙj¸@ü׉�X™÷o÷<Ís7<ýQÞÒ}ýçxCÂ_ÜV¹=ú<}²#¤øÌ”œ XþÞ‰…!èÄaÌ×éþÆòȶ¯Ãug+9â·7�ýqÓƒ[С’�‘RÉ?ŽÊA ÔRƒFñït`mÚÌû­ËCZPÔÞ«>ü Uk.ÒÌ=Oè¯�Rn ãæKWáqX°7�°©J³þm:  þ 5/\`‰ü ÇÔ@6BчüËÉhxáÆ<ãÁ½Gšáß{›¹3:ØT…ãÈIëŠeûZ�}L1pý©£×ÈP!W`Üøã»“…ص¿Ï·ÛÄXgÝbÀY·GöæJ ) Ž3íï¾h*˜Ðøô²ÞÃÄÉ©[]õIcøu2s X71Aì·èûó^w{Œ:á‘Lßb_úà9øøŠêµà8ÀìøÛó§¹ye½9¼ÞÓ¥—6#‡×{°a³óbCÓ ³8Ò,¼NÆ'è|8•P¾Ñ•ââ#$2ÿv< oú4ìnF%,(‚Ž*‹ü°%šû:ÎÝ)ÿ’¬é’™’· DôБ‹BÛ%äl«%wÔ°s‘ƧŠÝn\üú¥–i2·K™ë ŒP±Èj`gˆ j¾´ÂµseLàoX¶¦”Šdä0iÉï±i¯à%Vàº�x¬¯1¸tR+·c‡¾¹ûB¸Es¬,šuã�Ö:ÿÏ2vˆµNJéÄç£Ï›}½¹‹6‚ƒH‘p€‹áâ)äd’SZ vh35O¿½ßÀF*}ˆ2¼vR®îÚb�WÒÍ™(y¤8Ê<þ>_…Í èõb ûzKÑÆÒå+ë\Jw É1’"Ùr¬¦M)—a":2<�VL¢¶„©n¼IÓáÛb^‡]Cž¥–ª·Aœš˜ð4t„Ž5ó§µ–Ø=ºäçV÷(µŸ E£>u¡¿ÔÊÇÁ£?sÎûÚgô(¹@i<0ƒfÓ“NN�… �jYµC*€»Ð*p4Ä5Žr3"G-"šÇz^Ú«’;Ê/»,³¬¼ó½Á3G¾«f™¾¯ö›˜³xf'qÚŒ|ï24¼&xÛ:³ÜÀî}ŒüÐLÉì aЗ×ýfä)ο[— Šf-7žYÞ¬;„ÞÏ|¦ˆS^;œ¤uþ ùüì‘zõzÖÇ“Üæs§ ³'ÞóoMàÆ9ø¬zƒ‡ý𓤇Ÿ&§µX?mç5—ÕöÄÅkx¥Ô®_œüË“€g¨E­÷Æàίã©O¿öSg†&ýs˜¢ùZÙÞT(¸ /6mdXM3]@ÿ(°À=ÞU§å÷­AµíqëÞó‹J—²j—}x'}*ÿÙ�Íý�O_bî/ilŠ©–«‡PäÒQ¤;›xx ��Òá2Ða‹Q”ña§3l!údòÁÕ¦,3®KF鹨JÓ­\¡/é‡g>´sQ0Ç´f﹡ܼ¶ÓÃÓsPV�e'ôò18Üitð0ã±—9ª?Å€ƒR•ï�ÜÔãŒùM=©%Gp» Žœó92ëôæ+à„2Ï]Š©Sïx¸ƒ?—ó:³š¸o�ÃEYåpxBxΙ"LþÃDÎ"µJ€´ûÌ…ü» ¼v®K“–ŽejW~ÌåEvå²[Ulj½ÓR-—c=Sš;­Wê@HŽè…V…ež.æšwŠˆã p¸^Øfí‰ÅÀ^ŽÑi¾7;xæ�ab«›åO:Ëø=ƒ³¹_.WàÌ% be"d•Ëe˜ÌWÚ¥y<À¥¶ Ê&ùI˺X°ÜV,|‡x¼Vp5ˆ&X¡·˜sp+WsÃi =ýË_ÏÜØØ–Ž쩞}:‹0@F˜.BÄ0/kl^ÙŠ9§4“–�f¸ÚC#£??ElûâK­²´ïˆï4Áç±eäcG¤¾l–ðSRÛ8VÀß§Ú¿ü0”§ã |Ú ™cϧxo{ ñĸ¶Û.é¦Ç=ˆáøo€¾Jq’pˆÁJBðÏØŒsßHž÷Z<ÊËpPû!³FX€ØéÇv'w*Ët;PòYl[býûé´ÂÌ]RI]Ew¥«¸ªw­QÚTfBC\c~HKæµÎìa`6ä`I¬ŸªF¸÷ ^«ÌÊ�Ïw"´×Lz2tA÷Oˆ0ày� š)°M€²trG]¬ImØl°»w2Àã¬=W¢ƒøÁÙ$!¥mV~U™/uùÜ´NàØoª*£:KRp@›qE‡&µ)3[®¡ËëdÏp�{ȃ�“,Eéýã–é…£HeÄ«"#¾ÇÁ%|{… ¬×tЍ}àRa!0.¬ �14Žæ%¶Ø8ºº ¥éîºÙÓ:½ènñeYrü4(»ªÁfë߸¥qöË0[zî’|‚l¼dqÑ1¶ ”¤³ª„½ÝŒ�­‹[l¥æV[±'\œ 1t“PîP¶I´€e"ôp„E�†ÍØeÁY(Þ&Ò(,¶eR{—¤“ h"£~pNr‡â)µáæd * âRj·Â3(ªí ØÙ¹Ê°ïÏnKì¥Ç k”)'¯­ÔÊ„"Û‚Û—Z²ëÜ;h)Ê¥+kºGwù¹qÝ^†�>´ãw©wÀ'bw U\MМÏ:6Ýî,׋2ßÔ–ê1-M0‰¥è�’®Ã›gžý"”ðï ò‘ÕÆOO†¨ÕW;o¢«%¼;x¾èz.0 ÅYØÜоyƒŠQ~• IT :FNWÔUCDÔ/\˜Xq$Ö;—`¸ìvÖœQ­\XÐBNÙ½ tún]“êJ-¸É‚L;R…¿‘z}|E÷Ævf†éÓ¶ú7Pµü3k¢Ž äÜR’P g•�tQ ;P‚[½ý¾™ÝUh:i†Ðá™ eAz8MÝK¥‹oÓÕØŠ™Ÿ=mU:Ð?…a9ö«ÏãKÌû½Ì¦UjЇœkˆ„…»p]óª0”P–î{ÀÙ5W`•o›mwÄ Îq ,+·îé33<¦†C– ΞEv‡*@7€\SgY·#ÍrË—I„r½è¥Ã+˜UF}ã9Þ†ê£ØJÙ(<†¥ø\”sVõæ¾Ü½6S·¨þ· ñ¼yî¢�?+Sgv¤ª3©òŒØm°…‡¼Ì¯[0ˆçÈ^­òƒ/Y2È�=-ƒÞ’¦ VV®ÛÏI)º“�†Ý†\2˜>TN4ÚEEÈÚ–9EçŒ-Š£w“Ú(c@_þ˜MÜ)Üæf+Üõ€²XÕE#ì¼I#ÆÔ­è½ ÃK]deFf•b³4eJ)†Û—™Vß?z¯ÇvEí~­nOJ×pôQ.“á€v¶3œ�Å™Ív‰7Xë]YÏ=ó(‡¸«ŠMá‚ À�ûʧ­¼üs0iMÞ‰–î®›¾l½D+|Ÿ•ÂäÆ쪺¨jæ#Á¡È#ᵜŠÀs/ä`æ#ÆZÆbƒ^"ÄÀžîsÚ�I.±²«3*èBzM¸qÀ‹FEyW¸`•"Rw¯“^*&%õ‹*ÐEY¬à¬€QÝ‚f¦e´¦ÂRUJcC\épÉ©fUbGº¼ «”øÜ´»1áÛ‹ ^ ºwÕç(›¨ý%[x7µ�µq�Ç j·æ€´¡Ÿ‹†1ÓBÝZe`p+©ŠÝf Ѥ·•rØ�ônœ„–Ãq¼A:"+8}Jpg>pâÌŸ0kð0>\·°å�¬RºßÊ�‡ü¾ˆâ…1L0 .ãgÜ¥j—¿ UðP5á†wSF9_L+®˜ìÜÖÉnë'}ÈH4¾>è¼™¡×Fòµ,Á@ K¦9Òï íqÙ”´˜”7ô¨7IàP¥q¹ÄYÓâWÖà.R:¶Ã¯$د#Tk§fq¬6‰Q8pD�húòÐà)NÕ Peè&ãBÇ¡fÁ²(ÔJRúÚé¹è¡6bâj(¥ÖT岞¼[Ò­õ¸B?•|Èþç—2ƒC•3ÍCqV§È¥¬´³ r™¢þµ.¶Ä_zh¼Ô‡™‡Ô`Eý´­'À­–¾+E>¼3ØŒ�,tëUZr>ѧDSÐ�ßaXÉ»v�‰å=U…òèfÕnÐ)àiÊ6™º¢Ü¬÷]9±åŽ®^  Â›ØÊ`á8v¿ÜN ; þË%`XXÑ.c·ºº…GVuÎvWC« ¡êH½Dqaž´ÓœCÈÍ&Ú²-rÞ®À™ –s¹–·È�,åÁZ^l?Èüb :5Cæ€sB쟸p…g¹woÌ †²æ!�G¤ô£ÚްŸaÒ¦#8Ôrdˆ_7°²êÐá ¬©ÔŽÎ³…šw�âC .58‚Mc9LnõÇhÓâ lÝe4\Ë÷GX¸Ú½/ܱÆÒ­zž¡{=3§dÓ´aQ¦j4 _/‘Á�ÍGPðþÓÐl'"þÃgÎå~|Œ€ÂN$cŽ.""4ýAxÐÆ™2× Ô¬Ž:e|ó~ÝÿhHkçd`ë^—»Ñí_i€m¾Ùb�g)ëÌκ /DèÒ¾o@‹?¼ŸbKßX÷d`]³ ó¹ K”\ºÚ8F’Ä`¹6¡�踰¢#‚ì¼PR¶žª—)Ý®p1KÓ�-¨GTii1½à6Cšíh¼¢ÒC9ÿäzI_pAßä†TË·�³tw»t^ò;c¤&38$æÊgA(¤ìhóÝ nÛ}¡# 6Zô@ÌšT'|vb-æ‚®¡»@°)�ßU[aõŒÄQÚh –»Ê#‹xÒÈøNôæò¹oSÀœÁìB)þÒÑç< WIg@qËå“Ý]ÜÐÝj«�ttE*:¬bÞ;ââ4 ì2+%å=ÉñÕY³Á¿ñßèªdê~FßÍðN)äû¤H�8þ`7‡À“ÛPønÈ·òm?añ]±ŒSü�?iïD®7ð þh؃ý]jµûµ=q;”ƒ8T7ÄÉ0u.9ÙØ|?…³ÝGÝkí^›ptRÚà}s[l•ÁãP9yÁ*·ÚÔ¦0ê‡ÍRkˉìÛ&"ÎOнÄ}–©ó±¬ý�f®Ë¼u‡­ÉöÁŠäþßwÖ§®¼- µÊ7š.¦öÒ…pГó(Žîˆ ·eÆ�|9jœ-}âÆØϼ½âh=Ä�PϽMF*.¦Ì·®Ë¨9¬Ž¸Ü²¨ÞWàÅâ�¼åÖZ3íu·辺‡A–ÝÐúM‡|ûp7ì¨Ï¸h@×4SþC9¼aphÛ®±Û ñEì¾Ô.�L¤’mè ›�ü[üŠÜÔVÊñ`þ-€abìRîÆ›6»•úž^�_ÅûA:³çºa(ô-žÅ0ï’ÏF4é×1uÕc­úRkˆ[UrÞwŸ†»ˆ?aT=|Áð�á‚ákj8¥îØ †OãKkäà'Îì®+k¾²&_àçPþ ïñ‹Óó§Â]5÷gg®íÅèâö»ÃqzïP8?vK“Ëû½ûý¨\Y‘T;dÐØþ6}A™¬V 6žÅݦhêéƒ.Yæb–Ãø“qNÀŽã(‚Ú8n¿{n7<7Õ¶ òÙH¤˜®²Ûÿß]ñhÝ»c¿³òœ£Ã{ñUfŸ‹'3ñÄý~�?ÂàÊ>O�½£In¶9¦çvߦ@¤f ÷‡îÇnøûÄ?i"È£ÅÿüòHT OûŠ0H¨ÞuYç2]¸Ïm_ð—½ÿN®žY»°³ü¾u5,GLk 6 ë³NVz kf¦¼¬å-²ÊafÐÇK»ð@ìùùÖ|(9Zý\Ôf6{ š…lH}8uè¢7!Ù2µÚ{ 07”Ù»-sŽÑk4B°¨XÕöw0¾‡éƒì· —D&L‚^Ì@QZ‰ÒZwÁU…‘ ÅsÈuÖ&ÖÛ3Z�0ý4’ãUÁ ø"¥ûq\Gqšl جÚ0!HÌ?‹±I²’.:þB¾pŽ6•y DЬ‘ú‚w ã— ö¦8cÇÚè"¹s ƒä˜4A♟Sh&¹˜ä×Ö@Ùóʼu0Phø7j£om„°¿'œ§b×Ê¡¼9’ Ãaq`\nƒ!VüÓiK^Tx1^b/ön»¡:—* ‘csŠ—ÅVÜZ¸¬õXNJwwš,—rä;#©–^ž‘c®”(5Å�0×^ áD ÆSèù†˜ ˜´ø;œ!ExE gâ˜v6pÊù ~¹Æ6ŽפP*Ñá‹eÁS`}�{%&jn¹ìp2¤Á²¾F1†ù¯–$Q(A¼ñßqwóAíBL{[´”Uú‰ãY©'†'päaæä¥oJòrã¦Ù'«8ÜŠ?®ÒðVPÝiàf‚ƒªôX´ \�ŸäÚ*`½»3…[¸i›[�“ÁÆÝ…U¬%rÙ3lmŽÚœÉª& †Ä^ÂX�AçjÆ$ïµM(ÐM“Å`YôÊîÈ jÒÊÕþ,Ez_~¡ÝH½šê6¾F†É/\-·«XÎû””4Y½­¦îI¤íŠ;êGÝFq»2ž-œµ—ˆlÍШ-q"ç˜|Qs”- qéW�ô››Û¿¿W€–¨Å‹I‰;(ýY¦A›+�&¼_�+ˆ|ßÞq?ŽÛ•W·°HFü&“„. LçrÍ|”3vV<î.H%‹ÿ? /ù?4ÅÖÌÝ+ƒËV1´Ÿ«b€C.LùS·J€]¶EÙTFZº‚Pqïb&^ÍÄÛ™Àrµû‘qÛ2V“(4I²Ê”€mâÒó´ ¯)%hþ4³Ÿ_õÐÈGªª\„Û}N ìRÔ©´ 3·Ƨ{ûPŠÝþÑükô_wè‡F&è�Øüþ ¯/…ˆkc L«p0Ý8.ô"¡oÊê†ùUu‘WV×ÓP½©?�ä÷îÿu[¿eÏr/�ývß;P]aú~ÑÕ9ÖëO�ëUX‰s¸NÊ{ý#ŒkÛW‹¯Ôõuõ>¨#ïלE4'…k=�`çz$H8ëÛAXÛÚ,S¼;rƒ³" ÅP\¿€k`ß‹¶ÔИ\½‹ðaÑçïǪ¢ëlG­·íéó;«dkEÐ`¥š±á¢mÈ (©SÃ6Í; áï�QëJL¸´’(çÞ»™8m+)Y º`ŠökÀ0ÃÆžö°¸W¢±A—¡xΆ×1᜷˜Ä‚E¬‰É6â­=?˜ï¼ÖBrÅ¡wU<ðéœÿ½xС˴˜æÂóØÃœøÜw킟Á�ߎƒù½Ýé[Â7–вTÌ+±7=\ìã*½«É.t +/+'Ú½[P>Ô�¤ YIä_…¯ØŠ>‡õîäÛÕ‰•c§÷ÿ`�<úË%ÿ3»ä¯Ç(y0S`�Ä$]€‰M¥tsUß(U òDh>‘‰È‡·!þ\~)ë±A8qjJµæhëq~Áá„­R泦A÷ùC æÊ›½Íœ¸ó üái†Ž}˜©sñÆ=[b5Þ.û‰‹(wìÜ ½è†ŒÞl}9�&3ŒH~AÄÞ;¢fÏP|¶…ygõ¤ä¡Ç×(Sé¹GîÝÅg¶ ÆjÞÁzµ?}x¼(34'ü]R–U&œ?ª›vž=wíkcíÇîpê1{0 çEаÚ´X™£ÃeòÝC€ß+­.é3ñ¾™Äûûí‚xßxôO?ÀÅŽùÏoýçMÇ÷EzÐöa[ úùŠH¤‡\|LOkõ¡)ëÇ´ÿhîøÊBmê-MÊûˆìIØ!-jµ„Ý—Ô¹u·,Äé–Gþ9ÿ û–ÍèÃÖÞIø€ƒØTù ±„JœjUñí™JóóŠà�f¯à˜ëñc®½cF6pi*´*Y,BO÷‚%N6áÄã_b¥ìÔó{�KÌ£šˆKm�Ëþƒ³ï¸e»[€�7½çþí#ÞÎçˆ è, óZ½öo‹… ƒN;ýÅ›—²ªIVLB€-_‹¯×vž?Æcqϸw ÂÛ:ÀF|=‚ ú5ÕÒÈH;n&xºÿÁé‚'^øÃ^v|žZc¼°÷‡£ð涃èîG%ÏQ �ÙÓµûùí 4®yžhŸñàAóìŒ%»úN+Ík$´³fû@cˆ—µ…b‚C›ÇO8ò\ÕV„GSš ×»þkqΗ^¥¯’¥šÆ8ò0EùTÏË}†Hìž“âi¹ç_Ú'¾¸Àͧ‡é ‰ ß º]‘a\‡ŸAa/‚áŠbˆ�,ÄZHqù[Æ…“&Vþkƒˆ’;A´¥÷4¡Ä,8÷p`³Ý;š ДæùSÄ©¬~£†k¡åFãÓ4±�•—Y7&¥é‡ÝÈ»ÁÚz|w†Ož°8ºJÑãÃâcØM.©}&¨±vºb¤PÀx®°ó‚uD)šÔYz¥(›­ñå•­bD¬l ª>¥'cüìëFV a‹¾—�Ñtf#v…¾Ãcq¦—KáùzÛïêPôck6«Æ€ò€,Ó}‹Lw‰±¦a£°/-¹…´?{0r lŸ7¹Oªr55€GCSŽÏÃÜj8lа;•òctù’›v]RÊãë�#92"ÿ_}h$†hžãÒ²(ç _À8ã·+&µÎ¢�šÓL–ù-Üô4ŒµÁ¼¡þZ¹ª"rÏšˆ¹]ÕL/@Àõ3^á�ÞrÖˆéÞÏÄôVLy5ƒ« ;½Á¿™b†�D°•ÎwÔÝf ¤Æx‘XVø ðפ€DÔ+¸™ h󜶓¼MÚë ÞóW@%¼×·wøŽmô[wÅÁËÝòt 1Á)FÔ þ«@cŸt"¸\#NòÆ>ç€QùÖÖ’=z=�Á¢´lÒ™&Ëìt¡…w05H:¡Ëœ=;­“iuˆk¦µU£Á€6ɉ¸vª6cjxfàZp?)OÖÑŽaÁû!|ªcŒó…Õ:•Â6š›>É~7�ÁÕÐþòG¶+›r;.JJ~=�‡VŒ¯Á‘c¥1;b«M|Þ´ëuS´óyÓî×MÑÎçM^7íó,h¹žú,h¹¾›8N¶ö&¯»§Â>—%vW‹±\À$WÞZ¦ó«!¦?½nÁDïD=žõèziÐUÏZY2ÈÍ”ÓÀB 8%€‚}zV9y0ýGÁ÷>ÞÇ®‹Mþë•xyïê>üßÇÿ½ºo4?là NËQ Z�Ëõ$ïõ97ع˜u³3ðó3œ=¯…ΚCïE=˜iúÀɯÌFéLñ´Öht#³lscêËÁ�¾"Âì(,×2 3±3‹mÉÐy­l›?<”÷Ú»’P«)¼²”mê,Uœl2^mÄÉ/Ù Nü%¾ïL|ƒ_×Ì_îÓ/ÄŸqÇMCÂ¥?]åÄ­BöÑkâøï–‹ïlˆ¶ãÓjzÛö>t£ WºíÚž˜§BÜC2r}cý À‘¨‰ãèšÈVSŠ<,U| Ä?eá„ «†MÙTZe éªmã êpôˆ;’‚\•ëµI@¶- \,ÂLvCÞ9“]ÛÖ·éîQQS�ÇÅ‘³q›ÕÖ%8!±T…ª°ÊÝô®“ÌÝøÒ*§oŒ1‚–`³VM¤OÛzw;$1ãžS¢Ç䦹ÑDa2ôè%ð}¬ŒÒJ·äÿç{T†{îÆ˜Ë{àùh ôÔšg£þAeá:¨üw‹¢g¼ÝìÆ«½­tyÓ¸ õ‡)FAWÍï6!ÄwŠÞ¶H è¥n I�TM$9 Â�oG.Ï&'®Üð»!Õ#•g#)‘))�;¦Rɳ�plŸF8¥s…øá^3à!§ðÍí@y¡nA —“{B¸áwØÂ[óWYnÛö ¶!URúçn©ò2¤ÊË�'Ð}ªäž‡P¥)–$¥ïšxw›°6. ÒèPP±q¬Ö&ÇÅù¶�Q®ŽM-¯F/”=¥Mâq§tÄ¤Û ˆVʵٞ:ôîµãƒN%ÓŠšÓ†¨®Ë ì¸ ì‚šÒ"“¿oð÷ Œ‚GXë†ådžkŽo|‡hî§®)ÓŠzfv€6”¹”Ø÷qf|+‡;j=gÇ[²±éI·ÿþ¬®åµÊ¹h «»á‡ ýù¹·Øh„g‘fªË¿Ÿ‹Wò¼£–zœ½@šúwRø›ð«Š#rB­7êû9®ñ søî7²q�œ;1;ߺ}Øo_½„`ͨ7çÒt ºþä½ÛÐðã{®üg…¡ßrùË„ÊÚ2MŽ1‚;�t¯øŒµ©÷<üM‹}>Â¥ça+pËAÊuºà»…xK§ˆî™)HêaY¥IkÙÿƒïŒW0_ªà7ƒl;€f ˆÃ: rz3û¿ß Ý|ÕAŒ™wЇR<"­¹}Z3n ¡y FxHÖ€â7—µoÖe¯é)Ó"<ÅxÐa$Ü‚™„a(j~´±âÈ4'kÒ´=gKËÚ�]°Ù®4‘y©§ŒkD~|ÿ:‹A\ ÈmûÐàðTO°á-æ~†—§Éaº`K;ù?ÑË o~ô¢É·~3mñ: �üâ£p‹°!1v‡§ŒÁµ²oyfâô¡÷†l†•Sí×FZÄŠÓ�»¹;˜ûdé$\¾ò�Z�N|kÓ ï}º Ñ{xº¢,Ží÷bµ'ô<ñƒMH ‹É�mŒH«šÂ;)%nˆÝ€ùØÆe·ÇqhD>Ó\�´RIí“*&ÌlÞ 3c‡ZXf"&§ɸ ¦¯‹Ý̸h9ð«ã”¾N¥'tgÙ:“ÜSx„B'<íÞǯaà1 ƒ ¹3µoˆ‚/=ðqdR“ÝÜæ¸<Ûù蓈Ó>!„ª7,M!âpOj°‘xÙ´IØ ZGozCZ Ðe¾.-j—D’Jb“œERæí×M´„¡Wl*T&MœRY%ñ·¤áf®¬…¿­”¿¾ÁzH�'(SS>Ú×)” Á/fâÈ1ï÷¯Òüvª“ök®êU™è]°v¿ÄgêȾ#w2/“ þ‹;žGÿ�Zž ³bAçH€îxœí=koG’ßçWôép‰�£,+Àb[à(ÎnÙ1bŸ8‚æL“œhôôŒd÷ã¯Ý=Ýó ‡¢;{6kIìGuu½«ºxöoßÿ|ñúýËgbUçÙytfÿQ2�rUK¯d¥Uý䨩Çß�Àßë´ÎÔùE&µNi,ë´,¾Ou\¥yZÈ¢ÏU½*}vÂ#£³³â¼L6¸þé¹®aš®ÓX?SV‚)Ñ™…ÌÕ“£ñ Æ?::?;‘¸ù7[@ß2`ø¦¯EŒCž­þ&�Ëd2‰³kY�—óßÎNðñDœÍ·Œ?;™Ÿ‹{4çϘñ ïù·ûg'´ãÙÉú�…æõ< àùEéuYhõFV/àV¾0èe•7™ü šp‡ð¿cøµþTp}µRY–®ˆ wuæ×2k†/ó\\TJÖJH± 43‘&B×".‹Z¦EZ,…ŒDâ³¹,d¶Ñ©9LËD¼c’^[ÔäúøoG ïÝ;ú!2�›D¥ê¦*t‡Û²Ÿd&nÒz%üU…Ô¢^)±®T’ÆuY‰DÖpJY$Á~>p<#(“s•ž°�VÕ5m¬EZ{X´4(¢³ô<2¨xgÏ�7ºsº•;ÌÉzutþâ«:Í•~üòìDåç¢hrU¥±€«ô#nœë¦& ÅÍJUJTå `#.«Šø<uB‡G‹Ë¬Éáçpب¤�ßEYE Oå‰laÀýñꨋH°hÄ"­k{ßHI_1Ón–IdåIBå‘ÕY�²ò~¯ ¹(u½ý"Šq!ýË Nj¥3�51ð)b„ê´ß¢ˆU�¸f¬Ù¢>$‚Tw¨´)Ò�r2[õÑ#Ä� ðnÓ!xâ:Û 5É'Ðqi­íj:œ§È,sµfUZñ$o #ª›èñ!3�¶ø�]Q×g5É%�¢�&dyâóybÌÏ4ó‰Ðe®ðGsUþw‘¨…l2°j:°Q°O1z¤,÷HÝ{wF÷–‰{W÷Þß¿o5ÄDâyY¥eµƒ�-q 21£M�uUÎå<ÍÒ¯ ©CI�gË”…¹?öž?}}ùô;gðñ-O$œWÀEêu÷䓿ýÅWÇ �Òo©) °‡‹$Cª©„ܪБ‚,z�99oÒ ”¦[ΟN|Ô:Æœ\¯³”±ÁŠJDGÿh<•ö~Õ¬‘`INDyœ5‰²,ÉxÌÊeêäÌ~–×á‘ûDçòcoüð*ÀXª¿t‚-­7ý¥uº,zÃõ&Ã`ü“HDëÝ1€éØ1¯Á Ù×”Ãˇmá5p®¬ú‹VÄp¬ïWUxï¬]KànX"¼Pr/ ú�*eµçñ~/÷¢T‹ÅNk|÷éȉ2df–/Ý]zj \,Ü_=Œ/£AA:úÞdþÆÜ.éA¹Àý�‘ùM­jßÛÍŠXrEË¡ä% �N²Í®H 2›.Àì‹&#aÜJŸ‰hý‡Ìs9Ñf`ôõ ÿJ-ÁâªÒß™£�sꙕV�› 0ä“üN [ŠLc±,À[.¯X…3rp©äe»N®nQ.Ð]I®¯ ìKD�K¸,°Ú×0K§¿c$å©!N í‡]3¥(»g³JB¶³N;³PoáÝe€Ðqn>Ü$»þµœÐ ðp®mÐhµ,–ç¯Ú¤º|$ÎNÌŸ"!ΤXUjñäè9dp¸_/á|hÛ?ÀŒÃ¿}Ąãs÷#ëÉ…çm¦Dÿ‹KÂ3Ü´€0å°áÇa;ëº�° ÉÜix¿­� ÷ZGÈÏÅ ç�~!S†‰²Ãó5Ú;!#ÕpÓzUfÉä0n |ùœ8k‘’èGÉŒBpÑÇ)o¬Áþ=cÀØêîðûÃ6ŽQÚÈ‚›Öâ¦l²„àB°ÈQq˜sn¦·Þ Ýui'6F#Vå š~½Ñ¨¶ôJ®Uâë©ÄÔMu ¦8.Ùn ¢ yØq!¥ö<«+*­­ÈAx¦º,Œñƒ"–�`hxö¡Ã”ä"Kï✌7IJJרªâ²!? Mæ§Àr^^ã7Ú 4*>µ@ñ›)‰Ç.À¹¦LØ¿ö†�Ì=t±Ç S`$9ø*+pÑßxn³_k¿´Æ‘¶&3NC¸Áþ‚yÈeLŸú<‘ëë2aà’20ò|ýÒv&ü¹[D&­²-Ѻ-1ÔÛ©'®wn·kìºkÈ­TŠ!ÎiÊÄ >L�P$ΗçnÕQIþîânuÊw1û�Ûü¥ ýÏ�ÀžÛ4ÂFꆳ,ë2åø Ø5^Ž"]=!uÒˆbQ•ù”ìÄdEÞŒ9éÄSZË…ElÄ×ó4'G5/Úð°3j²T´;@÷à�ï󇫆 &ŒC˜úxœµY[oÛ6}ׯàü°>̉׆Ä5Ðe6ô’`í�æ‘’h‹¨Dj$×ýõ=¼H¶lÉrHGüxH�ïþyþË_w7ŸïoIfŠ|Íë?Œ¦øS0CI’Q¥™y3©Ìòâ�É Ï 79[ÜäTk¾ä 5\ŠÛ›»R*Y2e8Óó™Šæ³Ëtc¡_.´Ámx¢¯È¤£9%‚ìÍäPöòËd1ŸQ ûªã>—_€ð «%Iìâ›Iöš,5~.ð¯ÁÞD¦]/rU_bCæñ(1Žq²óYiñäsÆ ÄRž©HJÁ”Á£B9YSÂ�a)‘bŠ_F”\ k¦žÜI—‘GKùS}¿B_¼žø2¦X7æRÉ‚à<çlêDš›hBÃ.{Ý4"5Š#T¤DHãVܾ+~{?Ÿ±bQgVP}ñrJ~wiñ7|²Ö~°Kø=1ÛñUl�B »q©ôƒC¬c’�+'ÂìÎýªôÿ•¼ÞT=7Ëz^ß{fŒ¼-;¢Z·�Ý&nå«­GíË,[b…ÈÅô’{�Ëv;N`ûZ#Ð×ì©d|͆:ޝP™ÒCK}wܾO3µgP÷˧gåF¤ÍÃõU¬Õyu�@$¾Pª$j梩T\:t)ËÓˆùò…È O�"Ï�ƒ …êé8›mÙQ݇ìñciPž–—�5€ àÁñ›Zû,FhYc m+7ŽîÀuÓ•Ú–Y$¦ÕŒ —ÊõŠp ¿|yK‹7öQ�d^liÌT–ö>×­ò±7jNk¤{[Ø×6.Ô[´©Ÿ¢hs}háïe³—½X‡ÍG#iî¾.'OÐ^[v îCö‡’¦FßÀç ·ßJ(ˆ¥?ÃN÷žQXêƒþ)¶HèëÔO4];[Û§11.PÃVhªÐz5w ][ƉÝÈ})ß–Cºò$ÛÍ;œ¢õ*J »¼%l)Å­UÞÚ®/êŠÆ>Ât±>bóÍt‰ö… ×»’ã4á-Àáº@ùf¨vÖvë6] 9q-5VeÀº™€}f�µ6¬JäLïù-¶ æ§C…3p$~g{nö™JvÆÀø÷€%UX7LÝ!ü»[†ê«|¨lÜ> ï'œÒ­å–ÝÀoªûά…h§¹ùšntP’å} DDW¶…Þ90ô®¼(sV0aÎ9Pø`Ç«÷Í%�kbOxÞ0»&qÚ¨*1•jb€/¦z&Äv<~Æüzg©OkÛ!7fËîàº-1—ÎëQ‚*�C÷’I3ÉЉ•\ ™¥$ØúÄØvDR”41¯ºûg•‘¨m‡ªGWþŽR4:¤¾¯—ìøâ1�yαv¶µ?³×v—<ÚeÛb~”þÚõÕ&Ú©‚ë¤Ó3šx·;šxŸ¬µ}VTè¥TÅq*Ú²£�²Ù;êò„–eÎEÜ(°I¯ö+ t‚-ÌÂ÷¯3ÿï )[�WBŸ2#ÿxœÍZmoÜÄþî_1éöIö„„èÖ„WmhD ¥ŸÐ¬=»a{ÌÌ8Ûå×ßç̋׻k')t³HicÏË™óúœ3Ç™òÝ«Ë7ïn®Xië*Kæñ—à~ÕÂr–—\aŸ§�]ž•Î0n¥­DvYqcäRæÜJÕ\]¾ºœÏüL2Ÿ UlˆÞÓÌX,3Vææk6¶K’9g ¯ÅóôpAšÍgœ(}>z.†“yËršzž–_²¥ÁÏ9^-væª; Ì£T8Ä­�ÏZ¢—±oXÝUØC‹Y�©Š-:YY¶Ôªf Ùp½a•àºÚ\$~_!o#'µ9ÿ2 ¤&´VšåJk‘[Ù¬˜êlÛYFgFò¶Ó�aœµZ-*Q3µdsüŠô¸-ÓìÅ|&êÌ� “0ÙX…-°-·kå§" sÁ ¡#AAË÷l%oq¤jÆ«®nX+t²'3|C[Ö¥ÌËx³%·ýŠœW•aŸ== ‹¤õC ;ÇoŠ0n0�Èû Œ6bÍÔÂ}ë”Ϥï�e�BÜŠ �8V+‚TëR™žm’3A*iA ú%mr‹skeÀQ… Õ&š¶xƒ-KbÉ€]­¥(ˆ5¶Ø8bÑžôRc'4ä#îæý ûs‘«<¸ [s˜ÍüRK¥½ìD¬Añ‹MÜy©ê–çvÌÝ|T¦-‰?'s©ªÂxz1 ©9 �À‚ÐZ‹��tscµjVÙk!¯Œ‚¿ÏÂP‚¬Ôbù<½æy)ñûK^PðÅJr°4Ü9)€'19�Þ%úþ!ãì!Päªñ nŸ=68ùgˆ¾€ª±ð*Сü"Û ppþ€•¸ƒÒì¦ ê ò!_8|³:›Û" Ë�ýtDg¿¥Ùoþˆ€=DÿÈI[- ™[ †³»suäm0�%ÏmÎ\ kµ¦(FÖ…'6»—çwiönŠgï±_ˆÊLq DzCÞŽÚP$j¤ŒsÕ P�ÅõÛ4{;Åõ�ÖB®J‹ 73ðk”G=Á#Ø0qZ-UÀ*øT fºÚ Á2ö–•Ü @,à–ëÉ[à“ˆ%òÎÀf³-2¿êày§QÛÏjm~1¢H³ø4¦ÄoX¥VØVEh®¹þƒ€�ä„oö2+-WÈ3Uti(v-´HXÊ.U‘'C²¬µ‚RðŸíU‡™ZRÍ™Ÿò9£•dgt×ï÷v¥é¿„V”‰è¤°”‘aêœô´ÜiÄ×™7þ^‚´¼—0R¹åU¿ ´§#Õºaº;•å~êêW[¿6i¶70 MW/I–ð0Ñ@'ä[§w Nxcüm¢;±Ì]ŽX︼¾ EDd9¾O©ÜjŽlWì×”Ú Ö·O¬"¨¬¸v…O x‘œÂ6CFÒ=¶&2„¯ˆ|þF©4Rá¾ékÜPØAj*(ÿëʪϞ'LN"éO¸¢D9éy2ˆ01oOb€R@þ¼3VÕ=ð;¨´ðÞX£@Õ_§ Àà¾!Q¥Ùîû¤S+ œ?L­£`ê"5DA_!<º‰iˆ¬ ·ÏSòt¡lr»Só lPÚ•97³�ò èíè/ñ.ðd{Hïª%vží$Îɘ?‹”n¨ ‰$(Lâcã(Ò•p—ÏàÒ16âQÉ#¨ú&&Ù îÝ÷»¢ª/%¶yú4Qqõ¾…¦E±/ÊøøŠÄBÁØ_G _;ëíËšÒÈ”ƒ+`Gíà@¶DCÖm%jÑØS•ä×t«¿éH³½�ñ;¢±ºËm§{ç÷Én¢Q°–¶<Ú…ª]™m�ç)3WÊ„Îhìßî¢Ô´ù³SÏBw26lsäfˆ÷Zˆ{[ƒC|±¼¼ŒB"�õÄT_ŠZÔ_æ YIø̹m�±§†¾¯i¦2Œqá†ûx¾³âžñbxÏ8߯Éìo4oÌRé:Ívßï¸úŒ·mE]j«†xK­)×íÞá{›”÷t<¦¥*b»óÚ¿ý“^ç�àdm½ßíï+ã{ia½ƒO1£aþ– ïc=×¼[=ŧ16/iŽ:@²@]CþæƒzªÑ$^=†€Uÿ0ÆéwpYçIîÂ宦Ԍs =ÛÁ§Ó,<Œ*ØoÛ¸/HŽcÊà8ÞOÂ9n�9×Åë®mqñûU„ÂxtxLªP½²ÎÛÃøÕìÖ/�YòQœÜˆ §º4ö‡oc\¿¢¥�ŽÝ"~wôÕ•´XuŠÙP± é‹feË#ñ]s½’MšùßwøMØÄüBÿí>�y`zŽgÑͨâN3úÿ"Ò²�* ?÷ˆâU®Ôª&Ь=ñ\½õ1Å«þiauŸxtXÜDŒ¾Ý t‡ýo@<Çþu»ÁËÄ^s(Úy½r±Ó?>8€—KÛè÷qÙW„³ð÷83ÿw>ÿ½¨ü SS‹/E–b•xœÅY_oÛ8 ϧài¯mƒmp8¤¶®û´[±õÐíi�m&Ö�m’Ò\÷é�”l×i•´»ƒ]`‹%‘¢~¢HŠb¿½ý|rùýâ W•ÉlÑ}Pæô©ÐIÈ i,ºc±vËÃßÅœÆ�r%&'¥´V-U&�Òõim±JK\Ìu¶˜·RR�ß°Ìç‰uÄj�Êì°k6±ÍjY᱈3‰d1—,ñÅN Dš-Ș|,ŠW°´ôï�ºŽfg:Ç-0‹t§¤”óü‹yÃ2x£µu˜¶< —�c¦,Ígi)mÂÚ½ÀY˜ž«ëTe_‰ ñ5H»0x"èô/̺Ì-¸aƒòo(Qš�9ƒ”‘©ztp…ÎaepºF�K‡d­iš9”Y –„èÔ¢¹ö ÚÐT«‚·—Þx:£I:í‘DZ„isÊ€Ó+d�°Q®ð̹$ƒQnIJPŽåèúàu.½ÚΟÀ{¬É@|— åp¦WÊ….M·ÑAEhf·_@ÅëºBÖÌr4ëÏãÄ tè÷é�]’¨ ·5È8 •«Õðð”×\Lÿo<ç�S8ð�{¦«Ff®;ƒnm꡸@ߪ Y¯]¯¾ÁÆÖ]¯’¯H'XZMv:o‡fä$…Áå±8§ÓT5þ8cK Ó?b'~6Ü¿¸£ IÛJØ 4ˆÛË"Úê½÷(º]o‡xà 86xÎÉÝÚ8¡kGfgÛÈP¼Lv`é¤ühŒnÐ8…ÖÉE?À+� — eêc�3ÉÂåmdùEÑÏvèý›H¾…¥ÚøÃ Ðÿ ƒ¹Ê…¶—0<'ã ø.’ï1žJ™biÇÆðEoìŸs‘t­"¦…8Õ"ŠŽ‚$E�µŸ2.È+‘\Å`}¾ °‚ëèú:'Yå…4t‡RÜ·"¹3µªžÚé�bÆØ@?­«�zè��PbIéÎ!�ëk’#~£êÓ|…„³kÅ �‰�"›ÎaG?ø7ÄÕt'ù¹qªR?=Ë´ëÒòY¢®H^éag¼·bKÜèìcß'ZЊd»5&L'”’¬´¡á²ÇHx£Ã1Øk"àÙíœ 7@GÞ�_!ëO$ÃÞ.-wJ6-/\K£dxŒ õôŸ†’2ÌïG|üŒdH´ûÛÊ T£[·»¸m?ˆ¼M›ÇwB¥ û}â H�:•©*Ë›á‰æwÿÆð�+»ý8ƒŒ8Ç¿•ý£Œ.cÿ�ëXÛ÷ÛØ€Î‚Y·v6ì=hi[OÏñC‘´º~§Í%šJÕžÈ!éþh ÷UqÓ¥3`�nzýÉ<ç lª ¼Sîc½Ô"i1˜DE4Sµ;™Ò[´™QMÐèý±Ð™­! ê76à<½8¦ÓÙ·F+’{CqSPY1Œï¾ÒZg�£[%¯—FÒk=÷¹nÛÞŸæºÀ4­Ïõ(÷@¼|B`Wá%Õã»ê^V‘S¤.t=‰:Ot•Òª=êí~ õ½‰ùè1›…¹£__3m�Ô[[ŽJ"Ùîï0ˆ@Ù4¥"%;=L©hÀ²�-uÏ»Ê#Š2á^ì*2á"ýß嘡н Y¬îµ³à‹ï2Th÷ÖŽ�þó±>f™ÑÖ^KR^׊Öv˜vHD•seUÖ=Þ‘Ñ…*¢¸­¬F®2Þ[?æ[Ü×z–FWÓ%^ó³(|c0ßò__±Þràqaµn&’¶±³n§–7Pã&¨ÎÍ'S]I¦%þ݃®ËË™�]d2t\· ÿ>³MŠ®’f¥(Q ßG ŒÓª°O¯>V�6NÖö9Œ>€¬S‡vpÕ³2~ŸªõbÆwïô¢s¦ao¯GÝ‹J <óîÔ7íSõ“ =õîÕ7ícOƒö¼u·Aç×|n?è>ñ™·!Ÿ‡¿¾ÿ d‘xD£t2¹xœå[{�Û6ÿ_Ÿ‚çÃ5w€³n-R¯�Ü6íêm‚K°— 8”DYºJ¤Ž¤íø>}g8”,ÉÉ�&[ØÅÚÔpf8�ßÌPØéŸ¾}u÷öýë—,µE> ¦ÕÁcøSËY”rm„½-mòôëÑÖmfs1»Ë¹1Y’EÜfJ¾¼{uÇî…MUl¦¢¦Ï)Tñù~13È�Í"óœã¤Á”3É q;Ú%¼Ù]ͦŽBžíQm=Èxä%‹ðÑí(ý’%˜D*Þw¸çlºâz¦ÂÿN'ø�ݲi¸‡n: g쯎öQŽiç{úö·éÄI˜NÊßVúÛ0oÉÿ—0¥’FÃöL.X,L¶�ŒË8`ZØ¥–†ÙTŒØtÈì•t8€ˆì ó„ L$"U�3 3«hYiEÌÖ|Ã29F� U�”,Trb™*Q„a¡²)³üQÙ g¬Vr1{#à$¹Qàû‰_ @ÎR-’ÛÑ=�ÒLŠŸçh8â ëŸ+ÕF %9µ¹óN%�ìs,>Í>Ú²_=r)¸ýL êÎ?�`:ŽÊ‘VÆ@ÇâŠê<¾{¸�óf¦mÙÕy^õU2»Gâ%}‡œPd1·¢?Ú§u[çJáCæa%×P³]Øc X IêR“[@²XŬ�,QylÆ”ðHg଄€B@X9’Ï´ùßR}c¶|�ÿø” ])$âãÇ·<ÌÅSMÅ-[ 4…[k0"ƒTÍJ@ú* ]5…Û‹-K›y©³` âÄjøMÙ:‹mz;zöõ_FdímA…Þ'mÑ Igí«Ï«}ÛÐ ¢‰¥OÔ69qq[œ³Ã“¾3?©£ÉÆ-B’ÙZs2ß‚Ëä²–�½3¹DÿŠ,.´€(Ô˜° ¶TÅ=±H8ÀRŒ‚1SÚyËJ…Fè§Z§)ð\CAàZ°D¬…&–°û†4Ã;à?@9µ´gѤ(Ôr¸Œ3`§ *µ1"›oX(ìZ€ÊŸ»‚ðÅ@;Wâ ý~%ŸÔõì‰å‚53hÚs̪ IRïPhÞufÄ@eïê|:Yß^�hUƒD»ôŽpy©�ÈG¸àB ®³Ej[JN\NÕÕÄáÖ+ n@ xŸ%j©YÁÑ5l9-O¯À è¾Ê9 dE§}¬;$]);øÖØ07±o,ŒÚÎ8¥@S“ÐSŸ‰hoy^4»5³bÚ[¤gì[­ÊÝ:D}îKd«B’z dUµ?¼üÃí툸ÖÔÒéÎÒÀzPµÀ‡TùÀÂ¥eRY/â�gUAÀû:ï´_ùwMa´²¬„j -/ 0‘‚ºÈ¤X“ý�{Ä% €Ä‚(Ë,¶ÃÀ-jµ!ñ#H·ß¦ œ'Þ�ÇóÄ�ׯæ<­©¹í&ˆo$ßÝ]6.ÿÔìùþ(sðkíõßß`ôÐÚöhXu½—vØ?UŒŽ 9«µ&a›mü¯3èÁ8� ÄýE8òOëFCÊä Äî:ŠuÊm/NlÇ’:Vº±=Ýw™s‡#R¹6º58„ߌÕa›¬®¿«Nëë'§îv J)# Çu†ß�kÍv¢`Ehä¹*Meœ:û54éTHn\Ö¸s;$Ù°RW‰ž®ˆ·w"Ív7t�%¿¬„;zÀ-)Dlh<ð•d8œ'+¦Q�3çn–Bs�öà§+�ThÁ‡ ë,Oë(Ý? pÒ}¼ÈösAwŸ=·…¦‘uâÅ™�Š¿Y–¥ÒöA¸(8Ž{·\Ö5t.ÑHÐB|m#[Rah;[ÑþªË¼Æ�Ù^6¢(m&L /ò2å�ß=*mÄðßó|ŽP^)0|8R瓳„w,éïî�01!™à‡Ùã›ò /hÕç³± ˆAJh,*Ès ämêžÀïàÒ¬±ß‰`ú[óä ¹¡ñ¥V4ËEb1}3Ô²Ýåmð…ì  U@ƒ„2+4ðƒ ÐèmŸ0¨�È HlwÛW÷`kî:‚pÀŠ¥$]CÔ¢yópß�ˆîãC9<�ð$¸0"‡]÷è‚”hR^f6Á¡Íø`×”Å`ÄB”.)`Ú¶õìIµJ‹Å2‡Zù§^¹°ép”h*ÛÔt¨–`z­ÕÚPJï\­SÅÞêÕáB³øØæ¾€’šE?tŸÛ=·ú¾ŽôŸó"ŒycØpP ðG^oH&Hg7ºC“âÓ¯‘ÓUÑGRƒ‡àùV]‡¼†hxŒ0�-ãæîLÇ’Ž(:ÉÐKrR¢\/2y<ňæ¼9¤h¦TÅèðrõWš§É¿ø¥æ`°9S¯CcˆÀˆ½ë@ƒðïŸ#*å†Ü~4|èPs«1%ìùøÐ@oÖ.nªzË\Áï~2smˆ1 èòõ«s„Q…žn 9E»í+Ñi-Ó[Ûø+öZϦWÜÝ'|0YL ÈI¸¿™gàæ¥CH…JýQyÔ‰x!ŽRœs¢™ÎÄæ#‚Ü)Ò?Ä�¥UÀ!Ÿ«Â)ö;7g•G mCtûÃaP_4žj¹2=Rîî^ϵy3�‰ÍGµS¤PC>W5RìwjÎ*�Ô†èö‡5 ¾h<ÔàüËðõ�wdMÊËnv^˜µY¸®ßí¾Z¾æ«¡³ï.ŽÝ4t®þ¯sçï,x?à‚¡Ax™‹Þ­‡ŽÒrןŸÊAõXt•yÈÙçeïPT“]æ±ãš—F‚a#À#r‹oê®ÐÍ9»Ì{[ºšì2—Ìw\2?ÐÐ k`‘K|IZ‹&þ?H&ô*¿;"\ ¼¼�AF¡)Txœ½ZmoÛ6þ®_ÁåòNŒ0tn€4ͶbM4.²~¤,Úæ*‰IÙó†ý÷Ý©7K‰”ŽÐ6�xg*OçQý6[¡í,ƒ·LÛÛZk•Á"–Ãvg$Òzîg!6IÄ2nµü“ñ ¦A÷AíÍG#’q+É`@6 ûxâSk�#ü‡[B Sž„ø’¥j+#ãÒ2Ë™ÚÁáCMµЦ,ŠTŠdÚišäÃ8‚Á {Âì}ã^)¹ÙÚq*z9Ǭ3€0Ð(‡7bÖrZdÁW[ï L™1«"<ã°; ® öóX¦ÒÛK0€±•œÑ…¬ÁYÀ2·¸ˆÈ@î­_È“W ÍÞò�Ó·ÉSÿÂõfÁ}¤ä1U"¾ÎšQˆcqƒNèŽk�±B›qÛMféžÞ�ÀU�V¸�#9!ÆêreK8Nµß]K‘&Æc¾Çã×™ùG7ÂnUR=ŒüÓw‚ë\è¥ÈŠ”[Œö~¹/-à?DOR'n¼?ƒ™G�ªUnIaGK^f1Fϵ›�AcuX¥�ògŒgÀ𗘚–rØ–Ú™ã;Ûo2<Hà€¦€£Y@ÌH™ uBK:\­ÐQ:ÌæŠÌÚËÖêÿç®ÿ, ÷ÉsÀð¼`À=¦~²C@®u27Ã÷”¸�2Ø Mx•Qo tNF�'bÞȆó -•#ØÒRbÜx¹n3àˆã㔦¦�®Ä”NÀ!S�±øÐ.¤šMÖµFG•ÿè;2X.×û4+Ë0Ë? gp·Ô…´D@ÛÝañ8%V‚XÀIÚ†�ÝRV>Ït—Ô×sÑUWØlUÆÔ ¤y!3Ä+E¯?Æ|� ÇyRÖðFšUg:õÆP3”Ø|tÝ‚ªÄ%¾‘³ß¿­øê’7ÇVc)¾º¥ÔEÊ·Êœz˜-DV/ÊíöäB.æ"» @:,ð; P#V°ªÏ¨Ž zæ¢05RU¿ëÂ2Èd%u˜ªh•¥~m9®o{ÛÔSi)úZêGó(K¾¸­A/0¡›ákýPM ¯®O•ú¾ÀCûE¼»Løk4ÈÍw§GŠfèEoègÈôSA£§Ç=ŒÓwj#mw¬úðñ~xàÝÝ#Ê–hêá9¯ù¦q¼ºzz_Ʀà+qÚ ŠAúX¾=31.¶¥ƒ¿«t$6îÿ\§Øð÷éÒâÓj¤VìU˜Öb¿ÞÞÖR Õoà(j(sžÛZ"hòÌ�ÊRz)4®‚ƒS’èþ¬€Éô�ò¾e¶‡ª�ˆ7:E�W:‰¿ê™`¢Ë^�ן¤}›¯Õ8”^0zµ¾>`09ð/£ùcà¸Û‰:E®Aé�Ç÷¥?Vü£¨*´F&#ß§8ëN)Œ(u–*ˆqÁÔ*­Ü]ËEÝ©™à¹�ʉ[«É>7�ÜU¥¨¸å`]¶qª`u ó“'ÉxuA@@ðê¶4j0Àû…Œë ¤ŸøÖGÁÒÝ(¢ ö󚼦Šx�ø€× Ï»7ÝF1í4÷ç„fcGuŸ˜­a´¡S_™~û_Šä�F`lðQa»Þ”`Ð hò�èÖnÆ ý<5±‹þ´ DÞ· °ãPïÓ p}gÙ> ­�¢fÄwò¤å/ÚÄwG³rÔ°d7—Ëw—¯Áz¶Ô9^·½Yj.ó)wÉ�lÈ«�ZåS· Ö =#ÓÐdtBCó.Ëg£QÇ­£V°Ê€Ø–HGdc ùœ”—y"êŒëÊ Ž½úciØ×Ç^³¿Kžl/ÚµÚ�ã‹HkŸÏ×~fáBú‘£ÖxSY ;œ lW>`3¢«¶ì/j߯1胧•›;©ü|ðéÑ}™Õ^½mÕ$˜ ^î„æÑ�“Üc§˜•Ì=ÇMЕf‚cµƒîÆ 2î¾Àaî+�*¸á·'Ôu|Ò÷BLr&Çûq>¥‹ìÜÕ8wOþ *‘7„ þþ�G 8‚xœí[moÛ6þî_Áy@³NÜ+0¤Ž�$M×bItAÚ¢ J¢-®’¨‘´ ÿûÝ‘’,;’õ9ð‚QÇ»ãéžçŽ”2úéõ‡‹Û/7—Ä×a0î�Ò_Œzð+dš×§R1}ÚŸéÉáïý!Œk®6¾¨R|Â]ª¹ˆ.#ÅB'`äši_xj4´R½Ñ0Ñæo‰º_Œ•†)JsW��*- ÞQÑ��ö‹…�ЇûãÑ�¢ÁãWKæ�Íc˜oŸöý—d¢@™+¼²EŸ�ќʱpþ ñrJFN‰ìhèŒÉ/Fþ³•ØÙ_ìÕ¯£¡±4Æ�ãÅ­¬ùñ‘©X€Ø•ï!ê{âÕDÈpÐ=ñ¦Æ³‚ŸC¸Ô]ûóÌgAÀãW‰'Qöˆ’�9 fÅmLÞpM(q„Pšy„¥`â1—+°E´dLõì�ÏÓ5…êðeßj±ë™pí²Ì33D¸"ÚgÄî,dÚXÐ%áÑ€ÐÈ#\+ÎÇ$ðÉ1®P�OÚ'š~gG„œ‡N§yA/˜C}βGŠâynf\®û“.}VZŠh:þ‹1B% îÃd¨GŒ/Ùä´M]ŸGìÛ£2âÑô ñçüRû §÷¼†mnYmÛ$úËB>3K£©&FW ¥ !ª©0•lO~w×^�Oâ•Ê FYšwœ «Ëj: îQ YeùXÞ¥®•ƒ@q Œ6# -)ÇŒ$1•Ë Uñ·f™ˆÀS 1špƒë™Eždz&e´<7 ’ã÷DȇÈ„�wðç0u�ÜEå=€·fS&ÉT2ˆ“w!T/錷0AÌtn‰f¡‚Ó Þ?7>¾¤ÎúælmNrCDé,YBl˜Y:‰A›p“s³ž2Svq—-/ëÎãôfª sõ€„À\®TÉìÈaÁ@ŸÔkÎ.%Ø+`…2>¸¬É;O´ÈL�ئÅÂÜØ4ž £5“诹)ƒ‰0¦®®A`V°�¿š1Ê&¡¤Š+YlL^Køfhê d"EØŽ?ÖèÃ:²…=6An'”e��å~ÐQô:ÀªÇ¤2x\øTCÖKæqפ}ʧ€ÞétTÁØÂ.‘{PïÁD,÷ Û‚�˜Ø&Ò¯ cÄ–LAoT�ÏV®}9>Ïçpªì~ 'uðý,L[ž‹¥0Õq™.õfU ŸIõïL¼z#¡ $iÙ.�ÛR,H($# F¿gé[¯¼?|£Ú.8´¡3À°Ø2™I˜E&ƒÌ|‰E}b6õ�Z¨æÄ§Œ~‡’Œ FCKxöÁRŸØòbšrâ‚ñ©Ðr©”K,ÿF|Ö Ö0GÄ1ó°MŽr~ûX§À^Æ<0óhêVdm#a›Ó£#j¡nN ØKã>ÀY˜peÅÓ>�ƒ|=ºY't¥�\ŸÝ^��“�ÇÏ�£ÀQfÍ6ê!WuX�-ÚŒ3�žc$®�íl®@k „Ù?¤KJ§€žˆx|2�±$® ЄÍALƒ7¸~E0€=”_*™�?u¿ØªÍþ£ý$Ì]Í?‰`{2�6ûLc) }~øîþë¦íDµr!¯ôþ]àOÊC]0Þ×TÙ6C­v$ÖàrUv _Õî,Ι�µBôÖèV)`Á�M¢?Ù¹#xޡ˪©Ùô«¢nd™¯ØP; J@û á,X(ÇkePRíúÈP"iº­ B&ŒŽ]Ðì88xƒhÜ‹„ÆëX(Ómžô ¬ÏD™Õl`´�s&FXÖô:¨²<Ù)x‚¡Œ-Ïîª(1+k¡�pç`¤0bž­ØEÙq´žÜ°. À÷Š~¶æÃ·ÐžÉÖhšV4S3òXéj C©öüu•ç.«ªŠ¸:>ÈmâÁãâ¶ò¨³&²ÄzŒšØ#¨{‡]1ªuº5�æsÍÐ)ÒDŽSï¥ÂvŠ5ÝÐ)6kMè:ÈÍ|Ld"¸ê˜Ó¥lQd\µç^M|Ëìß·µ–Þ*.Ѓc�˼Ì?‘)ŸCcê0c{"`TáüÉJ`·¯µ žw?ªA‡Õ %íz˜hw’-ä»0RÓÈ­±3(˜Ô¾Vð0.8ß^×½›O_¾®ÛO’8¤EçÛ ý“K¥yˆ_¼`¾òlnöÍI¦w‡ÍëÞ¶üŠëŽÂüm£ïŸ�›ºo�òÒ¾ ZWÛ #¾ºïÆ._•[{л {Ÿ›ÔùºdãÕx;þ9ûB *púYH­¾d�Y’U¯+º}OaùU­—™hGo,rúvøùå6“�ŸËGír=è~/ÑMCk‡¾d¹o?.°_QÚ{ŸRñYM´Ê¾�ød?CÈ>�ÀîÚô|OÉQn‡g¸&M.käf¢�ææôíÛLv {fÙùÉ h Šä˜«Ãó-“&×5¹rÂ�t­iÜ!4¶í¹³œVøX?¨©‡�ò–©÷{¦lsßlW?Lþ phÿÕð? âiî Ï©5IÒnV´xœÍ7&«SóTLÀÁÉ•¥,Ô³ƒ¡�ÿöàùÕ¹D O†›yü7˜ÿ^îDŠïž=Ø~+Öþ-á§…©i•�â©ßÁA\­ž�,BC¯Îwî¹xW«L§¶ªE&­Løy¦oüB…Y~û€‡^‹²)T­SQH[ë[‘V¥•ºÔåFØ­M Ø(3Ygú7•ÁÒ1äÇB¼”é6�IyS”¢Z‹«Yã¦ð�¨Œ7ª´FT¥Š&ŸÁk-W¹z´](€"êjŸô§T+£ê::,öq«M{zø ¤[Ve~xœ„ÃÚªZÑÞ @J.öpª­…ݯëªRX\yAC–Œ�n¢2K<:pweeé Í{ üîŶÊ3ÄT®nTnpG`ÁJ°¦¶&áÒðIÝ 8 FQ;+öÚnÛýú“œ½&™ëó$s}žƒ¹>�1͹\Á‰'YëÃN¥›±!E^m`�\ù=Ë9¶¹QH�¾¡‘(Ð2µ ~Öµ<$ôB¤*ÏùòY;†g›�†K¬à%Œ¶2^%ãü‹C„.™w?;ÞÕ4Ã1 œ!m�-ÖU�8JÖÀ­»ª$‚Æ`œœÄ´GIý¦)Þ¶‚`& ß; @™âM@w$±Óìq-v•ÑVß(@šU$¥Ì ñÍÁk r4"š(jk§­� `DcPÒ�Âá Bêq'msÐç=°ØÏ°ì$aü 9(ÒÂ’ßµH@Í88B /›�ìIT®Ê ü–±\ôhQÕz£K€Ó³ŽD€Þ­�ȸ7¯õa²´¤§ý¶¿ïIÚ ñÊ"ÁT±³‡…`Ìýý!‹Dì·ª �ëC¼ß°ÒB˜ KY•Káø¯P²4¼'ÔIÐò…†mÀ_ íLTÙ©l69¦û¨”·#g’ñà”„·6sfùŽŒñ¿“t¼¼�Ódé�ƒ.}ˆ#„Áôÿä�Ž™Ô>¥üJÏ)i—#)ðãПb2‚^È*üØouJ*å $ˆ�¬ï$ô<õžŒ³Qˆ£iå œEAwà�(i÷¾u%ñÇÈÕ#Ð}lE�¢½p§ ×Fâ-ÍOOâ�¸H¢JRw¾þõû<’Òé$ùâm‘ $>I=~Þ÷ÇÇ[þéÕíÕýÇ»k–Û²H¢eû!x†�RXÎÒœk#싸±ëóïâÆ­´…H® nŒ\Ë”[©ª/o– ?-�ÀJe{"÷·ÄX¬2V¦æ{6ñ"VDKÎ*^Šñh>N– Ntþ>µ)F£eÍRšyçß²µÁ¿s|µx1U™8Ú}¹š"²ÂnérQ¹„ý(*¡y!ãY&­Ü VbIá·êÞ�ü+™Ü¶Jsþmì©Üç‚y ›ºqÿ“e]ˆRTÖ0Î6šgÏl¥`W›ˆñb£´´yÉÖJ\0ì#ñb]kÅÓ‹ µ3Ìæ-b«XÊkÛh±JUç…¬×L‹Â0¹¬ [ »¢bµ™L­Ò XeŽÊJV\ï±ÞÔX "[®%_â"úïÆ*ët CËíÄmÄăUfXÀè(ÜÞsa|ž!D>:6Zü|ŒjÕlrfJ¥lÎÖM•:réËRU‚sa"÷¶¬¬Ð jùJÒî™Z@î  ·t]ˆÙ­ò2¯¹†y[-ÓÀ¯é ïJ nÄòcQ«‹Ô’4C¢°�}­¥M7¼ £Žþi•7³¼YÝ�Tz»ßïÛ þ‰`Z„~4Àl¹,HJg̈ÛB±ØØo÷ZÚÇEذGÍ/øÚٮȬÂ|V�9(™Xó¦°gÞ¼ S•`&wêÅ\* Á¦÷X-t_�Ò«–AgUvÆv¹Äé ݤ9ü‡Ý\Þ¿½|ùgm~kÔ†mF ­²1‚ô| î –ŒÕ97d0 Ìagj+4«¹„XH—÷ BÃàÞp¼äÌ'Èj}$ã S“ÉtiwâÁI§~ ñÞc�@D�ÈÏMù†vt¶„j¨W»#0¹ è¼q–˵už@îWEì6µ²pîF?cŸD Ó)‡d:D©Á¼ëB¨ ûˆer½†*OY­- OXl‡³d{¼òî.uJ†×Û”ußü)LZ¡åŸDŸ¦Þ4.&ž9òD(ŒÀ…8þR,$ë‘pQ-ÖÀíL”ìб;„”· ä¦R�ç`Û4W2% l ã˜+#ˆ¥P�•N¨^ð?UʚΔýح΄nÇZ ½R¯Ë^+]6Œ¾%ƒÎßÁùSSJîè¿„ oȈ/¹±Ý›Ê¶VAbóS¼áˆ>íT&LªåÊiÚY\ä4ì e�à?>~w>w~)ª ^pXˆ¨¿ù��rˆ!hï7ª‚î….‘Nˆ¯Á’—²ºÎ6bÄíLçh:,êqxd}¡ô9t¨{Ñ$Áë¨7½Â?Núߦàá”u¾ãûþq�á©tºv§F_j”¾†™°+`ã¶ŽCtNŸ¡š5Ô¹Ò9n+è17áÎ Ï%{ìëö1�WbãO‚kTZBdª|6õOñà¼0ÚÎÓ¹G8Œ£VÒlYëpZ‰‡‡— Wå²lJVuÙKöÖÏæ«)N¸ÁQéq Y7ù|^ù!N>LÚÿ¡úa;!7¹�I6W°j/¦¼èדÃÓ2äèâXzxoö¢äú¡Æ¹DdÃâdzüdºu§�ðÚóU,ÝQ>îŸêÇ0i24®êÞ{H]!î7Ë·4¾ÿ)�ÒàL®ì[q¿«1Æw�ãñÞ¥Ø^+î<—&Cg"î52&ôHMLšlMnö²}ª{?ÖT9Ø€¬b™{í=è(ÅãÓDyz0ǃÒwÜ´qœlt®³Ç¸Ã?Ò÷ãþ�ï}ʉЎv�!:”ÎRT·Mü¸ßÏŸÂF�vß>jåØ wh]3/î÷õ&ŠêC†69¯Åá Ç÷º{µvèVÍd«ÿØ×®Çî�ï¶¶²”¿»Yœ2›‚ÊîÏ.yäŒJSm´È�h0Õ#2Sm è•þN6�0NÐn¤9¾ä›ÌûTiq¯yeè\'Çß'í£� ÇÁöVl|‹Ê ;²ŽEÛ<ÝQl¯ó|;Ñ»ÑÿÓKìÓ{¬“p|?>q_8¡«'Þ1ú›ÂpÓ˜ªÊréÎü‘Û´�]P¸ø_ôÿ¾±áñ�{00Åóe–Mœ·‘ÃøQ]?Ü�×ã$<œèý†¬3ÊJì\#¸wÁÛ¿Y¿l¥î‹¯¿`§¿F”%@‰E|üóŠ Ê9xLµ2fËaõíÓ´áaŽý dÞO040¦¡«²æ¤žðpÒc®üšçmÉ5ÒmœøÏG›÷=¿s )9êàÖ{¨TÆ ý}LZöG€,`ƒqBŸ’–ý ‘šÕ]8úßN€ö÷ÝV‡0í‚G÷K”Çñ3i©.ŽÚ@¨ªÙ8º ¶ÜûòÅ šÑŠÜo5<ÿ‘Àl¬\;[ï¿�Á?3o�/t�_È!ž� ÷¢ô‡ƒîqЉ÷ô—ç͈$Ô2XÈ£—N˜9øë“cLW�.Âï*þçšÿÊÍÓ»¢˜€�‚7PxÙEWHªõ�¬Íá7œX €“˜¦À?•=ɘw¥œ Iã!=ßP×È™(÷tà·hŸË…8¦>oj°ˆt�»¤w½{” ï-yfœjI2âÜTb¢˜³Vª’5¨% §Fgu‡óÈ@=œªâ„ã5¹œ”7f  †2ôš’9Ìñn:27ÿ'Í&§‰,ÏOø3pÀgñæ­–ÝS’‰_ö£ÜÕ¸‘úz˜ÎOt§R¦ëYµŽqzÑx<9MŸÇŠ¿jÁNÛñxàÄîÂRüã¾ZîA&熉³zD¿#¹:WÅsŸçëøÏºÌÔ­ØI]ƒÀ¡ÖAM„:Ǻ-á6’ˆyR°eÚ§øøð¿©º2âìb!ž<Џ¦Úúáy²î¡¼'Ø}*}(ì px'ÍŽ‚\›V T~„Ÿ ŸdžÇºÉGùQjS¾‡±â£é÷"¾"3·­Ô~ŸªË¯a�%µ^ƒÐùС“æõ·z\²þÆí»±v[+²4€âZ1fK¥ [2H —°ÀC.üŽ.Œ@1 î0Ük:z+°¸GãRyÅ.WÅ,›3´'¦ÆÉ2õ—²²ÓÂD#æ�"h(>ô‚ì¿ÙåºT'¦‚Ð>B\Ó"ó{gÈþ%,â Ñ (ô µ»‰y»·È4“h¦s Ù¢ù­w…fÇrÇÉòЃ;7´óàôEõã$Fáýø$0Cl¾P{ÒðkøOV¿gãëÜ…Úß“‰E…™�÷ÒNç(â�s�  oH ÿmsÞ¦–™Æ8dUUætJxóa÷ ìß©òË>EòÎj”æó¹¡Á²`¬6U š¾˜-ïú }Œ“ª+íÈ™ò®1À¿WÞê"•Vçê0þ¾ù×¾ã;šs¬ÿ쓘ª�=î}â¨Ù\O6Äxxù~¦ÿ˜ó¹Æ nײʲaò >WD¾„uYÆ$#¯Ð cLCå º*3åE?M¢úÓ(þ4j,ã*ŒÏÏãáÑRñX: .ð Š‹|íº�¹6㱿KÕ¹ÐÙÊyÑŸx£Þ“N8Cî&ÎÑL‹¬jNµ¿p¨—!ïm goêçˆê3æØ±/ûŶ„–€p�""JÉèúŽÝºþ Øp~àøƒ®]Ñ9NÏö‚^F¹Î>±H5:c–ð8à1Q†—mÚ6mç�^ÅêåÜ|1�cßöÒõ~�ï”´”ÆJâE1¨ÖØÒB›y„úåíÀ«ìõªñ)s�è.È){ÊEºi¿C1 ÐÖ&JÛ®Ó†ñ‰0Šs0…ÏÚt O|V´{Ž!›¶F]+-�cãõp*0ĕȚ¢8ôŠf]â'_Aýï¥Q”#›$x5�#`C²âK—Ù) ­�ŠZ›&íu¡µ<£¬Zðh ªÆbÉ"I’qÆ;ßKÓmT 3rjoóæ7‰@ $ƒó—BBȳHÚq�G°tš7f æ�fBì|‘Iz†QsÐ3vWþ5"á&B÷ŒÄeäS†Cª¹‹²è"Ñhuk•(ºÄ^ˆ¨ªŒä}HÒêƒÙø\$ÜíCéòÊc:×¶hr«!šV®Ó&L \ÓN_èX�jTµ‘ .0 {ˆ©ë;-!°÷ùC, 2�§:lÜe4’$\têÝ|v( PZ×ë�¤“BnËW˜À¯¯?þtý}˜ÓÚ!AÙ< ü\j§€õ†Mü«ªW˜N>ü„7Z(©�gÆö§ fˆ}/;�o òÝ~$íÚëHKË—nˆç ™¡Rð…ñz´ ìp2±P�‚NhâÜARif€²Ê˜>ÈA£¶ubàêŸÇUnÔ@ç›L%æê��%¸–ÈÀ \xw®ÐÛÁ®«JvžQe­ó$tÕôyóNªuKC}"õj*¸–`Ù)¥å�…2)nªT®`ùšz~Ùã¯àüó*È÷Jšª„“~ý­Kz~¤ãw8ažÎß1¸C}ùi{à¬aÐod8�¥û'kÊpGƒ•Z?UEª"¢àíã! Þ’é™’T6ÚoÎÃÅÝ&Q…;SÙSµ[N½«,ðA�ͨy…JL\ Ô×U“ãU;:K¬Ué°zùÉ2’h�’\�B?0¡_ìlvû¾Ê¢]ƒ�Œó;°fä¸a¶x�ÆUÁ@²!PÊɃÈ�לv¨Â+‘H¬pè0ƒu»•�™Y~Ô–ìtßž5Kç^ lŒ×©ÄÅšÏùmÖõ~Ý{À¢ÜâãTº³ÿÄbmø[‡Þ=ìäH_�»§Þ e fÖ®Q$¾bðù*…‹É;Eýä}G°9�­@±Ï%T >QÞÝ2ú�Mh™ŽL¦Ã8fEwõï©4%É–i:/â™vDi¦-©¯ÊIÜ<¥«*3]qijôpœ‘ÏKÀsd€×Ú¤Çlɯ Þ8uZ·×é–: E(>”œÙloÅÁà3½ø%´í‡âq¸›Ë…}&/ù%t i6TãÁéÔ~…3Žš ÛúbE{íÊÕj‚ç¡öS"h߯F;½è¿\ñ¨wó+iWBXN{‡‹ÈûÞëô&'§Þ~•£³r2²´C};Ï]ÇŒVŒ[ê`™ïA—;ï~Ñ'¨x&.äµ'‚øÃ3ñK0‘�ç]ŒÌk§¡—\:¿p¸q„$Ž�#\$¤d�©=ävØ5™üñ!ñµŠŒ®†BTQrTŒ‘Ĭ¡j_VYÔn1ÄÙàú&à{¨ÕêwG˜$ñßNÅ“øú2þöÜݹ‹ÉñΫtT±ŒîÓ‘c‚þ^?gq÷mÔÞ}¸€Ü™éÅhþÖH¸�è›çœFÀпF�9_‘jߊÆú�î%Ç;·‡›¿‚#óF—Jñ÷‚¾”ÕÞ—\Å9º*IEwÑtíF�á›2îæ �î\©aäâ0@ì:¯$gŽP-hY³Ãz ÿŸ‹Õ· úÀE©6’;[©x9 øb_–2-7UéÛÓ<Ú;´?.á'ß #ëWåºåþk÷�{"#éÝëL•\·âÜ+Ùs‹Žš¥ êc �Ö¿' Tw親q$4ò/ݽDÔw¥Ý °{}8)@™ÏråÁ`ẶÒ-ƒ”¹Caôu—êH¾Jwt¡ƒyèOñ¿Hâ‡]ýq:˜ÿ `wé¡OXì3]<3IT&¬Âèu÷„ëÛKÝú…bÛßâd„‚ܱ7J3®g·OÚ6”z<äúüØÒ™:eäzÖÀ_)[‚ªÕ˜_â_+RAá§)h.~ø® |WëBÖ� :´4ˆ¯Ý>{ðäâ�˜6®"Æ6ÀnãAÿñ­ô‚rX;N}·CÏ-ÿ�¿·G+dÝ8ÑÅ*›�ìà i6‹‡óZýK�—âñ¹8»ÂðTœ-Ÿˆo dPæ;qûè]÷d8¸‘© èòæ+V‡)âìÖmayß%M!o'×û ¬ÏÂQH™”`R1Í‹a< Ôî%«u�w<ÙIñ†Þ_œ¶ÁÈq?Œ`!é~ëÝò�Þ3J³Ç ùщ4Se!ðû×®Š÷ø'W^^^²»x+¾Éíwâb!.Üïgø#¼ÜØï$ïÉ(7‡âÑZ§÷Ã@$fÀ÷—_³à?ÇþIëRÞ›ý——÷ÜêW“¡ Œ"Ꜵpð‡BÖuX­èlbÁ–Ù%1:m±!›Y˽s 8Gè/%-\ö€�¤/jYîïËõB¬ÿÁ¼Î)1»¯’¾¹ 5èX¶º ò o¶©KÎv7{‡‰Þ‘/—pQ<¾§ãÚXPJ†`Ý!!_—ò‘œQlï\ùïwºç�%Ðuu¢XÝNx£P˜©4*¿Qý&Ès÷éØsþ8íÿR7·2´­CèÀOdžB†j9\Bä| þº‚õFƒ:?a¥Ñž?0+æ¹%·"l 1+ò¾o2—ƒ>¦/_€P …:Qú ¢Ÿv䟣þÄ�ª><¤Àõð—G3’[.:^=üôè‘�+•ç]­«ú€ðʱÓ€/Ó˜Ž¬ëj.ç:‡\PDíP HE’Â{‹R¹¢HCR¥Ù ‡Iœ JbúßGÖ4b¿`†/Q@¥Äô7ßÎĶjHtŽ÷áU¬5ež8!x ÆÓ¿LœÍw :�.ub0X'Ôo d'ÎGu>wŒ!Š�ÒË[Á¼¡«ìÜÖJ¡Ûz1ð€ø@ö` :ŠjŒ¦Õ 0÷Ó]˜u >­×V^½üg´ÁíÎ(õþšf`#íüßÔæ·¦znÕ:È„±·Èsǧͪ2ªðÿU«Po\WJ¹ß\ã”wÏn9þFÖ%=�DÛ—~|ùljzýôG}¨eib¡Œ(8Ⱥ>KÔ%âÖ”)1jÜÈ1ˬ…Ü Á(½V¶yؼÑ9�À…¬-ŽõرW•ëu®Ù¹ô@eý5›Aª 0ù}³^SŒ`Ÿ�üIó&S^²YÕ@Ϋ¥¶­tÝ;]Þt_}Ц�ŸãwC)�ê!è LZÛí´ÑËr0Ül ÈhAÓÆßôP‘$ƒ1'êÇ\•Ø« He»Fæ?Ã|h SŠù‰p5ŸžàO�õ'ñWŠ`unYØ[…Õ ”À�ƒ$ó$W¼¦ÁÎè—ýesX.2!"*$…<~?‘o¯ÛïeÈ(ô!3Avb]*EðôK`£�ü—K&pÙÖ0dÞzd (d¡žÓ`Zse7J•»è�HØ••ÝOÑP{D"òˆJ$QJs�s´±qˆ¬W7“óˆ)ø Ãã½PÐ ¼·q*&âû§ê‡{Á6SK›´TXÀ„šûbù�ŠCÔÏ�<žF §|N}ˆ’hÖO~%w‰Kƒ´¢qx/k™iðœ3Pý/Ç|Ù®Š‰D¼mŠ×hK‡mzH…·hoÐ1â {DùYMY�“…EÄT".¨N€dúÆ•g^’2²¾VÞ!öðô>ÖjU¹ é$J¬½q02¾†TMc�ÍG mywâÁL<àñ�CÏÅc3Çÿ–+h#”Ó¸Õ\'‰s“àâò«ºF–¸Ä€Od‰Œ–OË|æÒÂBòUmJG– Pë«”J7ÀF…I}N—²)q¨2_‚¯n\½W¸Ž1Õ3qyá%B\J±ªÕâÅÙ°˜÷W¯é�q³è_Ô³<îÃh–Ìžm™eqÙ»‰Ó»s çÄÂýtØ<]!Aw†í,ٿ̲aD¥9�ÉéÕö¢G#‰˜¤3Š($Z¿Ž´`X{ßO%m¦šƒëŽmX’°qAG5ëL¶ä³ZÃB‰œ¯­ÜÐ&rµ° –2•ã„óÕ!Mor¿×œîîÊF„QÖ+?(¶ ÕœÒlT +™Ï’ʸ¨À’'=F&Ð*$¶�–Xyí²+Ìë\m^ÛºÁÈâM4Âu’l» ë¨ ¤Ó¥ZÊØ—Ð\bµH˜ï˜Aus*·Rrõ@±†oB€›EKÓ(‘Šè{¹C†[.âôÕD© °Î*ð-s4DS¬TlVÒº¥H~ÑàšÜVÈÚÃw@k>)vçß\_ Ø-M Â�K@È¢âˆXðb•Ö”‹Páó¥:Ì—¥©ÊˆÚ¿ÆLŽt¬5*‡Âpí þ¥‹5W%ÈuľöY¨Í%$³Ò‡_N�˜¨2]�ÇVמ¦\ëÒp©kãJ`�Y²¼f[*ÐWÄ@‘WÚU ž76Ôvh˜W"žÖÙ%U*7rK†D6»7„k �§âÖ}ƒ™ˆ¿:vúpdžþŽÍÎé‘ÅéþÞˆœÊ ÍTí�cH6;~yu«}ïÿîÏëÀR€ŸüÏ]¢2!–žˆR´5vЃ‡�61~ÝÆ�/Ç£'(Õ†‹KT˜å’»+Ž›óÖ7ä¢i� wµo†íÙŒ›ûÚâ˜1Q6�ªAIL`¨ž…B»ƒ!æÝaUFaº%ŒVHãûï]ìbAxMþ‘Ä¢ÇIÉ}€>ظ=‰O\±‘ c´/¾Æ¤™ªŽ#%é¨6 ¾õ{Š8¸„Цû8¦,ÚåàÂÒ’Rân)¯Û 7£È¸ÂG.À¸G­ ägâ+ªý�%®è�‘‚æX¢ZJ^ÔðdèØ!ȆÐþÜeønA⯪<ãö�.ŽÚq'ÎÞÊ·mN”E˜¸†’¨Y½TÆrñö~•X´ÓLõ <9î†`iòm·úÄ´DÑêj˜Ë˜UÕä˜îùši†ƒK�ÛuÔ—€3íè é&çQ_ÈôV�nïEÇr¼voö6špfAêíT•‘¦“¨]jAÉboEîªã 0ͲSWÛÐeF¬®zqàæƒ/UïH3ËØ¸½ºÝ˜ ç^È-óŸê3·ç•:kÅè®�_»÷9È ¬\ž Ø@:FÛÈ3Ì{á7:*HiµßýÇ´_£Á½)ÞŽ™Ñö$³¥R™kî‘%|�‹Î`iæ6_œÑ¶ÆœCJœóÆ&ª*m?ƒ3 Ç Íî^ØP»::¯»e~ÖMöî¾b�Ö•1À—½‰�tRf÷êcØ­åÜ¥…6̧ðÀ„·kHâ#$:·îÉ{zÛKGf8]"<cìŠ�hèù�C1ó»&�$$>ó¡•:Ùüø~²2s©ÞÓ'ç °ž¤;‹f³vµ¹AL;‚ž#d’D»1܆Y•?=g;Ç…aÛu…>*…P ,öV" ©I•1;„ô´¾�Рõï§5h}ïÑü‚«Pü÷ng¿ÛJ‚ú`ü¦¾†ÀxM*2`.vìo`}mŸÓtü§“ñu§yÎ/èãW>ôúÎ’šy¹Ê+žî©ÈÇtþ'Ì[5£M=J>Œº2ÅF µF@⃢Òz�H7W‡Ìõ8™ëˆÌ¤ ôÜ&Ë¥f.ŸÌž>¢¬e"ù9nÔTúÛÌë_X‘Kp·äF�CÜ=GÚHÙÏÖáÀÇA§dÛÀÔ{�¬p™P3ÂÚ›¶X¸¿&Rÿêã;Y[ò“à“Ïôfí¿ îÇõ�†úØVDSw¸‘ÞŒ°‚~j#��ôãf‚?�Pâ!¼èÙ®��¿ï¢TXD)ƒ‰œuE³� •QˆÞ…TLÃþû£r­ªXË547æV»1ÃÈ=˜pE'$^ñG·®Õô"?CMdRšžËÈ<ò|ÓúßÓ ,_‘¶�±ÝÁy  GÞ£ �,u¹W?yÈI �"VIg¼¤z×G¾Ž›þÖ§¾¦ë‰h í¶w‰ ¡ÑtŽ,µ'–ŽÛ4m±t€OZ¼H¡:‰ïç÷+òY[GÄB àÞJ[0X‹JXÑ£nÍV0�¯³õº™!i1&ú\[÷í2QŠã{i_&mß…#µPÒUFãšV Ú&èÐÍ¥rTTéÖQß]àÜiU.¨•/ß Sõ·Dý9#,¾ §Š*0½Ü_�Iz‡!N)È„þý½B?ÊTÚ’hÔwZV=1{ñnf§[qvížý`¶k’–7n{0àKÕ “÷¹sÐyútjºÛhÒòkÁ'‚G€>VÙRí€LÏï|9ò.ðøüÀ‡]Ê=›“ÓÃ!óbO0d¦œ UìôÊïG&ïï"òªÂµ±ó�îâæÞPù²ù$¤Åc„sgѸÅ�ÀÞ&“ëƒÇ­0«áIfò­†O?P!w„z¡ywɯ�åm,|"Ë•ÊRR2s§pO€ßS×ÎŽTâ+‰º{„ûÅcîÜþq§Ä,¿è_kýÿŠºŒìž¨ËXŸuŒ;Cùý¢î1“ßû•#'!s±udâ[íÉu½³@Û¢K`ohnñ·k%ÚU…\bOŽuÚ`qëZ”ƼÝßq(Ûþ/ʦàöåCÙ¸Rž`ñù4iõqræÖ9®Ó¶51’;.á_nPýÑLX7ÌÀ¿Õ„ã<¤i ºb¨Tþ 5WûÜyã÷Ýü3#ý=ÊLw›‘‚n7ówàã�wÓß…xçýRÓÎ�D$¤e[ŸOó]]¸J§£‰£ÚQ/œžÖã=ºé¨ý¡ÄÞ±(gÆI¸$#¾ùa$úÚ–Ÿ÷`*oo#FãnµÕ] ŒåÍèÚÝl™Q*5”ûK|èj´ýÚù5(¯”|¨|FÝJÔ@¾©ÙÞE�öVü‡"òQ¢ÿñP 'Œº}!'u?ýÄûfû� ÿ°Úw¨B9UûŒ¼Q÷míѾ0êdÇ3¿Š`Œ–Î:WåXÁîJ¼—tiÿˆ«€ÅQüÌø[;*XíXºiQŽwWñ5o²?¥ц%¸¬·áóBÝßVØž^n/Òôí]lft*}éZ—xº–¡®â—�|u”;€K·öv¨~ðºSÌ¢3½Ú6V=âÏFµÕ™9¹¹ä¯t��>|‰í);g 0ùí9ôMU_›µLÕñyßë_bs(�ÅØ�~öã‹FÇXŽ÷Åí‚}¯±ÑCµÀ²)>àÉ¿ ü™¾o—'wR4Žqd�BŽ>&»FpŸdUìÂ\�Sç–«�xÍ�†o7à嬂d¡‘ò~ð¼À3¡ºlÚ‹@×+I÷öI‹ÝèÅÕ¯ñðñ{¼0…o^àÉö\~@îîs> J÷hY�œ3+ Æ®|£ju|·`‚%T¿&§Ráµ»tOÓ¢Iè…ºj€oÆÊ& 4Ýš¨±Ê.Jøf ¤PùKùD¸€'¥——�Ž—ˆØÏ�rº)\³Xs:s3o É‚/ž¤•Á+gMÚÌN…‡$û—¹ã@#é8_o¤é&>q`éߺ=bK¼µB^Íâ tE½kØ@+<“à×qÝ uJÞH�S9›NÒ†«:öÝ÷ƒº2—é52‡lEgïJàö’�r‡_•ƒž”©¢÷eåïùúŒ¯æxÞ-Ó¥Á=?‡ ]ù…ûH\ðÿ¢âoõµŸÏ m ¯mŒ?K™áxœ½YÁnÜ6½ë+XåêxÑŠv- q’"¨í¶Ñ §€+qW¬%R )Û›¯ï’ÒjmÚnÚ*€mJäpøøf83”—?¼ût|õçù{V»¶)²åÐ^¡i…㬬¹±Âå½[¿ü9_ ßI׈â¸áÖʵ,¹“Zý.ŒÍrƲå"êXéjK,¬ƒ u²´¿°ô\eKÎoÅQžɋ傓¶ŸYÙ²c% åõk¶¶øy‰W‡¹¥®ÄŒåê=+,䥗‹Ž4ì7ÞCŽ+víEØJ*n¶a%L†­µa 7Á*îøa¦Vòf€ÓÚ—¯ó í `Ò«û!¦W‰Ò±–w–¹Z°ÎˆJ–NˤršñŒ®*Ýʯ¢bkÁ]o³/»­µSÀÕ]õPÄ;<ÝÉ–;a7”Å0(£…[KG�T‚ÖMCF2 �Lv.mÆA�`Ü2¥UœÌI‘í»NÇnÁÇŽÊ£ìVºzdÀ‘½l+5¬¤6xãn‡Øü¬§´«I@¯™l)Ú‘jîê¼8»;[.D[ 7Âdd£VTƒm`ˆ++î:®,ø'9b:ÐÊ>èÞÐöVÜb«\ðZ¶Òót!ÊÚ°Êð[Å´*¡u4=†)Àê`’¿� –˜fu´´é`rok%n½§F÷›Ú+µ8 ŒpúæêäÍÛ}‹ŽBƒƒn©+càûk­+°¦¬3ðìô€8.kfl1�\áh¹êI�91λfû«71s·po™€;Õï9‘×"y’O¼³f¥µâi®uSÑØ{·¥mxR — žœ…ÈÁØÇ…¤½DpÁHæŠ5V’àËzfp&0¼Ò½ª°ëÕ6ØJõí ›À";¯d+ü“…—Ó™…$Ž^p–ÝŒ x­07~3v²ãcxŒÏÿ<~y§Ò°Jy=•™(^Ây´Ú—Ú«œ±+c Y¡Fx9ÊOCìørz‰²CÊX/vŠó½ExqÀ¦³SF R#ù#F}Zëå§)•辯]!�yÈ1µi.˜6¦³úUñÈR�ï/�0N ë΋ó±ƒôCÍ+JÚtN©5ÅÒU1~“â)¡Ã·úîØGø0’ìÞkX>&RZ¿ˆ¡ˆ$†#�NaÒ�ÿܾó ÀF»øçb?0žU@ŸdHv>tçFjC–D“Âä(µ¯øJ6ÒùXBÁ<€›•8MÅýM£~ÂÒJ+„-åhĘID™Þ%eˆ+¤pKÉ>/ößS�ÇA¯†²j#F�ž–]è¹g^«Ç/úåþû³¾¹«gµ?ÀFt6#>:C©îä� ƒ;ÐånÞwÚÀ…°� îòbúöÃVeçD÷žr5Òø}_H÷ƒOP-N=TA[TexnçÜÈYß¾êŽwcÙ�ª<ÙŸÜÈXÀLÊ–¸/éž9÷ñÁ×’'Úûî9…—úÙºW¾Ä¹nQÛù;Úœ Ox»ªx^„6îBlz\Oå×å�…Ú¸zNP§T ŸsÄmôÝëHÁ$†ý¥®ó¥çAôÚ-»¥ú~#QÀϦ,å÷XKfb¡xCùW=¨ÍçšKDMİÉK ¢νëö¬NIEº@’gfú‘awmû¾Gü´‡oöé:+†OÖ „,ÖÛPXhÜT8׳3rÓâTûæix V‰®mÏ�] W‚go8®µ®†ëÅixûow‹©Ê' ôÓ7ÎdyøméáW»Ãi½çáÇÄ›ï¾ñ$ŠåX!ûo>{7îÙ`µÜl$[hŸ§± 8^/&(ç)ª ¢ýMü,䦦8AGr,#S¨gä±ñõ@óH%p�C£1|Ú›*ÔžªÚЦ/h ¶ßÈüÕìáÝùðY~#|¬Ï‹ñ1™²0˜BFáŒz#“¡lÿß°ÿÉø«Ý莨FJÑbºxœí=k�Ü6’ßõ+xs¸Øz½û^¬l‘_%—þ?JfðŸBY)Ò•¬�²/λ8ÿîìž[msuõ*—Æè…N¥ÕUù/߈7Ê®ªÌ\^ð€äòšWÙÁ>½2F«SóLì#“K)JY¨gƒq�Oή./$Îðí­áh€ÿ- ^‹ß¼8[ýA, €H«lYÏÄå�¬¯ªùÿ^^àâ…¸œ‡]^̯ÄCú œñ‡Ÿø×£Ë ‚y±¾Ï¹?ÌóÎì?+³®J£>Êú-pó‹â²¨ê¢ÉåÅa‚4àŸsøiï‹oV*Ïõú¹›¿ BpndÞìË•xU+i•�b7ž±€�J­H«ÒJ]êr .U©j™ëÿSY"d–i«o�€D@òÇ O›éÏ�œÿáŒ1adMx‚ˆZÙ¦. ´S1mW"&¤v¥ÄºV™NmU‹LZ @ʬ3MÌÏÌÉå\åÀ£L`T}Có¡ËΞ zÁ\_%Žð_<êEc¬˜“HTø$íêìêí7VÊ<wy¡Š+Q6…ªu*àe­?ãĺ\7–�›•ª•¨« 0!­êšŒ4¶êb‡¤¥UÞðwwØ£�ߢª˜k9Ï•y,ºÛè¤ö‡Yþ6f ©�"„Ÿ‰s6Ýø3c?YˆXhk½8"WÇYšÄÛ†¬mä9ÐéøxÔ‰/^Ÿ¼ !¡xq­ÔÚ qÖèÄÁ(ž£gj!›Üâ¨-ÍjªÚªl*ÿ*c÷˱÷è?ïŒ[­+-Ô¦—9¤0!1”¨êxu]B®_¦j ›3Ž~ɇ�6=%oJý[£‚ƒWC6 ñzÑ Lš Â'µù6ä;äÞ jk<Ž $$�YLŸ6!5xœÕZ]oÛ6}ׯàò’sc4@�¡s t]7 k·bÍË^Ð-s“H�¤ì¸¿~çòC–m¥J6½ (`Gº¼¼¼ç~^wñÕ÷¿¾½ÿýã;¶quµÌéCðµpœån¬pw­[¿øæbŽçNºJ,ßVÜZ¹–9wR«Ÿ…Q¢b�Ñ�0N »˜±l1�êVºØ“ò—Kë°Æ:™Û×lT ä³gŠ×âîbHúæ;ýðV+ë —Ê],sNÛÜZx, í·�mXN¢w›Wlmñïþ$M¹.†ú:™¸g‹ÕòDç &ø•‹yCÚ— ,ï$˜^3·̶M£�c[‘;mXÍó�T" « ¹MVÕöÅ«‹ è k´•Nn±:ç77Œý䘴^¡¹„]x‘¶À_E›ÓŽ §yÏëUÁ£�Œ«ÂË©¶^ C«ôÊ ³õGµ3f5øNª’ Aã5Ñ ·ÓL*ÿ5œ¬õ‡XËŒÑ$>ó;”rKVxD¦r•1aŒ&ãï7xÐùß ‚å…VÕþ&:bOŒE€ø ÞÛ1ø’aßWx¼‘À —ùÍ„9×U[«.N „¹}#O8Û>TI õ)³ à‡w@%¥M�†Plµ÷�Â1/§¸L¡¡:²‡H@—€î¢’s]<úsÌÂ> ¦ö#U¨X‰J««ôd¸4R›1ȽÐDhG]ç@û$ºâ+YIHÃU6Áé'ŒÌ™Ñ»T vH7ïHQ‰Z hÀ†äã#8©ÝÅCzÛ¶®)û"PêE!Ö¼­œíâÆÛÈÖFüÝ •Ë^ÌRÅ¢åw|º|Õv´P“ÌT9êU�ƒFÏé µ´yP²§£rÕ¯‚#ÐÙ¿[nD‡`Í�‘ôQB�¾Ç¬î¹Ûh‹ÌXˆºÓÍÝæby%g^/æ¢^vðÇtË£É�Ûë¿>×+ƒNô ý˜ÈŸ^ä…&Fzíd¹÷K[¿KÛ}ßí6/ë&Š—Ç”ÄK7_é9*†�ˆQÇlÃó'�@lE •ý1P�u$ qËnþ@ŒÉŠ]�±+0Ž[vµØr³lsú¸f_³W3öòÕõuŸiôE²^$yr¼Ö­éG±töòö›Þé`Ñâć'1;£$X à…iŸçÎ9È­¨ö¾ú;9DGÒÍÏIh²–úƒt ”ïA§Çâç 9QÌôžÇ =?�ÐTJ€œ¬ågQ|18b¦‚–O#�Þ×�³Óì@O�(ÑOlšMA˜C޹?M+'q}RvîößDÙ"‹äçpÁ%S¨ÒmƦ6Z)QòÞ4vöä1ìÉÈø)ÓØS×gÿÚ÷°[õ�¦h5£9p">gZÏa¡\!F‘vÛë>t3?›ÎøqA é�® ?Æ41í›BšÍÂõÀßbVðÖé.)¶¼j…°�îGã¸,ª£×ÄŒ¬®¶þÚ8íGê8íKOÆB�• Cj6�@Óïb …R(7Âö«ß'šsidQ võþö:Að¥¬ëˆBͦihj£Yè£4Œ"„pÉÆ6Ä&Ø´@J»Mä·è9¨‰Ó±Úðñ ¹?zAè‰N„Ì‘ÆsXü‹ð?òÚ°Á¡ÏûÕ ]}+ûéÔݺz¶Ž†yÚ 7‰±ô ¿âEÑ�,Ò$M¤qX’¯µÿ-)‚”MÐ{´e´ù±©ºOÒ6ÐõÑŒáÔ.¦�\>—¾ ?ÐÅì±ÛúðSŽé·yêèÓ_ç>´£m¤�ªs´Ã¿À¤¬⎵6Ì­ƒ¹)ÀŸž>jüõ¥‹r?;ì�(^æg…AÒ¡[×3 3Y#ø$Ëz”>y¡©æ¸A×—ŸNŠ‹ÈVÆÉúÿ†yü� óð¿#þðq­ ‘‘˜lM±\5”xœÕ[{o7ÿ?χkîÙJ|É]Ë’´M‹sÒ 1úÀáp v)‰õ.¹%¹’•O3rÖJZ+iêØ»K‡óøÍƒôäO_}ÿêú—w_³¥+òi2‰?ÏàG!gé’+ÜåIåæ§_žŒá½“.ÓW9·VÎeÊ�Ôê_Â(‘³7Â-uf'c“LÆ�ÖLg¤üdjL°N¦ö9ÛO'Î/ÄåIßг¾—'ÓɘãRç½,öÎ�µÎaJÉRüxy²|Ææ¥:ëßès6Yq3Õ³_'cü…]²É¬wäd<›²¿úÑ?ÓØÍý…žþ6ûU&ãò÷çàz–wxøAØR++~äæ-Hùp4צ¨rþ8 øw �îSòòÅRä¹,/ªVKx±âyÕ¯¨)ûF:ÆÙk^m®Ø ùÓL*n6Ä8,)ÌYB32¹ŠÛ)ìé³"B[Ùbº×UØ5›KgO˜­ÊRÇV"uÚ°‚§K©“ X6\eº�D¶Å½-y*˜ÓÌ-EÂ&¢¨Yæny2}{ûn2Å”•Fd2PvFÞ²6k è#�¾ùOh¾ ®�tv1bë¥L—¬¨¬ó@ÄÄ-O]¾an­‰’°Qž÷ÞnU';uÍ¿Öïe®ó\¯¥Z„UуO¤)4¶äÒØm;>ËÅiر\ Ô¼×Àh˜i– zši“ S?ÚÂÏE�ï/�,ÀêÀ3œ�ÿK¶–™[^žœù—“)a�[¶?üó>îñËç�]žHÖ!B›~t%8HÓ<ª}Áeí¡žljWE6òÌÄœW¹1m’ ÄG¹^øèGžEdgo«âëÛ’ƒ8µúJÂÿb‡pÅ+§;l%m¾À9Jm¥-�«8±÷ÆYÙû”çbO¢ÍÌ6ÍòÍÚà¾@ˆÏR/ãä'�uàŽàÅ“+ðà™pk!mPϬ0+ï vàv®x1ËøQm6DLJK\‘ÊÒhØÓs?PUÅL|jsô ´RbÁ½*HgŒ}ç–r¥´ƒ²|Sð‹•™ˆbz©o_g8èïžVÕ;·wï/3‰šÚRlmXžoÆË2—¨- ›Û�Î<×J dö=(:ã&ßËêOKHð@ʰn*”3£3r-H/5”ƒäç<·¢e”C©ëµÎ…AÛÛËÍ"'•º8œé`¥©sÀl LÚZœâôé¶« äêµáRÆÙ‹™Õyåú8[B_û[¥/,@Ã\*é6`¤¦hñú�£yýÎÁšhþW²�ûm…ÀøX{�Œ“í(ë“Ç�ÍÌ·Âçߘh³yÈÂÞÖŒ¤:q6æc!Iĺ‚Ï7B”�^ŸÍÜË\§7yz# `Ý£Œˆ�ÜlÃtšVåf*L4|¶q¢fë©øûÑ|ù¤Cíþ0t½1‘A—D©Œ�IonÌmJÄìù�Ì(|õ A>¬‘“Xñ¹¸hñŒ/6Œ/~ãD6tÚ>p Ë iÓNšþ FIÙàY¾G (­Rok ¹dÃyfY+{÷òŠ"\JX_j�\Ù;¡Ó7pDHvõZ©„°ÅëéAô)9“(Ý.úò@Á¼3RÊzDQJ|çý1±N|*%±øjFñ `&g|&sˆ0"ÆÃ®,“}Rl{¤êo¤ÐÎ ÈRÞ—¸9”Ôc¿½ìW°E£‚8•ãXi1ªz²�]ÐN¹ã“as‹óˆÔÏÙ’GÿÌCÙ›×W/^�w)P¼`¶m¹í0RÇç{³º®±H ‹F”á–É�+Îæ•Jý¤%H2�r±tû]ÿ{ˆðí¬fíç y¶ó/,à@P9ú;Ët…õ™$_œB�©‹²r´?S)/P9ؤCÜ? Zžs,Ç�×:_¸Nìzà2Ôæ×µyCŠ%âPèâ�®kïzõÎ@q3«�%d¡ðˆfâí+¼Ô[4dkLäê× ù¬­½¦ÎEA�œÒâ¬* TdFÚC0+s¨¿Gžp éH¡>£�z6|ð€æ3} –Ê|Õ sC1ƒæ°X¬¢;FC÷{ªyº}Â./Ùy �BÕN Ñòäï˜oÛ&ßÌ`/˜Ñë„-užá2<ð±µ60šùù°_ïvY]?¿íÎgPŸ‹Œ$[ã8î?�Ç© X!ÊÒâµàð=¾µ,H�'ùdœõ+nEbç•§�IÅŽ†ž'^%ÏPµýÃÊ#Âc¿¶1˜U B§À?¼½`ÔD¥.K—¨;†É÷ÆÇõ}þ;#o)·Xˆ¦y•݉ác_øã/™\MßAƒÈEQó|Í73‚J‘�”½üæõ{ IZ囘œcÖ1¥ÚA>T’eŠ•ÂH ò—Ùå¡xî6í– H@«Åô½À0aõs6‡W cΖFÌ/OÞPÅó_ßO9ža/üÏÀ|zÚÊÍïØ`±öìnzp„ :ôú¾œLûÞRÛÚ³M"Üßûöt¨Ý†ÛáöN`Û­Ëš`Ý9­;]¡‘õó«OÕ:ý÷a Š—ôê?É×4¶ç zënobh§ôŽ˜/ù—„ŒY #ˆ ›wÆ�s]-–Û¹¢O5©yÊfÜJŸbBœ‚¬LÙd%5fyÑmµ,Kú~[K¼Ëu8êiÁžS 6&A”îE¸§6çÎô°`ŽIŸ»9Y”ïðt†Í§�vµv�…Z÷‘!3§æfdö#ñ™"�‡ÏVæÖÄèV ´†1ÕÄ\N!æyøEÝøFÑ&4k´zNˆÞ�‰cšÙX/`ùŒ‰R•Y¨(BÿiáGû¸ 2Á&(æb«À(Š©¹æ. ïqõðÁÅ\ùì˜T€�Iu’hÔ±ˆT´Á(’Úíó¿Ã™Ûý8øè3·{œ2ÉÙ.@ŒuQÂLÁƒT !dÃY v„ºÚ0J¦XaC)±•\±3@û, ›™˜)6míÀ�ô —6riÖ­2ÛPÙ>RB°ðàÖ Æ–Â÷Hâ9û89‹À¸W¬÷²Á¦‰ŸÔ%OSMÇÍì!D­�Q»}2Œ»æÌpkµŽh’F6 ÎëViÖÑJ§ï­Ä“òUP˼îprÌCð†cŽ7Ñv`"ôY¡mÇú­“ÊÞîBàá§ß“‡?\�âk ZIr`»…àªÓ ¼‹·²O4{jØÄ’Ø<÷gèX°‚·y°&PôÙ ÐVg�¾|Mbo,vh›,éAjà¡ÂÝÞ>ØåÚÚC`‡cŽ®-ÛE„>+ØÝgýÏqsê(~>¸íXûcn#ÝI"‘(úv¬¶†×Ö5kžÄG'ŽÛç†;ñ’æíÁŽzCxûcQo¿aaì$ƺƒ}t¾©T“a±Þ4XgRÁÏ•lý7pÁ(ÓÖ‰çèÎ7aLsèW· ¡œl�ñÕT!Xˆ­)xv°½J!U{õ¶nýVñ ÏôÓn{¾?¼O”½VÅÁ¤šF}¢{º‘Øg�6{x8e?ÑuÔƒ9&üAÐ`”ªª�]›6þG^F%~� ÖqãôéÒÉB~ 0PCRûâO –xnÏ6,寸cðЗ%€§þ+ÞÀ ðéFÙé-g0?6\"#<Þw}©vÓ�׈:Û�gDUA8ýG�@i°ÆÒ ~±”âéwæŸÑ’?�=ÍDs"ß�¾ˆ7ÅV ]&QTRnBµT´îñyÎ :gtÌB”ƒ<ß ‰íž„eš> j5­«X<�˜Å pÄJk×ÉŽ×;¡Øƒ 1¾àPoÁ’þx\Ë<VšÐW˜nXø#Sà’Ž¸€Ò’¯Â¥I‹áÍ7ϱ:´•ññ/a?œ?>ÊGD�ühËȹ_Í-uå|ŸØ«´UÎxz“D[ 2@Ñây¨>'¢˜V*R¤^<�}ðQ'+ K�*œªÂÀÝ8寶Ĭƒd�Ä–}õ¢'§èô¨3—7¢ÿ‚P§: ŽGç Jàñ&z{ pü Päö¦9ž=-ØŽ—Ü¡Zì�¡CÇlÚ"³3XÌe.Ôî¨ðHôEºˆŠ“‡Ä„š‘{1Álík~rr7J\ô ¿Sðî“]žAå”+t3`t9:ÑîÍÕ§æ]õŽÃŸF�é�¯þån v �v¬OØ^LxœåW¸�¬j—¹HÖÚ2ÁßxîÓ 6yf q”˜ä�_&yç—9xç—1Þù±V™&êg²‘G²M!›Z?÷”�Ô„nDb[‚$•™¬3ý;pʦ°„Ì’8–\_ÝË7…ÿÆ`¼Æ3Ä�aò3x­%àùd!ºPÈruµMúS"Î:Š$p¢�k`Ú{HÉÅØ™ù¼® !Eƒ+/hˆß’ÒÎBTf‰CzCó.€Û½XWy†˜Ê�{sƒ;2 V’$3 ¿ �†OjWÀI0úNm–P¿ß£™ë×IæúuæúuŒ¹hŽÈåN<ÉZï7*ØŒ )òêÖÉQÆF5i‰Æ@¦ (P?r—Ð ‘ª<çÈga Ï6�†K,QÕ­�­Œ`•�Œó/µÅ¼û«å]ÍJ„Î�†c£¾L,%kàÖMUAc0VæÑ#7m™3IøÞØ9Ø`rÈ7ݱ-8 y6•Ñ è_@Z£n‘”2o�~Ä7;§�ÊQЈh¢hS[m…"X4ÿ¡ }+msÐç'`±¿Â²“„qƒæ H€5$¾˜õ”p²9â:x‰ÊUy ¿e,=ZTµ¾Õ%ÀéŠYG"@ïÖ@dÜ›Óú0Y6¤§Ý¶·ïIÚ‘µ‚©bÓìÖÂÿíï!‹„� ÐÀà÷Ä>�[ ¼a¡5"ìn`qš‚~ Ë»^hƒîÁ"sýÄ™›Ï>¾þ]ÕÕË4.eÇ ä/aª¢c‚´ßWŸ)“ÏÓÞ8¨3ÂÈ™4F pJ_„ãϬ-b¼þÒ/70`š,Ý¡sÐ¥q„0ø¢ƒ>ïí"Ð!ݧ”[©ã‡%a97½3&#HY†±ÈñZ§¤ vB‚ðÊz/¡ç1ÃßI`<Ðq )�¬Ñsrà!=¿×$ xNüã%ϹkÖ, qÙNh $²²ê÷X¥£…€2J6ìF{±Qêñ(Ôf&MøîaðUöÙŸ2éöAž�Å~¤b¢ðFYpø¨ø2 eƒ �Oº†u]°ÝÝ%=L ,%_}Œ¡ÈÂú�w«ÈŽ:™É÷"ÿÛ(Üß´ÿ œÅëÀñÃìû-âQ¨G“1¥é8¢,0ó`—ž––0låA›� ZA9ë>DÇ4ªË m©kûæ8ì.¬ñY»À’\drv›ÝFy¥'½Ðr~_�:°ª»&�rQb¹KâŒY@†ÏYU›#ò!"Bu,» ±%lü(YîÜaWUžW[g0é—}-¦js qÿ´ò€á0ŠGöâ÷ðæ/6Îà9Ñ‹›Ž‰±/Îqí#˜ô½¾-ä$Òˆ9XÓšJ¹¹|™ÈÔ½>*Ú×=A›]ÓO2ø@-[¼—¤]Lq—WÖ¨õ÷;aÌ*˜[*`Î8H´ÒñiµüÄhölê"³£4 â]g[”Lk“Œ|N™|$xœÅYÝoÛ6×_Á©/-ÇèŠÃàè¡i¶mÜ`ÉÖä© $Úb-‘IÙuÿúÝ‘”,%¬lc ´?ï~¼/Ï‹Ÿ^¿?¿¹»º •iê,YôFKø4ÌPRTTifÎÒάf¿¤s7ÜÔ,;¯©Ö|Å j¸o—ËÅÜM$‹¹'�Ër�äžgÚÀ*mx¡%��°"YP"hÃÎÒói¶˜S¤ósˆ)Œ&‹–8s–V/ÉJÿt l,dÉ&ÜyˆH,ìÒżEry;Œ*¦ Œ¯«\*íx ·¶äÛžy£g/S·ý¦bÄ1p³ãnáM[³† £ 0M¦L™b…9!»Š)¨ ­b%/ �=­šÁÑ¥‚Í;RRP^§¹XS±Äƒñë{ áJ–§ÄÂ�°’Ðz-7U­Zî4ÙËLAJͦ¢b $劔|µb ö@ ¤, †4HúÙÑ59_‘Òy°âôCš}p |ˆ@²ðÿ}®™Ú:Ù¡Jñ87Þÿ'€Û4» ¸rÞ šDKŒÃû.ÍîB¼íJRӜՑN½ìš‘„ušÝ¡ZñFNVÆÀ÷'ÄÅ¿ÀëÓ¬o…áÜ!²€/ð�h,‰ëÐ=.¬v´.²Çâ°N³i?ˆ '&ÐÈ–*޾ ã9Z¼ Z�¥!~#`Òa¶qõ°/ºx/>·T”¬¼/æðøQq#ô\¼¢í…‹kÂ<9j"ù”ÏcøÕ¨ô-??˜†Í #™Žx¹ÚGei¯VÖ_òCŽ6 �ôš¯šfösüÒÀ„­¤ª$%Ûò˜‘ò²K³Ëî8˜†ÑXì—×—6Ï…�è[AµÝË,}:êsä8È^uņ™kþŒýС»¤ŸyÓ5£”ÓÚƒÔ RÜ2’-½ââ¢\£É÷­¶W'�Hö„áèÊ%õ ƒô½�ÈßÜÍKdk ýć¶=dn�w¯ù–‰8`ÿØÃ‚¶ÇöØ7ü‹�…ðÕÕàsÉW‚Nõ2¯&4ì1{"Ѳ�eÿޱɯÐ;žkÝ`=ñ’ëi¥#Vòϥœ>á&àÎ�9Íy�e”“½Ù,¼HiR!»Q�t·ò¥I?hûý¤{ñ…”@ÅH#±hb.eš!¸8Þ_¿ƒÀ­øgx˜À¦1´y_19^}qyP_zqyÔ©»Œé}-ßžV�Õ¯€q?²bæKs®nVHa(wå@²™ùT0!ý•v_¿ö=túoôû�SûÌ)=j¿Ryá«ý¡Ì;äƒFïN¦G¥ÀÍr9ªs®”l°|9ôûŸ³–‚:þ ¨¦í@‰8oÝ‹£@?ðô|@Ê«0Ȇª5iæ¾! ž<è2ø!gÄ-'Oí0 s'Äöo'½;×{Ö¥b¦S—ÃïëÂ’uò˜’&;P«³„1WarŸÊ˜í)™ ·esÑ5 ®%²eöUñ£fb ä5†“§·'ÏŸÅu¡@Õ[ �¥o…} âoZóÒùø÷´|á¤þ=┽˜q™ûßµCM(¨Ñ`Á«»üª&ãÞш‚ ­§aà°eMëPÂú%€$áQ„l‘^z¿u!òÞ?¼¬^ÇŸ^äNLÏ û…µ’¡ùHSùÁ ßÙh<4ÚFæZÓ-³\ÓlhS ˜|l”² ¹ÿÕzî~ ÿn÷ ;‹6ò»KÝ�„r6Ù'›¤;ÕfQÖläåruÒÇ`@2%œÅ L£óœ’¡µ^¶ ‚ÙšÒ’G«Ïëv’-¯Û9x¡L1d¡ä¿’q9§KÏzQ´ñ(Ö»n‡|Ç 2ùÿÈx7ï¯U³®¦3RnÐ,¿‡5b±!4S¦%¦D–xFx(/hÆçyD6­¬4âÏ1Â]á…rŒàltJ!ðÆ-z Rü9;Å¡ˆÛ•Æo‹†’9¬zXËÖ`îáiœ[BØ8Œq§“9úÅͺ‹ô,6­Ohe™9U‰“.LÝ7•ÆÒG_ºön3âb°ºå­Š\‡@– O®–˜0ÁöcvÆzE90ù ‡v2fýJV¼â=–jVÂ.Hxœÿ¶MïTódn’gð9¸6†6äÛkù ‹¶`'.P†tX #›°2àúÈ�K_ØUB檻{ƒ¹’+ZÔéfËÁ äø¤ñQ¬›ˆ)F¹ÙäXÁ¥¨:*„îemÆ¥iŠÓ†kÏÄk°ã6À ê‹M—ï²Û�4 “yXCûù _Ãüãcù‘¿³ð懒ìþùWµù­­^±Dã-l}席. (*О B�vxUÁ×*ÐÉ¿ œÌ:”ÛÎ>½dÓåà­03>šÍØa†�Ò@‰¸~óá/o¾õ;¬öÀÀÒ®DGǬ-lIb¦=Ï#îœ!ê½ÆÌÊ�`wÀB©)¾ÞØYº>È!ßüÉ–G8ÄÚ²Å"vMp y€LS·iÓÖ*b ‹_Õ‡†º¹t:£-nœá †pTŸ¸jÌ[kéz� ÃÃù¾Ló6S´Xû ¸ƒ£ˆ|OrðÞ=€÷ rÑÿ]uß{ò”s›ïƒËùtr€í*ù<ìg@P¡7¹«_q?ÿѵ{ÚÈ[ÓwD-Þ›5ú'ÎSˆ ³TyUÞ’_ˆBOÑsÇÐÙeiKL(Pj$s9Žáó¶O ]ÞU[s§ŸöQp—k0feµÔ!/Î#&ùî£Ú4×DÞ¬ˆá*{à Ÿ‚ŽâR!¸Ì˜ýɼ±§ãnµQäýåjϹ£Æ¦bå•ló1ÿ‚ôŠÜ‹Ö¤™æÆÎÈ¡k.>âs+3\ZÒ®^˜J£\KA`g/ã}³éÓ7CËŠÚTß)<¢§5åb)`c×ù`�KL,�Ž;ß|Ô�¶jŽtû‡×žaUèßéíOÊ¢§êÁÙs¨Ø#Íèâ+'¡ë RÅȤî½Õ üúM¾õvòìêÜ>uF5S0³¶åI¶ˆEü¥Î5¼·lr�ÙP4 ËXIÙ6UãRÇÔ¬7Ü2 ­Eáãd.yÆiÚç'Qàê¶4Àý't^„ÞÆî_‚á瘟í÷l�0Þ*iG¶j–Švoª =ˆÑ¾´®=º(&¬vé)gg3.Lˆ¬­‘$Ö÷ârðå“ýV~¶Ð1‡��÷zöß®­¿T‚ûCѬUGÞé‹J�£[YF‡ì¶4yå ÈiSÆ!p}-)¿€SáÍŠº3ŽåUç°Lòip�¾œG¬!ºwÖ]øÂ2TÖ…Æ\·Ü19î•'æR5[¥ÊA�ZåÁÇì`‰©xg‰xy¼Ôý|“xo²¬&œ»Ž²�’¹­—ö dÃRó)ØñTRÇi$D3jÉAOâ2¸×]çÔ644òNÿu0¸ðêi >Ð2¯Ò»á›µZîà÷qd’ì÷éqŸÌÀ%zëC…ä÷—$ˆ—^­@Œ�ý¬þ«'x}ÛÄÐ- ‰ žUbƒ9ë)Eám¼Åᜭ<ÚqÄ|”rÁõ|ÆÈAœ0IÜ*‰çô"õHó4jXöÙ,¿\°2¾T k¹§2Ωú=ò÷TýÖÊ|Pyðå/Bçože' ËYT·�t §A›Nñ6ÅÌ *¯ªví ûbŸ‘ý× `UÑß>?·h£Ó;¾âÆ»èî5±›ª°û9�Zòsà6ôG SêQ‚p%s£&<Ô~b}ÊCMUÚs€�^ô™ÓCyÉ@j3[¥'¦³÷Bšm„SË^èìÕ�@ËRmm ”F¾…UJ ^\L \`#ú±*4‚0©D;+}¹í€);åò–’d>H(ôÁ{îrѰüíp2RÀ>´CÝîkzù³½x3€¼/Õx5þ£Ys¬éu™BHûkO¡Ž—–¼,ïÛ/ëcå@°§ú‡P’ˆ&Rçˆz "»'Ð=®ÏcdÛ3…‘d'¥+à€ùô1‘Œ§2Qà5Oi´óŒìQy6Ÿû{ f±Aç–à9¿ ŒÝ0!›H󉓤;ÀKFÏaÙt6:¸¾š ,pŠYãý~ R·ûœ‹ÑJ ÷ø§Nè.Y�XÏa�>Ò*Û™xí¦DQ¶ì÷Ò‹Ê*¾øÏ¿sé‚ Fâ¨A""ÚQHa�IÄ*ªÛL¹ï²Zçñ@Àþ„Ô/m©³O´ÅDÂ÷j8…÷ìo?™°ý“%Ê Z’ÏŽ‚èD)Uz8›ŠYˆeÛX¿È×Ä©¶žøì1¹«˜ÃÛ¡Cv¹—SÌg˜†èý«Íá8ÂY!£’Y\1ãÆ–­µÒº^>ŸKzöŠQYñ­ëüæ®B.ÆõÅ7Ç`¶†ÄK«&‰V¢þ8†-E:æ×�íÅÝJ ÙiI]€J·`CBüY Ã7! LÓñdi¶Xfälå.$'Jˆp¤q_Aš#omˆAí˜S÷èÅy dÙ£Ì-ÆMŒÞ`Òk‡ÂˆÍ€ ™Ú"ùH6{ÓH¤¨ Ê…¡„+C¦­W2娆®= R@„�ƒÞ4´ U”ˆœxÖã/àTf:s‡f¹tCpF>÷Ï‘!®µI;õQâÉepz¦(µ@‹>PJSpè®°Ààgzñé$Rc1Wêäk.uÒ�3ެÈ'Ÿb×+â&º…î.yÛAÚÇÇÄÚp¿Õ=‚­t~&BpEóðin�޶¬æ¾$qbo·òÍ4~%Θ‡6{�ß¼ÒÍÚ5Ù£›Q{n¹»µ½¯dtVNF–Ö}ÃeïNí¹‹Ë|»ó 6‹>AAí_xÈ+Gñ‡Wâ“W¹‘b�ç=™¦¡[_Z=»ÜXBÇÀž'¤e � ‡z,ˆX¸`5\މ´î›Œîq¥}Û#m&oœ$ÑJ<É5‰ÖÁì!ÎB �~…vÒvq ŠâÀ�Yi•gfÿÕ±P4ë*ÜYh×cíÍ×¶þ,D},Êj öÆÏò` Eì(›è ¬hw{4S­öooYÝ+{ñ"Å@Br¥¸M뮬¶¼3Èg€üír{ÿM uuÙÖ-SÜ¢‡:Wy%©vAŠgA‹™ ~ÍŒÿÆÂP®ðGyêrÕÜAHf)Cœ°©”·UéîBƒæ‹Š7òÆW¬êdàÑžËÎ_³Šdv 6¯dÄ#ø+W½Öî#¾] «éðË~›kŽ/ ±{Mð‡.%÷A…O‰Ðr�»èÔ‘zÀ1ðí‚gÅ™t®+FG ·ó�²9ôõa�ÓYr&•Ãȶ:‡�ó3ámZéô>ü„- û?FGg_t/ÂV«½:ÈUˆÈ›#†6mak§îþú.Vs¡’\l4ÝS¸ó`ðDžË›pÀ�D=Ót�#Õä�ÀNþ«Ø…SEìÔw/.0õÒ*ÖDq¼a7Ø¡Ã7d�Õ‡|O<èôUÙÎÐYîÌö ŽÔgÜK·¸""é®q6ìQßë—$íç^xAï�v][» ªÔ=ÒÖ?Ô¼Áq:àSwŠÿE?íJÞñ`¾ñ`û$ø#Ft¦‹Ç~=l�ñÐDÁ<�È[çÍ©E„Þ U…�س¼8¡ )Ü-Kß×#¿B×ÒªßZ aŸ*}ž=âúzß©õLA9 ·Ð³ÎþÈß)HºÆÏAñ¯%©%ÿÓ4¿9Ú²�õä¢íìn³~õäÅó{´ùƒR§=›u<è?¾vƒÞ‚vƒåºS†ž7üê”VȺ+°:cÛŽYw_=ƒiY<œ×4œ‰³sñìBü;æÍijÓÂvj‰‡“JH3ÜÈÔtyÿ«ÃñìÁnáô±KšB>L®÷^Ù¶fí�íU;©W\/vå‚-] À?)bg°áýóã689‡ü‚ÎãÖ{àšÃ3p8›ŽB>9’àc6`»þµ«¢ýœ\qX\9åèîA|•7/Åó…xn¿Âþåmó2Aò�r³+8©÷8 Db|ñ9 ~û'!|4ûŸ^h£/ŠÞr³]A�V0þK%IÒ[!8Q?~;¿|«÷§Íû2Jp¯ÇsFMŒSˆz¹â$nî\¿¨¯Í©wH©JY3èÉ:wF²¤ÒÏgqÀ€*æšô©s*]ʵ-¾)ß±L±«}x¸k#ÿ=2Op~ä;º¬‹L}rU"M ‚‰¸¨ÊfmZYs_¦C’�,ìç ·âåÎBÐna{ÐÔ að`Y¹êBço!¿2ÂSôÅdf ÛN j¢À_5+×UÚ'óui4LçÔŠžò%tá‰/e»id—hcs¼EÙ™š¦Ë$ЕEÛè"½¢„f]Âî"kª ñÒŽ³9ÈDÉ‹?œÈ„5g°{Òø-R¬!ŠØå‚í b”ó:ó-ËÁª7.|ä4Îy<@õ@}¸X¢o$Ííߘ©µ>•Ì'9ìÓåªåžµ kRÍÉÇNÚŸiLÇÁß{ 9¸QÒí›ý eÀvH'r8E–iªÖ5.à¬>éä1~/Žám{Å­'п«¨d$Bƒq‹¬´�Xa'†CÉcŸ®b:ÙµÊNº\”-ƒž$A„Q•Q{ž•. á¼µ.ж‡'Ž.ªÞžx7� MLÛ(éî´k£ÿ~D[M.Š7û’YBÕ Ó;É µG^ÍõH¢¶h'cHKjU�á08r”iù€´’K™Ë¢œc:ei¢ì>·Maïã(íѵ‡HýIÆ[îíp‹ ÝLYäÎ5PèŠLµë,ÚòòN€Ò5Ãô³Ï EØ}ªÇ6�PË U‚d©j''~¯—½ÔÅ%¸—ºE]Ùí¿†‰O&Qï‰wÛxÒ]ø#[X ¨t¡å? çY/€DŽ;çK5ߦKµiE<5/–ù¾.ZèVFkröÿÈovz]»¦­X€9ÏšØUJ¬�®®K\?Ñ©§7ÓimÄŽ°’TÕûåP RXØÏú ¥3·Êh+ðÄ«Sc"0âŠëuÇ�«Ä5$Zß1¡L¸¬6¼Í§K)Nú]©¶IÛ×�†ëuõ«BܼCG2ÀVl{ùîh÷wn‚QK ÛJ÷A¦T[:5K¸9ìšM¨)×7É+oJJ16Ú FÀ,|Ü SÛBTAË0 Ð"T”¨-hΡ¶MÍßð þ�;Ê;3”NpYüs7`QŸÏûËˬ®”׈ ÷£>/%ØfàLÞ(5ž¬qUV$Ü Òœ—x¼Çšã HjŒ&h¾Ñ–”�ýФ—ª~â9,õ õªY¾�:Vd(¾Éž+‰yVæ‹óçÙ1òÕªXywì¡øE“³ëè;�A'NÒ­‰Å×ë¿âÊoDœ;¡ó$üìÁ�♺‚ªé Å+sSB^vb¿J„8•bY©ÅÓ£—õúõ…â¶Ü}œ‹ù·…®SІ£3ûŽœÌDøœ=kÿÕEü W¡£P›&> %Û…r�&àBâÈ8'CçzÌ—c6¶BÚ9\ã«ðFjøä$œ¡ðÐüü†?lµ':Îoe€ã_=,|îE_µMüÿº!–gîtw Õ±/pßÁy �Iøú6¨k±� É·츥¹ŽŽ,,‰Éóõû;òàÎâ‚ÖÛ䃧óöàÎG…�IÚ°»ïG4xh"„îýâ�mbœj[ Ó˜:ëÌÿ Ì�ÄsÁÜÈôAžx6#äJ oàpðåÎA”ÑRÉ�”ôZ@îÌsxxÃ÷­ß¼³gªÀÓ.Ʀ*7Æ15MFô}¾WÖ�-\©Kí€hàÈõ�Û/Æ+$1xÐ0r€æÛŸ{ÅY«;%åå¬fj‰t(é wÑ™¯�ꆂi§œá›W1Ó ˜=ÒC NvöÜÍÂM‘+ƒ˜ÙÞM*â#‚$lßÐXU´mž(Ëò!5α(Åræú‚½¯¿ ÷1½ÿ¼Îuªñü˜ÝÆh†gµ¹Í4ã3ÃÎ$WJtîb …zµæ*Ãeûc©¥u&cé£ëäµT½¥þ®�+¹eWAéËÌž-ú!/aûP§fÇ$4ÃÚ8‰+ Ïu!]¦9 Ô1Êñ·6/,‹Ç õ®â'qMû40w�Jʧx|¨1mS™áóȶù‘‚²6Ü+³§w6ß\–yÆC‰¹Ú ÀR'�$ ló÷¸»ý¡v¥³¾p ×ÏpèÁ�‰ã'~Ã|pj^–—ÆìLÊpÁAÙ‹0c(ã9ÎmO9_gó/>æ|2·1U<²ñ�ƈ©™ Å@#×*yLllÚprÞØ¢KÐPÛkµM8„âÂ÷‹^è l&Å—¶+˜óTc㼄�’÷°f¢Ë_:~ùÍ„³;:;0UûW!þ«ÍÇc½És½a÷ c/¥ˆmî¹›Ýñ8éŒv�ý4⋺–ÿJ.•Mxü€¦ŸKò­¿q@<¸M LÁÍïßß+bKÒòÅö°=–¡D°S�-=£3ÀÂç£&n�+Œû©ü)·ò°Ç•e$(™ûq�h»ñ¸ �æ­ÿoŽW¿æÿ£)vv,ßDÓ±§çá>°�s;ÅÙ= æ w�“b�qvq÷|&ÞÌÄû™À©Û{‰›‰ðgÃö ‹ÓU�£]H�íðÖ\cçsC�xûÕÌýfë-Œ?#?©ª*«îoîhJK7!6óÛá‰aošËq_ÚÛ¸×]ú©‘Y� µŸï÷Cáy6ϵ‡NÇG½þsòúv:x`Y8œ}å^ˆê=ú|d®Ø–ÊtŽêÇP-wõì·{Au…g$‹nÐq²>®—ñ¸#–�á)Ùo°ÂÁöã⛈uý`}ÖQ³ÀRƒ.â‡!•íWòÜ~i¤�áú~×vÔŽHpÖbä ¦Dö=o'¦mªÕtÄǾp?Ý9vnv˜N<¿µqÙÖ ŽÃÚ$.ÙÅÌ‘óVÿ†¤æèð�6™õ§µZ>­^‰dŠ-H†wPMk“Šdȃ3X®Ä;”¼PÝ¿ûZô +ž[ˆ0ßíå�{yxoG‘ÿEkæ$Týÿšù¨™o·d~;¦ÒíÁG'«¢Î�^@ ¬ð4d®ê+¥ŠAãˆÓJá˜x‹SðoôŒ=¸€0¢Út±µó?q¹¿ÓÝ|õ‚v_¿äÿ_‘J¤¤MP‡îL&Ü¢ƒÒ‰ówþGž-´~ø¼�H½gÃN?æU·ónç5˜�¾ÃÁD¾.ãÆ]íY‡é[‚-=ÞXT2VøÄ_kÑ>³ó>VθøÃÇ‹2ÇT ÜÅúÙÎøp÷k=׺± }g#h‡á£ÇÜ?ÁWÐ|Áé<å ³~Ý­K”"©2f@H_±/öhB_ìï‡õÅEéó€`ÇJÒ¿·%é¶SN¢>÷Ê+N\\’Šô˜‹}s‚ýͧ¦¬ŸÐvü§•ñet#„ïé?QâW$~Šð‚.§Áp+‹�/Ë„tþö-›ÑÑó¥ µÃµþ%ZŒ»xšŒïXrÄW†N‰c"í^™ëq2×™‰ëÚ³|š»ë§rw@½ï͆�|75•~À�ÞƒÁ—UÅó98. øоGÊn·ˆïz×´¯÷w~GVØÞaYØ?0Quß-®³8�úów¯eU“¯˜Ä7.•nÖî9ï~l�áïuØÃŠ`ëˆéf„ôQi}¤[7üx€ácÁÓŽ-üõú1°€R|q‹f7^*{‰Qn©îš.†Ý߯“S©ìBí̧pÁA¹” Ó†òÇ5fF6ò©À l�?´ï#€«/†Ê×í„ÔØÜ®+6¿‚3îÃ3w®Ï39él1Eð¶�²&µ-Y÷½*Öp½¡]|mÐ^Á™E@š-ûèò®'±Gпâ§Z¹5—¯ ø®‚joÝØƒŒ°˜§ GèðB7„Wá[)X-Cتä]j€ÇR€ìNL¡æÓ\Õ˜xØ\™jÔ¦Èõ¥¢3,cìûTgã›Â8J6 ì« W²Êè5›¾ZŸbtç8»Äôf޾½ˆ¦ÒÑݲº©¹…{tÍ�³fíK¦ö½Ë±û8’è㟭ɱCµ¿z‹c n_·Á±7ÈÁ&Íüõ„÷6Â…‡¿¼úÖâ­7þÕÝ5œb޾齝qMÌ‚·4ì•"܈å&² gI[!Ó U×{׳.Än´Ãà�;SÏö‚“Ä_³v²GìÔÏ”×TEžÔ�Zî ²ÿ”Ü[ +vÿÛ²©ŒÊηú×jœTxÌ×á$"«ÊõÚfåOn©wáµß>¬»‡3>ÆÄÁ ;¼¶î;¿ex�NyU�Ÿr}enòw¦ª‘·y�ŽÔPãxà½Ó…* ÊäöúžUÆ—\×µ^ÑÅt!½�Y5iTS´­+/�+Œ²X†Ò°‘-Øxø¿°e+½INòXÚ k£Tq=Û~¹ÿ�1X7hÙ�­E@§›P÷ÌËfJÌ<¨Y’‘‘M6¤UlD/[Ô}ŠÓ àŽ ’Ð4Â@lÛ0ˆƒ‡ðU Eƒ’Ñ¥¨â§FÒU>6A‰T-Ö´kéij}•�_u#}P=}x6’NOIŸoYT¬ ÏBy_¸¸‰„%ÃŒ ÅmNæ‹}Ãý~ÕÍ'üP_¦é¿k·M´÷‡LnYŸ^Äúôb`ܵ¯O<®}²]| ¼e{×G<ØncUíoÁñ‘˜×c’{lûxŒ œ±‘Ô^ÿ„ÕÔ87*¢Ü5B4ÆhK¦„›¿ð�‹|tä^s=ˆQ±¬ ]Ê!üÖT×A-÷û?W{cŸ[™íå~{^�\Ò•@¤­ð¡>Kº;¯<˜Ùó¼ðù¾¾ÙÀiFçíó¦šl©*Ô7¨vXª_u°Û�Ÿ0FÛ �«b¬ir&ÞàYÿÍÅ<×Â@àcöÖ ¼äQU;[wÙ°{-2 F¯ÝÜCŒ XKŽ7¥ñU›x (…¯Ú\2Ì7Ú�ºä·ÓÍqC-K•ʵQñ „ÿ4ª��—™•›u…½èÃñ°ppâ“ï’Ý éÒ¬eª®ÔýÓ¯?s«Õ£tb×mÚþ)NÆOì1ä„ÿƒ$ÿÀøM„  »‹!Q–Fxœ½XmoÛ6þ®_Á©ß¶4BW E@›uÀÐd šîíSAKg‹‹$ $m×ýõ{ޤl9‘›u›$–Dòî>wä™õýÏ—ïÿ¸y#j×6E’’-9)ÊZKî"]»åÓïÒ íN¹†ŠËFZ«–ª”NéîJu$Mž…¾$Ï¢Ž…®v¬ñYaZ§JûRLËbP’KÑÉ–.Ò©!i‘g’µ}{Â::’¼%w^¤õ ±´ø{ŠOÙRWt#_œÐ³€!?:ÏzÖXˆÐ#ª“f @ŠŒXj#jµªE¥Zê,”ÈFTÒÉó$Wj3jíÓiÐ÷J8Óö}—Ћ?©tb©œR4A‹®æ,‰âùFšâ÷<ã‡øZ¼f�}#^+i£’3á4„ÝV¢7zÑP+;ѪNµê“êVB&ÂÐjÝH£>Q%䆌\‘h´µçB¼¯Ã«PV8¼×‰-<})ìºïµqb¼Ú$¢•%Æ��]å*Ú(Ù•‡7zå]À6 Y&íl?¶'0èv°•Rha £*X ˜…º£�`°ùKר;òâÚKd…^¢È{YÞñ¤ü¨@m­›ÊŠÜè~‡�‰ïtF‚°Ân|é›JMK8IQçì™oQ�#SRïöÀá'ÇRºgW½4ÁØf(8Γ)¹åg+ï´:0¨ˆ‡³Ø¨Øbâ�0”ÀaT)¦•¹—ÌÁ¶h ½;qG¦£jœQ�rÝT¢”ÆìŸ*ZÚ1Ž$F@-ý”C•º/n9,ùZW^Cà›õŽrkžQì´#Þ_E�3Çûøñ‘,¬3Ô­\mCüÂ_�ø×ˆ�lÖL!‡(k€-xü\ɨu<8GàkIº:-n>^åµÅÀ� k€£û�ä³AÒèí™ ün˜½!·6]À\êfÝvô0#Oí°-5€}Í3ۛåvh•Þvì\n:0³Åš©8‘z°0âõÒ�tÄþ»›„˜0|—ÍxÌHqãº[·í�ÕØ³Ø”€z)jCË‹ô:¬ßWPÂ+áœÓƒâôȈÄ.3–>†ûÖ‡hÐ0Õ“S­�i½ýõzJ%šïëCSÈžƒ ÛqÌ +Ë9f�úyqÂTàû6ND�Sd½aÄÞ¾�õCÍsÎ�ráóŸ3Es¾Hñ“©Aç¾ñ'dFçç߃ᘩØþ}GØôè°ïóX�Ø�îÆ(m˜<¦0ùŸzäB5Êñ^+AÀÍ íRsîçß)`ÜÎXZecÊØùtØa]Y2¯iNx·%6ù÷Fv©±M‹ãï)ÈûΰÏʾoTÈ>{†l�ËšfözÌL1.�¿�ÍCb›ÕÿØHWÚ ±Ùãã54Õ<ùÇ�ÐåAîšÀ;²=Ê bîÒbüuŠá«‰cçD÷æc�lHÕýX˜nÿ‚˜ÒU•°rËïíœñ©Ž°‰Å—)¨ñPÉDzÆq9·•¾ÜÁò›*×Fi¬�‚üÁÛŽ ãÂuVL(·ÒXt�Ä´¯™çDl]áp’‡÷Iwú̺+½‡`‹§¢y]x%ÛE'†ç¸wÓåôœ |{# $üúî5œòìèÐs—íNl §Ÿ•ÚЬéó˜¥ôk“¥H<^F�ŸZÙPÈ=Z§¢P^‡¯WŽU~¦|p;2}N˜tÚáBáÁ•Æù?tØã¨ãîžñe²"‹e˜èh;.Âì|°ZiV ÁžŸµßñýÀ} ;B9HªVHÿü;ð7R«š·Ø–d·¯U¦PÏÈcã÷ÜæÄn{�C{„'»Ù_—Ík|DOïØÂ|‹Ã¸¿Ñ‚SÝÀdÈé'¯8fÄ.7äë ^§Pߢó°¤Gדþöo©:ŸÜݲx?›…›ß¿Ë\Þn¬:PØxd¨xœí=k�Û¶–ßõ+xg±Mx2I€âÉd€vš»·È£A“Í»-ÑcvdÉ%OÜ_¿çAR¤¶Æ3iÚÝzoÇ6uxÞ/±§ûñçó·_?Ëz•Ÿ%§î_Jf𯕪¥H—²2ª~zÔÔ‹ãïŽNàûZ×¹:;Ï¥1z¡SYë²xþê•x©êe™™Ó^�œžX@ó2Û"؇g¦†Õ¦Ö©y,v€•É©…\©§G½u÷{ß��žHÜáQ­þj€ÿ¯EŠ¿<=Z~+@¤e6@Öcqº‘ÕY9ÿíôÿOÅ鼿ìôd~&îÒÒ¼pÆ~äO÷NOþéÉúKîývžG»ÿ¢Ìº,Œz'«WÀͯŠË¢¬VM.¿*¤ÿÃÇúv°øf©ò\¯ŸØý /ûÅFæÍ°XÎÄy¥d­„#ˆØõŒ½|TZ‹´,j© ]\Àƒ—ǯ”¬”©ñJé‹å¼¬xµ#V"¿Ÿð¦™Þ8^¬Ìñ·GŒoß#ºoÀô_TªnªÂ™ `fâJ×KÒˆz©ÄºR™Nk #“à�d‘E»$!ù¸žY“˹Ê�C ØÀ¨jCû¡‹hÇ„}`®þÁ¡¾jL-æ$µò|’õòèìÕ7µ^)óäõé‰Z�‰¢Y©J§~¬ôgÜXë¦f¤¯–ªR¢*¯ ¢[Ud¢™¨Ë;¤--ófÇË  |e•À\Ëy®Ì}sìJç9bÛEOÕð…˜r œöbBCb?:bµ§ò!Sé¨�%wwH AN´´à’Ž@»"‘xE* ø³õv­(ÊE ,—9ê>H¢[Ê�,å3WiÄ/ Ô®®& _NP®i|ÒŨM°I¬eðjDݬU ë@¶ó-2:‹2ÏË+dj€í®ý�¨×Rƒ¬ŠÊê”·¨Íã¾O¨QÑŽYzFoúú.Z x™mX£êÑ'ð<™ªüG³¢g1Aˆ�¯+½’Õœo ÉW:«—O�~÷ïGLQÇ ÓX†k¾{àÖ¼ ‰ÄE'5ÿÅ �ÎbÐÄŠ;¯�:â.æŽ÷Èu>A}ÏZ/«JnI!S’)ÈlR!'£­3”é\äÉÀpßÂç\@ ý ¶G`Ö[0¬tÉHoÙŠØ3t †QErw‘éÒ¤r‘Ž,KU‡¨„¨rDYÕPÞ+Mć´~?Ë_… $µQÄÿ1±Ž&Ž=3ö��ˆ…®k'Žÿøþ¥sƒa„¥Mœi@ðtû‘ã@‡ãbQ[œ>^K祩wó¿÷¿ ÆšÖý‚öhÓ‰ö)lHŒ "O¬ È΋TõtjÆ+écb1Ц£œM¡?5Ê;eÕg�?-ÀƒûMBø¤u¾õ9 y%ˆ]º6ÚÖ¢tà#2O!§¬9ÄA´{8ƒx+ª+ jø€m‹V;þÀ®ÃóšÜ8ÏÆ³§ù2³Yã}zò©0åJáŸVT€þ[‘©…lrÈV;r«€Q‡±±¢,€w1ì‹»îÍHn¹¸kyu÷ã½{.LTž×•.«=Öë”cÐvÁiL!ÖU9—s�ëE…Ú¡$8�Š$ïí,AåŠ"I•v'T|4kõÊPâÑ}>°Ü»‹üT€ÖnT¾�‰mÙ�è,öÁ;jµÖõpC°vÆÓý˜X[� :-�'! X¬êS…õ-Ñ㈢{µ,�e&êäü¿J­Ê :�ª\2”ÞÌ죜²y.dÂW²*è I\yùýÛßÿ0Q Þcb_ï‹U¨ã9ÊAdeƒAÚjˆ¤(‹B]§“0W y†ª+Ö ?P^| “¡=óʪØÜkb0d«0Ê7*²”n \]—mÈÊ¢«vgL‚�?/ ¡²øÄ·z)k»°OéåmedÏdÁ%Da h�ßÉd•éß™Ø5’‚ØVJÚÜ­sßhÆ\ áåøñ»ªJû(Š>W‹Z”MmM‡BÌ7•ùÔ”OŒÕ¸÷NÓ.•ZŽ£¶Š[„£ëÕ ,̯X® Cct«†¹ÇÒrÊdÀ+o®:X³|¾ÉZ1QÑ” óóŽ Rœ¢xœ­YßoÛ8 ~÷_¡ëË6 mÐC Ûí€ÝݰëË›N´É’'ÉI³¿þHý°�Ä©ÛÍ@�¤EIüÈ�¤²øã¯ßßýü�m\%—Ù"}/ð£ÇY¾áÆ‚{{ѸòòÏ‹9Ž;á$,ßKn­(EÎ�ÐêN(à†ÕF×`œ»˜±l1�êVºØ“òë¥u¸Æ:‘Û7lT Êg ίàíÅ�ô•üçíÅr1ç´ÇÍàñz’¨÷k–ÓÐÛ‹ÍkVZü»ÄªÉu1|Å7ép{¶X-û W¸¹_¶˜×¤zÉüÓ%s`n§Ãf`³ QˆmÚ¾²—¯/¢[–kÙT*­³¨„¹} Œ[?`ÀÖZYœiêZ (˜Ó~"ÇQgšÜió&cHɸ1|OºHž;0l $bg(¢š ŒÈã�H½ÆûÊ8’1mï­­¸3âaƬ6wÆY¡üæÚ8½[Plµ§¡Œ+¾èŒô"Zˆéš,zÅØ=ÝðäEßN"Ü´ÖV8Ô˜3ÍÂF -ÑN1›klR«5®Ò^'®oQÂïðR+¹¿Š–Ÿ£éǼê³ÚŒ9”šÈ—¢®S7ò$ºâ+!…ópÏ7aÃoJ½‹ ²�poG�P�r Ï�L|§.ˆ}Gr„Ì6U%ÔšÕ¡é?:V@Éélë•þŒ¬4𣕋^D.-/¸ã“áö^{£?Î(3xU§ Ñ8]´6JötUŽq½²`¶^átöGÃѹ‚!ö:ôQB�¾GÎè¹ÛhK1U«›»ÍÅò¥˜}{µ˜Cµlá�á–Ç#Ÿ?¯w?}ªW�B9}Nä›9uïŠ[0ûÝ žr¨�?S!øZ+.é>=�úÌãQŽä÷éöþîö• ò-Ý‚®´VáºtD�W%WÇ…Ñ`H¯8XhrPŒ‰.ÅM…1‡.@ìG¤H折 –@}§Tœá~ÖáÙ’#´÷ÉÚq¹ãßqW¥YÍ�K!Xâi^EæÁ±à‘µ�Bä.ºÛ,‹v1à£lq¤äf ¸‹çÈ`öh§cŠÆ(¦ó¼èwœŠÞp0-µõf•õψÍÿè ÷†+[jS�Eé¡ôDñz¬ô4rÛÉàòü0ÉFû〷êh.?ʺdWÄ~½mf”J=)ƒ‹‰¤Ù¨Übƒ€HœÜpÇvè¨)õ#¨·Þ±# “Ëä<–Ò˜«ì8Ø"¬”ç;@KŸæiŒÊ2&Ñl L ºÖµ²xéB˜ƒñb‡¹ é}æ½§Û‰ü÷p£s�õ¼ôìqÐæI…ß¡ôd ûPéHX'ñQŸy¼në™'P~¿bÌë,¡]$¢òp�0™µC7ño‰˜Ñ&JËÜÁZª-[»�×ëC‹¦JÜúOAü¨p²ƒ1ïÖ=Ò^ñE�¸DPgÝ…48†f›¾VÛ)^ñu$?ÐÁ*”‘]i€¨auò(^|i�¢Ân«®/7ýú€j�éCæ8>fR’™ÈžAÕ©1ÿövb2ÚTC‰Zöc\~ÒÖ|~¸½OàõYx3» bÇ}\×»‘E× –íâgÀ›Pk”óÉ!\ùŽW«‚'Ä6Z}ÀøêPÅ>œ Ê~?Þ >Êe$3T^ÕY¨0ÞÁPq;Ö6#kps†ëåäÁ—�C ²„¶ƒ¢­mt³ÞèÆpˆ1C(vQèõ·M^ƒnØ›GÉ©DR„É’{°Õ6hcÀu’Á×W8@^8Þ54)¿`3$*ñs„°¢±GXµg:‰~;Ðéd]«ƒ`c‘aÓk$š=¾†P�›r£^}CõØà÷ž!”vY'ö’þîa#t.›2šðžÃíwô <Ãttè`4GEÒ˜&EEe§ ¦¯±'J­¬y|Ï}rðp*zá6,°Úó—§]o(&s�× š"⊕õÊ?sö¹2ëûýØHu‘=Y�~"mŸ±Hª)m4R�Ä'BñDë0ñz£úçúðF†Ö؇BÌ¿x�q±kPšéŠNÚ7A�d@�Ϲl¼T|é·Y(l8+DYâ¦HÑVKTÁvº‘Ûpô�—tÀU1êyãtô[.Òµ‚2$Êt }ÒVLØ[õ`¼»êKOÖ_*øA—ô3‚Š�•O¬}¢i�(ÖÀ^Þݼªq‘ÆÉëW¿T9Î㯄óðSäÿr˜×ö ððœ S¸NFá�àü)„å,�sm„½<©íôô«“!<·Òæbü2çÆÈ©L¹•ª¼–¥àš½v®23Ò˜d4ô´&*[!åó±±0ÁX™šçl? œŒ8+y!.Oº†žu=<�†—ºèd±s¬uS*–âËË“ù365@(UY· ÏÙhÁõXMþ;âvÉF“Α£ádÌþâFÿDc4÷gúö×ÑЭ2V¿=w“|�‡·ÂTª4âG®o@ËŸGS¥‹:çŸ'=ìÿNá«ý5yùó\乬^x.ÊÆ,þÁ‚çu·¡Æì;ig9m£‰,¹^¿°’Ðg Ìä"HQ˜Óg'4—$Øâµs‡öpg6•Ö0žnL]UJ[¶©Uš<�à f³sÁF¢hXâv~2¾¹¿ E1f•™ôS¬–÷ ‹×f¼Ìv8'Ú»ø—¶œËtΊÚX‡/LÜóÔæ«„Ù¥"JÂ…ª�Ý&LvÚ�YþA'ËTå¹ZÊræWÅ�yú#M¡±—Út˜ÓòI.N½Är!дîÙÚFà ¨³™ÿ6Q:ºùj 7Á{�x¥eno5üŸ³¥Ììüòäâ«?�Œ Cì<~ñ÷ xá¹Ç7Cœ7´>,8"ÙúÉ5¨²úIãã6‹‡:²~¨YaØÀi0S^çvÀ”N¼Ÿäjæ¢NyÆØ*4iY!KYÈ�Þà™3ÁreLã_F¤ªÌO}!y™Š3â Ú'È[1@Óò£ó‘>òh™ÍD¸&BE¼²ATƒ;©k8º(I•/„fÒ4äL¡�7#�¬ • &€‚^JÓWèk^L2ÞGX^[µÃz´G¥3J9••V)Ï™šº�e]L@"ø¦&Fè…S°A« –ª,Å -@t˜Ä5¹DD PãUxä0K,ÀMÎ* î¶jO¡ß9 ïú ‡…ƒfY+zÐGü°1UVó|kèd:3[óø©³i§µ�´SS PŸÖ|ÂxUå�@‚ßÔº0`Í–\ r7m™œúŽ=KÜ.ò&ƒ¿•(3Ô!Y’C¶¼á¹] 70ΤZN`‘‰¬ë©ß¯!Ý«ÝW¥´ü#Ub  ,EiÁøvt¸ Ñ!Uy]”Þ1:†]Ó0Š=Û;î£ÐÊôå^rÓ‹{YZ¡SQYd�\—lµÅß¹ç6(q‹;ÔúRÈÙÜ‚¢9¸&Ÿ‰°w–sÐ?¤1HBµ‡H–öD@)iÌŽ0ÞS�ž<¨„÷s�PƒËN1›)[u0ÿò@B×€“U¶çò·ÊØYЙ@}µ\(€~ ±›ÁÂ(a¡êÉlÊsÈ8æðMc¯ú… µ4Ç>òòh !«Õ¼„T2�õ¤j"ÀØ-îõ6ç�ÊÁÉ@?{Y+r‚e†{¼H66.Q>§_8;X£ÿÐ=Úæ·€ÂÇðQ¹…)ÌèB¤Mœ©\ªgU:çÊÉÀ¦ÃÂ-R´IÂy#É4‚�Øcú¢·éüdÔ{åz ;èt‚C™�±Á7Zö“-þ�Wµcé] —¢o¬wÅË[n÷‹ÿΊj‡àAî¾�£²®~é·nàæÚc€•—s‘~x©J°ÅL<„Æ7›Û³a:m)°Išþ |ÑÊ{1ø™ŸÐ�¡B“u ½hÛPÊè9‹aùY.ÀÉiæ¼ ä‰$&–²L±ë~TöUð÷ˆþ¼ü&)½z.Zý¦5¦I5$)®BüüRs#OoÄÒÛ~ÿ°BT�+<;Ú\£ÛûSÇ»�Y —€Óâ3Ø@¥«¦à­]UÀ*–€Q; ̧~øš—+*º}¦Žée¦UU‰Ìw.\�š¸5L›Ò C+�Ù\GçE$+ŽZQ¢ æB=e‡¼yÚÄ 'B²…4éZG »+T=%ì�+!DRg»¥„x>•"Ï‚ë¶jŽ Ø$~Ù�F®ÌF®��6�‹øeIò¯Õ;Â=PÔöE�u‘¯“œ6Cr×7x˜õjP‰¢’øÎ»Sõ&ÐÖ¥Äþw;*nVTZMøDæP$ŠÐäX×e²O‹ñ»[ Õ¼#€u®¼«-çPÀr jê©[‹v˜¼J3‹ùžd&›jU´�€õ²b@æçØX(Ýwî\÷õÕÝõÕ×};.)P¼Ã2&ÖÛ'µaqÚäX�(�(C‘ißWœM!€¹I~YÞ7Ó|ïjïýˆ� ¡¸UEõºk=EM-§Hð (  ˜ËT�­\r@„oíV.û¯=pCÐu ¥‘½]Ú·ÛÆ:Ç9¶Î‘צMß»)J]y\æ³¹5Bfºù}¡ R¡™r[¯‘¬‚·³ZÁ0áw _ÑMœ_`ÇGù]ê<û‡‚J°uÕОmÛ¯°a¢^î·œz�Y]hÈŒ:pfUÎS1p„C¿jJ­§Äílxá�eؾO¥®%ÃÖ…;U±Gs‰[ÍmÇàèN¦†§ûsvyÉ."¾®°·Ä|®…€äHo‘ƒµ3pdëÀÕm¥€87ü&†!×rÊEFÊj[Q RîSo=§*îVØ1´z{$ ²âó¡@Œ—*NVÀdâ»2ÎdxžÍS(ã|X‚…§ÚŠg¨óÆ¥aáA¬[šZ¤¡e O_0:š¦C¤óT4»ëUÔäíÞRáÀià,œB… T^gayèŽ]ðC&ãÄFÅצ«¿£©ß™ƒYÿIÏ«±Ï^ Ë´¤Ö:É¡(@¤×ˆY2µ C œÄ;Åz"Vʵ~¹íÌr»ŠIs07®Û†¸ŸÔ4*ÛB|VÆQålüN`P2ê9 ý£„±gPÀoñd%JÛ“ÿœ¬ÎÕÝÉ "`(¥A\Oø7iϺ›Œ²+ã2.—èÀ¾�¨»u�pÎÊã£Ê±O”3;7®1ç&ŽHˆ˜aøÖ¹yo«¦ÆÄmï5e†$@©ãJÀE|šãSZß÷§î;Sh ÞÝTÃ)Å×ZlµŠô¥qépt‹ÞéÔCÜ|ù7ÿ²I‡ø2Ô�—·fÐPÈC ·ž57-TŽ•¼hãÈŠwî´oI�ÞÇYN7^�`‰ßI* £Üº� Ý® é*”©.š¢Ü�Gà÷J‰ŽøœôúLÓÎÆŠ ¼ÌµD)W‚XV ‘ùš/å%Mú0Ú¥ ¨BNÓæ8¹XÀ*0ŠÒ0n\Küs\Ý¿°Í!É8^À6—åC0N£ŽEñ"ð@j7NþÓãàÑÓ¸Šu$g»‚H¨„‰b±Cí(ªô*-gžØæŠáе¡⺷þದ0Ãj8ÁÞ¡ „�p=‰e GŒûëa#»+¿‡" "I—„+­!ácëÍ„8nÄ瑈8é[ØÙòšý(DGP�C¡³öÚæ Gn/}E±»išaö¢Ž× î’õ㮽¦·µÚšj’V7>v5|ÆV¡VÉD™ùÚБrÅnÜç<%”%a$Ž9!EŒDèwÅÇëGw;›Hž‡÷¿%Ÿ¡�âk Ÿß‡«/…àåZx´û§ý-gÈÖ£ðAÔ%É z×\ã1¾ôÖÏåGßC�ÍŽ'ñÈîR.¹­@Ý\ß�mŽšLï³ÆÛ=šþähÛ‡·ÏkñZÇCX‹cŽî)Äû™ý®X{Èú¿ÇO$ŽâçWÂÖk?æg‰°»8 CÅØ¿§Ò°æH<:ùÝ>­ÒÜúÜ–J¾}Àûÿ‘ŒîÑÔ'Ç>¼}ZpÜïGì Rcsœr t¾«Ë6ySk÷¿'²Tá×Ûw¾½ï¦Ññû`ã�к=�n^Š{PŒ(ñ2ñ6Uw‘oûÖ¸šÉíU ¼ÓÑ®ÛöÉ/5Ïð*\�V·=û�¬õ�`dD¥ák4üƒA)ÛœFSObh\'½žev¿¢rP<7×½ïÀœþyhÌlÌi_. ä¡º_ªÌWþBØZó·!6ºÿuý±(^“VÁEè7 íÏX¶ÏKÝõEiò¾Žã]# |Œ_€´dvúÀTæ¢Ü•ŒÙ; Ñþ,¯ý=¦êœáä>fo9ˆ †Ó¢“lþ ÖEÏ8ö™‹ÌyæA”S(ª´à€ž~ذáÐÿÞwH¿(þK4RÀ  ‡ ‡ŒvT™p ãxœÍZ[oÛ6~ׯàT`}IblE�au´i†kÚ í�ö© %Úb"‘IÙó†ý÷}‡ÔÍ�ì&+¢Htá9âùxîd2ÿî図÷/ÏYîÊ"‰æíMð ·R8ÎÒœ+Üi\»åñOñ ãNºB$g·V.eÊ�Ôê5—kñ‚o…�Ï=šÏšy:ÛÒ¬?$Ö�Ù:™ÚŸÙþïÁÍ9S¼§ñ>¶8™Ï8Íúã$ FóŠ¥ÄpçOÙÒâç¯ß§:;�æ‹s- Ð1ŸU4kÂ<•yrÑ}žL®[Ù¥=~‡ÏÞç‚Ùû…yrø–ɲ*D)”³Œ”ÒÉŒ:¡Â0½¸©;b›\¦9K¹b•™L3ÂVZY`\jÔذŒÃ®µ•jÅ\.¢LÃßʆñs��DÝjŸeE h¯äƒZ V+¹æFâ�eBY鶬SX WYä_aÚÑsFp,hz ¸!F«L’rxQl™T™¨.ÊEl0Êâç>a^±×Z*Ç y# ™k�1½„¨Ç ³öjfÒ 2êA•ÑY U�Ww0šUHa¨„Q['Œq©‹Bo Z£Ë¨ƒ‰ºØm}oìµ~ÆL]ˆg¿”T`¡¶.«œáF[Ìfjqĵ((Â:R0´@Zª¶‡j8- 0hŪQFÔ1ë5ˆP$­Œeë-�¹É¹c%¿Á%\ €‹ðE!@ zŽvlÝ•\m;� 4 uˆÍÀsÎȺXå½>¸2)ï–�.¥KÕ¢e\Ö*õJÓf z¸Œ…êæÃ9ÇB{M¥Ü˜Ô›[oË�Œ„®1Ý‘Ï<LÔŠ}9 ¾­…Ú{€9|Rq2|Ã’Žž¹(»$ûÄÉÍ|&ʤ‰…Ñ-¶ë† ŠÎEApFÐ\vÿ¾9ºþ§Á{Þb¬´)yñ¸UZ/Ñâ"(§¸�f"¤h¯¨KÅÖ"g§ÖhŠHð0Z;²§Ê¸Éàckéíù¬³óãa”‘õ¢ÛÔ£'¿yîáâvb`Ì9x´Z%J°™{Ö EŒ¡täˆîÓø6—J|z%¸Qð¤*o�‚Å�/QñÏ�®›‚¢•£|Û�üIÒ͹Ïy?a FÊ^Lœ\v$S=¡ÒIEw“Ì]Ö” {OþhãÉÇ8ùÄ5¥Š„à×À ¾… ƒ3@x8âäÃŽË>­¢M�ä-ÒóïVdqÒ>�á"Ò B˜Š$E3üe xWqr5èÍ `m„\ån»]Pâï“Jœ|60õŠjÇÅ:$Á�s½™ïëºh x?à –uLË�š5ÚR�g+ê]Û§1| 1ˆ© H_×âd÷ýpû>�:xíúˆ•6  €Mƒ=ã ¢“é4 xÛ4ã¤]d¤ÁÛhVj軚žçùŸ¢XdŸ»Çøøö–s çoà/Dh ÷Ï_*�MÜ6u¾ží¯[ðu-älJù—g‚#Ô…Ã"¾È²Ç�ˆÔ.+ß™ƒéÁ$Ódl�(Ñà¶ßaE�!_ÈB†6j‡Ñt2@×1hÒîîóý¶l \ïR4Ìï WýI'»ïcX=ö� Ëdæ½µWŠÇ¶O;Ãû4MO�>§> Šv<“€y'W%‡�évÒí½×$Ç·ŸñžméØ~ yuhçÁÎyâ>â•X#åïôah y ´‰4ë&&ÁöÊqîcH…ÙRk—Ó)J{X2�×U¥ 2ióp'„6ðNðJftÆâow·@¬mˆ{Þ¬ÝÏßéˆ ©¶çáíë†Ó~¡=Gß7RœÿËQÞÉWXôn‹k¢/î±÷ôr¹í�À7Òåtüe¸TÈGÿ¯%¥F[»æp”öiÜ ƒ(³`—v1ózßÀN€X—'#4cx_"@}îô8)°¼%èø|~"À%7+©â$ÜøLÛíF†§Ö‡$�Ä ]ï�ئ…WÀÿâ„®w€GlSÃ[UoU�–s½b(‹ƒƒí~ñíÿl²œ°Á^mqÙ¦³áÛÁœv;¤|r놧 *ù¢‰¬Á˽ÂëÖr&Â}{¼GÄ}+į|üu�÷Âo€Øòµðn'ÝãèÞD”ƒ}óŒÕà®›5ÿm0 ÿËð/g Þ —uU¯n5%xœ½[[o¹~Ÿ_Áú¡Þd»ÀE¢p.E‹Ý\°qëE(‰’Ï µ$ÇŠRô¿÷\ÈÎhd�íI�ÅÚ’‡‡ç|çJfú§×ï_]þðF¬}‘_fÓøCÉü(”—b¾–Ö)ÿâ¤ò˳¿œ\Àw¯}®._åÒ9½Ôséµ)ßI}§^Ê�rbcÍFY¯•›^ðÔlzHÎÌb‡üté<¬s^ÏÝ31ˆ¬É¦R”²P/N­8ÿ|r9½�¸ÅÏ9<ÿ Ô~†91Ç)/NÖ¿ˆ¥ƒÿÎàOæfqø€Ï"[;1�]­lI+¦¤zɹœ©Üeüu¡ïân…;ûå„'~ܨ9l¡B:!EnV°].ŒeU(«çbnòª(Å�š{c'8B3Q-rîüi­Üe4 æ*Ïùƒ0Ëd¯vb«ýZøµ¤ˆ[Ì`fZ³¢.Ó;i/?M/ðǹoä|�ƒB—<ô™‡„vDÆÌœ²wÀý¼9°X+0˜ëÖ*·1åB—«™f‡ëµv�<áw @93e¾;‚»É Ñü§!šÿ4¢æ?õiþƒU � é彺¿ªu\HoõW�Wé¥.QX(ÜM‹}*`¯\lKí=ÈÝ”çk)à”™JÄ dœ*½ƒ¹)Í`XËY®~œY.„")©³„uL’xà �Âv_ZS_ÀÍ„ñCÒøàô§A8b¡ÜÜê™rÔpi¼¸U?Hû §›µ²ªÿøÄˆÏ1i ðV!‹, ÏŠà­{¢,ÄÚäÖ\Ý�é"Gh±yj¤"éXna\³ñ\�•¿ÅqÞ87‚ Õ É}p㘨 „:€cÁxti-?ÖqD¹*Wð·L� 1éÐc�/1V¯t t:V‘‚`+E¼©è¸üZzBLd;‹|'âïµ§Š�ßMˆÿõïpü‰Ø®U)@ñv—2‡d3¢ ®Ø¿¹Ú6h @&¸F)4ˆ¿\µÈÕ§ÉZ3PBqýVéÕšØû¦¬Á¡0­Þ5ü-Ðn2áLÑ2ÍgÞÈœÏwZ”§µ�B$­èXÚƒ²ªÑÎÒj¶…a²œð±¨r£)´ÌñH^Û9MØ‚1)r)£úù›!†q3¢EÜô™Âû¤¡ûýU@g¥ ›‹xÀxœ�îÛô[Á×຤eAH�ŠÜ+n"Ü¥- @.XSjb³M¹ $5é�o,qßÌÜðQ#p2±™•X4h“q[≶à¶Ù´ÖÁ²™IQÄD?ÉH íÜTàšGÅÐ[4û�AŠ«¬‚¨Î’ñµGym7Ñ‹qÜãȳ6Û#�£®æ¾‚88‡¤p‡*fÆ^'öþöuµ«ãñ_•-U?FU}¬6ˆŠÎܽðëη�”v¡¿©×ॻ„þ Ç„�;‹Þ*¿6u΀'äï×»� _“¬`ed˜ †–´+°/ÌYk;‚Ÿ˜ªâ’5½€ßÈõrV�%µE@dHŠñLfKŸƒ¬Èä˜ízƒIÅŽ,¦"dÈ¥‰¨Þ“p�ïÈ)æY MfŸÑžñî�Dil!ózqŽRAÛ¢yè,špè×lo×x¸[R €Â ½çBK�çÀ(˜wŒŒK-%8rÇv+ãÂ…*�öMtÒݲbÜåjé9T¦Æ.]�…f�7ÑwU‘øØA&ÚY2¢‰îQÞ7Ñwu1•:×#º1 eØ”âÕ –;ªw1=){‰rT@Çl¡\ÈšD†S`*æbê;’“³>MåC(ÉLé¤ÐhhkÊFnK³-TÈ1¡-%{w²�F„T3•�,�çÈ_êòÍb¥Á#Î É}@À˜P8xIÍÞ“ Ôº˜Ôµ ¥´õ÷?[÷GežCG(І�—ÝJ°g‹ ÞÜ*þx�ê›)ЛŠI%’%QгPh-1…�Ñ à(­nàAEDп#Tß­)‹u±©)•ùVî\@ ÃS×y7UM35—€N¬qîìNæ’|*Þˆ!ŒbšR!i C/®Dù³t,2ŠKNiP°1øàÎË*Hc�‡“"‚YV¢'E:eÃ.�ºÙ8Ø­[† ¸½bDw ß×רke<ÞÀÝÛ�{íì°·� s×\_P(–àפím%©Θ7Šz!˜~ýé´›…œ~ý¹þF8r˜Q�êž^zW†|b-ÀAšî]8¢ÂÐïéd6Ó¸PÂ/G•+ôü¡Ü ™V¨�™½¦•�{ ^C¸Ìè$º/ïx¼Æ~§†¥Shƒ:0Éü1»0-²=�˜0Þ¶ÆcÆØ1¿Ôúl—`ÌÊO?×èlíë<Qäo¾nÀúÔâáþ°åˆj8´Á¾BâÌÄH¾¿£Œ³P‰8Å�—‚«ú£’yLÿº'`�Ve®]'4V–æX¸Ïv«âñ8¢¢ÝÊôŒ#'ÿ40Íì1=dBôþ žèÔB¦ÌÊx’¶'C®‚â•Oû"(Mõõt9÷Ì„JÐ’t€PÙ ¹ô¥A �3nÛ$õ2$[ãêûoPâÛMl�¼ßx]èo4 –§C`p”Ȉè°W¯Ó¦R;àeÝ¢!LBä^øL�·¦\]^qJMñ|z¾Æœz¡`¥ Ý÷W׿]½ ?Ó¹Æ>7• æÇˆÇíÌz+o áÄ9È´ ïŠ�ÃË ;(ðuVÌ8¡ª�üT3�+FjÄP¦e+ª„«€:o‚å÷0²pI´-Iå?`}:,ㆉ£&ÚDï�CáF;,˜IR’~LŽÕSEÒÊŽ�›eL„SWÒá½&3€ªo5S�¹*1;["l›Æ4ÝKG~â}5}<؇ÜÛÑÑbëÈ¢a‹kÎ�˜Ôí†:A�á0V•p_œÖ}CqElÓ5Ö‡g PQ$vÓi0T†pà”Q߯T€Of~Г/?F……gÈ$Bæ�"ËÖ5E=Œ-ÝúPªŸ\jêírfr`Ê—Ðþ�¶±LD¯EwÜËeˆ—reð–'òuÞ‡=ÿAzñ™Æ�_ë¥æ‰Ï�scÕµ•¥Cs‚ÔöŠ1Û%¼�^š�MMžr<Þ_Ò,+È|:r³Éuè˜b\y*r´V�jJÌ€ ´á³„Ü·åqJkPRŽïn¢‹@‚þªu¿Ìšë§¿©vobÒô÷KÕ4 r%ïB‚V3˜A‚î ÄHµx¨â÷î†è¾çZh4õ÷о÷õL2ý©µÓýmUFLVçËõ=xÈ穃øìÀ¥Lqøâ$ 8#ÞVVƒ.(«1ï$«ûÕS(yD-ˆxšUßVÌAü!�L{}ÔÊiê×\:RJÞ'Q߇В­õ‡rŠ{|àÜ!aä8Ð\4Ïñåž‹Zâ%>TÀEk•ÓeØþ�hzXK›Ù“ÔùQ¯ 9ÈãÄ1=0Ó»O¯Qúà¬îô€+,ÔrÏšZçÒ3uDåYKçORy–ê¼�±d�AÀ“пÓCôwy‹phƒ}Ìü•½cËc6ß#î¼0Ϭ“Ëú¢;É:k�߯²ÿÜN¾ü·¾5§Ê¶SŽÖž=íuH‰Ap2Ë[š„ÏüÖ•+ºÚïvj|p†uºÍ êò@œiâUÆ·­uºbfçüZ#Fžf_€…6åv ¢ònÌ`•\üF�(xÝÁ‹¾ÏUG¤½�f©¯'ú.=¾Ã=nÒ ã§TEÑ%k/ ‰W\ò‹+ü㸊5Dqñ¡ÐhÚŠ÷UôkxÒR˜ÐŠYùHŠIK‡f¶~æ‰UÂ!3Jô{LI#]‡×Xƒ‰øpk¼T"R 'ÇsÇRÓJßÕjbÊh42;¦˜ú¡DxÏðS=�ôh•_Ò�÷p•é PÑ D±ÅÙCÛ�}M¢aÁ{’¥¬7®ßc´˜µo…°TxéÁÌÁ!î"ü#¥ þ×Pÿ=DyŠ BB•;Vªz5ïxœÕ[m�Û6þ®_Áóáº-°»¾(¤^¹mŠ+šM‚6ݶ(-Ño%Q%);¾_3J¢lË–íÝ$ìJ‡óòÌpÈ�üíÛ7·ï~{û’¥6ϦѤþ!x?ra9‹S®�°7£Êίž�ÆðÞJ›‰émÆ�‘ss+UñšË¥ø_ Ãî„MUb&c¢‹&cÏo¦’5rÿrj, 2VÆæ9;ÌDÎ ž‹›Qùu߇Ñt2æ8íW½"÷Ž…¹¿‚¡%‹‘àf”~ÍæÈ0VI¿ž³É’멚ýw2Æ_Ø ›Ìz©'ãÙ”}îFüJô—4þ7zúb2v³MÆå‡“äÝ,ëÈò£0¥*Œ¸çú5Xá’l®t^eü’h€Ýàß<Ú§�é³Td™,¿ñÒ�¹ü‹%ϪÝœ²[-¸Œ³ýbùa ä±½Žh|"—õ"ssõõˆX§­eôý€H`si ã c4.ö …fV1› Vj‘ÈØ*Í8òe¼H�±ŒÏDf:“]3öLU–J[‘ø…�÷»_ÇôæžôI’•\jM‹Ø+#˜šû� s\Ì<Ý%[¥2N™4NŠDÅU. œeÅ×(?Ø>ÛT‚p1žÕ®¤V5êÞ©b1ýI€½2£ÀUÆþUÄ eªÅüft|d!þx%¸.d±¸F„ÿ; 3j„BxtŒÇ`Ä!Xëu;Z=é9HêÌF@ó ÂÆ·¼Ëüzû˜Qøû¦~+Ý�®XK/þs¦¨Ó:FÖ¬+òá•´)D§Õlš›Fs:Ù:D’°†Û ·Mm÷[¹ ²±?i r]£ü^Ó#ížÌí"!å/”¯^ÌFNXÚ{']Û¡Üš­£š¨“3pØÈZ� ”' X’R²c…ã;­Ê#Y$ 1Ž‘î0!гÅ=2e4¯¿¹H­ ^¶zj9>‰„p’l‡Òò9)´Ò8ç¤×r†Åá7>.]8cˆ3lþ@PU>`5Uߤë¥jâ( ÅpPã¹~°@ßÕ¡y 1 ¥¡Î“ÿÃþ¾Äº3h½Q'‡û²±I9®wAìÏÍa޵ãÄË2Œ¿Ž>i(ßc¸�äCdûkÀx¦Ì ³m¤;ëô%„böÁaü>ÔÙõI2="l÷ÌV篋áÈô$ oEs,ÎÅð\2¯òèЙÇ_¤ÒÝ£“��Cdû¸ð¸ßÓNî�’ç*ËÔ “¿*]¯g‡Ë;ñ®üR@vt9$ U\ázÑÓLéDèæÑän,ÞŽê2‡ìUÈ€ÀjøŸBÙ�Øôfô峌hÝ­'c›†4ÏþYÓÜ·k%¢±¥ßè6–c�tYû./¨ò»ªéâÂ�$$u3¼ét‘g²€ŸKÉ‹X45tíS>T1D·:Êþ›ÐZé­�ï°Ó-¬äÙ6Wì߉­!™ZÈíYAÂÙÙ牘ó*³_0¬²<ÑŸO4 IÜ´¯i娶}êÚØ;ìT× ,0…–1[ w º ·ý¾\¼¤9ì[tŽÇ}„òŒ�«4õÂ1uâV7,ÊÇhFge7Q!×CÄVfz¥¬‰ÃRžuÍ©½ëÏ�ÏUe!‹r˜\�nr‚͢ФtJ,mš£lxªT‹Z5çÚ$ 6‹ŠÈ�µ@fíÁ2â´Û5ÔL-ÈÐí\‡ ãA$°Q#¡\¯éÁly·(�‡¶dÉäæ,ß—Öeµº×k‰ÚSlÚµ ‰ª²Ä¥TwÍÀåÉ#2ð9ߦFÙ9çÛþx;:ç|»=ÞŽÎ>߆¼SÍÞ¿~Ò?ê¬.ã§:ë>ãæÕ‘÷­¶O¿ÜÅ«æõ°ó/w}Ð9&¸Ärïq“�È}÷˜0k 1x·Zá v¢^«¹Äiˆý�@ð3xtsM¦¾ƒÍë¹eª²'yÝÝàƒœ€ü12H¾åww½ÇÃ�)¶¬¼ÇœGééåÀþjCü:[:zÙÓÉÚ¹{2ý¼ظhˆ­{p|ÒË9û&}‚ýù1–B`j®¬9xò;�?¨—йÀ1Ô¾†/EN·0Û·!>×ÿgÓ€Uoi9—™(ú@Sö“»mÚ1}ø5ÚF˜£a8ÌP�µ-”“TÐòè�LþöSÁÞÄ›1Ø<2œ†ŠPW©yÔÞ:7¥ˆé´°p‡P�M2ÎDŽÞƒ5¥š¿k‰×* *ò¾d×¥húâÞA½|ð¹Sè¹íÒVVàO#ÚæO¼I%Ô:¡ûøì³{§Z~ÝB‡Õ*¨}K‹ãïR|ÿǺ/ш4 ¥Û¼A4ô–}wÔ�›ýW4ßÍû¿¸ÓßuýÔàc½ 8 Õ8§QYÏ&g3xœí=k�7rßûW0ĺf_ .q¤Õ:�|1²’…“N¶§›3ÓQwsÜdïhï×§$›ý˜™ž}H²cÀ–f¦ÉbU±ÞE¶.þé/?¼x÷áÍK±²eq™\ø¿”Ìà¯RY)Ò•¬�²ÏŽ»8þöè~·¹-Ôå‹B“/òTÚ\W¯USËâµ²]¯”]éÌ\œòÐäâÔ�œëì8¿4曧托 æ$RT²TÏŽvÌ8ÙñìèòâTâú�w¡¿ àñ¬EŠcž­þ$À¦:ÛÉ’'ââZÖ—zþ¿§øA<ó].Nç—â4é'ž2cøÛ/NiÍ‹ÓõçÆçݼè`ô7eÖº2꽬_Ãî|uø-t]6…üêðš°“ðß1|µ‡Ù7+Uùú©Ã© è~¸–E3¾¥—âE­¤UBŠ½È¹™L›UjEª++ó*¯–B&¢¢ñðë}Ú&J˜^œ$¼t–_{.•æøOGŒ #1`Â.ã2A¿D­lSW&2w�bšfb“Û•ˆÁ i„])±®U–§V×"“è•UÖY0fŽgVr® àØ0ª¾¦•�ȫΞ1 Zï"¿L3~òD”�±bN[¥ÊÀ;iWG—¯¿±y©ÌÓ7§ª¼USª:O<¬óO¸p^­KH‹ÍJÕJÔzìHu]“âgÂê.vHZª‹¦„ÏÝa ˜¾ ]'0Ïå¼PæDt9¶É‹±mŒ¢Y¶¾SzB¤hŽéþàéÎÍv‚Ï™`Oh-RÐÿuÇf$È”–,ÒÝÛÁîˆÞFËÊÃVÝ{³V ´i©�û@":éΣM\ÉkE(ÐTÜ­¹ªq.n ˆZ2� dÍ) Æ­•d»‰H¶Ú¯6“´†”&kYh«j#ÌZ¥0¶~CÔ.tQè ò:¢`—±rx ÔvÇI±¬— ß•5O†¦Ä¢,ó®šüZ¡Y¡ß:3 %k”Nú6×u¦êðÕ”4cž.ðu�—²¾knC0›<³«gGçßþË“Ô:PžVñ˜oÏü˜÷1•8èÔò'ޱtÖM¼xô¢•¯7^bÍ£`ÕmO¤õÞņˀþk£q "Ø‚B]ƒJÌÐÈå  ©âÏRzIr|­H/È,êJ U(ܱV`èêÌsˆÊ¢ðh�ò¼”é 59kÊòF௸�"·F¬ ™ªNœÅA]ø‡ª5[!x`p)ÆS¥*œJ„iFádÆØ{ãñèÓ¹xöL<~ŒHåž¼ü´ * Ì�¥6XÊpTxœí�®›ê]+0f"[ôVÍË$àC–¹ŽÄH¤ý8ó¼`[m¤•7PjÅ Í˨K\µÚK�‚F2½mí5’Œk j  A(Çl…—Ùyéø0圌(W˜T$cxœÍZmoÜ6 þî_¡¹_×¶¢À0\ ´i€Èšli>²­;k³%C’/½þú‘’ürWŽn™ =ë…¢RIIYþôîêäöþú”T¦©³dÙ-áÓ0CIQQ¥™9N;³zù[º€vÃMͲ“šjÍW¼ †KqÉ:EëKf¤ú{¹p$ÉráYå²Ü"ã_2m€^^èßÉ,  M–”Ú°ãt†2Í– м�‡ýɲ%Ò§Õk²Òðï%T °(dÉv°-óyv9Lk--2Έ# ÂQ¸‰†Ñ‰#+ù¦GÐè—¯S7ò¶bÄ!˜�ÒR¸á„7mÍ&Œ& £�™“aj¦ˆÌÿb…ù™ÍîCxì0ÔœÕ:šË®¹Ê5SnCè4Ûk!’]Í ÌzJ óù ÿЬL³¾B‰}èlJëïÀÈ ˜~TÅæÔ:Vç•ÚNèb¡°]BøÕi¶[âÅŽ¸dCÇ-Ó|PC4`“ZÐ$|ÿ€ÔfñÐÚ>¯á±<«]ëþ]hÄh ö%ˆþ†¯šfö3ïO!­%U%)Ù†Çv]š]tóFcB:Çäã†ÁeË!ˆ¶—}ÕÕõ–Œù‘Ë_âA~3äH€yR �~ʧÐçîä^ñ°_Ù|Í*z„–fÁæå±â¸<ЉÑx|Òö2N0ŸI-„½ï' TD½V´ä�ÂÁfÔ|ã!?ƒ<Ý6Ÿó†Cž¹[¡¿ ŸyÓ5D ±Ð(Ê­<¼Ó•×úš*ðÅ0;l„½† ãÞ=58Î,ã�h±FŠ Sk& v&V2ÍöBào{m#-D0£Æï–XèßqÝÂFsÈ'•jß=±•1YÕ4bÒw#kÐIû zxÛ3æ¥=èÈÑèŽñueúxäk�:ÀúàHpO28N†Td!Þrª‡˜ê*s"ä@ñ\ð÷;í=×��lÓl¯!$Çà?ñað‡�r­‰³ŒylàRái>á” :ð^%§9¯¹Ùú·ym< wiv89êö&ÑECŒ^K}õô¨l„€N—/Ú›¢qÜ9Cž~n!ïgåþY2Ü>{êAqFÕ¶ÅSæÙaSOº·\œ–k”§/…$€>°ók)âA}¿Ò!ç¸j ä[_,€»Ýþ7I9>cW/ZµÃƒÈ “˜i^çãoðN ÚnÃõî¥nÄ”¼3ï.4&%�ìÔÉÂ\gà ²w5­‘ÙŽe-úKÕC¯jÝÕ|O{ájOrI;åüík‘Ç{f/×+þÝWóî‚Ý_ÐCB`|I“½w–}å[wtôß,é`y?’ŽÏ3�Ü óÕv|Üi%Ç#ø‘Cx`z|ƒxô%i*"Y)Ù€FgàyõÅRØ|—_÷™ÖfÕ‚‡„ÝÇ/׎Qó ‰+gCÕš‹4sßÙ†õ²„èñö 'o„±ÐcL3ü=9’=ܵÔ!ñ÷ÜHöLpÛ}páMfRy*»!ø¼‹O«~ %îý5¦t§Ö¤†âÓØÕ3�ëÜšÜP|»û±r n(ä})®QŽ?iÍK¶ýýÙe8äÆu¿ø'#Œ/Ìæ'ŽæùIM7ÌÆï4ŠÁ)èüþè>ä™ ÿ§> ÷·Dÿ]–TaŸá( ¦ÏK¹ê�d8Ö" µ+àêŠ ïˆuO.K¡Öð›§ÒÕ£™®åZ*€ÓÖŽ\�n¯�ðlÁ²ÀbnÉ„cgáÜiÌØk‹„ÕÖîÌaó¯ó¨Ydl·Š�¾¯÷éÉãž f4(*¥Õ9�ð[ ®Œ;ê¢,Ú”JÂ�à/M†V­úlEñÐÑe8Ak´kf×)èc£µß3ë‹Ä1øÿ®-"®®aêTòuÍK¿>ìâ@Íѧº‹�wú>EÃNo/k·#Y óÐtä=STø±ÛÈœTÔžq>^dˆ¹Åò'rŒ@lM5É’™ÍAòˆIðã­sŒ/x1{ä£^ ^=Ø õÈæ*;E Úóâ=…{D�h/”ÕYÇ ¡Z‰j”üµÁêLÂýÂËÈ&øÑdËÉ*ÛýVÿÚ=éyÒhkŒÍtÝ•¼5(kÅ–{çJ;¤}Ý"ãk� ¦·‚C– Ýãҭι‚cƒYàj^v¥ËRï‚\Ó\zô5˜­(%„9ǵL‡Ynf/\iGÞx‡È­I®}Ô¸À½Obá÷r]ñ‰ÜKsçe\òXv"¤X!nä$·_uPÓOVŒÀ¨~ð°¢ÝnS!ÃðŠ<3òä:«ï’¦´ÍXš™¤zàôïàg×§%çWéèêµÛ …ƒGÆÊŠ[AéƒÌÛ!8ÆNÈõ¼JòÉÓuqèg!¶NäÌç:d]·”Ö3iAêãð¥œ8I@‡ýVôr¸‘…¸‡}ÛL佷ͼŒ‡ðŽqyÜ_ÄgÎgpVZÜ—³þËXw3Ö¾õ{@ÐTÓÜ.˜—ÑR¸C†£ÄЪ)�>ÀB ˜ ÐQâ²Óã•QÉ„MÈpu7ÊüNÁ®*zøÓ‰jÑù•çÀ.¢p æ2}?g.}Ú+I«4vÛØ¸?F8äø)mXhx=” ©ò²)Dß:¾4%Fqüȉ�Ûž—;¾7ä£ü) çFÏ%ïOÃ7KªO�r˜}‘sù"Òž”£KVÌËÃÀC&n‡Å*wÚMçF¨²I¼<ðòQ£žÆeŒ�„ô­g½Î�Æ™ ôéûf»uþæ(@Ïe—Áß,¥‚€ïë6ÇãŸðm´,â@x^‹²<�¾<4ze+~;]ŽÂØŽ>í<ÃꂾNæŒÞ‘ä�¤¶Ì1‘‹G×ÎË϶˜ÄÙÄØN38>úÂèu„Åcvv9À­ì.n + ·8è$ÊEÅh©#ÚN�Lù¤G!V¼)mà'Ï“-KÖ·Ç¢ ,\ò¥,%`Yßx«ȧþðN¡;Eª\rµ?_à xµ, œ³ WÑ¥bæ×†cõLÔµ®}H—ÍÆ¹o@”Ñ…û œž©žC²dfß¡yÈžaå2Ùëš t!L§:bLò�@ñÖ�-;{�2]îi$ì•…ÍÀ|®u ^eÅŠ¦&>ù5wÒë5È=~#+i'Ò©»h^Jõaiõ–ßʪ©ç!feX|Ïüt‡^Õ`�Ï|c|Å1MB‡’Ŭ¤y‹@�W x­©®KoÕ̱^øHŠlÄádÛ¸äº[7¹mê¶ÖïœNëJüÌpóÙÅ}—‡üØhÞ‡:9(߆«»œ†?R€e^+`^Â.uÖ{]‚Ž,΂>�wþàµÁ]Â4•÷W¿Å> ‹ß[±=´ð{i¶`_«•î¿Añq 4ôP¸i�wOÐ䟾v£‹`óZ¿èÃ~NÀ,_›NÓÇxµ"Ë܆®àWAaÂe²sJ‰d§¡¾“܈¾í•S +¥F3üƒá?Jl#Ùû•>æ5.mßæ¥V@äµ´&ô�ÄL‚«¶/9‰‡|%„¶Ì7ºøŸ<•¯j°Ðú©ñ1>Øf_•ä;E9‡ÅÀ¹a(ÖÑŸÞˆ˜·]QÛF Ž“ó˜:.~êË–ºÖ¶·Ìì+�^LÐh%be–{‡ØCFŽY,ðºS µ‡œ6­]N„‡G¤¶ žÃF´Ô«• c§4:˜FMÎO- Äð³GExL4“÷ÎÌ;x¨Áyþ™¯EĿީ§ìí‹o^|G-œ/× ¦v@±ÙàVÞÍ[IQ@,`ŠàÑQb‡@»ÄNŒ³yžcOjµzÝ`B>œS-‹(‹„1°R²p£”¼�´x^às`Lz°ªÅ¯ üÚG�àÙUjÉÔgµÓ`#g2š‡„¾Äö°Ùë=]išZôéËକŸ>ð¡m 3a. è_ôÉÆ-Ip5¤d.6rÕ¡ß.tt´uÇ€ƒ"J0‰9@̃¶cçij>cI¢è'°�‹ŒNUÖwF“"JW£¢‰ênPb4âœ,PBXs*îµ�¬DpÞTMÉÉ—³0–æ³p¡%/ËÄ!äfÀÚ€î"Ô‹|� ;%ïûg……²�7X®ÖmUÌUC"u)kzæ|Äl¡DväE‚2e%.K 6Ó`°J±€°—T"(£"‹T,ÆÍÕ˱�êF�ÏàZYa,©§,X0Tµ¤âÃk€¶e�Ë ƒ–S`|zÛ×¹äl§0À¹6h$¶¨ÿbL»Û,Z_š”šu]=YK¼{(˜Ä×™¨\:ÞÑœŠ¥x¨TüpÄ$Z`Uò»½–PÈÝTX[Ð� x&.æ×ps:pûQåÌêݙݩÅPo£g­†z˜#u'Oƒf¢/L>y1mãvÚpOð]Ru,¿`B™¼^ Ú‚}ï2OÌUû-¡ûêæ:®~óÝ~xß_BÙúó%· j�ÿ6s¢'ñpO)Ew~ö"Q„| Ñ”ñ.5EõhƒF+Ó{£B´ŠMô¾"ç;Ú£ß;tÖ³�ÇÃa´ðOù¯@It:4Öó r’ó< +QÅ#r»9{.¯.Då|<\è»@·Ôc… ç4`6ÍHÞâËê-I4v ?„ømvv€sÃRòÿ+t›o'±ÂÂyt¸“·û½)’ðå¶™ÉÖ ‡'’nDÏI¾ð! c.“©ÑcðNfš¼ƒ‚tÛ¤Å}�´qÑÉHµ¹ìà¢×¢Hîn °ÂÓèZ§.°42f‡éŸgà1»þ‡ñÅØuô8ë1¿$Ûù©1œ‡mïÕ÷+õT—†æÎÝåK Ç:m$ê–´b´JšHNë¼Ñ=´jÑíl›=~ fšÊ…ºMùÒ—!�ˆ:]Ì–µéŠî±Ã~�¦ßã*/!5+tî²) ^O]ÌáÙ�ÐÕ²¡/²’6Ϭ `¤“vêÝ¡B1#Ú�}k�å#ç8;(¬À¶,…oÔÄòc¨JW[Ycgã ^Ý(‰R³î‘ÛÝòÊ’Ð5ôa÷f]âØ�ÀrqÌå|kjÆ�©× ¥ˆ )o‚’vô¥^ã´, #®*s¯xïãDÉü8¯T~“Èwƒö¬©R8rY§ËUQ’0Е]‘[ .c}Ï(Ñ�Sræÿ!j�¹cw#PòµÆþ}×n¡DÒmk_B¶¦º"–�CKiZg.´E–ò 1 Bñ~¡C>^/ÔË_üýP"Á3w÷0'2áÿb“tÛì0ƒÁ-/�u“;J(0Mjܾµbb„鎧I²YÅÆ›,\>öa†§>nWã‡lЕ$Ìr›ŒT� >B¿ÊsèxKPþÃàu””Ùî‹7ŸÞÕqѳ”ôüú›ÒÞËia>»¯М”â4@çªÔ® ¸ÕpÔ…»ö´Å¯>¹ÿ£6+Åí‚>• ÄÚçe•È<@sÀ â&p�ó‡ {EÊKÖÒŠ_yÃKêÚp•9ÎýstäWúH'¸jÈ9¨_‰!î×’ÔRüi*Z‹Ÿ}ëßÖ²âT?² ñAsƒpÛͳ³'ßüæÌÑæ/Ø,‚ݤ“~÷m˜„pØÞÚ©ÖýÏ}mŽv(º;¸�S øœXC‡eE:ÝíÕ7¥�Ù£ öà1û-†�ìÁùö6™§ìöáC2³äؤº¹Çî°„=¸õG8?uK“ÜqÝï½°¡Ù”"$�˜™hfñ a½ÆÆ 7 ”òc§ôC[•¦´m0N0þÍ´Î@ŽÓ0‚©šÓö»u®Ö¥[ýHšëáD€!³üßîŠÙö£;]ÆóÇDÎ[öUiŸ²oìÿûþˆƒkû4CòNFyl> ‰˜ß?¾Ï†_ÆþYëfžÌþç�O<ê=áÉPFuAZ8zKÑkºð_æ¼p_ý7‚+>ÝÃÝ2nØŒ$cA:Èlª´ñV™¬³ü*ªžW±Ñ+]�)‘UäÑt#Ë3´t+- Ø Å‹Ê�ÊDv XP€¤+0 qÈ]Ú0‰(sPFZ'2Zlp"žG?•ò-0³§» <´+‰ÜËÔ'•9�˜ÈT¶õ móZfÈœOü‡µaŠ@µÁÅëàã–“Ñ…_Ÿ íÙȰt….R€¨Ô7H�Û›.ëéO€Eq3#¡H%i^¥E(œ°\£ŠìR�q¸£:ñœ…£aÑò qÂîÍ×�÷HíJ'·‡êU¨j‰º�OAÈ l±�!;ïýjßÇL#Ó‡êHå>"ÃÖ:î•ðv9¢œÕTÁ§Oć|Ã~–¿ŽH2®ˆákâ|s7†›qñ¿XäÖ†í "â 6ÖkÒ_·4ù\t×>¨ëi~K>´%oQ�ßÕ²2˜§ìÙ §>ÎÔ"¯�®¦JÙv€(0„¨;*9Ü7ÔLGJëPçM^o8`fO~­ÇŽM•\¯‹œ#3'Ì*ëÇëdœÐ¨¼mÖk]ãv^1½BgºqAoaKêžåÕu÷A0ѹ)å§Áøq(P=��eÉíÍ´É—Õ`¸¹)!2ϹýI%Ø’d0¦ƒàTùhÝÆáð.×À²³ï·£OpË"74’—€cúq¥¼Ã³5Ž¢®ñBµŸ \¤ÀÝ®ËbAA*zaQ*YÑæÊ-[‡*�j®ä�ªß¢û òZ›ÜBÈÂΘè'¬¿+h@SNT•" .pµ`VºFØòm¨žl ‹ÑÈùÙD*žƒV¹ÌèP“ë“ïk’Á��¯‘aµV¡ÉâÃU^¹J©@Ô>í,ÀÐÈz(ôù²Ôy6TžZÍ`4ÜjB/ì¨w@ÀëQÈ�ß(¡æß—ÑÞºhç¶ Ë±í²–YŽñ)ì0ZKÊx¯V’`ôÐ)‰I¼N(xâøn±ÍGŠ%!îb5—éGà|­›%;ræüÌeÒ1'ÑáƒF»JÓ!ë‘àÿ5RüÒÈw~ATn±‰²÷Cc!í!=jÅð@)¸ƒ ¦iA&3–¯Ä zÓàJ%‹ævEÙ¾yùÙ¬r–Ê ³¢8WÏå¼#�¸Ô G˜Í×^- { Èø¬ÉsÞX½6!õÁ´dÍj¡+0×›Ö9=z« @§MÓBúh–Ù¡N¶jªy†Øë„!˜"ÙdDžfBå$6ÞÊÍË6{iU Æ|èrÆ&Áv•äÞÈÌ ÔzaGÓVxYE„�²hL4xb´4L=.U©awÿüÝ_ßRz÷êù»«çæìPˆï-ç™ î^G¸fnÏ4{N0w þ„%kôõ`!¼Û°}®äò¨u ŸzƒèyœcŒBÕ¦•“@†&��ET˜´€ `^»²c æü!Ê(xnÂZÈ$צñ4 ÉÏÅun0ƒtj](öÍ0V Ùê[ÔfzS±¦Qœ·™¨›ŠˆŠŒÛ÷Ï+äv›¸"Væ)� Vïl¤�sµÀ È1hbÏOþãdjÌñW‡Ò;¿ {D»Â(ˆµ×Ù?·7IØß”m·'ø›lœ~Šû Ú× ’±��ÒSQ¸æ½꼘æŽF°�{!³ ÷Åj=4JLÒ¹:þ·V3œMH0 ­™{«õðeèVìc‘»ö÷’‡cßèôTö�e(†|‹Ø–tKbd/ÆXå8€á .D4F RbŒØ4Á®WK°bTnAØÌÔ§dBýÇßW Ï QŸÙäX›!?¸8œéÄóäA˜>nh¦&NýŒ B¬¼lʨv µ¼CúÏÏÎÎÀÂþ½ÊÔ˜oãRWª› ØÏ&pQE†Ǩ‡tî5Zr3þ’›5'°yú^’çŒáEL¨B]d›€�5¶‡§ÔÀY~íû3"Íjyù\(ÌÓOÄÅ©û)â"®Z-ž½’(©êg/žàÉ�^€LBÀqté>ðaêsð“ÎO¸âY 7úŽç"¨‚÷­ØÐ=í*WãûéÅ=·Ïÿ»�‰[ˆ þåîˆÜ¥o†�©Wn®uŽZèÐ+јñre2Ú‹Z—“zm“{èÝ-q$O$7tÿ_ËXø"mT5ôic·�Šy‡Ž¡Êý¢­è† g¤ëJ«v'¡óͽèz¨‡Á¾]<#7ÍÝBBk;‰ï³Þ¡‹‘F|·Õ5â÷I䨙‰n¯;æL ¹¶¼_íìñSonØRNÚný ä«àãÍðPiëK0‹¬ßÛóƺÕó@EëšAíô—ëÓwŽpøj·/1`‰)ø¦d±-7 £€S¾âe:t1:<ƒŒú—¦­Jx1 Ké#×;¤µ"úøv}z .ù;ÿCÜ|ªÙ¹´.å–k�œU¾\aÀåz’¥Æ&_Qz }d²yG»=�¶U_뵪!ªÍ å¨`e¿ùà+N³N‘§ŽšXl’)<@Î7>�š+ÊWi¿UÖãp°Æ®ï)¹“? owR…V¢8Ñ�LéØ!æ K÷k[¹EîÖ�eS»ÌGžð�C/]=á†iw&Žig;(›Í(!¦v7Fž•R™áFC*+žða4µm9ã–\©ô—)þʸ÷ÍU þWw¬ïšEÌšŸìŠ(TÙyHs§Q” �Ÿ+ÙjæoЇâ)÷uaƒ�/ ˆR 8D!ýŽÇv‡>wŠhbÌ·a}ŸqÌ€^ˆ6ZŠïË$cÁL/–™p¾rÖW>W8sK�?i %~þÿ—a-e½Ì«‰&•ßј–±!õ·çowäþ0Dî|Öþðä·Dp›¥† qÛh‹-Rõd[Þ"éàÞb£3ª|ªÊàòÁÒ¬UëZoèZhˆ@£èm™_S ëÜuãOõŽA¬e\£Æ�Óc0Ôñü¶Ïèãö¨Vç(Æ&£‹ÅãPÞ´ìtÞ‚OÆQœ–ÄZšåK\ à‚Å@Ältl˜"’)Q‘í¬ªþô ž;œ¾º<òÎ�í9(¥�fBò8ëæŠÉH²è®`µ™b¯}ø{NñÛ+¦>|NA�äW‡ô$££ÆÛnðÓOUÔkóÇßm¸£@€ïÀEÈní‡ÓÐ^L�—WÉ“~'kk4ø{ ¡«î·÷—Ókøaü=‰ºˆúË-¥á{(ßQÌUWÌ_ŽTÖ†BÎÆ°/âÎOñY[•…T§=Ûó£À~ó*0ÝáÜRü¯¦—‘Âøû¬%E@ð ßÞu?o�䎺xÕÕÅ«‘¬z¨‹\z@]Ä«±Iô&:E¿7‰íåÁ¿}�ž^–>\£)ífNTh?üŽúüâý«¬s>·…ûÐ*½géžV¿îkõû�/õDµ‚yÆ/÷äõÅëž6ßî nq�õÞó·’‡¢ÂZkj^»Ù>ó7¢üQâÑë:þ:ÑùÙ1¤½xŽ>^%çʣˣÇ+eÓè9Àº&á݇@u–Q�mi×\´/2D[–6ÆêÒ½Ðg„ˆu­SeÌÈ&}Áw>žðÎÿÝ;G7@ÿë;ØØmw? þ[¾êɇ¤Ã5×Õ,€¹ ½L.IDÌí€h9þèöø£ÇÚÕXø…ñ#ªhÀ þî’^«ëÞ·w¾ã¦kL纺Ùzßw@é_eõ èØ9¦aX¬Á׸pÅGºÒ%Ò­Õ!s½�ÌuDfâ/غSé5^½Þ/ûÃÙìü�tÇl"ùWJ^«©ô»½ô3Uã�!z]ʵ:Öüߨ}»ìWëpàýà壎 Éà9²ÂUÖtå>àvÿÛbá>M¤þÅû7²¶d+&1À×O¯×~^0?áÍy{X-ÝáFz½…ô57ÒÙH?n&\u²E‰¾àYO>ËõéÉ�Ð}Ç\tôÃQwãvc¶Ge¯ÐŽ!ÚÓǵÿü–Qš.×ròE*7úžJÀɃŸRtðï9Á“v,wÇxˆáo ‡RB„¬óHÿr¶âɤW”w�žyü ´gÿF¹÷Žƒ…÷޹�:ð!%kÛ$…ãïA%æ—´­G6y¡ªmGF.Å[|Ëã}½s às.§¹&c¸=Œ5ˆ)šÀï\€ë‡Ô")ó¼‚@UàJÜÆõèÅìáå;í{Éý�Ž.^3мûL£ñ¥SŸ ÿ�üYÒ¶æòÊ• èu²£I>H!H;ÜX|{Õß�j�ÈÌí¥+~ŒÎ1ÂÁ‡h0FW 0ù¾Ó8‹ ˆ¼:Ü4|ÿó¬X¥£»Û_1ûŒ–¤«É§îŸ¬9åçÿFM¶Y é Öõéˆ \�xœÕXm�Û6 þî_ÁeØÚI¼P¬èœEvûÔ{V´+†a�m:Ö*Kž$ç–?J²Ó8¾%ç» Û€;ئȇERd’/~¸Y½ûx{ ¥­Ä2Jº²œZYÉ´A»˜4¶˜½šÄD·Ü \®3†Ѐ_-˜›®qýVh¿>ʬ%Ü4v¦Š™ 6‚*7¨· RƒzãmšGA ç›n'•™½œŒK–•ûÜÀMà¢=aél‰àUØ’Y(‘Þ¸ÕØ)åV#ðû3À4:º³É°ª8x#{èR,ã1îJž•42G JZ%:Ô�'�A•2Öí�”USÈT…R–}‚Š“¡r šÑ’Žœ‰rÏü)0é¬5÷8´ºÛ‡ ÇyÍ®[/wn#%Æj%×ËŸ� £(®â–P”‹SyàRúË£,“ãÁäRg =…ªªYfûRŽõ·*$j«ôïØæu—–'YBÞz‡ÄHã²ÿŠé5—ã’?ÈÜ›û#SÐ}5ÈÿNÁcòl§ *�òˆ¼;GLóˆ2<¦ú¢î¬&]æk|�=ŽñIQ4�‰Á òLˆƒ ðO‹?$Œ©/ÑwÈ×¥¥’]!“m±lÃÓüC±« #JÁÎÞ•ýqg.Š'vog`ÿÑ–ã¤æ¯éò¼þ¾mEœ{½É†‰LJ¢ Gm�dY*p¦ÑÔJ¾A·ìi=l‰¼¦°_©ÒÔ'ì>Måe]KÛ¯5§Øß’ˬ¦ÿîxnËÅäâÕWDóÍÀnÛÔ—=Çr@ûî›Nîýgï¦Ø†·ÐV{uy_�wá3w?6òÙÎ¥6ï±­=ZÐÄ[o£ÖJwð<Ç‚5¾˜FÐç˨eêØ¦ÝZÅå½ô”Kzn8“îwˆø'�JË™H–Ô}á€*Ôšï´€Ò;¨?–S¯Æ³nq¶ì|H¡ãÏn—àÿ»{òÑå©hFvG$p¶‹‰ÒÝ ª…W0,Sm…ð«÷Ö‡7u-¶ÔÆGæ«‚UmgäPOÜC=h?ˆ0!(×…%Ô4ø²ÃLNÁÖÕUÞµ~¶šqÙ#}’>ì“¢N�=<¤|øLéœá‡Žª¡‘E£m´$wȦBÍ37sDÝÝL#‚@¹¦*\dyåF¦ÃS) výÀÒyáÌÓÉ¿“!qûD~îø k:Øy‰^[“@Yxœ½YMoÛ8½ëWÌúÒÃÚql�Eàh³ÙKÛ$è›ôHQãˆ[ŠHÚ®ÿ}‡¤ä�XNä…, ˆõ1ó8~|œáГßþº»~ø~¹+ä4™ÔÈ2ú(Ð1à93ÝÕ`îf£?czî„“8½–ÌZ1œ9¡Õ=3ô˜.0»¹¾»†ÒèéÚÉ8Ú'“q…›êlåG9ŸZGÎÖ n/¡=9&Šx5xÕíì“P̬¾¦“1óC^¼vmOc\�y Ü[_ ò0³ô7¢[G`\gopYG¼‚I:]æHpžŒK?ÀsæÏA§Í"€Á’Yp:>Oƒ3HdF¡S0Á¢­`.Lo~™Œ±˜Ýñ3=ƒÑùÞ“u¿ŸŸ<äÂn‚¢kC³1ÒJ®Î’M&kT;ú0ðSFÏZ“}­3¡ž¿†Z3¾íÔ5í»ØûÜ?ä<Ø@†V<+ÀÕ3-³03áf ±¾Oúç]Ò+ޓӒ¥ôE[{¤¨½Ëit‘›é•ô]T*¶\´^ž,ÒÌ ’`)\Þ Óä:õooɰ=…—Î5º…ÜLaæ3¤â.Ž’¥(mO :|¥¶ Ùºî°Ç<´p­ó:*„å;ˆ½Pso„6­¹ Ö]“S�6³Sú—Þ,ÐZŸ¦²‡3‚¥{‘éí¼¸Ûtíú¯kòöàð7/R4žÁ­6ÇÂÜbÖ {O­ùzêš¡§Ã5ºZŒ@åƒiù-±…%•ŒMïñ¿7Å¡»ôP9–®­¦fftA›pç<|‘wîÜÅjpB/©´ o‚ß%Ô‹`�o$5QÒúY´„E#јÂQÌáE€´Ã€å6ýYÿÀÒ….`oý厚êöÝ~ç}~c‡ròÖÖ½û‰>ŠžÇÖôP<箟Ò6}ô�LQô_øuÍÓü>kiq™9ws³.KºŒ9µY^MíurR tÝšÌʾk×°Í’ ùåYN¿šU?G)«�VLY~U…¨‹’q·©¯n(Â0dHº 8g8‚òE g ­4”’qÚ”)J±[m‡Ÿ¨pJE ÔRÈ6‚½ôI23´Åv›’£&öóßuëi Ö]OjúÖ#Ìh?çõ}ûv«¾ï¼ÓÚ7óù¢ÜFEêô?ä. ̲­…\ð<Š*œ„“º韞»³¤^? u“=ÑÅÖ'8â¬p›IMi…¢7Øï^i“#—žÎØimœ¸ÄÿãY)ÿ);Ó¦hMá®[×D¾Do¦ÓÕÀÊRŠ�‹­4´Í éÙ(h2'V«<ž€ é‘3cˆ^wéÓ"1^ÃiEY•0—^ì¼"ú©Šªa%zaë¾]¼¿øƒA¦ ÿÍyW¿i�ã/h¿Î6c`Œ'Z˜RlxœÕYY�Û6~ׯ˜*@óP{�£ŠT+ ÝnÑ"{!Yäx (‰¶ØH¤JÒvüï;CR²|(›´ÅFÖk‰Çh¾9> ÇÉw¿^ŸÝ¾¿9‡ÒÖU%íg~ÕÜ2ÈK¦ ·§ñÒΧ?Å3·ÂV<=«˜1b.rf…’7Lã0^ðâüìú,™ùEQ2 Â2UlHôãÔXÜa¬ÈÍs¸C®Ž’Õü4þìÚ8MfŒä?¹K1\% ä´ê4.ŸÁÜàßo- ÉUÁw4L²»føh·-™5$:…�keÌtÅ*Q0Ë ¨—Š#9PãÒ ²¥¨,̵ª!’é Tœiɵ9‰¼œB¬Z%k3}{Ñç,/a®ªJüg@--4¨¨9Ø’>ŽM`.¬&�k­4äJkž[!´¼Á¤®‰‚*Jº�š;ÁM~Ìq£N¶°Pr|$QÀWV™ázål�0^Y4ÂÍ6À‚t[2 ’v€akÕâC0�&57ÞÞöÎrh¸`SÒŽ…©]?xã» ¯6 ‰ÂR4@Îs•·RbÅQ�Ü��ü’‡/C >ìD„Ñßqqvã­Ä‡œ­xoвaêìMílåm‰ÆÞœ5±áÃBÈ`ïü#[po6Ü$•¥ñƒƒUGMÑ ÉK2z«8 3­y;ìTÂ%³Z|ÚÃþ‹ Ä Ô®…HfíϽïvØRsÒe¡hÒ¼UÐÃ)é¾.¹ÆU (èHØZØ’Bv] Œcæ"ù{mþZªŸ  TR4g’`gÍœ‡‰»«ªgÓÄX­ä"}�J°Ê(ÌÓYŠ�2P½ùi|‰Ù"$ÿpA¹…ªžÁ=hƒ#î… CLý�‡±æ÷ŽÇGãò@žªÑµvPìÐ4Jšò„ç,2C²䧤åÒšÀsåÓtÒ�}h´j8qã4ŠÓ›n€„òžõ³Ì±ºÕib‹@—ÿì >»ú$\¼�<¶åYz4~ÞR’pbÄÁCCÎ�ïr+ñ$¼îx•Ù2N¯>]$3^§P»¤ˆˆ"¦�'ðÈEì�1�\ ý7”–_jS%«Í‰Wf†¹«ô³7ÞËåCûÜbZæn f’ ˜Û½?ÈLÿÄ@JNˆd÷ ór×0ÁÑ( ±åªx‡·Ž¡Â9GR!<Ó�£=ÓÜAquÄáÑ·ó8Í^adѸë! •-2·�…+–ñÊŒ+€©Ì¢ÿÃk\5S “-”&ŽÄ¯! M’¶ËD%¬«=+9\c‚sF%É›¶"q5:éppêG�LI>Û–|D/{•NWcÑ3»2ŠJ„QQË�æ…È­Ò!ëvï‡L@gÓVáM»e\^F',”ÆÙªÃD¬rlx8;«e-�;}Ð{%ßn¯‹õŸÄ2‹^aFåãWÜ4JN.�ÓþÝçüÛºW‡õ°bZPE4&/_-ëëm5„þݸ¬3<¤!Ä^1e`ix1&xïâôÝ0ã†tÇ`窹;%¯éÒ;£z¿c…{´¶uÉÖ{‰ÿ?м�Ó·Chvjt.¥;ºbž[N¼¸7p Ö ÀCç2·KÝqƒj|Þw×±ò2ú–¨o5Ãr§ábÈyîÍ>„ʪûUÚunð|B_wÓ™ÓwL‘ÖÝcmÑ^ÁM±U³]ºOÙŸ<·¾Ö2lcBKE«µñE¦o†¶ÝI4øx\;/ÜÜÜÕx<­§‡uX«5Áö…F¯ˆ¾¦&æ”4s¥ë8ݽlÛÀš¦.µp|¯±ÐpΜªùÔy×cî�–QhµRÓ `ŸSïmÒŠS2wý¸5… Þ ��½ðüïÃG7Á«'�žüÈ P(¿o¹YÛúòÎÒg©Š¶­téïþ£žR_öùH nœöïŽyãºgfÿ¦ÃÛ~ßû_Õ×êÉôBÈ ¾¿ù"í;©P»Mã@CɰÐåW"!f˜¸ìðÕýÌ™l¹Â5÷Ê÷¤ºË¯E�ª{Wy¾lC ¯ŽÖ9Hô“NçNM«z­Äã1 ¿øÍü�ŠÙ¼ù ú …Ëú‹N_— ÌxœíYmoÛ6þ®_qó°6ìh-P¬hA–îK“h�´†�’(‹«$j$åÂÿ~w$%[–cËi²è€$’¨{#yÏÝCeúïïÏ®?]�CfŠ|L› g ^ nÄSš›“QmÒÉ«QˆãF˜œÏÎr¦µHEÌŒ�åS8Œ7<9/5/¢œÃ7™Lô4t Á4ô†#™,ÉÍó™6¨­�ˆõk8À jS%+øÉh¯ÞñçTæÉ•≈Íh6 9±Eôúõ*ˆIíd”½„T£µX&ã5LLÍrñ|Ò-œÀ4šu=D38²r2úËI=›†ÖÁ4¬zÎñg‚�æ€ ~ߌbìâÒ±T\»¡?î٬ٿ%pg #ÍÕÂF_„ÉÀdÈ$Þ0Ç;a@Öæ8p&±h¦WèÉË‘³Ú 24¢{Nž(ýw-ßhp²�*YXŸ¼É™Òs°=ˆ1°Ôpn%?��kÅJ�JUø‰¢H™ô¶1°NlÜä!óŒkã¢88-;+QJçÞzvn5ÔeÂUÌÎ ¡j¶n ¾w1]²KH3E §ã|®83è=€J ©ð¯ŒX$ra–Í"ÓŠj£d9Ÿ}àX®%¦f臄U¦x:VT-~Ü+6ÚŸ—„È1äüÏÂÕ‚�A8$¿“Zg%ìJ…]¸Óóð¢ã<Zqœë(7ïz¥Æ™~ô:³×ûD@.ª7¾þТv Ò‚å5¿«Ð(43A ‘`º'.Ö6Èñ퀚ÒD'´iùAÀœÈtb‘ê°6¶ã„Ú t Ò:€/AO�DK�Õ[òþ@îsžºBð¾ÄÕ¾‹j_H¬iTÍ9S%Wc´¡°pi(μœƒ].ŒûéRÛ¢"ÊÀs&‹ŠÅ¦»kmÂåvK\´«q‰û1¹!£ÀÔ¼.xiôëþš†&&¸>.„XpZ_;Ö'†5FT8÷I…U¯}Ô…Õ%‚Ð5Ž•¬`j‰¹gþfØL“�Œ^üüŽÑ^¶¹ƒL#ëÈ�ÈÆØ/¯½›UŠ9¡Ð¸;GR¬»¤ëÎ.ÓÓ·Ô€ž¶Yi’Ž óÙ³>¯}ÿÃt“Pk>†JÊœ'¸ëô*á)«s,ݘ,Ï}*ÇÎ…¶+$ÖÛº<8¨S›÷g °8æ•ÑÝØ1ôÆÒS�I®”TMTçr�žˆ×e ®Ø¼U‡#Ú3‹¬Q~¾åÔ×�LkÎñÅÒA÷(7ó%¦½Hj–·¶É" –y]¬qœ¦�X‚B†Æ U»,uQçèmÁ»f¼¬%™ÔÈYrN�ƒ)/ÚòÅL6š!ãÅŒBsU+Jß-æËÅÆÄ:ÛZ”µýí;'çÉ|H8+á¯&Îã¡Á¹~‚À{-Ú™þwÂ.ï�H8:ÙM¨¨µ]½à¬ÜÆ °-ÍE‰Ýöîþ?úä]¤Äô·0 Ëõ±É7G‡m݆8[c× %¶i곟yIÍ¿ d�um¶øïåvãã܉5Ò¯‡b½q¿í÷Å]ÑÃ]ãdÈ17¸<*ðbØyÞt‡dËy·°ó-0kúý&áþÿIŸKë{¤>*=hÞcÒê^ê['ý¼÷mƾ݊ƒÓªÊ—XÊQ"¶™�´¾å|Ò¿µLŒ?FÊ�X’eÌ¡Â<·)ËZ:½¡ptvq5†�F1QŽá“¿ÞúëGúDÃörK—gkýˆ, ¾7q:>6ßæZÐÅî$°úxÖ|kr½ŽÈ'ñT™›ÏJ5²RÅM­°�’_¨�mìUãÏŽÅ¢€þæqyßôÇú„ç烒ߩ<Á;»¹H:œÛwfüe]¼£sæÅÙ2ΛϮ�Áÿv·"€>+/¯”(�hháàü S ì «3ö�µ3ÐN|ž70nñÐâ±ê‚LìW–móím£• ‘ô…†¨ÙŽenãýæñ°«O iN=€ìYÅM†þ¿H¡û§Õ?5WC‰7^’r~xœÅYßoã6 ~÷_Áå¥/Mƒ;à€¡K´½ºöŠ[°¶�²-ÇÚdË�ädþïGÉ–óKh*àÔıÈ�ÒGJ¤˜ù/_¿Ý._Ÿî Ô_$s÷EIŽ_Õ²’HEõÕ¤ÕÅô×É ßk¦9]Ür¢+XF4õ‘øh~W+Z¥œB#ECñ5UóY¯“Ìgv*òÎXú´P”f™º„0LTNæjRÑ«ÉIÕ‹¥$¬&ø4YÌgÄÿ|z[-´÷•ÈŒÎÕ¤ü…ÂÏj„ÌDþR.Ý :˜§‹ð'e!æ³Æ˜YÀ²¤P´œu‹P’•Pž'½\ÎÖnB•š~™ôª×� ÞV5dõ‰”¤ƒ Ó%àTÀ7S7½a dâ•»!«Õv1N×cçt›’áüzMIU[9™VRlÔಘڒ€Ï#b*jÞ] kšá¢‚<ûØV–Iš?Qù;N$ÈÃGÚ1<í1rìqJ‘Jt3§DÖT*`õ»]ŽüšfZÈ8,ßJ¡Ôß„³œhš?àÌyÏýL{Ísý@u)rõÆ}–¥éÚi�[îMÊÍí§Øƒ�›§Çtç Zô QƒðÙuNn„PúáÓÙñ~;Ã-æ^Çñ㓤93aòˆ *È…ûª1¼whÁ³IÌ€ã¸qâê„§~ünÀ8Y ‰|œs™^„(;Âoè˜Ú?jìÉͶ:ï%ùšoH§€V�îâDîwª�?LQ½«ƒá}|̺�•ƒ,¬‰däô#[~K•k+³º‘2徉·ò¤Ø‘üh_‚˜{‰ÁÕ‹��q�æ2ò&+Ï%•ÔFeeò$l0IL› XHQMlÛ?k±¢ìµŒá<�ŠhÉþRçP mG¬Þ%¸(‡+V¯°´YS®Œ b¡%´É´Jú ©Î-Ö`Á(¡ô¿´Ñ};Î×¹.Ž_ƒüûÿ¯>ÿZ à$E?:柃8yŽÁɳ�“�6”­J}ŠêäË5üA´Ý E¡Íi8¾Ä‰šÍWøh�,FI9Üê½s ÒÎ Vžœ˜ü—Ø P\ÓÀä@7†»ŽLx¬qÔ[Ž#ø‰›�Ò²Ít‹‡Òx> ]á*îe׸{蹯b?‹oÏrÎ4ÿì¯fK¬s8†…‡Ñ¬Ã9Z´÷bÐOT˜ynÌ9<œ¼¤?þÖ£5¸kê1ÿYí8ÅÖp] o–Ð(èÚß(ÉDÕ�LGê•ôào·Lp¦‰µÇ÷Á}�ûX½ŒûÓý —�œÆwawZ÷.Êuv î�ÛÆ ¸ÃáÝíõœÙºÙΙôšé8qxÃê;“õ‚8vJ1(Þb3|cÚg6G‡õ â_e­Dxf«¥Q°ƒ~¢ñb­Ò�ﺻ¸ÂP! U×ÓVd*ÛCÂì¢N£z|LÈÀÖjÄéèY`ß=ŒhJRÆ™ùOç'G×_^H±Ð¨U!dDß¾j -xò‰Ò4œa)©EŸSZ=ÅÔ$ePæôå¨/K3,‡:¼Ó×ô7BŽMM„__ÔuFz`óØ™k –ãõ�#fß~³þoÆÿ–ì+ˆx]‘tqxœÍYmoÛ6þî_Á±_c ]Q`Mš C›Æhƒ¶ùPÒÉâ*‰I;È¿ï)ɲM'YPL’ˆ/ÇãóyÇ#ÿöþêüúfyÁJ[WÉ,î? rüÔ`ËJ¡ ØS¾¶Åüa»•¶‚伯ÈBfÂJÕ,…Æf,@~Ñ¨Ó âÈ Îâ¨S˜ªüžÔ¿NŒÅQÆÊÌüÉž GÌbÁQÃ)Tž'q$hžßŸ¥fqË2’<åå[Vü™cÕ¢¢Lå°ƒ6NŸ¢4Enhµ¤>açZ3߈JæÂBΠ“eª`9dÒ f5ή´‡3Ì1ójr¹éqÖfþ–{Íï˜GùXNŽ©ô_È,+U•†[,-èN_å„J°¶×qÂ@d%+¤%è´D੽q“×àþh…²²‘Íꄉ&Ç_sÚ³ƒ èû±v'm锌íÒ+EU¥@À€ú¥ejm³‘a êx8%Þ5ƒ=Þ+¹�ie㥠ÉUëIèˆ×ãdQ´×�>QªÎr©ÃÆ¡Ë!QÌÊŽ2ÓðZjÈef•þ„ç¶¡}?®—‡:z2m/n&Z4áJi”¨äÈ"Ø"óOƒ�žN¶39­Ï`Z<±�lÍ“qíØŠô ¢;Y¶ZŠÉ¶úðÕ(õpž?nxØïÕŽäø¿óä{ã°›:¯˜Û OnBØÜHV‰ª‰¬ö�'ßBÈ®ÆI$ÈUi'èB4v ÃX @±×\ò¡7Á]:2—î í¥”d> {I™ÁŸ |x< Œí•å6õ\ŽÏ��‡Ì´ß"ÓG°3Ù\ä+:ÒûRüeVÔùbt’èr‘mùÑ<Äݸa*ÌŠž!èo0û“f÷¡=ÒL—Z*MY~Âçvà&P©He%Iõ‹0ñ—LiÀ@טBéš'»õ [ö�L´m%1ѶʻæÚÎU1w!†ÔìPŠú;Þ»?ޝâ¼KøåÍñ�«~È\?ˆ]—«ðd\;š8ÈâÞ Ág­ƒž_±žÃì#ž¿-*9íݲö\°ÂÞÉàS4îàSñð)dOÿRè•l:¾ò µ8Ý"ëž–Bøßaظg‚awæcüò¦)`c¤ZÓuÐC�ÿÖêŽÕÒv_NÜCîê!ÒEÝ“äÿ³ðAVét › ›Žpa�dzxœµYÛnÛ8}×Wpó°yqb´@�E×5�mw�¢IlƒfóHI´ÅVU’²ëýú=Ë,_G[H›—ápÎpæÌdòË»�oïïþd…­Êi2‰Ïñ§–³¬àÚû欵³‹ßÎÆ·Ò–bú¶äÆÈ™Ì¸•ª¾ãÃø òB×¢d�V�À 0“±ß‘LÆArªò�óbj,¶+3óš ‘ˆ­É„³šWâÍÙ��—nþKÍÙt2ætòËCú÷6á´—ØÓ°Œ†Þœ¯ØÌàç_-$f*?hŽ×Qû›¤Ó¾ì*9 “qC§L™›`jÆl!˜]*®0‰_‘ËEÔ¤2¯Îü¦+–©²­ê¸Ï@³«F0nÜ€¦Qµ#f©ÈYºbb!ôŠÍT™_&ì¾�f­%>k€u¡êruNãè!fWÎ<Ï68–ŸÀÔNꮑiœlUI“yy+YχS#ôÂÉ>`qó­…%YÝVBËŒUÜjù�-¥-ÔaZ-!/wŸ8PËKºdì½…‘V` аª‡Ó�ià}´¸È! ãY�ÍÛ`%?�Ö�–J€Ë­?>^Aì.`n‚–¦<•¥Äj GÖðg€*bD˜,Df!Ê´UE�ÃòPfÄdíÞ‰Ò°6d'¬§}x«nÖ¿–_…[œ‘IÒ}màµýUÃ1ÔïÆ��™á¤1ž¡bÜ£�‹ç¶NyÉë Ké–¥¨X®jeÙ·V [®ÏUcìkœ5°QZ„Še½Ç%~Ú#>eJ‹{Ík3Sºà›�ï#Ûòw�¥›tâošRúGæ`Bø“™Å€!I+â%Çr=æs±‚Ù�cFð³ÅwâpÂfm�9%È JrnÙAÙ´^«[n\ñK)\9ÇÅŸY £ÂåJ‚4meicì讥à4ñn”)*8O¼[pÂĹÑ:ÖÐRò= DWë{¸ñRpØ\3Y‘x3‚ÌøÎK5wéúYèR€ŽÜ€`(¶4Ê`õ/ ¶b™&CÐ#ìnkëÒSᘄñ ›V/ð”®RÀ> <ÆÐr|o÷böÓ�=éñø°<îâï@jãóÐ(Y$Ï>.OBðÞg}Ç”åf`&§°ãÃ;>ߎûìØsx¶r^ØŽ[³ øIœ[=›Þ~1‹jÚq­ð¬šòe)ÿ…�))€v‹K¼0Ÿ›àèî[Ǥˆg]ðüKk¬£G ?'"ÁŽ(=´ZÜÜx Z¼)ÿ@ÕØÄå¹�(KƵæŽNo3�“Ä‘·ÈTsáew«A…ù¾ý'HÎû�Ù5üû“kÓgë}χ¡W– ¾X”^ðˆ‰ª�NK"H¾€èäzfçßÛ)°Šñ•œkDýmÇGfSúÓ|)v=R|0W1ãmiCà ¤éñ|]?ø h¡Ü5ÀÎaÇñMÜ ÞSZEФµ[åÄsÂMpï®O²O?¯T,2¨mâ‹fŸ*6Þà`T»¢ÚÒ¨º8 VþÂ�rë��SûC},?`¡®6,°_µ¶Gl î‹6gf*ãxB5ãBÀ øÝY‹ŸŠ×«ÐˆÔ¨Qõ&h¡ä¨¡«’ÅãÈ øZôþ7µ¶\X”³�y²E³–ëߊJ¿ „1*>~L‰ObqWÀb‹I‡¤ô­�Ç·þλ<ü ¦9/ÔòP3Øê6³-\›^L¬ÜšîÌuŒÃƪÃ( ¢ÚCÅ‘`‘Ví¼`3­6›1›%?ë©— #s;4·×´a£¬¯7cÁ(´^bàý åÖþ3Š‚n„-T½Š¶øñ{TJqÔòyà=pg”‘»P”qí•¿�)¡lì Æé�òì+>;‡I¼nGüÕ²ö§PÌÐb¨?Úo�� ÍÔínûôù†6)mÍþ[uí�ÿ9ïß¼V¶ë€AfÇ’Î{&€_Õóé;±�¾Œrú6Êåd¦Yl¬D­6|£™Ò¹k>;*Ù¿ÉDÍ5µoðùZËÅÓ¾9ï vš° êZC­\£p�KË8Ñþ”b5<{¿k¾í儹‚P€}Τ E#º=¾S1r‰· FÐÆ%Ål€?qnŽxœÍXQoã6 ~÷¯à¼‡{ijl‡†ƒk`èºa¸öZìŠ}:ȶkµ-CRt¿~%;q.î¥Ý É€¦¶(ŠüHQ$åô»_®Ïoïo.¨rM�Eéð�¢Ä£‘NPQ c¥;‹—n>û)N@wÊÕ2;¯…µj® á”no„/²ü M+ë4 lQšôâr]>±ð2ë°Æ:UØ÷´W ø£TP+yïᎳ4¬ãÇýðÀ¥ÌwWïhnñ7ÃÐAL¡K¹…3Í÷‹Ì¡Þ/L“Ž…gtn´µ³GQ«R8YÒob ¢¥¿„rÕ ó@@²4§QX[ªÇZcgïâ îg Àö"ñ\¤ó¿dá¨Òui ÓÑäòí5�44ÇÒsÔ ¢OHŠ¢¢¹rl†Óä*ˆÓ¹•æÑK±L +�ªe…§D�öµ<;èApUº¤ÎÈRÎzñ[i¥\5Z X"ñ¦¥²ÉôÒ¥ ž'd=’Ø©^CDFº¥e ÞïLÏgV4]-Ùƒ›Ÿõé¶S¬ÓFZj5º{b×(@övªvAØ[5%�DT ÏþeDné� æZÏ…Ù ²ÐM' 7h™kÓÀe·@û½úˆVl¸Ò{XêjQÈ÷TŒC‹U¨°+ñ„�«U+…¡‚k/FØl\¥Wì¿•^‘¥†+xÐnGVÊväž»J¶›]ðò€NÃZV- SËZõwÀÑ W!\üƳ7Û×Ã…õ²i} Ygd»`v”hS‚ ÿò 8éÍ¥hòR¼é½…HXm0 ^Á=7’C€õøø 2¶EðÈàkÞ±ëCÝ…`×&¢Rœžúi$=`Ý.²OR’¨­FZHzRD„,U9?‹¯ËÐöù 9@N9¯~¿oáñê©@ ¦fâg÷ëRwRÆ”‚‰ôº7ñpöõÞJ�ÁúL¬['[gû”[½Í^ êsg4ÂÄ)i=¾8»YX$¾åz$r_hœÉRWö¹ûßêØçƒS?ÿÕÈöîðïNŸü~~bf·ÒáP3/3$€{0Ìš‹ÿŸÂÉt†Ù(Û¢'Îjœ77éøð oŒÒ†·�)Ø~Dç"Wµr>/û*å­8<àOªÄ­­åLgÛã)Ö“!ûˆ®«Õº¼µË’Ž7œÿº—+Nþ£]âsÑ?(CëÄ]lwE‡·ëã²¹µ.qöaÒže“‡Ú´Óõ¬»$Ô6”½#lÔ}œÝO�þCÚ(×�:Œž¥·¬=4Ü»8»›‚{=nÿ¤ZTîàn±ӦOòÛ㽉¾Ø�q^qœÚ`¾^£æ25Ež2ä÷“SŠÍº£š5D1û9ÎÆ£}‰Çô¼‡Ç|küA‹³þe2ý©Ñn_¬|¯}?øzãÌ?¾ž�pMÁ©^Ÿsì&)ã®&ëùáÑúÒ2âÑI>‡_¦�ß=S#Oü�×·±ÉÐù¾¦§îo¢}C}FÿY7=–þ²Vú¯;Íì–ß67áÝ/-߯2ÖýèÓoœ�G“•0|±ð—îð-€›[§•î£Øp%ÌBµ½ að" Ör©ñ‹þ?]” ÙÛï“çÒw#ÈÐ�íP]ô>íQÌ¹Ä èÍá×WnN�%§“Y&é¿à&á3ñ?ÝF¶ Ç ºljpc“d5xœÍXÛnã6}×W°êþÄÚÅÅBP¤)Pl² ºAƒ<-(il±‘D�¤c¸_ßËl)–ãM[ØâH"‡3g.œ2ýî—ÛËûÇ»+V™¦Î¢´/ñhÈpVT\i2ñÒÌg?Å Æ�05e—5×ZÌEÁ��íWÆ •×¢%®ÒÄ“EiØå²\[æ?dÚ`�6¢ÐÙA6 �RÎZÞÐE|€:ÎÒ„[?†š(íXaé.âê›küÍðiÀ¦�%�p¦ùa–9Ä»…iÒYæ»TRëÙ3¯EÉ •¬v”,-Wk/ I�G~I)ž{D�ž}ˆ=—Ÿ™Çs€£b2ÿ“ Ã*Y—ša:š\>^Ó‘bs,`rÎ8ëzÖgŒxQ±¹0½‘ÌT`'sMêÙqÑvį4Š‹Ö b+«¸áOä,,ëj^ÐGV #ÊŠÞ+¾†ãB|5`\;6\?Áq•\Yû­ä†,%LáÁcÌã6Lµóæ{å6Zœ…—Éôß§F=>X¹^ûvþô+䯙{¼žOp3‚]Ù¿î3ì6)ã¬Fõüøh]iÄ0âÑ�݇/¦�?ì©‘gîÄ‚ãÛP�¤ï|ßÒS‡“hh¨oü×ÖM¹[+ýÚ%ÎÁ{މfvd·íIx÷¦åßÄ›”u·!ýÆÙðk²ú wèöw¶¹ugêAé>‰7\-DTðߤÁ†/kÜ¢ÿ�FWå‚‚>öur_ºnº!ÞöÕERQŸD�k쀠Ž}}£sj,9ŸÌ2I¸¸MüíðßÓ: ŠCb• pxœ½XoÛ6ý_ŸâæÍØ1V À�8Ò,Å€&m€ÝŠahélq‘H—¤œúÛï‘”d9vk'N±,òîÞßý G?üöþâöÓÍ%å®,ÆÉ¨ù`‘á£d'(Í…±ìÎz•›~í ñÞIWðø¢ÖÊ©L…“Z݃×xàì-Å]³ËufGø= kµ�-½‘_ÆÖAÖ:™ÚÚ[ä’‘ %J>ëí�:Þ±Þ�†Âcy¹Ë�]š€ë%Í)õûÎzù+šZ¨Ou¶3T'4Z3Ö“GCÿ@g4šì 'cú)þÅúQͧøíçÑ0Ø ç¸ð7ÀW÷ýñ½È¹(äü´Ææm ìBo<¦ £­`‡Ì„cTHÅÂÐD*a–Ñ!ØgsœD™L.7K;xÕ‹j¢‹ìâÛ¡MhÞˆYr9p ’Êir¬È:ݘÑT™%¡ð$Úø&pa‰ …Eš7ÎàI{Éþ§@Nܱ%¨!=­-½}?ŽêýƉ£ßñVW®]hÞ_±Xpg! ^»øØ=êxlÅ2¡\ßûç6¸ý (Ô’ôÜß'x½Ó‰×Ø(Dì�©àÙõQ­u‰sISž;΂½\XR8ƒé”SPl¸�SƶÀ1š°T3J×HšÑÄœ©R�ªsz· H�dÛl™©M0å¿¥Ú:†É°ÕŪ´Jü›°~Ÿë‚“H±OeØäY‚-Ë@1xw®–À`O¡Ï6îŠÊ鎫 \s'�`A²±Úî¿å$ÛN.úC* >jAÌ„„×äÙ­ï�úRyFßÃ]3"¦ø$(¯Tf8KHO,›…ˆIðÌÀkkFƒ’ž�PÌN¢,ñ* KµZ—#¯5vøˆ^Ÿß^�¿~aìçJŸúƒËÅBêÊô©da+ìï&ÎN«Ùø£äV£ø ëW È䆧g½k ŒÀqã=ö­óG0,�Uª7^=ûÓ§®ô6êF ÛVz_¡ºï\ö•ïMñΟà ܖ©{DìŠ=C»+Ä„ Ûí(ëÚöÖÖºgmim€¨‹¡M5’!¾úûIÀÆô¾r=„ÌŠÕÔ�yÚáû·û֥υÎnj*Ol)(4¾,à¿C�“t„‚.G¹ˆþ…,Гå¼àP)Ö´¨wAaÐÝç2ÍBZ!Ï@ßXêWµÙ£*}邃°XöQÈJ”‘‰H﨔@ë‹@HÓ$äiLJX×}Ɉ1¢Q2â;ñ®ŽìñL¿f&Õc‰¥¶òüiãS¹AõÆÄSÕš£2hyÁ|C �DÒ %l4“¾ø5t³ºwõÑWl|DϸÕFÊ1\ßä×Cjux’<ëq_f3~ìa{™g(i¼qÄQñw.e;m2…ÿÁr–ûɽRÕY¾F–­t´{Ïä+Ü-è'Ž#_;“úIÕ®&RA¶›2UO"ž0¡1‹sV¨G­†kü_)`¨ÝLSQjµ¦žŠª@ÍÔ¦U" ³j5gÖ%—«ŒŠ6†$šC{ˆ§Ÿ£pQ>$®0“>6¼Ìstõ FÅÿG |Ëö!)ð�â[ÀÀÞT/Ö©îÁ}-(-=d[â{ê 讼ý–¸Õàá¹åõ¼©ÔŠÑë·DÜÏñ¹�÷—ÍËb Þ_w6.Œõ£ÍÆ"™ÃŽr²“K­Våá ‘BÏ䦕Rª®õµ¤ÿ\‰ÌßÚÓfõtkA9í^ˆº…âAÖëߺ†ñ‡µÿòª«/ �Šye•v=xœ½XoÛ6ý_ŸâæÍ8V ÀÐ(ڬŀ%m�íŠahélq•H—¤œfŸ~�¤$Ë‘['q: ˆe‘÷“ïÝ�}÷Ëëó·Þ¼ ÒÕÕ,ɺ>jv‚òRËîlÒ¸ÅñÏ“ï�tÏÎ+a­\È\8©Õað\\HÅÂÐ%»R6Kãö$K[µs]\{#?ͬƒ¬u2·OéÖê —d‚”¨ùl²GêdÏúd–¥Âûòx_8û4Á¯ÇP´¢Üï;›”Oha¡>×ÅÞT=¥l-ÌLÏÿÉRÿ@g”Í÷ eé|F?Á?¢Ø4ªù¿ý˜¥Áv–®F~áï_Ý·÷ïQÉU%W§­oþжœ]‹ªá�ÏèÜhk�±CÂ1 ª"æR s‚}6'I”)äº ³¶ÇO&QM qÁ>¼Ý"µ ­:1K®d‚Ÿ‚¤rš+²Î@7,h¡«Â’Px’ íJ|—¸°DÐÂ"/»àˆ¤?„äö§@N|dKPCzÑZ>úí%â8jwL» Ž~Å[ݸ~¡{Áb̓…�€víü]ïèÑ b+®*õ•î“; ‚B]“^ùï·I^ðÞéÄkì"÷'Ô;àÑõNT½u‰sÉs^9.‚½RXR8ƒÅ‚s§Øð§Œmc4g©–”o�´ 9€ 88ÓäÞ«Áé½@‚$Ûi@ËÊHm‚)ÿ-×Ö‘0L†­®ÖP¥UŽä¯Ù„õ«RWœDˆE÷T�M%Ør †èž©køÇ*^@ŸíÂ�ÓƒP„枀ÉÎj¿ÿBÔóBlNn)$¢"�^}¥Ж f2ú ñØùˆsFPÙ¨Âø(æ–ÍZD’{KvW�g'QÖ¡D D§Pa©v%"z®]sùìíųç�ŒýÔèSŸÿR¬¥nÌ”j¶Áþ!WpZ-g¿3*Ge5jXÚ¾J€øgxq6¹„Cpão@ÕøxN|ü@É«¶Alž}«˜ÒPz£†]+“/ Ö7 àvŠv‡ÞöÑgû –¹»C#Š=@תĜ+;l Ûú.Õ—¬íLŽœhkšÍ50_ýu/ÇfôºqÇzqIhGº `ýõöóÂC~°›º;ê…gw0ø;ÔÖ‘áWŸŽÆO`ã¥0K©îJÆ(µ“‹÷›Ôê;÷}oŽê å$ð½/A»X¨a»¢¥ô­Ç¯¡q¶mrŠfãË šxVl6êÐ1ú˜7á?Àr2†$Åz…ɆýÖCõ¢Xò]áäe °óDQñ7.è{mr¥xÏrYú1 ƒjkÝwÞÞú‚±ñ»wú^Çùµ°ýØm7ãµ Ûî]±n§.L‚·�XÆ¡1Tå^Ã%þo° äéFÃ(µYW^ˆ¦BçЦW" ‹f34·�$q�Q±c ðùôC#ný‡Pàö])àeb¶Á0*þ?(ð5Û‡Pà+彂�[C½Ú†ºwîKIéáqƒm‰Ÿ,nø€ƒw_yw<œ[^ÏËFm½}å�K…ϵ¸Œ�o¾­óþî6ºý¶klŒ6£Eþ¼‚åä€K½Vã‘H¥—rl¥–jh}‹ôŸQøŸ ònõtgA9Þˇ…âkÓö‡»4þJø!òBŽnd�`zxœµYmoÛ6þ®_ÁåÃòʼnцÎ5�µP,É‚5h–�”D[l%Q%)»Þ¯ßs|‘å—ÄÑj bóåx¼çx÷ÜeòÓ‡¿Þß?ÞýÎ [•ÓdÿžãO%,gYÁµöÝYkg¿œ�1n¥-Åô}É�‘3™q+U}Ç5†ñAäײ\³F«F`P˜ÉØïH&ã 9UùŠÎy55Û�•™yˆHÄÖdÂYÍ+ñîìÀÆK7‹¥æl:s:ùõ!ý{›pÚkìiXFCïΊ7lfðs�¯3•4ÇÛ¨ýŠMÒi_v •œ„ɸ¡S¦ÌM05c¶Ì.•?W˜Ä¯Èå"jR™‹7g~ÓËTÙVuÜg „ÙU#7n@ Ó¨Úˆ3€Tä,]1±zÅfªÌ/v_H³ÖŸ5ÀºPu¹º '�qô³+gžËO`j'u×È4N¶ª¤É¼¼•¬çŒÃ€©zád°¸ùÖÂ’¬n+¡eÆ*nµüΖÒ ê0­–�—»Ï¨å%]2öÑÂÈ +°hXÕCƒiÈ4pƒ>Z\ä�¿�ñ¬Àæm°’FëNK¥ÀåÖ¯ v07AKSžÊRb5Ð#kø³@1"L"³eÚª"Èay(3b²vïDiX²ÖÓ>¼U7ë_˯Â-ÎÈ‹$é¾6p‡Úþ¬áêWãÀÇL‡pÒƒÏP1îÑÆE€s[§¼äu†¥tËRT,Wµ²ì[+…-WŒçª± ö5ÎØ(-BŲÞã?ìŸ2¥Å½æµ™)] p�Í�Ç÷‘mù»ÎÒM:qŒ7M)ý#s0!üÉÌbÀ�¤ƒ�ñ’c¹Žžó¹XÁìÆ1#øŽ�Ùâ» q8a³¶Îœä%¹ ·l‰ lZ¯Õˆ- 7®øŠ¥®�œãâŒÏ¬ �Qár%Aš¶²´1vt×Rpšx7Êœ'Þ-8aâÜhkh)ù¢«õ=Üx ƒ×8ZV$ÞŒ 3>†óRÍ]º>�‹úó6N޼#ǯ|TìN%N/K.KáÑ¿Ÿºw#(Ùxÿþ¿ÙG+c>óRæHß�‡$£ÝÝ'ÈMûÙÏbZ÷žÐùPF.QÄóŽ\.ùªÊë³FÊq†BiëÌËn®î¯¯~c-hÇI²Ím[ýµÎ³CxÙÖÎ㣲sÀDÚ*uI¢Oºà…#7 ØŠ-�2X½À ¨­˜C¦É�ô»ÛÚºôT8&a<è¦Õ G<¥«°O�1´ßÛ½˜ý0d�@z<>,�û€ø;�Úø<4JIÁ³�˳|ôYß1e¹˜�É)ìø0ÀŽÇ·ãÃ>;öž-…œö€ãÖl~'ÀVϦ·ß_MÆ¢šv\ë žUS¾,å¿00%Ð.bq‰æsÝ}ë˜ñ¬ ži�uôäçD$Ø1¥‡V‹›OA‹7娛¸ü ·eɸÖÜÑém¦s’8ò™j®à ¼ìn5¨0ß·ÿÉyÿ1»†ÿXcrmúl½ïå0ôÊ‚Á‹Ò 1Q5ÐiIÉ�\Ïìü{;V1¾’s €¨¿íøÈlJž/Å®Ç@Šæ*f¼-m\�4=ž¯kÀã-”»Ø9ì8¾‰;Á{J«ÈBq‚´v«œxI¸ îÝõIöé畊EµM|ÑìSÅæÁŒêaWT[ºU'ÁêÏ?pƤÜúããÄ>ÇP_Ë,ÔÕ†ö«ÖöˆmÁ}ÑæÌLeO¨f\¿;kñSñz‘5ªÞ- ”5tu#C²x9A_‹Þÿ¦Ö6ƒ ‹rv2¯B¶hÖrý[Qé„0FÅÇÓ”ø$w,†±‘tHJßÚy|ëï°‹ÁÃ5õȹx¡–‡šÁV·™máÚôbbåÖtg®c$6V…FQÕ(*Ž‹´jç›iµÙŒÙ,ùYO½T™‹Ø¡¹½¦ e}½ F¡õïg(·öŸQt#l¡òèU´Å�ߣRŠ£–Ïï�û8£ŒÜ…¢Œk¯ü=L ec56ÐH‡”g_ñÙ9¤Hâu;⯖µ?…b†CýÑ~{ü«hlh¦nwCاÏ7´Iiköߪk‡ìøÏyÿæµ²] 2;�tÞ3üªžO?ˆ…ôe”CзQ.'ã0Íbc%jµá#Í”Î]óÙQÉþ}H&j®©¨}ƒÏ×Z.žöÍxO°#ЄeP×jýà…k\ZƉö§«áÙû]ómh/'Ì„ìs&-¢Ñíñ�Š‘K¼]0‚6.1�(fü‰ssä�zåôX;"Xšu§ …©h„¥¿™Y¤%k³-CÒ¥¸?R/Ží(wM7ø$g™¢Éç‘IJtöݯo¯nï¶×¤1]›'Y¸pVÁ¥ã†‘²aJssI÷¦¾ø‘¦ 7´<¿j™Ö¢%3Bö[¦@ ^ÝÈŠ·Yê´’,õÖ YÐö÷¹6ðˆ6¢Ô?‘SV@=ÉéYÇ/éqešg)C?œÄ*I6�Õ.ióŠÔþ.àÖ€•”f ³â¤ÅœÛç²t@Û9¹RRë‹{ÖŠŠ^9g£Òás‰Ó®Ä}ÀÒé‹WÔ¸m8qXN9·JÎÑF*®I9÷žDÝkb`‡€¤uÍï ‚MdM Àl“�׆”¬'ƒâ•( ?(�·Z*" ÍÕ=sÏõÒ�½Ã8c�ˆ~øM¬EGìïZ¶ÕÖ™ 4 êYm’ÉBr @سCq€XœSô™xvi`e‚j½ïK´0q™i£d¿ËßsðÛj !�zQBÄc£x}I­ÇÏ×`koÁl0^ûtâ ãÒOá]û•½�ÕÖ>›—ùh÷8Åσ’×Ö!Í·£=�Á—˜¥¬°ùgTž™ÊGõ7ºxq\}c§ß@Žjš?ŒŸþ톕�GŠÞÆCˆ%rÏ”@äî‰ЯDAbîão 6Êõ�ÐóDZæV ©ð�Ã%ÔN@†Ê‚¢æ`3�³²q+¾6Þ÷%”£[Åz 8:šÏïc ÆIW¨BvZ‹"¦ÑØê”laÿ*«ßz"Âh-6[†Ïóf|Ù•ÊçíüþhîAuÌÖõ_,ßN*˜nGàX|bâ—×=LúJôðÌ³Õ ½ó™æÓ»üwËriOHkc~³ïÞNv}š/Ñ ÚwW¸îr¦¹.òO4ÿ/Ÿ!²ñȳ6ª;šßE«†=Ùµ¬€#ÛÚ˜>Òüc Óä=“/\ì³:4û Rˆ|Ã1û‚l«‚'ÜQg]Ì·îÀMs?xãìln÷o<+¯ ÷�ßÀ)Ííåx6/‹vý=-`; Ãhš‡IRʾ»½:ËaîÑ_W;Ü|Ã(æw“«—w¥[®\ <Åÿ.¿�Žõˆ`[켤ÆðY §¡KyFûãÄÐûܸ»ÿªñ™ÿª®çÈçˆS�jìxø ­®ëû}à §ädž‹å¡ßZöÿ›RÏY¯i·O½ÿ“[ñó¿2@º°äQ=Šñ?ý?•çŽÃx¯Ù {ˆl‹O°haö $n˜Ú‰ÞÓp7O[&KnÇoJKjÓ7{NX~=#~¬ÉÿS2Ð4{.0ŠQùyÚÔ™±»6rR§�{DéqÆŒÅ;õßvS÷ýøد%B ÚÚš#g´J2Þxœµ[[o·~ß_Áæá8ä¸-PàÀÇ1ग4vƒ&h’GJKIlv—[’+Uýõç›!¹7­c)GŠÚær†äÌ7W2WßüøÛ«÷ŸÞþ$Ö¾,®³«ôCÉ?Jå¥X¬¥uÊ¿xÒøåù¿Ÿ\`Ük_¨ëW…tN/õBzmª·Òb¿¨üÖ䪵5µÂ˜rW� »ºˆŒç&ßÑ2ß];jçõÂ]Š#‚2»’¢’¥zñäËtÏùófº'×W’Öýþ�Í÷h°Ö÷ ©Å‚†^Ü5¥²z1üpAkãwhð,Ý¡(ÆìÓã—™î#—ÆIv·Ú-쾌Táþj¤U¢”Þê¿…«ÕB/w¤,RÝ"2-ÇLiPŠÚèÊwhÀä§zöç³íL"fiQo‡ÈäÏHœ ½Úƒƒ…õ„:MÒi…§~-ÁÄÍ‚U™­Ãj6XW.Àš†‰CYHæ=MYMÍ®­Êõ‚,ˆ)ž±%$#2ˬ[‰x%>d…­Zch™»Ä=± vÝs2÷T\½jÊy ›X9›X:о£‹nè·†e^Â3äj)›ÂÏÆ /Äw-çeR‚øæ…HêšÑžö€ºo'è:2¬ûXÂV¬ØB6Q‘ŒáÛ ÎÃ9.¼A�¤ðOÐÕô”çYk7^U½}aY5ot%ià´j£ì޼VÞ)潕öØžÖ™tØÏ4ñmMbH;ë}}cœë>¹-ÛT„V‘ÚòyÉÂgÁÑ.G.g ês„ ï©r1å1ÞýqÛîl»6.HÐeBU>.37c°ržâ)À èh–ò\‘X–Úƒý%V,9ÜË‚üý.ŽgqÇpJA;Ñ_ÀßWƃrE(Ï•[X=§Àzíë ‘îÀÝ¢ Q ñL l­�·Ú¯CØÐ :¹?ÔtB ƒ4ëzâ HZqXg› HžÅl«t Haké TÎ6'}ïœoÙ�ðÙ]õؘåýÛ››�" �eQ`v¥À‚Äõ¹2Û°³ÄˆQmøP)ŠÓ‚ Q6�#â“gO—¦A°ËÂ@Ð+ûÙ/ëjJ%Ãï SÖ…ú›þ¨Î+µÂä ŽEBýGY¨‘¤CRʵ\™Jlá�ýšL£2ñèwò®ÍlR×ëjÙ™øMtØO¡z.j«s«"Ø„¹iæíŒt¸ãBô[«�=5K>-\¶ JE[¼w8½Z{¦‹i ÌÆ±~3dôLÍþÄ~!ÓxºäŸ­ÚhÄ%– [‰h‹Ô­‹‹Úw�°'ƒƒâ×™ê;:U(-·Ù!ÝÉ�wÌ~ߊÛ�!Y6Õ"üBÉÊ0Ia`}٬߱kXÇÁ–`–Š™ú´`›˜Sü¸ @Ž "'x‹€ ú¦ì<Ík 0!3ž(L¬ÈÔëD?ò)@Ê’°H;C½‡•Ü´µâãž¾ÂfR¢à¾<¯ÈWü‘\�Q™íŸ¾P›Zc¢nåw!û:$Œ«dŽË n�"ä ÑÍ'—ÀþnKúbw+Ú¥I‰âöæý››—ð5èIÕ—©WÐ÷»g�?�_¹¹mÓØ¯ww{cwª±²¸SpÁöóÙ~âpÀœõªÎPÉ…µ¨ï ‰§š%Ç™aë+à×±ñ!¿� ©_è¢p&ø@wãëÒ®Œ=²Ó5¤{„DdÈþ ¯:Mm›RðËM›qzR¥•ze6)¾].»<�Âî �…?B2¢r]CóqF&¸¬I-&§ü#hølde,µ˜Z©ÓÒœ"?½Ó™^e_í¯+|Œ­ÎަSËCº�.‡pc(Ù�¶ÀGíë]O.•õÖzmý;ïÕԧ:¹n†Ì÷Uòû^S˜Nzl˜0ºÃºÎHO­„»¦ümî”Ýðç#ŒeDxrUìñŸð‹mëÌôf> �±-„¶ÿžN&YßëË7ƒb¾p0œJA‰WÛÇ“+êãt�"IèÀ’95¬Lå{Bl*ç‘9 Òÿ@†õ€3ÕT’z4¡×Êݲ”�Á`(I‚IqG}±Žøi2°g!/áÜšm6&ééÿ ˆ}h³÷�)"“‹=ºPEJáiåÙ0ʆxz«9UÖQ´»”]15[÷�Ê`Áýw® ¸Pá"–?0K7ëõÖ™³?«º×Ћý ~:ƒŸNŽÁO“i;§¢…œC0ÇTwE¼¤J2#º‚ÿžÑž¹]tñ‡s®VÚ!ÛÝÄè$ÌiÊø61Ý#œ¦;‘plòHYTxê‘Þûl¢¥œ¦Ñ (©cs k h�œ¼„ÝQï¹ï&fÈ¡¬%8™�‚üÛ–Uð©¸×v� þ?F1<"¸�O®�=þûºŠ/Ú9Ý‘òÕDcÕ ÄîÈÓ5w›÷Ñ“&[}‘©XÓ¬Ö�ÊÆù~A>WTÂ¦Úøî ´õ0•àå;'(þÀæ ¦qµ~«üÚäû)ïwuÛž÷r‹5í†Íã¸íY÷WcþîQx6…¸B.pø^³”ã@º ·xQï·Ý†ÉݥᳱÖÏú@›l!t�ó Z4ÕêúGµÑ]¿+6<ž_]ÄÏbܨ£ðvy{<8ñ¼RåµBŽ]ò=þݶ¾4†ý(‚Ñ{Ç�Ž5ß�{!ÉÑÏMC÷-Ûi„]/Äìo”ªâ­Yè©'8˜†î€¨gW·B´,7I¬„W%Ôæ©™´îèõï£Ã¼gÏ�q|±„Ì”~"¨%]äÃü€�;ÃMçll!sb�(C·=ÑÚ#ͦ¬ÉRû‹væ]Oq ª)º±†ú¢Sh$sðiéÒ�ÓøW_ x–†ó™8ArKì+ægÔà‚_[lGuçõk�†´ëf^hGgaåÆç8}¿þŒ#�º^-rGãdžùi4áÃÄeçT[1Lê^hõ`j·ÿ‡»ûö¦àÄn¾å{�{w¡\ ÷èœ�ØŽji•÷óIIĢ¥Ëó¬OÂÁYW�;'(³=FÀï·ž’�Ï/Ú¯©ÜëU‚�–qd0StèÙD¤Ïþ_�3¦×x4�ë;rýR¥>.‰Ù»œ¸dŸ\ãÊîvàˆ^rçfNÝFî8O”ãé#ÙÁR¯{È»³°‰Å¦ùF$»ÒKžñÝöô"lñ•Ð('Càˆ˜®¾8fwkrÈ�Õ‚CƒÏº;©6æ²�RnæêBÇ÷j³ìÅ~DÄÇ@ÂK]ý”¯Ž¹TH'ÇAÇxø&}<ÜÅN”:mi:ëåÚȆÚñš31O "Þ§žHJ©ùê]ynêhz³41kÇt¯” j©CK©�2@­È™<2 ,^Qm®ù� vXqÕ&$á*½Kü )rñY®�ziz‘Ã/1.S’²ÔEŸ¥á©åp¿ÜÜ&–sŠ:sµ�°ƒðœ ritáÃi8Y£”˪VL¦±ÜÊéM#lW�š5ÀGÇxýVÙã¢ÉéctƒÇ+ìÃû¿p%½j²l�ô@î� r! i{…$C?&ëéÑMDø{Z —øuoª�IK~П2 {‘Üx™°(Úä,ÜÕgb¬Z ™ìBæyˆ|³sà°MùÛáñP÷ªdøŒ'eß[‚}'l°AJ“κK; Öê{/V÷*õø2€²mTBÔ‹¬×Ȇ÷®Iæ|ÉÞêôÔ¼2(GQµÑõ` •nT*ºÊ%TMíÛ;bEåUÛ#ãzµw´hzÁ5¸`£&¼…ƒ8Ï“ív…TN™^AÍÒ»¾¾m#®ˆ*Y HÇ åŸ:•Jv4ð3Qϸd.ôç¶ðÚ½‹Ý궈ýYwo~ù®ÅVEí ÄÆHÒÀ¦¨Šƒ±Rý@Ë»%¹ÑÝ-³»'™-úÝ;�Ý{’"iYi�ưarowvÞó›9N~÷íç×Þ¼K_äÓdÿS2ƒÿ å¥H—Ò:åO�*??þæh ë^û\MÏs霞ëTzmÊ7ÒÂ2|PÙ…ÉT..”_šÌMƼ;™ŒÕ™ÉÖxÇSçá¨ó:u/ľÔàX2‘¢”…:=ºÿÐÉý��¦“±DFžïeàé9�Y‰·�-¿s�᫇KRØ´ã†br+íÔ/5HˆŸÄ©˜ÌvšŒgSñG:x‘å|nĄꭼúl2&&&ã2:çVI¯„û0³xÂÌ~V©sc‘ZãÜñ­ÌuF'…™ó®š–(�‚;IøêLßF-îø«#憙j`‡KôEOî•]Xå+[:z]–p$î´_šõ%B:á—Jx+5Ø(mT‘,³Í,E"«¸¬ËEˆúL·àÝÌMž½±*Ó©ß?ÌÚ§6ÕAÁ”Ë™ÊÛÑÔ%_ÇNãr½0yP<ÿØç!D¦K�U�•Ô8Ï ?}ŸS6ƒ‹»•)�ržàJNÙ[âωÒxp)ð+|BÞ‹N§K!{a¼Å©÷ŽàI摸BAGâ¤û©¶B[´­b%u¨R�ðîÈ– ònÔg’ô Ët½g¨aV %©"Þ°*h½¯Žå�í`ëå+í̧ñ'‰¸†Ûc4GyYøhPb^ÉtÙ6*ì�^ÜI—ˆ¥Ê3a*/²Ê¢]ÛÈÆ·˜T9×ç¬Ò.�@ƒ5ä5;RcØ$spwí� ¶š ¨\Z4r)/£Øø¶³!‘b¡æ:´�PÅʯÂ.°âT²£üETžQ+×Xi ³Á1¾op HDYÊê´Vä‰/3ekS=ý+¢=�ëäÇ*þÔ­²ÀS[;pÀéL‘n—L@Fê1_ƒcˆLÏçÊ‚ëYS$ââìúõÙ_êŒ f©Á¢V­Œ} Dêd¿5àÖÜ9¬wàÄFÌ*Ÿ„h`xê˜*¨x{ULçÚbŽ�Ox5ø8Â-Ú&µ~^°Ôµm�B® ï£E#˜ƒú€üÐç: ¦·Ž"y0ªŒƒª“å:„V!3E‹5�Uú„-d×Áí–¸‡”’‰;S�KΔ¸C]‚ÞÏDnpy3KŒ páà_Ü¿Ró } ;ÓV+@Ñuf¥Sq6¤=/g¹:·%L�´ÖÙ xúè øåo3cÁ÷꯮ ³‹»ÄWVÒ®¡Öx ÿ–`ÍÌ/O�žó‡#β?T~U…²{ÙÞõç?Å]ß*—Âý òà­cÏŸŽÓY÷ ’=¯Pj|ÖÞFw¼Ù’š9}cvpƒ!„áo¿'”ôüzE;¨×…ôéR¨Å›”ú³-'6cõB—`õ:ãaz?¯0Wª\ªô›n@7µlÃÌQ–š]uáhNà=èÿÀ~Šk Õ{�Z¯lWëùæÊÖÓjÌl *«?‰cP"7W Òè Û�­QBAHå%5yU”ƒý¬6‡ˆŽ—ÑM:ÂZª½L�üÜ”FPà .“d¤^öŸÎ™‚éÜ´Ÿ¢Ï Xí­gÊ[‡«ù¼†o¿Ž–“P_·iyø¹GÇÉv% ªa†2 7”H Ö�SæëFWzŽ%ä—J9ˆžŽuÆ”Ñêâ�6¥kŸï//¹Y,÷- KØqŒÄvbWo/6ƒå>1XÚElÐÇl <œ=ìˆé¼^Ú;°íÂ#ï¹^ú-¦[71€y?¯µóê'`s½z:.Td§£`Û6*1ø¿ÚØ"ôB-7nÿIÇënS„ÌnÓQ§š#0æYKÒmñè)�"DX[�8ãÈÒÝòÀL0[×{ŸX÷Ke^B­cLFè/ÓÁ|ñ©}Ä©�5ºMj‡Í¨‚ '‰¹ž†Ê]‹%Š RÒìÐñSl—&c 9#õ½tú Æ2MÕ [š §ÜÜaÍ�`·PÇoùTì`ÿ�AÅ/#¼¬ů㞷�ö€ˆ_Ö%ö)ªø»ª|º­Î~Ýl ©¬56îqG®æ²Êý(N»âV,]ÍÎð :…î:u†uû ýF†ú…®;|lZ‚ [ƒ›p¸�w¼áªóí]ç[SêŸÕ·ßA—£‚‡Ÿ·'±aàÀý6hÞB]_Ak„…LwCýú’$±?kóBO# l³Eî”^,}è‚{€„@W“ Ú�à>Õ²/Fà>Æ•ÐKÊ…Úl׺ѥô��WÍ@ &ô ÝLØŠ;Zš>HwÉ m]ûˆ~«³JæO[ÆhÝÓ¾õ�´ö”ú;Ì¢÷Š}Vw5sfÙIá8°˜<õ¬~t9X…õo>j„¶5>`z,XrúGÁ¯mö¹»74>hVü°t!-´�Â%>ôùÞüôoØ �¦½÷˜Tठ£ hiGÚÞh¥èVVfn'^�h¢Šš²ôË£éåÇ/xÅ”ÐüŒÆ4ãäAV«mj²M Í]Ù.·, q¼Q~ óå ؅  dã´ŽáDÊд*ô_â,á#6è’3z�‡SSÎu†SêÞè5B+Ê¿àvjlQL>r�æ_¸àU_L¼!Œ¶ZèŒYÅékÂc+Fc+œiû|µ ÓQšHÐ@¸~=Õ¼ú•ÀØ;,‡øï�óp`º²&«R…X³5 Ј�@£úþŒH­)'@H7 çÒ­Õ.*œü8lº]¼Ö“ÚîO>=g¼ÊêÀŒ�GÞ`©Až`º¿BƒußÕj°ºÙKÑÏšºT7u!Ó{5Z…’e`d0¹ªS,ÂÎY÷ðÚtPI‡r«“º?¡þ÷:ª�Êû­£zÄŽê7ÄýPÄMä�ˆûzƒ‚¤p*Wá-–��þT£Ô «"_óË,øYàŸØ~eKߥ ¤* î£0Ž�e£vßž%°¹ õQ¦ïpíÄÁC.,l(jÐXÓ¨•þ\²†”ºaXÜB¢©[ùAQ9[­PUÍ\Í�ƒ0r2æÆ÷ý{W•·‘Õ}Ù¤ñ„ë=„èIÁ{cà�/5¿YàèïŸx](÷ò‚Ñt÷=Äex³9t–b�„~¹û‡‡8Ããï‘�ÁþÊ»få mc¾ †sYc–‹îÛ¡8æ ›…¹öèÿ8¡˜.ÃZî^»ôÉ5·ÿºùwpæ—{i£õk¸w�åÐ.„w¯ÆÒ+¸ðý#η"ë�¼¥oiºžQ3»W§®Tkn†Ñ¼¯s¡öõ�aÿÝÝÿó|æq¹â^û¿çzl~âLé�QÒcóÄ=ëþ­j¦6¿ÛóoÄÿêd ¡ ‘¡£8jÆtLãxœå\{o7’ÿ¿?WÀEÒfdY‚;ز°²ãÅkKFäµcìœiÎ ã~…dKšî»_=È~«§åë X Ž=C²HVýêÁ*rÎÿðãõë�_>¼[—&ÑyøKÉþJ•“bµ•Æ*÷ò të“ÿ88…ï�v‰ºx�HkõZ¯¤Óyvóé½(L^(ã´²ç§Ü':?õ´–y¼CÊgÖÁëôÊ>ã4 st.E&Sõò ×õÉÏ秉>ë/æÉÏ0þ4b…m/¶?ˆµ…ÿNࣃ¡«<ØÄó°‚�8_^‘%LB]ÏO $w!>ë•Ë�ˆ¥“ëÛ0QjO~8à®—"+SeôJ¤Ò}/Vyæ¤Ît¶n«D™7²XšXÿ¦b˜ºIù‰oäjÁ ¤L3‘¯Åù­4¸(üKý­Êœy¦ƒ� YËe¢Ž¨ T„Éï¢î�|i•¹¥­Ãd·ÚÖ»‡ÝIž%»'QµùÏ[e­=¦$âNOµs°úµÉS!…ÙÔ¥Z’Ò�BVÆQ`®.˵иç@ ¬^ló$FN%êV%Wd�Ìsjg#n ’–wêgÀAÐû«*œ¸Ón[¯7ìää5 ®/£àú2¸¾ �§Œli™\!©xœÅZ_oÜ6 ÷§ÐÔ×&‡­(0 kšbÅ’&X²þy*t¶|Öj[†$ß%ûô#%Ùgû”Ëu›Z =ÛMþDJ$EgùÃëëó»O7¤4u•&ËþÂY—šF²’)ÍÍíLqò3]ÝSñô¼bZ‹BdÌÙܾ¿Z.Ü@²\x+™? ¸Sm€K‘é_HàEàH–Œ4¬ægtoœ¦ËC9?…”5Y¶$Ñ3Z¾$…†'ðhàÅLæ|¢}¹ Y� ˺\´(.%·]ÛJeÈ{ž©ÈËJÑp§fx7qì¹Øôúk}ò’: w%'N@¡¥»—ˆ¨ÛŠ×¼1š°Gô&ƒb®ˆ\ýƒ¤€aÙð'ÌVú‡VɈÔÏɶYI2Ö�ç"3 Q\·²Ñ\[ ß’œ�£;-š51hÏßC…ÅPÊü4ù:�sÒ]ËÕFhž“Š3Õ "'´ò<±@f/¦¿èSBÞ²•ê‹&«Rˆ&ïAÊÖˆšU¤|ÙmÅ@µ)LOó–)f`zV"\Ecß(83�âD·,ãd+LiÉ5»uWÃU­EßH•�F6'̇©êÁ‹‹ZK=' nM¾pÕðŠ]“!fMŒ1­³'ܳ„”b]ru’ ð¯@ìõ™÷`½¥Z 5#+Ÿ+@ÍÁ†O¬$¿&ÀHs?ÂdzS{Æd@kWLéF2€oT‡®i_M6ëô–„JKØ> OJ�[*^œQïûÏ—Þ¹§GžyÅt‡7±½€ã7´l ®|¿�Ëé uo®ŸaYƒ³�àÚj éÍ@@á ã†'ô^Uº4¹ÇK}¶Çqú‘¦�#P,ü¿q[ ‰wä¨ý?u¢é§�nËI*¶â•Ž£ù]W_¯4l\KÑ4�B¨€e…ª€9æŒ�ï¹ÕBø iB„c6ÊØ KÑÀ�f°aE8sí«ñÅ@6àxVÓtúĆhdÔÀ­Ë�>-¡fpéè)èV?> ²ÅCd–â-·»?hµYêQs´q€ÞŠuÍhj/‡ãTDMÎTNr¾17éUGÓ«î0˜š³hêABu)Ê Ž›rFƒ÷ˆQLØ2¥±ÆøkÕ–à2{ �¹C ¼Få_&y"„K›1@½‚Ÿ¦ø‚tiË¢Q`ø€ßáoÒ ¸¨Ž£ü­ö¥®«ti:#×�/Ž7®8ƺ"o”}?†siKšˆG Í¢—\sœG@Œ–"úÖ^Âá ìÁŠ­D%Pú¡‚Ž•Ï%žcñ7XaÔBOÏOqÀ| é‡ŒQYH¶Ž<±ÁïöÄ5èsJÐwÃhï²ÙÉ-RÔ’÷çöX!Ü`üš<#™¼÷'1ÏÖug*È#o³s§éŒvqßÂæÃ"Ú3Ùß½7T9CÒŒwz×a].¦w¹zÖé{‘6(œÏ×R¥WÿArø[Ì £`²{/ú±¼ e Ïçǃ0ý`Á‹Ðwp &dì­`µE¸‡¤H9ú•h.ò5·E‚»  á8¸�8¬ßlsª_–ר²ÛQ8¿t…'Y9ýàP?�r"õƼ�H9<“ŠCiÚhØi5äðÉó#u¬tIbhB:…M²è[:‡ÛC®¥Ø÷†®ÜÓh �å=v,œö©í¹@Î>²¥çûº®±YÁøè:ôˆ“'z¯n÷Ñ&>ý7«ä ëäP‘Às^íÉ!+½vŒv}ëiˆËž×�%xZ³�1 è®þH#N»Nz+1yCV4rç\�³Oõø§þS²†éî�ó+#Æ´aûu«›~î㧃Ø«FS6þ{äÔãÍ͵ùiê®æ3xÁ2º�%O|óà*âJjØ8ø>ÔmáÇ-F6Øú›/™K‹t¸=×2i#Lg�ü�Ñg ôod�þ.¸å{V‰Üðã>�E„ Æd¸EýÍÁlsîx¾¥Y anà€ÊÝÑzüBúF˜>TLJŒ®&Àojm/bz�KEšâïaÙž»¢—³fÒØ‹%Ñ6à•n£‡¯ˆp³K;øE+Ìm3�éѰ_Xk·Gšüû�ÖlíVš·Áî ›¿‡Bwáÿtbáþ"ã|á V‹ÆˆD.aǣк)Ô h37¤Hò Ì“€œ‡Í­B ,°…z¢BË•Sðѹ‹¨A¬T’ðˆ³º�¶5-0 §XB#ôXY¤ MÑ0~±‹Ðc÷‹Ç®¦€°‡U½m±ÎMä%i­Ež‘@›d¼ Lí^Q_•éu­vTð�¾sÀ G²Š«ŠÝ �‡Ç¥(r«�¾UÀ4§6(J™” ?ÂÍ.X l�42š$ꌷV(Pp¢´¨é ᪩ôí¸×¶9äó@ìo0í¨`B§9$RÓê‹Ûj& åì‘DÐÍ–Bv4*QÙ>˦^td‘½ÑÐi«YK#Àî2®-X},Ùé°ì(¬{TvB¼u(0•n·̹¿ÿ—gÈ"w[• °Àf×\o5¸d¥µ"î09 Óíõ]ƒ O5ÌžmMrõalÕyÆß)½ÙÒò~S&œñT¾¯@| ›§-¤y]Ï�ˆ¢D'Ȭ…N�`Ž - mðsØ(Lªv^¶,ŠÜ¸ Á8¬Á�êÊæ™7\i‘(2Có¸@©¾¿üøîò•ˆseg³ZU ²×fÕ=g²XM‚cöªfÿÌÖª)×"[Uñå :Œ‹¥Ýu¹t)Zì«¢­}Ú t%fjÅ�Q=iAè‡Ñ¡×ÅÜÄ@>€Ú­È@î„ã!̓‚žG¡~¢PÃ*äѸ«itœÅÝ´è ¸ß^Ƹ…}âêhH>A<¦7A3V›G1è›ýJQw›ƒ³Mj#Ê@3¨ÊôØ1Îê¬ÖŠ2Ó¿–ŠM—�ÆÝ…Çù6ÄÐ �_w»B…Øš¿éDÑ pq†ÜŽikÏ i&–;£™U‡53='D^L8ú‰«=/yôJf°l0Ù2Û…Í®ó$Éï‚nÒž{ö•˜YH4qÆ- t‡^ܳsT©[Þù�ŠÇ4®üá§ÕpŠsOé�Þ¤rŸÔchzBcÙ…�±ºÕ“Bþ¬µù “�ø’#QÛ=O5Ò ��%<à=0\|ØØ3Nð¤¿T€ÌfÒƒ2!èò\a|…Øs’‚ý_cÐ׊œ)ÄÎò*,¬×­2D.WY§SéÔ‚ã?ö7àŽ8Âå²9GÐOsÜúU©‚i·Ø—µÃ`¡B¬ àÅsúRA,ÂÚ‚±*ÆÉ¥H«c5ÙŽ¾/Gñù¾œœHe ™©’¿?iÒéè_´}¸íô&fÅþ5 ‡aò°È)möœ9:}ge—d¡ÌWÁaQu݃T Gæ•+M�p$ UÃÙ±i>C“kõ±+À˜�‘x÷öNr�F­À#Z¸á;c-ó=v0uŸÞ£{ ˆã8˜²š ª´LX”H�ûÌed¶ªFÔM7[£³¯°ðª ÃiÌñÞeé‘Éû¢»­÷Òm?çæ+B�µ–ÎJ8îæý5‘{óã%D$yr«| W.1{ŒZ£Ã!Þ”ñ;SyeZÁ«û«2.OvYžj™\cˆVºTIžm€Å9¯®ê@ÇaBB;ÀW”Â, r|æç\pF{€á£vcMwð vÐi×�¢ÆêlHÚuœµVIlŸŠ3¤K^å÷¯ÁIWgN<ÞA«Õ X¡Î fØ „7Ã-×ë5(o—·ý¹ÌVœÎÀaî‡Ö«²K¬ ¶ÅÚæ9}]&Åv<ô¢sO¨o>>¢˜B2sëà¢Ö@HcÉdÔŒ|$OVwÏyšˆ¥]ÛÊÞƒRiEÔòàÂ~¾AÙgç§*½ˆ:e€æþ0êÒëuº¤�¹Eúµsy¸-Sq”/yòÖÞp×OÔó¸)dïžÈËM¦]×ÇœFm fp@†ª ÿßHÌð°bÛAŽ­a´7°à©w% jèÐà‘Ø\é;*™„å†}yYW^½È 4­>/Ä&Ä[šµW�ÊšNß9¸Ô#9º·K+ >£ Îhî­‡Ó«¶éIZ‰">N­ îpNJ)ÛN¦}”ª D­Š›¿½ QLaá¼5´O½yÞ3kU¾vn{&W4 îãI®!¿:CÊkˆì^$4nì÷Zöá±� ¹&;­ÿÇ�ëÒWÁ€Ú÷g€�hoàGY†z?†òË ÛŠúG0_ˆcæyS^gÿûÙÿ ¯ðä,š<{Pgî"FN~ÖžΆ'Ü•Ïi^Ô×uv ,œÇ˜¶ýìtMš_‰&èÏcuæ—*:§6˜¿» ùcëÄ03N]º€y�ôïì)ëy\°'4”ÕÅ»Ðu)—:Ñxó2@À×Å&&z1±Ø¼Ú‚ ùÀV(oŠí2²ü�âÒdŸ¦”¥É‘Ǽ±ZË2q•½ ¢d†ÊV�¥·Ü(À u;¤s1ÍÖ*�³l<5ÝÛ¹{7óγ`21´¦„¢¼É…TîÀpú"Ÿ´�Ò!´Á`ç°“ß9,3�IÅú{ÙË'à9%jäÉh#Uþê µ}ðTmœóeÍH (7™?é×(ÛáŸ-7U¢�¤X ¼Ö-½‚³øäòrNš0RX†³””‰ÎÀ½MøÁ[µ(LÔ ¦Í¨®¡äé#½øå¸‘Ç`ÏJSFÁõÚ±)؆‰0Ô‚¼ÖæeWÙ 2`Xaê ‘ˆó<ÜT£÷¦.BqY‰Ê|w¨8Fä˜éó¥‡îù&¢)`ýÈSoÁÇ—Ê)º�›eX3ÓKV¾ö0­fÕí<¶ú4äõW�LÛ†/�ªJí´:ð.êë‡+iU³üŽoP80óŠ^­£IoÜÖˆõ†Y·ycް|¹Qul×\ômGŠëÒ'ÍŸñÞ|5ЏNß9 ×#ÙÇÜ›{ˆžù½w¢T�Nƒí³”(­úïOâ¬>R¸â�Acvû•K@Íj5@,Ï6?†«Nü É§¥ÏO}3—þ9ë¡Cé<¼Iª�8ôeI¸0Uß"àHˆ›Ökpd ¶®ž×>j+~ À(QR¼ Ÿ 8ðVœjó•u_bå¾÷9š”cdÄŽŠõÔ`UéÞUèG„ÿ™•žÜ|z]ëHôpGP÷2‹ýÖÒN¥ÙÀpe —t�£îœÂÆ(…¡åÝVº¨y:çË?á¹.œ60G‡õÐMào±ÚWã—¬®f¹du5xÉꪬï$�÷­S\‰’&ÛÿÚ¤Jà±^ø'€þâiAýz�2Ø�”¡·hý´%It¢Ïyr¯¥S›œÎ«ß1 Ž˜åL7L¸/œ·˜ÞläGêqÓ:ôïÅywª=qr¼îŸaUuc¥‘·pÀvo{kiJ¼Ž›õ�ܾE9À~S8õˆ§ÃCæÝC”Gn{·™n!²>(O¿/Ýí}r5ŽLSˆHÚ«¤/£ –rPäSª…:ªêXãäLuÓìM¼Ù#ÍÐižúi 5TC��}©MÍ@tZ‘iƦ`åªï+¸ÄC+/á®z÷ì�$¸p»2ÊQd‡n�¯Xú—uí×säfù›urç¿}ηè½5›ÁI×nü�MqµÂ ÈÌÏD[ç{|8ú>6bœ òº@\Ôuátª£ÖŸ”ÅÑ(vöŽžT&|eC .�µm‹½”DFÁ¢ÈËä®t3xdTÄ F É{W|4ô9=êc6 ÿ¤Þƒ˜eér°*˜2à€“¦”KÉx›‚/oôCE­YÝ�/Nút2F˜eV_MáIþìB+ÂF@.çæ€3ÖðþÈG‘f«Ç k­®³Ö:¯r£?øÔɰ�óå›0�z¿_QÃ\E¢Â³Ež0øþNfÙOƒ•Иqn£‘XënŠ£‰jÛ%•Cb¼"õ—�0é½Ùart¡'_þ↵©‹—â0ìâ(âpþh¼Î™FæOfF›ŽÌÿE‘uoÍtýð‚�æVL†Þ)á1M'îD×E†^an:¢c^Lf(˜xV{ú� R4mz¢U¿–Î�à�žôB¿‚qâߌ�J"Zè»Vþ±Œ°*ŸUó§%Õ–ª�6¥±øû+mâtMÎì@/œ�?[ð‘±Û¾ã ŸÒµðuCŸÎÂö§Ó8ƒ8Ç<šDÕNWÛST, ÇØØÀÇ*b¼á`]8ŒÜ»D‹ÝÈ ì ⎈³6e¸&*Ø =ó»~ÂõÉ¡Þ�»»ïB›Sw»‰®ñ{±h ÇG�­®IÆ£–—Ãø]à$¶é_Žás@Ë¢çWìSƒ\ýÚÛwô.>%ŠÍÓ+ºÁâó•GÂ(t[¥n¯:ñ‡»: !»£ÚŒÊmù:©ò1�X°Ç™64&[*-:$¢©k 8š5ÇÆtì9Í. ê3æÞ’6ˆ0°%ÚÒuýjÜZ䨷¥>wŸ˜˜xΙ©stÄ„�bNºÝY;&àäX2Œnè€N9–ã,Ï�®uóϘ\n‰[Ð67ø¸�£—JælšKU·ÐŠ3uæè‰äÃT#wèÈì/ “b�XÑöœ´wXW®@» ‡ ýñ’O,àb'̲4‘KY<à3+ñ“ØÆ?MƒÑxš Ùq¶Ð#=� M3NSÐÉ›³¤™ïñÉ´9*D«@Ö‰ù{7?Ô6…Õƒ�¾µ©Q6ç.™e~m·„ýáÕ5c´ën3»” f ÃÔ_ÏŠnæ?¯ö?ÊY}}ä=ÎãúäãH†5º ˜Ÿl´U�qÙ`VwéæýšÑ5‹�A²ù2¡G«lÜ0Ò¼”YN‡ïÌPú°Æ&x:Hš ¶£í‰‚ÄŸ©íŠÁæé]"Îçàr�y�’³Ü,Ú$òXvø<Ž—:ú%%S» Lgd�µ¬ðX ç›ñ˜î¥ñ@8Žl"¬Ï²ÿðâE{[5n€¼Ê0Yá·†’q/$DîOäÒ¥íp)Pl ÝÁVæþ!o«_•B^TÊ$¸ÑštåˆipGcdL¡ÉEØÃY [%uSñþxÿÞýÿ”Û‡· éo?[®­¿Ú­‡ o²Ç×&n¨ß„�O Ï·ä,å�£™ §ïA›7„ã7ÂÛá3)·ˆ²;ã%ÁÈo†´“>¨ÝBÚý2í�pF”ØBߟ xoSµ×ØU“‚S;áI—ÌVè´N“›£Óœöp÷†§_×Äõq}Z�4—hÀJxÄMvFóY:Ãy¹Û¾»]÷7¾»éÒŽl !#ç�‰»’5>[UcÏ,Óßeî¼st úÙn91Jp¬‰ïqRÜS‰.ÄbFKíRgþh™¹BûÞûïFÇk¢ïz µ0×ê,-­]{çlŽ7Onõ¤»€ç�xW´Ã÷{/§?µψ§—5Üè²æë!ÍõÐ-<dÜašM§ }„cU_)Uô®†6¨«ÆÞÞm›Ëël.óŽÚÇÒy{p'ô.‘V¡FÄê“}û·:÷—´.Tí’û»k=úJ«²•Iö÷ñ1Ê�O³ƒYAˆOCE'Ý7ãÎqÐí-MâecÎK:>ÃY¹¤‚w”5î{“B)©ãuö§Ù»Ý‹ä¥^}šì„Cž…—©ü~(d›Î?y�Ž�˜¹ Ð3Ðñ�j`˜¨©‘ ”ÎóÞx‹Õ³KÔw=&¨eóˆTlväYgÃÔÅ <{ÔÃÞ�âÁÐ!Ž;jè+4A,Ú°dk¸Ò)W±®Ä­ÿL€%Ô?'dù#»ý«ê³;ýMxûü.ØVí`Sñh?­7™¦4Q2å‚×YT¤Eñ°M�ê~üÿ,óñ „ÿ]³úØî¹‘¥w � ž‰8ã 5²}YÜÿ/›ª]VEìŸ�ÄùH¼QÖòÀï÷%`Ì!aÞç”<’‰0GøÇYQÒ¥UÌW#û›ñ?˜ø‰\Sû7u S�7¶¥½'0rÝáÙÃN?tÓÚ~iþ�gE_¿¿42­ÀLÌç»]OxùA{ÏÚ; ü�þæšû¢n3ö ™±<¾NDÇCuý!|4¬0aÃ6²ö:�Kt[Å1›¶}Ž=¸“â�ÑV<ª@h¹ÍP€üd�[<�D×õÕÛˆŽ"c34Ñà?4©ìÚ08v�C¼¾í嵓æ˜Á‘çÈß+î^3ìé7N÷Až"yø¤¸ï�]Eû²R·•ZþüÖî[zÔ{ŸÒ`¸d•0#MòLU¯Ó¼îÔ" ’@û6Û&ެ:ße3X`©Š~Žo1š[“½—?±¯à6¥næ–'¾Ùæ·©–¼0�ë‘x;»,:��/Å[;~b&‚îöžd'ù‚Moë²ÄïbÓvæ³uq¶kuó`; uë§ÖövÁ%‘ÔuV7nÎ?]¬éø¾w8-sTú°—Œ3Ú=÷g`7ÏVÕ›µ³Y>ÅGÐ è ÐÞO3VÎM‰ÀžA€m˜(ÝW§ý3ÕûÕ¿îTï‡Õ½¾ƒ‰òõ>bÙ²ïS:Ë0•.Á}$é‹Œí¯šÕ©;þÓÌñÇèȰYÁOäR¡[îî‚*ü›ò¿«JH„ãü_è·lk«tFV„¨mÜêÁSÁX#‚¡‰Òt*¤é+æbx˜‹`˜Io•n 9,bÁ§o7þ3¬w¶éø}1,nƒ'—.˜vˆçÕqìkfÙöIàM§º‘BÒùEaÀ¼«L†�Šýn:µP³ÑŸ½y%«:[U;$€=H?¹\ØçüÙXÛ]!ß5¢ºŽ¤1¹}Ì´46Ò¶ƒh‡Xâ/œ/xÔZ ¿K�’OXÊ )“ ¾Ø‹§f5nTú½\Sí6mÛ¿o…³¸¶Âj˜ÅmnT€¥ ~,ÑM.Ùצ U‚¸!’aªÃÙ;ꞀŒiy©*ííïª÷™´.ÖüYýq wÀÝ–•Z6Ü)  Û:ðѬ!¯ª1å[»x³N·v».ØÃà(`=¸Ý„ßý}ˆyÝtÖöwYmïÊ…•Ï)àOäê-µøÓÆ ¾_Ì!››Ü L‚(ñY‘¾£ïC£¡‹6”8²fC« ž}"‚£�]o0h^¨R /68â›?ÚK%‰Rþ" 6Zu žU/G>}ÁÍ]9–v²?¾JhhŽF‰»“cÏ}éf>7˜ŽÍùš–À2ÜGc5H‚#:xðFÇ)LP#[ˆÀïZŒ0›"Wxç°¯V¿ÛT�KÍ¥Ÿ®2Lz Ãz°éënø…g³ÚQQbân½çUÕ · •^…H`ƒ(<íË<ì[בšò’f“ez�4Ôö~›Q‘iëC‚ýQ<ÔÕûQPL•n Ü[Ž\F,6`)ùk\-Z»ÊÐÞ®‹!^ù2XÏþÛul ¥Ð‘½A›ÃO¢šVJÙ½A»_#» à¶ ì‘ zéT^ý±øw¡á×0vuÃÉèw/­’^á˜*|;§;d0y«…ä“Ë…}=^›Òã¥ÚŸ˜^Œž‚=¤[)Í\äÙG•g³²Lƒ·Pa!L0è°`u€píåls†!¾à×± mF`ª<Èr«¢¶³ Ì´Ê û} s­~O±-­°@k­ôBéÞË&Ëë�4Ù´˜ëøI+=\¦fÞŒ*Û7gÁøðGÜÞïí�$À÷oÎ4RV5D˜jûxùûŸ_røæXÚ ¹»­hñVê�Ët‰ÿ"�Óäÿ65¬ îî¶kìŠhxœí]{sÜ6’ÿŸŸ§«‹¥Û‘eù.·)[V�­8w©øU‘ãGmí¦0CŒ†6‡œ¤¤IåÃ_?ðäc^’ãdÏU©X36�F÷¯@óä_¾}yöúý«§bVÏóÓäÄþ£d ÿÌU-Åd&+­êG{M==üfᆵ³:W§g¹Ô:›fYgeqþæ¹x®êY™ê“#n�œBã2]"ÙãS]Ck]gý@¬ -“) 9W�ö:íîv¾Ù;=9’ØÃý.[ÝÖ@ÿ>4^ˆ þòhoöµ˜j 1)Óža='—²:-ÇNŽðñHœŒ»ÍNŽÆ§bŸš¾ã†#~ð=:89"ú'G‹OÙ÷ëqõþ£Ò‹²Ðê�¬^€4?+/Ó²š7¹ü¬…´ª.©?-²"êà ¡A[™g@Œþβ>ot-Æ4qjîä$ëÙÞ鋯êl®ôÃW'Gj~*Šf®ªl"àÇ*»ÆŽ³bÑÔÌôÕLUJTå•Fv«Š–r*ê2æÇ6)ófÇͦ > |†¹LpžI˜D}WÄ»Êò¹m´¢§ê äBBAI“jàˆa á`ßÛÁfzx”Ç�…ŸçåJ9àvUÿvî2ƒi1:+« sQë]#Q£æòtêìR¡Á ï¢‚›iP�ê"}—Uª*÷QÏéYD1ñE•Íeµ«]ƒ0ä«,­g�öŽ¿ù·=‘w€Qfa›oîÙ6oÂAb££šÿb$C¤Ó˜4‰âÎXꋲBízeUUßq½Né¿×¡™Ò°ÚK­ ô_ÂÊÏÕ%¬…êX‹`¢øoiµØh*O0n¡ÀDÈê¼#MÊŽzÊŒÊ<·l�Š?‡€K8mæó¥Àoq:EVƒÍÏåD��pbì jý¯ª*ÙæÀ»b>…VªÀGÉ|€.�¾¥Ì½µw®�Å£Gâþg= óËÓëU©Ζ6ÍFÔ!Y âИ;SQÖpe‘Ê*Í~U)ŒêqšH1+ó”ý!³Š„,›dN‘f)š»E)4Å�Ê�J D�ƒ¦+0  q(]š0‰˜g°©ŸÀZ•`qñ8øjŒ‹§’h¢ ‰ÒKÕµJ Io‘œwHhš2E!àó$è¸l°s¶hRƒß> ¥Ù˜±É $WP•ŠlŸ™›XônŠÔ5p‘/G¤ Jš“¼î²^ãYµ4úéö®‰ÇCV9±fÙ˜¯¥u80š•ÈëƒõÆå•«â×þ JNd“@ L�L:{ö¢mç²Ù0-ÔÆQ®dËÙ´•ðt™A«©œß�rëEþ" é¸" ¸�‰qÊ1N1^@Ì8ÍêÚN‡‡ !h ×2®ÙÄö‡NUÔË…²¸-ÂavÞ[çJ“Ê`õ£R Ýå9C°þâ!:·TMe“רjI¦BÞXmÏѼ®d¡1pY3£ Œêúp BiŠ ° 9޶ŠÖswÒqY‘xg•¹V[rþæ9®¹›)Z¶´�qÍÿ°€¿ŽZáº3ýN~Ö—n¢ÄþÙ¡æÆ¦ƒ¤¥«^SšøÉ¢i=Ú}2œfгŸ©P™{*>Ö…l(»TU¨ü;³$waB2üHÔ¼ÁØHŒ9YuH5žºÁA_æÛ Ù€ddwé,Ê|Y”óLæ‹#»˜—Yºåªx娾D;¼F@‹Rg5Ä`|>«®€È˜;÷îh·/ŒÿØj.ÏaU®[ ŽO³„!´ÉRž¬–Ø`7e踴<"ð`ÃLÀîùã×Ï?!\8‡„È�c´¤G7?Sö¢ýÛ%ÿ†r!n@U‚hÂ+—Q�é;L`ÔÓ­º^ˆýÃß~kÄ¡¸üí·Ü?à¦Ý–ûÇâ/Hüã—¬‚a›‰}h�'ƒÝN˜ÄXùHÔLÑËéT«:[¤~Â�·šð˜ÚÀŒƒs:,Ô… f}d"�-u=<‹mxý˜i�­ÏqÓoŽK\ïæk@öàå³ ©–‹é Ñÿ�0ÞÄ–b®€Ï8"1ÐBH‰€µäõô ˆòG pJŠ:Œ†bž©+Ô{ õ­Ê.fõ:\��QŽëY0°°Æ¢¶’N¢¸íŠ)�8êáH‡â•8óC1lðÜW•þ¥)j1.¯ž/ öêÞTUa–櫌Ò=)¯ÏÜCÖ r �˜«uÃ"iHÞ®g…,±o™@Ô‘™G)b4¤X—tMaDŽÆƒ‘{ÍjbÕ|‘QJÁ<©›9?h»E¡Ðƒ°ž<~`é5й!ê„’&WhÛÁ\_fœCäøZù&ä›GQh¹š›Mmt„ÔË ™eöÖÊ©…Ë�Þ&~.AiaöY¹'å|ÑÔÌnÕpäÅêA*ˤ_áÐ}¶1± ÄéQ³plŒÕlãÀ(¯Ô[ÓÐ̓f¡x÷ÛY(`H¾"Ac¦�Cеf‚©Ú„ÛN3•§z8‚B#ë{U�ò[Ûˆ©–<'”Xqp9S_ü¥W7 &²§�߃QÁ#•š——ÏTåœ ¦^�‡3QŸÉÁKq%+NûPÂÇÌ˦¡*ô¾Î~üÒ w>Q>07Hs?}p ìW‚e'¢%W€ÉeÁulÄëÛbód)tȬ¿¤ž�Á�ª¥û÷=/.8}H�xØEœ pÐÚCvœ"AûS^1¢²±3ÒËȜ̛Úl´Wú†sYÏ-aU; <´ß.‰ýM½ÿ‹fKvL¤B¯)öï�ŽÿÞ‰À‹ÆsEÉM02 R_Ü 9¾w÷ëMW8u¾>Ä\'É Òëø«¢Åù¤%Á{÷6õö¯Ë\�@&7e¨¶tHAï²Â¬â*ƒ�L¬ÿkÓðvVeÅGèi%‹OioHÜÃõwÜ“‡ úPÆr–$`¥¦â »)á˦†P úãÂõä*�LÄß@%Ú95”i“;Ó­­]TÇ�Û#ÚAÁ? ’°ûV°èÊââô\¡KÐåqrd¾J„8‘bV©é£=³¡üó3Åæÿ.!ùWð µ÷NÍ|:ƒ6…¸‹‡;R°ë€NLÌûÖzå�Þ'zy층?Ð�;dàšwvð¿å'Y‡¢Œ!#)8–�wZ·Þ`ïåoˆ7•u†{LLíq¾˜ÉVn =Rçt’¨ <<“Al§04xœÅ[[o¹~ׯ`g�î‹c¡Ý(ZeÇë¶âĈ½›ÍÓ‚Që™á„äHV~}¿ÃËh$Q¶Óvd`cqx;ß¹òð²“?üüñòîËÍ[ÚªÌG“ø#ø ?•°œK®�°o²ÖÎ_ý5£ÞJ[Šü²äÆÈ¹,¸•ª¾ÓBLƾe4‡¦j¶¡ùþ”‹nÆÊÂü�¥F¢ËhÂYÍ+ñ&;ì�å“1§™þœ¤‹êѤa5½É–¯ÙÜà¿Wø´Y¨™Ø0™&g™‚ˆë;74_ÎÞÊšë ›‰BtcýØ\iO©>òfr!TæÕëÌÏq·ÌCHÑt ~“USŠJÔÖ0Î./>ݱé.ý‘pÎØ?µZÛ%3M)ÑYðbÉjLÄ"ÈY/JÁ-f²°€Ëë+ZË%k‹Y–ܲR­…vÝA·ÕÒn˜š»o-L£j#ÜG¥Œ=s«ÃÖKQ‘1i†Š3f•b¦âe‰› th¸'g Ô—Ü ¿GËJÁW€ÇD­ÚÅrÄÔÔ½r1„ªˆ19|’ìà5Hä,‘W uµ °ÎÚBÌÎFl.µ±lºa•Ð ¢@艚0žái»!¼(ZÍ‹�ãÉM‡1¸á/ià�DÑè¶Æ$gàWBºJÏ„vÔÙTó:ÈÛÐÐ¥Z30ÃgZš{’®�š™vJ˜ Vj%5- Là¸)™ Ø—äÏë ¸ÿÙ¼&IÒ˜•›ŒÏ­ÐkN¬%„ä�Š€F;:u–{©·¤‡§LOMÿ- '·ÖD©ù1si »í ·uAÃI§ÔÍ[mÅYÝ’�õè“æ0©¬àZKàW­#ãFBвD�¨¡ªH�ˆYÍe±Af£hÅÎ>0|Û54„ÎQSÞ€°á­ÇÄ0ØÓÝEÝ·;VIÓñÞsïSðg˜Pw!jÒžbµ€„†’–²œÁPßyNU[[p%k¯b7ž� %nBÍ” Bz·uzìsœ $:~’…$SvUaî?jóµUG¸¨ œùÌ[üÅyfM$™�²¹�¦« <¬.ù§uŠÚœ�ØåR)'î}öŒÜ¹Ë}M“QT5~n�ázÞ@×ÀÈIÝõÈ+C­j´tCÏ¢ãTmPY“Lð –àÛÞU¹õÓs²oIäh¤Üºàvù—ó׎3/”Q’�¹Xx^è3šÀÏ�‰ƒ /U-ú–)Y±P®Tö&%`ŽXÅìáN�ÝÌ‚V¢4>„òz'àÁd•¾GàLÎCvëŒÀY€c P¢­ŽöŒ•XíÁœÀ%U½È?vI¶éœµÓõL±�°ç“qèÈØm«µZPÐËJˆ�Ñ@S ücg¡ŽÝšx!-ÐÌÝ*†¨˜z4÷dWÞx¢Ò*$ß^sb—d_³¯­&&5YTXŒ|ˆ�:ÈS£U#´•`’à8É�ü§\ó Vʪ�=‘(Ñ´q}$`]_ܽ¿xë¢*)³'‰„\oÍir‰(ÁcH`– ø&»ÆŠŒ¸öû{Á5-#ç”XýjÖ ­�ëé‘]dÌúAòùcƒ�d»q“*ÇØ™KH®ô†$¤TËŸònöÃ%ä÷­¸©,¿é*hvLò¥‹|ê’B«ó‰�…tì;¦ýá°ËùoYþ›'6šÿn¶Á-÷ÕcþÿRÿ’å_RÔ]WVò)~(ÚÚêc/eÊò½Š.t™Â©ànj§ç0?!¢þ_ÎòXJa¢6æ\ÞEb$P°Ø¡ }ÎòÏ)û±XÈÅÒ&•Î2?`‹c²|÷;©5jˆ Û6Ò¯¸–äQÃé/¤ÿD:ì}Cy°qˆ‡‚誂 ·å'…èÖÀáw¹Í :íÀTu ë»�=´©Dc0ðW k1Û·Ót} ~ìÙ³ÕÚ÷16çW³aŒ¥ª·”{Sã�' ìs”7\ƒyäFÀ·W‘\¸ºV’¶Nk¤)ØÜ¹´d(¨ÿÚ GiDšVÉo®~ß–àŸì’Œî¾Éo –;3¸d0N1à*ù�6ènyt¥Ç×E¿›—þøÄgfÅÚ0j�D.–’‰µP@×ð ¶8¹Ãš,÷¿Gì“6’§óμuûTR[–÷¿RÀ>»]¿åóýã™Á”ØÚ.B‘½¯§VÈmhÜÚ…†S€Å #v»ª#«‘xxaÜt\éáR)é)ñTó$¦ jwfRˆ¨žÀÐy+6ß§À3 —õ?“ëZ}ú1zÇ9AntK›ê�´ÈÕ<†•R p* rÈM¡æV~ƒšcéù›ª“¨œ`¹j�Ð�ïGiø¢†¦­: ´+­).vÅ´kivïPÜüIœ˜pÝh5åS ‹Úx ½Šô¹B×LµpG-‹Ó€ý$ͽGI¥dº…úÓ@ªº¤cî°ñråã¶çNÄOã®jG¯û5Ça6]'wtx´7tmó^ÒýdWLž»‡ gÄ'É ÎEÙ,y€æÊIÙ‘Çöï̶)~ÀÙôѸ;Þ[ ÓõO,‰ayÙ?>ö”²ÙEM5Ç’Õýmñ ‚)ÚAÕ£¹Ú‹"ö Ü~ͱLîe…»Ýd$«=9ä„ cT!]�©æG8‘+Á*ÁÁ…Wߎ:5‡7Z’.ÜO1z‹É¦ „…OÑZù-#ô÷ù™Ö`F@—¿wØJ˜¹Ò´¾ó�ô²ØÈxíù<Õi•†îm¯’ž¸—Â"´T³x)uí¿þ—©þ„Gs�½K©'‡ÜÓKº™ÞC£óÿJmO¡ïn{7ƒÉÄzå ²+~‡Užå{g£]ñ; õD( _ gcYÞS(o9=?N¼ºjéÑè\–»q±ËµÇáÿØÿÏÿc|I£ ˆˆžum½nOxœÝ\Ý�ܶ×_ÁÞCí¶{>Ä@€Â9à¸.bÄ>»¹K/FÑ®ÄÝe,‰k‘º½Í_ߙᇨ�[íÙôC ±-‘CjæÇùæžÿéï^^xÿŠmLU^dçþÁ ø£†³|Ã-Ìó“Ö¬Nÿ~rÏ�4¥¸xYr­åJæÜHU_7B°m£¶¢1Rèó3;(;?sÄ–ªØ#éï.´�ÚÈ\?c3D`tvÎYÍ+ñüd<öÉo'çgÉ>�ØÏ“߀ÂSx»e9¾|~²ùž­4üw ÿ407WÅÔ‡<󛨳óåPYÂ24öül‹ô.ØûF27ªa7<³Ï yëWªôé÷'vè V·•hdÎ*nyÇ6ª,d½ffƒ_Ó¡G,T²‡�JcDÁTý$c¯x¾a¹*ÛªfRóx.¯ &p@£vôJ-µhn郞0v½‘:ë¾ ¦7 ”SU—û'Yø¤›�hÄôV�ªìŽ/K±èï[3îf!ƒŠÌ$n©V†Þмg@à–7ÿXPŠ[QjÜ‘Z°¬) l–^I½ Znœ£?Š­a;i6Ý~ý—œ�£æÃaÔ|H‚šS¨¡I¬äKøèÌ”j ´KÒõðqÒ¿Èwà �Mžh¾_à`x*ÊÒ>ȘZEƒìDmù†(A´À†-jVìăhÇ?1J3‡'�OÞ}N!Ð�Ávkóºl«wQ}X^ƒÁI¤7¢9–% Yï€ Ñ÷ÏIu«´4òV0Y±�=Lõ$©É£˜1bòkhhkc�üì‘X0­àüÐyªxYQ³áõ`ž±5l£f»�¨á¨!n¡·°¬ Õ* ü©×‰äù ¬ø«ÅaAúQI$؋߱V£bƒƒŒ…O=ò<öaw”D)*Q›ø8�Õ{fÕ1˜ØâoZA»ê{¸/+m໨¶f¿È˜ýþÿü×}Õ‚dÈPˆ{"€Ô‘�„¢êSš Ú·„Å*Á [Üd8;Tû°n+ Xì�õÜ#K”rðaÏÜêÝׯ#�MžÄNÈõ†¾æѨu°˜ŠØ.÷°ëJ ÿp|dfœÖO„È›ÃP¼I‚Á›)ðEzűcNiLê�0G‚þˆq’'÷¡.œ¬Im ”´ØÃíI8Hj€9ˆÀƒ ¡= ô¾µº­˜qcéÉŸý©U? ˆ$Ù©º#:Ém'­ü¥É< Ø´û†Ûï± ¸µ,tc7ûȱ5lð~…j…jÁÅH„“àç]ˆCÔ›AC’f_ŒÕ�÷´æ°Õ9 3¾rˆ åP¨VYÏaHe)œŽÁÏš±ÑÈ4£GpšÓžÑAz>ϱyÀXd€<­D¤GG µ—„{1¹”Ò23ÈÅ ŒëÜÄN'DÒú3f¿}Y³ CÀNjÕPôÔøª)@vÄr+•nóA¯¨íñÁÓBáàî©=ˆp¤çä35%�¨¦)�¥öº†—‘ܺy‘ažA>Ú÷ÎbùÖ/X ÂŽè`Dk€Ÿ$¸±ña´�…uЬQ£š’zâeŸ#²Ww[°\¢xˆ˜ž“Dh÷‘KÍ�Œ¬~äZk™ ¹Z 0 ˜ðžèpGîü +GráÓÀ!+¿ä=úPŒ'ho(€~¦Ì1ÚÉZuP(f …žnf•ÂN‘[ÃË߃Ëü©åe"½ú£¬_ë9øQIdÞKÞ1�/G¶ÿ‚Ö}äZ.y”A í\ÆÚ| =LQ(SÁãÑB#¢(v\»ô ˆ-c²ÚÚ É…*$[øÏËJ$»·E¿DW =#ÂÁà$’ÑœÈ(†·Á—GƒÖ´5‰iF¨ÉÜ´ šÃ �ס֌Y’Ó«d$A)× B0‡Ïü)¾Ú–Ò¼l$l&„ØÒ¾|+k؈jüü�à+ÿÔÓzË ¿ü7o®ÕGÁ)‰f-€ÖmÐ ‹N¿´õp8=»o·ÿzu­JÑ é\^µM£Ö ‹†kó;0‰ƒ‡/Êuôp¤ë® àÏͧ ¤Q…W‚úØç×à­¸§QÊuš»r”—�×û��UƒhÒìÜŠß±Ç�À•üC,ØSöWæ$ƒþïÎY_ÜåB6·ç| ‘àqýѦ#�ÄŸ€ÍÖCý ³’Ðkp¿ÛÒÌœÍÙéINë«L$•ì+TžœmzèzòO÷ÏszsFmfIõæe[]b ~6K£Re^±C)ׇ„Hˆ|aÞÕÒ Jsßi,^Ãi£—Ÿ’4TH]›i³äsXjÖG5>¿’[¿Õæâ Œ æM\VXâlV*0�~?”²ðy°|#˦>ÉÒĈŽÜLâF¥ :±‰²¾s2ðÉ%ü׌PÏEžs³9 ع{z~&ª _ŠŠBŠR¬L('5”‹$Öúe3Z—²  êU$!ÌW"à–Êø:\²PÃêݙ䒳)’JŽÔ¤‚ð|€ úŽc¼�IÙ¼#D„ Åc3âl"}õZÿHGQp˜�ñÈ$Lí³ö† w´EO·|FéaX:@B. 0«Ñ C¸glÅK�Fp§Ê7µ&xE3ú$™F§ôÎØuqgÇ®¼E{}Ø"? E xÇ"*r >¨ Ö“{!+„[!QáfH;¡þ§ÄÆp5°BÍÅ×ßkˆ! O.HGu2%§±œŒýƒ´pí.ÓÝKQ™e�Ðl‘ƒLÄ×��’sÖÁ�J& KlÂT·œ)ù%FâË»�Çê®\UhHt6:_sYkó OJTœbkL ‘‘%GKRâcMt\=<_‚²³¾€BŸk'uäÐqìe°`bFJvïK~B»T2ÇxqVä6¨L#qKk,p|NzØØ>Zä3JÓëF|æª6²nUÛ%àñ e±™tÖ 95ÑxðH !Z‰-¦‹Êšˆ¹ –R m;Ò¢§ƒ³qñæ+œqŸ“Ÿ­ÚÄCSÉ>¦8qäÃ[Û›àQ`Eó¥>z”žbà#™gÓyä…sì©‘L£ÏÙsòãtȱ—Žºè°;£-Óæ£ %ÂŽ®ôÐèÔEGô~É"?½-[R�%;!ÒQ«Y>\’êyÖœR}®ç6=D¬®M&ë—„œ‘½²Qªìì“e3Ù7*Mv$;¶!íAJ{ÊNß;ˆ¤ë=€Œ.>éÑo�º(†ásÔ×3°Ý³Ø_#á`3›Õ-¶3‰�6¶°·%¶%×ÕdÃËÐj¢�'!An€¨E#Ï»Ki?/!èñ<èq2(8j÷5š"�5ÖbŒJf¸mˆ?í/—¶]»wáC°¬“`yÀAÞû�7¼·•xïNYÒPª¸/ùRÂéË‚úË4º+Þ–Æ!�ýŽ˜Ý»žImüüÒmkÑu ¯d£M¯ÁÖGmS1F` –LUùG1½jš¹Ø8 K† GmŒ�·ÓÒø*þüýȪ�B4,;ì–|æk%Zö]ÛÚV8|×(jK¬ó5L@,oÖBûV“ˆZBñ¾£:†hp2HôhNÝièxˆmÂ(KTßéaa”íìšA-²aï3ÈЛÔúq£âRX«¢°µvÎþMƒ ²Œ¶³0•‘úã¼qT2ÉYbE'xþuN.¨Ó¶qHè2VXÝß�Ž] ”XF mìX*[RŸ9õ Úèl]ÈGãJé£BÜJˆ,ºä}µë¨�ú‚Úœ½zp|%áI`¹ £NìOh89þ.Шv½étÐÀî<ëáó¶®;Öê ø²ð—×9Ù`G§ôö°¾E¶»Ò،Ӟaªvž‹] ûà¹l^° G/Ñé:¢I�Æ¥ktäîsZܽ†¸­÷ǘ?÷ê@G:´ˆÐ¨¿9öo£2¨ªï¹Ý¢8ùG<&èƒÊ\™Aï´RJ!mT†£Ó tƬ¼˜TªM|5éN*³ÎÒØ(ÅJ–WÊwÉ4=3DøÕª!)zƒÕs/b“ºÜe «,ådÅçÕ ±æ�ÔseC?,Q;z 6å>´u¸Ó—Ò(E9À(Ù\GÒ¶˜(.ÅjR->µì‚̵˼^¸õŽïSŒR÷Y—×ó_ŒÄPv]*¼ñÅ€¶ÈØë/äºîÐ+ñ÷%.ivÂt7ÉÈų©ß1,]óq×jj×à,#±?˜ÅZ…Q÷Tx�Æ2tïЉœ0Ûñggjޝ¬°M!Ù¥ Øç‹r»áG€“Æ¥C§#7¡†0fˆ`uÙ8¨nc?³“×Àz(õ´=óY?õ}]‰Ø©eàÿšNŽlk$�±Ž—úÛw¡«¬ÝÚë?`ûÄ�‰\í/�C¿ÞòãÓs’Àë>Ò3æNüÚÏuyÖƒèº ¶ÝE³­_½ vRæÿ³”ÛãÙŽ£“3Ü�®GÚTòåá÷ßÑðäŸ/#Üž/µG'çõ¡âë7Âå#{„&§¤Çö¡®¡îNÔ7Àùð1/´V¹¤GJ`jjZIL¯p¯D°×åHl›u7ëÿWRï1(œs“e¢3à(Mq^ ³y]äÈ êÑ]¿I�Øå¾t[Ù6ÑîÊwæSûÚ;“.d�1>™º¾]òYaa»ñjïÀ„Ê_‘Cë.!u Ã_Ü›¬„§|öv÷³Éö§uºð.üx‘+?b+ÐMWuTÇpiÂN$˜^ždp7e˜�ïß›éJ}£ç®Ì6S´Yc÷ªD¿¿,ÕÎe'ô†oE\ï7êlÃoé÷ðz|b.‹8å+í•Zˆ"0ˆ´}V]nöù+5—½°)Üy*5�³˜®…ÍݵúÔº~ÿøW�z­ˆšè£/Abþ9‹ÒÓ�åâ÷H¹èÍ—ÅâÌñ ËÍ¿u៌³ �ôü÷p3onFçËnà;vÊÄ^°Ç?÷nUø»AiQÜÚôL¾á5ÞEÜa kǹ~‚Ž‚Þ‰FÇØÎ&Áí�Çø$ÄIáÙ—@ü*W�¸n`w@«š1¼½±i,í€ä„‡é_2¾#h{È’âÔÙ«¤~Ö`ÕÖtAîyÑj^ÛûŒl?Iìn�6“lŽlå~ä*Pdî×>D°"x–\j ›G|Nl ·3÷›tgögïþSÎB²hQúŒògølßED™ÃWù„ /Ð¥šžáAœWª.©êc“[–V…ŽAjçªÚ*ÐÌ ºÀÖ܈eQ$èuVÚ®&f·œ7%ñ/U±Ï‹“DV%¢ƒ˜èÅv•,SÃmX˜)ûZäË‹7 Í›)‹ó3ûU$Ĺ@€Å““0“ÎÕOà+‘);ÅúØ‚�‹+*8¹¿°ð4ûßı  t~ráÿ<ä]»õü¶ýÀE/Zo讪™°«TæSKå\¥aÄCóEŸ›¶ÉçžÞ® ô¯îÄ.iVþæpq)ð÷8­Ë®ˆÛá1¥u >ö‹üÓƒ¼p……‘«-{‚Öó#ÆVÃÉ“�JËÔ:U›·,Ž™ F4éL`Qj9zt›7¹„°rÔSé¤)5y�”$ÇC5W4ò@9¦ƒ¸Ü§\¤"M¨Í]©‹¼23TäA—š|ÂPwµ'•‡ŒkS¤Úg>�\jüÌf‚r«*±nG¦�+gÚÚsbˆr³H Œá3GµEþÍhÔyÇ4oƒõ\£á€ ÑîÍr¥VéYb>h.z%\�6ªÅh˜<(Vô8 w3aêÉÊ~ “GîAåâ�^al¢žð|Õå{¦³eŠh@¸d>T¡ü–“”6‹êVÔðÔ‚59×`Ázz?žXÌsk D¡°&Y ƒÉ+j{§>Î9|݃Šj7!¢–x4Ù´ÝÈ7�ê�©+‹f4€'âBž©CÌŽU¶–ÈI®‘¨: ãe!çÌ€-m�*ÐÊB]¯A£mR1o2¡ŒTãbÚ¹‹&€ÔN.³F: }ä.‰È0³q|ÕèëÔÅue¬HšŠª ('$@ôœ«ÏäBQå17[”ذþ3WƒÓLŠRº,fÕ6à–aÄÌ1`“ŠÕA,ËòæpÇkÀ+!'h¨ëgȼLs©†¹œ)ˆ�÷¹R¶Ùhô¨íG¨¾uÛæ™i³øjësÔ¸A >ÿ ˜Pn¥ƒÉåÝ6ôˆÑK¬Gt0z9ÒËsâÖËáXñm'¢éãy¸G€í¯´t7Í_ƒ,Ë|©’Ñ~’= aaM(ÇÂA1Çz.#¹5︀] 6)Á$¬ÔmÉ�‰”²,ÑùÃéªÒ–W@Ù�ÃFøŠ_UYPŠÓS4¡J>ì¶U¨ ü², LÏT€ g‘m _"Š—òá·0¨mdnMð r`ˆ|Ò‰2üØgaÙ�lÖHÖ˜ðL©›l IaXú�\�`ž lÖ§UoOähÔØ u+ôª[Sd‚:¤Ž“°šÈ­˜à ÞbžâÛŒ6|ɱNÃR‘å),ª±këq´ãÊxñæð‚x¦Yçk+ó¥bOÊxKo(Äz¡X«Òz‰,g‘mÛ�¤Ü [ x" {kVrÝxò¤wl …2¸¥^ê\¦Ç§J>¤Ån¿Ù‡þSC¹1¤N½²Bï4PEw8ÉÁø|VLðHâMZ³Û%á1Çù$ÏBsèÝ�Gò¯p+¶oÚ_¢Vo}ó%ˆ©ðVã8ä&¥Y\”10Ómz{Ÿbjü>ŒÐ œj ÇÔ'· ÷Üêl3Ù/h¶�‘õ ‚œ+k*©I+QjU èK0» þ@�…ÆB výÍZí¿®û׿�Pw g®�Ö¸n¡œ'N“ûè á;#�ìíùƒ©00†~>ÙÍ Ð_BPÌW›�1ÑÁ&ÅÒ¥�S;X¶&‚3ÚÖ¯øÕƯé¬`ÖrÙÄÆ¨õ)ac- òjؤîÉ3‰!èJš^ÄÍ,Í;ÚÒ²Où�éÁíM qô{î!~èûƒÇ{ˆ?÷}Æõ? êkoê9îÓhi‹¦¸tÕ+ê‹6�ŠêÒÄ*OÈý 5‡%¸ Ë“ü9ö¶™a\ó¢ 1ú…Ƴv]9dGÅ.\W—ù×eÇ5Vbt>jäù“Ib>@ý/Í1@_,[N.¾1‹Êd:o×%{þ°Oíà»—ýíW ¹»÷2wÝ@ 1•¬\råaG/D¬?›~0Îàähf ÷²Ñ6‚Ô1§}uµ·xI;/ÐÆAF8`?ÔF@˜RÚæM»v8Db© ˆè�‡’¹�m©©™`[ˆë£ó¢¦á°ÃE¸w­ð‚SlØý¼Åì7áLíkMŠ[†;IUXP«˜°F.6¡ç#ÖJ,}P;ÑÏPèhÇgþœ­`î*¨½µ';fÜ„3ãðŠ4}Øðê+Æ>öt,�°Aï¦É¼{ø0wä^ð߯úæËvÀuª¤Þ½G2iãˆîÞ�ÚBÆÖ{cÓšÖìùÝÚ®(Ϩl�70‚áåúwÓI‚†ü6 CSýNQÿPGx6ñ› XœEap%��'¢Km·’Ø>Ðq§þýïŸý7ÏËÍínðøÑ_ÿò׿ÌlÜ$졵öz‰ŠKÚí}ä†Ҽ‰œTʳ·2åÓH ø:�#Á}FÞ×z@uÕ%w-±T|ø€n NÊø°çh�d¬ãyÕ;˜ƒ:R€‰§qïd&ÍFSÅ/Êo2äcl³™TÑ_ '§Fu¶¨ñ·áï ½@}Úö:–Rÿ® šBÆäP'Dl«½ùP‰å‹hgŸfp¨&,æUE s _ç�mÈãÓ\¤Š°™Ú×fæ|$,_5ÃN*�4š=ðÂäÿ‚Ï&å¨;ƒ™¸§Œtk§ŠEøaKq+몹îÃtÄäÙw€~“u56‰_Ú4"ËÝtS¶ËX}¯+{ 'e¯d iÊ4F.f)R)Án‰zm„å6§ñÔ:6¶C¸1³k‹èP}Ö˜»¬j?ô!à•y´Õnô’ò ÊO•m;¬Wÿq’_nËÕSè(ܾ·¶ÿà;¬+ìVP½á·SOe¨œ`�¶"5¥æž®¢Wûš‰…),ÉîÕQ v=Ô&£Õ(¬^¯827õ‹·µ•uI±™Ü5oS“:�I\½¸£à¶Å}§KG >«ÈB]*Ê0¸Ä5…¢ >ì7t�µ‰%Â÷¹&s !Îj.(f×y qŽë6’ÜF‚g>)UlRðëäð¤{Œ¼:j®ÿ¸¤~² n´Úî–=q¼¸Í/øý Öþ5¼*(�À¶«ëIÝ_8ÅxÒÁÚ#^:çc§È�;FŽ"¯¨Ó �M\ŽÙ)´ÄÜc›à«if¡îSðÁ*¤¹N%ª3ò�ý¦5„.0z™³Íððž>!‹MiQ2ú—·E�q�°AC§ÆÆ1Rï°dÓÒr6–j0(üÿÀádî÷õ´ÚA·3+O_$­ÖVtóÿ¯~R�tqÓ0IýdCÒÂÇ"³ßÃõ'…ø…}Ý™ÜÂã�€õš¢°˜îŽ¨ØžQ~›úüfb^W|Ì«Œá¡7ŸÀˆ:�ŽØû›L° ëšK±É¨B’Þ8]¿�¥;­FµmlÁ§x»f¥×GpüQ,ã�=>ÙEÎýNÚ¡b5]8|x»S:|œ{LUðéÛ®\xpw_~›2ãÝ•áÑlݶ3]D[˜:4�*ÀEöH.„)6͆ö‹z]lk1§®¸±Ïí•‘Ã4[< Èb 6¹ŠuÛ£3>%=´m¯Ý’TòÅ}¬Ku'¹Ž.5ñq›«ú¤Å–�$æ|Ò�®ý!kOw_¬˜V$ù'Œç|_É‹²º.*@Ûeç]à;¾‚�9² X8¦:ª?à±M:ˆJ“÷üùXb;Y:¼¿ÛþAÔÁã§Ü�“G½cgÓ°yúÖËÒòñ ñÆ«“Î�‹}e ÷ZÖh+¦�­ÂÊû}�ÍU­Œ�#Ê6!¨œl6‰0sI|vèuS’ ROssz¶X¬Zr¹Jß>ÍÓUlC6¼;¦kÏûÊñî}…6¼€)PðÅI›ep3@“-ñTÛ.'ƒÇçbd¡±w€Æ”zÇßÏë›';BGâ5v’ËZ`H”†µÎ÷d‡§ÁÈ9b‹¼gAgŒ,œŽÎ|ÌÉõ˜Ï£ºSH¬ê;±Fs`·*kåîYðim׬ʰ´Ïµ¯j²I)w©‡EŠ+š�ùè”’‹_𚤲T|K% "ž±${ÀtÕy‡ÏÝù‚bž´u‚" ßÐé¯ î> ÖIˆuìÔ…”;·ü ©…üÝŽ>qé³�³HÁ‰£iØy ú³µHµBÖ+nòe<Ã]ákçŠrGäP(é�¹»Â>xn戊¦J–ÃåY|ðÛÔLï ÆlOD�?;È2MvÙA&Î1VP…ÚžÁü†6pdö 4c7µs_ƒÅ`ûþ0øÀ6ø(¬öY`„s”ým°!·±¾¤|ÙiG“tq±&5³&¨²Ñ ©3x¦)½ÂBz`Ãõd÷\שS»X�U^”™Lõ¯*q=”�çÀÃ]RFŸ|PBñ@ä.&ªÜ‰xñŠnqú[ÓøÁméÜþmI'[!?í›þ�Ötÿq–ã³´Èc§EÞ›5=dö÷“ÿQØÜ‰õ™ùV)ü¶)åÕ#LiƒÚС™ÃMi¦s�ÕÙ¾Û>ë´ƒ&Ü:MÁíÃ[§qN»åÉ_çpglþ�Žç@ʯêÝ©òW\²oÌ56htî­ó�YQ�ƒ{³g�g�»Ý�‰×@c,]7׆6P±€®z¯àíîýY@�„³_üK-<žO¸0$מköle¡s‘iûĉ?¸`ï‚çûßwÞÉ:T}ø·Ûö[§YîÚmµRÏ_O¹k.yGεA¾‡ÀÝioqÏÚ�¸W®õ[‰|�ŽM—µZ|îë¸ÖBìF[´¿s÷…lþóéþÊiî±ó¿*C?¥PÔÕºþ=4«ý^äèÅ„ÚZ0ðv-nYO’^ŒV•¦W‘zÌzD9)@f/îÌõ¬ØûæGÿk #N2H‡±ß³½ m?ìv¬§z¬÷l$™:5yz<Û©6Û=ÈvLäÄ|£…=!µ˜ù#œöúG¼²ß}Õ¾øæ£á¹½îÉaüö|oÊÇ»ƒ¼Oë=õ*îšî=d9ŽçùŽÝ¾ë¢Ï󜱹 Ï9>˜éÄp6ž…8ük½i:�»± C7¸ƒ?©Çe}<;®sSAé:ÅüñàýŒ7*ÙßMYyQ—§_rwGO�ܨŒ;™v‰¨v›ƒ%�ÑdäB§*Ë�^ˆ7tóîÐ/ ²“L7 Œ)Râñ9aš+^ìQ<ú= ¡e{î¹­7¯â W`ð'˜5Çé±Ò 1ksº¬�NëÖ2ü�µk Ÿòò7,$j¡s÷›Jøk“Íos9McQ„aª‘J}¦ÒU]á ÿão|K ™Ø]´º#ç«J�øøχµfˆZT`øÊÞ¶(ß™µŒÕá‚øÍO¯XB¶F­?°ë¹_ ù6óWN¦—Žç½þ©´‚ôÌkùyì/îÐÀy9¼–︬øº‚í`Y·×lÃkYIa¡f áj¾õ°èP>ÍÏã’�Ç=/$ß^Žºà›E×ÛXÑúòÖÔFë fÖ †Ô;n$W9€³“M¼—^H²¢á:÷=5ÛK'a%ø˜¦âJœ&õ§–&óx¡ï˜„þçí^Z3ÿ3fÁrníAñ!…¦‹�1òdÝ’9´ýzˆ¤@¦b…hlpwRH·‚…=Bj“ASOôjn{�ù�×5ï�cÃ^,< >æÕ­˜„%¥B¾ñ)˜<´­àj R‚�+…Fl‚Ë¡ÏÙÑ=o ±÷hîc–œx«´|ã i#¥—�V¢Î'¤Yÿ•Ê‘ν?Ù)7%**{<�%Ї®RŽ6ýXé”/³#ŠÇòê‡ÆhD›£´E¸Òì}ß@‹AæK*)ÑÑÓd+WÄ¢éÛWyöèŒÓwmý>lzÚØ4½`±ú"8øÇ€5Õ-›n¯ôã¨s Ðÿ'òç;äK ¾Ob§Žô�¨ ó™8ü&Ü^å#-͆oSøcÿIHÙƒd‡ `äçy(œ#ûl56^ i²yŠÔ[…Î`¿•çÍîao>7H{¢¸íiÓízQ;Ò¡½ï¸ˆ(Îïºó0ý+ž5oš ߦXuý}ôøóäLîG]Ñ.ÇïÚ¢«Öd؈Ím6ó‰9àÞàÿÛWwb°Wmš]µS0ý$ ¸š)hCu’öUÊDꑹÄ^2g>Wú¿•ã}I³ñûö+yë¶f~$ã.VˆÔ`W+€®Þº™¼C*_t¦Y÷í!*87·"«�~8bÒôò‹Îè\‚s¦0óð”Ãé¸6¾Ë ÝÀíʺËúáŽ§ÚØ´ŸåY·ñÁ9(ŠîrNôg±y G»×Á~DWòq}Rµ>®ØRWEñ’#?¦®ÙÒ+ÝæÒçtjµÞ˯0e$c®#‚¤´êSp}ò¦^󵬤;x}ûdêáÏT äÚˆƒhj”£÷)}gðñM«òðE›[âÌ’°±ewèüºm¸÷ëN³Wáí;e‡òŸ 4.íºÝôúð9ViõE=b´ÁÞÉ }pž~÷øJšq�O�×þ÷”`rs8þfÐhIlð¡û àã¥òðî5¹uY/­ÿu©»Ö�A-•¶0:}Ng¥ºi’(Ý×RLððãŽ\?2©š›­TižXº‹í00P|ì—§9‰b‹â‚>Ÿ@††-Â)YpÕ_L2žÇ:%¯È8xLѹþÔrÄ(»¢\é5Ô£8_é!{6á°4OÔlŠšmsO%˰‘hUHšéïcŽe!”¥g´‹^[av~‰Y¨X¾>[¥YÿuÒ*èüÖ\Öo”Ëøóû2üÊÿTžN°Enáy¶xœí=k�7’ßûWðæpëd!ÏØÞÍmÖ0ñ:Ù ~!žs,ÕMIŒû¡4»¥™üú«Éf¿¤–fÇ—x¤f‹ÅzW‘:ÿ�¼zzõãëgbUeéEtîþQ2�2UI¯diTõ䤮÷??9ƒï+]¥êâi*�Ñ ËJùU©”x¡ªU‘˜ó3�ŸYHó"¹A¸/LÃM¥cóXì‚C£s)r™©''ý�§ý¯N.ÎÏ$Nòhµ�ñ0Ç#¾1>zr²úL, ‰‹dhq�ÅùF–Åüçó3üC<çó�qçgó ñ �ý�GÎøÍùÓ§çg4ÃùÙúýÎ~5O[ó§ÌºÈ�z+Ë—@Ռ͢(³:•‹ {ÿ݇�Õ]áñ§•JS½þÂb�û­°_ldZoÎ…øº,¶BйÎey#k°E…’e¼ýœ§¿˜è�[Qfîv°x5=Ìdr3‹%`f5D%ˆÓJ‰s•ù‰eµ:¹xyýúüLe"¯3UêXÀ×¥¾á¢Xˆu©W°¢D‚ ’y‚ð¢!€`i™»…ÝŒÞ,UU—¹ºPŒÄØ>ºµ9T\�Š,@õFÌ,ÑáØ¥ÅÞO#±Qˆ-ÌGêRÆ•*…,Ky3Ãað-ì9�ËkÆð{†�ˆf´«äwcÞ6ËäAgÿÅv‹@'mÐD‹{—é²(uµÊ¾*ʧ²Rø XážÙ* _¥ÿIÒ›Ãsø°©V¸E‰¬(-i€Àf�ê …)nà6"ñØnó=u <äæ›9¶¾·®Ó4U‹æ‰Ë{žÅK÷m±‘óšÙ=Æÿ•ËBÖiÅLIS £d¯„6Ĺخ³%­hU¤ ]ÁZLå±y!¯_Ö™#�ò“ˆTmTÊŒ/ Rn5H/B›+S1 @V¢q<†àF ‘*¹ÑùrLQÔ %æÅõi]º-9åíDÞØÅ¼vÛevrĬ×﬊@=�¾P ™@å0LñßÒ©«A˜ŸŠ¶*U™Ê+±VÀV4«!ã.SϨ§‘¸ ô­Õr¨@sJÜØ‰Š&Q×*±³7ÌjpÜ)œ~-“ÞÄ÷fµ"Šx¾tÐiDøö Á>žÈjØu¯`á¹€%”¤+-Æžž(ÔÍvc¦ eQ`b ÄÓyœÖ€ÂÔ-à nÁÝ''žU!ºš!rÈ®±GÅ�“‚ôjnT¹‘¬,á™ß0·[=�[œ”ÅÔŒÆY2À Ê;¥Ö¦1ʰ Ù#õø…˜{ÅQ7Ée5}å ¦;×|)Ì/5‚µ[ìYŒœ�³¸Hë,gN#,g¨ʆ{æú=ûùS6ØÚ®° “È6ꆸ$^jç÷ái +‡=ò3 ±f®ŽÁ`ýÁ>*TZ�C@¬ÐW&mФ3�¶¼B×Äm†vàŠûBÝ€©ýÖÙω3¦2G6)/ò\-¯ 2?¼¤@Ú+â$Ôì@ÍNÇê�v‡?¬L:Y3XâÃ-æ Y´c’Êr©p-àyÍAt€æ¥ÌÁᚘ�ÈF兦ɊR€/²mªŒ‰X™‘ÑòÀ^#Ölä@š3”WéüZP0¹XÉ�šºvx?›³g�5ʽZûÄ¢þ)9îùb˜èïÑø9*×µÔ´Ž”¦²Fd ’›ÓÚóBlÁ¸¨`íÕ¶°ê+u¶T¤‘`h†'Îå¡‚7* jê¢uK^¼Ñ¿ª=û½.ŒîoöBmi¯ÃMAs0iÑÍèïïCøêÒ: ÞÊ#¨Ò-š˜@(MK5d�HS±/3}q¯‰w½<N2�¸>41`„y9°`®–üÀŠ%¹ †×d+œÊäµø¤…õL<Á.}Úx�Œ'F8&ƒ�L„0=’ à 1Û¶_§ €£]Y‚ ‚{3‘€Ãf|„‚{‚¦´lGБ� ƒÉÉh颜"£*[떟߈jã�×¹ÆìFãw61ãoÍ™P4TQÇRM0T­°öu ü:xl­fÀÅ"‹< ÉøËL›vò€,3;(À 8VÅK•�oœˆEYdDwŠÏçj�–د‚ý˜1Ê¢Ô2§Ï伊—WÏ/¿œ¼#u¾[ÂvkÍ!¥5JÓ»b¯Su­« K�è‚!ü¥V(@"�Uˆ2Á©b—jM..‡' <ŸkSõ}zt™®W²å*°)ᤄ˜Öªsœ@[°šÚÊ›/,0zØŽÃANë9àprG-ö‡Ðù)²Àž )wŠ@Y6²Â”,òô\@Eh¤)òõdgÔå&‘«÷i…Ž´@äÓ@}ui'úñÞaÎØ›8û ¼ Šô�¨�éɤ¬òb!XÎM)ïöèùJäá97ë“›7W3‘ßLç»éÀ+G—­U–zõG«:°Üä‚:a|®*cÕ}+J–…/&l+äjmÀíç; ·Ê˜Û\‹ò–„¢™à`D+�E Xx^²¦¿I³îr_üòÁ2Ù –dù錷£'I÷Íæ—ìš=6c…¥É‹´;îç9/ÿýs^ýFÿ¬«&àTxâX™0iÀD� zÃÍ•)áéÍ£b®7ÏàKéçxާ.îx¾ÉÄIP0,b×@ÆN_U§&OÏØ”å°qö˜ýºû0a­ÿÌØi�/`H`ø_—ggÈb‰–…¬Ì�Å™e7!cAÿME¡ÐH1”€Û¡J«b.9ÆnUŠß¼Ökm¦ã ›ó B¤©€QÊ⨿«Ivúrzqܾ$1‚Ê0äçvÙ+mJU¬nœqhù c {8a…2̺ۛRªÒ9ÍS†>·¢hÂÁËà Ë¿€�íZ``‚?¸¦åÜ£ü,¹ÃI€ï€ñ6˜Ý¶‡<}¬xZ‚Í-Ø’ç2ƒÈT,²*éDïÀM œ;ºöø¦ÁêðˆÛ¸âé‰sÉtÏa/+U«ì��¥—Â1BßÀBÝøC/ëhQÏ;ÓZ0Å�ptê©¬Ê P¢%XG9›ú»c…îc`´Y‚,†ë˜— E‘o`w`k)¸ŒÔGRô÷wXO¤ŸbúICÕC+ÝjšBGYãBƒL8rø(ÿ<¨cD€ÅÖõ1¥É%=L ÒNú ÃJÍY™oà²'É1ÐÍ_„ÞÀI(�ßQO<†Þ›ô»ÚöhËû4ÚÒaZõTv¸‚¿n~ºšá?Çѽï�õ´£Ûˆú=^ç,‡ß;â»_5EíhU!?VÂfŽú0mL\–°¶¾‡**Ó:„C  'À`C“÷g αc!Ø¿€y �6µ\.U–©­OIhé§nßÖ82yÞßU`8Œ²#Ã;½ùQ­(„Ø9Ñ‹×�³Ê½˜áÚ°Æñ)ÏÑr�Á$Ѿìæ8éV ÛµÃÒk¾ñôáÜ1ôéKëÍ/l ”4Î�¾¬ÀbVï,O»|°ž:s—·âÄç�ÌT+7x˜Ñ¯9åÌá§Îi.Ö¾šÄÞæ¼Ü�¸lc [JÍúúäwç¿‚gè�}ãüëxèå×~¨ÓLߠ߆A`¯/„é u©„«ÎÙÔ§É Éüq& Èû¸Ç-ªTýØxéS¦&£mÝy}1 ~ÄË:µ¹��ãXùsûýÁÜܽôüt-æî‚ÞÆb*ùê!¹pi�&Ž‘2ð@9\9ôrq�d|bê¢]ÈO­ ùôk£T¦)/� |Q*ÝL¾Ð…é<é$p>ù³T NÙ9÷&�ìÍ+c³~x:gO òcú8t˜òïþÈst!û³›¶ŠôÁ�ãU5*¿ªŽÞ‘j7¶?�JE#‹U/�z„~æ*S>Þ» U õá þÏ«ÌH2 j/T™Ã1 Y½-0Á&p ->©ÒFi*vÑêW)ŠúÒ¡“ºþÚPYÍ•SúŠæOšÖ–|!ªÄr9�Ò”£ OG¹Ç<ð�\HQ«ØÅÞâõ �8�š«…©°Ø¦ u=k9m÷”_(ÎiGkm.fþAf™Ý§˜lÐ/€ýÊ±Š �Õr†r¹\�ô²^í¯ŸHù0•§ ?ljÚG[‘ðOÿñÏ3÷îɹÈ;¦z*'ïÏt¶ÂÕmÑ´?WU‘Das[âLQÙÒ•žùýù¾%”Ž/¥ÈÒ. ¸Óžd1‚Ù‘Œ/ê)ö)ž8]Àß§Òo¾ŸÊåaTÞS´hbîí™Ýg±A<ÐeÂk3Q¬à~OÄéP ‡}Mô]_É“Œƒõv³ñy¨X<ˆQ<ËËp,ö£ŽImÖ€,ćýØY�Z“næJ¾ª dxØaX´+Á*‘–Ù*×;p­+°ÂBa2²b"ÓÂöµ’(¤�—Ì+™™ý'þ¤ï¼Ç¾Y·t�t?¼™á¡x5*ô7§ãh¯Ð= ^A¤Î«œÑHÆ�+Ð9]—ÙR  !¦2{N‰Rm^� `G8/г¥}Ûà6E„Èd’о䦞ù¨Ô+õL7ŽPã'Qª¨™`Í+`Ðz#®3QW5yfÔJ`£À%Ñ$²ŒèiÛ:ï±qÕ¤P"7â1œ9"#¥Eö{0.ñÛéf`S§ÕÉ@—…ݸûÒ#ÆT M}›\áÍóp¬+ r¨qˆ×M: î ´‡mÚ˜S"£á îUÑ]~"úX¸ß°í™À–¥ÊÙ9êöÂÚ0Ø“6m¢d²µ¥%ðfö¥Ò²¸Å{1�q»;· ²M7� ñi˜` %Ü “â á°ÌÚå$ÜN¸ئ˸ôØ¥U#¨3¦nÒNƈ6Ë%v'±Ú~[ç’K ·ÂÓ*ê öDä¹€ã%$tK¼©r�—<Ø‚£BÊ(#n`¾¼7ÈwM^ƒ]™uîñ]ŠÆêº¤îѶ¿�wØeoØÎIšÉ>ÚYšðÕƒM4zµ þI+úû,ßûÄTjÚ4ÜCƒðÐÂ}5º ;ϼ(º�*æ÷ F“UÚOú¤ÕõEÌ�«%ì€3"צ§8J¤IÝCŽÄüŽJãèq(:�Zp<ÂlÑÍ:á¸$¢ëÅÞıSº„hfÜé@2i΂kÚj=É«@Ìx+¬RRY‚–èÎÅ%‹Ã< Çõ¹��F1ª;*ŠÉw}®q�Å:¶5u°·\Þˆn¡–Iø� ßIŒØ›…ÊI²ÍßSö�°ñP-–ìØE”Ç}EÀF¾ù ¢G �hêpõïÛÆ&vÍ>�ø Û"Gçî¢ú\"±síÁ‰-d ZÁs:æriƒe~ˆt ¡âjœÔq‹œcf²† •%!@Þo6Ã|àu[¸(J¡ò©• â6‹’®@ˆL¨XT�·.ìjà-šÓŒ­TA‘عUŽ—œê+ !wÐÖU|Îå® úk4 ©¥¼s5Ôc”qobÁá`¶˜tA׊l¤¬åçÂ4âê ª à%[Ý“@ëmœ…ÃÉ\]µh”]¥ÆÛ£¡ÚAHû…Á+äó¥,>ôÜ54jËËT“ û>´rÔÐSÂão‹ HRêdï"ÔrC‰ÀÞöÒ*Êe0é¶<0¸†QÉpHd{+9j�Úâ놺oÛ(¾Fxè)Øð4Gù�ÓuýÆ � ¯g½ÎcBIÂÁáI}¿m«ª,µEK_Ij#ümÐÝ�ßä‹úe£ã‹"G«…ôž÷Tú)u ÇYý¾æ¸* ±â”–9§#Ì ºÑ�@t(JÜíjI»°áÊ×Tß H æ°ä¿6ƒê&ýRD� †¢!V©œ§è”�«žàŸX.Nå'‘†«ž1O©æŠêþÝÌÓ‚³âVrßðA?{†@òÄ‚ G.Ͱ€ñ¡¿ã°äÛfAÅ:A*6�Ô�ª \ U­’ÔúÒUIéEªaâÍwȰ%  ;1¥+AÂÁí~¹•Àv &,—ÀaaZ…ÜF�¾]e³¾TŠ�Ü1.}…jfÑ¡ÄFm²¸ŠOšH2(¾ÙdXU5ª'msÕ¤(.5`èšß¢ Ÿ{ WqP¡˜ŒUØT÷ň„¤h‘ îo »½×1FoE-ÅôØ(Œé°a}ÿý5¤Û×]Ó¤ÄW0³Ajß1 ú*Å@§uáˆæm¢ø�"Y_¹‚Á�¶‘Ô "ŽÂÎÝÙÆ¹öò¤»Òßh %LZ76eå?B��VÒ):©­}—x¨nóz3E I³8ën÷�vv#Üœ3`íç-˜@…•ÈðYDB¨ÛfžxpOÆÅ·ú ÞÀ­5ÿµMèìô¬œô,ÝieºKz—þ`™ïvX¸Xò*3“¶BÙ5 m3é+ç’}uÍÞÇá¿c0ï¼g^= 3`KT{Ø‚lœ"Éb°6™Ð±Hr\Ö2A sp –Þ!£ê 6«Æ•1ÝâÄoȹVpªXâ®3dë)¶FŽ=Êá ‡QÆÐà Å£ƒÜl,¶ “ŒÂ}Hå²¹º&»-1AÂÚ 7Ñ;ØUîêZÖÐ’ÚÒÙ›CÞf0ƒã1ƒÑ…Ôc 8ä]ñÖŠû¹[¼3¡á˜q º‹ÕˆŽ.äFçH�pqv™)�„ ö;½ÁÏHí߈"2q‡?Š©Ç76Ú,IQ:q¾`‘§½ðí÷×üu·êðC±Œë ö!¢ÚLD>´…ô³°5g;"UÕ<ŒøŒ¯åȦ�k ãÛCÙߢ£8tÎç2“0š]õg\£>Âh�+›qMò¾L@"'´f£±ú%âíȈñ–ö%í"|O'ÒÝ9j|0Y—ä:¼Xƒ†�wZÄ:+üTý· »8F…ÄTäIß,tjqp €œó(miÊöòã6¸½?{XŒ{ ïl|œø8b³Þ›k»š{ÌIÁ#®DÏw®áT‡òñ—¢Á*Ä»ðn|ÜíèÆÔ£\“n­ÐuÃðÒžy˪XØ?TÙ>5»ß/[æ¨�°ÓLø�¶í‚6Ѥ[æ owñ¾7f�º¹l»tMVâõüyÿå|¼†)33•õþqý×xÎÀ@üTg]Ïj ÕZc}çŽg[¾ÃÌ÷�ódÙ‘„øXIH—DвÿW¾/Õï¿b2×�û›p�û5õ7¸³Cïq_Æwƒé´ËÔJš¡›Áßôß fOfø]êoð3)v:½dîÞ»;;Ûs;:f@·X¦°Ó;ÇÂtì’:çw÷®÷kt”Ú£¡…Â��aB–ËKHI�e›÷.éËÚ,s¨×Iðþü0¿€:ÆI?n·Þ�mÔž3�øuv R¬’˜ÝÿvU<žî]±Û“žÚüãŽ=ÊÌ3v>açî÷5þ/WæY‚ê=Xäz—cUhøB~¿"7»ß÷BgþI��6ÿéÅHVà—]GèTç«;fîÿ‘ffÿ?pþ 'ûq ¢ pÄGxœÍ\{o¹ÿ?Ï@»•"ÛíáŠÄ6p—\‹C.iÚm½ö@i)‰ÉîRYr-+E¿{g†�å¾,­-ZÄÚ%‡Ãyñ73Ü^}õâOÏßýôæ{¶6yv“\ùOáŸ\Îk^ja®O*³œþþdÏ�4™¸y®ò _˜ç×Z.å‚©ŠR/J™Ë‚†mJµ¥‘B_ÍìœäjæhÏUºÃ•.n´�™ÚÈ…~ÊÆÑ„ÉÉgÏÅõÉÞ©O^Wù›R¤raT©On®f¸Ü¿‘æLX÷&nØ'\Ÿ¬¿fK ÿ�ÂOd*=@8OýNvìj~ÓZ`Ì™«Ù—ºa0`.J¦–0Í�KìÛTÞzVr=ýúÄNø–m”–FÞ & #V0ù–g•`z#r¹“ÅŠ™µ`Ea˜�ïfJ. ™rÃÁX¥EÊ–ª¬ß �½›MX»Éž0ön-u½sø»s˜ª"Û=qÛ™Á~F©5î5L§×æÔc(¶½B�fñECþ ¨Ròy&öix!²Œñ²ä;œ�Ž œ¡¢…Uc[Õ~%üVKêåH?~/…³¦Ê¨Â�íZ.ÖøhÇøf#x9h)ÇQõwÂl…(ÞÊUÎG):žx 57éw•ìÞOiI¶P$® ··œFq¹CQxœí\{oÜ6ÿ_Ÿ‚çÃ%1°ñž (g�œÓŠ:npÉ5)‡‚+qWŒ%j#R^oÑ3CR¢^ûŠó8 @{Wâpf8�ߌF¹øË‹_.ßüöêG–š<›Eþ‡à üÈ…á,Ny©…yvR™ÅãN¦ð½‘&³Ë"_ñØ\f\k¹�17²P/¤ŽK™KÅ•a/…I‹D_Lí‚èbêÏ‹dƒÛœÏ´�eÚÈX?a„•ÑgŠçâÙÉÎugêJ*ÁËËB,údv1å¸ûw»Eh¯„}¿ƒ…+ã‚g'é÷ŒÈÅE²‡:ž°‹[^ÎÔŰgìb>ë�ŸÏØ#º©˜¿··�^L‰úÅtÕÛþ<†�æÓr0±‹‘>ÈÓŒ]Wù\”¬X0U¨?DY°Œˆ²©ÊX e4ã†qVŠe•ñRþAÌ1“–B§E–œE–Z"o½Œ¹~üý‰ÝÀÊ×a¾Åù˜Þ`CS•JÃN‚)ÏgÄV¥HdlŠÒ^IB+»bEìJàºX+·ý «úݳ{oã ¹�<»Ò°uQe ñ…l §�æ˜T=zÆAPVå*b·%ei±·@°{7“šé”¯Dˆõ¼QÓUy+o’l¶]§B‘†í¼òKy€W¡#¸qBŠŽÑFK©á쥊³*‘jÉćŠgÒl&L}îPâ;°ëlÃxppJý¡*žÚºåY%Ä‘,ƒ-*47iÎHÑ0ÌçÅ­`¨�M´•iೋ¢,ÅV…I‘O®’ú„ÐÌu½!Ù`cT¦á7 ›uÊ éåvûY3{³Y ·�ÔËço®žÿƒ–!ß“¹Dç(ªe:`}� k¾ÎÝ Ð…CÊ %BÕ¦,ÔröZ¸¦ ˆS÷UÄDQÐÐâÙÉK8) óûPR íæ‚¿.¤‰É9Nfõ¯>',\»%€•ñë'³ñkûìŒ×v»]·�쎕6G�§”J7ÎFJ4nͽ¤˜ŒÏEùšøhxwùIòͺü¸ít îÕú&.´±_ü÷#yž1Çц)±f ³*$Åe |ŠeQJp•J£[w@ŽÎ³�OÎ�nÆe‘CìÛ�O€Cµ{ç²ö99�÷¶•Ø\€ÇÄF4µ'&�ÌEÞ"áªPr¥ä¥bŒ7ª½Á¤ÑJÔYäh“:�¤)9DŒÄ‰´Í—l¬‹¬>�Dµ„^_BÏLBTj_gy¥ ›Côb{ž×i? ލ;x‘› @"¯�›ôdöò�‘¹ÐO_]LE>ä,J3¸XÊ;¶–&%]hˆ !ÀXçúít„¸8––šê¦µGd- ¢hSTa`eǺߘ)F#¶ˆi¥6ÈþëTb‰ eÀ^¬Ø�}º3¢ u ¾ª ¨+‹9ŸK€è¬hÈ}¼ízµT±JɈ®ÐÊÚ=K!�;IÇžân%�?ô0¿UËä4! جÜXCbR“÷X@’rí¡Æ5¿öÚr¢"_HÛþf“½n~)`ï*£ƒ5ºJîHᔼC“‘Ýk!1‹Pàq4!1¯2#�¶„ï•9Ï:–¥á'˜z¥bBó$nO�‡�v½�ZÔª(îÒ€i€HÁ€8¸’ÛþÌñ«óÆйå-Â�bš“Ô‹?ÛÎqÚ“î+¢M…wϹ†©j×­×�†-€2ØÄÀFºÃx�«r¾±ÁÎðy&ÐT Ð|&e¡T€«IQ§;åŒ k1ÚŒez¾!üB  ÅÏ ƒD~ž@ˆT¢³ïiVƒ§¬ÐŸà@Ú Ý…‰"!™A[\Ù%@îÆ½ë|Ü{ p‚Ñ€«o1Œi†‘ØPÈ!¼ÙïqwwÁ²€hô`<û™±b¨¦�Å™Y¡ëfà‚{A˜W!º´TÇaZëÓo»qæ§dæÍ¼� ÿE‰C‹_yy ZÿJ˜û]ŸF¤"Ëäê©cAÕJp_�ëŽálR£¸ÉkHxïX¹aŸv;ÂÔ| w%3Pñx檇|!¹-8ä?'šOp–Ÿ‡ Fð‡�fP€_é{âÎnM@ƒöÚ#˜�œC˜•h½ó4TÜ…˜×[1+¤b©@6ݬ±ÛÁÊb­C¸�™%H²­`?Iwn«‘nQFMšàü·@£,Ÿ[–=«%‹Á>G0šEJ¶*‰P¬0m­ ‡™[Fbµ™ò[‹ä}ó�p êsTkuqW)‡(ny/þlÏëQ“�Ìëg^ó[\栀ؠԈ•îÆ¦ô¢l!D¬ZL~oõþý½Zj‰½XˆÙpžÈRb{m.´L,{@Ö›nòvÑÉÈßx’ÐA².VÖÏj܈Ù¡ø¸K0™,õ“~ %�;áAZŒ§VÈð+÷cOr…FGŸæE™@DðuNkñÁM›8€üœ—HC¦„¿)T@‰IŸ�œÿð·«©æØ/¦& ïùáïþž_aìMSc³ŠˆtÒ&mcð(ÿŸ•zXç$“„·Ò¯�.›ˆß„r\•à!—\Y/­/¥`� ªèEUºh ÕÄQl#=ºœ°×öviN›ÓÈU“¾æPÖp(ÞìÅÆŒÂb`¶kZÏ¥¦o%V±î«‰¿æJL0#—DYÖíïúÔ4°‹2’gþR½6¯{ûdÅRÖ;¸Ÿ.±uoýPñ¤„<»Ïgý\wÙÊtHT$ uÝxÐÿ™‚þÄå¢þm6'Èös ÚËP½¥?‡Kƒ•®;ç�QBdOÖËòHNÞŸ™ŽQ.º9Ç7äc“F!/¸†ži¸úŒ½‡;\åÛO‹¯[ªëçêCTGű“C4äM&Ûo^ØÚ{¤=8ÆëÛA^ëö#pj ¢Žœà¤áˆ:5¸o¨¶µ�ËÔ í{I}ˆ¥zÝÒ‡W_¸ŸMÝZÀ,îÕtp|Of¨Ý³±Ž�t\·ç->+ܬZ­² {ëå'fZ¸ÜîF}·¶¹â­gáÁL)ÕuóWÜ8¶¾‡üº”ê  ß.¹—? FO÷ËUù‡±óÑuþ±eö‘lŽ=ÃòÖo)Ù*»5¬P?ªòî"ʽkê†WKþ¨ªÚ¡žh(Å„\wžéº¢º¬U8ÙVb›J “·µ¼= ùøw~ú­DÿV¢»!�! |°àAc"¹>pš ³B ÚzNd´Âû'9¿ƒŠ)ë™w{áÒ–hàiS�~Åí…­Ñã‹7öãîË·>ØÎÿìÉRzpÁ½@&oKõËžf‚~‘{¹yêµýPÏ–�õgcíëBbG1¹ ‡!;ɘ ®†Ç<ìÙ‡5œ"ù#QXRÅ"œЍkÅÀÞèP�½*"™/,Ñ\Ø,Ücds,ÜÎÐì'éx£qº’WDKÀ/º— ©‘ˆ28:ŠS‹8áPÊ[l�*ɽä¨a�¬ÑDi•Bð ò¯�/ÜÅøJeòFPÏIãð$={Ʊ]eˆ–:%†›»#7£à¦å;m”¤9®Ö«vMý„Ãõ÷?°::©ºÇ êõÝ–UTˆí `úÆ<„ñƒ.·Úa I~l îÊÇM‡Íö#ý,ÚaÒ®áÚ>uºôüÖô”6º>Úƒr}žlÒ šÒøLÛº¡éN ®Á«ÊÍX¦ž8¾hÇï§ÖH0F�ÙJ÷ß5zc‚-óR×þ1î>1•bU”Øq×E¾†åO|�B´ðΆ²{nf;ÿIaß‘A¡…ÕOðQíQcPc+ÒˆöcÜ5ÅícÜ=ÅícÜcŠñˆI¿åêÀI¿åj0—›K³U{~ ɘÓgìªX`�±ùçÆн'†½Íý_©hØDï%už»ÔY¿FGç?iÌ ‹ÓÓ:l»‚”À ؽIsª|úo<º;†u•ÿ~Ã^=º9…îþ¼9uYÐΣ�é—å˜"}¥æ›‡6ð�ìêò|]Ûzï7ngpòqõ¿ž;††ÅžÝ��Ò�Ûè[>õùô¹©tųlv³vC$P�ß�}v’™C â•vµp¶Áª8ÁÂßuôcìõø^"гE”´D+“I´*Æ©(¦ö�—H[JþÚï;´k€ $ÔAñ½^uoA~> hŽFö…Ì„kÛAE„øç~ß”«¹:ˆ#¦éÍ]Š~�cÀGm âÑÞÌÙ·Á~‰ Š5‡‹àN¸ßÄ "¡µÑÚµšá=ÿ$«ÍÝ„^Î]º—…w&š'rÞIFêšyøNo·¿èø3ô MívølP›4•gìßZÔ–s•—xœµX[oÛ6~ׯàü°l¨' ™c ËòP´]‚-À0 { %ÚbC‘*IÛÉ¿ßw(Ѷl9QZl‡:7žï\5ùá÷›«»n¯Yî 5M&ñKð _…ðœ¥9·NøËÁÒÏG¿ Æ8÷Ò+1½2EÉS¥¸sr.Sî¥Ñ×W7W¬´¦ÖKá&ãŠ6™Œk™3“=’†ó©óàp^¦î‚u“¦d™慸e9ýMjn? nµ°n0�Œ9i|{Üâ=hz Ž’¥Dy9Èß±¹ÃßÿzÈKMöÄõ/¢Í�l2›îKžÁœÀ?—¤cÊîrÁ¼åR‹ŒÍ5S5yRerM)ÜèÝ â{ÏR¡K�Zš­¥Ï™Ñ[Q”(;"…™',šžI½øÌ½•µ5C&xš3‰xÄ:?e¸Žt[à·ð#£Õãi}�1îÓ È];;ÁؼX 6å¶C˜– 'úà&¢ØœsŸ¦>MÆ¢˜Â‘¤XÁ™CvƸÎàî-ºÖ¬+L‰;<Åiá¡'Í ïkbÂqqƼ1÷LVrë™Ôàd‡¡Gë¸ÿ[ÈEb“¥O÷%·ã¸�«thú’­¹Û¤—ycBj›P¢Ú·4 ¹æ�犙™v¬O¢ˆ„ƒdŒö k[pK¾'õèèumKÞkÚíH=’t!²Ÿ õt±$⌺�N}MÏœºŠÿºj†üåž㩱¹9yŒ'*hõèÖJcë³� ‘Ãùxêù½ ©E ZÒS¶ÝZ‘ÉÔÛ¸&KŸàíKn�®äb�—‘Å CÖ4š×!L§I‡RÕ�ýyýP@‘}ƒ_ÛYûôï1 /ò3£Š†“8p0Ç×12?xdÌ|N§Ôì“M|7õU!M.gë\XÁ02r/ÆÂtµÕª§¨�¢¾å’Ž Oïù j`£6Ðk:µ¡ïÁöjkãæFk]g¯e¯]Á±±ƒòõ&HÕ-r¡²vH`Eérž×T$³W÷ýŸÂ•F;AÔÉå» }zº)÷xæDïÚšž­¸•|¦Ä«»ª^Œ{ÑòWØa‚Ôv')<«¢Í/-¢‘ïM9?Z÷ui~E}I J„ÔÞP�@O|²kßh±í¹'‰ï•ä:'{]÷•᫽äjó,–¬“œ’â€)Çá¡(l@°úPŠ2 éO6ÏÆó¯KžY¸.�Ï"ðaŒÃÝœÓèÃñÿ§Ñ=¾ÁÇÏÕ@ªåJÀ…FÇø+Ølé“£j‡qxR׿gÃóÿvµP¨óÔÎs¨ïà)â ónÂø5Èù�1@šFZEK"5€µ•Þ£ÖÌ�­Ì c>¨?ûîÝâá·»7¬q­,³bx¯ñhÉqV5ÜXr7yïVW_åsÌ;á$• Ýv¼r É­+Qq'´z³x·(æQ +æIÑR×[¯ö‹Ò:ˆY'*ûš�QɬàLñ–nòI¹¼,æÜëýòœ1øš«ü§›¼yÅVÿ®0tPP階¬*–ç”-±eXRÌ;¯¶dß°¶—Xê…Y‹O’-{![ݲ¥PÜl™$n;cáÝ;&œeÎp¡„Z³š;~�E•µxlmíÕ«|Ø%ZzÆ´ð�UÜAÐÝÐéæL¯WŒŒÑ&c•6†*ç·‡=Lò \ÕAæ¼DË�1ó|kÙÆ¿¯(ͼdË6Ô[øõ8�|”¡V`õFà+¬3Z­Ë{"Æ¥ÕÈôyšÊÃakà§›ü1Åæïßzs óÚS‹C~€†ëáÊ¿º$®;·cî;Õ7n RT¬U´eÿy @�ã œ )g4/˽­SÇè}gtGÆá…ýòòn7á7�®—žù2œ3eáêÄ"Ÿ®ýŤäõ·!+Þ¦¤ÈËãq4%‘7È¥áü�äT”™ÃÚÿÒäEàŠÛ@ñÁhÊÜÄ.1Ž—12yð×È+yy<ž24ÒP$¼cßÑž¾ˆ0õŠ£MG)¼Oz9”¦H¬—±ïn`ödãñxÊN_ím¢¯}qÝ�÷vFºÈ¾UƒÄIõd�°,Ù¸ex7h>®´’ÛëË ~ó±÷S}Š~|þ½Àx¬é»2aù&ÔÝkæKW-V+?{XxN÷Š˜Üúâƒ*ƒæEp­ PȃrìSš’ѳ,UÜË«|M¬Ö¾Þ¢¼Öûÿïž_ìaì@û£16=ÍE>¹ìhvÅÆ§!Y�{ÌÂKÔvn«ºò [‹ú9¸æg²�V–|äåáè\^0I“¾æ=H©¢‘Û•E¼OÁ‘¾á ñs½A|ù �nìï½þÚÆ.’ …ÞC[w)®ÚxŠÄc Cç?zŸ/ùRH·#>¡nŸÁê.mjФïȺԣÓÚå¾m}f(õRGTûëÖ"¸uêZçqµÂVGë/cà½ÿƒáÊ®´ióòx|¦áЏêtR¤«Ïž¹i¸�‚˜íãÓZR\Ã]ýèmý Íè¡Þ3ŽIHò2½Œ¹"-Ûîï`þž‹s4tFO»½þ£\|"œéÅM}ßw�6îJµbtz ê÷â‘Xß… Û(Í£øÀš'Ý÷%PáJ nýÝ`Gc~$êÛ/-¹ÁêtI6´î%Øþ� œáŠHjíš‹ h¹Y •—ñy&ÑÒ"ÃÏ(Ũ^£ÖúÿŸÁ‹=72ÔV9QUO„ûwì¨pž~#œÇŸ ÿÎLl >D>ˆu� ‹xœÍXmoÛ6þ®_qS¿&¶¢À0(Ö,Ã4]Ðfö© ¤³ÅM"’N–¿;’’-O±³vP Ø’H�Ïso"•óÓ¯—wÜ^A㺶Hòᆢ¦[‡N@ÕcÑ]¤[·>ÿ>ͨßI×bq©»^Tî²Öʵ¬„“Z])‹]Ùbž¡$Ï¢²R×�¬úÛÂ:µNVö8¡„¤“\€^¤GeÓ"Ïëÿî0’Hò*¾H›7°¶ô;§¦#%•®q‚0/O),ii?-ÏzV] ¬£ è5< ø ZF¡¡µ F}I˜]ËûZgÏߤAá�€�@âe@—"-^ c$Zp ‚3B*¬=ˆd‡‚` (µ¶ŽÆQ)6za ESá 7XËŠ…XÛ3(·.¥�W¯K‹æÞƒ± <b(�תV×£Kó X²FE&mû˜€Ó^Ѹ²düÂÚèî,à˜Ø¯ Jƒ�¾§UXŒæ¬’� Gȃt�×lXÖ¢ otíM‘ V>n^3+öÖÛî@8‰ÎØ–[g´Ú‘ìÛZM–Å®€‚½1¸¾HoDÕ�Ë>½cÆRmVœš¯È¨Ž¿ßâè?ƒ} ó,‚ž§rè)æÇuO©Ï­0•8\çÐtœÇÞ\eÃ�ÓÊQ¸Ø˜¾Íëb‡åXN|ê�îÑ8J�+-nÇ^ˆô½æŠ&J_¬œ)rWÇ*ðy+¼:*½º¤¨ÜhÃqRBK™ëciaXô¿V4ˆ>u«Ýœ!½ü$–̈É"t¸û=eª�NõÏsÀýÃæLô—ƪ»’Rêw”›Æ1ÞI{ó/úÁÃ�Öaˆ@˜[/Í€ßQ|�C{#íô•B(­[ØÈW÷T­±£8Ç|ÿÉ@ÙÅ5ˆð*Ý`…wN·,¿÷Ûî.¼LÓb÷<ËcÛ•h<‘½·ïˆY؇îøl7, [jÃhé6Ò:]ŠR¶’Õ¾h‰ù€¶§=²ñÒb¿õ”‰¸&ʽ0R„ô‚Àǘ>Ðw/ÆÂX§í9Ðqh0óW�Š¿YäìûY›waý´øW×,“FVÍ~ADÚ"`»x¬¬´Ar€²t˜éÒbÚ~"€Â ˆ¾o%¹!nû#)ê°¬dB$öfÏßó…-ý°á» ­ÿi··¯û„�"«´ˆs&‰SwÇ"ZP!W_êÜÿ¼Õ–r‹¯G {  É—„Œõ† ._Ÿ™Å^r'ÌFª´÷gÀ‚_�±ÇtÝõÚ8¡*ƒ|¿sŽÓ•u²ã“9ç½E™”¯g£š% …ϱöRèOÛs4>x‰Ã�:üâ„kÆr–ÅbYøæöê‘9t¢v,@xœíZm�Û6þ®_Áó¡M‹sÖ×E‹Ôk Ý¶‡¢›¤h‚ôŠCQÐmñ"‘*Iy×ýõ�á�²d[^­íl´À.,KÃy!9Ï3kú�¯_\½úù‡oXîÊb–Lã‡à|”Âq–æÜXá.Gµ[<þ|4�ûNºBÌ®tYñÔ]ÜZ¹�)wR«o®^\±gÂå:³Ó &ÓIP8×ÙÕ2³Ä­“©}Ê(‚É”3ÅKq9ê•¿¨ŒÈdêF³é„£�'ýNFYÐýD+–¢Èå(ÿ”-,(Huv ħlºâfVð¹(¦¼d—l:Ÿ5Jç3ö‘Ñóÿ“À˜†ü÷о~<�xÓIuŒùÿmÛꟋ嵶–îý²Ç©sQ²úâ!ì‡{?|õ2ÕFœæü=†¯nˆoQß![Á7ÜQgW¼¨ÅÞ%š±`lÍ”¸a‡ä¸‘.gœ9Ã¥»Ãµ ï"!�™\ÅKûøÓ!{¶Wcà¾bV¨Ì2ÁÓœ}Ãô"é 2§™X ³fs©8|‚%̘¹Ú(Ë\.˜Åuó—kfÞgRÁ8ž°T[Ç*a(‚1ã* "4Ôß«¨$Qü‚myPÖÖ%,ç+Á¸e%Wk�+êTpÒâ'/Jˆ©`7p{!�ƒ©Ö*Nc3gƒs¢™Éí�Çxau #ñ†§¢l‰»|4{~ûýt"Êl‚%GOÀ}ׂçÇÜ,-”ƒy…9à~ `¯IµLZÓãrîü# l)WâÈ ú­7Фi_ ×èflmð×®‰'¡í†ÁÄŒ€Í’ò¢`ÿúdÏ®w|^ˆÇ »ÒÊBô˜þ^G€‘˜uFV¨Ñ›k“ Ó|µ¥‹4ÓU^Y‚Ç€ÎÀ?8'3—_Žž|þÁˆ’ìyƒÀWyGE¶î}öï8îõ(Hhâ芨Λ˺æüÔ?úÊÏ!.Þ£`\Ö‘&Ã�{Þ𫜶N«ÁX7k’ÄeBH3 ›ÒÈ ö-®·³±÷{e4ä°[³…6 (­_­ 2Ž äkŸ`áÞa„ñÉ��˲(ê`ebÁëÂ}Ì´IXKrÎíF  çb! ¤YH@Ú¡~z0zú7*"¥œÖoXÅ�$£lµ€—C€Ø !oç©…|ˆPL×jq†-l#¼0Ëâ@âaL¤Ò °¹®UkЙá‰ßªx¹1K|òct Ë”@Z Ï à˜ÝäÚ ;Oâw°F!gÓœ,&1õµ±}øÉæk_PyèVÚá÷J[‰lõ4±5e6£a¯T¢ ™{yéÀi´*a¦¸¢! ¤ÑVFDÁ™§Y–ëÂoQS¾+¸ÿÀ\FðÏ�è>ZÈÔFJÀ­–³—Bx\ƒêln%À9œåF,Õ�Xöþ³÷ñè£sߎ‘>íûÔÐ÷ �ô×kÜ“¶×s�¶MG³xE¯ŸÚFÊåLÚ”›ìe]UÚ¸×Âo—AÅóÞ‘{Ké{Ômݺ†j·C»åõN±ö VW”œ4œ­hCÂÐ}#`Pa†î€cV•R€šAÜÂrë1ƒzÒ=€˜„\ÆÃŒÀ1RFp ežF¤lÜÏ ±p˜õýôÀ £@=T·0�Øl¥¼õ6#ˆhÂ\éèh7¤ ‘—€ ßnHá†cñà ž¨ˆ©ùš½óòõ³_K:&ïxÜ—æ�+ ü —gž´œFl=oÃHWïá\f·=hò½•OŸ¹.âG C#–5”üòwïäˆjéòáxÒv¶íéP/a= œ",%²“ýš@0úµ¥Å¯¦Åt—P¼‡Z8[¦Tþ¼×*¥>4ö·Zaƒÿ×¼œg¼ ƒ0¢—°E„_WI<ü½PÅ+¨�¨­ì�UŠZ ‚w=²ð4à`a…}/róÚz(=Ib+_î9&%á´”jP2’èI�±²�|Q_O¬óíç³4È»q|è¦ãÕ¼{Ôÿ‘Èâ57›Ãê™ze§¸×× i¢þÇ™;c�ƒ‘#6VÓý€“Kõ`G‹Úe O=ÆŽ�1‰œ©c7Žb–JŸØê  Çx ó¥P8p­D´_ ®‚’ØJÃl�8áAÕV-M¯ÉPÝD¤T�“^Gsðô'?£K¶Ô²íW’«T4�âÖ êHÉ‹�‘ø𨹋mõÆJûåˆßjž€‹´y9bØëîà¹{�¶V¾·…�+®Vç,éÖÙ´Ýäb—ªV*óòÎw  ,÷y±éH½�fÔNÑþw³¢¯Y1 oðNèEá?‚ l Öl’gw¥R-ëxœíZmo¹þ¾¿‚UÑK‚ÊVàЃ# HÝ\Q4ñâ\ÒCQ¸»”–—]RGr¥è~ýÍp¸Ü]½[6ê¸8 ±½+rf8œyæŠã?üý»«÷?~ÿš®*'ɸù%x¿*á8Ë n¬p—ƒÚMϾŒà½“®“+]Íyæ®Jn­œÊŒ;©ÕkeE•–‚½®Ð¹�hp2¡©ÎW¨âùÄ:˜b�Ìì;RÌJÆœ)^‰ËÁÞ9çs#r™¹Ád<â¨ïÅ~ƒ›ñ ã Ÿ³ ‡]НÙÔ‚�Lç–|ÁÆ n&%OE9áŸì’�ÓIœNØS?D§?Ó€!Mù7==�¼–ñh~ªÿY7!h°™6ÂÒ«ÿn1ë«B”¥œ¿Ügü;ƒGw¬)�Ì}ú‚y¸Ÿ={¼¬ÅV¯LXP¸bJ,YÎ!D—Ò "• b¢ÙÒ„æärÑ,¤²g_HÌÆfºÌ-s… ÁLOY!g…°Žy÷±©6Lð¬`F/ñÓîÞ &•ŸìVsá?%'¡¨kXž Ë2®òÍMñSÇ¢Š†rW &ןÿ5‰j’�–ñ©&ˆ¾ÁWï W «‚øfͰÀ÷ 1Ls8Kµ¶NäÑ=ŒÃšP­­+¦ JÁ�ÆÏÁÅ&Í›¯Œý¥Ö/-[ ðŠcN¢˜t0²vóÚ�3ö-8èUÎÿ†ŠÞ>÷âè3&-ûóst`ÒqpG Ña½wÎüPš?ÌKÿø¡I¼p•°7z&=K¯'1ƒ[EæŒÑê ß7²§ÒÀT²Å ÄÅ(1ƒ@^ˆ8ÊŠL«œLˆ‹{áí|÷ÃÍšúMo‘ø¹Ñ)Oe)�<µ±7|6Û²5 ã°/|æ ­Ã%Ù {‡èW><]ÁS:ú·â«D¬à6DÐ5¿aã V+™�T?À«Üa.Ì�ÄmlÕ®:1‡‘~ö“–0«+Ø2{±™†ŽÃ"Ï@á\+ º0%ý»ÞFì3rΡ§T›\˜øh+?kK_8˜Yq³°rþ¹+./¾ù¼Ã”»Ž Eªè�Á!kïþú—fÞ‡›hÐÈÑ_Tß¼º¼¯ÎûøÉ›�ZO"¢¹¼7–ÔöÞyµ¯ØBdâ£@ASØtÓòJ³ÚŠ[¹˜òºôQÉË2 ®ÎI:Ú»Ïάø.µ�ÌÁàÛÛ«¶AÙu]ZI%ò€i%äoÆKˆMØáÏÌò•T3¶,$@l'nCØÒ{ˆíþ1ã 1äÉ*΢ôgt×=òñ�@@N’•±š Êâ—"lе ¶[â³õ¹ &g¡¦4 è¢ �Pî*Ùm*b. Nºò´Å§˜ÒŸçÚJD¤‹ˆµ™8¦� 9oâ¬jèÇê(!ò^W4…RuåTl8óåÔ8ô0?-@ ŒÂ}…ÖãEÞ£öð™“:9>†„Ôj6¹°˜Òjà=£ð*a ØYaÄô;C¢ùǽCû ²º!ë*| Õ ¢ë'µh²×ög"Šì>/ô«¢P8’X–ÚÚ£Y%¾3¥|Ó¥“$ñ— O?Þ³¼µ!ïÓ>}Gh/>pÓ"ïƒv�dwŸ÷Àuƒ>†B‰0œÀuóQ"IÞu-'¡5GÆNIAàXz g�3>p‚àËòjÛ‹ÁÛ¤–PToå¥üŒ°:!Îâ 6±ŽŒ#Çô¬céèY�è�¨Ô�ð�xÜ�èDQï†èçIð¡wî=$¬ä“3ð& ¨FWÞºf){ySí�jÅñ¶Eý›ºznIZ¿„�ŽVvw„ˆe*¬ÌImÎïÅÊïŒò8F ;øm­nMÐÂôŒÐE£#ÉcO±z6lâäI*ü^H®2ñ$ö±}!ôAŸ‰Ï°°c’—ñ³(ŠõlS’ÂMA•T=QÈ/5Ï €cÖNœàlZ+jö xŒ Ážétƒ±§WCv3d‡ì ÄàƒìqPwÉ‚ÎÑ4F�<ÜÑJbAnzæe™u|e/°�Êt­P‡Mö~€Ãóx ·D[|Ú‚Ž]¯ãõkjEç^Ž2bV{^ž¦W"Ÿ‰HÐJJ·ÔPså'h_qÁàŒc›ÂÖŽºA«·wøO}[p„âstßíçäâÓzE’d¯HŠîØ+ŠnB¦WÜaÈéÝÐÿÄ%wîZOiO2ðP¯ˆrNîÅŽ^±¦¡W†3ƒ†¯yĦpã[b/ v÷ú½Û¶{l £éBéZñ]¯XÚ×ÛänU±#y­>ŽöÔÉÚžcÛƒ·§ÿ÷’²üP�¤Q§VÉFÇ‘u²QvÇJYuQ·‘ù0µr§)P-oç–©—'šx¨bv ÚIE³1Ë_$î¿.жüíW»uè8° _�÷/ ð p¥ýZœFû/}w±ä”Þî1×3ÚÂ/µ¢gÝï5í±w}1˜ÿY͵qxlzÛkhݹw®t²šo¹�ÖW± Ÿ÷y¬oFˆøŠo»�vK³&ìµuà„‚[ƹñ/Ê=Â[OáÍ �ÜëÇqÍ1å¼½µ¤ áy¸¬“´gRþ¾N´¤w!©‡²ýE‡ö$v]íy[¸@z�ö0hínT÷jTSÀ¬G4´¾—à•4ë§|UéîÒ°‹Ó€) 6ƒ¶¦ÎÔ™¤2š ·áúP ëCô¨¨æn5l=“°¸�â”f Ëèn¤�CËg¿HD:Œ¨ šÚ|4ô§m�SÈÓU Öuì$Q2ÿ¼#ýÞy¬üS{þïK×I$êªÍ? “x#óσ{_3³h®Dõ MFáÏ�é<ÞÐÙ¿mïjAúâµeH‘E‘›òø=Aõbs.Z�èN÷o‹a»XŠ'v”RxœµXÛnÛ8}×WÌúaÓJŒ(°HN6{šn‘¸Xì#%�%¦¢¨%)»þû’’,_âKV‚Ø&9Î93rôÓ¯ÝOÿùú™ù85ÈúhÄSÍí 2³Ë_C7Üä8¾—¢d±¹Ï™Ö|Æcf¸, �"ÊJ%KT†£ ýú`4¬õF2YZ+ÆÚ�”6<Ö7p¼> F &ðv°WìêžL¥¢áü«Â„ÇF*=�†Ì:p½»¥Éþ5 —[¡ÛAöfšþ.é§!Õ±Lç¦ÙÍFÑø#9éT�†¥57†? šD rñJ†t5B�_šðyã›Ð—^zšag)àÕŒB´ÿ˜ÑÀtWm(Jrp‘a…,0®¯€Ôp½rŸ¾“pr)‹|yU›’ýãñ±Ã_há  ´"½#ÑѼ~7aƒo(’Î"Žx½.^89³,±Ñ¡P—’V$ͼT *oAæ•(tЬֱ$0ì;Úßâ XHñÿFžfæ<ÖÄzÇdCû6.È… ÐÙwÈ‘©‰ßLÙ:Ùd/JÞ£ o“çJ\Ô`&0ˆ¤Ô4XûRNHÝb 9|*á°Bg�?+ýo%?k*SV ·â”l g.$¼7˜Pb²'‚Æ –¦ä%â+öµ<0¯Àúb-Ù×µ#�ß¿ pÄ"žs[OÏA$õ±ôѦÒX�ÛTyä:^'Šh³?‹'@¡³AÌ(þ#„ÆmÞñðå}C–y‘Ö™«�+ÒÞÔÒŽ³‚ÈiTso·™†ŠÖ(3Ù`ÌGCcW�w/xq v üOØ~”T‰0iO¡Óêònñ¾¡}ÍÊ�zÝ9ù(²v¤­š¹ú!ÖrM ­´÷yÓ7ÒQ¹²Usk—kÈ‚3íK%¦ŠÙw4L+‘¾¡éjÞG%":Ú,~ÑzÁ>�ƒÔ�˜’ñ9ÒQI] ªþëÖ‰^¿™è"cžÓá¡ØÒ#êòÉ!hŽNúÑåR�TZÝ,�Òíº‰õcñ”NoŠš9Xp“Ùsº£n ä‚— ×;=9 š¥Ó|º�þxªNkåh ºB}ã±®{7³·ºå9Sœ‘ªCüÞ`tÿá<µòž©ìÓ7TZïè–‡–бw É¥k�7ÀZïr|J=h·©ž$ìÎvÕ�ÂÕ÷ëž¾=»ïöFÀKCw7b뺟0%2èzÇÂäØª€Ï2åÆý êš9uæ3nöF��“éçÉ,+m{`ûëÍU6qYÐtuÜ&oôBÔjm¹(>A†¾_º¾<Ã<©û 7p† †øÔKۺؙ(¹çÒVO5Éý–– &jËNêÒ²ÿ”ÿ¦ÑžÇ¿IõÙ›8:à[’}Ç|‡�aw9Ð}êÜV¡:tΑrW}e}®"MîµwUH©“PÒBN™g§ü¥i’='Éå Ó$ÛDõÕÅṎ߃{„²¹ížªj� Ê¢:išJö^Ý?àÏö’Mt.4ù ŽF{]¬o¨7µï,ü~XYæœ2ËÈn{HþùàÄCÔÆÌ]g!ªxn.m/Ó˜ AWD& LÍûlØrã]‚3Våæ}çÍ#‘mÎrÅ\´7(÷Ü1«ŠØ €Œø‘ûÊIø³sB?—þU “Ô>�ƒS)/ÚwŸ²²{œ))춃•—íÎ7è0¬¥‡þ ü?é ¹  š šŠcx•JNxœÍ™moÛ6€¿ëWp,°n@c+ �# M³¢@Ý‹‡aŸ J:[\%Q#©¸î¯ßIɲ-)¶¨KâËñáÝñŽdçß½þízù×í ËmYÄѼ}€ÈðQ‚,Í…6`/ycWç/ø Ë­´Äת¬Ej¯ aŒ\ÉTX©ª7W‹ùÌ×GóY�“¨lKRŠ�ÅVÆÊÔ¼dãý±a4¬%\ò±f<žÏIýù VFóš¥TsÉóçleðß9~Z쟪 ö�æÉ Y èzÌg5I�Û°5T E!?CÆD–I+ï�•Ø´ð#wr"ß5“÷-RiΟs/m™óHã ®Ú÷eÒ0´N@¸m°S+&"ö¦tÕ-ö€@Ÿµ£� sÁØ[Ë2&b•²LViÑdÀ,rZ-d%«5Ë„gLƒi Kß²B(SŠ¢Ý‹dÁžÂF,K€5áVJ³R|¤žµ†L¦„€&Òªdl‚ø¤±Ž€Z[a>fš4gÁR­P÷8×Ìá_D�u®¨M•ü ¨AÂRU±%¶Tƒ°„G¨ÏÓŠ ¦N¶8'š‰í ¬Óµ›«¬‡H&‡j-+¯¶’–†%^&Ú8ZK,ThbR¯—ü«´ '-È>‹˜¨|ÏÖ%¼„ VCxTkpQ1“æPzK®š¢ÀyË$’±?ª ‡ñcS¨º�s´ÚRºl ±ÓU f�ΕmÞVö Âjù©ë²É%úââjùîêÕ÷ÚüÓ¨_È/÷Œ@‹Èy0s‹ ëOêdÑC“{•*üÑøK3‚²¶ÛžÛÌ�ÕªZÇwLFaàš…¢ˆ1Œœ¹†FÎC½ ðþd ’x¶À©õ…-Dš£²?¼¡iå{YHŸ®EÉãðâC²ãœa”kÃse¡²&„ãüY¼CY›j­jÐV‚qñø¶+ 1PÔ3JD"q‰Æêxn³пXø“±†ï›òÖ¯¥ �÷>=FH48þaƒmŠA¸îµ£Ê">gGõW·!eõ¿I©bƒ¨–¤ÏIˆSc¤âàqÿkˆ¶­ïÝÞ` NW´º{?©Q—õðc5-e}È>�CH¥Éð1ë*Ðö*‰,¤ÝºÜ ¸Æ=þŒ!(ó]t>æô{SCÚu%Rßüñ1Úã>ÊoJxÜÿ‚]nÔùFlÑð];†o¥óŠ�()Ö—>å<>~—ßx?ÕƒSe»]ý©'q�.Ñò~ÎsƒÛH›÷õ< m—ûypL{ƒy}ë,ßÛÉ¡í§‚�Bmƒ¿ÒëdWùµœówØk¥±¤èçØÁâá `%tk+Ýõ›8ùÞ|ªqÙa.?™:h"º}­ÌÜî¹yo“>20»U¸�ÉEM‡Šjºe9xbàc‰SSqGR«ðAÝ&�/Ý™Ž÷�wÇÀîðОÏS€v§:Þ?à $ÄQ¿p'ª~ž4�^‰t¿VtûC¿CÐTNN½�fÿòf ´»TiXjQÚåðxÿ{PÇmeØ »ýÜ>?žúQØž†gíêAg2}Ñȼþ§±¾ØqÍ„hÈãð2¼«uU¬ Æ© ½Dê]Íœ¸ûGú�Ýt!ówYc@áÚèâ?xÅÃ&^ � �Çþ9è±ûfö ÝM`w 1~Cùø ØÊcú}<5ûvÐ e0“ÐïЩٷƒnÄ=”>Fw¯C“¸ÃÊ/tó.jÌÂÅþÌÿ·Á¿sµ¢U ŸŸ˜^y±@3îxœ½[ÿoÛºÿ]_€µ f×I€bCëKûº¡ØKW¬ÁŠah‰¶ÙJ¢žHÅñûaûîx$EÙr"çi Ä�wä}ùÜñ¨Îøñoïï¾~þÀ֦ȯ“¹ÿ#x a8K×¼ÖÂ\�4f9ýãÉ Þirqý^OÍûœk-—2åFªò/7·¬ªU%j#…žÏˆ4™Ï˅ʶ(àâZ˜ �Lõ6ˆÌI朕¼W'‡f¼úÔŸk‘ÉÔ¨ZŸ\ÏgÅ]\mwH¹úŠ¥Hwu²~Í–þMáÑ·Te‡7þÆ/wËæ‹ë¾ XŠ�=ŸU(ášÁBÔL-aš§Kh4“÷~…ž¾>¡ 7¬RZy/˜,�XÁä{ž7‚éJ¤r¹•劙µ`ec˜�c 35—%RfÜp°+k´ÈØRÕíòØ7E‹Ï_%ìn-u»Qø]ƒe§ªÌ·¯Üêg°ü!¦ êùtƒlÕ�1¢±v÷X ::å×’/rñ”ÕR‘çŒ×5ßâ|Œ'XO�ivÍç%áC�–´â^10‚§ãµp¶eªÎ€+f�tÁ2—@�éLp°¸•úDh_NE. QLœ‘zvC¼ÚEI”g‡ÌÄÎ莹õ|Ú®ÛˆóU“ófàƒŠÜ³3? œ¾6*ˆÿYÕE“ó!v¤#š8pÜ7ò-–Î$n:jÉ#tÛÂÆóòø„�>¸_~eÿa0¿Ð/½Õ6ky—†¿ú—µ€l­­­û1‰—™çiÙù‰nWBwS~›ñq¦­ùp#¸s;}ÔLò±å>(—Äô#z@—í¾ÜmÔtظ§Œ€±Ò(h­QŸpŒ¹(Â{nÖ'×}¸œÏDqÍà±–ÝjO–™x`—µž0U‚}Õ†Áº™qKÂe$‘G¥Ë`Ó‰5éàWQ+ÍNÏ'ìò,r¥’IãÉY ¼!¢?šJÕàM›57lIÊm®†Õ¢à5/Û1®¿S!?aZ18·© M-ÒÏÄ;3ÏsïÆ°4–»\á¶Œkå!€–å³n ³f¾ƒ"P3tôXˆ5<B «óêO&–KˆD_薱˓üµ\Á§„ƒ¸|GkÖµ°ø*®i¶Òj“;·ÆHUÞ„Vž‚jaÍ0vC懻µ[o£×H¡˜‚ÏæÂ`,NÛm‹{Q“­�5Ž ´wPw[0e�xÄ‹xîÇ:ÿ­6 Øü“§Rmê&5M�ú*Mtˆl9øè\´ì7`Õ¬�’l£LÑï ëJ”0#—¿‚Óó,³Çâ$bÍÐõ([—ƒØk]ÍÊ9ÑiÞdhÇÑÔêw¾5ÄÈ�xD#G<åÒÈ:Va‘ F´8YÄ J9MëCæ…µx0ÂÖC]Xl­¥ËU"³€¤mËcÔ²(ÀÄÀÜè1eÌÄèyîôC JQE#ÍSvÜ7QjC޲øÒÁ*¾Jž`KÒ€­‡îmN4 ò‡‘UNE ?L³ÜX qt`�ÄiQ< Ù B:¬!69¿M>0žìg�¡ié£Á‘ÀŸt ½nmj>›¢=Ú$�r²nTO²Û›»ŸnÞ…9ð¾°™Þn:×*ª<ñ—KرØP€ªŠ|Ÿ/xúÝfº ]t å„?K@€R´‚¢j±hdžY [QÞdö@ìŠÊgÔ�©¨!] »b$žû�ŸQúÓ †áPUát˜©Ê²ý¾C!xéÛ ¶DHBV¢x@p\fB¾Za…1nóí=7b¥j çãúß½Çìõóï³ ¶-�´�7¼A¾s‚_«<ó©ÉÁ–p؈êÐ9ú‡Ä %�EÛ°5:æ`¢Q­öá¡)";¾Þ?sD»ðH�϶;Ü´ÿAŸÜ%ŒÎ9ôî(¢™"VHléÞøÆKw+¥M KCLŒŒg$<\…]AR,kŠb»Ó�ïzDò|—x'ËÙj˜xÚ1O�eÏá³2î ©^ó šrPñ™¹Sp¸˜íÖ݆ãB¥ÀšäB) ò§ Q®$pZȲD;ÁÁ„äjÓF²Þ”å=ð(QJ?Ùx¦ì9ÜòÙ£Füg.ë¡Â°ª:ÆÖ½Çl¯öóÌ ìÕ!ƃ�vÌâ8°æƒâÎ$LüÒð|º¬á¯(Ó-šHSS†m°R3=‚a�A÷^S&Aó<…ŒÓH•æVÀ7ÔJõÄc6S[žûFü™*ŠÆ-Œ>f¼ÔLqÅ© AŒÀ¨8 Ã^:j8Eh�Aº±Y—öƒí«ZQ§kTƒÜÁ7^ñˆ‰xö46Âé^óŠÈ%OŸH§È¯=¦[°n æB/O @àš—+‘M¨SMÀ²‹•ã¯J:? ݘ¥©e·o |�)îVê´Ãá©¶€S-üizçš!uL‹]¦ø’S^kï�øTN¾….q8ŸÇÁ2ws©4±ŽÉ779ar ±¦é¨NÒI/á”z4šàÀXGwSî8i9¸».w÷…uµƒpð/;ãlçz,i%!/ϧ½è²æ9tMÖ¹%K†^“Å_ÇôHNzD;Õ·óÜ ô�móN™Xò&7“]ƒ²+v8/½ØWì[ ’í=Q<ï¼g^; ‹ð% ÎÐ�3¤õØÂyb/+­!Wï¸ jáÂȲŸ$îâgB<�EÔ©ˆ•¬± ‰LÈ¸Æ C�í]|{õ®ßÜå»�Ä+º�Ç�ø>{ïîÜbSu�³ ¨-lSe!Ø ŽáV.w˜HU•°"Ïôá{üöò¦ &¸2,Fu£¿¤W¸tjSzD3ljÄxìfYwä&…�ñ‡ŒîMðÁå-ºý�Ïs .°e³ï%TTvežQ|ŸlË% iÀ t¦FÓÅŒ»è kíø‡”‹d Øe®@`` A+VWøÁ ý¦~Ú>”ÓR¬8µéQ©x~±)­–2ÉWªô§nË+,ÓhëŸø§¶¼h/o–­÷߸—èp¹ˆ¢w#3QR¥J™j�âØ\õâCÜÕ¼ÔØ©’µº3FÌ_»Œ{ª ?è.Ì]MAPÐ…�̯)¾¸{êŒB)p Èrá?"$�!íu¨ƒæ:€"»›:„'ɳ¥Ë å¥ßÅ¿Ñî/» 2œÍŸRÆ!pú�#º«ÇÝNÇ„®ƒK °¾¯‡†m\3•­AQ¡Ÿ0b�»“Á …`Ì7|«Ñ08OÖ{¦…ã–¼ç¹ÀeÏC vA¦.ÿA|£·ØwFd€¿²Â`´O ‹KáQv.~+Üe^Õ²àõÂÃðtCL›õÕÉåùïNÈ6ÿÀëhŸÌ:&úÃkOô£Ð)ˆÇ�Ť3C¿èe+!ëJ Ó á^š˜—!ÒL““¬Ý .Ø«;½`¿ÇF;�^²FŠ¿eggö“⃠yl²¼†t˜ÂNܦNJÔxTÞá®dmh øÀy6a9¯Wp8wT€Å„õH�ßPåⳌŸ[àæ8N#Ø>NÞ}Ur ÉÌtùl  i¶ÿ_©Z®ÊG%î@3¥ò�½ÈÍ[v>açîù ÂàʼMмƒU®·E!8Òã4…øýÅsþ6÷OÚ:óh÷Ÿ^¹ÔgÂå~ ô*jfQ8I¡Áà9íi²�ÊÌîÞ³sÇÿ¢†Ã²z«YÍ7®( «_Ã6qM J’ôm$¦½-üsºœ°å¿È×7k¥ÞÀ&áL§›µ‰Õæ$YC<Øò«pÓÔx4Qí]ag«ç›O{Nvlé[-wý˜¢)üÔÚRÚ~æÔ½@´Ÿ.A…ìÞwD`=>>ŒátB í°Âë-ò{±{y;sÿÛfFÿ­ç¿o˜Ãg ÚÂÚ‰e{“N&xœÅX_oÛ6×§¸©/�XØŠàXÓÖlÆ´ëSq’Î[IHÚ™¿ýîHÉ–S9N¶ÁlQ"�w¿#ï™~óæ�ë»�ó·P¹¦Î¢thKnrE…Æ’»ŠWnqùcœp¿S®¦ìZ7îºFkÕBè”nGµ¦×¸!›&�,J“ž]®Ë�0ÿ>³Ž‰­S…ý ޲aú(Eh±¡«øuœ¥ ŠŒŽÃcš(í ‚«¸z Ë¿KþțÐ%íáLóã,sï'¦I'̳A;FÏTàÉ‚Äíü(Жj=@iìå«8Lÿ�£’=èü3±°�Q,ÈU å•”ÔZå¤W/¶Gø5ü¬ÛhMfã*Õ.£~ng¨T…H[¢Ò^@¾b]µó¢unɬ=ë€%°»GÛ£ÝÎ"øÕmW„ѵt%²Ý)FÌ+�u½§Gó•(ذfF7œb†h"° S“ò/Dhÿj+Õ͢ݎBÇàZ‚{å*Ï8hT„EÞ*O®Ò¥_®è‰ëÖðš \Y¬•®KV‹50¨Z¿€¢ß…C?˜²«|_¸õêØÊ’æ…ÑÖ^®±V̂ʑV©uF·Ëì–°¶ší5é»"vŸÊЂÝç€3qü‡½ê°â˜]ÀX l%ôé¡mgÞï¢Íã,´ÁG=ô„í~ð×ÖñŽŽY½Ìv¨·þO�Ñ1n/-ÎæÛÅ_JÔÂ܇#g²Ô•½‡ÿ[/ŽÐÏÞpÜ0ŠÝÁw7²ˆ_up}ÀHüŸ·bÓ-Gä6Œ'ŒýT ܬâìfõ8ĆðÐnÕ²Á8óÍã9€·%š’ÃßZá™Vr¼ïs4¼óŽÌ{ØõO)ôK—c‹€n4ãÔ]sôYj#Ñùg†Z"Äî)=ˆ„T‰ºÅn tÖ�A›ßÈ´ÄQ#´S¸Ãç­}„Ū-ÎdM·«®Ó†K”þåIxm =ܪtUœùæIPsöØ{¡>‡]Ë@­wïS°ý(Ô˜�ì¹/j|•rzìs£´‘ÔÇÍaÄ� óS瘫Z…yòeÖRbËs è�²û2žòäŽVhCw†3ÜB›†ýmï{ ¹§�:/�x†ç0„>”ö†¼ÿýx¾\£QR&ùÓÖyËÔ~r™�Å„‘O¥P-¿œC�?Évœ$HV>ÎÆ_Sà‡ñý]8=ê·wƒ©|hHÓýS𠔣=8lYÉP˜?§äïÏG}½¾þ·bÌýèrõ:Æ»ñ�à«›ÝñÖŸ=ЉûŸ@fÿ}ߟ¥Eƒf©Ú8 í#: A9ò9z«Ó©!S¹d¯’çà ÙX>Ó³Mf¥‰4¬ÎÆœå˜�àÍ'Üåu ¹€ûJ�¼“çí9Ê[ºÓ‘/v…ì¡+È7Þ üÙÞì7Ði¹œñ×!aÅ<Üo=�<俇°ñMˆ,ÒùÝp‘àðK¸ ŠÀ ñEnN<º­SfüÕß³<ÛVþײö–%]ÜñÐåxG–¼é¼pdU„ ’ù^îÇ õÔXkËqQžO°@!;§»ÔzÙ Øe7y4ÓKàC�æT)ó|þŠÄM{¸‘J÷\ð-®É›Mœm_'‹0d>Ân¸ª›LDIS�„ëðjY�;z z*´xœíZ�Û¸ý_Ÿ‚u�ܰY£PdAz=È&A“»öP%�m6’¨#);î§ï’’%¯í®÷6-\n-i8óf8œy"5ùÃ_Þ¾üðë»Å•Å4™´HæøS’“"[HcÉ=5nöôÏ£1î;å š¾Ôe-3÷²�Öª™Ê¤Sºúéŵ¸&·Ð¹�Œƒ\2G}©Î׬ýOSë m�Êì3q\$)*YÒóÑ>ñËÚP®27šNÆ’­|¿a+ ÍßC²K<-~3‹ñ™Î÷»÷LL–ÒL ™R1óOñ\LÒi§3�Šo½ˆNÿ.Â�¼ —ßMÆÞÂd\ßÁú?·ÍGí6Ó†Â��þ{ŠK÷`Èž,¨(T}ioà›WUV49½ªX‚zûM5ª›;™ŠwÁ’ð¨à‹6¢¢•È%2»±ªš · ñUPT¨ÿP.^ä¹rjI⊠ñ-\û.ÖÁ‡\¨JÈý ‹ðRæ.“`=WË6�¥}úÃ( >îH¡“gJr�©¬‡‡Ržôýä'ÞO€è©´ðEW^¤ônnüý䘋T²u¹óïÄT¸¥Ï²°:é<ßVx!V •-D¦+'ÕvhÂT/o…6‰¨µEú(Dª6:•©*0ïbGŠtjÉ,½ë—Bü¸$³F¯„²µ—‰ø¹ÊɈT³º\8CÐ!E©¬O¯¥,2Ï­¨´[ð­™Ñ%K¤ ÙŒã™Yåø‡ èRZàW)œüDÁ• I9mÌ9I¥G�%�“®§Í•h<6[ªò¨Xx¡‹œalcT1:È¢0‰oä›.ò@·AB©gÈ63'ËƇM„hga8µ" y ½²b­á4BB™š­ä º b´xc¡›")"”ç,\aÞ>qÐ"l¶ÔzÞpï,Ô4ÙZ¢l¤”gëp m±Æwå:{z»uܩŧ²+6Ò-FÓë'N•d¯ÞMÆTNEÕ”Èõ à�QŸÙC!ËcFË*é'¼Ÿqÿ<ÓESVøc°pj]!‘?¹É2¤�Q2-è&Ò½³â-ŠBÏá;Šþ'y r ·�‡õ•옡P­¶g‚!Œy*|*~€@WñK¹Óà/ m©ÃH "”-x!ëP ;?m¯\®$ê„r.”Ñtí¹‰�9_£¸(Fô Ï)ÙÉ2›Ñ\N0q|�sÁ°ÊšrëUf²ÂˆCpi¶•_„Š×îfM´„Hq&AÀ†šào'Þz| `PÝZgt5Ÿ¾'òÅ\go%B€‚- ÍP0¦�Ü÷tt í%–ûÀÂÕ;tWv�4Fýûøš¤©Øa¯ ÞgsYަñG`�>c´îSg‰ò§ªSg�¼ß,û\³U·ŸØ ®~=*é°ðÿÍK¿Hó‘='÷¼º¤q¨_MÜïð6ˆy�1Ê–1ú~2ýÛ �î0•’±N'bI¾(ÇŠp±¡3\èáÃP)&ß�{%Љ…e¢�ºeš¶Ý·,2"@E±¶7+Œã[Úo?ÌéEË!‡(¸Z’Œ­ßs¸jÅÐ3a㎓¹bJ�/Æžp6¦Úr Û`|3•SYau¿žõ|õÅŸ‹Z¯ÜHÓà !é·›;6„Ë–]œô[­œ 3crÉ ´18s@‘k}˜o‡®CpÓÚ 4É&6ÈnÉÎþ¬p!xd1™^A˜$ßO½ÿû†µÏìÇ2숆}Iùœöc÷�Ïn³ÐÖ°É�Ïn³Ûð9¾ÏsëÖbx¼±‡`Þ½­S¿ee¿{S߃a{_æÉo�vW'5_8.†»2«‡FôE�Œ;a;F1XÏÙ Æ©Wzá«9z60&›-€mŠdýŒnø]{‹mÜØ ؉ò¾i×ï†+? ôØБěÓJ¹…Í ÏE Ð;ØÑÿ‘80¹�N#NÁö•D|‰$¢}·=úJû•H$ÁŸãD"8vw"ñºß–‚²ß�HÜýÛöCb:#]Øc¿×åøqÕ¹oøm½Ó¸+=;wØÀöêïË"ÆþFh<~ÚE„œóŽ å›· â±Ö¾þwçá{ôî| ¶ÇïÎû“ò‹HÈ,£ÚÅ@Î4 ñÆ9�}öÒk©Œ}vóH§ÇF_Ãü_›jÀF…¥‚2·Éw„á}S×ÚpT¼jŸhCM¥ª2mݯõG·4“Má†48U.–JV Ç ÕFz¾Cõ)2F›bôóH•S²8d“Ï6é€>í¢ñ §/ò[#sƒª– Ä.�‡L°#s›íƒÛxŒÓ;²$ÔîDÀ' ÇÂPNYþ0µAºpft–ðÜKaí ‡•CbË<&P;#;ÂqÑjBkôß³xþ°®oìFE|x<`y؈S®qÄ_íàQË‚d'1D„ÏúLî¿>ü�™õN€$ºßå•6Ÿ,âF·ßaxõñmXD¤QÝÝϵ¡Þ§`Wÿ8~6:§þŒ£°  E|ˆt~‘l xœÍXßoÛ6~×_Á©¯I„®(0 Š€ÎëC�& –EŸ J<[l%R#)gÞ_¿;R?¬Tµ�­˜ Ø’H�ßw<Þ‘LúýÃêáÓÝ[VººÊ¢´øªÁqV”ÜXp×qëÖ—¿Ä Ö;é*ÈVºnxáV·V®eÁ�ÔêZë[p�Ú|M“ ¥I§1×bGú_fÖ¡¼u²°¿²S4a—(åLñ®ããâ,M8�ôóI8Q,JV�Ìu\¾fk‹¿K,:ÔThÀi~’ÖAø¾iÒ�þ¬gʔė— ãZ¢ .ä¶TÛË×qÐðP €NAàƒ&릂”³ g5tŽØŒÅ~L¯™CÕ߆éü î‚=–²(YÁk I: ØF+ h3m°ï#=§µRm¼Ö¸“ïQ¡w•Z\°¼uLh쬴cÖi‘ïä —Š4�²«h°á›üa+²G©˜$¶¶æU…¦d#¶n«ª·æq3…®ä{x¥*ªV@P7EÌØ;‡P•mk®§ †Z›©Pž¢ß« f@«¢Òˆ5`Ð’5sÜ~µaD¶ÒÀ·–¹`¶Å á–F[{¹å•žÄžÍV¸´Äs]'Ì:ËwãlF�’n¦ÓÉкüt“õ{@SëŒV›ìme5®µ¤«ŠÃPXcø¾ê+Ša/††ƒÀxvÁö‡ºáE)|~Ü�уþµtú]œu!Úx .Ü>ò(Gë­‹/å«l$pt>7F£8 ÖgwC�†J_Q,æ¹�°Îd©]¬úü8Þåê¶­ïÂZÖÆÆÙ¤ uQ�árŠ ë>dx9jLöÿ‹}@z‹™Ä’Q÷˳è©ažm¹‘d÷ÅXüÑE\Âgû¥9}û€Û§Ñ¥°û¶Îúã÷AËûÈŽŸ2„Mó”ÏRTî妿qæ_sç¼ƕàF0[é5-æ;7mœÝ´‡áÖÀ—øžïÀÜË¿ÉAÆï9À¾�„鎒­Ât‚UÔm1o '·a’ãl¯0GalFªðr~UJ!@-ÌäCëšÖùI�ÆÙlõ‰ì<9ír‹ºÙG�›ÒõŽÖ•æ˜øô�žõDÈéwóž·(¥ß$·ÃÒ …C„r”8O6wFjC_óá 0+ëœç²’n7Ððùb±ü†;í�6ØVíï’f«çh½SØÂ%»bìwÛ§·5˜È@<ÝFÍ×LêDºnç´·Zi:ôÓs>ÕúZÚéys±=H�'æƒùšŽŒ¸™”ç( �3�ùÉÚ’²ÉL$ý�ã™§™p<ì�27¡ô#Ï1ûœb·ÎãâñZbf²CÿÝx§ÑhI(øÐÃÊDw¯:¦wÑü¥ÄÚè�ÉÇQöäâ\ϵPÍÍFb¶ïöÈyAZüØ ˜e8�Øà�‰ž'ð!±³fSi‹±—ž'°!±³fcùüúˆ³ásv×��ÿzõ ñ+épçý¯±pµ‹F}—ÀxœÕYmoÛ6þ®_qó€vœ+P`H]¶Ò¤X³—b J:[\(Ri»þ÷»#)YŽ“ØŽÝ$8¢îå¹ãÝCé4üêÇ˳«�ï‚•j” ›9ý”èd…¨-ºÓÞÔ��¾ï hÝI§ptfÊJdîL kåXfÂI£/„œáb�Þ¡+Ln‡ƒ ž Ñljò;ùnd)Y'3{[›#½d(@‹O{´Ž«s™¹Þh8ìóÕ&Ø�ùyE d,xÚ+^ÃØ’™ÌäC?�áLÔ#%RTÃÿ §0LG­étßx“þúAåϳpùípà Õú;¢K·5˜¿n¢‰Îlfj\YÉŒuaáï=� âY€Æ9ä‚*i.]TP/m A„°¹%©ªã$(çrÖ\Ú£×½`ïª@°´çáV(8Z÷ê d,0®M s3U9LÈʼnñáƒ' �’ø¹ÂÌasß‹TôÖÒÔ$(²"¸êƒÐ¹ht(¥½ƒÍ4zM „ú-‹Z©'SIÖ¥{-«Šœ7–Mj±žEKÑ0Ýá%P£uÇ¿ék/vþe©_Æ´CN}TËtÊ&ú0/$ÁÏÑf´Fùe�yaÛ2sUgÈ‘­¸n° ½ €¦ÚA!l'/ —M"€ qÑø÷‘ˆëèËÇÀéP¢žxŠžô—p8‹A°¥NªxƒÛZ)ÅR¤jq"UÈŠÆ"P¡I¾¶ jÚuᲂ²äLÈR›aÛ)Š90–Î…l¦ Ï­6ޝ):ÉÁŸ$$€74Yf©MWQ\|A1ô½<�FÌ-›L¨þtP!û$;òP?‚À«)mBîSLu8㢵À $F-\×Ù{¼ �R“¬uÈXÒH½…0`Ùe¿ÆêÝT²rõq3~I0Wé*Yþж2Úâ ¬šÞä]ìÛ°S°Ä5ÜtØýdÛÝß‹BYÓiȵÝߟIà×½úó¸!�˶mIXžb½`8=_Ì=†<Øp@솮E°îm%5É27ÄÿÌ�-Îî®ð1GÉC+ó` lëw{é�ü�ùw`?w`·‚ͧbŽ; ¼(P)Y½é7Çñ('…³Í™áÌ Îs¢±AÜDblg•ž5Oy¸Ï”¥¶Áö´u˹´#m)cí´Åâ‡xéëV~°ùT´µ ”½yâ ž¦î€Ñ!v¾­ÛÄÿ$¿g±Ñí»®Ú¨x¢œ“úÏÓå[óÍí‡ ˜Š6/1 ÷{‡^:ïaÏ™S}:Ÿ)§nƒíõܧ̤ډ@'Õ­úÀÞ¬V›“�obÒµn<7˜êÌèÜבPTˆ&©TÒ- Gmù׌·}- ÖoÀclÛá¢]tÓZ[?9ó€ ë¤;ƒ £(2Iuኒ±Ig[¨Óv $” —³NüX‚ŠC-g0\£~”5÷tA ÔVlÐd¨¶Þò»á5æjANçTâ×ìÍéвBy¾a ‹’×ܯ WðHC:ßÑ®RûÒí'Ë�O˜±ˆÌ1�ÐîO´çˆØçQFA¼?É^£ 8 Jö-'AÉ¡FAVÌЇ´C_·:jîtÔ±xgG�¥B}דÇ>� Jܦ�d3·MS·˜v¬fC‡³P3:É–Ð@jªYj´ËÌqH©Ô¢^{ë‡RöEJ&ÃH—gÇfa<,õmبȔ¡² ãvzmúL™´ ³SiƧ ·¨ÖæêÝîvW¢¶Nº©CžH[lO?‘Ç= ™áqSíÉÊßäÉÿЇd%!þµýwjÞXb " Êv©Áºš}@ô¬@µ4ˆK õ@Qãø´÷˧ËcþŽõu ©×E'(A]ñw´w”ˆOç(jÍ�åu©Í2�öFáw]kSÓx#„z›š%´h·ÏñÛÙ |¨û¼c�<‰8|’tˆxœ½™ÝoÛ6ÀßõWÜü0l€c �b`K»—-[°(úHIçˆ�$j$e×ûëw$õi)QäH8¦xwÔýt_²ÿÍû?ïöŸ>@¬ÓdçùÕ²ˆ>RÔ Â˜I…úvUèÃæÇÕ–Ö5× îîDš³Pß%L)~à!Ó\d0~Ä_ØäRä(5Gåo�„çoKÍ�ˆÎÆÎ;¥INiªw0E#‰z>ƒŒ¥x»¼yO$ ·œ¢Zíü-3x3v}Y²ý†Ds�Èí*~ EúªIq(¢Q缫îå ~°0Э"›c;x�ñP Qk»ò܆ˆ«ó¤jóvåd~†“˜”ì â`YÒ©PÂ�*'®cfZž�ŽD'+ ­�wyöè�ޱc\kŒ@ àú`sÕÜý/ óFdÉù¦<ß–8Ø}1�Ð}1?£ó9)²ßïÉgvʹ*$“�3k«5œbÌìbJ¶ €È"&#þ*ëùt ´˜æt4A òÄ.áõ¿ùcÊ&8ÞîŸß÷¥ÚçÜ_9 "Ì_] 餕¼–xyZ«3˜'›¿ì~ Ƭ†")Òì"±y‹$¢Ôæ�×V[y Y/AòŽi|’.$¿ã“){²óó0ÑG箘‚bÜ6"­è¹à` jƒ2Ï ÏB ‰=€gN`8 ¡›úF"²ŒÒK®Þ`C™a2�¦˜a¥·ÏÍ]• rƒqê¡ oNë"\K`q4zþqŠ+å×EBὌÅ,u¬Ès!õ”Jæ$¨e•âÐPnï�üXÃpÂO2øVª ñ“¢ê—)®Ï_›ÉGéx»~¥ÚÐÈ?'³{ŒGV¤H©RF…ìË•õ¨ÉU¶éÈD+�Q“BòÑ’ß#ïõÅÉ\˜: 5B ”£–î>3{´ªQYÁ]Ç6ÖÖÅ^vM]Ù†ŸXÝ ˆl Ü5‚BFeoÅ37p3œ`K„ЃäBNc÷ÏϤTûŽÜ\6 xÂ�C^@&`Ü\:ÔXÉ âàAy­Å.¬)#¦®T £éÚˆjJ1Ûˆ«µïð{®ÂŽÊ.J�x›Ê“ØËZô4cåKRò_þ¾´gž]ÛèѺuyiôl¼Ë2�Byt0/ìcàcZ›g&ós‹)éÊÌ$<¼å³ÝrmÑ…Ľ¤�›ªY:¥öwh.ô÷9Ú «-VÛXpñ&§Ý—U]°O™pi¿Åòeè”–w[þ´u?8ýžÒGh ïï’¤ +>xœ½ZmoÛ¶þ®_Ákà.)ÇM€b­cÜݬŠuY±wè§�–h‹-Ej$eÇûõ;‡õbÉŽåVZÄ&y^xžóFÒóýôëýÇOÞ’ÔfbÍÃFø“1KIœRm˜½›v5ýÏdã–[Á÷*Ëilï5†¯xL-Wò�šŠf·J!¹V9Ó–33Ÿy¢h>+™/U²CQ7 c�ÔX›×d S Žæ”Hš±»Éó´×EöA³„ÇVi3YÌgU¸=a/mR�| ”9‰‘ân’¾"+ÿ¦ðÕßX%§èuØÌŽÌ—‹= KPÏñ™Ïr”µ °`É4Q+ ë"?›ðMÐ%3ÓWOð#É•á–oáÒ²5o¨(19‹ùjÇåšØ”ÙÃ(p."VS.qeB-O …a Y)]Ï8ncðÇ£”�æâ:"Snê]Âg ðO•»ëRõè> ÅÊJ@1Æ6í(8î‹è'Zæ\4§KÁž4fBª5Ý!='¨†¸2�Ú>²A~©¤Eµ¸k…uT³v¢t\áË6åqŠC;BóœQ}Ð1«o�öoÌäJ†F†u“r¤Ûº8‡ùÊÜn×Ï!¼‡i¤QÝ0‡O‹Oóþ)üÜÜ¡WÓ�F“ý‘�s"YHo] GGÖ±WHþgÁ|B5§rUFSJ�!ƒXbø»Ë1ݸ[`�.R­a7@b*AT*wAË•BmCè:ѯË}Xÿ_�û㉖Ã*¿²šsË3ïÕp%Mc*ÓcÙ�Ê w|v ž¯�’�S3|Dà^2ˆ*Œ"KFÖ|â(z«±º@þh:õÂDb¿ÔGÏ6êhǺ4úw ÿ*èÎôå{ 6Ö[ð…·š[ ºpé¶Cmcv•6DKƒ þ zKµaPš• D!\',ðºé‹·pšFèòJÈÝ~ÚB�eä`:÷dÆ6 ·ºÇ¸¸ » ¬®’+àqåoSsü„ÿŒ ö„_äT²5Å ÁÈõ/¦!% uÐJ §k%Cqqì±F]¹õúöŒw½~ð�\…«ªùAúÖpï-O¶À6ðJT±¬VœŸhc¥ÙGM¥�sh6ðA¥E;ÎËÊžˆnª&{®Úy‰dv¼=ºËîðÆŠ[ ˆ ~áV½G;û•bÌ$qWi@¼f�3þþ¦ÚÇ|R÷J‡o/à~^uŠw_�÷:ÏνBÊ)ß ÛÜɹ»ø½"Ü;5^æNcóߊ�7F›Où�&üÛ)Ó¶ã~A‡ŽÝºWÊeÿeD–vÊ7'`ÐŒgXˆEûêdƒFáÆÙXé¸î@Ëþ,ø† 8Z\w=Äb±Ÿ–½ýè-n¬µ€øe��yŽ¡ê¾-]Öª¾šÌÑâ¢ÚÌsÍ3ªw(VÃÿ#Þ¦w“Û—ÿžxlþ�¯Î�ù̦ÍE?¼ ‹~b&ñþ­^:³þ“ÿ–“�´%ø›IŸU„Zs{QEšMšË½¬ýÚxC®gäò†|�ý¹œÞ’ï,‡ÿ†<½xáê¦çƒŠS€ËÍÒ�„\>•*L‡Š4�‹Þ^y�ÌúÃ…vucçÛ„P…ñ}E¯$}¿ 2õ�¯¸ÊQ°JWµ æ_ž¦à7€c˜Eð04LÞ“ï�.¡ÔÙVB~q"PÝ,·»VªákyTb·œÞ88ŸÈw¾!/¯ÈËòû~©&×öM„ðžlr³Ë2‰#f�F˜�ßßœ#ðëÜ?ª»ÐÁî?½¨ê�pÛ „^CÍ\®Z¨ª•š•¿Y�ùŸÉþ ¯‰ü. ¿‰:�’x’xœÅX_oÜ6 ÷§ÐÜ×䌭(0 Ž�6í€b½.h‚{d›wÖbK†$'Ë>ýHIöý‰îrEq.�Ø'J"¤HŠtþÓû?¯ïþºùÀÛµE’�/à5¾:°œU ×ìU:ØÕå¯i†t+l ŵêz^Ùë–#V¢âV(yûu™g~>ɳÀ§Tõqý¹0W+*ó;¼&9g’wp•Z–yƉë/G�àd’÷¬¢™«´yÃVÿ.qhq¥jØ�”—Gx•(Ðíȳž¸£ìvè{¥-û •Uš-yÕ ^êÄ#ñÛjñ0ÂéÌå›Ôsºk€y8‡å»i¿—‰®o¡i à 0@\ÌÔŠñä%P ™*ÿÁ9¶ÂY%áÒsÆ�}T~�°^«™ ö؈ªa—HƒZà> ¦WÒ€q$<²š£¿ FÈ5³“BayÀŸ0tªFÕ‹d²âÛ5pÅ!•™ñ'Hu*wAå··£”ìŒ}´¬V¨ŽT(EVíPé‘0«¹�¤•SõZKC!IjÇÛ­¹eX#þƒm‹•€V�šì„úó{ÚÌ‚Ð~Zu“/X9X‡�Ìj¹¹7¨râ†UZÑ©<ðVÔ¿GM2”lŸHP¥�[’E<ù J{Ó$¬|zvlÁÂ{Ævx¹±ZÉuq ÀxkÆPH cÄ�†ñ>€ešW‘ Ž@¥(wò2 ž1â¥%ïÞ¼.6¢¸ÑßèË=h+À8áiq3H²zM¹�—.wY]ä¶9â›™¿:´pñyènüÑ+mÒbgèa„CÂñ”««ÑcÜ:šÌâùpN¨>c6d¬íq)MìEGÕ‚ì9 â?@Kho¸Fȸû”êÍ,A'ÿ¿w»Øj�.<ç€~�!»V)í¶sDÉ1%>Jœ„Iƒj³of¯ùðoÏe õ¾÷ÄéG½ˆÙ€§ìG—°vDêæÐëK¸ë]Zl�b:Œó“ÿ»RfG"J°ùæ÷Q;»{ âeFÖûØçI6BiÊ1øŠ)M¸b„—¢Äqòuã*ªéÃHtÕ ³[øÍíV¬;žî7à˜’±Ü•5×5«áA¸ý³Øn9¤År8­>˜·mß µÜ+éŽê=Ì.[%]¥`…,„/:Îñ¶^iAÏÀO�ë­‹vvx‚c†¡g Ñ^©³¤æÐçø6ç/¡Edb èØ#…†Áw@mX>3ô}Ô'žd¥4`DHƒýH‡ wÆÂÇO:S!åú™ÝÔÈ 1ÛQ# ð“jzß�ŒýÒ�¾¿šßf{Ø25fz̤û§%ÇìôÞ/tטÙ=Xßж>ÌQ–¯{ßqÞ§©2JºéÚ#�ßõ´iù{%èK>ÔT{â ½i)ãß"’}oðÅ]è\a ­èùmÑq½2-üûˆ%&øn¡ÿòâ×—óão•Ao¤g¼\éúÁ£y‡˜ûÏxíýpËC½Æ*›ž'X�–]øÞ¸Ü) ŸÍù¡¯„½ÁB|A»=Š©ò»°­Ý˘>52 ©~d±[Ÿ_ÃÀÅdZL?£·N~cÄNi> ‡3ÿéùk#�=� ~-1xœíZm�Û¸þ®_Áº@М=à€CÖ1�¦=à€¼𽶇¢hil³¡H…¤ìøßw†CÉ’×/òîf7Ų¶IÎ<3Î<|™þá¯ï^]ÿöËßÄ*”z–M›? üSB�"_Iç!¼ÕañôÇÑ*h˜½²e%óðJKïÕBå2(kÞBí¤~ acÝGñÂÊ~:áÙt’$Ïm±%=žù€ã|P¹..‘ˆC³©F–ðbt~à³ÊA¡ò0šM'’4?3µ}�c*‘SߣÕbáQRn‹!nx.¦kéfZÎAO'ôQ¼Óù¬•>Ÿ‰?Å.vþ_î0æ!ÿzÅ_¿›N¢®é¤º�ÿ=ůá<ÿÞ”ôùÜ:à_þsGŒ3‘l…��($†Re• ^àV .XZ§À‹Ú+³aØ“`⟈3c;[#D‰òµX8[ 9$X*äáYÆ� µnWú§?Œ%;íÀ%ËZ-„ÚÍX£Vë„]d"Êô�0(ĨzÚ�¯¬)Èxep˜È9}ã�{²=ÿ$xÔ…‚q É=ï–ŒÇ%CZÃ7Õ”´še{í¢¬}s@× ´Æ7ÑëYg:P|GEcêNúÊv²dX�fožU‚¿úe:�r&L]‚S¹ÀF§>‹� «h¾Ç|@�s`ÿ/@â¼ _oL‚¤?0iº¬ó(ä`d²ë›Éê9‘Œ›�;ñ5pÁ]sR{›µ‘·/p,6+•¯Ð4¤J¡¹ Äè û£•.ÃÕéúã³rv.çJ«@Ë“"$ j‚´ñIßgµQŸjHk|Nè‚kì»®™¸¸½åž°µúcó³LüÔü`çÜš;ß\7‡Ì�3 «³á¶‘³RË`üE­B–m’Z·öñ3›Í3úžº];i<â+ÓŒdäº \Ø’¬š3�¡éÆPµÉéCLVJTÚöƒ"2Çò¬ÞâšX¨9�£y¾¡Øóp›eK¹åeä\E�õ˜“¤Sô�5!’|…^Ax1±tž0qtopm,T ï!êù6[\œ…06ÐwŒE†<ÏÈ—{#©Ïn4º£"?â—5¸q쯂†Â“HŒixÊÇÞ¤«GmÁëgÈêiRhXó4’× ^Ã2déwÒžê8kêƒÃÉ�½ˆ+Ëó$ý” �œbå`1ˆS[úãù~£A©RbÈöôŸU|Rã%ªÞàbR>¼é 9+ÊGÏå˜ïF³ô�‰Sôâkéeô«”n©Ìeì‹ÇÜù*»Ä«|œÓô¾ývž„}a<×ó~Ùø{L»þ!Ý[œ�/Cï‚ól™\”D…7ŒeŸÜ4ÅÜ`·›äßbvÛrJÀt]š¬¡s)ýŒwÉÙ ™ÐÚæôN>\ª5Q»€$(¸:º¶d%˜½<§_�èw¥Ó$�—f+,¶»¶¾L)x �Å%ÈTL{ÅÑïÜYpÝ΃ގ9ó¶*éRM pTáÍB`°¯ð¶›C;&Ç‚C‰´Sun„íé"”u‹õ-‹ºƒCáäÜ_´’Ж�à¹BºÔ‘·‘å'cNŠ`}ró%èZ7µõ\“í|ƒ…h@‹³;+?è<ð8™QO�ïFà·"yJÕYxJ>œjò3(–0ÄîØñAiëý DÔñAµç6—×Ü�¨ðœ\BSxr@·Ø²ØG¤(GÐB.é:$p=¢³¢tƒx¬"}Åtã„û�n ÁöøtãxPÞb Ñ�T!9raµ¶ºN¡±Où†¥’Êùç7/‚;ôú5ªÿ©6=z-Õ²p˜üò^?tø9× 2x‰IéÆÕi³ìùT’Ìöm¾:EQq¡ÓS %Nåf%C¼qK2K´<‹Ýp‘a°«€+@¾w¿I'êíóÕ )Ëö5¦E÷Äyìv…X7¦IVdHD°çŠ£3˜ÑI7Fš2éU†l‹†5z‹bÞÚlf'(E¡6¾M Y÷L>VÏo1â•gþQ.¡¹‰oD�Ã#¹�—ªo^^¿~ù—Öx~IÒí÷ŸUœÙÇ» ?`c;ÆäîJ©•OåÁ×%]ÓÈt)ÍW²ª´â;é½÷ß¶L¿×-Ó×w¼ýÐ[&/×3Öeû¦vØÁÍÓ­èø|Özt;°P̱]ÉL¼G÷ýò®…u$AÃR)kŸÜÐ> ŽŽß!J#Þå� C‚!ÝV�ÂqznG ¥rö¦=Xùµ¹»íÃÃ|­éÝP<6DÉð98„ÔÐÆNÀªÃå1l«g¤ 6w©:UXܨPÀêô«‡–ËÊ"Í,;‡nÖ]Aç«VP#m�z²ž Øþ¶®�‹<º.?øúùÃ;^‹-¤QÝíß�<`²î/öIz©<á—ÑÿÿáÜ OO“-�¦^,`xœ½Zmoã6þ®_Á pMÒµã8À¢Å®c4I{À¢ÙmpÎíµŸ´DÛÜ£D•¤û÷ßo†/²õbGÞ*Zl,g†|æ]šüåÇ_î{ø‰¬L*¦Ñ$üÃhÿ¤ÌP¯¨ÒÌ\Ÿf1üþdÏ 7‚MïdšÓØÜ ª5_ð˜.³Ùç�$W2gÊp¦'#GMFžå\&0žj´á±~G:±‚=Ñ„’Œ¦ìúdߎ‹OEú XÂc#•>™NFÅ]íÕ¶º¤\}Nb¤»>Y½% ÿ á§n±Löü]PwC&ói�ïT±»'£%L Ì™"rÛ]äVþ4Hõðí‰ÛpCr©¹áOŒð̰%l~¢¢`Dç,æ‹ Ï–Ä¬ÉZÃ\‹ˆQ”gH™PCWRh–�…TÛä�—Ÿ‚ºâ‚�Ç×Û£q`e&6^áhÜ�òF>]'xª;zħθ \¨\#Ü·ât.ØK@ÅLB•¢Ü�. !^Ì¡QG,HÂ¥´h+΢è¨bN"U\áÇóŠÇ+|´!4ÏU{oôëñü™©Œ‰8Ä ÖÍáê{zĴɺ‰êv5\ø¿í.²(²¹¾­6ªˆM±Ù‘0Ï¥,) I©— ¥®¥}‘"ƒœš´[™õÑR1k2Ö~h%ƒ]D=æ�¿3�ËL3T³ Ì»ô=‚[eÛ„4¬—·`Ïõl5 Ú’w0Õ0�¿¦¿MFøO¯WnŸtö«-uŸ±q‡éÿ±‚X(A›wtøþ³­Ëÿ½`®èÕÝ®|à+£õyR#ZX ³ÉYéÍö (X�ëp¥ä]t÷\@Žiò J§Ù&h¹�BÈçP†YÑïü¹ 츟’»ÃñÈ�ÊQ–k–|gå^.m0p{v>UR‹_¡ìcªw.U·¢{­Õ-¿–b�tNç\plË쬮cÆUò9¸·-ë0+²Ì¨ %ÜÎAµè–‹½%W‘º¤,‘S¯%÷�´·÷²Ç]Ÿ¾nÙ5Àçxå)×q…CNj׿ØÏ@ÚS|�ÞÊàWPL›3>ør‹;_��Ȉx�6ƒÏɹfêÉ /Ù°´”OÍêdÊ'#–NmYÔNðÅxÌ¢~@›ñeJ» f {„ÍókóœP-h…U IØ·\Ž/SYË–à>4 …Õ«.ë¯.°Âó̸®4r[›*ìé»±HÚß&»°›ICæ jÛÀ–ÿ‡%Ü”…nÌÝJiU¶î5;,º ý±èjdvç”Ñ?Y+´;–-PF/`‰Ç®Ùg¸½ùª“çZÂáôüšˆ>â@ ûn1?I¶F2Öa„-t[‰ñ?%ß:÷³æO.¾�HƒòžÎ™(=²£ëû“hœŽºÑ…-–î\^)›`Ö>Á :  žˆ¼!·œn‹njÓ‘ö[¾¡˜N.iÕéÏ%-»¦KâsÝßá�ºß~b WøOlŠjUE}f= ÜØqø¼}½}Á…ò- x¡Ÿ0¦!¾ät¾ÐˆPñL7�t ƒû¸j@ õ.‡<EòEÓB ºÞпQ€Ü€ÖbŸUˆ#ÓRr|ógÍíT¸ü©S»¿zª2·Í…Ú€{ÿ¯ HÌêúäêò¯'›Ï˜å:“‘Yí}÷6ýÈt âÝÇ[Ò‘q¹�­¬„¤*Á5‰,@!—Üœ–žf’]r'«ÂÆäbDÎÆPz³uNΆWÄG\²>?·áÍñAE)À³§¯�[ÈÙÚ«0Qí|Ã= ‘9YÁ| Ë¥÷Î:J®yV�-Ð#쳨OÃ6ðƒ0z´ÔðÚyí»Àr;ƒ hh„Õ¦Úu�ýÅë×!g‰F ÙÖ2—/ì0…é¶}ÍåZ<å[ÒÜ ”p³Í!G˜'GqÚŸÐtìF1~¡f"Bš í(ÁþµŒØCá’Wä4E’U)e¯7"h {øàá¿øõiÿŸÁ¸ï4A¸&}ϹÿÛ…½»–·!ïy›¸qÓÔ³óëÃ]�øhñfÑ é¿w0â-ׯÁ²wIäo£Ý?¯I5Ir„`œ‡×4\è�Yu£¬““• H9v6­v�Ф ÞÃÇÇ®”ö°G]3c ©)fÍí!8 ÞÈÿ ¶ÿ>>>¸’(“ z6½Ó©á$72î~d|})3µe„Ç…À¶¡¼fÔ½&­ŒVL¯Yͧ‘›ÕŒh¨÷‡Æ“ ;…ƒ¢š%"ËÂOO`C–è2—[E8© Â^§Imë±®RdÌ_»¨ù|�¸þo`dC�î~ÿfÊ'kjݶgÁ©\.ÁüHGÂn„(F½Ûј�5Z]…ÙãªCä_dγ�Cw;.!\ùžn�BDë¸F~k-º;¸ËÙoÔ¦=7d¿G�E\Š×Â\˜ËgaÂá¦kèX*¥ë~2—"eMÜ>ºÝM»ZƒáhÓD�mÀ?Ñ£j=Û«rìŒ' žò’C�(ì߰\“edÊìt[�ínÃ0^¶£§öÕùÅut¸5»ø3uºCV31þ½ÄYžø‘‹zÊþå²SÇg÷(� � $æ‚wuhØhMƒÜ„Üø3–(%Ž:iÖ‹›Q¸æpúÜ÷€.0î{é#¤„ž“Š!1º-!¿1!�?´Œð=P“»e&ßst¿d»|󨾋nß>²OƒÐ 0Éh¿ñ4¥…âS¥Ûg¦ &}).dOâa_ +¹ÔæÑ?iB‡~ò0[ÄR7Év7ïC ›¹˜ =òÔ—LÃ5èú¢ÝµÕ³ÎÎ%D&peÃ\öeüé 5!Êå²s •<´«¨Ÿv 0Z¿?A ×s$,žwL7ŒÕ×1%À~6Ñ? ¹ì•Ùn7tNéúnT×Á9ÍÄ Æ�F0ˆ‹pm³m„¼XÙ±q»gËc”çEÇÍy{3ž ÒtÍÑ ·éÅsB„¢Ë è�SwG™h8\t™1;žÔx026ä4ïÁ‡Ÿ‡³ug#î‡ã2v|ÜùÈÁ`¡tÎ3xí<Ãëã®að2jDY‹ÜËíSåA§ïpZ×…[·1PŽó#önñv«**+ʶì]˜?)Óáän41<4ĦGí…™Qª“Þ¿X.Ò ±KŽín“�‹v¦ç„}õ |B‡s")÷ ¶ØïîGSã‡6¶FßÅ�Ã�t»ê¾å¸{¸\áQ˱ˆàO÷wÎÇMQü /Ò*íRžÈ'´þu? 9@z±55V¨\™&*”xÛÛIþØ/2½­a+µaK®[‰€‘œÆ s¼žÅÂIÈ­å©;ÞR Í/\Y'ÐPF'£P´ZÈJt(’ ä§*2ùYW2xçŠ"鞯ÿ0â4Zl¸N&‹%$,xˆ dÌjrˆ_ L)|'`ߌ§-ýµºíæk§‚³Ý4·àH ‘úº÷¬•ÃÍ{'•þ®sçK\û›çλèömsç]Ã!Œ]—íÛío©â ~¾B3´�·;ÀW™·Õëkdßµžß¡ï²*xD©¸¥n«ôŸýwJáÞƒ¨ÿ3¹¼ V‘å{<ë«n�ßÎqÜ´m5o²[º5Vv£|ɬC¨±jƒ©™ƺôÖ#<­&�q{*M))­î ³ÿð }TØ6f²-3]1�n +¸¬†zAø³[d†S—|w)ÎÄÅu –?‹�0Ë}ÁÚ1•’&νÝ\ág¡Üe 6ÄÃðæû“žý¤évz¯¿Bä7ýÈÚYÖÈÇe¸BQ¹½ÒŽ¨Ê²Dë9ò$S÷ÄÑ÷†äp‡º¡®–ÐÑ)``Ô0k:¢wƒ‡ZÄ#_ó×õ±+¼ÈÄÒ2Pỿ—¯+üsèkR…¶ñ£‰Ð34N Z¬ [³;tää¹’¡Úð˜ÍØþ1û«NÙþöOfàÇÓú,ÂoΰU‡…§î(g&î]ÑoiÚÞ>Hö3^ÆAÙÅ6rµUÎ�fgr¥TZ{_sh¡ÜwOþˆã§`EYGƒÁ�(9–�6éÚùŽnÜ13’È ÚøƒÜJÃ3�?ǞݓܜgY¿}À«Õë¡ÕªßU(kñ¤›ÃÊÑGP'¾¤a9®�ÛHv\þ¹ï¾¼v4e ÐØŠ›á OÄ\]¶žaµ+€¤°,}uê³ðdÎÒ†ñjŒz¿¦«h±þ|<3 hêÑm ÈxÀ㫸£-X7»Ãìé¦�»=J4 ÙdÚ+/!  'ê*Á›U�7K)²ÚÞlKѾNuôûï~¾Üv¬ÃS¦šž-ÿú¥€‘QÚ&ëó2aC\&ð¥Úý³Ö~eòïeÙ}ÓªD># [ Z�³Æ±ê�ÐháªÝ;oO *µBM‚ÈPN2{Wòwð!¬~+Eé=Ø¡løcî>1ž(î¥@W¹í®_öF½ò«Á«?¼öQ§ákAñd—Ä®n|Ûo¢ó–¨I–¿”™(¦¢9;Ç‚€;ùê­VæZŠ0ìfܹ{ zøvt•Nî»?^°_‹“XÈ¿¾Àa‰$ê�ZõÇ ÍÕs(•è*ö¡�’ 뀹5mˆƒ¤T,eᙢ˜êŸ2zýàu‡DÑ.—¶²x)û;¼ å<õ‹èÑ$à 5T|‰Ñmt4@ä§ï/.�#*ýÙ€ÝÄõ�á—�¿ºMT«´×Öî/þ]ñÌÿÿfîÿÊðô˜†¹  Þ ÞŒ�˜@"¡xœÍZQoÜ6 ~÷¯ÐÜ×$‡­(0i–š.h‚ {*d™wÖbKž¤»äöëKJ¶Ï—È—Û|Ú³LÒÔGŠ¢H;ó~ùíüöÏë VººÊ’yw^à¥Ç™(¹±àNÓ•[ÿœÎ� ;×uÃ…;¯¸µr!wR«[0Ÿ�d>kåºØ�Ú3ëPÌ:)ìG¶GJ&sίá4•K³ùŒ“ÞŸö�An2o˜ ÖiZ~` ‹ÿŽñÖ¡¡ ØA5Ï÷)ËqJÿÈ|Ö�Ú¬³‚åRq³aiQœ9”g mÂĽš$Óù_€s nŒË\ a>žtF­ñ·bK÷Œ«‚ÁÌÆ•R-Û'…®W…=JX¾rLiç§Ò¹³öš-“ŽÝsô¨t ¦Õ c—®7Á)œ©à¡É:™WÕ†9íU4’L©Ñ]F×G–D�Ü0[£0’¾h˜¦aÂl)›“d»¸CX ؽt¥WŒÁ‘½íàJ� Œ?秤õ1šó _Y ”¥® ´±.9�,;ò,îA­»Ê{MaZë \‘s`Âhk�×¼’¨ŠÖn7�hÌJAñ‘ìIü Mháï(ä"�6ävZÇïˆÏNíƒA#ME•¸ØFº( ¢,0\x^Á»/¥(IÕ½6w>\·«âJîòÆIg½µÇÏ­3Z-³ ½ÊjÜ=³–”0†¸4°À üÄÉ'”hÞÅvtlAxvĆú®¸À€…oŸ�òIІ(œ×Ñ�Bvð`g¸áºL¡†¡mCù>ÛâÛvߣ0ÃÙÏ•f×=�&A]ï)/’Oéj²¹+ÚÜòzíïF%O¾¬ê/¨Ô¦Y7 Ó·)‰&ÅÿÈËqÓàÚÓRãÊ©>C™"ü?až—²* (\Îvƒéy-DÄ ¸ªþnŒ×aá ®1|�sh—ö“áJ”´Ài6¼‹ÁüÃoáàFDÌr/-÷Ê]‡SC\òÁ]4:)Ó£S)$›Npëb&V˜h´šú¥*àa¿'ÅŒðŒ7e…–ÍÝ(ºßðh·¢äxj™oÅŽЙµZH/1°'ÆÞc—\«�£Ma¡øö©ÃÚÛÛA�¯M³Ýû±Ù>m.×ÜHzÃ4î¯`¬¶�ФÙðî¹®Þ´²=â +‰ÖÉÛñ³öuðd¯J¶½×v?î´m[òÈ{)Ø­\3xp S.® (Çwœ3¦“ĸ ²ÓìKIùÓ_âyƒsÓ—@"´“Ä‹¦â’~_ÞÅM”Æ…6pk¸² mjÌÛ;÷Ñ3´c2Þ`þ ý±ÏËôèŽCgÝ«ô—½žo?°´ïæ¯ÂÝðb~¨w�+ÚðM·ßÎF¹ØôŸÃ§*¾$¡/^õåïä߬ñë¬Ò沦¯L\ è-£;Û:YÓAKË+{ÑAѪ™Âê³~oO¢¯\-ÑŒ'¤hçNŒ§�©Ïä¾y›üZÂ}šÑïHŽRnðù×2“DGÍÍRb¹®{"¾ËLA�á¨ÛSÀ„‚–›~_‘ĦXiz©H¿/HbS´| þ{pšõÃÔdâ¾UÔ}TÖ(´�Õîæéóý¬ýS‘YøK”ƒÄ ��˜l�±\A’xœå\_�۸ק`÷¡—C½Y$ÀEÎY ·M�à’4H¶hûHK´Í‹$*$e¯ïÓßÌ�”([kË]ú¡)p¸ä$rHÍoþÏøæúÛ?îîÿóé-[Ûª¼ÍæáÁ ø£–³|͵öõUk—×½º�çVÚRÜÞ©ªá¹½+¹1r)sn¥ªïµ¬ÑªÚJaæ7nm6¿ñ4ªØá /n�…ÆÊܼbÓhÁ¦lÎYÍ+ñúêÑ-Ï?¶ÕGUsu;¿áxÖËÇïÚ-ê/amÃr\óújý[øçþÓ¥=NæU¸çŽÍ·=Í\�vÎo¤~ËàÝBh¦–¬Æ%LÖÌ®³@$s‹ ¹ —¨ÌõOWnßÖ(#­ÜØbÅJèm¬÷èbl­ÊÂÌØBó:_»—ãuÁJÁ7¸P­,ÖÏ£Ë2®5Q0N”vô0ׂ[Q̘QŒg¬�gµe¼Üò�a9×ZânVª-\ÄßÇ®yÍ„ÄðnÒÂʵ, ØúÍ̇ÅIë=ÍcõžÈËš³lþ˜ï‡?·¬âõ$9>Ð×H È^î�RPIø˜�·vve/ 1ÕÖÖY!´ ŽŽ³âþRµ%PD!ÂÚ�Oƒÿ¬Í·Vý Â�\²Ï!ªð� ±‘�îˆî]×ÓBüWÃÔ¬E› ¤/ƒxä:Ø—À^c KæÇBƒ¼pÊF|C«vµî�Øž7{5�Q¬Zˆ_ᛜ¡�/ëþÒë@N^ ØÑ[Í�nnƒ� qtÇfØŽaªöA’;«�Æj¹hQ<gMøèüiùR·¢Pô¬°ùñlø×Amb@�bKG;¼ãø:j÷€í¢Ž©¿-bÌ¿¢î` ,sAdö¸äæ¥ëÈžëªö7%Gù„³ºÛóÞÔùä£f¯÷_.urpóJùŒé¸CãXõ¯Vš  npÉÄŽ2va˜œuá�T:uV·µx/ÍÄFlX�´Û‹TZR;ª,¤ôiQ±3*`u¡+k,“—â ±ßZî%�Ü ËŒUMˆQø�bÔÍÈúføšÎwAbçÎ¥þÈûÐi5È‘ "…Î[ €2¿Tâ‘v+ ÷Âp—|d¼[ÖVõGRù%ª¾€Øù ïX‰ý�W­U÷⃀èù=Í…t¯Ñ×ró{‰…?#�%t¶nH¤fxË GDÒš0ºî›²YóéKË“‹¬§:b°0‘‰d§�¼Ö|HøšXªÏä¾ZŠ ËlX¾ˆn ™eìÙK¯ñâ Òc,üÛ�jàʶFXu@2öž.¹'cÎ"™Š—%fX¦]8yöÙ¼jüÒ©ÉqÏ�,bðŽõ=OŸÅGýåÅ�AøÚõ…ÂNñ`£ >U’ÿ¥…ôz2{~w`|kJ™{ì„£ý/&ìõå£"÷eo±›Ãj"ˆBçǾ,j5fõnè+®ß_‘¿—²9 Üt)íñf®«t ³¾/¦·Ðv] †ÓMõï ‚ÉÍëýM—àX;ûûbýy“\£;/¦Çf»º—ßÝ7½1Få’VœË…‹À3~У0á8·¯ú^¼ßõ?_Ç 7œZ ·¤ ㇔LJ"MY‡¾(¦{S§&T'OCÒÀ#uâÃÄ\6™K›C}¦Qµøu“ ˆ7¤`H÷øLªök;æŸâýþ$Q4™h] º6]®ûåÉ+jÓä™N;!ÃX‡ÂJ3$ú¡�Vò n…©àÐÚ *ÈSoXÖnÊÚ(�¼�ÉôîíÜŸ~ç…¦|âFre26^uÊÆ`�ÐK8Úg?¤'Lµ Ø�Ì?PèÏÀdŸ[÷Ÿè³áÜN�]ÜÄÈ· Ȉ(þ Ï0¾5%’��peXy šJxš‡p¼B.—بÂjIÖ•K†7r%DÀ�ÅÇ¥‰­9–±(Â�IH[/<]Iiæ‹E8å/pc‡BaÈàjKv«(¦q÷6õ4×',}OŒdÚ Û‹ÖàÅ~¯³ï�> ÕFê;ZÛó�AÓVNm•¯{½³Y˜œ0¡,æÛQG*4À»ú.VÁ-Ÿ9Û\�è-Ðz‡á Æ—Tš“¬þ*j§~öK\+v\ð ø³Á¨TÝý€ÓÕ Ñ,.†æ¥»–onQÐéÁ8hÑãY˜ߟipo³è5QÙæGföÊ}ÿÜ¿)±ZY–jKÓæð!kÞtÁ7ƒìÏ@‰6ÈÔ#øÂ\qï¥B‚~-•Æz¸˜ï»Ô€Ý‡7÷ïßü ÄŽ�qêÕ‰\§$4Êäš ¦î 4S/–­ñ?êìËL?37Pi„µ46…]mê=I7€é`@ë+_õ}'7„ ã<`mÎwÈjbɵàº[5Þ;Ÿ‰:¡—Àÿ›WROßLÌ"‘©ôºE†gÑ”Á39û­«]û‚y�Šö~kÞú@Æ]§=ôü·.1@w�욉�`Ï~ý1–}PöìÉÚ~¾ÎeãJçÚdàôê�Ý¢½pë¼»ê)˜­ÐÆ·EI1³QÍô|TA °‡¤žÙÿ‘~~É•÷˜ Ÿ^M+• ¶$­�ìQ©†—Œ7M)Ý,Õ>pëɸêHö·l!™AU6:-øÔెã C-ÂN­'[Íýj¢£b]”~¤óÕ(¾¿Šáth¬î�yãÿ‡7î½ðœ»Å„ 1Ò1‹�–>¬xœÅX]oÛ6}ׯàT`}‰mlE�as¤I6¨» ñ0ì© ¤k‹›Dª$eWýõ»ü’›Iº§@bI¼äåá¹—ä!—ß\ýz¹þãöšTº©³d@K|4 ))**èó´Ó›ÙéË5Ó5d—¢ii¡ßÃV‚RLð_.VË…³%Ë…÷‘‹²7¿Ë”¦š)Í õ#‰·ÅJÉ’N8OcUÒl¹ ÆÛ÷ôކdÙ’¢¦J�§Õk²Qø7ÃO�m QÂ=Ëü?9vdk/­ñ˜Äd $­Ùg( -K¦ÙHƒUk"‰kV²]€Ò¨ÙëÔyZW@”xßÖäÚÖ´54Àµ" aÒT&bCtÉ—A""ÿ }Fö+*ÒJ(Y�nÑÜ ®y2!ö¤¤ö=Ó•qï¡úúfF%J’wš”Ûr¡‰ÒB‚m¢%eœñmb]Í“�Ä‹a ^�=w ¶¯$:m„ìÑåÖͦ«kß}Œ¢Ä99³( Ê †$£!šª¿”ñ¤qDHË•uFT‡4P•XH�6jØèrL�ÈY-–�¶9æ /©,I ;Fµ Lâ÷d —¨´>h’÷¤SÄȹçj®#;¦�ÐâNÀ˜„¾e#¦µçÄ…�)äOJ†…“‹0îýÿÌôÊvê}Ÿ%I°-ïrH§¼wdá&ª¨0wÇP P²rž�ßx‰Ý¸>^æB(Ä£%€z:²­JØÐ®ÖîQ %¨B²¡æ}â]¬±éÊàZ»ò7Œ_—[P�ƒÝYn)“W ÁÆí Ú!>ÕÖHÝ�ãlaæu5¹ò(¨Š¢³eû®�áaáŠjÉ> Mܬ]]¬ß^¼ùVª��øÉ§î„a&š`ö8ŒßM#0= ¹£Tà�$¸¸›AÓê~NpކLO&©Ž莲šæ5†BŠÆ…ÙR’‹ÖáIÞ-•–‚o³;06%p½]ø¢„\è+ ›óô^ÏÍôâ`Ñ?HsŠ|L¬hQaÿÞ•f‚;8d¹¥Mšù·sXl \˜Ã.µY]ýÎQ½Ê¯±9û¡•—Í@ÙNÒìv(0þÑÍ+³?–ÌSfK]ú}ç9~«4×5·n¢ ©ÒìÞ§ëÞïY¦SüÇ 9—©vRÏíÿÇ7 y‡Ó_r¦ßQ„Æà7²$ÙQÉ ‡'EûÞï|ƒÉ±ñ+†4ØlV¦œß%î [!YAëiÔ£Å1Ä7�#»ÅØî™ÒáúS‹ ”‡i/2=À7{î<ÁÅd-*Èá5η7À~ÑmÜÚ:€~ßIÇ èœ7h'ÇdÉ v{Už7ÿC·Þ‹Ùžö(<†zßõÜTÞ¨;¿ù]…­5 Z%º¦[#¥Q£yÐáxòq š#�Ê�c´Ö`µ¾@}Dÿ)!ò'�*¡Ç >/¾Aƒ¥S9vŒïõUoáL5†þ¤à‚dM'÷ÚãÁ4Rm�&çnFž9íG:«zG]—xa‡¢p]¡ôÈz#ñ|SίûùIEL¦§©÷Ç蔑¿ç i6Í$3|¶­4��­g‰OàEOrÆ•9P&㆘ÚWÀ'Ñv‡'£½£d%ÏÄÖpZK§·cVì± ˆöã“Á#Ç2b®âÇ¢¯—$Ãé/�#›ÌƒÀž¼¦‹‹ê䆢v:šÉý#Ï×t�£kI¹2W£‚Š¢$£Ûˆh‹Ãq3c¢À± \ Mǰçš'�Iî¦"œ‘\&þÇÒÔeœ>搜‡ï%çxÍÕ f.×ÜÕË—Ýí… –Þ§bn�ÒÌüÆ"Õž»ž2—P#?LEwиy7¼Æß¡!Æ|Ĩ²lᯎîRúo¬Šj”*�¨X3 xœí[a�Û6ý®_ÁsqM 쮯 [Ç@�ëŵI‘ìõ®8%Ñ/©’”½î¯ï ‡’%[öÊÞݦéH²+‰œgÞ›¡”Ù_þþêÅÍO?|à _ªy2k~žÃ�Rxβ‚['ü³Ií—O'S¸ï¥Wbþ”Ïü Å�“ ™q/�¾±B°ï…/LîfS˜Ì¦Q`jò Šÿbî< w^fîš�3’gš—âÙäàø«ÊŠ\f~2ŸM9êùò°‘ÍX�ý% ­X†CžMНØÂ�€ÌäG–xÍf+n犧Bͦø+{Æf鼚ÎÙgaˆIÿG.hÊ^Ðåç³iP1›Vç¨ÿï®þ(ÞeÆ ºóó€IŸB)Y}ýøÚã ’îgü¹„KÿHfÅ™®Ë“휳hņi±f9‡tYK_0μåR‹œÝas”Ç JD毜ËUã‚Ò]~5!]$j #Ž9¡sÇÏ fÍš™EÒÈr³ÖÌ9ÈuάðµÕ.Ü †Á¬p�~dҳ̔›†:ϸ¿b;BËî|+¹ÞÀU— ”“ܸ cÉ�p+a%ˆWl #Ò{p§Ñ��Ð!}|ør†¥ýÏS%.غ0N0x.ñtY}V€,oHI«˜ìÐÉÒM€žà m<^WÆIÜÅkxBú3qÌv¶t¬ŸáÅJØ œ€>ÓBÀ> ÈŒk˜‘À¸€Ñ¨+¿�8¯jÁ £r©—p­Ä ´À(ÜÍœ¼Ç}¼ ʓ晓º. !0<Ú¸Ú òs Šd Ǹf3Q¶1Ì}1™¿¼ýçl*JÜ(oåm»¢mLUÖ¤<• ü*\ˆË6ÄÚhUàXœÔ dá…�+zƒÚo,×nalÍâ`2d8a§>©uf´kúHN�� ÏìbÝÙ®s)WØÜìzKrÖŸ4NMÙ•ëQYŠï�˜éœÄŒ¨~òô�öDÜŽjýQÅÁ2 B±.$ˆŸ†üÀH½Þ!Lô›Kvªcìà›´U} to@ 1^¿“q@÷Xîç(Æ4m*ÜX�ðN_rÑYKMüß±ãSë~©Í׎ŒÅ‰„b¶7ymj•GHÞB-b ÑtT¡Ã�„™Ô »¢fcÛô —¥ðÿ�¹Pr»”zT6ÐÐ{Š–]‚jä>í]ýô ‡£g˜q“ö›ï×ÂU¿È`Ø ßmÖ©4~¦y‡N ›.›$!…4'’£©{kQrƆµÔÅ¢„¢Nr^Ss�…rçØŒ( ¯;C” ±uÄ…‹mY¯@>7’,Ùò Þ0ö0!Rn8¯[‰Æ¨Rp½§/TþÑ¡x4b¬…vJm"È%¤Ñ”%ÒïÌ)M(òõO`êÞyåáãʽH<~z¹âg�^^5‡PG·ü¤äˆÇ'tn˲f1G‹ôŽÈÆY×Z°¯­çš¤sšL ZkgwW0˜ð¸'ð]àë Š7'ž1ôÿ |r¸­*é¥Ú]=•È—Ç4†ç� V玩Åç� ¶}s8â…áéL¾A¿äÔ{�¯èÒ Iûý©÷€Í©ôEÌê'ÿrYx÷$ªŒ†¬Ý�?Fp–qwñ?Ê9‹ý·Ö÷áþýë°·HˆÀ“Í�=á8b;Ãò\µÇ…®.á™ 5{�ǘí€w˳DvÝÆSBRKT©�-¹’¿‚ØZ�„þ…º‹ðöšƒpÕv#ÐE‹s‹X�l‚$^U Èýáò#øÞ™|Œmyü!™­iþîîù>rù.§E�àrZÝ}¾.êIûý¹ü#îM¡�jÔCr÷:>Öí¢ã�p¢<ŽÈQèYD¾5-ˆ¸7‘—R˲.ïzµÿ�p㟼wncÛûçÆÃ‘vFôwݸ0J™5V€¦Â=s!Ì»ŒKÛ1üýöÃ�8€Vq ”-ñ ºJ�Í…m/]æâ‡©}áP!n ¼…¿Ô޹/žM¾xú× ­{»‰³©/ºcžþ­óãv­4hêé7ú6ˆÎû¢©'ú\ù�Z?iáÁçÝ¡AÃ+ÞOÅ©Äõ•įÚVª‰©'1U1E;}Vï™°ÖØ½‡�ïFC.¹Ú— ÅôRìMQf)÷µ€tµ³Ïâ'?Ÿ3,µã _jž[@“¬vE+G·s×N 9è®çØe+³æã|ÉÞ9ô‹=¬ÎÄ'ô]dÈzöLC¨´UÃÇ¢ñÃ?wzÜ¢Ññ•Ø<ªrlG?À{ÉŽ¬ƒ%ÒB*¡hsö&|µ{ä3}b�_Ò ¨1¥JkÖI&1œæš¯Â×!LTž±Ž¾­¦/¹f¯2�ëÔ”öÒ;OÔ– °öý¢ƒ‚‡õ¥² È[AzÇO¾5THÞò–�NHÀ�ÔñÄÂoªÝ;T4Ftë p â¼ôµ@âÿ‚gM¡Àó¸§ñSIaÕ†áCü,¡§!éy�\Ðz¬�}çÀyâôøÛ·¯(±Z“&]ëð+¡Cx9”ƒÙOÙiü¯(Sú/¿’J• –š–��  ëxœÅYy‹Ü6ÿߟâuJ/˜ÝI¡m2;Ð6I ä(Mz„R‚lËc5²äHòL§Ÿ¾ïésdw(2¶Ÿ~ï¾´ËO¾øñÕëŸAí¹Ê–é?ÎJü¯áŽAQ3c¹»šu®ºøv¶À÷N8ÉW?ê¦e…û…¯ ·VhõÓ÷Ïàwµ.írh²å"båºÜò×+ë˜Ö‰ÂÞ‡óHœ-(Öð«Ù1ÒËÖðRn¶Z.¡ß=*U"CÄ»HÕB!™µW³úTϺ<®Î}Xn˜Yí ˆLŸ£�FRüÆdÇß/þ»ÀGw^œ?'òD»—ÇÏOT$øëŒÄqVðs Å·P2 »V å,tV¨5¬¹â†Iñ//�•¥pbáA f/eè^¸Ë, –b“ÔlìŽY`T<´mó½þŒ‚#_×e�ÁyjºÊJ‰2µZYnçS?ΡBjWóD�O^c¡ aΈ²)OÈ™EL­ü±ŸÆø>ã™7BŒÅ¿<Äj:ë fx†À,Æ ¨®É9ig¨·D*X.9*h‰0g˜PäŒ$ì„À£ 7;0z Âö¦À¿ªás­->'\h„õÞÝP˜’_-(íjzUݲ^Uhá90…ÎG/c\ þ…dnpì-÷BF$�qx¬É4Ì‹”˜¹š¹¬7ý¬žPp¯Fïœø+pðÌ Þºt^-ÛJ¡¼>RkYÈT»¡EФYŒÃçìy ¨µÕ_)VÙľÁo9©s:KSpú°¤â-„·5�ýGÊ&¶ÇiUÍЄÊa—±ûºŠß.kÓt’%(¬ý·' zU–›í dh¶Âò9 …c3)ÎVè¾÷í“jüõ@r¥'VdÆGšÅ|M…Ê·d4¢×9:t�”±H6µKnŒ6†ÙyË0k±h,j”É¡v$Ê<”¦>Ò°XyÆØ�Vò4¨ ‡EѨ`éΡD»ƒn)ÔÑ\[†k§;ÀC ·Ã̲a$Ø�,½S2Ž4¾¿£‚XBÉ2h�Qä/|¥¡XÚb/ÄëgΆíB‡õ„”dzÐN‚ pD+j*gzdmBw‹2a†c@»àùί0¾©á€CÏ­¶¾tßÇï<›œ$šþ4MX'é“9 O+Î1é ­…'2<‚HM¼Ê8.ŲÑO%¡p!ù½ ÃúÙ¿Î<÷ø!ˆ€‡†ýK»VëÕKÎ}_Çe_e¸©Õ†WW³gØX1.ß<åÌÐÄxI[å§gÖ ËüAÛ†çÆ;›?4z5[M†Zù¼lÁ¡çWE©­}ßžH4·]Ÿ×´tz==½þ(›âMø¿Êåèù—8:ýÆLßeÿy>|M=Ç{2Ž©é8¶9¹/¯ gH½Ÿ�ÙK¯½„ö¢zØ[ÄËhºÞr±®ý–ÕPc¶ï:F×oi^ •&i\Z¼2C>ÀÖÔ]l2Ïúáz(Á°4 �Sõs_Lâî|mÍÆƒø­jëeêtg {£DèÍ�=ömi¼uò¢Z_šo"Ûžÿ!¯‘Y²Þ.8D½®DC�ÐRŒ†ãV” 8È/¥'œ{:\o‘RÀ Z £!+-qç æDg/B¿¢íÄÞ?܈ýùÏßuÚ=xŠìw*<$dË%O›:ɇŸ ‰lt²±|tÊ�3ƒ�…AÕ©0 Ôè,¿Ú¼ ·!!ö–�upï“ùˆì32x=¦jhÅ w/ƒRXÒš~ èïáüXQjèÖ�pûó÷“LžcR˜+”�ie»†F:™sÚ-i–Àq€µ­ÄÕäÈeÁõzþÑvê[ø±/³·a"Üv9&Þ›&ÜKŸV«¿Ž> }£¹Å² ÷)þ¾áeOxt‚¹f÷ÌW˜“ýº’«SƒÂ ^2ívôrÿúw´{An$Ð1ë/wÒ­ÈÎ7ëìœ<´“к/ GÂçB1³b2Ü7ºâ ‹·Þ³¤»Ð±@˜ð³$^Õ(àÿàRÒWn�ã×{{è Ë®%I$|5ê�¸©Z'\ç8]©R‰I-„•ÑaqVà/](Ñé£� 9+Þeƒ¦7ŸîŸ¼y2`Ïw6áceà/‹ø§Eø›ÖK»*Žb��HExœ½Y[�Û6~ׯàÎKvQO¼ @8ÆN.]Ûi‹v€"O-ÛÜH¢–¤fÆ}Øß¾ßáE’m¥rZm� c‰‡‡‡ßwn¤Vy÷ýÛû�?¼{W•ël•þ�,ð§"'E¾—Æ’{}Õºíõ×WK¼wÊ•´~««FæîGÚ²VéúŸ·w¢1º!ãÙÕ2ˆe«eT·ÑÅ�•¿X['�²Nåö•˜Tùl%E-+z}5&ýü»¶úÁP¡r§�½Z¯–’—¹µðXÚo Ûˆ¼”Ö¾¾Ú¿[‹×xtДëb|£¯’‰±Ú¬Otn`‚Ÿ¹Z6¬}- °!#ôÓ’\F õ�V¯ìõË«0áV4Ú*§H¨ÚÑŽÌB¸=‰zD?ñ˜3RÕªÞ‰B:ù<÷{e{3ñÛ€‡k]—‡çqí%Ÿ·ÛØw�™D÷Xz&xO•ŽàË ‡ñ �’›’¦°Î©,…4FX{< Ì<Œë‘r»3þ•6™Œ„l’&�3"fbäG²�®-ñ¦§øÊÎÄÆ±Ês.Òx‡¾ßË'˜!7g Wøµþ¸ZòŸköÇq}+hÚ¨\–—§“ÑI3!ýÝç�¨1Ø@ÞÏ»<ß ³¦$ü÷º,¹.Ûªfí$óý ¾pvT ø‹.„¬ AUƒm<®‘Çìlðþ©Á T|YnŸ5WŸS>‘«(Nû³’¨òo*˜TfÂùç Ô|p¢’.ßãMØôéVüKÑÖ0 z†„gó±B·+Ô.-жªý¶F¼ û}nð•ÑäÔ¸)æ;Á™Èè Æ8˜8Þ*ÇA ¿,-@•±äW$ë¤*¥ÂâJ!é� ŠNQ„V WGæØÖpdîP�¬›-ò¾Ñ¦jK9x› îNÛ9Øq(ás Æ•N`¾‚Q{¤}á³ |°þÙGñ_ñôB|%žnø¿¯žnžE+h †i°ÓფÖ.;Ðvkš�!ï~x‹ÇÉŒ8”�34:•ç„Ý?êëG¤,Õ˃üѲ¢ª{/Ýþjý¯§›Õ’ª5'2£žŽÚd¬XГh¤âÖ� ’Ñ�v M ÑÓ“”#¹âªYØÿ¯d´ýûBÜü-%DO­rIÜ;ŸN9«5Ú8 é<çqsÖòÂ{Dv7&í' m5ÂÞjίº­9‡dŸEr8Y–½ƒIQê�/ôqËl«d->, +J’œ°-Ë}˜ø ™Y¶$6´ÇóEÞç1¯�Om·ï…ÖCÙ£Öß«6xí;co~”u{C0r•6�­Iy4öŒ�'#ö¼ �E’͵øM8ß<àpw~ë‰Cl£��KÕ”ä8°sã[Ÿ2½M��/ .û“ƒÍÒïèAIöÞoÔtù�5WÀ}FùH®,åŽAeÿ~hãLQ¤©ÅÝFë’‹Ò Áa‰àI=ÃV––zïdàJ^‰ œøá¥¡ãÈÆ=2ù?o�®R^VE ݃,‘…¸ŒÁËûTóÕ½7ÒÒ+žâºœ‰Þ�¾sFïB1AtèÖ�§Ô ­3mîÐ&5û*µ/¡ûhåô¨P]j])äšx´}§·™J”䇼ä4zLd¬°Y÷Jضâ©ö?­4t\t“ÃŽl5¦ÇÙ˜ö˜>"«L1Ý ÎÄô@ßo1ýhŽˆr Tþ8ìøY ^Ž ÁQaÀ@ï'C3ÆO}G•m¶K©.é_Ðá¤ê0W{“ô�“ô¾/-Gž;ÅÐI ÷5(¬‘Áå—±8òû>úÅü‘+ã™®uèœjJå“.�n âåÖ/»_¨žâ×âA­_pa† CÚ1†QÒSE2;¯ý—68€ªÑ²½ë þ ÃL5?4Z¬1d•aS×_ÕqK!în�}ÓÍé�¿¼éýF’ Í`l»†Ë¦þKè&ø¿ü€9çù~¥ÅŒ)šB§ƒ99fù8P†6­òÁ]D'Ê|d†tö%µIÕï‹ÝôýG’›«2uêÎ#á ;žtõv/<´uÀqÓE[ ø*{"Õ;uá¡h$ª.º×ÆAà|Ì[r)„£“æºå×ý[඘ò ‹Ÿ6ŒM†q衚–×[ƒ¿Tç±áÖ¸áûêAí=£žœg.¸³ù�L ]½#ÔõbòV! Îu¯Ðë;GùçpÁâý0¹ïà f´Â#9p�L:œ< þÜ ý?ú6\Ô¢v‚3Á8Ð7rÞÿlðûê1ì"lk¶2§ó¬�Å´ð'd…ôñâÞÈÚr ¸ôK7aæ¯,½#à¦Áp|Âù•¾ˆE'8ß7De}H¢Gñº(éT_w9Ö爸wß·Eá?Çäqík},´ã«.; ÊStøÏ8zóïø!çlËâµx†Þh$®.˜ù�´›_ÂnúD¥/h+Ñ!‰ao1T9Ïõ+¾Å-¨vÊ�v¦Ëø!{¾–ÿ߉ÂF G b G�9� švýxœíYÿo7ÿýþŠ·L ]› •4t M‚‚ ƒUÓ„œ»—œÙùœÙ¾„ì¯ß{öù¾4M›ÒNã‡I”ÆÎûæ÷õcwòÍϯOÎÎß<‡Â©ršLâ/9ýRèd…0Ýñ¨vóƒÇ£1í;éJœžhµ™{‹ ƒÖJ]=¯,ªY‰ð ]¡s;Âd2nÎt¾añ?N­NZ'3{{"Žd"  �G;é—s™¹Ñt2¬çán##-É~H¤KÈJaíñ¨xsK2�_qÄ#˜¬„™næ’ð'8†ÉlÚÊœMᾧгO� ¿…ÕƒÉØ+˜Œ—[Êéß-Ý�ñ}�e)—OõìÀ�=+QÖx©MSx�+HõR“^XKWeE0Šö£U€Ñ³I��ËU<—²�FAèE“ ]æ6…¹6€"+Àè5èùÀY©·ÀÖ ô �_8ƒh‰. L¼üÞØ¿jýÄBs~6ÉIEdÒYX£\Ϋyùî™ÖÖ¥ ƒ¬ðæ /Èú[%�ØžTë¥<‹ÄÜ¡I Äémã¢3#*K$ªñç!ÀS,W•örA‰M” ÒF§â4r%mN)hï9P Ì¢VX9{´íd'ÈùM ä Ùá~o@�Ì:#—˜7«™69švi•çå¢ _©„ÙPf:C?eCîŠãÑÃÇßÑÇê´M0ªþb@Ã$ö~ú!ò½ïò0�]ø‡W—Õy7Ý{‰ÂThì½6{]>  j{^íSXaæ(’mYQ|Bõ²Ëéj‹¾3ÚËq.ê’c¢,JuD³±Wù«Å×3ûB›ÆÚ›[ÁU[N£ééçÓZQ²É óÉÕ4�R/d&JJ. ïg°b#«¬ ÙÔÈ ëÂ6%æð„$©`�ja˜"CØb†þ©Ç>5øåâ4ñËâ*\C.h€°Î¥nH®”ôjê#iÉkr§a»\VpþivkUjc}@9«„µ Ò“ŽJ¨¶g?@T9•ãõR[Éz”^àdšŽ›Â¸$_ò‚N•zzIÕ‰˜[™@&ªÀBò‰šuå©×&ÀwMߺؿ‚<¶"-DÅQ%ëk=iöY{óE0�˜zå=¡ZÔÕbúŽ\-J«©­�›­€¦^ap~ÕÔã¡ýíίG»OÉúJ^Q7¥|úèó”Íô²Éfƒ­¸þ*ÌY’þ=†t©­ÝkB3á­ÆóËþX Ò®ÌÍêüú1ýoq6+ë8RÞ Ó5Öÿ̨»-; ¸ \ét äÝᔀ“hš´x EJ4&Œ&¹ˆ | £>i/" j×Ö ’,í©á6e”(åß¡2è¡_Ô�’üxÉL„Xè�œ=áÏ4B‡m!DIr µ*F%ÎèÚõ[PßFî×܇zM{+;¯îáIß+_ØÃ#$ò±¼ƒr¡“ü‰¶‰MtÍÜhå­‹G¹B�7Õ¦a:ìo[«[×À-Iç—f ·Vö#² dGŽC+ó ˆùûIõ?|Ü>R_ÔÕ�Yî,FV¸ß¨|±κ ·�‚2†ÝH¸1€ƒ.yˆpN±OáCÌ•4\³2]ÖªJ ÚÂÓ˜6Ýê>\H¼×ë"McI9ë]í§:•µ%k„Ù¾¾¢ÿ¿ÔC±ñ^â&n•¾³…v=¼Ûò&¸ÕÉjU—ÂQV¶b˜Y4 íˆî‚%rvoCåO³ÌЋæÒØÝd ¶ç b,ô{|CXɼå 8^,:Ž/e I=]¯«==ý!ë .3•®*\xÅ‹�·‰�mHBÕäá-¼Ïã¡®$_X¯Åõ;î\>í®¹êøÁ-¸â ˜m¢gÛ׊½ïÊí‹Ùž´Ã“u6í’¿›™hÝWйw’~TáùëeÝ«×�]7‡ëíÿE-µq¢Êð&ïk}¾[�y©–—¼r ÅocØ;¬¿Mhf¼aùÇmLšÂsëh.º0ÎeËÛv‘Vîàµó?TøRºØ b—änЊ£¤yK¶Ú.ygÔïðÐqˆEèÜ•{ó^Ñô¹®êþÎ5jh½mmÐ?øÑ±u&…Ÿ¬3tk¤Ž2„›)ŸÕÒmÒÞä ž}ÁSfô‚]n—%u详3Ü´8 * UìÇ{Õå�åÒ’¼éeî¤_˜åï¼WÊüóΛ‹hâæaómœO†wºÈé‹‚'I—&áf‡†&ñµšÈ¥i§N8qÞbõ•¦Ó¸ùóË8ü•ç¶Þá†Z� �8}xœµWaOÛ0ýž_q뇱I-Ñ��&*Æ´IŒ!è„öÑM®�§ÄÎl§¥ÿ~ç8 iÚÒ° ÑÆö{÷òîr¹o>ÿ¸œþº½‚Ø¤ÉØ ªd}¤h„1SÍù 7óÑÇ�O놛Ç—2ÍXhîp¡Pk.ŕИ΄LÉ •á¨ß�õ¿äœÉhm#|kà ׆‡ú ºqÈ –âù`/äøVaÄC#Õ Ôƒqà3ñd¿â„"�"ƒ0aZŸâS˜kúÑ¥!¾PFÏÜþY¥y ÁlÜfž‘œø™�1†bäLlo·<®=w âËJFªG§‡™@ˆIL)¶¶P›$ƒ –X �¦1×OR転 Œ¤HÖÇ%·Oä�½dRñ�%õýt3v'²O÷ضù› Mgtø„éjøt#7€KTk0 ÑþcFÓMÚ!`š‘ÀUŒ„èQúÍÉêLÒ…-ŸN©húÌÀ&ïîú®Ê[•gaÉgDuÀôÞܺz̘ˆ0ú‡Þ°Ú§ƒû"tî¶úìJ‚L ¤kÍVv!=XÓÚ&‡ÐNo[@±ØoÙþÔh£|‘êډ픃-TŸöï ßvþ!æa¼Ñ]—†C®ñ@ÃNVl­]WZÿO¦×“ 2ÒäJhàæR¹ŽRe±m¼÷ÎßäéT1.0êdùÓñ>½n²î(ï<�‘›¶¾Ý¡Â�C�¹·Ú|‰=¯àÍ3ÆL;ÛA“�LòT´„=B¦ÄWÆ9ûM³CUu^¿ž> _Ħ[ÃÝ„¼‚Ã5óŽÇÜmU-¶{Ö¾¯¸‰�Þø°*¨€‚ðž'2™ÎHÙKlÝ„ô:ƒµ˜·mý*W¥Ÿä#U%’[&zÖT§âÈctŸ§GUµÚny}!¥6C³¤®ß‡$ÓzkÃé<µ™´­Ú+b¿UúO.?éæYÃí;•SÒ]Ɔ@Ô­Èš÷Ø›Ñ=¸`‹ý¡W%˜ƒZ6Fá@)£çG­Ĩʅ&…鋯Âõ³aƒ|gss›À²,áöq“͇ª¹±KµØq»NÔ»ç,OÌûaµ��Yµ[¯%rQ#(ù æ¹(²éAL“YbÞÛ:ÅTÀ"‚DjzéÛ7Õ)ÜM4Ô¶2è—¿}}÷+û/óÏ^ȇ� Ž8NxœÅWQOÛ0~ϯ¸…Wh´!¤iJ#m¬Ó�€!Æ„ö„œäÚxJìÈvéø÷;ÇŽ›BZ�¶‰ÒÚw¾ûîË糓¾ùüíôæçÕ *ÓÔY”ö_ÈJújÐ0(*¦4ši¼4ó£÷qB󆛳SÙ´¬0׸P¨5—b&46y�iâ¢4ñ�rY>ذo3m˜áÚðB€È3JÖà4ÞêgiÂlÜw»À�5J[(j¦õ4®N`®é†²Ä Ti¾+XN)»%iÒÚ°Y_ ÷9V‚Qˆ:rÞ%¿ïa4úè$v>‚±#kg™ÿBJT0¥8j0RtÆ–.‹ÍÌ"ȥԆ椂œ-ôk' d¢„UÅ ´ K^kˆ¥>„|i@Hud®QÝ3k×À ¬±ÇM—@LÎBMëWP2Ò /Q›¬®ÀH(dæ¶„a®dsØápè & GÛÈ{ŠnÍä;‰ÖL+d† „7UÕQW8êz¢H·•,=Îc«ê‰Ë§Ž¶qúxRm”‹ì;�µ–¤�ÄOE$ßJá|_°¢¢‡twŽL .»ÉˆC…AÇÑÕô! #<ïbŒíˆ±B·ÇÛ¬ôqÔMk¼ƒ!» ;ZÒx¿#…!h¿«ã,äߪô»VÉ•!ywXâì*LØ$ëØö–wíŨ,5¥ßÃ/�~°Õsrå$-Õ%)U[ñƒâÛ€@ŸÎ`%gUÙöîÚÙBú?ážÒ¾XHew\@J¨G§ÇÀŸ 2:øÅzÍÞ˸FÝR›AË¥UÛz´�ñžpå}áž)ÎÜ´À³ß-õ/,Ëe|þ²æú{m{ÒX³•�höS×�ú©Î€8{25VÍmÅ‹jXRïë‹€¥Æ= érÙܸ“1ÎÖ¿Gù_69A³`x”îfÀ¸àÍ«ÁºE¾¨Œèüxô¹;S¯ãW ’,9å ˜7Çc˜¿Ê•k/L!nM¹ßfGì =—ªYw¼0µEάmkni–ÃîÑÝ÷\œa!Ih>ï vW¨þ¾p£pãî Ç×gþǾ¹nöÿîbÈÀ_Çn¾“¿y¼Ï„_KM´ÿÇ€¯€uéî©ñ>a†F}Ö´R& Œ'ÇŠ˜ÑëRc¯ã–~\m1]ÓaöQˆ{gð1¿9ÖÃoþîUãù¢ [)ñ﵉{mþ½Úƒí Ô åŠÔ‡2�Žh %xœÅW]oÔ8}ϯ¸ÌÃÂJÓF !¡’FBeAH|TP±‹Vûp{ƒcÛ™2ÿ~¯?’É´3ÓNABª”:¾÷œs¯íãLñà凳‹/çAãZYfÅðàÈèÑr‡P7h,w§³Þ-ŽžÍrzï„“¼<Óm‡µûÈ/ ·VhõúÞq×hf‹<†dEž *ÍVøqi:a�¨í 셠ج@PØòÓÙ–ÈãÎp&j7+‹=ö“m’†(Â{BAÔ­=�5Oaa)µÖlk)'P,Ñ”«W‚Rýp EUŽhU �B„®¾Æ€yÌøç,ÿ,ò€]äÝa¼ÿ^#N¸«O/7ÇoT øïWJ£¿#ºýÿh¸”¢{¾�z˜#Ò€ðð…ì|˜8“ôﶪ)á<¢�k8i§•å°Ð¿†´'¯„k€¶fÔ¯±'i¨ 3º&•О<Î""Ë¡ÂÖ=�E’¨íæ§RîØAÒçz£lКR9ËÖ²õ"Líìå€Ã&'`èA�ÊYjSÄP±¿è = èÞu=E¢áá=�5ÅÐ0`|)èØi•yEëf­v-v€KSNœŽ›%J;þƒ´Ë �ª½”I÷¤ íÐvêña�kÖâ *⇕äs¸j4QRÂ�£æ]Ý�"§SáQˆ6±Á‘ìŠ,„óÊI`µ ´+íü¸ÓVxñ'T¬â×2}Ì:[XèhgøÁ’›yˆŽDsf=d5ª˜Bøí¹Xl<’xÙsh´dB]ÒXò%±P”_S….Kï={šˆ(ihVN›ô·“ÚÚ[¬Î‡ÜÓçÞNM.âì¶‘�Ñ—Ÿ´»ƒé/*¹1þ˜¶ïg4ï©{¿MÎO»ê>ꉩúi5š^„­¸ÃO=YïxòÓ�<¸wƒ_æ£kéú›gÃN[ŽdWö{OÎÀ€C7@²Ó­nI%ù¹`6SªpX§ló,žßºæ�‹\Ö[Hº®Þ’øW½.¬˜§þæâ²qvœêP˜›n˜€'F¸!Ç!ªž¸á��½ß³©KßÓ�³TÔžµ:è¤Q%ßxjÆúÖ6º ê†Rö©vm÷îÚFþ›\mÉÖ}9†p5�*§+r)–œÇ­` .�ϟÛâ’·¸÷·ÕÂïæ U9AÙiY !¹Úe•%|"ˆ[]ÁßÑé.þ0ª:Hø4»óÄgþëDoðŸJè?Ûq#!K]‰Èt­Óú#ƒ ëo×3O?VòøkèN¹m3ˆW�‘2çxœÍ™ßoÛ6ÇßýWÜüÒ—$B  Å€›%C±µ3â Ãiêl“H�¤{ýN¤$[?RɰEž¾"?<Þåø—ßþº{úgu©Í³Å,®¿�%ô•£eÀS¦ ÚÛùÞn¯�GÔn…Ípq§ò‚qûˆ;�Æ%_A¡U�Ú 4qä­fqT©mTr,µß/ŒeV+¸ùc*d>‹H–ãí|Àøf¥1Ü*ý�LÌ|G¬|ʇ¡ñuŒIýÙÀ3fÌí<ý[CŸkº´¤ÄU28ÍOõ�o]Í ÁÝGE©¾€Æž™l“¡›�™y‹D<×#ÈÍõǹ¿i ³ ˜ÖìjëÖ�Æ�$‚¥–¹xJ…9�…~kB}­dv¼©´#x(˜L0¹äðMa€¾¦Ý[[Òío™ð#šBIƒåÖ»>J  h·OL5Krc½çv¯^„Ma+0Kê í«-÷óªjò[5êuz¢çΆMªº7hºªê˜IÉ¿eô°4¹ÈÙ¿X—P$wyöÅm™éX,u6a8UR}>£Ø¥ä¥®(:q¡S %u–�?k•_”›¼÷Rþ�§ 4 Á¹x‹ö• ÿ§“ÒÒ)¯Ñ~Ç)Õ}×<ÞžjŸôz¿)§__«™�Bî a–y÷¤3Ù5—bÌ›fÓùâëaG˜/šõ f-WM~ÿÑS¨dÐxªÙZ�3°šF0g¯ñL ëÀ+49TÌh1©“÷N‰�TåÁF=�5­ÔT‚¥m`~^rÀ½©½v–½“ià‚¬9dÿ©ø”¢gø�ÞIvÈû˜l…aÚAi¯r:4PlŽ#4{á}7"Ô§‡ÐÇÿf†kZ¿1ïlF\iøgï�0L»ôÀ· »~Ãö¤Écʲtâ;ÀÆ>ì‹À3Ù>õ¦“ùš¸ #¯¯gì©AWb@\—NñФÖÎy„Ú˜…ff`öþù´KšÓÎ%` �ÁçXPòC*^PwV#ªþkˆüÿÿñÛ‚¡Š� ”27xœÅXÛnÜ6}×W°Ê«½B( Y€ãÆ�Ѥ5²F‹>”4Z±¦D�äîÚþúoZɦ/i+°W"9œ9ÎUùw?ÿvvõçåÒêŽI@k|t )©Z*è“t«›ãÓ ç5ÓŠ3Ñ ´Ò_`#A)&ú�—yæ–’<ó,JQ߆ßJSÍ”f•ú‰D·"M’SÒÓNÒEZä5¼~ˆ‹Æù$HÅ©R'iûŽ4 ÿŽq¨qk%j˜aÈË8›ÅXâ< Ã" %é©hO)*Ü@ä¸1qÄ5Ûù�:~—ºý§ÄI�г+D”Jh¯¡OJ"îàdÏt+¶š0­ˆ–”õ¬ß�šjzD®3Ú·×éj¨‰Èj†rh_'¤–b°tº ô*9YÕ€H^м¼%[˜¹M•Ûä 7 ⚈fdû8¿ ”Sâùƒ›Ó)+çØ'j!œ ï\ŠîÈø)«µ=°M«•[¨ÄŽJÔo¤Ùö–ÂèÄ*s -Pƒô´p3pV¡€’*¦’‘®Ð4¸}àÚ ¦ŽF *kRÃŽÑ‘µ!Kì5•.ªÃºª(‡!Ú^Œ ·2ˆ^�ã/Jrç¶L�“)ö õu8Þ¶×FÑ“jDÑÑÖ±;´.6„³kà¬÷*‘MVÖ𜥔[;î¾"c)Ðw”3´4#MBô}’Pu�L|Cý$ü¶DgÓ[‹˜›­n� BécÜI9¹ñ«öI-n•°^›óqã ú¡;Ê8-9%ˆ·B7‚û¦bì¯Ç[Å)~K|oZÌi}�n†Îêvt./Wȃ*jÆìÎ4z+Í¥•[ƵµC<íä`¹ÒRô›b ˆˆ+�A(óS !ûZÔçI:u‹•‰Èoæapî6´8"ÓÝŸiÕ²¾~* nÇÏ)7CZ¸§ ¤V†1+UÔ&³�¤íÛbäñÿ¯š�š�²Òârœ0ì‘Ë[“*Ìe˜§,r]û0ü-|ßDhV—á*~EWUFôtìÄûn„âÿHA¬ç#(›h”#ÈÞ�ñÃÍ€fõ}¬ñùæ@y°¼WÿŃÍÜaÖçð–Cw†Ágƒ~‡!sÔ"j6:Ã{Ñã"(ïâè·ã¾‰“/ÿœéÏ6¦ÅøƒéVl\7É34ëLV¶ApÌRËá|oÞ¹Ï�i1>b¯“L´kCë‚ ±FElø…äuVÏÒu€6OíËa\³M‡ íãi”�•ËaûdüpËóq í¹�Ýè: ©¥¡^ôXattŠv2|ö¢jo9Ü>ŸŸ _à÷½ž= =åC‹vj1`¸¢ó^†å�4Ue ¶äYž-âÖ ñïÏÄ@¯±ueŸ�=k•œŠMÁú:ê hƒ%Ü›x�-(à¦Ûš—´¯€x�ý¯Fu�óAÓ¯�oLóŸ|+5)ÃT\«´Ÿ™±)ê÷€f\a~À)ìg_õÚô|ÈvÕ냄Go›ÈeÁ‡ZïJ¢ MØ=”‡ãTìã¢oæL½Î¦'Á‰ÐòNO�….⹞Ä}b ‰ó�×�L9Fõá‘§…y¢ù˜µô6ìô°w�È|Í!tüÒñàû�-¢VÿìVŸ=‚éÌÓÂüƳg7l}Õyø åÛùæ­è¬€´_£®ƒ‹Ï‚4fHIÃ8¬¢v—ùϘ™û>ú7S¢–- t �ÖtŠ^�•@wxœíXQoÛ6~ׯ೎;a�(ì;TnKæœ)ÞÀùdÿ¦“…�Rn’ÍSNˆ§�ØÜm@”S”_°BrkÏ'õ+VYü›á’´º|Ìý36_r“­ßÔFߨ9›çY�g쥗ÐùïA`vüz–ßÍS3OdJÆ>†­ÌÕÀq¡•ViìXÉñ¤VÂÕ ,†ÏÉüð%¸©³”5¨Yž$Aw)–�£��½š¸àä}'¢Z� ®5Êz³dÛ¨„-¡phu­e)Ô­w'ê„rìð¢fF¯˜®ÆÚO{ëÆÑ°ÁmRSµR×p),Cã‹­¸íb‚•ÑMçyK¹.E–°±3¬i­c9 Õ‡œuç«bD%Ô<ÐÞ?P®ÚŒ(XÃ�n±˜Üôc†|�áÑÃh/áÏ›¾wÉEg¸Pè VvJþùüBÉ>M¾ðŽç¦lUk 8š&h�È¢¢FeNJ24aŒZ ç<(Ë×¾(W%SÚÑz¡­p­³`k'Élvãq-0z´X‚™zyA9 ¥%• +¸ [P?JV9õh�—-ô)Å™„%¢:÷URtjŽž‡„ð?pS—”ùÖ­n³Ï€ÎH«±”Óø(a ¹§6P=Ê=Ĥ_ï—™<šWëjˆ¹l'ÊõÑë9àpZ•ÚÚÃ9•¤$Ô£ˆôÝ�EƒÊÝ:ZÝ<ȧÏcÉU.GëO‘Þ®¹¹ÄHÿ,Û‡þ¤–·ÃŠok�R,~Œøªw?>ð%¼£õnë ¶¾¾‹‘râÇ]-ïÈ6·1Ü+~BnõÝÎsæ ÄmMôÖWÌþÑ"±– ŒÁÖ–£…jÜ̇*=±y5ýÏúŒll}—šõï4¶çîkè›�ÐqÊ#‘EKÔšF Bº±q¯û„½’zÀ aÞ«ìýý3N8OìŸ'I<®=‰yÕ 'w`·ò‹&#o]çÊEÞT; �ùpÛzüûX£°$›¸à¬w5,’á‰ÜŠ%ÖPV”AA8 Ú?L�aoß]îO (Æ‹.²ÒRê …¢5»»‚ø‚ cà#oö,ºˆ>7W†Á»Ž"bA©åW¹6%˜~i¿—nZcå #nÖÈäÎà�•Qºú|rúú›IˆÇæpñÊVe~8íd®71B© ßÂÍΫ.Ǫ½ç/Þaˆß´êEÏ쮊„ ÚXèĦ� â­t8¬ÁªUÕDÂjL> —2ÌxƒÃ¥#EÉxÄ/‡Ù0ݾ™Å_Æg›øB „†J‹HÜœÛÉñ}ñäo÷:üÓ6[!'êÜ‚Yrò06LfÏb4ƒriést-ÜŠºhYÝ9k•ÀÑDUÈ#TKQ¯'*…¿r)þ ³¾mú ¨pc¨*_Âwœ¹GH}võäÿóó!ó³åKðTwøÝoùg^MäÙ@áαԮ15cŸQÅîÙkûmCÈGRyÈ@Ö›w”iŒ¶Y†—<ª£±PÜË$ôàc&žøê`kzŠúùà/, Ö G²²Úß9é‹7WêV†H9çÅ�¯çMÛäe<¿>þ"úö·!�{U“¡ÖíxžºFßù¥áýâß" ̇C�� .xœåX]oÓ0}ϯ¸ô�½t«@š„ «cHlLÛ$Ä£ß.Žl§%üz®§ß£ ¤©RÛØç\çœøÞ댟½ÿtùðåö 2—ËI2n¾�qúÊÑ1H3f,º‹Aéf§¯#ºî„“8¹ÔyÁRw‡�­ZÝ`i˜¼A·Ðæ+Fhœ@;Õ€d<ŠÄSÍ+æÅÄ:æ„u"µ¯¡!!“1År¼üwvSæ·¹H�6v0�˜ýòÀú7añ%¡ H%³öb��ÃÌÒç”þ:âL5?$Èëæ*O'[ìSZVà� g4aŠôŒ`ͼ¤åbÞ¬#·§çƒð m…s¡>ئL2sð� » O¿ ùpª•¬Î"çˆHÛKº\ú Íî é&®wQ·é÷¨ê¼¨.Ãö¦(%0cXå¡~CЊHß9ô¾C[heѯ·½¼ë¨ÞÅÝ$ß/m£¬‰saÎŒ`S‰‡Þ’ôŠÞ‹Çœµ—2Lï]ÃȺ+Þ½cŠ3Ã�ã\PFÔªóCjô"ª á2Ð •3PøÍJ‹f4ÍÆ âGˆx–ÀU^ÐBªÍØÀU‚¥Ýù«’?õêºloÔuÙ»Kžr×¢kdÿµ)Y…æžØ;äû¦w“Ö©÷ì'?и5+¥¬ ÕJ‘$s&8'1¥gè` /Å[Öƒ£ÞËuÒmù“ò×Û”‡G¢ƒþk Þ ØàÞu`5Lú«4Ìk ébÀvi Å °C%yk·%-êÁ¡O¥+JžËÕÍ·÷j/¼wמˆÒÊ¿Æ>8j»özÍ'ŠÌ9‰A�…¤¾>° òˆÎ Õ¬–4ùÎÇ'�(—ŠnÓö2y&A…^ù–�>£xÌ\׬QÇÉ{Kò]ßâP�RÅ““Dfòö]îžZÜîøO™0•%_–œ¤wùß f;לtñî]íë‘BúKFNÓ&còwÏ{á½ÛñD”]c>(\5é ×¾•£$ƒYëR-Ëœ²RM>¤ÜÄWÍšÏpäÄ1ܹú^P0ä¿{ß�ïÝŸ§Â´>—«wSN“%`¤ó—ò¿öÈþ`˜²ÔÉçÝÏíKèÑïkvMXÖe¿.ʤ¶^w%ÔÜšì€˶!#×$—„Ó*6‘²éb ˜:Ÿû­ÔolÌytúà '?§°™÷“ºzêÊhHÉ“.‹Q}·ØðÖ/(³eQhãïÅmÜó–ó£øžrT¿ý fäp°ˆ_�‘B xœÍXßoÛ6~×_Á©¯Ž…­(0Š€6ëÃ€Ø šÅž J<[\%R ©¸î_¿ã/Inä¸éЏ@‰¼ãÝ÷QÇ;žóßþzu÷ÏÍ;R›¶)’<>€2|´`(©jª4˜Ë´7›‹?Ó ç 7 W²íhe>ÀV�Ö\Š5ôŠ6k0;©>ç™×Jò,X+%Û[Û¿ÚPõá•~MNYAõ$§DÐ.ÓÇ•Ó"ϨõðÇIl¨’䩪õeZ¿"�84h¥’ @æåI‹%:wëò¬³¶‹ÈŒ§ˆ§IÔ`!ñªŒßG ­¾x•úÕoˆ‡qʯS"²üÐU-¦ ýÆe2ñIZÔoˆ©)jSM˜’]Œp£‰Q” .¶„QC—ÉHD5€fQ¹'½¶6M I0PyQ£¬–ŒÈÍàá»LOÞÕ@‚ÑÀï3@§Éβä÷r4’P¡w -eo?ÌÎQ]Xp¤¡{P ÙßÖ¸T0RrªA{1Úæ÷R„ ŒÁ¨bü«›u ì<"ïP Ùâ~ ½‘ª]8¡Ýê€IŠfOèJñ´[¶áæµ{é0^©ì¢d07A*K Êc‰p½@É�Æí懢oK$+7Ét‰�rÊ[Á¯4†0¨1ü¥Ú/ãw Èâg°f¼ ‘ZÇY4¬IãŽáG²ã¦öû¦‰Ñ'}8ØãFßr³À}á {ȹ/oŽysÒë‰K&‰�†ÀÄ›ëáa�,!C›4°1„³„GÜô&á”k£¤Ø·Ÿ6ZâùÏÂTB&£^¦G¢tisè‹£)êhlÓbA¦æW´ª¹€O×@•…îíb˜(<Öi^|ÆsÈ3L$1û Âè�çê—Å`õñÃû©Ãe8hç.-n† ë ¾´ùŸ–.³U䆅|ùƒ.^<®¾\÷íM<:-†RȺþ¯cÐ�I{a†pŸó€p�UKÛMœŽgQ[�}�žø‡�l,ªãèèˆyHS÷TqÏ ý–o[šî1ö6¤kL»÷Ü'º³o÷ªO‹U?‡vôÀwmKâ-ÿjcx|ŸÝ]+ˆ€m¦ßcAË4ÖÝš3ÂØgçðf¬Ûi1̱ÅÈAT¾ZRg%ñ¾7]oÜ'1¦Åìôw‹¼¤³áy�%¾>úLˆ°0š#Dþj.Ó�-ŽÀÎü­»Ü~0ÛK~ÔWx‹ßJÅ+ÚLKêìô“¿ Çc^�ëΘ£Þ}é0±û¶æÎÏ?¡öê?š¿²B0g§Ús•廨PŒµy˜šc6C‡Òu ÷}Є*NÄâ=å•ÅKÞn�¾“‹WÇ•ý¬{ãÔø© ÔÒ±ey¸9!0o/¸YC[è[:t–§šêåÿ‹§P´=W:¯‡äìblj¹§U÷�ö<©sÑôœÛ´^g¯(<üá/ØìcSÝÀr6ª³ðkTæñú7’î@ ( N(Š"�”H¾xœåXmoÛ6þ®_ÁùòuvŒ(PtŠ�$Ͱ¢K4A·}¤¥³ÅM"U’²ãýúÝñE~S&7ЇØ1�Ï�žçîxbúÍÛ�×ÜݰÂVå,Iãð?*°œe×ìŨ±‹Éëѷ–0»VUÍ3û –ŒJÞ¾eµV5h+À¤So–¤Ó7Wù†ÀœË­0Vdæ ë…Aû$åLò .F]ÖçšêNC.2«´ÍÒ)'7/;#Ü7Fô—h[³¬äÆ\ŒŠWlaðo‚ÿZDÊTÞý obˆ–Îg˜s ÁíL§5¡ÏÌA3µÀmÑ.ñ«¹XEï•™¼ù —¬VFX±&¤…%n6/¹>gì¡fë¿käw¢d¹9˜Sí#­ øÚô²¶o=m‡ ¼ÑÑf 8�º Ê’q­ù†¶RcÈà ÜîÁ(ü¦VÒEÙGà®í@ôíCv“¹ÓÁ–­¸|^B…¤ ÆÙMmD©d]Ál ¦Z´c’~áåb²¹-"Ui’¡öl3ïcJ*9‘°äÎÞ×è˜e˜}r¦VÈàB«Ê!Wè¹ÄoÈ�’ÀÖÜ$,óÁ£-™!Íï,q».¸u{ü#ž�‚�ØÎ“°RÂAÁ–X+|ÉÑȦ׽XV¼O-g4�VëX©{ËeÎuÎrX ¤[ɯn Z­C>³µ ÅQ�VoºßÂŒYcH43Á©øÛyfÔº„|¢«7µazÑΣ}*�Í@"x¨c î´¨T»GßËæç(0fð…¶* �ÿ;5ÿóüÞ‹ðÙK˦Ì|Æ^0Z ‰âlÇTX)T­Kn‹Ñ G+¨fD;ê•°¿@KTŒÎ*@EþЇ �¡ScEÓà?Å�aö˜NéÃyFÇD û�] n¢ë3‚âöòá×Ë«öWµTÑ\n¢c…Qhr*Y)$´3m2@†`H½âÂ&CÔq†¼Ã‘]gPÛX� a]54Òñ{bn´ÿoT˜<°iÜiƒ^²˜3¿‡Ü2¹¯[©†› ö²²wDسjV8=Vâá¸5q•�r§vI¬"-·í‘γ®æ¨|Ct…åKÍ÷Fj‰Vã\F¡L€÷.îpPÇuè—àÐ~ Ža;Zf»+"$ñ‰a¬n2Û`ss",”ylQ?ˆ8ãVîIˆ¶9âï±B>êt´wÝ _·Ë�T•ÀîëCC™ºñ¥{'Ýí¤(¯ï{aMX×V9Ó<'À97¨q0Ö@9ÓëÙ’7È+—í<¿.DV½8˜_d…\ºZâÌß•;Ïi®¹…¥Âìæåéw'�›J£'°»š+.no²í¾Ó'ÒXÙað¡[ î²%œ^næ”T÷ wRÝ<Öèò¯»†éÞ5Ô[òà'_ËP!l'Bp®ßþÇnl4— ¿:õÚ¦Ý0ðÝÍnÇáýÛ.¯ë’.¬Ú列;=$Ç^ËpòÊiô‹€óMhF2v¢àÈÉù­6_õªÞغ±nÂÃó ›ã‰8?.H° ãp½ýà4+ü¨G½Ð;eJGX®§xRz¶½G>�vn�§þšú×�Éu‰.�’`ªxœ½XmoÛ6þ®_qS¿&6¶¢ÀP(º4À†Õ�Qö© ¤³Å•"’²›ýú_$˳®Û ±DÞñî¹�*¾{ûÛíýïë;hm'ʬÈzthÔ-ÓíM>Øíõ�ù’æ-·Ë[Õõ¬¶p§Ñ®äæãªXZV,£ŒJ5Nâ÷¥±Ìrcym^Cz-1eÉ:¼ÉS,yY,™“öÃÚ‰�=Ô‚s“·¯`kè–ÖÖªÁEõ„œŠyîbÙ;‰åˆ6Cß+má#ÖViX±ºåAOë³°¤áûFg®_åAÊ ÒZ= TõɆV‰ÆîƒÂ™"èˆ_€mq3�V}� pkÀjÆ%—;h˜e‹ìh†Ff‘ÄþcÕ ÆI²-ÆUuX ÚV5 ¶“Ü¿8sO£¬hËgÄÞÀÁYÄ HÄ†ì± ˜4ÔÀ*5Xš>x³®¤ìÌAä2Ù8×$·[^s”ÖxfàÒ¢®±·aøµD1ʱ´’é†ÿI ¢ä(‡ÖM§”þ‰¡¢�Kµ¡Ÿñ9â’ ýÿø&4ï)ÿ�sÎ|œDèàÉf½(ȱ8Õ.µŽ£§Žø¦ò±gš»_æ]o¸P2/ãK ÜÏLl¯¼¡Ê!r‚' ·|O%s¨.ŠpÃwËKÿH¡ÛÄšNÅyÏCá|Ö@¯†¼\ )d+dÏŒå�è[ò•¤½˜892Gxgì%1þDg^ºßµæÝÆ+:{É�Ta&'^gTSÜo ß/cw1zŽÊ¾¥f;ÈÚåÞEwBRhK ãé8…öþqhèΠØãE£ü«o»ÖLSa#�Úó™dô'êèÞо=‹{o©kÞ)Ík&æÇ_r:�D<3õqÝ3íþ»/=•BlÎÏÇôü7œ“À|ÚÄn£8ŸIÏq„Þ�Mùñ�¦’Y?c“ß÷‚‡;ÆÌ,<öés–cSõÕ-܈ÆmFÿ­;›‹L»¤á¦¦³î¼$§“@`LÕüp·Ôá2 ¤+D{ñ/üuSbòã=%uxÒi[D‘�î‡á¾Â¦+æ©MÙì*}isÜ¥*�W«Ç†¸%tk‰†L˜{ð{Úèâ·‡ ƒ7l�^E^N¯É^‹ˆOƒ=w¶¿ÕÓ)p‘ÜbËøáh>IýS&©5 Ò „Ò‹.�–`ÇxœÍYmOä6þ¾¿ÂÍ}Víé¤ê´Dâ(•�Ž+ÔªŸNN2»qIìœíÝ…þúÎØÎË.^¸¶J@‚�=�Ÿyñdì,~øå·ó»?¯/Xië*�-Úð5XÎò’kö4YÛåñÏÉéVØ ÒsU7<·7°Ò`ŒPòN,æ~p¶˜!™*Iä�©±Ü cEn>²“‘k¶àLòN“(O’.æœäýtŽÌ Ë+nÌiR~`KƒÇص89Wì Yd‡e¸”c_Ì™¶¨Y&$×�¬€\;³ÈÏ–J3݉˜ùI…Ø´Hjsü!ñrΘÇq`a7ÆTöàZ9×Z€a¶¿Ÿ…Ùñi5þV G¶ŒË‚Áô£-…\…Y�†Bä¶e—…9bÙÚΘTÖ-£2zƒ&RÒ0aÙ–£…µP0%O»´(ÚlAœ¾eÇ0H4(¯ªÇ³ÊÉñPéP£}´ª�&�â¸f¦FnÐÄ}Ð0嚦Íɬ7¹n’¶¶tr½"¹7^§7ØR!À%ãÏÙgìŠj|‚œ¯ ÀRU*ƒ°5’ŒEJ¹!î¼H”·Î:I~Iãtʹ$óeÀr­Œ9ÞðJ  (‚ÊvÀÑ资â#é2sZÐÀ·5ÈÈ:¥ÉÜèfù=�s†K»PHD5Q$:Y 냃ÜW`ˆð¬‚#¶-E^’¨­Ò÷.4{‡Ø’[†Ñ_UN9a�Óv`ô…±ZÉUzKáV…»dH3Æpƒ––§É®�O(y¼Ûß­ûNàéʸâ9'|ý \“¼Œ6mÝü¶åw½8ÇMÕfi1ìLØôåû´“ÝZ_­Ð÷“['I¯;-€rÞS~#ÒS§ [„œñï$¿‹r�|Y×_P˜IÒ¶å— i†ÃËp_ �É¥è!Ùí~Ï3GdcÀ;/EUh�IÚ¶bðÜX€†=èzãb»æ3¸Æpù‘ !]šOšË¼$G&é°ƒ÷‡Û’Þlˆ”eŽÛFvëÚ^û¬¯4ºvЋF%k4"…\Ó2ö&eù†’ÓA¾”<ìâv¤x7ð&Ð+AÑÚ¶¢ûóxCƒ†,®y÷Ø€F�.¢$4Ã7ÅtÈÎñ}¹RTí8|}7j½n”áK¼è�úm52Ö°6–;·M%ÈÏ{”ç#3ÄdÚ@?³SRº£ãÛÖó/ž�pº pWÀ¥‡I­L¢cíc£»Ðš’h×<ˆÏ|[c–Çœ¸&Üá„ «šŒgCìÑC¢oÎ~˜�jpEÙjJÈ7ÂÜ{¬ÔŠ�$ú”õ韻fÜv¾~¯ð¤UM�ï¬jJºv4ï(ƒ§RÁy¸á_ñú¨l†:Œ ûv�ûa…™p/ßÇé/$þ�>M;7¤Õé4øµÍ.v¢zÙs²’5�X^z(Pž�ž­TÞn_¾ìSÕ1oð ôŽ’ãI% ¾6þÈ™1*® è~F±VG]¼cx?ëuôìðÓ¡Ç$énÿÐéÈ<=`l¸tk02Þ›Pô º[é{/�äºr©E:Y…ÜÇòN™Ü“œãDÞ:”ÎÍ`â˜*\<4O“Å~ŒÄé1%ZÎAœHÏ;EŒÜáAÈ,•®û@éHÑtÙ2ÞàÖCØá7 GBDCæímÙË·oá¾4\½]ùÞÿ¼wÊ<`�€?鯾&¨Ý­â.œ¹¿Fí¿ûÎþä¿:øû•Qú²¦;b<ëB§Ø��GcEM©•´ë  bÆT€*ãßCºár…ðŸ�¢å? <…JÇ?îÊí1Aol“”~ãÁC—8ýÃ,<Œ•2˜‰è7jªŽ™ Ãgû�fLP†oÀ}aHÒ®ƒw‹ƒè³¨�ö…B†¥¨v�Ú¥šyø¨8÷ß+ÿ•=^ Œx�™t°xœÝXmoÛ6þ®_ÁyØÒN„(VdŽ�¾¬@�v-š,]>”tŽØR¤FRö¼_¿;’’%Gvâtë€-bI÷λçî8ûæÅÛç—×ï~a¥«ä<™µ€ø§ÇY^rcÁ�O·8~2Iñ½NÂü¹®jž»÷pcÀZ¡ÕÅÕö\© ;KM2K£¬Lk’üãÜ:î„u"·gl¿ $Nfœ)^ÁùdŒô¤6禸hêZw¹ÓÆNæ³”“®ÓQÇ™PÛ)òÔ,—ÜÚóIù˜-,þ;ÆG‡s]Œ»|ÆfKnæ:û4Ké;g³l¾CG6g�†ä?ÌR/z–Ö¤~Î^NæJ`6°³eàgzÁ8“B7Œ"U!§®A¢"Ä©•cÙªäŽ ×¾´"ƒ…6€_„†‘1k’¦ÄØ3¡X-ytWë6êâKT­©ëPV´ÍýM`ÐÑ5Cñר©r «9¾AÙD@ÞSv±E£¼ß>È9Wè%I/Œ®k(P|L²Õ­DŽr„¥X c´ œ%W7`Û õLŸYg´º™_0.­ÆdNã«„1¬½ÒÀâ|ò†çÈ_£SŠ$N|»ÎØe5™ÇTuSÖçÖ�g¼šÌ�·ŒÖ¯—3öe2Z  ¼Ç)ÖÈ]�“â.‰d_ ë—Âõq£{)¦�ã÷ç;€ã]`�™ok­,0Ìj,‘+8BùJ¸$ælåa‚L­}ËÀ‰èÁ=­�5j½Y²©TÒ\©eÈkt§+Ô¡c€iÉŒ^QÝ ¤ce¼r ã覽w›Ä,)#`! œfŸP[qÛÆ5,Œ®ZÏj†R`-lh[çœí>áÖCº¨‹£¼žÌÖäžHÕT`DŽ0ãŒøsc¾Å¤¥�âzƒâÐ�2ýN’cÎpAˆàu§äU\]rT|Ô;žIÄÐU©1Ìhš g�RdQ^¢0çñ5é›0ÔŠˆà¼R–­}±yòð…x®­ T;K<À9‰fÃ�‡Tcôè[ÁÔÓ#p{Ðí#¢âB |Â>ÔUL½6ŽÆËºDÂΉýD �Õó$¼ià?•ÿb}1�yRÛ;‡&¢»{€Üë>ÂA»ámðt=Šuÿ¦þËLžßGÀ¹âæWŒàgÏ>�4œº¿/AJQÿµªÎÕøÂ—ÒŽÆCÚ±ñt=„„‡‰u¼á šø©âÀf³qÀ+x@u=ÇcØ ÄMIpSMö��®ˆÃSn †-µ/Ò�Ó}ÞÀY7oR¯ê›õ5ÛË×î.Õ×;UÇóãˆõã4µqÒt+NÃŽ5ô ;!q¯mݪáý],éOìb'í ³'ôä3Ø­¬¢©Ä[׺²G�7ÕNûëÕ}lëôßÖ5K²‰ ÎY—ýÒèŸÈ�Xbåd`i‘#ဈ¿Ÿý»»ÈOŒç9Ô.r¡¥Ô«ÍJÑ:¾ \�¼æÂØòfGÑ'B¤àJŸ xwŒ�¨)µüS¦ nÝ£­L~¸%KÜǃk—}ñøà;‚Ý�§Û`†�©3 fɽs¡« Oählåè.Ñu°±ô–6˜–ækÎ%°qTQ •Z”ëqLáW.Å_a ·MEHU¼Ì óñå#| ÂÁx€Ô'_7"þ߇\Ë—à1ì®I·#ü’Ý>›÷ÄìîB‚Ú5UÎÙŠØ=>m¯ë!kHä}f©Î¼ƒLcÄf»{9�éì̡֘‘Þ=¬Ä�{kÜ MњЭ[&b£O`îâÕ^¸¾ÃÕ��!<Íxþ9ÔZÛðx£ïŸ_ä^}|r°5éKý:7Ui¼6OÃÅüßî‚ Ö ¿¿’=�¤~1çxœÝ[ß�Û6~×_ÁîÃ¥‡sb4@�"ç,�ÛËÒt‘,лGZ¦m"’¨�”½î_ß™!)Q–Ö²»ôò‰Ró}ó“Ìâ‡ÿv÷ð¿û÷lkËâ6[„‚¯àG),gù–k#ìۛƮ_þr3‡çVÚBÜÞ©²æ¹ý,6Z#Uõ …`µVµÐV ³˜»qÙbîå-Õê€Òº5–[i¬ÌÍ6-&d Î*^Š·7£Ã_}jÊOj%ÌÍíbÎq�×ãûk‚Ô×0®fyÁ�y{³ý™­ üy ÿ´ %‡Aã"Þ„½ØbyÛÉ[ÂÒ4k1¯Qò-ƒwK¡™Z³ ‡0Y1»Ì‚�Ì ZÉ]Ø@i^þ|ãæ½cµ2ÒÊ�€)Vl„žÑÄêH^ƶªX™[j^å[÷2c¼Z±BðTƒõ+Æh³Œë M¬'Iz˜kÁ­X͘QŒg¬†g•e¼Øóƒa9×ZâlV¨=lÄïÇnyÅ„ÄpoÒÂÈ­,V0õUƶÒtƒßa…ÕKU‡W^ sÐÂ$Àw^â$Àa`*€;yC€é�¾\p¯ý t¢lŸs»½iIôøz1å-ƒ§Z>¢>dµ!˜ ±¶„(þCËÍÖ:‡e3Zð}‡˜¯#¨þZ!ó–ÊnY®Š¦¬ Œ;†%û‹¸ÜC&QqÃRa¤ qo.é„íDn•ŽU^E‘Þ@ÿJÙNéxD¥Q£̿Ȏ‘“z�§Òn_æPÇ¿oeð2ä3znç´² µ‘9/úJç¦ÁÖ þVÓèT"3S’ ck^˜h4Ò=�òï{>Q⾦]M48™»éɉ)°yä$14 Œô–7à~U5EzQ€�æ”…!¶îÛbؼ9H ,@X�ΤÏ(ÒxÉH‘ýP¤Â­f¸r\#Ê>òx5ˆTCÊÕpüP­ÄãE`ÒŒk êa¥ pí93Ö¬oZ.—ð#aÅÅŽëÛÿ.æø£/ëЛ|F®”’gD�00%,NÞH\o,¤`ò9�ä”îw¼hýæÜZŽ»Õ˜³õÑç.+cß í¨¥zY3˜ÓF�O¤Œ`¿%\N¤Ädžäp—kæí— ˆ.qP˜©í¥‰Ò2Pp·�l*í\©#´ð˜f‰@}¯õ5q;2%¬^ฃ9ö1ŽºNÆÞ×/˜u+RW®¥×R5¶­¤ZËÄ© o¨—{­–|)ÁÁÎ&ŸžžØ‘ž\÷uEtGg˜&«,ø:Vk‚§ .F×€gu>Zl¼{óû=®zŽzàŽÓ:ÎÏÒ|= I˜B'oˆ>OoR�Ê�éf�™->TŽ3�ÛSRºS�#9m --—�u¾‘À†M¹ü.t÷º©ÄGiÎ胇‘ÉZá�À1Ëk(M¡d1%ŒQ�Õ!ÁæÊ`ó6_ˆGÄΈo�¨rм7¨F­ªƒÍñ¾Ä¨á”uµgøšöØ uëR ëcha5 ]P­C$Ø£Ag~¨Ä%í^@rŒÉ®Ô› µê–tѸK¨!ó]¾ä&õ·Åv¥Ú¿ ½éø­}½ç�„š¯ÀÓ5Öo�æØEU�~öî,jqce‰'q‰Iú®¨·ü<–ÒФ4õGê0 LÌÈiáC¬è¢y3ù"òv­ìB”X-aÖ£k�P˜9²–ÚPi=�£$0ÆXøÛ¥`8²©PÀê½ÊGÚ䯼£*yQ`†fš¥ã0$x©j¼p&FÜíô‘E YkUzŽu:ý1^ê?ý=®©ƒ×«Ä£�ÂUŠœðKÎv½¬™3>-Ïž’~²½ãy`Â\ß 8I³/GƒÝ‘:]ˆÍŸ¢[´^øãÏÐãvKjþSÈú"ýã„khÞÉï«Ã"rS¡‚¾ÅŸw‚1˜q ÕOŸi|?j?ëìàxÂ5”~ê4áûQ÷ù㣳®ÂöSGåíËï‚ö{Þ£rIíùPŒÍNÉø"OBƒ7Ü Xœ™ðnÖÿ5d­ð¶ÆtJÔž.åîK¿Kb†— ‰”|YL^æ9û Ý¡Ã�pé ëÝ:HWã|ö=8ü²IµÇƒS)½/óôõ�¶a>¥ïãƒÙè²Z�u•ƒ¼Î‘^rš×ͺ‘^,|ô6�Ì;j�žŸN±;ë^ÏÈnÇ\éS— ¦ð¬ws°[KBnÝ%Ûh³þ!]…G6 {Ãïk(PÅêB?4>-zOIÂFF> ¿ö™þȵsVr½¦ã¨£³¶�îïÈϨugÛrl&aWH„1OO˜¶Ù–yh÷l…×x0:­%×u°{EQ+D¦oMÊãÚàž4¯ÌZéòl'ÙÎHí)#Á# txÉx [,*61x<ß3œçº©rÊ9ÆÜhåÐ?ŽŽô¤ Û@À.Ñ c÷1´¨é„®¾†`ç™…÷îz~7œWs)íÁ8�g¹þÊñJ¬yS¸ó}ßaD·ñ¢=œè‘`îÿ§ÅÜýŽ?Ð F» C“CˆL�‘Ùxœ½XYoÜ6~ׯ˜*¯ö m (déº)àĨ(òTPÒhÅ@U’ëÍþû]»\§— ØÅÎ|3œK›~wÿi»ûüø Ôºm²(ÈJz´¨5“ õ]|ÐÕí�qBûšë³­h{Vè�Dü™í÷(ÓÄ¢4ñrQžŒ¸ï3¥™æJóBý�ƒÄ¥ :Öâ]|A�³4aFÎ!¥´¥= Sê.®ßA¥èï–^5,D‰ íi’“ Ëš&½— (;…mÞ ˆ rÃ]B‰W\t I‚ŠÜ‘’?Zuû.vRÞƒCPi÷Aä_�´LJŽ t�N*0òâH4AÚK<ëJ8ÖLC/±ä…6À:ÄRÝ@~ÐÐ m¥Š\¡|f†¬€k82Aŵ&ƒDwcÅ(Aì�5Õ9è[QÝ’ÕÀ»JÈÖžÞ|•)RuŒ d)¼ÄŽ|ÌšæZX #RnLl*)ÚM49Y"Ó¤ˆt¹®ígvá6¸‰B±¥vÕ/Þ•›vGÅÙõ©ÉV«E4ö¾Œ¯%ÂÁ;"Ñ„!§M/;UZŠnŸ=!k” ¨JüV@�\K¬îâ âÆ$Ö›yDÏá³ìÂçþpv«óóoÞ\ÎftÙ¹<|[c-¯è†Ì5šƒNìUrœ]%]UòRP*R°$�—Û.Ó­£J©!ë;M±¥|²×o³sµ3—õRô(5e“Õg�ã†N2ÞšÆr[ª´ÌR]ú:ñ÷¥¾¹àØ<Ø«Š3÷tª|I1 èwêÇÜâÒ‚ñbùxhÍ›Š³aÂC´¥EäXÖ€âq\±+Þ«xƆòGJ~Â4­ƒÞ1„”¯ë`ºÇŠý™*TœÍ^B¨§"O™ªö¼°Sõ¶¾„–�ˆ¤Žt»Äµj�DHïÍåû7=Úì+9uK�e/¤éG#2sç¡íØg0�[ú=6šm%×(éRïý´ñÔ7.>®CfX?õÅNKNƒ��^é.¨ÐŒÎöf\l…ÀÖ�2ÊôndE=á]îÓAJ±§ ¯”(¸M­8 í¾”¡Ï4Lœ�£>"v/ø;zÒËým> Ýæ¿t¶Åˆqé �5Îüâ“�úxOaEYc§96Na~pJûæßÜÛ7p"ÝÍUöÂøÀU±cèDÎrN‘u*¥-R1Uh$¤FÌyGZvËäžSt¹gørøÇh ÙT˜UÿUà‘Sˆã:ówäûÚ ØÖ�EôýŒOÃú%´_i´ú(X},/úò=X£QÏËÂ?êíkX@‰ñ•·”&éý2œöžH˜¨©|¿gŸškà£ïÒ†£|@¦’®~ùBúÉq˜Ø4,¯–=¥2¤Ôq‹`]¢Àà%o)& MŠF Þí§Þ¼tò&Ø2ÿSKâ~Áù FV�i�¡VlxœåYm�ÛÆþÎ_±UѤt§&@ÃÑ 0ÜàĆsM�±"‡Çí‘\vw)Uùõ}f‡¤H½Üél�Q ŽnÉÙÙ™Ù™g^¸üÓß_¿¼ýåÍ·ªU¹J–ýé ?­ÒB;OáfÖ†üêÙl�çÁ„’V/mÕè4¼¥;GÞ[ß:"õ…Âf~¹¢d¹è˜­m¶cÖ_®|ÐÁø`Rÿ\=ÂÔÉR«ZWt3;I{Ý8ÊLf«åB3ÿ¯N ÖÓ�çW kTZjïofÅ×*÷ØœÚìŒJÏÕr£Ýj÷�ÁfþKݨåz5ð[¯Ô_#…]ÿ[æ²ã_/eùÅr¹/ÍÑÉøï Ëðˆ¿ˆÐ�Pc‹<øí„PŸT–¦ùæàü•z#T*¤pLckOjkB¡´ N›š2õ€8;}) ׉ðÍ̦׬òW_Ïä(aslÀNÐ -§<Õ™W¤ÓB9»U6O¦„*³Û:êØt�A±ÐºÚLJéz¯©ÍãC6ž2!Q©­t–)‚Òa®tÈu²¿í¶Ý:]ûܺª“éZˆPµØ]è %J{UézÎe[A-btJ[§2�ÈŠ’�U©¶ ÈM°»­{ƒ²õ¦è�Ò¥·jM|Qz]Ò\m •ðÞðG90Õ!-À *áŒd®?w¦Zïb�EËÕ6ðº±Þ\ùs¼“éN¦Ùï6^5¸y^lÈÍy,«j"ܳLu� ¶`j>+›ÇW—-©Â–™©ï°.iƒS@ÅvÎÄx:t�qxÒ¿°il²x9ÅËàƒ‡�"V9P¢ê¶ZÃ%Æ®3xc 5 ŽßÜ‹ø¦2è4½|-ûÓ6øÎx>ئ�¢ªñ=¸Âêú—j›ÑoÙ¶Nƒ›c:Ü1wG�Ðj /…X`K“Q�’ÈÏÒd–äâRíÜnl3œ­ïV?qè@u Þ¢{”À˵*åçð—SÆŸO¾š�ƒ�û3þ–è¼{EÚÕ|�‘'îօȦÿK>J½Ü\š¬û¾j¬ ° ]š&Æ{N¦Œ �;º�©š™czÄq9‚ío‘3ÚñF•ö²SF_ø^ ÉcÁNHuN¢!44ûU¢6ý:f}*©bŸlà§Ûùá¦mХʜm7Ê/ã›ÒHXÄ@ØÍÚEpXÛø Å ‡P*†cœIïÿ@ýbÄaÈ5€Ä�Nò©Å�©ßÉÙQPÜ‚<Ê1ô˜/,ï;þˆÿ½…ø½i€Á‚²¬_ H¡ÚÖ¢�2e樎¨ɶ¬"p’ ãÕZ§÷sàÑ¡™„!�q¾Ã@5Liî Õ6‚ P!êˬ<5�Ř6ïucz$·ë$ª%ö’\‘²ß:Î@“°%œ1J�Ÿ/tHªÄ{:~ê?Q™îÅ@ßÖ^õézcĨ+t[& ”SNå±nHÙwÖ™Ø9(GãýÆ¿ä”à€£ú£­S”¸}鯥F+)*­·É^Ï;³á2óÐZµc}ª€ð”ÐÜÚ>�LôA¸^ ‹ *ƒ7HŽaÔþÀè¿uf|ÄÙ4ß0[¡„c°G>?Hˆl+ŸT¹HFƒûó,ÐR€¡$­ií‘Îáp:c6¶o ð æÓVÕÄÂ_òûHŽÏœÿOk¿ñ",wŒ‰øæáæ­mÕ`Áəʜ»(Ã!#çH¥�Ÿ-®'·ÑÜ�ŠùIarGáÿÁçKëý£>ÏDï=`y5N6Âéühe²úåñ9ËÇàv]NÖý”YèGXñè#L›:¼ï•ûŒ:(Û=ˆ‰ì\êÎQÌ0®€†\œô÷bEïáÓÆ~K(�yŒgDÅ™"çìÐíб�táï9/'“3•¾Cõ‚Æ”íYŒŠøÅ�¹Ìpzš„q¨üÉMH§%±Â7Ü7 ú>1ú‹qbXtä¹ÏŽý½fGh8ä'…R7‘™FoÔ¡ëUy€‘䓹LW.—m8ÿø¬‰Y’ÑOÅfk�r|#\Dñø&潘·#+Þ?ñ�ùÖÉx��›1·ei·ìl¶á;ó'b1ŠwÕ©Ù9.Eä1�hq…tgxš%«µu¹a髸—? L™£Ø¨´Ûš‚ÿ©; ÅÍìËg™‰ÞûK\.B1¦yö·žæç½®B´ò—|„ˆ¬³)ëh�Ï_Á”ßµõçf…lL*'iå©'“Ò&£\·eˆ…ºV9ªi¶c¢ 8Y´ÊýPû»vïóq€�A¢«�zpoí`KF¤èà mÚ{]jw-Ò²ª©øOáö Š/ÖÝP‡'£"¨—(‰U!„2�?ÀÝÚºJ—æwBû¶âÞ±& &u8¦›¦4”MÄ_Doª’O®¦ºd°PÉç­‡¦ ÃW­G>f=­ˆózCª­äÊlaF|Ζ.¹)©>W4­ÔO`—?ý¥P€7ðÐŒÙ\RF "=IÅÛ|?´ˆƒ¡û½NÎH&_ÇW«×i`=Ö¦ÈEY%ücæäÉåЂø†Rƒ^š¿(œ’hPÂõ»vµB§çô�Žæ='€|V²vØ5qÊàöX'ÞŽKþ´‚âÅ„6ò×?¸bér¤Îº{s@¶ìl{~ɳàCî‰h/ªÝÙÖº{ƒÑÓ»¦ïß½– Ä™�%ûxq{ CÁã5¦¡¸è¾x/ä£úÿWÊ  ¦ ¦ˆV�‘0Bxœ­XÁŽÛ6½ë+¦>4ï  H´6ÞÈ¡iÐø�)il± H•¤ìúï;CI–d{¥l"`­Ì7Ãá{3ñã_žþÚl¿}þ¹/Ô:ŠÛŠŒzi.¬Cÿ°¨üîî÷ÅŠ>÷Ò+\oLQŠÔo-â{±ß£…Òš­—èâU�‰âUã+1Ù‰=ÿ¶v^xé¼LÝ[÷Aà( E�‹+èýŸès“-ÖñJ°ç××5òôš%¤J8÷°ÈßÀÎÑß½z²OMvã8oÛXN'ëÖSBÛ|¼*Ùç¶§ÁìÀ稉¨^Íä¡Ý³pwoµA½ß«° w2%:Œ~Õ¸c[€Å½E‚t‹÷Û\º.0é"°Dð�Ñêtß캢mG‰ûTüæF©kAs�×ùº¦�ÖÒ� dÈ(s� �Ö¸'ÆR{¤­–�y=tC‹Aæ¢>u?ÀÜ4m³qö,a[:V}Æ—$Ý#¤FU…†•a­8AnT&õŒFÄ´¤&GÒeg4„þ}NÒ,ùSt˜êLyÂúD‹ãt°9tè{»‘½¼Ð v™Hám‡c3Ã6psÙf°ïµbº4ˆ@z8 :Šô3„ÎZ c3â]z.5dBj#(! Wy’âCQÒÑvÔh«ÐêwY+?Y*O¸•òß(ÄQµz¸9主Öë³ÅL¦�é@€¦Tuh5ùÚ„t…‚ŠChw$ 5ªçäe(á²ËÖ�胕&Üo‰H¤’þÔJ 4® ™U·äË€?+$ù§BU"AU…¯­ÄÎã¹&éžœ¿.Æ£ôy{§ôn޼S¿ŽúˆrŸSFEP sS‡ä­�š÷oó*­È h?0„°Gé&Šüz�!s»€nFüÝ9Ö¨iìtSBçÈ´K�Í¡láSW\¯A“)�_eâƒ5“ÁZ„\ -ýz‹2áÅæÈ]‡î_éfº$ŸPy±±’R†Êú SÉåý¥T“}øY»yúòˆûkä-žø×Þʤâûò’êizŽ ÔõŒrvTwÁº÷Ð6Ý�ÇRÞEš‡×_­û·2ï\ÓÄÎæ‹ÒX/tŠÏì?6¯Ÿó}ZÈ+ôLü¥ÓkÑZUÁ1ÂñX(ëúÒ¬2¹ª¸P‰DŠê¥œgž›’%–ôÉ©è2‡{4DËÁrФ{Œêq€ƒ¡Ñih*²Œz1Ý~TõóÈü¥²ÖÐw|tΤ2 £Jß2˜CìÛ~Ÿ¦è»˜è�� ?"ê—•(©Ã’‚Êü¿N~N mùÿ¦2!0®Ñ=ÃiˆsíšL\‚3Óä‹ów8ŸRkÁ�Ý« ŠI¡æ}©Î_;¯Ïºð9ÆH û%câ²CèCU¦O4è6°Krê£Âtpë™:XT"Œ¡v¼—½ÎÓîLÛ�£ÆÆm›ì8oÚæG±žÏ8Ïya²Æl|F±JF}P`[ƒn÷!$~½õ1êé’Ãt•å˜*U.+0Áì)4f~±R}ͽ•Ýû�aßk�h@{€ûaëÚBÒF;16µ13« >ÛÀqoªLƒóEäiW)Š6u!©Pâ‹Û ý·°­»7”B�öÇ ‚fð „Ý!,Ôá®°Áᆸ;€p‚n”ðCc'rÖ‡†ñAS¥ÞýÍ*‹"®Õfˆ‹»Cއ@ìÙÜiÞ³!ôí�r6:y.œ�ëÛ äÈGQ&8¨»&„÷Sz¡óYºŠU|_^5qnbBGuÔ»¹ ··†û¾¨i£ÿÇÀ·;èUoßx43ÙTçj0¹2½‘h_gwúºJ`åè.5&ÄÅãnY7¦J¬ó`Õîáݼu¾\•ÖPBÌMšš L/XoÞ©4ó ¨J¶²´qê¡ÅK)°F´ÙØtÇÈÖª*9ÚEë†Vz%Å“*Î\ò‰x¾ï•Z:xºóð¥Ýµ?xªï±ç"—K•Ç�~uȬ¶ª Œ18¹›EonŽ·åîŒéuÊ_g¶¿Se)0”Ó™É>‡y'·6S+kË·” XÔ¼•¦`•@ ÷±yZ;øÒŸ•ò^®_QaBÆëO ªÈáén¨ DÙ�)›}f‚) €¤Ð3X³ŒtßBË~{¶û.Cû:ƒ‘¿&ß•:iñ¡”f"%õÀT~Lçvuýq’sí!îÝ#Þ*ÑÔ°Û Ì�“£ï<«™7Ý-Áß§ ç&*mŠ tëËxðÔêŽÀÏôè×ÏY¨ßl¤Q©¼Óm þŽ8Í¢(»¶ýíWþÆYkÚ`mÏaq}ƒI·Z.L+‘ã2°€Cöšî$QŽ�OOI”syrÑ)‰ruž\tïåÀO§Z—JÚU¡úüM³ÄMÀæØâŠìÅÍüé(À2×iÊr{^â-Û&p™mÛ±E"BwÙ–xÊõ§}]ö*œr)ç ËÃÀ��;2)ä&Ђ!Þqêßé+f½øÄ²µŽ‰@°Û°«ÌðYb>@iÎ!Ùµ¯BòAU•Î«Í ðiÚ­„Éä«sšCÉYk8e:"ïH¼ØÓ‰z¤4狘¿´Íæ ŸœZ(\ªúÚ„ïchµŠÊƒ¿Ó2oØ úU[*,Ô/º²8ÀrŠ]g„Àî½m9Íšç�cÐeQ˜Õ²ºƒ‚ø¯tJEµt‹0œy>òHÙEšÝ*‚W…‰ø¶K¯-ÁW@‰ê&“¬Q3fTmLÔQ…bïÙ�”/ŽÌ‡ûê¸|¸ ™ë¥,åA :A~O zòáp1DMDâcvU�’ùC¦SÑšòàš ¥DAè_¶±'ÓÏÀXÛØHJ_ Ló-y�ƒ�sUÝ�ãìÍW:r7 i"]&l�g›¼uæÚt¦~“ï… £px6Œ:àðluÀá‘Ø0j�Ã{� Ú8ÈÀ”Zªck¯½iÁ¬�Ÿ:ÖóØrèLσÌÃÐ_˜~×€=¯¥+Á· ÚÔäìv þVÇân¹7Äí6s·f›^rȽX.S„Y¼ßEi¢;ä�jO;ƒÎÀKåml0lûÛXs†/ýHÄl�ãîÛ¨“[Ãîj­„Ò •“íƒëÔ¯ê[ÂA�{†8ž‰8¾<q<9q|¹âÓç{p„´ %è±MŸ8ÞÀ‚@ÿ “íï݃�ë–õAî EâÕ¾¿.}¾Lâ�‘혽¥c€Î9…7Œš©Ô1–di©)-ÇÑJpQOÕÖ¸}_Þ'£caVÆRˆ…pK-ìi¡#€Ó—wœÀšn'Üó»ÎýZì-ÛÒÝPàl:Ÿ=;ùòÝE=:‡acˆÛ$xJѼC9U;;ïÝœ¡ÖÝxÖé§G#ñøçÚ8WxáOâ"Zõa¦Tküw6¥ð”Øñä2AßìMc#L�ñ¤ÿ—ؾVn-§DntëJúG,ûˇe_ÖØ¾ÿò�ŠWtÈÅíxlr ”#¶.+ÔÚ¿Ú¿c÷o;�ùŸŽú/ÂF |  |��š0$xœÅZmoÛ6þ®_Á©ºMŒ­(0Ž€ÕM×`i´Ò~*(‰¶¸J¢@Òq³_¿;¾èŦœt›e ‰(y|îáñHž2ÿáõÕâæóõ9)tU&ÑÜ?ÍáQ1MIVP©˜>‹×zyòk<ƒzÍuÉ’l%™R\Ô¯èjÅòóZ±*-Ù|fåÑ|æô¤"¿G­?'JSÍ•æ™zIÆûCÃhNIM+v�5‹“ùŒ¢Ö_ö a4oHVR¥ÎââY*ø9�W ý3‘³¤yºGW šóYƒZb[æš±$²íN´dLE¶qÎï<ˆJ�¼ˆmÿ߈…0>¦‘þÅ2M QæjgÂhVDd%Ŧ&PI‰¢UhrI¡f)EEtÁ 1å5¯W$§0«¼&���Ú(Ø$‚�“wÔÔm_Z=3ú‰jJ®Á„bN§…ž1^’¯ÉŒ<ÿÉÁ�@3Ëy¦…T]ä‚PMØ“÷¤†v§„\hßT‘ôžP�ÑBäZEÖºÓ¨e{!Õ@rÍȆëÂXrh·Å­s'}úŽéBäO½Œ�¼èö" '£Rräaê±Ð�IÚžž\0õ,2£n dÁTØi16lê/?¾BÁøðªÌ,¿T}'Z iÌj±}p-<Àg^ðf[B kÛ©)iÖë#™^ËZÁ (5.ˆ—H±ó*I @Ôºª >Áå–#ßÐ0BOC¯2¦‚g»¹š+-E½J>‚ù´T0Þ|æª"B`m’-Ïâw ‘×ìË%£}óãÏ“þ”Æ[LÁ¦¾†ÝY±:vëãà në[ˆª¡™S;*Ž“QÑî 7À»YéÒjíÞã¤+Ûðf�Aü°¡n!jÍj­\h+ž'*†Qä .t V'×mŽªžcP§© ÚZ&s�»àøÝÊŸŒ5�Àk€JföÛ)€|Š“O! ×~7ž ÉØÀñr˜EW „[‡[çæLb6Æ ƒÕ¿6]ô6NnCЮvÏASÀyúÊk*áÜ«™Tq²UœÝV깄 s °ï×U�&»U MRˆ”tm¦šîW¼>ÏW °úR$ÈC¡'´;ÎNòí=´kü¬^5šW.Ú€èl2.�QÂöðѲ¨›ÄÈ6&½‡AUœ ߃®ƒ‚ãÌÈ®+!yFË'`V‡ _Ô ´à³®ÏÄFøíyŒ“þÛÛžlé7²;ØbéD»êù·î",ßv”pýw8 \·LMiÏC nˬ¨& ðæ6 qÝΧñd2‰WÀÁèósxfîÊû�PÚ6šÎu[„{àÝ Ô­=¥·Øný©=0ãVäcØä4.D•ˆ-âá{ñ[±ée0=’Ù>“ìþ¬ÌÖ ƒRw|h«F\Á mš’#Å¢¿C…?Zô͘ùDÔ£r[v›ô‰-»¯þY­¾Ú½9‹‘Oãýp´§$}ø[Âé˜íÇ”I¡Ô&})x�²òò;Ø�³MøÕ]9Ln�Ìz ‘7™ön5ÏG0@’×_ãÄ>CÀÿ`¬y¦M˜Ð�y˜ð‡x6…¯Üy#|é¾‚ÙÆxè:÷™MÔn[j 8¾š™_-äÞúœÒõÕº2n�Ïöß%l•�Þux}dÈ.Þlj+ìIR/œ&…~\ì%¸oœàßpN¥…„MŽäÐè éµ'¹ÿöh¦ûg¡ÖÛ‡ôOlÏ¥!¾-þ[öýwÊ�e“¹¼�~c§5~C TSJóÊKÞ6EóÌݤUsx3„èüª+3æk}"–'À¾Çg2»Ý^;)ý€Õú�+<€Xi^™¬Öº—Èê{Ê~ùãøæ{ΓޓÆh4…�…í�{æþOgfÿ èëÿHƒz�‡xxœÅTQ‹Û0 ~ϯÐ2´ ;877/×½]YacpŒ1”Xi¼9v°•ÞõßωӱôZ®O;(V>}ú,)¯VŸo¿Þo>AÍ�Îq0„2˜†¡¬ÑyâeÚq5ÿ�fÁÏŠ5åGR•¬v´¶’4¬‰k+½ÈâçDd#Maå¾'}Ÿ{FVžUéoàlxÀ%Á`CËôµhѱB½¢–Œ$SRš‹ {ö«cAO±�û*@[(5z¿Lëk¨| (ü8úÄ]ÞJ‘õ– Šüc‘ÃÛi‹_:‹‘ßBÙ¢ã�Ȇ "k_ ûè¸Ã‚ôÿô¦&­Uûq̼BÆ—ÈÛФ;Ô=¯$<óp䳊¾O$�ä�“Ó>ž~\¤÷HL·¶i;&ã@þ \$$Õî ¹ñóë4ÆE½O²]<  SCÀ×­ÏáJ¬�ó,N9�©”«Ô'à2¢T|ŒµPKÝg ~9އBJ5BÀV‘;H„õKÕV,Iuc~I‡!b\&�‹/n‘ûC±0äw’nŸ~Ý~y œþÑ$�.ê70éa©zÅ/8 š;k¸&Áç 3~ix:�ކ‹éô7='¿‘±Y6i'‹}¿È "Y]S‡>¥ÿB{v¹™Ðq¯é{šÈôÄíQ6G‰94áãï�ÝO‡@��¹çëM²©X‡¿H­òÔ®Ò_Á?•«E ±�� ž 4Ýxœå[moÛ6þî_qõ€vœ+P`hhšÃÒ4躮Å0”E[\%Ñ#igé¯ß‘õþf[îÒ["�Gêyž;ž˜Ùƒ¯Ïß~¸¾€@Eá|4s(ññOD�E@„¤êl¼QË“ŸÆS¼®˜ éü ] *%ãñs²ZQÿ"–4òB ¯¨ ¸/gSÛn4›&ö<îßjë?Î¥"ŠIÅò)tÛÁ£�˜DôlÜÔü´éÆx>›=ìãF—ûâØ�±ë!‘òl<�¥Dƒ î7/ÀS˜m‰˜sïïÙT€3˜y�­gSoß›ïmû‰íÿÁ~ûa65£Í¦ëŠ'øs‚_Õ1žW¯Yì§§¸Ê}ˆ´\J|¼}ˆãÚB”ów¯ü0ÌÌhJ�ºÒ¡cÜCp®-�` æE«\hÇýs�-•@$”'R³ð¦Å©¥6a@F•n×D ã‡<ËM›Ý݈…*Ë*Ü,7͈¯ñ»H·-ÿ<Þœ¹ãÊ~Óˆ‡ ûWJ×XÆÛD§EyhÕ&,îȃúàÜM¤9œóhMª.E¸OÄpàmY£Ý3¢mJ¸¶{’âO5YÀ^êüÑ^ú+ÉV¶Ç¦Ë¾¾ ˜9iJYÓËÇ ™ó÷‰f[§¼;¬Œô¢‚mZË„]E»œª;Ó�ØŸÃ Á�6£Öj!´,�28åv¦ý�õÆ(,"«§¼â&SVóêZÙ½ªÏ³Íñ]bã,í�·Çmë³G:.qß/×-Lí¹*êi»ÚDn§~»©k¬ ýýëþGðá­¾¿I ýŽˆ«”ÿ—OêBÃø‡(B6"hƒCìRš‘o]®�?#ˆÈ­QðtåY]{(?㛀£lág¦oãÈù#¢�‰³#£n ɸ°/C"3„ô¥U¿<Áùz·†oæýIÌ•þ¾æÒÔ[O]²kÖdЙ0É+œTxM¯½sSi1d\•æeÏ.¾¥ãWÇ*,Ë([—S€·ùð�"+¶¥¸pT2ß°Èlrô¿—ÊnÅ· Ü{m¼ëþ)N¾ý�yNÑîQ‹˜ÝCžã ¬U7ÑÅÄgØíA6Ÿ;›ëì™Þg`íœú~d¸ì™å¤�Kur�J€¶A�Ìuä«Pa Ì_ÖÇ÷¯ð�í“ÞwWËÅ/Ñš EâE¯šPM·CàÏ¢uÍF³hý¸úÔƒß)ç÷óhR±ÈáYÚWS‚’E�å‡íÐ×™PDI |KKï{�ý-©ø8rÔz\©¢vnkúƒ®³n”®p©»Í¯bË&–•üàÜ»¦" ú×u䨳ãÜÔûgdYë!ó±¼Õ/š�•®”f“Â¥,g™ÿo_oÔ _ž`r¬cú\Zv€¢w º°”KÚsOR±%Úš>—ØÆP‡¹Œ©v³˜Tc!¤K5BúߨIЊ…)gJó¬pZ¯\»u§óFŃ{ ¡Èû ƒ/Õåiófиd”µåÖ¸ IqÇœ_WäÊÅSpN¸åO�âÃU¸›Îú~ªC9×·Ø�ï}“Ît��RÎÔÞ—K8 C’næˆ�«ë̳;¨ºüÔœT­²Ò§KT‚ûiµ§O Ï]4ý#‹°qQ:¸ÕRbeýÈÑ |¹‰S–é*‘;´‹¿³ëúµùDc­&™G½Š`“¼Õ2¯Gß±kv”-1v¯pÞ–Xô ñmÆ` x×@GU‰=|9Br€iA._×EYã±uÙ6]Áx›e哤ډò%«Y¿¡í:™�Yc‹³9ñ�ZtW}¸´luTL³~³=ÀÏhwÙB q»ÅŽ&£¼VU¤Jé7ÂáÑ¥~y­­“¥¢B‹Pñy<ôåD§L£ÒDì4íŪ©`ºÞUÜ®àz1ßz�“Át×'Ÿn™UÊb� |¦‚còDÍF@�õå´:þ­æ"µ{÷ݶìe›&ÿ_4µÿÅôßNžŽK��'.xœ½Z_oÛ6÷§àò°lCR¯ �k i´@›‰‹4�´t¶¹J¢JRvÝaß}w$%˶ìР¨c‘w<ýîÿуŸÞ^¿Ýº`3“gÃÞ þžâG†³dÆ•óú¨2“Ó?ŽúøÜ“Á𦠴²8çÓ)¤…†|œ+•,AzÐw[{ƒ¾g9–é’x>Ô†¡�Hô+Ä izÎ žÃë£]Ï.o@ó¼Ìàh8ès:êÅNIW›‘û Ü[²$ãZ¿>š½d�ÿNñ«AN‰Lw¿ð«ZÌ%Œ‡-žcÁRú%q²[˜œ03Ã�±5Gd¡ðdÆŒ`©â Ýs$©˜×"åúôå‘ãrÆŠ*ƒb(7øif¼`¿3^¤¬�ëOŸŸl°Å´ÇÜ % 2öK#0û�]ýê%^îc£™Ð«×Ä¿ñŒôTÙò™µ�²†(èÊŒ'Aêñ[#*§á¸­š» R”Õ �¥-Zl!Ì _Ö’åP˜“É©HxÆ4þÇÕ rª€�—,… ¯2ÉÏ®ÇúRªÀU*ÒMšˆØn³îY -î4|X쇷`È›nfGëïWU>R\�ú�àºß½ ¨Z`Üô³Òß*ù§fÞìg2Ku½§-Xd›ŸVhâ‡efúmе´Ëö×YnkÚ(…'UïÅha=<˽ú¹ÈK<ª* †'SÃê¸2Y¦™0α8Ó¨žÄT.*GÙc Ó™Á”-w<±á ð)ãSÔ89V}Ž®òú ¯º^Ý݇¨ë>Іîw(¥D?A÷à†ï…„P©zw[ilÁQa ' ’)¹`¹@QP½œÍy†.2ãs›ÆË`‚†PÅ X_B üÊ/]P~R�ŠÄ UaYÖÛõ^4eO<{-€ôÚ©E9�ä(OÖf0Q2§°Ä‘ñ‰ÝÒ:•{*z‹´çšÏòÆy·oN*T‚þ¶ÁŒ´šÁÈÑ0Ê!/òò:t »`Yz—ò'îþ ¥÷ÍFÞÖ›D ‰r¡1c¤AÁÐï�f¾†kuuF&B%¢AÚ«�$0÷'2«rL&s_M(âŠðëª,3 ^>Õûï@Þ… xº».Ì®W&_î=«ðS1ô Bªœg˜$Ð%”ÓºõÞ`\rþgYyíÔìñ�ãt\§Ï‘:yº¬€˜aì#9Å'®p–Ù:ÿ ’(ÚØâÙðšÕÚª1ì<`Å«„ÜD�‰¬�<ä£eY'Úÿè#VÎ2­ÖºñeàSv†ýˆÞ º²ëÍC:Çw5TКÍFÉ2É€Üç+EKT(¦}že.tÑ÷·çRjÓ>ô�{”ZKÍ–¹i~ƒ$Šæ·xnkþª�r­Ø~0ŠÅó’sQ\¤S©Þ�³mXp�-Öþ°Ê¶ûÝ"[ð¥¶Õçò„�EAÆi³ðlâ¤F‚ÚÃØÑýz›*k7¾Æâ7÷¼+Öƒà}�IÜNÙÕrXuHGQw³5v‡¨GWÔc¶ÎSTï™{S[^á¾ ­¬SDQÁ&ËŽ¸@ ‡?K ˰U|I¤4ä M˜æ`©£f×7¼§RQ%Õ¼Nž�„Q`ÝÁyÝ÷.:|“M(Î=Öéjé&#†&z¶†l±=qöN£ ²ðÈ€#�-\3IF6´Xí�4²h3ì¶æÚ˜›Î·n…ž(Ï]|/±�ôðÐM¸]¬ƒóEwæŠ8š•-èAþ kÒ ¯«ÅMÚEY¤jÜŸAE¸/ScÔÞžÕ6žÍÕEn·ì…Ë÷0¾‚mz,jý ¾Àñc`æKó`Kmï�‚ß:ìrükcˆ!u€“áïãÆÎãÀàZ—»•‹â€±î6]¤HÙŸk¿¬û@ •%M†R;ü±SÖ' š—¼/&2èŽÎm�‚RÃk\ÂÖrB“Ân/—«¶“†©~ÞÓ´Ënº�ÀæÀ ¦¿Uœæo ”´ ]xô\^·Slmêü%Ò «š�¯„F¢áºÁ:u»NÜ–ÖCòt¢DjÇÛT1õ³Æv[U­eÂÓ±nTk¿=‘Ö@ îà+¸u¢(ÐupÝuýÖ®1éò͇VÒf¡�~<}8;GüL¥ {«c¯lÈ´ëx·ËZÝõÎ^š›Á8c—†Ý¾‰‹q›ž4 €JLHöà1 Fᬎ ëm¦“à�ÌKl6W‚PR_ýHá/ì@u܈è÷“åõ1�mXvø²Ÿ¡û")ÜèÎÖÝ^¹„e/°ˆ…µ½”Ù{�м£%šêšC:ø†(jßâÚ/Ü"ã.�ZM)>¨[ü½*¹Åä³#tì{M×#¬cÜ÷¿V뻟Åý Ì–uŒ (  (Œt�"™l!mxœ½Z[oÛ6~÷¯àò’—$Æ  7@Ò¦h�6-RiiéØâ*‰IÙõ†ý÷�Ë,ÛŠ¤´ì€ ŽÅÃ�äwîTf¿½þøjþðé†e¦È/'³ð<Å� gIÆ•óò¤6Ëó?N¦øÜ“Ãå¬h-dySj(9°JÉ ” gS'4™M=ØB¦[‚þýRn„6"Ñ/ØJOfœ•¼€—'Dzø¨Î¹#¢,O.gSNk<ëØÜ�,b?CÑŠ%9×úåIöœ-5þœãWƒ@‰L»Žø"loËf‹ËCÈnÀNœM+¿Äóé:7L.™ ²¢\1“9q©X‡½úüù‰›ST¸T]‘3·§üZl)ò\3aØF˜Œq¦�ªS+ e QNØÄ*3x@©ìâFè3ÆË”.! e|ÅEÉÛf]a� Ææ™Ð“Ýé…Æñô\–ùöÂaŠgè××C¿Š¢håáETÅYÊ ï¥|Žô¨ ÝVÛpÜ‹0É’%’ǔܰBàVP¥œ­y^Ëø¿NØ d9,Qùµñüý,}_ûéû…¾¯]ô}R�ŠÄ õŒâ¯ ⺗AY‚å×fr¡A­­#Bì>å´Pà~ò6ÀRÉY7�ϬHkUîgÑ)Ò +¸Qâ»5÷RçÜžœÔ¦ýÍ2™§¤ÉÖ@¥Ñ“¤"Ï ïBã·Ò»Ž_�&¡ô7¨¼6ûm�$†þïäFÑ�:/'Ä5`>…ccè¸døGHc5Néµ’+–Ë•H8ñ™×EÉä\hR„Š”ëºªr L¾3ùòîûY»�B×}Ow¦ñ …€½k@O)¥*xŽA�N"5} û 9ï ÆçgÊkdâãøé½C: éÂ#RÐB£'�–5'D} ³ÿÄŽPºŸóá(8ÂìfÍh°^ )ÖºKª�‡/ä©ö4Ï·UH–gþÑ0™LÃà�÷ÀU jŽi7熊œ½I·v¼yHë”u± ‰ÉœÆlvI¶Iä&ß(¢1uóÂ<ÖömC_+ÅèáîEy“®`€˜ …‘Ø18Æ€ƒÝï2f¿ùç¾Õ¶RÜž±…(Ém&]€M~hËP@iâEë·èOª ŽúKÔ—ٮ¤ tpz®G¬òXK`U ÝŒÐd{pOQ‰®“, ¹<3J9“4림E‰MìËF¡ý²Ãçià©FÎÈs¬ßÒTj|y‚Š`k°³#eÈW†WRQíÓd€ÃÎ)Q¨|ù˜Ñw%:N“Ýœ±ÜôXG«­íAé7¶ÞkÁž9»f› È’K C�DhÉD†. v’‘® Ú€ÝVŒ¶é@C{òËsÕÍ÷ ‹HŸâÜÝs¢�õôhgg¾(Î]Á… ßЃbÐR5é�‡Êîpíê§«e["ɾŒŒQ{¨c›Û·ÂŠôRäû _a6}�~䚯Nãò䋿Ù–ŒÂÙ>à€õm€k nLþv{øçtޱðô߸¤Ýײ|#±×P…(G]‘ψùº€�ɼ϶¡£Ë—ª¢Û—Ô^°ØË_ßó®\Ê~š¼Pf¬c2pÛº%uþÄWïÙßìZ>º˜ôw*M«ên�ÌxÉô_5§{-PJÚÆx’M\>¶·ÀÚ„$Ò³<Ý-ï.ÞŒDuÓÔi¢·Çj'=¯A'JTÃöz,S'{°Çêi ‡ºQ§ýöËÍõ‹JO¨}¦úù:�BWj—g lQÚ5 Ú\•ÔŒ?¥ÝA[üp5u�œ™Z•öí‡}µA&âo¬nç¶.æ ½bèÞu'ëj£�ë»Õ0Nè �£˜ˆICóÑ'¿ q¿Ås;x%‹ ½ÝF(7UÍâOìþt¬ç·ìïdG±èec’Ù@vø©¿wöÍxãºÚK:ôfÇ%ûr‡¦ÇjŸe±À-�bp_6NÃ|yÌà[¹Ù½bµ/¢7gTU}ï3õçºh*ëe;¹È]a+ÍW°'KyµYF»)5U"ã:æF.,×–VNps•g-EÛÑR}IÐKÉ¢¦Ž¾_êXê~‡ƒ M”–‚/òO€Jm”@×û©!è�µ„ùyöÌ8|¹*9ã‹3'¯†ûζ´@q C {A:Q{ÉÙ¡ím/²O©n�}Ôì€1LÈY]�ѽ]øM×ÀµòAW¿Î#ë _:):�š�² ²|ÖOé(äµI(´°iCË|¢+°ß!öUD¯8µl¨dÿ`Ø n79xw‹û`7Kâß-¨e£}ˆ;^´µu›µa ïqþX°Yþ°R'�ÉúmªÅ?gùçØâ3 +µ�IW¿+õ—‚ -äGQE ÍX¦ÏRº¦˜§ÚÒ~’Ü}–ßÇàÜ ä£õ$Áµ‰‡4> ŒfF%FLú*Ë·&¢–ë©Ag˜�Rü¸lê@€[1€ÈR`áApbÀ™Ô”Œ_V @|a†4†×… Hfà÷OÈÑ‹Ýtšµ>I¹„„ˆŸeÙŸÎìF„û"d³zC\²�õùã#.¤²|ó=ê†p<ÍO±™YÉJÚôØgt:÷Š#Ñ.×ßx( F_Y>|Û§Õ T Ê#–7š°¢]~ë°…ƒjÛ âóßá „º6¹1½˜ÉpŠ®ÌD›,c“,°<¹g i`%­gIäÚíØ+røö¬úV@¿öKç•T þNÈ;�-ã}ßµ;Ã{_?…JE”]‡GZU6g™óK*Ôâs‘å~Æ$Â�&[?)”· JÉ:§¾Ý¹À٘ܡé74àýñ ±c3±Œö™å;Sq{3<µâÚžb½�šN ™Å±ù¸“Ïs•íUüøp›¢SZWìQ€twD ÷®³íñ܇N7bMG ùæ(ê“q�«ô(7ßc(ß‹ÕàB„âá¡tß$ËÙ¡6£¹2 d]®û©=fvDB»®aë‹08Jùú8œ¤Ÿ9–»BÎ䮲ý¯ùPàžÓøÎÍFüJ#–{)?xGЧpIvöŸ ùúR ¥)ª*ŒbH§†vŠDV™[²èD¨ÜÍP¶¾!‹tÑuDšbjOÏs)г†ŠL ß—¦UuÏÐ?$Æfk.(­IÓÀðÁ“å~pàÖc³[¶·�GÔWƒž”åæÿøÉ®‡aXLH$w¶¾P_µ��šòz5'£ý±Âã§ £SÖ³ܶè÷bÒ9_1 ¦¾½Øþò‚(!Økkþ~ø"àÇCè/Gm<‡q 04sVóJœn<ºýhp6qdòt‰hKÆ�§0¼aiɵ>äÏØT‘TfË÷œ�¯¸:““ŽGøvÊÆ“%ãÆ£É{dÇþD#‡4ó}{<YãQs¿Ü/'e‡ÿ;¡ Ã>põvõ�¥™JUÍKþÀRlp&ðß!|5û’ã«\”eÑœ8 êpîÁ/çËçŒ}WÆk&¼µÈ)S� 3JÍ&7ìõû—Rjs”ÐĬ¸ò+ªôá³Ñ¢ÕL ˜ä³�X¡™ÉËd:¯DmDÆ®ù +ê!ðÎXa4ƒ%4¬3¶#&oirføgqÄØ 6á³LÒ]`‡ô&7 [ÜÍ—vô«®,~ (¯6JÖ³³÷B0^j »>r�ÆÀUäJLOoxšµøåµàª.êÙº´?ÆË,,šÃÎÇV‰D”V½ô,½’]Æޢ߭¥Jj *ÐïÌü¨Ý\Øù‡7Y«jK.JP潘ÆŽw1‰s¤t#ŠŒ[Ç&ê¿D:/Úª�‹›¤ø‘!ÖdFñµŽ5\4OꈟFÔl*ËL“)aæ †ß¶ƒ ˜Y 9Ý6Æ~¬­ÑÓœƒ¾Šî-NÀJ�˜ Å€l‡©`Gž ýŒ¿Á97ÑVÏ« œs-@Èc+ß“¡ì éJt渲ö�`e ¶@ز¨É‰êŠã2†Lª@ìüCX^,½M¯ÿ҆ݩ² PšƒS´Ïa ,XÀÛ;Š%fµ`àËLûÕncÉ .’^2d°NFÛE䣃îfnã€dÕðÔ¬ñ?4h©ûÙÜ),úOt­:cß*ÙX ÖÏÙTÉ Ô)ЉÛ;ƒŽ/ ‘z\Á‚ŸÓ„eúB3r°—dÇx�æv�sÃ%²"ÅKX-D¦ÁÞ¿�5<»¦UDå"åeyóðj¿rÅDvåëAß^m¡¯ðPJ¿ºÒ˜Ý‚åËXE=¡Ûê"ÕÛyå�ÇùMZ ½Ç ºR’6|~¥ô¿æòäí…‚8à¾ø ºÂ’þªä5«¤rê¸ÆNb9Š…x]øëÛ6�+˜¡ˆS1J^׌#9Ÿå–„V–ó ž~œˆqžVdØøÒGÀÖÇZéAŒ\}Z¢€ƒWB\„§Qä… ÑhfÑ:¬¥iÊBè#Ýr•6Ý�8SÞ:ol§Î�õª´›¥Þ€l±‰b+­õ§=Zg/÷»Ü "dÕE¹žlm‡@÷R䀿4ÚÇš€Õž*2Ú½†$ ˇPvÆÕÌ&‹±_ÂÀ´ÁM`êCyæ`Å~‰ÀUܤ9˜=GâCŸTÄ´AJÈEC Ë+ÇÝ´âÕÒà÷Fj {ž'“vgâ˜v6Xy[„_Àˆ‡v<­�¾H2a)¯i f¾ÚòÊÂsfO-DtÎJð%ŽÂËȱq“¸çÈݽ ÐÂÜæWžÿ/UâÖ„ñÖ®7Ф-ÜE IX¿¯À»9Š×±�™u.b�%¼m¸ß o'iöâ0Wp¾‹«ly1$ˆ™òÆõ€-<# »³[ŒõʺE4÷È7Þ:ú~W™XÎws•PÙmÛƒ.¶{’´A+€8¿”BVT*–l#[à›Wg[’v_ ^Æ¡5>‘Yq`r"4¤\”²YR8?öÿnœüí:�ßïIõ}ÕHex�®IÒ–LØÍ½U³ˆuéî¿Îùs—·SÌÊ]püý.²œ±WÚ–7Q‹07Ý}£¿®¤;–äÿã”}©nn¹[–(&›ä?ñÈý$A]Š÷TÞ_Ïtï –ÔË›Ôø²‹ôü§P�†Èå tÎ×Fó‡®6/Wþ>¤¾'ˆnOùõZœ†í¬G´îI•ûØíšÖ»éoŒ=o)¯…vôèSÐgª8ÙKW÷î£>¯NêPLûHµ©PMCœiЩî„höe”¯®-3ûq{)5·ÄîI×{ùÝ­Mô%’¡Þé*¹îγ;ÝÒ´¢·r¯¼¥ ×µÁÚBÁF„ –+˜G1ÚÚh1XÎa¹­"Ù‹V•ŠÆ¸ HÑ’°ZÌ!lŠ…°7`DC[bR 7öF¸5/M!n„mÊÔ­©»À7/ pí«ZC*ó�´<,í—¥”¼šdˆ†ÕYíÿ¶‚ý©`/½OÐóŠ=úÈŽ¾f�>±CvÁ¾fü1;ú{ú˜�ØSögFÄá¹Ê·~'ÊexD=±µ2m«l©,çUmÃx¸2�¶½q·‘GûùmÓI{Çìƒ\ ÀÃ(‹‘ȶ„¦_"‘9Ñ*ÅÔØŒ®0˜.QA›ô:��£Du¤D0@¹á!8âÁÄÜ~›H•Á6ø¯º²s±)®K¼Q€ïÕ ˜¬Qð'‡ƒÏL~:xzü§i|›�G&ïŒÁ! Ïþò�Ÿ÷¡µS42ô7j̳ì².;wïoö ضÉ:#‰iç™eú‚] ÄÊx´µ¬½þ·úN wÂ21åV§5;ÆÎ�R^S”mT\ÕL&Ç"ˆBå F<°E9c:å%Bu â¿‚J>9>>5½kñ»Š—úŒ7²l-ÀÜm)’øM(9Œ™œ2üÿ#>ÑìÑÅh>ȧÇ�½òÑÎàN÷íðþë÷àD¶ÞâK[É@é±vm½i*AÑŠSÄLè0jBþ¦õ¹è�)Y¤5»ºGt Oij %'ÊKYî$¸�i#ÈD§Æ9EÀDàܬù&è_ݤ9¯g¤tI‰ý,&GPT®ê~p†‡IS°ßÄŽHÐÕt´ŽL›Ð3çn­ÿðÉqgFÖ:Hˆ¢ÐÊ[C+Sóœyä½§…@ÇGS©ÅÕëcÛgƒ!ÆoéIŸ×Æ=¥;ÑOëµsøRïÌjS˜9Jüæý«ŽE”X Ø`ˆƒþ2DJ‘)[Í!–'‘S†¤y.È·€(Eö~^Ü.Ð]©‹º`ê²D�M‘?(ˆT–Lç’©lù£¸v´U´à(†&0½{ÁjÃOVL§@ÓWµð‚+“´­:é[ît^Û0 Îð‹Z"©bh‹žíÝ4*õÁmh~ðA¨ À…ƒ(Tz‘4Ð 6ŠÿEân�«¢þ¼yÓ«½pI¯zøø—}œˆ²Åuƒ3û±UNà¹÷å^ˆ�z4clî ÝWwæ*^wÉ~¢‰¢ïäˆvÁ±­ë6‰º²‚ˆ$ß;²B% Ť«-m�ÏKڮˮç?Z‰+ù0Þ逿i¾˜,LÚÛ‰nèb™¼®‡Î×r¥n¨ÕÆ/{Û”‹B%´ p·6µD½áQ²‚‰¸”6U»줕Ì]†Ý‹•‘¹ïkѶ%Üt• gbŸ®ê!ú߄д�çVƒvKm–õ Í1Š�~Óåè˜ëù]h6„s—^v@ûŒO½Ä'¨õ6ÅŒ³bÛ>ªHvQØï™’Mc5wAòã %ß1I¹ð¸÷3ª XÀf{²†tÉgÉÆ¥2…LŠ�k¶‘Ÿ-Çõñær¦(¿qºž2žb¹A‡vé¥êEfï·O ôÇ2LœÄð‚áÛ^¾X”×äÖ”â~O´±y›õÕ&¢%½%†øÙÇgÝ Øu‰­}�+É–Å}݉n/Åb³}Kcé]êŽ.Ưd%BÁ®×Å2ŸAÀ½u'Ðù ÉÔ"ˆpsÛþ€Æ~MœKjõÞvŸZ'ýC·Ô›jè]‹PäCǹM]áyHÑà\²C97qí ë9 [Õù`±Iœ£Z ékàÿ|}*ÚHphÔ®G¥š¥2y‘âÃqBÙ�·Ý3¸Å–…ûŒ­E"üŒU] ×þ0óõÀ¥H{¡ªÁû°nÒ»Õ\›¶�k?÷�‹V¬:è:–,‚5{ç·Ð;UíŒ~¶êç_«~ãÕ÷ƒ-FB…Ÿ­öJ¾ R`QŽê·þæ~ÉÒÂOäv+Ÿ—C¬Í�L¯‚{I|"‚µkn³ÐèbN;07ÍÓ',M®RádY¾æ´·–­åœXü ’C'ùEºß©l~^Ûóò6�¹¿`Dÿ>¿åÈEÈ  ] ]��$šB�xœ½YÝoÛ6×_Á±}IjlE�as¤IÚkš õÐö© %Úâ*‰IÙuÿúÝñC6ãv›\ �hy÷ãÝñx<κ¾»Z|¼¿!…©Ê4™‡g9|*nÉ ¦47´5«ó_é ú�0%OßòµâZ Y¿¼¼�Ï\g2ŸùÉK™ï�ÕÏ©6ÌmD¦#{“€šÌ©YÅ/èˆFÓùŒáü_öAO2oHV2­/hñŒ¬4ü;‡Ÿ&e2ç#‰óå>ƒ%°¶Ãæ³Y¥ä%¯¹b¥øÊsÂò\±á¤‚!%YIET7ÿIâæäbTúüul.‰¿'Ïö¹ü›g†²Ì5ad}D¤0z�L`\&k#êV¶`èFÖšŸVç¤Q<™Ñļ#Y¸5ß’œ�é¶ÂHN<.?% ó2BÈ¢¢BÓmEäŠÈš]°˜¶uf`5¤á* ‚¥r8dƒ$V–;;†�† …º‘úŒp–¤äLÕ°´•’•fq*m¬& Ó¶âyP4hu±•„×kQs�ª!Âd· ÇK)5èÊ(Îõc¿®3«‘œ¯X[š3b‡h\IYÊ-Hä™@Ó;Éa8 3°~#*þ{� ›%æ»JJSˆz�?Âë±QT`°Î¹Î”X ‹È­WiÈ–iRKCZ�æW@¬³.&x�ùÌy£aÕ`iÅD�ìPkgd[Щà Ð.ïGö=K†´×Rë°4›#e º7¬ ”­TŸA%aõ™¬Ö;LkC$È Ÿ3­ªÑaÜà+78æü+lɲìY¯Ów¤•ZB|˜ù®¼Š‘BñÕ½oc}z� ‚�ñ‘—G{Á –6œÓ�›£ÐtvÀW$V"cfÄr¿øíw¹ jW?ƒ åêÄ^íciñ4íD��|ê�ÙJ¤é}×�ŒaþS<ØÒF~£Ò¹É}þ>Ž�Æšú@Óޱ�ÏÈþßw=ßuÏ@Ü2?ÒôcLæÛR§ù¦­î–š«�µ’¦é^G  YrUåh䔸ÞÊ­þ "MC+ª ¹À…‡ V°A&WPgr§žþçqå4ƒqS"êä¿�TEÓtü;Š ÉÃfÔÀý2µé¼·¢Tšõí€Æf`Ó"º‚€¾– BQ9´e´;†ñU Ä^ƒY?ïdF¾ùÒÀÉÆó}cÇû¿itî§�Þúïiú>gTÈ–‹ua&ü •ñ2ï®7¦'Ýø×f�ÓBz!UÕ–Œ¦¾ƒãIÌ PسÌÇ÷á¯HqÏ·lGD?Ž@«Ò§Wן�†Bû‰A³„Ã¥º§‰ùw*犦öõe$ü ×òMáO Ä5Žt;|�\V?ÑBBꢮ#Kp™:sW?Hz7\ÁÝ2›8š¿Òï 3¦òk¾6¢¼¸ë£ýÑ=W²µÅÈ‚7k?.J~ê)ü9ÓüÙÒ´kÆðY‚½]ÈÖîÄiAYa[ 4íšÇA�¸eF‰/6Z¹f Ñ ø×ÎèC'tb8ÏE}“¯ñð ­ ŽÄƒ3$wWÒ®¬d…‚ØÉáp^ŸÒÓß¼zÿíí§^‹¥_•×Ù4ýP²À�•òRäKY9å_^Ô~~ù§‹+|ïµ/Õõµ¨”sÚš¿Þ¼ëÊ®UåµrÓ«0žM¯"�™-¶DõÙµóÒkçuî^ˆáõ˜˜M¥0r¥^^ìL{úÏ‹ëé•$bϯ÷F°î9Ö"/¥s//–_‰¹Ã¿K|ôX–Ûb�éi×­˜Î®A`âm²r—_]„©7ÂÔ+Ué\¬¤¯ôƒØh¿Ö(QÙ�À6ÂΜªî!k„4�嶬W††3p“6³sá—JøJj£Í‚7*ÄíR»–gü^AÀ—Ö”Û§YÃòÇ¥ª/_á(¥ØHHA{¯ 1¯ìJHáå¬TžÒl鄌«HE–A|ë;´îÜËJÓïbiË‚ø+Õ½*qä@ ;aOí]˜¤›0­¸-Âì;µöAN ¿é$W�ò |4ƒOcÍàSŸ|Pnm�SŸcQ³÷Š5Úƒ2¾Ú˜CPxö?*§ïëÕûv7(µ½ycex@îP¢˜2Slè1¸GD»¶N{}¯„6^-TlØô’êõ¡¬ß/¬™�“º %)VçÂ’*©]=äe ·ˆÉF¨äöùÉ©bPiÂX%´tzì™[; °M’ŽûˆðK»Ð¹,ûìš„êpV¸»Y�µ£m‚…ÛJ/´�õNhSŠÁôz­jż)™/9€ú¥ô¬³Äv–øæ½v³Iˆ$B¼õ¤ µZûíD‰ýëßQ“Ll–ÊD(8d×Ó.á,¬ÁXsÉ$÷J°µRÒ¸ÀMÖDÁd6< ÆDq¸€FÖ�áŒså&uävÖܸK옷yd¤ wRoÌÆÊ±9Ñ÷äî´±’ܧÖ#JHGn]#eÚÇÄš«²D¯ä–hf“¹‡Dƒ—:’Dò”V“è”ðÉ‚ð~Ý s•U9s>Jy”N;;“FÁZG{iˆíÇ„½'Þ®t«‚Ø>…Àv†üñ­„ß ˆ"Ÿzg�êÑCé¾5lÍ:o×�0œŠ¢N±6àHÎD ÎY¢õ‘zF¡YwÓ€@C(çÐo�i7Þ®_?¬AZ'—þécÕ2Dõ‘`£â²_+ê$tÀ9:N)þ¼ éeG¾Ä7á´ûGá/EmÀètuÛ­ž(7§Bž±¢¨W«m{¬…gŸ©ñ�ƒÊý8V�ûTö¾‹O”^,ý©ŽóYuHƒ SàŸÈ˜~ò1ìü$iÂØj%KM‹ eW¯è¨GOû¹†§·žF€( d”½Œ¦Àì'{ØŠ—‘�§’2V& [C™ã�÷-�H•£Ti3c¬j;„ú¢eL’ˆÕIp”Ó”íà 2‚*©‰TJK¨C#VÐ+‹"€1l¼ÊHrXãj.àпóã¥û&R—rP¶q|¬d2‡r�CI§ˆó¤ ŸÚ$<·C}™*Úò'ñ?ñðLüA<<§ÿž½xxžìz"¨ÈiSÒn}j¬�C^u&SÇÁŽ6º“Îbð ­CÝÜnìåéC·óvåù>0U«æ{é—×ß=<Ÿ^©ÕuêeuË ìX¨±–špp·Kç#KÁ'Z}äHt:µ±ÂÁQ•uâwœˆç¿Oqе¨}šÎ‘�£e˜µ­< ÃTÊÆÃUà–6^Â_›1éîB(ŽUhnkC‘! òIåìY–­-ɦJï´ï$QaÀ\”èò^5{&î4u"ç±¶�©%>ŸÚj¼Á6°r5ŸÃKØà‹²•r؉P®ªK.3˜ý8×/+Å-ˆ¸ŠIÃ(ÍÒ Ü Y1�Ñ!XiF¨Tœ ~'ê Ü;>zÒG¥ˆÇ˜gìj]*O>œƒFŽ™§ �“üè;8æ°ñèXωD]†’{¸u©�:±ƒ³¥{ @£¸ý²|õß¹¬»'scí¸TÛPƒTϲޓÁ Š™GÇŠ9é\l¬¿Š˜×¶Ü»Òp[w=.éñ‰à•}3(WŒ�•*“8”é+Z*�9~)ìê×0Sl¤á7Á¤3úxÇŽD1‡Xòã{kËAÁbl¬`™DOµ%Ò§ÉCÏ4·þ·Pø|j7®‹ñËB9O È,Tê�#]ž�Ât÷øŽ%)ÅLæw©›oóR‹å¤Å_â©/·|nXxë~ôP›¬ŠWê^sáòFAï½ÓG#›ª=ø³” Ö‹LáÑÅ•¢HKO83 5#ÌAɲJI…ò\–Nµ0€Òdê*èR„2;ëÛœU}†>ó=aWé]8–íà•¤tQ�SŸ¡løF:õŽ(ªµ™1V“B‡Ê{°8BŒ­ý!L}DcÎWuîQPíô­t*ôbø×*WÑ;‰ 4µ²#)yßåÜdOg±@Ɇ|w§fI¶ÙsÔ9Ç+•…¹DTj3c¬R;„Ž)u„FSæ¢{¯¹VeÁq.0Ö1©è’ÜSi»^a·&Ñe£¿W¹SŒ¿ iÀò±Z0ÁéÑ…`"t¨�×-ß±ÇÇ”±Wì<¸`Ї/-:>•…›Æ Õ÷ªjêy�zÉk€å±× o[ �íb`Ù­Š&T€‡®†ÓK@ Tû™VI§–aá–³§À™„ƒµ¥Q§OÕQ(I‰b|�Ð)Û"*¾Ä»›Û¿ß|Ó¬i›¶tè•Yš¹Ó3ìnÛtóì:˜º¼ƒ€)’qe·–áå³·�à†àrT¥fµf?‹&b“ÊØ C�:)¹hóºXiЧ £SKCçÐè¿!¡Áƒ[¿Ïh»ǾmÒéZðo+Äû5ä®Ú ›½GC…vy¥¼þ…:è^ÌÔœŒ4¦›Ý‹u®¿36i@‹müöE@ ¡P~ÉtšncØ£¤NÐF(_ÊSƒ’,£ÿâ`K/•’e“lN´C3#Àä(Ìz®:ˆÃYlß‚T 7­*ÓâBáØzÆ×mgšZÍ9jã.u–´yêf>©¹¤Ôùõ�öiã¿"hq9"�"»›²àÛ3:†’Íë€�{›ËY�ºŸŸÐc©Œ;›í|PÒ¡@¶Õ-ÌZŽ£G€Nÿ¤�æ¡)}\nÃk‚FõGœçg\'Qìce°w�}K )é.žB´î”I`ÉÉ-q´YncFgÖ” oÂç,ŒP€g‚kø5’ýË­½øÄæ• FnÌ gV##0XSKY»3ZÌíß)”¢Ã%›£ï[B}Á÷¥ÁÅb<ùÒkDè1\]DïŽñƒáI �«Æû#(JC1Cj.Q��H�Љ�Ïö"ÑX†ìNA®ŸáT‰“Ä‹€µ2áòÿrÏP`jïsO÷LêÁ+ãøÑyì&p’…Ö ªY}s“¸íö®¢À#þ„I3­gOÿüô�A\°Ä�]@� Ò�nµ`ذ¸J_ðݸç¢<<¢Éœ�ÍlžÄ±žº�.½0ô´ý|N~¼ÅÕÌÝ6n õ4�ÛkV·”k|¨M€s¡9Þ%®®æ2ô]Ð^܉�?Òÿà²�¥Æè`¶3Ð�Œ4Ø.H9šoK÷3ñ®³ÂL’É©‡µuŒŒ×é{YÂ&1@†º“‹¢ˆsgu £´C|ÔËÝ0X)׫l¨ÚõÙ{_PíÄÔ¬TÁñ*J8¡k_ ì÷�:÷Î�æV g³É¿‹T Úy�*}¥ádùA9�aÔ÷èʱ6|½ï;i(¡�å îB$"GMz ‹¶fq}zî¬Ó§Ó«ømRu¡°²Š¨b³„:2 ?Ó¥¦7è›P,CÛdÂñ)¦ÚÛæåˆ°²BÂiZ»,C`ˆ›íß!4Q2 ßÎ<Í,…·#d9UmØò]MH‚7‰68â‚aÉÀ×F—8ß{ß[G�¡Gý63Ïõò·C°'ô¥ÁøGY ~‹Ø|JÅ$½ÐˆÄ‰{)p#™5á½æåP[¤Ä-(ãÞçþtí†2�›aê8¨FÆÎ!Eˆùi±�ý7>.>8²x)ž”vÑ-NXù—tšÿ„Ó´ ä­O„€€MX�ÊŠ]ô¡ XØýž­\Å?¼ nø<ÃYö HKë`K "P`sR0\+jèôÍÀl5¢mÌJ@>Ý{( po…¨Æèl§œbîX)$¢¶:sBýúƒÄœ‚�ÏPE$Ÿ«#ö”Øëª6»Euì-°X¢TÊÐ-D70é±¼E0P›Ë"Œ�Dx¦ØTà!�(®I™‡º“K@}ía!9R¨O�t8&Ø û c/ëÉL¬È3êkð>ø—î$÷yð@ü‹xÒ‹ÜǺ#×9OMaÃËy 1úó’~+%„~®*£…€³dFª[®Ìv9*‰�ö¤3E-_—΂ãÜ}“C Ñ7‰ ¬qÆã ßB`çS�Wx �'é��LŽ×þ´þ\Rgõâë:ÃÐ^±c ²ºdxñþLéN¦©?–î§H÷Óƒ¤{úÈ{=ù2T7uqGª{#iÉ�q“2l|`50_ˆ:¢¸˜7ª].³Ž¼°Þ »q˜w„¢eœ”�©ãÇS1ì)åIðC–ߨTÔḔíµrAÎng1³ƒ¹›0ãA eÕ£óժߺ‚A/sÃ¥�{3q�Ç{Ã÷ h¬Éw¨q•®Ê¬OäXj^טP:6@\·INy-Õ¶˜7–0§HPesEÁò²%³¼9±À7­þ­«ŸÓ¬+{<ÍCV§”Ç$0(|à—v)K¯�|j;fT•,r[ƒb¾Ó©I,4˜‡“-ˆ> �c¼³îFý ¢O! :¯Rðz1Ô(63±±’„¸]C´ƒH5XJ–@T€E¾ÉLLY²Y‡Š×f•Äül™! 5Ï(d‹RÀµïÀµ’׸¶ÁØ®ò x½•&'=£� eµÌ€¼œ/à·ó‹Fvz®)ˆEn¦&y¨Á©W-à5Úh8fZ5‡/¹uzÓ•ýýæêýë«o=ו�dX ��#£+8]ŠùõügÅî¼̰º­@©WMe‹>V™Ù4ÐØ½××-�±2�I·X�²–C*Y]Ш0]ÒU&/™užu@[Ý•”¡ü­“¨R�Ë%ü 4‚˜O“œºmß÷Š“MÆ Ø Æè¾\jˆÙXìߦ#Bõɨ ‰2תÁê@*;2 t€ÙëF*Aup „Þ~àÂ5Db˜Å2�ÉJ¯­BAwXC„ߦHÊ»vecéc`¦l3`Ž›œóv¸”tèÁD°‰¬[<ë‚¡+…E1Ф*dy›A2òZ… ºä:ËÂeŸˆRÉÊ�<Õ¤)åAi‘9GC5i¤@Izío›®ê<%Íz“s¼h&}=+:D¬?¡v(‡·¨Ü3§G¾«_†½tµÒè)0ÂàÉ·€ð7(Ã%éáb³qRn°eJ§mN2Ë¾È „B�U6fZœ CAâ\¨pïw^gAûQI]WpÂ÷uÚH{x¬¼ú>qN}<ö,Jl3œð=@b%@x(UÓ‚�úçi¨FµiÁ|^þ$�…®ÏÅÅ©}”€ÐH±©|~ôÌ€÷ëk4hèÀaŸÉ?[ßâ¨w2$ðK8¯/Øñ [*?º´öžñë²ñ“à37�Ð!ØÜÒsŠôU`M¶v� ÆMö�Ð�0ZÖwCø�¶šGÃ&{®²l"CDQ��#?äN=@ï $B`«4c‘0ìêL/"pdݸ(Ÿ&%a>¼h•Ì6à ¶m®´uÔ¹HŒ£º&“ýñYh»*§ÀɨÄÑB-¬Ý"©cLX“çëP¢Y�ðŠòÞêÕT £ë`ƒØ¨^á¡ƨ&€±–(Ɖ}LµeÌG†ò·%hGµ/@¸²h¢Ù¡ÿâTYë�ì‘óZçÀ&U]Uj)£$'y*äˆÞg£t¶-³»¤näFQ–}œ Îw5Aà —‡l’k«½ …°½?š°ôš½düš¼Û‡ÃÀºÖGoƒ ÆV�Aý·›ç&Ïm6p91 @Œ—3ç¼9×ÄUä8¡ß’Ù Nû3AlS·µìëkVÙð'Çj!jx²²a´C–¬®Y–JÀÊ€h«ÜxÞW�â0ÇD¼­R;k¹±æ‹-“Äls -OþÛ×05¨ÐiÒ ãpжŸ±¼¾ÕrxrÇNÅ ø.¡©ðël5¿,ÜŒC;õvî9èÕùaØ«óak›Ü]¬ä±‹o~ú.þNn2~úóPG&ÒÖ{E¥ÖÜÕÔ9ºRå v´ñõ-|‰+…ìkF#¤Z0o¥à¨“Ï僈Xe¾ãAφœyU4nûb½� ÷s‰*¬¶­k2®2ÈC÷E4þÉx­q}3Hf»¶ºÓ¾}‚ËfAÆ>êQëwâ{ÒΘÒeéQ/þ‹Âë¨'6™ E™sޏ@W2#äk[kPb»×ï\:Qòì-®Mû„–ym&×má¨IB;¨ëÁ62=qì'ÝûH)ÄþÏ\góv‘U]dœôŽOb„ú\gÛË;è»ì]½ ór,žŽ+£’à–Ö¿ƒy›xŒøË'ÑPDÎÞ8y¡Ç[#‘ûdFâ³ÚíÔƒ5sn縻htB…T WÜE•ó Ñ´O;D98Ù(…µC‰ ’®7‰î&:ë“®0zGçdÔ8Ùó×ßnçäÓ=:'Ö9ù´/¤È¢YmïŸÃõu¾¬¨OË!h‡ÂW5§u̦Qo¥­¾Çµ˜¤t2ùpvögŸ¼×6Q<eHnežc)œ`QŒ¹…¸ÛÙõ‘2ÅxØ"rQcóšœ{ÌVˆò›çã—æó[W›g¯aû—]Å_úT='`ñTßÌ5»„3K­¢YqgEÂmn‹®b·cÄ¢€åÚµ� @xÌÎBF Ò8‰/Ó±DÚª8‹*Xw �üøZaîÏ(šiîþàå)W�é„�$cI°-ACi+VØø€—&+e ¶%K6M‘s½}�ØÏ’`‘c“þE\�_K¾œ>@ŸÝÝ’ÔÝÏE@�yþî–Dq8èàúâ0ƒ/:¶¡[3¤Þÿ§Â¶ÓþRž xL¬Xˉ†0nƒ¯�Á9Ë‚‰ëÙ‹.önÑÿww9ÿ¡õõ.‡Ö�ø<¯6XæV¦ü;^ 8OÅ䨾·G¸ÊȾ¡¾ÛÛªËFî¥íûƒ£èC¿Æ­·Gý‚ô¥gêýy�©7G[ð~x¤°[jÐÃ%óøÆô#bÍùe0k=d‘ªÚV�¹?Ño¦ ~)kNôÜiw�Óôp[¯ÞÁ×à.õB¹ßyðíàñ‚wç¼W¿¾eªµÌ,uüÇß)vürÅEŒýKµ3ƒ¨9PÏ Ôœ[m+k�CŠ÷t¶]@ø‘æ÷ªâ‹”ºBÀ;G ÉÔ€£U¶ƒÏýzFß7¼CÀMîê6_æ Ù<(Õá 5à„÷Ï.ì!*ÿ»Aîö£4èv $UpÃ2hhOHŸûƉòT€‹Ÿs$–Ž0Õ·žé•l”/Xp%-¼Á—ÃdÜaÅy›~-Ú„zíé¢(¶àµhJ í ç7x3¶°•ú 6Ž~êÅ–[ó8åÄeNíîìH¾˜Ú·–.�­W³è’b_ÍñÂ&O¾Mj+¸¼‚<Õ߬Œï£íºx޼2—é5"‡lE}|ÛžZKø2Ž¿ŽÏ—SâðVݱø{LÝ�.|ÝrÏk¨¦Oí¯Ÿò)ÿqp¢ ž žŒl�'™\²xœ½YmoÛ6þ®_Á±_“[Q`X­ûŠ¥mPwkû© %Úâ"‘IÙÉ~ýŽo2eÓI¶ÉâØ¼#yïŽÇ;rúÓ«O³/߯_£J7užMÃ%%|5TTTD*ª/q§—ç¿â Ð5Ó5Í?Ó•¤J1Áß^O'Ž–M'~ìB”wf¦Ÿs¥‰fJ³Bý††c€™M ⤡—8fá|:!fô/;R€�M[TÔD©K\=CKçÐÔ0¦%ˆ›.vÆ/`bÛk:iÍL9zK:àŽZ) èˆd?5г¾È\ß’­ƒÜF�?Ãnø ä¤ÅX‹¿h¡Q%êR!²/iÉ´¦%Ò‘ ‚kÆ;Ѫ\Ñ3Dx ½iÉ ­�®hÏBK!§T0цéʰ3Æ 8ÀŒ•(ÏЦbE…H­Ì£;ÉÝœ 0^Y¢’®è.–ˆ’¢Ê‚pC3XHL`\S¹&õB_bhL9ÅÕ°6 碷mÍ ¦Ñ‚(`kÖPå±¾ó@ÏP[wFS¥$´”¢±�²6•PÔ¨iM$(±°rVlM9ZÜÙuÜPÉi�–·øülŒ—´¥ð�kÄ|b™¥ÖìàÌÙª xüÂü¼-‘à§°deU±ß=CDRDÁõbì °rËö7ã+‹°+ Éã¤F5»¡5«„(ƒ‡�;yÞmn(m¾1¼$Œ›9Œ±ƒ�|Pz·¸˜û,‹yWB©à[Ø…2˜.p6BÞ ÁŸ¶hZ²õŸR ‹ÄL�õ®C|ç™ëœØ*h¤®{?ŽV;“4ÕÏtϲîzÁ®‘«¶çK«Y»1í&RZv…23šà«|N©õ~ˆ OÊ‚TIº¼ÄÀã§?®(‘FÝ&>qâp/–À²âQ‰å»� N+ËD:‹u1ÆE½DðXå#^õ4ïÆcÀîh©ÔŒ*+ç×=ÁÌ ÃŸšxM66k™Ouéãä£&|2X×7œsÓúj&ƒ�w@°…ñPGž€°ÿ/ñ;ο§$~Áfl�»æÓB™¸f‚‚Âù!º,¨4ASD=Q§\p·Û×z© ‹c¢ý,6êƒóð+©,àt‚�Ì…{pœâWœM�Š”Š6”­*ãìã‰í]ò#m…óaû~'¶' 웘ŒŠêõm 1”–»èÒôÊÐ3„ӣ »Ì ÁyܺwO�‰g§Å <¸ u¯)Ð^’œBøž“ö{¡ØŽÛªsT¾düu¹2¿R°€‡¨a`Çó†é6Äyÿ3Çqú؇¬O‚FU“É ßø¼ t7l‚8�ôZ=t™Ôû5¢Oô A—KÀcìf˜ñŽ‰Ê¦œ8dž÷áréî~ª;&š+±ºêYœšIçr1 ;� � îƒñˆëÞeq¾CHÛ4ÿ§ãﶶغþ°�Â÷FȦ`ÛbhÙÜ{µfCb|)q¶ËÖ¦–9Nña?„·Aó!®!74ªlG…ö¢n+Ø®ö+å«Ë}â¸o³GSK6¤¤¶XÓ9ÕR~îRR0çäk:8Þ "uj4ÇRaÀì»Cx„…­Mi°;(ú(çÜ"„¦™Ô'Ѓ6�ƒè½Ú±+Î÷HI‡´5û ’1¾è#à‘µ÷Ž)€u)ÐSRH=k_�ÞÜ#‡Ã—³WƒX8l'»Z7¥…,ᔃª¤ª0Q,w„¢çJä)½YW;™'e`uƒVÚ»¬£àœCêM#�¶�Ü.{IL¯2Œ ÷ÝðúƒëS«McA@ÉÓÕBèƒ]L†ü·Ì�D4É1Šº/ŒnÜp[Ùõ¤ÚžéjvÒB‚ëó£­!€î�cÌ“puð"ËÝQ‡[,½ÿãV<ÕîÒ㻸ÝK¸½LËÜx>â}à_ç0<¯C¼½¿?xKñÐc@÷È`ã›g¼sýHØÉdâDðk¡`ÛÖöv<•Á6mçKÞÈÞ¦»A}"í^YŒÑ5þC@¡#ºÓ§Û ¥a³Ìû‡7ðà]ÒCàkJÖô\pøtÚ,…•©O´€ð‚£‡‘\ÃCÀd¥{µ8:,÷X€·ï1©3ļÆßç”ñJ�Éì”8ï&o`T—}þDKVÓ‹ä‰1ñÉ÷4ý�´ëÅ«f�&×PY`xœå<ûsÛFz¿ã¯Øªs±ÝR’eŸ§®,kFqâÖsvìI\'žÎMf ,Iœð v!šý¡{¿Çîb)Švν»™D&�}|û½_Ë‹úîí‹÷ß}/V¦,.“ ÷�’üS*#Eº’­VæùQgÇO�Ná¹ÉM¡.TËVi�×Õ\½o”YÕ™¾8å—ÉÅ©]d^g\òìRirmòTŸ‹-“aTr!E%Kõü(s};º¼8•¸ê£Ë£`½G0¨i!µ~~´z"¦¦u6ÿ\\ÜÈö²žÿåâ?ˆçâb~9Xl~)îÓ°_xÐŒ'}äo.NiÝ‹Óæ÷Øóý¼ˆvýQ馮´ú Û[_†EÝ–]!¿ÊÞ{`þ;†¯æóvÿf¥Š"ožÙ}+�lûàFÝ4ú/ÅËÜ)–ªR­,òÿQ™�Y–›üF‰0(Z¿çIÂ3³üƤÔÇOŽx1>ÄêX’ö`PØÐtm¥…¬,¤R#êEÂÆ«ÍÄ:7+QJÓæŸD¸²HëÊȼʫ¥0+%šVeyjà4™42�¥3q£èAÂp~Í«®î4ÅüL 8Dt¨ŒŠü2‰7/;mÄ\j/Té1%Íêèò‡oL^*ýìÝÅ©*/EÕ•ªÍSw„z!òªé m"Ö+Õ*ÑÖk §¯[† ¦´hÕÞ€¶ª M¼.º>ÇÃJ&•u›€,—óBéC»Î‹¡í´¢Y¦ Ðñ‘`Äx^8fpÔ�ƒ£n=èY|P‹ôž#䆇¥õ OÑ)N&ÀZIàc ò�àösÕ"~�Bêx²=Þ)Òó¼|€è%“Ü=ÅÜÌÛ,�u% @@ݨÖä ØA7*Í9l¾±À¢–=þÀ;²52o…l—€úÊhQ€mà ª¨×’l�9Ž-%@ TÓ³h€àaصAv¡oóºÍ€UÝW]Ò\´¯ñâM›—²Ý€¾3-ü¿‚#ffõüèÑÃ?1²{S†zŽù·'ṅ«<èÔð'¶ç´t/Mø¸÷Îq�¾ç¡ÉÂÑ´‰&>X‘!{¯gÖ��€O�—ñ¿Â¨¶ôlf59¿þØ›9PiE3‰• '+jvžD˹‰±6 õúûè:{\‘‚† Ô:Ÿdƒ�w`>Híø¹Ä¤øñLVç‹—È0ŸdÙê<<ܧ3ñ¯Àaøç1þù#þ9;ç'�Î?=öw´ÖV× Ä±l;¼'â.oA[Ó{'¾�uþ ⸜ösÛôˆ¢íãÝ7´‰Y×@+ØX¦DJB"δ9~+à*�Ûìø�fåA¢€„Ý‹ ømUÐr@%^ 4žIŒEÜ•09s0éc�ì“4 À D¦*eµñ;£p´jŒS¥@Á€Ï€¸,-Û4¯Yf'EˆÀÂÓ´èÈÓ˜×f•ÄâÅî> ˜‡Ï·§ö çíTARõ4MaøM­ÙÇU– ˜UÃÙ΄•«…ì^@€{¡¶'â;$D˜+³&þ˜V$HtÖvb  =ÿ:úX= Ç\�¨$¼£áì�<�ì&E–/ˆ'€”CA…Áå|›ז¤d�­¦l½çøv›XØV6�‚¹lʃXc.0Šv’72/)!¢¶˜O‹µ RÀv±ÂËi«sò¯èpÖ›§ Ï ²£²¢A�)ÅF¢xœÍkoÜÆñ;ÅVEc§8Y¶Ó´…-`;qRÔNœÈpA±GîÝmLrîRÒå×w»ä’¨ÕRܧUÿâ5 ~çg¾úüô„ žžTw�ñÝ*ïáüQÙÊ”V½—õwÀ¥ÿkSM.ÿ˜gðþ;†KwÜŸmUžëê©ÇZ¶lö7ÎeÞìgüR¼ÒNHñ�l¢,EU›€‹ºE" XŸ?Hø�LŸò {üåaÒGÄöTf†<V×Ô¥�J&)Z ˜³ÚmEŒE¬”.7Âm@Q™N�©‘I0²Ìz$ТԔN—�iK­0kzdVVÕç` à¦ÐeMàMƒv(×ˤODÑX”JqªŠ–{Òm�–ß]ªb)ʦPµNÜ®õ%bm)&‚aw[U«DÔæÂ¡5˜ Ò–Y�(­64¦xœÝ[moܸþ®_ÁúCÓ›9à€CncÀÉ%wA�Ä�Óóå#WšÝe-‰ªHy½ùõ�á�z—wm[ Àá‹£ÑhÎ3/”—úåãëÏ_®ßˆ­ÍÒ‹h~€LðGVŠx+KöåYe×O:;ÇëVÙ.>Á¦c”νE© (­³<çåhyîÕ¬t²'¥Ï/Œ•V«bóBLÞŽrÑRŠ\fðò¬-õì�³‹å¹$U?\tð®ðz!âTóòlû£Xüï)þjñ®X']ƒ_„GîÅru�÷¯Pµ“Zž¤éB\—�¨ØêR$Òʈ¯'ê><#3OÉm"^p*ÍÂéòO ›Pú |5e·�½­7ù¼E‘Úïøï7ÈS�§û tŽþ�BÿËú_f¢ÿe ýO` „¾üç]ðï�@9ü™®úPeWÊ{ 7T;帞ØL7´ �Š"+(…^ Ý’•ñn(¥Ê›�ó¸×E¡�²ê„Ê-lP­‰e*ÑDZ®rK’4•zgÄn«É2@ŠîÄÀ�Pø"ùæ™@m(~Ç€¨Ãˆd�>H¼\¼¿ü|uùŠÂMW·Ñ#ŠG y^]YÔ9ÂOh÷?Ð/SØ…õ™ 5jFB€\‡X‘u©6*—© ‡ï,Õ…ùX`šÐ9H!ƒÜ 4ÊÁàéžÔ ôxY¡¯w5’0…™³k™"¶¼Š\ÕÀM" Ôï,b È ôJç¸jM º¦€d6z·S°ÝÎÄëv ¨V؉¨Í™ú»+0!ÃÊ0­€Â1þ ‚¼fªŒ~�JS÷Èð ¿ë“[¶ø‰«Hl0¨sÔ…x¥´óD¢+LN³A©ú\›$Å®ÔL¸úÊ«2ê$L¦PÄ�¦˜áK¹'oR�&c‹01z†á#Üb�VYNBlëÞïÍèûÝùæ¡@d!9έãÒ3Ý;¥tèæ ÙböSú›y%Që5`åEeYäýÝ7Í]äcOUXaKÌ_H°Ä��y[¤£@ŽTlÇD§ñ( Û/`æ'O•dãdÂiÉÌM:U�Ô^ˆ!ÔÃd¶S^7°Ô NnïQá™nšÐ9ô×;ÚPóêè~:´Á}RFýºŠŠžBAÅ„b†Œ‚«‹*j!&LX™w/’T¢G7pôm`½Rù›d3M?a}&$�š! ¸&€Gû¾E.MQ”Ûrï¼V³¨Û3òtsýÏ¥ùW¥FºPÄ΄]¯iL”‰K°ê+шEbYc[çÚÈVU䱡[#—µ±_ßû«Ô&þ*+ôŠÌÉ%¸ß 6�w®â5�¾ÓÐs÷Ôž¬÷{«ì{À‚{²®fÜÒ3D˜WêòŒUX<±› dŸ �~ÛפÂj–ö0Ïñ(y¡‚œÒ¬Ð«"u÷PM£w¹XPEô3@Íãb,×ð# 5'˜&I?HJEE’4Ürˆ+’OŠÌŸ¥T¼‘•ñ�Ü@0¡U.±s™3;ùÃGy³eøòßw¼í%¸ Ä,ƒ¶ë|CG0¥Žr:_žûëgù oeÜÓ_¡Ôõd�$ü�ÆUð‹vÅ× HÑ÷[."œÞµ®Ø+%ºäÊ%jÍ�ZïÚz¯Îë6§DN‚îo¿ý»�†´‡ÊXË*E|q·I~µ5Å-á…yI�h.oKµª,�xºÅoÚIä¡v@-xs µkbçuC5º�ý9DÛ½ë•ÆìùªEœ c(Ú»«öc}¦@{ÙèôÞ1£ Šj•*³yãOùg­ õS94Š(c�eŽ|P›Ê�JhtçmûXø¡>ÛÔ‚ÛÇ%Ôɹ³% Ž}§ö~  V±íÆ9žBMˆíWÓ¾)™#M|©¿° —1å °½ˆèšÖ¾JäYö–Ö õÙDà5ï¸ø½±PÜ ¶¾–°ÜEî³N¡¤*¾§ìw(WÚ@k—×ÖÈx ­üÕÅ’\,[m«¬àb†<ÔEaÇ–b4ÎÎì�CõyWjf¦è+&Š·Øõx£i’BUü�£!zífœ1Ð6(º£CU÷\o·˜ëq‡w(î>ïèºýõÐ9Ì>Ôñ×ð¶s´+È=°}<ìÉd&k¢¥'Òž¬ò)8¹ € Z�±"Xè2qLíÓsŸêˆáñYEGè4 GÏ,2yW¦ˆ¿yX1ÛE—i±�ìƒÜâL—xCWøƒ¦ölΕ‰„&1Û¹¹ý‰ŽúÝ¡ró”�Å›öSiex/¶8hv¾Ía‡­—ÂÆ5L€dÌm`Rà³9áš�Ù_ Ô öwxtÖÝ—›‹×@ݺ› S¡ ÀõÛÍa4Çnë1ßO}­še×ö§ªíŨ7­‡ª$¾ÃöFÍw眼;r»�³:W?t�òOÀOµ¯g¨žØ©>š¥ ¤Ô‡u·ü1Tõ ñÐÙé¸*XN¥ÙAgqývW±®‘@ÀëaéšÉ/ef»ç�éÅ‚3Ý4¢o„î· 3çC#ß ~“óŽùj¥ŽèÖÓædëwüMQy²?¸½Ü©ö`­nè[¿4܉>tÕäaq™îê¦gÍ,Â3gxgéÙÝwŽ±Î ôïJ¥ÊîÌÈ®c-!ÆšÎôí‰éÓ¯œ:oW*L½<£Á´)Ý`Â3-Ÿ^®*EÕi«‘êõ]¡oq´ë»ÈÚoHa&:Àù|¶æeÂÈÏÍ,�Xè"öÞÇȇün³ãõÕë_Žè4ºRs§þ=e#Ǧ©ŽïV|A•SÿŸ€‰)°ZµÿÕ^’b5a]wwÔ“Dßo$Þ}TÔŒ( @N÷IÓ•Žu xª›}cY]æ�r›Faî„‘\XòðqöjÏû…ZàåpŠ3¬Ìz…ق϶[szEŸî¸áû鹡/á¢á¤N…W6R4 |ÆQñ_ïýß�òÛï«G~2èüð �‚§p:xãLèŽÐ?úu”_û̲íèpgAÉ›6é ÆŸzHL¹•ÕDß1¦:YR)Ƴñ¾É†�êz‰)u­B§ÞÈ)gŸ<Ðn)«Ü�µM…Úù! áFÿÆ ß¦}.‘Û(Aú^®<ÑGs-}ýQ/ús¨îŸøpà áÛèoœÖ“Q u7>õiBÔž’^â;mrâ(c:ÍÀm§¬žãÏý_ �óŸ#ýÊ.ÿpgú´áÆR“7üÏÖJK•E�´ˆ¼Ÿg‘÷¨OÞ¹"ж&ÏgœJ"âpI#Ö‰=Älå¹"Ð9AÜ+U#xÚ»´#æãÆN�û$ŒÏÇÒ\å~‚XUµJÌ9À[7eJ,[ˆ�>W¥Xí¼Xx9Ôaͳ¶ä%À �á`ÀnÞÅÑ6aYÍUFtùûígNÊ>}^‹]å Ò™ÕòB¬kSé¢}4(}™ªü¯tÀ£™õàªÊLÖ™ÈÔ¹&r¼VžéM!½‚y)ÚCc×–ö¤.«\§À¯¤Õa[cD%k8§j–·1"!‹Ï àaï«0B^êBÿÄ&7¸Uot)s‘ë�*×[c²E�¹ÂðÖ&Ïñô‘' (P”ÒÜX�ù‹p7ªTµÌõïp?W`X…ý­jlKNP×¹vëÖ5‰, Û&™±üÀŽMÅ‚d+•êµ&yrÐK¿g Z%u ,Ý€–λ,qUKöf'W¹:öR¢�Fšîõ^fÁÞTŒ¯V¦Î@@Â¥-è]Œ‹úÀA�àðvà´\ ÿ¶°¿ÌmŸ}ñøOGÌÏÎ×C€µ�×üõïaÍûŽs¼èÄño‡è¬šr.UþÊkç½Ö§¹,~ƒ=§hP¦ ¬ÁzÁ z)Lb¡î”ÖÈöÌQ™»>Ò[†�44ÓFÑ$$¶²S �Ë«N¦Ö²Éê—ß ešfJ8S-ó°‹ ½b›ª253 œ%…ê�;ï9v=§Þ¶úz ¯vö§K�Ô ’rØ'èÚÀLFÖ¶û”OUnà¼ÙLÙ`)÷VæX0ܘ°Ú!�W‹ u,‡äÃ)�k}©²Ä¯›É‰hÊn/2CãÒR’öhJ¸¼×š[<Úg7z�ëRÉ:Üžø'xí ŽÒ�D³uÀ“`…åD¯�0&sHê\î‚­>X¶˜ Ô¨õ8Ц-ÈCBÑXö›KsQ©0e¾C7üÛ=ˆ`9kÙØí&>�™;!7vÍV*cA+ J±©Ìe½ öö4¥�†ØA�½óèÂÉ[Àv¿‹ðaµ÷~ØV^ú³œ³%ðÇ6ãwÔýÔ±ØÕ‘/­Nm 9Þ väì€%¨AObÖ‰�ÌÛaJ×2·êç÷Ú\¨ú…iÊì°“+|'( m3Ù·O ÍØO�ã#uüXüYŒ�ó°íŒ¤{¦ÉZ!\z8êžÞëNÑ´Ìçü¨‹èL½O{ð��JU{ÁùÂO æªï&ÍJýÌ­¿åÔáFÛB¾C‘ìÅ�té¢Ê†S*èX§[¼ âfØç •)³‰¸´ìÒ>qðΟé%=¸�ädÆä¹Ô9†M3÷ý=û;UßÂK™#a+êLÀÕUá7æcLÄ'œÓ…ëœ%ûÿ| ¨À µÏ0Ji“6` æ?Öä˜iwj~ê%cÑÂÉWëM•«rØ F¸…†“9.Taê�xñ꛳pJ˜{b\M1€O4·iƒy¸M —!…—$ÿ�@•ä3óŽè¾Uœ"e>çI!è¬Rº5Û±‡Á)‚©¬k­Ð}–»H[â|…xbÁŸ§ÛNÑà�‡Ä”KPhÐOñ¡\(½ÙâÒ|:C=öEºG¨ÀE�Ür‡òÎlŠÌ �ºDO@$t†µ+ÇáYU�Àû„ŒNH]�C¨‰Øº–àp(BOUžÃí§‚[\×` @.êÛ.ÊZIƤÙv¶ƒœV¾[�̤+Ješ7@ÃLmù‰ØuMÒwUÝO ´¦Á¢A¼cd[—jC†-.ƒùCÚ È(öGç¾FöY 9±y"†Ÿ‚KèDŽH gí‘�WºvT²¢jQÉOÃ¥Z¦ü°˜cpSN™��üÏÀƒ]ˆrlv¼öR­Û¡¿1ªŽÎ£9tX�ä[„ ­Ò$1ÜÏÆo�®wÁaJ\/ÙMB¶¼�3ýú3^p±Íóz,�ɵyá Õ·ð—LŸû‚r¿'®¬F¡I}òUœûËÃÉÂ[T�kuE£¹%:FîQektìÝòœµ=ôóÕ/½7öÉYŒjº–ö©0bÚT—_<þïàùò“ã©%—¥œæ |žW[ùã7à ¡ÆÜsóêöá5UÏ0P}ôT„êü�Ñ(=ŸˆŠC$ê“ ž'dwŒú0¹»Žõ3qÊ¼ËÆÌgRp'¹:§²v†¶¼#5œZÊw ú*7eL‚RÇ(Xµ&^ûŽ*3à?0øå¹à$Š›�C·@§ÌÌ™ÊR‹ºÖ«ëäQ#ë:5éŠÿÿ¼Ôs¼ÔßÚE_)›z<£Ù�¤Hã@`(tŒd_rd|¿4ƒLøó+¢¢£ÆÆ­v»ÿ.V0o‡!„ÄýËÏ í<�:†kPûPìUÒ ÓÕÛ¸ ,Þ<÷úù J]!êÓ+Uc5u%3iõ½jLl±ó´RîB�¢qߎ²’795ª)ËkµVâh vè£ÒµÀä¬T¡ûìii,è¬e.@—ÊÆ§ý7‚]š�íQ€tW2‘Bn$âÇ,Påë6�¯M³Ù»”Y@X¼ÞñÀh®Î(óæUT¬õ�pxÑ8ψ¥ksÉeàµvhU¸•Õ2¹ jÛž•ͺbB¸U�n­;cÞ«®Òèug6\ ¢: n X@ÕxåÚ9m³Kâb±ÀO¸xÐÕpÛ"c­ÖTðŒ«2ÄY«7¥��Å`¼L¹Yž)EÅß'âôÄßJ„8•b Ÿ½‘éV—ê߯!K£‚Nþ8Voª£%ÿÄ¿…ˆßz é(l¨7òG/îyp´Üss ²{zöþ ëÝ:Zö.¯ðÍó¸€Kž[$n±6îvôâ25çØÖqܽÒ.ž|kÁ´wÑô'Î/o:å÷a€ÑÜ�}Õ¿þGéür—4]=ø!Ä8ûpFã)¬aEúˆ¸%Þ›˜ô%�~Œ‚ö§T\*æá•ñXÕa“�½Óô[˜É²vœ&ª¤(êo÷§ôÆÌ ¸00˜ôá.Vë ʤ�=tº¡4éëÿmÕÔFU‡­´Q•g4et#kÙN¹å«¤Ç—=})`‹Â*tWbm9æ½H²¯k 'û¼d�²DI°x¸ PÉ•Sð«]Û1éÆÀ  *™ñÍríÀ·¹©/ž\^ðŽ»ë¾£¦D€r ¢©¼ë·CôgAcu@ÄøbUq÷PB?e™p ¾EƒÓ£Ô�‰ðBÙr•CÜ㚺±QPôèáCá§ÅýGâxnç†�›Þ¡ ׆áNj[rwÈ×!†Üì™üðp!ýÂ(�â8†FIG‹>xø%-$"­ÝðõvÉ©ª“) ) ¸æ¦ÅÛj^÷:Õº‰Áð„"4Œ¯¨¨Žb×ÜÆ7åê� Þ䉡ð6v`xq®jn€´–Je¾ûgȯpw�Ú ßãå†F×àà“¦)–Œ;xÀA »àB|õ`†;…ßšÕÛ«}j¼æ¶ŽµkìÉnáºnèV¤h‹ ûBŸÊ›ÅÔNQÚ³ÕqÑ:9Ôm…òv˜²Áá¨~'0iÍæº©)6�«g}ÞØhdmЬ�!ž¹±vJ,ñÙ¡âø:–E0MÝá-‡àý”ŸµÜˆŽ[~T2�ó6Ÿ’¼¤î›òÞ~­�À9^¸¥Êu$ÈÈGO …�__þª®¹1ÙùŽ�Úù1V²!ÃQ˶8èˆ×¡;¤£Î'Ždëj™ [ï7p‘‚½¹‚‰ »I KÖåüíB·•+ñ„ó".Ì¡­Å?ÆÕcKÒñeø�F|"þ#,œø‰Eã‡;-š7P#¬C©Êu=Ê×éUSvQh ¯°ã9ãBv÷ºÑ·ÊêÜ”8šTò°c4o|Å {ûq fZÁŸŒ3‰ÞÔ`à`·ügL Ì �^_á€Ú·òB”» ‡®EtwöÀZ§]ã>‘ n ŸðZÍL®2¯�£=£ †ׇ߬!Ñ·aÝ·2~èOÚȂҀutÑ�µüN–²† ÷ŠÙœ 87îõN·Tëu¿º4Æ6#Ž!Wò\C’s ! ¯ ƒÉ»­0Ì܈þ)Úû¥Œé�¤n€èŠ�M YWNè>*z�£h �A ±žNã;6½Häo�Æ�ÉjåÓ¹Ù±2�Yð—j�SZK¤Ø¾ƒAÍk%³r>kÒ^Mš&£�"–—'þ» 刟œkgéaTÖ\`µ�O¬$êéMö×½Éè"øò"LV¹ÁgRƒoÿ¢êNŸúhcŽê¥5¨*ص)� ϵç/ß¿ÉòØÔv€îܤ_ƒëVV!aÝAgÔÞº­ï“è¼ÖˆËqšú?×B©VÙ$!¼²jtî:ØLã¡N•Ç -¶H!ê¾kŠð»ˆµþù è…SßÂM0.£û¯ÑôDz ®—ï[¾DŸm­}-Ô±›HU'_ãÙÙ9ŠÂ ¢I=áÇ7ðGÌÞ¡p3Î�ÜÁ”¿t‡O GV?[X÷öÔ|ì»úì~[Ê#K‰Õ>¥2.GúP7 IÔ–¾XÊ­z²oöºWÕÁ7h"•_â’`F“ +o^¹m]›ªêlÿU‡oAäˆsSÇß.¸‰¬–Ñ듵‡5öQ§ŠKqF-’ û„Å�6r5çè[j¢DàkvÒèà˜7ֱǰ*.¹-¢G™±MKa?è°Á÷¦çuâÿ`Ì ÿ)šÿ—eÒ¢ ” ”��+šxœÝY]oÛ6}ׯ¸ËḴoºÂ еÙZ,í‚¶/}@K´MT"5’²ãþú’¢¢XŽâ¬zÀ¶tyyyϹ_Ìì‡7½þüåöšV¶È¯’Yüà,ÃGÁ-£tÅ´áöò¨²‹³G<·Âæüê#_jnŒPòO®%ϩԪäÚ nf“ ’Ì&µª¹Ê¶Nñù•±Ì cEj^R¯ È&3F’üòhWr|]‘+yt5›0§xÚ±'Š@Ó%¥93æòhuA ƒ¿3ü´XŸª¬{˜—Ñ”-ÍæW�¦9¶óò³Iét^Ñ[–/È®8mDfW¤Âœ i¸4Š5§9“YÖeb­)ÌÙÅQPõФ’’/™7)Ë™¦…ÒÄÈTe©´¥5O-,] ÉG•Ä‹Vz¹„rŽC‘ù§b8-„Ñf%Ò­˜�z(Âwgɘè�¥Œ/X•[CV9»ÒríV[‘sÒL.y<– ç!µæšÎÿò«3€8À,˜õrš„€°Ì˜Î }-p%Gë°EðõOãóÚ‹°�K(g¶ÞJúƵ‚qŸWªðBƒIgJæÛqíÅ ÜØG‘ßÔÝkl5éúˆò@pºìèë’”6ÑÁûA~‚2¥ªéø`…·b©€]x·À¯¬JÝŽI Æ +æ‹x8>99Ys€ŒUjn¸^{͈ Ä­rI\@P{Mn…Ý(�Ç '«ü!âŽc”Œ]еÓ0Ç#g*“ q­•®aÿ>Ô?Ö4ý FDEч|Gxô÷èì2 yé=K¬,sÁ³: ¡Žg"µxƒî ¸$Ͱ@×ä ç`áw ¿•Lýn+€€èöq·a.b«°?x…s-¥ƒ¶K,My‰à>„Ûp¥?à½éCà¡äîßUØõ½qµ¸yÊÅ<ωiͶné®Ã ò¿Kÿ’{—ÌAäU!ë�%.Ø�c3fÙ(¦ÞÚÓádÇwçǵµ£æÑô¸�ˆ@%ÇÉ�ò9{©´@zh|Ö ÕÞ öˆÞ.pï$^ÞC—Þ¯;F­61Á:D"(ÕµÔW%Ž*·½×K®!-rì7hžrl<$E9¹³SP·?8v+þ³“Ï^n‰<Þã¾äùþ»¾+<;<Õì_1D{øˆâƒS±P#ѯ‘a÷½ø¾túŽN.75»Öù‡ U2ÇÉ &ÚpÍ)«ŠbKN>Cˆ -qJL†l”"¹¹|—#dò‘; �&¡Ñå%M@�öØ9Ìy“2QpÿÍ *¡‹‘*ñzÍ 1ö¡*®ãVoš�ú8²ÅyLñŽ4½WË9q®¨™F¦déÁ­¡kê—PÙn[�l§4þ¼9�@ð$WË)�ÌÖL_•³‰û8¥ébDç§§±&¹¹£-’´ØãûÅ…ªt›QKgÑùôEët°è½rxíÚ£òµŸ´†h!ÛÌìo"Û’ƒ´‘v±ºåHÆVñR€/"\Ú'Zô|ö±g9Ö"[r:¹™žF·¿D¶iŠã�2ñê!ÖÇ$HwùåPA~Â6 ]J°)\†ÙUÝ ¢˜ × Ó–†�OˆïÞ®¾%6´u¡ð/"±ÿ`ô€˜Á‰Ï»^B‰^‹vØ4ãÑœ/\i¶íîÎ×w)Étl?8êÉ×úF¨Fq#2¼ŒéÍ·À…ò·EÃ\Þp†OÝ[E‚Èe$jÚS»QEáÁ†´îzg‡¶´u×áÚ­³.št¤š§¾×µxØ¡ë}Õ[ª!Ê@µ7›4D+8ÈE• ·eñ®ÝÏáƒsUƒ¸GÇiýôÒPÙß�µ.@´Ä]–EäœUÙ–ˆŠA²ú'±,z{/0€§k=}Îî^åÿ\?©ÿ5 ÿîúh«I‡‹^�*—@ xœ½XmoÛ6þ®_Á©_“[Q`ØmšnÅ’,H‚ ûTÐÒÙâ"‘IÙq}Ÿ#%[¶•®+ê6$’wLJ÷NMxûçÅÃ?·—¢ôu•%ÓþA²À£&/E^JëÈŸ§­ŸŸþœN0(»£…%ç”Ñ�ÕTM'q>™N:þ™)Ö,íÇÌyé•ó*w¿ˆC>$S)´¬é<Ý_N³éD²”ŸFvÄd2mD^IçÎÓò•˜;üN1ôàËMA;[Og#2fØ PN' KËÄo²…Ôâ1�»á5+17VTÒ.HÒ˳$rjÙ#©Ýé«4 {-"ŽÃ�ô0³)÷¢–�¾$ÑX*Tî�uBio„VêÂÔê#‰˜“ô­%ᙓX•ÆÈ4Y𙢅 ÙàíIÕÒ“óÞYN„ñ>²”gŠJi’¶;V,�  ><»rq§'!�ÐFGÞ„GR¸¶iŒõbI 'ÊK,‹•òå�U©*¹�mô# ìI�ØÂŒ¦�/™ÂÌ…ƒÄ”ê�š¥/Óìæéf:¡:c€´„*°¦¢7 lð½ÒS#õà�ZÅ;ÓZ¦™tXÀ&w²Tµ zº£¼ô'¢°r¥…Ñ9%ÀI:°G}�P=�þ‘Ï«ÀæLgúΤ½Åƒ©5­‚ó$fM»(ƒP‡ˆH`ƒë×W¯ßìtCÔ™g¶æ©D¼ƒÇÏ�) 4í¼…#à¤AÇy ÂCæaÕ¬õÑ¥aN¬Ë¦Zÿš"¿‚ßYö,O @/HÎ*ï²âÁæ b(üÜxX†ˆ°ãÜBÕ…šÏá?Ú‹Áeeçä`X‹\: *Ķ�’º¸‹ªf' »¸W71�¾â.ì :n»9¶#ö ñ7»ZaÈE쑆 .âµ ø$ë{àBWê‘"ù*8koWÞ§‹æÒT‹Çá›5!hÕJ¥áÉIÌB¼ï`0u ˆ>Øé¼4Ÿ«\Aa.¨1�å™iu�ÏÖÑXº­g86Ùºx¡j onÎA J„^ô–-G¼ŽìRUN{—ñÐñ—å®àñ‘Ö°�Cš�Ð)<ÇèEvO�\9ƒ|<é¦!PJä–óô:æ�WP-«ëŒ‹Ò‹­àtg™�ˆ!÷¾q"÷þl:bÄç%Ýÿu½/SCÆ*Î:A%ˆë ‚`¾®R•/³ñQ§å Y¯È…ÍÒìv3Á²!â%×^D~Úlê‹®Æ}±Ðûg—�S•ÑiֽĭºzÈàÿ»¬¢¯TKwîL‘áTiǾæÕ’X‘m€ßåót"•}g8†]&dŠñx�:Þ;r ¶§Çåˆ�eoj ÷f1D$§þJ!ÔýNZÄ„í„ûmß€Ü g:vÃáx uX蕼í_Žîiia¬ÊeµÁŒ£ÓcPßk,nÁæ[¾#ïÍϺÚ:�žÓh�ñ˜¶¾äŠ�b²oóñùÿa{îy†û8‡æïõ1pÓÖ—}Õ{»)zè GçG°)Ÿƒ¢Ù§¸N ±Ý>þw¡{¹2˜·ïc8y^Ì[Ú¢�nktájp pW²ž2Íâs XZ\„Ôǘ¯�oI/|y 0×ÜÞJ CS uíMŒÁc�†«Cœ“Î+×bÅäK'Ö‡ZI÷´4ô–´¬<:E}Ðù`|Ü#ë! cÐÂB{—¸£8·}„­{�…á•u{ø>!{ÝÂ÷ÚQ+öéOÔ„Ô#Zk·ó€$m�8= {µ¨¥áñyX=QÐRŦÿ¿@Nú†ó³½+B°4E߸^ÇÑ×w­Cq£áµ÷…èð~2š ¿B|Ú9û }mWÓíW€g-´Sî9äø#ÁΠ훣«B婞©9[rÁ$œžúïß F-÷Fñ9‡oSJ·ñÎÏy]~?3:¹¤ :Í6¯£é‹ÏãáƒÞ\Ut6d“î“é$~ˆýo¸= 22ŠI�-•ìxœµXmoÛ6þ®_Á©ß¶$BW E@›uØÐ¤ ÚnØ>´t¶¸P¤FRvÝ_¿‡/²E.º"H"ywŸ;Þ]~÷óïWþ¾}ÍZ×É*+Çñ�ŽguË�%w™nyþS^`Ü '©zG+CÖ ­®…"nÊ"Žge‘äºÙzmO+ë¸Ö‰Ú¾`å° +9S¼£Ë|:�WeÁ½–g,b0+{VKníeÞ>gK‹¿s|:ÈÕº¡{¦ËÅŒŽ „•eÑ{m‹3Ìì–²ó’-µa­Xµ¬)?Á%k¸ãY”mÄzÄÒÙóçyT÷’E$M‡a¦ÿPíØR8Ë8“Ñx°x–%ÑrÍMõWYøûž½òŽù�½Ü&%gÌi×Z9¡=X ·½V–ØbË:¡D'> µb<óû$7â35Œ¯Éð1©­½`ìC_™°Ìá�z+¤Vçаa'ÖiÞSUvè{m[céxÝ>㪠âöß�X!c0d˜$8'ÍX¿é3¿< ë{£në­“À€_oD|QÌj¦�|ƒÜ?”w¤u�˜zÍ2½Ä4Tö¼¾ó{ k#ç­–�eJg`®ßÆ•X`8YÞ·/ÂP­i¹µ åìYÊ‘©©w»Ýb/ •1Ý;˜‡3M4¶�ŠhÇÜùnü³ãw`A€�A80/z£lƒíg�† O¯w WàᎌB8vÜñ ÐÙ°š³ TzmAý6"÷¬e¬å~Ÿ)œjÝ� 7 Îy«› ‹%`oXàÀ¸Áä= üåènе‹%[gH­\kc4ÃUéèôðÌšËÁó†Xð$)Kn)—�ëTŒ@¿¸¤nwª¸kóêöÓuYPW� xàIÁþDøð| ùt”4zsÆÁ¿woÈ FE̵–C§èqG)JSN! Ø7!¼Fs Ð{]�Þ(ïÑpvÌlp`àÛ;¢,ðzeˆ;òþštá#$­×F®9PZ°V«ê=A³´I°HChç¬5´¼ÌoâÉýx %>ô/|%x²Wœß3‘o¥÷P߄ЌÒÓѼšŽ|IÓû?o¦j0t¨Ÿ±4„½H¾±L\! úsšÊCû¬šQ9ýØ /;A6Clí¼n¨xæ _„ÂæLUº&–¯VúdºàâuL«y•^¢©T„¼üÿÊeÌFÑÀÓ)5Í%ä"3Šøx(ߥ òÁpe‘¼;Oþdhùn2æÞ÷RÄÔr|LgÔì*Ô)°ßŽIó-š ëÝzø=‡:LŒ4ïsî)À]ᘯ´5—;\À8;<õ7…É=Øz/wbà£û=Wû`ð_Ç1žÒׯ?õ8ÔL}>?þ?|ϸ ;³|ãß»Sl $]BŽM/s�õ¤Îw .Õ)àùªœ§ÚüØ/ÁîØ¶öI'Á‚Ÿ§2Ë®5;‚hãMp^íßg]åAÕ! Ž�”zñÓ¸êšw‹ΊÏ9Pïæ¶S€ ýÑ-78\ðèš ó ï¼S }–Žà–m­ð åN�&±²’OXšz›®*ɳÇâ¾[‡/v!c�[�›øõíýÇ¡ºÙ½N.Ø;ÍYÇì/§Ó,tñ >ù2È”„ó*½Ìº NÝ«/>‰+Ú I¶dÖÁ{öñÑÉpúå‘s¿_ïÊxzHᇂG‡rxÉ'“‡ÐÞàÒá¯_¸à‘ ^¬G¯r'ÀÌ×â€Ç×9´ï1y<èâ/K!éböÄéç§"þ¨õ¯Ö¹=”}�,©~')xœÍZks·ý¾¿U§q;C‰–b·©MqÆvüÈTv2–&�Op$a�-€Eÿúž `¢¥éL2æîb/îãÜÇjô§o¿sõËoÙÜ—jœ�š/ðO)iÝý¥AlZênÊfÙ h°S^+?`ÆfI‰O”@”ºÿÔêiVŸD©i”·•“Êè�P>p5 {‡›ÌÄ ‚%fZ=N”õ x•Iª¢“΄ÝQ »KhRìã´ ¢É²Û¾Û;K&Üc“.\K0ˆ«@qÂ5Hk"üBh&NØØ:Úó8¼œü …vê;\Eä²²kS×厂«>Âd‡u ¾”±œkm}z0˜"”A@&Æ./ïó°O-�¼¦ ®ác¡8ˆÚƒZ)\ Quv}~0¸×Êä×÷bú(J@,#šD�0[2“çuµÀ„HsÆ'K/ZXÏÄ×ãjZ–+øŽ£®áñͺ`¹DEª6Û¨Ä�¦$„†² vŒÄÇ©¶�Ö:ækµ/yü$älîw'ÌïAH}^„wXE¦îèz`ÀÊ(Á(W˜šê»Hmྷ°þÑ]Vµ�gµµ¦J+÷„üCCSTßí�«Ö–ÞhGÚæ†=`Âàf�™öÜæ Gua¬·'Û}ª+l’woõy;##H�KòåàÊ̪”a’êAèE(�rɯª&¦\×–©\©6u€8í÷æ‰å ”Ôíè›ôŽ@^ †ÛœD(¦wrÆ–$Þèr �ì$Rj(–7nçÍËæ“ȯ ‰8”Dꂾ«eêÖËÞ‡‹¼Œ“ Ï\É•‚:?E’Ä)M|@bbÃÚu1i"à_ñ¥wêlˉۓÄLA€Ÿq©¶ 6l¸�J%ÌâS¼SkIÍDºel˜!iÎoÒW"GÉ=ò ª'\mC¶ÊØç³§gÏø Œ¡ÒŽ3¦a7?7µu~°h;ï@ûÎókJȰzÚ$ª–;M?F¢׺‘?yCìÿ}ý²’ÃãW,�f4X¸�Š¡¯I5BÔAÖ´líðvC°âÐ/aN%¯EÆ6siêWRÐʼnŠ4(¡Hï1I�žAw×]ky_"tð™ ±]¹°]tè|a2î‰ØÊÿS©„ÞNô—±�Üã÷6±Í·p…¹6ÂÂËÙÝ<'5Plÿ%mS�ò"©#JFïLÁ…Š•-NÅVfíämÕ°Ãô7kÃøqÿy5™  } }Š�.–Òxœ­X]oÛ6}ׯàüÒpâ%[�­p ´]†ȶ ÈKiéÊâF‘IÙI~ý?$'¶£8‹€¢I¤ËÃËsx/5ÿî׿¾Ü~»¾d•«å"›w?ˆøQ“ã,¯¸±ä.&­+O~žÌðÜ 'iqC+CÖ ­®„"nXctCÆ ²óY Éæ³µÔŽ>[XÇ�°Näö#„@l6çLñš.&»‘§—�R«Éb>ãø|/Ÿ.HçˆhX.¹µ“ê+-þ�àO‡ñ¹.öó±KåžÍ—‹i‰éBü|ÖxÌû�Ë’¹ŠØF®b:þAqÀ‰P–”N¬‰-¹*²8®ë.›Úž|˜D¨OLi¥hÅC¸Í¹#¥6Œ3Û6�6Ž­)wxPó¼BšSHFuƒ,C\Æ$aQÌþÛr,‡•ÂMÙ¦yÅ*n üî39eì«c•¼•Î2§}ÞÊ‘ñ£��Ä W+êÖÀFc=L¯É°³OúÅ'ÀbÖÜ…8álÆ °*¸)€¾XŒVS†ì0Eäú‡Ó³Ä"r#pîÒT²2ÉÝVP½xa°“N´’÷§‰ÅhÚ"7)á[`[ðSm–½à¶ÍÌý Ô¿ L1Þ4RP‘"wxÐÑÿÂòåÊ1À¤½©’£;×IY¶*³UP :6Ükׯù¡À'¬k¥„Zù\„c<Ï©ÁV!s¬×qÚü‰÷vH�§‘#п ¸Ï}xÑ‘Òtáö%ŠIJÆ�á÷~è.áNà�@Q „!–kÙÖ*M”1g¸ÄÜñiW„‰é¸²wwgïR¶ÓþÑy÷(Ô½Õ˜ä4 •ò¶Bù‚ê]i#à8=gƒR0‚bÏàî ÷UáåVº|;îx�Þt~êeŒ‰—á´ìv¾]×½‡ëþihÚRÏ6ni|F?D£?‡fŸÀß?LFsMe)rAʽäÍsªûçÜU“ÅõzFªɧCWÃ…]Å0üiÄÝö(NöŽŒ=‹«­µxˆ—u†Ô qÁìãr¯x½,x§T¥ea¡½ �’ò€ãt¨Ÿô)ÿ~ y̳ò„žß÷v/ˆo$ÏQÖQïÏÕžw’TCïÐ=å=ë‰�•Z0lt»ªtëºÛ¥²ÛV\ÀϺòÁ2ü ûÂÂmÕ6% �F9 #OWÚе�A²Ç`Œ Ï·M|w^Ôh*kñð‚!%ÒÒ õѵçoèÊé;ÏžøÚîÚ»~vü‹x¬IŸx×ëåßÀÆÌÝUø+ªÛÆid !ÎbÈçd»£J¸`‹Üþƒ=� FQ;Öþàá“ÜáígOÚWøæ°G½úûÁQÕ©a 2'œêh×´)ü-ïÀWPš ,Ã7©�A;¼¤ÐGë¥%³YC&n²éƳ-åQtúÃ#]£Û© þ5Xš;¡#(·‡xØ]‘M cšZ»ûØI­ ‘zÉp�is×"ÚŸ@+]Eºyûû¼ÿÎ#T.ÛåÁ|Q¡Sá¬e‰IáÃVKÿõf£[Yàö�}4ò÷I?*6o�î+{Íeë±–Tư[èñhÅH— Ç~øô8r”‹ÐSÀ})¯Iq‰àd¶G72‰N#бï¯Îßjþ0HãåÙûÿÕúÍÒ—ÕYütû Iw‚ ÈÈ–4�/¬l*ÙxœÅZ[“Û¶~ç¯@ÕiÒÎ쮼[;Mm­fRÇ®=YÛ;ö6Nž: ЍA‚@iå_ßsÀ›Di)Ù™ÌØ³ |8÷‹fúñÝó»_o_°ÌåjÍê?‚'ð'޳8ãÆ w=©\zþýd Ï�tJÌß‹¥ÖJ]ÜÈBpÃÞ—éÄΦþ}4›: �l�êåÜ:î¤u2¶OÙþý°0šqVð\\O¶—]l?˜ÌgSŽä¯v í¬ÚW°´d±âÖ^O²',µ@ ÖÉî…ž²ÙŠ›¹^üo6ÅìšÍ;«fÓÅœý•Vþâ×�ù}¿úo›M‰úlZþ~'ß-Tïì÷–º°âgnÞÿ@$©6y¥øˆ`„àß9|u_Ã7™PJ–ÏÂéEÃþð`ÅU5,�9{)ãLys0 y–Ã*uùu‰\Õàs{þdâ·zà;0wŒl„¦²T:Ûàˆ˜­ÊRÇV"vÚ°œÇ¼aN3— 6y‡»l2{;›Š|ÎJ#¶8#ïY÷hÆ‹$ÚGàÒˆuádQéÊ3¼B÷×9æúûÅí•sü“°6ÕJéµ,–áD´®óŸý¿¶äÒØY9¾Pâ<ÜC®Ê�žõ0¿Ì¿J‘„o maš¯6§½è\ûÄK#sn6 ÅÎÀÿŒ­eâ²ëÉÕ÷™Ì½pY÷Å?®àE@�o¦¸oê‚Û&"I�ˆ¿ô·7ÀÊB˜oýuIw)‘ Kí*¯—�‘òJ¹3¦AµÂ"%ÀøìoöÔ«/»C†7Ëe!sù9A”V*]œKPà¥nFLikQ©h… ;á#‘M˜0F› ïyè~/üï÷Š«” ‡Lû/Hlh@« �é¢KNmÌè!E8‹WN·Zg2Θ´�˜á;©à††K�um€é•0ìòï�ÿ { þ£,•DFi8yØtÜNŒdØ8p#?ÃàIØad²õžœAYuGÀ7¨áZ…¤VxUir¶„Ô„7+Iø •-@Á¬¥{éž/>æ²}ùõ4¼+LŸˆei4È¿V–¢Êp#ø¦V˜1Ø¢e *�7^ ÓÉ��?Ô�Èqƒd}À`%'w6òÒˆÃ/ý®ð°LÚ«×üè>lD•T\í,]¤K»óPuŸ’L¥ ¤‰M1¸sÆ�ᛚ!Á0æ*Sœ1jÏÖÜä òO0&ðÞÄ~qïFræ_� äËël$k‘¦2–¢p H¾ÜnÛপ¼"Xvã—ùйk+Ÿ…Ñv,zÉí(ôäpbQ:„Þzª|—¨µ^ï¢#¿(ä2s ‡\ðbÇm¥D—pÑ`×)æ?ÞŠQ“; 4üžaŸ6’�a=È��™@o�(< –/ŒÃ?¥ÀÎM&½ØÔ·yü­¶îHF ˆ& ]ÄÂ'O]}3ÀÈç øJÎXʤKG#|×ñD¯Çy{½¶‡x¯îv;Ò9j�YX½ìp!@ØýšëVz§•š±8ý½PÞ³ºz9ðÔ»é¾{Ê—âüq푎æè¿ O�Ú8d?, ÎUnÙ2úÆ€Áèg"…¼Èm V˜¼ƒõ»“±þ(”ãǾà`^*‘ÈÝ’—uĈ!®×ð]\¶è£#á¿!Õ½�lÑ„}ÃÍR€ïic°¬7Û&l]>zôèd^Þ‚»<G ûÀ(¯#kâŽGäû( r:Î8¶PãZ%ÝEzÖ‰Ÿ—ÍMÒŽYv%1Öʸ‹³�Œ¼×ÐÌó.eÖÖ ÐÂ�vðŸÎj‚t ¯ó¥L©öyÏÝáëp¢ÜsñúÞcré¨ôwn'pd\¡ƒ£³˜¨Í¦¡J†ø‰.Ð@Vl}D;žów¦*b²š[a¤N‚{;Èwðõn-D� ‘b¤ƒT Ó*¿oYU@勉Ö�Ð\¹ÍWЀò<ñ§çºY,ÅC^îí¶y6 ã–‹‘¤ï쮆\co Åú¨ PJt\åÛý†'aÃ3öþêÑÕcNö�¿ãl5V”õé�dÊ+l•ðâ•e5›M¼åI\aÊPAÀ¦î‡�ÔYyþV¬† óì“e#¾''‹¯îoÞaú€-ÆÃ1Õ§¸ŽTµ©|¦Õ�*xR÷™Î¨ûœ`ö»#�-ØKAÙ‰@|WcsÎ�”ÆNø°®êV|>õ¥ ®Sž 0° %\¢+ìù\TçpyC¸rþ®` ˜)„•ccWݺîÑä°Š°6-¿#åˆÇ< B:£Ž‚µ˜FóÜæR Ms³Ã·ÂÖSÜîj/†ÉG†ÂWt›¤J/iU(IÖBÀáÓ >k|}ßv1 žè´D^pß2Hª«]hí[ia¯õ¡?ÅÒe`C5g·ò�R?É@¶`~Ó*ëµ ×ÞmÆýˆ»#8À$~÷­r3»Y‹ö Wèù®9^‚?$Vä¦ðnà•ñ{©q<¡‹§Þóõwâšv7ºpÌv*F}ä'Q‘X?¿‹yá·}XMþ×s²ŽmÄPb§À*Ô¸%÷…çxzxáš¶ÏHsÁÑÏ![Á÷§LþU×:<‘ý¦ñ•gÿÇœý{OÿOÂòæí{Îý’ {{’ÂßÚGû¦‘8AEÖ'0Ñé�)½©uVϵn¨ùºÍSt&þ:eGQ;ß\þ2sQg“¸t”æ¶cø¨ 2©Ñ9¡«¯r€AõÊ1ØÚßEìœÕcKÔò% ¯”]‰øzr!¬LBM¤¨"xðÇ Ûºw‚-x66©þ ÐyYmc^HJŸÃëÜNrþ3în tÒ·­Ò׳%àûû„ÐB£sŒs‹þD + U°Åù•:Mé— wû&CaØ=2–X¨Ùb÷ez0¦t×}If«E×ËõÉîõ²2¹ßãà~¢$V@Öåêò)èå72>+ë‚í"‹²ñ{Ü[MÔ¤ºP›Ð«  ¶HÔ¶ëÇF!] U8NÆt­š~rߪìnyN�õ¶nxP#øJÚƒêP/:U@ô-‰½rO¥ž�>g€Äþ�ùŸ„ ‰1oà…á¶N¸ˆ¶9‡¢"€Í Ž%^ÞxfžvxÊ�k•8¸î�?mIo~c:õ¿`ý?žç`  ä äŒ�0˜4lxœÍYmoÛ6þî_Á±_“[Q`Xmš¡ò†¤CÚO-ÑWIÔHÊ®ûë{ÇIv('Ý`e@™<òî¹ãÝñHÎ~yw}öñóÍ9ÉMY$“Yøp–Á§ä†‘4gJssJ³8þ�N¡ßSðä–/×ZÈêŠ7ŠWܬ¥ú:›:òd6õlæ2Û Ó_m˜ÚˆTÿA§Ã¸ÉŒ‘Š•ü”Œ¢ÉlÊ�çoÃ0€6™Õ$-˜Ö§4EþŽ¡i`z*3¾…g6f5qvÂlZ#Ó„¸¤r#ˆjg’Æ'72« Ôǯ¨›ü†8ñƒò,•Èùß<5$—E¦ #‹¦(6$•U½<›�çÙBª5SÙ³†#aÌ0¢jd£¦®e¥ùaUFjÅ3‘=!&ç-�Kà³&ƒÕ_ “[²Cì§„à ¹ÌNù#‚l� `Køjc9”œUDÿÓ0àA¸R Q£‰lLÝR° ‡Îº.×8gBDƽÙ�ºxùhg6©HSn£ZT@4圫×DÂ|µ �eÄ«% ÂYˆEVA£3\±)3CëÐh®Ã‚Âê¡®~i¾r^k«�UXTKkµ#²ÎEš{ °™ßlîhÒ§]H­ƒ,´ˆ#¥ ºW¬k[Y½dYÖ¬[‰LI‹LÀ¦Q:�WÒ Þïo5ÄCQ´ÎÑSü ìkxËôiÞkÈM§Tà$íåVÆÆz)¬c“©z"gÐ'«erÇAn¡%„éÔwM�,‘+¾8¥—,Íau¿\p¦pN0�½ðòh'˜�’ýyûíâ¸ìCµí™Ãç ûÝ3¢µÜòŽËŽgüAÚ§ÃüeÒ@ü¥WâÊ@Z<4¹i;Ppz‰{›Û$oT23™Ï§?ËûÅ�å?Ñ䓿Ó.Š€Fà:g®{ …ã3M>Çp܆d9Œ«¦¼žk®VvÕ5Mv:b¯l$r‘Ø�Ädf÷›°\^Æ ëð:Üʵþ „Ó$üŠh#nC¸�A8�bàÖµœy»æ~ãÖ½q‡FÙbº‚ÚHÓd»ʼn„‰ =&Ô €&ýÖÌ€²­CVL L2‡{'–%£‰ýÄàÝØU±ÊÊøJØ8z“^64¹lbø.±²zDX¯Ý‰ïè‹Ý﨑 î²$Ty®úõ¨ÛDÚkÄpwd@]¥.…z5F†}mkdkæM¢ÝOT%hÒ/¿Gð_¤ÝB1 ~ÓkÅ`:QX{⦰T,PÓ@4꾇GüÁpe-w!Jõäv;‘ì›(›ÒF¬�ÃA„Éc¤ ÜÚo˜‚ô RÁÝw:¢%VK îsU{À?0l(ZW\-y•òÕBb�½Õƒ�ýªtþ­x*Uˆ³F¡Éƒí�ü�Ð5„‘CÝkÄßç�¨�gÔó!ž'A •0Âæ' °-ì~öMÞ–©Gs›Þî¹Xæ&l5¾5¬#õ!’Ó–œ£@}+˜n7E׈u”çÁùÑ{Ý{¡¡XØÐd§#c!u‘ù¦Ëc>ÜÚÝ]Œ�‘ïirCØ;‘uð�ƒ§+Ø™–RÁ™¼è"¢ÝñÔÄ®vûfº�`âJÌSiíÙhù•H,�››¸wë�ÅôJçfößé?ˆ31�õ žNfçâ!Mø•‡�yú{þöÅtBkM'«OÅÇÛYÚâäµ²+“[õN/AúŸ _‰)²*•Ÿ ?N þ;‚¯åÝsôÏ¥JS½zâxÉýA¹W2­¶ݹx¦K–EDá^áWŒN�#ë«zK™=újÄ$x;\÷Yõ…ʪȭ�Ñ>ÖÆq�ŵ.—"\C̔΢\ªH¬ ëyi K@6™Ç-p�˜›¼Ôye* «±ú “ÐOffUqÈ…ΣÖ:µ”*ÄÏTŸ·YeK`ÖS•y9Êr9:yój:QٹȫLz.àq¡opÙ6ǰ½Ù˜d�1±L8xœå[Ýoܸ×_Áú¡i�µ�;à€Â·1�¯Þˆ� öÕ Š>p%DêHÊ›mÑÿ½3CRßkˉŠ>°±DÉùüÍp´þÃë÷¯n?xÃrW—É:þžÁO)giÎ�îùIí¶§99‡çNºB\~;#¬•Z]‹ÚðâZ¸½6_Xet%Œ“®ÏýÈd}(ntv@ú?\ZÇ�´N¦ö‚Í¡S’5gŠ—âùÉ‘ gŸN.×çøñØöÎ>­aHÅÒ‚[ûü$ÿ‰m-ü;…?HuvôpqO¶Þ\© ,HÖç½dŒÈdê´aw<ñÏ3y—+íéO'~è ÅÖ¢l^p—Ÿ\^ý°>å%Su)ŒL<6òëŠqˤc{ø±uURdÌiærÁR­¬35®¹JØFlµŒ«3zoYÊ�9Hµc¥„óÀï=/jaÙ^À¨ NS‰ìŒ±Û\ÚöpÒ&Ì€ÈNµ*gIs¸»gá¢%º íl¥s°™­Ñ%ãÌñM!V4¤Šœ°Œ‡YȪ, g‚=fLiGohÞ¸çFâÿY®‹ ÷[ˆ{QÀááà@ V"NÀé‘´+¢VÀI0ú‹¨€_Òåí~ãIÎA3Ôéó uú¼œ:}žR§�ÂV _ñ­ÚôC_›î©ÉU;Ñýwà *£P|5ªdCfÕF4•ÿ¤v½!·Mn¾·¶>ä 8·çbø™b¹ªgÈäª^N Hk,�+ˆXÿ—üÇÂÜÙ9œ<º4'¬_DÁlëÐ+¤ ˜Íe–ß ¤ðYa¸DÑŽ'ïõø(Ó°B£Ó¢Î|‚©kWÕÎ�[�5¸¹½Ì@ ¤ãèí¢ÑàE '˜�ãtF/'»ѱðÚ× :•z¼dùÙ Ã$•�IR©R$�N»ô@˜Pi/x'ãè=e´é/ggïI]HÙ[¶Ì�Ûä¼å$x„ü,YFQv-á[<¤bËëÂÅääâYLBHØRx£’ '!WI|æü%î$>¼Vphg$9VìpY§)¸Q ~¹›ĺÃtœ=ªc‰Å÷ÌÀŠ>;Ã3 ü~Û~ç¦>ŹÈJª˜]½5(Ìm|}!®•ÄÅ/v�㺼dYmˆÖ6à?K¢¼· [¤¥ïd)Ý Ùô','�!Ý ÈÁ¿Ê².;q"2„É8ù³Á%9z……‚`¶¥€]Í 5ƒ B·!ቚxó6z'b*è¥òa¨/HÖ¦- ð�®SÇäö‹‡sÁ_u!L,œø7½Ü™sÄñö§Óë;!w¹³oHš°ŠÒºÑÄÀÑäžèM*gâ/ÁtouÃT:œýN[;z'ß8Q›øZÚ ¢Ä[µÕôðö5àÞ–h,¶ûaƒ³wØž^ —ë,<¤þùí¡Š;`Žïlïv�;–R¦ ‘+ÂKéka…{¼}¸è¬Ü•Dg¥ž ^Jnè\)‘*yHèu0nü6Øû¯ïafâˆÂàÔö4¯´!ï°µÃ@F;EÍTFk9K‡Èú}lµi.€#TY¤`þGc¯õÏX§£éI,Øñ½"ð´Å†FíâYÞDqŠ?œèú‘ˆ¶Eí)Ÿí m´L³‡Œo4EK$Wiã@M0E‚a‚êž~WBíÀîqhM%ÇŠ6‡[¤û#ÒßAz°™”C? ¸ÔÀs 8<âìƒ&7¥ç9˜�6Ÿ‚šÔš[ ¯#ÑÉRôÄla¯@â´þ²ê:¼èº0ô¦béŸ�Úr¼?ÛñùvÇù1î‚}f”–‘»iÏ,ä‡úMe )Ág›íÎ6.ƒМ/¥Ñ‘öCÛ„ Gíì�ÕÎ�éFÌ>,Y3žÑÖÆ_Ðe"õî h¥A ÙjÚu¿2xÔ®Ÿ]�0ÃAæB¦šDoŽn‡Ü]<X~D)hUƒÆEtÑâ1… óÛõ¸’íu Á(Õf…¾]-¿‡8%Z™“�;Ÿ^' =ÍØ:1†¡õÂRFÖ#:6°»ð² ¼2ˆ¦ƒg3šV˜y¸<üödNÝ:DšÅ ×�ÞDMÎ[ÁøŠzÄWs„$kŽ6Ê^ûı‡èªŒˆ`KO´HÝÌ~÷ò¯¿Ü §PåðtÃ]Jõž%ƒI^Í­ 68rÑjCuBÅý«ž¦Ô]dó/À&ªe¡Ú6�¡N `t„þKþ;б RÄâ,Ò«`¸ðTF=&Òw˜ c…YI¼t«hE\�K¸;Z¾û>){|?WÈ1XTÆ‘èXÄþÍÿXÂ~Jbš Þ“wrLàK r�rÍæ(I[J #ÂØ/]¬ª58ÀÀNÚúˆˆý¦¨ï­I#Œ7³vX˜€H“ ¯k¿ãÖì~¿[jóM []ÔÙPé>»ËºpBòÀÚ?q¢ÿ~€­’�…k% çÑ~F”vtdC«Ùy§NÁTpŸö5™",ûD‹¸›awËiýÝ”ž¿ï¶…ZÃÃ*» 'š®ú.¦QÉNû^Ôeî�(oðV¦y[2¤†P�Œß�õyJ‡áò$^]X¤ÄÑ¡a¶áµ†ÍS>3ÈC/˜ÏÇ1j‹MÏB¤l4j'ïÌÃ: t¼Ö5Øì¢w¯¸X û¤Þ©Éy fÈÓä§òdxÙÞ§í¼ùúýÞÕ¶� Â�'‰ALÀb‡O7�ë,J$¸ó=Ä�½xá‹öÒó4�Û¢¢|óµ‚UDöäö¬é‰Ë óýÙíZ±ƒÒ÷„‹@ŽŠ_ß×Éå³ì3„&w‚A­ Z®™R1º!h6FúBÊÃñ]l—Ì@^Jõ&ÛÍ’oºœD[ŠS�T1�/¿§íîáÆšö’P󼉆©’°…}ó=�‡ÅàC¥M�pØQƒ…‘ð5Cè�íwÆ2œì/#¬ã‡ðôÂWT#½­ ÖH] „bÌ ^Çr Wa%::Æ å´‡?{(i!¥ùõPQñØ_–½¯œ,COÒGa1žÍЦGi,§f3–šü �"sì8éÑ`ºCäAu\[g´Ú]¾(ö�èÏÖçáiÔ“LÀLÚ¼B-.Õ%VÆ7²Àޝ‚T–GµE•äXBg¼v ¼)³ Jˆ2bE®¿eƵʚ&"ZµkÇù‹,B ¤´¦VT:´5P÷‹Pç¶ ;¢»€<à!¯’‹¶û‡þ'¼Ê">zBój3gùÖé±ò4/ÃýPÿÓ•`âÔbâ‰=âÑšž™ä�Ÿ-E‚<ýÖÉ1â‡ê͇þ[§“nªß^QŽÚÖ:È´Û:C¤Kp­ák¼[‡Éwó5AÔP’#Pë·Œ°3PR¡Å—Ó'= ,ø¥•ë1l !çás¼sÿÙß’Å�f¥*`#…¹¶ÀiÁÆ¢4mæpgs“V|†ß%A¢Ê"�ÀZÀÞûAì�¶Ù»RÄÑÒ¤±mWTQÀé^çmщ-Ì,å•"Ò:µgª@ò´wi7„�› ôI-ßÂÌZk4¡�:W±� ø Ô‘p¦`·ö÷JsB…)Æ$8]ZaªrU•‘HåV•«Uª•¥Y:V  åz,¬Ûq;C�Ô(wQå°o8¾y T %SÏ{Ò„?åFIaAB ®+xBÒ†ÓÀ5Ѽ�ÉÓµˆu’À‰çsE ‡ÏU¾€­Àa•×JåðÆÖg2¡ ü®='}è€�x k¯a:œ�J­uÞ°Õ(Ë$lΩ•¡d‘ã$Ø*LÌlé\—Z¦ÚÒØ1ð‡'h�¼æ2M×µ‰Ü5úA.êE¹G,dkäPŬØZìJÍAXp^³uäXCwôމ2µ•Ô  ÅŒ  ��â ÏTj®· d)g©:rf¡¯¢%=k < õeÄøÛ̱*üW›Ñ\Œ¡ÚÄW…Îd±�Rð¿%ì/.—g£/ýcÄ¢mœ6cËpÌ×ßÖcÞ5‚ãA“’?qÌF¤ã6i’ǃ7%œ½,bP•Þ¯”q8œVy*R³@•—©,œ¸×¨BgÎ&6ÑÙ.M•¢€ÅŽò@ýF$ö͇Ì‘éÔPs‹!‚ôfIö‹ÇëN×-åØv³c•È*-aB­‰»¨ñæP:»¤rF±0î÷U½»S>¸´ßº‘€QRgà° î ìsŒ˜§NçŠ?K/W‡Ç¤Þ A¥ uT¬THu,` â&3 ù Ùÿr¾DIÅU–­>�¥ W©œ8áÈùDá?Taqà‹K1ŸÂ"(ÁÔúhƸÇÌ}-Ø7§âìL<ª9 �@Ÿ~¸YQ{á¡[7Œñ™½4rè\‹)7¥°�Nư«§yè½Ã’ŽU$T³I:ÞÆDDáǬKt;"Ó€MD �*a¢¹À6µDéDmÒµsDŽ-Ïäµ›Å:8eu£bó“ê¡÷D ÅOk´~pYAÇ0žëYÞÇq‡Ç�C¯Qjj¡¿pÎÔ|½­Áî–Ì¡"Õ k«Õʨñmµ "©rŽt@Iã0¡ÆU1C§ÀÇŃ=�Ä*0´š½ÚÈÑ1åÀï�’ºÄˆæ 4{D´2"ƒ+TkÀðFR�¿¢]6¡h•£)€b)„Ϥ‚ó‡\‘ –ì3çˆjÃňÓ°$”~8ŒÂg•3á�û{Úh‡êü×2³hÓÎ:»ÂÁ:+ ù·õÉ5ªT+�%�v<§ÁÊâ�‡u÷ØêEftìŸ{wQ¨´Ú X¿Üx˜nºj=õd· ]-mk^_ÀÐbtàùýD©iis”‡ždsJQÏ1qFÓ§�[69fmoò�ë¥æ áz)ñÚ HÆqŒò­Ü¨ã±Z "Ð�ÔIúU+úõx¨…»Œä5Ä^Cm|3¥Ý¶“änQÈXc4+‹�è¦ìNŽON¾$ßœèa¬®5.p-J´È¦ñ0dÛâsµ�È i‚ÊRFé‚< )ÁŽÒCˆKfê8¨NqmiV6 ‡Ÿº”•²H³›Ðë�I�Ÿ&òò‡oñ�‚o“º…[´D£ž2bîm¸­±PšÂ¢Ú¸gÉ¢ HÕ 9ïu­Éµ6':CßàÂ8È¢ s¤0OUÔ„qV�Üá1·–ÂDPí£LeŽ÷»g?¾¡¸üÅÓ·—O¿±¡Œþy iÜ åáøZÚ5v‡f8`)—…‚ÿ‡%Á÷BŒÏ™%G¼úEbizžˆcp@‰ÂÇNQüøÎ¨„Dj±dúžC8X†ayn,81‹J ¹„#A÷L® Lã1ÇÑ™‹+mÑ:Ã^¥¤wJJ”:Sœ¹• v›ëœMÍë2ð„¼�EQå´©ž×|^¢´›X¹²O`âƒä2°H—ÂvšN�ÿu|2PŸt,½­Oa�jçèëÙ|  ú¹³‰üùÎóê ÿ³°¿Wæ ž[‚·&k Sd"!ðÄ›¼‚Ïv×€.ïñ÷J×JV*<—Ò˜>D?UG_7–á@!jPa/(´¬f(,ÀIß�Q‡n%>V¹»I/º?ñ]‚M^0nÊ-['%¼Ø&*'”²ï@ ‡S¾§Ò„ŒscÃ÷€ëùPŒòH¤ÍB}BâöôþjÏ`^‚B{õ—rqE—�û» Uè½€›Ð(~ˆõUpåßä.à |IoÊŒQéãÏë ÷¤÷z¶ô�ô=„¦°«ë!ß}ð(d¸Vz±D´Ùìø Úèou#ºRKÔüÜàŸƒLwͰ¿ô¾¢ÊOTîh ív‰²ó³ÀI¾Ø»ÙÞĈÇA ¸a¼»ãƨÓ#r›¸Ñ×=vhäAhÂU¡Žb%…Ɉ»z+;«vÌépÞüú›kuܺ—‹»œó\†'²ÐW`<3euÌø€p~¨Û;?»v~ Dr>W«Ò 21ij®1žÙÕÄi?¯vÍG¾³¿]ó›G½÷�».ù˜tSUxVõ6|\ÿf¶ÝPßËvÚ€"×DeÎm‚ÃÅ#…‘Ú0îFàîÁ/í³�\표“)Wè迊 7ü ¡~oO møi­°µ.ˆtÙs€27½•Ô,çÚíš›ßÈþ¼¬¥E‰e̤)×ÿå躾EÈÆSìXÄ ¶Ê¨2CÞÜãZ¦5¹Ê5-ÖR@.tÏÀS„+ñ‰¤í¤Šz°= 89ªÚÄ{/ÓÿÜÑ0%M—ÃBb?ö®ââ€àÞ4ó®Â¾ÎšwûY[ê²"s¹ÏPÐï`o’[zs!_Ô‰ù\’ξÎå³ü^JÙ�wÈ ��‰óÚé^ ä`û¯\öŽ­w^€\AEo=ô#l÷âÝ‹8 m©¡yŸæ»gÙ�²`¤„7•:¦WxîìÀ6Ï5Ãûo¥¢ �%‹’o¿@ê)õ}úGÐ1î¨ëX½ö¡`ÌøSªüˆ¾­jšÇào[^vË©Çpn„â¢>áÅSê¶¢^}ìÖ _½ûÄv¾_ö·P”ÈGÕ �±{É#]ó7ž…Úý‘çž�öìŽ;sß:§/ËZ›N9Þö :ëñ®¶�pÝÃXSõ6¯¸ˆ×µjè-Êÿðd,N¹ÍŒ$عI¡~;0¶´*ÆšÝ|`×âû06/1(ÝǧÛtBíž&IÂ>Nê3‰0j^ǽÕÄñ¾q ‹ï<í‘&O˜_ypñì’Ér/&¦ÃtÿÑ~I·{Äÿáy?^wWèþ>Ú‰õÝuñ2áàÁ¨-üƒc ˆfå ú“y¥§‹®g¯)¨:]\xïqÁS÷8‘Æ} sã-OÂ+ *-ñȺó¶~­h�Ÿsæ²ñ:?õo~PØ:LN¸¶‘:V™�µÇ…áŸÛu7zC"I¼ŠŒí›š®.Š‚ÚÕr³Õ�ß×Í=›ènq}=;ÇŒö*Ù¡6gÿ3F�½VçÇ~¤ÝÍÎJ½Õ—t"ï«üœ‹7øoì —ùÞI ±8ÏÖA, K;\›]�þ«¢\ß�ÅaXb¢m+cÀ0Ó9„U‚ûGȼ¸Þ[ÒË.3 É!›Éùº0kî¥eœQüáÆõü·ŸXU=©QHõÛÔmóÚ‰û�&ü¯,ý‘;ž ¯ ép¯‰=�5’~«xœåXmOãFþî_1MÕƒ“Ü"�z⌥Šrý)jŸª�=Ž·g{ÝÝuPúë;ã];q!9Zé¤JÛëÙyÙ™gžI¢o~þõâöáær[qu)]J´’\hƒö|ÔØlò~Òº•¶Àø7œk4FªêFhZ¢L/+ƒå¬@¸F›«ÔD¡¢Ð+�©tÉ&~ˆ�V+s{*£]A$ %ž�vî9ùœ©"½Ñ˜ÊÄŽâ(lôt·×ƒMdí”öÔ�˜óQþ2Czd�‰J_8„3ˆBÇËL’2¾ƒsˆfñÐÈ,†ãVLÍþtRo£°Õ…5Û�ÁËŠ$53¨‚-Á£´9Ø�5Ò�°�#ÝI ª±'�Ó�ÊECi&ïFNé¦k9©0[&ÞhóW£>¨�l5Óªl­b—•ñsð´c™E Ý�™ZѾ[-*“)]ú`IªJ½ÌTLý*«Ôôfu¥œ¡ÖÆZœ”±ZUóøwD…Q”�Ð/T9¹Æì¥Êa|»Sd´;ï\lcØÛà¥+ï= »½RƼàÂJÐU~{@!UÂaøq–�3ùrAÌÕ\œÚ±òj¬î²ü† ®�õ‡±åCtÒ~a!ŠŸ�ð…&5’�©°˜‚î}�‚ÞìÕÎ3iZì=¢œç¬©D‚�T@µ&¼8824&*›pÁ~͸}Ëè™�È:´ÌJíléÁxïCYCÙ”"ŸÜq´ ô¼)±²æl;+è\'Ú^.�£i×àÄ¥²&»îi¦tŠº4e»—d¨¼Ö²zI¶š>9uÄÔæç£Óï¿£5>¯iŸ%¢¢| Ã"k?¾ïöÝ­’é„BëæÒ¡¹ö˜Ž>r=êóoÓ� ³9XkmÞú&NÉUÐC­T�é ¿J1Maξ( ŸáòÄéb×v¹Ä%ü±©vÊo/ v[áØ{ò¸/CÖTmM�SûæÌ’w”6a|õpq“?Œá�Èf ÷6{UD„Š¿i“«=³gP×ôÿK# ÔbþTTL7ÔZP HÎAi#tøH/–Ž…(YéØG{$+ª™6¢èu·@¢Š¦\cl6ÐjbÂeEc>Q¯&iʦ k ªñZxÌ•!þ-�±–}×6ÅÄéXÆìšk™Ôæy1«7¢ä l±ÖóÇÿˆhûññ�©ñtKH9Œmiÿ6¦9™-ök lÓ®g¼öí“|÷S]˵Ám­/õ=Ø®×ÌÏ7U%5!²Å� |ºà66_\ߌá“ÕBVÔtüõÞ_?Y4–—ÛË=_:çÇ�³Mû½ªÊjÞØý,œ¨²<�wó/�®+RfÌ1Üñ¿}¿�¿?ßAlÒdÒûd}¤hð˜)�æ¦_˜åð—þˆÖ�0 N^p¥Pk!³g¦h‰¾`t—iL B®dŽ´Œz<ªè{ãQ-w!£Òjùi¢ 3BÁõ5t—GŒ½1ƒŒ¥xÓ?Êv9WLdŒ¾õ'ã³J¯Ž¾å =WÄ�O˜Ö7ýø,5ý é§!q\F'@¸öV—0^Lv/ÈÇ>åVÅæ1²H@`¹d<†¥L¢^E‰µ7&ÕÃoýŠu \&EšGâgJ±6ÂÄ@¦À¾•Þ´Z?HÕ; ™±Õj{OIçp¶ ` ²«â"–"mhVJnôe�N#ôöìô]‘ã‡2KÊËú(#:Kg'>©£gT¿“ ��yÀÚ©- �KD B�²…_zKPÛ {îÉõQ C0¹x%•à,iìí`+wð nWrçmV€ò-OW`§É†•0ÍM>B_Pç’~X—w†w—)4ªe·Ç¦MUÓš)ÁN¥‚ð•ëûB£Z3»Ñ=6÷ø>¡j}¬fÉʯÄî­3Zo¡ñykC¤¹¿@¥…Eâ5F….úR[¶`ÃÈalAZ*™c#q°—;©y«¸¬â¨)3Jü ,‹ “Æí8¾kà£bj"D¶‚טh »&Y¤‰t £{Õ†©NV­Á2õ?˜›ª_lìõî ïÓ÷Î>}íÓ÷6ŸúDrÚ¥��xí Äkh ^Û€ØI°A±ŠÍ©»ŽžÎ'Úfr113.ä÷š±,Êzx z LÚ¥ŠçÄ8 1&ia>¤œðå̵’´A¤ÏhöøBûå@|K jv[û]~bœÐFÜ”fš¿„º®;Ýy™ûanP/Uýµ_ôÓâEËq.ö8�© Õœº’„ü¯›mçýZúþ¡ëÖû ¤ÐMK6Zú\ʪ„æe=9…»Z kª˜ãKõðwÞëC±í/ \¦9ãæsn+áÿÿÆà_Â? ÜŸõpÿ÷§‡~z_Y\šµîä_ >Gn·‡fî ü�ïÜnNëøãë|«¤N‹¿‘›ð·}&²»huÆ�îBú•{ˆêÌ>/ÙÍ3‡ó¯™!)Sez)Uzö Ùp~Ö4¹£ %Vý&°Á1ß¼XŠp§Àü~Ý8¬P,@ý…ÛS¶GŠÙ€BK,V¶NØâHD{* \ ÿ8”ðkÜ#!BŒYq=[tòÝ`Z{¾NKõ›©¶A¾GÏÂ2#ºA\O| è²R*‘ózDŒè£ä˜·ÈôfäÛ5'3çt‡}¬omÉݾ:�áT/K6\ >yø`bÞƒ�Kç�ð|2Ë'’SâþLÙç¶1j&%ŸÓ=e÷1LC8œÒ'Ê>Š݄㈲2ŸŸ«±ÈÀL0`3‡=Û‘-Án`˜Ò†¾íõí¢�Ï6ïÜ�Fÿ‡þýÖ÷ì·Çû´Å6qL,¶Cà"c>gÄ!NWŠ.D{U”¶÷O1Ðvä±ÌSwÞ¡aж/~Ͷ_�¤¨×&á]W xŒôž_›™\ÍÜu²7j÷ÒaªþçÓzxÁ¡ýäõ_Íé.O1o}µfõM…²ðí™îã³;zev^Ú»!ÿlü[ƒÞaÙì­±�1Sv¿uß7jäLyµn{Äøü£òoúÈvhÃs¬²SÂEÖÚN�þ7õ�’ßH#·!÷•eê˜8iÿ�-õŸò¾�8N ö :8öˆ>�8‘ xœµWaoÛ6ý®_qó€fl +P`hi–`@“6X‹bý4ÐÒÉâ"‘I9ð¿ï#)ÉJâ¸i‚A,S¼w�wïîèì§?>ž}þz}N•kêE’õ, |4ìå•0–Ýɤuåì÷IŠu']Í‹¿xeØZ©Õµ0XÂïÙ(®éŠ]¥ ›¥qk’¥äR[ïà·…uÂIëdnßÒ“ `“d‚”hødrÀb~àÝd‘¥Âû}ˆþ!ðx €5åµ°ödR½¡Ò6×ÅÁ�¼¥l#ÌB/ÿÍRÿ@'”-dérA¿£¿£É4B|�ß~ÍÒà3K×øào†¯îÿãõªâº–ë㎓OÊ’Q·¼—è‚ÎŒ¶v†²ŽIP- CfpJ êyM ¹éO×ØÙ›ID‰'{@þ�”žֽ̈́™%W1�¢ ©œ&ÇŠJ]–„*¨” Ýp¤°L0c‘WýIžIûñ`'�F›œ¸aK€!]v^�Þ_€ÿQ·cÚ¯þ‰Eݺa½?ÔÑ%‹ �Þ„ƒwïξ D�F§µbK•¾õ�C è …Ú’^û…ï-0¿ †pÏ^A@_D=8–HEžóÚq\UÂ’BèË’sGž"™Ød”Ð’¥ZQ~G‡-·”#íδ¹g9JÚ©ÚÂ&5—ŽÞ­Óc8Ž®7Êç=è…ÄJHÀ“�¾Uä <œÑ·0Й?¡�®ùÚ„¯L·•®£§dGL|JU­* œé¥e³Q³ÁíŠÁ²#y)še!z’0ô’€wv�5¹çž{TWMC(ž×º'Ï×VÖZ�DÑg&æñS.jîÞΉÞiW‘0LW§Ÿ/Oß½2ö¿V[$¡©[3¥†…mát\'ȃV«Å'F‡¨­FŸJ»¥„s 2\žL®@gùR5>¤s?Á~†ZLl*“ÅîÙ·þ)�­ï+0Zß_�ìQª"�jŠîôĹtã“w�cÊÜ=q�MöNžïôÛ éÆ­þ®‹aæ ÍéAûþغ™.gAÚëhçkºÔ†ÊÝŽõx¸…Ÿ{m�vûêc ù"~ N_Çx’ŽÐ‘b=TœŒØ 2ÙYѬk†úNÕ­! @�– Ô$¸@ú(¨#vÄ] óÌ�FýÀm3E§hФ–"¿éÅÿA|èEÿLm\¢ýˆ0üþÞGêRˆ ßÕÁ UxÈïKîcqŽî5ÀŸ|�Øqˆ= øp­é® zäM%„N'|‡fc´Ù ØëìˆSs7º�sѪݬgÚ|÷º±<Ì-m+Ž�ÓŠ•EÚ6îã~‹¿Aج Û¢¼úÂèç&6®F¬â4;ß®ðÿ1^�Ñîu¨ñ‚KÑÖ(z=l” i”E»»L»î3âgæ+­�îYÝ+Ê´û%’ÆŸ<ß`Øyá‹~�7˜ØxœµX[oÛ6~ׯàü°¼81Z ÀÐ:²t†&iÐÉòHI”ÅU"5’²ëß�7ù¦8Z§ $S<ß¹žùO?_?<ßÿFJSW‹dŒæxÔÌP’•Tif.'­)Ι̰n¸©Øâ [*¦5—âž*,á…埘¬"�’ Ã"Óó™ß�Ìg5•ùÆòx³Ð†® Ïô{2 dÉœAkv99Atñ…éF ͺ�ªž,æ3j¿=%ú1x¾YC²Šj}9)ß‘Bãï? @3™Ÿ4Æû(ÿ†ÌÓE| ÁÈ|ÖXF Ò}¤ˆÐ¦©8ˉ‘Ä” p,ç™Á‚ `‰§Ìù* Yëówvå\HA ÈŠeFª)‘Ê!öÍY¸÷¢™ãVR‘WøVRCÖT'D·žÿ”¬Kž•¤¦’2>| - -,´Å¨¡EE¸&iË+sAȃ–UžøOšdT© P$ªøsEäZ|À+Èa<‹õ–"ƒ<2q<À–Õi…å`Ȭ/‚f0ÁÀø¸VRëGZñœÂ”·V¼�ÒC9nŒô28Ž’;è½ç-}F2‹p¾Š§ã£ZÓ�&^Ƴ.Ug�×Ô�ëR*ãLJn¯n®~%­fÚ¹^ê$爅?—¢Úü€GîÚúsª™Z¹¨×½q@5®'ŽÀ{¼ÐÖ)Â~�;;}¢Fp’ÉSú•4m¤ÆîB_¶¦ÎhE‘­™l…ábé@•\kïhݪ¶ç~™ÕþN¬ 5‡nØŽßm¯Ÿþ“›ž:æy\W<÷?VÓ˜ '·5i×§Í.ÈœÕÝjÊÉâîÛ›ùŒÕ "Úš)ž…²9¶!Ÿòi\C>õr'ÊÉšñeiF3â´®]ôJ[ØG/÷þ�ÊÖáUcŸh\b÷Wnc¶‰Û_««*‚óÇ/HõÑóü'ÇRÂ�´ê4jÞ^Ú‘É~ÇÆþCàãÖÜÙ–n¸éQtc4[Ó˪­Ê´žV7�i]2Aêüf‹ë‚;¤ÃØþ‰µÏÓ¿ìm-ÉÿÓÖzäÓ½Êw­9+h[™PKBÃò{•Ñ ºp{– ´mØ=®Y;О;Bìú€Î�Ù^l¿=¨”„0^sSºŽûP6/PàK “ƒF¸S\<Ï$2Ýë}¸Ñn«mò¹°íüرÿéwÀtŒÛ;®[ä©fpˆ^hø`¬+R‚^¶f§‡,©¿9ËâŽFhB*FW FÄÿÑ^üÕTlìR¸9©}?§$£y¥[zÇ•‘Ïß-lÚlí„HeUñŠ›<ûlÕlq}JÈôoT&b{û—»ÏÑ­ìîƒXÆFÇ¡'òÕ¸??¶ûÓ WÓ© åR®_q‚6ªÍL‹¶™/CMÇs[úпbW¨‰¶¸MÝÔ�»jU*Ù.Q½”¬ꨃ›3Ù/ešç, Qpwc önÇb?ç½Böƒ§x„pÛ˜™F [fJ™ÇH²$~ýaÓ°¸jè2´-g”©S(bÜxáÐTVG:ÄP4PJ³¯x÷£˜$ªû³Òÿ´òƒ¶#ÏÅÖœٱµÃÚD�¯¬1S¢%¡GCòçã­%’Êè~­º©ÂQüœíj.¤‰*�³;Г.zæp¿ËÅG¶âþ’â<è§óYøLâ|"Jµ#XͤBv~’´§�ÅÄ�fÁ„/ù›Œ«›»æ-L°#¼ Ëp\Åíj”Öîˆ$j;õÔÖdDvh¾·X¨? q×-†æ±àÌn²u¡£ñ—ÿ©;S»iÜ`¡ˆ îOÂp 5ßɱ D$±qN¥Ô®}ÒÌØ§†"…ÁñƒDFˆÜ¡aͨHS+µ8ôݨÁ'P§ÒÔ†¬tjaå¤8_7ÇÑM¡p�¼ç!«úƒ¢gŒØŸ¢‡®6vv¨˜7p¯, Èåt·7¼ÿÚwpw.Yœ;º2ܦ×V(ê�\ çzHn¿vPõãú_‡ý|°áé8¹{{¡½êfÜ;a?¸gš…¡úÌð¿B@M‰B�6“HxœÅXßoÛ6~×_qÓúÛØŠC¡(ÒlÚ´A,ÈSAKg‹‹$ $e5ÿý>’’-;Št›$�t$ï¾ûx?H'?¼ÿ|qsw}I…­Ê4J†‹�Š­ ¬Ú°=�[»šý/ ·Ò–œ~áµfc¤ª¯…†/œ`]s™,”(Yôª–*pŠJ�V+3󖎪ÀÜ(T‹ŠÏã#3ã4Y§ûçã�0% e¥0æ<.ÞÐÊào†O ™Êy[²<®n ³~Q²hœâ”.´2f¶¥Ì…åœ~-‹šîýÒ[uTaa9�ÂÒ\nT•™½‰ƒ¶w0ág�ZþÅ™¥B•¹! G�–îÏoXÓ “I­HP3¨=#YA+iz«ÈP¥–†õF¸ ÆIÂJ«…¬�±9õÆî�üZs.3»5>�W¼§Š:i‹‘ºB?ãMZJ¸JU %xž‘QÔ¹Q h¶­†Ui"­˜3S«™USò` º^'IÜgÂX9ÕŠ2Õ<8>$Ðzçd½&죀¥Ü�(¬b"æ·KĈm½È‘B�v7 £Á\¦ªFìhX)]�§›Bš°ÑðÀ;‘÷ìi•ššRdü–²q92ðÞ‰ìVK>€Üˆ0÷œGØûÎ1Ô©æ ÀCà@fÉ0�©¹Ü°~ìçZtÆ{®ºš4\Q-…‘æ ú�Žª5öŒ–ìHæ!NæÝtÊ+ñ5ÃU&Ñ4Z}“²À#ÇñÜZµë‚r¹Za~ ø[#j·70×!èüç­ÎØíÉFh¨X²íàFoQÁO¼Xâ5ò¶¼×xy êi-7ìbô•B¯1=lЫOmu9˜}/+ö_õ;†�Z+Oʈ² ÍÎ%dXß  <èRÖ,ô°ßÒã}TMÈ2¥#¸¯‘²åÃH{b¬Võ:ýƒÁ`iJТEH2Aè=�¯�J°öõ# º �»šýãD¼H ƒÆ«!¬>”Æ ó´¦GuéPéDÉ>ZÕ\5÷¬,PCe¿PµE°˜¾Œ¯Ó—ùŠ8D±³’�ǧ×[�3m¯]?K߬¬N›÷½à{ôóyþ…Mƒã$˜q%Á±p  ú¶â à;¢ÉUÊm}ʺW.àÌí‘oxýîÊ•¤8�NyõÉÕä¶ó!3ß:Ô›l¿�žÎ…Ï£n§‚I?Új‰²O5ÊmcEsA¥2§sä.Nï¦Àñ6�†ŠN‡ØQ{^ž æmœÞNÁü<>'°\ö„ ú#ŒÒ.B�«ãï§BÙ ”6ÃôB¾@𬕖™(·h�|R<åÀï5w.d»uÿ‹;C”:^w5Ò}=WHN_o´O 8í_& øPâÌþÙ:œbN‡õï°§þq¼”�ØV‚l^Ÿ"rWTqåru:”¾•AŒx³ìòë@0…øö‰žvæÏˆ8´�á/†sÈKO7ýÝ£?Ú\…¯å\3ÖüL®~ÿ!ï�¬ýû3NÐáûøÞüÏöüž�o´ñÁýv¢i…Û¨¿Píî¢îîGì¯Z£{R>‚Õ¾{}ûˆíÓ瓺èÏY„ŒþŠ› ! Š‹!ˆf�;‘P ZxœµWïoÛ6ý®¿âæÍØV ÀÐ(Ò,Á€9m°Ýúi ¥“Å…"]’ràýõ{%Yùå¦ ±LñÞ=Þ½»£³ï~ùpöñóÕ9U¾V‹$ë?Xø¨Ù Ê+aû“IãËÙÏ“ë^zÅ‹ßymÙ9iô•°XÂK©YXºd_™ÂeiÜšdi¹2Å.8øiá¼ðÒy™»·ô$(Ø$™ -j>™°˜x7Yd©þ_¢<^`C¹Î�Lª7T:Àæ¦8’·”m…]˜Õ?Y脲Õ!ƒ,]-è‡Öè¯h2�Ÿã·³´õ™¥›{|ð7ÃWÿÿñzU±RrsÜq I¹Er+TÃ]Й5ÎͰCÂ3 R1ÓvpJ5 Ô<‰&…Üö§«ÝìÍ$¢Ä“Ý#HJOˆfB›ÞÌ‘¯˜@Q�ÔÞ�gM¥Q…#¡ *¥ÇCBwÜ©]&˜±È«þ$Ϥýx°“G£M^\³#À�);¯G¿]€ÿQ·cÚ¯þŠEÓøa½?ÔÑ’Å–GoÚƒwïÎ> D�F§ubG•¹ �C  …Þ‘Ù„…¯­e~ áž¼Z}jp,‘Š<ç�ç¢uU G¡/KÎ=UlyŠdbW+£„V,õšò[:,hµ£i÷¶ÉËQÒNõæ0Q\zxGD4ÞŒIà8Fm#TÈ{«k!KPLBæF“·XpÖÜÀH@gü„FºräkÛ~eº©ŒŠ œ’ 0 )T5º°pfVŽíVDͶn× –É¥¨W…èIÂ0HÞÙKtÖäŽ{îQ}5mCÙâ­âñ|ã¤2ºƒœ½3¾"a9¡ËÓ�ËÓw¯¬ûÒ˜c‡8Wb+Mc§T³p pÇ¥€P½^üÁhÊ´¢´[JˆÐê+ËåÉä @÷o¨Ñ†¨ÍÃ�ú‚°ªëéûçÐݧ4¶¾+²h}wuò€Üh©¦h@O=×!?W8¦ÌýgÍØäÁáòíÍ|w!ý¸›ßv1Œ•¡ÿÜëÐ?3å¬Uï&Ú…²-�%†8wcÉîÒçA>£Ý¡J:ÄX&Aç­0”*ž¤'4�(ù ¢Ú3pAeáe`çD½Q ù�ê[Z T‹Ú´Õ$Áê†Ú¡ŽØôö]*0« Ê%·õÍ FZ‰üºï<ïÅû^õ{ÿ)¡þŒëáô(¨Éb™¥\/ÂLk”°òßÈ òf½ö•»—¨Ð²BùåF5µ¦ ÛdØ=¦—èoߢ°ÿ…÷uOwô«¢{¡äù}É]e\ £û‰ø“¯'{n±Ç‚¾±ÝÕÃŒ|£}jâK#BÇgk�}øRñ ³gD NáýU8�ÞßX¶c£{];æ ±ƒÇI�éÇÚ!mÛa×q¿%ÜHÜV�kPË}öó7\E¬XÇéx<¾‰\âÿc¼:£ýë¶¡\ŠF¡Ã˜a£ÔH£,šý¥bÚ5Œ>#¡81¯Ce¶¬îeÚý²IãO¨ÿÚÉ”À‹{�:—zØxœµX[oÛ6~ׯàü°¼81Z À�:²t†%iÐÉòHI”ÅU"5’²ë¿�7ù¦8Z§ $S<ß¹žùŸ>ß<¾<üBJSW‹dŒæxÔÌP’•Tif®&­)Κ̰n¸©Øâ [*¦5—â�*,á…å·\0ªH£dðÈô|æw'óY@Me¾±<Þ-´¡†kÃ3}I†¢�,™S"hÍ®&'ˆ.¾0ÝH¡Ù£¢BRÕ“Å|F-ã÷§D?¦Ï÷ kHVQ­¯&åRhü�ã§h&󓯏ŒòoÈ<]ôÀ§Ì�Ìg�e´ ÝGj�mšŠ³œILÉÇrž,¨–xÊœ¯¢�µ>ÿ0ñ`×Î…ЬXf¤š©’aß ‘…{/Z‘9n%y…o%5dMuBtëùOɺäYIjº!)#à×ВÐÂB[ŒZT„k’¶¼2„ª5Ýhâe<óòœ^S®K©Œ3)¹»~¼½þ™´šiç^x©“œ#Vrü\Šjó¹oëÏ©fjå¢^ôÆÕ¸ž8ïñB[§øAîìô‰ÁI&WLé7Ò´‘»W}aؘ:£E¶f²†‹¥Ur­½£u«VØžûeVKø;±‚Ôºa;~·½~úOnz蘗q]ñÒgüXMc(œXÜÖ¤]_œ6» sVw¨)'‹ûoïæ3V/ˆhk¦xÊæØ†|hÈçq ùÜgÈ�('kÆ—¥ÍlˆÓºvÑ+ma½l<øc@*[‡V�}¢q |ˆÝ_¹uŒÙ&n«>d¬ªÎO¿ =<ÔGÏóœK ?ÒªÓh¨y{iG>$ûYû7��[sg[ºá¦GÑ�ÑlM/«¶(ÓxJXÝ@¦uÉa¨ó›-® î�cû'Ö>Lÿ²·µ$ÿO[ë‘O÷*ßݵ欠meB- ËKìUF/,èÂíY2жa÷¸fí@{î±ë:7f{=°ýö RÂxÍMé:îCÙ¼@�/�LáNq ð<“Èt¯÷áF»­¶Éç¶ócÇþï¿~ cÜÞqÝ O5ƒC¼ðJÃc]“ô²5;=dIýÅÈYw4BR1ºb0"þ�ö⯦bcŸð�ÂÍIíû)8%Í+ÝÊÐC8®Œ|þnaûÓfk'D*«Š7ÜäÙg«f‹ëSB¦¡2ÛÛ¿Þ}ŽnewÄ26¢8=‘¨Æµøø±ÝŸ_¹šN](—rý†´QmfZ„°ÍŒxj:žÛÒ‡þ»BM´Åmê¦ÜU«RÉv‰ê¥dí(�@J8÷íŠGvÄK™æ9KBÜßZ‚½Û±ØÏy¯�ýà)ž Ü6f¦莙Ræ1’,‰_Ü4,®º m BÆeꊷ^øGô„Õ‘1 ”Òì+Þý(&‰êþ¨ôß­ü¨íÈÅs±µA1Àdv¬DmÁ°6ä+kÌ”hIèÑP�üñtg‰¤2º_«nªp?g»š i¢JgÀìô¤‹ž9Ü/Årñ‰­¸¿¤8úiÄÅ|>“8ŸˆRíÅV3©��…Ÿ$íéc1q£Y0áÇKþ&ãêæ®9B ìoÂ2Wq;A�¥µ;"‰ÚN=µ5‘Ýš— õ'!îºÅÐ<œÙM¶.t4þò?ugjW€ �;,1ÁýI®¡æ;9¶�ˆ$6Îé”ÚµOšûÔP¤08~�È‘{4¬Éaj¥‡¾5øêTšÚÀ�uƒN- ¡œGãëæx"º)ô.�÷÷`j3{#?S€rÒ·× µø‹sK¥ª CŽŽ¦Žõ[Ö´„2©% j{³Ä"/i)­mÙ¦Ô°~NÁ8I˜iµ��svI�³''¿Ó\ÈÜî…Oã �LÑFÚr`®ÀÏx“–R®3µ†$Ðúàd³",Ÿ€§Â�(¼BúëRî}Ø¥íN £Á]®êVìiX*]ƒ§ûRš°ÐˆÀ‘Oìi•šÚJäüžòaö82ð¾[¬V—K>�܈0O\DXû�ch£Öà°PX˜22K†yHÍCÉÍ~¼= Z;/仄]WBË¿ŽVØò‚ ×EuKâ’ VëºñÉe¬æfå´eð¡t1èu�£7×¢^âMO’õ|lD’#m€v®[Éûñ™l Uu5ZP· â˜BÕ†W:¢Bjì–j;°ž°jVÙÌ$*£°é“N!¿•š—Wñ ²Þ>_áË�KW%¿ßƒˆG€’w8û0ÃìCi<‘«/[”„¬Ô”£G ñtA ƒ¡~zVT¥PK窱ÜXÓÎòmvH@ý¹Õ ©`%�)ÎîvçÖÞº"¾=X�¥¶èªïרŸŽ¹cú›å‹ïµhŒÛ�Ž…QÕrÿ»Á�E¢m+¹+�}Å�@wÆÂÄÁü×ùógßanÜî�³ áTTÑHû½êˆ/¿nëåãÎu¾x>®ëÛA£‰³ÁdëzªÊQ�Úõ4T¥gÖæ|�<ÆÙãØ>ßzâQ8Ñd�;jå¹`>ÄÙÃÌÛa‹f¹*íAu§¥]†W1†ß/¥²é)m{õ3Bžc›¬”–¹¨vh�|R<Àï ÷!äûyÿK8}–:^÷5Ò}½VHÎ_ïµß@qÖ½Lð¾Ä™ñ±ÖzÎÈë‡ßà/Îüãt);3°�»­}‰È}QÅ!™«åùPúV1òͲÛ_‚)Ä/ô´ FÄyy?éÏ!_zºéÀÝÑæ&|}“sÍÐò+{õÄMøä­ñ˜¬ñ�uÍ8º²þ»5ÿâØ†—Éøàj9Ñ´ÂEÐ_dö×@wƒ F÷ß{ìY#¸«|÷úöÛÔ/'34é~AIÂO4ÿC½f, ¹ ¬¹‘o�=£b$_xœÍZ[oã¸~ׯ`óÐigŒ.°@‘õÈ^;è&ì“ä‘–h‹�$ª$å¬ûëûÞdËJ¢éj€ƒq"ñžËw®ÎêOßÿòÝÝãí¬´uu™­â‡à>ja9ËK®�°Î:»9ÿûÙÏ­´•¸üUlµ0Fªæ–k<¢¸V…¨X«U+ðL˜ÕÒÎVËÀt­Š=]ñ·Kc¹•ÆÊÜ\°‰Ì@•­8kx->œ½LóþWaZÕq§yc6J×g—«%§k¿zEîS2Üø¨Z–Wܘgå×lcðï¿ZðÌAö Ë(üž­Ö—#Ü×ËñX-[ºç’¥—œø0Þ¶•³ŠÙR€�(dnñ@f™§,ä.ÊX›ó¯Ï<³+¶éšÜ1*ySTbÁr®51T;¡ÙF«Úñ­�¥mÉ-{æ†åZ“±¯dÁqÛ{ÆîJizmð³†;ÏUSíß–�ašw¾#öŸ"s÷lšFgõÐ(ÿSÝ@A¦6ÎrÎTçÉRÞ’o8…BŠÃ‹šíDn•f¥ª ÙlCS*m� ã•àºÁáèžÌ_Ú»gÁà³ë«»Ÿ¯¾…[ZÐ&íóÊ¿ûéêú]Pa‘žÝ¦GY|v#:Í«aŸ•~НÄ ïû”ø-¡ QÖ›�ÔŒRg^Ô 7¤kŠþ�ÜÈÜcÛÈB°Îãõ&Zb`Ú²¹àvëƒFiò›™†´cšYA6d=Ž/½ßÆãæ-T‰ªbˆn¾'Ò!ÆÌÜ1 àm•†/«¤ÐDÛŽ’ÎÇã7œZúcƒ—½­óžnºÝ›® ‰±L¶WUW7Lzæÿ¢n!Ûs)ÄL#`ý¹aË ¡çóÊQ|‘J翞@c!C:Ó’¯«·*ÚØ3ãú¦«Y¡w.GMDô€hV[žð1gW¯…Cž:8ù†[epéHlz� žÏm·i|˜ÕdcFJ‰€¡fò‰1�.LËß�êó¡‰z'j›¬æ²¡ÞªYdÛêÙ¸ŽkOÔµ„ìøDÝî�sxEvÝú5š±±+;µ<¤»/…ƒ›7ÒRóáú9Î,…ÑbP=P<™¥È¢j”›bwt`C1)^‰�¨ aÁ€nâÔa@@÷±4u^9"œ~-ZiË^ÞÏÇÐã4 =Ί¡Ç1 żö9 Å Tg Êÿ¢±zÏp�„B׋÷Ð7X³‡ûi6¼ŸÕ†÷c6A8Øn8ä\ [ª"Î6DâŸßí[ŸZ¾ !0ãŒâÐ$²(öŸµùw§¾Á¡çÆŸ¦ÜŠÉ«â9�æb8Ý—Ì($ÕÁ¤˜1ÌSi\•.>~7„»C�½³±­¯+xQ5ÛËïÅNúÀsŽðãûÕ2¼fÃòÈÕxš+]„ñn0øÏ•¨/’c�±Zâæ Õ¡5ŽZ8†‘£ßÞM’%á €à4ȬUgºçq„]/W�\ h«7RÐ!ŠïDƒœÑ5pN’&ÁˆÚÙ�+fјWðGæë4Þì�=žŽÖ9x­”±$¢–> Ù` C „Ü )å¼É†²&öηvÓò~8©äÄè/FÄîÐéÕÂ�E�ß§†Ã5µÔHP>ñj}—‘_RÇ©¨§.¨hå¬�”åó\½CÅaÐcY/}«ºOY&Œm£ie#µ±‹@ÜcréÖAê’–|Ò.R8;w¦÷¼‚<´á3YòÅÉ÷Àaðô_ò NÜ`Çh_XÁ1ºn¸ ­¥` #…={ðÏ7þXœÖ0�¥ÝàÓ¶¸"lèÜJ³¡9!W¤hcY^*™‹/”UÐ…VöVèéÍÛ ÙÜ;ö!÷Ó4òÌ¢5ÆÃ�•Sƒ²É[‹Ÿ*ú}Š«­af ³R,¡wtÁÁü�óý!}÷5�„Æœ»MâHÉöÍ» “lR–z1ë`¡‡ÐgÂ{oØ!÷Á.±äÐÖÚšVÓ:f¸Åò:Ç@¥]t‹�´vnÉæº _¢!Â)•!7Ô5£>k„Yœ‰ûÞçüF¿é&V´eH_:»ø;‘º ‰™4_Ð7›š$_Ë‘.—Uò)íÁ^*zÞl>ë¥ÄèòVïÁàÞÌÙÅm.Žóã0=þñ¿ ܳôGô_Ü�§HˆP�<‘$xœÅX[oÛ6~ׯàØ×ÆÂV E@—¦X±µ1Úbiž Z¢,n’(�”ƒüû}$u‹MkÙMâØ:<<üΕç(ùîÍÍÕç»í5)M]¥Q2|q–ã«æ†‘¬dJssI;S\üHcÐ�0O?ò½âZ Ùl™ ?xþ^æ¼JbÏ%q/i'ó+÷ûTf„6"ӯȒ°F # «ù%=ÏHÓ$fVò‹x°%-É*¦õ%-_’Bãï�20=–ì¥íp¨Û“Ä­•›’+%µ¾8°JäÌðœ¨q7©ížMäYsq@Ôúâ%õ»_aéLÇ@äîwžRÊ*ׄ�C2‹#"yà Œí �È‚£‰QL4¢Ù°±W‘LÖ-ƒp/©å*"ÎyNDÓãûl7ñ¼óœp–•¤Æt/L);CLÉ�Rsu`ö<œT2ãEÄp�£e³!½À?,}«x.23J�-ý mÕY“Ï—Š®™Vô½G«‡¾s,àµr`ËD4»‡%Ç8r˜ ¬Óî«¶ßç\ô\A•2·†8 �Çý.0à†FÕeFª¸4ÙìÓO+-£qOŠ`MFJÅ‹Kêð}½Æñ�Ó~cúÙ‰ÎÀ1Xz¾3;Œ1ÒošØ7=œn½²¸E!2wä±µ¼´eš.¯ûw¶ˆ‘B>߯dcxctŸÝå‹tDtÞw_[%ÚFpí€Ñt;ì)öÂV8¶sõ˨41y_!þ�øgçY7¹nákެj4b·¦é ÉCêk�‚ϸè㛵m%�€Fº j}6¹ä…ù�1Tùõq5ð·¡ò ~=%†tú€2o3Ä¥ÁQ-­û+e%úb$•E¤mtÌŸÏA×öv`׫Y6ÚK…Ä©F¬À$‡à¿k°8)�Mû¾�2Cü[›NÙ`Ÿþ*h†`ÇÕ§„MÞµ èê›ÙuGÓ#BxWïpº|Ĺâ/4ýB5Ɖë ÖBsGÓ»šÁù«‚¹¥émÌͼŸàb_šÕ¼åþƒŠX7Üfö!èÉqÕåFáºE´H|Þ¬­¥@ß4Ò©{ \i@éʼâ6óÝ%°¯…ô—·8Œ¦îk9m�EW»’¦6l;oÂBFœúüÉñ™-“…’õZ€Íu¾··çð+k„ÛÅoww¾ÓŸ Æ ¦ò7ü \z¿Ãô�·%š{ÄÄdí>~ѽ’2Öíd­}Æm¹ò‘}B ©ô³¼'5k0"r8gÌÂ^³9üxh“ŸØ{ûIhh¼ßû§ÿ¢ëž ^î/οGXš÷N­ôxàNvO™¼7ÿ*žªö|̦GC÷Ù>àq[ð§£µ-*cèûÙºäø%LDdgVTξ( ów�ɺíàªÐˆ1s^…•�]Cg/4Na¿Æx÷€8Âræ ûìfô/‚Ó&˜¨qÿ".ö/úþ”Í?f  ö ‹e�?—NßxœµYëoÛ6ÿî¿‚c¿&1¶¢À°)Ú4C‹%MÐd}|*hélq¥D�¤ì¦ýŽ=M'í ‰eÉ»ß=x<ž“_^ß\ܾ½$…)EºHÚ°%F²‚) æœ6f}ú;]"Ýp# }ZsYÝ}¸N–ž¸H–añJæ–Õ¯©6Ìpmx¦ÿ “E8ºH©X çt4FÓdÉìúߦ‚�²Hj’ ¦õ9-^�µÆ¿S|5¸(“9Œ$&«)ƒ²vÓ’emY¥ä®©k© ÙBf¤"%Ë ^QÝ:Râ|q¶ð+r¾mÅ—úôõL^/|"Íш\ý‹¼I!E® #:.pÍ��œIØ‚d²2¼jd£ˆ®e¥�¬He'œ™‚¦Pk.d•,¡LO9Ϊ47|;„BX•“ZAÎ3£ÄÐó\#€ v$gèí7…öº„%­…ÌϹTJ*MtÉ„e²*,¹láø%™Ô†TÒ ~›¢%‘•xpäJƒÚ¢£‘�Ñ<ËÈ »f…ºB&K˜˜J{UX«Œõ ·ÝßÖxº)‰\“¯ * [&š „mÚÇXé%*q_8‹ÛõSŠãb„am|õöú®u7úöÞáu.ü hpÂ�&F!;TËYî„ì žÁhÜfu;2ÞÉb8v%µnMdòC™B2nGvR}EƒýÙ˲f½7r%Ë`�þ4�²J†É~r,[¬¸C„èÂb î…f cõXD»€ñÓЖJoËn‚rnv±ê¢YÕX%H“Õ&½”&´Äíº ¤!˜ ëszí÷Ç—+`ÊZüÌæ©gAí3Tm¸.f¿66BØl�«Õˆ¯yæÂªg9%#¿)Éç4§ý³ˆÏo¸Ñ¡2:¤¶âyÚ‰!ùR£ÃAÚI¤émG°Œqýs›�ÙÊ%b£ÒÄä!#þÇgcK}¢é'Ï8¤KËÿC`£ƒmä{òÅÍ!ó3M?Çd¾oÓÕü"ß5åÍ ÑtBˆÁÁ)+P6ÑŒÒX£}úv©Á…¿;8æÅû^îô?(ˆ¦í·¨ÁpÌã± Þž,¸qf7\ ÞlýëãF«óæDÔɇ…¦éø=ŠÉXHÖYǂկFÓáÛ!H-¢î”Þ2ÅíÆžX8¬ijïÃyÃÄútÇsÌó԰ȰGõ¼˜îø¦d4u�ž;ƒçS9ÉaËݾ;¶û®š^71,×ÀŽ/Ýf�[¦0* Ø}6!D3t7Ú'!²cº­0í©=/Ê—¢.ÐkîCôºa‹XãiÈíiךmRßÍ‹êÞ`hj?ãç/'¨N°ÜBWb–êÜ93"ÎÐ…ö3†è-V*ƒÚ´Ö þZ7•«uçÅòV‡‹Ïg{šN1„]娕¾£Ó�©#»s„Í8~�Á½ß�0Wþk[˜ÝßîÊ1Ü©SÊ[5øÜ_]ŽäóWòÛ…+ÅÑ�ÆFâè=g„ê=L™ÎÞ36Ra…,†¥D”ß08ØàY¿îhùøò[�äÓZ#Nÿ‰šƒ°a¶†ÀÎ…ë¼|¤éÇ®AÑîÓs§c^]æp Ћ–Î`�}²¾yÀÁºÝ7µá%ÿîÔÇ­v{<9åÀ]ŵð‹"LŽSfÞ+Vi¬ú˾ÖìHÑ$Ùz׳º<Üczë#¡-F‡¨—íuóðµÕw�Ú;ëµûŸÖ!¯=íGwù½KüTë¿lçäP'n¯õ÷³žzhÎu†õëô0‹’£¥”Ÿ«žlÈ1"P¦Êš|ˆÚ·öžj3î¿7úb~àÖ�4è~P…°ŸG}¿¸*‹£��ãE¸â>tÛÄj è ;݇Ç��?ŽÅ¯ÖAÏó)À8Qn—Š~ÿB¦mÇÒAc6ߎáý\ðÜ7MFõú†%í[Ã1|£fî#}áÐÓ� f[pZÓ´û½ÔãàÓ¦s¿Œà�HÀYô Z†�–þw¬ÿ–×�ë’�>¤!@xœÝZk�·ý>¿‚UQ»ôHŒ¸¯VÀ"±‹ Þí¢v�AP3”†Ùy…äH‚þ÷žKrœ‘dyÓ¢E‚$»æ�—÷yî!éåo¾þÛWoßß¿d©É³U´l~žàG. gqÊ•æzR›ÍìO“Æ�4™Xý]l•ÐZ–Å=WÂ/"¹-‘±[aÒ2ÑË…›-^âºL$ÿ‹•6ÜHmd¬_°K$aI´ä¬à¹¸žœ^0?ýi²Z.8mþìŒêgÖC‡gX^±8ãZ_OÒçl£ñï 4cÒÉ/ØrÇÕª\ÿ¸\Ð/ìš-×gæ/ëû½]s›dnÍÔÉh§ºÑÏ– »÷rQ‘~+ö•*µžíx&nãLµ»°ÜºµÜ …ñªÄÊ “F3£¸,d±eXÈç‘“˜È]cs®gÏ'ngïȦ3±�µJoH¥p+‹X° ÚoÊ,!µ�É€hÁã”™ ¬`&Å�µjÇiìL¹qRüO³²6­ „6 ?e¼H"¦M {¬„¸Ì+Ó:•Ól'”d²ðîxKþ‰7aÎØ7ø¨Y�<ËŒ‚^"�F£¯w¢V<»f_ª‡3RÞ¼kö`¥:&è­¢�±þ6‘E0¤gÌ ¼#“â]ÕÍè‹ò‰®YÎ x?È>Ê­=÷÷–E£Æ‚¼nT™Ûi)-˜Y¸•@&@×Å%rߢS^„^¦VÔ$€qY�l‡nBPMøjñ]Úéi�Ü>Ñq¼Ó|bLa}—¢Ø š{×U�”GÕÅ%¡ŒlÓiJÑÀÙ£H¸‡;éünf³1dÔejÓº3ŸèêJ~jHR¼Ë CaÌtÃNû�µŸ(½DW›ôȉ1‹jÉš *å�t¶ÚžNùê(¯M × ÑÊ Ë h vŠj° ¥–y€5±ÒROd¤Äv”pYY”Nä™!&±cPV¢¼è!=^àâÞÇ›0ž�šˆrÜGZn ¹‘ I85d˜Œ‚B_ŽÆ×S"6¼ÎP�¦d~nk¢QÑ®y‚à#~añâbVhÛ¨z©šXõà£}&‰ºw®îJcŒ}B/%úêÖB#Õ®@5ÇÚ}Z‚�(:¸¿È&F˜…ãÑ@ ·¥GJ«È óS'‚. G½Äp{ÄÕ¨d:¹Ø£Qg¢mQkE‡íÉ)Å­Õ�}$’ö�Æõ¨^‡­VOе®®LZ-AÆKÄžWƒSž¨U˜€¡Iþ‘'ëwöÊ'z 5Ìpþãzâxùw¤Æ@àpäÛnä32Z5 ë)…Ä.,·]PÈ—ÍE–§”ô|²‘†òŸnɦ&á.ñ¢£Ï…sÿ\öóÿ|2_]ä ÷272ÜžX{gûRáDjGõυèÔ›`øäÞ<>îÓ@«nÔØêñÙl(Mûxôë?1?¦åüÿ�Ýþ/0,Ü_”ø7ƒã€‰ aRa¡o�AÃbJƒxœí\ësÛ6ÿοç›;'sr§×Ç%Žf7éuš×4¾´ýpí@$d¡! –m«Ó?þv±R¤DÙéãf:Ó‡D‹Åb÷·‹Ý¥Oÿòùë³óïÞ茹×ùv0?=æHõÁ|Ë( ÷U,͹Ö�V³¥†©©Êzì?d§—¼ž«Å�§Çø�=f§‹y�ØbÎîØaßÒ Mú޾Ý==¶tO�«_cÍóEÞYõk¡+UjñŽ×¯@Z¿ KUMΗµ'Hþ9‚¯æv«ÿ}%ò\V�ܺe¶{pÉófXüsö\Æ™nªJÕ†]ŠÔ¨š<]ÉR°:¬Å ˜•ßKh^&/ý6 }ôñ‘¢-lðܵ£ ê Ëš¦.uËWâë34ë‰hÆ®¤Y±x ¶²¼`f%XU‹LZ:7O·L:¶†[WU!EÆœf.l®•u¦Æ9O’f�¹0‚ÚKX_A#Ò9·0ºdœ9žbB]ª¸hËx…»Ê’0=ã*cJ;j¡q§ `Å�Ä¿Y®‹Lª%+ÄJ°NX#È‚™hÑ6ñ $ÒNHV˜Aï'QÁÖ¤ËÛõvvò�C—Fïð·�kUlb§)èw/¾ìÀ—±ø2€ÏÂV`ñ­öÿ®oÿ•@»¼ÚþcµuS—·©fÇäîÕÝV¿±šÜ·«Wè’ Ãô‚éNOVÛ  Ã¥jñsXñ¬ÒV:¹L*'– ÖÎyÁAÍs]+‡°D�‘8£×þàoV0†Ð›€^K½â.¨”°+�µ°'l¬>Ô¿À¾öj?v«öVÎŽqߤÝ�Rlñµz)A‘ Å¢.UÀn{²-ì¸@-á™[zÕ³¥7`´‘K©@NÕ#S6ÆAÖÅ .S Z›àóÍä­„G$.;‰ë¦¹úþÃÓ cW $ÊÊm&Ìkìÿ ʘ$l� Å€¾Ì¦»Öf– ³¨PiuL"<X)¸ ¨Aü$ Eö¡ÂÖHÒX¤ÙxÜÀiÜÏÁ£ÛözƒƒÛvèØ¶NæÛçxý4+½�Æý êw«¡mi*ÂÔPß¿ ¥¹( ðÖ†op(]|î@Yþн G†ËÛÏD�N£Ù¨'kXOQM&zÙ„¼¤­-ýŒWÏeee¡Õ^Í„ö±JiÄìêãG^,Ž×26 Z‘ �«Ó—”‚¬¥Ä’Sÿæ�AX"Œ^µ%/ ÐИ ÌØ¬†ž€ç-†wæ@jÄ…ä7{¬®kgeæ£J\KÅ\ƒ%1ŽÑÆ5€»ÜËeÉ÷ÚƒZÇZ#ٵŽƒ°–›Œeb%½‹øÚ�¬ë1µL(ަoÅLZ§läò?4#hð’|ù¬ÞÜÞ’·Á0¾8èv’¯Tüu½Wë×õX•£„]}_ƒƒýÒ0+w@a�a‹¾}«ßhÝo‹Èw›Öádý‚Y|öR›6‘DI£� ʺ ÕûDõIò1…„Ä’UÍä©a{ªŸC>Ù€› ³VL@­.V] ½ÇD@@ì A(ìç« \Eìo€·à„7 4ö÷Z¿·Ä{~&xë6•ˆ3)òì”w¶B}ÿ ËÕ\t×DåFª'XXÓ ÂMëµJp“Îè¾÷(l�p iä£6OHžH¯–pŒãî¯oIüÕý§ó°wXJå@ר-‡(% 1S+…–Pì'º…¿ºŸÈwºØ(]J^ÜšL˜¸ÒT€¶—6&¨UÓ«±´ƒŠÉŒ!öfµ 1ÒÆ9cÎ+)1¸>øùüläç¿©ãdþ0áVmTôXÓÿ~¤|E6Ó ú¡„¼¬ßzö® H½aó~ªwªþ—]™wqB—]/óa‹G¬P|Ÿ€&PNˆ™¢ãÃŽó¢Ê÷;9j{êƒ�ݳþ©¦ÜN,r.1MкåÊ_ ;Bic Kì3r_ì¤K Ë$šIÀÙ‰«pkÍàŒ9‰å8 ”v“¶9„Ë’ô_T!ŸøÁärÚã  ²Ä“aåR‰ìßÿFiVðË\ˆnþäãì—`¾¢}B¨zóòT¯:ñb7ŠÇd�Ȥšì�q8ÈP³i„âÞððv´>CÂí‡6ŽE�—1T•å„&¤ŠžÿýøL˜øÑétæŸtúï“{� _=.Ù”Ù?¿caØPîóüÀ;å8ë‹qÒzÌúÂPY�¡)Ãçqéhkèæ§jų¯VÐÌ01*†ý•]HnÛïÜPÕ³´xº0–Fú欟ÄMXÀ{ÿR¢ o“€RX€´òªf#�¢[ÏC,i÷ bÜÌ~4ÑNG¢‰dì¢éJ�Ç›cí8@¬o׊lñJ5‰Ï#2Жª»Až7oÄ×oGÁ¤ôgkÖñ‡ôÊöÀ»WÃ[ýÆ*{GÜ®Þs ÄJ!UJ»É‘æWyƒ5þ7š0¨9“5ö(búØë6:�Ü’¶«ä‡]·ŠX,­€Àíµ6ÜÞ4ÊÄhȱjŸÌ¯y¦kãxR»@�5ée5‘2iÙ×�p¬-†ŽOéC˜ùrâ³Ýq¬�våL}Îx%µyb�"‹ÞãïAÄ@^€Fjü¼�„Ô Á½×؉¼›øy[¾ÏTºNLBè˜Å„Z·á L¹¥«;êr�¸”wK^ƒ^¹j‚æ5r† t«E�ÇÁµ”+¡Þwux[–Ãöå°\xXâC¬Eí$§ûˆ÷«š~þØä�\[¯Ûh'·%mn½ÜöÍ‚ì›ö"lÌ¥“΄�ex{ý(Ýén̤|Ìä$¢Æï»·¹°¯$ôغŠQ8[ ¹Ì]kîÇN¬†þ§õÄCÒýÅÐîµ Öm :òÂÃý �¸ê¼µÇ«Þ)ò»¹hà¡D�ÅmÞð›ˆÂÑÌ÷‘;±Ô]ã+ît{�…ä¡C�4¶WóvÜëksýûnºÒðÈ•^¸ÿn@´Õ7…0–ö "€ËçŠxæ•×CÃÝG×ù÷H}õuQ¼Qõž[q3ü¯o’÷êòq¬Ú‡4tÛ=ñþ¤¿[¡¼1�mQX´hë`ä¤ãFc=N:ŸäåÁ`±èä½op �~Ù�ÿ$Ž SCˆ†åf]§ÅÔ“.¤ºÌ–à;ŒÏ⢜×,/°qÌ-èá’~ëÙ coÞ7Þ&•* KX7_N�*“I;7‘yÁ¦œ‹ˆŸô?!  ¤¯lB°¼ oO}Òå-°¢3xz7˜@…¨ÎæYa„Wa&Úº/îâè5<¾ÝWS?n``SV¾­ÀW‡;�ÏÂÖÅ�°èÅ‘c�ôŠ ¿Ê¦ˆ¯¼'ƒéŽ�ƒ€›A”¡Õòì¼X7Æ=™MÃÛˆ„LÀH®�ÂéŸë²ù©,$~h²ö8ÓùbM…$Åxí4„QX–°`ýrE7°ÛKƲ~­²Æûyi�Õöótâ‚¥©ˆl Òý$¡”åWdãu.R˜ÝÛ}Kð`¸²xòâMÏ·úª #p ÛŽ�¡LÖÿÂ.Sx?;x�•bH – £Àt>ˆÜMï`†:½ë·bQ³qí !èÚUuð%™XpÂwX±Ì€h$¸rLÞs1¾L$í'ƒôû+ÊÂÑÁI*|tÁcÑ¿ u=um!`¾Þ�úoƒÿ ´¬$¼JÔ"aµºÒÀhM f̨.o¸±TåMŸá·¥à P¡‘G`-bï»Iì�tÙsg±Ryæ%Û“T1‡CºÛØ`fÅ/…%­A›\{!j$o÷Îõ†ðqcÇ(}«‹ç0Ój“`dÒ‘±Åzss¢Ò2W¥Ýà‘„”Zy«þC&êZÕ:aºày\˜ÊÉêê3?Ÿ47UÀz© XÏÅŒi8–2_oèNÂTc´ÌpƒÜ0Ó€ä"U…è¢FÑ ­“‰%orƒ§›¸ò§Ú—7'vÌN>º÷ϹI3°§Z-P¸\ìßgF”`3þäüQþ½Ö?5ê‘fàËŒ×,z)íPeºBÑ^á^>9ñä)k4©XµÜ»’ 茭Ñó,“ÈÏ™ªÈt%R¹”îìídôDG£VqY3^_€z— Òœ!ru5x†/rq䤚‚àgŸu0¦Á�+ FߪÎÀ~ýW]ع„t‰W5œN½§`jøwûËÌêñÁGþv@mý)D3«xÌ'Ÿù1ïZÉÑ cCŸ(豤³.i+�÷îÀåÏâ0¸ “ÅÃí*OX®.d òÖð^;q¯Ñâ¨`¡šTªgþz¥šÌR8Êñ iX±ƒËñÕ\–@ à<€<,5zt«…¨~¼ªòÎñº¥Ûn¶³˜à�eÉsíàm¥³M*g`/ªÆý¾ñûÑ[ås»4iõøà°É\\Š\ÏÍ$e*è3ruHkÕ[�?¹@e•¨#©ÎŒAD$F£Ûý?ãd¨YSk†O3Ä_ Š^å<3$œ8o� ð³¨yøAãRÄ'ÓB”8ÕÍÌÆÀqï{x}Â?f<ë Á®+ *² <ÔaÝbº1ë~‘Cç¼HL€§„@'3ØÕ“2ÔàŠ¸c y6�¯Ïdf �(œàˆuÄÄ+VH@$K ’*R¹òb…C Pè°aÉK„5ñ[2;v^)·ç§ª)Cèž5 BaK«l_ðPè¿u‰w9xzÒ•HbEBÞᄦ•€HiÖDq}cq|"�Š´ÊeX¦ŒèœU¡)ž‡<„ž`H£Üz7œ®Ç ÁõB]£ðýAZÍê+ ÝN„ìÂë]çô½TiB7ñ4#]v ú ,Y7…½2–âQäXi;Ä·^¯œ®“ÉÀ5@Ó¹p¿c<*”—w¦D·”‹¥AýqØhƒö ÚÑ7~ïÜÉHQÞÕ1ÃÕÜ|½FÉUcˆóº¡[9צÅ6¥D¼›¨0_Yízîðð~Á)wâpØÃ\.KpY‡&LËìÌ�©Ëv€{váŒ&ü|z¥òu© ÉCø‡‡á~Ôò¢P2ÛXß’¯ÑÙN…ÐIq!º’›öÀÚ2éíwãœ>‚G_ÚOò¢T5E÷ö¼Éð“ý,Ÿò-*ÿž0 —>iaÒ)[×¹K bñÝElHçnÄ™Â�^+‚3 ã|ACæmÝeÿ·Kgú(!ä„Ý(ZõÒ6PÞ$}HfËŠëŠÝ9úå—†±Ë_~ùþÁ]º9òÎ ûRw¿ÿi «+v†Áàôîè²)‘ ¤\çˆ^/—Z˜Ã±«x�}¼Kmß�À"2/Íø,9» .Œ#ÑñÛK&³`Cî1õàØ¤A &½ö÷:Gô‹šî&à)æHЉ»l–ÑP À§¼@½=¢ô×íøI†nuS¨÷' õí»—f]íkØ(AaÂ\6žrHBZöÜú?èË1@l�¬lâaàPœP—è÷B¶mkžÏ²ñh˜ 5ä’½N È;� ‘Á8ÙÏBGáòH�Ÿa£)w4¸{ôEp�½L9ÑÁdÞÎ \��;åøÆÈ.@Eˉ‡õªÙqNàä@¶÷Îý;ù/É bä'‘ý€2}<9™’®ÄÛÝ ¦$Ï"ȃû)ੇLx±6B€ÆýûSUü\å.Eé.žFAÃxáºE‡JáIZKø ŸîýC8GŸØ#˜j�«Z–ïÁ̶²þŒ î¾�³gmŠNáA“�jOࢩm%tÛœ%~eßÛ«&¥¼lô¾ëj‹nìá+çz4ŸÚú4 ú\h8h›~žœz½ßd|ùíŸ!-ÒW•k2Õ;÷Ý+ýÝ-ª­1Ei¤Yÿ¶«Bð±ß‚6Z¹¦°bÚ¹µ†á%`ÂàC ~ J¦Ê‹ù[Q`®ÕCvzì%Œ�r¶ªÅòñÁKª£þðB}›ÍÆæ…¿´×úÑ}À>�‹ç�!_îãV¦Ø4`'n<>˜o<Ú$Ö\^�Móü•0Wª~O$G~<˜�ü@= Ö>I#­™d¬³·ïÈ޶D Žl�¸AÁ|„x(Û‡áµñÏifœ9 îÜVÎèǰ�uóúø c¼1Q¸ˆ�ky’W+¾™qëoÖÅTE¶q÷Sa⩞äSÉý ô ™H1ëC�ª»|�{â÷ïr)¶®ü A­îò#\Å·­žŽ«B¨òЉ¿\Ñ=$S¯}Ó•¶ØÒžAw°¨æxÞ†¾¤M<»ÿ)ñ"Úæ�†N•cu½âuÅq÷¶Ý dFmm�—Öõ¬VUeKNYl¬{%SL"RðhvfˆÎ­�âré¯ )]ƒëÙ¾…’^cÉ&©¢‚èv€ÎÐ/€LO§¢‡Ó†Q¼™æ"Äú¹41Dz› àÊÐßž�`„Kkw» 0Ô)Å•;©8Ýo±iû ’=±£Ã¼ã|"×Qáœ*lÉP3Ç@Ž7$°’ØInžmm}i1.t—°�# ­”í[ü m6ªøµ"Z½n+fß0õ‹Dù®),·ÇbB‡È×TÉXóî%镞©¢*ñl|›ìUPºÉOÕ˜ª1ó¤�¯=Vöp-(”«*r ig¡°\KüN© [úÛ¬Ûø¬Q»q—M€=ÁnléaÊ¢ÛÚ]¿Tù�²—Ý™8¦��uU8@ür‰©?Û±>�¼‹Í¥ú�(BŸA¶W„�2®Ô®ŒŽ"qÏqu÷±p#$½5"N‡â)˜ÉF�¯[Iû@è×%:!.ļ~I|û’›÷vX�ù�—úˆx˜‚x”tŽŠ= Hz¨ñë``‹CÿŸz�+=øã�{·@¿ˆ•—(ŒûíٽϿj3ô�¹mòÈ¢·i>>³å;á€$`§ ÖH_ò~ Ñ2n ß@QŠDUWeàÈ9æ—(Æ_À5û¤:ÐØénu�Nm£e*áZdse»»‹?lˆõ[GX[Ú�û½ÆäG´½îMìîî B&„Ì(nÚ0ÓíaTÒkCºIuÏ_±·èá^¸;y/tO�–µ*,w~+[YV©Ž¿oaý͵z2ÈÅ¥‘—ñ‰ØÚ|hÛK,)œ«Çpoqߺo€GŒ§©¨ŒöEª‹?¯·Ãf—Ö %мQ«óö6º³w/³<6‡–Ò‡·»‹ÝÊô�fød†¯œmÉ„ïi]]ž=û3AÉ†Ž¾áµ±ºÈ^" ~ŠÏ)û.,zSï²ZÍ·ïâ@̘…—éŽì·ÊÓ´Á 5R�´öÒ\Ä7?{¤�ÈŽO\ù.  Þy“ð7±ËÝ¿ÑK|>è†}ÏÜûFùš¾9ˆû3�Ͼz:³=:+}#.ƒ3çÆëÐ …ãáþNÚ;5.ü7ŒUÍhc¥‹ w5˜ù^Ó¸i.é…�š�VeX!±q´Á—V­§±ùû%wñ鯶íÝQŸ¡ZF½×®U*ÁØr¥S¶Ÿ;Šv9¾H�=øY<{`g‡4iBz %°kMÖV\ì­È¦Gºo‚÷QtðG‰ƒÇq¸O­ÿûÁV ï¯ �Gºâ�èû©Ä=îúé÷jÎú>ÙÓ›P£9³&p¸kÑt·ÛñDwRù…F‚®jj‘¥ˆt‹_rê½ñ'ì…·}O¤÷G"´O±ÊºóZ[€Ñb¿†Þ,T¹æé¤ÍÀR_fÖýV”R úà?{8:d5pAF:baÄ� d1�æ�V?–pråX½eÎÞâ_™Ø‘R*IM1’ÀÖ^,1mü}›•‚°Ò¬ol$èùÕÐr¶_z!Kˆ\µ3´ï§àËó° ˆv<}ïþœŠ»ò¬ˆ¢ãýõþË^“¶R1Õ®î7½é»?uLkê…N [[“!�B¦F*6xœÅZ[�Û¸~ׯ`µ@Ÿ&c´ÛE×0™MÛ™d03Ýlž´D[l$Ò!){œ_ßï�”,ÙòL¶�7@2–x9ç㹓ÔüO?¸~øtû†•®®²dÞþ^à§޳¼äÆ ÷*mÜòÅßÓÚ�t•ÈîÄÊk¥VFˆù,´&óYœ½ÐÅŽhý%³Ž;i�Ìí?Øá,t'sίūtØ™fó' =â…¦d¾fyÅ­}•–/ÙÒâß ¼:ÌÊu!Lç‹# ÷ãæ³5ÑÊØk©¸Ù±Bä’†1‡ql© 3ÝÔ$ .ä¦e]Û/Ó0ÿ¡,°>äåà &ëu%j¡œeœ]_Ý=°ÅßË„ýËè­+™]WcÏK¦@‡Ñp²R­*ÁÖF2w@ÉUÁòƱµ–Êaw¬Ò[a’0üKÃ1” c0V/}#@®µ²Â¿ÔÚº OÅ:½¶l[ ˆž§´Ìi�0[óªÂ#[t­AQaNÉ-†¤¬|hL(ݬJ¦V˜ 4>}¡] :²ö‚êÊÀZvŒç¹nH(KßX±–ÂÁª†«\´�~^k—°œ#EqɼøW�YTA¦è5ñÅ–[4¹(.ØRëØbÇjaVDDÈ… ’[4;ZP5†ç;/O“r H��Òí ‚F�ÈE‘IhI›BÏ�-{Л¥©¥Þ2 ~²_ˆ4Ì6 Âl�°Ö‘xvFä ñxšÌQ;˜ÂÕ¢ü* E*¡)�e‚ÂÒ ³åD`+aFÁ0 jk�—Igù×Ð�#�>e¾zñ_‘{©5d{~}aüR:ãö#¶lTNRgQUÁòs˜€3 k�7© H_Q—L7ž�Ÿ 1ËŠìVAQ-Cbæ —ªÅy%­+xKÃôýÐØ_D5uÀäRæÞ@ƒáXÄ;a{¯TߊY-m·þž÷G�z3‡¾s¡H{š)�楬ÈR߆Ւ½ceRûù¤x4hq[P)“Á5“N�ýE‡•äõо'²’dËÒíiÿÙ èŸv”EX¸Ö‡I�"–ÌŸYA>Ä[ÛÕ„îJ>(©S�³;¯¬ÂÔu©µ—à‚C'–ÏŠˆ‘OkDÄpÞp»°baä¤r•D§ÞzÔèé¦^´žS7Qm1ÒPt³Ñ…Vo¼H¢¯rÈs²ž`Mäi¤Ü6–Àµìúo—/ýÊ‚P’Ñ,Å6 À˜%œÔ—ÞÌÁ�WZ‰¾å@JN¬´��U=¢Ì/‘Z3·Eìd7KÄ �¨¼ùz��v–ôÚÃ:j�oÃwˆÚ|h¦ÛR[æµâ.$ ˆ [2so3Þ`¼ €ü„i“`b|õ"÷ÌmÂVä—î'vsõðîêõÞ4xµå;(žäBá>€HCŠWZ1µ%¬æ;Š^9!†ëÈ%ä C¬vÑFÖëS‡§¥šzDD´žðç6Z­²�„À•äq> u0 ÍvÂ]Îgq c÷ ‚òŠBa̺d,ŠÒL—µ)}´Òö™²ûL‚ˆš” Ëh, Ãp’/Éæý¥‘kbr¥Èä£D­|Œ>Ý1o½zmôZ'EÀáU.”‚€E½†Á“òзóƒ;¢6(êS µk‘Ð=Ì�WV£Nje‘0†¢¬ÃWé JÄÞßÞ n(Ñ]R‘øCôÓ^øç®ý™×ƒðJÉ$Ì=nO³ã¶czãHºl�ö÷Ï�–žsž^P3T{¡½ÖÐ?j”X~–?fõaÊüm¯:Ï&Ín»¢ ?R;`v&›»"–­ßHò‡a÷å¯iök ‹Z"ˆÿ·{7掇æNÃõSš}ãz×–ç`ú¾©?ôªÊ4;hôÞÇ Š°z0rZdwHÿ�ã§Yû4*J">>ø´‚ræ85”�iöqŒù‡~ŠrUºÉ¥Ð™Ü{ìélš ßGµCmVÞ'ž 7’\dz=Eó$¾iÖ;…îhÇÔb›Úõ¾`èäŽ6��}«Ð¹æhý1¹<ß<®‘Eq¨ùñö1ØíÈžöU;-Rìñß+ÂÖ>�¡yM%8uÙää€n0·ºå‹Å† ¸Fcz×ëÁÑîi‹"{;Ÿõ§†øïz×-ϨzjùÕGxNS9€~vȉ$A]a?P(øÚª%q†üñž6ä>qø§§3FؽËpì �‰}žö…¨xáæñi �ï‹`€ª;š<|ûS�4 ¿'ì�ö‰çñÖ¾öÛOROšõ߯}ôú ª‚ûÇ.“+«q]8ƒÂzoÏåŽ}hÛæÅó‚s‚DF�C¤¾éDö�ß /[˜ô4êÝéæ9M\Œ<zCBí‚Î[±»<'–6óS¼¼ŽJ¨ë f‹-8Äk•{Ú[ÊßòF*Iâ6;Ü9b+…Œ{ùêlŸ¾}{pVÕœÁUFOcÀnúçê©�v‡ow¼ý™ÐÃÙáÉÛ91ÿ³’ë!Zj9•Dé¤m¥Â%äwSýQÓ“9ÿ» …ÀaË©Šàûs_ŒŽ6?y(øÇ#îX_Y«sémùX÷+�A·@„Ï÷³þ¨•µ§G(±ìR›z Ô5�LÛÉøÀ�Lé*h_vÓõA¤Ó‡�fê{�NY„�ZDÃq¦E¶©À¸üïó¨ Fã ™W0]=â�6MíG#gqmÞÖkm}zÓ9M¿qô×:Y“,Ié²Ú«à"™i!SáúK<Ž¿ãjÀGM£õ8uƒ£]V¸sžæFŠmšÑßñà¾ÚQÏ–9ñ8q0 ¶wÊòöƒ£Ñ)¦ÄÖûì´0ðÜ,nÛ€Ü{.*w�džÃ×k#þ2-dR9±ì<Ìw^€Ýã7Jñ(N‹Ìò�ð«O³îq Ù=:aØÃÙQnŽ>³XÊjhò]­1‹ŸrÎÂç¡ÿx+v 55™G�C³@›xœÝ\moܸþ®_ÁúC}×nb\€Šœc —º¸‰“ÆnÝ è®ÄÝ%"‰‘òzóë;3|õ²Úu¥~¸‡KN©y†Ï¼�{—øë‡7wŸ?^³�)ò«äÒÿ!xÂp–nx¥…yuV›Õ³¿œ]Às#M.®>‰u%´–ª¼«„`ÛJmEe¤Ð—V ¹¼pŠ–*Û£ÚŸ®´áFj#Sý’�(Éä’³’âÕY[îù¿Î®./8ª{qÕ}#_À›-Ks®õ«³ÍÏl¥áŸgðŸÆ¥*ë.ü¥ŸxÏ.—W a êIîòb‹º®ØÇJd25ªb7<±Ï3ùàg)ô³ŸÏ¬èkVÖ…¨dÊ n*ùÈ6*Ïd¹ffƒ_ë¡GL”³‡EJcDÆTùyÊ'¡·ªÔâ)Žâp~„.}'¢*rQˆÒ0˜Ð"½b—`RôPücáÍš�U¬€M+¿�'/é{GìIß;ÆmzS�ú°…;‚“íÝÓ×·>ˆ,ÁZðåÑGë#0l•–F>&K#Ö¢²._ªÜ0 #˾5€d]+ˆYÇ]0­ÀËÉë žç ÔlxٙŶ†e”l·%lØè ˜8[!Á2åzħbø fû‡Ùað¼ÄdÔE›mTk¤p|4&ºó8Z¹ZË”çOØ4}âM,ñ�QÉäyU ZE Öîº,Â`kQlÍ~‘0ûíÿþ�ûªáƸ=)@í¨„€På3 ¼˜Ãd…àäOÜ$ÖYvHȘn+2˜ì5)�ùÜ#«è²q |ÂNž¸Ù›¯�%ÐL^ÅNÈõ†¾æ›¨Ô/èi0™EÎ÷°ê‚Hå£àøx/¼?ì~÷“ýî~Èá"þp&8FÃí¼Ê© ¿ƒ—8´)X£ûAò$K¢‡‹}£^…sC ® PÅt]à Ì:Þ†kKô´J¿k‡ó{«åÜ}oÐ`BøE¯W5âÓ’ƒqÐB*toGØ¿-7ήºîÇýýî“�c@§"Ï•ŠïQæ¼<5€¢E\7 ƒV!|�K¨UÒŠËsгÛàøI#IM§é–²aëzãþñ¶=fÚŽ1ñ£}FìuÍ`µ7·ª0:oqÐAñÉv< µoз%¼l6mÆEd{İÈÙ #ùrÁ)†lFuÆÈîÑû76�–±°�ÎÆŒ=_ê9òÇëÇ-dø";•H†å'uHm)/‘ ~ØDB±¬žÉÕJ»`Éå3ŠîŠ,¥SH&,ø°³`„`KAY€_1ùBç|i‹j-ÌdŠõ i#J¦Ài°†Ëf,6ؘ�¢PÅóßCêóµæù [õWY^gë1Ô½ÄdœE}dáø²6žn ½aԣ݃n·±V—–t·À¦4†BæpB*¢ V\º¢ J˜,¶6áui&á ÿx¼p¢ðz�ÏGðàB€ �ÀÖœŒ^Oß@o&¼ yfÞU]4G€Ô�ø§¦®0¥ª �µmê=ƒÍ)ª€pK€-ÕK¿[o·¹4o* €¡°/ßË–ðôŸ¿|åŸz]ïù#©Óñ›òêNÝr»«ITkºS,©Ë®8=;´Ú¿_ß)(O»znnëªRkàœîÜüÂ]çáë|=ìqÚ-h©°”0•y²Ã�}~·ßúiŸ�š<3ø‘DcKá“æB‹ó° Â)6õã¨úöži}–ÚSÉ Áp_‘»à3^¦‚üÈAµ…-i¢îÛ°{x ¶@lW–¯1ÞÚ±d?·ò›X°ìO̹>ø16 �Cl%S!¨¿ìÐRJ®¿Øúwúø @¬¶~Ÿ~€­UÈoTAÚYçf„PŽ�L1'Ì0ØæÃWÈòœmZˆ:¼ŠQöùíð8GðGø=™�àoê⻯£Ý<’˜£�ç�õïJ�ü%ðÄ&žÕØ[§°——¸O7öeBÛ$'*…=¼Bèü´Xb«Í&ÊÆñ©Mžm“*kÚI>þ¦�"HÍrqݯ‡úBâ¾Á’ndžÁP ¹Éu�S5Rú8‰éÕNPÔÇ’Þ9»ûV4þ× /Ežs³9 þòøâòBWþÄ!ªer±2áÔ ¢Æ™ÓO›Ð¼ÔRxW*ØüB'øàò·Yj˧#ÍÇúS›"NÍ`R„nø»·³×¸R쫲M,(?�¡‚1²æ \ôVÿJÛQ?lÂXj²!ÛÊúæ¼§à[&hñÆÿЯîö›Qµ‚#ßÔ˜åØlcÅsI£ÏÑ/©MHËF¸"’šÎ-eã]¦¦°mL”ÖsÇ£êZyàר-Gbí‹ø¨6±}¾ÞZ(ªàR\ 1ò9uKº³AT1HÿóCöÊ�ÇÓp#ÑYÁsÛZâq[läpY´7ÌÂ�».l«×pJ ìÉ2Qþܘ(9Æö^b¬¢�p[H‚äÒ3sS’X^JU��Á�IDwÖ\–Ú¼Ä{°5v—(P‡3(«RâcMzÜ¡h¾ñ²ñ\a®´“:Šâ´íZ­0˜�î}åÊošÚqœ±…ÙV«ÓQ¶zú ãsâ40øðÉ0!DÏ{® LUidY«º9P “ÄaÏE+ÜÌ<’En€Œ@ -€óªX/1)s"Ÿ '¶·E-~Mú‡ 3ïeßÀ«…ZbsàkØÚá­=”öÈ[8¦æÓ‘s´€÷pN†Ï —„ÓÝ�¹b+!�_ÐfÆëMtLM›Úa9_³2:‚¡NÑIG@$9çé�SxM´¡�M«SËÜ>š0†üχ褲;å‹U•aµßR{”îNDÒ>+r1B¶Î“æèRÀбI5Ò¥pÓ»AÑ©·ŒžDÈCqþÜAu[îÁ ˆâ“–þ µ‹¬[ÒF7:±]+ι¼¡d9Ä^=!¢XcOù–"Ç{'Æ][±åß+ýµV¿ö`“М‚°.JQ�WQÆŒW:í ?n&ðß >ÒÖð³€oõÁÇçÍ øTÌs-{'@Â+BŽ’è^߃¨¢+áb¬Å»ˆ ;¶$¢l¿Ù¹FÓÖ;:|$¶G?²d¯ ê€áŒt£³šåâõºªÆjÍ 2 ‚NÓÁ/9öòJýræ6Žn¤™±u—‚/UmBÙö�åÖ&(ýX©%_J æý8‘à,H´ô Ý®¯é*$šÙl~DŒ2@Vîò›ßXcÛ(O×-É*¼{WºMu—�¦˜½½5ó}’úË8h(1 ZVÑÀ™<Ÿ£`´(z2i6ÏrïÂÚ°“ FþlÌÖ§m=¡aƒUM%—µ±äF¸Â¢lÚ5JtêøNê±¶¯™á:\Ð4´Ÿê2\·Ÿ±¨þ‹’þp…Ii¬¿Y="LZ|­žƒrã!2î0Ôî$ÞÖµg’¦¦ó_În<;/5|Þù†O]Áöµ÷_ ïnSÿÓ‰Sš�€ôs:+ŽGÓE™0etÏ×Ý«-Ýi�+eÉü¡ˆ-UxLJ×xs œ¦Ò¥ÌÛ‹ª±ÏÎ(A�«�,ð8i.|�o7üˆC’Ì<éT =à±kD§žÎ=·±Ó>ÉOï~·<Óëö_$îp½ÒXïÃÓrÿ¶™JÖ%ªÀssËïh‘rôCwüA¯®—Ö]1¢àoxðx¢í&rÓÆIdúõMt‘�lúC<ÕŸ 'ûõÖsY)Mo&¥jáÂljý�aùÉ.uHíh‡ÄA®ýXWc�zÔmGØžö=~ou,f3øßr¹=ÍÔ(9«‘­ÂáÞ2h—ëmñØøHþ':«•�·ð¿ �·Ê»’³Úw¬yþXö„3ÛAñy}xì·¹øþ;·vø�×Z«TR‰x‚Õ‡†ÍgýaíQÀ;QP_Öùy3ê÷‰Žÿ¥Ê”’©âø/d‚èl?“‰4�Éøldϵf£éNøå̪.é"í�ÞÍì+ݨ{%˜Õþzy%°xòÅ45ƒò äÁX#ó/ôÃ@:‘ âPµ‡SEr$¼2�bwA(+^ç&ºµzŽ-æóÐ1iá}áþ'öÿAð_Ö�5\ªúV’æñ�9À¤+Ym™Yiý¦.å­{vp¿V0áðÑÝpS–üÜ�þ[ ¥×ÓŸ™ùü³Ž ¾[* t…‚±–Ú·r ` ’_ÑÙ+#6 Ttöfcpü¹�Y(QºBˆ 1“ùG$�Á�[jì¡uGž¿×?©{è½6Vï{®6Dë˜(¨ª`M¢ïҔŀ �0ß"@Qd´TíML ”¦£Z](FMC S�?Ü;BÆc™Õžh‚ ”�U;(ö=#UÏ‹ÃðÖ h¯¥WòN|ž@=ÏÄŠˆJÞ O2@K•+Ô´+ØÐDPÓ#šx”™ ‘/uYÀrD•˜¤ÍH«à=DõîÕý<2l®@–Aé£EÀ£ç¦lW€JÔö‘35¬¶:LÏÃ2:$¢Y'¢ 77«u©ît³sÙV¼Uÿh¢hœ¾GþZZzmjüLÁùB?úuÑ««r½”©¡ ÅÅ®YV—m…ûÃÒdîY(6rûÂ-F/ýì�W´3önÛ†L5®ã¡?Ï×µ*ꕇ¾²�.ŒGS•[‘û%ÀÎdž�à[ʱÆé¿[Y€¢óWumê<fe´`[ð�d�š'cQaÍkÿÑJô3.ŠÐˆ9ŠS%HŸæÈÇd§IƒcšßÔ0ͼ@ÑÍH-±ðÏÛi3 ýêÉFžÒǵ(r÷I\OÆ" ˂Ӱ ŒÛè‡ÀûÇvX·÷ž„2�_‘,×kàÂvðŒÑ N9�ó¶b*€}(Jå”éî©À-QGŒÜ�O‡Í¿Sz±<ÊýC°€0 ãOÓb@EY?DˆmÙ.o€–Cº¸+ôi=¯{Ïæ;¯i>*µfýD™—� ÃO®À‚>é–TiÛЦ´ †6™ƒv"lB{¶±¹Ãl@¢”xsuóúê�!*æ#'½ €ÿ€tšq ñ†©Ó÷@YZó\\^¸G™—R,k5yöF‚•¬Ô�`dkTóç˜ü°ÏuC7ÿfÞ&"žy�'×sˆËŸ†£¹»ÏϦ»Ïv׆÷™ÑÕÙ4üyÌ\‡Yží~p‘"vFؾôc˜¼/ï<6á¸ýJ7qÞ),^!wáòß_?<ãõ·ÞÞ>÷cùÁÿ@ã%;¹-ç"±eðÙ Í1 :©âˆÌͽy¯e‘¸ë�a£.Á�œ‚d ܦê³8 …“PÔ ;R 28ÆvG  ‘Îu¿?{Ïvt^O ;Õe! ó¾¢´±–ðɶƫ)pˆ,ºÔ ¹Iq*,ÝÜSÔè±KN.MB¦Öø›µ%&Tá Sçlöì‰Q;%6sèÀoö>LõÕpÖ›‰cºÙ }×@yüq‹ñêmð)*¥€j¸$»ö…ÿaƆ“…Ò…Ž��å°G!ž FžlÜcØ<ó/Týìá(A <Ø—ÆrÄ@qB °NàÆŽA¾sƽYik}ÜÐßayÛX‡<Û˜õšODG¥÷䙳KŒù"ྠåo¢4×L±cæ‘@út ·z¢»Ï.@SÅ„Ë!ÔÙÆ8gjzº“Tt¿ÀÓ×ãý°¹©âÁÑ?¤à)¾8¡žô,Uï´Ì®rHÝdÜï¾Fþï|7õú_~=(®¯–s$4o1-׃æížRÎ ˆÒNpéD3���)å<‹L@»�?sDûCP"¼PÅÞÒÇ=KÂ6ÏœGå4�ýØôMá#dÀ�y8Ñ…oÇ~¤ç1U¸ÆLLIÅÄ�ìM†9QÎË'ûDÙã]\íÐ4ÎÖd�Pw¹Bõ¿ú6##äг åþã¤:Wo0óÍbžâ<ó„¾`×´c©Ìñæ–Ù•piiçÏñf—�x&‹œ_–ùZ±i·Á°Û(ª3u6Z…ѧ·hM®y5„lxïK²s¦­]ÊuçO‘Þ±*Àdj½Ð• ê§o�Ó™éÊ?v˜ä<(–>Hç‘Êy„-ˆÏ‡÷p"0Ò«@”‡G7`гeë®ßelë‚÷úSñ'@…ãlˆÒ�‹)ž˜×f…nä¬�6e <˜{�Y(í_ó„Cq"{�ÞaZ,Ï>%ì‚WžMAÄDÌÚ†‹(( ó€K¢ŸTÀ~8³é.lî»§9Òá‚¿ÛÙ»ûnÆ£N�ÐûWAÓUGÝ=ÌPNO/dvšÔ)Q.íœtõ2†oo„¢m¯w�¿Ï¿÷sÿ Òç�Èd¯ºöaÈýûh�Ÿ¡cöÐV�ìÕît™œàÞpoäÀ„7W®ÊR&¡¾ænébøêiT‡ïÆY@@øPRÏ4:7Ü›?kü0�6/¼º€EXÞ¢iÜGRºË`ž?y‰zxiÇÊ[µâ.¼}²†œšõ‰Ø%Îu©ª}�éT¼‡%ÀÉè}‹�53U qþ–°‡°]ë�Ë{Ó‹dÖÉݹàðJ|“7¹ûŒ%.>é ´°€OÓd®U®ç:º‘•‚’\ÆÂOßÝ—ß%|2tüt忘¶]Ó&ñwã>´D[Ü‘HU¤Æñ¿ßs/©—-O=q�dlQ——ç¾�äÕ?^ÿôêÝïw߉ÂWå:YuJæø¨”—"+d㔿M[¿½ù2]bÝk_ªu©+µZ†ïÉj÷ll~ Ÿ®�—^;¯3÷•²XLVRY©Û”–Òõj)Iú³¨ _’U-²R:w›/ÅÖáß .=d3›«‰ÚÕ&îÛ@ß]-kÒ°?ÚL–B¯šº�›R‰ å�ÜKû…z_—Ò@›58Å6BŠ­ö^åAðy”åú¡T¹›—iÐÿ�pÂùüUØÍÿTæƒ^ �Ö(�Ãs�Ñbs`ýÚìp’ÓUÝAZ$X(µQ²á= Ų,í^ä*ÓŽ¶ûF©…° B§š‡ˆë³÷·(³½lrÆD�8J\ÞÅÄy}°½¼Wî$h8¹ƒÌŠ¡ ÓP^—´•jÐ*éý^Ø-'uf˶2¢VÈßX½¶Û0R…­!K„A—‚ÛÌaÜ;ë6Ô|¼÷ ‰`«ŸàÑ–^K/{ßrõˆì±Oû¸!_~íJ³ËKj°”ÁnÀœÚ`,kê¾°NEù˜6X~´MŽt -Á#ô‰iÃÉ.;?‘h8–ê…ô5ƒ“àBtNº¢*£#¤x�e«ÐˆÊ<´Â�€› Y–S´<™–I)¯s‚ÇþÉÅxT8ÑŠ˜øc08T#5Ä7‰‚„{䨩ÊmìdƒÉT_”l°ÅŒ¼BÍ3a|@røÉ-{“ÎB@€ÚLQ¦Ê8¤„˜êd?brÀ‘{4ÉuÙ�L» ø½‚õ,:Jþ.8´ÂEÞS2+8»ûÜ}w¨UŸ¬ÜÑ’M=¶yÊ.Þ| ‰�˧Ç�:òùD.9Yã#í}Ý•á$¦ƒÇ‡†Ó¨‰Sô͈Âå9&‹N–}¦·:ã¤éä‘u?x®îPÛÆVGì)Tuš?[MñˆëhÚ›ÈC�y†z":íâ¡È\¦é?LN‚ÿUgñ®´Yžá‘¢¶²-At¶ÔxESÚmZ¶#†Öö¨´縬¿î—d@_,¬‰†zØeÞ¶Õ["kÈb׉•-+åü1µZàx�¡qÏÐDº7vo.ôñÜÑûøß`••4 cmã’Gìöv!>}ùâ� èÛËQÎNʧ#=fX” ¨ úX�Ìð‹‘ýt¨uÔñ�GÛà¤"€<0ÍaÁ�¹�)Ô’Q�oº]׿»Ôp÷�)’å]?ŸlÊ»élÝÓHƃ€g»[Äö��óóîȆ¨; †I`02»—;:ެk<± µ¬)ÌìŽÊ¢ŒHv–»�½‡òîòj8%lO2~”\3Òñ/ŠEzí�Ò?Tµm¼4þÔç1†‰ïi¾¥¾¿×äÙ Sª­Ÿåß—Ö#8ž^`Sî™L]ôøÑjŒoÉ㙾€¬&VßqfØšÝy„]ñÀ÷¨Ä™BcÌ$ÕžÝ ¤E7ní�ß„ D 4ýóì¿NÝɆ¨ÖÐݱ`K+eȾ~n[âÁ£'`Ämg,¼4Ö�±bÍný³RlÆÒ2.% zRPt›ºBâ„Ãsz_óQ¼H×ñ ½SYˆ±øt $Ï?Rqæ}µl¼–åkU+“+äTº>Y:ÕS.~ÔU-¦ë£…ðš‡� q¥W>¯,d¼‹¯}ŠÏ×ý!Ä–ÿ¨‹‡0�‚çCHi·@ú°íóŽö‰“ùQEÑâóoÁ¤ï7ö}<¹ ê'yÏe=î,}EžV×å ÆÄ&]�¯æ œa2{ÕQf2×à™mÿézvùœ“²AxÔ®AEÄ}ç½"ýú{Í¡¿¥ÄàæMœr‡î�Ãè}‘·×= Ÿt=|¿Ìß“”óƒ ]ϯÏÁ:3¦#€ÜÕ�ç+ñN˜#�¬<ŠñL-]ƒèÎ_b™Í©sóòA]å­ïYMºŸO(¨þ=ih5c¼WùŠÍŒ]xtqÎO·DŸ„—ßÇ�D× š-€ ³ÿˆýÎ&~O~®Ùͬ.¤ÓëGC;ðÉi�åɃåßÛãÉÒ£0'~» cO OøD¥|aóŽL¼ WOccÁÄð3VøèÔ�oéG z©rá¯QôÊp†®_ˆI•ÊzŠäû¹bÝÐÃäÈ› .-Ðß9wXo³O€8y@\7¶= 홇ƒø3ã2ü`ù+ûèþ§-�DÎ^X8xœå<û“Û¶™¿ó¯@÷¦Mz#ïÆž¶—±×šÙn�6SÛñÅ{N2�› DBjŠP rµÊ_ßï€Ei)í¦qïfšzEâñá{¿ÀË_ýé›ë›Þ½ËfUN³Kÿ�’ü³R�ùRÖV5/ÏÚfþä˳ xÞè¦TÓoÕ¢VÖjSÝÔJ‰7ªYšÂ^^ðÛìò­23Å×|:µ�l´mtnŸ‹}³aXv)E%WêåY:è<ýy6½¼�¸ð³éÁq°æ3¶y)­}y¶ü½˜[˜œ›¢ˆçâòVÖS3ûûåþ!^ŠËÙ´¿Ül*>§qßó¨ Ïú�ýöò‚V¾¼Xÿ<»ÞÌÊdßo•]›Êª²~ Xû… ˜›zÕ–òÚ}îáOàgóÐý³Te©×/ÜÎU@¹{p+Ëv˜SñçÚl„3]Éz+ •k\[4(€@Q‡ýÎ3žTè[Š•}òû3^‡O°qO¦F0©XD@"L•‰f©Ä¥Z…Me³<›¾½{wy¡VSQµ+Uë\ÀãZ߉xaæb]«Bç œ¤� :dUàzÙЂOÓkÇÄ ”Z¡VM[WVèFH5Côóçt¨¼ñØ�ª¨ ¸’•%Œ™uKY1S¥Ù Ю‘³R=q‡Ô· éHÏ’‚‡YÀíZî×ÌÔ…ªÃO»¢¹¨[ÓÅ×µ^W{75ü·]4Ë—gÏ~÷ë3>§@I/ã1ÿÆ|èÎɃ.þ‹u9-]¤K2>»–�Z ¥,ßy.°ŸhŠx"íwx cÅfi€ü„g› Y+Qª[UÚ R]W0LñßR”f�ûˆ[E¼¶ÑpS)¡JµRU#֪κ¥'ÆÀ<¨,KÖy&®"ž]É-�Lpð©ª�¸²ÂÝÕ�*ÜîheÞ¨ÚK�™g´ýZÌÄù‚Ñ‚)ÉY-àD5¬� ˆx ž¾lçε .–/áà•€#€vqr|"N…º(ÊíÊ\òt•—-€pÎøFÒ"Ùy÷¶]y©ÃÔº•©*µ ~‹;Á$¨­UŽüYdðÈÁûþ¯|8÷AÕÖãZ$x¡æ²-¡­xú<»Štž N·¬ƒ˜øb¶Å_p­3^KL€[t¾s  àL íº%±Y‰n‘ð(˜Ð°É+…›÷¸àIx¡#Èz`e ³ZVwË1ܤw‘ñP—ìâÙªìd̈é�ÌÌã 6 á6�'�GžRcxœÅXasã¶ýÎ_�ºÓÜe*K:·×dbY3¾\2í$—¸=OÓ~I$—"b�`Ð:¥ÓÿÞ‡IQ¢|±¯i;sg‘Äb±Ø}ow�Õ¯^ûùíßo¾¥¯ô:Yõ?$süTä¥ÈJiù«³Ö矞-ðÝ+¯i­UEâ ùÒänµˆß’Õ¢››š|4½X;/½r^eî3q8ƒÉJŠZVtu†æáÏÙzµ�aêÅzøÙ |jD¦¥sWgåKQ8f&�†|&V÷Ò®MúÃjÄ•X¥ë85]‹ç<ú&×qôãÕ‚g®Í/¨u¥ÿö_X£hë§®�çxõOY룒´VÍe·JÌÁ²÷R·tréµxÕ*� )´É¤ªöd ¥šDI}.7µ 8ô®ÑRÕdçIœž«ûÞöÊ�¿<‹oKÒnÚŠjïðD¸q$rr™U)å¢0ß(Î� žaRGö�3µKxÔ©ªé� ŽQÞc²¹'ËZs+·µ(Éҥ؆ŸDD—=ûsKvwc°™gÝv…¬ó~ô›¶úSÕëeío,å*óÆº½$T§Æ—‰Ø¨{ªÙÞyÍÆK2S5m°EzaêŒfòd· »T>Hh*¼ð¦·¦wê{×ÁO+ç­©7ë·igåE÷)+-ÁÁYÃ.Ûc©T‘¡{é9Ž3ælÂ#g5ÚôÓÂcä5›½@È�ÉÏkrŸû êóÌ�?Ã`ä‡À|®OFô—äÚ`ÖBµ/�� °¥5-°ij¼;Ñ„í½Ÿa'}`´X³¥Ñ¹ª7üΉ¤izׯ XÆ,·Èü"ݱia?nù@vVæf�AÉÏ»Ò[QÉz·_Í�bs2йFø’¶©[‡oÌfä¤A…�yîD –aW½)ìÜ“†D";¹sP¥²R@;�p)ïÿÜ’k�t@öàÞ蓱@tÓ9¨©ßRcs(í_]ÅsC=TÞXUI»¼Å˜¢r_^�]|ú| ûf€ªqy D޾}²ìçýu�´(´ðñ)r^.?\.æ¿×¨êYëÙ€OŸÈÆe¾ñ²Dhk’¶&‡˜ë³ÕФkáõ;ŽbA!€I@)á%æÊRS É›ÚL«œdÝÛ7c9²Õ~Ö‹¹[à%ŸJŠÜ MÝP%K‰„nRå&c™ò»Õðnª±Rõ�Ùº;5�TRºÃŸûéˆq`ÑTWf¬%Í%åT‘r ]íMïJœp�;QÓ=ix<:±›¶1&—Zÿîß�I� )ò^¡zf¡ªµµO¢^QXBhëL!pÅe=\qÇ•šM>ÂòŠÙP(ˆÐü€]Áä0í¡áF#\Ô¦ÊÜÁÓ}ôXÒ O(‡’:䥥EP¡ ÎrÉ~êñrUC¾•¾<[¿ ñü…ø­x�tNÕ:(aÃ'‚qÑ@÷ÔÚÚÍq-ÐfÜ5”pR L ¥YãŽêY—�Þ\ß~}ýJä†8Ç] ‡Ž %m¶óÈÃÀì÷1ú+'õwaàƒHípVht¬[R›Ò‹ါäVî tä]Ö˜¦?´Î ™brÁ‹®�ë 6ºœòò‘xËÅáM¨ ·»æé™©S£UHOØON�ß[Öx¤…¯ÐßMöƒ2ç_˜éð•ïU°¯£½ÉC�®7'ùbùý?Ïÿ¯ˆ6Tín{�ÜÆçfš¡n&_Þâ(ðôMÞÀJ¶:¶ï=`ð‚69žYY¡|÷éª 2= ÷"›Yp!zÏáˆÑ|”ÓÄĵhâ;ô4âùùrþR<ÏÁëÀííÇß_DfÏ"Î'“òžØn¿%¦£k\ÿ8}O߯éÁ#wLÓè³�J!]ÃÄ.A?g|Ÿubá o£’Zì�=É££Niœ*¹…ZŒ8³H=l2%¿¥pÚšèàä õ�N2×£N­o=cy”.œ/ëG½oLØ™Ñmía#$‘c bózêã ó‘…sÌ%šRôxa1ÔF–‡6ÆÖ5�ub‚‰Kd†ŠB¡¼qß{`¹AÃëü$°ýrQÇhß';œJ½œàåèˆÙSC$:„Ã&¸wm¯ìê6™:¸ÒîL QäâÀ‹´³¡Ù´i«3Ýæ”w]µ„¸Üt¤eBtÍ{,¢Ü§šº}ÌQøÆ ‡¡RØVóé¸/sxÈøŽ /ƒul£û3 ÌÛ6‹]Zodö¶¥Æš¼ÍÈq0d×br.½<>ª'�\äk4ª&ømÝ£Tê°öƒÚ ^8öF¿µËã¢=ìæT(SÚ !D4¸Iâ€ä3èZF†èŒðè:³û,îNxëpcÛ„b;ÉËùrùÎIðHðC¡ÞùÐA ùÿöâáiWQúð!NûùK„t%§—xô/ÆÿGê^‹ÈòÄŸ¢è•Z̾÷ó¯A,«~2µ—Z¤Hžá¯úïø8ß�/ÕF™-´Ï§Sow·O.½ÞŒbI =Ò Ù!Ò‘O»Ç´ÏÂYVî350êÔOÊÛØ—±¦…Cÿ¦í¯îþßWa‡x\t7ß‹x—þoèPÑ>Š�F”:#xœµXÛnã6}×W°~É‹cX Hmvƒ.°í¦]M)il±¡H•¤ìèï;CÝeÅ”-`À¶f8ž9s¡Ö?|üz¿ýûñK\*7ÁºþãW ޳(áÆ‚»[änwýãb…Ï�p6R¤À2£30N€]¯ÊÇÁzU-u\�±wë¸Ö‰ÈÞ²“e(Öœ)žÂÝ‚¤7¿H=‡úå7ƒ\lÖ+NfÞoN…¸ú=Ê2IníÝ"ùÀv?×ø×áÊ•ü²ÛzË‚­ÃÍÀFˆ[xÍõ*#k¶M€¥$d!µgð’I.ÄA©‹C½ej¯?,ÚU;áÄåâ%Ó†9z˜«È ­XÂU,aéÖ6 ;rËÂ\HÇ´º ÐŒ°­»øÛ ž×ZÉâ¦Ú~…ûAûÈÿ¢#.…+†˜ue3!ë›8Eì¯ øséЂ9p:¯eGz~Tlgtz¾r뫽Ô!—WÕìØŽ‘BF„¹G’0«°vÚ‹�‰–¸û.¨ =U–µaIîì 7bI�Ø'¡Î�õæèÑ¿9˜‚eZ(wÃfFèž;Øk#ЋG±ˆœ6vªQ¥™1{ÅÖ8Ý£V�ÔÚù.jåqjWR<,°—9Š9b-áÒ.¤úçñ÷࢈ÄJ+˜�í¶È`%=›‰\¹tŒåÜùs–Å�+‹Ç£ —L¡¶�½k‘¿ ±BTB¿Jì0®£0‚H0 ’§!O3Áxz�2½L÷ЉÞϧ³¨üL‘Lå) ïXÊ1½_ˆIÈfô‘¡ ÝMïØË"-óT‘8h7‰;*Õ^·A¾2ÇêRžž´€ ˆƒÚQòFé’^û—¸ô›a5Š©u”¬'g,:‰  uá0ˆ^àMbN�­jZ„ÚÏ�9O¬ÊÕ rçÂýU¨G*Pø·’™è¸È„‰½òkâ& «¯>èMPëj‘+'0ߺuØg‚Øìªñ{ž~N3mWîõ’<®5Ë׌�âú+¢“rUtk*QÁŠ4“u¢dãöiµÓæ,àŸÎàØÀø˜¸k�sË-Ä˪OòN¢1Ç–˜Ÿ`àÞ‚ý·Bá�p,¤ábõž|>Þ3g�~et¹Ðçœþìѿǹ/z´€÷¸%š:®pž\u6�’e5ïq£óªªuÙ†ü)«¬Î±Šî‡ü­G¶«û‹d?`gá4¨ü†—Og‚[/ž< €M‡T=Ü/ã 6C=8é†;JEÖÎ�KUSÉCô€ÊjÕÃ`E}Ȩäú¾…ƒN=»5µþ·½<ð½hv¾yb�^è:¢¹\ïZgz�Ø«Ç zab)}ø³ý v:nW�ûÞ(×W NLl ÀÉ8Ø7ÑQY6I©Èõ€‰º�t—Dš*Žt™!ý°`ä‡ÂM닎?vÐ\k<¡ZªÐí¨fðÒî’ÅæÝz鯓Ð.$+j€ îÊ-Bý91tݱä‹0] ¨ïˆ(!~1¥hæzëÂXª}ËDáà{.õvÁ¶— <Ôgßàü\jó¸=a„ø~óÃÄáaÛ¿‹�Î �.]ò„©¯yKü�Ÿq±~j31üÏ3Zý®f¼Œ÷¥o|4¡¬¹]§G%žZGÞ\DÇÑ8Qx{A�€I�X3i/eî¨ÇÒ£ÉÉŸx´�~?µ/‡ ë€fÒ%Õˆ*J³†ÔUõÚrU¾ ýÂü)² ­Œ*�I˜X°xœ¥XMo7½ï¯`}ÉE¶š ŠDà¦)šCš1÷HíŽ$ÖÜå†äJÖ¿Ï’û!{e¹DÞr8|ïÍp¸³Ÿ~ÿüþæß/ÄÚ—zžÍÚ’~JòRäkiùw�_^þz1Å{¯¼¦¹[ËZÓNÔÖÔd½"7›FK6›& SìØß˹óÒ+çUîÞˆ±™’ͤ¨dIï.Ò€«ß´Ìïæþ“)H_ÌgSÉÎ^ÍGíðñ æZäZ:÷îbýZ,þ]âÑcrŽAíÌ7íÚ;1[̸Y`¡0x6­Ùá\ܬI”l RÕJÐ}­¥ª¨Èâ�BmÚUKwùú¢ŸµTÞS'O„±Âó˦ʽ2•X˪Ð4 /[ŸVl¥‹Fi/Lu•Á�r}¸øÛÛKSéÝUZ~ŠõGлAìöt”n!cŽìFòŽœ�²r…-üñ$<×¼-ª¦$«rQJoÕ½0Kìš„5[�•‡¾À ¶Ü覬؜!@*Tî��R€ÑŠ•ÚP%Lo«µ�Ë;¼YZS WB|ô &Þdb»6šxéíZú€ð2ÝKc +ï„“e­A>f>d${L ö÷ ÑPXq -¿�Ab‰èI;!ÓƸÈZHxß•ñÁF¿Á´�´Šÿ½`MjÚ�v¼%8@¼+�Ø‚!¸t“à+­À“0úŽjlØ¥Pÿ£ÜÖß ÙÝ£*ïF$6°ž.¶='Ï�Ýó2òY’ûÎK‹š×†¨Ê5•W:²hq�«@㈠ž�¢måù#”‰'*\p~‰ký<†ôK¿ËÅA©Øf|9Å-z–\ RBFI<­ðÂqGש ¦Ô…Rµ\ðk ΢GµT„ŒfV�‚d¾>BË€•¬£å:y¾#ª]LŸ]¢òÃ(ûMsæÇŠ�…”˜p!}ìn’YUÌTL¹s˜ÿG(O–Ó¹îŒçM2£‚膞&ó:Ö›‡2(ºmõ Âx«¨>U&b˜_ZKЧ+C1Æ5ü‡ŸÛALýP>Y¯ùø e—ðÃqvµ‘ƒ »š„�P—žO  Eï½Ü@æÖ™kðß^uuN�½BÐ$OIò`¡99èåÉêzá„§ÃÚuºŽF�hŽ0TÈ4v_a,ž�À�<ž%9Tó‚OqÎÀcBxÚ—^XŸ;޾Xu…:¥{]ÔÈøÆ‡S;  ÒH]�‚Nö眓î+/ÇQ;BZg;�ª�‹q‚R[Ö’ÀÙt¬ŠGLSOôø`èÐ>ÜþM€ p¹ÔÒ>Jæ'r¹kô¢~BŸBËÙ›¾Š„ÓfÈ¥kÐñ™Øâ´Q±�Ä0ôE$ÚÖP¹sþD~mÆŽðh8' ãüÄꕱr9¬§¡ÇÌMY7|¼òIv$çø’'q[±!NQ#z¡XY(^ ”Ô—è€IÚm‰Þv=n ³ëǹ±Ý7àXŸiØÝXTäíZ¡*á¨( l[ô,Í…ICoÄâ-Ñ‹ýâ \u±�u”EC%'}™Î2,QŽ­¯I.Åb~ßfV½#[‘®›ÅA˜pÿHW½è�B«)=¥6X [|[“z8²”‹¦XaŒÔÚl¹‡ù@¸;—ÁÑ€gÄÚ¡Œ �Ï„ûÞàÀqÉ&O¸>¼ nÒžrSj|Cab·ù(¾Î(ÝóH;ñJtÝ!¢HZIÈp�JÓ=d¦w(Þ[-xµÀ¯AkT‘IHÖ"‚‹Mêã"Œ¡;}ùó«_ÎÉÛ¿šòkô6’»½ñôüúxœÃ¢§Ú¡ÛÜÁ„>zµæÂ Qgù� x˧¼ïãéie©q{ÄmÙÖ+•ã7Y"^DW£ù;TöY=Î{蛫NŒ®ccetÜép7^oó~ð0}�±¡�-9¹Ç‰Ïß\Ç3çJ¼È·wœ”Ž>$›+Tij.üƒŠÏý©ý1Æ6v94öŒËÉa—ã€[.‚€-ú¼m%ëךÊÿøâäŒåJ½<J‰yó`‰¾ÃŒçÛÏ~qD& öánLÜ\—V…Šn +·Ü ëCî²§È›¦¨Óøaöt¿’O�H¥"Ôxœ½YïoÛ¸þ®¿‚óaë sâ6·ÃWÇ@ÚÞ¶—6C²]÷eZ¢#¢’è’Tﯿç}IÊ’-wi h‰"_¾?Ÿ÷!=ÿÝ›÷¯oþ}õ“(}]-²yú£d�?µòR䥴NùóIëW'?Lf÷ÚWjáJ¹®Ôv> ¯Ù|—-M±%!/ÎK¯�×¹ûQtÓ1žÍ¥hd­Î'qt²˜Ï$­9Û‰Ås6_‹¼’Î�OÊïÅÊáß ^=¦ç¦PùóånéâxÂ|¶&! q>‰;Yµ rŒR¬´÷ª5fVBza%ð¡6V‰O­²[±6ºñî4 r }—Ô©ÝÉ÷“ úb(\8¹u¢4Q·yÉ2×V:÷�›Æ[½li[o„/•(Ô�†¦É„YA©8beMÍsä�²òVõ¾� qƒÑ^ØÓ8®­Ã0«ÛA¨{™ûj;›RC7íXúÚšµ²~Ë/¹©×­ÓµËIJՕ'Q•Z'GÀê€ù�´ u¿®¤n”�ÏèUœ§X‹oùûeQ…/ŒQa¡…’4Šk9$P Fe(@8‚Õ4K§ì«é„öb#]ZÈι`C`m‘úN4ÇT8CKêÖy±TâVß©†„ÖBxÃa»)4,h6O•P­mBdJ|¨öyg|©›[reðh¯«A iã>S¡4¶²Y ?2ÂyÛrvÐöì gÿ ÅW¼öYrMXye7ÒÂwÞdq2ÌŽSžéÀø.X^~T.„fàu�ò’Ãrè<žÁÛÃ펖ƒéaçÁï|³ß]€s12ÓÐÍ ½­ÙLwŲС µ’-‚©Só ­·RÀΈª±‰·M¨úÓ Ùú9ßµõûžš7æZÖ¨�/¶÷ïÄTd³Ý7:…•ÑHÓz$5—M­?MÌ„�/ö€€OSêRQAÔO‡ÂHõLŒÐ™d­¸ßºèÅóç°kª®-ÕÑ~µT)\@$é>WŠË©LÚ{Fp` #©Þ€‰‘Gð=6°äéB¯VÄ8`Dâ]SCnÔ½9fô¡F²W“O¬)Ú\£¶×Ò«[`�ÕU‡�_•¤=èÝP$ºD"ÀeäwÓ¼@ ´¤àdž%ŒápwÉ?"~pÛ[pœõºÒÊ‘Wª-wøý&1M ‰^ÌÍÇ}Dì Êóͦy .å=2øºEÒ}E‘vI»£Ö.ˆ’)ŸcÎ)YLMIÞ§6ºÄ�xq˜€”•B¿µŒ«s91~éËÉâì?—ó™ª= ܶ�™}¸.¬êÅ,¤ù‹çg>ÍÄO}ÔjéYûK±Rè3BáÀ¡1ƒþ’QÓoFt¾üÓYØ~¸äÔX„Y5¦½-É_ – ù¦õðÌßô3[øYO‚X»ÄÓ“¿#K¨Ð·¤�™Á‡«BåÚQÞx«Ó‘LlL[(¨V7ØAtŽ›'¥AJÌ!(qkU@�8…˜Õ5X3ºÀC“M«Š¯ª2YQµúŽŽÂ(‡,Ø…ìà!£ ‹‰”’#B£;rla–BƒSlSœ&‚ú '‚B…ï$E»w=) Šj‹��Ç!ŠJ dë!G(âRUERšµ¡�Aé´ ä%‚þNBÄ Ê >7Ç(¤—œ™D•Ã1*ÙôÙo ¾x#¨ ÌÆdMZäjÀY޶‰U7Ý>¡ÒÔ˧V2œSëµæ¥$ùŒI=€å-²m~öO§®¤%Ú:hZd’¦FH­›X½jéXa%•#Cxûmƒ }JŽR4ÍíâZ)>‚ý(æ³8”�6ƒ BÐùä’»³úõgê.t¾ ›¤oÖÒz-«7ìP…àLCtñ3�U5"oløPfìµ€ÖKfü,rop²ØwRì‰àmºŸzmª9/¨ÊïÝ&ñ„òk¼PA.ð>$7 �H¬ü.Ñð¶"Tý/YßÄñÓW c—æ>j>x › °Ž±¬ß·Óa£8DÐ/UåÄN_ã[¯O¿o�åO½—éò„ö'ßÿ•éû.á}L›_ʈvñê1 åb»Çkw�nX¯Óíè¸w†7¤�ß÷RÉæbép2òªÓadðh²b*¸H˜»wÅŠ.BÇ�]z¼ºo©«çjí'‹îñ˜j‡×±Oá.¢(ä!ú{tçÄFú}„ÈÑîö‘šÞãÕÙqóÉb÷<¦Ög¨øQŸ¦þG�R“Åèð1‡æ»É=RóUÇGÏ¢ÇpÞ‚-€� �}:¦£5¾â*|èn¸DدGqµ# c]ªæLK-*äÝ—÷§¾”ÎúîWw¼Æ_Ño G©²»Û¯NÿE›¯4j˜¯y7}„þðø†‹ÎÁ¯?�Ô‡Jà)c]a|Tºè+‘™ÿß@r.qº¢ŸâücÕAcb9“EzSë�•(€)a×DÜ÷´ìŽ!Oé0‡chuÜ=FKžôÿדBºOE‡tTß1õV{¿Ê[I'½î<ïwÇñ÷ÖYøñö7�`. >¾>Ž|�K�|$¨xœÅZmoÛ6þî_Á©À>%1¶¢ÀÐ9R· 4KšHó© ¤“ÌN"U’Š›ýú_ôfÓNºÉÐTâËÝ=¼;Þ)/~z{±¼¹»|GVº*ãÙ¢}ÍðQ�¦$]Q©@ŸF�Î�‹æØ¯™.!¾‘ohQ€\Ì]Ïl1÷”‰È Ÿ_b¥©fJ³T½&C š-(á´‚Ó¨ˆâÅœÊ_Gü±9[Ô$-©R§ÑêÉþ;ƦFŠTd0´HFÔ 2µsóÚð‰É;® JJ "'‰™•‘ R¦˜àD#¥š¹©»o…VêøUä¨Ïˆ9”a;ˆH¾Bª S„Iy&*’ J¿&”HmÅÍœ¼#4]‘BŠ5'‚’D­´¤5Q´ª‘^N§Œ3^�Œ¢uP†¥œ¡�„P¦îA>U—L“\"‚Šj´d˨–�±T ©N¹YÁ¬Çç‡IEv´0Œvx°HÏ¤Ó N MŠc Ìrgn€å,E“˜Å HG�ª¨J±ó°U`‡ÑÂɬ7�…/©¼÷äVêÏR}kÄïªUG ˆLÑXu›‡³¥å¬WT[ª£Y+ÝL9ù±™‚޽™"_›¬;Ø›`% (dR G¤`÷FÆ–èjÕÖ,F1 %ÊEjkgš5CF‹5•8m�dØÔƒõ.%X6,…E³Ë½RÄ¢ecŒælæg¦¢ªiªý´™_ɤ¨Õ§‘ É�Ÿ:KÇbKæée ^Äר%pŸÍ}׌ÜÌ+ ùi´ÅéÄD•[ÝQH&��ȈÙÈ“ÌLÏm«Ùmõmó»ê\®ç5î‹âq{›Ç9î7ÆáËG Ò(ÕqÉ™Nµ¥oß~€Rv”²¥ôzŸcrñp)¸®•�†«—qÇ·×àÜ�5H³-ó(¾ì: W$~i"8Ml¼Ö2^èÌGÐ'°{1°ê¹u²(vOÇÜGYÃÿnê.lµÌ ÎQð“þgS™–Šâö-„Ç�ƒ›2�p/y§Ø»åÀ„§Ã¬þs ¾lcºÝèSI»‹â»�´+Í'vÅ·!aƒŒ°V¬ôdæ´�ãO¬D”"ö=èOf 5ªÍrÓ9Õ¥dBšýŠ�°mq`#ÛÉR˜²ÊüÂqÎÔF~OqædÂßBN›Rßa,ŒâAc�» &ëcZÖ \ØMH*ŠU Wk ¹QíTæò[Í;θý¨óôÕ×dVÃ2¢�RvXŒC‡ºCð>pøTOw°˜{47âÚÖt6–owï tÊήÜÅB05Å·…ïTXÏÇ„�_³¿á ÂõÖ¦Ä�Xba­š[7\‰2› ÖŽ/ï%µþÅ£fš�lŒkõ¨¨§ÂåŒv‹µìÔ%M¡Âª%ŠƒÝ!œ·+,(@Z'tÈP…“Œ¤x6²5²r˜*æUu£áââMT\…ºÙ»óàŒÁ8›ÊE"ôË¿ >V!?Tµ�šò ‡ÃODßíxÂ:R»½ïÉœAú¸Åý{$C¿m¬Î“qƒåÚÀŸ–²«¾µµ†G7ìzB½ésÒÖQuº´Yjº”L– ü­¿]¸6aѤÑ�ƒÁHaƒiÚÎ7ÇN-YÒ8­,� ƒ¿¾Õ‚Û®Ç%eáVïÂÉ"[ƒ‡üóâ™R"eÔňPï¾båOÏýL’€^ðéÔ�•Ù²�"­7¼3÷Ö«÷Œ‹~†?ãÃ`áî>Ö+0w5ƒÈ¡Híy|9ç@ù9•ã{ÖÔO ¦nZ– «�ó¶¡øß–· ×Wµ¾o}›³‚…»Q¤X C†AS)¿:E¤¡Íže•—R|gÓör½†7Œܺúf�Étßè’�DQ�Ä,:nO¤n†ñ3åYPžƒ,ࣽú47/]ã±BÊ¥£Êø›Óé¬Ùp0–ÄÇÓPtW§ÆÏ«*ß°ÕÉt4g²hL…è/jºfДµËmFg Å¢G;D4o¯ÍvܽùkeñæîÅþÕ­Û�Ñæ²ÚËÓñ­éæ‚þ�b½ùÉ!ø¡ãä‡T¾ ˜�Qì_öä¿Á7[ËÓW uDB$—-¨þ}Ç6Þ¨z­+¸ÏWçÓ`—{aWŠÝ¾aéo}0J——ððFM÷_Æ�“)Ñ× íÛ# ¬~ù4H+Ÿ«�Ùoüu Íêíyx«'S]›¯»×G”—îEJ+énP;U{K$wðAËY3ñN§½ªQó1<04ô]®Y ]tÙèØúÚ©Ý!ª�…m(쿈N¿“?mßî{Dµ-ü犓­<ƒFÍÔv5Xá*«âçQh"?=†0¤Ìg›³²Ä@üÝ|atoÁŽùch[xc™&Ù÷àO&Š=™rÀüK06¥fÃâͨ†–D¥´´‘0Â�™ÿ¦õ×7X�¾¯»_]LmDóÛÿá±{ÝYúUB¶å6ãÓc¡u <‹b÷ ¡8˲AÉ�–2?pèªQ¬°)æ\0ÂÓØsÿ¢¹ûAÒ?³û �>�J¡)•xœíZmoÛ6þ®_ÁyغN¼(V´Ž�õ[´ÉÖ`Y? ”DYj%Q#©ØÞ¯ßݑԋ#'¶·E �-êxwä½=G³é„#�‡³µ70ï!¼¨X”s­OFé#–h �dܨö„M/¹šÉðýt‚_Ø ›†³†A8cßÁë8·ßO'4:©îœ÷ØNøë`’’ºÜUü;‚G³»ÄoS‘çYõÔÉBãõ„_ò¼ƒ ÌØ³:ËcÆÙ[ç bYå<+…:,Mœ]z5 }ôhd§�§‚q5¯ Q ßÒ‚ÅBG* EÌ©`Lعnj]¤B‰€Ùµ=ø½ju&3˜ýÀ©Ä2ÍæÙ¥(iél9G²¨j,¹a²ŒÄ8`HÔ"Ó(”Fª\$†é$™qlý:@é©6J–óÙ[ÊçZÂÖNÜPÀ¸}ªDÒº=Æß×�ï7;Îa[Èÿ.l õ¦ƒÍÔcR #ˆšÀÎ͸h4u ÈvrbÒ¸hóÒyIÇ$|å¹5ÙÇû‹·“¡Qk¿�«ˆÕõn4,=�3%xÎJð0•E¬àFeK&ðÁ”\°J¨® €ñ2¦—‘Ì뢤÷•q©Æ ¼‘3ÃÃ\ 㘅+R÷^ã· õ|–ÊþÆPù7M¨L{4H²6öÓ~ÞŸmDZ¢‰±ß,†$qq_œ5Ù›ºðÖ2gÞk›ÊâÞL«DoŒ”øòzÁËUëùèŠÎ”’€A.¾(­I ÔšBöbÒz,^çÆDиf·*�pƒãè4„¤{ »bð¢å“Z!ÐÈÐ!²r>«™® çøHÖ 7ëÑ–emŽí2q¯Û¾çh^4Úî[vî!VëôcÂ^<°ãY’E±°U±È]Ys›äcÆí·pžÖ,­ŸJ0bqÓV]*·½[®´­õ¯J�¸Ç‚/Ò,JûÆ42p çyîÒMÑu…®n ¢¦¢~*`Ê%eÛú‘éWÅ«vìfÑ*rE´m‰$Ú ·G×ñ:¢w0TÑ—µY¼À¬Ç¡¾/]av…t hPâøö°›@°ÝdŒ™®=KʬëB»èb�öÜæ²ŽÇ›*þÛºÀÊÖ+÷”ur,¾-˜ÊW 8n΢†ÃÆZ ÷L.Ÿ[Z%ïáÀG†Û!€‹ e>h*4SK¹6–[i¬LÍ360¨’)g/Å‹‹Žæñ­°¹Ê.®§Wyýq½3sÿ€¡š¥7æÅEþ”Í ü»„?-LLUnùY³ì†Mg× ‹,@„Ó«™]³ûM-˜š3› &*#ÊY!7šÉU³Xi.Ÿ^¸ n¡G4 ç2…#«ê‘gË”n´Xh$ÝàcÆîsiº�I“0 B¼TU±yìW½‚eã2ú°,ñ/—R3:JN“]IÁØ ÐDY!É^!ݰZiåJ0YYëLHÂUŸ ö¿-¡DtŠ„öˆg¼ls'pÇ9E�nXªŠeY±TãZó ËU‘ÉjÁT%¼½ReÍSûª§m¸“Ná’>é]«w=²Ù7‘Z‡¡­ŽUÅ/q!%à/1á~Ò"“©»Ê¸å{%Š8Ô ¹é!ÁÖ¶!­wBÖjMòP3#ôŠ b¹Ih`�+$ÂÔ X –„ ØÀ¯•²ì»¨íqR„�=äB ÚP ‡+ÂÝ̵*g–Ã.��Gà~Š$KXÉ­–?¯2ڎмgÀ`ŵÄß[*ÄJ·k€¬kJ ¤bi&Ä˯à‡ckión¿‡õák\¾ŽÒ‡¯1}¸k 9JZ÷)ƒwB#�´4ºÏUÁÀ$!Q¨¥“v *fXEÆ×C\˜£„ùæÇîÀ?8¾MtOœÒLª(z,µŒµc¹þðé@`ëÆGéGÈ&ûq 9?±?�Üwt8�ï`ïm±o}~�Ʀ‚Ì(��V¡F˜ü™sŽ—âùŒ€]d{GíÌx¶iBä£î”ARö¦¬As˜ÉY—µ�-)ù„ ÇŽ†FÁæ9Äâ& éŒÏd!1û>¿Š‚+A‘—¢ÙuËnC6Nó&}„Ô¼MH•je½zC)Jq‹TM°‘ÞÖ'ˆ?º­Ñª$ܳß—µtæhø’Ÿ¶?E(Ä,FÆÙ1ØÅîVš-sI�ò`àBÑ$\’0i“Bc““o¿7‰àL�³¦�o,ª„_jƒŸó* }j;̦¢lå6¿¸–Ó+Q^cR1@ð�¼ãvé1n� wQÀ|Þ Ìm(ø R¹å{sð· Œƒñ…p~Ü:`{{sÿþæ%8ÜtYŠÊ¯?¤_‹9_ö+äqíF)I�Ï`~ŒG£³V=ü*ål­„˯Ìì¨ö*Dê-g=éâäq¨·\̦ñIëÍÁsÚk‡Š[¡ Á¥à•wVI 07¯‚ØÝäÉëùwò 1Øœ[×{)ï=€½h}ÑÈÝÞQö„é>ÍHŸßgu \w÷‰C¹]wE»Ï¹†ë$hŒ‹ÏÝùLMñ—~�ãç½-¨ÖB�óçE{Ì¡Ä'F:ÎÇ9îÊ÷]ƒA@íæ/í ¾¢´]^-=cYAú:…; LBék úrënwÁ6&L�û[ç�@…Ñö å§N*÷ê3ÙÑ`-j—tla*ÆqO½À¸û…©Á3¡¦x]«è¾}r骫êñ¢è•óàêüs­|ÖpSIýèª^ÁÝ~¾ãg)Ûå˜ é ‰�?Kz~R«e•ÜËz¤Ïº•Õ{ÁçŸå_ãJ¯!Ÿ]¼ÞŠ5Æ’ÞE޳fPÐÂ$ïd¸žà%®Áe@ÎO·e‹ñL%£ÄÔi JÒ™üAé&ƒX{ƒ Û5*Z]É+÷(,ÞUðË[ô¶°�8=’QxlqÚE‘¢ðˆæƒÑ±MË\è<‡Wv—%àe âñ¿}ͪÿéÌÒšEºë€ªš D*dÁ~ëm˜ý‹}ø½+jvJ3Ö»9�òié�¨ ž Ì#ãXDIGa2Àq›‡\€CÑA"ã²j¬†ø‹”î8@ªP YþãX ×KXÎØº…*ë¥?¾ìRÔ¡{Ô.åÈ{UŒá°4ƒ¤RVàJç&|1ôD)Îya[VÍp ‘j‹½}¶�$#,Ö°<ÍV),bAºŒÍ-P÷Jkyvà”~WbU�Wé@¬Ø;å¼Pö9…it™l§RFàxŠ/ç8ÜýA¤'{$¸ÇïBÔÆ]†4Õ*5tØ¢Ojãwã£à ÙİÃãöR�6AØ�[<ÿ~´ªÝBć]WK žM¡„ÜÞš:4[WG×ê¡›#�E¶Êà•¡ëÃíz5&‰°ÜÈ|�¤hH‰\zf!ÙxèúÜöˆ|["Åf  ªE´ý†¶™Bþk#ý…‡ä ÆYÍkQXþJKHi€÷k‘JÌ$?cò8Tœ0²H´‡o$Å£ô6mèAªØÂlélîÔºA¬R, Ê@©–‘+ó¸êÎÊg1î-A<ßtg®cŽèAÅÏ^h÷`¶Cv¶‹ì > €îˆøfY+Šó"‚ݶ> Tŧ¡ÓË(:3 Päte2®ÊÖÜšúêãšêäÛ^jo*Ï2ò³y¼uzR¿ÔZ-à†qcŒJ%Î9c”ãRø(Ãází /�-%› »¢:ÍðzÿiDï[Î_’ÿì�ÖäºtáÕÌ4ðJ7a°M]Cf <Ø÷q«�mŠ¡SºÄô«5rbo°!;ÏöÌÜö²Ž¼C�Øóï¤]/™ê‡V'ç¸~Òê‡,!kê ûá‘íà–K,ÝôƒM�´ ΊÞ@Ê37e)ðEN´¸âª*MJà5ЏCîŸ¢à„¦_†ÉBÒíN4—‰™¦vgl»Ã¯Q YxˆÒ¾$2õ—:7¸«/V^]æ²(@Ú?ÍÀ¿ñ©ÁÄ=NsD˜Cû “£ê†x]‹*k>w]YXŽ/à\£“è¥-¤ÐQ!y(ÍöiÆyÂ-V‘÷¤Žƒ’ŒÑ¹íÂOèÛ6�w£Î®—z`Ó%HHmËMðlñ݈�Ð6døbu¨­Hɤ{Xì2Ùzïòë«Þ­Ð ñ^ðÕPÁ8 ùÍ�€Ïþ†˜s%N@_|⸷²ïËñÁúmÁëü®hŸ ôgÆ„Œe5`Ê442T‡cÄ(Œ•%V0=Y’õñçR@�<ƒTi#yFÄ™l= ¬ÐºhO")küõF/Ü“Áᯠµ$£¿>pŠ8ÏÚ7÷hÙ=_?ê«Výw[8 ß]þ«¬æRoñLaHn(w¤PK¸Æ¶s/ç»§q[¯{ᷠÄֱ<©ë~å¿)x径ø7XW!qO¯=zXÞ)¿€*ì³¾Ó<îðïp2Úñ:N&Þº÷ç”íòèòE‘€ºBŸäçÕÕñhkŽC6¦¸ˆŽ x£Ši�|2Åœ§Ø¤j–½ÔE  WÞ]P+6¯¿]H˜HWÉäEQèâ½NaJð†u~ «lÕ`£”ªÝ6¸Sw ·Qƒ8( ‚‘s½„FŽ`’KYàÄ\^É$/ÃàÅ»ˆu…Ä‚â|s°–4U)…2ô(ø#­A™ù¾®�„¤ÄÑT<â¹ 5=çö—ŸPlŽKö™JeÈW ÁåùûŸÏŸÁ ¢MMRü¨ä°ÑV³F¶‰�Éto5ws;•| �.®g+EéL#@=¼Ùs¶«�Ðn¼4 êõa¦¶€ÇY€šÐLøIï¡éÊÉ'y…¦¬�„]@©„è’rxc]9g”C¾‰å‡õˆ ‡•y™€Ò€èÚà�|åâ's‘­ù¶ù@`T„Õ~ ó¤xÞ{ÕÄm‚.<…j�cž½ƒ¡+ÓR?§'æR$Ä©3èöÚÂ.eük¸z†o%(3|ïhôtóõÖ壳֥va—ÀƒD´)rÄA*ËšÚQ`t½ß,Ü›…}Óè†á´kåÄðõîessØš õUw¥¸È¼ÿÞeºð�f}¦8¦|éŸ2Ø�üMÔg/U±º¾1ØÜ\ݦeûþ†¢wYß0ŽÜY¼�i9š¡Ôîå�gjF¾F:ÀlÙ©“ØÎ5È—|R<¡*AfçÏi£åüñjcö:×ÿ-ÕOàÏС‚wƇï8të þ˜…|8WKËJ�Bpñ"�Ý%ø7û<˜4Lf6`h"å•�E‚Ÿ-‘ZWL.Æ+2|mªðo±éüIDž±þ&>ãß&—À<åcqÄð* ›”&Ð/s~ý±ZðÅìŠõ?-Æð F Þ'ŧP=1vû¾Žµ›,âÛWxï7j¾¬õø�…ÌÎË=ÜF½ðµ£µÃ�ø�ÜÚCQ»Ð=”y/€ jcü×EÙ¦,7´»ÔØkו>†}^™ßFÈʯö ÷µ¶Í]$­$†L¤±bކŠp´vi¹1\ѤY‰leÊÃù"ÇÀÍÚfBË›¼ÛÔA®ÅR/Ò˜]CE%™Yh HD «Q]ŽÜ¢ºYöôó*ÁWBþµ:²Ua*ÁøKI$” .3wx ôa°×ªôí>ŽÝËÏe%ÐWÆ«Ü"lÀš×Ç7oÒ² €tq§çˆ:é‹ÓrŠ…‚oðJˆn¦„mäõÉ>ë+Þ�dÝH“÷N/ÚšR³«@-¼»lVkQeÊ�¹‰ÇáÄzš+ó{³O#½g�uê0Ĩ“"Ör`�"¢Ý<±Õ)›icd mÚxlù2kìÃÄ+%HŠ¡ÈŠgÛ²¡ ’Õ¦ž Î/t·*«)&òš±ù#V̰O®’kÀö±*“˜ à.’6OÊf+}Lj>˜Ð|^kýÈì‹õ(ïeÝè}!ÃHìP0«×³Ïü9 s­¤í8·׈¹„p¨»™£Éé«Ç‘Y–T`‰õ•L\×u+é0cL®“xıÑàü*1²Êßñ•™ŠV`Fz™�º2-˜¤1ë0AaÖI %L 4àŽ½Àµ (Œï¡035Ña{¿:ö ðÿ�ñ‚ÞQWTë^A-QÈöŠÜì`ìŠËŸ�u�x÷+õ ãRâªE³`§De¦�.€ÊL1Cà) AÃðhèÚzoš…kðÍ“ÐåiÁtcšKr3a†È¾LÓ ù§¤šFo·”kö“î/²IÀÚ ·j÷¤›ˆ×ÛG?Ây¹ÐywÆô Œ3ÎMÕˆpN6‚š£ƒ½Ñ�2à¨�r3ãÕ¸K�}u/â„ 5] ,`XQ?d5&¹™bEß%Çꈗ�µ `�Y V'¿ „d�u”ò5ˆÖÆÊoœm oúW \{ÕB«fÊsßNÀÚŸq"®c][×€o5ër <Ä ˆÕŒyõ–°ûŠê:ú3²¯� ~8 ªÙl7=ÙØÐ�w/Ð(èÐÔl[]‡ îˆyÄÃð]·_´›õšÈ– ôµã|nËå†àYP/6Edjû>�Ûfjƒ&:‡ˆÔoµ¤Ï ™WIª6¦†4fM_Ú¤ ã­Ân&ÐWk©Ù‡ZžF§‡MÖØXÅ> r¶à²™Hlæ|ævÏeK¿Ym°t9íi”nö3a’ÍîðX=­oÎBrqN.I™dÊçB¸TaŸçÍù� >Ìßh5À§�”É.w:UÒ´jǽڴÓ.„¬�©œ#:�œJ‘½ÏIi¨Bsúó2)í&r³‘ a)>…¼2o>¡cä2C’‚•ç¶_%9Sv#Ú¥4ÇsQºN<¸äÌ ·ç ‡{�º{"GJ¢"³YÄgÐÜ32â^2•‹Œ7™h|·É:cMhûzKÔË/ok¢šT¾Qs8´Žj0;_íØ ¤xýÆÝ1=ìÚìóA¹Ù„|×(»Aɘý–ºÀ \%î•ãM‡%®™Mb}­f•*æ:•èep_‹,R̬–ŠGT&xïh‚û+xl 핱�¸«tjÆEP¡]†¢¡œán¬š·8‰¼þíLy¬Vä3G¨7Ïœ“rsÍ£�Èg^.á6íö™\“ÐzüÑf…ÉEeFnŸ�)Τíævlõ¦éÊôIp'r ­í»‚vzq5 ò³V".ÔrË� ·ë€„Gî ƒa$ü—;XaÐ �‚í•oMOÙëEÕžÛ–`oÞBµ¹ÑÏÔ;6¤?8~Ì6œ�ƒ’›ö߆’×Âr½ÅnŸyPÛ÷í7ŽâøÅå‰÷“œ)зØx~CSÈ6  ‡»éûY£p_°ËFlèÖXÁ޵­~r¯ÏAï/~'üû“}À Dî¤ãÌ&Àå,±„¢ŽRæÀØö�~[ _%ž{jŸ|x“í“ïÓá£[Ÿÿ¶9£Øx{§Z¬skÏ}jw¾Iòi—ÖÄ'ZªØgZÕUÞ¡²ï·Í¥ÖÕ}ðÜ÷�µì! ˆéþ‰ð�䨖_[`Ú4×6äl{½ÝáÞ8Y¬_þ{´3ot­}ð‡Ž Î+ò>OªI[ºs¥¶-Ö¹”¨¨–µUŸ›ƒLÑw›Õ çlÒÄ~øÞxzÛÄßÞ¨ éœ Pß#§³QÒ×Oâïà+'ó÷UÁWOê,X ÌíàNÊ'âì{i£]qŠ6®®�šMJÊà<\ýç º�5Ï,¡º$ã�(ý¨³*qw%î‹Ï÷ õ^âŽøÆ ˜Í·V¬I˜µŽUª—ár `œmhÔ~ù3¿Lܼw÷!ˆ2Ýw?ï edJpVh0Ö+)–Öð€½Û£¬O¶]ZK扨;a¤Ý/’‚(~¦ø¼š‡ÙÊkYÉ>ÊSÏJNÐe”aœ.2ÇuP@Gûc™¦IþÅÝË×—Ú�ð¡s³˜™lMŠ®Ç±hN Ìm ë¯J¿J˜444 Ñ‚g[uŒ Z»w{Nðo îãâ<ц@OOñn$fbð4ê–ZÏã„j¤óÛü»sõ{ðüp@Wýa(7 [�â;É,©ùÿÛš\2HÈ¿òKnß|dp ªUì ]jo·OoÚbî·kíí6"}àÝkõ:$³¡P±µ ü"Ü’Û½…�‚ÉžËzë�t]ÓéŠ\Ù�Íù@�DÓ<÷Ó7йϬPe£�éí¼B½ÝÈñ;o l*Õ½k�©¿ðȉ§2L°}ZÙóé5dùÒjÌUFã ¹ãÍ€¿pË Î½+ÒµÁÖU›n Ç>;c¬n:·ÅX%õ0^ý{Wƒ‹µ•?Wâáðl`¥ûÀÛ%˜`'À`ƒLE9‘)!Æ´õ6Üù|žb…³¬)'bMù#¿…d��‹kð”Õ‘›ZÇ ‡ìÓsmâ£]3 r)©`<�OëTVX4Úu�/è¾­På¤,&D¡ÞmýþÔ+ïj %s¤ï¨x¦SàóûŠO>Þ1>ùÃnñÉÇ>”ö?JÍÅÑ~Ø’&5ZŸ'…×ë¿¥Já‡õÀë¼jÄÃmSt0hÒ…Nƒóº{7ê¼õ)-œ`‘xpd…wÝ`¨~Àˆ;EOF�]„fÁ.�¾l…á\¦¡óåáI.-Õ•±�àËöcb&S³–ðláu*úi÷ô½,WmVêyQ€šÓS›—Ïß]øÆ Öêû¥”ç@³¨ZÆ\Ô:ˆÞ p”zÝ÷†þñp$ý³¶Ú¾"tˆH!êËD©†ÊÖV¥0îE ÝvRfUÆ‚Þx f㕱ž„pD‰Çƒûüî­áoÎoü4¤sö±ÿV,ËÖÞjWòÜ{1€ñß~Qñ‚¦í\‡õbG�?ëAV&\7Y17;ÉÊÖùÐE8r%m%)gây¡çí™�9“¼ỏ…jb¤X'�;ô]®Ýuî"$ãßûjó"Ãç…èÓF>…)âDDþz޹\ú†“ÌÁ>]±ÛŸQ~c°×>²F>8èéýímë°Ã/Ð,­{à¹Û{Lôƒ2Ö&2Ô?‘y¸é£îÖ±vöX?%6_côÑÑLvŒ%ù;Ø>åo+BLmyàúøË•ê �tqB¬õ‘Kú"€ÙgáÃÆIŽKf�L+Þ9áÈ|dc†¢ºg“Ìâ'²y•P¶nII½ÈQz‡WNŠdŒ‰sa«žÚ` Ÿ ^Yä@Yà•ØGº¦ïa¸ lv´}tl‹¾þãl»>KÎç*�k‹/õ²Hûv—9Úbvq5»°9ƒ|ö°f�Ï­1�ó8@¿0”ãˆ�̉^ 3†šþŒaWIM&Á»ýøÑ8fSc·_ŠBZø™�Q°òênxšUÿž+o¤u�¹Ï&�/ ãŽyý“'vò "}­V±‰"«Ã¡­µ?*Ç}¥Ö}!nd6HÎ`?õ»Nò¦©·Ô[e�£éö/{S9 cP[•3·Úöwb>°~Ânÿ´Ôi¶ ¸ j¿¸†�OŒ sxœµWQo›0~çWÜò´=$h•*M�4¥�VmK÷0MÍã—` ld›lÙ¯ß0I¤De²}÷Ýùû޳ ÞÜ=.~¬¾ßCb³tîîEó+#‹%¨ ÙÛIa×ÓŸç­°)͘ i~‰Ï¨1Dó÷~‹i�V( ¹V9i+È~eì~ ªxW†x?7–­�‘¹��`ìå3º�ôûÌia,i!7Ÿ •S“yàc÷êLâ~óŠÝrˆR4æv’\ÃÚð3å¡eÐHÅLEå¶Ð¢tVòÆ%½ƒ œw�†œÎÞ5ðó~+Àt£*ɼj5[>3ÓëIåðq/ Fì[ЬÒÀº Ê8%09Eb½+�mB äâx‡@PŠÁ*Ø�$�–NLÁ¨´(AÍ àÁB„BÊrÞ¢X{½æ€š çÔy$Aȼ°�¡ÕâƒýH„9°%Œšëdªdº›ÕÛ÷yÿew,Ùr âÇ.cˆý¯­s9j½gŒU9¿K5?‘WÁ7˜�¼u@ê¬C’QùÉSd.Ô¤rS‡Øñý5dm÷6/è ‹Œ­#§B¤¤E!� Dó5ÅîN÷ŽÔY3a”t 2²²¬ÎèË0)ö#¨p Öà+Ÿ®r™Ý�KœG5%¯“#= Ý|ñÌ•ìoÂNaÓµñ47HmŽë¥a%žª�ˆ0uœ óЖî¿ÁÕªBCz‹UŸrÏnŒ|e¨š»ÁƒìYTx[mb‰Ë:½w#ù²È�rÆÿ‰Ó:´;ƒ¦�É<¯Ky„ZÚP»õ7`RÉ©#½O%¯ÿ ~ ÷�¹¦Û‹³�íT›æzíâ¶ÒKµêÞ!÷{µ'Úë<áÿã«‹_�Çøêã'Úe�\S®É�´íûkƒÈóÈwR¥Ù2W26|Õñ^Ðidž†±ÿ4íO]|ß•}wË+/Gwüad?ïØŽê¸ô”;ïy~ýÃçWÿ–ÿ¢ì ±‡'�NŽRxœÅWÛnÔ0}ÏW á*±ª„Pj ZnâÒŠ" OÈIfCb‡ØéR¾žqœ[·Þˆë"í%���Ç“ãq|ãèøðÝÙÉÈMY° îÿ�gôW¢á�æ¼ÖhÂÆ¬ˆìF˜Ù!/„Ô_Äs^ó„ëïO.xÑp#”Œ#çÄQ‡”¨ìÒâÞeÚ�‹6"Õa�\ƒ˜ƒä%„ÛCGÜ"ß›åCÝA\AZp­Â|Vš> jBHU†WˆÅÉ,ZB“¶c⨲¸lXÉ¢w§©m°òpº ܈L\ô\J½ØÈ#pLæ¦n@%Ÿ15�*i8¹‚ÉjÔMa4¨U0ÌH“OxhU4EC£mÛŽò¯…õɳw^O!3ü·y¡|‘j-�;*ïy-¸LÞZÖpØ#î�ÐdH­RHÌ ¹tÔ[7µj š5¢\tÄA7¥íÒ_NK„Û§§�÷À(ëÀZ˜\Ìù~Ø[6ç9õoYC6^h"å†FrKÃ"zÂwDY—.‚Üô.4¨F»:©¸Ì`�E±ÐXÑT³I4k$ Íñ‹ÛMÑ6.èÛ�î ôî«F¦vx�6¦Î÷ÖæL·ú¾=�µ©•«ºv{"�NôHPÝI\~Ÿ�ÑÝšWŸªZUXZ|K"d'ƒÁÎB`÷­Ìó¤qS³Ød�LþüÍí®Ë1}žvY;l÷ÄæHu’k©Ðwô¢ôú¨qªðM¥ØJM‹�)¯¨?~òp¡[C®²þdyíZãX™ϪÜÌma®Øòh弩ª½~¹X£ µSzªÕ¼EøP�ÍÕà” ?0žY‘µ¿¾ <Ê2�¸†¡Èß©B¶ù2¹Øì„gU(º®Ù_ϲ·ñô^½úûÑÒ›ÚQwA�ÜølËn„­ �MÚ}Ðxœí]û�Û8’þ]¯·INw’¹Ü-’ž:�ä0¸é<&¹É‹Å€¶è¶&’èHr;ž¿þêÁ—¶%Û�tìlÜz�Å"ë«�Å"uú/Ï__¼ÿøæ…˜UYz�Ú”ŒáŸLURLf²(UõãÑ¢šÞÿûÑ \¯’*Ugï ¥žÉ«+UˆKUÍt\žžð�èôÄ”0Öñ Ë{xVV²JÊ*™”OD×›ðHt*E.3õã‘àØÿ<:;=‘XØ£³ÎûPÆ#¸=“T–å�G³ÇbZÂK‡Â>§×²8{vz‚ÿˆÅéø,,d|&îÒ¯^/ùÁ¿öµ¿>ò_÷NO¨’Ó“ù� ð~œÖþþE•s�—êWY¼Ý}{�¦ºÈ©üö‚ôèøß}ø³ÚM”¿ÍTš&󧦾ܩß\¸–颻CÎÄz)d.t]6N•ÐS1Ʋc«IR&:6ñ8âwâäÚŠž•÷q1,vCÔÀ¼vÉâ ä+#Ññ:K‘L“ ´•Rà�™§*sRÊjvtöê˛ӕ�‰y¡âdRéBÀ�"ùÕ¤UÄT@a†sMªcQûSÌt—BrM"•c•Š9´dEBâ­|‘©"™¨/ÕW k*®V?Â÷d¥®t�WG¢iò+|‘NN*U@E!W#¼OC/óì$÷�)¯áÎs¡Ç¥*®Y#Ë™.Uä›’”"K@eP üLÕ´zQ=�)wC ‚‡>©ye{ºù…œÌHÑx»&G…KQÊlÎc‡ÂD%©¸ûS½ÿ¥Diþ]¼²½…b–#JZ&Õ „�§r¢2•W"ÉA=×÷¯¦ŽYªäjVÁKÐShACK¡®U±åz;{:uƒ°Ÿx¯_?3¦±‹¦ÛÕv›uhÎà—>)5o ~ƒzgQÝ×ÓûÀ¢À3ЬœyÒxÙˆ|[y麶s‰Þ–þ�éɈ”ë¨*¨lp÷�“îùèî¦èì6×U^�¹FƒÒöÏŒ­"õs:*M–¹’‘¡#{#&³ßN‘Þ΀x;Äö±¬“ÕɪÜÔ;gèËÀ¾®°k4"zΉM�òšO&¯þ&û'½†�QŒ8 wxœ½WmOÛ0þž_q3_¡Ñ†�¦)XbÀ6´ñ"�&ø49‰K¼%vd;Eì×sNœ6MÜjBeR›ØwççžøÎ—Kòîìúôîáæ [•4Júg9Þ*ndÓ†ÛcÒØùÁG£Ü [rzZ6Ær}ª^…’IÜÉ£$öëS•?;´÷ÔXf…±"3Ÿ`º ¢„�d?&c5¡IÌʇ€GFI YÉŒ9&ÅÌ þpjq]¦r¾æ:I):h-“¸vhN°µòø‚• B( *ýÍ3;‹:Ã\,zÇ•98"ÝÚ»‚Cçxꩃ0ÀÀ45×íj˜+öy‡fž ‘À4‡Lsfyé3Ø¥+·ÆC˜Þͼ‘™C™\XçT*AÅ™´`¤„Ľ‘V´x¹Ðèª|îŸ%1V+ùHo9V…û{Q€+4Ÿ/†Üg.}ö†dȈ£û°¶–•Èâ�øÆ4K™ù{¾|øk³žÐͺ©Ÿ3¶Ü|VM™ 9v²AIèÅþ+«Ç k"BצS€[QªáÖò1NHChHÚ“6x1æ¦?2JZ.­ñG¥8¤ã¸-óóW­&¥ÅGn]z³8l„8t‡Ÿ¥í!·š&6÷‡ìŸA÷ƳUú|ñKèTÖðÇÔ¹ÅÿÊ ÓóQ!bQuÊÉí�a?ºÂ…ä†Ó/'5oÏç zdƒBùv¢?n,;ApÿzXx›Ýóº�¦ÆºÂóï„®Æ!6?°H÷»&›*åÚÍV¥c÷Ü.…1˜<„úAˆ•W½á]5Õuj¸^´Y‚‘ ‚¶Üµf¹{v×µ+1zý(ÄÇëþSܼ·‡%§‡mœ¯V£ÊƾѼ'ô>DãŒaÕ|î¿ç‡⾦n-ÏØ�*ïkóe7{}aÂm¬Êƒ.lÚ΄*ŠkN°½ÙÞù>êaØNšå®Ì¸kˆÜIž#3ÉŸ ô¥ÆGç­qm‘¯×|÷äêRaOê®!r7(oK_¸óìÊÏîIeªªY†¼ü`kP½Íöæ8˜×±ÿˆ»ï‹œórªˆ�P� CxœÝWmkÛHþ®_1çƒKNÅ ¥U%íq�kh9¸�cidm½ÒŠÝ•÷×wV«•¿(.Ç}9Hp²;¯Ï<3³N~ùøpûõŸÇOPÚJ¦Q>sþ¨È"d%jCöfÖÚâúí,æs+¬¤ô¥¨ÍJü‰h¾Z£lÑ UÃg²¥ÊM{É(‰{‹ •o�ýßScYÔX‘™wpŽ%V‰„+º™�Vxuúj–&1:ç¯'BŸÐç^³z™Dcnfå( ÿ\ó¿–�g*Ÿå$kÔ©Z|Kb÷Ü@²˜�OâE —�Γט{ ™l�}vr÷—'WIÜÅ‘Ä�‹5…[U«ÛÌÂK!öºÀ1+Xäï(‚Î+iQ/Á(Ù:qaK°% ¼F!Ó‰3¢‘�%ë€\e®ßÌ|x>¤d&ñ$ÑILB¦ 9G&·¨al ¥9gÒÛ ™jkËr‡fë|ÈaVмøÊ>h¶n/´ d(ŸÎYaŸ¨µXS�­°&T-àù7›$Râ¢^⡱¢By7\IÍM·nÞPƒÖ5·éó¤Æ8P5Ôm9Kïc{dÞ?&1U)ÔmÅþ3àK-ž@LCz�ž —ZmÌUÔ�ÒhÊEf#}™1"Um®‘À2P*™;:Þã}ˆ˜‘TXP-ÿ!¯û¶zyê…]JœÉ.›º)á«<¢mÕ�’9¨šFÖ/Vam.z³ópÌü[á’.F@‡«e•3 ´Xt¥ï�¹ƒmÝÕJV‘ÄQ =dž—ž@ýó1Ô#ûÑNÌTÕ´–˜�ÒЦ$M>Q?4ŽÉ^/ò”ž;îoJÁ¤ÍÐP`Ù`û�>�ýÚPÕØíøã¶pt‚5¹ú»pe„eF³?KKÆ`>.ŒÆDnØpÇ7¬ŒÇŽ­£!áš)à8ŠÊkM‹6¸uf2¦½¥¾Ìîš³óÍ„>ˆy�·N0ÏïÎ[Nò?YÞð° †Û0üO þy5m �øÎ=áÜô³Ù¹?Ïõhå¨óÍ利¤G\6à*/ ÁfZº’öûéz�Ãéj^¹RwÖ~r«=Ï$ àÒåžVõ2ýBijÓ(®FÜEL–RS1I÷zúuê2$2ªÏü z»cúÏöšÁ±ó'‰ÝHeÏ#¶“z¥o¨óñ©â¢OÂu2g@xÕ¨ 6š¿Å¸V’+n#:Ñ„À, y{. Ìþ‚Ü® ‹ˆn çLðon‚rã-WÎñª�ܳº ¹¬ ã5&—ƒ(t'ÃQÊBGÄHÔWMkàx"„ãünÑ+¦„$ 6  Î�À?ˆÄt(ô…uÙ¹Ò ,VëI6†WL�º­2P©Ÿ<Ès© ›4± Sj¨Ñ–©¼Œ<´e4u¥€!<öIÊžH«;ÇCmöûÈ~tD6m�»*k“3!z+W_Ì™6ªÍ (¸Bßâi)A¥¬·é .¡%®ÎØ‹"Bp#”˜©Kºʽ´òÅ � cé Øu¦R¨ü‘'\/µþ›ÿÆ˘þ¶Ïž÷yR�ÞOêŽã\³}%[�µ�9¡¤é űû.JÙ‚10õÒÐ4$uTÔU1ÆÝïhie8îdÏHåëtˆy°Öþl”l@„ÛÅ¥éý °ŽÑþµ%U–uüiTš˜ÂÙ÷y|q ]^ÑôÊ9ö,gÝáÿ‡aKí÷cÁ Cv×nPŒÁç@p�<ÀÐ=–É?…ðô:»uÏç…𱟢�¢xÖºêÅ!p÷)�쎆³Í‹ù¡£²ÛŽÈp!ŽÞB�ÞóÞˆE{½™ܺcn(ÞËígšŽßBàz=rK¾rSòzáIË&ãh×5̈Ð!;…è.Ó v?ÑÃ˸W~ÁTA@)ÙíIeìóÌ0Œ¯š8 ¨€º„gµ?äÞùSu8”F²Ôý( º?Pz„Ï ‚9zË�3'¢ßkíÖÉ M÷Ï!ï‘LûL·xó¢ºåÚ6u4õ!<^õ,YÁóÖ‘Žkwi:œ?£åÁÈ9qݹkÕ?ùòd#þ,h4�çÐnBóÂùBÓ/Áf%¼$ ÞÇÑ㾇;Ý ú ”oÝáþ»À±¯É,&WéÉÝ'@ý½¯Kƒû¶žØ xu÷§èø2¸ü¯å93+VX‚±Ÿ¡9¼- RÃWrã7³ÅÌ͈¡ÒÐÔ~†0Ü£|úk‚ÝŠÃ{X(±ÿ•$v?¼ü ¼‡=̇ �VŽ 2xœ­VÛNÛ@}÷WLóP@ XEBªÀX¢@¥ŠrQ…*õqc�ã-k¯»—„ü}g½^Ç$"µ(°;—3gÎÌ&ùpuùøëáJS‰4J²œ>*4 ²’)�æ|bMqøyÓ¹áF`zÅõiEÎëë–.k¸ESÊ\'±7‹’¸ 7“ùÊÿ”jC¦ÚðLŸÂ»aÈ>JÔ¬ÂóɈõÑÈù$Mbær�Áó¤¼ÇäØ@&˜Öç“ò M?‡ô¯¡°™ÌG)8…dÁT*g¿“Øýç�ÌÆŒ“x–Â~ëðìͧÞ=V›'7ßy89HâA7e —²ÖFÙ̃7ÁuŽ@è�¬�ôwmFT¼žG ¥°Î\Ã’›L‰]§»ˆ�)ÂÈ£Èù"°UéÓ‰æÁl2Öö÷˜ˆF©ðu 6g¤]B9 …TT-ªU°…LÚÚ�ÝvX`uÞS£YVö¤ƒsâ`:lŒ£#·fx­r Ác§ÖÁ†pÃx rÁ¬¶LÀ’­\˜ŒdO»Ê·Ù]Su~—é01m¢Þ –ç7;<Îìÿ¿>j¿úûÛ°êGÖüEžC�Ëž_Ь^ϯküz[o/XÏÕ;{Gö!Íì9³WÙ{ŸµYê�·IÚ åºR{²cºÈpó9>¨­XËñõw±�áµ±ú�͵ÒÀ7ßOÌo P²r¶ÑhW»Éîc9A´sh•Bzá\iGü@cUÝ>ì™ruKŒ„ù%§lCwßÚbÿÕð/ÿâ…5…�UŒ7xœµ—ßo›0Çßù+nyÚ´J•¦Š mm'UÛÒ=TSóhà–Œ�°É–ÿ~Ç %+“"E˜»¯Íç{œ�÷îîñöiûó“ ßñ첈þR4 „åÍzQ˜xùiáÒ¸áF ÇöõUˆˆËû=3\IÈr•anèžçÖ‘Žç6Š�Š¥þG_ŠÖ†‡ú¦(QŠã1�,Åõb$au+ m0çr÷µ�a9´ð=—•3^�­w ‰f»¢œ BÁ´^/’kˆ5ý–tiH1TúMÚmÎËd%oìràþ�h@k©R=7+å}8F;ERIêÔw#¾·Ó§zy½¨>Wv°�’`�¡Q9Ð�AÜL “‘@І<>”Â&A²ó8lj Ð�Q°C‰93x Z‰¢Õ+€!“ `šÑ#òØ –#d´t’gt%�ˬ0�2“ó?$ö”p}¤Åµ9•ÇRIqX5�ïÒóO1ÜÒßPد»ñsØüR¯ïp9*®Xá±²C›w©Û'ÆÊŽ|«ÙÛLˆãV ˜ñv/~ÑêKܨ3æôÃ*¼s-¦}óвH):´üC% ãÒò¯%Ú7(‚àpbµÓñ%&da2dÅœN<ȲLh9ß&˜p ž�W¬�þ;5|[­Ä5 ²¦«ÆÛŒg¥Û7!Z9óaþÁµ¦ù'0n"gÜ*õé6·¦•µP;2airÑ¥)Ó'œ*Tó=«û9—׈Ñq nâ¿é‹Yá}ý¶i–÷aÎÂÞécgaÈŸdÌà@Oq é·ØÅxÞ‘r“4¸Ã~‹oŤ’K‹{Ìg|—ýgê�™á)Sš‰ ��óQª¸¹wq…¬F!`ånª*ÄÎàþXo§³ÞÎÇz{ŽõÀžuYÇÎ1ËQ£4ýSi«HãŒNš*§ÈLÉHÓ1ÆyÅ¡9ñ?Oàþ<ðç!Òwe­ÎnåA¡sfŸ†ùeg¶�£ÑSjÎ9lnóÝæÖ߇Š¢òΆl�T�\JxœÅWmOÛ0þž_á…¯ÐhCHÓ”Fb…½hc �&ø49ñ•xKâÈvZm¿~çØyië0ØT&Aß�?w>Ÿ/ñ‹³ËÅÍÝÕ9ÉuY$AÜ=€2|” )Ér*èyØèåÑë0B¹æº€äŒ®8¨·¢)¯ÎW´h¨æ¢Š#«âÈÁ¤‚ý4 /¥ÑDiž©7dr:Ú1%-aNX…IQƒùjšꂸ&YA•š‡ù Y*ü;¡Æé™`°Á'N§¡R\®�GµMû#gKDú2M´ `§®Û( ’W÷D‰¢1@*°ó_u´JutZÈSbIM²hµÝR\ê&,ìJ ÉÍz½eЙ6 Ø©¡•ë¶| HÖÏ‚Þÿ›»ÁÌpK©Yß  4�ôÀJF�V¨ ;RèVÅȚ뜲CœD[Ÿ{k¨´œµæÐ,'ŒjZ ^éŠkÕ[o9�‚Ò}Í2!Yá‰Ã¦F/*,±Z Ö 7­¸kƒ»5ÛÄFëÇ U÷É5¡…˜€‘„`þç–ópÑoÕ̜ͳ‡]TÂd<2â�xæöya¶¥¡'v�hÁ+õƒ ’âÎþ²ÒaNê}R·»Î{ZoCoˆÂdc¸ pÍ‹\4 5lãø4aâ“ÚÊÒîS„'ÕV™…¨4&‘re%?Nú5'Îë7LФF]Ë L®z�Y‘ŽM-¥i[,µLbÍ\]z*öÁ„ÝlHŸwM•éñv�d–Ž+p†þV˜ž÷s#/­2Bª{ãÛåà¬þHu<ô±4r"–í¹ƒf8ªÏÈø«9àjÄÙ ¼±í+ÉÊÙì›åÇJÕxû&û�Ûg¬Ì]D«¦LAšÑPdöÍô‚+s…‰{ñqtªg‹Þ—¦¼LÈU;Ä=Þx3³��ذÜ7×ËZó’¸ÏÝ›��Óý—vkßõ ïbèéUöOñ6Ln}¤Î°³°íÔRȵ1£¨«è�¹#°ÃÎë.ˆ ;úçÛaŒ:éãdg=Ùz ¦´– ö˜ôIÍ«¿ FݨilÂfŸ� e¦Vš_Ÿã§Œ‘ ÖywŽ”qŒÛòŠÏÿ|‰}{Q?v̯ϋ+”oxt‹JPM¡Ỡݑûª‹ì'ão"� — Àý—ˆb�Z‘HzxœÅXmoÛ6þ®_qc¿&¶¢À0(w 4]°öi $Zâ,‘I9Í~ýޤ(Ë6ávƒêqdïåѽ�wξyûËúé÷‡¡1]›'Yx0Zá£c†BÙP¥™¹!ƒÙ\OR¤nZ–×]ŵQ¼ —"K=5ÉÒQº�Õ‹Õõm® 5ÈÊKýKáv’Q´c7äp“äYJ­†ïNl!)Éz([ªõ iÞÀFãß5. J•²bF³âDC�Ê_–öVW?ÓAkNtü“ƒ¹À*ñ|ß«�¾~C¼è-x›ÇFdñ'+ (Ö+¦™0èdCn` Ýо£ŠSÃ’=ŽRv½N€ ¨Àq-Ã2 ã :F…MwÃhîÊo&(îDÉÏ#¯;PQQ5ü5ôJöRYà:ÖÁô7Нž†L#« ¿¯6O)'еÔ*ü.»°Áv´ðÍ"M»¾eXpÑ<7I£ÂvІ©`&h쥥r©æo7:†6´�ÈèÙZ`D4S;êÞŒŽ.Þ{;Ä»VÌ!û’ø>sÓ8à.?PŸ@Ö¡4RÁFÉ.�­�Ïzª0×­¾ÜrR^û†o (¶bPQC�Êh«%æQ)U…1²¶0£0y´Í#»D3ô�¢Îs¢XéHJ°êÅ67dí}Š9°²õÿjÂAæ�l%:Å)Ö€¯ÊµÆ:l¬Çæu>é<ôÔ6·æÓÎÉ&‚ÕŒ ^ÛsÅf·}ª<3ÕXÇ_¨òÕáöªˆ« º¯s«?SRûúñôm.cØ µvÎü¬@—)\¯æS€÷'ÇÁÒèÞΰH~L‰á²ô�çG7Ï¢è> Ýä)Mòƒe×ÐLYdáP/gü‹cûnmZ�G¶Û³-�h!‡Ÿ^zfsl¾Ž�úIª.„±¼D-ùx�݈2+Ò²†~6hÔÓíræl;=ËIn¹XþËú«-ŽY®$Ü íTcŠ<^fE>8©Tsï]¾(�%Ucym.à„1ieÉ6¸œíè,vN³²È™÷ú²|D‹ü½$¥jÁŒYκWÐ2­U³ÿŠ-Ó¥ªþ+rÿ,¡¨Ê=gU gAí.*=‹FµpÆîH®ßñ$9/ò­Èû?Œd�¼ù>.ú›ÓÑþ¾¿;‚÷—C\ßÒ ^�‚)a¥¤±ÚÕØÞ¤P\$±U€Qˆ²FÍåŒΫp†ÎØaÍzGo‘Šߦœ7fþjãÆ|Ü…ðË3��Fë4¡cp¤²ƒVJ±V²åk§±ÙÉ–2ʦ)·NÖ–鱿-'õêþ�‚¯EÃèEo˜Õü.ƒiÐ*4ÁÕ¦ÃIk€Ëƒ´R)©nz� Rë�5Ã�ó\"xªë›”áÞ»MEÎ)ŽÆ5Ê:b#¢1>Ý5JÔ>ݳ[æ„…ÏŸŸûˆänâòé[‚ÃÈúéƒï$¢/Íë@&@Ù�•ê˜l3°ºÆÞ5 ú&$��чhæ+ºZð™L��ú”ròP4¯ÂLN~zÈy¢àóv†º»µ¯Y2¾L2'9ul3:åŒë{¥)™c�n‘麋<;A>²A�€¢nn¨™×—46tl³äe-TÅÄ »»½�Pe‡Û–kcw/S*�yáæ~^â;¤ª(¹.o‘Ú$Œ"ÒÉQ@ ¢£±YÎVã3XøÅõ—$ÃܹNO~[<ƒ#¶+ͽÚÁþtVîKâÞ ã�‰œ\W¬i®On)yt9ýõF_ãFo� Øš$þ׈8¼:.=‰ØG)ÒÛ±`™‚iÍî'æž+ü}—â|îhp]OO=Ç�;Ï"Æ*ÍÖ8éF‘t¾bZ(â C³Dô)›‘MdD$îIá+¥âßó.}-~¤c½PödÇüåO}NTe49lÌï®ÞnÚý RÂGRô‹,ÎÎPÏGûìÖ(œÕéA ós[zg.´#MõA¢'¦}X)±Ä|½ ¹A­©Ë}cœž¡´~¯y7“Þg>¶l˜&KÜò€ß,`Ü<¡ùq¹zKvGƒy~¯÷ùð=šÇOÝo‘¸Š<�X”|³xœµ™moÛ6ÇßóSÜü¦0ÇX�CáèRo(Ú$ES,ÉKZ¢-)�”“|û%R–hÇ• p,�äÿw<ååoŸn¯~<~[Cb²tE–þƒÑ?2f(D Uš™ËYa¶ó¿f |n¸IÙê_š¯÷4-¨áR@®dΔáL/U;Y.œŸ�Œ_¬×?WÚ µ6<Ò Ü É’‚ »œuÌ.þž­– j�½[´`¿wØ�C”R­/gÉ{Øjü›ãWƒÝ"Lúƒõ–›:Ø óÒl¹È­«ÜÙ†)�[Pl˃˜ŠŠhRYÅ|ï‡Ìôüý¬êør©¹á{\¶C':gß¾p±ƒD>AFÅË€W ŠÁŽ ¦¨a1#!’Y^&aÀžÑ 6@*wðÄMÂÅ!BŠs²ðc‘4~úd|`”*�€a£˜FV¤=Ê'‘¨‚m!¢2ö*┵ùX¥ãfª Åäè•·D*Å­©’…á‚ixK‘]±ÜÒÙàüËùæÖ�›ûï(ý}ÂP¢È°䧃 t‘ç)Ç¾ÜØ¯6jT®˜õf=£0ÌB\Ï�¸þã™~÷�huU|SاAÀƒÖciœöÑo6M˶¨c;æÏŒTÅ£WÃ޾ß�d`û"‰;)•ÅdÊ,JŒ—˜¹ˆx“Gô�[`äÿ¼—�ù¶ñLïUQrÍL"ã ʶÑX‚]_}pU»Ý±h`•Ô,EñAæ†g4…/ç2;زαçä¼Û7B¹ï\Bw{×§dVíhõ udMl—Ê M¯éóݺMÔ7o¹Ò¦Ó:À”œ u퀯rw„Ú6 µë«uýªéÙƒtá6¦\x®­] L[’x"Âö�H<$›lƒ ¦PT8-ªpZ™ïœãê K#%µ®ÂÂÆÃÛ},У §w»ÑLí)8é‡pl‡†œÁ¦!ƒÿ[KÌÒµ .ÈèL¹çÇõ謸Ì…x€ÇTÅÀ”’eBj—v÷£d×=ß�Ò±‚AFìödŒäæx sí£Å7ÁÄUˆ–Ëå ÿk(tüä3R'´zwÝŽ$œ«ºxýÇUÀA}ÓWñpÝ®0˜°ó!’!§}:�Ðt'˸ü‰"Ÿž*ó›Rž4õJ½SÐ2-kB{|6QÜaYn‰lIм¼Ý¹{„­êÖ˜Öð2ˆeæóp½æ7ØæÚ¶ši×ßÀ=Ÿ×y¨¹õG¾ßÈÂO´Ü×>kˆn@Æ2¾Ö®ø¯L®§•¯ì¦ÔÞ{ØKµ$.õ�Ikî(oª"¼ëv±’{Æ0ŠEÿ€ìãUÿ,t•­¿oL&кí¬/óW¼8Ô%Nïž1Jôô¨ë:Âãñ¥Ð5×G êéÚ'³öÔWÒ5½.\ñ¼ä–/þUŠˆñ«±ÝŸŽ‘W¡´Ê¿¼ªmÙè’®—š@ÖÞV‹¸¡7­÷7#Õ½)²ÛÖt‚*ØM vÏã±—�mÉŽ«/†^ov£TH1÷Ò†X�ð x¦Â·ÕM?œ¼Áš6®nWî�ƹ‰ (¨ :„ Õõň ž¯“éúxJ×Çét}<¦ëÀyr^†U,WL3aú•`íŸS¬î¤BË\ŠXc9ANÐ/õCP㇠Ä}|›nóaY/Ù»U¿NÒn&­ß œ*DŽI´p¿-ªß¦þ|Â4Å ¶ !¶Œg�\™RbxœÕY[oÛ6~ׯ8ËÃÐv�(0tŽ�.í†M4ÅÖaØ%QcRTEÊŽûëwIÝ|‹Ó¤7 ˆ-^Îõ;�èéO/ÿ<ÿÏå+È­’³hÚ|p–â‡â–A’³Êp{vRÛlüËÉÇ­°’Ïæ*ÆV"®­Ð\p›ëÔL'~6šN‚”X§k’y:3–YÜ"óöíÆeÑ”AÁ?;.z2|<™M'Œ?�\‡2Ÿâ²ÉŒ9;ÉŸAfps¢ÓM'žÃtɪ™Ž¯§úg0�g›â∡Ë%¾ÆTC\ i y9Êk‚7AVØŽ$i¶ÿðÀÉ�'ÆQl™ô¹ÒIj ²%ÒÀ€ö±_­0sƒ2«‹Ä}q ¿/5~è•LŰè3(µpø´!ü¸�5Â`6–˜(dê4Ð#í¹ IâC´qÈ 6qì™wÚTvŸ¹É(Iî¶Ð¡?=ŽW£�zü¹2kýë^A†(Caq­„õ1Ù&$ò®b‚àËvº…¾ õnŽD¬¬ ÖÏÔúwïuDz3Àl#玸=Já¿C�A`!õ\ú‘ÿ¾#ÂÈåwkRxÂé’æ¡­¼�èîomºxÛgð™p^„ÊŽ ¯–�µüØu$÷&ÇžŽý�at€õ¶IïÙNêœ+¾=ñ”Öömu‘¢$OǵqJKÔ“ÜͶ¡«ë]Š6ô:Â"ØÇ5ÜÛËdçGÓáÜX¶Ç|ëK1od`FŠ{#ô²'°±f18p(ùMó ),v:>¼‰–µ*†�/¶«\Nq7M+‡3�Â>Þn;F"r ócsÕ´ÿí±Ê #°çhOŽ.÷�¦‹¾’hÛ5ö6³Õ~–'¸Æ3d2O˜HÁdÔ͹JâÌ^í½ Äºž^#ÀÝ€’»)ì2ª($À´ %_áNÜÅÈ*§ú à)Ãáú˜Ž@!ï3)òýUØé·zœÒ¯,åä8«nølbÊ·±t�%b:ÎðRÑŠ­ˆlÓÈ„»µ–ŠÛ?ö°³D¦lKŽ]E¨ÒºÌóŒ#½†°Œ'NFûk3 #©$·Ùà¨}Ý =„Öh"šÀjtKÕ¾™…Ep’Hpbì,å±/¡sÍô‘ÊÞ;®ú@ðÙg`³"þ`ÏÖ0EtÊpŽ¢�µ lil¤ê˜®Á©­W àhµŽkÖ”tLrÂHÐ%·—ë Ž—þ[¢Ü—ÏVÂaôɿݶ±9Ƕ§Ä5&±bh‚SEô¶ayÇ�M—ìiJ\ C!f9âæ,“2�"/u;¬JTP|Ãe±½)Ð|ø–G•}�xpM/å;ý;‡\ýƒÎ„Øýµ’< ~Z3ãýrYînL�» èq7¬ÇÙS×ü ENkx‹úHD¨²kSiäJSµu»Ôø&óBÝÆe;«­º—�œÒd¬0…� Ô¤i<­á“bºÁ¯Nb’²¥eb˜%�Ì©ò錙̿jbþAíüÑÉgæM8ê1ÅÏ2ý\Ä‘nm(üü¶mu ;bb*^êŶ ƒ0«÷bj“ø3Ù5¬C#þ64,ùCu´Æ;í&ì­v­¯U3Z[ÄH¹ùÁ~ûMüMK<6ì?÷<ßH^ŽtkÁg“=ˆÙ§¼$Iü ÓùV<:!…�¢¶ˆ÷M±ùµîµÝFF°¡´°t-iB×L`ü¹šÎÃÇå¹ÿvý?/ܧ Ç ùÈLJ}�_�~ lxœ­Vm�›Fþί˜R5±%Û´'E�.©º»Õ¥ÉI©*õãš›m–ì.¾»ß0¶9'Š"Ù–gfžy'ýéöãÍßÿ>ÜAáK�EiA!éR¢�Â:ôë¸ñÛåÛ8¡s¯¼Æì“Ò…iÐ{¼Û ݯL¡/Œti0Qštº6F>³æß2ç ê¼ÊÝ5\ÖAà(P‰×ñt5ugi"ØÔÕ$ÅI²uE"5äZ8·Ž‹7°u¤(7rÚÑkH÷Âffó_šð ¬!ÝL"Ód“Á¬E?ì"Èæºqþèäþ½êOæiÒÚN“úŒý–ôè$¿Wj­êw“ò#jÿ'©epcQx/ÓéD€ø`îÁÀð¡ ZUí"pF7 '�)¨üÆ�ƒÆl$e[*gñ³G ¾À4…̶‚ª)7hÝYÀWQðGª}ìÒ-ßÄtÔpKh•EÇd”QAŠå€¾ˆ³¯¼*ѽ{H,3¶GTs —V=1³qh÷"ø9³æÑÍI‘Œ ¶(Uî ±›å‰²rs¦E¦GæÇn2p¾ u _Èé‰Í8žø$ÊZSLBž^.QTîu—›E¬UõYìðpN¢4H¥)aÛTyÛ¤QÖ8ÁoÓÀo�ì{^§¼Ú#¨Ê㎳àjÌÕöù„x4d‰ EU ò½¡„³Ñ–ÝÇš‰Ý·%—B-”u×ç©ôb£qiÑÕtbÀimÏŽ`Ðe÷´1V¢]ÙÊòT;V^[U ûLMé-ý xTÒëøê×_è츩h<cÌïo{̨Ï(ñá.LÑVµ¤�däÛšèA\\”¹ÅiNg¼¯ÛZr…®nq+í¯ûbq_°Éµ’TI½åU0ÍÄ/¾ ¹~°Š þ"ë;EîÛÞ"–µ"bB÷2Ìd`5gâ=ìKs€QJÚ|ñ HWK)åÜT»ìÒÓÎ�IwÐ*,n×ñƒ6ÞS±®xOþì†Ag‡{^> KÝ íäxôu•N;füô¢ì�U¬�¶W«áô4ÎNO lÛ ¸yy� )ï¿¶73¹'¿•Ã~Þöûð……ó‡”4W*|<™ê£qÁ,.‡5sH´£ÂEíŽ!%úˆÊÓqa#|KHkª•¯…”1ßóé±É‚äyä~ÐwC1ÎÒ7ÚÊ€Û£�ã£`ãỎÆ/5饸Žuä})wù‚™ ¥N‹i¤�çNPÆ}ŸöC;d¼ý³GÜw´çm°©m„ê¤@iÖõ † Ä¢olE5ÃpñD{-ÌÊAÇhuÜpI÷É›„�êÿ;ìæ.ˆ-�^�^IÎ6ÿ¾#[²�ĉ³­!`"Í|ßÌHròLJÇûoÏ_>BaK±Š’ð@–Ñ£DË -˜6hïµÝ,ÿZÄ4n¹¸úÊE¡j´?n™¨™åJB¥U…Úr4IÜšEIìáÖ*Û9ð?WÆ’µ±<5ïa†ì£„�d%Þ-Ƭo?“).VIÌÜ oGƒëìó-™U� fÌÝ¢xC¿%ýµ’ªlüõÞ‡Àv�¬W=Üšnœ’¸rÀ+sP"3µÆ¨�Éø6,Zšå»Ekü7«¹ÌÝ;ºƒ¬w§™ô††6µLn &3�` f!UeU[4Qçan@i2—u‰ä [L­0.ô¬N1ƒ5EßD[9'ù-À·‚P÷†Ü€&á–JŠÝ­�?¦˜Rã^ÔÆ¢þ¢¹ÒfJ’=ã™t9À<Ç�Y´|íg%úZaÊ7”�P¨šo–·(ÀtËs�y›ÃÄŸ—'#r¿3Á³\ÈÂ4z Þ`YqЉ7>L¸ÊpÃja¯�"ëÌþ¯{“ÙÅêç.Ulà1¯l{ÀÇÚõӞˉÊJQ¢[³¼H´#K @�%JKbŠÌP)5u*‡J6à 6Që৪­œoQ†!W°W©ÒM¥dæþZEÊ—†’†Ô‡Ï×7Õ­šl™^ñ$v§ UJè-[Ÿ‚ïç«¡ñuH‘öÕo£& fËŠýße.Ì‚�ÇEYàÝî5wÎ'2`ôdõ6�R䊠Šr* hc©Ó/­G:ªij|×(HÑëõ AmHP8G‰šY<0%™Eí@ Õè'jДkJº²ròP6�2oZBE¡<5ë&¨w@Éhøá ~¨tô‹RêÈfRå¡ñïãkëÆ]8–°?¤Áïµ:H*ðf'©_O‹4"Ãoªð½)ä‹uhÍçT" ŽÔYGÐE�öàÀ‘*i—�y¿ï…ªi!û"GE60l»CfÓ ï±Sô÷–30?;&ý_:‘… %F×í–ãiø= ÄYè.û³Ùv•ÿ¸1´ø»Þlj;¤c^ýÔe©,Tîd�GN»zJ±’ûKÁ)+ÕÚ Þ²¶osÙ0š1º+µÍ:A¹·*\µ/ñÀ|x׳%óC]>¢šâüÀ|î�Gš{—tCÏká¶A‹9·òL*¹ DŸR&:½�þß�•å%“­#ØÍÀpuL­Ÿ{uË8I¯: kæöKÕ��îÒs³ü|!ËÏó±ü|Žå‘]éu�YcE׺ Ÿ5;ÄpÁ º¦¨hB›ÙˆšbüiªŸF?œ¸>ÚœÈÜ!`p¿ŒàýèÍN€ò�#,ö߯âö#ÙOyñ²Š�]” µxœ½X[oÛ6~ׯàÔ‡¶@ba+ …" sº zI€Eó4Ð"q£E�¤œz¿~爤DÛtÒns€Ä¶ÈÃs¾sûHªüîâr~s{õ–4v%«¬ _œ2øZqKIÝPm¸=Ë{»<ý1/`Ü +yu-d£zn-»¦²§V¨¶,Ü\V^ÇB± jü¾2DŒµyCÒkA(+)i銟å)‘¼* ŠÚ~8`&²²#µ¤ÆœåÍk²4ðw �ÖÖŠñ-儤ˢC�U„˜ðQ�,•c½±\‹ö.sÒL¬‚•9}�;7 'AÚä0åÖ±ê$_ñÖB[¢ðÚ«‚ežEF‰Q²G†ôŸ-2ÚZ TíŒ�óì+ xsl{é¹36š¶%3°H-©Õªë-7±mÔ #ÛQŠÓ–Ã%(1;# n,iûÕ‚k¢–aóDµœÜ Û �¸kPœ®¹¦w±Ï™3<ËÆìÍ5‡°�}…û‹ÍÎ5 €Ä—}[¥>"*—À¼³º¯­ÒŠ÷PwèV×/¤¨I§Uǵ³Ž×b c�äG!¾ u&‡ ÓšÓÿÀZc¶ò:ÜçX¥mÍÇŠö_ ¬"ç.Áó‚ú^ìCòü€0¼<�à�¾6�8ÉOÐgŠé‚¼ÖtŽþb®4·½n9Ã(:*÷¦¡o1: î7êžtTè{a ؤ]óPWlˆYÂ=_œWP˜cVR>"�A„ °qÎp;|w}¾Ã´:!‰µ#S: »£y‚Q÷4AK¶æOñ+T킚¿'ð:΃öƒsûv.è:ð'ÕKHî‘“yu`b_ý/´ÛUº5”W[�n{VÀ6ê¶ú9Ô)î…~{o^U£úKþ>ñê`0¯®ÆÔj^á!†.†ƒŠÕUi™o¥oRü,%4 t›GÄK§VDSð1q-5½æn¢0ÿ?¢-†Ìw sÛ<æ)ÇõÐ O€0b°<ÉjûX¯w‰ë `B0~ö;á3{ ¤HtTÞ)hûfuT¤�X> ãÇ>Ç“ 2jt¼­'~z¬Ÿ†Fh?ùŒ&â9î ÇÏúoa«{—WÓïªp¾Ã(îb�‰ñƒ0xXÍ+ÿ#…ÎO=AÄ>ö«ËéÔÝHVà/µ%y<”—�+*!«áW —Ÿ{â|z«·#¶Û‡°%NVÇ÷9¯>'÷4j)žyÙÎ…/ÆR„m÷Ñ].!�ba ÿàžþÛþ«L»–~é�¾‹¥Xé[o™ñ¥#z�]‘1” [®©®¾”~ù«i”w皆½ ‚c±;ºœŒY:%"O,pÓHF¹Òo»iaè,að;Clpɦˆ»×@ü0ì:ÏXT\!ã"€U^êÔ͵ÌÓ6ž)žRµF©…ß™™øxðêÍO�ÎE*(,nŽyós9æ÷:z?hlü7ÏtÎtÔ6í³ç¬È•çUÍ™¨9Ö-qÖN¨!Ñ ùœ#:Š-ÓÐ`¯–Hfß 6ZŒ†[S´z’rq�ºæå“—³aqž‹ÐíVŒŒJ؈�³"í¹Þj£®+g¶ Ëš˜;ȳL*·} zäõ`íér':”dRs›±D‡4¡ª€g]T,a7HWÖ {ç¼ »ªU¸Ž=ä‘"YÛ:+¹®³C Ø _§C\žˆz}9/&¾óÒÚ~kïÕiÌfkTܲöj?|®öî BÏ”B°u0Iâ9 kd¸åš>p’%,…¿Úf—}^ á¢Me8[ôm-+˜³¹XÁmƒ‰¨ŠøWlý»2kv`ì¿|¤ûÒ¯“¤@¼ø\2ÒžxžÊ徇f€–�æè)Ú~„qÕ*<¦Ò=! %îr_ëÞ m¥ÎÖ9�‘ÜBÎÛ³¶¶´ :ø² º±ãNûd= ý¤³pE5—�; ˜]ßêËfäã’)…}ö™Tô¤gJÿ“Ë#MäJdR E ÁüÄ!rä‘óvKǺ+Ú'—eÖ–�3%3¦ gš\3–YKôjy^ƒ¢2Ö$’vKÏO>}<ù¥Ù\ÑŒ¤XL-ÒDKèµqq+ 6VlÞ+a­Øþ±OÇöI 0›ö.¼æúë¢Ð\Ú›»Â7˜V_÷ž¬¨X°rzãbo¾ ª†`å¸ÃÉ'Ï.=ßp|—ŒouGõþ‡+FÝ´´Ì™¿úÓ«ê†õJLWŠ©˜ñåqDý#»s­ÿ+G]ØRëhÓ…&ÝÐõŽÓì„¶z6’ü“3µUr´’}4ÿ­ÑW¡ï· п½B{@\Ý<þq½ÿéÌÚ‚‰ÚÌ—¶‘Í}?ßu&± 5÷¹8£ômŠéîøãº¤wk‡l€[z<¶áû„io¢õ ~5[àQ`Ðt½ÚGèÛ•î“нEèV«†Ç­Wxýo:‹œòÛö¤š¿‘jn•ï³�šgGwÑÈOùv‰|%•yï©íÁºOÃV‹+g뮈«S±!ÖnÓrÅ_ð�ºÛMmU¿£h“·}±)kÒÏ&ï~·òëA¯o“^ãâ?Æþ?þ¤'�†W�a�2{xœ­VÁnÜ6½ë+¦{hlÀ^¡ެKì½´âƒ{¤ÈÑŠ�D "¥õþ}EQNÖë P0 ‹œyófæÍh‹_îÿþüôÏã5¾kˬH �޽ ÙˆÁ±¿ÛŒ¾¾þ}“ãÜkßrùðÒˆÑy=ñƒlx ~°=^³+òh”ùVYuп•Î ¯á(Ý-ýÖY!ȈŽï6om·Ï›²ÈE€½9Ágû „Üö$[áÜݦùHµÃß5^=|¥U§¹M$TT%P*„™m‹¼x%=Zm<)áEÏ”žR”Î]Ü›]‘ëYêZ³"áH*¸[=„o6å_¿zݱûôXäÜ•dÆŽ- —ƒ~¡=ˆ1±� vŸ‘žAlåx˜PPkðªâ½´íؙقj~xKO Þ×¼æÿ59›ý¤0Æzª˜:«–µG;lõ/KOrà9È6[Kp�¬ÈÖ1еtÚìZ^Š‚8o‡€….VW‘6í2p@Mp¯`r ëƒRâ Òš ¬áêíHÙ±Z%R9šp^0÷ž0’Ïë&Yýù¼‚½UQº#Œ2>+¥#[êQÛ!ÕÕ+l]!C'´–¼Ým¯²¥„x”­V,̇…×Uºé´ùj÷î«N7—wˆi4r–Zƒ†¶Ú!2’Hßv¤Ádµ¥{®ÅØúÐÃ÷bn‰¾Œ}oC{³%7�oõÎÐ^û†|cÍD„>„¹ù_�D9€‡w5ýiøñŒúduV O Jèy«I´#‡9Äì¼ö`"Å=å] Ž[ˆ!Ô:r•gœÙE]ËN aÌ_Û¶µ{ÌîíÂo Ÿ‹V—ôôñžx0ÛÔ[§C�ÈIÑŠ�ø¥·†±/Ô¢‘›ËO‰ÓŸ¦NtvðqsìÏ WÔvZ“ ÝGìãøî„®¡ k ïÄÌa$Ø ªE8qYX¶² '!$Qų]ܨîU†Ï‰àÊ^v(›P[Å“ŽKÎß{\¾C» *cl¥ÝÉÂ!Ž6Ú3¸LbÐqÌŸ†#Ö vª{ü;ºñ ptQ2…Çå)¢]-‡uwtT,㟦Þ%·<ˆc�Í|ù�Çßÿž浆�`Œ> AxœµVMoÛ0 ½ûWpîu�±†Áѥ逡X =¬§B‘éX›-y’Ü®ÿ~”e;Îìôc[�Žh>ò‘ɤoV_Ï®oÖçP¸ªdQÚ?�gô¨Ðq7Ý2n\~ü!NHî¤+‘�ÿ*xc�¼Ã+äFhÒ$¼‰Ò¤³°ÑÙƒ·÷ŽYÇ�$ma?Â’T¢”ƒâ.ã©BÌÒ„{Kïgý’8Jk%·v§�[úÓÑRè ÷¤›Y+rÒê¦Iíí±SP¤†ÖÑSn‹�6`;\´3y×û¯ìñi \ÁÿœÃöE@�¬ê+TÎW€;ÇÁO¼Üj#]QANÞ't~6h$ÚÀgÖiz Îp©¤ÚBÆ©’\eئ®µ!'wÜHÝXÈ()\ ª¶¡ì�Mˆ{ò®@i æ†ªâЦlÈ,ù� FCÏÊ[ŸãK<|ìù( ¡«ºqh#ïgÇ#7ºä¤êCz€ZKåÀiB–­f+ „©ö´c䃄œ YJª7qå‡ �õ¨PœJi_R#c†g’R40\mq²ˆ†[s£>)&e­±Ø2}Þ=ZYgAô,uHa�BR¶×Ï%œÔÊç&¼ó•xÊ‹Þ|GáFlSrGgWHøÒjj�¤EÔ“…Á|_†4Þ^vi´ ?,ŽzRñˆgoa ½X]ˆܾ,fûç©�BÙû} cIÌÆ§)úù¡ìñø<<ºñþ}ðƒªÍnBC! ­3­œïínT'l0?-Õmm4µ—óíì]Ål=¼u2râÇ0ß´ÃÖ–º¬s/0{4UY|‹Ù·à¢„Þ0}×mú ¢„œþ_Ï«®ÿcÖÿšã±ÚŸV¯IfÝ�½Àh8>J«Þií˜%}¥ž(; �õ5ÿNÿRð±Á¹0çöìÜä˜|öÂy3»³¢~‡ÚÅ_ò©ðµ÷ŸÁ|’´4Ú �հù#ØEš`Å&Äç—�ŸÃ£•õJA=6vfóksGÜïu"ÏÁÖ(d.1뷜γnpÒý£KÂ?Åßœ ›  ¨v Œi�e™VêxœíXmoãDþî_1A{Rš´•�ŽÔRiA ®¡ROp'„ÐÆÞÄKm¯Ù]'ÍýzžÝµ;N“œ�ZÅ/³³óòÌ̳ž|vûÓÍÛ÷÷ßRb²4 &õg1~2nE Sš›«Aiæg¯c<7¤h8K™ò%b’yº¶ÕT÷²^6ZŽö"5 Úá¸]çÍ®$çÕÂòy­l´ít“ðÙµµlìEÜ}Yß]¿}sý Š•ŠJ^)‰ŽSuÌR®ø¹SS™ç|ÁlXjÐÀfÛâæÌ>Aó@ω’ªéÙF4ãN®j.$òN[ë68"²z†z@Ó0ÿô]ÝYŽŒÂ�\î�Á}… ÛÌÑò1¢" 7„¯5×mÿ•d,,s9zO`ÛLJàŽ=MËì À«ï¹šâÙþBb˜ÕeÖò³ÖÀB�'9–ZŸm0¾Ÿ>üB ÅŠ¤]dxkø”©íÁÅWGÚûÖßnćý–ZkŸ5$^c–`@F˜{S…ž"Ú—Ê&Ι¸á˜Aí½ß¦š ÀŸwùf—Ž0ôæöê©çL¿Oë4x�yî¹Ó l.�^9 —ƒ²ÖWG/õ}ÐtvK•ÝÐöqÙͯ[Æî&×-ŽfÖ¿ºªñS‡…Üú»ß<ûmém¨oÃrªïÿ)íþĆüNþ�ßsíi‹aëºT»œÖHú³äj�ø�bß½l\<.ÎÍ€sdö‹³±»‰£±õ€Ô�ëÊßvš‚æ 4ªÉ6üå –9w›ŽÝ¼aõÛœ}ó‚f•Õ[·òªåWá+íá6áöALècNKV o¼ï.ß>#Üí;#Øð´3\�v5ªåî8â&Ý�1× kÏâMâÐk çÉN�ÜgR¦w欧üv—êH*8ˆ¶k¨Ô‹Ç6%Ä]ûlLÓ[;£\<ªüIJDŸofpwk±¯§ºEN#ï©$DZë�Pª^^wè:¤­�LDLó,aµ²NgEi4ýZÂ/Ú6ÔŒœÏ{EÒÚ·š÷Ž�ŸªÈû=,8|Hdq,ìlf)ÉÂc—‚ýJ´¬èǽTèºÇeªÆµîÑÖ`Gù¡;‚ýÆ]¾f�3äÃŽ“Ui–ûn½é��;G�ë^zåbì¾® ÛΈfC‹ßÈa¸¶ØÎ%eÒþ¶­ó£âØ“�Ãç1 ò`Øp\l‘̆\zné¨ñv(ݯº([ 8¨©¨UTUP¡$*Z#Ð=¦Ÿ±':½8¯Êè¦[0›^Þzi×߸‰ö+Ø|œ2”rŸîV¶ÃÔyªAÑÚØ}—²X¨p´¾£µ�ø/ÓëC4w\}xûOÚ@¹»ˆ.�d�` MxœµWÛnã6}×WLýÐM€ÄÞ °E‘õ(6[l�6 �M)rd±‘H�¤ä¸_ßCê’›m I ,à•8œË9gf”åw¿¹ùóê+•¡®VÙrüa¡ðSs$Ká<‡Ï³6§?Îxt¨xU¿¹fádÉŽgvA³_.úãl¹ÜäVm£Ó+DÐ>héÏiïuØeKAFÔüyöØj~�»ÂHž­– =ž­vžÃÇŽ’•ðN>Ráñï�—¥UOÓ?ØÒ2_=¸É(/Mt¸¢ñŒ€�Ó2ë”îÆXµ?ý8ÛiK­gE…uäû°þ„|ÃRï…§k!éXXñ|=?ɨOù·²ÒŠ…y7äu2žÔÚÜÙ�¿Ó/N¤Û¼²òn<9žÓ¢­BFÚïu=§ë¶i¬ H®/Vzmh£CI¡´žI›1·F¡Ú³é.Ý”ð=¡šþ¯;˜„Q$…16Ph¬ê/PvF6ÿ •“t ¥X3^áCª¸nH’Ý!iLFÿ�>ù: ’f´:¨“›’)l›šNT-“-0“záˆ7l”'k’…ç x±ixÐnz¥7€f;º/lUÙ�6ëó!¿66¥Wô3´·_Y‚ëuÐâKQ G|ßXÃ&Бê¥EgÇŸÆœ~1ŘÎw|Šý¥ä| l»©ÈÈ5b?�ïwˆ:°ÆðZ¤†^AQ1”C…ˆo|Fµ²ŒobHÓÖ9';i«¶6ò¢ÜŽ NùC±1-%°UÜé¤Çxùé�ã=i×¢2ÆæÚïq´Ñèi;át?'Ⱥ–µÒ5�Øþeð'éöGpGGK¸ŒFñçxW¢_ëZŽ³È†8‡MQƒ6_ÑÆ¯éâßÄýe[ÿ ±ù+v—Èx_+¿´|c?ïrø²©/©Ú“!Q’JÆ: YB˜Šv;¢éº­©Ñ°^—9ôJH)9ˆÂŒÜ»¼þƒÖN4åœ~* õýc6%Ña¼'2ú¥BBÊ;d‹ø‚)ÛæS gúX ö‹þ¯†ô4ª†T�c�, ôxœ­VMoÔ0½çW é$º J¥¹´E è²Ò®=UN2Ù˜&v°'¥å×3Ž“%Ù´¥ J[¹{Þ¼ùtâg§_N6«3(©®’ 9/5’€¬Æ"‡-‡oÈå$©Â¤Töç…ÉJ4qäeAõº©ÎoÒ«Ä’ iIföLuø0ˆ(Qãq8> “8NûõžqY%¬e•#(,ÿyK¬“é'æâtO?eàîV5)��;•™¨`)®åV¤ºU_µ©rxþa¹þú£ %^å¶LµÛÃz:‹ÀcæòzàWÛãЛٔžß”P'ò÷AÖM…5*²@|ÝYQmµ‘TÖP°E, ™I¾ÌÙühÑH´ €�–4Ÿ!•T[ÈgR¨lÛ4Ú°…ka¤nm9K¨ �³m8j�¡ /µSí½tÀ›9«´•UnAP·ÉJÜr<¶F4%s0mF­AöF r�exΈ¦1úFÖ‚pæI°3 é-‡K 9&TÝnË.8� ïk!2YIÎ:+̱ú°ô˜ÐZ‡ä3q¥”—öi2ÈéºÐ-dB²“ìPk±³ú· fZy¯9„üc• ×É 2Ae‡ûE«2’Ziðgœžðuú31ŒÙ�VÛd�¬YYÍÅõ¢€»ª4X‡K�Ëe»p-~0Ð GÄDòƪg7¥h¹‘®q ãuçò0™ËæxŸN7÷°¦²0™îç÷¸³Ëd8Nª› ]¸"îI?MN´"×`ý$)ß$;èqÔ/¹:¹Èõ“3&«�Àá²ú7]E»Õ$1åýüyàÁøpqÚ·`˜ ÿy#ý¤rÐüw:mT/�ØöÓX Ãs˜\&Û©4n=›sq³lëÏR]Ùš%£„É\v/¾’r»êÂu¢Â®Ãlß_¹Ÿ ‚‡€êÔŸŽðÆ�Ù5ÒZþbªãÝÃ$íŠQòó?·ìSÒû&ßî"²ÒRQ÷4Œ­ECUßÛœðRçCgœûÝÿµÅjJzú90‰sWN=B»*xÜó³øÇ$ÜïÌ}óiß�÷’Ÿh÷ÐÌê¤ê¦/:?î}»åJâTN ïòõŸb‘ÿ¸û PÑ� ) Œ I)Ž�hœ<¶xœíYoÛ6ý_Ÿ‚ó°f’¸-P h]²A–,@‚aÁ0 ´DÙ\$Ò%);î§ß#)Ê”e;Nšu( ©,éîx÷îíÁw'¿_ß\þL&¦,†É \Íp)™¡$�P¥™9êU&?x×ëã¹á¦`ó“kÅØ£*�0EΙ™ÈLúþm2è×VF2[X›¯‡ÚPõá©~O6iC,P"hÉŽzm¡Ãömo8èSkøÍp«l¾�Ø”¤Õú¨7yKr åTf«A¼'ƒUC9úgзÈŒ†«æFCò£“ûÃK½ô�­AÚYÿpkžr½}¯e!j=˜Ñ¢bk]’cŨa„’unÔÂ~°Ô�\*"ðŠiƒ+OFx ½¬>L¼ÍŒÏB ¥>xÛóËxë� V’½=’J¡�ªR£ MvñsÎÍ„E¹àbL2Šb� “JÛçfÂ’±œV…©­î±*-xÆ¨Ø &3Ô%)#¨yÅÓö¥²#À'à+›À©™ô†/ /™þp9è³rHDU2X x©øÝ>™ÃqF”œk¢Ø 2a�fj†n@̰š!ø¢*…N"™«ÔñÇÁû€bI- ëgŠpeÉ?;ÿ ’ÕxâZòÀ©‘)åJ¿ïV‚¡£‚Àç)ââ3f«Â=k /†dó)Ëê;ÔX Â­.�®!mãSÅKªè'£ð7AdfrÔ{ýGæ¢ ³h˼{d~_Fï…úÆò#Ë™ÎÚ¦}åœÔu²×4˜ÉbY·ÄI»˜ö‰ž²”çÑQWÜv¦ÒÔ W3²´)ÛP˜û¡dSn£B¦·Í›Ð#{%·HÛ-ï*MØh�ÿfáÍËCBNê^à:ÙØ‡>.‹Ê64.·ÂpüZv»Ce ¸÷1‰¦Rsƒ"IˆNiA\CZxF¤(¶mÙ:\ßûaHÞÀèGÑ,Kd^+žŠ<ÃmÇ9p5ïˆÃOUzËÌÿ¼½.Îé/«ÒŽˆrgÜКJ.0ô¸ð³ª`4'F¬€uëìäÀ ¿cÌ lؘ©V:ÉÛW-û®cì´è0qäFV$¥�®%I°)DÃÔK{e¡ƒ\^‰Ô΋θê¦Ï¼Õ "rÆ”Bº�ëÝF½PúS%?h"ç¢î&»=%¢�…«–NÉ› «u³o.Ñx°Ó„—�©’S¦ G Ü26µ–8úÒ x¯aØ¢oñU@çJ–n=—¬š¦ljX†Å¸í%¬²À¦„õ±¢¬Äx!·™«—Ä&Ú*z0ã¼`ôBkx…°l!€†ôëG œ£d¢XÞa_–~¿JÁVk†bøÄ6.<�øû¢æÚ›¹Â{Õ6wVV€™õèfg¡t{Q[&éðñͲ‰‚FŽobŸQ@ žº}‰gw­]úÄßýå‰ad¹á„ ¨5n¾œ“>±+�¡«¿pt­ëéU¶uV³¬~Æç D_|ª˜ZÔ3vòzoôMè;…}¶Ê±«~ÃA”<Å€h/éFVØ�tký°%l€%Y�Kpó0ðYÀÂ(p¨4�ã1qnS™ˆÉÄv—3×> (Wö(Yðáê!‡ë�¶kÚK 7‘¾èBp¾�‡[àâJ¨yù:_«;Ê�'@ɾܲk¦¾׎ÇË”âpcP˜í´×É\—³ä~ã'ëL§Ryú�¹E”û6'¾ØÛþYL‹9]8§u†2Y�DrEÚKó¥xؼlé4òžÅ1œÓ€]§ ¥êäu� ð\@=Ÿpd"¥ÚóƒewØÝ1•å´2¾&½.vuÔ©ÙwÝ$ódsTDZÿæ\³wyâaÍÀdã §0šeÜ* Ž3õ'ŧÏÇ0Ë�ζòì�ž\Ï­E`gM;¯›˜˜Ž9Zô0¦Ö¯íA³B‚³Š5 Z3öåph�ë×îHnxé¹ÃØ}�á8ln-+#j‡²öÄ�”Ò^cïü²ãÙäT¤E•±kÜ­ ý*Ǽ“äû …0à5Zص¯‘¶�-çsä±lÙšðV—гP7§«€p²ìý„t9nNÃyͨŠ-O·Ýí²5–ÜL`ð‘*…‘¶#PWR™S?™¿( [­Q?Zt£^Ewï îÿ'ïÛø÷N$ºe`�n~b"ݲ½#U_‡X�k�&Úív¶�ŒÑE#®§hÆ+½Ê¡ž„MGñ>& OÀ§môÍW0]Þð¨1ˆ�°ÚÏ<ú+ðhG,¤lLÝöî¿9\ÝÝërò{¦¾ÿõ}dçæ»NžÉðýdø[!.cmõgn¾ºÇñ97iükÏvûÓ¥,Q«cù›ø´M£úõOè}ÿóü¿ƒ§¶†?�g�úxœ­VßkÛ0~÷_qËÃÚBo…Âh]ÃÖt0ʶ@û°=Êò¹Ö"KF’“fýN–•t)q„$öÝ}÷ãûNvöfþýúþçâj×È<Éâ²’~t xÍŒEw5é\5ý0Ié¾Nb~;¿7ˆwÈ ¯Ñ@kt‹Æ ´Y’,€ ]n<ìûÜ:æ„u‚Û  Ï$c XƒW“¿ýf?&y–2w–ï›(òŒ,-pɬ½šÔçPYúLéÒQ×å~á1ñ²"'„‚à{¿,m=V -”ƒ’9–„{¥XÅ ��žOöÝNÁ¶ÈE%°f�)ȰÙF0WOòoo�hÐ^.²›T× ÈhÄ#¬©0d¼£× lâAtaѬh€ZÑeì\Ë®Qä *d®38¸¯éƶ±þ¿ HLú@ΔÒ „F—C™•#tñ ¹n°Ï2K¶3˜S[ «Ð„AZ¡$ÓòI¬ÓÆcQ Æ[‚Püª�†b ÷”\6 �§=v€ „kµ¢ª)Ôé!Q©»b›(• ‡Uò©ãKtwâ7–ËÎçÕºy õ\@_Ù£hºÆ÷Zø)W½J õ‚±~B=“YŠGUv_k/áÑdwpÞîoÜΧŽ˜%0ÇŠuÒÁŠÉýÌÏß�hæ€d’qÍŒÓ3§C€)>BNôx55; çÄDÐQG"üž/t–$Zi¥n÷·¾?:÷3ZÑ„´¡Éßum«{qr£EMaÇ¥(‘©£¡ÎÓháÂm ©ùrkI¢©j©×v)vAž¯Xc±¡¯U´Ïûì'óÿ;— ê”:B3NèÖí¿°úm„Ú6z½¸VnÓ±‡É kS>‚[T¥6Í¢$~ýF„ºwŠ 'ã ˜a�üõÇ_€¯´”zM'äÅP_çŸÀRäð™´5B2­»N¬(?g’ÀÇV+¤GÐq90|vrkú¢ªXÎÅØ>÷uTǶIÏ8åîóß4-ÍÚ+<œÔQ±Ç¯‘ë3AžÌà£sH¹hþȧ— jNi5ž€@†/ƒª°ô‰u¬…”`ÐúN…"ã·,´�úAnµ›ï ixÅùYò‹†p�f�d ÍxœµV]oÔ:}ϯÂ+Ýè‚�Jóp[�*µå Í:“Æ�ØÁ…òëÇI6Ùt—rÕ+í*ÉØsæÌx|ìüÉù§³«Ïo vmS$ùø ,ùÑ’C5Kî4õ®:y•flwÒ5T¼?¿2D—„FÔdò,Z“<¼·º¼ XÿÖ¡“ÖIa_þ'9‚–NÓå`Zä„ç«XlJòDƒÖž¦õK¨,ÿNøÓ±—Ð%-‚æÛ–ÁûyyÖ¬‚™�8žŠç�uü”7õV°ƒS§–òv ÜÚ“—iô¾ª bàýH½1z€l»†ZRÎN#>`s£�tu •6ÉšÆwOF’ݼs`�æQp¥’êJä¥BU‚õ]§ ãߢ‘ÚÛJ®*AÀËi¸H³Ç®&i CÃåwdØ©ñ Ë #Ã�6€OT“�+sùŽ ì˜3·�ÔÊè=zR9 ˜@-É)°�Xg¼pÞPŸÂÙŽ…ÑlÀº½ ˜·d,£&C ``C¥ç$ƒIùvËÉèj—»Ðmç]D‰¬PÈF²‰ìábƒ�†…ý¦Td4®i k°”Þ.ç'ƒƒAuC —M2µÛgíA âÂ1Yo#÷?÷�àúz1Mþ±ÓOât”çW^‰�q(OcÊG#èíWnÆ2çPÜ-Åe¿ÌVó^ÊSÀ·6T�¦cý¾|êg7AIžŽ„Ò5,žÁÜõÍÏ=ïÓÛ‰Lô]ÛÓbm[ãÕÊþX"Í-i1ÿZ{HdZútÞtžµAºì‰ n}¥3•¨tgZ¹ ƒÆÕ/Š ~¹d_:6† ’¤ÅÅdÈ ð"h5n{Uv¦È]9hã!Ÿ.‡7×iq¡å €ü¿Ð¼µû=M{œˆÿzñ�Ü¥üEi±{¿�Ãü)[ßÎ÷~М.PcÝå}†,% aŠ›èù 4i1¾ÝGò|)Åÿ‰‹QË#“éó(�n7kÇ([çH²S­Ë±?įÿÚ}s°ýÔöoûòµNîl¼è]Þ:öíæ¯—éX"‡”dŸþ[ɇK P;]7бÛû<£¶Xñí›|}è²ô‡³énØ �›Ì1e»7!lšápaÒ¶#!+IÓqÊÛ÷ ë©/³á†™Å[ëov]Ä$ ‰ C ”n‰‡a�l�F ]xœµVß�Û6 ~÷_ÁyÝ€\¼[Q`Ø|Š»ÝSÛ+Ö{Ø0 …bÓ±[ $:Áý÷#%9—κn+�À¶DRüH~¤Ê¯în{ÿ3t4ôUVNT ?$u§œGºÉGj¯~È ^'M=Vol­ú‡‘z�î^Õd]YÄ�¬,’…•mžÄÞuåI‘ö¤kÿ#Ìi²HV*0jÀ›ü\ ¯ÊB‰¥ïgÏåå¬Ü@Ý+ïoòî´žWüI¬YÛ�(W³VV|H�-‹�Ø«¢§�Ä ÊÁÀ"=´ü¦ŒTÿ Ö¤­YfQµÑÛÉ™Á_½Ê£µ×]™;;l€]ýɆÀózPóçóæáþÛlrCadYšÓ¶e#ÝV‰?~Á>6É?ŸüÕbz°f ÔY�`]wGz¼åì¸îø‰ÉkíÜÉY.�Î6K€[‡ŠTöOñí4uI¶·í~Ó…ÓBÜ f'È�¢<•#øÈûŒ^P*sè0Ë%AÓŒ³akÐxMOñ4±|´œ‰6¨×ÝÊ:ÿ‡p«úyI9¸m]32 œì:d¸~/»Ã¾Ï@­ìOUlÊuŠ „]IÓFåÀáZ<ç]#ŽdÏž .9 œªêr€{o¹x‹´”0c:‡íMþ:¦æãݾ…Ê_stóbU-àPá}Ô<ß_î ŸÉðò <„¨Áÿ‚yI{k ¡!Ÿ¨Ú½¬þΓ�³tÄ ÎäÕûý‚Xg#/¥ ©Uh6䪒šDóÏ0;‡ñÝ8¼›²�W‡_ñàÔä8þK9šqXq2¹˜¤jÐÓsaô‘šRœ#3DŠpŽÒËh±`$ÿ/œ;îxÊÔ˜WÓÛ%MÚV;]ï]o¹¢Ÿq;‘Ê—™À°ÿ[ü¢þKÙ¨A›Àñ{9“Ÿy5»| w“q`0m’‹Ícj„Ú„¬�SÚhn‰�"µ�Õ‡} WÔåÕïß-àú�²À¡úbx?ÔÜøÙ‚ïlßäÕñ÷%„^¤R ÚuºîN›£æiØ«õš£ BgJT=ÂQL|ú9ÛD^½�_ÿ…–Ÿê;s·�¹sœ{M—†h˜žÿ.�ŸÑ'�šâ©w±WŸ×âÙ,.·ÊU¿òrYÈsS‡á�iúOw� ÷¶é£æ_�F†¹ ¿µ–8DÆFYj“ÅPbtïL.—VTu磉 âÂK�NØf‡„&tŃé&<…4GC ÕMSùóänÕØóÈ&;]5N Ÿò%^,É ž`Åw�-:§¾ì¯“©ÇÊ/&m¹’^ÅY¯Üš[‰¡yÞé¶[Ä[ô_(\ÿ&Š"�k”H¡xœ­WûoÛF þ]§ak ØQK04Š�>V @ãkPtŠâ$QÖm’N»;ÙõþúñR$Ùn–m@['>>É�tüÝë÷¯n»ù ]•‹ î>�eôQ¡f�L*ÔWa«óùÏaDçšëo•(™æ¢~#$* ר ‘©8r¯ƒ8òf‘m�Ñ“…Ò¤ 4OÕs8¨NrAÌ f^…©£És¸ˆ#flŸNáL%Éî) 6�–L©«°8‡\‘z*²�PžC¼frq�•qd¾ÀÄÉT(Ž’<µ‚ŸœØ³8²Öâ¨ÙñDszÔÿÝã�–%o.gNÅ\“ÓñkV¶¸ÐÞp ¬ÞÙ…ÜÝUà$2¾î@Wj~:%xß4{¯r®Õ>— Eº@ˆ±ê}2]„‹eaµ˜'ÛùΫû èAò¯0ô7ƒM!‚r'D¢P®­Gã? e[Õî-©p–”¨fæHÔ­4¯`orºXDò;¦:˜¤êFH j)º‚­˜†)çp6‚3³q«Š•€Ì©6Q¬jJ„ë·ï^¼–¦ØÐµi!.»3Íþ@*"Ü „d æ¬-õ}@ð+ÂY’yòœ²ºtÍT, êÑ…@.EEª‚pÈ.÷�Hô¿¨ÀÀ`"ÏEYŠ ¯WÞá’ôç�Ž»è†q©ö¥69›ˆ†®‘¯Ñ¨= €ST$ fþ)2CÙ?ªÊê‚o$¯˜ÜR›jIÿlx¦‹«ðì§Bw5Ë>\bºb(sqÑÉ|¼¿'i÷Í¢5��MÛÈŸ,Ûê2Y£TOúæÕÙPÜz1÷X·UBÕ òAsi‰¶¨h„âšîx­qErO»š99>~vä¬XÀy?¨Ü”݃àî‹Zñ¿Ze®žZ~E ÈÒÂB�Yjðy½Kwg3X~vmßàÀU$ï8gé«/€Óó‹®âþiˆ¯D­Ùr.Yj> � ¶…”{ÊB`:ŠššS™›PFìÄ@¥¬d‚<žÁÉ4HÊÀ[ 4z©µ÷µæÑ‡”ò¶ ~(D™uÍ£Å~^=�¹uáùkoܾçÿlY­9åŽjËFkÛR †Ìé0ºË€È)�š Ž­†îP™#sP±¯¼j+gÂÒVM8%ã5A0b'ÒÈK¶Za6JadÔ|!FX–±^bʨ´œKSú–ÝM�y’“ä‰>újTŽ1ýiSÒt²txḛ̂Á:£kGÒ´¯ô·àµ|Û­�Ú€i!­EÂLt7RdmÊ ±T ¥@¶4ZÚšœ»«¸×$-…˜Q SæùhòØ)jùx¨Æ-+•è§× SÕYÆSãÄE¼?‹æt4('u;„Et*êÕâ¢õJ Kä�€ (áùUøÂùøò5ÚŠ:2å÷>œð>.FM1Tœ ™/•Û�útµãÊÇîl}ý+·Zø®`®”k—É�ÙG¯‘:îtKýþØ·´ ŸjÜ<¼Rôz7rëmº¬»£Ïÿ7”·Û»ngØïz°Q8VS[GÇ“>°Ýº¢Ó°”¸—ù$2šNf±» ×³Ž v½_­VŠuX kòI½gã$(AëK> P’@÷RJc{¼èÃhä ûï3ŠwÛÍvã@R0Œ4~çÕèa�úróp½üùx :9[ÍÔ.¶%„FcdJ‹bHÝükQJ>™d©úÆÁb2Áß…Hœ �¡§˜ ±*GÅL•[§:´›ì{^q’N¦á+˜réL!xt´(„§÷ƒûN=E.*Ub¶¾8lh_%~"ꡱȼ(ô%t,ß\¶I,šÐþs¡«]?Puõʬ–C_JTÙgÛ –šÀ®¦¡³s‚‰Œ,D@žÉÕ–Ngc]kÖ»†Ï/‹ LrÓ¨™âú%É�·MMc8^ó_ ¼eý¢°W<ÔŒ®·ÔB °Šá }ÀñãØ®ƒOèŒs›'�Õ×¶ÇQɃ ÝV—¡¡­ù;Q)¢ñƯ Å„'9«Èý�ƒIÕ¯³8ÿ­JrÕÇþh$.µ¬u°í¹×ÂÏ#;ô;Ί³ °k‚'mdrÐïO˜ tW+ቜŽ$7ï)·OW9>�Ï¡Êʇ9�iŽv ·xœµVm‹Ü6þî_1u¡ù²·n¥ñÊ¥�BÓÍ}h)%ÈöØV‘¥­4Þcÿ}G’íøå.=Bwx5š=�æMùW¯ßÝÜýqûtÔ«"ɧŠš?=’€ªÖ!]§5Wß§ËI’Ââgg” iôcÑQžEq’g£yiê‹wö¼pÄŠŽdå~€�ï'¹-z¼N7»i‘gÂûønË’ü•Î]§ÝKhÿ]ñÒ›U¦ÆÕ¹y¹wQ²û ˜g'ï¬øˆ¢ô¼­ 1„6½P¨‘°ò*Ç$šÕò<¡èÝÕË4zúQC±;5HÁ”³pä¥ìP;ìK…`�3 ²ˆ.�Fa d@GÃë˜Ò¡=-o_�ÀÜT²Wê¬ÚŽ¿˜Œh¤yL88È�©�7¡GòÜ÷’ºÙg³Ö°þÀx%P1:²CÅ4§û —3A…“h=¹ÈàŒ÷q'‰·½[kL /À2iÓ'›Ëz´\g,±‘ô÷qF+Z„“ ê–?¢²Æ¹ :Ýû! ËN¶úh//øëþÌ+7'�«˜ò‚PÎ$�n‹÷È7¨œá|ËFQÀéÝYl®ÓÈûòáõœC¾â¾/0ýx“¢8ÀÒpŒ1j.šo6�sŒÓm�‹ðûº ð3NàXc7Fjrcqu/ŠGœ¬9¡%aÀ�·³À»f/|§eh d‹œê±*ŸêsÇëסÿ…Õh]Z,ñÀ±�ý1üÇ¡ÔC_r(÷õR¯BŒfÃü±¾[è-Úm@þðÖóXÕ¼J'ú“Š}¡µæž+©ê6d¿7Ÿ&¢—:ʬ™œŠãĽaèy3ê…’ž»Â²Bj©[¨‰ƒ—æØÏ½–Ë9-þüöÏÿÊ3ì‹/Àô½/ó;¶å~¢ê´X¯ãšˆÒœî;É!YwŽ4J´-ó¡÷�E¹b�MÅó©2\ö�´xWŸ]€Ÿì*»Ñ¼ûÛxÃí[èEFÆþö‘zjÃ[5¸-œØv÷y¦ñ~]ZùYØâwç™ÿƒóYèUã NÆwAÐ{[«¨ÆCô7¤Áê8X”ie%œÑO¿¨KÍè1Äß;�Á$aˆ{ÃiÈ‹�L.Zb�›’¸.8yJ~�pŸ¼Ÿ äd9ü·ý��ú<¥)‘ùéЈAñ¬$3 õ µmæÇQï÷â%¿ZxÊZYרÃÛ`ùæx¶6~6Yû'ßÕY¨�)Û–Û�¦‡+ Ÿ’Y|Ÿþ ‰–ËÜ Û= EÛ„2�pˆh¯xœµ•A‹Û0…ïùSŸZHÖÝÀBY_²ÛºéJéalMlQK2’œnúë;râ¥)-1…€bÍӳߧ±•½º{\n¾|¾‡Ú«&ŸdÃ@(xPäÊ­#¿H:¿�½KRž÷Ò7”?jZ6èÜúi­5-Y/Éeé¡:ÉÒ£KaÄ>x^çΣ—ÎËÒÝBl5Ë&‚FE‹ä7ÑÕG²ššu‰ %y–b°œçûÌYÐBŠ‹¤¾�­ãߌ/=//�8 p;<ò"?1*øf½`çœD ßû…€-»=K…žû^³%ô�% çµ“F_M†BTnv“r<¡ð€ªp(ŽË>xDb{Kºòõ�ÜJQ¼~˜¿KU× •?y��OV]ñS§îHwRQÿÇE"Ÿ�Có<�Dwª €ˆ퀧ßlÁM0ì¾k±¤Ë°¬º‚U7.nX>ZkIÈÒ Št.DáK~‘µ@+Ž›=ž#ÕúýeÑÖ²R±žîkã-þ–q‚v²Ïõÿ/�ö¨¤îÍÞ[,Ã!pV;ŽHÄòtœhmQ`ØEÒîÚ¶X‚aÃ0'ñ,qI�¤œº¿~GRr¬ÄF³-�@"u/ϽŸË¯.Þ�_ÿþþt^õUVÎBÁE¡éÐ:ògùè×'ßçß{é{ªÞi:ïѹ«_/Ë"]ee1±ÖFlƒ §•óè¥ó²q?À‚…¿e%‚FEgùÞ—¼* ¼Ï–*øœ•4áâ,ï^ÀÚñß =³4FÐBWY/Ùk‰Êb‚ª€æ$J7ƒ±6ÔxcAaÓIM ˜¼‡5ß 6 û-òL"�>Í’!73"åN^äIòKHxâ ˜ú/–Žõ�ó¬¥÷$À¦æ@€Y³Gvƒˆ©Чµ`º5¡-�°¡j—MfHVì;kƶã'MX¥›Œœ‘rô;#NÎ-‹$Ö~̨l¶êFún¢2�Û¨�Ñ6*Jv6 ÚÛ19{‘]vÝÍþV8¸H.�Ó/šmÙ £äç»æ1zÀa°æ“T ’=šÁO8:'Ù'Éj\ÀN ÞîÙ£h¡6£h·‰ŸtÓGIi=öƒ‹ù=[øùGŒÔ,ÅŒÞIA‰zd©!¹!èdÛÑm¾¸&„yÏÌ’M7º­®ˆÚ;ÃyZLW—Cgi}–¿Lü.vù*óëèÔ|ö-V+ØgÚ Ì‡<—Øö>œîœW¯SíE¸'wªÃs£=±íSvÏ«ƒJ9 YÏùõæÕûÝEËÜÏC÷À:¶ o«Ò‹©r"oaÇÏ1¶W ö”W{‡¤h*ô žÿCn¹ðq ìý ¹Í õ6Ò̉ Ë…$yãþ¯àß ªæUz…ì-é–+jBm¥h ¾yóì[N·vìÑÊÏ17Á“U�ïí¨^ÍV_HEñÅåÕáûcðõ¨j./v´³)‡»Õ#šp9æÕåx Ú`IÈÔe‰Í�Ñ(|äêYLŽ]…ÎEjðÛG„v%[ű��/œqßÈ©ãÿ¯pCí£’:jxm1¶£¼:x}Ì5ª€y¢ ip;‹x€…œð¥–º�}nKR»áŠ¾Ë«?¾[ÁÓ?Ë‚Tõ˜ Múšy\gzÁ‘Yœ�7¦¬ wý›N6]˜Â{$¯'=¶-Û�.|œ:ìy1wÃc=u¿‘sJ§Ó¿ê¦GGÂb÷ZL÷û–¿–þKË? ÎC†Õb@ÝE•Æäý”Òt³Ü•Ê Úê7¾.‹ðÆ•.¶û¸eeÓé.EŸÈxú…¸Mé´"ô¦•$è#ÁTRÊ;ýÄTñâ&M(_L³íôàꃻüPìúŠ'°©¤¦ äõ*�Ì4Uk„§x" ÈeÝpánYÉjª'îJQÓ({Ác‰k“Ù47{Ylcù}�û�ìF`È=Õ.p¥¬k¹abtÞ ÿô“çñ¶®Sz$ÏÆº>UT¯06lMmÎÝÙ€<™ÆX¯h¾2© ªºOͯí Cá+Å8Q[,£ð¿‚ +L5�g¯~‹}tn:w±ÁT}š?f-ͧ]´Ç–ÕžÌc;jOø�¬¦¦ìoˆ=aÝBÚõ‘0N> ºyzG=¦øn 9ˆõðG_~¶5ÏÙ˜ïÚEi\{o�òå4ìª'ÝdõÎ=².{'€ºt3„ýžsX×vs 38ó¢Pr{­àŸ^éÕrÉìf¼¦n&ÜõN+´3fW—£ûüAš&@I^aâ«Æ¸!'yðÆ-8Ñn€ïfÁÎ¥àzÛÜ¡ †„•f7B·EäwM7ë '[Ȱ�¬©R¬(pôôÄñ$º¢œºÅ÷ø%NpaŽü� µœ„¬‰ÿAü?)ò ¾ƒ?�m‡Gxœ­”Okã0Åïù³>'ëm¡°Å—þ9•¶Ð\–ecy ,ÉHã”|ûާ%»ÞôЀaðøé½ñ�AêÛíÓÍê×óv]5S‡BØHqÄÚ`LÄËbàvñ³(¥Ï–;ª‚ÆîiàÎR¼GÍ!BCO‘-%UîE3UŽfuh¶Ùú¢JŒl[�®áQÏ‚GGËâ_í÷ÇÁ=’]›:ÄTTªÄœp91Ú‘R|/E؃î0¥ea® Mò,ä•ÅF‡fê÷®£mAÕÕ±a-á»cªì³u+CàWS„Ђ'Œ”Xêx†D p\?0‹~— c(Œ“Ïö–�ÝæuiqUd´Ò;ÍçV8£×tšÍAu.fÓLšñ;ÈvE«ß9´Ö7xæ€ öË'cC_†q<£³^¶/øûì+õ4™É#gÁôçif38ÁÔŽº¼QèƒCqO`ýny8¢õÖ¯¡AÆyî*rï”�MQýþ1‡‹?ª$W}‰ä‹‘VF6Ú„®9�ðX{v[NCKYX‡ Á«±Ú2u¢¸ÙA+Á®×6/›‘n?GSŽ—Y¹¿/ßï4Κ - ÐSÑ-ƒ �t†D½xœ¥SMkÜ0½ûWL}-dW$Y­ ¤-ä�¯²‡¶·Y{‹Z’‘f—æßg¼þ`ƒËR(¤=½½'é_o7?Ÿ¾BÍ®1™'ÂR&GŒPÔñ:ßqµ¸Î•äÙrCæ;Á'Ž»‚mðw·Ÿáž¸eÒªdZ DÛP¾v´&1²Ml‹t'™i�ŽÖù ·œer£v.çmÍÑÂ)àŠSZçõTIÆBBª"”¹Þ è=Fs_6Zu XƒÞÎaZm |<@ôÀóþàó�ÕÔ4¶]}ÒêPI«¶ëÆÀ7Ë€ß1B§‹X³ØÔ6Á´"Hhý>ü¦¶¯ô�G[à@½‚D4¤¥j G{P ×_vŽ<§eÖ·RÚý(�K‹«¼sRr'-áˆ> £;iÅ„ú/ ~pD9 ?y4H?1|3ž…å©cEWyÂèEàŠ�w‘ µXйØ$‘·þ¥‰ôä&Í�ëÜ<À[Gib<9’ŽöÏû„j¤ýñÕð�TÿKߎ@8Š~�s–ÑxœÅXmoÛ6þ®_ÁùòaqŒ(0dŽ�4k2I“.†}¤¥“ÅV"U’²ëþú_�H–Ú4ÆŽmŸ;ÞóðîèéOÞ].þ½K ]•³dþÍð_š’´ R�>5:ÿ>šà÷šéfC*¸Ò²I5|~yAj)j�š�šNœM2�x¬¥È¶ùt¦4ÕLi–ª32Œ�ÆÉ”N+8uLOnEå=•øXƒT£ÙtB�‹WÝÐÚ¶ˆý Mk’–T©óQñšä ÿÆøQ#PŠæ]”³Ý–L—³ä° §“Ú€ÏÈ]mV*¢ 9Ó¤Q�%îiÆÖÁy¥Æ¯GnÁQ)-©$Îm#�¢Ì_Y Ñ“”“ ÓÅYB\ÈsŒ„›V1íÃ9öohµÌhëËMyFeƾ@xÀ.×óq•òÖ¢9gšÑr�þU.dõ°U¡UàZÒŒ× QbX<…Žo¨{Ÿ‰r ÒI0ÆúNðkÚ(Å(±�óŒ¥T‹`y’�EÁÔŽ(|/Q�cÁËí‰Ïý“?¨¬wMu/‘xXWO,¡ª`WSh°IDN¯�‹ƒõ7”U …¬­×iX!ĪhHu1šÝO'P!F^<ºIEÙT(:|k $GA&$£šž�åû¨ä�]5ªÿfÒ[æÊ|u(ý¥3&¹·~qúßïM€·_{Ÿ‰aÁ={ÚÍ>²&u:n›áZÚ¤|6ûÒµO*ÀÚŠI6p¬ˆ¡Pñ çÚI–¬Á"™ I‚¥Hn F°;4.ÛÆ—É5дHÂê–ÞƒÜçÚ$ª·Ñð”ò…éèQ)=òûJȆ"s²�Ã0óÀV$ÇZ‚4DQ „d°fÔµ¦ÿ†°®ç$ð÷lú’þLßÓsž‹A½Í!(ŒP]ÿ L`n}–p˜xî\bIrk¡ÌT{ÙÓ¹ï–�_,�±Õ/….žrÛŒÂb¹ã+]œt¦›žŠ†kÌŠùRomg¿œþÌ™-¢¨”ðŒPä„#'Šdã™c¢„«·˜l¯Â ¯¢0l[{ã0˜,Îß%Ìé©óƒÝ÷Öáâ «ê*ôiwú³TŸñ¦eÃ-0 äsÚ”:ÊöSCã°Ñ‚GÙ*;=‘ ¥_1e›‡ͪ w©¦kˆ¨"G»†§»ñm—Æ#7‡“£r™¯ÔΔ¸ïJfØ„l\AevòæêúÁG��Û‹ÅÍÅSLsz‹¢Á¸6tëó5BÐ�X.,‰‘ù6#8m°<Ç¥ÖÌ�†]ìØ²r *¼ÄÍfÌgÔ“Ý8Èck�Dš•a¹È©wˆÇl Ÿã ^5Ub8M ,vÌ‹Ðd ­ÀØí~wyhÏÓƒu¢;|¿¼`t1»•ãæéØÓžD_ÃÕ¤gô/·ãž±ˆàÉ>?¼âaeÈå£)µÁ‹Aj‡;3n÷1Ô b¼¹"#´®K¼p"ÑbïX›üX½ï¹' òÚ{·z9½½Ð]–Âùûhv]Þ;‰Ö#=<’¶•'¾q¬ðäâ9ŽYφwÝÝÌ)fÐÀ6Ÿ¸ÕDZ^ࢠ�¾uCÈ$lˆ0§zƒuuO©O~äú³ÿJ;|ê»à"Ô½ç6$øø‘ml’­»Ñseò>Êä4Ì„ˆÜ1ûíÔiƶ«öÃñ©¿N~ÍïN>¨³~@Ô…‘¼áÜËŸ…6…ë¼j1îÜ9=x4¦» Ëáë”5è0d°·åÍÇ;Kê›!ØÔÐ-•Ò07�Ä.;£™ï>*B‰ªÍ/zqk»Í:ïå`¦ €Ñ¸Ilòds}x:¸KZ–½Nžu¨&þwʼnûåò+¥Z%i†y�r�v Üxœ½VmoÜ6 þî_Á¹@±½3Ö¢@±8Šô-@ÓfI€­ûRðlú¬Î– ‰Nzýõ¥,ۻܹi±ö ä"‹¢È‡âC‰é/ÏÞž\½;7u¥ã@XÈÐ#äZG|w\.žÄ‰ÈYqMÙåF;¶]ÎÊèÓ“§i¢4 ¬L±ñæ~Ï#+Ç*wÀÌFшR� Ç{ëq–&èí<œs*Ò(m!¯Ñ¹ã¸z ¥“¿…LY6榠[ÞÓÕœ‘•¸èUÓ¤õæ²” tA-É?Í�›¦5Ú¡Æz㔃_/ÄÌoPrgiAÙb¿1‚FÌÖË(.Ôõˆ´q‹ÇqðõÎ`½Ìêå Ž�%\ˆ}íJcìáÕ„VS«M4ز*Çq»èÁI¯G”rLÖ4P ãàÄŠ”@â‚ÅÕ¾•£°më (6ý¶ éáí ÔèXT™b ^"M%:£×Ù%`íŒä#D€P ²TÇÏTCÚI`X+Þ¼¿ "ÉÒôžÐ0{ß¾ÓµžÌ¥ª…ÿ}võIP`Ú‰Ñ,)vÁªGÙäc/Mï[kZ²¬Èõ~âì|xãb㑯\õåÁ6K¹¸ùíVïíi,Ï<·ÎÑJÝ0Yg;‚à| ¶w)¿·­ßXTJ.;GEXLØ�D÷¦kÎ-*Ú ¶[Ó9d¢°" ¦”jk;†vKû@ø^‡ºy1TDrGv7Ò±ðÊIûHÏ:Im7‡d:Q©5)¾>›pS‘–Ëu�¶PŸ¤æëR­©Ä~¸Ü ºVøS¡¾P|ªKgÃÇÜWÊ_°ŸÖ¡8ƒåj¼&ÿ"µ®XX·+™C÷ú6Óv_€›qç!ŸjÅ ë}à_X˜ÃÉh·ŸŸŸÀ£_bçœB¼9ÕBK”\K½Ï/̽ы-]O¼ڽ¶HÆ+ÿîç#<“ãÛqfßñplÛ›9Œ™k¦óØ?)@°·#ÑZ€«JZ¡iUÊ^¦J_›¿Ô™�£±y¨¨na·kñuˆvÝ5þ)^þn|å,&ÆÛÝË~ìÃCã“^£ÍþN?HˆÒy¤;Li1rz '&3í©/Jr ÔL= r%„û,m‹;‚?Ó„i›�­úœü38¶�oÌr–bÉÐm'¡‰ÿ e3Š,�q”\xœµWmoÔFþî_1½ª…H¹hA8–*UÕ& &BT¡=Żëî®ï¿¾³/>윓¢á|ëyyæíٹ⻓O/þ~ù 'Û2+†dœ>$:UÃŒEw¼èÝzùË"§s'\‹å …O[fíù«S8E×hn‹<¾ÊŠ<™Xi~é Þ/­cNX'*ûfUI&+(&ñx1’8=/Ê"gÞÞƒò ²ó€:¨üÁñ¢ykKj•æÈ�¡Ø0Sžò¶ÈýC±*'†V%Ü B¯£ÈA‘+EÞíy Kúê¾ÞÓ� ¶­èžFqŸ†(Ÿ6¬íqH Ï…Zá2`Ûw�66X9m@²ª �²¨ÀÅfÀ.íòá"Úˆ¸÷ ŽK5› X goðž îÁip B�rçž¹fQž9Êr¹º\î½z^‘gÄ»?„m£-‚Ñ[òn0½²h6ÔdZщâPé¶—*¼RlÕ¢= ¯ ºÞøW°W®–·÷p¥xC?3a_QTpì=Ú,¤j­ÛVo…ª“³3Ò_¾Š:Q¸cÂØ™Â:ìÒ í(bƒ¾Èál"QÌRv;äéÛJŽf÷ÕÊ ëçyj¼3B2sIï ý5°Ü5Ç‹Ÿ~þaÓr¶ —ˆ¡Ë,ƒ‹,B›�;‘ø¿=SNŒ#vI1�CëOÇÑ=Éb‹ÐLMp/h¸•? ågÄ8½Œ&B(Âi˜PÄî“»Ëk¬[V×È'%Ìëú¢ù2 WÐ…¿#Ò á0¸Ð[iȇ–Ô ÚßbqþlL�¤<µÔ}*NѨؠ~?ÛEœ´Ó`רÐ0ßå8áSÚC‘mÞWÂß#¬2ÚR�LOÍÜ+ºgcØŸ4IË"5ìÝ º²9Y&º;Rï”FRïníqM„¢ôlçëåO'ûÁ•ƒ"®Õª.σWZßòt”ЂJ¥]/~�>Þ� ‹ù=ò»ó÷!˜Å£Ö+�vƒw2î½Qm¼Ì ›ð/ÊÙ㸸±f—æ‘•Ù•ydî‹f·o±#+»my7 iÇy­p{û=ëñÍÄe²«�Þ~k7îño†unÞíhÙK·Ò„ RMxÜ5»|l¨)™†K·Ó¾�½_ÃÚ(œówpŸ+¬ ð×hŒ†«0ýR˜–8Ð�7º³?c{%!LŒNþô¹÷$Ù%¬ht7hh/âIJ[A[$ÈnNr˜ˆeX¯ér­é²Rî†ß?i�òôs5�¿ƒÿç9Ö | Å||‡|�x�|&xœ½˜ÍnÛ8€ïz –½Öv‹E¡X¸ÝöÐ4Æ& ’SAI”Å%$åÄyúþH–ÚA·•�9Ù�j8töêãÕâæ~ù Õ¦$Éú£%<f(*jª43¸3Õì=N¡ßp#Yȶê4—í%5Š?.@Ïd©—%Ylä²ÜZ‹m¨áÚðB@ñ± ”dµ´a8¦‚I–RkíÏ#ÞA�dkTªõ®ß¡JÃß švl!K¶‡‘åGìäàÈigéÚZ$;bÔ8E·.URyo¼â¹bºF'~hÉ7=N£gï°·vS3äqâþ�È�C¼Y Ö°Öhшb$Hæÿ²Â¼A5/jTr½t«‘?xk¦º¡mÁ�¬¬É g åÛéZ>ðvå‡Êκ5£:†ÖRsÃ7ì�o¶lE}³¢B�Å´-“ÐÙ+¡µb%/,‚ž'òþudFˆÛÙn¸î¨€…ÁVÀÏÀâÜþD’ÑLÂL©XIÅMÝÌ‘[p%ôΖ³áæá†1m©mgH¢kÚCÝ0Vö‚Ù’Ó•l“õaÆ%a•‚àˆín©K˜fjã@µ ›`NYVÕì™Åä¥áúˆ©ÑÒ.£†Á²¼¼Ñüî�ð#Ðð±O°!<Åx@Õµ.”#w™6J¶+rÍÀ§Ð¾®4t%Ág]+V]àK/~|ÚPÑ9â¹M9¯÷½àg^íGï¥ð= Àص }ý– >bSý±VvŒá7ë“åÐa탙·6µÑÜ¥/£HfÊ�2~Êðë˜Òüî+Í8õOï0$ëþïfô6½ðrÛ™Äï'¹$÷GIîÏDrc� LÜ#Æá´�ñâé0þ†�ô ŽLú·Œ•¹“É¥|ÃÍðuN wÍŸœ}; §Ax^¸�!O-¤� “½f4 VàŸe¸)1¯ªê€ô°ç4l4%O üÍk‚?¹‰É^3†º§€ j«eé+“óզ]6)ûÂ��×]ÓPµÅd¯yd™Ai¯ÑW.Sþ#¼Ý{Œ ¤çû¬xyË5ÏmV5bhVlË#žsÁͤX_ .ìvð…ÉaO Ðëœ ñª3L-]ùi?˜½f Î)„zuWP†Zc:Ìá)¸å¹±¾·ÜhLÜã$Pç5&L¶¨uu ðŒÑÄá.G"ȧOƬ¼uÉs—�ûžS)ÙÞΟ„—TÁÛÌ=£1u’Pø�QÒ¾~± öçL_M_úÖ¯•Òc“ÇŽ—دñ»MdÏüô•Èž¢þÊŸàf”œ¸MÏÿgD_ž´–Ê,ü�“Q#6Åk©ÌîÚ·ZÞ†Khìš=�†> ?Ú¤þç ÿh�Šûƒ�w†B¸xœ¥SMOÃ0 ½÷W˜&�Ø"��ËrvãKì֥ܲI[%ÞÿgíªÑ‰ ©RjûùÅy/‘G7׋×Ç[(ÉY•Èí‚:çÅ!iÈJíÒ,]Q1¾LçÉ�EõÜÄÂÜXBoªw¸C*ëŽ3D2ï9Ȭa––PþÆÓdu¾w¤+�kíÕ]n¥ˆ?0¹‚¤X*8Þ_ZØiÛö´�F%Zkšé‰›]¤hâ$ æ†@CØðAÑ+ád'‹ÒÈê*�_eT{àÐTëúsX~%ÐÎܶÇîŽ~ ±+òÎ ì! `2*´_9¬(L’v¨Ü¬·¹0¾H£‹œ;`y]æs¬è1ÿ0ámׂÂ^üÞ¥NþÎŒ=ÑÛ^È5_ï],«KõF‹ÚW,s�šV£„ž‚GŽªèƒØt½ZšÊTÝÈŒÃ06�N{óùóuÑ[Þ²ÿA}ѽ!Ñ>Ðo &@æ‰"�v’HêxœÅXmoÛ6þ®_qó‡eâ-P`ÈéÖt’%ƒ ûHK'‹�Hª$e×ýõ=¾É‰«-ll@àØâñ¹ã=ïΞþðûÃo‹ßAeE=˦鲂þ ´ òŠiƒöjÔÚrüËhBÏ-·5Îæ�[¸áµEÍå ­Ô–£™N‚E6�D¤¥*¶÷ÕÌXf¹±<7—0„@¦Ù”�d¯F=Ë{U`ýÈ4-Ò3šM'ÌÁ¿îÕ·$Ü×dØ@^3c®FÕ( ý�é£%˜œÌû—)®-L—³=À%9÷Û¦“ÆAÏࡱ\I¶B(¹…Ö`‘…Õ‚¯“kaÆoFaÃ5˜œÕLƒ±ºÍm«*U.%Cõð4“°á¶ºÌ |K‘0gsÇ·1œó¸xÇIJ`½‡sËdÁtÁ?cZéÀ$·œÕ òbJ¥ÅßÈW•5=€÷š¥]¨š|Ë|fn±é/9M˪^£ŽÏ/2XTÜìòLï5 g¬d½½ˆ©›Pî$ñg+5<·jPÏ쎗Cn_ d°D ª.›ÖÒædýI4Êk¤}W1EÑ2[�f�Ó bÄéBvnrU·‚ÔBo݂ՌK ;ƒ‚Yv'Éô2-±¸AæÄú•t÷ŒO’ó=Ì¡Ä×ÁÊh}tâÿz1ñ Þ?Ž>3—ÿp�üuç(ÚÍ)ˆ¸o‡ _{‚Z×¾”ØNï �ÎæËl*”T¾RUq‚Îr�)¬Ñ#¹rJ"PIÑ[ v%äÙùrYF–WYÚÝÓx’ø­uéDÑÐ1ZY£1±êœ=©{gñ\lq¦[<'s¾l€¿~<3fˆœD¸æ,t�ÿ†ª}ÏYb˘;)q7ÜÞÊR P-Ž'¯Ú§ï�e5懺ý÷žž°ëÂô'„'�8¥öaù�˜¥‚×µø¥²ÕsÖ»vb(yTÜäÊV{ãGÏU+-åD+ŸQ«'}¿söÓ«Ÿ“9÷%“4’Ö€Ù$$šI›0ƒœƒQ¡ºRª£þ’°ˆ¡°}¥í¦%¤¼P)þ&‘PNA—ü¹·—^¸hjäÓŸôGm>¶êWJËFz`$±—¬­m'Ø�-3\âÆ*Ù Öø©‰^pã[…Víª‚‡Ü²5v¨¢$»Væ»Éj—Ƴ0?��ÃY½,Wfçk <ö 7 b1(ÜIÞÞ¼ŸÇ2¸¿^Ü]¿uÁµ¢w$Škö1ïØû— …'±c¾ÏM¼,i«7  é;¶¼\’‚ª(qw÷™'õdI7òÜ[‘®îwÊð\”,:Ì•\ã§nH­È§yEeN¢{Q–Ø Ð÷ C¨û3ó±¥bq¿fÜ=oúGbj¸Ž.ÇËíøÀøôAóOO¬è*^ˆp¼.š®ÝÒ§JÀš¦¦ozÄ?Xu²áôÀ·—Ò~ß9–»ƒÀûÎÓµú8 m;:é §¿ëNWtÓ|oÎb‘M̯èBÒõ솫Þµk7x¸É�úvvŸwe€6t£½Ó.„Bã”»¬*—/è#;(�IüÉa~ÒøСÓ׆7�uŒr ×xœ½VmOÛ0þÞ_q Ò>ÑFBšFˆ4ÁÐ�`ti/_�›\o‰Ù ûõ;ÇI€¤Tlê&Ágû¹Çççî½8¾8ºú69•E<‰ºŠ”?%’€$Æ"5eÓ7AÈv’T`|Y¹‰Y©–QèÍ“(l·/tºr`¯bK‚¤%™Ø·0ÚÆó“H€%ƒÙ ŽBá0^�ݱmU�ÂÚà ߇ÌòÏ”‡ÄÛ�â#¿Ñb ±`øfaV,nÙAÖ­� Õ§xGF$$µ‚’w³‰ß“Ê›ŽBi§û�‡yžÀÈcc½ø� �%mÐå ®l¦M) £0…ÅjÒ"ÙÉñê@x5t¬î‰r Œ.!$fG†­Z!ÜJÊŸ;QUÅ $éfs³¾¡æ·ô;÷,Ž\§]øÌ‘%£Õ2¾DQXÍ![Ó€o87˜DzDeù�¢�´ºþŒiÝ„uæ´·sÏ,xÄRÄ»ð|#ÄOC1ä«òR:ÒŠP‘m5”ïÅ=îà®+£+4$Ñ6ÈA<ï šö\BˆE£|2qDi+¾çbî ægçN_sa8!Øbƒx`ðŽ[Ý:wü{Q¹Ó{-e|‹µÅÔO†Lj[Ì>ÖåÜ`*.óz4\ÇŠ,ЀÎ@ªª&¨¬þÜÎ|Öœ´™ÐØ6³ìÒ.ëWo›åyÍ×Y¯cÑG’3‹S­¹A¸ÍQq™*&•¿|™Ý2¥K¹,9[šÏfbHñF /¸ÿFóDÒ©Êt·ÿ¬£úAº’ºrWÙ&Âöy\u…ð ÊeN¬²¡e³³ÇÊÖûÛnç¶Ùž*IRcÒOL¬ã~I T xT…/�{Šb jxœÅ–_o›0Àßùžûœ ­ª4MÄ/]7í¡kµäeO•�#XŒl©ýô»³�ÁBÓnJ5©Mìóýùù|9;y÷ùîz÷óþ†•®®D” _ süªÁI–•ÒXpÞ¹bõ‘Ç(wÊU ²C+ •n’8ˆ¢$îMS�?’£÷Â:é”u*³ŸØÌ×¢D²FÖ°á“.’X’í‡yœGI˲JZ»áå+,þ­pêÐ$Ó9Ìb%éÜ:f8bN³BfªR¸ 8baº`ãeÅjŒSYÖ94x"ŸÎHå�Ë&Gg€z©Kѹ%k‡À}°u4¦!±Îèf/¶LVVcêâ^1†'T(6ü–Â=Ü OçqÖT+e\ £pxÞyŒy y­�³ý)–—bô;ÉÝCkt 8ë½sq? È-Z_RùÉÔך3"qyô¯ñw1Y[ïê;ÌŒ9øÝX.þ„€}ÅPüG• ¥RÏ4i9Fœ30íðضx^žg˜œf¡“fÖ«� dG¥´UOÀÅ8\‚ 9!€ÌÊPrFJ@ÀèG/SôÙ8#ÄcKø¹œä|òC�öÅ31|³×�uºæb-±|©ä>ô1¯ò6 _�îZȉ¤žDÙ�·aÙbÍ9U¨€ó{v’ÈŽjËPñÐ`žkSx/–:zÔm˜ýSƒšzšmmvή—ã�ý€öå›lýwiÌÀl2ùï`¶«ki¹è‹�Â/Q·˜ÔÀ¤F�®Üѽ=&µ'.ès±™PóRM®2|2¼e¶†·¢ô£EœáEñj¤ñ·÷/À8<)ŒŠµÒŒm�z™^@xœµYmoÛ6þ®_qó°¾¶§ š¥Ž�Î}Ù‡5-š¬[1 %QWJTI*±ûëw$%E²åD® €-ùøðyŽÇãñ2ýéÕûùåç¯!Ñ)ŸyÓꃒ?Rª „ ‘Šê³A¡ãÑÉÀÇ÷šiNgs‘Å…b"{G´dË9ÚixGu""5õ��7õK¬@D+ƒ<™)M4Sš…ênÇ@coJ #)=t™Ž»^fSŸ˜©;)vŽÁ¹qH!'J� ’cˆ…"êz Ó+"gaN}óÎ`tZNý`O¬uB–ÎzX�n?r÷øtêÛy§~¾Á ÿFø¨ï“Û£„rÎò% ãî¯+ ÚImsI‰¦@`;�rˆà?jˆ…„+¦ ÂÙw–-<'�Å,İäT¢IJ²�Ž=7OÄ®*'¤jt\3!Õsî• 0†ËÄJ pŽ.—Å‚£°0±CûNͯ]Â+’¾YµÝãuûžô¶nÊ�â•D3û�pº�®Aù*§!îEA°j2¾-)T®Ï õB—/0D2­N7·¯&§#\Ù\dŠ]Q³•í»–83…ÌstO��•õ£JíXsº´ÁsÉR"W˜ 5RC¥×,ÒÉÙ`ròËÀi9¯µà1•4mN*›OMyÆÈ×î›;Í,tÔ†¶Nxü÷&T×yRGMK;ÁË�`,ݾ*cуåˆ,Y™ ÆðŠÆ¤àðÍ£o…Ð/>T{æFº{9v“¢·ü|/W·¼4ûzwn—¦hØ�šKrØú£ÅŒ¦¹^� ªlÑ“ÏL&ZöõVŒ8¶Œ±§Ÿ¦K ”S»IZ ›ºë;½ÀltÑ*w«�p²…¦Mj»Ð4zÒ|ÅȤœ¹àBÞÁu²|ßþ¶éR¯Ì·ayF%hÍž„BJ“éÚ¥…zÚTáÁ?ãƒ!Œ�ŽžOÌçó£Éá¿=•¼�ã#âxÔ!(eêN-'Ç+çÙáñ±ù<øõä ¯œsSqaAf±<Ú�𬠩­æÇpQä¹�&g¹ŠL1QNO…ç¾®ÃwX�ë�]=0ª iT›TXQtþŒÕD�¢…&¼ÃªÕ[¸ôôàܽ(RsøìãA¬“ЉÒVC‰¸-ªJ­,Ž@ÙIB�N­Uc,mxkÃÓ=ý£žlÌßÓ‰ÅõÃzƒåaÜwßQ¸»ïÞJ}bŠûœ 7Îk8m�ÈÀY¶Íc5ç:s´d7¥t)Ívú»½ÖX©Œ3½W¨˜tU^“®jÀ¾*‡]‘âÙ°«6á< á׾Ǔ„b=²ñdyTë[ y‰W_Xl3¤¡û㲋?3¦Õ¾¥XM 0h·/uˆ‡/s7£Ž“©šImÄË„n¾îÈ5ZΖxÁlå�ú'Á²Ý-_ùöÊc¾à«ºfb…+²Åì‚b‘Æ•8…©_¾ò¦Iã³Á;ã_^Ñöœ›ÖßÏõ-6t�´ö³ë«Ù;�›ðöæœB¿ÎÝ•ý®ž\ô³׳íÌZ@uã­¾o——W{•ÜÒѺ@„›†Ë:/øeèöhQ58õçf”ê ÒÑMr}�µÎ ± ŽlÜ ×­¨¯öësV" 㵟 $ôt­/×Û+ÍîU鱈*Û°p7=›ú,-´¨Œ‘ó{ †ð<Áoš™4ºêP¸A�ºb^‡ VPÝÜÜFßÄC¾=o¬îF y¡0C¬£àâáè„QId˜yPZš<¹Þ7ú+¡Y9¤vØŠ“¿R¦seü�%#KÞøðpónŸÝôéX˜ØeO(Ï�E& Æ+‡©p­ñž(é·_óU‹4jöæªPzØD4„&ʼVàÆc¹ò•,è`V~ÙqNÑSJ9§l‘B*70Ǫç³›ùÎ/ÿ�á»”üÑa�s‹�y– ÏxœÅY]oÛ6}ׯàüÐn@l'i‚f©c`K¶öaKƒ& { $ÊâF‘*I%vý.II–-Ù²¥8Ò½:W:çèòC“®>^Þ=Üü†b�°©7)~á'!£ ÆR}1Èt4<Œá¼¦š‘é¥àQ¦¨àb-éüò4J¥H‰Ô”¨ÉØ¥y“qç‹paÀ�¦JcM•¦�:G­0�ïM0â8!ƒ¦ìÑç?°OØ`:cƒÜxkyàCRІ•ºħ(Rð7„C �›í¼¸©šøÓ̇¢ö’É85°SôyˆçT!fâž;ÒÇ¢\¢†§—ø‹¥šHôH-$R) h´ |†tLŠ `Žæx„ЉpÆ4¢ÊC¯¾fB¿»‘$¤¢KSÆ�åÕÇP¾�Á‡�|è“Á‡� >¼ƒ‹Í ÞÉŒìKÞ�ñyw6©'êr¬:sîå±ïÝ3‰³HDö (î%ö–ìÕzäUxD$Iáæ„ù¬ y¿ ®¯!ÜÆ_‘×…K¸:‹&foÙ‡Ì5"Œ$„kõLV£9Sð²xÌØZ D¹cÝ>7ÚDòzŠi$eóñþ)+tÕà–~ÛI“×£nƒ ‚�5àYB$ � 0ÃÍ XÜý8ÜB»�íBûÅ3Á1»LÈ6îW’{` ³¡Ÿ˜€%)ÌSwUáhþ}zÿë†÷ (qÍQ ;ï;^­ñäK’J¢ 2¤„îhD˜X¾Þ㿎@¸ÃÑÉÉÛ#óûöäèøï.R}Œ¢Nj­ç÷$Xv›f"І/©[#>œõ�ºv U­²��YåÞŸžšßßÏ;)w-d‚ýf ´É¶’Ü“fk˜uÁV̹cáSmØ}Ä,#σbñ„´@<¯H…òò: Æm–¦Bšy¥;…°$çyýÌ,�"GÄkì+Á2M^çχ†èŠª”á*BP*3ù1t"ÿP€PE‡e –wVà®ËÛöÈ%ÂÓhÌTGЧ@¤å£ÁžßÃæ™QÇ'Òš_øŠÈGgèvlÈÚͯ—ö¹n³$ÁrÑæ×•äžüº†ÙØ` óÊe@÷¶"?ÕO1x"˜‡…CsÐ\e//F�)»x:TÕŽÆùz »|Ž-¦ÜÝÚÝ ]€,ƒKgw2d3�½ »?‰§í·ÌìÉ{UÀºñ ú}\'ËBÆq^–“•'x!¿ml|Í�ôÿÏiï% ï©¢~ûò¹’Ú“×Vëf3aôhâ”Q݇Ñ*›pF9XÉ-5êSÝŽ£¯à "UÊlqÛªc?Ð�T.ì.êx¡¹/™kSv=¿'yë°u�]N?*›™Ô¨Ø9þ?�ùJª¯™x§Pl nè(u)$:° «\`íêãàß"ØiI]DÞEw™¯$÷µ˜YŬËcPšg”›Q©usRî4K%6c—¯ddzœF_‰ôò…Ö"�U u|€bBg±î´Ù•ìžyÞFñÍ‹�û}iý‹S­Ú8µI=šcma33Ïl&£\k—”.¿Œ´´‘–ØV“DªZ?)k/C^£<ˆIý’úl  ¥tN˜ZiP%Z*(LSöéOv3Þ~#h·’Ú×"¨ŠØ°2a÷q¡UfÂÈ"±ÖEWfÜטòr‡¥‚ì¶()oÞO� Â]LÕò¶áIp8œ-öÙ!!á½5Ñ®›$E~Ïû$KØÍ[%@Ë~›#Eß*¾c¬’ÏÛá!C{…pºdRšÝ,¾²�£ˆÖæKH]$o?•n°©Ò:tج¾Ž¬¡ÑÙH>»i¡ÜÍy ¶RweDg´*·Ée$0{'­cãü“òØ}·þ{뎉 A ã ò¼A†w��r Çç¬* N`/«C#F¿D[Ï„âÎ]ÎÚWláð“ãŸ#…iö“~3n¼ae]m1jÜ"x–EOh»»0¶ãZsö¼V�E—F®Æ-;—¿šÅ¨ò`fÁúÃð9[Ñ�WjÄт{ÐømÖ8„o!B‚˘Ô,æ~M+7H�K)2c°l-}ËŒ$uMÛ±†XÖÂ#Ž×Õ\0öÐBưºá¶a+n%eè·ð?ÁÏÓúÈ2®gÉðž+wÆI/Wp�Øâ.c°Ž»,ŒRf�g®7”|¶M -–m¢‹Yø1€#zðxœ­WmoÛ6þ®_qU�!, [Q`he™—Ãê&h3ûTPm1¡D�¤œøß÷ø¢7òºn’P$ïå9Þñá%{ñëÍúî¯Ûk¨L-ò(ëFJjfЊ(ÍÌ*îÌ6ù9NqÝp#X®$EÅ©ÎR¿eiÐ,dy°v~̵!†kƒRo`ª�[QF !5[ÅãFœg)±š?ÙÇi”µ@ÑzW¯a«ñ'Á©A *Kvä(+Ž´ 4êd²´µvrøÀ(ã{¦@¶L¡^³sqj˜rFàâÃÍú‚ ØJå�ó-·Z�i;³Œ¼½’ï{dµN^ÇÞÅ]ÅÀ#›Bq ^ØžˆŽ¦�LŒÿ ôß�|«‘¡Ûš4”Aqˆ€Ê�"Ô…ƒÃ­¼ÚõeÃŒGw:!B‚Ú©=#wÌTˆù‘› 1•È-˜J1]IQ&%kYS²Æy ˆ—¿ÐF¢4ê0À±Foì4 !nŽã3¤ HS:4›øˆ‰b:t¦Ü†Gˆ–¼‘«?×ÁÀ"rÊ­’{^Ç^†”屯E¿�Ñ“V­¾ßi…4a9²aU²ô ÝJ!äcÒµ(LÄAsÝgÓ÷ ‹#©ýáeøÑ癘*ÎÿÈRV‡lÚ,¢KMÝIé®mg%»=W}ïT“â�œ²ŠÅŸ.ö’kªxÍ‚Yr. Óh}(Kô°'*ÈR;÷GöUQÔDíxÓ§ì£Czñfñp î=ÁŰtyÙ§RÚ[â5îáÅ †F€´ÂrŒ`su÷îê—¡pOT=–Ò5;„Ò² û€çd|A—öö5kÔn1_©¸é±Oò“i£d³Ë?2,4¡%^ú4,EÈ,XßÛU¼AÏâóµ÷Ãe³´ ÷Òº+¾8>-ï,àÛºT6ÛNãã�óéì»-XÊ™ÚpsÏ.Ì™ÅsáZ6ohτի|ð1žóg¬V Çp¦�£8¿¬UT~eÙÛ^N;ª<3e`Ïaî帵Üôl=\F2r¬5Š¿·ÏÙă—HÑÿÿ�|{Òxîüê˜i><—ïµ½uïññÂ3¿ç�¸]÷Îé³9—öÙ³çn�,ÂkB]ÕáË¡Mà“sA¸U\*[`8Ì&ßn„wÎ’()¸à¾Ïà)˜ÀÃ÷ã„›B3µ÷' ‚Ôy|{žŒ…žLú˜Ïãþã»Ê ÿðñ­à{±Ñ}Ú“À & Ïd ‘�Ÿý™šÆ0íø¦/ÅÌQâM6Èñ'›)YÜ3j`«dòì^b:—‚åwåàø± ‰�’jmo(Ä- "æz,ìÑ1°'÷öíN…Hσï—â±qš9z×qÚF‚T9é, URûF£ï€ðµ>™§ó·M]Z»öÁõ¯ZÏ÷?£œ9\–4ü[“ú“¾ ¹ ë )�}ÀVSBxœí\{oÇÿ?ÅVE  Ä8i€T’¸n’Nl#‚EP,ï–äEÇ;zwO4ûé;3û¸½EŠ”ââÈ<îÍÎÎÌÎüæ!_þéïï^Ýüëý·|nù»ô?¤HáÇBÁ“¹PZšG•™ž~s4†ç&3¹¼Jî–BÁ_³²à?J3/S}9¶_±Ë±#1)Ó5|~¥�0™6Y¢Ïyï«°†] ^ˆ…|q­8‹þ~tu9HïË«�@çKX°äI.´~q4ÿšO5¼–”iƒås~y'ÔÕ«Ë1þà/øåäªAfrÅ�iIa—Œ¸¥ñù›ïÊ<ýürLŸNÜÏËñò wY2·vͧÀCXó½*«eVÌ~*“\êÖú™ZÂýÄœÿ/+óäZkíëd°ümOßäâ×ÜRÜÉßBuí�?q9ý$u5÷j*\øèÒúçoâ7îúDNqšk£ øM×­eåÒ<1³OåïÙ÷‰üàN\DYðß)|4fÛ²ðªÒ¦\¼÷ßµ81R›kiú=ÌÿIÖà*€à¦¥â‰*µ>½y�21»>Íîüúôë#K²Ñâ7m; #ØZ `™ Æ;BñkÊɯ21 �øjž%sžÊiVàë\‰"-ù†�|Œ6¸xœÍ—ßkÛ0ÇßýWÜòÒš˜ £¸�Ñý /ë …±Ç³¬ÄÚËXg—ì¯ßÉ–�4©Ýfî¶BÀ’îîëó}β½ùpsu÷ýëGHi¥çAÔ^$&|YIB)VÒ夤ÅôÝ$äuR¤å\T9ó�Sïçp'-ÝJ`ÐzŒ®ÿFh¨öÄ^ÀµxñÂ/ŒN8_S@—b´Î¥…&÷£ŸÎåȧƒ֠%VÒ”ÔÚ¸ðßR™m„j®~ÐF¤¬´°-VH[ÆüæPé"[�—mæ}«^©Ñžî?”÷]�*»U¿díÎ>Žõ–Ì>i·îpH)�ót´_¶õÚnÜI;äÁ*ûÇœ€û¨¤ S·Õnj¶/©Í;ËS‰Ò‹ã6�ºÓz›G¶D§òTGø×ÿ/4„W~]ýÀI½²vàÛöµ›F¶A­°ßnÝÕ¤B­lÝ&=Ü îLƒ‚¸<ƒ�r“—vã½ì:ˆñžpM 0ƒ¸õ­ëÞmÑÍšï‘�ÕD·ìh6¦‡�ÓêPN×öª´dV=¬Zó8^•}fŸ4.ëÏ©¨]ž L›%Ÿ_ô#o,p˜RñTÜ£Á#™@YŸ’vóØŒœõ¸Q¤¢”^êm�(3ÔÚ¨mgÏçsaÊ\&½€¼},¡NfѲñù·Œ¢`‡Q�ˆTX(wˆµ=`ú¹c¹ÜòG˜ÔB  Ù¸Œ¥³­4Èvnÿ“ѽ¢°ý´øœÖÝg‡wç²ÞK÷ˆ%ô"ÃæêoŸ5¼( mU”É”ñš)<çíéxè—Ã1˰¡–ꎾÓ|•™9—"™3No£+‹e.Oø\Ü�¯çb¹TåÇl'É×ÜÌ%‡’Õb"/§ ‚gŒßÀ à\T¹‰!uÍ3ÍŸAÜFröŒ=›™‹ð}�ŒâòC%rnJÖ:p¡V™–^—R\¿å"×ÀAPfS ^7Û)åÍHˆmÑXe§|²n9Rk ¸|"a÷e©aŸ;ɳÂÈ™TŒëDäBq½y$Í\­-:�å𨱽€Ã6ׇõÝ”€éÞž> ±®Š²Â‰\ÞIµŽEÇ5$„t%ø Äs1‘ùˆ Í:êkœh„†ž&›áSÖ·y8ãümé(f@J/ó umVRZE‚±em\Àëà9¤¥zÄuÉ…½·s²!ððÀ{™KfÉé x=£k‡djè$»ÍÌi.…*8XO®�„IQ¤Ò  Ø“q`ˆ¬P®ë²â+QàNÀ^Ç›€ø¼x„, ¼]¤r)á…¦+�­Ð\Á#PoÊRèJÉ|«ÉX NEjѼ¦�‰Œ¿t‰°QÅ�=-%-àIYAûƒÇZ– W�ûXd‘,J€øbVošŽ6ú¼Ì öy‹A«†n<PãwÞ¢Am2±!+zlÄ\àÆÓ9n€û¼\™sw +¬@äÙ‰wWª,áw …R�x^ÎàÜyø 2�³R5Ÿ�yŠÄ KJ‰õ%Ffc?a0”yn?"áGB{âKÀ(  çD‡¶¸„à PÊ� "ZfUâ—‹RõÊîŒ !^È«]@”NMÓ’#˜”JI½, ºF›Y£8#×6ì…èzü]b ñáò£‘…& ûñåÍ/ÿöLéUy¡7Dñ¹@´¶mMÆš®U±®�ïT¬àZ¸K’'‹�¯£µJ ÉÑ}5ë%xq+-%f]Î*4$¼ššWÛÓ2ŸžwŽÁóžZÑiˆvèYc·ËÐj�–ý4)U*Uø¨ô.¹šÄ— 0‚Zþ3 þÌÁ¤fþâèËo>;ºzß8ÊåØÌã%_ý–4|’?ag%ûv:sµ_�q³±q57Ú9mìܦà…4^Hû8ÀÎZ$JNÖùX>ÍÀäk e_@¾6ñÓä÷qt=¯¦Ó\^£ÕlË#<—à"a§íØj§þø¢¥ï Iü½«6²…N/Lxá˜\Û‚ñ[¹t°�z¡¹sK’¼c| 2i½Tè}©¢€¤¤¼Ã˜çBjŠP«v¸ð ¥çq4ì;š¶k¿¸¾ã\èÇP–Ê8iŠ–Ó¦kh¿m(òNÖ‰×Êɦ­mð{Ëf� "ML°áÍ)Xƒué=:2¼×›õ!ʘ¹…ZÛ¶Þ¬†Œ»#FÖ ÷DŒj�î|ãÿ€‹ÖÄá]‡Sã¿3¯ñðvlÀ ½‰¤Vzˆt ¯c=yˆN#�¬Í?t5÷°!ãuyÁ¶*\u[÷´,6Å‹”ËbW¬c®o­ÅÓý« ÝÄaó ½…§–ªgÉþ,7ü­_…åYÛœYÃQß õý‚¼GQ k,þú¦X¿sxï žŽVE.|ëËP[!PùzT÷ûœHv;ðÛ_¼˜'ÙœK55M­·)u]b$b®s§¸Ì]Ǯْ‰…J Ý{Å6¾›þc8;%1võo¥e»Izý»¶TͧFF…+¹…n�*Wô¼Øl ð}÷<Ãò·¾p×Á�’ÕÇÜAïƒ×ÂGп!ãå;©šm£ë]UõíÀa—ÅìêZJ óçürì1` WÉisbǸÿÜ›n 1aàÕÿ,ì8v‡Ä™®¶³Õóð�$QlMzøä¡Äœ[ÝS;6NÎÚ¶&z'Í•ŒDÖ;i­ØuÒ¼�«xö«A)Œ¬½Úk$n—-\$Öpi2×¶ãÞ%är&’u;™x¬Q¶˜Åˆ¿~ÑGXd˹›š|kô¾èB<—tP@¤Î¶p>i0}й€d[�pPóä^x¥]x§ÐÌBó©^�~ ½ño@Hþ0î�3YHÀ˜ + •Z�˜AG×8 篻[`�3À3l‘E°h„ÂÀJ¥}Öç,‹ø Ä[¹^˜³MBÈ‘?~ö¡*Í-·ÏFm^R3’-eÚá^!ÁÄ-Ë3ðÒ=¼PÍÖGC27,à±€PæeO ãV‘¹@!�ÙÎ]ò[| òü| µÒ€Ä_¤WRΑyü� ¾ÏýÙdÇ–•4•*¨È:"­g™„R¶±Y÷ø|çˆ"·5ÔÄV’R%VXEžÔX •j_¬li«;ãráf\à ˆÔDUŒŸ`7Hè¤X´ŠX�šÅûujæH‡#ÌáÓž—u“«ä%¶~År™gXAÕÈÁê‹î ñ%ÀÆ-õCo½b$\JoçaËâÔ)h!ŠÂ–÷€ú¶¥’wYYià—¬bçha×SmÖùÇg 9¾±Wÿ"p›ÜŠ™tÞ^AùÙµ ”ÕÚ^Ÿ:›ö³Ca÷Fãã öT,œ±…Ãö`¯ûAŠ™ä1F Ä>Ifê¯ieö_9`4Ãy”ÞžÄÌ9Îú¹òÎãŽ[g×Zdp.ÎÅñ[¾ûe57Âë7œ—N:ðƒÚ‘Ö"Ð'Uê8r¡²IåñŽg9Φ‘O¨0å&=ð+'Ô.H¬D 2§LÙ΄{šöYQ—(ÌCÐ ë¯VúÐU{�Ú¢ÜI6˜âéëbY,ò+¯G]MpäŽ Þ®»ü Ó¶PŠ…Æ w¿GÊΓ¹C]�ÞƒüªD¥¥™¯<•~�›ÆT3aIW…KÎnhÐІŒÐ•qŽòv5BH î‹y³ÐWš{y‚*§¶Ñ™ôò÷¤1î š>“g>º%g¯u=u5Ù꯿·VfÂìÖ=E™“�B£„X3ŸÞ¤æw•¹_ÏnQ¿¢ÃÝ«UM7³…â™`Œœî×�ÍOYá»Ì/·CEX`Á,ÈŽ,UEö¡’v”{ì&Êü8Úý1 £‘½#[¦8q´}aH( ¶”æð`áÔ€hì_ÐA¥Æ{¨kY�6ÂSø*µÆà¾VÇ'�•J�ÀͧæëϾ—äUŒ ¦wLq}ZÁ}vü$Àâýyç—0ñ(ÑSi’3`,mó| .»ŸoS‘oè9n©'M—þ(õ~ê±(?å‡SYhÚÊåŒ'qÊ�à% ãàš ×Oê_7nXÌH{ƒÖµ¡Â‘·¿Ì¥¥ Z�#¿Âqºþ#ˆ’Pˆ’LÔûj²zi,¯u$@¥4d‹3Çœ\ÔýõІ¡ÃÄ‹º‡yoÛ\¯‹Aãn5Â,×tSJD°þlÁLè�~£qÚu?Ÿ”l”º+\ÿ�Ò2Ô‹'-g;ò²ôc "-…Ãð¶"íjÙ!ªvÛnØñÙ„|ƒP‹ÚCàë÷*ÓÅ<€ë/ÏÕ ¸ÕÖi±3,Ýz~hˆ ôÌúä}|>ú5´þE¹Îè‰ï…¹ÅÄ©_£¢ä�¯Õë“0áâÏA8ëòc"¥›T¯c5î¡dų­AG2haŒ~gΊý Îjv§PvJb›Ñ÷}]ý2šTmeUø»’ÞÐG…kÖ¾ÐÍûŒoyóнwóÿC°ö<Ï!0óÖ�Ý!p‹€Äáˆ!;ÛK°®v°D$¡­ñD￸±ž` hðžø”ú`M1vÿ¢åØþS™ÿ ¶ˆÇ a¢·a‚R‚…(Üxœ¥RÁjÃ0 ½ç+´œ:XV(Œâæ²íº¶ËŽJ¬4†86¶ÒÒ}ýä6eí¡e0°‘-==YÏRw/ïÏŸ_ëWhÙve¦N†P‹±Äu‹!¯ò�›éS^ˆŸ wTz ÄŽÑt|pžŠª8F3UŒ,•ÓûÄùXFF6‘M—p-[`™BèÑÒ*?ÍÞûAK=ǼT&Êyy!lœ.ò¤­ø®ë´Æ n¦pC©_Ì¿µ:§º¥É×[dÒéóÇ ˜ðÎAê¾1œBê)`g¾å¼>=èò1ŠqЊãÿÖ‚ùß‹g‚—R’xœÍXm‹Ü6þî_1u ÜÁÞ-I”ÄgHîÚRhÒm/PJ)A¶gwÕØ–#É{ÙþúŽ^lKû’ˆK)Üž­—ÍÌ3=röÍÍ/×oÿX|kÝÔy’ d=Ô Ê5“ õUÚëåÅwéœú5×5擨…f¼VÙÜu%ÙÜ‹¢ÚE�s¥™æJóR=‡H„Æ’ŒA˼Jƒ‘4ÏæÌÈ>‰— v’uPÖL©«tý –Šþ.¨©I¤FkeE,^�Z;)›wFQ Ž%Þq…P‘�äE¯¹háŽë5¬°EÉjþV°°jÀê¹LœxÅ7ƒ%�ºx–:�/ÁÙ-l{@c©�+`Ð� wR¬à5×ÛØˆ%× EŠ5]M2Í.�ô°+’;ûqqsnVq Y¹¶–' – ×NÕ ¬Å™BÿQ‡žµšÓ:¬­€bcÞ ïº`lwk3…t$Ððª¢÷Aeh­]¡*©M&[;›ŽK^²ʾékiCJb‡û¶´/�™äñÛ5é[Ñt7ÕAØ4ЖB:[l˜gÐ+\ö®?j‰ †սñ„Õ[ÅiŠ+$é ¦Ô-!EùGf”‰7» ­(±Õn�¸¢Þ|BúZ"Ó¤û^ÀíRV•hÉݾÔB&^*Û0™w:››'\…ÛÎìàG767µŽ›½kžû% B(BÉÄàa–É¢) zú¡G¢3;lÊVnðÐv‚Ë £Àå‹1 Ñ·“4�0¼±kj d\ÚYØbÆ $üš@¯Ñ6óV[ .;Í„ÈÞ䀆ɞ™µÏôITzšáÓXáª! xíQºÔ¥}¤)‘wѤœY‹Jy Ëj9bàbêÐæû³$™%š�N»íNwë­T»z}ÿÎt>›Ý£€CÙ¾ÛÐD£�óå©hWù¡ñy6÷-ëö{”-Ön#RX†:0nšL5¬®ó×/ßþüòÉÚVBÙ¼»MNIð|û¡ú…[ݽIo‹]+4lQS¹$Ç„$À^€hk‡`…KÖ×:(BÞ§ ÷6jô^™Z½![YQã�ðÜ"íÿZ :j†%t¢­%.¯Ò› º½û�²ˆ·«Ks¾>Zu”Tin樛Áq9_Õ IÆI›J§IvN²;]’·#I�’CÍÜà»Ûñôu’h�Åûmý]2“�^Œ¢bN†4÷/û¦]þ8�„î U:M÷ÏKóûç86bÑŸÓ±ï˜ÉµhµÛŒ––¬Ÿæ“uS’¿#íä˜)—Ö¢4_ŒF-I?5|Êd˜yÊ<Ó•ç2Ÿ£ïQ0vù¦on}‰Hó áòÔǨ§ TiÃUxkwKw˜�±¨ 3¿G¨YÊІÅßפK"ž_ºçäÙ¸·p‡5eŠupj~ÚE*ù¼a¦Šw£œé;Æd_à�¾Øƒù€Ô1¼ý±àÁ~íZ_„t¨)ŠCÄ”#Þ±*;Ýü|žk¸cX Oò¨õ¶$øŠ°kíõH5>M ï¡\µxhá÷ŒÅÒB›ð_ÃC[g»Ã.Bº�­2Ͻ‹�Kþkï¸�Að§vC{+$�G�<›Øšï:÷þ'§`ßÿˆ¤¹ð°Ap´-éÛ~ ~³#•dwä¼ÍéÓUmK$Þ–.#­²ôÕòyC)̉(,í`âì«Þó¥¨ ÑžXæÀò(GM¶º…W~lçâBb��!üÐ?ä7ÄxB‰eò%ñcŠî^uß´°AsûR„‘‘§!9?å°=É“‡2;¸¤ñ5aßôÛ5ëÜWEÄÃÔ-ý§5ñ}DÛ_5Ìõü5:ä7÷}ß/üg‹øh/_#Á5(�ïDÿ¯Päêk…§f:]'„À QöV¼D5|‚91�nÉw9KÜÕþð™g–¢Då‚.qz{~áã'Uݾª�‘*cŽw˹ñx0Þ}8I"Þë¦<¦LßÂËõÓpx‚î0“œû/¥s÷éõ_ó3”ñŽZ‚�8xœ¥Xmo#¹ þ>¿‚ ÐnØ1n�E‹¬cÀMwQ ·Ù`/wEQôƒ<ÒxÔjFSIãÄýõ%EÍ›_rÞØE<#Š¤È‡©YþæO_žÿöô ÊP™U¶ìþ(!ñO¥‚€¼ΫpÕ†bþ‡«¾:µr6G§s_T(­ô˯dËEÒ°±rOú~Xù ‚ö¥ïàÔNÉ–jQ©û«AàvøyµZ.){¿:¹Ž:Þãr¹Þß_•  M¹•cgï`¹ne7ÿ\.èÜÃr³«Ù¬à:ʱQƳ،·ùÜ:5}Í‘ßéõÍrM.Í‘;øoŽ�áuëw¥2F7“éG´:ñåaZuÒ�<8%‚‡¦“  m•(œ­€�¢–ì¿.tŽ ´5pn3Ö+õ®;`åç®ØÔQô@£.Ø¡zëÀ\«x_géE‡l­@U©:@£\†.yåvÑðGÐ*±‡�‚º­z>c·è—™ãTäA9Âc½ÎMák XÂ9܉†)vÆßÂqb£·5,UÕŸM„òjõ¸\¨j5ßìçGK‰KYçYò!Z<¾¼”Ê©” ~w­gÿê’F„Re,NjF‘`‡uòµÀc¢hŠ&+ yÝ«¼ÍNà –e´v|`>¿StÍ–¬i«Ã+%3;`‡„Â3n(¬1öE×ÛäÁvž ÊÇm„vs<ªo›Æº äÝ1‚Ø5G+�­½Þ)B[|7ó˜€FÉô´‰Þö�¾Š{‰¥¦Ê§+áöXŸÁáÿq)CyõþÃoñÝ´æ�îʱÌï{™Q²Ð"ð/fŨZNUÇ8¼[K©)ÅÂ|áÚ|×Wq�ãMÑÖú΄yB<>#àp‘GbÅ,[M£°ªñÛü2!,‡Ÿº ¤=˜•,ɾ{ƺ}²}ܩ޷Y·úY¯ÕVL—³Éúùí¤ü�Ý?µÕׂdÖµ<£©—ý†L÷µèž0ñ:§¸žíìží$×yÞb´÷gÖ© …ožm"Ðë8tv§"ŠŽ4zz(×+üôÚ &”|°> ZˆÑ“@ñC¬Ùní–1FP} ¢ON[÷&,£Dâ£ÆÙ�ØhƒG#ÖÛ¢»5ß;©ªF;¢ðN#\KUˆÖ„›þÄm­‘äªá ‘Ø;¦eàgCÙQ8¨}° k¥3Åv†ßèPi?åÿyŽv²®=´«l³gò�¦ÇIë"ÂŒ•I\š¢¥ððålR‹-1FL-Æ*`Õóþ-öR‡±¿0 Uz[†_£�¡‘×x¤:!rÜ£0¬éÀíƒ9óu©g�âñ³Û7=û³}!b 4éô½æ±‹#w¨»{¶Ò¡õ1ò@:ÛpKEó1>[ìÞÚ ¬¨ëÜ´R�uE¢Š Ἀ½ïgâÖ´ OÿY¿*ÉTËýí ¥Mœ(+ â.µSn�èÁ¡¼Æ±/”œÒy²¾âÐÖIp‰á©É‰¼$�NQ×à¡uˆ†ëZaëð°AµÄ~;7z§Bëj%o&XÄ6L?°ï¯2\1ä80FVæéT`|…ÆÍPááLš\g£:¬‹A[§ÑºE1š™D=ò‰™qcm@[§v[sš+{¥Þ°ßµÅSëªá1UÉnÖa‡O]a„LŽœ¹Â ë'¯0ß{g«ë¯2½Lº(ô£ÿɫšgÑO#D§ÄÁ‚âÑÝÝh˜¨A½FmÏ]+²”� n §˜*FÓË0Ïãüx¸™á4d½Bê‰!,Ðh;L”S¦=?kQîpÔW„Û¶”éDddJö! @ð@…/±Îkõ‚Åá4�ØsÍô41´xý8h2Èr� Syºþ�f£Š€ŒŽC^½½hXÑb«Î Œ¿b�©ùüôm‚ŸçÃç¾�&¯×??ðó?6;'Î3웳X[5-Ý`q‚`=¾}}Ht#rg½®Bÿ³·Ýï€åàx¼�æé¶Ä>ò«øã®VE® ÄÛÖÜ •ľ¯Ðy†•Ôœ‡¤x? <•ž(àÑŠéxµ±ÖàsGàØBçøÃ·~Ž|�dåUéM¤ñ¤°s`Ð9r$;ò:yªVgº´û&•B�ÐŒœÌTžn¬}j Š#«ÆÛ~“æ[p8þz˜¢ŠÇÒ`EuسùÂ'#ÇœÆZ4B)Á:Ö�Œl?v-óà’:]$öQÔæDlþ|ñÅáʧ/uŒÌ—õó�ë?R”C¤Œ8£].<Å{Чo5lZmdÌg8òbÞÆ×ê›(8û⇳v’ç­èÞ'5Î-¼¡<š=3"{öë4ÒNs­\þ™m³bùãZÿ¿>‘•ãfw¡…<¡àAÙõÜñ6S\DŸNA¬V×IÓ=>ò—–¾± æMK@W*ŒyWs*Œ ¸–¹z¦k ¶kœþkI_:ljsµJ^¢yd7[E_Æ1‹ˆsêß8½�.‹ôµvÁ_�ÿ ¿8†Ë \ ° f\Œ‚˜½xœÅYmoÛ6þî_Á©_: µÖ†B°%Ë�ë´Û0 CAI”ÍU’r’¿çH½¦’�tp ØEÞÞ�wtôÍÅoçþºþ™mmYÄ«¨{žáQ ËYºåÚû6hlþ⇠D¿•¶q­U²þ $ÒX-“ÆJUE¡\Ea+$QÙ=‰|Ë-HejÞ°fP­"Î*^Š·Á,MG!'yß/ÀÈ*ªYZpcÞÛ×,7ø¼À«sª21A%K‚LåÈ£°&‘1#�'²�öže#¦’Ej×+OžÉ]‡¡4/^^Â�Ì#X˜Ò�µ‚Xª*ƒQÃTÎj®¡+´9cœ• +V,&Õ²&VôV3¼¬ Á2˜¹Ò Lö!î ׇ­ð´“I9/eqO ÈʪQ�YiU.+‘1ð[H•PïxÁ"Qö ávÄwÆ^þ…¢ŒAÅ@VºµÚ[Åje¤•;1^8‰®”…èäžñ¢ÞòV™Ï£×1�Bz|Ûi‘â–4&J¦Dg€È-ãu-¸^1n˜¸«U%*¯uÂrÉh•äIô§Š”\‚†YÅ^ž1-L {mqïì°ršÒªpÌ–×¢“¶¬~�ð5b'4´uËï ÉOµàC�÷›NlC;¹�1»”XáÔ°윤1²Ú8`^|.-Ñuó¦J;¤�ä¤�w€&o¥Ý²\ÞÁ8½Å º¢f˜dÕÎRòOâÐ4¿ápÑ&°ºI-Üy� ØÄÎØœ±W}~îíûÖ0–îM<ža¸‹1)ƒ”­ÊöA_Cé-ü»~+ÝùødE¹7“åv¿yàúXg>ktç‚0üŠ6ÜxÊ‘' ˆÐ®­Л¦t›;cðRä/™/$Ïsn}e)„låf‹¢v¤eÐ/ 4fÁéhŸÍúɪŸi¼3„cÄK ÂYÔmAƒVLJF8b¦ÛØ`ȉG›õ²Ñ Ö� z„2šŒ' ú²³Q4åz�¬Œ[—³­9'kkó& Eµ¾•Ÿd�ÕòµÒ›�ÞBr”�Ùäì{"ƒ?*;ÜTµ‰ß ,µ0 g_Øv­Øjì›ÿÔµYSfð¬µT0˜ŒÃÉÜY3Ùu/ûegmÏOŠM³<ˆÛÆÓ¹eµóÜh<�»î殿dn]ež�CÜÒâ˜Ý ¼Ðv0lº§ñò“ðÌÔ:Àý¾Ï‹´ ¼j�<*D†ã±sœv%ÚÜkû*îÅÏF¼�è…_X)ü? âÉëT$Ü“|ú¸{<¤=@œ¾Ïb¤�“@¼ʉÐQïÜÑðI@ÿáò³^ß1 uìœ3¼ãÂý q"p¯Ù¡=rí+§ãB»2í”Ôèe6>|ã KÛQ-¦__ÉüçÊÕgU*F.0tÎ-beHÞ´¼ëÃÁ%Î@ŒÛ+zÀWæ’ÊŽÑÚž½FxPªìU¨à%]sv—ùºÈ+ïò‡_ýÛÿLÆ2´àÐ6ùüܱJè¥-[›²)¸»Á˜œ‰]ÞŸ_\~»þR“=©tPå#°"†÷<³<:è Ì7ú0d8üMÃáµ…`šWwòçªëˆ KÁÖé÷l¢9%NT‹)=c%ªÓ¡­Ä¦P-AÜ7a¡÷_O. ¹U*;Ý\lNe·�ƒQCU¹ÌMÝáo\]¼ä¢ÿQ×A¡¤>Ia!äúȤ.ÊUú�­UпǺÔcâÓ*wNÝ6*ªœâÚÈ…{úký.Á6Seûçü_D%4]ÍyƵ¦Ëì\£½×KŽÞX$Yø9äÈÆ:×ËÄNò®¸9Mérí îZ{êq"�H©ƒ?‡Û§Þ�WgŸ†í_‰¡ÿ—ò?Ê�EÚ„F‚‰™xœ…T=oÜ0 Ýý+Ø:%1 @?œЦC§dÈÒ‘’èZ=Y4$ÚÁýûR²/iÐ"Žz||�ä¹{wwÿíñçÃwd û¦;„N�‘Á˜2Éín–þòã®Õ¸x ´Ÿ›«}¡ñÁËñÎgIÞÌâ9víŠiºvã2쎅ùÃ> ŠB½ÍŸám7BÄ‘nwoAwû®ÅÂ~}F•šn0çÛÝp}ÖçRŠrXvôJ^gÎð-\³ºv*Ì{øjô­€ÁLk!àd jvc÷ °ùMVòU³²8¿œŽùòf·ÿ8¸\© ¸xžÐBé¿t�up”á}t˜‡/šü^À禠§€Gr[4ÄI»,”Àrì½£h |ÔÀ‚!ƒ2éÅ8Íò’T©¦À¢1Àèj‹ËÞ‚àüÀ삊ÏZ0˜¨JMÕ²»|™›jîúÓk?µêÄÇ�’Öå¨ÒÆ)ÐH± K!¥|äâBJ·7q‰P·×¨˜>ñQSªB'±ëŠ’Ø«ÈT±± ߦÚôa¹~G‡¢L”�DÐù¤'qÝ{Ÿ¤IL¬¾›t@õÜ @9'ZÏ'å;XØa¹*“ßC²"uZ™$¶O˜C’¥�ž,…ËVfíD¦~›�k¾IâvŠ$®¦Çß6Í`†oæÅbÑÈ£–¥ð3@ b4§ÏLS(p[j–5†) ÏÌ”°¤’Ö„³Ï´€»Öp›…/}[®#§´`«nÁB_½›¸y;�È"4P1˜’BÞˆ†£s¬(TµÊHÆ83ŒjP‹¶Ÿ«gZÛñ4U…¯ÖÈT# R£ÜÔíØ~ ÜÌæ{“Ûo{³;±·Î5Àmtl݆[‰ð² ²f¢ÀÙ#å¬Tª°lÐÚè:§´ 2Ç0 ”äekñ¬]Œ`EÁ)hºTh4JÙïTT¬f9áQŸG0;â8¿#ÝßsÔÏinô±)íFtAn܆¶.žef•¸7Ïeµ*ú2�´–”R ª­…ZÎÓO·¿Ü~Hbײà¤2°¡4 -:ûcôž�ÈGk"{ŽFú„Z?ÒZ5Ûë¢pp>ˆŸ�ûƒôóÃZÌžVö7nN´útExC ÓŽ·mçC_s/�¬IV£In‚3k É»m¸oÐ%¥¶Ï3)†£—"vH6Þ#×}ŽìE—¬@þ,W³ôàѱ…zùÔiݧ7ttéCÐÁÙöŽ£]ãI¥Äo×yf¶¯û¤; ¬_Jå_«Ûï£þüUÃYSŠ %»ÖEI9gÕìÀ¥~oõuáSÔäݨÆ/gåf„è\Ô ¡z¡jšˆŠ3¹ÄÜû€:5¦]¬ –�ÍR¸zÅ1ñû™ìâºL=Ê—|µ°ð¦®ûüþ³ïçî“kýå6f§û”§¥ðÁËîBAâ²ù»‹bLØ™>ÓÛ*³]DC®x#$¬°WÕzsÙÙ1}Û?òÜ”}÷ï);�ù×FdXG"®-*Ìg“‡¥-gñC`zc‹?xÇ'ÐyšÐ}I*ÚºšÆš”B;’Œ`�Èùýcxª±¥ Þ÷·Ü|r9UZÛºÚUоÞo‹ü®äý:âí­áEâ=‰W#èüï]«^‡¸«Ú¾w$éòVÕ‰ÒóDýtïïFLÌÞÛ0?�8î‘$ß8þ1d ú„Kwcíëx4,Æê½;q´— :UÕ¡ªu Êšã—å–ÑÝuím¬µphS/rÓ‰ìî�Swso?×ïÞ9Íi‰ýÿ@±ûƒé1;r  Ë ËŠ1‚”f�xœÍY]oÛ6}ׯàü°§8F2ÇÀV¯@^º ô‘¢®,©‘”]÷×ïRõm)^´@ÑÄÒåáå9÷‹Îö§ýŸ_¾<ÿA“¥»`ë�ðG†–P¥Á<® ¯Ymð¹á&…]®dxû;šì¹6Ї…áR|šƒ2ôvãì‚í¦Â et¶èw;m¨ÁUœé2�ƒ ‚-%‚fð¸5¿¥«ÝvC-øý¸c·qîÑ ',¥Z?®’$Öøo� .g2ºp¨ïÍ™lÃ…¸Yi¾ÝärG>q¥ Ñ Í�äT¡§Tà^Güè÷ÌôúÃÊ­ø�äRsÃ�@4£)UäHÓJ¾)ÃÕüb¨`e\~ѹ€Dí£òE„QA(c uiçÎC+G›ÚÂFÒ”|ýâ‘�0·•¿tx–ðp–ðp)ÂÃ1Âÿ&EôÝ¿Hxø^„·?|F»Yþû –’cˆ;TçahÈSŽKÚÄ9 ¦Eªå G`F*¢s`<>{m,„—&¿°O@dø®ÆÔxI¸nüÇߣµéù*ú?Ù³�2=Ë}Çz)â{ CÖÑ DÚ�›:!ô ÙU"paà>!ú„�Àö’&˜Tc9êóÛÀ›—¡k¾”}Ô!ì‹«tØBV?§&Yíî6A~½Ûn ÛiJ¨Rôlaû)¢É‰›„eø_ ¶¶,�†rQ‹ãÓ†v²öðDÚƒfŠçöáë¥j-Z\°öP¶ÖëH<ŠÝH*C¢®wWè¼UÈ¿mY¸"ÝœýâòyØåF©påì­êaùÃ*Ç|7j¤)ÓÊmáûQ·54�\Íü†±!0 EÂs5BXœ¼ FŽÍ}‡¤îd´fë«ÂáE‚ÑWescºT´‡ú7oËV¦�’kU¿«T¿U½×Íp?£¾ ¦Ú »&ô²Ü��©Ú§oWKCtµ¬÷âï�ú«0Æ“!ú¬©k.Ÿ91vÄYÇ甆n4�uÆã¤)šÖ¸çq`Û”‰ùòüÝû¾½‰ìäTÜûÕêúÕ•¥g™îˆ y'))N¬4ʈfqµ¥¯‘ñ ¶¹ÐO}ÙªÍy¢¹Ž:¿gyÃ4Oc¦ 1z]QF=Î~1%ÿcÆó\Ô>¬ÒXrÝÈÒÊN ÉyÁK�%D;Ë9k5Q9ƒ«YÂ6GCEL‰1,s|÷1ô]qÐyëÁ¨é»©(ï9’Ðw³üµSŒ:l}Èa?7¬Ô"çñsã³^Ngt’åDˆøsO®yðY·ÿgy¶ùeÏüDpÏúGì­¹„n¥Ñ­då�Ñ)Õâd‹w’A¬‘ú&‘å„5ËTgåQ[Ñ ³�U„ƲçY‹æ\È<�,†èûxî;A]Þ ©˜dvi£XšsÍB³¾‚a Ùi¦ñ|mã”ü.¯îrñé¨}Ú‘g›¨�¿…f Hϳ&ÚMÍNç¹Èª*½�åúb¶2N2 ,êK�¶©Í²hÍ$+qÔžnÜÉðž¦v&l©3àÉ^3•ŸÓÝ? ¢Þ‹ ˜àSIUnEÊËÄí`@�³’ã63²p U“}+XŒ[É$·Ê«.cV¦±�ÕQ¼©š2eRàwçYzõÍÐÌ 8K²NÚ37ã™ ša–vÍ‘•oÐ"Bg1KSA’b}UM_T¬jžˆ­úÛ [Z ‰éµcd9ÔL@�ò®¡²CÅ9VM38<`CÍ69¿–\Õà€¨&{šw½±Fú«AÌ>m*™rÙ>ªḀ̂ʾO¼–¢`ò � %þeX_ª³7W·¯¿¹ZïhnµÔY8æõ ?&P¦´Ôö›í$ÒiŸ´­ÒÞæuÆžµa¡Óp ¡ÿ6V Ë™´hšÀ4y›Eu+^´îÎtvµ~þbq kòb9ƒ<‰òÎÌP⮄}FæÌù=·®9y7qü‘\ å[ÖäÚ1qóâFKk‘±Q!á�xþ�]iâ�>ø°8¢êê°„Âç±ÉñÇâU/üI²'SÃîR»Zè‚�R¹Í/™@xb)Îý‡ùþÉëÃWÙm6ÜÉ�h'™1È;–ÄB…-�{¶{Oö;<…2€ Ü6ˆÆ2‚xÙÏ4j+4ÅÖ™2ÍP¯UPˆ|¨þyxQ# MúÅýô³ã± é¶sÀÓy{â„ò½¾@õþÔۢ淼ú;Ñh^îªösŠöNºú¤&Ó‡_°É4ͬy¥�ÛƒN9° ™: ÑœLt>l9ÙK³ž—¶âøÖÝžO~À�€Ú™|г1 C¿©]6û->û?Õ wpKÿ¸ÝIGÈ�š  —Šl Î*Š Gòr¹jóääÇ3ñió‘� ä%Õ�‰¢®&T÷Ó¢8ÃàêyÒ£3â³ ´2-yÄ£8µxÈà$bý(4 ‘·|ªù\°»Ãƒ ð+Áñi¤Ýg}wßKóƒ ÿÆ^ n¦íN:î�%Âj×#ë‹­lÉ laßxO´wóD{7(fÛ 0§Q7Q«°N5Ïå A�"ݳðð€˜G¦^ê½va=Ú! Ð ImAf�¸g"7ž=ÙLÄ�ÈÏ 0Í1wË«;£ÂÁÃùÉB ¯"«‡v«–~ÍŒ–g¸ ¦IFêÌEi�ÖZÀv POÏ J�s»L Ù¦̰oµlóÊ"¦$òð�üÉ Ö¨ƒ­ð (lo’�¦ƒ²*cÅ}Þ‘™By\”MÕ¨þ$ç�û¦bcS¢À´Ö�Q±TÈ[Ó $ÒÃæD@>"_#kì8ÊÂt r³UEq c´åì"HâvÏvóáhã(¿r‘+I˨’(&$g„䩸ƒ{mÙŠ-¼ê®ðŠxòG(db°(ìV¶‹$Úã>Ö-9ƒ=i£¨Úl‰ØñÚWó` ¬¶S:˜nÜ)`ð§ÝŽ ¸TÙ7?;<噌Û•Mq6Žü*ò´g¶àUç¬M¹n`jßþfe§Åu@æw!äïHë(@&ˆáÞ˜¶À¸`ãÕ FÑ�cpÐæHÓù¸D!3Q›N⩉ ¥C•{'ØÎGN=\EmÐNÔ›HEó»Îíç]+Êðí¸î*ÒÍ›J »)bjÂ|iÔ_ÌpƒÊ›Îî�rÍÝÝŽ¶mhwÍì×ÃÍlß5‰o_ÜÆüsƒ@a Ow—¡xUÜk×cà›… 4Â@:쀪ÒWˆ¦=—½¼õ$ÛNÁæ¤(¥1»ƒ€Ñp©wY¿YDã¹Ã¾¯Ê½«7OU¿úØ  Ý;*Š-’ô�×ðü~@§¿ÀJÇó; øJaÏ’×ö1ýMÊ| ÝÏ4Y¦¸Ðd¿žºž„a¤l¡¹’uá§3fb�°æVÁ°V�m¶OêåARnÚÆ ¤6%Ê-W;Û¸IN®$ÜU¬‰»8Ù0BemŸtGk»™�'8D<}1Ëo·kè¦ijçUqÎ4­¸w™¶uö†j£ˆ5PqÙí§j´Ï‘›îGn` 냳ª#�I«âhqd‡�| wqÍÓºhç|:—¸êWeë^$UÞ¾A­—¹/x@2›PÕ—)÷@ó_yÉ%|:¶4ì$‘ø>á¾Ê ·£‘ý£ËÓnòÌå¼5zOÁ$˜„å9Oݽ�˜z…9�F#Mûæ·O+”=e™îË¢è6#x»½£m­§¢ æ(iÛ5¾ór-*I{]ì"°éZywÛš¤½R?TAKžÑ4ö Ô˜»]d•�Ô@Î98œ7…k“±’Ú´Ò´;2¾a\š° ‰º(—ÀŒ^ da=�¼ºkN�àÜž€oC ¾ÈýrÂ�žÒéÑD…1gŸÓª0l Áy÷Δftí&ð¼̺àEîœu‚AªIW‚†EùÚ—lµDI�|qÔZ~àÙ&Ó¡É:ªcYØlê½7¦¾±íGGï&ìõRž!a—|˜3d»pg˜bàl©×æîׯ^ÔÔ7d»xo±� | –rà[íë›p!QPÊÛA{K:‡Ý;<1Ì<ø:uõ×@¦iÅU‘ã²èzÝîؤLخɵÏÂ-Û°€�øŽâ Ÿæ„ØvÕÓà˜•øÈÖVdÃÝ÷Ÿ1d3„Ê$oRîê#§|ÛXq—–æ#x�CŒ9;ïÃ4çeM æÚ×ere'>&åÊP€ó3äÒýǼ¥ý¿ÿ¶Ž ì GìŠW‚ •2xxœÍYKoã6¾ëWL}èÉ�&ÀEêh7 �KšCP`±è�¢(‹…Dª$¯÷×w(êA=¬È� �Xšç›ùfèlxøãóë——ß!1Yº ¶õ/F"ü•1C€&Difî…‰W?/6øÜp“²]®d¸þ� ™q’>pm Ã¥|“3e8ÓÛ�“ ¶›Êf(££õp³Ó†ÔâTßÁ<[¨l ’±ûÅI•õób·Ýëäöt�ëg´w‹B9Дh}¿H>A¬ñg… š 2š8ä]Ù¶á�…è´TÙnrkvÏE22Ô"©Ü›ˆ¿Õ.3½ú´p¿B.57ü�†íQñ�¤+óO¨aŠçb&a z†QÃ> ¬…È?)|‘P"€PÊ´.M¸Ó=W!·g)´uI”i “ïEÆ„YWáo0þY0ä³`È/ C>à ê’�§e0eº(³ðš’”Ô¸ô‚"b�™cY£AL²Ø}ýi 7m7,Û��•·îÏ¿5ÊM„%–(< ÀÀC0?�à‡ô?<£ì,<ûJ—„wh{m?‰åiß½AÞ5R�Îåñ±é;›Ð ©ü„Ä8üµ×ðšp݆�+dÀ•éñl$�I^04äc¦ô@¥0„‹§º™H·M›¯ƒ×ÓTñÜ><5Oñ*Øuìô^ÿ�p$ © DÝèæC\Ó?-_œÙ„Nç*HÖ¦G@ÍŠã»�‰üˆ4HëÉÕ¢T6›sQÏ®î¼jki²åš%‹ÄîAS† ‡Bcþ\9cÑ:=«æ}'�ò5…b}�eŠ+�ý„ œFú®²WØïÏS¾«€‹ÊhnwÍôšùgèÑEu"‹š÷\P&´çBËrÑ;`¦PØB¯º5;¨ä ¹äÚ¹fäáÐ*^ï¬Ü9×fžW˜·uêMVÛ¹5Qh-Ïùg– 4 {Z¶ðf¿HЭRÑ�li™?³¸ Á´lS-d°äò|§—‡¿ð‹�ýꙜnÌ„ †Ï÷£zß»§S%…È–,Ø �Ÿ;+·Bí˜61�–MAš—Œ·Ábcr�F$ÀÞdoZ(�»¶ñ«Ôˆ ÆcÐ?¹È�βúJ›Yð]QÞÉRQUfËdJ«føfýyˆ"A¿Òxc,d:¤!ò õ›]hÉdò]0æÁŸ¡HEÌØ q€œm´W/[‰Ùz1¿¢v1ºð—žML)­É–!×µ°Î$(…d9έõ&ŠX½ùÄ?òÓ��»È²á(ÆGÅ/}ä�¼qršåaÒ5/Í뽓FãñÒ�_»ùšµe�9i4–¤¹Öؘƒ,¼ðq²%ÿÈœ°i-H¿ëË�A�ÙN�i�¡�pµÏ¹¨µÉ]©S¼Jzõ³ ïFÀ Í™[+Lnú³t½2•ÝÚBNË$ÖYW-=^û³ùÄ{š¬N‡²Ê,ƒÏu_øl"ù?Ñ4¡9R¢¹9XY¬ƒiüp�ƒ"Lî÷,!ž¤ÅÚ ¬!¼éKÓ0™<y3š·Ò—ñ& NŸb5O õb(ðG€G½‡`�†ŸüŸ¶¨áv!�É{¿,\ö{‰…:OíC`‡Q”*€ ”ë@¼RÝÒ çÜèáÈË’îìPûY³ÝÊ´8¨Œ9ÅåoD�¡ó�1Ã(A](ùg_sTa8R1¶[zàWêÒÜeF¦t=Gƒrïþ³è+sù¸À5ìñ̓»ÑÝ^�Ðaµåν«÷�"_MœV¡¸K€/O~sOÿAm2Ö{Ä#¶Öíê܇¤«à£î¢ÜVm‰-¿gÓsÖ_œÈóó‹Ë›Æs ™Ÿ€8˜u v¾ø[ ì·r:6ÅmKÁê’Ië�}Q4ÏÀ5ÀWŒâh0ßKðÍœo/.¯±ù]Ælf==êšíJ±Ã=*LúævLtüvÓYÉ !²§·Åæ÷”ãºá‹YFÔ9ÏìkQ_X(›‚gßÅ 'È*ö˜ÔÓ,gžY¸CʹY)ã4¥ÀÍÚ|¼?¡ÿGü;ß5pJ‘[Òw�™Û^nòáˆâ½CÝËþoÀ$ëLTaâ~™ñ+«™4oÝB¥4¯äs‰öQö¬g„Ò(èðµDt¥a•ÉØžSázÚ¤ãëý0ñ­#wöÄ`QÖ‡ ¾–Ý—ÿ§»·/D£î“‘û¿çà¢x‚ óó– ‚ ¬8÷xœí[m�ܶþ®_¡�Úön} ¸éúÇnŠmãFŠ"È®Ä=±–D™¤îÅ¿¾ÏðE¢v¥µt·v À»Ò’3Ãyã3Czó§·?½yÿŸwK S•WÉ&|p–ã£â†¥YÁ”ææÕYkvç/ÏÖxo„)ùU£äöâ{QËJ°ò­ÐF‰mk„¬ÓrSÈ\oÖn`²Y{‚[™ßùË+m˜Á‘éïÒ„0#Ù°´fu69þ"ËwgW›5#ϧ¤q ùÚ4+™Ö¯ÎŠéNcr&ó#Kû.ÝÜ0uÕlÖô‘¾J7Û+Kl{•>u?åî·•y瞾ݬ-áͺ9`Š¿s<š/ÆO–ð“¶i¸zâEÙé*}#«¦5<5O³¶jKX膧yl�][göËÓ7oøö"q“sqVTéógŽžcº'2ä�¥¨4sÒh+˜¥rG_ë-l+JaÉíyf<¹ž:¿aeË ÏSf,1z]Q'Îa1G%ÿsÁËR4=®ÒTqÓªÚÉN )yÅkƒ%${ËyÔj’árFW³†mf…Ž˜;b*x–úñ]ìÇâ¨#7£Q4tYQßp$ª®+ùn/¦˜tÞæ˜ó~lYmDÉÓ§ÖƒœÞH–:CrÄ·r-w†�jž/|T'q5p"zà Ÿ±½á zVVÏŠÕ×V¿lR£³­ßK±&Dš{B–ä®Åf«8«gÙ�žÄpUl8Gu™åhΉLÕËb‰Î±Uà¾|É—1N.f›‡†~ 9ºKMD³¾€‘,Ùyf | CÕüº”×¥ø0ËVÝ蓘«­Ä&‹È/³&ºMÐMç¥(¤ÌOdÅ¡˜�Œ³ŒK†’%Û[·aŠU™˜e[?öQ¥MïÍÙQüœ1åEsyz]þ *¼ø™PÄ Ëd½9¯3¿ë}@V©¦mheáªgûZ´¿’YnVÊ[®RVç©…èIº•m�3%ð»÷´¿¼øfl vœeE/í#7ð… Z`–n͉“oÔ"·Â)ËsA’b}²¡/:Õ ÏÄN=n‘I�„ÄôÜ3r& GuÝR £Ó«¦ ±¡aÛ’Ÿ+®p@”“=í»Á€Ô #ý5 æž¶Rå\u�º²s©s0$Þ(Q1u�`0 ÿ ¬/7Å«³Ë—ßœ]íin³6E<æå³0&R¦´6î›ëTXÒù�´«ø^—MÁžtaaòx ¥ÿ:Õ+™rhœÀ8y›C‚^uîÎLqvõôÙêÖäÕUâ r/êk;C‹ëöÉ™³ä7ܹæDä]¤é{r�œïX[ÏüÙų­œErĆÄ0áùn9¤‰cxÂâ3Z ÎË(|n�,ÍPAß»·õðþ ½¯…>)µ»ür[„'–âÝÜñ¡‘ïïƒ>BÅÞeýœÑHö’ƒ¼SI,VØÚºg·Ea¿ÇSh <±À]‹h¬Ë{,ˆ×ÃÜA£vÂP¬a�93 õž„BÔ­@õ8Ä«YÈj2,î—_=�Mw]ž/ß#g¶šuîÛÕòvÚpWšÌÑ}àÝ’@/]ó æÕ»¯Ø¼šoâRšy6ÆÀ XÓN_„vEx9¤y”çÏíD mÂ?}‡!G¸wƒ9®I̦€ý¦÷ÙpìÇøÁ\ÇÜÃ5iúãn/]!&€*|ªr)º`¨@¶ÉÍç²í½—ÏħËW!R�·t À&ªFjL�7ã’´À`y�<Ðñ9X#ZY‚¦"Ò\J:¼dq±þ?Tš•ÈÛßß7|)NØG | BpíÍ2ÑÞLŠ…B4–4ê"éÖ«æÉ±¬'; J!© !È 7L”Ö³g#Ÿ™¸ù9æùœ»•òڪǃtððþF²À«Äé¡Û¾UX3£åY.£i’‘:KQ;§µ€°ÞôÓóŠÒ_äÜ>hv)3Ü[£º¼²J)I€<|§¼·¨6éa-| ;˜d¡ë¨¬ÚZñ�wb§Pu+[=œä½óÐTljJÒ�šÎú)*‰¼5¿Ð@"=nNäò5±ÁŽ£ƒ!l*·[U’Ö0FW$îöl¯±Ž..PH¹ŠäpŒ¤B±¡8#t˜ÅoýkÇVìàÕP·Ä+âÉï �™U p[Ù>’èŽY¿äö¤�BvÙ±´¯—CC%ws»Ÿ~ìCâ/ûÝÄ€©`ro~õØ*0š<"¬Ûê$Øò‹ÈÔ�ƒWS².û�¹{û»–Ÿ>47™?Œ 0ퟨ@™!ŠcÛ ÓÂMW3E‡œÑùB—Cmçä…ÎL�z‰ç&‡$J¤uœl`»Ÿ8EñU¸ECÉ`~$Íï;o´ß÷­,O ´óú¨°·o¤nÓÄÔŒ…Òi¸˜ñW0�Û/Zí›Ãûq×ï›á/Ç›á4¾o2_>»LùÇÃ2žï/Cóª 8Ø­ÇÂ;Qh„…|Ø!TE¤® *C{21z~Hv�.‚ÕÈQµÚ`v“ñRð´~³J¦sˆ{/ëƒk@WrX�ì… Ðð5Íi†Îm|g`D§_a¥Ó¹ž„¼¥°�g©s÷˜ÿ.e>’öš,‰S\l²ßN}ÏÂ2Ò®Ý Éúð3³±GXt'îú^ð!Æw‰N¬ˆJ:Œó+\‰»"i!KJ‹+:!±çmJÞ®â.g(¯¯ òAÕni»•øä¿±�Œ?&‰Òoâ�Däî`…H2¯�~"Ý}à¼AÕÊÛÚÍ�´÷ƒÏy[¿÷¶·U é{7…Ö.GÍÖ3õ­¥N•]¥vHêù©žRÈW�‰në8Ôd¾ÀvÁ1‘¡|Ý:ºµ8;÷Á²e�„jßáféw�Î(n²»Ot;z„c×ßíÎÚm‰ç½Ò’R^%® ãŠ=lr«‰uP–¹=H¶FƒGà‹ðÍÃ7Ò¡sÄÅ%H岚UA¹¡�ºb0¸=è�¼å>Ÿ“ψfý›³ö/2Yv/&àìé.0‘Îe[ýɧÑDýw^s_O wºi¾Ï¸ó€k�žÑÄæÒ'q? h‹H �^Àš¼aOÆÊ’çþRBJ�Æ’Žº‘ÃCç<¤J­ªÎeÑt½¼ýÆÒõåsQQg‚t½žÐ¶Š¹VRцA7ʉú>`ÏÝõ5i#5·2êç3šÆ>€óW™œ2�2Èá�•˶ò=6VS�WÙžbO&t£n!I¿í›ÕK…ìlq ’ZgÃ+œ»ãõ]L!TŸxB¨M>+�aÜI�uB–è² oÚ0ºß“¡ÞæÜñ$—ÝzÁ Õ¬»Gã¢|�ÀF¡þFþ˜e¹0ø$æ3±ùzÊSÚ‚€Á[”NØù½§w sö‚)!aŸ”Ž˜6f»ò£bäÀjÐ;½AÔ˜ÕŽåõ}0ÇÕþöWÃQÉïïv[–ï eYF@)8[µýDÓäÄÍ‘0 ø/c¶Š*….jüªäƒnZ×.‹ãÓTñÂ޼͖‚E1íØ"Ûzü?°WJeHÒõîíPG?ë¿m�¹2i½ì¢W&FÀ�–¯›? 0ÖY,§´êŒ z.9½‰ª7vûañÉ­ $�8°IE$>‡�Ç: Ø©rÀÈñ—€H¦Æ)<€\Í›/ªfW†Fl ¶´µ‰ÒOÆ—EÑZ<Ý(jÚËÊq¯/ºéK›Û‰Gª’©¨‡”=Êäjz=é€Kfñ«%·Á:ê‡ {Ìàà!®V]ž˜§™—ÉÇ9°{ÚjñëtÄø"¼ÙþÐø�N ¹Ì|^×è�òAç¢Fäm%m3C ›§s^ñoõÙ§n-LcÀ¨^Ý]zå#ØtïÆš6UÇê_6Øá ’&”]i쨳:ìHÕ¢I£îƵ !Å:™TÖ¸l8'ÆÄ¥YʵªÝ"õ\±ÉØ-§eYV‹A $'@è¹±Žb§KÀÀ†< {n8‚Úå¡Hk+àÊTÊ¿µ7©ol6@ËY7uƒÊeÂSÎ’kQÈZ1 Ôƒ #ß™’>墥rîI?ÚÝ]—pAxÑl«mL”Ë>@?Ø–«â9l˧#Ça¼5RY”¼u$~žÛ›urlšW7·0wQ©Ó…íÀHÌ˪֘B¦§E/¤ÍÏŸöŸDQš˜ù¹–Z¤¥6ÊGf{¼OJ�qõ41Æ¢x¹é½íÕ ®/©)ë—ÜTf8bÙ+Lø,ÑwA_iPÈø>š@=bß¡3Ó‡„¤½…¨[^†,œ¨½e�2¡/[ÀªSSÜÓ‰‹„ÛQÓ+­¬TÖ<h2÷hC8ç˜?1ôëØ[|µ¥WÈ–•à”­Ò5RÝ#L ÔX^»øâÉ7…23îƒÃ'Ë ÄÓ�§‡!Iûw2$¶k v÷–ºóöÌöƒШ¬d‚žgÇ(êi2F�•‘�‡÷šm-=ëb° ¿Jmýï`ÿUÍ¡þ‹s‚ —j°xœÍYmoÛ6þ®_Á©(Ð�µµ(Ю€-i†|X ݆} (‰²¸J¢BRNóï÷�Ô«+[NLl™¢îŽÏCïŽòú‡‹?Î?ÿsý‘dºÈCoÝþ0šà§`š’8£R1ýÁ¯uzöÎЯ¹ÎYXI­~岌h]ÜкL˜T\iÉ£ZsQ®'è­ƒÆ`$’cþ§Piª!ÊcõžaÞš’’ìƒ?+ï‡ë€šq^RÞº"qN•úàgoIªð9톡X$l„vc4«º*c>$­üY«`yÓˆç\?�d MDô/‹õÊsº ß¶à uöÖwæ~!ÚX¬\c”Ä¢TxªˆHIE%&TCá¡) —“„©Xòʨ¢·Lˆ¢E•3’P8C*$¡‡©x#.- þœ± Íq®`5/kQïŸ!�èŒjX¡Íˆæ.‚¤”çµd†]AÁŒS¨Úrg‰‘ˆâœÇ$4áåfå‘+mÆMXÊKHDDÅ|»É!ë-•a„½°LËMJF«�Œç„6´(©1q ¢ø*¶e’æäž>(‹E2€çÇ­¥·ã!µÙ³9É%×06šRŒbW®V`k§Ê�•rmäZïHë2n× –¬µóÝÈà=×IùWÌU?E[š×LõƒxÍ(ýÂæ†ùS1‹Ëx¦–u¬…ôŽ�òbgq^}³/Û±µðú ŸqÄ$½I˜Zþ]‚6ŠÄÆ2‘Ëp……òš_Gi^ecÖ1+•�£®T²»Q‡°;[}37`«MØuÜ:/ÉÄÀ¡ W5 ÈM]°á…ª¡D\Q °Ï ãÁv¯¸Í˜ñM–?ÉbQÀ\âvn­ö8¯]€)󺑆Ûm&î"rx"2–‰"Ö3Môõ†Jð1O("3„¡�£D€ËZBZÂ.ä—Ì`4˜0=cZ’ˆ!ôâ¡G(¼™0“,E²ÒºR•«{þ…WàNWBns´vŸ|ýüÝ�Ï~ÓÚ¿MFÉò)¬¸„Ûò]ã�(7á ÃåJ kM—GzøC?¿ý[Òª‚Ù•©Až5+ì÷KMáÁG*·^àüá úeë%Nßxkœ¤~Ø4¯ÍË­ÓFãñÚU;võ=cË2qÚhÌis­Mìt1Û~¿Y§›ó/Ì)›ÖŒöMWQõÌîqLkàQJ WÎ�£60ᥩڲ7ag~6’ÞB>¢9scúáu×aƒÍ7¦¸¥‘­Yµ ×:i ÀïåÙ|„7©Äo íëF38¾7ãjÄõ@÷B´™ÍoÜÈq9´ ÈáÍ'ñÃÝž)è×û²Ai– ò©.®»ÒÜG·S !»Œ*úe�w8Ílúø~»ypRÐ/úЀÀ wŠÆàñI‘ùËÖ¬®c’ÂÐùw«Þeh|–HoÔ­Dßžß?E‘Ø@½ ä+Õ@aȃ›)З9ÝØ#µn¥öÖ¦ »Ñ¹@ÍÏi³�+õ�Säú§æ$.ù×¶šî]‹¡0§ô¥7É•º4G½µ¦çà¢í"qeÎ]ÔT]srw»—@ì°Ûâ ác.hk¢ÇÕ[îÔ[¿»»'¬´†ö�˜)[Ñ7Õü·ÎZ`îš÷uQç%[6®Ú£$yq~qùrõËýHÜRàGpÀ±ÊÇpá‹‘¹“àr'ç©`sÝÕ»u°¤åƾÝÛï¹K�)EŠ2×9:FæñãÌo˜ßyFêôX”l“‹ NŸ~Ø5ç¸@Ðí'Îrž ‘œ7›gbî‡Mc6Љ2å 3é·-ˆ” ý{ß~õ™l~&´Uó‘m/ü>¤]/Ѫ\hpÀuòœ‰þ>Õ¾Ò_·©Ý4McÏ)95ñw°Eº w � 1$‹D~è~§hýÆJ&Í;a'B¨”毙T¢}ÐÛ–#¥4 U\æ6ŠÒ DeB¶å´=˜žVPkÏ=~ضœí؉�ÇñÆq™[ˆîtüôwwÐüc¸?æÿ›â�® éé–‚ ¬*:Çxœíi�Û¸õ»~;À6YÀ3Î�nÙ¤[,Ð#@ƒÅb?Ðm±‘D…¤<ãüú¾ÇC¢.�äѤ À¶dòÝ7™ÍïÞÿýÝÇøIužm£�ÿ`4��œiJâ”JÅô›«Jï¯__­á½æ:cÛRŠÝÍ�\;Zåÿ U‘0©Þs¥%ßUš‹‚ü•éT$j³¶¢ÍÚÞ‰ä„hn·JS [x¬~ 3ÂÎhCIAsöæêÁ}7q²¿ÚnÖq¾|˜p\8^Âò’ÄUêÍUúŠì‰E2�õÈæHå¶Ü¬ñƒ¼!›ÝÖÝmÉsûSb[Ù•÷öéûÍÚ Ø¬Ërø»†GýäDÀ“Að¬*K&Ÿ9’:¤mÉ;‘—•fD§ŒÄU^e É##I¨°}UÄæËówïúþ&²›~ôœåêúÕ•…g‘vHz' ŒÄ–eÈdDìñkd¬ŠîxÆõ©M›Øý‡ÅÚ�k ³#Í*ªYB¨6Àðàò"jaöÌœ¥ü÷)Ë2^þñ¼H‰dº’…¥ÉXÎ ,DvÅMÔfg�›5èf–kñ¹¾ÅÇœëRû¾훟5ðrÐËڦ̋#à™bÒ|–M7dòQ£.Ïõ犚gŒ<7víét†�´\h$Ñ›oÑu¹‘|–ólä³\ÔDdËDxÏB° Í$È_ùKZŒÜ騤'[EC�5BRÛ Fh¹ Ö]¬ÎœÑb–>qâ ÍC…Zèó4Š{RaC‹:E‡{ÇY£§UZÂg« ·<¥â,ü¹ªÃ]O <všú<_S�;dâ�ñO³tXïZT�ÀüK¨ÊÍÍ»Öb—¡üJfŸvB‚=×�*7{qðÑ^JžSy§Ðþ¥À_¢Ó7W·¯¿»Úv$·Yë4\óú…_Ó.ZkûÍZ è¤ Ú6¢o³2¥Ïj·ÐI¸ÐÀKTL3*m3€½Z›-87,¯Í�êôjûüÅê´ÉòmärâÅÁìPüP€~bŠêÌØ‘YÓñ¼B>¢ $lO«L;ä/n^¼‚ÕÒj$ß�F,ÿƲƒ’8'�Þ-�ÀhŒîsßÊá�’{°'IO¦Mï.R])4Nˆ¡ßÆ—»”ƒ{+Îü‡ $òãÉËÃêh؉ €¨Ì(Ð;ÄB�­�yÖ¹*pûN®L î+ðÆ";C¬hÇ\µç} øL¨¦Ðn ˆ¼ãмBÅò¢�‘¤gî—_Žn·Ã–\žCgN(Ê…§V›?lg«ÑØÝ &>Ì™K4Ô•ÍÚ>|ÅYÛ|ÕgBÏÓ=lxtáZ6`fUI‹ ˜_ -báiËÂkÒüô³gÏ`É™úÞ™ËFdc…þ¦ºhäsøl;ËTîÔE„ü¼ï„;ˆ¿šr¨J\¨³!>¥ÐàìG w'G?<#ž:Þy�‚¸§*(øx^ ÄËÀˆ¤°X ûêñô ´©Ì©Æ|E¥‚r+ªë-S‡!êÿ—ZÓJ-´ö�§’Í­3º€�ú�á9вŸµÍ½—"  ü ru4˜¬ëL=n�¸U×#k‹5mñ máX½GÚ»y¤½uSVp Eg†«n¢Z`�hž�‹ƃ*…²§áù "�L?ÖzíÜzt`�E-Pj]hGÊ3cÙ“+§‰u-Äçj¦‡Ì-#WägoH hð*²r¨Ó¼ô�ŽH@ÜšÞ¨@ =¯NpÈ{ˆ×àŽ%dåa¦ r“ª"R€2êvyq›³�ļ;Z¿€.B8Ï•H‡E$$4+’Q¬¡cwîµEË÷`Õ n¯'»�Lì2:…MeÝJ¢>1¥ Ë)è…¨£%øŽ—¾º¼„”b?wèêö<¶�ü¥;Äô46böͯ®æòGO>‹*_´}ÚêcrÀUf´Ñnabß~|à‡b:óÍü�jcágIî�kŒ9Þ Á*<Ë êl&7K4J%ë(žD¢ »ªµwðåÂÈ!�ëöM5µöTáþfò‡õB3Jsü8±ùæ�PÜ&]ØSßzµ™°yÕÙ|S)7œîNäí@¾Æ¿ÆãúfÈ}ûâ–°Ï8�YÒeC± D�ådÍ�)m‰ƒ+LÉ6‚® %”†GcNGD/o=ÈzÒ†ey«dÉ+¥awSbFíä²v³ŠÆcŠ}/ŠÞí((ÎD»»é¸ TÓlºm¥ê'ÇáÕˆ™~NÇs å1º X–¼¶�Éo’æ3i`¦Ê¢0Ä…*ûßÉ ™yDÊ6²{.�²ÆýtJ�ïa-»ç÷Í,zÃY¢&«Wâ¢ Ãø .ù}IE†aq…'4æ¼OŠ»U8Mõí¹d9ˆ¼A•e-§÷<ç_ì€9Ô‘vÇ4Aø�Ü‘ŒØ÷8„ óÖÊ'�Ý'ÆJè"@´â®°{éýäbžÏÁ×> ·òÜ*ì {ñÝvôu�Ý·cšÖb­»¾>´—ƒÖÐ…v yîÁ¬˜¾z<×°[g‰X®L5Vï�óì(Pƒ½t;yºlR+Éæ%“�‚“áÁ#%#ƒ:gXíWy@ž=„#ÕÈuΩ¶Í#$ ±S'ÐæÙœ$*­‡Ç îœôݹ£³ Ë[2™ˆ|VGf·,r%¢u™ÐÃ}²QÿtŒ.¾€÷«ß îE,²úÅH¼ü=Ï3TÚh­¾¸0Ü«ÈÿÌ &Á7ˆ…aO'�* ß'\þ¹àö§C4’œš$àÖAÐÚ�gz‚ï��^â0�*¦YÆw©‚à 3ãzÈ~rïÃÓŽ+*‹¤O‹Â[¨€Û%¦ú\ á9Nv��zÖäÇf!Ö\HL8x‘+Y7‡l°Û¹*&b}'‚óŠÛè'€FÝM-+ 1hx‘«µ³*w3>ZàŒYš™fÆO;ƒ%–‘¨a@¹@hä’C47u ÁæÎ‰ `®¯ìC¾³þòÈ“L¥“YÖ/zˆ­B×2ÀçÝóSšâ½¥*ß#§ÖL¹ã×TMºS5LÊ×¼@­%ô÷_fiÔoZT­:Tkƒa,²›"£õÆ4Á#úÿèà-¡æ†POå#(l‚Ø•‡hWî —°µfýí&Û“šø©t7êZÌ[¼eP„kß„…ŒDÁ¼Á.ê±6tÝ;A2È| >tôìïÚL“Šk‰"‡eÕ üÝ1¸ ½1ÞE¬ N[þÙY…- )vIáÓ“Û£…$8kF|9D}K²Áî‡d+´@EœU se»¾�þ¸›a—:,à›å«°~Q7=†nj€Ï‹¾°€›ûwËÄ܆ø˜sCi×î?�®íÿWý/º¶1q µ µŠY‚•65xœÍYKoã6¾ëW°>ôä’ÅEêh7] —m€öHQ”ÅF"U’Š×ûë;I½vV ±%Í ‡ó}ó ¼ÿéáÏOÏ_Ÿþ@‰ÎÒC°÷Gð‘Q�I°TT߯ o~Yíà¾f:¥‡\Špû{!åSZ²°ÐLpws*5£j¿³rÁ~çì…":ë7¥±-FÔš· Á#Ž3z¿ßâ4Oðê°ßa³Àí°sV ìÝ‚PŽHŠ•º_%Q¬ào—L�lîÎ{uFûðàŒ…°h©²ßåÆìýEpJQŽ%ø«© 새½ú3µù¸²²¿¡\(¦Ù+E Ô°D¯8-huL@›}güˆtbŸS$âò¸ ¨¹9ôUˆ`Ž0!T©RÌnÆyZ~¯·P(c9º l€À5väåzë\Þ�ϳ‘'³Q'KEœ Eû3“J#•àübw%—b@Þ+þ/³ñY*þ/ƒl§Dðè½PƒÖß„ÀË{!мør³€t–§o·×˜Á!K¨4gC0�Z…z¥D ‰TN ‹Ï,c¨|d�‰ðÐޢ焩Ú}ø.¡KlOÏWEÿK‘=y^¨Ùз¤—Š{Çh?è ZÖVV3±v‰Á¸¦Gê¤ï³�$ &ÁX „jû†vó(´Å—‚¡kuóà*ö4«îc�¬~Ö Œüz³ßÑì€MS„¥Ägc¶› �˜NÅþ¥ÔTõLcÆ+l|Òàv:V.�ÑUD²Üܼ©†Òâxµl÷Qk<þa‡¡) ©QÔöîrƒÅñoS®H6+¿8zÞìpƒ‘°µìGÁƒÚ%ŽøNT#S&•]Â÷¢vÿ©ù3™ZÕ ÄàG:i…g7<8=y´Ã×�±p�0�‚‘IAW1äYœà‹ò»]ŠM‹}JÔOËÖ&!N×áÆáv��©ûë¡ÄÊéq¼-ÚCE'íí0™Šp8�°<¯?¢ yÞž.åáÌ¥wœBAsÓ¨r'¯ãéäLDtu•CˆF³ÜiÈ.Ež–É�³^Š�>/5>gN³*GVûüÑàÎ)�ØtÑ7¼öœâb¹©¶ª®ŸÄ+– s2¬ÐY¼Ø7M÷ñ©Ÿ"ÈUɾU§�ªøSÙ {sýÿÐN{·N¯þ[76¤öÈŸ|ÙB«ÂGÁ�.�•·Ø2gl˜� ¨T£Ú\YÂ�|M«] PHõ‰R>#C§¦‚ß-Ъ,ÅfËq‘†±¼×¤Ð ÜLG¡EXã-zìwEw(3â"@Y£P–—˜}knR­ Óqû(Žý½üÈDÄbF£µYÎäi#&ŽCPaIÑw*…M§`É|zTŸÍŽ.O&§°x&Uv'Ê\7øom•.g|Ñë·ÊSÂ`Ìm 4»z9—ØçæfEümÝtª†ã¦"¤¤*7]H×c�oW1NÕ´êHJ,3G?ò¼Ð�ó�ÎK.ÖæjƒS3ÜG…‚˜Y hmoD—½Hù‚èBÒn©ŒE #�¹‚DM#uçì懀”XQémùõœ™¿£æÚ¯©ý† T`½±¡\Õ+pD³vZžÏÖH1S&ì4a14‚VlŠ�8!cUÈÁcIÿß”'SË9=âòMeëMŒ©’�0‚±‚rŒä°éÌΜ5�K9{èTÀ ®áh™¢ÍEc祩ïÓ�v¥W›¬)†kŒ‹T—§“vœ}Œvü*�vîw›�ýuè?¡`/ñ‹j‚—XxœÅYßoÛ6~×_Á©/�XØŠCá Ø’eÈúí6ì© %Êâ"‘ I9Í¿�¤dIŽd9T‰M‘÷ããÝñî(¯¿»þãêÓ?w¿’Ü”E¬Û/FS|•ÌP’äTifÞ‡µÉ. #Ìn WJnV¿ÔJ]smßÔ†K±Žüb°Ž!™>Y‘?ÄÚPRžèwd‚TÁšAKö>¥ ãuD­¼§`%XW$)¨ÖïÃü-É4þ.ñhÀœÈ” �¬7S‚6PåÈ×QeEÆÄÒ8ètà nžHÚc ró/KÌ*ðä)ßµJ}ù6ô~&Á„J·Ö"‰«šÈŒTTÁ&†)}A()AV$e:Q¼²¬˜)Ñ´¬ FR ¿eR�ò(â*p}Ê™§ìˆkˆ ÃE-k=½u“SãQé,BŠKÁ¶0òŽd²$;ª8ÝìËRŠâ‰Ôš¥ÄHÏHrh`¹,RÂHØ* ·ÆBHYÆ(7O­ŠîÍAÖ�Ó¢Êé:²Cgó(‰ÎiÕ#.GšôÈÜĽŸÚB1Bñ¯ÙŽ)Z�Gú¤-ÐD1j°tº[±µ=V�É 7àOz�Õš‹-LÉñ7–®˜Õ"i½INÚU h 𑛜dü lÖjG‹šéNIÐh)é=›Só§f—�H£êÄH³y}è–‹�õ/¦ÿ¾Uݳp0�Ƕ5§îžùûp{.-$‚s™Ûß ®iöø¥ÑLB`BK5˜Ê{LHw†õssX=°Åa¸X°.d¸ }½B*`X®jPÛºd‰„ê¾ç7ØL9>²1ë„?†9ßæ8´ŠÙóËDêÏ.Îìx¸º6aÁ^SÿLÍdU¤=ŽØCI±ÙÐŢͭA?$ þì EêcvÃ`È�W{Çü¦V V� ú’:et#a03š *Ȇ!¯ÖØ5°"%¹bjŒ1•~EL¬ù=¯°[º’jÙ§ÈJúœJØ |Åkq¯± Å6þȰÕBK”°¨™ Hª¯ŸÿV´ª�W¶À¿j<v.£ˆÄ™[o†=¿e¿i½íùmÔ%iÆÍàåÜ\ì<7/ç®ZÝÕ×èV"õÜÌqsƒº¹íxaí°;t/ã-ø=óÌv4ÃýqßÞtì)ðì¨QßO]¡ÐÛ4Ñ´Pù›x/~4 ~Æ,âÂpæõ„ñÝ~Â*€œ7¶[´Uß~«xmÒ¦{™äWãYØ%۰͹´k׬üö ~>Ž%À$¡­6ÏAÜpXü°YÊ}h«Üˆ=�ºm—ø-±ô>@aÎŒ!½›ªÂ1, øC]Þí›Æ0<ŽAÁUdУ/‹p�ÇZ‡Ï£íÂY ^wW”ÐÞìÜÞòY@ÿåzÊ^?1 µœ‡]é²p?)”'ê-Û�Ç@v«hvè–…v«• Õ²÷0š ºu—UÓRMö‚ßÈýWÒÝUEÂz!ÐMŽm¢[%è$ÿÒv©]H0d¬-{À·úÆ^•z[hfŽ:áàzµ0Ø[{?¹ÆEQÒGO™¿™Çèš#ÜzO ”¨í?æûEh›™ßýÓÿìdú2§:Û‘6�ðóÀ*a—æö]—uáß© jb{�"¯¯®o¾_}­ËN@ÊT~V´æˆžS0óÅA?(`~Pó�ð5EÔî­Õ–ÙS<eK‚.EZ·Ÿs°-Í9qâêê�Úïy¬–ê|hÛr‹Tï‡s˜÷ï1=9+x.ez¾=¸Üœð0n³YCŠŒ§Ì–¦¶øk—F'߸tÙÑ}ØTRÍg’I˜] ¹[8ƒT…ÄÝÙ~ŽÞU0$ÚÁKâS2sAÝ &nT™Ík½ÞÐÿTpÆ øŸÂØ�Áÿ� ¦ì{Âæ×ª”}'Ÿ)Œ�FÉòàµA“…�¹@ÖЩJIÊvœ¶—›ó$‘¶×ãvtäÞÀÎ-uãcΰûÖûtsî›Â¨ùy2ò¿|þ`qnç ññ– ‚¬8_xœí[ë�Û¸ÿ®¿‚]àº9À»ÞM ½:rI¯(ÐG>-ŠÃ} %Úb#‰ Iy×ùë;ÇD½lÉvîØÀk-93œço†Êêwïÿùîã>ü™¤:ÏÖÑÊ0šÀGÎ4%qJ¥búÍM¥·w¯o–ð\s�±u)ÅæþûJÊ÷\iÉ7•æ¢ g:‰Z-í¢hµtÄ6"9 éǵÒTëïÈ "°:ZQRМ½¹\{'Û›õjI‘öËa¡p Ðz KJgT©77é+²U°1ÉÈQ¾#«=•ërµÄò†¬6kCh³&/ìŸû·…]ùl¿}»Z¢«eÙc?wðUÆðͽ­Ê’É['FGœ5y'ò²ÒŒè”‘¸Ê« ¬±g$ °­ŠØüòâÝû¾½�ìæ„ïýiru÷êÆÒ³L;"ƒ¼“”Db+�2â3"¶økd<ƒnxÆõ¡-›Øü—ÅÚ‘k¨³=Í*ªYB¨6Äð;ÐåEÔâìsTòß§,Ëxù§ã*%’éJVvË_1ù¨Ã–ÇösE Í3F^Ÿõr:££,g:@tÄŸ[rÍs€Ïò´ý?Ë‹Í/[æG‚=런·ft+�n%-vF§tT‹“-ÞHb�ˆÔ6ñˆ,gä¨Y¦Ê-NÚ ]l¬<4–¥8ÏZ¸çJæid1D§ØÇsïYt}ƒ$|’IpÙµ�biÎ5 îú †1d§™ÆKðµ�S°]&vÿtÒ>õÊ‹MT�¿…f Hϳl´EÍngO…H®d¹¶˜µŒ“ è‹ú’ͯi³ ZRIó˜Ÿ4§[w6ºÇ­�kj§ìw¶ÓLåçt÷PQëÁ¿Lp©X[ž°"v ÀÈ)ä¸ÍŒ, –ªÉ®Æ�d’Weâ‰IB‹„T‘�¨Š„JwžõÇWß Áœ€Ñ8m¤½°Ï<Ð ³Ôgެ|ƒyâ:%4I8J ç%þ¢ˆ*YÌ·Àß’¥•™Þ9F–CI9èQî*ì:ÉàÔ¸ƒ�‚l¨é&cw’©8@P£=ͳÖb—¡þJ f¿m„L˜¬¿ªÜìÅÆ¾M¼”<§ò� %üKá|‰NßÜ<¾þæfÝÑÜj©ÓpÍë¿&P¦]´Ôö7;H0¤“6iÛ¤½ÍÊ”ÞÖa¡“p¡¡ÿ–¨˜fTZ0�X½Í‚ºËkw§:½Y¿xX<‚5Y¾ŽœA¼Ø™Šï °OLÑœÛ3ëš#‘wOÈGt�„mi•iÇüáþᬖÖ" Ć€|?âù÷ö8¨‰cøàÃâ„p¨Cc Ÿ=ÄH?”ÄÐô‚?Iz0-lw‘êj¡ BLå6¿<¥ÂŽâÜØñA#ß¼>|“]gÃNÎhD�dFAÞ±$*liܳ®=AØwxreð$p[A4ÙÄŠvîÀU[®1Öàœ ÕÚ5 ‘Oš?@<,/! MúÃýø“ã±ÀívpÀ’y5qB÷^^¡y?´JÔü‰W»�æå¦iÿ0§go¤+Ïš1}øgLÓÌš }Ú®°è,�4[g!š³‰Î‡-g{iÚòÒZ?¹ëùäXrO;“OqCd6tðoªË†A½…϶ÃOuÂn!ä¯ÛN:‚ü¨)ÔàR‘MÁ)…†bà y¹\µ98ùá;ò©ó‘� ÈKª@ÆóR(Ø öÓ"’Âb±ƒ<éÑò騭ÌAKñ¨E528 Yÿ MƒBèí%›‹ºÈ~BàxŽ4¤ÝÛ¶»÷Òü Ã…C-�‹i]Iǽ±€°êºbd}±–-”-÷D{7O´w£�bÊ>— 3\uÕ kTs{,O˜ªꞆwÈ<2ýRë± ëÑ‚N�Ô†È $ö”gƳ'#›‰¸òs ˜æ”»ebgÔã@8ðpþ†² €‚G‘ÕC]ª¥?3Åã.ƒi’¢:3^X§5€`»P�ߘþçv™€d�B`‡}ªe�W“�ßɵF lß…õ6h:(«2VìóŽÌÌ㼨D¥Ú›œwöMEǶD5€©­O c�·¦7�H�›òò5„c GyC˜)¨Ü”ªˆ`Œº�]IÜÖl§1Ž6.å ¹å°Œ„„fB2ŠH {r�-[¾¯u x„<Ù3(dbж”u‘D}ÛG›#§`O,¢Î–;^ûj ”b;e€éÖ�ì=ÀÅ&È>ùÉá)ÏdôÖ®¨ò‹qäW‘§¾²^eFë”ë&öé¯VvüPLd~Bþ†´~…d‚î‰ Œ 6Þ­À*¼o †îuŽ4“�k42µé$žš¢ Q:TÙ»ˆ€r>réá:jƒv¢Öþ@*ÜßLΰž7£(GÀ�ãš?`“nžÅmQ„­1õ­Qû0Ã*o:[*冻݉¶h7Ãì×ÃÃl\ß ‰ û\A Ð˜%Ýc(–�*îÕç1ðÍB\a TÀºÔS:�5½|ô$ëIÂæ¤È+¥aw£áVïº~³ˆÆs‡}.ŠÞ›7žD»ûè„  Ý6ÅIúÉkx}? ÓŸá¤ãù…øŠaž%ïì×äW)ó‘t?ÓdQ˜âB“ýr:hf†‘²�æ–K�¬ ?�R{ˆ5·ü¹™åÎb¸JÔbõ (ê0̯à’?÷ˆ�Td˜xÃaîˤxZ„Kß>K–ƒÊ[TÙ£åô™çü‹â†6Òîš#H¿‘»ÒÛÞ !ɼµú t÷‰±P>¨V<vo ½\ÎúÛ*m‹®7. {0YÃÏ'Š÷COíÓzy”–Û{2QÐÒõiýa -_P˜~z7×ˆÛ Ét®¿,QÖ_š ÛP{ävÑuU¨6®­g¦Š7²ƒW9æøu­±^Så�xöò‹dTã‰s¾Kµm ¡Ðˆ�B"8‘€öÍÖ2Qi<<_HI? ¦µ=«Õ2‰ÈOvZvÙÙ¯´^‚ó´®:†ŸÎÅåÈêeëÄ"«Œ@àë¼{xD2›�Õ—v{ü/¬`|šXö6$‘ðû„—_Îx#Ñ1)FMÒwë Ïl ¢¼@ƒïT�¾ô`LL³Œ%î%‚ƒÇ ¯¶!çûIºO+е,’¾, ߌÞ®Õsú„ç8iAêÙ�c…\s!±ÀàKbˆ\Ý\°ánçœXxõ“æû·ÑO@�º7•¬2 5 ³E[gUîfn´À™¯43ƆŒŸ>o`؃DÍ”K`F/9daƒc y5ïh8çú:}Rðó—3n•NN&*Xsñ¥¯ ÃÆœ÷›ÒßáIÅî9µ.x•÷×Á@ªIï ‹òµ_ØÕúsÈ'­å^l2š¬¡:–…MQo=1Íêˆm?:z×0a#¨—ò ›äsÄœ!Û…»åU­™y»ö¢&~ºÛÄ{�}Là[°”XÖ¾Y s»¨w´¡KÝÞMŒaæ‘ìЮ§dšV\ë9.‹fpMÊ4Œñ�»àY¸eg °QDòIáÓ\7Û}ÜÙ"¿²µÙp÷ìÙ ¡"Ϊ„¹fË)ßNiÜPs‚xœŒCXsqîÃ4çeMXÀÍ;d×É•�8ð1)W†\ž!—î?ù-íÿ#ü¦«3À e Re‰i‚“VËxœÍXKoã6¾ëW°>ôÛH€ŠÔ1Pl6@ÝæØ#MQ6©%)'Þ_ߊ¢žVd¯ b=8Î÷̓ZýöøÏç×o/_ÈÎfé:ZU?œÆð“qK ÛQm¸}˜6™ÿ1[Âs+lÊ×¹V›Å—÷\I.­ é£0V‹Ma…’^æ\[ÁÍjY.�VK¯v£â¹]K-H fîÉdu ­(‘4ã³1©EVÌÖ«%E[w£îÂJÐ{ sÂRjÌÃl÷‰$þæpkA Sñø–ï+'dµY£Â wb«eŽª×äoNa/Tƒë–ë¨|‹}e43óO³ré_$WFX±çÄ0šRMö4-¸ƒƒ2�?…Ü»ã$C­*ÁëˆðÚ;7·H¾©‚0XIãÆ8ÉrWè«»ˆê-µÇʺ8ðNlezÞë%¸=‡æÍWX>•®Ü…1ê«ï#öÚèF¤DšátÛþÀÙsf•&&çL$‡ 7Ù7Çþ!;Q›ï ½ ¯;aj÷áZC6Í•Lç@òµÈ^*š©x´„. FGw X°�PB¼B™ð‰#¤å[^%P„µ�$‹Fº42! HÉÉд¥.ŒMWù8øâ$lV< Ï©ÝÍÖ·¿[Jþ¼]-y¶&Œ§)¡ZÓªí¦’!oÂî§ þ¥ aJZ*d¬J/ÚNÜàáÕ€{ä†i‘ãÓákÈ^ Ä–‰>”�×ÿ#@)1;¥-‰ÛÞMÇ6º¸ÿb9=-K±kAZi@s0èœÃ…W]HFOIïZâ h*îƒ_¿u=PCHÎ…ünòN/µ}{$�en0=Þ?Ÿ„6¶›õ倚ª7DW2¦úpÃá}<¼¸ÈóÆâñòÈ…·•ã0çu³¬On'%H•Œ:Láv§âsøôl(‡ûYí©T²ÉÇ—Ñk5€¦…>hõ[Y­Å{8u„Êáï>ì ­PÊ3i·ò—Ææ¬¶[Oû0TÑu€s¹‰§[Мi„œ�½>@Þ(„PXä”û˜Zº Ïý¾ãOR¸<" H�.«ñŽ<“Î r‰6|u“†7Ñc`¦b‘ß Ey4°ŸŸ\«’ŸÑúlžp'³ÓË]‹šAýH1éð‹ƒIUZú]ª**mò ã=P: ¼Œu¡E¾â»ÖÜ@bƒŒëxSuˆ„¦f\r8.:¸>˼°��“[K%péÆRëSá9) „±¤ƒµå wfoñU Ö̺W¡•Âd�w�¸ilî½¾?~¦bíŒ�·îò§ÊÑy7nn!j— ¿!Ä´7,0.Íq Pv•ÝÀcU,pÂÂ#Ê*�æ…“Y9xÕ sŽ•ã¯—LÇ'øŠ%Tª†ïVÛ€T{÷¢¡º¼‚øÂ©-¡EjçaæâYxº³`?$‰æ?FC"•œK¾¥îÛhëÛÖõР¬à’NŽQÔÒhŒj+1ï91Z"=Cö/ýgôeùÍþ?£šnP‹*‚–XxœÍYß�Û6 ~÷_¡¹/-ÐÆØŠC‘îzÃ=´; Ý†=ŠMÇZmË'ɹ»ÿ¾Ÿ$ÿ¼:qÚmY€$’%‘üHQ鬸üíâã_7oYnÊ"Ö]C/ðhÀ#‘)Mà­7 ü6ì¨ÖQm9Çl´ÔiÇ7¢æ�¥#:&7SbV�§JÅ®CTê¯BÏèæñà–´üX"+�YÍdÆj®`6CJ?g<`%Ö,%�(Q[RŒV)Ó¼¬ b)ÇFgR1Îh0Q C˜sšÐLô�ʈª‘�Þo�;ar€$^ ÀÙzÇU\6ëȶÌäÜx54úÄŒ(‰mÈÜUŒv¯™¨ ïF ­eXi i=«ˆq|5(àÞñ°“,QÄ ¦Ž7zðh{p ³+aÀg¢87ZT[‡Þ‹É„±ëºmÌš*éÌ NŽÛEì+{±LÜS:²×Ž éAHÐJ)ùgZó»&‡Ëº�QMbà ˦`O'{ô¬7ÍØ¢{½c¿Ó­ŸÃéNi�1¹L�@º‚©ƒî½gûÜKáE�óÉHB•–j2”)º� HwˆôTo«vfw?øjû-fçpÑ1ˆ‘Kà,¦ë Ô¶)�Gs=ÞI˜ü÷ícd]Ðùùµ±G/Û¼x`ŠY‚[ ;I£÷xŸ�³ô’ÆGe!¸!츂¿�Fεlˆ &†��á=dõ9@Növtz¯…Õ |±¾äNßHØË B ‚ʆÞ(Èá¸RrEâ¿1µ~ET­îÄgQCi¾’jÙ§häIŸÒÉeó}tþŠê”YcBVÛøAÿBK\6Q;°â؇?ý©x]#¢¯ì5ý¤Ý¾pØGï<’¸Ûâp´Ùɯ:ðô°v’faìÛo¦ÕÎÑ¢ýfÚº•[‡\U¥Ží­0¸µ¶=%Œöçï›( ñ™©í,Ð~è“�žÜžGn;#Š�6ø”ç¬ mz“¿Œ{æ‡Bâ'LÂ!Œ /,Œoú+ì^Úd�o\"gT¼6i›$}—€'tÙ„.âó!¥²âð}7I üp8ÿ=¦ñÃ{ˆãÇ#sxoöEÆÊœþû¦¼éÓÀ0ž<ÎÇ‚ âë${<%Þ�µ,OŸgÛ‰3|9$Ö#Ø£Ñ9ð£é3Pá—=ŽÐû�Yàc§~œžüG…ϽՇþäaéÀë)�^ëáþ=ÌA½*øÖc¦[µ7×ú_åB"q¼Jhä,ÃàœJÃ,CÞ¦Ä}—ÎC¸õ0wZÿéá_ë+[‡ŒjGnУÚå¤Ð¯m¡p‰ŠþÔugϪ¯;�Ó#v¹ ª²c\*êîþ£S Ÿ³wùÄ;ÿôï$cÖ‡MãÓ6+ýÚK«­u›²)v4½¦»j‡=½¸¼z¶ú‡»zSèô§S«¢ 2Dü,m W)Ki'xWß�C`êJŠ0îzŠ%: ȨÂ?KFï+ŒãMÝç·QûïdäÿÿüP\�¿ •|‚«|9Xxœí[m�Û¸þ®_¡.pM𮳤Wg�4wWèK>-ŠÃ} %zÅF’ò®óëû II”-9’íMs@� lÉäÌpÞøÌ�YýîǼûðï÷?Å™)ò»hÕ|p–â£à†ÅIÆ”ææÍUm6ׯ¯–xo„Éù]¥äúæ§ÇJ–¼4‚å? m”X×FÈ2þ7™LõjéÆF«¥§¹–éŽ8ÜÞià ¦ˆDÿO£…IÑŠÅ%+ø›«cSn’tsu·Z2âôò¨¤4”_bd'9ÓúÍUö*ÞhÌOdz|™?Ä«-SwÕjIñ›xµ¾³ôÖwñs÷Sê~[¸‘�îéûÕÒÒ^-«¾ø»Æ£yJþx²´ŸÕUÅÕ3/ÍžTwñ;YTµá±ÉxœÔE�Ã`[§¡q6u™Ø/Ïßýøó÷7‘›œŠm³¨B_¿ºrôÓ=‘!ï$]ʼn“F[qÀ,–úYçak‘ ³ëË&×ÿá‰ñä:ê|ËòšžÆÌXbô º¢Œzœ›Å•ü÷ÏsQýñ¸JcÅM­J';-$ç̉%D{Ë9k5Q9ƒ«YÂ6SÃḦ#1H'8ôcèÐâ¨GWƒÕ÷]Qn9rسœ¸SŒzquÌ‹?Õ *ÈyüÜ:r#§÷’åD¯ˆŽ8yO®“¼â“šìŸÔ¥|Bõ|‚è¸ÄœÀp…+«pÅÊ{«h6ªÚÉnÐI±FDêÛ}D–²Ù)ö+8+§�Æ^Ê‚EhAGxž iÎ…lÖÉb‰N1ZÃ}/£'³R*æØ‰F?‘¥é¹¶¢YO`-Kvš½ ¾’ÅJ~ŸËû\|œj´vÂ¥ìV‚Ü_CÛæ™ÝFé¦ó\dR¦2g_ÌVÆIVm‹%›¿OžbåŠ)V$bª�ýðsK ¢Ð™µ%ú%£žëPSÙz½þêë½ø'�Ž î–Èr#R^&~oX�¸R�ÛÓÊÂ1TOv»`1~%“<.—\ŬLc í£x-ë2eJàwïuxõÝÐì 8K²NÚ3·ù™ ša–vÍ‘“oÐ"Âd1KSA’b}²¢/:ÖOÄFl®‘]�„ÄôÚ3r*& Gu_Sé£ã«¦ °¡aëœ_+®áŸOö´ïzb7ŒôW�˜{ZK•rÕ>êÂÎ¥DŸx¥DÁÔñ`þeX_j²7W·¯¿»ºÛÓÜji²pÌë͘@™nÐÒ¸o®áaI§}Ò®R|›W{Ö†…IÃ�–þÛX',gÊ�wÂîäm/®xѺ;3ÙÕÝó‹[X“w‘7ÈN”÷v†÷%ì“02gηܹæHäÝÄñr�”oX�ÏüÅÍ‹W­œERĆÄ^0âù7n9¤‰cx߄Ŵ@=(–Pøl�,NPyßÛÙ:z�Þ×B„”æ]~yÈÂKñî?ìøÐÈŸv�>šJ¿Í†{9£#í%3yÇ’X¨°¥uÏv_ Â~�§Ð•b�›ÑXæ;,ˆ—ýÜA£6ÂP¬a�)3 å¡„BÔƒ@± ˆÄ‹ YÈj²YÜ/¿z šîº<=i¿œÞB¨.×AØõö­ù-¹þö4š¬»ÎÁû9�ƒNºê¤î×û¯Øýšeë\šÉÆÆØsPÌj)Ì‚@çÒž�sÎuä¬çÈ­TMÛñÀmßcÈŒî½bЧ³1€D¿é}6û4>û11ÕO÷ðNÿe³—Æ�W @>…¹Ô�1)kޤçsÜzçåÇ3ñióX8Ègº�E%5&È-Á»(Î0XÞ#¿6¨Žø¯­ÌAY RÒŒŠZeñ±þ?„š¡ÈÛ?ì*>?ì#�î!8ž'�Ìü¬ïî;Á ÃƒÂ¿°Gƒ›p»�{c‰°ÚwÅÈùb+[2([ØÞ>íÝ<ÑÞ�Š… BLæ4ê&jÖ©æÙ±¬G›V!© !È [&rëÙ“ÑD¼Šüœ }ÉÝryoÕãÁ;xx#YxáUäôÐîæªY3£åY.ƒi’‘:sQ:§µ@pß  ç¥¿À¹}&mSf¸·FµyeS’yøN¾³h7êà.| ;˜d!í ¬ÚZñ�wd§Pe-kÝŸä½óÐTllJÔbœÖú1*‰¼5½A"=nNä#ò5±Ž£CØîTn·ª(.aŒ¶ ^IÜíÙ^cM8º¸@u }ä*’Ã1’ EˆâŒÀPð¯[±�WCݯˆ'„B&V{€ÂmeûH¢=ªdÝ’3Ø“6 ÙfKÄN£}}RTr3£Sꇟ�Ùï<6À˜**÷æW²^£GŽe]\ j>‰XíY4xU9kÓ±˜º·ßúèCs�ù-ÉúÛ³Áå — Òø7¶ 1.ßx•ƒQtt´¹ÕvZ.QMTª—xj®ˆ‚ëÑèÁ¡`ÀÈŒ/Ö-JŠzó©h~ש#е¾<�¦ý×ý@õ¿}#µp›)¦&¬)©ú‹nˆ5¦sûH­}3y¿ƒîè]óüõpóœÆwMéÛ·1ÿT#lXÂÓýehžCÛõXØç  �°Pî¡Z"�pmP¹Ú«‰ÑËÛ†dÛ#¸Ýƒ"E­ fwÐ1./ë7‹h<“¸÷²<¸nÐ%ûUË^¸%ßS1íhÓé o" èô+¬t<é“�€½6ð,uíÓoRæ#ɦɢ0Å…&ûßé ëeXFÚ¨¡ Y~&c6ö£nÄc×;ž#Äð.ÑŠu]I‡a~E€+ñx@$ÎdNiqA'*ö|NɇEØ mÊnÅ ¨¼—Aµ[ZÁE!>»þph#ã�U‚ôù#¹9X!’Ì[§Ÿ@w9¯P@µò¡tsíýìsï¶ßÞ·Ñ~ç©PÜí å:¾n^QûaO[˜¡Èò³óê‘Ôâ ÑÁ=Á¨óò5ƒ(TÌöw9Ÿö[mº ÄnÁ‘ëàY�Íæmrwfª‹@÷^AïV]Cò)ZêÓ™ù GHêo�»‘ȼ}1K/zÃñˆ€.qêÏ>#€ã?ó’+xìh¸ƒ=¤ð}™î=zF#ûD—�ý8ä¤5b¯hð>Öèý›Š–ç<õ÷bê%ætÊ�tÜ4Ç›´æ†©2=”EÓýKðö{DÛzOEAÍSÒ¶sšÎTȵ�Šr?]0#Pé[}w׺¤=Ñ<È eÏhûjÌ_hrÊ@!Ÿ‹<ìÍë·ÑXIm\eÛ†™¦¡\Æp ‰ºhŸê¬^ äk 1�æºë^�àÜž¬oB Miûùô3AmÒ©) C/uþ«ÃX²tçÝӆџ¨s+˜óË‹Ü}ëƒT“î ‹ò•î …²¹dª ›ñ—²£ íØKÛ(ôÞØŠsÄà<½Kص´‘ò »4uÄÆ!Û…? §T½†y¿¢mDM›Ön—Z~VXð–R$dzPù–§O%#õdHZRsãÑQÞ7„M Y•LzZ}ãÂÀÊdêâ0 ¶“pÁ(H³ƒS…ÁÒr::m±¹áéjÀÇ.¼ ž ¤Õ{jö‹íÍ’?¯7kH·„A’ª=ZµÝ„ÒäÀÍžeø+[¡“ÂP.*ÌÊ$£íô­<¼v÷ ™â™}ù~Âñe£�fcù”½—Ê�¨íÝtxƒ¹ðuKg$g!w1TKõ€F¨¨‰k(–JVvº1—„…‰²×µû[Í«ÑT¬†$ŒØÁ¨"ýÐbp(0rh(%TDcÞ ÎbʳÊE1=O&I-27?ššûÔ¨W]«T¯÷âÚâf��–kúöHŒÛÜ({ºÍ>p¥M·,hQ9 —C™‹ˆªãU@4àz4¼9Ï²ÆæñPÏ>–ŽãP|ÒÍ¢€¹“(•2%Ý�¤{�E©Gí‘‚h2§2s“ª¥ºÏª‡„î XË]§çÜq¶%rÇq’k߉œ:ì1¦é*¶ò×~ SBÎ?UWUø‹|¡ŠSÁ¦_wd/Ö$š&ú¸Õ«s[ñ×ê–R5 ИíɳûF§Lx;½¾Q¸±dµGå…wÀ—¶8º“‰âÓ^—[ê¬;`•hT«»rí@H±ŒF•Õ.$s§bdiÖ(O‹tsE%j�çI`™,zÍ�(ÂÎ�u{XD ]‘Ç~7õ—E»= ©l\9Šùkó�úÊfm8ëf(o¢—7©ŒxÌ!º²¡I#&žCPQä'(Y¤Yp‰<{ÔödïO2/x± «ô�”Å.(l¹e‘ì·ÜÞã8Ý�,2…u${à×íË*!Vuóª—Ÿ¢˜T tf»+’±Ç¬²íÅ4Ñã¢'ReÞyýQd¹¹ÇŒ™Þ0K‰ÙÛe­x`:Ç÷$×Ë‚ÆXäN7´i_|àþœ™\A·´Æ2Á‘É>ab'‘¾õúrû}·ÄÈyë>Þ¢3ãc~Ô<E%ýÁ‹†®…6JÌ#dGž(ˆ£VØ\fÏVô‰6Ü€T¤ú›ê æ¬â%½Ò†Iq+ÿÇ)` ⚥×ú}¸|Ër�¿×x5’ÊŒFãù”À9¶vlqT[Ñ kiÙÊ;ù\Â<³lÀÉäüoJÍ,ð|™Xu JýúmèEýÌ<¤ ަÈRYi¬j&sVsãRúã+AW°ŒtªDmY1[eLó².ˆeϥ£}:#%:ô€úeI\#m…f¼Ð’ÝWò±b\3ú�M9·€†„ üI”Ü£Æ1³B”ˆj±EîD9ަ´Jƒ”«±ÊJ‡0Ly ái%K¬ÃD�ÔÈF�µaìÖm–Q.*ÊØü™2åv-èmÉâWIÙÄ‘½SnPh±(¹'ÛIãø×´"Å öÈŸaÉREÜ`éŸQÔØó[ˆ„ÝÀj|l)ìà<ÜhkDk3¿O.Œ¥ë¢(oª´s-$9iW²‘ÀGa–L×”Š\ÀX½öÎýºß(hw*ù=Mmõ»&‡ÍF±QMjàß#ìÁ^Žœò*ØrÁ÷Ý®KoÆùõ‚-ýv¡v©#e [ÊìÜ3X?hÁ?µØ=t^ÔK>šI©ÒR�¦rE£ 鎵ÞV9·lE„í¢BTlb%8;„åº�µhJª�[üîK+ˆßçÕÈ¥ }¸–b±,ž™¢T––!t¤Ñ{âѯ�á¬wžž‰t‹<(X(J6-8†‘f7³:s¤²š‚øFnœå›F�ZA$èK—'ŸKXj_vKssBÖj°‡ëQß–Šr”"cjý.Ѝš=Š{QCU>“jÙ·ÈËúš�jÞÉ,¾Hvèc,Èj‘|&r¹Õ.j§Ö†ë×?¯k”“™í^´® {Ÿqâ‘Ì�;Ã�c²ßtîöü´J³}OUe–ç0¶Îu|°mØ�°Sø qO–ÑŽœŸ×OÇl#Þ2Ûq5Úßf]¡„ÚLÐvTË7ÉZôÁ„÷«ˆ#Èï&wë »ä½±M&Ÿ»þѨ$6YÛ˜ý»^NÀeº²Áû>Înˆÿ_ÚÒßg~¿Ô9�¹âv5lßçqÓq^p×O&›3» ßíËΕc8§Ÿšòn݇ÉèuôO®›7ÕçE¼Æg­ Èã÷�˜íÂE@¾îïàƒÙ]ðË¡„[ÞOì„> îÍÎñ¼ð¿(T"î-ß?ïݯ¢£\ =/Ô[ÝB ”ÊÁË.°7_øÛ_Gµ·÷ûFár%ÑI ^¥4™~r—Rý*.´ùÔv©ƒ"Ôi¬�;ŠÖ Üêñdý³9sÐI¹¥øf'øÖÞ^®qÍATu�;O­¿©Çìú)\{� ¬¨kYŽo…ü}¢ëƒ~õoÿS4”=a×F·Môv@–°W{%oʦ@zXm\”»k{yu}óý쿺öäÂAG`G§�¨;Fqv%txPÓ*àà<4Q�¶OñjAþÛ®}ÑyN%Jâ(/ösJ KsI¸qgvÈí8�ÝR]úŠ…\à‚&ëÇ)@èãß“S!–Rf—£“«©“öa2+É*Ù’Ù5-Ú¥ï½ßõUè¬zÙTUOgª½°ûuwæ UÒ;>wÞé0 vºo¨Ï‰WÉÜŠöaÏM4·ytpÖv¿�\’BHö™,ÃÄ�»ÔùH)ûý§'a\)û«‚û½ã`T�_mÐ<âcê hU¸ÊXF+Á»Kàe$©îÎ&ÝÓ�û]h\-ÂS†__AŽ7÷ºù�Úz#ÿ[ò?S]T ìì–‚¬$9áxœí[[¯Û¸~ׯP°Mð9Î  Ý:ÒìnQ —<-ŠÅ>Ðm±Ñ-$u.ùõý†‰²%G²}Ò,Pà¶drf87~3dV¿ûñï>üûýOq¦‹|­üg)> ®YœdL*®ß\5z{ýúj‰÷Z蜯kYmn~zÐ’üŸ,oø�Bi)6�Uÿ�ë¬JÕjiG«¥#º©ÒGbq»VšiL‰ú!žH ³¢‹KVð7WGçÜ$éöj½Z2âõò¸°4´_bh'9SêÍUö*Þ*Hªô Ký!^Ý1¹®WKúˆßÄ«ÍÚܬãçö§Ôþ¶°#ìÓ÷«¥!¾ZÖŒñw�Gý¤àÉÖÔ5—Ïœ8{b­ãwUQ7šÇ:ãqÒM«Ýñ8 ´mÊÄ|yþîÇŸ¿¿‰ìäTÜùUêúÕ•¥g™î‰ y')+N¬4ʈfqµ¥¯‘ñ ¶¹Ð�}ÙªÍx¢¹Ž:¿ƒ™æiÌ´!FÏ +ʨÇÙ/æ¨ä¿Ïxž‹ú�ÇUK®YZÙi!9ÌYj,!Ú[ÎY«‰úË\Ͷ™JbN,‰±`:ŧBŸG�ºŒª¾ûŠòŽ#™Mpc1Ë�;1Ũ#×ÇùSÃJ-r?7¾ìåtÎ@²œèÑ?ïÉušc|’Óý⓼˜[Èž[á¯ø‚h.¡sit.Y¹3ºf£Ú�ì �dkD¤¾éGd9!§�d‚³r² iðÅŒX„F´”çY‘æ\Èl�,†è»yî{A=�¡R1ËT4ü©ŒeiÏ5Ízƒ²ÓLæ%øZF+ù.¯v¹ø8Ùn팋™®½¿†æ X̳ &ÚMÓNç¹Èª*½�Eûb¶2N2¬,:”lþžy’¡k&Y‘ˆÉfvãÏ®:ˆDgÙ–ê—ìz¶SMåëtûw¨°÷ÂÐ�àrIUnEÊËÄm”/�·’ã65²p U“]/XŒ[É$¯Ë«{.cV¦±AûQ¼©š2eRàwçyxõÝÐÌ 8K²NÚ3÷ü™ ša–vÍ‘•oÐ"÷Bg1KSA’b}UM_T¬jžˆ­øÜ ÉZ ‰éµcd9ÔL@�r×P5¤â«¦ °¡f›œ_K®jp@Г=ͻހØ#ýÕ fŸ6•L¹lUaæRc¢O¼–¢`ò¡%þeX_ª³7W·¯¿»ZïinµÔY8æõ ?&P¦´Ôö›m„ÒiŸ´-ßæuÆžµa¡Óp ¡ÿ6V Ë™´`ž°4ò§G¯_ü·Ùp/gt¢½dÆ ïX ¶4îÙîMAØïñÊàS,pÛ Ëü âe?wШ­ÐkXgÊ4C¹XA!ò^ øRâE�,d4é÷˯ŽÇ‚¦Û†OOÛ3gtê 6{[×üN]‡Í×]3áýœ^B']}ROìýWì‰Í3w^ééöÆà³À,kHÌBBgŸwÎöæ¬çÍ­X¾#yà»ï1ä^w®1Å]‰ÙP¢ßÔ>ŽýŸýÀ˜ê¬{¸'Žÿ²ÝKgȯš  —Êl Ï – Gòs¹nóèäÇ3ñió™�ä5ÕЉ¢®&Twó¢8Ãàj‡<ëÑñ9`Z™ƒ¶ _#kì8ÊÂt) r³UEq c´åð"HâvÏvóáhãUBå"W’–Q%QŒHÎñzñ{÷Ú²[x5Ô]áñäPÈÄ*bPØ­lI´§˜¬[r{ÒFQµÙ±ãµ¯Nƒ‹²ÚÎiœºñç€Æ_ö‘ Sqeßüêp–g6zY6ÅÅðæ“ÈÕUƒW�³6%»�©}ûͯ�>יߔ°¿A+\°€™ Ž{cÚãŽW;EçªÁ¡@›cMçå…ÐD­:‰§&Œ(H´•”ŒʸÊÝ ¥¨7?�Šæw�;Â]+ÌðíÀîj˜7•vSÅÔ„ùÒª¿˜á™7�ÝOåšËûuÛPïšé¯‡›é4¾kRß¾¸�ù§�Þî/Cñª ¸Ø®ÇÀ? ah„�„ØA#TM¤®4*M{61zyëI¶�2‚Ý=HR4Jcv!£áRñ²~³ˆÆs‰}_•7’¾ª~õ².@Ë;*ª-õ�ßðšÂ€N¿ÂJÇó> øKaÏ’×ö1ý&e>’þgš, S\h²ÿ�ºž†a¤l¡º’uá§3fb�°êVœé´»…µ{SâÙã³8gš4Xˆ]¦mYˆ­¢Ú("B= pv7ª­ÀÃóE §‡�èѺäIÅÈ¥U1¹Ö²ÃϾ¼Ð»Æçi>IÃ~:7—Içê›`ï^$UÞ¾»—½UyDB›�Õg—h0÷ŸyÉ%b ¶4ìù!$’ø>á:Î w-£‘í§Kón’Óè¼ý5zÍÂ$¤„å9Oݵ‡˜Z•9¦#ËûÞ»OC®™,ÓCYÝùo·õ´�ýTÔ›… m·È7¾B®E%iK¡ël„U]'±ãn;£´Õêû*8`4�}5æîNYe •�ÓEMçMáºt¬¤.±4]ÉŽŒïWw>ìB¢nÊ%<£—YÛ $»îVˆS 8·øÛ�‚¯™?Ÿqî¨t:9±aìÅŽ™UN†ð¼ÛvJ3ºM”ÏÞ f]ó"7í:Á Õ¤›Nâ|­+ÊZ¢bG>™lE?áb¦Ô¡);êcÙÛ€†ÞSÎŽØüƒ£w Óv‚z)Ï�°KVG̲]¸#W1pÖëÊ÷Ëe/jêûÇ]~h±•IŒå€ÓÚ—SáB¢ s`,mèØøà¬Ç0ó°tè�Øßz™¦WÜDŽË¢kÍ»k“b cºØH çl×ÁBFX?Š3|šm{�§Âį@v·"î¾Ýµˆ!›!T&y“ré�òmÇÝÑ:%HÁkr|bìÅBó. MCx^–Åan¿]&·vâàcRn 8?£.ݯ\ÚÿÉù_ŸÔ¸  G Š:‚”xBxœÍYKoã6¾ëW°>ôÛH€ŠÔ1Pl6E.Û ìqDQ©’”½Þ_ß!E½eÙN @[ÒÌpø}ó¢¼ùåñ¯Ï¯ß^¾�Ädé6ØT "üȘBPš™‡Eaâåo‹5Þ7ܤl›+®þ„,ƒG®�âaa¸oçLÎôf] ›µ7ÊèhÍßnµƒZœê{r�!Ô6@dìa1.¿‚Åv³kþî„o+@Kw(‘š‚Ö‹ä‰5þ-ñÒ >•Ñ©�ÝWÉ&Ü¢¥—sò›unmnÉß äŒä ÐMÃTP>ˆø¾Z.ÓËO‹Rö’KÍ ß3¢)¤ ÈÒ‚9¸�¢6ÿÉÅŽ˜Ÿ;³2¶ÙYÇHÔÞù& BA ”ií”Ê­€w1hœ/´µIãÐ$è߉Œ ³òþ®Ñáóh‡çÑgC;Eaû´�Ù7¡~Ú틯(xü¾Æl\ ©yA;ò”£J;·É3DÕ”�=£F*¢sFy|¬øq8–ô¸Ê1¶N@døµWä5áºq¿+¬DK)Òãu|-²—*Òôyô;â³Aß³:ÄBÑ©ÓBŸ�Û§†íX•}ÈGÌöR'˜äc>êýÛл€ˆ®ülLôÍŽPa\ÅĆeõ}0Éb{÷«áhä÷ÛÍše[BYšP ŽÖl?M49p“ÿ¥Ì–B¥0ÀEMO•:Ý‚Öxø4=2MÏíÍ+ÈjiÍOYÇø�¸Öãÿ}€S€T†D]ï.g2x7•ÿØÒpMÊ• óXÙánŒ²¨½—?,‚XëhÕ•r\j•KT}©Û‹ššL°z®ÀØ;6iˆ„G?KX;T92ýÑ©ÇëëâáU‚Âe ÝÈÎm“ÃhžºŽ¦•ki¿õ´ß�ÒÞëŒf¸‰QÌÍŽ§»áWÚôó¼œ$SyÀˆ e!"PÇ›€h†Ï£qá"Ï[ÂÓå»/{Y9ŽéI7ËŠävRÒSéTQ…¡˜ÈèºèyÖž"�Ÿ–ðlñÓ±9  §v%ƒ•Ôé!s:°R¹ã8[uÏ­ð9$²7A€­Ú�è”�3Ž´u=ý,÷ 8zÁ±bDiþß¶=¤¨yJ0cÿQŸê‚Ï4æ0‚öæšßK~¿Î æ—n,iãQunñe…í vR`LødÖ•ÅŽ9kÃÎA­5æn\-R,£Ic�K ™90&Nad#ª­Pí#Ë•ŠŒ�Ýr\¤� Z1hLäÈ77ÖQì?X‘ça'ô‡2+ŠQk\‘‰ù�ö&õ� vh9ëF¿Ä E2ñ˜³èÆ®ˆÇò´…‰�1ª@1ò“)YfT0kJ=ë'»¥+òÉkÌŸLµá‰b×Çÿ�=³*}ÞyH8¸­9Æ’P®Žqøçöfû«¦ûÔ�Ç;T*ÅtnÛ#ÆÝ ˆª¾Cª§UOdÅLô³È óˆYqAÇ«Dçëw�Å‘yï“B#lecI:Ý‘.{—€ò5…bý‚ËÇ{…éšFúÞÛ+ì;ç”o=_‘óÖ}½Gg¦悔… [ü†,[h½µeBŸ^kFÁe´pq;æÙ“˜Õ•Ê.Ï.™•Ó_Oαr ïW¡K|µ…SÈÖ*Þ)[ck¦ºÇ¯êËŸ±aëðÅÃb EjÜ©?–åȧ;‚!‰ûwÛsÛ�{‰Úy]d«y�h4V0A�WctAšÄ¨Ye£qxß‚ÑÚ†g�ëkÿƺü©ä?\ܹ‹5‚–nExœÅYÛnÜ6}×W°ÊK $+´A€"Ø(ì:ðCSI[ô)IÔŠ�$Ê$µ‰ÿ¾‡¤®¶´k§Ø,àÕ…œž‡òö‡Ë?.>þsó+LUÆÁ¶¿qÊp«¸!–¤47oÃÖä/ #´aJ7J&›wTUt)´Q"i��õ6ò½Á6ê¬$2»³6е!Q‘ê7lMbÁ–XM. …ñ6"kñçU è ¶ KKÒúmX¼f¹ÆßK¼h§2ã30ÛdÕR‚Áœü6j¬Í˜9!‡ŸQ sDz‰“É¿<5›ÀËgbߣ¨ôËס7ñ+óÖu��%–ÊZ£W3™³†c¸Ò/± beÀ2®S%«ŠÖ:cšª¦ä,#L_.$w1÷`�ìcÁ;á™SBÀQ·²ÕëÞ›‚ŒÇ¥¼pfD…‹d9‰²UÜ:ဤ\ë cׯÚÎx.jž±äŽé‚>úɶ{R1m#{ ¼w)•$/1÷Ã~šï¹¢’}¡;m¡¤Š“A×&¡·ÛÚÕQŠ˜] ÃîÅ–]Ä[-ê�óÝÛÏ…±r½Á¼­Ó>è°ä¬]ôˆf¿S°\|E`Fo÷T¶\�ƒÝ(}æÇ†ùSs‡Ë2ʨ65àÆ¡°çÓ还EúÇn `ÑêòbjÞÎmð`2ïCw«6e°RÈì ø âÞ9ðµs 3OeSÌ}Jy­¥š5åŠßΤ[`zî½c¶¥Bð€ ­ãƒ¨ÙÅ„X§ÝM j×V¼Æ*'=�Vö×&5²|tl÷먻¢¼cЧ²‚µ 1’V¯PÑ®¦EöÃHÓs$å!% ð Iß.rÇ3›ø‚Ýðg{i‰[¡�³Y�¬á«VAZÁ.ä+rƒQ"/³œ§RªY‘ôZ¸FQ$V(žc0¦Ño¢ˆ×›/â³hà.m¤ÚEö-rìù”Í6™§jøM©‡¾E‡¬wñoK-±ÉD]SÀFXSÆ~ú[QÓ ·oì6ü¬›¬pœ5©ÜOh8™ÚƒêWý„{}Ä6Íò0ö÷'ëŠzïtq²nÓ�Û|øªÎœ.îÇt…Áf¶4äpXmOÒ,ÅgîTíÃÝCÑ1¨[î;uû0áP„ÒÁ9ØwmjèÊšâU<_N}ŸÐ *Áý0a|34Ø`è•-ã(q…šQñÖd]YôDÓÏV’/…6¯ÓX<Ùðû0ßÞ}{§�‘„v3Y€1¯!N cúòC†ñý–%�7kù¶v §…ü¾­n†b3Œg¯K`!� SÏjÔScÙäü}¥í8È˱LŸ@�´.žtŸ ö_®T› ö ‹`§$½_ì�ðG…Ý€|tÇç%˜c/ ¾Sƒ»ÖÝ ûÓäe ÞUI;wl3½ÔjåõÝHp!Qº ªS>!ÂØ¸äÆØËP¹)ñµ¯ Gbpì„è;=7È×úÊžD&Nt-'âÞéåäp¯í‘àgp¥\\oþ¸ l¥«KØú9Š;êwÿG”¾.ïk‰ßýÛÿ-$¦F×áJЮþ|ȯ ¡é´mÕ–XÑ{>ß(ûS {~qyõãæ›çí1X…+�E1HôÔâ;À¾U@}«Žƒóo[{Q9)ªwã·œå�J'…]qBž·×cÀ­Ìy‘âÜè°Úûq´Vêœxk¾+åg™0�¡† ç±ç¥(¤ÌÎé…KÖ©ãîáh‘u.2nw«¾&Ð.«®~ô·ƒ{bÓJs<«¬ÓÉÍɳISJfíuñ@ƒöŒè¿µž¡’¹#w÷°rðÊm–›Py¢ÿž~V�|3Y…±¿/9ðŽ×\ÙOv^„‘RöÛw®ð|�+ß¾6¨¿p9FhmžTÆ2¾ÔŸ€Î•Púb<Œû§G ~6˜¨¹Ã—cÁjóLJt¨£î?z‘ÿoá2n— ûû–‚¬8Jxœí[ë�Û¸ÿ®¿‚]àšà]gH¯Ž�4éèc?-ŠÃ} %Úb#‰ Iy×ùë;ÇD½lÉöÞ(°�-™œ΋¿2«?|üç‡Oÿyø+Iuž­£•ÿ`4��œiJâ”JÅô»›JooßÞ,á½æ:cëRŠÍÝ4ÏéG®´ä›JsQ�¿3�ŠD­–vT´Z:j‘�öýZiªa �Õwä­()hÎÞÝ ¾‹“íÍzµ¤Hýõˆ\8¨½†1%‰3ªÔ»›ô Ù*˜‹dl9ߑ՞Êu¹ZâyGV›µ¡´Y“—ö§Äþ¶°#ŸìÓ·«¥¡ºZ–=Žðw �úy8Ó¡ú¢*K&_89:ò¬É‘—•fD§ŒÄU^e`’=#Ih„mUÄæËË¿ÿö.²“¾÷ËÉÕí›KÏ2íˆ òNÒ‰­4ʈ̈Øâ×ȸÝðŒëC[6±ù/‹µ#×Pg{šUT³„Pmˆá3ÐåEÔâìsTò?¦,Ëxùçã*%’éJVv\HÆrVhXBÔYÎE«‰ÚË\Íls:\ø¤xác3Ë}ŸB÷åGý·Œœ¶§òbÏ MðX>Ëe1ù¨Ï–Ç|öKE Í3F^·õr:»£,gú@tÄ¥[rÍô�/r‚ |‘—{€lyRì9À “k&A½Ò¨WÒbgÔJG9Ùè�d ÖˆHm+�ÈrF¦šg­œÑâ´¹pÔåöÊC{Y’ó †s®d¡FCtЉ<÷N¨EÏ`“„O³ Ž»º],ѹ–ÁYÏ`Cvšu¼ÏnŸ‚í2±ËøçÓ&ª‡^n¥ý-´T@{ž±`¢Ýàìt–ñTˆäJÆk‹YË8Ɇ^°¨/ÙüýmžMK*ióÓuÏü8·1bMî” Ïwœ© �þþjj½ø‚‚ n‹bËVÄn70‚ 9n7# ƒ¡j²{‹q+™äY™xd’Ð"!hGd#ª"¡’Ãïλþô曡%˜0§�´nÌ34Ã,õš#+ß E¹N MŽ’ÂúD‰_Q%‹ù–Ü@δ"Ó[ÇÈr()=Ê]……ˆ"¬g0Pð€ 5ÝdìV2Ul´§y×@ì0Ô_ ÄìÓFÈ„ÉúQåf.ümâ¥ä9•ˆ-á_ ëKtúîæþí77ëŽæVK�†cÞ¾òceÚAKm¿Ùƒ!�´IÛºí}V¦ôE: úiF¥׈­ÑÛ,Â[±¼vwªÓ›õËW‹{°&Ëב3È�;3Cñ]ö‰)š3c{f]s$òîù„.�°-­2혿º{õFKk‘bC@Îñü;»ÔÄ1 <ø°8¡ìöÐÃg±’Ã%1ÔÁàO’LUÛ¤ºZh‚Ó¹Í/�)‡ð„¥8÷v|ÐÈ_^¾î®³a'g4¢N2£ ïX ¶4îYï?AØwxre�%,p[A4ÙÄŠvîÀQ[®1Ö`� ÕÊ7 ‘�ŠA>,/! MúÅýô³ã±Àé¶—À’™û│¾¼F=híRóaíÍh457uüÜ2¾‘®<«óôð vž&Z6z‚iaÔyXŒhæÎ6çS��^Îwմ媵<¾©×sÌr[;»OñEd6xð7ÕeÃ`߅϶×OõÄ~!äÇm'-AžÔ”zp)ɦâ”Bq±a�Ä\ÎÚœüðŒ|ê¼äCò“ª˜ñ¼ &ˆ=µˆ¤0Xì _z”†|z@j@+sP“G>*€EQ�‹ ^BÖÿ‡DÓ zû§CÉæâ�îNdÇ!pA¾†p,aÇQÞ¦Û*7[UD 0F]Ö.‚$n÷l§1Ž6.í ¹å°Œ„„¢B2Špp{t¯-[¾¯u x…<Ù(db5Ðv+ë"‰ú �6KNÁž¸Qˆ:[Bìxí«™XPŠí¤~¦x"ü©Ûô@ë!ûæg©<—ѽ¢Ê/“Ï"P}¢ ¼ÊŒÖi× LìÛß®ðø¡˜Èü>¤ü=éý…È9ÜÓ&—l¼j�Qx4âë\i:!×(h&ªÓI<5DAÂtè²w8ÛúÈAˆ+¯ ê‰Zó©p~ÓIÃ}½iM9¾=×ü€»y#·›#L�©/‘Ú‹nXyÓÙ}¡R®ÙÛípÛwÓÜ~;ÜÜÆñMÓøþÕ=a_*³¤» Å2P¾z=ÆY(‚# ´ƒ�0‚ê5”†JDãÞ‹Œ^ß{’uç ás Zä•Ò0»�‚ÑpÉw]¿YDãÙþEïr€(Ñ®B:á¨w‡Å±E”¾žíèôXéxŠG!ÆbØ€gÉ[û˜ü&e>’ðgš, S\h²_OMoÂ0R¶àÜr ’5á§Sjb1ç–?5½Ý9B ïµX=(Š: ó+¸äO="$¦Åžx˜ó3)aûÒ—Ñ’å òVUvi9}â9ÿj;º¡�´;öÒoäŽ8Ķ·BH2ï­~Ý}f¬´ª�…�hï{—óv¸ñ¶ö¶EˆÛ•Ýجaè3 ­õX—c}R¯�’rÓ6nP ¶©Õ îrE´ Œ‘ìäjÓÁmÅÚ¸ ” A°¾mo”nç¨ b÷ ³ó§ªƒÇ1fùõþ`-]å�xö‹dTãŠs¾Kµ-è`s…D°›¥—ÝD¥ðð|!t“~èÖ°N8¯L:‰ÈOWIvÜùWZÛ<±ëöѧ³qù¢Wýº|Ý‹Xdõ‹ìz¥+…GD³yU}u ³‡�`“àÙÄÒ°çr Š„ï®²œqÑÐ1ÙFštíÆA¶Ù@\y�/I�^_0i&¦YÆw�€`ë0ÃCjÈÖ¾î“ $QY$}QÞwÖn©í ϱU rÔÍ߇ ™æBâ΀—¾rºÆ^ÃÜ6*qÇÔ�"hÐSœF?5ê®Y]@~@g‹(ΪÜ5Íh�M[iš„ ß> ®RØ…DÍ”ËbF-9¤b@ ƒ5—-œþ€s}.¾ )øb÷ë9g|J'§³ ºüôV…�c(듦4Åû8 Ð=§Ö ¯r­ ¤štWhX”g¿‰«%×�2NÌ�¼Üj:´ZCv,›ý½õÆÔš#æýäè]Ê� ^Ê $lRЋ†lî\“œ7µZßíZÖ‹šø&mõ5 2áoqSXWûZ'\H”õvPoiCg³½ÃÌãС“XEdšV\å9.‹¦ÿíN…Mâ4Œñ ]�õ,ò²-‹Ý(ñˆ¤ðiN�m§= Ž^‘_9ÛŠl¸û^Ô‚€l†PgUÂ\­ä”o›,îBÓ¬x&§C]…û0 Åy¹ps+ì:³>&eÌP€ËóäÒýw¾¥ý/ƒÿD)ƒ ‚ ‚‹ ‚–ìxœÍY[oÛ6~ׯàü°§ØFR2ÇÀÐ4C€¡+° @iéÈæ"‘IÅq~ý/ºËš#WÆ€¢‰Dž Ï÷� £ÕO÷~zúþõ3Ùé4Y«âФ ) wT*Ðw³\Çó_fK|¯™N`�I±Yü$MØDŸ_µ„¾Ñ$‡{¦´d›\3Á nÌ@jjµt¢ÁjéMlDt0¯×JS�R,T·d”jÔ¬(á4…»Ù©ϳõjI� 7'ŸhñŒÖnP&#aB•º›í>’Xá¿9>jÔŠèôÝÇ8�Õf�º7è’Õ°ZfÆÊšüµ£�ŒJ<œ¸…ˆ½¤jþqæöþFTH*É‹1dñ£! ±7Æ·Dï€(«MÄöa[9HÀyè$ÕÏL¾‹œ„”† ”•uç|öÞVÇÈ•1 mѺȶ<®Þõ%ú>.Ô“Ò1�YÁ aóú{ C0N‡.Šiöÿ�¡UÛÅ0h‚x*†Þý&ŽA Ȉcš�1Í'DÐ(ïÂ÷‡©+5?(ù’Báñü Þ‡�qüb Xø‚¢c`lë˜Ô®©.Ä_Q3ݰ„¡H=ä.vÃh—“µ�De²øP mÓÆ#�±±ù¥äiÇTå>þ.±KÎOçÂö%O¿Vc0k(˜°–�.Z¸aƒáƘ–©NKIÆ5l¡HÍ6P=j[é ¢8%zeD …GÁ×Ô0!~mC=š…wá·‚´|Oõn¶þð³f¨ä×ëÕÒ5 !I•’ŒÚvJ*²gzG€†ø_¦’PpM/A-Ò”6 @éáEÀ½J–™—gA\Ós  æºp×–ÿG S‚…Ô$jzw:þÁ°K祷Sq Ø K=ˆ÷†Ð•ÝsQÇ2�Õ8,ºm©Mcg¢è·Í[o0™Ë1 Å·0¨ˆl~²2pØhÑs=ºêµ¯å8²™ãðZ{.·ždÎݼ9†V•ô„Œªé’©Zµý[b„ßK¡kO¡›^ µæÝµGb܆kÁ@ï`Rév¥Q~àß#û7"畇«€(Àõ¨sžeµÍÃm§¤ªy,ÇKÚQ7]M´'q€2ï�Ö;�˹GåAƒh éjâ²®a¥K»‡„nîÅ®ããû0±e86ï…5ÒíwtÄ|6Óm*?Ð).&½,”5þ“x¡’QŽºæõ¨¹Dcª[ë[­¬’½–·³²Q�Âz�¡Ý«>4 �·ÓéUÎ�yXyä\ ú|Y`[¥[Á‘I¾p¨BcC�Ña¦ž �*u¶Ý {ø<TV¹� è=?#Ãú@qZä£-K)Psä8OCuÞi¨dO‘LG±oFTÓyìvpI6Û"×�€-h1{­R]™¡5gí`çMt+‹DWÆ"'4©ÅÄS£**�¼�.ƒ‰ñQ=˜Cž•…^Ç%R°45PXÛ¨�íê>ÙŠ2ÛíêûÃK@mj3Ð9ëvørëæe™1‹ª?–½Ñ�v¡�Tf8²µC½¢³Æ4QâGri²[Æ#Ïr}�Ù5ª'ÂSväÊFÏ�ß“\a°w´6Ðï™§ýE÷ç¡Î%´‹s,ÛÌ–†$R·^_n¾Ž%líQެ·ö×[tførÕ�Ðü¸RÈ`ŒÚkBà긬O%ïÇ�˜_Í×È iÌ3Ž&S7ÕV,´Ž¹›J»â�â«)Ò\Ô¬x§L=/‘j^�¼¨/µÚ�ÝÆ¯á1ÍmoN8ÐBš!žörß I,áŸÁ�˜þÆaKíw”ÆýLçh�ÊràááÝ1 šAŒQe¥'Fýᣥ¡gY!–þÛêÒ}ÔýÄ“«A‹m‚—^"$xœÍZmoÛ6þ®_Á©_Z µ°†B0$Í`ë¤Û°O-QITHÊMöë÷�Ôk,Gr[ bK"ïŽ÷ðŽ÷"'üáü�³Ïÿ\}$™.òÈ Û £ .ӔĕŠé~­Ó7?ûÆ5×9‹*)¶›_YÉ$Íù,ùx¯%+Ø_4¯Ù9WZòm­¹(ÃÀÑ{aÐÈÝŠäÁ¬òc¤4Õ å±zO–Ë£RRÒ‚}ð—²ùQP³êO'hb/¬HœS¥>øÙ;’*ü½Á£†¼X$l!Üž { …¬„0¨Ì*°æøÈÞ0Ú½¡[žsý@’�"¶ÿ²Xo<'#áûV×B½yç;±¿§érÕ,y#›Ä¢T˜UD¤©¨Ä®k&ÕkBIºœ$LÅ’W†ƒeB-ªœ‘„Â}R!A¸;DæÍ@k1ÀçŒMIhŒöƒ+¬�5/kQ«¡toL©3ª5¨6„\j#)a)/±Þö�¨ŒV¬Gï‘pOetæòš¨˜æƒi7«ø® -…Ù—\ÄÔ®ýˆ°¨Õ²d„â£ØÞ'_èƒ"Z�X2ª1å}“UÛ¥jsÜs‘ ®±sã΄µâåûÅšS® ]+0­Ë¸5$Yig�’c�_¸ÎHÊï±¥ý¸=ïiqô–Í-ó§bV/ã¢ZÖ±†³�¶+y9¶ä¡åF&zÕª3°Äqßô–ãŸÓþãzËã�°Á(&�œ‰äTÙÀ°ÍvÜ�¡y•µŠ4J¤’Ý�h„=ÿêp—ÒIÇ2ÊZçâ%®è ¼ a„a¾ª!@îê‚•BT uVêï§ ô³¨?ÖàR5Š1T+ƒ‡)9ÝÙ÷º¥:ZįZç_g�§eÌN×NAíg ÉïÛ&¤wB†Ú sëðÃÖ¥º0ýòh3ò¤AõØ«€tiÙs´¼ðËöv2F¸W.Ðß!±1:è%®´UæW²®Ál«ØßÝÓ÷/a‡Ë,ß>Û25ýÒ¡wØÐæUP]Ô9âÒþQŸÛ¶ìäåÙùÅ«Íwôˆ¯ÃÃ- ¾=¸ðd|%Ðî$�ÝÉy`8›w5ÅiB=/i¹c&,wõç‡V0ŠÌh¾çÀšõ£I¸Ãc®óˆ ÕÚ1•l—‹]ÎÑïv·sÈ@èΕ#g9Ï„HÖŽÔ¦·˜ûQs3E™ò„™ ­Ø”Í1Gß\ö thM¸¬æ£åQ0}˜¼ZE”¬r¡�ß“­6ÆŸð¾öWœçG!Ej[ssäµAj"üàhuÆp?®&’S" ?r×)�Nжy $„Ji~°K%îŸô˵@TU6¾æ˜ÒHeB¶ç´íÍ×(ÛöÎ�Ú»'Z¶j(èâü_sF꺽å¦éz‡ ù÷ŒÀý3Èÿ‚P{\ ÎΖ.‚¬`<�xœí[m�Û¸þ®_¡.pMð®³9H¯Ž�4¹+ ôÚ|Z‡û@K´ÅFo!)ï:¿¾Ï�”DÉ’#¿% À^kÅ™á¼>3dxûÏ7ïÿóî§0ÑYº õg1>2®Y%L*®_ÝTz}ûòfŽçZè”/KY¬îþÊs.Y*>ñø§G-yÆÿÅÒŠ¿JK±ª´(òð®“"V‹¹],æŽþªˆwÄí~©4ÓX""õcx<],ÌYÆ_ÝL]~Åë›åbÎH‚ç“wCËÀñ9V•a”2¥^Ý$/µ­¨ˆ§«åÇp±erY.æô¾ «¥¡½Z†OíŸbû·™}óÑ~û~17|órOüÜâ«þR²à›áó¤*K.Ÿ8Éz.Ã7EVVš‡:áaTeU coyûÆ\Wyd~yúæíÏßßvq,¶õ3uûâÆÒ³L{"CÞIz #+�2â€YX¬é×À8[‰Tè]W¶bõ_iG®¥Î·Ð#Ó<™6Äè;èŠ<èp®7sPò?&5Î^Ëé¼…d9Ñs‚�БëlÏù(Orœ�ò~#;~C<öÜæ3Ž¢¹„Q¤1Šdùƃ�ª²«´’A¬‘º¾1"Ë Yñ\gœå§™Ö]ÃÊ™oeËä83Óš Ùµ•Å�bØš{/¬ƒ/bÉXœjKZù¬iÙkOZu‹²ÓlZKð¬šóMZlRñáÃ6‹¯aÛ¤ÿîÛ×ãvœ‰±Ðn»œ§")ŠøB&ïŠÙÈ8Éòµ`Á¾dÇ×ís=¡d’e‘8ÅÜÒK¶QD­5}Ãàs†¿¤NÁÙáPwç�¡;Á=£"_‹˜ç‘«Û[½�ãö7²p¼ª&»©··“Iš\†,�CÓ¾᪨ò˜I�¿;/ýӋ`vÀY”´Òž AŽÜÐfiöXù-ò t²8$)öW”ô‹ UÉ#±Ë+dl+!1½uŒ,‡’ èQn*jïT˜b×´‚CÁ6Ôl•ò[ÉU HdOó¬óBh_#ý• f¿­ sÙ|U™YKš.ñRŠŒÉbCKüK°¿X'¯nî_~w³ìin1׉ÿÎËgõ;ž2íKsm³!C:î’¶Ýðë´LØ“&,tì¿hè¿UÄR&móA½y›Å² ž5îÎtr³|úlvkòl8ƒìD¾1+”Øä°OÄÈœ)ßrëš#‘w†ïÉb¾fUªógwÏ^àmi-#6 ÔŽÏ¿³Û!MÒÀ»:,>£š×±ˆÂg‹Ø€äøaaÄÓþ$ÙÎÌ ú/©¾Ú ¤²`óËC"žØŠsÿaLJFþ²«õQO3šlØË-� —ÌäKb¾ÂæÆ=›:æ…}�§P cƒë ј§;lˆçÝÜAo­…¦XÃ>c¦ÚÛ ‘Í2`ÏJd!£Ézs¿þæxÌh¹�ÐðøìúzÚ˜¤¼Î”dשmÇ�)»%l4¡·Ó‘wÇ GZéÊ“¦€ï¾àðdH }’C`Ý¥�Lo¨«.ÉçxìtÉ H:AÐHX�n÷\þ^9Ð38�šâåÄl €ÑßTŸ Àg7ž¦úxO…áßÖ½4‰¼­™šq)Ò–†„¡iZq$U—CW;'?¾Ÿ&OÖA‡|©*E‘•…‚bKð1¼\l�¿kÔH|ö€Ý€VŽAq5SL œfð±þ?D›ÑÈÛßïJ~,>é#�®!8ž#�¬þ¤ëî{UdÐáAáߨñÁ`‘o*ü¸7櫾+ÖÙ¢AÙüñÿžhoŽíÍh 8"$ÀjJoÝ�ÂZÕ<9”'LUŠtÏüÃ#b˜>®óØ…õè„À0$µ!™AbËDj<{2⚈‡‘Ÿ#`­Ï¹[ZlŒz\sÎßHvxX=4H@Ö{f´=Ãe0M2Rg*rë´h£�Ðh6èûŒÒŸçÜ.à6)+ìS-›¼2 )I€<|'Ý4´p¾…í-2�yPVe¬¸Ï;0K(�‹¼**Õ]ä¼sßTllIÐà£Æú!:©ykzƒƒDzØœÈGäk„c‰Š£jC˜éTnJUæ0FÓfϼ$nk¶ÓXŽ6.Ð}.r%ÉaMŽäŒ€"p�-[±†WCÝOþ…LìNz€Â–²>’hŽ{Y»åö¤BQ4Ù±Sk_��2e±>qºë–^kþÚŸ–ÖÀ›º:ûä7Äj¾£Ç¶y•]¦^EÄæü¼Ê”5éÛ½Û§ßÒvèCqí‘ùVåþ¶ms�Æh‚dŒË:ÞEá-:–ö<šÜm&E—h°&*ØI<5Ï^whwï0cäÀÉ  :ë=©h};i$œÑŽî�z|Ùþ�fæI¡„-ÖX±ºeënfx W›ÎÖ©J¹axÿÀ´Ãÿ—ÃÃz¿ªß?»ùÇ áÄ"÷·¡x U möc`¥…Fô†�š¨Ìº1ÒW�‘&,@Œžß×$›ÉÁùÔÉ*¥±º…¦Áp zY¿™ãÆ>/ò½+auE·+ê… Pø†šu‹pëIµËc@§_`§ã…�„¬¦°�gÉ[û5þ]Ê| (i²ÀOq¾É¾žÚY‰a¤l¼’µá§fb�0ðZ<¶³ïc„®�X{ИtèçW¸�{D¤H)-ÎèDÈœ/Êâaæjë¶*ïdPe·–±G‘¡ ÚçÙH»c!/ýî¨XïíIæµÕ�§»œ—è> Úâ!·k=íýìrÞ¦-Åè2L-v|üz7ó{‹Öo¡Æû¦ y†ô¤°ë>ô\wŸÎóAèÓQb“±†VÛîÓûa½¬r¯yš1í)ZM77°±7’];>X¹¬µ±¸b…Zún-vÅ©±¹-s¦¸yÛƒ'bFM ²ÎTežxö 1L™¦-gb“hÛâþ+EDh€‚nÓ–¸¢Ò áÔ 7h£‘ªØV÷rÛ 1\ 4x#oô¶ŠIiKS»Û#!MfSº“€âS5Ô‰l÷2�÷EQt‘¬]AlÎ1b‘Ñ$r4³±zÌç3Í I…Žn‚vsÓ–¹�Ð…wþÁhûjÌÝV³º@."÷ ÆO«ÌÍ$YN3qif°-™z:ëÝœ± Ú (—1�Z2¤}ƒ§�-Û»5NàÜ\ƒXûêþþÓeg•ŽOÉŒXv�Ãz凛áqÜH¥]ÚŠ÷‚Y×½ÈåÇV0H5éBÙ°(_ábº–UNò)f®×^ÃÖÚ·uËh¬ÜÒybÚô§xïè]Âö­ µ”gHئ»~à³�¹#j1ptØ9ÅèŽjQãzÞÞf˜Þ™Tcñ`Š6A×m¢¿‘À›ˆØ—ö¶6t̾w6f˜ÕC‚¡CõúöÑ4­¸¦-p\fíQ†;à7IÚ0¦Û™ †µˆÒB{‹Iõ0A˜àÓ\°‡&±wŠNü2Ô+²á^�ñf!d3„ò(­bîÚ§|;ŸrwåÎŒb°=%€±ì±»õc×ð8.Oãa®)^&;·âàcRvö8?'ÏÝÿžÛÿ…ü?­ùè€ x ùxŠ}‚•~ðxœÍYÛnã6}×W°~èSl#Y,P¤Ž�bÓŠm€ö‘¢F6‰TI*^çë;¤¨»¬µ¼QQ H"‰sáœ3J›îÿøôüåéW²7i² 6å þIÁPÂöTi0w‹ÜÄËŸk¼o¸I`›)®~Š&ü ¢'ªÀÈ{®�âan¸—d  ½YBÁf핇2:ZS×[m¨A)Îô-™¨¥ƒ %‚¦p·ø¶ìêe±Ý¬©5{s†ÿ«´pƒ«3ªõÝbÿ‘Ä–xiP“Ñ9�¸-�>’M¸E­!ºád7ëÌêß’?÷4’Q…[1 ‚âAÄ_KÓ©^~\k!šÑ„*òJ“B”¡ãbG̈vÚdì.®‘·€DÍm’/2'Œ B­�P±µïfí®­�Hô€ o|'Rfå}^£ÓÓPA )�†Œ™¯y!ŒùùeRsÃ_áÛP9µ}¨�Õ¹Py¿OÁ88Þ.´i&ÂåDf�ËkîÃõ»d´¨ï”SI©ðûÓÊ;ý¤Vóâ3 Mƒ­+= ‚}#}0ŸP' yÂQ¤ã"Rã¸VP’W`F*¢3`<>–¸º¼ð˜f'ìD†£ôŠ<ï¹®ÝÇÿ¶³¥Éñr�>çéSIR= ¡–è,ðt,ô±Á!#Xeš>/Õ¸0°ƒ2庰 ¨í¤e0ŠÙ=páòr2=«\ãà4Õr³P¨©¾Ïžú©k½ C:•3מ37ƒœé´pÓ·Gb\†Ï‚‘¶ýÀ•6ÝB¢ý ~@º‡2UÇ«€hÀçÑðâ<Ë‹Ç{HÅM{Y:ŽÇ¥“n%ÏíÄSÌË”,Cïet9õ‡ ¢ikα–þ>ɺ+P.W�žªÇÉ—ÈÇA±}0kPì€I½G„G@²­£ö�r¦¾*ÞŸä+Uœ 6ñ¬5 `Þ.Ó´Ó‡±~J0óÿZ‘ª®köâÆó¡]D¼�^ã)ÜX²Ú£Â•`È—öHº“yã‹‚.5¶ÔYv` *ѨVçzrE,£QeµK ÁÄ©YÖ5ÊÝ"û\ÉI�Ú-ÇyXb‹^w$ŠØscµo…¨¡+òØoÇþ¤j—„!³­€+V1ÿÚܤ¾² Aκ™Ì›è¥Q*#sˆ®¬EAhÒˆ‰§ÆTÈFòJYÌ–v�úÁnïœóÒó&\ed¤hv1º´?ûÔ*Kh¿?ö§õÆÀe�*¬»¹©xnoVù±ª;]ÕåüTƤR 3ÛŠ‘›=¢•=2¦‰=‘93E–›{:õMf%6Oo­µ ÿxŸäC[0Å äéîwÞK\Ÿ3“+èÞX&8nÙ+Lû$Ò·^_n¿%|ë1�h5Yߢ3㧈¨¹…öëÒrCQÔÞ°À@èÓ°öT,/ÅEÄíØi�žVV*kž 4™ÓhÍ9çXq¤èV³s|µXȆï”­ÕRí3ŽõeÔXj»øâé8¦ybÜQH3ÄÓ�¹û!‰ü3Û»ì¨ûÑzÏf»BhT–ƒ`ÇÉ1 ÚA�Qme FÃá½$FkKϪ¬ýWÆuñaó_£2E5Œ‚˜ ÄxœÍZ]oÛ6}ׯàÔ—h-lE�¡p É2غé6ì) %Êâ"‰ I¹IýIÑ’9RÖb2�XÅ{yÎååýp²þîü÷³O_ýLr]q°öFS\J¦)Ir*ÓÂFgo~ #Œk® ×RlV¿°ŠIZð/,½¢’iqΕ–|Óh.ªuäfë¨Õ¸éƒÑÿ}¬4Õ˜ÊõžÌÑ‘`MIEKö!œãuDÍJ?̉iÁº&IA•úæïH¦ðó�š‘²àõf–Ö @XÙuTý1é 'aÉÓ /¸~ iOœˆÍ?,Ñ«À §|çá•êÍ»Ðéû‰8psÐØ‰­V’ˆJá­""#5•°ªfR½%%æ$e*‘¼6¢¯ ­R¢hYŒ¤>‘ ‰‰O“ l< `þ”³1Ñw® 5¯Ѩãz‰Î©v�îÑ”dÃrºã v£r £^CuYŠª€²F‚áÃîµd%#;Z4 Œiñ ­¹ÔNÊ2^aêæ�¨œÖ¬³Y兀oב¹À‚*¡Å£×ŠoKÚN±æ,DB-�ƒ‰@«Û‰C›I€Â¯b;c9ò™>€° ‰dTãUð=Á/Ò˜s_ð˜\p Ûm,ܶ7ŠW[ke·VƵ™çfM•ø½†&«í¬…7Tø™ëœdüöìè[ënϨ¤·lj™?³¸Œ[kÙ$:×y9ØÁ‘ ;Ü™WKoF|:˜Ïz \&u�C Ø0•(ÏE:Û+l¥·Ãý1Öí-ê|8’Iv76n¨ÇÆÊŒsž`a�[ãécèyÂÃëº�¹mJV!xQÕw”m õǼ$2nÏ’;Ï9ßæÅ‘Ì„V¥ØA4÷msdGÝ1دÔ?ŽAˆÃQ‘M„²ŽkB{0ð_ü˜7Á‰ºÈÁþö"ÄE#m ãæ—.´Ð�€¹ôŒx›Ð Aq°1~Fá ȵ¹d’¡ÖµzE¬Z}æ·¼yºr™§¨§øÆ)¾Iiø«Ä]÷×x!ªm|Í`•B äᨠH¸ï×7IZ×Èl+SÙ¼h75ìv—Âeg û�{.ð¤ø…w '¿­“4 c{y®$¯vF—çJÖnÍúùkÊ*5’¸LIr�<½õr°mèÏâsä ~ËŒ ¹NH^ïk1/lŽ…6מ×D(›\x†bÂÄŒ¶ÖË߯{ÕÓáñSàš3·`_íÌjPúÖÔÅtc«]-ãµNÛºñ+–y1#p߆&mЮÒ4 ã÷zX¤¸ñÈþ_x6‰…>—�ÀKKÁ´Y.ôÉî1Ì_UkK!í?|”0>Ãu,UV`*›òjßz„ñàqŒ&l˜v,Kaß#5øáó(zóâÄÀŸwM^�BotŒHïõ‰ÑùÓÖÒ=&n`”DÿVãKù$‘ƒ©Û�î~ ~÷€÷R /U †¡\è=ŒÁ¾(èÖ6ÛÚÏ:Z)/îLg§UÂzÕ ŽÑëÞTà’ßûê¾s0†ªï–ó±=•KuaZѹväÉ�;h_£qi:Ásj’öþvôœ»ï-Úïx2Wq¢YŸãv‘¯çžY,ºÞÌWŠ¿¹§oY&ö˜c,Ûw´]ÇcŸ-a¾öÛ“¦l D• ¾Ã%/ÏÎ/^­¾Éž?—·$ø h’à˜sØðéÜI°¹“ÓdpÊîŠÓ�:YÒjËLP9îÀËÐ)EÎ2ŸS„ÌœÓd�rÇÁ\§Y˜Y§È£bÛBlÑC‡ñþvŠ &ºs⦳‚çB¤§ÈÎ&ž„‡q{3ÑD•ñ”™Œìë$e3ÁÑ/èºÔ¶Cæêé(w”@Þ®‹nu!4Xàs´Åøžåÿ0± r)2{xÚ›#Ítf¢qï¨ì�îþ2v’Ô�µe»ÿ°X;r uv¤YE5KÕ†~º¼ˆZœýfÎJþû”e/ÿx^¥,WÉÂÊŽÉXÎ [ˆ:ÛyÒn¢övw³ÛÌ />;¾øX€=ÚÅBçg}¼Œ´¶7óâÈ óMðj>Ë­1ù¨_—çüúSE Í3F^×ör:ß@Yé'Ñ·oÉõ?ù$gºÉ'¹¬—È–— õž“|Á-4“`iL iq0ª§£Êžì�d ÖˆHmO‘åïñÍ-æ™W,kÓ<´©%?Ϩ¸f!+6²¢SÌè¹wB6zf»%|¾åpͳÚÎ2˜k=\õ ö3d§YÐKðUmX°C&ÿ8ÏŒõ²e-YÑ¿…Ö øÌ3(,´×.gO…H2p[ÌZÆIvö‚E}Éæ×ÛÇÛ½¤’æ1Ÿgu·h™¦é4†®IÉÌË8ÚTæNßµ¶üAÍ7ŒE±ç +bWi �ÐBŽÛÙÈÂàU5ك͸�LòÄLÜ3Ih‘ÓLDd'ª"¡’Ãß�7þáõwC[0;`4NiŸfnh†Yê=GV¾A‹Üs�š$%…ý‰QD•,æ{`vyØJˆL/#Ë¡¤ô(6[Šd°k\Á@Á6Ôt—±KÉT   =Í³Ö Ä¾†ú+�˜ý¶2a²þªr³"mâ¥ä9•'ˆ -á_ ûKtúöâæÍwÛŽæ6k�†ï¼¹öïÊ´/­µýÍ` é¤MÚö¦ï²2¥/ê°ÐIø¢¡ÿލ˜fTÚæ{ô6‹>7,¯Ý�êôbûòzuÖdù6r9ñâ`V(~(À>1EsfìȬkŽDÞ!ж§U¦óë«ë×ð¶´I 6ԈϿ²ÛAMœÓÀ�‹/h'c4Æð9Bl€äðCI ½>ø“¤'Ó¹w_R]-4Aˆéßæ—û”CxÂVœû;>häO'¯?[¨³a'g4¢N2£ ïX ¶6îY׫ ì;<¹2ˆ6¸¯ ‹ìbE;wà[{®1Ö`Ÿ ÕÚO ‘÷šYS,/! MúÍýò«ã±Âåv^Â’'ÔѹC‹ré™Å©UÉæÛk4}7³Š»9£ŠFºòQ¸»¯8�{„õ3¡gšV<?�¡ �Y`ióÑ2nž¶Ü¼–ÍF{N}¯œÁúÎg¦ø12Tø7Õeà®Ãg;b¦zqò×}'íAÖ”:q)Ϧú”B³³c�$]NÜ�œüðùÔyχä?Uðãy),G„ƒIáeq€|ìQ òéµ­ÌAeY©vE5î2x YÿrMƒ\èíN%›‹7ºHÈNBàxŽ4äímwïÕ‰A‡ ÿ‚š íºb�{caÕuÅÈúb-[<([8nï‰v;O´ÛÑ@1ð‚KŸ¾uÕ kTóâ\ž0T)Ô= �f�ydú²Öc֣à · © !�H)ÏŒgOFPñ-äçòô—Ü-£ö�‡ó7”�<ЬêZ/ýž)nÏpL“Õ™ñÂ:­ÎÐhhðû Ó_àÜ.`­S¬°Oµ¬óÊŠ`’òà;ÙÉ ã¨�Çà °Þ"�eUÆŠ}Þ‘Y‚yœ•¨T{‘óξ©èØ’¨F@µõ tFòÖô†éysB@>@¾†p,¡â(o3Í•›R‘ŒQ·Í« ‰Ûší4æÃÑÆtÂE®D9,#!¡i‘Œ"LÆîÝcË–ïÁ«AÝ!Oö ™Ømt…-e]$Q¦ÒfË)Ø …¨³%Ď׾zŽ”b?{ë=MþÒ�iz �=™}ò«ƒ]žãè¡hQåËÑg®>E^eFë4í^LìÓoc#ø¡˜È|{«öXºÑ™ “{bÆãRŽwEðôu.6“œ%¦‰ªuOÍ$Q��zíÆl9øq­¿AUQk} ®o&�ˆšÑš#àÇ‹Ípš`žÅmñ…¥1õ-X{3Ã7o:[w*å†ÕÝ ½Ð7Ãù7ÃÃy|¿zß\ßö©‚¢1KºÛP,U ¬¬÷c`¢…:ø†�ŽPi#è®P#Lièt4ÖvdôêÆ“¬'oÏ[Ð%¯”†Õ ÔŒ†[Êeýf�gû\½ TÒD»Ëé„  ê6ß±úIrxob@§_a§ãe…˜Œaž%/í×ä7)ó™B0ÓdQ˜âB“ýïtÐÌ> #eÚ=— Y~:¥&öÓîùC3›ž#Äp•¨ÅêA]Ôa˜_!À%è!©È0-®ðÄÆœÿIq¿ G«¾M—,•·2¨²[ËéÏ¡ÚØH»c› ýFîˆFì{;„$óÎê'ÐÝGÆJè&@µâ¾°kíýär^P„‰­Â­B· ›„ÆaA7F�J`×}ìølŸÎ«GhÈø´¥ø!§5­¦‘ìÓûa=x¨=½@+¦Õ„¶ÑÍlÜ�$?×ZV-ëBMî(Hƒíy»»ÂTÛÛ–8SØ‚CçÁÓ*£ƒºüXGªò@<{¾G2ªÑ9?¤Úö£P{ÄN!†@çhË›¨´ž/d†¤Ÿ=Z|—4‘Ïkòìšen\´î-zÂÏw„0�¥KW�LÔoG÷ Yý`V?íÒ3bÚô¯>»¼Þƒø–—6Çé@ÂX~ŸpÃèwM£‘j×T÷¤®ħhð~Ûè-“®bše,q·6NP3¼ EÅ ø$åõØEá•W`í ]}Þ�ð'Æ G=Ãòã¸�i.$0¼¯‡ÈØÍ7æv^‹…]ß‹àœ‚â2ú¨QwÌêò :^ä°{VånvH œ]K3+mÈø)jpcÅn$j6 \64jÉ!¥œ™°¹Óâôœëëû�‚ïÕ??õ˜Téd^փ˒«0¸ õy× •¦x5*0}äÔ:ê"W Á@ªI×¶†Eùª¶µ¬ ìpçÕ¯ZÖ²:´lÃb,¿¼ÑzbZëøàè-aéFP/å$lRÙ«‡lWßµNÚ­»5ñ3ï&{԰̤‹ã2€öÚ·váF¢`Ša_êmm註w>e˜yD;t°íoôLÓŠk´"ÇeÕ'¸Cv“€ c¼ñXcO‹-*·X’bß‘>Í!¼=¸H‚“lä—Cî·"î~ô¶" ›!TÄY•0×:åÛ™’»ö蘆óÂ,©Ç0R õy9^àæ¢ß2™·>&eÞP€§çÛµû¬kû?fÿ ¨†_# á 0áŠK‚!•4xœÍY[kä6~÷¯Pç¡O™ ,”t2P6 ”4ÐeaeY«È’+É™�ýõ=’%ßÇñ$(„ÌØÖ¹è|߹ȳýéá¯Ï_¾½üŽ2“ó]´ 'ð‘SƒɰÒÔܯJ“®Ymà¾a†Ó]¡d|ýæé³T9æLÅâÒ0)<+¨2Œêí¦Zm7^k,“£µq³Ó�bDß¡¹Ú@,Úb$pNïWB×y¹Úm7ØZº�ò‚Ö[XW ±Ö÷«ìJ5ü­áÒ€"“ÉíÞ�hמּL;©í¦°šwèOIpµ¬ÀwCUT=KØk°›ëõ§Uµü7¤ æX¡WÌKê@À„Ø&öÈdñ P¦ö:Bø¶Î9”´7ˆÐ7Y"‚„P­�xµ)ë«û5[(µ5‘HれÀ3¶9æÚ{¼—g‚¢9ž‰ƒ[»,^å�¿!¸t6…Ô̰Wú&&Ú©ý ÞãKbÒ¾x†Õ3áé‹-‹ÔPû´P†cƈ´ãéöü„5Xè•#Ò%,=ä\X+à\)³!ÿҀߗŒéÆø® ¶­¥àÇwò\æ/�‰z&™e¡è©â b$D«N =¯˜1a螆êC0¢¶—dÑ$>KãRGÁr.0]¡e‘éë�Æ>8 ™-ÍëûØd«Ýíφ�’_o¶šï¡œ#¬>Zµý4ÒèÀL†(&ð�S[‘‘Â`&j¸BjánÒÖ^ ¶ª‰b…½y.x-Ñ Aر0²õø'F:“Ê ¤ëÝÈFË@ûÕ�³S²’º Aù–£Á©ŠàGñ„¢ µ‘„®Ö€åR¯2úZ·—5”šNÀzV²ˆ=�Ԅ⣟M¬‚‚FŽ �‹djî��ÿ$ù¢JQ�Æ3ùÑ,K�¶Þ!+š§®-*ˆÔ¹\¸ñ\¸åB¯½š¡=”Â27ªN´ÔG¦´éWí� àq,K‘`u¼Š�¦ð<_\EkñE¿&�½ ®ÃÜ{ÒѪp¹½T(™@5 h&“wÐéI{¤h2“O-‰e ÕQE(¦æ@©8$G±¶DØ/PͲ,‚B€í®Ó’[‹A?C °3c=…¶•`ƒAéÓX ­ŽƒN€�­„+C)ûÞÞ¦¾²üÇ-wÝÌTÙˆúiƒr™°”ÑäÊZóVT<;FЂáLQôƒ*y±,{Ò�vc禘»P~ÕÚ'êa�öÙP‡}ö�1˜œ[ó�Ť²L�üs{³Î@«nXu³òS‘JQ]Ø– L°*´ºs=-z*Q–œÍŸDQšÈ–¹]2¬_¸G6jG&q¸�J a¬haŒEít›÷VÖ—Ä”Šö‹j*9ŒHö ò™'úÎë+í;vÎv¿Äyë¾Þ�3Ó#}ÒÞB¯xø Yð@{Ë¡BŸ¶E¥¦tÅ&fgE{æ³²RYóL¤6øÎTÃ/çX5Þ÷ËÔ_]i²eÆ{e«j U·îzY_ �¥± 0œKS\rãÎ0<Ò¼@Ýiw“TÑ'cbû’ {ìÞüvÞ\A¦ô‘e%äxv�¢n”¦ƒÔ˜ Òx€ß¤�%h�üÿƒÍ¦úqè?9·‹L‚ —­xœÍYm�œ6þίpÉ—DÊ-j£HU´AªîzíImzRÒVýyÁ,ns¶Ùäþ}Ÿ±a�;öÒvï¤; xfüÌxØ6ìNÝCø ùI#� œï÷õJÇL€˜ixM„B×HçHÊÚz(ïEÏĵ�˜ƒX)üJa|½AË@Ú+* ùÆU}VÇk›¶EÕ×È6cË&tñ”÷-†ÿ_îå{?Ðÿ�Êe’°K(÷±½—§6|x‡ÅÃøî›)¸×‡btåNþ]S^ïëÙ0=NÁÁq~TŸíYpÇÏ“xiâÑá^ôÝÁôàíôÁô£+ð‡+òØý‹IØCg¾[ýžú�<ýÅûû)Àý,Ê@ÒÓÁ¼2íòIpð0ô²à[×KÚŽê`¥÷.r®P3K^%bà&ýË)…úYêAµüÒÕ£½Ûd_êOÃé¯Ì%5;uÚ7³›s§A:!ð+êO.ÐÈÀ“ºÛÉê{] ôx]u„¾ègŠºäØÂÆ· ]Uó«úOJš¡äY»¸r¸-†ï»` KµÍuS6ÁNŒ3r×b±çç—/VÿnC�F-lynTëð³cðË“*p£�ÿF/ÃÇ1¹i8¼œæÕÖ}•:ì�§R Ƀ®K*ÍSÁŒ†×¡¦q7Q= 䕨j‹¦,Œ÷·KøAèýÝ“‹BæJ¥OC—ÆíÍbôQheSAɰ+CŒ пæô9æd:QHª—#ÒAÈ}(º>a$ª …¦�®“�ÞÏøK÷ùTXµÊÜ!hoôŽÅÊ�Ëï ëkx"Ê ˜§ª c?N©ò“¨„¦o–ž„q­é‹¦q?ëI§UÄX”�¸,9¾±Pƒë”¥b'y׺=~0ê:†0îîf:!ñ£1c\– ¾o Ž7ó¾„�ÚM#ÿ«ì?)C°è ïï– ‚"¬9kxœí[m�Û¸þ®_¡.pM𮳤WÇ@šôzÚë~Z‡û@K´ÅF’ò®óëû I½Zr$¯7Í6ðZKÎ çõ™¡²úÝû¼ûðï»?‡‰ÉÒu°ª>8‹ñ‘qÃÂ(aJsóæª4Ûë×WK<7¤|](¹¹ù‰¥ÛŸ¥ÊXú^h£Ä¦4Bæáß¹Id¬WK·4X-=É�ŒÄàv­ 3Ø""ýC8‰ö+æ,ão®N츉âíÕzµdÄçå)1i%è¾ÄÂ"ŒR¦õ›«äU¸ÕØÉøäW{¦ÖÅjIá›pµY[r›uøÜý)v[¸•îÛ÷«¥%½ZGlñs�¯æ Ùã›%ý¬, ®žyazB­Ãw2+JÃC“ð0*³2…­ö<ŒÛ†Ù–ydyþîý�ßßns,öÕ™2}ýêÊÑsL{"CÞIª #'�¶â€Y(·ôk`ý†mD*Ì¡+›Üü‡GÆ“k¨ó=KKfx2c‰ÑwÐyÐá\æ¤ä¿OxšŠâ�§U*nJ•;Ùé )Ïxnp„ wœG�&ègð4KØfb‰é1$Æ‚h¾7?´½Yœtçb0šºŽ+ò=GêšàÀb–7bŠQ.N¹ð§’åF¤<|n½¸’Ó»Ér¦K'<¼#×9.ñIMõˆOêB¡:Ad�üá `¸‚¶•Õ¶bùÎj™�êu²4’A¬‘ºF‘åŒs0n�{a’�ű Aq#s¦|Ï�gŽØù& ?�‡Æ|ËÊÔxæ/n^¼Âjå,#4$ÊÀˆã߸ã�&NiஊŠ/h�&M,¢èÙ#4 9~X¡Ë†?)v°=s‘îk¡‰AÊð.½Ü'щ£xïùÓ¡ÒGÕÕ×ɰ—2A/—1È;–ÃÚ [Z÷¬KR+ê{<…¶PÜ–ˆÆ<=à@<ï¦Zµ†b 猙aè%¢îzK@#žHBV“Õá~ùÕóXÐv7©àñ9¥rò¸ ¸Ø´àЩYóGoÝÒ4š¨›)ÁÝœ!A#]qÖ˜ëî+޹æ:•fª¥±ôh6µfAŸG’ž�oéÃILJk¡ªÑâ‘ÇÞaÉ XîbŠ“³1\DÓ}6õŸÝp˜ê¢=˜†ÝöÒò©a(ç.—²†¾dÑì|nÛ¼üøN|êüUÅ ò˜.�ßDVH� rO¨.,–;äÕ ÌŸ#¼5 •9àªBHºŸ‚?Y\E¬ÿ�¦A'òö‡‚ÏÅ }D2€» Ç󤑔ŸuÝý¨ :<(ü µ7,¾uå÷ÆaÕwÅÀùb-[4([{Š}$Ú»y¢½  „ˆLiÕMP+¬QͳSyÂFP©I÷¬}¹AÌÛ^uû°�3H…¤.„ 3Hì™H­gOFBq*òs ô%wKåΪǃvððþF²à£Àé¡.äª:3£ãY.ƒi’‘:S‘;§µ0ß   ï J-çö™À³N!ØážUç•EHIäá;éÁ¢Ü �¹ð (ìh“…²ƒ²jkÅcÞ�ÝBy\ä¥,uw“÷ÎcS±±-A ojë‡èp$òÖôƉô´9�È×ÇGW†°C ¨Ü–ª ÌaŒºý]´’¸«Ù^cU8º¸@W }ä*’Ã1’ ͇âŒp¿÷�[±…WCÝ�ˆ'€B&v =@áJYIÔב¬9r{R¡�u¶DìTÚ×ç€D%·Ó'£~õùPñ—þ¤±ÄÔF¹'¿z„U±½VÌËìB(óI¤ªïšÁ«HY�ŠýÂØ=ýÆO@š›™ß�¨¿9 \¬a™ ŒbÇãâ�w7XE—£­Y�Sídå�ÏD�z‰§¦‰ •X= =ºÿ@ù¹kñý¹EGAgK*ÚßLæ¨þ7£.O ÷5 –ß>‘Z¸"Š­«Z©îa†`•é\ý(u5;î Ìݼ¼™•¿ž•Óúf}ûâ6äŸJ ‹xÜ?†æ)TAð°>�…{²Ð Q1tI¤® :C5š½¼­HÖ“0‚Ù’•Ú`wƒáÖð²~³Æóˆ{.ó£W‰¶d·[é… ÐñŽšh‡<«Énû]ƒ�~…“Žg|p—ž¥®Ý×ø›”ùDêŸi² �âÚ&ûßé ™aXFÚ5¦[¡ Y~&a6ö›nÅC3+ž#Äp•¨Å:‚¬¤Ãv~E€+ñpD$LdJiqA7(ö:NÉûE{þYµÛŠgPy'ƒjw´Œ=ˆL|v#á¶�Œ¿Fi¥ßÀ_™ÈíÑ ‘dÞ:ý´t÷‘ó]T+ïs··¥½}Σê{�ÛòÛ©p‹6Êo<Š»íY.è•·/+kuÖÝÛ1­—ƒ^Ч¥Å.c~eë¶¿C¯æo$#™Ê÷³ƒ%ÆÙ» š ƒ4Ôw‹¦¯"µq\=²U¨ua;xÕcuP× gõ2k‰ç.Ç”:u&v‰qM …Üh"B´k®ÉÒhð¨ø"Œãã0®Õè=çœÖ Äb™Mì¬ÜâG¾‘Ðy¯¢øCùé¼|þ@tëo€¹É´~0p/ù2ä ù\ÖŸ}j=BÙá9WðûÐÑp7‚�Gá÷ ïÕœñФg4RpšÄî×!!mu•@ƒ/p�¾7a“PÄÒ”ÇþE†�†‘)]�#¯WÓõ*õ$¹ÊãcI4½¨ ξÔÔ“ûXd4{…õ4¨lµyfRQ ¡÷Ñ›úIaÃÛM>©´š{Ùšø3ÚÆ>‚ó¯@9U y�Ã=§eæ§p,§)°²SdžL5�l½Ãá4Ð>ÅY­dÈÓ© ½5oyxõ�s}!¿mS¨úãÏgß&jOLeXy¡kcÝ#KvÞërÚ0z-(rÝ æ\ò"¯Ê5‚AªI¯, ‹òuÞ(6 �9²ÈDûUË/dDÓ6bC{,[[hÐyb[Ökðô.aÔFÐJÊGHØ$¨n³]økT1p½Õ™´w[âJÔ¸š 79¡FP698È•2›ªeꤸ5G'º >º¾ ³ñ{ßêÅ•iJñýKà¹,ši»¿ƒ¶Y•ÓmJƒfs‡r¨�œÂŸöŽÚÍõãÖE/ñË�Ð�È–{5ÑZ„�ÍÊ£´Œ¹ÇÚ^÷nTã_³š�à410±òB1¹oǤ%;/±b�°¯®]&�6âàcR:m ðø$ºôÿÕqéþOå5Œ�° Ç ÇŠN‚$• ðxœÍYKoã6¾ëW°>ôÛH€ŠÔ1Plš"‡n4X`�EÙ,$R%){½¿¾3”¨·åȉ€A,KœçûæAyóÓã_Ÿ_¿½üNö6M¶ÁÆpÁGÊ-%lOµáöa‘ÛxùËb ÷­° ßfZ…«gyàðøš#¨|ÆjæV(I`AƵÜlÖ…H°Y—ªC�ÐÐíÖXjAJ0sO&©Ù`C‰¤)X\’\¥ùb»YS´ywÑuX úï`qFXB�yXì?‘ØÀß¾ZPÅTt9÷ÞáÙ„[T‚Nt³ÎPý–üÉ)ì‹j؆å:(îGâà §fùiQ,ý�dÊ+œFªÉ�&9wQÒâ‡�;b÷œ¤¨UÅx�ÒCâ]$Qs¯ä›Ê ƒ»”1nŒ/¶‡»‹ ÞGnÐD¤¬ <ÅN¦\ÚUéú|ŸLBÓ0¢SÀ)$fÈ+îƒô÷žfüãQ2Ní{aònÏUóË™ZWvøú&ú@¾€FŠD€H3¼.`­$άÒÄdœ‰øäÑtQ.Àt•lÈN@TøH¯Èë^˜Ú}¸ÖP—J&§káù’§/ž¡f 6-Á€éèï£ B+Ä®J1sŒ2µ„´|Ç}ŠuPÛIÃ`­9Pª"��S[rœº€Â“pÚð´ºOí~±½ûÙ PòëífÍÓ-aíh;¡+gñ‘¦E†7¯‚²!?' -3}X�ÿGàRè�J[µ½{;ÎÁý ÌuéZˆÎ ¯·0€ì`¨Šrù^t¡¼Be¾Öй´,Lø~Øî�5ÁF“³y€9rÇG‘ðTN8è€äGï€UC-¡2Ÿàˆq-g^u.�Zj©˜ÒTÞ'IýÔõS ‘›J�Û’wƒÔèôeÛ·GbXæ¦àó½øIhc»•¢„uV‡*—Õ§›€Ï£áÅy–5�·‡Š‚øÕ;³ôY7‹šævRåe<ñ€®{]Ë­gS‚Å£)äjˆÍÀ®–ö>½žº+ðõ«Î�Çã´KÔNÀÜ×>W5ÈuÜCpÛx°+Ô~€SRÍ2ŒWµù³:P OÙ¤ãÒ€øœ ¤i¥`ý”@¶kñ½:åT …ÈéÕ=¥S8J;½žR¸±dµGþô<àË ÚÝ) Œ) �ñ[êPN!A%Õên\«�J.£QeµK ¹=r.ÏÅùÖð»Þ¹2ƒ/o`ËqžHiÙk|äHyaÑQèoµtEžû�¶Ç›Uf¬ê¾Võ´rÔbJkn2l¼ÀÊÅ|GŒibÆEÏä̇O÷Ï2Ëí#dϤ^ê…æè¤µî�Yî“Ü@P ŽX‹žïuo{ƒësfsÍ»Å6V ŒUø R=‰Ì}©/Ç_±-ÑŒœ·îòœ?DÍ-íÂRn±í ŒKsÞÔ›Šß—„ŒŽ—x†DY¥Ñ¼�`2-¦Îšmαâ„Э`oñ‹®T +¥SXŸ+¤ÚG–R´,�Iíâ Çܘæ‰u'8yšžîðÜI¬ù¿£!Á~%ùŽº×Э—dØ :@ƒ²œKv𣠤ÑÕVb4Þkb´FzV•`]þδ.~Øúh�š‹c‚#—JKxœÍY]oÛ6}ׯàÔ—h,lE�¡P ÉRä¡]€töPeq‘H…¤œäßïPÔg,GNWÄâ˜"y/Ï!/ï‡þtþÇÙ·®~'¹)‹È »/FS|•ÌP’äTifNýÚd'¿úú 7‹*%ãÕ¥Ø0 ¦µÖœŠs®�âqm¸aàæyaÐê‹eúhµÿiC ¦òD"Ëz à…”Z²Siº…µ«ü²BLòŠ$ÕúÔÏ?’Lãï�z™² Ô0ÞCg �dTV{DÚ餛ßP¦1/¸y$éH˜Èø_–˜•çDS¾é •úä£ï´ýF°e$Í´V'I¤ÐÕDf¤¢ {i ùžP�”˜W�”éDñÊŠ¢W¤DÓ²*I)¬ “ŠÐg‰x&àý–³mÁ k®¡ø µ¬õÎíyï‘ûœ'¹�™a‚Ôš¥ÄÈ–��âD°5ÎkÃH%5·�â‘è[v�y–ÈŠ�K y eè�IÉ,•nOH¸¡**ë0°ßn'rZ±§3 ZÆ)u³¦t#Í@šäž>j 2QŒ ½ìü¼'FQÛ Zðˆ\p]“�Ä"ÍiÕš‹51Øx·TÆ��×YDV‹¤;$hj´�uà& ï¹ÉIưOû -j¦‡E¼v•’Þ²¥eþ„X\Ö�ª#•·ç~�·“ƒy¿} ﺕûÍöæŒvo†[†àÍŸüSr�ËH`V&—éžìV8#¯¥ø0aH‹*§“ž„ -Õ¤+SìnÒ!›‹¬·7&³fãmÙ�ÅÝØd bdKð ÃU j]—LÀ›P=6.6k,°Ëk¼Î%{‰s¾Îq?Kd u©»Ì¸Ôóvk¯â¬©yýJã‹¶àeáû8Œ¡ÉºÄÆ(­¯õƶIðgG(ü³„!�“Ýý‹Za¶‚^Ì/i³�%6Ì,:Á1ƒ£­Á’ÂñrÅ2„%c*ý)˜XÝó[^�9]IµìSÐj½é´Þ¤“`ø?„]í¸…�b]3lG¡%¢aÐvyd;¶é›¿­*,°²yÅ›ö4ýáX)¬uOáîÄýÑÙ?+~ÑY„“·–™¤™µ�—Kãã¤ñy¹tÕ­]}ÏÚJ¤N�%inG׃,vÛ.æËd ~Ëœ°m-H_÷™Ò ÀÞ§À¶F ¹qYÚ¿u%m6–ˆzõKNó`"†3·¤]õv-¨ü`óU7y¨QQhÒ6¯ûîEÞ,ºò²ö›°A‡LÐ.‹Ï—I¦áºÀz=l.vù} ÛÆx=Mvrüð0üèiÏð«]þ^4¯Oãk]^õ™¶Mç`BŒè1IÐ�»GiwÀ§Ï³ÈíÀ?j™üQï‰ÑðQù«ÉEG,\Ç,�±Ñ?ÍfAâ›B(£î†öôaÉ@ó!_êC¼=ÌA¾(躩‹M7kgÆyP:“Èã1š°‘ �sÔ†Q‚,Vñ‡.CŒŠ!Ðcì0vÕÓ¸Ô¶tk{ž=°'åÞA(\Ú:êì¬kÎÞiWϯCÞ$h¨j÷1µ K|^”S¹ª¦K¨¾¸§—M�Õ/oS“²·éú¶•–ظö}B]Ö…{ñ3 ý]]HÞž�_¼[ý€“~~îjŽ=ð¶:\fÂDåN�É�Z&‚;uWS܆‚EÅšY÷±Ûd_ŸŠ}á‡ÿ—È4¯�=jø¿ý^f`gÁÖ…´E¥õÍ%&ý«]7�<—2=6fMXI¸µ�EÏ%EÆSfcm—ùèÆ×ï|}5®°³î¬Zöf;Ánìê ^¬*¤üŸ-!ÑÿŒ5u¯â_µ’YsYÚÆŽò7³wt5úÍv?ß-‡T–~ä¾çH}f‚)û2×M!T)ûkJ¦Ð~ÖÎEId£ø·tA´!ªR’² §]ÍyLެ+kü¨k=S¸±£‚ŽêÅ�ðoéú*gÿ­ïsê ý5;p¿™ÿ©Êa ‘摈Q‚&‘&ÇxœÅ˜mo¤6Çßó)\îM+%‹ÚÓIÕ• µIS�Nm#媪¯V†Å]°‰m6Úoß¿1ì’²›«6+%à‡ñÌoÆöØlüÍÍŸ×_þ¹û••¶®’ ^Äs¼j²œe%׆ìUØÚâòÇ0B»¶¢¤Ñ*]|&-©ºÆj‘¶V(G¾;ˆ£^Mªò­Sú}b,·™ùÈf‡C.ˆ9“¼¦«pF*Lâˆ;�?Ìc /ˆ–Uܘ«°üÀ ƒ¿KT-†g*§'A þĈ©’«äžˆñÊ(ì÷¨o Ør¼:–kÞ4X³ —ßõÓîç“'ìÉàaÓlhy¿Ë,~ôÚô„a²+¾NC½éUF~FÈ3>)^+i±‹LŸË÷ÉÎÆÌ^X¢ÎZAÞ^˜Üíœ hzïR?O»ÜnuÛ¼O£¯ÕýnFn1®ü�e&Ï[Š<’Ø1ige©ò!'þîkÿ;!ŽµÎÆ"Ë‹0ÁcÊÿkU#:þî�µu[qwh´«×gÍåŠü]|nÍ�¼&Žì㞇Ð�̹YqkëhÝû0¯“:/±¤U¥V¸ ‡É®xˆ‚~={qªD©T~^?·-›Ã»ò _±ßïÄ»7؈M¥pú»çä-í/„qøâ>1#¶}®ê0ñï)ÎßH’v_Ü^„q­ùÖ°B£üÒ�å�07<-c�Ï5>Òi#üõå¬Ëx¸œ„ÉPzá²EgÝp'x 0D�ã²×œ!»‹KÔÿ¤ùßëþÚ"\–‚%¬0:�xœí[m�Û¸þ®_Á.pM𮳤WÇ@šô\Û|Z‡û@K´ÅF’ò®óë;ÉzóJ^mz l`K&g†óÆg†Ìæïÿñîã¿?ü•¤:Ï¶ÑÆ0šÀGÎ4%qJ¥búÍU¥÷ׯ¯Öð^s�±m)ÅîæçâÈàçŸh¥§Å{®´ä»JsQ�¿1�ŠDmÖv|´Y;º;‘œ�ËíViªa �Õd:=˜m()hÎÞ\=4í&NöWÛÍš"Ç—J�Ã�ÃK]’8£J½¹J_‘½±H^öds¤r[nÖøAÞ�Ínkhî¶ä¹ý)±¿­ìÈ{ûôýfmèoÖe�7ü]ã~jàÉÐV•%“ÏœDɶä�ÈËJ3¢SFâ*¯20â‘‘$4Ö¾*bóåù»÷?~ÙÉ ?ú…åêúÕ•¥g™vDy'é‹ÄVeÄfDìñkdŠîxÆõ©-›Øý‡ÅÚ‘k¨³#Í*ªYB¨6Äðèò"jqö‹9+ùS–e¼üóy•Ét% +;.$c9+4,!ê,çQ«‰ÚË\Íl3'¬øÌ¸âc�u¡s߇ÎÍÏzw9am?æ–áæ³º“�ztyΣ?W´Ð0"ËYîR[æŒsŒ‰ã—´fZÓŸgNœ³�ýY Ñ)ôÜ;a=©Å>×f8ã ­fÉϵÎz˲Ólç%øŠÖ+Ø!‡ŒšcÀzÒ’6,€ä/¡.óL íÆj§³Œ§B$ ™¶-f-ã$ {Á¢¾dó÷ÕK-^RIó˜Ï±·›²D¹‚Tׄ2ð6•µÓóßA�­ÿDÐ2ÁýbQìyŠØí§v@d!Çíkda0TMvÃ`1n%“<0wLZ$Ä” Ù‰ªH¨äð»óÂ?½únh fŒÆi#í#¡ÁÌÍ0K½æÈÊ7h‘;®SB“„£¤°>QâETÉb¾çVw�y­„ÈôÚ1²JÊA�òPa¥H«Æ <`CMw»–L•ÀÚÓ¼k vê¯böi'dÂdý¨r3mâ¥ä9•'ˆ -á_ ëKtúæêöõwWÛŽæ6k�†c^¿ðceÚAkm¿Ù†Š!�´IÛªómV¦ôY: úo‰ŠiF¥ÿˆýÑÛ,ÆÜ°¼vwªÓ«íó«[°&Ë·‘3ȉ3CñCö‰)š3cGf]s$ònùˆ.�°=­2혿¸yñ FKk‘bCÀÞ0âù7v9¨‰søàÃâ-`Ÿ‹Æ>Gˆ �þ(‰¡Š’ôdjòî ÕÕB„˜öm~¹K9„',Źÿ°ãƒFþròúð]ƒ:vrFC ê$3 òŽ%±Pakãžõ>„}‡'W½Â÷Dc‘�`A¬hçµçc Ö™PM¡¼ yÇ¡XøÄò²�Ѥ_ܯ¿9+œn;!,¹xÿœ×Ž(—íFœZ{Øüv_{«MÜMâÜ&D#]yQWíÃWìªÍ¶{&ô,ÃÃøÇ"&0±¡2 -A>Z¹Ӗs×’ùgÏ•?À�3˜ÞyÊïEfc S]6 öqølÇÉTßíà!B~ÞwÒä]M9 —âljO)5;IÑåÀÝÉÉÏȧÎs>˜ ß© €ÏK¡`‚8"ü‹H ƒÅò¯G}ȧÌ´2…y$¥˜Õ8Ëà/dýˆ5 b¡·<•l.¾è" ¸�ã9Ò�­ŸµÝ½·; :(íª¦.€¢Xl[„ê;Åáí‡�~…•Žo($Àb ð,ym“ߥÌg6�™&‹Âšì§ƒ¦×a)[Àî¹ÉšðÓ)5±‡vÏï›Þó!†w‰Z¬´E†ù\òû’Š Óâ OdÌùžw«°�êËrÉrPy+ƒ*»´œÞóœ±=åÐFÚËé7rG0bß[!$™·V?�î>1VBõªw…�hïG—óÜLüÜÚæVaIи+hï¶c¾¨³ØyyUë´.õú´^ºB—VFó]BÝÐ`!¦„ÊΕé6TFò•«~7kõ&tvÄÁ º½uº½¤6‘Ý•Ì^œ %Ô;†µ}•âÙ#7’Q�ËÎù!Õ¶d„íBìÁ~wvG•VÀÃó…`NúÁ\ëÑùÏÅ…PLD>§³3–¸üк*èÉ>Uo:C—Y îÕïE÷"Yýbÿ.~…óŒ�6K«/.ýö�øO¬`¢‚Xö„’ð}Ÿ .v:F#›R“üÝ8ÈW;ˆI/ÐàŲÑË&EÅ4ËXâ.Ollfx$¹ßwê}bâÅñ ‹¤/‹Â ¦ÀÛmHõ9@Âsìä‚ uoÉ·ÉB®¹�¸ÑàM9D°®ïØp·}TÜ€õ�Î(N£Ÿ€u·±¬2 ¹ ßEcgUîzz´Àž²4=̆Œïn7GìB¢fÊ¥@£—ò¸Á3�þš»%N�À¹¾°)øzúËã-•Næ¤:¾äaµ ãÊОw‘OiŠ—“€»GN­�.r‰¯ ¤štqjX”¯x1ZK¨é!·Ì1§Ÿ³¤MuhÓ†ÁXJ7À¢õÆ”½#Æÿèè-aãFP/å#$lÒ×{‡lWîÈ–¥µºúí²Ú‹šøþs“1jüeR‡lÀníË®p!QÐa°ƒzK:vî�f¾è:dö·i¦iÅA‘ã²jZûîÀÛ$]ÃoÖ ÓB>Û�° ‘bM‘>Í�¸=DH‚Seä—C¾·"î¾-¶" ›!TÄY•0‡Õ�òm¿ÇÝýº0Z�Ýœ@…áKÆè1ŒQC{^Þ…Ü\¯[&Û6âÀǤl ðø»vÿtmÿ×éU/ : ¾:�‚(š!ÌxœÕZmoÛ6þ®_ÁØÒIŒ(P´Ž�!݆aoùl†} EÚâ*‘*I9qýîHQ¢lË‘e/@ Yôñî9>÷BÒY|ñá—ëÛßo¾!™-òe²N< n)I3ª ·W³Ê®/ÞÎæ0n…Íù²Ôjuùג焱Z¬*+”$?q›)fs/–,浺•b;Tþji,µ0E¤æyP È' J$-øÕ¬Gú2eëÙr1§¨ÿu4”}¯A¨$iN�¹šeoÈÚÀÔT±^—Þ‘Å–êe¹˜ãƒ\‘ÅjéT­–ä…ÿŠùïνä½{¹˜;µ‹yy`þ]À«}&ÓðæÔžUeÉõY dÐ’\«¢¬,'6ã$­Š*^¶œ°˜ˆu%S÷áÅõ‡o_^&~2ÛàOa.Þ̼>ot2à´L$õhŒƒƈZãÇÄÅ]‰\Ø]›ZýÅS[«kµó-Í+j9#Ô:eøz…L:–ƒ3'‘•ñ<åûÓKJ4·•–;:’ó‚K .${î<É›¤ëN¯7Ñ0)*cÉŠ×ð™ªV9¨•®‡��›fø=¨¶|Ãõ9ÉÕF¤4?'T²ÄšZ® Õšî <8xް9 öçRV ËYÑ—´ã2è>Î q2…Ê£ÙÛM!·Ê․£²¦…)zÓ¦<•6Ÿ**­È9yá2'à¬C±<2 “YÕÁµ†å< ?é!QøIO„º„¨ò ˆ:ðÖŽaMåÆ1K{¹w-2€Õ©h=XQ¯GVp*0†bPVÄ”y�ã8Ã9‘ÔbqJ‡°¬ï%|ò´01�œž¯u,98ëèqj‡ªœdµ‹i,ÇŸ�ºtv–…KƒÏvçt3fãÔ+uܸù‡�ÿƒ z™+;$°@ì‘§j!7ù¡"0•Úš¹ŸÁíÎÀ¯È×Ôgö¬“*  pœ8T<âî§6Ô³uhÓ£–“U±‚=±?Ù€ß2. Íœ3ß‹)ñ[\x|æMã ·îa÷¡¼‚ÓË÷mWg¢À–8šÚõêÀh¡4Eð.A‹�﨎Œû‚«anïT´Àmµ¥A­€~- Fc´%®lC˜U¢Ä.±yi�7uoŒ6(µ#Ië€Ü·Y½,áÜ(<.4Ž…õË¡éâÏ­†:ìC¨�,[Ʋ5 ¤&8_š8wœÊqÆÂbSÍ`ðÔÇá$—-0@5èJà8”翵z-”�œÑ ˆ³1q­Þ¾’ ›h®;#îWµ†ok}SÙ (Ÿ€°-D'H�Íž“;ØWdDÙâFs’n4PY؉´¹ïîá·P8\p¨á¬R{žqK‘ó£–9ðld°Á”qR„Ø61§r\mAe:ÕO&-x ª1€§ßöÎë¿§˜û?Úøz2{cˆ>‚'‘/xœ½XMoã6½ëW°>t/±… °@‘:Ú¦ -ÚšE#jd±K‘*IÅõ¿ï�"%ÙŠco뙚ï÷f†Îú‹‡Ÿ¿{úøá{V»Fn²uúƒPÒŸ0^ƒ±èî�«–_-r:wÂIÜ´F«Ð(”Â:#ŠÎ ­�·hœ@»Î{Él�G‹….÷ÞþÛ�uàHKp{Ç.±D*Ù˜‚ï'VÓ?‘äb³ÎÁû»=íL…<Ý’F˸kïõ;VYúYÒGGö¸.O¦~—"Þ³u±™[.(œ ¿Î[ïcÃ>�!(„¤RNS÷yf½X)žS0�]¾[ôšß`€;4ì¹Ó†Ù¹¨öBm™«1˜`º Ïí ?ÓŤ½bì©vŒŸž A¶ÔJîW1�œ"¹„þè‚Ò÷G×+x²7/sÿ†ÙFkWûúT�â!ÿWb·oC‰GßzO�Å’U¤@R+QY_y$¾7à�ªýè|‰µ ê}Žo”6 È71ø›t\迆³,R @m%ŽÒä,¾ÃòZ‰Oú9½'—uÇ8(œ£µ}d½F*\ø0–´³>£R»žJT±U *÷™„øT¹¥«/àÄ {=ZLLΙ1¼L�r 冴š�ϤÇAñd‡Ç4)Î9Ye/ Á™„ÿáóK×¶Ú¸ Љ’×Ãf08G&¾:;Á^Ç'ÂBòw�AƵr T‚¦ÒRê] eiίA)6C͆1Â;†‚TCoÏ'DìÁNºS%–©©?‡æM¤9h^`n§—(Ñ#ÈT× |°ŠÍ�øFúíÇöëïë› ÛQ8Yˆ†R¡Hˆ0ŒŠGO!;°®¯¨ge6ÉŽKMó*9†#õVP%çc0c»C5xÝ;ô“­79w%¬@¨Nwvâ+Ùïáºa w°·Q‡°Kµ3²4f§DH¾r]@ÿjíÉкÿxA�ŒÂ×k“©Íy§Œo™PDEšAgºbF§·_ÒfBûõmϨ#öo½¹¿q×�êSZ@ï…±Ž%JÅF´âö€ÙÓš³HïË—…'Ü&á3W—a°ú�)t–' í4ÝŽI'Q�nŵ.?“B�6¢„åšH_�DFç,z/aÛØ¤þéÌ=læùJLã „Í&qPPJ_õŽú¨ÚÎ=€ƒKÀH²W„b49Ÿ�ÈJ8ç‹vºXÿÏ,C´ÃÀ>šG6Ëi Ù!´1¡¶Ý’GŠaÓR¦` ìo]u8…N"Úx/„=Q"­´ä^[ £§ÊàŸ§s¡m©´Z*ÜB¸Ùù·ÅÈcâåqýÈX‡ŠUåoð!K®�AÛÒ r:ˆÆi–rvX§—ËD_|<71ÌØ3%¾ñNý¢blé‹H�¤( Ã mÏØFyü¾ž÷ÿø‡Ñ§Ö Ø ØŠD‚*• ÐxœÍY[kã8~÷¯ÐæaŸš„–n¦[( ³…- ó(Ër¬Å–¼’ÜLæ×ï9²|w«ƒ0V°Gø­EXX¡$�•œk+¸Ùm˽Ánëu†*:¡…Û½±Ô:YsOæé¡`G‰¤X�ÙdÅj¿ÛR´rwÞKØï`WNXJ�yX%Hlàg �t0Mó¾òíDváµ…`ÖÉì¶9êݓϊÑòTƒ×–ë \‹Ä[e53ë«rûGbM©&o4-¸ =e $¾ y 6á$­ªŸxQzF¢öÙÈWUF%¡ŒqcœhyôÓ}÷ ƒê#e]h x%2ãÒn¼·[pwVèA0£³¢ïv. €W8Äà/)Ÿ @®Œ°â�ÿ ãÔ¾ïíõ�h?|�½³@é -‰ÏP÷ªPEC‘ iÒ�öÀÕ‘7άÒÄ䜉øTáåZÂåÊ͘�€¨ð�Þ�×D˜Æ}ø¬¡|­•LO#ñ¥È^*ò™Y0t$–Ä §xl!‚¦:_̼Š%¤å^åK?ö#j{9L³( u�†óéŠ, I_ó&¸p$;žÕï©MVû»_­%¿ßî¶<ÛÆÓ”P­é ÕöÇ�£° á”Á¯”cõ!LIK…¬qª’‰vÓ´öð:x=rôÈñåe¨µ¯‚]GÿÁÖòÿGJL¢´%Q×»ù�K`ú7Ö‹ “°”¹ ’•êG£RÖ»Ÿê#”AVu®%—l¥‰ªwuûUåɔ« ‰<ðIE$<ùÁ�üX9`ÕØI¨Œ¦†˜Ü/fÇ«.d9çÎ"F³}IN´µéЬºÖ§!F—’àÖ“àn”½j‡öH ÛÜôy¾m> ml?ý�¿J�¿¡*dDõé& †Ãz4¾¹ÈóÖæéò^“ +Ça˜=ëfY¨ÜIJ„*™Šb@ÌDEÓèÙx”x4‹G­ýK©£vȤ§”J(«]ççÓi†á%¦±îեţcq'�ÀªÞøNIµì4\WÚOê�jA%›w5‘»Jh«bÕ¬Èa-¾Õ7Šº!p0kïï ½ràí zBéÆš5UwÑ_6оèAI ‡OoSiì¨C80µhÔ¨»q¥^*¹Ž&•5.$äöȹ<#¤V[ :-PÌ�ŒS?ÙN«b8l§ÇDÀHÜx�Ò:<ðëø²N‚MÓ˜ê¦ä§"¦´æ&ÇÎ °©ji1MÍ´è™ôXnæ~–ya!Cæ5Ãj÷¢­°Q:2aÃ{Rˆ_Ik­óÍjÞ°¿`¶Ð¼_Bc•„O�Àidî½¾¿OÅÞ9oÝÇ{pfzT�ÚGºåÂaí- ŒKsÞT‘šÊ%m„ŒŽ‚x‰CY¥Ñ¼�`2+'ĆXαrnï×¥9¾b)•ªeÅ;…U·Fª{‘𢾠Z䯋/Ü3cZ¤ÖÝ#`8äYxºÛë0$±æÿN†»�ä꾪í|÷„õ½4(+¸d§‹ctƒ4£ÆÊHŒÆÃûžm‘žuÒoý¿S¶å?nþOûí‹\‚)—<hxœÍYmoÛ6þ®_Á©_Z ±°†B0$Ë ë$Û°O-QITHÊiþý’z�eËé6¥@bQä½%_ך‹2 �� µHžŒØï#¥©¶ ê#9 ”^HII væï¥ó£0 Fî‡À`Õ +çT©3?û@R…¿S¼jˆEÂF¨Âõ!ak¨´,aP±ié¬9tÍs®ŸH2`"bý7‹õÊs, ß¶X uúÁwR~"ÉÕv½FbQ*¬*"RRQ ?i&Õ ¡¤Yî‘„©Xòʰb¶Lˆ¢E•3’Pìi*$(ó9ä-dà»ÍXO?²Ž+ˆÍËZÔj¯°ò˜ñ8ó C,ŠB”ù©K/{Ñ’m$SÊ6 SÆ€}¤2!%«%ÍñÐ�BÞ«•G®´–°”—³~‚˜˜ZP�KH¸¥2*ê00O爘Â…çHßÔQ�Í–ŒPü+¶eFý#}RD KF5–ŽÜ7ïYÔæ˜å<"—\CÈÈ£�nw©V¼Ü Ï;)׆®�„´.ãv— ÉJ;oQ�>r�‘”�“zÇli^3Õ+ñ-½gsj~WÌâ2Q¨ekÄÓŒÈÛÑNœìºý]«qèÝé˜;`�7±ãSüÜ›bQ™HæŒYa+ƒ¾Œí¡y•5ZNÜLÌJ%äh*•ìa4!ì9U»~0z¨·° �!ŠAÈà¸3,W5ÈM]°É‚ªáNßBÁ¾�LŒÚSàÎYÆ7ެdæô²2�ó'x:<­ã§"Êë4 ÒLöDjãˆ5\'&ãÙØ39Ô… þÌ EÆ`Æ`°ähggû²– –� úÂå ºp˜Þ›ìbZ’5Cþ¬aÕð$%™d)î­+õ1X¹zä÷¼‚Åt%ä&0oADwCi~ôLîÆkm± ÊMtÃ`v®®¯ ™òHn»wJZU¸,Væ²ÓìšßoETÉÜî¬?Øãƒì—íÎ;~ãä8Iý¨¼œ›—[Ç�Á˹«Vwõ5ºe™8n 渹Æý¸éyám¿?€/ãÍù=sÌf4Ã}Ó•6½s"œ3DT€âÄÕSç¸ÐMÊhʧì}ԉߛï°‚ØÐœ9]~tÝM%�õÞT�tm‹C-£P'Möréoögç¢öí@ûbÍ(Âÿ§�Á-ó"²—�ßÞA»¸nÆ5É2 †/Ÿ¡Ø�žÏLA½Þ—¨K˰ðÏuqÝÕ¼~4z�‚ ‚5ý¨T^i‡ËxPÇï“XÍ«B½è;‡àÁììÁò«‚ÿÃ�Ünbò0xŸ—‘ËÀ¾•¸U¨ót?žÛ¯¢^L \â•jT3\vƒ—)�—9ÝØÞR·T{˺…Ãâ\ <洌٠4úÉ)cúU‚âPò/máÙ‡ ƒB¬--ð+uizŸ�)ÍÌÁMyÖ/-úÊ´ èU=ípò4ºŽV[ó <&€‚¶¤8®Lq�@[£üêÞþƒe(÷€GlÁÛ»»AWÀGMs]uŽ#¿eã{¶ížÈÛó‹Ëw«³�G"v5:?3ªpDÖ1Øùbà$°?Èyè85ET£“´Ü0sâ÷Gàà Fq5˜Ï9ø†æ[À‹Ö"6Ïy̆êõQ—lƒfÍ”uÃ9ì tñíÈYÎ3!’×·Åæ÷˜ûQ3˜Í2¢LyÂÌ5×ʦà½_Èô7È"ö˜ÔSÍgž½pû”s½PÆ©r�öÚ|NöO˜?#í÷ÄKà”"µAß öt{©É‡ƒïê~Eø A²NDáGî9eÆ/¬dÒ|ÍèH•Ò|�ŸJŒFÏrF(�‚s�®4L0¿‡$lËiÛp½nÒië}?jGzöÊ`QÖû>æÝ•ÿÇ»·+DƒæÏÀý úé!Ÿb óó– ‚+¬8üxœí[m�ܶþ®_¡�Úön} ¸éz×nŠiâF‹"È®Ä]±–D™¤îÅ¿¾Ï�”Di¥µt·v €í]iÉ™á¼ñ™!½ùÓÛŸß¼ÿÏ»¿Ç™)òm´i>8KñQpÃâ$cJsóê¢6ûË—k¼7Âä|[)¹»úQ„6"y‹•ØÕFÈ2þ'7™LõfíF›µ'¸“é=‘¿ÞjÃŒ�¨¿‹gÂŒhÃâ’üÕÅäø«$Ý_l7kF<žO Hã@ó9†Uq’3­_]d/â½ÆäD¦'–ö]¼¹aj[mÖô¿Š7»­%¶ÛÆOÝO©ûmåFÞ¹§o7kKx³®Ž˜âÏ%ÍcŽ'KøI]U\=ñ¢ DÚÆodQÕ†Ç&ãqRu Ýð8 ²¯ËÄ~yúæí÷ß^Enr*nšúòÅ…£ç˜D†¼³'NmųXîékd½…íD.Ì}_6¹û/OŒ'×Qç7,¯™áiÌŒ%FÏ +ʨǹYÌIÉÿœñ<Õ_O«4VÜÔªt²ÓBr^ðÒ` Ñ`9�ZMÔ_ÎèjְͬÐscGLÏR?¾ ýXœtäj4Šú.+ÊŽD5ÃuÅ"ßíÄ“Î[�rÞ�5+�ÈyüÔúo#§w’å�Î�ðíž\Ë�ᣚç ÕY\Aõ\�ˆyÂglo¸‚ž•Õ³båÁê—Mjt¶õ;É Ö„H}sOÈò€ÜµØlgå,»ÑÀ³® ç¨.³Í9“©:Y,Ñ9¶j¸‚/ú2ÆIÅlóÐÐ/a Gw©‰hÖ0’%;ÏL�_ÃP%?äò�‹³lÕŽ>‹¹JÐú14Y@~™Õ0Ñm‚n:ÏE&ez&+öÅleœeÌF°èX²å{àbãVL±"³LëÇ>ªR é�5[ŠŸ³å£œh.O¯ËŸ ²Þ‹ˆ˜áb‰,÷"åeâ7=€È*Õ´ ­,CõlW ãW2ËËryËUÌÊ4¶=Šw².S¦~÷žö—ߌ-Á®€³$ë¤}äþ½pA ÌÒ®9rò�ZäV˜,fi*HR¬OVôEÇºâ‰Ø €Ç©“�˜^zFŽCÅô¨5U0:αjšÁ¡à¶Ëù¥âº9ÙÓ¾ë ˆÝ0Ò_bîi'UÊUû¨ ;—}â•S÷£ð7ÃúR“½º¸~ùÍÅv ¹ÍÚdᘗϚ1�2Ý µqß\£Â’Nû¤]Á÷:¯2ö¤ “†-ý×±NXΔã„ÅÉÛÜð¢uwf²‹íÓg«kX“ÛÈä^”;C‹C û$ŒÌ™óî\s"ò®âø=¹@Ê÷¬Î�gþìêÙ ŒVÎ")bC"ÿOxþ•[iâ”Þ5añ-Pãˆ%>7ˆ HŽ?,NP@ß»·åðp�j¡ BJí.¿Üfብx÷w|häo÷�>š‚½Í†ƒœÑˆÉŒAÞ©$*lmݳ݋‚°ðÚâN,p_#Ëü âe?wШ½0kXgÊ C¹'¡u+P< ñ¢B²šl÷˯žÇЦ»&O—ï‘3;Õ™÷½íjy7­¿+Mæè®ðnIýßIW=¨wõî+ö®æ›8—fž�1ðÁÖ´Ó¡�G^iå¹YÏs[‘š.ᑟ¾Ã�øÛ»Á×$fS@ˆ~ÓC6û1>ûA0×1¸&ŽØÒò§a¨Â§*—¢3†dÇ‘Ü|.ÛÝ{ùñL|Ú|ÕD ò–®ØDQI� ò†`\g,È£ z#>GkD+KÐTƒˆt—¢/YE¬ÿ•æA%òö÷÷_Іd€oAŽçI#?é»ûQêuxPø7öÚht³mwÚio,VCWŒœ/¶²%£²…mé#ÑÞ,íÍd XX @cN£®¢Va�jžœÊ6‚jMºgá91�l=Õ{íÃz²¡@ ’º‚Ì qÃDn={6ò™‰K‘Ÿ`žÏ¹[ŽtNêñ <¼¿‘,°ð*rzh·oÕ¬™Ñò,—Ñ4ÉH�¹(�ÓZÀ XoúéyEé/pnŸ 4Û‚î­Qm^YÅ”$@¾“ß[Tu°¾…M²ÐuTVm­xÌ;²S(�‹²–µîOòÞyl*65%jAMký�DÞš_h ‘ž6'òùáXaÇÑ�!l*·[U—0F[$îöl¯±&]\  �>rÉáI…bCqFè0‹ßú׎­ØÃ«¡n‰WÄ“ßA!3«„ p[ÙI´'‹¬[r{ÒF!Ûl‰Øi´¯—CC%÷s›Ÿ~ìCâ/Ãfb€©`ro~õتa4yBXÖÅY°å‘©=.¯*gm öS÷öw-?}hn2Aÿ`Ú?S�2Cÿƶ¦…›®f0ŠÎ8ƒ&~›Cmçä…ÎL�z‰ç&‡(H¤ul`»Ÿ8DñU¸ECQo~ Íï:o´ßw­,O içu?PaoßH-ܦ‰© kJ§þbÆ\�éÜ~QkßvÄ]C¼k†¿o†Óø®É|ýì:æk Kx:\†æ9TAp°]�…w¢Ð ù°CF¨ŠH#n[ÐêV–Ñóë†dÛé"X݃E­ fw1/Ïë7«h:‡¸÷²<ºp%ûÕÉ \€†T4;¤ÙtnÃ+#:ý +�Îõ$$à-… oÒÍNh¦ÊôXMW,ÁÛï.ms>µW!HÛðizW!×B*Ú5èVÁQß ì¸»æ&í¦æVM}FÓØPcþ:“SR9\äs^¾ÑÆJjô*ÛXìÈ4-ÇàZ†[HÔ-@ûÄfõR ;[p‚¤Ö]Üð çöŒ}RhÊáO<&Ô&�•À0î,'Á: !KtÙ¥7m]òISosîx– o�`�jÖ¤qQ¾Æ-`£P„#̲\3ø,æ3¡ù:ÊSÚ‚€Þ[™NØù½§wsv‚6R>BÂ.)�0mÈvåOGÅÈ©U¯�Þ¯|QÓ¦ÕÛå�+Ù„àÀUdlšÊ(\H4Ü £¥��ðËXfM]s®ýÿ>\»ÿäø?†(yj · ·ŠV‚-•0ŸxœÍYmkë6þî_¡åÃ>5 -\]·+¶»ÂÊà~”åãXÖ¦¾±)@[îºù©´ô3‡d2â1‡èÆZ„¦­¨xvŒ …ƒšò”¼^¢½èg{²³³ÌË]+ÅjõU±Él¸U�6ÜCÂq�nÍF–Ò:’=ðëöa�Xݹê®å'(&•�ÛÞŠd«êy1Mõ´è©\YtTyaž0cf·ËJ`éfÙèÌñ9)4F²d†1¸ÓílÞÛÜ_0S(è—ÖX¦8.Ùo˜Õi¤¼¾Â¾ÎOùÎC9oÝÇtfzÂ�ÚGè• ‹joY` ôi å«Ô’Õ%�¸ˆ¸�í-ÐÊJeÍs�&³rœl(æ+§ý~±šã«+°B¶Ìx¯lm­¡êV_/ëˤ±LvÆ›jL‹Ô¸ë’�娻ÿc+øg2&¶; ØS÷ƸóF “¥�4*+@°ãÙA ºQšRcf$Hã¾$HKÐ:ÿ7þ_C›òÿPÿºïj§‹k‚,—ZRxœÍYmoÛ6þ®_Á©_Z ±°†B0$Ë`í$ë°O-QITHÊiþ}’zudËí6×€-Q$ïøÜñx/RøÃåwßüJr]‘v7FÜJ¦)‰s*Óç~£Ó³Ÿýýšë‚EµëÕï"+DÆ•æñ%®’¯ÍEnŽ-¯µHž ç#¥©¶êÙÏ“½�’Š–ìÜß7Õ�€î?- Â/¬I\P¥Îýü-I~gxÔà‹„Mà…ë~k,l©Â 6œ#‚©gÝ\+]ó‚ë'’Œ‰XÿÃb½òYÂ7¤R�½õ§_ˆ´��Òò#±¨F)©©„Þ4“ê5¡)1¯ S±äµ!Eo•E˺`$¡ØéTHBI±Co"A8ïr6%šHÊx—æU#µ[%:§º…©H%ª³Še؉ #@EI6Trº.˜"�¹PvMôè¼ð¢� ,l¯E±—�km°$,åKÈú 3cjWî5EB¬•M˜»ÓOL æmOQ<+©›5U„d„â¯Ø†IZ�GúT‚Ä’Q�¡ÃwÔÛ2‘ÆË‚GäŠkð™jN¸ýk¯2«·Lʵ™×ÙHÚTq·uàd¹]tÀ& ¡U’òÏÐÓ ø† tß/⵫”ôž--ó'¶Ëà2ö©ekXÚ²*ÈËÉ~¼öž)ÿU·èXÇ»mqN.¯ìùÞÏmõ¶DÖ[Ĭr‘ Ó ›âµ‚}når ŬRBNºRÉ&ž\õ\øô™Ux¦Û ´¦Á+2^rd*p Ãu2kJVÁ}P52#ˆQ€o˜ÛÛ´À±œgyñD$‹E v ´ (�Úa–VÓs–4¬4>C .ÎŽÃÆsŒ´6g«71=üÌ…ÃcF`�äd+GÇúª‘˜-ÁóKç2èZ@az¯ç‹iEÖ Î©�„Æ·!”å’¥ˆ;Z×ê]°jõÈïy ©éJÈ,0OÁ8‚|sô£o$tÁ±“'Ä€¨²è–A…sAÛå‘äØj?ý%i]#”¬L‚ð¢ÝAØJ =�¸Ûe´ß{ɯ:+pôÆã$õ£¶ñõÔ¼Ú8j4¾žºîÖ®¿emY%Ž�%j®4³�Úö‡Óøu´¿gŽØ´¨oûh``N‡c`Z#‹ �½¸ÔëQÞ¸�6ÍÊßD=û}.ñaš3·œÝôf°{c’NûÍ]F¡NÚdí›x±×A—�oÃR;³þïN4¼JŸxàÄ»àᦀöÿã³ÉïâÒs”·&cùþ0Çcüh»güÍ.ï^Y‚ãÁÿД7}íG“Ç9à˜°FŒ˜äÝÇÄÛ£3šàéó,b3p€/‡’d{Ô;~4|"|´§k„þc{Üf€��z;¹>&ø;‰àD�Ö‡öäa)Àë1�^«Cä=ÌA½*hfËXÝÍÚ™/~C¹¶ˆ­b62–¡sN¤a” ÷”üs—×ÆÃ° ÆŽk?=ükue*ª‘@mÏÞ ÚªÂŽ ýÚT;—(‹`O]sö¬º¢8b›R¡Ì:Ĥ‚.]98 ruG—½wOÿMþ3f½_56±n“êç–XBYm!ß”MáÞÎLÂtW±‘——W¯VÿrWÇÍ-p~räü0·C$àGáAB‚¹,ÎËCCaí…}7–Ùlo·YO„ 2’g\—„(ÛTú4P£Š¶¸Í}¹™u*Ø+–ÙBöÞ�úæ’ý{U7�<"9‰lHˆ¹µ�EO$ª”'ÌÄÇ.;QÖ_ï|Q4�#JeÜS½ì�v‚ÜÒÍQ½R]”ýæ:[®¡�Õtï°�‡VŠÔ†¶±£ÄL�ç™~¯\÷ädÄq>üÈÝç„ù�ULš×¢í7*¥ùä�J´÷ÚÓ±EQ".K@iBeB¶á´«ïNÁ1u%…u­=Å; Ȩü—%¥÷ÆáªîóÛ ý°¸OÇ_ä›ÿ« öö–‚.¬9¥xœí[Y�¹~ï_Á °±ÐŒ< 8yÇ› l? ‚Å>PÝÔ4ã¾L²5#ÿú|Å£/uË­c/À¶Ô-²ªX¿*Ò«ßýø�wþýþÏ,5yv­Â‡à >ra8‹S®´0o®j³¹~}µÄ{#M&î*U®o~.ï³ò^j#ãñ¯’ëÚȲ`&-½Zº±Ñjéi®ËdGnï´áÆNÔ?°y´0)ZqVð\¼¹:4å&N6Ww«%'N/JJCAù%FV,θÖo®ÒWl£1?.“ÃËü�­¶\ÝU«%}°7lµ¾³ôÖwì¹û)q¿-ÜÈG÷ôýjii¯–Õ_ü¹Æ£yJþx²´ŸÕU%Ô3/Í@ª;ö®Ì«ÚfRÁâ:¯3l+XÒ5Φ.bûåù»úþ&r“¹ ‹Êõõ«+GÏ1ˆ ygéŠÅNmÅ3Vnèkd�‡¯e&Í®/[¹þ�ˆ�'×R[žÕ܈„qc‰Ñ3èÊ"êq‹9(ùïS‘e²úãa•2%L­ ';-$¹( – –sÖj¢þrFW³„m憑<"ŽäT �àÐ�]‡–=º�¨¾ïÊb+�Ãfø°<ʉ[1å¤W‡¼øSÍ #3Áž[GrzO YNôŠè€“÷ä:É+>©ÙNñI]Ê'TÏ'ˆîžK|Á ŒPP¸² W¼¸·Šæ“ª�í­dkB¤¾Ý'd9!›�b¿\ðb®iì¥,˜w-ègBšs!›µ²X¢sŒ¸Â1z2+%ò;Ñè'²”#}¬­hÖXË’�g¯ ÁW²X!è}&?Î5Z3áRv+ðÛÏ]Ûu8g>Lt¥›.2™–er!söÅldœeÕ X´/Ùñûä)V®¸ây,çÚØ?·Ä ­Y¢_2ê¹5—­×ëß¡¾Þ‹è˜ánqYld"ŠØï�+·TÓö´² ճݮ³¿’Y—•B1^$ÌBûˆ­ËºH¸’øÝ{Ý^}7¶»Áã´•öÌmþÈa–fÍÑ´A¤IOI‚byeE_4Ó•ˆåFk®‘\�€ÄóÚóq *.¡Fu_Så£Y†EÓ ýŽ˜Ððu&®•Ð8 ÞÉœö]osÃH}ˆ¹§u©¡šG�Û¹Ô诔̹Ú!ŒÂßëKLúæêöõwWwÅ­–&íŽyý"ŒéèÒ Z÷Íõ;,é¤OÚŠo³*åÏš¨0Iw ¥ÿ–é˜g\9ìNÐ�LãàâJä�·s“^Ý=±¸…1E~yƒìdqoghy_À>1'sfb+œgNØù†±䡉Øð:3žù‹›¯0Z9‹$�[Á„ã߸å�&ià}ˆŠ/h�ZP<¦èÙ"4 9þp£ð†?)¾³eôp�j¡�AÊò.½<¤щ¥xï�FùÓ.è#úM2¤Œ–@4ÈeòN尮–Ö=›m©õžR[PŠnjDc‘í° QôS�ÚHC±†u&ÜpT‡%¢$jM $‘WHBV“aq¿üêy,hºk^ˆä¤ír~¡º\a×Û¶ŽïÈõw§É\Ý6ÞÓ7h¥«Nj~½ÿŠÍ¯£l�•f¶±1öP³Z G! sisÎuä´çÈ�T¡ë¸ç¶ï1äD÷^1ÇS‰Ù>¢ßô��À>�Ï~LÌõÓÜa쯛AC^5\møæRwÊQ£¬’žÏqë�—ÏħÉc!p�Ït 'óªÔ˜Pn ÝE,Åàòù5€:ⳇ»F´r È HIw`TÔà(‹¯ˆõÿ!Ô<EÞþaW‰cñÀ `/ÁñïÈN¡<.‹º¬u’÷Î}Sñ©)Qƒqë3T:%òÖü‰ô°9��È×Ç ;ކ°Í ¨ÜnU+`Œ¦ ^t’¸Û³½ÆB8º¸@uPúÈU$‡cT*!Jp{@]âÁ¿vlå^ u—xE<Å#2³z ·• ‘DsRÉÛ%§°'me“-;Aûú$¤¨ÊÍ�R?ü ¼øË°ñ€1UTîͯd^“'ŽE�_ j>‰XÍQ4xUoÒ±˜¸·ßúèC Ó!ó[’õ·gƒË.3¤ñolbZ¾é*£èä´Óÿor«í´\¢š©T/ñÜ\u¬G£{g"€ç/¾X·()êÍïHEóÛNံõå „ö_ûÕÿöM©¥ÛL15桤ê/f¼!Lçö‘Z‡^ò �îúçmïüõxïœÆ·=éÛ·L|ª6<ÉpZdPÁÄf=ö9èB#,ÄΡZ"�¸mN«[XF/oɦ3Fp»EòZÌn¡c4^"^ÖoÑt&qïËbï¶@WÙ¯Zá”|OÅ´C ¡ÓÛ½ˆ0¢Ó¯°Òé¤OBöRØÀ³Ôµ{L¾I™$ÿ#MuS\×dÿ;´½ ËH»u#$kÃÏ¤ÜÆaÔ�|l{ÇÇ1¾K4bíAWÒa7¿"À•|Ü#ÂÒ2£´¸ {<§Ê‡E·Ên%r¨¼—Aµ[ZÎe.?»þp×FÆ«tÒoä�PÊÍÞ ‘dÞ:ýtt÷Qˆ ÕT[>nnG{?ùœ‡í÷:쿽-nÑ…û­«Bs·ÓEƒ=ÀÍËëFŸM·Oëå¨ iiyŸs?²³[è¡h󸋒‰Tå ÛÑ=Ƽ�š5‡4T÷wM¿�4Öq’݆:'¸£g?VÍfáÌ^çñÜi˸¡Uçò>5®ÄNQ®5¡Vê6·•µÑàø"Ž“ý8nÔè]ç¤ Ô’2Ÿ[b¹Ñç^Sè]Ò $Ÿ¢E?Ÿ™O"qý-p÷/â2k^LÀÜ‹^˜<  KÄú³Ï°{`û/¢ ÞÏ wP�¾Ï¸rsÂ5JÏhbßi󻇴´Fì�F¯wM^§°©(æY&¿�Qo2£Ss¤÷Ðl (èq(Š¦Ûœ`í·œ¦“ŸÈœz±�£é…FW—i^*ÚJèºaTß9l™»N(m±æ¡ìœpšÆ?‚÷×£œ.�CÈå"�¢³:÷]9^PWXÙ.dK&ô';w;ÜB¢vÚg:«–éÚ"d¹öö‡×87õ›.…P)>ýˆQ›dnFÃÐK'ën(YºÇݦӆÓ�¡ v+¹sˋܤkƒT³n3�‹ò•n…*©d® ÃøKÙÑtíØŸÊÚ'ôÞØvÂà<½Kص4Hy†„mš:`ã.Û…?\•#‡^½þ{¿@¢&¡SÜf†NÙáðWmBÕ[HÛ*pcöV6v>¼w¨9fÓ§ÁáZË<¥øj&ò\mÞŸLÛÜJŒéŒ¥…ŒÀ¹E9È ÜG,ŧ=¹vÝþ¤süKür¤u'²åú[ Ù,¡"ÎêDxàíuï7þÖ  VscC/–ÛnXZºÇ¥W �önÛe’j+>f%Õ®ç§Ò¥ÿ?’K÷¿1ÿ îlÀ³ Ç -ÇŠb‚0•HhxœÍYmkë6þî_¡åÃ>5 -\]·+¶»ÂÊà~”åãDÖ˜ËÒê>5‡ÕîþGÃQÉÏwÛ ¤; IUŠž­ÚnirÂzG€2ü—€-D„Ia(VeRÑvºV^ ³'ÐLñÌÞœ‰\Cò:øµ ôQl<þaI‰>He°ïµ¼›k°®¹Î:ÒBè:h–º€ŒÌ´�ä=0±VbIde'«‘rI×?Úý«æÓxêUà 2EìaT Ï~±8•94 œ¡朠qj˜Ï�7• F§ç{½~Q^4Õö)Q?u­Pa˜æáÎá~��–júöHŒËܤ9ÒFŸ¹Ò¦[´fOHâPæv>ßD>�†çYÖXüN­¯g¿–®ã”{ÑÑ¢d5¦æR¦ä²ó £ù\zÑ(ˆ¦‘©!°(›ZzûtzNè¾À³\uyp§Q8ŽhíóOƒL§†¡ÃVøÚtJÈ¥Çäªè~–G«ÄŽA%Õún\ÝR¬£QeµO ÁœÄ¥ 9~5%Êý"Ï,Å÷ºwç‰å°èµ1r¢ˆ:7ÖSìV5•¾ uÎâäç’ØJ¸óoÍmêK~Úp×ÍI…� ›3$•�9D7Ö¢ 4iDųc-È�ï äµRìE?Û}ÍÌ//u�䪔�TÂ.ÿ±½–u±ß^OŽ£rc²€Ö‘æ�noV©€PU}ªêQ~RbR)Йí¤HÃ¥ÊÓD�‹^Ê’‡ñ‘åæ 3ebs,—/Ûk­£7Þ'¹Æœ0ÆBv¹yM{{�ësfrÝrËÇ"û 39‰ôƒ×—Û÷ò ßyð"ç­»|@gÆgø¨¹…NÙð²È¡ö†B_¶P¼-ø\P‡‹ˆÛùОð¬¬TÖ<± ¾3U“Ë9VÌóÝ5ÅWWT…l˜ñ^ÙzZAÕ®¸^Ö—Fc9쌧Иæ‰q !ÍPw¶íÇ$VðÏhLlG°§îpë ¦IiT–ƒ`çÙA ÚQRmf HÃþH�6– Uæoü¯<›â¥AÔS}‹O‚/—"¼xœÍYmoÛ6þ®_Á©_Z ±°†B0$ó`í$ë°O-Q6ITHÊiþ}’zudËÉ6%@bQäÝñ¹Þíð‡ó?ήÿ¾ü•ltžE^Ø<MðÈ™¦$ÞP©˜>õ+�žüì˜×\g,*¥X-~ëBÈœfç\iÉW•æ¢Gá…A-i%’#÷ÇHiªAÊcõ‰’R/¤¤ 9;õ÷úQP#ù§ƒx°ì…%‰3ªÔ©¿ùHR…¿¼jHˆEÂÀÂÕAi+ljy 4r#ÒZ�èŠg\?�¤ÇEÄêë…çx¾mÐäêä£ïÄüB–C›[‚Z‰E¡°ªˆHII%Œ¥™Tï %9È2�$LÅ’—†³EBÍËŒ‘„µ©� Ì&±7 �ðzÃz ¹‚0Ò¼¨D¥ö[â~#óŒ*¹Þä†Ó Ìz”,Yr¡ÍbÂR^°„¬HÎhÑiê‘pKe”Wa`žNA�O*pm9µ¶ôŽ\ñuNkØM;•{dK³Š©¡Ò’ŠŶLBñ{ú ˆ$–Œj,ë7o' *sÚ2‘%×�20Ä[/UŠk‹Òm’rmèšHH«"nœIVÚYkhw(HRþ –ì,â”í6ñê]rz˦¶ùS1‹ËD¡–U¬OS† o.{ï=òÉ»f˾}÷ÄܘJ�Gbcw«1elˆhz#’IuðF­Ò·¡F1+”�õœ›J%»L{0Õc½ÓÑp°î±1Á Òß³#8ß Ëer]å¬@v ª?Æ”�ßFä®g”6îÝéÛðõS²Xä—ÀÖÀR©=ñhí<BÝNý£3‘/‘Ë8‚ U¤9ª6kzƒ˜ÃŸY¡ÈgÌ( v– Ù;ÍËJ‚ZBnjÜi7£+ƒéýÉ-FÚY1dÌ êQ S²‘,E1ѺTŸ‚€‹{~ËK¨LB®ó hNœ¼›dP¶žÅæj]£HˆQ¬£+Ý3%P·‚zÊ#À~¸Þü%iY¢D,L¥S»Îï|HšG27îõ{Ž>ȾlÜïø�¥ã$õ£zðtn^l7Oç.›½Ëçì-‹Äqc0ÅÍ5jâºã…µýî>�7ã·Ì1›Ñ÷UÛÓṯp̨Qz×I�¡ˆ›¼Q÷M›Q+~&¼Á‚Csæ6ó£ËvÂìaLïHW¶3Ô2 uR÷^Ïÿæ@NÎ+ßæ}Úõif+ü6íI¯Þ×Õ‘€õÿb³ÕÇoŠÐc„W�Û—ÅÛù‚ÚâG»3cZ\îËê…e˜ú—*¿l{b?¼Ž�Á uaÐJÏ…µEf, °Ã÷Q´fá…Ážw·‹äÞìðÞò ÃÿjORù×úh�€îñn÷<ðk‰:D�µ»ñÜnm€ç\ /T½9C�콌Á\ftmo¡º¡ÚÛÎg�5§EÌzÒMީӭô•’kzÖ.`ª1Öæ‹™ú…Zš{RO™zæ cvîV³Á¾07˜s\uCÍpô\º20:´¶[ÂÕé˜0 š^äÈÇÝ$šîæ³{û/Z›¾àCF±ÝrÝ)?޽fªïãU^e8ý[6,¾Íý‹¼=;_¾[ü+_‹™[ÐüÔhâ`Ç ç3¿“@'§ÁãtÜUÑ�1"i±fæðïÄyà›¯ËÐ ãsJ�¼îŒ_1.³yN£6T¯wÁÖhÒq'ó£v8…„.Î9ËøFˆä5hc~Ìý¨Lf�»lÂLåkú e3òÞ¯wº’2“F& •ÓYh/à.ý\Ζ}ÊLà®n>G/Y˜?)ÍWÍó •"µÁ_ö\ S“{¡ÞÕýñ*TAòNDîGî9¦Èo¬`Ò|yéH•Òü�JŒÆÐœj(�^S?ò{É+H@Í…À�šÑ�k{q¸èûýSÆnïÇ›¸íRƒúGÔÀýHû0ëÃ' “ó“ˆ\‚2‘<QxœÍXï�›6þÎ_áÑ/­Ô ÚªJSÅ!M¹ÝT©ë¢íº©ŸNÞ¯€©mrºÿ~� Nàš�´Û¸“Àöë÷}Þ߆ø»«ß–7W?³ÂTeÄþF<Ç­"ÃYVp¥É\†­Y_üF˜7””4J¦‹wrÓÖb-Uu%´Q"m��uu$Aõ¬R™ß[Æß'ÚpR‘é7l’hƒ˜³šWtNP†IqËû‡iHXâ†e%×ú2,^³µÆïC™Ìi„-N§Ù¥ë6ÅQc' ”=©ÓŒ§¢æžåƒ}L¦SfA·+[¨Ò¯ÃŽÑO¬ƒ3)ßQôìX&k�UÍäš5\Ád†”f¼ÎXÒ’å¤3%‡ìgå€=RÀº)h´e¤—€4 Ⱥ•­>nSpXr‹ÙðÚ°”ÌQ Þ·$šÅ[®’wòŽTÙG§ˆ›üÐ4»I0s:p%LQ‰,`:ã%�+bM[R¼dwü^3#Y¦ˆ,�méà�çZ›'¥HØÒsiyHL7”‰µ |ï¶åeKšµZÔØ‚‚AÅ?‘åà»nëÌÛbœ¨šì–ÎrªÍ ¼x?{þК/ƒ/lù‹𿍝�)xíÎòŸ×,²¦tîzkl\bS”÷LQ&«Šê€ª…îÓ–ÚcN¥Ÿ~ÉLˆÒdÆ™Ô&áаƒ(ºnH¶XS¸e<•­q;�Z)ã5B©ÖÚ,DÌ£˜ŠÖ¨=Æ4úMQ½¸ŸDuøBªMdGÑï°+ð#”oóQ™û¦m]môªÄX�õ&ùƒààRK”¹¨Ÿ Øà0~nÿRÜ:M/lsxæ­ü°“¡žu•x‰J@µÑ}Ù-^%;æ±z‹5ˆ2‚:ia²ÚMX1àöÊöžºæbT›¼¯ÝßÂÿÙíÂÅoèØï˽‰¿[`¥¨„éæ"àù¿A¹ü }} Ê-Ì j8x�R&gA]ËÑÚm˜ üû¶Zí:f˜Œ†‡`ƒ …‰G�v>´;lÖª€;ÄkîÕþð1=˜=}°üè üé:Û{7qö0˜vþù ß(4FÞY|ÿ|ð~=P�t>˜ou/žò0 ½.ùÆ�]�§:Úé‡ D¾aœÛ†à°Bæ¾ýÚ�þ“4ä²óÕŒQÞ”x{=x0Ãü„mý‹ðLX‘[¹¬Â¤»Âû Õx·7. A‚÷}eßò× Ï“ÆŸWmÐàp9+ö£HÎUÎrÚ î?�ë¾ç†‰š8Kмå¨å¸œ28HPP²¯)à»EÔØŒº/§ÿ™ª¼ô– ‚1¬93xœí[m�ܶþ®_¡�Úön} ¸éú×nŠiâF‹"È®Ä]±–D™¤öîüëû _ô¶ÒZº[; PàŒ]iÉ™á¼ñ™!½ùÓÛŸß¼ÿÏ»¿Ç™)ò›h>8KñQpÃâ$cJsóê¢6»Ë—k¼7Âäü¦Rr{õ£Ü—R,+´Qb[!ËøŸÜd2Õ›µmÖžâV¦÷DÿúFf0E$ú»x%L‰6,.YÁ_]LO¸JÒÝÅÍf͈Ëó2Ò@P}ŽqUœäLëWÙ‹x§1;‘é©å}oLÝT›5}įâÍöÆRÛÞÄOÝO©ûmåFÞ¹§o7kKy³®Ž¸âï�æËqÇ“¥ü¤®*®žxY2ÝÄodQÕ†Ç&ãqRu3xœv�²«ËÄ~yúæí÷ß^Enr*aI…¾|qáè9¦‘!ï,Mʼn“F[qÀ,–;úY—a[‘ sß—MnÿËãɵÔù�å53<�™±ÄètEõ8‡Åœ”üÏÏsQýõ´JcÅM­J';-$ç/ – –ó¨ÕDý匮f ÛÌ 1;~ÄT-vå»®+‹“¾\�FRßkEyàÈX3¼W,rßVL1é¿Õ)ÿýX³ÒˆœÇO­ 9½�,ô‡è„{÷äz€?|T3Ýá£:�7¨ž7Õ#gøŒù WPµ²ªV¬Ü[³I¥Îv€V2ˆ5!Rßâ²< ƒ-·\ÁY9Ït4ò<¶+º¶sd—�æœÉZ­,–ès¾�}R1ßB4ö‹ØÈ^j%šõìdÉγT�à«Øªäû\îsñaž¹šáç±X b?v­Ö¡¿Ìp˜è6D7�ç"“2=“!ûb62βg,:–lù~¸Ü¾S¬HÄ<ëúÁ�+h~kІäçÌù8GšËÔëó'¨­÷â_*f¸Y"Ë�Hy™ø`ÂJ5mG+ ÇP=ÛÝ:‹ñ+™åi¹¼å*fe[ÐÅ[Y—)S¿{oûË‹oÆ–`WÀY’µÒ>r3_¸ fiÖMäV˜,fi*HP,OVôEÇºâ‰Ø `É-Ò©�x^z>ŽAÅÔ¨ö5Õ4:αhšÁ¡ß¶Íù¥âºÄ9™Ó¾ë ˆÝ0R_bîi+UÊUó¨ ;—ú }â•S÷£ð/ÃúR“½º¸~ùÍÅÍ@q›µÉºc^> c:ºtƒÖÆ}sý K:í“v%àë¼ÊØ“&*LÚh鿎uÂr¦6'hN¦q pËÆÛ™É.nž>[]ؼ¸‰¼AîE¹·3´Ø—°OÂÈœ9?pç™v¾Šã÷ä¡)ß±:7žù³«g/0Z9‹¤ ‰-`Âñ¯ÜrH§4ð.DÅg´@ %–Pô�,NPRß»·òp�j¡�AÊî.½Üfщ¥xï�FùÛ}ÐG(á›d8H-�h�Ëä�Êa]…­­{6ÛQ'ê<…¶ ÜÕˆÆ2¿Ç‚xÙO4j' ÅÖ™2ÃPýI(DÝ Ô’ÀD¼¨�„¬&Ãâ~ùÕóXÑt×–àé¶É¹½�ê\­�ûÞ†µ¼ÇÖß—&³tÛx·¤#ÐJW=¨¡õî+6´X9—f¦™1òá0µóažÇQ^lç¾YÏ}™BÿðÈYßaÈ (î}a޳)§mï½üx&>MÞ á‚ü¥kà6QTRc‚<š‹â ƒåù4€8âs„³F´²Td¤;°)jp“ÅSÄúÿ�id"o_ñ¥xaˆ@0„àxž4òñ“¾»åÿQ‡…cÏ�F7ÝfÇ�öÆa5tÅÈùb#[2*[·[}$Ú›e¢½™  „xÌiÔUÔ(¬UÍ“SyÂFP­I÷¬{‚AÌ#[Võ^û°žì-8…¤.„ 3H˜È­gÏF@3ñ)òsìó9wËåުǃuððþF²ЫÈé¡ÙÃUX3£åY.£i’‘:sQ:§µÀðÞüÓóŠÒ_ǹ}&àlRf¸·F5yeS’yøN~oÑmÔÂ[øv4ÉBØQYµµâ1ïÈN¡<.ÊZÖº?É{籩ØÔ”¨A6�õcA[)�é삉ô´9�wÈ×Ç ;ކ°Í¨ÜnUQ\ÂMÙ»ê$q·g{�…ptq�j@úÈU$‡c$ŠÅA<`-~ë_;¶b¯†º%^O~…̬€ÂmeC$Ñœ9²vÉìI…l²%b'h_?*¹›Ý õƒŒ¶¦ÚɽùÕëÀiòì°¬‹óÌ/"Ts˜ ^UΚ4ì¦îíï{ô¡¹é�ùãHúGÓÿ¹ •²ø7¶Í0-ÝtUƒQtþÙéë7¹ÔvRÎQðÌT©—xn†ˆ: գϣ³lûç*¾$·¨(êÍïHEóÛNíûmkËí½öªòí©…Û<15a¡„ê/f¼áLçö�Z‡^ñ AîúãmoüåxoœÆ·=çëg×1ÿX#dXÂÓá24Ï¡ ‚…Íz,ÌsP…FXè‡�2BuDáÚ R1´7£ç×�dÓù"x݃E­ f·P1/ Ïë7«h:‹¸÷²<º'�%ûUÊ \€Š÷T<;Ä:¹Ýë#:ý +�N÷$$`.… ½Y½ÈÑ£ µµ÷9¼Á¹9zßu)„ºøÓC�µIç¥1 <ϱîF‘¥ºìFœ6Œnÿ¤¬ÁœGžå6\+¤šu3i\”¯rWØ(ÔãÈ!óŒFŸÇ‚¦kÁ–ôT¢¶€ ÷Æ–©¦~ïé�â­ AÊGHئ¦Öí²]ù#S1r”Õëª÷Ëà jú¿m6hp“M hå€É&”I½…´ 7æhec§¾GG5‘c6}Æ.§ÌSНY"ÏeÕvÖýy³Í§Ä˜NNZlè�š[”ÃzŒ |gø´çÑ®‡Ÿvu‰_�TîD¶ÜCkC6K¨Lò:åa{Ý»öŒ¿Jµ84Áh^TbàyòÐ HKuYJÅaï¦�'‘¶âàcV"í ðøô¹öÿgqíþoäÿŸV‘Œ ~ ©~‰‚5’:ÃxœÍXßoÛ6~×_Á©/-�XØŠC¡’eèCÛËVô)8Ig‹‹$*$åÀÿý>R’-þÕnóØ’IÞ¿ïx¼#ÿpóùúþëݯ¢°U™ñðbÊñªØ’È Ò†íUØÚùåÏa„~+mÉI£U:ûØ–VÖª’TÞHcµL[+UG�LG½­Tå+gùÇÄX²•™y/Û€p“¨©â«ð�h˜Ä9ë?A� nDV’1WañNÌ >—hZØÈTÎxqzÄ^Љ½V5Îr"F¢ž¥²”v%ò‘žPé_œÙYÐiår9 ªÌå»°3ô‹èðàEz{"SµÁ¨j.Òp›em.¢‚\)r6™–�SEo� CUS²È =WZ�¨vã&ä€y_ðDgÂSXDO¦�d¿;lA¶‡hÐ`¡Z›©Š=X©sn�Ú (QéûI<^fdy¡ôJ˜•±\]ˆç‚5‚)+:QQ�Ã0žºs*KY/`ÙªNx°4âƒnð幬9éÊCZÂÁp�óìÚ–Bç5o¡Ç£Z¤Œxk±oÝ~Af,4ϑƬmÌû(âzö,e"4Sz¹V4Z•‡|’3¿O¯Ë´™ª^$¿3V®4 93ê»±�8އ‡/šš‰iæªÍ«Áóáh (¹{Õoû¥ôë&ŸCÙ½¾US×¹ÓÄkÄ*B>îjɵª-’ŽéëFñ6YÛ=ð?æ~¢Ð…~ßáæ�¹·®ŠRê+¤ÕIló¾ú|ׯ Ï&û.ÜÞ†´©`¾Ÿ»D6ÍsÝh¬ÿ=àqãvx˜l÷ì‚}·owÖ^á|ð?µÕݺø†É¤¹ 8RÖÓš}N¼ktγ.@&í�ˆ©¯Íÿ3à›Íqf{Ô» ühøPøÓW®ú®c'ðqPo×¾s‚¿×È£Ôy}ó{äͨ;j±Ösý`zŒ4?jì‚z[ÒÂæì µ·Þ�)DC?¹8`Õ ••ác×úwÊÂØôaÏd®df]ÁÜöĵªšÖöG½ç,à’§9u}¸ç¦b,jÇ"ÆX!�‹œ—’†SÑKˆù¡‡ÉðëÀƒ_ä%!·ãqÌéArɾ%¡¯QÿOjÔýWû7Ý’èñ�‚4š!axœÕZmoÛ6þ®_ÁØ’N¼(P´Ž�­Ý†Ý–Á†aØZ¤-n©’”÷×ïŽ/z±-UvœbšÊ¢�wϽ™Ì¿z÷ÛÛ»?o ™-òE2�N< n)I3ª ·7“Ê®®^Mf°n…Íù¢Ôjyý^­+)VJZ,++”$¿p›)fæ3OšÌg�åR±- x¾0–ZØ"RóšŒb{’9%’üf2°ã:e«Éb>£(çÅL¤¾/€°$iN�¹™d/ÉÊÀöT±A_“ù†êE9ŸáƒÜ�ùráØ-äÒÅüwSOùàßžÍgŽõ|VWðjŸP<¼9ÖUYr}Àì€Z�·ª(+ˉÍ8I«¢ÊÁWNXÛ1«J¦îÃåÛw?>»Nüf&6Q§Â\½œx~^èdÀ;ÊT$õhŒƒˆZáÇÄÅ ]Š\Øm›ZþÃSØ5Üù†æµœj3|¾B&ÉQ™Aäßd<ÏEùfؤDs[ié±£"9/¸´ B²£Î£´IºêôjÓZ&Ee,YòŸ©j™óˆZé°l„\×Ëo€µåk®§$Wk‘Ò|š*™+4µ\ª5ÝxpÐasåÏ 6F¦°ŸÃ¢/‰�Ϧ‡v6‰Át*fs7q„Üp(�#H•A LÑ›BåP }¨¨´"çäÒeQıœ’É@†upí†dù„ä=6"?è3¤î$²Ý‹ÇOD èÞÖÎۚʵó2íõëèl�¬HÝ ëÁrB?Áy§r¤÷�ôLî+Úîó|�óî9“Ã,Žé�Eé;… y*1q„“�øiÜä9ë(Üõ®rlÇ9+"ø<î*G7èòlýyÛvTyü°ÝõYoÑiúòí1m¹AWž4ØÞ~æÁö Ë\Ù±‘¤�8ÏAL9ŸJýs²žüÌÐYøý÷'Ƭ“C5¨x˜Ù˘[ (j! Ç$ ë+eø�ÙÃ¥Ág7ǦÈá7æWSòT¬deL Xš'D•øÉ�{a3—…^r¼ \‚ºT`z¬+< ’|HÝ%ÏÕýKZ Ùx¥¹)�9œ"Ъn­C@<¢fþm©4ãº~5…Û‹w1]æ¥Õ[#«á'˜Ín&Ï_}=Yì˜e>³Y›æÕ·‘¦e)O4³þ“¿ûq¬Y—µ?9c�ÝmK~QGŸemZ'â»ViÙ€‡¡.y7l¡.ùŠlà Œ®I<”C-¾èFÙ^í?g×Iä�äÐ>Ôí1pÇ9¯š`HuHÑTsû)ÕC�%þ£šXu'÷<¸”8xM/Ññr�b‚8)3…bæåBzâ>šˆÈ½OÉrÛ6´Ç½¢¹ÙïV×1>%°À,”oÁßã$ÂW´Ê�¥°D˜ýMŠÖa¬Æõ–}Ù‰Û’*8ÏÊJU¦» Äþ‘qÙ­(™ömIê듉ÅZ*È¡‘½…~'í°;%¡P;Öš–™HMtª‚&Ø?¯"Á±Ð°i« ø²,ƒÎ‡&4eâS#/Hé„€M)ŽÐsù}XöbÅŠ(p>S°„2ù¤£íÌU‰ºoïN>MOñeu·™Ôw`´Q9bÑRuæB ŒÖ7'Ì p¢dª9)xâGÞývNç‘ã ãe…Ž¥Õ½ù ©Êë…ž1ãœ×$øþrïæ£ÿ{oÖù‰K®!IˆçØ•†Ï#nÃN¸< ‚z¦¢&m�¬Š%”úèàÑ®wðq•ÚGΙŸP(ñ=<>òz‰×ÙÑŽ»P Þá€è‚‚ä‡fÖa¢À>8êšËo[h¥èñ¨*�Kèg�pdµŽ#ö^µf$Ïe# 42…22Ò‘üLN´m'6¼ûÊ59¸î¬¸_cõxû.ð;‡S å#6jÀÁm±SœEšá|7TÚ“�Ù1Beqrij‚»àÞ@AqÅÁ¡†©ºvšPK”Ó#ÐìivBàÁ¶‘1”g ·M;ÜÛãj*Ósý^¢��Q•¢ àñõaþÀaæÿ’â?ã�d †S‚3�*ÀxœÍ˜ßkÛ0ÇßýWÜò°§¥¦�Âè\ÃXWŒ®Ý �²¬Ä¶d$9!ûëw¶,ÿLݤu PšXºûê¤Ïé$'øpûëÛãÓÃwHL–†^à>‰ñ#c†MˆÒÌÜ, ³^~^øØn¸IY˜+]ü”›BðµTÙ-×Fñ¨0\ À¾œ)Ù|kí~­Éx_ŽqjC zqª¯áX5tó‚dìf1á„Í;¦aà“r°ÕT¸ÖµWhšM‰Ö7‹ä Öÿ–øhPˆÊxrÒ×.Î=QXKF@åøy)BÕ)ϸñlcÌ·nÌL/¯Öî+äRs÷ 4%)Q°%iÁ*„¦ø?.6`i+ r]6yضYÖABÜ�+À“,€„R¦u¥`'WÇ\}÷ÚÙº(–¦ZuÀ@ùFdL˜‹z>ÎàH*¿óüh*•í¼TjÉ1•ªc^*E+ù&*uÌç¤Ò}¸Gë# Ýæe5Vc{@1ñ”£Kw=«9¿²A[F�T sFùzïøUËjÁUÕéÐ8Èè/z#¿Ç„ë6~ü®°Ü-¥H÷¯r_d`¦ô‘4z>ó¢H�9 A„ ‰«•7v/,½}¸0lÃÜ6"8 ;ØjÞ$Ÿ¹¹4«P&ä±`úNó’j@SvœD&`YÓNL²W G‘/—�ϲ(KS J‘});ÜFvÜ$ÀÅ)++P) á¢Áå¶éoÚ&³a»eš*ž—�§Â븞 ao„1ÈN÷;ÂI@'RˆûÑ�@Ö›ퟲ€œ¼%­×™€:ñ,.Ž-‚oå‰Ek#u§Z «Úzvw®õϲ6¥¦7`sWÁd6©Ѿ¾›”¶sùÌ͈ˆ§®>øðŠßáe.ºy¤•·y`ìh(ò\¿s£vþ‡÷?>|øéño¬tuµÈæé!x�G-gyÉ�îþªu«Û·W3¬;é*±hŒ^Þ}ßVN*]K^½—Ö¹l�ÔŠ}/\© ;Ÿ…½Ù|i.u±!_/¬ãGdn¿cÓháP6çLñZÜ_;r—««Å|ƉÓ7G%¥­ ü v6,¯¸µ÷Wå·leq>×Åq5¿có57‹f>£»góåÂÓ[.Ø›ðU¾» ;?…·¯æ3O{>kvøâï¯î5ùãÍÓ¾n›F˜ë(Í–T ö ë¦u‚¹R°¼­Û [ V �³jUî?¼yxÿ÷¯î²p¸�ë¤Tmo¿½ ôÓ-‘!ï$[±f<|)+é6cÙôò¿"w‘\O]¬yÕr' Æ�'Fï +U6✔9*ùŸJQU²ywܤÌ×d'E*Q å B¶¥Îgi“�Õ9¨Í`™Õ­ul)¢ø…n—•ˆR'�¬TOƒEmW ïÄ“0Ìmñ.c•~’9¯ðMáSϾäÆð�ÅCÀ$¾(îûg<*ʤ%�k�ï ¿Þ/ˆÕ^5#,"�ŽðêÙSÚ+©8ü¦f yBª�‡rÅçÆ¬<´ÍÞ¤1O©ÖizB˜Ê3ãTž¨[®œ¬{ãc5ÉÁN²œ üìH~ó2ð â{ ÿ.�þ&Áý†ðž½øsPùÑLåGs)Lš&‰î$_!L‡ïpÃÕ“w4?èÚÉ0ì%ƒXDãî€,gŒsüW ®¦:�ö^ʃõЃ�ði.¤3òY/‹':Åi‰ûV:È^ÍK…<ÅO´û•<HŸê+:õ Þòd§ù+Ið…<ÖL/ÖÍåjõfè«æôþ~춃٧¯Ñ�§”è^ºæ¬ ýø…[éߛϣ•v“ñŒ½Ÿ3¨¹žÂK9梴#`~€)F ÿ"˜¼Ê,\Žbµ“*�i;‘ùˆ-Gògþ”`$f[ê§x£¯ì6¡,=ÇQ?5·R)`½/!Æ•ã#gÁG·ü\ʼd%Çd°ÙF:ŠÛå&Š�wâÓ�>¥ÀݶieÝh‹zM¡•±›õ“áuJÿÄggdÞc”0Ál#r¹’È(¼($éÏ«Œé†>Yö,]éY^Dñ6R‰vç’ÒÀSKöeD&•E¥Ÿ÷xÔqý-¢¼qLPä]¿6ÚÀÂ6òHbám©M!L÷jk–®¼ÆÄ˜‘› ðì€Hÿ, WÞ_}ýö�W‹-³Ìg®îyûç´g`©°iæÂ§pÅæIcÒán‚Àþ)óº‹W ÷z¤Ð5 †ŒܰA& d2Ÿ{¼Hµçz ÷�Z·ð ðïR¤rÑ¡¨ )#{øF£BXmC1 XìdË÷Ê6•wD{8M´‡ƒ�¢˜¨i|y®»¬3XošëcyÂGPKU_ yæ+Þh9†õÁ~�±0W !È k.+�l­ª ò„P{5&²Q黂Ý1¸!=çF¸—à–ª·Å?Ü�ˆ7’:ÑRìÐõ+&éÌI=ÏeošädÎJªZß=d Y öï7”þàŽ™�Wv÷&À™¶ï(I€<°Sm`Ò¿RÃ+V•é†Iß"ìb+�Ú+«õ^Üå�ù#”Ç¥jukLJ":w]Åɺ.®ó>“OJ#oMtè#z)去ù ùáØ âØäR…LîKUÆœ‘’{q3Hâ¡dG‹¥p q�¦OÇÈ5$G`¤MÆ`SNí,š.ñ—[¹ªan�%â)>Á #mg>3w�ÛvwÝ÷¡”m7ÝÍ.ïU.áO*ºË–¾½ "ÙsEŠºžÚ*†ÝŸû«ÆhêI$_£{œÎ,v F?Ûß÷¸�ëª[8Ðg^t="àÏþÝþÞÿ³ÓíþC(a*,ÐH#Ò #Д;Ø3îë"£· }ðÄ}ª­—HøI ½÷û¯ €Ï‡("•(BoÈY˜ñøEt�`š,keT±+‰¥[Cp®9ê›ÌBÖÔ&@Œ.q¥<äY#Ÿb?]‹(P‰E­çŠ´ÁÜëžõ 9¥1Õñÿ�ýI4j!.”z@­­cÁàŠã dO&•ξ_ŽŠd½ÖwÑ*5Çæ #.ÆðN±d>pN= ýTÕSˆÐOH;'§YWLMhØz©»;Œ$O÷´K7ë`{n Ôûµä•¹€ëƒT“nßö‹ò…n¸ÑÙ¨™dª ÓþKùÑ ýØ?”´1ñ 3Zñ¿Ópø‡Hï~íMR¾œ 1ÍFºßÄ™^†>wˆIŠ"ö'Ù êý�&k¤ þÞfhžkéöY‘r�7bÜ’íØußµÄÎ,x¼ƒÈ¢YŽÌ4c@ÈÐè&7}çïC2Ÿ7=c$5ar’&\\­B�4Œ%þ¾$ô˜EW¢º5Rv�ÙsO-+& º]ÄŸÊ«¶ˆmï{ïhêsbœ¦†¶^*âÖÈótOËœØ ¹Ê/õ“`/ÿ×|9‹ÿ§iþ÷Ô¯qÝÈU†)‚6ŒVxœÍ—ÝkÛ0ÀßýWÜò°§5¦…Âè\ÃXWØÃÚ>”ÁeY‰5dÉHrƒ÷×ïdÅ߉óA ƒ@béît¹ßÝé}xxþöúûå;d6q5_Œ¤ø•3K€fDfï¥]]}^„¸n¹,.´J–?Ka¹T9'â�«yRZ®$àfÁ´åÌD¡¢pk6Qi幎�%µ85wp´9Ô "’äì~1§µ|ÁM’pÁ�â"ŽB⎽™õ|¨„§Ý NTcîÙ-¬ ~®ðÑ¢EªÒù@Ü5®W%ñÈv‚.Õ¢°p§Äð\ZªrV£ üvÊß7rsu»ð_A« ¼1j•µj ¡(ox ð[•@‰B)3lÆðÿcä]½Ö¹_.×�*[Gо–9“v¹õ/D�%ÓxBñcáŒõ.Ìgj~Ѝ‹Ri?GÝß>€ÊÕ¡–é˜)å«ÊEAøÈ"D÷»ØsN*ùƒÚˆò5ã¦ók,°+%Eu“§2A÷°è™>ºZJ¦1²=E� óYßÊ `(D—–­"J6¡°Ãl‡ŽÿE¹`ÑÅÑ´apIy|'h]º•�Œï ã6N‚±¼]'6[Ä×-G#_®£�å1P&­IåÌŽ‹ÉÀ†ÛÌ·;&˜kM@•´„Ë–XS`dXº­‡ïGî�ªyáOæ×Ó}/Šƒ#¦,{ÛÿQ&SÚB:ôî¸Á…èþr}äôÂôjïÅ´±¾çÎøønx)vGl‚´¹Á:u�yÍ6¼·º¬™/³vDÁ|�k6k ’j;Š8$Û4XUëíŸl‚öôsÈ¿êRRrJ5wæÝ7`î§»…Ë¢è èçm¦¹ÇÆuœm÷:êRofntšq³2Sé9õÃlQ±ôØ”ê©\8§–§Iõ(ÈÚSm¤ö�«óÉ&ÔšãHÖÌe“yl“a@è ×Å;?Ð)©NŽÃíûhè_~ÿ¯UÙ Ý &ÝŠE‚9•çxœÍYËnã6Ýë+X/ºŠc$ÀEê(& �M A�YReqF"U’ŠÇóõ½|émEÎÈ@� ±DÞï9÷AgûËÃ_Ÿ_¿¼ü‰2]ä»hþPœÀŸ‚jŒH†¥¢ú~UétýÛjï5Ó9Ý•RÄ×ÏøÞã‚=0¥%‹+ÍG°RR©UÛ�Ûm7^g,’£±p³Sk�bDÝ¡yº@(ÚbÄqAïW'E®‹jµÛn°±r{ÚKØoaW‰HŽ•º_eŸPªàg �t‘Ló.øvDÛxg´Å`ÖÊl7¥Ñ»Cg¸¤¨Ä\ÖTFn!aoÁd¡ÖŸVn見iöF‘"8ǽἢL@šý`|�tëV­HÍC„‚s(i¡/¢Bs„ ¡JYAw$ã«ý5G¨”QžmËÀ9¶çåúÚ;½¯g…_t�g!`w. ‚W8‚C)�~Âéý0ÞáË�Ñ~x†½³pé - ÑP÷­P…c–3iGÒžöèj�Ð%ZH€ˆ–b6 /[uÆìDHÄ_A�{͘jü‡Ïïµàùñl(ž«â%ðOÍ¡#±$=ÅC`C !„8Õ)£Þ ¼OÆ5ÝÓ�2ýà�¨í¥U4‰Ì²ˆÔ10DœIWdILúšG@1 ga²¥Eýëlµ»ýU3PòûÍvC‹"4Ï–�Ú~ê(t`:Cø•SS\cÆk B:án¢Ö^°ªˆd¥yyl-Á‹€×Ñ?„°µü?Ã@!¤FI×»30�–õS2ÎLC's(ƒêGÃâJÞÏ" %*! Ý«�ɦ›3úW·g5dšNºzšð=�Ԅ⣟>Œœ‚ZŒÍ“ódj²�)þlz¼ÊŠ<;Ó›íK’¢­uȇfÕ¶? 1:—7ž·£,èµQ=´‡RØfgЉÖùȤÒý àÆÑ\€Á±¨x‚åñ*BŠÂz2¾¹*ËÖæwJ|M7ó\‡™ö¤£®XÙ³8Œ‚L P3ÉÙDzR'šÌbRkÿ’Tê¨ré1Ç{fØuzL�æX.ö f²î¦Å¤Cq'�0…½ñœâbᡸ®¶ŸÅ– s2ïŠ2"w‘&ÐV?«YE�Æ’}¯ouS  Â÷á¾Ð«ÞΠ/87Ö¤ñ(\JG| Þ ôð®‚ÊŽ>£ÄŒ Q-ÛºD_ÙrÏ_'“ÊŸ"S} ”Ÿ ’%W["œHføAòcsê´Ê �ù {¡Й6žB“J°Æ ôi¬aºKž À`#aKOʾ·�©® óqË];9Q?aP!–2š\‹påÏ[QñìA †0IÑ*Å…òëI=šc�—\^è"™U램�}$~²«†Š8쪇ŒÁlÜš{ Î:p<òëæe�€SÝžêÖä§#"¤¤ª4 88àShl)ÎÕ´è©Ynú~âe¥ Kæõİ{ÑŽØ(™µá=ªÐBkƒ×éž5ï[ Ø_]IÚ/¤©Èa2O�Ãy¢î¼¾Ê|-ž³�G.±ÞÚ�wàÌôО´�Ð+þ@6ÐÞ²@(W§-@!©ÉìxÃxÂÌLhîsFVHcžq0Y¸Q±a–uÌ ðýÒ4ÇW[N¹h™ñ^™JZCÕ­µ^ÖEml wÎW¹¶7 iQ ö&;ŒI*é¿“11½ˆÃ½Ã~wÛù& r¤�4(«('dzƒu£4¤ÆÌH�Æü‘ m Aë´ßøÿ²lÜÿsþ|eî‹V‚8—0XxœÍYßoÛ6~×_Á©/-ÐZØŠCá ’eÈúé6ì© $Êâ"‰ I¹É¿�¤hI‰d9ݦH,м;~÷ƒÇ;{ûÝùogŸþºú™º*ã`ëŒfxTLS’T*¦?„­ÎßüF˜×\—,n¤H6é ÝÑŠŸs¥%OZÍE½�A°�:A‰Èî�Øïc¥©)OÕ{rD(ƒ-%5­Ø‡p–.Œ·5r8«Á¶!iI•úïH®ð÷¯R‘±ªmrLX‚--Ë6jŒØ˜x:«MxÉõ=ÉLD$³ToÇ’ñ½ÇR©7ïB'å'â�ÙÚ®wÂH*j…UEDN*a'ͤzM(©@V$c*•¼1¬˜­3¢hÕ”Œd>Í…å"rø>¬§iÇDŽæu+Z5o]P8x /Œ@\·3JH–2¾gÀÉw5Í¡©è¯ÚŠH8H@ÞZBr€«„×¼Þm¹ÔEÆr^C@rOTAÖÛ…l÷TÆU»�ÌÓY£‘Íž$p4¢b;êÈÆÊKÀÅ¿b@Kò…ÞCARHÑXzš÷¼èÖœµ’Çä‚kÈ[K8Wµ :Zs¹-r® �˜·uê]IVÚ™5ø…ë‚äüŽ ´&{Z¶Lõ›Ý.½aKÛü®˜ÅeBQË6Õª3�—#O¼~lõW~Ç¡q§oJ¡ÀkôØãSþ}¨ŠÍ)�¨BdKÊlàŠN¡»±>)«•�Ýœ›Ê%»M{4Õc­óÉX0èL<„×d¸ç @p–›ä®­X�ü@Õ xj¨ùs^�L@Úˆw'ªà»¢¼7GST�–ÁÒ€Òª™X´Vž Ÿà°ÓðÐ,äK$3ŽÀ2§éÁšÉšÁ(Þðg3 3ú‚ݤ�¡çø¢• –� úŠÚÍh"`/=›ÞRZ“„!c¶ÐÎ$0\K…d9n­õ>ŠX½ùÂox�éFÈ]dÞ"/ís6º°¾‚ÉÝq^‡-D½‹¯Ô.•À…uSéÁ õóŸ’6 çÆ\ï/:¯…½û(‚òDfïÙpàã£ìÞóŽßD`šåaÜ žÎÍë½ãÆàéÜ�ß»ùš½e�9n –¸¹Öæv:ðÂÚaŸÆ[òæ˜Íh�ûúPÌổpÌhQ®TWA�á 7£+˜Š·ñAül üŒĆæÌíÆW‡ ³ d½5õ"Ml9¨e¼ÕYWs=]ú‹ùT\µ¡Íö´/ÏÌFø¿—n>’ÿ޽fBÛL€zP}¬ƒjøò‡ñÙ)¬Wsiº¶ kÿØVW‡7ŒG¯S�A� Í�Jãu�pkêø}«YxV¨ç}§0<˜�‚=X~VðØro€ÛMLBïÃxØŸ$îê,Ý�§Àö«¨V(×�x©º­®ºÁËÈ‹’îl/©=ÕlQ·rXœ ÔÆœÖ)„F?9¥L¿Š¦"ï|ÙÙ‡ Ã­Šµµ¢åüR]˜6g J7sÔ)Z£•@_šþã� ¢Ç'O£kn�ÐaµŸS(òÅiEŠë|…ò«{ûÊ“¡Ü#±ånWê>º 6êúè¶jKù=ß³¾w"/ÏÎ/^mþ�ODÌ-d~fÔàˆ¬S°óÕÀßJ`¿•ËÐq(n[Ѝ.‘´ÞÙï¡æ#p ð£¸Ìç|Có-àEÿj›ç2fCõü¨k¶+Å­Z"?\ÂBߎœ•¼"{~]l~OywƒÅ,#êœgÌ\s¾°P6Ï~Óß «ècRO³œyfáö)çj¥ŒÓ”͵ùœìŸ0$FüWÂkà”"·Aß fº½ÜäÃAˆ ê~5øA²ÎDÆî9¥Æ/¬fÒ|ÉèH•Ò|cŸKŒ�FÏzJ(�‚K�®4T 2#Ûsê®çM:¾Þc?:Òðg‹²>Œñ±dèCùºy…hÔý¹PÿˆýŽÏ ÷÷–‚:¬8úxœí[[�ܶ~ׯPHí³;^œt<€k7E€6õƒ‘¢òÀ‘8#Ö’(“Ô^üëû^ti,íŽ�(°ÆŒ4ä9‡çÆïÒ›?½ý×›÷ÿy÷·83E¾�6ჳ7,N2¦47¯.j³¿|y±Æ{#Lη•’»«ŸØv`…x+´QbW!ËøŸÜd2Õ›µmÖžàN¦÷Dþz« 3˜"ý}<ƒfD—¬à¯.&Ç_%éþb»Y3âñ|Z@šÏ1¬Š“œiýê"{ï5&'2=±´ïãÍ SÛj³¦�øU¼Ùm-±Ý6~ê~JÝo+7òÎ=}»Y[›uuÄ—x4_Œ9ž,á'uUqõÄ‹2i¿‘EU›ŒÇI]Ô9,tÃã´k�}]&öËÓ7oøö*r“SqVTèËŽžc:òÎRTœ8i´Ìb¹§¯‘õ¶¹0÷}Ùäî¿<1ž\K�ß°¼f†§13–=ƒ®(£ç°˜“’ÿ9ãy.ª¿œVi¬¸©Uéd§…ä¼à¥Á¢Árµš¨¿œÑÕ¬a›Y¡#æÆŽ˜ ž¥~|×õcqÒ‘«Ñ(껬(o8Õ ×‹|·SL:ouÊy?Ö¬4"çñSë¿ANï$Ë�!:áÛ=¹–;ÃG5Ï>ª³¸‚ê¹=ò„ÏØÞp=+«gÅʃÕ/›Ôèlë·’A¬ ‘úæž�å¹k±Ù ÎÊYv£�g1\Ñ5œ£ºÌr4çL¦je±DçØ*p_ôeŒ“ŠÙæ¡¡_Â@ŽîRѬ/`$Kvž™‚_ÃP%?äò�‹³lÕŒ>‹¹JÐúG×dòˬ†‰ntÓy.2)Ó3Y±/f#ã,cÁ¢cÉ–ï�‹�[1ÅŠDÌ2­û¨J�¦·Öl(~Ζ�r¢¹<½.‚Êz/~&1ÃÅYîEÊËÄoz�UªiZY8†êÙ®ÖYŒ_É,/Ëå-W1+ÓØ"ô(ÞɺL™øÝ{Úw/¾[‚]gIÖJûÈý{႘¥Ys4m�[a²˜¥© A±[]ؼØFÞ ÷¢<ØZJØ'adΜßpç™v¾Šã÷ä¡)ß³:7žù³«g/0Z9‹¤ ‰ô?áøWn9¤‰Sx¢â3Z ¾K(zn�,NP?ß»·Õðp�j¡�AÊì.½Üfщ¥xï�Fùë}ÐG¨×›d8H-�h�Ëä�Êa]…­­{6[Q'ê<…¶° Ü׈Æ2¿Ç‚xÙO4j/ ÅÖ™2ÃPíI(DÝ ÔŽC¼¨�„¬&Ãâ~ùÕóXÑt׃àéò-rf# :Sྷ[-o¦õ7¥ÉÝÖÿï–”ÿ­tÕƒZWï¾bëj¾‰siæÙŒ`M;}ØyáåˆæQž›õ<·)4 �üô†œ€ßÞ æ¸&1›ÂAô›²áØ�ñÙ‚¹Ž9€5qüã~�®�? @>U¹�1Ô;ŽäæsÙîÞË�gâÓä«)È[º^E%5&ÈBqQœa°< �ðF|ŽðÕˆV–€©€ˆt.E ^²8ŠXÿ*̓Jäíïï+¾' ÈÞ‚Ï“F*~Òw÷£Ô?êð ðoìµÑèfÛì´ÓÞX"¬†®9_ldKFeëv¥�D{³L´7“�ba�P�9�ºŠ…µªyr*Oت5éžu�)ˆydË©ÞkÖ“ý¥�Ô…d‰&rëÙ³‘ÏL\Šüœó|ÎÝry°êñ <¼¿‘,°ð*rzh¶oÖÌhy–Ëhšd¤Î\”Îi-à¬7ýô¼¢ô×qnŸ 4›‚î­QM^YÅ”$@¾“ß[Tµ°¾…M²ÐuTVm­xÌ;²S(�‹²–µîOòÞyl*65%j@Mcý�DÞš_h ‘ž6'òùáXaÇÑÁ¶ •Û­*ŠK£)wW�$îöl¯±Ž..PH¹ŠäpŒ¤B±¡8#t˜ÅoýkÇVìáÕP·Ä+âÉï �™UÂP¸­lˆ$šƒEÖ.9ƒ=i£�M¶DìíëåÐPÉýÜÞ§ûP€øË°—0Lîͯ[F“„e]œ[~™šÓbðªrÖ¤`?0uo×òÓ‡æ¦Cæ#èLûg*Pfˆâߨ¶Â´pÓÕ FÑg§‡ßäPÛ99G¡3S£^â¹É!ê$R�:�Î5°ÝOœ¡ø*Ü¢¡¨7¿#Ío;o´ß·­,O ´óÚ¨°·o¤nÓÄÔ„…Ò©¿˜ñW0�Û/jzƸ뇷½ð—ã½pßö˜¯Ÿ]Çüc�€a O‡ËÐ<‡*6ë±ðÎAa!vÈUi„kƒ ÅОLŒž_’M§‹`urµ6˜ÝBÄh¼<¯ß¬¢éâÞËòèÀ•ìW'ƒp>PÑì�fèÜvo Œèô+¬t:ד�€·6ð,uéÓߥÌ'ÒþB“EÝ×5Ùo§ƒ¶gaiWˆî…‚dmø™ŒÙØ#,ºwm/x‰ã»D#ÖD%vó+\‰»#"q&sJ‹+:!±ÇmJÞ®º]ÎP^+^@å½ ªÝÒ v' ñÉ5~»62þ˜¤“~#$"÷G+D’yíôÓÑÝÎ+TP­¼-ÝÜŽö~ð9/ì½½ímÕ…ô­›Bk׳Eƒüïæu£Ë¦T;¦õ|Ô†´d�=Ì�ìÈo‹9f¾Êv2‘¦|ñ:º¿8c·³c�† àþŽé·�Æ2n3²[Pç4vôÇê Ù(œÉë¢#ž;ùŠsfhÕ…8dÆU|Ø%äNj7 6s‘¬��À1œÇp£Fï6Ëë(�Je1«ŒrCuÍ w�.Ð;{ß}>'Ÿ4Òú7gí_$2o^L`ÚóÝa€󷙜2�2Èá"˜óºð�6VR£WÙÆbK&´;×2ÜB¢vÚ'6«—ÙÙ‚$µöâ†W 87gìû.…Pzà1¡6鬆qg9 ÖݲD—Ýyӆџ0õF0çŽg¹ïÖ ©fÝ?åk\6 E8òÇ,Ë…Ág1Ÿéš¯¥<•¡-è½±•é„�ß{zç0g+h�ò¶Ié„i»lWþtTŒœZõèýÊ7ˆš†Vo›¬d‚W9�± •Qo!mÀ�9ZÙØïÑ©Lä˜Mç†û'ó”âË”ÈsYµMt´l3)1¦C’:tæåð#ÔÅ>íѳk×§�ó[âW ‰;‘-÷иZÅ�Í*“¼N¹GÕ^÷®#ãoK-�Kð™’w–h¼éF£%º,™b€°wÏΓB[qð1+…vx|â\ûÿ{¸vÿÅñ òpî ² õ²Š?‚<•&xœÍYKoã6¾ëWL}è)Ž‘ ©b ]7@.©öHQ”ÅB"U’Š×ûë;¤Þ+‘‚Ø9Î÷̓¶ÿÓîϯ/ßö@lÒdëùÕ #!¾¤Ì 1Qš™ûUn¢õ/« >7Ü$l›)\?±1ü•ýÎ…L9Iv\ŃÜp)wdLδ¿)d<Sêdx²–n¶Ú m8Õw0O' {>ARv¿zSôúyµõ7Ľ}ÛùëgÔ‹›3  Ñú~�Hãß?TEeøŽ ÜUŸÀ¶¨4@'œ¨¿É¬ú-<åiÀÈtN)Óši¯X ùke=Õë/«bÿo )Iˆ‚W’äÌD¨aŠÿàâ&f †� »ä�(�… ôÂö±¾É(@œ¤ÓWœõ¹tÜkŽ”kk2”Æ�è)?ˆ” s]ž`ƒG˜Ò~Hû%@Ú��´G$à Ç=M\?ˆS6ªô ÚÿPµ?<¡Ì,äúÂK9´1�k;º. oÀ[# ¯Œ©@gŒòèT§¡ 4âÚ‡ºmÇü�ÒˆæKÌuã?¾WX*×R$§‹!ÂÒ²G±�3¥gáÓ‘\œž�©²˜ÕûÞ—o\v`UÞõAQÛËMo±e�ªca‰:ª®è"±ga,»0 +Ÿ¥õsbâÕöögÃQɯ7þ†¥[ ,I€(E\iì§š†#710Bñ_Âl*…!\ÔVéGº‰]{¸0�;¦©â™}xœ-‹‚Ú±3„¶µü?˜€Ž¥2v½›�µ÷™`ÿeKÍ…i[È. qebÝÑp¥ó£c©ÅŠJ«îØÀçÒ³0QõÇnOlH6�¤õ„ô6© ‚S9õX;V92äá¹a /3æEå‚’ÙE¡[‚'míCŠ4«®³* Û\bܔĸ%F¯C›¡=ˆp››‰'ºòWÚô‹E1'òˆ¤d.B¢NWh†ëáøæ<ËZ›ßè5íÇÊu¯Ï:ZÔ5w–«J¦"²5–áÅ{Ô%^,œÅ°–Üë¨rì!!‡äj×ù‰yš{‰7ÖSìs!1•>ŽõÜâê(2ÛJ¸Rñïícê+›¤å®» ^?‘ •!�8 ¯¬E$iE¥dÇZ8ß)?˜’ çÝ£~°Ç»,éJáE3®¶1Q3û}°;WtØ��1Çq¼5RY” ëÈ}¯\·ëü@üê6W·¸rð¢R)¦3Ûˆ‘›žU 2"‰ž=—:Ÿ?ð?Š,7;Ì¢y½µ’Z¤³6ÊGÆ{|¹ÆÀD1Æâx¾÷½ï ÜŸS“+Ö/¼‘LpÔ²Ÿ0ç“Pß•úrû«B·%¢¡óÖ½½Cg¦ï aû½SȉÚ[(ú¼,<5É >qr;sÚ«¥••ÊšçM¦Å(Ú0Î9VÜú¥ì=¾ºò+dËLé•­¼5TÝÚ\Ê–EÔXb»ãõ7"ybÜ%‡P–f¨»Tc)öÏdLl謹sî|‰†¹ÓG•åLÐÓì yÝ(M©13¤ñ_¤�%h]6å�S›âç°¦ ›AŒ‚;˜ÕxœÍYßoÛ6~×_Á©/-ÐXØŠCá X“eÈÃ:£í6ì) $Êæ*‰ I¹Éß�¤hÑŽm9]1Hlм_ßñx¼“ç?\ýqùéŸÅ¯d¥ë*�æþ‹Ñ_5Ó”ä+*ÓoãN—?Ç æ5×K[)²Ù{¶¤š¯Ù;ÞˆšÓêŠ+-yÖi.šyâ£yÒ ÌDñ`Äÿ˜* 6¥y®Þ��#šSÒК½�GéãtžP£ç§SŒU4oI^Q¥ÞƫפTø»À£† \lËÚyvŠÐ &XÖyÒñ)ñô$ë,nšñŠëRÜDdÿ²\Ï"Ç[ðµ7®V¯c'îâL;ÁK× %¹hV%i©„C5“ê%¡©AW‘‚©\òÖ°b¶)ˆ¢u[1RPC)$Á6ƒmañ `ñ§Ûùœ+HÇL.aÕAÿDD¯¨væ*Œ!¶«3& ¤’òª“LÞ@”bwkrfVxS°–á£ÑVD¸òœVU žä“�誊LQhÈ`3#®e9/9hž7¢¹�#êQ ^uyΔ‚~‘ç�T3Bn´�U°’7`Ìv ÞpDd¾¦2ý0OÌ—õ¼¡ �0�;Ú…£Ýö±±ÿŠ­™„¿ÐøH¸”jâiaícgÒCÅSr͵۫a÷ ÅI§x³´Æ;]%׆ÎbÙ5¹ H²Ò.½u[¿p½"%¿‡ß6¡JÖ´êàß�’¨×RÓÏlLÍŸŠY»Ì!в˵�Ñ©!ÏÃýy¹µ/¼ÒÀÑÑX°C· Ñ£ ß…dSTN e%Š“AͰ7Q�ì¾GÖë*%»ÛÂ*lNP�—{CÁeÃ'1”ñ�Ô°Þv —]�‰  ‚Øià;È?´©‰‰Gùør…ã,Y.êÚö˜ҩ¡hÙÞè‰6šÂÃ3’°‘E9â W�9©6ÎLÚŽÂp#ø3+Ô¤ƒì0rk÷‚ó|ÝIPKÈ}M­2š øK�gÓœ6È]ÈÙ� dLÜ +ÉJ\rZ·êM’°fö…æ- Ó™�ËÄ<%>`n½ØÛbërý/ÜîböðæXÍ2ýÈà‘J Ü®I?‘ÁÜ0boÿ–´mqeÍL­ò¬ßÐxØYŠx=‘ÙozlÿQökŽßg^”qÚžÎÍ›µãÆàéÜ­×Ý~‹nÙŽƒ1n®5ÎìÀ oÇÃÙ|oÅ?3ÇlF#Ü7•× À'ÀŒ‚ˆJP*¹²ïR4Ú$“¾º[½J7âGsâ-(#š3§3N› £ 2_™"˜f¶¶Õ2�ë¢/¿]˳ñ\ý!6W*K£ÿïn-�mÿ§�‹ØÜP� \ì­a¦±1|x�ë'NwgÆl娯2L»¾ØÔïqºõxóGbÊ¿ŒŸÂn)J{húÁ�.¹4ù78"‡»_ŽÎ˜û'NÝ÷>X¿±†Ióþב*¥ùQ¥”�¶é@)�BcEi@¢² [sêÓóJj¾ï‰S?:ÒÛ±33íMœâcl#6mÐéîßÜIÿ³zâ~½ÿ \ËŒ† ðð– ‚=¬:¿xœímoÛ¸ù»~àÖàÄI�ÝÍ1ÐKwÀí ņáph‰¶¸J¢JRNÜ_¿çá‹DÉ’#9Jw¤°%“Ïû;ÙÕï>üýîã¿ïÿDR�gëhå?Mà#gš’8¥R1}{Qéí廋%¼×\gl]J±¹ú™í¨æ{ö#/DÎiö�+-ù¦Ò\äoL§"Q«¥Ý­–ðF$Ds³V¶+Ícõ™vF+J š³Û‹'÷]ÅÉöb½ZRÄùæiÂq=àxËKgT©Û‹ô-Ù*‹dë?�ÕžÊu¹Zâ¹%«ÍÚݬÉkûSb[Ø•�öéûÕÒ X-Ë#äðw �úʼn€'ƒàUU–L¾r$uH[“;‘—•fD§ŒÄU^e†’„ ÛVEl¾¼¾ûðÓ÷W‘Ýœð½ç,W—o/,<‹´C2Ð;J`$¶Ô(C #b‹_#cUtÃ3®mÚÄæ?,Ö\�íiVQÍBµ†Ï—Q ³gæ$å¿OY–ñò�§EJ$Ó•,,íÈHÆrVh`!ê°ó,n¢6;½Ü,A7“\‹Oõ->ä\çÚ÷chßü¤�—½^Ö6e^ì¼&Í'ÙtC&4êò”Q®h¡yÆÈkcמNgHË™F�°ù]çÉg9ÍF>ËYMD¶L�YÈ6¡™ùK#I‹�‘;”ôh«h(²Hj›Á-gĺ³Õ™3ZLÒ'n˜U¡y¨P }šFqÏL*lh1@ÇèÐcï8kô²JKødµá–—Tœ…?Uu¸ë”gÀŽSŸ§àk*°`»Lì2þi’ë]³ª±˜ U ™¦ÍÂ!#v;Ëx*D2“vÛdÖ4ŽR²',:¦lzŽ=[é%•4�ù$•»=³t0¦Ñr ù)Ïbdcq;Yÿ "m½ø'1#L0Å–'¬ˆ]r…âhrXdžKÕhS ˜qœŒ²ÂL<0Ih‘Ó9Dd#ª"¡’ÃïÎÿðö»> ŒÆiCí3넉 MPKÍs4¬�®SB“„#¡Àž(ñ‹"ªd1ßr¨]7-�ˆóÒá±JÊAŒrWac¥HLãòíQ¡¦›Œ]J¦JÀFŠê4ïZ ˆ]†â+˜}Ú™0Y?ªÜìŹGx)yNå|BKø—‰No/nÞ}w±înµÔi¸æÝµ_ÈÒ.ZjûÍÎY è¤ Úö¡ï³2¥¯j¯ÐI¸ÐÀOTL3*m/€­ªÆÖ›+–×ÖNuz±~}½¸e²|9…x±3;ß Ÿ˜¢:3¶gÖ2ô|EÈG´Ð„mi•i‡üúêú-¬–V# ¸†€ô0`øW–”Ä) Ü{¯xB 8ÿ¢1zÏ\(‡?JbèëÁž$=˜.½»Hu¥Ðø F~^RÞ ¬8ë�z $òãÁËÃÏê`Ø  €¨Ë(Ð;ÃB�-�yÖ©*ðúN®L n+ðÆ";C¬h‡\µå} øL¨¦Ðm ˆ|àлBÅò‚�‘¤gî—_Žn·³–œŸB'(Ê™ç‡V›>l'«ÁÐÝÌ%î§Œ%êʳFm÷_qÔ6]õ™ÐÓtž]7�– ˜IEÒ,¦WB³XxÚ²ðš4?ü<²ç{Xr¢¼wæ2Æ„ÙP…¿©.ù>ÛÎ2Ö€;e!ÙvÂÄ_M9T%.ÔÙŸRèo6 ‚£‹…›ƒ£žOï¼GAÜSÔ{B¼w,!ã(¯3Ä‘›T‘”Q·Ë‹ ˆÛœí$æÝÑútÂy®D:,"!¡Y‘Œbå{p¯-Z¾«q x…8Ù#dd—Ñ)(l*ëVõ�)mXNAŸ˜(D-Áw¼ôÕù%¤Û©3W·ç¹…ä/ݦ/ ±³o~u5—G8xðYTù¬5è‹ÐVŸ’®2£uˆv ûö›à?Ó˜oŽàoT378#HroÌXc˜ÈánVáQnpÆPÇ`3¹™£Q)YGñØ �ØU­Gç.P. œñ¸nßTSQk@îo&X/4£4À�›p€`ÞÅmÒ…­1õ­W›™þ›W�Í7•ò³éÎ@ÞÎã›Yü»þY<®ofÜ7×7„}®À�hÌ’.Še ,'k~LyhK\aJFȰtU(¦4t8s:"zsãAÖ“6,Ë[%K^) »›3êo%çµ›E4Sì{Q]Ž‚âL´»›Ž»@5½Ã¦ÛVª~rÞŒè‘éWàt8 ‘P£Û€eÉKû˜ü&i>‘&ª, C\¨²ÿ� š™‡A¤l#»å(kÜO§ÔøÖ²[þØÌ¢§ÑŸ%j²ŽJ\”a_ÁÁ%<BR‘aX\à �9î“âaNS}{.Y"oEPeYËé#Ïù;`u¤Ý1M~#w$#¶GB�yoåÈîc%t ZñPؽ�ô~r1¯Î—„[ynö�½‚øn:ú‹:‰ÀîûG-Óºå;õ¦×º îݪ€ÓBsç:uë%¡Ê5À½9Æ*¼ñš J°‰ngM—FjíØ„dÒPp"Ü{–dø¯“…U{•äÙÓ7’Q�ç|—jÛ5B¦…@pdý�MF¢Ò px¼àÇɱ×"t¦s~/ ‘OjÅì–Y®B´.z¸/6ã�Ñp{õ›!Á½ˆEV¿¨ç¿ßy‚J¦Õ�Jñ?³‚Ið baØcI JÂ÷—~θõé d¥&ú»u°6à™ž Þûfƒ—7L Ši–±Äݦ 8áÌðŒ‚¿ÙûðT@,Nd‘£ðú) w)©>HxŽ3] ¤ž2ù�Yˆ6S Þ ÃÖM ôv¢Š)X?ˆà$�â6ú  QwEËJb Z^äªì¬ÊÝt�8]–fšÙ€ñsÎà.‰e$jP.ÁäÎMEQ°¹mâ$˜ë‹Û‚ï©¿<ó SédRăõ³_«Ð· ðiü”¦xa)�šwÏ©µÓY.÷5„U£.Sõ“ò5oNk �=˜Iõ›fU«ÕÚ` í¦Êh½1íï€þ?:xs¨¹!ÔSù › vBå!Ú…;Âå=Gk­)»½ö¤&~ÝÄ�º3ÄVo”ßÚ·_-FšAƒ]sÄYß)ôÑÑQd‘ Ÿ9ûK6ã„âz¡ÈaY4“~wþm"/"Æ“œ¦à´åŸeÊ�[ƒˆ¤ðiÎÇí™B2#¾‚¾%Ù`÷Ó±Ú  "Ϊ„¹’ÝÉÞŽ}Ü•°sýðMrUX?«—îC/5À§_XÀÍÅ»yBnC|Œ ¹!Ï´K÷G—öÿ©þ?¼)à æ 6æŠL‚?•xœÍY[kã8~÷¯ÐæaŸš„–nX¦S(,Ý–…y”e9Ö`K^IN&óë÷H–|·ÇiX(MlŸstô}ç&g÷Ëã_Ÿß¾¾~A‰ÎÒ}°óGð‘Q�I°TT?¬ ¯[má¾f:¥û\Špó"d†ÓG¦´da¡™àîçTjFÕn[J»­³Šèlìßî•Æ´Q÷hŽ%P vqœÑ‡ÕˆÂ&+VûÝ›îÆü!°v29")Vêa•|B±‚¿5\j°@D4º½{ïÕí½±Â’Vc·Í�Õ=úS\nKðWS”Ï"vôkfjýiUŠÿ�Á)–èˆÓ‚ZÀ1%öƒñÒ E©7(b{Í­[(jn+@_E�æB•²‚åVŒ—öKí{¡ŒíHh‹(—Ø�g”ë�su ¾Î@Ô2<t+·îÎ\ú¿I:÷\(¦Ù‘þ”eͶÐÚð�£ï|�$ x?Í‹�œAFWe9^ú–û½‚!²”�JˆiÂ*jБ-$R9%,>{žl0;šò‘u$Âo ½Ao Sµûð]B•Z žž/äà¥È^}È©´ä—C¿c¶=„€TeˆšWš×ô@}†tQ0ÛÉ¢`’’©¨0á7‡‹¶Ârdtí°a\DÆŽfÕ}¬“ÕþîWÍÀÈï·»-ÍöˆÐ4EXJ|6f»É¢Ð‰éQLà_JM­ADp�¯ò „Û©Yyx ¦©"’åææ%|5Ô®ÀZËzŸ»Æãÿƒ©DH�¢¶wóÉ >Îæ?¦B\”x¥Æ8ô†èÄ£¬n¥ª!=â;TÍ�M°r ߣÚ}©Ž¢É4«F ~ “†Pxv£…q€Ó“w@‹Áóh`l £ø…qñ& ^¯3B¢^.š6û�P?µ-N:—Òë迤¿Ó*u=ƒ< &Úã“JwS^¹³Á "7�°<ßHQx yÞž.æU˜™Kï8FÝ,‹“ÝIÉ�×ñÁ!™ˆèÂzVŽ#͈ †ôr!Ô2Ú�¡§J½Ôøì9[©80˜·ÚÇ‘F�@œàÀÔðÚpŠ‹%'ݪ¶~G,ædÎ�c@ë å¾i¼ÏRýAÞJö½:'TåŸ*ÈdÀíÝ SÜ:½Pº±&µG¥+Á�/hVø 8„…Kiå-¶Ìf0*Õ¨6wc ;|M«] PHõ‰R>†‘ ª¦‚ß-—-ÅfËq'eˆ[ÞkSè„�p¦�£Ð�"¬ñ=÷û¢;®ñ\£`KM̾77©nL¼ã†³vöqKô²$‹�nÌŠpnO˜¸Ð¢ KŠ~P)ʤ –ͪgõdötIJ9•+äSey¢äu)ø`óô°ßs@3ºBšå� øv©:¤¬cådÞ­Es|5å“‹Æ*Î)Si+¦ÚG§êŠ 6‘kñ…3dŒ‹TÛ“ �4Ë�O{2íCKúï$$¦ópzÀöekë]’©é¢ÁXA99_ŒQÐi£z•Œ†á}F[žUºoÝïÛòw–ÿi~ûŒ‹F‚>—½xœÅYmoÛ6þ®_Á©_Z ±°†B0$Ë`ë´Û°O-QITHÊIþ}’¢^Év:8Z‹"ïŽÏ�LJJüÃÅŸç_þ½þ•º*“ öF3<*¦)I *ÓÃVçg?‡ú5×%K)Ö«OBV´¼àJK¾n5u¹á Ž:3k‘=£?&JS QžªdQrALIM+ö1\� “8¢ÆæOË00Ä IKªÔǰxOr…gxÕPOEÆ&xâõ²©5¦³ qÔ£ qRÖ ºæ%×$©±þ�¥z8…Œo=ŽJ�½��_ˆC±8­íL‘TÔ £Šˆœ4T":šIõ–PRA¬ HÆT*ycTÑ[gDѪ)É(V1’õnÔ.°})˜—žøÅÌŠæu+Zµ]P 逨‡ P%O ]‹Vc€‘ŠQ`Œ·T&UGæù–¨BÜñzã4-èšQÙË*Y_%#¹d·-«a�NÓVJV§Ì(Ö�·ÐË¥¨zå¹2h£œ×,#뇀”"¥jÍ)"Ô"„�$ßTÔ MCl@IÛ2‰ÈÝÑE´ ©dTcè9ëí ·f?–ÉR��Ygô[µ�Æ€¨7Ég‡K%pE]W@`ã¼úú�¤MƒÚ¿2§õ«n½Âaá(rè@e¿¦áhuwª_ú5wú&¼i–‡I×x¾6¯·N�çk7~îæ{æ–uæ´ÑاÍ5Ž¼Í ‹h;]4ž«[òæ”Mk�ö瞣 Ì^pLk”Q˜†£Eç8£±wTǃŠwIo~¡^}E?2Csæf “ë¾ÃLKï ù£kËî´Lb�uDê¹¶_-Õ̪ mU¦ã2“àÿïOŽk7È±ÐØÃ ôgÂSLŸ§ìàø€Æ/Ÿ0i˜<y½T”k«plПÚêº'¬a2y�ƒ �5 ú„çe�ÉD0§ï³8ÍÀÉ`^ tvÔ;y4|2à[6Âì:f᎓õ1…;>ä/guÚs@‡Qð@ÂãûRÝ´ ÇØèeàeI7ö¨½Ô"U{ÁT8`ªœâò6J‡¡sΑa”€ìI~ï‰ä� §%Æ^"CzÐWêÒÜ3Fnt=;ãÑÝä_ÕM«/pY@ÆøæìÎs÷J s8-ƒÁ½ã�¤‰ïÇîíÓ�±ÕÅXXâÚ‘Ö§iV!:Ýåµ­Ú›{˦§§¿ÿ�×ç—oVß¿x¡å.?/˜4réÜüE€ßJྕûac ܶY R%i½afo/çܱ�›G`£øÝÝ~Ÿ:1VÜ;-ZóÜ�×H�qÍ6¥Øà„‹Œoîà A—ÏNœ•¼";­¶z§=w±Dx ©èsÂMæGOa@EÄ–<¯I{.p–ŒR¤+Y¦¢\ ´á¯•dá’«h m;n† ø9Þš–¤xÎùJ3#Y¢ˆ<:.ÿÞVQÕv‚ç"b×�¹�\< “1]Q"æìûJé`×Ú2Bx½FÁ¿’µÐ•[\ënœ«t›Ô Qµ‹ÆËH°·.Nm˜.¼Í°½ëœ£s¸Ö6ùyÏ ý¹l>K”eØ0º|5Õ’‰E–¯˜¢D•)BL5@4<ç ƒäìî ߺ8‡)Y/0‹‹EÖÖ˜ ´­Po#ÞxÙ'3™€Ðª¤[݆d2Tá´V�V° yºuÆcYgäpˆ^²˜°dÔv.䌳LÑ ¬1•þTNÅWQ�9ŸHµìUЗÁ}cõ¾ …}§bÓ :N!0Êr}!„!×ëyÐÞòØä°üîÿU¼ª°NNlg|Ó%ˤ�Gl¯ú´Kf£_åI:÷#7«)Ê¥ÕÄp¬fÕø¬Ž÷©ÊÔjbÑüÒ7ÉNÕ–Õµã zZÓœ¯±Þ£ uÛ„³÷QozlQ¸‡’b5îühÖß°¾`ò½Ý¦ðØm?ŒŠB“¶íü»�¼S˜<¹fÀ×íß:Åû®_þŸÚE¦yØë¡›Þtߺ=�áðâ3e?Ú¾³ ýlßÚX:…×§ñ¹.fýþÊ�6.w€@Ljs[v Ü=Jiß¼Þ‰Ü>8#à7ëìþàî.ƒÇgDåÎÍ‹»vZî 0,úííÌ)HÜ*ôÞdaý}ôõSl4˜Oø“n�ÚÝàbäiÎî4d:©½»³!• kFGõ¹f[Ö5¹¿š«×á†æÇ£ä¶.íÆe;*ײ¨jsðصuêšü€D‡_8â °ÇÂü q"* LÔ8Lª‡šc2à ¯ì1×®q|oɾ>•‚8– û9FÆÊœzD~;Ž3°RçÆ¡9ŒÏн‡Ðõt˜�d6T¹tG ivîqÿ@¼»_G^5fc* ?jÆ]Èÿ ’”=þ7"Œ+e š+|?˜�SQÒýc•„3a™r•²”–‚w;¯sš]s÷£îÛ�í �ô%GoÀÇX ‚Å(9¦!ô›˜ ý%?hþ/øÕäÇé–‚@¬8yxœí[ëoã¸ÿ®¿‚ pÍàØÉ l¯Ž�m¶WÐ^óaÑ¢8ÜZ¢-v%QKRN¼}gø�¨—#%Î=€YØ’É™á¼ø›!wý‡�ÿ¼ûôŸû¿’TçÙ&ZûFøÈ™¦$N©TLß^Tzwõþbï5×Û”Rl—? ™Óì#WZòm¥¹(È?˜NE¢Ö+;,Z¯¹­HŽHüf£4Õ0…Çê;ò$­))hÎn/FF/ãdw±Y¯(Ò;&ŽzoaPIâŒ*u{‘¾#;Sc‘Œ.é;²>P¹)×+ü ·d½ÝRÛ ycJìo ;òÑ>}»^²ëUÙc Wð¨_‰5<²—UY2yéé´!w"/+͈N‰«¼ÊÀ.F’лªˆÍ—7w¿ÿvÙÉ ?øõäêêÝ…¥g™vDy'©‰ÄVeÄfDìðkd|„nyÆõ±-›Øþ—ÅÚ‘k¨³Í*ªYB¨6Äðèò"jqö‹9)ùS–e¼üói•Ét% +;.$c9+4,!ê,çE«‰ÚË\Í l3!dø´˜ácA3σCæ']¸Œž¶³òâÀ -MpZ>Ëk1ù¨Û–§ÜöKE Í3FÞÏõr:Ó£,Ïtƒè„W·äšë_ä/ø"Ïà²åH²çOX]3 –FÃ’{£Y:ªËÉvo$±FDjzD–gä«™Ë-&X ‡�Ádyh2KsžÍpΙŒÔÈbˆN±’çÞ ¸è5Ì’ð‰†Á�ç7�¥:×88ëÌcÈN3�—àõMT°}&öÿ<ÁJõØ3ªJ�Ÿg/˜h7;;�e<"9“ýÚbÖ2N2£,êK6¯›iÖ’JšÇ|‚QÝÈÔ8¹±cMï)+¾Ày¦rt:üTÕzñ/ \+ÅŽ'¬ˆÝÖà$rÜvFCÕd ãV2É»2ñÀ$¡EB öŽÈVTEB%‡ß�‡ýéÝ7CK0+`4Ni_¸KÏ\Ð ³ÔkŽÆ òÀuJh’p–'Jü¢ˆ*YÌw°á2§y^9>–AI9¨Qî+,MÉ`Ñ8ƒ�~L¨é6cW’©8@l£9Í»Öb‡¡úJ fŸ¶B&LÖ�*7s±Ð&^JžSy„HÐþ¥°¾D§·7Øt·^é4óþÚ� ti­´ýfû†tÒ&m+¹Y™ÒË:*t4ô?ÓŒJ‹µj£i,Ú[³¼övªÓ‹Í›ëÅ “å›ÈäÈ‹½™¡ø¾ûÄÍ™±³ž9bç%ù„š°­2íx_/¯ßÁ`i ’@dÈú#~¿´«AEœRÀ½Š'”€M cð 2@pø£$†ÂÜIÒ£)s»ƒTW MbB·Ùå!地çüÑ ß/É_Ž^¾¯Sa'a4ó£N&£ îX õµ2ÎYo@AÌwxre&¬oWA,ÙÖÊvâÀQ;®1Ò`™ Õj9ú�*C?,/!EúÅýô³ã±Àé¶·À’¹ã¤¿=05 ”9ÈÉÃ`£¨G4!ëÿã¢i¸�ýÓ±dsQAw¿2€e�ÐÒ÷Î!ý^¶�½—îÝ}Iþ k4¸³ÖÛê¸/S]GŒ¬'zÉâAÉÂþrO°»Y‚Ý�‰A\:ÌpÔ2ªµÕèåòTŠ0ÑS)Ô; O�ydê¦ÖkÒ£ @Ÿ ¨�((ÏŒSOÆ8(dæÐÍSž–‰½ÑŽãÀùÊ‚P ^EV õv-ý’)®ÎpL�µ™ñÂú«A¶ß5€{|^`æ üÚ%A€”uö€ö­–uJYÌ@<';"|�ü žúêÍ1uPTelØg™)˜ÁyQ‰Jµ'Y×슎͈jSÛž@Ý" aM¯' ƒž6&Äâ#$jˆÄ¶åÍ`Z  pÜ¢"R€%êšv$o»U;}ùH´1`_¸ •(†å#$”’Q„r€©Øƒ{m¹ò¸4([À+äÉA‹�ް[X@Ôç‚´Yq ÖÄ BÔYÇ+_ÍÅ�R즵4ÝÈç¡ÁŸº-B�s±"²o~v`ʳ=ß+ªü @òU$ªy�W™Ñ:íº�‰}û–?ә߉˜¿+ÍŸ¥™ ˆ{cºã¢�-0 O'ƒ†|�3MCäõÌD}:‰§¦„(Hœ]ö)`o9q%¶A>Qk~ Îoúi¸¹7 *GÀ÷èš°j7o„âv�„©1õ%R{1K2Ô·ò¦³ûC¥|÷Óå¶Mî¦Áý~¸Á�ã›ÆñÍõ a_*³¤» Å2PB¿z=ËY@‚# ¾ƒ1‚ê5”†JD㌌ÞÞx’u ÖØFy¥4Ìnð`4\ò�×oÑxþ°ïEÑ»³PJ´Ë�N¸ôÝcqla¥ïdž‡ý:ýV:žåQHÀ²6àYòÊ>&¿I™O¤ü™&‹Âšì×ÓAÓ›0Œ”-9w\‚dMøé”šØCì¹ã�M‹wŽûD-V’¢Ãü .ùc�IE†iq�Çæ MЇEØÂô…´d9¨¼•A•]ZNyοڮnh#íÎ>‚ô¹ƒ±ë­’Ì«Ÿ@wŸ+õƒjÅCaçÚûÞå¼Âì¼­ÍmøÆIAg7ƒFkXÚyyUk².Ëú´ÞÐjHùt¥ø>§nd ½©Ü  sµ��‘$åêÔÁÝÅšº‰—-i°Ömï—n©íb·"³¬ƒg3Fõ6a ^å�xæ0‹dT£s¾Oµ-ï`‹[…4°±…˜Ý…D¥°ðl!€“~j´®8³hB‰È'ÔLvà . ´®¼yjgî¨Oçã2ıú•»±Èê#0ö\÷ OÈfs¬úê’gGÿ�L‚{KÃÕ�,¾O¸ÞòŒ[ˆŽÑÈ–Ò¤n7òÎ¢Ë 4xwjô:ƒÉ51Í2–¸û[‰žZCæö}qŸa0¡Ê"éË¢ð6$ðvÛIÝuOxŽ­S¤îè,ûLs!q—À»`?]§¯an—¸{ê4ë)N£Ÿ�uW‘¬. K ·E gUîÚh´À®4]ÆŒï'w+ì:¢F~eS™ÑJéØ`HcÍå §>`\”ïB¾òýú¬#?¥“ ) F�áG,üÉÑ;‡!A½”/�°IC'Œ²]¸ƒN>pÕj‰·‹[/jâ»·Mì×€È$‹ 2¿Ú?­…4e¾Ó[ÙÀQmï�%²¼ÆfýÅ‘i:q…Hä¸,š¶¸;$6¹ã™Gƒù,³k² Ž".�H ŸæÙ6à“à$ùå�¶­È†»oM-ÈfqV%Ì¡p§zÛsq·œæ$p™‹0ê axÃÐ�œ—?a7—ÅΓ5qàcRÖ xy®\¹ÿü·²ÿÃð«=H&  gŽO‚C�"#¶xœÍZmoÛ6þ®_ÁØÒI¼(PtŽ�-݆{ɇ`Ã0ì-Ñ7ITIÊŽûë÷I½Ù–c¹N7 ©,†¼{îî¹ã‘Îì³w¿ÞÞÿq÷KmžÍ£Yý<Á#–³8åÚ{3©ìòêÍdŠq+m&æ¥V‹ë;)b±–Fü$ Áõ;i¬–‹ÊJU°Ÿ…MUbfS??šMƒÜ…J6¤å«¹±Üb‰ŒÍ[v¼<,Œfœ<7“§–]ÇÉr2ŸM9i|õ$jš ¯0»dqÆ�¹™¤¯ÙÒ@F¬’§Í~Ëf+®çålJvÃf‹¹“¹˜³þW‰ÿÝ¥Ÿùèß^ΦNþlZîèÆ¿+¼ÚçÆ€7'ÿ¢*K¡/¢-dsv«ò²²‚ÙT°¸Ê« A\ –tƒµ¬ŠØ}xqûîû—ב_œÈUmXn®^O¼<¯t 2ðå/{4ÆÁ�2¦–ô1r„â ™I»écS‹¿Elƒ¸VºXñ¬âV$Œ['ŒÞ!WQOsmÌAä_¤"Ëdùõa—2-l¥ �� ÉD. ¢-s>Êš¨oÎ^k¦ˆÍ˜´’#óJ%Ö‰ä~ì’[dw¹7Ãú<–ÅJ ÎÁg9ŠÐ-L9Èèò£ßW¼°2ì…#u�3°‚°œÈ�èá{¸NfÈ{=Š ïõ9ù¡{ü Ù;ôx‚Vh8_;çk^<8§óA7M‰` @ês`Ë UîÔXæ‚c‚IóÏͼM/|\8iÍ™â×bqB� `­}+M£g�X"ÇÆŒVaôÊq›kyÞ½uÓ�[9¾yí‡p°Bµ{êݘ-µEWžÔ#Þ}ÂqtÜ3eGó?ö¸‚;)O%æÙåïþ×ô~#Ÿ>Û©(í‘»AV7ì;T¾Ã”Å'0åö’²¡’C¿3ÛjDaèÙÏ“c¹»UˆûqÙBV–Kœ>n9úÁu*㔥½ïBä§´ÄôÅ&àÇ;é �/›d’†™ §™—Ê`�B‹ÊxÄRLVšçuñ$=;§§=^A fJË¥Dò$‘d?Ï"¦JúdØZÚÔ©öºHâU�üÎ%°ê‡ŠÎ]†e€L&‹L­÷„ÔòE&®´0%„ãŒ@áuc½ ÌO£ˆ”æßJ'B7¯&wkéF¤/¼„¹Þ€ÔVã'… ‰Mo&_½ù|2ßrËljÓîœ7_Ös:žò“¦Öò70NtÒí�©ÄöûM).š4°Iw®Sñ�»â1ºR¶Õ”aØÈâÁ×|ˆÁ©‚@¼ Õú¢O÷�Ýa/á!á÷TÔ¶a/XB2«<}‡ÙX ­¶©y.6Øâ½Øº‡Áh·ã Ý&JÁD^J-cžÑ¬ë¨qXëš‹CuÂePeÈ÷¼{e@Ê#ÈOúÃ!­»)Æîá®�BÀ +.3ÇlUdÔ Q쵘Ä£¯=qˆv‡è†úkaŸ¢[¦œ{ þãšt¾ØDC‘÷C³ÃëÚfNæ9-{Ë$'wfØ-üzåJª@îÞ/©üuÈ*!ÏLSB°Â�ZÝÔ•KFEâÁ�l—~Kí¢Xò*ƒHpÛYÄ–µ«qQÜÕ¹%TÇeQ©Êôv-‰š¾§‰>“…BÝ:2 w\£�'òõéXbÇ1u Èr¹Ûª"V uqO.;EÜïÙÁcu:ú¼ÀAY…ÌÕ„Ã+R:bð)§�˜X‡a¯V.Áj¸[aˆtŠG8¤gíÔU榛ÛîGÛ†ÂoeÛ�DsÉÇ[“SÄ“6 ÕTKäNí}sj÷¨!UåcúG¿âÞ½û–Zìsµ”Ç+ �ƒVkóAb•5ÍçÙïÁ€üÓ½›þý¯�6øQ�b^z&Í7¥ñùˆÛÇnÇ‚¢�†¹Mª0¯¨ò6‚ÐÞÓùþÃ9¸:‰Í%‰ï9£ý)jäÑ4ˆõmþBX®‹d‹¡[:èÎ9T?¶íg"sj ¤)iuuîjÍQi1Ÿ® H Û]«ÝoßÚ`›[«NÛÊiÿÒ¸ï\‚3°ûï|ÂUyØJxA­Œv[g+¦ÞTÛN:µ×¿ä“3£Ð0·†Õ„æº;¤ï3Z !j®�ZíŒMÆ”:L?ç‰éæ•“=îbËXÄ�ë]ÁJrÏѳ\rµÀ€ê¨®ýP>áí2z "FmÎzÍ9cj»1m •tœ…î�¸/ù‚ä�#Æ-ÐåÓerHiÔ³ý2ÜÈ=§³nüû=e�" MÔ©îË‹J‰+ Îgh·si÷y‘j“sb˜íøußEÆÎéÃë¼µˆ‚[œ‚ú„�¾5ôJ.Û^1Ü D®ž:Å(vÂXá �¿ôVùžåR<Ü ‹ïJ“fŸææ(å³Ó^7¹8‹TÖ *â¬JBöß»n=¸úÔ<„¶1)ˆéç̾U7ûœìq$/âs}M×ÂÁã?­£Óð3Sÿç9ÿ½™:BŽ.žxœÍ˜ÝkÛ0ÀßýWÜò°§6¡�ÂèÜÀhŒ®¥ÐGY¾Ä¶d$9iö×ïäïÄù˜Óy ‰£»Óù~§»³ýóϯO_!²I<óüê YH_ Z×tiÉWáùÜUînÁfd3 rM’:ë3xAn•µ„7X³8Cãk¡XW{'æúvTˆ™%¨­6°®•}Ljif£‘Û “Yi˜…M$x6Bx˜/S¹¢“WhL•¶#áÜv.�kÔ°a[d€–Vb�r ðª2àLã�qV=(BòVÞ`sç™r¡²9(ÚÀˆ•LPÚqy§ºÕ> ½H.†@¹8ÃÒ…ø4u -±ãJk4©’!XÈd¾ BzÇÐ_½�©—§J›Å?€Ú¾x$•>ˆ÷uÞÝ¢‹ÿ‰,²@Ä‚TÂvÍr!8“®Æ2n‰L™&E.–[e"�2e†û�ÙÇü$mbù ÓøO¿5Ýk%ãí¥|³ä‰\¤†€Úô�³£8™=û],$`~¬ÒZî ÃYÌ4�.‹+‡$?kûD˜m0Š_$ç�Ä5¦:.I{qÚÕÔþH¹…^ :µoúÑ 2òù¦hã˜ÖTíÈìþ!3°6*ê)ÆèÊ•Zi™�5½êà±Ý#]{8,Å9®Eêþ¼ˆeKH¢;Ût¹¶–ÿ#º LDýÂ]ïz€öþ"é—¼Ý_¹P’oµÃ´cõg³Ï9¼ÕlTvĆ]~0Ëù¨ì‰»}°É°ÓdzžyŠqø¤%¶å˜ã<�¸©'4Õ•êI‰Ñ¬¶7=y­‡—KóåYg’³¾õ Ñ KÚÆ» Ò¬æíTSÐú¦ÅM™Óƒi±×–mw?X’Xþˆr¢/„6v¿NäO6« ¥t 22½¥‡%ƒÜ�â…³4m Ÿéuþ¹ËÊuš¦�:Z”´ÖãV¥S¥åj¤ÂK³ë›)qaØ'½Zjä׎õn‚-b¶*WRÇäÓ‰«• Á¯šþ:Sß&¢àÜx\Ohü §¤ê5ŽOÊ·"“â5Ìo§×0ˆ a Ea‰`‚E“D8xœÍXKoã6¾ëW°>ô”XH€ŠT1Plº@.Û  ì‘¢(›…Dª$e¯÷×wøõ´VÞÊ@� –D΃ó}32ùéå��ï_Þ~G]»(i~(Î৤#rÀRQý¼©u~ÿË&†ïšé‚î*)Òí›`J þ”–,­5Á@E¥fT%±›%±W™Šìl <ì”Æ¤QOh‘*�‰Œ8.éóæ’ĶÀešáÍ.‰±±óxÑM?ô>ÂÄ ‘+õ¼9|@¹‚¿{xÕ †ˆìòRŸÏ(Iw�ÂŒ[±$®ŒêúkŠ*,ÁuMeä¾gìØ-Õý‡�›úª„bš)RX¢#.jjaÀ¤Ù7Æ÷H(’F«ÈÍs„¼w(ë.}5"˜#LUÊJ¹5¾Ú—¨]F­ŒöLhgÞ±=/)×[ïu n/Á¡ûò¦.Ad(³"6cÕc”Þ@NYÁ@¤F»Ôï€àAGJ´�HU”°üÜ`e£é°²lŸ²!‘þ Ò[ô~`ªuž%dϽàÅùZ>×å[Ã;µƒžÀŠ ôŽ£RÄ($ŠúNÐ}‚0®éž6‰2 ü„ÚA2E³¨¬‰Fˆ€¡à"8ú+â1T<ˆ¸ �„–á;Ö‡ÍîágÍ@ɯILË"´(–Ÿ�ÚaÊ(tbú€(&勉è "¸ÆŒ�š4Âý Þ¬ªˆd•ùxd¹[×S?†¯3ü?#uR£¬ïÝr<£ýËŠëÒωÜÆFó‚“!qeî¿¢eªiv«"›fÎD³_õ÷¨–H³ÉZ`ßÓYE(=ûNÃ8Àé©q@‹K-K°z- ÞeÍ ^šËíì¡ï*£ÞŽÚ�MB(~ëÇI¬¤ÛC9L³]åܦHQΤÒÃ4w]f!N@ÕTÔ<Ãò|窷¢0'›¨«ª#0Ÿ÷�[æ­Y4«ÝuEÉ®ÈÕÈ4lDv-—^•‹fKÈÔ™¾"›zZÇtúTà½Ã³™u¹ÿœ§Y!ö ®þ±¤C¦Ó‚ÎÀ``lý§¸XµÛ Eõ£8bÉ0'‹Îb·¨ô]íc ÚQY,Ù×p\•ŸÂqÆV+þÞΨø{7†ö£à€ÇÌËÓMÄ’b£$¯ Ã>*ùè„!„Lö‘)íÖx‹^Ç›Œ?ÙùÈ`Èb38g_ ÃÆuEXÂ…Ù^ÂÙR•"c9£Ù�1È]9‹…ñoTŠ›°ñU}2Ž_EE/s Õ3ÕbéëjÃDUè3™)oA´¬†òk¸ÛÔçP›ýý‡�’ª öÛࡇwðÉÉæ¸P4Ül\�'՚ȿòªÖ/@ïEÛB3yÍM¡Õ9ÑWÂwT+ˆžCZkÔå²½ìók¢kI‡õ$tæ ’¯ÈÔ“×W››Æ‚í>�3ó jÖ]BÔÏt¿ ƒhïX ”«Ë r4,öÝ'ã3�‘9·Y!�yÆÁd首VÖ1×­úö¦mŸøjª +Þ)SRýöÙ‹†bfS â G«×…vD爖àilã�ä’þ3.ø=§{l/${-°•�e5åä|uŒ¢~�fcÔZ™ˆÑtx$F±¡gÈøØß[Çî‚ü_ø»3‹7‚D–rOxœÅY[oÛ6~ׯàÔ—H,lE�¡P É<äa™±föPes‘D…¤œæßï#©›SÉrRÄb›"Ïå;*áW\Þþ»ú•lt‘G^Øþ0šâ§`š’dC¥bú¯uvþ³`^s�³¨’"^¬WJ”W\iÉãZsQ†�[÷ ‘‹ôÉHý1Ršj�òD}&Óü ôBJJZ° ŠÌ�€©?íA‚E/¬H’S¥.üÍ'’)ü�ãQƒ?)Û�Æ{dÅPh9 2R#Ò�YKhÌs®ŸH:à!"þ�%zá9Ž”o[$…:ÿä;!¿‡cZ±]nd‘D” «ŠˆŒTTÂEšIuF()@–{$e*‘¼2¬˜-S¢hQ匤ÁÌ„åî0ÐÝnXG¾cW„™DÀ´ô†jÏASx`¤¬‹˜IƒžmYiìH’ZJ^® /!2ã_YŠ!¬ÚÒÜÐi^0ܪ¢ ;óÈšƒ”Æš–šÐ-“t͈¤@'‘• [�km�¦,ã%ÄÆOƒ!ôzï‘pKe”Ó"Ni˜ñ®$#ÅŒžœ<Ò'˜",7 é‹â×J®Í6ËyD–\;G|&\°je¼b; ׆®˜ÕeÒF ’¬´ËÓŽÀG®7�c{£áÝš©^‰×h)è=›Só—b— �¬�ðì÷yÿ�“?4¼gŽ&Û´ã±{ŽÓîñ„€o#Ò¤ ¸¹AûÕIDÎÙÇL²‡]èy6@£ÕI=”åüí9T0ëU r]v;P5ˆxeàAÁT,“F6M]žoøz“?ÉQ@\ K­&2ÈT‡Ñ {�¦a¦Ï”9!Žt@É·»Ö¤‡)vÞN–àϬP"f ;@îÄg°ù–µµ„\ÐÔ*£±€ÃôT]JhIb†BWÃ8S{p�l$ËPýµ®Ôç `åâ‘ßó Ó…�ëÀ<�°»tçˆy9�;”ZB,ˆr}a°9WGLÐLy¤‡6Ì¿»$­*Ôô…9Œß5!óûØÑèŒÈ܆Õx/û² »ã·ù—¤™µ£WðórÛðcô þªÓ_½J¿,Ó†£9~®5öç€n÷ûð…Ü9¿g »ÎðéZ‘�Ì50dW€vµ?—§%jGÓîl>F�ø©"w‡¤‰æÌ©ò£U7at@ÔGÓèÑØ6rZF¡N›†éÅÂßM–ZW·ý®~Ó¾Å2êðùÓž¶æ»épÞÓðájýèùÌÎÕTm,-ÃÛþ©‹U×úÑÎãà›®«to�³Ce<  »Ï£HÍ �^õ õî`v ô`ù„Ðÿ¶=Þµ›8KñQpÃâ$cJsóæ¢6»Ë×k<7Âäü¦Rr{u+…Ö²ü^h£Ä¶6B–ñß¹Édª7k·.Ú¬=½­LDýúFf°E$ú»øót°!Ú°¸ds1µü*Iw7›5#/'¥£e ø«ª8É™Öo.²WñNco"Óiµ¾‹7{¦nªÍš>â7ñf{cimoâçîO©ûÛÊ­|p߾ݬ-Ýͺ:≟K|5_Š7¾YºÏêªâê™—d ÑMüNUmxl2'uQçpΞÇièŒ]]&ö—çï¾ÿáÛ«ÈmNžQ¨Ð—¯.=Çt 2ä�e§8qÒh+˜ÅrG¿F6PØVäÂú²Éíyb<¹Ž:ß³¼f†§13–}]QF=Î�2'%ÿcÆó\T>mÒXqS«ÒÉNŠä¼à¥� Ñ@�'iõÕÕf ßÌI13oÄTâ, â‡0ˆÅÉ(®F3¨¯¢ÜsÔ§q+n'¦˜ŒÜêTä~¬YiDÎãç6x9½÷I–GFBt"°{r-Ž„�jV |TçˆÕ‹¢yŸq¼á FVÖÈŠ•wÖ¸lÒœ³]ßI±&DêûzB–GT­¥>+8+ç8�Ö�ÃkEè5Gt™ÛhÏ™üÔÉb‰ÎqTÃ}�vÑñL*æú†V~ï8²KýC»¾€‡,Ùy>j$ø ^*ù].ïrñaŽ£ÚÅçðU R?…þ ¨/s6ºƒÏmç¹È¤LÏ侘­Œ³<ÙK¶üÜ[êÙŠ)V$bŽ_ýÒ§´´»seKðsŽ|JÍeéíø˜«÷à_f„W"Ë�Hy™øs`¢J5í?+ ÇR=;Ìe¼&³",—÷\ŬLc Ç£x+ë2eJàï>Êþôê›1¬œ%Y'í�ì… -pK«s4í�{a²˜¥© A¡ž¬èëŠ'b'€·( N@âyéù80£º«©[Ñq¥i‡}G\hØ6ç—Šë �ßäNû¬· vËÈ|ˆ¹o[©R®Ú¯º°{i>Ð'^)Q0u@*…ôKMöæâúõ77ÃmÖ& ×¼~Ѭ lé­�ûÍÍ#,é´OÚ5woó*cÏÚ¬0i¸ÐÒë„åL9ìMЛ\ã ß†m´3“]Ü<±º†3yqy‡DygwhqWÂ? #wæ|Ï]dNøù*ŽßS„¦|ÇêÜxæ/®^¼Âjå<’"5$JÿDà_9uȧ,pÛdÅg¬@ã!–Pöì‘�?,NÐ,#ž;ØÖw¸H­Ðå UuW^î3�ì„*>ú£ÑÀ‡EþrhìÑ4çm1”ŒŽ@4¨e òNÕ°Ð`kží1dý€§ÐlBÁ]�l,óâe¿tЪ�0”kÐ3e†¡»“0ˆºè�‚xQ¡YK6Êýü‹ç±¢ínàÀÓÅÇã¼®¿:OÓèU˧fýi²>wÍþí’^¿“®zÔ�êö+©fû7—f–ƒ±î±À®´»¡œ§Ð]ež´Y/h[‰šIàQˆÞbÉ Èí#`NT³)üCÓC6ç0>ûñ?7&p&ŽÜ Êê¦ah—(Wš3†žcËQÔ| Û¼üøN|Ú:Õ$ ê•®�ÓDQI� rOè-Š3,–w¨Ÿ h#>G¸jÄ*K@Tƒ„t“¢'YüD¬ÿ‘æA$Šö÷‡Š/ÅÓdkA�çI£ ?ë‡ûQÕ xPø7ÎØhô�mOØéh,‘VÃPŒ\,¶²%£²…Óç#ÑÞ-íÝd¢X8 ÀbN«®¢Ö`�iž�ª6ƒjM¶gá]1�lÕ{ìÓzr†@`’º‚Ì ±g"·‘=ñÌÄ£¨Ï °ÎçÂ-—wÖ<œƒ‡�7’…€EÎíÉ­�©g¹Œ–IFæÌEé‚Ö]Ày°OßWTþ‚àö•³-!ØážÕÖ•ULEä;ùÁ¢Ù¨ƒ³ˆ ìh“…¬£²jëÅcÞ‘ÝBu\”µ¬uÓüžµï´�C(±È  ‡„nl×ô4d)Äí�h JذíNWAíuG­W´É"ÎíÒ'œ"Y3‰Æ¦`„ÇŒø½ìX‹‚V’xD<ùTŸ ê8À�@CÐ^ú±Ní n ú.[­ò‘I/sJîfÎ(ýÒGBºŸ‡3¿¯RsãžüâñPÃgòò®¬‹s Á/"R{‹ ^UÎÚªé¦îéoY|úÐÜd~/rþ¾lž~b†$þ‰íþ§e›n>°Š.ƒ1{[;í€ã}ÉLƒz‰çÖ…(( $]=àtž¸æðý²/Qo íïdtôÇØ ©c Ù4@.Û ],°GŠY,(R%©x½¿¾CRoÉŠ\Ø‚$9ß ç›µýéé�O_¾½þN2›‹]´­ÿMðO––QmÀ>¬J›®Ymð½åVÀ®Ð*¾ý“ð}öÄ�Õ<.-W’àJÚr0ÛMØm7f¬’£Óðag,µ(Å™¹'˰P(ÚR"i«“"·�«ÝvC�’»ÓFÞ>"Þn*Ô˜‡Uö‘¤Öøh‚©dæ�÷µeG²�w£R/²ÝvGþbT)¨F{-è(,$ü­Ö˜›õÇUØû)”á–¿1(F5y£¢ï}ÊPšÿàrOlÖ�¨Ô=D¤¶�$ÝÃòM•„QI(c`Œ 'z¬ÌŒÚ”Æa'Êz×´�ïeÒÞV6oÐèE®ï>|ƽ‹˜ ]’˜1ö˜§W„¢1Eº^ô§}‡µ†òÌ*MLŒ§Çš,ïÐÀ•ò)=Qñß(�¬}ɸiíÇÿ5fÍZIq<›ŠÏeþZ‡žYÄCOâ’$ €Ç à†]ˆ~j²Å¼ãø*K¸´°‡:[†ÎŸ€dT4ËÌei|àq%}‘Kr2Dž Å-œÅ !Õš�Ø05 9p›   põ…0%-å²!¢NÚOÄÆ‚+ò†i^¸—çÑÒ¼ 9=ü1E�åÿ‘(JL¦´%I_ûœE— í«Kù3Ó(È\…ªz‚¥I·„’õSX°R±º»´4øt ué÷”6擦™0 äf‘H|¬¦g�„Cm�U³ÆÙ$Ñ¥dtq>¶Û/ImuÌj»ê›�FO¼Ãåòæ=µÙj÷ág˱Æþz·Ý@¾r=hfv¬�¤¸ÍO�3 ì™kc‡yB¡‡±*eBõñ&"p=™Þ\Egó;…¸ *÷X›Ž“åICCÉñg Õ2•Û `¦’³éÅT/‘‹Ÿ“›ºS§¥p*G=ˆ(’Ê­³[MB-EЗ©¶®R^€a„: _ZRþ½{Lsã"›vÌõLÐ ‚ä*á)‡äÆiÄKµèx¥b‚-•4� Õ•òçÅ<»c�—<•ÐU2§Áž©qC&V´qWI5ü3ë×k$ì©ÿøÙûžƒ92dÁJ�ìx¶“¢¾—æ�Ôª™pÒ´ƒÿ‹“6.@›´ßTŸÆ7á#ü¿?áVÇŒ7‚G˜rƒxœÍYmoÛ6þ®_Á©_º¡µ°†ÎÐ&Ë`í‚5Û°O-Q6W‰THÊiöë÷©7§²•t›S ±(¾Ÿ»{xÄôê/‘¹E†ärÛa©ìÓçq�òŠ$¦öí­0–ieÑj™.XÍ ì䄱Ogº•Ë…ÍŒ¬i(jUÎ,¯êR°œÃ§…6è9‹¼ƒ |—1ôßÑNZˆ'U£»ß ˜4bJ+%Ö°öV0Xºb[n$_•Â.;w$ON(ÖX‘3§ƒBÌ@Å×Jº&èaÖ-,M¤bîZc²JÀ(0 »Þ#ü�LWµVBÁRUJ›Š—å™È#v-݆ý- æ\yk‰«†—šÊD�,…T€µºa6ã0goûˆ-Ñ;}½Lè1²Ü¸šDª¥¨zp·‰S+×_&¢Jɡԥ±€Z~$&x¢)ÜÂÈQ˯|Ó±£hTæ¼ héxA– ЂG<šv6*þ€>åÁDpQYêkËÞ¼ºüùÕë]÷ÃzdA+¶ÂÌ5¿±ä�ÌîÐt?þv¢Š6¥LÙ™t�±Ã÷dm¬Të`6/¢�ŽúÝV™DB’—vÒ�ÚèÝ\È�ð_ï6¸¸l„&éÌZñbnšßlG2…iOÆ3°Çc>|ÝJ�v,9½Îö£ÿ„y·ñùð–�Ün£ó9„ Ø·Eù1ˆ|Òr;ÃòÒ¦­ U…W»ª�&Ť3 ‰w(ÖëX|4ò°'«TufÝTaáÚ‘÷‰î�¿Ï- 1ÊS6,Ø ´Ãr7�ÒòRZ¦ÉDKÒÿÑhi ¾š ùˆÇÌÀØ­êv�� ƒ?já�‚ôÅp€ÜqÙh!ž5½)æÊüd|¥a/·7Bgˆi+�ÅÝ@;îàMÎ6FØ �«í‹$jq-?Èó…6ë„Þ’NÚû|gÏýŒAa›î#"´Z§ïÔ.­Æž›´UÀ�IùþÃë;Ü‚2”G­×âÁ}Œ¼ãàγñÈLJŸužã‰�Y^Äi[¸ÿh©¶a4 ÷]wsן3·Qy�ÂÜhé°©¯‡±°v<,Àû�-åSifô»>Њ¨4bT‚Œ*$�'ÈB(b´9ßæYÚ‹ßîÞ£ÜpR„¹âô¢¯ I 를”¡ÐÓ¤K—·iãý¥?Úv_ÇÂù�_Ò4ø·›g„ú8þO0ã—·˜8No×LA½Ø•p ào›ê¢O‡ãtçu 2:¬Nw²èã íq‘5u÷}+5<(ÔÓáP1<ª�‚=j~Pð¿ûLo„;TLB“÷v®xØ—±›Kå)°C+²ÀÊã@<·íÔ[Êèe äYÉ×þØéº^{“§#ÓâDw§¼5†Ê)e†V: ù±Kïªì^h;[zàçöŒŽ#UÚšƒN¹u¬8èsÊóOq"{ºâäj §@ X}f�Æ]”t÷Ý’��ow™À›ðö¤c¹,âÓÊ6¥ü”tlÔ8›ª)Ã%ÊîK›ï±Ç'§g_/þ�ïˆXzÈò˜‘ë‚YwÁ.�þÊû•™‡ŽEqÕp°ºô×Vkº�:ÀÀc€§«*að;ß_j}xqNôˆé9�™z=Nñ3gè>ý¿»yûD4i?†&á[ë?ú�¢¼ •t‚I«l8ªxœí[m�Û¸þ®_Á.pM𮳤Wg�\Ò+ ´wA´(÷�–è½…¤¼ëüú>ýYr¤]'wØÀ–LÎ ç�Ï ™ÍÞþôæýÞý…¥&Ïn¢Mø<ÁG. gqÊ•æÕEmv—//Öxo¤ÉÄM¥ÊíÕ?ù!ò6}+µQr[Yì¤e¢7k70Ú¬=Ám™ˆüõ�6Ü`ŠŒõwl!̈6œ<¯.&Ç_ÅÉîâf³æÄãù´€44ŸcXÅâŒkýê"}Áv“ã29±´ïØfÏÕMµYÓ{Å6ÛKl{ÞºŸ÷ÛÊ�¼wOßnÖ–ðf]1Åß%ÍcŽ'KøI]UB=ñ¢ Dºaoʼª�`&,®ó:ƒ…ö‚%]ƒìê"¶_ž¾yû÷W‘›œÈ}XQ®/_\8zŽé@dÈ;KQ,vÒh+˜±rG_#ë-|+3i}ÙÊíEl<¹–ºØó¬æF$ŒKŒžAWQ�sXÌIÉÿ˜Š,“ÕŸO«”)ajU8Ùi!™ÈEa°„h°œG­&ê/gt5kØfVèȹ±#§‚g©ßwýXžtäj4Šú.+‹½@¢šáºr‘ï¶bÊIç­N9ïÇšFf‚=µþäô@²<Т¾Ý“k¹3|Tó|á£:‹+¨ž+Ñ#OøŒí�Pг²zV¼¸µúå“�mýV2ˆ5!Rßܲ< w-6[.x1Ën4ð,†Ë»†sT—YŽæœÉT­,–è[îƒà‹¾Œq9Û<4ôKÈÑ]j"šõŒdÉÎ3S�àkª·Yy›É³lÕŒ>‹¹ Ðú{×dòˬ†‰ntÓE&Ó²LÎdž˜�Œ³Œ‹Ž%[¾.6nÅÏc9Ë´~ì£*šÞZ³¡ø9[>ʉæòôºü*ë½ø�ˆ.—ÅN&¢ˆý¦ðYK5mC+‹ÀP=ÛÕ:‹ñ+™åeYy'ãEÂ,B�ض¬‹„+‰ß½§ýéÅ7cK°+£êñ˜¢g�Ѐäøã,Fý Rü`«áá =ÔBƒ”Ù]z¹K%¢KñÞ�:>4òý!è#ÔëM2¤Œ–@4ÈeòNå°®ÂÖÖ=›­¨õžR[؉îjDc‘° QôS�ÚIC±†u&ÜpT{%¢î$jG€!‘WHBV“aq?ÿây¬hºëAˆdù9³P�©pèíVË›iýMi2E·õÿ»%å+]õ ÖÕ»¯Øºšoâ¬4ólŒ�Æ?°¦�¾ì<ŠðrDó(ÏM{žÛˆš„G~úCNÀoïs\“˜Má úMÙìÇøìÁ\ÇÀÆþ¶¤+äOÃ%P…OU.E§õÇV ¹ù\¶=xùñL|š|"yK×Àk2¯J� åžP\ÄR .o‘Gx#>GøjD+KÀT@Dº—¢/YE¬ÿ•æA%òö÷‡J,Å C2€· Ç󤑊ŸôÝý(õ�:<(ü{m4ºÙ6;í´7«¡+FÎÙâQÙº]é#ÑÞ,íÍd XX @cF£®¢Fa­jžœÊ6‚jMºçÝc bÙrª÷Ú‡õd?�@)$u!™AbÏef={6ò™‰K‘Ÿc`žÏ¹[VÞZõx�ÞßHXx9=4Û· kæ´<Ëe4MrRg& ç´ðÖ€~z^Qúë8·Ï„šM Á ÷Ö¨&¯¬% �‡ïd‹j£ÖÂ7 °£IºŽÊª­�yGv åqYÔe­û“¼w›ŠOM‰PÓXŸ¡¢)‘·æH¤§Í‰€¼G¾F8VØqt0„mB@åv«ŠXc4åÄÝží5ÂÑŪ€ÒG®"9£R¡ØP‚ºÌwþµc+wðj¨»Ä+â)î¡�™UÂP¸­lˆ$šƒEÞ.9…=i£(›l‰Ø Ú×Ë¡¡*ws{Ÿ~ìCâÏÃ^bÀT0¹7¿xlMu~lùEdjN‹Á«Êx“‚ýÀĽýMËOZ˜™ß� ¿3ퟩ@™!ŠcÛ ÓÂMW3EGœ�~“Cmçä…ÎL�z‰ç&‡¨“H=ê<:×Àv?q†â«p‹†¢ÞüŽT4¿í¼Ñ~ß¶²<�ÐÎk ÂÞ¾)µt›&¦Æ<”NýÅŒ7¸‚éÜ~QëÐ4Ä]?¼í…¿ï…Óø¶Ç|ý욉�5†Ç".C‹ ª 8جÇÂ;Qh„…|Ø!#TE¤¡ *C{21z~H6�.‚Õ=È‘×Ú`v £ñRð¼~³Š¦sˆ{_G—€®Ê~u2 á[*šÒ �Ûî���~…•Nçz𖞥.Ýcò›”ùDÚ_h²¨›âº&ûõtÐö,,#í Ñ�T�¬ ?“r{„Ewò¾í/b|—hÄ:‚¨¤Ãn~E€+yD„¥eFiqE'$ö¸M•w«n—3”×JäPy/ƒj·´œßË\~r�ß®�Œ?&é¤ßȉ”»£"ɼvúéè¨¶¼+ÜÜŽö~ð9/ì½½ímÕ…ô­›Bk׳Eƒüïæ}ïGuøÙâ …”¯Š�GO¤_lŽîÎ8­‡o9$¡‚µ¿Ãù”ßhÒmvËèœžŽž»ØLÞ$vg¢:ïˆçNªXÆ ­8‡òŒ«Ð�ÕË­&"Ô@-å6޲6<_Ä\rs� ½™—×= •”ù¬²Ç }Ôµ€Þ…·@ïì}òùœ|�#õ¯ÎÚ¿ˆË¬y1�AÏwçð„t.EêO>÷Á࿊B(ø:s4Ü‘¤Qø>ãBËn"zF;B›yý8$ -"-4zgjòâ‚M<1Ï2‘ø›Œºƒ�O#ñ†vwH7[a¸*’cY4݈o¿4ÍôDæÔ… Mƒ&ôšº\óRQ–§K`}ó®åîš‘´û™»²Ó„ç4�5îo9e e�ÃEàfuîc¼ Æ¬²�À–Lhv®Q¸…Dí´OlV/9²³HjíE ¯@pnÎÄw] ¡|ýôÀc=m’Y ãÎrr«»!d‰.»£¦ §;9 `å^rçŽg¹ŸÖ ©fÝåk\Ú5 E3òÇ,Ë…Ág1Ÿéš¯¥<•¡-è½±•ä„�ß{zç0g+h�ò¶Ié„i»lWþ4SŽœ2õÞýJ5ˆš„Öl›¬d‚W�¬ •Lo!mÍîÆ­lì@öè%r̦�_Ã}‘yJñeE乬ڦ·? ¶™”Ó¡F‹:s‹røŽÊŽXŠO{TìÚëI缕øåHâNdË=4šV ²YBEœÕ‰ð…�×½ë øÛMKã|f…$Æ�%÷Ýh´D—%S �ö®ØyRh+>f¥Ð®�Oœkÿ×î¿$þ ¦[¹ ú PúŠR‚K•(¤xœÍYËnã6Ýë+X/ºŠm$ÀEê(Æ ›4hƒ³¤HÊâ@"U’ŠÇóõ½|è-käÄ ±%ñ>xϹÊ›_v~~ýúò¥&϶Ѧú`˜ÂGÎ F$ÅJ3ó°(M²üm±†û†›Œm %ãÕ_œp,v\ÅãÒp)Ü/˜2œéÍÚ¯Œ6ë 1–ôhõßnµÁ¤8Ñ÷hŽ&‰6 œ³‡Å �•^l7kl Ü�roeuÝÁ’‘ ký°H?¡DÃß. ( ’žÜÜ}åÓmâ-¨ŠÁ ج «t‹ž¥ L…3« ¬ÀcÃTäPþVÙÍõòÓÂËü�„KÁö“7†4ÁVè g%sñÇ4ð\ì‘I™]Ü2!{3BÞ[DÛ›Eè«,�Û˜¦µ÷;ÔÁñfG¥¶¨4‘ 3/ù^ä`i¼_ƒûs`àûÏ�®» ^Ý’¿!žl6…Ô| Ú©õáÑ�æ‡ß;{ áèý´/žaå 4ú"—f¨yˆÑ (Â1wŒnÇÐb±ôƈ‘ 邞ë|±l8'ìDHÆß@0{M¹nü‡ï Š×RŠìx&ÏeþR‘nNiꬿ\ø{jGJV™Ç<ˆP�#ú'!¹Á…a{VåH?ì#j{yMbrI,êXΣ+p94úzGà°ÎBcÃòú>6éb{÷«á ä÷ÛÍšå[DX–!¬vÝ¢Ÿ.¸IÃþeÌVD¤0˜‹¢*…p79k¯ÕŽi¢xaožXKì °u´Ák=þAˆ‘N¥2ˆv½;Íèãpþc‹ÄY©ç%®b¥x¿Ñ€ø÷Q ¡ BÝ#U—jr)æMT}ªÛ›M'Z3pCÄžMªBñ1 ÖÁ• FŽÍ‰XБÙ%jMégãU•‚à™ÙÝ,¾Ú:‡Lhžº6§ 8çâð¿Å¿×.ÍÐJ`™î'Zä#WÚô³Þ�š™<wcY ŠÕñ&BšÁs:¾¸,ŠÖâŸôšgö²rŽ 'õÊíÅ#TÉTôN¦’žI¡'Pbt‡Z«/G¢ŽÒ!‹3¼÷0V«N� ÓìÊäžÃÔÕ=–´8tH!‚€à¶Œ7~€SB^tà­ëëgù†<%s#RW(ùmåC˜š§RWñïõy¡nLC2CàÞÝzU ØtïÆ’4U'ü_½Ç{)€!«u¥²£Ï*±ãATËÒFß�+îö�TÖø¡˜™câT�VÀª¶@µ]`—%VùŽí¦“2³ÌƒN…çÆ: ‰bƒa³OcÝÑŸÜœêZ Wmþ½½K}c)�[ÞºÈÛˆú™‚rIy½±áŸµ‚È1ÌZŠ¡LÉ«$Ö“~´›:'«‚ÈRªÖº$©bÔ`é5áj×v;Ùž�.]SfT›¬é^+ûÝT½ÔaÒ(kÚ$«Ÿ«ñƒÚ¯GÀr0@ö,Û#á%°›acpx@ý‡+( “ç3c0¯§j²p ‡}U—×ÛV3L�£á®Ë%êù C=<Ê-&ëEÀ>�â´ GƒyÞ5ê=°½Ù1Ƚå£ÿËu_=Ì~bn?YŸ÷o‡‡|«plPïán<´[Eˆ@xxx—ºQËp’õÆ^tå®o¦¥šìÔÞ0Î$U¬¦¬—Ýä˜!Ý*î‚ùØö‘]zà\äX{‹ Ù‚¾Ôö’Ñ3£™ÙŒg“7|)ªÚœã®€Œi‡£;Ï_)�ÎãtM ®û$MÔv ûô¾�o›ŽßüÓÿî8úR'}ázצo}™f%¼ÓÜ[ë².°¹7lxz¶×òþìüâÃâûƒ·Zîàò=ð¢™F.탛¿ ðÜj6¶ÀCM‘Åhª«æ%Ï䋦/Eá·ßsÐ-ͱ±ââéÐÚßy¼–금[r…;P˜l‡s¸AèóÙ“ãµ–2;®®z§ãy.ê?žWi,¹ndie§…ä¼à¥Æ¢£åô`qÖ…ëÁèé;«(÷ii‚ÓŠY^Û‰)Fݶ>ç¶ŸVj‘óø¹ñ\/§3=Éò@7ˆÎxuO®¹nðYNñ‚ÏòN {N@$O|à+V×\BÃÒhX²rg4ËFu9Ùî�dkD¤¾¡Gdy@¾ši°‚³r‚ÅhØLV„&³4çÙŒæ\ÈH�,†è+yîG=…YR1Ñ04ðò¦±Tç‡f=�y Ùiò<½‰J¾Ë«].>M°R;ö†*A鯡±âóì…‰v³³Óy.²ªJ/d¿¾˜­Œ“Ìè‹N%›¿×Í4kÍ$+1Á¨nä#jšÜÙ±¥÷5+>Ây¦rt:ü;TÕ{ñ/ \+©Ê­Hy™¸­ à’VrÜvFŽ¡j²‹‹q+™ä]yuÇeÌÊ46Ø;Š7US¦L üî<쯾Z‚YgIÖIûÈ]zæ‚f˜¥]s4n�;¡³˜¥© A±¼ª¦/*V5OÄVn�9­€ÄóÚñ± j& F¹k¨4QqŽEÓ ý˜P³Mί%W58 ¶Éœæ]o@l‡‘új³O›J¦\¶�ª0s©Ð'^KQ0y@$h‰Ö—êìÍÕíëo®ÖGŠ[-uŽyýÂ� ti-µýfû†tÚ'm+¹·y�±gmTè4hè¿�UÂr&-Ö&¨M¦±hoÅ‹ÖۙήÖÏ_,naL^¬#g�ƒ(wf†»öI™3ç{n=sÄÎ7qü‘<4å[ÖäÚ1qóâFKk‘¡Q!í�8þ�]iâœ>ø¨øŠ¨ ÄŠž=B’ã�Å *cø“dSçRÇZèb�2ºM/w™@tb)Îû£ALJFþtðúð•x› �RFG :Êe òŽå°PaKãžíDýO¡ ÄÄ· ¢±ÌX/û©ƒFm…¦XÃ:S¦ª¹ ‘wµ!à/j$!£I¿¸r<4Ývx:wkœTâשð½}j~{¬¿�&箲ÿ0§°ï¤«ÔŽúð3¶£¦7¯ôëbØìh&Ï‚7� ;Ã<Â_³ž¿¶ùv߉w~À�30ÛŠC³1ÜC¿©c6û/>û®?Õ�`Lÿ°=JOÈ—š  —šlJÎêŒ G2s¹kspòã™ø´ùÉÇò”j€ÏDQW ª=¡¶(Î0¸Ú!oz°F|NðÔ€Væ€'�€T�¢ÜD¬ÿ�¦A#òö�‡šÏÅÇ;>È΂Ï‘F~Öw÷“„?èð ðoì­ÑàæÚî¬ãÞX"¬Ž]1²¾ØÊ– Êv™OD{7O´w£�b`€�‰9�º‰Z…uªyv.O˜jéž…Ä<2åSïµ ëѾ�PHjC2ƒÄž‰Üxöd¤3‡"?'À8_s·¼Úõ8PÎßHTxY=´›¶ôkf´<Ãe0M2Rg.Jë´àÆk€|z^Pú œÛeBË6…`†}«e›W1% �‡ïäƒb£ÆÂ7 °“IªʪŒOyGf åqQ6U£ú“œwžšŠ�M‰Z(ÓZ?FS!oM/,�HÏ›y�|�p¬±ã(oÓt€ÊÍVÅ%ŒÑ–·‹ ‰Û=Ûi̇£�  þÊE®$9,£J¢¸�œ¦¸âwîµe+¶ðj¨»Â+âÉï¡�‰UÁ °[Ù1’h�Y·ä ö¤�¢j³%bÇk_Í„²ÚNënº‘ƒ…?w =à¥ÒȾùÉ¡*Ïfô¨¯lŠ Ê'‘¨=ï¯:gmêuSûöW,=}(®2¿1Sš¿HA2A÷Æ´ ÆE¯^0Š*ƒÞ|›3Mgä…ÍD}:‰§¦„(HœežœW`{9qµ¶A?Qo~ Íï:k´¿w­*GÀ·ëº¨|7o*%ì&‰© ó¥R1à ,o:»?4Ê÷~�Þ¶ßÝõº_÷ºi|×C¾}qóÏ ‚…%<=^†â9TAð¯]��s’Ðñ°#F¨‚H#\iT$šö`bôòÖ“l;Y£{£h”ÆìFÃ¥ßeýf�çû¾*O®ïLUýjä(\€~wT$[dé;³á¹ÿ€N†•Žgyp–ž%¯ícú«”ùLÊŸi²(Lq¡É~9t= ÃHÙÂs+$$ëÂOgÌÄaÏ­¸ïz½s„Þ%Z±N )é0̯p)îOˆÄY•SZ\Ð ˆ9N“ÕÝ"ìeúrZò*ïePe—V°{Qˆ/¶½ÚH»c� ýFîÈ£Úž¬Iæ­ÕO »Oœ×@ýPmuWÚ¹�ö¾w9Ïî¼½ÍmøÎI¡³ÛA£u,í<Õ*²­ËNI½ ÕQòÙJ‰]ÁÜÈ@xS¹¡ s%µ �‘å*ÕÁÍÅZº — ƒ4Tíö·K·´f±;‘Ù‚£ÖÁC£ƒv—°önŠ@<{¬çL“ ±Ë´-ï°ETED¨·€BÌîBU£xx¾àô4€=ZWœY4�PZj&;ðwz·ß<µ ·Ö§óq™q¬~aÆîERåí‹{©«‡gd³9V}qÉóGÿ…—\¿cKÞÙA‰ïnº<àB¢c4²¥t©Û�CâÙ º¼@ƒ×¨Fo6˜d“°<ç©»jS;1§ldnß÷)FŠ„Ë2=•EÑÅHðvÛIÛ}OEAýSÒvt|s*äZT’¶ ºFøÓuû:î¶{IÛ§¾«‚®=£iì¨1w-É*i‚Ü-r9o ×Ic%ur¥évd|O1¸gau P.™½ÈÈ� ‘u71œÁ¹=4߆|íûåA§J§’F]àhW…ÁcH붦4£ë:)é^0눹²Ö ©&]%åéoíj‰jyc‚ÍüÐ N‡†ëèŽåd³Ù÷Þ˜òsĽK²ÔKù »Dtƨ!Û…;òGQ½®x¿¼õ¢¦¾ÛÅ~‹‰L° *üÕ¾üé-¤+ô혓• �ÚžµD–Ùø­¿D2M)®‰—E×wçÅ&{c:ùèpŸEavQÇ1‚æQœáÓœ'Û|Ê¿‰ÛŠl¸ûîÔ"†l†P™äMÊwº·mwåi^D‚Ë„`Ĩ Äá>ŒCCr^Åa®Ž]&mvâàcRÚ x|²\ºÿ ¸´ÿßðº‘E# – .–‰‚N’,'xœÍYm‹ã6þî_1͇²…dÓ]î Üeå¶ åºp¡p÷A–'±ZÇ2’œìö×ßH¶¿%ëì9PX6Ž<óxô<šÑÈYütÿׇ՗Ç? 6Ûd,ü²ˆ>¶hð˜)�æn’›õì·ÉœÆ�0 .3%Ãëφ… Þ m”s#d 4ž¡2õb^X‹y‰ÊèÙâß,µa†¼×ï`¹ )ÛâÝäˆÃ5K²˜M–‹9³¹=baG˜·d–O˜Öw“ø-¬5ýÍè«!.££“|çc{†E¸,áBz°sZÌ3 ¼„ ˆ4Â'¸Z ¥ è˜eS4ƒê— °�Ä·±Õ³·“ÂýwМ%LÁŽ%9˜A±tƒ°ÀmåÁLÿ‹ûÔNÿJ#—itAí¾În¦ps\<íC‘ëà5²St—-Ñ‚TÛ°íÑÈj<ÍX�dDyM¡ʤFì°©Tý¶‘1Á`î]¨}ÔW̯§>ÂdPÆ8»ñè/áºü)9+J÷@ ^¢>ñ€'êUpœü2Гô¿žýú—Od9@ˆ¶Ëxšt‘»ò< E"È¥Na± O+U‰;äF*Ðr±~öJ9*K•²#Ï @†ÿ�7åË*ú?]+êf2MžÏáS¾}ô‹MP a?ý-Ø.÷d¢Ûw«ÜÐÃ’C¤7蓤M{l+‘‚“šŒ©EÅ€]€CÄh:Œ§F·G{ã,5:»ö›Ÿ� �÷7å¾�IL)ölaÛé¢a/L È8ýKÐV ®‚Ú²´’ȧk&gáE¤ºGÍ•Èìà9‚ÕÜ. [½+^íöÿHBF¢T¢ftg¨ü¸œÛ"qVêÑ÷è×KHQà~TC*ˆT÷¸ß¥¹+á÷©æÞtXFS h{'©"´e¸l*ÚÅÅ ¶Õ Éùžzìë8@¦GŽWÓnï> :=å�LŽ7;tv>s%­Tž=×€Et0oýÔ1{ŽÔÕ]·/*ô…cöræÓµµ8\>»Óqu Â-’f§oU-�1³?蕇À{Íz<â ]æ?&lkb­š3N«�È� vÆ÷4&f¦äacÚÉÇ>¦ƒq·¾rÇ”`)Ò¹÷x] fÖÁ»îÚ¥ÄSÕpW55UC¢ìÕeôM³Œ–Ïé”Ñ"Œ?D$ûÒåPS/Ò¼<è�âiPÆ´]. ]…ÜŸ;Ú%ÏÚšŒ×±ø´êžÉö± >¥v(` !Æ$*B VƒðÐebZ­šqKZšåæž zAQÙŽXÎ�=�Û=9*E1Æ/í3jæÜäÄk+=¢ ÝH Þ&šŒkøp›Ù7iâ_\4ÒgÏ´ Ž�Ü^ULŸF¬•ßêmI¶‰ç^©óƒ�¿OúÇò,eçå{úyñ{Àwücª�N‚M› xœÅYaoÛ6ý®_Á©_RÀ±Ú†B°%Ë ë$Û0 CAK”ÅE’râþú=’’%ÙRœtp $6EòŽïŽ�wG9üîü·³›¿®~&™.òÈ Û/F|LSgT*¦?úµNOðôk®sUR,æ×š.rvΕ–|Qk.Ê0pÃ^4j"Y¥o#¥©ÆT«dRó¼�’’ì£?1Ë�€�ï¦a`Ì +çT©�~öž¤ §xÔ�EÂxÂÅ´ª–³aP¥q³¬tÁs®×$鉱ø—Åzî9�„¯Z…:}ï;?‡brY;Ú¨"±(F)©¨„w4“jF()0-÷HÂT,yeDÑ[&DѢƄbS!1S=Žº… l7kgìâ jE󲵚v@J ž¯=˜-KH]&L’œ—ŒJÈ ^R3,YÉ$ÍùW^.‰Æº¥�ÍgäŒÖq¶v¶|b+(믠æ„\jhã þ•%3ÂK«å³ÈiÙxøúMãL¯?™:W-Ö0ISžC2aä$åRi¢2Z±núk®¨Œh^e4 LÓ=²b³³Tg~tòfFÞý¬ˆÌÞ¨[v_2¥È‰bp[2¡v�“ö˜Ö¿OßÎÈÛžÚ˜æ=-NÉ’�Ž¥dÔtë .øÊ¤pN¤ÉElÍÞ–ƒZCHF(þ[™M"÷t­ˆ$¶+É3(Ü*®MˆÉyD.¸††m ÚòµV-Ü)×f^«0­Ë¸e,4Ymg-¤�Â{®3’òp°3yEóš©n¯Y¥ ·lß2¿+fq™ó¨ekœ¬G�@N¶™3ó¶w}¶µƒ³�]yÝbê;섃½)‹w�ìbˆ3F‘mØX,—‰äq�̱›�W†Ù…–Jv7è6©]¯¤£l2Ð £<úKö(†@É0\ÕP —uÁJ„YªzôSz‘Cÿ/Ci{b\DÊø2Ë×DâÈЖ`+¥Vl¶.# ·Y©èö$eB ÒȽ&UXªšPâ +¤¡È ÌØ q€ìa/à jã,˜†hYÐ^Ô¢ù-Ëy&0•�ŸXLëŽ ––  3¸²M&ðL**Ñ:Š^¦Äè-áyÉÙ€ÑFY“ P„ÐØDm“­;·ÌÈ}ÆãŒ¿)à ӦJä X•‹û6¦�ÙÊ©A²�TzF×s%À˜y.jÒ…IôDrŒ±Ä‚a•Ú[ NS’I–¢úкR‚€•ó{~Ë+l2� ¹ ÌSàNÈ—dPà<[ÄUD-ú¢\F× »œ+�'hº<ÒëË/JZUpüÜ”�¯’ú[)Îà…["û=J?*~ÑÝÉ›W%©5�çKÇ­tü-Ò` “FãùÒ²Lœ4û¤g;YxÛïâÍód͉t¦Õ#D€ Ô•Ëg¨ÝL|kêãìûh£{"ZA?6Vsæ,ô£«M‡Yš¾7—#g¾eê¤)°Ÿ«ûÕTư‰Áoóíêq³þoöÖonjx‡Âh²¨ßäÒ]„×û*ÁÃDV÷]j�7¬(Æ–~[Uìú´S©Sÿá3õ£íž1¤WSù¸´‡ý¹.®6÷A?<ŽÁÅ„ÒÚàyx”LÆ‹€9|ÅiŽó¼»M÷ÀözÇ ÷†�ü[é÷0»ŽQ¸}²nßùF"sRçá®=Ý7£ˆñ€„‡‡w©še’yïa àEN—®ºlg "Á nÿ™Àå†Ó2f= t�cà»QÔÑPùÐVÝ%ÊnŒ½+6 /Õ…¹ÄöÌhz¦6@Ùغù¾âKss<§&›mš£ÇͽØ:‡Ó]Vìu‚+A[T=¥Ls·¶¶FûÕ=ýï­¯uÒ¶Önêì]vðIóΤ.êçxµuqj¯sääìüâõüÛ·ìIh¹…ËŸ€×ÝCŸ‚›¿ð; Üwr?lÿ®¦à.j:IË%3GšN¿ø=0ð‚QÄx󹺙sl¬¸h[´æ{?^3븈K¶ÌÅ7>?Ú4÷áÆDÇg7½y�s\;lÌŽ¹5�½ÑD”)O˜É[mUàRÀä ².+Übªýfµy_ÖE•«*U.4 âsônƒþG(ѾÙ?4F)RËï¦1q KM¸ë±yãH÷ëב�@N.àî{Ì„_ì/aÚ†lL!TJósK*ÑÞ!ËË ´}«5Zõ9­4àS‰˜­8m/CÇ‹+mMîGmë‘;;"P”á~„�}Þ”ëOw릒 š_ã÷Sÿ‘ž{; .M.Œ‚P˜: {xœÍZmoÛ6þ®_Á)ØÐ�…µ(6tŽ€!Y†] Ù†} h‰²¹J¢BRn³_¿‡¤¨GŽìns Ä–D�Ïï¬Ì¿ºøõüöÏëŸÈJyÌý…Ñ—‚iJ’•Šé³°ÖÙé÷a„vÍuÎâJŠÅL¿ Õ\”7 ÍÙWZòEmæ‘ æQ£n!Ò£üÛXiŒQš'ê™Tù`NII vNH‡ñ<¢fŽ×Óð Ì+’äT©³põ–d §xÔP“ˆ” pÎÓ*˜ÞœG•Q/|j¥É�®SVêo¤º¯ÅŠÜZóé‚ç\?�´§ŒˆÅ_,ѳÀ©JùÚ#-ÔéÛÐiÿ‘8œ“À¬T£’$¢TèUDd¤¢~ÕLªW„¤€\NR¦É+3­eJ-* O)ø� I(ɽ]jÜ®`«aÞ"À¿]±IEC§p…¹�_ó²µêO %õŠj²d%“4ç3…F°žeJe:5ÍÚx™äuÊËe ѹÂÂìÜ6#WÚÀJYÆK–bh+tb™¯©Œ‹z™ë«M5®[ñeA½„™,eKɘ]¨ 7©(œ`ÙèúR2BñQlmŒ&Ÿè¬$‘ŒjtíÅ•`ƒµ 9�É%×P5tµp̨•ñ–ñ³›)ãÚÈyöeu™x@“Õvî± ~âzE2þîì<´¦y W´“Í,ýȦ¦ùM1‹Ë0_Ë:ÑàðNÞ /†ëŒ,Ô`E^z}ÇïÅñàIó7h4N›GÙôƒ�k �æ•HwóÄ «4îø<˜>a¥rвބ &ê±�²Gt L³i9ÅKÒŸ²Ç1Ä$†îª†¹¬ xÁAõø§sõ-¡7‰NÛ½ãvðŠ/Wù‘,´¥p?�Ôj �­£ÇØÍÔß~qá—ƒ›8(Íf·\5Ñ>P¦‡"3c/†ä`1{á²–�–Ð ù¢‰` é½ãnB±. †à_Ãl &àD^I–áàÔºR•³Oü#¯à :r™§¨ÑûõëïÔ�>íëYbTâ‚\ͤÏÓl3[-Xë1MK�¾ T¡”a)¯fùK´‘04åÙà’¾EË«j¹ÐÐZ®WFÕz…žÙŸ2ûÛÜJÞÙoÏ— £v¹¨¦„gðU}¡©á›Qû䦿êu]UDØ�RÚ ½åEU+‚TNPZ5íÊB—lê25ž½}÷ýóóÄÎèί¬�g/gVŸ�¾�G ¥�4p`2Ä7úcb؂הQµïbãë?HªœºV;ÙaVcE2„•Q¦¿ƒ^Z&�™Ïú¿òJ+Ãìë¡ ÃÛ.H#„¦¬ÒiVŠ 1�ã1"ò8VÝ…¬¢÷Òª:Êè.mh¹#*"èCGñ§…I TÝG ›—Š2‚žyœ–Sš^Ïýç‘ôJîáW×X܈܈ H :$Ð*8ð€×`aa,,p¹5–ѶŒö{‹ ` @ê:zË ‘c¤Ã ‚Ë�i± \V„.³:ÇùL�™ÈI-£4ÆK~öÞ†KbÜbôÑ@¥Ãñ¿oâ2Iˆ³*Çá¦^_¡K':G·9@¥ÕeF6´$Ùh¿g4ÒóZpzß[­c½¯G}ÿµq ðNáÀ8•dËø–ÑO^jd'pT š~ �(ç/hOS;œ0šsžMä¿.Ìc”=°äÙøÃ¶ö�*A‚¨Z”=HQÁHs êÍZvåõ¶-¹Bªôñ«8ʰÂ#ÉQa�‹”FPÃI>¢|Ѓ[64úâÂ#(;£óÄ{0UçÁ/ÚôMy¹¡)SwƒË¤Åà 0XˆÊh¢‹q+‰â(ã·�—ã2³z‚Ö‚=~w<ýæåWÇ–`V@pš·h™LŒ\з4kN†rKUŽp–QWâØZG"Y‘”n(ì¡õ>iŽÔ‚œ¹yì¦`F±­ R*‰,Z� `ß#.4Œ<DV0DíNó¬#€¬˜6_¥·°ù¶æ"ƒƒÙ•…«[]å• {Ø JÀÿÖ—©üjvùê«Ùªg¸åBå¡Ì« /ØÒ -”ýd[FuÖUmìñô�Î,ž6»Be¡ ÑÿÉ3,\ƒŠ@»Æ&¥KR4lÇ*Ÿ­ž]Ì/Á™¤X%Î!{ZnÍI·%ø'ÅÚ�Œìˆe怟m†ç(Ùàš)7ùÅùÅK�Ö#™ÔáÿÜ.G[â> \û]ñ€t §z÷ì`krø‡QJ> ¼‡M•ɾÚ=¨#º /·9…Ý KqìOŽ,òÝÞÛÃ÷Eš`Ø ­‚¤Ë0àŠa¡Á†žÍìúÞœp~é<¸©a7–l?pÊuO8(:9DÜRIz–qµ'†´Ö£0 h·y$ø­{8Fõ"ªIZûÎI5¾·Ö=�:á™@‚¿m÷ázLó¡V�Ô¼ºþ3šW#ýʸŠq,ˆ�˜î€ ÍàQ¹Í#ÔŽO`AÕ¼CÕГ"­^óDîÉÔ�óc©'Jzôo2’ü&âFdè ºI ì‰ÒÍàÐ"L�0ZŠ&m3©Î¾( Ã��A°‚à:GX¢Œ§f´‰R>óøøQÿøÑ­k,µßÄ•zNò4‚ÿÖ/�üÖÕ¡Ø>ùÝñÃO3ØX-ëb‚½ñEùÃò)ÌU1¼ÚôNŒ`fŸþ�Ñë?’¨@Í?æ?Êò“„Ö î‰É~†¡ Ça�Òmá QÑD(“àM¢#íédž„ -â“èƒæ $�"ŸBÆIg|€J�o �u䕾êhÐaÛ<áÒT•ð ƒBÅT†ûþbŽçáÞuö«¥/a{u»-ÛÛ’ýÕñ’]Ë·¥ðåÅ%"75lœ’¬¿ {bÁóv=&ÿ¶a-arr^Bæ¶&Ú"D*¨9aÈÜLôâÒ«lrXc7}/j©`t›Ã'�6ÕâÍ<Žö9/ý£&ƒ€B‡wÓˆÞvIÞbZJ{Êû3¼e9bÓ?a¥ÃQÙ®¾Þ6À,qæšüKÌ÷„ü‘.Kº쯳AƒØN$m=´¡�µÛOåXµmÝ»ö6f ˆã§D«<†ñ6¸ wJPΙ‹s]Q›® TÔó°*óUž ˜¼A¥]Z�ïhA?Û5ô‘rÝœ ü&®sZû+„ óÆÚ'°Ý'B*i²l~[Ú±�õ¾÷1ÏöÝÂÃmÞyU£!)j¯ÊB§¹7 Γ;7ÐݱuÈz¨ìÅ1eò¹…í)}¸Zõ°¦¯�jJ1¬ËÙjC9%§“AMÿ>¦‰q8ÀôYfGe„5˜F#°Ã2^DÔ"VðW �;|¯mââ;~·uÇé/žØ=H9k ¤‡S½@q6»äg”òÓHIdÈê°ÍQÀ"àsD±~Âkn¢�P݆D'[g ‘Æ:zWÛoam8ƒL®me6MëTýæîA7",4©ÖL”Ùi;Mª,b›�Ô�JºÛ¨w› !Xß?e�›ì¨�4ÓÜä¶ÀUÔÝØq(§\pé»Û“^ÈhÎÿ>†=P5·T÷:!]¡ŠŒd„PÚÁfŠ …�€*äF«w(P™§óÄÔ:$úàôMÁ•¨Gù„>R<ÐSìF~^s¯Ñ¼»¥k8}O™¸ú�A¾¤ ea  IÁ�š€"»�"Få¸ðÑì�i‚F þD�Àiop ‡Šû"Erj¨X¸÷Îöåöÿv®õl  o o‹ ‚Q–¾xœÍY[k9~Ÿ_¡õCŸâ˜¤–Ô1,õK·Ð°ÐG�¤±µÌHSI×ýõ{t›»íŒ×^B<�‹Î÷�‹ìå/ë??½|ûò;Úš"_%ËøÁ0…�‚ŒÈ+ÍÌã¬2Ùü×Ùžnr¶*•LoÍ’`Ã¥øJpÎÖ\ÅÓÊ>@ð¾dÊp¦— /‘,As*éÞÚ¹[iâÚp¢Ð� š,1¸`�³‚·E5[-ØZ¼?å7,í÷°¶D$ÇZ?ζP¦áo·4IOnÿ!z»GËteu¦à‚“\.J«}…¢0*±‚}¦ÿŽò×h»Ðó3¿ü7¤Á VèçsÀ`Bü'd¶ åQ¡Ìì}R?˜[I†¾šŠ2aÞ)ý½’5zA´½s„¾É ,&„iítúÝÚ ¸‹¤ÙW¥­]*�C�»|# ж±€}L@ Ä <(·þòXµC¸œÜ›±*¥æ†¿²“ yhÒ”Ÿ¸Ò¦_7t8í€å©¬Åj“ Íà=_\•ekñ‰VQSÒÞF×a¢>è¨/qn/©(éôÝJz&µžu@‹Ñ ÜjI]ž\åCv=åxãá�«ÉÇY—Ë Wº‡®·v[ˆ, {Û ?À)!¯2’וú“|ÅŠcA¦™F¤¯ØLÚF†ð5o¤ºâ?ê“NÝ\˜†ä‡€žÝ_Þw«F°3è/Þ�9i<Šgé_Õ5Ç)€0¡ 訲£Ï*±IRËÒFŸë@1§G•5>%(efǘ8$G·¶DÜ/ÐÎ2.��í®³*·”ƒ.ˆv çÆz ÍŽbƒAéóXãõ‡N'@€ÓV•§ŒÿhoSߨ\À-wÝÔåm$ýB…¤<ãŒÞX‹á¼•ÀŽ´`¼S ýdJÞ^1ážõ“ÝÛ9ÙD¯˜jµ…#e²͹­8$U,šÃV¼ÛrÃ[•…Ç[wc‘oÖ‰4«{ZÝÏÂØE¤RL—¶ë)‹Ý0ù>.z(g.=è?‹²2kHž)�4Ê\¡�6ªGÆzxŽ* !õ1Æ"x¸Ñ½íkX_S)Ö¯µ™Ìa¢²w�æ9ÕA_eîÉù*`I�·îòœ9~> í-ôjJØ�´·,&ôa Pkjz{&qA¹�¶=LZY©¬y.Àdá'ΆkÎ1VèW¯·øj ®�-+Á)[kk¤ºÕ8ˆ†²i,£]|á¼›á*7îì£&+JÀÓ�¢‡!Éû~4$¶[ ¶Áî êÎ7e�4} AYÅÙOŽQÒ Ò�… 5fF‚4ßs‚´°ü¬ëÀ"ül¸ð?Sþž„(ë ÏÏ–-‚R¬^:ªxœí[m�Û¸þ®_¡.p—à]gSHïœÒ¤Wpm¸ Eq¸´D¯ØH¢BRÞÝüú>ýÙòJ^oz l`K&g†óÆg†Ìêïÿñîÿ¯ÿg¦È¯¢Uøà,ÅGÁ ‹“Œ)ÍÍ›³ÚlÎ_Ÿ-ñÞ“ó«JÉõ…ùY&ÌYþ’°œ¿Ú(±®éEü7n2™êÕÒ �VKOv-Ó{bry¥ æj#ý}<™æE+—¬àoΘu‘¤›³«Õ’¿—‰L£Aÿ%Wq’3­ßœe¯â�‰D¦.ùûxµeêªZ-é#~¯ÖW–äú*~î~JÝo 7òÎ=}·ZZò«eµÃçx4O,ž,ùguUqõÌ 4ì*~'‹ª6<6�“º¨sH±åqÚ5Ô¦.ûåù»÷?~w¹É©Ø†uúüÕ™£ç˜D†¼“Ô'NmųXnèkd}‰­E.Ì}_6¹þOŒ'×Rç[–×Ìð4fÆ£gÐeÔãsPòo3žç¢úá°JcÅM­J';-$ç/ – –ó¨ÕDýåì]Ͷ™Rb^L‰± :γﺞ-ºvµ7ºúN,Ê-Gz›àÌb–7·bŠQw®¹ó§š•Fä<~n=:Èé]‚d9Ò=¢ÞÞ“ëX÷ø¤æxÇ'uBçP=ç Ò;¾ñ€7® ye5¯Xyc5ÎFu<ÙZÉ ÖˆH}‘åˆüv¤! ÎÊ–¤á'4eÑ5¥£=Ï–4çDÆke±D§X/phô”æJÅLƒÑ„§3™£>×h4ë ÌfÉN3\�àË™®ä7¹¼ÉÅÇÖkæœÐ€%(þÜ5b‡É<;b¢ÛLÝtž‹LÊôDví‹ÙÈ8ɼA°hW²ù{鑿®˜bE"fÛÏ8AmBDZû6t²î œk*g¯ã¿C•½ÿ$�2ÁõYnDÊËÄo¡7�XªqÛZY8†êÉ.ØYŒ_É$ïËå-W1+ÓØÖQ¼–u™2%ð»÷À?½úfßì 8K²VÚG¢�™ ša–fÍѸAn…Éb–¦‚ÅòdE_t¬+žˆ�6]#ã:‰ç¹çãTL@�ꦦ’IÇ9M38ô»Ç„†­s~®¸®À±Oæ´ïzb7ŒÔW�˜{ZK•rÕ<êÂÎ¥>FŸx¥DÁÔ="Â(ü˰¾ÔdoÎ._sv5PÜji²î˜×/˜Ž.Ý ¥qß\ßÄ’Nû¤]…ù6¯2ö¬‰ “vZúoc�¸dÊa}‚úd‡*W¼h¼�™ììêù‹Å%ŒÉ‹«Èä^”7v†7%ì“02gηÜyæˆ�/âøyhÊ7¬Î�gþââÅ+ŒVÎ")BCb[qü ·ÒÄ! \‡¨x@ ÔÍb EÏ¡ÉñÇâ;üI±{[é¡Ú¤ŒïÒËm&�XŠ÷þh¯ãC#¾ú‚&RFK ä2yÇrXWaKëžÍÕ‰úO¡-dÅ75¢±Ìï± ^öS�ÚC±†u¦Ì0T“ Q·µ)`/*$!«É°¸_ó<4Ýu=xzìÖ9«õP�´ópßÛ¿æ·õúÛÔhÒn;×s­tÕQí³ë/Ø>›kô\š9VÇðG"%Ø×™‹N@~>ö9�_g=¿n mÌ/¾Æ�0Þ;ÉÇ%fc¸‰~ÓC6û7>û!2Õm0(þi3ÈnH·† €ŸÙ\FÏʘ5G.ô©o}ïÅÇ3±iÒ[#¤9]Þ‰¢’ä–@_g,o�vÖ#>;pl�Ræ`¯ t]E ¼²°‹XÿYMCVäìî+>V È ƒ€'�<ý¬ïí;ûÂ^…akŽöîÍÍÆ<î�%¢j芑óÅF¶d¯lÝ&ùŽhïæ‰ön4P,Š 3§QQ£°V5Ï¥ Aµ&ݳî9 1�lõÕ{íÃz´-A’º‚Ì ±e"·ž=(M„±HÏ ÒCî–Ë«�éÁÃûÉBx ¯"§‡foWaÍŒ–g¹ìÍ’ŒÔ™‹Ò9­ÅǨ jz^Púë8·Ï„À¥M Á ÷Ö¨&¯,bJ ßÉï-ŽZ ߀Âv&Y¤»WVm­¸Ë;²S(�‹²–µîOòÞ¹k*66%jOcý�DÞš^— ‘6'òùáXaÇÑÁ¶g•CìŸP””0FS/:IÜmÙ^c!]\ h�>rÉáI…ÚDqFÐŒßú׎­ØÀ«¡n‰WÄ“ßA!‹Šžp[ÙH4'Ÿ¬]r{ÒF!›l‰Ø Ú×ÇâF%7óšª~ÆãÐã¯Ã&eÀËTq¹7¿y°Ø�ž`–uqBàù$’5ÇÜàUå¬IÍ~`êÞ~« ÍM‡ÌW&îWi‰“Ö3òol×b\Äñâ£è<¶stÐä\Û˜9E]4Q¯^â©©#ê$^�RwŽSFŽn|IoÑSÔ›ß‘Šæ·�=Âm§ÌÝÂöêØ7R ·ÉbjÂB©Õ_ÌþþY0�Û_jZσ~»k··­ö×û[í4¾ma_¾¸Œù§ÁÞ—¡yU|lÖcá ƒ44ÂBD쨪(Ò×�¡=œ½¼ $›FÁðD)jm0»…”ÑþÒñ´~³ˆÆó‰{/Ë�[Mc²_Í Âèù†Šl‡LCc¸{½a�N¿ÀJdz? 8LaÏRçî1ý]Ê|` ˜i²¨›âº&ûßé íqXFÚ®¡ Y~&c6ö»nÄ]Ûjž#Äþ]¢kÒ’»ù®ÄÝ‘8“9¥ÅÀØÓ<%oÝ–i(Ç/ ò^Õni»…øìºÈ] ÓI¿‘?q‘›�"ɼuúéèî#窨VÞ–nnG{?úœ—û øœÊOÿbj$Pó­ÒŸjùƒŽ?ô¶½E·4hÝÚ¼lö¥Ž=;¸yE=pá]B/÷úÅ��7khµõã.½?N¢WÁ:²å%JE_÷»I„¾œÞ»ƒ9wjcrÍ •äý=ÙoR�íÝvg7¹ÎqòÞƒ(«€f+rNUñÜÑ]œ3CK.ÄMf\ Š}H®5¡ªE·ÕÉÚhð|‘%ÒÝ,ÑèÐ;汕¦²˜Qع '¸?Ñ»a¨>Ñ1Át~>]!™è߉þE"óæÅ¦>õµÏ2ºÄ¯?ûŒ¾îÿÊK®±£áÎ1!“Â÷ ·ƒŽ¸ ê�ìsí~âÇ!K­�A ½WÒFo{Øä æ<õ×/bê‘æt¨�í$4ýCJ2¹Veº+Ц;©`í·¸æD!õ„!GÓ¥ ·.ÓB*ÚºèŠabßÁl™»Ž,méæVvN"McA�ù›\NÈ*äu‘Gíy]øî +©;­l7´%ú¤�«'n!Q»ísŸUK�nò^{9Å뜛{›.…PŸ~ÔÁ§6錇Ñ'<íÖݘ²¤çÝÔ†Ñͦèy+˜óÏ“ÜþkƒT“n]íåË]¤6ª.©ž�aË0å„5]ƒ¶ôÇR¹Å½7¶„±üOïn R>BÂ6q0v—íÂûŠ=Çq½“�~‰DMC»Í ä²IÃa´Þ„®·�¶YáÆì¬Ì홺;‡H;Óį@¢w"[î¡Ã¶ˆ!›%T&9Š�̽î]ëÈß;.RÁmF�bô ãsÛ�OKz^ÂÅaoå�&ͶâàcRší ðøäºôÿKtéþKê`pk þ ÇWþŒU‚U™.åxœÍYmoÛ6þ®_ÁØ’N´(PtŽ�­Ù†}è–Á†aØZ¤#n’¨�”÷×ï!)êÅ/ªì8Å€u¶”ãÝswïŽôì«Ûß>Üÿy÷#IMžÍ£Yøà”á#熒$¥Jss3©ÌòêÝ$Æ{#LÆç¥’‹ë{%hñPeTÝ m”XTFÈ‚|ä&•LÏb/ÍâZåB²�5ðz® 5X"ýžŒR…5ÑŒ’‚æüf2°â:aËÉ|SkçÍL+ ½o X’$£ZßLÒ·d©±<‘lÐÅ÷d¶¢j^ÎbûAnÈl1wêsréÿÄüߦ^òÉ?½šÅNõ,.wÌâ¿+<š4�'§ú¢*K®.j0[ æäƒÌËÊpbRN’*‡u#Vœ°nb–U‘¸/—nzuùÅL¬‚O¹¾z;ñú¼Ñ-ÈÀ;*T$ñh´ƒcD.í×Èñ†.D&̦�M.þቩյÚùŠf5œjœ2û ½¢ˆz–ƒ3ƒÈ¿Iy–‰ò»á�ÅM¥ �Ý:’ñœ.D[î<Ë›¨ïÎ^obäfäã÷�8´‰ŽgóS—Íb�ÎåÞÝÔ'®(V¥k�ÅQ naŠƒ.‡(üXшŒ“KÇ″¦�År"%¢†÷p�B‰G5–�êL„P=BXµ;|ø  Wˆ¶rÑV0ä¢LÆu4Zd€uR?é°œPÇNH^Îi12{VôLéË»éóz�ËŸ]s¦„µXœÒ1 Ö·6bôR)bâˆ$Yá—I“×|l¢ìªH•S;.YÁ—IW9ºA–gë�›n¢Êã‡Í~Îzyâ…¶ŸmK¼;¦#¶ÀÊ“fº»/8Ó“ãLš±I†è3ŽH§Sð¹]wNÕu$Ez/~·ñ{‰ÃJÚ£o*ÌÑ;d½ƒÈ@=© 1†¤ÖØ¡*bÿ¦·ÍìÝc)º¿¶ �9"D—<K†SÆ„5K³ˆÈÒ~Ód-Lêv�·e5^ÕZjw©P„ª‡ÊžB4É[gÁ3¹ÞIC¿R\—PŽÚFÕ½ë /fQB™ZHŸjuîÖÚk€¾òR‰œª hdþ¥p�™ôfòúÝדùVXf±I»2ï¾ 2�Hy¡ØøoþÚÁ©f}ÕþÐfIv¿)ùEÃ>ú²ÎÄ÷î„&˜àÈ –ªNÃF¾˜B Î`PtMÂyµð¢Ï²±e7hHöjèžgv¸“Œw†`ÈulѧÓϹžÉ‘ÐŒhü�*bdð�¬S<öUä൵\…s-µÄYÙ»S¨Ýy™(|í:02wÏS²Øtíq/i¦:c…kTÃñ)±„…zD(Û ?Ø!€/i•A¥0DèÝEd U{±bÁöØŽÜ’Dâ(WT²ÒýE0ûGÊ‹~-°–é¡%QÓáL 9¿£í ñPHl¤‘Y½£ ;{8§¡O( Š–©HtȆõ'�$üe‘Y ‡M;•Å×ï:t�}ž£8éÈš¨ÊbñƤŠ‚KmÏGóåëúµ7-–D‚L╵ɟ™žÇ±+MßAÚæâëëvWiîahëvŠÄÚê%¯Q Cô NuLæ#G/üÌûÇÞ 9h|�)b¼­ºu)¹Öÿãõ‹DfÍ‹óÆ9¯*ðýåžõ'ÿü÷ÎÐó3/¸Â&!^Zµ¢àQø>âFè„ ŒÚÐ�ñ¨Ý6µ\Qå ÔühïñêàäJ"úHÆ™U(Ñ¨ìØ„Z|âÍ\2mê³PÛ…¢í= LçOíÐÃDn"p45+Ôá®Ñåòö¸X@KÝØZãVÕR(�†¶–�a‰Úeô_h£¾×±@Ÿ³Œ‹\£Õª¼n´°­Y¹&Ùª í³�ßjG¢ÖÛ?îÓ:,9…p¦%Æ´Ö±?X3‰½Sn5ÔÔT;¡œiÃFÖ2HžéŒ«»ûÈ©=î&Bž*††¿Ôsò,·-0 u%±Ê—¹ðÃdS$(##óÄÏ”DÓMb«ûP¹ÆÙƒ«Þ÷SÊ�lß×úΑÔh@ù „m�Hp×ì´HR;ã •öhkˆ PY˜\Úšà.™W((®88Ô¯sa¦ÑGl‰r~Ô2;ž�@<,É9Hž‰n«.ÝœÚãj =9×o-|Œª]ϯqý#{ìÍÿLéá†l‚T�\4xœÍ˜Qo›0Çßù·<ì© j§JSG‘’t•&M]²I}4Æ ÞÀF¶i”}ú ”’–H“¢$`ßùìßÿìïÃÝ�åêéñ+D&‰}dz?Œ„ø“0C€FDifn'™YO?O\¼o¸‰™Ÿ*ÌVбÉb¢î¸6Š™áR¶¥Lδ疽ÏÝ{ d¸ËǸôµ!­8Õ70Ôš9Av;é1šÍ'¾ç’| «¾Pgsôy…ÝR 1Ñúv]ÃZãgŠ—�PöNöÆÆ·/ðÑ]€Fž›æŽ}ø.·LAÌn % c7L9esÈŸíȉž^OJ‹9hJp x&qÆ „¢ÿËÅLÄ ®ù”ëü–¦ Âúžd” ”2­ åÄæûX�Ã,2��JS¬2`h|#&Ìl²‹1¤°Ha1.…E…GFþà²QRŠj$iÃë;H,ÎKb9�Är\Ë.?Ótô|Èj>ßAay^ õ‹ì=JÛl\FÇÞ;’�‘€ÇMêkYÌùt-xfÔH:e”¯w]±¬%³âèÇüFkd·Š¸>Ä�ÿ,S)âÝ€VÔ}L6%RÛë庶_i±Üp,ßšO?5=m#\M¤ÙÃ#ß÷q`PBžTE»û6nùrèà­a‹Š5‚S”nixœÍYÿo£6ÿ�¿Âã´Ó�t ZoÕN7ŠÔ¦ë©Òv«ÔÞ¦ýTp‚WÀÔ6‰òßï=$ÒnK*5Ûï=üü¾™úß]ý>½ÿëö’è, ¿n�¡É˜¦$J¨TLŸ»¥ž�|r=×\§,(¤'÷’Ó|^¦T^q¥%KÍEî{–Äñ½JT(â þ!Pšj å‘úLE­ãS’ÓŒ�»”nà{eŸC‚yÇ/H”R¥ÎÝäŒÌü�@WƒˆHĬƒÍ‡Å…°¬aò½dMi6FCžr½"q‹�ˆðoé‰c™b¾¨ñdêä̵r.ˆE3¸¼¡¨Ä‘Hä f3RP ÓLª„’ ÈR‡ÄLE’È £yLÍŠ”‘˜Â Ï„J=޾†�{MX›£³ÇR1E4ÌDšŠ%Ïç-P=[×4LÙ‰dªÀm,ªÁŒuˆ%Ãu W½PȘɦ«2Ë6×^HžQ¹‚sÖ~ YòX'çîéÙ÷0¶ 2¸­úvÁ‚“6á�Ÿj«µ*ûIOªIïÊ¢R·ÉOÒ‘,YÆò£†À`5¢¶õœ±¸!—ƒ>1ü@$ÞÐo+z^—&%CìÍŒïŠRž]ZŠhNBŒªeö5�ì˜H6ƒd¦u¡>{Ë'Kþˆñ‘Ó‰�s{Þú,âNÞ|›M¶õN|˜ù<¸cpÀ©�7½jÈ!k€m#zøSRtU5ÁjãM­v·uNy'ûuu.?LDñÌ lûl^ž/ /´Ïæ-ªu‹¬+óØðB;Â{×% 3Ö)†_ZbãVBS‘k–kU•=ÉÇ >àß0'£9³kA|op�ö±NÎMœ®bóKä¿ �\¸èò4؈ùíô·ŽÛéãÿ‚uébìÙ†ÕÉ„Ç6u1nkåÅcÀjw¾Âân°9Ò«Í]�67 ‡ÿµÌšÂP¹A§ÛBPv§ ?Újàvû½xqâèp[%u t§ÐÞ†Þš>úþ0eJ »è…Ý6æÍBçpÐï%d$j5¾~>…r ÒÃÁ¼QÕò ²e«Óô:¥ss—Õ5ÕÎr­·Øß7GÂ�%"®äo¶÷ŸdǶäAµ˜‚§ªv6Õ0YQꪺ/3àÕp§îѦ´}7½º~?ùwç¹7j[§ñ=pC1޼~~Ð {k2PkýUã@XÁ­b‘¹�mûð~a9“øåÁ’*%~²™IxßÒûñ6¢4ä6xŒ™ Üõò˜ÊldÁi]÷ßÌët‹Rû6PF°WxA!ŒÃcLá@üÑsb÷úËaõÏþGåqíL v ÇvŽM‚X�"‰xœÅZßoÛ6~×_ÁØ’N¼(PtŽ�-݆=lËC¶aö@KtÄMU’râþõûޤ~Ù–#'N´µ¥ï¾»ûîxd:ÿâï׷Þ|ÏR›g‹h^žà#–³8åÚ{5©ìêâÝd†÷VÚL,J­–—¿r¥tþA«å²²RìgaS•˜ùÌËEóYзTɆ´¿^Ë-–ÈØ¼g�ëÁ‚hÎYÁsq5¿Œ“Õd1Ÿq²ðf‰AãH•,θ1W“ô-[¬�U2ìÖ{6_s½(ç3ú`Wl¾\8]Ë;÷?JüϦ^òÁ?½šÏœÞù¬Ü±‰?x´/eONïYU–BŸ$[ˆìZåee³©`q•W’³,é&cU±ûr~ýá‡W—‘_œÈuíPn.ÞN¼>ot 2ðŽŠ‹=ãàÀS+ú9¢ð¥Ì¤Ýô±©å?"¶A]«]¬yVq+Æ­SFÏÐ+‹¨g¹væ ò¯R‘e²üæpH™¶Ò…ÇNŽd"…… Ñ–;Ïò&껳כr3¦läȺ‘C…s$‰º$–Y\î­ >_e±èO#x+�"n S2·<ÄÜ�/¬Ì;wä­q†ì–'2!:@쮣™ðQ�"ÂG} èHç I¼AÖ.Èšw.¸|0œ£Sß"¬Hý\`yB×:6g¹àؤ‘Ü)²–w³æ•—6Zs¢<µXœÒ1‰ª­o•]ô"™IäØÜ�ä dÇ«=6?´ê2äÔŽËQ�à3d©·ù•§Ùû6Ýü”Ç�ýT v–vÏ»9fËkÑ•OšÕn>ã¬6:¿™²£ ¹§ �J·ú±B;™Þ½_àzïÅﳓŸ:ÒiDõ@¼Cшh�cXIƆZýÌl›…¡Ï>ÿÇrr«‘0öÓªÿ l/,—˜în9æ°ûTÆ)K9fÎ¥¨;i‰ÁËMÀ�g² O›"‘†™ §™—Ê`�ÂhÈxÄR«;Íóºù‘��ÓÉž¨AfJË•Dmñ$‘ä?Ï"¦Júfؽ´©3ím‘Æ‹ %Ä�K`Õw�k Ë™\™ºß“RË—™¸Ð”PŽÙœÒëÞõ˜£Œ”P柖J'B7�&wké&¡¯¼D¹Þ€ÌVão ›^M^¿ûr²Ø Ë|fӮ̻¯k™N¤¼ÐÌúoþæÂ©Núªý1�Ø~»)ÅYS6éÊ:ߺ[cJdkPMé�†�,î|/‡œê Ä ªÑ…Ïútßéú{  ¤¢nœ {Á:’Yåé;ÌÆeµMÅÈs±ÁïÅÖ=„í@»>Úõ`¡Lä¥Ô2æI]FMÀÚМꮂ*C±çÝ#9� ?é¿e=8 1v‹p…f¨Xs™9f«"Û Oˆb¯Ç¤68}é‰C´;D7ôçX ûÝ2uçÂcð×d#ð�°À'zù84;·®}æäž³²·Mr g& OZZ‡ ¡ äîyJí¯CîÐ yfš‚þ­ÕM_™2jPîd„ô;÷ÄŠWT‚ØÎ"†k?V㲸k;rK¨�Ë¢R•é/ ìÜMZ5óL“}&ï …¾52¡7\£‘N' òýåXbÇ1u"È ¹Ûª"V usO¦�&î÷챺}]ààªBåjÂá )1Ä”Ó`‡ K܇×Þ¬\�Õ·Â+²)�ž·3×™›)m{Îl ¿•mOÍ%o]N‘OÚ(TÓ-Q;uôͱS!Nè‰ÊÇÌ…^ò9ƽ{ŽZÝ©GÅñ†Âd Õ½ù¿-‡±ÊšCåÉûË=›Oþùï�±öGQ�’`^f Í7£ñ}Ä-ÞnŸ‚¡�¸-’ WTù�½´÷´¼ÿ° ®ïa³ÈDâg@ÎhDÉùI4_}û]!ܺHv±ºƒíœÃôC;N&2§�@šUwÛ®Õ�òtü/ %l_­u¿kƒmë^uÆPNËø¿ÐÆý$‚�ÝŒøæ7u­ÊÃÖÀ M´Û [5õ&ÙNÆÁ‘¨uÀ¸ý>Ä%çÎŒÂÜ:V–ëi�îÿ[ �ø5׎í^Æ&cZÄNqgaºõãtw¡d,âÍu‚Ý|-¹çâI.—Z`@5êfi?”ÏpK‹™¥ˆÑ;Ƥ­–=Eîl7w­â¡ÖŒ3œÐ½7î—\I¾ úN‘Ëh�òÛvt ¯]³Óp’—{NX½a¯?ÖP“z,i;€û…Àíõ‡#s.í4Úã5$çG�ÙñlßeÄÎ "òƆ¯|Xœdú¤‘n¼‹‚•i;ð…k×CÉ0 ôÂXà �¿¸óNù¹-YBOŠOwMâGˤs×àîŠÑ¾=dg½Tqž¨¬STÄY•øJŽêØ»‘;ÄúØ¢„™1õ±S”âº[ŠNçqm’ñ©~ÇÕÂÁǨæÙðü–9 ÿdæÿÇÉèÄk†H‚W�dxœÍ˜_kÛ0Àßý)nyØÓÓ@at®a¬+ FׇvÐGYVb [2’Ü�}ú�e+þ—¤6u`Ûº;�îw'�|¸ûõíéåñ;$&KC/p?ŒÄø“1C€&Difn…Ù,?/||n¸IY˜+­žßH•Ýqm� åș2œéÀ¯D½À¯MF2Þ—\…ÚƒZœêe u¼€€ »]œÒXý”;¦aà“ršõI/+A´ºF¹hJ´¾]$×°ÑøYâ­A+TƧzãÜÛC…µ½§¶Z�Ÿ—–C°�òŒȉB÷ S^5óW7u¦—׋Jã+hJR¢à•¤³E%þ—‹-˜„AjmF²1È�}TTz·W ð"  D¡”imE«eÕÛëf%….爥ñl¤}ã[‘1aVµÏ>:=†Äsž�#ag$QÛ’°3“(¬Í÷‘¨¾‰öÍŠŽ�Ò×™‘ÏÐôÕ#Z"O9ª´Ãh—ú²%xeÔH:g”oö™�fÍ*?1�2úƒÚˆí)áºñ¯neK)ÒýTEöèÒN��ÐQ˜‘@Ïî0ü(aü0H‡:Ñã … öÌL?òGÌöŠÊ;‹eV‡”I8ŠGWcF }ÃGˆ”“€,;<'&Y„ë�†£‘/W�ϲ(KS J‘}i¶_4vÜ$Àů”•ÛP) áâ@Éé–èÁÃËкcš*ž—'1ké]‚\Çü�_kø?¢H@'Rˆ»ÞMêÍ@ôw¹WL+ÀJå�å#�ƤÚéÞ‹wFÜ©;±F¶Ðª)Ü™Õ=§šL:_nMÿ�I"¶ì¬)ˆöu¿Qº ØÎ¹`ä©’`4lg¼V ?5?žT!([ç�ôŒYÑ6:LˆfÔ{ c45 ®ê4XMƒÞñi†óú_ŽyçŽÌ{®´é׿î¿Hµÿä�f8nõº(üÆî~H·òÖ¹Ž]ìIG«­Ê®¥Bät\†aj&2žšE?t�‰ÅcÒ¨%>cu¬é>%ÛŠ¤“:ݘžO°Tn96bÝ×–Ví "B<ƒ¡ÜÓ?Ð)!'µÁ~ýŽïW&ü€ÀM‰i‚V“VPxœÅXmo¤6þίp¹/wR²¨=�T�R•4ÕI×k¤KZõSdÀ,nÀp¶Ùtÿ}c ì,I£ìJ»`ìyy<3ÌŒ ¸úãòöï›_I®Ë"òBwc4Å­dš’$§R1}á7:;ÿÙ0¯¹.XTË*^Ý žU²¼âJK7šW" ìº�œ¸J·Fê�‘ÒTƒ”'ê#™ç¡R"hÉ.ü92? j¤þt ½°&IA•ºðó$Sø�ãQƒ?©R¶)ŒÈŠ¡°åƒÚH�Èe%4MÕ(ÒXŽvS4æ×[’ŽØIÿýò,sÊ7T©Î?øVÞ/ÄBšÇÐ.w²HR …UEªŒÔTÂZšIuF()AVx$e*‘¼6¬˜)Q´¬ FR ¿B8(—p;À@w›³ž|gk\AP2ØbÖ:§Ú³à‘T•dC%§qÁT»J¨d„}khQlIÁnº"š>0l` ê¢aä‘ëœ B=¢j–ðŒ³”p�Íc›Î¸ÀD N .ªG&!ªäš„P}6a`†­Mšºvž¥¸33–bwûÀfð)¶ašéVt‰dTcéYÞs’ó¾<"×H÷ìUYW5Š‹u»«!ãÚÐ9�Y#ç+Hj¥]:L;�áHÆÿ…yú€±6Uƒ¯ÓRÂèKjîkq™PÔ²I4‚ê°ÈÛ}/œ}oöwNçØº“±7µ#ÏmiÒßS.6Û Œ'ZOÒ&¤s¾Î}’%UY2‘BÐ4jÆ ­è)»y½áÆÁ²�'ðsXéӼܭ…M¶ðƆÅæu#A#Ác,D[A4®휳Ÿ¦vT&T�˜!!4"õ^BäÞ\² SëZ} &V�ü�ר]Ur˜§`H÷�Üût'A¿ˆÝfw·Ã •XG_B¡PruÐMyd<Ž´û¿$5nV+SÕÞtþòÇÑèŒ<‘ÙùÔy÷ ûµó¹å7LÒÌ�ºÁ󹹨Xn žÏ];ÝõÿÑ-Ej¹1XâæN]�pëö’žöux`6µÙ²›Ñ("P[ûM1¡UWëó÷Q/~.Ýc®ÕœYU~tÓOõÞt9¦,™»ŒB�vݳ…¿™#\µ‰Éwù‰Ý…Q†ÿçQåês›] €éõ€µÉÑw9ò{`wCÅ<.°ñèõ£ý™)¸7s©V´ ¯ûKSÞôí™íxOŸ‚œ ú7J\–�ãeÀ±ÁŒó7޾kfr�?w¿:ô’Q¼ÎæºÞМ-Î-^s_Fl¨N�¹mºë娞= á|s”h®‹ í¶¹NöQ˜?`O÷]ãÕQÚG~dïSHc‚IsÊî¾1Q)ͧšLb|ÐàÇÚ‚Ò¨S¸,EN="¥2%)Ûpê:–SÆ´+š~äFÚvR¨ŠÜŒË’‘Û/�"yNBî›� û´ØçÿLÛj � Ã�Š>‚Z•¤xœÍYKoã6¾ëW°>ôÇH€ŠÔ1�¬ —m€]ì‘¢F6 ‰TI*^ï¯ï�Ô[²"ge @�X"çÁù¾yÐYÿ²ýóóë·—?ÈÞ¤É&X—€Fø'C ÛS¥ÁÜ/r/[¬ð½á&�M¦dxýx˜'É–k£ð“áR\È@z½ò[ƒõªPÊèh Ül´¡¥8Ówd’*” Ö”šÂýâ”ÄõÃb³^Qkâö¤‡×¨í÷d„%TëûÅþ‰5þ,ñÑ &£Ó¼+Ý:’u¸A]!štëUfµnÈ_Œ&@2ªÐY*ð + ¦zùiá÷>�Ljnø�bT‘7šäàOJó\ìˆÙûu 2¶)\#Qóh„|“9aTÊhíäüy /ƒÚÿ\[Õ‘4.¬]ã;‘‚0×…Ë+ôyJا„ýqư?†}O³K„Ý©ýhØ/öæÃÜ:…®ÌŒ ôU÷1zAM4ä G‘fÝQßA¬Â†¼3R�ãñ±ÊEÓãäjÊ��€Èð”FÈ^÷\×þãg…Ej)Er<‡/yúR²NO¡%0#½ýðã†ã‡AªÒD¿õ"=¸0°ƒ2Mº‘PÛI¥`–Yá¨B`I8 �¶ÄŒ€t bÎd iõžšýbsû«á¨ä÷›õ Ò a€å‰*E�Vm7i49p³'@þJÀ–¤0”‹ ¥2‘h;E+/ƒÖ4S<³/Ϭ!w äZêûø5–ÿG(RìZRµ½;Ð`Dÿ¶µâ¼ô"—À±Ô<á`L|¥ûY±2bdeǪ1r‰æM”=«Ý§j&�§[5z GÄF5‘ðXŒÖ‡Ò#&EBEtjŠÁ™ü\Z¼ª\0:5½ëÝ3’¡©´ÏƒzÕu;…±9ý›ýÛAô;]Óôí‘·¹9s¤S>q¥M7íýÌ™È27”¹ˆ¨:^D®GÛó,kl~§¨W4³�¥ë8¼žtÔW(wQ)S² )¹—ѹ,zÖLM¡QcûŒ½5%%-ØNÌ×5VÞ��Þº"qN•úàgïHªð÷¯ú±HØÒ:Úa+€VcTÆjH1ë �xÎõI:DDÿ²X¯<§‘ðÛI¡Þ¼ó�‘âpÌl»[$¥B¯""%•‘fR½&”Ë=’0K^U´– Q´¨rFŠÉL…„äî0Ð]e¬¹Æ Œæe-j5�Qí9p /Œh^à"HJy^Kf)(œàÀgdrž2+g:(Xò˜Â9õ 4+ÔŠ� m $,å%KHô@X²¾¥2%¿o«Èž$ {¦9d–ÜêÜœUŽ4-;'dë|sÈæ(q†3xÓ>N®Aw0>‡ÔV28ÃìC� ­ö*@\EßVŸÜÛ÷—C³óá°¥kS·>f[�5ຨs¬ó[6ÞRÛ3yyzvþjõ3¸^nó=£ ¥öAÎýFù�\޵pSS°¥Ÿ¤åÆ~›§ÞóC/ÅN`®Kà�ÌñÑâjñšû2b#ulÌ%Ûäbƒs‘v�KÈ!èxíÄYÎ3!’c{bÓyÌý°yXÌ,¢LyÂÌžÖVÊæÜÙO)ý†qoLº©–³Í,Ø>Í\$ËT¹À!Ù\'�FhßÁŽöÛíó£”"µdofŽq©É€jwÁt¿nÝ $çD~èîSNüÎJ&͇A'B¨”æ«z*ñ¼“7‡rAiTm¸,\i8@eBvËi{–:f¢iËy?lŸvPØQ¡¢j÷C\–‚ÜU÷û‡¶«5ƒæ·×Àý²û¯ÞÁ… ÿÿ•}‚[«~8»xœí[[�ܶ~ׯ`Hí³;^Üt¼€c7E�6ñƒÑ òÀ‘8+Ö’(“Ô^üëû^t™‘ÆÒîØI€kÌHCžsxnüÎ!½ùÓÛŸÞ¼ÿÏ»¿±Ü–ÅU²‰‚gø(…å,͹6¾:kìîüåÙï­´…¸ªµÚ^ü,ä¶)Š·ÒX�oVªŠýKØ\ef³öã’Í:Ð۪잨_^Ë-¦ÈÔ|Ç>O’ g/Å«³©ái¶;»Ú¬9qx>) ÅçU³´àƼ:Ë_°�ÁÜTeÓËúŽmn¸¾ª7kú`¯Øf{åhm¯ØSÿSæ[ù‘wþéÛÍÚÑݬëžø;Ç£ýR¼ñäè>iêZè'A’=‰®ØUÖ�Ìæ‚¥MÙ0Î�`Yß»¦JÝ—§oÞþðíEâ'gò&.¨4ç/Î<=ÏtOdÈ;KO,õÒ'˜1µ£¯‰s¾•…´÷CÙÔö¿"µ�\G]Üð¢áVdŒ[GŒžAWVÉ€s\ÌQÉÿœ‹¢�õ_�«”ia]yÙi!…(Ee±„do9�ZM2\ÎèjÖ°Íœ°‘3ãFNÎB'¾ë;±<êÅõh ýUV7ùi†ßÊEŽÛ‰)'=·>æ¹^YYöÔ9o”3XŸdy '$G{ ×bOø¨g9ÂG} ?Ð? šnðÃ[¡¡d픬yuí”Ë'Õ9Ûô�dkB¤¡­'dy@ÖZj³RðjŽÑhÜ)¬Vö­æ‰.3Í9‘�:YÑ9†ŠÜ÷Â.ù"–Éä\ÛÐÈ/`Ov©}hÖ°�#;ÏFQ‚¯`¥J\êº�æª| [U õϾ½zÔ—™ ýÆç§‹BæJe'2áPÌVÆY–Œ‚%‡’-ß÷–Z¶æš—©œc×0ô1eÍîLÙüœ!ã@sY=þu ^ü›@à ÷JUµ“™¨Ò°Ïl@T¥§íçdjf»Yo1a%³<¬P·B3^eÌÁñ„mUSe\Kü¼ì//¾[‚[�àiÞIûÈ-{႘¥]s2m�[isƳL’ Xžªé‹a¦©ÜI`Å-¨�xž>žAÍ%Ô¨¯ªV +°hš! ßZ¾-ŦÄ7™Ó½ `~©¯1ÿ´U:º}4¥›Ký�!ñZË’ë{„‚Õø—c}™Í_�]¾üæìjOq›µÍûc^>‹czºôƒÖÖóýG:’öÅÝë¢Îù“6*lÖèè¿f&å×{ô&Óxè·eëíÜægWOŸ­.aLQ^%Á ÷²ºv3Œ¼®`Ÿ”“9 q#¼gNØù‚±÷ä¡™Øñ¦°�ù³‹g/0Z{‹d …Ô?áø~9¤‰cx£â3Z öO)zn�œ¥(–áOšß»Òw�Ù×Bƒ”Õ}z¹Í%¢K ÞŸŒ:>4òý}ÔG,ÎÛd¸—2:É^.ã�w*‡õ¶vîÙnC½¨ßã)�›Xà®A4VÅ=$ªaê Q;i)Ö°ÎŒ[ŽêNA!úV¢V e�$ä4÷˯�ÇŠ¦û†ƒÈo�óªþú4Eÿý`«ZÞ5îH“ù¹+öß-©õ;éê5©Þ}Å&ÕlûÊÎ20Æ=øÀ”nö"”óºË¡Ìcœ68m+Qì¸è; 9¹ƒÌñJb6…è7³ÏF`ÆçÐÿçúäœa컽4…¼i¹š)Ê§æœ£æØ $µ�ö÷A~<Ÿ6OÅ A¾2 pš,ke0AÝzKXŽÁêù3‚6âs€«F´²DE$dz0)iq’ÃOÄúÿiD"o_‹¥ø`çÀZ‚ãÒÈÂO†î~�õG~Æ›Œn²í;í�Âjßï‹­lé¨lýîó�ho–‰öf2P�`± QI«°N5OŽå A�!ÝóþY1O\5xÂz²‡@`’ú‚Ì qÃeá<{6♉G‘ŸS`�Ϲ[¡®�z8�ào$ +¼J¼Ú�[Ç5sZžã2š&9©³�•wZtç-À>=¯(ýõœ;dBÌ6…`†ku›WVŒ’ÈÃwŠ{‡f“ÎÂ7 °ƒI²ŽÊjœy'n åqY5ª1ÃIÁ;Mŧ¦$-ži­ÏPÉ(ä­ùéqs" ï�¯Ž5v ášP¹ÛªVÁm™»ê%q¿g�Åpôqô¯Bäj’Ã3RE†œ€–¸ ¯=[¹ƒWCÝ ¯ˆ§¸ƒBfV{€ÂoeûH¢==äÝ’sØ“6 ÕfKÄNÔ¾YŒ µÚÍlv†¡Ć¿ì7#ð¥*É¿ù5«Ègò°jÊSÀÊ/"R{ ^uÁÛôfþíïY|ú0ÂöÈüQäücéþ4…É IÂ×F˜–mºŠÁ(:ÅìõëÛÜé:%§(pf*4H<7/$½ÐæÁ¶ù‰ó’Px;” æ÷¤¢ù]§�öù®uÄö]÷Õòî�2Òo–˜šòX2 3ÞЊ¦óûDcb/x¯îûß]ïûåxï›Æw=åËg—L|l.<Ùþ2Œ(  ‚�íz¬óЄF8¨‡�1A5DÆ¢2±´£ç—‘dÛÙ"8=€ec,fwÐ0/Oë7«d:ƒø÷ª:¸áP¥†UÉ^¸_S±ìfìÔö/Œèô+¬t:Ñ“�€µ6ð,}î³ß¥ÌG’þB“%ý×7Ùo§ƒ®Wá_€î¤†d]øÙœ»Ø# º“w]ïw‰ã»D+Ö4%öó+\Ë»",W¥Å�ˆ¸ã5­nWýÆf,«µ(¡òA5~i%¿“¥üä{½}Ùp,ÒK¿I8Q»ƒ"ɼöúééîƒ5Ð?T«n+?·§½BÎ [ï`w[õ‘|ç¥PÚå¨Õ:ž~ÞëV“m�vHêù,R߇Q=É]ù†R,ÔÕ>6&T(WGwoæ.V¶’PÉ;Ü+ÃæÑÚÄoCnóé�»ŽžØ¸õ·[„7vSöÄóg\¬à–V\ÊëÜúûƒÚ"B Tc~ R�5àù"z³ÃèmUfqåJ™*çN~äc. ®ÆEr§n²ÏgRÙü֜ËTí‹ {²«‰G„óiÖ| ùóJÿ]TBÃË™§á�ñ ŒÆ÷`pa10šØUºìÆ!õlcQ Ñ V“—\ÊIyQˆ,Ü>`ÔY,èLÉ;¶Êc¢¹ÝºÊE1to¬Ã†Òöá3YR'r´½�ئê3-•¦�‚.Œ }¿Ž¹ïcÒjoU¯Ïiÿj<\Vòº@® wKF.š2ôÔxE=]ízˆ™Ø]ìݼð Iº˜�ÑœZJ¤e‡G�ͺ»AàÜ£ïúbýûéa‡�Æfs2†�â°×ôÃÇÑ\vŸÍXNwx2ÀÒɽ+žä.['¤šu¿h\”¯p¯×jÔÜHsÌÇžÂv¶o»ŽðTfvûþà�+C'Œü>Ð;…-;A£”��°KGGìÚg» G rähjÐ%–¹QÔ,ös» ÐÂ#— <ž*ƒm,ƒ é ~?æ`ec§¸G/‰g6}f/—ÌSJ¨I’ÀeÕuÊÃù±Ë¡Ä˜NB:è™_”‡tœ zÂr|ºóeß“Ïz‡´Ä¯Dúö";î±Kµb�ͪҢÉDÑA÷¾ý®B- J°™�vŠP¼é‡¢£¹,�b€t·ÊN“<;qð1+yöx|Ê\‡ÿ=¸öÿGñ¢í^Ò ¦¦’V‚\¥0' xœÍZm�Û¸þ®_Áú€n xí$‡\‹Ä1°Ù{én/i²hºŸ Z¢WìI¢"Rv|¿þæ…¤$¯ìÝ¢çM€+Qä¼<3ýøÓ·o.¯oÞ~'rWËdþ(™ÁŸR9)Ò\6V¹W“Ö­Ïÿ>™Ã¸Ó®PK뤳3üìf¯Í§K|XÌù[²˜{+“í�âSš¯­Ó©}!Æ×¤d!E%Kõj26e²\Ì%R{v€;|HµH ií«Iþ\¬-ü;‡W\›šL ÄX¬ÐY#š½˜×Hq).ÄÊ|"0ÜTH+˜Œ¥¼‚^EÖÈ­ÚÍ^˜éM¦´çÏ'�V'Ê]ÞL)7Ef…Ë•È$XAv셶ħëÆ”BVYBóÒÜèT!÷nÆV»|ŠSD£‚lVk±ÍU¥6ª¦R¬‘@ ˦ž ñ#ÑØæÒP´Q®m*Ë´IJ“¶¥ªœÊÄVî„30E¦y _ Ç‹`F ŒSS˜¶·�ik§QžÕÙ8ôzw š³À�ÄJ[ò:À“ø*h ” –�J�i¬0þ] 'W…š V›ÆVƣÚÕa‘°r‡ªë4‡Qå6÷‚ýû[`uOA&Ô�?Ýì}š%â© ˜‚üCÉà“UˆR rVʪ•ÅY�|XÿK6åa}ç°Ueív/=¬àw™0È~7à.[gú¼r]ÝrÏ!ú¸š±ŠTHÐ�mY�Ó¼WÎá‚$�œ©€b]› »@µQëÖªÌû˨ '…õH Ç«‹ëŸ/^‹wÏž<ûFzr–ÌZ‚dÖêÛª“í=x^ª®¨ÝJ]ÁÂs„„�ú �—5Á¬nÁ…åÚ�qÜaPneˆ9½Öé¯0§­÷|cØÖ²bú…Ù Dº­1">¶@H€+†-M)U¦8™6ÆbìNrðNû«j,ÇÍ\· :”´ŠÖ“ÐŒr�"—ö´ |Ñ .ÖÇ?JÇñŒ6¦()4|]©��Á¸§€¢R”²Úkôℜit:0Òð^á{€µn|ûFÓ¢ PþBÉ��ð!ð˜ÃðDÞ@�2FÞ…u P\¾WàT…5�@ç~(2w�÷jò¶0´̰šø*dŽI/‡ÈåT^a·²)ýšîù¾U@½†bƒ\)�èsHÃ\5\€ÊY_)ä_/#µ±düŸÎ]ˆÉdù6 } ó5ÖC¸5àßf¹p™¯3þ'Â_�M‡:sùdž˜©¯L�ü§è ´PCà£5ۊ܃£;7�þ Ô—…�Ÿ´åЙZ˶p ºç“ÙóS�ƒ&'Qç{™*Êë¤R|Së’ë¿1áŽ[ÌZV\Öà."ÞýðZ¸F×.!�÷Â>cC÷™ 2¸Ͻ��ÃEQç2â@oc8üÃl…©åÇV,0Ø”3ú Þ§C{²9Ÿ�Z�ï²Ûž9ãÛsv©FÐ^åÜ1£E’_‚Ñ®(ÿ 2w:ï�ݯ9ªLÙSsŽð)þ( {L>;¸üè!±?r?±„ñ…€†�¬ícÉýÞí 5�›FÆäÆ�p`Ýj_öûóX7¯v!§xôßã„XWœ½ˆCqÖ,’é–VP;<ŠQ/eí“Mx:šl¨ò•5: >*°bHAžÑ4”ÏC~ß:mBƒzUã㽺âŽÅµ1)Gq<"üÓ“J~EU·÷ÝÞ˘ôü™Ž]\Tïw¼ƒ™¾›Œy ¦¤!pð1TÕ¿ušÂó˜¢8ŽVêiÚ?�ô­óÍ#ˆìwÄÞËñÍpLì© Gó³ç€_ŒK!€èÏhðäÊŸ]8vð@�sñè¼…é乯sÅõzÄg‰¸àµ½³³?×EˤÔU‹8©2öÛ$øÓÙó¿‰ÿùNÌ…ý‡Ÿ¿T]ÌU¹¤“Û_[nsc•¢Äà£0p@,d�‡¶MàˆÜïL¯×\NPƒ¨ØñNÜÛ‚3œ±6ª8© ~ÒŽóoØ{ìd9|?f•¡Çqmmkêù^å¹ý �Ï£ H}·O¦¸ÞgÅSñ½ŸÒ'ËÞËÌ $ã‘þlƒÌÒ.ˆ¦ÜÂëåt.î�]2{” £ä„‘tÃæ&a³²ªÙ ±Bí'TŽØ’ V9loö lSPW–zl¾÷6U[ªF§ˆ@*�ºÍ&ج˜ieºE[ ¡x ÁPù&K�¼° ‰â–Hm¬¦1®a‹à& ãÕãË¢5­ãÈl“]3t×RºÓ8¥>²�¾¹7í5h µéH…ƒ¾ß§+ë ,NjÖ6ëÍ!³^Ss:BHÁYZUlÔ1Cî” :Ô>tR½{mØÉ°';ŽAl\³ÅÈæ°c¡É¦Üòëxû61¦‘؆ ‡¾GzîÉ1Ôú�âõ‰“^£ø¤ÛF�vÒWjcÆ#í`«|Ø’¡¨V»� *I]Ä&oü…úñ2¨ïŒwïI’h¸“†^ÍÍM¸šp{;Ð>yˆú�¢}¼Ñšô/·å•Þ-Xlçq_Œ— ]<¢#pJ�v˜aHÏi¯­>¼‰ÚÆ{4  ²Ð€ÃË�á�V¸ês´b¥�z?-Aµ&‹"ôä=@;¿3ÊìÜTø0óó÷I0ßÐ¥)|V dÇD=B:`«Ü<Ô*7ŒUØ÷ØÂ¯‹{oØó<²IÚ/ Ùý6ðd¤1|¸b¼ÛJޏ)!º³Ìq.ÕµM:gžŽ¡<â Š�ÖÑ�ðìµ`O¯¼ÏgÕm¤�:ÖÃ}°žwz¿Ð6y$u÷Z“»½ƒû40¾;†æ$kÕ»_¥®±ùåèój¨ÊM–ü÷Pé!?ùûë½_éÊ™‡Ë>•÷Þ{–›ÉÂ¥ç¿ý7ž}’Cô’o}»C�tÎ@7ˆ<à7T|M>ÁÜÿ0mÎ?yûAo�_ % 9%„‚^ˆ$Xxœ}Tß‹›@~÷¯˜Ú—’Hʱñ¡¹>=¸P.�ã:Æ-êÊî˜Äÿ¾³jB¼qÇ™o¾ù¹ª/Ï6ÛÝëo(¹®ÒH�Â\ŽšA—è<ñ:î¸XþŒѳáŠRÏÈ~̼úeO› À qis¯’©dâÊlÞæƒŸñl´‚Ï9)„kZÇsЋ§*Á@ÿ0›ÖEÎ�µ +ô~—�Pxy–òH´Íç {u@—ÚìŸJ‚kPÙ,R%Y ßt‰§½½}KúF�nïGÅw• ±UÒ†üRØ8B&@ø8§É$)Ò¼ŠFßÜÎÕÕ~ù�t·ù€ñÀ¥°ŸH ¹Ã#˜†í¬ª[î¡)`tç5<`K£ËàïÈÛê@9ئêå¥ è@®‡VA‰2¢Pkj™òÜ7! 4(;à:Í�“Pè\oš}4ÎQ¶OXÆdö¤¯‹¾„7¼€£árð) U¹�&Ÿ÷g¡˜“n7£{³�ӴŬ¢;Ëû_tæZ¦ÞÝ›ü:ê‹H®Áv;ï�m4lörìW‹Æy¨¨à0¹pÒ¯mè‚Õ]-ý—J�Ø›,†Õ©SÛflŸ Ë|Ъób„-°möéÉì+oe«“IÉxJGÅ:~­,³Ìa~_Ïtñq¸ki"ëŽéz'ãä?ýsx&�C‚]› &Zxœ½š[oÛ6€ßõ+¸<¬àØM�lCæHÓnÝд]¬õ#-QQITIÊ®ûëwIÝlÙR`.@€(Ôá¡ô�+ÅÌxùîö~ñþIt–^óê£üʘ¦$L¨TL??+u|þÛÙ Æ5×)»Všj5ÅÛzúB|»Å RHQ0©9Só™ æ3§n)¢-*¿0S¹Ò+Pñ517ˆˆ £aB–âÛ„ðœè„‘2çZáü#’¹¦)¡ß¸šr£‹i™ê€pEžN/§�UñuõÄ™:¿q7ü ½I‹„ŽDkdý¢u*÷Ѿ" úµd^ÜjBb)2ò”hA.º^g�îÙéd^E«ÑNWËú#ÓRyÐé\"Qê”çì¨kÕ*ǵîXÄiþžj,Æ�þ`î)BŠ,IFå$‰™ÑpïÎ"�ùSÂÕ&G#Þ•÷x_íÞ���‰1Â.IH åÇG½MÙpyÿ8œÚ}xZ¸»‹äªò”óÊIÈr[%· ²¾_ L*'|rUÕRÓZM359óä‚·´Õ»Trž7êŽô.ˆÜ £/øZÕÑ8Ú½]Mp¼­qæÔ¶=`¹Zк–¾vXW0@,“{`\œHâÒ,“£"¶%ê‰FGã>{û7S`á^8’t“»æÎE”8¬A_Èaׂ:ú�Ç¿�e’~Ñ…ûäp©….3öè/ FUÀ–¨W #ê^ˆÍDKç#to…³�ò„ÌéêÉ> ƒ¼So¦Ð7r”…­]<‚IÚvbMäÅqOìMrcçUÐ\µz8R¤¥m<2ž—Èœe56Šùøbzù+ùëŸdFÔWØÍÿ”ÿ<Ÿ± ŠªDo„Ûèm¡˜{@H /‰X3™Òj]W+âê`»ˆÇ1æM¤(WIº5s�4$d¹&)[³ôD{þ͵fò�õ55dØ®´' ï*=lênHØÝ³*$£`òP e-·Ù-K’âìÁæ.C%§¦D ºÆItßI†¢š‹|mKÔ׎ÆÁø¯•àèŽÇ?I…MÞ˜Ávk ¦¨+VÓyÊ3Ÿ_R=ø™Áy¢åtõr73� X*&׆è�R}-ÅïÊ8˜§ ôékš–6F[ÒW„d‘\`_SÉé2ßËËŒI"Í�j¶Š€r½]4ͤ …쳂øÏ „½Ü¶ÕïÄõ ¡¸³»%ÈŠðC+CµV©;ýØÌ³É¤Se`¢dq©˜éêÒÚÚŸÿu/P©@;Â"$i™åÎS(~Oê2ß_pP㬢OiÚ]d1ÆE]dqÈE°Ei™Ã¤¡L±tÍŽ9Å.ì KÛÁ^‡}"Ã�Ðo„ìõ‘l‰zâÙÑØOÕ8LãOÆ#!Û£CMË P´1¡CxMëjkj!Öu¥ø*wˆéŠBÞ—ìuØZa!ªCŽ 1³ƒ)òÔ=%¼j¾Âî#Ö.­š—ÇÈ­ªظàá*‹Sdõ<ƒI²yp?‰²Ñ×o3ËóŠUÙ1³ÛÓt>€›L–ÛŽM³å1ø�Cñò–š|‚ûɩ•t*?G8-ÑŒE½ðŒzáu"Ò¨ú.ÙÎQšÁœžhšB‹;•QÕÝniüUx«ïP•wiÀæy˯~ý«þýT“ÐQm�¦ª[ï · iuW´ÔbGC“0"è Ö`®ÝÑ(³)ú„ª–ÂŒ%Cíí&¶ 4Mͦ Z¿¼­-4:y 1f×»»¹sóåÅFáókØ«0xv̘ÌKqcå…g+/ÆZyáÇÊÖ¸¶uóêÚUÕ g© mªÇ¶Ôî1âCŽM=Úm_íá-Ëþùg�(‡¸Õnöü]CÚ� ‹bÐÄܤÏx=qÔÇã™áHê;'rÞÏGSo�6îRÿŸ‘õœš=ü¤Ð/¾^å# î�2Ž@xc¹óÑð_B=ÒÛÓ:„­óùô1]¯:Aê-ëùk¥æ¸ë1½‡N>×Cp¬”',•²þÞ˜âö¢iªê£žkq"ˆ™ûï®™ý²ÿ0ãñÉ ¶¶šF‚_µ;?xœÍ[ë�Û6ÿ®¿‚§âºÎ»Îæq-ºé¶i 4mФmŒ¢8Ðmó"Q>‘Ú]÷¯¿™áC”,9.î¢-�¬õàc^œþ8ºþÛW?Þ¼]¾þšmMY,’kÿ#x?¥0œe[^kaž§�YŸžÎṑ¦ m¸ÑøÚ\|+¸)ùîo®çö}r=w㬪|�£^R©�Ìôl¼?4L®9S¼ÏÓ±féâzÎqÔÇG(�—ÉõŽe×úyº}ÆÖþ�Ã-öϪ\tHº^kR�ëùG]°lk[Ì×Ì瞸¦,¯ù�fÒ\$¶W.o=5¥>–ú�ZZ†'·£m«"׌³’›ZÞ³jÍnyÑ=cf+XÁW¢Ðð4� 5˪¢)4W9««;h„WØ0ÛV2H“šHTìNš­mQ O´Œu·JÜŠšUJà”0B‰ýÀ(.ûŽÆ¸Ûr3,€Z˜¦Víä Ë«¬)…2"gw|ÏLmx¶�„y dZÞ€�R0ä ëº*#þÃÃW…˜1K'=[Uf{‘°—�÷4OŽÚV™±ã#G–ðw¿ðZb;OºDQ[)õØ;í¾ìwç(õYèÉ2QNƒ¨Õ”+*Hµã$k)ek^Rm˜T –2ŠïJÁÕ þæuSÂ�T²lJh‘€Tîíµ'é¦*ªºOV…"BFFK _ ³­rßLó½ù½’Z#!NLJæ5¡XV5ÆóÓ¡9é�¢qD[9eUCv±Ú»iÎèÉ™› ²§²¯¸áavÀ©"~Œ^ÄZ}o=ô˜‰úß~|©wß�MsøÄ‹A‚ºýJA¡èmuG6” Fi±Tu.êY4šŸë{ZÅã³ußÓX0Ï>¡‰2°�_¥Ü:GGÂ:\’,­=º56AÔF²nŒó½°­a�Ì{–5u šñ{k"è}v‘�½zñöû_~Zëÿ4Õ•F€ë½;´,@Öh1`–»JK#+åѺÑ0¢VY¹zÕàBŠ8¨ðù\ STà•x¤n-7M-FhOo ôTß]º­4­SØÔ|ÄÀ´«‹ÌxžQl \Ñ#lÓ-p š|~3‚ºœ{Eúb‰A�• §w [” ÈD( AÜ"t¾"' ³Eüg…€À cTZØÞÁ$`‘Â|›Šœ�sÁÎ^“âbûÃ~H3j �lÌ«ú¦–ù/RËÖ Ì’ØàV¼î¾Ž¦ê½@•úu_©³à°ê$<]¯ÏZË@‹Õ‘Œ ä~�(·±»Ð¦®ÔfñF€, ]A¨Ÿ»G pjc{ž¾Á�sß\`ô‰³Œ´ i|1c‰,ùFèÌ >‡ˆn³�› |—2ÚeÛ'‹0îX\ÿ×®®v¢6Rhš2]¼p6ê æ[èdñ·^\›Üå-zðOÆ^ÿ—Æ®�·ùN ÿß¶n"J=ÐÔÁŸ²5D) t`›ß£ÐØ÷ŸÐ%ñ‘u¼[ìÝÁÞ~%…·F|Ç]î’t²—ÕÀÚ5¸Dè]YqzÈ\ EWjiÝL~aÙœƒ�?ž¤ßY)¿;&a X!ðAk4VÍÚ… ^ׂpa‡Déà:†[pVU­A˜_îÁ‡¬ySØèAëÚ-²Ë³þB>{±O·0‹Ü]…„n, Ð?’«åbœV±\I;#¦�Sˆ}iž‰Ø¶þÀ‰É¡}šü@¤f›ÅÁtè0Žï5e9ñžÖf;ã²%§NJ˜È­/{Ríçê§Hµµ¤Ëé&ý®ãôÆó½,A `5öbŒ±µ¬µ ©#‚-^q-œ]ž¯öç�»Q+ø�$v<¾êØáJ˜;!”&ÔÑ͛ߌQž.À�ªB� l3™Öy!/ÿ´�1 ;° ?JÏíLj8oª¦ÎÄ[LÀÒEt3Æ›MÕR=Êæ�\_”;³·m§1�w¤1V4ÌÁ­{ßÚV‡m¿…Á½™ƒVx-&×˲åhùñ8BÏ6 ;P*ícT#i_SŠZf§±Ç7›Zl8n+hûp=H•1Zƒ¬,Žõ¼[/íây‡|ë²õn–;ÀoCrë¨Kþ^P$&vœnæŸ"ø7ð±ÀƒÇº)7¥èÏï#lÀ?U�†œõó‹ùІÓ¶†¶‘Ò‚)íöÏá <†iËŠ,˜`AXw‚=†XvÝUu …¾¸EÈ#þ"¹!,r#2Þ(Õòøt–õ&ãh δÜÝ1ÛŠ}@S+¿bÀ»�™áQ‹Ó:Œ$òVq„5Ùh;CD{§mXwXv¯­ã:d�Wݱœÿ ò"ÔµÓÍ>¼®=tKÙmRj ¥x2ÑØ�ÃáÝåU©¨6aO%dþг¼`ðto¶ˆ¬1….ânk€�£¬.,BËe¥âñ<ñ®VCÙ$…,Òå|Þ#ùZøN‹£:bÁ¡ûL[Mg!>m‰nÆC ÙôßϦJA,½}x{¶'’ReµàvkI8ˆïg΃û ¸²h…î‘’¦/œu‰ º£¥àõ®Áù€úƒ;n¶éâ–�³Ëë¹(¢yøþþ=-ìö€ñÚSÛ�‰~{4c—¿G#1åÐåµTÒ4×;��%-bT[ÿŽ:#Ÿf3äµ”Fú…!ÐØý€b¯P�OÆ�ß?"A_vЫ„=~öO?.W\7ÜáÂ^Ñšó~…ùèâéÓÏðç³§(W‹/N&™OòC9ýºtòi‚¼tç¶!*F€y»›ÁÎ'aÖ�âVŠØLý'ÇÍ ŠÆtøÐÅŽ¼ÝüôÍ— LfWC;ƒÄ�€Á6TüöèâòjÝÿýýP^S †¶õÁГ£€Á)rq*ÿ�ÿ0`Ó˜:ÐG¼¼D`ÀÔ»FwµôÖFc.±ºB©@Á@Ƚ«XÐÞÂõ®†kϰMþd{þyöwXø›ˆ[ Ýû/ÓMÆl?6ý6[á�©†s,’-úØ‚gïñ�•³Bn¶ÆÁ2 óbŠ1�²p–ó‰ñìÔ y°¥sÙFg�i÷@rÜŸBräÒS ,D ñ®ÿºžõ-–W¥ úEPð%� õœ•t@7{㸠É–ÎËÞÉš&<ÖXA‘ÂrX²íj<»¡=0Œ¾d·ú‚½س½œN~im³é&lÁšÖ ¿;Íù"ãÛª–TÊ@ªÏ量êž@wœ£à½½àÃpºtœ.OçôÎNæ³_õ0|¾Õü„¥ 5o¬‘â¾iÏŒ¸7�â™à¾Å- â¡â2òFþáX«1–üì(KR±]¡\Ü壇àÃ…ÀpyZôs. /ÑýŽØïA9ï}ÐIþj9ßw <ð®ð7]D7ÇÔœ�P2tÈpoÇPQð¨ÍR6 ó¯]UWºðWc ÷õ_^åˆåxmã CÆ•eA̰ͣäÓàéÀ�Ì13„Í<$2Aãá�úgÕž‡PY‰,„ÉxS)mj.ÛB¨÷£R5Oi�o{XAåe™q„c¦¦ÿ»·‚p£S­íŒ‚ßb_Wàf×¶ì3é[z» œEvþ„,ü’öµO_>¥ûÏ#“§23Èâ°ü‹ksÊ`º¬œQ†š1bÂâ¦Dô~XbaÏ}&•:”XX¤ßáÛ�Ø©5îরюVÒ¾’N³V{˜h� g«NƒReE“G0Áà†-ÂÓwM¡ð·[é磢>T©©ª÷�ÊF0ë7m�éëu%G-|RWÑ’—’<”×#âDÔ.G¬3Õ¶b5â4ïÔ&:¿d¹·Uj]è°AO! r^©÷aÃvÙ(iBY�§²ÝÏ(Üb�$ä•Ë„½Ú©¸—]R¡<ⳇڗÙÊÙ4TÐŽ�ÛS92�¹ã ò샴€ñ¨Ä×»É÷ð2Áï$òóJûéÐÊNAÔaù!‹#•Ümâ v´P OSÙ¿…¬¥]·T¹ï)ŽVáÖy<Ìêå¶�È“Ur•k´Ä¡ï€Ä'x'¦ý¸8’ìãö‹÷½û�ÕÏ}ñI…É%ûªd{”ü(IއD÷3¯î‡P‡b¸-T²ƒ?ð •-Ùÿ_zºªÍ}º Ÿ!²²jý*_î?H˜†Â½¥pÿ! }©øäͱhÅë ïtü ÚŒŠ¨¢Sa·ìÜq+�‰³ë[^/ì“ë9^|ܲLø÷ÃEVÃUlƒ5Vá¯òŠê­°" *\+¼žÑq$¶ùølî‰ÍýélÕ‘-ÿÚ<ÚbÒ…ýâó+zcÑ;÷É�ßP¹Dºýâï ëœ»/Yçö;Ùÿ“â¡ >€>ˆ>‚a‘âxœÅXmk#7þ¾¿bêBÝ‚ã¥å²ñ—¤Ð=z¡$åݱW‡VZ$­�ý÷�‘äwûâä’„ÄÒÌ<3š—GŠ‹Ÿnþ¹¾{¸ýjߨIV¬þ ¨èOƒ^@Y ëÐ_ :?»øc�Ó¾—^áÄyáݘÅ~ü ߈öšð}m*WäQ/+ò„75UÏè¿[é¼,Ý'x‡ ²B€ ^ N©ï,“"ìêÃÉ0wäã©·P*áÜÕ þ3G?´d°ÒT§ü Š…°3ýZäü® ˜žÔ.òé~ ­°¨}4E×b¹³!ìÜÅ�ߊ<„Qä-‡:�kK°¾^2ŠK?΢}%«Ã6îâã B„Ò�¯frÞYc¡x­%að R�XŽMë{˜›EYg-Œ`Y˲fT‹Î¨V`´êéaám ¡:„Z8˜"ê DYbë±Ã~ŠFPÿØ®ô])¬í¥žÇ• æ.KÙ©cFVÉX{—žb’¾N§DU¹dqm”±7„’lF+¨ûíÍ´÷pdï‹él‰wbªpOrÿŸ°r{ý°'¡«ípöí¢|·Oø$Y–‹˜ÊVHë@áÌsÙh–)“wœ!Sv Õ†²°=˨¡J΄¤´•FÇÌRM¥;šÅU/qã�®Ñóɤ†TÎÐLäi+£º ¨-ή·ÊxO3ÑüœÐXžÙ™Sž3ôÎXÿxδÅ£c~ÆxÓ4GóõèÖGgòÕpÓð[³|_?i]IKT!�~Úû3Hr'Š_jTJ¶—«‘~–ÎQgÜ2«pW73Ã14í ü�­ÐF²1ªk´3#^HMõáÅ 9®ã•…¶ón\ê°Œ^É-¤ }êÆÙiÀS%];hN ‡!¦�LRΪƒÒ ¨BîØø�—.))%eˆ&iÞÍ,c0,‘mŸ(Ð:¢Dú¨W[=ñ6ZÜh€—ñ¸GÚeCCáJÔÕ¦”�˜q&:åÃe‘Ô*ÜÑ#ÜÔ éð07ÀMÆQhˆ ,eö‹áA‚! j§:·Û<ëf FÃ=V²‹áúº\G':o6ºNôT C'±Ké0q©Æeªž(–‚\ßÒÑUÙobü’ëÏ%¹þûH®#’;‚›&"ÎÕ»»zªëß�êÂänóÜhÓr‰fW/�`D¼ECž8lD£Ÿgi¾·Dñ6½lgìLzn'?*ÙœÓȬ÷Ò>æPÉœŽ»ý�ˆÏiíg•û8üè0šƒ¢Þ­)•Ù,°\*ƒ«ÍRo•õþï„ [~«òÅq s¹à§ò�·Qà"Öy…âõg¯õâõo[¼ãð/,ßÏû•{øÁ•«è]àñœÚEÍïýwx:Yá‰ÈB*y-™™<ߨ•KM,õt.òô%C¿ÊøÇQ±b”|‚`©|9«xœÅ[m�Û6þ®_Á[àn?ܾy“\‹ÝÍ×í¥ Ð¤Á%M×(ú�¶h‹X‰HÊ^ç×ß _ôbK�Ò= ÈÚ"9¤žyf83¤ïþöã/Ÿ§ÿC2]ä÷É�ÿÃh  ¦)™gT*¦_ŸÔzqþýÉ%<×\çì^iªÕ6ë‹·Œê‚Vø…T²¬˜Ôœ©»KÛ5¹»t"geºÁ &f8WšÏÕ c’;J-Øë“±e^Ê©¦'÷w—§º]iÛ¤_CߊÌsªÔë“ìY(øw_5Hš—éø ßøenÈÝì¾#sK0#ï.+”~O>gŒTKþLÊYѼfꌔ‚Y®É¢”„ÑyFXÎ &4ö±SO­@ó™P‘â�„Ì˼.Äža��a„ü–1Ɉ†5˜— kªH*éZ$d!Ë‚P¢é,g„kÂH/ªZ³Ô¶á(ÛŠÓ!4K×S²E­Xz‘Ø÷LùÊãX¨óW'¨~x¢½ÇPÍ=FÖÚã>�ᢢj 3 «äŽÂÍ÷3ÀÑifÎòœP)éº'$E¾‹¹6ðÓ¹f’¬Ø\—R�r~Ø�”-h�kœfØ›àtrêE'þÑuóˆü#ƒYxu{f4cÆ.Ê<>árÿÊÆ×Jj¡yž´3‚b#¨qªÆid5NƒÕö¶_‡jÀ†œ®•7>…ô\U9Ý„s¿íÕºb‡!ôäWY¹žo\’R¦LМRÃNèlÃx½„ˆ|‡™7òUkJ ®àJ¡Ó±nLæß`]hW2f�5E¡óa šGAÓZ�kÇ#Õd[ò0úƒÙ?ÓËÕ!ܶ#ŽÀn/x¾Ü´n»zƒ§Ý¦öÚdÕ3²Î8tnÜk‡–@_;ú\Œ€6Ь¹Ît/žõ댉=J3{¯Ñn”Ýwz°º¦ÇR×¶àuµÜàºd�scÓýg^€ò‚xn»Æ$¸—8 Õ‚K¥½O (Þs¬EŒ’Éùls~Ý�[—žt}úo{&3czÍ€»ð¬7¿ÇFlØ�k“À•®³2g{§3Jbº÷pµM£«mz°Ú0SØa¹—²åÌ#’ûSYË9ûŒ)@R�îÑêIFÌ&)IŽÉcÀ‹K€“°¢‰¦o ÃÿB%E¦éÓø[™Ã¨¬\{kW=(-mÀòsá•1˸ š‚Òô(M�‡î= 2yÈ!0õÄ®b| .QLòylt¹”lI± À….m… $µTYƒ,�o�“FÇô=ÓY™#j»ÇÆÓKÝEó­ËÖûYéŠ ¹>^¦F¸Mô‰™�Ñ0ò¦©Û*�{Z0*¦|౪‹ÝÂCÁÆÓgÿ Uéž ˆ—O·aX¬Ý˜LË,¿‚A,‹Û\[NÂàž»×„  U¤(�½�‡ñ/¼Ó Žze7\”Ý¿et@#{ÒÎûur"0îçÊ–WÞÍäÔ•õbñõÓœ¢žƒ ëúÇfl#vœ²]GXKáͶ `¯ºiÈ’XÚò‡ºÀ‡%(+ÂP·ë”žbˆã³§Û¾,·15jõ$ýhMM³dæ¡ó;®¨ M4sÊÚÆä ¿MnIy¹lDøUBJ-(¼,Í 4¥tV`ÕO ƒÐ«å5¾3†öš³…&µ°¨ ·a ŒXS) ÙE¾†è.5Á¯ƒ¯)ÑŠåØ­3p£+&m�Ϫ(ñÂcáN÷Ø´Û;ÆÁø �t„Îoì­"Œj}Z7çáb.µ&S”¥üŠÕQWäÆ,>ÙÒªê©ÆDã7ŽªÍÜZr*—ð<%PÛ«…êìä~EÎÉäî’¶ž?ÐþOßn¼P§F òú›¾ý~uF&t$a@&¸fhk0�´åmi7#œÐ™NŒƒ×A0ÁŒ¾â²Ëˆ­o¢×²Ù° ï‘/vNlšìöÊènÒ«Þ'äúÕ¿¼ ô+hàþ|¥B43ÎÁïK¨Ÿ«‹—/¿Ã?ß½DUÙóšH`ϨüÂ?$žl‡Ä†¾+yW¿e c�fã…HU“À5ñFÙîÐmA Û8ír±8�ã{[wÆ„y‰·ÇDrWô>.#k;!r‘ö+éžìÿýé<¯r¦M’œx PÜÓõbò ©êÿÿcWq17•»17cŽƒ¹½§ ¹#êúa€Ê1Lf7+}ƒþN™~HLÓß–†m“ˆdVJþµ²iúÌÕmÿ.’v7º­[Uœc@7 †nºi8t+¼7nú÷4ùº„@çûF¯9cXËÄòɆhö¬qjN‹Oú+-ˆíqb\æ'þ5%ì%+r%il%.HUBÄ[Ïä*>4Á‘TÓ928ÄOn»Á�h…¾~ç+QÀ¡§&lùks¨wö¾J2ø?ËN÷ˆhö¤ŽW‘‘c6ÆáÍ€´)CëX娗ii§ÏÈ!DH?–Šk^Š<}߈`¶"‡™Y¹vÏÍ%˜øÙ+`´˜@U€²ÐÝìéw¬NŸ‘Y©5ž�¯yŠÙUÆ0fo¨ÙÜÕûU´7 Ì¥bž3/É/ð¡JK ºìœ´ø´F»Ž�ÊÚû ²Wðv Çl¯™þÞá¤kÅòÅÉ]a–-KØ 1¢35Í-“lKKgƒ|aLqbªp/®'/Í÷ï;¶ 8r ‹ÆY”¾m.A÷_½9âÏþ†y {ÊeÖPK¡¾�XS!<åBì"Öx“wغˆ�ZaéæÏÓ¾‡y÷·”Ͷä†Y�½PdɵeŽ\Ìó:횢,ìm¶t¾í›Í%sˆ‰ËÙ™�>›â0¸,ŸÌCihsùÔÞœ7< A²×r"zµvòCü[wÉÑ=]WøP†Œ‡Ê þ­yb¬Röö$ªÛ©ÚIJsD>ˆçV)£ÝJF¬ðO£oüiàÎñFÃØÉ6¢=£ý[·•�µºÂÄô8òµ™øu™Ð# ëQš;§ß(§óÑx² {5=cÚ¨8vsqYKsºXQ)k1ãñ¦€9ùÆ^.‡�Q2šž—"ßÄ:;ðþþö˜è¿Û�ä§:TùDÝ7£}Ú#Zˆ)ü±-·^ÐÜ ïfþC÷n›jëàÍu`áoᤶK“¶8ï~Ãa&ŒPfT]ö"�°”F% Ù’;’Dcñ…noðÁ–é~*yi“ù?iù…à ÌÌ”0‚b¨d,ŒxœÍZm�Û6þ®_ÁS�Û/Þu¶EïŠØ1°ÝôÚ’fÑì58Å�–h‹]šTIjç×ßÌ�”d[ÞupÅÙ@ڵķ™‡Ï¼p¨é_^¿»½ÿ÷Ýw¬ò+5˦é�à%üY ÏYQqë„•7~qùM>†÷^z%fÎsï®°Ù_ÝqË•êÖ[JͽpwÊøé8tͦã8åÜ”\àš†Kçeá^²£¦‚1Ù”3ÍWâU~Ĉ|6s\ëËãD…~Ù´f…âνʫ¯ÙÂÁ¿KxÄ© SŠ-™§óã¦�ƒ4x:®q�»auìÌŠ®7«¡ûˆqÇÂZ©OM³Ð+VZ¾vLú«,LVÊÇ$ïÊ]~�§ù;iŸ/L\U:fæNØGPÑhxX0' ËØ#·’Ï•p#fjl†¹6ÌÕJz&µ7liMSC+×%ó•ÒY€Nð{ø$¹ÎØZú*ô²")ã„Z°u%4®ÆŒ¸4 \1é@ÅØ�¯+î³'бÂ7V÷¤(MѬ„ö¢dk¾a §¼¨@ƾƒ_�f8?g l.WÌ6JX˜¨·AaZßà º(©EÆ~7RK½D}`RÕ€ò¼°Æa>—¶ öè^nÁ]˜•`à Ö¬`b Â[Y°÷V~EÍ_sÏ£Æ#€Ä8X¼0ªYéÄœn�{}qK /î`»€GÍG Ò÷¦±…¸ÇwÛówÛ¿·À/©ew•Œ…í¤•æÆW€øMX°Ãö¹€I–ÆJÌ‹�¥SfI›`ÌÀ08r ‘-Ñútá݆&re"ôqã�*¸~Öí'cW\ÉOx’×ñM$LèøOé½°mk „)Ãâ6ƒ‘‚±Á6gð (¿¬!HÒp%©=¼asþ$ÇáÐ÷= €ñ ²`-Fö.ò>>%ª"5½¤'|XW²ˆË.¤u`¼®øhl J|� ôö¾Ç»òçžzºíÒ�µ`0Œ$J¦¯FìAˆUƒ~€5 Åjã$♌zÑ8d> O²¡ÐÅ$\%¶NÐv8MFœG‚B,ä²—qHD’QcDßÞÜ¿¹ùö¯Öýј‰KÍ@@ó$@Çö»ÒD…ÌÖö  ¿é��äKÉW‘)ƒ@Ù0ræX¶@µÁ *ÍKÓº.ôg;úŠv ›Aìw…ç�¢;°«àÖn˜,׎€/6#æLÄð ôºQu•66c`FJtÀ9:‰¼Ys¼áœH=8BIªIí<�,6ìx›¢‚÷WTÄ&ò—dóŒÜ¨eKáédç8•dz;-HƒØraôEz fŸ^.­ÿé Uï7íDY¯í-·Âö[YÈ[x�ð?ÂÞ´'|+”‚€d!Ä­³¤p…\9.AÊM-Æá·EbÐøžQMá=8 Ù{•3�Œã« `z�a¼Ê1à‚÷Z^a–õE?‚å;ñŒƒêÏ�¤Øäº±á9$=$Úòƒ�݈‚Ú»˜åT_ÍÚ¹�ÈþS[SchŽ$Ègwí \fý ÓBtêø×Φ¾Œ)Óÿ²ÎGŒ¹B?—ǨȻ\ €ÿvãìh/®RNBžÀš5[Kvc| öq¶ë”"Ù„‰Uí7ÑX:/µæÁ›hŠƒY ¹WA¼1õÿFk;7È÷r…aS~&³—„D¹ë'³;a‹º€‡áènJÊ ÉÛHO Œ3yÐù†•bÁ…ÉÚ{AüGOKûñí°MТ;Q¨5©ú“÷ AÃÎÂÔü�Fôüx€ËFë Œýïç¡x¯¾“o{†÷xE]÷½êìhoW‡‰sRW~괅߇”uЖø§Jpm¤öGpûogâϷʈùnUq_õ• »b8uN Äò:^UT°ª6fåÆI(YÇüó©”²7ÚKE×Q§Æè¯TBôp°¬€�­¯£þ“íjIJ‡ÈÅ÷.º“OLÚÏAýî™’Ç~FI/žN&]Fºôj '>€E=ÚŸOk0Ž ýûÄÓIIx<ýÂt§øFûŸ€éù,ý:$öÚ)Izv/%u§CëO~êQàuñyET/ÄŽôë¹ÈqPÑ^¹~qÚÝÅûÆ|–~Ò.ÝL&Æ¢@(JÛ^kr—<߯J,àÅÜx�EWÊG :/+ÿ[{u™.Yÿ¥¥O×Þp2úPI%ÒLI¶[£�· ìÝ|¦Àƒ÷§IÈ–B] 8Üzgñ:Qúª]ùŽìÔ�ÓwtÑ©z¹�Ãê*ijp�AßH<+§OWŽRë}9¯’¦?bëž,á éÀÈNH•-˜ò]ÔŽ#ͺ2ªwåÜ¿'ŸŒ|*TSвýŒâ×#ÿ Á¾þmß1ŒRbûú|ûFÖîó°u÷}6×±$dÎP’†7Ê­’¹ñ[‚®@8h £ð O„-ëÝ3òtø�'È“C&V£»Rf ˆõË�Xvåy“6?2O__N½{¥ºÁ;ÿSòœ,¸M¦áëƒDCV¶¥v,…KMŸúà‹ Ç&ÞñòÒhuʺÿ–ÃÎwý÷�DO|7h mØ8}ð¹_¬€C¦LŸ¼ Oê·ýŽ¥Ãðg¥¿¥ÜGï¶¿ùü/2Ã'KQ>[÷R(áÁìÃß!Í^SK�ýé£ËÂÐÂ�)à EÆñ[àqøèø¿çÏÜ ?¡?„^‚d‰@Kxœ­U]«Ú@}ϯ˜¦/-hB/\(—˜-}*®ú8ÙÍ–5v'Zÿ}góq¯µµ(Ę™=gfÏ|X¼Y}]n¾¯?AÍ{[&Åô ÔòØ#¨} ^¤oçÓ\ìlØR9dÑÍÙ=ZKvéœ×¦A¦°¶Žá qít(ò’ùH]9}Š�>ô4&°Qá î¢lR 4¸§Ezòš=-‹c.·\éš]òy’”ÅiýÛ Ÿ¹¼ÆÊé›${‚‾tÕ�"�?`Eu °È«Þõà=5<àg_hIýf@¿ ƒá}‘÷Éy/PÂÒ“�ÂÍI� Y“â,¨´9LjìÃü1ØÿÈL® ¶f×yç¡Å†,°ƒÖ¢¢ÞÙ‡ÓÌ¢Ÿö-Ÿ`ë|2ø:ÙF‚k£êÈê)8{ ®±'ù.:�?ÁmGPc€Š¨I•¢–Igp©V¤AéKß)ŽÙ)ôþdš]X£Ì†dªÅiÏyIÁ°$f¸¯JV‡¶–ñøl¢zv�W´ÁÊÒä=¯Êÿ ÷êý†ÞDx¸Àö®kÏ €�NÎ}ð'.[¦¿D¼ù d‹Æ°´åX5ÙY›(�SÝ^J#÷?â)ú¤µTÔ@dU®t•Ššp]é�bïÀ5»ò™¤=mp2,ùhJ¤tµ§í"�-ÉR¤,®³·ç”éE€8÷=y.Ízç:Ñd‰éÎí1€þ㲨ʉòeüë+s½êž�S¬l|ߤAc´Çc#sö�¥ÊÇžÿ¿ÿ‡00Ž[‚c�:+ÁxœµZÝoÛ6×_Áå¥/Š“è6Än€®ÝÚ ]¬Y‹aè-ÑšôHÊ®ó×ìÈ©-˜@�Ä$ïŽüÝ7éÉo>¾¾ûçöWRÙ…¸É&ñ£%üY0KIQQm˜}yVÛÙùÏg0n¹ìÆXjͧíè–j*¯•Ò%—Ô2s+”%K­–L[ÎÌäÂSe“‹À}ªÊ ʺrœ¸±¼0×äX®@žM(‘tÁ^ž@ÄâòQæ÷WòUDaaÎ_œ¡NaìH=´Ÿhd›8�nveôkÉckˆš/Ç/vÿÍ¢”.Ö0´_’g¸ˆË’ Xi@\¨9/¨ +VX¥Çö‹2€pA¦R²­…ò‰YËåœpKœÎƒ[ýEõnˆf³Ú°ò´šü¤j]°;”q¤;”it¸% _�›}ÆŸ£6œ—¤²þÏÁÍ`h8¤öƒŽ ~,c`é¸Ã–õŠ,¸.BLÝô^çÈÉ=[ÚȢ܂ÌùŠÉqv ×ô;Mˆ³ÜdiÜæ­Võr@ìkèÒ¨»Ã¾_És\€¨î&œkÍk¤MNL°“¶Í=�­²þ` ƒli®4G…[öÍæ˜ô¦L;µ7J§¢Æ ÌÄç/d.•ߘI ¤h÷Ci*«ñ´W>锕%:„�6©Bêü8)üÁÙÍ€jl‹>]i¶#æ1¼ï ö²b´IB¡ŒZ Z°"� hêZ€_èŸi*çìY`”ãŠ,Æ%ï``ãA\ò¤Þ›_pkB4㚨5¸ÀT‚¯Ež{˜8:*½Ò€±–úÁJoÑ?œ»!3ŽKE³”ÎK8$„TÚ‹¿~3``§5Ä­“i�;¨¥0ɇÙåºR"êTŒ¯Ñ4]®E“-D]²2o õ2'ðøÕ×Çq3�7ÏÎÂÉ£hÜ‚a)uïFœ¥Átš ÑÚÖÀðÑ5Δ�¤+§¯âÐ16`¶~zÏØ²}çêõj÷ÞÕ¨'Cý<í\§Ôƒ‹SGBb[ ´ë~ߨeûØ×`mòîƒ>L²2y¹0¬eOÚ¬ئ‡HòD_Þy¿j-±Ó¡Ÿ8¸`v¬ï;¢Dþxï©\ù¼Æ'T,$t:[�†Wn.!áßU8ùúïËÐò\Iqâol¥Ø#qÜIÏ)àܱ§�ÅË º[vloá[³þ›ºÿ^\-Œ ,,•P‚e«$4txœÕ[m�Û6þ®_ÁS�Ëï:i�»b×1�lÒ¦@’·i³Æ¡(h‹¶x‘D…¤ì8¿þf†¤^l)ëíåN{@K¤HÎ33œ7rgyñËÕûÅ»—,µy6�fáGð~ra9[¥\aŸÆ•]ŸýO¡ÝJ›‰¹±Üšsì¶ç×+n­Ð¯¤±j£y~…­³©û0šMý„K•ìqúG4>–+sÁN˜FD3Î ž‹§ñ­ßÇóÙ”ã:ߟB$|ÍJ¶Ê¸1Oãô [øï ^q¢•JD‡ÚÙò”I—@ �MKœ~Ξ1ã>ee¦,ÛI›2ÎÒ0Ž©5|•²-×’/3Áx¦Š “Ö0#a&Œ›ˆyzÜLõ`¿K4ßr¹e¹ ¸rsö$”4¨n�á¦MU–fwŠž—Ò¢J+UÁ³l1SfÒ2YXÅ6ZU%’Z$̦”GÉ•0ø¼g\ ¢° ðî#-ÍFdëˆíRQˆ-0IYs&�ž3ö³Åç]Êí-ŒÐÂVº0“¨¦$Q«*… Ûñ=Z5±»apæ=|è!²•Ê@(³Ö*g[±²J¦ü;gå4aŽ`j[*›žG4OM“!à…Ò9ÏäXŸVjµäK |Û³DFZI\Ðk˜Ÿ(#^Ò„°”…ñ¸qò•ª kbà´2@vw8« ÀRëÍÛ*“„1ð©k‡Æ2±iÝc€E ¡ï¤Àxäd  ? $'eªB†¬�¬é&M€�í™HÆî‘£n­gü*“ÐrQ·DõGç¡�¦ ­õXP7†"žÐ,ä–…—]*=±`-µ± e)À†a§›ç'Dò‚[î'kiÀ3²w–P’ÚÂnå+Qc¬´%bü³ ܸ;KŽMöQˆRÂŽ…癩˜ªh¯+”­*‚¶¯+’hõ"…MVƒÃÞKаŒƒ¹ †D¬å¦�"‘è÷ìѽyöþõ³çÕæS¥.Mè•à Q^°Øò_ Ü�ÚÁÚ6%"Òf ¨“D¶„ö0E‚"øP ÑaŒŸ)mƒW°86çIÍMÇ0!™°@×FÕ­A‡ @‰{›ãÜk±Eò›4¶ã¡&½áú£Ð?Ê,ɱFu‘¡ º¦ŠGûä�Z¯[ºÚÌóÔüµüx G@·“¤ÀWˆÊY\ZE–Á¬Zãîs,˜ @ØNŠÞó$s´�ë´‡g�œ¥á-�žA;8’ù5J)3 <ØÔ7EÀA�-håÓøp×Âøs ¾;´«q�¥å°�Oš¡3Ø�k Xw’¬´ç²\×^×MùÑ8S¹�çõ£sô„u ÞÎ9ý+vžl#ùöôñ¼žÿVŸ÷˜æRh´Ç´j<W7àb0çc �Ðô㯞ÏlâÄ?¿Êw·Ž8¿AƒÏoœÝâM|�ËÃÿh¢·<«Ð¼Pä€;0UZ~nð v¨DÇÌ pƒZ®Ð\WyáõöÒÔ³¨eBvÜxwÝvw¸OÁ$€óL\;p=¨†à§Q¢ú34xɹ£r Üú_²láX¶¸˶¸Æê6†¡§!MPvÓö(ã®[ÜöqýÀ]|¢Nµ4Bo9ºª À|²#Œ…ìÏ” £+>ƒ-v-… “2µ!N:O°Iä%„<¤!Z‰W׬¬Ä{ÔÜxÞzâ—Óñ¡íÑ€ ÎÍo>o‡uÞ§ }GZã ÑŸÜå^4ÿ„‹ñÒžmPv^¿!ÒÃðwtÝ­__HÈ%ùž‚¥xÞÛÜLJWjç¼]7å¢ÝyB7 Ò�&÷ ¤P¬y•ajZa;æ,—aØ’ë&�è¢ÏºÏä ’¤v7‡,@"óI$ǘŒä¤1¢­àÅçn*/![ Ì} øÄ0n2Ä~msÆu�’Ñà=jÆ£¿üþl¹?{\šzÊÛ¡ˆOkW”xââ`šá-äÑ~•ç~£&Á…ÅßÂØµ <0†¹ÆHĸ|œ±—äuv.ÂqìP®:ÿêñHÒ©Óó¸�©÷Kh‡½!D!M˜IyéÊ}U€ï¤Ï{#Vj± ¥˜Vbq£oàħ¯“´µÊ2µ3.$M�}+f"¯ÕŠ¢ðíÇ�}Ìû@`V ¶¯>Ê^_&‡V,…Ïø±:¹BãX`òŒ¨ÍXe›~kÐo�¢Îw/yû;ßóVénO+/¥¾öìã Ó& êPMó�ƒºêºšáÔ †o+½Ií €xlÔRØ�Í|H£MØÃó¿?aË}`ùXqZËðÀ&süéiVQ*_âææMí­SFî Õµ4â¢Ö±ªpƒ�B%jWŒ¬K‹>Nõ4þÇœZ sJ£²ÝʬL¬ÇÞx7¯e.-8ÿ0´¹2ê­™qXÀFÍ‹�Уª°2‹¨?’Rœ‹»à<Èé{P.ŽPŽòJeJÇsúªíëöal5DÃÐÌ ö�Ÿž‡ *DM—`£ Ã­²}o$øu15n×U‡äÎŽ6¸9dNoÙ5òu×nÙuDäÞ9äôxR ×X5d•­|¬tß©8Ã'ÑøÎ°U�»…ò~Ì9}r’\{ÅÚ„Yª¢î ä—†ð<Ĉ�x�ºcl™OFM¥’TRxöøo÷¸;š‰jú\º ³Ø6p„¼¦u”Ü:¶iÒ>?>Â÷³¬Ly€M/C¥ UòO•8>q)ÆC<ʆèöÑøòìœÃŇÇr_—hFÅöGÍxîŸÒibß™\ŸD/Ý©*ž˜Yú5$\JÛ­;†¼Ç;1�;ô2XqÆÎÀÇ ËîY và�*Å·Š{MÑЮcC÷ O‡ë&+–æK‘a(�¿ƒ‘vÒ·‹éæÞ‰háQ/NGÝ «oǼ¸w˜‘½"ãyx½†~Š;óžNºÐÒÔšZ[¨W"Û dÊý‰/š‹.ÂÓ`åú¾ �a²wþrM<OCÈÂ5œ ¹�Ò jßãiNtÿ‰‰ý‹ÈOø(âžø Ôï­‹Hîë_ i[EÕ©Ìêjs ïJÆj,<.5Ó…¡@h­>Í�#Ýò1t²Ž%ñzÝ_p\Ã?-ݧ» çñ8h-?ûûB}ä+gÎQbMïÌÝJ1^úñ$Ë¢8&¹¾—ó3öæI€½6’Ötø²ï4ýÙ¥*k]µj«YHŒÉWY•@`Z+ÓCȺ&ªý~l¨œD÷Ø:ׯÂzt½JÚ˜¯>vî|�¼õõˆ{Uf¨4WlôïÏ™‹¯î†ÃšyS‚ë×ÄqxC† ö _¡ª¢Už t›Ú�4·Eï�©ûCz¿;¾Ãw´û“Ÿ½£� úL¿ƒ�»… „—O|;�iw&`è ¿HÅžn^âå©3Udc�·w s|h§bŒåø¡wèÔ4Ü�»Û½»4G%áÒÝ÷ö oܵç?ý$ëð îÉóì „ŒW3ïz×Þ]Åý&ŠqGÄŸ3™Çsü÷O4·ÄBmÞAò>�Šü’mäVà·\Ï3ÿ>O0Rùv¥×;BßôýÉÐïà‹ÿ7à‰È„“î~ûÀ¿ ž–u —7|H‚ÖÎ]ƒc×k¦þ¯~¦î�‹þ ¸¢q ûÕû…V‚g‹0 xœÍVMoÛ0 ½ûWpÞe’+P`è_Úa;lX�E{dd:Ö [†$§õ~ý(Ë.²$kê}a@Û”øH?’ON_\|9¿¾»|¥«T¥ã…0çKEA”h,¹eܺbþ6NØî¤S”Y‡Î.ü²[\ tŽÌGi�^¬Î½>“+unÓ$8Di2¯tÞù0ozv’žÁ@öŒR„+ZÆGý[ã,MÐgqrüU[9�h@(´v—§PXþÍùÑà �?ƒ¦3H7h2½úš&þ–�®Ž»¥É*ƒW½kƒ†j¼gÍ6$~0 YÛ`x�&}biÒøä387„Žá™ þÀ“p‹(år3òPÙùi°÷òiÁ•…\·†@h°&NC£PP¿ØYÏü:U�ë Ð& k­ñhÀ îK)J�jÈjµ¡t­:þc,Ú�é`ƒª%(ÑŠ¨Ž… ÆQ¾€]®< ršV8Ÿ�@c:Y¯ûÀ9ò4 �Fš5åHÍÈÊcÒqrÒ•Ãë’Êíàz{ÁPÃþÙˆw·mlŒn›ö+ÝA׸R´ s{ƒFn/Œð;vÀ:ߎ²çÆëÑnëô¯â‡n8mP Š çëÇ*Á”^{ª´h+.³p�¯EL O3,t(æâJû4�cwùVb']¯³+â^UVóÜ$ƒ)âJ"”†Še|©´s\³…ײ—»°ñ�@^ú ÷ð$]yP²š$#Þá jLU Æ‘În F€~Ô„òà°ÿ¶Rý,Îl?­=�ñ�6€.úV*µ‘ßtíP>HËM÷8"Ÿ¤Ð¬¥öÓÃ.Õ;XË�Ÿâ½x3߀ýž'$ir‰»©%îþ^‰»TâÃq~­Ä¬À|ºïøî)pNŠM*qpùc§ÿ*�”6ƒ‹~ãÖéUÜ?áY>q¼žæïk><�“” ßaIøìûeTvR�'‚f R5Šxœ½›ÝoÛ6ÀßõWpyé‹óÕ!Û�¤¶´[tY°fK…¡´D[D(Q#©8î_¿;RÔ‡-'Q*�XÔéß}ðNTο{ûçåM|ýŽd&ѹÿÅh ¿rf(I2ª43oö*³Øÿiï®n»Ð†}€ÃæàcB�aê=×F.Í/ñ*)•,™2œéóC'�Öºç2]ãLÇVÈñDŸ’q:A8:§¤ 9{³÷¤èÁ§·Ôн‹óCŠ¿~ú 8 ˜ç5”$Të7{Ù YhøÙ‡�Ô%2}ŒS¿ò59Ÿ_ÔŠç°+~~Xâä&c䞊ŠiB…,–ÄÀ…L*þE† B¸žøÎUÎOH"E•äž%FªBn3¦Xd¥ìbÈŠj’*º*ÈBÉ$ � F¸!\¶p×QÀ�Ð"%`oËeÔܶ¨4K"·Ì”ß{¹Þ?ÙC«ÂµQ¶ˆGÛ"e‹x”-îÑù’§,A€º¡:"nUµÉíßS’üMɪM³‘ A´£|˜êo rAM2"çš©{H²8 [‚lD`!l ¡ÁÀ†=˜ÚbΔ& .äÒšÉp†—X^ÂúðW!Ýô”†ø(+•° ¢Q¦èÈ…0FOý°9\àïÊ-»)i}ú‡*>šU#$)·Êw%7ŽžëfèÁuž™I‘Bî õñ‹ðÅ!ñÅðÅ!“æ‹ö$ƒ%Ï(;IÙ~ÖÍÇ·\—‚®?šõHÀƒB€Þ1Ñ6ð÷råÊ0/õ€b.CžÖt_Áä\éW \HÙ‚Vö%ȧ𙫈ÈÊ^°3/6§ÊËà]ðQ7c:—ÒdÝaJî˜*˜Í…æf…sûà¬Ý'ït=ê™—•�*†›)M}Uå¿ðB�2n-œ�êáˆq¥ÆËîjj�ƦXʽޟ¯÷�}‚…X-ê•ßP÷ª@ˆ6šj W,¢øYq“ÁEWFV¡+xäÊ „|tÅ6ܨA#UX}ƒ'€éað�­YV®ªw\vø­³5×ÃÞ3¥áó-OM6Îô�Tãw”›…£­‡0#:£%KÛ¸iÈk M�¡w *ùR±„At%�xÁg`-F†‰ ~’BÈ•v=Ö´áW_½Ò|�‰­žÇ•�ÛòAÊÏ¡i¶mtk™%²›�:Œ”#%:ü³Í$ ‰”×]™YɈhžb«¶¢kÏ»Éΰ;šì’åp‚Žz÷½£Ýûê‘+©ú#ðݱ®ö@v¾®0ÕWXºÕØÖ݉†#ÂO1‘.<1»Ö`·\ff§'ÐÈ'ô93+‘vd¥�gäèàÇ2_{ÛNÚ³tR/d‚ц�ÒÇ M³+ä0ZlN¤J7ð½|[^öšoÅ5;mb¦*wT=m€¤rU„ˆ�ø+ 2 ¤3 `�x·A†Î“6l$_}úÀsnÆÕµL�`𪇳�°£ çÍ­xQÑbÉvDBU."¬Ø&uë ŒÃ!ŒÇ Üx>:0Þ85¿K)¤EÏJ„`W+&�tíM/b;Ñ�AË —iAþúíßNøþá ¶·È‡´{lg56ðzUÊÔ”\?@ç:¾¡o¤Bðí(ßå�€Sã[ÉTo’w‡]4Áj£n%ôìp™ ph¥ 5;%ùÔXm÷2«• …µVþŒ~ªÝž�¶=©[Y$ü›Î·}tpªÑþ êŽ©ñ^Ú‘ ´§~inoy–OºdÛÅȦG ʘyb HØj ýõ.ë`k ¢ÿ«°)%/Ì#¾y‹|ÿCP¬¿r!Xú°N0Z¯¨peÀTõÀ"Ò…•hZhY ø¥Í°þ†Å"DEê–ÿ³(³qg¤¹pTkõÃO�eIÿ«Ø&×™{¼qDŒÄ†÷8�;~`KV¤ÿpÍÇž‘ô$ƒlOý ÷Haoö�ï»âsR�AW:äœÃyÙÀ=ˆE=ʦ“ZÛ,Lë¾å ¾8óüV.üZýŽeôÄÿ³þû'8€ìA~瀪=øª»× dÇ3 Fóù�ÊI;u:gbd£Ž"aút§yG�ƒ;ºô§=ëÓ·ñªx<Ï8Ïøù<{-ûÓ4ãoCóW°ðÜ;Ч A´Õ=Ìtã¶zžkûBîMä6;Í{&î\¥ãzG×è^(ÀÇês c�„bÝWéÇG�¨]KÍG?GöB!¨µº‡©•õ¸�æÞq Ãgs%Ô*…}Áñ¿øøw†§Ìßð²íü¬>uùÜ&õÝöùŸ?½Í¸hŽ£ýú.e¡�¢`ží³hYÁŠüB·o_8+Mð|•{fÞÌû'ʵjµF3±8ó/©¢Ð‚?0ÝýöQ}Zä 5<´¯T¡Ÿ\±‘MåÇ‹b{ÉðÝÝðï8ºµ°z àiSzdÂ(·ÜÀ7½onLð<]eRxÃoø§Ý–°)OD•BçØxëÑŒÀ6;Ÿ·“¦=Õ@ÍÖͶæ£öÕ—¬+ïìën0"o´Nõ¢ ÒõÉp¹¤;Ë®s¨c¦wŒ•úôÑ Þ _UClassificationBaggedEnsemble_methods.html>ClassificationBaggedEnsemble MethodsY= e[ClassificationBaggedEnsemble_properties.html=ClassificationBaggedEnsemble propertiesC< OEClassificationBaggedEnsemble.html� I?CompactTreeBagger_methods.html›CompactTreeBagger MethodsD� OECompactTreeBagger_properties.htmlšCompactTreeBagger properties.� 9/CompactTreeBagger.html™CompactTreeBaggerF� QGCompactRegressionTree_methods.html˜CompactRegressionTree MethodsL� WMCompactRegressionTree_properties.html—CompactRegressionTree properties6� A7CompactRegressionTree.html–CompactRegressionTreeD� OECompactRegressionSVM_methods.html•CompactRegressionSVM MethodsJ� UKCompactRegressionSVM_properties.html”CompactRegressionSVM properties4� ?5CompactRegressionSVM.html“CompactRegressionSVMX� cYCompactRegressionNeuralNetwork_methods.html’CompactRegressionNeuralNetwork Methods^� i_CompactRegressionNeuralNetwork_properties.html‘CompactRegressionNeuralNetwork propertiesH� SICompactRegressionNeuralNetwork.html�CompactRegressionNeuralNetworkB� MCCompactRegressionGP_methods.html�CompactRegressionGP MethodsH� SICompactRegressionGP_properties.htmlŽCompactRegressionGP properties2�  =3CompactRegressionGP.html�CompactRegressionGPN�  YOCompactRegressionEnsemble_methods.htmlŒCompactRegressionEnsemble MethodsT�  _UCompactRegressionEnsemble_properties.html‹CompactRegressionEnsemble properties>�  I?CompactRegressionEnsemble.htmlŠCompactRegressionEnsembleD�  OECompactRegressionGAM_methods.html‰CompactRegressionGAM MethodsJ� UKCompactRegressionGAM_properties.htmlˆCompactRegressionGAM properties4� ?5CompactRegressionGAM.html‡CompactRegressionGAMN� YOCompactClassificationTree_methods.html†CompactClassificationTree MethodsT� _UCompactClassificationTree_properties.html…CompactClassificationTree properties>� I?CompactClassificationTree.html„CompactClassificationTreeL� WMCompactClassificationSVM_methods.htmlƒCompactClassificationSVM MethodsR� ]SCompactClassificationSVM_properties.html‚CompactClassificationSVM properties<� G=CompactClassificationSVM.html�CompactClassificationSVM`� kaCompactClassificationNeuralNetwork_methods.html€CompactClassificationNeuralNetwork Methodse qgCompactClassificationNeuralNetwork_properties.htmlCompactClassificationNeuralNetwork propertiesO~ [QCompactClassificationNeuralNetwork.html~CompactClassificationNeuralNetworkY} e[CompactClassificationNaiveBayes_methods.html}CompactClassificationNaiveBayes Methods_| kaCompactClassificationNaiveBayes_properties.html|CompactClassificationNaiveBayes propertiesI{ UKCompactClassificationNaiveBayes.html{CompactClassificationNaiveBayes @¢·Xÿº_ Å j  Ò y & ý ¾ … Z  Þ Ç š s V # ö Ó š g $ËxIň5è¿€GÛ iÕ¢Yï²{PÔ§d'ô«hCÓ¢.�r 9/ReconstructionICA.htmlòReconstructionICA2�q =3OneClassSVM_methods.htmlñOneClassSVM Methods8�p C9OneClassSVM_properties.htmlðOneClassSVM properties"�o -#OneClassSVM.htmlïOneClassSVM@�n KALocalOutlierFactor_methods.htmlîLocalOutlierFactor MethodsF�m QGLocalOutlierFactor_properties.htmlíLocalOutlierFactor properties0�l ;1LocalOutlierFactor.htmlìLocalOutlierFactor:�k E;IsolationForest_methods.htmlëIsolationForest Methods@�j KAIsolationForest_properties.htmlêIsolationForest properties*�i 5+IsolationForest.htmléIsolationForest8�h C9KDTreeSearcher_methods.htmlèKDTreeSearcher Methods>�g I?KDTreeSearcher_properties.htmlçKDTreeSearcher properties(�f 3)KDTreeSearcher.htmlæKDTreeSearcher4�e ?5hnswSearcher_methods.htmlåhnswSearcher Methods:�d E;hnswSearcher_properties.htmlähnswSearcher properties$�c /%hnswSearcher.htmlãhnswSearcher@�b KAExhaustiveSearcher_methods.htmlâExhaustiveSearcher MethodsF�a QGExhaustiveSearcher_properties.htmláExhaustiveSearcher properties0�` ;1ExhaustiveSearcher.htmlàExhaustiveSearcherD�_ OESilhouetteEvaluation_methods.htmlßSilhouetteEvaluation MethodsJ�^ UKSilhouetteEvaluation_properties.htmlÞSilhouetteEvaluation properties4�] ?5SilhouetteEvaluation.htmlÝSilhouetteEvaluation8�\ C9gmdistribution_methods.htmlÜgmdistribution Methods>�[ I?gmdistribution_properties.htmlÛgmdistribution properties(�Z 3)gmdistribution.htmlÚgmdistribution6�Y A7GapEvaluation_methods.htmlÙGapEvaluation Methods<�X G=GapEvaluation_properties.htmlØGapEvaluation properties&�W 1'GapEvaluation.html×GapEvaluationJ�V UKDaviesBouldinEvaluation_methods.htmlÖDaviesBouldinEvaluation MethodsP�U [QDaviesBouldinEvaluation_properties.htmlÕDaviesBouldinEvaluation properties:�T E;DaviesBouldinEvaluation.htmlÔDaviesBouldinEvaluation<�S G=ClusterCriterion_methods.htmlÓClusterCriterion MethodsB�R MCClusterCriterion_properties.htmlÒClusterCriterion properties,�Q 7-ClusterCriterion.htmlÑClusterCriterionP�P [QCalinskiHarabaszEvaluation_methods.htmlÐCalinskiHarabaszEvaluation MethodsV�O aWCalinskiHarabaszEvaluation_properties.htmlÏCalinskiHarabaszEvaluation properties@�N KACalinskiHarabaszEvaluation.htmlÎCalinskiHarabaszEvaluation0�M ;1TreeBagger_methods.htmlÍTreeBagger Methods6�L A7TreeBagger_properties.htmlÌTreeBagger properties �K +!TreeBagger.htmlËTreeBagger*�J 5+shapley_methods.htmlÊshapley Methods0�I ;1shapley_properties.htmlÉshapley properties�H %shapley.htmlÈshapley$�G /%lime_methods.htmlÇlime Methods*�F 5+lime_properties.htmlÆlime properties�E lime.htmlÅlime8�D C9RegressionTree_methods.htmlÄRegressionTree Methods>�C I?RegressionTree_properties.htmlÃRegressionTree properties(�B 3)RegressionTree.htmlÂRegressionTree6�A A7RegressionSVM_methods.htmlÁRegressionSVM Methods<�@ G=RegressionSVM_properties.htmlÀRegressionSVM properties&�? 1'RegressionSVM.html¿RegressionSVMP�> [QRegressionPartitionedModel_methods.html¾RegressionPartitionedModel MethodsV�= aWRegressionPartitionedModel_properties.html½RegressionPartitionedModel properties@�< KARegressionPartitionedModel.html¼RegressionPartitionedModelR�; ]SRegressionPartitionedLinear_methods.html»RegressionPartitionedLinear MethodsX�: cYRegressionPartitionedLinear_properties.htmlºRegressionPartitionedLinear propertiesB�9 MCRegressionPartitionedLinear.html¹RegressionPartitionedLinearR�8 ]SRegressionPartitionedKernel_methods.html¸RegressionPartitionedKernel MethodsX�7 cYRegressionPartitionedKernel_properties.html·RegressionPartitionedKernel propertiesB�6 MCRegressionPartitionedKernel.html¶RegressionPartitionedKernelV�5 aWRegressionPartitionedEnsemble_methods.htmlµRegressionPartitionedEnsemble Methods\�4 g]RegressionPartitionedEnsemble_properties.html´RegressionPartitionedEnsemble propertiesF�3 QGRegressionPartitionedEnsemble.html³RegressionPartitionedEnsemble 5�¹xKË”G Û   k H  Ü · | G Ç x / î — F õ Ž -ô¥\¸a¹`%Ô‰*µFóŠ'â‡2ã~â�P‚' [Qprob.KernelDistribution_properties.html'prob.KernelDistribution properties:‚& E;prob.KernelDistribution.html&prob.KernelDistribution\‚% g]prob.InverseGaussianDistribution_methods.html%prob.InverseGaussianDistribution Methodsb‚$ mcprob.InverseGaussianDistribution_properties.html$prob.InverseGaussianDistribution propertiesL‚# WMprob.InverseGaussianDistribution.html#prob.InverseGaussianDistributionR‚" ]Sprob.HalfNormalDistribution_methods.html"prob.HalfNormalDistribution MethodsX‚! cYprob.HalfNormalDistribution_properties.html!prob.HalfNormalDistribution propertiesB‚  MCprob.HalfNormalDistribution.html prob.HalfNormalDistribution`‚ kaprob.GeneralizedParetoDistribution_methods.htmlprob.GeneralizedParetoDistribution Methodsf‚ qgprob.GeneralizedParetoDistribution_properties.htmlprob.GeneralizedParetoDistribution propertiesP‚ [Qprob.GeneralizedParetoDistribution.htmlprob.GeneralizedParetoDistributionl‚ wmprob.GeneralizedExtremeValueDistribution_methods.htmlprob.GeneralizedExtremeValueDistribution Methodsr‚ }sprob.GeneralizedExtremeValueDistribution_properties.htmlprob.GeneralizedExtremeValueDistribution properties\‚ g]prob.GeneralizedExtremeValueDistribution.htmlprob.GeneralizedExtremeValueDistributionH‚ SIprob.GammaDistribution_methods.htmlprob.GammaDistribution MethodsN‚ YOprob.GammaDistribution_properties.htmlprob.GammaDistribution properties8‚ C9prob.GammaDistribution.htmlprob.GammaDistributionV‚ aWprob.ExtremeValueDistribution_methods.htmlprob.ExtremeValueDistribution Methods\‚ g]prob.ExtremeValueDistribution_properties.htmlprob.ExtremeValueDistribution propertiesF‚ QGprob.ExtremeValueDistribution.htmlprob.ExtremeValueDistributionT‚ _Uprob.ExponentialDistribution_methods.htmlprob.ExponentialDistribution MethodsZ‚ e[prob.ExponentialDistribution_properties.htmlprob.ExponentialDistribution propertiesD‚ OEprob.ExponentialDistribution.htmlprob.ExponentialDistributionF‚ QGprob.BurrDistribution_methods.htmlprob.BurrDistribution MethodsL‚ WMprob.BurrDistribution_properties.htmlprob.BurrDistribution properties6‚ A7prob.BurrDistribution.htmlprob.BurrDistribution^‚  i_prob.BirnbaumSaundersDistribution_methods.html prob.BirnbaumSaundersDistribution Methodsd‚  oeprob.BirnbaumSaundersDistribution_properties.html prob.BirnbaumSaundersDistribution propertiesN‚  YOprob.BirnbaumSaundersDistribution.html prob.BirnbaumSaundersDistributionN‚  YOprob.BinomialDistribution_methods.html prob.BinomialDistribution MethodsT‚  _Uprob.BinomialDistribution_properties.html prob.BinomialDistribution properties>‚ I?prob.BinomialDistribution.htmlprob.BinomialDistributionF‚ QGprob.BetaDistribution_methods.htmlprob.BetaDistribution MethodsL‚ WMprob.BetaDistribution_properties.htmlprob.BetaDistribution properties6‚ A7prob.BetaDistribution.htmlprob.BetaDistributionD‚ OEprob.ProbabilityDistribution.htmlprob.ProbabilityDistribution2‚ =3paretotails_methods.htmlparetotails Methods8‚ C9paretotails_properties.htmlparetotails properties"‚ -#paretotails.htmlparetotails0‚ ;1rocmetrics_methods.htmlrocmetrics Methods6� A7rocmetrics_properties.htmlÿrocmetrics properties �~ +!rocmetrics.htmlþrocmetrics2�} =3cvpartition_methods.htmlýcvpartition Methods8�| C9cvpartition_properties.htmlücvpartition properties"�{ -#cvpartition.htmlûcvpartitionD�z OEConfusionMatrixChart_methods.htmlúConfusionMatrixChart MethodsJ�y UKConfusionMatrixChart_properties.htmlùConfusionMatrixChart properties4�x ?5ConfusionMatrixChart.htmløConfusionMatrixChart:�w E;SparseFiltering_methods.html÷SparseFiltering Methods@�v KASparseFiltering_properties.htmlöSparseFiltering properties*�u 5+SparseFiltering.htmlõSparseFiltering>�t I?ReconstructionICA_methods.htmlôReconstructionICA MethodsD�s OEReconstructionICA_properties.htmlóReconstructionICA properties 1“³rʃ& Ï Œ 3 à › @ ë ¤ G ð ¯ X  ¶ Oî±^Â]þ¿jÚƒ2õ¢UÅx+Èk&Ëv7â“L‚X WMprob.UniformDistribution_methods.htmlXprob.UniformDistribution MethodsR‚W ]Sprob.UniformDistribution_properties.htmlWprob.UniformDistribution properties<‚V G=prob.UniformDistribution.htmlVprob.UniformDistributionR‚U ]Sprob.TriangularDistribution_methods.htmlUprob.TriangularDistribution MethodsX‚T cYprob.TriangularDistribution_properties.htmlTprob.TriangularDistribution propertiesB‚S MCprob.TriangularDistribution.htmlSprob.TriangularDistributionZ‚R e[prob.tLocationScaleDistribution_methods.htmlRprob.tLocationScaleDistribution Methods`‚Q kaprob.tLocationScaleDistribution_properties.htmlQprob.tLocationScaleDistribution propertiesJ‚P UKprob.tLocationScaleDistribution.htmlPprob.tLocationScaleDistributionJ‚O UKprob.StableDistribution_methods.htmlOprob.StableDistribution MethodsP‚N [Qprob.StableDistribution_properties.htmlNprob.StableDistribution properties:‚M E;prob.StableDistribution.htmlMprob.StableDistributionJ‚L UKprob.RicianDistribution_methods.htmlLprob.RicianDistribution MethodsP‚K [Qprob.RicianDistribution_properties.htmlKprob.RicianDistribution properties:‚J E;prob.RicianDistribution.htmlJprob.RicianDistributionN‚I YOprob.RayleighDistribution_methods.htmlIprob.RayleighDistribution MethodsT‚H _Uprob.RayleighDistribution_properties.htmlHprob.RayleighDistribution properties>‚G I?prob.RayleighDistribution.htmlGprob.RayleighDistributionL‚F WMprob.PoissonDistribution_methods.htmlFprob.PoissonDistribution MethodsR‚E ]Sprob.PoissonDistribution_properties.htmlEprob.PoissonDistribution properties<‚D G=prob.PoissonDistribution.htmlDprob.PoissonDistribution\‚C g]prob.PiecewiseLinearDistribution_methods.htmlCprob.PiecewiseLinearDistribution Methodsb‚B mcprob.PiecewiseLinearDistribution_properties.htmlBprob.PiecewiseLinearDistribution propertiesL‚A WMprob.PiecewiseLinearDistribution.htmlAprob.PiecewiseLinearDistributionJ‚@ UKprob.NormalDistribution_methods.html@prob.NormalDistribution MethodsP‚? [Qprob.NormalDistribution_properties.html?prob.NormalDistribution properties:‚> E;prob.NormalDistribution.html>prob.NormalDistribution^‚= i_prob.NegativeBinomialDistribution_methods.html=prob.NegativeBinomialDistribution Methodsd‚< oeprob.NegativeBinomialDistribution_properties.html‚8 I?prob.NakagamiDistribution.html8prob.NakagamiDistributionT‚7 _Uprob.MultinomialDistribution_methods.html7prob.MultinomialDistribution MethodsZ‚6 e[prob.MultinomialDistribution_properties.html6prob.MultinomialDistribution propertiesD‚5 OEprob.MultinomialDistribution.html5prob.MultinomialDistributionR‚4 ]Sprob.LoguniformDistribution_methods.html4prob.LoguniformDistribution MethodsX‚3 cYprob.LoguniformDistribution_properties.html3prob.LoguniformDistribution propertiesB‚2 MCprob.LoguniformDistribution.html2prob.LoguniformDistributionP‚1 [Qprob.LognormalDistribution_methods.html1prob.LognormalDistribution MethodsV‚0 aWprob.LognormalDistribution_properties.html0prob.LognormalDistribution properties@‚/ KAprob.LognormalDistribution.html/prob.LognormalDistributionT‚. _Uprob.LoglogisticDistribution_methods.html.prob.LoglogisticDistribution MethodsZ‚- e[prob.LoglogisticDistribution_properties.html-prob.LoglogisticDistribution propertiesD‚, OEprob.LoglogisticDistribution.html,prob.LoglogisticDistributionN‚+ YOprob.LogisticDistribution_methods.html+prob.LogisticDistribution MethodsT‚* _Uprob.LogisticDistribution_properties.html*prob.LogisticDistribution properties>‚) I?prob.LogisticDistribution.html)prob.LogisticDistributionJ‚( UKprob.KernelDistribution_methods.html(prob.KernelDistribution Methods  1Álæ™R ¾ o  ¯ J ù ’ 1^‚g i_stats.chart.ScatterHistogramChart_methods.htmlgstats.chart.ScatterHistogramChart Methodsd‚f oestats.chart.ScatterHistogramChart_properties.htmlfstats.chart.ScatterHistogramChart propertiesN‚e YOstats.chart.ScatterHistogramChart.htmlestats.chart.ScatterHistogramChartb‚d mcstats.chart.ParallelCoordinatesPlot_methods.htmldstats.chart.ParallelCoordinatesPlot Methodsh‚c sistats.chart.ParallelCoordinatesPlot_properties.htmlcstats.chart.ParallelCoordinatesPlot propertiesR‚b ]Sstats.chart.ParallelCoordinatesPlot.htmlbstats.chart.ParallelCoordinatesPlotL‚a WMstats.chart.HeatmapChart_methods.htmlastats.chart.HeatmapChart MethodsR‚` ]Sstats.chart.HeatmapChart_properties.html`stats.chart.HeatmapChart properties<‚_ G=stats.chart.HeatmapChart.html_stats.chart.HeatmapChartD‚^ OEstats.chart.BoxChart_methods.html^stats.chart.BoxChart MethodsJ‚] UKstats.chart.BoxChart_properties.html]stats.chart.BoxChart properties4‚\ ?5stats.chart.BoxChart.html\stats.chart.BoxChartL‚[ WMprob.WeibullDistribution_methods.html[prob.WeibullDistribution MethodsR‚Z ]Sprob.WeibullDistribution_properties.htmlZprob.WeibullDistribution properties<‚Y G=prob.WeibullDistribution.htmlYprob.WeibullDistribution@methods.htmlMethodsBstats.drift.DriftDiagnostics.html8stats.drift.DriftDiagnosticsXstats.drift.DriftDiagnostics_properties.htmlPropertiesRstats.drift.DriftDiagnostics_methods.htmlMethods0Experimental_Design.html&Experimental DesignRegression.htmlRegression.CompactLinearModel.html$CompactLinearModelDCompactLinearModel_properties.htmlProperties>CompactLinearModel_methods.htmlMethodsCoxModel.htmlCoxModel0CoxModel_properties.htmlProperties*CoxModel_methods.htmlMethods@GeneralizedLinearMixedModel.html6GeneralizedLinearMixedModelVGeneralizedLinearMixedModel_properties.htmlPropertiesPGeneralizedLinearMixedModel_methods.htmlMethods6GeneralizedLinearModel.html,GeneralizedLinearModelLGeneralizedLinearModel_properties.htmlPropertiesFGeneralizedLinearModel_methods.htmlMethods$LinearFormula.htmlLinearFormula:LinearFormula_properties.htmlProperties4LinearFormula_methods.htmlMethods*LinearMixedModel.html LinearMixedModel@LinearMixedModel_properties.htmlProperties:LinearMixedModel_methods.htmlMethods LinearModel.htmlLinearModel6LinearModel_properties.htmlProperties0LinearModel_methods.htmlMethods4MultinomialRegression.html*MultinomialRegressionJMultinomialRegression_properties.htmlPropertiesDMultinomialRegression_methods.htmlMethods&NonLinearModel.htmlNonLinearModel<NonLinearModel_properties.htmlProperties6NonLinearModel_methods.htmlMethods4RepeatedMeasuresModel.html*RepeatedMeasuresModelJRepeatedMeasuresModel_properties.htmlPropertiesDRepeatedMeasuresModel_methods.htmlMethods*Machine_Learning.html Machine LearningBClassificationBaggedEnsemble.html8ClassificationBaggedEnsembleXClassificationBaggedEnsemble_properties.htmlPropertiesRClassificationBaggedEnsemble_methods.htmlMethods>ClassificationDiscriminant.html4ClassificationDiscriminantTClassificationDiscriminant_properties.htmlPropertiesNClassificationDiscriminant_methods.htmlAMethods.ClassificationECOC.html$ClassificationECOCDClassificationECOC_properties.htmlProperties>ClassificationECOC_methods.htmlMethods6ClassificationEnsemble.html,ClassificationEnsembleLClassificationEnsemble_properties.htmlPropertiesFClassificationEnsemble_methods.htmlMethods,ClassificationGAM.html"ClassificationGAMBClassificationGAM_properties.htmlProperties<ClassificationGAM_methods.htmlMethods2ClassificationKernel.html(ClassificationKernelHClassificationKernel_properties.htmlPropertiesBClassificationKernel_methods.htmlMethods,ClassificationKNN.html"ClassificationKNNBClassificationKNN_properties.htmlProperties<ClassificationKNN_methods.htmlMethods2ClassificationLinear.html(ClassificationLinearHClassificationLinear_properties.htmlPropertiesBClassificationLinear_methods.htmlMethods:ClassificationNaiveBayes.html0ClassificationNaiveBayesPClassificationNaiveBayes_properties.htmlPropertiesJClassificationNaiveBayes_methods.htmlMethods@ClassificationNeuralNetwork.html6ClassificationNeuralNetworkVClassificationNeuralNetwork_properties.htmlPropertiesPClassificationNeuralNetwork_methods.htmlMethodsDClassificationPartitionedECOC.html:ClassificationPartitionedECOCZClassificationPartitionedECOC_properties.htmlPropertiesTClassificationPartitionedECOC_methods.htmlMethodsLClassificationPartitionedEnsemble.htmlBClassificationPartitionedEnsemblebClassificationPartitionedEnsemble_properties.htmlProperties\ClassificationPartitionedEnsemble_methods.htmlMethodsHClassificationPartitionedKernel.html>ClassificationPartitionedKernel^ClassificationPartitionedKernel_properties.htmlPropertiesXClassificationPartitionedKernel_methods.htmlMethodsHClassificationPartitionedLinear.html>ClassificationPartitionedLinear^ClassificationPartitionedLinear_properties.htmlPropertiesXClassificationPartitionedLinear_methods.htmlMethodsFClassificationPartitionedModel.html<ClassificationBPartitionedModel\ClassificationPartitionedModel_properties.htmlPropertiesVClassificationPartitionedModel_methods.htmlMethods,ClassificationSVM.html"ClassificationSVMBClassificationSVM_properties.htmlProperties<ClassificationSVM_methods.htmlMethods.ClassificationTree.html$ClassificationTreeDClassificationTree_properties.htmlProperties>ClassificationTree_methods.htmlMethodsLCompactClassificationDiscriminant.htmlBCompactClassificationDiscriminantbCompactClassificationDiscriminant_properties.htmlProperties\CompactClassificationDiscriminant_methods.htmlMethods<CompactClassificationECOC.html2CompactClassificationECOCRCompactClassificationECOC_properties.htmlPropertiesLCompactClassificationECOC_methods.htmlMethodsDCompactClassificationEnsemble.html:CompactClassificationEnsembleZCompactClassificationEnsemble_properties.htmlPropertiesTCompactClassificationEnsemble_methods.htmlMethods:CompactClassificationGAM.html0CompactClassificationGAMPCompactClassificationGAM_properties.htmlPropertiesJCompactClassificationGAM_methods.htmlMethodsHCompactClassificationNaiveBayes.html>CompactClassificationNaiveBayes^CompactClassificationNaiveBayes_properties.htmlPropertiesXCompactClassificationNaiveBayes_methods.htmlMethodsNCompactClassificationNeuralNetwork.htmlDCompactClassificationNeuralNetworkdCompactClassificationNeuralNetwork_properties.htmlProperties^CompactClassificationNeuralNetwork_methods.htmlMethods:CompactClassificationSVM.html0CompactClassificationSVMPCompactClassificationSVM_properties.htmlPropertiesJCompactClassificationSVM_methods.htmlMethods<CompactClassificationTree.html2CompactClassificationTreeRCompactClassificationTree_properties.htmlPropertiesLCompactClassificationTree_methods.htmlMethods2CompactRegressionGAM.html(CompactRegressionGAMHCompactRegressionGAM_properties.htmlPropertiesBCompactRegressionGAM_methods.htmlMethods<CompactRegressionEnsemble.htmCl2CompactRegressionEnsembleRCompactRegressionEnsemble_properties.htmlPropertiesLCompactRegressionEnsemble_methods.htmlMethods0CompactRegressionGP.html&CompactRegressionGPFCompactRegressionGP_properties.htmlProperties@CompactRegressionGP_methods.htmlMethodsFCompactRegressionNeuralNetwork.html<CompactRegressionNeuralNetwork\CompactRegressionNeuralNetwork_properties.htmlPropertiesVCompactRegressionNeuralNetwork_methods.htmlMethods2CompactRegressionSVM.html(CompactRegressionSVMHCompactRegressionSVM_properties.htmlPropertiesBCompactRegressionSVM_methods.htmlMethods4CompactRegressionTree.html*CompactRegressionTreeJCompactRegressionTree_properties.htmlPropertiesDCompactRegressionTree_methods.htmlMethods,CompactTreeBagger.html"CompactTreeBaggerBCompactTreeBagger_properties.htmlProperties<CompactTreeBagger_methods.htmlMethods(PredictiveModel.htmlPredictiveModel8PredictiveModel_methods.htmlMethods:RegressionBaggedEnsemble.html0RegressionBaggedEnsemblePRegressionBaggedEnsemble_properties.htmlPropertiesJRegressionBaggedEnsemble_methods.htmlMethods.RegressionEnsemble.html$RegressionEnsembleDRegressionEnsemble_properties.htmlProperties>RegressionEnsemble_methods.htmlMethods$RegressionGAM.htmlRegressionGAM:RegressionGAM_properties.htmlProperties4RegressionGAM_methods.htmlMethods"RegressionGP.htmlRegressionGP8RegressionGP_properties.htmlProperties2RegressionGP_methods.htmlMethods*RegressionKernel.html RegressionKernel@RegressionKernel_properties.htmlProperties:RegressionKernel_methods.htmlMethods*RegressionLinear.html RegressionLinear@RegressionLinear_properties.htmlProperties:RegressionLinear_methods.htmlMethods8RegressionNeuralNetwork.html.RegressionNeuralNetworkNRegressionNeuralNetwork_properties.htmlPropertiesHRegressionNeuralNetwork_methods.htmlMethodsDRegressionPartitionedEnsemble.html:RegressionPartitionedEnsembleZRegressionPDartitionedEnsemble_properties.htmlPropertiesTRegressionPartitionedEnsemble_methods.htmlMethods@RegressionPartitionedKernel.html6RegressionPartitionedKernelVRegressionPartitionedKernel_properties.htmlPropertiesPRegressionPartitionedKernel_methods.htmlMethods@RegressionPartitionedLinear.html6RegressionPartitionedLinearVRegressionPartitionedLinear_properties.htmlPropertiesPRegressionPartitionedLinear_methods.htmlMethods>RegressionPartitionedModel.html4RegressionPartitionedModelTRegressionPartitionedModel_properties.htmlPropertiesNRegressionPartitionedModel_methods.htmlMethods$RegressionSVM.htmlRegressionSVM:RegressionSVM_properties.htmlProperties4RegressionSVM_methods.htmlMethods&RegressionTree.htmlRegressionTree<RegressionTree_properties.htmlProperties6RegressionTree_methods.htmlMethodslime.htmllime(lime_properties.htmlProperties"lime_methods.htmlMethodsshapley.htmlshapley.shapley_properties.htmlProperties(shapley_methods.htmlMethodsTreeBagger.htmlTreeBagger4TreeBagger_properties.htmlProperties.TreeBagger_methods.htmlMethodsClustering.htmlClustering>CalinskiHarabaszEvaluation.html4CalinskiHarabaszEvaluationTCalinskiHarabaszEvaluation_properties.htmlPropertiesNCalinskiHarabaszEvaluation_methods.htmlMethods*ClusterCriterion.html ClusterCriterion@ClusterCriterion_properties.htmlProperties:ClusterCriterion_methods.htmlMethods8DaviesBouldinEvaluation.html.DaviesBouldinEvaluationNDaviesBouldinEvaluation_properties.htmlPropertiesHDaviesBouldinEvaluation_methods.htmlMethods$GapEvaluation.htmlGapEvaluation:GapEvaluation_properties.htmlProperties4GapEvaluation_methods.htmlMethods&gmdistribution.htmlgmdistribution<gmdistribution_properties.htmlProperties6gmdistribution_methods.htmlMethods2SilhouetteEvaluation.html(SilhouetteEvaluationHSilhouetteEvaluation_properties.htmlPropertiesBSilhouetteEvaluation_methods.hEtmlMethods,Nearest_Neighbors.html"Nearest Neighbors.ExhaustiveSearcher.html$ExhaustiveSearcherDExhaustiveSearcher_properties.htmlProperties>ExhaustiveSearcher_methods.htmlMethods"hnswSearcher.htmlhnswSearcher8hnswSearcher_properties.htmlProperties2hnswSearcher_methods.htmlMethods&KDTreeSearcher.htmlKDTreeSearcher<KDTreeSearcher_properties.htmlProperties6KDTreeSearcher_methods.htmlMethods,Anomaly_Detection.html"Anomaly Detection(IsolationForest.htmlIsolationForest>IsolationForest_properties.htmlProperties8IsolationForest_methods.htmlMethods.LocalOutlierFactor.html$LocalOutlierFactorDLocalOutlierFactor_properties.htmlProperties>LocalOutlierFactor_methods.htmlMethods OneClassSVM.htmlOneClassSVM6OneClassSVM_properties.htmlProperties0OneClassSVM_methods.htmlMethods:Dimensionality_Reduction.html0Dimensionality Reduction,ReconstructionICA.html"ReconstructionICABReconstructionICA_properties.htmlProperties<ReconstructionICA_methods.htmlMethods(SparseFiltering.htmlSparseFiltering>SparseFiltering_properties.htmlProperties8SparseFiltering_methods.htmlMethods*Model_Evaluation.html Model Evaluation2ConfusionMatrixChart.html(ConfusionMatrixChartHConfusionMatrixChart_properties.htmlPropertiesBConfusionMatrixChart_methods.htmlMethods cvpartition.htmlcvpartition6cvpartition_properties.htmlProperties0cvpartition_methods.htmlMethodsrocmetrics.htmlrocmetrics4rocmetrics_properties.htmlProperties.rocmetrics_methods.htmlMethods$Markov_Models.htmlMarkov Models(Random_Sampling.htmlRandom Sampling2Distribution_Classes.html(Distribution Classes paretotails.htmlparetotails6paretotails_properties.htmlProperties0paretotails_methods.htmlMethodsBprob.ProbabilityDistribution.html8prob.ProbabilityDistribution4prob.BetaDistribution.html*prob.BetaDistributionJprob.BetaDistribution_properties.htmlPropertiesDprob.BetaDistributioFn_methods.htmlMethods<prob.BinomialDistribution.html2prob.BinomialDistributionRprob.BinomialDistribution_properties.htmlPropertiesLprob.BinomialDistribution_methods.htmlMethodsLprob.BirnbaumSaundersDistribution.htmlBprob.BirnbaumSaundersDistributionbprob.BirnbaumSaundersDistribution_properties.htmlProperties\prob.BirnbaumSaundersDistribution_methods.htmlMethods4prob.BurrDistribution.html*prob.BurrDistributionJprob.BurrDistribution_properties.htmlPropertiesDprob.BurrDistribution_methods.htmlMethodsBprob.ExponentialDistribution.html8prob.ExponentialDistributionXprob.ExponentialDistribution_properties.htmlPropertiesRprob.ExponentialDistribution_methods.htmlMethodsDprob.ExtremeValueDistribution.html:prob.ExtremeValueDistributionZprob.ExtremeValueDistribution_properties.htmlPropertiesTprob.ExtremeValueDistribution_methods.htmlMethods6prob.GammaDistribution.html,prob.GammaDistributionLprob.GammaDistribution_properties.htmlPropertiesFprob.GammaDistribution_methods.htmlMethodsZprob.GeneralizedExtremeValueDistribution.htmlPprob.GeneralizedExtremeValueDistributionpprob.GeneralizedExtremeValueDistribution_properties.htmlPropertiesjprob.GeneralizedExtremeValueDistribution_methods.htmlMethodsNprob.GeneralizedParetoDistribution.htmlDprob.GeneralizedParetoDistributiondprob.GeneralizedParetoDistribution_properties.htmlProperties^prob.GeneralizedParetoDistribution_methods.htmlMethods@prob.HalfNormalDistribution.html6prob.HalfNormalDistributionVprob.HalfNormalDistribution_properties.htmlPropertiesPprob.HalfNormalDistribution_methods.htmlMethodsJprob.InverseGaussianDistribution.html@prob.InverseGaussianDistribution`prob.InverseGaussianDistribution_properties.htmlPropertiesZprob.InverseGaussianDistribution_methods.htmlMethods8prob.KernelDistribution.html.prob.KernelDistributionNprob.KernelDistribution_properties.htmlPropertiesHprob.KernelDistribution_methods.htmlMethods<probG.LogisticDistribution.html2prob.LogisticDistributionRprob.LogisticDistribution_properties.htmlPropertiesLprob.LogisticDistribution_methods.htmlMethodsBprob.LoglogisticDistribution.html8prob.LoglogisticDistributionXprob.LoglogisticDistribution_properties.htmlPropertiesRprob.LoglogisticDistribution_methods.htmlMethods>prob.LognormalDistribution.html4prob.LognormalDistributionTprob.LognormalDistribution_properties.htmlPropertiesNprob.LognormalDistribution_methods.htmlMethods@prob.LoguniformDistribution.html6prob.LoguniformDistributionVprob.LoguniformDistribution_properties.htmlPropertiesPprob.LoguniformDistribution_methods.htmlMethodsBprob.MultinomialDistribution.html8prob.MultinomialDistributionXprob.MultinomialDistribution_properties.htmlPropertiesRprob.MultinomialDistribution_methods.htmlMethods<prob.NakagamiDistribution.html2prob.NakagamiDistributionRprob.NakagamiDistribution_properties.htmlPropertiesLprob.NakagamiDistribution_methods.htmlMethodsLprob.NegativeBinomialDistribution.htmlBprob.NegativeBinomialDistributionbprob.NegativeBinomialDistribution_properties.htmlProperties\prob.NegativeBinomialDistribution_methods.htmlMethods8prob.NormalDistribution.html.prob.NormalDistributionNprob.NormalDistribution_properties.htmlPropertiesHprob.NormalDistribution_methods.htmlMethodsJprob.PiecewiseLinearDistribution.html@prob.PiecewiseLinearDistribution`prob.PiecewiseLinearDistribution_properties.htmlPropertiesZprob.PiecewiseLinearDistribution_methods.htmlMethods:prob.PoissonDistribution.html0prob.PoissonDistributionPprob.PoissonDistribution_properties.htmlPropertiesJprob.PoissonDistribution_methods.htmlMethods<prob.RayleighDistribution.html2prob.RayleighDistributionRprob.RayleighDistribution_properties.htmlPropertiesLprob.RayleighDistribution_methods.htmlMethods8prob.RicianDistribution.html.prob.RicianDistributionNprob.RicianDistribution_properties.htmlPropertiesHprob.RicianDistribution_methods.htmlMethods8prob.StableDistribution.html.prob.StableDistributionNprob.StableDistribution_properties.htmlPropertiesHprob.StableDistribution_methods.htmlMethodsHprob.tLocationScaleDistribution.html>prob.tLocationScaleDistribution^prob.tLocationScaleDistribution_properties.htmlPropertiesXprob.tLocationScaleDistribution_methods.htmlMethods@prob.TriangularDistribution.html6prob.TriangularDistributionVprob.TriangularDistribution_properties.htmlPropertiesPprob.TriangularDistribution_methods.htmlMethods:prob.UniformDistribution.html0prob.UniformDistributionPprob.UniformDistribution_properties.htmlPropertiesJprob.UniformDistribution_methods.htmlMethods:prob.WeibullDistribution.html0prob.WeibullDistributionPprob.WeibullDistribution_properties.htmlPropertiesJprob.WeibullDistribution_methods.htmlMethods2Distribution_Fitting.html(Distribution Fitting6Distribution_Functions.html,Distribution Functions8Distribution_Statistics.html.Distribution Statistics4Distribution_Wrappers.html*Distribution WrappersPlotting.htmlPlotting2stats.chart.BoxChart.html(stats.chart.BoxChartHstats.chart.BoxChart_properties.htmlPropertiesBstats.chart.BoxChart_methods.htmlMethods:stats.chart.HeatmapChart.html0stats.chart.HeatmapChartPstats.chart.HeatmapChart_properties.htmlPropertiesJstats.chart.HeatmapChart_methods.htmlMethodsPstats.chart.ParallelCoordinatesPlot.htmlFstats.chart.ParallelCoordinatesPlotfstats.chart.ParallelCoordinatesPlot_properties.htmlProperties`stats.chart.ParallelCoordinatesPlot_methods.htmlMethodsLstats.chart.ScatterHistogramChart.htmlBstats.chart.ScatterHistogramChartbstats.chart.ScatterHistogramChart_properties.htmlProperties\stats.chart.ScatterHistogramChart_methods.htmlMethodsI_O.htmlI/OUtilities.htmlUtilities Š/èȪ˜†tT<"ìФ„nX@$ ø â Ì ¬ Š p V @ &  î Î ¶ š ~ d .  ô Æ ¬ – € j T 0  ü ä Ê   € j L 2  Ü Æ ® Š t \ > & ÞÆ¦Žd:$ ðÖÀ¬–~dJ6 ôÀ „rXB,êȲœˆr\D0æÐº’vR:"êÒ´ „dD&ḛ̀–t\=$îÕ¼¡~eN/� ! !fitrkernelfitrkernel�  fitrgpfitrgp� fitrgamfitrgam �% %fitrensemblefitrensemble� fitctreefitctree� fitcsvmfitcsvm� fitcnetfitcnet� fitcnbfitcnb�! !fitclinearfitclinear� fitcknnfitcknn�! !fitckernelfitckernel fitcgamfitcgam ~% %fitcensemblefitcensemble} fitcecocfitcecoc| fitcdiscrfitcdiscr{ fcnntrainfcnntrainz# #fcnnpredictfcnnpredicty! !designecocdesignecocx! !stepwiselmstepwiselmw# #stepwiseglmstepwiseglmv# #stepwisefitstepwisefitu robustfitrobustfitt ridgeridges! !regress_gpregress_gpr regressregressq! !plsregressplsregressp nlpredcinlpredcio nlparcinlparcin nlinfitnlinfit"m' 'mvregresslikemvregresslikel mvregressmvregress&k+ +monotone_smoothmonotone_smoothj mnrvalmnrvali mnrfitmnrfit.h3 3logistic_regressionlogistic_regressiong lassoglmlassoglmf lassolassoe invpredinvpredd glmvalglmvalc glmfitglmfitb fitrmfitrma fitnlmfitnlm` fitmnrfitmnr _% %fitlmematrixfitlmematrix^ fitlmefitlme] fitlmfitlm\ fitglmefitglme[ fitglmfitglmZ fitcoxfitcoxY coxphfitcoxphfitX x2fxx2fxW sigma_ptssigma_ptsV# #sampsizepwrsampsizepwr2U7 7parseWilkinsonFormulaparseWilkinsonFormulaT fullfactfullfactS ff2nff2nR ztest2ztest2Q ztestztestP vartestnvartestnO vartest2vartest2N vartestvartestM ttest2ttest2L ttestttestK swtestswtestJ signtestsigntestI signranksignrankH runstestrunstestG rjtestrjtest(F- -regression_ttestregression_ttest(E- -regression_ftestregression_ftestD ranksumranksumC# #multcomparemultcompareB mmdtestmmdtest$A) )meanEffectSizemeanEffectSize @% %mcnemar_testmcnemar_test? manova1manova1>! !lillietestlillietest= kstest2kstest2< kstestkstest";' 'kruskalwalliskruskalwallis: knntestknntest9 jbtestjbtest*8/ /hotelling_t2test2hotelling_t2test2(7- -hotelling_t2testhotelling_t2test6 friedmanfriedman5! !fishertestfishertest4 dwtestdwtest3# #detectdriftdetectdrift(2- -correlation_testcorrelation_test1 chi2testchi2test0 chi2gofchi2gof/ binotestbinotest. barttestbarttest"-' 'ansaribradleyansaribradley, anovananovan+ anova2anova2* anova1anova1) adtestadtest( tiedranktiedrank,'1 1standardizeMissingstandardizeMissing& rmmissingrmmissing%! !randsamplerandsample4$9 9normalise_distributionnormalise_distribution# multiwaymultiway" isoutlierisoutlier! ismissingismissing  grp2idxgrp2idx# #fillmissingfillmissing dummyvardummyvar! !datasampledatasample crosstabcrosstab combnkcombnk trimmeantrimmean tabulatetabulate % %partialcorripartialcorri# #partialcorrpartialcorr nanvarnanvar nansumnansum nanstdnanstd nanminnanmin nanmediannanmedian nanmeannanmean nanmaxnanmax nancovnancov# #mvksdensitymvksdensity* / /limitsOfAgreementlimitsOfAgreement  ksdensityksdensity  jackknifejackknife  harmmeanharmmean  grpstatsgrpstats geomeangeomean# #fleissKappafleissKappa ecdfecdf dcovdcov corrcorr! !cohenKappacohenKappa# #cl_multinomcl_multinom cdfcalccdfcalc †áȯ”kR7íÄŸ€e8 ä É – k D # ü Í ¬ ‰ h K 2  ö ß ¼ Ÿ | e J 1  Ý Â Ÿ ‚ m T 9 $ ì Í ´ £ Ž q T 9  òáʳ …fM:òÓÀ™v[>!ßÄ¥ŠoT=èÏ´›€gL/ìÓ¼£Œu\;&òÓ¾§ŽsZ?& ò×¾£ŠoV; ê϶›‚gP7‚ unidfitunidfit‚ tlsliketlslike‚ tlsfittlsfit‚  stbllikestbllike‚  stblfitstblfit‚  ricelikericelike‚  ricefitricefit‚  rayllikerayllike‚ raylfitraylfit‚ poisslikepoisslike‚ poissfitpoissfit‚ normlikenormlike‚ normfitnormfit‚ nbinlikenbinlike‚ nbinfitnbinfit‚ nakalikenakalike‚ nakafitnakafit� lognlikelognlike�~ lognfitlognfit�} logllikelogllike�| loglfitloglfit�{ logilikelogilike�z logifitlogifit�y invglikeinvglike�x invgfitinvgfit�w hnlikehnlike�v hnfithnfit�u! !gumbellikegumbellike�t gumbelfitgumbelfit�s gplikegplike�r gpfitgpfit�q# #gevfit_lmomgevfit_lmom�p gevlikegevlike�o gevfitgevfit�n geofitgeofit�m gamlikegamlike�l gamfitgamfit�k explikeexplike�j expfitexpfit�i evlikeevlike�h evfitevfit�g copulafitcopulafit�f burrlikeburrlike�e burrfitburrfit�d bisalikebisalike�c bisafitbisafit�b binolikebinolike�a binofitbinofit�` betalikebetalike�_ betafitbetafit�^# #slicesampleslicesample�] qrandnqrandn�\ pearsrndpearsrnd�[ mhsamplemhsample�Z johnsrndjohnsrnd�Y! !hmmviterbihmmviterbi�X hmmtrainhmmtrain�W# #hmmgeneratehmmgenerate�V# #hmmestimatehmmestimate�U hmmdecodehmmdecode�T perfcurve perfcurve�S crossval crossval �R% %confusionmat confusionmat$�Q) )confusionchart confusionchart�P tsne tsne�O! !sparsefilt sparsefilt �N% %sequentialfs sequentialfs"�M' 'rotatefactors rotatefactors�L rica rica�K relieff relieff�J! !procrustes procrustes�I princomp princomp�H ppca ppca�G pcares pcares�F pcacov pcacov�E pca pca�D nnmf nnmf�C mdscale mdscale�B factoran factoran�A cmdscale cmdscale�@ canoncorr canoncorr�? robustcov robustcov�> ocsvm ocsvm�= lof lof�< iforest iforest�;! !squareform squareform�:# #rangesearch rangesearch�9 pdist2 pdist2�8 pdist pdist�7 nomdist2 nomdist2�6 nomdist nomdist�5 mahal mahal�4 knnsearch knnsearch �3% %editDistance editDistance�2 createns createns&�1+ +spectralcluster spectralcluster(�0- -optimalleaforder optimalleaforder�/ linkage linkage�. kmedoids kmedoids�- kmeans kmeans �,% %inconsistent inconsistent�+ fitgmdist fitgmdist �*% %evalclusters evalclusters�) dbscan dbscan�( cophenet cophenet�'# #clusterdata clusterdata�& cluster cluster�% treetraintreetrain�$# #treepredicttreepredict �#% %templateTreetemplateTree�"# #templateSVMtemplateSVM,�!1 1templateNaiveBayestemplateNaiveBayes$� ) )templateLineartemplateLinear�# #templateKNNtemplateKNN$�) )templateKerneltemplateKernel(�- -templateEnsembletemplateEnsemble0�5 5templateDiscriminanttemplateDiscriminant� svmtrainsvmtrain�! !svmpredictsvmpredict2�7 7plotPartialDependenceplotPartialDependence*�/ /partialDependencepartialDependence� gamtraingamtrain�! !gampredictgampredict"�' 'gamboosttraingamboosttrain&�+ +gamboostpredictgamboostpredict"�' 'gamboostpairsgamboostpairs"�' 'gamboostintergamboostinter� fsrftestfsrftest� fscchi2fscchi2&�+ +fitSVMPosteriorfitSVMPosterior� fitrtreefitrtree�  fitrsvmfitrsvm�  fitrnetfitrnet� ! !fitrlinearfitrlinear ›GéÒ¹ ‡nU<# ñØ¿¦�t[B) û Þ Á ¤ ‡ n U < #  é Ì · ¢ � x a J 3  ö ã Ð ¹ ¢ ‹ t ] F /   ê Ó ¼ § ’ } h K .  ô ß Ê µ   ‡ n U < # ñؽ¢‹tU6øßÆ­”{bI0þåÌ·¢‹t]F/ÿæÍ´›‚iP9" ôÝÆ¯˜fM4é릑|aF+÷ÞŬ“zaH/ýä˲™€mZGƒ+ tpdftpdfƒ* tinvtinvƒ) tcdftcdfƒ( stdrrndstdrrndƒ' stdrpdfstdrpdfƒ& stdrinvstdrinvƒ% stdrcdfstdrcdfƒ$ stblrndstblrndƒ# stblpdfstblpdfƒ" stblinvstblinvƒ! stblcdfstblcdfƒ  ricerndricerndƒ ricepdfricepdfƒ riceinvriceinvƒ ricecdfricecdfƒ raylrndraylrndƒ raylpdfraylpdfƒ raylinvraylinvƒ raylcdfraylcdfƒ poissrndpoissrndƒ poisspdfpoisspdfƒ poissinvpoissinvƒ poisscdfpoisscdfƒ plrndplrndƒ plpdfplpdfƒ plinvplinvƒ plcdfplcdfƒ normrndnormrndƒ normpdfnormpdfƒ norminvnorminvƒ  normcdfnormcdfƒ  ncx2rndncx2rndƒ  ncx2pdfncx2pdfƒ  ncx2invncx2invƒ  ncx2cdfncx2cdfƒ nctrndnctrndƒ nctpdfnctpdfƒ nctinvnctinvƒ nctcdfnctcdfƒ ncfrndncfrndƒ ncfpdfncfpdfƒ ncfinvncfinvƒ ncfcdfncfcdfƒ nbinrndnbinrnd‚ nbinpdfnbinpdf‚~ nbininvnbininv‚} nbincdfnbincdf‚| nakarndnakarnd‚{ nakapdfnakapdf‚z nakainvnakainv‚y nakacdfnakacdf‚x mvtrndmvtrnd‚w mvtpdfmvtpdf‚v mvtcdfmvtcdf‚u mvnrndmvnrnd‚t mvnpdfmvnpdf‚s mvncdfmvncdf‚r mnrndmnrnd‚q mnpdfmnpdf‚p lognrndlognrnd‚o lognpdflognpdf‚n logninvlogninv‚m logncdflogncdf‚l loglrndloglrnd‚k loglpdfloglpdf‚j loglinvloglinv‚i loglcdfloglcdf‚h logirndlogirnd‚g logipdflogipdf‚f logiinvlogiinv‚e logicdflogicdf‚d! !laplacerndlaplacernd‚c! !laplacepdflaplacepdf‚b! !laplaceinvlaplaceinv‚a! !laplacecdflaplacecdf‚` jsupdfjsupdf‚_ jsucdfjsucdf‚^ iwishrndiwishrnd‚] iwishpdfiwishpdf‚\ invgrndinvgrnd‚[ invgpdfinvgpdf‚Z invginvinvginv‚Y invgcdfinvgcdf‚X hygerndhygernd‚W hygepdfhygepdf‚V hygeinvhygeinv‚U hygecdfhygecdf‚T hnrndhnrnd‚S hnpdfhnpdf‚R hninvhninv‚Q hncdfhncdf‚P gumbelrndgumbelrnd‚O gumbelpdfgumbelpdf‚N gumbelinvgumbelinv‚M gumbelcdfgumbelcdf‚L gprndgprnd‚K gppdfgppdf‚J gpinvgpinv‚I gpcdfgpcdf‚H gevrndgevrnd‚G gevpdfgevpdf‚F gevinvgevinv‚E gevcdfgevcdf‚D georndgeornd‚C geopdfgeopdf‚B geoinvgeoinv‚A geocdfgeocdf‚@ gamrndgamrnd‚? gampdfgampdf‚> gaminvgaminv‚= gamcdfgamcdf‚< frndfrnd‚; fpdffpdf‚: finvfinv‚9 fcdffcdf‚8 exprndexprnd‚7 exppdfexppdf‚6 expinvexpinv‚5 expcdfexpcdf‚4 evrndevrnd‚3 evpdfevpdf‚2 evinvevinv‚1 evcdfevcdf‚0 copularndcopularnd‚/ copulapdfcopulapdf‚. copulacdfcopulacdf‚- chi2rndchi2rnd‚, chi2pdfchi2pdf‚+ chi2invchi2inv‚* chi2cdfchi2cdf‚) cauchyrndcauchyrnd‚( cauchypdfcauchypdf‚' cauchyinvcauchyinv‚& cauchycdfcauchycdf‚% bvtcdfbvtcdf‚$ bvncdfbvncdf‚# burrrndburrrnd‚" burrpdfburrpdf‚! burrinvburrinv‚  burrcdfburrcdf‚ bisarndbisarnd‚ bisapdfbisapdf‚ bisainvbisainv‚ bisacdfbisacdf‚ binorndbinornd‚ binopdfbinopdf‚ binoinvbinoinv‚ binocdfbinocdf‚ betarndbetarnd‚ betapdfbetapdf‚ betainvbetainv‚ betacdfbetacdf‚ wbllikewbllike‚ wblfitwblfit‚ unifitunifit …IíÖ¿¨‘zcL5êѸŸ†mXC. ë Ô ½ ¤ ‹ r W < !  ë Ê « ” { f M 4   å Î ³ ˜ } b G ,  ø ß Ä © ’ w \ A & ô ß Ä © �  f S 8 ' ÿèÇ´Ÿˆ_D+óؽ�sR7 ð˰‰fO4üѲ“|cD#íØÁª‘xcJ%Ó–_.óÄ�`7ñÐ¥~I2„07 7anova.plotComparisonsanova.plotComparisons$„/) )anova.boxchartanova.boxchart(„.- -anova.groupmeansanova.groupmeans„-# #anova.statsanova.stats„,# #anova.anovaanova.anova"„+' 'anova.Metricsanova.Metrics&„*+ +anova.Residualsanova.Residuals,„)1 1anova.Coefficientsanova.Coefficients2„(7 7anova.NumObservationsanova.NumObservations,„'1 1anova.ResponseNameanova.ResponseName8„&= =anova.CategoricalFactorsanova.CategoricalFactors.„%3 3anova.RandomFactorsanova.RandomFactors4„$9 9anova.SumOfSquaresTypeanova.SumOfSquaresType:„#? ?anova.ExpandedFactorNamesanova.ExpandedFactorNames*„"/ /anova.FactorNamesanova.FactorNames"„!' 'anova.Formulaanova.Formula"„ ' 'anova.Factorsanova.Factors„ anova.Yanova.Y„ anovaanova„ statsetstatset„ statgetstatget„ probitprobit„ makimamakima„ logitlogit„ cholcovcholcov„ loadmodelloadmodel„# #libsvmwritelibsvmwrite„! !libsvmreadlibsvmread„ wblplotwblplot„ violinviolin„! !swarmchartswarmchart„! !silhouettesilhouette(„- -scatterhistogramscatterhistogram„# #scatterhistscatterhist„ qqplotqqplot„  probplotprobplot„  ppplotppplot „ % %parallelplotparallelplot$„ ) )parallelcoordsparallelcoords„  normplotnormplot"„' 'manovaclustermanovacluster„ histfithistfit„ hist3hist3„ heatmapheatmap„ gscattergscatter„# #gplotmatrixgplotmatrix„ glyphplotglyphplot*„/ /gardnerAltmanPlotgardnerAltmanPlot„ einsteineinsteinƒ ecdfhistecdfhistƒ~! !dendrogramdendrogramƒ} cdfplotcdfplotƒ| boxplotboxplotƒ{ boxchartboxchart&ƒz+ +blandAltmanPlotblandAltmanPlotƒy biplotbiplotƒx bar3hbar3hƒw bar3bar3ƒv# #andrewsplotandrewsplotƒu randomrandomƒt pdfpdfƒs mlecovmlecovƒr mlemleƒq makedistmakedistƒp icdficdfƒo fitdistfitdistƒn cdfcdfƒm wblstatwblstatƒl unifstatunifstatƒk unidstatunidstatƒj tstattstatƒi tristattristatƒh tlsstattlsstatƒg stdrstatstdrstatƒf ricestatricestatƒe raylstatraylstatƒd poisstatpoisstatƒc plstatplstatƒb normstatnormstatƒa ncx2statncx2statƒ` nctstatnctstatƒ_ ncfstatncfstatƒ^ nbinstatnbinstatƒ] nakastatnakastatƒ\ lognstatlognstatƒ[ loglstatloglstatƒZ logistatlogistatƒY invgstatinvgstatƒX hygestathygestatƒW hnstathnstatƒV! !gumbelstatgumbelstatƒU gpstatgpstatƒT gevstatgevstatƒS geostatgeostatƒR gamstatgamstatƒQ fstatfstatƒP expstatexpstatƒO evstatevstatƒN! !copulastatcopulastatƒM# #copulaparamcopulaparamƒL chi2statchi2statƒK burrstatburrstatƒJ bisastatbisastatƒI binostatbinostatƒH betastatbetastatƒG wishrndwishrndƒF wishpdfwishpdfƒE wienrndwienrndƒD wblrndwblrndƒC wblpdfwblpdfƒB wblinvwblinvƒA wblcdfwblcdfƒ@ vmrndvmrndƒ? vmpdfvmpdfƒ> vminvvminvƒ= vmcdfvmcdfƒ< unifrndunifrndƒ; unifpdfunifpdfƒ: unifinvunifinvƒ9 unifcdfunifcdfƒ8 unidrndunidrndƒ7 unidpdfunidpdfƒ6 unidinvunidinvƒ5 unidcdfunidcdfƒ4 trirndtrirndƒ3 tripdftripdfƒ2 triinvtriinvƒ1 tricdftricdfƒ0 tlsrndtlsrndƒ/ tlspdftlspdfƒ. tlsinvtlsinvƒ- tlscdftlscdfƒ, trndtrnd 1�ÇšW—< é Œ  ¨ K ø § H Û x  ® Iö©Píˆ)¸‰.Ý”Eä­bÞ¥d-ö¿‚Cô©`Ê�F„aK KCompactLinearModel.VariableInfoCompactLinearModel.VariableInfoF„`K KCompactLinearModel.ResponseNameCompactLinearModel.ResponseNameJ„_O OCompactLinearModel.PredictorNamesCompactLinearModel.PredictorNamesF„^K KCompactLinearModel.NumVariablesCompactLinearModel.NumVariablesH„]M MCompactLinearModel.NumPredictorsCompactLinearModel.NumPredictorsL„\Q QCompactLinearModel.NumObservationsCompactLinearModel.NumObservations<„[A ACompactLinearModel.FormulaCompactLinearModel.Formula:„Z? ?CompactLinearModel.RobustCompactLinearModel.Robust4„Y9 9CompactLinearModel.SSTCompactLinearModel.SST4„X9 9CompactLinearModel.SSRCompactLinearModel.SSR4„W9 9CompactLinearModel.SSECompactLinearModel.SSE>„VC CCompactLinearModel.RsquaredCompactLinearModel.Rsquared6„U; ;CompactLinearModel.RMSECompactLinearModel.RMSE4„T9 9CompactLinearModel.MSECompactLinearModel.MSEJ„SO OCompactLinearModel.ModelCriterionCompactLinearModel.ModelCriterionH„RM MCompactLinearModel.LogLikelihoodCompactLinearModel.LogLikelihood4„Q9 9CompactLinearModel.DFECompactLinearModel.DFE^„Pc cCompactLinearModel.NumEstimatedCoefficientsCompactLinearModel.NumEstimatedCoefficientsL„OQ QCompactLinearModel.NumCoefficientsCompactLinearModel.NumCoefficientsF„NK KCompactLinearModel.CoefficientsCompactLinearModel.CoefficientsN„MS SCompactLinearModel.CoefficientNamesCompactLinearModel.CoefficientNamesX„L] ]CompactLinearModel.CoefficientCovarianceCompactLinearModel.CoefficientCovariance,„K1 1CompactLinearModelCompactLinearModeln„Js sstats.drift.DriftDiagnostics.plotPermutationResultsstats.drift.DriftDiagnostics.plotPermutationResults\„Ia astats.drift.DriftDiagnostics.plotHistogramstats.drift.DriftDiagnostics.plotHistogramb„Hg gstats.drift.DriftDiagnostics.plotEmpiricalCDFstats.drift.DriftDiagnostics.plotEmpiricalCDF`„Ge estats.drift.DriftDiagnostics.plotDriftStatusstats.drift.DriftDiagnostics.plotDriftStatusV„F[ [stats.drift.DriftDiagnostics.histcountsstats.drift.DriftDiagnostics.histcountsJ„EO Ostats.drift.DriftDiagnostics.ecdfstats.drift.DriftDiagnostics.ecdfP„DU Ustats.drift.DriftDiagnostics.summarystats.drift.DriftDiagnostics.summaryb„Cg gstats.drift.DriftDiagnostics.WarningThresholdstats.drift.DriftDiagnostics.WarningThreshold^„Bc cstats.drift.DriftDiagnostics.DriftThresholdstats.drift.DriftDiagnostics.DriftThresholdf„Ak kstats.drift.DriftDiagnostics.PermutationResultsstats.drift.DriftDiagnostics.PermutationResults`„@e estats.drift.DriftDiagnostics.NumPermutationsstats.drift.DriftDiagnostics.NumPermutationsj„?o ostats.drift.DriftDiagnostics.CategoricalVariablesstats.drift.DriftDiagnostics.CategoricalVariables\„>a astats.drift.DriftDiagnostics.VariableNamesstats.drift.DriftDiagnostics.VariableNamesN„=S Sstats.drift.DriftDiagnostics.Targetstats.drift.DriftDiagnostics.TargetP„<U Ustats.drift.DriftDiagnostics.PValuesstats.drift.DriftDiagnostics.PValuesZ„;_ _stats.drift.DriftDiagnostics.NumVariablesstats.drift.DriftDiagnostics.NumVariablesp„:u ustats.drift.DriftDiagnostics.MultipleTestDriftStatusstats.drift.DriftDiagnostics.MultipleTestDriftStatusn„9s sstats.drift.DriftDiagnostics.MultipleTestCorrectionstats.drift.DriftDiagnostics.MultipleTestCorrectionZ„8_ _stats.drift.DriftDiagnostics.MetricValuesstats.drift.DriftDiagnostics.MetricValuesP„7U Ustats.drift.DriftDiagnostics.Metricsstats.drift.DriftDiagnostics.MetricsX„6] ]stats.drift.DriftDiagnostics.DriftStatusstats.drift.DriftDiagnostics.DriftStatush„5m mstats.drift.DriftDiagnostics.ConfidenceIntervalsstats.drift.DriftDiagnostics.ConfidenceIntervalsR„4W Wstats.drift.DriftDiagnostics.Baselinestats.drift.DriftDiagnostics.Baseline@„3E Estats.drift.DriftDiagnosticsstats.drift.DriftDiagnostics*„2/ /anova.multcompareanova.multcompare6„1; ;anova.varianceComponentanova.varianceComponent 8†µx7ø»€9 ê ¯ ” _ ( ñ È � Z /  É ‚ K  Í r # î Á ˜ [ (÷•Tù®Y¡@åx¸Y¿dë�3Èy$Ó†J…O OGeneralizedLinearMixedModel.anova&GeneralizedLinearMixedModel.anovaN…S SGeneralizedLinearMixedModel.predict&GeneralizedLinearMixedModel.predictR…W WGeneralizedLinearMixedModel.residuals&GeneralizedLinearMixedModel.residualsL…Q QGeneralizedLinearMixedModel.fitted&GeneralizedLinearMixedModel.fittedh…m mGeneralizedLinearMixedModel.covarianceParameters&GeneralizedLinearMixedModel.covarianceParametersZ…_ _GeneralizedLinearMixedModel.randomEffects&GeneralizedLinearMixedModel.randomEffectsX…] ]GeneralizedLinearMixedModel.fixedEffects&GeneralizedLinearMixedModel.fixedEffectsv…{ {GeneralizedLinearMixedModel.GeneralizedLinearMixedModel&GeneralizedLinearMixedModel.GeneralizedLinearMixedModelX…] ]GeneralizedLinearMixedModel.ResponseName%GeneralizedLinearMixedModel.ResponseNameN…S SGeneralizedLinearMixedModel.Formula%GeneralizedLinearMixedModel.FormulaF…K KGeneralizedLinearMixedModel.DFE%GeneralizedLinearMixedModel.DFE\…a aGeneralizedLinearMixedModel.ModelCriterion%GeneralizedLinearMixedModel.ModelCriterionZ… _ _GeneralizedLinearMixedModel.LogLikelihood%GeneralizedLinearMixedModel.LogLikelihood`… e eGeneralizedLinearMixedModel.CoefficientNames%GeneralizedLinearMixedModel.CoefficientNamesj… o oGeneralizedLinearMixedModel.CoefficientCovariance%GeneralizedLinearMixedModel.CoefficientCovarianceX… ] ]GeneralizedLinearMixedModel.Coefficients%GeneralizedLinearMixedModel.Coefficients^… c cGeneralizedLinearMixedModel.NumCoefficients%GeneralizedLinearMixedModel.NumCoefficients^…c cGeneralizedLinearMixedModel.NumObservations%GeneralizedLinearMixedModel.NumObservationsT…Y YGeneralizedLinearMixedModel.Dispersion%GeneralizedLinearMixedModel.DispersionR…W WGeneralizedLinearMixedModel.FitMethod%GeneralizedLinearMixedModel.FitMethodH…M MGeneralizedLinearMixedModel.Link%GeneralizedLinearMixedModel.LinkX…] ]GeneralizedLinearMixedModel.Distribution%GeneralizedLinearMixedModel.Distribution>…C CGeneralizedLinearMixedModel$GeneralizedLinearMixedModel*…/ /CoxModel.survival#CoxModel.survival2…7 7CoxModel.plotSurvival#CoxModel.plotSurvival.…3 3CoxModel.linhyptest#CoxModel.linhyptest0„5 5CoxModel.hazardratio#CoxModel.hazardratio:„~? ?CoxModel.discardResiduals#CoxModel.discardResiduals&„}+ +CoxModel.coefci#CoxModel.coefci*„|/ /CoxModel.CoxModel#CoxModel.CoxModel2„{7 7CoxModel.VariableInfo"CoxModel.VariableInfoL„zQ QCoxModel.LikelihoodRatioTestPValue"CoxModel.LikelihoodRatioTestPValueX„y] ]CoxModel.ProportionalHazardsPValueGlobal"CoxModel.ProportionalHazardsPValueGlobalL„xQ QCoxModel.ProportionalHazardsPValue"CoxModel.ProportionalHazardsPValue,„w1 1CoxModel.Residuals"CoxModel.Residuals4„v9 9CoxModel.StandardError"CoxModel.StandardErrorD„uI ICoxModel.CoefficientCovariance"CoxModel.CoefficientCovariance6„t; ;CoxModel.Stratification"CoxModel.Stratification*„s/ /CoxModel.Baseline"CoxModel.Baseline(„r- -CoxModel.Formula"CoxModel.Formula2„q7 7CoxModel.ResponseName"CoxModel.ResponseName6„p; ;CoxModel.PredictorNames"CoxModel.PredictorNames&„o+ +CoxModel.Hazard"CoxModel.Hazard4„n9 9CoxModel.LogLikelihood"CoxModel.LogLikelihood4„m9 9CoxModel.NumPredictors"CoxModel.NumPredictors2„l7 7CoxModel.Coefficients"CoxModel.Coefficients„k CoxModel!CoxModel8„j= =CompactLinearModel.anova CompactLinearModel.anovaL„iQ QCompactLinearModel.plotInteraction CompactLinearModel.plotInteractionD„hI ICompactLinearModel.plotEffects CompactLinearModel.plotEffects8„g= =CompactLinearModel.feval CompactLinearModel.feval:„f? ?CompactLinearModel.random CompactLinearModel.random<„eA ACompactLinearModel.predict CompactLinearModel.predict>„dC CCompactLinearModel.coefTest CompactLinearModel.coefTest:„c? ?CompactLinearModel.coefCI CompactLinearModel.coefCIH„bM MCompactLinearModel.VariableNamesCompactLinearModel.VariableNames 2Š­^Ì{" ¿ h ÿ ¬ U Í € ! Ð � J ÿ ° ] ¿€A½b¼iÇ|%Ì…BÝ–SÅt/Ü…6ÕŠH…KM MGeneralizedLinearModel.plotAdded)GeneralizedLinearModel.plotAdded^…Jc cGeneralizedLinearModel.plotAdjustedResponse)GeneralizedLinearModel.plotAdjustedResponseL…IQ QGeneralizedLinearModel.plotEffects)GeneralizedLinearModel.plotEffectsT…HY YGeneralizedLinearModel.plotDiagnostics)GeneralizedLinearModel.plotDiagnosticsP…GU UGeneralizedLinearModel.plotResiduals)GeneralizedLinearModel.plotResidualsB…FG GGeneralizedLinearModel.random)GeneralizedLinearModel.randomN…ES SGeneralizedLinearModel.devianceTest)GeneralizedLinearModel.devianceTestF…DK KGeneralizedLinearModel.coefTest)GeneralizedLinearModel.coefTestB…CG GGeneralizedLinearModel.coefCI)GeneralizedLinearModel.coefCI@…BE EGeneralizedLinearModel.feval)GeneralizedLinearModel.fevalD…AI IGeneralizedLinearModel.predict)GeneralizedLinearModel.predictb…@g gGeneralizedLinearModel.GeneralizedLinearModel)GeneralizedLinearModel.GeneralizedLinearModel@…?E EGeneralizedLinearModel.Steps(GeneralizedLinearModel.StepsD…>I IGeneralizedLinearModel.Formula(GeneralizedLinearModel.FormulaV…=[ [GeneralizedLinearModel.ObservationNames(GeneralizedLinearModel.ObservationNamesT…<Y YGeneralizedLinearModel.ObservationInfo(GeneralizedLinearModel.ObservationInfoH…;M MGeneralizedLinearModel.Variables(GeneralizedLinearModel.VariablesN…:S SGeneralizedLinearModel.VariableInfo(GeneralizedLinearModel.VariableInfoN…9S SGeneralizedLinearModel.NumVariables(GeneralizedLinearModel.NumVariablesP…8U UGeneralizedLinearModel.VariableNames(GeneralizedLinearModel.VariableNamesR…7W WGeneralizedLinearModel.PredictorNames(GeneralizedLinearModel.PredictorNamesN…6S SGeneralizedLinearModel.ResponseName(GeneralizedLinearModel.ResponseNameX…5] ]GeneralizedLinearModel.LikelihoodPenalty(GeneralizedLinearModel.LikelihoodPenaltyB…4G GGeneralizedLinearModel.Offset(GeneralizedLinearModel.Offset<…3A AGeneralizedLinearModel.SST(GeneralizedLinearModel.SST<…2A AGeneralizedLinearModel.SSR(GeneralizedLinearModel.SSR<…1A AGeneralizedLinearModel.SSE(GeneralizedLinearModel.SSEF…0K KGeneralizedLinearModel.Rsquared(GeneralizedLinearModel.RsquaredR…/W WGeneralizedLinearModel.ModelCriterion(GeneralizedLinearModel.ModelCriterionP….U UGeneralizedLinearModel.LogLikelihood(GeneralizedLinearModel.LogLikelihoodL…-Q QGeneralizedLinearModel.Diagnostics(GeneralizedLinearModel.DiagnosticsH…,M MGeneralizedLinearModel.Residuals(GeneralizedLinearModel.ResidualsB…+G GGeneralizedLinearModel.Fitted(GeneralizedLinearModel.Fitted>…*C CGeneralizedLinearModel.Link(GeneralizedLinearModel.LinkN…)S SGeneralizedLinearModel.Distribution(GeneralizedLinearModel.Distribution\…(a aGeneralizedLinearModel.DispersionEstimated(GeneralizedLinearModel.DispersionEstimatedJ…'O OGeneralizedLinearModel.Dispersion(GeneralizedLinearModel.Dispersion<…&A AGeneralizedLinearModel.DFE(GeneralizedLinearModel.DFEF…%K KGeneralizedLinearModel.Deviance(GeneralizedLinearModel.DevianceT…$Y YGeneralizedLinearModel.NumObservations(GeneralizedLinearModel.NumObservationsP…#U UGeneralizedLinearModel.NumPredictors(GeneralizedLinearModel.NumPredictorsf…"k kGeneralizedLinearModel.NumEstimatedCoefficients(GeneralizedLinearModel.NumEstimatedCoefficientsT…!Y YGeneralizedLinearModel.NumCoefficients(GeneralizedLinearModel.NumCoefficients`… e eGeneralizedLinearModel.CoefficientCovariance(GeneralizedLinearModel.CoefficientCovarianceV…[ [GeneralizedLinearModel.CoefficientNames(GeneralizedLinearModel.CoefficientNamesN…S SGeneralizedLinearModel.Coefficients(GeneralizedLinearModel.Coefficients4…9 9GeneralizedLinearModel'GeneralizedLinearModelX…] ]GeneralizedLinearMixedModel.designMatrix&GeneralizedLinearMixedModel.designMatrixL…Q QGeneralizedLinearMixedModel.coefCI&GeneralizedLinearMixedModel.coefCIP…U UGeneralizedLinearMixedModel.coefTest&GeneralizedLinearMixedModel.coefTest B˜Ûœ[ß®y: õ Æ � N  ê ­ l = ß   U ­ h  Ä } 4÷Ä‘^+ø½x+æŸJÒ—`#ꥄ7ô¹x%üÔWϦqFêÁ˜&† + +LinearModel.SST1LinearModel.SST&† + +LinearModel.SSR1LinearModel.SSR&† + +LinearModel.SSE1LinearModel.SSE0† 5 5LinearModel.Rsquared1LinearModel.Rsquared(† - -LinearModel.RMSE1LinearModel.RMSE2†7 7LinearModel.Residuals1LinearModel.Residuals&†+ +LinearModel.MSE1LinearModel.MSEF†K KLinearModel.ModelFitVsNullModel1LinearModel.ModelFitVsNullModel<†A ALinearModel.ModelCriterion1LinearModel.ModelCriterion:†? ?LinearModel.LogLikelihood1LinearModel.LogLikelihood,†1 1LinearModel.Fitted1LinearModel.Fitted6†; ;LinearModel.Diagnostics1LinearModel.Diagnostics&†+ +LinearModel.DFE1LinearModel.DFEP†U ULinearModel.NumEstimatedCoefficients1LinearModel.NumEstimatedCoefficients>…C CLinearModel.NumCoefficients1LinearModel.NumCoefficients8…~= =LinearModel.Coefficients1LinearModel.Coefficients@…}E ELinearModel.CoefficientNames1LinearModel.CoefficientNamesJ…|O OLinearModel.CoefficientCovariance1LinearModel.CoefficientCovariance…{# #LinearModel0LinearModelB…zG GLinearMixedModel.designMatrix/LinearMixedModel.designMatrix6…y; ;LinearMixedModel.coefCI/LinearMixedModel.coefCI:…x? ?LinearMixedModel.coefTest/LinearMixedModel.coefTest4…w9 9LinearMixedModel.anova/LinearMixedModel.anova8…v= =LinearMixedModel.predict/LinearMixedModel.predict<…uA ALinearMixedModel.residuals/LinearMixedModel.residuals6…t; ;LinearMixedModel.fitted/LinearMixedModel.fittedR…sW WLinearMixedModel.covarianceParameters/LinearMixedModel.covarianceParametersD…rI ILinearMixedModel.randomEffects/LinearMixedModel.randomEffectsB…qG GLinearMixedModel.fixedEffects/LinearMixedModel.fixedEffectsJ…pO OLinearMixedModel.LinearMixedModel/LinearMixedModel.LinearMixedModelB…oG GLinearMixedModel.ResponseName.LinearMixedModel.ResponseName8…n= =LinearMixedModel.Formula.LinearMixedModel.Formula0…m5 5LinearMixedModel.DFE.LinearMixedModel.DFE0…l5 5LinearMixedModel.MSE.LinearMixedModel.MSE0…k5 5LinearMixedModel.SST.LinearMixedModel.SST0…j5 5LinearMixedModel.SSR.LinearMixedModel.SSR0…i5 5LinearMixedModel.SSE.LinearMixedModel.SSE:…h? ?LinearMixedModel.Rsquared.LinearMixedModel.RsquaredF…gK KLinearMixedModel.ModelCriterion.LinearMixedModel.ModelCriterionD…fI ILinearMixedModel.LogLikelihood.LinearMixedModel.LogLikelihoodJ…eO OLinearMixedModel.CoefficientNames.LinearMixedModel.CoefficientNamesT…dY YLinearMixedModel.CoefficientCovariance.LinearMixedModel.CoefficientCovarianceB…cG GLinearMixedModel.Coefficients.LinearMixedModel.CoefficientsZ…b_ _LinearMixedModel.NumEstimatedCoefficients.LinearMixedModel.NumEstimatedCoefficientsH…aM MLinearMixedModel.NumCoefficients.LinearMixedModel.NumCoefficientsH…`M MLinearMixedModel.NumObservations.LinearMixedModel.NumObservations<…_A ALinearMixedModel.FitMethod.LinearMixedModel.FitMethod(…^- -LinearMixedModel-LinearMixedModel0…]5 5LinearFormula.string,LinearFormula.string,…\1 1LinearFormula.char,LinearFormula.char>…[C CLinearFormula.LinearFormula,LinearFormula.LinearFormula:…Z? ?LinearFormula.NPredictors+LinearFormula.NPredictors.…Y3 3LinearFormula.NVars+LinearFormula.NVars0…X5 5LinearFormula.NTerms+LinearFormula.NTerms>…WC CLinearFormula.FunctionCalls+LinearFormula.FunctionCalls4…V9 9LinearFormula.ModelFun+LinearFormula.ModelFun,…U1 1LinearFormula.Link+LinearFormula.LinkB…TG GLinearFormula.LinearPredictor+LinearFormula.LinearPredictor<…SA ALinearFormula.HasIntercept+LinearFormula.HasIntercept2…R7 7LinearFormula.InModel+LinearFormula.InModel.…Q3 3LinearFormula.Terms+LinearFormula.Terms6…P; ;LinearFormula.TermNames+LinearFormula.TermNames@…OE ELinearFormula.PredictorNames+LinearFormula.PredictorNames>…NC CLinearFormula.VariableNames+LinearFormula.VariableNames<…MA ALinearFormula.ResponseName+LinearFormula.ResponseName"…L' 'LinearFormula*LinearFormula <ŒÑ¤s2õºy6 ÷ ¼ � D  Ö ¥ v I  ç ¸ … L  Î • J  ê © x K ëœ;ä�DÝžUц+è—Dû´oÉz%Î{,ÝŒN†IS SMultinomialRegression.VariableNames4MultinomialRegression.VariableNamesL†HQ QMultinomialRegression.VariableInfo4MultinomialRegression.VariableInfoL†GQ QMultinomialRegression.ResponseName4MultinomialRegression.ResponseNameP†FU UMultinomialRegression.PredictorNames4MultinomialRegression.PredictorNamesT†EY YMultinomialRegression.ObservationNames4MultinomialRegression.ObservationNamesR†DW WMultinomialRegression.ObservationInfo4MultinomialRegression.ObservationInfoL†CQ QMultinomialRegression.NumVariables4MultinomialRegression.NumVariablesN†BS SMultinomialRegression.NumPredictors4MultinomialRegression.NumPredictorsR†AW WMultinomialRegression.NumObservations4MultinomialRegression.NumObservationsB†@G GMultinomialRegression.Formula4MultinomialRegression.FormulaD†?I IMultinomialRegression.Rsquared4MultinomialRegression.RsquaredF†>K KMultinomialRegression.Residuals4MultinomialRegression.ResidualsP†=U UMultinomialRegression.ModelCriterion4MultinomialRegression.ModelCriterionN†<S SMultinomialRegression.LogLikelihood4MultinomialRegression.LogLikelihood@†;E EMultinomialRegression.Fitted4MultinomialRegression.FittedX†:] ]MultinomialRegression.EstimateDispersion4MultinomialRegression.EstimateDispersionH†9M MMultinomialRegression.Dispersion4MultinomialRegression.Dispersion:†8? ?MultinomialRegression.DFE4MultinomialRegression.DFED†7I IMultinomialRegression.Deviance4MultinomialRegression.DevianceF†6K KMultinomialRegression.ModelType4MultinomialRegression.ModelType<†5A AMultinomialRegression.Link4MultinomialRegression.Linkd†4i iMultinomialRegression.IncludeClassInteractions4MultinomialRegression.IncludeClassInteractionsH†3M MMultinomialRegression.ClassNames4MultinomialRegression.ClassNamesR†2W WMultinomialRegression.NumCoefficients4MultinomialRegression.NumCoefficientsT†1Y YMultinomialRegression.CoefficientNames4MultinomialRegression.CoefficientNames^†0c cMultinomialRegression.CoefficientCovariance4MultinomialRegression.CoefficientCovarianceL†/Q QMultinomialRegression.Coefficients4MultinomialRegression.Coefficients2†.7 7MultinomialRegression3MultinomialRegression(†-- -LinearModel.step2LinearModel.step*†,/ /LinearModel.anova2LinearModel.anova.†+3 3LinearModel.compact2LinearModel.compact>†*C CLinearModel.plotInteraction2LinearModel.plotInteraction(†)- -LinearModel.plot2LinearModel.plot2†(7 7LinearModel.plotAdded2LinearModel.plotAddedH†'M MLinearModel.plotAdjustedResponse2LinearModel.plotAdjustedResponse6†&; ;LinearModel.plotEffects2LinearModel.plotEffects>†%C CLinearModel.plotDiagnostics2LinearModel.plotDiagnostics:†$? ?LinearModel.plotResiduals2LinearModel.plotResiduals6†#; ;LinearModel.removeTerms2LinearModel.removeTerms0†"5 5LinearModel.addTerms2LinearModel.addTerms,†!1 1LinearModel.dwtest2LinearModel.dwtest0† 5 5LinearModel.coefTest2LinearModel.coefTest,†1 1LinearModel.coefCI2LinearModel.coefCI*†/ /LinearModel.feval2LinearModel.feval,†1 1LinearModel.random2LinearModel.random.†3 3LinearModel.predict2LinearModel.predict6†; ;LinearModel.LinearModel2LinearModel.LinearModel2†7 7LinearModel.Variables1LinearModel.Variables:†? ?LinearModel.VariableNames1LinearModel.VariableNames8†= =LinearModel.VariableInfo1LinearModel.VariableInfo8†= =LinearModel.ResponseName1LinearModel.ResponseName<†A ALinearModel.PredictorNames1LinearModel.PredictorNames@†E ELinearModel.ObservationNames1LinearModel.ObservationNames>†C CLinearModel.ObservationInfo1LinearModel.ObservationInfo8†= =LinearModel.NumVariables1LinearModel.NumVariables:†? ?LinearModel.NumPredictors1LinearModel.NumPredictors>†C CLinearModel.NumObservations1LinearModel.NumObservations.†3 3LinearModel.Formula1LinearModel.Formula*†/ /LinearModel.Steps1LinearModel.Steps,†1 1LinearModel.Robust1LinearModel.Robust ;—·VÐ�J ´ c  ó ² i  Ï v 3 ì ½ Ž ] . ÿ Ð � H ÔŸj3ò­j%QÕŽSÍ~#Òƒ6ÝŽC¥bØ—>‡C CRepeatedMeasuresModel.anova;RepeatedMeasuresModel.anovaB‡G GRepeatedMeasuresModel.mauchly;RepeatedMeasuresModel.mauchlyB‡G GRepeatedMeasuresModel.epsilon;RepeatedMeasuresModel.epsilon@‡E ERepeatedMeasuresModel.ranova;RepeatedMeasuresModel.ranova^‡c cRepeatedMeasuresModel.RepeatedMeasuresModel;RepeatedMeasuresModel.RepeatedMeasuresModel:†? ?RepeatedMeasuresModel.DFE:RepeatedMeasuresModel.DFEH†~M MRepeatedMeasuresModel.Covariance:RepeatedMeasuresModel.CovarianceL†}Q QRepeatedMeasuresModel.Coefficients:RepeatedMeasuresModel.CoefficientsV†|[ [RepeatedMeasuresModel.WithinFactorNames:RepeatedMeasuresModel.WithinFactorNamesJ†{O ORepeatedMeasuresModel.WithinModel:RepeatedMeasuresModel.WithinModelL†zQ QRepeatedMeasuresModel.WithinDesign:RepeatedMeasuresModel.WithinDesignN†yS SRepeatedMeasuresModel.ResponseNames:RepeatedMeasuresModel.ResponseNamesX†x] ]RepeatedMeasuresModel.BetweenFactorNames:RepeatedMeasuresModel.BetweenFactorNamesL†wQ QRepeatedMeasuresModel.BetweenModel:RepeatedMeasuresModel.BetweenModelN†vS SRepeatedMeasuresModel.BetweenDesign:RepeatedMeasuresModel.BetweenDesign2†u7 7RepeatedMeasuresModel9RepeatedMeasuresModel8†t= =NonLinearModel.plotSlice8NonLinearModel.plotSliceD†sI INonLinearModel.plotDiagnostics8NonLinearModel.plotDiagnostics@†rE ENonLinearModel.plotResiduals8NonLinearModel.plotResiduals6†q; ;NonLinearModel.coefTest8NonLinearModel.coefTest2†p7 7NonLinearModel.coefCI8NonLinearModel.coefCI2†o7 7NonLinearModel.random8NonLinearModel.random0†n5 5NonLinearModel.feval8NonLinearModel.feval4†m9 9NonLinearModel.predict8NonLinearModel.predictB†lG GNonLinearModel.NonLinearModel8NonLinearModel.NonLinearModel@†kE ENonLinearModel.VariableNames7NonLinearModel.VariableNamesB†jG GNonLinearModel.PredictorNames7NonLinearModel.PredictorNames>†iC CNonLinearModel.ResponseName7NonLinearModel.ResponseName4†h9 9NonLinearModel.Formula7NonLinearModel.Formula2†g7 7NonLinearModel.Robust7NonLinearModel.Robust2†f7 7NonLinearModel.Fitted7NonLinearModel.Fitted8†e= =NonLinearModel.Residuals7NonLinearModel.Residuals6†d; ;NonLinearModel.Rsquared7NonLinearModel.RsquaredB†cG GNonLinearModel.ModelCriterion7NonLinearModel.ModelCriterion@†bE ENonLinearModel.LogLikelihood7NonLinearModel.LogLikelihood,†a1 1NonLinearModel.SSR7NonLinearModel.SSR,†`1 1NonLinearModel.SST7NonLinearModel.SST,†_1 1NonLinearModel.SSE7NonLinearModel.SSE.†^3 3NonLinearModel.RMSE7NonLinearModel.RMSE,†]1 1NonLinearModel.MSE7NonLinearModel.MSE,†\1 1NonLinearModel.DFE7NonLinearModel.DFED†[I INonLinearModel.NumObservations7NonLinearModel.NumObservations@†ZE ENonLinearModel.NumPredictors7NonLinearModel.NumPredictorsV†Y[ [NonLinearModel.NumEstimatedCoefficients7NonLinearModel.NumEstimatedCoefficientsD†XI INonLinearModel.NumCoefficients7NonLinearModel.NumCoefficientsP†WU UNonLinearModel.CoefficientCovariance7NonLinearModel.CoefficientCovarianceF†VK KNonLinearModel.CoefficientNames7NonLinearModel.CoefficientNames>†UC CNonLinearModel.Coefficients7NonLinearModel.Coefficients$†T) )NonLinearModel6NonLinearModelF†SK KMultinomialRegression.plotSlice5MultinomialRegression.plotSliceN†RS SMultinomialRegression.plotResiduals5MultinomialRegression.plotResidualsL†QQ QMultinomialRegression.testDeviance5MultinomialRegression.testDevianceD†PI IMultinomialRegression.coefTest5MultinomialRegression.coefTest@†OE EMultinomialRegression.coefCI5MultinomialRegression.coefCI@†NE EMultinomialRegression.random5MultinomialRegression.random>†MC CMultinomialRegression.feval5MultinomialRegression.fevalB†LG GMultinomialRegression.predict5MultinomialRegression.predict^†Kc cMultinomialRegression.MultinomialRegression5MultinomialRegression.MultinomialRegressionF†JK KMultinomialRegression.Variables4MultinomialRegression.Variables -°½v/è›V Ô ‡ D í š 5 î § R ¨ E ð f –9Èo Ó‚%¸eü•<éˆ'ÆIô¡Pý°J‡1O OClassificationBaggedEnsemble.loss>ClassificationBaggedEnsemble.lossP‡0U UClassificationBaggedEnsemble.predict>ClassificationBaggedEnsemble.predictN‡/S SClassificationBaggedEnsemble.resume>ClassificationBaggedEnsemble.resumeP‡.U UClassificationBaggedEnsemble.compact>ClassificationBaggedEnsemble.compactR‡-W WClassificationBaggedEnsemble.crossval>ClassificationBaggedEnsemble.crossvalz‡, ClassificationBaggedEnsemble.ClassificationBaggedEnsemble>ClassificationBaggedEnsemble.ClassificationBaggedEnsemble^‡+c cClassificationBaggedEnsemble.ScoreTransform=ClassificationBaggedEnsemble.ScoreTransform^‡*c cClassificationBaggedEnsemble.CombineWeights=ClassificationBaggedEnsemble.CombineWeights^‡)c cClassificationBaggedEnsemble.TrainedWeights=ClassificationBaggedEnsemble.TrainedWeightsP‡(U UClassificationBaggedEnsemble.Trained=ClassificationBaggedEnsemble.TrainedV‡'[ [ClassificationBaggedEnsemble.NumTrained=ClassificationBaggedEnsemble.NumTrainedd‡&i iClassificationBaggedEnsemble.UsePredForLearner=ClassificationBaggedEnsemble.UsePredForLearnerf‡%k kClassificationBaggedEnsemble.FitInfoDescription=ClassificationBaggedEnsemble.FitInfoDescriptionP‡$U UClassificationBaggedEnsemble.FitInfo=ClassificationBaggedEnsemble.FitInfoj‡#o oClassificationBaggedEnsemble.ReasonForTermination=ClassificationBaggedEnsemble.ReasonForTerminationZ‡"_ _ClassificationBaggedEnsemble.LearnerNames=ClassificationBaggedEnsemble.LearnerNamesN‡!S SClassificationBaggedEnsemble.Method=ClassificationBaggedEnsemble.MethodJ‡ O OClassificationBaggedEnsemble.Cost=ClassificationBaggedEnsemble.CostL‡Q QClassificationBaggedEnsemble.Prior=ClassificationBaggedEnsemble.PriorV‡[ [ClassificationBaggedEnsemble.ClassNames=ClassificationBaggedEnsemble.ClassNamesn‡s sClassificationBaggedEnsemble.ExpandedPredictorNames=ClassificationBaggedEnsemble.ExpandedPredictorNamesZ‡_ _ClassificationBaggedEnsemble.ResponseName=ClassificationBaggedEnsemble.ResponseNamel‡q qClassificationBaggedEnsemble.CategoricalPredictors=ClassificationBaggedEnsemble.CategoricalPredictors^‡c cClassificationBaggedEnsemble.PredictorNames=ClassificationBaggedEnsemble.PredictorNames�‡ � � ClassificationBaggedEnsemble.HyperparameterOptimizationResults=ClassificationBaggedEnsemble.HyperparameterOptimizationResultsR‡W WClassificationBaggedEnsemble.BinEdges=ClassificationBaggedEnsemble.BinEdges`‡e eClassificationBaggedEnsemble.NumObservations=ClassificationBaggedEnsemble.NumObservations`‡e eClassificationBaggedEnsemble.ModelParameters=ClassificationBaggedEnsemble.ModelParametersD‡I IClassificationBaggedEnsemble.W=ClassificationBaggedEnsemble.WR‡W WClassificationBaggedEnsemble.RowsUsed=ClassificationBaggedEnsemble.RowsUsedD‡I IClassificationBaggedEnsemble.Y=ClassificationBaggedEnsemble.YD‡I IClassificationBaggedEnsemble.X=ClassificationBaggedEnsemble.Xb‡g gClassificationBaggedEnsemble.UseObsForLearner=ClassificationBaggedEnsemble.UseObsForLearnerP‡U UClassificationBaggedEnsemble.Replace=ClassificationBaggedEnsemble.ReplaceT‡Y YClassificationBaggedEnsemble.FResample=ClassificationBaggedEnsemble.FResample@‡E EClassificationBaggedEnsembleA AClassificationDiscriminant?ClassificationDiscriminant�‡= � �ClassificationBaggedEnsemble.oobPermutedPredictorImportance>ClassificationBaggedEnsemble.oobPermutedPredictorImportanceT‡<Y YClassificationBaggedEnsemble.oobMargin>ClassificationBaggedEnsemble.oobMarginP‡;U UClassificationBaggedEnsemble.oobEdge>ClassificationBaggedEnsemble.oobEdgeP‡:U UClassificationBaggedEnsemble.oobLoss>ClassificationBaggedEnsemble.oobLossV‡9[ [ClassificationBaggedEnsemble.oobPredict>ClassificationBaggedEnsemble.oobPredicth‡8m mClassificationBaggedEnsemble.predictorImportance>ClassificationBaggedEnsemble.predictorImportanceX‡7] ]ClassificationBaggedEnsemble.resubMargin>ClassificationBaggedEnsemble.resubMarginT‡6Y YClassificationBaggedEnsemble.resubEdge>ClassificationBaggedEnsemble.resubEdgeT‡5Y YClassificationBaggedEnsemble.resubLoss>ClassificationBaggedEnsemble.resubLossZ‡4_ _ClassificationBaggedEnsemble.resubPredict>ClassificationBaggedEnsemble.resubPredictN‡3S SClassificationBaggedEnsemble.margin>ClassificationBaggedEnsemble.marginJ‡2O OClassificationBaggedEnsemble.edge>ClassificationBaggedEnsemble.edge 7‰¯`̃* Ó € - Ú ‰ Z ' ô Á € 1 ò ¥ \  Ê … 8Û€7öƒ4ï´{.Ù˜Y¿v9Ç~7ô±z?»€)Ò‰FˆK KClassificationEnsemble.BinEdgesFClassificationEnsemble.BinEdgesTˆY YClassificationEnsemble.NumObservationsFClassificationEnsemble.NumObservationsTˆY YClassificationEnsemble.ModelParametersFClassificationEnsemble.ModelParameters8ˆ= =ClassificationEnsemble.WFClassificationEnsemble.WFˆK KClassificationEnsemble.RowsUsedFClassificationEnsemble.RowsUsed8ˆ= =ClassificationEnsemble.YFClassificationEnsemble.Y8ˆ= =ClassificationEnsemble.XFClassificationEnsemble.X4ˆ9 9ClassificationEnsembleEClassificationEnsemble@ˆE EClassificationECOC.resubLossDClassificationECOC.resubLoss@ˆ E EClassificationECOC.resubEdgeDClassificationECOC.resubEdgeDˆ I IClassificationECOC.resubMarginDClassificationECOC.resubMarginFˆ K KClassificationECOC.resubPredictDClassificationECOC.resubPredict6ˆ ; ;ClassificationECOC.lossDClassificationECOC.loss6ˆ ; ;ClassificationECOC.edgeDClassificationECOC.edge:ˆ? ?ClassificationECOC.marginDClassificationECOC.marginFˆK KClassificationECOC.selectModelsDClassificationECOC.selectModelsXˆ] ]ClassificationECOC.discardSupportVectorsDClassificationECOC.discardSupportVectors<ˆA AClassificationECOC.predictDClassificationECOC.predict<ˆA AClassificationECOC.compactDClassificationECOC.compact>ˆC CClassificationECOC.crossvalDClassificationECOC.crossvalRˆW WClassificationECOC.ClassificationECOCDClassificationECOC.ClassificationECOCJˆO OClassificationECOC.ScoreTransformCClassificationECOC.ScoreTransform6ˆ; ;ClassificationECOC.CostCClassificationECOC.Cost8‡= =ClassificationECOC.PriorCClassificationECOC.PriorB‡~G GClassificationECOC.BinaryLossCClassificationECOC.BinaryLossL‡}Q QClassificationECOC.ModelParametersCClassificationECOC.ModelParametersp‡|u uClassificationECOC.HyperparameterOptimizationResultsCClassificationECOC.HyperparameterOptimizationResults>‡{C CClassificationECOC.BinEdgesCClassificationECOC.BinEdgesF‡zK KClassificationECOC.ResponseNameCClassificationECOC.ResponseNameX‡y] ]ClassificationECOC.CategoricalPredictorsCClassificationECOC.CategoricalPredictorsZ‡x_ _ClassificationECOC.ExpandedPredictorNamesCClassificationECOC.ExpandedPredictorNamesJ‡wO OClassificationECOC.PredictorNamesCClassificationECOC.PredictorNamesB‡vG GClassificationECOC.ClassNamesCClassificationECOC.ClassNamesJ‡uO OClassificationECOC.LearnerWeightsCClassificationECOC.LearnerWeightsB‡tG GClassificationECOC.CodingNameCClassificationECOC.CodingNameF‡sK KClassificationECOC.CodingMatrixCClassificationECOC.CodingMatrixJ‡rO OClassificationECOC.BinaryLearnersCClassificationECOC.BinaryLearners<‡qA AClassificationECOC.BinaryYCClassificationECOC.BinaryYL‡pQ QClassificationECOC.NumObservationsCClassificationECOC.NumObservations>‡oC CClassificationECOC.RowsUsedCClassificationECOC.RowsUsed0‡n5 5ClassificationECOC.WCClassificationECOC.W0‡m5 5ClassificationECOC.YCClassificationECOC.Y0‡l5 5ClassificationECOC.XCClassificationECOC.X,‡k1 1ClassificationECOCBClassificationECOCN‡jS SClassificationDiscriminant.cvshrinkAClassificationDiscriminant.cvshrinkP‡iU UClassificationDiscriminant.savemodelAClassificationDiscriminant.savemodelP‡hU UClassificationDiscriminant.resubLossAClassificationDiscriminant.resubLossP‡gU UClassificationDiscriminant.resubEdgeAClassificationDiscriminant.resubEdgeT‡fY YClassificationDiscriminant.resubMarginAClassificationDiscriminant.resubMarginV‡e[ [ClassificationDiscriminant.resubPredictAClassificationDiscriminant.resubPredictF‡dK KClassificationDiscriminant.logpAClassificationDiscriminant.logpH‡cM MClassificationDiscriminant.mahalAClassificationDiscriminant.mahalF‡bK KClassificationDiscriminant.edgeAClassificationDiscriminant.edgeL‡aQ QClassificationDiscriminant.compactAClassificationDiscriminant.compactN‡`S SClassificationDiscriminant.crossvalAClassificationDiscriminant.crossval 5�…0Í| Ê ‡ F  ° O  « P  ¼ g  ½ X ȃ<ûºuÅz/ೂQÅ|1ê§n1ê±xCü³rAè�XˆK] ]ClassificationGAM.ExpandedPredictorNamesIClassificationGAM.ExpandedPredictorNamesVˆJ[ [ClassificationGAM.CategoricalPredictorsIClassificationGAM.CategoricalPredictors.ˆI3 3ClassificationGAM.WIClassificationGAM.W>ˆHC CClassificationGAM.InterceptIClassificationGAM.InterceptFˆGK KClassificationGAM.NumIterationsIClassificationGAM.NumIterationsDˆFI IClassificationGAM.LearningRateIClassificationGAM.LearningRate2ˆE7 7ClassificationGAM.DoFIClassificationGAM.DoF6ˆD; ;ClassificationGAM.OrderIClassificationGAM.Order6ˆC; ;ClassificationGAM.KnotsIClassificationGAM.KnotsDˆBI IClassificationGAM.InteractionsIClassificationGAM.Interactions:ˆA? ?ClassificationGAM.FormulaIClassificationGAM.Formula6ˆ@; ;ClassificationGAM.PriorIClassificationGAM.Prior@ˆ?E EClassificationGAM.ClassNamesIClassificationGAM.ClassNamesDˆ>I IClassificationGAM.ResponseNameIClassificationGAM.ResponseNameHˆ=M MClassificationGAM.PredictorNamesIClassificationGAM.PredictorNamesFˆ<K KClassificationGAM.NumPredictorsIClassificationGAM.NumPredictors<ˆ;A AClassificationGAM.RowsUsedIClassificationGAM.RowsUsedJˆ:O OClassificationGAM.NumObservationsIClassificationGAM.NumObservations.ˆ93 3ClassificationGAM.YIClassificationGAM.Y.ˆ83 3ClassificationGAM.XIClassificationGAM.X*ˆ7/ /ClassificationGAMHClassificationGAMLˆ6Q QClassificationEnsemble.resubMarginGClassificationEnsemble.resubMarginHˆ5M MClassificationEnsemble.resubEdgeGClassificationEnsemble.resubEdgeHˆ4M MClassificationEnsemble.resubLossGClassificationEnsemble.resubLossNˆ3S SClassificationEnsemble.resubPredictGClassificationEnsemble.resubPredict\ˆ2a aClassificationEnsemble.predictorImportanceGClassificationEnsemble.predictorImportanceBˆ1G GClassificationEnsemble.marginGClassificationEnsemble.margin>ˆ0C CClassificationEnsemble.edgeGClassificationEnsemble.edge>ˆ/C CClassificationEnsemble.lossGClassificationEnsemble.lossDˆ.I IClassificationEnsemble.predictGClassificationEnsemble.predictBˆ-G GClassificationEnsemble.resumeGClassificationEnsemble.resumeDˆ,I IClassificationEnsemble.compactGClassificationEnsemble.compactFˆ+K KClassificationEnsemble.crossvalGClassificationEnsemble.crossvalbˆ*g gClassificationEnsemble.ClassificationEnsembleGClassificationEnsemble.ClassificationEnsembleRˆ)W WClassificationEnsemble.ScoreTransformFClassificationEnsemble.ScoreTransformRˆ(W WClassificationEnsemble.CombineWeightsFClassificationEnsemble.CombineWeightsRˆ'W WClassificationEnsemble.TrainedWeightsFClassificationEnsemble.TrainedWeightsDˆ&I IClassificationEnsemble.TrainedFClassificationEnsemble.TrainedJˆ%O OClassificationEnsemble.NumTrainedFClassificationEnsemble.NumTrainedXˆ$] ]ClassificationEnsemble.UsePredForLearnerFClassificationEnsemble.UsePredForLearnerZˆ#_ _ClassificationEnsemble.FitInfoDescriptionFClassificationEnsemble.FitInfoDescriptionDˆ"I IClassificationEnsemble.FitInfoFClassificationEnsemble.FitInfo^ˆ!c cClassificationEnsemble.ReasonForTerminationFClassificationEnsemble.ReasonForTerminationNˆ S SClassificationEnsemble.LearnerNamesFClassificationEnsemble.LearnerNamesBˆG GClassificationEnsemble.MethodFClassificationEnsemble.Method>ˆC CClassificationEnsemble.CostFClassificationEnsemble.Cost@ˆE EClassificationEnsemble.PriorFClassificationEnsemble.PriorJˆO OClassificationEnsemble.ClassNamesFClassificationEnsemble.ClassNamesbˆg gClassificationEnsemble.ExpandedPredictorNamesFClassificationEnsemble.ExpandedPredictorNamesNˆS SClassificationEnsemble.ResponseNameFClassificationEnsemble.ResponseName`ˆe eClassificationEnsemble.CategoricalPredictorsFClassificationEnsemble.CategoricalPredictorsRˆW WClassificationEnsemble.PredictorNamesFClassificationEnsemble.PredictorNamesxˆ} }ClassificationEnsemble.HyperparameterOptimizationResultsFClassificationEnsemble.HyperparameterOptimizationResults 9‡¿~=þ¥X À   × Œ ; î ± r 5 ú à Œ E ¿ ~ = Ï€7ø»jºm «b!Î}2ï¶w×–YÛ”g6Ô‡J‰O OClassificationKNN.NumObservationsOClassificationKNN.NumObservations.‰3 3ClassificationKNN.YOClassificationKNN.Y.‰3 3ClassificationKNN.XOClassificationKNN.X.‰3 3ClassificationKNN.WOClassificationKNN.W*‰/ /ClassificationKNNNClassificationKNNDˆI IClassificationKernel.savemodelMClassificationKernel.savemodel>ˆ~C CClassificationKernel.resumeMClassificationKernel.resume:ˆ}? ?ClassificationKernel.lossMClassificationKernel.loss:ˆ|? ?ClassificationKernel.edgeMClassificationKernel.edge>ˆ{C CClassificationKernel.marginMClassificationKernel.margin@ˆzE EClassificationKernel.predictMClassificationKernel.predictZˆy_ _ClassificationKernel.ClassificationKernelMClassificationKernel.ClassificationKernel<ˆxA AClassificationKernel.SigmaLClassificationKernel.Sigma6ˆw; ;ClassificationKernel.MuLClassificationKernel.Mu@ˆvE EClassificationKernel.LearnerLClassificationKernel.LearnerHˆuM MClassificationKernel.KernelScaleLClassificationKernel.KernelScaleNˆtS SClassificationKernel.RegularizationLClassificationKernel.RegularizationPˆsU UClassificationKernel.ModelParametersLClassificationKernel.ModelParameters>ˆrC CClassificationKernel.LambdaLClassificationKernel.LambdaFˆqK KClassificationKernel.FittedLossLClassificationKernel.FittedLoss^ˆpc cClassificationKernel.NumExpansionDimensionsLClassificationKernel.NumExpansionDimensions^ˆoc cClassificationKernel.ExpandedPredictorNamesLClassificationKernel.ExpandedPredictorNamesJˆnO OClassificationKernel.ResponseNameLClassificationKernel.ResponseName\ˆma aClassificationKernel.CategoricalPredictorsLClassificationKernel.CategoricalPredictorsNˆlS SClassificationKernel.PredictorNamesLClassificationKernel.PredictorNamesNˆkS SClassificationKernel.ScoreTransformLClassificationKernel.ScoreTransform:ˆj? ?ClassificationKernel.CostLClassificationKernel.Cost<ˆiA AClassificationKernel.PriorLClassificationKernel.PriorFˆhK KClassificationKernel.ClassNamesLClassificationKernel.ClassNamesLˆgQ QClassificationKernel.BoxConstraintLClassificationKernel.BoxConstraint0ˆf5 5ClassificationKernelKClassificationKernel8ˆe= =ClassificationGAM.resumeJClassificationGAM.resume>ˆdC CClassificationGAM.savemodelJClassificationGAM.savemodel>ˆcC CClassificationGAM.resubLossJClassificationGAM.resubLoss>ˆbC CClassificationGAM.resubEdgeJClassificationGAM.resubEdgeBˆaG GClassificationGAM.resubMarginJClassificationGAM.resubMarginDˆ`I IClassificationGAM.resubPredictJClassificationGAM.resubPredict4ˆ_9 9ClassificationGAM.lossJClassificationGAM.loss4ˆ^9 9ClassificationGAM.edgeJClassificationGAM.edge8ˆ]= =ClassificationGAM.marginJClassificationGAM.margin:ˆ\? ?ClassificationGAM.compactJClassificationGAM.compact<ˆ[A AClassificationGAM.crossvalJClassificationGAM.crossval:ˆZ? ?ClassificationGAM.predictJClassificationGAM.predictJˆYO OClassificationGAM.addInteractionsJClassificationGAM.addInteractionsNˆXS SClassificationGAM.ClassificationGAMJClassificationGAM.ClassificationGAMHˆWM MClassificationGAM.ScoreTransformIClassificationGAM.ScoreTransform4ˆV9 9ClassificationGAM.CostIClassificationGAM.CostnˆUs sClassificationGAM.HyperparameterOptimizationResultsIClassificationGAM.HyperparameterOptimizationResults>ˆTC CClassificationGAM.TreeModelIClassificationGAM.TreeModel>ˆSC CClassificationGAM.FitMethodIClassificationGAM.FitMethodTˆRY YClassificationGAM.ReasonForTerminationIClassificationGAM.ReasonForTerminationJˆQO OClassificationGAM.ModelParametersIClassificationGAM.ModelParametersVˆP[ [ClassificationGAM.PairDetectionBinEdgesIClassificationGAM.PairDetectionBinEdges<ˆOA AClassificationGAM.BinEdgesIClassificationGAM.BinEdges>ˆNC CClassificationGAM.IntMatrixIClassificationGAM.IntMatrix>ˆMC CClassificationGAM.ModelwIntIClassificationGAM.ModelwInt>ˆLC CClassificationGAM.BaseModelIClassificationGAM.BaseModel 8¶Áx-Ôy2 ï ¶ ƒ D   u  ½ ~ 3 ò ­ d - ô © h Ú£h)ò«f%ä£p'è«Z ª]ü¹|?öµb´q0ó¶:‰<? ?ClassificationLinear.lossSClassificationLinear.loss:‰;? ?ClassificationLinear.edgeSClassificationLinear.edge>‰:C CClassificationLinear.marginSClassificationLinear.margin@‰9E EClassificationLinear.predictSClassificationLinear.predictZ‰8_ _ClassificationLinear.ClassificationLinearSClassificationLinear.ClassificationLinearN‰7S SClassificationLinear.RegularizationRClassificationLinear.RegularizationP‰6U UClassificationLinear.ModelParametersRClassificationLinear.ModelParameters>‰5C CClassificationLinear.LambdaRClassificationLinear.LambdaF‰4K KClassificationLinear.FittedLossRClassificationLinear.FittedLoss:‰3? ?ClassificationLinear.BiasRClassificationLinear.Bias:‰2? ?ClassificationLinear.BetaRClassificationLinear.Beta@‰1E EClassificationLinear.LearnerRClassificationLinear.Learner^‰0c cClassificationLinear.ExpandedPredictorNamesRClassificationLinear.ExpandedPredictorNamesJ‰/O OClassificationLinear.ResponseNameRClassificationLinear.ResponseName\‰.a aClassificationLinear.CategoricalPredictorsRClassificationLinear.CategoricalPredictorsN‰-S SClassificationLinear.PredictorNamesRClassificationLinear.PredictorNamesN‰,S SClassificationLinear.ScoreTransformRClassificationLinear.ScoreTransform:‰+? ?ClassificationLinear.CostRClassificationLinear.Cost<‰*A AClassificationLinear.PriorRClassificationLinear.PriorF‰)K KClassificationLinear.ClassNamesRClassificationLinear.ClassNames0‰(5 5ClassificationLinearQClassificationLinear>‰'C CClassificationKNN.savemodelPClassificationKNN.savemodel>‰&C CClassificationKNN.resubLossPClassificationKNN.resubLoss>‰%C CClassificationKNN.resubEdgePClassificationKNN.resubEdgeB‰$G GClassificationKNN.resubMarginPClassificationKNN.resubMarginD‰#I IClassificationKNN.resubPredictPClassificationKNN.resubPredict4‰"9 9ClassificationKNN.edgePClassificationKNN.edge<‰!A AClassificationKNN.crossvalPClassificationKNN.crossval8‰ = =ClassificationKNN.marginPClassificationKNN.margin4‰9 9ClassificationKNN.lossPClassificationKNN.loss:‰? ?ClassificationKNN.predictPClassificationKNN.predictN‰S SClassificationKNN.ClassificationKNNPClassificationKNN.ClassificationKNN>‰C CClassificationKNN.CacheSizeOClassificationKNN.CacheSizeH‰M MClassificationKNN.ScoreTransformOClassificationKNN.ScoreTransform6‰; ;ClassificationKNN.PriorOClassificationKNN.Prior4‰9 9ClassificationKNN.CostOClassificationKNN.CostF‰K KClassificationKNN.DistParameterOClassificationKNN.DistParameterB‰G GClassificationKNN.IncludeTiesOClassificationKNN.IncludeTies>‰C CClassificationKNN.BreakTiesOClassificationKNN.BreakTiesH‰M MClassificationKNN.DistanceWeightOClassificationKNN.DistanceWeight<‰A AClassificationKNN.DistanceOClassificationKNN.DistanceD‰I IClassificationKNN.NumNeighborsOClassificationKNN.NumNeighborsn‰s sClassificationKNN.HyperparameterOptimizationResultsOClassificationKNN.HyperparameterOptimizationResultsJ‰O OClassificationKNN.ModelParametersOClassificationKNN.ModelParameters<‰A AClassificationKNN.BinEdgesOClassificationKNN.BinEdges@‰E EClassificationKNN.BucketSizeOClassificationKNN.BucketSize<‰A AClassificationKNN.NSMethodOClassificationKNN.NSMethod0‰ 5 5ClassificationKNN.MuOClassificationKNN.Mu6‰ ; ;ClassificationKNN.SigmaOClassificationKNN.Sigma@‰ E EClassificationKNN.ClassNamesOClassificationKNN.ClassNamesD‰ I IClassificationKNN.ResponseNameOClassificationKNN.ResponseNameX‰ ] ]ClassificationKNN.ExpandedPredictorNamesOClassificationKNN.ExpandedPredictorNamesV‰[ [ClassificationKNN.CategoricalPredictorsOClassificationKNN.CategoricalPredictorsH‰M MClassificationKNN.PredictorNamesOClassificationKNN.PredictorNamesF‰K KClassificationKNN.NumPredictorsOClassificationKNN.NumPredictors<‰A AClassificationKNN.RowsUsedOClassificationKNN.RowsUsed 1{³l1ò³f' Ì q $ Ë d  ¦ U Ô � H ï � O Ÿ@÷¬eø­`̇Bý¨U·h'â�<éŒ-Ò{T‰mY YClassificationNeuralNetwork.ClassNamesXClassificationNeuralNetwork.ClassNamesX‰l] ]ClassificationNeuralNetwork.ResponseNameXClassificationNeuralNetwork.ResponseName\‰ka aClassificationNeuralNetwork.PredictorNamesXClassificationNeuralNetwork.PredictorNamesZ‰j_ _ClassificationNeuralNetwork.NumPredictorsXClassificationNeuralNetwork.NumPredictorsP‰iU UClassificationNeuralNetwork.RowsUsedXClassificationNeuralNetwork.RowsUsed^‰hc cClassificationNeuralNetwork.NumObservationsXClassificationNeuralNetwork.NumObservationsB‰gG GClassificationNeuralNetwork.YXClassificationNeuralNetwork.YB‰fG GClassificationNeuralNetwork.XXClassificationNeuralNetwork.X>‰eC CClassificationNeuralNetworkWClassificationNeuralNetworkL‰dQ QClassificationNaiveBayes.savemodelVClassificationNaiveBayes.savemodelL‰cQ QClassificationNaiveBayes.resubLossVClassificationNaiveBayes.resubLossL‰bQ QClassificationNaiveBayes.resubEdgeVClassificationNaiveBayes.resubEdgeP‰aU UClassificationNaiveBayes.resubMarginVClassificationNaiveBayes.resubMarginR‰`W WClassificationNaiveBayes.resubPredictVClassificationNaiveBayes.resubPredictB‰_G GClassificationNaiveBayes.logpVClassificationNaiveBayes.logpB‰^G GClassificationNaiveBayes.lossVClassificationNaiveBayes.lossB‰]G GClassificationNaiveBayes.edgeVClassificationNaiveBayes.edgeF‰\K KClassificationNaiveBayes.marginVClassificationNaiveBayes.marginH‰[M MClassificationNaiveBayes.compactVClassificationNaiveBayes.compactJ‰ZO OClassificationNaiveBayes.crossvalVClassificationNaiveBayes.crossvalH‰YM MClassificationNaiveBayes.predictVClassificationNaiveBayes.predictj‰Xo oClassificationNaiveBayes.ClassificationNaiveBayesVClassificationNaiveBayes.ClassificationNaiveBayesD‰WI IClassificationNaiveBayes.WidthUClassificationNaiveBayes.WidthH‰VM MClassificationNaiveBayes.SupportUClassificationNaiveBayes.SupportF‰UK KClassificationNaiveBayes.KernelUClassificationNaiveBayes.Kernel\‰Ta aClassificationNaiveBayes.CategoricalLevelsUClassificationNaiveBayes.CategoricalLevelsf‰Sk kClassificationNaiveBayes.DistributionParametersUClassificationNaiveBayes.DistributionParametersD‰RI IClassificationNaiveBayes.SigmaUClassificationNaiveBayes.Sigma>‰QC CClassificationNaiveBayes.MuUClassificationNaiveBayes.Mu\‰Pa aClassificationNaiveBayes.DistributionNamesUClassificationNaiveBayes.DistributionNamesV‰O[ [ClassificationNaiveBayes.ScoreTransformUClassificationNaiveBayes.ScoreTransformB‰NG GClassificationNaiveBayes.CostUClassificationNaiveBayes.CostD‰MI IClassificationNaiveBayes.PriorUClassificationNaiveBayes.Prior~‰L � �ClassificationNaiveBayes.HyperparameterOptimizationResultsUClassificationNaiveBayes.HyperparameterOptimizationResultsN‰KS SClassificationNaiveBayes.ClassNamesUClassificationNaiveBayes.ClassNamesf‰Jk kClassificationNaiveBayes.ExpandedPredictorNamesUClassificationNaiveBayes.ExpandedPredictorNamesR‰IW WClassificationNaiveBayes.ResponseNameUClassificationNaiveBayes.ResponseNamed‰Hi iClassificationNaiveBayes.CategoricalPredictorsUClassificationNaiveBayes.CategoricalPredictorsV‰G[ [ClassificationNaiveBayes.PredictorNamesUClassificationNaiveBayes.PredictorNamesJ‰FO OClassificationNaiveBayes.BinEdgesUClassificationNaiveBayes.BinEdgesX‰E] ]ClassificationNaiveBayes.NumObservationsUClassificationNaiveBayes.NumObservationsX‰D] ]ClassificationNaiveBayes.ModelParametersUClassificationNaiveBayes.ModelParameters<‰CA AClassificationNaiveBayes.WUClassificationNaiveBayes.WJ‰BO OClassificationNaiveBayes.RowsUsedUClassificationNaiveBayes.RowsUsed<‰AA AClassificationNaiveBayes.XUClassificationNaiveBayes.X<‰@A AClassificationNaiveBayes.YUClassificationNaiveBayes.Y8‰?= =ClassificationNaiveBayesTClassificationNaiveBayesD‰>I IClassificationLinear.savemodelSClassificationLinear.savemodelJ‰=O OClassificationLinear.selectModelsSClassificationLinear.selectModels ,�³l¼O ô • 4 Ó z + Ð w  É „  ¨ UÍ‚#ªYþ¯dÀkÃr؃$ÉnÎaþ�nŠs sClassificationPartitionedECOC.CategoricalPredictors[ClassificationPartitionedECOC.CategoricalPredictors`Še eClassificationPartitionedECOC.PredictorNames[ClassificationPartitionedECOC.PredictorNamesjŠo oClassificationPartitionedECOC.CrossValidatedModel[ClassificationPartitionedECOC.CrossValidatedModelNŠS SClassificationPartitionedECOC.Prior[ClassificationPartitionedECOC.PriorLŠQ QClassificationPartitionedECOC.Cost[ClassificationPartitionedECOC.CostXŠ] ]ClassificationPartitionedECOC.ClassNames[ClassificationPartitionedECOC.ClassNamesXŠ] ]ClassificationPartitionedECOC.BinaryLoss[ClassificationPartitionedECOC.BinaryLoss\Ša aClassificationPartitionedECOC.CodingMatrix[ClassificationPartitionedECOC.CodingMatrixRŠW WClassificationPartitionedECOC.BinaryY[ClassificationPartitionedECOC.BinaryYBŠG GClassificationPartitionedECOCZClassificationPartitionedECOCRŠW WClassificationNeuralNetwork.savemodelYClassificationNeuralNetwork.savemodelNŠS SClassificationNeuralNetwork.compactYClassificationNeuralNetwork.compactPŠ U UClassificationNeuralNetwork.crossvalYClassificationNeuralNetwork.crossvalRŠ W WClassificationNeuralNetwork.resubLossYClassificationNeuralNetwork.resubLossRŠ W WClassificationNeuralNetwork.resubEdgeYClassificationNeuralNetwork.resubEdgeVŠ [ [ClassificationNeuralNetwork.resubMarginYClassificationNeuralNetwork.resubMarginHŠ M MClassificationNeuralNetwork.lossYClassificationNeuralNetwork.lossHŠM MClassificationNeuralNetwork.edgeYClassificationNeuralNetwork.edgeLŠQ QClassificationNeuralNetwork.marginYClassificationNeuralNetwork.marginXŠ] ]ClassificationNeuralNetwork.resubPredictYClassificationNeuralNetwork.resubPredictNŠS SClassificationNeuralNetwork.predictYClassificationNeuralNetwork.predictvŠ{ {ClassificationNeuralNetwork.ClassificationNeuralNetworkYClassificationNeuralNetwork.ClassificationNeuralNetwork\Ša aClassificationNeuralNetwork.ScoreTransformXClassificationNeuralNetwork.ScoreTransformHŠM MClassificationNeuralNetwork.CostXClassificationNeuralNetwork.Cost�Š � �ClassificationNeuralNetwork.HyperparameterOptimizationResultsXClassificationNeuralNetwork.HyperparameterOptimizationResultsPŠU UClassificationNeuralNetwork.BinEdgesXClassificationNeuralNetwork.BinEdgesl‰q qClassificationNeuralNetwork.ExpandedPredictorNamesXClassificationNeuralNetwork.ExpandedPredictorNamesj‰~o oClassificationNeuralNetwork.CategoricalPredictorsXClassificationNeuralNetwork.CategoricalPredictorsB‰}G GClassificationNeuralNetwork.WXClassificationNeuralNetwork.WJ‰|O OClassificationNeuralNetwork.PriorXClassificationNeuralNetwork.Prior^‰{c cClassificationNeuralNetwork.TrainingHistoryXClassificationNeuralNetwork.TrainingHistoryV‰z[ [ClassificationNeuralNetwork.LayerBiasesXClassificationNeuralNetwork.LayerBiasesX‰y] ]ClassificationNeuralNetwork.LayerWeightsXClassificationNeuralNetwork.LayerWeightsL‰xQ QClassificationNeuralNetwork.SolverXClassificationNeuralNetwork.SolverV‰w[ [ClassificationNeuralNetwork.DisplayInfoXClassificationNeuralNetwork.DisplayInfo^‰vc cClassificationNeuralNetwork.ConvergenceInfoXClassificationNeuralNetwork.ConvergenceInfo^‰uc cClassificationNeuralNetwork.ModelParametersXClassificationNeuralNetwork.ModelParameters\‰ta aClassificationNeuralNetwork.IterationLimitXClassificationNeuralNetwork.IterationLimitX‰s] ]ClassificationNeuralNetwork.LearningRateXClassificationNeuralNetwork.LearningRatej‰ro oClassificationNeuralNetwork.OutputLayerActivationXClassificationNeuralNetwork.OutputLayerActivationV‰q[ [ClassificationNeuralNetwork.ActivationsXClassificationNeuralNetwork.ActivationsT‰pY YClassificationNeuralNetwork.LayerSizesXClassificationNeuralNetwork.LayerSizesD‰oI IClassificationNeuralNetwork.MuXClassificationNeuralNetwork.MuJ‰nO OClassificationNeuralNetwork.SigmaXClassificationNeuralNetwork.Sigma *–¡<óªa ü § V ý ¦ C ä ‡ . Õ ~ 1 Ð _êŸ2á�?Òu»\ù¢IÞwµPñ–XŠC] ]ClassificationPartitionedEnsemble.resume_ClassificationPartitionedEnsemble.resume\ŠBa aClassificationPartitionedEnsemble.kfoldfun_ClassificationPartitionedEnsemble.kfoldfunbŠAg gClassificationPartitionedEnsemble.kfoldMargin_ClassificationPartitionedEnsemble.kfoldMargin^Š@c cClassificationPartitionedEnsemble.kfoldEdge_ClassificationPartitionedEnsemble.kfoldEdge^Š?c cClassificationPartitionedEnsemble.kfoldLoss_ClassificationPartitionedEnsemble.kfoldLossdŠ>i iClassificationPartitionedEnsemble.kfoldPredict_ClassificationPartitionedEnsemble.kfoldPredicthŠ=m mClassificationPartitionedEnsemble.ScoreTransform^ClassificationPartitionedEnsemble.ScoreTransformVŠ<[ [ClassificationPartitionedEnsemble.Prior^ClassificationPartitionedEnsemble.PriorTŠ;Y YClassificationPartitionedEnsemble.Cost^ClassificationPartitionedEnsemble.Cost`Š:e eClassificationPartitionedEnsemble.ClassNames^ClassificationPartitionedEnsemble.ClassNames\Š9a aClassificationPartitionedEnsemble.BinEdges^ClassificationPartitionedEnsemble.BinEdges^Š8c cClassificationPartitionedEnsemble.Partition^ClassificationPartitionedEnsemble.PartitionVŠ7[ [ClassificationPartitionedEnsemble.KFold^ClassificationPartitionedEnsemble.KFoldZŠ6_ _ClassificationPartitionedEnsemble.Trained^ClassificationPartitionedEnsemble.TrainedjŠ5o oClassificationPartitionedEnsemble.ModelParameters^ClassificationPartitionedEnsemble.ModelParametersNŠ4S SClassificationPartitionedEnsemble.W^ClassificationPartitionedEnsemble.WNŠ3S SClassificationPartitionedEnsemble.Y^ClassificationPartitionedEnsemble.YNŠ2S SClassificationPartitionedEnsemble.X^ClassificationPartitionedEnsemble.XjŠ1o oClassificationPartitionedEnsemble.NumObservations^ClassificationPartitionedEnsemble.NumObservationsdŠ0i iClassificationPartitionedEnsemble.ResponseName^ClassificationPartitionedEnsemble.ResponseNamevŠ/{ {ClassificationPartitionedEnsemble.CategoricalPredictors^ClassificationPartitionedEnsemble.CategoricalPredictorshŠ.m mClassificationPartitionedEnsemble.PredictorNames^ClassificationPartitionedEnsemble.PredictorNamesrŠ-w wClassificationPartitionedEnsemble.CrossValidatedModel^ClassificationPartitionedEnsemble.CrossValidatedModelnŠ,s sClassificationPartitionedEnsemble.NumTrainedPerFold^ClassificationPartitionedEnsemble.NumTrainedPerFold^Š+c cClassificationPartitionedEnsemble.Trainable^ClassificationPartitionedEnsemble.TrainableJŠ*O OClassificationPartitionedEnsemble]ClassificationPartitionedEnsembleTŠ)Y YClassificationPartitionedECOC.kfoldfun\ClassificationPartitionedECOC.kfoldfunVŠ([ [ClassificationPartitionedECOC.kfoldLoss\ClassificationPartitionedECOC.kfoldLossVŠ'[ [ClassificationPartitionedECOC.kfoldEdge\ClassificationPartitionedECOC.kfoldEdgeZŠ&_ _ClassificationPartitionedECOC.kfoldMargin\ClassificationPartitionedECOC.kfoldMargin\Š%a aClassificationPartitionedECOC.kfoldPredict\ClassificationPartitionedECOC.kfoldPredict`Š$e eClassificationPartitionedECOC.ScoreTransform[ClassificationPartitionedECOC.ScoreTransformTŠ#Y YClassificationPartitionedECOC.BinEdges[ClassificationPartitionedECOC.BinEdgesVŠ"[ [ClassificationPartitionedECOC.Partition[ClassificationPartitionedECOC.PartitionNŠ!S SClassificationPartitionedECOC.KFold[ClassificationPartitionedECOC.KFoldRŠ W WClassificationPartitionedECOC.Trained[ClassificationPartitionedECOC.TrainedbŠg gClassificationPartitionedECOC.ModelParameters[ClassificationPartitionedECOC.ModelParametersFŠK KClassificationPartitionedECOC.W[ClassificationPartitionedECOC.WFŠK KClassificationPartitionedECOC.Y[ClassificationPartitionedECOC.YFŠK KClassificationPartitionedECOC.X[ClassificationPartitionedECOC.XbŠg gClassificationPartitionedECOC.NumObservations[ClassificationPartitionedECOC.NumObservations\Ša aClassificationPartitionedECOC.ResponseName[ClassificationPartitionedECOC.ResponseName )�·X°I Ø o " Õ n ù – = è ‹ " – 3ÒuÏpÈað‡:톮U£:®Kê�ZŠl_ _ClassificationPartitionedLinear.kfoldEdgeeClassificationPartitionedLinear.kfoldEdge^Škc cClassificationPartitionedLinear.kfoldMargineClassificationPartitionedLinear.kfoldMargin`Šje eClassificationPartitionedLinear.kfoldPredicteClassificationPartitionedLinear.kfoldPredict�Ši � � ClassificationPartitionedLinear.ClassificationPartitionedLineareClassificationPartitionedLinear.ClassificationPartitionedLinearfŠhk kClassificationPartitionedLinear.ModelParametersdClassificationPartitionedLinear.ModelParametersZŠg_ _ClassificationPartitionedLinear.PartitiondClassificationPartitionedLinear.PartitionRŠfW WClassificationPartitionedLinear.KFolddClassificationPartitionedLinear.KFoldVŠe[ [ClassificationPartitionedLinear.TraineddClassificationPartitionedLinear.Trained`Šde eClassificationPartitionedLinear.ResponseNamedClassificationPartitionedLinear.ResponseNamerŠcw wClassificationPartitionedLinear.CategoricalPredictorsdClassificationPartitionedLinear.CategoricalPredictorsdŠbi iClassificationPartitionedLinear.PredictorNamesdClassificationPartitionedLinear.PredictorNamesJŠaO OClassificationPartitionedLinear.WdClassificationPartitionedLinear.WJŠ`O OClassificationPartitionedLinear.YdClassificationPartitionedLinear.YfŠ_k kClassificationPartitionedLinear.NumObservationsdClassificationPartitionedLinear.NumObservationsnŠ^s sClassificationPartitionedLinear.CrossValidatedModeldClassificationPartitionedLinear.CrossValidatedModeldŠ]i iClassificationPartitionedLinear.ScoreTransformdClassificationPartitionedLinear.ScoreTransformRŠ\W WClassificationPartitionedLinear.PriordClassificationPartitionedLinear.PriorPŠ[U UClassificationPartitionedLinear.CostdClassificationPartitionedLinear.Cost\ŠZa aClassificationPartitionedLinear.ClassNamesdClassificationPartitionedLinear.ClassNamesFŠYK KClassificationPartitionedLinearcClassificationPartitionedLinearZŠX_ _ClassificationPartitionedKernel.kfoldLossbClassificationPartitionedKernel.kfoldLossZŠW_ _ClassificationPartitionedKernel.kfoldEdgebClassificationPartitionedKernel.kfoldEdge^ŠVc cClassificationPartitionedKernel.kfoldMarginbClassificationPartitionedKernel.kfoldMargin`ŠUe eClassificationPartitionedKernel.kfoldPredictbClassificationPartitionedKernel.kfoldPredict�ŠT � � ClassificationPartitionedKernel.ClassificationPartitionedKernelbClassificationPartitionedKernel.ClassificationPartitionedKernelfŠSk kClassificationPartitionedKernel.ModelParametersaClassificationPartitionedKernel.ModelParametersZŠR_ _ClassificationPartitionedKernel.PartitionaClassificationPartitionedKernel.PartitionRŠQW WClassificationPartitionedKernel.KFoldaClassificationPartitionedKernel.KFoldVŠP[ [ClassificationPartitionedKernel.TrainedaClassificationPartitionedKernel.Trained`ŠOe eClassificationPartitionedKernel.ResponseNameaClassificationPartitionedKernel.ResponseNamerŠNw wClassificationPartitionedKernel.CategoricalPredictorsaClassificationPartitionedKernel.CategoricalPredictorsdŠMi iClassificationPartitionedKernel.PredictorNamesaClassificationPartitionedKernel.PredictorNamesJŠLO OClassificationPartitionedKernel.WaClassificationPartitionedKernel.WJŠKO OClassificationPartitionedKernel.YaClassificationPartitionedKernel.YfŠJk kClassificationPartitionedKernel.NumObservationsaClassificationPartitionedKernel.NumObservationsnŠIs sClassificationPartitionedKernel.CrossValidatedModelaClassificationPartitionedKernel.CrossValidatedModeldŠHi iClassificationPartitionedKernel.ScoreTransformaClassificationPartitionedKernel.ScoreTransformRŠGW WClassificationPartitionedKernel.PrioraClassificationPartitionedKernel.PriorPŠFU UClassificationPartitionedKernel.CostaClassificationPartitionedKernel.Cost\ŠEa aClassificationPartitionedKernel.ClassNamesaClassificationPartitionedKernel.ClassNamesFŠDK KClassificationPartitionedKernel`ClassificationPartitionedKernel 2q£\ÿ®[ ö ‡ " ¯ N ç œ Q  Ÿ H õ š AÖNí’3ØR!ð£dЉF Ú�Tæ™DùÀ‰X ¾qJ‹O OClassificationSVM.OutlierFractionjClassificationSVM.OutlierFractionH‹M MClassificationSVM.BoxConstraintsjClassificationSVM.BoxConstraintsL‹Q QClassificationSVM.KernelParametersjClassificationSVM.KernelParameters.‹3 3ClassificationSVM.WjClassificationSVM.W4‹9 9ClassificationSVM.CostjClassificationSVM.Cost6‹; ;ClassificationSVM.PriorjClassificationSVM.PriorH‹M MClassificationSVM.SupportVectorsjClassificationSVM.SupportVectorsR‹W WClassificationSVM.SupportVectorLabelsjClassificationSVM.SupportVectorLabelsJ‹O OClassificationSVM.IsSupportVectorjClassificationSVM.IsSupportVector4‹9 9ClassificationSVM.BiasjClassificationSVM.Bias4‹9 9ClassificationSVM.BetajClassificationSVM.Beta6‹; ;ClassificationSVM.AlphajClassificationSVM.AlphaJ‹O OClassificationSVM.ModelParametersjClassificationSVM.ModelParameters0‹5 5ClassificationSVM.MujClassificationSVM.Mu6‹; ;ClassificationSVM.SigmajClassificationSVM.Sigma@‹E EClassificationSVM.ClassNamesjClassificationSVM.ClassNamesD‹I IClassificationSVM.ResponseNamejClassificationSVM.ResponseNameH‹ M MClassificationSVM.PredictorNamesjClassificationSVM.PredictorNamesF‹ K KClassificationSVM.NumPredictorsjClassificationSVM.NumPredictors<‹ A AClassificationSVM.RowsUsedjClassificationSVM.RowsUsedJ‹ O OClassificationSVM.NumObservationsjClassificationSVM.NumObservations.‹ 3 3ClassificationSVM.YjClassificationSVM.Y.‹3 3ClassificationSVM.XjClassificationSVM.X*‹/ /ClassificationSVMiClassificationSVMV‹[ [ClassificationPartitionedModel.kfoldfunhClassificationPartitionedModel.kfoldfunX‹] ]ClassificationPartitionedModel.kfoldEdgehClassificationPartitionedModel.kfoldEdge\‹a aClassificationPartitionedModel.kfoldMarginhClassificationPartitionedModel.kfoldMarginX‹] ]ClassificationPartitionedModel.kfoldLosshClassificationPartitionedModel.kfoldLoss^‹c cClassificationPartitionedModel.kfoldPredicthClassificationPartitionedModel.kfoldPredict�‹ � �ClassificationPartitionedModel.ClassificationPartitionedModelhClassificationPartitionedModel.ClassificationPartitionedModelh‹m mClassificationPartitionedModel.NumTrainedPerFoldgClassificationPartitionedModel.NumTrainedPerFoldVŠ[ [ClassificationPartitionedModel.BinEdgesgClassificationPartitionedModel.BinEdgesXŠ~] ]ClassificationPartitionedModel.PartitiongClassificationPartitionedModel.PartitionPŠ}U UClassificationPartitionedModel.KFoldgClassificationPartitionedModel.KFoldTŠ|Y YClassificationPartitionedModel.TrainedgClassificationPartitionedModel.TraineddŠ{i iClassificationPartitionedModel.ModelParametersgClassificationPartitionedModel.ModelParametersHŠzM MClassificationPartitionedModel.WgClassificationPartitionedModel.WHŠyM MClassificationPartitionedModel.YgClassificationPartitionedModel.YHŠxM MClassificationPartitionedModel.XgClassificationPartitionedModel.XdŠwi iClassificationPartitionedModel.NumObservationsgClassificationPartitionedModel.NumObservations^Švc cClassificationPartitionedModel.ResponseNamegClassificationPartitionedModel.ResponseNamepŠuu uClassificationPartitionedModel.CategoricalPredictorsgClassificationPartitionedModel.CategoricalPredictorsbŠtg gClassificationPartitionedModel.PredictorNamesgClassificationPartitionedModel.PredictorNameslŠsq qClassificationPartitionedModel.CrossValidatedModelgClassificationPartitionedModel.CrossValidatedModelbŠrg gClassificationPartitionedModel.ScoreTransformgClassificationPartitionedModel.ScoreTransformPŠqU UClassificationPartitionedModel.PriorgClassificationPartitionedModel.PriorNŠpS SClassificationPartitionedModel.CostgClassificationPartitionedModel.CostZŠo_ _ClassificationPartitionedModel.ClassNamesgClassificationPartitionedModel.ClassNamesDŠnI IClassificationPartitionedModelfClassificationPartitionedModelZŠm_ _ClassificationPartitionedLinear.kfoldLosseClassificationPartitionedLinear.kfoldLoss 7«ÍtÚi Í t 7 ð µ ~ = þ Á z C þ ½ | M  ç ˜ W $׎Iî‘PŽM φ=ê©jÎ�J¸w2ážYü«N‹US SClassificationTree.SurrogateCutFlipmClassificationTree.SurrogateCutFlipZ‹T_ _ClassificationTree.SurrogateCutCategoriesmClassificationTree.SurrogateCutCategoriesB‹SG GClassificationTree.PruneAlphamClassificationTree.PruneAlpha@‹RE EClassificationTree.PruneListmClassificationTree.PruneListN‹QS SClassificationTree.ClassProbabilitymClassificationTree.ClassProbabilityB‹PG GClassificationTree.ClassCountmClassificationTree.ClassCount>‹OC CClassificationTree.NodeRiskmClassificationTree.NodeRiskL‹NQ QClassificationTree.NodeProbabilitymClassificationTree.NodeProbability@‹ME EClassificationTree.NodeErrormClassificationTree.NodeError@‹LE EClassificationTree.NodeClassmClassificationTree.NodeClass>‹KC CClassificationTree.NodeSizemClassificationTree.NodeSizeN‹JS SClassificationTree.CategoricalSplitmClassificationTree.CategoricalSplitH‹IM MClassificationTree.CutCategoriesmClassificationTree.CutCategories<‹HA AClassificationTree.CutTypemClassificationTree.CutType>‹GC CClassificationTree.CutPointmClassificationTree.CutPointP‹FU UClassificationTree.CutPredictorIndexmClassificationTree.CutPredictorIndexF‹EK KClassificationTree.CutPredictormClassificationTree.CutPredictorF‹DK KClassificationTree.IsBranchNodemClassificationTree.IsBranchNode:‹C? ?ClassificationTree.ParentmClassificationTree.Parent>‹BC CClassificationTree.ChildrenmClassificationTree.Children>‹AC CClassificationTree.NumNodesmClassificationTree.NumNodesp‹@u uClassificationTree.HyperparameterOptimizationResultsmClassificationTree.HyperparameterOptimizationResultsL‹?Q QClassificationTree.ModelParametersmClassificationTree.ModelParameters>‹>C CClassificationTree.BinEdgesmClassificationTree.BinEdgesZ‹=_ _ClassificationTree.ExpandedPredictorNamesmClassificationTree.ExpandedPredictorNamesX‹<] ]ClassificationTree.CategoricalPredictorsmClassificationTree.CategoricalPredictorsB‹;G GClassificationTree.ClassNamesmClassificationTree.ClassNamesF‹:K KClassificationTree.ResponseNamemClassificationTree.ResponseNameJ‹9O OClassificationTree.PredictorNamesmClassificationTree.PredictorNames0‹85 5ClassificationTree.WmClassificationTree.W>‹7C CClassificationTree.RowsUsedmClassificationTree.RowsUsedL‹6Q QClassificationTree.NumObservationsmClassificationTree.NumObservations0‹55 5ClassificationTree.YmClassificationTree.Y0‹45 5ClassificationTree.XmClassificationTree.X,‹31 1ClassificationTreelClassificationTree>‹2C CClassificationSVM.savemodelkClassificationSVM.savemodel>‹1C CClassificationSVM.resubEdgekClassificationSVM.resubEdgeB‹0G GClassificationSVM.resubMarginkClassificationSVM.resubMargin4‹/9 9ClassificationSVM.edgekClassificationSVM.edgeD‹.I IClassificationSVM.fitPosteriorkClassificationSVM.fitPosterior:‹-? ?ClassificationSVM.compactkClassificationSVM.compact<‹,A AClassificationSVM.crossvalkClassificationSVM.crossval>‹+C CClassificationSVM.resubLosskClassificationSVM.resubLoss4‹*9 9ClassificationSVM.losskClassificationSVM.loss8‹)= =ClassificationSVM.marginkClassificationSVM.marginD‹(I IClassificationSVM.resubPredictkClassificationSVM.resubPredict:‹'? ?ClassificationSVM.predictkClassificationSVM.predictV‹&[ [ClassificationSVM.discardSupportVectorskClassificationSVM.discardSupportVectorsN‹%S SClassificationSVM.ClassificationSVMkClassificationSVM.ClassificationSVMH‹$M MClassificationSVM.ScoreTransformjClassificationSVM.ScoreTransformn‹#s sClassificationSVM.HyperparameterOptimizationResultsjClassificationSVM.HyperparameterOptimizationResults<‹"A AClassificationSVM.BinEdgesjClassificationSVM.BinEdgesX‹!] ]ClassificationSVM.ExpandedPredictorNamesjClassificationSVM.ExpandedPredictorNamesV‹ [ [ClassificationSVM.CategoricalPredictorsjClassificationSVM.CategoricalPredictors0‹5 5ClassificationSVM.NujClassificationSVM.Nu /“­\–[" Õ € A  É r  æ § f ) ð · n ' ä ¡ ^ ¨=Ö]â{¿l¦Gâ}$Ët°Gê“TŒY YCompactClassificationDiscriminant.lossqCompactClassificationDiscriminant.lossZŒ_ _CompactClassificationDiscriminant.predictqCompactClassificationDiscriminant.predictfŒk kCompactClassificationDiscriminant.nLinearCoeffsqCompactClassificationDiscriminant.nLinearCoeffshŒm mCompactClassificationDiscriminant.ScoreTransformpCompactClassificationDiscriminant.ScoreTransformVŒ[ [CompactClassificationDiscriminant.PriorpCompactClassificationDiscriminant.PriorT‹Y YCompactClassificationDiscriminant.CostpCompactClassificationDiscriminant.CostV‹~[ [CompactClassificationDiscriminant.DeltapCompactClassificationDiscriminant.DeltaV‹}[ [CompactClassificationDiscriminant.GammapCompactClassificationDiscriminant.Gammab‹|g gCompactClassificationDiscriminant.DiscrimTypepCompactClassificationDiscriminant.DiscrimTypeb‹{g gCompactClassificationDiscriminant.LogDetSigmapCompactClassificationDiscriminant.LogDetSigma\‹za aCompactClassificationDiscriminant.MinGammapCompactClassificationDiscriminant.MinGammah‹ym mCompactClassificationDiscriminant.DeltaPredictorpCompactClassificationDiscriminant.DeltaPredictorX‹x] ]CompactClassificationDiscriminant.CoeffspCompactClassificationDiscriminant.CoeffsP‹wU UCompactClassificationDiscriminant.MupCompactClassificationDiscriminant.MuV‹v[ [CompactClassificationDiscriminant.SigmapCompactClassificationDiscriminant.Sigma`‹ue eCompactClassificationDiscriminant.ClassNamespCompactClassificationDiscriminant.ClassNamesd‹ti iCompactClassificationDiscriminant.ResponseNamepCompactClassificationDiscriminant.ResponseNamex‹s} }CompactClassificationDiscriminant.ExpandedPredictorNamespCompactClassificationDiscriminant.ExpandedPredictorNamesv‹r{ {CompactClassificationDiscriminant.CategoricalPredictorspCompactClassificationDiscriminant.CategoricalPredictorsd‹qi iCompactClassificationDiscriminant.BetweenSigmapCompactClassificationDiscriminant.BetweenSigmah‹pm mCompactClassificationDiscriminant.PredictorNamespCompactClassificationDiscriminant.PredictorNamesf‹ok kCompactClassificationDiscriminant.NumPredictorspCompactClassificationDiscriminant.NumPredictorsJ‹nO OCompactClassificationDiscriminantoCompactClassificationDiscriminant@‹mE EClassificationTree.savemodelnClassificationTree.savemodel@‹lE EClassificationTree.resubLossnClassificationTree.resubLoss@‹kE EClassificationTree.resubEdgenClassificationTree.resubEdgeD‹jI IClassificationTree.resubMarginnClassificationTree.resubMarginF‹iK KClassificationTree.resubPredictnClassificationTree.resubPredict6‹h; ;ClassificationTree.lossnClassificationTree.loss6‹g; ;ClassificationTree.edgenClassificationTree.edge:‹f? ?ClassificationTree.marginnClassificationTree.margin>‹eC CClassificationTree.crossvalnClassificationTree.crossval<‹dA AClassificationTree.compactnClassificationTree.compact6‹c; ;ClassificationTree.viewnClassificationTree.viewP‹bU UClassificationTree.nodeVariableRangenClassificationTree.nodeVariableRangeT‹aY YClassificationTree.predictorImportancenClassificationTree.predictorImportance:‹`? ?ClassificationTree.cvlossnClassificationTree.cvloss8‹_= =ClassificationTree.prunenClassificationTree.prune<‹^A AClassificationTree.predictnClassificationTree.predictR‹]W WClassificationTree.ClassificationTreenClassificationTree.ClassificationTreeJ‹\O OClassificationTree.ScoreTransformmClassificationTree.ScoreTransform6‹[; ;ClassificationTree.CostmClassificationTree.Cost8‹Z= =ClassificationTree.PriormClassificationTree.Priorh‹Ym mClassificationTree.SurrogatePredictorAssociationmClassificationTree.SurrogatePredictorAssociationX‹X] ]ClassificationTree.SurrogateCutPredictormClassificationTree.SurrogateCutPredictorN‹WS SClassificationTree.SurrogateCutTypemClassificationTree.SurrogateCutTypeP‹VU UClassificationTree.SurrogateCutPointmClassificationTree.SurrogateCutPoint -}¥Nõž= ¥ N ó   E Ú q  Ç ~ 7 Ü � &Ï„=ö±@å‚3Àe±Rýš1Îy*Ûˆ¸}8Œ1= =CompactClassificationGAMxCompactClassificationGAM`Œ0e eCompactClassificationEnsemble.removeLearnerswCompactClassificationEnsemble.removeLearnersjŒ/o oCompactClassificationEnsemble.predictorImportancewCompactClassificationEnsemble.predictorImportancePŒ.U UCompactClassificationEnsemble.marginwCompactClassificationEnsemble.marginLŒ-Q QCompactClassificationEnsemble.edgewCompactClassificationEnsemble.edgeLŒ,Q QCompactClassificationEnsemble.losswCompactClassificationEnsemble.lossRŒ+W WCompactClassificationEnsemble.predictwCompactClassificationEnsemble.predict`Œ*e eCompactClassificationEnsemble.ScoreTransformvCompactClassificationEnsemble.ScoreTransformfŒ)k kCompactClassificationEnsemble.UsePredForLearnervCompactClassificationEnsemble.UsePredForLearner`Œ(e eCompactClassificationEnsemble.TrainedWeightsvCompactClassificationEnsemble.TrainedWeightsRŒ'W WCompactClassificationEnsemble.TrainedvCompactClassificationEnsemble.Trained\Œ&a aCompactClassificationEnsemble.ResponseNamevCompactClassificationEnsemble.ResponseNameNŒ%S SCompactClassificationEnsemble.PriorvCompactClassificationEnsemble.Prior`Œ$e eCompactClassificationEnsemble.PredictorNamesvCompactClassificationEnsemble.PredictorNamesXŒ#] ]CompactClassificationEnsemble.NumTrainedvCompactClassificationEnsemble.NumTrainedpŒ"u uCompactClassificationEnsemble.ExpandedPredictorNamesvCompactClassificationEnsemble.ExpandedPredictorNamesLŒ!Q QCompactClassificationEnsemble.CostvCompactClassificationEnsemble.Cost`Œ e eCompactClassificationEnsemble.CombineWeightsvCompactClassificationEnsemble.CombineWeightsXŒ] ]CompactClassificationEnsemble.ClassNamesvCompactClassificationEnsemble.ClassNamesnŒs sCompactClassificationEnsemble.CategoricalPredictorsvCompactClassificationEnsemble.CategoricalPredictorsBŒG GCompactClassificationEnsembleuCompactClassificationEnsembleDŒI ICompactClassificationECOC.losstCompactClassificationECOC.lossDŒI ICompactClassificationECOC.edgetCompactClassificationECOC.edgeHŒM MCompactClassificationECOC.margintCompactClassificationECOC.marginTŒY YCompactClassificationECOC.selectModelstCompactClassificationECOC.selectModelsfŒk kCompactClassificationECOC.discardSupportVectorstCompactClassificationECOC.discardSupportVectorsJŒO OCompactClassificationECOC.predicttCompactClassificationECOC.predictXŒ] ]CompactClassificationECOC.ScoreTransformsCompactClassificationECOC.ScoreTransformDŒI ICompactClassificationECOC.CostsCompactClassificationECOC.CostFŒK KCompactClassificationECOC.PriorsCompactClassificationECOC.PriorPŒU UCompactClassificationECOC.BinaryLosssCompactClassificationECOC.BinaryLossTŒY YCompactClassificationECOC.ResponseNamesCompactClassificationECOC.ResponseNamefŒk kCompactClassificationECOC.CategoricalPredictorssCompactClassificationECOC.CategoricalPredictorshŒm mCompactClassificationECOC.ExpandedPredictorNamessCompactClassificationECOC.ExpandedPredictorNamesXŒ] ]CompactClassificationECOC.PredictorNamessCompactClassificationECOC.PredictorNamesPŒU UCompactClassificationECOC.ClassNamessCompactClassificationECOC.ClassNamesXŒ ] ]CompactClassificationECOC.LearnerWeightssCompactClassificationECOC.LearnerWeightsTŒ Y YCompactClassificationECOC.CodingMatrixsCompactClassificationECOC.CodingMatrixXŒ ] ]CompactClassificationECOC.BinaryLearnerssCompactClassificationECOC.BinaryLearners:Œ ? ?CompactClassificationECOCrCompactClassificationECOC^Œ c cCompactClassificationDiscriminant.savemodelqCompactClassificationDiscriminant.savemodelTŒY YCompactClassificationDiscriminant.logpqCompactClassificationDiscriminant.logpVŒ[ [CompactClassificationDiscriminant.mahalqCompactClassificationDiscriminant.mahalTŒY YCompactClassificationDiscriminant.edgeqCompactClassificationDiscriminant.edgeXŒ] ]CompactClassificationDiscriminant.marginqCompactClassificationDiscriminant.margin -�©Pûªc à t % Ö ‡ · j  ´ e Ç | 3î©Z¤U‰Ål¸c©BÍjóšCð�PŒ^U UCompactClassificationNaiveBayes.loss}CompactClassificationNaiveBayes.lossPŒ]U UCompactClassificationNaiveBayes.edge}CompactClassificationNaiveBayes.edgeTŒ\Y YCompactClassificationNaiveBayes.margin}CompactClassificationNaiveBayes.marginVŒ[[ [CompactClassificationNaiveBayes.predict}CompactClassificationNaiveBayes.predicttŒZy yCompactClassificationNaiveBayes.ExpandedPredictorNames|CompactClassificationNaiveBayes.ExpandedPredictorNames`ŒYe eCompactClassificationNaiveBayes.ResponseName|CompactClassificationNaiveBayes.ResponseNamerŒXw wCompactClassificationNaiveBayes.CategoricalPredictors|CompactClassificationNaiveBayes.CategoricalPredictorsdŒWi iCompactClassificationNaiveBayes.PredictorNames|CompactClassificationNaiveBayes.PredictorNamesdŒVi iCompactClassificationNaiveBayes.ScoreTransform|CompactClassificationNaiveBayes.ScoreTransformPŒUU UCompactClassificationNaiveBayes.Cost|CompactClassificationNaiveBayes.CostRŒTW WCompactClassificationNaiveBayes.Prior|CompactClassificationNaiveBayes.Prior\ŒSa aCompactClassificationNaiveBayes.ClassNames|CompactClassificationNaiveBayes.ClassNamesRŒRW WCompactClassificationNaiveBayes.Width|CompactClassificationNaiveBayes.WidthVŒQ[ [CompactClassificationNaiveBayes.Support|CompactClassificationNaiveBayes.SupportTŒPY YCompactClassificationNaiveBayes.Kernel|CompactClassificationNaiveBayes.KerneljŒOo oCompactClassificationNaiveBayes.CategoricalLevels|CompactClassificationNaiveBayes.CategoricalLevelstŒNy yCompactClassificationNaiveBayes.DistributionParameters|CompactClassificationNaiveBayes.DistributionParametersRŒMW WCompactClassificationNaiveBayes.Sigma|CompactClassificationNaiveBayes.SigmaLŒLQ QCompactClassificationNaiveBayes.Mu|CompactClassificationNaiveBayes.MujŒKo oCompactClassificationNaiveBayes.DistributionNames|CompactClassificationNaiveBayes.DistributionNamesFŒJK KCompactClassificationNaiveBayes{CompactClassificationNaiveBayesLŒIQ QCompactClassificationGAM.savemodelzCompactClassificationGAM.savemodelBŒHG GCompactClassificationGAM.losszCompactClassificationGAM.lossBŒGG GCompactClassificationGAM.edgezCompactClassificationGAM.edgeFŒFK KCompactClassificationGAM.marginzCompactClassificationGAM.marginHŒEM MCompactClassificationGAM.predictzCompactClassificationGAM.predictVŒD[ [CompactClassificationGAM.ScoreTransformyCompactClassificationGAM.ScoreTransformBŒCG GCompactClassificationGAM.CostyCompactClassificationGAM.CostLŒBQ QCompactClassificationGAM.TreeModelyCompactClassificationGAM.TreeModelLŒAQ QCompactClassificationGAM.FitMethodyCompactClassificationGAM.FitMethoddŒ@i iCompactClassificationGAM.PairDetectionBinEdgesyCompactClassificationGAM.PairDetectionBinEdgesJŒ?O OCompactClassificationGAM.BinEdgesyCompactClassificationGAM.BinEdgesfŒ>k kCompactClassificationGAM.ExpandedPredictorNamesyCompactClassificationGAM.ExpandedPredictorNamesdŒ=i iCompactClassificationGAM.CategoricalPredictorsyCompactClassificationGAM.CategoricalPredictorsLŒ<Q QCompactClassificationGAM.InterceptyCompactClassificationGAM.InterceptLŒ;Q QCompactClassificationGAM.IntMatrixyCompactClassificationGAM.IntMatrixLŒ:Q QCompactClassificationGAM.ModelwIntyCompactClassificationGAM.ModelwIntLŒ9Q QCompactClassificationGAM.BaseModelyCompactClassificationGAM.BaseModelRŒ8W WCompactClassificationGAM.InteractionsyCompactClassificationGAM.InteractionsHŒ7M MCompactClassificationGAM.FormulayCompactClassificationGAM.FormulaDŒ6I ICompactClassificationGAM.PrioryCompactClassificationGAM.PriorNŒ5S SCompactClassificationGAM.ClassNamesyCompactClassificationGAM.ClassNamesRŒ4W WCompactClassificationGAM.ResponseNameyCompactClassificationGAM.ResponseNameVŒ3[ [CompactClassificationGAM.PredictorNamesyCompactClassificationGAM.PredictorNamesTŒ2Y YCompactClassificationGAM.NumPredictorsyCompactClassificationGAM.NumPredictors +s­P–) À [ « F ß d û ” 9 ¾ Aè{½c ¥i·Yñ‡1ß—Q Ç9ó�5Ûse� i iCompactClassificationSVM.discardSupportVectorsƒCompactClassificationSVM.discardSupportVectorsW�[ [CompactClassificationSVM.ScoreTransform‚CompactClassificationSVM.ScoreTransformW�[ [CompactClassificationSVM.SupportVectors‚CompactClassificationSVM.SupportVectorsa�e eCompactClassificationSVM.SupportVectorLabels‚CompactClassificationSVM.SupportVectorLabelsC�G GCompactClassificationSVM.Bias‚CompactClassificationSVM.BiasC�G GCompactClassificationSVM.Beta‚CompactClassificationSVM.BetaE�I ICompactClassificationSVM.Alpha‚CompactClassificationSVM.Alpha?�C CCompactClassificationSVM.Mu‚CompactClassificationSVM.MuE�I ICompactClassificationSVM.Sigma‚CompactClassificationSVM.SigmaC�G GCompactClassificationSVM.Cost‚CompactClassificationSVM.CostEŒI ICompactClassificationSVM.Prior‚CompactClassificationSVM.PriorOŒ~S SCompactClassificationSVM.ClassNames‚CompactClassificationSVM.ClassNamesSŒ}W WCompactClassificationSVM.ResponseName‚CompactClassificationSVM.ResponseNamegŒ|k kCompactClassificationSVM.ExpandedPredictorNames‚CompactClassificationSVM.ExpandedPredictorNameseŒ{i iCompactClassificationSVM.CategoricalPredictors‚CompactClassificationSVM.CategoricalPredictors[Œz_ _CompactClassificationSVM.KernelParameters‚CompactClassificationSVM.KernelParametersWŒy[ [CompactClassificationSVM.PredictorNames‚CompactClassificationSVM.PredictorNamesUŒxY YCompactClassificationSVM.NumPredictors‚CompactClassificationSVM.NumPredictors9Œw= =CompactClassificationSVM�CompactClassificationSVMaŒve eCompactClassificationNeuralNetwork.savemodel€CompactClassificationNeuralNetwork.savemodelWŒu[ [CompactClassificationNeuralNetwork.loss€CompactClassificationNeuralNetwork.lossWŒt[ [CompactClassificationNeuralNetwork.edge€CompactClassificationNeuralNetwork.edge[Œs_ _CompactClassificationNeuralNetwork.margin€CompactClassificationNeuralNetwork.margin]Œra aCompactClassificationNeuralNetwork.predict€CompactClassificationNeuralNetwork.predictjŒqo oCompactClassificationNeuralNetwork.ScoreTransformCompactClassificationNeuralNetwork.ScoreTransformVŒp[ [CompactClassificationNeuralNetwork.CostCompactClassificationNeuralNetwork.CostzŒo CompactClassificationNeuralNetwork.ExpandedPredictorNamesCompactClassificationNeuralNetwork.ExpandedPredictorNamesxŒn} }CompactClassificationNeuralNetwork.CategoricalPredictorsCompactClassificationNeuralNetwork.CategoricalPredictorsXŒm] ]CompactClassificationNeuralNetwork.PriorCompactClassificationNeuralNetwork.PriordŒli iCompactClassificationNeuralNetwork.LayerBiasesCompactClassificationNeuralNetwork.LayerBiasesfŒkk kCompactClassificationNeuralNetwork.LayerWeightsCompactClassificationNeuralNetwork.LayerWeightsxŒj} }CompactClassificationNeuralNetwork.OutputLayerActivationCompactClassificationNeuralNetwork.OutputLayerActivationdŒii iCompactClassificationNeuralNetwork.ActivationsCompactClassificationNeuralNetwork.ActivationsbŒhg gCompactClassificationNeuralNetwork.LayerSizesCompactClassificationNeuralNetwork.LayerSizesRŒgW WCompactClassificationNeuralNetwork.MuCompactClassificationNeuralNetwork.MuXŒf] ]CompactClassificationNeuralNetwork.SigmaCompactClassificationNeuralNetwork.SigmabŒeg gCompactClassificationNeuralNetwork.ClassNamesCompactClassificationNeuralNetwork.ClassNamesfŒdk kCompactClassificationNeuralNetwork.ResponseNameCompactClassificationNeuralNetwork.ResponseNamejŒco oCompactClassificationNeuralNetwork.PredictorNamesCompactClassificationNeuralNetwork.PredictorNameshŒbm mCompactClassificationNeuralNetwork.NumPredictorsCompactClassificationNeuralNetwork.NumPredictorsLŒaQ QCompactClassificationNeuralNetwork~CompactClassificationNeuralNetworkZŒ`_ _CompactClassificationNaiveBayes.savemodel}CompactClassificationNaiveBayes.savemodelPŒ_U UCompactClassificationNaiveBayes.logp}CompactClassificationNaiveBayes.logp .x´j$Þˆ8 ú ª Z  ¶ ^ ü ¬ ^  ¤ T  ° R ®Nü¨<Üz°6Ú‚.ÄXÆj¶T ÀxE�7I ICompactClassificationTree.edge†CompactClassificationTree.edgeI�6M MCompactClassificationTree.margin†CompactClassificationTree.marginE�5I ICompactClassificationTree.view†CompactClassificationTree.view_�4c cCompactClassificationTree.nodeVariableRange†CompactClassificationTree.nodeVariableRangec�3g gCompactClassificationTree.predictorImportance†CompactClassificationTree.predictorImportanceK�2O OCompactClassificationTree.predict†CompactClassificationTree.predictY�1] ]CompactClassificationTree.ScoreTransform…CompactClassificationTree.ScoreTransformE�0I ICompactClassificationTree.Cost…CompactClassificationTree.CostG�/K KCompactClassificationTree.Prior…CompactClassificationTree.Priori�.m mCompactClassificationTree.ExpandedPredictorNames…CompactClassificationTree.ExpandedPredictorNamesg�-k kCompactClassificationTree.CategoricalPredictors…CompactClassificationTree.CategoricalPredictorsQ�,U UCompactClassificationTree.ClassNames…CompactClassificationTree.ClassNamesU�+Y YCompactClassificationTree.ResponseName…CompactClassificationTree.ResponseNameY�*] ]CompactClassificationTree.PredictorNames…CompactClassificationTree.PredictorNamesw�){ {CompactClassificationTree.SurrogatePredictorAssociation…CompactClassificationTree.SurrogatePredictorAssociationg�(k kCompactClassificationTree.SurrogateCutPredictor…CompactClassificationTree.SurrogateCutPredictor]�'a aCompactClassificationTree.SurrogateCutType…CompactClassificationTree.SurrogateCutType_�&c cCompactClassificationTree.SurrogateCutPoint…CompactClassificationTree.SurrogateCutPoint]�%a aCompactClassificationTree.SurrogateCutFlip…CompactClassificationTree.SurrogateCutFlipi�$m mCompactClassificationTree.SurrogateCutCategories…CompactClassificationTree.SurrogateCutCategoriesQ�#U UCompactClassificationTree.PruneAlpha…CompactClassificationTree.PruneAlphaO�"S SCompactClassificationTree.PruneList…CompactClassificationTree.PruneList]�!a aCompactClassificationTree.ClassProbability…CompactClassificationTree.ClassProbabilityQ� U UCompactClassificationTree.ClassCount…CompactClassificationTree.ClassCountM�Q QCompactClassificationTree.NodeRisk…CompactClassificationTree.NodeRisk[�_ _CompactClassificationTree.NodeProbability…CompactClassificationTree.NodeProbabilityO�S SCompactClassificationTree.NodeError…CompactClassificationTree.NodeErrorO�S SCompactClassificationTree.NodeClass…CompactClassificationTree.NodeClassM�Q QCompactClassificationTree.NodeSize…CompactClassificationTree.NodeSize]�a aCompactClassificationTree.CategoricalSplit…CompactClassificationTree.CategoricalSplitW�[ [CompactClassificationTree.CutCategories…CompactClassificationTree.CutCategoriesK�O OCompactClassificationTree.CutType…CompactClassificationTree.CutTypeM�Q QCompactClassificationTree.CutPoint…CompactClassificationTree.CutPoint_�c cCompactClassificationTree.CutPredictorIndex…CompactClassificationTree.CutPredictorIndexU�Y YCompactClassificationTree.CutPredictor…CompactClassificationTree.CutPredictorU�Y YCompactClassificationTree.IsBranchNode…CompactClassificationTree.IsBranchNodeI�M MCompactClassificationTree.Parent…CompactClassificationTree.ParentM�Q QCompactClassificationTree.Children…CompactClassificationTree.ChildrenM�Q QCompactClassificationTree.NumNodes…CompactClassificationTree.NumNodes;�? ?CompactClassificationTree„CompactClassificationTreeM�Q QCompactClassificationSVM.savemodelƒCompactClassificationSVM.savemodelS�W WCompactClassificationSVM.fitPosteriorƒCompactClassificationSVM.fitPosteriorC� G GCompactClassificationSVM.edgeƒCompactClassificationSVM.edgeC� G GCompactClassificationSVM.lossƒCompactClassificationSVM.lossG� K KCompactClassificationSVM.marginƒCompactClassificationSVM.marginI� M MCompactClassificationSVM.predictƒCompactClassificationSVM.predict 1–¸f2â�B â € 8 ô ¦ D ü ´ l & Æ ~ 6 Þ š \ Öz¸Lê–Hì�.à˜2Ö¤Tô¨J¶z<ì–S�hW WCompactRegressionGP.KernelInformationŽCompactRegressionGP.KernelInformationM�gQ QCompactRegressionGP.KernelFunctionŽCompactRegressionGP.KernelFunction;�f? ?CompactRegressionGP.SigmaŽCompactRegressionGP.Sigma9�e= =CompactRegressionGP.BetaŽCompactRegressionGP.BetaK�dO OCompactRegressionGP.BasisFunctionŽCompactRegressionGP.BasisFunctionC�cG GCompactRegressionGP.FitMethodŽCompactRegressionGP.FitMethod[�b_ _CompactRegressionGP.CategoricalPredictorsŽCompactRegressionGP.CategoricalPredictorsI�aM MCompactRegressionGP.ResponseNameŽCompactRegressionGP.ResponseName]�`a aCompactRegressionGP.ExpandedPredictorNamesŽCompactRegressionGP.ExpandedPredictorNamesM�_Q QCompactRegressionGP.PredictorNamesŽCompactRegressionGP.PredictorNames/�^3 3CompactRegressionGP�CompactRegressionGPY�]] ]CompactRegressionEnsemble.removeLearnersŒCompactRegressionEnsemble.removeLearnersc�\g gCompactRegressionEnsemble.predictorImportanceŒCompactRegressionEnsemble.predictorImportanceE�[I ICompactRegressionEnsemble.lossŒCompactRegressionEnsemble.lossK�ZO OCompactRegressionEnsemble.predictŒCompactRegressionEnsemble.predict_�Yc cCompactRegressionEnsemble.ResponseTransform‹CompactRegressionEnsemble.ResponseTransformY�X] ]CompactRegressionEnsemble.CombineWeights‹CompactRegressionEnsemble.CombineWeightsY�W] ]CompactRegressionEnsemble.TrainedWeights‹CompactRegressionEnsemble.TrainedWeightsK�VO OCompactRegressionEnsemble.Trained‹CompactRegressionEnsemble.TrainedQ�UU UCompactRegressionEnsemble.NumTrained‹CompactRegressionEnsemble.NumTrained_�Tc cCompactRegressionEnsemble.UsePredForLearner‹CompactRegressionEnsemble.UsePredForLearneri�Sm mCompactRegressionEnsemble.ExpandedPredictorNames‹CompactRegressionEnsemble.ExpandedPredictorNamesU�RY YCompactRegressionEnsemble.ResponseName‹CompactRegressionEnsemble.ResponseNameg�Qk kCompactRegressionEnsemble.CategoricalPredictors‹CompactRegressionEnsemble.CategoricalPredictorsY�P] ]CompactRegressionEnsemble.PredictorNames‹CompactRegressionEnsemble.PredictorNames;�O? ?CompactRegressionEnsembleŠCompactRegressionEnsembleE�NI ICompactRegressionGAM.savemodel‰CompactRegressionGAM.savemodel;�M? ?CompactRegressionGAM.loss‰CompactRegressionGAM.lossA�LE ECompactRegressionGAM.predict‰CompactRegressionGAM.predictU�KY YCompactRegressionGAM.ResponseTransformˆCompactRegressionGAM.ResponseTransformE�JI ICompactRegressionGAM.TreeModelˆCompactRegressionGAM.TreeModelE�II ICompactRegressionGAM.FitMethodˆCompactRegressionGAM.FitMethod]�Ha aCompactRegressionGAM.PairDetectionBinEdgesˆCompactRegressionGAM.PairDetectionBinEdgesC�GG GCompactRegressionGAM.BinEdgesˆCompactRegressionGAM.BinEdgesE�FI ICompactRegressionGAM.IntMatrixˆCompactRegressionGAM.IntMatrixE�EI ICompactRegressionGAM.ModelwIntˆCompactRegressionGAM.ModelwIntE�DI ICompactRegressionGAM.BaseModelˆCompactRegressionGAM.BaseModel_�Cc cCompactRegressionGAM.IsStandardDeviationFitˆCompactRegressionGAM.IsStandardDeviationFitK�BO OCompactRegressionGAM.InteractionsˆCompactRegressionGAM.InteractionsA�AE ECompactRegressionGAM.FormulaˆCompactRegressionGAM.FormulaE�@I ICompactRegressionGAM.InterceptˆCompactRegressionGAM.Intercept_�?c cCompactRegressionGAM.ExpandedPredictorNamesˆCompactRegressionGAM.ExpandedPredictorNames]�>a aCompactRegressionGAM.CategoricalPredictorsˆCompactRegressionGAM.CategoricalPredictorsK�=O OCompactRegressionGAM.ResponseNameˆCompactRegressionGAM.ResponseNameO�<S SCompactRegressionGAM.PredictorNamesˆCompactRegressionGAM.PredictorNamesM�;Q QCompactRegressionGAM.NumPredictorsˆCompactRegressionGAM.NumPredictors1�:5 5CompactRegressionGAM‡CompactRegressionGAMO�9S SCompactClassificationTree.savemodel†CompactClassificationTree.savemodelE�8I ICompactClassificationTree.loss†CompactClassificationTree.loss 0š²t ΀* Ú „ B  À x  ® L ø ª L ì x ¶BÌ`¶Z&Ö„6ò²x8ú¼j´RúšVК3Ž7 7CompactRegressionTree–CompactRegressionTreeEŽI ICompactRegressionSVM.savemodel•CompactRegressionSVM.savemodel;Ž? ?CompactRegressionSVM.loss•CompactRegressionSVM.lossAŽE ECompactRegressionSVM.predict•CompactRegressionSVM.predict]Ža aCompactRegressionSVM.discardSupportVectors•CompactRegressionSVM.discardSupportVectorsUŽY YCompactRegressionSVM.ResponseTransform”CompactRegressionSVM.ResponseTransform_Žc cCompactRegressionSVM.ExpandedPredictorNames”CompactRegressionSVM.ExpandedPredictorNames]Ža aCompactRegressionSVM.CategoricalPredictors”CompactRegressionSVM.CategoricalPredictorsSŽW WCompactRegressionSVM.KernelParameters”CompactRegressionSVM.KernelParametersOŽS SCompactRegressionSVM.SupportVectors”CompactRegressionSVM.SupportVectors;Ž? ?CompactRegressionSVM.Bias”CompactRegressionSVM.Bias;Ž ? ?CompactRegressionSVM.Beta”CompactRegressionSVM.Beta=Ž A ACompactRegressionSVM.Alpha”CompactRegressionSVM.Alpha7Ž ; ;CompactRegressionSVM.Mu”CompactRegressionSVM.Mu=Ž A ACompactRegressionSVM.Sigma”CompactRegressionSVM.SigmaAŽ E ECompactRegressionSVM.Epsilon”CompactRegressionSVM.EpsilonKŽO OCompactRegressionSVM.ResponseName”CompactRegressionSVM.ResponseNameOŽS SCompactRegressionSVM.PredictorNames”CompactRegressionSVM.PredictorNamesMŽQ QCompactRegressionSVM.NumPredictors”CompactRegressionSVM.NumPredictors1Ž5 5CompactRegressionSVM“CompactRegressionSVMYŽ] ]CompactRegressionNeuralNetwork.savemodel’CompactRegressionNeuralNetwork.savemodelOŽS SCompactRegressionNeuralNetwork.loss’CompactRegressionNeuralNetwork.lossUŽY YCompactRegressionNeuralNetwork.predict’CompactRegressionNeuralNetwork.predictiŽm mCompactRegressionNeuralNetwork.ResponseTransform‘CompactRegressionNeuralNetwork.ResponseTransformsŽw wCompactRegressionNeuralNetwork.ExpandedPredictorNames‘CompactRegressionNeuralNetwork.ExpandedPredictorNamesq�u uCompactRegressionNeuralNetwork.CategoricalPredictors‘CompactRegressionNeuralNetwork.CategoricalPredictors]�~a aCompactRegressionNeuralNetwork.LayerBiases‘CompactRegressionNeuralNetwork.LayerBiases_�}c cCompactRegressionNeuralNetwork.LayerWeights‘CompactRegressionNeuralNetwork.LayerWeightsq�|u uCompactRegressionNeuralNetwork.OutputLayerActivation‘CompactRegressionNeuralNetwork.OutputLayerActivation]�{a aCompactRegressionNeuralNetwork.Activations‘CompactRegressionNeuralNetwork.Activations[�z_ _CompactRegressionNeuralNetwork.LayerSizes‘CompactRegressionNeuralNetwork.LayerSizesK�yO OCompactRegressionNeuralNetwork.Mu‘CompactRegressionNeuralNetwork.MuQ�xU UCompactRegressionNeuralNetwork.Sigma‘CompactRegressionNeuralNetwork.Sigma_�wc cCompactRegressionNeuralNetwork.ResponseName‘CompactRegressionNeuralNetwork.ResponseNamec�vg gCompactRegressionNeuralNetwork.PredictorNames‘CompactRegressionNeuralNetwork.PredictorNamesa�ue eCompactRegressionNeuralNetwork.NumPredictors‘CompactRegressionNeuralNetwork.NumPredictorsE�tI ICompactRegressionNeuralNetwork�CompactRegressionNeuralNetworkC�sG GCompactRegressionGP.savemodel�CompactRegressionGP.savemodel9�r= =CompactRegressionGP.loss�CompactRegressionGP.loss?�qC CCompactRegressionGP.predict�CompactRegressionGP.predictS�pW WCompactRegressionGP.ResponseTransformŽCompactRegressionGP.ResponseTransformM�oQ QCompactRegressionGP.PredictorScaleŽCompactRegressionGP.PredictorScaleS�nW WCompactRegressionGP.PredictorLocationŽCompactRegressionGP.PredictorLocationK�mO OCompactRegressionGP.ActiveSetSizeŽCompactRegressionGP.ActiveSetSizeO�lS SCompactRegressionGP.ActiveSetMethodŽCompactRegressionGP.ActiveSetMethodQ�kU UCompactRegressionGP.ActiveSetVectorsŽCompactRegressionGP.ActiveSetVectors;�j? ?CompactRegressionGP.AlphaŽCompactRegressionGP.AlphaK�iO OCompactRegressionGP.PredictMethodŽCompactRegressionGP.PredictMethod 3x¸p,ÜŒ2 ê ¤ R ú ² j Ê ‚ 8 ì ˆ 0 Ö ~ ªV¤@æ Bè¨hð´t:ö°d ¤Rú¼‚FÄxIŽKM MCompactTreeBagger.setDefaultYfit›CompactTreeBagger.setDefaultYfit;ŽJ? ?CompactTreeBagger.combine›CompactTreeBagger.combineAŽIE ECompactTreeBagger.meanMargin›CompactTreeBagger.meanMargin9ŽH= =CompactTreeBagger.margin›CompactTreeBagger.margin7ŽG; ;CompactTreeBagger.error›CompactTreeBagger.error;ŽF? ?CompactTreeBagger.predict›CompactTreeBagger.predictUŽEY YCompactTreeBagger.SurrogateAssociationšCompactTreeBagger.SurrogateAssociationOŽDS SCompactTreeBagger.NumPredictorSplitšCompactTreeBagger.NumPredictorSplitcŽCg gCompactTreeBagger.DeltaCriterionDecisionSplitšCompactTreeBagger.DeltaCriterionDecisionSplitWŽB[ [CompactTreeBagger.CategoricalPredictorsšCompactTreeBagger.CategoricalPredictorsIŽAM MCompactTreeBagger.PredictorNamesšCompactTreeBagger.PredictorNamesCŽ@G GCompactTreeBagger.DefaultYfitšCompactTreeBagger.DefaultYfitAŽ?E ECompactTreeBagger.ClassNamesšCompactTreeBagger.ClassNames7Ž>; ;CompactTreeBagger.TreesšCompactTreeBagger.Trees=Ž=A ACompactTreeBagger.NumTreesšCompactTreeBagger.NumTrees9Ž<= =CompactTreeBagger.MethodšCompactTreeBagger.Method+Ž;/ /CompactTreeBagger™CompactTreeBaggerGŽ:K KCompactRegressionTree.savemodel˜CompactRegressionTree.savemodel=Ž9A ACompactRegressionTree.loss˜CompactRegressionTree.loss=Ž8A ACompactRegressionTree.view˜CompactRegressionTree.viewWŽ7[ [CompactRegressionTree.nodeVariableRange˜CompactRegressionTree.nodeVariableRange[Ž6_ _CompactRegressionTree.predictorImportance˜CompactRegressionTree.predictorImportanceCŽ5G GCompactRegressionTree.predict˜CompactRegressionTree.predictWŽ4[ [CompactRegressionTree.ResponseTransform—CompactRegressionTree.ResponseTransformaŽ3e eCompactRegressionTree.ExpandedPredictorNames—CompactRegressionTree.ExpandedPredictorNames_Ž2c cCompactRegressionTree.CategoricalPredictors—CompactRegressionTree.CategoricalPredictorsMŽ1Q QCompactRegressionTree.ResponseName—CompactRegressionTree.ResponseNameQŽ0U UCompactRegressionTree.PredictorNames—CompactRegressionTree.PredictorNamesoŽ/s sCompactRegressionTree.SurrogatePredictorAssociation—CompactRegressionTree.SurrogatePredictorAssociation_Ž.c cCompactRegressionTree.SurrogateCutPredictor—CompactRegressionTree.SurrogateCutPredictorUŽ-Y YCompactRegressionTree.SurrogateCutType—CompactRegressionTree.SurrogateCutTypeWŽ,[ [CompactRegressionTree.SurrogateCutPoint—CompactRegressionTree.SurrogateCutPointUŽ+Y YCompactRegressionTree.SurrogateCutFlip—CompactRegressionTree.SurrogateCutFlipaŽ*e eCompactRegressionTree.SurrogateCutCategories—CompactRegressionTree.SurrogateCutCategoriesIŽ)M MCompactRegressionTree.PruneAlpha—CompactRegressionTree.PruneAlphaGŽ(K KCompactRegressionTree.PruneList—CompactRegressionTree.PruneListEŽ'I ICompactRegressionTree.NodeRisk—CompactRegressionTree.NodeRiskSŽ&W WCompactRegressionTree.NodeProbability—CompactRegressionTree.NodeProbabilityGŽ%K KCompactRegressionTree.NodeError—CompactRegressionTree.NodeErrorEŽ$I ICompactRegressionTree.NodeMean—CompactRegressionTree.NodeMeanEŽ#I ICompactRegressionTree.NodeSize—CompactRegressionTree.NodeSizeUŽ"Y YCompactRegressionTree.CategoricalSplit—CompactRegressionTree.CategoricalSplitOŽ!S SCompactRegressionTree.CutCategories—CompactRegressionTree.CutCategoriesCŽ G GCompactRegressionTree.CutType—CompactRegressionTree.CutTypeEŽI ICompactRegressionTree.CutPoint—CompactRegressionTree.CutPointWŽ[ [CompactRegressionTree.CutPredictorIndex—CompactRegressionTree.CutPredictorIndexMŽQ QCompactRegressionTree.CutPredictor—CompactRegressionTree.CutPredictorMŽQ QCompactRegressionTree.IsBranchNode—CompactRegressionTree.IsBranchNodeAŽE ECompactRegressionTree.Parent—CompactRegressionTree.ParentEŽI ICompactRegressionTree.Children—CompactRegressionTree.ChildrenEŽI ICompactRegressionTree.NumNodes—CompactRegressionTree.NumNodes /ؾr4 ¼€0 ä † , ì ¬ ^   f  – < Ô ~ ÊtÂ`®b®Nà’@ö¨\Æ€*Úv$ØIŽzM MRegressionBaggedEnsemble.oobLoss RegressionBaggedEnsemble.oobLossOŽyS SRegressionBaggedEnsemble.oobPredict RegressionBaggedEnsemble.oobPredictaŽxe eRegressionBaggedEnsemble.predictorImportance RegressionBaggedEnsemble.predictorImportanceMŽwQ QRegressionBaggedEnsemble.resubLoss RegressionBaggedEnsemble.resubLossSŽvW WRegressionBaggedEnsemble.resubPredict RegressionBaggedEnsemble.resubPredictCŽuG GRegressionBaggedEnsemble.loss RegressionBaggedEnsemble.lossIŽtM MRegressionBaggedEnsemble.predict RegressionBaggedEnsemble.predictGŽsK KRegressionBaggedEnsemble.resume RegressionBaggedEnsemble.resumeIŽrM MRegressionBaggedEnsemble.compact RegressionBaggedEnsemble.compactKŽqO ORegressionBaggedEnsemble.cvshrink RegressionBaggedEnsemble.cvshrinkGŽpK KRegressionBaggedEnsemble.shrink RegressionBaggedEnsemble.shrinkOŽoS SRegressionBaggedEnsemble.regularize RegressionBaggedEnsemble.regularizeKŽnO ORegressionBaggedEnsemble.crossval RegressionBaggedEnsemble.crossvalkŽmo oRegressionBaggedEnsemble.RegressionBaggedEnsemble RegressionBaggedEnsemble.RegressionBaggedEnsemble]Žla aRegressionBaggedEnsemble.ResponseTransformŸRegressionBaggedEnsemble.ResponseTransformWŽk[ [RegressionBaggedEnsemble.CombineWeightsŸRegressionBaggedEnsemble.CombineWeightsWŽj[ [RegressionBaggedEnsemble.TrainedWeightsŸRegressionBaggedEnsemble.TrainedWeightsIŽiM MRegressionBaggedEnsemble.TrainedŸRegressionBaggedEnsemble.TrainedOŽhS SRegressionBaggedEnsemble.NumTrainedŸRegressionBaggedEnsemble.NumTrained]Žga aRegressionBaggedEnsemble.UsePredForLearnerŸRegressionBaggedEnsemble.UsePredForLearner_Žfc cRegressionBaggedEnsemble.FitInfoDescriptionŸRegressionBaggedEnsemble.FitInfoDescriptionIŽeM MRegressionBaggedEnsemble.FitInfoŸRegressionBaggedEnsemble.FitInfocŽdg gRegressionBaggedEnsemble.ReasonForTerminationŸRegressionBaggedEnsemble.ReasonForTerminationSŽcW WRegressionBaggedEnsemble.LearnerNamesŸRegressionBaggedEnsemble.LearnerNamesGŽbK KRegressionBaggedEnsemble.MethodŸRegressionBaggedEnsemble.MethodgŽak kRegressionBaggedEnsemble.ExpandedPredictorNamesŸRegressionBaggedEnsemble.ExpandedPredictorNamesSŽ`W WRegressionBaggedEnsemble.ResponseNameŸRegressionBaggedEnsemble.ResponseNameeŽ_i iRegressionBaggedEnsemble.CategoricalPredictorsŸRegressionBaggedEnsemble.CategoricalPredictorsWŽ^[ [RegressionBaggedEnsemble.PredictorNamesŸRegressionBaggedEnsemble.PredictorNamesŽ] � �RegressionBaggedEnsemble.HyperparameterOptimizationResultsŸRegressionBaggedEnsemble.HyperparameterOptimizationResultsKŽ\O ORegressionBaggedEnsemble.BinEdgesŸRegressionBaggedEnsemble.BinEdgesYŽ[] ]RegressionBaggedEnsemble.NumObservationsŸRegressionBaggedEnsemble.NumObservationsYŽZ] ]RegressionBaggedEnsemble.ModelParametersŸRegressionBaggedEnsemble.ModelParameters=ŽYA ARegressionBaggedEnsemble.WŸRegressionBaggedEnsemble.WKŽXO ORegressionBaggedEnsemble.RowsUsedŸRegressionBaggedEnsemble.RowsUsed=ŽWA ARegressionBaggedEnsemble.XŸRegressionBaggedEnsemble.X=ŽVA ARegressionBaggedEnsemble.YŸRegressionBaggedEnsemble.YWŽU[ [RegressionBaggedEnsemble.RegularizationŸRegressionBaggedEnsemble.Regularization[ŽT_ _RegressionBaggedEnsemble.UseObsForLearnerŸRegressionBaggedEnsemble.UseObsForLearnerIŽSM MRegressionBaggedEnsemble.ReplaceŸRegressionBaggedEnsemble.ReplaceMŽRQ QRegressionBaggedEnsemble.FResampleŸRegressionBaggedEnsemble.FResample9ŽQ= =RegressionBaggedEnsembležRegressionBaggedEnsembleKŽPO OPredictiveModel.partialDependence�PredictiveModel.partialDependence'ŽO+ +PredictiveModelœPredictiveModel;ŽN? ?CompactTreeBagger.mdsprox›CompactTreeBagger.mdsproxIŽMM MCompactTreeBagger.outlierMeasure›CompactTreeBagger.outlierMeasure?ŽLC CCompactTreeBagger.proximity›CompactTreeBagger.proximity 9²†VÔ ^* Ú Š H Ô † * à ‚ D ú   ` ¶ p 0â”@ê¨h*ê°XÊ„FÞ´ŠD ʆFô v<Æ”b4²Q�3U URegressionGAM.IsStandardDeviationFit¥RegressionGAM.IsStandardDeviationFit+�2/ /RegressionGAM.Tol¥RegressionGAM.Tol+�1/ /RegressionGAM.DoF¥RegressionGAM.DoF/�03 3RegressionGAM.Order¥RegressionGAM.Order/�/3 3RegressionGAM.Knots¥RegressionGAM.Knots=�.A ARegressionGAM.Interactions¥RegressionGAM.Interactions3�-7 7RegressionGAM.Formula¥RegressionGAM.Formula7�,; ;RegressionGAM.Intercept¥RegressionGAM.Intercept'�++ +RegressionGAM.W¥RegressionGAM.WQ�*U URegressionGAM.ExpandedPredictorNames¥RegressionGAM.ExpandedPredictorNamesO�)S SRegressionGAM.CategoricalPredictors¥RegressionGAM.CategoricalPredictors=�(A ARegressionGAM.ResponseName¥RegressionGAM.ResponseNameA�'E ERegressionGAM.PredictorNames¥RegressionGAM.PredictorNames?�&C CRegressionGAM.NumPredictors¥RegressionGAM.NumPredictors5�%9 9RegressionGAM.RowsUsed¥RegressionGAM.RowsUsedC�$G GRegressionGAM.NumObservations¥RegressionGAM.NumObservations'�#+ +RegressionGAM.Y¥RegressionGAM.Y'�"+ +RegressionGAM.X¥RegressionGAM.X#�!' 'RegressionGAM¤RegressionGAM?� C CRegressionEnsemble.cvshrink£RegressionEnsemble.cvshrink;�? ?RegressionEnsemble.shrink£RegressionEnsemble.shrinkC�G GRegressionEnsemble.regularize£RegressionEnsemble.regularizeA�E ERegressionEnsemble.resubLoss£RegressionEnsemble.resubLossG�K KRegressionEnsemble.resubPredict£RegressionEnsemble.resubPredictU�Y YRegressionEnsemble.predictorImportance£RegressionEnsemble.predictorImportance7�; ;RegressionEnsemble.loss£RegressionEnsemble.loss=�A ARegressionEnsemble.predict£RegressionEnsemble.predict;�? ?RegressionEnsemble.resume£RegressionEnsemble.resume=�A ARegressionEnsemble.compact£RegressionEnsemble.compact?�C CRegressionEnsemble.crossval£RegressionEnsemble.crossvalS�W WRegressionEnsemble.RegressionEnsemble£RegressionEnsemble.RegressionEnsembleQ�U URegressionEnsemble.ResponseTransform¢RegressionEnsemble.ResponseTransformK�O ORegressionEnsemble.CombineWeights¢RegressionEnsemble.CombineWeightsK�O ORegressionEnsemble.TrainedWeights¢RegressionEnsemble.TrainedWeights=�A ARegressionEnsemble.Trained¢RegressionEnsemble.TrainedC�G GRegressionEnsemble.NumTrained¢RegressionEnsemble.NumTrainedQ�U URegressionEnsemble.UsePredForLearner¢RegressionEnsemble.UsePredForLearnerS�W WRegressionEnsemble.FitInfoDescription¢RegressionEnsemble.FitInfoDescription=� A ARegressionEnsemble.FitInfo¢RegressionEnsemble.FitInfoW� [ [RegressionEnsemble.ReasonForTermination¢RegressionEnsemble.ReasonForTerminationG� K KRegressionEnsemble.LearnerNames¢RegressionEnsemble.LearnerNames;� ? ?RegressionEnsemble.Method¢RegressionEnsemble.Method[� _ _RegressionEnsemble.ExpandedPredictorNames¢RegressionEnsemble.ExpandedPredictorNamesG�K KRegressionEnsemble.ResponseName¢RegressionEnsemble.ResponseNameY�] ]RegressionEnsemble.CategoricalPredictors¢RegressionEnsemble.CategoricalPredictorsK�O ORegressionEnsemble.PredictorNames¢RegressionEnsemble.PredictorNamesq�u uRegressionEnsemble.HyperparameterOptimizationResults¢RegressionEnsemble.HyperparameterOptimizationResults?�C CRegressionEnsemble.BinEdges¢RegressionEnsemble.BinEdgesM�Q QRegressionEnsemble.NumObservations¢RegressionEnsemble.NumObservationsM�Q QRegressionEnsemble.ModelParameters¢RegressionEnsemble.ModelParameters1�5 5RegressionEnsemble.W¢RegressionEnsemble.W?�C CRegressionEnsemble.RowsUsed¢RegressionEnsemble.RowsUsed1Ž5 5RegressionEnsemble.X¢RegressionEnsemble.X1Ž~5 5RegressionEnsemble.Y¢RegressionEnsemble.YKŽ}O ORegressionEnsemble.Regularization¢RegressionEnsemble.Regularization-Ž|1 1RegressionEnsemble¡RegressionEnsemblewŽ{{ {RegressionBaggedEnsemble.oobPermutedPredictorImportance RegressionBaggedEnsemble.oobPermutedPredictorImportance ?ÈÆŒRÈ‚2 ø ¾ T È ‚ L  Ü ¢ j 4 ú Æ ¢ z R  Ø ° n ÞŽX ಂBþ¼t4¾z:ò¬j"àx0ò¾€RÒœh0È9�r= =RegressionKernel.Epsilon«RegressionKernel.Epsilon)�q- -RegressionKernelªRegressionKernel5�p9 9RegressionGP.savemodel©RegressionGP.savemodel1�o5 5RegressionGP.compact©RegressionGP.compact3�n7 7RegressionGP.crossval©RegressionGP.crossvalE�mI IRegressionGP.postFitStatistics©RegressionGP.postFitStatistics5�l9 9RegressionGP.resubLoss©RegressionGP.resubLoss+�k/ /RegressionGP.loss©RegressionGP.loss;�j? ?RegressionGP.resubPredict©RegressionGP.resubPredict1�i5 5RegressionGP.predict©RegressionGP.predict;�h? ?RegressionGP.RegressionGP©RegressionGP.RegressionGPE�gI IRegressionGP.ResponseTransform¨RegressionGP.ResponseTransforme�fi iRegressionGP.HyperparameterOptimizationResults¨RegressionGP.HyperparameterOptimizationResults?�eC CRegressionGP.PredictorScale¨RegressionGP.PredictorScaleE�dI IRegressionGP.PredictorLocation¨RegressionGP.PredictorLocation?�cC CRegressionGP.BCDInformation¨RegressionGP.BCDInformationC�bG GRegressionGP.ActiveSetHistory¨RegressionGP.ActiveSetHistoryE�aI IRegressionGP.IsActiveSetVector¨RegressionGP.IsActiveSetVector=�`A ARegressionGP.ActiveSetSize¨RegressionGP.ActiveSetSizeA�_E ERegressionGP.ActiveSetMethod¨RegressionGP.ActiveSetMethodC�^G GRegressionGP.ActiveSetVectors¨RegressionGP.ActiveSetVectors-�]1 1RegressionGP.Alpha¨RegressionGP.Alpha=�\A ARegressionGP.PredictMethod¨RegressionGP.PredictMethodE�[I IRegressionGP.KernelInformation¨RegressionGP.KernelInformation?�ZC CRegressionGP.KernelFunction¨RegressionGP.KernelFunctionA�YE ERegressionGP.ModelParameters¨RegressionGP.ModelParameters=�XA ARegressionGP.LogLikelihood¨RegressionGP.LogLikelihood-�W1 1RegressionGP.Sigma¨RegressionGP.Sigma+�V/ /RegressionGP.Beta¨RegressionGP.Beta=�UA ARegressionGP.BasisFunction¨RegressionGP.BasisFunction5�T9 9RegressionGP.FitMethod¨RegressionGP.FitMethod3�S7 7RegressionGP.BinEdges¨RegressionGP.BinEdgesM�RQ QRegressionGP.CategoricalPredictors¨RegressionGP.CategoricalPredictors;�Q? ?RegressionGP.ResponseName¨RegressionGP.ResponseNameO�PS SRegressionGP.ExpandedPredictorNames¨RegressionGP.ExpandedPredictorNames?�OC CRegressionGP.PredictorNames¨RegressionGP.PredictorNames%�N) )RegressionGP.W¨RegressionGP.W3�M7 7RegressionGP.RowsUsed¨RegressionGP.RowsUsedA�LE ERegressionGP.NumObservations¨RegressionGP.NumObservations%�K) )RegressionGP.Y¨RegressionGP.Y%�J) )RegressionGP.X¨RegressionGP.X!�I% %RegressionGP§RegressionGP1�H5 5RegressionGAM.resume¦RegressionGAM.resume7�G; ;RegressionGAM.savemodel¦RegressionGAM.savemodel3�F7 7RegressionGAM.compact¦RegressionGAM.compact5�E9 9RegressionGAM.crossval¦RegressionGAM.crossval7�D; ;RegressionGAM.resubLoss¦RegressionGAM.resubLoss=�CA ARegressionGAM.resubPredict¦RegressionGAM.resubPredict-�B1 1RegressionGAM.loss¦RegressionGAM.loss3�A7 7RegressionGAM.predict¦RegressionGAM.predictC�@G GRegressionGAM.addInteractions¦RegressionGAM.addInteractions?�?C CRegressionGAM.RegressionGAM¦RegressionGAM.RegressionGAMG�>K KRegressionGAM.ResponseTransform¥RegressionGAM.ResponseTransformg�=k kRegressionGAM.HyperparameterOptimizationResults¥RegressionGAM.HyperparameterOptimizationResults7�<; ;RegressionGAM.TreeModel¥RegressionGAM.TreeModel7�;; ;RegressionGAM.FitMethod¥RegressionGAM.FitMethodM�:Q QRegressionGAM.ReasonForTermination¥RegressionGAM.ReasonForTerminationC�9G GRegressionGAM.ModelParameters¥RegressionGAM.ModelParametersO�8S SRegressionGAM.PairDetectionBinEdges¥RegressionGAM.PairDetectionBinEdges5�79 9RegressionGAM.BinEdges¥RegressionGAM.BinEdges7�6; ;RegressionGAM.IntMatrix¥RegressionGAM.IntMatrix7�5; ;RegressionGAM.ModelwInt¥RegressionGAM.ModelwInt7�4; ;RegressionGAM.BaseModel¥RegressionGAM.BaseModel 7ª¸hÆ€& Ì Š P  º v :  Ð ‚ F  Ö – j . Þ ” <öœ`*ô²x,â”X"Üœb$æŒ@ê’>ø¸h°\ªW�)[ [RegressionNeuralNetwork.ModelParameters±RegressionNeuralNetwork.ModelParametersU�(Y YRegressionNeuralNetwork.IterationLimit±RegressionNeuralNetwork.IterationLimitQ�'U URegressionNeuralNetwork.LearningRate±RegressionNeuralNetwork.LearningRatec�&g gRegressionNeuralNetwork.OutputLayerActivation±RegressionNeuralNetwork.OutputLayerActivationO�%S SRegressionNeuralNetwork.Activations±RegressionNeuralNetwork.ActivationsM�$Q QRegressionNeuralNetwork.LayerSizes±RegressionNeuralNetwork.LayerSizes=�#A ARegressionNeuralNetwork.Mu±RegressionNeuralNetwork.MuC�"G GRegressionNeuralNetwork.Sigma±RegressionNeuralNetwork.SigmaQ�!U URegressionNeuralNetwork.ResponseName±RegressionNeuralNetwork.ResponseNameU� Y YRegressionNeuralNetwork.PredictorNames±RegressionNeuralNetwork.PredictorNamesS�W WRegressionNeuralNetwork.NumPredictors±RegressionNeuralNetwork.NumPredictorsI�M MRegressionNeuralNetwork.RowsUsed±RegressionNeuralNetwork.RowsUsedW�[ [RegressionNeuralNetwork.NumObservations±RegressionNeuralNetwork.NumObservations;�? ?RegressionNeuralNetwork.Y±RegressionNeuralNetwork.Y;�? ?RegressionNeuralNetwork.X±RegressionNeuralNetwork.X7�; ;RegressionNeuralNetwork°RegressionNeuralNetwork=�A ARegressionLinear.savemodel¯RegressionLinear.savemodelC�G GRegressionLinear.selectModels¯RegressionLinear.selectModels3�7 7RegressionLinear.loss¯RegressionLinear.loss9�= =RegressionLinear.predict¯RegressionLinear.predictK�O ORegressionLinear.RegressionLinear¯RegressionLinear.RegressionLinearG�K KRegressionLinear.Regularization®RegressionLinear.RegularizationI�M MRegressionLinear.ModelParameters®RegressionLinear.ModelParameters7�; ;RegressionLinear.Lambda®RegressionLinear.Lambda?�C CRegressionLinear.FittedLoss®RegressionLinear.FittedLoss3�7 7RegressionLinear.Bias®RegressionLinear.Bias3�7 7RegressionLinear.Beta®RegressionLinear.Beta9�= =RegressionLinear.Learner®RegressionLinear.LearnerW� [ [RegressionLinear.ExpandedPredictorNames®RegressionLinear.ExpandedPredictorNamesC� G GRegressionLinear.ResponseName®RegressionLinear.ResponseNameU� Y YRegressionLinear.CategoricalPredictors®RegressionLinear.CategoricalPredictorsG� K KRegressionLinear.PredictorNames®RegressionLinear.PredictorNamesM� Q QRegressionLinear.ResponseTransform®RegressionLinear.ResponseTransform9�= =RegressionLinear.Epsilon®RegressionLinear.Epsilon)�- -RegressionLinear­RegressionLinear=�A ARegressionKernel.savemodel¬RegressionKernel.savemodel7�; ;RegressionKernel.resume¬RegressionKernel.resume3�7 7RegressionKernel.loss¬RegressionKernel.loss9�= =RegressionKernel.predict¬RegressionKernel.predictK�O ORegressionKernel.RegressionKernel¬RegressionKernel.RegressionKernel5�9 9RegressionKernel.Sigma«RegressionKernel.Sigma/�3 3RegressionKernel.Mu«RegressionKernel.Mu9�= =RegressionKernel.Learner«RegressionKernel.LearnerA�~E ERegressionKernel.KernelScale«RegressionKernel.KernelScaleG�}K KRegressionKernel.Regularization«RegressionKernel.RegularizationI�|M MRegressionKernel.ModelParameters«RegressionKernel.ModelParameters7�{; ;RegressionKernel.Lambda«RegressionKernel.Lambda?�zC CRegressionKernel.FittedLoss«RegressionKernel.FittedLossW�y[ [RegressionKernel.NumExpansionDimensions«RegressionKernel.NumExpansionDimensionsW�x[ [RegressionKernel.ExpandedPredictorNames«RegressionKernel.ExpandedPredictorNamesC�wG GRegressionKernel.ResponseName«RegressionKernel.ResponseNameU�vY YRegressionKernel.CategoricalPredictors«RegressionKernel.CategoricalPredictorsG�uK KRegressionKernel.PredictorNames«RegressionKernel.PredictorNamesM�tQ QRegressionKernel.ResponseTransform«RegressionKernel.ResponseTransformE�sI IRegressionKernel.BoxConstraint«RegressionKernel.BoxConstraint -d¦T ¸f Î h ´ 6 Ø n $ Ð Œ > ò ¨ Z ºPâ~ ¬Fü²h¬Z¨>Þ„,Ø–0Æd_�Vc cRegressionPartitionedKernel.NumObservations·RegressionPartitionedKernel.NumObservationsg�Uk kRegressionPartitionedKernel.CrossValidatedModel·RegressionPartitionedKernel.CrossValidatedModelc�Tg gRegressionPartitionedKernel.ResponseTransform·RegressionPartitionedKernel.ResponseTransform?�SC CRegressionPartitionedKernel¶RegressionPartitionedKernelQ�RU URegressionPartitionedEnsemble.resumeµRegressionPartitionedEnsemble.resumeU�QY YRegressionPartitionedEnsemble.kfoldfunµRegressionPartitionedEnsemble.kfoldfunW�P[ [RegressionPartitionedEnsemble.kfoldLossµRegressionPartitionedEnsemble.kfoldLoss]�Oa aRegressionPartitionedEnsemble.kfoldPredictµRegressionPartitionedEnsemble.kfoldPredictg�Nk kRegressionPartitionedEnsemble.ResponseTransform´RegressionPartitionedEnsemble.ResponseTransformU�MY YRegressionPartitionedEnsemble.BinEdges´RegressionPartitionedEnsemble.BinEdgesW�L[ [RegressionPartitionedEnsemble.Partition´RegressionPartitionedEnsemble.PartitionO�KS SRegressionPartitionedEnsemble.KFold´RegressionPartitionedEnsemble.KFoldS�JW WRegressionPartitionedEnsemble.Trained´RegressionPartitionedEnsemble.Trainedc�Ig gRegressionPartitionedEnsemble.ModelParameters´RegressionPartitionedEnsemble.ModelParametersG�HK KRegressionPartitionedEnsemble.W´RegressionPartitionedEnsemble.WG�GK KRegressionPartitionedEnsemble.Y´RegressionPartitionedEnsemble.YG�FK KRegressionPartitionedEnsemble.X´RegressionPartitionedEnsemble.Xc�Eg gRegressionPartitionedEnsemble.NumObservations´RegressionPartitionedEnsemble.NumObservations]�Da aRegressionPartitionedEnsemble.ResponseName´RegressionPartitionedEnsemble.ResponseNameo�Cs sRegressionPartitionedEnsemble.CategoricalPredictors´RegressionPartitionedEnsemble.CategoricalPredictorsa�Be eRegressionPartitionedEnsemble.PredictorNames´RegressionPartitionedEnsemble.PredictorNamesk�Ao oRegressionPartitionedEnsemble.CrossValidatedModel´RegressionPartitionedEnsemble.CrossValidatedModelg�@k kRegressionPartitionedEnsemble.NumTrainedPerFold´RegressionPartitionedEnsemble.NumTrainedPerFoldW�?[ [RegressionPartitionedEnsemble.Trainable´RegressionPartitionedEnsemble.TrainableC�>G GRegressionPartitionedEnsemble³RegressionPartitionedEnsembleK�=O ORegressionNeuralNetwork.savemodel²RegressionNeuralNetwork.savemodelG�<K KRegressionNeuralNetwork.compact²RegressionNeuralNetwork.compactI�;M MRegressionNeuralNetwork.crossval²RegressionNeuralNetwork.crossvalK�:O ORegressionNeuralNetwork.resubLoss²RegressionNeuralNetwork.resubLossA�9E ERegressionNeuralNetwork.loss²RegressionNeuralNetwork.lossQ�8U URegressionNeuralNetwork.resubPredict²RegressionNeuralNetwork.resubPredictG�7K KRegressionNeuralNetwork.predict²RegressionNeuralNetwork.predictg�6k kRegressionNeuralNetwork.RegressionNeuralNetwork²RegressionNeuralNetwork.RegressionNeuralNetwork[�5_ _RegressionNeuralNetwork.ResponseTransform±RegressionNeuralNetwork.ResponseTransform{�4 RegressionNeuralNetwork.HyperparameterOptimizationResults±RegressionNeuralNetwork.HyperparameterOptimizationResultsI�3M MRegressionNeuralNetwork.BinEdges±RegressionNeuralNetwork.BinEdgese�2i iRegressionNeuralNetwork.ExpandedPredictorNames±RegressionNeuralNetwork.ExpandedPredictorNamesc�1g gRegressionNeuralNetwork.CategoricalPredictors±RegressionNeuralNetwork.CategoricalPredictors;�0? ?RegressionNeuralNetwork.W±RegressionNeuralNetwork.WW�/[ [RegressionNeuralNetwork.TrainingHistory±RegressionNeuralNetwork.TrainingHistoryO�.S SRegressionNeuralNetwork.LayerBiases±RegressionNeuralNetwork.LayerBiasesQ�-U URegressionNeuralNetwork.LayerWeights±RegressionNeuralNetwork.LayerWeightsE�,I IRegressionNeuralNetwork.Solver±RegressionNeuralNetwork.SolverO�+S SRegressionNeuralNetwork.DisplayInfo±RegressionNeuralNetwork.DisplayInfoW�*[ [RegressionNeuralNetwork.ConvergenceInfo±RegressionNeuralNetwork.ConvergenceInfo ,¬ºt¦J ø ª T ò x  Æ „  ´ R Æ føœJü¦DÊnØt ®BèˆD¼\ Àl¬k‘o oRegressionPartitionedModel.IsStandardDeviationFit½RegressionPartitionedModel.IsStandardDeviationFitO‘S SRegressionPartitionedModel.BinEdges½RegressionPartitionedModel.BinEdgesQ‘U URegressionPartitionedModel.Partition½RegressionPartitionedModel.PartitionI�M MRegressionPartitionedModel.KFold½RegressionPartitionedModel.KFoldM�~Q QRegressionPartitionedModel.Trained½RegressionPartitionedModel.Trained]�}a aRegressionPartitionedModel.ModelParameters½RegressionPartitionedModel.ModelParametersA�|E ERegressionPartitionedModel.W½RegressionPartitionedModel.WA�{E ERegressionPartitionedModel.Y½RegressionPartitionedModel.YA�zE ERegressionPartitionedModel.X½RegressionPartitionedModel.X]�ya aRegressionPartitionedModel.NumObservations½RegressionPartitionedModel.NumObservationsW�x[ [RegressionPartitionedModel.ResponseName½RegressionPartitionedModel.ResponseNamei�wm mRegressionPartitionedModel.CategoricalPredictors½RegressionPartitionedModel.CategoricalPredictors[�v_ _RegressionPartitionedModel.PredictorNames½RegressionPartitionedModel.PredictorNamese�ui iRegressionPartitionedModel.CrossValidatedModel½RegressionPartitionedModel.CrossValidatedModela�te eRegressionPartitionedModel.ResponseTransform½RegressionPartitionedModel.ResponseTransform=�sA ARegressionPartitionedModel¼RegressionPartitionedModelS�rW WRegressionPartitionedLinear.kfoldLoss»RegressionPartitionedLinear.kfoldLossY�q] ]RegressionPartitionedLinear.kfoldPredict»RegressionPartitionedLinear.kfoldPredictw�p{ {RegressionPartitionedLinear.RegressionPartitionedLinear»RegressionPartitionedLinear.RegressionPartitionedLinear_�oc cRegressionPartitionedLinear.ModelParametersºRegressionPartitionedLinear.ModelParametersS�nW WRegressionPartitionedLinear.PartitionºRegressionPartitionedLinear.PartitionK�mO ORegressionPartitionedLinear.KFoldºRegressionPartitionedLinear.KFoldO�lS SRegressionPartitionedLinear.TrainedºRegressionPartitionedLinear.TrainedY�k] ]RegressionPartitionedLinear.ResponseNameºRegressionPartitionedLinear.ResponseNamek�jo oRegressionPartitionedLinear.CategoricalPredictorsºRegressionPartitionedLinear.CategoricalPredictors]�ia aRegressionPartitionedLinear.PredictorNamesºRegressionPartitionedLinear.PredictorNamesC�hG GRegressionPartitionedLinear.WºRegressionPartitionedLinear.WC�gG GRegressionPartitionedLinear.YºRegressionPartitionedLinear.Y_�fc cRegressionPartitionedLinear.NumObservationsºRegressionPartitionedLinear.NumObservationsg�ek kRegressionPartitionedLinear.CrossValidatedModelºRegressionPartitionedLinear.CrossValidatedModelc�dg gRegressionPartitionedLinear.ResponseTransformºRegressionPartitionedLinear.ResponseTransform?�cC CRegressionPartitionedLinear¹RegressionPartitionedLinearS�bW WRegressionPartitionedKernel.kfoldLoss¸RegressionPartitionedKernel.kfoldLossY�a] ]RegressionPartitionedKernel.kfoldPredict¸RegressionPartitionedKernel.kfoldPredictw�`{ {RegressionPartitionedKernel.RegressionPartitionedKernel¸RegressionPartitionedKernel.RegressionPartitionedKernel_�_c cRegressionPartitionedKernel.ModelParameters·RegressionPartitionedKernel.ModelParametersS�^W WRegressionPartitionedKernel.Partition·RegressionPartitionedKernel.PartitionK�]O ORegressionPartitionedKernel.KFold·RegressionPartitionedKernel.KFoldO�\S SRegressionPartitionedKernel.Trained·RegressionPartitionedKernel.TrainedY�[] ]RegressionPartitionedKernel.ResponseName·RegressionPartitionedKernel.ResponseNamek�Zo oRegressionPartitionedKernel.CategoricalPredictors·RegressionPartitionedKernel.CategoricalPredictors]�Ya aRegressionPartitionedKernel.PredictorNames·RegressionPartitionedKernel.PredictorNamesC�XG GRegressionPartitionedKernel.W·RegressionPartitionedKernel.WC�WG GRegressionPartitionedKernel.Y·RegressionPartitionedKernel.Y =�œ&Ìx& Ö ¬ f . ì ¨ h 2 Ô Ž \ , ü ¶ r * æ ” @ Þt*è–` ð¶~H溎F àšX®t,À†LÔ’F Ô�A‘?E ERegressionTree.CutCategoriesÃRegressionTree.CutCategories5‘>9 9RegressionTree.CutTypeÃRegressionTree.CutType7‘=; ;RegressionTree.CutPointÃRegressionTree.CutPointI‘<M MRegressionTree.CutPredictorIndexÃRegressionTree.CutPredictorIndex?‘;C CRegressionTree.CutPredictorÃRegressionTree.CutPredictor?‘:C CRegressionTree.IsBranchNodeÃRegressionTree.IsBranchNode3‘97 7RegressionTree.ParentÃRegressionTree.Parent7‘8; ;RegressionTree.ChildrenÃRegressionTree.Children7‘7; ;RegressionTree.NumNodesÃRegressionTree.NumNodesi‘6m mRegressionTree.HyperparameterOptimizationResultsÃRegressionTree.HyperparameterOptimizationResultsE‘5I IRegressionTree.ModelParametersÃRegressionTree.ModelParameters7‘4; ;RegressionTree.BinEdgesÃRegressionTree.BinEdgesS‘3W WRegressionTree.ExpandedPredictorNamesÃRegressionTree.ExpandedPredictorNamesQ‘2U URegressionTree.CategoricalPredictorsÃRegressionTree.CategoricalPredictors?‘1C CRegressionTree.ResponseNameÃRegressionTree.ResponseNameC‘0G GRegressionTree.PredictorNamesÃRegressionTree.PredictorNames)‘/- -RegressionTree.WÃRegressionTree.W7‘.; ;RegressionTree.RowsUsedÃRegressionTree.RowsUsedE‘-I IRegressionTree.NumObservationsÃRegressionTree.NumObservations)‘,- -RegressionTree.YÃRegressionTree.Y)‘+- -RegressionTree.XÃRegressionTree.X%‘*) )RegressionTreeÂRegressionTree7‘); ;RegressionSVM.savemodelÁRegressionSVM.savemodel3‘(7 7RegressionSVM.compactÁRegressionSVM.compact5‘'9 9RegressionSVM.crossvalÁRegressionSVM.crossval7‘&; ;RegressionSVM.resubLossÁRegressionSVM.resubLoss-‘%1 1RegressionSVM.lossÁRegressionSVM.loss=‘$A ARegressionSVM.resubPredictÁRegressionSVM.resubPredict3‘#7 7RegressionSVM.predictÁRegressionSVM.predictO‘"S SRegressionSVM.discardSupportVectorsÁRegressionSVM.discardSupportVectors?‘!C CRegressionSVM.RegressionSVMÁRegressionSVM.RegressionSVMG‘ K KRegressionSVM.ResponseTransformÀRegressionSVM.ResponseTransformg‘k kRegressionSVM.HyperparameterOptimizationResultsÀRegressionSVM.HyperparameterOptimizationResults5‘9 9RegressionSVM.BinEdgesÀRegressionSVM.BinEdges'‘+ +RegressionSVM.WÀRegressionSVM.WQ‘U URegressionSVM.ExpandedPredictorNamesÀRegressionSVM.ExpandedPredictorNamesO‘S SRegressionSVM.CategoricalPredictorsÀRegressionSVM.CategoricalPredictorsA‘E ERegressionSVM.BoxConstraintsÀRegressionSVM.BoxConstraintsE‘I IRegressionSVM.KernelParametersÀRegressionSVM.KernelParametersA‘E ERegressionSVM.SupportVectorsÀRegressionSVM.SupportVectorsC‘G GRegressionSVM.IsSupportVectorÀRegressionSVM.IsSupportVector-‘1 1RegressionSVM.BiasÀRegressionSVM.Bias-‘1 1RegressionSVM.BetaÀRegressionSVM.Beta/‘3 3RegressionSVM.AlphaÀRegressionSVM.AlphaC‘G GRegressionSVM.ModelParametersÀRegressionSVM.ModelParameters)‘- -RegressionSVM.MuÀRegressionSVM.Mu/‘3 3RegressionSVM.SigmaÀRegressionSVM.Sigma3‘7 7RegressionSVM.EpsilonÀRegressionSVM.Epsilon=‘A ARegressionSVM.ResponseNameÀRegressionSVM.ResponseNameA‘E ERegressionSVM.PredictorNamesÀRegressionSVM.PredictorNames?‘ C CRegressionSVM.NumPredictorsÀRegressionSVM.NumPredictors5‘ 9 9RegressionSVM.RowsUsedÀRegressionSVM.RowsUsedC‘ G GRegressionSVM.NumObservationsÀRegressionSVM.NumObservations'‘ + +RegressionSVM.YÀRegressionSVM.Y'‘ + +RegressionSVM.XÀRegressionSVM.X#‘' 'RegressionSVM¿RegressionSVMO‘S SRegressionPartitionedModel.kfoldfun¾RegressionPartitionedModel.kfoldfunQ‘U URegressionPartitionedModel.kfoldLoss¾RegressionPartitionedModel.kfoldLossW‘[ [RegressionPartitionedModel.kfoldPredict¾RegressionPartitionedModel.kfoldPredicts‘w wRegressionPartitionedModel.RegressionPartitionedModel¾RegressionPartitionedModel.RegressionPartitionedModela‘e eRegressionPartitionedModel.NumTrainedPerFold½RegressionPartitionedModel.NumTrainedPerFold J°¶|B¾„H ´ j  Ô €  Ð Š R  æ ¬ v & Ú ¨ v 4ø¼¨xJ ìÔªh2à´xFðÔ¶œfHÞ´rDì¦X. 輌T4Ô¨„`<Ú°'’ + +TreeBagger.CostÌTreeBagger.Cost)’- -TreeBagger.PriorÌTreeBagger.Prior3’7 7TreeBagger.ClassNamesÌTreeBagger.ClassNames!’% %TreeBagger.WÌTreeBagger.W!’% %TreeBagger.YÌTreeBagger.Y!’% %TreeBagger.XÌTreeBagger.X)’- -TreeBagger.TreesÌTreeBagger.Trees/’3 3TreeBagger.NumTreesÌTreeBagger.NumTrees+’/ /TreeBagger.MethodÌTreeBagger.Method’! !TreeBaggerËTreeBagger5‘9 9shapley.plotDependenceÊshapley.plotDependence-‘~1 1shapley.swarmchartÊshapley.swarmchart)‘}- -shapley.boxchartÊshapley.boxchart!‘|% %shapley.plotÊshapley.plot‘{# #shapley.fitÊshapley.fit'‘z+ +shapley.shapleyÊshapley.shapleyK‘yO Oshapley.SampledObservationIndicesÉshapley.SampledObservationIndicesC‘xG Gshapley.CategoricalPredictorsÉshapley.CategoricalPredictors-‘w1 1shapley.NumSubsetsÉshapley.NumSubsets%‘v) )shapley.MethodÉshapley.Method+‘u/ /shapley.InterceptÉshapley.Intercept?‘tC Cshapley.MeanAbsoluteShapleyÉshapley.MeanAbsoluteShapley'‘s+ +shapley.ShapleyÉshapley.Shapley5‘r9 9shapley.BlackboxFittedÉshapley.BlackboxFitted/‘q3 3shapley.QueryPointsÉshapley.QueryPoints‘p shapley.XÉshapley.X3‘o7 7shapley.BlackboxModelÉshapley.BlackboxModel‘n shapleyÈshapley‘m lime.plotÇlime.plot‘l lime.fitÇlime.fit‘k lime.limeÇlime.lime5‘j9 9lime.SimpleModelFittedÆlime.SimpleModelFitted/‘i3 3lime.BlackboxFittedÆlime.BlackboxFitted9‘h= =lime.ImportantPredictorsÆlime.ImportantPredictors)‘g- -lime.SimpleModelÆlime.SimpleModel‘f# #lime.FittedÆlime.Fitted-‘e1 1lime.SyntheticDataÆlime.SyntheticData3‘d7 7lime.NumSyntheticDataÆlime.NumSyntheticData?‘cC Clime.NumImportantPredictorsÆlime.NumImportantPredictors'‘b+ +lime.QueryPointÆlime.QueryPoint‘a lime.XÆlime.X‘` lime.TypeÆlime.Type=‘_A Alime.CategoricalPredictorsÆlime.CategoricalPredictors+‘^/ /lime.DataLocalityÆlime.DataLocality-‘]1 1lime.BlackboxModelÆlime.BlackboxModel‘\ limeÅlime9‘[= =RegressionTree.savemodelÄRegressionTree.savemodel9‘Z= =RegressionTree.resubLossÄRegressionTree.resubLoss?‘YC CRegressionTree.resubPredictÄRegressionTree.resubPredict/‘X3 3RegressionTree.lossÄRegressionTree.loss/‘W3 3RegressionTree.viewÄRegressionTree.viewI‘VM MRegressionTree.nodeVariableRangeÄRegressionTree.nodeVariableRangeM‘UQ QRegressionTree.predictorImportanceÄRegressionTree.predictorImportance3‘T7 7RegressionTree.cvlossÄRegressionTree.cvloss7‘S; ;RegressionTree.crossvalÄRegressionTree.crossval5‘R9 9RegressionTree.compactÄRegressionTree.compact1‘Q5 5RegressionTree.pruneÄRegressionTree.prune5‘P9 9RegressionTree.predictÄRegressionTree.predictC‘OG GRegressionTree.RegressionTreeÄRegressionTree.RegressionTreeI‘NM MRegressionTree.ResponseTransformÃRegressionTree.ResponseTransforma‘Me eRegressionTree.SurrogatePredictorAssociationÃRegressionTree.SurrogatePredictorAssociationQ‘LU URegressionTree.SurrogateCutPredictorÃRegressionTree.SurrogateCutPredictorG‘KK KRegressionTree.SurrogateCutTypeÃRegressionTree.SurrogateCutTypeI‘JM MRegressionTree.SurrogateCutPointÃRegressionTree.SurrogateCutPointG‘IK KRegressionTree.SurrogateCutFlipÃRegressionTree.SurrogateCutFlipS‘HW WRegressionTree.SurrogateCutCategoriesÃRegressionTree.SurrogateCutCategories;‘G? ?RegressionTree.PruneAlphaÃRegressionTree.PruneAlpha9‘F= =RegressionTree.PruneListÃRegressionTree.PruneList7‘E; ;RegressionTree.NodeRiskÃRegressionTree.NodeRiskE‘DI IRegressionTree.NodeProbabilityÃRegressionTree.NodeProbability9‘C= =RegressionTree.NodeErrorÃRegressionTree.NodeError7‘B; ;RegressionTree.NodeMeanÃRegressionTree.NodeMean7‘A; ;RegressionTree.NodeSizeÃRegressionTree.NodeSizeG‘@K KRegressionTree.CategoricalSplitÃRegressionTree.CategoricalSplit 9¸ÈŠ>òº~2 è Œ V  º v , Î f ü È Š R & ê ´ „ N "ðÂŽXÜ–Z涆R$ä~"Âl¼jÔ^ÊžL¸I’BM MClusterCriterion.CriterionValuesÒClusterCriterion.CriterionValuesE’AI IClusterCriterion.CriterionNameÒClusterCriterion.CriterionNameO’@S SClusterCriterion.ClusteringFunctionÒClusterCriterion.ClusteringFunction)’?- -ClusterCriterionÑClusterCriterionG’>K KCalinskiHarabaszEvaluation.plotÐCalinskiHarabaszEvaluation.plotG’=K KCalinskiHarabaszEvaluation.addKÐCalinskiHarabaszEvaluation.addKs’<w wCalinskiHarabaszEvaluation.CalinskiHarabaszEvaluationÐCalinskiHarabaszEvaluation.CalinskiHarabaszEvaluationA’;E ECalinskiHarabaszEvaluation.XÏCalinskiHarabaszEvaluation.XO’:S SCalinskiHarabaszEvaluation.OptimalYÏCalinskiHarabaszEvaluation.OptimalYO’9S SCalinskiHarabaszEvaluation.OptimalKÏCalinskiHarabaszEvaluation.OptimalK]’8a aCalinskiHarabaszEvaluation.NumObservationsÏCalinskiHarabaszEvaluation.NumObservationsM’7Q QCalinskiHarabaszEvaluation.MissingÏCalinskiHarabaszEvaluation.MissingS’6W WCalinskiHarabaszEvaluation.InspectedKÏCalinskiHarabaszEvaluation.InspectedK]’5a aCalinskiHarabaszEvaluation.CriterionValuesÏCalinskiHarabaszEvaluation.CriterionValuesY’4] ]CalinskiHarabaszEvaluation.CriterionNameÏCalinskiHarabaszEvaluation.CriterionNamec’3g gCalinskiHarabaszEvaluation.ClusteringFunctionÏCalinskiHarabaszEvaluation.ClusteringFunction=’2A ACalinskiHarabaszEvaluationÎCalinskiHarabaszEvaluation+’1/ /TreeBagger.appendÍTreeBagger.append1’05 5TreeBagger.growTreesÍTreeBagger.growTrees-’/1 1TreeBagger.compactÍTreeBagger.compact-’.1 1TreeBagger.mdsproxÍTreeBagger.mdsprox/’-3 3TreeBagger.fillproxÍTreeBagger.fillprox?’,C CTreeBagger.oobQuantileErrorÍTreeBagger.oobQuantileError9’+= =TreeBagger.quantileErrorÍTreeBagger.quantileErrorC’*G GTreeBagger.oobQuantilePredictÍTreeBagger.oobQuantilePredict=’)A ATreeBagger.quantilePredictÍTreeBagger.quantilePredict9’(= =TreeBagger.oobMeanMarginÍTreeBagger.oobMeanMargin3’'7 7TreeBagger.meanMarginÍTreeBagger.meanMargin1’&5 5TreeBagger.oobMarginÍTreeBagger.oobMargin+’%/ /TreeBagger.marginÍTreeBagger.margin/’$3 3TreeBagger.oobErrorÍTreeBagger.oobError)’#- -TreeBagger.errorÍTreeBagger.error3’"7 7TreeBagger.oobPredictÍTreeBagger.oobPredict-’!1 1TreeBagger.predictÍTreeBagger.predict3’ 7 7TreeBagger.TreeBaggerÍTreeBagger.TreeBagger9’= =TreeBagger.TreeArgumentsÌTreeBagger.TreeArguments)’- -TreeBagger.PruneÌTreeBagger.Prune5’9 9TreeBagger.MergeLeavesÌTreeBagger.MergeLeaves;’? ?TreeBagger.OutlierMeasureÌTreeBagger.OutlierMeasure1’5 5TreeBagger.ProximityÌTreeBagger.Proximityg’k kTreeBagger.OOBPermutedPredictorCountRaiseMarginÌTreeBagger.OOBPermutedPredictorCountRaiseMargine’i iTreeBagger.OOBPermutedPredictorDeltaMeanMarginÌTreeBagger.OOBPermutedPredictorDeltaMeanMargin[’_ _TreeBagger.OOBPermutedPredictorDeltaErrorÌTreeBagger.OOBPermutedPredictorDeltaErrorG’K KTreeBagger.SurrogateAssociationÌTreeBagger.SurrogateAssociationA’E ETreeBagger.NumPredictorSplitÌTreeBagger.NumPredictorSplitU’Y YTreeBagger.DeltaCriterionDecisionSplitÌTreeBagger.DeltaCriterionDecisionSplitA’E ETreeBagger.OOBInstanceWeightÌTreeBagger.OOBInstanceWeight3’7 7TreeBagger.OOBIndicesÌTreeBagger.OOBIndicesY’] ]TreeBagger.ComputeOOBPredictorImportanceÌTreeBagger.ComputeOOBPredictorImportanceG’K KTreeBagger.ComputeOOBPredictionÌTreeBagger.ComputeOOBPredictionI’M MTreeBagger.SampleWithReplacementÌTreeBagger.SampleWithReplacement9’= =TreeBagger.InBagFractionÌTreeBagger.InBagFraction5’9 9TreeBagger.MinLeafSizeÌTreeBagger.MinLeafSizeI’ M MTreeBagger.NumPredictorsToSampleÌTreeBagger.NumPredictorsToSampleI’ M MTreeBagger.CategoricalPredictorsÌTreeBagger.CategoricalPredictors;’ ? ?TreeBagger.PredictorNamesÌTreeBagger.PredictorNames5’ 9 9TreeBagger.DefaultYfitÌTreeBagger.DefaultYfit =¦¾‚6øºŠ< Ð ” Z ú ¤ J ú ° V ¾ €  Ò Ž h > ´t4Ø¢VÎ’\Þ¦|: Ú²„P¶r0ê p@°lÖ¦-’1 1gmdistribution.cdfÜgmdistribution.cdfC’~G Ggmdistribution.gmdistributionÜgmdistribution.gmdistributionM’}Q Qgmdistribution.RegularizationValueÛgmdistribution.RegularizationValueA’|E Egmdistribution.NumIterationsÛgmdistribution.NumIterationsQ’{U Ugmdistribution.NegativeLogLikelihoodÛgmdistribution.NegativeLogLikelihood9’z= =gmdistribution.ConvergedÛgmdistribution.Converged-’y1 1gmdistribution.BICÛgmdistribution.BIC-’x1 1gmdistribution.AICÛgmdistribution.AICG’wK Kgmdistribution.SharedCovarianceÛgmdistribution.SharedCovarianceC’vG Ggmdistribution.CovarianceTypeÛgmdistribution.CovarianceType?’uC Cgmdistribution.NumVariablesÛgmdistribution.NumVariablesA’tE Egmdistribution.NumComponentsÛgmdistribution.NumComponentsG’sK Kgmdistribution.DistributionNameÛgmdistribution.DistributionNameM’rQ Qgmdistribution.ComponentProportionÛgmdistribution.ComponentProportion1’q5 5gmdistribution.SigmaÛgmdistribution.Sigma+’p/ /gmdistribution.muÛgmdistribution.mu%’o) )gmdistributionÚgmdistribution-’n1 1GapEvaluation.plotÙGapEvaluation.plot-’m1 1GapEvaluation.addKÙGapEvaluation.addK?’lC CGapEvaluation.GapEvaluationÙGapEvaluation.GapEvaluation'’k+ +GapEvaluation.XØGapEvaluation.X5’j9 9GapEvaluation.OptimalYØGapEvaluation.OptimalY5’i9 9GapEvaluation.OptimalKØGapEvaluation.OptimalKC’hG GGapEvaluation.NumObservationsØGapEvaluation.NumObservations3’g7 7GapEvaluation.MissingØGapEvaluation.Missing9’f= =GapEvaluation.InspectedKØGapEvaluation.InspectedKC’eG GGapEvaluation.CriterionValuesØGapEvaluation.CriterionValues?’dC CGapEvaluation.CriterionNameØGapEvaluation.CriterionNameI’cM MGapEvaluation.ClusteringFunctionØGapEvaluation.ClusteringFunction3’b7 7GapEvaluation.StdLogWØGapEvaluation.StdLogW)’a- -GapEvaluation.SEØGapEvaluation.SE-’`1 1GapEvaluation.LogWØGapEvaluation.LogW=’_A AGapEvaluation.ExpectedLogWØGapEvaluation.ExpectedLogW=’^A AGapEvaluation.SearchMethodØGapEvaluation.SearchMethodO’]S SGapEvaluation.ReferenceDistributionØGapEvaluation.ReferenceDistribution5’\9 9GapEvaluation.DistanceØGapEvaluation.Distance'’[+ +GapEvaluation.BØGapEvaluation.B#’Z' 'GapEvaluation×GapEvaluationA’YE EDaviesBouldinEvaluation.plotÖDaviesBouldinEvaluation.plotA’XE EDaviesBouldinEvaluation.addKÖDaviesBouldinEvaluation.addKg’Wk kDaviesBouldinEvaluation.DaviesBouldinEvaluationÖDaviesBouldinEvaluation.DaviesBouldinEvaluation;’V? ?DaviesBouldinEvaluation.XÕDaviesBouldinEvaluation.XI’UM MDaviesBouldinEvaluation.OptimalYÕDaviesBouldinEvaluation.OptimalYI’TM MDaviesBouldinEvaluation.OptimalKÕDaviesBouldinEvaluation.OptimalKW’S[ [DaviesBouldinEvaluation.NumObservationsÕDaviesBouldinEvaluation.NumObservationsG’RK KDaviesBouldinEvaluation.MissingÕDaviesBouldinEvaluation.MissingM’QQ QDaviesBouldinEvaluation.InspectedKÕDaviesBouldinEvaluation.InspectedKW’P[ [DaviesBouldinEvaluation.CriterionValuesÕDaviesBouldinEvaluation.CriterionValuesS’OW WDaviesBouldinEvaluation.CriterionNameÕDaviesBouldinEvaluation.CriterionName]’Na aDaviesBouldinEvaluation.ClusteringFunctionÕDaviesBouldinEvaluation.ClusteringFunction7’M; ;DaviesBouldinEvaluationÔDaviesBouldinEvaluation9’L= =ClusterCriterion.compactÓClusterCriterion.compact3’K7 7ClusterCriterion.plotÓClusterCriterion.plot3’J7 7ClusterCriterion.addKÓClusterCriterion.addKK’IO OClusterCriterion.ClusterCriterionÓClusterCriterion.ClusterCriterion-’H1 1ClusterCriterion.XÒClusterCriterion.X;’G? ?ClusterCriterion.OptimalYÒClusterCriterion.OptimalY;’F? ?ClusterCriterion.OptimalKÒClusterCriterion.OptimalKI’EM MClusterCriterion.NumObservationsÒClusterCriterion.NumObservations9’D= =ClusterCriterion.MissingÒClusterCriterion.Missing?’CC CClusterCriterion.InspectedKÒClusterCriterion.InspectedK <¨È”d(òÂŽH ø ž D ô   V  ¾ x 2 ú œ ^ ð ¼ z .Ø”L(ò²h*ÄŒd8úÀ|6úº�NðšRÊšP²dʨ“;# #OneClassSVMïOneClassSVMA“:E ELocalOutlierFactor.isanomalyîLocalOutlierFactor.isanomalyS“9W WLocalOutlierFactor.LocalOutlierFactorîLocalOutlierFactor.LocalOutlierFactorK“8O OLocalOutlierFactor.ScoreThresholdíLocalOutlierFactor.ScoreThresholdY“7] ]LocalOutlierFactor.ContaminationFractioníLocalOutlierFactor.ContaminationFraction?“6C CLocalOutlierFactor.DistanceíLocalOutlierFactor.DistanceG“5K KLocalOutlierFactor.NumNeighborsíLocalOutlierFactor.NumNeighbors-“41 1LocalOutlierFactorìLocalOutlierFactor;“3? ?IsolationForest.isanomalyëIsolationForest.isanomalyG“2K KIsolationForest.IsolationForestëIsolationForest.IsolationForestE“1I IIsolationForest.ScoreThresholdêIsolationForest.ScoreThresholdS“0W WIsolationForest.ContaminationFractionêIsolationForest.ContaminationFraction[“/_ _IsolationForest.NumObservationsPerLearnerêIsolationForest.NumObservationsPerLearner?“.C CIsolationForest.NumLearnersêIsolationForest.NumLearners'“-+ +IsolationForestéIsolationForest=“,A AKDTreeSearcher.rangesearchèKDTreeSearcher.rangesearch9“+= =KDTreeSearcher.knnsearchèKDTreeSearcher.knnsearchC“*G GKDTreeSearcher.KDTreeSearcherèKDTreeSearcher.KDTreeSearcherA“)E EKDTreeSearcher.DistParameterçKDTreeSearcher.DistParameter7“(; ;KDTreeSearcher.DistanceçKDTreeSearcher.Distance;“'? ?KDTreeSearcher.BucketSizeçKDTreeSearcher.BucketSize)“&- -KDTreeSearcher.XçKDTreeSearcher.X%“%) )KDTreeSearcheræKDTreeSearcher5“$9 9hnswSearcher.knnsearchåhnswSearcher.knnsearch;“#? ?hnswSearcher.hnswSearcheråhnswSearcher.hnswSearcher%“") )hnswSearcher.XähnswSearcher.X;“!? ?hnswSearcher.TrainSetSizeähnswSearcher.TrainSetSizeG“ K KhnswSearcher.MaxNumLinksPerNodeähnswSearcher.MaxNumLinksPerNode=“A AhnswSearcher.DistParameterähnswSearcher.DistParameter3“7 7hnswSearcher.DistanceähnswSearcher.Distance!“% %hnswSearcherãhnswSearcherE“I IExhaustiveSearcher.rangesearchâExhaustiveSearcher.rangesearchA“E EExhaustiveSearcher.knnsearchâExhaustiveSearcher.knnsearchS“W WExhaustiveSearcher.ExhaustiveSearcherâExhaustiveSearcher.ExhaustiveSearcherI“M MExhaustiveSearcher.DistParameteráExhaustiveSearcher.DistParameter?“C CExhaustiveSearcher.DistanceáExhaustiveSearcher.Distance1“5 5ExhaustiveSearcher.XáExhaustiveSearcher.X-“1 1ExhaustiveSearcheràExhaustiveSearcher;“? ?SilhouetteEvaluation.plotßSilhouetteEvaluation.plot;“? ?SilhouetteEvaluation.addKßSilhouetteEvaluation.addK[“_ _SilhouetteEvaluation.SilhouetteEvaluationßSilhouetteEvaluation.SilhouetteEvaluation5“9 9SilhouetteEvaluation.XÞSilhouetteEvaluation.XC“G GSilhouetteEvaluation.OptimalYÞSilhouetteEvaluation.OptimalYC“G GSilhouetteEvaluation.OptimalKÞSilhouetteEvaluation.OptimalKQ“U USilhouetteEvaluation.NumObservationsÞSilhouetteEvaluation.NumObservationsA“E ESilhouetteEvaluation.MissingÞSilhouetteEvaluation.MissingG“ K KSilhouetteEvaluation.InspectedKÞSilhouetteEvaluation.InspectedKQ“ U USilhouetteEvaluation.CriterionValuesÞSilhouetteEvaluation.CriterionValuesM“ Q QSilhouetteEvaluation.CriterionNameÞSilhouetteEvaluation.CriterionNameW“ [ [SilhouetteEvaluation.ClusteringFunctionÞSilhouetteEvaluation.ClusteringFunctionW“ [ [SilhouetteEvaluation.ClusterSilhouettesÞSilhouetteEvaluation.ClusterSilhouettesM“Q QSilhouetteEvaluation.ClusterPriorsÞSilhouetteEvaluation.ClusterPriorsC“G GSilhouetteEvaluation.DistanceÞSilhouetteEvaluation.Distance1“5 5SilhouetteEvaluationÝSilhouetteEvaluation-“1 1gmdistribution.fitÜgmdistribution.fit3“7 7gmdistribution.randomÜgmdistribution.random9“= =gmdistribution.posteriorÜgmdistribution.posterior-“1 1gmdistribution.pdfÜgmdistribution.pdf1“5 5gmdistribution.mahalÜgmdistribution.mahal5“9 9gmdistribution.clusterÜgmdistribution.cluster :’Æ–Fð¢b( ò Ä v , Ø ¤ j , Ü ~ Î Œ b  Ò ‚ R â–<ò´€>ü¼v0àŠ:ê Tþ®h(܆Dæšx6üÆ’1“u5 5cvpartition.TestSizeücvpartition.TestSize3“t7 7cvpartition.TrainSizeücvpartition.TrainSize7“s; ;cvpartition.NumTestSetsücvpartition.NumTestSets?“rC Ccvpartition.NumObservationsücvpartition.NumObservations“q# #cvpartitionûcvpartitionI“pM MConfusionMatrixChart.sortClassesúConfusionMatrixChart.sortClasses[“o_ _ConfusionMatrixChart.ConfusionMatrixChartúConfusionMatrixChart.ConfusionMatrixChart?“nC CConfusionMatrixChart.ParentùConfusionMatrixChart.ParentS“mW WConfusionMatrixChart.NormalizedValuesùConfusionMatrixChart.NormalizedValuesI“lM MConfusionMatrixChart.ClassLabelsùConfusionMatrixChart.ClassLabels=“kA AConfusionMatrixChart.UnitsùConfusionMatrixChart.UnitsC“jG GConfusionMatrixChart.PositionùConfusionMatrixChart.PositionM“iQ QConfusionMatrixChart.OuterPositionùConfusionMatrixChart.OuterPositionS“hW WConfusionMatrixChart.HandleVisibilityùConfusionMatrixChart.HandleVisibilityI“gM MConfusionMatrixChart.GridVisibleùConfusionMatrixChart.GridVisibleG“fK KConfusionMatrixChart.RowSummaryùConfusionMatrixChart.RowSummaryM“eQ QConfusionMatrixChart.ColumnSummaryùConfusionMatrixChart.ColumnSummaryM“dQ QConfusionMatrixChart.NormalizationùConfusionMatrixChart.NormalizationS“cW WConfusionMatrixChart.OffDiagonalColorùConfusionMatrixChart.OffDiagonalColorM“bQ QConfusionMatrixChart.DiagonalColorùConfusionMatrixChart.DiagonalColorC“aG GConfusionMatrixChart.FontSizeùConfusionMatrixChart.FontSizeC“`G GConfusionMatrixChart.FontNameùConfusionMatrixChart.FontName=“_A AConfusionMatrixChart.TitleùConfusionMatrixChart.Title?“^C CConfusionMatrixChart.YLabelùConfusionMatrixChart.YLabel?“]C CConfusionMatrixChart.XLabelùConfusionMatrixChart.XLabel1“\5 5ConfusionMatrixChartøConfusionMatrixChart;“[? ?SparseFiltering.transform÷SparseFiltering.transformG“ZK KSparseFiltering.SparseFiltering÷SparseFiltering.SparseFilteringW“Y[ [SparseFiltering.InitialTransformWeightsöSparseFiltering.InitialTransformWeightsI“XM MSparseFiltering.TransformWeightsöSparseFiltering.TransformWeights7“W; ;SparseFiltering.FitInfoöSparseFiltering.FitInfo3“V7 7SparseFiltering.SigmaöSparseFiltering.Sigma-“U1 1SparseFiltering.MuöSparseFiltering.MuM“TQ QSparseFiltering.NumLearnedFeaturesöSparseFiltering.NumLearnedFeaturesC“SG GSparseFiltering.NumPredictorsöSparseFiltering.NumPredictorsG“RK KSparseFiltering.ModelParametersöSparseFiltering.ModelParameters'“Q+ +SparseFilteringõSparseFiltering?“PC CReconstructionICA.transformôReconstructionICA.transformO“OS SReconstructionICA.ReconstructionICAôReconstructionICA.ReconstructionICA[“N_ _ReconstructionICA.NonGaussianityIndicatoróReconstructionICA.NonGaussianityIndicator[“M_ _ReconstructionICA.InitialTransformWeightsóReconstructionICA.InitialTransformWeightsM“LQ QReconstructionICA.TransformWeightsóReconstructionICA.TransformWeights;“K? ?ReconstructionICA.FitInfoóReconstructionICA.FitInfo7“J; ;ReconstructionICA.SigmaóReconstructionICA.Sigma1“I5 5ReconstructionICA.MuóReconstructionICA.MuQ“HU UReconstructionICA.NumLearnedFeaturesóReconstructionICA.NumLearnedFeaturesG“GK KReconstructionICA.NumPredictorsóReconstructionICA.NumPredictorsK“FO OReconstructionICA.ModelParametersóReconstructionICA.ModelParameters+“E/ /ReconstructionICAòReconstructionICA3“D7 7OneClassSVM.isanomalyñOneClassSVM.isanomaly7“C; ;OneClassSVM.OneClassSVMñOneClassSVM.OneClassSVM=“BA AOneClassSVM.ScoreThresholdðOneClassSVM.ScoreThresholdK“AO OOneClassSVM.ContaminationFractionðOneClassSVM.ContaminationFraction+“@/ /OneClassSVM.SigmaðOneClassSVM.Sigma%“?) )OneClassSVM.MuðOneClassSVM.MuM“>Q QOneClassSVM.NumExpansionDimensionsðOneClassSVM.NumExpansionDimensions-“=1 1OneClassSVM.LambdaðOneClassSVM.Lambda7“<; ;OneClassSVM.KernelScaleðOneClassSVM.KernelScale B¤Ô j.ôºˆ\( Ø ° z P $ ö È ˜ b , ü Ò ° v 8 þ Ô ª ~ N  ä ª p >úÄŠPø¦RòœP¤LÄ„FÂx2ô´n*ì¤f(æ¤?”7C Cprob.BinomialDistribution.p prob.BinomialDistribution.p?”6C Cprob.BinomialDistribution.N prob.BinomialDistribution.N;”5? ?prob.BinomialDistributionprob.BinomialDistribution;”4? ?prob.BetaDistribution.varprob.BetaDistribution.varE”3I Iprob.BetaDistribution.truncateprob.BetaDistribution.truncate;”2? ?prob.BetaDistribution.stdprob.BetaDistribution.stdA”1E Eprob.BetaDistribution.randomprob.BetaDistribution.randomC”0G Gprob.BetaDistribution.proflikprob.BetaDistribution.proflik=”/A Aprob.BetaDistribution.plotprob.BetaDistribution.plot;”.? ?prob.BetaDistribution.pdfprob.BetaDistribution.pdfC”-G Gprob.BetaDistribution.paramciprob.BetaDistribution.paramciG”,K Kprob.BetaDistribution.negloglikprob.BetaDistribution.negloglikA”+E Eprob.BetaDistribution.medianprob.BetaDistribution.median=”*A Aprob.BetaDistribution.meanprob.BetaDistribution.mean;”)? ?prob.BetaDistribution.iqrprob.BetaDistribution.iqr=”(A Aprob.BetaDistribution.icdfprob.BetaDistribution.icdf;”'? ?prob.BetaDistribution.cdfprob.BetaDistribution.cdfG”&K Kprob.BetaDistribution.InputDataprob.BetaDistribution.InputDataU”%Y Yprob.BetaDistribution.ParameterIsFixedprob.BetaDistribution.ParameterIsFixed[”$_ _prob.BetaDistribution.ParameterCovarianceprob.BetaDistribution.ParameterCovarianceK”#O Oprob.BetaDistribution.IsTruncatedprob.BetaDistribution.IsTruncatedI”"M Mprob.BetaDistribution.Truncationprob.BetaDistribution.TruncationS”!W Wprob.BetaDistribution.ParameterValuesprob.BetaDistribution.ParameterValues]” a aprob.BetaDistribution.ParameterDescriptionprob.BetaDistribution.ParameterDescriptionQ”U Uprob.BetaDistribution.ParameterNamesprob.BetaDistribution.ParameterNamesO”S Sprob.BetaDistribution.NumParametersprob.BetaDistribution.NumParametersU”Y Yprob.BetaDistribution.DistributionNameprob.BetaDistribution.DistributionName7”; ;prob.BetaDistribution.bprob.BetaDistribution.b7”; ;prob.BetaDistribution.aprob.BetaDistribution.a3”7 7prob.BetaDistributionprob.BetaDistributionA”E Eprob.ProbabilityDistributionprob.ProbabilityDistribution/”3 3paretotails.segmentparetotails.segment7”; ;paretotails.upperparamsparetotails.upperparams7”; ;paretotails.lowerparamsparetotails.lowerparams3”7 7paretotails.nsegmentsparetotails.nsegments1”5 5paretotails.boundaryparetotails.boundary-”1 1paretotails.randomparetotails.random)”- -paretotails.icdfparetotails.icdf'”+ +paretotails.pdfparetotails.pdf'”+ +paretotails.cdfparetotails.cdf7”; ;paretotails.paretotailsparetotails.paretotails;”? ?paretotails.NumParametersparetotails.NumParameters7” ; ;paretotails.NumSegmentsparetotails.NumSegments” # #paretotailsparetotails'” + +rocmetrics.plotrocmetrics.plot-” 1 1rocmetrics.averagerocmetrics.average3” 7 7rocmetrics.addMetricsrocmetrics.addMetrics3”7 7rocmetrics.rocmetricsrocmetrics.rocmetrics-”1 1rocmetrics.Weightsÿrocmetrics.Weights+”/ /rocmetrics.Scoresÿrocmetrics.Scores+”/ /rocmetrics.Labelsÿrocmetrics.Labels)”- -rocmetrics.Priorÿrocmetrics.Prior'”+ +rocmetrics.Costÿrocmetrics.Cost3”7 7rocmetrics.ClassNamesÿrocmetrics.ClassNames%”) )rocmetrics.AUCÿrocmetrics.AUC-”1 1rocmetrics.Metricsÿrocmetrics.Metrics“! !rocmetricsþrocmetrics1“~5 5cvpartition.trainingýcvpartition.training)“}- -cvpartition.testýcvpartition.test/“|3 3cvpartition.summaryýcvpartition.summary7“{; ;cvpartition.repartitionýcvpartition.repartition7“z; ;cvpartition.cvpartitionýcvpartition.cvpartition9“y= =cvpartition.IsStratifiedücvpartition.IsStratified3“x7 7cvpartition.IsGroupedücvpartition.IsGrouped1“w5 5cvpartition.IsCustomücvpartition.IsCustom)“v- -cvpartition.Typeücvpartition.Type ,  Fê‚$ Ð z  ´ b  Ô Ž F ú ¨ Z  Ì ~ 2ìœV°Væ|˜*Æ`êzÂj¼`þ [”c_ _prob.BirnbaumSaundersDistribution.paramci prob.BirnbaumSaundersDistribution.paramci_”bc cprob.BirnbaumSaundersDistribution.negloglik prob.BirnbaumSaundersDistribution.negloglikY”a] ]prob.BirnbaumSaundersDistribution.median prob.BirnbaumSaundersDistribution.medianU”`Y Yprob.BirnbaumSaundersDistribution.mean prob.BirnbaumSaundersDistribution.meanS”_W Wprob.BirnbaumSaundersDistribution.iqr prob.BirnbaumSaundersDistribution.iqrU”^Y Yprob.BirnbaumSaundersDistribution.icdf prob.BirnbaumSaundersDistribution.icdfS”]W Wprob.BirnbaumSaundersDistribution.cdf prob.BirnbaumSaundersDistribution.cdf_”\c cprob.BirnbaumSaundersDistribution.InputData prob.BirnbaumSaundersDistribution.InputDatam”[q qprob.BirnbaumSaundersDistribution.ParameterIsFixed prob.BirnbaumSaundersDistribution.ParameterIsFixeds”Zw wprob.BirnbaumSaundersDistribution.ParameterCovariance prob.BirnbaumSaundersDistribution.ParameterCovariancec”Yg gprob.BirnbaumSaundersDistribution.IsTruncated prob.BirnbaumSaundersDistribution.IsTruncateda”Xe eprob.BirnbaumSaundersDistribution.Truncation prob.BirnbaumSaundersDistribution.Truncationk”Wo oprob.BirnbaumSaundersDistribution.ParameterValues prob.BirnbaumSaundersDistribution.ParameterValuesu”Vy yprob.BirnbaumSaundersDistribution.ParameterDescription prob.BirnbaumSaundersDistribution.ParameterDescriptioni”Um mprob.BirnbaumSaundersDistribution.ParameterNames prob.BirnbaumSaundersDistribution.ParameterNamesg”Tk kprob.BirnbaumSaundersDistribution.NumParameters prob.BirnbaumSaundersDistribution.NumParametersm”Sq qprob.BirnbaumSaundersDistribution.DistributionName prob.BirnbaumSaundersDistribution.DistributionNameW”R[ [prob.BirnbaumSaundersDistribution.gamma prob.BirnbaumSaundersDistribution.gammaU”QY Yprob.BirnbaumSaundersDistribution.beta prob.BirnbaumSaundersDistribution.betaK”PO Oprob.BirnbaumSaundersDistribution prob.BirnbaumSaundersDistributionC”OG Gprob.BinomialDistribution.var prob.BinomialDistribution.varM”NQ Qprob.BinomialDistribution.truncate prob.BinomialDistribution.truncateC”MG Gprob.BinomialDistribution.std prob.BinomialDistribution.stdI”LM Mprob.BinomialDistribution.random prob.BinomialDistribution.randomK”KO Oprob.BinomialDistribution.proflik prob.BinomialDistribution.proflikE”JI Iprob.BinomialDistribution.plot prob.BinomialDistribution.plotC”IG Gprob.BinomialDistribution.pdf prob.BinomialDistribution.pdfK”HO Oprob.BinomialDistribution.paramci prob.BinomialDistribution.paramciO”GS Sprob.BinomialDistribution.negloglik prob.BinomialDistribution.negloglikI”FM Mprob.BinomialDistribution.median prob.BinomialDistribution.medianE”EI Iprob.BinomialDistribution.mean prob.BinomialDistribution.meanC”DG Gprob.BinomialDistribution.iqr prob.BinomialDistribution.iqrE”CI Iprob.BinomialDistribution.icdf prob.BinomialDistribution.icdfC”BG Gprob.BinomialDistribution.cdf prob.BinomialDistribution.cdfO”AS Sprob.BinomialDistribution.InputData prob.BinomialDistribution.InputData]”@a aprob.BinomialDistribution.ParameterIsFixed prob.BinomialDistribution.ParameterIsFixedc”?g gprob.BinomialDistribution.ParameterCovariance prob.BinomialDistribution.ParameterCovarianceS”>W Wprob.BinomialDistribution.IsTruncated prob.BinomialDistribution.IsTruncatedQ”=U Uprob.BinomialDistribution.Truncation prob.BinomialDistribution.Truncation[”<_ _prob.BinomialDistribution.ParameterValues prob.BinomialDistribution.ParameterValuese”;i iprob.BinomialDistribution.ParameterDescription prob.BinomialDistribution.ParameterDescriptionY”:] ]prob.BinomialDistribution.ParameterNames prob.BinomialDistribution.ParameterNamesW”9[ [prob.BinomialDistribution.NumParameters prob.BinomialDistribution.NumParameters]”8a aprob.BinomialDistribution.DistributionName prob.BinomialDistribution.DistributionName 1´ªRô˜B â Œ V  Ú   H ö ¢ B ì   R ô œ R Ô–VÈ‚D¾z<ô¶r(Âb’.Ôx ¦N´K•O Oprob.ExponentialDistribution.icdfprob.ExponentialDistribution.icdfI•M Mprob.ExponentialDistribution.cdfprob.ExponentialDistribution.cdfU•Y Yprob.ExponentialDistribution.InputDataprob.ExponentialDistribution.InputDatac•g gprob.ExponentialDistribution.ParameterIsFixedprob.ExponentialDistribution.ParameterIsFixedi•m mprob.ExponentialDistribution.ParameterCovarianceprob.ExponentialDistribution.ParameterCovarianceY•] ]prob.ExponentialDistribution.IsTruncatedprob.ExponentialDistribution.IsTruncatedW•[ [prob.ExponentialDistribution.Truncationprob.ExponentialDistribution.Truncationa• e eprob.ExponentialDistribution.ParameterValuesprob.ExponentialDistribution.ParameterValuesk• o oprob.ExponentialDistribution.ParameterDescriptionprob.ExponentialDistribution.ParameterDescription_• c cprob.ExponentialDistribution.ParameterNamesprob.ExponentialDistribution.ParameterNames]• a aprob.ExponentialDistribution.NumParametersprob.ExponentialDistribution.NumParametersc• g gprob.ExponentialDistribution.DistributionNameprob.ExponentialDistribution.DistributionNameG•K Kprob.ExponentialDistribution.muprob.ExponentialDistribution.muA•E Eprob.ExponentialDistributionprob.ExponentialDistribution;•? ?prob.BurrDistribution.varprob.BurrDistribution.varE•I Iprob.BurrDistribution.truncateprob.BurrDistribution.truncate;•? ?prob.BurrDistribution.stdprob.BurrDistribution.stdA•E Eprob.BurrDistribution.randomprob.BurrDistribution.randomC•G Gprob.BurrDistribution.proflikprob.BurrDistribution.proflik=•A Aprob.BurrDistribution.plotprob.BurrDistribution.plot;•? ?prob.BurrDistribution.pdfprob.BurrDistribution.pdfC”G Gprob.BurrDistribution.paramciprob.BurrDistribution.paramciG”~K Kprob.BurrDistribution.negloglikprob.BurrDistribution.negloglikA”}E Eprob.BurrDistribution.medianprob.BurrDistribution.median=”|A Aprob.BurrDistribution.meanprob.BurrDistribution.mean;”{? ?prob.BurrDistribution.iqrprob.BurrDistribution.iqr=”zA Aprob.BurrDistribution.icdfprob.BurrDistribution.icdf;”y? ?prob.BurrDistribution.cdfprob.BurrDistribution.cdfG”xK Kprob.BurrDistribution.InputDataprob.BurrDistribution.InputDataU”wY Yprob.BurrDistribution.ParameterIsFixedprob.BurrDistribution.ParameterIsFixed[”v_ _prob.BurrDistribution.ParameterCovarianceprob.BurrDistribution.ParameterCovarianceK”uO Oprob.BurrDistribution.IsTruncatedprob.BurrDistribution.IsTruncatedI”tM Mprob.BurrDistribution.Truncationprob.BurrDistribution.TruncationS”sW Wprob.BurrDistribution.ParameterValuesprob.BurrDistribution.ParameterValues]”ra aprob.BurrDistribution.ParameterDescriptionprob.BurrDistribution.ParameterDescriptionQ”qU Uprob.BurrDistribution.ParameterNamesprob.BurrDistribution.ParameterNamesO”pS Sprob.BurrDistribution.NumParametersprob.BurrDistribution.NumParametersU”oY Yprob.BurrDistribution.DistributionNameprob.BurrDistribution.DistributionName7”n; ;prob.BurrDistribution.kprob.BurrDistribution.k7”m; ;prob.BurrDistribution.cprob.BurrDistribution.c?”lC Cprob.BurrDistribution.alphaprob.BurrDistribution.alpha3”k7 7prob.BurrDistributionprob.BurrDistributionS”jW Wprob.BirnbaumSaundersDistribution.var prob.BirnbaumSaundersDistribution.var]”ia aprob.BirnbaumSaundersDistribution.truncate prob.BirnbaumSaundersDistribution.truncateS”hW Wprob.BirnbaumSaundersDistribution.std prob.BirnbaumSaundersDistribution.stdY”g] ]prob.BirnbaumSaundersDistribution.random prob.BirnbaumSaundersDistribution.random[”f_ _prob.BirnbaumSaundersDistribution.proflik prob.BirnbaumSaundersDistribution.proflikU”eY Yprob.BirnbaumSaundersDistribution.plot prob.BirnbaumSaundersDistribution.plotS”dW Wprob.BirnbaumSaundersDistribution.pdf prob.BirnbaumSaundersDistribution.pdf /~´f¼h Î z ( Ü † : ô ¨ V î Œ ( ¸ Rö˜*ÂhÊ|,Ø~(ÚŠ4à’:ì´x<âŽ8Ö~U•CY Yprob.GammaDistribution.ParameterValuesprob.GammaDistribution.ParameterValues_•Bc cprob.GammaDistribution.ParameterDescriptionprob.GammaDistribution.ParameterDescriptionS•AW Wprob.GammaDistribution.ParameterNamesprob.GammaDistribution.ParameterNamesQ•@U Uprob.GammaDistribution.NumParametersprob.GammaDistribution.NumParametersW•?[ [prob.GammaDistribution.DistributionNameprob.GammaDistribution.DistributionName9•>= =prob.GammaDistribution.bprob.GammaDistribution.b9•== =prob.GammaDistribution.aprob.GammaDistribution.a5•<9 9prob.GammaDistributionprob.GammaDistributionK•;O Oprob.ExtremeValueDistribution.varprob.ExtremeValueDistribution.varU•:Y Yprob.ExtremeValueDistribution.truncateprob.ExtremeValueDistribution.truncateK•9O Oprob.ExtremeValueDistribution.stdprob.ExtremeValueDistribution.stdQ•8U Uprob.ExtremeValueDistribution.randomprob.ExtremeValueDistribution.randomS•7W Wprob.ExtremeValueDistribution.proflikprob.ExtremeValueDistribution.proflikM•6Q Qprob.ExtremeValueDistribution.plotprob.ExtremeValueDistribution.plotK•5O Oprob.ExtremeValueDistribution.pdfprob.ExtremeValueDistribution.pdfS•4W Wprob.ExtremeValueDistribution.paramciprob.ExtremeValueDistribution.paramciW•3[ [prob.ExtremeValueDistribution.negloglikprob.ExtremeValueDistribution.negloglikQ•2U Uprob.ExtremeValueDistribution.medianprob.ExtremeValueDistribution.medianM•1Q Qprob.ExtremeValueDistribution.meanprob.ExtremeValueDistribution.meanK•0O Oprob.ExtremeValueDistribution.iqrprob.ExtremeValueDistribution.iqrM•/Q Qprob.ExtremeValueDistribution.icdfprob.ExtremeValueDistribution.icdfK•.O Oprob.ExtremeValueDistribution.cdfprob.ExtremeValueDistribution.cdfW•-[ [prob.ExtremeValueDistribution.InputDataprob.ExtremeValueDistribution.InputDatae•,i iprob.ExtremeValueDistribution.ParameterIsFixedprob.ExtremeValueDistribution.ParameterIsFixedk•+o oprob.ExtremeValueDistribution.ParameterCovarianceprob.ExtremeValueDistribution.ParameterCovariance[•*_ _prob.ExtremeValueDistribution.IsTruncatedprob.ExtremeValueDistribution.IsTruncatedY•)] ]prob.ExtremeValueDistribution.Truncationprob.ExtremeValueDistribution.Truncationc•(g gprob.ExtremeValueDistribution.ParameterValuesprob.ExtremeValueDistribution.ParameterValuesm•'q qprob.ExtremeValueDistribution.ParameterDescriptionprob.ExtremeValueDistribution.ParameterDescriptiona•&e eprob.ExtremeValueDistribution.ParameterNamesprob.ExtremeValueDistribution.ParameterNames_•%c cprob.ExtremeValueDistribution.NumParametersprob.ExtremeValueDistribution.NumParameterse•$i iprob.ExtremeValueDistribution.DistributionNameprob.ExtremeValueDistribution.DistributionNameO•#S Sprob.ExtremeValueDistribution.sigmaprob.ExtremeValueDistribution.sigmaI•"M Mprob.ExtremeValueDistribution.muprob.ExtremeValueDistribution.muC•!G Gprob.ExtremeValueDistributionprob.ExtremeValueDistributionI• M Mprob.ExponentialDistribution.varprob.ExponentialDistribution.varS•W Wprob.ExponentialDistribution.truncateprob.ExponentialDistribution.truncateI•M Mprob.ExponentialDistribution.stdprob.ExponentialDistribution.stdO•S Sprob.ExponentialDistribution.randomprob.ExponentialDistribution.randomQ•U Uprob.ExponentialDistribution.proflikprob.ExponentialDistribution.proflikK•O Oprob.ExponentialDistribution.plotprob.ExponentialDistribution.plotI•M Mprob.ExponentialDistribution.pdfprob.ExponentialDistribution.pdfQ•U Uprob.ExponentialDistribution.paramciprob.ExponentialDistribution.paramciU•Y Yprob.ExponentialDistribution.negloglikprob.ExponentialDistribution.negloglikO•S Sprob.ExponentialDistribution.medianprob.ExponentialDistribution.medianK•O Oprob.ExponentialDistribution.meanprob.ExponentialDistribution.meanI•M Mprob.ExponentialDistribution.iqrprob.ExponentialDistribution.iqr *ª²b¨\ Ú š X  Æ ~ > ü ´ n . ä ¤ H è €  (®%©7Ã<¾Nê„ ºPàtªc•mg gprob.GeneralizedExtremeValueDistribution.plotprob.GeneralizedExtremeValueDistribution.plota•le eprob.GeneralizedExtremeValueDistribution.pdfprob.GeneralizedExtremeValueDistribution.pdfi•km mprob.GeneralizedExtremeValueDistribution.paramciprob.GeneralizedExtremeValueDistribution.paramcim•jq qprob.GeneralizedExtremeValueDistribution.negloglikprob.GeneralizedExtremeValueDistribution.negloglikg•ik kprob.GeneralizedExtremeValueDistribution.medianprob.GeneralizedExtremeValueDistribution.medianc•hg gprob.GeneralizedExtremeValueDistribution.meanprob.GeneralizedExtremeValueDistribution.meana•ge eprob.GeneralizedExtremeValueDistribution.iqrprob.GeneralizedExtremeValueDistribution.iqrc•fg gprob.GeneralizedExtremeValueDistribution.icdfprob.GeneralizedExtremeValueDistribution.icdfa•ee eprob.GeneralizedExtremeValueDistribution.cdfprob.GeneralizedExtremeValueDistribution.cdfm•dq qprob.GeneralizedExtremeValueDistribution.InputDataprob.GeneralizedExtremeValueDistribution.InputData{•c prob.GeneralizedExtremeValueDistribution.ParameterIsFixedprob.GeneralizedExtremeValueDistribution.ParameterIsFixed�•b � �prob.GeneralizedExtremeValueDistribution.ParameterCovarianceprob.GeneralizedExtremeValueDistribution.ParameterCovarianceq•au uprob.GeneralizedExtremeValueDistribution.IsTruncatedprob.GeneralizedExtremeValueDistribution.IsTruncatedo•`s sprob.GeneralizedExtremeValueDistribution.Truncationprob.GeneralizedExtremeValueDistribution.Truncationy•_} }prob.GeneralizedExtremeValueDistribution.ParameterValuesprob.GeneralizedExtremeValueDistribution.ParameterValues�•^ � �prob.GeneralizedExtremeValueDistribution.ParameterDescriptionprob.GeneralizedExtremeValueDistribution.ParameterDescriptionw•]{ {prob.GeneralizedExtremeValueDistribution.ParameterNamesprob.GeneralizedExtremeValueDistribution.ParameterNamesu•\y yprob.GeneralizedExtremeValueDistribution.NumParametersprob.GeneralizedExtremeValueDistribution.NumParameters{•[ prob.GeneralizedExtremeValueDistribution.DistributionNameprob.GeneralizedExtremeValueDistribution.DistributionName_•Zc cprob.GeneralizedExtremeValueDistribution.muprob.GeneralizedExtremeValueDistribution.mue•Yi iprob.GeneralizedExtremeValueDistribution.sigmaprob.GeneralizedExtremeValueDistribution.sigma]•Xa aprob.GeneralizedExtremeValueDistribution.kprob.GeneralizedExtremeValueDistribution.kY•W] ]prob.GeneralizedExtremeValueDistributionprob.GeneralizedExtremeValueDistribution=•VA Aprob.GammaDistribution.varprob.GammaDistribution.varG•UK Kprob.GammaDistribution.truncateprob.GammaDistribution.truncate=•TA Aprob.GammaDistribution.stdprob.GammaDistribution.stdC•SG Gprob.GammaDistribution.randomprob.GammaDistribution.randomE•RI Iprob.GammaDistribution.proflikprob.GammaDistribution.proflik?•QC Cprob.GammaDistribution.plotprob.GammaDistribution.plot=•PA Aprob.GammaDistribution.pdfprob.GammaDistribution.pdfE•OI Iprob.GammaDistribution.paramciprob.GammaDistribution.paramciI•NM Mprob.GammaDistribution.negloglikprob.GammaDistribution.negloglikC•MG Gprob.GammaDistribution.medianprob.GammaDistribution.median?•LC Cprob.GammaDistribution.meanprob.GammaDistribution.mean=•KA Aprob.GammaDistribution.iqrprob.GammaDistribution.iqr?•JC Cprob.GammaDistribution.icdfprob.GammaDistribution.icdf=•IA Aprob.GammaDistribution.cdfprob.GammaDistribution.cdfI•HM Mprob.GammaDistribution.InputDataprob.GammaDistribution.InputDataW•G[ [prob.GammaDistribution.ParameterIsFixedprob.GammaDistribution.ParameterIsFixed]•Fa aprob.GammaDistribution.ParameterCovarianceprob.GammaDistribution.ParameterCovarianceM•EQ Qprob.GammaDistribution.IsTruncatedprob.GammaDistribution.IsTruncatedK•DO Oprob.GammaDistribution.Truncationprob.GammaDistribution.Truncation )v”*ÆX ô ¤ P ô ˜ & º L Ò b ü ” ªFî”<â„ Àh®Pø–>ü´f¤DØv_–c cprob.HalfNormalDistribution.ParameterValues!prob.HalfNormalDistribution.ParameterValuesi–m mprob.HalfNormalDistribution.ParameterDescription!prob.HalfNormalDistribution.ParameterDescription]–a aprob.HalfNormalDistribution.ParameterNames!prob.HalfNormalDistribution.ParameterNames[–_ _prob.HalfNormalDistribution.NumParameters!prob.HalfNormalDistribution.NumParametersa–e eprob.HalfNormalDistribution.DistributionName!prob.HalfNormalDistribution.DistributionNameK–O Oprob.HalfNormalDistribution.sigma!prob.HalfNormalDistribution.sigmaE–I Iprob.HalfNormalDistribution.mu!prob.HalfNormalDistribution.mu?–C Cprob.HalfNormalDistribution prob.HalfNormalDistributionU–Y Yprob.GeneralizedParetoDistribution.varprob.GeneralizedParetoDistribution.var_– c cprob.GeneralizedParetoDistribution.truncateprob.GeneralizedParetoDistribution.truncateU– Y Yprob.GeneralizedParetoDistribution.stdprob.GeneralizedParetoDistribution.std[– _ _prob.GeneralizedParetoDistribution.randomprob.GeneralizedParetoDistribution.random]– a aprob.GeneralizedParetoDistribution.proflikprob.GeneralizedParetoDistribution.proflikW– [ [prob.GeneralizedParetoDistribution.plotprob.GeneralizedParetoDistribution.plotU–Y Yprob.GeneralizedParetoDistribution.pdfprob.GeneralizedParetoDistribution.pdf]–a aprob.GeneralizedParetoDistribution.paramciprob.GeneralizedParetoDistribution.paramcia–e eprob.GeneralizedParetoDistribution.negloglikprob.GeneralizedParetoDistribution.negloglik[–_ _prob.GeneralizedParetoDistribution.medianprob.GeneralizedParetoDistribution.medianW–[ [prob.GeneralizedParetoDistribution.meanprob.GeneralizedParetoDistribution.meanU–Y Yprob.GeneralizedParetoDistribution.iqrprob.GeneralizedParetoDistribution.iqrW–[ [prob.GeneralizedParetoDistribution.icdfprob.GeneralizedParetoDistribution.icdfU–Y Yprob.GeneralizedParetoDistribution.cdfprob.GeneralizedParetoDistribution.cdfa–e eprob.GeneralizedParetoDistribution.InputDataprob.GeneralizedParetoDistribution.InputDatao•s sprob.GeneralizedParetoDistribution.ParameterIsFixedprob.GeneralizedParetoDistribution.ParameterIsFixedu•~y yprob.GeneralizedParetoDistribution.ParameterCovarianceprob.GeneralizedParetoDistribution.ParameterCovariancee•}i iprob.GeneralizedParetoDistribution.IsTruncatedprob.GeneralizedParetoDistribution.IsTruncatedc•|g gprob.GeneralizedParetoDistribution.Truncationprob.GeneralizedParetoDistribution.Truncationm•{q qprob.GeneralizedParetoDistribution.ParameterValuesprob.GeneralizedParetoDistribution.ParameterValuesw•z{ {prob.GeneralizedParetoDistribution.ParameterDescriptionprob.GeneralizedParetoDistribution.ParameterDescriptionk•yo oprob.GeneralizedParetoDistribution.ParameterNamesprob.GeneralizedParetoDistribution.ParameterNamesi•xm mprob.GeneralizedParetoDistribution.NumParametersprob.GeneralizedParetoDistribution.NumParameterso•ws sprob.GeneralizedParetoDistribution.DistributionNameprob.GeneralizedParetoDistribution.DistributionNameY•v] ]prob.GeneralizedParetoDistribution.thetaprob.GeneralizedParetoDistribution.thetaY•u] ]prob.GeneralizedParetoDistribution.sigmaprob.GeneralizedParetoDistribution.sigmaQ•tU Uprob.GeneralizedParetoDistribution.kprob.GeneralizedParetoDistribution.kM•sQ Qprob.GeneralizedParetoDistributionprob.GeneralizedParetoDistributiona•re eprob.GeneralizedExtremeValueDistribution.varprob.GeneralizedExtremeValueDistribution.vark•qo oprob.GeneralizedExtremeValueDistribution.truncateprob.GeneralizedExtremeValueDistribution.truncatea•pe eprob.GeneralizedExtremeValueDistribution.stdprob.GeneralizedExtremeValueDistribution.stdg•ok kprob.GeneralizedExtremeValueDistribution.randomprob.GeneralizedExtremeValueDistribution.randomi•nm mprob.GeneralizedExtremeValueDistribution.proflikprob.GeneralizedExtremeValueDistribution.proflik ,¦¨Nä€* à ” J þ ® X  ¼ p  Î „ 0 æ š H8Ìj’$ÄpÆp¶Z°Tú¦Q–BU Uprob.InverseGaussianDistribution.std%prob.InverseGaussianDistribution.stdW–A[ [prob.InverseGaussianDistribution.random%prob.InverseGaussianDistribution.randomY–@] ]prob.InverseGaussianDistribution.proflik%prob.InverseGaussianDistribution.proflikS–?W Wprob.InverseGaussianDistribution.plot%prob.InverseGaussianDistribution.plotQ–>U Uprob.InverseGaussianDistribution.pdf%prob.InverseGaussianDistribution.pdfY–=] ]prob.InverseGaussianDistribution.paramci%prob.InverseGaussianDistribution.paramci]–<a aprob.InverseGaussianDistribution.negloglik%prob.InverseGaussianDistribution.negloglikW–;[ [prob.InverseGaussianDistribution.median%prob.InverseGaussianDistribution.medianS–:W Wprob.InverseGaussianDistribution.mean%prob.InverseGaussianDistribution.meanQ–9U Uprob.InverseGaussianDistribution.iqr%prob.InverseGaussianDistribution.iqrS–8W Wprob.InverseGaussianDistribution.icdf%prob.InverseGaussianDistribution.icdfQ–7U Uprob.InverseGaussianDistribution.cdf%prob.InverseGaussianDistribution.cdf]–6a aprob.InverseGaussianDistribution.InputData$prob.InverseGaussianDistribution.InputDatak–5o oprob.InverseGaussianDistribution.ParameterIsFixed$prob.InverseGaussianDistribution.ParameterIsFixedq–4u uprob.InverseGaussianDistribution.ParameterCovariance$prob.InverseGaussianDistribution.ParameterCovariancea–3e eprob.InverseGaussianDistribution.IsTruncated$prob.InverseGaussianDistribution.IsTruncated_–2c cprob.InverseGaussianDistribution.Truncation$prob.InverseGaussianDistribution.Truncationi–1m mprob.InverseGaussianDistribution.ParameterValues$prob.InverseGaussianDistribution.ParameterValuess–0w wprob.InverseGaussianDistribution.ParameterDescription$prob.InverseGaussianDistribution.ParameterDescriptiong–/k kprob.InverseGaussianDistribution.ParameterNames$prob.InverseGaussianDistribution.ParameterNamese–.i iprob.InverseGaussianDistribution.NumParameters$prob.InverseGaussianDistribution.NumParametersk–-o oprob.InverseGaussianDistribution.DistributionName$prob.InverseGaussianDistribution.DistributionNameW–,[ [prob.InverseGaussianDistribution.lambda$prob.InverseGaussianDistribution.lambdaO–+S Sprob.InverseGaussianDistribution.mu$prob.InverseGaussianDistribution.muI–*M Mprob.InverseGaussianDistribution#prob.InverseGaussianDistributionG–)K Kprob.HalfNormalDistribution.var"prob.HalfNormalDistribution.varQ–(U Uprob.HalfNormalDistribution.truncate"prob.HalfNormalDistribution.truncateG–'K Kprob.HalfNormalDistribution.std"prob.HalfNormalDistribution.stdM–&Q Qprob.HalfNormalDistribution.random"prob.HalfNormalDistribution.randomO–%S Sprob.HalfNormalDistribution.proflik"prob.HalfNormalDistribution.proflikI–$M Mprob.HalfNormalDistribution.plot"prob.HalfNormalDistribution.plotG–#K Kprob.HalfNormalDistribution.pdf"prob.HalfNormalDistribution.pdfO–"S Sprob.HalfNormalDistribution.paramci"prob.HalfNormalDistribution.paramciS–!W Wprob.HalfNormalDistribution.negloglik"prob.HalfNormalDistribution.negloglikM– Q Qprob.HalfNormalDistribution.median"prob.HalfNormalDistribution.medianI–M Mprob.HalfNormalDistribution.mean"prob.HalfNormalDistribution.meanG–K Kprob.HalfNormalDistribution.iqr"prob.HalfNormalDistribution.iqrI–M Mprob.HalfNormalDistribution.icdf"prob.HalfNormalDistribution.icdfG–K Kprob.HalfNormalDistribution.cdf"prob.HalfNormalDistribution.cdfS–W Wprob.HalfNormalDistribution.InputData!prob.HalfNormalDistribution.InputDataa–e eprob.HalfNormalDistribution.ParameterIsFixed!prob.HalfNormalDistribution.ParameterIsFixedg–k kprob.HalfNormalDistribution.ParameterCovariance!prob.HalfNormalDistribution.ParameterCovarianceW–[ [prob.HalfNormalDistribution.IsTruncated!prob.HalfNormalDistribution.IsTruncatedU–Y Yprob.HalfNormalDistribution.Truncation!prob.HalfNormalDistribution.Truncation 3¤¢N¸p" Ø ˆ 6 è ¦ b Ü ” F  À x 6 ê ¨ j &Ü|"Æ^¬Vð�>ø°j"Ö„6ð¨ZÈx2î¤G–uK Kprob.LoglogisticDistribution.mu-prob.LoglogisticDistribution.muA–tE Eprob.LoglogisticDistribution,prob.LoglogisticDistributionC–sG Gprob.LogisticDistribution.var+prob.LogisticDistribution.varM–rQ Qprob.LogisticDistribution.truncate+prob.LogisticDistribution.truncateC–qG Gprob.LogisticDistribution.std+prob.LogisticDistribution.stdI–pM Mprob.LogisticDistribution.random+prob.LogisticDistribution.randomK–oO Oprob.LogisticDistribution.proflik+prob.LogisticDistribution.proflikE–nI Iprob.LogisticDistribution.plot+prob.LogisticDistribution.plotC–mG Gprob.LogisticDistribution.pdf+prob.LogisticDistribution.pdfK–lO Oprob.LogisticDistribution.paramci+prob.LogisticDistribution.paramciO–kS Sprob.LogisticDistribution.negloglik+prob.LogisticDistribution.negloglikI–jM Mprob.LogisticDistribution.median+prob.LogisticDistribution.medianE–iI Iprob.LogisticDistribution.mean+prob.LogisticDistribution.meanC–hG Gprob.LogisticDistribution.iqr+prob.LogisticDistribution.iqrE–gI Iprob.LogisticDistribution.icdf+prob.LogisticDistribution.icdfC–fG Gprob.LogisticDistribution.cdf+prob.LogisticDistribution.cdfO–eS Sprob.LogisticDistribution.InputData*prob.LogisticDistribution.InputData]–da aprob.LogisticDistribution.ParameterIsFixed*prob.LogisticDistribution.ParameterIsFixedc–cg gprob.LogisticDistribution.ParameterCovariance*prob.LogisticDistribution.ParameterCovarianceS–bW Wprob.LogisticDistribution.IsTruncated*prob.LogisticDistribution.IsTruncatedQ–aU Uprob.LogisticDistribution.Truncation*prob.LogisticDistribution.Truncation[–`_ _prob.LogisticDistribution.ParameterValues*prob.LogisticDistribution.ParameterValuese–_i iprob.LogisticDistribution.ParameterDescription*prob.LogisticDistribution.ParameterDescriptionY–^] ]prob.LogisticDistribution.ParameterNames*prob.LogisticDistribution.ParameterNamesW–][ [prob.LogisticDistribution.NumParameters*prob.LogisticDistribution.NumParameters]–\a aprob.LogisticDistribution.DistributionName*prob.LogisticDistribution.DistributionNameG–[K Kprob.LogisticDistribution.sigma*prob.LogisticDistribution.sigmaA–ZE Eprob.LogisticDistribution.mu*prob.LogisticDistribution.mu;–Y? ?prob.LogisticDistribution)prob.LogisticDistribution?–XC Cprob.KernelDistribution.var(prob.KernelDistribution.varI–WM Mprob.KernelDistribution.truncate(prob.KernelDistribution.truncate?–VC Cprob.KernelDistribution.std(prob.KernelDistribution.stdE–UI Iprob.KernelDistribution.random(prob.KernelDistribution.randomA–TE Eprob.KernelDistribution.plot(prob.KernelDistribution.plot?–SC Cprob.KernelDistribution.pdf(prob.KernelDistribution.pdfK–RO Oprob.KernelDistribution.negloglik(prob.KernelDistribution.negloglikE–QI Iprob.KernelDistribution.median(prob.KernelDistribution.medianA–PE Eprob.KernelDistribution.mean(prob.KernelDistribution.mean?–OC Cprob.KernelDistribution.iqr(prob.KernelDistribution.iqrA–NE Eprob.KernelDistribution.icdf(prob.KernelDistribution.icdf?–MC Cprob.KernelDistribution.cdf(prob.KernelDistribution.cdfK–LO Oprob.KernelDistribution.InputData'prob.KernelDistribution.InputDataO–KS Sprob.KernelDistribution.IsTruncated'prob.KernelDistribution.IsTruncatedM–JQ Qprob.KernelDistribution.Truncation'prob.KernelDistribution.TruncationG–IK Kprob.KernelDistribution.Support'prob.KernelDistribution.SupportK–HO Oprob.KernelDistribution.Bandwidth'prob.KernelDistribution.BandwidthE–GI Iprob.KernelDistribution.Kernel'prob.KernelDistribution.KernelY–F] ]prob.KernelDistribution.DistributionName'prob.KernelDistribution.DistributionName7–E; ;prob.KernelDistribution&prob.KernelDistributionQ–DU Uprob.InverseGaussianDistribution.var%prob.InverseGaussianDistribution.var[–C_ _prob.InverseGaussianDistribution.truncate%prob.InverseGaussianDistribution.truncate .”°Jêˆ ¶ \ ” . Ö Š < ð ¢ P ø ¤ X ¶dÂv6ð¤B戾h¨Fòª`΀,Ü”E—#I Iprob.LognormalDistribution.pdf1prob.LognormalDistribution.pdfM—"Q Qprob.LognormalDistribution.paramci1prob.LognormalDistribution.paramciQ—!U Uprob.LognormalDistribution.negloglik1prob.LognormalDistribution.negloglikK— O Oprob.LognormalDistribution.median1prob.LognormalDistribution.medianG—K Kprob.LognormalDistribution.mean1prob.LognormalDistribution.meanE—I Iprob.LognormalDistribution.iqr1prob.LognormalDistribution.iqrG—K Kprob.LognormalDistribution.icdf1prob.LognormalDistribution.icdfE—I Iprob.LognormalDistribution.cdf1prob.LognormalDistribution.cdfQ—U Uprob.LognormalDistribution.InputData0prob.LognormalDistribution.InputData_—c cprob.LognormalDistribution.ParameterIsFixed0prob.LognormalDistribution.ParameterIsFixede—i iprob.LognormalDistribution.ParameterCovariance0prob.LognormalDistribution.ParameterCovarianceU—Y Yprob.LognormalDistribution.IsTruncated0prob.LognormalDistribution.IsTruncatedS—W Wprob.LognormalDistribution.Truncation0prob.LognormalDistribution.Truncation]—a aprob.LognormalDistribution.ParameterValues0prob.LognormalDistribution.ParameterValuesg—k kprob.LognormalDistribution.ParameterDescription0prob.LognormalDistribution.ParameterDescription[—_ _prob.LognormalDistribution.ParameterNames0prob.LognormalDistribution.ParameterNamesY—] ]prob.LognormalDistribution.NumParameters0prob.LognormalDistribution.NumParameters_—c cprob.LognormalDistribution.DistributionName0prob.LognormalDistribution.DistributionNameI—M Mprob.LognormalDistribution.sigma0prob.LognormalDistribution.sigmaC—G Gprob.LognormalDistribution.mu0prob.LognormalDistribution.mu=—A Aprob.LognormalDistribution/prob.LognormalDistributionI—M Mprob.LoglogisticDistribution.var.prob.LoglogisticDistribution.varS— W Wprob.LoglogisticDistribution.truncate.prob.LoglogisticDistribution.truncateI— M Mprob.LoglogisticDistribution.std.prob.LoglogisticDistribution.stdO— S Sprob.LoglogisticDistribution.random.prob.LoglogisticDistribution.randomQ— U Uprob.LoglogisticDistribution.proflik.prob.LoglogisticDistribution.proflikK— O Oprob.LoglogisticDistribution.plot.prob.LoglogisticDistribution.plotI—M Mprob.LoglogisticDistribution.pdf.prob.LoglogisticDistribution.pdfQ—U Uprob.LoglogisticDistribution.paramci.prob.LoglogisticDistribution.paramciU—Y Yprob.LoglogisticDistribution.negloglik.prob.LoglogisticDistribution.negloglikO—S Sprob.LoglogisticDistribution.median.prob.LoglogisticDistribution.medianK—O Oprob.LoglogisticDistribution.mean.prob.LoglogisticDistribution.meanI—M Mprob.LoglogisticDistribution.iqr.prob.LoglogisticDistribution.iqrK—O Oprob.LoglogisticDistribution.icdf.prob.LoglogisticDistribution.icdfI—M Mprob.LoglogisticDistribution.cdf.prob.LoglogisticDistribution.cdfU—Y Yprob.LoglogisticDistribution.InputData-prob.LoglogisticDistribution.InputDatac–g gprob.LoglogisticDistribution.ParameterIsFixed-prob.LoglogisticDistribution.ParameterIsFixedi–~m mprob.LoglogisticDistribution.ParameterCovariance-prob.LoglogisticDistribution.ParameterCovarianceY–}] ]prob.LoglogisticDistribution.IsTruncated-prob.LoglogisticDistribution.IsTruncatedW–|[ [prob.LoglogisticDistribution.Truncation-prob.LoglogisticDistribution.Truncationa–{e eprob.LoglogisticDistribution.ParameterValues-prob.LoglogisticDistribution.ParameterValuesk–zo oprob.LoglogisticDistribution.ParameterDescription-prob.LoglogisticDistribution.ParameterDescription_–yc cprob.LoglogisticDistribution.ParameterNames-prob.LoglogisticDistribution.ParameterNames]–xa aprob.LoglogisticDistribution.NumParameters-prob.LoglogisticDistribution.NumParametersc–wg gprob.LoglogisticDistribution.DistributionName-prob.LoglogisticDistribution.DistributionNameM–vQ Qprob.LoglogisticDistribution.sigma-prob.LoglogisticDistribution.sigma 0z¶fÐ~6 ô ¦ X ô – 6 Ê h  ¶ l Ö Š :ð¤T ¶l(Èb 2ÎtÌ~2ä’Fø¦Z¸z;—S? ?prob.NakagamiDistribution8prob.NakagamiDistributionI—RM Mprob.MultinomialDistribution.var7prob.MultinomialDistribution.varS—QW Wprob.MultinomialDistribution.truncate7prob.MultinomialDistribution.truncateI—PM Mprob.MultinomialDistribution.std7prob.MultinomialDistribution.stdO—OS Sprob.MultinomialDistribution.random7prob.MultinomialDistribution.randomK—NO Oprob.MultinomialDistribution.plot7prob.MultinomialDistribution.plotI—MM Mprob.MultinomialDistribution.pdf7prob.MultinomialDistribution.pdfO—LS Sprob.MultinomialDistribution.median7prob.MultinomialDistribution.medianK—KO Oprob.MultinomialDistribution.mean7prob.MultinomialDistribution.meanI—JM Mprob.MultinomialDistribution.iqr7prob.MultinomialDistribution.iqrK—IO Oprob.MultinomialDistribution.icdf7prob.MultinomialDistribution.icdfI—HM Mprob.MultinomialDistribution.cdf7prob.MultinomialDistribution.cdfY—G] ]prob.MultinomialDistribution.IsTruncated6prob.MultinomialDistribution.IsTruncatedW—F[ [prob.MultinomialDistribution.Truncation6prob.MultinomialDistribution.Truncationa—Ee eprob.MultinomialDistribution.ParameterValues6prob.MultinomialDistribution.ParameterValuesk—Do oprob.MultinomialDistribution.ParameterDescription6prob.MultinomialDistribution.ParameterDescription_—Cc cprob.MultinomialDistribution.ParameterNames6prob.MultinomialDistribution.ParameterNames]—Ba aprob.MultinomialDistribution.NumParameters6prob.MultinomialDistribution.NumParametersc—Ag gprob.MultinomialDistribution.DistributionName6prob.MultinomialDistribution.DistributionName]—@a aprob.MultinomialDistribution.Probabilities6prob.MultinomialDistribution.ProbabilitiesA—?E Eprob.MultinomialDistribution5prob.MultinomialDistributionG—>K Kprob.LoguniformDistribution.var4prob.LoguniformDistribution.varQ—=U Uprob.LoguniformDistribution.truncate4prob.LoguniformDistribution.truncateG—<K Kprob.LoguniformDistribution.std4prob.LoguniformDistribution.stdM—;Q Qprob.LoguniformDistribution.random4prob.LoguniformDistribution.randomI—:M Mprob.LoguniformDistribution.plot4prob.LoguniformDistribution.plotG—9K Kprob.LoguniformDistribution.pdf4prob.LoguniformDistribution.pdfM—8Q Qprob.LoguniformDistribution.median4prob.LoguniformDistribution.medianI—7M Mprob.LoguniformDistribution.mean4prob.LoguniformDistribution.meanG—6K Kprob.LoguniformDistribution.iqr4prob.LoguniformDistribution.iqrI—5M Mprob.LoguniformDistribution.icdf4prob.LoguniformDistribution.icdfG—4K Kprob.LoguniformDistribution.cdf4prob.LoguniformDistribution.cdfW—3[ [prob.LoguniformDistribution.IsTruncated3prob.LoguniformDistribution.IsTruncatedU—2Y Yprob.LoguniformDistribution.Truncation3prob.LoguniformDistribution.Truncation_—1c cprob.LoguniformDistribution.ParameterValues3prob.LoguniformDistribution.ParameterValuesi—0m mprob.LoguniformDistribution.ParameterDescription3prob.LoguniformDistribution.ParameterDescription]—/a aprob.LoguniformDistribution.ParameterNames3prob.LoguniformDistribution.ParameterNames[—._ _prob.LoguniformDistribution.NumParameters3prob.LoguniformDistribution.NumParametersa—-e eprob.LoguniformDistribution.DistributionName3prob.LoguniformDistribution.DistributionNameK—,O Oprob.LoguniformDistribution.Upper3prob.LoguniformDistribution.UpperK—+O Oprob.LoguniformDistribution.Lower3prob.LoguniformDistribution.Lower?—*C Cprob.LoguniformDistribution2prob.LoguniformDistributionE—)I Iprob.LognormalDistribution.var1prob.LognormalDistribution.varO—(S Sprob.LognormalDistribution.truncate1prob.LognormalDistribution.truncateE—'I Iprob.LognormalDistribution.std1prob.LognormalDistribution.stdK—&O Oprob.LognormalDistribution.random1prob.LognormalDistribution.randomM—%Q Qprob.LognormalDistribution.proflik1prob.LognormalDistribution.proflikG—$K Kprob.LognormalDistribution.plot1prob.LognormalDistribution.plot -~¼r¸\ ô – B ì † & Ô Ž F ¸ l  Ì † >ð¤^Èz(Öfü�ªFàjú˜Bê”<à~_˜c cprob.NegativeBinomialDistribution.negloglik=prob.NegativeBinomialDistribution.negloglikY—] ]prob.NegativeBinomialDistribution.median=prob.NegativeBinomialDistribution.medianU—~Y Yprob.NegativeBinomialDistribution.mean=prob.NegativeBinomialDistribution.meanS—}W Wprob.NegativeBinomialDistribution.iqr=prob.NegativeBinomialDistribution.iqrU—|Y Yprob.NegativeBinomialDistribution.icdf=prob.NegativeBinomialDistribution.icdfS—{W Wprob.NegativeBinomialDistribution.cdf=prob.NegativeBinomialDistribution.cdf_—zc cprob.NegativeBinomialDistribution.InputDataprob.NormalDistributionS˜W Wprob.NegativeBinomialDistribution.var=prob.NegativeBinomialDistribution.var]˜a aprob.NegativeBinomialDistribution.truncate=prob.NegativeBinomialDistribution.truncateS˜W Wprob.NegativeBinomialDistribution.std=prob.NegativeBinomialDistribution.stdY˜] ]prob.NegativeBinomialDistribution.random=prob.NegativeBinomialDistribution.random[˜_ _prob.NegativeBinomialDistribution.proflik=prob.NegativeBinomialDistribution.proflikU˜Y Yprob.NegativeBinomialDistribution.plot=prob.NegativeBinomialDistribution.plotS˜W Wprob.NegativeBinomialDistribution.pdf=prob.NegativeBinomialDistribution.pdf[˜_ _prob.NegativeBinomialDistribution.paramci=prob.NegativeBinomialDistribution.paramci 0ެVü¨R ø ¤ F ò ¶ l  ¶ \ ö š H ô � 2âžX΄4è¤^È„6ò´r¸\ô–Bì†&ÔŽC˜_G Gprob.RayleighDistribution.cdfIprob.RayleighDistribution.cdfO˜^S Sprob.RayleighDistribution.InputDataHprob.RayleighDistribution.InputData]˜]a aprob.RayleighDistribution.ParameterIsFixedHprob.RayleighDistribution.ParameterIsFixedc˜\g gprob.RayleighDistribution.ParameterCovarianceHprob.RayleighDistribution.ParameterCovarianceS˜[W Wprob.RayleighDistribution.IsTruncatedHprob.RayleighDistribution.IsTruncatedQ˜ZU Uprob.RayleighDistribution.TruncationHprob.RayleighDistribution.Truncation[˜Y_ _prob.RayleighDistribution.ParameterValuesHprob.RayleighDistribution.ParameterValuese˜Xi iprob.RayleighDistribution.ParameterDescriptionHprob.RayleighDistribution.ParameterDescriptionY˜W] ]prob.RayleighDistribution.ParameterNamesHprob.RayleighDistribution.ParameterNamesW˜V[ [prob.RayleighDistribution.NumParametersHprob.RayleighDistribution.NumParameters]˜Ua aprob.RayleighDistribution.DistributionNameHprob.RayleighDistribution.DistributionName?˜TC Cprob.RayleighDistribution.BHprob.RayleighDistribution.B;˜S? ?prob.RayleighDistributionGprob.RayleighDistributionA˜RE Eprob.PoissonDistribution.varFprob.PoissonDistribution.varK˜QO Oprob.PoissonDistribution.truncateFprob.PoissonDistribution.truncateA˜PE Eprob.PoissonDistribution.stdFprob.PoissonDistribution.stdG˜OK Kprob.PoissonDistribution.randomFprob.PoissonDistribution.randomI˜NM Mprob.PoissonDistribution.proflikFprob.PoissonDistribution.proflikC˜MG Gprob.PoissonDistribution.plotFprob.PoissonDistribution.plotA˜LE Eprob.PoissonDistribution.pdfFprob.PoissonDistribution.pdfI˜KM Mprob.PoissonDistribution.paramciFprob.PoissonDistribution.paramciM˜JQ Qprob.PoissonDistribution.negloglikFprob.PoissonDistribution.negloglikG˜IK Kprob.PoissonDistribution.medianFprob.PoissonDistribution.medianC˜HG Gprob.PoissonDistribution.meanFprob.PoissonDistribution.meanA˜GE Eprob.PoissonDistribution.iqrFprob.PoissonDistribution.iqrC˜FG Gprob.PoissonDistribution.icdfFprob.PoissonDistribution.icdfA˜EE Eprob.PoissonDistribution.cdfFprob.PoissonDistribution.cdfM˜DQ Qprob.PoissonDistribution.InputDataEprob.PoissonDistribution.InputData[˜C_ _prob.PoissonDistribution.ParameterIsFixedEprob.PoissonDistribution.ParameterIsFixeda˜Be eprob.PoissonDistribution.ParameterCovarianceEprob.PoissonDistribution.ParameterCovarianceQ˜AU Uprob.PoissonDistribution.IsTruncatedEprob.PoissonDistribution.IsTruncatedO˜@S Sprob.PoissonDistribution.TruncationEprob.PoissonDistribution.TruncationY˜?] ]prob.PoissonDistribution.ParameterValuesEprob.PoissonDistribution.ParameterValuesc˜>g gprob.PoissonDistribution.ParameterDescriptionEprob.PoissonDistribution.ParameterDescriptionW˜=[ [prob.PoissonDistribution.ParameterNamesEprob.PoissonDistribution.ParameterNamesU˜<Y Yprob.PoissonDistribution.NumParametersEprob.PoissonDistribution.NumParameters[˜;_ _prob.PoissonDistribution.DistributionNameEprob.PoissonDistribution.DistributionNameG˜:K Kprob.PoissonDistribution.lambdaEprob.PoissonDistribution.lambda9˜9= =prob.PoissonDistributionDprob.PoissonDistributionQ˜8U Uprob.PiecewiseLinearDistribution.varCprob.PiecewiseLinearDistribution.var[˜7_ _prob.PiecewiseLinearDistribution.truncateCprob.PiecewiseLinearDistribution.truncateQ˜6U Uprob.PiecewiseLinearDistribution.stdCprob.PiecewiseLinearDistribution.stdW˜5[ [prob.PiecewiseLinearDistribution.randomCprob.PiecewiseLinearDistribution.randomS˜4W Wprob.PiecewiseLinearDistribution.plotCprob.PiecewiseLinearDistribution.plotQ˜3U Uprob.PiecewiseLinearDistribution.pdfCprob.PiecewiseLinearDistribution.pdfW˜2[ [prob.PiecewiseLinearDistribution.medianCprob.PiecewiseLinearDistribution.medianS˜1W Wprob.PiecewiseLinearDistribution.meanCprob.PiecewiseLinearDistribution.meanQ˜0U Uprob.PiecewiseLinearDistribution.iqrCprob.PiecewiseLinearDistribution.iqr 4|¸r*ÞŒ> ø ° b  Ð € :  | Ê r  ´ d °TÄ€>ú²dØ”JÀt2ø²n,æŠ4ÜxÎ|O™S Sprob.StableDistribution.IsTruncatedNprob.StableDistribution.IsTruncatedM™Q Qprob.StableDistribution.TruncationNprob.StableDistribution.TruncationW™[ [prob.StableDistribution.ParameterValuesNprob.StableDistribution.ParameterValuesa™e eprob.StableDistribution.ParameterDescriptionNprob.StableDistribution.ParameterDescriptionU™Y Yprob.StableDistribution.ParameterNamesNprob.StableDistribution.ParameterNamesS™W Wprob.StableDistribution.NumParametersNprob.StableDistribution.NumParametersY™ ] ]prob.StableDistribution.DistributionNameNprob.StableDistribution.DistributionNameC™ G Gprob.StableDistribution.deltaNprob.StableDistribution.delta?™ C Cprob.StableDistribution.gamNprob.StableDistribution.gamA™ E Eprob.StableDistribution.betaNprob.StableDistribution.betaC™ G Gprob.StableDistribution.alphaNprob.StableDistribution.alpha7™; ;prob.StableDistributionMprob.StableDistribution?™C Cprob.RicianDistribution.varLprob.RicianDistribution.varI™M Mprob.RicianDistribution.truncateLprob.RicianDistribution.truncate?™C Cprob.RicianDistribution.stdLprob.RicianDistribution.stdE™I Iprob.RicianDistribution.randomLprob.RicianDistribution.randomG™K Kprob.RicianDistribution.proflikLprob.RicianDistribution.proflikA™E Eprob.RicianDistribution.plotLprob.RicianDistribution.plot?™C Cprob.RicianDistribution.pdfLprob.RicianDistribution.pdfG™K Kprob.RicianDistribution.paramciLprob.RicianDistribution.paramciK˜O Oprob.RicianDistribution.negloglikLprob.RicianDistribution.negloglikE˜~I Iprob.RicianDistribution.medianLprob.RicianDistribution.medianA˜}E Eprob.RicianDistribution.meanLprob.RicianDistribution.mean?˜|C Cprob.RicianDistribution.iqrLprob.RicianDistribution.iqrA˜{E Eprob.RicianDistribution.icdfLprob.RicianDistribution.icdf?˜zC Cprob.RicianDistribution.cdfLprob.RicianDistribution.cdfK˜yO Oprob.RicianDistribution.InputDataKprob.RicianDistribution.InputDataY˜x] ]prob.RicianDistribution.ParameterIsFixedKprob.RicianDistribution.ParameterIsFixed_˜wc cprob.RicianDistribution.ParameterCovarianceKprob.RicianDistribution.ParameterCovarianceO˜vS Sprob.RicianDistribution.IsTruncatedKprob.RicianDistribution.IsTruncatedM˜uQ Qprob.RicianDistribution.TruncationKprob.RicianDistribution.TruncationW˜t[ [prob.RicianDistribution.ParameterValuesKprob.RicianDistribution.ParameterValuesa˜se eprob.RicianDistribution.ParameterDescriptionKprob.RicianDistribution.ParameterDescriptionU˜rY Yprob.RicianDistribution.ParameterNamesKprob.RicianDistribution.ParameterNamesS˜qW Wprob.RicianDistribution.NumParametersKprob.RicianDistribution.NumParametersY˜p] ]prob.RicianDistribution.DistributionNameKprob.RicianDistribution.DistributionNameC˜oG Gprob.RicianDistribution.sigmaKprob.RicianDistribution.sigma;˜n? ?prob.RicianDistribution.sKprob.RicianDistribution.s7˜m; ;prob.RicianDistributionJprob.RicianDistributionC˜lG Gprob.RayleighDistribution.varIprob.RayleighDistribution.varM˜kQ Qprob.RayleighDistribution.truncateIprob.RayleighDistribution.truncateC˜jG Gprob.RayleighDistribution.stdIprob.RayleighDistribution.stdI˜iM Mprob.RayleighDistribution.randomIprob.RayleighDistribution.randomK˜hO Oprob.RayleighDistribution.proflikIprob.RayleighDistribution.proflikE˜gI Iprob.RayleighDistribution.plotIprob.RayleighDistribution.plotC˜fG Gprob.RayleighDistribution.pdfIprob.RayleighDistribution.pdfK˜eO Oprob.RayleighDistribution.paramciIprob.RayleighDistribution.paramciO˜dS Sprob.RayleighDistribution.negloglikIprob.RayleighDistribution.negloglikI˜cM Mprob.RayleighDistribution.medianIprob.RayleighDistribution.medianE˜bI Iprob.RayleighDistribution.meanIprob.RayleighDistribution.meanC˜aG Gprob.RayleighDistribution.iqrIprob.RayleighDistribution.iqrE˜`I Iprob.RayleighDistribution.icdfIprob.RayleighDistribution.icdf /ŽžBô²n, è   R  Æ ‚ 8 ð ® b Ö † 0 à t ¦2Èh”(Êx$Ò~&ÈnÈnÄhÔŽC™BG Gprob.TriangularDistribution.ATprob.TriangularDistribution.A?™AC Cprob.TriangularDistributionSprob.TriangularDistributionO™@S Sprob.tLocationScaleDistribution.varRprob.tLocationScaleDistribution.varY™?] ]prob.tLocationScaleDistribution.truncateRprob.tLocationScaleDistribution.truncateO™>S Sprob.tLocationScaleDistribution.stdRprob.tLocationScaleDistribution.stdU™=Y Yprob.tLocationScaleDistribution.randomRprob.tLocationScaleDistribution.randomW™<[ [prob.tLocationScaleDistribution.proflikRprob.tLocationScaleDistribution.proflikQ™;U Uprob.tLocationScaleDistribution.plotRprob.tLocationScaleDistribution.plotO™:S Sprob.tLocationScaleDistribution.pdfRprob.tLocationScaleDistribution.pdfW™9[ [prob.tLocationScaleDistribution.paramciRprob.tLocationScaleDistribution.paramci[™8_ _prob.tLocationScaleDistribution.negloglikRprob.tLocationScaleDistribution.negloglikU™7Y Yprob.tLocationScaleDistribution.medianRprob.tLocationScaleDistribution.medianQ™6U Uprob.tLocationScaleDistribution.meanRprob.tLocationScaleDistribution.meanO™5S Sprob.tLocationScaleDistribution.iqrRprob.tLocationScaleDistribution.iqrQ™4U Uprob.tLocationScaleDistribution.icdfRprob.tLocationScaleDistribution.icdfO™3S Sprob.tLocationScaleDistribution.cdfRprob.tLocationScaleDistribution.cdf[™2_ _prob.tLocationScaleDistribution.InputDataQprob.tLocationScaleDistribution.InputDatai™1m mprob.tLocationScaleDistribution.ParameterIsFixedQprob.tLocationScaleDistribution.ParameterIsFixedo™0s sprob.tLocationScaleDistribution.ParameterCovarianceQprob.tLocationScaleDistribution.ParameterCovariance_™/c cprob.tLocationScaleDistribution.IsTruncatedQprob.tLocationScaleDistribution.IsTruncated]™.a aprob.tLocationScaleDistribution.TruncationQprob.tLocationScaleDistribution.Truncationg™-k kprob.tLocationScaleDistribution.ParameterValuesQprob.tLocationScaleDistribution.ParameterValuesq™,u uprob.tLocationScaleDistribution.ParameterDescriptionQprob.tLocationScaleDistribution.ParameterDescriptione™+i iprob.tLocationScaleDistribution.ParameterNamesQprob.tLocationScaleDistribution.ParameterNamesc™*g gprob.tLocationScaleDistribution.NumParametersQprob.tLocationScaleDistribution.NumParametersi™)m mprob.tLocationScaleDistribution.DistributionNameQprob.tLocationScaleDistribution.DistributionNameM™(Q Qprob.tLocationScaleDistribution.nuQprob.tLocationScaleDistribution.nuS™'W Wprob.tLocationScaleDistribution.sigmaQprob.tLocationScaleDistribution.sigmaM™&Q Qprob.tLocationScaleDistribution.muQprob.tLocationScaleDistribution.muG™%K Kprob.tLocationScaleDistributionPprob.tLocationScaleDistribution?™$C Cprob.StableDistribution.varOprob.StableDistribution.varI™#M Mprob.StableDistribution.truncateOprob.StableDistribution.truncate?™"C Cprob.StableDistribution.stdOprob.StableDistribution.stdE™!I Iprob.StableDistribution.randomOprob.StableDistribution.randomG™ K Kprob.StableDistribution.proflikOprob.StableDistribution.proflikA™E Eprob.StableDistribution.plotOprob.StableDistribution.plot?™C Cprob.StableDistribution.pdfOprob.StableDistribution.pdfG™K Kprob.StableDistribution.paramciOprob.StableDistribution.paramciK™O Oprob.StableDistribution.negloglikOprob.StableDistribution.negloglikE™I Iprob.StableDistribution.medianOprob.StableDistribution.medianA™E Eprob.StableDistribution.meanOprob.StableDistribution.mean?™C Cprob.StableDistribution.iqrOprob.StableDistribution.iqrA™E Eprob.StableDistribution.icdfOprob.StableDistribution.icdf?™C Cprob.StableDistribution.cdfOprob.StableDistribution.cdfK™O Oprob.StableDistribution.InputDataNprob.StableDistribution.InputDataY™] ]prob.StableDistribution.ParameterIsFixedNprob.StableDistribution.ParameterIsFixed_™c cprob.StableDistribution.ParameterCovarianceNprob.StableDistribution.ParameterCovariance 1®ºt²R æ „ , Ò ˆ < ò ¦ V À p & Ò ˆ L ¼^¬Fê˜Dºv0æ¢\΀<À€"Êp ®Y™s] ]prob.WeibullDistribution.ParameterValuesZprob.WeibullDistribution.ParameterValuesc™rg gprob.WeibullDistribution.ParameterDescriptionZprob.WeibullDistribution.ParameterDescriptionW™q[ [prob.WeibullDistribution.ParameterNamesZprob.WeibullDistribution.ParameterNamesU™pY Yprob.WeibullDistribution.NumParametersZprob.WeibullDistribution.NumParameters[™o_ _prob.WeibullDistribution.DistributionNameZprob.WeibullDistribution.DistributionName=™nA Aprob.WeibullDistribution.BZprob.WeibullDistribution.B=™mA Aprob.WeibullDistribution.AZprob.WeibullDistribution.A9™l= =prob.WeibullDistributionYprob.WeibullDistributionA™kE Eprob.UniformDistribution.varXprob.UniformDistribution.varK™jO Oprob.UniformDistribution.truncateXprob.UniformDistribution.truncateA™iE Eprob.UniformDistribution.stdXprob.UniformDistribution.stdG™hK Kprob.UniformDistribution.randomXprob.UniformDistribution.randomC™gG Gprob.UniformDistribution.plotXprob.UniformDistribution.plotA™fE Eprob.UniformDistribution.pdfXprob.UniformDistribution.pdfG™eK Kprob.UniformDistribution.medianXprob.UniformDistribution.medianC™dG Gprob.UniformDistribution.meanXprob.UniformDistribution.meanA™cE Eprob.UniformDistribution.iqrXprob.UniformDistribution.iqrC™bG Gprob.UniformDistribution.icdfXprob.UniformDistribution.icdfA™aE Eprob.UniformDistribution.cdfXprob.UniformDistribution.cdfQ™`U Uprob.UniformDistribution.IsTruncatedWprob.UniformDistribution.IsTruncatedO™_S Sprob.UniformDistribution.TruncationWprob.UniformDistribution.TruncationY™^] ]prob.UniformDistribution.ParameterValuesWprob.UniformDistribution.ParameterValuesc™]g gprob.UniformDistribution.ParameterDescriptionWprob.UniformDistribution.ParameterDescriptionW™\[ [prob.UniformDistribution.ParameterNamesWprob.UniformDistribution.ParameterNamesU™[Y Yprob.UniformDistribution.NumParametersWprob.UniformDistribution.NumParameters[™Z_ _prob.UniformDistribution.DistributionNameWprob.UniformDistribution.DistributionNameE™YI Iprob.UniformDistribution.UpperWprob.UniformDistribution.UpperE™XI Iprob.UniformDistribution.LowerWprob.UniformDistribution.Lower9™W= =prob.UniformDistributionVprob.UniformDistributionG™VK Kprob.TriangularDistribution.varUprob.TriangularDistribution.varQ™UU Uprob.TriangularDistribution.truncateUprob.TriangularDistribution.truncateG™TK Kprob.TriangularDistribution.stdUprob.TriangularDistribution.stdM™SQ Qprob.TriangularDistribution.randomUprob.TriangularDistribution.randomI™RM Mprob.TriangularDistribution.plotUprob.TriangularDistribution.plotG™QK Kprob.TriangularDistribution.pdfUprob.TriangularDistribution.pdfM™PQ Qprob.TriangularDistribution.medianUprob.TriangularDistribution.medianI™OM Mprob.TriangularDistribution.meanUprob.TriangularDistribution.meanG™NK Kprob.TriangularDistribution.iqrUprob.TriangularDistribution.iqrI™MM Mprob.TriangularDistribution.icdfUprob.TriangularDistribution.icdfG™LK Kprob.TriangularDistribution.cdfUprob.TriangularDistribution.cdfW™K[ [prob.TriangularDistribution.IsTruncatedTprob.TriangularDistribution.IsTruncatedU™JY Yprob.TriangularDistribution.TruncationTprob.TriangularDistribution.Truncation_™Ic cprob.TriangularDistribution.ParameterValuesTprob.TriangularDistribution.ParameterValuesi™Hm mprob.TriangularDistribution.ParameterDescriptionTprob.TriangularDistribution.ParameterDescription]™Ga aprob.TriangularDistribution.ParameterNamesTprob.TriangularDistribution.ParameterNames[™F_ _prob.TriangularDistribution.NumParametersTprob.TriangularDistribution.NumParametersa™Ee eprob.TriangularDistribution.DistributionNameTprob.TriangularDistribution.DistributionNameC™DG Gprob.TriangularDistribution.CTprob.TriangularDistribution.CC™CG Gprob.TriangularDistribution.BTprob.TriangularDistribution.B 4¨®Zö˜H ¾ z 4 ê š N Ä x . ê œ X $ Þ � BôšDî¨`Ê~>ì ` ÔŒDü´^¦RÂ|@ð¨Eš'I Istats.chart.HeatmapChart.XData`stats.chart.HeatmapChart.XDataMš&Q Qstats.chart.HeatmapChart.ColorData`stats.chart.HeatmapChart.ColorData9š%= =stats.chart.HeatmapChart_stats.chart.HeatmapChartCš$G Gstats.chart.BoxChart.BoxChart^stats.chart.BoxChart.BoxChart?š#C Cstats.chart.BoxChart.Parent]stats.chart.BoxChart.ParentKš"O Ostats.chart.BoxChart.CapWidthMode]stats.chart.BoxChart.CapWidthModeQš!U Ustats.chart.BoxChart.MarkerColorMode]stats.chart.BoxChart.MarkerColorMode_š c cstats.chart.BoxChart.BoxMedianLineColorMode]stats.chart.BoxChart.BoxMedianLineColorModeSšW Wstats.chart.BoxChart.BoxEdgeColorMode]stats.chart.BoxChart.BoxEdgeColorModeSšW Wstats.chart.BoxChart.BoxFaceColorMode]stats.chart.BoxChart.BoxFaceColorModeEšI Istats.chart.BoxChart.YDataMode]stats.chart.BoxChart.YDataModeEšI Istats.chart.BoxChart.XDataMode]stats.chart.BoxChart.XDataModeEšI Istats.chart.BoxChart.YVariable]stats.chart.BoxChart.YVariableEšI Istats.chart.BoxChart.XVariable]stats.chart.BoxChart.XVariableIšM Mstats.chart.BoxChart.SourceTable]stats.chart.BoxChart.SourceTable=šA Astats.chart.BoxChart.YData]stats.chart.BoxChart.YData=šA Astats.chart.BoxChart.XData]stats.chart.BoxChart.XDataIšM Mstats.chart.BoxChart.Orientation]stats.chart.BoxChart.OrientationOšS Sstats.chart.BoxChart.JitterOutliers]stats.chart.BoxChart.JitterOutliers=šA Astats.chart.BoxChart.Notch]stats.chart.BoxChart.NotchIšM Mstats.chart.BoxChart.MarkerColor]stats.chart.BoxChart.MarkerColorGšK Kstats.chart.BoxChart.MarkerSize]stats.chart.BoxChart.MarkerSizeIšM Mstats.chart.BoxChart.MarkerStyle]stats.chart.BoxChart.MarkerStyleEšI Istats.chart.BoxChart.LineWidth]stats.chart.BoxChart.LineWidthCšG Gstats.chart.BoxChart.CapWidth]stats.chart.BoxChart.CapWidthSšW Wstats.chart.BoxChart.WhiskerLineStyle]stats.chart.BoxChart.WhiskerLineStyleSš W Wstats.chart.BoxChart.WhiskerLineColor]stats.chart.BoxChart.WhiskerLineColorWš [ [stats.chart.BoxChart.BoxMedianLineColor]stats.chart.BoxChart.BoxMedianLineColorKš O Ostats.chart.BoxChart.BoxEdgeColor]stats.chart.BoxChart.BoxEdgeColorKš O Ostats.chart.BoxChart.BoxFaceAlpha]stats.chart.BoxChart.BoxFaceAlphaKš O Ostats.chart.BoxChart.BoxFaceColor]stats.chart.BoxChart.BoxFaceColorCšG Gstats.chart.BoxChart.BoxWidth]stats.chart.BoxChart.BoxWidth1š5 5stats.chart.BoxChart\stats.chart.BoxChartAšE Eprob.WeibullDistribution.var[prob.WeibullDistribution.varKšO Oprob.WeibullDistribution.truncate[prob.WeibullDistribution.truncateAšE Eprob.WeibullDistribution.std[prob.WeibullDistribution.stdGšK Kprob.WeibullDistribution.random[prob.WeibullDistribution.randomIšM Mprob.WeibullDistribution.proflik[prob.WeibullDistribution.proflikCšG Gprob.WeibullDistribution.plot[prob.WeibullDistribution.plotAšE Eprob.WeibullDistribution.pdf[prob.WeibullDistribution.pdfI™M Mprob.WeibullDistribution.paramci[prob.WeibullDistribution.paramciM™~Q Qprob.WeibullDistribution.negloglik[prob.WeibullDistribution.negloglikG™}K Kprob.WeibullDistribution.median[prob.WeibullDistribution.medianC™|G Gprob.WeibullDistribution.mean[prob.WeibullDistribution.meanA™{E Eprob.WeibullDistribution.iqr[prob.WeibullDistribution.iqrC™zG Gprob.WeibullDistribution.icdf[prob.WeibullDistribution.icdfA™yE Eprob.WeibullDistribution.cdf[prob.WeibullDistribution.cdfM™xQ Qprob.WeibullDistribution.InputDataZprob.WeibullDistribution.InputData[™w_ _prob.WeibullDistribution.ParameterIsFixedZprob.WeibullDistribution.ParameterIsFixeda™ve eprob.WeibullDistribution.ParameterCovarianceZprob.WeibullDistribution.ParameterCovarianceQ™uU Uprob.WeibullDistribution.IsTruncatedZprob.WeibullDistribution.IsTruncatedO™tS Sprob.WeibullDistribution.TruncationZprob.WeibullDistribution.Truncation /�¸b ²X À l  Ì t Ê v ( Ì n  ´ Z ¾t*ÜŽ>êœDâšN¦Nø°h"Ü’@ät �wšV{ {stats.chart.ParallelCoordinatesPlot.CoordinateVariablescstats.chart.ParallelCoordinatesPlot.CoordinateVariablesgšUk kstats.chart.ParallelCoordinatesPlot.SourceTablecstats.chart.ParallelCoordinatesPlot.SourceTablemšTq qstats.chart.ParallelCoordinatesPlot.CoordinateDatacstats.chart.ParallelCoordinatesPlot.CoordinateDataYšS] ]stats.chart.ParallelCoordinatesPlot.Datacstats.chart.ParallelCoordinatesPlot.DataOšRS Sstats.chart.ParallelCoordinatesPlotbstats.chart.ParallelCoordinatesPlotGšQK Kstats.chart.HeatmapChart.deleteastats.chart.HeatmapChart.deleteCšPG Gstats.chart.HeatmapChart.ylimastats.chart.HeatmapChart.ylimCšOG Gstats.chart.HeatmapChart.xlimastats.chart.HeatmapChart.xlimEšNI Istats.chart.HeatmapChart.sortyastats.chart.HeatmapChart.sortyEšMI Istats.chart.HeatmapChart.sortxastats.chart.HeatmapChart.sortxSšLW Wstats.chart.HeatmapChart.HeatmapChartastats.chart.HeatmapChart.HeatmapChartUšKY Ystats.chart.HeatmapChart.InnerPosition`stats.chart.HeatmapChart.InnerPosition[šJ_ _stats.chart.HeatmapChart.ColorDisplayData`stats.chart.HeatmapChart.ColorDisplayDataGšIK Kstats.chart.HeatmapChart.Parent`stats.chart.HeatmapChart.ParentIšHM Mstats.chart.HeatmapChart.Visible`stats.chart.HeatmapChart.VisibleEšGI Istats.chart.HeatmapChart.Units`stats.chart.HeatmapChart.Units_šFc cstats.chart.HeatmapChart.PositionConstraint`stats.chart.HeatmapChart.PositionConstraintUšEY Ystats.chart.HeatmapChart.OuterPosition`stats.chart.HeatmapChart.OuterPositionKšDO Ostats.chart.HeatmapChart.Position`stats.chart.HeatmapChart.PositionQšCU Ustats.chart.HeatmapChart.Interpreter`stats.chart.HeatmapChart.InterpreterMšBQ Qstats.chart.HeatmapChart.FontColor`stats.chart.HeatmapChart.FontColorKšAO Ostats.chart.HeatmapChart.FontSize`stats.chart.HeatmapChart.FontSizeKš@O Ostats.chart.HeatmapChart.FontName`stats.chart.HeatmapChart.FontNameGš?K Kstats.chart.HeatmapChart.YLabel`stats.chart.HeatmapChart.YLabelGš>K Kstats.chart.HeatmapChart.XLabel`stats.chart.HeatmapChart.XLabelEš=I Istats.chart.HeatmapChart.Title`stats.chart.HeatmapChart.TitleQš<U Ustats.chart.HeatmapChart.GridVisible`stats.chart.HeatmapChart.GridVisibleWš;[ [stats.chart.HeatmapChart.CellLabelColor`stats.chart.HeatmapChart.CellLabelColorYš:] ]stats.chart.HeatmapChart.CellLabelFormat`stats.chart.HeatmapChart.CellLabelFormat[š9_ _stats.chart.HeatmapChart.MissingDataLabel`stats.chart.HeatmapChart.MissingDataLabel[š8_ _stats.chart.HeatmapChart.MissingDataColor`stats.chart.HeatmapChart.MissingDataColorYš7] ]stats.chart.HeatmapChart.ColorbarVisible`stats.chart.HeatmapChart.ColorbarVisibleKš6O Ostats.chart.HeatmapChart.Colormap`stats.chart.HeatmapChart.ColormapQš5U Ustats.chart.HeatmapChart.ColorLimits`stats.chart.HeatmapChart.ColorLimitsSš4W Wstats.chart.HeatmapChart.ColorScaling`stats.chart.HeatmapChart.ColorScalingQš3U Ustats.chart.HeatmapChart.ColorMethod`stats.chart.HeatmapChart.ColorMethodUš2Y Ystats.chart.HeatmapChart.ColorVariable`stats.chart.HeatmapChart.ColorVariableMš1Q Qstats.chart.HeatmapChart.YVariable`stats.chart.HeatmapChart.YVariableMš0Q Qstats.chart.HeatmapChart.XVariable`stats.chart.HeatmapChart.XVariableQš/U Ustats.chart.HeatmapChart.SourceTable`stats.chart.HeatmapChart.SourceTableIš.M Mstats.chart.HeatmapChart.YLimits`stats.chart.HeatmapChart.YLimitsIš-M Mstats.chart.HeatmapChart.XLimits`stats.chart.HeatmapChart.XLimitsWš,[ [stats.chart.HeatmapChart.YDisplayLabels`stats.chart.HeatmapChart.YDisplayLabelsWš+[ [stats.chart.HeatmapChart.XDisplayLabels`stats.chart.HeatmapChart.XDisplayLabelsSš*W Wstats.chart.HeatmapChart.YDisplayData`stats.chart.HeatmapChart.YDisplayDataSš)W Wstats.chart.HeatmapChart.XDisplayData`stats.chart.HeatmapChart.XDisplayDataEš(I Istats.chart.HeatmapChart.YData`stats.chart.HeatmapChart.YData &³š,¶: Ú |  ° J à x   . È j ¢>ÐXú˜8ÊEå—=ã�¹Wís³_š|c cstats.chart.ScatterHistogramChart.BinWidthsfstats.chart.ScatterHistogramChart.BinWidths[š{_ _stats.chart.ScatterHistogramChart.NumBinsfstats.chart.ScatterHistogramChart.NumBinswšz{ {stats.chart.ScatterHistogramChart.HistogramDisplayStylefstats.chart.ScatterHistogramChart.HistogramDisplayStylegšyk kstats.chart.ScatterHistogramChart.GroupVariablefstats.chart.ScatterHistogramChart.GroupVariable_šxc cstats.chart.ScatterHistogramChart.YVariablefstats.chart.ScatterHistogramChart.YVariable_šwc cstats.chart.ScatterHistogramChart.XVariablefstats.chart.ScatterHistogramChart.XVariablecšvg gstats.chart.ScatterHistogramChart.SourceTablefstats.chart.ScatterHistogramChart.SourceTable_šuc cstats.chart.ScatterHistogramChart.GroupDatafstats.chart.ScatterHistogramChart.GroupDataWšt[ [stats.chart.ScatterHistogramChart.YDatafstats.chart.ScatterHistogramChart.YDataWšs[ [stats.chart.ScatterHistogramChart.XDatafstats.chart.ScatterHistogramChart.XDataKšrO Ostats.chart.ScatterHistogramChartestats.chart.ScatterHistogramChart]šqa astats.chart.ParallelCoordinatesPlot.deletedstats.chart.ParallelCoordinatesPlot.delete�šp � �stats.chart.ParallelCoordinatesPlot.ParallelCoordinatesPlotdstats.chart.ParallelCoordinatesPlot.ParallelCoordinatesPlotkšoo ostats.chart.ParallelCoordinatesPlot.InnerPositioncstats.chart.ParallelCoordinatesPlot.InnerPosition]šna astats.chart.ParallelCoordinatesPlot.Parentcstats.chart.ParallelCoordinatesPlot.Parent_šmc cstats.chart.ParallelCoordinatesPlot.Visiblecstats.chart.ParallelCoordinatesPlot.Visible[šl_ _stats.chart.ParallelCoordinatesPlot.Unitscstats.chart.ParallelCoordinatesPlot.Unitsušky ystats.chart.ParallelCoordinatesPlot.PositionConstraintcstats.chart.ParallelCoordinatesPlot.PositionConstraintkšjo ostats.chart.ParallelCoordinatesPlot.OuterPositioncstats.chart.ParallelCoordinatesPlot.OuterPositionašie estats.chart.ParallelCoordinatesPlot.Positioncstats.chart.ParallelCoordinatesPlot.Positionašhe estats.chart.ParallelCoordinatesPlot.FontSizecstats.chart.ParallelCoordinatesPlot.FontSizeašge estats.chart.ParallelCoordinatesPlot.FontNamecstats.chart.ParallelCoordinatesPlot.FontName[šf_ _stats.chart.ParallelCoordinatesPlot.Titlecstats.chart.ParallelCoordinatesPlot.Titlecšeg gstats.chart.ParallelCoordinatesPlot.DataLabelcstats.chart.ParallelCoordinatesPlot.DataLabelošds sstats.chart.ParallelCoordinatesPlot.CoordinateLabelcstats.chart.ParallelCoordinatesPlot.CoordinateLabelgšck kstats.chart.ParallelCoordinatesPlot.LegendTitlecstats.chart.ParallelCoordinatesPlot.LegendTitlekšbo ostats.chart.ParallelCoordinatesPlot.LegendVisiblecstats.chart.ParallelCoordinatesPlot.LegendVisibleešai istats.chart.ParallelCoordinatesPlot.MarkerSizecstats.chart.ParallelCoordinatesPlot.MarkerSizegš`k kstats.chart.ParallelCoordinatesPlot.MarkerStylecstats.chart.ParallelCoordinatesPlot.MarkerStylecš_g gstats.chart.ParallelCoordinatesPlot.LineAlphacstats.chart.ParallelCoordinatesPlot.LineAlphacš^g gstats.chart.ParallelCoordinatesPlot.LineWidthcstats.chart.ParallelCoordinatesPlot.LineWidthcš]g gstats.chart.ParallelCoordinatesPlot.LineStylecstats.chart.ParallelCoordinatesPlot.LineStyle[š\_ _stats.chart.ParallelCoordinatesPlot.Colorcstats.chart.ParallelCoordinatesPlot.Color]š[a astats.chart.ParallelCoordinatesPlot.Jittercstats.chart.ParallelCoordinatesPlot.JitteryšZ} }stats.chart.ParallelCoordinatesPlot.CoordinateTickLabelscstats.chart.ParallelCoordinatesPlot.CoordinateTickLabelssšYw wstats.chart.ParallelCoordinatesPlot.DataNormalizationcstats.chart.ParallelCoordinatesPlot.DataNormalizationkšXo ostats.chart.ParallelCoordinatesPlot.GroupVariablecstats.chart.ParallelCoordinatesPlot.GroupVariablecšWg gstats.chart.ParallelCoordinatesPlot.GroupDatacstats.chart.ParallelCoordinatesPlot.GroupData Ίš$ Æ h  ¬ J ä €  ² H â ˆ ,Ðp°FÒx¾TÚ‚*ÎY›] ]stats.chart.ScatterHistogramChart.deletegstats.chart.ScatterHistogramChart.deleteU›Y Ystats.chart.ScatterHistogramChart.ylimgstats.chart.ScatterHistogramChart.ylimU›Y Ystats.chart.ScatterHistogramChart.xlimgstats.chart.ScatterHistogramChart.xlimw›{ {stats.chart.ScatterHistogramChart.ScatterHistogramChartgstats.chart.ScatterHistogramChart.ScatterHistogramChartg›k kstats.chart.ScatterHistogramChart.InnerPositionfstats.chart.ScatterHistogramChart.InnerPositionY›] ]stats.chart.ScatterHistogramChart.Parentfstats.chart.ScatterHistogramChart.Parent[›_ _stats.chart.ScatterHistogramChart.Visiblefstats.chart.ScatterHistogramChart.VisibleW›[ [stats.chart.ScatterHistogramChart.Unitsfstats.chart.ScatterHistogramChart.Unitsq›u ustats.chart.ScatterHistogramChart.PositionConstraintfstats.chart.ScatterHistogramChart.PositionConstraintg›k kstats.chart.ScatterHistogramChart.OuterPositionfstats.chart.ScatterHistogramChart.OuterPosition]›a astats.chart.ScatterHistogramChart.Positionfstats.chart.ScatterHistogramChart.Position]›a astats.chart.ScatterHistogramChart.FontSizefstats.chart.ScatterHistogramChart.FontSize]›a astats.chart.ScatterHistogramChart.FontNamefstats.chart.ScatterHistogramChart.FontNameY›] ]stats.chart.ScatterHistogramChart.YLabelfstats.chart.ScatterHistogramChart.YLabelY› ] ]stats.chart.ScatterHistogramChart.XLabelfstats.chart.ScatterHistogramChart.XLabelW› [ [stats.chart.ScatterHistogramChart.Titlefstats.chart.ScatterHistogramChart.Titlec› g gstats.chart.ScatterHistogramChart.LegendTitlefstats.chart.ScatterHistogramChart.LegendTitleg› k kstats.chart.ScatterHistogramChart.LegendVisiblefstats.chart.ScatterHistogramChart.LegendVisiblec› g gstats.chart.ScatterHistogramChart.MarkerAlphafstats.chart.ScatterHistogramChart.MarkerAlphae›i istats.chart.ScatterHistogramChart.MarkerFilledfstats.chart.ScatterHistogramChart.MarkerFilleda›e estats.chart.ScatterHistogramChart.MarkerSizefstats.chart.ScatterHistogramChart.MarkerSizec›g gstats.chart.ScatterHistogramChart.MarkerStylefstats.chart.ScatterHistogramChart.MarkerStyle_›c cstats.chart.ScatterHistogramChart.LineWidthfstats.chart.ScatterHistogramChart.LineWidth_›c cstats.chart.ScatterHistogramChart.LineStylefstats.chart.ScatterHistogramChart.LineStyleW›[ [stats.chart.ScatterHistogramChart.Colorfstats.chart.ScatterHistogramChart.Color[›_ _stats.chart.ScatterHistogramChart.YLimitsfstats.chart.ScatterHistogramChart.YLimits[›_ _stats.chart.ScatterHistogramChart.XLimitsfstats.chart.ScatterHistogramChart.XLimitss›w wstats.chart.ScatterHistogramChart.YHistogramDirectionfstats.chart.ScatterHistogramChart.YHistogramDirectionsšw wstats.chart.ScatterHistogramChart.XHistogramDirectionfstats.chart.ScatterHistogramChart.XHistogramDirectionwš~{ {stats.chart.ScatterHistogramChart.ScatterPlotProportionfstats.chart.ScatterHistogramChart.ScatterPlotProportionsš}w wstats.chart.ScatterHistogramChart.ScatterPlotLocationfstats.chart.ScatterHistogramChart.ScatterPlotLocationstatistics-release-2.1.2/inst/000077500000000000000000000000001527311104300162765ustar00rootroot00000000000000statistics-release-2.1.2/inst/Anomaly_Detection/000077500000000000000000000000001527311104300216745ustar00rootroot00000000000000statistics-release-2.1.2/inst/Anomaly_Detection/IsolationForest.m000066400000000000000000000303361527311104300252030ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef IsolationForest ## -*- texinfo -*- ## @deftp {statistics} IsolationForest ## ## Isolation Forest model for anomaly detection. ## ## An @code{IsolationForest} object stores an ensemble of isolation trees ## fitted to a set of observations and detects anomalies through the ## @code{isanomaly} method. Create an @code{IsolationForest} object with ## @code{iforest} or the class constructor. ## ## Anomalies are easier to isolate, so they sit closer to the root of a random ## isolation tree; the shorter its average path length across the ensemble, ## the higher an observation's anomaly score. ## ## @seealso{iforest, IsolationForest.isanomaly} ## @end deftp properties (GetAccess = public, SetAccess = private) ## -*- texinfo -*- ## @deftp {IsolationForest} {property} NumLearners ## The number of isolation trees in the ensemble. ## @end deftp NumLearners = []; ## -*- texinfo -*- ## @deftp {IsolationForest} {property} NumObservationsPerLearner ## The number of observations subsampled to grow each isolation tree. ## @end deftp NumObservationsPerLearner = []; ## -*- texinfo -*- ## @deftp {IsolationForest} {property} ContaminationFraction ## The assumed fraction of anomalies in the training data, in @math{[0, 1]}. ## @end deftp ContaminationFraction = []; ## -*- texinfo -*- ## @deftp {IsolationForest} {property} ScoreThreshold ## The score above which an observation is flagged as an anomaly. ## @end deftp ScoreThreshold = []; endproperties properties (GetAccess = public, SetAccess = private, Hidden) Trees_ = {}; # ensemble of isolation trees (nested structs) scores_ = []; # anomaly scores of the training observations tf_ = []; # anomaly flags of the training observations endproperties methods (Hidden) ## Custom display function disp (obj) printf (" IsolationForest model\n"); printf (" NumLearners: %d\n", obj.NumLearners); printf (" NumObservationsPerLearner: %d\n", ... obj.NumObservationsPerLearner); printf (" ContaminationFraction: %g\n", obj.ContaminationFraction); printf (" ScoreThreshold: %g\n", obj.ScoreThreshold); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {IsolationForest} {@var{Mdl} =} IsolationForest (@var{X}) ## @deftypefnx {IsolationForest} {@var{Mdl} =} IsolationForest (@dots{}, @var{name}, @var{value}) ## ## Fit an isolation forest. ## ## @code{@var{Mdl} = IsolationForest (@var{X})} fits an isolation forest to ## the @math{N}-by-@math{P} matrix @var{X}, whose rows are observations and ## columns are variables, and returns a @code{IsolationForest} object ## @var{Mdl}. @var{X} must have at least 3 observations, the smallest ## subsample MATLAB accepts too; MATLAB takes fewer by default and returns ## a model that cannot tell one observation from another. ## ## @code{@var{Mdl} = IsolationForest (@dots{}, @var{name}, @var{value})} ## takes the following @qcode{Name-Value} pairs. ## ## @multitable @columnfractions 0.34 0.66 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'NumLearners'} @tab the number of isolation trees, a ## positive integer (default 100). ## ## @item @qcode{'NumObservationsPerLearner'} @tab the subsample size used to ## grow each tree, an integer in @math{[3, N]} (default @code{min (@var{N}, ## 256)}). ## ## @item @qcode{'ContaminationFraction'} @tab the assumed fraction of ## anomalies in @var{X}, a scalar in @math{[0, 1]} (default 0). It sets ## @code{@var{Mdl}.ScoreThreshold} to the @math{1 -} ## @var{ContaminationFraction} quantile of the anomaly scores of @var{X}; ## when it is 0 the threshold is the maximum score and no training ## observation is flagged. ## @end multitable ## ## Because the trees are grown from random subsamples and random splits, the ## model depends on the state of the random number generator and is not ## reproducible across runs unless the generator is seeded. ## ## @code{iforest} fits the same model and also returns the anomaly ## indicators and the anomaly scores of the observations in @var{X}. ## ## @seealso{iforest, IsolationForest.isanomaly} ## @end deftypefn function obj = IsolationForest (X, varargin) if (nargin < 1) error ("iforest: too few input arguments."); endif if (! isnumeric (X) || ! isreal (X) || ndims (X) != 2 || isempty (X)) error ("iforest: X must be a nonempty real numeric matrix."); endif [n, p] = size (X); if (n < 3) error ("iforest: X must have at least 3 observations."); endif ## Defaults numlearners = 100; numobs = min (n, 256); contam = 0; ## Parse Name-Value pairs if (mod (numel (varargin), 2) != 0) error ("iforest: each NAME must be followed by a VALUE."); endif while (numel (varargin) > 0) name = varargin{1}; val = varargin{2}; if (! ischar (name)) error ("iforest: optional argument names must be strings."); endif switch (tolower (name)) case "numlearners" numlearners = val; case "numobservationsperlearner" numobs = val; case "contaminationfraction" contam = val; otherwise error ("iforest: unknown parameter name '%s'.", name); endswitch varargin(1:2) = []; endwhile ## Validate options if (! isscalar (numlearners) || ! isnumeric (numlearners) || numlearners < 1 || fix (numlearners) != numlearners) error ("iforest: NUMLEARNERS must be a positive integer scalar."); endif if (! isscalar (numobs) || ! isnumeric (numobs) || numobs < 3 || fix (numobs) != numobs || numobs > n) error (strcat ("iforest: NUMOBSERVATIONSPERLEARNER must be an", ... " integer in [3, N].")); endif if (! isscalar (contam) || ! isnumeric (contam) || contam < 0 || contam > 1) error ("iforest: CONTAMINATIONFRACTION must be a scalar in [0, 1]."); endif ## Grow the ensemble, each tree from a random subsample of NUMOBS rows. maxdepth = ceil (log2 (numobs)); trees = cell (numlearners, 1); for t = 1:numlearners idx = randperm (n, numobs); trees{t} = IsolationForest.buildTree_ (X(idx, :), 0, maxdepth); endfor scores = IsolationForest.scoreAll_ (X, trees, numobs); if (contam == 0) thr = max (scores); else thr = quantile (scores, 1 - contam); endif obj.NumLearners = numlearners; obj.NumObservationsPerLearner = numobs; obj.ContaminationFraction = contam; obj.ScoreThreshold = thr; obj.Trees_ = trees; obj.scores_ = scores; obj.tf_ = scores > thr; endfunction ## -*- texinfo -*- ## @deftypefn {IsolationForest} {@var{tf} =} isanomaly (@var{Mdl}, @var{Xnew}) ## @deftypefnx {IsolationForest} {[@var{tf}, @var{scores}] =} isanomaly (@var{Mdl}, @var{Xnew}) ## @deftypefnx {IsolationForest} {[@dots{}] =} isanomaly (@dots{}, @qcode{'ScoreThreshold'}, @var{t}) ## ## Detect anomalies in the new observations @var{Xnew} using the fitted ## model @var{Mdl}. Returns the logical vector @var{tf} flagging anomalies ## and the anomaly @var{scores}, each obtained by dropping @var{Xnew} ## through the isolation trees. The threshold defaults to ## @code{@var{Mdl}.ScoreThreshold} and may be overridden with the ## @qcode{'ScoreThreshold'} name-value argument. ## ## @end deftypefn function [tf, scores] = isanomaly (obj, Xnew, varargin) if (nargin < 2) error ("isanomaly: too few input arguments."); endif if (! isnumeric (Xnew) || ! isreal (Xnew) || ndims (Xnew) != 2 || isempty (Xnew)) error ("isanomaly: XNEW must be a nonempty real numeric matrix."); endif thr = obj.ScoreThreshold; if (mod (numel (varargin), 2) != 0) error ("isanomaly: each NAME must be followed by a VALUE."); endif while (numel (varargin) > 0) if (! ischar (varargin{1}) || ! strcmpi (varargin{1}, "ScoreThreshold")) error ("isanomaly: unknown parameter name."); endif thr = varargin{2}; varargin(1:2) = []; endwhile scores = IsolationForest.scoreAll_ (Xnew, obj.Trees_, ... obj.NumObservationsPerLearner); tf = scores > thr; endfunction endmethods methods (Static, Access = private) ## Grow one isolation tree by recursive random splits. function node = buildTree_ (X, depth, maxdepth) n = rows (X); if (depth >= maxdepth || n <= 1) node = struct ("leaf", true, "size", n); return; endif mn = min (X, [], 1); mx = max (X, [], 1); valid = find (mx > mn); if (isempty (valid)) # all rows identical node = struct ("leaf", true, "size", n); return; endif q = valid(randi (numel (valid))); sp = mn(q) + (mx(q) - mn(q)) * rand; isleft = X(:, q) < sp; if (! any (isleft) || all (isleft)) # degenerate split node = struct ("leaf", true, "size", n); return; endif L = IsolationForest.buildTree_ (X(isleft, :), depth + 1, maxdepth); R = IsolationForest.buildTree_ (X(! isleft, :), depth + 1, maxdepth); node = struct ("leaf", false, "feature", q, "split", sp, ... "left", L, "right", R); endfunction ## Path length of a single observation down one tree. function h = pathLength_ (x, node, depth) if (node.leaf) h = depth + IsolationForest.cFactor_ (node.size); return; endif if (x(node.feature) < node.split) h = IsolationForest.pathLength_ (x, node.left, depth + 1); else h = IsolationForest.pathLength_ (x, node.right, depth + 1); endif endfunction ## Average path length of an unsuccessful search in a binary search tree. function c = cFactor_ (n) if (n <= 1) c = 0; else H = log (n - 1) + 0.5772156649015329; c = 2 * H - 2 * (n - 1) / n; endif endfunction ## Anomaly scores of the rows of P over the whole ensemble. function scores = scoreAll_ (P, trees, psi) m = rows (P); T = numel (trees); H = zeros (m, 1); for t = 1:T node = trees{t}; for i = 1:m H(i) += IsolationForest.pathLength_ (P(i, :), node, 0); endfor endfor Eh = H / T; scores = 2 .^ (- Eh / IsolationForest.cFactor_ (psi)); endfunction endmethods endclassdef ## Direct construction and isanomaly dispatch on the class %!test %! rand ("state", 5); %! X = [randn(40,2)*0.3; 10 10; -9 8]; %! Mdl = IsolationForest (X, "NumLearners", 50); %! assert_equal (isa (Mdl, "IsolationForest"), true); %! assert_equal (Mdl.NumLearners, 50); %! assert_equal (Mdl.NumObservationsPerLearner, 42); %! [tf, scores] = isanomaly (Mdl, [0 0; 12 12]); %! assert_equal (all (scores >= 0 & scores <= 1, 'all'), true); %! assert_equal (scores(2) > scores(1), true); # far point is more anomalous %! assert_equal (islogical (tf), true); statistics-release-2.1.2/inst/Anomaly_Detection/LocalOutlierFactor.m000066400000000000000000000301301527311104300256040ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef LocalOutlierFactor ## -*- texinfo -*- ## @deftp {statistics} LocalOutlierFactor ## ## Local Outlier Factor model for anomaly detection. ## ## A @code{LocalOutlierFactor} object stores a Local Outlier Factor (LOF) ## model fitted to a set of observations, and detects anomalies among those or ## new observations through the @code{isanomaly} method. Create a ## @code{LocalOutlierFactor} object with @code{lof} or the class constructor. ## ## The LOF of an observation compares its local density with the local density ## of its neighbors; a value near 1 indicates an inlier, whereas a value well ## above 1 indicates an outlier that lies in a sparser region than its ## neighbors. ## ## @seealso{lof, LocalOutlierFactor.isanomaly} ## @end deftp properties (GetAccess = public, SetAccess = private) ## -*- texinfo -*- ## @deftp {LocalOutlierFactor} {property} NumNeighbors ## The number of nearest neighbors used to compute the Local Outlier Factor. ## @end deftp NumNeighbors = []; ## -*- texinfo -*- ## @deftp {LocalOutlierFactor} {property} Distance ## The distance metric used to find neighbors, as a character vector. ## @end deftp Distance = []; ## -*- texinfo -*- ## @deftp {LocalOutlierFactor} {property} ContaminationFraction ## The assumed fraction of anomalies in the training data, in @math{[0, 1]}. ## @end deftp ContaminationFraction = []; ## -*- texinfo -*- ## @deftp {LocalOutlierFactor} {property} ScoreThreshold ## The score above which an observation is flagged as an anomaly. ## @end deftp ScoreThreshold = []; endproperties properties (GetAccess = public, SetAccess = private, Hidden) X_ = []; # training observations DistParameter_ = []; # metric parameter (exponent or covariance) kdist_ = []; # k-distance of each training observation lrd_ = []; # local reachability density of each observation scores_ = []; # LOF scores of the training observations tf_ = []; # anomaly flags of the training observations endproperties methods (Hidden) ## Custom display function disp (obj) printf (" LocalOutlierFactor model\n"); printf (" NumNeighbors: %d\n", obj.NumNeighbors); printf (" Distance: %s\n", obj.Distance); printf (" ContaminationFraction: %g\n", obj.ContaminationFraction); printf (" ScoreThreshold: %g\n", obj.ScoreThreshold); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {LocalOutlierFactor} {@var{Mdl} =} LocalOutlierFactor (@var{X}) ## @deftypefnx {LocalOutlierFactor} {@var{Mdl} =} LocalOutlierFactor (@dots{}, @var{name}, @var{value}) ## ## Fit a Local Outlier Factor model. ## ## @code{@var{Mdl} = LocalOutlierFactor (@var{X})} fits a Local Outlier ## Factor model to the @math{N}-by-@math{P} matrix @var{X}, whose rows are ## observations and columns are variables, and returns a ## @code{LocalOutlierFactor} object @var{Mdl}. ## ## @code{@var{Mdl} = LocalOutlierFactor (@dots{}, @var{name}, @var{value})} ## takes the following @qcode{Name-Value} pairs. ## ## @multitable @columnfractions 0.28 0.72 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'NumNeighbors'} @tab the number of nearest neighbors, a ## positive integer less than @math{N}. The default is ## @code{min (20, @var{u} - 1)}, where @var{u} is the number of unique ## observations. ## ## @item @qcode{'Distance'} @tab the distance metric used to find neighbors, ## one of the metrics accepted by @code{pdist2} (@qcode{'euclidean'} by ## default). ## ## @item @qcode{'ContaminationFraction'} @tab the assumed fraction of ## anomalies in @var{X}, a scalar in @math{[0, 1]} (default 0). It sets ## @code{@var{Mdl}.ScoreThreshold} to the @math{1 -} ## @var{ContaminationFraction} quantile of the anomaly scores of @var{X}; ## when it is 0 the threshold is the maximum score and no training ## observation is flagged. ## ## @item @qcode{'Exponent'} @tab the Minkowski distance exponent (default ## 2), used only with the @qcode{'minkowski'} distance. ## ## @item @qcode{'Cov'} @tab the covariance matrix used only with the ## @qcode{'mahalanobis'} distance. ## @end multitable ## ## @code{lof} fits the same model and also returns the anomaly indicators ## and the anomaly scores of the observations in @var{X}. ## ## @seealso{lof, LocalOutlierFactor.isanomaly} ## @end deftypefn function obj = LocalOutlierFactor (X, varargin) if (nargin < 1) error ("lof: too few input arguments."); endif if (! isnumeric (X) || ! isreal (X) || ndims (X) != 2 || isempty (X)) error ("lof: X must be a nonempty real numeric matrix."); endif [n, p] = size (X); ## Defaults k = min (20, rows (unique (X, "rows")) - 1); distance = "euclidean"; contam = 0; exponent = 2; covmat = []; ## Parse Name-Value pairs if (mod (numel (varargin), 2) != 0) error ("lof: each NAME must be followed by a VALUE."); endif while (numel (varargin) > 0) name = varargin{1}; val = varargin{2}; if (! ischar (name)) error ("lof: optional argument names must be strings."); endif switch (tolower (name)) case "numneighbors" k = val; case "distance" distance = tolower (val); case "contaminationfraction" contam = val; case "exponent" exponent = val; case "cov" covmat = val; otherwise error ("lof: unknown parameter name '%s'.", name); endswitch varargin(1:2) = []; endwhile ## Validate options metrics = {"euclidean", "seuclidean", "mahalanobis", "cityblock", ... "minkowski", "chebychev", "cosine", "correlation", ... "hamming", "jaccard", "spearman"}; if (! any (strcmp (distance, metrics))) error ("lof: unsupported distance metric '%s'.", distance); endif if (! isscalar (k) || ! isnumeric (k) || k < 1 || fix (k) != k || k >= n) error ("lof: NUMNEIGHBORS must be a positive integer less than N."); endif if (! isscalar (contam) || ! isnumeric (contam) || contam < 0 || contam > 1) error ("lof: CONTAMINATIONFRACTION must be a scalar in [0, 1]."); endif ## Select the metric parameter for pdist2. if (strcmp (distance, "minkowski")) distparam = exponent; elseif (strcmp (distance, "mahalanobis")) distparam = covmat; else distparam = []; endif ## Pairwise distances, excluding each point from its own neighborhood. D = LocalOutlierFactor.pdists_ (X, X, distance, distparam); D(1:(n + 1):end) = Inf; [kdist, lrd, scores] = LocalOutlierFactor.lofTrain_ (D, k); if (contam == 0) thr = max (scores); else thr = quantile (scores, 1 - contam); endif obj.NumNeighbors = k; obj.Distance = distance; obj.ContaminationFraction = contam; obj.ScoreThreshold = thr; obj.X_ = X; obj.DistParameter_ = distparam; obj.kdist_ = kdist; obj.lrd_ = lrd; obj.scores_ = scores; obj.tf_ = scores > thr; endfunction ## -*- texinfo -*- ## @deftypefn {LocalOutlierFactor} {@var{tf} =} isanomaly (@var{Mdl}, @var{Xnew}) ## @deftypefnx {LocalOutlierFactor} {[@var{tf}, @var{scores}] =} isanomaly (@var{Mdl}, @var{Xnew}) ## @deftypefnx {LocalOutlierFactor} {[@dots{}] =} isanomaly (@dots{}, @qcode{'ScoreThreshold'}, @var{t}) ## ## Detect anomalies in the new observations @var{Xnew} using the fitted ## model @var{Mdl}. Returns the logical vector @var{tf} flagging anomalies ## and the Local Outlier Factor @var{scores}, each computed from the nearest ## neighbors of @var{Xnew} in the training data. The threshold defaults to ## @code{@var{Mdl}.ScoreThreshold} and may be overridden with the ## @qcode{'ScoreThreshold'} name-value argument. ## ## @end deftypefn function [tf, scores] = isanomaly (obj, Xnew, varargin) if (nargin < 2) error ("isanomaly: too few input arguments."); endif if (! isnumeric (Xnew) || ! isreal (Xnew) || ndims (Xnew) != 2 || isempty (Xnew)) error ("isanomaly: XNEW must be a nonempty real numeric matrix."); endif if (columns (Xnew) != columns (obj.X_)) error ("isanomaly: XNEW must have the same number of columns as X."); endif thr = obj.ScoreThreshold; if (mod (numel (varargin), 2) != 0) error ("isanomaly: each NAME must be followed by a VALUE."); endif while (numel (varargin) > 0) if (! ischar (varargin{1}) || ! strcmpi (varargin{1}, "ScoreThreshold")) error ("isanomaly: unknown parameter name."); endif thr = varargin{2}; varargin(1:2) = []; endwhile Dq = LocalOutlierFactor.pdists_ (Xnew, obj.X_, obj.Distance, ... obj.DistParameter_); scores = LocalOutlierFactor.lofQuery_ (Dq, obj.NumNeighbors, ... obj.kdist_, obj.lrd_); tf = scores > thr; endfunction endmethods methods (Static, Access = private) ## Pairwise distances between the rows of A and B in the given metric. function D = pdists_ (A, B, distance, distparam) if (isempty (distparam)) D = pdist2 (A, B, distance); else D = pdist2 (A, B, distance, distparam); endif endfunction ## LOF of the training observations from the self-excluded distance matrix. function [kdist, lrd, scores] = lofTrain_ (D, k) n = rows (D); [NB, Ds] = __knnselect__ (D, k); kdist = Ds(:, k); lrd = zeros (n, 1); for i = 1:n rd = max (kdist(NB(i,:)), D(i, NB(i,:))'); lrd(i) = 1 / mean (rd); endfor scores = zeros (n, 1); for i = 1:n scores(i) = mean (lrd(NB(i,:))) / lrd(i); endfor endfunction ## LOF of query rows against the training neighbors, k-distances, and lrd. function scores = lofQuery_ (Dq, k, kdist, lrd) m = rows (Dq); [~, ord] = sort (Dq, 2); NB = ord(:, 1:k); scores = zeros (m, 1); for i = 1:m rd = max (kdist(NB(i,:)), Dq(i, NB(i,:))'); lrdq = 1 / mean (rd); scores(i) = mean (lrd(NB(i,:))) / lrdq; endfor endfunction endmethods endclassdef ## Direct construction and isanomaly dispatch on the class %!test %! X = [0 0; 0.1 0.1; 0.2 -0.1; -0.1 0.2; 0.1 -0.2; -0.2 0.1; 0.15 0.05; 5 5]; %! Mdl = LocalOutlierFactor (X, "NumNeighbors", 3); %! assert_equal (isa (Mdl, "LocalOutlierFactor"), true); %! assert_equal (Mdl.NumNeighbors, 3); %! assert_equal (Mdl.Distance, "euclidean"); %! [tf, scores] = isanomaly (Mdl, [0 0; 6 6]); %! assert_equal (size (scores), [2, 1]); %! assert_equal (scores(2) > scores(1), true); # far point is more anomalous %! assert_equal (islogical (tf), true); statistics-release-2.1.2/inst/Anomaly_Detection/OneClassSVM.m000066400000000000000000000314341527311104300241540ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef OneClassSVM ## -*- texinfo -*- ## @deftp {statistics} OneClassSVM ## ## One-class support vector machine model for anomaly detection. ## ## A @code{OneClassSVM} object stores a one-class support vector machine ## fitted to a set of observations in an expanded feature space, and detects ## anomalies through the @code{isanomaly} method. Create a @code{OneClassSVM} ## object with @code{ocsvm} or the class constructor. ## ## The model maps the data to a randomized feature space that approximates a ## Gaussian kernel and fits a linear boundary that encloses the bulk of the ## observations; points outside the boundary receive higher anomaly scores. ## ## @seealso{ocsvm, OneClassSVM.isanomaly} ## @end deftp properties (GetAccess = public, SetAccess = private) ## -*- texinfo -*- ## @deftp {OneClassSVM} {property} KernelScale ## The scale of the Gaussian kernel approximated by the feature expansion. ## @end deftp KernelScale = []; ## -*- texinfo -*- ## @deftp {OneClassSVM} {property} Lambda ## The strength of the ridge (L2) regularization term. ## @end deftp Lambda = []; ## -*- texinfo -*- ## @deftp {OneClassSVM} {property} NumExpansionDimensions ## The number of dimensions of the expanded feature space. ## @end deftp NumExpansionDimensions = []; ## -*- texinfo -*- ## @deftp {OneClassSVM} {property} Mu ## The predictor means used for standardization, or empty. ## @end deftp Mu = []; ## -*- texinfo -*- ## @deftp {OneClassSVM} {property} Sigma ## The predictor standard deviations used for standardization, or empty. ## @end deftp Sigma = []; ## -*- texinfo -*- ## @deftp {OneClassSVM} {property} ContaminationFraction ## The assumed fraction of anomalies in the training data, in @math{[0, 1]}. ## @end deftp ContaminationFraction = []; ## -*- texinfo -*- ## @deftp {OneClassSVM} {property} ScoreThreshold ## The score above which an observation is flagged as an anomaly. ## @end deftp ScoreThreshold = []; endproperties properties (GetAccess = public, SetAccess = private, Hidden) W_ = []; # random projection weights (D-by-P) b_ = []; # random phases (D-by-1) beta_ = []; # fitted linear coefficients (D-by-1) scores_ = []; # anomaly scores of the training observations tf_ = []; # anomaly flags of the training observations endproperties methods (Hidden) ## Custom display function disp (obj) printf (" OneClassSVM model\n"); printf (" KernelScale: %g\n", obj.KernelScale); printf (" Lambda: %g\n", obj.Lambda); printf (" NumExpansionDimensions: %d\n", obj.NumExpansionDimensions); printf (" ContaminationFraction: %g\n", obj.ContaminationFraction); printf (" ScoreThreshold: %g\n", obj.ScoreThreshold); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {OneClassSVM} {@var{Mdl} =} OneClassSVM (@var{X}) ## @deftypefnx {OneClassSVM} {@var{Mdl} =} OneClassSVM (@dots{}, @var{name}, @var{value}) ## ## Fit a one-class support vector machine. ## ## @code{@var{Mdl} = OneClassSVM (@var{X})} fits a one-class support vector ## machine to the @math{N}-by-@math{P} matrix @var{X}, whose rows are ## observations and columns are variables, and returns a @code{OneClassSVM} ## object @var{Mdl}. ## ## @code{@var{Mdl} = OneClassSVM (@dots{}, @var{name}, @var{value})} takes ## the following @qcode{Name-Value} pairs. ## ## @multitable @columnfractions 0.34 0.66 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'KernelScale'} @tab the scale of the approximated Gaussian ## kernel, a positive scalar or @qcode{'auto'} (default). ## ## @item @qcode{'Lambda'} @tab the ridge regularization strength, a ## nonnegative scalar or @qcode{'auto'} (default). ## ## @item @qcode{'NumExpansionDimensions'} @tab the number of expanded ## feature dimensions, a positive integer or @qcode{'auto'} (default). ## ## @item @qcode{'StandardizeData'} @tab a logical scalar (default ## @code{false}); when @code{true} each predictor is centered and scaled and ## the means and standard deviations are stored in @code{@var{Mdl}.Mu} and ## @code{@var{Mdl}.Sigma}. ## ## @item @qcode{'ContaminationFraction'} @tab the assumed fraction of ## anomalies in @var{X}, a scalar in @math{[0, 1]} (default 0). It sets ## @code{@var{Mdl}.ScoreThreshold} to the @math{1 -} ## @var{ContaminationFraction} quantile of the anomaly scores of @var{X}; ## when it is 0 the threshold is the maximum score and no training ## observation is flagged. ## @end multitable ## ## The feature expansion uses random projections, so the model depends on ## the state of the random number generator and is not reproducible across ## runs unless the generator is seeded. ## ## @code{ocsvm} fits the same model and also returns the anomaly indicators ## and the anomaly scores of the observations in @var{X}. ## ## @seealso{ocsvm, OneClassSVM.isanomaly} ## @end deftypefn function obj = OneClassSVM (X, varargin) if (nargin < 1) error ("ocsvm: too few input arguments."); endif if (! isnumeric (X) || ! isreal (X) || ndims (X) != 2 || isempty (X)) error ("ocsvm: X must be a nonempty real numeric matrix."); endif [n, p] = size (X); ## Defaults ("auto" values resolved below) kernelscale = "auto"; lambda = "auto"; numdims = "auto"; standardize = false; contam = 0; ## Parse Name-Value pairs if (mod (numel (varargin), 2) != 0) error ("ocsvm: each NAME must be followed by a VALUE."); endif while (numel (varargin) > 0) name = varargin{1}; val = varargin{2}; if (! ischar (name)) error ("ocsvm: optional argument names must be strings."); endif switch (tolower (name)) case "kernelscale" kernelscale = val; case "lambda" lambda = val; case "numexpansiondimensions" numdims = val; case "standardizedata" standardize = logical (val); case "contaminationfraction" contam = val; otherwise error ("ocsvm: unknown parameter name '%s'.", name); endswitch varargin(1:2) = []; endwhile ## Resolve and validate the "auto" defaults. if (ischar (kernelscale) && strcmpi (kernelscale, "auto")) kernelscale = 1; elseif (! isscalar (kernelscale) || ! isnumeric (kernelscale) || kernelscale <= 0) error ("ocsvm: KERNELSCALE must be a positive scalar or 'auto'."); endif if (ischar (lambda) && strcmpi (lambda, "auto")) lambda = 1 / n; elseif (! isscalar (lambda) || ! isnumeric (lambda) || lambda < 0) error ("ocsvm: LAMBDA must be a nonnegative scalar or 'auto'."); endif if (ischar (numdims) && strcmpi (numdims, "auto")) numdims = 2 ^ ceil (log2 (max (n, 2))); elseif (! isscalar (numdims) || ! isnumeric (numdims) || numdims < 1 || fix (numdims) != numdims) error (strcat ("ocsvm: NUMEXPANSIONDIMENSIONS must be a positive", ... " integer or 'auto'.")); endif if (! isscalar (contam) || ! isnumeric (contam) || contam < 0 || contam > 1) error ("ocsvm: CONTAMINATIONFRACTION must be a scalar in [0, 1]."); endif ## Optionally standardize the predictors. if (standardize) mu = mean (X, 1); sigma = std (X, 0, 1); sigma(sigma == 0) = 1; Xs = (X - mu) ./ sigma; else mu = []; sigma = []; Xs = X; endif ## Random Fourier feature map approximating a Gaussian kernel, then a ## squared-hinge one-class SVM with ridge regularization. W = randn (numdims, p) / kernelscale; b = 2 * pi * rand (numdims, 1); Phi = OneClassSVM.features_ (Xs, W, b); beta = OneClassSVM.fitOneClass_ (Phi, lambda); scores = - (Phi * beta); if (contam == 0) thr = max (scores); else thr = quantile (scores, 1 - contam); endif obj.KernelScale = kernelscale; obj.Lambda = lambda; obj.NumExpansionDimensions = numdims; obj.Mu = mu; obj.Sigma = sigma; obj.ContaminationFraction = contam; obj.ScoreThreshold = thr; obj.W_ = W; obj.b_ = b; obj.beta_ = beta; obj.scores_ = scores; obj.tf_ = scores > thr; endfunction ## -*- texinfo -*- ## @deftypefn {OneClassSVM} {@var{tf} =} isanomaly (@var{Mdl}, @var{Xnew}) ## @deftypefnx {OneClassSVM} {[@var{tf}, @var{scores}] =} isanomaly (@var{Mdl}, @var{Xnew}) ## @deftypefnx {OneClassSVM} {[@dots{}] =} isanomaly (@dots{}, @qcode{'ScoreThreshold'}, @var{t}) ## ## Detect anomalies in the new observations @var{Xnew} using the fitted ## model @var{Mdl}. Returns the logical vector @var{tf} flagging anomalies ## and the anomaly @var{scores}. The threshold defaults to ## @code{@var{Mdl}.ScoreThreshold} and may be overridden with the ## @qcode{'ScoreThreshold'} name-value argument. ## ## @end deftypefn function [tf, scores] = isanomaly (obj, Xnew, varargin) if (nargin < 2) error ("isanomaly: too few input arguments."); endif if (! isnumeric (Xnew) || ! isreal (Xnew) || ndims (Xnew) != 2 || isempty (Xnew)) error ("isanomaly: XNEW must be a nonempty real numeric matrix."); endif if (columns (Xnew) != columns (obj.W_)) error ("isanomaly: XNEW must have the same number of columns as X."); endif thr = obj.ScoreThreshold; if (mod (numel (varargin), 2) != 0) error ("isanomaly: each NAME must be followed by a VALUE."); endif while (numel (varargin) > 0) if (! ischar (varargin{1}) || ! strcmpi (varargin{1}, "ScoreThreshold")) error ("isanomaly: unknown parameter name."); endif thr = varargin{2}; varargin(1:2) = []; endwhile if (! isempty (obj.Mu)) Xnew = (Xnew - obj.Mu) ./ obj.Sigma; endif Phi = OneClassSVM.features_ (Xnew, obj.W_, obj.b_); scores = - (Phi * obj.beta_); tf = scores > thr; endfunction endmethods methods (Static, Access = private) ## Random Fourier features approximating a Gaussian kernel. function Phi = features_ (X, W, b) D = rows (W); Phi = sqrt (2 / D) * cos (X * W' + b'); endfunction ## Squared-hinge one-class SVM (unit margin from the origin) with ridge ## regularization, minimized by Newton iterations over the active set. function beta = fitOneClass_ (Phi, lambda) [n, D] = size (Phi); beta = zeros (D, 1); I = eye (D); for it = 1:100 d = Phi * beta; active = d < 1; # observations inside the margin Pa = Phi(active, :); r = 1 - d(active); g = lambda * beta - (Pa' * r) / n; H = lambda * I + (Pa' * Pa) / n; step = H \ g; beta = beta - step; if (norm (step) < 1e-10) break; endif endfor endfunction endmethods endclassdef ## Direct construction and isanomaly dispatch on the class %!test %! rand ("state", 8); %! randn ("state", 8); %! X = [randn(60,2)*0.3; 9 9; -8 7]; %! Mdl = OneClassSVM (X, "KernelScale", 2, "NumExpansionDimensions", 64); %! assert_equal (isa (Mdl, "OneClassSVM"), true); %! assert_equal (Mdl.NumExpansionDimensions, 64); %! [tf, scores] = isanomaly (Mdl, [0 0; 12 12]); %! assert_equal (scores(2) > scores(1), true); # far point is more anomalous %! assert_equal (islogical (tf), true); statistics-release-2.1.2/inst/Anomaly_Detection/doc-cache000066400000000000000000000672241527311104300234400ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 28 # name: # type: sq_string # elements: 1 # length: 15 IsolationForest # name: # type: sq_string # elements: 1 # length: 533 statistics: IsolationForest Isolation Forest model for anomaly detection. An IsolationForest object stores an ensemble of isolation trees fitted to a set of observations and detects anomalies through the isanomaly method. Create an IsolationForest object with iforest or the class constructor. Anomalies are easier to isolate, so they sit closer to the root of a random isolation tree; the shorter its average path length across the ensemble, the higher an observation's anomaly score. See also: iforest, IsolationForest.isanomaly # name: # type: sq_string # elements: 1 # length: 45 Isolation Forest model for anomaly detection. # name: # type: sq_string # elements: 1 # length: 37 IsolationForest.ContaminationFraction # name: # type: sq_string # elements: 1 # length: 114 IsolationForest: property ContaminationFraction The assumed fraction of anomalies in the training data, in [0, 1]. # name: # type: sq_string # elements: 1 # length: 66 The assumed fraction of anomalies in the training data, in [0, 1]. # name: # type: sq_string # elements: 1 # length: 31 IsolationForest.IsolationForest # name: # type: sq_string # elements: 1 # length: 1408 IsolationForest: Mdl = IsolationForest (X) IsolationForest: Mdl = IsolationForest (..., name, value) Fit an isolation forest. Mdl = IsolationForest (X) fits an isolation forest to the N-by-P matrix X, whose rows are observations and columns are variables, and returns a IsolationForest object Mdl. X must have at least 3 observations, the smallest subsample MATLAB accepts too; MATLAB takes fewer by default and returns a model that cannot tell one observation from another. Mdl = IsolationForest (..., name, value) takes the following Name-Value pairs. Name Value 'NumLearners' the number of isolation trees, a positive integer (default 100). 'NumObservationsPerLearner' the subsample size used to grow each tree, an integer in [3, N] (default min (N, 256)). 'ContaminationFraction' the assumed fraction of anomalies in X, a scalar in [0, 1] (default 0). It sets Mdl.ScoreThreshold to the 1 - ContaminationFraction quantile of the anomaly scores of X; when it is 0 the threshold is the maximum score and no training observation is flagged. Because the trees are grown from random subsamples and random splits, the model depends on the state of the random number generator and is not reproducible across runs unless the generator is seeded. iforest fits the same model and also returns the anomaly indicators and the anomaly scores of the observations in X. See also: iforest, IsolationForest.isanomaly # name: # type: sq_string # elements: 1 # length: 24 Fit an isolation forest. # name: # type: sq_string # elements: 1 # length: 27 IsolationForest.NumLearners # name: # type: sq_string # elements: 1 # length: 84 IsolationForest: property NumLearners The number of isolation trees in the ensemble. # name: # type: sq_string # elements: 1 # length: 46 The number of isolation trees in the ensemble. # name: # type: sq_string # elements: 1 # length: 41 IsolationForest.NumObservationsPerLearner # name: # type: sq_string # elements: 1 # length: 118 IsolationForest: property NumObservationsPerLearner The number of observations subsampled to grow each isolation tree. # name: # type: sq_string # elements: 1 # length: 66 The number of observations subsampled to grow each isolation tree. # name: # type: sq_string # elements: 1 # length: 30 IsolationForest.ScoreThreshold # name: # type: sq_string # elements: 1 # length: 103 IsolationForest: property ScoreThreshold The score above which an observation is flagged as an anomaly. # name: # type: sq_string # elements: 1 # length: 62 The score above which an observation is flagged as an anomaly. # name: # type: sq_string # elements: 1 # length: 25 IsolationForest.isanomaly # name: # type: sq_string # elements: 1 # length: 480 IsolationForest: tf = isanomaly (Mdl, Xnew) IsolationForest: [tf, scores] = isanomaly (Mdl, Xnew) IsolationForest: [...] = isanomaly (..., 'ScoreThreshold', t) Detect anomalies in the new observations Xnew using the fitted model Mdl. Returns the logical vector tf flagging anomalies and the anomaly scores, each obtained by dropping Xnew through the isolation trees. The threshold defaults to Mdl.ScoreThreshold and may be overridden with the 'ScoreThreshold' name-value argument. # name: # type: sq_string # elements: 1 # length: 73 Detect anomalies in the new observations Xnew using the fitted model Mdl. # name: # type: sq_string # elements: 1 # length: 18 LocalOutlierFactor # name: # type: sq_string # elements: 1 # length: 614 statistics: LocalOutlierFactor Local Outlier Factor model for anomaly detection. A LocalOutlierFactor object stores a Local Outlier Factor (LOF) model fitted to a set of observations, and detects anomalies among those or new observations through the isanomaly method. Create a LocalOutlierFactor object with lof or the class constructor. The LOF of an observation compares its local density with the local density of its neighbors; a value near 1 indicates an inlier, whereas a value well above 1 indicates an outlier that lies in a sparser region than its neighbors. See also: lof, LocalOutlierFactor.isanomaly # name: # type: sq_string # elements: 1 # length: 49 Local Outlier Factor model for anomaly detection. # name: # type: sq_string # elements: 1 # length: 40 LocalOutlierFactor.ContaminationFraction # name: # type: sq_string # elements: 1 # length: 117 LocalOutlierFactor: property ContaminationFraction The assumed fraction of anomalies in the training data, in [0, 1]. # name: # type: sq_string # elements: 1 # length: 66 The assumed fraction of anomalies in the training data, in [0, 1]. # name: # type: sq_string # elements: 1 # length: 27 LocalOutlierFactor.Distance # name: # type: sq_string # elements: 1 # length: 104 LocalOutlierFactor: property Distance The distance metric used to find neighbors, as a character vector. # name: # type: sq_string # elements: 1 # length: 66 The distance metric used to find neighbors, as a character vector. # name: # type: sq_string # elements: 1 # length: 37 LocalOutlierFactor.LocalOutlierFactor # name: # type: sq_string # elements: 1 # length: 1314 LocalOutlierFactor: Mdl = LocalOutlierFactor (X) LocalOutlierFactor: Mdl = LocalOutlierFactor (..., name, value) Fit a Local Outlier Factor model. Mdl = LocalOutlierFactor (X) fits a Local Outlier Factor model to the N-by-P matrix X, whose rows are observations and columns are variables, and returns a LocalOutlierFactor object Mdl. Mdl = LocalOutlierFactor (..., name, value) takes the following Name-Value pairs. Name Value 'NumNeighbors' the number of nearest neighbors, a positive integer less than N. The default is min (20, u - 1), where u is the number of unique observations. 'Distance' the distance metric used to find neighbors, one of the metrics accepted by pdist2 ('euclidean' by default). 'ContaminationFraction' the assumed fraction of anomalies in X, a scalar in [0, 1] (default 0). It sets Mdl.ScoreThreshold to the 1 - ContaminationFraction quantile of the anomaly scores of X; when it is 0 the threshold is the maximum score and no training observation is flagged. 'Exponent' the Minkowski distance exponent (default 2), used only with the 'minkowski' distance. 'Cov' the covariance matrix used only with the 'mahalanobis' distance. lof fits the same model and also returns the anomaly indicators and the anomaly scores of the observations in X. See also: lof, LocalOutlierFactor.isanomaly # name: # type: sq_string # elements: 1 # length: 33 Fit a Local Outlier Factor model. # name: # type: sq_string # elements: 1 # length: 31 LocalOutlierFactor.NumNeighbors # name: # type: sq_string # elements: 1 # length: 115 LocalOutlierFactor: property NumNeighbors The number of nearest neighbors used to compute the Local Outlier Factor. # name: # type: sq_string # elements: 1 # length: 73 The number of nearest neighbors used to compute the Local Outlier Factor. # name: # type: sq_string # elements: 1 # length: 33 LocalOutlierFactor.ScoreThreshold # name: # type: sq_string # elements: 1 # length: 106 LocalOutlierFactor: property ScoreThreshold The score above which an observation is flagged as an anomaly. # name: # type: sq_string # elements: 1 # length: 62 The score above which an observation is flagged as an anomaly. # name: # type: sq_string # elements: 1 # length: 28 LocalOutlierFactor.isanomaly # name: # type: sq_string # elements: 1 # length: 513 LocalOutlierFactor: tf = isanomaly (Mdl, Xnew) LocalOutlierFactor: [tf, scores] = isanomaly (Mdl, Xnew) LocalOutlierFactor: [...] = isanomaly (..., 'ScoreThreshold', t) Detect anomalies in the new observations Xnew using the fitted model Mdl. Returns the logical vector tf flagging anomalies and the Local Outlier Factor scores, each computed from the nearest neighbors of Xnew in the training data. The threshold defaults to Mdl.ScoreThreshold and may be overridden with the 'ScoreThreshold' name-value argument. # name: # type: sq_string # elements: 1 # length: 73 Detect anomalies in the new observations Xnew using the fitted model Mdl. # name: # type: sq_string # elements: 1 # length: 11 OneClassSVM # name: # type: sq_string # elements: 1 # length: 589 statistics: OneClassSVM One-class support vector machine model for anomaly detection. A OneClassSVM object stores a one-class support vector machine fitted to a set of observations in an expanded feature space, and detects anomalies through the isanomaly method. Create a OneClassSVM object with ocsvm or the class constructor. The model maps the data to a randomized feature space that approximates a Gaussian kernel and fits a linear boundary that encloses the bulk of the observations; points outside the boundary receive higher anomaly scores. See also: ocsvm, OneClassSVM.isanomaly # name: # type: sq_string # elements: 1 # length: 61 One-class support vector machine model for anomaly detection. # name: # type: sq_string # elements: 1 # length: 33 OneClassSVM.ContaminationFraction # name: # type: sq_string # elements: 1 # length: 110 OneClassSVM: property ContaminationFraction The assumed fraction of anomalies in the training data, in [0, 1]. # name: # type: sq_string # elements: 1 # length: 66 The assumed fraction of anomalies in the training data, in [0, 1]. # name: # type: sq_string # elements: 1 # length: 23 OneClassSVM.KernelScale # name: # type: sq_string # elements: 1 # length: 105 OneClassSVM: property KernelScale The scale of the Gaussian kernel approximated by the feature expansion. # name: # type: sq_string # elements: 1 # length: 71 The scale of the Gaussian kernel approximated by the feature expansion. # name: # type: sq_string # elements: 1 # length: 18 OneClassSVM.Lambda # name: # type: sq_string # elements: 1 # length: 80 OneClassSVM: property Lambda The strength of the ridge (L2) regularization term. # name: # type: sq_string # elements: 1 # length: 51 The strength of the ridge (L2) regularization term. # name: # type: sq_string # elements: 1 # length: 14 OneClassSVM.Mu # name: # type: sq_string # elements: 1 # length: 80 OneClassSVM: property Mu The predictor means used for standardization, or empty. # name: # type: sq_string # elements: 1 # length: 55 The predictor means used for standardization, or empty. # name: # type: sq_string # elements: 1 # length: 34 OneClassSVM.NumExpansionDimensions # name: # type: sq_string # elements: 1 # length: 100 OneClassSVM: property NumExpansionDimensions The number of dimensions of the expanded feature space. # name: # type: sq_string # elements: 1 # length: 55 The number of dimensions of the expanded feature space. # name: # type: sq_string # elements: 1 # length: 23 OneClassSVM.OneClassSVM # name: # type: sq_string # elements: 1 # length: 1478 OneClassSVM: Mdl = OneClassSVM (X) OneClassSVM: Mdl = OneClassSVM (..., name, value) Fit a one-class support vector machine. Mdl = OneClassSVM (X) fits a one-class support vector machine to the N-by-P matrix X, whose rows are observations and columns are variables, and returns a OneClassSVM object Mdl. Mdl = OneClassSVM (..., name, value) takes the following Name-Value pairs. Name Value 'KernelScale' the scale of the approximated Gaussian kernel, a positive scalar or 'auto' (default). 'Lambda' the ridge regularization strength, a nonnegative scalar or 'auto' (default). 'NumExpansionDimensions' the number of expanded feature dimensions, a positive integer or 'auto' (default). 'StandardizeData' a logical scalar (default false); when true each predictor is centered and scaled and the means and standard deviations are stored in Mdl.Mu and Mdl.Sigma. 'ContaminationFraction' the assumed fraction of anomalies in X, a scalar in [0, 1] (default 0). It sets Mdl.ScoreThreshold to the 1 - ContaminationFraction quantile of the anomaly scores of X; when it is 0 the threshold is the maximum score and no training observation is flagged. The feature expansion uses random projections, so the model depends on the state of the random number generator and is not reproducible across runs unless the generator is seeded. ocsvm fits the same model and also returns the anomaly indicators and the anomaly scores of the observations in X. See also: ocsvm, OneClassSVM.isanomaly # name: # type: sq_string # elements: 1 # length: 39 Fit a one-class support vector machine. # name: # type: sq_string # elements: 1 # length: 26 OneClassSVM.ScoreThreshold # name: # type: sq_string # elements: 1 # length: 99 OneClassSVM: property ScoreThreshold The score above which an observation is flagged as an anomaly. # name: # type: sq_string # elements: 1 # length: 62 The score above which an observation is flagged as an anomaly. # name: # type: sq_string # elements: 1 # length: 17 OneClassSVM.Sigma # name: # type: sq_string # elements: 1 # length: 97 OneClassSVM: property Sigma The predictor standard deviations used for standardization, or empty. # name: # type: sq_string # elements: 1 # length: 69 The predictor standard deviations used for standardization, or empty. # name: # type: sq_string # elements: 1 # length: 21 OneClassSVM.isanomaly # name: # type: sq_string # elements: 1 # length: 408 OneClassSVM: tf = isanomaly (Mdl, Xnew) OneClassSVM: [tf, scores] = isanomaly (Mdl, Xnew) OneClassSVM: [...] = isanomaly (..., 'ScoreThreshold', t) Detect anomalies in the new observations Xnew using the fitted model Mdl. Returns the logical vector tf flagging anomalies and the anomaly scores. The threshold defaults to Mdl.ScoreThreshold and may be overridden with the 'ScoreThreshold' name-value argument. # name: # type: sq_string # elements: 1 # length: 73 Detect anomalies in the new observations Xnew using the fitted model Mdl. # name: # type: sq_string # elements: 1 # length: 7 iforest # name: # type: sq_string # elements: 1 # length: 2114 statistics: Mdl = iforest (X) statistics: [Mdl, tf] = iforest (X) statistics: [Mdl, tf, scores] = iforest (X) statistics: [...] = iforest (..., name, value) Detect anomalies with an isolation forest. Mdl = iforest (X) fits an isolation forest to the N-by-P matrix X, whose rows are observations and columns are variables, and returns an IsolationForest object Mdl. X must have at least 3 observations, the smallest subsample MATLAB accepts too; MATLAB takes fewer by default and returns a model that cannot tell one observation from another. [Mdl, tf, scores] = iforest (X) also returns the N-by-1 logical vector tf flagging the anomalous observations and the N-by-1 vector scores of anomaly scores in the range [0, 1]. A higher score indicates an observation that is more easily isolated, and therefore more likely to be an anomaly. The score of an observation is 2^(-E[h] / c), where E[h] is its average path length over the isolation trees and c is the expected path length of an unsuccessful search in a binary tree of NumObservationsPerLearner nodes. Each tree is grown from a random subsample of the data by recursively splitting on a random variable at a random value, so anomalies, being easier to isolate, obtain shorter paths. Additional parameters can be specified by Name-Value pair arguments. Name Value 'NumLearners' the number of isolation trees, a positive integer (default 100). 'NumObservationsPerLearner' the subsample size used to grow each tree, an integer in [3, N] (default min (N, 256)). 'ContaminationFraction' the assumed fraction of anomalies in X, a scalar in [0, 1] (default 0). It sets Mdl.ScoreThreshold to quantile (scores, 1 - ContaminationFraction); when it is 0 the threshold is the maximum score and no training observation is flagged. Because the trees are grown from random subsamples and random splits, the scores depend on the state of the random number generator and are not reproducible across runs unless the generator is seeded. Use the isanomaly method of Mdl to detect anomalies in new data. See also: IsolationForest, IsolationForest.isanomaly, lof, robustcov # name: # type: sq_string # elements: 1 # length: 42 Detect anomalies with an isolation forest. # name: # type: sq_string # elements: 1 # length: 3 lof # name: # type: sq_string # elements: 1 # length: 1940 statistics: Mdl = lof (X) statistics: [Mdl, tf] = lof (X) statistics: [Mdl, tf, scores] = lof (X) statistics: [...] = lof (..., name, value) Detect anomalies with the Local Outlier Factor (LOF) method. Mdl = lof (X) fits a Local Outlier Factor model to the N-by-P matrix X, whose rows are observations and columns are variables, and returns a LocalOutlierFactor object Mdl. [Mdl, tf, scores] = lof (X) also returns the N-by-1 logical vector tf flagging the anomalous observations and the N-by-1 vector scores of LOF values. A score near 1 indicates an inlier, whereas a score well above 1 indicates an outlier lying in a region sparser than its neighbors. The Local Outlier Factor of an observation is the average ratio of the local reachability density of its NumNeighbors nearest neighbors to its own local reachability density, where the local reachability density is the inverse mean reachability distance to those neighbors and the reachability distance from p to o is max (k-distance (o), d (p, o)). Additional parameters can be specified by Name-Value pair arguments. Name Value 'NumNeighbors' the number of nearest neighbors, a positive integer less than N. The default is min (20, u - 1), where u is the number of unique observations. 'Distance' the distance metric used to find neighbors, one of the metrics accepted by pdist2 ('euclidean' by default). 'ContaminationFraction' the assumed fraction of anomalies in X, a scalar in [0, 1] (default 0). It sets Mdl.ScoreThreshold to quantile (scores, 1 - ContaminationFraction); when it is 0 the threshold is the maximum score and no training observation is flagged. 'Exponent' the Minkowski distance exponent (default 2), used only with the 'minkowski' distance. 'Cov' the covariance matrix used only with the 'mahalanobis' distance. Use the isanomaly method of Mdl to detect anomalies in new data. See also: LocalOutlierFactor, LocalOutlierFactor.isanomaly, dbscan, robustcov # name: # type: sq_string # elements: 1 # length: 60 Detect anomalies with the Local Outlier Factor (LOF) method. # name: # type: sq_string # elements: 1 # length: 5 ocsvm # name: # type: sq_string # elements: 1 # length: 2543 statistics: Mdl = ocsvm (X) statistics: [Mdl, tf] = ocsvm (X) statistics: [Mdl, tf, scores] = ocsvm (X) statistics: [...] = ocsvm (..., name, value) Detect anomalies with a one-class support vector machine. Mdl = ocsvm (X) fits a one-class support vector machine to the N-by-P matrix X, whose rows are observations and columns are variables, and returns a OneClassSVM object Mdl. [Mdl, tf, scores] = ocsvm (X) also returns the N-by-1 logical vector tf flagging the anomalous observations and the N-by-1 vector scores of anomaly scores. A higher score indicates an observation that lies further outside the boundary enclosing the data, and is therefore more likely to be an anomaly. The observations are mapped to a randomized feature space that approximates a Gaussian kernel of scale KernelScale using NumExpansionDimensions features, and a linear one-class boundary is fitted there with ridge regularization of strength Lambda. Additional parameters can be specified by Name-Value pair arguments. Name Value 'KernelScale' the scale of the approximated Gaussian kernel, a positive scalar or 'auto' (default). 'Lambda' the ridge regularization strength, a nonnegative scalar or 'auto' (default). 'NumExpansionDimensions' the number of expanded feature dimensions, a positive integer or 'auto' (default). 'StandardizeData' a logical scalar (default false); when true each predictor is centered and scaled and the means and standard deviations are stored in Mdl.Mu and Mdl.Sigma. 'ContaminationFraction' the assumed fraction of anomalies in X, a scalar in [0, 1] (default 0). It sets Mdl.ScoreThreshold to quantile (scores, 1 - ContaminationFraction); when it is 0 the threshold is the maximum score and no training observation is flagged. The feature expansion uses random projections, so the scores depend on the state of the random number generator and are not reproducible across runs unless the generator is seeded. The 'auto' selections and the fitted model differ from MATLAB's implementation, which uses a different feature expansion and solver. For a deterministic, classic one-class support vector machine (a nu-SVM with an exact kernel, computed through libsvm), use fitcsvm with a single class in the response or with the 'Nu' name-value argument; that path returns a ClassificationSVM object whose predict method labels observations, rather than the anomaly-scoring interface provided here. Use the isanomaly method of Mdl to detect anomalies in new data. See also: OneClassSVM, OneClassSVM.isanomaly, iforest, lof, fitcsvm # name: # type: sq_string # elements: 1 # length: 57 Detect anomalies with a one-class support vector machine. # name: # type: sq_string # elements: 1 # length: 9 robustcov # name: # type: sq_string # elements: 1 # length: 4456 statistics: sig = robustcov (X) statistics: [sig, mu] = robustcov (X) statistics: [sig, mu, mah] = robustcov (X) statistics: [sig, mu, mah, outliers] = robustcov (X) statistics: [sig, mu, mah, outliers, s] = robustcov (X) statistics: [...] = robustcov (..., name, value) Robust multivariate covariance and mean estimate. sig = robustcov (X) returns a robust estimate sig of the covariance matrix of the NxP data matrix X, computed so that it is not distorted by outlying observations. Rows of X are observations and columns are variables. Rows containing NaN values are removed with a warning. [sig, mu, mah, outliers, s] = robustcov (...) also returns the robust mean mu (1xP), the robust Mahalanobis distances mah (Nx1) of each observation from the estimated distribution, a logical vector outliers (Nx1) flagging observations whose distance exceeds sqrt (chi2inv (0.975, P)), and a structure s holding the estimate metadata. mah and outliers have one row per row of X; for removed rows, mah is NaN and outliers is false. When sig is not positive definite, every distance is 0, removed rows included, and no observation is flagged. Additional parameters can be specified by Name-Value pair arguments. Name Value 'Method' the estimator, one of 'fmcd' (default, the Fast Minimum Covariance Determinant algorithm), 'ogk' (the Orthogonalized Gnanadesikan-Kettenring estimator) or 'olivehawkins' (the Olive-Hawkins concentration algorithm). If any of 'ReweightingMethod', 'StartMethod' or 'NumConcentrationSteps' is given and 'Method' is not, the method is 'olivehawkins'. 'OutlierFraction' the maximum fraction of outliers, a scalar in [0, 0.5] (default 0.5), used to set the size of the subsets in 'fmcd' and 'olivehawkins'. It is not valid for 'ogk'. 'NumTrials' a positive integer. For 'fmcd' it is the number of random elemental subsets drawn (default 500). For 'olivehawkins' it is the number of starts and must equal the number of starts in 'StartMethod' (default 2); with 'elemental' alone it is the number of random starts. It is not valid for 'ogk'. 'BiasCorrection' a logical scalar (default true) that applies the small-sample bias correction to the raw and the reweighted 'fmcd' estimate. Only valid for 'fmcd'. 'NumOGKIterations' the number of orthogonalization iterations for 'ogk', a positive integer (default 2). Only valid for 'ogk'. 'UnivariateEstimator' the robust univariate location/scale estimator used by 'ogk', either 'tauscale' (default) or 'qn'. Only valid for 'ogk'. 'ReweightingMethod' the reweighting used by 'olivehawkins', either 'rfch' (default) or 'rmvn'. 'NumConcentrationSteps' the number of concentration steps taken from each start by 'olivehawkins', a positive integer (default 10). 'StartMethod' the starts of 'olivehawkins': one of 'classical', 'medianball' or 'elemental', a function handle [T, C] = f (X) returning a 1xP location and a PxP scatter, or a cell array of these. The default is {'classical', 'medianball'}. Note on reproducibility. 'fmcd' and the 'elemental' start of 'olivehawkins' draw random subsets, so their exact estimate depends on the random number generator and is not identical to MATLAB's on data where the optimal subset is ambiguous; on well-separated data both converge to the same estimate. Small-sample correction. With 'BiasCorrection' enabled, 'fmcd' scales the raw and the reweighted covariance by the small-sample factors of Pison, Van Aelst and Willems (2002), interpolated over 1 - OutlierFraction, as the covMcd function of the R package robustbase computes them. Where those formulas give negative or very large factors, at the default 'OutlierFraction' for up to 9 variables and small samples, the simulated factors of robustbase are used instead. MATLAB applies the formulas there as well, so it can return a covariance with negative variances (for example, with 3 variables and 6 to 8 observations), and for 2 variables it uses other factors, so its estimate differs from this one at every sample size. When the subset is the whole sample, the classical estimate is reweighted with no small-sample factor. Singular subsets. If 'olivehawkins' finds a singular subset, a warning is issued and the selected estimate is returned without reweighting. A singular estimate gives zero robust distances. When several starts end in singular subsets, the 'medianball' start is selected if present, otherwise the first one listed in 'StartMethod'. See also: mahal, cov, mad, dbscan # name: # type: sq_string # elements: 1 # length: 49 Robust multivariate covariance and mean estimate. statistics-release-2.1.2/inst/Anomaly_Detection/iforest.m000066400000000000000000000152551527311104300235350ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} iforest (@var{X}) ## @deftypefnx {statistics} {[@var{Mdl}, @var{tf}] =} iforest (@var{X}) ## @deftypefnx {statistics} {[@var{Mdl}, @var{tf}, @var{scores}] =} iforest (@var{X}) ## @deftypefnx {statistics} {[@dots{}] =} iforest (@dots{}, @var{name}, @var{value}) ## ## Detect anomalies with an isolation forest. ## ## @code{@var{Mdl} = iforest (@var{X})} fits an isolation forest to the ## @math{N}-by-@math{P} matrix @var{X}, whose rows are observations and columns ## are variables, and returns an @code{IsolationForest} object @var{Mdl}. ## @var{X} must have at least 3 observations, the smallest subsample MATLAB ## accepts too; MATLAB takes fewer by default and returns a model that cannot ## tell one observation from another. ## ## @code{[@var{Mdl}, @var{tf}, @var{scores}] = iforest (@var{X})} also returns ## the @math{N}-by-1 logical vector @var{tf} flagging the anomalous observations ## and the @math{N}-by-1 vector @var{scores} of anomaly scores in the range ## @math{[0, 1]}. A higher score indicates an observation that is more easily ## isolated, and therefore more likely to be an anomaly. ## ## The score of an observation is @code{2^(-E[h] / c)}, where @math{E[h]} is its ## average path length over the isolation trees and @math{c} is the expected ## path length of an unsuccessful search in a binary tree of ## @var{NumObservationsPerLearner} nodes. Each tree is grown from a random ## subsample of the data by recursively splitting on a random variable at a ## random value, so anomalies, being easier to isolate, obtain shorter paths. ## ## Additional parameters can be specified by @qcode{Name-Value} pair arguments. ## ## @multitable @columnfractions 0.34 0.66 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'NumLearners'} @tab the number of isolation trees, a positive ## integer (default 100). ## ## @item @qcode{'NumObservationsPerLearner'} @tab the subsample size used to ## grow each tree, an integer in @math{[3, N]} (default @code{min (@var{N}, ## 256)}). ## ## @item @qcode{'ContaminationFraction'} @tab the assumed fraction of anomalies ## in @var{X}, a scalar in @math{[0, 1]} (default 0). It sets ## @code{@var{Mdl}.ScoreThreshold} to @code{quantile (@var{scores}, 1 - ## @var{ContaminationFraction})}; when it is 0 the threshold is the maximum ## score and no training observation is flagged. ## @end multitable ## ## Because the trees are grown from random subsamples and random splits, the ## scores depend on the state of the random number generator and are not ## reproducible across runs unless the generator is seeded. ## ## Use the @code{isanomaly} method of @var{Mdl} to detect anomalies in new data. ## ## @seealso{IsolationForest, IsolationForest.isanomaly, lof, robustcov} ## @end deftypefn function [Mdl, tf, scores] = iforest (X, varargin) if (nargin < 1) error ("iforest: too few input arguments."); endif Mdl = IsolationForest (X, varargin{:}); tf = Mdl.tf_; scores = Mdl.scores_; endfunction %!demo %! ## Flag a handful of outliers around a Gaussian cluster. %! rng (42); %! X = [randn(200,2); 6 + randn(10,2)]; %! [Mdl, tf, scores] = iforest (X, "ContaminationFraction", 0.05); %! gscatter (X(:,1), X(:,2), tf); %! title ("iforest: inliers vs. flagged anomalies"); ## Well-separated outliers score higher and are flagged (RNG seeded) %!test %! rand ("state", 42); %! X = [randn(60,2)*0.3; 12 12; -11 10; 10 -12]; %! [Mdl, tf, scores] = iforest (X, "ContaminationFraction", 3/63); %! assert_equal (Mdl.NumLearners, 100); %! assert_equal (Mdl.NumObservationsPerLearner, 63); %! assert_equal (all (scores >= 0 & scores <= 1, 'all'), true); %! # outliers rank highest %! assert_equal (all (scores(61:63) > max (scores(1:60)), 'all'), true); %! assert_equal (tf, logical ([false(60,1); true(3,1)])); ## ScoreThreshold follows the quantile rule; contamination 0 flags none %!test %! rand ("state", 7); %! X = [randn(50,2)*0.3; 9 9; -8 7]; %! [Mdl, tf, scores] = iforest (X); %! assert_equal (Mdl.ContaminationFraction, 0); %! assert_equal (Mdl.ScoreThreshold, max (scores), 1e-12); %! assert_equal (tf, false (52, 1)); %! [Mdl2, tf2, s2] = iforest (X, "ContaminationFraction", 0.1); %! assert_equal (Mdl2.ScoreThreshold, quantile (s2, 0.9), 1e-12); %! assert_equal (tf2, s2 > Mdl2.ScoreThreshold); ## NumObservationsPerLearner caps at 256 for large samples %!test %! rand ("state", 1); %! X = randn (400, 2); %! Mdl = iforest (X); %! assert_equal (Mdl.NumObservationsPerLearner, 256); ## isanomaly scores new observations against the trained forest %!test %! rand ("state", 3); %! X = [randn(60,2)*0.3; 12 12; -11 10; 10 -12]; %! Mdl = iforest (X, "ContaminationFraction", 3/63); %! [tf, scores] = isanomaly (Mdl, [0 0; 15 15]); %! assert_equal (all (scores >= 0 & scores <= 1, 'all'), true); %! assert_equal (scores(2) > scores(1), true); # far point scores higher %! assert_equal (tf, logical ([false; true])); ## Test input validation %!error iforest () %!error iforest ([]) %!error iforest ("a") %!error iforest (1) %!error iforest (randn (2, 3)) %!error ... %! iforest (randn (10,2), "NumLearners") %!error ... %! iforest (randn (10,2), "foo", "bar") %!error ... %! iforest (randn (10,2), "NumLearners", 0) %!error ... %! iforest (randn (10,2), "NumObservationsPerLearner", 2) %!error ... %! iforest (randn (10,2), "NumObservationsPerLearner", 20) %!error ... %! iforest (randn (10,2), "ContaminationFraction", 2) statistics-release-2.1.2/inst/Anomaly_Detection/lof.m000066400000000000000000000162371527311104300226430ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} lof (@var{X}) ## @deftypefnx {statistics} {[@var{Mdl}, @var{tf}] =} lof (@var{X}) ## @deftypefnx {statistics} {[@var{Mdl}, @var{tf}, @var{scores}] =} lof (@var{X}) ## @deftypefnx {statistics} {[@dots{}] =} lof (@dots{}, @var{name}, @var{value}) ## ## Detect anomalies with the Local Outlier Factor (LOF) method. ## ## @code{@var{Mdl} = lof (@var{X})} fits a Local Outlier Factor model to the ## @math{N}-by-@math{P} matrix @var{X}, whose rows are observations and columns ## are variables, and returns a @code{LocalOutlierFactor} object @var{Mdl}. ## ## @code{[@var{Mdl}, @var{tf}, @var{scores}] = lof (@var{X})} also returns the ## @math{N}-by-1 logical vector @var{tf} flagging the anomalous observations and ## the @math{N}-by-1 vector @var{scores} of LOF values. A score near 1 ## indicates an inlier, whereas a score well above 1 indicates an outlier lying ## in a region sparser than its neighbors. ## ## The Local Outlier Factor of an observation is the average ratio of the local ## reachability density of its @var{NumNeighbors} nearest neighbors to its own ## local reachability density, where the local reachability density is the ## inverse mean reachability distance to those neighbors and the reachability ## distance from @math{p} to @math{o} is @code{max (k-distance (o), d (p, o))}. ## ## Additional parameters can be specified by @qcode{Name-Value} pair arguments. ## ## @multitable @columnfractions 0.28 0.72 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'NumNeighbors'} @tab the number of nearest neighbors, a positive ## integer less than @math{N}. The default is @code{min (20, @var{u} - 1)}, ## where @var{u} is the number of unique observations. ## ## @item @qcode{'Distance'} @tab the distance metric used to find neighbors, one ## of the metrics accepted by @code{pdist2} (@qcode{'euclidean'} by default). ## ## @item @qcode{'ContaminationFraction'} @tab the assumed fraction of anomalies ## in @var{X}, a scalar in @math{[0, 1]} (default 0). It sets ## @code{@var{Mdl}.ScoreThreshold} to @code{quantile (@var{scores}, 1 - ## @var{ContaminationFraction})}; when it is 0 the threshold is the maximum ## score and no training observation is flagged. ## ## @item @qcode{'Exponent'} @tab the Minkowski distance exponent (default 2), ## used only with the @qcode{'minkowski'} distance. ## ## @item @qcode{'Cov'} @tab the covariance matrix used only with the ## @qcode{'mahalanobis'} distance. ## @end multitable ## ## Use the @code{isanomaly} method of @var{Mdl} to detect anomalies in new data. ## ## @seealso{LocalOutlierFactor, LocalOutlierFactor.isanomaly, dbscan, robustcov} ## @end deftypefn function [Mdl, tf, scores] = lof (X, varargin) if (nargin < 1) error ("lof: too few input arguments."); endif Mdl = LocalOutlierFactor (X, varargin{:}); tf = Mdl.tf_; scores = Mdl.scores_; endfunction %!demo %! ## Flag a handful of outliers around a Gaussian cluster. %! rng (42); %! X = [randn(100,2); 4 + randn(6,2)]; %! [Mdl, tf, scores] = lof (X, "ContaminationFraction", 0.05); %! gscatter (X(:,1), X(:,2), tf); %! title ("lof: inliers vs. flagged anomalies"); ## MATLAB parity: LOF scores are exact to machine precision (k = 5) %!test %! X = [0 0; 0.1 0.1; 0.2 -0.1; -0.1 0.2; 0.1 -0.2; -0.2 0.1; 0.15 0.05; ... %! -0.05 -0.15; 0.05 0.12; -0.12 -0.05; 5 5; -4 3]; %! [Mdl, tf, scores] = lof (X, "NumNeighbors", 5); %! exp_scores = [1.066572826392391; 0.965333187918473; 1.035972400143920; ... %! 1.019798243339781; 1.055271577857807; 1.047371774849485; ... %! 0.941746286231341; 0.987111513710991; 0.948358372496933; ... %! 1.004238203765983; 29.441894930160952; 20.058918161335612]; %! assert_equal (scores, exp_scores, 1e-12); %! assert_equal (tf, false (12, 1)); # contamination 0 flags none %! assert_equal (Mdl.ScoreThreshold, 29.441894930160952, 1e-12); ## MATLAB parity: ContaminationFraction sets the quantile threshold and flags %!test %! X = [0 0; 0.1 0.1; 0.2 -0.1; -0.1 0.2; 0.1 -0.2; -0.2 0.1; 0.15 0.05; ... %! -0.05 -0.15; 0.05 0.12; -0.12 -0.05; 5 5; -4 3]; %! [Mdl, tf, scores] = lof (X, "NumNeighbors", 5, "ContaminationFraction", 0.2); %! assert_equal (Mdl.ScoreThreshold, 2.965807359886740, 1e-12); %! assert_equal (tf, logical ([0;0;0;0;0;0;0;0;0;0;1;1])); ## MATLAB parity: default NumNeighbors is min (20, unique-1) %!test %! X = [0 0; 0.1 0.1; 0.2 -0.1; -0.1 0.2; 0.1 -0.2; -0.2 0.1; 0.15 0.05; ... %! -0.05 -0.15; 0.05 0.12; -0.12 -0.05; 5 5; -4 3]; %! Mdl = lof (X); %! assert_equal (Mdl.NumNeighbors, 11); ## MATLAB parity: isanomaly reproduces LOF for new observations %!test %! X = [0 0; 0.1 0.1; 0.2 -0.1; -0.1 0.2; 0.1 -0.2; -0.2 0.1; 0.15 0.05; ... %! -0.05 -0.15; 0.05 0.12; -0.12 -0.05; 5 5; -4 3]; %! Mdl = lof (X, "NumNeighbors", 5); %! [tf, scores] = isanomaly (Mdl, [0 0.05; 6 6; -0.1 -0.1]); %! assert_equal (scores, [0.954484172585537; 27.876003442184082; ... %! 1.001784195086020], 1e-12); %! assert_equal (tf, logical ([0; 0; 0])); # cutoff 29.44 from training %! [tf2, ~] = isanomaly (Mdl, [6 6], "ScoreThreshold", 5); %! assert_equal (tf2, true); ## A non-euclidean metric is passed through to pdist2 %!test %! X = [0 0; 0.1 0.1; 0.2 -0.1; -0.1 0.2; 0.1 -0.2; -0.2 0.1; 0.15 0.05; ... %! -0.05 -0.15; 0.05 0.12; -0.12 -0.05; 5 5; -4 3]; %! [~, ~, scores] = lof (X, "NumNeighbors", 5, "Distance", "cityblock"); %! assert_equal (scores(11), 34.198447151536712, 1e-10); %! assert_equal (scores(1) > 1 && scores(7) < 1, true); ## Test input validation %!error lof () %!error lof ([]) %!error lof ("a") %!error lof (ones (5,2), "Distance") %!error lof (ones (5,2), "foo", "bar") %!error ... %! lof (ones (5,2), "Distance", "taxicab") %!error ... %! lof (ones (5,2), "NumNeighbors", 0) %!error ... %! lof (ones (5,2), "NumNeighbors", 5) %!error ... %! lof (magic (5), "NumNeighbors", 2, "ContaminationFraction", 1.5) %!error ... %! isanomaly (lof (magic (6)), ones (3,3)) statistics-release-2.1.2/inst/Anomaly_Detection/ocsvm.m000066400000000000000000000171061527311104300232060ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} ocsvm (@var{X}) ## @deftypefnx {statistics} {[@var{Mdl}, @var{tf}] =} ocsvm (@var{X}) ## @deftypefnx {statistics} {[@var{Mdl}, @var{tf}, @var{scores}] =} ocsvm (@var{X}) ## @deftypefnx {statistics} {[@dots{}] =} ocsvm (@dots{}, @var{name}, @var{value}) ## ## Detect anomalies with a one-class support vector machine. ## ## @code{@var{Mdl} = ocsvm (@var{X})} fits a one-class support vector machine to ## the @math{N}-by-@math{P} matrix @var{X}, whose rows are observations and ## columns are variables, and returns a @code{OneClassSVM} object @var{Mdl}. ## ## @code{[@var{Mdl}, @var{tf}, @var{scores}] = ocsvm (@var{X})} also returns the ## @math{N}-by-1 logical vector @var{tf} flagging the anomalous observations and ## the @math{N}-by-1 vector @var{scores} of anomaly scores. A higher score ## indicates an observation that lies further outside the boundary enclosing the ## data, and is therefore more likely to be an anomaly. ## ## The observations are mapped to a randomized feature space that approximates a ## Gaussian kernel of scale @var{KernelScale} using @var{NumExpansionDimensions} ## features, and a linear one-class boundary is fitted there with ridge ## regularization of strength @var{Lambda}. ## ## Additional parameters can be specified by @qcode{Name-Value} pair arguments. ## ## @multitable @columnfractions 0.34 0.66 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'KernelScale'} @tab the scale of the approximated Gaussian ## kernel, a positive scalar or @qcode{'auto'} (default). ## ## @item @qcode{'Lambda'} @tab the ridge regularization strength, a nonnegative ## scalar or @qcode{'auto'} (default). ## ## @item @qcode{'NumExpansionDimensions'} @tab the number of expanded feature ## dimensions, a positive integer or @qcode{'auto'} (default). ## ## @item @qcode{'StandardizeData'} @tab a logical scalar (default ## @code{false}); when @code{true} each predictor is centered and scaled and the ## means and standard deviations are stored in @code{@var{Mdl}.Mu} and ## @code{@var{Mdl}.Sigma}. ## ## @item @qcode{'ContaminationFraction'} @tab the assumed fraction of anomalies ## in @var{X}, a scalar in @math{[0, 1]} (default 0). It sets ## @code{@var{Mdl}.ScoreThreshold} to @code{quantile (@var{scores}, 1 - ## @var{ContaminationFraction})}; when it is 0 the threshold is the maximum ## score and no training observation is flagged. ## @end multitable ## ## The feature expansion uses random projections, so the scores depend on the ## state of the random number generator and are not reproducible across runs ## unless the generator is seeded. The @qcode{'auto'} selections and the fitted ## model differ from MATLAB's implementation, which uses a different feature ## expansion and solver. ## ## For a deterministic, classic one-class support vector machine (a ## nu-SVM with an exact kernel, computed through @code{libsvm}), use ## @code{fitcsvm} with a single class in the response or with the @qcode{'Nu'} ## name-value argument; that path returns a @code{ClassificationSVM} object ## whose @code{predict} method labels observations, rather than the ## anomaly-scoring interface provided here. ## ## Use the @code{isanomaly} method of @var{Mdl} to detect anomalies in new data. ## ## @seealso{OneClassSVM, OneClassSVM.isanomaly, iforest, lof, fitcsvm} ## @end deftypefn function [Mdl, tf, scores] = ocsvm (X, varargin) if (nargin < 1) error ("ocsvm: too few input arguments."); endif Mdl = OneClassSVM (X, varargin{:}); tf = Mdl.tf_; scores = Mdl.scores_; endfunction %!demo %! ## Flag a handful of outliers around a Gaussian cluster. %! rng (42); %! X = [randn(200,2); 5 + randn(10,2)]; %! [Mdl, tf, scores] = ocsvm (X, "KernelScale", 2, ... %! "ContaminationFraction", 0.05); %! gscatter (X(:,1), X(:,2), tf); %! title ("ocsvm: inliers vs. flagged anomalies"); ## Well-separated outliers score higher and are flagged (RNG seeded) %!test %! rand ("state", 42); %! randn ("state", 42); %! X = [randn(60,2)*0.3; 10 10; -9 8; 8 -10]; %! [Mdl, tf, scores] = ocsvm (X, "KernelScale", 2, ... %! "NumExpansionDimensions", 128, ... %! "ContaminationFraction", 3/63); %! # outliers rank highest %! assert_equal (all (scores(61:63) > max (scores(1:60)), 'all'), true); %! assert_equal (tf, logical ([false(60,1); true(3,1)])); ## ScoreThreshold follows the quantile rule; contamination 0 flags none %!test %! rand ("state", 7); %! randn ("state", 7); %! X = [randn(50,2)*0.3; 9 9; -8 7]; %! [Mdl, tf, scores] = ocsvm (X, "KernelScale", 2); %! assert_equal (Mdl.ContaminationFraction, 0); %! assert_equal (Mdl.ScoreThreshold, max (scores), 1e-12); %! assert_equal (tf, false (52, 1)); %! [Mdl2, tf2, s2] = ocsvm (X, "KernelScale", 2, "ContaminationFraction", 0.1); %! assert_equal (Mdl2.ScoreThreshold, quantile (s2, 0.9), 1e-12); %! assert_equal (tf2, s2 > Mdl2.ScoreThreshold); ## 'auto' defaults are resolved to concrete numeric values %!test %! rand ("state", 1); %! randn ("state", 1); %! X = randn (64, 3); %! Mdl = ocsvm (X); %! assert_equal (Mdl.KernelScale, 1); %! assert_equal (Mdl.Lambda, 1/64, 1e-12); %! assert_equal (Mdl.NumExpansionDimensions, 64); # 2^ceil(log2(64)) ## StandardizeData stores Mu and Sigma and isanomaly reuses them %!test %! rand ("state", 3); %! randn ("state", 3); %! X = [randn(60,2)*[3 0; 0 0.2] + [5 -2]; 20 5]; %! Mdl = ocsvm (X, "KernelScale", 2, "StandardizeData", true); %! assert_equal (numel (Mdl.Mu), 2); %! assert_equal (numel (Mdl.Sigma), 2); %! [tf, scores] = isanomaly (Mdl, [5 -2; 20 5]); %! assert_equal (scores(2) > scores(1), true); # the outlier scores higher ## isanomaly scores new observations against the trained model %!test %! rand ("state", 5); %! randn ("state", 5); %! X = [randn(60,2)*0.3; 10 10; -9 8]; %! Mdl = ocsvm (X, "KernelScale", 2, "ContaminationFraction", 2/62); %! [tf, scores] = isanomaly (Mdl, [0 0; 15 15]); %! assert_equal (scores(2) > scores(1), true); %! assert_equal (tf, logical ([false; true])); ## Test input validation %!error ocsvm () %!error ocsvm ([]) %!error ocsvm ("a") %!error ... %! ocsvm (randn (10,2), "KernelScale") %!error ... %! ocsvm (randn (10,2), "foo", "bar") %!error ... %! ocsvm (randn (10,2), "KernelScale", -1) %!error ... %! ocsvm (randn (10,2), "Lambda", -1) %!error ... %! ocsvm (randn (10,2), "NumExpansionDimensions", 0) %!error ... %! ocsvm (randn (10,2), "ContaminationFraction", 2) statistics-release-2.1.2/inst/Anomaly_Detection/robustcov.m000066400000000000000000002032171527311104300241050ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{sig} =} robustcov (@var{X}) ## @deftypefnx {statistics} {[@var{sig}, @var{mu}] =} robustcov (@var{X}) ## @deftypefnx {statistics} {[@var{sig}, @var{mu}, @var{mah}] =} robustcov (@var{X}) ## @deftypefnx {statistics} {[@var{sig}, @var{mu}, @var{mah}, @var{outliers}] =} robustcov (@var{X}) ## @deftypefnx {statistics} {[@var{sig}, @var{mu}, @var{mah}, @var{outliers}, @var{s}] =} robustcov (@var{X}) ## @deftypefnx {statistics} {[@dots{}] =} robustcov (@dots{}, @var{name}, @var{value}) ## ## Robust multivariate covariance and mean estimate. ## ## @code{@var{sig} = robustcov (@var{X})} returns a robust estimate @var{sig} of ## the covariance matrix of the @math{N*P} data matrix @var{X}, computed so that ## it is not distorted by outlying observations. Rows of @var{X} are ## observations and columns are variables. Rows containing @code{NaN} values ## are removed with a warning. ## ## @code{[@var{sig}, @var{mu}, @var{mah}, @var{outliers}, @var{s}] = robustcov ## (@dots{})} also returns the robust mean @var{mu} (@math{1*P}), the robust ## Mahalanobis distances @var{mah} (@math{N*1}) of each observation from the ## estimated distribution, a logical vector @var{outliers} (@math{N*1}) flagging ## observations whose distance exceeds @code{sqrt (chi2inv (0.975, @var{P}))}, ## and a structure @var{s} holding the estimate metadata. @var{mah} and ## @var{outliers} have one row per row of @var{X}; for removed rows, @var{mah} ## is @code{NaN} and @var{outliers} is @code{false}. When @var{sig} is not ## positive definite, every distance is 0, removed rows included, and no ## observation is flagged. ## ## Additional parameters can be specified by @qcode{Name-Value} pair arguments. ## ## @multitable @columnfractions 0.22 0.76 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Method'} @tab the estimator, one of @qcode{'fmcd'} (default, ## the Fast Minimum Covariance Determinant algorithm), @qcode{'ogk'} (the ## Orthogonalized Gnanadesikan-Kettenring estimator) or ## @qcode{'olivehawkins'} (the Olive-Hawkins concentration algorithm). If any ## of @qcode{'ReweightingMethod'}, @qcode{'StartMethod'} or ## @qcode{'NumConcentrationSteps'} is given and @qcode{'Method'} is not, the ## method is @qcode{'olivehawkins'}. ## ## @item @qcode{'OutlierFraction'} @tab the maximum fraction of outliers, a ## scalar in @math{[0, 0.5]} (default 0.5), used to set the size of the ## subsets in @qcode{'fmcd'} and @qcode{'olivehawkins'}. It is not valid for ## @qcode{'ogk'}. ## ## @item @qcode{'NumTrials'} @tab a positive integer. For @qcode{'fmcd'} it is ## the number of random elemental subsets drawn (default 500). For ## @qcode{'olivehawkins'} it is the number of starts and must equal the number ## of starts in @qcode{'StartMethod'} (default 2); with @qcode{'elemental'} ## alone it is the number of random starts. It is not valid for ## @qcode{'ogk'}. ## ## @item @qcode{'BiasCorrection'} @tab a logical scalar (default @code{true}) ## that applies the small-sample bias correction to the raw and the reweighted ## @qcode{'fmcd'} estimate. Only valid for @qcode{'fmcd'}. ## ## @item @qcode{'NumOGKIterations'} @tab the number of orthogonalization ## iterations for @qcode{'ogk'}, a positive integer (default 2). Only valid ## for @qcode{'ogk'}. ## ## @item @qcode{'UnivariateEstimator'} @tab the robust univariate ## location/scale estimator used by @qcode{'ogk'}, either @qcode{'tauscale'} ## (default) or @qcode{'qn'}. Only valid for @qcode{'ogk'}. ## ## @item @qcode{'ReweightingMethod'} @tab the reweighting used by ## @qcode{'olivehawkins'}, either @qcode{'rfch'} (default) or @qcode{'rmvn'}. ## ## @item @qcode{'NumConcentrationSteps'} @tab the number of concentration ## steps taken from each start by @qcode{'olivehawkins'}, a positive integer ## (default 10). ## ## @item @qcode{'StartMethod'} @tab the starts of @qcode{'olivehawkins'}: one of ## @qcode{'classical'}, @qcode{'medianball'} or @qcode{'elemental'}, a function ## handle @code{[T, C] = f (X)} returning a @math{1*P} location and a ## @math{P*P} scatter, or a cell array of these. The default is ## @code{@{'classical', 'medianball'@}}. ## @end multitable ## ## @strong{Note on reproducibility.} @qcode{'fmcd'} and the @qcode{'elemental'} ## start of @qcode{'olivehawkins'} draw random subsets, so their exact estimate ## depends on the random number generator and is not identical to ## MATLAB's on data where the optimal subset is ambiguous; on well-separated ## data both converge to the same estimate. ## ## @strong{Small-sample correction.} With @qcode{'BiasCorrection'} enabled, ## @qcode{'fmcd'} scales the raw and the reweighted covariance by the ## small-sample factors of Pison, Van Aelst and Willems (2002), interpolated ## over @math{1 - @var{OutlierFraction}}, as the @code{covMcd} function of the ## R package robustbase computes them. Where those formulas give negative or ## very large factors, at the default @qcode{'OutlierFraction'} for up to 9 ## variables and small samples, the simulated factors of robustbase are used ## instead. MATLAB applies the formulas there as well, so it can return a ## covariance with negative variances (for example, with 3 variables and 6 to ## 8 observations), and for 2 variables it uses other factors, so its estimate ## differs from this one at every sample size. When the subset is the whole ## sample, the classical estimate is reweighted with no small-sample factor. ## ## @strong{Singular subsets.} If @qcode{'olivehawkins'} finds a singular ## subset, a warning is issued and the selected estimate is returned without ## reweighting. A singular estimate gives zero robust distances. When ## several starts end in singular subsets, the @qcode{'medianball'} start is ## selected if present, otherwise the first one listed in @qcode{'StartMethod'}. ## ## @seealso{mahal, cov, mad, dbscan} ## @end deftypefn function [sig, mu, mah, outliers, s] = robustcov (X, varargin) ## Check number of input arguments if (nargin < 1) error ("robustcov: too few input arguments."); endif ## Validate X and drop rows with NaN values if (! isnumeric (X) || ! isreal (X) || ndims (X) != 2 || isempty (X)) error ("robustcov: X must be a nonempty real numeric matrix."); endif nanrows = any (isnan (X), 2); if (any (nanrows)) warning ("robustcov: rows with missing data were removed."); endif X = X(! nanrows, :); [n, p] = size (X); ## Defaults method = "fmcd"; outlierfrac = 0.5; numtrials = []; biascorrection = true; numogkiter = 2; univestimator = "tauscale"; rewmethod = "rfch"; numconc = 10; startmethod = []; given = {}; ## Parse Name-Value pairs if (mod (numel (varargin), 2) != 0) error ("robustcov: each NAME must be followed by a VALUE."); endif while (numel (varargin) > 0) name = varargin{1}; val = varargin{2}; if (! ischar (name)) error ("robustcov: optional argument names must be strings."); endif switch (tolower (name)) case "method" method = tolower (val); case "outlierfraction" outlierfrac = val; case "numtrials" numtrials = val; case "biascorrection" biascorrection = logical (val); case "numogkiterations" numogkiter = val; case "univariateestimator" univestimator = tolower (val); case "reweightingmethod" rewmethod = tolower (val); case "numconcentrationsteps" numconc = val; case "startmethod" startmethod = val; if (ischar (val)) startmethod = tolower (val); endif otherwise error ("robustcov: unknown parameter name '%s'.", name); endswitch given{end+1} = tolower (name); varargin(1:2) = []; endwhile ## Validate options if (! all (cellfun (@ischar, {method, univestimator, rewmethod}))) error (strcat ("robustcov: METHOD, UNIVARIATEESTIMATOR and", ... " REWEIGHTINGMETHOD must be strings.")); endif if (! any (strcmp (method, {"fmcd", "ogk", "olivehawkins"}))) error ("robustcov: invalid METHOD '%s'.", method); endif isoh = ismember (given, {"reweightingmethod", "startmethod", ... "numconcentrationsteps"}); if (any (isoh)) if (! any (strcmp (given, "method"))) method = "olivehawkins"; elseif (! strcmp (method, "olivehawkins")) error ("robustcov: %s is only valid for the 'olivehawkins' method.", ... upper (given{find (isoh, 1)})); endif endif if (any (strcmp (given, "biascorrection")) && ! strcmp (method, "fmcd")) error ("robustcov: BIASCORRECTION is only valid for the 'fmcd' method."); endif isogk = ismember (given, {"numogkiterations", "univariateestimator"}); if (any (isogk) && ! strcmp (method, "ogk")) error ("robustcov: %s is only valid for the 'ogk' method.", ... upper (given{find (isogk, 1)})); endif notogk = ismember (given, {"numtrials", "outlierfraction"}); if (strcmp (method, "ogk") && any (notogk)) error ("robustcov: %s is not valid for the 'ogk' method.", ... upper (given{find (notogk, 1)})); endif if (! isscalar (outlierfrac) || ! isnumeric (outlierfrac) || outlierfrac < 0 || outlierfrac > 0.5) error ("robustcov: OUTLIERFRACTION must be a scalar in [0, 0.5]."); endif if (! any (strcmp (univestimator, {"tauscale", "qn"}))) error ("robustcov: invalid UNIVARIATEESTIMATOR '%s'.", univestimator); endif if (! any (strcmp (rewmethod, {"rfch", "rmvn"}))) error ("robustcov: invalid REWEIGHTINGMETHOD '%s'.", rewmethod); endif if (! isposint (numconc)) error ("robustcov: NUMCONCENTRATIONSTEPS must be a positive integer."); endif if (! isempty (numtrials) && ! isposint (numtrials)) error ("robustcov: NUMTRIALS must be a positive integer."); endif if (n < 2 * p) error ("robustcov: X must have at least twice as many rows as columns."); endif if (strcmp (method, "olivehawkins")) if (ischar (startmethod) && strcmp (startmethod, "elemental")) if (isempty (numtrials)) numtrials = 2; endif starts = repmat ({"elemental"}, 1, numtrials); else starts = oh_start_list (startmethod); if (isempty (numtrials)) numtrials = numel (starts); elseif (numtrials != numel (starts)) error (strcat ("robustcov: NUMTRIALS must equal the number of", ... " starts specified in STARTMETHOD.")); endif endif elseif (isempty (numtrials)) numtrials = 500; endif ## Robust cutoff for reweighting and for flagging outliers. cutoff = chi2inv (0.975, p); if (strcmp (method, "fmcd")) h = subsetsize (n, p, outlierfrac); alpha = 1 - outlierfrac; if (h == n) ## The whole sample is the only subset: classical estimates, and no ## small-sample factor at either step. Traw = mean (X); Sraw = cov (X); correct = false; else ## Raw Fast-MCD subset and its robust distances. [Traw, Sraw] = fastmcd (X, h, numtrials); Sraw = mcdcons (p, h / n) * Sraw; correct = biascorrection; if (correct) Sraw = mcdcnp2 (p, n, alpha, "raw") * Sraw; endif endif ## Reweighting: keep observations within the 0.975 cutoff. d2 = mahal2 (X, Traw, Sraw); keep = d2 <= cutoff; mu = mean (X(keep, :)); C = cov (X(keep, :)); hk = sum (keep); fac = mcdcons (p, hk / n); if (correct) fac *= mcdcnp2 (p, n, alpha, "rew"); endif sig = fac * C; elseif (strcmp (method, "ogk")) ## Orthogonalized Gnanadesikan-Kettenring, then MCD-style reweighting. [Togk, Sogk] = ogk (X, numogkiter, univestimator); d2 = mahal2 (X, Togk, Sogk); ## Scale so that the median squared distance matches the chi-square median, ## making the reweighting cutoff meaningful. Maronna and Zamar reweight at ## the 0.9 quantile, not the 0.975 used to flag outliers. scale = median (d2) / chi2inv (0.5, p); d2 = d2 / scale; keep = d2 <= chi2inv (0.9, p); mu = mean (X(keep, :)); sig = cov (X(keep, :), 1); # 1/N normalization, as MATLAB's OGK else h = subsetsize (n, p, outlierfrac); hs = [h, h]; if (outlierfrac == 0) hs(2) = max (n - ceil (n / 5), p + 1); endif [mu, sig] = olivehawkins (X, hs, numconc, starts, rewmethod); endif ## Final robust Mahalanobis distances and outlier flags. md2 = mahal2 (X, mu, sig); mah = double (sqrt (md2)); outliers = md2 > cutoff; ## Restore one row per row of the input for rows removed as missing. if (any (nanrows)) dall = zeros (numel (nanrows), 1); oall = false (numel (nanrows), 1); if (ispdmat (sig)) dall(nanrows) = NaN; dall(! nanrows) = mah; oall(! nanrows) = outliers; endif mah = dall; outliers = oall; endif if (nargout > 4) if (strcmp (method, "fmcd")) s = struct ("BiasCorrection", biascorrection, "NumTrials", numtrials, ... "OutlierFraction", outlierfrac, "Mu", mu, "Sigma", sig, ... "Method", "fmcd", "Distances", mah, "Outliers", outliers); elseif (strcmp (method, "ogk")) s = struct ("NumOGKIterations", numogkiter, ... "UnivariateScale", univestimator, "Mu", mu, "Sigma", sig, ... "Method", "ogk", "Distances", mah, "Outliers", outliers); else s = struct ("OutlierFraction", outlierfrac, ... "reweightingMethod", rewmethod, "NumTrials", numtrials, ... "Mu", mu, "Sigma", sig, "Method", "olivehawkins", ... "Distances", mah, "Outliers", outliers); endif endif endfunction ## Squared Mahalanobis distances of the rows of X to (T, S). function d2 = mahal2 (X, T, S) if (! ispdmat (S)) d2 = zeros (rows (X), 1); return; endif R = chol (S); Z = (X - T) / R; d2 = sum (Z .^ 2, 2); endfunction ## One MCD concentration step (C-step): keep the h closest points, recompute. function [T, S, H, dt] = cstep (X, T, S, h) d2 = mahal2 (X, T, S); [~, ord] = sort (d2); H = ord(1:h); T = mean (X(H, :)); S = cov (X(H, :)); dt = det (S); endfunction ## Fast-MCD raw location and scatter of the best h-subset. function [Tbest, Sbest] = fastmcd (X, h, nsamp) [n, p] = size (X); bestdet = Inf; Tbest = mean (X); Sbest = cov (X); ## Candidate starts: random elemental subsets plus the classical estimate. cand = cell (nsamp + 1, 1); for t = 1:nsamp idx = randperm (n, p + 1); T = mean (X(idx, :)); S = cov (X(idx, :)); k = p + 1; while (rcond (S) < 1e-12 && k < h) k += 1; idx = randperm (n, k); T = mean (X(idx, :)); S = cov (X(idx, :)); endwhile cand{t} = {T, S}; endfor cand{nsamp + 1} = {mean(X), cov(X)}; ## Two C-steps from each start, then keep the best few and iterate them. dets = inf (nsamp + 1, 1); sols = cell (nsamp + 1, 1); for t = 1:(nsamp + 1) T = cand{t}{1}; S = cand{t}{2}; if (rcond (S) < 1e-12) continue; endif [T, S] = cstep (X, T, S, h); [T, S, H, dt] = cstep (X, T, S, h); dets(t) = dt; sols{t} = {T, S}; endfor [~, ord] = sort (dets); nbest = min (10, sum (isfinite (dets))); for j = 1:nbest t = ord(j); T = sols{t}{1}; S = sols{t}{2}; dt_prev = Inf; for it = 1:50 [T, S, H, dt] = cstep (X, T, S, h); if (dt >= dt_prev * (1 - 1e-12)) break; endif dt_prev = dt; endfor if (dt < bestdet) bestdet = dt; Tbest = T; Sbest = S; endif endfor endfunction ## MCD consistency factor c = alpha / F_{chi2_{p+2}}(chi2inv(alpha, p)). function c = mcdcons (p, alpha) if (alpha >= 1) c = 1; else c = alpha / chi2cdf (chi2inv (alpha, p), p + 2); endif endfunction ## Small-sample correction factor (Pison et al.) of the raw or the reweighted ## MCD, as robustbase's covMcd computes it: the formulas at alpha 0.5 and ## 0.875, interpolated linearly in alpha and to 1 at alpha 1. At alpha 0.5, ## where the formulas give negative or very large factors (p up to 9, n from ## 2p to NMAX), the factors simulated for robustbase are used instead. For ## p = 1 that would be n = 2 alone, where h = n and no factor is applied. function f = mcdcnp2 (p, n, alpha, kind) if (strcmp (kind, "raw")) p1 = [0.262024211897096, 0.604756680630497; ... -0.351584646688712, 1.01646567502486]; p2 = [0.673292623522027, 0.691365864961895; ... 0.446537815635445, 1.06690782995919]; c500 = [-1.42764571687802, 1.26263336932151, 2; ... -1.06141115981725, 1.28907991440387, 3]; c875 = [-0.455179464070565, 1.11192541278794, 2; ... -0.294241208320834, 1.09649329149811, 3]; else p1 = [1.11098143415027, 1.5182890270453; ... -0.66046776772861, 0.88939595831888]; p2 = [3.11101712909049, 1.91401056721863; ... 0.79473550581058, 1.10081930350091]; c500 = [-1.02842572724793, 1.67659883081926, 2; ... -0.26800273450853, 1.35968562893582, 3]; c875 = [-0.544482443573914, 1.25994483222292, 2; ... -0.343791072183285, 1.25159004257133, 3]; endif if (p == 1) fp500 = 1 - exp (p1(1,1)) / n ^ p1(1,2); fp875 = 1 - exp (p1(2,1)) / n ^ p1(2,2); elseif (p == 2) fp500 = 1 - exp (p2(1,1)) / n ^ p2(1,2); fp875 = 1 - exp (p2(2,1)) / n ^ p2(2,2); else fp500 = mcdcnp2_solve (c500, p, n); fp875 = mcdcnp2_solve (c875, p, n); endif if (alpha == 0.5) fsim = mcdcnp2_sim (p, n, kind); if (! isempty (fsim)) fp500 = 1 / fsim; endif endif if (alpha <= 0.875) fp = fp500 + (fp875 - fp500) / 0.375 * (alpha - 0.5); else fp = fp875 + (1 - fp875) / 0.125 * (alpha - 0.875); endif f = 1 / fp; endfunction ## Simulated small-sample factors at alpha 0.5, from the MCDcnp2s table of ## robustbase (covMcd.R): one row per p from 2 to 9, n from 2p to NMAX. ## Empty outside the table. function f = mcdcnp2_sim (p, n, kind) nmax = [6, 10, 13, 16, 18, 20, 20, 20]; f = []; if (p < 2 || p > 9 || n > nmax(p - 1)) return; endif if (strcmp (kind, "raw")) tab = {[3.15342, 1.799889, 2.258497], ... [2.12777, 2.718898, 1.993509, 2.220433, 1.820585], ... [2.950549, 2.145387, 2.382224, 1.927077, 2.032489, 1.8371], ... [2.229573, 2.494447, 2.016117, 2.190061, 1.877996, 1.978964, ... 1.767284], ... [2.580264, 2.109121, 2.240741, 1.944719, 2.043397, 1.821808, ... 1.89725], ... [2.165233, 2.320281, 2.007836, 2.086471, 1.884052, 1.950563, ... 1.76926], ... [2.340443, 2.048262, 2.161097, 1.926082, 1.995422], ... [2.093386, 2.185793, 1.948989]}; else tab = {[2.216302, 1.418526, 1.635601], ... [1.73511, 2.06681, 1.545905, 1.659655, 1.456835], ... [2.388892, 1.847626, 1.96998, 1.630723, 1.701272, 1.521008], ... [1.968153, 2.168931, 1.784373, 1.894409, 1.667912, 1.693007, ... 1.545176], ... [2.275891, 1.866587, 2.014249, 1.750567, 1.829363, 1.650019, ... 1.689043], ... [1.95771, 2.074066, 1.847385, 1.905408, 1.71393, 1.768425, ... 1.63908], ... [2.164121, 1.871024, 1.979485, 1.782417, 1.84489], ... [1.938822, 2.050409, 1.834863]}; endif f = tab{p - 1}(n - 2 * p + 1); endfunction ## Shared p > 2 interpolation for the Pison correction coefficients. Each row ## of COEFF is [coefficient, exponent, reference-dimension] for the two anchor ## dimensions (p = 2 and p = 3), following the robustbase covMcd formulas. function fp = mcdcnp2_solve (coeff, p, n) a = coeff(:, 1); b = coeff(:, 2); dim = coeff(:, 3); y = log (- a ./ p .^ b); A = [1, - log(dim(1) * p ^ 2); 1, - log(dim(2) * p ^ 2)]; c = A \ y; fp = 1 - exp (c(1)) / n ^ c(2); endfunction ## Orthogonalized Gnanadesikan-Kettenring robust location and scatter. ## Maintains the affine invariant x = A * z + centre where z is the current ## (rotated) coordinate of an observation, so the final diagonal scatter and ## location in z-space map straight back to the original coordinates. function [T, C] = ogk (X, niter, estimator) [n, p] = size (X); Z = X; A = eye (p); centre = zeros (p, 1); for iter = 1:niter ## Robust marginal location/scale of the current coordinates. m = zeros (p, 1); d = zeros (1, p); for j = 1:p [d(j), m(j)] = uniscale (Z(:, j), estimator); endfor d(d == 0) = 1; Y = (Z - m') ./ d; # standardized coordinates ## Pairwise robust covariance (Gnanadesikan-Kettenring). U = eye (p); for j = 1:p for k = (j + 1):p sp = uniscale (Y(:, j) + Y(:, k), estimator); sm = uniscale (Y(:, j) - Y(:, k), estimator); U(j, k) = 0.25 * (sp ^ 2 - sm ^ 2); U(k, j) = U(j, k); endfor endfor ## Orthogonalizing rotation E; update the affine map and rotate the data. [E, ~] = eig ((U + U') / 2); centre = A * m + centre; # uses the pre-update A A = A * diag (d) * E; Z = Y * E; endfor ## Robust location/scale in the final rotated coordinates map straight back. gm = zeros (p, 1); gs = zeros (1, p); for j = 1:p [gs(j), gm(j)] = uniscale (Z(:, j), estimator); endfor T = (A * gm + centre)'; C = A * diag (gs .^ 2) * A'; C = (C + C') / 2; endfunction ## Robust univariate scale (and location) via tau-scale or Qn. The tau-scale ## thresholds apply to residuals over the raw MAD. A constant factor on the ## scale cancels in the reweighted estimate, so the tau-scale carries no ## consistency factor. function [s, m] = uniscale (x, estimator) x = x(:); med = median (x); s0 = median (abs (x - med)); if (s0 == 0) s = 0; m = med; return; endif if (strcmp (estimator, "qn")) m = med; s = qn (x); else ## Tau-scale (Maronna & Zamar): bisquare-weighted mean, bounded rho scale. c1 = 4.5; r = (x - med) / s0; w = (1 - (r / c1) .^ 2) .^ 2; w(abs (r) > c1) = 0; m = sum (w .* x) / sum (w); c2 = 3.0; r2 = (x - m) / s0; rho = min (r2 .^ 2, c2 ^ 2); s = s0 * sqrt (mean (rho)); endif endfunction ## Qn robust scale estimator (Croux & Rousseeuw). function s = qn (x) x = x(:); n = numel (x); d = abs (x - x.'); diffs = sort (d(tril (true (n), -1))); h = floor (n / 2) + 1; k = h * (h - 1) / 2; if (k < 1) k = 1; endif s = 2.2219 * diffs(k); endfunction ## Subset size shared by the FMCD and Olive-Hawkins estimators. function h = subsetsize (n, p, outlierfrac) h = floor (2 * floor ((n + p + 1) / 2) - n ... + 2 * (n - floor ((n + p + 1) / 2)) * (1 - outlierfrac)); h = max (min (h, n), p + 1); endfunction function tf = isposint (x) tf = isnumeric (x) && isscalar (x) && isreal (x) && isfinite (x) ... && x >= 1 && x == fix (x); endfunction ## True for a finite, numerically positive definite matrix. function tf = ispdmat (S) tf = all (isfinite (S(:))); if (tf) [~, flag] = chol (S); tf = (flag == 0 && rank (S) == rows (S)); endif endfunction ## Normalize STARTMETHOD into a cell array of start names and function handles. function starts = oh_start_list (startmethod) names = {"classical", "medianball", "elemental"}; msg = strcat ("robustcov: STARTMETHOD must be a name, a function", ... " handle, or a cell array of them."); if (isnumeric (startmethod) && isempty (startmethod)) starts = {"classical", "medianball"}; elseif (ischar (startmethod) || is_function_handle (startmethod)) starts = {startmethod}; elseif (iscell (startmethod) && ! isempty (startmethod)) starts = startmethod(:).'; else error (msg); endif for i = 1:numel (starts) if (ischar (starts{i})) starts{i} = tolower (starts{i}); if (! any (strcmp (starts{i}, names))) error ("robustcov: invalid start name '%s' in STARTMETHOD.", starts{i}); endif elseif (! is_function_handle (starts{i})) error (msg); endif endfor endfunction ## Initial location and scatter of one Olive-Hawkins start. function [T, S] = oh_start (X, spec, h) [n, p] = size (X); if (is_function_handle (spec)) [T, S] = spec (X); if (! isequal (size (T), [1, p]) || ! isequal (size (S), [p, p])) error (strcat ("robustcov: STARTMETHOD function must return a", ... " 1xP location and a PxP scatter.")); endif elseif (strcmp (spec, "classical")) T = mean (X); S = cov (X); elseif (strcmp (spec, "medianball")) m = median (X, 1); d = sqrt (sum ((X - m) .^ 2, 2)); idx = d <= median (d); T = mean (X(idx, :)); S = cov (X(idx, :)); else k = p + 1; idx = randperm (n, k); S = cov (X(idx, :)); while (! ispdmat (S) && k < h) k += 1; idx = randperm (n, k); S = cov (X(idx, :)); endwhile T = mean (X(idx, :)); endif endfunction ## K concentration steps from (T, S); a start with a finite singular scatter ## is kept as it is. function [T, S, ok, sing] = oh_conc (X, T, S, h, k) ok = false; sing = false; if (! ispdmat (S)) sing = true; ok = all (isfinite ([T(:); S(:)])); return; endif for i = 1:k [~, ord] = sort (mahal2 (X, T, S)); idx = ord(1:h); Tn = mean (X(idx, :)); Sn = cov (X(idx, :)); if (! ispdmat (Sn)) sing = true; return; endif T = Tn; S = Sn; ok = true; endfor endfunction ## Olive-Hawkins estimate: attractors from each start, selection, reweighting. function [T, S] = olivehawkins (X, hs, k, starts, rewmethod) nst = numel (starts); Ts = cell (1, nst); Ss = cell (1, nst); ok = false (1, nst); sing = false; for i = 1:nst hi = hs(1 + ! (ischar (starts{i}) && strcmp (starts{i}, "classical"))); [T0, S0] = oh_start (X, starts{i}, hi); [Ts{i}, Ss{i}, ok(i), si] = oh_conc (X, T0, S0, hi, k); sing = sing || si; endfor if (! any (ok)) error (strcat ("robustcov: unable to find a positive definite", ... " covariance estimate.")); endif if (sing) warning (strcat ("robustcov: a singular subset was found while", ... " running the Olive-Hawkins estimate.")); endif idx = find (ok); if (any (cellfun (@(t) ischar (t) && strcmp (t, "medianball"), starts))) m = median (X, 1); r = median (sqrt (sum ((X - m) .^ 2, 2))); inball = cellfun (@(t) norm (t - m), Ts(idx)) <= r; if (any (inball)) idx = idx(inball); endif endif dets = zeros (1, numel (idx)); for i = 1:numel (idx) if (ispdmat (Ss{idx(i)})) dets(i) = det (Ss{idx(i)}); endif endfor ismb = cellfun (@(t) ischar (t) && strcmp (t, "medianball"), starts(idx)); ord = [find(ismb), find(! ismb)]; [~, j] = min (dets(ord)); j = idx(ord(j)); T = Ts{j}; S = Ss{j}; if (! sing) [T, S] = oh_reweight (X, T, S, rewmethod); endif endfunction ## Two reweighting steps; RFCH rescales by the median distance, RMVN by the ## truncated normal consistency factor. function [T, S] = oh_reweight (X, T, S, rewmethod) [n, p] = size (X); cp = 0.975; cut = chi2inv (cp, p); c50 = chi2inv (0.5, p); S = oh_rescale (X, T, S, c50); for stage = 1:2 keep = mahal2 (X, T, S) <= cut; if (sum (keep) <= p) break; endif Tn = mean (X(keep, :)); Sn = cov (X(keep, :)); if (! ispdmat (Sn)) break; endif ref = c50; if (strcmp (rewmethod, "rmvn")) ref = chi2inv (0.5 * cp * n / sum (keep), p); endif T = Tn; S = oh_rescale (X, Tn, Sn, ref); endfor endfunction ## Scale S so that the median squared distance of X to (T, S) equals REF. function S = oh_rescale (X, T, S, ref) scale = median (mahal2 (X, T, S)) / ref; if (scale > 0 && isfinite (scale)) S *= scale; endif endfunction %!demo %! ## Robust covariance is unaffected by a cluster of outliers. %! rng (42); %! X = [randn(80,2); 8 + randn(10,2)]; %! [sig, mu, mah, outliers] = robustcov (X); %! gscatter (X(:,1), X(:,2), outliers); %! title ("robustcov: inliers vs. flagged outliers"); %!demo %! ## Olive-Hawkins robust distances expose outliers that the classical %! ## Mahalanobis distances mask. %! rng (42); %! X = [randn(80,2); 6 + randn(10,2)]; %! [~, ~, mah, outliers] = robustcov (X, "Method", "olivehawkins"); %! dcl = sqrt (mahal (X, X)); %! plot (dcl(! outliers), mah(! outliers), "bo", ... %! dcl(outliers), mah(outliers), "r+"); %! xlabel ("Classical Mahalanobis distance"); %! ylabel ("Robust distance"); %! legend ("inliers", "flagged outliers"); %!demo %! ## Tolerance ellipses of the two Olive-Hawkins reweighting methods. %! rng (42); %! X = [randn(80,2); 6 + randn(10,2)]; %! [s1, m1] = robustcov (X, "ReweightingMethod", "rfch"); %! [s2, m2] = robustcov (X, "ReweightingMethod", "rmvn"); %! t = linspace (0, 2 * pi, 100)'; %! c = sqrt (chi2inv (0.975, 2)); %! e1 = m1 + c * [cos(t), sin(t)] * chol (s1); %! e2 = m2 + c * [cos(t), sin(t)] * chol (s2); %! plot (X(:,1), X(:,2), "k.", e1(:,1), e1(:,2), "b-", e2(:,1), e2(:,2), "r-"); %! axis equal; %! legend ("data", "rfch", "rmvn"); ## FMCD without bias correction is bit-exact: consistency factor c(p, |J|/n) %!test %! X = [0.1 0.2; -0.3 0.5; 0.4 -0.1; 0.2 0.3; -0.2 -0.4; 0.5 0.1; -0.1 0.2; ... %! 0.3 -0.3; -0.4 0.1; 0.2 -0.2; 0.1 0.4; -0.3 -0.2; 5 5; -6 4]; %! [sig, mu, mah, ol] = robustcov (X, "BiasCorrection", 0); %! assert_equal (mu, [0.041666666666667 0.05], 1e-12); %! assert_equal (sig, [0.130395818314974 -0.012781705320642; ... %! -0.012781705320642 0.122435282545100], 1e-9); %! assert_equal (ol, logical ([0;0;0;0;0;0;0;0;0;0;0;0;1;1])); ## OGK is bit-exact on separated data: covariance of the retained set (1/N) %!test %! X = [0.1 0.2; -0.3 0.5; 0.4 -0.1; 0.2 0.3; -0.2 -0.4; 0.5 0.1; -0.1 0.2; ... %! 0.3 -0.3; -0.4 0.1; 0.2 -0.2; 0.1 0.4; -0.3 -0.2; 5 5; -6 4]; %! [sig, mu, mah, ol] = robustcov (X, "Method", "ogk"); %! assert_equal (mu, [0.041666666666667 0.05], 1e-12); %! assert_equal (sig, [0.080763888888889 -0.007916666666667; ... %! -0.007916666666667 0.075833333333333], 1e-9); %! assert_equal (ol, logical ([0;0;0;0;0;0;0;0;0;0;0;0;1;1])); ## FMCD with default bias correction inflates the covariance (formula factor) %!test %! X = [0.1 0.2; -0.3 0.5; 0.4 -0.1; 0.2 0.3; -0.2 -0.4; 0.5 0.1; -0.1 0.2; ... %! 0.3 -0.3; -0.4 0.1; 0.2 -0.2; 0.1 0.4; -0.3 -0.2; 5 5; -6 4]; %! sig0 = robustcov (X, "BiasCorrection", 0); %! sig1 = robustcov (X); %! r = sig1 ./ sig0; %! # scalar inflation factor %! assert_equal (all (abs (r - r(1)) < 1e-12, 'all'), true); %! assert_equal (r(1) > 1, true); ## mah is the robust Mahalanobis distance, outliers use the 0.975 cutoff %!test %! X = [0.1 0.2; -0.3 0.5; 0.4 -0.1; 0.2 0.3; -0.2 -0.4; 0.5 0.1; -0.1 0.2; ... %! 0.3 -0.3; -0.4 0.1; 0.2 -0.2; 0.1 0.4; -0.3 -0.2; 5 5; -6 4]; %! [sig, mu, mah, ol] = robustcov (X); %! d2 = sum (((X - mu) / chol (sig)) .^ 2, 2); %! assert_equal (mah, sqrt (d2), 1e-12); %! assert_equal (ol, mah > sqrt (chi2inv (0.975, 2))); ## The metadata structure carries the expected fields %!test %! X = [randn(40,2); 10 10; -10 8]; %! [sig, mu, mah, ol, s] = robustcov (X); %! assert_equal (isfield (s, "Method") && strcmp (s.Method, "fmcd"), true); %! assert_equal (isfield (s, "Sigma") && isfield (s, "Mu"), true); %! assert_equal (isfield (s, "Distances") && isfield (s, "Outliers"), true); %! [~, ~, ~, ~, s2] = robustcov (X, "Method", "ogk"); %! assert_equal (strcmp (s2.Method, "ogk"), true); %! assert_equal (isfield (s2, "NumOGKIterations"), true); ## OGK is bit-exact in 3-D: cube vertices have identity covariance (1/N) %!test %! X = [1 1 1; 1 1 -1; 1 -1 1; 1 -1 -1; -1 1 1; -1 1 -1; -1 -1 1; -1 -1 -1; ... %! 10 10 10; -10 8 -9]; %! [sig, mu, mah, ol] = robustcov (X, "Method", "ogk"); %! assert_equal (sig, eye (3), 1e-12); %! assert_equal (mu, [0 0 0], 1e-12); %! assert_equal (ol, logical ([0;0;0;0;0;0;0;0;1;1])); ## OGK on moderate contamination: expected values from MATLAB R2024a %!shared A, B, C, D, E, F, G, H %! A = [0.34 0.6; 0.32 0.96; -0.49 0.23; -0.84 -0.8; 2.77 -0.6; -0.21 -1.31; ... %! -1.57 -2.15; -0.76 1.76; -0.57 0.68; -0.88 -0.18; -1.39 -0.6; ... %! 1.75 0.58; -0.54 0.15; -0.52 -0.7; -1.7 0.16; 1.26 -0.11; ... %! -1.05 0.84; -0.25 1.72; 1.09 -1.17; 2.23 2.41; 3.22 3.23; ... %! 1.76 4.47; 1.9 2.17; 1.23 1.13]; %! B = [1.39 -0.25 -0.72; -0.31 -0.24 1.55; -0.02 -0.07 -1.59; ... %! 1.5 -0.84 0.91; -0.04 0.02 -0.01; -1.04 -1.34 -0.22; ... %! -0.3 -0.25 0.62; -0.42 -2.01 -0.04; -0.81 -0.14 1.13; ... %! 0.91 0.5 1.91; -0.06 -2.11 -2; 0.08 -1.65 -0.98; -0.85 0.25 -0.14; ... %! -0.12 0.12 0.66; -0.84 -1.26 -0.92; 0.12 -0.3 0.54; ... %! -0.3 -0.19 0.87; -0.22 0.14 0.76; 0 -1.45 1.23; 2.96 0.65 3.32; ... %! 3.93 0.25 -0.76; 1.93 3.02 0.77; 3.35 1.08 2.56; 2.19 1.23 3]; %! C = [0.29 -0.61 -1.36; -0.17 -0.49 1.17; 0.69 0.11 -0.14; ... %! 0.42 0.66 -1.19; 0.58 -0.41 -0.62; -0.77 -1.08 1.15; ... %! -1.01 -0.89 -0.12; -0.17 0.4 -0.69; -1.13 2.84 0.86; ... %! 1.51 -0.72 -0.72; 0.64 2.95 -0.08; -0.12 -1.92 -0.55; ... %! 1.57 -1.36 -1.85; -0.02 0.51 -0.32; -1.18 -0.25 -1.38; ... %! -2.82 -0.44 -0.05; 0.82 -1.14 -0.01; -0.25 0.81 -1.19; ... %! 1.42 -0.67 -0.98; -0.14 2.88 1.61; 2.25 1.46 1.94; ... %! -0.18 3.07 1.89; 2.67 1.94 2.91; 2.39 -0.03 1.95]; %! D = [A(1:12,:); A(13:24,:) + 8]; %! E = [A(1:14,:); A(15:24,:) + 6]; %! F = A(1:23,:); %! G = A(1:17,:); %! H = [B, C(:,1)]; %!test %! [sig, mu, ~, ol] = robustcov (A, "Method", "ogk"); %! assert_equal (sig, [1.722767148760330 1.020670247933884; ... %! 1.020670247933884 1.646486776859504], 1e-12); %! assert_equal (mu, [0.116818181818182 0.436363636363636], 1e-12); %! assert_equal (find (ol)', [5, 22]); %!test %! [sig, mu, ~, ol] = robustcov (A, "Method", "ogk", ... %! "UnivariateEstimator", "qn"); %! assert_equal (sig, [1.324408616780046 0.636798412698413; ... %! 0.636798412698413 1.335555555555556], 1e-12); %! assert_equal (mu, [-0.030952380952381 0.303333333333333], 1e-12); %! assert_equal (find (ol)', [5, 21, 22]); %!test %! [sig, mu, ~, ol] = robustcov (B, "Method", "ogk"); %! assert_equal (sig, [0.4791385802469 0.0781234567901 0.0867438271605; ... %! 0.0781234567901 0.5096617283951 0.2086302469136; ... %! 0.0867438271605 0.2086302469136 0.8511209876543], ... %! 1e-12); %! assert_equal (mu, [-0.0705555555556 -0.4977777777778 0.3088888888889], ... %! 1e-12); %! assert_equal (find (ol)', 20:24); %!test %! [sig, mu, ~, ol] = robustcov (B, "Method", "ogk", ... %! "UnivariateEstimator", "qn"); %! assert_equal (sig, [0.673841 0.264091 0.378851; ... %! 0.264091 0.737886 0.606261; ... %! 0.378851 0.606261 1.394296], 1e-12); %! assert_equal (mu, [0.043 -0.492 0.328], 1e-12); %! assert_equal (find (ol)', 20:23); %!test %! [sig, mu, ~, ol] = robustcov (C, "Method", "ogk", "NumOGKIterations", 1); %! assert_equal (sig, [0.64969 -0.302265 -0.327265; ... %! -0.302265 2.27578475 0.799012; ... %! -0.327265 0.799012 1.068354], 1e-12); %! assert_equal (mu, [0.14 0.2345 -0.226], 1e-12); %! assert_equal (find (ol)', [16, 21, 23, 24]); %!test %! [sig, mu, ~, ol] = robustcov (C, "Method", "ogk", "NumOGKIterations", 1, ... %! "UnivariateEstimator", "qn"); %! assert_equal (sig, [1.159454320988 -0.2871074074074 -0.3650679012346; ... %! -0.2871074074074 1.065891666667 0.1486175925926; ... %! -0.3650679012346 0.1486175925926 0.7041015432099], ... %! 1e-12); %! assert_equal (mu, [-0.0188888888889 -0.2583333333333 -0.4438888888889], ... %! 1e-12); %! assert_equal (find (ol)', [9, 11, 20:24]); ## Olive-Hawkins values %!test %! [sig, mu, ~, ol] = robustcov (A, "Method", "olivehawkins"); %! assert_equal (sig, [2.18619861641093 1.05116184773748; ... %! 1.05116184773748 2.20459884562983], 1e-12); %! assert_equal (mu, [-0.0309523809523809 0.303333333333333], 1e-12); %! assert_equal (find (ol)', 22); %!test %! [sig, mu, ~, ol] = robustcov (B, "Method", "olivehawkins"); %! assert_equal (sig, ... %! [0.539535973554938 0.0742685780314062 0.248554707369894; ... %! 0.0742685780314062 0.97500208295236 0.643292674523965; ... %! 0.248554707369894 0.643292674523965 1.65058254051043], 1e-12); %! assert_equal (mu, ... %! [-0.151111111111111 -0.601111111111111 0.237777777777778], ... %! 1e-12); %! assert_equal (find (ol)', 20:24); %!test %! [sig, mu, ~, ol] = robustcov (D, "Method", "olivehawkins"); %! assert_equal (sig, [32.4372591926917 31.9759974697833; ... %! 31.9759974697833 33.1062365099099], 1e-11); %! assert_equal (mu, [3.88 4.32238095238095], 1e-12); %! assert_equal (find (ol)', 5); %!test %! [sig, mu, ~, ol] = robustcov (E, "Method", "olivehawkins"); %! assert_equal (sig, [4.44755827554569 0.827837857580459; ... %! 0.827837857580459 3.27753304453576], 1e-12); %! assert_equal (mu, [-0.185 -0.0985714285714285], 1e-12); %! assert_equal (find (ol)', 15:24); %!test %! [sig, mu, ~, ol] = robustcov (G, "Method", "olivehawkins"); %! assert_equal (sig, [0.671430029617765 0.214180155408852; ... %! 0.214180155408852 1.06777070102528], 1e-12); %! assert_equal (mu, [-0.573333333333333 -0.0313333333333333], 1e-12); %! assert_equal (find (ol)', [5 12]); %!test %! [sig, mu, ~, ol] = robustcov (H, "Method", "olivehawkins"); %! assert_equal (sig, [1.4773032249017 0.633509046093723 0.903971467669543 ... %! 1.01676000987605; ... %! 0.633509046093723 1.05742783733659 0.955810531417285 ... %! 0.610833205323175; ... %! 0.903971467669543 0.955810531417285 2.01745154899905 ... %! 0.768192889806327; ... %! 1.01676000987605 0.610833205323175 0.768192889806327 ... %! 1.41197078963033], 1e-12); %! assert_equal (mu, [0.2045 -0.423 0.429 0.409], 1e-12); %! assert_equal (find (ol)', [16 20 21 22]); ## Olive-Hawkins with ReweightingMethod rmvn %!test %! [sig, mu, ~, ol] = robustcov (A, "Method", "olivehawkins", ... %! "ReweightingMethod", "rmvn"); %! assert_equal (sig, [1.86045727877771 0.894539817250082; ... %! 0.894539817250082 1.8761158928325], 1e-12); %! assert_equal (mu, [-0.0309523809523809 0.303333333333333], 1e-12); %! assert_equal (find (ol)', [5 22]); %!test %! [sig, mu, ~, ol] = robustcov (B, "Method", "olivehawkins", ... %! "ReweightingMethod", "rmvn"); %! assert_equal (sig, ... %! [0.244711990865239 0.107713539485174 0.194534248297532; ... %! 0.107713539485174 0.63579655102699 0.366819588781046; ... %! 0.194534248297532 0.366819588781046 0.717288266600757], ... %! 1e-12); %! assert_equal (mu, [-0.276 -0.52 0.464], 1e-12); %! assert_equal (find (ol)', [1 3 4 11 20:24]); %!test %! [sig, mu, ~, ol] = robustcov (D, "Method", "olivehawkins", ... %! "ReweightingMethod", "rmvn"); %! assert_equal (sig, [23.5704669292852 22.5233205014285; ... %! 22.5233205014285 21.9411394241276], 1e-11); %! assert_equal (mu, [3.840625 3.975], 1e-12); %! assert_equal (find (ol)', [5 8 15 17 18 19 22]); %!test %! [sig, mu, ~, ol] = robustcov (G, "Method", "olivehawkins", ... %! "ReweightingMethod", "rmvn"); %! assert_equal (sig, [0.608331988611545 0.356119261505905; ... %! 0.356119261505905 1.67877003559776], 1e-12); %! assert_equal (mu, [-0.704285714285714 -0.0257142857142857], 1e-12); %! assert_equal (find (ol)', [5 12 16]); %!test %! [sig, mu, ~, ol] = robustcov (H, "Method", "olivehawkins", ... %! "ReweightingMethod", "rmvn"); %! assert_equal (sig, [0.800488564971132 0.378310709774622 0.451742172174145 ... %! 0.643235580770011; ... %! 0.378310709774622 0.894583657008321 0.78836418951942 ... %! 0.334654019100639; ... %! 0.451742172174145 0.78836418951942 1.74955325492995 ... %! 0.504616991965037; ... %! 0.643235580770011 0.334654019100639 0.504616991965037 ... %! 0.995733910134607], 1e-12); %! assert_equal (mu, ... %! [0.0883333333333333 -0.543888888888889 0.342222222222222 ... %! 0.218888888888889], 1e-12); %! assert_equal (find (ol)', [13 16 20:23]); %!test %! [sig, mu] = robustcov (A(:,1), "ReweightingMethod", "rmvn"); %! assert_equal (size (sig), [1 1]); %! assert_equal (size (mu), [1 1]); %! assert_equal (sig > 0, true); %!test %! i = (1:80)'; %! X = [norminv(0.001 + 0.998 * mod (i * 0.6180339887, 1)), ... %! norminv(0.001 + 0.998 * mod (i * 0.7548776662, 1)), ... %! norminv(0.001 + 0.998 * mod (i * 0.5698402909, 1))]; %! r = robustcov (X, "Method", "olivehawkins") ./ cov (X); %! assert_equal (r(1,1), 1.08696964749719, 1e-10); %! r = robustcov (X, "Method", "olivehawkins", ... %! "ReweightingMethod", "rmvn") ./ cov (X); %! assert_equal (r(1,1), 1.1180955548406, 1e-10); %!test %! i = (1:40)'; %! X = [norminv(0.001 + 0.998 * mod (i * 0.6180339887, 1)), ... %! norminv(0.001 + 0.998 * mod (i * 0.7548776662, 1))]; %! X(1:6,:) += 7; %! [sig, ~, ~, ol] = robustcov (X, "Method", "olivehawkins"); %! r = sig ./ cov (X(! ol,:)); %! assert_equal (sum (ol), 6); %! assert_equal (r(1,1), 1.15911540848535, 1e-10); %! [sig, ~, ~, ol] = robustcov (X, "Method", "olivehawkins", ... %! "ReweightingMethod", "rmvn"); %! r = sig ./ cov (X(! ol,:)); %! assert_equal (sum (ol), 6); %! assert_equal (r(1,1), 0.942767404122863, 1e-10); ## NumConcentrationSteps is an exact step count %!test %! [sig, mu] = robustcov (B, "Method", "olivehawkins", ... %! "NumConcentrationSteps", 1); %! assert_equal (sig, ... %! [1.25376010044175 0.491174146513676 0.923105291911069; ... %! 0.491174146513676 0.929536611388943 0.893493059181142; ... %! 0.923105291911069 0.893493059181142 2.11540419879994], ... %! 1e-12); %! assert_equal (mu, ... %! [0.181904761904762 -0.437619047619048 0.47047619047619], ... %! 1e-12); %! sig2 = robustcov (B, "Method", "olivehawkins", "NumConcentrationSteps", 2); %! assert_equal (sig2, robustcov (B, "Method", "olivehawkins"), 1e-12); %!test %! [sig, mu] = robustcov (D, "Method", "olivehawkins", ... %! "NumConcentrationSteps", 2); %! assert_equal (sig, [25.2931432445733 24.9466427944171; ... %! 24.9466427944171 26.3210109498855], 1e-11); %! assert_equal (mu, [4.36217391304348 4.78565217391304], 1e-12); %! sig3 = robustcov (D, "Method", "olivehawkins", "NumConcentrationSteps", 3); %! assert_equal (sig3, robustcov (D, "Method", "olivehawkins"), 1e-12); %!test %! sig1 = robustcov (A, "Method", "olivehawkins", "NumConcentrationSteps", 1); %! sig30 = robustcov (A, "Method", "olivehawkins", "NumConcentrationSteps", 30); %! assert_equal (sig1, sig30, 1e-12); ## OutlierFraction sets the concentration subset size %!test %! [sig, mu] = robustcov (A, "Method", "olivehawkins", "OutlierFraction", 0); %! assert_equal (sig, [1.91585537598172 0.594684519490792; ... %! 0.594684519490792 1.56440812034418], 1e-12); %! assert_equal (mu, [0.0963636363636364 0.262272727272727], 1e-12); %!test %! [sig, mu] = robustcov (A, "Method", "olivehawkins", "OutlierFraction", 0.1); %! assert_equal (sig, [2.17025568191781 0.963936502972127; ... %! 0.963936502972127 1.81121588386622], 1e-12); %! assert_equal (mu, [0.232173913043478 0.391304347826087], 1e-12); %! sig4 = robustcov (A, "Method", "olivehawkins", "OutlierFraction", 0.4); %! assert_equal (sig4, sig, 1e-12); %!test %! sig = robustcov (A, "Method", "olivehawkins", "OutlierFraction", 0.05); %! assert_equal (sig(1,1), 2.07915771918762, 1e-12); %!test %! sig = robustcov (C, "Method", "olivehawkins", "OutlierFraction", 0.2); %! assert_equal (sig(1,1), 0.963904697885623, 1e-12); %!test %! sig = robustcov (F, "Method", "olivehawkins", "OutlierFraction", 0.3); %! assert_equal (sig(1,1), 2.263922006869, 1e-12); %!test %! sig = robustcov (H, "Method", "olivehawkins", "OutlierFraction", 0); %! assert_equal (sig(1,1), 0.90133965942982, 1e-12); %!test %! [sig, mu] = robustcov (A(1:10,:), "Method", "olivehawkins", ... %! "OutlierFraction", 0); %! assert_equal (sig, [0.531998699152933 0.65445152173685; ... %! 0.65445152173685 1.54235378327137], 1e-12); %! assert_equal (mu, [-0.4875 -0.24625], 1e-12); %!test %! sig = robustcov (A(1:17,:), "Method", "olivehawkins", "OutlierFraction", 0); %! assert_equal (sig, robustcov (G, "Method", "olivehawkins"), 1e-12); %!test %! [sig, mu] = robustcov (C(1:12,:), "Method", "olivehawkins", ... %! "OutlierFraction", 0); %! assert_equal (sig, ... %! [0.682305684670065 0.202111073398596 -0.396039314208948; ... %! 0.202111073398596 0.712912692984757 -0.230146533982739; ... %! -0.396039314208948 -0.230146533982739 0.940572931634535], ... %! 1e-12); %! assert_equal (mu, [0.125 -0.495 -0.307], 1e-12); %!test %! [sig, mu] = robustcov (C(1:15,:), "Method", "olivehawkins", ... %! "OutlierFraction", 0.02); %! assert_equal (sig, ... %! [0.714900001270072 -0.158312562788954 -0.345298199782001; ... %! -0.158312562788954 1.80384391946054 0.31843983688034; ... %! -0.345298199782001 0.31843983688034 0.788227595223814], ... %! 1e-12); %! assert_equal (mu, ... %! [0.0753333333333333 -0.0173333333333333 ... %! -0.389333333333333], 1e-12); %!test %! [sig, mu] = robustcov (A, "Method", "olivehawkins", "OutlierFraction", 0.02); %! assert_equal (sig, [2.23760836399026 1.59153534794663; ... %! 1.59153534794663 2.86229325333207], 1e-12); %! assert_equal (mu, [0.188260869565217 0.611739130434783], 1e-12); %!test %! [sig, mu] = robustcov (A, "Method", "olivehawkins", ... %! "StartMethod", "classical", "OutlierFraction", 0); %! assert_equal (sig, [2.07915771918762 1.14430744580376; ... %! 1.14430744580376 2.35957206487605], 1e-12); %! assert_equal (mu, [0.295833333333333 0.56125], 1e-12); %! [sig, mu] = robustcov (A(1:10,:), "Method", "olivehawkins", ... %! "StartMethod", "classical", "OutlierFraction", 0); %! assert_equal (sig, [0.528700494767184 0.548289625422536; ... %! 0.548289625422536 2.14245039772679], 1e-12); %! assert_equal (mu, [-0.517777777777778 -0.0233333333333333], 1e-12); %!test %! sig1 = robustcov (A, "Method", "olivehawkins", ... %! "StartMethod", "medianball", "OutlierFraction", 0, ... %! "NumConcentrationSteps", 1); %! sig10 = robustcov (A, "Method", "olivehawkins", ... %! "StartMethod", "medianball", "OutlierFraction", 0, ... %! "NumConcentrationSteps", 10); %! assert_equal (sig1, sig10, 1e-12); %! assert_equal (sig1, robustcov (A, "Method", "olivehawkins", ... %! "OutlierFraction", 0), 1e-12); ## Starts and attractors %!test %! sig = robustcov (A, "Method", "olivehawkins", "StartMethod", "classical"); %! assert_equal (sig, [1.91585537598172 0.594684519490792; ... %! 0.594684519490792 1.56440812034418], 1e-12); %! sigmb = robustcov (A, "Method", "olivehawkins", "StartMethod", "medianball"); %! assert_equal (sigmb, robustcov (A, "Method", "olivehawkins"), 1e-12); %!test %! sig = robustcov (A, "Method", "olivehawkins"); %! sig1 = robustcov (A, "Method", "olivehawkins", ... %! "StartMethod", {"classical", "medianball"}); %! sig2 = robustcov (A, "Method", "olivehawkins", ... %! "StartMethod", {"medianball", "classical"}); %! assert_equal (sig1, sig, 1e-12); %! assert_equal (sig2, sig, 1e-12); %!test %! f = @(x) deal (mean (x), cov (x)); %! sig = robustcov (A, "Method", "olivehawkins", "StartMethod", f); %! assert_equal (sig, robustcov (A, "Method", "olivehawkins", ... %! "StartMethod", "classical"), 1e-12); %!test %! g = @(x) deal (median (x), eye (columns (x))); %! s1 = robustcov (B, "Method", "olivehawkins", "StartMethod", "medianball"); %! s2 = robustcov (B, "Method", "olivehawkins", "StartMethod", g); %! assert_equal (max (abs (s1(:) - s2(:))), 0.354744, 1e-6); %! s1 = robustcov (H, "Method", "olivehawkins", "StartMethod", "medianball"); %! s2 = robustcov (H, "Method", "olivehawkins", "StartMethod", g); %! assert_equal (max (abs (s1(:) - s2(:))), 0.749506, 1e-6); %! s1 = robustcov (G, "Method", "olivehawkins", "StartMethod", "medianball"); %! s2 = robustcov (G, "Method", "olivehawkins", "StartMethod", g); %! assert_equal (s1, s2, 1e-12); %!test %! rand ("state", 1); %! sig1 = robustcov (A, "StartMethod", "elemental", "NumTrials", 5); %! rand ("state", 1); %! sig2 = robustcov (A, "StartMethod", "elemental", "NumTrials", 5); %! assert_equal (sig1, sig2); %! assert_equal (sig1, sig1', 1e-12); %! assert_equal (all (eig (sig1) > 0), true); %!test %! [~, ~, ~, ~, s] = robustcov (A, "StartMethod", "elemental"); %! assert_equal (s.NumTrials, 2); %! [sig, ~, ~, ~, s] = robustcov (A, "StartMethod", "elemental", ... %! "NumTrials", 1); %! assert_equal (s.NumTrials, 1); %! assert_equal (size (sig), [2 2]); ## Method resolution and the metadata structure %!test %! [~, ~, ~, ~, s] = robustcov (A, "StartMethod", "medianball"); %! assert_equal (s.Method, "olivehawkins"); %! [~, ~, ~, ~, s] = robustcov (A, "NumTrials", 2); %! assert_equal (s.Method, "fmcd"); %! sig = robustcov (A, "ReweightingMethod", "rmvn"); %! assert_equal (sig, robustcov (A, "Method", "olivehawkins", ... %! "ReweightingMethod", "rmvn"), 1e-12); %!test %! [sig, mu, mah, ol, s] = robustcov (A, "Method", "olivehawkins"); %! assert_equal (s.Sigma, sig); %! assert_equal (s.Mu, mu); %! assert_equal (s.Distances, mah); %! assert_equal (s.Outliers, ol); %! assert_equal (fieldnames (s)', {"OutlierFraction", "reweightingMethod", ... %! "NumTrials", "Mu", "Sigma", "Method", ... %! "Distances", "Outliers"}); %! assert_equal (s.NumTrials, 2); %! assert_equal (s.reweightingMethod, "rfch"); %!test %! [~, mu, ~, ~, s] = robustcov (A, "Method", "olivehawkins", ... %! "ReweightingMethod", "rmvn", ... %! "NumConcentrationSteps", 5, ... %! "StartMethod", "medianball", ... %! "NumTrials", 1, "OutlierFraction", 0.3); %! assert_equal (mu, [0.116818181818182 0.436363636363636], 1e-12); %! assert_equal (s.NumTrials, 1); %! assert_equal (s.OutlierFraction, 0.3); %! assert_equal (s.reweightingMethod, "rmvn"); ## Olive-Hawkins on small, univariate, degenerate and non-finite data %!test %! x1 = A(:,1); %! [sig, mu, mah, ol] = robustcov (x1, "Method", "olivehawkins"); %! assert_equal (sig, 2.64311810032498, 1e-12); %! assert_equal (mu, 0.295833333333333, 1e-12); %! assert_equal (mah(1:3), [0.0271666657367651; 0.0148647793653997; ... %! 0.483361618674896], 1e-12); %! assert_equal (ol, false (24, 1)); %!test %! [sig, mu, mah, ol] = robustcov (A(1:6,:), "Method", "olivehawkins"); %! assert_equal (sig, [0.471176873257531 0.554010079012926; ... %! 0.554010079012926 0.774995718278673], 1e-12); %! assert_equal (mu, [-0.1675 0.2475], 1e-12); %! assert_equal (mah, ... %! [1.01449908743213; 0.813258155753472; 1.13104603226515; ... %! 1.22201620084884; 12.9622207511509; 4.28863012812016], ... %! 1e-11); %! assert_equal (find (ol)', [5 6]); %!warning ... %! [sig, mu, mah, ol] = robustcov (A(1:4,:), "Method", "olivehawkins"); %! assert_equal (sig, [0.34445 0.15355; 0.15355 0.06845], 1e-12); %! assert_equal (mu, [-0.075 0.415], 1e-12); %! assert_equal (mah, zeros (4, 1)); %! assert_equal (ol, false (4, 1)); %!warning ... %! [sig, mu, mah] = robustcov (B(1:6,:), "Method", "olivehawkins"); %! assert_equal (sig, [0.34013333 0.44226667 -0.20316667; ... %! 0.44226667 0.57843333 -0.21868333; ... %! -0.20316667 -0.21868333 0.73623333], 1e-7); %! assert_equal (mu, [-0.36666667 -0.46333333 -0.60666667], 1e-7); %! assert_equal (mah, zeros (6, 1)); %!test %! [sig, mu] = robustcov (A(1:4,:), "Method", "olivehawkins", ... %! "StartMethod", "classical"); %! assert_equal (sig, [0.21566688 0.14285887; 0.14285887 0.12814338], 1e-7); %! assert_equal (mu, [0.056666667 0.59666667], 1e-7); %!warning ... %! [sig, mu, mah, ol] = robustcov (ones (10,2), "Method", "olivehawkins"); %! assert_equal (sig, zeros (2)); %! assert_equal (mu, [1 1]); %! assert_equal (mah, zeros (10, 1)); %! assert_equal (ol, false (10, 1)); %!warning ... %! [sig, mu, mah, ol] = robustcov ([A; Inf 1], "Method", "olivehawkins"); %! assert_equal (sig, [0.561683516483517 0.264930769230769; ... %! 0.264930769230769 0.682857692307692], 1e-12); %! assert_equal (mu, [-0.507142857142857 0.425], 1e-12); %! assert_equal (mah(25), Inf); %! assert_equal (find (ol)', [5 7 12 16 19 20 21 22 23 25]); %!warning ... %! Xc = [A, A(:,1)]; %! [sig, mu, mah, ol] = robustcov (Xc, "Method", "olivehawkins"); %! assert_equal (sig, [0.19422652 0.049432576 0.19422652; ... %! 0.049432576 0.92357197 0.049432576; ... %! 0.19422652 0.049432576 0.19422652], 1e-7); %! assert_equal (mu, [-0.45416667 0.32916667 -0.45416667], 1e-7); %! assert_equal (mah, zeros (24, 1)); %! assert_equal (ol, false (24, 1)); %!warning ... %! [sig, mu] = robustcov ([A, 2 * A(:,1)], "Method", "olivehawkins"); %! assert_equal (sig, [0.19422652 0.049432576 0.38845303; ... %! 0.049432576 0.92357197 0.098865152; ... %! 0.38845303 0.098865152 0.77690606], 1e-7); %! assert_equal (mu, [-0.45416667 0.32916667 -0.90833333], 1e-7); %!warning ... %! [sig, mu] = robustcov ([A, A(:,1) + A(:,2)], "Method", "olivehawkins"); %! assert_equal (sig, [0.57159015 -0.25405606 0.31753409; ... %! -0.25405606 0.78003333 0.52597727; ... %! 0.31753409 0.52597727 0.84351136], 1e-7); %! assert_equal (mu, [-0.17083333 0.39833333 0.2275], 1e-7); %!warning ... %! Xc = [A, A(:,1)]; %! sig1 = robustcov (Xc, "Method", "olivehawkins", ... %! "StartMethod", {"classical", "medianball"}); %! sig2 = robustcov (Xc, "Method", "olivehawkins", ... %! "StartMethod", {"medianball", "classical"}); %! assert_equal (sig1, sig2); %! assert_equal (sig1(1,1), 0.19422652, 1e-7); %!warning ... %! Xc = [A, A(:,1)]; %! hX = @(x) deal (median (x), ones (size (x, 2))); %! [sig, mu] = robustcov (Xc, "Method", "olivehawkins", ... %! "StartMethod", {hX, "medianball"}); %! assert_equal (sig(1,1), 0.19422652, 1e-7); %! assert_equal (mu, [-0.45416667 0.32916667 -0.45416667], 1e-7); %!warning ... %! Xc = [A, A(:,1)]; %! hX = @(x) deal (median (x), ones (size (x, 2))); %! [sig, mu] = robustcov (Xc, "Method", "olivehawkins", ... %! "StartMethod", {"classical", hX}); %! assert_equal (sig(1,1), 2.0378775, 1e-7); %! assert_equal (mu, [0.29583333 0.56125 0.29583333], 1e-7); %!warning ... %! Xc = [A, A(:,1)]; %! hX = @(x) deal (median (x), ones (size (x, 2))); %! [sig, mu] = robustcov (Xc, "Method", "olivehawkins", ... %! "StartMethod", {hX, "classical"}); %! assert_equal (sig, ones (3)); %! assert_equal (mu, [-0.23 0.405 -0.23], 1e-12); %!test %! [sig, mu, ~, ol] = robustcov (single (A), "Method", "olivehawkins"); %! assert_equal (class (sig), "single"); %! assert_equal (class (mu), "single"); %! assert_equal (class (ol), "logical"); ## mah is double for single input and keeps the values of the double run %!test %! X = [0.1 0.2; -0.3 0.5; 0.4 -0.1; 0.2 0.3; -0.2 -0.4; 0.5 0.1; -0.1 0.2; ... %! 0.3 -0.3; -0.4 0.1; 0.2 -0.2; 0.1 0.4; -0.3 -0.2; 5 5; -6 4]; %! for m = {"fmcd", "ogk", "olivehawkins"} %! [~, ~, mah0, ol0] = robustcov (X, "Method", m{1}); %! [~, ~, mah, ol] = robustcov (single (X), "Method", m{1}); %! assert_equal (class (mah), "double"); %! assert_equal (mah, mah0, 1e-3); %! assert_equal (ol, ol0); %! endfor ## FMCD in 3-D returns a symmetric positive-definite estimate (p > 2 path) %!test %! X = [1.2 0.4 2.1; 0.8 1.1 1.9; 1.5 0.9 2.3; 0.9 0.7 1.8; 1.1 1.3 2.0; ... %! 1.4 0.6 2.2; 0.7 1.0 1.7; 1.3 0.8 2.4; 1.0 1.2 1.6; 0.7 0.9 2.0; ... %! 15 16 17; -9 12 -10]; %! [sig, mu, mah, ol] = robustcov (X); %! assert_equal (size (sig), [3 3]); %! assert_equal (sig, sig', 1e-12); %! assert_equal (all (eig (sig) > 0, 'all'), true); %! assert_equal (ol(11:12), logical ([1;1])); ## FMCD small-sample factors, as robustbase's covMcd computes them %!test %! ## BiasCorrection off drops the raw factor too: values from MATLAB R2024a %! X = [0.34 0.6; 0.32 0.96; -0.49 0.23; -0.84 -0.8; 2.77 -0.6; -0.21 -1.31; ... %! -1.57 -2.15; -0.76 1.76; -0.57 0.68; -0.88 -0.18; -1.39 -0.6; ... %! 1.75 0.58; -0.54 0.15; -0.52 -0.7; -1.7 0.16; 1.26 -0.11]; %! sig = robustcov (X, "BiasCorrection", false); %! assert_equal (sig, [0.803778168945674 0.56638131840886; ... %! 0.56638131840886 1.51698276152923], 1e-12); %!test %! ## Simulated factors where the formula fails; MATLAB R2024a returns %! ## negative variances here %! X = [0.34 0.6 0.7156; 0.32 0.96 1.0624; -0.49 0.23 0.4701; ... %! -0.84 -0.8 -0.0944; 2.77 -0.6 7.0729; -0.21 -1.31 -1.2659]; %! [sig, mu] = robustcov (X); %! assert_equal (sig, [0.642130715611748 0.786276534802183 ... %! 0.521007489038906; ... %! 0.786276534802183 2.23824866274835 2.04183516457436; ... %! 0.521007489038906 2.04183516457436 2.02201013312305], ... %! 1e-12); %! assert_equal (mu, [-0.176 -0.064 0.17756], 1e-12); %!test %! X = [0.34 0.6; 0.32 0.96; -0.49 0.23; -0.84 -0.8; 2.77 -0.6]; %! [sig, mu] = robustcov (X); %! assert_equal (sig, [0.831907348545357 0.978157210290628; ... %! 0.978157210290628 1.36832826422452], 1e-12); %! assert_equal (mu, [-0.1675 0.2475], 1e-12); %!test %! ## The whole sample as subset takes no small-sample factor, as in R2024a %! assert_equal (robustcov ([0.34; 0.32]), 0.0002, 1e-15); %!test %! ## The factor is interpolated over 1 - OUTLIERFRACTION %! i = (1:20)'; %! X = [norminv(0.001 + 0.998 * mod (i * 0.6180339887, 1)), ... %! norminv(0.001 + 0.998 * mod (i * 0.7548776662, 1))]; %! s1 = robustcov (X, "OutlierFraction", 0.25); %! s0 = robustcov (X, "OutlierFraction", 0.25, "BiasCorrection", false); %! assert_equal (s1 ./ s0, 1.08548960970478 * ones (2), 1e-12); ## Rows with NaN values are removed with a warning and padded in the outputs %!warning ... %! Xn = A; %! Xn(3,1) = NaN; %! keep = [1:2, 4:24]; %! for m = {"ogk", "olivehawkins"} %! [sig, mu, mah, ol, s] = robustcov (Xn, "Method", m{1}); %! [sig0, mu0, mah0, ol0] = robustcov (A(keep,:), "Method", m{1}); %! assert_equal (find (isnan (mah)), 3); %! assert_equal (size (mah), [24 1]); %! assert_equal (ol(3), false); %! assert_equal (sig, sig0); %! assert_equal (mu, mu0); %! assert_equal (mah(keep), mah0); %! assert_equal (ol(keep), ol0); %! assert_equal (size (s.Distances), [24 1]); %! assert_equal (size (s.Outliers), [24 1]); %! endfor %!warning ... %! Xn = A; %! Xn(3,1) = NaN; %! [~, ~, mah, ol, s] = robustcov (Xn); %! assert_equal (find (isnan (mah)), 3); %! assert_equal (size (mah), [24 1]); %! assert_equal (ol(3), false); %! assert_equal (size (s.Outliers), [24 1]); %!warning ... %! Xn = A(1:12,:); %! Xn(3,:) = NaN; %! Xn(7,2) = NaN; %! [sig, ~, mah, ol] = robustcov (Xn, "Method", "ogk"); %! assert_equal (find (isnan (mah))', [3 7]); %! assert_equal (sum (ol), 2); %! assert_equal (robustcov (Xn, "Method", "ogk"), sig); %!error ... %! robustcov ([NaN(9,1), A(1:9,2); A(10:12,:)]) %!warning ... %! Xn = A(1:12,:); %! Xn(1:8,1) = NaN; %! [~, ~, mah, ol] = robustcov (Xn, "Method", "ogk"); %! assert_equal (find (isnan (mah))', 1:8); %! assert_equal (mah(9:12), [1.41421; 1.41421; 1.41421; 20.939], 1e-4); %! assert_equal (find (ol)', 12); %!warning ... %! Xn = [A(1:12,:), A(1:12,1)]; %! Xn(3,1) = NaN; %! [~, ~, mah, ol] = robustcov (Xn, "Method", "olivehawkins"); %! assert_equal (mah, zeros (12, 1)); %! assert_equal (ol, false (12, 1)); %! Xn = A(1:12,:); %! Xn(1:8,1) = NaN; %! [~, ~, mah, ol] = robustcov (Xn, "Method", "olivehawkins"); %! assert_equal (mah, zeros (12, 1)); %! assert_equal (ol, false (12, 1)); ## Test input validation %!error robustcov () %!error robustcov ([]) %!error robustcov ("a") %!error ... %! robustcov (ones (5,2), "Method") %!error ... %! robustcov (ones (5,2), "foo", "bar") %!error ... %! robustcov (ones (5,2), "Method", "bogus") %!error ... %! robustcov (ones (5,2), "OutlierFraction", 0.8) %!error ... %! robustcov (ones (5,2), "Method", "ogk", "UnivariateEstimator", "mad") %!error ... %! robustcov (ones (2,5)) %!error ... %! robustcov (A, "Method", "ogk", "ReweightingMethod", "rmvn") %!error ... %! robustcov (A, "Method", "fmcd", "StartMethod", "medianball") %!error ... %! robustcov (A, "Method", "fmcd", "NumConcentrationSteps", 3) %!error ... %! robustcov (A, "Method", "ogk", "OutlierFraction", 0.25) %!error ... %! robustcov (A, "Method", "olivehawkins", "BiasCorrection", 0) %!error ... %! robustcov (A, "Method", "olivehawkins", "NumOGKIterations", 1) %!error ... %! robustcov (A, "Method", "olivehawkins", "ReweightingMethod", "bogus") %!error ... %! robustcov (A, "Method", "olivehawkins", "StartMethod", "bogus") %!error ... %! robustcov (A, "Method", "olivehawkins", "StartMethod", 5) %!error ... %! robustcov (A, "StartMethod", @(x) deal (mean (x)', cov (x))) %!error ... %! robustcov (A, "Method", "olivehawkins", "NumConcentrationSteps", 0) %!error ... %! robustcov (A, "Method", "olivehawkins", "NumTrials", 2.5) %!error ... %! robustcov (A, "StartMethod", {"classical", "medianball"}, "NumTrials", 3) %!error ... %! robustcov (A, "Method", "olivehawkins", "OutlierFraction", [0.1 0.2]) %!error ... %! robustcov (A, "StartMethod", {}) %!error ... %! robustcov (A, "StartMethod", {"classical", 5}) %!error ... %! robustcov (A, "Method", "ogk", "NumTrials", 3) %!error ... %! robustcov (A, "Method", "olivehawkins", "UnivariateEstimator", "qn") %!error ... %! robustcov (A, "Method", 5) statistics-release-2.1.2/inst/Clustering/000077500000000000000000000000001527311104300204155ustar00rootroot00000000000000statistics-release-2.1.2/inst/Clustering/CalinskiHarabaszEvaluation.m000066400000000000000000000273641527311104300260500ustar00rootroot00000000000000## Copyright (C) 2021 Stefano Guidoni ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef CalinskiHarabaszEvaluation < ClusterCriterion ## -*- texinfo -*- ## @deftp {statistics} CalinskiHarabaszEvaluation ## ## Calinski-Harabasz clustering evaluation. ## ## A @code{CalinskiHarabaszEvaluation} object contains the results of ## evaluating clustering solutions using the Calinski-Harabasz criterion. ## ## The Calinski-Harabasz index (also known as the Variance Ratio Criterion) is ## determined by the ratio of the between-cluster sum of squares (SSB) to the ## within-cluster sum of squares (SSW). A higher Calinski-Harabasz index ## value indicates a better clustering solution, implying that clusters are ## dense and well-separated. ## ## Create a @code{CalinskiHarabaszEvaluation} object by using the ## @code{evalclusters} function with the @qcode{'CalinskiHarabasz'} criterion. ## ## @seealso{evalclusters, ClusterCriterion, DaviesBouldinEvaluation, ## GapEvaluation, SilhouetteEvaluation} ## @end deftp properties(Access = protected) Centroids = {}; # a list of the centroids for every solution endproperties methods(Access = public) ## -*- texinfo -*- ## @deftypefn {CalinskiHarabaszEvaluation} {@var{obj} =} CalinskiHarabaszEvaluation (@var{x}, @var{clust}, @var{KList}) ## ## Construct a @code{CalinskiHarabaszEvaluation} object to evaluate ## clustering solutions with the Calinski-Harabasz criterion. ## ## @code{@var{obj} = CalinskiHarabaszEvaluation (@var{x}, @var{clust}, ## @var{KList})} clusters the data in @var{x} for every cluster count in ## @var{KList} and evaluates each solution. The evaluation runs at ## construction, so @var{obj} arrives with its @code{CriterionValues} and ## @code{OptimalK} already set. ## ## @itemize ## @item @var{x} is an @math{N*P} numeric matrix of observations (rows) ## and predictors (columns). A row holding a @qcode{NaN} is left out of ## @code{NumObservations}. ## @item @var{clust} names the clustering method, one of ## @qcode{'kmeans'}, @qcode{'linkage'} and @qcode{'gmdistribution'}; or a ## function handle that clusters the data; or an @math{N*M} numeric matrix ## of clustering solutions computed elsewhere, one column per cluster ## count, in which case @code{ClusteringFunction} is left empty. ## @item @var{KList} is a vector of positive integers, the cluster counts ## to inspect. ## @end itemize ## ## @code{evalclusters} is the usual way to create one of these objects. ## ## @end deftypefn function this = CalinskiHarabaszEvaluation (x, clust, KList) this@ClusterCriterion (x, clust, KList); this.CriterionName = 'CalinskiHarabasz'; this = this.evaluate (this.InspectedK); endfunction ## -*- texinfo -*- ## @deftypefn {CalinskiHarabaszEvaluation} {@var{obj} =} addK (@var{obj}, @var{K}) ## ## Add new cluster sizes for evaluation. ## ## @code{addK (@var{obj}, @var{K})} evaluates clustering solutions for the ## number of clusters specified in the vector @var{K} and adds them to the ## @code{CalinskiHarabaszEvaluation} object @var{obj}. ## ## @seealso{CalinskiHarabaszEvaluation, evalclusters} ## @end deftypefn function this = addK (this, K) this = addK@ClusterCriterion (this, K); ## if we have new data, we need a new evaluation if (this.OptimalK == 0) Centroids_tmp = {}; pS = 0; # position shift of the elements of Centroids for iter = 1 : length (this.InspectedK) ## reorganize Centroids according to the new list of cluster numbers if (any (this.InspectedK(iter) == K)) pS += 1; else Centroids_tmp{iter} = this.Centroids{iter - pS}; endif endfor this.Centroids = Centroids_tmp; this = this.evaluate (K); endif endfunction ## -*- texinfo -*- ## @deftypefn {CalinskiHarabaszEvaluation} {} plot (@var{obj}) ## @deftypefnx {CalinskiHarabaszEvaluation} {@var{h} =} plot (@var{obj}) ## ## Plot the clustering evaluation results. ## ## @code{plot (@var{obj})} plots the Calinski-Harabasz criterion values ## against the number of clusters. The optimal number of clusters is marked ## with an asterisk. ## ## @code{@var{h} = plot (@var{obj})} additionally returns the handle to the ## plot axes. ## ## @seealso{CalinskiHarabaszEvaluation, evalclusters} ## @end deftypefn function h = plot (this) yLabel = sprintf ("%s value", this.CriterionName); h = gca (); hold on; plot (this.InspectedK, this.CriterionValues, 'bo-'); plot (this.OptimalK, this.CriterionValues(this.OptimalIndex), 'b*'); xlabel ('number of clusters'); ylabel (yLabel); hold off; endfunction endmethods methods(Access = protected) ## evaluate ## do the evaluation function this = evaluate (this, K) ## use complete observations only UsableX = this.X(find (this.Missing == false), :); if (! isempty (this.ClusteringFunction)) ## build the clusters for iter = 1 : length (this.InspectedK) ## do it only for the specified K values if (any (this.InspectedK(iter) == K)) if (isa (this.ClusteringFunction, 'function_handle')) ## custom function ClusteringSolution = ... this.ClusteringFunction(UsableX, this.InspectedK(iter)); if (ismatrix (ClusteringSolution) && ... rows (ClusteringSolution) == this.NumObservations && ... columns (ClusteringSolution) == this.P) ## the custom function returned a matrix: ## we take the index of the maximum value for every row [~, this.ClusteringSolutions(:, iter)] = ... max (ClusteringSolution, [], 2); elseif (iscolumn (ClusteringSolution) && length (ClusteringSolution) == this.NumObservations) this.ClusteringSolutions(:, iter) = ClusteringSolution; elseif (isrow (ClusteringSolution) && length (ClusteringSolution) == this.NumObservations) this.ClusteringSolutions(:, iter) = ClusteringSolution'; else error (strcat ("CalinskiHarabaszEvaluation: invalid return", ... " value from custom clustering function.")); endif else switch (this.ClusteringFunction) case 'kmeans' [this.ClusteringSolutions(:, iter), this.Centroids{iter}] =... kmeans (UsableX, this.InspectedK(iter), ... 'Distance', 'sqeuclidean', 'EmptyAction', 'singleton', ... 'Replicates', 5); case 'linkage' ## use clusterdata this.ClusteringSolutions(:, iter) = clusterdata (UsableX, ... 'MaxClust', this.InspectedK(iter), ... 'Distance', 'euclidean', 'Linkage', 'ward'); this.Centroids{iter} = this.computeCentroids (UsableX, iter); case 'gmdistribution' gmm = fitgmdist (UsableX, this.InspectedK(iter), ... 'SharedCov', true, 'Replicates', 5); this.ClusteringSolutions(:, iter) = cluster (gmm, UsableX); this.Centroids{iter} = gmm.mu; otherwise error (strcat ("CalinskiHarabaszEvaluation:", ... " unexpected error, report this bug.")); endswitch endif endif endfor endif ## get the criterion values for every clustering solution for iter = 1 : length (this.InspectedK) ## do it only for the specified K values if (any (this.InspectedK(iter) == K)) ## not defined for one cluster if (this.InspectedK(iter) == 1) this.CriterionValues(iter) = NaN; continue; endif ## CaliÅ„ski-Harabasz index ## reference: calinhara function from the fpc package of R, ## by Christian Hennig ## https://CRAN.R-project.org/package=fpc W = zeros (columns (UsableX)); # between clusters covariance for i = 1 : this.InspectedK(iter) vIndicesI = find (this.ClusteringSolutions(:, iter) == i); ni = length (vIndicesI); # size of cluster i if (ni == 1) ## if the cluster has just one member the covariance is zero continue; endif ## weighted update of the covariance matrix W += cov (UsableX(vIndicesI, :)) * (ni - 1); endfor S = (this.NumObservations - 1) * cov (UsableX); # within clusters cov. B = S - W; # between clusters means ## tr(B) / tr(W) * (N-k) / (k-1) this.CriterionValues(iter) = (this.NumObservations - ... this.InspectedK(iter)) * trace (B) / ... ((this.InspectedK(iter) - 1) * trace (W)); endif endfor ## A criterion that came out undefined everywhere leaves no optimum to ## report, and the solutions are echoed back only when this object built ## them: given a matrix of clusterings the caller already has them. if (all (isnan (this.CriterionValues))) this.OptimalIndex = []; this.OptimalK = NaN; this.OptimalY = []; else [~, this.OptimalIndex] = max (this.CriterionValues); this.OptimalK = this.InspectedK(this.OptimalIndex(1)); if (isempty (this.ClusteringFunction)) this.OptimalY = []; else this.OptimalY = this.ClusteringSolutions(:, this.OptimalIndex(1)); endif endif endfunction endmethods methods(Access = private) ## computeCentroids ## compute the centroids if they are not available by other means function C = computeCentroids (this, X, index) C = zeros (this.InspectedK(index), columns (X)); for iter = 1 : this.InspectedK(index) vIndicesI = find (this.ClusteringSolutions(:, index) == iter); C(iter, :) = mean (X(vIndicesI, :)); endfor endfunction endmethods endclassdef %!test %! load fisheriris %! eva = evalclusters (meas, 'kmeans', 'calinskiharabasz', 'KList', [1:6]); %! assert_equal (class (eva), "CalinskiHarabaszEvaluation"); %!function C = count_calls_calinskiharabasz (X, k) %! global count_calls_calinskiharabasz_n; %! count_calls_calinskiharabasz_n += 1; %! C = mod ((0 : rows (X) - 1)', k) + 1; %!endfunction %!test %! ## custom function must be called exactly once per inspected K %! global count_calls_calinskiharabasz_n; %! count_calls_calinskiharabasz_n = 0; %! evalclusters (rand (20, 2), @count_calls_calinskiharabasz, ... %! 'CalinskiHarabasz', 'KList', [2, 3]); %! assert_equal (count_calls_calinskiharabasz_n, 2); %! clear -global count_calls_calinskiharabasz_n; statistics-release-2.1.2/inst/Clustering/ClusterCriterion.m000066400000000000000000000342411527311104300240770ustar00rootroot00000000000000## Copyright (C) 2021 Stefano Guidoni ## Copyright (C) 2024 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef (Abstract) ClusterCriterion ## -*- texinfo -*- ## @deftp {statistics} ClusterCriterion ## ## A clustering evaluation object. ## ## The @code{ClusterCriterion} is a superclass for clustering evaluation ## objects, which are created by the @code{evalclusters} function. It is not ## meant to be instantiated directly. ## ## @seealso{evalclusters, CalinskiHarabaszEvaluation, DaviesBouldinEvaluation, ## GapEvaluation, SilhouetteEvaluation} ## @end deftp properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClusterCriterion} {property} ClusteringFunction ## ## Clustering algorithm ## ## A character vector or a function handle specifying the clustering ## algorithm used to generate the clustering solutions. It can be empty if ## the clustering solutions are passed as an input matrix. This property is ## read-only. ## ## @end deftp ClusteringFunction = ''; ## -*- texinfo -*- ## @deftp {ClusterCriterion} {property} CriterionName ## ## Name of the evaluation criterion ## ## A character vector specifying the name of the criterion used to evaluate ## the clustering solutions. This property is read-only. ## ## @end deftp CriterionName = ''; ## -*- texinfo -*- ## @deftp {ClusterCriterion} {property} CriterionValues ## ## Criterion values ## ## A numeric vector containing the values generated by the evaluation ## criterion for each clustering solution. This property is read-only. ## ## @end deftp CriterionValues = []; ## -*- texinfo -*- ## @deftp {ClusterCriterion} {property} InspectedK ## ## List of the number of clusters ## ## A numeric vector containing the list of the number of clusters evaluated. ## This property is read-only. ## ## @end deftp InspectedK = []; ## -*- texinfo -*- ## @deftp {ClusterCriterion} {property} Missing ## ## Missing values ## ## A logical vector indicating which observations in the data matrix contain ## missing values (@code{NaN}). This property is read-only. ## ## @end deftp Missing = []; ## -*- texinfo -*- ## @deftp {ClusterCriterion} {property} NumObservations ## ## Number of observations ## ## An integer specifying the number of non-missing observations in the data ## matrix. This property is read-only. ## ## @end deftp NumObservations = 0; ## -*- texinfo -*- ## @deftp {ClusterCriterion} {property} OptimalK ## ## Optimal number of clusters ## ## An integer specifying the optimal number of clusters based on the ## evaluation criterion. This property is read-only. ## ## @end deftp OptimalK = 0; ## -*- texinfo -*- ## @deftp {ClusterCriterion} {property} OptimalY ## ## Optimal clustering solution ## ## A numeric vector representing the clustering solution that corresponds to ## the optimal number of clusters. This property is read-only. ## ## @end deftp OptimalY = []; ## -*- texinfo -*- ## @deftp {ClusterCriterion} {property} X ## ## Data used for clustering ## ## A numeric matrix containing the data used for clustering. This property ## is read-only. ## ## @end deftp X = []; endproperties properties(Access = protected) N = 0; # number of observations P = 0; # number of variables ClusteringSolutions = []; # OptimalIndex = 0; # index of the optimal K endproperties ## Every subclass must define an 'evaluate' method: this class provides ## the data, the K list and the shared bookkeeping, but never computes a ## criterion itself. MATLAB declares that method abstract; Octave cannot, ## because a bodyless signature in a 'methods (Abstract)' block is read as ## an external method and rejected outside an @-folder, so a subclass that ## omits 'evaluate' is caught when it is called rather than when it is ## defined. All four subclasses shipped here define it. methods(Access = public) ## -*- texinfo -*- ## @deftypefn {ClusterCriterion} {@var{obj} =} ClusterCriterion (@var{x}, @var{clust}, @var{KList}) ## ## Create a @qcode{ClusterCriterion} object. ## ## @code{ClusterCriterion} is a superclass and is not meant to be ## instantiated directly. Use @code{evalclusters} instead. ## ## @seealso{evalclusters} ## @end deftypefn function this = ClusterCriterion (x, clust, KList) ## parsing input data if (! ismatrix (x) || ! isnumeric (x)) error ("ClusterCriterion: X must be a numeric matrix."); endif this.X = x; this.N = rows (this.X); this.P = columns (this.X); ## look for missing values, one flag per observation and shaped like ## the observations themselves this.Missing = any (isnan (x), 2); ## number of usable observations this.NumObservations = sum (this.Missing == false); ## parsing the clustering algorithm if (ischar (clust)) if (any (strcmpi (clust, {'kmeans', 'linkage', 'gmdistribution'}))) this.ClusteringFunction = lower (clust); else error ("ClusterCriterion: unknown clustering algorithm '%s'.", clust); endif elseif (isa (clust, 'function_handle')) this.ClusteringFunction = clust; elseif (ismatrix (clust)) if (isnumeric (clust) && (length (size (clust)) == 2) && ... (rows (clust) == this.N)) ## Nothing clustered the data, the caller did, so there is no ## clustering function to report this.ClusteringFunction = []; this.ClusteringSolutions = clust(find (this.Missing == false), :); else error ("ClusterCriterion: invalid matrix of clustering solutions."); endif else error ("ClusterCriterion: invalid argument."); endif ## parsing the list of cluster sizes to inspect this.InspectedK = parseKList (this, KList); endfunction ## -*- texinfo -*- ## @deftypefn {ClusterCriterion} {@var{obj} =} addK (@var{obj}, @var{k}) ## ## Add a new list of cluster numbers to evaluate. ## ## @code{addK} adds a new list of cluster numbers, @var{k}, to the ## @qcode{ClusterCriterion} object. ## ## @end deftypefn function this = addK (this, k) ## A compacted object has no observations left to cluster if (isempty (this.X)) error (strcat ("ClusterCriterion.addK: this method can only be", ... " called when the X property is not empty.")); endif ## if there is not a clustering function, then we are using a predefined ## set of clustering solutions, hence we cannot redefine the number of ## solutions if (isempty (this.ClusteringFunction)) warning (strcat ("ClusterCriterion.addK: cannot redefine the list", ... " of cluster numbers to evaluate when there is", ... " not a clustering function")); return; endif ## otherwise go on newList = this.parseKList ([this.InspectedK k]); ## check if the list has changed if (length (newList) == length (this.InspectedK)) warning ("ClusterCriterion.addK: the list has not changed"); else ## update ClusteringSolutions and CriterionValues ClusteringSolutions_tmp = zeros (this.NumObservations, ... length (newList)); CriterionValues_tmp = zeros (length (newList), 1); for iter = 1 : length (this.InspectedK) idx = find (newList == this.InspectedK(iter)); if (! isempty (idx)) ClusteringSolutions_tmp(:, idx) = this.ClusteringSolutions(:, iter); CriterionValues_tmp(idx) = this.CriterionValues(iter); endif endfor this.ClusteringSolutions = ClusteringSolutions_tmp; this.CriterionValues = CriterionValues_tmp; ## reset the old results this.OptimalK = 0; this.OptimalY = []; this.OptimalIndex = 0; ## update the list of cluster numbers to evaluate this.InspectedK = newList; endif endfunction ## -*- texinfo -*- ## @deftypefn {ClusterCriterion} {@var{h} =} plot (@var{obj}) ## ## Plot the clustering evaluation values. ## ## @code{plot} generates a plot of the criterion values against the number ## of clusters. ## ## The optimal number of clusters is marked with an asterisk. ## ## The optional return value, @var{h}, is a graphics handle to the plot. ## ## @end deftypefn function h = plot (this) yLabel = sprintf ("%s value", this.CriterionName); h = gca (); hold on; plot (this.InspectedK, this.CriterionValues, 'bo-'); plot (this.OptimalK, this.CriterionValues(this.OptimalIndex), 'b*'); xlabel ('number of clusters'); ylabel (yLabel); hold off; endfunction ## -*- texinfo -*- ## @deftypefn {ClusterCriterion} {@var{obj} =} compact (@var{obj}) ## ## Create a compact clustering evaluation object. ## ## @code{@var{obj} = compact (@var{obj})} returns an object of the same ## class holding the results of the evaluation but none of the data it was ## computed from, which is useful when the evaluation is kept and the ## sample is large. ## ## @var{X}, @var{Missing} and @var{OptimalY} are emptied; @var{InspectedK}, ## @var{CriterionValues}, @var{OptimalK}, @var{NumObservations}, ## @var{CriterionName} and @var{ClusteringFunction} are kept. The object ## compacted from is not changed. ## ## A compacted object can still be displayed and plotted, and compacting ## one again does nothing, but @code{addK} raises: there are no ## observations left to cluster. ## ## @end deftypefn function this = compact (this) ## Drop everything held per observation and keep every result, as ## MATLAB does. The object is a value, so the caller's own copy is ## untouched: only the returned one is compacted. this.X = []; this.Missing = []; this.OptimalY = []; this.ClusteringSolutions = []; endfunction endmethods methods(Access = private) ## check if a list of cluster sizes is correct function retList = parseKList (this, KList) if (isnumeric (KList) && isvector (KList) && all (find (KList > 0)) && ... all (floor (KList) == KList)) retList = unique (KList); else error (strcat ("ClusterCriterion: the list of cluster sizes", ... " must be an array of positive integer numbers.")); endif endfunction endmethods endclassdef ## Test input validation %!test %! ## compact drops everything held per observation and keeps every %! ## result, returning an object of the same class. Verified against %! ## R2024a. %! X = [1, 1; 1.2, 1.1; 3, 3; 3.2, 3.1; 6, 6; 6.1, 6.2; 1.1, 1.3; 3.1, 3.2]; %! e = evalclusters (X, 'kmeans', 'CalinskiHarabasz', 'KList', 2:3); %! c = compact (e); %! assert_equal (class (c), class (e)); %! assert_equal (isempty (c.X), true); %! assert_equal (isempty (c.Missing), true); %! assert_equal (isempty (c.OptimalY), true); %! ## the results survive %! assert_equal (c.NumObservations, e.NumObservations); %! assert_equal (c.InspectedK, e.InspectedK); %! assert_equal (c.CriterionValues, e.CriterionValues); %! assert_equal (c.OptimalK, e.OptimalK); %!test %! ## the object compacted from is left alone: these are value classes, %! ## as MATLAB's are, so compact hands back a compacted copy %! X = [1, 1; 1.2, 1.1; 3, 3; 3.2, 3.1; 6, 6; 6.1, 6.2; 1.1, 1.3; 3.1, 3.2]; %! e = evalclusters (X, 'kmeans', 'CalinskiHarabasz', 'KList', 2:3); %! c = compact (e); %! assert_equal (size (e.X), [8, 2]); %! ## and compacting twice is harmless %! assert_equal (isempty (getfield (compact (c), 'X')), true); %!test %! ## a compacted object still plots, since plotting needs no observations %! X = [1, 1; 1.2, 1.1; 3, 3; 3.2, 3.1; 6, 6; 6.1, 6.2; 1.1, 1.3; 3.1, 3.2]; %! c = compact (evalclusters (X, 'kmeans', 'CalinskiHarabasz', 'KList', 2:3)); %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plot (c); %! assert_equal (all (isaxes (h), 'all'), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!error ... %! addK (compact (evalclusters ([1, 1; 1.2, 1.1; 3, 3; 3.2, 3.1; 6, 6; ... %! 6.1, 6.2; 1.1, 1.3; 3.1, 3.2], 'kmeans', 'CalinskiHarabasz', ... %! 'KList', 2:3)), 4) ## This class is abstract, so its input validation is reached through a ## subclass, which is the only way a user can reach it either. %!error ... %! CalinskiHarabaszEvaluation ('1', 'kmeans', [1:6]) %!error ... %! CalinskiHarabaszEvaluation ([1, 2, 1, 3, 2, 4, 3], 'k', [1:6]) %!error ... %! CalinskiHarabaszEvaluation ([1, 2, 1; 3, 2, 4], 1, [1:6]) %!error ... %! CalinskiHarabaszEvaluation ([1, 2, 1; 3, 2, 4], ones (2, 2, 2), [1:6]) %!error ... %! ClusterCriterion ([1, 2, 1; 3, 2, 4], 'kmeans', [1:6]) statistics-release-2.1.2/inst/Clustering/DaviesBouldinEvaluation.m000066400000000000000000000315511527311104300253600ustar00rootroot00000000000000## Copyright (C) 2021 Stefano Guidoni ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef DaviesBouldinEvaluation < ClusterCriterion ## -*- texinfo -*- ## @deftp {statistics} DaviesBouldinEvaluation ## ## Davies-Bouldin object to evaluate clustering solutions ## ## A @code{DaviesBouldinEvaluation} object is a @code{ClusterCriterion} ## object used to evaluate clustering solutions using the Davies-Bouldin ## criterion. ## ## The Davies-Bouldin criterion is based on the ratio between the distances ## between clusters and within clusters, that is between centroids and ## between each datapoint and its centroid. ## ## The best solution according to the Davies-Bouldin criterion is the one ## that produces the lowest Davies-Bouldin value. ## ## @seealso{evalclusters, ClusterCriterion, CalinskiHarabaszEvaluation, ## GapEvaluation, SilhouetteEvaluation} ## @end deftp properties(Access = protected, Hidden) Centroids = {}; # a list of the centroids for every solution endproperties methods(Access = public) ## -*- texinfo -*- ## @deftypefn {DaviesBouldinEvaluation} {@var{obj} =} DaviesBouldinEvaluation (@var{x}, @var{clust}, @var{KList}) ## ## Construct a @code{DaviesBouldinEvaluation} object to evaluate clustering ## solutions with the Davies-Bouldin criterion. ## ## @code{@var{obj} = DaviesBouldinEvaluation (@var{x}, @var{clust}, ## @var{KList})} clusters the data in @var{x} for every cluster count in ## @var{KList} and evaluates each solution. The evaluation runs at ## construction, so @var{obj} arrives with its @code{CriterionValues} and ## @code{OptimalK} already set. ## ## @itemize ## @item @var{x} is an @math{N*P} numeric matrix of observations (rows) ## and predictors (columns). A row holding a @qcode{NaN} is left out of ## @code{NumObservations}. ## @item @var{clust} names the clustering method, one of ## @qcode{'kmeans'}, @qcode{'linkage'} and @qcode{'gmdistribution'}; or a ## function handle that clusters the data; or an @math{N*M} numeric matrix ## of clustering solutions computed elsewhere, one column per cluster ## count, in which case @code{ClusteringFunction} is left empty. ## @item @var{KList} is a vector of positive integers, the cluster counts ## to inspect. ## @end itemize ## ## @code{evalclusters} is the usual way to create one of these objects. ## ## @end deftypefn function this = DaviesBouldinEvaluation (x, clust, KList) this@ClusterCriterion (x, clust, KList); this.CriterionName = 'DaviesBouldin'; this = this.evaluate (this.InspectedK); endfunction ## -*- texinfo -*- ## @deftypefn {DaviesBouldinEvaluation} {@var{obj} =} addK (@var{obj}, @var{K}) ## ## Add new cluster numbers to inspect in the DaviesBouldinEvaluation object. ## ## @end deftypefn function this = addK (this, K) this = addK@ClusterCriterion (this, K); ## if we have new data, we need a new evaluation if (this.OptimalK == 0) Centroids_tmp = {}; pS = 0; # position shift of the elements of Centroids for iter = 1 : length (this.InspectedK) ## reorganize Centroids according to the new list of cluster numbers if (any (this.InspectedK(iter) == K)) pS += 1; else Centroids_tmp{iter} = this.Centroids{iter - pS}; endif endfor this.Centroids = Centroids_tmp; this = this.evaluate (K); endif endfunction ## -*- texinfo -*- ## @deftypefn {DaviesBouldinEvaluation} {} plot (@var{obj}) ## @deftypefnx {DaviesBouldinEvaluation} {@var{h} =} plot (@var{obj}) ## ## Plot Davies-Bouldin evaluation results. ## ## Plot the CriterionValues against InspectedK from the ## DaviesBouldinEvaluation ClusterCriterion to the current plot. ## Returns an axes handle if requested. ## ## @end deftypefn function h = plot (this) yLabel = sprintf ("%s value", this.CriterionName); h = gca (); hold on; plot (this.InspectedK, this.CriterionValues, 'bo-'); plot (this.OptimalK, this.CriterionValues(this.OptimalIndex), 'b*'); xlabel ('number of clusters'); ylabel (yLabel); hold off; endfunction endmethods methods(Access = protected) ## evaluate ## do the evaluation function this = evaluate (this, K) ## use complete observations only UsableX = this.X(find (this.Missing == false), :); if (! isempty (this.ClusteringFunction)) ## build the clusters for iter = 1 : length (this.InspectedK) ## do it only for the specified K values if (any (this.InspectedK(iter) == K)) if (isa (this.ClusteringFunction, 'function_handle')) ## custom function ClusteringSolution = ... this.ClusteringFunction(UsableX, this.InspectedK(iter)); if (ismatrix (ClusteringSolution) && ... rows (ClusteringSolution) == this.NumObservations && ... columns (ClusteringSolution) == this.P) ## the custom function returned a matrix: ## we take the index of the maximum value for every row [~, this.ClusteringSolutions(:, iter)] = ... max (ClusteringSolution, [], 2); elseif (iscolumn (ClusteringSolution) && length (ClusteringSolution) == this.NumObservations) this.ClusteringSolutions(:, iter) = ClusteringSolution; elseif (isrow (ClusteringSolution) && length (ClusteringSolution) == this.NumObservations) this.ClusteringSolutions(:, iter) = ClusteringSolution'; else error (strcat ("DaviesBouldinEvaluation: invalid return", ... " value from custom clustering function.")); endif this.ClusteringSolutions(:, iter) = ... this.ClusteringFunction(UsableX, this.InspectedK(iter)); else switch (this.ClusteringFunction) case 'kmeans' [this.ClusteringSolutions(:, iter), this.Centroids{iter}] =... kmeans (UsableX, this.InspectedK(iter), ... 'Distance', 'sqeuclidean', 'EmptyAction', 'singleton', ... 'Replicates', 5); case 'linkage' ## use clusterdata this.ClusteringSolutions(:, iter) = clusterdata (UsableX, ... 'MaxClust', this.InspectedK(iter), ... 'Distance', 'euclidean', 'Linkage', 'ward'); this.Centroids{iter} = this.computeCentroids (UsableX, iter); case 'gmdistribution' gmm = fitgmdist (UsableX, this.InspectedK(iter), ... 'SharedCov', true, 'Replicates', 5); this.ClusteringSolutions(:, iter) = cluster (gmm, UsableX); this.Centroids{iter} = gmm.mu; otherwise error (strcat ("DaviesBouldinEvaluation: unexpected", ... " error, report this bug.")); endswitch endif endif endfor endif ## get the criterion values for every clustering solution for iter = 1 : length (this.InspectedK) ## do it only for the specified K values if (any (this.InspectedK(iter) == K)) ## not defined for one cluster if (this.InspectedK(iter) == 1) this.CriterionValues(iter) = NaN; continue; endif ## Davies-Bouldin value ## an evaluation of the ratio between within-cluster and ## between-cluster distances ## Compute centroids when clustering labels are provided as input. if (numel (this.Centroids) < iter || isempty (this.Centroids{iter})) this.Centroids{iter} = this.computeCentroids (UsableX, iter); endif ## mean distances between cluster members and their centroid vD = zeros (this.InspectedK(iter), 1); for i = 1 : this.InspectedK(iter) vIndicesI = find (this.ClusteringSolutions(:, iter) == i); vD(i) = mean (vecnorm (UsableX(vIndicesI, :) - ... this.Centroids{iter}(i, :), 2, 2)); endfor ## within-to-between cluster distance ratio Dij = zeros (this.InspectedK(iter)); for i = 1 : (this.InspectedK(iter) - 1) for j = (i + 1) : this.InspectedK(iter) ## centroid to centroid distance dij = vecnorm (this.Centroids{iter}(i, :) - ... this.Centroids{iter}(j, :)); ## within-to-between cluster distance ratio for clusters i and j Dij(i, j) = (vD(i) + vD(j)) / dij; endfor endfor ## ( max_j D1j + max_j D2j + ... + max_j Dkj) / k Dij = Dij + Dij'; this.CriterionValues(iter) = sum (max (Dij, [], 2)) / ... this.InspectedK(iter); endif endfor ## A criterion that came out undefined everywhere leaves no optimum to ## report, and the solutions are echoed back only when this object built ## them: given a matrix of clusterings the caller already has them. if (all (isnan (this.CriterionValues))) this.OptimalIndex = []; this.OptimalK = NaN; this.OptimalY = []; else [~, this.OptimalIndex] = min (this.CriterionValues); this.OptimalK = this.InspectedK(this.OptimalIndex(1)); if (isempty (this.ClusteringFunction)) this.OptimalY = []; else this.OptimalY = this.ClusteringSolutions(:, this.OptimalIndex(1)); endif endif endfunction endmethods methods(Access = private) ## computeCentroids ## compute the centroids if they are not available by other means function C = computeCentroids (this, X, index) C = zeros (this.InspectedK(index), columns (X)); for iter = 1 : this.InspectedK(index) vIndicesI = find (this.ClusteringSolutions(:, index) == iter); C(iter, :) = mean (X(vIndicesI, :)); endfor endfunction endmethods endclassdef %!test %! load fisheriris %! eva = evalclusters (meas, 'kmeans', 'DaviesBouldin', 'KList', [1:6]); %! assert_equal (class (eva), "DaviesBouldinEvaluation"); %!test %! ## Verify DB index for a known 2-cluster case %! X = [ones(5,1); 5 * ones(5,1)]; %! clust = [ones(5,1); 2 * ones(5,1)]; %! eva = evalclusters (X, clust, 'DaviesBouldin', 'KList', 2); %! assert_equal (eva.CriterionValues, 0, 1); %!test %! ## Deterministic 1-D example; expected value is 7/30 (matches MATLAB) %! rand ('seed', 1); %! randn ('seed', 1); %! X = [0; 1; 4; 5; 9; 10]; %! eva = evalclusters (X, 'kmeans', 'DaviesBouldin', 'KList', 3); %! assert_equal (eva.CriterionValues, 7 / 30, 1e-12); %!test %! ## Verify aggregation uses all cluster rows in Dij %! rand ('seed', 1); %! randn ('seed', 1); %! X = [0; 1; 4; 5; 9; 10]; %! eva = evalclusters (X, 'kmeans', 'DaviesBouldin', 'KList', 3); %! idx = eva.OptimalY; %! k = 3; %! vD = zeros (k, 1); %! C = zeros (k, 1); %! for i = 1:k %! Xi = X(idx == i); %! C(i) = mean (Xi); %! vD(i) = mean (abs (Xi - C(i))); %! endfor %! Dij = zeros (k); %! for i = 1:(k - 1) %! for j = (i + 1):k %! Dij(i, j) = (vD(i) + vD(j)) / abs (C(i) - C(j)); %! endfor %! endfor %! Dij = Dij + Dij'; %! expected = sum (max (Dij, [], 2)) / k; %! assert_equal (eva.CriterionValues, expected, 1e-12); %!test %! ## MATLAB reference case: well-separated tight clusters %! rand ('seed', 1); %! randn ('seed', 1); %! X = [0; 0.1; 0.2; 5; 5.1; 5.2]; %! X = horzcat (X, zeros (rows (X), 1)); %! eva = evalclusters (X, 'kmeans', 'DaviesBouldin', 'KList', 2); %! assert_equal (eva.CriterionValues, 0.0267, 1e-4); %!test %! ## MATLAB reference case: uneven spread clusters %! rand ('seed', 1); %! randn ('seed', 1); %! X = [0; 0.1; 0.2; 10; 20; 30]; %! X = horzcat (X, zeros (rows (X), 1)); %! eva = evalclusters (X, 'kmeans', 'DaviesBouldin', 'KList', 2); %! assert_equal (eva.CriterionValues, 0.3885, 1e-4);statistics-release-2.1.2/inst/Clustering/GapEvaluation.m000066400000000000000000000472411527311104300233420ustar00rootroot00000000000000## Copyright (C) 2021 Stefano Guidoni ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef GapEvaluation < ClusterCriterion ## -*- texinfo -*- ## @deftp {statistics} GapEvaluation ## ## Gap evaluation for clustering solutions ## ## The @code{GapEvaluation} class implements the gap statistic criterion for ## evaluating clustering solutions. A @code{GapEvaluation} object is a ## specialization of @code{ClusterCriterion} and contains fields and methods ## to compute the gap statistic, its Monte-Carlo reference expectations, and ## to select the optimal number of clusters according to a chosen search ## method. ## ## Create a @code{GapEvaluation} object by using the @code{evalclusters} ## function or by calling the class constructor directly. ## ## @seealso{evalclusters, ClusterCriterion, CalinskiHarabaszEvaluation, ## DaviesBouldinEvaluation, SilhouetteEvaluation} ## @end deftp properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {GapEvaluation} {property} B ## ## Number of reference datasets ## ## A positive integer specifying how many reference datasets are generated ## to compute the expected log within-cluster dispersion via Monte-Carlo ## simulation. This property is read-only. ## ## @end deftp B = 0; ## -*- texinfo -*- ## @deftp {GapEvaluation} {property} Distance ## ## Distance metric ## ## A character vector or function handle specifying the distance measure ## passed to clustering routines (as accepted by @code{pdist}). When a ## numeric vector is supplied it is interpreted as a precomputed distance ## vector. This property is read-only. ## ## @end deftp Distance = ''; ## -*- texinfo -*- ## @deftp {GapEvaluation} {property} ReferenceDistribution ## ## Reference distribution for Monte-Carlo ## ## A character vector naming the reference distribution used to generate ## reference datasets. Supported values include @qcode{'pca'} and ## @qcode{'uniform'}. This property is read-only. ## ## @end deftp ReferenceDistribution = ''; ## -*- texinfo -*- ## @deftp {GapEvaluation} {property} SearchMethod ## ## Search method to select optimal K ## ## A character vector specifying the method used to select the optimal ## number of clusters from the gap statistic. Supported values include ## @qcode{'globalMaxSE'} and @qcode{'firstMaxSE'}. This property is ## read-only. ## ## @end deftp SearchMethod = ''; ## -*- texinfo -*- ## @deftp {GapEvaluation} {property} ExpectedLogW ## ## Expected log within-cluster dispersion ## ## A numeric vector containing the Monte-Carlo estimate of the expected ## values for the natural logarithm of the within-cluster dispersion, ## computed across the generated reference datasets. This property is ## read-only. ## ## @end deftp ExpectedLogW = []; ## -*- texinfo -*- ## @deftp {GapEvaluation} {property} LogW ## ## Observed log within-cluster dispersion ## ## A numeric vector containing the observed values of the natural ## logarithm of the within-cluster dispersion computed on the actual data. ## This property is read-only. ## ## @end deftp LogW = []; ## -*- texinfo -*- ## @deftp {GapEvaluation} {property} SE ## ## Standard error of expected logW ## ## A numeric vector containing the standard error of the expected values ## for the natural logarithm of the within-cluster dispersion. This ## property is read-only. ## ## @end deftp SE = []; ## -*- texinfo -*- ## @deftp {GapEvaluation} {property} StdLogW ## ## Standard deviation of expected logW ## ## A numeric vector containing the standard deviation of the Monte-Carlo ## estimates of the log within-cluster dispersion. This property is ## read-only. ## ## @end deftp StdLogW = []; endproperties properties(Access = protected) ## -*- texinfo -*- ## @deftp {GapEvaluation} {property} DistanceVector ## ## Precomputed distance vector ## ## If a numeric vector is supplied as the distance metric it is stored ## here and used instead of computing distances via @code{pdist}. This ## property is read-only. ## ## @end deftp DistanceVector = []; ## -*- texinfo -*- ## @deftp {GapEvaluation} {property} mExpectedLogW ## ## Monte-Carlo results matrix ## ## Internal matrix storing the log within-cluster dispersion values ## computed for each Monte-Carlo run (rows) and each inspected K (columns). ## This property is read-only. ## ## @end deftp mExpectedLogW = []; endproperties methods(Access = public) ## -*- texinfo -*- ## @deftypefn {GapEvaluation} {@var{obj} =} GapEvaluation (@var{x}, @var{clust}, @var{KList}) ## @deftypefnx {GapEvaluation} {@var{obj} =} GapEvaluation (@var{x}, @var{clust}, @var{KList}, @var{B}) ## @deftypefnx {GapEvaluation} {@var{obj} =} GapEvaluation (@dots{}, @var{name}, @var{value}) ## ## Construct a GapEvaluation object to evaluate clustering solutions using ## the gap statistic. ## ## @code{@var{obj} = GapEvaluation (@var{x}, @var{clust}, @var{KList})} ## returns a @code{GapEvaluation} object configured to evaluate the ## clustering function specified by @var{clust} on the data matrix ## @var{x} for the list of cluster counts in @var{KList}. ## ## Optional inputs: ## ## @itemize ## @item @qcode{B} - Number of reference datasets to generate (default 100). ## @item @qcode{'Distance'} - Distance metric name or function handle as ## accepted by @code{pdist} (default @qcode{'sqeuclidean'}). ## @item @qcode{'ReferenceDistribution'} - Reference distribution to use ## (default @qcode{'pca'}; @qcode{'uniform'} is supported). ## @item @qcode{'SearchMethod'} - Method to select the optimal K; one of ## @qcode{'globalMaxSE'} or @qcode{'firstMaxSE'} (default ## @qcode{'globalMaxSE'}). ## @end itemize ## ## @seealso{evalclusters, ClusterCriterion} ## @end deftypefn function this = GapEvaluation (x, clust, KList, b = 100, ... distanceMetric = 'sqeuclidean', ... referenceDistribution = 'pca', searchMethod = 'globalmaxse') this@ClusterCriterion (x, clust, KList); ## parsing the distance criterion if (ischar (distanceMetric)) if (any (strcmpi (distanceMetric, {'sqeuclidean', 'euclidean', ... 'cityblock', 'cosine', 'correlation', 'hamming', 'jaccard'}))) ## Report the metric under its canonical spelling, as MATLAB does. ## The name has already been matched exactly above, so validatestring ## only canonicalises it and never widens what is accepted. this.Distance = validatestring (distanceMetric, ... {'sqEuclidean', 'Euclidean', 'cityblock', ... 'cosine', 'correlation', 'Hamming', 'Jaccard'}); ## kmeans can use only a subset if (strcmpi (clust, 'kmeans') && any (strcmpi (this.Distance, ... {'euclidean', 'jaccard'}))) error (strcat ("GapEvaluation: invalid distance criterion", ... " '%s' for 'kmeans'"), distanceMetric); endif else error ("GapEvaluation: unknown distance criterion '%s'", ... distanceMetric); endif elseif (isa (distanceMetric, 'function_handle')) this.Distance = distanceMetric; ## kmeans cannot use a function handle if (strcmpi (clust, 'kmeans')) error ("GapEvaluation: invalid distance criterion for 'kmeans'."); endif elseif (isvector (distanceMetric) && isnumeric (distanceMetric)) this.Distance = ''; this.DistanceVector = distanceMetric; # the validity check is delegated ## kmeans cannot use a distance vector if (strcmpi (clust, 'kmeans')) error ("GapEvaluation: invalid distance criterion for 'kmeans'."); endif else error ("GapEvaluation: invalid distance metric."); endif ## B: number of Monte-Carlo iterations if (! isnumeric (b) || ! isscalar (b) || b != floor (b) || b < 1) error ("GapEvaluation: b must a be positive integer number"); endif this.B = b; ## reference distribution if (! ischar (referenceDistribution) || ! any (strcmpi ... (referenceDistribution, {'pca', 'uniform'}))) error (strcat ("GapEvaluation: the reference distribution", ... " must be either 'PCA' or 'uniform'.")); elseif (strcmpi (referenceDistribution, 'pca')) warning (strcat ("GapEvaluation: 'PCA' distribution not", ... " implemented, defaulting to 'uniform'.")); endif this.ReferenceDistribution = validatestring (referenceDistribution, ... {'PCA', 'uniform'}); if (! ischar (searchMethod) || ! any (strcmpi (searchMethod, ... {'globalmaxse', 'firstmaxse'}))) error (strcat ("evalclusters: the search method must be", ... " either 'globalMaxSE' or 'firstMaxSE'.")); endif this.SearchMethod = validatestring (searchMethod, ... {'GlobalMaxSE', 'FirstMaxSE'}); ## a matrix to store the results from the Monte-Carlo runs this.mExpectedLogW = zeros (this.B, length (this.InspectedK)); this.CriterionName = 'Gap'; this = this.evaluate (this.InspectedK); endfunction ## -*- texinfo -*- ## @deftypefn {GapEvaluation} {@var{obj} =} addK (@var{obj}, @var{K}) ## ## Add new K values to inspect ## ## Add a new cluster array to inspect in the @code{GapEvaluation} object. ## This updates internal storage for Monte-Carlo results and evaluates the ## newly requested cluster counts. ## ## @end deftypefn function this = addK (this, K) this = addK@ClusterCriterion (this, K); ## if we have new data, we need a new evaluation if (this.OptimalK == 0) mExpectedLogW_tmp = zeros (this.B, length (this.InspectedK)); pS = 0; # position shift for iter = 1 : length (this.InspectedK) ## reorganize all the arrays according to the new list ## of cluster numbers if (any (this.InspectedK(iter) == K)) pS += 1; else mExpectedLogW_tmp(:, iter) = this.mExpectedLogW(:, iter - pS); endif endfor this.mExpectedLogW = mExpectedLogW_tmp; this = this.evaluate (K); endif endfunction ## -*- texinfo -*- ## @deftypefn {GapEvaluation} {} plot (@var{obj}) ## @deftypefnx {GapEvaluation} {@var{h} =} plot (@var{obj}) ## ## Plot Gap evaluation results ## ## Plot the gap statistic (criterion values) versus the inspected numbers ## of clusters and display error bars representing the Monte-Carlo ## standard deviations. Optionally returns the axes handle. ## ## @end deftypefn function h = plot (this) yLabel = sprintf ("%s value", this.CriterionName); h = gca (); hold on; errorbar (this.InspectedK, this.CriterionValues, this.StdLogW); plot (this.InspectedK, this.CriterionValues, 'bo'); plot (this.OptimalK, this.CriterionValues(this.OptimalIndex), 'b*'); xlabel ('number of clusters'); ylabel (yLabel); hold off; endfunction endmethods methods(Access = protected) ## evaluate ## do the evaluation function this = evaluate (this, K) ## use complete observations only ActualX = this.X(find (this.Missing == false), :); colMins = min (ActualX); colRange = max (ActualX) - colMins; ## Monte-Carlo runs for mcrun = 1 : (this.B + 1) ## the last run use tha actual data, ## the others are Monte-Carlo runs with reconstructed data if (mcrun <= this.B) ## uniform distribution UsableX = colMins + rand (this.NumObservations, columns (ActualX)) ... .* colRange; else UsableX = ActualX; endif if (! isempty (this.ClusteringFunction)) ## build the clusters for iter = 1 : length (this.InspectedK) ## do it only for the specified K values if (any (this.InspectedK(iter) == K)) if (isa (this.ClusteringFunction, 'function_handle')) ## custom function ClusteringSolution = ... this.ClusteringFunction(UsableX, this.InspectedK(iter)); if (ismatrix (ClusteringSolution) && ... rows (ClusteringSolution) == this.NumObservations && ... columns (ClusteringSolution) == this.P) ## the custom function returned a matrix: ## we take the index of the maximum value for every row [~, this.ClusteringSolutions(:, iter)] = ... max (ClusteringSolution, [], 2); elseif (iscolumn (ClusteringSolution) && length (ClusteringSolution) == this.NumObservations) this.ClusteringSolutions(:, iter) = ClusteringSolution; elseif (isrow (ClusteringSolution) && length (ClusteringSolution) == this.NumObservations) this.ClusteringSolutions(:, iter) = ClusteringSolution'; else error (strcat ("GapEvaluation: invalid return value", ... " from custom clustering function")); endif else switch (this.ClusteringFunction) case 'kmeans' this.ClusteringSolutions(:, iter) = kmeans (UsableX, ... this.InspectedK(iter), 'Distance', this.Distance, ... 'EmptyAction', 'singleton', 'Replicates', 5); case 'linkage' if (! isempty (this.Distance)) ## use clusterdata Distance_tmp = this.Distance; LinkageMethod = 'average'; # for non euclidean methods if (strcmpi (this.Distance, 'sqeuclidean')) ## pdist uses different names for its algorithms Distance_tmp = 'squaredeuclidean'; LinkageMethod = 'ward'; elseif (strcmpi (this.Distance, 'euclidean')) LinkageMethod = 'ward'; endif this.ClusteringSolutions(:, iter) = clusterdata ... (UsableX, 'MaxClust', this.InspectedK(iter), ... 'Distance', Distance_tmp, 'Linkage', LinkageMethod); else ## use linkage Z = linkage (this.DistanceVector, 'average'); this.ClusteringSolutions(:, iter) = ... cluster (Z, 'MaxClust', this.InspectedK(iter)); endif case 'gmdistribution' gmm = fitgmdist (UsableX, this.InspectedK(iter), ... 'SharedCov', true, 'Replicates', 5); this.ClusteringSolutions(:, iter) = cluster (gmm, UsableX); otherwise ## this should not happen error ("GapEvaluation: unexpected error, report this bug."); endswitch endif endif endfor endif ## get the gap values for every clustering distance_pdist = this.Distance; if (strcmpi (distance_pdist, 'sqeuclidean')) distance_pdist = 'squaredeuclidean'; endif ## compute LogW for iter = 1 : length (this.InspectedK) ## do it only for the specified K values if (any (this.InspectedK(iter) == K)) wk = 0; for r = 1 : this.InspectedK(iter) vIndicesR = find (this.ClusteringSolutions(:, iter) == r); nr = length (vIndicesR); Dr = pdist (UsableX(vIndicesR, :), distance_pdist); wk += sum (Dr) / (2 * nr); endfor if (mcrun <= this.B) this.mExpectedLogW(mcrun, iter) = log (wk); else this.LogW(iter) = log (wk); endif endif endfor endfor this.ExpectedLogW = mean (this.mExpectedLogW); this.SE = sqrt ((1 + 1 / this.B) * sumsq (this.mExpectedLogW - ... this.ExpectedLogW) / this.B); this.StdLogW = std (this.mExpectedLogW); this.CriterionValues = this.ExpectedLogW - this.LogW; ## As in the other criteria, the solutions are echoed back only when ## this object built them. this.OptimalIndex = this.gapSearch (); if (isempty (this.OptimalIndex) || all (isnan (this.CriterionValues))) this.OptimalIndex = []; this.OptimalK = NaN; this.OptimalY = []; else this.OptimalK = this.InspectedK(this.OptimalIndex(1)); if (isempty (this.ClusteringFunction)) this.OptimalY = []; else this.OptimalY = this.ClusteringSolutions(:, this.OptimalIndex(1)); endif endif endfunction ## gapSearch ## find the best solution according to the gap method function ind = gapSearch (this) if (strcmpi (this.SearchMethod, 'globalmaxse')) [gapmax, indgp] = max (this.CriterionValues); for iter = 1 : length (this.InspectedK) ind = iter; if (this.CriterionValues(iter) > (gapmax - this.SE(indgp))) return endif endfor elseif (strcmpi (this.SearchMethod, 'firstmaxse')) for iter = 1 : (length (this.InspectedK) - 1) ind = iter; if (this.CriterionValues(iter) > (this.CriterionValues(iter + 1) - ... this.SE(iter + 1))) return endif endfor else ## this should not happen error ("GapEvaluation: unexpected error, please report this bug."); endif endfunction endmethods endclassdef %!test %! load fisheriris %! eva = evalclusters (meas([1:50],:), 'kmeans', 'gap', 'KList', [1:3], ... %! 'referencedistribution', 'uniform'); %! assert_equal (class (eva), "GapEvaluation"); %!function C = count_calls_gap (X, k) %! global count_calls_gap_n; %! count_calls_gap_n += 1; %! C = mod ((0 : rows (X) - 1)', k) + 1; %!endfunction %!test %! ## custom function must be called exactly once per inspected K per run %! global count_calls_gap_n; %! count_calls_gap_n = 0; %! evalclusters (rand (20, 2), @count_calls_gap, 'gap', ... %! 'KList', [2, 3], 'B', 2); %! assert_equal (count_calls_gap_n, 6); %! clear -global count_calls_gap_n; statistics-release-2.1.2/inst/Clustering/SilhouetteEvaluation.m000066400000000000000000000402211527311104300247470ustar00rootroot00000000000000## Copyright (C) 2021 Stefano Guidoni ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef SilhouetteEvaluation < ClusterCriterion ## -*- texinfo -*- ## @deftp {statistics} SilhouetteEvaluation ## ## Silhouette evaluation for clustering ## ## The @code{SilhouetteEvaluation} class implements an object to evaluate ## clustering solutions using the silhouette criterion. A ## @code{SilhouetteEvaluation} object is a @code{ClusterCriterion} object ## that computes silhouette values for clustering solutions and selects the ## best number of clusters as the one with the highest average silhouette ## value. ## ## Create a @code{SilhouetteEvaluation} object by using the ## @code{evalclusters} function or the class constructor. ## ## List of public properties specific to @code{SilhouetteEvaluation}: ## @table @code ## @item @qcode{Distance} ## A valid distance metric name (string), a function handle, or a numeric ## vector as returned by @code{pdist}. This specifies how pairwise ## distances are computed. ## ## @item @qcode{ClusterPriors} ## A character vector specifying how to evaluate silhouette values across ## clusters: @qcode{'empirical'} (default) uses empirical cluster priors, ## or @qcode{'equal'} treats clusters equally. ## ## @item @qcode{ClusterSilhouettes} ## A cell array containing silhouette values for each observation for each ## inspected cluster number. ## @end table ## ## The best clustering solution according to the silhouette criterion is the ## one that yields the highest average silhouette value. ## ## @seealso{evalclusters, ClusterCriterion, CalinskiHarabaszEvaluation, ## DaviesBouldinEvaluation, GapEvaluation} ## @end deftp properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {SilhouetteEvaluation} {property} Distance ## ## Distance measure ## ## A string naming a distance metric, a function handle that computes ## distances, or a numeric vector as produced by @code{pdist}. This ## property is read-only. ## ## @end deftp Distance = ''; ## -*- texinfo -*- ## @deftp {SilhouetteEvaluation} {property} ClusterPriors ## ## Cluster prior handling ## ## Specifies how cluster-level silhouette aggregation is computed. Valid ## values are @qcode{'empirical'} (default) and @qcode{'equal'}. This ## property is read-only. ## ## @end deftp ClusterPriors = ''; ## -*- texinfo -*- ## @deftp {SilhouetteEvaluation} {property} ClusterSilhouettes ## ## Silhouette values ## ## A cell array where each element holds the mean silhouette value of ## each cluster of a given clustering (corresponding to an inspected K), ## so element @var{i} is a vector of @code{InspectedK(@var{i})} values. ## This property is read-only. ## ## @end deftp ClusterSilhouettes = {}; endproperties properties(Access = protected) ## -*- texinfo -*- ## @deftp {SilhouetteEvaluation} {property} DistanceVector ## ## Precomputed distance vector ## ## If a numeric vector is supplied as the distance metric it is stored ## here and used instead of computing distances via @code{pdist}. This ## property is read-only. ## ## @end deftp DistanceVector = []; endproperties methods(Access = public) ## -*- texinfo -*- ## @deftypefn {SilhouetteEvaluation} {@var{obj} =} SilhouetteEvaluation (@var{x}, @var{clust}, @var{KList}) ## @deftypefnx {SilhouetteEvaluation} {@var{obj} =} SilhouetteEvaluation (@dots{}, @var{Name}, @var{Value}) ## ## Create a @code{SilhouetteEvaluation} object to evaluate clustering ## solutions for data @var{x} using clustering method @var{clust} over the ## list of cluster numbers @var{KList}. ## ## @itemize ## @item ## @var{x} is an @math{N*P} numeric matrix of observations (rows) and ## predictors (columns). ## @item ## @var{clust} is a string naming the clustering method (for example ## @qcode{'kmeans'}, @qcode{'linkage'}, or a custom function handle). ## @item ## @var{KList} is a vector of positive integers specifying the cluster ## numbers to inspect. ## @end itemize ## ## Optional name-value pairs: ## ## @multitable @columnfractions 0.20 0.78 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Distance'} @tab Distance metric name, function handle, ## or numeric pdist vector. Default: @qcode{'sqeuclidean'}. ## ## @item @qcode{'ClusterPriors'} @tab Either @qcode{'empirical'} ## (default) or @qcode{'equal'}. ## @end multitable ## ## @seealso{silhouette, evalclusters, ClusterCriterion} ## @end deftypefn function this = SilhouetteEvaluation (x, clust, KList, ... distanceMetric = 'sqeuclidean', clusterPriors = 'empirical') this@ClusterCriterion (x, clust, KList); ## parsing the distance criterion if (ischar (distanceMetric)) if (any (strcmpi (distanceMetric, {'sqeuclidean', ... 'euclidean', 'cityblock', 'cosine', 'correlation', ... 'hamming', 'jaccard'}))) ## Report the metric under its canonical spelling, as MATLAB does. ## The name has already been matched exactly above, so validatestring ## only canonicalises it and never widens what is accepted. this.Distance = validatestring (distanceMetric, ... {'sqEuclidean', 'Euclidean', 'cityblock', ... 'cosine', 'correlation', 'Hamming', 'Jaccard'}); ## kmeans can use only a subset if (strcmpi (clust, 'kmeans') && any (strcmpi (this.Distance, ... {'euclidean', 'jaccard'}))) error (strcat ("SilhouetteEvaluation: invalid distance", ... " criterion '%s' for 'kmeans'."), distanceMetric); endif else error ("SilhouetteEvaluation: unknown distance criterion '%s'.", ... distanceMetric); endif elseif (isa (distanceMetric, 'function_handle')) this.Distance = distanceMetric; ## kmeans cannot use a function handle if (strcmpi (clust, 'kmeans')) error (strcat ("SilhouetteEvaluation: invalid distance", ... " criterion for 'kmeans'.")); endif elseif (isvector (distanceMetric) && isnumeric (distanceMetric)) this.Distance = ''; this.DistanceVector = distanceMetric; # the validity check is delegated ## kmeans cannot use a distance vector if (strcmpi (clust, 'kmeans')) error (strcat ("SilhouetteEvaluation: invalid", ... " distance criterion for 'kmeans'.")); endif else error ("SilhouetteEvaluation: invalid distance metric."); endif ## parsing the prior probabilities of each cluster if (ischar (distanceMetric)) if (any (strcmpi (clusterPriors, {'empirical', 'equal'}))) this.ClusterPriors = lower (clusterPriors); else error (strcat ("SilhouetteEvaluation: unknown prior", ... " probability criterion '%s'"), clusterPriors); endif else error ("SilhouetteEvaluation: invalid prior probabilities."); endif this.CriterionName = 'Silhouette'; this = this.evaluate (this.InspectedK); endfunction ## -*- texinfo -*- ## @deftypefn {SilhouetteEvaluation} {@var{obj} =} addK (@var{obj}, @var{K}) ## ## Add a new cluster number to inspect and re-evaluate silhouette values ## for newly added cluster numbers. ## ## @end deftypefn function this = addK (this, K) this = addK@ClusterCriterion (this, K); ## if we have new data, we need a new evaluation if (this.OptimalK == 0) ClusterSilhouettes_tmp = {}; pS = 0; # position shift of the elements of ClusterSilhouettes for iter = 1 : length (this.InspectedK) ## reorganize ClusterSilhouettes according to the new list ## of cluster numbers if (any (this.InspectedK(iter) == K)) pS += 1; else ClusterSilhouettes_tmp{iter} = this.ClusterSilhouettes{iter - pS}; endif endfor this.ClusterSilhouettes = ClusterSilhouettes_tmp; this = this.evaluate (K); endif endfunction ## -*- texinfo -*- ## @deftypefn {SilhouetteEvaluation} {} plot (@var{obj}) ## @deftypefnx {SilhouetteEvaluation} {@var{h} =} plot (@var{obj}) ## ## Plot the silhouette evaluation results. ## ## Plot the criterion values (average silhouette) against inspected cluster ## numbers (@code{InspectedK}) for the given @var{obj}. Optionally returns ## the axis handle for the plot. ## ## @end deftypefn function h = plot (this) yLabel = sprintf ("%s value", this.CriterionName); h = gca (); hold on; plot (this.InspectedK, this.CriterionValues, 'bo-'); plot (this.OptimalK, this.CriterionValues(this.OptimalIndex), 'b*'); xlabel ('number of clusters'); ylabel (yLabel); hold off; endfunction endmethods methods(Access = protected) ## evaluate ## do the evaluation function this = evaluate (this, K) ## use complete observations only UsableX = this.X(find (this.Missing == false), :); if (! isempty (this.ClusteringFunction)) ## build the clusters for iter = 1 : length (this.InspectedK) ## do it only for the specified K values if (any (this.InspectedK(iter) == K)) if (isa (this.ClusteringFunction, 'function_handle')) ## custom function ClusteringSolution = ... this.ClusteringFunction(UsableX, this.InspectedK(iter)); if (ismatrix (ClusteringSolution) && ... rows (ClusteringSolution) == this.NumObservations && ... columns (ClusteringSolution) == this.P) ## the custom function returned a matrix: ## we take the index of the maximum value for every row [~, this.ClusteringSolutions(:, iter)] = ... max (ClusteringSolution, [], 2); elseif (iscolumn (ClusteringSolution) && length (ClusteringSolution) == this.NumObservations) this.ClusteringSolutions(:, iter) = ClusteringSolution; elseif (isrow (ClusteringSolution) && length (ClusteringSolution) == this.NumObservations) this.ClusteringSolutions(:, iter) = ClusteringSolution'; else error (strcat ("SilhouetteEvaluation: invalid return", ... " value from custom clustering function.")); endif else switch (this.ClusteringFunction) case 'kmeans' this.ClusteringSolutions(:, iter) = kmeans (UsableX, ... this.InspectedK(iter), 'Distance', this.Distance, ... 'EmptyAction', 'singleton', 'Replicates', 5); case 'linkage' if (! isempty (this.Distance)) ## use clusterdata Distance_tmp = this.Distance; LinkageMethod = 'average'; # for non euclidean methods if (strcmpi (this.Distance, 'sqeuclidean')) ## pdist uses different names for its algorithms Distance_tmp = 'squaredeuclidean'; LinkageMethod = 'ward'; elseif (strcmpi (this.Distance, 'euclidean')) LinkageMethod = 'ward'; endif this.ClusteringSolutions(:, iter) = clusterdata (UsableX,... 'MaxClust', this.InspectedK(iter), ... 'Distance', Distance_tmp, 'Linkage', LinkageMethod); else ## use linkage Z = linkage (this.DistanceVector, 'average'); this.ClusteringSolutions(:, iter) = ... cluster (Z, 'MaxClust', this.InspectedK(iter)); endif case 'gmdistribution' gmm = fitgmdist (UsableX, this.InspectedK(iter), ... 'SharedCov', true, 'Replicates', 5); this.ClusteringSolutions(:, iter) = cluster (gmm, UsableX); otherwise error (strcat ("SilhouetteEvaluation: unexpected", ... " error, report this bug.")); endswitch endif endif endfor endif ## get the silhouette values for every clustering for iter = 1 : length (this.InspectedK) ## do it only for the specified K values if (any (this.InspectedK(iter) == K)) ## Custom call to silhouette to avoid plotting any figures. The ## metric is the one this object was built with: hard-coding ## 'sqeuclidean' here made every Distance give the same answer. if (isempty (this.Distance)) metric = this.DistanceVector; else metric = this.Distance; endif si = silhouette (UsableX, this.ClusteringSolutions(:, iter), ... metric, 'DoNotPlot'); ## ClusterSilhouettes holds the mean silhouette of each cluster, one ## value per cluster, not the value of every observation. means = zeros (this.InspectedK(iter), 1); for k = 1 : this.InspectedK(iter) means(k) = mean (si(this.ClusteringSolutions(:, iter) == k)); endfor this.ClusterSilhouettes{iter} = means; if (strcmpi (this.ClusterPriors, 'empirical')) ## Weighted by cluster size, which is what averaging over the ## observations does this.CriterionValues(iter) = mean (si); else ## equal this.CriterionValues(iter) = mean (means); endif endif endfor ## A criterion that came out undefined everywhere leaves no optimum to ## report, and the solutions are echoed back only when this object built ## them: given a matrix of clusterings the caller already has them. if (all (isnan (this.CriterionValues))) this.OptimalIndex = []; this.OptimalK = NaN; this.OptimalY = []; else [~, this.OptimalIndex] = max (this.CriterionValues); this.OptimalK = this.InspectedK(this.OptimalIndex(1)); if (isempty (this.ClusteringFunction)) this.OptimalY = []; else this.OptimalY = this.ClusteringSolutions(:, this.OptimalIndex(1)); endif endif endfunction endmethods endclassdef %!test %! load fisheriris %! eva = evalclusters (meas, 'kmeans', 'silhouette', 'KList', [1:6]); %! assert_equal (class (eva), "SilhouetteEvaluation"); %!function C = count_calls_silhouette (X, k) %! global count_calls_silhouette_n; %! count_calls_silhouette_n += 1; %! C = mod ((0 : rows (X) - 1)', k) + 1; %!endfunction %!test %! ## custom function must be called exactly once per inspected K %! global count_calls_silhouette_n; %! count_calls_silhouette_n = 0; %! evalclusters (rand (20, 2), @count_calls_silhouette, ... %! 'silhouette', 'KList', [2, 3]); %! assert_equal (count_calls_silhouette_n, 2); %! clear -global count_calls_silhouette_n; statistics-release-2.1.2/inst/Clustering/cluster.m000066400000000000000000000245431527311104300222640ustar00rootroot00000000000000## Copyright (C) 2021 Stefano Guidoni ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{T} =} cluster (@var{Z}, "Cutoff", @var{C}) ## @deftypefnx {statistics} {@var{T} =} cluster (@var{Z}, "Cutoff", @var{C}, "Depth", @var{D}) ## @deftypefnx {statistics} {@var{T} =} cluster (@var{Z}, "Cutoff", @var{C}, "Criterion", @var{criterion}) ## @deftypefnx {statistics} {@var{T} =} cluster (@var{Z}, "MaxClust", @var{N}) ## ## Define clusters from an agglomerative hierarchical cluster tree. ## ## Given a hierarchical cluster tree @var{Z} generated by the @code{linkage} ## function, @code{cluster} defines clusters, using a threshold value @var{C} to ## identify new clusters ('Cutoff') or according to a maximum number of desired ## clusters @var{N} ('MaxClust'). ## ## @var{criterion} is used to choose the criterion for defining clusters, which ## can be either "inconsistent" (default) or "distance". When using ## "inconsistent", @code{cluster} compares the threshold value @var{C} to the ## inconsistency coefficient of each link; when using "distance", @code{cluster} ## compares the threshold value @var{C} to the height of each link. ## @var{D} is the depth used to evaluate the inconsistency coefficient, its ## default value is 2. ## ## @code{cluster} uses "distance" as a criterion for defining new clusters when ## it is used with the 'MaxClust' method. ## ## @seealso{clusterdata, dendrogram, inconsistent, kmeans, linkage, pdist} ## @end deftypefn function T = cluster (Z, opt, varargin) switch (lower (opt)) ## check the input case 'cutoff' if (nargin < 3) print_usage (); else C = varargin{1}; D = 2; criterion = 'inconsistent'; if (nargin > 3) pair_index = 2; while (pair_index < (nargin - 2)) switch (lower (varargin{pair_index})) case 'depth' D = varargin{pair_index + 1}; case 'criterion' criterion = varargin{pair_index + 1}; otherwise error ("cluster: unknown property '%s'.", varargin{pair_index}); endswitch pair_index += 2; endwhile endif endif if ((! (isscalar (C) || isvector (C))) || (C < 0)) error (strcat ("cluster: C must be a positive scalar", ... " or a vector of positive numbers.")); endif case 'maxclust' if (nargin != 3) print_usage (); else N = varargin{1}; C = []; endif if ((! (isscalar (N) || isvector (N))) || (N < 0)) error (strcat ("cluster: N must be a positive number", ... " or a vector of positive numbers.")); endif otherwise error ("cluster: unknown option '%s'.", opt); endswitch if (columns (Z) != 3 || ! isnumeric (Z) || ! (max (Z(end, 1:2)) == rows (Z) * 2)) error ("cluster: Z must be a matrix generated by the linkage function."); endif ## number of observations n = rows (Z) + 1; ## vector of values used by the threshold check vThresholds = []; ## starting number of clusters nClusters = 1; ## the return value is the matrix T, constituted by one or more vector vT T = []; vT = zeros (1, n); ## main logic switch (lower (opt)) case 'cutoff' switch (lower (criterion)) case 'inconsistent' vThresholds = inconsistent (Z, D)(:, 4); case 'distance' vThresholds = Z(:, 3); otherwise error ("cluster: unknown criterion '%s'.", criterion); endswitch ## A node is a cluster only when it and everything below it are under ## the cutoff. Testing the node alone leaves a low node holding a high ## descendant undivided, and so returns too few clusters. smax = subtree_maximum (Z, vThresholds, n); for k = 1:numel (C) T = [T; cut_by_cutoff(Z, n, smax, C(k))]; endfor case 'maxclust' ## Undoing the highest N-1 merges leaves exactly N groups. Cutting at ## the height of the Nth node from the top cannot: when merges tie there ## is no height between them, and on a tree whose merges all sit at one ## height -- every observation identical, say -- it separates nothing and ## every observation comes back in a cluster of its own. for k = 1:numel (N) T = [T; cut_by_count(Z, n, N(k))]; endfor endswitch T = T'; # return value endfunction ## Largest criterion value anywhere in the subtree each internal node roots. ## The rows of Z are in merge order, so a node's children are always earlier ## rows and one forward pass suffices. function s = subtree_maximum (Z, v, n) s = v(:); for i = 1:rows (Z) for j = 1:2 child = Z(i, j); if (child > n) s(i) = max (s(i), s(child - n)); endif endfor endfor endfunction ## Put every leaf below an internal node into one cluster. function t = collect_leaves (Z, n, index, label, t) for j = 1:2 child = Z(index, j); if (child > n) t = collect_leaves (Z, n, child - n, label, t); else t(child) = label; endif endfor endfunction ## Descend from the root, taking a node as a cluster as soon as its whole ## subtree is under the cutoff and splitting it otherwise. function t = cut_by_cutoff (Z, n, smax, c) t = zeros (1, n); k = 0; pending = rows (Z); while (! isempty (pending)) index = pending(end); pending(end) = []; if (smax(index) < c) k++; t = collect_leaves (Z, n, index, k, t); else for j = 1:2 child = Z(index, j); if (child > n) pending(end+1) = child - n; else k++; t(child) = k; endif endfor endif endwhile endfunction ## Perform the lowest n-N merges and let what remains be the clusters, so that ## exactly N of them come back however the merge heights fall. function t = cut_by_count (Z, n, N) if (N >= n) t = 1:n; return; endif keep = n - max (N, 1); comp = 1:n; # component of each leaf nodecomp = zeros (1, rows (Z)); # component of each internal node for i = 1:keep ca = node_component (Z(i, 1), n, comp, nodecomp); cb = node_component (Z(i, 2), n, comp, nodecomp); comp(comp == cb) = ca; nodecomp(i) = ca; nodecomp(nodecomp == cb) = ca; endfor [~, ~, t] = unique (comp); t = t(:)'; endfunction ## Component holding a node, whether it is a leaf or an internal node. function c = node_component (node, n, comp, nodecomp) if (node > n) c = nodecomp(node - n); else c = comp(node); endif endfunction ## Test input validation %!error cluster () %!error cluster ([1 1], 'Cutoff', 1) %!error cluster ([1 2 1], 'Bogus', 1) %!error cluster ([1 2 1], 'Cutoff', -1) %!error cluster ([1 2 1], 'Cutoff', 1, 'Bogus', 1) ## Test output ## The partitions below are MATLAB R2024a's on the same data. Cluster numbers ## are arbitrary, so the tests compare groupings rather than labels. %!shared X, Z %! X = [0, 0; 0.1, 0.1; 0.2, 0; 5, 5; 5.1, 5.2; 5.2, 5.0; 10, 0; ... %! 10.1, 0.2; 20, 20; 0.05, 0.3]; %! Z = linkage (pdist (X), "single"); %!test # MaxClust returns exactly the number of clusters asked for %! for n = 1:5 %! assert_equal (numel (unique (cluster (Z, "MaxClust", n))), n); %! endfor %!test # and the groupings are MATLAB's %! t = cluster (Z, "MaxClust", 3); %! assert_equal (numel (unique (t([1, 2, 3, 4, 5, 6, 10]))), 1); %! assert_equal (numel (unique (t([7, 8]))), 1); %! assert_equal (t(9) != t(1) && t(7) != t(1), true); %!test # merges at equal heights still divide: a height threshold cannot %! ## split a tree whose every merge sits at the same height, which is what %! ## five identical observations produce. %! Zflat = linkage (pdist (ones (5, 2)), "single"); %! assert_equal (all (Zflat(:,3) == 0), true); %! for n = 1:5 %! assert_equal (numel (unique (cluster (Zflat, "MaxClust", n))), n); %! endfor %!test # a tie at the cut does not cost a cluster either %! Ztie = [1, 2, 1; 3, 4, 2; 6, 7, 2; 5, 8, 3]; %! for n = 1:5 %! assert_equal (numel (unique (cluster (Ztie, "MaxClust", n))), n); %! endfor %!test # a cluster is a node whose whole subtree is under the cutoff %! assert_equal (numel (unique (cluster (Z, "Cutoff", 0.5))), 6); %! assert_equal (numel (unique (cluster (Z, "Cutoff", 0.8))), 4); %! assert_equal (numel (unique (cluster (Z, "Cutoff", 1.2))), 1); %!test # the depth the inconsistency is measured over reaches the result %! assert_equal (numel (unique (cluster (Z, "Cutoff", 0.9, "Depth", 2))), 4); %! assert_equal (numel (unique (cluster (Z, "Cutoff", 0.9, "Depth", 3))), 5); %! t = cluster (Z, "Cutoff", 0.9, "Depth", 3); %! assert_equal (numel (unique (t([1, 2, 3]))), 1); %! assert_equal (numel (unique (t([4, 5, 6]))), 1); %! assert_equal (t(10) != t(1), true); %!test # the distance criterion is unchanged %! assert_equal (numel (unique (cluster (Z, "Cutoff", 1, ... %! "Criterion", "distance"))), 4); %! assert_equal (numel (unique (cluster (Z, "Cutoff", 30, ... %! "Criterion", "distance"))), 1); %!test # a vector of cutoffs gives one column per cutoff %! T = cluster (Z, "MaxClust", [2, 3, 4]); %! assert_equal (size (T), [10, 3]); %! assert_equal (max (T), [2, 3, 4]); %!test # well separated groups come out whole %! Xs = [randn(10, 2) * 0.05 + 1; randn(10, 2) * 0.05 - 1]; %! t = cluster (linkage (pdist (Xs), "ward"), "MaxClust", 2); %! assert_equal (numel (unique (t)), 2); %! assert_equal (numel (unique (t(1:10))), 1); %! assert_equal (numel (unique (t(11:20))), 1); %! assert_equal (t(1) != t(11), true); statistics-release-2.1.2/inst/Clustering/clusterdata.m000066400000000000000000000120021527311104300231010ustar00rootroot00000000000000## Copyright (C) 2021 Stefano Guidoni ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{T} =} clusterdata (@var{X}, @var{cutoff}) ## @deftypefnx {statistics} {@var{T} =} clusterdata (@var{X}, @var{Name}, @var{Value}) ## ## Wrapper function for @code{linkage} and @code{cluster}. ## ## If @var{cutoff} is used, then @code{clusterdata} calls @code{linkage} and ## @code{cluster} with default value, using @var{cutoff} as a threshold value ## for @code{cluster}. If @var{cutoff} is an integer and greater or equal to 2, ## then @var{cutoff} is interpreted as the maximum number of cluster desired ## and the "MaxClust" option is used for @code{cluster}. ## ## If @var{cutoff} is not used, then @code{clusterdata} expects a list of pair ## arguments. Then you must specify either the "Cutoff" or "MaxClust" option ## for @code{cluster}. The method and metric used by @code{linkage}, are ## defined through the "linkage" and "distance" arguments. ## ## @seealso{cluster, dendrogram, inconsistent, kmeans, linkage, pdist} ## @end deftypefn function T = clusterdata (X, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("clusterdata: function called with too few input arguments."); endif linkage_criterion = 'single'; distance_method = 'euclidean'; savememory = 'off'; clustering_method = []; criterion = 'inconsistent'; D = 2; if (isnumeric (varargin{1})) # clusterdata (X, cutoff) C = varargin{1}; if (fix (C) == C && (C >= 2)) clustering_method = 'MaxClust'; else clustering_method = 'Cutoff'; endif else # clusterdata (Name, Value) pair_index = 1; while (pair_index < (nargin - 1)) switch (lower (varargin{pair_index})) case 'criterion' criterion = varargin{pair_index + 1}; case 'cutoff' clustering_method = 'Cutoff'; C = varargin{pair_index + 1}; case 'depth' D = varargin{pair_index + 1}; case 'distance' distance_method = varargin{pair_index + 1}; case 'linkage' linkage_criterion = varargin{pair_index + 1}; case 'maxclust' clustering_method = 'MaxClust'; C = varargin{pair_index + 1}; case 'savememory' savememory = varargin{pair_index + 1}; otherwise error ("clusterdata: unknown property %s", varargin{pair_index}); endswitch pair_index += 2; endwhile endif if (isempty (clustering_method)) error (strcat ("clusterdata: you must specify either 'MaxClust'", ... " or 'Cutoff' when using name-value arguments.")); endif ## main body Z = linkage (X, linkage_criterion, distance_method, 'savememory'); if (strcmp (lower (clustering_method), 'cutoff')) T = cluster (Z, clustering_method, C, 'Criterion', criterion, 'Depth', D); else T = cluster (Z, clustering_method, C); endif endfunction %!demo %! rng (42); %! r1 = randn (10, 2) * 0.25 + 1; %! r2 = randn (20, 2) * 0.5 - 1; %! X = [r1; r2]; %! %! wnl = warning ("off", 'Octave:linkage_savemem', 'local'); %! T = clusterdata (X, 'linkage', 'ward', 'MaxClust', 2); %! scatter (X(:,1), X(:,2), 36, T, 'filled'); ## Test input validation %!error ... %! clusterdata () %!error ... %! clusterdata (1) %!error clusterdata ([1 1], 'Bogus', 1) %!error clusterdata ([1 1], 'Depth', 1) ## MaxClust must deliver the number of clusters it was asked for, whatever the ## merge heights do. Verified against MATLAB R2024a. %!test %! assert_equal (numel (unique (clusterdata (ones (5, 2), "MaxClust", 2))), 2); %! assert_equal (numel (unique (clusterdata (ones (5, 2), "MaxClust", 4))), 4); %!test # the depth reaches the result, as it does in cluster %! X = [0, 0; 0.1, 0.1; 0.2, 0; 5, 5; 5.1, 5.2; 5.2, 5.0; 10, 0; ... %! 10.1, 0.2; 20, 20; 0.05, 0.3]; %! t2 = clusterdata (X, "Cutoff", 0.9, "Depth", 2); %! t3 = clusterdata (X, "Cutoff", 0.9, "Depth", 3); %! assert_equal (numel (unique (t2)), 4); %! assert_equal (numel (unique (t3)), 5); %! t = clusterdata (X, "MaxClust", 3); %! assert_equal (numel (unique (t)), 3); %! assert_equal (numel (unique (t([7, 8]))), 1); statistics-release-2.1.2/inst/Clustering/cophenet.m000066400000000000000000000110761527311104300224050ustar00rootroot00000000000000## Copyright (C) 2021 Stefano Guidoni ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{c}, @var{d}] =} cophenet (@var{Z}, @var{y}) ## ## Compute the cophenetic correlation coefficient. ## ## The cophenetic correlation coefficient @var{C} of a hierarchical cluster tree ## @var{Z} is the linear correlation coefficient between the cophenetic ## distances @var{d} and the euclidean distances @var{y}. ## @tex ## \def\frac#1#2{{\begingroup#1\endgroup\over#2}} ## $$ c = \frac {\sum_{i < j}(Y_{ij}-{\bar {y}})(Z_{ij}-{\bar{z}})} ## {\sqrt{\sum_{i < j}(Y_{ij}-{\bar {y}})^2(Z_{ij}-{\bar{z}})^2}} $$ ## @end tex ## ## It is a measure of the similarity between the distance of the leaves, as seen ## in the tree, and the distance of the original data points, which were used to ## build the tree. When this similarity is greater, that is the coefficient is ## closer to 1, the tree renders an accurate representation of the distances ## between the original data points. ## ## @var{Z} is a hierarchical cluster tree, as the output of @code{linkage}. ## @var{y} is a vector of euclidean distances, as the output of @code{pdist}. ## ## The optional output @var{d} is a vector of cophenetic distances, in the same ## lower triangular format as @var{y}. The cophenetic distance between two data ## points is the height of the lowest common node of the tree. ## ## @seealso{cluster, dendrogram, inconsistent, linkage, pdist, squareform} ## @end deftypefn function [c, d] = cophenet (Z, y) ## Check input arguments if (nargin < 2) error ("cophenet: function called with too few input arguments."); endif ## Z must be a tree [m w] = size (Z); if ((w != 3) || (! isnumeric (Z)) || (! (max (Z(end,1:2)) == m * 2))) error ("cophenet: Z must be a matrix as generated by the linkage function."); endif ## Data set size n = m + 1; ## Y must be a vector of distances if ((! isnumeric (y)) || (length (y) != (n - 1) * n / 2)) error ("cophenet: Y must be a vector of euclidean distances."); endif ## Compute the cophenetic distances d d = zeros (1, length (y)); N = sparse ((m - 1), m); # to keep track of the leaves from each branch for i = 1 : m l_n = Z(i, 1); r_n = Z(i, 2); if (l_n > n) l_v = nonzeros (N(l_n - n, :)); # the list of leaves from the left branch else l_v = l_n; endif if (r_n > n) r_v = nonzeros (N(r_n - n, :)); # the list of leaves from the right branch else r_v = r_n; endif j_max = length (l_v); k_max = length (r_v); ## Keep track of the leaves in each sub-branch, i.e. node; ## this does not matter for the last node, which includes all leaves if (i < m) N(i, 1 : (j_max + k_max)) = [l_v' r_v']; endif for j = 1 : j_max for k = 1: k_max ## d is in the same format as y if (l_v(j) < r_v(k)) index = (l_v(j) - 1) * m - sum (1 : (l_v(j) - 2)) + (r_v(k) - l_v(j)); else index = (r_v(k) - 1) * m - sum (1 : (r_v(k) - 2)) + (l_v(j) - r_v(k)); endif d(index) = Z(i, 3); endfor endfor endfor ## Compute the cophenetic correlation c y_mean = mean (y); z_mean = mean (d); Y_sigma = y - y_mean; Z_sigma = d - z_mean; c = sum (Z_sigma .* Y_sigma) / sqrt (sum (Y_sigma .^ 2) * sum (Z_sigma .^ 2)); endfunction %!demo %! rng (42); %! X = randn (10,2); %! y = pdist (X); %! Z = linkage (y, 'average'); %! cophenet (Z, y) ## Test input validation %!error cophenet () %!error cophenet (1) %!error ... %! cophenet (ones (2,2), 1) %!error ... %! cophenet ([1 2 1], 'a') %!error ... %! cophenet ([1 2 1], [1 2]) statistics-release-2.1.2/inst/Clustering/dbscan.m000066400000000000000000000255261527311104300220370ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{idx} =} dbscan (@var{X}, @var{epsilon}, @var{minpts}) ## @deftypefnx {statistics} {@var{idx} =} dbscan (@var{D}, @var{epsilon}, @var{minpts}, @qcode{'Distance'}, @qcode{'precomputed'}) ## @deftypefnx {statistics} {@var{idx} =} dbscan (@dots{}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{idx}, @var{corepts}] =} dbscan (@dots{}) ## ## Density-Based Spatial Clustering of Applications with Noise (DBSCAN). ## ## @code{@var{idx} = dbscan (@var{X}, @var{epsilon}, @var{minpts})} partitions ## the observations in the @math{N*P} numeric matrix @var{X} into clusters ## using the DBSCAN algorithm with neighborhood radius @var{epsilon} and ## minimum number of neighbors @var{minpts}. Rows of @var{X} correspond to ## observations and columns correspond to features or variables. @var{epsilon} ## must be a nonnegative scalar and @var{minpts} a positive integer scalar. ## @var{idx} is an @math{N*1} vector of cluster indices, numbered @math{1} to ## the number of clusters found; observations flagged as noise are assigned the ## value @math{-1}. ## ## A point is a @emph{core point} when at least @var{minpts} observations ## (@strong{including the point itself}) lie within distance @var{epsilon} of ## it. Clusters grow from core points to every observation that is ## density-reachable from them; a non-core observation that lies within ## @var{epsilon} of a core point becomes a @emph{border point} and joins that ## point's cluster, while an observation that is neither core nor within reach ## of a core point is labelled noise. A border point that is reachable from ## more than one cluster is assigned to the first cluster that reaches it, ## following the order of the observations in @var{X}. ## ## @code{@var{idx} = dbscan (@var{D}, @var{epsilon}, @var{minpts}, ## @qcode{'Distance'}, @qcode{'precomputed'})} treats the @math{N*N} matrix ## @var{D} as a precomputed matrix of pairwise distances between observations, ## such as the output of @code{pdist2}; @qcode{@var{D}(i,j)} is the distance ## between observations @math{i} and @math{j}. ## ## @code{[@var{idx}, @var{corepts}] = dbscan (@dots{})} also returns an ## @math{N*1} logical vector @var{corepts} that is @qcode{true} for each ## observation that is a core point. ## ## Additional parameters can be specified by @qcode{Name-Value} pair arguments. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Distance'} @tab is the distance metric used to find neighbors, ## specified as one of the metrics accepted by @code{rangesearch} ## (@qcode{'euclidean'} by default, and also @qcode{'seuclidean'}, ## @qcode{'cityblock'}, @qcode{'chebychev'}, @qcode{'minkowski'}, ## @qcode{'mahalanobis'}, @qcode{'cosine'}, @qcode{'correlation'}, ## @qcode{'spearman'}, @qcode{'hamming'}, @qcode{'jaccard'}, or a custom ## distance function handle), or the string @qcode{'precomputed'} to interpret ## the first input as a matrix of pairwise distances. ## ## @item @qcode{'P'} @tab is the Minkowski distance exponent, a positive scalar. ## This argument is only valid when the selected distance metric is ## @qcode{'minkowski'}. By default it is 2. ## ## @item @qcode{'Scale'} @tab is the scale parameter for the standardized ## Euclidean distance, a nonnegative numeric vector of length equal to the ## number of columns in @var{X}. This argument is only valid when the selected ## distance metric is @qcode{'seuclidean'}. ## ## @item @qcode{'Cov'} @tab is the covariance matrix for the mahalanobis ## distance, a positive definite matrix matching the number of columns in ## @var{X}. This argument is only valid when the selected distance metric is ## @qcode{'mahalanobis'}. ## @end multitable ## ## @seealso{kmeans, rangesearch, pdist2, knnsearch} ## @end deftypefn function [idx, corepts] = dbscan (X, epsilon, minpts, varargin) ## Check number of input arguments if (nargin < 3) error ("dbscan: too few input arguments."); endif ## Validate X if (! isnumeric (X) || ! isreal (X) || ndims (X) != 2 || isempty (X)) error ("dbscan: X must be a nonempty real numeric matrix."); endif ## Validate epsilon if (! isscalar (epsilon) || ! isnumeric (epsilon) || ! isreal (epsilon) || epsilon < 0) error ("dbscan: EPSILON must be a nonnegative scalar."); endif ## Validate minpts if (! isscalar (minpts) || ! isnumeric (minpts) || ! isreal (minpts) || minpts < 1 || fix (minpts) != minpts) error ("dbscan: MINPTS must be a positive integer scalar."); endif ## Detect a precomputed distance matrix among the optional arguments precomputed = false; for i = 1:2:numel (varargin) if (ischar (varargin{i}) && strcmpi (varargin{i}, 'Distance') && i < numel (varargin) && ischar (varargin{i+1}) && strcmpi (varargin{i+1}, 'precomputed')) precomputed = true; endif endfor ## Build the epsilon-neighborhood (including the point itself) of each ## observation. For a precomputed distance matrix simply threshold each row; ## otherwise delegate the metric handling to rangesearch, which already ## includes the query point and applies a "distance <= epsilon" cutoff. if (precomputed) if (numel (varargin) != 2) error (strcat ("dbscan: no other Name-Value arguments are allowed", ... " with the 'precomputed' distance.")); endif if (rows (X) != columns (X)) error ("dbscan: X must be a square distance matrix for 'precomputed'."); endif N = rows (X); neigh = cell (N, 1); for i = 1:N neigh{i} = find (X(i,:) <= epsilon)(:); endfor else N = rows (X); neigh = rangesearch (X, X, epsilon, varargin{:}); neigh = cellfun (@(c) c(:), neigh, "UniformOutput", false); endif ## A point is a core point when its neighborhood (self included) holds at ## least minpts observations. corepts = cellfun (@numel, neigh) >= minpts; ## Canonical DBSCAN scan. Labels: 0 = unvisited, -1 = noise, k = cluster k. labels = zeros (N, 1); C = 0; for i = 1:N if (labels(i) != 0) continue; # already assigned to a cluster or as noise endif if (! corepts(i)) labels(i) = -1; # tentatively noise (may become a border) continue; endif ## Start a new cluster and expand it over the growing seed queue. C += 1; labels(i) = C; seeds = neigh{i}; k = 1; while (k <= numel (seeds)) q = seeds(k); k += 1; if (labels(q) == -1) labels(q) = C; # border point reclaimed from noise elseif (labels(q) == 0) labels(q) = C; if (corepts(q)) seeds = [seeds; neigh{q}]; # density-reachable: keep expanding endif endif endwhile endfor idx = labels; endfunction %!demo %! ## Cluster a set of points with two dense blobs and scattered noise. %! rng (42); %! X = [randn(30,2)*0.3 + 2; randn(30,2)*0.3 - 2; 5*(rand(6,2)-0.5)]; %! idx = dbscan (X, 0.6, 4); %! gscatter (X(:,1), X(:,2), idx); %! title ("dbscan: clusters (>=0) and noise (-1)"); ## Two well-separated clusters and one noise point (euclidean) %!test %! X = [0 0; 0 1; 1 0; 1 1; 10 10; 10 11; 11 10; 5 5]; %! [idx, cp] = dbscan (X, 1.5, 3); %! assert_equal (idx, [1; 1; 1; 1; 2; 2; 2; -1]); %! assert_equal (cp, logical ([1; 1; 1; 1; 1; 1; 1; 0])); ## Border point reachable from two clusters joins the first one reached %!test %! X = [0; 0.3; 0.6; 0.9; 2.5; 2.8; 3.1; 3.4; 1.7]; %! [idx, cp] = dbscan (X, 1, 4); %! assert_equal (idx, [1; 1; 1; 1; 2; 2; 2; 2; 1]); %! assert_equal (cp, logical ([1; 1; 1; 1; 1; 1; 1; 1; 0])); ## Cluster order follows the order of the observations, not geometry %!test %! X = [2.5; 2.8; 3.1; 3.4; 0; 0.3; 0.6; 0.9; 1.7]; %! idx = dbscan (X, 1, 4); %! assert_equal (idx, [1; 1; 1; 1; 2; 2; 2; 2; 1]); ## minpts counts the point itself: two neighbors + self meets minpts = 3 %!test %! X = [0; 1; 2]; %! [idx, cp] = dbscan (X, 1, 2); %! assert_equal (idx, [1; 1; 1]); %! assert_equal (cp, logical ([1; 1; 1])); ## minpts = 1 makes every point a core point and yields no noise %!test %! X = [0; 0; 5; 5; 10]; %! [idx, cp] = dbscan (X, 0.5, 1); %! assert_equal (idx, [1; 1; 2; 2; 3]); %! assert_equal (cp, logical ([1; 1; 1; 1; 1])); ## Precomputed distance matrix matches the euclidean result %!test %! X = [0 0; 0 1; 1 0; 1 1; 10 10; 10 11; 11 10; 5 5]; %! D = pdist2 (X, X); %! [idx, cp] = dbscan (D, 1.5, 3, "Distance", "precomputed"); %! assert_equal (idx, [1; 1; 1; 1; 2; 2; 2; -1]); %! assert_equal (cp, logical ([1; 1; 1; 1; 1; 1; 1; 0])); ## A non-euclidean metric is passed through to rangesearch %!test %! X = [0 0; 0 1; 1 0; 1 1; 10 10; 10 11; 11 10; 5 5]; %! idx = dbscan (X, 2, 3, "Distance", "cityblock"); %! assert_equal (idx, [1; 1; 1; 1; 2; 2; 2; -1]); ## Every point is noise when no neighborhood reaches minpts %!test %! X = [0; 10; 20; 30]; %! [idx, cp] = dbscan (X, 1, 2); %! assert_equal (idx, [-1; -1; -1; -1]); %! assert_equal (cp, logical ([0; 0; 0; 0])); ## Test input validation %!error dbscan (1) %!error dbscan (1, 1) %!error dbscan ([], 1, 1) %!error dbscan ("a", 1, 1) %!error dbscan (i, 1, 1) %!error dbscan (ones (3,2), [1 2], 1) %!error dbscan (ones (3,2), -1, 1) %!error dbscan (ones (3,2), "a", 1) %!error dbscan (ones (3,2), 1, 0) %!error dbscan (ones (3,2), 1, 1.5) %!error dbscan (ones (3,2), 1, [1 2]) %!error ... %! dbscan (ones (3,2), 1, 1, "Distance", "precomputed") %!error ... %! dbscan (ones (3,3), 1, 1, "Distance", "precomputed", "P", 3) statistics-release-2.1.2/inst/Clustering/doc-cache000066400000000000000000002551641527311104300221630ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 112 # name: # type: sq_string # elements: 1 # length: 26 CalinskiHarabaszEvaluation # name: # type: sq_string # elements: 1 # length: 754 statistics: CalinskiHarabaszEvaluation Calinski-Harabasz clustering evaluation. A CalinskiHarabaszEvaluation object contains the results of evaluating clustering solutions using the Calinski-Harabasz criterion. The Calinski-Harabasz index (also known as the Variance Ratio Criterion) is determined by the ratio of the between-cluster sum of squares (SSB) to the within-cluster sum of squares (SSW). A higher Calinski-Harabasz index value indicates a better clustering solution, implying that clusters are dense and well-separated. Create a CalinskiHarabaszEvaluation object by using the evalclusters function with the 'CalinskiHarabasz' criterion. See also: evalclusters, ClusterCriterion, DaviesBouldinEvaluation, GapEvaluation, SilhouetteEvaluation # name: # type: sq_string # elements: 1 # length: 40 Calinski-Harabasz clustering evaluation. # name: # type: sq_string # elements: 1 # length: 53 CalinskiHarabaszEvaluation.CalinskiHarabaszEvaluation # name: # type: sq_string # elements: 1 # length: 974 CalinskiHarabaszEvaluation: obj = CalinskiHarabaszEvaluation (x, clust, KList) Construct a CalinskiHarabaszEvaluation object to evaluate clustering solutions with the Calinski-Harabasz criterion. obj = CalinskiHarabaszEvaluation (x, clust, KList) clusters the data in x for every cluster count in KList and evaluates each solution. The evaluation runs at construction, so obj arrives with its CriterionValues and OptimalK already set. x is an NxP numeric matrix of observations (rows) and predictors (columns). A row holding a NaN is left out of NumObservations. clust names the clustering method, one of 'kmeans', 'linkage' and 'gmdistribution'; or a function handle that clusters the data; or an NxM numeric matrix of clustering solutions computed elsewhere, one column per cluster count, in which case ClusteringFunction is left empty. KList is a vector of positive integers, the cluster counts to inspect. evalclusters is the usual way to create one of these objects. # name: # type: sq_string # elements: 1 # length: 116 Construct a CalinskiHarabaszEvaluation object to evaluate clustering solutions with the Calinski-Harabasz criterion. # name: # type: sq_string # elements: 1 # length: 45 CalinskiHarabaszEvaluation.ClusteringFunction # name: # type: sq_string # elements: 1 # length: 289 ClusterCriterion: property ClusteringFunction Clustering algorithm A character vector or a function handle specifying the clustering algorithm used to generate the clustering solutions. It can be empty if the clustering solutions are passed as an input matrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Clustering algorithm # name: # type: sq_string # elements: 1 # length: 40 CalinskiHarabaszEvaluation.CriterionName # name: # type: sq_string # elements: 1 # length: 201 ClusterCriterion: property CriterionName Name of the evaluation criterion A character vector specifying the name of the criterion used to evaluate the clustering solutions. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Name of the evaluation criterion # name: # type: sq_string # elements: 1 # length: 42 CalinskiHarabaszEvaluation.CriterionValues # name: # type: sq_string # elements: 1 # length: 195 ClusterCriterion: property CriterionValues Criterion values A numeric vector containing the values generated by the evaluation criterion for each clustering solution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 16 Criterion values # name: # type: sq_string # elements: 1 # length: 37 CalinskiHarabaszEvaluation.InspectedK # name: # type: sq_string # elements: 1 # length: 171 ClusterCriterion: property InspectedK List of the number of clusters A numeric vector containing the list of the number of clusters evaluated. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 List of the number of clusters # name: # type: sq_string # elements: 1 # length: 34 CalinskiHarabaszEvaluation.Missing # name: # type: sq_string # elements: 1 # length: 174 ClusterCriterion: property Missing Missing values A logical vector indicating which observations in the data matrix contain missing values (NaN). This property is read-only. # name: # type: sq_string # elements: 1 # length: 14 Missing values # name: # type: sq_string # elements: 1 # length: 42 CalinskiHarabaszEvaluation.NumObservations # name: # type: sq_string # elements: 1 # length: 175 ClusterCriterion: property NumObservations Number of observations An integer specifying the number of non-missing observations in the data matrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 35 CalinskiHarabaszEvaluation.OptimalK # name: # type: sq_string # elements: 1 # length: 179 ClusterCriterion: property OptimalK Optimal number of clusters An integer specifying the optimal number of clusters based on the evaluation criterion. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Optimal number of clusters # name: # type: sq_string # elements: 1 # length: 35 CalinskiHarabaszEvaluation.OptimalY # name: # type: sq_string # elements: 1 # length: 198 ClusterCriterion: property OptimalY Optimal clustering solution A numeric vector representing the clustering solution that corresponds to the optimal number of clusters. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Optimal clustering solution # name: # type: sq_string # elements: 1 # length: 28 CalinskiHarabaszEvaluation.X # name: # type: sq_string # elements: 1 # length: 140 ClusterCriterion: property X Data used for clustering A numeric matrix containing the data used for clustering. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Data used for clustering # name: # type: sq_string # elements: 1 # length: 31 CalinskiHarabaszEvaluation.addK # name: # type: sq_string # elements: 1 # length: 296 CalinskiHarabaszEvaluation: obj = addK (obj, K) Add new cluster sizes for evaluation. addK (obj, K) evaluates clustering solutions for the number of clusters specified in the vector K and adds them to the CalinskiHarabaszEvaluation object obj. See also: CalinskiHarabaszEvaluation, evalclusters # name: # type: sq_string # elements: 1 # length: 37 Add new cluster sizes for evaluation. # name: # type: sq_string # elements: 1 # length: 31 CalinskiHarabaszEvaluation.plot # name: # type: sq_string # elements: 1 # length: 387 CalinskiHarabaszEvaluation: plot (obj) CalinskiHarabaszEvaluation: h = plot (obj) Plot the clustering evaluation results. plot (obj) plots the Calinski-Harabasz criterion values against the number of clusters. The optimal number of clusters is marked with an asterisk. h = plot (obj) additionally returns the handle to the plot axes. See also: CalinskiHarabaszEvaluation, evalclusters # name: # type: sq_string # elements: 1 # length: 39 Plot the clustering evaluation results. # name: # type: sq_string # elements: 1 # length: 16 ClusterCriterion # name: # type: sq_string # elements: 1 # length: 340 statistics: ClusterCriterion A clustering evaluation object. The ClusterCriterion is a superclass for clustering evaluation objects, which are created by the evalclusters function. It is not meant to be instantiated directly. See also: evalclusters, CalinskiHarabaszEvaluation, DaviesBouldinEvaluation, GapEvaluation, SilhouetteEvaluation # name: # type: sq_string # elements: 1 # length: 31 A clustering evaluation object. # name: # type: sq_string # elements: 1 # length: 33 ClusterCriterion.ClusterCriterion # name: # type: sq_string # elements: 1 # length: 222 ClusterCriterion: obj = ClusterCriterion (x, clust, KList) Create a ClusterCriterion object. ClusterCriterion is a superclass and is not meant to be instantiated directly. Use evalclusters instead. See also: evalclusters # name: # type: sq_string # elements: 1 # length: 33 Create a ClusterCriterion object. # name: # type: sq_string # elements: 1 # length: 35 ClusterCriterion.ClusteringFunction # name: # type: sq_string # elements: 1 # length: 289 ClusterCriterion: property ClusteringFunction Clustering algorithm A character vector or a function handle specifying the clustering algorithm used to generate the clustering solutions. It can be empty if the clustering solutions are passed as an input matrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Clustering algorithm # name: # type: sq_string # elements: 1 # length: 30 ClusterCriterion.CriterionName # name: # type: sq_string # elements: 1 # length: 201 ClusterCriterion: property CriterionName Name of the evaluation criterion A character vector specifying the name of the criterion used to evaluate the clustering solutions. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Name of the evaluation criterion # name: # type: sq_string # elements: 1 # length: 32 ClusterCriterion.CriterionValues # name: # type: sq_string # elements: 1 # length: 195 ClusterCriterion: property CriterionValues Criterion values A numeric vector containing the values generated by the evaluation criterion for each clustering solution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 16 Criterion values # name: # type: sq_string # elements: 1 # length: 27 ClusterCriterion.InspectedK # name: # type: sq_string # elements: 1 # length: 171 ClusterCriterion: property InspectedK List of the number of clusters A numeric vector containing the list of the number of clusters evaluated. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 List of the number of clusters # name: # type: sq_string # elements: 1 # length: 24 ClusterCriterion.Missing # name: # type: sq_string # elements: 1 # length: 174 ClusterCriterion: property Missing Missing values A logical vector indicating which observations in the data matrix contain missing values (NaN). This property is read-only. # name: # type: sq_string # elements: 1 # length: 14 Missing values # name: # type: sq_string # elements: 1 # length: 32 ClusterCriterion.NumObservations # name: # type: sq_string # elements: 1 # length: 175 ClusterCriterion: property NumObservations Number of observations An integer specifying the number of non-missing observations in the data matrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 25 ClusterCriterion.OptimalK # name: # type: sq_string # elements: 1 # length: 179 ClusterCriterion: property OptimalK Optimal number of clusters An integer specifying the optimal number of clusters based on the evaluation criterion. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Optimal number of clusters # name: # type: sq_string # elements: 1 # length: 25 ClusterCriterion.OptimalY # name: # type: sq_string # elements: 1 # length: 198 ClusterCriterion: property OptimalY Optimal clustering solution A numeric vector representing the clustering solution that corresponds to the optimal number of clusters. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Optimal clustering solution # name: # type: sq_string # elements: 1 # length: 18 ClusterCriterion.X # name: # type: sq_string # elements: 1 # length: 140 ClusterCriterion: property X Data used for clustering A numeric matrix containing the data used for clustering. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Data used for clustering # name: # type: sq_string # elements: 1 # length: 21 ClusterCriterion.addK # name: # type: sq_string # elements: 1 # length: 161 ClusterCriterion: obj = addK (obj, k) Add a new list of cluster numbers to evaluate. addK adds a new list of cluster numbers, k, to the ClusterCriterion object. # name: # type: sq_string # elements: 1 # length: 46 Add a new list of cluster numbers to evaluate. # name: # type: sq_string # elements: 1 # length: 24 ClusterCriterion.compact # name: # type: sq_string # elements: 1 # length: 629 ClusterCriterion: obj = compact (obj) Create a compact clustering evaluation object. obj = compact (obj) returns an object of the same class holding the results of the evaluation but none of the data it was computed from, which is useful when the evaluation is kept and the sample is large. X, Missing and OptimalY are emptied; InspectedK, CriterionValues, OptimalK, NumObservations, CriterionName and ClusteringFunction are kept. The object compacted from is not changed. A compacted object can still be displayed and plotted, and compacting one again does nothing, but addK raises: there are no observations left to cluster. # name: # type: sq_string # elements: 1 # length: 46 Create a compact clustering evaluation object. # name: # type: sq_string # elements: 1 # length: 21 ClusterCriterion.plot # name: # type: sq_string # elements: 1 # length: 275 ClusterCriterion: h = plot (obj) Plot the clustering evaluation values. plot generates a plot of the criterion values against the number of clusters. The optimal number of clusters is marked with an asterisk. The optional return value, h, is a graphics handle to the plot. # name: # type: sq_string # elements: 1 # length: 38 Plot the clustering evaluation values. # name: # type: sq_string # elements: 1 # length: 23 DaviesBouldinEvaluation # name: # type: sq_string # elements: 1 # length: 636 statistics: DaviesBouldinEvaluation Davies-Bouldin object to evaluate clustering solutions A DaviesBouldinEvaluation object is a ClusterCriterion object used to evaluate clustering solutions using the Davies-Bouldin criterion. The Davies-Bouldin criterion is based on the ratio between the distances between clusters and within clusters, that is between centroids and between each datapoint and its centroid. The best solution according to the Davies-Bouldin criterion is the one that produces the lowest Davies-Bouldin value. See also: evalclusters, ClusterCriterion, CalinskiHarabaszEvaluation, GapEvaluation, SilhouetteEvaluation # name: # type: sq_string # elements: 1 # length: 54 Davies-Bouldin object to evaluate clustering solutions # name: # type: sq_string # elements: 1 # length: 42 DaviesBouldinEvaluation.ClusteringFunction # name: # type: sq_string # elements: 1 # length: 289 ClusterCriterion: property ClusteringFunction Clustering algorithm A character vector or a function handle specifying the clustering algorithm used to generate the clustering solutions. It can be empty if the clustering solutions are passed as an input matrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Clustering algorithm # name: # type: sq_string # elements: 1 # length: 37 DaviesBouldinEvaluation.CriterionName # name: # type: sq_string # elements: 1 # length: 201 ClusterCriterion: property CriterionName Name of the evaluation criterion A character vector specifying the name of the criterion used to evaluate the clustering solutions. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Name of the evaluation criterion # name: # type: sq_string # elements: 1 # length: 39 DaviesBouldinEvaluation.CriterionValues # name: # type: sq_string # elements: 1 # length: 195 ClusterCriterion: property CriterionValues Criterion values A numeric vector containing the values generated by the evaluation criterion for each clustering solution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 16 Criterion values # name: # type: sq_string # elements: 1 # length: 47 DaviesBouldinEvaluation.DaviesBouldinEvaluation # name: # type: sq_string # elements: 1 # length: 959 DaviesBouldinEvaluation: obj = DaviesBouldinEvaluation (x, clust, KList) Construct a DaviesBouldinEvaluation object to evaluate clustering solutions with the Davies-Bouldin criterion. obj = DaviesBouldinEvaluation (x, clust, KList) clusters the data in x for every cluster count in KList and evaluates each solution. The evaluation runs at construction, so obj arrives with its CriterionValues and OptimalK already set. x is an NxP numeric matrix of observations (rows) and predictors (columns). A row holding a NaN is left out of NumObservations. clust names the clustering method, one of 'kmeans', 'linkage' and 'gmdistribution'; or a function handle that clusters the data; or an NxM numeric matrix of clustering solutions computed elsewhere, one column per cluster count, in which case ClusteringFunction is left empty. KList is a vector of positive integers, the cluster counts to inspect. evalclusters is the usual way to create one of these objects. # name: # type: sq_string # elements: 1 # length: 110 Construct a DaviesBouldinEvaluation object to evaluate clustering solutions with the Davies-Bouldin criterion. # name: # type: sq_string # elements: 1 # length: 34 DaviesBouldinEvaluation.InspectedK # name: # type: sq_string # elements: 1 # length: 171 ClusterCriterion: property InspectedK List of the number of clusters A numeric vector containing the list of the number of clusters evaluated. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 List of the number of clusters # name: # type: sq_string # elements: 1 # length: 31 DaviesBouldinEvaluation.Missing # name: # type: sq_string # elements: 1 # length: 174 ClusterCriterion: property Missing Missing values A logical vector indicating which observations in the data matrix contain missing values (NaN). This property is read-only. # name: # type: sq_string # elements: 1 # length: 14 Missing values # name: # type: sq_string # elements: 1 # length: 39 DaviesBouldinEvaluation.NumObservations # name: # type: sq_string # elements: 1 # length: 175 ClusterCriterion: property NumObservations Number of observations An integer specifying the number of non-missing observations in the data matrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 32 DaviesBouldinEvaluation.OptimalK # name: # type: sq_string # elements: 1 # length: 179 ClusterCriterion: property OptimalK Optimal number of clusters An integer specifying the optimal number of clusters based on the evaluation criterion. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Optimal number of clusters # name: # type: sq_string # elements: 1 # length: 32 DaviesBouldinEvaluation.OptimalY # name: # type: sq_string # elements: 1 # length: 198 ClusterCriterion: property OptimalY Optimal clustering solution A numeric vector representing the clustering solution that corresponds to the optimal number of clusters. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Optimal clustering solution # name: # type: sq_string # elements: 1 # length: 25 DaviesBouldinEvaluation.X # name: # type: sq_string # elements: 1 # length: 140 ClusterCriterion: property X Data used for clustering A numeric matrix containing the data used for clustering. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Data used for clustering # name: # type: sq_string # elements: 1 # length: 28 DaviesBouldinEvaluation.addK # name: # type: sq_string # elements: 1 # length: 118 DaviesBouldinEvaluation: obj = addK (obj, K) Add new cluster numbers to inspect in the DaviesBouldinEvaluation object. # name: # type: sq_string # elements: 1 # length: 73 Add new cluster numbers to inspect in the DaviesBouldinEvaluation object. # name: # type: sq_string # elements: 1 # length: 28 DaviesBouldinEvaluation.plot # name: # type: sq_string # elements: 1 # length: 268 DaviesBouldinEvaluation: plot (obj) DaviesBouldinEvaluation: h = plot (obj) Plot Davies-Bouldin evaluation results. Plot the CriterionValues against InspectedK from the DaviesBouldinEvaluation ClusterCriterion to the current plot. Returns an axes handle if requested. # name: # type: sq_string # elements: 1 # length: 39 Plot Davies-Bouldin evaluation results. # name: # type: sq_string # elements: 1 # length: 13 GapEvaluation # name: # type: sq_string # elements: 1 # length: 642 statistics: GapEvaluation Gap evaluation for clustering solutions The GapEvaluation class implements the gap statistic criterion for evaluating clustering solutions. A GapEvaluation object is a specialization of ClusterCriterion and contains fields and methods to compute the gap statistic, its Monte-Carlo reference expectations, and to select the optimal number of clusters according to a chosen search method. Create a GapEvaluation object by using the evalclusters function or by calling the class constructor directly. See also: evalclusters, ClusterCriterion, CalinskiHarabaszEvaluation, DaviesBouldinEvaluation, SilhouetteEvaluation # name: # type: sq_string # elements: 1 # length: 39 Gap evaluation for clustering solutions # name: # type: sq_string # elements: 1 # length: 15 GapEvaluation.B # name: # type: sq_string # elements: 1 # length: 237 GapEvaluation: property B Number of reference datasets A positive integer specifying how many reference datasets are generated to compute the expected log within-cluster dispersion via Monte-Carlo simulation. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Number of reference datasets # name: # type: sq_string # elements: 1 # length: 32 GapEvaluation.ClusteringFunction # name: # type: sq_string # elements: 1 # length: 289 ClusterCriterion: property ClusteringFunction Clustering algorithm A character vector or a function handle specifying the clustering algorithm used to generate the clustering solutions. It can be empty if the clustering solutions are passed as an input matrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Clustering algorithm # name: # type: sq_string # elements: 1 # length: 27 GapEvaluation.CriterionName # name: # type: sq_string # elements: 1 # length: 201 ClusterCriterion: property CriterionName Name of the evaluation criterion A character vector specifying the name of the criterion used to evaluate the clustering solutions. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Name of the evaluation criterion # name: # type: sq_string # elements: 1 # length: 29 GapEvaluation.CriterionValues # name: # type: sq_string # elements: 1 # length: 195 ClusterCriterion: property CriterionValues Criterion values A numeric vector containing the values generated by the evaluation criterion for each clustering solution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 16 Criterion values # name: # type: sq_string # elements: 1 # length: 22 GapEvaluation.Distance # name: # type: sq_string # elements: 1 # length: 287 GapEvaluation: property Distance Distance metric A character vector or function handle specifying the distance measure passed to clustering routines (as accepted by pdist). When a numeric vector is supplied it is interpreted as a precomputed distance vector. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Distance metric # name: # type: sq_string # elements: 1 # length: 26 GapEvaluation.ExpectedLogW # name: # type: sq_string # elements: 1 # length: 290 GapEvaluation: property ExpectedLogW Expected log within-cluster dispersion A numeric vector containing the Monte-Carlo estimate of the expected values for the natural logarithm of the within-cluster dispersion, computed across the generated reference datasets. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Expected log within-cluster dispersion # name: # type: sq_string # elements: 1 # length: 27 GapEvaluation.GapEvaluation # name: # type: sq_string # elements: 1 # length: 879 GapEvaluation: obj = GapEvaluation (x, clust, KList) GapEvaluation: obj = GapEvaluation (x, clust, KList, B) GapEvaluation: obj = GapEvaluation (..., name, value) Construct a GapEvaluation object to evaluate clustering solutions using the gap statistic. obj = GapEvaluation (x, clust, KList) returns a GapEvaluation object configured to evaluate the clustering function specified by clust on the data matrix x for the list of cluster counts in KList. Optional inputs: B - Number of reference datasets to generate (default 100). 'Distance' - Distance metric name or function handle as accepted by pdist (default 'sqeuclidean'). 'ReferenceDistribution' - Reference distribution to use (default 'pca'; 'uniform' is supported). 'SearchMethod' - Method to select the optimal K; one of 'globalMaxSE' or 'firstMaxSE' (default 'globalMaxSE'). See also: evalclusters, ClusterCriterion # name: # type: sq_string # elements: 1 # length: 90 Construct a GapEvaluation object to evaluate clustering solutions using the gap statistic. # name: # type: sq_string # elements: 1 # length: 24 GapEvaluation.InspectedK # name: # type: sq_string # elements: 1 # length: 171 ClusterCriterion: property InspectedK List of the number of clusters A numeric vector containing the list of the number of clusters evaluated. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 List of the number of clusters # name: # type: sq_string # elements: 1 # length: 18 GapEvaluation.LogW # name: # type: sq_string # elements: 1 # length: 231 GapEvaluation: property LogW Observed log within-cluster dispersion A numeric vector containing the observed values of the natural logarithm of the within-cluster dispersion computed on the actual data. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Observed log within-cluster dispersion # name: # type: sq_string # elements: 1 # length: 21 GapEvaluation.Missing # name: # type: sq_string # elements: 1 # length: 174 ClusterCriterion: property Missing Missing values A logical vector indicating which observations in the data matrix contain missing values (NaN). This property is read-only. # name: # type: sq_string # elements: 1 # length: 14 Missing values # name: # type: sq_string # elements: 1 # length: 29 GapEvaluation.NumObservations # name: # type: sq_string # elements: 1 # length: 175 ClusterCriterion: property NumObservations Number of observations An integer specifying the number of non-missing observations in the data matrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 22 GapEvaluation.OptimalK # name: # type: sq_string # elements: 1 # length: 179 ClusterCriterion: property OptimalK Optimal number of clusters An integer specifying the optimal number of clusters based on the evaluation criterion. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Optimal number of clusters # name: # type: sq_string # elements: 1 # length: 22 GapEvaluation.OptimalY # name: # type: sq_string # elements: 1 # length: 198 ClusterCriterion: property OptimalY Optimal clustering solution A numeric vector representing the clustering solution that corresponds to the optimal number of clusters. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Optimal clustering solution # name: # type: sq_string # elements: 1 # length: 35 GapEvaluation.ReferenceDistribution # name: # type: sq_string # elements: 1 # length: 249 GapEvaluation: property ReferenceDistribution Reference distribution for Monte-Carlo A character vector naming the reference distribution used to generate reference datasets. Supported values include 'pca' and 'uniform'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Reference distribution for Monte-Carlo # name: # type: sq_string # elements: 1 # length: 16 GapEvaluation.SE # name: # type: sq_string # elements: 1 # length: 217 GapEvaluation: property SE Standard error of expected logW A numeric vector containing the standard error of the expected values for the natural logarithm of the within-cluster dispersion. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Standard error of expected logW # name: # type: sq_string # elements: 1 # length: 26 GapEvaluation.SearchMethod # name: # type: sq_string # elements: 1 # length: 267 GapEvaluation: property SearchMethod Search method to select optimal K A character vector specifying the method used to select the optimal number of clusters from the gap statistic. Supported values include 'globalMaxSE' and 'firstMaxSE'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Search method to select optimal K # name: # type: sq_string # elements: 1 # length: 21 GapEvaluation.StdLogW # name: # type: sq_string # elements: 1 # length: 214 GapEvaluation: property StdLogW Standard deviation of expected logW A numeric vector containing the standard deviation of the Monte-Carlo estimates of the log within-cluster dispersion. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Standard deviation of expected logW # name: # type: sq_string # elements: 1 # length: 15 GapEvaluation.X # name: # type: sq_string # elements: 1 # length: 140 ClusterCriterion: property X Data used for clustering A numeric matrix containing the data used for clustering. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Data used for clustering # name: # type: sq_string # elements: 1 # length: 18 GapEvaluation.addK # name: # type: sq_string # elements: 1 # length: 231 GapEvaluation: obj = addK (obj, K) Add new K values to inspect Add a new cluster array to inspect in the GapEvaluation object. This updates internal storage for Monte-Carlo results and evaluates the newly requested cluster counts. # name: # type: sq_string # elements: 1 # length: 27 Add new K values to inspect # name: # type: sq_string # elements: 1 # length: 18 GapEvaluation.plot # name: # type: sq_string # elements: 1 # length: 276 GapEvaluation: plot (obj) GapEvaluation: h = plot (obj) Plot Gap evaluation results Plot the gap statistic (criterion values) versus the inspected numbers of clusters and display error bars representing the Monte-Carlo standard deviations. Optionally returns the axes handle. # name: # type: sq_string # elements: 1 # length: 27 Plot Gap evaluation results # name: # type: sq_string # elements: 1 # length: 20 SilhouetteEvaluation # name: # type: sq_string # elements: 1 # length: 1259 statistics: SilhouetteEvaluation Silhouette evaluation for clustering The SilhouetteEvaluation class implements an object to evaluate clustering solutions using the silhouette criterion. A SilhouetteEvaluation object is a ClusterCriterion object that computes silhouette values for clustering solutions and selects the best number of clusters as the one with the highest average silhouette value. Create a SilhouetteEvaluation object by using the evalclusters function or the class constructor. List of public properties specific to SilhouetteEvaluation: Distance A valid distance metric name (string), a function handle, or a numeric vector as returned by pdist. This specifies how pairwise distances are computed. ClusterPriors A character vector specifying how to evaluate silhouette values across clusters: 'empirical' (default) uses empirical cluster priors, or 'equal' treats clusters equally. ClusterSilhouettes A cell array containing silhouette values for each observation for each inspected cluster number. The best clustering solution according to the silhouette criterion is the one that yields the highest average silhouette value. See also: evalclusters, ClusterCriterion, CalinskiHarabaszEvaluation, DaviesBouldinEvaluation, GapEvaluation # name: # type: sq_string # elements: 1 # length: 36 Silhouette evaluation for clustering # name: # type: sq_string # elements: 1 # length: 34 SilhouetteEvaluation.ClusterPriors # name: # type: sq_string # elements: 1 # length: 212 SilhouetteEvaluation: property ClusterPriors Cluster prior handling Specifies how cluster-level silhouette aggregation is computed. Valid values are 'empirical' (default) and 'equal'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Cluster prior handling # name: # type: sq_string # elements: 1 # length: 39 SilhouetteEvaluation.ClusterSilhouettes # name: # type: sq_string # elements: 1 # length: 283 SilhouetteEvaluation: property ClusterSilhouettes Silhouette values A cell array where each element holds the mean silhouette value of each cluster of a given clustering (corresponding to an inspected K), so element i is a vector of InspectedK(i) values. This property is read-only. # name: # type: sq_string # elements: 1 # length: 17 Silhouette values # name: # type: sq_string # elements: 1 # length: 39 SilhouetteEvaluation.ClusteringFunction # name: # type: sq_string # elements: 1 # length: 289 ClusterCriterion: property ClusteringFunction Clustering algorithm A character vector or a function handle specifying the clustering algorithm used to generate the clustering solutions. It can be empty if the clustering solutions are passed as an input matrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Clustering algorithm # name: # type: sq_string # elements: 1 # length: 34 SilhouetteEvaluation.CriterionName # name: # type: sq_string # elements: 1 # length: 201 ClusterCriterion: property CriterionName Name of the evaluation criterion A character vector specifying the name of the criterion used to evaluate the clustering solutions. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Name of the evaluation criterion # name: # type: sq_string # elements: 1 # length: 36 SilhouetteEvaluation.CriterionValues # name: # type: sq_string # elements: 1 # length: 195 ClusterCriterion: property CriterionValues Criterion values A numeric vector containing the values generated by the evaluation criterion for each clustering solution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 16 Criterion values # name: # type: sq_string # elements: 1 # length: 29 SilhouetteEvaluation.Distance # name: # type: sq_string # elements: 1 # length: 205 SilhouetteEvaluation: property Distance Distance measure A string naming a distance metric, a function handle that computes distances, or a numeric vector as produced by pdist. This property is read-only. # name: # type: sq_string # elements: 1 # length: 16 Distance measure # name: # type: sq_string # elements: 1 # length: 31 SilhouetteEvaluation.InspectedK # name: # type: sq_string # elements: 1 # length: 171 ClusterCriterion: property InspectedK List of the number of clusters A numeric vector containing the list of the number of clusters evaluated. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 List of the number of clusters # name: # type: sq_string # elements: 1 # length: 28 SilhouetteEvaluation.Missing # name: # type: sq_string # elements: 1 # length: 174 ClusterCriterion: property Missing Missing values A logical vector indicating which observations in the data matrix contain missing values (NaN). This property is read-only. # name: # type: sq_string # elements: 1 # length: 14 Missing values # name: # type: sq_string # elements: 1 # length: 36 SilhouetteEvaluation.NumObservations # name: # type: sq_string # elements: 1 # length: 175 ClusterCriterion: property NumObservations Number of observations An integer specifying the number of non-missing observations in the data matrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 29 SilhouetteEvaluation.OptimalK # name: # type: sq_string # elements: 1 # length: 179 ClusterCriterion: property OptimalK Optimal number of clusters An integer specifying the optimal number of clusters based on the evaluation criterion. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Optimal number of clusters # name: # type: sq_string # elements: 1 # length: 29 SilhouetteEvaluation.OptimalY # name: # type: sq_string # elements: 1 # length: 198 ClusterCriterion: property OptimalY Optimal clustering solution A numeric vector representing the clustering solution that corresponds to the optimal number of clusters. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Optimal clustering solution # name: # type: sq_string # elements: 1 # length: 41 SilhouetteEvaluation.SilhouetteEvaluation # name: # type: sq_string # elements: 1 # length: 807 SilhouetteEvaluation: obj = SilhouetteEvaluation (x, clust, KList) SilhouetteEvaluation: obj = SilhouetteEvaluation (..., Name, Value) Create a SilhouetteEvaluation object to evaluate clustering solutions for data x using clustering method clust over the list of cluster numbers KList. x is an NxP numeric matrix of observations (rows) and predictors (columns). clust is a string naming the clustering method (for example 'kmeans', 'linkage', or a custom function handle). KList is a vector of positive integers specifying the cluster numbers to inspect. Optional name-value pairs: Name Value 'Distance' Distance metric name, function handle, or numeric pdist vector. Default: 'sqeuclidean'. 'ClusterPriors' Either 'empirical' (default) or 'equal'. See also: silhouette, evalclusters, ClusterCriterion # name: # type: sq_string # elements: 1 # length: 150 Create a SilhouetteEvaluation object to evaluate clustering solutions for data x using clustering method clust over the list of cluster numbers KList. # name: # type: sq_string # elements: 1 # length: 22 SilhouetteEvaluation.X # name: # type: sq_string # elements: 1 # length: 140 ClusterCriterion: property X Data used for clustering A numeric matrix containing the data used for clustering. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Data used for clustering # name: # type: sq_string # elements: 1 # length: 25 SilhouetteEvaluation.addK # name: # type: sq_string # elements: 1 # length: 144 SilhouetteEvaluation: obj = addK (obj, K) Add a new cluster number to inspect and re-evaluate silhouette values for newly added cluster numbers. # name: # type: sq_string # elements: 1 # length: 102 Add a new cluster number to inspect and re-evaluate silhouette values for newly added cluster numbers. # name: # type: sq_string # elements: 1 # length: 25 SilhouetteEvaluation.plot # name: # type: sq_string # elements: 1 # length: 272 SilhouetteEvaluation: plot (obj) SilhouetteEvaluation: h = plot (obj) Plot the silhouette evaluation results. Plot the criterion values (average silhouette) against inspected cluster numbers (InspectedK) for the given obj. Optionally returns the axis handle for the plot. # name: # type: sq_string # elements: 1 # length: 39 Plot the silhouette evaluation results. # name: # type: sq_string # elements: 1 # length: 7 cluster # name: # type: sq_string # elements: 1 # length: 1094 statistics: T = cluster (Z, "Cutoff", C) statistics: T = cluster (Z, "Cutoff", C, "Depth", D) statistics: T = cluster (Z, "Cutoff", C, "Criterion", criterion) statistics: T = cluster (Z, "MaxClust", N) Define clusters from an agglomerative hierarchical cluster tree. Given a hierarchical cluster tree Z generated by the linkage function, cluster defines clusters, using a threshold value C to identify new clusters ('Cutoff') or according to a maximum number of desired clusters N ('MaxClust'). criterion is used to choose the criterion for defining clusters, which can be either "inconsistent" (default) or "distance". When using "inconsistent", cluster compares the threshold value C to the inconsistency coefficient of each link; when using "distance", cluster compares the threshold value C to the height of each link. D is the depth used to evaluate the inconsistency coefficient, its default value is 2. cluster uses "distance" as a criterion for defining new clusters when it is used with the 'MaxClust' method. See also: clusterdata, dendrogram, inconsistent, kmeans, linkage, pdist # name: # type: sq_string # elements: 1 # length: 64 Define clusters from an agglomerative hierarchical cluster tree. # name: # type: sq_string # elements: 1 # length: 11 clusterdata # name: # type: sq_string # elements: 1 # length: 741 statistics: T = clusterdata (X, cutoff) statistics: T = clusterdata (X, Name, Value) Wrapper function for linkage and cluster. If cutoff is used, then clusterdata calls linkage and cluster with default value, using cutoff as a threshold value for cluster. If cutoff is an integer and greater or equal to 2, then cutoff is interpreted as the maximum number of cluster desired and the "MaxClust" option is used for cluster. If cutoff is not used, then clusterdata expects a list of pair arguments. Then you must specify either the "Cutoff" or "MaxClust" option for cluster. The method and metric used by linkage, are defined through the "linkage" and "distance" arguments. See also: cluster, dendrogram, inconsistent, kmeans, linkage, pdist # name: # type: sq_string # elements: 1 # length: 41 Wrapper function for linkage and cluster. # name: # type: sq_string # elements: 1 # length: 8 cophenet # name: # type: sq_string # elements: 1 # length: 1024 statistics: [c, d] = cophenet (Z, y) Compute the cophenetic correlation coefficient. The cophenetic correlation coefficient C of a hierarchical cluster tree Z is the linear correlation coefficient between the cophenetic distances d and the euclidean distances y. $$ c = \frac {\sum_{i It is a measure of the similarity between the distance of the leaves, as seen in the tree, and the distance of the original data points, which were used to build the tree. When this similarity is greater, that is the coefficient is closer to 1, the tree renders an accurate representation of the distances between the original data points. Z is a hierarchical cluster tree, as the output of linkage. y is a vector of euclidean distances, as the output of pdist. The optional output d is a vector of cophenetic distances, in the same lower triangular format as y. The cophenetic distance between two data points is the height of the lowest common node of the tree. See also: cluster, dendrogram, inconsistent, linkage, pdist, squareform # name: # type: sq_string # elements: 1 # length: 47 Compute the cophenetic correlation coefficient. # name: # type: sq_string # elements: 1 # length: 6 dbscan # name: # type: sq_string # elements: 1 # length: 2857 statistics: idx = dbscan (X, epsilon, minpts) statistics: idx = dbscan (D, epsilon, minpts, 'Distance', 'precomputed') statistics: idx = dbscan (..., name, value) statistics: [idx, corepts] = dbscan (...) Density-Based Spatial Clustering of Applications with Noise (DBSCAN). idx = dbscan (X, epsilon, minpts) partitions the observations in the NxP numeric matrix X into clusters using the DBSCAN algorithm with neighborhood radius epsilon and minimum number of neighbors minpts. Rows of X correspond to observations and columns correspond to features or variables. epsilon must be a nonnegative scalar and minpts a positive integer scalar. idx is an Nx1 vector of cluster indices, numbered 1 to the number of clusters found; observations flagged as noise are assigned the value -1. A point is a core point when at least minpts observations (including the point itself) lie within distance epsilon of it. Clusters grow from core points to every observation that is density-reachable from them; a non-core observation that lies within epsilon of a core point becomes a border point and joins that point's cluster, while an observation that is neither core nor within reach of a core point is labelled noise. A border point that is reachable from more than one cluster is assigned to the first cluster that reaches it, following the order of the observations in X. idx = dbscan (D, epsilon, minpts, 'Distance', 'precomputed') treats the NxN matrix D as a precomputed matrix of pairwise distances between observations, such as the output of pdist2; D(i,j) is the distance between observations i and j. [idx, corepts] = dbscan (...) also returns an Nx1 logical vector corepts that is true for each observation that is a core point. Additional parameters can be specified by Name-Value pair arguments. Name Value 'Distance' is the distance metric used to find neighbors, specified as one of the metrics accepted by rangesearch ('euclidean' by default, and also 'seuclidean', 'cityblock', 'chebychev', 'minkowski', 'mahalanobis', 'cosine', 'correlation', 'spearman', 'hamming', 'jaccard', or a custom distance function handle), or the string 'precomputed' to interpret the first input as a matrix of pairwise distances. 'P' is the Minkowski distance exponent, a positive scalar. This argument is only valid when the selected distance metric is 'minkowski'. By default it is 2. 'Scale' is the scale parameter for the standardized Euclidean distance, a nonnegative numeric vector of length equal to the number of columns in X. This argument is only valid when the selected distance metric is 'seuclidean'. 'Cov' is the covariance matrix for the mahalanobis distance, a positive definite matrix matching the number of columns in X. This argument is only valid when the selected distance metric is 'mahalanobis'. See also: kmeans, rangesearch, pdist2, knnsearch # name: # type: sq_string # elements: 1 # length: 69 Density-Based Spatial Clustering of Applications with Noise (DBSCAN). # name: # type: sq_string # elements: 1 # length: 12 evalclusters # name: # type: sq_string # elements: 1 # length: 3745 statistics: eva = evalclusters (x, clust, criterion) statistics: eva = evalclusters (..., Name, Value) Create a clustering evaluation object to find the optimal number of clusters. evalclusters creates a clustering evaluation object to evaluate the optimal number of clusters for data x, using criterion criterion. The input data x is a matrix with n observations of p variables. The evaluation criterion criterion is one of the following: CalinskiHarabasz to create a CalinskiHarabaszEvaluation object. DaviesBouldin to create a DaviesBouldinEvaluation object. gap to create a GapEvaluation object. silhouette to create a SilhouetteEvaluation object. The clustering algorithm clust is one of the following: kmeans to cluster the data using kmeans with EmptyAction set to singleton and Replicates set to 5. linkage to cluster the data using clusterdata with linkage set to Ward. gmdistribution to cluster the data using fitgmdist with SharedCov set to true and Replicates set to 5. If the criterion is CalinskiHarabasz, DaviesBouldin, or silhouette, clust can also be a function handle to a function of the form c = clust(x, k), where x is the input data, k the number of clusters to evaluate and c the clustering result. The clustering result can be either an array of size n with k different integer values, or a matrix of size n by k with a likelihood value assigned to each one of the n observations for each one of the k clusters. In the latter case, each observation is assigned to the cluster with the higher value. If the criterion is CalinskiHarabasz, DaviesBouldin, or silhouette, clust can also be a matrix of size n by k, where k is the number of proposed clustering solutions, so that each column of clust is a clustering solution. In addition to the obligatory x, clust and criterion inputs there is a number of optional arguments, specified as pairs of Name and Value options. The known Name arguments are: KList a vector of positive integer numbers, that is the cluster sizes to evaluate. This option is necessary, unless clust is a matrix of proposed clustering solutions. Distance a distance metric as accepted by the chosen clust. It can be the name of the distance metric as a string or a function handle. When criterion is silhouette, it can be a vector as created by function pdist. Valid distance metric strings are: sqEuclidean (default), Euclidean, cityblock, cosine, correlation, Hamming, Jaccard. Only used by silhouette and gap evaluation. ClusterPriors the prior probabilities of each cluster, which can be either empirical (default), or equal. When empirical the silhouette value is the average of the silhouette values of all points; when equal the silhouette value is the average of the average silhouette value of each cluster. Only used by silhouette evaluation. B the number of reference datasets generated from the reference distribution. Only used by gap evaluation. ReferenceDistribution the reference distribution used to create the reference data. It can be PCA (default) for a distribution based on the principal components of X, or uniform for a uniform distribution based on the range of the observed data. PCA is currently not implemented. Only used by gap evaluation. SearchMethod the method for selecting the optimal value with a gap evaluation. It can be either globalMaxSE (default) for selecting the smallest number of clusters which is inside the standard error of the maximum gap value, or firstMaxSE for selecting the first number of clusters which is inside the standard error of the following cluster number. Only used by gap evaluation. Output eva is a clustering evaluation object. See also: CalinskiHarabaszEvaluation, DaviesBouldinEvaluation, GapEvaluation, SilhouetteEvaluation # name: # type: sq_string # elements: 1 # length: 77 Create a clustering evaluation object to find the optimal number of clusters. # name: # type: string # elements: 1 # length: 9 fitgmdist # name: # type: sq_string # elements: 1 # length: 2473 statistics: GMdist = fitgmdist (data, k, param1, value1, ...) Fit a Gaussian mixture model with k components to data. Each row of data is a data sample. Each column is a variable. Optional parameters are: 'start': Initialization conditions. Possible values are: 'randSample' (default) Takes means uniformly from rows of data. 'plus' Use k-means++ to initialize means. 'cluster' Performs an initial clustering with 10% of the data. vector A vector whose length is the number of rows in data, and whose values are 1 to k specify the components each row is initially allocated to. The mean, variance, and weight of each component is calculated from that. structure A structure with fields mu, Sigma and ComponentProportion. For 'randSample', 'plus', and 'cluster', the initial variance of each component is the variance of the entire data sample. 'Replicates': Number of random restarts to perform. 'RegularizationValue' or 'Regularize': A small number added to the diagonal entries of the covariance to prevent singular covariances. 'SharedCovariance' or 'SharedCov' (logical). True if all components must share the same variance, to reduce the number of free parameters 'CovarianceType' or 'CovType' (string). Possible values are: 'full' (default) Allow arbitrary covariance matrices. 'diagonal' Force covariances to be diagonal, to reduce the number of free parameters. 'Options': A structure with all of the following fields: MaxIter Maximum number of EM iterations (default 100). A value of 0 returns the starting point with its likelihood, where MATLAB fails. TolFun Threshold increase in likelihood to terminate EM (default 1e-6). Display Possible values are: 'off' (default): Display nothing. 'final': Display the total number of iterations and likelihood once the execution completes. 'iter': Display the number of iteration and likelihood after each iteration. 'Weight': A column vector or Nx2 matrix. The first column consists of non-negative weights given to the samples. If these are all integers, this is equivalent to specifying weight(i) copies of row i of data, but potentially faster. If a row of data is used to represent samples that are similar but not identical, then the second column of weight indicates the variance of those original samples. Specifically, in the EM algorithm, the contribution of row i towards the variance is set to at least weight(i,2), to prevent spurious components with zero variance. See also: gmdistribution, kmeans # name: # type: sq_string # elements: 1 # length: 55 Fit a Gaussian mixture model with k components to data. # name: # type: sq_string # elements: 1 # length: 14 gmdistribution # name: # type: sq_string # elements: 1 # length: 561 statistics: gmdistribution Gaussian mixture distribution. A gmdistribution object represents a mixture of k multivariate Gaussian components in d variables: their means mu, their covariances Sigma and their mixing proportions ComponentProportion. The methods pdf, cdf and random evaluate and sample the mixture, while cluster, posterior and mahal assign observations to its components. Create a gmdistribution object with the class constructor from known parameters, or with fitgmdist from data, which also records the results of the fit. See also: fitgmdist # name: # type: sq_string # elements: 1 # length: 30 Gaussian mixture distribution. # name: # type: sq_string # elements: 1 # length: 18 gmdistribution.AIC # name: # type: sq_string # elements: 1 # length: 228 gmdistribution: property AIC Akaike information criterion A scalar, twice the negative log-likelihood plus twice the number of estimated parameters. It is empty unless the object came from fitgmdist. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Akaike information criterion # name: # type: sq_string # elements: 1 # length: 18 gmdistribution.BIC # name: # type: sq_string # elements: 1 # length: 268 gmdistribution: property BIC Bayesian information criterion A scalar, twice the negative log-likelihood plus the number of estimated parameters times the log of the number of observations. It is empty unless the object came from fitgmdist. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 Bayesian information criterion # name: # type: sq_string # elements: 1 # length: 34 gmdistribution.ComponentProportion # name: # type: sq_string # elements: 1 # length: 233 gmdistribution: property ComponentProportion Mixing proportions A 1-by-k row vector holding the proportion of each component. The proportions are scaled to sum to 1, and are all equal when none was given. This property is read-only. # name: # type: sq_string # elements: 1 # length: 18 Mixing proportions # name: # type: sq_string # elements: 1 # length: 24 gmdistribution.Converged # name: # type: sq_string # elements: 1 # length: 226 gmdistribution: property Converged Whether the fit converged A logical scalar, true when the fit reached its tolerance within the iteration limit. It is empty unless the object came from fitgmdist. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Whether the fit converged # name: # type: sq_string # elements: 1 # length: 29 gmdistribution.CovarianceType # name: # type: sq_string # elements: 1 # length: 228 gmdistribution: property CovarianceType Form of the component covariances A character vector, 'diagonal' when each covariance was given as a row of variances and 'full' when it was given as a matrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Form of the component covariances # name: # type: sq_string # elements: 1 # length: 31 gmdistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 149 gmdistribution: property DistributionName Name of the distribution The character vector 'gaussian mixture distribution'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Name of the distribution # name: # type: sq_string # elements: 1 # length: 36 gmdistribution.NegativeLogLikelihood # name: # type: sq_string # elements: 1 # length: 244 gmdistribution: property NegativeLogLikelihood Negative log-likelihood at the fit A scalar, the negative of the log-likelihood of the data under the fitted mixture. It is empty unless the object came from fitgmdist. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Negative log-likelihood at the fit # name: # type: sq_string # elements: 1 # length: 28 gmdistribution.NumComponents # name: # type: sq_string # elements: 1 # length: 142 gmdistribution: property NumComponents Number of mixture components A positive integer, the number of rows of mu. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Number of mixture components # name: # type: sq_string # elements: 1 # length: 28 gmdistribution.NumIterations # name: # type: sq_string # elements: 1 # length: 162 gmdistribution: property NumIterations Iterations the fit took A positive integer. It is empty unless the object came from fitgmdist. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Iterations the fit took # name: # type: sq_string # elements: 1 # length: 27 gmdistribution.NumVariables # name: # type: sq_string # elements: 1 # length: 177 gmdistribution: property NumVariables Number of variables A positive integer, the dimension of each component, which is the number of columns of mu. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Number of variables # name: # type: sq_string # elements: 1 # length: 34 gmdistribution.RegularizationValue # name: # type: sq_string # elements: 1 # length: 264 gmdistribution: property RegularizationValue Regularization added to the covariance diagonal A nonnegative scalar added to the diagonal of each covariance to keep it positive definite. It is empty unless the object came from fitgmdist. This property is read-only. # name: # type: sq_string # elements: 1 # length: 47 Regularization added to the covariance diagonal # name: # type: sq_string # elements: 1 # length: 31 gmdistribution.SharedCovariance # name: # type: sq_string # elements: 1 # length: 236 gmdistribution: property SharedCovariance Whether the components share one covariance A logical scalar, true when a single covariance was given for every component and false when one was given per component. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Whether the components share one covariance # name: # type: sq_string # elements: 1 # length: 20 gmdistribution.Sigma # name: # type: sq_string # elements: 1 # length: 324 gmdistribution: property Sigma Component covariances The covariances of the components, in the form they were given in. A full covariance per component is d-by-d-by-k, a diagonal one per component 1-by-d-by-k, a full covariance shared by every component d-by-d, and a shared diagonal one 1-by-d. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Component covariances # name: # type: sq_string # elements: 1 # length: 18 gmdistribution.cdf # name: # type: sq_string # elements: 1 # length: 444 gmdistribution: c = cdf (obj, X) Cumulative distribution function of a Gaussian mixture distribution. X is an n-by-d matrix of points at which the distribution is evaluated, one point per row, where d is the number of variables of obj. c is an n-by-1 vector holding the value of the cumulative distribution function at each of them, the components' cumulative distributions summed with the mixing proportions in ComponentProportion as weights. # name: # type: sq_string # elements: 1 # length: 68 Cumulative distribution function of a Gaussian mixture distribution. # name: # type: sq_string # elements: 1 # length: 22 gmdistribution.cluster # name: # type: sq_string # elements: 1 # length: 870 gmdistribution: idx = cluster (obj, X) gmdistribution: [idx, nlogl] = cluster (obj, X) gmdistribution: [idx, nlogl, P] = cluster (obj, X) gmdistribution: [idx, nlogl, P, logpdf] = cluster (obj, X) gmdistribution: [idx, nlogl, P, logpdf, M] = cluster (obj, X) Assign each observation to a mixture component. X is an n-by-d matrix of observations, one per row, where d is the number of variables of obj. Each is assigned to the component under which it is most probable. idx is an n-by-1 vector of component indices. nlogl is the negative log-likelihood of X under the mixture. P is an n-by-k matrix of posterior probabilities, one column per component, whose rows sum to one. logpdf is an n-by-1 vector holding the logarithm of the mixture density at each observation. M is an n-by-k matrix of squared Mahalanobis distances from each observation to each component mean. # name: # type: sq_string # elements: 1 # length: 47 Assign each observation to a mixture component. # name: # type: sq_string # elements: 1 # length: 18 gmdistribution.fit # name: # type: sq_string # elements: 1 # length: 391 gmdistribution: obj = fit (X, k) gmdistribution: obj = fit (X, k, Name, Value) Fit a Gaussian mixture distribution to data. X is an n-by-d matrix of observations, one per row, and k is the number of components to fit. Any Name-Value pair accepted by fitgmdist may follow, which is the function this method calls and where the options are documented. obj is the fitted gmdistribution object. # name: # type: sq_string # elements: 1 # length: 44 Fit a Gaussian mixture distribution to data. # name: # type: sq_string # elements: 1 # length: 29 gmdistribution.gmdistribution # name: # type: sq_string # elements: 1 # length: 1005 gmdistribution: obj = gmdistribution (mu, Sigma) gmdistribution: obj = gmdistribution (mu, Sigma, p) gmdistribution: obj = gmdistribution (mu, Sigma, p, extra) Create a Gaussian mixture distribution. mu is a k-by-d matrix holding the mean of each of the k components, one per row, where d is the number of variables. Sigma holds the covariances in one of four forms. A full covariance per component is d-by-d-by-k and a diagonal one per component is 1-by-d-by-k, while a single d-by-d matrix or a single 1-by-d row of variances is shared by every component. The form given sets CovarianceType and SharedCovariance. p is a vector of k mixing proportions, scaled to sum to 1. A proportion may not be negative and they may not all be zero. When p is omitted or empty the components are equally weighted. extra carries the results of a fit and is passed by fitgmdist. It fills AIC, BIC, Converged, NegativeLogLikelihood, NumIterations and RegularizationValue, which stay empty for an object built by hand. # name: # type: sq_string # elements: 1 # length: 39 Create a Gaussian mixture distribution. # name: # type: sq_string # elements: 1 # length: 20 gmdistribution.mahal # name: # type: sq_string # elements: 1 # length: 372 gmdistribution: D = mahal (obj, X) Squared Mahalanobis distance to each mixture component. X is an n-by-d matrix of observations, one per row, where d is the number of variables of obj. D is an n-by-k matrix holding the squared Mahalanobis distance from each observation to the mean of each of the k components, measured in the covariance of the component it is taken to. # name: # type: sq_string # elements: 1 # length: 55 Squared Mahalanobis distance to each mixture component. # name: # type: sq_string # elements: 1 # length: 17 gmdistribution.mu # name: # type: sq_string # elements: 1 # length: 183 gmdistribution: property mu Component means A k-by-d matrix holding the mean of each of the k components, one per row, where d is the number of variables. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Component means # name: # type: sq_string # elements: 1 # length: 18 gmdistribution.pdf # name: # type: sq_string # elements: 1 # length: 382 gmdistribution: c = pdf (obj, X) Probability density function of a Gaussian mixture distribution. X is an n-by-d matrix of points at which the density is evaluated, one point per row, where d is the number of variables of obj. c is an n-by-1 vector holding the density at each of them, the components' densities summed with the mixing proportions in ComponentProportion as weights. # name: # type: sq_string # elements: 1 # length: 64 Probability density function of a Gaussian mixture distribution. # name: # type: sq_string # elements: 1 # length: 24 gmdistribution.posterior # name: # type: sq_string # elements: 1 # length: 315 gmdistribution: c = posterior (obj, X) Posterior probability of each mixture component. X is an n-by-d matrix of observations, one per row, where d is the number of variables of obj. c is an n-by-k matrix whose (i,j) element is the probability that observation i was drawn from component j, so its rows sum to one. # name: # type: sq_string # elements: 1 # length: 48 Posterior probability of each mixture component. # name: # type: sq_string # elements: 1 # length: 21 gmdistribution.random # name: # type: sq_string # elements: 1 # length: 359 gmdistribution: c = random (obj) gmdistribution: c = random (obj, n) Random numbers from a Gaussian mixture distribution. n is the number of observations to draw and defaults to 1. c is an n-by-d matrix holding one observation per row, where d is the number of variables of obj. Each row is drawn from a component chosen with probability ComponentProportion. # name: # type: sq_string # elements: 1 # length: 52 Random numbers from a Gaussian mixture distribution. # name: # type: sq_string # elements: 1 # length: 12 inconsistent # name: # type: sq_string # elements: 1 # length: 947 statistics: Y = inconsistent (Z) statistics: Y = inconsistent (Z, d) Compute the inconsistency coefficient for each link of a hierarchical cluster tree. Given a hierarchical cluster tree Z generated by the linkage function, inconsistent computes the inconsistency coefficient for each link of the tree, using all the links down to the d-th level below that link. The default depth d is 2, which means that only two levels are considered: the level of the computed link and the level below that. Each row of Y corresponds to the row of same index of Z. The columns of Y are respectively: the mean of the heights of the links used for the calculation, the standard deviation of the heights of those links, the number of links used, the inconsistency coefficient. Reference Jain, A., and R. Dubes. Algorithms for Clustering Data. Upper Saddle River, NJ: Prentice-Hall, 1988. See also: cluster, clusterdata, dendrogram, linkage, pdist, squareform # name: # type: sq_string # elements: 1 # length: 83 Compute the inconsistency coefficient for each link of a hierarchical cluster tree. # name: # type: string # elements: 1 # length: 6 kmeans # name: # type: sq_string # elements: 1 # length: 4991 statistics: idx = kmeans (data, k) statistics: [idx, centers] = kmeans (data, k) statistics: [idx, centers, sumd] = kmeans (data, k) statistics: [idx, centers, sumd, dist] = kmeans (data, k) statistics: [...] = kmeans (data, k, param1, value1, ...) statistics: [...] = kmeans (data, [], 'Start', start, ...) Perform a k-means clustering of the NxD matrix data. If parameter 'Start' is specified, then k may be empty in which case k is set to the number of rows of start. The outputs are: idx An Nx1 vector whose i-th element is the class to which row i of data is assigned. centers A KxD array whose i-th row is the centroid of cluster i. sumd A kx1 vector whose i-th entry is the sum of the distances from samples in cluster i to centroid i. dist An Nxk matrix whose ij-th element is the distance from sample i to centroid j. The following parameters may be placed in any order. Each parameter must be followed by its value, as in Name-Value pairs. Name Description 'Start' The initialization method for the centroids. Value Description 'plus' The k-means++ algorithm. (Default) 'sample' A subset of k rows from data, sampled uniformly without replacement. 'cluster' Perform a pilot clustering on 10% of the rows of data. 'uniform' Each component of each centroid is drawn uniformly from the interval between the maximum and minimum values of that component within data. This performs poorly and is implemented only for Matlab compatibility. numeric matrix A kxD matrix of centroid starting locations. The rows correspond to seeds. numeric array A kxDxr array of centroid starting locations. The third dimension invokes replication of the clustering routine. Page r contains the set of seeds for replicate r. kmeans infers the number of replicates (specified by the 'Replicates' Name-Value pair argument) from the size of the third dimension. Name Description 'Distance' The distance measure used for partitioning and calculating centroids. Value Description 'sqeuclidean' The squared Euclidean distance. i.e. the sum of the squares of the differences between corresponding components. In this case, the centroid is the arithmetic mean of all samples in its cluster. This is the only distance for which this algorithm is truly "k-means". 'cityblock' The sum metric, or L1 distance, i.e. the sum of the absolute differences between corresponding components. In this case, the centroid is the median of all samples in its cluster. This gives the k-medians algorithm. 'cosine' One minus the cosine of the included angle between points (treated as vectors). Each centroid is the mean of the points in that cluster, after normalizing those points to unit Euclidean length. 'correlation' One minus the sample correlation between points (treated as sequences of values). Each centroid is the component-wise mean of the points in that cluster, after centering and normalizing those points to zero mean and unit standard deviation. 'hamming' The number of components in which the sample and the centroid differ. In this case, the centroid is the median of all samples in its cluster. Unlike Matlab, Octave allows non-logical data. Name Description 'EmptyAction' What to do when a centroid is not the closest to any data sample. Value Description 'error' Throw an error. 'singleton' (Default) Select the row of data that has the highest error and use that as the new centroid. 'drop' Remove the centroid, and continue computation with one fewer centroid. The dimensions of the outputs centroids and d are unchanged, with values for omitted centroids replaced by NaN. Name Description 'Display' Display a text summary. Value Description 'off' (Default) Display no summary. 'final' Display a summary for each clustering operation. 'iter' Display a summary for each iteration of a clustering operation. Name Value 'Replicates' A positive integer specifying the number of independent clusterings to perform. The output values are the values for the best clustering, i.e., the one with the smallest value of sumd. If 'Start' is numeric, then 'Replicates' defaults to (and must equal) the size of the third dimension of start. Otherwise it defaults to 1. 'MaxIter' The maximum number of iterations to perform for each replicate, a non-negative integer. If the maximum change of any centroid is less than 0.001, then the replicate terminates even if 'MaxIter' iterations have not occurred. The default is 100. A replicate stopped by 'MaxIter' returns the assignment its last centroids were computed from, with the sums of distances to them, as MATLAB does, so the centroids are always the means of the returned clusters. A value of 0 returns the starting centroids and the assignment to them, where MATLAB runs one iteration. 'Options' A structure, such as statset returns, whose MaxIter and Display fields are used when the 'MaxIter' and 'Display' options are not given; its other fields are ignored. Example: [~, c] = kmeans (rand (10, 3), 2, 'EmptyAction', 'singleton'); See also: linkage # name: # type: sq_string # elements: 1 # length: 52 Perform a k-means clustering of the N*D matrix data. # name: # type: string # elements: 1 # length: 8 kmedoids # name: # type: sq_string # elements: 1 # length: 2959 statistics: idx = kmedoids (X, k) statistics: [idx, C] = kmedoids (X, k) statistics: [idx, C, sumd] = kmedoids (X, k) statistics: [idx, C, sumd, D] = kmedoids (X, k) statistics: [idx, C, sumd, D, midx] = kmedoids (X, k) statistics: [idx, C, sumd, D, midx, info] = kmedoids (X, k) statistics: [...] = kmedoids (..., name, value) Partition observations into k clusters using the k-medoids algorithm. idx = kmedoids (X, k) partitions the NxP numeric matrix X into k clusters, each represented by one of the observations (its medoid), and returns the Nx1 vector idx of cluster indices. Rows of X correspond to observations and columns correspond to features or variables. Unlike kmeans, whose centroids are the mean of each cluster, a medoid is an actual data point, which makes k-medoids more robust to outliers and applicable to any distance metric. [idx, C, sumd, D, midx, info] = kmedoids (...) returns additional results: C a kxP matrix with the coordinates of the k medoids, one per row (C = X(midx,:)). sumd a kx1 vector with the within-cluster sum of the distances from each point to its cluster medoid, measured with the selected metric. D an Nxk matrix with the distance from every observation to every medoid. midx a kx1 vector with the row indices into X of the k medoids. info a scalar structure with the fields 'algorithm', 'start', 'distance', 'iterations', and 'bestReplicate' describing the chosen run. Additional parameters can be specified by Name-Value pair arguments. Name Value 'Distance' the distance metric, one of 'sqeuclidean' (default), 'euclidean', 'seuclidean', 'cityblock', 'minkowski', 'chebychev', 'cosine', 'correlation', 'hamming', 'jaccard', 'spearman', 'mahalanobis', or a custom distance function handle accepted by pdist2. 'Algorithm' the optimization algorithm, either 'pam' (default) for Partitioning Around Medoids, each iteration of which takes, for each medoid in turn, the swap with a non-medoid that most lowers the total distance, as MATLAB does, or 'small' for the faster Voronoi iteration that reassigns points and re-selects each cluster medoid until convergence, without the online phase MATLAB adds by default. 'Start' the method used to choose the initial medoids: 'plus' (default, k-means++), 'sample' (a random subset of the observations), 'cluster' (a preliminary pass on a subsample), or a kxP numeric matrix of starting medoid locations, each snapped to the nearest observation. A kxPxR array supplies a separate start for each of R replicates. 'Replicates' a positive integer number of times to repeat the clustering, each with a new set of initial medoids; the solution with the lowest total sum of distances is returned. The default is 1, or the size of the third dimension of a numeric 'Start'. 'Options' a structure, as created by statset, whose 'MaxIter' field caps the number of iterations (default 100). A 'MaxIter' of 0 returns the starting medoids. See also: kmeans, linkage, pdist2, dbscan # name: # type: sq_string # elements: 1 # length: 69 Partition observations into k clusters using the k-medoids algorithm. # name: # type: sq_string # elements: 1 # length: 7 linkage # name: # type: sq_string # elements: 1 # length: 3152 statistics: y = linkage (d) statistics: y = linkage (d, method) statistics: y = linkage (x) statistics: y = linkage (x, method) statistics: y = linkage (x, method, metric) statistics: y = linkage (x, method, arglist) Produce a hierarchical clustering dendrogram. d is the dissimilarity matrix relative to n observations, formatted as a (n-1)xn/2x1 vector as produced by pdist. Alternatively, x contains data formatted for input to pdist, metric is a metric for pdist and arglist is a cell array containing arguments that are passed to pdist. linkage starts by putting each observation into a singleton cluster and numbering those from 1 to n. Then it merges two clusters, chosen according to method, to create a new cluster numbered n+1, and so on until all observations are grouped into a single cluster numbered 2(n-1). Row k of the (m-1)x3 output matrix relates to cluster n+k: the first two columns are the numbers of the two component clusters and column 3 contains their distance. When several pairs of clusters are equally close, which of them is merged first is not determined by the data, and the cluster numbers in the first two columns are therefore implementation-defined. Only column 3, the sequence of merge distances, is reproducible across implementations, and even that is so only for the methods whose recomputation rule does not depend on the merge order ("weighted", "centroid" and "median" do depend on it). Code that must be portable should read column 3, or the cluster assignment obtained from cluster, rather than the raw numbering. method defines the way the distance between two clusters is computed and how they are recomputed when two clusters are merged: `"single" (default)' Distance between two clusters is the minimum distance between two elements belonging each to one cluster. Produces a cluster tree known as minimum spanning tree. `"complete"' Furthest distance between two elements belonging each to one cluster. `"average"' Unweighted pair group method with averaging (UPGMA). The mean distance between all pair of elements each belonging to one cluster. `"weighted"' Weighted pair group method with averaging (WPGMA). When two clusters A and B are joined together, the new distance to a cluster C is the mean between distances A-C and B-C. `"centroid"' Unweighted Pair-Group Method using Centroids (UPGMC). Assumes Euclidean metric. The distance between cluster centroids, each centroid being the center of mass of a cluster. `"median"' Weighted pair-group method using centroids (WPGMC). Assumes Euclidean metric. Distance between cluster centroids. When two clusters are joined together, the new centroid is the midpoint between the joined centroids. `"ward"' Ward's sum of squared deviations about the group mean (ESS). Also known as minimum variance or inner squared distance. Assumes Euclidean metric. How much the moment of inertia of the merged cluster exceeds the sum of those of the individual clusters. Reference Ward, J. H. Hierarchical Grouping to Optimize an Objective Function J. Am. Statist. Assoc. 1963, 58, 236-244, http://iv.slis.indiana.edu/sw/data/ward.pdf. See also: pdist, squareform # name: # type: sq_string # elements: 1 # length: 45 Produce a hierarchical clustering dendrogram. # name: # type: sq_string # elements: 1 # length: 16 optimalleaforder # name: # type: sq_string # elements: 1 # length: 1142 statistics: leafOrder = optimalleaforder (tree, D) statistics: leafOrder = optimalleaforder (..., Name, Value) Compute the optimal leaf ordering of a hierarchical binary cluster tree. The optimal leaf ordering of a tree is the ordering which minimizes the sum of the distances between each leaf and its adjacent leaves, without altering the structure of the tree, that is without redefining the clusters of the tree. Required inputs: tree: a hierarchical cluster tree tree generated by the linkage function. D: a matrix of distances as computed by pdist. Optional inputs can be the following property/value pairs: property 'Criteria' at the moment can only have the value 'adjacent', for minimizing the distances between leaves. property 'Transformation' can have one of the values 'linear', 'inverse' or a handle to a custom function which computes S the similarity matrix. optimalleaforder's output leafOrder is the optimal leaf ordering. Reference Bar-Joseph, Z., Gifford, D.K., and Jaakkola, T.S. Fast optimal leaf ordering for hierarchical clustering. Bioinformatics vol. 17 suppl. 1, 2001. See also: dendrogram, linkage, pdist # name: # type: sq_string # elements: 1 # length: 72 Compute the optimal leaf ordering of a hierarchical binary cluster tree. # name: # type: sq_string # elements: 1 # length: 15 spectralcluster # name: # type: sq_string # elements: 1 # length: 2722 statistics: idx = spectralcluster (X, k) statistics: idx = spectralcluster (S, k, 'Distance', 'precomputed') statistics: [idx, V] = spectralcluster (...) statistics: [idx, V, D] = spectralcluster (...) statistics: [...] = spectralcluster (..., name, value) Partition observations into k clusters using spectral clustering. idx = spectralcluster (X, k) partitions the NxP numeric matrix X into k clusters and returns the Nx1 vector idx of cluster indices. Rows of X correspond to observations and columns to features. Spectral clustering builds a similarity graph over the observations, embeds them with the eigenvectors of the graph Laplacian, and clusters that embedding, which lets it recover clusters that are not linearly separable in the original space. [idx, V, D] = spectralcluster (...) also returns the Nxk matrix V whose columns are the eigenvectors associated with the k smallest eigenvalues of the Laplacian, and the kx1 vector D of those eigenvalues. The signs of the eigenvectors, and the basis within a repeated eigenvalue, are arbitrary. Additional parameters can be specified by Name-Value pair arguments. Name Value 'Distance' the distance metric used to build the similarity graph, one of 'euclidean' (default), 'seuclidean', 'mahalanobis', 'cityblock', 'minkowski', 'chebychev', 'cosine', 'correlation', 'hamming', 'jaccard', 'spearman', or a function handle accepted by pdist2, or the string 'precomputed' to interpret the first input as an NxN similarity matrix. 'SimilarityGraph' 'knn' (default) to connect each observation to its nearest neighbors, or 'epsilon' to connect observations that are within a fixed radius. 'NumNeighbors' the number of nearest neighbors for the 'knn' graph, a positive integer. The default is ceil (log (N)). 'KNNGraphType' 'complete' (default) to connect i and j when either is a nearest neighbor of the other, or 'mutual' to connect them only when each is a nearest neighbor of the other. 'Radius' the radius for the 'epsilon' graph, a nonnegative scalar. Required when 'SimilarityGraph' is 'epsilon'. 'KernelScale' the positive scale factor sigma in the Gaussian similarity kernel exp (-(dist / sigma)^2) applied to the graph edges. The default is 1. 'LaplacianNormalization' 'randomwalk' (default), 'symmetric', or 'none', selecting how the graph Laplacian is normalized before the eigendecomposition. 'ClusterMethod' 'kmeans' (default) or 'kmedoids' to cluster the eigenvector embedding. 'P' the Minkowski exponent (default 2), used only with the 'minkowski' distance. 'Cov' the covariance matrix used only with the 'mahalanobis' distance. 'Scale' the scaling vector used only with the 'seuclidean' distance. See also: kmeans, kmedoids, dbscan, linkage, pdist2 # name: # type: sq_string # elements: 1 # length: 65 Partition observations into k clusters using spectral clustering. statistics-release-2.1.2/inst/Clustering/evalclusters.m000066400000000000000000000525261527311104300233210ustar00rootroot00000000000000## Copyright (C) 2021 Stefano Guidoni ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{eva} =} evalclusters (@var{x}, @var{clust}, @var{criterion}) ## @deftypefnx {statistics} {@var{eva} =} evalclusters (@dots{}, @qcode{Name}, @qcode{Value}) ## ## Create a clustering evaluation object to find the optimal number of clusters. ## ## @code{evalclusters} creates a clustering evaluation object to evaluate the ## optimal number of clusters for data @var{x}, using criterion @var{criterion}. ## The input data @var{x} is a matrix with @code{n} observations of @code{p} ## variables. ## The evaluation criterion @var{criterion} is one of the following: ## @table @code ## @item @qcode{CalinskiHarabasz} ## to create a @code{CalinskiHarabaszEvaluation} object. ## ## @item @qcode{DaviesBouldin} ## to create a @code{DaviesBouldinEvaluation} object. ## ## @item @qcode{gap} ## to create a @code{GapEvaluation} object. ## ## @item @qcode{silhouette} ## to create a @code{SilhouetteEvaluation} object. ## ## @end table ## The clustering algorithm @var{clust} is one of the following: ## @table @code ## @item @qcode{kmeans} ## to cluster the data using @code{kmeans} with @code{EmptyAction} set to ## @code{singleton} and @code{Replicates} set to 5. ## ## @item @qcode{linkage} ## to cluster the data using @code{clusterdata} with @code{linkage} set to ## @code{Ward}. ## ## @item @qcode{gmdistribution} ## to cluster the data using @code{fitgmdist} with @code{SharedCov} set to ## @code{true} and @code{Replicates} set to 5. ## ## @end table ## If the @var{criterion} is @code{CalinskiHarabasz}, @code{DaviesBouldin}, or ## @code{silhouette}, @var{clust} can also be a function handle to a function ## of the form @code{c = clust(x, k)}, where @var{x} is the input data, ## @var{k} the number of clusters to evaluate and @var{c} the clustering result. ## The clustering result can be either an array of size @code{n} with @code{k} ## different integer values, or a matrix of size @code{n} by @code{k} with a ## likelihood value assigned to each one of the @code{n} observations for each ## one of the @var{k} clusters. In the latter case, each observation is assigned ## to the cluster with the higher value. If the @var{criterion} is ## @code{CalinskiHarabasz}, @code{DaviesBouldin}, or ## @code{silhouette}, @var{clust} can also be a matrix of size @code{n} by ## @code{k}, where @code{k} is the number of proposed clustering solutions, so ## that each column of @var{clust} is a clustering solution. ## ## In addition to the obligatory @var{x}, @var{clust} and @var{criterion} inputs ## there is a number of optional arguments, specified as pairs of @code{Name} ## and @code{Value} options. The known @code{Name} arguments are: ## @table @code ## @item @qcode{KList} ## a vector of positive integer numbers, that is the cluster sizes to evaluate. ## This option is necessary, unless @var{clust} is a matrix of proposed ## clustering solutions. ## ## @item @qcode{Distance} ## a distance metric as accepted by the chosen @var{clust}. It can be the ## name of the distance metric as a string or a function handle. When ## @var{criterion} is @code{silhouette}, it can be a vector as created by ## function @code{pdist}. Valid distance metric strings are: @code{sqEuclidean} ## (default), @code{Euclidean}, @code{cityblock}, @code{cosine}, ## @code{correlation}, @code{Hamming}, @code{Jaccard}. ## Only used by @code{silhouette} and @code{gap} evaluation. ## ## @item @qcode{ClusterPriors} ## the prior probabilities of each cluster, which can be either @code{empirical} ## (default), or @code{equal}. When @code{empirical} the silhouette value is ## the average of the silhouette values of all points; when @code{equal} the ## silhouette value is the average of the average silhouette value of each ## cluster. Only used by @code{silhouette} evaluation. ## ## @item @qcode{B} ## the number of reference datasets generated from the reference distribution. ## Only used by @code{gap} evaluation. ## ## @item @qcode{ReferenceDistribution} ## the reference distribution used to create the reference data. It can be ## @code{PCA} (default) for a distribution based on the principal components of ## @var{X}, or @code{uniform} for a uniform distribution based on the range of ## the observed data. @code{PCA} is currently not implemented. ## Only used by @code{gap} evaluation. ## ## @item @qcode{SearchMethod} ## the method for selecting the optimal value with a @code{gap} evaluation. It ## can be either @code{globalMaxSE} (default) for selecting the smallest number ## of clusters which is inside the standard error of the maximum gap value, or ## @code{firstMaxSE} for selecting the first number of clusters which is inside ## the standard error of the following cluster number. ## Only used by @code{gap} evaluation. ## ## @end table ## ## Output @var{eva} is a clustering evaluation object. ## ## @seealso{CalinskiHarabaszEvaluation, DaviesBouldinEvaluation, GapEvaluation, ## SilhouetteEvaluation} ## ## @end deftypefn function cc = evalclusters (x, clust, criterion, varargin) ## input check if (nargin < 3) print_usage (); endif ## parsing input data if ((! ismatrix (x)) || (! isnumeric (x))) error ("evalclusters: X must be a numeric matrix."); endif ## useful values for input check n = rows (x); p = columns (x); ## parsing the clustering algorithm if (ischar (clust)) clust = lower (clust); if (! any (strcmpi (clust, {'kmeans', 'linkage', 'gmdistribution'}))) error ("evalclusters: unknown clustering algorithm '%s'", clust); endif elseif (! isscalar (clust)) if ((! isnumeric (clust)) || (length (size (clust)) != 2) || ... (rows (clust) != n)) error ("evalclusters: invalid matrix of clustering solutions."); endif elseif (! isa (clust, 'function_handle')) error ("evalclusters: invalid argument for 'clust'."); endif ## parsing the criterion parameter ## we check the rest later, as the check depends on the chosen criterion if (! ischar (criterion)) error ("evalclusters: invalid criterion, it must be a string."); else criterion = lower (criterion); if (! any (strcmpi (criterion, {'calinskiharabasz', 'daviesbouldin', ... 'silhouette', 'gap'}))) error ("evalclusters: unknown criterion '%s'", criterion); endif endif ## some default value klist = []; distance = 'sqeuclidean'; clusterpriors = 'empirical'; b = 100; referencedistribution = 'pca'; searchmethod = 'globalmaxse'; ## parse the name/value pairs pair_index = 1; while (pair_index < (nargin - 3)) ## type check if (! ischar (varargin{pair_index})) error ("evalclusters: invalid property, string expected."); endif ## now parse the parameter switch (lower (varargin{pair_index})) case 'klist' ## klist must be an array of positive integer numbers; ## there is a special case when it can be empty, but that is not the ## suggested way to use it (it is better to omit it instead) if (isempty (varargin{pair_index + 1})) if (ischar (clust) || isa (clust, 'function_handle')) error (strcat ("evalclusters: 'KList' can be empty", ... " only when 'clust' is a matrix")); endif elseif ((! isnumeric (varargin{pair_index + 1})) || ... (! isvector (varargin{pair_index + 1})) || ... any (find (varargin{pair_index + 1} < 1)) || ... any (floor (varargin{pair_index + 1}) != varargin{pair_index + 1})) error ("evalclusters: 'KList' must be an array of positive integers.") endif klist = varargin{pair_index + 1}; case 'distance' ## used by silhouette and gap if (! (strcmpi (criterion, 'silhouette') || strcmpi (criterion, 'gap'))) error (strcat ("evalclusters: distance metric cannot", ... " be used with '%s' criterion"), criterion); endif if (ischar (varargin{pair_index + 1})) if (! any (strcmpi (varargin{pair_index + 1}, ... {'sqeuclidean', 'euclidean', 'cityblock', 'cosine', ... 'correlation', 'hamming', 'jaccard'}))) error ("evalclusters: unknown distance criterion '%s'", ... varargin{pair_index + 1}); endif elseif (! isa (varargin{pair_index + 1}, 'function_handle') || ! ((isvector (varargin{pair_index + 1}) && ... isnumeric (varargin{pair_index + 1})))) error ("evalclusters: invalid distance metric."); endif distance = varargin{pair_index + 1}; case 'clusterpriors' ## used by silhouette evaluation if (! strcmpi (criterion, 'silhouette')) error (strcat ("evalclusters: cluster prior probabilities cannot", ... " be used with '%s' criterion"), criterion); endif if (any (strcmpi (varargin{pair_index + 1}, {'empirical', 'equal'}))) clusterpriors = lower (varargin{pair_index + 1}); else error ("evalclusters: invalid cluster prior probabilities value."); endif case 'b' ## used by gap evaluation if (! isnumeric (varargin{pair_index + 1}) || ... ! isscalar (varargin{pair_index + 1}) || ... varargin{pair_index + 1} != floor (varargin{pair_index + 1}) || ... varargin{pair_index + 1} < 1) error ("evalclusters: b must a be positive integer number."); endif b = varargin{pair_index + 1}; case 'referencedistribution' ## used by gap evaluation if (! ischar (varargin{pair_index + 1}) || ! any (strcmpi ... (varargin{pair_index + 1}, {'pca', 'uniform'}))) error (strcat ("evalclusters: the reference distribution", ... " must be either 'PCA' or 'uniform'.")); endif referencedistribution = lower (varargin{pair_index + 1}); case 'searchmethod' ## used by gap evaluation if (! ischar (varargin{pair_index + 1}) || any (strcmpi ... (varargin{pair_index + 1}, {'globalmaxse', 'uniform'}))) error (strcat ("evalclusters: the search method must be", ... " either'globalMaxSE' or 'firstmaxse'")); endif searchmethod = lower (varargin{pair_index + 1}); otherwise error ("evalclusters: unknown property %s", varargin{pair_index}); endswitch pair_index += 2; endwhile ## check if there are parameters without a value or a name left if (nargin - 2 - pair_index) if (ischar (varargin{pair_index})) error ("evalclusters: invalid parameter '%s'", varargin{pair_index}); else error ("evalclusters: invalid parameter '%d'", varargin{pair_index}); endif endif ## another check on klist if (isempty (klist) && (ischar (clust) || isa (clust, 'function_handle'))) error (strcat ("evalclusters: 'KList' can be empty", ... " only when 'clust' is a matrix")); endif ## When 'clust' is a matrix of solutions and no 'KList' was given, each ## column is a solution in its own right, so the number of clusters it holds ## is the number of distinct labels in it -- not the column's position. ## Numbering by position evaluated column j as k = j, which mislabelled every ## result and, because the criteria loop over the labels 1 : k, silently left ## out every cluster past the j-th. if (isempty (klist) && isnumeric (clust)) klist = arrayfun (@(j) numel (unique (clust(! isnan (clust(:, j)), j))), ... 1 : columns (clust)); if (numel (unique (klist)) != numel (klist)) error (strcat ("evalclusters: two or more columns of 'clust' propose", ... " the same number of clusters, so 'KList' cannot be", ... " inferred from them.")); endif ## The evaluation classes sort and de-duplicate the cluster sizes they are ## given, so the columns have to be put in the same order; otherwise a ## solution would be scored against another column's number of clusters. [klist, korder] = sort (klist); clust = clust(:, korder); endif ## main switch (lower (criterion)) case 'calinskiharabasz' ## further compatibility checks between the chosen parameters are ## delegated to the class constructor cc = CalinskiHarabaszEvaluation (x, clust, klist); case 'daviesbouldin' ## further compatibility checks between the chosen parameters are ## delegated to the class constructor cc = DaviesBouldinEvaluation (x, clust, klist); case 'silhouette' ## further compatibility checks between the chosen parameters are ## delegated to the class constructor cc = SilhouetteEvaluation (x, clust, klist, distance, clusterpriors); case 'gap' ## gap cannot be used with a pre-computed solution, i.e. a matrix for ## 'clust', and klist must be specified if (isnumeric (clust)) error (strcat ("evalclusters: 'clust' must be a clustering",... " algorithm when using the gap criterion.")); endif if (isempty (klist)) error (strcat ("evalclusters: 'klist' cannot be empty", ... " when using the gap criterion.")); endif cc = GapEvaluation (x, clust, klist, b, distance, ... referencedistribution, searchmethod); otherwise error ("evalclusters: invalid criterion '%s'", criterion); endswitch endfunction ## Demo code %!demo %! load fisheriris; %! eva = evalclusters (meas, 'kmeans', 'calinskiharabasz', 'KList', [1:6]) %! plot (eva) ## input tests %!error evalclusters () %!error evalclusters ([1 1;0 1]) %!error evalclusters ([1 1;0 1], 'kmeans') %!error ... %! evalclusters ('abc', 'kmeans', 'gap') %!error evalclusters ([1 1;0 1], 'xxx', 'gap') %!error evalclusters ([1 1;0 1], [1 2], 'gap') %!error evalclusters ([1 1;0 1], 1.2, 'gap') %!error evalclusters ([1 1;0 1], [1; 2], 123) %!error evalclusters ([1 1;0 1], [1; 2], 'xxx') %!error <'KList' can be empty*> evalclusters ([1 1;0 1], 'kmeans', 'gap') %!error evalclusters ([1 1;0 1], [1; 2], 'gap', 1) %!error evalclusters ([1 1;0 1], [1; 2], 'gap', 1, 1) %!error evalclusters ([1 1;0 1], [1; 2], 'gap', 'xxx', 1) %!error <'KList'*> evalclusters ([1 1;0 1], [1; 2], 'gap', 'KList', [-1 0]) %!error <'KList'*> evalclusters ([1 1;0 1], [1; 2], 'gap', 'KList', [1 .5]) %!error <'KList'*> evalclusters ([1 1;0 1], [1; 2], 'gap', 'KList', [1 1; 1 1]) %!error evalclusters ([1 1;0 1], [1; 2], 'gap', ... %! 'distance', 'a') %!error evalclusters ([1 1;0 1], [1; 2], 'daviesbouldin', ... %! 'distance', 'a') %!error evalclusters ([1 1;0 1], [1; 2], 'gap', ... %! 'clusterpriors', 'equal') %!error evalclusters ([1 1;0 1], [1; 2], ... %! 'silhouette', 'clusterpriors', 'xxx') %!error <'clust' must be a clustering*> evalclusters ([1 1;0 1], [1; 2], 'gap') %!test %! load fisheriris; %! eva = evalclusters (meas, 'kmeans', 'calinskiharabasz', 'KList', [1:6]); %! assert_equal (isa (eva, 'CalinskiHarabaszEvaluation'), true); %! assert_equal (eva.NumObservations, 150); %! assert_equal (eva.OptimalK, 3); %! assert_equal (eva.InspectedK, [1 2 3 4 5 6]); ## A matrix of solutions with no 'KList' takes the number of clusters from the ## labels in each column, not from the column's position. Numbering by ## position evaluated column j as k = j, so the first criterion was always NaN ## and the rest were computed for the wrong k, over only the first j clusters. %!test %! x = [randn(20,2); 6 + randn(20,2); [12, 0] + randn(20,2)]; %! k2 = [ones(30,1); 2*ones(30,1)]; %! k3 = [ones(20,1); 2*ones(20,1); 3*ones(20,1)]; %! k4 = [ones(15,1); 2*ones(15,1); 3*ones(15,1); 4*ones(15,1)]; %! sols = [k2, k3, k4]; %! for crit = {'CalinskiHarabasz', 'DaviesBouldin', 'silhouette'} %! eva = evalclusters (x, sols, crit{1}); %! assert_equal (eva.InspectedK, [2, 3, 4]); %! assert_equal (any (isnan (eva.CriterionValues)), false); %! ## an explicit KList naming the same sizes must give the same answer %! ref = evalclusters (x, sols, crit{1}, 'KList', [2, 3, 4]); %! assert_equal (eva.CriterionValues, ref.CriterionValues); %! assert_equal (eva.OptimalK, ref.OptimalK); %! endfor ## Columns need not be given in ascending order of size. The evaluation ## classes sort the cluster sizes, so the columns are reordered to match and ## each solution is still scored against its own k. %!test %! x = [randn(20,2); 6 + randn(20,2); [12, 0] + randn(20,2)]; %! k4 = [ones(15,1); 2*ones(15,1); 3*ones(15,1); 4*ones(15,1)]; %! k2 = [ones(30,1); 2*ones(30,1)]; %! fwd = evalclusters (x, [k2, k4], 'CalinskiHarabasz'); %! rev = evalclusters (x, [k4, k2], 'CalinskiHarabasz'); %! assert_equal (rev.InspectedK, [2, 4]); %! assert_equal (rev.CriterionValues, fwd.CriterionValues); %! assert_equal (rev.OptimalK, fwd.OptimalK); ## Two columns proposing the same number of clusters cannot be told apart by ## the cluster sizes alone. %!error ... %! evalclusters ([randn(20,2); 6 + randn(20,2)], ... %! [[ones(20,1); 2*ones(20,1)], [2*ones(20,1); ones(20,1)]], ... %! 'CalinskiHarabasz') ## Shape and naming of the returned object, verified against MATLAB R2024a. %!shared X, sols %! randn ("seed", 11); %! X = [randn(20, 2); 6 + randn(20, 2); [12, 0] + randn(20, 2)]; %! sols = [[ones(30,1); 2*ones(30,1)], ... %! [ones(20,1); 2*ones(20,1); 3*ones(20,1)], ... %! [ones(15,1); 2*ones(15,1); 3*ones(15,1); 4*ones(15,1)]]; %!test # given the clusterings, the object does not echo them back %! e = evalclusters (X, sols, "CalinskiHarabasz"); %! assert_equal (isempty (e.OptimalY), true); %! assert_equal (isempty (e.ClusteringFunction), true); %! assert_equal (class (e.ClusteringFunction), "double"); %!test # asked to cluster, it reports both the solution and the function %! e = evalclusters (X, "kmeans", "CalinskiHarabasz", "KList", 2:4); %! assert_equal (size (e.OptimalY), [60, 1]); %! assert_equal (e.ClusteringFunction, "kmeans"); %!test # Missing carries one flag per observation, shaped like the observations %! e = evalclusters (X, sols, "CalinskiHarabasz"); %! assert_equal (size (e.Missing), [60, 1]); %! assert_equal (class (e.Missing), "logical"); %! Xn = X; Xn(5,1) = NaN; %! e = evalclusters (Xn, sols, "CalinskiHarabasz"); %! assert_equal (find (e.Missing), 5); %! assert_equal (e.NumObservations, 59); %!test # a criterion that is undefined everywhere leaves no optimal K %! e = evalclusters (X, ones (60, 1), "CalinskiHarabasz"); %! assert_equal (isnan (e.CriterionValues), true); %! assert_equal (isnan (e.OptimalK), true); %! assert_equal (isempty (e.OptimalY), true); %!test # criterion and option names are reported as MATLAB spells them %! assert_equal (evalclusters (X, sols, "CalinskiHarabasz").CriterionName, ... %! "CalinskiHarabasz"); %! assert_equal (evalclusters (X, sols, "DaviesBouldin").CriterionName, ... %! "DaviesBouldin"); %! assert_equal (evalclusters (X, sols, "silhouette").CriterionName, ... %! "Silhouette"); %! e = evalclusters (X, sols, "silhouette", "Distance", "sqeuclidean"); %! assert_equal (e.Distance, "sqEuclidean"); %! e = evalclusters (X, sols, "silhouette", "Distance", "cityblock"); %! assert_equal (e.Distance, "cityblock"); %!test # ClusterSilhouettes holds one mean per cluster, not one per observation %! e = evalclusters (X, sols, "silhouette"); %! assert_equal (numel (e.ClusterSilhouettes), 3); %! assert_equal (size (e.ClusterSilhouettes{1}), [2, 1]); %! assert_equal (size (e.ClusterSilhouettes{2}), [3, 1]); %! assert_equal (size (e.ClusterSilhouettes{3}), [4, 1]); %!test # the silhouette criterion honours the metric it was given %! a = evalclusters (X, sols, "silhouette", "Distance", "sqeuclidean"); %! b = evalclusters (X, sols, "silhouette", "Distance", "cityblock"); %! c = evalclusters (X, sols, "silhouette", "Distance", "cosine"); %! assert_equal (isequal (a.CriterionValues, b.CriterionValues), false); %! assert_equal (isequal (b.CriterionValues, c.CriterionValues), false); %! ## each cluster mean is the mean of that cluster's silhouette values %! si = silhouette (X, sols(:,2), "cityblock", "DoNotPlot"); %! m = [mean(si(sols(:,2) == 1)); mean(si(sols(:,2) == 2)); ... %! mean(si(sols(:,2) == 3))]; %! assert_equal (b.ClusterSilhouettes{2}, m, 1e-12); statistics-release-2.1.2/inst/Clustering/fitgmdist.m000066400000000000000000000527621527311104300226010ustar00rootroot00000000000000## Copyright (C) 2015 Lachlan Andrew ## Copyright (C) 2018 John Donoghue ## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{GMdist} =} fitgmdist (@var{data}, @var{k}, @var{param1}, @var{value1}, @dots{}) ## ## Fit a Gaussian mixture model with @var{k} components to @var{data}. ## Each row of @var{data} is a data sample. Each column is a variable. ## ## Optional parameters are: ## @itemize ## @item @qcode{'start'}: Initialization conditions. Possible values are: ## @itemize ## @item @qcode{'randSample'} (default) Takes means uniformly from rows of data. ## @item @qcode{'plus'} Use k-means++ to initialize means. ## @item @qcode{'cluster'} Performs an initial clustering with 10% of the data. ## @item @var{vector} A vector whose length is the number of rows in data, and ## whose values are 1 to k specify the components each row is initially ## allocated to. The mean, variance, and weight of each component is calculated ## from that. ## @item @var{structure} A structure with fields @qcode{mu}, @qcode{Sigma} and ## @qcode{ComponentProportion}. ## @end itemize ## For @qcode{'randSample'}, @qcode{'plus'}, and @qcode{'cluster'}, the initial ## variance of each component is the variance of the entire data sample. ## ## @item @qcode{'Replicates'}: Number of random restarts to perform. ## ## @item @qcode{'RegularizationValue'} or @qcode{'Regularize'}: A small number ## added to the diagonal entries of the covariance to prevent singular ## covariances. ## ## @item @qcode{'SharedCovariance'} or @qcode{'SharedCov'} (logical). True if ## all components must share the same variance, to reduce the number of free ## parameters ## ## @item @qcode{'CovarianceType'} or @qcode{'CovType'} (string). Possible values ## are: ## @itemize ## @item @qcode{'full'} (default) Allow arbitrary covariance matrices. ## @item @qcode{'diagonal'} Force covariances to be diagonal, to reduce the ## number of free parameters. ## @end itemize ## ## @item @qcode{'Options'}: A structure with all of the following fields: ## @itemize ## @item @qcode{MaxIter} Maximum number of EM iterations (default 100). A ## value of 0 returns the starting point with its likelihood, where MATLAB ## fails. ## @item @qcode{TolFun} Threshold increase in likelihood to terminate EM ## (default 1e-6). ## @item @qcode{Display} Possible values are: ## @itemize ## @item @qcode{'off'} (default): Display nothing. ## @item @qcode{'final'}: Display the total number of iterations and likelihood ## once the execution completes. ## @item @qcode{'iter'}: Display the number of iteration and likelihood after ## each iteration. ## @end itemize ## @end itemize ## @item @qcode{'Weight'}: A column vector or @math{N*2} matrix. The first ## column consists of non-negative weights given to the samples. If these are ## all integers, this is equivalent to specifying @qcode{@var{weight}(i)} copies ## of row @qcode{i} of @var{data}, but potentially faster. If a row of ## @var{data} is used to represent samples that are similar but not identical, ## then the second column of @var{weight} indicates the variance of those ## original samples. Specifically, in the EM algorithm, the contribution of row ## @qcode{i} towards the variance is set to at least @qcode{@var{weight}(i,2)}, ## to prevent spurious components with zero variance. ## @end itemize ## ## @seealso{gmdistribution, kmeans} ## @end deftypefn function obj = fitgmdist (data, k, varargin) if (nargin < 2 || mod (nargin, 2) == 1) print_usage; endif [~, prop] = parseparams (varargin); ## defaults for options diagonalCovar = false; # "full". (true is "diagonal") sharedCovar = false; start = 'randSample'; replicates = 1; option.MaxIter = 100; option.TolFun = 1e-6; option.Display = 'off'; # "off" (1 is "final", 2 is "iter") Regularizer = 0; weights = []; # Each row i counts as "weights(i,1)" rows ## Remove rows containing NaN / NA data = data(! any (isnan (data), 2), :); ## Used for getting the number of samples nRows = rows (data); nCols = columns (data); ## Parse options while (! isempty (prop)) try switch (lower (prop{1})) case {'sharedcovariance', 'sharedcov'} sharedCovar = prop{2}; case {'covariancetype', 'covartype'} diagonalCovar = prop{2}; case {'regularizationvalue', 'regularize'} Regularizer = prop{2}; case 'replicates' replicates = prop{2}; case 'start' start = prop{2}; case 'weights' weights = prop{2}; case 'options' option.MaxIter = prop{2}.MaxIter; option.TolFun = prop{2}.TolFun; option.Display = prop{2}.Display; otherwise error ("fitgmdist: Unknown option %s.", prop{1}); endswitch catch ME if (length (prop) < 2) error ("fitgmdist: Option '%s' has no argument.", prop{1}); else rethrow (ME) endif end_try_catch prop = prop(3:end); endwhile ## Process options ## Check for the "replicates" property try if isempty (1:replicates) error ("fitgmdist: replicates must be positive."); endif catch error ("fitgmdist: invalid number of replicates."); end_try_catch ## check for the "option" property MaxIter = option.MaxIter; TolFun = option.TolFun; switch (lower (option.Display)) case 'off' Display = 0; case 'final' Display = 1; case 'iter' Display = 2; case 'notify' Display = 0; otherwise error ("fitgmdist: Unknown Display option %s.", option.Display); endswitch try p = ones (1, k) / k; # Default is uniform component proportions catch ME if (! isscalar (k) || ! isnumeric (k)) error ("fitgmdist: The second argument must be a numeric scalar."); else rethrow (ME) endif end_try_catch ## Check for the "start" property if (ischar (start)) start = lower (start); switch (start) case {'randsample', 'plus', 'cluster', 'randsamplep', 'plusp', 'clusterp'} otherwise error ("fitgmdist: Unknown Start value %s\n.", start); endswitch component_order_free = true; else component_order_free = false; if (! ismatrix (start) || ! isnumeric (start)) try mu = start.mu; Sigma = start.Sigma; if (isfield (start, 'ComponentProportion')) p = start.ComponentProportion(:)'; endif if (any (size (data, 2) != [size(mu, 2), size(Sigma, 1)]) || ... any (k != [size(mu,1), size(p,2)])) error ("fitgmdist: Start parameter has mismatched dimensions."); endif catch error ("fitgmdist: invalid start parameter."); end_try_catch else validIndices = 0; mu = zeros (k, nRows); Sigma = zeros (nRows, nRows, k); for i = 1:k idx = (start == i); validIndices = validIndices + sum (idx); mu(i,:) = mean (data(idx,:)); Sigma(:,:,i) = cov (data(idx,:)) + Regularizer * eye (nCols); endfor if (validIndices < nRows) error (strcat ("fitgmdist: Start is numeric, but is not", ... " integers between 1 and k.")); endif endif start = []; # so that variance isn't recalculated later replicates = 1; # Will be the same each time anyway endif ## Check for the "SharedCovariance" property if (! islogical (sharedCovar)) error ("fitgmdist: SharedCoveriance must be logical true or false."); endif ## Check for the "CovarianceType" property if (! islogical (diagonalCovar)) try if (strcmpi (diagonalCovar, 'diagonal')) diagonalCovar = true; elseif (strcmpi (diagonalCovar, 'full')) diagonalCovar = false; else error ("fitgmdist: CovarianceType must be Full or Diagonal."); endif catch error ("fitgmdist: CovarianceType must be 'Full' or 'Diagonal'."); end_try_catch endif ## Check for the "Regularizer" property try if (Regularizer < 0) error ("fitgmdist: Regularizer must be non-negative"); endif catch ME if (! isscalar (Regularizer) || ! isnumeric (Regularizer)) error ("fitgmdist: Regularizer must be a numeric scalar"); else rethrow (ME) endif end_try_catch ## Check for the "Weights" property and the matrix try if (! isempty (weights)) if (columns (weights) > 2 || any (weights(:) < 0)) error (strcat ("fitgmdist: weights must be a nonnegative", ... " numeric dx1 or dx2 matrix.")); endif if (rows (weights) != nRows) error (strcat ("fitgmdist: number of weights %d must match", ... " number of samples %d."), rows (weights), nRows); endif non_zero = (weights(:,1) > 0); weights = weights(non_zero,:); data = data(non_zero,:); nRows = rows (data); raw_samples = sum (weights(:,1)); else raw_samples = nRows; endif ## Validate the matrix if (! isreal (data(k,1))) error ("fitgmdist: first input argument must be a DxN real data matrix."); endif catch ME if (! isnumeric (data) || ! ismatrix (data) || ! isreal (data)) error ("fitgmdist: first input argument must be a DxN real data matrix."); elseif (k > nRows || k < 0) if (exists ('non_zero', 'var') && k <= length (non_zero)) error (strcat ("fitgmdist: The number of non-zero weights (%d)", ... " must be at least the number of components", ... " (%d)."), nRows, k); else error (strcat ("fitgmdist: The number of components (%d) must be", ... " a positive number less than the number of data", ... " rows (%d)."), k, nRows); endif elseif (! ismatrix (weights) || ! isnumeric (weights)) error (strcat ("fitgmdist: weights must be a nonnegative numeric", ... " dx1 or dx2 matrix.")); else rethrow (ME) endif end_try_catch ## Done processing options ####################################### ## #sed to hold the probability of each class, given each data vector try p_x_l = zeros (nRows, k); # probability of observation x given class l best = -realmax; best_params = []; diag_slice = 1:(nCols+1):(nCols)^2; ## Create index slices to calculate symmetric ## completion of upper triangular Mx lower_half = zeros (nCols * (nCols - 1) / 2, 1); upper_half = zeros (nCols * (nCols - 1) / 2, 1); i = 1; for rw = 1:nCols for cl = rw+1:nCols upper_half(i) = sub2ind ([nCols, nCols], rw, cl); lower_half(i) = sub2ind ([nCols, nCols], cl, rw); i = i + 1; endfor endfor for rep = 1:replicates if (! isempty (start)) ## Initialize the means switch (start) case {'randsample'} if (isempty (weights)) idx = randperm (nRows, k); else idx = randsample (nRows, k, false, weights); endif mu = data(idx, :); case {'plus'} # k-means++, by Arthur and Vassilios mu(1,:) = data(randi (nRows),:); d = inf (nRows, 1); # Distance to nearest centroid so far for i = 2:k d = min (d, sum (bsxfun (@minus, data, mu(i-1, :)).^2, 2)); # pick next sample with prob. prop to dist.*weights if (isempty (weights)) cs = cumsum (d); else cs = cumsum (d .* weights(:,1)); endif mu(i,:) = data(find (cs > rand * cs(end), 1), :); endfor case {'cluster'} subsamp = max (k, ceil (nRows/10)); if (isempty (weights)) idx = randperm (nRows, subsamp); else idx = randsample (nRows, subsamp, false, weights); endif [~, mu] = kmeans (data(idx), k, 'start', 'sample'); endswitch ## Initialize the variance, unless set explicitly Sigma = var (data) + Regularizer; if (! diagonalCovar) Sigma = diag (Sigma); endif if (! sharedCovar) Sigma = repmat (Sigma, [1, 1, k]); endif endif ## Run the algorithm iter = 1; log_likeli = -inf; incr = 1; while (incr > TolFun && iter <= MaxIter) iter = iter + 1; ## The parameters this step's log-likelihood belongs to prev_mu = mu; prev_Sigma = Sigma; prev_p = p; ####################################### ## "E step" ## Calculate probability of class l given observations for i = 1:k if (sharedCovar) sig = Sigma; else sig = Sigma(:,:,i); endif if (diagonalCovar) sig = diag (sig); endif try p_x_l(:, i) = mvnpdf (data, mu(i, :), sig); catch ME if (strfind (ME.message, 'positive definite')) error (strcat ("fitgmdist: Covariance is not positive", ... " definite. Increase RegularizationValue.")); else rethrow (ME) endif end_try_catch endfor ## Bayes' rule p_x_l = bsxfun (@times, p_x_l, p); # weight by priors p_l_x = bsxfun (@rdivide, p_x_l, sum (p_x_l, 2)); # Normalize ####################################### ## "M step" ## Calculate new parameters if (! isempty (weights)) p_l_x = bsxfun (@times, p_l_x, weights(:,1)); endif sum_p_l_x = sum (p_l_x); # row vec of \sum_{data} p(class|data,params) p = sum_p_l_x / raw_samples; # new proportions mu = bsxfun (@rdivide, p_l_x' * data, sum_p_l_x'); # new means if (sharedCovar) sumSigma = zeros (size (Sigma(:,:,1))); # diagonalCovar gives size endif for i = 1:k ## Sigma deviation = bsxfun (@minus, data, mu(i,:)); lhs = bsxfun (@times, p_l_x(:,i), deviation); ## Calculate covariance ## Iterate either over elements of the covariance matrix, ## since there should be fewer of those than rows of data. for rw = 1:nCols for cl = rw:nCols sig(rw,cl) = lhs(:,rw)' * deviation(:,cl); endfor endfor sig(lower_half) = sig(upper_half); sig = sig/sum_p_l_x(i) + Regularizer*eye (nCols); if (columns (weights) > 1) # don't give "singleton" clusters low var sig(diag_slice) = max (sig(diag_slice), weights(i,2)); endif if (diagonalCovar) sig = diag (sig)'; endif if (sharedCovar) sumSigma = sumSigma + sig * p(i); # Heuristic. Should it use else # old p? Something else? Sigma(:,:,i) = sig; endif endfor if (sharedCovar) Sigma = sumSigma; endif ####################################### ## Calculate the new (and relative change in) log-likelihood if (isempty (weights)) new_log_likeli = sum (log (sum (p_x_l, 2))); else new_log_likeli = sum (weights(:,1) .* log (sum (p_x_l, 2))); endif incr = (new_log_likeli - log_likeli)/max (1,abs (new_log_likeli)); if (Display == 2) fprintf ("iter %d log-likelihood %g\n", iter-1, new_log_likeli); endif log_likeli = new_log_likeli; endwhile ## Converged, return the parameters of the last log-likelihood, as ## MATLAB does; stopped by MaxIter, the last update if (iter > 1 && incr <= TolFun) mu = prev_mu; Sigma = prev_Sigma; p = prev_p; endif ## With no EM step run, the log-likelihood of the starting point if (iter == 1) lik = pdf (gmdistribution (mu, Sigma, p'), data); if (isempty (weights)) log_likeli = sum (log (lik)); else log_likeli = sum (weights(:,1) .* log (lik)); endif endif if (log_likeli > best) best = log_likeli; best_params.mu = mu; best_params.Sigma = Sigma; best_params.p = p; endif endfor catch ME try if (1 < MaxIter), endif catch error ("fitgmdist: invalid MaxIter."); end_try_catch rethrow (ME) end_try_catch ## List components in descending order of proportion, ## unless the order was implicitly specified by "start" if (component_order_free) [~, idx] = sort (-best_params.p); best_params.p = best_params.p (idx); best_params.mu = best_params.mu(idx,:); if (! sharedCovar) best_params.Sigma = best_params.Sigma(:,:,idx); endif endif ## Calculate number of parameters if (diagonalCovar) params = nCols; else params = nCols * (nCols+1) / 2; endif params = params*size (Sigma, 3) + 2*rows (mu) - 1; ## This works in Octave, but not in Matlab #obj = gmdistribution (best_params.mu, best_params.Sigma, best_params.p', extra); obj = gmdistribution (best_params.mu, best_params.Sigma, best_params.p'); obj.NegativeLogLikelihood = -best; obj.AIC = -2*(best - params); obj.BIC = -2*best + params * log (raw_samples); obj.Converged = (incr <= TolFun); obj.NumIterations = iter-1; obj.RegularizationValue = Regularizer; if (Display == 1) fprintf (" %d iterations log-likelihood = %g\n", ... obj.NumIterations, -obj.NegativeLogLikelihood); endif endfunction %!demo %! ## Generate a two-cluster problem %! rng (42); %! C1 = randn (100, 2) + 2; %! C2 = randn (100, 2) - 2; %! data = [C1; C2]; %! %! ## Perform clustering %! GMModel = fitgmdist (data, 2); %! %! ## Plot the result %! figure %! [heights, bins] = hist3 ([C1; C2]); %! [xx, yy] = meshgrid (bins{1}, bins{2}); %! bbins = [xx(:), yy(:)]; %! contour (reshape (GMModel.pdf (bbins), size (heights))); %!demo %! rng (42); %! Angle_Theta = [ 30 + 10 * randn(1, 10), 60 + 10 * randn(1, 10) ]'; %! nbOrientations = 2; %! initial_orientations = [38.0; 18.0]; %! initial_weights = ones (1, nbOrientations) / nbOrientations; %! initial_Sigma = 10 * ones (1, 1, nbOrientations); %! start = struct ('mu', initial_orientations, 'Sigma', initial_Sigma, ... %! 'ComponentProportion', initial_weights); %! GMModel_Theta = fitgmdist (Angle_Theta, nbOrientations, 'Start', start , ... %! 'RegularizationValue', 0.0001) ## Test results against MATLAB example %!test %! load fisheriris %! classes = unique (species); %! [~, score] = pca (meas, 'NumComponents', 2); %! options.MaxIter = 1000; %! options.TolFun = 1e-6; %! options.Display = 'off'; %! GMModel = fitgmdist (score, 2, 'Options', options); %! assert_equal (isa (GMModel, 'gmdistribution'), true); %! assert_equal (GMModel.mu, [1.3212, -0.0954; -2.6424, 0.1909], 1e-4); ## No EM step returns the starting point with its likelihood %!test %! load fisheriris %! X = meas(:,1:2); %! S = struct ('mu', X([1, 51],:), 'Sigma', cat (3, eye (2), eye (2)), ... %! 'ComponentProportion', [0.5, 0.5]); %! options = struct ('MaxIter', 0, 'TolFun', 1e-6, 'Display', 'off'); %! G = fitgmdist (X, 2, 'Start', S, 'Options', options); %! assert_equal (G.mu, S.mu); %! assert_equal (G.NumIterations, 0); %! assert_equal (G.Converged, false); %! nll = -sum (log (pdf (gmdistribution (S.mu, S.Sigma, [0.5, 0.5]), X))); %! assert_equal (G.NegativeLogLikelihood, nll, 1e-10); ## Values checked against MATLAB R2024a. Converged, the parameters are those ## of the reported likelihood; stopped by MaxIter, those of the last update. %!test %! load fisheriris %! X = meas(:,1:2); %! S = struct ('mu', X([1, 51],:), 'Sigma', cat (3, eye (2), eye (2)), ... %! 'ComponentProportion', [0.5, 0.5]); %! options = struct ('MaxIter', 100, 'TolFun', 1e-6, 'Display', 'off'); %! G = fitgmdist (X, 2, 'Start', S, 'Options', options); %! assert_equal (G.NumIterations, 20); %! assert_equal (G.mu, [5.0163712005056782, 3.4547434334001554; ... %! 6.2366722729173318, 2.8683079098768118], 1e-8); %! assert_equal (G.NegativeLogLikelihood, 225.91571677229396, 1e-8); %! assert_equal (-sum (log (pdf (G, X))), G.NegativeLogLikelihood, 1e-10); %!test %! load fisheriris %! X = meas(:,1:2); %! S = struct ('mu', X([1, 51],:), 'Sigma', cat (3, eye (2), eye (2)), ... %! 'ComponentProportion', [0.5, 0.5]); %! options = struct ('MaxIter', 5, 'TolFun', 1e-6, 'Display', 'off'); %! G = fitgmdist (X, 2, 'Start', S, 'Options', options); %! assert_equal (G.mu, [5.2685393078279175, 3.1752930402569604; ... %! 6.4612669859457217, 2.9305204763341486], 1e-10); %! assert_equal (G.NegativeLogLikelihood, 250.92335726435553, 1e-10); statistics-release-2.1.2/inst/Clustering/gmdistribution.m000066400000000000000000000535671527311104300236560ustar00rootroot00000000000000## Copyright (C) 2015 Lachlan Andrew ## Copyright (C) 2018-2020 John Donoghue ## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef gmdistribution ## -*- texinfo -*- ## @deftp {statistics} gmdistribution ## ## Gaussian mixture distribution. ## ## A @code{gmdistribution} object represents a mixture of k multivariate ## Gaussian components in d variables: their means @code{mu}, their ## covariances @code{Sigma} and their mixing proportions ## @code{ComponentProportion}. The methods @code{pdf}, @code{cdf} and ## @code{random} evaluate and sample the mixture, while @code{cluster}, ## @code{posterior} and @code{mahal} assign observations to its components. ## ## Create a @code{gmdistribution} object with the class constructor from ## known parameters, or with @code{fitgmdist} from data, which also records ## the results of the fit. ## ## @seealso{fitgmdist} ## @end deftp properties ## -*- texinfo -*- ## @deftp {gmdistribution} {property} mu ## ## Component means ## ## A k-by-d matrix holding the mean of each of the k components, one ## per row, where d is the number of variables. This property is ## read-only. ## ## @end deftp mu ## -*- texinfo -*- ## @deftp {gmdistribution} {property} Sigma ## ## Component covariances ## ## The covariances of the components, in the form they were given in. ## A full covariance per component is d-by-d-by-k, a diagonal one per ## component 1-by-d-by-k, a full covariance shared by every component ## d-by-d, and a shared diagonal one 1-by-d. This property is ## read-only. ## ## @end deftp Sigma ## -*- texinfo -*- ## @deftp {gmdistribution} {property} ComponentProportion ## ## Mixing proportions ## ## A 1-by-k row vector holding the proportion of each component. The ## proportions are scaled to sum to 1, and are all equal when none was ## given. This property is read-only. ## ## @end deftp ComponentProportion ## -*- texinfo -*- ## @deftp {gmdistribution} {property} DistributionName ## ## Name of the distribution ## ## The character vector @qcode{'gaussian mixture distribution'}. This ## property is read-only. ## ## @end deftp DistributionName ## -*- texinfo -*- ## @deftp {gmdistribution} {property} NumComponents ## ## Number of mixture components ## ## A positive integer, the number of rows of @code{mu}. This property ## is read-only. ## ## @end deftp NumComponents ## -*- texinfo -*- ## @deftp {gmdistribution} {property} NumVariables ## ## Number of variables ## ## A positive integer, the dimension of each component, which is the ## number of columns of @code{mu}. This property is read-only. ## ## @end deftp NumVariables ## -*- texinfo -*- ## @deftp {gmdistribution} {property} CovarianceType ## ## Form of the component covariances ## ## A character vector, @qcode{'diagonal'} when each covariance was ## given as a row of variances and @qcode{'full'} when it was given as ## a matrix. This property is read-only. ## ## @end deftp CovarianceType ## -*- texinfo -*- ## @deftp {gmdistribution} {property} SharedCovariance ## ## Whether the components share one covariance ## ## A logical scalar, true when a single covariance was given for every ## component and false when one was given per component. This property ## is read-only. ## ## @end deftp SharedCovariance ## -*- texinfo -*- ## @deftp {gmdistribution} {property} AIC ## ## Akaike information criterion ## ## A scalar, twice the negative log-likelihood plus twice the number of ## estimated parameters. It is empty unless the object came from ## @code{fitgmdist}. This property is read-only. ## ## @end deftp AIC ## -*- texinfo -*- ## @deftp {gmdistribution} {property} BIC ## ## Bayesian information criterion ## ## A scalar, twice the negative log-likelihood plus the number of ## estimated parameters times the log of the number of observations. ## It is empty unless the object came from @code{fitgmdist}. This ## property is read-only. ## ## @end deftp BIC ## -*- texinfo -*- ## @deftp {gmdistribution} {property} Converged ## ## Whether the fit converged ## ## A logical scalar, true when the fit reached its tolerance within the ## iteration limit. It is empty unless the object came from ## @code{fitgmdist}. This property is read-only. ## ## @end deftp Converged ## -*- texinfo -*- ## @deftp {gmdistribution} {property} NegativeLogLikelihood ## ## Negative log-likelihood at the fit ## ## A scalar, the negative of the log-likelihood of the data under the ## fitted mixture. It is empty unless the object came from ## @code{fitgmdist}. This property is read-only. ## ## @end deftp NegativeLogLikelihood ## -*- texinfo -*- ## @deftp {gmdistribution} {property} NumIterations ## ## Iterations the fit took ## ## A positive integer. It is empty unless the object came from ## @code{fitgmdist}. This property is read-only. ## ## @end deftp NumIterations ## -*- texinfo -*- ## @deftp {gmdistribution} {property} RegularizationValue ## ## Regularization added to the covariance diagonal ## ## A nonnegative scalar added to the diagonal of each covariance to ## keep it positive definite. It is empty unless the object came from ## @code{fitgmdist}. This property is read-only. ## ## @end deftp RegularizationValue endproperties ## Kept out of the documented surface. MATLAB carries the same alias and ## hides it, along with eight other legacy names this class does not have. properties (Hidden) ## A second name for NegativeLogLikelihood, holding the same value. NlogL endproperties properties(Access = private) DiagonalCovariance ## bool summary of "CovarianceType" endproperties methods ## -*- texinfo -*- ## @deftypefn {gmdistribution} {@var{obj} =} gmdistribution (@var{mu}, @var{Sigma}) ## @deftypefnx {gmdistribution} {@var{obj} =} gmdistribution (@var{mu}, @var{Sigma}, @var{p}) ## @deftypefnx {gmdistribution} {@var{obj} =} gmdistribution (@var{mu}, @var{Sigma}, @var{p}, @var{extra}) ## ## Create a Gaussian mixture distribution. ## ## @var{mu} is a k-by-d matrix holding the mean of each of the k ## components, one per row, where d is the number of variables. ## ## @var{Sigma} holds the covariances in one of four forms. A full ## covariance per component is d-by-d-by-k and a diagonal one per ## component is 1-by-d-by-k, while a single d-by-d matrix or a single ## 1-by-d row of variances is shared by every component. The form given ## sets @code{CovarianceType} and @code{SharedCovariance}. ## ## @var{p} is a vector of k mixing proportions, scaled to sum to 1. A ## proportion may not be negative and they may not all be zero. When ## @var{p} is omitted or empty the components are equally weighted. ## ## @var{extra} carries the results of a fit and is passed by ## @code{fitgmdist}. It fills @code{AIC}, @code{BIC}, ## @code{Converged}, @code{NegativeLogLikelihood}, ## @code{NumIterations} and @code{RegularizationValue}, which stay ## empty for an object built by hand. ## ## @end deftypefn function obj = gmdistribution (mu,sigma,p = [],extra = []) obj.DistributionName = 'gaussian mixture distribution'; obj.mu = mu; obj.Sigma = sigma; obj.NumComponents = rows (mu); obj.NumVariables = columns (mu); if (isempty (p)) obj.ComponentProportion = ones (1,obj.NumComponents) / ... obj.NumComponents; else if any (p < 0) error ("gmdistribution: component weights must be non-negative"); endif s = sum (p); if (s == 0) error ("gmdistribution: component weights must not be all zero"); elseif (s != 1) p = p / s; endif obj.ComponentProportion = p(:)'; endif if (length (size (sigma)) == 3) obj.SharedCovariance = false; else obj.SharedCovariance = true; endif if (rows (sigma) == 1 && columns (mu) > 1) obj.DiagonalCovariance = true; obj.CovarianceType = 'diagonal'; else obj.DiagonalCovariance = false; ## full obj.CovarianceType = 'full'; endif if (! isempty (extra)) obj.AIC = extra.AIC; obj.BIC = extra.BIC; obj.Converged = extra.Converged; obj.NegativeLogLikelihood = extra.NegativeLogLikelihood; obj.NlogL = extra.NegativeLogLikelihood; obj.NumIterations = extra.NumIterations; obj.RegularizationValue = extra.RegularizationValue; endif endfunction ## -*- texinfo -*- ## @deftypefn {gmdistribution} {@var{c} =} cdf (@var{obj}, @var{X}) ## ## Cumulative distribution function of a Gaussian mixture distribution. ## ## @var{X} is an n-by-d matrix of points at which the distribution is ## evaluated, one point per row, where d is the number of variables of ## @var{obj}. @var{c} is an n-by-1 vector holding the value of the ## cumulative distribution function at each of them, the components' ## cumulative distributions summed with the mixing proportions in ## @code{ComponentProportion} as weights. ## ## @end deftypefn function c = cdf (obj, X) X = checkX (obj, X, 'cdf'); p_x_l = zeros (rows (X), obj.NumComponents); if (obj.SharedCovariance) if (obj.DiagonalCovariance) sig = diag (obj.Sigma); else sig = obj.Sigma; endif endif for i = 1:obj.NumComponents if (! obj.SharedCovariance) if (obj.DiagonalCovariance) sig = diag (obj.Sigma(:,:,i)); else sig = obj.Sigma(:,:,i); endif endif p_x_l(:,i) = mvncdf (X,obj.mu(i,:),sig)*obj.ComponentProportion(i); endfor c = sum (p_x_l, 2); endfunction ## -*- texinfo -*- ## @deftypefn {gmdistribution} {@var{idx} =} cluster (@var{obj}, @var{X}) ## @deftypefnx {gmdistribution} {[@var{idx}, @var{nlogl}] =} cluster (@var{obj}, @var{X}) ## @deftypefnx {gmdistribution} {[@var{idx}, @var{nlogl}, @var{P}] =} cluster (@var{obj}, @var{X}) ## @deftypefnx {gmdistribution} {[@var{idx}, @var{nlogl}, @var{P}, @var{logpdf}] =} cluster (@var{obj}, @var{X}) ## @deftypefnx {gmdistribution} {[@var{idx}, @var{nlogl}, @var{P}, @var{logpdf}, @var{M}] =} cluster (@var{obj}, @var{X}) ## ## Assign each observation to a mixture component. ## ## @var{X} is an n-by-d matrix of observations, one per row, where d is ## the number of variables of @var{obj}. Each is assigned to the ## component under which it is most probable. ## ## @var{idx} is an n-by-1 vector of component indices. @var{nlogl} is ## the negative log-likelihood of @var{X} under the mixture. @var{P} is ## an n-by-k matrix of posterior probabilities, one column per component, ## whose rows sum to one. @var{logpdf} is an n-by-1 vector holding the ## logarithm of the mixture density at each observation. @var{M} is an ## n-by-k matrix of squared Mahalanobis distances from each observation ## to each component mean. ## ## @end deftypefn function [idx, nlogl, P, logpdf, M] = cluster (obj,X) X = checkX (obj, X, 'cluster'); [p_x_l, M] = componentProb (obj, X); [~, idx] = max (p_x_l, [], 2); if (nargout >= 2) PDF = sum (p_x_l, 2); logpdf = log (PDF); nlogl = -sum (logpdf); if (nargout >= 3) P = bsxfun (@rdivide, p_x_l, PDF); endif endif endfunction ## -*- texinfo -*- ## @deftypefn {gmdistribution} {@var{D} =} mahal (@var{obj}, @var{X}) ## ## Squared Mahalanobis distance to each mixture component. ## ## @var{X} is an n-by-d matrix of observations, one per row, where d is ## the number of variables of @var{obj}. @var{D} is an n-by-k matrix ## holding the squared Mahalanobis distance from each observation to the ## mean of each of the k components, measured in the covariance of the ## component it is taken to. ## ## @end deftypefn function D = mahal (obj,X) X = checkX (obj, X, 'mahal'); [~, D] = componentProb (obj,X); endfunction ## -*- texinfo -*- ## @deftypefn {gmdistribution} {@var{c} =} pdf (@var{obj}, @var{X}) ## ## Probability density function of a Gaussian mixture distribution. ## ## @var{X} is an n-by-d matrix of points at which the density is ## evaluated, one point per row, where d is the number of variables of ## @var{obj}. @var{c} is an n-by-1 vector holding the density at each of ## them, the components' densities summed with the mixing proportions in ## @code{ComponentProportion} as weights. ## ## @end deftypefn function c = pdf (obj,X) X = checkX (obj, X, 'pdf'); p_x_l = componentProb (obj, X); c = sum (p_x_l, 2); endfunction ## -*- texinfo -*- ## @deftypefn {gmdistribution} {@var{c} =} posterior (@var{obj}, @var{X}) ## ## Posterior probability of each mixture component. ## ## @var{X} is an n-by-d matrix of observations, one per row, where d is ## the number of variables of @var{obj}. @var{c} is an n-by-k matrix ## whose (i,j) element is the probability that observation i was drawn ## from component j, so its rows sum to one. ## ## @end deftypefn function c = posterior (obj,X) X = checkX (obj, X, 'posterior'); p_x_l = componentProb (obj, X); c = bsxfun (@rdivide, p_x_l, sum (p_x_l, 2)); endfunction ## -*- texinfo -*- ## @deftypefn {gmdistribution} {@var{c} =} random (@var{obj}) ## @deftypefnx {gmdistribution} {@var{c} =} random (@var{obj}, @var{n}) ## ## Random numbers from a Gaussian mixture distribution. ## ## @var{n} is the number of observations to draw and defaults to 1. ## @var{c} is an @var{n}-by-d matrix holding one observation per row, ## where d is the number of variables of @var{obj}. Each row is drawn ## from a component chosen with probability @code{ComponentProportion}. ## ## @end deftypefn function c = random (obj,n) if nargin == 1 n = 1; endif c = zeros (n, obj.NumVariables); classes = randsample (obj.NumComponents, n, true, ... obj.ComponentProportion); if (obj.SharedCovariance) if (obj.DiagonalCovariance) sig = diag (obj.Sigma); else sig = obj.Sigma; endif endif for i = 1:obj.NumComponents idx = (classes == i); k = sum (idx); if (k > 0) if (! obj.SharedCovariance) if (obj.DiagonalCovariance) sig = diag (obj.Sigma(:,:,i)); else sig = obj.Sigma(:,:,i); endif endif # [sig] forces [sig] not to have class "diagonal", # since mvnrnd uses automatic broadcast, # which fails on structured matrices c(idx,:) = mvnrnd ([obj.mu(i,:)], [sig], k); endif endfor endfunction endmethods ######################################## methods (Hidden) function c = disp (obj) msg = ['Gaussian mixture distribution with %d ', ... "components in %d dimension(s)\n"]; fprintf (msg, obj.NumComponents, columns (obj.mu)); for i = 1:obj.NumComponents fprintf ("Clust %d: weight %d\n\tMean: ", ... i, obj.ComponentProportion(i)); fprintf ("%g ", obj.mu(i,:)); fprintf ("\n"); if (! obj.SharedCovariance) fprintf ("\tVariance:"); if (! obj.DiagonalCovariance) if (columns (obj.mu) > 1) fprintf ("\n"); endif disp (squeeze (obj.Sigma(:,:,i))) else fprintf (" diag("); fprintf ("%g ", obj.Sigma(:,:,i)); fprintf (")\n"); endif endif endfor if (obj.SharedCovariance) fprintf ("Shared variance\n"); if (! obj.DiagonalCovariance) obj.Sigma else fprintf (" diag("); fprintf ("%g ", obj.Sigma); fprintf (")\n"); endif endif if (! isempty (obj.AIC)) fprintf ("AIC=%g BIC=%g NLogL=%g Iter=%d Cged=%d Reg=%g\n", ... obj.AIC, obj.BIC, obj.NegativeLogLikelihood, ... obj.NumIterations, obj.Converged, obj.RegularizationValue); endif endfunction function c = display (obj) disp (obj); endfunction endmethods ######################################## methods(Static) ## -*- texinfo -*- ## @deftypefn {gmdistribution} {@var{obj} =} fit (@var{X}, @var{k}) ## @deftypefnx {gmdistribution} {@var{obj} =} fit (@var{X}, @var{k}, @var{Name}, @var{Value}) ## ## Fit a Gaussian mixture distribution to data. ## ## @var{X} is an n-by-d matrix of observations, one per row, and @var{k} ## is the number of components to fit. Any @var{Name}-@var{Value} pair ## accepted by @code{fitgmdist} may follow, which is the function this ## method calls and where the options are documented. @var{obj} is the ## fitted @code{gmdistribution} object. ## ## @end deftypefn function c = fit (X, k, varargin) c = fitgmdist (X, k, varargin{:}); endfunction endmethods ######################################## methods(Access = private) ## Probability density of (row of) X *and* component l ## Second argument is an array of the Mahalanobis distances function [p_x_l, M] = componentProb (obj, X) M = zeros (rows (X), obj.NumComponents); dets = zeros (1, obj.NumComponents); # sqrt(determinant) if (obj.SharedCovariance) if (obj.DiagonalCovariance) r = diag (sqrt (obj.Sigma)); else r = chol (obj.Sigma); endif endif for i = 1:obj.NumComponents dev = bsxfun (@minus, X, obj.mu(i,:)); if (! obj.SharedCovariance) if (obj.DiagonalCovariance) r = diag (sqrt (obj.Sigma(:,:,i))); else r = chol (obj.Sigma(:,:,i)); endif endif M(:,i) = sumsq (dev / r, 2); dets(i) = prod (diag (r)); endfor p_x_l = exp (-M/2); coeff = obj.ComponentProportion ./ ... ((2 * pi) ^ (obj.NumVariables / 2) .* dets); p_x_l = bsxfun (@times, p_x_l, coeff); endfunction ######################################## ## Check format of argument X function X = checkX (obj, X, name) if (columns (X) != obj.NumVariables) if (columns (X) == 1 && rows (X) == obj.NumVariables) X = X'; else error ("gmdistribution.%s: X has %d columns instead of %d\n", ... name, columns (X), obj.NumVariables); endif endif endfunction endmethods endclassdef %!test %! mu = eye (2); %! Sigma = eye (2); %! GM = gmdistribution (mu, Sigma); %! density = GM.pdf ([0 0; 1 1]); %! assert_equal (density(1) - density(2), 0, 1e-6); %! %! [idx, nlogl, P, logpdf,M] = cluster (GM, eye (2)); %! assert_equal (idx, [1; 2]); %! [idx2,nlogl2,P2,logpdf2] = GM.cluster (eye (2)); %! assert_equal (nlogl - nlogl2, 0, 1e-6); %! [idx3,nlogl3,P3] = cluster (GM, eye (2)); %! assert_equal (P - P3, zeros (2), 1e-6); %! [idx4,nlogl4] = cluster (GM, eye (2)); %! assert_equal (size (nlogl4), [1 1]); %! idx5 = cluster (GM, eye (2)); %! assert_equal (idx - idx5, zeros (2,1)); %! %! D = GM.mahal ([1;0]); %! assert_equal (D - M(1,:), zeros (1,2), 1e-6); %! %! P = GM.posterior ([0 1]); %! assert_equal (P - P2(2,:), zeros (1,2), 1e-6); %! %! R = GM.random(20); %! assert_equal (size (R), [20, 2]); %! %! R = GM.random(); %! assert_equal (size (R), [1, 2]); statistics-release-2.1.2/inst/Clustering/inconsistent.m000066400000000000000000000103031527311104300233100ustar00rootroot00000000000000## Copyright (C) 2020-2021 Stefano Guidoni ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Y} =} inconsistent (@var{Z}) ## @deftypefnx {statistics} {@var{Y} =} inconsistent (@var{Z}, @var{d}) ## ## Compute the inconsistency coefficient for each link of a hierarchical cluster ## tree. ## ## Given a hierarchical cluster tree @var{Z} generated by the @code{linkage} ## function, @code{inconsistent} computes the inconsistency coefficient for each ## link of the tree, using all the links down to the @var{d}-th level below that ## link. ## ## The default depth @var{d} is 2, which means that only two levels are ## considered: the level of the computed link and the level below that. ## ## Each row of @var{Y} corresponds to the row of same index of @var{Z}. ## The columns of @var{Y} are respectively: the mean of the heights of the links ## used for the calculation, the standard deviation of the heights of those ## links, the number of links used, the inconsistency coefficient. ## ## @strong{Reference} ## Jain, A., and R. Dubes. Algorithms for Clustering Data. ## Upper Saddle River, NJ: Prentice-Hall, 1988. ## ## @seealso{cluster, clusterdata, dendrogram, linkage, pdist, squareform} ## ## @end deftypefn function Y = inconsistent (Z, d = 2) ## check the input if (nargin < 1) || (nargin > 2) print_usage (); endif ## MATLAB compatibility: ## when d = 0, which does not make sense, the result of inconsistent is the ## same as d = 1, which is... inconsistent if ((d < 0) || (! isscalar (d)) || (mod (d, 1))) error ("inconsistent: d must be a positive integer scalar"); endif if ((columns (Z) != 3) || (! isnumeric (Z)) || ... (! (max (Z(end, 1:2)) == rows (Z) * 2))) error (strcat ("inconsistent: Z must be a matrix generated by the linkage", " function")); endif ## number of observations n = rows (Z) + 1; ## compute the inconsistency coefficient for every link for i = 1:rows (Z) v = inconsistent_recursion (i, d); # nested recursive function - see below Y(i, 1) = mean (v); Y(i, 2) = std (v); Y(i, 3) = length (v); ## the inconsistency coefficient is (current_link_height - mean) / std; ## if the standard deviation is zero, it is zero by definition if (Y(i, 2) != 0) Y(i, 4) = (v(end) - Y(i, 1)) / Y(i, 2); else Y(i, 4) = 0; endif endfor ## recursive function ## while depth > 1 search the links (columns 1 and 2 of Z) below the current ## link and then append the height of the current link to the vector v. ## The height of the starting link should be the last one of the vector. function v = inconsistent_recursion (index, depth) v = []; if (depth > 1) for j = 1:2 if (Z(index, j) > n) new_index = Z(index, j) - n; v = [v (inconsistent_recursion (new_index, depth - 1))]; endif endfor endif v(end+1) = Z(index, 3); endfunction endfunction ## Test input validation %!error inconsistent () %!error inconsistent ([1 2 1], 2, 3) %!error inconsistent (ones (2, 2)) %!error inconsistent ([1 2 1], -1) %!error inconsistent ([1 2 1], 1.3) %!error inconsistent ([1 2 1], [1 1]) %!error inconsistent (ones (2, 3)) ## Test output %!test %! load fisheriris; %! Z = linkage (meas, 'average', 'chebychev'); %! assert_equal (cond (inconsistent (Z)), 39.9, 1e-3); statistics-release-2.1.2/inst/Clustering/kmeans.m000066400000000000000000001030301527311104300220460ustar00rootroot00000000000000## Copyright (C) 2011 Soren Hauberg ## Copyright (C) 2012 Daniel Ward ## Copyright (C) 2015-2016 Lachlan Andrew ## Copyright (C) 2016 Michael Bentley ## Copyright (C) 2021 Stefano Guidoni ## Copyright (C) 2022-2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{idx} =} kmeans (@var{data}, @var{k}) ## @deftypefnx {statistics} {[@var{idx}, @var{centers}] =} kmeans (@var{data}, @var{k}) ## @deftypefnx {statistics} {[@var{idx}, @var{centers}, @var{sumd}] =} kmeans (@var{data}, @var{k}) ## @deftypefnx {statistics} {[@var{idx}, @var{centers}, @var{sumd}, @var{dist}] =} kmeans (@var{data}, @var{k}) ## @deftypefnx {statistics} {[@dots{}] =} kmeans (@var{data}, @var{k}, @var{param1}, @var{value1}, @dots{}) ## @deftypefnx {statistics} {[@dots{}] =} kmeans (@var{data}, [], @qcode{'Start'}, @var{start}, @dots{}) ## ## Perform a @var{k}-means clustering of the @math{N*D} matrix @var{data}. ## ## If parameter @qcode{'Start'} is specified, then @var{k} may be empty ## in which case @var{k} is set to the number of rows of @var{start}. ## ## The outputs are: ## ## @multitable @columnfractions 0.15 0.8 ## @item @var{idx} @tab An @math{N*1} vector whose @math{i}-th element is ## the class to which row @math{i} of @var{data} is assigned. ## ## @item @var{centers} @tab A @math{K*D} array whose @math{i}-th row is the ## centroid of cluster @math{i}. ## ## @item @var{sumd} @tab A @math{k*1} vector whose @math{i}-th entry is the ## sum of the distances from samples in cluster @math{i} to centroid @math{i}. ## ## @item @var{dist} @tab An @math{N*k} matrix whose @math{i}@math{j}-th ## element is the distance from sample @math{i} to centroid @math{j}. ## @end multitable ## ## The following parameters may be placed in any order. Each parameter ## must be followed by its value, as in Name-Value pairs. ## ## @multitable @columnfractions 0.15 0.83 ## @headitem Name @tab Description ## @item @qcode{'Start'} @tab The initialization method for the centroids. ## @end multitable ## ## @multitable @columnfractions 0.19 0.75 ## @headitem Value @tab Description ## @item @qcode{'plus'} @tab The k-means++ algorithm. (Default) ## @item @qcode{'sample'} @tab A subset of @math{k} rows from ## @var{data}, sampled uniformly without replacement. ## @item @qcode{'cluster'} @tab Perform a pilot clustering on 10% of ## the rows of @var{data}. ## @item @qcode{'uniform'} @tab Each component of each centroid is ## drawn uniformly from the interval between the maximum and minimum values of ## that component within @var{data}. This performs poorly and is implemented ## only for Matlab compatibility. ## @item @var{numeric matrix} @tab A @math{k*D} matrix of centroid ## starting locations. The rows correspond to seeds. ## @item @var{numeric array} @tab A @math{k*D*r} array of centroid ## starting locations. The third dimension invokes replication of the ## clustering routine. Page @math{r} contains the set of seeds for replicate ## @math{r}. @qcode{kmeans} infers the number of replicates (specified by the ## @qcode{'Replicates'} Name-Value pair argument) from the size of the third ## dimension. ## @end multitable ## ## @multitable @columnfractions 0.15 0.838 ## @headitem Name @tab Description ## @item @qcode{'Distance'} @tab The distance measure used for partitioning ## and calculating centroids. ## @end multitable ## ## @multitable @columnfractions 0.19 0.75 ## @headitem Value @tab Description ## @item @qcode{'sqeuclidean'} @tab The squared Euclidean distance. ## i.e. the sum of the squares of the differences between corresponding ## components. In this case, the centroid is the arithmetic mean of all samples ## in its cluster. This is the only distance for which this algorithm is truly ## "k-means". ## @item @qcode{'cityblock'} @tab The sum metric, or L1 distance, ## i.e. the sum of the absolute differences between corresponding components. ## In this case, the centroid is the median of all samples in its cluster. ## This gives the k-medians algorithm. ## @item @qcode{'cosine'} @tab One minus the cosine of the included ## angle between points (treated as vectors). Each centroid is the mean of the ## points in that cluster, after normalizing those points to unit Euclidean ## length. ## @item @qcode{'correlation'} @tab One minus the sample correlation ## between points (treated as sequences of values). Each centroid is the ## component-wise mean of the points in that cluster, after centering and ## normalizing those points to zero mean and unit standard deviation. ## @item @qcode{'hamming'} @tab The number of components in which the ## sample and the centroid differ. In this case, the centroid is the median of ## all samples in its cluster. Unlike Matlab, Octave allows non-logical ## @var{data}. ## @end multitable ## ## @multitable @columnfractions 0.19 0.79 ## @headitem Name @tab Description ## @item @qcode{'EmptyAction'} @tab What to do when a centroid is not the ## closest to any data sample. ## @end multitable ## ## @multitable @columnfractions 0.19 0.75 ## @headitem Value @tab Description ## @item @qcode{'error'} @tab Throw an error. ## @item @qcode{'singleton'} @tab (Default) Select the row of ## @var{data} that has the highest error and use that as the new centroid. ## @item @qcode{'drop'} @tab Remove the centroid, and continue ## computation with one fewer centroid. The dimensions of the outputs ## @var{centroids} and @var{d} are unchanged, with values for omitted centroids ## replaced by NaN. ## @end multitable ## ## @multitable @columnfractions 0.15 0.838 ## @headitem Name @tab Description ## @item @qcode{'Display'} @tab Display a text summary. ## @end multitable ## ## @multitable @columnfractions 0.19 0.75 ## @headitem Value @tab Description ## @item @qcode{'off'} @tab (Default) Display no summary. ## @item @qcode{'final'} @tab Display a summary for each clustering ## operation. ## @item @qcode{'iter'} @tab Display a summary for each iteration of a ## clustering operation. ## @end multitable ## ## @multitable @columnfractions 0.15 0.838 ## @headitem Name @tab Value ## @item @qcode{'Replicates'} @tab A positive integer specifying the number ## of independent clusterings to perform. The output values are the values for ## the best clustering, i.e., the one with the smallest value of @var{sumd}. ## If @qcode{'Start'} is numeric, then @qcode{'Replicates'} defaults to ## (and must equal) the size of the third dimension of @var{start}. ## Otherwise it defaults to 1. ## @item @qcode{'MaxIter'} @tab The maximum number of iterations to perform ## for each replicate, a non-negative integer. If the maximum change of any ## centroid is less than 0.001, then the replicate terminates even if ## @qcode{'MaxIter'} iterations have not occurred. The default is 100. A ## replicate stopped by @qcode{'MaxIter'} returns the assignment its last ## centroids were computed from, with the sums of distances to them, as MATLAB ## does, so the centroids are always the means of the returned clusters. A ## value of 0 returns the starting centroids and the assignment to them, ## where MATLAB runs one iteration. ## @item @qcode{'Options'} @tab A structure, such as @code{statset} returns, ## whose @qcode{MaxIter} and @qcode{Display} fields are used when the ## @qcode{'MaxIter'} and @qcode{'Display'} options are not given; its other ## fields are ignored. ## @end multitable ## ## Example: ## ## [~, c] = kmeans (rand (10, 3), 2, 'EmptyAction', 'singleton'); ## ## @seealso{linkage} ## @end deftypefn function [classes, centers, sumd, D] = kmeans (data, k, varargin) ## Parse optional Name-Value paired arguments optNames = {'EmptyAction', 'Start', 'MaxIter', 'Distance', ... 'Replicates', 'Display', 'Options'}; dfValues = {'singleton', 'plus', 100, 'sqeuclidean', 1, 'off', []}; [emptyaction, start, max_iter, distance, replicates, display, options, ... args] = parsePairedArguments (optNames, dfValues, varargin(:)); ## The 'Start' check below distinguishes a 'Replicates' the caller gave from ## the default, which the parsed value cannot show on its own; 'Options' ## likewise yields to a 'MaxIter' or 'Display' given by name. optargNames = varargin(1:2:end); given = @(name) any (cellfun (@(x) ischar (x) && strcmpi (x, name), ... optargNames)); replicates_set_explicitly = given ('Replicates'); if (! isempty (options)) if (! (isstruct (options) && isscalar (options))) error ("kmeans: 'Options' must be a structure."); endif if (! given ('MaxIter') && isfield (options, 'MaxIter') && ! isempty (options.MaxIter)) max_iter = options.MaxIter; endif if (! given ('Display') && isfield (options, 'Display') && ! isempty (options.Display)) display = options.Display; endif endif ## Input checking, validate the matrix if (! (isnumeric (data) && ismatrix (data) && isreal (data))) error ("kmeans: first input argument must be a DxN real data matrix."); endif ## Remove rows containing NaN / NA, but record which rows are used data_idx = ! any (isnan (data), 2); original_rows = rows (data); data = data(data_idx,:); if (isempty (data)) error ("kmeans: DATA must not be empty."); endif ## Number of samples, counted after the missing rows have been removed n_rows = rows (data); ## Anything left over is an unimplemented option, which is ignored with a ## warning, a known name given without a value, or an unknown name. while (! isempty (args)) name = args{1}; if (! ischar (name)) error ("kmeans: optional arguments must be Name-Value pairs."); elseif (strcmpi (name, 'OnlinePhase')) warning ("kmeans: Ignoring unimplemented option '%s'.", name); args(1:min (2, numel (args))) = []; elseif (any (strcmpi (name, optNames))) error ("kmeans: option '%s' has no argument.", name); else error ("kmeans: unknown option '%s'.", name); endif endwhile ## Process options ## check for the 'emptyaction' property switch (emptyaction) case {'singleton', 'error', 'drop'} ; otherwise d = strtrim (disp (emptyaction)); if (length (d) > 20) d = ''; endif error ("kmeans: unsupported empty cluster action parameter '%s'.", d); endswitch ## check for the 'replicates' property. An empty value asks for the ## default, as MATLAB does, so it does not count as explicitly set. if (isempty (replicates)) replicates = 1; replicates_set_explicitly = false; elseif (! isnumeric (replicates) || ! isscalar (replicates) || ! isreal (replicates) || replicates < 1) d = strtrim (disp (replicates)); if (length (d) > 20) d = ''; endif error ("kmeans: invalid number of replicates '%s'.", d); endif ## check for the 'MaxIter' property. An empty value asks for the default, ## as MATLAB does. if (isempty (max_iter)) max_iter = 100; elseif (! isnumeric (max_iter) || ! isscalar (max_iter) || ! isreal (max_iter) || max_iter < 0 || max_iter != fix (max_iter)) d = strtrim (disp (max_iter)); if (length (d) > 20) d = ''; endif error ("kmeans: invalid MaxIter '%s'.", d); endif ## check for the 'start' property switch (lower (start)) case {'sample', 'plus', 'cluster'} start = lower (start); case {'uniform'} start = 'uniform'; min_data = min (data); range = max (data) - min_data; otherwise if (! isnumeric (start)) d = strtrim (disp (start)); if (length (d) > 20) d = ''; endif error ("kmeans: invalid start parameter '%s'.", d); endif if (columns (start) != columns (data)) error (strcat ("kmeans: the 'Start' matrix must have the same", ... " number of columns as DATA.")); endif if (isempty (k)) k = rows (start); elseif (rows (start) != k) error (strcat ("kmeans: number of initializers (%d) should match", ... " the number of centroids (%d)."), rows (start), k); endif if (replicates_set_explicitly) if (replicates != size (start, 3)) error (strcat ("kmeans: the third dimension of the initializer", ... " (%d) should match the number of replicates", ... " (%d)."), size (start, 3), replicates); endif else replicates = size (start, 3); endif endswitch ## check for the 'distance' property ## dist returns the distance btwn each row of matrix x and a row vector c switch (lower (distance)) case 'sqeuclidean' dist = @(x, c) sumsq (bsxfun (@minus, x, c), 2); centroid = @(x) mean (x, 1); case 'cityblock' dist = @(x, c) sum (abs (bsxfun (@minus, x, c)), 2); centroid = @(x) median (x, 1); case 'cosine' ## Pre-normalize all data. ## (when Octave implements normr, will use data = normr (data) ) for i = 1:rows (data) data(i,:) = data(i,:) / sqrt (sumsq (data(i,:))); endfor dist = @(x, c) 1 - (x * c') ./ sqrt (sumsq (c)); centroid = @(x) mean (x, 1); ## already normalized case 'correlation' ## Pre-normalize all data. data = data - mean (data, 2); ## (when Octave implements normr, will use data = normr (data) ) for i = 1:rows (data) data(i,:) = data(i,:) / sqrt (sumsq (data(i,:))); endfor dist = @(x, c) 1 - (x * (c - mean (c))') ... ./ sqrt (sumsq (c - mean (c))); centroid = @(x) mean (x, 1); ## already normalized case 'hamming' dist = @(x, c) sum (bsxfun (@ne, x, c), 2); centroid = @(x) median (x, 1); otherwise error ("kmeans: unsupported distance parameter '%s'.", distance); endswitch ## check for the 'display' property if (! strcmp (display, 'off')) display = lower (display); switch (display) case {'off', 'final'} ; case 'iter' printf ("%6s\t%6s\t%8s\t%12s\n", 'iter', 'phase', 'num', 'sum'); otherwise error ("kmeans: invalid display parameter '%s'.", display); endswitch endif ## Done processing options ######################################## ## K is checked here because the 'Start' option may have set it above. if (isempty (k)) error ("kmeans: you must specify the number of clusters, K."); elseif (! (isscalar (k) && isnumeric (k))) error ("kmeans: K must be a numeric scalar."); elseif (k < 1 || k != fix (k)) error ("kmeans: K must be a positive integer."); elseif (k > n_rows) error ("kmeans: K cannot exceed the actual observations in DATA."); endif ## used to hold the distances from each sample to each class D = zeros (n_rows, k); best = Inf; best_centers = []; for rep = 1:replicates ## check for the 'start' property switch (lower (start)) case 'sample' idx = randperm (n_rows, k); centers = data(idx, :); case 'plus' # k-means++, by Arthur and Vassilios(?) centers(1,:) = data(randi (n_rows),:); d = inf (n_rows, 1); # Distance to nearest centroid so far for i = 2:k d = min (d, dist(data, centers(i - 1, :))); centers(i,:) = data(find (cumsum (d) > rand * sum (d), 1), :); endfor case 'cluster' idx = randperm (n_rows, max (k, ceil (n_rows / 10))); [~, centers] = kmeans (data(idx,:), k, 'start', 'sample', ... 'distance', distance); case 'uniform' # vectorised 'min_data + range .* rand' centers = bsxfun (@plus, min_data, bsxfun (@times, range, rand (k, columns (data)))); otherwise centers = start(:,:,rep); endswitch ## Run the algorithm. Each iteration moves the centroids to the means of ## their clusters and measures every point against them, reassigning the ## points only to go on, so that the centroids, the classes and the sums ## returned belong together, as in MATLAB. With no iteration, the ## starting centroids are returned with the assignment to them. [D, classes, sumd] = update_dist (data, centers, D, k, dist); total = sum (sumd); n_changes = n_rows; converged = false; iter = 0; while (iter < max_iter) iter++; ## Calculate new centroids replaced_centroids = []; ## Used by "emptyaction = singleton" for i = 1:k ## Get binary vector indicating membership in cluster i membership = (classes == i); ## Check for empty clusters if (! any (membership)) switch emptyaction ## if 'singleton', then find the point that is the ## farthest from any centroid (and not replacing an empty cluster ## from earlier in this pass) and add it to the empty cluster case 'singleton' available = setdiff (1:n_rows, replaced_centroids); [~, idx] = max (min (D(available,:)')); idx = available(idx); replaced_centroids = [replaced_centroids, idx]; classes(idx) = i; membership(idx) = 1; ## if 'drop' then set C and D to NA case 'drop' centers(i,:) = NA; D(i,:) = NA; ## if 'error' then throw the error otherwise error ("kmeans: empty cluster created."); endswitch endif ## end check for empty clusters ## update the centroids if (any (membership)) ## if we didn't "drop" the cluster centers(i, :) = centroid(data(membership, :)); endif endfor ## Distances to the new centroids, the sums for the current classes, ## and the change in their total [D, new_classes] = update_dist (data, centers, D, k, dist); for i = 1:k sumd(i) = sum (D(classes == i,i)); endfor err = total - sum (sumd); total = sum (sumd); ## display iteration status: the points moved by the last reassignment if (strcmp (display, 'iter')) printf ("%6d\t%6d\t%8d\t%12g\n", iter, 1, n_changes, total); endif ## Stop when no point would move or the total barely falls; otherwise ## reassign, unless this was the last iteration allowed n_changes = sum (new_classes != classes); if (n_changes == 0 || err <= 0.001) converged = true; break; endif if (iter < max_iter) classes = new_classes; endif endwhile ## throw a warning if the algorithm did not converge if (! converged) warning ("kmeans: failed to converge in %d iterations.", max_iter); endif if (sum (sumd) < sum (best) || isinf (best)) best = sumd; best_centers = centers; best_classes = classes; best_D = D; endif ## display final results if (strcmp (display, 'final')) printf ("Replicate %d, %d iterations, total sum of distances = %g.\n", ... rep, iter, sum (sumd)); endif endfor centers = best_centers; classes = best_classes; sumd = best; D = best_D; ## display final results if (strcmp (display, 'final') || strcmp (display, 'iter')) printf ("Best total sum of distances = %g\n", sum (sumd)); endif ## Return with equal size as inputs if (original_rows != rows (data)) final = NA (original_rows,1); final(data_idx) = classes; ## other positions already NaN / NA classes = final; endif endfunction ## Update distances, classes and sums function [D, classes, sumd] = update_dist (data, centers, D, k, dist) for i = 1:k D(:, i) = dist(data, centers(i, :)); endfor [~, classes] = min (D, [], 2); ## calculate the sum of within-class distances sumd = zeros (k, 1); for i = 1:k sumd(i) = sum (D(classes == i,i)); endfor endfunction %!demo %! ## Generate a two-cluster problem %! rng (42); %! C1 = randn (100, 2) + 1; %! C2 = randn (100, 2) - 1; %! data = [C1; C2]; %! %! ## Perform clustering %! [idx, centers] = kmeans (data, 2); %! %! ## Plot the result %! figure; %! plot (data(idx==1, 1), data(idx==1, 2), 'ro'); %! hold on; %! plot (data(idx==2, 1), data(idx==2, 2), 'bs'); %! plot (centers(:, 1), centers(:, 2), 'kv', 'markersize', 10); %! title ('A simple two-clusters example'); %! hold off; %!demo %! ## Cluster data using k-means clustering, then plot the cluster regions %! ## Load Fisher's iris data set and use the petal lengths and widths as %! ## predictors %! %! rng (42); %! load fisheriris %! X = meas(:,3:4); %! %! plot (X(:,1), X(:,2), 'k*', 'MarkerSize', 5); %! title ('Fisher''s Iris Data'); %! xlabel ('Petal Lengths (cm)'); %! ylabel ('Petal Widths (cm)'); %! %! ## Cluster the data. Specify k = 3 clusters %! [idx, C] = kmeans (X, 3); %! x1 = min (X(:,1)):0.01:max (X(:,1)); %! x2 = min (X(:,2)):0.01:max (X(:,2)); %! [x1G, x2G] = meshgrid (x1, x2); %! XGrid = [x1G(:), x2G(:)]; %! %! idx2Region = kmeans (XGrid, 3, 'MaxIter', 10, 'Start', C); %! figure; %! gscatter (XGrid(:,1), XGrid(:,2), idx2Region, ... %! [0, 0.75, 0.75; 0.75, 0, 0.75; 0.75, 0.75, 0], '..'); %! hold on; %! plot (X(:,1), X(:,2), 'k*', 'MarkerSize', 5); %! title ('Fisher''s Iris Data'); %! xlabel ('Petal Lengths (cm)'); %! ylabel ('Petal Widths (cm)'); %! legend ('Region 1', 'Region 2', 'Region 3', 'Data', 'Location', 'SouthEast'); %! hold off %!demo %! ## Partition Data into Two Clusters %! %! rng (42); %! r1 = randn (100, 2) * 0.75 + ones (100, 2); %! r2 = randn (100, 2) * 0.5 - ones (100, 2); %! X = [r1; r2]; %! %! plot (X(:,1), X(:,2), '.'); %! title ('Randomly Generated Data'); %! [idx, C] = kmeans (X, 2, 'Distance', 'cityblock', ... %! 'Replicates', 5, 'Display', 'final'); %! figure; %! plot (X(idx==1,1), X(idx==1,2), 'r.', 'MarkerSize', 12); %! hold on %! plot (X(idx==2,1), X(idx==2,2), 'b.', 'MarkerSize', 12); %! plot (C(:,1), C(:,2), 'kx', 'MarkerSize', 15, 'LineWidth', 3); %! legend ('Cluster 1', 'Cluster 2', 'Centroids', 'Location', 'NorthWest'); %! title ('Cluster Assignments and Centroids'); %! hold off %!demo %! ## Assign New Data to Existing Clusters %! %! ## Generate a training data set using three distributions %! rng (42); %! r1 = randn (100, 2) * 0.75 + ones (100, 2); %! r2 = randn (100, 2) * 0.5 - ones (100, 2); %! r3 = randn (100, 2) * 0.75; %! X = [r1; r2; r3]; %! %! ## Partition the training data into three clusters by using kmeans %! %! [idx, C] = kmeans (X, 3); %! %! ## Plot the clusters and the cluster centroids %! %! gscatter (X(:,1), X(:,2), idx, 'bgm', '***'); %! hold on %! plot (C(:,1), C(:,2), 'kx'); %! legend ('Cluster 1', 'Cluster 2', 'Cluster 3', 'Cluster Centroid') %! %! ## Generate a test data set %! r1 = randn (100, 2) * 0.75 + ones (100, 2); %! r2 = randn (100, 2) * 0.5 - ones (100, 2); %! r3 = randn (100, 2) * 0.75; %! Xtest = [r1; r2; r3]; %! %! ## Classify the test data set using the existing clusters %! ## Find the nearest centroid from each test data point by using pdist2 %! %! D = pdist2 (C, Xtest, 'euclidean'); %! [group, ~] = find (D == min (D)); %! %! ## Plot the test data and label the test data using idx_test with gscatter %! %! gscatter (Xtest(:,1), Xtest(:,2), group, 'bgm', 'ooo'); %! box on; %! legend ('Cluster 1', 'Cluster 2', 'Cluster 3', 'Cluster Centroid', ... %! 'Data classified to Cluster 1', 'Data classified to Cluster 2', ... %! 'Data classified to Cluster 3', 'Location', 'NorthWest'); %! title ('Assign New Data to Existing Clusters'); ## Test output %!test %! samples = 4; %! dims = 3; %! k = 2; %! [cls, c, d, z] = kmeans (rand (samples,dims), k, 'start', rand (k,dims, 5), %! 'emptyAction', 'singleton'); %! assert_equal (size (cls), [samples, 1]); %! assert_equal (size (c), [k, dims]); %! assert_equal (size (d), [k, 1]); %! assert_equal (size (z), [samples, k]); %!test %! samples = 4; %! dims = 3; %! k = 2; %! [cls, c, d, z] = kmeans (rand (samples,dims), [], 'start', rand (k,dims, 5), %! 'emptyAction', 'singleton'); %! assert_equal (size (cls), [samples, 1]); %! assert_equal (size (c), [k, dims]); %! assert_equal (size (d), [k, 1]); %! assert_equal (size (z), [samples, k]); %!test %! [cls, c] = kmeans ([1 0; 2 0], 2, 'start', [8,0;0,8], 'emptyaction', 'drop'); %! assert_equal (cls, [1; 1]); %! assert_equal (c, [1.5, 0; NA, NA]); %!test %! kmeans (rand (4,3), 2, 'start', rand (2,3, 5), 'replicates', 5, %! 'emptyAction', 'singleton'); %!test %! kmeans (rand (3,4), 2, 'start', 'sample', 'emptyAction', 'singleton'); %!test %! kmeans (rand (3,4), 2, 'start', 'plus', 'emptyAction', 'singleton'); %!test %! kmeans (rand (3,4), 2, 'start', 'cluster', 'emptyAction', 'singleton'); %!test %! kmeans (rand (3,4), 2, 'start', 'uniform', 'emptyAction', 'singleton'); %!test %! kmeans (rand (4,3), 2, 'distance', 'sqeuclidean', 'emptyAction', 'singleton'); %!test %! kmeans (rand (4,3), 2, 'distance', 'cityblock', 'emptyAction', 'singleton'); %!test %! kmeans (rand (4,3), 2, 'distance', 'cosine', 'emptyAction', 'singleton'); %!test %! kmeans (rand (4,3), 2, 'distance', 'correlation', 'emptyAction', 'singleton'); %!test %! kmeans (rand (4,3), 2, 'distance', 'hamming', 'emptyAction', 'singleton'); %!test %! kmeans ([1 0; 1.1 0], 2, 'start', eye (2), 'emptyaction', 'singleton'); ## An empty 'Replicates' or 'MaxIter' asks for the default, as in MATLAB %!test %! x = [1 1; 1.2 1.1; 8 8; 8.3 8.1]; %! idx = kmeans (x, 2, 'Replicates', []); %! assert_equal (numel (idx), 4); %! assert_equal (numel (unique (idx)), 2); %!test %! x = [1 1; 1.2 1.1; 8 8; 8.3 8.1]; %! idx = kmeans (x, 2, 'MaxIter', []); %! assert_equal (numel (idx), 4); %! assert_equal (numel (unique (idx)), 2); ## Rows holding a missing value are dropped, and reported as NA in IDX %!test %! idx = kmeans ([NaN; 3; 4; NaN], 2); %! assert_equal (numel (idx), 4); %! assert_equal (isna (idx([1, 4])), [true; true]); %! assert_equal (isna (idx([2, 3])), [false; false]); %! assert_equal (numel (unique (idx([2, 3]))), 2); %!test %! idx = kmeans ([1 1; NaN NaN; 8 8; 9 9], 2); %! assert_equal (isna (idx), [false; true; false; false]); %! assert_equal (idx(3), idx(4)); ## An unimplemented option is ignored with a warning %!warning ... %! kmeans ([1 1; 1.2 1.1; 8 8; 8.3 8.1], 2, 'OnlinePhase', 'on'); ## 'Options' gives MaxIter and Display unless they are given by name; values ## checked against MATLAB R2024a %!test %! load fisheriris %! S = warning ('off', 'all'); %! [~, C] = kmeans (meas, 3, 'Start', meas([1, 51, 101],:), ... %! 'Options', statset ('MaxIter', 1)); %! warning (S); %! assert_equal (C, [5.0057, 3.3698, 1.5604, 0.29057; ... %! 6.0567, 2.7967, 4.4817, 1.4467; ... %! 6.6973, 3.0324, 5.7324, 2.1], 1e-4); %!test %! load fisheriris %! [~, C] = kmeans (meas, 3, 'Start', meas([1, 51, 101],:), 'MaxIter', 50, ... %! 'Options', statset ('MaxIter', 1)); %! assert_equal (C, [5.006, 3.428, 1.462, 0.246; ... %! 5.9016, 2.7484, 4.3935, 1.4339; ... %! 6.85, 3.0737, 5.7421, 2.0711], 1e-4); %!test %! out = evalc (strcat ("kmeans ([1 1; 1.2 1.1; 8 8; 8.3 8.1], 2,", ... %! " 'Start', [1 1; 8 8],", ... %! " 'Options', statset ('Display', 'final'));")); %! assert_equal (strncmp (out, "Replicate 1,", 12), true); ## Stopped by MaxIter, the classes and sums are those the last centroids were ## computed from; values and display checked against MATLAB R2024a %!test %! load fisheriris %! S = warning ('off', 'all'); %! [idx, C, sumd] = kmeans (meas, 3, 'Start', meas([1, 51, 101],:), ... %! 'MaxIter', 1); %! warning (S); %! assert_equal ([sum(idx == 1), sum(idx == 2), sum(idx == 3)], [53, 60, 37]); %! assert_equal (sum (sumd), 96.1098007, 1e-7); %! assert_equal (C, [mean(meas(idx == 1,:)); mean(meas(idx == 2,:)); ... %! mean(meas(idx == 3,:))], 1e-12); %!test %! load fisheriris %! S = warning ('off', 'all'); %! [idx, ~, sumd] = kmeans (meas, 3, 'Start', meas([1, 51, 101],:), ... %! 'MaxIter', 2); %! warning (S); %! assert_equal ([sum(idx == 1), sum(idx == 2), sum(idx == 3)], [50, 62, 38]); %! assert_equal (sum (sumd), 79.3554652, 1e-7); %!test %! load fisheriris %! out = evalc (strcat ("kmeans (meas, 3, 'Start', meas([1, 51, 101],:),", ... %! " 'Display', 'final');")); %! assert_equal (out, ["Replicate 1, 3 iterations, total sum of distances", ... %! " = 78.8514.\nBest total sum of distances = 78.8514\n"]); %!test %! load fisheriris %! out = evalc (strcat ("kmeans (meas, 3, 'Start', meas([1, 51, 101],:),", ... %! " 'Display', 'iter');")); %! assert_equal (out, [" iter\t phase\t num\t sum\n", ... %! " 1\t 1\t 150\t 96.1098\n", ... %! " 2\t 1\t 14\t 79.3555\n", ... %! " 3\t 1\t 2\t 78.8514\n", ... %! "Best total sum of distances = 78.8514\n"]); ## No iteration returns the starting centroids and the assignment to them %!test %! X = [1 1; 2 2; 3 3; 9 9; 9.5 9.6; 20 20]; %! S = warning ('off', 'all'); %! [idx, C] = kmeans (X, 3, 'MaxIter', 0, 'Start', [1 1; 2 2; 3 3]); %! warning (S); %! assert_equal (C, [1 1; 2 2; 3 3]); %! assert_equal (idx, [1; 2; 3; 3; 3; 3]); ## A replicate that runs out of iterations warns %!warning ... %! kmeans ([1 1; 2 2; 3 3; 9 9; 9.5 9.6; 20 20], 3, 'MaxIter', 1, ... %! 'Start', [1 1; 2 2; 3 3]); ## Test input validation %!error ... %! kmeans ('a', 1) %!error ... %! kmeans (rand (2, 2, 2), 1) %!error ... %! kmeans ([2i; 2; 2], 1) %!error kmeans ([], 1) %!error kmeans (NaN (5, 2), 3) %!error ... %! kmeans (rand (5, 2), []) %!error ... %! kmeans (rand (5, 2), [], 'start', 'plus') %!error kmeans (rand (5, 2), {3}) %!error kmeans (rand (5, 2), 't') %!error kmeans (rand (5, 2), 1.5) %!error kmeans (rand (5, 2), 0) %!error kmeans (rand (5, 2), -1) %!error ... %! kmeans ([NaN; 3; 4; NaN], 3) %!error ... %! kmeans (rand (3, 2), 5) %!error ... %! kmeans (rand (4, 3), 2, 'Distance') %!error kmeans (rand (4, 3), 2, 'bogus', 1) %!error ... %! kmeans (rand (4, 3), 2, 42) %!error ... %! kmeans (rand (4, 3), 2, 'Start', rand (3, 3)) %!error ... %! kmeans (rand (4, 3), 2, 'Display', 'verbose') %!error ... %! kmeans ([1 0; 1.1 0], 2, 'start', eye (2), 'emptyaction', 'panic') %!error ... %! kmeans (rand (4,3), 2, 'start', rand (2,3, 5), 'replicates', 1) %!error ... %! kmeans (rand (4,3), 2, 'start', rand (2,2)) %!error ... %! kmeans (rand (4,3), 2, 'distance', 'manhattan') %!error ... %! kmeans (rand (3,4), 2, 'start', 'normal') %!error ... %! kmeans (rand (4,3), 2, 'replicates', i) %!error ... %! kmeans (rand (4,3), 2, 'replicates', -1) %!error ... %! kmeans (rand (4,3), 2, 'replicates', [1 2]) %!error ... %! kmeans (rand (4,3), 2, 'replicates', 'one') %!error ... %! kmeans (rand (4,3), 2, 'MAXITER', i) %!error ... %! kmeans (rand (4,3), 2, 'MaxIter', -1) %!error ... %! kmeans (rand (4,3), 2, 'MaxIter', 1.5) %!error ... %! kmeans (rand (4,3), 2, 'Options', 1) %!error ... %! kmeans (rand (4,3), 2, 'maxiter', [1 2]) %!error ... %! kmeans (rand (4,3), 2, 'maxiter', 'one') %!error ... %! kmeans ([1 0; 1.1 0], 2, 'start', eye (2), 'emptyaction', 'error') statistics-release-2.1.2/inst/Clustering/kmedoids.m000066400000000000000000000474511527311104300224050ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{idx} =} kmedoids (@var{X}, @var{k}) ## @deftypefnx {statistics} {[@var{idx}, @var{C}] =} kmedoids (@var{X}, @var{k}) ## @deftypefnx {statistics} {[@var{idx}, @var{C}, @var{sumd}] =} kmedoids (@var{X}, @var{k}) ## @deftypefnx {statistics} {[@var{idx}, @var{C}, @var{sumd}, @var{D}] =} kmedoids (@var{X}, @var{k}) ## @deftypefnx {statistics} {[@var{idx}, @var{C}, @var{sumd}, @var{D}, @var{midx}] =} kmedoids (@var{X}, @var{k}) ## @deftypefnx {statistics} {[@var{idx}, @var{C}, @var{sumd}, @var{D}, @var{midx}, @var{info}] =} kmedoids (@var{X}, @var{k}) ## @deftypefnx {statistics} {[@dots{}] =} kmedoids (@dots{}, @var{name}, @var{value}) ## ## Partition observations into @var{k} clusters using the k-medoids algorithm. ## ## @code{@var{idx} = kmedoids (@var{X}, @var{k})} partitions the @math{N*P} ## numeric matrix @var{X} into @var{k} clusters, each represented by one of the ## observations (its @emph{medoid}), and returns the @math{N*1} vector ## @var{idx} of cluster indices. Rows of @var{X} correspond to observations and ## columns correspond to features or variables. Unlike @code{kmeans}, whose ## centroids are the mean of each cluster, a medoid is an actual data point, ## which makes k-medoids more robust to outliers and applicable to any distance ## metric. ## ## @code{[@var{idx}, @var{C}, @var{sumd}, @var{D}, @var{midx}, @var{info}] = ## kmedoids (@dots{})} returns additional results: ## ## @multitable @columnfractions 0.18 0.8 ## @item @var{C} @tab a @math{k*P} matrix with the coordinates of the @var{k} ## medoids, one per row (@code{@var{C} = @var{X}(@var{midx},:)}). ## ## @item @var{sumd} @tab a @math{k*1} vector with the within-cluster sum of the ## distances from each point to its cluster medoid, measured with the selected ## metric. ## ## @item @var{D} @tab an @math{N*k} matrix with the distance from every ## observation to every medoid. ## ## @item @var{midx} @tab a @math{k*1} vector with the row indices into @var{X} ## of the @var{k} medoids. ## ## @item @var{info} @tab a scalar structure with the fields @qcode{'algorithm'}, ## @qcode{'start'}, @qcode{'distance'}, @qcode{'iterations'}, and ## @qcode{'bestReplicate'} describing the chosen run. ## @end multitable ## ## Additional parameters can be specified by @qcode{Name-Value} pair arguments. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Distance'} @tab the distance metric, one of ## @qcode{'sqeuclidean'} (default), @qcode{'euclidean'}, @qcode{'seuclidean'}, ## @qcode{'cityblock'}, @qcode{'minkowski'}, @qcode{'chebychev'}, ## @qcode{'cosine'}, @qcode{'correlation'}, @qcode{'hamming'}, ## @qcode{'jaccard'}, @qcode{'spearman'}, @qcode{'mahalanobis'}, or a custom ## distance function handle accepted by @code{pdist2}. ## ## @item @qcode{'Algorithm'} @tab the optimization algorithm, either ## @qcode{'pam'} (default) for Partitioning Around Medoids, each iteration of ## which takes, for each medoid in turn, the swap with a non-medoid that most ## lowers the total distance, as MATLAB does, or @qcode{'small'} for the faster ## Voronoi iteration that reassigns points and re-selects each cluster medoid ## until convergence, without the online phase MATLAB adds by default. ## ## @item @qcode{'Start'} @tab the method used to choose the initial medoids: ## @qcode{'plus'} (default, k-means++), @qcode{'sample'} (a random subset of the ## observations), @qcode{'cluster'} (a preliminary pass on a subsample), or a ## @math{k*P} numeric matrix of starting medoid locations, each snapped to the ## nearest observation. A @math{k*P*R} array supplies a separate start for each ## of @var{R} replicates. ## ## @item @qcode{'Replicates'} @tab a positive integer number of times to repeat ## the clustering, each with a new set of initial medoids; the solution with the ## lowest total sum of distances is returned. The default is 1, or the size of ## the third dimension of a numeric @qcode{'Start'}. ## ## @item @qcode{'Options'} @tab a structure, as created by @code{statset}, whose ## @qcode{'MaxIter'} field caps the number of iterations (default 100). A ## @qcode{'MaxIter'} of 0 returns the starting medoids. ## @end multitable ## ## @seealso{kmeans, linkage, pdist2, dbscan} ## @end deftypefn function [idx, C, sumd, D, midx, info] = kmedoids (X, k, varargin) ## Check number of input arguments if (nargin < 2) error ("kmedoids: too few input arguments."); endif ## Validate X if (! isnumeric (X) || ! isreal (X) || ndims (X) != 2 || isempty (X)) error ("kmedoids: X must be a nonempty real numeric matrix."); endif [N, P] = size (X); ## Validate k if (! isscalar (k) || ! isnumeric (k) || ! isreal (k) || k < 1 || fix (k) != k) error ("kmedoids: K must be a positive integer scalar."); endif if (k > N) error ("kmedoids: K cannot exceed the number of observations in X."); endif ## Defaults distance = "sqeuclidean"; algorithm = "pam"; start = "plus"; replicates = []; opts = statset ("kmedoids"); ## Parse Name-Value pairs if (mod (numel (varargin), 2) != 0) error ("kmedoids: each NAME must be followed by a VALUE."); endif while (numel (varargin) > 0) name = varargin{1}; val = varargin{2}; if (! ischar (name)) error ("kmedoids: optional argument names must be strings."); endif switch (tolower (name)) case "distance" distance = val; case "algorithm" algorithm = tolower (val); case "start" start = val; case "replicates" replicates = val; case "options" if (! isstruct (val)) error ("kmedoids: 'Options' must be a structure."); endif opts = statset (opts, val); case "onlinephase" ## Accepted for compatibility; the medoid update is already exact. otherwise error ("kmedoids: unknown parameter name '%s'.", name); endswitch varargin(1:2) = []; endwhile maxiter = statget (opts, "MaxIter", 100); ## Resolve the distance metric and map it onto a pdist2 metric metrics = {"sqeuclidean", "euclidean", "seuclidean", "cityblock", ... "minkowski", "chebychev", "cosine", "correlation", "hamming", ... "jaccard", "spearman", "mahalanobis"}; if (ischar (distance)) dname = tolower (distance); if (! any (strcmp (dname, metrics))) error ("kmedoids: unsupported distance metric '%s'.", distance); endif if (strcmp (dname, "sqeuclidean")) pmetric = "squaredeuclidean"; else pmetric = dname; endif elseif (is_function_handle (distance)) dname = distance; pmetric = distance; else error ("kmedoids: DISTANCE must be a metric name or a function handle."); endif ## Validate the algorithm switch (algorithm) case {"pam", "small"} ## supported case {"clara", "large"} error ("kmedoids: the '%s' algorithm is not implemented.", algorithm); otherwise error ("kmedoids: unknown algorithm '%s'.", algorithm); endswitch ## Resolve the start method and the number of replicates if (isnumeric (start)) if (columns (start) != P) error ("kmedoids: numeric START must have the same columns as X."); endif if (rows (start) != k) error ("kmedoids: numeric START must have K rows."); endif startmode = "numeric"; rep_start = size (start, 3); if (isempty (replicates)) replicates = rep_start; elseif (replicates != rep_start) error ("kmedoids: REPLICATES must equal the pages of a numeric START."); endif else startmode = tolower (start); if (! any (strcmp (startmode, {"plus", "sample", "cluster"}))) error ("kmedoids: unknown START '%s'.", start); endif if (isempty (replicates)) replicates = 1; endif endif if (! isscalar (replicates) || ! isnumeric (replicates) || replicates < 1 || fix (replicates) != replicates) error ("kmedoids: REPLICATES must be a positive integer scalar."); endif ## Precompute the full pairwise distance matrix in the chosen metric. For ## 'sqeuclidean' this holds squared distances, so sums match MATLAB. Dall = pdist2 (X, X, pmetric); ## Run the requested number of replicates and keep the cheapest solution. best_cost = Inf; best_M = []; best_iter = 0; best_rep = 1; for rep = 1:replicates switch (startmode) case "numeric" M = snap_to_data (X, start(:,:,rep)); case "sample" M = randperm (N, k)(:); case "plus" M = kpp_init (Dall, k); case "cluster" sub = randperm (N, max (k, ceil (N / 10))); Msub = voronoi_step (Dall(sub, sub), randperm (numel (sub), k)(:), ... maxiter); M = sub(Msub)(:); endswitch if (strcmp (algorithm, "pam")) [M, iter] = pam_swap (Dall, M, maxiter); else [M, iter] = voronoi_step (Dall, M, maxiter); endif cost = sum (min (Dall(:, M), [], 2)); if (cost < best_cost) best_cost = cost; best_M = M; best_iter = iter; best_rep = rep; endif endfor M = best_M; ## Assemble the outputs from the winning medoid set. D = Dall(:, M); [mind, idx] = min (D, [], 2); midx = M(:); C = X(midx, :); sumd = accumarray (idx, mind, [k, 1]); info = struct ("algorithm", algorithm, "start", startmode, ... "distance", dname, "iterations", best_iter, ... "bestReplicate", best_rep); endfunction ## Snap each starting location to the nearest observation (Euclidean) function M = snap_to_data (X, coords) k = rows (coords); M = zeros (k, 1); for j = 1:k [~, M(j)] = min (sum ((X - coords(j,:)) .^ 2, 2)); endfor endfunction ## k-means++ style seeding on the precomputed distance matrix function M = kpp_init (Dall, k) N = rows (Dall); M = zeros (k, 1); M(1) = randi (N); d = Dall(:, M(1)); for i = 2:k total = sum (d); if (total <= 0) M(i) = randi (N); # all remaining points coincide else M(i) = find (cumsum (d) >= rand * total, 1); endif d = min (d, Dall(:, M(i))); endfor endfunction ## Partitioning Around Medoids: each iteration gives every medoid in turn the ## swap with a non-medoid that most lowers the cost, until no swap helps function [M, iter] = pam_swap (Dall, M, maxiter) N = rows (Dall); k = numel (M); curcost = sum (min (Dall(:, M), [], 2)); iter = 0; while (iter < maxiter) iter += 1; improved = false; for j = 1:k bestcost = curcost; bh = 0; ismed = false (N, 1); ismed(M) = true; for h = 1:N if (ismed(h)) continue; endif Mtry = M; Mtry(j) = h; c = sum (min (Dall(:, Mtry), [], 2)); if (c < bestcost) bestcost = c; bh = h; endif endfor if (bh > 0) M(j) = bh; curcost = bestcost; improved = true; endif endfor if (! improved) break; endif endwhile endfunction ## Voronoi iteration ('small'): reassign, then re-select each cluster's medoid function [M, iter] = voronoi_step (Dall, M, maxiter) k = numel (M); iter = 0; while (iter < maxiter) iter += 1; oldM = M; [~, lab] = min (Dall(:, M), [], 2); for j = 1:k members = find (lab == j); if (isempty (members)) continue; # keep the medoid of an empty cluster endif [~, loc] = min (sum (Dall(members, members), 1)); M(j) = members(loc); endfor if (isequal (M, oldM)) break; endif endwhile endfunction %!demo %! ## Cluster three noisy blobs and mark the medoids. %! rng (42); %! X = [randn(20,2)*0.4 + 3; randn(20,2)*0.4; randn(20,2)*0.4 + [3 -3]]; %! [idx, C] = kmedoids (X, 3); %! gscatter (X(:,1), X(:,2), idx); %! hold on; %! plot (C(:,1), C(:,2), "kp", "MarkerSize", 14, "MarkerFaceColor", "y"); %! hold off; %! title ("kmedoids: three clusters with their medoids"); ## Exact anchor: numeric Start is RNG-free, verified against MATLAB R2024a %!test %! X = [1 1; 1.2 0.8; 0.8 1.1; 10 10; 10.2 9.8; 9.8 10.1; 1 10; 1.1 10.2; ... %! 0.9 9.8]; %! S = [1 1; 10 10; 1 10]; %! [idx, C, sumd, D, midx] = kmedoids (X, 3, "Start", S); %! assert_equal (idx, [1; 1; 1; 2; 2; 2; 3; 3; 3]); %! assert_equal (midx, [1; 4; 7]); %! assert_equal (C, [1 1; 10 10; 1 10]); %! assert_equal (sumd, [0.13; 0.13; 0.10], 1e-12); %! assert_equal (C, X(midx,:)); %! assert_equal (D(6,:), [160.25 0.05 77.45], 1e-12); %! assert_equal (size (D), [9 3]); ## Default path (sqeuclidean, k-means++): partition and medoid set are unique %!test %! X = [1 1; 1.2 0.8; 0.8 1.1; 10 10; 10.2 9.8; 9.8 10.1; 1 10; 1.1 10.2; ... %! 0.9 9.8]; %! [idx, C, sumd, D, midx, info] = kmedoids (X, 3); %! assert_equal (sort (midx), [1; 4; 7]); %! assert_equal (sort (sumd), [0.10; 0.13; 0.13], 1e-12); %! assert_equal (arrayfun (@(c) numel (unique (idx(idx == c))), 1:3), [1 1 1]); %! assert_equal (info.algorithm, "pam"); %! assert_equal (info.start, "plus"); %! assert_equal (info.distance, "sqeuclidean"); ## Euclidean metric: sumd is the un-squared distance (numeric Start, exact) %!test %! X = [1 1; 1.2 0.8; 0.8 1.1; 10 10; 10.2 9.8; 9.8 10.1; 1 10; 1.1 10.2; ... %! 0.9 9.8]; %! S = [1 1; 10 10; 1 10]; %! [idx, C, sumd, D, midx] = kmedoids (X, 3, "Start", S, "Distance", ... %! "euclidean"); %! assert_equal (midx, [1; 4; 7]); %! assert_equal (sumd, [sqrt(0.08)+sqrt(0.05); sqrt(0.08)+sqrt(0.05); ... %! 2*sqrt(0.05)], 1e-12); ## Cityblock metric: sumd is the L1 distance (numeric Start, exact) %!test %! X = [1 1; 1.2 0.8; 0.8 1.1; 10 10; 10.2 9.8; 9.8 10.1; 1 10; 1.1 10.2; ... %! 0.9 9.8]; %! S = [1 1; 10 10; 1 10]; %! [idx, C, sumd, D, midx] = kmedoids (X, 3, "Start", S, "Distance", ... %! "cityblock"); %! assert_equal (midx, [1; 4; 7]); %! assert_equal (sumd, [0.7; 0.7; 0.6], 1e-12); %! assert_equal (D(1,:), [0 18 9], 1e-12); ## The 'small' Voronoi algorithm finds the same partition on separated data %!test %! X = [1 1; 1.2 0.8; 0.8 1.1; 10 10; 10.2 9.8; 9.8 10.1; 1 10; 1.1 10.2; ... %! 0.9 9.8]; %! S = [1 1; 10 10; 1 10]; %! [idx, C, sumd, D, midx, info] = kmedoids (X, 3, "Start", S, "Algorithm", ... %! "small"); %! assert_equal (midx, [1; 4; 7]); %! assert_equal (sumd, [0.13; 0.13; 0.10], 1e-12); %! assert_equal (info.algorithm, "small"); ## Two clusters, a custom number of replicates runs and returns a valid result %!test %! X = [randn(10,2) - 5; randn(10,2) + 5]; %! [idx, C, sumd, D, midx] = kmedoids (X, 2, "Replicates", 3); %! assert_equal (numel (idx), 20); %! assert_equal (all (idx >= 1 & idx <= 2, 'all'), true); %! assert_equal (C, X(midx,:)); %! assert_equal (size (D), [20 2]); ## k == number of observations places one medoid on every point %!test %! X = [0; 5; 9]; %! [idx, C, sumd, D, midx] = kmedoids (X, 3, "Start", X); %! assert_equal (sort (midx), [1; 2; 3]); %! assert_equal (sumd, [0; 0; 0], 1e-12); ## Values checked against MATLAB R2024a, from fixed starts %!test %! load fisheriris %! [~, ~, sumd, ~, midx, info] = kmedoids (meas, 3, ... %! "Start", meas([1, 51, 101],:)); %! assert_equal (midx, [8; 79; 121]); %! assert_equal (sum (sumd), 83.91, 1e-10); %! assert_equal (info.iterations, 3); %!test %! load fisheriris %! [~, ~, sumd, ~, midx] = kmedoids (meas, 3, "Start", meas([1, 51, 101],:), ... %! "Options", statset ("MaxIter", 1)); %! assert_equal (midx, [27; 79; 121]); %! assert_equal (sum (sumd), 85.76, 1e-10); %!test %! load fisheriris %! [~, ~, sumd, ~, midx, info] = kmedoids (meas, 3, "Start", ... %! meas([1, 51, 101],:), ... %! "Distance", "cityblock"); %! assert_equal (midx, [8; 56; 113]); %! assert_equal (sum (sumd), 162.5, 1e-10); %! assert_equal (info.iterations, 3); %!shared B, SB %! B = [sin((1:80)') * 5 + mod((1:80)', 3), cos((1:80)' * 1.3) * 3]; %! SB = B([3, 17, 40, 66],:); %!test %! [~, ~, sumd, ~, midx, info] = kmedoids (B, 4, "Start", SB); %! assert_equal (midx, [43; 78; 40; 37]); %! assert_equal (sum (sumd), 297.96737390268027, 1e-10); %! assert_equal (info.iterations, 3); %!test %! [~, ~, sumd, ~, midx] = kmedoids (B, 4, "Start", SB, ... %! "Options", statset ("MaxIter", 1)); %! assert_equal (midx, [73; 78; 40; 37]); %! assert_equal (sum (sumd), 302.11064611307177, 1e-10); %!test %! [~, ~, sumd, ~, midx, info] = kmedoids (B, 4, "Start", SB, ... %! "Algorithm", "small"); %! assert_equal (midx, [66; 61; 40; 73]); %! assert_equal (sum (sumd), 372.93746096722396, 1e-10); %! assert_equal (info.iterations, 4); %!test %! [~, ~, sumd, ~, midx] = kmedoids (B, 4, "Start", SB, ... %! "Algorithm", "small", ... %! "Options", statset ("MaxIter", 2)); %! assert_equal (midx, [66; 74; 40; 62]); %! assert_equal (sum (sumd), 382.94324386778817, 1e-10); %!test %! ## No iteration returns the starting medoids %! [~, ~, ~, ~, midx, info] = kmedoids (B, 4, "Start", SB, ... %! "Options", statset ("MaxIter", 0)); %! assert_equal (midx, [3; 17; 40; 66]); %! assert_equal (info.iterations, 0); %!test %! [~, ~, ~, ~, midx] = kmedoids (B, 4, "Start", SB, "Algorithm", "small", ... %! "Options", statset ("MaxIter", 0)); %! assert_equal (midx, [3; 17; 40; 66]); ## Test input validation %!error kmedoids (1) %!error kmedoids ([], 2) %!error kmedoids ("a", 2) %!error kmedoids (ones (4,2), 0) %!error kmedoids (ones (4,2), 1.5) %!error ... %! kmedoids (ones (3,2), 4) %!error ... %! kmedoids (ones (4,2), 2, "Distance") %!error ... %! kmedoids (ones (4,2), 2, "foo", "bar") %!error ... %! kmedoids (ones (4,2), 2, "Distance", "taxicab") %!error ... %! kmedoids (ones (4,2), 2, "Algorithm", "clara") %!error ... %! kmedoids (ones (4,2), 2, "Algorithm", "foo") %!error ... %! kmedoids (ones (4,2), 2, "Start", "middle") %!error ... %! kmedoids (ones (4,2), 2, "Start", [1 1; 2 2; 3 3]) %!error ... %! kmedoids (ones (4,2), 2, "Start", [1 1 1; 2 2 2]) %!error ... %! kmedoids (ones (4,2), 2, "Replicates", 0) %!error ... %! kmedoids (ones (4,2), 2, "Options", 5) statistics-release-2.1.2/inst/Clustering/linkage.m000066400000000000000000000430031527311104300222050ustar00rootroot00000000000000## Copyright (C) 2008 Francesco Potortì ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} linkage (@var{d}) ## @deftypefnx {statistics} {@var{y} =} linkage (@var{d}, @var{method}) ## @deftypefnx {statistics} {@var{y} =} linkage (@var{x}) ## @deftypefnx {statistics} {@var{y} =} linkage (@var{x}, @var{method}) ## @deftypefnx {statistics} {@var{y} =} linkage (@var{x}, @var{method}, @var{metric}) ## @deftypefnx {statistics} {@var{y} =} linkage (@var{x}, @var{method}, @var{arglist}) ## ## Produce a hierarchical clustering dendrogram. ## ## @var{d} is the dissimilarity matrix relative to n observations, ## formatted as a @math{(n-1)*n/2}x1 vector as produced by @code{pdist}. ## Alternatively, @var{x} contains data formatted for input to ## @code{pdist}, @var{metric} is a metric for @code{pdist} and ## @var{arglist} is a cell array containing arguments that are passed to ## @code{pdist}. ## ## @code{linkage} starts by putting each observation into a singleton ## cluster and numbering those from 1 to n. Then it merges two ## clusters, chosen according to @var{method}, to create a new cluster ## numbered n+1, and so on until all observations are grouped into ## a single cluster numbered 2(n-1). Row k of the ## (m-1)x3 output matrix relates to cluster n+k: the first ## two columns are the numbers of the two component clusters and column ## 3 contains their distance. ## ## When several pairs of clusters are equally close, which of them is merged ## first is not determined by the data, and the cluster numbers in the first two ## columns are therefore implementation-defined. Only column 3, the sequence of ## merge distances, is reproducible across implementations, and even that is so ## only for the methods whose recomputation rule does not depend on the merge ## order (@qcode{"weighted"}, @qcode{"centroid"} and @qcode{"median"} do depend ## on it). Code that must be portable should read column 3, or the cluster ## assignment obtained from @code{cluster}, rather than the raw numbering. ## ## @var{method} defines the way the distance between two clusters is ## computed and how they are recomputed when two clusters are merged: ## ## @table @samp ## @item "single" (default) ## Distance between two clusters is the minimum distance between two ## elements belonging each to one cluster. Produces a cluster tree ## known as minimum spanning tree. ## ## @item "complete" ## Furthest distance between two elements belonging each to one cluster. ## ## @item "average" ## Unweighted pair group method with averaging (UPGMA). ## The mean distance between all pair of elements each belonging to one ## cluster. ## ## @item "weighted" ## Weighted pair group method with averaging (WPGMA). ## When two clusters A and B are joined together, the new distance to a ## cluster C is the mean between distances A-C and B-C. ## ## @item "centroid" ## Unweighted Pair-Group Method using Centroids (UPGMC). ## Assumes Euclidean metric. The distance between cluster centroids, ## each centroid being the center of mass of a cluster. ## ## @item "median" ## Weighted pair-group method using centroids (WPGMC). ## Assumes Euclidean metric. Distance between cluster centroids. When ## two clusters are joined together, the new centroid is the midpoint ## between the joined centroids. ## ## @item "ward" ## Ward's sum of squared deviations about the group mean (ESS). ## Also known as minimum variance or inner squared distance. ## Assumes Euclidean metric. How much the moment of inertia of the ## merged cluster exceeds the sum of those of the individual clusters. ## @end table ## ## @strong{Reference} ## Ward, J. H. Hierarchical Grouping to Optimize an Objective Function ## J. Am. Statist. Assoc. 1963, 58, 236-244, ## @url{http://iv.slis.indiana.edu/sw/data/ward.pdf}. ## ## @seealso{pdist,squareform} ## @end deftypefn function dgram = linkage (d, method = 'single', distarg, savememory) ## check the input if (nargin == 4) && (strcmpi (savememory, 'savememory')) warning ("Octave:linkage_savemem", ... 'linkage: option ''savememory'' not implemented'); elseif (nargin < 1) || (nargin > 3) print_usage (); endif if (isempty (d)) error ("linkage: d cannot be empty"); endif methods = struct ... ('name', { 'single'; 'complete'; 'average'; 'weighted'; 'centroid'; 'median'; 'ward' }, 'distfunc', {(@(x) min(x)) # single (@(x) max(x)) # complete (@(x,i,j,w) sum(diag(w([i,j]))*x)/sum(w([i,j]))) # average (@(x) mean(x)) # weighted (@massdist) # centroid (@(x,i) massdist(x,i)) # median (@inertialdist) # ward }); mask = strcmp (lower (method), {methods.name}); if (! any (mask)) error ("linkage: %s: unknown method", method); endif dist = {methods.distfunc}{mask}; if (nargin >= 3 && ! isvector (d)) if (ischar (distarg)) d = pdist (d, distarg); elseif (iscell (distarg)) d = pdist (d, distarg{:}); else print_usage (); endif elseif (nargin < 3) if (! isvector (d)) d = pdist (d); endif else print_usage (); endif d = squareform (d, 'tomatrix'); # dissimilarity NxN matrix n = rows (d); # the number of observations diagidx = sub2ind ([n,n], 1:n, 1:n); # indices of diagonal elements d(diagidx) = NaN; # min passes over a cluster and itself ## For equal-distance nodes, the order in which clusters are ## merged is arbitrary. Rotating the initial matrix produces an ## ordering similar to Matlab's. cname = n:-1:1; # cluster names in d d = rot90 (d, 2); # exchange low and high cluster numbers weight = ones (1, n); # cluster weights dgram = zeros (n-1, 3); # clusters from n+1 to 2*n-1 sz = n; # current matrix size (avoid size calls) mcase = find (mask); # pre-compute method case for cluster = n+1 : 2*n-1 ## Find the two nearest clusters. An infinite distance is merged like ## any other; where every distance left is NaN, the two clusters first ## in the original order are merged, as MATLAB does [v, midx] = min (d(:)); if (isnan (v)) midx = (sz - 1) * sz; endif ## Compute row/column indices directly (faster than ind2sub) c = ceil (midx / sz); r = midx - (c - 1) * sz; ## Here is the new cluster dgram(cluster-n, :) = [cname(r) cname(c) d(r, c)]; ## Put it in place of the first one and remove the second cname(r) = cluster; cname(c) = []; ## Compute the new distances. ## (Octave-7+ needs switch stmt to avoid 'called with too many inputs' err.) d_rc = d([r, c], :); # cache row slice switch mcase case {1, 2, 4} # 1 arg newd = dist(d_rc); case {3, 5, 7} # 4 args newd = dist(d_rc, r, c, weight); case 6 # 2 args newd = dist(d_rc, r); otherwise endswitch newd(r) = NaN; # Take care of the diagonal element ## Put distances in place of the first ones, remove the second ones d(r,:) = newd; d(:,r) = newd'; d(c,:) = []; d(:,c) = []; sz -= 1; # update cached size ## The new weight is the sum of the components' weights weight(r) += weight(c); weight(c) = []; endfor ## Sort the cluster numbers, as Matlab does dgram(:,1:2) = sort (dgram(:,1:2), 2); ## Check that distances are monotonically increasing if (any (diff (dgram(:,3)) < 0)) warning ("Octave:clustering", "linkage: cluster distances do not monotonically increase\n\ you should probably use a method different from \"%s\"", method); endif endfunction ## Take two row vectors, which are the Euclidean distances of clusters I ## and J from the others. Column I of second row contains the distance ## between clusters I and J. The centre of gravity of the new cluster ## is on the segment joining the old ones. W are the weights of all ## clusters. Use the law of cosines to find the distances of the new ## cluster from all the others. function y = massdist (x, i, j, w) x .^= 2; # Squared Euclidean distances if (nargin == 2) # Median distance qi = 0.5; # Equal weights ("weighted") else # Centroid distance qi = 1 / (1 + w(j) / w(i)); # Proportional weights ("unweighted") endif y = sqrt (qi * x(1, :) + (1 - qi) * (x(2, :) - qi * x(2, i))); endfunction ## Take two row vectors, which are the inertial distances of clusters I ## and J from the others. Column I of second row contains the inertial ## distance between clusters I and J. The centre of gravity of the new ## cluster K is on the segment joining I and J. W are the weights of ## all clusters. Convert inertial to Euclidean distances, then use the ## law of cosines to find the Euclidean distances of K from all the ## other clusters, convert them back to inertial distances and return ## them. function y = inertialdist (x, i, j, w) wi = w(i); # The cluster wj = w(j); # weights. s = [wi + w; # Sum of weights for wj + w]; # all cluster pairs. p = [wi * w; # Product of weights for wj * w]; # all cluster pairs. x = x.^2 .* s ./ p; # Convert inertial dist. to squared Eucl. sij = wi + wj; # Sum of weights of I and J qi = wi / sij; # Normalise the weight of I ## Squared Euclidean distances between all clusters and new cluster K x = qi * x(1, :) + (1 - qi) * (x(2, :) - qi * x(2, i)); y = sqrt (x * sij .* w ./ (sij + w)); # convert Eucl. dist. to inertial endfunction %!shared x, t %! x = reshape (mod (magic (6),5), [], 3); %! t = 1e-6; ## Z(:,3) holds the merge heights and is the meaningful output; Z(:,1:2) are ## cluster labels whose values depend on how ties in the distance matrix are ## broken. This fixture has only 20 distinct distances among its 66 pairs, so ## the labels are implementation-defined and are deliberately not asserted. ## Do not reintroduce cond (Z) here: it mixes the labels into the metric, is ## blind to the merge order (a row permutation cannot move a matrix's singular ## values) and ranges over 32.6 to 95.5 on this fixture under nothing but a ## different tie-break. The heights below are exact against MATLAB R2024a. %!test %! Z = linkage (pdist (x)); %! assert_equal (Z(:,3), [1; 1; 1; 1.414214; 1.414214; 1.414214; ... %! 2.236068; 2.236068; 2.236068; 2.236068; 3], t); %!test %! Z = linkage (pdist (x), 'complete'); %! assert_equal (Z(:,3), [1; 1; 1.414214; 1.414214; 1.414214; 2.236068; ... %! 2.236068; 3.162278; 3.741657; 4.690416; 6], t); %!test %! Z = linkage (pdist (x), 'average'); %! assert_equal (Z(:,3), [1; 1; 1.207107; 1.414214; 1.414214; 1.962117; ... %! 2.236068; 2.948887; 3.081139; 3.515667; ... %! 4.177650], t); ## 'weighted' (McQuitty) feeds the Lance-Williams recursion with whichever pair ## merged first, so its later heights follow the tie-break. Three pairs of this ## fixture lie at distance exactly 1 and observation 10 belongs to two of them, ## so at most two can merge as singletons and the choice is forced to be ## arbitrary: we take {9,10} then {1,7}, MATLAB takes {1,7} then {4,10}, and the ## heights part company from row 8. Both replay correctly through the WPGMA ## recursion. Only the first seven merges are tie-break invariant, so only they ## are asserted by value; a tie-free fixture would restore the rest. %!test %! Z = linkage (pdist (x), 'weighted'); %! assert_equal (Z(1:7,3), [1; 1; 1.207107; 1.414214; 1.414214; ... %! 2.030604; 2.236068], t); %! assert_equal (all (diff (Z(:,3)) >= -eps), true); %! lastwarn (); # Clear last warning before the test %!warning linkage (pdist (x), 'centroid'); ## Regression values only -- 'centroid' and 'median' were not measured against ## MATLAB, so these pin our own behaviour rather than parity. Both build a ## non-monotonic tree, which is what the warning above reports. %!test %! warning off Octave:clustering %! Z = linkage (pdist (x), 'centroid'); %! assert_equal (Z(:,3), [1; 1; 1.118034; 1.414214; 1.224745; 1.885618; ... %! 2.236068; 2.708013; 2.980378; 3.041381; ... %! 3.529418], t); %! warning on Octave:clustering %!warning linkage (pdist (x), 'median'); %!test %! warning off Octave:clustering %! Z = linkage (pdist (x), 'median'); %! assert_equal (Z(:,3), [1; 1; 1.118034; 1.414214; 1.224745; 1.952562; ... %! 2.236068; 2.452677; 3.041381; 3.057394; ... %! 3.163667], t); %! warning on Octave:clustering %!test %! Z = linkage (pdist (x), 'ward'); %! assert_equal (Z(:,3), [1; 1; 1.290994; 1.414214; 1.414214; 2.236068; ... %! 2.309401; 3.511885; 4.690416; 5.228129; ... %! 7.713624], t); ## All four ward call forms must return the same tree, labels included. MATLAB ## does not manage this -- its raw-data and distance-vector paths break the ties ## differently and return different trees for this fixture, at identical merge ## heights. Ours agree exactly, so assert the whole of Z. %!test %! Z = linkage (pdist (x), 'ward'); %! assert_equal (linkage (x, 'ward', 'euclidean'), Z); %! assert_equal (linkage (x, 'ward', {'euclidean'}), Z); %! assert_equal (linkage (x, 'ward', {'minkowski', 2}), Z); ## Structural validity of Z, which no cond value could ever check. The merge ## bookkeeping is shared by every method, so one method exercises it: each of ## the 2*n-2 labels is consumed exactly once and every row is sorted. %!test %! Z = linkage (pdist (x)); %! assert_equal (sort ([Z(:,1); Z(:,2)])', 1:22); %! assert_equal (all (Z(:,1) < Z(:,2)), true); ## Additional tests for method/metric combinations %!test %! y = [1 2; 3 5; 4 6; 7 8; 9 11]; %! L = linkage (y, 'single', 'cityblock'); %! assert_equal (size (L), [4, 3]); %! assert_equal (all (L(:,3) >= 0), true); # distances non-negative %!test %! y = [1 2; 3 5; 4 6; 7 8; 9 11]; %! L = linkage (y, 'complete', 'cityblock'); %! assert_equal (size (L), [4, 3]); %! assert_equal (all (diff (L(:,3)) >= -eps), true); # monotonically increasing %!test %! y = [1 2; 3 5; 4 6; 7 8; 9 11]; %! L = linkage (y, 'average', 'chebychev'); %! assert_equal (size (L), [4, 3]); %! assert_equal (all (L(:,3) >= 0), true); %!test %! y = [1 2 3; 4 5 6; 7 8 9; 10 11 12]; %! L = linkage (y, 'weighted', {'minkowski', 3}); %! assert_equal (size (L), [3, 3]); %! assert_equal (all (L(:,3) >= 0), true); %!test %! y = [1 0 1; 0 1 1; 1 1 0; 0 0 1]; %! L = linkage (y, 'single', 'cosine'); %! assert_equal (size (L), [3, 3]); %! assert_equal (all (L(:,3) >= 0), true); %!test %! y = [1 2 3; 2 3 4; 5 6 7]; %! L = linkage (y, 'complete', 'correlation'); %! assert_equal (size (L), [2, 3]); %! assert_equal (all (L(:,3) >= 0), true); ## Test with 2 observations (minimal case) %!test %! y = [1 2; 3 4]; %! L = linkage (y, 'single', 'euclidean'); %! assert_equal (size (L), [1, 3]); %! assert_equal (L(1,1:2), [1, 2]); ## Test output structure: cluster indices are valid %!test %! y = rand (6, 3); %! L = linkage (y, 'average', 'euclidean'); %! assert_equal (all (L(:,1) >= 1 & L(:,1) <= 11), true); # valid cluster refs %! assert_equal (all (L(:,2) >= 1 & L(:,2) <= 11), true); %! assert_equal (all (L(:,1) < L(:,2)), true); # sorted within rows %!assert_equal (linkage ([1, Inf, 2], 'average'), [1, 2, 1; 3, 4, Inf]) %!assert_equal (linkage ([1, Inf, 2], 'complete'), [1, 2, 1; 3, 4, Inf]) %!assert_equal (linkage ([1, Inf, 2], 'single'), [1, 2, 1; 3, 4, 2]) %!assert_equal (linkage ([Inf, Inf, Inf], 'average'), [2, 3, Inf; 1, 4, Inf]) %!assert_equal (linkage ([1, 2, 3, Inf, Inf, 4], 'average'), ... %! [1, 2, 1; 3, 4, 4; 5, 6, Inf]) %!assert_equal (linkage ([1, NaN, 2], 'average'), [1, 2, 1; 3, 4, NaN]) %!assert_equal (linkage ([NaN, NaN, NaN], 'average'), [1, 2, NaN; 3, 4, NaN]) %!assert_equal (linkage (NaN (1, 10), 'average'), ... %! [1, 2, NaN; 3, 6, NaN; 4, 7, NaN; 5, 8, NaN]) %!assert_equal (linkage ([NaN, NaN, NaN, NaN, NaN, 1], 'average'), ... %! [3, 4, 1; 1, 2, NaN; 5, 6, NaN]) %!assert_equal (linkage ([NaN, 1, NaN(1, 8)], 'average'), ... %! [1, 3, 1; 2, 6, NaN; 4, 7, NaN; 5, 8, NaN]) %!assert_equal (linkage ([Inf, NaN(1, 5)], 'average'), ... %! [1, 2, Inf; 3, 5, NaN; 4, 6, NaN]) statistics-release-2.1.2/inst/Clustering/optimalleaforder.m000066400000000000000000000306501527311104300241300ustar00rootroot00000000000000## Copyright (C) 2021 Stefano Guidoni ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{leafOrder} =} optimalleaforder (@var{tree}, @var{D}) ## @deftypefnx {statistics} {@var{leafOrder} =} optimalleaforder (@dots{}, @var{Name}, @var{Value}) ## ## Compute the optimal leaf ordering of a hierarchical binary cluster tree. ## ## The optimal leaf ordering of a tree is the ordering which minimizes the sum ## of the distances between each leaf and its adjacent leaves, without altering ## the structure of the tree, that is without redefining the clusters of the ## tree. ## ## Required inputs: ## @itemize ## @item ## @var{tree}: a hierarchical cluster tree @var{tree} generated by the ## @code{linkage} function. ## ## @item ## @var{D}: a matrix of distances as computed by @code{pdist}. ## @end itemize ## ## Optional inputs can be the following property/value pairs: ## @itemize ## @item ## property 'Criteria' at the moment can only have the value 'adjacent', ## for minimizing the distances between leaves. ## ## @item ## property 'Transformation' can have one of the values 'linear', 'inverse' ## or a handle to a custom function which computes @var{S} the similarity ## matrix. ## @end itemize ## ## optimalleaforder's output @var{leafOrder} is the optimal leaf ordering. ## ## @strong{Reference} ## Bar-Joseph, Z., Gifford, D.K., and Jaakkola, T.S. Fast optimal leaf ordering ## for hierarchical clustering. Bioinformatics vol. 17 suppl. 1, 2001. ## ## @seealso{dendrogram,linkage,pdist} ## ## @end deftypefn function leafOrder = optimalleaforder ( varargin ) ## check the input if ( nargin < 2 ) print_usage (); endif tree = varargin{1}; D = varargin{2}; criterion = 'adjacent'; # default and only value at the moment transformation = 'linear'; if ((columns (tree) != 3) || (! isnumeric (tree)) || ... (! (max (tree(end, 1:2)) == rows (tree) * 2))) error (strcat ("optimalleaforder: tree must be a matrix as generated by the", " linkage function")); endif ## Read the paired arguments. Only the names have to be character vectors: ## 'Transformation' takes a function handle as well, and lower-casing every ## argument rejected it before it could be used. names = 3:2:(nargin - 1); if (! all (cellfun ('ischar', varargin(names)))) error ("optimalleaforder: character inputs expected for arguments 3 and up"); endif varargin(names) = lower (varargin(names)); pair_index = 3; while (pair_index <= (nargin - 1)) switch (varargin{pair_index}) case 'criteria' criterion = varargin{pair_index + 1}; if (! ischar (criterion)) error ("optimalleaforder: character inputs expected for CRITERIA"); endif criterion = lower (criterion); if (strcmp (criterion, 'group')) ## MATLAB compatibility: ## the 'group' criterion is not implemented error ("optimalleaforder: unavailable criterion 'group'"); elseif (! strcmp (criterion, 'adjacent')) error ("optimalleaforder: invalid criterion %s", criterion); endif case 'transformation' transformation = varargin{pair_index + 1}; if (ischar (transformation)) transformation = lower (transformation); elseif (! is_function_handle (transformation)) error (strcat ("optimalleaforder: TRANSFORMATION must be a", ... " character vector or a function handle")); endif otherwise error ("optimalleaforder: unknown property %s", varargin{pair_index}); endswitch pair_index += 2; endwhile ## D can be either a vector or a matrix, ## but it is easier to work with a matrix if (isvector (D)) D = squareform (D); endif n = rows (D); m = rows (tree); if (n != (m + 1)) error (strcat ("optimalleaforder: D must be a matrix or vector generated by", " the pdist function")); endif ## the similarity matrix, basically an inverted distance matrix S = zeros (n); if (strcmpi (transformation, 'linear')) ## linear similarity maxD = max (max (D)); S = maxD - D; elseif (strcmpi (transformation, 'inverse')) ## similarity as inverted distance S = 1 ./ D; elseif (is_function_handle (transformation)) ## custom similarity S = feval (transformation, D); else error ("optimalleaforder: invalid transformation %s", transformation); endif ## main body ## With two leaves the tree has a single node and the only orderings are the ## one below and its reverse, both optimal. The tables below index the root ## where a leaf is expected and run off the end of M. if (n <= 2) leafOrder = 1:n; return; endif ## for each node v we compute the maximum similarity of the subtree M(w,u,v), ## where the leftmost leaf is w and the rightmost is u; remember that ## M(w,u,v) = M(u,w,v) M = zeros (n, n, n + m); ## O is a utility matrix: for each node of the tree we store the left and ## right leaves of the optimal subtree O = [1:( n + m ); 1:( n + m ); (zeros (1, (n + m)))]'; ## compute M for every node v for iter = 1 : m v = iter + n; # current node l = optimalleaforder_getLeafList (tree(iter, 1)); # the left subtree r = optimalleaforder_getLeafList (tree(iter, 2)); # the right subtree if (tree(iter,1) > n) l_l = optimalleaforder_getLeafList (tree(tree(iter, 1) - n, 1)); l_r = optimalleaforder_getLeafList (tree(tree(iter, 1) - n, 2)); else l_l = l_r = l; endif if (tree(iter,2) > n) r_l = optimalleaforder_getLeafList (tree(tree(iter, 2) - n, 1)); r_r = optimalleaforder_getLeafList (tree(tree(iter, 2) - n, 2)); else r_l = r_r = r; endif ## let's find the maximum value of M(w,u,v) when: w is a leaf of the left ## subtree of v and u is a leaf of the right subtree of v for i = 1 : length (l) if (isempty (find (l(i) == l_l))) x = l_l; else x = l_r; endif for j = 1 : length (r) if (isempty (find (r(j) == r_l))) y = r_l; else y = r_r; endif ## max(M(w,u,v)) = max(M(w,k,v_l)) + max(M(h,u,v_r)) + S(k,h) ## where: v_l is the left child of v and v_r the right child of v M_tmp = repmat (M(l(i), x(:), tree(iter, 1)), length (y), 1) + ... repmat (M(y(:), r(j), tree(iter, 2)), 1, length (x)) + ... S(y(:), x(:)); M_max = max (max (M_tmp)); # this is M(l(i), r(j), v) [h, k] = find (M_tmp == M_max); M(l(i), r(j), v) = M_max; M(r(j), l(i), v) = M(l(i), r(j), v); if (M_max > O(v,3)) O(v, 1) = l(i); # this is w O(v, 2) = r(j); # this is u O(v, 3) = M_max; # this is M(w, u, v) endif endfor endfor endfor ## reordering: ## we found the M(w,u,v) corresponding to the optimal leaf order, now we can ## compute the optimal leaf order given our M(w,u,v) ## the return value leafOrder = zeros ( 1, n ); leafOrder(1) = O(end, 1); leafOrder(n) = O(end, 2); ## the inverse operation, only easier, to get the leaf order: now we know the ## leftmost and rightmost leaves of the best subtree, we may have to flip it ## though for iter = m : -1 : 1 v = iter + n; extremes = O(v, [1, 2]); l_node = tree(iter, 1); r_node = tree(iter, 2); l = optimalleaforder_getLeafList (l_node); r = optimalleaforder_getLeafList (r_node); if (l_node > n) l_l = optimalleaforder_getLeafList (tree(l_node - n, 1)); l_r = optimalleaforder_getLeafList (tree(l_node - n, 2)); else l_l = l_r = l; endif if (r_node > n) r_l = optimalleaforder_getLeafList (tree(r_node - n, 1)); r_r = optimalleaforder_getLeafList (tree(r_node - n, 2)); else r_l = r_r = r; endif ## this means that we need to flip the subtree if (isempty (find (extremes(1) == l))) l_tmp = l; l_l_tmp = l_l; l_r_tmp = l_r; l = r; l_l = r_l; l_r = r_r; r = l_tmp; r_l = l_l_tmp; r_r = l_r_tmp; node_tmp = l_node; l_node = r_node; r_node = node_tmp; endif if (isempty (find (extremes(1) == l_l))) x = l_l; else x = l_r; endif if (isempty (find (extremes(2) == r_l))) y = r_l; else y = r_r; endif M_tmp = repmat (M(extremes(1), x(:), l_node), length (y), 1) + ... repmat (M(y(:), extremes(2), r_node), 1, length (x)) + ... S(y(:), x(:)); M_max = max (max (M_tmp)); [h, k] = find (M_tmp == M_max); O(l_node, 1) = extremes(1); O(l_node, 2) = x(k); O(r_node, 1) = y(h); O(r_node, 2) = extremes(2); p_1 = find (leafOrder == extremes(1)); p_2 = find (leafOrder == extremes(2)); leafOrder(p_1 + (length (l)) - 1) = x(k); leafOrder(p_1 + (length (l))) = y(h); endfor ## function: optimalleaforder_getLeafList ## get the list of leaves under a given node function vector = optimalleaforder_getLeafList (nodes_to_visit) vector = []; while (! isempty (nodes_to_visit)) currentnode = nodes_to_visit(1); nodes_to_visit(1) = []; if (currentnode > n) node = currentnode - n; nodes_to_visit = [tree(node, [2 1]) nodes_to_visit]; endif if (currentnode <= n) vector = [vector currentnode]; endif endwhile endfunction endfunction %!demo %! rng (42); %! X = randn (10, 2); %! D = pdist (X); %! tree = linkage (D, 'average'); %! optimalleaforder (tree, D, 'Transformation', 'linear') ## Test input validation %!error optimalleaforder () %!error optimalleaforder (1) %!error optimalleaforder (ones (2, 2), 1) %!error optimalleaforder ([1 2 3], [1 2; 3 4], 'criteria', 5) %!error optimalleaforder ([1 2 1], [1 2 3]) %!error optimalleaforder ([1 2 1], 1, 'xxx', 'xxx') %!error optimalleaforder ([1 2 1], 1, 'Transformation', 'xxx') ## A leaf ordering and its reverse are equally optimal, so the tests below ## compare against MATLAB R2024a up to reversal. %!shared X, D, Z %! X = [0, 0; 0.2, 0.1; 3, 3; 3.1, 3.4; 6, 0; 6.2, 0.3; 9, 9; 9.1, 9.2; ... %! 1, 5; 5, 1]; %! D = pdist (X); %! Z = linkage (D, "average"); %!test # a custom transformation is a documented input and now reaches the fit %! o = optimalleaforder (Z, D, "Transformation", @(x) 1 ./ (1 + x)); %! assert_equal (sort (o), 1:10); %! assert_equal (o(end:-1:1), [1, 2, 9, 4, 3, 10, 6, 5, 7, 8]); %!test # the named transformations are unchanged %! o = optimalleaforder (Z, D, "Transformation", "linear"); %! assert_equal (o(end:-1:1), [1, 2, 9, 4, 3, 10, 5, 6, 7, 8]); %! o = optimalleaforder (Z, D, "Transformation", "inverse"); %! assert_equal (o(end:-1:1), [1, 2, 9, 4, 3, 10, 6, 5, 7, 8]); %!test # two leaves have a single ordering, where the tables used to run off %! ## the end of the similarity array %! for n = 2:5 %! o = optimalleaforder (linkage (pdist (X(1:n,:))), pdist (X(1:n,:))); %! assert_equal (sort (o), 1:n); %! endfor %! assert_equal (optimalleaforder (linkage (pdist (X(1:2,:))), ... %! pdist (X(1:2,:))), [1, 2]); %!test # the ordering is optimal: no reordering that keeps the tree beats it %! o = optimalleaforder (Z, D); %! Dsq = squareform (D); %! obj = @(p) sum (Dsq(sub2ind (size (Dsq), p(1:end-1), p(2:end)))); %! assert_equal (obj (o), obj (o(end:-1:1)), 1e-12); %! assert_equal (obj (o) <= obj (1:10), true); %!error ... %! optimalleaforder (Z, D, @(x) x, "linear") %!error ... %! optimalleaforder ([1 2 1], 1, 'Criteria', 5) %!error ... %! optimalleaforder ([1 2 1], 1, 'Transformation', 5) statistics-release-2.1.2/inst/Clustering/spectralcluster.m000066400000000000000000000417741527311104300240270ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{idx} =} spectralcluster (@var{X}, @var{k}) ## @deftypefnx {statistics} {@var{idx} =} spectralcluster (@var{S}, @var{k}, @qcode{'Distance'}, @qcode{'precomputed'}) ## @deftypefnx {statistics} {[@var{idx}, @var{V}] =} spectralcluster (@dots{}) ## @deftypefnx {statistics} {[@var{idx}, @var{V}, @var{D}] =} spectralcluster (@dots{}) ## @deftypefnx {statistics} {[@dots{}] =} spectralcluster (@dots{}, @var{name}, @var{value}) ## ## Partition observations into @var{k} clusters using spectral clustering. ## ## @code{@var{idx} = spectralcluster (@var{X}, @var{k})} partitions the ## @math{N*P} numeric matrix @var{X} into @var{k} clusters and returns the ## @math{N*1} vector @var{idx} of cluster indices. Rows of @var{X} correspond ## to observations and columns to features. Spectral clustering builds a ## similarity graph over the observations, embeds them with the eigenvectors of ## the graph Laplacian, and clusters that embedding, which lets it recover ## clusters that are not linearly separable in the original space. ## ## @code{[@var{idx}, @var{V}, @var{D}] = spectralcluster (@dots{})} also returns ## the @math{N*k} matrix @var{V} whose columns are the eigenvectors associated ## with the @var{k} smallest eigenvalues of the Laplacian, and the @math{k*1} ## vector @var{D} of those eigenvalues. The signs of the eigenvectors, and the ## basis within a repeated eigenvalue, are arbitrary. ## ## Additional parameters can be specified by @qcode{Name-Value} pair arguments. ## ## @multitable @columnfractions 0.22 0.76 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Distance'} @tab the distance metric used to build the ## similarity graph, one of @qcode{'euclidean'} (default), @qcode{'seuclidean'}, ## @qcode{'mahalanobis'}, @qcode{'cityblock'}, @qcode{'minkowski'}, ## @qcode{'chebychev'}, @qcode{'cosine'}, @qcode{'correlation'}, ## @qcode{'hamming'}, @qcode{'jaccard'}, @qcode{'spearman'}, or a function ## handle accepted by @code{pdist2}, or the string @qcode{'precomputed'} to ## interpret the first input as an @math{N*N} similarity matrix. ## ## @item @qcode{'SimilarityGraph'} @tab @qcode{'knn'} (default) to connect each ## observation to its nearest neighbors, or @qcode{'epsilon'} to connect ## observations that are within a fixed radius. ## ## @item @qcode{'NumNeighbors'} @tab the number of nearest neighbors for the ## @qcode{'knn'} graph, a positive integer. The default is ## @code{ceil (log (@var{N}))}. ## ## @item @qcode{'KNNGraphType'} @tab @qcode{'complete'} (default) to connect ## @math{i} and @math{j} when either is a nearest neighbor of the other, or ## @qcode{'mutual'} to connect them only when each is a nearest neighbor of the ## other. ## ## @item @qcode{'Radius'} @tab the radius for the @qcode{'epsilon'} graph, a ## nonnegative scalar. Required when @qcode{'SimilarityGraph'} is ## @qcode{'epsilon'}. ## ## @item @qcode{'KernelScale'} @tab the positive scale factor @var{sigma} in ## the Gaussian similarity kernel @code{exp (-(dist / sigma)^2)} applied to the ## graph edges. The default is 1. ## ## @item @qcode{'LaplacianNormalization'} @tab @qcode{'randomwalk'} (default), ## @qcode{'symmetric'}, or @qcode{'none'}, selecting how the graph Laplacian is ## normalized before the eigendecomposition. ## ## @item @qcode{'ClusterMethod'} @tab @qcode{'kmeans'} (default) or ## @qcode{'kmedoids'} to cluster the eigenvector embedding. ## ## @item @qcode{'P'} @tab the Minkowski exponent (default 2), used only with the ## @qcode{'minkowski'} distance. ## ## @item @qcode{'Cov'} @tab the covariance matrix used only with the ## @qcode{'mahalanobis'} distance. ## ## @item @qcode{'Scale'} @tab the scaling vector used only with the ## @qcode{'seuclidean'} distance. ## @end multitable ## ## @seealso{kmeans, kmedoids, dbscan, linkage, pdist2} ## @end deftypefn function [idx, V, D] = spectralcluster (X, k, varargin) ## Check number of input arguments if (nargin < 2) error ("spectralcluster: too few input arguments."); endif ## Validate X if (! isnumeric (X) || ! isreal (X) || ndims (X) != 2 || isempty (X)) error ("spectralcluster: X must be a nonempty real numeric matrix."); endif N = rows (X); ## Validate k if (! isscalar (k) || ! isnumeric (k) || ! isreal (k) || k < 1 || fix (k) != k) error ("spectralcluster: K must be a positive integer scalar."); endif if (k > N) error ("spectralcluster: K cannot exceed the number of observations in X."); endif ## Defaults distance = "euclidean"; simgraph = "knn"; numneighbors = []; knntype = "complete"; radius = []; kernelscale = 1; laplnorm = "randomwalk"; clustermethod = "kmeans"; Pexp = 2; Cov = []; Scl = []; ## Parse Name-Value pairs if (mod (numel (varargin), 2) != 0) error ("spectralcluster: each NAME must be followed by a VALUE."); endif while (numel (varargin) > 0) name = varargin{1}; val = varargin{2}; if (! ischar (name)) error ("spectralcluster: optional argument names must be strings."); endif switch (tolower (name)) case "distance" distance = val; case "similaritygraph" simgraph = tolower (val); case "numneighbors" numneighbors = val; case "knngraphtype" knntype = tolower (val); case "radius" radius = val; case "kernelscale" kernelscale = val; case "laplaciannormalization" laplnorm = tolower (val); case "clustermethod" clustermethod = tolower (val); case "p" Pexp = val; case "cov" Cov = val; case "scale" Scl = val; otherwise error ("spectralcluster: unknown parameter name '%s'.", name); endswitch varargin(1:2) = []; endwhile ## Validate option choices if (! any (strcmp (simgraph, {"knn", "epsilon"}))) error ("spectralcluster: SIMILARITYGRAPH must be 'knn' or 'epsilon'."); endif if (! any (strcmp (knntype, {"complete", "mutual"}))) error ("spectralcluster: KNNGRAPHTYPE must be 'complete' or 'mutual'."); endif if (! any (strcmp (laplnorm, {"randomwalk", "symmetric", "none"}))) error (strcat ("spectralcluster: LAPLACIANNORMALIZATION must be", ... " 'randomwalk', 'symmetric', or 'none'.")); endif if (! any (strcmp (clustermethod, {"kmeans", "kmedoids"}))) error ("spectralcluster: CLUSTERMETHOD must be 'kmeans' or 'kmedoids'."); endif if (! isscalar (kernelscale) || ! isnumeric (kernelscale) || kernelscale <= 0) error ("spectralcluster: KERNELSCALE must be a positive scalar."); endif ## Build the N-by-N similarity matrix S. if (ischar (distance) && strcmpi (distance, "precomputed")) if (rows (X) != columns (X)) error (strcat ("spectralcluster: X must be a square similarity", ... " matrix for the 'precomputed' distance.")); endif S = X; else ## Pairwise distances in the requested metric. metrics = {"euclidean", "seuclidean", "mahalanobis", "cityblock", ... "minkowski", "chebychev", "cosine", "correlation", ... "hamming", "jaccard", "spearman"}; if (ischar (distance)) dname = tolower (distance); if (! any (strcmp (dname, metrics))) error ("spectralcluster: unsupported distance metric '%s'.", distance); endif switch (dname) case "minkowski" Dmat = pdist2 (X, X, dname, Pexp); case "mahalanobis" if (isempty (Cov)) Dmat = pdist2 (X, X, dname); else Dmat = pdist2 (X, X, dname, Cov); endif case "seuclidean" if (isempty (Scl)) Dmat = pdist2 (X, X, dname); else Dmat = pdist2 (X, X, dname, Scl); endif otherwise Dmat = pdist2 (X, X, dname); endswitch elseif (is_function_handle (distance)) Dmat = pdist2 (X, X, distance); else error (strcat ("spectralcluster: DISTANCE must be a metric name or", ... " a function handle.")); endif ## Adjacency of the similarity graph. A = false (N); if (strcmp (simgraph, "knn")) if (isempty (numneighbors)) numneighbors = max (1, ceil (log (N))); endif if (! isscalar (numneighbors) || ! isnumeric (numneighbors) || numneighbors < 1 || fix (numneighbors) != numneighbors) error (strcat ("spectralcluster: NUMNEIGHBORS must be a positive", ... " integer scalar.")); endif nn = min (numneighbors, N - 1); for i = 1:N d = Dmat(i,:); d(i) = Inf; [~, ord] = sort (d); A(i, ord(1:nn)) = true; endfor if (strcmp (knntype, "complete")) A = A | A'; else A = A & A'; endif else if (isempty (radius)) error (strcat ("spectralcluster: RADIUS is required for the", ... " 'epsilon' similarity graph.")); endif if (! isscalar (radius) || ! isnumeric (radius) || radius < 0) error ("spectralcluster: RADIUS must be a nonnegative scalar."); endif A = Dmat <= radius; A(1:(N + 1):end) = false; # drop self-edges on the diagonal endif ## Gaussian kernel on the graph edges; zero elsewhere. S = zeros (N); S(A) = exp (-(Dmat(A) / kernelscale) .^ 2); endif ## Graph Laplacian and its k smallest eigenpairs. deg = sum (S, 2); Dg = diag (deg); if (! strcmp (laplnorm, "none") && any (deg <= 0)) error (strcat ("spectralcluster: the similarity graph has isolated", ... " points; increase NumNeighbors or Radius.")); endif L = Dg - S; switch (laplnorm) case "none" [Vec, Val] = eig (L); case "randomwalk" [Vec, Val] = eig (L, Dg); case "symmetric" Dh = diag (1 ./ sqrt (deg)); Ls = Dh * L * Dh; Ls = (Ls + Ls') / 2; # enforce exact symmetry for real eig [Vec, Val] = eig (Ls); endswitch ev = real (diag (Val)); [ev, order] = sort (ev); D = ev(1:k); V = real (Vec(:, order(1:k))); if (strcmp (laplnorm, "symmetric")) rn = sqrt (sum (V .^ 2, 2)); # row-normalize the embedding rn(rn == 0) = 1; V = V ./ rn; endif ## Cluster the eigenvector embedding. if (strcmp (clustermethod, "kmeans")) idx = kmeans (V, k, "Replicates", 5, "EmptyAction", "singleton"); else idx = kmedoids (V, k, "Replicates", 5); endif endfunction %!demo %! ## Two concentric rings are not separable by kmeans but are by spectral %! ## clustering. %! rng (42); %! t = linspace (0, 2*pi, 100)'; %! Xin = [cos(t), sin(t)] + randn (100, 2) * 0.05; %! Xout = 4 * [cos(t), sin(t)] + randn (100, 2) * 0.05; %! X = [Xin; Xout]; %! idx = spectralcluster (X, 2, "NumNeighbors", 10); %! gscatter (X(:,1), X(:,2), idx); %! axis equal; %! title ("spectralcluster: two concentric rings"); ## Exact anchor: precomputed similarity, random-walk Laplacian (MATLAB R2024a) %!test %! S = [0 1 1 0 0 0; 1 0 1 0 0 0; 1 1 0 0.1 0 0; 0 0 0.1 0 1 1; ... %! 0 0 0 1 0 1; 0 0 0 1 1 0]; %! [idx, V, D] = spectralcluster (S, 2, "Distance", "precomputed"); %! assert_equal (abs (D(1)) < 1e-9, true); %! assert_equal (D(2), 0.0314065796348158, 1e-12); %! assert_equal (size (V), [6 2]); %! assert_equal (idx(1) == idx(2) && idx(2) == idx(3), true); %! assert_equal (idx(4) == idx(5) && idx(5) == idx(6), true); %! assert_equal (idx(1) != idx(4), true); ## Symmetric normalization has the same spectrum as random-walk %!test %! S = [0 1 1 0 0 0; 1 0 1 0 0 0; 1 1 0 0.1 0 0; 0 0 0.1 0 1 1; ... %! 0 0 0 1 0 1; 0 0 0 1 1 0]; %! [~, ~, D] = spectralcluster (S, 2, "Distance", "precomputed", ... %! "LaplacianNormalization", "symmetric"); %! assert_equal (D(2), 0.0314065796348156, 1e-12); ## Unnormalized Laplacian ('none') spectrum %!test %! S = [0 1 1 0 0 0; 1 0 1 0 0 0; 1 1 0 0.1 0 0; 0 0 0.1 0 1 1; ... %! 0 0 0 1 0 1; 0 0 0 1 1 0]; %! [~, ~, D] = spectralcluster (S, 2, "Distance", "precomputed", ... %! "LaplacianNormalization", "none"); %! assert_equal (D(2), 0.0637708504262773, 1e-12); ## Precomputed path graph splits at the weak middle edge; eigenvalue is 2/7 %!test %! Sc = [0 0.5 0 0; 0.5 0 0.2 0; 0 0.2 0 0.5; 0 0 0.5 0]; %! [idx, ~, D] = spectralcluster (Sc, 2, "Distance", "precomputed"); %! assert_equal (D(2), 2/7, 1e-12); %! assert_equal (idx(1) == idx(2), true); %! assert_equal (idx(3) == idx(4), true); %! assert_equal (idx(1) != idx(3), true); ## Full knn pipeline (graph + Gaussian kernel + Laplacian) matches MATLAB %!test %! X = [0 0; 0.1 0; 0 0.1; 5 0; 5.1 0; 5 0.1]; %! [idx, ~, D] = spectralcluster (X, 2, "NumNeighbors", 3, "KernelScale", 2); %! assert_equal (D(2), 0.00358001836237202, 1e-11); %! assert_equal (idx(1) == idx(2) && idx(2) == idx(3), true); %! assert_equal (idx(4) == idx(5) && idx(5) == idx(6), true); %! assert_equal (idx(1) != idx(4), true); ## Default NumNeighbors is ceil (log (N)): for N = 8 that is 3 %!test %! X = (1:8)'; %! [~, ~, Ddef] = spectralcluster (X, 2); %! [~, ~, D3] = spectralcluster (X, 2, "NumNeighbors", 3); %! assert_equal (Ddef(2), D3(2), 1e-12); %! assert_equal (Ddef(2), 0.113482181378817, 1e-11); ## Default path clusters two well-separated blobs %!test %! X = [0 0; 0.2 0; 0 0.2; 0.2 0.2; 10 10; 10.2 10; 10 10.2; 10.2 10.2]; %! idx = spectralcluster (X, 2); %! assert_equal (numel (unique (idx(1:4))) == 1, true); %! assert_equal (numel (unique (idx(5:8))) == 1, true); %! assert_equal (idx(1) != idx(5), true); ## kmedoids embedding-clustering and the epsilon graph both work %!test %! X = [0 0; 0.2 0; 0 0.2; 0.2 0.2; 10 10; 10.2 10; 10 10.2; 10.2 10.2]; %! idx = spectralcluster (X, 2, "ClusterMethod", "kmedoids"); %! assert_equal (numel (unique (idx(1:4))) == 1, true); %! assert_equal (numel (unique (idx(5:8))) == 1, true); %!test %! X = [0 0; 0.2 0; 0 0.2; 0.2 0.2; 10 10; 10.2 10; 10 10.2; 10.2 10.2]; %! [idx, ~, D] = spectralcluster (X, 2, "SimilarityGraph", "epsilon", ... %! "Radius", 1); %! assert_equal (numel (idx), 8); %! assert_equal (idx(1) != idx(5), true); ## Three clusters: mutual knn graph and output shapes %!test %! X = [0 0; 0.2 0; 0 0.2; 10 10; 10.2 10; 10 10.2; 0 10; 0.2 10; 0 10.2]; %! [idx, V, D] = spectralcluster (X, 3, "NumNeighbors", 2, ... %! "KNNGraphType", "mutual"); %! assert_equal (size (V), [9 3]); %! assert_equal (size (D), [3 1]); %! assert_equal (numel (unique (idx(1:3))) == 1, true); %! assert_equal (numel (unique (idx(4:6))) == 1, true); %! assert_equal (numel (unique (idx(7:9))) == 1, true); ## Test input validation %!error spectralcluster (1) %!error ... %! spectralcluster ([], 2) %!error ... %! spectralcluster (ones (4,2), 0) %!error ... %! spectralcluster (ones (3,2), 4) %!error ... %! spectralcluster (ones (4,2), 2, "Distance") %!error ... %! spectralcluster (ones (4,2), 2, "foo", "bar") %!error ... %! spectralcluster (ones (4,2), 2, "Distance", "taxicab") %!error ... %! spectralcluster (ones (4,2), 2, "SimilarityGraph", "tree") %!error ... %! spectralcluster (ones (4,2), 2, "KNNGraphType", "partial") %!error ... %! spectralcluster (ones (4,2), 2, "ClusterMethod", "dbscan") %!error ... %! spectralcluster (ones (4,2), 2, "KernelScale", 0) %!error ... %! spectralcluster (ones (4,2), 2, "SimilarityGraph", "epsilon") %!error ... %! spectralcluster (ones (4,2), 2, "Distance", "precomputed") %!error ... %! spectralcluster ([0 0; 10 10], 2, "SimilarityGraph", "epsilon", "Radius", 1) statistics-release-2.1.2/inst/Data_Manipulation/000077500000000000000000000000001527311104300216675ustar00rootroot00000000000000statistics-release-2.1.2/inst/Data_Manipulation/combnk.m000066400000000000000000000057031527311104300233230ustar00rootroot00000000000000## Copyright (C) 2010 Soren Hauberg ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{c} =} combnk (@var{data}, @var{k}) ## ## Return all combinations of @var{k} elements in @var{data}. ## ## @var{k} must be a non-negative integer. @code{combnk (@var{data}, 0)} ## returns a 1-by-0 array of the class of @var{data}, the one empty ## combination. ## ## @end deftypefn function retval = combnk (data, k) ## Check input if (nargin != 2) print_usage; elseif (! isvector (data)) error ("combnk: first input argument must be a vector"); elseif (!isreal (k) || k != round (k) || k < 0) error ("combnk: second input argument must be a non-negative integer"); endif ## Simple checks n = numel (data); if (k == 0) retval = resize (data, 1, 0); elseif (k > n) retval = resize (data, 0, k); elseif (k == n) retval = data(:).'; else retval = __combnk__ (data, k); endif ## For some odd reason Matlab seems to treat strings differently compared to ## other data-types... if (ischar (data)) retval = flipud (retval); endif endfunction function retval = __combnk__ (data, k) ## Recursion stopping criteria if (k == 1) retval = data(:); else ## Process data n = numel (data); if (iscell (data)) retval = {}; else retval = []; endif for j = 1:n C = __combnk__ (data((j+1):end), k-1); C = cat (2, repmat (data(j), rows (C), 1), C); if (! isempty (C)) if (isempty (retval)) retval = C; else retval = [retval; C]; endif endif endfor endif endfunction %!demo %! c = combnk (1:5, 2); %! disp ('All pairs of integers between 1 and 5:'); %! disp (c); %!test %! c = combnk (1:3, 2); %! assert_equal (c, [1, 2; 1, 3; 2, 3]); %!test %! c = combnk (1:3, 6); %! assert_equal (isempty (c), true); %!test %! c = combnk ({1, 2, 3}, 2); %! assert_equal (c, {1, 2; 1, 3; 2, 3}); %!test %! c = combnk ('hello', 2); %! assert_equal (c, ['lo'; 'lo'; 'll'; 'eo'; 'el'; 'el'; 'ho'; 'hl'; 'hl'; 'he']); %!assert_equal (combnk (1:3, 0), zeros (1, 0)) %!assert_equal (combnk ((1:3)', 0), zeros (1, 0)) %!assert_equal (combnk ('abc', 0), char (zeros (1, 0))) %!assert_equal (combnk (int8 (1:3), 0), zeros (1, 0, 'int8')) statistics-release-2.1.2/inst/Data_Manipulation/crosstab.m000066400000000000000000000576411527311104300237020ustar00rootroot00000000000000## Copyright (C) 1995-2017 Kurt Hornik ## Copyright (C) 2018 John Donoghue ## Copyright (C) 2021 Stefano Guidoni ## Copyright (C) 2022-2025 Andreas Bertsatos ## Copyright (C) 2025 Yasin Achengli ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{t} =} crosstab (@var{x1}) ## @deftypefnx {statistics} {@var{t} =} crosstab (@var{x1}, @var{x2}) ## @deftypefnx {statistics} {@var{t} =} crosstab (@var{x1}, @dots{}, @var{xn}) ## @deftypefnx {statistics} {[@var{t}, @var{chisq}, @var{p}, @var{labels}] =} crosstab (@dots{}) ## @deftypefnx {statistics} {[@dots{}] =} crosstab (@dots{}, @var{Name}, @var{Value}) ## ## Create a cross-tabulation (contingency table) @var{t} from data vectors. ## ## The inputs @var{x1}, @var{x2}, ... @var{xn} must be vectors of equal length ## with a data type of numeric, logical, char array, categorical, strings, ## datetime, duration, or cell array of character vectors. Missing values, ## such as @qcode{NaN} or @qcode{NaT}, are not counted by default. A ## @qcode{NaN} duration is missing too, whereas MATLAB counts it as a group of ## its own. ## ## The following @qcode{Name-Value} pair is supported: ## ## @multitable @columnfractions 0.28 0.72 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'IncludeMissingGroups'} @tab A real double, single or logical ## scalar that is not @qcode{NaN} (default @qcode{false}). When nonzero or ## @qcode{true}, the missing values are counted as a group of their own, placed ## after the other groups of every input that holds any. The name is not case ## sensitive and may be shortened to @qcode{'IncludeMissing'}; any other text ## is read as data. Only its last occurrence counts. ## @end multitable ## ## The label of the missing group is @qcode{NaN} for numeric inputs, ## @qcode{} for categorical, @qcode{} for strings, ## @qcode{@{''@}} for cell arrays of character vectors, @qcode{NaT} for ## datetime, the text of a @qcode{NaN} element for duration, and the blank row ## itself for a char array. ## ## As additional return values @code{crosstab} returns the chi-square statistics ## @var{chisq}, its p-value @var{p} and a cell array @var{labels}, containing ## the labels of each input argument. ## ## With a single input, @var{t} is a column holding the count of each group, ## and @var{chisq} and @var{p} are @code{NaN}, there being no second variable ## to test independence against. A char row is a single observation. ## ## A categorical input has a row or column for every one of its categories, ## an unused one included, holding zeros. The chi-square statistic and its ## p-value are computed over the rows and columns that hold observations, ## where MATLAB returns @code{NaN} for both whenever any row or column is ## empty. With the missing group counted, it follows every category here, ## whereas MATLAB returns a table one row longer than its labels when an unused ## category follows the last used one. ## ## @seealso{grp2idx, tabulate} ## @end deftypefn function [t, chisq, p, labels] = crosstab (varargin) ## check input if (nargin < 1) print_usage (); endif ## Trailing Name-Value pairs start at the first text, from the second input ## on, that is the option name or its short form, case free; any other text ## is data. optnames = {"IncludeMissingGroups", "IncludeMissing"}; isopt = @(s) ischar (s) && isrow (s) && any (strcmpi (s, optnames)); nvars = nargin; incmissing = false; k = find (cellfun (isopt, varargin(2:end)), 1); if (! isempty (k)) nvars = k; if (mod (nargin - nvars, 2) != 0) error ("crosstab: optional arguments must be Name-Value pairs."); endif for j = nvars+1:2:nargin name = varargin{j}; if (! isopt (name)) if (ischar (name) && isrow (name)) error ("crosstab: unknown parameter name '%s'.", name); endif error ("crosstab: optional arguments must be Name-Value pairs."); endif value = varargin{j+1}; if (! (isscalar (value) && (isfloat (value) || islogical (value)) && isreal (value))) error (["crosstab: 'IncludeMissingGroups' must be a real double, ", ... "single or logical scalar."]); endif if (isnan (value)) error ("crosstab: 'IncludeMissingGroups' cannot be NaN."); endif incmissing = logical (value); endfor varargin = varargin(1:nvars); endif ## main - begin v_length = []; # vector of lengths of input vectors reshape_format = []; # vector of the dimensions of t X = []; # matrix of the indexed input values labels_data = {}; # temporary cell array for unique labels coordinates = {}; # cell array of unique elements for i = 1:nvars vector = varargin{i}; ## Convert char, cellstr, categorical, string, datetime, or duration to ## indexed numeric vector if (ischar (vector) || iscellstr (vector) || iscategorical (vector) || isstring (vector) || isdatetime (vector) || isduration (vector)) try [vector, gnames] = grp2idx (vector); labels_data{i} = gnames; catch error ("crosstab: x1, x2 ... xn must be vectors."); end_try_catch elseif (isnumeric (vector) || islogical (vector)) if (! isvector (vector)) error ("crosstab: x1, x2 ... xn must be vectors."); endif vector = vector(:); unique_vals = unique (vector(! isnan (vector))); labels_data{i} = arrayfun (@num2str, unique_vals, 'UniformOutput', false); ## X is double: an integer vector would convert the others to its type vector = double (vector); else error ("crosstab: unsupported type for data vector."); endif v_length(i) = length (vector); if (length (unique (v_length)) != 1) error ("crosstab: x1, x2 ... xn must be vectors of the same length."); endif X = [X, vector]; if (isnumeric (varargin{i}) || islogical (varargin{i})) reshape_format(i) = length (unique (vector(! isnan (vector)))); coordinates(i) = unique (vector(! isnan (vector))); else ## A group has a row or column for every name it carries, an unused ## category included, so the table lines up with LABELS as R2024a's ## does. reshape_format(i) = numel (labels_data{i}); coordinates(i) = (1:numel (labels_data{i}))'; endif ## Missing values form a last group of their own when they are counted miss = isnan (vector); if (incmissing && any (miss)) labels_data{i}{end+1,1} = crosstab_missing_label_ (varargin{i}, miss); reshape_format(i) += 1; endif endfor if (nargout > 3) max_rows = 0; for i = 1:nvars max_rows = max (max_rows, numel (labels_data{i})); endfor labels = cell (max_rows, nvars); for i = 1:nvars col_labels = labels_data{i}; labels(1:numel (col_labels), i) = col_labels; endfor else labels = {}; endif ## A single input gives a column, not a square matrix t = zeros ([reshape_format, 1]); ## Main logic: ## For each combination of x1, x2, ... xn, search in unique elements stored ## in coordinates for each dimension and increment the position value in t ## multidimensional matrix (always if there is no NaN element in the ## combination, or if the missing values are counted, in which case a NaN ## goes to the last position of its dimension). for idx = 1:size (X, 1) if (incmissing || ! any (isnan (X(idx,:)))) location = zeros (1,size (X, 2)); for jdx = 1:size (X,2) if (isnan (X(idx, jdx))) location(jdx) = reshape_format(jdx); else location(jdx) = find (cell2mat (coordinates(jdx)) == X(idx, jdx)); endif endfor t(num2cell (location){:}) += 1; endif endfor ## The test runs over the rows and columns holding observations: an empty ## one has no expected count to compare with, and R2024a returns NaN for ## the whole table instead. if (nargout > 1) nd = ndims (t); keep = cell (1, nd); for d = 1:nd tot = t; for o = setdiff (1:nd, d) tot = sum (tot, o); endfor keep{d} = find (tot(:) > 0); endfor tt = t(keep{:}); if (nvars == 1 || isscalar (tt) || isempty (tt)) chisq = NaN; p = NaN; else [~, p, st] = chi2test (tt); chisq = st.chi2stat; endif endif endfunction ## Label of the group that holds the missing values of the input X, whose ## missing elements are flagged by MISS. function lbl = crosstab_missing_label_ (x, miss) if (iscategorical (x)) lbl = ""; elseif (isstring (x)) lbl = ""; elseif (iscellstr (x)) lbl = "{''}"; elseif (ischar (x)) lbl = x(find (miss, 1),:); elseif (isdatetime (x)) lbl = "NaT"; elseif (isduration (x)) lbl = strtrim (char (x(find (miss, 1)))); else lbl = "NaN"; endif endfunction ## Test input validation %!error crosstab () %!error crosstab (ones (2), [1 1]) %!error crosstab ([1 1], ones (2)) %!error crosstab ([1], [1 2]) %!error crosstab ([1 2], [1]) %!error crosstab ([1 2], {1, 2}) %!error ... %! crosstab (repmat (datetime (2026, 1, 1), 2, 2), [1 2 3 4]) %!error ... %! crosstab (hours ([1 2; 3 4]), [1 2 3 4]) %!error ... %! crosstab ([1 2], [1 2], 'IncludeMissingGroups', 'yes') %!error ... %! crosstab ([1 2], [1 2], 'IncludeMissingGroups', int8 (1)) %!error ... %! crosstab ([1 2], [1 2], 'IncludeMissingGroups', 1i) %!error ... %! crosstab ([1 2], [1 2], 'IncludeMissingGroups', []) %!error ... %! crosstab ([1 2], [1 2], 'IncludeMissingGroups', NaN) %!error ... %! crosstab ([1 2], [1 2], 'IncludeMissingGroups') %!error ... %! crosstab ([1 2], [1 2], 'IncludeMissingGroups', true, 5, true) %!error ... %! crosstab ([1 2], [1 2], 'IncludeMissingGroups', true, 'Foo', true) %!error ... %! crosstab ([1 2], [1 2], 'IncludeMissing', true, 'Include', true) %!test %! load carbig %! [t, chisq, p, labels] = crosstab (cyl4, when, org); %! assert_equal (t(2,3,1), 38); %! assert_equal (labels{3,3}, "Japan"); %!test %! load carbig %! [t, chisq, p, labels] = crosstab (cyl4, when, org); %! assert_equal (t(2,3,2), 17); %! assert_equal (labels{1,3}, "USA"); %!test %! x = [1, 1, 2, 3, 1]; %! y = [1, 2, 5, 3, 1]; %! t = crosstab (x, y); %! assert_equal (t, [2, 1, 0, 0; 0, 0, 0, 1; 0, 0, 1, 0]); %!test %! x = [1, 1, 2, 3, 1]; %! y = [1, 2, 3, 5, 1]; %! t = crosstab (x, y); %! assert_equal (t, [2, 1, 0, 0; 0, 0, 1, 0; 0, 0, 0, 1]); %!test %! x1 = [1, 3, 7, 7, 8]; %! x2 = [4, 2, 1, 1, 1]; %! x3 = [6, 2, 6, 2, NaN]; %! T1 = [0, 0, 0; 0, 1, 0; 1, 0, 0; 0, 0, 0]; %! T2 = [0, 0, 1; 0, 0, 0; 1, 0, 0; 0, 0, 0]; %! T = zeros (4, 3, 2); %! T(:,:,1) = T1; %! T(:,:,2) = T2; %! t = crosstab (x1, x2, x3); %! assert_equal (t, T); %!test %! x = [1, 2, NaN, 1]; %! y = [1, 2, 3, NaN]; %! t = crosstab (x, y); %! assert_equal (t, [1, 0, 0; 0, 1, 0]); ## Test categorical input %!test %! x = categorical ({'A', 'B', 'A', 'C', 'B'}); %! y = [1, 2, 1, 3, 2]; %! t = crosstab (x, y); %! assert_equal (size (t), [3, 3]); %! assert_equal (t(1, 1), 2); # A with 1 %! assert_equal (t(2, 2), 2); # B with 2 %!test %! x = categorical ({'low', 'med', 'high', 'low', 'med'}); %! y = categorical ({'X', 'Y', 'X', 'Y', 'X'}); %! t = crosstab (x, y); %! assert_equal (size (t), [3, 2]); %!test # MATLAB parity: an unused category keeps its zero row %! g = categorical ({'hi'; 'hi'; 'lo'; 'lo'; 'hi'; 'lo'}, {'mid', 'lo', 'hi'}); %! w = warning (); %! warning ('off'); %! [t, chisq, p, labels] = crosstab (g, [1; 2; 5; 6; 3; 7] > 3); %! warning (w); %! assert_equal (t, [0, 0; 0, 3; 3, 0]); %! assert_equal (labels(:,1)', {'mid', 'lo', 'hi'}); %! assert_equal (chisq, 6); %! assert_equal (p, 0.01430587844, 1e-10); %!test # MATLAB parity: rows follow the category order, not the names %! x = categorical ({'hi'; 'hi'; 'lo'; 'lo'; 'hi'; 'lo'}, {'lo', 'hi'}); %! t = crosstab (x, [1; 2; 5; 6; 3; 7] > 3); %! assert_equal (t, [0, 3; 3, 0]); %!test %! ## Test categorical with numeric %! x = categorical ([10, 20, 10, 30, 20]); %! y = [1, 2, 1, 3, 2]; %! t = crosstab (x, y); %! assert_equal (t, [2, 0, 0; 0, 2, 0; 0, 0, 1]); %!test %! smoker = [1 1 0 0 1 0 1 1 0 0 1 0]'; %! gender = [1 0 1 0 1 1 0 0 1 0 0 1]'; %! w = warning (); %! warning ('off'); %! [t, chisq, p, labels] = crosstab (smoker, gender); %! warning (w); %! assert_equal (t, [2 4; 4 2]); %! assert_equal (chisq, 1.33333333, 1e-8); %! assert_equal (p, 0.24821308, 1e-8); %! assert_equal (labels{1,1}, '0'); %! assert_equal (labels{1,2}, '0'); %! assert_equal (labels{2,1}, '1'); %! assert_equal (labels{2,2}, '1'); %!test %! ## Test for categorical %! smk_cat = categorical ([0 0 1 1 0 1 0 0 1 1 0 1]'); %! gen_cat = categorical ([0 1 0 1 0 0 1 1 0 1 1 0]'); %! w = warning (); %! warning ('off'); %! [t, chisq, p] = crosstab (smk_cat, gen_cat); %! warning (w); %! assert_equal (t, [2 4; 4 2]); %! assert_equal (chisq, 1.33333333, 1e-6); %! assert_equal (p, 0.24821308, 1e-6); %!test %! x = [1 1 1 2 2 2 3 3 3]'; %! y = [1 1 1 2 2 2 3 3 3]'; %! w = warning (); %! warning ('off'); %! [t, chisq, p, labels] = crosstab (x, y); %! warning (w); %! assert_equal (t, diag ([3 3 3])); %! assert_equal (chisq, 18.00000000); %! assert_equal (p, 0.00123410, 1e-8); %!test %! ## The two columns holding only rows with a missing x7 are empty, and the %! ## test runs over the other four; R2024a returns NaN for chisq and p. %! x7 = [1 2 3 4 NaN NaN]'; %! y7 = [10 20 30 40 50 60]'; %! w = warning (); %! warning ('off'); %! [t, chisq, p, labels] = crosstab (x7, y7); %! warning (w); %! assert_equal (t, [eye(4), zeros(4, 2)]); %! assert_equal (chisq, 12); %! assert_equal (p, 0.2133093051, 1e-10); %! assert_equal (labels{1,1}, '1'); %! assert_equal (labels{1,2}, '10'); %! assert_equal (labels{2,1}, '2'); %! assert_equal (labels{2,2}, '20'); %! assert_equal (labels{3,1}, '3'); %! assert_equal (labels{3,2}, '30'); %! assert_equal (labels{4,1}, '4'); %! assert_equal (labels{4,2}, '40'); %! assert_equal (isempty (labels{5,1}), true); %! assert_equal (labels{5,2}, '50'); %! assert_equal (isempty (labels{6,1}), true); %! assert_equal (labels{6,2}, '60'); %!test %! a = ones (15,1); %! b = ones (15,1); %! w = warning (); %! warning ('off'); %! [t, chisq, p] = crosstab (a, b); %! warning (w); %! assert_equal (t, 15); %! assert_equal (isnan (chisq), true); %! assert_equal (isnan (p), true); %!test %! ## all NaN → empty table + NaN stats %! na = NaN (6,1); %! nb = (1:6)'; %! w = warning (); %! warning ('off'); %! [t, chisq, p] = crosstab (na, nb); %! warning (w); %! assert_equal (all (t(:) == 0), true); %! assert_equal (isnan (chisq), true); %! assert_equal (isnan (p), true); %!test %! ## single observation → 1×1 table, NaN statistic %! w = warning (); %! warning ('off'); %! [t, chisq, p, labels] = crosstab (5, 'Z'); %! warning (w); %! assert_equal (t, 1); %! assert_equal (isnan (chisq), true); %! assert_equal (isnan (p), true); %! assert_equal (labels{1,1}, '5'); %! assert_equal (labels{1,2}, 'Z'); %!test %! xx = [1 1 1 1 2 2 2 2]'; %! yy = [10 10 10 10 20 20 20 20]'; %! w = warning (); %! warning ('off'); %! [t, chisq, p] = crosstab (xx, yy); %! warning (w); %! assert_equal (t, [4 0; 0 4]); %! assert_equal (chisq, 8.00000000); %! assert_equal (p, 0.00467773, 1e-8); %!test %! set1 = repmat ((1:5)', 20, 1); %! set2 = repmat ([1; 2], 50, 1); %! w = warning (); %! warning ('off'); %! [t, chisq, p] = crosstab (set1, set2); %! warning (w); %! assert_equal (t, 10 * ones (5,2)); %! assert_equal (chisq, 0); %! assert_equal (p, 1); %!test %! ## 3-way table with NaN %! a = [1 1 2 2 3 3 1]'; %! b = [1 2 1 2 1 2 1]'; %! c = [1 1 NaN 2 2 1 2]'; %! w = warning (); %! warning ('off'); %! [t, chisq, p] = crosstab (a, b, c); %! warning (w); %! expected(:,:,1) = [1 1; 0 0; 0 1]; %! expected(:,:,2) = [1 0; 0 1; 1 0]; %! assert_equal (t, expected); %! assert_equal (chisq, 6.00000000); %! assert_equal (p, 0.53974935, 1e-9); %!test %! ## sparse cellstr %! g = [1 5 7 12 1 5 19]'; %! h = {'A', 'B', 'A', 'C', 'D', 'E', 'A'}'; %! w = warning (); %! warning ('off'); %! [t, chisq, p] = crosstab (g, h); %! warning (w); %! assert_equal (sum (t(:)), 7); %! assert_equal (chisq, 16.33333333, 1e-8); %! assert_equal (p, 0.42994852, 1e-8); %!test %! ## string array %! str1 = ['low'; 'high'; 'med'; 'low'; 'high']; %! str2 = ['X'; 'Y'; 'X'; 'Y'; 'X']; %! w = warning (); %! warning ('off'); %! [t, chisq, p] = crosstab (str1, str2); %! warning (w); %! assert_equal (t, [1 1; 1 1; 1 0]); %! assert_equal (chisq, 0.83333333, 1e-8); %! assert_equal (p, 0.659240631, 1e-9); %!test %! ## cellstr %! c1 = {'A','B','A','C','B','A'}'; %! c2 = {'1','2','1','3','2','1'}'; %! w = warning (); %! warning ('off'); %! [t, chisq, p] = crosstab (c1, c2); %! warning (w); %! assert_equal (t, [3 0 0; 0 2 0; 0 0 1]); %! assert_equal (chisq, 12.00000000, 1e-14); %! assert_equal (p, 0.01735127, 1e-8); %!test %! ## datetime and duration, against MATLAB R2024a %! d = repmat ([datetime(2026, 1, 5); datetime(2025, 3, 1); ... %! datetime(2026, 10, 13)], 6, 1); %! d(4) = NaT; %! h = hours (repmat ([1; 2; 3], 6, 1)); %! w = warning (); %! warning ('off'); %! [t, chisq, p, labels] = crosstab (d, h); %! warning (w); %! assert_equal (t, [0 6 0; 5 0 0; 0 0 6]); %! assert_equal (chisq, 34, 1e-12); %! assert_equal (p, 7.4519e-07, 1e-11); %! assert_equal (labels, {'01-Mar-2025', '1 hr'; '05-Jan-2026', '2 hr'; ... %! '13-Oct-2026', '3 hr'}); %!test %! ## a NaN duration is missing, where R2024a makes it a group of its own %! w = warning (); %! warning ('off'); %! [t, chisq, p, labels] = crosstab (hours ([1; NaN; 2]), [1; 2; 2]); %! warning (w); %! assert_equal (t, [1 0; 0 1]); %! assert_equal (labels, {'1 hr', '1'; '2 hr', '2'}); %!test %! ## numeric labels without padding, as R2024a's %! w = warning (); %! warning ('off'); %! [~, ~, ~, labels] = crosstab ([1.5 2 10.25 1.5], [1 2 3 1]); %! warning (w); %! assert_equal (labels, {'1.5', '1'; '2', '2'; '10.25', '3'}); %!test %! w = warning (); %! warning ('off'); %! [~, ~, ~, labels] = crosstab ([1e-5 1e6 1e-5], int8 ([1 2 1])); %! warning (w); %! assert_equal (labels, {'1e-05', '1'; '1000000', '2'}); %!test %! [t, chisq, p, labels] = crosstab (zeros (0, 1), zeros (0, 1)); %! assert_equal (t, []); %! assert_equal (chisq, NaN); %! assert_equal (p, NaN); %! assert_equal (labels, cell (0, 2)); %!test %! w = warning (); %! warning ('off'); %! [t, chisq, p, labels] = crosstab (int8 ([1 2 10 1]), [-1 0.5 2 -1]); %! warning (w); %! assert_equal (t, [2 0 0; 0 1 0; 0 0 1]); %! assert_equal (chisq, 8, 1e-12); %! assert_equal (labels, {'1', '-1'; '2', '0.5'; '10', '2'}); ## Test a single input %!test %! [t, chisq, p, labels] = crosstab ([1 2 NaN 1 2]); %! assert_equal (t, [2; 2]); %! assert_equal (chisq, NaN); %! assert_equal (p, NaN); %! assert_equal (labels, {'1'; '2'}); %!test %! [t, chisq, p, labels] = crosstab ([1 1 2 2 2 3]'); %! assert_equal (t, [2; 3; 1]); %! assert_equal (chisq, NaN); %! assert_equal (p, NaN); %! assert_equal (labels, {'1'; '2'; '3'}); %!test %! [t, chisq, p, labels] = crosstab (1); %! assert_equal (t, 1); %! assert_equal (chisq, NaN); %! assert_equal (labels, {'1'}); %!test %! [t, ~, ~, labels] = crosstab (NaN); %! assert_equal (t, zeros (0, 1)); %! assert_equal (labels, cell (0, 1)); %!test %! [t, ~, ~, labels] = crosstab ('abca'); %! assert_equal (t, 1); %! assert_equal (labels, {'abca'}); %!test %! [t, ~, ~, labels] = crosstab (['ab'; 'cd'; 'ab']); %! assert_equal (t, [2; 1]); %! assert_equal (labels, {'ab'; 'cd'}); %!test %! c = categorical ({'a'; 'b'; 'a'}, {'a', 'b', 'c'}); %! [t, ~, ~, labels] = crosstab (c); %! assert_equal (t, [2; 1; 0]); %! assert_equal (labels, {'a'; 'b'; 'c'}); %!test %! t = crosstab ([1 2 NaN 1 2], 'IncludeMissingGroups', true); %! assert_equal (t, [2; 2; 1]); ## Test IncludeMissingGroups %!test %! ## missing values are counted as a group of their own %! x = [1 2 NaN 1 2]; %! y = [1 1 2 NaN 2]; %! w = warning (); %! warning ('off'); %! [t, chisq, p, labels] = crosstab (x, y, 'IncludeMissingGroups', true); %! warning (w); %! assert_equal (t, [1 0 1; 1 1 0; 0 1 0]); %! assert_equal (chisq, 3.75, 1e-12); %! assert_equal (p, 0.440896, 1e-6); %! assert_equal (labels, {'1', '1'; '2', '2'; 'NaN', 'NaN'}); %!test %! ## missing values in several inputs share the combined cells %! x = [1 NaN 2 NaN]'; %! y = [NaN NaN 1 2]'; %! z = [1 1 NaN 2]'; %! w = warning (); %! warning ('off'); %! [t, chisq, p] = crosstab (x, y, z, 'IncludeMissingGroups', true); %! warning (w); %! expected = zeros (3, 3, 3); %! expected([7 9 15 20]) = 1; %! assert_equal (t, expected); %! assert_equal (chisq, 26, 1e-12); %! assert_equal (p, 0.165812, 1e-6); %!test %! ## the missing group of categorical, cellstr and string inputs %! y = [1; 2; 1; 2]; %! w = warning (); %! warning ('off'); %! [t1, ~, ~, l1] = crosstab (categorical ({'a'; 'b'; ''; 'a'}), y, ... %! 'IncludeMissingGroups', true); %! [t2, ~, ~, l2] = crosstab ({'a'; 'b'; ''; 'a'}, y, ... %! 'IncludeMissingGroups', true); %! [t3, ~, ~, l3] = crosstab (string ({'a'; 'b'; missing; 'a'}), y, ... %! 'IncludeMissingGroups', true); %! warning (w); %! assert_equal (t1, [1 1; 0 1; 1 0]); %! assert_equal (t2, t1); %! assert_equal (t3, t1); %! assert_equal ({l1{3,1}, l2{3,1}, l3{3,1}}, ... %! {'', "{''}", ''}); %!test %! ## the missing group of datetime, duration and char inputs %! y = [1; 2; 1; 2]; %! d = [datetime(2026, 1, 1); NaT; datetime(2026, 1, 1); datetime(2025, 1, 1)]; %! w = warning (); %! warning ('off'); %! [t1, ~, ~, l1] = crosstab (d, y, 'IncludeMissingGroups', true); %! [t2, ~, ~, l2] = crosstab (hours ([1; NaN; 2; 1]), y, ... %! 'IncludeMissingGroups', true); %! [~, ~, ~, l3] = crosstab (days ([1; NaN; 2; 1]), y, ... %! 'IncludeMissingGroups', true); %! [t4, ~, ~, l4] = crosstab (['ab'; ' '; 'ab'; 'cd'], y, ... %! 'IncludeMissingGroups', true); %! warning (w); %! assert_equal (t1, [0 1; 2 0; 0 1]); %! assert_equal (t2, [1 1; 1 0; 0 1]); %! assert_equal (t4, [2 0; 0 1; 0 1]); %! assert_equal ({l1{3,1}, l2{3,1}, l3{3,1}, l4{3,1}}, ... %! {'NaT', 'NaN hr', 'NaN days', ' '}); %!test %! t = crosstab ([1 2 NaN 1 2], [1 1 2 NaN 2], 'includemissing', true); %! assert_equal (t, [1 0 1; 1 1 0; 0 1 0]); %!test %! t = crosstab ([1 2 NaN 1 2], [1 1 2 NaN 2], 'IncludeMissingGroups', ... %! single (2)); %! assert_equal (t, [1 0 1; 1 1 0; 0 1 0]); %!test %! t = crosstab ([1 2 NaN 1 2], [1 1 2 NaN 2], 'IncludeMissingGroups', ... %! true, 'IncludeMissingGroups', false); %! assert_equal (t, [1 0; 1 1]); statistics-release-2.1.2/inst/Data_Manipulation/datasample.m000066400000000000000000000261511527311104300241650ustar00rootroot00000000000000## Copyright (C) 2021 Stefano Guidoni ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} datasample (@var{data}, @var{k}) ## @deftypefnx {statistics} {@var{y} =} datasample (@var{data}, @var{k}, @var{dim}) ## @deftypefnx {statistics} {@var{y} =} datasample (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{y} @var{idcs}] =} datasample (@dots{}) ## ## Randomly sample data. ## ## Return @var{k} observations randomly sampled from @var{data}. @var{data} can ## be a vector or a matrix of any data. When @var{data} is a matrix or a ## n-dimensional array, the samples are the subarrays of size n - 1, taken along ## the dimension @var{dim}. The default value for @var{dim} is 1, that is the ## row vectors when sampling a matrix. ## ## Output @var{y} is the returned sampled data. Optional output @var{idcs} is ## the vector of the indices to build @var{y} from @var{data}. ## ## Additional options are set through pairs of parameter name and value. ## Available parameters are: ## ## @table @code ## @item @qcode{Replace} ## a logical value that can be @code{true} (default) or @code{false}: when set ## to @code{true}, @code{datasample} returns data sampled with replacement. ## ## @item @qcode{Weights} ## a vector of non-negative numbers, at least one of them positive, that sets ## the probability of each element. It must have the same size as @var{data} ## along dimension @var{dim}. Elements with an @code{Inf} weight are sampled ## before any other: always when sampling with replacement, first when sampling ## without. Elements sharing an @code{Inf} weight are equally likely, whereas ## MATLAB draws only the first of them when sampling with replacement. ## ## @end table ## ## ## @seealso{rand, randi, randperm, randsample} ## ## @end deftypefn function [y, idcs] = datasample (data, k, varargin) ## check input if ( nargin < 2 ) print_usage (); endif ## data: some data, any type, any format but cell ## MATLAB compatibility: there are no "table" or "dataset array" types in ## Octave if (iscell (data)) error ("datasample: data must be a vector or matrix"); endif ## k, a non-negative integer if ((! isnumeric (k) || ! isscalar (k)) || (! (floor (k) == k)) || (k < 0)) error ("datasample: k must be a non-negative integer scalar"); endif dim = 1; replace = true; weights = []; if ( nargin > 2 ) pair_index = 1; if (! ischar (varargin{1})) ## it must be dim dim = varargin{1}; ## the (Name, Value) pairs start further pair_index += 1; ## dim, another positive integer if ((! isscalar (dim)) || (! (floor (dim) == dim)) || (dim <= 0)) error ("datasample: DIM must be a positive integer scalar"); endif endif ## (Name, Value) pairs while (pair_index < (nargin - 2)) switch (lower (varargin{pair_index})) case 'replace' if (! islogical (varargin{pair_index + 1})) error ("datasample: expected a logical value for 'Replace'"); endif replace = varargin{pair_index + 1}; case 'weights' weights = varargin{pair_index + 1}; if (! (isnumeric (weights) && isvector (weights)) || any (weights < 0 | isnan (weights)) || ! any (weights > 0)) error (strcat ("datasample: the sampling weights must be a", ... " vector of non-negative values with at least", ... " one positive value.")); endif otherwise error ("datasample: unknown property %s", varargin{pair_index}); endswitch pair_index += 2; endwhile endif ## get the size of the population to sample if (isvector (data)) imax = length (data); else imax = size (data, dim); endif if (isempty (weights)) ## all elements have the same probability of being chosen ## this is easy ## with or without replacement if (k == 0) idcs = zeros (0, 1); elseif (replace) idcs = randi (imax, k, 1); else idcs = randperm (imax, k); endif else ## first check if the weights vector is right if (imax != length (weights)) error (strcat ("datasample: the size of the vector of sampling weights must", " be equal to the size of the sampled data")); endif if (replace) idcs = zeros (k, 1); ## easy case: ## normalize the weights, if (any (isinf (weights))) weights_n = cumsum (weights == Inf) / sum (weights == Inf); else weights_n = cumsum (weights ./ sum (weights)); endif weights_n(end) = 1; # just to be sure ## then choose k numbers uniformly between 0 and 1 samples = rand (k, 1); ## we have subdivided the space between 0 and 1 accordingly to the ## weights vector: we have just to map back the random numbers to the ## indices of the original dataset for iter = 1 : k idcs(iter) = find (weights_n >= samples(iter), 1); endfor else ## complex case idcs = zeros (k, 1); if (k > imax) error (strcat ("datasample: K must not exceed the number of", ... " available elements when sampling without replacement.")); endif if (k > sum (weights > 0)) error (strcat ("datasample: sampling without replacement", ... " requires at least K elements with positive weights.")); endif ## choose k numbers uniformly between 0 and 1 samples = rand (k, 1); for iter = 1 : k if (any (isinf (weights))) inf_idx = find (isinf (weights)); idcs(iter) = inf_idx(randi (length (inf_idx))); else weights_n = cumsum (weights ./ sum (weights)); weights_n(end) = 1; idcs(iter) = find (weights_n >= samples(iter), 1); endif ## remove the element from the set, i. e. set its probability to zero weights(idcs(iter)) = 0; endfor endif endif ## let's get the actual data from the original set if (isvector (data)) ## data is a vector y = data(idcs); else vS = size (data); if (length (vS) == 2) ## data is a 2-dimensional matrix if (dim == 1) y = data(idcs, :); else y = data(:, idcs); endif else ## data is an n-dimensional matrix s = 'y = data('; for iter = 1 : length (vS) if (iter == dim) s = [s 'idcs,']; else s = [s ':,']; endif endfor s = [s ':);']; eval (s); endif endif endfunction ## some tests %!error datasample (); %!error datasample (1); %!error datasample ({1, 2, 3}, 1); %!error ... %! datasample ([1 2], -1); %!error ... %! datasample ([1 2], 1.5); %!error ... %! datasample ([1 2], [1 1]); %!error ... %! datasample ([1 2], 'g', [1 1]); %!error datasample ([1 2], 1, -1); %!error datasample ([1 2], 1, 1.5); %!error datasample ([1 2], 1, [1 1]); %!error datasample ([1 2], 1, 1, 'Replace', -2); %!error ... %! datasample ([1 2], 1, 1, 'Weights', 'abc'); %!error ... %! datasample ([1 2], 1, 1, 'Weights', [1 -2 3]); %!error ... %! datasample ([1 2], 1, 1, 'Weights', ones (2)); %!error ... %! datasample ([1 2 3 4 5], 2, 'Weights', [NaN 1 1 1 1]); %!error ... %! datasample ([1 2 3 4 5], 2, 'Weights', [0 0 0 0 0]); %!error ... %! datasample ([1 2 3 4 5], 1, 'Weights', [0 0 0 0 0], 'Replace', false); %!error datasample ([1 2], 1, 1, 'Weights', [1 2 3]); %!error ... %! data = 1:5; weights = [0.077846, 0.103765, 0.703748, 0.840937, 0.422901]; %! sampled = datasample (data, 8, 'Weights', weights, 'Replace', false); %!error ... %! data = 1:5; weights = [1, 0, 1, 0, 0]; %! sampled = datasample (data, 3, 'Weights', weights, 'Replace', false); %!test %! dat = randn (10, 4); %! assert_equal (size (datasample (dat, 3, 1)), [3 4]); %!test %! dat = randn (10, 4); %! assert_equal (size (datasample (dat, 3, 2)), [10 3]); %!test %! ## Edge cases with empty arrays and k=0 %! assert_equal (datasample ([], 0), zeros (0, 0)); %! assert_equal (datasample (zeros (0, 3), 0), zeros (0, 3)); %!test %! ## k = 0 with weights, sampling with replacement %! assert_equal (datasample ([1 2 3], 0, 'Weights', [1 1 1]), zeros (1, 0)); %!test %! ## k = 0 with weights, sampling without replacement %! assert_equal (datasample ([1 2 3], 0, 'Replace', false, ... %! 'Weights', [1 1 1]), zeros (1, 0)); %!test %! ## Inf weight with replacement always selects the Inf-weighted element %! assert_equal (datasample ([1 2 3 4 5], 2, ... %! 'Weights', [Inf 1 1 1 1], ... %! 'Replace', true), [1 1]); %!test %! ## Several Inf weights with replacement draw among them alone %! sampled = datasample (1:5, 20, 'Weights', [Inf 1 Inf 1 1]); %! assert_equal (all (sampled == 1 | sampled == 3), true); %!test %! ## Inf weight without replacement selects the Inf-weighted element first %! sampled = datasample ([1 2 3 4 5], 2, ... %! 'Weights', [Inf 1 1 1 1], ... %! 'Replace', false); %! assert_equal (sampled(1), 1); %!test %! ## Several Inf weights without replacement are all drawn first %! sampled = datasample (1:5, 3, 'Weights', [Inf 1 Inf 1 1], 'Replace', false); %! assert_equal (sort (sampled(1:2)), [1 3]); statistics-release-2.1.2/inst/Data_Manipulation/doc-cache000066400000000000000000001121101527311104300234140ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 14 # name: # type: sq_string # elements: 1 # length: 6 combnk # name: # type: sq_string # elements: 1 # length: 203 statistics: c = combnk (data, k) Return all combinations of k elements in data. k must be a non-negative integer. combnk (data, 0) returns a 1-by-0 array of the class of data, the one empty combination. # name: # type: sq_string # elements: 1 # length: 46 Return all combinations of k elements in data. # name: # type: sq_string # elements: 1 # length: 8 crosstab # name: # type: sq_string # elements: 1 # length: 2132 statistics: t = crosstab (x1) statistics: t = crosstab (x1, x2) statistics: t = crosstab (x1, ..., xn) statistics: [t, chisq, p, labels] = crosstab (...) statistics: [...] = crosstab (..., Name, Value) Create a cross-tabulation (contingency table) t from data vectors. The inputs x1, x2, ... xn must be vectors of equal length with a data type of numeric, logical, char array, categorical, strings, datetime, duration, or cell array of character vectors. Missing values, such as NaN or NaT, are not counted by default. A NaN duration is missing too, whereas MATLAB counts it as a group of its own. The following Name-Value pair is supported: Name Value 'IncludeMissingGroups' A real double, single or logical scalar that is not NaN (default false). When nonzero or true, the missing values are counted as a group of their own, placed after the other groups of every input that holds any. The name is not case sensitive and may be shortened to 'IncludeMissing'; any other text is read as data. Only its last occurrence counts. The label of the missing group is NaN for numeric inputs, for categorical, for strings, {''} for cell arrays of character vectors, NaT for datetime, the text of a NaN element for duration, and the blank row itself for a char array. As additional return values crosstab returns the chi-square statistics chisq, its p-value p and a cell array labels, containing the labels of each input argument. With a single input, t is a column holding the count of each group, and chisq and p are NaN, there being no second variable to test independence against. A char row is a single observation. A categorical input has a row or column for every one of its categories, an unused one included, holding zeros. The chi-square statistic and its p-value are computed over the rows and columns that hold observations, where MATLAB returns NaN for both whenever any row or column is empty. With the missing group counted, it follows every category here, whereas MATLAB returns a table one row longer than its labels when an unused category follows the last used one. See also: grp2idx, tabulate # name: # type: sq_string # elements: 1 # length: 66 Create a cross-tabulation (contingency table) t from data vectors. # name: # type: sq_string # elements: 1 # length: 10 datasample # name: # type: sq_string # elements: 1 # length: 1296 statistics: y = datasample (data, k) statistics: y = datasample (data, k, dim) statistics: y = datasample (..., Name, Value) statistics: [y idcs] = datasample (...) Randomly sample data. Return k observations randomly sampled from data. data can be a vector or a matrix of any data. When data is a matrix or a n-dimensional array, the samples are the subarrays of size n - 1, taken along the dimension dim. The default value for dim is 1, that is the row vectors when sampling a matrix. Output y is the returned sampled data. Optional output idcs is the vector of the indices to build y from data. Additional options are set through pairs of parameter name and value. Available parameters are: Replace a logical value that can be true (default) or false: when set to true, datasample returns data sampled with replacement. Weights a vector of non-negative numbers, at least one of them positive, that sets the probability of each element. It must have the same size as data along dimension dim. Elements with an Inf weight are sampled before any other: always when sampling with replacement, first when sampling without. Elements sharing an Inf weight are equally likely, whereas MATLAB draws only the first of them when sampling with replacement. See also: rand, randi, randperm, randsample # name: # type: sq_string # elements: 1 # length: 21 Randomly sample data. # name: # type: sq_string # elements: 1 # length: 8 dummyvar # name: # type: sq_string # elements: 1 # length: 1653 statistics: D = dummyvar (group) Create dummy variables. D = dummyvar (group) returns a matrix D containing the dummy variables associated with the grouping variables in group. Each row in D corresponds to the same observation across all variables in group and each column in D corresponds to a separate dummy variable. D is a numeric matrix containing ones and zeros, of single data type when group is a single array and of double data type otherwise. The grouping variable in group can be specified in one of the following options: a positive integer vector representing the different group levels in the ordered range 1:max (group). a positive integer matrix with each column corresponding to a separate grouping variable and the integer values representing the group levels within that grouping variable in the ordered range 1:max (group). A NaN in a column produces a row of NaN values in that column's own block of dummy variables in D, without affecting the columns produced by any other grouping variable. A column that is entirely NaN has no levels, so it produces no dummy variables at all and a warning names it. a categorical column vector, in which case the number and order of columns in D correspond to the categories returned by categories (group). Categories that are defined but not present in group produce columns of zeros. Elements of group that are result in rows of NaN values in D. a cell array with its elements containing grouping variables specified as any of the above options. Note that all grouping variables in the cell array must have the same number of observations. See also: tabulate, grp2idx, grpstats # name: # type: sq_string # elements: 1 # length: 23 Create dummy variables. # name: # type: sq_string # elements: 1 # length: 11 fillmissing # name: # type: sq_string # elements: 1 # length: 7374 statistics: B = fillmissing (A, "constant", v) statistics: B = fillmissing (A, method) statistics: B = fillmissing (A, move_method, window_size) statistics: B = fillmissing (A, fill_function, window_size) statistics: B = fillmissing (..., dim) statistics: B = fillmissing (..., PropertyName, PropertyValue) statistics: [B, idx] = fillmissing (...) Fill missing data in arrays. Replace missing entries of array A either with values in v or as determined by other specified methods. 'missing' values are determined by the data type of A as identified by the function ismissing, currently defined as: Standard missing values and their corresponding data types are: NaN - for double, single, duration, and calendarDuration arrays. NaT - for datetime arrays. - for string arrays. - for categorical arrays. {0x0 char} - for cell arrays of character vectors. For any data types that do not support missing values, ismissing returns TF = false (size (A)). A can be a numeric scalar or array, a character vector or array, or a cell array of character vectors (a.k.a. string cells). v can be a scalar or an array containing values for replacing the missing values in A with a compatible data type for insertion into A. The shape of v must be a scalar or an array with number of elements in v equal to the number of elements orthogonal to the operating dimension. E.g., if size(A) = [3 5 4], operating along dim = 2 requires v to contain either 1 or 3x4=12 elements. If requested, the optional output idx will contain a logical array the same shape as A indicating with 1's which locations in A were filled. Alternate Input Arguments and Values: method - replace missing values with: next previous nearest next, previous, or nearest non-missing value (nearest defaults to next when equidistant as determined by SamplePoints.) linear linear interpolation of neighboring, non-missing values spline piecewise cubic spline interpolation of neighboring, non-missing values pchip 'shape preserving' piecewise cubic spline interpolation of neighboring, non-missing values move_method - moving window calculated replacement values: movmean movmedian moving average or median using a window determined by window_size. window_size must be either a positive scalar value or a two element positive vector of sizes [nb, na] measured in the same units as SamplePoints. For scalar values, the window is centered on the missing element and includes all data points within a distance of half of window_size on either side of the window center point. Note that for compatibility, when using a scalar value, the backward window limit is inclusive and the forward limit is exclusive. If a two-element window_size vector is specified, the window includes all points within a distance of nb backward and na forward from the current element at the window center (both limits inclusive). fill_function - custom method specified as a function handle. The supplied fill function must accept three inputs in the following order for each missing gap in the data: A_values - elements of A within the window on either side of the gap as determined by window_size. (Note these elements can include missing values from other nearby gaps.) A_locs - locations of the reference data, A_values, in terms of the default or specified SamplePoints. gap_locs - location of the gap data points that need to be filled in terms of the default or specified SamplePoints. The supplied function must return a scalar or vector with the same number of elements in gap_locs. The required window_size parameter follows similar rules as for the moving average and median methods described above, with the two exceptions that (1) each gap is processed as a single element, rather than gap elements being processed individually, and (2) the window extended on either side of the gap has inclusive endpoints regardless of how window_size is specified. dim - specify a dimension for vector operation (default = first non-singeton dimension) A one-sided window_size, [nb, 0] or [0, na], leaves the gap at the corresponding end of the data with no values in its window. The fill function is still called there, with empty value and location arguments, as MATLAB calls it: a fill function may depend only on the gap's own sample points. One that cannot take empty arguments is reported as such. Along a dimension in which A is singleton, every element is a vector of length one, so a missing value has no neighbour to be filled from and is returned unchanged. MATLAB fills it regardless, which is a defect there and not a convention this follows: it returns [1, 1, 3, 1, 5] for fillmissing ([1, NaN, 3, NaN, 5], @testfcn, 99, 3), inventing values from a window that contains nothing. Measured against R2024a, two releases after the behaviour was first recorded here. PropertyName-PropertyValue pairs SamplePoints PropertyValue is a vector of sample point values representing the sorted and unique x-axis values of the data in A. If unspecified, the default is assumed to be the vector [1 : size (A, dim)]. The values in SamplePoints will affect methods and properties that rely on the effective distance between data points in A, such as interpolants and moving window functions where the window_size specified for moving window functions is measured relative to the SamplePoints. EndValues Apply a separate handling method for missing values at the front or back of the array. PropertyValue can be: A constant scalar or array with the same shape requirements as v. none - Do not fill end gap values. extrap - Use the same procedure as method to fill the end gap values. Any valid method listed above except for movmean, movmedian, and fill_function. Those methods can only be applied to end gap values with extrap. MissingLocations PropertyValue must be a logical array the same size as A indicating locations of known missing data with a value of true. (cannot be combined with MaxGap) MaxGap PropertyValue is a numeric scalar indicating the maximum gap length to fill, and assumes the same distance scale as the sample points. Gap length is calculated by the difference in locations of the sample points on either side of the gap, and gaps larger than MaxGap are ignored by fillmissing. (cannot be combined with MissingLocations) Compatibility Notes: Numerical and logical inputs for A and v may be specified in any combination. The output will be the same class as A, with the v converted to that data type for filling. Only single and double have defined 'missing' values, so except for when the missinglocations option specifies the missing value identification of logical and other numeric data types, the output will always be B = A with idx = false(size(A)). All interpolation methods can be individually applied to EndValues. MATLAB's fill_function method currently has several inconsistencies with the other methods (tested against version 2022a), and Octave's implementation has chosen the following consistent behavior over compatibility: (1) a column full of missing data is considered part of EndValues, (2) such columns are then excluded from fill_function processing because the moving window is always empty. (3) operation in dimensions higher than 2 perform identically to operations in dims 1 and 2, most notable on vectors. See also: ismissing, rmmissing, standardizeMissing # name: # type: sq_string # elements: 1 # length: 28 Fill missing data in arrays. # name: # type: sq_string # elements: 1 # length: 7 grp2idx # name: # type: sq_string # elements: 1 # length: 2059 statistics: g = grp2idx (s) statistics: [g, gn] = grp2idx (s) statistics: [g, gn, gl] = grp2idx (s) Get index for grouping variable. g = grp2idx (s) returns a numeric column vector of integer values g indexing the distinct groups in the grouping variable s. s can specified as any of the following data types: categorical vector cell array of character vectors character array datetime vector duration vector logical vector numeric vector s must be a vector, unless it is a 2-D character array. In the case of numerical, logical, datetime, and duration data types, the group indices are ordered in sorted order of s. In the case of categorical arrays, the group indices are allocated by the order of the categories in s. For the rest of the data types, the group indices are allocated by order of first appearance in s. Note that in case of a categorical grouping variable, the indexing integer values might not be continuous, since s may contain unassigned categories. For every other data type, g will contain integer values in the range [1:K], where K is the number of distinct groups in s. [g, gn] = grp2idx (s) also returns a cell array of character vectors gn representing the list of group names. The order of the group names in gn follow the same pattern as the group indices in g according to the data type of s, as described above. [g, gn, gl] = grp2idx (s) further returns a column vector gl representing the list of the group levels with the same data type as s. Note that standard missing values in s, such as NaN or NaT, appear as NaN in g and are not present on either gn and gl. This includes a NaN duration, which MATLAB makes a group of its own. An empty s returns g, gn and gl as 0-by-1 arrays, whereas MATLAB returns a 0-by-0 gn for an empty categorical s. A character array without rows returns a 0-by-1 g and gn, and a gl with no rows. Each row of a character array is an observation, so a row without characters, being unnamed, is indexed as NaN, whereas MATLAB returns no index for any empty character array. See also: grpstats # name: # type: sq_string # elements: 1 # length: 32 Get index for grouping variable. # name: # type: sq_string # elements: 1 # length: 9 ismissing # name: # type: sq_string # elements: 1 # length: 2322 statistics: TF = ismissing (A) statistics: TF = ismissing (A, indicator) Find missing data in arrays. TF = ismissing (A) returns a logical array, TF, with the same dimensions as A, where true values match the standard missing values in the input data according to their data type. Standard missing values and their corresponding data types are: NaN - for double, single, duration, and calendarDuration arrays. NaT - for datetime arrays. - for string arrays. - for categorical arrays. {0x0 char} - for cell arrays of character vectors. For any data types that do not support missing values, ismissing returns TF = false (size (A)). Note: the generic ismissing function from the statistics package only operates on core Octave datatypes and it explicitly identifies missing values in double and single arrays, as well as in cell arrays of character vectors. All other data types are handled by the overloaded methods from their respective data class from the datatypes package. Use help class_name.ismissing to find more information about the functional specialization of their respective class implementation. The optional input indicator can be a scalar or a vector, of the same type as the input data A, specifying alternative missing values in the input data. When specifying indicator values, the standard missing values are ignored, unless explicitly stated in the indicator. Additional data type matches between indicator and A are: double indicators also match single, all integer types, and logical data in A. string and char indicators also match categorical data in A. char and cellstr indicators also match string data in A. single, integer, and logical indicators also match double data in A. Any other pairing of numeric and logical classes between indicator and A is an error, such as an int32 indicator with single data in A. Note: the generic ismissing function from the statistics package only accepts indicator argument for numeric, logical, and char arrays, as well as for cell arrays of character vectors. For all other core Octave data types, ismissing produces an error. However, indicator is supported for data classes from the datatypes package through their respective class implementation of overloaded methods. See also: fillmissing, rmmissing, standardizeMissing # name: # type: sq_string # elements: 1 # length: 28 Find missing data in arrays. # name: # type: sq_string # elements: 1 # length: 9 isoutlier # name: # type: sq_string # elements: 1 # length: 6712 statistics: TF = isoutlier (x) statistics: TF = isoutlier (x, method) statistics: TF = isoutlier (x, 'percentiles', threshold) statistics: TF = isoutlier (x, movmethod, window) statistics: TF = isoutlier (..., dim) statistics: TF = isoutlier (..., Name, Value) statistics: [TF, L, U, C] = isoutlier (...) Find outliers in data isoutlier (x) returns a logical array whose elements are true when an outlier is detected in the corresponding element of x. isoutlier treats NaNs as missing values and removes them. If x is a matrix, then isoutlier operates on each column of x separately. If x is a multidimensional array, then isoutlier operates along the first dimension of x whose size does not equal 1. By default, an outlier is a value that is more than three scaled median absolute deviations (MAD) from the median. The scaled median is defined as c*median(abs(A-median(A))), where c=-1/(sqrt(2)*erfcinv(3/2)). isoutlier (x, method) specifies a method for detecting outliers. The following methods are available: Method Description 'median' Outliers are defined as elements more than three scaled MAD from the median. 'mean' Outliers are defined as elements more than three standard deviations from the mean. 'quartiles' Outliers are defined as elements more than 1.5 interquartile ranges above the upper quartile (75 percent) or below the lower quartile (25 percent). This method is useful when the data in x is not normally distributed. 'grubbs' Outliers are detected using Grubbs’ test for outliers, which removes one outlier per iteration based on hypothesis testing. This method assumes that the data in x is normally distributed. 'gesd' Outliers are detected using the generalized extreme Studentized deviate test for outliers. This iterative method is similar to 'grubbs', but can perform better when there are multiple outliers masking each other. isoutlier (x, 'percentiles', threshold) detects outliers based on a percentile thresholds, specified as a two-element row vector whose elements are in the interval [0, 100]. The first element indicates the lower percentile threshold, and the second element indicates the upper percentile threshold. The first element of threshold must be less than the second element. isoutlier (x, movmethod, window) specifies a moving method for detecting outliers. The following methods are available: Method Description 'movmedian' Outliers are defined as elements more than three local scaled MAD from the local median over a window length specified by window. 'movmean' Outliers are defined as elements more than three local standard deviations from the from the local mean over a window length specified by window. window must be a positive integer scalar or a two-element vector of positive integers. When window is a scalar, if it is an odd number, the window is centered about the current element and contains window - 1 neighboring elements. If even, then the window is centered about the current and previous elements. When window is a two-element vector of positive integers [nb, na], the window contains the current element, nb elements before the current element, and na elements after the current element. When 'SamplePoints' are also specified, window can take any real positive values (either as a scalar or a two-element vector) and in this case, the windows are computed relative to the sample points. dim specifies the operating dimension and it must be a positive integer scalar. If not specified, then, by default, isoutlier operates along the first non-singleton dimension of x. The following optional parameters can be specified as Name/Value paired arguments. 'SamplePoints' can be specified as a vector of sample points with equal length as the operating dimension. The sample points represent the x-axis location of the data and must be sorted and contain unique elements. Sample points do not need to be uniformly sampled. By default, the vector is [1, 2, 3, ..., n], where n = size (x, dim). You can use unequally spaced 'SamplePoints' to define a variable-length window for one of the moving methods available. 'ThresholdFactor' can be specified as a nonnegative scalar. For methods 'median' and 'movmedian', the detection threshold factor replaces the number of scaled MAD, which is 3 by default. For methods 'mean' and 'movmean', the detection threshold factor replaces the number of standard deviations, which is 3 by default. For methods 'grubbs' and 'gesd', the detection threshold factor ranges from 0 to 1, specifying the critical alpha-value of the respective test, and it is 0.05 by default. For the 'quartiles' method, the detection threshold factor replaces the number of interquartile ranges, which is 1.5 by default. A factor of 0 for 'grubbs' or 'gesd' detects no outliers and returns NaN for L and U, as MATLAB does. With 'grubbs', a positive factor as small as 1e-300 still gives finite thresholds, where MATLAB returns NaN. 'MaxNumOutliers' is only relevant to the 'gesd' method and it must be a positive integer scalar specifying the maximum number of outliers returned by the 'gesd' method. By default, it is the integer nearest to the 10% of the number of elements along the operating dimension in x. The 'gesd' method assumes the nonoutlier input data is sampled from an approximate normal distribution. When the data is not sampled in this way, the number of returned outliers might exceed the MaxNumOutliers value. [TF, L, U, C] = isoutlier (...) returns up to 4 output arguments as described below. TF is the outlier indicator with the same size a x. L is the lower threshold used by the outlier detection method. If method is used for outlier detection, then L has the same size as x in all dimensions except for the operating dimension where the length is 1. If movmethod is used, then L has the same size as x. U is the upper threshold used by the outlier detection method. If method is used for outlier detection, then U has the same size as x in all dimensions except for the operating dimension where the length is 1. If movmethod is used, then U has the same size as x. C is the center value used by the outlier detection method. If method is used for outlier detection, then C has the same size as x in all dimensions except for the operating dimension where the length is 1. If movmethod is used, then C has the same size as x. For 'median', 'movmedian', 'mean', and 'movmean' methods, C is computed by taking into account the outlier values. For 'grubbs' and 'gesd' methods, C is computed by excluding the outliers. For the 'percentiles' method, C is the average between U and L thresholds. For the 'quartiles' method, C is the average of the 25th and 75th percentiles. See also: ismissing # name: # type: sq_string # elements: 1 # length: 21 Find outliers in data # name: # type: sq_string # elements: 1 # length: 8 multiway # name: # type: sq_string # elements: 1 # length: 1829 statistics: groupindex = multiway (numbers, num_parts) statistics: groupindex = multiway (numbers, num_parts, method) statistics: [groupindex, partition] = multiway (...) statistics: [groupindex, partition, groupsizes] = multiway (...) Solve the multiway number partitioning problem. groupindex = multiway (numbers, num_parts) splits a set of numbers in numbers into a number of subsets specified in num_parts such that the sums of the subsets are nearly as equal as possible and returns a vector of group indices in groupindex with each index corresponding to the set of numbers provided as input. numbers is a vector of positive real numbers to be partitioned. num_parts is a positive integer scalar specifying the number of partitions (subsets) to split the numbers into. groupindex = multiway (numbers, num_parts, method) also specifies the algorithm used for partitioning the set of numbers. By default, multiway uses the complete Karmarkar-Karp algorithm, when the set of numbers contains up to 10 elements and the requested number of subsets does not exceed 5, otherwise it defaults to the greedy algorithm, which is optimized for speed, but may not return the optimal partitioning. The following methods are supported: 'greedy' (Greedy algorithm) 'completeKK' (Complete Karmarkar-Karp algorithm) The multiway function may return up to three output arguments described below: groupindex: A vector of the same length as numbers containing the group index (from 1 to num_parts) for each number. partition: A cell array of length num_parts with each cell containing the numbers assigned to that partition. groupsizes: A vector of the sums of the numbers in each partition. Example: numbers = [4, 5, 6, 7, 8]; num_parts = 2; [groupindex, partition, groupsizes] = multiway (numbers, num_parts); See also: cvpartition # name: # type: sq_string # elements: 1 # length: 47 Solve the multiway number partitioning problem. # name: # type: sq_string # elements: 1 # length: 22 normalise_distribution # name: # type: sq_string # elements: 1 # length: 1874 statistics: NORMALISED = normalise_distribution (DATA) statistics: NORMALISED = normalise_distribution (DATA, DISTRIBUTION) statistics: NORMALISED = normalise_distribution (DATA, DISTRIBUTION, DIMENSION) Transform a set of data so as to be N(0,1) distributed according to an idea by van Albada and Robinson. This is achieved by first passing it through its own cumulative distribution function (CDF) in order to get a uniform distribution, and then mapping the uniform to a normal distribution. The data must be passed as a vector or matrix in DATA. If the CDF is unknown, then [] can be passed in DISTRIBUTION, and in this case the empirical CDF will be used. Otherwise, if the CDFs for all data are known, they can be passed in DISTRIBUTION, either in the form of a single function name as a string, or a single function handle, or a cell array consisting of either all function names as strings, or all function handles. In the latter case, the number of CDFs passed must match the number of rows, or columns respectively, to normalise. If the data are passed as a matrix, then the transformation will operate either along the first non-singleton dimension, or along DIMENSION if present. Notes: The empirical CDF will map any two sets of data having the same size and their ties in the same places after sorting to some permutation of the same normalised data: normalise_distribution([1 2 2 3 4]) => -1.28 0.00 0.00 0.52 1.28 normalise_distribution([1 10 100 10 1000]) => -1.28 0.00 0.52 0.00 1.28 Original source: S.J. van Albada, P.A. Robinson "Transformation of arbitrary distributions to the normal distribution with application to EEG test-retest reliability" Journal of Neuroscience Methods, Volume 161, Issue 2, 15 April 2007, Pages 205-211 ISSN 0165-0270, 10.1016/j.jneumeth.2006.11.004. (http://www.sciencedirect.com/science/article/pii/S0165027006005668) # name: # type: sq_string # elements: 1 # length: 103 Transform a set of data so as to be N(0,1) distributed according to an idea by van Albada and Robinson. # name: # type: sq_string # elements: 1 # length: 10 randsample # name: # type: sq_string # elements: 1 # length: 778 statistics: y = randsample (v, k) statistics: y = randsample (v, k, replacement=false) statistics: y = randsample (v, k, replacement=false, [w=[]]) Sample elements from a vector. Returns k random elements from a vector v with n elements, sampled without or with replacement, with an optional weight vector. If v is a non-negative integer scalar, samples from 1:v; it must not be Inf. Any other scalar, -Inf and NaN included, is a population of one element. k must be finite. If a weight vector w of the same size as v is specified, the probability of each element being sampled is proportional to w. Unlike Matlab's function of the same name, this can be done for sampling with or without replacement. Randomization is performed using rand(). See also: datasample, randperm # name: # type: sq_string # elements: 1 # length: 30 Sample elements from a vector. # name: # type: sq_string # elements: 1 # length: 9 rmmissing # name: # type: sq_string # elements: 1 # length: 2028 statistics: R = rmmissing (A) statistics: R = rmmissing (A, dim) statistics: R = rmmissing (..., Name, Value) statistics: [R, TF] = rmmissing (...) Remove missing data from arrays. Given an input vector or matrix (2-D array) A, R = rmmissing (A) returns an output vector or matrix R of the same type as input A and any missing elements removed. If A is a vector, missing elements are removed individually, if A is a matrix, then rows containing missing elements are removed. Standard missing values and their corresponding data types are: NaN - for double, single, duration, and calendarDuration arrays. NaT - for datetime arrays. - for string arrays. - for categorical arrays. {0x0 char} - for cell arrays of character vectors. For any data types that do not support missing values, rmmissing returns R == A and an all-false TF. Given an input matrix (2-D array) A, R = rmmissing (A, dim) further specifies whether rows or columns containing missing data are removed from the output R based on the value of dim, which must be either 1 or 0. 1: remove rows. 2: remove columns. R = rmmissing (..., Name, Value) also accepts the following paired arguments. Name Value 'MinNumMissing' A non-negative integer scalar value specifying the required minimum number of missing values for removing any particular row or column from a matrix input, or element from a vector input (default 1). With 0, everything is removed; above 1, nothing is removed from a vector, whose elements are each missing at most once. 'MissingLocations' A logical array of the same size as input A indexing the locations of missing values in input array A. Note that specifying 'MissingLocations' overrides any standard missing values in A. Optional return value TF is a logical array where true values represent removed entries, rows or columns from the original data A. It has one element per element of a vector, and one per row or column of a matrix, an empty A included. See also: fillmissing, ismissing, standardizeMissing # name: # type: sq_string # elements: 1 # length: 32 Remove missing data from arrays. # name: # type: sq_string # elements: 1 # length: 18 standardizeMissing # name: # type: sq_string # elements: 1 # length: 1911 statistics: B = standardizeMissing (A, indicator) Replace selected values by standard missing values. B = standardizeMissing (A, indicator) returns a standardized array B of the same size and data type as the input array A and with all elements specified by indicator replaced by the standard missing value corresponding the data type of A. indicator can be either a scalar or a vector. Standard missing values and their corresponding data types are: NaN - for double, single, duration, and calendarDuration arrays. NaT - for datetime arrays. - for string arrays. - for categorical arrays. {0x0 char} - for cell arrays of character vectors. For any other data type input that does not support missing values, standardizeMissing returns B = A and any indicator value is ignored. The nonstandard missing value indicator must be of the same type as the data input A or have a compatible data types according to the following rules: all numeric indicators match both double and single data types in A. indicators specified as string arrays, char vectors, and cell arrays of character vectors match categorical data type in A. a char vector matches a cell array of character vectors in A. Note: the generic standardizeMissing function from the statistics does not operate on table inputs, which is handled by the overloaded method of the table class. Use help table.standardizeMissing to find more information about the functional specialization on tables. Standardizing a category of a categorical array removes that category from the array's type, as MATLAB removes it: no element carries it once the values are missing, so leaving it in the category list would be stale metadata, and the codes of the remaining categories shift down accordingly. Only the standardized categories are removed; one that is declared but unused is left alone. See also: fillmissing, ismissing, rmmissing # name: # type: sq_string # elements: 1 # length: 51 Replace selected values by standard missing values. # name: # type: string # elements: 1 # length: 8 tiedrank # name: # type: sq_string # elements: 1 # length: 1987 statistics: [r, tieadj] = tiedrank (x) statistics: [r, tieadj] = tiedrank (x, tieflag) statistics: [r, tieadj] = tiedrank (x, tieflag, bidir) statistics: [r, tieadj] = tiedrank (x, tieflag, bidir, tol) x may be a vector or an array. An array is ranked along its first dimension, so a matrix is ranked column by column, and tieadj then carries one entry per column: its first dimension has length 1 by default, or 3 when tieflag is set, and its higher dimensions are those of x. Compute rank adjusted for ties. [r, tieadj] = tiedrank (x) computes the ranks of the values in vector x. If any values in x are tied, tiedrank computes their average rank. The return value tieadj is an adjustment for ties required by the nonparametric tests signrank and ranksum, and for the computation of Spearman's rank correlation. [r, tieadj] = tiedrank (x, 1) computes the ranks of the values in the vector x. tieadj is a vector of three adjustments for ties required in the computation of Kendall's tau. tiedrank (x, 0) is the same as tiedrank (x). [r, tieadj] = tiedrank (x, 0, 1) computes the ranks from each end, so that the smallest and largest values get rank 1, the next smallest and largest get rank 2, etc. These ranks are used in the Ansari-Bradley test. tieflag and bidir are read as an if reads a condition: each is set when it is not empty and all of its elements are nonzero, so an empty [] keeps the default, as in tiedrank (x, [], 1). [r, tieadj] = tiedrank (x, tieflag, bidir, tol) treats two values as tied when they lie within a tolerance of each other rather than only when they are exactly equal. tol is either a scalar or an array the size of x giving each element its own tolerance, and two neighbouring values are tied when the gap between them does not exceed the sum of their two tolerances. The default is 0, which is exact comparison, and an empty tol takes it. signrank uses this to rank differences that are equal to within the precision of the values they came from. # name: # type: sq_string # elements: 1 # length: 30 x may be a vector or an array. statistics-release-2.1.2/inst/Data_Manipulation/dummyvar.m000066400000000000000000000254231527311104300237170ustar00rootroot00000000000000## Copyright (C) 2025 Jayant Chauhan <0001jayant@gmail.com> ## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{D} =} dummyvar (@var{group}) ## ## Create dummy variables. ## ## @code{@var{D} = dummyvar (@var{group})} returns a matrix @var{D} containing ## the dummy variables associated with the grouping variables in @var{group}. ## Each row in @var{D} corresponds to the same observation across all variables ## in @var{group} and each column in @var{D} corresponds to a separate dummy ## variable. @var{D} is a numeric matrix containing ones and zeros, of ## @qcode{single} data type when @var{group} is a @qcode{single} array and of ## @qcode{double} data type otherwise. ## ## The grouping variable in @var{group} can be specified in one of the following ## options: ## ## @itemize ## @item a positive integer vector representing the different group levels in ## the ordered range @code{1:max (@var{group})}. ## ## @item a positive integer matrix with each column corresponding to a separate ## grouping variable and the integer values representing the group levels within ## that grouping variable in the ordered range @code{1:max (@var{group})}. A ## @code{NaN} in a column produces a row of @code{NaN} values in that column's ## own block of dummy variables in @var{D}, without affecting the columns ## produced by any other grouping variable. A column that is entirely ## @code{NaN} has no levels, so it produces no dummy variables at all and a ## warning names it. ## ## @item a @qcode{categorical} column vector, in which case the number and order ## of columns in @var{D} correspond to the categories returned by ## @code{categories (@var{group})}. Categories that are defined but not present ## in @var{group} produce columns of zeros. Elements of @var{group} that are ## @code{} result in rows of @code{NaN} values in @var{D}. ## ## @item a @qcode{cell} array with its elements containing grouping variables ## specified as any of the above options. Note that all grouping variables in ## the cell array must have the same number of observations. ## @end itemize ## ## @seealso{tabulate, grp2idx, grpstats} ## @end deftypefn function D = dummyvar (g) if (nargin < 1) print_usage; endif [nr, nc] = size (g); ## --- CATEGORICAL branch --- if (isa (g, 'categorical')) if (! isvector (g) || nc != 1) error (strcat ("dummyvar: categorical grouping", ... " variable must be a column vector.")); endif g_cats = cellstr (categories (g)); g_cstr = cellstr (g(:)); K = numel (g_cats); D = zeros (nr, K); for i = 1:nr if (isundefined (g(i))) D(i,:) = NaN; else for k = 1:K if (strcmp (g_cstr{i}, g_cats{k})) D(i,k) = 1; break; endif endfor endif endfor ## --- NUMERIC branch --- elseif (isnumeric (g)) if (ndims (g) != 2) error (strcat ("dummyvar: numeric grouping variable", ... " must be either a vector or a matrix.")); endif gv = g(:); gv = gv(! isnan (gv)); ## Inf is positive and is its own FIX, so it passes both tests below and ## would reach ZEROS as a column count; it is refused on its own account if (any (gv <= 0) || any (gv != fix (gv)) || any (isinf (gv))) error (strcat ("dummyvar: values of a numeric grouping variable", ... " must be positive integers or NaN.")); endif ## Force vector to column vector if (isvector (g) && nc > 1) g = g(:); nr = nc; nc = 1; endif K = max (g, [], 1, "omitnan"); ## MAX over a grouping variable of no observations answers empty rather ## than with one level count per variable, and SUM of that is a row where ## ZEROS wants a scalar. No observations means no levels either way. if (isempty (K)) K = zeros (1, nc); endif ## A grouping variable that is nothing but NaN has no levels, so it earns ## no columns and the ones the other variables earn are unaffected. MAX ## returns NaN for such a column, which SUM would carry into ZEROS. allnan = find (isnan (K)); if (! isempty (allnan)) cols = sprintf ("%d, ", allnan); warning (strcat ("dummyvar: the following columns of GROUP contain", ... " all NaNs and produce no dummy variables: %s."), ... cols(1:end-2)); K(allnan) = 0; endif D = zeros (nr, sum (K)); ij = 0; for i = 1:nc tmp = g(:,i); for j = 1:K(i) ij++; D(tmp == j, ij) = 1; endfor D(isnan (tmp), ij - K(i) + 1:ij) = NaN; endfor if (isa (g, 'single')) D = single (D); endif ## --- CELLSTRING branch --- elseif (iscellstr (g) && isvector (g)) if (! isvector (g) || nc != 1) error (strcat ("dummyvar: cellstring grouping", ... " variable must be a column vector.")); endif g = grp2idx (g); K = max (g); D = zeros (length (g), K); for i = 1:K D(g == i, i) = 1; endfor ## --- CELL ARRAY branch --- elseif (iscell (g) && isvector (g)) if (any (diff (cellfun (@(x) size (x, 1), g)))) error (strcat ("dummyvar: all grouping variables in cell array", ... " must have the same number of observations.")); endif D = []; for i = 1:numel (g) g_var = g{i}; D_var = double (dummyvar (g_var)); D = [D, D_var]; endfor else error ("dummyvar: unsupported type of grouping variable."); endif endfunction ## Test output %!assert_equal (dummyvar ([]), []) %!assert_equal (dummyvar (ones (2, 0)), ones (2, 0)) %!assert_equal (dummyvar (zeros (0, 3)), zeros (0, 0)) %!assert_equal (dummyvar ({[], []}), []) %!assert_equal (dummyvar ([1; NaN; 2]), [1, 0; NaN, NaN; 0, 1]) %!assert_equal (dummyvar (single ([1; NaN; 2])), ... %! single ([1, 0; NaN, NaN; 0, 1])) %!assert_equal (dummyvar ({single([1; 2]), single([2; 1])}), ... %! [1, 0, 0, 1; 0, 1, 1, 0]) %!test %! ## numeric grouping vector %! g = [1; 2; 1; 3; 2]; %! D = dummyvar (g); %! assert_equal (D, [1, 0, 0; 0, 1, 0; 1, 0, 0; 0, 0, 1; 0, 1, 0]); %!test %! g = categorical ({'a'; 'b'; 'a'}, {'a', 'b', 'c'}); %! D = dummyvar (g); %! cats = categories (g); %! g_str = cellstr (g); %! for k = 1:numel (cats) %! mask = strcmp (g_str, cats{k}); %! assert_equal (all (D(mask, k) == 1), true); %! assert_equal (all (D(! mask, k) == 0), true); %! endfor %!test %! g = categorical ({'a'; ''; 'b'}, {'a', 'b', 'c'}); %! D = dummyvar (g); %! assert_equal (D, [1, 0, 0; NaN, NaN, NaN; 0, 1, 0]); %!test %! colors = categorical ({'Red'; 'Blue'; 'Green'; 'Red'; 'Green'; 'Blue'}); %! D = dummyvar (colors); %! assert_equal (D, [0, 0, 1; 1, 0, 0; 0, 1, 0; 0, 0, 1; 0, 1, 0; 1, 0, 0]); %!test %! g1 = [1; 1; 1; 1; 2; 2; 2; 2]; %! g2 = [1; 2; 3; 1; 2; 3; 1; 2]; %! D = dummyvar ([g1, g2]); %! D1 = [1, 0, 1, 0, 0; 1, 0, 0, 1, 0; 1, 0, 0, 0, 1; 1, 0, 1, 0, 0; ... %! 0, 1, 0, 1, 0; 0, 1, 0, 0, 1; 0, 1, 1, 0, 0; 0, 1, 0, 1, 0]; %! assert_equal (D, D1); %!test %! phone = {'mob'; 'land'; 'mob';'mob';'mob';'land';'land'}; %! codes = categorical ([202; 202; 103; 103; 202; 103; 202]); %! D = dummyvar ({phone, codes}); %! D1 = [1, 0, 0, 1; 0, 1, 0, 1; 1, 0, 1, 0; 1, 0, 1, 0; ... %! 1, 0, 0, 1; 0, 1, 1, 0; 0, 1, 0, 1]; %! assert_equal (D, D1); %!test %! colors = {'red'; 'blue'; 'red'; 'green'; 'yellow'; 'blue'}; %! D = dummyvar (categorical (colors)); %! D1 = [0, 0, 1, 0; 1, 0, 0, 0; 0, 0, 1, 0; 0, 1, 0, 0; 0, 0, 0, 1; 1, 0, 0, 0]; %! assert_equal (D, D1); %!test %! colors = {'red'; 'blue'; 'red'; 'green'; 'yellow'; 'blue'}; %! D = dummyvar (colors); %! D1 = [1, 0, 0, 0; 0, 1, 0, 0; 1, 0, 0, 0; 0, 0, 1, 0; 0, 0, 0, 1; 0, 1, 0, 0]; %! assert_equal (D, D1); %! D = dummyvar ({colors}); %! D1 = [1, 0, 0, 0; 0, 1, 0, 0; 1, 0, 0, 0; 0, 0, 1, 0; 0, 0, 0, 1; 0, 1, 0, 0]; %! assert_equal (D, D1); %!test %! g = [1, 2, 1, 2, 1, 3, 2, 1]; %! D = dummyvar (g); %! D1 = [1, 0, 0; 0, 1, 0; 1, 0, 0; 0, 1, 0; 1, 0, 0; 0, 0, 1; 0, 1, 0; 1, 0, 0]; %! assert_equal (D, D1); ## A NaN in a numeric grouping variable produces a NaN row in that ## variable's own block of dummy columns. %!test %! g = [1; 2; NaN; 3; 2]; %! D = dummyvar (g); %! assert_equal (D, [1, 0, 0; 0, 1, 0; NaN, NaN, NaN; 0, 0, 1; 0, 1, 0]); %!test %! g = [1, 1; 2, NaN; 1, 2; 2, 1]; %! D = dummyvar (g); %! assert_equal (D, [1, 0, 1, 0; 0, 1, NaN, NaN; 1, 0, 0, 1; 0, 1, 1, 0]); ## A grouping variable that is entirely NaN has no levels, so it produces no ## dummy variables and the columns of the others are unaffected. %!test %! D = dummyvar ([1, NaN; 2, NaN]); %! assert_equal (D, [1, 0; 0, 1]); %!test %! D = dummyvar ([1, NaN, 2; 2, NaN, 1]); %! assert_equal (D, [1, 0, 0, 1; 0, 1, 1, 0]); %!test %! D = dummyvar ([NaN; NaN]); %! assert_equal (size (D), [2, 0]); %!warning ... %! dummyvar ([1, NaN; 2, NaN]); %!warning ... %! dummyvar ([NaN, NaN, NaN; NaN, NaN, NaN]); ## Test input validation %!error dummyvar () %!error dummyvar (1, 2) %!error ... %! dummyvar (categorical ({'a', 'b'})) %!error ... %! dummyvar (ones (3, 3, 3)) %!error ... %! dummyvar ([2, 4, 0, 8, 1]) %!error ... %! dummyvar ([1; 1.5; 2]) %!error ... %! dummyvar ([1; Inf; 2]) %!error ... %! dummyvar ([1; -Inf; 2]) %!error ... %! dummyvar ({'a', 'b'}) %!error ... %! dummyvar ({[2;3;4;5], [1;2;3]}) %!error dummyvar ([true; false]) statistics-release-2.1.2/inst/Data_Manipulation/fillmissing.m000066400000000000000000003554401527311104300244000ustar00rootroot00000000000000## Copyright (C) 1995-2026 The Octave Project Developers ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{B} =} fillmissing (@var{A}, "constant", @var{v}) ## @deftypefnx {statistics} {@var{B} =} fillmissing (@var{A}, @var{method}) ## @deftypefnx {statistics} {@var{B} =} fillmissing (@var{A}, @var{move_method}, @var{window_size}) ## @deftypefnx {statistics} {@var{B} =} fillmissing (@var{A}, @var{fill_function}, @var{window_size}) ## @deftypefnx {statistics} {@var{B} =} fillmissing (@dots{}, @var{dim}) ## @deftypefnx {statistics} {@var{B} =} fillmissing (@dots{}, @var{PropertyName}, @var{PropertyValue}) ## @deftypefnx {statistics} {[@var{B}, @var{idx}] =} fillmissing (@dots{}) ## ## Fill missing data in arrays. ## ## Replace missing entries of array @var{A} either with values in @var{v} or ## as determined by other specified methods. 'missing' values are determined ## by the data type of @var{A} as identified by the function @ref{ismissing}, ## currently defined as: ## ## Standard missing values and their corresponding data types are: ## ## @itemize ## @item @qcode{NaN} - for @qcode{double}, @qcode{single}, @qcode{duration}, and ## @qcode{calendarDuration} arrays. ## @item @qcode{NaT} - for @qcode{datetime} arrays. ## @item @qcode{} - for @qcode{string} arrays. ## @item @qcode{} - for @qcode{categorical} arrays. ## @item @qcode{@{0x0 char@}} - for @qcode{cell} arrays of character vectors. ## @end itemize ## ## For any data types that do not support missing values, @code{ismissing} ## returns @code{@var{TF} = false (size (@var{A}))}. ## ## @var{A} can be a numeric scalar or array, a character vector or array, or ## a cell array of character vectors (a.k.a. string cells). ## ## @var{v} can be a scalar or an array containing values for replacing the ## missing values in @var{A} with a compatible data type for insertion into ## @var{A}. The shape of @var{v} must be a scalar or an array with number ## of elements in @var{v} equal to the number of elements orthogonal to the ## operating dimension. E.g., if @code{size(@var{A})} = [3 5 4], operating ## along @code{dim} = 2 requires @var{v} to contain either 1 or 3x4=12 ## elements. ## ## If requested, the optional output @var{idx} will contain a logical array ## the same shape as @var{A} indicating with 1's which locations in @var{A} ## were filled. ## ## Alternate Input Arguments and Values: ## @itemize ## @item @var{method} - replace missing values with: ## @table @code ## ## @item next ## @itemx previous ## @itemx nearest ## next, previous, or nearest non-missing value (nearest defaults to next ## when equidistant as determined by @code{SamplePoints}.) ## ## @item linear ## linear interpolation of neighboring, non-missing values ## ## @item spline ## piecewise cubic spline interpolation of neighboring, non-missing values ## ## @item pchip ## 'shape preserving' piecewise cubic spline interpolation of neighboring, ## non-missing values ## @end table ## ## @item @var{move_method} - moving window calculated replacement values: ## @table @code ## ## @item movmean ## @itemx movmedian ## moving average or median using a window determined by @var{window_size}. ## @var{window_size} must be either a positive scalar value or a two element ## positive vector of sizes @w{@code{[@var{nb}, @var{na}]}} measured in the ## same units as @code{SamplePoints}. For scalar values, the window is ## centered on the missing element and includes all data points within a ## distance of half of @var{window_size} on either side of the window center ## point. Note that for compatibility, when using a scalar value, the backward ## window limit is inclusive and the forward limit is exclusive. If a ## two-element @var{window_size} vector is specified, the window includes all ## points within a distance of @var{nb} backward and @var{na} forward from the ## current element at the window center (both limits inclusive). ## @end table ## ## @item @var{fill_function} - custom method specified as a function handle. ## The supplied fill function must accept three inputs in the following order ## for each missing gap in the data: ## @table @var ## @item A_values - ## elements of @var{A} within the window on either side of the gap as ## determined by @var{window_size}. (Note these elements can include missing ## values from other nearby gaps.) ## @item A_locs - ## locations of the reference data, @var{A_values}, in terms of the default ## or specified @code{SamplePoints}. ## @item gap_locs - ## location of the gap data points that need to be filled in terms of the ## default or specified @code{SamplePoints}. ## @end table ## ## The supplied function must return a scalar or vector with the same number of ## elements in @var{gap_locs}. The required @var{window_size} parameter ## follows similar rules as for the moving average and median methods ## described above, with the two exceptions that (1) each gap is processed as a ## single element, rather than gap elements being processed individually, and ## (2) the window extended on either side of the gap has inclusive endpoints ## regardless of how @var{window_size} is specified. ## ## @item @var{dim} - specify a dimension for vector operation (default = ## first non-singeton dimension) ## ## A one-sided @var{window_size}, @qcode{[@var{nb}, 0]} or ## @qcode{[0, @var{na}]}, leaves the gap at the corresponding end of the data ## with no values in its window. The fill function is still called there, with ## empty value and location arguments, as MATLAB calls it: a fill function may ## depend only on the gap's own sample points. One that cannot take empty ## arguments is reported as such. ## ## Along a dimension in which @var{A} is singleton, every element is a vector ## of length one, so a missing value has no neighbour to be filled from and is ## returned unchanged. MATLAB fills it regardless, which is a defect there and ## not a convention this follows: it returns @code{[1, 1, 3, 1, 5]} for ## @code{fillmissing ([1, NaN, 3, NaN, 5], @@testfcn, 99, 3)}, inventing values ## from a window that contains nothing. Measured against R2024a, two releases ## after the behaviour was first recorded here. ## ## @item @var{PropertyName}-@var{PropertyValue} pairs ## @table @code ## @item SamplePoints ## @var{PropertyValue} is a vector of sample point values representing the ## sorted and unique x-axis values of the data in @var{A}. If unspecified, ## the default is assumed to be the vector @var{[1 : size (A, dim)]}. The ## values in @code{SamplePoints} will affect methods and properties that rely ## on the effective distance between data points in @var{A}, such as ## interpolants and moving window functions where the @var{window_size} ## specified for moving window functions is measured relative to the ## @code{SamplePoints}. ## ## @item EndValues ## Apply a separate handling method for missing values at the front or back of ## the array. @var{PropertyValue} can be: ## @itemize ## @item A constant scalar or array with the same shape requirements as @var{v}. ## @item @code{none} - Do not fill end gap values. ## @item @code{extrap} - Use the same procedure as @var{method} to fill the ## end gap values. ## @item Any valid @var{method} listed above except for @code{movmean}, ## @code{movmedian}, and @code{fill_function}. Those methods can only be ## applied to end gap values with @code{extrap}. ## @end itemize ## ## @item MissingLocations ## @var{PropertyValue} must be a logical array the same size as @var{A} ## indicating locations of known missing data with a value of @code{true}. ## (cannot be combined with MaxGap) ## ## @item MaxGap ## @var{PropertyValue} is a numeric scalar indicating the maximum gap length ## to fill, and assumes the same distance scale as the sample points. Gap ## length is calculated by the difference in locations of the sample points ## on either side of the gap, and gaps larger than MaxGap are ignored by ## @var{fillmissing}. (cannot be combined with MissingLocations) ## @end table ## @end itemize ## ## Compatibility Notes: ## @itemize ## @item ## Numerical and logical inputs for @var{A} and @var{v} may be specified ## in any combination. The output will be the same class as @var{A}, with the ## @var{v} converted to that data type for filling. Only @code{single} and ## @code{double} have defined 'missing' values, so except for when the ## @code{missinglocations} option specifies the missing value identification of ## logical and other numeric data types, the output will always be ## @code{@var{B} = @var{A}} with @code{@var{idx} = false(size(@var{A}))}. ## @item ## All interpolation methods can be individually applied to @code{EndValues}. ## @item ## @sc{Matlab}'s @var{fill_function} method currently has several ## inconsistencies with the other methods (tested against version 2022a), and ## Octave's implementation has chosen the following consistent behavior over ## compatibility: (1) a column full of missing data is considered part of ## @code{EndValues}, (2) such columns are then excluded from ## @var{fill_function} processing because the moving window is always empty. ## (3) operation in dimensions higher than 2 perform identically to operations ## in dims 1 and 2, most notable on vectors. ## @end itemize ## ## @seealso{ismissing, rmmissing, standardizeMissing} ## @end deftypefn function [A, idx_out] = fillmissing (A, varargin) if (nargin < 2 || nargin > 12) print_usage (); endif method = varargin{1}; if (ischar (method)) method = lower (method); elseif (! is_function_handle (method)) error ("fillmissing: second input must be a string or function handle."); endif sz_A = size (A); ndims_A = numel (sz_A); dim = []; missing_locs = []; endgap_method = []; endgap_locs = []; endgap_val = []; fill_vals = []; idx_flag = (nargout > 1); maxgap = []; missinglocations = false; reshape_flag = false; samplepoints = []; standard_samplepoints = true; v = []; if (idx_flag) idx_out = false (sz_A); endif ## Process input arguments. if (is_function_handle (method)) ## Verify function handle and window. if ((nargin < 3) || ! isnumeric (varargin{2}) || ... ! any (numel (varargin{2}) == [1, 2])) error (strcat ("fillmissing: fill function handle must be followed by", ... " a numeric scalar or two-element vector window size.")); elseif (nargin (method) < 3) error ("fillmissing: fill function must accept at least three inputs."); endif move_fcn = method; method = 'movfcn'; window_size = varargin{2}; next_varg = 3; else switch (method) case {'previous', 'next', 'nearest'} next_varg = 2; case {'linear', 'spline', 'pchip', 'makima'} next_varg = 2; if (! (isnumeric (A) || islogical (A))) error (strcat ("fillmissing: interpolation methods only", ... " valid for numeric input types.")); endif case 'constant' if ((nargin < 3)) error (strcat ("fillmissing: 'constant' method must be", ... " followed by a numeric scalar or array.")); endif v = varargin{2}; if (! (isscalar (v) || isempty (v))) v = v(:); endif if ((! ischar (v)) && isempty (v)) error ("fillmissing: a numeric fill value cannot be empty."); endif ## Type check v against A. if (iscellstr (A) && ischar (v) && ! iscellstr (v)) v = {v}; endif if ((! isempty (v)) && ... ((isnumeric (A) && ! (isnumeric (v) || islogical (v))) || ... (ischar (A) && ! ischar (v)) || ... (iscellstr (A) && ! (iscellstr (v))))) error ("fillmissing: fill value must be the same data type as 'A'."); endif ## v can't be size checked until after processing rest of inputs. next_varg = 3; case {'movmean', 'movmedian'} if (! (isnumeric (A) || islogical (A))) error (strcat ("fillmissing: 'movmean' and 'movmedian' methods", ... " only valid for numeric input types.")); endif if ((nargin < 3) || ! isnumeric (varargin{2}) || ... ! any (numel (varargin{2}) == [1, 2])) error (strcat ("fillmissing: moving window method must be", ... " followed by a numeric scalar or two-element vector.")); endif window_size = varargin{2}; next_varg = 3; otherwise error ("fillmissing: unknown fill method '%s'.", method); endswitch endif ## Process any more parameters. if (next_varg < nargin) ## Set dim. If specified, it is the only numeric option allowed next. if (isnumeric (varargin{next_varg})) dim = varargin{next_varg}; if (! (isscalar (dim) && (dim > 0))) error ("fillmissing: DIM must be a positive scalar."); endif next_varg++; else ## Default dim is first nonsingleton dimension of A. if (isscalar (A)) dim = 1; else dim = find (sz_A > 1, 1, 'first'); endif endif sz_A_dim = size (A, dim); ## Process any remaining inputs, must be name-value pairs. while (next_varg < nargin) propname = varargin{next_varg}; if (next_varg + 1 == nargin) ## Must be at least one more input with 1st containing value. error ("fillmissing: properties must be given as name-value pairs."); else propval = varargin{next_varg + 1}; next_varg = next_varg + 2; if (! ischar (propname)) error ("fillmissing: invalid parameter name specified."); else propname = lower (propname); endif ## Input validation for names and values. switch (propname) case 'samplepoints' ## val must be sorted, unique, numeric vector the same size ## as size(A,dim). if (! (isnumeric (propval) && isvector (propval) && (numel (propval) == sz_A_dim) && issorted (propval) && (numel (propval) == numel (unique (propval))))) error (strcat ("fillmissing: SamplePoints must be a sorted ", ... "non-repeating, numeric vector with %d", ... " elements."), sz_A_dim); endif samplepoints = propval(:); standard_samplepoints = all (diff (samplepoints, 1, 1) == 1); case 'endvalues' ## For numeric A, val must be numeric scalar, a numeric ## array with numel equal to the elements orthogonal to ## the dim or certain string methods. For non-numeric A, ## "constant" method is not valid. if (ischar (propval)) switch (lower (propval)) case {'extrap', 'previous', 'next', 'nearest', 'none', ... 'linear', 'spline', 'pchip', 'makima'} endgap_method = propval; otherwise error ("fillmissing: invalid EndValues method '%s'.", propval); endswitch elseif (isnumeric (propval)) if (! (isnumeric (A) || islogical (A))) error (strcat ("fillmissing: EndValues method 'constant'", ... " only valid for numeric arrays.")); endif endgap_method = 'constant'; endgap_val = propval; else error (strcat ("fillmissing: EndValues must be numeric", ... " or a valid method name.")); endif case 'missinglocations' if (! (isnumeric (A) || islogical (A) || isinteger (A) || ... ischar (A) || iscellstr (A))) error (strcat ("fillmissing: MissingLocations option is not", ... " compatible with data type '%s'."), class (A)); endif if (! isempty (maxgap)) error (strcat ("fillmissing: MissingLocations and MaxGap", ... " options cannot be used simultaneously.")); endif ## val must be logical array same size as A. if (! (islogical (propval) && isequal (sz_A, size (propval)))) error (strcat ("fillmissing: MissingLocations must be", ... " a logical array the same size as A.")); endif missinglocations = true; missing_locs = propval; case 'maxgap' ## val must be positive numeric scalar. if (! (isnumeric (propval) && isscalar (propval) && (propval > 0))) error ("fillmissing: MaxGap must be a positive numeric scalar."); endif if (! isempty (missing_locs)) error (strcat ("fillmissing: MissingLocations and MaxGap", ... " options cannot be used simultaneously.")); endif maxgap = propval; case {'replacevalues', 'datavariables'} error ("fillmissing: the '%s' option has not been implemented.", ... propname); otherwise error ("invalid parameter name '%s'.", propname); endswitch endif endwhile else ## No inputs after method. ## Set default dim. if (isscalar (A)) dim = 1; else dim = find (sz_A > 1, 1, 'first'); endif sz_A_dim = size (A, dim); endif ## Reduce calls to size and avoid overruns checking sz_A for high dims. if (dim > ndims_A) sz_A = [sz_A, ones(1, dim - ndims_A)]; ndims_A = numel (sz_A); endif ## Set defaults for any unspecified parameters. if (isempty (samplepoints)) samplepoints = [1 : sz_A_dim]'; endif if (isempty (missing_locs)) missing_locs = ismissing (A); endif ## endvalues treated separately from interior missing_locs. if (isempty (endgap_method) || strcmp (endgap_method, 'extrap')) endgap_method = method; if (strcmp (endgap_method, 'constant')) endgap_val = v; endif endif ## missingvalues option not compatible with some methods and inputs: if (isinteger (A) || islogical (A)) if (any (ismember (method, ... {'linear', 'spline', 'pchip', 'makima', 'movmean', 'movmedian'}))) error (strcat ("fillmissing: MissingLocations cannot be used", ... " with method '%s' and inputs of type '%s'."), ... method, class (A)); elseif (any (ismember (endgap_method, ... {'linear', 'spline', 'pchip', 'makima'}))) error (strcat ("fillmissing: MissingLocations cannot be used with", ... " EndValues method '%s' and inputs of type '%s'."), ... method, class (A)); endif endif ## Verify size of v and endgap_val for 'constant' methods, resize for A. orthogonal_size = [sz_A(1:dim-1), 1, sz_A(dim+1:end)]; # orthog. to dim size. numel_orthogonal = prod (orthogonal_size); # numel perpen. to dim. if (strcmp (method, 'constant') && (! isscalar (v))) if (numel (v) != numel_orthogonal) error (strcat ("fillmissing: fill value 'V' must be a scalar or", ... " a %d element array."), numel_orthogonal); else v = reshape (v, orthogonal_size); endif endif if (strcmp (endgap_method, 'constant') && (! isscalar (endgap_val))) if (numel (endgap_val) != numel_orthogonal) error (strcat ("fillmissing: EndValues must be a scalar or a %d", ... " element array."), numel_orthogonal); else endgap_val = reshape (endgap_val, orthogonal_size); endif endif ## Simplify processing by temporarily permuting A so operation always on dim1. ## Revert permutation at the end. dim_idx_perm = [1 : ndims_A]; dim_idx_flip(1 : max (dim, ndims_A)) = {':'}; dim_idx_flip(1) = [sz_A_dim:-1:1]; if (dim != 1) dim_idx_perm([1, dim]) = [dim, 1]; A = permute (A, dim_idx_perm); sz_A([1, dim]) = sz_A([dim, 1]); missing_locs = permute (missing_locs, dim_idx_perm); reshape_flag = true; orthogonal_size = [1, sz_A(2:end)]; if (idx_flag) idx_out = false (sz_A); endif if (! isempty (v) && ! isscalar (v)) v = permute (v, dim_idx_perm); endif if (! isempty (endgap_val) && ! isscalar (endgap_val)) endgap_val = permute (endgap_val, dim_idx_perm); endif endif ## Precalculate fill data for several methods. zero_padding = zeros (orthogonal_size); samplepoints_expand = samplepoints(:, ones (1, prod (sz_A(2:end)))); ## Find endgap locations. if (sz_A_dim < 3) ## All missing are endgaps. endgap_locs = missing_locs; else ## Use cumsums starting from first and last part in dim to find missing ## values in and adjacent to end locations. endgap_locs = cumprod (missing_locs,1) | ... (cumprod (missing_locs(dim_idx_flip{:}),1))(dim_idx_flip{:}); endif ## Remove endgap_locs from missing_locs to avoid double processing. missing_locs(endgap_locs) = false; ## Remove elements from missing and end location arrays if maxgap is specified. if (! isempty (maxgap)) ## missing_locs: If samplepoints value diff on either side of missing ## elements is > maxgap, remove those values. ## For endgaps, use diff of inside and missing end samplepoint values ## and remove from endgaps. ## First check gapsize of any interior missings in missing_locs. if (any (missing_locs(:))) ## Locations in front of gaps loc_before = [diff(missing_locs,1,1); zero_padding] == 1; ## Locations in back of gaps loc_after = diff ([zero_padding; missing_locs],1,1) == -1; ## Value of samplepoints at front and back locs sampvals_before = samplepoints_expand(loc_before); sampvals_after = samplepoints_expand(loc_after); ## Evaluate which gaps are too big to fill. gaps_to_remove = (sampvals_after - sampvals_before) > maxgap; ## Convert those gaps into an array element list. idxs_to_remove = arrayfun ('colon', ... ((find (loc_before))(gaps_to_remove ) + 1), ... ((find (loc_after))(gaps_to_remove ) - 1), ... 'UniformOutput', false); ## Remove those elements from missing_locs. missing_locs([idxs_to_remove{:}]) = false; endif ## Then do any endgaps. if (any (endgap_locs(:))) ## If any are all missing, remove for any value of maxgap. endgap_locs &= ! prod (endgap_locs, 1); if ((sz_A_dim < 3) && (abs (samplepoints(2) - samplepoints(1)) > maxgap)) ## Shortcut - all missings are ends and exceed maxgap. endgap_locs(:) = false; else ## Check gap size of front endgaps. ## Find loc element after gap. nextvals = sum (cumprod (endgap_locs,1)) + 1; ## Compare diff between values at those points and at base with maxgap. ends_to_remove = abs (samplepoints(nextvals) - samplepoints(1)) ... > maxgap; ## Remove any with gap>maxgap. endgap_locs((cumprod (endgap_locs,1)) & ends_to_remove) = false; ## Flip, repeat for back endgaps, then unflip and remove. nextvals = sum (cumprod (endgap_locs(dim_idx_flip{:}),1)) + 1; ends_to_remove = abs (samplepoints(end:-1:1)(nextvals) ... - samplepoints(end)) > maxgap; endgap_locs((cumprod (... endgap_locs(dim_idx_flip{:}), 1)(dim_idx_flip{:})) & ... ends_to_remove) = false; endif endif endif if (any (strcmp (endgap_method, {'movmean', 'movmedian', 'movfcn'}))) ## These methods only called for endgaps with "extrap", so endgaps ## are processed together in the missing_locs section. missing_locs = missing_locs | endgap_locs; endgap_locs(:) = false; endif ## Actually fill the missing data. ## Process central missing values (all gaps bound by two valid datapoints). ## For each method, calculate fill_vals, which will be used in assignment ## A(missing_locs) = fill_vals, and if idx_flag, populate idx_out. if (any (missing_locs(:))) switch (method) case 'constant' if (isscalar (v)) fill_vals = v; else fill_vals = (missing_locs .* v)(missing_locs); endif if (idx_flag) ## If any v are the missing type, those get removed from idx_out ## unless using 'missinglocations'. if ((! missinglocations) && any (miss_v = ismissing (v))) idx_out(missing_locs) = true; idx_out(missing_locs & miss_v) = false; else idx_out(missing_locs) = true; endif endif case {'previous', 'next', 'nearest', 'linear'} ## Find element locations bounding each gap. loc_before = [diff(missing_locs, 1, 1); zero_padding] == 1; loc_after = diff ([zero_padding; missing_locs], 1, 1) == -1; gapsizes = find (loc_after) - find (loc_before) - 1; gap_count_idx = [1 : numel(gapsizes); gapsizes']; switch (method) case 'previous' fill_vals = repelems (A(loc_before), gap_count_idx)'; case 'next' fill_vals = repelems (A(loc_after), gap_count_idx)'; case {'nearest', 'linear'} ## Determine which missings go with values before or after ## gap based on samplevalue distance. (Equal dist goes to after.) ## Find sample values before and after gaps. sampvals_before = samplepoints_expand(loc_before); sampvals_after = samplepoints_expand(loc_after); ## Build cell with linear indices of elements in each gap. gap_locations = arrayfun ('colon', (find (loc_before)) + 1, ... (find (loc_after)) - 1, 'UniformOutput', false); ## Get sample values at those elements. [sampvals_in_gaps, ~] = ind2sub (sz_A, [gap_locations{:}]); sampvals_in_gaps = samplepoints(sampvals_in_gaps); ## Expand first and last vectors for each gap point. Avals_before = repelems (A(loc_before), gap_count_idx)'; Avals_after = repelems (A(loc_after), gap_count_idx)'; switch (method) case 'nearest' ## Calculate gap mid point for each gap element. sampvals_midgap = repelems ( ... (sampvals_before + sampvals_after)/2, gap_count_idx)'; ## Generate fill vectors sorting elements into nearest before ## or after. prev_fill = (sampvals_in_gaps < sampvals_midgap); next_fill = (sampvals_in_gaps >= sampvals_midgap); fill_vals = A(missing_locs); fill_vals(prev_fill) = Avals_before(prev_fill); fill_vals(next_fill) = Avals_after(next_fill); case 'linear' ## Expand samplepoint values for interpolation x-values. sampvals_before = repelems (sampvals_before, gap_count_idx)'; sampvals_after = repelems (sampvals_after, gap_count_idx)'; ## Linearly interpolate: fill_vals = ((Avals_after - Avals_before) ... ./ (sampvals_after - sampvals_before)) ... .* (sampvals_in_gaps - sampvals_before) ... + Avals_before; endswitch endswitch if (idx_flag) ## Mid gaps will always be filled by above methods. idx_out(missing_locs) = true; endif case {'spline', 'pchip', 'makima'} ## Pass more complex interpolations to interp1. ## FIXME: vectorized 'linear' is ~10-100x faster than using interp1. ## Look to speed these up as well. ## Identify columns needing interpolation to reduce empty operations. cols_to_use = any (missing_locs, 1); ## missinglocations may send columns with NaN and less than 2 ## real values resulting in interp1 error. Trim those columns, ## pre-populate fill_vals with NaN, mark as filled. if (missinglocations) fill_vals = NaN (sum (missing_locs(:, cols_to_use)(:)), 1); cols_enough_points = (sum ( ... ! isnan (A) & (! missing_locs), 1) > 1) & cols_to_use; interp_cols = (cols_enough_points & cols_to_use); interp_vals = (missing_locs & cols_enough_points)(missing_locs & ... cols_to_use); fill_vals(interp_vals) = other_interpolants (A(:, interp_cols), missing_locs(:, interp_cols), endgap_locs(:, interp_cols), ... method, samplepoints); else fill_vals = other_interpolants (A(:, cols_to_use), missing_locs(:, cols_to_use), endgap_locs(:, cols_to_use), ... method, samplepoints); endif if (idx_flag) idx_out(missing_locs) = true; endif case {'movmean', 'movmedian'} ## Check window size versus smallest sample gaps. If window smaller, ## nothing to do, break out early. if ((isscalar (window_size) && ... (window_size/2 >= min (diff (samplepoints)))) || ... (isvector (window_size) && (sum (window_size) >= min (diff (samplepoints))))) switch (method) case 'movmean' if (sz_A_dim > 1) allmissing = (missing_locs | endgap_locs)(:,:); ## Create temporary flattened array for processing, A_sum = A(:,:); A_sum(allmissing) = 0; if (standard_samplepoints && ... all (round (window_size) == window_size)) ## Window size based on vector elements. ## Faster codepath for uniform, unit-spacing samplepoints ## and integer valued window sizes. if (isscalar (window_size)) window_width = window_size; if (mod (window_size, 2)) ## Odd window size: ## Equal number of values on either side of gap. window_size = (window_width - 1) .* [0.5, 0.5]; else ## Even window size: ## One extra element on previous side of gap. window_size(1) = window_width/2; window_size(2) = window_size(1) - 1; endif else window_width = window_size(1) + window_size(2) + 1; endif ## Use columnwise convolution of windowing vector and A for ## vectorized summation. conv_vector = ones (window_width, 1); A_sum = convn (A_sum, conv_vector, ... 'full')(1 + window_size(2):end - window_size(1), :); ## Get count of values contributing to convolution to account ## for missing elements and to calculate mean. A_sum_count = convn (! allmissing, conv_vector, ... 'full')(1 + window_size(2):end - window_size(1), :); else ## Window size based on sample point distance. Works for non ## integer, non uniform values. ## Use A_sum (flattened to 2D), project slice windows in dim3 ## automatic broadcasting to get window summations & counts samplepoints_shift = ... samplepoints(:, ones (1, sz_A_dim)) - samplepoints'; if (isscalar (window_size)) ## [nb, na) window_size = window_size * [-0.5, 0.5]; samplepoints_slice_windows = permute (... samplepoints_shift >= window_size(1) & ... samplepoints_shift < window_size(2), [1,3,2]); else ## [nb, na] window_size(1) = -window_size(1); samplepoints_slice_windows = permute (... samplepoints_shift >= window_size(1) & ... samplepoints_shift <= window_size(2), [1,3,2]); endif if (missinglocations) ## NaNs left in A_sum will cause all sums to produce NaN ## FIXME: when sum can handle nanflag, the 'else' path ## should be able to be made to handle the vectorized ## summation even with 'missinglocations'. A_nan = isnan (A_sum); A_temp = A_sum .* samplepoints_slice_windows; A_temp(! samplepoints_slice_windows & A_nan) = 0; A_sum = permute (sum (A_temp, 1), [3,2,1]); else A_sum = permute (... sum (A_sum .* samplepoints_slice_windows, 1), [3,2,1]); endif A_sum_count = permute (... sum (! allmissing & samplepoints_slice_windows, 1), ... [3,2,1]); endif ## Build fill values. fill_vals = A(missing_locs); # Prefill to include missing vals. fillable_gaps = missing_locs(:,:) & A_sum_count; fill_vals(fillable_gaps(missing_locs(:,:))) = ... A_sum(fillable_gaps) ./ A_sum_count(fillable_gaps); endif case 'movmedian' if (sz_A_dim > 1) if (missinglocations) ## Median assumes empty locs have NaN. Missinglocations ## may point to a non-NaN number that will be assumed valid. ## Replace with NaNs. A(missing_locs) = NaN; endif cols_to_use = any (missing_locs(:,:), 1); samplepoints_shift = ... samplepoints(:, ones (1, sz_A_dim)) - samplepoints'; if (isscalar (window_size)) window_size = window_size * [-0.5, 0.5]; ## [nb, na) samplepoints_slice_windows = permute (... samplepoints_shift >= window_size(1) & ... samplepoints_shift < window_size(2), [1,3,2]); else window_size(1) = -window_size(1); ## [nb, na] samplepoints_slice_windows = permute (... samplepoints_shift >= window_size(1) & ... samplepoints_shift <= window_size(2), [1,3,2]); endif ## Use moving window slices to project A and use ## custom function for vectorized full array median computation. A_med = A(:, cols_to_use); nan_slice_windows = double (samplepoints_slice_windows); nan_slice_windows(! samplepoints_slice_windows) = NaN; A_med_slices = A_med .* nan_slice_windows; A_med = permute (columnwise_median (A_med_slices), [3 2 1]); fillable_gaps = missing_locs(:, cols_to_use); fill_vals = A_med(fillable_gaps); endif endswitch if (idx_flag) ## Matlab compatibility - NaNs filled back in by movmean and ## movmedian should _not_ show as filled. idx_out(fillable_gaps) = true; still_nan = missing_locs; still_nan(missing_locs) = isnan (fill_vals); idx_out(still_nan) = false; endif endif case 'movfcn' ## For each gap construct: ## xval - data values in window on either side of gap, including ## other missing values ## xloc - sample point values for those xval ## gap_loc - sample point values for gap elements ## If window has xval fully empty skip processing gap. ## missing_locs might include endgap_locs. ## Need to build gap locations accounting for both types. ## Missing values can include more than just numeric inputs. ## Windows containing no data points (e.g. endgaps when the window is ## one sided, [3 0] or [0 2]) are still passed to the mov_fcn, with ## empty value and location vectors, as MATLAB passes them: the fill ## function may depend only on the gap's own location. if (isscalar (window_size)) window_size = window_size * [-0.5, 0.5]; else window_size(1) = -window_size(1); endif ## Midgap bounds loc_before = [diff(missing_locs, 1, 1); zero_padding] == 1; loc_after = diff ([zero_padding; missing_locs], 1, 1) == -1; ## Front/back endgap locations and bounds front_gap_locs = logical (cumprod (missing_locs, 1)); front_next_locs = diff ([zero_padding; front_gap_locs], 1, 1) == -1; back_gap_locs = logical ( ... cumprod (missing_locs(dim_idx_flip{:}), 1)(dim_idx_flip{:})); back_prev_locs = [diff(back_gap_locs, 1, 1); zero_padding] == 1; ## Remove gap double counting. back_gap_locs &= ! front_gap_locs; loc_before &= ! back_prev_locs; loc_after &= ! front_next_locs; ## Build gap location array using gap starts and lengths. ## Simplest to use front / mid / back ordering, track later with sort. gap_start_locs = ... [find(front_gap_locs & [true; false(sz_A_dim-1,1)])(:); ... find(circshift (loc_before, 1, 1))(:); find(circshift (back_prev_locs, 1, 1))(:)]; gapsizes = [(sum (front_gap_locs, 1))(any (front_gap_locs, 1))(:);... find(loc_after) - find(loc_before) - 1;... (sum (back_gap_locs, 1))(any (back_gap_locs, 1))(:)]; ## Separate arrayfun/cellfun faster than single fun with ## composite anonymous function. gap_locations = arrayfun ('colon', gap_start_locs, ... gap_start_locs + gapsizes - 1, 'UniformOutput', false); gap_locations = cellfun ('transpose', ... gap_locations, 'UniformOutput', false); ## Sorting index to bridge front-mid-back and linear index ordering. [~, gap_full_sort_idx] = sort (vertcat (gap_locations{:})); ## Remove front or back gaps from gapsizes & gap_locations. ## If front/back window size = 0, or if full column is missing. ## Index to track empty/removed elements. removed_element_idx = true (numel (gap_full_sort_idx), 1); removed_front_elements = 0; removed_back_elements = 0; ## Simple front/back gap trimming for either window size = 0. if (any (missing_col_gaps = (gapsizes == sz_A_dim))) missing_col_elements = ... repelems (missing_col_gaps, [1:numel(gapsizes); gapsizes'])'; removed_element_idx(missing_col_elements) = false; gap_locations(missing_col_gaps) = []; gapsizes(missing_col_gaps) = []; endif if (! isempty (gapsizes)) gap_sample_values = cellfun_subsref (gap_locations, false, ... {samplepoints_expand}); ## Build [row,column] locations array for windows around each gap. window_points_r_c = cell (numel (gapsizes), 2); window_points_r_c(:,1) = cellfun (@(x) ... ([1:sz_A_dim]')((samplepoints= ... max (x(1) + window_size(1), samplepoints(1))) | ... (samplepoints>x(end) & samplepoints <= ... min (x(end) + window_size(2), samplepoints(end)))), ... gap_sample_values, 'UniformOutput', false); window_points_r_c(:,2) = cellfun ( ... @(x,y) (fix ((x(1)-1)/sz_A_dim)+1)(ones (size (y))), ... gap_locations, window_points_r_c(:,1), 'UniformOutput',false); if (! isempty (gapsizes)) ## Aval = A values at window locations ## Aloc = sample values at window locations A_window_indexes = cellfun ('sub2ind', {sz_A}, ... window_points_r_c(:,1), window_points_r_c(:,2), ... 'UniformOutput', false); Aval = cellfun_subsref (A_window_indexes, false, {A}); Aloc = cellfun_subsref (window_points_r_c(:,1), false, ... {samplepoints}); ## Build fill values. A window can be empty, when a one-sided ## window meets a gap at that end of the data, and the fill ## function is still called with empty values and locations, as ## MATLAB calls it. A function that cannot take them is reported ## as such rather than leaking whatever it raised. try fill_vals_C = cellfun (move_fcn, Aval, Aloc, ... gap_sample_values(:,1), 'UniformOutput', false); catch err if (any (cellfun ('isempty', Aval))) error (strcat ("fillmissing: invalid call to the fill", ... " function when its value and location", ... " arguments are empty.")); else rethrow (err); endif end_try_catch ## Check for output of move_fcn having different size than gaps. if (! all (cellfun ('numel', fill_vals_C) == gapsizes)) error (strcat ("fillmissing: fill function return values must be", " the same size as the gaps.")); endif [~, gap_trim_sort_idx] = sort (vertcat (gap_locations{:})); fill_vals_trim = cell2mat (fill_vals_C); if (! isempty (fill_vals_trim)) fill_vals = A(missing_locs); # prefill to include missing vals fill_vals(removed_element_idx(gap_full_sort_idx)) = ... fill_vals_trim(gap_trim_sort_idx); if (idx_flag) ## For movfcn with A of type having missing: ## Any outputs still containing class's 'missing' values ## are counted as not filled in idx_out, even if the value ## was put there by the movfcn. This is true even if ## missinglocations is used. If missinglocations changed ## a value with no apparent change, it still shows up ## as filled. ## If A has no missing value (int or logical), then without ## missinglocations, idx_out is always empty. With ## missinglocations, compatible behavior is undefined as ## Matlab 2022a has an apparent bug producing a error message ## saying missinglocations with int/logical needs a method that ## includes function handle. Expect behavior should match other ## methods, where any processed missing value should be marked ## as filled no matter the fill value. if ((isnumeric (A) && ! isinteger (A)) || ischar (A) || iscellstr (A)) idx_out(missing_locs) = ! ismissing (fill_vals); elseif (missinglocations) ## Any missing_locs processed and not skipped must become true. idx_out(missing_locs) = removed_element_idx(gap_full_sort_idx); endif endif endif endif endif endswitch if (! isempty (fill_vals)) A(missing_locs) = fill_vals; fill_vals = []; endif endif ## Process endgaps: if (any (endgap_locs(:))) switch (endgap_method) case 'none' endgap_locs(:) = false; case 'constant' if (isscalar (endgap_val)) fill_vals = endgap_val; else fill_vals = (endgap_locs .* endgap_val)(endgap_locs); endif if (idx_flag) ## If any v are the missing type, those get removed from idx_out ## unless using 'missinglocations'. idx_out(endgap_locs) = true; if (! missinglocations) && any (miss_ev = ismissing (endgap_val)) idx_out(endgap_locs & miss_ev) = false; endif endif case {'previous', 'next', 'nearest', 'linear', ... 'spline', 'pchip', 'makima'} ## All of these methods require sz_A_dim >= 2. shortcut path otherwise. if (sz_A_dim < 2) endgap_locs(:) = false; else switch (endgap_method) case 'previous' ## Remove any gaps at front of array, includes all-missing cols. endgap_locs(logical (cumprod (endgap_locs,1))) = false; if (any (endgap_locs(:))) ## Find locations of the 'prev' value to use for filling. subsval_loc = [diff(endgap_locs, 1, 1); zero_padding] == 1; ## Calculate the number of spots each 'prev' needs to fill. gapsizes = (sum (endgap_locs, 1))(any (endgap_locs, 1))(:); ## Construct substitution value vector. fill_vals = repelems (A(subsval_loc), ... [1:numel(gapsizes); gapsizes'])'; endif case 'next' ## Remove any gaps at back of array from endgap_locs ## including any all-missing columns. endgap_locs(logical (cumprod ( ... endgap_locs(dim_idx_flip{:}), 1)(dim_idx_flip{:}))) ... = false; if (any (endgap_locs(:))) ## Find locations of the 'next' value to use for filling. subsval_loc = diff ([zero_padding; endgap_locs],1,1) == -1; ## Calculate the number of spots each 'next' needs to fill. gapsizes = (sum (endgap_locs, 1))(any (endgap_locs, 1))(:); ## Construct substitution value vector. fill_vals = repelems (A(subsval_loc), ... [1:numel(gapsizes); gapsizes'])'; endif case 'nearest' ## Remove any all-missing columns. endgap_locs &= (! prod (endgap_locs, 1)); if (any (endgap_locs(:))) ## Find front end info. front_gap_locs = logical (cumprod (endgap_locs, 1)); front_next_loc = diff ( ... [zero_padding; front_gap_locs], 1, 1) == -1; front_gapsizes = (sum (front_gap_locs, 1))(any ... (front_gap_locs,1)); ## Find back end info. back_gap_locs = logical ( ... cumprod (endgap_locs(dim_idx_flip{:}), 1)(dim_idx_flip{:})); back_prev_loc = [diff(back_gap_locs, 1, 1); zero_padding] == 1; back_gapsizes = (sum (back_gap_locs, 1))(any (back_gap_locs,1)); ## Combine into fill variables. [~, fb_sort_idx] = sort ... ([find(front_gap_locs); find(back_gap_locs)]); fillval_loc = [find(front_next_loc); find(back_prev_loc)]; gapsizes = [front_gapsizes; back_gapsizes]; ## Construct substitution value vector with sort order to mix ## fronts and backs in column order. fill_vals = (repelems (A(fillval_loc), ... [1:numel(gapsizes); gapsizes'])')(fb_sort_idx); endif case 'linear' ## Endgaps not guaranteed to have enough points to interpolate. cols_to_use = (sum (! (missing_locs | endgap_locs), 1) > 1) ... & any (endgap_locs, 1); interp_locs = ! (missing_locs | endgap_locs) & cols_to_use; endgap_locs &= cols_to_use; if (any (endgap_locs(:))) ## Process front endgaps: front_gap_locs = logical (cumprod (endgap_locs, 1)); fill_vals_front = []; if (any (front_gap_locs(:))) front_gapsizes = (sum (front_gap_locs, 1))(any ... (front_gap_locs,1)); ## Collect first data point after gap & expand to gapsize. front_interppoint_1 = repelems ( find (... diff ([zero_padding; front_gap_locs], 1, 1) == -1), ... [1:numel(front_gapsizes); front_gapsizes'])'; ## Collect second data point after gap & expand to gapsize. front_interppoint_2 = repelems ( find ( ... diff ([zero_padding; ((cumsum (interp_locs, 1) .* ... any (front_gap_locs, 1)) == 2)], 1, 1) == 1), ... [1:numel(front_gapsizes); front_gapsizes'])'; front_interp_Avals = A([front_interppoint_1, ... front_interppoint_2]); front_interp_sampvals = samplepoints_expand( ... [front_interppoint_1, front_interppoint_2]); front_gap_loc_sampvals = samplepoints_expand(front_gap_locs); ## Hack for vector automatic orientation forcing col vector. if (isvector (front_interp_Avals)) front_interp_Avals = (front_interp_Avals(:)).'; endif if (isvector (front_interp_sampvals)) front_interp_sampvals = (front_interp_sampvals(:)).'; endif ## Perform interpolation for every gap point. interp_slopes_front = diff (front_interp_Avals, 1, 2) ... ./ diff (front_interp_sampvals, 1, 2); fill_vals_front = interp_slopes_front .* ... (front_gap_loc_sampvals - ... front_interp_sampvals(:,1)) + ... front_interp_Avals(:,1); endif ## Process back endgaps: back_gap_locs = logical ( ... cumprod (endgap_locs(dim_idx_flip{:}), 1)(dim_idx_flip{:})); fill_vals_back = []; if (any (back_gap_locs(:))) back_gapsizes = (sum ( ... back_gap_locs, 1))(any (back_gap_locs,1)); ## Collect last data point before gap & expand to gapsize. back_interppoint_2 = repelems ( ... find ([diff(back_gap_locs, 1, 1); zero_padding] == 1), ... [1:numel(back_gapsizes); back_gapsizes'])'; ## Collect 2nd to last data point before gap & expand to gap. back_interppoint_1 = repelems ( ... find ((diff ([zero_padding; ... ((cumsum (interp_locs(dim_idx_flip{:}), 1) .* ... any (back_gap_locs, 1)) == 2)], ... 1, 1) == 1)(dim_idx_flip{:})), ... [1:numel(back_gapsizes); back_gapsizes'])'; ## Build linear interpolant vectors. back_interp_Avals = A([back_interppoint_1, ... back_interppoint_2]); back_interp_sampvals = samplepoints_expand( ... [back_interppoint_1, back_interppoint_2]); back_gap_loc_sampvals = samplepoints_expand(back_gap_locs); ## Hack for vector automatic orientation forcing col vector. if (isvector (back_interp_Avals)) back_interp_Avals = (back_interp_Avals(:)).'; endif if (isvector (back_interp_sampvals)) back_interp_sampvals = (back_interp_sampvals(:)).'; endif ## Perform interpolation for every gap point. interp_slopes_back = diff (back_interp_Avals, 1, 2) ... ./ diff (back_interp_sampvals, 1, 2); fill_vals_back = interp_slopes_back .* ... (back_gap_loc_sampvals - ... back_interp_sampvals(:,1)) + ... back_interp_Avals(:,1); endif [~, fb_sort_idx] = sort ... ([find(front_gap_locs); find(back_gap_locs)]); fill_vals = [fill_vals_front; fill_vals_back](fb_sort_idx); endif case {'spline', 'pchip', 'makima'} ## endgap_locs not guaranteed to have 2 points. ## Need to ignore columns with < 2 values, or with nothing to do. cols_to_use = (sum (! (endgap_locs | missing_locs), 1) > 1) ... & any (endgap_locs, 1); ## Trim out unused cols from endgap_locs. endgap_locs &= cols_to_use; if (missinglocations) ## missinglocations may send columns with NaN and less than 2 ## real values resulting in interp1 error. Trim those columns, ## prepopulate fill_vals with NaN, mark as filled. fill_vals = NaN (sum (endgap_locs(:, cols_to_use)(:)), 1); cols_enough_points = (sum ( ... ! isnan (A) & (! endgap_locs), 1) > 1) & cols_to_use; interp_cols = (cols_enough_points & cols_to_use); interp_vals = (endgap_locs & ... cols_enough_points)(endgap_locs & cols_to_use); fill_vals(interp_vals) = other_interpolants ( ... A(:, interp_cols),endgap_locs(:, interp_cols), ... missing_locs(:, interp_cols), endgap_method, ... samplepoints); else fill_vals = other_interpolants ( A(:, cols_to_use), endgap_locs(:, cols_to_use), ... missing_locs(:, cols_to_use), endgap_method, samplepoints); endif endswitch endif if (idx_flag) idx_out(endgap_locs) = true; endif endswitch ## Some methods remove fill locations, only process if not empty. if (any (endgap_locs(:))) ## Replace missings with appropriate fill values. A(endgap_locs) = fill_vals; endif endif if (reshape_flag) ## Revert permutation: A = permute (A, dim_idx_perm); if (idx_flag) idx_out = permute (idx_out, dim_idx_perm); endif endif endfunction function varargout = cellfun_subsref (x, TF, varargin) ## Utility fcn for cellfun (@(x) A(x), x, "UniformOutput", true/false). ## ~50% faster than anonymous function call. ## pass A, x, and truefalse. ## If nargout > 1, repeat for C2 with B(x), C3 with C(x), etc. x_C = num2cell (struct ('type', '()', 'subs', num2cell (x))); for (idx = 1 : numel (varargin)) varargout{idx} = cellfun ('subsref', varargin{idx}, x_C, ... 'UniformOutput', TF); endfor endfunction function fill_vals = other_interpolants (data_array, primary_locs, secondary_locs, method, samplepoints) ## Use interp1 to perform more complex interpolations. Will only be performed ## on numerical data. ## primary_locs is missing_locs or endgap_locs, whichever the fill_vals are ## being returned for. secondary_locs is the other. ## ## FIXME: splitting out from columnwise cellfun to interp1 would increase ## speed a lot, but cannot count on same number of elements being processed ## in each column. ## ## Will error on any columns without at least two non-NaN interpolation ## values. ## ## Matlab incompatibility - using interp1, if 'missinglocations' sends through ## data_array values with NaN, interp1 will ignore those and interpolate with ## other data points. Matlab will instead return NaN. ## Find logical data and empty location indices for each column to be ## used in columnwise call to interp1 (cast as columnwise cell arrays). interp_data_locs = num2cell ((! (primary_locs | secondary_locs)), 1); interp_locs_tofill = num2cell (primary_locs, 1); ## Build cell arrays with sample and interp values to pass to interp1. [A_interpvalues, interp_samplevals] = cellfun_subsref (interp_data_locs, ... false, num2cell (data_array, 1), {samplepoints}); interp_empty_samplevals = cellfun_subsref (interp_locs_tofill, false, ... {samplepoints}); ## Generate fill_vals using interp1 for missing locations. if (strcmpi (method, 'makima')) ## Column-wise compute makima interpolation manually ncols = numel (A_interpvalues); col_results = cell (1, ncols); for k = 1:ncols xvals = interp_samplevals{k}; yvals = A_interpvalues{k}; xq = interp_empty_samplevals{k}; ## If not enough points → follow original behavior → NaN if (numel (xvals) < 2) col_results{k} = NaN (numel (xq), 1); continue; endif ## Call our new makima function (supports “extrapâ€�) col_results{k} = makima (xvals(:), yvals(:), xq(:), 'extrap'); endfor fill_vals = vertcat (col_results{:}); else ## Default path (same as original) fill_vals = vertcat (cellfun ('interp1', interp_samplevals, ... A_interpvalues, interp_empty_samplevals, {method}, ... {'extrap'}, 'UniformOutput', false){:}); endif endfunction function med = columnwise_median (x) ## Take a column of values, ignore any NaN values, return the median ## of what's left. Return NaN if no values. ## Uses only built-in fns to avoid 3x 'median' slowdown. szx = size (x); if (isempty (x)) med = NaN ([1, szx(2:end)]); elseif (isvector (x)) x = x(! isnan (x)); x = sort (x); n = numel (x); if (mod (n, 2)) ## odd med = x((n+1)/2); elseif (n > 0) ## even med = (x(n/2) + x((n/2)+1))/2; else ## Only called for types with missing = NaN. med = NaN; endif else x = sort (x, 1); # NaNs sent to bottom. n = sum (! isnan (x), 1); m_idx_odd = logical (mod (n, 2)); # 0 even or zero, 1 odd. m_idx_even = !m_idx_odd & (n != 0); k = floor ((n + 1) ./ 2); med = NaN ([1, szx(2:end)]); if (! ismatrix (x)) szx = [szx(1), prod(szx(2:end))]; endif if (any (m_idx_odd(:))) x_idx_odd = sub2ind (szx, k(m_idx_odd)(:), find (m_idx_odd)(:)); med(m_idx_odd) = x(x_idx_odd); endif if (any (m_idx_even(:))) k_even = k(m_idx_even)(:); x_idx_even = sub2ind (szx, [k_even, k_even+1], ... (find (m_idx_even))(:, [1, 1])); med(m_idx_even) = sum (x(x_idx_even), 2) / 2; endif endif endfunction %!assert_equal (fillmissing ([1, 2, 3], 'constant', 99), [1, 2, 3]) %!assert_equal (fillmissing ([1, 2, NaN], 'constant', 99), [1, 2, 99]) %!assert_equal (fillmissing ([NaN, 2, NaN], 'constant', 99), [99, 2, 99]) %!assert_equal (fillmissing ([1, 2, 3]', 'constant', 99), [1, 2, 3]') %!assert_equal (fillmissing ([1, 2, NaN]', 'constant', 99), [1, 2, 99]') %!assert_equal (fillmissing ([1, 2, 3; 4, 5, 6], 'constant', 99), [1, 2, 3; 4, 5, 6]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], 'constant', 99), [1, 2, 99; 4, 99, 6]) %!assert_equal (fillmissing ([NaN, 2, NaN; 4, NaN, 6], 'constant', [97, 98, 99]), [97, 2, 99; 4, 98, 6]) %!test %! x = cat (3, [1, 2, NaN; 4, NaN, 6], [NaN, 2, 3; 4, 5, NaN]); %! y = cat (3, [1, 2, 99; 4, 99, 6], [99, 2, 3; 4, 5, 99]); %! assert_equal (fillmissing (x, 'constant', 99), y); %! y = cat (3, [1, 2, 96; 4, 95, 6], [97, 2, 3; 4, 5, 99]); %! assert_equal (fillmissing (x, 'constant', [94:99]), y); %! assert_equal (fillmissing (x, 'constant', [94:99]'), y); %! assert_equal (fillmissing (x, 'constant', permute ([94:99], [1 3 2])), y); %! assert_equal (fillmissing (x, 'constant', [94, 96, 98; 95, 97, 99]), y); %! assert_equal (fillmissing (x, 'constant', [94:99], 1), y); %! y = cat (3, [1, 2, 96; 4, 97, 6], [98, 2, 3; 4, 5, 99]); %! assert_equal (fillmissing (x, 'constant', [96:99], 2), y); %! y = cat (3, [1, 2, 98; 4, 97, 6], [94, 2, 3; 4, 5, 99]); %! assert_equal (fillmissing (x, 'constant', [94:99], 3), y); %! y = cat (3, [1, 2, 92; 4, 91, 6], [94, 2, 3; 4, 5, 99]); %! assert_equal (fillmissing (x, 'constant', [88:99], 99), y); %!test %! x = reshape ([1:24], 4, 3, 2); %! x([1, 6, 7, 9, 12, 14, 16, 19, 22, 23]) = NaN; %! y = x; %! y([1, 6, 7, 9, 12, 14, 16, 19, 22, 23]) = [94, 95, 95, 96, 96, 97, 97, 98, 99, 99]; %! assert_equal (fillmissing (x, 'constant', [94:99], 1), y); %! y([1, 6, 7, 9, 12, 14, 16, 19, 22, 23]) = [92, 93, 94, 92, 95, 97, 99, 98, 97, 98]; %! assert_equal (fillmissing (x, 'constant', [92:99], 2), y); %! y([1, 6, 7, 9, 12, 14, 16, 19, 22, 23]) = [88, 93, 94, 96, 99, 89, 91, 94, 97, 98]; %! assert_equal (fillmissing (x, 'constant', [88:99], 3), y); %! y([1, 6, 7, 9, 12, 14, 16, 19, 22, 23]) = [76, 81, 82, 84, 87, 89, 91, 94, 97, 98]; %! assert_equal (fillmissing (x, 'constant', [76:99], 99), y); ## Tests with different endvalues behavior %!assert_equal (fillmissing ([1, 2, 3], 'constant', 99, 'endvalues', 88), [1, 2, 3]) %!assert_equal (fillmissing ([1, NaN, 3], 'constant', 99, 'endvalues', 88), [1, 99, 3]) %!assert_equal (fillmissing ([1, 2, NaN], 'constant', 99, 'endvalues', 88), [1, 2, 88]) %!assert_equal (fillmissing ([NaN, 2, 3], 'constant', 99, 'endvalues', 88), [88, 2, 3]) %!assert_equal (fillmissing ([NaN, NaN, 3], 'constant', 99, 'endvalues', 88), [88, 88, 3]) %!assert_equal (fillmissing ([1, NaN, NaN], 'constant', 99, 'endvalues', 88), [1, 88, 88]) %!assert_equal (fillmissing ([NaN, 2, NaN], 'constant', 99, 'endvalues', 88), [88, 2, 88]) %!assert_equal (fillmissing ([NaN, 2, NaN]', 'constant', 99, 'endvalues', 88), [88, 2, 88]') %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], 'constant', 99, 'endvalues', 88), [1, 99, 3, 99, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'constant', 99, 'endvalues', 88), [1, 99, 99, 99, 5]) %!assert_equal (fillmissing ([NaN, NaN, NaN, NaN, 5], 'constant', 99, 'endvalues', 88), [88, 88, 88, 88, 5]) %!assert_equal (fillmissing ([1, NaN, 3, 4, NaN], 'constant', 99, 'endvalues', 88), [1, 99, 3, 4, 88]) %!assert_equal (fillmissing ([1, NaN, 3, 4, NaN], 'constant', 99, 1, 'endvalues', 88), [1, 88, 3, 4, 88]) %!assert_equal (fillmissing ([1, NaN, 3, 4, NaN], 'constant', 99, 1, 'endvalues', 'extrap'), [1, 99, 3, 4, 99]) %!test %! x = reshape ([1:24], 3, 4, 2); %! y = x; %! x([1, 2, 5, 6, 8, 10, 13, 16, 18, 19, 20, 21, 22]) = NaN; %! y([1, 2, 5, 6, 10, 13, 16, 18, 19, 20, 21, 22]) = 88; %! y([8]) = 99; %! assert_equal (fillmissing (x, 'constant', 99, 'endvalues', 88), y); %! assert_equal (fillmissing (x, 'constant', 99, 1, 'endvalues', 88), y); %! y = x; %! y([1, 2, 5, 8, 10, 13, 16, 19, 22]) = 88; %! y([6, 18, 20, 21]) = 99; %! assert_equal (fillmissing (x, 'constant', 99, 2, 'endvalues', 88), y); %! y(y == 99) = 88; %! assert_equal (fillmissing (x, 'constant', 99, 3, 'endvalues', 88), y); %! assert_equal (fillmissing (x, 'constant', 99, 4, 'endvalues', 88), y); %! assert_equal (fillmissing (x, 'constant', 99, 99, 'endvalues', 88), y); %! y([8]) = 94; %! assert_equal (fillmissing (x, 'constant', [92:99], 1, 'endvalues', 88), y); %! y([6, 8, 18, 20, 21]) = [96, 88, 99, 98, 99]; %! assert_equal (fillmissing (x, 'constant', [94:99], 2, 'endvalues', 88), y); %! y = x; %! y(isnan (y)) = 88; %! assert_equal (fillmissing (x, 'constant', [88:99], 3, 'endvalues', 88), y); %! y = x; %! y(isnan (y)) = [82, 82, 83, 83, 94, 85, 86, 87, 87, 88, 88, 88, 89]; %! assert_equal (fillmissing (x, 'constant', [92:99], 1, 'endvalues', [82:89]), y); %! y = x; %! y(isnan (y)) = [84, 85, 85, 96, 85, 84, 87, 87, 99, 87, 98, 99, 87]; %! assert_equal (fillmissing (x, 'constant', [94:99], 2, 'endvalues', [84:89]), y); %! y = x; %! y(isnan (y)) = [68, 69, 72, 73, 75, 77, 68, 71, 73, 74, 75, 76, 77]; %! assert_equal (fillmissing (x, 'constant', [88:99], 3, 'endvalues', [68:79]), y); %! assert_equal (fillmissing (x, 'constant', [88:93; 94:99]', 3, 'endvalues', [68:73; 74:79]'), y) %!test %! x = reshape ([1:24],4,3,2); %! x([1, 6, 7, 9, 12, 14, 16, 19, 22, 23]) = NaN; %! y = x; %! y([1, 6, 7, 9, 12, 14, 16, 19, 22, 23]) = [94, 95, 95, 96, 96, 97, 97, 98, 99, 99]; %! assert_equal (fillmissing (x, 'constant', [94:99], 1), y); %! y([1, 6, 7, 9, 12, 14, 16, 19, 22, 23]) = [92, 93, 94, 92, 95, 97, 99, 98, 97, 98]; %! assert_equal (fillmissing (x, 'constant', [92:99], 2), y); %! y([1, 6, 7, 9, 12, 14, 16, 19, 22, 23]) = [88, 93, 94, 96, 99, 89, 91, 94, 97, 98]; %! assert_equal (fillmissing (x, 'constant', [88:99], 3), y); %! y([1, 6, 7, 9, 12, 14, 16, 19, 22, 23]) = [76, 81, 82, 84, 87, 89, 91, 94, 97, 98]; %! assert_equal (fillmissing (x, 'constant', [76:99], 99), y); ## next/previous tests %!assert_equal (fillmissing ([1, 2, 3], 'previous'), [1, 2, 3]) %!assert_equal (fillmissing ([1, 2, 3], 'next'), [1, 2, 3]) %!assert_equal (fillmissing ([1, 2, 3]', 'previous'), [1, 2, 3]') %!assert_equal (fillmissing ([1, 2, 3]', 'next'), [1, 2, 3]') %!assert_equal (fillmissing ([1, 2, NaN], 'previous'), [1, 2, 2]) %!assert_equal (fillmissing ([1, 2, NaN], 'next'), [1, 2, NaN]) %!assert_equal (fillmissing ([NaN, 2, NaN], 'previous'), [NaN, 2, 2]) %!assert_equal (fillmissing ([NaN, 2, NaN], 'next'), [2, 2, NaN]) %!assert_equal (fillmissing ([1, NaN, 3], 'previous'), [1, 1, 3]) %!assert_equal (fillmissing ([1, NaN, 3], 'next'), [1, 3, 3]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], 'previous', 1), [1, 2, NaN; 4, 2, 6]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], 'previous', 2), [1, 2, 2; 4, 4, 6]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], 'previous', 3), [1, 2, NaN; 4, NaN, 6]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], 'next', 1), [1, 2, 6; 4, NaN, 6]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], 'next', 2), [1, 2, NaN; 4, 6, 6]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], 'next', 3), [1, 2, NaN; 4, NaN, 6]) %!test %! x = reshape ([1:24], 4, 3, 2); %! x([1, 6, 7, 9, 12, 14, 16, 19, 22, 23]) = NaN; %! y = x; %! y([1, 6, 7, 9, 14, 19, 22, 23]) = [2, 8, 8, 10, 15, 20, 24, 24]; %! assert_equal (fillmissing (x, 'next', 1), y); %! y = x; %! y([1, 6, 7, 14, 16]) = [5, 10, 11, 18, 20]; %! assert_equal (fillmissing (x, 'next', 2), y); %! y = x; %! y([1, 6, 9, 12]) = [13, 18, 21, 24]; %! assert_equal (fillmissing (x, 'next', 3), y); %! assert_equal (fillmissing (x, 'next', 99), x); %! y = x; %! y([6, 7, 12, 14, 16, 19, 22, 23]) = [5, 5, 11, 13, 15, 18, 21, 21]; %! assert_equal (fillmissing (x, 'previous', 1), y); %! y = x; %! y([6, 7, 9, 12, 19, 22, 23]) = [2, 3, 5, 8, 15, 18, 15]; %! assert_equal (fillmissing (x, 'previous', 2), y); %! y = x; %! y([14, 16, 22, 23]) = [2, 4, 10, 11]; %! assert_equal (fillmissing (x, 'previous', 3), y); %! assert_equal (fillmissing (x, 'previous', 99), x); ## next/previous tests with different endvalue behavior %!assert_equal (fillmissing ([1, 2, 3], 'constant', 0, 'endvalues', 'previous'), [1, 2, 3]) %!assert_equal (fillmissing ([1, 2, 3], 'constant', 0, 'endvalues', 'next'), [1, 2, 3]) %!assert_equal (fillmissing ([1, NaN, 3], 'constant', 0, 'endvalues', 'previous'), [1, 0, 3]) %!assert_equal (fillmissing ([1, NaN, 3], 'constant', 0, 'endvalues', 'next'), [1, 0, 3]) %!assert_equal (fillmissing ([1, 2, NaN], 'constant', 0, 'endvalues', 'previous'), [1, 2, 2]) %!assert_equal (fillmissing ([1, 2, NaN], 'constant', 0, 'endvalues', 'next'), [1, 2, NaN]) %!assert_equal (fillmissing ([1, NaN, NaN], 'constant', 0, 'endvalues', 'previous'), [1, 1, 1]) %!assert_equal (fillmissing ([1, NaN, NaN], 'constant', 0, 'endvalues', 'next'), [1, NaN, NaN]) %!assert_equal (fillmissing ([NaN, 2, 3], 'constant', 0, 'endvalues', 'previous'), [NaN, 2, 3]) %!assert_equal (fillmissing ([NaN, 2, 3], 'constant', 0, 'endvalues', 'next'), [2, 2, 3]) %!assert_equal (fillmissing ([NaN, NaN, 3], 'constant', 0, 'endvalues', 'previous'), [NaN, NaN, 3]) %!assert_equal (fillmissing ([NaN, NaN, 3], 'constant', 0, 'endvalues', 'next'), [3, 3, 3]) %!assert_equal (fillmissing ([NaN, NaN, NaN], 'constant', 0, 'endvalues', 'previous'), [NaN, NaN, NaN]) %!assert_equal (fillmissing ([NaN, NaN, NaN], 'constant', 0, 'endvalues', 'next'), [NaN, NaN, NaN]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'constant', 0, 'endvalues', 'previous'), [NaN, 2, 0, 4, 4]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'constant', 0, 'endvalues', 'next'), [2, 2, 0, 4, NaN]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'constant', 0, 1, 'endvalues', 'previous'), [NaN, 2, NaN, 4, NaN]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'constant', 0, 1, 'endvalues', 'next'), [NaN, 2, NaN, 4, NaN]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'constant', 0, 2, 'endvalues', 'previous'), [NaN, 2, 0, 4, 4]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'constant', 0, 2, 'endvalues', 'next'), [2, 2, 0, 4, NaN]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'constant', 0, 3, 'endvalues', 'previous'), [NaN, 2, NaN, 4, NaN]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'constant', 0, 3, 'endvalues', 'next'), [NaN, 2, NaN, 4, NaN]) %!test %! x = reshape ([1:24], 3, 4, 2); %! x([1, 2, 5, 6, 8, 10, 13, 16, 18, 19, 20, 21, 22]) = NaN; %! y = x; %! y([5, 6, 8, 18]) = [4, 4, 0, 17]; %! assert_equal (fillmissing (x, 'constant', 0, 'endvalues', 'previous'), y); %! assert_equal (fillmissing (x, 'constant', 0, 1, 'endvalues', 'previous'), y); %! y = x; %! y([6, 10, 18, 20, 21]) = [0, 7, 0, 0, 0]; %! assert_equal (fillmissing (x, 'constant', 0, 2, 'endvalues', 'previous'), y); %! y = x; %! y([16, 19, 21]) = [4, 7, 9]; %! assert_equal (fillmissing (x, 'constant', 0, 3, 'endvalues', 'previous'), y); %! assert_equal (fillmissing (x, 'constant', 0, 4, 'endvalues', 'previous'), x); %! assert_equal (fillmissing (x, 'constant', 0, 99, 'endvalues', 'previous'), x); %! y = x; %! y([1, 2, 8, 10, 13, 16, 22]) = [3, 3, 0, 11, 14, 17, 23]; %! assert_equal (fillmissing (x, 'constant', 0, 'endvalues', 'next'), y); %! assert_equal (fillmissing (x, 'constant', 0, 1, 'endvalues', 'next'), y); %! y = x; %! y([1, 2, 5, 6, 8, 18, 20, 21]) = [4, 11, 11, 0, 11, 0, 0, 0]; %! assert_equal (fillmissing (x, 'constant', 0, 2, 'endvalues', 'next'), y); %! y = x; %! y([2, 5]) = [14, 17]; %! assert_equal (fillmissing (x, 'constant', 0, 3, 'endvalues', 'next'), y); %! assert_equal (fillmissing (x, 'constant', 0, 4, 'endvalues', 'next'), x); %! assert_equal (fillmissing (x, 'constant', 0, 99, 'endvalues', 'next'), x); ## Tests for nearest %!assert_equal (fillmissing ([1, 2, 3], 'nearest'), [1, 2, 3]) %!assert_equal (fillmissing ([1, 2, 3]', 'nearest'), [1, 2, 3]') %!assert_equal (fillmissing ([1, 2, NaN], 'nearest'), [1, 2, 2]) %!assert_equal (fillmissing ([NaN, 2, NaN], 'nearest'), [2, 2, 2]) %!assert_equal (fillmissing ([1, NaN, 3], 'nearest'), [1, 3, 3]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], 'nearest', 1), [1, 2, 6; 4, 2, 6]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], 'nearest', 2), [1, 2, 2; 4, 6, 6]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], 'nearest', 3), [1, 2, NaN; 4, NaN, 6]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], 'nearest'), [1, 3, 3, 5, 5]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], 'nearest', 'samplepoints', [0, 1, 2, 3, 4]), [1, 3, 3, 5, 5]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], 'nearest', 'samplepoints', [0.5, 1, 2, 3, 5]), [1, 1, 3, 3, 5]) %!test %! x = reshape ([1:24], 4, 3, 2); %! x([1, 6, 7, 9, 12, 14, 16, 19, 22, 23]) = NaN; %! y = x; %! y([1, 6, 7, 9, 12, 14, 16, 19, 22, 23]) = [2, 5, 8, 10, 11, 15, 15, 20, 21, 24]; %! assert_equal (fillmissing (x, 'nearest', 1), y); %! y = x; %! y([1, 6, 7, 9, 12, 14, 16, 19, 22, 23]) = [5, 10, 11, 5, 8, 18, 20, 15, 18, 15]; %! assert_equal (fillmissing (x, 'nearest', 2), y); %! y = x; %! y([1, 6, 9, 12, 14, 16, 22, 23]) = [13, 18, 21, 24, 2, 4, 10, 11]; %! assert_equal (fillmissing (x, 'nearest', 3), y); %! assert_equal (fillmissing (x, 'nearest', 99), x); ## Tests for nearest with diff endvalue behavior %!assert_equal (fillmissing ([1, 2, 3], 'constant', 0, 'endvalues', 'nearest'), [1, 2, 3]) %!assert_equal (fillmissing ([1, NaN, 3], 'constant', 0, 'endvalues', 'nearest'), [1 0 3]) %!assert_equal (fillmissing ([1, 2, NaN], 'constant', 0, 'endvalues', 'nearest'), [1, 2, 2]) %!assert_equal (fillmissing ([1, NaN, NaN], 'constant', 0, 'endvalues', 'nearest'), [1, 1, 1]) %!assert_equal (fillmissing ([NaN, 2, 3], 'constant', 0, 'endvalues', 'nearest'), [2, 2, 3]) %!assert_equal (fillmissing ([NaN, NaN, 3], 'constant', 0, 'endvalues', 'nearest'), [3, 3, 3]) %!assert_equal (fillmissing ([NaN, NaN, NaN], 'constant', 0, 'endvalues', 'nearest'), [NaN, NaN, NaN]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'constant', 0, 'endvalues', 'nearest'), [2, 2, 0, 4, 4]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'constant', 0, 1, 'endvalues', 'nearest'), [NaN, 2, NaN, 4, NaN]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'constant', 0, 2, 'endvalues', 'nearest'), [2, 2, 0, 4, 4]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'constant', 0, 3, 'endvalues', 'nearest'), [NaN, 2, NaN, 4, NaN]) %!test %! x = reshape ([1:24], 3, 4, 2); %! x([1, 2, 5, 6, 8, 10, 13, 16, 18, 19, 20, 21, 22]) = NaN; %! y = x; %! y([1, 2, 5, 6, 8, 10, 13, 16, 18, 22]) = [3, 3, 4, 4, 0, 11, 14, 17, 17, 23]; %! assert_equal (fillmissing (x, 'constant', 0, 'endvalues', 'nearest'), y); %! assert_equal (fillmissing (x, 'constant', 0, 1, 'endvalues', 'nearest'), y); %! y = x; %! y([1, 2, 5, 6, 8, 10, 18, 20, 21]) = [4, 11, 11, 0, 11, 7, 0, 0, 0]; %! assert_equal (fillmissing (x, 'constant', 0, 2, 'endvalues', 'nearest'), y); %! y = x; %! y([2, 5, 16, 19, 21]) = [14, 17, 4, 7, 9]; %! assert_equal (fillmissing (x, 'constant', 0, 3, 'endvalues', 'nearest'), y); %! assert_equal (fillmissing (x, 'constant', 0, 99, 'endvalues', 'nearest'), x); ## Tests for linear %!assert_equal (fillmissing ([1, 2, 3], 'linear'), [1, 2, 3]) %!assert_equal (fillmissing ([1, 2, 3]', 'linear'), [1, 2, 3]') %!assert_equal (fillmissing ([1, 2, NaN], 'linear'), [1, 2, 3]) %!assert_equal (fillmissing ([NaN, 2, NaN], 'linear'), [NaN, 2, NaN]) %!assert_equal (fillmissing ([1, NaN, 3], 'linear'), [1, 2, 3]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], 'linear', 1), [1, 2, NaN; 4, NaN, 6]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], 'linear', 2), [1, 2, 3; 4, 5, 6]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], 'linear', 3), [1, 2, NaN; 4, NaN, 6]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], 'linear'), [1, 2, 3, 4, 5]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], 'linear', 'samplepoints', [0, 1, 2, 3, 4]), [1, 2, 3, 4, 5]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], 'linear', 'samplepoints', [0, 1.5, 2, 5, 14]), [1, 2.5, 3, 3.5, 5], eps) %!test %! x = reshape ([1:24], 4, 3, 2); %! x([1, 6, 7, 9, 12, 14, 16, 19, 22, 23]) = NaN; %! assert_equal (fillmissing (x, 'linear', 1), reshape ([1:24], 4, 3, 2)); %! y = reshape ([1:24], 4, 3, 2); %! y([1, 9, 14, 19, 22, 23]) = NaN; %! assert_equal (fillmissing (x, 'linear', 2), y); %! y = reshape ([1:24], 4, 3, 2); %! y([1, 6, 7, 9, 12, 14, 16, 19, 22, 23]) = NaN; %! assert_equal (fillmissing (x, 'linear', 3), y); %! assert_equal (fillmissing (x, 'linear', 99), x); ## Tests for linear with diff endvalue behavior %!assert_equal (fillmissing ([1, 2, 3], 'linear', 'endvalues', 0), [1, 2, 3]) %!assert_equal (fillmissing ([1, NaN, 3], 'linear', 'endvalues', 0), [1, 2, 3]) %!assert_equal (fillmissing ([1, 2, NaN], 'linear', 'endvalues', 0), [1, 2, 0]) %!assert_equal (fillmissing ([1, NaN, NaN], 'linear', 'endvalues', 0), [1, 0, 0]) %!assert_equal (fillmissing ([NaN, 2, 3], 'linear', 'endvalues', 0), [0, 2, 3]) %!assert_equal (fillmissing ([NaN, NaN, 3], 'linear', 'endvalues', 0), [0, 0, 3]) %!assert_equal (fillmissing ([NaN, NaN, NaN], 'linear', 'endvalues', 0), [0, 0, 0]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'linear', 'endvalues', 0), [0, 2, 3, 4, 0]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'linear', 1, 'endvalues', 0), [0, 2, 0, 4, 0]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'linear', 2, 'endvalues', 0), [0, 2, 3, 4, 0]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'linear', 3, 'endvalues', 0), [0, 2, 0, 4, 0]) %!test %! x = reshape ([1:24], 3, 4, 2); %! x([1, 2, 5, 6, 8, 10, 13, 16, 18, 19, 20, 21, 22]) = NaN; %! y = x; %! y([1, 2, 5, 6, 10, 13, 16, 18, 19, 20, 21, 22]) = 0; %! y(8) = 8; %! assert_equal (fillmissing (x, 'linear', 'endvalues', 0), y); %! assert_equal (fillmissing (x, 'linear', 1, 'endvalues', 0), y); %! y = x; %! y([1, 2, 5, 8, 10, 13, 16, 19, 22]) = 0; %! y([6, 18, 20, 21]) = [6, 18, 20, 21]; %! assert_equal (fillmissing (x, 'linear', 2, 'endvalues', 0), y); %! y = x; %! y(isnan (y)) = 0; %! assert_equal (fillmissing (x, 'linear', 3, 'endvalues', 0), y); %! assert_equal (fillmissing (x, 'linear', 99, 'endvalues', 0), y); ## Tests with linear only on endvalues %!assert_equal (fillmissing ([1, 2, 3], 'constant', 99, 'endvalues', 'linear'), [1, 2, 3]) %!assert_equal (fillmissing ([1, NaN, 3], 'constant', 99, 'endvalues', 'linear'), [1, 99, 3]) %!assert_equal (fillmissing ([1, NaN, 3, NaN], 'constant', 99, 'endvalues', 'linear'), [1, 99, 3, 4]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'constant', 99, 'endvalues', 'linear'), [1, 2, 99, 4, 5]) %!assert_equal (fillmissing ([NaN, 2, NaN, NaN], 'constant', 99, 'endvalues', 'linear'), [NaN, 2, NaN, NaN]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'constant', 99, 'endvalues', 'linear', 'samplepoints', [1, 2, 3, 4, 5]), [1, 2, 99, 4, 5]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'constant', 99, 'endvalues', 'linear', 'samplepoints', [0, 2, 3, 4, 10]), [0, 2, 99, 4, 10]) ## Test other interpolants %!test %! x = reshape ([1:24], 3, 4, 2); %! x([1, 2, 5, 6, 8, 10, 13, 16, 18, 19, 20, 21, 22]) = NaN; %! y = x; %! y([1, 6, 10, 18, 20, 21]) = [2.5, 5, 8.5, 17.25, 21, 21.75]; %! assert_equal (fillmissing (x, 'linear', 2, 'samplepoints', [2 4 8 10]), y, eps); %! y([1, 6, 10, 18, 20, 21]) = [2.5, 4.5, 8.5, 17.25, 21.5, 21.75]; %! assert_equal (fillmissing (x, 'spline', 2, 'samplepoints', [2, 4, 8, 10]), y, eps); %! y([1, 6, 10, 18, 20, 21]) = [2.5, 4.559386973180077, 8.5, 17.25, 21.440613026819925, 21.75]; %! assert_equal (fillmissing (x, 'pchip', 2, 'samplepoints', [2, 4, 8, 10]), y, 10*eps); ## known fail: makima method not yet implemented in interp1 %!test <60965> %! x = reshape ([1:24], 3, 4, 2); %! x([1, 2, 5, 6, 8, 10, 13, 16, 18, 19, 20, 21, 22]) = NaN; %! y = x; %! y([1, 6, 10, 18, 20, 21]) = [2.5, 4.609523809523809, 8.5, 17.25, 21.390476190476186, 21.75]; %! assert_equal (fillmissing (x, 'makima', 2, 'samplepoints', [2, 4, 8, 10]), y, 1e-14); ## Test other interpolants code path on endvalues %!assert_equal (fillmissing ([1, 2, 3], 'constant', 99, 'endvalues', 'spline'), [1, 2, 3]) %!assert_equal (fillmissing ([1, NaN, 3], 'constant', 99, 'endvalues', 'spline'), [1, 99, 3]) %!assert_equal (fillmissing ([1, NaN, 3, NaN], 'constant', 99, 'endvalues', 'spline'), [1, 99, 3, 4]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'constant', 99, 'endvalues', 'spline'), [1, 2, 99, 4, 5]) %!assert_equal (fillmissing ([NaN, 2, NaN, NaN], 'constant', 99, 'endvalues', 'spline'), [NaN, 2, NaN, NaN]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'constant', 99, 'endvalues', 'spline', 'samplepoints', [1, 2, 3, 4, 5]), [1, 2, 99, 4, 5]) %!assert_equal (fillmissing ([NaN, 2, NaN, 4, NaN], 'constant', 99, 'endvalues', 'spline', 'samplepoints', [0, 2, 3, 4, 10]), [0, 2, 99, 4, 10]) ## Test movmean %!assert_equal (fillmissing ([1, 2, 3], 'movmean', 1), [1, 2, 3]) %!assert_equal (fillmissing ([1, 2, NaN], 'movmean', 1), [1, 2, NaN]) %!assert_equal (fillmissing ([1, 2, 3], 'movmean', 2), [1, 2, 3]) %!assert_equal (fillmissing ([1, 2, 3], 'movmean', [1, 0]), [1, 2, 3]) %!assert_equal (fillmissing ([1, 2, 3]', 'movmean', 2), [1, 2, 3]') %!assert_equal (fillmissing ([1, 2, NaN], 'movmean', 2), [1, 2, 2]) %!assert_equal (fillmissing ([1, 2, NaN], 'movmean', [1, 0]), [1, 2, 2]) %!assert_equal (fillmissing ([1, 2, NaN], 'movmean', [1, 0]'), [1, 2, 2]) %!assert_equal (fillmissing ([NaN, 2, NaN], 'movmean', 2), [NaN, 2, 2]) %!assert_equal (fillmissing ([NaN, 2, NaN], 'movmean', [1, 0]), [NaN, 2, 2]) %!assert_equal (fillmissing ([NaN, 2, NaN], 'movmean', [0, 1]), [2, 2, NaN]) %!assert_equal (fillmissing ([NaN, 2, NaN], 'movmean', [0, 1.1]), [2, 2, NaN]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], 'movmean', [3, 0]), [1, 1, 3, 2, 5]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], 'movmean', 3, 1), [1, 2, 6; 4, 2, 6]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], 'movmean', 3, 2), [1, 2, 2; 4, 5, 6]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], 'movmean', 3, 3), [1, 2, NaN; 4, NaN, 6]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], 'movmean', 99), [1, 3, 3, 3, 5]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], 'movmean', 99, 1), [1, NaN, 3, NaN, 5]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5]', 'movmean', 99, 1), [1, 3, 3, 3, 5]') %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], 'movmean', 99, 2), [1, 3, 3, 3, 5]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5]', 'movmean', 99, 2), [1, NaN, 3, NaN, 5]') %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmean', 3, 'samplepoints', [1, 2, 3, 4, 5]), [1, 1, NaN, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmean', [1, 1], 'samplepoints', [1, 2, 3, 4, 5]), [1, 1, NaN, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmean', [1.5, 1.5], 'samplepoints', [1, 2, 3, 4, 5]), [1, 1, NaN, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmean', 4, 'samplepoints', [1, 2, 3, 4, 5]), [1, 1, 1, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmean', [2, 2], 'samplepoints', [1, 2, 3, 4, 5]), [1, 1, 3, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmean', 4.0001, 'samplepoints', [1, 2, 3, 4, 5]), [1, 1, 3, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmean', 3, 'samplepoints', [1.5, 2, 3, 4, 5]), [1, 1, 1, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmean', 3, 'samplepoints', [1 2, 3, 4, 4.5]), [1, 1, NaN, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmean', 3, 'samplepoints', [1.5, 2, 3, 4, 4.5]), [1, 1, 1, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmean', [1.5, 1.5], 'samplepoints', [1.5, 2, 3, 4, 5]), [1, 1, 1, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmean', [1.5, 1.5], 'samplepoints', [1, 2, 3, 4, 4.5]), [1, 1, 5, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmean', [1.5, 1.5], 'samplepoints', [1.5, 2 3, 4, 4.5]), [1, 1, 3, 5, 5]) %!test %! x = reshape ([1:24], 3, 4, 2); %! x([1, 2, 5, 6, 8, 10, 13, 16, 18, 19, 20, 21, 22]) = NaN; %! y = x; %! y([2, 5, 8, 10, 13, 16, 18, 22]) = [3, 4, 8, 11, 14, 17, 17, 23]; %! assert_equal (fillmissing (x, 'movmean', 3), y); %! assert_equal (fillmissing (x, 'movmean', [1, 1]), y); %! assert_equal (fillmissing (x, 'movmean', 3, 'endvalues', 'extrap'), y); %! assert_equal (fillmissing (x, 'movmean', 3, 'samplepoints', [1, 2, 3]), y); %! y = x; %! y([1, 6, 8, 10, 18, 20, 21]) = [4, 6, 11, 7, 15, 20, 24]; %! assert_equal (fillmissing (x, 'movmean', 3, 2), y); %! assert_equal (fillmissing (x, 'movmean', [1, 1], 2), y); %! assert_equal (fillmissing (x, 'movmean', 3, 2, 'endvalues', 'extrap'), y); %! assert_equal (fillmissing (x, 'movmean', 3, 2, 'samplepoints', [1, 2, 3, 4]), y); %! y([1, 18]) = NaN; %! y(6) = 9; %! assert_equal (fillmissing (x, 'movmean', 3, 2, 'samplepoints', [0, 2, 3, 4]), y); %! y = x; %! y([1, 2, 5, 6, 10, 13, 16, 18, 19, 20, 21, 22]) = 99; %! y(8) = 8; %! assert_equal (fillmissing (x, 'movmean', 3, 'endvalues', 99), y); %! y = x; %! y([1, 2, 5, 8, 10, 13, 16, 19, 22]) = 99; %! y([6, 18, 20, 21]) = [6, 15, 20, 24]; %! assert_equal (fillmissing (x, 'movmean', 3, 2, 'endvalues', 99), y); ## Test movmedian %!assert_equal (fillmissing ([1, 2, 3], 'movmedian', 1), [1, 2, 3]) %!assert_equal (fillmissing ([1, 2, NaN], 'movmedian', 1), [1, 2, NaN]) %!assert_equal (fillmissing ([1, 2, 3], 'movmedian', 2), [1, 2, 3]) %!assert_equal (fillmissing ([1, 2, 3], 'movmedian', [1, 0]), [1, 2, 3]) %!assert_equal (fillmissing ([1, 2, 3]', 'movmedian', 2), [1, 2, 3]') %!assert_equal (fillmissing ([1, 2, NaN], 'movmedian', 2), [1, 2, 2]) %!assert_equal (fillmissing ([1, 2, NaN], 'movmedian', [1, 0]), [1, 2, 2]) %!assert_equal (fillmissing ([1, 2, NaN], 'movmedian', [1, 0]'), [1, 2, 2]) %!assert_equal (fillmissing ([NaN, 2, NaN], 'movmedian', 2), [NaN, 2, 2]) %!assert_equal (fillmissing ([NaN, 2, NaN], 'movmedian', [1, 0]), [NaN, 2, 2]) %!assert_equal (fillmissing ([NaN, 2, NaN], 'movmedian', [0, 1]), [2, 2, NaN]) %!assert_equal (fillmissing ([NaN, 2, NaN], 'movmedian', [0, 1.1]), [2, 2, NaN]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], 'movmedian', [3, 0]), [1, 1, 3, 2, 5]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], 'movmedian', 3, 1), [1, 2, 6; 4, 2, 6]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], 'movmedian', 3, 2), [1, 2, 2; 4, 5, 6]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], 'movmedian', 3, 3), [1, 2, NaN; 4, NaN, 6]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], 'movmedian', 99), [1, 3, 3, 3, 5]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], 'movmedian', 99, 1), [1, NaN, 3, NaN, 5]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5]', 'movmedian', 99, 1), [1, 3, 3, 3, 5]') %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], 'movmedian', 99, 2), [1, 3, 3, 3, 5]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5]', 'movmedian', 99, 2), [1, NaN, 3, NaN, 5]') %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmedian', 3, 'samplepoints', [1, 2, 3, 4, 5]), [1, 1, NaN, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmedian', [1, 1], 'samplepoints', [1, 2, 3, 4, 5]), [1, 1, NaN, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmedian', [1.5, 1.5], 'samplepoints', [1, 2, 3, 4, 5]), [1, 1, NaN, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmedian', 4, 'samplepoints', [1, 2, 3, 4, 5]), [1, 1, 1, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmedian', [2, 2], 'samplepoints', [1, 2, 3, 4, 5]), [1, 1, 3, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmedian', 4.0001, 'samplepoints', [1, 2, 3, 4, 5]), [1, 1, 3, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmedian', 3, 'samplepoints', [1.5 2 3 4 5]), [1, 1, 1, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmedian', 3, 'samplepoints', [1 2 3 4 4.5]), [1, 1, NaN, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmedian', 3, 'samplepoints', [1.5 2 3 4 4.5]), [1, 1, 1, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmedian', [1.5, 1.5], 'samplepoints', [1.5 2 3 4 5]), [1, 1, 1, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmedian', [1.5, 1.5], 'samplepoints', [1 2 3 4 4.5]), [1, 1, 5, 5, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], 'movmedian', [1.5, 1.5], 'samplepoints', [1.5 2 3 4 4.5]), [1, 1, 3, 5, 5]) %!test %! x = reshape ([1:24], 3, 4, 2); %! x([1, 2, 5, 6, 8, 10, 13, 16, 18, 19, 20, 21, 22]) = NaN; %! y = x; %! y([2, 5, 8, 10, 13, 16, 18, 22]) = [3, 4, 8, 11, 14, 17, 17, 23]; %! assert_equal (fillmissing (x, 'movmedian', 3), y); %! assert_equal (fillmissing (x, 'movmedian', [1, 1]), y); %! assert_equal (fillmissing (x, 'movmedian', 3, 'endvalues', 'extrap'), y); %! assert_equal (fillmissing (x, 'movmedian', 3, 'samplepoints', [1, 2, 3]), y); %! y = x; %! y([1, 6, 8, 10, 18, 20, 21]) = [4, 6, 11, 7, 15, 20, 24]; %! assert_equal (fillmissing (x, 'movmedian', 3, 2), y); %! assert_equal (fillmissing (x, 'movmedian', [1, 1], 2), y); %! assert_equal (fillmissing (x, 'movmedian', 3, 2, 'endvalues', 'extrap'), y); %! assert_equal (fillmissing (x, 'movmedian', 3, 2, 'samplepoints', [1, 2, 3, 4]), y); %! y([1,18]) = NaN; %! y(6) = 9; %! assert_equal (fillmissing (x, 'movmedian', 3, 2, 'samplepoints', [0, 2, 3, 4]), y); %! y = x; %! y([1, 2, 5, 6, 10, 13, 16, 18, 19, 20, 21, 22]) = 99; %! y(8) = 8; %! assert_equal (fillmissing (x, 'movmedian', 3, 'endvalues', 99), y); %! y = x; %! y([1, 2, 5, 8, 10, 13, 16, 19, 22]) = 99; %! y([6, 18, 20, 21]) = [6, 15, 20, 24]; %! assert_equal (fillmissing (x, 'movmedian', 3, 2, 'endvalues', 99), y); ## Test movfcn %!assert_equal (fillmissing ([1, 2, 3], @(x,y,z) x+y+z, 2), [1, 2, 3]) %!error ... %! fillmissing ([1, 2, NaN], @(x,y,z) x+y+z, 1) %!assert_equal (fillmissing ([1, 2, 3], @(x,y,z) x+y+z, 2), [1, 2, 3]) %!assert_equal (fillmissing ([1, 2, 3], @(x,y,z) x+y+z, [1, 0]), [1, 2, 3]) %!assert_equal (fillmissing ([1, 2, 3]', @(x,y,z) x+y+z, 2), [1, 2, 3]') %!assert_equal (fillmissing ([1, 2, NaN], @(x,y,z) x+y+z, 2), [1, 2, 7]) %!assert_equal (fillmissing ([1, 2, NaN], @(x,y,z) x+y+z, [1, 0]), [1, 2, 7]) %!assert_equal (fillmissing ([1, 2, NaN], @(x,y,z) x+y+z, [1, 0]'), [1, 2, 7]) %!assert_equal (fillmissing ([NaN, 2, NaN], @(x,y,z) x+y+z, 2), [5, 2, 7]) %!error ... %! fillmissing ([NaN, 2, NaN], @(x,y,z) x+y+z, [1, 0]) %!error ... %! fillmissing ([NaN, 2, NaN], @(x,y,z) x+y+z, [0, 1]) %!error ... %! fillmissing ([NaN, 2, NaN], @(x,y,z) x+y+z, [0, 1.1]) %!assert_equal (fillmissing ([1, 2, NaN, NaN, 3, 4], @(x,y,z) x+y+z, 2), [1, 2, 7, 12, 3, 4]) %!error ... %! fillmissing ([1, 2, NaN, NaN, 3, 4], @(x,y,z) x+y+z, 0.5) %!function A = testfcn (x, y, z) %! if (isempty (y)) %! A = z; %! elseif (numel (y) == 1) %! A = repelem (x(1), numel (z)); %! else %! A = interp1 (y, x, z, 'linear', 'extrap'); %! endif %!endfunction %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], @testfcn, [3, 0]), [1, 1, 3, NaN, 5]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], @testfcn, 3, 1), [1, 2, 6; 4, 2, 6]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], @testfcn, 3, 2), [1, 2, 2; 4, 5, 6]) %!assert_equal (fillmissing ([1, 2, NaN; 4, NaN, 6], @testfcn, 3, 3), [1, 2, NaN; 4, NaN, 6]) ##known not-compatible. matlab bug ML2022a: [1, 2, 1; 4, 1, 6] %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], @testfcn, 99), [1, 2, 3, 4, 5]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], @testfcn, 99, 1), [1, NaN, 3, NaN, 5]) ##known not-compatible. matlab bug ML2022a: [1, 1, 3, 1, 5] %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5]', @testfcn, 99, 1), [1, 2, 3, 4, 5]') %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], @testfcn, 99, 2), [1, 2, 3, 4, 5]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5]', @testfcn, 99, 2), [1, NaN, 3, NaN, 5]') ##known not-compatible. matlab bug ML2022a: [1, 1, 3, 1, 5]' %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], @testfcn, 99, 3), [1, NaN, 3, NaN, 5]) ##known not-compatible. matlab bug ML2022a: [1, 1, 3, 1, 5] %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5]', @testfcn, 99, 3), [1, NaN, 3, NaN, 5]') ##known not-compatible. matlab bug ML2022a: [1, 1, 3, 1, 5]' %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], @testfcn, 3, 'samplepoints', [1, 2, 3, 4, 5]), [1, 2, 3, 4, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], @testfcn, [1, 1], 'samplepoints', [1, 2, 3, 4, 5]), [1, 2, 3, 4, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], @testfcn, [1.5, 1.5], 'samplepoints', [1, 2, 3, 4, 5]), [1, 2, 3, 4, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], @testfcn, 4, 'samplepoints', [1, 2, 3, 4, 5]), [1, 2, 3, 4, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], @testfcn, [2, 2], 'samplepoints', [1, 2, 3, 4, 5]), [1, 2, 3, 4, 5]) %!assert_equal (fillmissing ([1, NaN, NaN, NaN, 5], @testfcn, 3, 'samplepoints', [1, 2, 2.5, 3, 3.5]), [1, 2.6, 3.4, 4.2, 5], 10*eps) %!assert_equal (fillmissing ([NaN, NaN, 3, NaN, 5], @testfcn, 99, 1), [NaN, NaN, 3, NaN, 5]) ##known not-compatible. matlab bug ML2022a: [1, 1, 3, 1, 5] ## Known noncompatible. For move_fcn method, ML2021b (1) ignores windowsize ## for full missing column and processes it anyway, (2) doesn't consider it ## part of endvalues unlike all other methods, (3) ignores samplepoint values ## when calculating move_fcn results. should review against future versions. %!test %!function A = testfcn (x, y, z) %! if (isempty (y)) %! A = z; %! elseif (numel (y) == 1) %! A = repelem (x(1), numel (z)); %! else %! A = interp1 (y, x, z, 'linear', 'extrap'); %! endif %!endfunction %! x = reshape ([1:24], 3, 4, 2); %! x([1, 2, 5, 6, 8, 10, 13, 16, 18, 19, 20, 21, 22]) = NaN; %! y = x; %! y([1, 2, 5, 6, 8, 10, 13, 16, 18, 22]) = [3, 3, 4, 4, 8, 11, 14, 17, 17, 23]; %! assert_equal (fillmissing (x, @testfcn, 3), y); %! assert_equal (fillmissing (x, @testfcn, [1, 1]), y); %! assert_equal (fillmissing (x, @testfcn, 3, 'endvalues', 'extrap'), y); %! assert_equal (fillmissing (x, @testfcn, 3, 'samplepoints', [1, 2, 3]), y); %! y= x; %! y(isnan (x)) = 99; %! y(8) = 8; %! assert_equal (fillmissing (x, @testfcn, 3, 'endvalues', 99), y) %! y = x; %! y([1, 2, 5, 6, 8, 10, 18, 20, 21]) = [4, 11, 11, 6, 11, 7, 18, 20, 21]; %! assert_equal (fillmissing (x, @testfcn, 3, 2), y); %! assert_equal (fillmissing (x, @testfcn, [1, 1], 2), y); %! assert_equal (fillmissing (x, @testfcn, 3, 2, 'endvalues', 'extrap'), y); %! assert_equal (fillmissing (x, @testfcn, 3, 2, 'samplepoints', [1, 2, 3, 4]), y); %! y(1) = NaN; %! y([6, 18, 21]) = [9, 24, 24]; %! assert_equal (fillmissing (x, @testfcn, 3, 2, 'samplepoints', [0, 2, 3, 4]), y); %! y = x; %! y([1, 2, 5, 6, 10, 13, 16, 18, 19, 20, 21, 22]) = 99; %! y(8) = 8; %! assert_equal (fillmissing (x, @testfcn, 3, 'endvalues', 99), y); %! y([6, 18, 20, 21]) = [6, 18, 20, 21]; %! y(8) = 99; %! assert_equal (fillmissing (x, @testfcn, 3, 2, 'endvalues', 99), y); %! y([6, 18, 20, 21]) = 99; %! assert_equal (fillmissing (x, @testfcn, 3, 3, 'endvalues', 99), y); ## Test maxgap for mid and end points %!assert_equal (fillmissing ([1, 2, 3], 'constant', 0, 'maxgap', 1), [1, 2, 3]) %!assert_equal (fillmissing ([1, 2, 3], 'constant', 0, 'maxgap', 99), [1, 2, 3]) %!assert_equal (fillmissing ([1, NaN, 3], 'constant', 0, 'maxgap', 1), [1, NaN, 3]) %!assert_equal (fillmissing ([1, NaN, 3], 'constant', 0, 'maxgap', 1.999), [1, NaN, 3]) %!assert_equal (fillmissing ([1, NaN, 3], 'constant', 0, 'maxgap', 2), [1, 0, 3]) %!assert_equal (fillmissing ([1, NaN, NaN, 4], 'constant', 0, 'maxgap', 2), [1, NaN, NaN, 4]) %!assert_equal (fillmissing ([1, NaN, NaN, 4], 'constant', 0, 'maxgap', 3), [1, 0, 0, 4]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], 'constant', 0, 'maxgap', 2), [1, 0, 3, 0, 5]) %!assert_equal (fillmissing ([NaN, 2, NaN], 'constant', 0, 'maxgap', 0.999), [NaN, 2, NaN]) %!assert_equal (fillmissing ([NaN, 2, NaN], 'constant', 0, 'maxgap', 1), [0, 2, 0]) %!assert_equal (fillmissing ([NaN, 2, NaN, NaN], 'constant', 0, 'maxgap', 1), [0, 2, NaN, NaN]) %!assert_equal (fillmissing ([NaN, 2, NaN, NaN], 'constant', 0, 'maxgap', 2), [0, 2, 0, 0]) %!assert_equal (fillmissing ([NaN, NaN, NaN], 'constant', 0, 'maxgap', 1), [NaN, NaN, NaN]) %!assert_equal (fillmissing ([NaN, NaN, NaN], 'constant', 0, 'maxgap', 3), [NaN, NaN, NaN]) %!assert_equal (fillmissing ([NaN, NaN, NaN], 'constant', 0, 'maxgap', 999), [NaN, NaN, NaN]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], 'constant', 0, 'maxgap', 2, 'samplepoints', [0, 1, 2, 3, 5]), [1, 0, 3, NaN, 5]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5]', 'constant', 0, 'maxgap', 2, 'samplepoints', [0, 1, 2, 3, 5]), [1, 0, 3, NaN, 5]') %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], 'constant', 0, 'maxgap', 2, 'samplepoints', [0, 2, 3, 4, 5]), [1, NaN, 3, 0, 5]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5; 1, NaN, 3, NaN, 5], 'constant', 0, 2, 'maxgap', 2, 'samplepoints', [0, 2, 3, 4, 5]), [1, NaN, 3, 0, 5; 1, NaN, 3, 0, 5]) %!test %! x = cat (3, [1, 2, NaN; 4, NaN, NaN], [NaN, 2, 3; 4, 5, NaN]); %! assert_equal (fillmissing (x, 'constant', 0, 'maxgap', 0.1), x); %! y = x; %! y([4, 7, 12]) = 0; %! assert_equal (fillmissing (x, 'constant', 0, 'maxgap', 1), y); %! assert_equal (fillmissing (x, 'constant', 0, 1, 'maxgap', 1), y); %! y = x; %! y([5, 7, 12]) = 0; %! assert_equal (fillmissing (x, 'constant', 0, 2, 'maxgap', 1), y); %! y = x; %! y([4, 5, 7]) = 0; %! assert_equal (fillmissing (x, 'constant', 0, 3, 'maxgap', 1), y); ## 2nd output ## verify consistent with dim %!test %! x = cat (3, [1, 2, NaN; 4, NaN, NaN], [NaN, 2, 3; 4, 5, NaN]); %! [~, idx] = fillmissing (x, 'constant', 0, 'maxgap', 1); %! assert_equal (idx, logical (cat (3, [0, 0, 0; 0, 1, 0], [1, 0, 0; 0, 0, 1]))); %! [~, idx] = fillmissing (x, 'constant', 0, 1, 'maxgap', 1); %! assert_equal (idx, logical (cat (3, [0, 0, 0; 0, 1, 0], [1, 0, 0; 0, 0, 1]))); %! [~, idx] = fillmissing (x, 'constant', 0, 2, 'maxgap', 1); %! assert_equal (idx, logical (cat (3, [0, 0, 1; 0, 0, 0], [1, 0, 0; 0, 0, 1]))); %! [~, idx] = fillmissing (x, 'constant', 0, 3, 'maxgap', 1); %! assert_equal (idx, logical (cat (3, [0, 0, 1; 0, 1, 0], [1, 0, 0; 0, 0, 0]))); ## Verify idx matches when methods leave gaps unfilled, or when fill looks ## the same. %!test %! x = [NaN, 2, 3]; %! [~, idx] = fillmissing (x, 'previous'); %! assert_equal (idx, logical ([0, 0, 0])); %! [~, idx] = fillmissing (x, 'movmean', 1); %! assert_equal (idx, logical ([0, 0, 0])); %! x = [1:3; 4:6; 7:9]; %! x([2, 4, 7, 9]) = NaN; %! [~, idx] = fillmissing (x, 'linear'); %! assert_equal (idx, logical ([0, 1, 0; 1, 0, 0; 0, 0, 0])); %! [~, idx] = fillmissing (x, 'movmean', 2); %! assert_equal (idx, logical ([0, 0, 0; 1, 0, 0; 0, 0, 1])); %! [A, idx] = fillmissing ([1, 2, 3, NaN, NaN], 'movmean',2); %! assert_equal (A, [1, 2, 3, 3, NaN]); %! assert_equal (idx, logical ([0, 0, 0, 1, 0])); %! [A, idx] = fillmissing ([1, 2, 3, NaN, NaN], 'movmean',3); %! assert_equal (A, [1, 2, 3, 3, NaN]); %! assert_equal (idx, logical ([0, 0, 0, 1, 0])); %! [A, idx] = fillmissing ([1, 2, NaN, NaN, NaN], 'movmedian', 2); %! assert_equal (A, [1, 2, 2, NaN, NaN]); %! assert_equal (idx, logical ([0, 0, 1, 0, 0])); %! [A, idx] = fillmissing ([1, 2, 3, NaN, NaN], 'movmedian', 3); %! assert_equal (A, [1, 2, 3, 3, NaN]); %! assert_equal (idx, logical ([0, 0, 0, 1, 0])); %! [A, idx] = fillmissing ([1, NaN, 1, NaN, 1], @(x,y,z) z, 3); %! assert_equal (A, [1, 2, 1, 4, 1]); %! assert_equal (idx, logical ([0, 1, 0, 1, 0])); %! [A, idx] = fillmissing ([1, NaN, 1, NaN, 1], @(x,y,z) NaN (size (z)), 3); %! assert_equal (A, [1, NaN, 1, NaN, 1]); %! assert_equal (idx, logical ([0, 0, 0, 0, 0])); ## Test missinglocations %!assert_equal (fillmissing ([1, 2, 3], 'constant', 99, 'missinglocations', logical ([0, 0, 0])), [1, 2, 3]) %!assert_equal (fillmissing ([1, 2, 3], 'constant', 99, 'missinglocations', logical ([1, 1, 1])), [99, 99, 99]) %!assert_equal (fillmissing ([1, NaN, 2, 3, NaN], 'constant', 99, 'missinglocations', logical ([1, 0, 1, 0, 1])), [99, NaN, 99, 3, 99]) %!assert_equal (fillmissing ([1, NaN, 3, NaN, 5], 'constant', NaN, 'missinglocations', logical ([0, 1, 1, 1, 0])), [1, NaN, NaN, NaN, 5]) %!assert_equal (fillmissing (['foo '; ' bar'], 'constant', 'X', 'missinglocations', logical ([0, 0, 0, 0; 0, 0, 0, 0])), ['foo '; ' bar']) %!assert_equal (fillmissing (['foo '; ' bar'], 'constant', 'X', 'missinglocations', logical ([1, 0, 1, 0; 0, 1, 1, 0])), ['XoX '; ' XXr']) %!assert_equal (fillmissing ({'foo', '', 'bar'}, 'constant', 'X', 'missinglocations', logical ([0, 0, 0])), {'foo', '', 'bar'}) %!assert_equal (fillmissing ({'foo', '', 'bar'}, 'constant', 'X', 'missinglocations', logical ([1, 1, 0])), {'X', 'X', 'bar'}) %!test %! [~, idx] = fillmissing ([1, NaN, 3, NaN, 5], 'constant', NaN); %! assert_equal (idx, logical ([0, 0, 0, 0, 0])); %! [~, idx] = fillmissing ([1 NaN 3 NaN 5], 'constant', NaN, 'missinglocations', logical ([0, 1, 1, 1, 0])); %! assert_equal (idx, logical ([0, 1, 1, 1, 0])); %! [A, idx] = fillmissing ([1, 2, NaN, 1, NaN], 'movmean', 3.1, 'missinglocations', logical ([0, 0, 1, 1, 0])); %! assert_equal (A, [1, 2, 2, NaN, NaN]); %! assert_equal (idx, logical ([0, 0, 1, 0, 0])); %! [A, idx] = fillmissing ([1, 2, NaN, NaN, NaN], 'movmean', 2, 'missinglocations', logical ([0, 0, 1, 1, 0])); %! assert_equal (A, [1, 2, 2, NaN, NaN]); %! assert_equal (idx, logical ([0, 0, 1, 0, 0])); %! [A, idx] = fillmissing ([1, 2, NaN, 1, NaN], 'movmean', 3, 'missinglocations', logical ([0, 0, 1, 1, 0])); %! assert_equal (A, [1, 2, 2, NaN, NaN]); %! assert_equal (idx, logical ([0, 0, 1, 0, 0])); %! [A, idx] = fillmissing ([1, 2, NaN, NaN, NaN], 'movmean', 3, 'missinglocations', logical ([0, 0, 1, 1, 0])); %! assert_equal (A, [1, 2, 2, NaN, NaN]); %! assert_equal (idx, logical ([0, 0, 1, 0, 0])); %! [A, idx] = fillmissing ([1, 2, NaN, NaN, NaN], 'movmedian', 2, 'missinglocations', logical ([0, 0, 1, 1, 0])); %! assert_equal (A, [1, 2, 2, NaN, NaN]); %! assert_equal (idx, logical ([0, 0, 1, 0, 0])); %! [A, idx] = fillmissing ([1, 2, NaN, NaN, NaN], 'movmedian', 3, 'missinglocations', logical ([0, 0, 1, 1, 0])); %! assert_equal (A, [1, 2, 2, NaN, NaN]); %! assert_equal (idx, logical ([0, 0, 1, 0, 0])); %! [A, idx] = fillmissing ([1, 2, NaN, NaN, NaN], 'movmedian', 3.1, 'missinglocations', logical ([0, 0, 1, 1, 0])); %! assert_equal (A, [1, 2, 2, NaN, NaN]); %! assert_equal (idx, logical ([0, 0, 1, 0, 0])); %! [A, idx] = fillmissing ([1, NaN, 1, NaN, 1], @(x,y,z) ones (size (z)), 3, 'missinglocations', logical ([0, 1, 0, 1, 1])); %! assert_equal (A, [1, 1, 1, 1, 1]); %! assert_equal (idx, logical ([0, 1, 0, 1, 1])); %! [A, idx] = fillmissing ([1, NaN, 1, NaN, 1], @(x,y,z) NaN (size (z)), 3, 'missinglocations', logical ([0, 1, 0, 1, 1])); %! assert_equal (A, [1, NaN, 1, NaN, NaN]); %! assert_equal (idx, logical ([0, 0, 0, 0, 0])); %!test %! [A, idx] = fillmissing ([1, 2, 5], 'movmedian', 3, 'missinglocations', logical ([0, 1, 0])); %! assert_equal (A, [1, 3, 5]); %! assert_equal (idx, logical ([0, 1, 0])); ## Test char and cellstr %!assert_equal (fillmissing (' foo bar ', 'constant', 'X', 'missinglocations', logical ([1, 0, 0, 0, 1, 0, 0, 0, 1])), 'XfooXbarX') %!assert_equal (fillmissing ([' foo'; 'bar '], 'constant', 'X', 'missinglocations', logical ([1, 0, 0, 0; 0, 0, 0, 1])), ['Xfoo'; 'barX']) %!assert_equal (fillmissing ([' foo'; 'bar '], 'next', 'missinglocations', logical ([1, 0, 0, 0; 0, 0, 0, 1])), ['bfoo'; 'bar ']) %!assert_equal (fillmissing ([' foo'; 'bar '], 'next', 1, 'missinglocations', logical ([1, 0, 0, 0; 0, 0, 0, 1])), ['bfoo'; 'bar ']) %!assert_equal (fillmissing ([' foo'; 'bar '], 'previous', 'missinglocations', logical ([1, 0, 0, 0; 0, 0, 0, 1])), [' foo'; 'baro']) %!assert_equal (fillmissing ([' foo'; 'bar '], 'previous', 1, 'missinglocations', logical ([1, 0, 0, 0; 0, 0, 0, 1])), [' foo'; 'baro']) %!assert_equal (fillmissing ([' foo'; 'bar '], 'nearest', 'missinglocations', logical ([1, 0, 0, 0; 0, 0, 0, 1])), ['bfoo'; 'baro']) %!assert_equal (fillmissing ([' foo'; 'bar '], 'nearest', 1, 'missinglocations', logical ([1, 0, 0, 0; 0, 0, 0, 1])), ['bfoo'; 'baro']) %!assert_equal (fillmissing ([' foo'; 'bar '], 'next', 2, 'missinglocations', logical ([1, 0, 0, 0; 0, 0, 0, 1])), ['ffoo'; 'bar ']) %!assert_equal (fillmissing ([' foo'; 'bar '], 'previous', 2, 'missinglocations', logical ([1, 0, 0, 0; 0, 0, 0, 1])), [' foo'; 'barr']) %!assert_equal (fillmissing ([' foo'; 'bar '], 'nearest', 2, 'missinglocations', logical ([1, 0, 0, 0; 0, 0, 0, 1])), ['ffoo'; 'barr']) %!assert_equal (fillmissing ([' foo'; 'bar '], 'next', 3, 'missinglocations', logical ([1, 0, 0, 0; 0, 0, 0, 1])), [' foo'; 'bar ']) %!assert_equal (fillmissing ([' foo'; 'bar '], 'previous', 3, 'missinglocations', logical ([1, 0, 0, 0; 0, 0, 0, 1])), [' foo'; 'bar ']) %!assert_equal (fillmissing ([' foo'; 'bar '], 'nearest', 3, 'missinglocations', logical ([1, 0, 0, 0; 0, 0, 0, 1])), [' foo'; 'bar ']) %!assert_equal (fillmissing ({'foo', 'bar'}, 'constant', 'a'), {'foo', 'bar'}) %!assert_equal (fillmissing ({'foo', 'bar'}, 'constant', {'a'}), {'foo', 'bar'}) %!assert_equal (fillmissing ({'foo', '', 'bar'}, 'constant', 'a'), {'foo', 'a', 'bar'}) %!assert_equal (fillmissing ({'foo', '', 'bar'}, 'constant', {'a'}), {'foo', 'a', 'bar'}) %!assert_equal (fillmissing ({'foo', '', 'bar'}, 'previous'), {'foo', 'foo', 'bar'}) %!assert_equal (fillmissing ({'foo', '', 'bar'}, 'next'), {'foo', 'bar', 'bar'}) %!assert_equal (fillmissing ({'foo', '', 'bar'}, 'nearest'), {'foo', 'bar', 'bar'}) %!assert_equal (fillmissing ({'foo', '', 'bar'}, 'previous', 2), {'foo', 'foo', 'bar'}) %!assert_equal (fillmissing ({'foo', '', 'bar'}, 'next', 2), {'foo', 'bar', 'bar'}) %!assert_equal (fillmissing ({'foo', '', 'bar'}, 'nearest', 2), {'foo', 'bar', 'bar'}) %!assert_equal (fillmissing ({'foo', '', 'bar'}, 'previous', 1), {'foo', '', 'bar'}) %!assert_equal (fillmissing ({'foo', '', 'bar'}, 'previous', 1), {'foo', '', 'bar'}) %!assert_equal (fillmissing ({'foo', '', 'bar'}, 'next', 1), {'foo', '', 'bar'}) %!assert_equal (fillmissing ({'foo', '', 'bar'}, 'nearest', 1), {'foo', '', 'bar'}) %!assert_equal (fillmissing ('abc ', @(x,y,z) x+y+z, 2, 'missinglocations', logical ([0, 0, 0, 1])), 'abcj') %!assert_equal (fillmissing ({'foo', '', 'bar'}, @(x,y,z) x(1), 3), {'foo', 'foo', 'bar'}) %!test %! [A, idx] = fillmissing (' a b c', 'constant', ' ', 'missinglocations', logical ([1, 0, 1, 0, 1, 0])); %! assert_equal (A, ' a b c'); %! assert_equal (idx, logical ([1, 0, 1, 0, 1, 0])); %!test %! [A, idx] = fillmissing (' a b c', 'constant', ' '); %! assert_equal (A, ' a b c'); %! assert_equal (idx, logical ([0, 0, 0, 0, 0, 0])); %!test %! [A, idx] = fillmissing ({'foo', '', 'bar', ''}, 'constant', ''); %! assert_equal (A, {'foo', '', 'bar', ''}); %! assert_equal (idx, logical ([0, 0, 0, 0])); %!test %! [A, idx] = fillmissing ({'foo', '', 'bar', ''}, 'constant', {''}); %! assert_equal (A, {'foo', '', 'bar', ''}); %! assert_equal (idx, logical ([0, 0, 0, 0])); %!test %! [A, idx] = fillmissing (' f o o ', @(x,y,z) repelem ('a', numel (z)), 3, 'missinglocations', logical ([1, 0, 1, 0, 1, 0, 1])); %! assert_equal (A, 'afaoaoa'); %! assert_equal (idx, logical ([1, 0, 1, 0, 1, 0, 1])); %!test %! [A, idx] = fillmissing (' f o o ', @(x,y,z) repelem (' ', numel (z)), 3); %! assert_equal (A, ' f o o '); %! assert_equal (idx, logical ([0, 0, 0, 0, 0, 0, 0])); %!test %! [A, idx] = fillmissing ({'', 'foo', ''}, @(x,y,z) repelem ({'a'}, numel (z)), 3); %! assert_equal (A, {'a', 'foo', 'a'}); %! assert_equal (idx, logical ([1, 0, 1])); %!test %! [A, idx] = fillmissing ({'', 'foo', ''}, @(x,y,z) repelem ({''}, numel (z)), 3); %! assert_equal (A, {'', 'foo', ''}); %! assert_equal (idx, logical ([0, 0, 0])); ## Types without a defined 'missing' (currently logical, int) that can be filled %!assert_equal (fillmissing (logical ([1, 0, 1, 0, 1]), 'constant', true), logical ([1, 0, 1, 0, 1])) %!assert_equal (fillmissing (logical ([1, 0, 1, 0, 1]), 'constant', false, 'missinglocations', logical ([1, 0, 1, 0, 1])), logical ([0, 0, 0, 0, 0])) %!assert_equal (fillmissing (logical ([1, 0, 1, 0, 1]), 'previous', 'missinglocations', logical ([1, 0, 1, 0, 1])), logical ([1, 0, 0, 0, 0])) %!assert_equal (fillmissing (logical ([1, 0, 1, 0, 1]), 'next', 'missinglocations', logical ([1, 0, 1, 0, 1])), logical ([0, 0, 0, 0, 1])) %!assert_equal (fillmissing (logical ([1, 0, 1, 0, 1]), 'nearest', 'missinglocations', logical ([1, 0, 1, 0, 1])), logical ([0, 0, 0, 0, 0])) %!assert_equal (fillmissing (logical ([1, 0, 1, 0, 1]), @(x,y,z) false (size (z)), 3), logical ([1, 0, 1, 0, 1])) %!assert_equal (fillmissing (logical ([1, 0, 1, 0, 1]), @(x,y,z) false (size (z)), 3, 'missinglocations', logical ([1, 0, 1, 0, 1])), logical ([0, 0, 0, 0, 0])) %!assert_equal (fillmissing (logical ([1, 0, 1, 0, 1]), @(x,y,z) false (size (z)), [2, 0], 'missinglocations', logical ([1, 0, 1, 0, 1])), logical ([0, 0, 0, 0, 0])) %!test %! x = logical ([1, 0, 1, 0, 1]); %! [~, idx] = fillmissing (x, 'constant', true); %! assert_equal (idx, logical ([0, 0, 0, 0, 0])); %! [~, idx] = fillmissing (x, 'constant', false, 'missinglocations', logical ([1, 0, 1, 0, 1])); %! assert_equal (idx, logical ([1, 0, 1, 0, 1])); %! [~, idx] = fillmissing (x, 'constant', true, 'missinglocations', logical ([1, 0, 1, 0, 1])); %! assert_equal (idx, logical ([1, 0, 1, 0, 1])); %! [~, idx] = fillmissing (x, 'previous', 'missinglocations', logical ([1, 0, 1, 0, 1])); %! assert_equal (idx, logical ([0, 0, 1, 0, 1])); %! [~, idx] = fillmissing (x, 'next', 'missinglocations', logical ([1, 0, 1, 0, 1])); %! assert_equal (idx, logical ([1, 0, 1, 0, 0])); %! [~, idx] = fillmissing (x, 'nearest', 'missinglocations', logical ([1, 0, 1, 0, 1])); %! assert_equal (idx, logical ([1, 0, 1, 0, 1])); %! [~, idx] = fillmissing (x, @(x,y,z) false (size (z)), 3); %! assert_equal (idx, logical ([0, 0, 0, 0, 0])) %! [~, idx] = fillmissing (x, @(x,y,z) false (size (z)), 3, 'missinglocations', logical ([1, 0, 1, 0, 1])); %! assert_equal (idx, logical ([1, 0, 1, 0, 1])) %! [~, idx] = fillmissing (x, @(x,y,z) false (size (z)), [2 0], 'missinglocations', logical ([1, 0, 1, 0, 1])); %! assert_equal (idx, logical ([1, 0, 1, 0, 1])) %!assert_equal (fillmissing (int32 ([1, 2, 3, 4, 5]), 'constant', 0), int32 ([1, 2, 3, 4, 5])) %!assert_equal (fillmissing (int32 ([1, 2, 3, 4, 5]), 'constant', 0, 'missinglocations', logical ([1, 0, 1, 0, 1])), int32 ([0, 2, 0, 4, 0])) %!assert_equal (fillmissing (int32 ([1, 2, 3, 4, 5]), 'previous', 'missinglocations', logical ([1, 0, 1, 0, 1])), int32 ([1, 2, 2, 4, 4])) %!assert_equal (fillmissing (int32 ([1, 2, 3, 4, 5]), 'next', 'missinglocations', logical ([1, 0, 1, 0, 1])), int32 ([2, 2, 4, 4, 5])) %!assert_equal (fillmissing (int32 ([1, 2, 3, 4, 5]), 'nearest', 'missinglocations', logical ([1, 0, 1, 0, 1])), int32 ([2, 2, 4, 4, 4])) %!assert_equal (fillmissing (int32 ([1, 2, 3, 4, 5]), @(x,y,z) z+10, 3), int32 ([1, 2, 3, 4, 5])) %!assert_equal (fillmissing (int32 ([1, 2, 3, 4, 5]), @(x,y,z) z+10, 3, 'missinglocations', logical ([1, 0, 1, 0, 1])), int32 ([11, 2, 13, 4, 15])) %!assert_equal (fillmissing (int32 ([1, 2, 3, 4, 5]), @(x,y,z) z+10, [2, 0], 'missinglocations', logical ([1, 0, 1, 0, 1])), int32 ([11, 2, 13, 4, 15])) ## A one-sided window leaves the gap at that end of the data with nothing to ## fill from, and the fill function is called there with empty value and ## location arguments rather than being skipped. Values are R2024a's, ## measured 2026-08-17. %!assert_equal (fillmissing (int32 ([1, 2, 3, 4, 5]), @(x,y,z) z+10, [0, 2], 'missinglocations', logical ([1, 0, 1, 0, 1])), int32 ([11, 2, 13, 4, 15])) %!assert_equal (fillmissing ([1, 2, 3, 4, 5], @(x,y,z) numel (x), [2, 0], 'missinglocations', logical ([1, 0, 1, 0, 1])), [0, 2, 2, 4, 2]) %!assert_equal (fillmissing ([1, 2, 3, 4, 5], @(x,y,z) numel (x), [0, 2], 'missinglocations', logical ([1, 0, 1, 0, 1])), [2, 2, 2, 4, 0]) %!test %! x = int32 ([1, 2, 3, 4, 5]); %! [~, idx] = fillmissing (x, 'constant', 0); %! assert_equal (idx, logical ([0, 0, 0, 0, 0])); %! [~, idx] = fillmissing (x, 'constant', 0, 'missinglocations', logical ([1, 0, 1, 0, 1])); %! assert_equal (idx, logical ([1, 0, 1, 0, 1])); %! [~, idx] = fillmissing (x, 'constant', 3, 'missinglocations', logical ([0, 0, 1, 0, 0])); %! assert_equal (idx, logical ([0, 0, 1, 0, 0])); %! [~, idx] = fillmissing (x, 'previous', 'missinglocations', logical ([1, 0, 1, 0, 1])); %! assert_equal (idx, logical ([0, 0, 1, 0, 1])); %! [~, idx] = fillmissing (x, 'next', 'missinglocations', logical ([1, 0, 1, 0, 1])); %! assert_equal (idx, logical ([1, 0, 1, 0, 0])); %! [~, idx] = fillmissing (x, 'nearest', 'missinglocations', logical ([1, 0, 1, 0, 1])); %! assert_equal (idx, logical ([1, 0, 1, 0, 1])); %! [~, idx] = fillmissing (x, @(x,y,z) z+10, 3); %! assert_equal (idx, logical ([0, 0, 0, 0, 0])); %! [~, idx] = fillmissing (x, @(x,y,z) z+10, 3, 'missinglocations', logical ([1, 0, 1, 0, 1])); %! assert_equal (idx, logical ([1, 0, 1, 0, 1])); %! [~, idx] = fillmissing (x, @(x,y,z) z+10, [2 0], 'missinglocations', logical ([1, 0, 1, 0, 1])); %! assert_equal (idx, logical ([1, 0, 1, 0, 1])); ## Other data type passthrough %!test %! [A, idx] = fillmissing ([struct, struct], 'constant', 1); %! assert_equal (A, [struct, struct]) %! assert_equal (idx, [false, false]) ## Test input validation and error messages %!error fillmissing () %!error fillmissing (1) %!error fillmissing (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13) %!error fillmissing (1, 2) %!error fillmissing (1, 'foo') %!error fillmissing (1, @(x) x, 1) %!error fillmissing (1, @(x,y) x+y, 1) %!error fillmissing ('a b c', 'linear') %!error fillmissing ({'a', 'b'}, 'linear') %!error <'movmean' and 'movmedian' methods only valid for numeric> fillmissing ('a b c', 'movmean', 2) %!error <'movmean' and 'movmedian' methods only valid for numeric> fillmissing ({'a', 'b'}, 'movmean', 2) %!error <'constant' method must be followed by> fillmissing (1, 'constant') %!error fillmissing (1, 'constant', []) %!error fillmissing (1, 'constant', 'a') %!error fillmissing ('a', 'constant', 1) %!error fillmissing ('a', 'constant', {'foo'}) %!error fillmissing ({'foo'}, 'constant', 1) %!error fillmissing (1, 'movmean') %!error fillmissing (1, 'movmedian') %!error fillmissing (1, 'constant', 1, 0) %!error fillmissing (1, 'constant', 1, -1) %!error fillmissing (1, 'constant', 1, [1, 2]) %!error fillmissing (1, 'constant', 1, 'samplepoints') %!error fillmissing (1, 'constant', 1, 'foo') %!error fillmissing (1, 'constant', 1, 1, 'foo') %!error fillmissing (1, 'constant', 1, 2, {1}, 4) %!error fillmissing ([1, 2, 3], 'constant', 1, 2, 'samplepoints', [1, 2]) %!error fillmissing ([1, 2, 3], 'constant', 1, 2, 'samplepoints', [3, 1, 2]) %!error fillmissing ([1, 2, 3], 'constant', 1, 2, 'samplepoints', [1, 1, 2]) %!error fillmissing ([1, 2, 3], 'constant', 1, 2, 'samplepoints', 'abc') %!error fillmissing ([1, 2, 3], 'constant', 1, 2, 'samplepoints', logical ([1, 1, 1])) %!error fillmissing ([1, 2, 3], 'constant', 1, 1, 'samplepoints', [1, 2, 3]) %!error fillmissing ('foo', 'next', 'endvalues', 1) %!error fillmissing (1, 'constant', 1, 1, 'endvalues', 'foo') %!error fillmissing ([1, 2, 3], 'constant', 1, 2, 'endvalues', [1, 2, 3]) %!error fillmissing ([1, 2, 3], 'constant', 1, 1, 'endvalues', [1, 2]) %!error fillmissing (randi (5,4,3,2), 'constant', 1, 3, 'endvalues', [1, 2]) %!error fillmissing (1, 'constant', 1, 1, 'endvalues', {1}) %!error fillmissing (1, 'constant', 1, 2, 'foo', 4) %!error fillmissing (struct, 'constant', 1, 'missinglocations', false) %!error fillmissing (1, 'constant', 1, 2, 'maxgap', 1, 'missinglocations', false) %!error fillmissing (1, 'constant', 1, 2, 'missinglocations', false, 'maxgap', 1) %!error fillmissing (1, 'constant', 1, 'replacevalues', true) %!error fillmissing (1, 'constant', 1, 'datavariables', 'Varname') %!error fillmissing (1, 'constant', 1, 2, 'missinglocations', 1) %!error fillmissing (1, 'constant', 1, 2, 'missinglocations', 'a') %!error fillmissing (1, 'constant', 1, 2, 'missinglocations', [true, false]) %!error fillmissing (true, 'linear', 'missinglocations', true) %!error fillmissing (int8 (1), 'linear', 'missinglocations', true) %!error fillmissing (true, 'next', 'missinglocations', true, 'EndValues', 'linear') %!error fillmissing (true, 'next', 'EndValues', 'linear', 'missinglocations', true) %!error fillmissing (int8 (1), 'next', 'missinglocations', true, 'EndValues', 'linear') %!error fillmissing (int8 (1), 'next', 'EndValues', 'linear', 'missinglocations', true) %!error fillmissing (1, 'constant', 1, 2, 'maxgap', true) %!error fillmissing (1, 'constant', 1, 2, 'maxgap', 'a') %!error fillmissing (1, 'constant', 1, 2, 'maxgap', [1, 2]) %!error fillmissing (1, 'constant', 1, 2, 'maxgap', 0) %!error fillmissing (1, 'constant', 1, 2, 'maxgap', -1) %!error fillmissing ([1, 2, 3], 'constant', [1, 2, 3]) %!error fillmissing ([1, 2, 3]', 'constant', [1, 2, 3]) %!error fillmissing ([1, 2, 3]', 'constant', [1, 2, 3], 1) %!error fillmissing ([1, 2, 3], 'constant', [1, 2, 3], 2) %!error fillmissing (randi (5, 4, 3, 2), 'constant', [1, 2], 1) %!error fillmissing (randi (5, 4, 3, 2), 'constant', [1, 2], 2) %!error fillmissing (randi (5, 4, 3, 2), 'constant', [1, 2], 3) %!error fillmissing (1, @(x,y,z) x+y+z) %!error fillmissing ([1, NaN, 2], @(x,y,z) [1, 2], 2) statistics-release-2.1.2/inst/Data_Manipulation/grp2idx.m000066400000000000000000000505721527311104300234350ustar00rootroot00000000000000## Copyright (C) 2015 Carnë Draug ## Copyright (C) 2022-2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{g} =} grp2idx (@var{s}) ## @deftypefnx {statistics} {[@var{g}, @var{gn}] =} grp2idx (@var{s}) ## @deftypefnx {statistics} {[@var{g}, @var{gn}, @var{gl}] =} grp2idx (@var{s}) ## ## Get index for grouping variable. ## ## @code{@var{g} = grp2idx (@var{s})} returns a numeric column vector of integer ## values @var{g} indexing the distinct groups in the grouping variable @var{s}. ## @var{s} can specified as any of the following data types: ## ## @itemize ## @item categorical vector ## @item cell array of character vectors ## @item character array ## @item datetime vector ## @item duration vector ## @item logical vector ## @item numeric vector ## @end itemize ## ## @var{s} must be a vector, unless it is a 2-D character array. In the case of ## numerical, logical, datetime, and duration data types, the group indices are ## ordered in sorted order of @var{s}. In the case of categorical arrays, the ## group indices are allocated by the order of the categories in @var{s}. For ## the rest of the data types, the group indices are allocated by order of first ## appearance in @var{s}. Note that in case of a categorical grouping variable, ## the indexing integer values might not be continuous, since @var{s} may ## contain unassigned categories. For every other data type, @var{g} will ## contain integer values in the range @math{[1:K]}, where @math{K} is the ## number of distinct groups in @var{s}. ## ## @code{[@var{g}, @var{gn}] = grp2idx (@var{s})} also returns a cell array of ## character vectors @var{gn} representing the list of group names. The order ## of the group names in @var{gn} follow the same pattern as the group indices ## in @var{g} according to the data type of @var{s}, as described above. ## ## @code{[@var{g}, @var{gn}, @var{gl}] = grp2idx (@var{s})} further returns a ## column vector @var{gl} representing the list of the group levels with the ## same data type as @var{s}. ## ## Note that standard missing values in @var{s}, such as NaN or NaT, appear as ## NaN in @var{g} and are not present on either @var{gn} and @var{gl}. This ## includes a @qcode{NaN} duration, which MATLAB makes a group of its own. ## ## An empty @var{s} returns @var{g}, @var{gn} and @var{gl} as 0-by-1 arrays, ## whereas MATLAB returns a 0-by-0 @var{gn} for an empty categorical @var{s}. ## A character array without rows returns a 0-by-1 @var{g} and @var{gn}, and a ## @var{gl} with no rows. Each row of a character array is an observation, so a ## row without characters, being unnamed, is indexed as @code{NaN}, whereas ## MATLAB returns no index for any empty character array. ## ## @seealso{grpstats} ## @end deftypefn function [g, gn, gl] = grp2idx (s) if (nargin != 1) print_usage (); endif if (ndims (s) != 2) error ("grp2idx: S must be either a vector or a matrix."); endif is_categorical = false; is_char_array = false; is_datetime = false; is_duration = false; is_string = false; if (iscategorical (s)) if (! isvector (s)) error ("grp2idx: 'categorical' grouping variable must be a vector."); endif is_categorical = true; elseif (ischar (s)) is_char_array = true; ## A character array without rows holds no observations gl_cols = 0; if (rows (s) == 0) gl_cols = columns (s); s = cell (0,1); else s = cellstr (s); endif elseif (isdatetime (s)) if (! isvector (s)) error ("grp2idx: 'datetime' grouping variable must be a vector."); endif is_datetime = true; elseif (isduration (s)) if (! isvector (s)) error ("grp2idx: 'duration' grouping variable must be a vector."); endif is_duration = true; elseif (isstring (s)) if (! isvector (s)) error ("grp2idx: 'string' grouping variable must be a vector."); endif is_string = true; s = cellstr (s); elseif (isnumeric (s)) if (! isvector (s)) error ("grp2idx: 'numeric' grouping variable must be a vector."); endif elseif (islogical (s)) if (! isvector (s)) error ("grp2idx: 'logical' grouping variable must be a vector."); endif elseif (iscellstr (s)) if (! isvector (s)) error ("grp2idx: 'cell array' grouping variable must be a vector."); endif else error ("grp2idx: unsupported type for input S."); endif ## A categorical array is numbered by its category codes, over every ## category and in category order, whether a category is used or not. if (is_categorical) g = double (s(:)); cats = categories (s); if (isempty (cats)) gn = cell (0,1); gl = categorical (zeros (0,1)); else gn = cellstr (cats); gl = categorical (cats, cats, 'Ordinal', isordinal (s), ... 'Protected', isprotected (s)); endif return; endif [gl, I, g] = unique (s(:)); if (isempty (g)) g = zeros (0, 1); endif ## Fix order in here, since unique does not support this yet if (iscellstr (s) && ! isempty (s)) I = sort (I); for i = 1:length (gl) gl_s(i) = gl(g(I(i))); idx(i,:) = (g == g(I(i))); endfor for i = 1:length (gl) g(idx(i,:)) = i; endfor gl = gl_s; gl = gl'; endif ## handle NaNs and empty strings if (iscellstr (s)) empties = cellfun (@isempty, s); if (any (empties)) g(empties) = NaN; rm = find (cellfun (@isempty, gl)); to_decrement = ! isnan (g) & g > rm; g(to_decrement) -= 1; endif empties = cellfun (@isempty, gl); if (any (empties)) gl(empties) = []; endif elseif (is_datetime) ## NaT sorts last, like NaN, so removing it leaves the indices intact g(isnat (s)) = NaN; gl(isnat (gl)) = []; else ## This works fine because NaN come at the end after sorting, we don't ## have to worry about change on the indices. g(isnan (s)) = NaN; gl(isnan (gl)) = []; endif if (nargout > 1) if (is_duration || is_datetime) if (isempty (gl)) gn = cell (0,1); else gn = cellstr (gl); endif elseif (iscellstr (gl)) gn = gl; if (isempty (gn)) gn = cell (0,1); endif elseif (iscell (gl)) gn = cellfun (@num2str, gl, 'UniformOutput', false); if (isempty (gn)) gn = cell (0,1); endif else gn = arrayfun (@num2str, gl, 'UniformOutput', false); if (isempty (gn)) gn = cell (0,1); endif endif endif if (nargout > 2) if (is_char_array) if (isempty (gl)) gl = char (zeros (0, gl_cols)); else gl = char (gn); endif elseif (is_duration) if (isempty (gl)) gl = duration (NaN (0,3)); endif elseif (is_string) gl = string (gn); elseif (iscell (gl)) if (isempty (gl)) gl = cell (0,1); endif endif endif endfunction # test for one output argument %!test %! g = grp2idx ([3 2 1 2 3 1]); %! assert_equal (g, [3; 2; 1; 2; 3; 1]); # test for two output arguments %!test %! [g, gn] = grp2idx (['b'; 'a'; 'c'; 'a']); %! assert_equal (g, [1; 2; 3; 2]); %! assert_equal (gn, {'b'; 'a'; 'c'}); ## test boolean input and note that row or column vector makes no difference %!test %! in = [true, false, false, true]; %! out = {[2; 1; 1; 2] {'0'; '1'} [false; true]}; %! assert_equal (nthargout (1:3, @grp2idx, in), out) %! assert_equal (nthargout (1:3, @grp2idx, in), nthargout (1:3, @grp2idx, in')) ## test that boolean groups are ordered in order of appearance %!test %! assert_equal (nthargout (1:3, @grp2idx, [false, true]), %! {[1; 2] {'0'; '1'} [false; true]}); %! assert_equal (nthargout (1:3, @grp2idx, [true, false]), %! {[2; 1] {'0'; '1'} [false; true]}); ## test char matrix and cell array of strings %!assert_equal (nthargout (1:3, @grp2idx, ['oct'; 'sci'; 'oct'; 'oct'; 'sci']), %! {[1; 2; 1; 1; 2] {'oct'; 'sci'} ['oct'; 'sci']}) ## and cell array of strings %!assert_equal (nthargout (1:3, @grp2idx, {'oct'; 'sci'; 'oct'; 'oct'; 'sci'}), %! {[1; 2; 1; 1; 2] {'oct'; 'sci'} {'oct'; 'sci'}}) ## test numeric arrays %!assert_equal (nthargout (1:3, @grp2idx, [1, -3, -2, -3, -3, 2, 1, -1, 3, -3]), %! {[4; 1; 2; 1; 1; 5; 4; 3; 6; 1], {'-3'; '-2'; '-1'; '1'; '2'; '3'}, ... %! [-3; -2; -1; 1; 2; 3]}) %!test %! s = [1e6, 2e6, 1e6, 3e6]; %! [g, gn, gl] = grp2idx (s); %! assert_equal (g, [1; 2; 1; 3]); %! assert_equal (gn, {'1000000'; '2000000'; '3000000'}); %! assert_equal (gl, [1000000; 2000000; 3000000]); %!test %! s = [0.1, 0.2, 0.3, 0.1, 0.2]; %! [g, gn, gl] = grp2idx (s); %! assert_equal (g, [1; 2; 3; 1; 2]); %! assert_equal (gn, {'0.1'; '0.2'; '0.3'}); %! assert_equal (gl, [0.1; 0.2; 0.3]); %!test %! s = [-5 -10 0 5 10 -5]; %! [g, gn, gl] = grp2idx (s); %! assert_equal (g, [2; 1; 3; 4; 5; 2]); %! assert_equal (gn, {'-10'; '-5'; '0'; '5'; '10'}); %! assert_equal (gl, [-10; -5; 0; 5; 10]); ## test for NaN and empty strings %!assert_equal (nthargout (1:3, @grp2idx, [2, 2, 3, NaN, 2, 3]), ... %! {[1; 1; 2; NaN; 1; 2] {'2'; '3'} [2; 3]}) %!assert_equal (nthargout (1:3, @grp2idx, {'et', 'sa', 'sa', '', 'et'}), ... %! {[1; 2; 2; NaN; 1] {'et'; 'sa'} {'et'; 'sa'}}) %!assert_equal (nthargout (1:3, @grp2idx, [2, 2, 3, NaN, 2, 4]), ... %! {[1; 1; 2; NaN; 1; 3] {'2'; '3'; '4'} [2; 3; 4]}) %!test %! s = [NaN, NaN, NaN]; %! [g, gn, gl] = grp2idx (s); %! assert_equal (g, [NaN; NaN; NaN]); %! assert_equal (gn, cell (0,1)); %! assert_equal (gl, zeros (0,1)); %!test %! s = single ([NaN, NaN, NaN]); %! [g, gn, gl] = grp2idx (s); %! assert_equal (g, [NaN; NaN; NaN]); %! assert_equal (gn, cell (0,1)); %! assert_equal (gl, single (zeros (0,1))); %!test %! s = {''; ''; ''; ''}; %! [g, gn, gl] = grp2idx (s); %! assert_equal (g, [NaN; NaN; NaN; NaN]); %! assert_equal (gn, cell (0,1)); %! assert_equal (gl, cell (0,1)); %!test %! s = {'', '', '', ''}; %! [g, gn, gl] = grp2idx (s); %! assert_equal (g, [NaN; NaN; NaN; NaN]); %! assert_equal (gn, cell (0,1)); %! assert_equal (gl, cell (0,1)); %!test %! s = {'a'; ''; 'b'; ''; 'c'}; %! [g, gn, gl] = grp2idx (s); %! assert_equal (g, [1; NaN; 2; NaN; 3]); %! assert_equal (gn, {'a'; 'b'; 'c'}); %! assert_equal (gl, {'a'; 'b'; 'c'}); %!test %! s = categorical ({''; ''; ''; ''}); %! [g, gn, gl] = grp2idx (s); %! assert_equal (g, [NaN; NaN; NaN; NaN]); %! assert_equal (gn, cell (0,1)); %! assert_equal (isequaln (gl, categorical (zeros (0,1))), true); %! assert_equal (size (gl), [0, 1]); %!test %! s = string ({missing, missing, missing}); %! [g, gn, gl] = grp2idx (s); %! assert_equal (g, [NaN; NaN; NaN]); %! assert_equal (gn, cell (0,1)); %! assert_equal (isequal (gl, string (cell (0,1))), true); %!test %! s = [duration(NaN, 0, 0), duration(NaN, 0, 0), duration(NaN, 0, 0)]; %! [g, gn, gl] = grp2idx (s); %! assert_equal (g, [NaN; NaN; NaN]); %! assert_equal (gn, cell (0,1)); %! assert_equal (isequal (gl, duration (NaN (0,3))), true); %!test %! [g, gn, gl] = grp2idx (duration (NaN (3, 1), 0, 0)); %! assert_equal (g, [NaN; NaN; NaN]); %! assert_equal (gn, cell (0,1)); %! assert_equal (isequal (gl, duration (NaN (0,3))), true); ## Test that order when handling strings is by order of appearance %!test assert_equal (nthargout (1:3, @grp2idx, ['sci'; 'oct'; 'sci'; 'oct'; 'oct']), %! {[1; 2; 1; 2; 2] {'sci'; 'oct'} ['sci'; 'oct']}); %!test assert_equal (nthargout (1:3, @grp2idx, {'sci'; 'oct'; 'sci'; 'oct'; 'oct'}), %! {[1; 2; 1; 2; 2] {'sci'; 'oct'} {'sci'; 'oct'}}); %!test assert_equal (nthargout (1:3, @grp2idx, {'sa' 'et' 'et' '' 'sa'}), %! {[1; 2; 2; NaN; 1] {'sa'; 'et'} {'sa'; 'et'}}) ## test for categorical arrays %!test %! [g, gn, gl] = grp2idx (categorical ({'low', 'med', 'high', 'low'})); %! assert_equal (g, [2; 3; 1; 2]); %! assert_equal (gn, {'high'; 'low'; 'med'}); %! assert_equal (isequal (gl, categorical ({'high'; 'low'; 'med'})), true); %!test %! [g, gn, gl] = grp2idx (categorical ([10, 20, 10, 30, 20])); %! assert_equal (g, [1; 2; 1; 3; 2]); %! assert_equal (gn, {'10'; '20'; '30'}); %! assert_equal (isequal (gl, categorical ([10; 20; 30])), true); %!test %! cats = categorical ({'high', '', 'low', ''}); %! [g, gn, gl] = grp2idx (cats); %! assert_equal (g, [2; 1; 3; 1]); %! assert_equal (gn, {''; 'high'; 'low'}); %! assert_equal (isequal (gl, categorical ({''; 'high'; 'low'})), true); %!test %! s = categorical ({''; ''; ''; ''}, {'1', '2', '3'}, {'1', '2', '3'}); %! [g, gn, gl] = grp2idx (s); %! assert_equal (g, nan (4, 1)); %! assert_equal (gn, {'1'; '2'; '3'}); %! assert_equal (iscategorical (gl), true); %! assert_equal (cellstr (gl), gn); %!test %! s = categorical ({''; '1'; ''; '2'}, {'1', '2', '3'}, {'1', '2', '3'}); %! [g, gn, gl] = grp2idx (s); %! assert_equal (g, [NaN; 1; NaN; 2]); %! assert_equal (gn, {'1'; '2'; '3'}); %! assert_equal (iscategorical (gl), true); %! assert_equal (cellstr (gl), gn); %!test %! s = categorical ({''; '2'; ''; '1'}, {'1', '2', '3'}, {'1', '2', '3'}); %! [g, gn, gl] = grp2idx (s); %! assert_equal (g, [NaN; 2; NaN; 1]); %! assert_equal (gn, {'1'; '2'; '3'}); %! assert_equal (iscategorical (gl), true); %! assert_equal (cellstr (gl), gn); ## test for duration arrays %!test # MATLAB parity: an unused category keeps its code %! s = categorical ({'b'; 'c'; 'b'}, {'a', 'b', 'c'}); %! [g, gn, gl] = grp2idx (s); %! assert_equal (g, [2; 3; 2]); %! assert_equal (gn, {'a'; 'b'; 'c'}); %! assert_equal (cellstr (gl), gn); %!test # MATLAB parity: an undefined element is NaN between defined codes %! s = categorical ({'b'; ''; 'c'}, {'a', 'b', 'c'}); %! assert_equal (grp2idx (s), [2; NaN; 3]); %!test # MATLAB parity: the categories keep their own order and ordinality %! s = categorical ({'c'; 'b'}, {'c', 'b', 'a'}, 'Ordinal', true); %! [g, gn, gl] = grp2idx (s); %! assert_equal (g, [1; 2]); %! assert_equal (gn, {'c'; 'b'; 'a'}); %! assert_equal (categories (gl), {'c'; 'b'; 'a'}); %! assert_equal (isordinal (gl), true); %!test %! g = gn = gl = []; %! [g, gn, gl] = grp2idx (seconds ([1.234, 1.234, 2.5, 3.000])); %! assert_equal (g, [1; 1; 2; 3]); %! assert_equal (gn, {'1.234 sec'; '2.5 sec'; '3 sec'}); %! assert_equal (isequal (gl, seconds ([1.234; 2.5; 3.000])), true); %!test %! [g, gn, gl] = grp2idx ([hours(1); hours(2); hours(1); hours(3)]); %! assert_equal (g, [1; 2; 1; 3]); %! assert_equal (gn, {'1 hr'; '2 hr'; '3 hr'}); %! assert_equal (isequal (gl, [hours(1); hours(2); hours(3)]), true); %!test %! in = [duration(1, 30, 0); duration(0, 45, 30); duration(1, 30, 0); duration(2, 15, 15)]; %! [g, gn, gl] = grp2idx (in); %! assert_equal (g, [2; 1; 2; 3]); %! assert_equal (gn, {'00:45:30'; '01:30:00'; '02:15:15'}); %! assert_equal (isequal (gl, [duration(0, 45, 30); duration(1, 30, 0); duration(2, 15, 15)]), true); ## Inconsistency Note: following test is inconsistent with MATLAB due to a ## probable bug in their implementation, where they include multiple NaNs ## in the output group labels for duration array inputs. %!test %! in = [hours(1); NaN; minutes(30); hours(1); NaN; seconds(90)]; %! [g, gn, gl] = grp2idx (in); %! assert_equal (g, [3; NaN; 2; 3; NaN; 1]); %! assert_equal (gn, {'0.025 hr'; '0.5 hr'; '1 hr'}); %! assert_equal (isequal (gl, [seconds(90); minutes(30); hours(1)]), true); ## test for string arrays %!test %! [g, gn, gl] = grp2idx (string ({'123', 'erw', missing, '', '234'})); %! assert_equal (g, [1; 2; NaN; NaN; 3]); %! assert_equal (gn, {'123'; 'erw'; '234'}); %! assert_equal (isequal (gl, string ({'123'; 'erw'; '234'})), true); %!test %! [g, gn, gl] = grp2idx (string ({'medium', 'low', 'high', 'medium', 'medium'})); %! assert_equal (g, [1; 2; 3; 1; 1]); %! assert_equal (gn, {'medium'; 'low'; 'high'}); %! assert_equal (isequal (gl, string ({'medium'; 'low'; 'high'})), true); %!test %! [g, gn, gl] = grp2idx (string ({'', 'high', 'low', ''})); %! assert_equal (g, [NaN; 1; 2; NaN]); %! assert_equal (gn, {'high'; 'low'}); %! assert_equal (isequal (gl, string ({'high'; 'low'})), true); %!test %! [g, gn, gl] = grp2idx (string ({'a', 'a', 'b', 'c'})); %! assert_equal (g, [1; 1; 2; 3]); %! assert_equal (gn, {'a'; 'b'; 'c'}); %! assert_equal (isstring (gl), true); %! assert_equal (cellstr (gl), gn); %!test %! [g, gn, gl] = grp2idx (zeros (0, 1)); %! assert_equal (size (g), [0, 1]); %! assert_equal (size (gn), [0, 1]); %! assert_equal (size (gl), [0, 1]); %!test %! [g, gn, gl] = grp2idx (cell (0, 1)); %! assert_equal (g, zeros (0, 1)); %! assert_equal (gn, cell (0, 1)); %! assert_equal (gl, cell (0, 1)); %!test %! [g, gn, gl] = grp2idx (string (cell (0, 1))); %! assert_equal (g, zeros (0, 1)); %! assert_equal (gn, cell (0, 1)); %! assert_equal (isstring (gl), true); %! assert_equal (size (gl), [0, 1]); %!test %! [g, gn, gl] = grp2idx (categorical (zeros (0, 1))); %! assert_equal (g, zeros (0, 1)); %! assert_equal (gn, cell (0, 1)); %! assert_equal (iscategorical (gl), true); %! assert_equal (size (gl), [0, 1]); %!test %! [g, gn, gl] = grp2idx (char (zeros (0, 3))); %! assert_equal (g, zeros (0, 1)); %! assert_equal (gn, cell (0, 1)); %! assert_equal (gl, char (zeros (0, 3))); %!test %! [g, gn, gl] = grp2idx (''); %! assert_equal (g, zeros (0, 1)); %! assert_equal (gn, cell (0, 1)); %! assert_equal (gl, ''); %!test %! [g, gn, gl] = grp2idx (char (zeros (3, 0))); %! assert_equal (g, [NaN; NaN; NaN]); %! assert_equal (gn, cell (0, 1)); %! assert_equal (gl, ''); ## Test datetime grouping variables against MATLAB R2026b %!shared d %! d = [datetime(2026, 10, 13); datetime(2026, 1, 5); ... %! datetime(2026, 10, 13); NaT; datetime(2025, 3, 1)]; %!test %! [g, gn, gl] = grp2idx (d); %! assert_equal (g, [3; 2; 3; NaN; 1]); %! assert_equal (gn, {'01-Mar-2025'; '05-Jan-2026'; '13-Oct-2026'}); %! assert_equal (isdatetime (gl), true); %! assert_equal (cellstr (gl), gn); %!test %! d2 = d; %! d2.Format = 'yyyy-MM-dd'; %! [g, gn, gl] = grp2idx (d2); %! assert_equal (g, [3; 2; 3; NaN; 1]); %! assert_equal (gn, {'2025-03-01'; '2026-01-05'; '2026-10-13'}); %! assert_equal (gl.Format, 'yyyy-MM-dd'); %!test %! t = [datetime(2026, 10, 13, 5, 0, 0); datetime(2026, 10, 13, 6, 0, 0); ... %! datetime(2026, 10, 13, 5, 0, 0)]; %! [g, gn] = grp2idx (t); %! assert_equal (g, [1; 2; 1]); %! assert_equal (gn, {'13-Oct-2026 05:00:00'; '13-Oct-2026 06:00:00'}); %!test %! [g, gn, gl] = grp2idx (d.'); %! assert_equal (g, [3; 2; 3; NaN; 1]); %! assert_equal (size (gn), [3, 1]); %! assert_equal (size (gl), [3, 1]); %!test %! [g, gn, gl] = grp2idx (NaT (3, 1)); %! assert_equal (g, [NaN; NaN; NaN]); %! assert_equal (gn, cell (0, 1)); %! assert_equal (isdatetime (gl), true); %! assert_equal (size (gl), [0, 1]); %!test %! [g, gn, gl] = grp2idx (NaT (0, 1)); %! assert_equal (g, zeros (0, 1)); %! assert_equal (gn, cell (0, 1)); %! assert_equal (isdatetime (gl), true); %! assert_equal (size (gl), [0, 1]); %!test %! z = datetime (2026, 10, 13, 5, 0, 0, 'TimeZone', 'Asia/Kolkata'); %! [g, gn, gl] = grp2idx ([z; z]); %! assert_equal (g, [1; 1]); %! assert_equal (gn, {'13-Oct-2026 05:00:00'}); %! assert_equal (gl.TimeZone, z.TimeZone); ## Test input validation %!error grp2idx (ones (3, 3, 3)) %!error ... %! grp2idx (categorical ([1, 2; 1, 3])) %!error ... %! grp2idx (repmat (datetime (2026, 1, 1), 2, 2)) %!error ... %! grp2idx (days ([1, 2; 1, 3])) %!error ... %! grp2idx (string ({'a', 'a'; 'b', 'c'})) %!error grp2idx ({1}) %!error grp2idx ([10, 20; 10, 30]) %!error grp2idx ([true, false; false, true]) %!error grp2idx ({'a', 'b'; 'c', 'd'}) statistics-release-2.1.2/inst/Data_Manipulation/ismissing.m000066400000000000000000000272311527311104300240570ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{TF} =} ismissing (@var{A}) ## @deftypefnx {statistics} {@var{TF} =} ismissing (@var{A}, @var{indicator}) ## ## Find missing data in arrays. ## ## @code{@var{TF} = ismissing (@var{A})} returns a logical array, @var{TF}, with ## the same dimensions as @var{A}, where @code{true} values match the standard ## missing values in the input data according to their data type. ## ## Standard missing values and their corresponding data types are: ## ## @itemize ## @item @qcode{NaN} - for @qcode{double}, @qcode{single}, @qcode{duration}, and ## @qcode{calendarDuration} arrays. ## @item @qcode{NaT} - for @qcode{datetime} arrays. ## @item @qcode{} - for @qcode{string} arrays. ## @item @qcode{} - for @qcode{categorical} arrays. ## @item @qcode{@{0x0 char@}} - for @qcode{cell} arrays of character vectors. ## @end itemize ## ## For any data types that do not support missing values, @code{ismissing} ## returns @code{@var{TF} = false (size (@var{A}))}. ## ## Note: the generic @code{ismissing} function from the statistics package only ## operates on core Octave datatypes and it explicitly identifies missing values ## in @qcode{double} and @qcode{single} arrays, as well as in @qcode{cell} ## arrays of character vectors. All other data types are handled by the ## overloaded methods from their respective data class from the datatypes ## package. Use @code{help class_name.ismissing} to find more information about ## the functional specialization of their respective class implementation. ## ## The optional input @var{indicator} can be a scalar or a vector, of the same ## type as the input data @var{A}, specifying alternative missing values in the ## input data. When specifying @var{indicator} values, the standard missing ## values are ignored, unless explicitly stated in the @var{indicator}. ## ## Additional data type matches between @var{indicator} and @var{A} are: ## ## @itemize ## @item @qcode{double} indicators also match @qcode{single}, all integer types, ## and @qcode{logical} data in @var{A}. ## ## @item @qcode{string} and @qcode{char} indicators also match ## @qcode{categorical} data in @var{A}. ## ## @item @qcode{char} and @qcode{cellstr} indicators also match @qcode{string} ## data in @var{A}. ## ## @item @qcode{single}, integer, and @qcode{logical} indicators also match ## @qcode{double} data in @var{A}. ## @end itemize ## ## Any other pairing of numeric and @qcode{logical} classes between ## @var{indicator} and @var{A} is an error, such as an @qcode{int32} indicator ## with @qcode{single} data in @var{A}. ## ## Note: the generic @code{ismissing} function from the statistics package only ## accepts @var{indicator} argument for numeric, @qcode{logical}, and ## @qcode{char} arrays, as well as for @qcode{cell} arrays of character vectors. ## For all other core Octave data types, @code{ismissing} produces an error. ## However, @var{indicator} is supported for data classes from the datatypes ## package through their respective class implementation of overloaded methods. ## ## @seealso{fillmissing, rmmissing, standardizeMissing} ## @end deftypefn function TF = ismissing (A, indicator) if (nargin < 1) || (nargin > 2) print_usage (); endif ## Check INDICATOR if (nargin != 2) indicator = []; endif ## If A is a cell array of character vectors and INDICATOR is a character ## vector, convert it to a single-element cell array of character vectors if (iscellstr (A) && ischar (indicator) && ! iscellstr (indicator)) indicator = {indicator}; endif ## A numeric or logical INDICATOR applies to A of its own class, and a ## double on either side applies to every numeric and logical class if (! isempty (indicator)) if (isnumeric (A) || islogical (A)) if (! ((isnumeric (indicator) || islogical (indicator)) && (isa (A, 'double') || isa (indicator, 'double') || strcmp (class (A), class (indicator))))) error ("ismissing: 'indicator' must be of the class of 'A' or double."); endif elseif ((iscellstr (A) && ! iscellstr (indicator)) || (ischar (A) && ! ischar (indicator))) error ("ismissing: 'indicator' and 'A' must have the same data type."); endif endif ## main logic if (isempty (indicator)) if (isnumeric (A)) ## Numeric matrix: just find the NaNs ## integer types have no missing value, but isnan will return false TF = isnan (A); elseif (iscellstr (A)) ## Cell array of character vectors - find cells with empty char {''} TF = cellfun ('isempty', A); else ## No standard missing type defined, return false. ## Other datatypes with standard missing values are ## handled by their respective overloading methods. TF = false (size (A)); endif else ## INDICATOR specified for missing data TF = false (size (A)); if (isnumeric (A)) for iter = 1:numel (indicator) if (isnan (indicator(iter))) TF(isnan (A)) = true; else TF(A == indicator(iter)) = true; endif endfor elseif (islogical (A) || ischar (A)) for iter = 1:numel (indicator) TF(A == indicator(iter)) = true; endfor elseif (iscellstr (A)) for iter = 1:numel (indicator) if (isempty (indicator{iter})) TF(cellfun ('isempty', A)) = true; else TF(strcmp (A, indicator(iter))) = true; endif endfor else error ("ismissing: indicators not supported for data type '%s'", ... class (A)); endif endif endfunction %!assert_equal (ismissing ([1, NaN, 3]), [false, true, false]) %!assert_equal (ismissing ('abcd f'), [false, false, false, false, false, false]) %!assert_equal (ismissing ({'xxx', '', 'xyz'}), [false, true, false]) %!assert_equal (ismissing ({'x', '', 'y'}), [false, true, false]) %!assert_equal (ismissing ({'x', '', 'y'; 'z', 'a', ''}), logical ([0, 1, 0; 0, 0, 1])) %!assert_equal (ismissing ([1, 2; NaN, 2]), [false, false; true, false]) %!assert_equal (ismissing ([1, 2; NaN, 2], 2), [false, true; false, true]) %!assert_equal (ismissing ([1, 2; NaN, 2], [1, 2]), [true, true; false, true]) %!assert_equal (ismissing ([1, 2; NaN, 2], NaN), [false, false; true, false]) ## test nD array data %!assert_equal (ismissing (cat (3, magic (2), magic (2))), logical (zeros (2, 2, 2))) %!assert_equal (ismissing (cat (3, magic (2), [1, 2; 3, NaN])), ... %! logical (cat (3, [0, 0; 0, 0], [0, 0; 0, 1]))) %!assert_equal (ismissing ([1, 2; 3, 4], [5, 1; 2, 0]), logical ([1, 1; 0, 0])) %!assert_equal (ismissing (cat (3, 'f oo', 'ba r')), ... %! logical (cat (3, [0, 0, 0, 0], [0, 0, 0, 0]))) %!assert_equal (ismissing (cat (3, {'foo'}, {''}, {'bar'})), logical (cat (3, 0, 1, 0))) ## test data type handling %!assert_equal (ismissing (double (NaN)), true) %!assert_equal (ismissing (single (NaN)), true) %!assert_equal (ismissing (' '), false) %!assert_equal (ismissing ({''}), true) %!assert_equal (ismissing ({' '}), false) %!assert_equal (ismissing (double (eye (3)), single (1)), logical (eye (3))) %!assert_equal (ismissing (double (eye (3)), int32 (1)), logical (eye (3))) %!assert_equal (ismissing (single (eye (3)), double (1)), logical (eye (3))) %!assert_equal (ismissing (single (eye (3)), single (1)), logical (eye (3))) ## test data types without missing values %!assert_equal (ismissing ({'123', '', 123}), [false, false, false]) %!assert_equal (ismissing (logical ([1, 0, 1])), [false, false, false]) %!assert_equal (ismissing (int32 ([1, 2, 3])), [false, false, false]) %!assert_equal (ismissing (uint32 ([1, 2, 3])), [false, false, false]) %!assert_equal (ismissing ({1, 2, 3}), [false, false, false]) %!assert_equal (ismissing ([struct struct struct]), [false, false, false]) %!assert_equal (ismissing (logical (eye (3)), true), logical (eye (3))) %!assert_equal (ismissing (logical (eye (3)), double (1)), logical (eye (3))) %!assert_equal (ismissing (int32 (eye (3)), int32 (1)), logical (eye (3))) %!assert_equal (ismissing (int32 (eye (3)), double (1)), logical (eye (3))) ## test empty input handling %!assert_equal (ismissing ([]), logical ([])) %!assert_equal (ismissing (''), logical ([])) %!assert_equal (ismissing (ones (0,1)), logical (ones (0,1))) %!assert_equal (ismissing (ones (1,0)), logical (ones (1,0))) %!assert_equal (ismissing (ones (1,2,0)), logical (ones (1,2,0))) ## test indicators and standard missing values %!assert_equal (ismissing ([1, NaN, 0, 2]), [false, true, false, false]) %!assert_equal (ismissing ([1, NaN, 0, 2], [0, 1]), [true, false, true, false]) %!assert_equal (ismissing ([1, NaN, 0, 2], [0, NaN]), [false, true, true, false]) %!assert_equal (ismissing ([true, false, true]), [false, false, false]) %!assert_equal (ismissing ([true, false, true], 1), [true, false, true]) %!assert_equal (ismissing ([true, false, true], 0), [false, true, false]) %!assert_equal (ismissing ([1, 0, 2, 1], [true, false]), ... %! logical ([1, 1, 0, 1])) %!assert_equal (ismissing ([NaN, 1], true), [false, true]) %!assert_equal (ismissing ({'', 'a', 'f'}), [true, false, false]) %!assert_equal (ismissing ({'', 'a', 'f'}, 'a'), [false, true, false]) %!assert_equal (ismissing ({'', 'a', 'f'}, {'a', 'g'}), [false, true, false]) %!assert_equal (ismissing ({'', 'a', 'f'}, {'a', 'f'}), [false, true, true]) ## Test input validation %!error ismissing () %!error ismissing (1, 2, 3) %!error ... %! ismissing ([1, 2; 3, 4], 'abc') %!error ... %! ismissing (1, struct) %!error ... %! ismissing (single ([1, 0, 2]), int32 (1)) %!error ... %! ismissing (single ([1, 0, 2]), true) %!error ... %! ismissing (int32 ([1, 0, 2]), single (1)) %!error ... %! ismissing (int32 ([1, 0, 2]), int8 (1)) %!error ... %! ismissing (uint8 ([1, 0, 2]), false) %!error ... %! ismissing ([true, false, true], single (1)) %!error ... %! ismissing ([true, false, true], int8 (1)) %!error ... %! ismissing ([true, false, true], 'a') %!error ... %! ismissing ({'', '', ''}, 1) %!error ... %! ismissing ('abc', true) %!error ... %! ismissing (struct, 1) %!error ... %! ismissing ({1, 2, 3}, 2) statistics-release-2.1.2/inst/Data_Manipulation/isoutlier.m000066400000000000000000001240761527311104300240760ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{TF} =} isoutlier (@var{x}) ## @deftypefnx {statistics} {@var{TF} =} isoutlier (@var{x}, @var{method}) ## @deftypefnx {statistics} {@var{TF} =} isoutlier (@var{x}, @qcode{'percentiles'}, @var{threshold}) ## @deftypefnx {statistics} {@var{TF} =} isoutlier (@var{x}, @var{movmethod}, @var{window}) ## @deftypefnx {statistics} {@var{TF} =} isoutlier (@dots{}, @var{dim}) ## @deftypefnx {statistics} {@var{TF} =} isoutlier (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{TF}, @var{L}, @var{U}, @var{C}] =} isoutlier (@dots{}) ## ## Find outliers in data ## ## @code{isoutlier (@var{x})} returns a logical array whose elements are true ## when an outlier is detected in the corresponding element of @var{x}. ## @code{isoutlier} treats NaNs as missing values and removes them. ## ## @itemize ## @item ## If @var{x} is a matrix, then @code{isoutlier} operates on each column of ## @var{x} separately. ## @item ## If @var{x} is a multidimensional array, then @code{isoutlier} operates along ## the first dimension of @var{x} whose size does not equal 1. ## @end itemize ## ## By default, an outlier is a value that is more than three scaled median ## absolute deviations (MAD) from the median. The scaled median is defined as ## @code{c*median(abs(A-median(A)))}, where @code{c=-1/(sqrt(2)*erfcinv(3/2))}. ## ## @code{isoutlier (@var{x}, @var{method})} specifies a method for detecting ## outliers. The following methods are available: ## ## @multitable @columnfractions 0.13 0.8 ## @headitem Method @tab Description ## @item @qcode{'median'} @tab Outliers are defined as elements more than ## three scaled MAD from the median. ## @item @qcode{'mean'} @tab Outliers are defined as elements more than ## three standard deviations from the mean. ## @item @qcode{'quartiles'} @tab Outliers are defined as elements more ## than 1.5 interquartile ranges above the upper quartile (75 percent) or below ## the lower quartile (25 percent). This method is useful when the data in ## @var{x} is not normally distributed. ## @item @qcode{'grubbs'} @tab Outliers are detected using Grubbs’ test for ## outliers, which removes one outlier per iteration based on hypothesis ## testing. This method assumes that the data in @var{x} is normally ## distributed. ## @item @qcode{'gesd'} @tab Outliers are detected using the generalized ## extreme Studentized deviate test for outliers. This iterative method is ## similar to @qcode{'grubbs'}, but can perform better when there are multiple ## outliers masking each other. ## @end multitable ## ## @code{isoutlier (@var{x}, @qcode{'percentiles'}, @var{threshold})} detects ## outliers based on a percentile thresholds, specified as a two-element row ## vector whose elements are in the interval @math{[0, 100]}. The first element ## indicates the lower percentile threshold, and the second element indicates ## the upper percentile threshold. The first element of threshold must be less ## than the second element. ## ## @code{isoutlier (@var{x}, @var{movmethod}, @var{window})} specifies a moving ## method for detecting outliers. The following methods are available: ## ## @multitable @columnfractions 0.13 0.8 ## @headitem Method @tab Description ## @item @qcode{'movmedian'} @tab Outliers are defined as elements more ## than three local scaled MAD from the local median over a window length ## specified by @var{window}. ## @item @qcode{'movmean'} @tab Outliers are defined as elements more than ## three local standard deviations from the from the local mean over a window ## length specified by @var{window}. ## @end multitable ## ## @var{window} must be a positive integer scalar or a two-element vector of ## positive integers. When @var{window} is a scalar, if it is an odd number, ## the window is centered about the current element and contains ## @qcode{@var{window} - 1} neighboring elements. If even, then the window is ## centered about the current and previous elements. When @var{window} is a ## two-element vector of positive integers @math{[nb, na]}, the window contains ## the current element, @math{nb} elements before the current element, and ## @math{na} elements after the current element. When @qcode{'SamplePoints'} ## are also specified, @var{window} can take any real positive values (either as ## a scalar or a two-element vector) and in this case, the windows are computed ## relative to the sample points. ## ## @var{dim} specifies the operating dimension and it must be a positive integer ## scalar. If not specified, then, by default, @code{isoutlier} operates along ## the first non-singleton dimension of @var{x}. ## ## The following optional parameters can be specified as @var{Name}/@var{Value} ## paired arguments. ## ## @itemize ## @item @qcode{'SamplePoints'} can be specified as a vector of sample points ## with equal length as the operating dimension. The sample points represent ## the x-axis location of the data and must be sorted and contain unique ## elements. Sample points do not need to be uniformly sampled. By default, ## the vector is @qcode{[1, 2, 3, @dots{}, @var{n}]}, where ## @qcode{@var{n} = size (@var{x}, @var{dim})}. You can use unequally spaced ## @qcode{'SamplePoints'} to define a variable-length window for one of the ## moving methods available. ## ## @item @qcode{'ThresholdFactor'} can be specified as a nonnegative scalar. ## For methods @qcode{'median'} and @qcode{'movmedian'}, the detection threshold ## factor replaces the number of scaled MAD, which is 3 by default. For methods ## @qcode{'mean'} and @qcode{'movmean'}, the detection threshold factor replaces ## the number of standard deviations, which is 3 by default. For methods ## @qcode{'grubbs'} and @qcode{'gesd'}, the detection threshold factor ranges ## from 0 to 1, specifying the critical @math{alpha}-value of the respective ## test, and it is 0.05 by default. For the @qcode{'quartiles'} method, the ## detection threshold factor replaces the number of interquartile ranges, which ## is 1.5 by default. A factor of 0 for @qcode{'grubbs'} or @qcode{'gesd'} ## detects no outliers and returns @qcode{NaN} for @var{L} and @var{U}, as ## MATLAB does. With @qcode{'grubbs'}, a positive factor as small as ## @qcode{1e-300} still gives finite thresholds, where MATLAB returns ## @qcode{NaN}. ## ## @item @qcode{'MaxNumOutliers'} is only relevant to the @qcode{'gesd'} method ## and it must be a positive integer scalar specifying the maximum number of ## outliers returned by the @qcode{'gesd'} method. By default, it is the ## integer nearest to the 10% of the number of elements along the operating ## dimension in @var{x}. The @qcode{'gesd'} method assumes the nonoutlier input ## data is sampled from an approximate normal distribution. When the data is ## not sampled in this way, the number of returned outliers might exceed the ## @qcode{MaxNumOutliers} value. ## @end itemize ## ## @code{[@var{TF}, @var{L}, @var{U}, @var{C}] = isoutlier (@dots{})} returns ## up to 4 output arguments as described below. ## ## @itemize ## @item @var{TF} is the outlier indicator with the same size a @var{x}. ## ## @item @var{L} is the lower threshold used by the outlier detection method. ## If @var{method} is used for outlier detection, then @var{L} has the same size ## as @var{x} in all dimensions except for the operating dimension where the ## length is 1. If @var{movmethod} is used, then @var{L} has the same size as ## @var{x}. ## ## @item @var{U} is the upper threshold used by the outlier detection method. ## If @var{method} is used for outlier detection, then @var{U} has the same size ## as @var{x} in all dimensions except for the operating dimension where the ## length is 1. If @var{movmethod} is used, then @var{U} has the same size as ## @var{x}. ## ## @item @var{C} is the center value used by the outlier detection method. ## If @var{method} is used for outlier detection, then @var{C} has the same size ## as @var{x} in all dimensions except for the operating dimension where the ## length is 1. If @var{movmethod} is used, then @var{C} has the same size as ## @var{x}. For @qcode{'median'}, @qcode{'movmedian'}, @qcode{'mean'}, and ## @qcode{'movmean'} methods, @var{C} is computed by taking into account the ## outlier values. For @qcode{'grubbs'} and @qcode{'gesd'} methods, @var{C} is ## computed by excluding the outliers. For the @qcode{'percentiles'} method, ## @var{C} is the average between @var{U} and @var{L} thresholds. For the ## @qcode{'quartiles'} method, @var{C} is the average of the 25th and 75th ## percentiles. ## @end itemize ## ## @seealso{ismissing} ## @end deftypefn function [TF, L, U, C] = isoutlier (x, varargin) ## Check for valid input data if (nargin < 1) print_usage; endif ## Handle case if X is a scalar if (isscalar (x)) TF = false; L = x; U = x; C = x; return endif ## Add defaults dim = []; method = 'median'; window = []; SamplePoints = []; ThresholdFactor = 3; MaxNumOutliers = []; ## MATLAB's constant for scaled Median Absolute Deviation ## c = -1 / (sqrt (2) * erfcinv (3/2)) c = 1.482602218505602; ## Parse extra arguments while (numel (varargin) > 0) if (ischar (varargin{1})) switch (lower (varargin{1})) case 'median' method = 'median'; ThresholdFactor = 3; varargin(1) = []; case 'mean' method = 'mean'; ThresholdFactor = 3; varargin(1) = []; case 'quartiles' method = 'quartiles'; ThresholdFactor = 1.5; varargin(1) = []; case 'grubbs' method = 'grubbs'; ThresholdFactor = 0.05; varargin(1) = []; case 'gesd' method = 'gesd'; ThresholdFactor = 0.05; MaxNumOutliers = []; varargin(1) = []; case 'movmedian' method = 'movmedian'; if (numel (varargin) < 2) error ("isoutlier: specify a window length after 'movmedian'."); endif window = varargin{2}; if (! isnumeric (window) || numel (window) < 1 || numel (window) > 2 || any (window <= 0)) error (strcat ("isoutlier: WINDOW must be a positive scalar", ... " or a two-element vector of positive values.")); endif varargin([1:2]) = []; case 'movmean' method = 'movmean'; if (numel (varargin) < 2) error ("isoutlier: specify a window length after 'movmean'."); endif window = varargin{2}; if (! isnumeric (window) || numel (window) < 1 || numel (window) > 2 || any (window <= 0)) error (strcat ("isoutlier: WINDOW must be a positive scalar", ... " or a two-element vector of positive values.")); endif varargin([1:2]) = []; case 'percentiles' method = 'percentiles'; if (numel (varargin) < 2) error ("isoutlier: specify a threshold pair after 'percentiles'."); endif threshold = varargin{2}; if (! isnumeric (threshold) || ! (numel (threshold) == 2)) error (strcat ("isoutlier: THRESHOLD must be a two-element", ... " vector whose elements are in the interval", ... " [0, 100].")); endif if (! (threshold(1) < threshold(2)) || threshold(1) < 0 || threshold(2) > 100) error (strcat ("isoutlier: THRESHOLD must be a two-element", ... " vector whose elements are in the interval", ... " [0, 100].")); endif varargin([1:2]) = []; case 'samplepoints' if (numel (varargin) < 2) error ("isoutlier: 'SamplePoints' must be followed by a value."); endif SamplePoints = varargin{2}; if (! isvector (SamplePoints) || isscalar (SamplePoints)) error ("isoutlier: sample points must be a vector."); endif if (numel (unique (SamplePoints)) != numel (SamplePoints)) error ("isoutlier: sample points must be unique."); endif if (any (sort (SamplePoints) != SamplePoints)) error ("isoutlier: sample points must be sorted."); endif varargin([1:2]) = []; case 'thresholdfactor' if (numel (varargin) < 2) error ("isoutlier: 'ThresholdFactor' must be followed by a value."); endif ThresholdFactor = varargin{2}; if (! (isnumeric (ThresholdFactor) && isreal (ThresholdFactor) && isscalar (ThresholdFactor) && ThresholdFactor >= 0)) error ("isoutlier: threshold factor must be a nonnegative scalar."); endif ThresholdFactor = double (ThresholdFactor); varargin([1:2]) = []; case 'maxnumoutliers' if (numel (varargin) < 2) error ("isoutlier: 'MaxNumOutliers' must be followed by a value."); endif MaxNumOutliers = varargin{2}; if (! isscalar (MaxNumOutliers) || MaxNumOutliers <= 0 || ! (fix (MaxNumOutliers) == MaxNumOutliers)) error (strcat ("isoutlier: maximum outlier count must be a", ... " positive integer scalar.")); endif varargin([1:2]) = []; otherwise error ("isoutlier: invalid input argument."); endswitch elseif (isnumeric (varargin{1})) dim = varargin{1}; if (! fix (dim) == dim || dim < 1 || ! isscalar (dim) || ! isscalar (varargin{1})) error ("isoutlier: DIM must be a positive integer scalar."); endif varargin(1) = []; else error ("isoutlier: invalid input argument."); endif endwhile ## Find 1st operating dimension (if empty) if (isempty (dim)) szx = size (x); (dim = find (szx != 1, 1)) || (dim = 1); endif ## Check for valid WINDOW unless Sample Points are given if (isempty (SamplePoints) && ! isempty (window)) if (! all (fix (window) == window)) error (strcat ("isoutlier: WINDOW must be a positive integer", ... " scalar or a two-element vector of positive", ... " integers, unless SamplePoints are defined.")); endif endif ## Check for valid value of ThresholdFactor for 'grubbs' and 'geds' methods if (any (strcmpi (method, {'grubbs', 'gesd'})) && ThresholdFactor > 1) error (strcat ("isoutlier: threshold factor must be in [0 1]", ... " range for 'grubbs' and 'gesd' methods.")); endif ## Switch methods switch method case 'median' [L, U, C] = median_method (x, dim, ThresholdFactor, c); TF = x < L | x > U; case 'mean' [L, U, C] = mean_method (x, dim, ThresholdFactor); TF = x < L | x > U; case 'quartiles' [L, U, C] = quartiles_method (x, dim, ThresholdFactor); TF = x < L | x > U; case 'grubbs' [TF, L, U, C] = grubbs_method (x, dim, ThresholdFactor); case 'gesd' [L, U, C] = gesd_method (x, dim, ThresholdFactor, MaxNumOutliers); TF = x < L | x > U; case 'movmedian' sp = SamplePoints; [L, U, C] = movmedian_method (x, dim, ThresholdFactor, c, window, sp); TF = x < L | x > U; case 'movmean' sp = SamplePoints; [L, U, C] = movmean_method (x, dim, ThresholdFactor, window, sp); TF = x < L | x > U; case 'percentiles' [L, U, C] = percentiles_method (x, dim, threshold); TF = x < L | x > U; endswitch endfunction ## Find lower and upper outlier thresholds with median method function [L, U, C] = median_method (x, dim, ThresholdFactor, c) C = median (x, dim, 'omitnan'); sMAD = c * mad (x, 1, dim); L = C - ThresholdFactor * sMAD; U = C + ThresholdFactor * sMAD; endfunction ## Find lower and upper outlier thresholds with mean method function [L, U, M] = mean_method (x, dim, ThresholdFactor) M = mean (x, dim, 'omitnan'); S = std (x, [], dim, 'omitnan'); L = M - ThresholdFactor * S; U = M + ThresholdFactor * S; endfunction ## Find lower and upper outlier thresholds with quartiles method function [L, U, C] = quartiles_method (x, dim, ThresholdFactor) Q1 = quantile (x, 0.25, dim); Q3 = quantile (x, 0.75, dim); C = (Q1 + Q3) / 2; L = Q1 - (Q3 - Q1) * ThresholdFactor; U = Q3 + (Q3 - Q1) * ThresholdFactor; endfunction ## Find lower and upper outlier thresholds with grubbs method function [TF, L, U, C] = grubbs_method (x, dim, ThresholdFactor) ## Move the desired dim to be the 1st dimension (rows) szx = size (x); # size of dimensions N = szx(dim); # elements in operating dimension nd = length (szx); # number of dimensions dperm = [dim, 1:(dim-1), (dim+1):nd]; # permutation of dimensions x = permute (x, dperm); # permute dims to first dimension ncols = prod (szx(dperm(2:end))); # rest of dimensions as single column x = reshape (x, N, ncols); # reshape input ## Create return matrices L = zeros ([1, szx(dperm(2:end))]); U = L; C = L; TF = false (size (x)); ## Apply processing to each column for i = 1:ncols tmp_x = x(:,i); TFvec = [(i-1)*size(x,1)+1:i*size(x,1)]; TFvec(isnan (tmp_x)) = []; tmp_x(isnan (tmp_x)) = []; ## Search for outliers (one at a time) while (true) ## Get descriptive statistics n = length (tmp_x); C(i) = mean (tmp_x); S = std (tmp_x); ## Locate maximum deviation from mean dif_x = abs (tmp_x - C(i)); max_x = max (dif_x); loc_x = find (dif_x == max_x, 1); ## Calculate Grubbs's critical value t_crit = tinv (ThresholdFactor / (2 * n), n - 2); G_crit = ((n - 1) / sqrt (n)) * abs (t_crit) / sqrt (n - 2 + t_crit ^ 2); ## Check hypothesis if (max_x / S > G_crit) tmp_x(loc_x) = []; TF(TFvec(loc_x)) = true; TFvec(loc_x) = []; else break; endif endwhile L(i) = C(i) - S * G_crit; U(i) = C(i) + S * G_crit; endfor ## Restore shape TF = ipermute (TF, dperm); L = ipermute (L, dperm); U = ipermute (U, dperm); C = ipermute (C, dperm); endfunction ## Find lower and upper outlier thresholds with gesd method function [L, U, C] = gesd_method (x, dim, ThresholdFactor, MaxNumOutliers) ## Add default value in MaxNumOutliers (if empty) szx = size (x); N = szx(dim); if (isempty (MaxNumOutliers)) MaxNumOutliers = ceil (N * 0.1); endif ## Move the desired dim to be the 1st dimension (rows) nd = length (szx); # number of dimensions dperm = [dim, 1:(dim-1), (dim+1):nd]; # permutation of dimensions x = permute (x, dperm); # permute dims to first dimension ncols = prod (szx(dperm(2:end))); # rest of dimensions as single column x = reshape (x, N, ncols); # reshape input ## Create return matrices L = zeros ([1, szx(dperm(2:end))]); U = L; C = L; TF = false (size (x)); ## Apply processing to each column for i = 1:ncols tmp_x = x(:,i); vec_x = [(i-1)*size(x,1)+1:i*size(x,1)]; vec_x(isnan (tmp_x)) = []; tmp_x(isnan (tmp_x)) = []; n = length (tmp_x); if (n > 1) mean_x = zeros (MaxNumOutliers,1); S = zeros (MaxNumOutliers,1); lambda = zeros (MaxNumOutliers,1); R = zeros (MaxNumOutliers,1); Ridx = zeros (MaxNumOutliers,1); ## Search for given outliers for j = 1:MaxNumOutliers ## Get descriptive statistics mean_x(j) = mean (tmp_x); S(j) = std (tmp_x); ## Locate maximum deviation from mean dif_x = abs (tmp_x - mean_x(j)); max_x = max (dif_x); loc_x = find (dif_x == max_x, 1); ## Calculate R R(j) = max_x / S(j); tmp_x(loc_x) = []; Ridx(j) = vec_x(loc_x); vec_x(loc_x) = []; ## Calculate lambda pp = 1 - ThresholdFactor / (2 * (n - j + 1)); t = tinv (pp, n - j - 1); lambda(j) = (n - j) * t / sqrt ((n - j - 1 + t .^ 2) * (n - j + 1)); endfor ## Find largest index idx = find (R > lambda, 1, 'last'); if (isempty (idx)) TFidx = 1; else TFidx = min (idx + 1, MaxNumOutliers); endif L(i) = mean_x(TFidx) - S(TFidx) * lambda(TFidx); U(i) = mean_x(TFidx) + S(TFidx) * lambda(TFidx); C(i) = mean_x(TFidx); endif endfor ## Restore shape L = ipermute (L, dperm); U = ipermute (U, dperm); C = ipermute (C, dperm); endfunction ## Find lower and upper outlier thresholds with movmedian method function [L, U, C] = movmedian_method (x, dim, ThresholdFactor, c, window, sp); szx = size (x); N = szx(dim); ## Constrain window to the element in the operating dimension if (numel (window) == 1 && window > N) window = N; elseif (numel (window) == 2 && sum (window) > N) window = N; endif if (isempty (sp)) FCN = @(x) median (x, 'omitnan'); C = movfun (FCN, x, window, 'dim', dim); FCN = @(x) mad (x, 1); MAD = movfun (FCN, x, window, 'dim', dim); else ## Check that sample points(sp) have the N elements if (numel (sp) != N) error (strcat ("isoutlier: sample points must have the same size", ... " as the operating dimension.")); endif ## Move the desired dim to be the 1st dimension (rows) nd = length (szx); # number of dimensions dperm = [dim, 1:(dim-1), (dim+1):nd]; # permutation of dimensions x = permute (x, dperm); # permute dims to first dimension ncols = prod (szx(dperm(2:end))); # rest of dimensions as single column x = reshape (x, N, ncols); # reshape input ## Find beg+end from window if (numel (window) == 2) w_lo = window(1); w_hi = window(2); else if (mod (window, 2) == 1) w_lo = w_hi = (window - 1) / 2; else w_lo = window / 2; w_hi = w_lo - 1; endif endif ## Create return matrices C = zeros (size (x)); MAD = C; for i = 1:ncols tmp_x = x(:,i); for j = 1:N cp = sp - sp(j); nb = length (cp(cp < 0 & cp >= -w_lo)); na = length (cp(cp > 0 & cp <= w_hi)); sp_ind = [j-nb:j+na]; C(j,i) = median (tmp_x(sp_ind), 'omitnan'); MAD(j,i) = mad (tmp_x(sp_ind), 1); endfor endfor ## Restore shape C = ipermute (C, dperm); MAD = ipermute (MAD, dperm); endif ## Compute scaled MAD sMAD = c * MAD; L = C - ThresholdFactor * sMAD; U = C + ThresholdFactor * sMAD; endfunction ## Find lower and upper outlier thresholds with movmean method function [L, U, M] = movmean_method (x, dim, ThresholdFactor, window, sp); ## Constrain window to the element in the operating dimension szx = size (x); N = szx(dim); if (numel (window) == 1 && window > N) window = N; elseif (numel (window) == 2 && sum (window) > N) window = N; endif if (isempty (sp)) FCN = @(x) mean (x, 'omitnan'); M = movfun (FCN, x, window, 'dim', dim); FCN = @(x) std (x, [], 'omitnan'); S = movfun (FCN, x, window, 'dim', dim); else ## Check that sample points(sp) have the N elements if (numel (sp) != N) error (strcat ("isoutlier: sample points must have the same size", ... " as the operating dimension.")); endif ## Move the desired dim to be the 1st dimension (rows) nd = length (szx); # number of dimensions dperm = [dim, 1:(dim-1), (dim+1):nd]; # permutation of dimensions x = permute (x, dperm); # permute dims to first dimension ncols = prod (szx(dperm(2:end))); # rest of dimensions as single column x = reshape (x, N, ncols); # reshape input ## Find beg+end from window if (numel (window) == 2) w_lo = window(1); w_hi = window(2); else if (mod (window, 2) == 1) w_lo = w_hi = (window - 1) / 2; else w_lo = window / 2; w_hi = w_lo - 1; endif endif ## Create return matrices M = zeros (size (x)); S = M; for i = 1:ncols tmp_x = x(:,i); for j = 1:N cp = sp - sp(j); nb = length (cp(cp < 0 & cp >= -w_lo)); na = length (cp(cp > 0 & cp <= w_hi)); sp_ind = [j-nb:j+na]; M(j,i) = mean (tmp_x(sp_ind), 'omitnan'); S(j,i) = std (tmp_x(sp_ind), [], 'omitnan'); endfor endfor ## Restore shape M = ipermute (M, dperm); S = ipermute (S, dperm); endif L = M - ThresholdFactor * S; U = M + ThresholdFactor * S; endfunction ## Find lower and upper outlier thresholds with percentiles method function [L, U, C] = percentiles_method (x, dim, threshold) ## Each threshold is asked for on its own. Taking both from one call and ## indexing the result linearly only works for a vector: on a matrix it ## takes two numbers out of the first column and applies them to all of it. L = quantile (x, threshold(1)/100, dim); U = quantile (x, threshold(2)/100, dim); C = (L + U) / 2; endfunction %!demo %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! TF = isoutlier (A, 'mean') %!demo %! ## Use a moving detection method to detect local outliers in a sine wave %! %! x = -2*pi:0.1:2*pi; %! A = sin (x); %! A(47) = 0; %! time = datenum (2023,1,1,0,0,0) + (1/24)*[0:length(x)-1] - 730485; %! TF = isoutlier (A, 'movmedian', 5*(1/24), 'SamplePoints', time); %! plot (time, A) %! hold on %! plot (time(TF), A(TF), 'x') %! datetick ('x', 20, 'keepticks') %! legend ('Original Data', 'Outlier Data') %!demo %! ## Locate an outlier in a vector of data and visualize the outlier %! %! x = 1:10; %! A = [60 59 49 49 58 100 61 57 48 58]; %! [TF, L, U, C] = isoutlier (A); %! plot (x, A); %! hold on %! plot (x(TF), A(TF), 'x'); %! xlim ([1,10]); %! line ([1,10], [L, L], 'Linestyle', ':'); %! text (1.1, L-2, 'Lower Threshold'); %! line ([1,10], [U, U], 'Linestyle', ':'); %! text (1.1, U-2, 'Upper Threshold'); %! line ([1,10], [C, C], 'Linestyle', ':'); %! text (1.1, C-3, 'Center Value'); %! legend ('Original Data', 'Outlier Data'); ## Output validation tests (checked against MATLAB) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! assert_equal (isoutlier (A, 'mean'), logical ([zeros(1,8) 1 zeros(1,6)])) %! assert_equal (isoutlier (A, 'median'), ... %! logical ([zeros(1,3) 1 zeros(1,4) 1 zeros(1,6)])) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'mean'); %! assert_equal (L, -109.2459044922864, 1e-12) %! assert_equal (U, 264.9792378256198, 1e-12) %! assert_equal (C, 77.8666666666666, 1e-12) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'median'); %! assert_equal (L, 50.104386688966386, 1e-12) %! assert_equal (U, 67.895613311033610, 1e-12) %! assert_equal (C, 59) %!test %! A = magic (5) + diag (200*ones (1,5)); %! T = logical (eye (5)); %! assert_equal (isoutlier (A, 2), T) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'movmedian', 5); %! l = [54.5522, 52.8283, 54.5522, 54.5522, 54.5522, 53.5522, 53.5522, ... %! 53.5522, 47.6566, 56.5522, 57.5522, 56.5522, 51.1044, 52.3283, 53.5522]; %! u = [63.4478, 66.1717, 63.4478, 63.4478, 63.4478, 62.4478, 62.4478, ... %! 62.4478, 74.3434, 65.4478, 66.4478, 65.4478, 68.8956, 65.6717, 62.4478]; %! c = [59, 59.5, 59, 59, 59, 58, 58, 58, 61, 61, 62, 61, 60, 59, 58]; %! assert_equal (L, l, 1e-4) %! assert_equal (U, u, 1e-4) %! assert_equal (C, c) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'movmedian', 5, 'SamplePoints', [1:15]); %! l = [54.5522, 52.8283, 54.5522, 54.5522, 54.5522, 53.5522, 53.5522, ... %! 53.5522, 47.6566, 56.5522, 57.5522, 56.5522, 51.1044, 52.3283, 53.5522]; %! u = [63.4478, 66.1717, 63.4478, 63.4478, 63.4478, 62.4478, 62.4478, ... %! 62.4478, 74.3434, 65.4478, 66.4478, 65.4478, 68.8956, 65.6717, 62.4478]; %! c = [59, 59.5, 59, 59, 59, 58, 58, 58, 61, 61, 62, 61, 60, 59, 58]; %! assert_equal (L, l, 1e-4) %! assert_equal (U, u, 1e-4) %! assert_equal (C, c) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'movmean', 5); %! l = [54.0841, 6.8872, 11.5608, 12.1518, 11.0210, 10.0112, -218.2840, ... %! -217.2375, -215.1239, -213.4890, -211.3264, 55.5800, 52.9589, ... %! 52.5979, 51.0627]; %! u = [63.2492, 131.1128, 122.4392, 122.2482, 122.5790, 122.7888, 431.0840, ... %! 430.8375, 430.3239, 429.8890, 429.3264, 65.6200, 66.6411, 65.9021, ... %! 66.9373]; %! c = [58.6667, 69, 67, 67.2, 66.8, 66.4, 106.4, 106.8, 107.6, 108.2, 109, ... %! 60.6, 59.8, 59.25, 59]; %! assert_equal (L, l, 1e-4) %! assert_equal (U, u, 1e-4) %! assert_equal (C, c, 1e-4) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'movmean', 5, 'SamplePoints', [1:15]); %! l = [54.0841, 6.8872, 11.5608, 12.1518, 11.0210, 10.0112, -218.2840, ... %! -217.2375, -215.1239, -213.4890, -211.3264, 55.5800, 52.9589, ... %! 52.5979, 51.0627]; %! u = [63.2492, 131.1128, 122.4392, 122.2482, 122.5790, 122.7888, 431.0840, ... %! 430.8375, 430.3239, 429.8890, 429.3264, 65.6200, 66.6411, 65.9021, ... %! 66.9373]; %! c = [58.6667, 69, 67, 67.2, 66.8, 66.4, 106.4, 106.8, 107.6, 108.2, 109, ... %! 60.6, 59.8, 59.25, 59]; %! assert_equal (L, l, 1e-4) %! assert_equal (U, u, 1e-4) %! assert_equal (C, c, 1e-4) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'gesd'); %! assert_equal (TF, logical ([0 0 0 1 0 0 0 0 1 0 0 0 0 0 0])) %! assert_equal (L, 34.235977035439944, 1e-12) %! assert_equal (U, 89.764022964560060, 1e-12) %! assert_equal (C, 62) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'gesd', 'ThresholdFactor', 0.01); %! assert_equal (TF, logical ([0 0 0 1 0 0 0 0 1 0 0 0 0 0 0])) %! assert_equal (L, 31.489256770616173, 1e-12) %! assert_equal (U, 92.510743229383820, 1e-12) %! assert_equal (C, 62) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'gesd', 'ThresholdFactor', 5e-10); %! assert_equal (TF, logical ([0 0 0 0 0 0 0 0 1 0 0 0 0 0 0])) %! assert_equal (L, 23.976664158788935, 1e-12) %! assert_equal (U, 100.02333584121110, 1e-12) %! assert_equal (C, 62) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'grubbs'); %! assert_equal (TF, logical ([0 0 0 1 0 0 0 0 1 0 0 0 0 0 0])) %! assert_equal (L, 54.642809574646606, 1e-12) %! assert_equal (U, 63.511036579199555, 1e-12) %! assert_equal (C, 59.076923076923080, 1e-12) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'grubbs', 'ThresholdFactor', 0.01); %! assert_equal (TF, logical ([0 0 0 1 0 0 0 0 1 0 0 0 0 0 0])) %! assert_equal (L, 54.216083184201850, 1e-12) %! assert_equal (U, 63.937762969644310, 1e-12) %! assert_equal (C, 59.076923076923080, 1e-12) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'percentiles', [10 90]); %! assert_equal (TF, logical ([0 0 0 0 0 0 0 0 1 0 0 0 0 0 0])) %! assert_equal (L, 57) %! assert_equal (U, 100) %! assert_equal (C, 78.5) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'percentiles', [20 80]); %! assert_equal (TF, logical ([1 0 0 1 0 0 1 0 1 0 0 0 0 0 1])) %! assert_equal (L, 57.5) %! assert_equal (U, 62) %! assert_equal (C, 59.75) ## Quartiles method: base case, ThresholdFactor override, dim = 1 and ## dim = 2 on a matrix, and NaN omission %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'quartiles'); %! assert_equal (TF, logical ([0 0 0 1 0 0 0 0 1 0 0 0 0 0 0])) %! assert_equal (L, 52.375, 1e-12) %! assert_equal (U, 67.375, 1e-12) %! assert_equal (C, 59.875, 1e-12) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'quartiles', 'ThresholdFactor', 1); %! assert_equal (TF, logical ([0 0 0 1 0 0 0 0 1 0 0 0 0 0 0])) %! assert_equal (L, 54.25, 1e-12) %! assert_equal (U, 65.5, 1e-12) %! assert_equal (C, 59.875, 1e-12) %!test %! B = magic (5) + diag (200 * ones (1, 5)); %! [TF, L, U, C] = isoutlier (B, 'quartiles', 1); %! assert_equal (TF, logical (eye (5))) %! assert_equal (L, [-86, -77.625, -94.25, -89.125, -73.125], 1e-12) %! assert_equal (U, [166, 157.375, 171.75, 165.875, 153.875], 1e-12) %! assert_equal (C, [40, 39.875, 38.75, 38.375, 40.375], 1e-12) %!test %! B = magic (5) + diag (200 * ones (1, 5)); %! [TF, L, U, C] = isoutlier (B, 'quartiles', 2); %! assert_equal (TF, logical (eye (5))) %! assert_equal (L, [-92.75; -72.125; -90.875; -83.625; -84.625], 1e-12) %! assert_equal (U, [171.25; 152.875; 166.125;161.375; 164.375], 1e-12) %! assert_equal (C, [39.25; 40.375; 37.625; 38.875; 39.875], 1e-12) %!test %! A = [57 59 60 100 59 NaN 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'quartiles'); %! assert_equal (TF, logical ([0 0 0 1 0 0 0 0 1 0 0 0 0 0 0])) %! assert_equal (L, 52, 1e-12) %! assert_equal (U, 68, 1e-12) %! assert_equal (C, 60, 1e-12) ## Percentiles method: a matrix gets a threshold pair per column or per row, ## not the first column's pair applied to all of it. %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'percentiles', [25 75]); %! assert_equal (TF, logical ([1 0 0 1 0 0 1 0 1 0 1 0 1 0 1])) %! assert_equal (L, 58, 1e-12) %! assert_equal (U, 61.75, 1e-12) %! assert_equal (C, 59.875, 1e-12) %!test %! B = magic (5) + diag (200 * ones (1, 5)); %! [TF, L, U, C] = isoutlier (B, 'percentiles', [25 75], 1); %! assert_equal (L, [8.5, 10.5, 5.5, 6.5, 12], 1e-12) %! assert_equal (U, [71.5, 69.25, 72, 70.25, 68.75], 1e-12) %! assert_equal (C, [40, 39.875, 38.75, 38.375, 40.375], 1e-12) %!test %! B = magic (5) + diag (200 * ones (1, 5)); %! [TF, L, U, C] = isoutlier (B, 'percentiles', [25 75], 2); %! assert_equal (L, [6.25; 12.25; 5.5; 8.25; 8.75], 1e-12) %! assert_equal (U, [72.25; 68.5; 69.75; 69.5; 71], 1e-12) %! assert_equal (C, [39.25; 40.375; 37.625; 38.875; 39.875], 1e-12) %!test %! B = magic (5) + diag (200 * ones (1, 5)); %! [TF, L, U, C] = isoutlier (B, 'percentiles', [10 90], 1); %! assert_equal (TF, false (5)) %! assert_equal (L, [4, 6, 1, 2, 3], 1e-12) %! assert_equal (U, [217, 205, 213, 221, 209], 1e-12) %!test %! [TF, L, U, C] = isoutlier ([1 2; 3 NaN; 5 6; 100 8], ... %! 'percentiles', [25 75], 1); %! assert_equal (TF, logical ([1 1; 0 0; 0 0; 1 1])) %! assert_equal (L, [2, 3], 1e-12) %! assert_equal (U, [52.5, 7.5], 1e-12) ## A zero threshold factor is accepted %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'ThresholdFactor', 0); %! assert_equal (TF, logical ([1 0 1 1 0 1 1 1 1 1 1 1 1 1 1])) %! assert_equal ([L, U, C], [59, 59, 59]) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'grubbs', 'ThresholdFactor', 0); %! assert_equal (TF, false (1, 15)) %! assert_equal ([L, U], [NaN, NaN]) %! assert_equal (C, 77.86666666666666, 1e-12) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U, C] = isoutlier (A, 'gesd', 'ThresholdFactor', 0); %! assert_equal (TF, false (1, 15)) %! assert_equal ([L, U], [NaN, NaN]) %! assert_equal (C, 77.86666666666666, 1e-12) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U] = isoutlier (A, 'grubbs', 'ThresholdFactor', 1e-300); %! assert_equal (TF, false (1, 15)) %! assert_equal (isfinite ([L, U]), [true, true]) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [TF, L, U] = isoutlier (A, 'ThresholdFactor', int8 (2)); %! assert_equal (TF, logical ([0 0 0 1 0 0 0 0 1 0 0 0 0 0 0])) %! assert_equal ([L, U], [53.069591125977595, 64.930408874022405], 1e-12) %!test %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %! [~, L, U] = isoutlier (A, 'ThresholdFactor', single (2)); %! assert_equal ([L, U], [53.069591125977595, 64.930408874022405], 1e-12) ## An option that needs a value says so instead of failing on varargin{2}. %!error ... %! isoutlier ([1 2 3 4 5], 'movmean') %!error ... %! isoutlier ([1 2 3 4 5], 'movmedian') %!error ... %! isoutlier ([1 2 3 4 5], 'percentiles') %!error ... %! isoutlier ([1 2 3 4 5], 'SamplePoints') %!error ... %! isoutlier ([1 2 3 4 5], 'ThresholdFactor') %!error ... %! isoutlier ([1 2 3 4 5], 'MaxNumOutliers') ## Test input validation %!shared A %! A = [57 59 60 100 59 58 57 58 300 61 62 60 62 58 57]; %!error ... %! isoutlier (A, 'movmedian', 0); %!error ... %! isoutlier (A, 'movmedian', []); %!error ... %! isoutlier (A, 'movmedian', [2 3 4]); %!error ... %! isoutlier (A, 'movmedian', 1.4); %!error ... %! isoutlier (A, 'movmedian', [0 1]); %!error ... %! isoutlier (A, 'movmedian', [2 -1]); %!error ... %! isoutlier (A, 'movmedian', {2 3}); %!error ... %! isoutlier (A, 'movmedian', 'char'); %! %!error ... %! isoutlier (A, 'movmean', 0); %!error ... %! isoutlier (A, 'movmean', []); %!error ... %! isoutlier (A, 'movmean', [2 3 4]); %!error ... %! isoutlier (A, 'movmean', 1.4); %!error ... %! isoutlier (A, 'movmean', [0 1]); %!error ... %! isoutlier (A, 'movmean', [2 -1]); %!error ... %! isoutlier (A, 'movmean', {2 3}); %!error ... %! isoutlier (A, 'movmean', 'char'); %! %!error ... %! isoutlier (A, 'percentiles', [-1 90]); %!error ... %! isoutlier (A, 'percentiles', [10 -90]); %!error ... %! isoutlier (A, 'percentiles', [90]); %!error ... %! isoutlier (A, 'percentiles', [90 20]); %!error ... %! isoutlier (A, 'percentiles', [90 20]); %!error ... %! isoutlier (A, 'percentiles', [10 20 90]); %!error ... %! isoutlier (A, 'percentiles', {10 90}); %!error ... %! isoutlier (A, 'percentiles', 'char'); %! %!error ... %! isoutlier (A, 'movmean', 5, 'SamplePoints', ones (3,15)); %!error ... %! isoutlier (A, 'movmean', 5, 'SamplePoints', 15); %!error ... %! isoutlier (A, 'movmean', 5, 'SamplePoints', [1,1:14]); %!error ... %! isoutlier (A, 'movmean', 5, 'SamplePoints', [2,1,3:15]); %!error ... %! isoutlier (A, 'movmean', 5, 'SamplePoints', [1:14]); %! %!error ... %! isoutlier (A, 'movmean', 5, 'ThresholdFactor', [1:14]); %!error ... %! isoutlier (A, 'movmean', 5, 'ThresholdFactor', -1); %!error ... %! isoutlier (A, 'ThresholdFactor', NaN); %!error ... %! isoutlier (A, 'ThresholdFactor', true); %!error ... %! isoutlier (A, 'ThresholdFactor', 'a'); %!error ... %! isoutlier (A, 'ThresholdFactor', 1i); %!error ... %! isoutlier (A, 'gesd', 'ThresholdFactor', 3); %!error ... %! isoutlier (A, 'grubbs', 'ThresholdFactor', 3); %! %!error ... %! isoutlier (A, 'movmean', 5, 'MaxNumOutliers', [1:14]); %!error ... %! isoutlier (A, 'movmean', 5, 'MaxNumOutliers', -1); %!error ... %! isoutlier (A, 'movmean', 5, 'MaxNumOutliers', 0); %!error ... %! isoutlier (A, 'movmean', 5, 'MaxNumOutliers', 1.5); %! %!error ... %! isoutlier (A, {'movmean'}, 5, 'SamplePoints', [1:15]); %!error isoutlier (A, {1}); %!error isoutlier (A, true); %!error isoutlier (A, false); %!error isoutlier (A, 0); %!error isoutlier (A, [1 2]); %!error isoutlier (A, -2); statistics-release-2.1.2/inst/Data_Manipulation/multiway.m000066400000000000000000000317311527311104300237250ustar00rootroot00000000000000## Copyright (C) 2025 Swayam Shah ## Copyright (C) 2025 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{groupindex} =} multiway (@var{numbers}, @var{num_parts}) ## @deftypefnx {statistics} {@var{groupindex} =} multiway (@var{numbers}, @var{num_parts}, @var{method}) ## @deftypefnx {statistics} {[@var{groupindex}, @var{partition}] =} multiway (@dots{}) ## @deftypefnx {statistics} {[@var{groupindex}, @var{partition}, @var{groupsizes}] =} multiway (@dots{}) ## ## Solve the multiway number partitioning problem. ## ## @code{@var{groupindex} = multiway (@var{numbers}, @var{num_parts})} splits ## a set of numbers in @var{numbers} into a number of subsets specified in ## @var{num_parts} such that the sums of the subsets are nearly as equal as ## possible and returns a vector of group indices in @var{groupindex} with each ## index corresponding to the set of numbers provided as input. ## ## @itemize ## @item @var{numbers} is a vector of positive real numbers to be partitioned. ## @item @var{num_parts} is a positive integer scalar specifying the number of ## partitions (subsets) to split the numbers into. ## @end itemize ## ## @code{@var{groupindex} = multiway (@var{numbers}, @var{num_parts}, ## @var{method})} also specifies the algorithm used for partitioning the set of ## numbers. By default, @code{multiway} uses the complete Karmarkar-Karp ## algorithm, when the set of numbers contains up to 10 elements and the ## requested number of subsets does not exceed 5, otherwise it defaults to the ## greedy algorithm, which is optimized for speed, but may not return the ## optimal partitioning. The following methods are supported: ## ## @itemize ## @item @qcode{'greedy'} (Greedy algorithm) ## @item @qcode{'completeKK'} (Complete Karmarkar-Karp algorithm) ## @end itemize ## ## The @code{multiway} function may return up to three output arguments ## described below: ## ## @itemize ## @item ## @var{groupindex}: A vector of the same length as @var{numbers} containing ## the group index (from 1 to @var{num_parts}) for each number. ## @item ## @var{partition}: A cell array of length @var{num_parts} with each cell ## containing the numbers assigned to that partition. ## @item ## @var{groupsizes}: A vector of the sums of the numbers in each partition. ## @end itemize ## ## Example: ## @example ## @group ## numbers = [4, 5, 6, 7, 8]; ## num_parts = 2; ## [groupindex, partition, groupsizes] = multiway (numbers, num_parts); ## @end group ## @end example ## ## @seealso{cvpartition} ## @end deftypefn function [gindex, partition, gsize] = multiway (numbers, num_parts, method) if (nargin < 2) error ("multiway: too few input arguments."); elseif (nargin == 2) if (numel (numbers) <= 10 && num_parts <= 5) method = 'completeKK'; else method = 'greedy'; endif elseif (! (ischar (method) && isvector (method))) error ("multiway: METHOD value must be a character vector."); endif ## Validate numbers vector if (! isvector (numbers) || isempty (numbers)) error ("multiway: NUMBERS must be a non-empty vector."); endif if (! isnumeric (numbers) || any (isnan (numbers))) error ("multiway: NUMBERS must be numeric and cannot contain NaN values."); endif if (any (numbers < 0)) error ("multiway: NUMBERS must be non-negative."); endif ## Validate number of partitions if (! isscalar (num_parts)) error ("multiway: NUM_PARTS must be a scalar."); endif if (! isnumeric (num_parts) || ! isreal (num_parts)) error ("multiway: NUM_PARTS must be a real numeric value."); endif if (num_parts < 1 || fix (num_parts) != num_parts) error ("multiway: NUM_PARTS must be a positive integer."); endif if (num_parts > numel (numbers)) error (strcat ("multiway: NUM_PARTS cannot be greater than", ... " number of elements in NUMBERS.")); endif ## Select method switch (lower (method)) case 'completekk' [gindex, partition, gsize] = completeKK (numbers, num_parts); case 'greedy' [gindex, partition, gsize] = greedy (numbers, num_parts); otherwise error ("multiway: unsupported method '%s'.", method); endswitch endfunction function [groupindex, partition, groupsizes] = greedy (numbers, num_parts) [sorted_numbers, sorted_indices] = sort (numbers, 'descend'); n = numel (sorted_numbers); partition = cell (1, num_parts); sums = zeros (1, num_parts); group_assignment = zeros (1, n); for i = 1:n [min_sum, min_idx] = min (sums); partition{min_idx}(end+1) = sorted_numbers(i); sums(min_idx) = min_sum + sorted_numbers(i); group_assignment(i) = min_idx; endfor groupindex = zeros (size (numbers)); groupindex(sorted_indices) = group_assignment; groupsizes = sums; if (iscolumn (numbers)) groupsizes = groupsizes'; endif endfunction function [groupindex, partition, groupsizes] = completeKK (numbers, num_parts) [gidx_g, part_g, gsize_g] = greedy (numbers, num_parts); best_diff = max (gsize_g) - min (gsize_g); best_partition = part_g; groupindex = gidx_g; if (best_diff == 0) partition = best_partition; groupsizes = gsize_g; if (iscolumn (numbers)) groupsizes = groupsizes'; endif return; endif sorted_numbers = numbers(:).'; [sorted_numbers, sorted_indices] = sort (sorted_numbers, 'descend'); n = numel (sorted_numbers); total = sum (sorted_numbers); average = total / num_parts; function rec (k, current_sums, current_assign) if (k > n) this_diff = max (current_sums) - min (current_sums); if (this_diff < best_diff) best_diff = this_diff; for jj = 1:num_parts best_partition{jj} = sorted_numbers(current_assign == jj); endfor endif return; endif rem_max = sorted_numbers(k); this_min = min (current_sums); lb_max = max (max (current_sums), this_min + rem_max); lb_diff = max (0, lb_max - average); if (lb_diff >= best_diff) return; endif [~, perm] = sort (current_sums); for jj = 1:num_parts j = perm (jj); new_sums = current_sums; new_sums(j) += sorted_numbers(k); new_max = max (new_sums); if (k < n) next_rem_max = sorted_numbers(k + 1); sub_min = min (new_sums); sub_lb_max = max (new_max, sub_min + next_rem_max); else sub_lb_max = new_max; endif sub_lb_diff = max (0, sub_lb_max - average); if (sub_lb_diff >= best_diff) continue; endif new_assign = current_assign; new_assign(k) = j; rec (k + 1, new_sums, new_assign); endfor endfunction rec (1, zeros (1, num_parts), zeros (1, n)); partition = best_partition; groupsizes = cellfun (@sum, partition); idx_cell = convert_to_indices (partition, numbers); groupindex = zeros (size (numbers)); for j = 1:num_parts groupindex(idx_cell{j}) = j; endfor if (iscolumn (numbers)) groupsizes = groupsizes'; endif endfunction function indices = convert_to_indices (parts, original_numbers) indices = cell (size (parts)); numbers_copy = original_numbers(:)'; for i = 1:numel (parts) idxs = []; for val = parts{i} pos = find (numbers_copy == val, 1); if (isempty (pos)) error ("Value %g not found during index conversion", val); endif idxs = [idxs, pos]; numbers_copy(pos) = NaN; endfor indices{i} = idxs; endfor endfunction ## Test completeKK method %!test %! numbers = [4, 5, 6, 7, 8]; %! num_parts = 2; %! [groupindex, partition, groupsizes] = multiway (numbers, num_parts, 'completeKK'); %! assert_equal (sort (cellfun (@sum, partition)), sort ([15, 15])); %!test %! numbers = [1, 2, 3, 4, 5, 6]; %! num_parts = 3; %! [groupindex, partition, groupsizes] = multiway (numbers, num_parts, 'completeKK'); %! assert_equal (sort (cellfun (@sum, partition)), sort ([7, 7, 7])); %!test %! numbers = [24, 21, 18, 17, 12, 11, 8, 2]; %! num_parts = 3; %! [groupindex, partition, groupsizes] = multiway (numbers, num_parts, 'completeKK'); %! assert_equal (sort (cellfun (@sum, partition)), sort ([38, 38, 37])); %!test %! numbers = [10, 10, 10]; %! num_parts = 3; %! [~, partition] = multiway (numbers, num_parts, 'completeKK'); %! assert_equal (sort (cellfun (@sum, partition)), [10, 10, 10]); %!test %! numbers = 1:10; %! num_parts = 2; %! [~, partition] = multiway (numbers, num_parts, 'completeKK'); %! assert_equal (sort (cellfun (@sum, partition)), [27, 28]); ## Test greedy method %!test %! numbers = [4, 5, 6, 7, 8]; %! num_parts = 2; %! [groupindex, partition, groupsizes] = multiway (numbers, num_parts, 'greedy'); %! assert_equal (sort (cellfun (@sum, partition)), sort ([13, 17])); %!test %! numbers = [1, 2, 3, 4, 5, 6]; %! num_parts = 3; %! [groupindex, partition, groupsizes] = multiway (numbers, num_parts, 'greedy'); %! assert_equal (sort (cellfun (@sum, partition)), sort ([7, 7, 7])); %!test %! numbers = [10, 7, 5, 5, 6, 4, 10, 11, 12, 9, 10, 4, 3, 4, 5]; %! num_parts = 4; %! [groupindex, partition, groupsizes] = multiway (numbers, num_parts, 'greedy'); %! assert_equal (sort (cellfun (@sum, partition)), sort ([27, 27, 27, 24])); %!test %! numbers = [24, 21, 18, 17, 12, 11, 8, 2]; %! num_parts = 3; %! [groupindex, partition, groupsizes] = multiway (numbers, num_parts, 'greedy'); %! assert_equal (sort (cellfun (@sum, partition)), sort ([35, 37, 41])); %!test %! numbers = [10, 10, 10]; %! num_parts = 3; %! [~, partition] = multiway (numbers, num_parts, 'greedy'); %! assert_equal (sort (cellfun (@sum, partition)), [10, 10, 10]); %!test %! numbers = 1:10; %! num_parts = 2; %! [~, partition] = multiway (numbers, num_parts, 'greedy'); %! assert_equal (sort (cellfun (@sum, partition)), [27, 28]); ## Test algorithm switch %!test %! grpidx_ckk = multiway ([3 2 4 3 9 3 64], 3); %! grpidx_greedy = multiway ([3 2 4 3 9 3 64], 3, 'greedy'); %! assert_equal (isequal (grpidx_ckk, grpidx_greedy), false); ## Test column vector input %!test %! numbers = [4; 5; 6; 7; 8]; %! num_parts = 2; %! [groupindex, partition, groupsizes] = multiway (numbers, num_parts, 'completeKK'); %! assert_equal (iscolumn (groupindex), true) %! assert_equal (iscolumn (groupsizes), true); %! assert_equal (sort (cellfun (@sum, partition)), sort ([15, 15])); %! numbers = [4; 5; 6; 7; 8]; %! num_parts = 2; %! [groupindex, partition, groupsizes] = multiway (numbers, num_parts, 'greedy'); %! assert_equal (iscolumn (groupindex), true) %! assert_equal (iscolumn (groupsizes), true); %! assert_equal (sort (cellfun (@sum, partition)), sort ([13, 17])); ## Test row vector input %!test %! numbers = [4, 5, 6, 7, 8]; %! num_parts = 2; %! [groupindex, partition, groupsizes] = multiway (numbers, num_parts, 'completeKK'); %! assert_equal (isrow (groupindex), true) %! assert_equal (isrow (groupsizes), true); %! assert_equal (sort (cellfun (@sum, partition)), sort ([15, 15])); %!test %! numbers = [4, 5, 6, 7, 8]; %! num_parts = 2; %! [groupindex, partition, groupsizes] = multiway (numbers, num_parts, 'greedy'); %! assert_equal (isrow (groupindex), true) %! assert_equal (isrow (groupsizes), true); %! assert_equal (sort (cellfun (@sum, partition)), sort ([13, 17])); ## Test input validation %!error multiway () %!error multiway ([1, 2]) %!error ... %! multiway ([1, 2, 3], 2, 1) %!error multiway ([], 2) %!error multiway (ones (2, 2), 2) %!error ... %! multiway ({1, 2, 3}, 2) %!error multiway ([1, -2, 3], 2) %!error ... %! multiway ([1, 2, NaN], 2) %!error multiway ([1,2,3], [1,2]) %!error ... %! multiway ([1, 2, 3], '2') %!error multiway ([1, 2, 3], 0) %!error multiway ([1, 2, 3], 1.5) %!error multiway ([1, 2, 3], -1) %!error ... %! multiway ([1, 2], 3) %!error ... %! multiway ([1,2,3], 2, 'greedyalgo') statistics-release-2.1.2/inst/Data_Manipulation/normalise_distribution.m000066400000000000000000000220521527311104300266360ustar00rootroot00000000000000## Copyright (C) 2011 Alexander Klein ## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{NORMALISED} =} normalise_distribution (@var{DATA}) ## @deftypefnx {statistics} {@var{NORMALISED} =} normalise_distribution (@var{DATA}, @var{DISTRIBUTION}) ## @deftypefnx {statistics} {@var{NORMALISED} =} normalise_distribution (@var{DATA}, @var{DISTRIBUTION}, @var{DIMENSION}) ## ## Transform a set of data so as to be N(0,1) distributed according to an idea ## by van Albada and Robinson. ## ## This is achieved by first passing it through its own cumulative distribution ## function (CDF) in order to get a uniform distribution, and then mapping ## the uniform to a normal distribution. ## ## The data must be passed as a vector or matrix in @var{DATA}. ## If the CDF is unknown, then [] can be passed in @var{DISTRIBUTION}, and in ## this case the empirical CDF will be used. ## Otherwise, if the CDFs for all data are known, they can be passed in ## @var{DISTRIBUTION}, ## either in the form of a single function name as a string, ## or a single function handle, ## or a cell array consisting of either all function names as strings, ## or all function handles. ## In the latter case, the number of CDFs passed must match the number ## of rows, or columns respectively, to normalise. ## If the data are passed as a matrix, then the transformation will ## operate either along the first non-singleton dimension, ## or along @var{DIMENSION} if present. ## ## Notes: ## The empirical CDF will map any two sets of data ## having the same size and their ties in the same places after sorting ## to some permutation of the same normalised data: ## @example ## @code{normalise_distribution([1 2 2 3 4])} ## @result{} -1.28 0.00 0.00 0.52 1.28 ## ## @code{normalise_distribution([1 10 100 10 1000])} ## @result{} -1.28 0.00 0.52 0.00 1.28 ## @end example ## ## Original source: ## S.J. van Albada, P.A. Robinson ## "Transformation of arbitrary distributions to the ## normal distribution with application to EEG ## test-retest reliability" ## Journal of Neuroscience Methods, Volume 161, Issue 2, ## 15 April 2007, Pages 205-211 ## ISSN 0165-0270, 10.1016/j.jneumeth.2006.11.004. ## (http://www.sciencedirect.com/science/article/pii/S0165027006005668) ## @end deftypefn function normalised = normalise_distribution (data, distribution, dimension) if (nargin < 1 || nargin > 3) print_usage; elseif (! ismatrix (data) || length (size (data)) > 2) error (strcat ("normalise_distribution: first argument", ... " must be a vector or matrix.")); endif if (nargin >= 2) if (! isempty (distribution)) ## Wrap a single handle in a cell array. if (strcmp (typeinfo (distribution), typeinfo (@(x)(x)))) distribution = {distribution}; ## Do we have a string argument instead? elseif (ischar (distribution)) ## Is it a single string? if (rows (distribution) == 1) temp = str2func ([distribution]); distribution = {temp}; else error (strcat ("normalise_distribution: second argument cannot", ... " contain more than one string unless in a cell", ... " array.")); endif ## Do we have a cell array of distributions instead? elseif (iscell (distribution)) ## Does it consist of strings only? if (all (cellfun (@ischar, distribution))) distribution = cellfun (@str2func, distribution, ... 'UniformOutput', false ); endif ## Does it eventually consist of function handles only if (! all (cellfun (@(h) (strcmp (typeinfo (h), typeinfo ... (@(x)(x)))), distribution))) error (strcat ("normalise_distribution: second argument must", ... " contain either a single function name or", ... " handle or a cell array of either all function", ... " names or handles!")); endif else error ( "Illegal second argument: ", typeinfo ( distribution ) ); endif endif else distribution = []; endif if (nargin == 3) if (! isscalar (dimension) || (dimension != 1 && dimension != 2)) error ("normalise_distribution: third argument must be either 1 or 2."); endif else if (isvector (data) && rows (data) == 1) dimension = 2; else dimension = 1; endif endif trp = (dimension == 2); if (trp) data = data'; endif r = rows (data); c = columns (data); normalised = NA (r, c); ## Do we know the distribution of the sample? if (isempty (distribution)) precomputed_normalisation = []; for k = 1 : columns ( data ) ## Note that this line is in accordance with equation (16) in the ## original text. The author's original program, however, produces ## different values in the presence of ties, namely those you'd ## get replacing "last" by "first". [uniq, indices] = unique (sort (data(:, k)), 'last'); ## Does the sample have ties? if (rows (uniq) != r) ## Transform to uniform, then normal distribution. uniform = ( indices - 1/2 ) / r; normal = norminv ( uniform ); else ## Without ties everything is pretty much straightforward as ## stated in the text. if (isempty (precomputed_normalisation)) precomputed_normalisation = norminv (1 / (2*r) : 1/r : 1 - 1 / (2*r)); endif normal = precomputed_normalisation; endif ## Find the original indices in the unsorted sample. ## This somewhat quirky way of doing it is still faster than ## using a for-loop. [ ignore, ignore, target_indices ] = unique ( data(:, k ) ); ## Put normalised values in the places where they belong. normalised( :, k ) = normal(target_indices); endfor else ## With known distributions, everything boils down to a few lines of code ##The same distribution for all data? if (all (size (distribution) == 1)) normalised = norminv (distribution{1,1}(data)); elseif (length (vec (distribution)) == c) for k = 1 : c normalised(:, k) = norminv (distribution{k}(data)(:, k)); endfor else error (strcat ("normalise_distribution: number of distributions", ... " does not match data size!")); endif endif if (trp) normalised = normalised'; endif endfunction %!test %! v = normalise_distribution ([1 2 3], [], 1); %! assert_equal (v, [0 0 0]) %!test %! v = normalise_distribution ([1 2 3], [], 2); %! assert_equal (v, norminv ([1 3 5] / 6), 3 * eps) %!test %! v = normalise_distribution ([1 2 3]', [], 2); %! assert_equal (v, [0 0 0]') %!test %! v = normalise_distribution ([1 2 3]', [], 1); %! assert_equal (v, norminv ([1 3 5]' / 6), 3 * eps) %!test %! v = normalise_distribution ([1 1 2 2 3 3], [], 2); %! assert_equal (v, norminv ([3 3 7 7 11 11] / 12), 3 * eps) %!test %! v = normalise_distribution ([1 1 2 2 3 3]', [], 1); %! assert_equal (v, norminv ([3 3 7 7 11 11]' / 12), 3 * eps) %!test %! A = randn ( 10 ); %! N = normalise_distribution (A, @normcdf); %! assert_equal (A, N, 10000 * eps) %!test %! A = exprnd (1, 100); %! N = normalise_distribution (A, @(x)(expcdf (x, 1))); %! assert_equal (mean (vec (N)), 0, 0.1) %! assert_equal (std (vec (N)), 1, 0.1) %!test %! A = rand (1000,1); %! N = normalise_distribution (A, {@(x)(unifcdf (x, 0, 1))}); %! assert_equal (mean (vec (N)), 0, 0.2) %! assert_equal (std (vec (N)), 1, 0.1) %!test %! A = [rand(1000,1), randn(1000, 1)]; %! N = normalise_distribution (A, {@(x)(unifcdf (x, 0, 1)), @normcdf}); %! assert_equal (mean (N), [0, 0], 0.2) %! assert_equal (std (N), [1, 1], 0.1) %!test %! A = [rand(1000,1), randn(1000, 1), exprnd(1, 1000, 1)]'; %! N = normalise_distribution (A, {@(x)(unifcdf (x, 0, 1)); @normcdf; @(x)(expcdf (x, 1))}, 2); %! assert_equal (mean (N, 2), [0, 0, 0]', 0.2); %! assert_equal (std (N, [], 2), [1, 1, 1]', 0.1); %!xtest %! A = exprnd (1, 1000, 9); A(300:500, 4:6) = 17; %! N = normalise_distribution (A); %! assert_equal (mean (N), [0 0 0 0.38 0.38 0.38 0 0 0], 0.1); %! assert_equal (var (N), [1 1 1 2.59 2.59 2.59 1 1 1], 0.1); %!test %!error normalise_distribution (zeros (3, 4), ... %! {@(x)(unifcdf (x, 0, 1)); @normcdf; @(x)(expcdf (x,1))}); statistics-release-2.1.2/inst/Data_Manipulation/randsample.m000066400000000000000000000212231527311104300241730ustar00rootroot00000000000000## Copyright (C) 2014 - Nir Krakauer ## Copyright (C) 2025 Andreas Bertsatos ## Copyright (C) 2026 Avanish Salunke ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} randsample (@var{v}, @var{k}) ## @deftypefnx {statistics} {@var{y} =} randsample (@var{v}, @var{k}, @var{replacement}=false) ## @deftypefnx {statistics} {@var{y} =} randsample (@var{v}, @var{k}, @var{replacement}=false, [@var{w}=[]]) ## ## Sample elements from a vector. ## ## Returns @var{k} random elements from a vector @var{v} with @var{n} elements, ## sampled without or with @var{replacement}, with an optional weight vector. ## ## If @var{v} is a non-negative integer scalar, samples from 1:@var{v}; it must ## not be @code{Inf}. Any other scalar, @code{-Inf} and @code{NaN} included, is ## a population of one element. @var{k} must be finite. ## ## If a weight vector @var{w} of the same size as @var{v} is specified, the ## probability of each element being sampled is proportional to @var{w}. ## Unlike Matlab's function of the same name, this can be done for sampling with ## or without replacement. ## ## Randomization is performed using rand(). ## ## @seealso{datasample, randperm} ## @end deftypefn function y = randsample (v, k, replacement=false, w=[]) if (isscalar (v) && isnumeric (v) && v == Inf) error ("randsample: The input v must not be Inf."); elseif (isscalar (v) && isnumeric (v) && (round (v) == v) && (v >= 0)) n = v; vector_v = false; elseif (isvector (v)) n = length (v); vector_v = true; else error ("randsample: The input v must be a vector or non-negative integer."); endif if (! isscalar (k) || ! isnumeric (k) || ! isfinite (k) || round (k) != k) error ("randsample: The input k must be an integer."); endif if (max (0, k) > n && ! replacement) error ("randsample: Sampling without replacement needs k <= n."); endif k = max (0, k); if (! isempty (w)) if (length (w) != n) error ("randsample: the size w (%d) must match the first argument (%d)", ... length (w), n); elseif (! all (w >= 0) || sum (w) <= 0) error ("randsample: the weight vector w must consist of non-negative elements and sum to a positive number"); endif endif if (replacement) # sample with replacement if (isempty (w)) # all elements are equally likely to be sampled y = round (n * rand (1, k) + 0.5); else y = weighted_replacement (k, w); endif else # sample without replacement if (isempty (w)) # all elements are equally likely to be sampled y = randperm (n, k); else # use "accept-reject"-like sampling if (k > nnz (w)) error ("randsample: not enough non-zero weights for sampling without replacement"); endif y = weighted_replacement (k, w); while (1) [yy, idx] = sort (y); # Note: sort keeps order of equal elements. Idup = [false, (diff (yy)==0)]; if (! any (Idup)) break else Idup(idx) = Idup; # find duplicates in original vector w(y) = 0; # don't permit resampling ## remove duplicates, then sample again y = [y(! Idup), (weighted_replacement (sum (Idup), w))]; endif endwhile endif endif if vector_v y = v(y); if (iscolumn (v)) y = y(:); elseif (isrow (v)) y = y(:).'; endif else y = y(:); endif endfunction function y = weighted_replacement (k, w) w = w / sum (w); w = [0, (cumsum (w(:))')]; ## distribute k uniform random deviates based on the given weighting y = arrayfun (@(x) find (w <= x, 1, 'last'), rand (1, k)); endfunction %!test %! n = 20; %! k = 5; %! x = randsample (n, k); %! assert_equal (size (x), [k 1]); %! x = randsample (n, k, true); %! assert_equal (size (x), [k 1]); %! x = randsample (n, k, false); %! assert_equal (size (x), [k 1]); %! x = randsample (n, k, true, ones (n, 1)); %! assert_equal (size (x), [k 1]); %! x = randsample (1:n, k); %! assert_equal (size (x), [1 k]); %! x = randsample (1:n, k, true); %! assert_equal (size (x), [1 k]); %! x = randsample (1:n, k, false); %! assert_equal (size (x), [1 k]); %! x = randsample (1:n, k, true, ones (n, 1)); %! assert_equal (size (x), [1 k]); %! x = randsample ((1:n)', k); %! assert_equal (size (x), [k 1]); %! x = randsample ((1:n)', k, true); %! assert_equal (size (x), [k 1]); %! x = randsample ((1:n)', k, false); %! assert_equal (size (x), [k 1]); %! x = randsample ((1:n)', k, true, ones (n, 1)); %! assert_equal (size (x), [k 1]); %! n = 10; %! k = 100; %! x = randsample (n, k, true, 1:n); %! assert_equal (size (x), [k 1]); %! x = randsample ((1:n)', k, true); %! assert_equal (size (x), [k 1]); %! x = randsample (k, k, false, 1:k); %! assert_equal (size (x), [k 1]); %!test %! n = 20; %! k = 5; %! p = 1:n; %! x = randsample (p, k); %! assert_equal (isnumeric (x), true); %! assert_equal (size (x), [1 k]); %! x = randsample (p, k, true); %! assert_equal (isnumeric (x), true); %! assert_equal (size (x), [1 k]); %! x = randsample (p, k, false); %! assert_equal (isnumeric (x), true); %! assert_equal (size (x), [1 k]); %! k = 30; %! x = randsample (p, k, true); %! assert_equal (isnumeric (x), true); %! assert_equal (size (x), [1 k]); %!test %! p = categorical ({'a', 'b', 'c', 'd', 'a'}); %! k = 3; %! x = randsample (p, k, true); %! assert_equal (iscategorical (x), true); %! assert_equal (size (x), [1 k]); %! x = randsample (p, k, false); %! assert_equal (iscategorical (x), true); %! assert_equal (size (x), [1 k]); %! k = 30; %! x = randsample (p, k, true, ones (length (p),1)); %! assert_equal (iscategorical (x), true); %! assert_equal (size (x), [1 k]); %!test %! p = {'a', 'b', 'c', 'd', 'a'}; %! k = 2; %! x = randsample (p, k, true); %! assert_equal (iscell (x), true); %! assert_equal (size (x), [1 k]); %! x = randsample (p, k, false); %! assert_equal (iscell (x), true); %! assert_equal (size (x), [1 k]); %! k = 30; %! x = randsample (p, k, true, ones (length (p),1)); %! assert_equal (iscell (x), true); %! assert_equal (size (x), [1 k]); %!test %! p = string ({'a', 'b', 'c', 'd', 'a'}); %! k = 2; %! x = randsample (p, k, true); %! assert_equal (isstring (x), true); %! assert_equal (size (x), [1 k]); %! x = randsample (p, k, false); %! assert_equal (isstring (x), true); %! assert_equal (size (x), [1 k]); %! k = 30; %! x = randsample (p, k, true, ones (length (p),1)); %! assert_equal (isstring (x), true); %! assert_equal (size (x), [1 k]); %!test %! assert_equal (randsample ('A', 1), 'A'); %! assert_equal (randsample (true, 1), true); %! assert_equal (randsample (5.5, 1), 5.5); %! assert_equal (randsample (-5, 1), -5); %! assert_equal (randsample (-Inf, 1), -Inf); %! assert_equal (randsample (NaN, 1), NaN); %!test %! x = randsample (10, -2); %! assert_equal (isempty (x), true); %! assert_equal (size (x), [0, 1]); %!error ... %! randsample ([1 2 3; 1 2 3], 5) %!error ... %! randsample (10, 100) %!error ... %! randsample (10, 5, false, ones (5,1)) %!error ... %! randsample (5, 2, true, [0, 0, 0, 0, 0]) %!error ... %! randsample (5, 2, true, [1, 2, -1, 4, 5]) %!error ... %! randsample (5, 2, true, [1, 2, NaN, 4, 5]) %!error ... %! randsample (5, 4, false, [1, 1, 0, 0, 0]) %!error ... %! randsample (10, 2.5) %!error randsample (Inf, 1) %!error randsample (5, Inf, true) %!error randsample (5, NaN) statistics-release-2.1.2/inst/Data_Manipulation/rmmissing.m000066400000000000000000000241001527311104300240520ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{R} =} rmmissing (@var{A}) ## @deftypefnx {statistics} {@var{R} =} rmmissing (@var{A}, @var{dim}) ## @deftypefnx {statistics} {@var{R} =} rmmissing (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{R}, @var{TF}] =} rmmissing (@dots{}) ## ## Remove missing data from arrays. ## ## Given an input vector or matrix (2-D array) @var{A}, @code{@var{R} = ## rmmissing (@var{A})} returns an output vector or matrix @var{R} of the same ## type as input @var{A} and any missing elements removed. If @var{A} is a ## vector, missing elements are removed individually, if @var{A} is a matrix, ## then rows containing missing elements are removed. ## ## Standard missing values and their corresponding data types are: ## ## @itemize ## @item @qcode{NaN} - for @qcode{double}, @qcode{single}, @qcode{duration}, and ## @qcode{calendarDuration} arrays. ## @item @qcode{NaT} - for @qcode{datetime} arrays. ## @item @qcode{} - for @qcode{string} arrays. ## @item @qcode{} - for @qcode{categorical} arrays. ## @item @qcode{@{0x0 char@}} - for @qcode{cell} arrays of character vectors. ## @end itemize ## ## For any data types that do not support missing values, @code{rmmissing} ## returns @code{@var{R} == @var{A}} and an all-false @var{TF}. ## ## Given an input matrix (2-D array) @var{A}, @code{@var{R} = rmmissing ## (@var{A}, @var{dim})} further specifies whether rows or columns containing ## missing data are removed from the output @var{R} based on the value of ## @var{dim}, which must be either 1 or 0. ## ## @itemize ## @item ## @qcode{1}: remove rows. ## ## @item ## @qcode{2}: remove columns. ## @end itemize ## ## @code{@var{R} = rmmissing (@dots{}, @var{Name}, @var{Value})} also accepts ## the following paired arguments. ## ## @multitable @columnfractions 0.2 0.75 ## @headitem Name @tab Value ## @item @qcode{'MinNumMissing'} @tab A non-negative integer scalar value ## specifying the required minimum number of missing values for removing any ## particular row or column from a matrix input, or element from a vector ## input (default 1). With 0, everything is removed; above 1, nothing is ## removed from a vector, whose elements are each missing at most once. ## ## @item @qcode{'MissingLocations'} @tab A logical array of the same size ## as input @var{A} indexing the locations of missing values in input array ## @var{A}. Note that specifying @qcode{'MissingLocations'} overrides any ## standard missing values in @var{A}. ## @end multitable ## ## Optional return value @var{TF} is a logical array where @code{true} values ## represent removed entries, rows or columns from the original data @var{A}. ## It has one element per element of a vector, and one per row or column of a ## matrix, an empty @var{A} included. ## ## @seealso{fillmissing, ismissing, standardizeMissing} ## @end deftypefn function [R, TF] = rmmissing (A, varargin) ## Validate data in A if (nargin < 1) print_usage (); endif if (ndims (A) > 2) error ("rmmissing: A must be a matrix; no more than 2 dimensions allowed."); endif ## Parse optional Name-Value paired arguments optNames = {'MinNumMissing', 'MissingLocations'}; dfValues = {1, []}; [MinNumMissing, MissingLocations, args] = parsePairedArguments (optNames, dfValues, ... varargin(:)); ## Validate optional Name-Value paired arguments if (! (isscalar (MinNumMissing) && isnumeric (MinNumMissing) && MinNumMissing >= 0 && fix (MinNumMissing) == MinNumMissing)) error ("rmmissing: 'MinNumMissing' must be a non-negative integer value."); endif if (! isempty (MissingLocations)) if (! (islogical (MissingLocations) && isequal (size (MissingLocations), size (A)))) error (strcat ("rmmissing: 'MissingLocations' must be a", ... " logical matrix of the same size as input A.")); endif endif ## Check for DIM if (isempty (args)) dim = 2; elseif (isscalar (args)) dim = args{1}; if (! (isscalar (dim) && (dim == 1 || dim == 2))) error ("rmmissing: specified DIM must be either 1 or 2."); endif ## Swap DIM to operate orthogonal to specified DIM if (dim == 1) dim = 2; else dim = 1; endif else error ("rmmissing: too many input arguments."); endif ## Get missing values if (isempty (MissingLocations)) TF = ismissing (A); else TF = MissingLocations; endif ## Remove missing values if (isvector (A)) TF = TF >= MinNumMissing; R = A(! TF); else ## matrix: ismissing returns an array, so it must be converted ## to a row or column vector according to the "dim" of choice TF = sum (TF, dim) >= MinNumMissing; if (dim == 2) ## remove the rows R = A((TF == 0), :); else ## remove the columns R = A(:, (TF == 0)); endif endif endfunction %!assert_equal (rmmissing ([1, NaN, 3]), [1, 3]) %!assert_equal (rmmissing ('abcd f'), 'abcd f') %!assert_equal (rmmissing ({'xxx', '', 'xyz'}), {'xxx', 'xyz'}) %!assert_equal (rmmissing ({'xxx', ''; 'xyz', 'yyy'}), {'xyz', 'yyy'}) %!assert_equal (rmmissing ({'xxx', ''; 'xyz', 'yyy'}, 2), {'xxx'; 'xyz'}) %!assert_equal (rmmissing ([1, 2; NaN, 2]), [1, 2]) %!assert_equal (rmmissing ([1, 2; NaN, 2], 2), [2, 2]') %!assert_equal (rmmissing ([1, 2; NaN, 4; NaN, NaN],'MinNumMissing', 2), [1, 2; NaN, 4]) %!assert_equal (rmmissing ([1, NaN, 3], 'MinNumMissing', 0), zeros (1, 0)) %!assert_equal (rmmissing ([1, NaN, 3], 'MinNumMissing', 2), [1, NaN, 3]) %!assert_equal (rmmissing ([1, NaN; 3, 4], 'MinNumMissing', 0), zeros (0, 2)) %!assert_equal (rmmissing ([1, NaN; 3, 4], 2, 'MinNumMissing', 0), zeros (2, 0)) ## Test second output %!test %! x = [1:6]; %! x([2,4]) = NaN; %! [~, idx] = rmmissing (x); %! assert_equal (idx, logical ([0, 1, 0, 1, 0, 0])); %! assert_equal (class (idx), 'logical'); %! x = reshape (x, [2, 3]); %! [~, idx] = rmmissing (x); %! assert_equal (idx, logical ([0; 1])); %! assert_equal (class (idx), 'logical'); %! [~, idx] = rmmissing (x, 2); %! assert_equal (idx, logical ([1, 1, 0])); %! assert_equal (class (idx), 'logical'); %! [~, idx] = rmmissing (x, 1, 'MinNumMissing', 2); %! assert_equal (idx, logical ([0; 1])); %! assert_equal (class (idx), 'logical'); %! [~, idx] = rmmissing (x, 2, 'MinNumMissing', 2); %! assert_equal (idx, logical ([0, 0, 0])); %! assert_equal (class (idx), 'logical'); ## Test data type handling %!assert_equal (rmmissing (single ([1, 2, NaN; 3, 4, 5])), single ([3, 4, 5])) %!assert_equal (rmmissing (logical (ones (3))), logical (ones (3))) %!assert_equal (rmmissing (int32 (ones (3))), int32 (ones (3))) %!assert_equal (rmmissing (uint32 (ones (3))), uint32 (ones (3))) %!assert_equal (rmmissing ({1, 2, 3}), {1, 2, 3}) %!assert_equal (rmmissing ([struct, struct, struct]), [struct, struct, struct]) ## Test empty input handling %!assert_equal (rmmissing ([]), []) %!assert_equal (rmmissing (ones (1, 0)), ones (1, 0)) %!assert_equal (rmmissing (ones (1, 0), 1), ones (1, 0)) %!assert_equal (rmmissing (ones (1, 0), 2), ones (1, 0)) %!assert_equal (rmmissing (ones (0, 1)), ones (0, 1)) %!assert_equal (rmmissing (ones (0, 1), 1), ones (0, 1)) %!assert_equal (rmmissing (ones (0, 1), 2), ones (0, 1)) %!error ... %! rmmissing (ones (0, 1, 2)) ## Test input validation %!error rmmissing () %!error ... %! rmmissing (ones (2, 2, 2)) %!error ... %! rmmissing ([1, 2; 3, 4], 2, 'MinNumMissing', -2) %!error ... %! rmmissing ([1, 2; 3, 4], 'MinNumMissing', 3.8) %!error ... %! rmmissing ([1, 2; 3, 4], 'MinNumMissing', [1, 2, 3]) %!error ... %! rmmissing ([1, 2; 3, 4], 'MinNumMissing', 'xxx') %!error ... %! rmmissing ([1, 2; 3, 4], 'MissingLocations', false ([1, 1, 1])) %!error rmmissing ([1, 2; 3, 4], 5) %!error rmmissing ([1, 2; 3, 4], 'XXX', 1) %!error %! rmmissing ([], 'MinNumMissing', -2) %!error %! rmmissing ([], 5) %!error %! rmmissing ([], 'MissingLocations', true) %!test %! [R, TF] = rmmissing ([]); %! assert_equal (R, []); %! assert_equal (TF, false (0, 1)); %!test %! [R, TF] = rmmissing (zeros (0, 3)); %! assert_equal (R, zeros (0, 3)); %! assert_equal (TF, false (0, 1)); %!test %! [R, TF] = rmmissing (zeros (0, 3), 2); %! assert_equal (R, zeros (0, 3)); %! assert_equal (TF, false (1, 3)); %!test %! [R, TF] = rmmissing (zeros (3, 0)); %! assert_equal (R, zeros (3, 0)); %! assert_equal (TF, false (3, 1)); %!test %! [R, TF] = rmmissing (zeros (1, 0)); %! assert_equal (R, zeros (1, 0)); %! assert_equal (TF, false (1, 0)); %!test %! [R, TF] = rmmissing (cell (0, 2)); %! assert_equal (R, cell (0, 2)); %! assert_equal (TF, false (0, 1)); statistics-release-2.1.2/inst/Data_Manipulation/standardizeMissing.m000066400000000000000000000305131527311104300257110ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{B} =} standardizeMissing (@var{A}, @var{indicator}) ## ## Replace selected values by standard missing values. ## ## @code{@var{B} = standardizeMissing (@var{A}, @var{indicator})} returns a ## standardized array @var{B} of the same size and data type as the input array ## @var{A} and with all elements specified by @var{indicator} replaced by the ## standard missing value corresponding the data type of @var{A}. ## @var{indicator} can be either a scalar or a vector. ## ## Standard missing values and their corresponding data types are: ## ## @itemize ## @item @qcode{NaN} - for @qcode{double}, @qcode{single}, @qcode{duration}, and ## @qcode{calendarDuration} arrays. ## @item @qcode{NaT} - for @qcode{datetime} arrays. ## @item @qcode{} - for @qcode{string} arrays. ## @item @qcode{} - for @qcode{categorical} arrays. ## @item @qcode{@{0x0 char@}} - for @qcode{cell} arrays of character vectors. ## @end itemize ## ## For any other data type input that does not support missing values, ## @code{standardizeMissing} returns @code{@var{B} = @var{A}} and any ## @var{indicator} value is ignored. ## ## The nonstandard missing value @var{indicator} must be of the same type as the ## data input @var{A} or have a compatible data types according to the following ## rules: ## ## @itemize ## @item all numeric indicators match both @qcode{double} and @qcode{single} ## data types in @var{A}. ## @item indicators specified as @qcode{string} arrays, @qcode{char} vectors, ## and @code{cell} arrays of character vectors match categorical data type in ## @var{A}. ## @item a @qcode{char} vector matches a @qcode{cell} array of character vectors ## in @var{A}. ## @end itemize ## ## Note: the generic @code{standardizeMissing} function from the statistics does ## not operate on table inputs, which is handled by the overloaded method of the ## table class. Use @code{help table.standardizeMissing} to find more ## information about the functional specialization on tables. ## ## Standardizing a category of a @code{categorical} array removes that category ## from the array's type, as MATLAB removes it: no element carries it once the ## values are missing, so leaving it in the category list would be stale ## metadata, and the codes of the remaining categories shift down accordingly. ## Only the standardized categories are removed; one that is declared but ## unused is left alone. ## ## @seealso{fillmissing, ismissing, rmmissing} ## @end deftypefn function A = standardizeMissing (A, indicator) if (nargin != 2) print_usage (); endif if (isnumeric (A)) if (! isnumeric (indicator)) error ("standardizeMissing: incompatible INDICATOR and input data A."); endif switch (class (A)) case 'double' A(ismember (A, indicator)) = NaN ('double'); case 'single' A(ismember (A, indicator)) = NaN ('single'); endswitch elseif (iscellstr (A)) if (ischar (indicator)) if (! isrow (indicator)) error ("standardizeMissing: character INDICATOR must be a row vector."); endif indicator = {indicator}; elseif (! iscellstr (indicator)) error ("standardizeMissing: incompatible INDICATOR and input data A."); endif A(ismember (A, indicator)) = {''}; elseif (iscategorical (A)) if (ischar (indicator)) if (! isrow (indicator)) error ("standardizeMissing: character INDICATOR must be a row vector."); endif indicator = {indicator}; elseif (! (iscellstr (indicator) || isstring (indicator) || iscategorical (indicator))) error ("standardizeMissing: incompatible INDICATOR and input data A."); elseif (! isvector (indicator)) error ("standardizeMissing: INDICATOR must be a scalar or a vector."); endif ## Standardizing a category to missing removes that category from the ## type, as MATLAB removes it: no element carries it afterwards, so ## keeping it would leave stale metadata. Only the standardized ## categories go; a declared but unused category is left alone. drop = intersect (categories (A), cellstr (indicator)); A(ismember (A, indicator)) = categorical (NaN); if (! isempty (drop)) A = removecats (A, drop); endif elseif (isdatetime (A)) if (! isdatetime (indicator)) error ("standardizeMissing: incompatible INDICATOR and input data A."); elseif (! isvector (indicator)) error ("standardizeMissing: INDICATOR must be a scalar or a vector."); endif A(ismember (A, indicator)) = NaT; elseif (isduration (A)) if (! isduration (indicator)) error ("standardizeMissing: incompatible INDICATOR and input data A."); elseif (! isvector (indicator)) error ("standardizeMissing: INDICATOR must be a scalar or a vector."); endif A(ismember (A, indicator)) = days (NaN); elseif (iscalendarduration (A)) if (! iscalendarduration (indicator)) error ("standardizeMissing: incompatible INDICATOR and input data A."); elseif (! isvector (indicator)) error ("standardizeMissing: INDICATOR must be a scalar or a vector."); endif A(ismember (A, indicator)) = days (NaN); elseif (isstring (A)) if (! isstring (indicator)) error ("standardizeMissing: incompatible INDICATOR and input data A."); elseif (! isvector (indicator)) error ("standardizeMissing: INDICATOR must be a scalar or a vector."); endif A(ismember (A, indicator)) = missing; endif endfunction ## numeric tests %!assert_equal (standardizeMissing (1, 1), NaN) %!assert_equal (standardizeMissing (1, 0), 1) %!assert_equal (standardizeMissing (eye (2), 1), [NaN 0;0 NaN]) %!assert_equal (standardizeMissing ([1:3;4:6], [2 3 4 5]), [1, NaN, NaN; NaN, NaN, 6]) %!assert_equal (standardizeMissing (cat (3,1,2,3,4), 3), cat (3,1,2,NaN,4)) ## char and cellstr tests %!assert_equal (standardizeMissing ('foo', 'a'), 'foo') %!assert_equal (standardizeMissing ('foo', 'f'), 'foo') %!assert_equal (standardizeMissing ('foo', 'o'), 'foo') %!assert_equal (standardizeMissing ('foo', 'oo'), 'foo') %!assert_equal (standardizeMissing ({'foo'}, 'f'), {'foo'}) %!assert_equal (standardizeMissing ({'foo'}, {'f'}), {'foo'}) %!assert_equal (standardizeMissing ({'foo'}, 'test'), {'foo'}) %!assert_equal (standardizeMissing ({'foo'}, {'test'}), {'foo'}) %!assert_equal (standardizeMissing ({'foo'}, 'foo'), {''}) %!assert_equal (standardizeMissing ({'foo'}, {'foo'}), {''}) ## char and cellstr array tests %!assert_equal (standardizeMissing (['foo';'bar'], 'oar'), ['foo';'bar']) %!assert_equal (standardizeMissing (['foo';'bar'], ['o';'a';'r']), ['foo';'bar']) %!assert_equal (standardizeMissing (['foo';'bar'], ['o ';'ar']), ['foo';'bar']) %!assert_equal (standardizeMissing ({'foo','bar'}, 'foo'), {'','bar'}) %!assert_equal (standardizeMissing ({'foo','bar'}, 'f'), {'foo','bar'}) %!assert_equal (standardizeMissing ({'foo','bar'}, {'foo', 'a'}), {'','bar'}) %!assert_equal (standardizeMissing ({'foo'}, {'f', 'oo'}), {'foo'}) %!assert_equal (standardizeMissing ({'foo','bar'}, {'foo'}), {'','bar'}) %!assert_equal (standardizeMissing ({'foo','bar'}, {'foo', 'a'}), {'','bar'}) ## numeric type preservation tests %!assert_equal (standardizeMissing (double (1), single (1)), double (NaN)) %!assert_equal (standardizeMissing (single (1), single (1)), single (NaN)) %!assert_equal (standardizeMissing (single (1), double (1)), single (NaN)) %!assert_equal (standardizeMissing (single (1), uint8 (1)), single (NaN)) %!assert_equal (standardizeMissing (double (1), int32 (1)), double (NaN)) ## pass-trough tests %!assert_equal (standardizeMissing (true, true), true) %!assert_equal (standardizeMissing (true, 1), true) %!assert_equal (standardizeMissing (int32 (1), int32 (1)), int32 (1)) %!assert_equal (standardizeMissing (int32 (1), 1), int32 (1)) %!assert_equal (standardizeMissing (uint32 (1), uint32 (1)), uint32 (1)) %!assert_equal (standardizeMissing (uint32 (1), 1), uint32 (1)) %!assert_equal (standardizeMissing ({'abc', 1}, 1), {'abc', 1}) %!assert_equal (standardizeMissing (struct ('a','b'), 1), struct ('a','b')) ## categorical array tests ## Values below are R2024a's, measured 2026-08-17: standardizing a category ## removes it from the type, and only it. %!test %! ## every instance goes, and the remaining codes shift down %! a = standardizeMissing (categorical ({'a','b','c','b','a'}), 'b'); %! assert_equal (double (a), [1, NaN, 2, NaN, 1]); %! assert_equal (categories (a), {'a'; 'c'}); %!test %! ## a category that is declared but unused is left alone %! A = categorical ({'a','b','c'}, {'a','b','c','d'}); %! a = standardizeMissing (A, 'b'); %! assert_equal (double (a), [1, NaN, 2]); %! assert_equal (categories (a), {'a'; 'c'; 'd'}); %!test %! ## standardizing a level that is not present changes nothing %! a = standardizeMissing (categorical ({'a','b','c'}), 'z'); %! assert_equal (double (a), [1, 2, 3]); %! assert_equal (categories (a), {'a'; 'b'; 'c'}); %!test %! ## several at once %! a = standardizeMissing (categorical ({'a','b','c'}), {'a','b'}); %! assert_equal (double (a), [NaN, NaN, 1]); %! assert_equal (categories (a), {'c'}); %!test %! ## empty indicator is accepted %! A = [1 2 3]; %! B = standardizeMissing (A, []); %! assert_equal (B, A); %!test %! ## matrix indicator is accepted %! A = [1 2 3; 4 5 6]; %! indicator = [1 9; 8 5]; %! B = standardizeMissing (A, indicator); %! assert_equal (B, [NaN 2 3; 4 NaN 6]); %!assert_equal (double (standardizeMissing (categorical (1), categorical (1))), NaN) %!assert_equal (double (standardizeMissing (categorical (1), '1')), NaN) %!assert_equal (class (standardizeMissing (categorical (1), categorical (1))), 'categorical') %!assert_equal (double (standardizeMissing (categorical (1), categorical (2))), 1) %!assert_equal (double (standardizeMissing (categorical (1), '2')), 1) %!assert_equal (class (standardizeMissing (categorical (1), categorical (2))), 'categorical') ## fix GitHub PR #419, requires datatypes 1.2.0+ %!test %! A = categorical ({'a', 'b', 'c'}); %! indicator = 'b'; %! a = standardizeMissing (A , indicator); %! assert_equal (class (a), 'categorical'); %! assert_equal (double (a), [1, NaN, 2]); %! assert_equal (categories (a), {'a'; 'c'}); ## datetime array tests %!assert_equal (isnat (standardizeMissing (datetime ('today'), datetime ('today'))), true) %!assert_equal (isnat (standardizeMissing (datetime ('today'), datetime ('yesterday'))), false) ## duration array tests %!assert_equal (days (standardizeMissing (days (1), days (1))), NaN) %!assert_equal (days (standardizeMissing (days (1), days (2))), 1) ## string array tests %!assert_equal (cellstr (standardizeMissing (string (1), string (1))), {''}) %!assert_equal (cellstr (standardizeMissing (string (1), string (2))), {'1'}) ## Test input validation %!error standardizeMissing (); %!error standardizeMissing (1); %!error standardizeMissing (1, 2, 3); %!error ... %! standardizeMissing ([1, 2, 3], {1}); %!error ... %! standardizeMissing ([1, 2, 3], 'a'); %!error ... %! standardizeMissing ([1, 2, 3], struct ('a', 1)); %!error ... %! standardizeMissing (categorical (1), 1); %!error ... %! standardizeMissing ({'foo'}, string ('foo')); %!error ... %! standardizeMissing ({'foo'}, ['a';'b']); statistics-release-2.1.2/inst/Data_Manipulation/tiedrank.m000066400000000000000000000304771527311104300236610ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{r}, @var{tieadj}] =} tiedrank (@var{x}) ## @deftypefnx {statistics} {[@var{r}, @var{tieadj}] =} tiedrank (@var{x}, @var{tieflag}) ## @deftypefnx {statistics} {[@var{r}, @var{tieadj}] =} tiedrank (@var{x}, @var{tieflag}, @var{bidir}) ## @deftypefnx {statistics} {[@var{r}, @var{tieadj}] =} tiedrank (@var{x}, @var{tieflag}, @var{bidir}, @var{tol}) ## ## @var{x} may be a vector or an array. An array is ranked along its first ## dimension, so a matrix is ranked column by column, and @var{tieadj} then ## carries one entry per column: its first dimension has length 1 by default, ## or 3 when @var{tieflag} is set, and its higher dimensions are those of ## @var{x}. ## ## Compute rank adjusted for ties. ## ## @code{[@var{r}, @var{tieadj}] = tiedrank (@var{x})} computes the ranks of the ## values in vector @var{x}. If any values in @var{x} are tied, @code{tiedrank} ## computes their average rank. The return value @var{tieadj} is an adjustment ## for ties required by the nonparametric tests @code{signrank} and ## @code{ranksum}, and for the computation of Spearman's rank correlation. ## ## @code{[@var{r}, @var{tieadj}] = tiedrank (@var{x}, 1)} computes the ranks of ## the values in the vector @var{x}. @var{tieadj} is a vector of three ## adjustments for ties required in the computation of Kendall's tau. ## @code{tiedrank (@var{x}, 0)} is the same as @code{tiedrank (@var{x})}. ## ## @code{[@var{r}, @var{tieadj}] = tiedrank (@var{x}, 0, 1)} computes the ranks ## from each end, so that the smallest and largest values get rank 1, the next ## smallest and largest get rank 2, etc. These ranks are used in the ## Ansari-Bradley test. ## ## @var{tieflag} and @var{bidir} are read as an @code{if} reads a condition: ## each is set when it is not empty and all of its elements are nonzero, so an ## empty @qcode{[]} keeps the default, as in @code{tiedrank (@var{x}, [], 1)}. ## ## @code{[@var{r}, @var{tieadj}] = tiedrank (@var{x}, @var{tieflag}, ## @var{bidir}, @var{tol})} treats two values as tied when they lie within a ## tolerance of each other rather than only when they are exactly equal. ## @var{tol} is either a scalar or an array the size of @var{x} giving each ## element its own tolerance, and two neighbouring values are tied when the gap ## between them does not exceed the @strong{sum} of their two tolerances. The ## default is @qcode{0}, which is exact comparison, and an empty @var{tol} takes ## it. @code{signrank} uses this to rank differences that are equal to within ## the precision of the values they came from. ## ## @end deftypefn function [r, tieadj] = tiedrank (x, tieflag, bidir, tol) ## Check input arguments and add defaults if (nargin < 1 || nargin > 4) print_usage (); endif if (nargin < 2) tieflag = false; else tieflag = read_flag (tieflag, "TIEFLAG"); endif if (nargin < 3) bidir = false; else bidir = read_flag (bidir, "BIDIR"); endif if (nargin < 4 || (isnumeric (tol) && isempty (tol))) tol = 0; elseif (! isnumeric (tol) || ! isreal (tol) || any (tol(:) < 0)) error ("tiedrank: TOL must be a non-negative numeric array."); elseif (! isscalar (tol) && ! isequal (size (tol), size (x))) error ("tiedrank: TOL must be a scalar or the same size as X."); endif if (isscalar (tol)) tol = repmat (tol, size (x)); endif ## A matrix is ranked column by column, as MATLAB does; a vector keeps its ## own orientation if (! isvector (x)) ## Rank along the first dimension, as MATLAB does: every higher dimension ## is folded into columns and unfolded again afterwards sz = size (x); xm = reshape (x, sz(1), prod (sz(2:end))); tm = reshape (tol, sz(1), prod (sz(2:end))); rm = zeros (size (xm), class (x)); tieadj = []; for j = 1:columns (xm) [rj, tj] = rank_vector (xm(:,j), tieflag, bidir, tm(:,j)); rm(:,j) = rj; tieadj = [tieadj, tj]; endfor r = reshape (rm, sz); if (numel (sz) > 2 && ! isempty (tieadj)) tieadj = reshape (tieadj, [rows(tieadj), sz(2:end)]); endif return; endif [r, tieadj] = rank_vector (x, tieflag, bidir, tol); endfunction ## Rank one vector, leaving NaNs at the end function [r, tieadj] = rank_vector (x, tieflag, bidir, tol) ## Sort X and leave NaNs at the end of vector [sx, idx] = sort (x(:)); stol = tol(:)(idx); NaNs = sum (isnan (x)); xLen = length (x) - NaNs; ## Count ranks from low end if (! bidir) ranks = [1:xLen, NaN(1,NaNs)]'; ## Count ranks from both ends else ## For even number of samples if (mod (xLen, 2) == 0) ranks = [(1:xLen/2), (xLen/2:-1:1), NaN(1,NaNs)]'; ## For odd number of samples else ranks = [(1:(xLen+1)/2), ((xLen-1)/2:-1:1), NaN(1,NaNs)]'; endif endif ## Define number of adjustments if (! tieflag) tieadj = 0; else tieadj = [0; 0; 0]; endif ## Check precision of X if (isa (x, 'single')) ranks = single (ranks); tieadj = single (tieadj); endif ## Adjust for ties. Exact equality always ties -- which also keeps equal ## infinities tied, their difference being NaN -- and a nonzero TOL ties a ## neighbouring pair whose gap is within the sum of their two tolerances. ties = sx(1:xLen-1) >= sx(2:xLen); if (any (stol(1:xLen) > 0)) ties = ties | (sx(2:xLen) - sx(1:xLen-1) <= ... stol(1:xLen-1) + stol(2:xLen)); endif tieloc = [find(ties); xLen+2]; maxTies = length (tieloc); tiecount = 1; while (tiecount < maxTies) tiestart = tieloc(tiecount); ntied = 2; while (tieloc(tiecount + 1) == tieloc(tiecount) + 1) tiecount = tiecount + 1; ntied = ntied + 1; endwhile if (! tieflag) tieadj = tieadj + ntied * (ntied - 1) * (ntied + 1) / 2; else n2minusn = ntied * (ntied - 1); tieadj = tieadj + [n2minusn/2; n2minusn*(ntied-2); n2minusn*(2*ntied+5)]; endif ## Compute mean of tied ranks ranks(tiestart:tiestart+ntied-1) = ... sum (ranks(tiestart:tiestart+ntied-1)) / ntied; tiecount = tiecount + 1; endwhile ## Reshape ranks including NaN where required. r(idx) = ranks; r = reshape (r, size (x)); endfunction ## Read a flag the way IF reads a condition: it is set only when it is not ## empty and all of its elements are nonzero function tf = read_flag (flag, name) if (! (isnumeric (flag) || islogical (flag) || ischar (flag)) || iscomplex (flag) || any (isnan (flag(:)))) error (strcat ("tiedrank: %s must be a real numeric, logical, or", ... " character array without NaN values."), name); endif tf = ! isempty (flag) && all (flag(:)); endfunction ## testing against mileage data and results from Matlab %!test %! [r,tieadj] = tiedrank ([10, 20, 30, 40, 20]); %! assert_equal (r, [1, 2.5, 4, 5, 2.5]); %! assert_equal (tieadj, 3); %!test %! [r,tieadj] = tiedrank ([10; 20; 30; 40; 20]); %! assert_equal (r, [1; 2.5; 4; 5; 2.5]); %! assert_equal (tieadj, 3); %!test %! [r,tieadj] = tiedrank ([10, 20, 30, 40, 20], 1); %! assert_equal (r, [1, 2.5, 4, 5, 2.5]); %! assert_equal (tieadj, [1; 0; 18]); %!test %! [r,tieadj] = tiedrank ([10, 20, 30, 40, 20], 0, 1); %! assert_equal (r, [1, 2.5, 2, 1, 2.5]); %! assert_equal (tieadj, 3); %!test %! [r,tieadj] = tiedrank ([10, 20, 30, 40, 20], 1, 1); %! assert_equal (r, [1, 2.5, 2, 1, 2.5]); %! assert_equal (tieadj, [1; 0; 18]); %!test %! ## TOL ties a pair whose gap is within the SUM of their two tolerances, and %! ## separates it beyond. Boundaries measured against R2024a. %! tol = [eps(1), 1e-10, 3e-10, eps(3)]; %! assert_equal (tiedrank ([1.0, 1.9, 1.9+3.9e-10, 3.0], 0, 0, tol), ... %! [1, 2.5, 2.5, 4]); %! assert_equal (tiedrank ([1.0, 1.9, 1.9+4.1e-10, 3.0], 0, 0, tol), ... %! [1, 2, 3, 4]); %!test %! ## A scalar TOL applies to every element, and the default 0 is exact %! assert_equal (tiedrank ([1, 1+1e-9, 2], 0, 0, 1e-9), [1.5, 1.5, 3]); %! assert_equal (tiedrank ([1, 1+1e-9, 2], 0, 0, 0), [1, 2, 3]); %! assert_equal (tiedrank ([1, 1+1e-9, 2]), [1, 2, 3]); %!test %! ## Exact equality still ties whatever TOL says, infinities included %! assert_equal (tiedrank ([Inf, Inf, 1]), [2.5, 2.5, 1]); %! assert_equal (tiedrank ([Inf, Inf, 1], 0, 0, 0), [2.5, 2.5, 1]); %!test %! ## An empty TOL takes the default %! [r, tieadj] = tiedrank ([10, 20, 30, 40, 20], [], 1, []); %! assert_equal (r, [1, 2.5, 2, 1, 2.5]); %! assert_equal (tieadj, 3); %!test %! ## An empty flag keeps its default, so BIDIR can be set on its own %! [r, tieadj] = tiedrank ([10, 20, 30, 40, 20], [], 1); %! assert_equal (r, [1, 2.5, 2, 1, 2.5]); %! assert_equal (tieadj, 3); %!test %! ## A flag with a zero element is not set %! [~, tieadj] = tiedrank ([10, 20, 30, 40, 20], [1, 0]); %! assert_equal (tieadj, 3); %!test %! ## A flag whose elements are all nonzero is set %! [~, tieadj] = tiedrank ([10, 20, 30, 40, 20], [1, 1]); %! assert_equal (tieadj, [1; 0; 18]); %!test %! ## A character flag is read by its character codes %! [~, tieadj] = tiedrank ([10, 20, 30, 40, 20], 'A'); %! assert_equal (tieadj, [1; 0; 18]); %!test %! ## BIDIR is read the same way %! r = tiedrank ([10, 20, 30, 40, 20], 0, [1, 1]); %! assert_equal (r, [1, 2.5, 2, 1, 2.5]); ## Test input validation %!test %! ## A matrix is ranked column by column, as MATLAB does, and TIEADJ carries %! ## one entry per column. %! x = [3, 1; 1, 4; 4, 1; 1, 5; 5, 9]; %! [r, tieadj] = tiedrank (x); %! assert_equal (r, [3, 1.5; 1.5, 3; 4, 1.5; 1.5, 4; 5, 5]); %! assert_equal (tieadj, [3, 3]); %! [r1, t1] = tiedrank (x(:,1)); %! [r2, t2] = tiedrank (x(:,2)); %! assert_equal (r, [r1, r2]); %! assert_equal (tieadj, [t1, t2]); %!test %! ## With the tie flag TIEADJ gains its three rows per column %! x = [3, 1; 1, 4; 4, 1; 1, 5; 5, 9]; %! [r, tieadj] = tiedrank (x, 1); %! assert_equal (size (tieadj), [3, 2]); %! [~, t1] = tiedrank (x(:,1), 1); %! assert_equal (tieadj(:,1), t1); %!test %! ## An empty matrix is ranked, not refused %! [r, tieadj] = tiedrank (zeros (0, 2)); %! assert_equal (size (r), [0, 2]); %! assert_equal (size (tieadj), [1, 2]); %!test %! ## An N-D array is ranked along its first dimension, as MATLAB does, and %! ## TIEADJ keeps the higher dimensions of X. Verified against R2024a. %! y = cat (3, [1, 2; 3, 4], [5, 6; 7, 8]); %! [r, tieadj] = tiedrank (y); %! assert_equal (r(:,:,1), [1, 1; 2, 2]); %! assert_equal (r(:,:,2), [1, 1; 2, 2]); %! assert_equal (size (tieadj), [1, 2, 2]); %!error ... %! tiedrank ([1, 2, 3, 4, 5], {}) %!error ... %! tiedrank ([1, 2, 3, 4, 5], struct ()) %!error ... %! tiedrank ([1, 2, 3, 4, 5], 1i) %!error ... %! tiedrank ([1, 2, 3, 4, 5], [1, NaN]) %!error ... %! tiedrank ([1, 2, 3, 4, 5], 0, {}) %!error ... %! tiedrank ([1, 2, 3, 4, 5], 0, 1i) %!error ... %! tiedrank ([1, 2, 3, 4, 5], 0, NaN) %!error ... %! tiedrank ([1, 2, 3, 4, 5], 0, 0, -1) %!error ... %! tiedrank ([1, 2, 3, 4, 5], 0, 0, 'A') %!error ... %! tiedrank ([1, 2, 3, 4, 5], 0, 0, [1, 2]) statistics-release-2.1.2/inst/Descriptive_Statistics/000077500000000000000000000000001527311104300227715ustar00rootroot00000000000000statistics-release-2.1.2/inst/Descriptive_Statistics/cdfcalc.m000066400000000000000000000060031527311104300245250ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{yCDF}, @var{xCDF}, @var{n}, @var{emsg}, @var{eid}] =} cdfcalc (@var{x}) ## ## Calculate an empirical cumulative distribution function. ## ## @code{[@var{yCDF}, @var{xCDF}] = cdfcalc (@var{x})} calculates an empirical ## cumulative distribution function (CDF) of the observations in the data sample ## vector @var{x}. @var{x} may be a row or column vector, and represents a ## random sample of observations from some underlying distribution. On return ## @var{xCDF} is the set of @var{x} values at which the CDF increases. ## At XCDF(i), the function increases from YCDF(i) to YCDF(i+1). ## ## @code{[@var{yCDF}, @var{xCDF}, @var{n}] = cdfcalc (@var{x})} also returns ## @var{n}, the sample size. ## ## @code{[@var{yCDF}, @var{xCDF}, @var{n}, @var{emsg}, @var{eid}] = cdfcalc ## (@var{x})} also returns an error message and error id if @var{x} is not a ## vector or if it contains no values other than NaN. ## ## @seealso{cdfplot} ## @end deftypefn function [yCDF, xCDF, n, emsg, eid] = cdfcalc (x) ## Check number of input and output argument narginchk (1,1); nargoutchk (2,5); ## Add defaults yCDF = []; xCDF = []; n = 0; ## Check that x is a vector if (! isvector (x)) warning ("cdfcalc: vector required as input."); emsg = 'VectorRequired'; eid = 'VectorRequired'; return endif ## Remove NaNs and check if there are remaining data to calculate ecdf x = x(! isnan (x)); n = length (x); if (n == 0) warning ("cdfcalc: not enough data."); emsg = 'NotEnoughData'; eid = 'NotEnoughData'; return endif ## Sort data in ascending order x = sort (x(:)); ## Get cumulative sums yCDF = (1:n)' / n; ## Remove duplicates, keep the last one keep_idx = ([diff(x(:)); 1] > 0); xCDF = x(keep_idx); yCDF = [0; yCDF(keep_idx)]; emsg = ''; eid = ''; endfunction %!test %! x = [2, 4, 3, 2, 4, 3, 2, 5, 6, 4]; %! [yCDF, xCDF, n, emsg, eid] = cdfcalc (x); %! assert_equal (yCDF, [0, 0.3, 0.5, 0.8, 0.9, 1]'); %! assert_equal (xCDF, [2, 3, 4, 5, 6]'); %! assert_equal (n, 10); %!shared x %! x = [2, 4, 3, 2, 4, 3, 2, 5, 6, 4]; %!error yCDF = cdfcalc (x); %!error [yCDF, xCDF] = cdfcalc (); %!error [yCDF, xCDF] = cdfcalc (x, x); %!warning [yCDF, xCDF] = cdfcalc (ones (10,2)); statistics-release-2.1.2/inst/Descriptive_Statistics/cl_multinom.m000066400000000000000000000132101527311104300254660ustar00rootroot00000000000000## Copyright (C) 2009 Levente Torok ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{CL} =} cl_multinom (@var{X}, @var{N}, @var{b}) ## @deftypefnx {statistics} {@var{CL} =} cl_multinom (@var{X}, @var{N}, @var{b}, @var{method}) ## ## Confidence level of multinomial portions. ## ## @code{cl_multinom} returns confidence level of multinomial parameters ## estimated as @math{p = X / sum(X)} with predefined confidence interval ## @var{b}. Finite population is also considered. ## ## This function calculates the level of confidence at which the samples ## represent the true distribution given that there is a predefined tolerance ## (confidence interval). This is the upside down case of the typical exercises ## at which we want to get the confidence interval given the confidence level ## (and the estimated parameters of the underlying distribution). ## But once we accept (lets say at elections) that we have a standard predefined ## maximal acceptable error rate (e.g. @var{b}=0.02 ) in the estimation and we ## just want to know that how sure we can be that the measured proportions are ## the same as in the entire population (ie. the expected value and mean of the ## samples are roughly the same) we need to use this function. ## ## @subheading Arguments ## @multitable @columnfractions 0.1 0.10 0.78 ## @headitem Variable @tab Type @tab Description ## @item @var{X} @tab int vector @tab sample frequencies bins. ## @item @var{N} @tab int scalar @tab Population size that was sampled ## by @var{X}. If @qcode{N < sum (@var{X})}, infinite number assumed. ## @item @var{b} @tab real vector @tab confidence interval. If vector, ## it should be the size of @var{X} containing confidence interval for each ## cells. If scalar, each cell will have the same value of b unless it is zero ## or -1. If value is 0, @var{b} = 0.02 is assumed which is standard choice at ## elections otherwise it is calculated in a way that one sample in a cell ## alteration defines the confidence interval. ## @item @var{method} @tab string @tab An optional argument ## for defining the calculation method. Available choices are ## @qcode{'bromaghin'} (default), @qcode{'cochran'}, and @qcode{agresti_cull}. ## @end multitable ## ## Note! The @qcode{agresti_cull} method is not exactly the solution at ## reference given below but an adjustment of the solutions above. ## ## @subheading Returns ## Confidence level. ## ## @subheading Example ## CL = cl_multinom ([27; 43; 19; 11], 10000, 0.05) ## returns 0.69 confidence level. ## ## @subheading References ## @enumerate ## @item ## "bromaghin" calculation type (default) is based on the article: ## ## Jeffrey F. Bromaghin, "Sample Size Determination for Interval Estimation ## of Multinomial Probabilities", The American Statistician vol 47, 1993, ## pp 203-206. ## ## @item ## "cochran" calculation type is based on article: ## ## Robert T. Tortora, "A Note on Sample Size Estimation for Multinomial ## Populations", The American Statistician, , Vol 32. 1978, pp 100-102. ## ## @item ## "agresti_cull" calculation type is based on article: ## ## A. Agresti and B.A. Coull, "Approximate is better than 'exact' for ## interval estimation of binomial portions", The American Statistician, ## Vol. 52, 1998, pp 119-126 ## @end enumerate ## ## @end deftypefn function CL = cl_multinom (X, N, b = 0.05, method = 'bromaghin') if (nargin < 2 || nargin > 4) print_usage; elseif (! ischar (method)) error ("cl_multinom: argument method must be a string."); endif k = rows (X); nn = sum (X); p = X / nn; if (isscalar (b)) if (b==0) b=0.02; endif b = ones (rows (X), 1 ) * b; if (b<0) b = 1 ./ max (X, 1); endif endif bb = b .* b; if (N == nn) CL = 1; return; endif if (N < nn) fpc = 1; else fpc = (N - 1) / (N - nn); # finite population correction tag endif beta = p .* (1 - p); switch lower (method) case 'cochran' t = sqrt (fpc * nn * bb ./ beta); alpha = (1 - normcdf (t)) * 2; case 'bromaghin' t = sqrt (fpc * (nn * 2 * bb ) ./ ... (beta - 2 * bb + sqrt (beta .* beta - bb .* (4 * beta - 1)))); alpha = (1 - normcdf (t)) * 2; case 'agresti_cull' ts = fpc * nn * bb ./ beta ; if (k <= 2) alpha = 1 - chi2cdf (ts, k - 1); # adjusted Wilson interval else alpha = 1 - chi2cdf (ts / k, 1); # Goodman interval with Bonferroni arg. endif otherwise error ("cl_multinom: unknown calculation type '%s'.", method); endswitch CL = 1 - max ( alpha ); endfunction %!demo %! CL = cl_multinom ([27; 43; 19; 11], 10000, 0.05) ## Test input validation %!error cl_multinom (); %!error cl_multinom (1, 2, 3, 4, 5); %!error ... %! cl_multinom (1, 2, 3, 4); %!error ... %! cl_multinom (1, 2, 3, 'some string'); statistics-release-2.1.2/inst/Descriptive_Statistics/cohenKappa.m000066400000000000000000000375031527311104300252300ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{kappa} =} cohenKappa (@var{x}, @var{y}) ## @deftypefnx {statistics} {@var{kappa} =} cohenKappa (@var{T}) ## @deftypefnx {statistics} {@var{kappa} =} cohenKappa (@dots{}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{kappa}, @var{ci}] =} cohenKappa (@dots{}) ## @deftypefnx {statistics} {[@var{kappa}, @var{ci}, @var{stats}] =} cohenKappa (@dots{}) ## ## Cohen's kappa, the agreement between two raters beyond chance. ## ## @code{@var{kappa} = cohenKappa (@var{x}, @var{y})} returns Cohen's kappa ## for two raters who each assigned the same subjects to categories, the ## ratings of the first in the vector @var{x} and of the second in @var{y}. ## The ratings may be numeric or logical, categorical, or text: a character ## matrix with one rating per row, a cell array of character vectors, or a ## string array, the two raters' of the same kind. A subject whose rating by ## either rater is missing, @qcode{NaN}, an undefined category, an empty text ## or a missing string, is left out. ## ## @code{@var{kappa} = cohenKappa (@var{T})} takes the agreement table ## instead, a square matrix of counts whose element @code{@var{T}(i,j)} is ## the number of subjects the first rater assigned to category @math{i} and ## the second to category @math{j}. ## ## With @code{po} the proportion of subjects on whom the raters agree and ## @code{pe} the proportion expected by chance from each rater's own ## frequencies of the categories, ## @tex ## $$ \kappa = \frac{p_o - p_e}{1 - p_e}, $$ ## ## @end tex ## @ifnottex ## @code{kappa = (po - pe) / (1 - pe)}, ## @end ifnottex ## 1 for perfect agreement, 0 for agreement no better than chance, and ## negative for less. Weighted kappa counts partial agreement between ## ordered categories, weighting each pair of categories @math{i} and ## @math{j} by @code{w(i,j)}, 1 when they are the same, in both @code{po} ## and @code{pe}. ## ## The following @qcode{Name-Value} pairs are supported: ## ## @multitable @columnfractions 0.2 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Weights'} @tab @qcode{'none'} (default), unweighted kappa; ## @qcode{'linear'}, @code{w(i,j) = 1 - abs (i - j) / (c - 1)}; ## @qcode{'quadratic'}, @code{w(i,j) = 1 - (i - j)^2 / (c - 1)^2}, for ## @math{c} categories in their order; or a @math{c}-by-@math{c} matrix of ## agreement weights between 0 and 1, with ones on its diagonal. ## ## @item @qcode{'Categories'} @tab The categories, in their order: numeric ## for numeric ratings, text for text or categorical ratings. The default is ## the sorted values of numeric ratings, the categories of categorical ## ratings in their own order, unused ones included, and the sorted texts of ## text ratings. A category neither rater used counts towards @math{c} ## and spaces the others: placed between used categories it changes linear ## and quadratic kappa, placed after them it does not. Not accepted with ## @var{T}, whose rows and columns are the categories. ## ## @item @qcode{'Alpha'} @tab The significance level of the confidence ## interval, a scalar in the range @math{(0,1)}. The default is ## @math{0.05}. ## @end multitable ## ## @code{[@var{kappa}, @var{ci}] = cohenKappa (@dots{})} also returns the ## @math{100 (1 - alpha)} percent confidence interval of kappa, from its ## large-sample standard error (Fleiss, Cohen and Everitt, 1969), clipped to ## @math{[-1, 1]}. ## ## @code{[@var{kappa}, @var{ci}, @var{stats}] = cohenKappa (@dots{})} also ## returns a structure with the fields ## ## @multitable @columnfractions 0.2 0.8 ## @item @qcode{po} @tab the observed agreement, weighted if weights are ## given ## @item @qcode{pe} @tab the agreement expected by chance ## @item @qcode{se} @tab the standard error of kappa that @var{ci} is based ## on ## @item @qcode{zval} @tab the statistic of the test that kappa is 0, kappa ## over its standard error when there is no agreement beyond chance ## @item @qcode{p} @tab the two-sided p-value of that test ## @item @qcode{n} @tab the number of subjects ## @item @qcode{table} @tab the agreement table ## @item @qcode{categories} @tab the categories, a numeric column or a cell ## array of character vectors ## @item @qcode{weights} @tab the matrix of weights used ## @end multitable ## ## Kappa is undefined when chance alone predicts complete agreement, as when ## both raters use a single category, and such ratings are refused. ## Agreement among more than two raters is measured by @code{fleissKappa}. ## MATLAB has no Cohen's kappa, so @code{cohenKappa} is specific to Octave. ## ## References: ## @enumerate ## @item ## J. Cohen. A coefficient of agreement for nominal scales. ## @emph{Educational and Psychological Measurement}, 20(1):37--46, 1960. ## @item ## J. Cohen. Weighted kappa: nominal scale agreement with provision for ## scaled disagreement or partial credit. @emph{Psychological Bulletin}, ## 70(4):213--220, 1968. ## @item ## J. L. Fleiss, J. Cohen and B. S. Everitt. Large sample standard errors of ## kappa and weighted kappa. @emph{Psychological Bulletin}, 72(5):323--327, ## 1969. ## @end enumerate ## ## @seealso{fleissKappa, crosstab, limitsOfAgreement} ## @end deftypefn function [kappa, ci, stats] = cohenKappa (varargin) if (nargin < 1) print_usage (); endif ## An agreement table, or the two raters' ratings T = varargin{1}; istab = (isnumeric (T) && ! isvector (T) && (nargin == 1 || ischar (varargin{2}) || isstring (varargin{2}))); if (istab) args = varargin(2:end); else if (nargin < 2) print_usage (); endif args = varargin(3:end); endif ## Parse optional Name-Value paired arguments optNames = {'Weights', 'Categories', 'Alpha'}; dfValues = {'none', [], 0.05}; [wt, cats, alpha, rest] = parsePairedArguments (optNames, dfValues, args(:)); if (! isempty (rest)) if (mod (numel (rest), 2) == 0 && ischar (rest{1})) error ("cohenKappa: unknown parameter name '%s'.", rest{1}); endif error ("cohenKappa: optional arguments must be Name-Value pairs."); endif if (! (isfloat (alpha) && isreal (alpha) && isscalar (alpha) && alpha > 0 && alpha < 1)) error ("cohenKappa: 'Alpha' must be a scalar between 0 and 1."); endif if (istab) if (! isempty (cats)) error ("cohenKappa: 'Categories' cannot be given with a table."); endif if (! (isreal (T) && rows (T) == columns (T) && ndims (T) == 2 && all (isfinite (T(:))) && all (T(:) >= 0))) error (strcat ("cohenKappa: T must be a square matrix of", ... " non-negative counts.")); endif T = double (T); c = rows (T); cats = (1:c)'; else [codes, cats, errmsg] = ratingcodes (varargin(1:2), cats); if (! isempty (errmsg)) error ("cohenKappa: %s", errmsg); endif codes = codes(! any (isnan (codes), 2),:); c = numel (cats); T = zeros (c, c); if (! isempty (codes)) T = accumarray (codes, 1, [c, c]); endif endif n = sum (T(:)); if (n == 0) error ("cohenKappa: no subject was rated by both raters."); endif ## The agreement weights if (ischar (wt) || (isstring (wt) && isscalar (wt))) switch (lower (char (wt))) case 'none' W = eye (c); case 'linear' W = 1 - abs ((1:c)' - (1:c)) / max (c - 1, 1); case 'quadratic' W = 1 - ((1:c)' - (1:c)) .^ 2 / max (c - 1, 1) ^ 2; otherwise error (strcat ("cohenKappa: 'Weights' must be 'none', 'linear',", ... " 'quadratic' or a matrix of weights.")); endswitch elseif (isnumeric (wt) && isreal (wt) && isequal (size (wt), [c, c]) && all (wt(:) >= 0 & wt(:) <= 1) && all (diag (wt) == 1)) W = double (wt); else error (strcat ("cohenKappa: a matrix of 'Weights' must be square, one", ... " row per category, between 0 and 1, with ones on its", ... " diagonal.")); endif ## Agreement observed and expected by chance P = T / n; pr = sum (P, 2); pc = sum (P, 1); po = sum (W(:) .* P(:)); E = pr * pc; pe = sum (W(:) .* E(:)); if (pe >= 1) error (strcat ("cohenKappa: kappa is undefined when chance predicts", ... " complete agreement, as when both raters use a single", ... " category.")); endif kappa = (po - pe) / (1 - pe); ## Large-sample variances of Fleiss, Cohen and Everitt, with the weights' ## means over each row and column taken against the other rater's margin wr = W * pc'; wc = pr' * W; D = wr + wc; v = (sum (sum (P .* (W - D * (1 - kappa)) .^ 2)) ... - (kappa - pe * (1 - kappa)) ^ 2) / (n * (1 - pe) ^ 2); v0 = (sum (sum (E .* (W - D) .^ 2)) - pe ^ 2) / (n * (1 - pe) ^ 2); se = sqrt (max (v, 0)); se0 = sqrt (max (v0, 0)); ci = min (max (kappa + [-1, 1] * norminv (1 - alpha / 2) * se, -1), 1); zval = kappa / se0; p = 2 * normcdf (-abs (zval)); stats = struct ('po', po, 'pe', pe, 'se', se, 'zval', zval, 'p', p, ... 'n', n, 'table', T, 'categories', {cats}, 'weights', W); endfunction %!demo %! ## Two raters who sorted 20 subjects into three ordered grades %! x = [1, 2, 3, 1, 2, 3, 2, 2, 1, 3, 3, 1, 2, 1, 3, 2, 2, 3, 1, 1]; %! y = [1, 2, 2, 1, 3, 3, 2, 1, 1, 3, 2, 1, 2, 2, 3, 2, 3, 3, 1, 2]; %! [kappa, ci] = cohenKappa (x, y) %! ## Partial agreement between neighbouring grades counted %! [kappa, ci] = cohenKappa (x, y, 'Weights', 'quadratic') ## Test output ## Values checked against R 4.5.0, irr 0.85 (kappa2) and psych (cohen.kappa) %!shared x, y %! x = [1, 2, 3, 1, 2, 3, 2, 2, 1, 3, 3, 1, 2, 1, 3, 2, 2, 3, 1, 1]; %! y = [1, 2, 2, 1, 3, 3, 2, 1, 1, 3, 2, 1, 2, 2, 3, 2, 3, 3, 1, 2]; %!test %! [k, ci, s] = cohenKappa (x, y); %! assert_equal (k, 0.473684210526316, 1e-14); %! assert_equal (ci, [0.156627045921977, 0.790741375130655], 1e-14); %! assert_equal (s.zval, 3.00357781880274, 1e-13); %! assert_equal (s.p, 0.00266825301537432, 1e-14); %! assert_equal (s.n, 20); %! assert_equal (s.categories, [1; 2; 3]); %! assert_equal (s.table, [5, 2, 0; 1, 4, 2; 0, 2, 4]); %!test %! [k, ci, s] = cohenKappa (x, y, 'Weights', 'linear'); %! assert_equal (k, 0.593023255813953, 1e-14); %! assert_equal (ci, [0.330633074635, 0.855413436992907], 1e-14); %! assert_equal (s.zval, 3.42637031579504, 1e-13); %! assert_equal (s.p, 0.000611705786193184, 1e-14); %!test %! [k, ci, s] = cohenKappa (x, y, 'Weights', 'quadratic'); %! assert_equal (k, 0.72, 1e-14); %! assert_equal (ci, [0.521837223191117, 0.918162776808883], 1e-14); %! assert_equal (s.zval, 3.22873201500379, 1e-13); %! assert_equal (s.p, 0.00124340351789742, 1e-14); %!test %! W = 1 - abs ((1:3)' - (1:3)) / 2; %! assert_equal (cohenKappa (x, y, 'Weights', W), ... %! cohenKappa (x, y, 'Weights', 'linear'), 1e-15); %!test %! ## A category after the used ones leaves weighted kappa unchanged %! assert_equal (cohenKappa (x, y, 'Weights', 'linear', 'Categories', 1:4), ... %! 0.593023255813954, 1e-14); %!test %! ## One between them does not %! x4 = x; %! x4(x == 3) = 4; %! y4 = y; %! y4(y == 3) = 4; %! [k, ci] = cohenKappa (x4, y4, 'Weights', 'linear', 'Categories', 1:4); %! assert_equal (k, 0.5703125, 1e-14); %! assert_equal (ci, [0.28133843832902, 0.85928656167098], 1e-14); %!test %! [~, ~, s] = cohenKappa (x, y); %! [k, ci, s2] = cohenKappa (s.table); %! assert_equal (k, 0.473684210526316, 1e-14); %! assert_equal (ci, [0.156627045921977, 0.790741375130655], 1e-14); %! assert_equal (s2.zval, s.zval, 1e-14); %!test %! a = {'cat', 'dog', 'dog', 'bird', '', 'cat', 'bird', 'dog', 'cat', 'dog'}; %! b = {'cat', 'dog', 'cat', 'bird', 'dog', 'cat', 'dog', 'dog', 'bird', ''}; %! [k, ci, s] = cohenKappa (a, b); %! assert_equal (k, 0.428571428571429, 1e-14); %! assert_equal (ci, [-0.0756238505834626, 0.93276670772632], 1e-14); %! assert_equal (s.zval, 1.69705627484771, 1e-13); %! assert_equal (s.p, 0.0896860217703646, 1e-14); %! assert_equal (s.n, 8); %! assert_equal (s.categories, {'bird'; 'cat'; 'dog'}); %!test %! a = {'cat', 'dog', 'dog', 'bird', '', 'cat', 'bird', 'dog', 'cat', 'dog'}; %! b = {'cat', 'dog', 'cat', 'bird', 'dog', 'cat', 'dog', 'dog', 'bird', ''}; %! k = cohenKappa (char (a), char (b)); %! assert_equal (k, 0.428571428571429, 1e-14); %!test %! a = {'cat', 'dog', 'dog', 'bird', '', 'cat', 'bird', 'dog', 'cat', 'dog'}; %! b = {'cat', 'dog', 'cat', 'bird', 'dog', 'cat', 'dog', 'dog', 'bird', ''}; %! k = cohenKappa (categorical (a), categorical (b)); %! assert_equal (k, 0.428571428571429, 1e-14); %!test %! k = cohenKappa (logical (x == 1), logical (y == 1)); %! assert_equal (k, cohenKappa (double (x == 1), double (y == 1))); %!test %! [k, ci, s] = cohenKappa ([1, 2, 1, 2], [1, 2, 1, 2]); %! assert_equal (k, 1); %! assert_equal (ci, [1, 1]); %! assert_equal (s.zval, 2); %!test %! [~, ci] = cohenKappa (x, y, 'Alpha', 0.1); %! assert_equal (ci, 0.473684210526316 + [-1, 1] * norminv (0.95) ... %! * 0.161766831995509, 1e-12); ## Test input validation %!error cohenKappa () %!error cohenKappa ([1, 2, 3]) %!error cohenKappa (x, y, 'Foo', 1) %!error ... %! cohenKappa (x, y, 'Alpha') %!error ... %! cohenKappa (x, y, 'Alpha', 1) %!error ... %! cohenKappa (x, y, 'Weights', 'cubic') %!error ... %! cohenKappa (x, y, 'Weights', eye (2)) %!error ... %! cohenKappa (x, y, 'Weights', zeros (3)) %!error ... %! cohenKappa (eye (2), 'Categories', [1, 2]) %!error ... %! cohenKappa ([1, 2, 3; 4, 5, 6]) %!error ... %! cohenKappa ([1, -2; 3, 4]) %!error ... %! cohenKappa ([1, 2], {'a', 'b'}) %!error ... %! cohenKappa ([1, 2, 3], [1, 2]) %!error ... %! cohenKappa ([1, 2] + 1i, [1, 2]) %!error ... %! cohenKappa ([1, 2, 3], [1, 2, 2], 'Categories', [1, 2]) %!error ... %! cohenKappa ([1, 2, 3], [1, 2, 2], 'Categories', [1, 2, 2, 3]) %!error ... %! cohenKappa ([1, 2, 3], [1, 2, 2], 'Categories', {'a'}) %!error ... %! cohenKappa ({'a', 'b'}, {'a', 'a'}, 'Categories', [1, 2]) %!error ... %! cohenKappa ([1, 1, 1], [1, 1, 1]) %!error ... %! cohenKappa ([1, NaN], [NaN, 2]) statistics-release-2.1.2/inst/Descriptive_Statistics/corr.m000066400000000000000000000646561527311104300241350ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{rho} =} corr (@var{x}) ## @deftypefnx {statistics} {@var{rho} =} corr (@var{x}, @var{y}) ## @deftypefnx {statistics} {@var{rho} =} corr (@dots{}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{rho}, @var{pval}] =} corr (@dots{}) ## ## Linear or rank correlation coefficients. ## ## @code{@var{rho} = corr (@var{x})} returns the matrix of pairwise correlation ## coefficients between the columns of @var{x}, whose rows are observations and ## whose columns are variables. For an @math{n}-by-@math{k} matrix @var{x}, ## @var{rho} is @math{k}-by-@math{k} and @code{@var{rho}(i,j)} is the ## correlation between the @math{i}-th and the @math{j}-th column of @var{x}. ## ## @code{@var{rho} = corr (@var{x}, @var{y})} returns the correlations between ## the columns of @var{x} and the columns of @var{y}. For an ## @math{n}-by-@math{k1} matrix @var{x} and an @math{n}-by-@math{k2} matrix ## @var{y}, @var{rho} is @math{k1}-by-@math{k2} and @code{@var{rho}(i,j)} is the ## correlation between the @math{i}-th column of @var{x} and the @math{j}-th ## column of @var{y}. @var{x} and @var{y} must have the same number of rows. ## ## The following @qcode{Name-Value} pairs are supported: ## ## @multitable @columnfractions 0.2 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Type'} @tab The coefficient to compute: @qcode{'Pearson'} ## (default) for the linear correlation coefficient, @qcode{'Kendall'} for ## Kendall's tau-b, or @qcode{'Spearman'} for Spearman's rho. ## ## @item @qcode{'Rows'} @tab How missing values are handled: @qcode{'all'} ## (default) uses every row, so an entry is @qcode{NaN} whenever either of its ## columns holds a @qcode{NaN}; @qcode{'complete'} first removes every row ## holding a @qcode{NaN} in any column; @qcode{'pairwise'} computes each entry ## from the rows where its own pair of columns is present. ## ## @item @qcode{'Tail'} @tab The alternative hypothesis of the test reported in ## @var{pval}: @qcode{'both'} (default) for a correlation different from zero, ## @qcode{'right'} for a positive one, or @qcode{'left'} for a negative one. ## ## @item @qcode{'Weights'} @tab A column vector of @math{n} nonnegative ## observation weights. The default weights every observation equally. ## @end multitable ## ## Option values are matched without regard to case, and an unambiguous ## abbreviation is accepted, so @qcode{'spear'} selects @qcode{'Spearman'}. ## ## @code{[@var{rho}, @var{pval}] = corr (@dots{})} also returns @var{pval}, the ## p-value of a test of the null hypothesis that the corresponding correlation ## is zero, against the alternative named by @qcode{'Tail'}. A @var{pval} entry ## is @qcode{NaN} wherever its @var{rho} entry is, and every entry is ## @qcode{NaN} when @qcode{'Weights'} is given. ## ## The p-value is computed as follows: ## ## @itemize ## @item @qcode{'Pearson'}: from a Student's t distribution with @math{n-2} ## degrees of freedom applied to ## @code{@var{t} = @var{rho} * sqrt ((n - 2) / (1 - @var{rho}^2))}, which is ## exact when the data are normally distributed. ## ## @item @qcode{'Kendall'}: from the exact permutation distribution when ## @math{n < 10}, and for a sample of fewer than 50 observations holding no ## tied values; otherwise from a normal approximation with a continuity ## correction and the usual correction for ties. ## ## @item @qcode{'Spearman'}: from the exact permutation distribution when ## @math{n < 10}; from the approximation of Best and Roberts (algorithm AS 89) ## for a larger sample holding no tied values; and from the same Student's t ## transformation as the Pearson coefficient for a larger sample that does ## hold them. ## @end itemize ## ## The exact p-value is the proportion of the @math{n!} orderings of one ## variable giving a coefficient as extreme as the observed one, so tied ## values need no special treatment there. ## ## A column of constant values has no defined correlation, so its entries, ## the diagonal one included, are @qcode{NaN}. @qcode{Inf} makes a Pearson ## coefficient @qcode{NaN}, while the rank coefficients order it like any other ## value. ## ## Two deviations from MATLAB. A character array is refused, where MATLAB ## correlates the character codes; its acceptance there is incidental, ## correlation having no meaning for text. An array of more than two ## dimensions is refused, where MATLAB refuses most of them from inside a ## matrix multiplication but flattens a leading singleton dimension and ## answers @qcode{NaN}; core Octave's @code{corr} answers @qcode{NaN} there ## too. ## ## References: ## @enumerate ## @item ## M. G. Kendall. A new measure of rank correlation. @emph{Biometrika}, ## 30(1-2):81--93, 1938. ## @item ## D. J. Best and D. E. Roberts. Algorithm AS 89: the upper tail probabilities ## of Spearman's rho. @emph{Applied Statistics}, 24(3):377--379, 1975. ## @end enumerate ## ## @seealso{corrcoef, cov, partialcorr, tiedrank, kendall, spearman} ## @end deftypefn function [rho, pval] = corr (x, varargin) ## Input validation if (nargin < 1) print_usage (); endif ## Y is present unless the second argument opens the Name-Value pairs y = []; haveY = false; if (numel (varargin) > 0 ... && ! (ischar (varargin{1}) || isstring (varargin{1}))) y = varargin{1}; varargin(1) = []; haveY = true; endif if (! (isnumeric (x) || islogical (x))) error ("corr: X must be a numeric or logical matrix."); endif if (ndims (x) > 2) error ("corr: X must be a matrix or a vector."); endif issingle = isa (x, 'single'); x = double (x); if (columns (x) == 0) error ("corr: X must have at least one column."); endif n = rows (x); if (haveY) if (! (isnumeric (y) || islogical (y))) error ("corr: Y must be a numeric or logical matrix."); endif if (ndims (y) > 2) error ("corr: Y must be a matrix or a vector."); endif issingle = issingle || isa (y, 'single'); y = double (y); if (rows (y) != n) error ("corr: X and Y must have the same number of rows."); endif endif ## Parse optional Name-Value paired arguments optNames = {'Type', 'Rows', 'Tail', 'Weights'}; dfValues = {'Pearson', 'all', 'both', []}; [type, rowopt, tail, w, args] = parsePairedArguments (optNames, dfValues, ... varargin(:)); if (! isempty (args)) error (strcat ("corr: optional arguments must be 'Type', 'Rows',", ... " 'Tail' or 'Weights' Name-Value pairs.")); endif type = matchopt_ (type, {'Pearson', 'Kendall', 'Spearman'}, 'Type'); rowopt = matchopt_ (rowopt, {'all', 'complete', 'pairwise'}, 'Rows'); tail = matchopt_ (tail, {'both', 'right', 'left'}, 'Tail'); if (! isempty (w)) if (! (isa (w, 'double') || isa (w, 'single')) || ! isreal (w) ... || ! iscolumn (w) || numel (w) != n) error ("corr: 'Weights' must be a column vector of %d real values.", n); endif if (any (w < 0)) error ("corr: 'Weights' must be nonnegative."); endif w = double (w); endif ## Both forms are computed as X against Y, the one-argument form against ## itself, which is what makes its result symmetric with a unit diagonal. if (haveY) b = y; else b = x; endif if (columns (b) == 0) rho = zeros (columns (x), 0); pval = zeros (columns (x), 0); return; endif ## Drop the rows holding a missing value before anything else if (strcmp (rowopt, 'complete')) keep = ! any (isnan ([x, b]), 2); x = x(keep,:); b = b(keep,:); if (! isempty (w)) w = w(keep); endif endif [rho, pval] = corr_compute_ (x, b, ! haveY, type, rowopt, tail, w, ... nargout > 1); if (issingle) rho = single (rho); pval = single (pval); endif endfunction ## Resolve an option value against the allowed spellings, without regard to ## case and accepting an unambiguous abbreviation. function out = matchopt_ (val, allowed, name) if (isstring (val) && isscalar (val)) val = char (val); endif if (! (ischar (val) && isrow (val))) val = ''; endif idx = strncmpi (val, allowed, numel (val)); if (isempty (val) || sum (idx) != 1) error ("corr: '%s' must be %s.", name, list_ (allowed)); endif out = allowed{idx}; endfunction ## Render the allowed spellings as "'a', 'b' or 'c'" function s = list_ (allowed) s = sprintf ("'%s', ", allowed{1:end-1}); s = sprintf ("%s or '%s'", s(1:end-2), allowed{end}); endfunction ## The coefficients and their p-values, column pair by column pair function [rho, pval] = corr_compute_ (x, b, symmetric, type, rowopt, tail, ... w, needp) k1 = columns (x); k2 = columns (b); rho = NaN (k1, k2); pval = NaN (k1, k2); pairwise = strcmp (rowopt, 'pairwise'); for i = 1:k1 if (symmetric) jj = i:k2; else jj = 1:k2; endif for j = jj a = x(:,i); c = b(:,j); wij = w; if (pairwise) keep = ! isnan (a) & ! isnan (c); a = a(keep); c = c(keep); if (! isempty (wij)) wij = wij(keep); endif endif [r, p] = corr_pair_ (a, c, type, tail, wij, needp); rho(i,j) = r; if (needp) pval(i,j) = p; endif endfor endfor if (symmetric) rho = triu (rho) + triu (rho, 1)'; ## A column correlates perfectly with itself unless it is degenerate, ## which the arithmetic above already reports as NaN. dd = find (! isnan (diag (rho))); di = sub2ind (size (rho), dd, dd); rho(di) = 1; if (needp) pval = triu (pval) + triu (pval, 1)'; ## A weighted correlation has no p-value at all, the diagonal included if (isempty (w)) pval(di) = 1; endif endif endif endfunction ## One coefficient and its p-value function [r, p] = corr_pair_ (a, c, type, tail, w, needp) n = numel (a); p = NaN; switch (type) case 'Pearson' r = wpearson_ (a, c, w); if (needp && isempty (w)) p = pearson_pval_ (r, n, tail); endif case 'Spearman' if (isempty (w)) ra = tiedrank (a); rc = tiedrank (c); else ra = wtiedrank_ (a, w); rc = wtiedrank_ (c, w); endif r = wpearson_ (ra, rc, w); if (needp && isempty (w)) p = spearman_pval_ (a, c, ra, rc, r, tail); endif case 'Kendall' [r, S, V] = wkendall_ (a, c, w); if (needp && isempty (w)) p = kendall_pval_ (a, c, r, S, V, tail); endif endswitch endfunction ## Pearson coefficient, weighted when W is given function r = wpearson_ (a, c, w) if (isempty (w)) ac = a - mean (a); cc = c - mean (c); r = (ac' * cc) / sqrt ((ac' * ac) * (cc' * cc)); else w = w / sum (w); ac = a - sum (w .* a); cc = c - sum (w .* c); r = sum (w .* ac .* cc) / sqrt (sum (w .* ac .^ 2) * sum (w .* cc .^ 2)); endif endfunction ## Midranks under observation weights: an observation of weight two occupies ## two consecutive ranks, so replicating it and ranking gives the same value. function r = wtiedrank_ (x, w) [xs, idx] = sort (x(:)); ws = w(idx); hi = cumsum (ws); lo = hi - ws; rs = (lo + hi) / 2; ## Tied values share the midrank of the block they form k = 1; n = numel (xs); while (k <= n) j = k; while (j < n && xs(j+1) == xs(k)) j++; endwhile if (j > k) rs(k:j) = (lo(k) + hi(j)) / 2; endif k = j + 1; endwhile r = zeros (size (rs)); r(idx) = rs; endfunction ## Kendall's tau-b with its score S and the variance of S under the null function [t, S, V] = wkendall_ (a, c, w) n = numel (a); sa = sign (a - a.'); sc = sign (c - c.'); if (isempty (w)) S = sum (sum (sa .* sc)) / 2; na = sum (sum (sa != 0)) / 2; nc = sum (sum (sc != 0)) / 2; else W = w * w.'; S = sum (sum (W .* sa .* sc)) / 2; na = sum (sum (W .* (sa != 0))) / 2; nc = sum (sum (W .* (sc != 0))) / 2; endif t = S / sqrt (na * nc); ta = tiecounts_ (a); tc = tiecounts_ (c); V = (n * (n - 1) * (2 * n + 5) - sum (ta .* (ta - 1) .* (2 * ta + 5)) ... - sum (tc .* (tc - 1) .* (2 * tc + 5))) / 18 ... + sum (ta .* (ta - 1) .* (ta - 2)) * sum (tc .* (tc - 1) .* (tc - 2)) ... / (9 * n * (n - 1) * (n - 2)) ... + sum (ta .* (ta - 1)) * sum (tc .* (tc - 1)) / (2 * n * (n - 1)); endfunction ## Sizes of the tied groups of X function t = tiecounts_ (x) [~, ~, j] = unique (x(:)); t = accumarray (j, 1); endfunction function tf = hasties_ (x) tf = numel (unique (x)) < numel (x); endfunction ## Student's t p-value of a Pearson coefficient function p = pearson_pval_ (r, n, tail) if (n < 3 || isnan (r)) p = NaN; return; endif ## The two-sided probability comes from the incomplete beta function ## directly, which keeps its precision in the far tail where doubling a ## computed t quantile does not. df = n - 2; t = r * sqrt (df / (1 - r ^ 2)); two = betainc (df / (df + t ^ 2), df / 2, 0.5); switch (tail) case 'both' p = two; case 'right' p = ifelse_ (t >= 0, two / 2, 1 - two / 2); case 'left' p = ifelse_ (t >= 0, 1 - two / 2, two / 2); endswitch endfunction function out = ifelse_ (cond, a, b) if (cond) out = a; else out = b; endif endfunction ## Spearman p-value: exact below ten observations, else AS 89 without ties ## and the t transformation with them function p = spearman_pval_ (a, c, ra, rc, r, tail) n = numel (a); if (n < 3 || isnan (r)) p = NaN; elseif (n < 10) P = perms (1:n); p = perm_pval_ (rc(P) * ra, rc' * ra, tail); elseif (hasties_ (a) || hasties_ (c)) p = pearson_pval_ (r, n, tail); else S = (1 - r) * (n ^ 3 - n) / 6; lower = as89_ (n, S); upper = 1 - as89_ (n, S + 2); switch (tail) case 'both' p = min (1, 2 * min (upper, lower)); case 'right' p = upper; case 'left' p = lower; endswitch endif endfunction ## Kendall p-value: exact below ten observations and for a smaller untied ## sample, else a normal approximation with a continuity correction function p = kendall_pval_ (a, c, t, S, V, tail) n = numel (a); ties = hasties_ (a) || hasties_ (c); if (n < 3 || isnan (t)) p = NaN; elseif (n < 10) p = perm_pval_ (kendall_scores_ (a, c), S, tail); elseif (! ties && n < 50) d = kendall_dist_ (n); p = disc_pval_ (d, t, tail); else switch (tail) case 'both' p = 2 * normcdf (-max ((abs (S) - 1) / sqrt (V), 0)); case 'right' p = normcdf (-(S - 1) / sqrt (V)); case 'left' p = normcdf ((S + 1) / sqrt (V)); endswitch p = min (p, 1); endif endfunction ## Kendall's score S under every ordering of C against A function S = kendall_scores_ (a, c) n = numel (a); P = perms (1:n); A = sign (a - a.'); C = sign (c - c.'); S = zeros (rows (P), 1); for i = 1:n-1 for j = i+1:n if (A(i,j) != 0) S += A(i,j) * C(sub2ind ([n, n], P(:,i), P(:,j))); endif endfor endfor endfunction ## Tail probabilities of a statistic enumerated over every ordering function p = perm_pval_ (S, s0, tail) tol = 1e-9 * max (1, abs (s0)); upper = mean (S >= s0 - tol); lower = mean (S <= s0 + tol); switch (tail) case 'both' p = min (1, 2 * min (upper, lower)); case 'right' p = upper; case 'left' p = lower; endswitch endfunction ## Tail probabilities of a discrete null distribution given as [value, prob] function p = disc_pval_ (d, r, tail) upper = sum (d(d(:,1) >= r - 1e-12, 2)); lower = sum (d(d(:,1) <= r + 1e-12, 2)); switch (tail) case 'both' p = min (1, 2 * min (upper, lower)); case 'right' p = upper; case 'left' p = lower; endswitch endfunction ## Null distribution of Kendall's tau for N untied observations, from the ## distribution of the number of inversions of a random permutation function d = kendall_dist_ (n) c = 1; for k = 1:n-1 c = conv (c, ones (1, k + 1)); endfor K = n * (n - 1) / 2; d = [(2 * (0:K)' / K) - 1, (c / sum (c))']; endfunction ## Upper tail probability of Spearman's S by algorithm AS 89 function q = as89_ (n, S) c = [0.2274, 0.2531, 0.1745, 0.0758, 0.1033, 0.3932, 0.0879, 0.0151, ... 0.0072, 0.0831, 0.0131, 4.6e-4]; b = 1 / n; x = (6 * (S - 1) * b / (n ^ 2 - 1) - 1) * sqrt (1 / b - 1); y = x * x; u = x * b * (c(1) + b * (c(2) + c(3) * b) ... + y * (-c(4) + b * (c(5) + c(6) * b) - y * b * (c(7) + c(8) * b ... - y * (c(9) - c(10) * b + y * b * (c(11) - c(12) * y))))); q = min (1, max (0, u / exp (y / 2) + normcdf (x, 0, 1, 'upper'))); endfunction %!demo %! ## Correlation between the columns of a matrix, with p-values %! x = [1 2; 3 5; 4 4; 7 8; 9 6]; %! [rho, pval] = corr (x) %!demo %! ## Spearman's rank correlation, which a monotone relation makes exact %! x = [1; 2; 3; 4; 5]; %! y = [1; 4; 9; 16; 25]; %! [rho, pval] = corr (x, y, 'Type', 'Spearman') ## Test output ## RHO and PVAL checked against corr in MATLAB R2024a Update 4 ## (Statistics and Machine Learning Toolbox 24.1) %!test # Pearson, one matrix %! x = [1 2; 3 5; 4 4; 7 8; 9 6]; %! [rho, pval] = corr (x); %! assert_equal (rho, [1, 0.80516104831610558; 0.80516104831610558, 1], 1e-14); %! assert_equal (pval, [1, 0.10016803410643096; 0.10016803410643096, 1], 1e-14); %!test # Pearson, two matrices %! x = [1 2; 3 5; 4 4; 7 8; 9 6]; %! y = [2 1; 5 4; 4 3; 6 9; 8 7]; %! [rho, pval] = corr (x, y); %! assert_equal (rho, [0.94518905671890663, 0.87745098039215708; ... %! 0.79999999999999993, 0.98019605881960692], 1e-14); %! assert_equal (pval, [0.015276771734465051, 0.050541693400160549; ... %! 0.10408803866182803, 0.0033355462806318316], 1e-14); %!test # the one-sided tails of the Pearson test %! x = [1; 3; 4; 7; 9]; %! y = [2; 5; 4; 8; 6]; %! [~, pboth] = corr (x, y); %! [~, pright] = corr (x, y, 'Tail', 'right'); %! [~, pleft] = corr (x, y, 'Tail', 'left'); %! assert_equal (pboth, 0.10016803410643098, 1e-14); %! assert_equal (pright, 0.050084017053215489, 1e-14); %! assert_equal (pleft, 0.94991598294678448, 1e-14); %!test # Kendall's tau-b, exact below ten observations %! x = [1 2; 3 5; 4 4; 7 8; 9 6]; %! [rho, pval] = corr (x, 'Type', 'Kendall'); %! assert_equal (rho, [1, 0.6; 0.6, 1], 1e-14); %! assert_equal (pval, [1, 0.23333333333333334; 0.23333333333333334, 1], 1e-14); %!test # Spearman's rho, exact below ten observations %! x = [1 2; 3 5; 4 4; 7 8; 9 6]; %! [rho, pval] = corr (x, 'Type', 'Spearman'); %! assert_equal (rho, [1, 0.79999999999999993; 0.79999999999999993, 1], 1e-14); %! assert_equal (pval, [1, 0.13333333333333333; 0.13333333333333333, 1], 1e-14); %!test # the one-sided tails of the exact rank tests %! x = [1; 3; 4; 7; 9]; %! y = [2; 5; 4; 8; 6]; %! [~, pk] = corr (x, y, 'Type', 'Kendall', 'Tail', 'right'); %! [~, ps] = corr (x, y, 'Type', 'Spearman', 'Tail', 'left'); %! assert_equal (pk, 0.11666666666666667, 1e-14); %! assert_equal (ps, 0.95833333333333337, 1e-14); %!test # tied values, where the exact p-value needs no correction %! x = [1 1; 2 1; 2 3; 4 3; 5 6; 5 6]; %! [rhok, pk] = corr (x, 'Type', 'Kendall'); %! [rhos, ps] = corr (x, 'Type', 'Spearman'); %! assert_equal (rhok(1,2), 0.88070484592797926, 1e-14); %! assert_equal (pk(1,2), 0.044444444444444446, 1e-14); %! assert_equal (rhos(1,2), 0.92318618234499539, 1e-14); %! assert_equal (ps(1,2), 0.044444444444444446, 1e-14); %!test # nine observations, the largest sample the exact route covers %! x = (1:9)'; %! y = [3; 1; 4; 6; 5; 9; 2; 8; 7]; %! [rk, pk] = corr (x, y, 'Type', 'Kendall'); %! [rs, ps] = corr (x, y, 'Type', 'Spearman'); %! assert_equal (pk, 0.11943893298059964, 1e-14); %! assert_equal (ps, 0.096797839506172836, 1e-14); %!test # twenty untied observations: Kendall is still exact, Spearman is AS 89 %! x = (1:20)'; %! y = mod ((1:20) * 7, 101)'; %! [~, pk] = corr (x, y, 'Type', 'Kendall'); %! [~, ps] = corr (x, y, 'Type', 'Spearman'); %! assert_equal (pk, 0.098330218734756586, 1e-12); %! assert_equal (ps, 0.65798513768943223, 1e-12); %!test # sixty untied observations: the normal approximation takes over %! x = (1:60)'; %! y = mod ((1:60) * 7, 101)'; %! [~, pk] = corr (x, y, 'Type', 'Kendall'); %! [~, pkr] = corr (x, y, 'Type', 'Kendall', 'Tail', 'right'); %! assert_equal (pk, 0.15127746350184884, 1e-12); %! assert_equal (pkr, 0.075638731750924421, 1e-12); %!test # AS 89 and its one-sided tails %! x = (1:60)'; %! y = mod ((1:60) * 7, 101)'; %! [~, ps] = corr (x, y, 'Type', 'Spearman'); %! [~, psr] = corr (x, y, 'Type', 'Spearman', 'Tail', 'right'); %! [~, psl] = corr (x, y, 'Type', 'Spearman', 'Tail', 'left'); %! assert_equal (ps, 0.49214166036069795, 1e-12); %! assert_equal (psr, 0.24607083018034898, 1e-12); %! assert_equal (psl, 0.75406296187189426, 1e-12); %!test # tied values above the exact route: normal for tau, Student t for rho %! x = (1:30)'; %! y = mod ((1:30) * 7, 5)'; %! [~, pk] = corr (x, y, 'Type', 'Kendall'); %! [~, ps] = corr (x, y, 'Type', 'Spearman'); %! assert_equal (pk, 0.67454343834388397, 1e-12); %! assert_equal (ps, 0.66780132080922749, 1e-12); %!test # 'Rows' 'complete' drops every row holding a NaN %! x = [1 2; NaN 5; 4 4; 7 NaN; 9 6]; %! [rho, pval] = corr (x, 'Rows', 'complete'); %! assert_equal (rho, [1, 0.98974331861078702; 0.98974331861078702, 1], 1e-14); %! assert_equal (pval, ... %! [1, 0.091257896685979945; 0.091257896685979945, 1], 1e-14); %!test # 'Rows' 'pairwise' uses the rows each pair of columns shares %! x = [1 2; NaN 5; 4 4; 7 NaN; 9 6]; %! y = [2 1; 5 4; 4 3; 6 9; 8 7]; %! [rho, pval] = corr (x, y, 'Rows', 'pairwise'); %! assert_equal (rho, [0.99591000331047852, 0.88678890262741183; ... %! 0.95638207148956245, 0.95638207148956245], 1e-14); %! assert_equal (pval, [0.0040899966895214749, 0.11321109737258821; ... %! 0.043617928510437588, 0.043617928510437588], 1e-14); %!test # the default keeps every row, so a NaN reaches its whole row and column %! x = [1 2; NaN 5; 4 4; 7 8; 9 6]; %! rho = corr (x); %! assert_equal (rho, [NaN, NaN; NaN, 1], 1e-14); %!test # 'Weights' gives a weighted Pearson coefficient %! x = [1 2; 3 5; 4 4; 7 8; 9 6]; %! w = [0.5; 1.25; 2; 0.75; 1]; %! rho = corr (x, 'Weights', w); %! assert_equal (rho(1,2), 0.7498537417425275, 1e-14); %!test # 'Weights' applies to the rank coefficients too %! x = [1 2; 3 5; 4 4; 7 8; 9 6]; %! w = [0.5; 1.25; 2; 0.75; 1]; %! rk = corr (x, 'Weights', w, 'Type', 'Kendall'); %! rs = corr (x, 'Weights', w, 'Type', 'Spearman'); %! assert_equal (rk(1,2), 0.43169398907103823, 1e-14); %! assert_equal (rs(1,2), 0.63438256658595638, 1e-14); %!test # a weighted correlation has no p-value %! x = [1 2; 3 5; 4 4; 7 8; 9 6]; %! [~, pval] = corr (x, 'Weights', [0.5; 1.25; 2; 0.75; 1]); %! assert_equal (isnan (pval), true (2)); %!test # a constant column has no correlation, its diagonal entry included %! x = [1 4; 3 4; 4 4; 7 4; 9 4]; %! [rho, pval] = corr (x); %! assert_equal (rho, [1, NaN; NaN, NaN], 1e-14); %! assert_equal (pval, [1, NaN; NaN, NaN], 1e-14); %!test # two observations leave no degrees of freedom %! [rho, pval] = corr ([1; 2], [1; 3]); %! assert_equal (rho, 0.99999999999999989, 1e-14); %! assert_equal (isnan (pval), true); %!test # single input gives a single result %! x = single ([1 2; 3 5; 4 4; 7 8; 9 6]); %! [rho, pval] = corr (x); %! assert_equal (class (rho), 'single'); %! assert_equal (class (pval), 'single'); %! assert_equal (rho, single ([1, 0.80516105890274048; ... %! 0.80516105890274048, 1]), single (1e-7)); %!test # logical input is correlated as the numbers it stands for %! x = [1 2; 3 5; 4 4; 7 8; 9 6]; %! rho = corr (x > 4); %! assert_equal (rho(1,2), 0.66666666666666641, 1e-14); %!test # an option value may be abbreviated and is matched without case %! x = [1 2; 3 5; 4 4; 7 8; 9 6]; %! assert_equal (corr (x, 'type', 'spear'), corr (x, 'Type', 'Spearman')); %! assert_equal (corr (x, 'Type', 'P'), corr (x)); %!test # a Y without columns gives a result without columns %! x = [1 2; 3 5; 4 4; 7 8; 9 6]; %! [rho, pval] = corr (x, zeros (5, 0)); %! assert_equal (size (rho), [2, 0]); %! assert_equal (size (pval), [2, 0]); %!test # a row vector is one observation of several variables, as in MATLAB %! assert_equal (corr ([1 2 3]), NaN (3)); %! assert_equal (corr (5), NaN); ## Test input validation %!error corr () %!error corr ({1, 2, 3}) %!error corr ('abcde') %!error corr (ones (2, 2, 2)) %!error corr (zeros (5, 0)) %!error corr ([]) %!error corr ([1; 2], {1, 2}) %!error corr ([1; 2], ones (2, 2, 2)) %!error ... %! corr (ones (5, 2), ones (4, 2)) %!error ... %! corr (ones (5, 2), 'Foo', 1) %!error ... %! corr (ones (5, 2), 'Type') %!error ... %! corr (ones (5, 2), 'Type', 'foo') %!error ... %! corr (ones (5, 2), 'Type', 5) %!error ... %! corr (ones (5, 2), 'Rows', 'foo') %!error ... %! corr (ones (5, 2), 'Tail', 'foo') %!error ... %! corr (ones (5, 2), 'Weights', [1; 2]) %!error ... %! corr (ones (5, 2), 'Weights', ones (1, 5)) %!error ... %! corr (ones (5, 2), 'Weights', [1; -1; 1; 1; 1]) statistics-release-2.1.2/inst/Descriptive_Statistics/dcov.m000066400000000000000000000114061527311104300241040ustar00rootroot00000000000000## Copyright (C) 2014 - Maria L. Rizzo and Gabor J. Szekely ## Copyright (C) 2014 Juan Pablo Carbajal ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{dCor}, @var{dCov}, @var{dVarX}, @var{dVarY}] =} dcov (@var{x}, @var{y}) ## ## Distance correlation, covariance and correlation statistics. ## ## It returns the distance correlation (@var{dCor}) and the distance covariance ## (@var{dCov}) between @var{x} and @var{y}, the distance variance of @var{x} ## in (@var{dVarX}) and the distance variance of @var{y} in (@var{dVarY}). ## ## @var{x} and @var{y} must have the same number of observations (rows) but they ## can have different number of dimensions (columns). Rows with missing values ## (@qcode{NaN}) in either @var{x} or @var{y} are omitted. ## ## The Brownian covariance is the same as the distance covariance: ## ## @tex ## $$ cov_W (X, Y) = dCov(X, Y) $$ ## ## @end tex ## @ifnottex ## @math{cov_W (@var{x}, @var{y}) = dCov (@var{x}, @var{y})} ## @end ifnottex ## ## and thus Brownian correlation is the same as distance correlation. ## ## @seealso{corr, cov} ## @end deftypefn function [dCor, dCov, dVarX, dVarY] = dcov (x, y) ## Validate input size if (size (x, 1) != size (y, 1)) error ("dcov: Sample sizes (rows) in X and Y must agree."); endif ## Exclude missing values is_nan = any ([isnan(x) isnan(y)], 2); x(is_nan,:) = []; y(is_nan,:) = []; ## Calculate double centered distance A = pdist2 (x, x); A_col = mean (A, 1); A_row = mean (A, 2); Acbar = ones (size (A_row)) * A_col; Arbar = A_row * ones (size (A_col)); A_bar = mean (A(:)) * ones (size (A)); A = A - Acbar - Arbar + A_bar; B = pdist2 (y, y); B_col = mean (B, 1); B_row = mean (B, 2); Bcbar = ones (size (B_row)) * B_col; Brbar = B_row * ones (size (B_col)); B_bar = mean (B(:)) * ones (size (B)); B = B - Bcbar - Brbar + B_bar; ## Calculate distance covariance and variances dCov = sqrt (mean (A(:) .* B(:))); dVarX = sqrt (mean (A(:) .^ 2)); dVarY = sqrt (mean (B(:) .^ 2)); ## Calculate distance correlation V = sqrt (dVarX .* dVarY); if V > 0 dCor = dCov / V; else dCor = 0; endif endfunction %!demo %! rng (42); %! base=@(x) (x- min (x))./(max (x)-min (x)); %! N = 5e2; %! x = randn (N,1); x = base(x); %! z = randn (N,1); z = base(z); %! # Linear relations %! cy = [1 0.55 0.3 0 -0.3 -0.55 -1]; %! ly = x .* cy; %! ly(:,[1:3 5:end]) = base(ly(:,[1:3 5:end])); %! # Correlated Gaussian %! cz = 1 - abs (cy); %! gy = base( ly + cz.*z); %! # Shapes %! sx = repmat (x,1,7); %! sy = zeros (size (ly)); %! v = 2 * rand (size (x,1),2) - 1; %! sx(:,1) = v(:,1); sy(:,1) = cos (2*pi*sx(:,1)) + 0.5*v(:,2).*exp (-sx(:,1).^2/0.5); %! R =@(d) [cosd(d) sind(d); -sind(d) cosd(d)]; %! tmp = R(35) * v.'; %! sx(:,2) = tmp(1,:); sy(:,2) = tmp(2,:); %! tmp = R(45) * v.'; %! sx(:,3) = tmp(1,:); sy(:,3) = tmp(2,:); %! sx(:,4) = v(:,1); sy(:,4) = sx(:,4).^2 + 0.5*v(:,2); %! sx(:,5) = v(:,1); sy(:,5) = 3*sign (v(:,2)).*(sx(:,5)).^2 + v(:,2); %! sx(:,6) = cos (2*pi*v(:,1)) + 0.5*(x-0.5); %! sy(:,6) = sin (2*pi*v(:,1)) + 0.5*(z-0.5); %! sx(:,7) = x + sign (v(:,1)); sy(:,7) = z + sign (v(:,2)); %! sy = base(sy); %! sx = base(sx); %! # scaled shape %! sc = 1/3; %! ssy = (sy-0.5) * sc + 0.5; %! n = size (ly,2); %! ym = 1.2; %! xm = 0.5; %! fmt={'horizontalalignment','center'}; %! ff = '% .2f'; %! figure (1) %! for i=1:n %! subplot (4,n,i); %! plot (x, gy(:,i), '.b'); %! axis tight %! axis off %! text (xm,ym,sprintf (ff, dcov (x,gy(:,i))),fmt{:}) %! %! subplot (4,n,i+n); %! plot (x, ly(:,i), '.b'); %! axis tight %! axis off %! text (xm,ym,sprintf (ff, dcov (x,ly(:,i))),fmt{:}) %! %! subplot (4,n,i+2*n); %! plot (sx(:,i), sy(:,i), '.b'); %! axis tight %! axis off %! text (xm,ym,sprintf (ff, dcov (sx(:,i),sy(:,i))),fmt{:}) %! v = axis (); %! %! subplot (4,n,i+3*n); %! plot (sx(:,i), ssy(:,i), '.b'); %! axis (v) %! axis off %! text (xm,ym,sprintf (ff, dcov (sx(:,i),ssy(:,i))),fmt{:}) %! endfor %!error dcov (randn (30, 5), randn (25,5)) statistics-release-2.1.2/inst/Descriptive_Statistics/doc-cache000066400000000000000000001754651527311104300245440ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 26 # name: # type: sq_string # elements: 1 # length: 7 cdfcalc # name: # type: sq_string # elements: 1 # length: 725 statistics: [yCDF, xCDF, n, emsg, eid] = cdfcalc (x) Calculate an empirical cumulative distribution function. [yCDF, xCDF] = cdfcalc (x) calculates an empirical cumulative distribution function (CDF) of the observations in the data sample vector x. x may be a row or column vector, and represents a random sample of observations from some underlying distribution. On return xCDF is the set of x values at which the CDF increases. At XCDF(i), the function increases from YCDF(i) to YCDF(i+1). [yCDF, xCDF, n] = cdfcalc (x) also returns n, the sample size. [yCDF, xCDF, n, emsg, eid] = cdfcalc (x) also returns an error message and error id if x is not a vector or if it contains no values other than NaN. See also: cdfplot # name: # type: sq_string # elements: 1 # length: 56 Calculate an empirical cumulative distribution function. # name: # type: sq_string # elements: 1 # length: 11 cl_multinom # name: # type: sq_string # elements: 1 # length: 2589 statistics: CL = cl_multinom (X, N, b) statistics: CL = cl_multinom (X, N, b, method) Confidence level of multinomial portions. cl_multinom returns confidence level of multinomial parameters estimated as p = X / sum(X) with predefined confidence interval b. Finite population is also considered. This function calculates the level of confidence at which the samples represent the true distribution given that there is a predefined tolerance (confidence interval). This is the upside down case of the typical exercises at which we want to get the confidence interval given the confidence level (and the estimated parameters of the underlying distribution). But once we accept (lets say at elections) that we have a standard predefined maximal acceptable error rate (e.g. b=0.02 ) in the estimation and we just want to know that how sure we can be that the measured proportions are the same as in the entire population (ie. the expected value and mean of the samples are roughly the same) we need to use this function. Arguments Variable Type Description X int vector sample frequencies bins. N int scalar Population size that was sampled by X. If N < sum (X), infinite number assumed. b real vector confidence interval. If vector, it should be the size of X containing confidence interval for each cells. If scalar, each cell will have the same value of b unless it is zero or -1. If value is 0, b = 0.02 is assumed which is standard choice at elections otherwise it is calculated in a way that one sample in a cell alteration defines the confidence interval. method string An optional argument for defining the calculation method. Available choices are 'bromaghin' (default), 'cochran', and agresti_cull. Note! The agresti_cull method is not exactly the solution at reference given below but an adjustment of the solutions above. Returns Confidence level. Example CL = cl_multinom ([27; 43; 19; 11], 10000, 0.05) returns 0.69 confidence level. References "bromaghin" calculation type (default) is based on the article: Jeffrey F. Bromaghin, "Sample Size Determination for Interval Estimation of Multinomial Probabilities", The American Statistician vol 47, 1993, pp 203-206. "cochran" calculation type is based on article: Robert T. Tortora, "A Note on Sample Size Estimation for Multinomial Populations", The American Statistician, , Vol 32. 1978, pp 100-102. "agresti_cull" calculation type is based on article: A. Agresti and B.A. Coull, "Approximate is better than 'exact' for interval estimation of binomial portions", The American Statistician, Vol. 52, 1998, pp 119-126 # name: # type: sq_string # elements: 1 # length: 41 Confidence level of multinomial portions. # name: # type: sq_string # elements: 1 # length: 10 cohenKappa # name: # type: sq_string # elements: 1 # length: 3875 statistics: kappa = cohenKappa (x, y) statistics: kappa = cohenKappa (T) statistics: kappa = cohenKappa (..., name, value) statistics: [kappa, ci] = cohenKappa (...) statistics: [kappa, ci, stats] = cohenKappa (...) Cohen's kappa, the agreement between two raters beyond chance. kappa = cohenKappa (x, y) returns Cohen's kappa for two raters who each assigned the same subjects to categories, the ratings of the first in the vector x and of the second in y. The ratings may be numeric or logical, categorical, or text: a character matrix with one rating per row, a cell array of character vectors, or a string array, the two raters' of the same kind. A subject whose rating by either rater is missing, NaN, an undefined category, an empty text or a missing string, is left out. kappa = cohenKappa (T) takes the agreement table instead, a square matrix of counts whose element T(i,j) is the number of subjects the first rater assigned to category i and the second to category j. With po the proportion of subjects on whom the raters agree and pe the proportion expected by chance from each rater's own frequencies of the categories, $$ \kappa = \frac{p_o - p_e}{1 - p_e}, $$ 1 for perfect agreement, 0 for agreement no better than chance, and negative for less. Weighted kappa counts partial agreement between ordered categories, weighting each pair of categories i and j by w(i,j), 1 when they are the same, in both po and pe. The following Name-Value pairs are supported: Name Value 'Weights' 'none' (default), unweighted kappa; 'linear', w(i,j) = 1 - abs (i - j) / (c - 1); 'quadratic', w(i,j) = 1 - (i - j)^2 / (c - 1)^2, for c categories in their order; or a c-by-c matrix of agreement weights between 0 and 1, with ones on its diagonal. 'Categories' The categories, in their order: numeric for numeric ratings, text for text or categorical ratings. The default is the sorted values of numeric ratings, the categories of categorical ratings in their own order, unused ones included, and the sorted texts of text ratings. A category neither rater used counts towards c and spaces the others: placed between used categories it changes linear and quadratic kappa, placed after them it does not. Not accepted with T, whose rows and columns are the categories. 'Alpha' The significance level of the confidence interval, a scalar in the range (0,1). The default is 0.05. [kappa, ci] = cohenKappa (...) also returns the 100 (1 - alpha) percent confidence interval of kappa, from its large-sample standard error (Fleiss, Cohen and Everitt, 1969), clipped to [-1, 1]. [kappa, ci, stats] = cohenKappa (...) also returns a structure with the fields po the observed agreement, weighted if weights are given pe the agreement expected by chance se the standard error of kappa that ci is based on zval the statistic of the test that kappa is 0, kappa over its standard error when there is no agreement beyond chance p the two-sided p-value of that test n the number of subjects table the agreement table categories the categories, a numeric column or a cell array of character vectors weights the matrix of weights used Kappa is undefined when chance alone predicts complete agreement, as when both raters use a single category, and such ratings are refused. Agreement among more than two raters is measured by fleissKappa. MATLAB has no Cohen's kappa, so cohenKappa is specific to Octave. References: J. Cohen. A coefficient of agreement for nominal scales. Educational and Psychological Measurement, 20(1):37--46, 1960. J. Cohen. Weighted kappa: nominal scale agreement with provision for scaled disagreement or partial credit. Psychological Bulletin, 70(4):213--220, 1968. J. L. Fleiss, J. Cohen and B. S. Everitt. Large sample standard errors of kappa and weighted kappa. Psychological Bulletin, 72(5):323--327, 1969. See also: fleissKappa, crosstab, limitsOfAgreement # name: # type: sq_string # elements: 1 # length: 62 Cohen's kappa, the agreement between two raters beyond chance. # name: # type: sq_string # elements: 1 # length: 4 corr # name: # type: sq_string # elements: 1 # length: 3742 statistics: rho = corr (x) statistics: rho = corr (x, y) statistics: rho = corr (..., name, value) statistics: [rho, pval] = corr (...) Linear or rank correlation coefficients. rho = corr (x) returns the matrix of pairwise correlation coefficients between the columns of x, whose rows are observations and whose columns are variables. For an n-by-k matrix x, rho is k-by-k and rho(i,j) is the correlation between the i-th and the j-th column of x. rho = corr (x, y) returns the correlations between the columns of x and the columns of y. For an n-by-k1 matrix x and an n-by-k2 matrix y, rho is k1-by-k2 and rho(i,j) is the correlation between the i-th column of x and the j-th column of y. x and y must have the same number of rows. The following Name-Value pairs are supported: Name Value 'Type' The coefficient to compute: 'Pearson' (default) for the linear correlation coefficient, 'Kendall' for Kendall's tau-b, or 'Spearman' for Spearman's rho. 'Rows' How missing values are handled: 'all' (default) uses every row, so an entry is NaN whenever either of its columns holds a NaN; 'complete' first removes every row holding a NaN in any column; 'pairwise' computes each entry from the rows where its own pair of columns is present. 'Tail' The alternative hypothesis of the test reported in pval: 'both' (default) for a correlation different from zero, 'right' for a positive one, or 'left' for a negative one. 'Weights' A column vector of n nonnegative observation weights. The default weights every observation equally. Option values are matched without regard to case, and an unambiguous abbreviation is accepted, so 'spear' selects 'Spearman'. [rho, pval] = corr (...) also returns pval, the p-value of a test of the null hypothesis that the corresponding correlation is zero, against the alternative named by 'Tail'. A pval entry is NaN wherever its rho entry is, and every entry is NaN when 'Weights' is given. The p-value is computed as follows: 'Pearson': from a Student's t distribution with n-2 degrees of freedom applied to t = rho * sqrt ((n - 2) / (1 - rho^2)), which is exact when the data are normally distributed. 'Kendall': from the exact permutation distribution when n < 10, and for a sample of fewer than 50 observations holding no tied values; otherwise from a normal approximation with a continuity correction and the usual correction for ties. 'Spearman': from the exact permutation distribution when n < 10; from the approximation of Best and Roberts (algorithm AS 89) for a larger sample holding no tied values; and from the same Student's t transformation as the Pearson coefficient for a larger sample that does hold them. The exact p-value is the proportion of the n! orderings of one variable giving a coefficient as extreme as the observed one, so tied values need no special treatment there. A column of constant values has no defined correlation, so its entries, the diagonal one included, are NaN. Inf makes a Pearson coefficient NaN, while the rank coefficients order it like any other value. Two deviations from MATLAB. A character array is refused, where MATLAB correlates the character codes; its acceptance there is incidental, correlation having no meaning for text. An array of more than two dimensions is refused, where MATLAB refuses most of them from inside a matrix multiplication but flattens a leading singleton dimension and answers NaN; core Octave's corr answers NaN there too. References: M. G. Kendall. A new measure of rank correlation. Biometrika, 30(1-2):81--93, 1938. D. J. Best and D. E. Roberts. Algorithm AS 89: the upper tail probabilities of Spearman's rho. Applied Statistics, 24(3):377--379, 1975. See also: corrcoef, cov, partialcorr, tiedrank, kendall, spearman # name: # type: sq_string # elements: 1 # length: 40 Linear or rank correlation coefficients. # name: # type: sq_string # elements: 1 # length: 4 dcov # name: # type: sq_string # elements: 1 # length: 656 statistics: [dCor, dCov, dVarX, dVarY] = dcov (x, y) Distance correlation, covariance and correlation statistics. It returns the distance correlation (dCor) and the distance covariance (dCov) between x and y, the distance variance of x in (dVarX) and the distance variance of y in (dVarY). x and y must have the same number of observations (rows) but they can have different number of dimensions (columns). Rows with missing values (NaN) in either x or y are omitted. The Brownian covariance is the same as the distance covariance: $$ cov_W (X, Y) = dCov(X, Y) $$ and thus Brownian correlation is the same as distance correlation. See also: corr, cov # name: # type: sq_string # elements: 1 # length: 60 Distance correlation, covariance and correlation statistics. # name: # type: sq_string # elements: 1 # length: 4 ecdf # name: # type: sq_string # elements: 1 # length: 1764 statistics: [f, x] = ecdf (y) statistics: [f, x, flo, fup] = ecdf (y) statistics: ecdf (...) statistics: ecdf (ax, ...) statistics: [...] = ecdf (y, name, value, ...) statistics: [...] = ecdf (ax, y, name, value, ...) Empirical (Kaplan-Meier) cumulative distribution function. [f, x] = ecdf (y) calculates the Kaplan-Meier estimate of the cumulative distribution function (cdf), also known as the empirical cdf. y is a vector of data values. f is a vector of values of the empirical cdf evaluated at x. [f, x, flo, fup] = ecdf (y) also returns lower and upper confidence bounds for the cdf. These bounds are calculated using Greenwood's formula, and are not simultaneous confidence bounds. ecdf (...) without output arguments produces a plot of the empirical cdf. ecdf (ax, ...) plots into existing axes ax. [...] = ecdf (y, name, value, ...) specifies additional parameter name/value pairs chosen from the following: name value "censoring" A boolean vector of the same size as Y that is 1 for observations that are right-censored and 0 for observations that are observed exactly. Default is all observations observed exactly. "frequency" A vector of the same size as Y containing non-negative integer counts. The jth element of this vector gives the number of times the jth element of Y was observed. Default is 1 observation per Y element. "alpha" A value alpha between 0 and 1 specifying the significance level. Default is 0.05 for 5% significance. "function" The type of function returned as the F output argument, chosen from "cdf" (the default), "survivor", or "cumulative hazard". "bounds" Either "on" to include bounds or "off" (the default) to omit them. Used only for plotting. Type demo ecdf to see examples of usage. See also: cdfplot, ecdfhist # name: # type: sq_string # elements: 1 # length: 58 Empirical (Kaplan-Meier) cumulative distribution function. # name: # type: sq_string # elements: 1 # length: 11 fleissKappa # name: # type: sq_string # elements: 1 # length: 3295 statistics: kappa = fleissKappa (X) statistics: kappa = fleissKappa (X, name, value) statistics: [kappa, stats] = fleissKappa (...) Fleiss' kappa, the agreement among several raters beyond chance. kappa = fleissKappa (X) returns Fleiss' kappa for m raters who each assigned the same n subjects to categories, X holding one row per subject and one column per rater. The ratings may be numeric or logical, categorical, or text in a cell array of character vectors or a string array. A subject with a missing rating, NaN, an undefined category, an empty text or a missing string, is left out, every subject having to be rated by all m raters. With N(i,j) the number of raters who put subject i in category j, p(j) the proportion of all ratings in category j, Pbar the mean over the subjects of (sum (N(i,:) .^ 2) - m) / (m * (m - 1)), the proportion of pairs of raters who agree, and Pe = sum (p .^ 2), $$ \kappa = \frac{\bar P - P_e}{1 - P_e}, $$ 1 for perfect agreement and 0 for agreement no better than chance. With two raters it is Scott's pi, not Cohen's kappa, chance being estimated from the ratings of all raters together; cohenKappa measures the agreement of two raters from their own frequencies. The following Name-Value pairs are supported: Name Value 'Categories' The categories: numeric for numeric ratings, text for text or categorical ratings. The default is the sorted values of numeric ratings, the categories of categorical ratings, unused ones included, and the sorted texts of text ratings. A category no rater used has a kappa of NaN in stats. 'Exact' false (default) for Fleiss' kappa, or true for the variant of Conger (1980), which takes each rater's own frequencies into account and equals Cohen's kappa for two raters. It has no test, and stats then holds NaN for its statistic and p-value. [kappa, stats] = fleissKappa (...) also returns a structure with the fields po the observed agreement Pbar pe the agreement expected by chance se the standard error of kappa when there is no agreement beyond chance (Fleiss, Nee and Landis, 1979) zval the statistic of the test that kappa is 0, kappa / se p the two-sided p-value of that test n the number of subjects raters the number of raters m counts the n-by-c matrix N categories the categories, a numeric column or a cell array of character vectors categorykappa the kappa of each category, the agreement on whether a subject belongs to it categoryzval the statistic of the test that each category's kappa is 0 categoryp the two-sided p-value of that test No confidence interval is given, there being no standard large-sample standard error for kappa away from 0. Kappa is undefined when chance alone predicts complete agreement, as when every rating falls in one category, and such ratings are refused. MATLAB has no Fleiss' kappa, so fleissKappa is specific to Octave. References: J. L. Fleiss. Measuring nominal scale agreement among many raters. Psychological Bulletin, 76(5):378--382, 1971. J. L. Fleiss, J. C. M. Nee and J. R. Landis. Large sample variance of kappa in the case of different sets of raters. Psychological Bulletin, 86(5):974--977, 1979. A. J. Conger. Integration and generalization of kappas for multiple raters. Psychological Bulletin, 88(2):322--328, 1980. See also: cohenKappa, friedman # name: # type: sq_string # elements: 1 # length: 64 Fleiss' kappa, the agreement among several raters beyond chance. # name: # type: sq_string # elements: 1 # length: 7 geomean # name: # type: sq_string # elements: 1 # length: 1775 statistics: m = geomean (x) statistics: m = geomean (x, "all") statistics: m = geomean (x, dim) statistics: m = geomean (x, vecdim) statistics: m = geomean (..., nanflag) Compute the geometric mean of x. If x is a vector, then geomean(x) returns the geometric mean of the elements in x defined as $$ {\rm geomean}(x) = \left( \prod_{i=1}^N x_i \right)^\frac{1}{N} = exp \left({1\over N} \sum_{i=1}^N log x_i \right) $$ where N is the length of the x vector. If x is a matrix, then geomean(x) returns a row vector with the geometric mean of each columns in x. If x is a multidimensional array, then geomean(x) operates along the first nonsingleton dimension of x. x must not contain any negative or complex values. geomean(x, "all") returns the geometric mean of all the elements in x. If x contains any 0, then the returned value is 0. geomean(x, dim) returns the geometric mean along the operating dimension dim of x. Calculating the harmonic mean of any subarray containing any 0 will return 0. geomean(x, vecdim) returns the geometric mean over the dimensions specified in the vector vecdim. For example, if x is a 2-by-3-by-4 array, then geomean(x, [1 2]) returns a 1-by-1-by-4 array. Each element of the output array is the geometric mean of the elements on the corresponding page of x. If vecdim indexes all dimensions of x, then it is equivalent to geomean (x, "all"). Any dimension in vecdim greater than ndims (x) is ignored. geomean(..., nanflag) specifies whether to exclude NaN values from the calculation, using any of the input argument combinations in previous syntaxes. By default, geomean includes NaN values in the calculation (nanflag has the value "includenan"). To exclude NaN values, set the value of nanflag to "omitnan". See also: harmmean, mean # name: # type: sq_string # elements: 1 # length: 32 Compute the geometric mean of x. # name: # type: sq_string # elements: 1 # length: 8 grpstats # name: # type: sq_string # elements: 1 # length: 6391 statistics: stats = grpstats (x) statistics: stats = grpstats (x, group) statistics: [means, sem, counts, gname] = grpstats (x, group) statistics: [stats1, ..., statsN] = grpstats (x, group, whichstats) statistics: [stats1, ..., statsN] = grpstats (x, group, whichstats, 'Alpha', alpha) statistics: tblstats = grpstats (tbl) statistics: tblstats = grpstats (tbl, groupvars) statistics: tblstats = grpstats (tbl, groupvars, whichstats) statistics: tblstats = grpstats (tbl, groupvars, whichstats, Name, Value) statistics: grpstats (x, group, alpha) statistics: h = grpstats (x, group, alpha) Summary statistics by group. grpstats computes groupwise summary statistics for the data in x, which can be a numeric matrix or a table. Numeric vectors are treated as a single column matrix. NaNs are treated as missing values and removed from calculations. Syntax for Numeric Input stats = grpstats (x) calculates the mean statistic for each column in x and returns it as row vector in stats. stats = grpstats (x, group) calculates the mean statistic for each column in x grouped by group. The returned argument, stats, is also a matrix with equal columns as x and the number of rows is equal to the groups specified by group. The grouping variable, group can be a vector of any data type supported by the grp2idx function. Alternatively, it can be a cell vector specifying multiple grouping variables with each cell element containing any of the aforementioned supported grouping vectors. If group is empty ([]), then input x is treated as a single group. A group whose values are all missing is dropped, so a grouping variable that is entirely missing leaves no groups at all. [means, sem, counts, gname] = grpstats (x, group) returns, without naming any statistic, the mean, the standard error of the mean, the number of non-NaN values and the group names, in that order. Up to four outputs may be requested this way. An empty x produces empty output with as many columns as x and no rows, one row per group being the general rule and an empty x having no groups. [stats1, ..., statsN] = grpstats (x, group, whichstats) calculates the summary statistics specified by the whichstats argument, which can include any of the available statistics shown below. The number of output arguments must match the number of requested statistics specified in whichstats. computes summary statistics for the numeric matrix x grouped by group. x must be a numeric vector or a 2-D matrix. Vectors are treated as a single-column matrix. group is a grouping variable that defines the groups for the rows of x. It can be a categorical variable, numeric vector, string array, or cell array of strings. group can also be a cell array containing multiple grouping variables. If group is empty ([]) or omitted, all of x is treated as a single group. whichstats specifies the statistics to compute. It can be either a string array or a cell array of strings specifying any of the following builtin statistics. If omitted, the default is 'mean'. whichstats can also contain function handles for custom statistics. The available statistics are: 'mean' Mean of each group. 'median' Median of each group. 'sem' Standard error of the mean for each group. 'std' Standard deviation of each group. 'var' Variance of each group. 'min' Minimum value in each group. 'max' Maximum value in each group. 'range' Difference between max and min in each group. 'numel' Number of elements (count) in each group. 'meanci' Confidence interval for the mean. 'predci' Prediction interval for a new observation. 'gname' Group names. [...] = grpstats (..., 'Alpha', alpha) specifies the significance level for the confidence intervals ('meanci' and 'predci') as 100 * (1-alpha)@%. alpha must be a scalar between 0 and 1. When not specified, it defaults to 0.05. Note that this paired input argument is also valid for table input. Syntax for Table Input tblstats = grpstats (tbl, groupvars) computes the summary statistics for the data in table tbl, grouped by the variables specified in groupvars. If groupvars is empty or omitted, then all of tbl is treated as a single group. groupvars can be a cell array of character vectors or a string array specifying one or more variable names in tbl to be used as grouping variables. Alternatively, all valid methods for indexing table variables are supported (e.g. vartype object, logical vector, function handle). The output tblstats is a table with one row for each group. It contains the grouping variables, an additional 'GroupCount' variable, and the specified summary statistics for the variables in tbl, expect for those specified as grouping variables. When input is a table, only a single output variable, tblstats can be specified. The output tblstats also contains RowNames, which are the unique combinations of the specified groups, for which data are available in tbl. When no groups are specified, the row name of the single row output table defaults to 'All'. tblstats = grpstats (tbl, groupvars, whichstats) specifies which statistics to calculate for the variables in tbl. Unless specified, the mean is calculated for each variable. When specifying more than one statistic, tblstats contains multiple variables for each variable in tbl and each is named by combining the applied statistic with the name of the original variable. When a function handle is applied, its string representation is used instead. For table input specifically, grpstats also accepts the following paired arguments. Name Value 'DataVars' A vector specifying the variables in tbl, for which to calculate the specified statistics. The vector can be any of the valid options for indexing table variables. 'VarNames' A cell array of character vectors or a string array specifying the names of the variables in the output table. The number of specified names must match the number of expected variables in the output table. Plotting Syntax The syntax grpstats (x, group, alpha) generates an errorbar plot with the group means and their respective confidence intervals. x must be a numeric vector or matrix. alpha is a scalar between 0 and 1 that determines the confidence level. This syntax is an alternative to calling errorbar after computing 'mean' and 'meanci' statistics. The optional output h is a handle to the hggroup object representing the data plot and errorbars. See also: grp2idx # name: # type: sq_string # elements: 1 # length: 596 grpstats (x, group) @deftypefnx {statistics} {[stats1, , statsN] =} grpstats @ (x, group, whichstats) @deftypefnx {statistics} {[stats1, , statsN] =} grpstats @ (x, group, whichstats, 'Alpha', alpha) @deftypefnx {statistics} {tblstats =} grpstats (tbl) @deftypefnx {statistics} {tblstats =} grpstats (tbl, groupvars) @deftypefnx {statistics} {tblstats =} grpstats (tbl, groupvars, whichstats) @deftypefnx {statistics} {tblstats =} grpstats (tbl, groupvars, @ whichstats, Name, Value) @deftypefnx {statistics} {} grpstats (x, group, alpha) @deftypefnx {statistics} {h =} grpstats (x, group, alpha) # name: # type: sq_string # elements: 1 # length: 8 harmmean # name: # type: sq_string # elements: 1 # length: 1723 statistics: m = harmmean (x) statistics: m = harmmean (x, "all") statistics: m = harmmean (x, dim) statistics: m = harmmean (x, vecdim) statistics: m = harmmean (..., nanflag) Compute the harmonic mean of x. If x is a vector, then harmmean(x) returns the harmonic mean of the elements in x defined as $$ {\rm harmmean}(x) = \frac{N}{\sum_{i=1}^N \frac{1}{x_i}} $$ where N is the length of the x vector. If x is a matrix, then harmmean(x) returns a row vector with the harmonic mean of each columns in x. If x is a multidimensional array, then harmmean(x) operates along the first nonsingleton dimension of x. x must not contain any negative or complex values. harmmean(x, "all") returns the harmonic mean of all the elements in x. If x contains any 0, then the returned value is 0. harmmean(x, dim) returns the harmonic mean along the operating dimension dim of x. Calculating the harmonic mean of any subarray containing any 0 will return 0. harmmean(x, vecdim) returns the harmonic mean over the dimensions specified in the vector vecdim. For example, if x is a 2-by-3-by-4 array, then harmmean(x, [1 2]) returns a 1-by-1-by-4 array. Each element of the output array is the harmonic mean of the elements on the corresponding page of x. If vecdim indexes all dimensions of x, then it is equivalent to harmmean (x, "all"). Any dimension in vecdim greater than ndims (x) is ignored. harmmean(..., nanflag) specifies whether to exclude NaN values from the calculation, using any of the input argument combinations in previous syntaxes. By default, harmmean includes NaN values in the calculation (nanflag has the value "includenan"). To exclude NaN values, set the value of nanflag to "omitnan". See also: geomean, mean # name: # type: sq_string # elements: 1 # length: 31 Compute the harmonic mean of x. # name: # type: sq_string # elements: 1 # length: 9 jackknife # name: # type: sq_string # elements: 1 # length: 2815 statistics: jackstat = jackknife (E, x) statistics: jackstat = jackknife (E, x, ...) statistics: jackstat = jackknife (..., 'Options', options) Compute jackknife estimates of a parameter taking one or more given samples as parameters. In particular, E is the estimator to be jackknifed as a function name, handle, or inline function, and x is the sample for which the estimate is to be taken. Each row of x is an observation, and a row vector is taken as a column of them. The i-th row of jackstat holds the estimator's value on x with its i-th row omitted, reshaped to a row, so an estimator returning m values gives an nxm matrix for n observations, and a scalar estimator a column vector. Every sample must give the estimator's result the same number of elements as the whole of x does. With fewer than two observations there is nothing to omit, and jackstat is the estimator's value on x itself. jackstat (i,:) = E (x([1:i-1, i+1:end],:)) Depending on the number of samples to be used, the estimator must have the appropriate form: If only one sample is used, then the estimator need not be concerned with cell arrays, for example jackknifing the standard deviation of a sample can be performed with jackstat = jackknife (@std, rand (100, 1)). If several samples are used, an estimator that takes as many inputs, or declares varargin, receives each sample as an argument of its own, as in MATLAB. A scalar is passed to every call unchanged, and the other samples must all have the same number of rows, the same row being omitted from each: jackstat = jackknife (@(x, y) std (x) / var (y), rand (100, 1), randn (100, 1)) An estimator declaring exactly one input instead receives the samples as the elements of a cell array, in their order of appearance, and the samples must all have the same number of rows. This form is an Octave extension. An estimator written for it that also declares an optional second input receives the samples as separate arguments. jackstat = jackknife (@(x) std(x{1})/var(x{2}), rand (100, 1), randn (100, 1)) 'Options' takes an options structure, as made by statset, for MATLAB compatibility; it changes nothing, the samples being evaluated one after another. If all goes well, a theoretical value P for the parameter is already known, n is the sample size, t = n * E(x) - (n - 1) * mean(jackstat) and v = sumsq(n * E(x) - (n - 1) * jackstat - t) / (n * (n - 1)) then (t-P)/sqrt(v) should follow a t-distribution with n-1 degrees of freedom. Jackknifing is a well known method to reduce bias. Further details can be found in: References Rupert G. Miller. The jackknife - a review. Biometrika (1974), 61(1):1-15. doi:10.1093/biomet/61.1.1 Rupert G. Miller. Jackknifing Variances. Ann. Math. Statist. (1968), Volume 39, Number 2, 567-582. doi:10.1214/aoms/1177698418 # name: # type: sq_string # elements: 1 # length: 90 Compute jackknife estimates of a parameter taking one or more given samples as parameters. # name: # type: sq_string # elements: 1 # length: 9 ksdensity # name: # type: sq_string # elements: 1 # length: 1665 statistics: f = ksdensity (x) statistics: f = ksdensity (x, pts) statistics: [f, xi] = ksdensity (...) statistics: [f, xi, bw] = ksdensity (...) statistics: [...] = ksdensity (..., Name, Value) Kernel smoothing density estimate. f = ksdensity (x) computes a probability density estimate of the sample in the vector x, evaluated at 100 equally spaced points xi that span the range of the data. [f, xi] = ksdensity (x) also returns those points. Both are row vectors, whichever way x itself lies. When called without output arguments, the estimate is plotted instead. f = ksdensity (x, pts) evaluates the estimate at the values in pts instead; f is then the same size as pts. For 'Function' equal to 'icdf' the entries of pts are probabilities in [0, 1]. [f, xi, bw] = ksdensity (...) additionally returns the bandwidth bw of the smoothing kernel. The following Name-Value pairs are supported: Name Value 'Kernel' The smoothing kernel: 'normal' (default), 'box', 'triangle', 'epanechnikov', or a function handle @(z) evaluating a kernel density at the standardized distance z. 'Bandwidth' The kernel bandwidth, a positive scalar. The default is the value that is optimal for estimating a normal density, bw = sigma x (4 / (3 x n)) ^ (1 / 5), with sigma a robust estimate of the standard deviation of x. 'Function' The function to estimate: 'pdf' (default), 'cdf', 'icdf', 'survivor', or 'cumhazard'. 'Weights' A vector of non-negative weights, one for each element of x. The default weights are all equal. 'NumPoints' The number of equally spaced points xi at which to evaluate the estimate when pts is not given. The default is 100. See also: hist, histc, ecdf # name: # type: sq_string # elements: 1 # length: 34 Kernel smoothing density estimate. # name: # type: sq_string # elements: 1 # length: 17 limitsOfAgreement # name: # type: sq_string # elements: 1 # length: 2423 statistics: LoA = limitsOfAgreement (X, Y) statistics: LoA = limitsOfAgreement (X, Y, name, value) statistics: [LoA, stats] = limitsOfAgreement (...) Bland-Altman limits of agreement between two measurement methods. LoA = limitsOfAgreement (X, Y) compares two methods that measured the same subjects, the measurements of the first in the vector X and of the second, in the same order, in Y. With d = X - Y the differences, dbar their mean and s their standard deviation, the bias of the first method against the second is dbar and the limits of agreement are dbar - 1.96 * s and dbar + 1.96 * s, between which about 95% of the differences between the methods fall when the differences are normal. Two methods agree well enough to be used in place of one another when the limits are too close to matter in practice, a judgement the data cannot make. LoA is a table with the rows Bias, LowerLimit and UpperLimit and the variables Estimate and ConfidenceIntervals, each row's confidence interval at the level 100 (1 - alpha) percent: dbar + [-1, 1] * t * s / sqrt (n) for the bias and [-1, 1] * t * sqrt (3 * s^2 / n) about each limit, t being the 1 - alpha / 2 quantile of Student's t distribution on n - 1 degrees of freedom and n the number of subjects (Bland and Altman, 1986). X and Y are vectors of type double or single, of the same length, and a subject with a missing value (NaN) in either is left out. Where either is single, so are the results. The following Name-Value pairs are supported: Name Value 'Multiplier' The number of standard deviations from the bias to each limit, a positive scalar. The default is 1.96, which Bland and Altman use. 'Alpha' The significance level of the confidence intervals, a scalar in the range (0,1). The default is 0.05. [LoA, stats] = limitsOfAgreement (...) also returns a structure with the fields sd, the standard deviation of the differences, n, the number of subjects, differences and means, the difference and the mean of the two measurements of each subject, which blandAltmanPlot draws. The bias with its interval is the paired mean difference of meanEffectSize. MATLAB has no limits of agreement, so limitsOfAgreement is specific to Octave. Reference: J. M. Bland and D. G. Altman. Statistical methods for assessing agreement between two methods of clinical measurement. The Lancet, 327(8476):307--310, 1986. See also: blandAltmanPlot, meanEffectSize, cohenKappa # name: # type: sq_string # elements: 1 # length: 65 Bland-Altman limits of agreement between two measurement methods. # name: # type: sq_string # elements: 1 # length: 11 mvksdensity # name: # type: sq_string # elements: 1 # length: 1502 statistics: f = mvksdensity (x, pts, Name, Value) Multivariate kernel smoothing density estimate. f = mvksdensity (x, pts) computes a probability density estimate of the sample in the NxD matrix x, evaluated at the points in the MxD matrix pts. Each row of x is a single D-dimensional observation, and each row of pts is a point at which to evaluate the estimate. The result f is an Mx1 vector, with one density value per row of pts. Rows of x holding a NaN, and rows whose weight is NaN, are removed first, so the default bandwidth, the weights and the sample size come from the remaining rows. A single observation is valid. The density estimate uses a product kernel: the multivariate kernel is the product of the univariate kernels applied to each dimension, each with its own bandwidth. The following Name-Value pairs are supported: Name Value 'Bandwidth' The kernel bandwidth, either a positive scalar applied to every dimension or a 1xD vector of positive values, one per dimension. The default is a diagonal normal-reference (Silverman) rule computed from x, for any number of columns, although MATLAB requires a bandwidth from three columns up. 'Kernel' The smoothing kernel applied in each dimension: 'normal' (default), 'box', 'triangle', or 'epanechnikov'. 'Function' The function to estimate: 'pdf' (default) or 'cdf'. 'Weights' A vector of non-negative weights, one for each row of x, with a positive weight among the rows kept. The default weights are all equal. See also: ksdensity # name: # type: sq_string # elements: 1 # length: 47 Multivariate kernel smoothing density estimate. # name: # type: sq_string # elements: 1 # length: 6 nancov # name: # type: sq_string # elements: 1 # length: 1268 statistics: c = nancov (x) statistics: c = nancov (x, y) statistics: c = nancov (..., normalization) statistics: c = nancov (..., method) Compute the covariance matrix while handling NaN values. c = nancov (x) returns the covariance matrix of the columns of x, treating each row as an observation. Rows containing NaN values are removed before computing the covariance. If x is a vector, the scalar variance of its non-NaN elements is returned. c = nancov (x, y), where x and y are of equal length, is equivalent to nancov ([x(:), y(:)]) and returns the 2-by-2 covariance matrix. c = nancov (..., normalization) specifies the normalization. When normalization is 0 (default), the covariance is normalized by N-1, where N is the number of observations used. When it is 1, it is normalized by N. c = nancov (..., method) selects how NaN values are handled. With "complete" (the default), any row of the data that contains a NaN value is removed before the covariance is computed. With "pairwise", each element c(i,j) is computed using all rows in which both column i and column j are non-NaN; the resulting matrix may fail to be positive semidefinite. x and y must be real floating point, although MATLAB accepts logical too. See also: cov, nanvar, nanstd, nanmean # name: # type: sq_string # elements: 1 # length: 56 Compute the covariance matrix while handling NaN values. # name: # type: sq_string # elements: 1 # length: 6 nanmax # name: # type: sq_string # elements: 1 # length: 1799 statistics: v = nanmax (x) statistics: v = nanmax (x, [], dim) statistics: [v, idx] = nanmax (...) statistics: v = nanmax (x, [], 'all') statistics: v = nanmax (x, [], vecdim) statistics: v = nanmax (x, y) Find the maximum while ignoring NaN values. v = nanmax (x) returns the maximum of x, after removing NaN values. If x is a vector, a scalar maximum value is returned. If x is a matrix, a row vector of column maxima is returned. If x is a multidimensional array, the nanmax operates along the first nonsingleton dimension. If all values in a column are NaN, the maximum is returned as NaN rather than []. v = nanmax (x, [], dim) operates along the dimension dim of x. A logical dim or vecdim is an error, although MATLAB accepts true as dimension 1. [v, idx] = nanmax (...) also returns the row indices of the maximum values for each column in the vector idx. When x is a vector, then idx is a scalar value as v. v = nanmax (x, [], 'all') returns the maximum of all elements of x, after removing NaN values. It is the equivalent of nanmax (x(:)). The optional flag 'all' cannot be used together with dim or vecdim input arguments. v = nanmax (x, [], vecdim) returns the maximum over the dimensions specified in the vector vecdim. Each element of vecdim represents a dimension of the input array x and the output v has length 1 in the specified operating dimensions. The lengths of the other dimensions are the same for x and y. For example, if x is a 2-by-3-by-4 array, then nanmax (x, [1 2]) returns a 1-by-1-by-4 array. Each element of the output array is the maximum of the elements on the corresponding page of x. If vecdim indexes all dimensions of x, then it is equivalent to nanmax (x, 'all'). Any dimension in vecdim greater than ndims (x) is ignored. See also: max, nanmin, nansum # name: # type: sq_string # elements: 1 # length: 43 Find the maximum while ignoring NaN values. # name: # type: sq_string # elements: 1 # length: 7 nanmean # name: # type: sq_string # elements: 1 # length: 1792 statistics: s = nanmean (x) statistics: s = nanmean (x, 'all') statistics: s = nanmean (x, dim) statistics: s = nanmean (x, vecdim) Compute the mean while ignoring NaN values. s = nanmean (x) returns the mean of x after removing NaN values. If x is a vector, a scalar value is returned. If x is a matrix, a row vector of column means is returned. If x is a multidimensional array, nanmean operates along the first nonsingleton dimension. If all values along a dimension are NaN, the mean is returned returned as NaN. s = nanmean (x, 'all') returns the mean of all elements of x, after removing NaN values. It is the equivalent of nanmean (x(:)). s = nanmean (x, dim) operates along the dimension dim of x. A logical dim or vecdim is an error, although MATLAB accepts true as dimension 1. s = nanmean (x, vecdim) returns the mean over the dimensions specified in the vector vecdim. Each element of vecdim represents a dimension of the input array x and the output s has length 1 in the specified operating dimensions. The lengths of the other dimensions are the same for x and y. For example, if x is a 2-by-3-by-4 array, then nanmean (x, [1 2]) returns a 1-by-1-by-4 array. Each element of the output array is the mean of the elements on the corresponding page of x. If vecdim indexes all dimensions of x, then it is equivalent to nanmean (x, 'all'). Any dimension in vecdim greater than ndims (x) is ignored. nanmean primarily operates on single and double numeric types, since they support NaN values, while preserving the data type. Nevertheless, it can also operate on integer types by treating them as double types. To avoid overflow on very large int64 and uint64 values, use the mean function, which applies special handling for such cases. See also: mean, nansum, nanmin, nanmax # name: # type: sq_string # elements: 1 # length: 43 Compute the mean while ignoring NaN values. # name: # type: sq_string # elements: 1 # length: 9 nanmedian # name: # type: sq_string # elements: 1 # length: 1264 statistics: m = nanmedian (x) statistics: m = nanmedian (x, 'all') statistics: m = nanmedian (x, dim) statistics: m = nanmedian (x, vecdim) Compute the median while ignoring NaN values. m = nanmedian (x) returns the median of x, after removing NaN values. If x is a vector, a scalar value is returned. If x is a matrix, a row vector of column medians is returned. If x is a multidimensional array, nanmedian operates along the first nonsingleton dimension. If all values along a dimension are NaN, the median is returned as NaN. As in median, m takes the class of x, so an integer median is rounded. m = nanmedian (x, 'all') returns the median of all elements of x, after removing NaN values. It is the equivalent of nanmedian (x(:)). m = nanmedian (x, dim) operates along the dimension dim of x. A logical dim or vecdim is an error, as in MATLAB. m = nanmedian (x, vecdim) returns the median over the dimensions specified in the vector vecdim. Each element of vecdim represents a dimension of the input array x and the output m has length 1 in the specified operating dimensions. If vecdim indexes all dimensions of x, then it is equivalent to nanmedian (x, 'all'). Any dimension in vecdim greater than ndims (x) is ignored. See also: median, nanmean, nansum # name: # type: sq_string # elements: 1 # length: 45 Compute the median while ignoring NaN values. # name: # type: sq_string # elements: 1 # length: 6 nanmin # name: # type: sq_string # elements: 1 # length: 1799 statistics: v = nanmin (x) statistics: v = nanmin (x, [], dim) statistics: [v, idx] = nanmin (...) statistics: v = nanmin (x, [], 'all') statistics: v = nanmin (x, [], vecdim) statistics: v = nanmin (x, y) Find the minimum while ignoring NaN values. v = nanmin (x) returns the minimum of x, after removing NaN values. If x is a vector, a scalar minimum value is returned. If x is a matrix, a row vector of column minima is returned. If x is a multidimensional array, the nanmin operates along the first nonsingleton dimension. If all values in a column are NaN, the minimum is returned as NaN rather than []. v = nanmin (x, [], dim) operates along the dimension dim of x. A logical dim or vecdim is an error, although MATLAB accepts true as dimension 1. [v, idx] = nanmin (...) also returns the row indices of the minimum values for each column in the vector idx. When x is a vector, then idx is a scalar value as v. v = nanmin (x, [], 'all') returns the minimum of all elements of x, after removing NaN values. It is the equivalent of nanmin (x(:)). The optional flag 'all' cannot be used together with dim or vecdim input arguments. v = nanmin (x, [], vecdim) returns the minimum over the dimensions specified in the vector vecdim. Each element of vecdim represents a dimension of the input array x and the output v has length 1 in the specified operating dimensions. The lengths of the other dimensions are the same for x and y. For example, if x is a 2-by-3-by-4 array, then nanmin (x, [1 2]) returns a 1-by-1-by-4 array. Each element of the output array is the minimum of the elements on the corresponding page of x. If vecdim indexes all dimensions of x, then it is equivalent to nanmin (x, 'all'). Any dimension in vecdim greater than ndims (x) is ignored. See also: min, nanmax, nansum # name: # type: sq_string # elements: 1 # length: 43 Find the minimum while ignoring NaN values. # name: # type: sq_string # elements: 1 # length: 6 nanstd # name: # type: sq_string # elements: 1 # length: 1713 statistics: s = nanstd (x) statistics: s = nanstd (x, w) statistics: s = nanstd (x, w, 'all') statistics: s = nanstd (x, w, dim) statistics: s = nanstd (x, w, vecdim) Compute the standard deviation while ignoring NaN values. s = nanstd (x) returns the standard deviation of x, after removing NaN values. If x is a vector, a scalar value is returned. If x is a matrix, a row vector of column standard deviations is returned. If x is a multidimensional array, nanstd operates along the first nonsingleton dimension. If a dimension contains fewer than two non-NaN values, the standard deviation is returned as 0 for a single value and as NaN when all values are NaN. x must be floating point, as for std, although MATLAB accepts logical too. s = nanstd (x, w) specifies the normalization. When w is 0 (default), the standard deviation is normalized by N-1, where N is the number of non-NaN observations. When w is 1, it is normalized by N. w may also be a vector of nonnegative weights whose length matches the operating dimension, in which case the weighted standard deviation normalized by the sum of the weights is returned. s = nanstd (x, w, 'all') returns the standard deviation of all elements of x, after removing NaN values. Use an empty value, w = [], to pass the default normalization. s = nanstd (x, w, dim) operates along the dimension dim of x. A logical dim or vecdim is an error, although MATLAB accepts true as dimension 1. s = nanstd (x, w, vecdim) returns the standard deviation over the dimensions specified in the vector vecdim. A weight vector is not supported together with 'all' or vecdim. Any dimension in vecdim greater than ndims (x) is ignored. See also: std, nanvar, nanmean, nansum # name: # type: sq_string # elements: 1 # length: 57 Compute the standard deviation while ignoring NaN values. # name: # type: sq_string # elements: 1 # length: 6 nansum # name: # type: sq_string # elements: 1 # length: 1432 statistics: s = nansum (x) statistics: s = nansum (x, 'all') statistics: s = nansum (x, dim) statistics: s = nansum (x, vecdim) Compute the sum while ignoring NaN values. s = nansum (x) returns the sum of x, after removing NaN values. If x is a vector, a scalar value is returned. If x is a matrix, a row vector of column sums is returned. If x is a multidimensional array, the nansum operates along the first nonsingleton dimension. If all values along a dimension are NaN, the sum is returned returned as 0. s = nansum (x, 'all') returns the sum of all elements of x, after removing NaN values. It is the equivalent of nansum (x(:)). s = nansum (x, dim) operates along the dimension dim of x. A logical dim or vecdim is an error, although MATLAB accepts true as dimension 1. s = nansum (x, vecdim) returns the sum over the dimensions specified in the vector vecdim. Each element of vecdim represents a dimension of the input array x and the output s has length 1 in the specified operating dimensions. The lengths of the other dimensions are the same for x and y. For example, if x is a 2-by-3-by-4 array, then nanmax (x, [1 2]) returns a 1-by-1-by-4 array. Each element of the output array is the maximum of the elements on the corresponding page of x. If vecdim indexes all dimensions of x, then it is equivalent to nanmax (x, 'all'). Any dimension in vecdim greater than ndims (x) is ignored. See also: sum, nanmin, nanmax # name: # type: sq_string # elements: 1 # length: 42 Compute the sum while ignoring NaN values. # name: # type: sq_string # elements: 1 # length: 6 nanvar # name: # type: sq_string # elements: 1 # length: 1633 statistics: v = nanvar (x) statistics: v = nanvar (x, w) statistics: v = nanvar (x, w, 'all') statistics: v = nanvar (x, w, dim) statistics: v = nanvar (x, w, vecdim) Compute the variance while ignoring NaN values. v = nanvar (x) returns the variance of x, after removing NaN values. If x is a vector, a scalar value is returned. If x is a matrix, a row vector of column variances is returned. If x is a multidimensional array, nanvar operates along the first nonsingleton dimension. If a dimension contains fewer than two non-NaN values, the variance is returned as 0 for a single value and as NaN when all values are NaN. x must be floating point, as for var, although MATLAB accepts logical too. v = nanvar (x, w) specifies the normalization. When w is 0 (default), the variance is normalized by N-1, where N is the number of non-NaN observations. When w is 1, it is normalized by N. w may also be a vector of nonnegative weights whose length matches the operating dimension, in which case the weighted variance normalized by the sum of the weights is returned. v = nanvar (x, w, 'all') returns the variance of all elements of x, after removing NaN values. Use an empty value, w = [], to pass the default normalization. v = nanvar (x, w, dim) operates along the dimension dim of x. A logical dim or vecdim is an error, although MATLAB accepts true as dimension 1. v = nanvar (x, w, vecdim) returns the variance over the dimensions specified in the vector vecdim. A weight vector is not supported together with 'all' or vecdim. Any dimension in vecdim greater than ndims (x) is ignored. See also: var, nanstd, nanmean, nansum # name: # type: sq_string # elements: 1 # length: 47 Compute the variance while ignoring NaN values. # name: # type: sq_string # elements: 1 # length: 11 partialcorr # name: # type: sq_string # elements: 1 # length: 3013 statistics: rho = partialcorr (x) statistics: rho = partialcorr (x, z) statistics: rho = partialcorr (x, y, z) statistics: [rho, pval] = partialcorr (...) statistics: [...] = partialcorr (..., Name, Value) Linear or rank partial correlation coefficients. rho = partialcorr (x) returns the sample linear partial correlation coefficients between pairs of variables in the n-by-p matrix x, controlling for the remaining columns of x. Each element rho(i,j) is the partial correlation between x(:,i) and x(:,j), adjusted for the other p-2 columns. rho is a symmetric p-by-p matrix with ones on the diagonal. rho = partialcorr (x, z) controls instead for the variables in the n-by-q matrix z, returning the p-by-p partial correlations among the columns of x. rho = partialcorr (x, y, z) returns the p1-by-p2 matrix of partial correlations between the columns of the n-by-p1 matrix x and the n-by-p2 matrix y, controlling for z. Element rho(i,j) is the partial correlation between x(:,i) and y(:,j). [rho, pval] = partialcorr (...) also returns pval, a matrix of p-values for testing the hypothesis of no partial correlation against the alternative selected by 'Tail'. A coefficient is NaN where the controlling variables explain either of the two variables completely, since the partial correlation is then undefined: no variation is left to correlate. This covers a controlling variable that duplicates one of them and any set of them that spans it. The following Name/Value pairs are accepted: 'Type' 'Pearson' (default) for linear partial correlation, or 'Spearman' for rank partial correlation (computed on the ranks of the data). 'Kendall' is not supported and raises an error, as in MATLAB. 'Rows' 'all' (default) uses all rows regardless of missing values (any NaN yields a NaN result); 'complete' uses only the rows with no missing values across all supplied variables; 'pairwise' uses, for each computed coefficient, the rows with no missing values among just the variables involved in that coefficient. 'Tail' The alternative hypothesis for pval: 'both' (default, nonzero correlation), 'right' (greater than zero), or 'left' (less than zero). The partial correlation is computed by regressing each of the two variables on the controlling variables (with an intercept) and correlating the residuals. The p-value uses a Student's t statistic with n - 2 - k degrees of freedom, where k is the number of controlling variables and n the number of observations used. The data are centered, and the controlling variables rescaled, before the regression, which leaves the result unchanged in exact arithmetic and keeps it accurate in floating point. Shifting or rescaling a controlling variable, or giving the two variables very different scales, does not change the coefficient. MATLAB's coefficient does change in those cases: a large shift gives a different value, a small-scaled controlling variable is dropped, and very different scales overflow to NaN. See also: partialcorri, corr, corrcoef, tiedrank # name: # type: sq_string # elements: 1 # length: 48 Linear or rank partial correlation coefficients. # name: # type: sq_string # elements: 1 # length: 12 partialcorri # name: # type: sq_string # elements: 1 # length: 1194 statistics: rho = partialcorri (y, x) statistics: rho = partialcorri (y, x, z) statistics: [rho, pval] = partialcorri (...) statistics: [...] = partialcorri (..., Name, Value) Partial correlation of each response with each predictor, adjusting for the remaining predictors. rho = partialcorri (y, x) returns the sample partial correlation coefficients between the columns of the n-by-p response matrix y and the columns of the n-by-q predictor matrix x. Element rho(i,j) is the partial correlation between y(:,i) and x(:,j), adjusted for the other columns of x (that is, all columns of x except the j-th). rho is a p-by-q matrix. rho = partialcorri (y, x, z) additionally controls for the variables in the n-by-r matrix z, so that rho(i,j) is adjusted for both the other columns of x and all columns of z. [rho, pval] = partialcorri (...) also returns pval, a matrix of p-values for testing the hypothesis of no partial correlation against the alternative selected by 'Tail'. The 'Type', 'Rows', and 'Tail' Name/Value options are accepted with the same meaning as in partialcorr. 'Kendall' is not supported and raises an error, as in MATLAB. See also: partialcorr, corr, corrcoef, tiedrank # name: # type: sq_string # elements: 1 # length: 97 Partial correlation of each response with each predictor, adjusting for the remaining predictors. # name: # type: sq_string # elements: 1 # length: 8 tabulate # name: # type: sq_string # elements: 1 # length: 2135 statistics: tabulate (x) statistics: tbl = tabulate (x) Create a frequency table of unique values in vector x. tabulate (x) displays a frequency table of the data in the vector x. The input x can be a numeric vector, a logical vector, a character matrix, a cell vector of character vectors, a categorical vector, a string vector, a datetime vector, or a duration vector. The table displays the value, the number of instances (count), and the percentage of that value in x. If no output argument is requested, the table is displayed in the command window. tbl = tabulate (x) returns the frequency table, tbl, as a numeric matrix when x is numeric and as a cell array otherwise. If x is numeric, any missing values (NaNs) are ignored. Similarly, undefined elements in categorical arrays, missing elements in string arrays, NaT elements in datetime arrays, and NaN elements in duration arrays are ignored. The values of a datetime or duration vector are listed in sorted order. MATLAB tabulates each NaN duration instead, as a value of its own counted once. If all the elements of x are positive integers, then the frequency table includes 0 counts for the integers between 1 and max (x) that do not appear in x. For categorical arrays, the frequency table includes 0 counts for any categories that are defined but do not appear in x. Missing values are not tabulated. The percentage column is count / total * 100 taken literally, so a category with no observations out of none at all is NaN rather than zero: there is no total to take a proportion of. A categorical carries its categories independently of its data, so an all-undefined one still tabulates every category with a zero count; a string, datetime or duration array has no levels beyond those its data carries, so an all-missing one tabulates to an empty table. MATLAB agrees on the categorical case but returns a malformed 1-by-2 cell for an all-missing string or datetime array, lacking the label column its own documentation describes, while returning a well-formed 0-by-3 for an empty one. This implementation returns the empty table in both. See also: bar, pareto # name: # type: sq_string # elements: 1 # length: 54 Create a frequency table of unique values in vector x. # name: # type: sq_string # elements: 1 # length: 8 trimmean # name: # type: sq_string # elements: 1 # length: 2554 statistics: m = trimmean (x, p) statistics: m = trimmean (x, p, flag) statistics: m = trimmean (..., 'all') statistics: m = trimmean (..., dim) statistics: m = trimmean (..., vecdim) Compute the trimmed mean. The trimmed mean of x is defined as the mean of x excluding the highest and lowest k data values of x, calculated as k = n * (p / 100) / 2), where n is the sample size. m = trimmean (x, p) returns the mean of x after removing the outliers in x defined by p percent. If x is a vector, then trimmean (x, p) is the mean of all the values of x, computed after removing the outliers. If x is a matrix, then trimmean (x, p) is a row vector of column means, computed after removing the outliers. If x is a multidimensional array, then trimmean operates along the first nonsingleton dimension of x. To specify the operating dimension(s) when x is a matrix or a multidimensional array, use the dim or vecdim input argument. trimmean treats NaN values in x as missing values and removes them. Integer, logical and character x is computed and returned in double precision, although MATLAB keeps the integer class and rounds under 'weighted'. m = trimmean (x, p, flag) specifies how to trim when k, i.e. half the number of outliers, is not an integer. flag can be specified as one of the following values: Value Description 'round' Round k to the nearest integer. This is the default. 'floor' Round k down to the next smaller integer. 'weighted' If k = i + f, where i is an integer and f is a fraction, compute a weighted mean with weight (1 - f) for the (i + 1)-th and (n - i)-th values, and full weight for the values between them. m = trimmean (..., 'all') returns the trimmed mean of all the values in x using any of the input argument combinations in the previous syntaxes. m = trimmean (..., dim) returns the trimmed mean along the operating dimension dim specified as a positive integer scalar. If not specified, then the default value is the first nonsingleton dimension of x, i.e. whose size does not equal 1. If dim is greater than ndims (X) or if size (x, dim) is 1, then trimmean returns x. m = trimmean (..., vecdim) returns the trimmed mean over the dimensions specified in the vector vecdim. For example, if x is a 2-by-3-by-4 array, then mean (x, [1 2]) returns a 1-by-1-by-4 array. Each element of the output array is the mean of the elements on the corresponding page of x. If vecdim indexes all dimensions of x, then it is equivalent to mean (x, "all"). Any dimension in vecdim greater than ndims (x) is ignored. See also: mean # name: # type: sq_string # elements: 1 # length: 25 Compute the trimmed mean. statistics-release-2.1.2/inst/Descriptive_Statistics/ecdf.m000066400000000000000000000260711527311104300240560ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{f}, @var{x}] =} ecdf (@var{y}) ## @deftypefnx {statistics} {[@var{f}, @var{x}, @var{flo}, @var{fup}] =} ecdf (@var{y}) ## @deftypefnx {statistics} {} ecdf (@dots{}) ## @deftypefnx {statistics} {} ecdf (@var{ax}, @dots{}) ## @deftypefnx {statistics} {[@dots{}] =} ecdf (@var{y}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {statistics} {[@dots{}] =} ecdf (@var{ax}, @var{y}, @var{name}, @var{value}, @dots{}) ## ## Empirical (Kaplan-Meier) cumulative distribution function. ## ## @code{[@var{f}, @var{x}] = ecdf (@var{y})} calculates the Kaplan-Meier ## estimate of the cumulative distribution function (cdf), also known as the ## empirical cdf. @var{y} is a vector of data values. @var{f} is a vector of ## values of the empirical cdf evaluated at @var{x}. ## ## @code{[@var{f}, @var{x}, @var{flo}, @var{fup}] = ecdf (@var{y})} also returns ## lower and upper confidence bounds for the cdf. These bounds are calculated ## using Greenwood's formula, and are not simultaneous confidence bounds. ## ## @code{ecdf (@dots{})} without output arguments produces a plot of the ## empirical cdf. ## ## @code{ecdf (@var{ax}, @dots{})} plots into existing axes @var{ax}. ## ## @code{[@dots{}] = ecdf (@var{y}, @var{name}, @var{value}, @dots{})} specifies ## additional parameter name/value pairs chosen from the following: ## ## @multitable @columnfractions 0.20 0.8 ## @headitem @var{name} @tab @var{value} ## @item "censoring" @tab A boolean vector of the same size as Y that is 1 for ## observations that are right-censored and 0 for observations that are observed ## exactly. Default is all observations observed exactly. ## ## @item "frequency" @tab A vector of the same size as Y containing non-negative ## integer counts. The jth element of this vector gives the number of times the ## jth element of Y was observed. Default is 1 observation per Y element. ## ## @item "alpha" @tab A value @var{alpha} between 0 and 1 specifying the ## significance level. Default is 0.05 for 5% significance. ## ## @item "function" @tab The type of function returned as the F output argument, ## chosen from "cdf" (the default), "survivor", or "cumulative hazard". ## ## @item "bounds" @tab Either "on" to include bounds or "off" (the default) to ## omit them. Used only for plotting. ## @end multitable ## ## Type @code{demo ecdf} to see examples of usage. ## ## @seealso{cdfplot, ecdfhist} ## @end deftypefn function [Fout, x, Flo, Fup] = ecdf (y, varargin) ## Check for valid input arguments narginchk (1, Inf); ## Parse input arguments if (nargin == 1) ax = []; if (! isvector (y) || ! isreal (y)) error ("ecdf: Y must be a vector of real numbers."); endif ##x = varargin{1}; else ## ax = y; ## Check that ax is a valid axis handle try isstruct (get (y)); ax = y; y = varargin{1}; varargin{1} = []; catch ##error ("ecdf: invalid handle %f.", ax); ax = []; end_try_catch #y = varargin{1}; #varargin{1} = []; endif ## Make y a column vector x = y(:); ## Add defaults cens = zeros (size (x)); freq = ones (size (x)); alpha = 0.05; fname = 'cdf'; bound = 'off'; ## Check for remaining varargins and parse extra parameters if (length (varargin) > 0 && mod (numel (varargin), 2) == 0) [~, prop] = parseparams (varargin); while (! isempty (prop)) switch (lower (prop{1})) case 'censoring' cens = prop{2}; ## Check for valid input if (! isequal (size (x), size (cens))) error ("ecdf: censoring data mismatch Y vector."); endif ## Make double in case censoring data is logical if (islogical (cens)) cens = double (cens); endif case 'frequency' freq = prop{2}; ## Check for valid input if (! isequal (size (x), size (freq))) error ("ecdf: frequency data mismatch Y vector."); endif ## Make double in case frequency data is logical if (islogical (freq)) freq = double (freq); endif case 'alpha' alpha = prop{2}; ## Check for valid alpha value if (numel (alpha) != 1 || ! isnumeric (alpha) || alpha <= 0 || alpha >= 1) error ("ecdf: alpha must be a numeric scalar in the range (0,1)."); endif case 'function' fname = prop{2}; ## Check for valid function name option if (sum (strcmpi (fname, {'cdf', 'survivor', 'cumulative hazard'})) < 1) error ("ecdf: wrong function name."); endif case 'bounds' bound = prop{2}; ## Check for valid bounds option if (! (strcmpi (bound, 'off') || strcmpi (bound, 'on'))) error ("ecdf: wrong bounds."); endif otherwise error ("ecdf: unknown option %s", prop{1}); endswitch prop = prop(3:end); endwhile elseif nargin > 2 error ("ecdf: optional parameters must be in name/value pairs."); endif ## Remove NaNs from data rn = ! isnan (x) & ! isnan (cens) & freq > 0; x = x(rn); if (length (x) == 0) error ("ecdf: not enough data."); endif cens = cens(rn); freq = freq(rn); ## Sort data in ascending order [x, sr] = sort (x); cens = cens(sr); freq = freq(sr); ## Keep class for data (single | double) if (isa (x, 'single')) freq = single (freq); endif ## Calculate cumulative frequencies tcfreq = cumsum (freq); ocfreq = cumsum (freq .* ! cens); x_diff = (diff (x) == 0); if (any (x_diff)) x(x_diff) = []; tcfreq(x_diff) = []; ocfreq(x_diff) = []; endif max_count = tcfreq(end); ## Get Deaths and Number at Risk for each unique X Death = [ocfreq(1); diff(ocfreq)]; NRisk = max_count - [0; tcfreq(1:end-1)]; ## Remove no Death observations x = x(Death > 0); NRisk = NRisk(Death > 0); Death = Death(Death > 0); ## Estimate function switch (fname) case 'cdf' S = cumprod (1 - Death ./ NRisk); Fun_x = 1 - S; Fzero = 0; fdisp = 'F(x)'; case 'survivor' S = cumprod (1 - Death ./ NRisk); Fun_x = S; Fzero = 1; fdisp = 'S(x)'; case 'cumulative hazard' Fun_x = cumsum (Death ./ NRisk); Fzero = 0; fdisp = 'H(x)'; endswitch ## Check for remaining non-censored data and add a starting value if (! isempty (Death)) x = [min(y); x]; F = [Fzero; Fun_x]; else warning ("ecdf: No Death in data"); F = Fun_x; endif ## Calculate lower and upper confidence bounds if requested if (nargout > 2 || (nargout == 0 && strcmpi (bound, 'on'))) switch (fname) case {'cdf', 'survivor'} se = NaN (size (Death)); if (! isempty (Death)) if (NRisk(end) == Death(end)) t = 1:length (NRisk) - 1; else t = 1:length (NRisk); endif se(t) = S(t) .* sqrt (cumsum (Death(t) ./ ... (NRisk(t) .* (NRisk(t) - Death(t))))); endif case 'cumulative hazard' se = sqrt (cumsum (Death ./ (NRisk .* NRisk))); endswitch ## Calculate confidence limits if (! isempty (se)) z_a = - norminv (alpha / 2); h_w = z_a * se; Flo = max (0, Fun_x - h_w); Flo(isnan (h_w)) = NaN; switch (fname) case {'cdf', 'survivor'} Fup = min (1, Fun_x + h_w); Fup(isnan (h_w)) = NaN; case 'cumulative hazard' Fup = Fun_x + h_w; endswitch Flo = [NaN; Flo]; Fup = [NaN; Fup]; else Flo = []; Fup = []; endif else Flo = []; Fup = []; endif ## Plot stairs if no output is requested if (nargout == 0) if (isempty (ax)) ax = newplot (); endif h = stairs (ax, x , [F, Flo, Fup]); xlabel (ax, 'x'); ylabel (ax, fdisp); title ('ecdf'); else Fout = F; endif endfunction %!demo %! rande ('state', 42); %! y = exprnd (10, 50, 1); ## random failure times are exponential(10) %! d = exprnd (20, 50, 1); ## drop-out times are exponential(20) %! t = min (y, d); ## we observe the minimum of these times %! censored = (y > d); ## we also observe whether the subject failed %! %! ## Calculate and plot the empirical cdf and confidence bounds %! [f, x, flo, fup] = ecdf (t, 'censoring', censored); %! stairs (x, f); %! hold on; %! stairs (x, flo, 'r:'); stairs (x, fup, 'r:'); %! %! ## Superimpose a plot of the known true cdf %! xx = 0:.1:max (t); yy = 1 - exp (-xx / 10); plot (xx, yy, 'g-'); %! hold off; %!demo %! rande ('state', 42); %! R = wblrnd (100, 2, 100, 1); %! ecdf (R, 'Function', 'survivor', 'Alpha', 0.01, 'Bounds', 'on'); %! hold on %! x = 1:1:250; %! wblsurv = 1 - cdf ('weibull', x, 100, 2); %! plot (x, wblsurv, 'g-', 'LineWidth', 2) %! legend ('Empirical survivor function', 'Lower confidence bound', ... %! 'Upper confidence bound', 'Weibull survivor function', ... %! 'Location', 'northeast'); %! hold off ## Test input %!error ecdf (); %!error ecdf (randi (15,2)); %!error ecdf ([3,2,4,3+2i,5]); %!error kstest ([2,3,4,5,6],'tail'); %!error kstest ([2,3,4,5,6],'tail', 'whatever'); %!error kstest ([2,3,4,5,6],'function', ''); %!error kstest ([2,3,4,5,6],'badoption', 0.51); %!error kstest ([2,3,4,5,6],'tail', 0); %!error kstest ([2,3,4,5,6],'alpha', 0); %!error kstest ([2,3,4,5,6],'alpha', NaN); %!error kstest ([NaN,NaN,NaN,NaN,NaN],'tail', 'unequal'); %!error kstest ([2,3,4,5,6],'alpha', 0.05, 'CDF', [2,3,4;1,3,4;1,2,1]); ## Test output against MATLAB results %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! x = [2, 3, 4, 3, 5, 4, 6, 5, 8, 3, 7, 8, 9, 0]; %! [F, x, Flo, Fup] = ecdf (x); %! F_out = [0; 0.0714; 0.1429; 0.3571; 0.5; 0.6429; 0.7143; 0.7857; 0.9286; 1]; %! assert_equal (F, F_out, ones (10,1) * 1e-4); %! x_out = [0 0 2 3 4 5 6 7 8 9]'; %! assert_equal (x, x_out); %! Flo_out = [NaN, 0, 0, 0.1061, 0.2381, 0.3919, 0.4776, 0.5708, 0.7937, NaN]'; %! assert_equal (Flo, Flo_out, ones (10,1) * 1e-4); %! Fup_out = [NaN, 0.2063, 0.3262, 0.6081, 0.7619, 0.8939, 0.9509, 1, 1, NaN]'; %! assert_equal (Fup, Fup_out, ones (10,1) * 1e-4); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! x = [2, 3, 4, 3, 5, 4, 6, 5, 8, 3, 7, 8, 9, 0]; %! ecdf (x); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect statistics-release-2.1.2/inst/Descriptive_Statistics/fleissKappa.m000066400000000000000000000270731527311104300254220ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{kappa} =} fleissKappa (@var{X}) ## @deftypefnx {statistics} {@var{kappa} =} fleissKappa (@var{X}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{kappa}, @var{stats}] =} fleissKappa (@dots{}) ## ## Fleiss' kappa, the agreement among several raters beyond chance. ## ## @code{@var{kappa} = fleissKappa (@var{X})} returns Fleiss' kappa for ## @math{m} raters who each assigned the same @math{n} subjects to ## categories, @var{X} holding one row per subject and one column per rater. ## The ratings may be numeric or logical, categorical, or text in a cell array ## of character vectors or a string array. A subject with a missing rating, ## @qcode{NaN}, an undefined category, an empty text or a missing string, is ## left out, every subject having to be rated by all @math{m} raters. ## ## With @code{N(i,j)} the number of raters who put subject @math{i} in ## category @math{j}, @code{p(j)} the proportion of all ratings in category ## @math{j}, @code{Pbar} the mean over the subjects of ## @code{(sum (N(i,:) .^ 2) - m) / (m * (m - 1))}, the proportion of pairs ## of raters who agree, and @code{Pe = sum (p .^ 2)}, ## @tex ## $$ \kappa = \frac{\bar P - P_e}{1 - P_e}, $$ ## ## @end tex ## @ifnottex ## @code{kappa = (Pbar - Pe) / (1 - Pe)}, ## @end ifnottex ## 1 for perfect agreement and 0 for agreement no better than chance. With ## two raters it is Scott's pi, not Cohen's kappa, chance being estimated ## from the ratings of all raters together; @code{cohenKappa} measures the ## agreement of two raters from their own frequencies. ## ## The following @qcode{Name-Value} pairs are supported: ## ## @multitable @columnfractions 0.2 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Categories'} @tab The categories: numeric for numeric ## ratings, text for text or categorical ratings. The default is the sorted ## values of numeric ratings, the categories of categorical ratings, unused ## ones included, and the sorted texts of text ratings. A category no rater ## used has a kappa of @qcode{NaN} in @var{stats}. ## ## @item @qcode{'Exact'} @tab @qcode{false} (default) for Fleiss' kappa, or ## @qcode{true} for the variant of Conger (1980), which takes each rater's own ## frequencies into account and equals Cohen's kappa for two raters. It has ## no test, and @var{stats} then holds @qcode{NaN} for its statistic and ## p-value. ## @end multitable ## ## @code{[@var{kappa}, @var{stats}] = fleissKappa (@dots{})} also returns a ## structure with the fields ## ## @multitable @columnfractions 0.2 0.8 ## @item @qcode{po} @tab the observed agreement @code{Pbar} ## @item @qcode{pe} @tab the agreement expected by chance ## @item @qcode{se} @tab the standard error of kappa when there is no ## agreement beyond chance (Fleiss, Nee and Landis, 1979) ## @item @qcode{zval} @tab the statistic of the test that kappa is 0, ## @code{@var{kappa} / se} ## @item @qcode{p} @tab the two-sided p-value of that test ## @item @qcode{n} @tab the number of subjects ## @item @qcode{raters} @tab the number of raters @math{m} ## @item @qcode{counts} @tab the @math{n}-by-@math{c} matrix @code{N} ## @item @qcode{categories} @tab the categories, a numeric column or a cell ## array of character vectors ## @item @qcode{categorykappa} @tab the kappa of each category, the agreement ## on whether a subject belongs to it ## @item @qcode{categoryzval} @tab the statistic of the test that each ## category's kappa is 0 ## @item @qcode{categoryp} @tab the two-sided p-value of that test ## @end multitable ## ## No confidence interval is given, there being no standard large-sample ## standard error for kappa away from 0. Kappa is undefined when chance ## alone predicts complete agreement, as when every rating falls in one ## category, and such ratings are refused. MATLAB has no Fleiss' kappa, so ## @code{fleissKappa} is specific to Octave. ## ## References: ## @enumerate ## @item ## J. L. Fleiss. Measuring nominal scale agreement among many raters. ## @emph{Psychological Bulletin}, 76(5):378--382, 1971. ## @item ## J. L. Fleiss, J. C. M. Nee and J. R. Landis. Large sample variance of ## kappa in the case of different sets of raters. @emph{Psychological ## Bulletin}, 86(5):974--977, 1979. ## @item ## A. J. Conger. Integration and generalization of kappas for multiple ## raters. @emph{Psychological Bulletin}, 88(2):322--328, 1980. ## @end enumerate ## ## @seealso{cohenKappa, friedman} ## @end deftypefn function [kappa, stats] = fleissKappa (X, varargin) if (nargin < 1) print_usage (); endif if (! (ismatrix (X) && ndims (X) == 2 && ! ischar (X))) error ("fleissKappa: X must be a matrix with one column per rater."); endif ## Parse optional Name-Value paired arguments optNames = {'Categories', 'Exact'}; dfValues = {[], false}; [cats, exact, rest] = parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (rest)) if (mod (numel (rest), 2) == 0 && ischar (rest{1})) error ("fleissKappa: unknown parameter name '%s'.", rest{1}); endif error ("fleissKappa: optional arguments must be Name-Value pairs."); endif if (! (isscalar (exact) && (islogical (exact) || isnumeric (exact)) && any (exact == [0, 1]))) error ("fleissKappa: 'Exact' must be a logical scalar."); endif m = columns (X); if (m < 2) error ("fleissKappa: X must hold the ratings of at least two raters."); endif R = cell (1, m); for j = 1:m R{j} = X(:,j); endfor [codes, cats, errmsg] = ratingcodes (R, cats); if (! isempty (errmsg)) error ("fleissKappa: %s", errmsg); endif codes = codes(! any (isnan (codes), 2),:); n = rows (codes); if (n == 0) error ("fleissKappa: no subject was rated by every rater."); endif ## Count of raters per subject and category, and agreement c = numel (cats); N = zeros (n, c); for j = 1:m N += (codes(:,j) == (1:c)); endfor pj = sum (N, 1) / (n * m); po = mean ((sum (N .^ 2, 2) - m) / (m * (m - 1))); pe = sum (pj .^ 2); if (exact) ## Conger: less the variance over raters of each rater's proportions Q = zeros (m, c); for j = 1:m Q(j,:) = sum (codes(:,j) == (1:c), 1) / n; endfor pe -= sum (var (Q, 1, 1)) / (m - 1); endif if (pe >= 1) error (strcat ("fleissKappa: kappa is undefined when chance predicts", ... " complete agreement, as when every rating falls in one", ... " category.")); endif kappa = (po - pe) / (1 - pe); ## Tests under no agreement beyond chance (Fleiss, Nee and Landis, 1979) qj = 1 - pj; if (exact) se = NaN; else spq = sum (pj .* qj); se = sqrt (2 * (spq ^ 2 - sum (pj .* qj .* (qj - pj))) ... / (spq ^ 2 * n * m * (m - 1))); endif zval = kappa / se; p = 2 * normcdf (-abs (zval)); ## Each category's kappa, against all the others pjk = (sum (N .^ 2, 1) - n * m * pj) ./ (n * m * (m - 1) * pj); ck = (pjk - pj) ./ qj; cz = ck / sqrt (2 / (n * m * (m - 1))); cp = 2 * normcdf (-abs (cz)); stats = struct ('po', po, 'pe', pe, 'se', se, 'zval', zval, 'p', p, ... 'n', n, 'raters', m, 'counts', N, ... 'categories', {cats}, 'categorykappa', ck(:), ... 'categoryzval', cz(:), 'categoryp', cp(:)); endfunction %!demo %! ## Six psychiatrists diagnosed 30 patients (R's irr::diagnoses) %! D = [4 4 4 4 4 4; 2 2 2 5 5 5; 2 3 3 3 3 5; 5 5 5 5 5 5; 2 2 2 4 4 4; %! 1 1 3 3 3 3; 3 3 3 3 5 5; 1 1 3 3 3 4; 1 1 4 4 4 4; 5 5 5 5 5 5; %! 1 4 4 4 4 4; 1 2 4 4 4 4; 2 2 2 3 3 3; 1 4 4 4 4 4; 2 2 4 4 4 5; %! 3 3 3 3 3 5; 1 1 1 4 5 5; 1 1 1 1 1 2; 2 2 4 4 4 4; 1 3 3 5 5 5; %! 5 5 5 5 5 5; 2 4 4 4 4 4; 2 2 4 5 5 5; 1 1 4 4 4 4; 1 4 4 4 4 5; %! 2 2 2 2 2 4; 1 1 1 1 5 5; 2 2 4 4 4 4; 1 3 3 3 3 3; 5 5 5 5 5 5]; %! [kappa, stats] = fleissKappa (D) ## Test output ## Values checked against R 4.5.0, irr 0.85 (kappam.fleiss) %!shared D %! D = [4 4 4 4 4 4; 2 2 2 5 5 5; 2 3 3 3 3 5; 5 5 5 5 5 5; 2 2 2 4 4 4; %! 1 1 3 3 3 3; 3 3 3 3 5 5; 1 1 3 3 3 4; 1 1 4 4 4 4; 5 5 5 5 5 5; %! 1 4 4 4 4 4; 1 2 4 4 4 4; 2 2 2 3 3 3; 1 4 4 4 4 4; 2 2 4 4 4 5; %! 3 3 3 3 3 5; 1 1 1 4 5 5; 1 1 1 1 1 2; 2 2 4 4 4 4; 1 3 3 5 5 5; %! 5 5 5 5 5 5; 2 4 4 4 4 4; 2 2 4 5 5 5; 1 1 4 4 4 4; 1 4 4 4 4 5; %! 2 2 2 2 2 4; 1 1 1 1 5 5; 2 2 4 4 4 4; 1 3 3 3 3 3; 5 5 5 5 5 5]; %!test %! [k, s] = fleissKappa (D); %! assert_equal (k, 0.430244520060141, 1e-14); %! assert_equal (s.zval, 17.6518305829914, 1e-12); %! assert_equal (s.p, 2 * normcdf (-17.6518305829914), 1e-80); %! assert_equal (s.n, 30); %! assert_equal (s.raters, 6); %! assert_equal (s.categories, (1:5)'); %!test %! [~, s] = fleissKappa (D); %! assert_equal (s.categorykappa, [0.244755244755245; 0.244755244755245; ... %! 0.52; 0.471127272727273; ... %! 0.566117806823969], 1e-14); %! assert_equal (s.categoryzval, [5.1920427989222; 5.1920427989222; ... %! 11.0308657865101; 9.99411868042136; ... %! 12.0091722046705], 1e-12); %!test %! [k, s] = fleissKappa (D, 'Exact', true); %! assert_equal (k, 0.441808540329333, 1e-14); %! assert_equal (s.zval, NaN); %! assert_equal (s.p, NaN); %!test %! L = {'Depression', 'Personality', 'Schizophrenia', 'Neurosis', 'Other'}; %! assert_equal (fleissKappa (L(D)), 0.430244520060141, 1e-14); %!test %! L = {'Depression', 'Personality', 'Schizophrenia', 'Neurosis', 'Other'}; %! assert_equal (fleissKappa (categorical (L(D))), 0.430244520060141, 1e-14); %!test %! [k, s] = fleissKappa ([D; NaN, 1, 1, 1, 1, 1]); %! assert_equal (k, 0.430244520060141, 1e-14); %! assert_equal (s.n, 30); %!test %! [~, s] = fleissKappa (D, 'Categories', 1:6); %! assert_equal (s.categorykappa(6), NaN); %! assert_equal (size (s.counts), [30, 6]); %!test %! x = [1, 2, 3, 1, 2, 3, 2, 2, 1, 3, 3, 1, 2, 1, 3, 2, 2, 3, 1, 1]'; %! y = [1, 2, 2, 1, 3, 3, 2, 1, 1, 3, 2, 1, 2, 2, 3, 2, 3, 3, 1, 2]'; %! assert_equal (fleissKappa ([x, y], 'Exact', true), cohenKappa (x, y), 1e-14); ## Test input validation %!error fleissKappa () %!error ... %! fleissKappa (['ab'; 'cd']) %!error ... %! fleissKappa ([1; 2; 3]) %!error fleissKappa (D, 'Foo', 1) %!error ... %! fleissKappa (D, 'Exact') %!error ... %! fleissKappa (D, 'Exact', 2) %!error ... %! fleissKappa (D, 'Categories', 1:4) %!error ... %! fleissKappa (ones (4, 3)) %!error ... %! fleissKappa ([1, NaN; NaN, 2]) statistics-release-2.1.2/inst/Descriptive_Statistics/geomean.m000066400000000000000000000252271527311104300245720ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{m} =} geomean (@var{x}) ## @deftypefnx {statistics} {@var{m} =} geomean (@var{x}, "all") ## @deftypefnx {statistics} {@var{m} =} geomean (@var{x}, @var{dim}) ## @deftypefnx {statistics} {@var{m} =} geomean (@var{x}, @var{vecdim}) ## @deftypefnx {statistics} {@var{m} =} geomean (@dots{}, @var{nanflag}) ## ## Compute the geometric mean of @var{x}. ## ## @itemize ## @item If @var{x} is a vector, then @code{geomean(@var{x})} returns the ## geometric mean of the elements in @var{x} defined as ## @tex ## $$ {\rm geomean}(x) = \left( \prod_{i=1}^N x_i \right)^\frac{1}{N} ## = exp \left({1\over N} \sum_{i=1}^N log x_i \right) $$ ## ## @end tex ## @ifnottex ## ## @example ## geomean (@var{x}) = PROD_i @var{x}(i) ^ (1/N) ## @end example ## ## @end ifnottex ## @noindent ## where @math{N} is the length of the @var{x} vector. ## ## @item If @var{x} is a matrix, then @code{geomean(@var{x})} returns a row ## vector with the geometric mean of each columns in @var{x}. ## ## @item If @var{x} is a multidimensional array, then @code{geomean(@var{x})} ## operates along the first nonsingleton dimension of @var{x}. ## ## @item @var{x} must not contain any negative or complex values. ## @end itemize ## ## @code{geomean(@var{x}, "all")} returns the geometric mean of all the elements ## in @var{x}. If @var{x} contains any 0, then the returned value is 0. ## ## @code{geomean(@var{x}, @var{dim})} returns the geometric mean along the ## operating dimension @var{dim} of @var{x}. Calculating the harmonic mean of ## any subarray containing any 0 will return 0. ## ## @code{geomean(@var{x}, @var{vecdim})} returns the geometric mean over the ## dimensions specified in the vector @var{vecdim}. For example, if @var{x} is ## a 2-by-3-by-4 array, then @code{geomean(@var{x}, [1 2])} returns a ## 1-by-1-by-4 array. Each element of the output array is the geometric mean of ## the elements on the corresponding page of @var{x}. If @var{vecdim} indexes ## all dimensions of @var{x}, then it is equivalent to @code{geomean (@var{x}, ## "all")}. Any dimension in @var{vecdim} greater than @code{ndims (@var{x})} ## is ignored. ## ## @code{geomean(@dots{}, @var{nanflag})} specifies whether to exclude NaN ## values from the calculation, using any of the input argument combinations in ## previous syntaxes. By default, geomean includes NaN values in the calculation ## (@var{nanflag} has the value "includenan"). To exclude NaN values, set the ## value of @var{nanflag} to "omitnan". ## ## @seealso{harmmean, mean} ## @end deftypefn function m = geomean (x, varargin) if (nargin < 1 || nargin > 3) print_usage (); endif if (! isnumeric (x) || ! isreal (x) || ! all (x(! isnan (x))(:) >= 0)) error ("geomean: X must contain real nonnegative values."); endif ## Set initial conditions all_flag = false; omitnan = false; nvarg = numel (varargin); varg_chars = cellfun ('ischar', varargin); szx = size (x); ndx = ndims (x); if (nvarg > 1 && ! varg_chars(2:end)) ## Only first varargin can be numeric print_usage (); endif ## Process any other char arguments. if (any (varg_chars)) for i = varargin(varg_chars) switch (lower (i{:})) case 'all' all_flag = true; case 'omitnan' omitnan = true; case 'includenan' omitnan = false; otherwise print_usage (); endswitch endfor varargin(varg_chars) = []; nvarg = numel (varargin); endif ## Single numeric input argument, no dimensions given. if (nvarg == 0) if (all_flag) x = x(:); if (omitnan) x = x(! isnan (x)); endif if (any (x == 0)) m = 0; return; endif m = exp (sum (log (x), 1) ./ length (x)); elseif (ndx == 2 && isempty (x) && szx == [0,0]) m = NaN; else ## Find the first non-singleton dimension. (dim = find (szx != 1, 1)) || (dim = 1); n = szx(dim); if (omitnan) idx = isnan (x); n = sum (! idx, dim); x(idx) = 1; # log (1) = 0 endif m = exp (sum (log (x), dim) ./ n); m(m == -Inf) = 0; # handle zeros in X endif else ## Two numeric input arguments, dimensions given. Note scalar is vector! vecdim = varargin{1}; if (isempty (vecdim) || ! (isvector (vecdim) && all (vecdim > 0)) ... || any (rem (vecdim, 1)) || any (! isfinite (vecdim))) error ("geomean: DIM must be a positive integer scalar or vector."); endif if (isscalar (vecdim)) if (vecdim > ndx) m = x; else n = szx(vecdim); if (omitnan) nanx = isnan (x); n = sum (! nanx, vecdim); x(nanx) = 1; # log (1) = 0 endif m = exp (sum (log (x), vecdim) ./ n); m(m == -Inf) = 0; # handle zeros in X endif else vecdim = sort (vecdim); if (! all (diff (vecdim))) error (strcat ("geomean: VECDIM must contain non-repeating", ... " positive integers.")); endif ## Ignore exceeding dimensions in VECDIM vecdim(find (vecdim > ndims (x))) = []; if (isempty (vecdim)) m = x; else ## Move vecdims to dim 1. ## Calculate permutation vector remdims = 1 : ndx; # All dimensions remdims(vecdim) = []; # Delete dimensions specified by vecdim nremd = numel (remdims); ## If all dimensions are given, it is similar to all flag if (nremd == 0) x = x(:); if (omitnan) x = x(! isnan (x)); endif if (any (x == 0)) m = 0; return; endif m = exp (sum (log (x), 1) ./ length (x)); m(m == -Inf) = 0; # handle zeros in X else ## Permute to bring vecdims to front perm = [vecdim, remdims]; x = permute (x, perm); ## Reshape to squash all vecdims in dim1 num_dim = prod (szx(vecdim)); szx(vecdim) = []; szx = [ones(1, length(vecdim)), szx]; szx(1) = num_dim; x = reshape (x, szx); ## Calculate mean on dim1 if (omitnan) nanx = isnan (x); n = sum (! nanx, 1); x(nanx) = 1; # log (1) = 0 else n = szx(1); endif m = exp (sum (log (x), 1) ./ n); m(m == -Inf) = 0; # handle zeros in X ## Inverse permute back to correct dimensions m = ipermute (m, perm); endif endif endif endif endfunction ## Test single input and optional arguments "all", DIM, "omitnan") %!test %! x = [0:10]; %! y = [x;x+5;x+10]; %! assert_equal (geomean (x), 0); %! m = [0 9.462942809849169 14.65658770861967]; %! assert_equal (geomean (y, 2), m', 4e-14); %! assert_equal (geomean (y, 'all'), 0); %! y(2,4) = NaN; %! m(2) = 9.623207231679554; %! assert_equal (geomean (y, 2), [0 NaN m(3)]', 4e-14); %! assert_equal (geomean (y', 'omitnan'), m, 4e-14); %! z = y + 20; %! assert_equal (geomean (z, 'all'), NaN); %! assert_equal (geomean (z, 'all', 'includenan'), NaN); %! assert_equal (geomean (z, 'all', 'omitnan'), 29.59298474535024, 4e-14); %! m = [24.79790781765634 NaN 34.85638839503932]; %! assert_equal (geomean (z'), m, 4e-14); %! assert_equal (geomean (z', 'includenan'), m, 4e-14); %! m(2) = 30.02181156156319; %! assert_equal (geomean (z', 'omitnan'), m, 4e-14); %! assert_equal (geomean (z, 2, 'omitnan'), m', 4e-14); ## Test dimension indexing with vecdim in n-dimensional arrays %!test %! x = repmat ([1:20;6:25], [5 2 6 3]); %! assert_equal (size (geomean (x, [3 2])), [10 1 1 3]); %! assert_equal (size (geomean (x, [1 2])), [1 1 6 3]); %! assert_equal (size (geomean (x, [1 2 4])), [1 1 6]); %! assert_equal (size (geomean (x, [1 4 3])), [1 40]); %! assert_equal (size (geomean (x, [1 2 3 4])), [1 1]); ## Test results with vecdim in n-dimensional arrays and "omitnan" %!test %! x = repmat ([1:20;6:25], [5 2 6 3]); %! m = repmat ([8.304361203739333;14.3078118884256], [5 1 1 3]); %! assert_equal (geomean (x, [3 2]), m, 4e-13); %! x(2,5,6,3) = NaN; %! m(2,3) = NaN; %! assert_equal (geomean (x, [3 2]), m, 4e-13); %! m(2,3) = 14.3292729579901; %! assert_equal (geomean (x, [3 2], 'omitnan'), m, 4e-13); ## Test errors %!error geomean ('char') %!error geomean ([1 -1 3]) %!error ... %! geomean (repmat ([1:20;6:25], [5 2 6 3 5]), -1) %!error ... %! geomean (repmat ([1:20;6:25], [5 2 6 3 5]), 0) %!error ... %! geomean ([1 2 4], Inf) %!error ... %! geomean ([1 2 4], [1 Inf]) %!error ... %! geomean ([1 2 4], [Inf 1]) %!error ... %! geomean (repmat ([1:20;6:25], [5 2 6 3 5]), [1 1]) ## Test default handling of empty arrays. %!test %! a = geomean ([]); %! assert_equal (isnan (a), true); %! assert_equal (size (a), [1, 1]); %!assert_equal (geomean (ones (2, 0, 3, 2)), ones (1, 0, 3, 2)) %!assert_equal (geomean (ones (2, 0, 3, 2), [1, 2]), NaN (1, 1, 3, 2)) %!assert_equal (geomean (ones (2, 0, 3, 2), 'all'), NaN) %!assert_equal (geomean (ones (2, 0, 3, 2), 1), ones (1, 0, 3, 2)) %!assert_equal (geomean (ones (2, 0, 3, 2), 2), NaN (2, 1, 3, 2)) %!assert_equal (geomean (ones (2, 0, 3, 2), 3), ones (2, 0, 1, 2)) %!assert_equal (geomean (ones (2, 0, 3, 2), 4), ones (2, 0, 3)) %!assert_equal (geomean ([], 1), ones (1, 0)) %!assert_equal (geomean ([], 2), ones (0, 1)) %!assert_equal (geomean ([], 3), []) %!assert_equal (geomean (zeros (0, 3)), NaN (1, 3)) %!assert_equal (geomean (zeros (3, 0)), ones (1, 0)) %!assert_equal (geomean ([], 'all'), NaN) statistics-release-2.1.2/inst/Descriptive_Statistics/grpstats.m000066400000000000000000002150741527311104300250270ustar00rootroot00000000000000## Copyright (C) 2022-2025 Andreas Bertsatos ## Copyright (C) 2025 jayantchauhan <0001jayant@gmail.com> ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{stats} =} grpstats (@var{x}) ## @deftypefnx {statistics} {@var{stats} =} grpstats (@var{x}, @var{group}) ## @deftypefnx {statistics} {[@var{means}, @var{sem}, @var{counts}, @var{gname}] =} @ ## grpstats (@var{x}, @var{group}) ## @deftypefnx {statistics} {[@var{stats1}, @dots{}, @var{statsN}] =} grpstats @ ##(@var{x}, @var{group}, @var{whichstats}) ## @deftypefnx {statistics} {[@var{stats1}, @dots{}, @var{statsN}] =} grpstats @ ## (@var{x}, @var{group}, @var{whichstats}, @qcode{'Alpha'}, @var{alpha}) ## @deftypefnx {statistics} {@var{tblstats} =} grpstats (@var{tbl}) ## @deftypefnx {statistics} {@var{tblstats} =} grpstats (@var{tbl}, @var{groupvars}) ## @deftypefnx {statistics} {@var{tblstats} =} grpstats (@var{tbl}, @var{groupvars}, @var{whichstats}) ## @deftypefnx {statistics} {@var{tblstats} =} grpstats (@var{tbl}, @var{groupvars}, @ ## @var{whichstats}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {} grpstats (@var{x}, @var{group}, @var{alpha}) ## @deftypefnx {statistics} {@var{h} =} grpstats (@var{x}, @var{group}, @var{alpha}) ## ## Summary statistics by group. ## ## @code{grpstats} computes groupwise summary statistics for the data in ## @var{x}, which can be a numeric matrix or a table. Numeric vectors are ## treated as a single column matrix. @qcode{NaN}s are treated as missing ## values and removed from calculations. ## ## @subheading Syntax for Numeric Input ## ## @code{@var{stats} = grpstats (@var{x})} calculates the mean statistic for ## each column in @var{x} and returns it as row vector in @var{stats}. ## ## @code{@var{stats} = grpstats (@var{x}, @var{group})} calculates the mean ## statistic for each column in @var{x} grouped by @var{group}. The returned ## argument, @var{stats}, is also a matrix with equal columns as @var{x} and the ## number of rows is equal to the groups specified by @var{group}. ## ## The grouping variable, @var{group} can be a vector of any data type ## supported by the @code{grp2idx} function. Alternatively, it can be a cell ## vector specifying multiple grouping variables with each cell element ## containing any of the aforementioned supported grouping vectors. If ## @var{group} is empty (@code{[]}), then input @var{x} is treated as a single ## group. A group whose values are all missing is dropped, so a grouping ## variable that is entirely missing leaves no groups at all. ## ## @code{[@var{means}, @var{sem}, @var{counts}, @var{gname}] = grpstats ## (@var{x}, @var{group})} returns, without naming any statistic, the mean, the ## standard error of the mean, the number of non-@qcode{NaN} values and the ## group names, in that order. Up to four outputs may be requested this way. ## ## An empty @var{x} produces empty output with as many columns as @var{x} and ## no rows, one row per group being the general rule and an empty @var{x} ## having no groups. ## ## @code{[@var{stats1}, @dots{}, @var{statsN}] = grpstats (@var{x}, @var{group}, ## @var{whichstats})} calculates the summary statistics specified by the ## @var{whichstats} argument, which can include any of the available statistics ## shown below. The number of output arguments must match the number of ## requested statistics specified in @var{whichstats}. ## computes summary statistics for the numeric matrix @var{x} grouped by ## @var{group}. ## ## @var{x} must be a numeric vector or a 2-D matrix. Vectors are treated as ## a single-column matrix. ## ## @var{group} is a grouping variable that defines the groups for the rows of ## @var{x}. It can be a categorical variable, numeric vector, string array, or ## cell array of strings. @var{group} can also be a cell array containing ## multiple grouping variables. If @var{group} is empty (@code{[]}) or omitted, ## all of @var{x} is treated as a single group. ## ## @var{whichstats} specifies the statistics to compute. It can be either a ## string array or a cell array of strings specifying any of the following ## builtin statistics. If omitted, the default is @qcode{'mean'}. ## @var{whichstats} can also contain function handles for custom statistics. ## ## The available statistics are: ## @multitable @columnfractions 0.2 0.75 ## @item @qcode{'mean'} @tab Mean of each group. ## @item @qcode{'median'} @tab Median of each group. ## @item @qcode{'sem'} @tab Standard error of the mean for each group. ## @item @qcode{'std'} @tab Standard deviation of each group. ## @item @qcode{'var'} @tab Variance of each group. ## @item @qcode{'min'} @tab Minimum value in each group. ## @item @qcode{'max'} @tab Maximum value in each group. ## @item @qcode{'range'} @tab Difference between max and min in each ## group. ## @item @qcode{'numel'} @tab Number of elements (count) in each group. ## @item @qcode{'meanci'} @tab Confidence interval for the mean. ## @item @qcode{'predci'} @tab Prediction interval for a new observation. ## @item @qcode{'gname'} @tab Group names. ## @end multitable ## ## @code{[@dots{}] = grpstats (@dots{}, @qcode{'Alpha'}, @var{alpha})} specifies ## the significance level for the confidence intervals (@qcode{'meanci'} and ## @qcode{'predci'}) as @code{100 * (1-@var{alpha})@@%}. @var{alpha} must be a ## scalar between 0 and 1. When not specified, it defaults to 0.05. Note that ## this paired input argument is also valid for table input. ## ## @subheading Syntax for Table Input ## ## @code{@var{tblstats} = grpstats (@var{tbl}, @var{groupvars})} computes the ## summary statistics for the data in table @var{tbl}, grouped by the variables ## specified in @var{groupvars}. If @var{groupvars} is empty or omitted, then ## all of @var{tbl} is treated as a single group. @var{groupvars} can be a cell ## array of character vectors or a string array specifying one or more variable ## names in @var{tbl} to be used as grouping variables. Alternatively, all ## valid methods for indexing table variables are supported (e.g. @code{vartype} ## object, logical vector, function handle). ## ## The output @var{tblstats} is a table with one row for each group. It contains ## the grouping variables, an additional @qcode{'GroupCount'} variable, and the ## specified summary statistics for the variables in @var{tbl}, expect for those ## specified as grouping variables. When input is a table, only a single output ## variable, @var{tblstats} can be specified. The output @var{tblstats} also ## contains @qcode{RowNames}, which are the unique combinations of the specified ## groups, for which data are available in @var{tbl}. When no groups are ## specified, the row name of the single row output table defaults to ## @qcode{'All'}. ## ## @code{@var{tblstats} = grpstats (@var{tbl}, @var{groupvars}, ## @var{whichstats})} specifies which statistics to calculate for the variables ## in @var{tbl}. Unless specified, the mean is calculated for each variable. ## When specifying more than one statistic, @var{tblstats} contains multiple ## variables for each variable in @var{tbl} and each is named by combining the ## applied statistic with the name of the original variable. When a function ## handle is applied, its string representation is used instead. ## ## For table input specifically, @code{grpstats} also accepts the following ## paired arguments. ## ## @multitable @columnfractions 0.2 0.75 ## @headitem Name @tab Value ## @item @qcode{'DataVars'} @tab A vector specifying the variables in ## @var{tbl}, for which to calculate the specified statistics. The vector can ## be any of the valid options for indexing table variables. ## ## @item @qcode{'VarNames'} @tab A cell array of character vectors or a ## string array specifying the names of the variables in the output table. The ## number of specified names must match the number of expected variables in the ## output table. ## @end multitable ## ## @subheading Plotting Syntax ## ## The syntax @code{grpstats (@var{x}, @var{group}, @var{alpha})} generates an ## @code{errorbar} plot with the group means and their respective confidence ## intervals. @var{x} must be a numeric vector or matrix. @var{alpha} is a ## scalar between 0 and 1 that determines the confidence level. This syntax is ## an alternative to calling @code{errorbar} after computing @qcode{'mean'} and ## @qcode{'meanci'} statistics. The optional output @var{h} is a handle to the ## hggroup object representing the data plot and errorbars. ## ## @seealso{grp2idx} ## @end deftypefn function [varargout] = grpstats (x, group = [], whichstats = [], varargin) ## Check data input if (nargin < 1) print_usage (); endif if (ndims (x) != 2) error ("grpstats: X must be a matrix or a table."); endif if (! istable (x) && isvector (x)) x = x(:); endif [r, c] = size (x); ## For table input no more than 1 output argument is allowed if (istable (x) && nargout > 1) error ("grpstats: only one output argument in allowed when X is a table."); endif ## Add default grouping variable no_group = true; if (isempty (group)) grp_idx = ones (r, 1); if (r == 0) r_names = cell (0, 1); g_names = cell (0, 1); ngroups = 0; else r_names = {'All'}; g_names = {'1'}; ngroups = 1; endif no_group = false; endif ## Check for plotting functional form with three input arguments if (nargin == 3 && isnumeric (whichstats) && isscalar (whichstats) ... && whichstats > 0 && whichstats < 1) do_plot = true; alpha = whichstats; whichstats = []; fcn_names = {'mean', 'meanci'}; if (! (isnumeric (x))) error ("grpstats: X must be numeric to plot mean and CI for each group."); endif else do_plot = false; endif ## Parse statistical functions if (isempty (whichstats)) ## Without WHICHSTATS the outputs are the mean, the standard error, the ## group counts and the group names, in that order. default_fcn = {'mean', 'sem', 'numel', 'gname'}; if (nargout > numel (default_fcn)) error ("grpstats: too many output arguments."); endif fcn_names = default_fcn(1:max (1, nargout)); else if (ischar (whichstats) || isstring (whichstats)) fcn_names = cellstr (whichstats); elseif (iscellstr (whichstats)) fcn_names = whichstats; elseif (is_function_handle (whichstats)) fcn_names = {whichstats}; elseif (iscell (whichstats)) TF = cellfun (@(x) ischar (x) || is_function_handle (x), whichstats); if (all (TF)) fcn_names = whichstats; else error ("grpstats: invalid WHICHSTATS specification in cell array."); endif else #if (! (isscalar (whichstats) && whichstats > 0 && whichstats < 1)) error ("grpstats: invalid WHICHSTATS data type."); endif ## At this point we have cell array with either function names or ## function handles which need to be tested before going any further valid_fcn = {'mean', 'median', 'sem', 'std', 'var', 'min', 'max', ... 'range', 'numel', 'meanci', 'predci', 'gname'}; is_char = cellfun ('ischar', fcn_names); if (any (is_char)) isvalid = cellfun (@(x) ismember (x, valid_fcn), fcn_names(is_char)); if (! all (isvalid)) error ("grpstats: unrecognized function names in WHICHSTATS."); endif endif ## Function handles need to be tested on the actual data with try..catch ## blocks, because there are numerous combination in I/O size which does ## not worth to test preemptively. endif ## Parse optional Name-Value paired arguments optNames = {'Alpha', 'DataVars', 'VarNames'}; dfValues = {0.05, [], {}}; [alpha, DataVars, VarNames, args] = parsePairedArguments (optNames, dfValues, ... varargin(:)); if (! isempty (args)) tmp = args{1}; if (isscalar (tmp) && tmp > 0 && tmp < 1) alpha = tmp; else error ("grpstats: unrecognized input arguments."); endif endif ## Check alpha value if (! (isscalar (alpha) && alpha > 0 && alpha < 1)) error ("grpstats: 'alpha' must be a real scalar in the range (0,1)."); endif ## Force VarNames to cell array of character vectors if (! isempty (VarNames)) if (ischar (VarNames) || isstring (VarNames)) VarNames = cellstr (VarNames); endif if (! iscellstr (VarNames)) error ("grpstats: invalid data types for 'VarNames'."); endif endif ## Handle group and whichstats for matrices and tables if (istable (x)) ## Get grouping variables for table input data if (no_group) try grp_vars = x(:, group); # () returns groupvars as a table catch error ("grpstats: cannot resolve GROUPVARS in input table."); end_try_catch ## Get unique groups and their indices to table data [g_names, ~, grp_idx] = unique (grp_vars); ## Get number of groups ngroups = rows (g_names); ## Convert logical to double c_names = convertvars (g_names, @islogical, 'double'); ## Create unique names for each group, one grouping variable at a time cstrtbl = cell (ngroups, width (c_names)); for idx = 1:width (c_names) cstrtbl(:,idx) = cellstr (string (c_names{:,idx})); endfor r_names = cstrtbl(:,1); [cr, cc] = size (cstrtbl); tmp_tmp = repmat ({'_'}, cr, 1); for idx = 2:cc r_names = strcat (r_names, tmp_tmp, cstrtbl(:,idx)); endfor endif ## Compute group count by default in tables GroupCount = zeros (ngroups, 1); for idx = 1:ngroups GroupCount(idx,:) = sum (grp_idx == idx); endfor ## Add group count and row names if (no_group) g_names = addvars (g_names, GroupCount); g_names.Properties.RowNames = r_names; else g_names = table (GroupCount, 'RowNames', r_names); endif ## Check that DataVars exist (if given) if (! isempty (DataVars)) ## Octave extension allows DataVars to be specified as a vartype object ## or a function handle to select variables based on their data types. ## To avoid multiple checking for all possible referencing schemes, ## let's add another try...catch blocks try work_tbl = x(:, DataVars); catch error ("grpstats: invalid 'DataVars' reference to table X."); end_try_catch elseif (isempty (group)) ## No grouping variables, so every variable of the table gets used work_tbl = x; else ## No specified DataVars, all table except grouping variables gets used work_tbl = removevars (x, group); endif ## From this point we can start applying functions on table variables ## We have to apply each function to every working variable ## We name the variable accordingly and append the output of the ## function at the g_names table. ## The function is applied on each group of each variable ## Handle functions need try..catch block to ensure they are compatible for var_idx = 1:columns (work_tbl) vname = work_tbl.Properties.VariableNames(var_idx); vdata = work_tbl{:,var_idx}; ncols = columns (vdata); for fcn_idx = 1:numel (fcn_names) new_vdata = []; fcn_op = fcn_names{fcn_idx}; if (is_function_handle (fcn_op)) fname = {func2str(fcn_op)}; new_vname = strcat (fname, '_', vname); try new_vdata = applyhandle (vdata, grp_idx, ngroups, ncols, fcn_op); catch # no-op end_try_catch if (! isempty (new_vdata)) g_names = addvars (g_names, new_vdata, 'NewVariableNames', new_vname); endif else # it must be a function name switch (fcn_op) case 'mean' new_vname = strcat ('mean_', vname); for idx = 1:ngroups gdata = vdata(grp_idx == idx, :); new_vdata(idx,:) = mean (gdata, 1, 'omitnan'); endfor case 'median' new_vname = strcat ('median_', vname); for idx = 1:ngroups gdata = vdata(grp_idx == idx, :); new_vdata(idx,:) = median (gdata, 1, 'omitnan'); endfor case 'sem' new_vname = strcat ('sem_', vname); for idx = 1:ngroups gdata = vdata(grp_idx == idx, :); new_vdata(idx,:) = std (gdata, 0, 1, 'omitnan') ./ ... sqrt (size (gdata, 1) - sum (isnan (gdata), 1)); endfor case 'std' new_vname = strcat ('std_', vname); for idx = 1:ngroups gdata = vdata(grp_idx == idx, :); new_vdata(idx,:) = std (gdata, 0, 1, 'omitnan'); endfor case 'var' new_vname = strcat ('var_', vname); for idx = 1:ngroups gdata = vdata(grp_idx == idx, :); new_vdata(idx,:) = var (gdata, 0, 1, 'omitnan'); endfor case 'min' new_vname = strcat ('min_', vname); for idx = 1:ngroups gdata = vdata(grp_idx == idx, :); new_vdata(idx,:) = min (gdata, [], 1); endfor case 'max' new_vname = strcat ('max_', vname); for idx = 1:ngroups gdata = vdata(grp_idx == idx, :); new_vdata(idx,:) = max (gdata, [], 1); endfor case 'range' new_vname = strcat ('range_', vname); for idx = 1:ngroups gdata = vdata(grp_idx == idx, :); new_vdata(idx,:) = range (gdata, 1); endfor case 'numel' new_vname = strcat ('numel_', vname); for idx = 1:ngroups gdata = vdata(grp_idx == idx, :); new_vdata(idx,:) = size (gdata, 1) - sum (isnan (gdata), 1); endfor case 'meanci' new_vname = strcat ('meanci_', vname); ## Preallocate twice the columns in variable (lower, upper) new_vdata = NaN (ngroups, ncols * 2); for idx = 1:ngroups gdata = vdata(grp_idx == idx, :); m = mean (gdata, 1, 'omitnan'); n = size (gdata, 1) - sum (isnan (gdata), 1); s = std (gdata, 0, 1, 'omitnan') ./ sqrt (max (n,1)); ## Avoid invalid tinv calls for degenerate df df = max (n - 1, 0); tval = zeros (1, size (gdata, 2)); pos = (df > 0); if (any (pos)) tval(pos) = - tinv (alpha / 2, df(pos)); endif d = s .* tval; d(n < 2) = NaN; new_vdata(idx,[1:2:end]) = m - d; new_vdata(idx,[2:2:end]) = m + d; endfor case 'predci' new_vname = strcat ('predci_', vname); ## Preallocate twice the columns in variable (lower, upper) new_vdata = NaN (ngroups, ncols * 2); for idx = 1:ngroups gdata = vdata(grp_idx == idx, :); m = mean (gdata, 1, 'omitnan'); n = size (gdata, 1) - sum (isnan (gdata), 1); s = std (gdata, 0, 1, 'omitnan') .* sqrt (1 + (1 ./ max (n,1))); df = max (n - 1, 0); tval = zeros (1, size (gdata, 2)); pos = (df > 0); if (any (pos)) tval(pos) = - tinv (alpha / 2, df(pos)); endif d = s .* tval; d(n < 2) = NaN; new_vdata(idx,[1:2:end]) = m - d; new_vdata(idx,[2:2:end]) = m + d; endfor endswitch if (! isempty (new_vdata)) g_names = addvars (g_names, new_vdata, 'NewVariableNames', new_vname); endif endif endfor endfor ## Last check. VarNames must equal the number of expected variables if (! isempty (VarNames)) if (numel (VarNames) != columns (g_names)) error ("grpstats: 'VarNames' do not match expected variables."); endif g_names = renamevars (g_names, g_names.Properties.VariableNames, ... VarNames); endif varargout{1} = g_names; return; else ## Get groups for array input data if (no_group) if (iscell (group) && ! iscellstr (group)) ## Multiple grouping variables in cell array grp_vars = numel (group); [grp_idx, g_names1] = grp2idx (group{1}); if (numel (grp_idx) != r) error ("grpstats: samples in X and GROUPS mismatch."); endif all_g_names = {g_names1}; if (grp_vars > 1) for g_idx = 2:grp_vars [tmp_grp_idx, tmp_g_names] = grp2idx (group{g_idx}); if (numel (tmp_grp_idx) != r) error ("grpstats: samples in X and GROUPS mismatch."); endif grp_idx = [grp_idx, tmp_grp_idx]; all_g_names{g_idx} = tmp_g_names; endfor endif ## Get combination of unique groups and their common index to X. ## Rows missing in any grouping variable belong to no group. keep = ! any (isnan (grp_idx), 2); [g_names_idx, ~, keep_idx] = unique (grp_idx(keep,:), 'rows'); grp_idx = NaN (r, 1); grp_idx(keep) = keep_idx; ngroups = rows (g_names_idx); c_names = cell (ngroups, grp_vars); if (ngroups > 0) for gvar_idx = 1:grp_vars gn = all_g_names{gvar_idx}; c_names(:, gvar_idx) = gn(g_names_idx(:,gvar_idx)); endfor endif g_names = c_names; else [grp_idx, g_names] = grp2idx (group); ngroups = numel (g_names); if (numel (grp_idx) != r) error ("grpstats: samples in X and GROUPS mismatch."); endif endif endif ## Check for plot option if (do_plot) if (ngroups == 0) error ("grpstats: no groups to plot."); endif ## Calculate mean and ci mu = NaN (ngroups, c); ci = NaN (ngroups, c); for idx = 1:ngroups group_x = x(find (grp_idx == idx), :); mu(idx,:) = mean (group_x, 1, 'omitnan'); n = size (group_x, 1) - sum (isnan (group_x), 1); s = std (group_x, 0, 1, 'omitnan') ./ sqrt (max (n,1)); ## Avoid invalid tinv calls for degenerate df df = max (n - 1, 0); tval = zeros (1, size (group_x, 2)); pos = (df > 0); if (any (pos)) tval(pos) = - tinv (alpha / 2, df(pos)); endif d = s .* tval; d(n < 2) = NaN; ci(idx,:) = d; endfor ## Plot the error bars h = errorbar ([1:ngroups]', mu, ci); xticks ([1:ngroups]); xlim ([0, ngroups+1]); ## For multiple grouping variables, the ticklabels need some work labels = g_names(:,1); if (columns (g_names) > 1) newlines = repmat ("\n", ngroups, 1); for ic = 2:columns (g_names) labels = strcat (labels, newlines, g_names(:,ic)); endfor endif xticklabels (labels); ## MATLAB functional form does not return an output. In Octave, ## if output is requested, we return an axes handle to the plot. if (nargout > 0) varargout{1} = h; endif return; endif ## Check consistent number of output arguments fcn_num = numel (fcn_names); if (! (nargout == 0 && fcn_num == 1) && nargout != fcn_num) error ("grpstats: inconsistent number of output arguments."); endif ## From this point we can start applying functions on the entire array for fcn_idx = 1:fcn_num if (is_function_handle (fcn_names{fcn_idx})) varargout{fcn_idx} = applyhandle (x, grp_idx, ngroups, c, ... fcn_names{fcn_idx}); continue; endif switch (fcn_names{fcn_idx}) case 'mean' group_mean = NaN (ngroups, c); for idx = 1:ngroups group_x = x(find (grp_idx == idx), :); group_mean(idx,:) = mean (group_x, 1, 'omitnan'); endfor varargout{fcn_idx} = group_mean; case 'median' group_mean = NaN (ngroups, c); for idx = 1:ngroups group_x = x(find (grp_idx == idx), :); group_mean(idx,:) = median (group_x, 1, 'omitnan'); endfor varargout{fcn_idx} = group_mean; case 'sem' group_sem = NaN (ngroups, c); for idx = 1:ngroups group_x = x(find (grp_idx == idx), :); group_sem(idx,:) = std (group_x, 0, 1, 'omitnan') ./ ... sqrt (size (group_x, 1) - sum (isnan (group_x), 1)); endfor varargout{fcn_idx} = group_sem; case 'std' group_std = NaN (ngroups, c); for idx = 1:ngroups group_x = x(find (grp_idx == idx), :); group_std(idx,:) = std (group_x, 0, 1, 'omitnan'); endfor varargout{fcn_idx} = group_std; case 'var' group_var = NaN (ngroups, c); for idx = 1:ngroups group_x = x(find (grp_idx == idx), :); group_var(idx,:) = var (group_x, 0, 1, 'omitnan'); endfor varargout{fcn_idx} = group_var; case 'min' group_min = NaN (ngroups, c); for idx = 1:ngroups group_x = x(find (grp_idx == idx), :); group_min(idx,:) = nanmin (group_x); endfor varargout{fcn_idx} = group_min; case 'max' group_max = NaN (ngroups, c); for idx = 1:ngroups group_x = x(find (grp_idx == idx), :); group_max(idx,:) = nanmax (group_x); endfor varargout{fcn_idx} = group_max; case 'range' func_handle = @(x) range (x, 1); group_range = NaN (ngroups, c); for idx = 1:ngroups group_x = x(find (grp_idx == idx), :); group_range(idx,:) = range (group_x, 1); endfor varargout{fcn_idx} = group_range; case 'numel' group_numel = NaN (ngroups, c); for idx = 1:ngroups group_x = x(find (grp_idx == idx), :); group_numel(idx,:) = size (group_x, 1) - sum (isnan (group_x), 1); endfor varargout{fcn_idx} = group_numel; case 'meanci' ## Allocate as 3-D: [ngroups x c x 2] (lower, upper) group_meanci = NaN (ngroups, c, 2); for idx = 1:ngroups group_x = x(find (grp_idx == idx), :); m = mean (group_x, 1, 'omitnan'); n = size (group_x, 1) - sum (isnan (group_x), 1); s = std (group_x, 0, 1, 'omitnan') ./ sqrt (max (n,1)); ## Avoid invalid tinv calls for degenerate df df = max (n - 1, 0); tval = zeros (1, size (group_x, 2)); pos = (df > 0); if (any (pos)) tval(pos) = - tinv (alpha / 2, df(pos)); endif d = s .* tval; d(n < 2) = NaN; group_meanci(idx, :, 1) = m - d; group_meanci(idx, :, 2) = m + d; endfor ## MATLAB returns [ngroups x 2] when nvars == 1 ## Octave used canonical 3-D. if (c == 1) ## Reshape to [ngroups x 2] varargout{fcn_idx} = reshape (group_meanci, ngroups, 2); else varargout{fcn_idx} = group_meanci; endif case 'predci' ## Allocate as 3-D: [ngroups x c x 2] (lower, upper) group_predci = NaN (ngroups, c, 2); for idx = 1:ngroups group_x = x(find (grp_idx == idx), :); m = mean (group_x, 1, 'omitnan'); n = size (group_x, 1) - sum (isnan (group_x), 1); s = std (group_x, 0, 1, 'omitnan') .* sqrt (1 + (1 ./ max (n,1))); df = max (n - 1, 0); tval = zeros (1, size (group_x, 2)); pos = (df > 0); if (any (pos)) tval(pos) = - tinv (alpha / 2, df(pos)); endif d = s .* tval; d(n < 2) = NaN; group_predci(idx, :, 1) = m - d; group_predci(idx, :, 2) = m + d; endfor if (c == 1) varargout{fcn_idx} = reshape (group_predci, ngroups, 2); else varargout{fcn_idx} = group_predci; endif case 'gname' varargout{fcn_idx} = g_names; endswitch endfor endif endfunction ## A function handle in WHICHSTATS is applied to each column of each group, on ## the values of that column which are not NaN, and returns one value per call. function out = applyhandle (x, grp_idx, ngroups, c, fcn_op) out = NaN (ngroups, c); for idx = 1:ngroups group_x = x(find (grp_idx == idx), :); for col_idx = 1:c col_x = group_x(:, col_idx); value = fcn_op (col_x(! isnan (col_x))); if (! isscalar (value)) error (strcat ("grpstats: function in WHICHSTATS must return", ... " a scalar for each column of each group.")); endif out(idx, col_idx) = value; endfor endfor endfunction %!demo %! load carsmall; %! [m, p, g] = grpstats (Weight, Model_Year, {'mean', 'predci', 'gname'}) %! n = length (m); %! errorbar ((1:n)',m,p(:,2)-m); %! set (gca, 'xtick', 1:n, 'xticklabel', g); %! title ('95% prediction intervals for mean weight by year'); %!demo %! load carsmall; %! [m, p, g] = grpstats ([Acceleration,Weight/1000],Cylinders, ... %! {'mean', 'meanci', 'gname'}, 0.05) %! [c, r] = size (m); %! errorbar ((1:c)'.*ones (c,r),m,p(:,[(1:r)])-m); %! set (gca, 'xtick', 1:c, 'xticklabel', g); %! title ('95% prediction intervals for mean weight by year'); %!demo %! ## Plot mean and 95% CI for a single grouping variable %! load carsmall; %! grpstats (Weight, Model_Year, 0.05); %! title ('Mean Weight by Model Year'); %!demo %! ## Plot mean and 95% CI for two grouping variables %! load carsmall; %! grpstats (Weight, {Origin, Cylinders}, 0.05); %! title ('Mean Weight by Origin and Number of Cylinders'); %!test %! load carsmall %! means = grpstats (Acceleration, Origin); %! assert_equal (means, [14.4377; 18.0500; 15.8867; 16.3778; 16.6000; 15.5000], 0.001); %!test %! load carsmall %! [grpMin, grpMax, grp] = grpstats (Acceleration, Origin, {'min', 'max', ... %! 'gname'}); %! assert_equal (grpMin, [8.0; 15.3; 13.9; 12.2; 15.7; 15.5]); %! assert_equal (grpMax, [22.2; 21.9; 18.2; 24.6; 17.5; 15.5]); %!test %! load carsmall %! [grpMin, grpMax, grp] = grpstats (Acceleration, Origin, {'min', 'max', ... %! 'gname'}); %! assert_equal (grp', {'USA', 'France', 'Japan', 'Germany', 'Sweden', 'Italy'}); %!test %! load carsmall %! [m, p, g] = grpstats ([Acceleration, Weight/1000], Cylinders, ... %! {'mean', 'meanci', 'gname'}, 0.05); %! ## check meanci lower bounds (first slice) with tolerance %! expected_lower = [15.9163; 15.6622; 10.7968]; %! expected_upper = [17.4249; 17.2907; 12.4845]; %! assert_equal (abs (p(:,1,1)), expected_lower, 1e-3); %! assert_equal (abs (p(:,1,2)), expected_upper, 1e-3); %!test %! [mC, g] = grpstats ([], []); %! assert_equal (isempty (mC), true); %! assert_equal (isempty (g), true); %!test %! ## column vector, no group %! x = [1; 2; 3; 4; 5]; %! m = grpstats (x); %! expected = 3; %! assert_equal (m, expected); %!test %! ## row vector, no group %! x = [1 2 3 4 5]; %! m = grpstats (x); %! expected = 3; %! assert_equal (m, expected); %!test %! ## matrix, no group %! x = [1 2; 3 4; 5 6]; %! m = grpstats (x); %! expected = [3 4]; %! assert_equal (m, expected); %!test %! ## vector, numeric groups %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! m = grpstats (x, g); %! expected = [15; 35; 55]; %! assert_equal (m, expected); %!test %! ## vector, cellstr groups %! x = [10; 20; 30; 40; 50; 60]; %! g = {'A'; 'A'; 'B'; 'B'; 'C'; 'C'}; %! m = grpstats (x, g); %! expected = [15; 35; 55]; %! assert_equal (m, expected); %!test %! ## matrix, numeric groups %! x = [1 10; 2 20; 3 30; 4 40; 5 50; 6 60]; %! g = [1; 1; 2; 2; 3; 3]; %! m = grpstats (x, g); %! expected = [1.5 15; 3.5 35; 5.5 55]; %! assert_equal (m, expected); %!test %! ## NaN handling %! x = [1; NaN; 3; 4; NaN; 6]; %! g = [1; 1; 2; 2; 3; 3]; %! m = grpstats (x, g); %! expected = [1; 3.5; 6]; %! assert_equal (m, expected); %!test %! ## single group %! x = [1; 2; 3; 4; 5]; %! g = ones (5, 1); %! m = grpstats (x, g); %! expected = 3; %! assert_equal (m, expected); %!test %! ## single statistic %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! m = grpstats (x, g, 'mean'); %! expected = [15; 35; 55]; %! assert_equal (m, expected); %!test %! ## single statistic %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! m = grpstats (x, g, 'median'); %! expected = [15; 35; 55]; %! assert_equal (m, expected); %!test %! ## single statistic %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! s = grpstats (x, g, 'std'); %! expected = [7.07106781186548; 7.07106781186548; 7.07106781186548]; %! assert_equal (s, expected, 1e-14); %!test %! ## single statistic %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! v = grpstats (x, g, 'var'); %! expected = [50; 50; 50]; %! assert_equal (v, expected); %!test %! ## single statistic %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! s = grpstats (x, g, 'sem'); %! expected = [5; 5; 5]; %! assert_equal (s, expected); %!test %! ## single statistic %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! mn = grpstats (x, g, 'min'); %! expected = [10; 30; 50]; %! assert_equal (mn, expected); %!test %! ## single statistic %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! mx = grpstats (x, g, 'max'); %! expected = [20; 40; 60]; %! assert_equal (mx, expected); %!test %! ## single statistic %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! r = grpstats (x, g, 'range'); %! expected = [10; 10; 10]; %! assert_equal (r, expected); %!test %! ## single statistic %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! n = grpstats (x, g, 'numel'); %! expected = [2; 2; 2]; %! assert_equal (n, expected); %!test %! ## single statistic %! x = [10; 20; 30; 40; 50; 60]; %! g = {'A'; 'A'; 'B'; 'B'; 'C'; 'C'}; %! names = grpstats (x, g, 'gname'); %! expected = {'A'; 'B'; 'C'}; %! assert_equal (names, expected); %!test %! ## single statistic (default alpha) %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! ci = grpstats (x, g, 'meanci'); %! expected = [-48.5310236808735 78.5310236808735; -28.5310236808735 ... %! 98.5310236808735; -8.53102368087348 118.531023680873]; %! assert_equal (ci, expected, 1e-12); %!test %! ## single statistic (default alpha) %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! ci = grpstats (x, g, 'predci'); %! expected = [-95.0389608721344 125.038960872134; -75.0389608721344 ... %! 145.038960872134; -55.0389608721344 165.038960872134]; %! assert_equal (ci, expected, 1e-12); %!test %! ## mean + std %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! [m, s] = grpstats (x, g, {'mean', 'std'}); %! expected_m = [15; 35; 55]; %! expected_s = [7.07106781186548; 7.07106781186548; 7.07106781186548]; %! assert_equal (m, expected_m, 1e-14); %! assert_equal (s, expected_s, 1e-14); %!test %! ## min + max + range %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! [mn, mx, r] = grpstats (x, g, {'min', 'max', 'range'}); %! expected_mn = [10; 30; 50]; %! expected_mx = [20; 40; 60]; %! expected_r = [10; 10; 10]; %! assert_equal (mn, expected_mn); %! assert_equal (mx, expected_mx); %! assert_equal (r, expected_r); %!test %! ## mean + median + numel + gname %! x = [10; 20; 30; 40; 50; 60]; %! g = {'A'; 'A'; 'B'; 'B'; 'C'; 'C'}; %! [m, med, n, names] = grpstats (x, g, {'mean', 'median', 'numel', 'gname'}); %! expected_m = [15; 35; 55]; %! expected_med = [15; 35; 55]; %! expected_n = [2; 2; 2]; %! expected_names = {'A'; 'B'; 'C'}; %! assert_equal (m, expected_m); %! assert_equal (med, expected_med); %! assert_equal (n, expected_n); %! assert_equal (names, expected_names); %!test %! ## all basic statistics %! x = [10; 20; 30; 40; 50; 60; 70; 80]; %! g = [1; 1; 2; 2; 2; 2; 3; 3]; %! [m, med, s, v, se, mn, mx, r, n] = grpstats (x, g, {'mean', 'median', ... %! 'std', 'var', 'sem', ... %! 'min', 'max', 'range', ... %! 'numel'}); %! expected_m = [15; 45; 75]; %! expected_med = [15; 45; 75]; %! expected_s = [7.07106781186548; 12.9099444873581; 7.07106781186548]; %! expected_v = [50; 166.666666666667; 50]; %! expected_se = [5; 6.45497224367903; 5]; %! expected_mn = [10; 30; 70]; %! expected_mx = [20; 60; 80]; %! expected_r = [10; 30; 10]; %! expected_n = [2; 4; 2]; %! assert_equal (m, expected_m); %! assert_equal (med, expected_med); %! assert_equal (s, expected_s, 1e-13); %! assert_equal (v, expected_v, 1e-12); %! assert_equal (se, expected_se, 1e-14); %! assert_equal (mn, expected_mn); %! assert_equal (mx, expected_mx); %! assert_equal (r, expected_r); %! assert_equal (n, expected_n); %!test %! ## meanci-alpha-0.1 %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! ci = grpstats (x, g, 'meanci', 0.1); %! expected = [-16.5687575733752 46.5687575733752; 3.4312424266248 ... %! 66.5687575733752; 23.4312424266248 86.5687575733752]; %! assert_equal (ci, expected, 1e-13); %!test %! ## predci-alpha-0.1 %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! ci = grpstats (x, g, 'predci', 0.1); %! expected = [-39.6786920489106 69.6786920489106; -19.6786920489106 ... %! 89.6786920489106; 0.321307951089366 109.678692048911]; %! assert_equal (ci, expected, 1e-12); %!test %! ## meanci-alpha-0.01 %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! ci = grpstats (x, g, 'meanci', 0.01); %! expected = [-303.283705814358 333.283705814358; -283.283705814358 ... %! 353.283705814358; -263.283705814358 373.283705814358]; %! assert_equal (ci, expected, 3e-8); %!test %! ## predci-alpha-0.01 %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! ci = grpstats (x, g, 'predci', 0.01); %! expected = [-536.283549691775 566.283549691775; -516.283549691775 ... %! 586.283549691775; -496.283549691775 606.283549691775]; %! assert_equal (ci, expected, 3e-8); %!test %! ## meanci-alpha-0.2 %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! ci = grpstats (x, g, 'meanci', 0.2); %! expected = [-0.388417685876263 30.3884176858763; 19.6115823141237 ... %! 50.3884176858763; 39.6115823141237 70.3884176858763]; %! assert_equal (ci, expected, 1e-13); %!test %! ## predci-alpha-0.2 %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! ci = grpstats (x, g, 'predci', 0.2); %! expected = [-11.6535212800292 41.6535212800292; 8.34647871997083 ... %! 61.6535212800292; 28.3464787199708 81.6535212800292]; %! assert_equal (ci, expected, 1e-13); %!test %! ## meanci, name-value alpha=0.2 %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! ci = grpstats (x, g, 'meanci', 'alpha', 0.2); %! expected = [-0.388417685876263 30.3884176858763; 19.6115823141237 ... %! 50.3884176858763; 39.6115823141237 70.3884176858763]; %! assert_equal (ci, expected, 1e-13); %!test %! ## meanci + predci, alpha=0.01 %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 2; 2; 3; 3]; %! [ci_m, ci_p] = grpstats (x, g, {'meanci', 'predci'}, 0.01); %! expected_m = [-303.283705814358 333.283705814358; -283.283705814358 ... %! 353.283705814358; -263.283705814358 373.283705814358]; %! expected_p = [-536.283549691775 566.283549691775; -516.283549691775 ... %! 586.283549691775; -496.283549691775 606.283549691775]; %! assert_equal (ci_m, expected_m, 3e-8); %! assert_equal (ci_p, expected_p, 3e-8); %!test %! ## matrix, mean+std+numel %! x = [1 10; 2 20; 3 30; 4 40; 5 50; 6 60]; %! g = [1; 1; 2; 2; 3; 3]; %! [m, s, n] = grpstats (x, g, {'mean', 'std', 'numel'}); %! expected_m = [1.5 15; 3.5 35; 5.5 55]; %! expected_s = [0.707106781186548 7.07106781186548; 0.707106781186548 ... %! 7.07106781186548; 0.707106781186548 7.07106781186548]; %! expected_n = [2 2; 2 2; 2 2]; %! assert_equal (m, expected_m); %! assert_equal (s, expected_s, 1e-14); %! assert_equal (n, expected_n); %!test %! ## matrix with NaN, mean+numel %! x = [1 10; NaN 20; 3 NaN; 4 40; 5 50; 6 60]; %! g = [1; 1; 2; 2; 3; 3]; %! [m, n] = grpstats (x, g, {'mean', 'numel'}); %! expected_m = [1 15; 3.5 40; 5.5 55]; %! expected_n = [1 2; 2 1; 2 2]; %! assert_equal (m, expected_m); %! assert_equal (n, expected_n); %!test %! ## 3-column matrix, mean+min+max %! x = [1 100 1000; 2 200 2000; 3 300 3000; 4 400 4000]; %! g = [1; 1; 2; 2]; %! [m, mn, mx] = grpstats (x, g, {'mean', 'min', 'max'}); %! expected_m = [1.5 150 1500; 3.5 350 3500]; %! expected_mn = [1 100 1000; 3 300 3000]; %! expected_mx = [2 200 2000; 4 400 4000]; %! assert_equal (m, expected_m); %! assert_equal (mn, expected_mn); %! assert_equal (mx, expected_mx); %!test %! ## one element per group %! x = [1; 2; 3]; %! g = [1; 2; 3]; %! [m, s, n] = grpstats (x, g, {'mean', 'std', 'numel'}); %! expected_m = [1; 2; 3]; %! expected_s = [0; 0; 0]; %! expected_n = [1; 1; 1]; %! assert_equal (m, expected_m); %! assert_equal (s, expected_s); %! assert_equal (n, expected_n); %!test %! ## group with all NaN %! x = [1; 2; NaN; NaN; 5; 6]; %! g = [1; 1; 2; 2; 3; 3]; %! [m, s, n] = grpstats (x, g, {'mean', 'std', 'numel'}); %! expected_m = [1.5; NaN; 5.5]; %! expected_s = [0.707106781186548; NaN; 0.707106781186548]; %! expected_n = [2; 0; 2]; %! assert_equal (m, expected_m); %! assert_equal (s, expected_s, 1e-14); %! assert_equal (n, expected_n); %!test %! ## unequal group sizes %! x = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]; %! g = [1; 1; 1; 1; 2; 2; 2; 3; 3; 3]; %! [m, v, n] = grpstats (x, g, {'mean', 'var', 'numel'}); %! expected_m = [2.5; 6; 9]; %! expected_v = [1.66666666666667; 1; 1]; %! expected_n = [4; 3; 3]; %! assert_equal (m, expected_m); %! assert_equal (v, expected_v, 1e-14); %! assert_equal (n, expected_n); %!test %! ## non-consecutive numeric groups %! x = [10; 20; 30; 40; 50; 60]; %! g = [1; 1; 5; 5; 10; 10]; %! [m, names] = grpstats (x, g, {'mean', 'gname'}); %! expected_m = [15; 35; 55]; %! expected_names = {'1'; '5'; '10'}; %! assert_equal (m, expected_m); %! assert_equal (names, expected_names); %!test %! ## unsorted string groups %! x = [30; 10; 40; 20; 60; 50]; %! g = {'C'; 'A'; 'C'; 'A'; 'B'; 'B'}; %! [m, names] = grpstats (x, g, {'mean', 'gname'}); %! expected_m = [35; 15; 55]; %! expected_names = {'C'; 'A'; 'B'}; %! assert_equal (m, expected_m); %! assert_equal (names, expected_names); %!test %! ## 20 groups, one element each %! x = (1:20)'; %! g = (1:20)'; %! [m, n] = grpstats (x, g, {'mean', 'numel'}); %! expected_m = (1:20)'; %! expected_n = ones (20, 1); %! assert_equal (m, expected_m); %! assert_equal (n, expected_n); %!test %! ## large sample meanci %! x = (1:50)'; %! g = [ones(25, 1); 2 * ones(25, 1)]; %! ci = grpstats (x, g, 'meanci'); %! expected = [9.96202357522388 16.0379764247761; 34.9620235752239 ... %! 41.0379764247761]; %! assert_equal (ci, expected, 1e-13); %!test %! ## large sample predci %! x = (1:50)'; %! g = [ones(25, 1); 2 * ones(25, 1)]; %! ci = grpstats (x, g, 'predci'); %! expected = [-2.49070107176829 28.4907010717683; 22.5092989282317 ... %! 53.4907010717683]; %! assert_equal (ci, expected, 2e-14); %!test %! Y = [5; 6; 7; 4; 9; 8]; %! X = [1; 2; 3; 4; 5; 6]; %! Group = categorical ({'A'; 'A'; 'B'; 'B'; 'C'; 'C'}); %! tbl = table (Y, X, Group); %! stats_tbl = grpstats (tbl, 'Group', {'mean', 'numel'}); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Y', 'numel_Y', ... %! 'mean_X', 'numel_X'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'; 'C'}); %! assert_equal (stats_tbl.GroupCount, [2; 2; 2]); %! assert_equal (stats_tbl.mean_Y, [5.5; 5.5; 8.5]); %! assert_equal (stats_tbl.numel_Y, [2; 2; 2]); %! assert_equal (stats_tbl.mean_X, [1.5; 3.5; 5.5]); %! assert_equal (stats_tbl.numel_X, [2; 2; 2]); %!test %! Y = [5; 6; 7; 4; 9; 8]; %! Group = categorical ({'A'; 'A'; 'B'; 'B'; 'C'; 'C'}); %! tbl = table (Y, Group); %! stats_tbl = grpstats (tbl, 'Group', 'mean'); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Y'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'; 'C'}); %! assert_equal (stats_tbl.GroupCount, [2; 2; 2]); %! assert_equal (stats_tbl.mean_Y, [5.5; 5.5; 8.5]); %!test %! Y = [10; 20; 30; 40]; %! X = [100; 200; 300; 400]; %! Z = [1000; 2000; 3000; 4000]; %! Group = categorical ({'A'; 'A'; 'B'; 'B'}); %! tbl = table (Y, X, Z, Group); %! stats_tbl = grpstats (tbl, 'Group', {'mean', 'numel'}); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Y', 'numel_Y', 'mean_X', 'numel_X', 'mean_Z', 'numel_Z'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'}); %! assert_equal (stats_tbl.GroupCount, [2; 2]); %! assert_equal (stats_tbl.mean_Y, [15; 35]); %! assert_equal (stats_tbl.numel_Y, [2; 2]); %! assert_equal (stats_tbl.mean_X, [150; 350]); %! assert_equal (stats_tbl.numel_X, [2; 2]); %! assert_equal (stats_tbl.mean_Z, [1500; 3500]); %! assert_equal (stats_tbl.numel_Z, [2; 2]); %!test %! Y = [1; 2; 3; 4; 5; 6; 7; 8]; %! Group = categorical ({'A'; 'A'; 'A'; 'A'; 'B'; 'B'; 'B'; 'B'}); %! tbl = table (Y, Group); %! stats_tbl = grpstats (tbl, 'Group', 'mean'); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Y'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'}); %! assert_equal (stats_tbl.GroupCount, [4; 4]); %! assert_equal (stats_tbl.mean_Y, [2.5; 6.5]); %!test %! Y = [1; 2; 3; 4; 5; 6; 7]; %! Group = categorical ({'A'; 'A'; 'A'; 'A'; 'A'; 'B'; 'B'}); %! tbl = table (Y, Group); %! stats_tbl = grpstats (tbl, 'Group', {'mean', 'numel'}); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Y', 'numel_Y'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'}); %! assert_equal (stats_tbl.GroupCount, [5; 2]); %! assert_equal (stats_tbl.mean_Y, [3; 6.5]); %! assert_equal (stats_tbl.numel_Y, [5; 2]); %!test %! Y = [10; 20; 30]; %! Group = categorical ({'A'; 'B'; 'C'}); %! tbl = table (Y, Group); %! stats_tbl = grpstats (tbl, 'Group', 'mean'); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Y'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'; 'C'}); %! assert_equal (stats_tbl.GroupCount, [1; 1; 1]); %! assert_equal (stats_tbl.mean_Y, [10; 20; 30]); %!test %! Y = [5; 5; 5; 5]; %! Group = categorical ({'A'; 'A'; 'B'; 'B'}); %! tbl = table (Y, Group); %! stats_tbl = grpstats (tbl, 'Group', 'mean'); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Y'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'}); %! assert_equal (stats_tbl.GroupCount, [2; 2]); %! assert_equal (stats_tbl.mean_Y, [5; 5]); %!test %! Y = [1; NaN; 3; 4; NaN; 6]; %! X = [10; 20; NaN; 40; 50; NaN]; %! Group = categorical ({'A'; 'A'; 'B'; 'B'; 'C'; 'C'}); %! tbl = table (Y, X, Group); %! stats_tbl = grpstats (tbl, 'Group', 'mean'); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Y', 'mean_X'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'; 'C'}); %! assert_equal (stats_tbl.GroupCount, [2; 2; 2]); %! assert_equal (stats_tbl.mean_Y, [1; 3.5; 6]); %! assert_equal (stats_tbl.mean_X, [15; 40; 50]); %!test %! Y = [1; NaN; 3; 4; 5; 6]; %! Group = categorical ({'A'; 'A'; 'B'; 'B'; 'C'; 'C'}); %! tbl = table (Y, Group); %! stats_tbl = grpstats (tbl, 'Group', {'mean', 'numel'}); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Y', 'numel_Y'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'; 'C'}); %! assert_equal (stats_tbl.GroupCount, [2; 2; 2]); %! assert_equal (stats_tbl.mean_Y, [1; 3.5; 5.5]); %! assert_equal (stats_tbl.numel_Y, [1; 2; 2]); %!test %! Y = [100; 200; 300; 400; 500; 600]; %! Group = categorical ({'Group1'; 'Group1'; 'Group2'; 'Group2'; ... %! 'Group3'; 'Group3'}); %! tbl = table (Y, Group); %! stats_tbl = grpstats (tbl, 'Group', 'mean'); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Y'}); %! assert_equal (stats_tbl.Properties.RowNames, {'Group1'; 'Group2'; 'Group3'}); %! assert_equal (stats_tbl.GroupCount, [2; 2; 2]); %! assert_equal (stats_tbl.mean_Y, [150; 350; 550]); %!test %! Var1 = [1; 2; 3; 4]; %! Var2 = [10; 20; 30; 40]; %! Var3 = [100; 200; 300; 400]; %! Var4 = [1000; 2000; 3000; 4000]; %! Group = categorical ({'A'; 'A'; 'B'; 'B'}); %! tbl = table (Var1, Var2, Var3, Var4, Group); %! stats_tbl = grpstats (tbl, 'Group', 'mean'); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Var1', 'mean_Var2', 'mean_Var3', 'mean_Var4'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'}); %! assert_equal (stats_tbl.GroupCount, [2; 2]); %! assert_equal (stats_tbl.mean_Var1, [1.5; 3.5]); %! assert_equal (stats_tbl.mean_Var2, [15; 35]); %! assert_equal (stats_tbl.mean_Var3, [150; 350]); %! assert_equal (stats_tbl.mean_Var4, [1500; 3500]); %!test %! Y = [1.5; 2.5; 3.5; 4.5; 5.5; 6.5]; %! Group = categorical ({'A'; 'A'; 'B'; 'B'; 'C'; 'C'}); %! tbl = table (Y, Group); %! stats_tbl = grpstats (tbl, 'Group', 'mean'); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Y'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'; 'C'}); %! assert_equal (stats_tbl.GroupCount, [2; 2; 2]); %! assert_equal (stats_tbl.mean_Y, [2; 4; 6]); %!test %! Y = [-10; -20; 30; 40; 50; 60]; %! Group = categorical ({'A'; 'A'; 'B'; 'B'; 'C'; 'C'}); %! tbl = table (Y, Group); %! stats_tbl = grpstats (tbl, 'Group', 'mean'); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Y'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'; 'C'}); %! assert_equal (stats_tbl.GroupCount, [2; 2; 2]); %! assert_equal (stats_tbl.mean_Y, [-15; 35; 55]); %!test %! Y = [0; 0; 0; 0; 0; 0]; %! Group = categorical ({'A'; 'A'; 'B'; 'B'; 'C'; 'C'}); %! tbl = table (Y, Group); %! stats_tbl = grpstats (tbl, 'Group', 'mean'); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Y'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'; 'C'}); %! assert_equal (stats_tbl.GroupCount, [2; 2; 2]); %! assert_equal (stats_tbl.mean_Y, [0; 0; 0]); %!test %! Y = [1e6; 2e6; 3e6; 4e6; 5e6; 6e6]; %! Group = categorical ({'A'; 'A'; 'B'; 'B'; 'C'; 'C'}); %! tbl = table (Y, Group); %! stats_tbl = grpstats (tbl, 'Group', 'mean'); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Y'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'; 'C'}); %! assert_equal (stats_tbl.GroupCount, [2; 2; 2]); %! assert_equal (stats_tbl.mean_Y, [1.5e6; 3.5e6; 5.5e6]); %!test %! Y = (1:10)'; %! Group = categorical (repmat ({'A'; 'B'}, 5, 1)); %! tbl = table (Y, Group); %! stats_tbl = grpstats (tbl, 'Group', {'mean', 'numel'}); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, ... %! {'Group', 'GroupCount', 'mean_Y', 'numel_Y'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'}); %! assert_equal (stats_tbl.GroupCount, [5; 5]); %! assert_equal (stats_tbl.mean_Y, [5; 6]); %! assert_equal (stats_tbl.numel_Y, [5; 5]); %!test %! Y = (1:20)'; %! Group = categorical (repmat ({'A'; 'B'; 'C'; 'D'}, 5, 1)); %! tbl = table (Y, Group); %! stats_tbl = grpstats (tbl, 'Group', 'mean'); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Y'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'; 'C'; 'D'}); %! assert_equal (stats_tbl.GroupCount, [5; 5; 5; 5]); %! assert_equal (stats_tbl.mean_Y, [9; 10; 11; 12]); %!test %! Y = [1; 2; 3; 4; 5]; %! Group = categorical ({'A'; 'B'; 'C'; 'D'; 'E'}); %! tbl = table (Y, Group); %! stats_tbl = grpstats (tbl, 'Group', 'mean'); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Y'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'; 'C'; 'D'; 'E'}); %! assert_equal (stats_tbl.GroupCount, [1; 1; 1; 1; 1]); %! assert_equal (stats_tbl.mean_Y, [1; 2; 3; 4; 5]); %!test %! Score1 = [85; 90; 78; 92; 88; 76]; %! Score2 = [82; 88; 75; 90; 85; 73]; %! Group = categorical ({'High'; 'High'; 'Med'; 'Med'; 'Low'; 'Low'}); %! tbl = table (Score1, Score2, Group); %! stats_tbl = grpstats (tbl, 'Group', 'mean'); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Score1', 'mean_Score2'}); %! assert_equal (stats_tbl.Properties.RowNames, {'High'; 'Low'; 'Med'}); %! assert_equal (stats_tbl.GroupCount, [2; 2; 2]); %! assert_equal (stats_tbl.mean_Score1, [87.5; 82; 85]); %! assert_equal (stats_tbl.mean_Score2, [85; 79; 82.5]); %!test %! Height = [170; 175; 165; 180; 160; 185]; %! Weight = [70; 75; 65; 80; 60; 85]; %! Category = categorical ({'M'; 'M'; 'F'; 'F'; 'M'; 'M'}); %! tbl = table (Height, Weight, Category); %! stats_tbl = grpstats (tbl, 'Category', {'mean', 'numel'}); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Category', 'GroupCount', ... %! 'mean_Height', 'numel_Height', 'mean_Weight', 'numel_Weight'}); %! assert_equal (stats_tbl.Properties.RowNames, {'F'; 'M'}); %! assert_equal (stats_tbl.GroupCount, [2; 4]); %! assert_equal (stats_tbl.mean_Height, [172.5; 172.5]); %! assert_equal (stats_tbl.numel_Height, [2; 4]); %! assert_equal (stats_tbl.mean_Weight, [72.5; 72.5]); %! assert_equal (stats_tbl.numel_Weight, [2; 4]); %!test %! Value = [10.5; 11.2; 9.8; 10.1; 11.5; 10.8]; %! Type = categorical ({'A'; 'A'; 'A'; 'B'; 'B'; 'B'}); %! tbl = table (Value, Type); %! stats_tbl = grpstats (tbl, 'Type', 'numel'); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Type', 'GroupCount', ... %! 'numel_Value'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'}); %! assert_equal (stats_tbl.GroupCount, [3; 3]); %! assert_equal (stats_tbl.numel_Value, [3; 3]); %!test %! Data = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]; %! Label = categorical ({'A'; 'A'; 'A'; 'A'; 'A'; 'B'; 'B'; 'B'; 'B'; 'B'}); %! tbl = table (Data, Label); %! stats_tbl = grpstats (tbl, 'Label', {'mean', 'numel'}); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Label', 'GroupCount', ... %! 'mean_Data', 'numel_Data'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'}); %! assert_equal (stats_tbl.GroupCount, [5; 5]); %! assert_equal (stats_tbl.mean_Data, [3; 8]); %! assert_equal (stats_tbl.numel_Data, [5; 5]); %!test %! X1 = [1; 2; 3; 4]; %! X2 = [5; 6; 7; 8]; %! G = categorical ({'A'; 'A'; 'B'; 'B'}); %! tbl = table (X1, X2, G); %! stats_tbl = grpstats (tbl, 'G', 'mean'); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'G', 'GroupCount', ... %! 'mean_X1', 'mean_X2'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'}); %! assert_equal (stats_tbl.GroupCount, [2; 2]); %! assert_equal (stats_tbl.mean_X1, [1.5; 3.5]); %! assert_equal (stats_tbl.mean_X2, [5.5; 7.5]); %!test %! Measurement = [100; 150; 200; 250; 300; 350]; %! GroupVar = categorical ({'Control'; 'Control'; 'Treatment'; 'Treatment'; ... %! 'Placebo'; 'Placebo'}); %! tbl = table (Measurement, GroupVar); %! stats_tbl = grpstats (tbl, 'GroupVar', 'mean'); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'GroupVar', 'GroupCount', ... %! 'mean_Measurement'}); %! assert_equal (stats_tbl.Properties.RowNames, {'Control'; 'Placebo'; 'Treatment'}); %! assert_equal (stats_tbl.GroupCount, [2; 2; 2]); %! assert_equal (stats_tbl.mean_Measurement, [125; 325; 225]); %!test %! Y = [NaN; NaN; 3; 4; 5; 6]; %! Group = categorical ({'A'; 'A'; 'B'; 'B'; 'C'; 'C'}); %! tbl = table (Y, Group); %! stats_tbl = grpstats (tbl, 'Group', 'mean'); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Y'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'; 'C'}); %! assert_equal (stats_tbl.GroupCount, [2; 2; 2]); %! assert_equal (isequaln (stats_tbl.mean_Y, [NaN; 3.5; 5.5]), true); %!test %! Y = [1; 2; NaN; NaN; NaN; NaN]; %! Group = categorical ({'A'; 'A'; 'B'; 'B'; 'C'; 'C'}); %! tbl = table (Y, Group); %! stats_tbl = grpstats (tbl, 'Group', {'mean', 'numel'}); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Group', 'GroupCount', ... %! 'mean_Y', 'numel_Y'}); %! assert_equal (stats_tbl.Properties.RowNames, {'A'; 'B'; 'C'}); %! assert_equal (stats_tbl.GroupCount, [2; 2; 2]); %! assert_equal (isequaln (stats_tbl.mean_Y, [1.5; NaN; NaN]), true); %! assert_equal (stats_tbl.numel_Y, [2; 0; 0]); %!test %! Val = [5.5; 6.5; 7.5; 8.5]; %! Cat = categorical ({'X'; 'X'; 'Y'; 'Y'}); %! tbl = table (Val, Cat); %! stats_tbl = grpstats (tbl, 'Cat', 'numel'); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Cat', 'GroupCount', ... %! 'numel_Val'}); %! assert_equal (stats_tbl.Properties.RowNames, {'X'; 'Y'}); %! assert_equal (stats_tbl.GroupCount, [2; 2]); %! assert_equal (stats_tbl.numel_Val, [2; 2]); %!test %! A = [1; 2; 3; 4; 5; 6]; %! B = [10; 20; 30; 40; 50; 60]; %! C = [100; 200; 300; 400; 500; 600]; %! Grp = categorical ({'G1'; 'G1'; 'G2'; 'G2'; 'G3'; 'G3'}); %! tbl = table (A, B, C, Grp); %! stats_tbl = grpstats (tbl, 'Grp', {'mean', 'numel'}); %! assert_equal (istable (stats_tbl), true); %! assert_equal (stats_tbl.Properties.VariableNames, {'Grp', 'GroupCount', ... %! 'mean_A', 'numel_A', 'mean_B', 'numel_B', 'mean_C', 'numel_C'}); %! assert_equal (stats_tbl.Properties.RowNames, {'G1'; 'G2'; 'G3'}); %! assert_equal (stats_tbl.GroupCount, [2; 2; 2]); %! assert_equal (stats_tbl.mean_A, [1.5; 3.5; 5.5]); %! assert_equal (stats_tbl.numel_A, [2; 2; 2]); %! assert_equal (stats_tbl.mean_B, [15; 35; 55]); %! assert_equal (stats_tbl.numel_B, [2; 2; 2]); %! assert_equal (stats_tbl.mean_C, [150; 350; 550]); %! assert_equal (stats_tbl.numel_C, [2; 2; 2]); %!test %! x = [1; NaN; 3; 4]; %! g = [1; 1; 2; 2]; %! muci = grpstats (x, g, 'meanci'); %! assert_equal (muci, [NaN, NaN; -2.8531, 9.8531], 1e-4); %!test %! x = [1; NaN; 3; 4; 5; 6]; %! g = [1; 1; 1; 2; 2; 2]; %! predci = grpstats (x, g, 'predci'); %! assert_equal (predci, [-20.0078, 24.0078; 0.0317, 9.9683], 1e-4); %!test %! ## An empty X keeps its columns and has no groups %! assert_equal (grpstats (zeros (0, 3), zeros (0, 1)), zeros (0, 3)); %! assert_equal (grpstats (zeros (0, 1), zeros (0, 1)), zeros (0, 1)); %! assert_equal (grpstats (zeros (0, 3)), zeros (0, 3)); %! assert_equal (grpstats ([], []), []); %!test %! ## Every reducing statistic keeps the columns of an empty X %! g = zeros (0, 1); %! assert_equal (grpstats (zeros (0, 3), g, 'sem'), zeros (0, 3)); %! assert_equal (grpstats (zeros (0, 3), g, 'std'), zeros (0, 3)); %! assert_equal (grpstats (zeros (0, 3), g, 'var'), zeros (0, 3)); %! assert_equal (grpstats (zeros (0, 3), g, 'min'), zeros (0, 3)); %! assert_equal (grpstats (zeros (0, 3), g, 'max'), zeros (0, 3)); %! assert_equal (grpstats (zeros (0, 3), g, 'range'), zeros (0, 3)); %! assert_equal (grpstats (zeros (0, 3), g, 'numel'), zeros (0, 3)); %!test %! ## meanci and predci of an empty X keep the columns and the bound pair %! g = zeros (0, 1); %! assert_equal (size (grpstats (zeros (0, 3), g, 'meanci')), [0, 3, 2]); %! assert_equal (size (grpstats (zeros (0, 3), g, 'predci')), [0, 3, 2]); %! assert_equal (size (grpstats (zeros (0, 1), g, 'meanci')), [0, 2]); %! assert_equal (size (grpstats ([], [], 'meanci')), [0, 0, 2]); %!test %! ## gname of an empty X is an empty cell column %! assert_equal (grpstats (zeros (0, 3), zeros (0, 1), 'gname'), cell (0, 1)); %! assert_equal (grpstats ([], [], 'gname'), cell (0, 1)); %!test %! ## Without a grouping variable X is a single group named '1' %! assert_equal (grpstats ([1; 2; 3], [], 'gname'), {'1'}); %! assert_equal (grpstats ([1; 2; 3], [], 'mean'), 2); %!test %! ## A grouping variable that is entirely missing leaves no groups %! assert_equal (grpstats ([1; 2; 3], [NaN; NaN; NaN]), zeros (0, 1)); %! assert_equal (grpstats ([1; 2; 3], [NaN; NaN; NaN], 'std'), zeros (0, 1)); %! assert_equal (grpstats ([1; 2; 3], [NaN; NaN; NaN], 'gname'), cell (0, 1)); %! assert_equal (grpstats ([1; 2; 3], {''; ''; ''}), zeros (0, 1)); %!test %! ## A row missing in any of several grouping variables belongs to no group %! y = [1; 2; 3; 4]; %! assert_equal (grpstats (y, {[NaN; 1; 2; 2], [1; 2; 1; 2]}), [2; 3; 4]); %! gone = {[NaN; NaN; NaN; NaN], [1; 2; 1; 2]}; %! assert_equal (grpstats (y, gone), zeros (0, 1)); %!test %! ## gname over several grouping variables has one column per variable %! y = [1; 2; 3; 4]; %! gn = grpstats (y, {[NaN; NaN; NaN; NaN], [1; 2; 1; 2]}, 'gname'); %! assert_equal (gn, cell (0, 2)); %! gn = grpstats (y, {[1; 1; 2; 2], [1; 2; 1; 2]}, 'gname'); %! assert_equal (gn, {'1', '1'; '1', '2'; '2', '1'; '2', '2'}); %!test %! ## Several empty grouping variables keep the columns of X %! g = zeros (0, 1); %! assert_equal (grpstats (zeros (0, 3), {g, g}), zeros (0, 3)); %! assert_equal (grpstats (zeros (0, 3), {g, g}, 'gname'), cell (0, 2)); %!test %! ## Without WHICHSTATS the outputs are mean, sem, counts and names %! [m, s, n, g] = grpstats ([1; 2; 3; 4], [1; 1; 2; 2]); %! assert_equal (m, [1.5; 3.5]); %! assert_equal (s, [0.5; 0.5]); %! assert_equal (n, [2; 2]); %! assert_equal (g, {'1'; '2'}); %!test %! ## The default counts and standard error ignore NaNs %! [m, s, n] = grpstats ([1; 2; NaN; 4], [1; 1; 2; 2]); %! assert_equal (m, [1.5; 4]); %! assert_equal (s, [0.5; 0]); %! assert_equal (n, [2; 1]); %!test %! ## The default outputs are computed column by column %! [m, s, n] = grpstats ([1, 10; 3, 20; 5, 30; 7, 40], [1; 1; 2; 2]); %! assert_equal (m, [2, 15; 6, 35]); %! assert_equal (s, [1, 5; 1, 5], 1e-14); %! assert_equal (n, [2, 2; 2, 2]); %!test %! ## The default outputs of an empty X keep its columns %! [m, s, n, g] = grpstats (zeros (0, 3), zeros (0, 1)); %! assert_equal (m, zeros (0, 3)); %! assert_equal (s, zeros (0, 3)); %! assert_equal (n, zeros (0, 3)); %! assert_equal (g, cell (0, 1)); %!test %! ## The standard error is taken column by column %! assert_equal (grpstats ([1, 10; 2, 20; 3, 30], [1; 1; 1], 'sem'), ... %! [0.5773502691896258, 5.773502691896258], 1e-14); %!test %! ## The standard error divides each column by its own count %! assert_equal (grpstats ([1, 10; 2, NaN; 3, 30], [1; 1; 1], 'sem'), ... %! [0.5773502691896258, 10], 1e-14); %!test %! ## 'VarNames' renames every variable of the output table %! Y = [1; 2; 3; 4]; %! Z = [10; 20; 30; 40]; %! G = {'a'; 'a'; 'b'; 'b'}; %! tbl = grpstats (table (Y, Z, G), 'G', 'mean', ... %! 'VarNames', {'grp', 'n', 'avgY', 'avgZ'}); %! assert_equal (tbl.Properties.VariableNames, {'grp', 'n', 'avgY', 'avgZ'}); %! assert_equal (tbl.n, [2; 2]); %! assert_equal (tbl.avgY, [1.5; 3.5]); %! assert_equal (tbl.avgZ, [15; 35]); %!test %! ## Without 'VarNames' the output table keeps the default names %! Y = [1; 2; 3; 4]; %! G = {'a'; 'a'; 'b'; 'b'}; %! tbl = grpstats (table (Y, G), 'G', 'mean'); %! assert_equal (tbl.Properties.VariableNames, {'G', 'GroupCount', 'mean_Y'}); %!test %! ## A function handle is applied to each column of each group %! assert_equal (grpstats ([1; 2; 3; 4], [1; 1; 2; 2], @mean), [1.5; 3.5]); %! x = [1, 10; 3, 20; 5, 30; 7, 40]; %! assert_equal (grpstats (x, [1; 1; 2; 2], @mean), [2, 15; 6, 35]); %!test %! ## A function handle sees only the values that are not NaN %! assert_equal (grpstats ([1; 2; NaN; 4], [1; 1; 2; 2], @mean), [1.5; 4]); %! assert_equal (grpstats ([1; 2; NaN; 4], [1; 1; 2; 2], @numel), [2; 1]); %! x = [1, 10; 2, NaN; 3, 30; 4, 40]; %! assert_equal (grpstats (x, [1; 1; 1; 1], @mean), [2.5, 80/3], 1e-14); %!test %! ## A function handle may be mixed with named statistics %! [m, s] = grpstats ([1; 2; 3; 4], [1; 1; 2; 2], {@mean, 'sem'}); %! assert_equal (m, [1.5; 3.5]); %! assert_equal (s, [0.5; 0.5]); %!test %! ## An anonymous handle is applied like any other %! fcn = @(v) max (v) - min (v); %! assert_equal (grpstats ([1; 2; 3; 4], [1; 1; 2; 2], fcn), [1; 1]); %! assert_equal (size (grpstats (zeros (0, 3), zeros (0, 1), @mean)), [0, 3]); %!test %! ## A table variable takes the handle by its own name, NaNs removed %! Y = [1; 2; NaN; 4]; %! G = {'a'; 'a'; 'b'; 'b'}; %! tbl = grpstats (table (Y, G), 'G', @mean); %! assert_equal (tbl.Properties.VariableNames, {'G', 'GroupCount', 'mean_Y'}); %! assert_equal (tbl.mean_Y, [1.5; 4]); %! tbl = grpstats (table (Y, G), 'G', @numel); %! assert_equal (tbl.numel_Y, [2; 1]); %!test %! ## A table with no grouping variable is a single group named 'All' %! tbl = grpstats (table ([1; 2; 3], 'VariableNames', {'v'})); %! assert_equal (tbl.Properties.VariableNames, {'GroupCount', 'mean_v'}); %! assert_equal (tbl.Properties.RowNames, {'All'}); %! assert_equal (tbl.GroupCount, 3); %! assert_equal (tbl.mean_v, 2); %!test %! ## Every variable of an ungrouped table is a data variable %! Y = [1; 2; 3; 4]; %! Z = [10; 20; 30; 40]; %! tbl = grpstats (table (Y, Z)); %! assert_equal (tbl.Properties.VariableNames, ... %! {'GroupCount', 'mean_Y', 'mean_Z'}); %! assert_equal (tbl.mean_Y, 2.5); %! assert_equal (tbl.mean_Z, 25); %!test %! ## An ungrouped table takes WHICHSTATS and 'DataVars' as a grouped one does %! Y = [1; 2; 3; 4]; %! Z = [10; 20; 30; 40]; %! tbl = grpstats (table (Y, Z), [], {'mean', 'sem'}); %! assert_equal (tbl.Properties.VariableNames, ... %! {'GroupCount', 'mean_Y', 'sem_Y', 'mean_Z', 'sem_Z'}); %! assert_equal (tbl.sem_Y, 0.6454972243679028, 1e-14); %! tbl = grpstats (table (Y, Z), [], 'mean', 'DataVars', 'Y'); %! assert_equal (tbl.Properties.VariableNames, {'GroupCount', 'mean_Y'}); %!test # MATLAB parity: a group per category, in category order, used or not %! g = categorical ({'hi'; 'hi'; 'lo'; 'lo'; 'hi'; 'lo'}, {'mid', 'lo', 'hi'}); %! m = grpstats ([1; 2; 5; 6; 3; 7], g); %! assert_equal (m, [NaN; 6; 2]); ## Test input validation %!error grpstats (ones (2, 2, 2)) %!error ... %! [a, b] = grpstats (table (1)) %!error ... %! grpstats (ones (6, 2), [1; 1; 1; 2; 2; 2], {'mean', 1.5}) %!error ... %! grpstats (ones (6, 2), [1; 1; 1; 2; 2; 2], 1.5) %!error ... %! grpstats (ones (6, 2), [1; 1; 1; 2; 2; 2], 'some_function') %!error ... %! grpstats (ones (6, 2), [1; 1; 1; 2; 2; 2], 'mean', 35) %!error ... %! grpstats ([1:4]', {'A'; 'B'; 'A'; 'B'}, 'predci', 'somename', -0.1); %!error ... %! grpstats (ones (6, 2), [1; 1; 1; 2; 2; 2], 'mean', 'VarNames', 3) %!error ... %! grpstats ({ones(6, 2)}, [], 0.05) %!error ... %! grpstats ([1:4]', {'A'; 'B'; 'A'; 'B'}, 'predci', 'alpha', -0.1); %!error ... %! grpstats (table ([1:5]'), {'Var_5'}) %!error ... %! grpstats (table ([1:5]'), {'Var1'}, [], 'DataVars', 'Var5') %!error ... %! grpstats (table ([1:5]', [1:5]'), {'Var1'}, [], 'VarNames', {'A', 'B'}) %!error ... %! grpstats ([1:5]', {'A'; 'B'; 'A'; 'B'}) %!error ... %! m = grpstats ([1:4]', {'A'; 'B'; 'A'; 'B'}, {'mean', 'std'}) %!error ... %! [a, b, c, d, e] = grpstats ([1; 2; 3; 4], [1; 1; 2; 2]) %!error ... %! grpstats (zeros (0, 3), zeros (0, 1), 0.05) %!error ... %! grpstats ([1; 2; 3], [NaN; NaN; NaN], 0.05) %!error ... %! grpstats ([1; 2; 3; 4], [1; 1; 2; 2], @(v) [min(v), max(v)]) statistics-release-2.1.2/inst/Descriptive_Statistics/harmmean.m000066400000000000000000000272651527311104300247530ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{m} =} harmmean (@var{x}) ## @deftypefnx {statistics} {@var{m} =} harmmean (@var{x}, "all") ## @deftypefnx {statistics} {@var{m} =} harmmean (@var{x}, @var{dim}) ## @deftypefnx {statistics} {@var{m} =} harmmean (@var{x}, @var{vecdim}) ## @deftypefnx {statistics} {@var{m} =} harmmean (@dots{}, @var{nanflag}) ## ## Compute the harmonic mean of @var{x}. ## ## @itemize ## @item If @var{x} is a vector, then @code{harmmean(@var{x})} returns the ## harmonic mean of the elements in @var{x} defined as ## @tex ## $$ {\rm harmmean}(x) = \frac{N}{\sum_{i=1}^N \frac{1}{x_i}} $$ ## ## @end tex ## @ifnottex ## ## @example ## harmmean (@var{x}) = N / SUM_i @var{x}(i)^-1 ## @end example ## ## @end ifnottex ## @noindent ## where @math{N} is the length of the @var{x} vector. ## ## @item If @var{x} is a matrix, then @code{harmmean(@var{x})} returns a row ## vector with the harmonic mean of each columns in @var{x}. ## ## @item If @var{x} is a multidimensional array, then @code{harmmean(@var{x})} ## operates along the first nonsingleton dimension of @var{x}. ## ## @item @var{x} must not contain any negative or complex values. ## @end itemize ## ## @code{harmmean(@var{x}, "all")} returns the harmonic mean of all the elements ## in @var{x}. If @var{x} contains any 0, then the returned value is 0. ## ## @code{harmmean(@var{x}, @var{dim})} returns the harmonic mean along the ## operating dimension @var{dim} of @var{x}. Calculating the harmonic mean of ## any subarray containing any 0 will return 0. ## ## @code{harmmean(@var{x}, @var{vecdim})} returns the harmonic mean over the ## dimensions specified in the vector @var{vecdim}. For example, if @var{x} is ## a 2-by-3-by-4 array, then @code{harmmean(@var{x}, [1 2])} returns a ## 1-by-1-by-4 array. Each element of the output array is the harmonic mean of ## the elements on the corresponding page of @var{x}. If @var{vecdim} indexes ## all dimensions of @var{x}, then it is equivalent to @code{harmmean (@var{x}, ## "all")}. Any dimension in @var{vecdim} greater than @code{ndims (@var{x})} ## is ignored. ## ## @code{harmmean(@dots{}, @var{nanflag})} specifies whether to exclude NaN ## values from the calculation, using any of the input argument combinations in ## previous syntaxes. By default, harmmean includes NaN values in the ## calculation (@var{nanflag} has the value "includenan"). To exclude NaN ## values, set the value of @var{nanflag} to "omitnan". ## ## @seealso{geomean, mean} ## @end deftypefn function m = harmmean (x, varargin) if (nargin < 1 || nargin > 3) print_usage (); endif if (! isnumeric (x) || ! isreal (x) || ! all (x(! isnan (x))(:) >= 0)) error ("harmmean: X must contain real nonnegative values."); endif ## Set initial conditions all_flag = false; omitnan = false; nvarg = numel (varargin); varg_chars = cellfun ('ischar', varargin); szx = size (x); ndx = ndims (x); if (nvarg > 1 && ! varg_chars(2:end)) ## Only first varargin can be numeric print_usage (); endif ## Process any other char arguments. if (any (varg_chars)) for i = varargin(varg_chars) switch (lower (i{:})) case 'all' all_flag = true; case 'omitnan' omitnan = true; case 'includenan' omitnan = false; otherwise print_usage (); endswitch endfor varargin(varg_chars) = []; nvarg = numel (varargin); endif ## Single numeric input argument, no dimensions given. if (nvarg == 0) if (all_flag) x = x(:); if (omitnan) x = x(! isnan (x)); endif m = length (x) ./ sum (1 ./ x); else if (ndx == 2 && isempty (x) && szx == [0,0]) m = NaN; else ## Find the first non-singleton dimension. (dim = find (szx != 1, 1)) || (dim = 1); n = szx(dim); is_nan = 0; if (omitnan) idx = isnan (x); n = sum (! idx, dim); is_nan = sum (idx, dim); x(idx) = 1; # remove NaNs by subtracting is_nan below endif m = n ./ (sum (1 ./ x, dim) - is_nan); endif endif else if (all_flag) error ("harmmean: dimension and 'all' flag are mutually exclusive."); endif ## Two numeric input arguments, dimensions given. Note scalar is vector! vecdim = varargin{1}; if (isempty (vecdim) || ! (isvector (vecdim) && all (vecdim > 0)) ... || any (rem (vecdim, 1)) || any (! isfinite (vecdim))) error ("harmmean: DIM must be a positive integer scalar or vector."); endif if (isscalar (vecdim)) if (vecdim > ndx) m = x; else n = szx(vecdim); is_nan = 0; if (omitnan) nanx = isnan (x); n = sum (! nanx, vecdim); is_nan = sum (nanx, vecdim); x(nanx) = 1; # remove NaNs by subtracting is_nan below endif m = n ./ (sum (1 ./ x, vecdim) - is_nan); endif else vecdim = sort (vecdim); if (! all (diff (vecdim))) error (strcat ("harmmean: VECDIM must contain non-repeating", ... " positive integers.")); endif ## Ignore exceeding dimensions in VECDIM vecdim(find (vecdim > ndims (x))) = []; if (isempty (vecdim)) m = x; else ## Move vecdims to dim 1. ## Calculate permutation vector remdims = 1 : ndx; # All dimensions remdims(vecdim) = []; # Delete dimensions specified by vecdim nremd = numel (remdims); ## If all dimensions are given, it is similar to all flag if (nremd == 0) x = x(:); if (omitnan) x = x(! isnan (x)); endif m = length (x) ./ sum (1 ./ x); else ## Permute to bring vecdims to front perm = [vecdim, remdims]; x = permute (x, perm); ## Reshape to squash all vecdims in dim1 num_dim = prod (szx(vecdim)); szx(vecdim) = []; szx = [ones(1, length(vecdim)), szx]; szx(1) = num_dim; x = reshape (x, szx); ## Calculate mean on dim1 if (omitnan) nanx = isnan (x); n = sum (! nanx, 1); is_nan = sum (nanx, 1); x(nanx) = 1; # remove NaNs by subtracting is_nan below else n = szx(1); is_nan = 0; endif m = n ./ (sum (1 ./ x, 1) - is_nan); ## Inverse permute back to correct dimensions m = ipermute (m, perm); endif endif endif endif endfunction ## Test single input and optional arguments "all", DIM, "omitnan") %!test %! x = [0:10]; %! y = [x;x+5;x+10]; %! assert_equal (harmmean (x), 0); %! m = [0 8.907635160795225 14.30854471766802]; %! assert_equal (harmmean (y, 2), m', 4e-14); %! assert_equal (harmmean (y, 'all'), 0); %! y(2,4) = NaN; %! m(2) = 9.009855936313949; %! assert_equal (harmmean (y, 2), [0 NaN m(3)]', 4e-14); %! assert_equal (harmmean (y', 'omitnan'), m, 4e-14); %! z = y + 20; %! assert_equal (harmmean (z, 'all'), NaN); %! assert_equal (harmmean (z, 'all', 'includenan'), NaN); %! assert_equal (harmmean (z, 'all', 'omitnan'), 29.1108719858295, 4e-14); %! m = [24.59488458841874 NaN 34.71244385944397]; %! assert_equal (harmmean (z'), m, 4e-14); %! assert_equal (harmmean (z', 'includenan'), m, 4e-14); %! m(2) = 29.84104075528277; %! assert_equal (harmmean (z', 'omitnan'), m, 4e-14); %! assert_equal (harmmean (z, 2, 'omitnan'), m', 4e-14); ## Test dimension indexing with vecdim in n-dimensional arrays %!test %! x = repmat ([1:20;6:25], [5 2 6 3]); %! assert_equal (size (harmmean (x, [3 2])), [10 1 1 3]); %! assert_equal (size (harmmean (x, [1 2])), [1 1 6 3]); %! assert_equal (size (harmmean (x, [1 2 4])), [1 1 6]); %! assert_equal (size (harmmean (x, [1 4 3])), [1 40]); %! assert_equal (size (harmmean (x, [1 2 3 4])), [1 1]); ## Test results with vecdim in n-dimensional arrays and "omitnan" %!test %! x = repmat ([1:20;6:25], [5 2 6 3]); %! m = repmat ([5.559045930488016;13.04950789021461], [5 1 1 3]); %! assert_equal (harmmean (x, [3 2]), m, 4e-14); %! x(2,5,6,3) = NaN; %! m(2,3) = NaN; %! assert_equal (harmmean (x, [3 2]), m, 4e-14); %! m(2,3) = 13.06617961315406; %! assert_equal (harmmean (x, [3 2], 'omitnan'), m, 4e-14); ## Test results for pure Inf arrays and omitnan interactions %!test %! assert_equal (harmmean ([Inf, Inf]), Inf); %! assert_equal (harmmean ([Inf, Inf], 'all'), Inf); %! assert_equal (harmmean ([Inf, Inf], 2), Inf); %! assert_equal (harmmean ([NaN, Inf], 'omitnan'), Inf); %! assert_equal (harmmean ([NaN, Inf], 'includenan'), NaN); %! assert_equal (harmmean ([0, Inf]), 0); ## Test NaN propagation in the presence of zeros. %!test %! assert_equal (harmmean ([0, NaN]), NaN); %! assert_equal (harmmean ([0, NaN], 'all'), NaN); %! assert_equal (harmmean ([0, NaN], [1, 2]), NaN); %! assert_equal (harmmean ([0, NaN], 'omitnan'), 0); %! assert_equal (harmmean ([0, NaN], 'all', 'omitnan'), 0); ## Test default handling of empty arrays. %!test %! a = harmmean ([]); %! assert_equal (isnan (a), true); %! assert_equal (size (a), [1, 1]); %!assert_equal (harmmean (ones (2, 0, 3, 2)), ones (1, 0, 3, 2)) %!assert_equal (harmmean (ones (2, 0, 3, 2), [1, 2]), NaN (1, 1, 3, 2)) %!assert_equal (harmmean (ones (2, 0, 3, 2), 'all'), NaN) %!assert_equal (harmmean (ones (2, 0, 3, 2), 1), ones (1, 0, 3, 2)) %!assert_equal (harmmean (ones (2, 0, 3, 2), 2), NaN (2, 1, 3, 2)) %!assert_equal (harmmean (ones (2, 0, 3, 2), 3), ones (2, 0, 1, 2)) %!assert_equal (harmmean (ones (2, 0, 3, 2), 4), ones (2, 0, 3)) %!assert_equal (harmmean ([], 1), ones (1, 0)) %!assert_equal (harmmean ([], 2), ones (0, 1)) %!assert_equal (harmmean ([], 3), []) ## Test errors %!error harmmean ('char') %!error harmmean ([1 -1 3]) %!error ... %! harmmean (repmat ([1:20;6:25], [5 2 6 3 5]), -1) %!error ... %! harmmean (repmat ([1:20;6:25], [5 2 6 3 5]), 0) %!error ... %! harmmean (repmat ([1:20;6:25], [5 2 6 3 5]), [1 1]) %!error ... %! harmmean ([1, 2; 3, 4], 1, 'all') %!error ... %! harmmean ([1, 2; 3, 4], [1, 2], 'all') %!error ... %! harmmean ([1 2 4], Inf) %!error ... %! harmmean ([1 2 4], [1 Inf]) %!error ... %! harmmean ([1 2 4], [Inf 1]) %!error ... %! harmmean ([1 2 4], [Inf Inf]) statistics-release-2.1.2/inst/Descriptive_Statistics/jackknife.m000066400000000000000000000313661527311104300251050ustar00rootroot00000000000000## Copyright (C) 2011 Alexander Klein ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{jackstat} =} jackknife (@var{E}, @var{x}) ## @deftypefnx {statistics} {@var{jackstat} =} jackknife (@var{E}, @var{x}, @dots{}) ## @deftypefnx {statistics} {@var{jackstat} =} jackknife (@dots{}, @qcode{'Options'}, @var{options}) ## ## Compute jackknife estimates of a parameter taking one or more given samples ## as parameters. ## ## In particular, @var{E} is the estimator to be jackknifed as a function name, ## handle, or inline function, and @var{x} is the sample for which the estimate ## is to be taken. Each row of @var{x} is an observation, and a row vector is ## taken as a column of them. The @var{i}-th row of @var{jackstat} holds the ## estimator's value on @var{x} with its @var{i}-th row omitted, reshaped to a ## row, so an estimator returning @math{m} values gives an @math{n*m} matrix ## for @math{n} observations, and a scalar estimator a column vector. Every ## sample must give the estimator's result the same number of elements as the ## whole of @var{x} does. With fewer than two observations there is nothing ## to omit, and @var{jackstat} is the estimator's value on @var{x} itself. ## ## @example ## @group ## jackstat (@var{i},:) = @var{E} (@var{x}([1:@var{i}-1, @var{i}+1:end],:)) ## @end group ## @end example ## ## Depending on the number of samples to be used, the estimator must have the ## appropriate form: ## @itemize ## @item ## If only one sample is used, then the estimator need not be concerned with ## cell arrays, for example jackknifing the standard deviation of a sample can ## be performed with @code{@var{jackstat} = jackknife (@@std, rand (100, 1))}. ## @item ## If several samples are used, an estimator that takes as many inputs, or ## declares @code{varargin}, receives each sample as an argument of its own, ## as in MATLAB. A scalar is passed to every call unchanged, and the other ## samples must all have the same number of rows, the same row being omitted ## from each: ## @end itemize ## ## @example ## @group ## @var{jackstat} = jackknife (@@(x, y) std (x) / var (y), ## rand (100, 1), randn (100, 1)) ## @end group ## @end example ## ## An estimator declaring exactly one input instead receives the samples as ## the elements of a cell array, in their order of appearance, and the samples ## must all have the same number of rows. This form is an Octave extension. ## An estimator written for it that also declares an optional second input ## receives the samples as separate arguments. ## ## @example ## @group ## @var{jackstat} = jackknife (@@(x) std(x@{1@})/var(x@{2@}), ## rand (100, 1), randn (100, 1)) ## @end group ## @end example ## ## @qcode{'Options'} takes an options structure, as made by @code{statset}, ## for MATLAB compatibility; it changes nothing, the samples being evaluated ## one after another. ## ## If all goes well, a theoretical value @var{P} for the parameter is already ## known, @var{n} is the sample size, ## ## @code{@var{t} = @var{n} * @var{E}(@var{x}) - (@var{n} - 1) * ## mean(@var{jackstat})} ## ## and ## ## @code{@var{v} = sumsq(@var{n} * @var{E}(@var{x}) - (@var{n} - 1) * ## @var{jackstat} - @var{t}) / (@var{n} * (@var{n} - 1))} ## ## then ## ## @code{(@var{t}-@var{P})/sqrt(@var{v})} should follow a t-distribution with ## @var{n}-1 degrees of freedom. ## ## Jackknifing is a well known method to reduce bias. ## Further details can be found in: ## @subheading References ## ## @enumerate ## @item ## Rupert G. Miller. The jackknife - a review. Biometrika (1974), 61(1):1-15. ## doi:10.1093/biomet/61.1.1 ## @item ## Rupert G. Miller. Jackknifing Variances. Ann. Math. Statist. (1968), ## Volume 39, Number 2, 567-582. doi:10.1214/aoms/1177698418 ## @end enumerate ## @end deftypefn function jackstat = jackknife (anEstimator, varargin) if (nargin < 2) print_usage (); endif ## Convert function name to handle if necessary, or throw an error. if (ischar (anEstimator)) anEstimator = str2func (anEstimator); elseif (! is_function_handle (anEstimator)) error (strcat ("jackknife: estimators must be passed as function", ... " names or handles.")); endif ## An options structure is taken and changes nothing: the samples are ## evaluated one after another and nothing is drawn at random. if (numel (varargin) >= 3 && ischar (varargin{end-1}) && strcmpi (varargin{end-1}, 'Options')) if (! (isstruct (varargin{end}) || isempty (varargin{end}))) error ("jackknife: 'Options' must be a structure."); endif varargin(end-1:end) = []; endif ## Each row is an observation, and a row vector is a column of them. for i = 1:numel (varargin) if (isrow (varargin{i})) varargin{i} = varargin{i}(:); endif endfor ## A single sample reaches the estimator as it is. Several reach a ## one-input estimator as a cell array, and any other estimator as ## arguments of their own with every scalar passed unchanged, as in MATLAB. if (numel (varargin) == 1) n = rows (varargin{1}); estimate = @(idx) anEstimator (varargin{1}(idx,:)); elseif (takesOneInput (anEstimator)) n = cellfun (@rows, varargin); if (any (n != n(1))) error ("jackknife: all passed data must have the same number of rows."); endif n = n(1); estimate = @(idx) anEstimator (cellfun (@(x) x(idx,:), varargin, ... 'UniformOutput', false)); else isdata = ! cellfun (@isscalar, varargin); n = [cellfun(@rows, varargin(isdata)), 0]; if (any (n(1:end-1) != n(1))) error (strcat ("jackknife: all passed data other than scalars must", ... " have the same number of rows.")); endif n = n(1); estimate = @(idx) leaveOut (anEstimator, varargin, isdata, idx); endif ## One row per jackknife sample, holding the estimate as a row. With ## fewer than two observations there is no sample to leave one out of, ## and the estimate on the whole data is returned. jackstat = estimate (1:n); jackstat = jackstat(:).'; if (n < 2) return; endif m = numel (jackstat); jackstat = repmat (jackstat, n, 1); for k = 1:n s = estimate ([1:k-1, k+1:n]); if (numel (s) != m) error (strcat ("jackknife: the estimator returned %d values for a", ... " jackknife sample and %d for the whole data."), ... numel (s), m); endif jackstat(k,:) = s(:).'; endfor endfunction ## True for an estimator declaring exactly one input. function tf = takesOneInput (fcn) try tf = (nargin (fcn) == 1); catch tf = false; end_try_catch endfunction ## The estimator on every data argument restricted to the rows IDX. function s = leaveOut (fcn, samples, isdata, idx) for i = find (isdata) samples{i} = samples{i}(idx,:); endfor s = fcn (samples{:}); endfunction %!demo %! rng (42); %! for k = 1:1000 %! x = rand (10, 1); %! s(k) = std (x); %! jackstat = jackknife (@std, x); %! j(k) = 10 * std (x) - 9 * mean (jackstat); %! endfor %! figure (); %! hist ([s', j'], 0:sqrt (1/12)/10:2*sqrt (1/12)) %!demo %! rng (42); %! for k = 1:1000 %! x = randn (1, 50); %! y = rand (1, 50); %! jackstat = jackknife (@(x) std (x{1})/std (x{2}), y, x); %! j(k) = 50 * std (y) / std (x) - 49 * mean (jackstat); %! v(k) = sumsq ((50 * std (y) / std (x) - 49 * jackstat) - j(k)) / (50 * 49); %! endfor %! t = (j - sqrt (1 / 12)) ./ sqrt (v); %! figure (); %! plot (sort (tcdf (t, 49)), ... %! '-;Almost linear mapping indicates good fit with t-distribution.;') %!demo %! ## Jackknife the correlation of two samples, each handed to the estimator %! ## as an argument of its own, and correct it for bias %! rng (42); %! x = randn (30, 1); %! y = x + randn (30, 1); %! n = numel (x); %! r = corr (x, y); %! jackstat = jackknife (@(a, b) corr (a, b), x, y); %! bias = (n - 1) * (mean (jackstat) - r) %! se = sqrt ((n - 1) / n * sumsq (jackstat - mean (jackstat))) %! r_corrected = r - bias ## Test output %!test %! ##Example from Quenouille, Table 1 %! d=[0.18 4.00 1.04 0.85 2.14 1.01 3.01 2.33 1.57 2.19]; %! jackstat = jackknife ( @(x) 1/mean (x), d ); %! assert_equal ( 10 / mean (d) - 9 * mean (jackstat), 0.5240, 1e-5 ); %!test %! ## Empty input %! assert_equal (jackknife (@mean, []), NaN); %!test %! ## Single-element input %! assert_equal (jackknife (@mean, 5), 5); %!test %! ## Estimator returning multiple values %! expected = [2.5, sqrt(0.5); %! 2.0, sqrt(2); %! 1.5, sqrt(0.5)]; %! jackstat = jackknife (@(x) [mean(x); std(x)], [1 2 3]); %! assert_equal (jackstat, expected, 1e-5); ## Expected values below are MATLAB R2024a's. %!test %! ## Each row of a matrix is an observation %! assert_equal (jackknife (@mean, magic (3)), ... %! [3.5, 7, 4.5; 6, 5, 4; 5.5, 3, 6.5]); %!test %! assert_equal (jackknife (@mean, [1, 2; 3, 4; 5, 7]), ... %! [4, 5.5; 3, 4.5; 2, 3]); %!test %! ## A row vector is a column of observations %! assert_equal (jackknife (@mean, [1, 2, 3]), [2.5; 2; 1.5]); %!test %! ## An estimate is reshaped to a row %! x = [1, 2, 3; 4, 5, 6; 7, 8, 10]; %! assert_equal (jackknife (@(v) [v(1), v(end)], x), [4, 10; 1, 10; 1, 6]); %!test %! assert_equal (jackknife (@(v) sum (v(:)) * ones (2, 2), [1, 2, 3, 4]), ... %! [9, 9, 9, 9; 8, 8, 8, 8; 7, 7, 7, 7; 6, 6, 6, 6]); %!test %! assert_equal (jackknife (@mean, zeros (1, 0)), NaN); %!test %! assert_equal (jackknife (@(x) [mean(x); std(x)], []), [NaN, NaN]); %!test %! assert_equal (jackknife (@(x) [mean(x); std(x)], 5), [5, 0]); %!test %! assert_equal (jackknife (@(x) mean (x) > 2, [1, 2, 3]), ... %! [true; false; false]); %!test %! assert_equal (jackknife (@mean, single ([1, 2, 3])), single ([2.5; 2; 1.5])); %!test %! assert_equal (jackknife ('mean', [1, 2, 3]), [2.5; 2; 1.5]); %!test %! assert_equal (jackknife (@mean, [1, 2, 3], 'Options', statset ()), ... %! [2.5; 2; 1.5]); %!test %! ## Several samples reach a one-input estimator as a cell array %! assert_equal (jackknife (@(x) mean (x{1}) - mean (x{2}), [1, 2, 3], ... %! [4, 5, 7]), [-3.5; -3.5; -3]); %!test %! ## and any other estimator as arguments of their own %! assert_equal (jackknife (@(a, b) mean (a) - mean (b), [1, 2, 3], ... %! [4; 5; 7]), [-3.5; -3.5; -3]); %!test %! assert_equal (jackknife (@(varargin) numel (varargin), [1, 2, 3], ... %! [4, 5, 7]), [2; 2; 2]); %!test %! ## A scalar is passed unchanged %! assert_equal (jackknife (@(a, b) a + mean (b), 5, [1, 2, 3]), ... %! [7.5; 7; 6.5]); %!test %! assert_equal (jackknife (@(a, b, c) mean (a) * b + mean (c), ... %! [1, 2, 3], 2, [4, 5, 7]), [11; 9.5; 7.5]); %!test %! x = [1, 2; 3, 4; 5, 6]; %! assert_equal (jackknife (@(a, b) mean (a, 1) + b, x, 10), ... %! [14, 15; 13, 14; 12, 13]); %!test %! assert_equal (jackknife (@(a, b) [mean(a), mean(b)], [1, 2, 3], ... %! [4, 5, 7]), [2.5, 6; 2, 5.5; 1.5, 4.5]); %!test %! assert_equal (jackknife (@(a, b) a + b, 5, 6), 11); %!test %! assert_equal (jackknife (@(a, b) sum (a) + b, [], 1), 1); %!test %! assert_equal (jackknife (@(a, b) mean (a) - mean (b), [1, 2, 3], ... %! [4, 5, 7], 'Options', statset ()), ... %! [-3.5; -3.5; -3]); %!test %! assert_equal (jackknife (@(x) mean (x{1}) + mean (x{2}), 5, 6), 11); ## Test input validation %!error jackknife (@mean) %!error ... %! jackknife (1, [1, 2, 3]) %!error ... %! jackknife (@mean, [1, 2, 3], 'Options', 1) %!error ... %! jackknife (@(x) mean (x{1}), [1, 2, 3], [4, 5]) %!error ... %! jackknife (@(a, b) mean (a), [1, 2], [4; 5; 7]) %!error ... %! jackknife (@(v) v, [1, 2, 3]) statistics-release-2.1.2/inst/Descriptive_Statistics/ksdensity.m000066400000000000000000000511621527311104300251710ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{f} =} ksdensity (@var{x}) ## @deftypefnx {statistics} {@var{f} =} ksdensity (@var{x}, @var{pts}) ## @deftypefnx {statistics} {[@var{f}, @var{xi}] =} ksdensity (@dots{}) ## @deftypefnx {statistics} {[@var{f}, @var{xi}, @var{bw}] =} ksdensity (@dots{}) ## @deftypefnx {statistics} {[@dots{}] =} ksdensity (@dots{}, @var{Name}, @var{Value}) ## ## Kernel smoothing density estimate. ## ## @code{@var{f} = ksdensity (@var{x})} computes a probability density estimate ## of the sample in the vector @var{x}, evaluated at 100 equally spaced points ## @var{xi} that span the range of the data. @code{[@var{f}, @var{xi}] = ## ksdensity (@var{x})} also returns those points. Both are row vectors, ## whichever way @var{x} itself lies. When called without output arguments, ## the estimate is plotted instead. ## ## @code{@var{f} = ksdensity (@var{x}, @var{pts})} evaluates the estimate at the ## values in @var{pts} instead; @var{f} is then the same size as @var{pts}. For ## @qcode{'Function'} equal to @qcode{'icdf'} the entries of @var{pts} are ## probabilities in @math{[0, 1]}. ## ## @code{[@var{f}, @var{xi}, @var{bw}] = ksdensity (@dots{})} additionally ## returns the bandwidth @var{bw} of the smoothing kernel. ## ## The following @qcode{Name-Value} pairs are supported: ## ## @multitable @columnfractions 0.18 0.82 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Kernel'} @tab The smoothing kernel: @qcode{'normal'} (default), ## @qcode{'box'}, @qcode{'triangle'}, @qcode{'epanechnikov'}, or a function ## handle @code{@@(z)} evaluating a kernel density at the standardized distance ## @var{z}. ## ## @item @qcode{'Bandwidth'} @tab The kernel bandwidth, a positive scalar. The ## default is the value that is optimal for estimating a normal density, ## @math{@var{bw} = @var{sigma} * (4 / (3 * @var{n})) ^ (1 / 5)}, with ## @var{sigma} a robust estimate of the standard deviation of @var{x}. ## ## @item @qcode{'Function'} @tab The function to estimate: @qcode{'pdf'} ## (default), @qcode{'cdf'}, @qcode{'icdf'}, @qcode{'survivor'}, or ## @qcode{'cumhazard'}. ## ## @item @qcode{'Weights'} @tab A vector of non-negative weights, one for each ## element of @var{x}. The default weights are all equal. ## ## @item @qcode{'NumPoints'} @tab The number of equally spaced points @var{xi} ## at which to evaluate the estimate when @var{pts} is not given. The default ## is @math{100}. ## @end multitable ## ## @seealso{hist, histc, ecdf} ## @end deftypefn function [f, xi, bw] = ksdensity (x, varargin) if (nargin < 1) print_usage (); endif if (! (isnumeric (x) && isreal (x) && isvector (x))) error ("ksdensity: X must be a vector of real values."); endif x = x(! isnan (x)); x = x(:)'; n = numel (x); if (n < 2) error ("ksdensity: X must contain at least two non-missing values."); endif ## Optional positional PTS argument (a numeric vector before any Name-Value). pts = []; ptssz = []; if (numel (varargin) >= 1 && ! ischar (varargin{1})) pts = varargin{1}; varargin(1) = []; if (! (isnumeric (pts) && isreal (pts) && isvector (pts))) error ("ksdensity: PTS must be a vector of real values."); endif ptssz = size (pts); pts = pts(:); endif ## Defaults and Name-Value parsing. kernel = 'normal'; bw = []; func = 'pdf'; weights = []; npoints = 100; support = 'unbounded'; bc = 'log'; if (mod (numel (varargin), 2) != 0) error ("ksdensity: optional arguments must be Name-Value pairs."); endif for k = 1:2:numel (varargin) if (! ischar (varargin{k})) error ("ksdensity: parameter names must be character vectors."); endif switch (lower (varargin{k})) case 'kernel' kernel = varargin{k+1}; case 'bandwidth' bw = varargin{k+1}; case 'function' func = lower (varargin{k+1}); case 'weights' weights = varargin{k+1}; case 'numpoints' npoints = varargin{k+1}; case 'support' support = varargin{k+1}; case 'boundarycorrection' bc = lower (varargin{k+1}); case 'censoring' error ("ksdensity: 'Censoring' is not yet supported."); otherwise error ("ksdensity: unknown parameter name '%s'.", varargin{k}); endswitch endfor ## Validate the kernel. if (ischar (kernel)) kernel = lower (kernel); if (! any (strcmp (kernel, {'normal', 'box', 'triangle', 'epanechnikov'}))) error ("ksdensity: unrecognised 'Kernel' value."); endif elseif (! is_function_handle (kernel)) error ("ksdensity: 'Kernel' must be a name or a function handle."); endif if (! any (strcmp (func, {'pdf', 'cdf', 'icdf', 'survivor', 'cumhazard'}))) error ("ksdensity: unrecognised 'Function' value."); endif ## Support and boundary-correction validation -> lower/upper bounds L, U. if (ischar (support)) switch (lower (support)) case 'unbounded' L = -Inf; U = Inf; case 'positive' L = 0; U = Inf; otherwise error ("ksdensity: unrecognised 'Support' value."); endswitch elseif (isnumeric (support) && isreal (support) && numel (support) == 2 && support(1) < support(2)) L = support(1); U = support(2); else error ("ksdensity: 'Support' must be 'unbounded', 'positive', or [L U]."); endif unbounded = (L == -Inf && U == Inf); if (! any (strcmp (bc, {'log', 'reflection'}))) error ("ksdensity: unrecognised 'BoundaryCorrection' value."); endif if (! unbounded && (any (x <= L) || any (x >= U))) error ("ksdensity: X must lie strictly inside the specified 'Support'."); endif ## Weights (normalised to sum to one). if (isempty (weights)) w = ones (1, n) / n; else if (! (isnumeric (weights) && isreal (weights) && isvector (weights) && numel (weights) == n && all (weights >= 0) && any (weights > 0))) error ("ksdensity: 'Weights' must be a non-negative vector with one element per X."); endif w = weights(:)' / sum (weights); endif ## Working space. With the default 'log' boundary correction a bounded ## support is mapped to the whole real line by a log/logit transform; the ## bandwidth and default grid are then formed in that space. The unbounded ## case and the 'reflection' correction work in the native space. dolog = (! unbounded && strcmp (bc, 'log')); if (dolog) xw = ksdensity_fwd_ (x, L, U); else xw = x; endif ## Bandwidth: robust normal-reference rule unless supplied (working space). ## Where the data is discrete enough that more than half of it sits on the ## median, the median absolute deviation is exactly zero and the rule has no ## scale to work from. MATLAB falls back to the range there, not to the ## standard deviation: the standard deviation of such data is small, and a ## bandwidth taken from it is narrow enough to leave a spike over each ## repeated value instead of a density. Measured against R2024a on setosa ## petal width and three constructed vectors. A constant vector has no ## range either, and takes a bandwidth of one. if (isempty (bw)) sigma = median (abs (xw - median (xw))) / 0.6745; if (sigma <= 0) sigma = max (xw) - min (xw); endif if (sigma <= 0) bw = 1; else bw = sigma * (4 / (3 * n)) ^ (1 / 5); endif elseif (! (isnumeric (bw) && isscalar (bw) && isreal (bw) && bw > 0)) error ("ksdensity: 'Bandwidth' must be a positive scalar."); endif ## Evaluation points. For 'icdf' the points are probabilities. A grid we ## generate ourselves is a row, as MATLAB's is whatever the data's shape. if (strcmp (func, 'icdf')) if (isempty (pts)) xi = ((1:npoints) - 0.5) / npoints; else xi = pts; endif f = ksdensity_icdf_ (xi(:), x, w, bw, kernel, L, U, bc); else if (isempty (pts)) if (dolog) gw = linspace (min (xw) - 3 * bw, max (xw) + 3 * bw, npoints); xi = ksdensity_inv_ (gw, L, U); else lo = min (x) - 3 * bw; hi = max (x) + 3 * bw; if (isfinite (L)) lo = max (lo, L); endif if (isfinite (U)) hi = min (hi, U); endif xi = linspace (lo, hi, npoints); endif else xi = pts; endif switch (func) case 'pdf' f = ksdensity_eval_ (xi(:), x, w, bw, kernel, L, U, bc, 'pdf'); case 'cdf' f = ksdensity_eval_ (xi(:), x, w, bw, kernel, L, U, bc, 'cdf'); case 'survivor' f = 1 - ksdensity_eval_ (xi(:), x, w, bw, kernel, L, U, bc, 'cdf'); case 'cumhazard' Fc = ksdensity_eval_ (xi(:), x, w, bw, kernel, L, U, bc, 'cdf'); f = -log (1 - Fc); endswitch endif ## F takes XI's orientation, whether XI is the grid generated above or the ## points the caller supplied. if (! isempty (pts)) xi = reshape (xi, ptssz); endif f = reshape (f, size (xi)); ## With no output requested, plot the estimate (as MATLAB does) and return ## nothing. if (nargout == 0) plot (xi, f); clear f xi bw endif endfunction ## Kernel pdf or cdf estimate at native query points Q, honouring the support ## bounds L, U and the boundary-correction method BC. WANT is 'pdf' or 'cdf'. function v = ksdensity_eval_ (q, x, w, h, kernel, L, U, bc, want) q = q(:); ispdf = strcmp (want, 'pdf'); if (L == -Inf && U == Inf) ## Unbounded: plain kernel sum. z = (q - x) / h; if (ispdf) v = (kernelpdf (z, kernel) * w(:)) / h; else v = kernelcdf (z, kernel) * w(:); endif elseif (strcmp (bc, 'reflection')) ## Reflect the sample across each finite bound (single reflection). a = x; aw = w; if (isfinite (L)) a = [a, 2 * L - x]; aw = [aw, w]; endif if (isfinite (U)) a = [a, 2 * U - x]; aw = [aw, w]; endif if (ispdf) v = (kernelpdf ((q - a) / h, kernel) * aw(:)) / h; v(q < L | q > U) = 0; else ## Integrate the reflected density from the lower edge of the support. v = (kernelcdf ((q - a) / h, kernel) ... - kernelcdf ((L - a) / h, kernel)) * aw(:); endif else ## Log/logit transform: estimate in the transformed space and map back. z = (ksdensity_fwd_ (q, L, U) - ksdensity_fwd_ (x, L, U)) / h; if (ispdf) g = (kernelpdf (z, kernel) * w(:)) / h; v = g .* ksdensity_logjac_ (q, L, U); elseif (isfinite (U) && ! isfinite (L)) ## upper bound only: t decreases if (is_function_handle (kernel)) v = 1 - kernelcdf (z, kernel) * w(:); else v = kernelcdf (-z, kernel) * w(:); ## the kernels are symmetric endif else v = kernelcdf (z, kernel) * w(:); endif endif if (! ispdf) v = min (max (v, 0), 1); endif endfunction ## Forward transform mapping the support (L, U) onto the whole real line. function t = ksdensity_fwd_ (x, L, U) if (U == Inf) t = log (x - L); elseif (L == -Inf) t = log (U - x); else t = log ((x - L) ./ (U - x)); endif endfunction ## Inverse of ksdensity_fwd_. function x = ksdensity_inv_ (t, L, U) if (U == Inf) x = L + exp (t); elseif (L == -Inf) x = U - exp (t); else x = L + (U - L) ./ (1 + exp (-t)); endif endfunction ## |dt/dx| of the forward transform (the density change-of-variables factor). function j = ksdensity_logjac_ (x, L, U) if (U == Inf) j = 1 ./ (x - L); elseif (L == -Inf) j = 1 ./ (U - x); else j = (U - L) ./ ((x - L) .* (U - x)); endif endfunction ## Inverse cdf at probabilities P by monotone interpolation of the cdf over a ## fine grid spanning the support. function q = ksdensity_icdf_ (p, x, w, h, kernel, L, U, bc) if (! (L == -Inf && U == Inf) && strcmp (bc, 'log')) tx = ksdensity_fwd_ (x, L, U); grid = ksdensity_inv_ (linspace (min (tx) - 10 * h, ... max (tx) + 10 * h, 4000), L, U)'; else lo = min (x) - 10 * h; hi = max (x) + 10 * h; if (isfinite (L)) lo = max (lo, L); endif if (isfinite (U)) hi = min (hi, U); endif grid = linspace (lo, hi, 4000)'; endif F = ksdensity_eval_ (grid, x, w, h, kernel, L, U, bc, 'cdf'); ## Keep a strictly increasing (F, grid) relation for interp1. dF = diff (F); keep = [true; dF > 0]; q = interp1 (F(keep), grid(keep), p(:), 'linear', NA); q(p(:) <= min (F)) = grid(1); q(p(:) >= max (F)) = grid(end); endfunction %!demo %! ## Kernel density estimate of a small sample, with a histogram for reference %! x = [1 1.5 2 2 2.5 3 3.5 3.5 4 6]; %! [f, xi] = ksdensity (x); %! hist (x, 6, 6 / numel (x)); %! hold on; plot (xi, f, 'r-', 'LineWidth', 2); hold off; %!test # density integrates to ~1 over a wide grid (normal kernel) %! x = [2.1 0.3 1.2 -0.7 0.9 1.5 2.8 0.1 0.4 1.1 3.2 0.6 2.0 0.9 1.7]; %! [f, xi] = ksdensity (x, "NumPoints", 4000); %! assert_equal (trapz (xi, f), 1, 5e-3); %!test # every named compact kernel integrates to ~1 %! x = randn (1, 200); %! for k = {"box", "triangle", "epanechnikov"} %! xi = linspace (-8, 8, 6000)'; %! f = ksdensity (x, xi, "Kernel", k{1}); %! assert_equal (trapz (xi, f), 1, 1e-2); %! endfor %!test # cdf is monotone from 0 to 1 and matches the analytic normal-kernel sum %! x = [2.1 0.3 1.2 -0.7 0.9 1.5 2.8 0.1 0.4 1.1 3.2 0.6 2.0 0.9 1.7]; %! [F, xi] = ksdensity (x, "Function", "cdf", "NumPoints", 500); %! assert_equal (all (diff (F) >= -1e-12), true); %! assert_equal (F(1), 0, 5e-3); %! assert_equal (F(end), 1, 5e-3); %! [~, ~, bw] = ksdensity (x); %! Fdirect = mean (normcdf ((xi(:) - x) / bw), 2)'; %! assert_equal (F, Fdirect, 1e-12); ## Orientation, measured against R2024a 2026-08-17: the grid generated here is ## a row whatever the data's shape, while points supplied by the caller keep ## their own shape. %!test %! x = [2.1 0.3 1.2 -0.7 0.9 1.5 2.8 0.1 0.4 1.1 3.2 0.6 2.0 0.9 1.7]; %! [f, xi] = ksdensity (x); %! assert_equal (size (f), [1, 100]); %! assert_equal (size (xi), [1, 100]); %!test %! ## a column of data still gives a row grid %! x = [2.1 0.3 1.2 -0.7 0.9 1.5 2.8 0.1 0.4 1.1 3.2 0.6 2.0 0.9 1.7]'; %! [f, xi] = ksdensity (x, 'NumPoints', 500); %! assert_equal (size (f), [1, 500]); %! assert_equal (size (xi), [1, 500]); %!test %! ## supplied points keep their own orientation, and F follows them %! x = [2.1 0.3 1.2 -0.7 0.9 1.5 2.8 0.1 0.4 1.1 3.2 0.6 2.0 0.9 1.7]; %! [f, xi] = ksdensity (x, [0 0.5 1 1.5 2]); %! assert_equal (size (f), [1, 5]); %! assert_equal (size (xi), [1, 5]); %! [f, xi] = ksdensity (x, [0; 0.5; 1; 1.5; 2]); %! assert_equal (size (f), [5, 1]); %! assert_equal (size (xi), [5, 1]); %!test # survivor and cumhazard are consistent with the cdf %! x = [2.1 0.3 1.2 -0.7 0.9 1.5 2.8 0.1 0.4 1.1 3.2 0.6 2.0 0.9 1.7]; %! pts = [-1 0 1 2 3]'; %! F = ksdensity (x, pts, "Function", "cdf"); %! S = ksdensity (x, pts, "Function", "survivor"); %! H = ksdensity (x, pts, "Function", "cumhazard"); %! assert_equal (S, 1 - F, 1e-12); %! assert_equal (H, -log (1 - F), 1e-12); %!test # icdf inverts the cdf %! x = [2.1 0.3 1.2 -0.7 0.9 1.5 2.8 0.1 0.4 1.1 3.2 0.6 2.0 0.9 1.7]; %! p = [0.1 0.25 0.5 0.75 0.9]'; %! q = ksdensity (x, p, "Function", "icdf"); %! Fq = ksdensity (x, q, "Function", "cdf"); %! assert_equal (Fq, p, 5e-3); %!test # evaluation at supplied points preserves shape %! x = randn (1, 50); %! pts = [-1 0 1]; %! f = ksdensity (x, pts); %! assert_equal (size (f), size (pts)); %!test # weights: a duplicated point equals a doubled weight %! x = [0 1 2 3]; %! pts = linspace (-2, 5, 40)'; %! f1 = ksdensity ([x, 3], pts, "Bandwidth", 0.5); %! f2 = ksdensity (x, pts, "Bandwidth", 0.5, "Weights", [1 1 1 2]); %! assert_equal (f1, f2, 1e-12); ## Where more than half the data sits on the median the median absolute ## deviation is exactly zero, and the rule falls back to the range rather than ## to the standard deviation, which is far too small on such data. %!test # MATLAB parity: the bandwidth when the robust scale vanishes %! load fisheriris %! pw = meas(strcmp (species, 'setosa'), 4); %! assert_equal (median (abs (pw - median (pw))), 0); %! [~, ~, bw] = ksdensity (pw); %! assert_equal (bw, 0.242194206816745, 1e-12); %! [~, ~, bw] = ksdensity ([1; 1; 1; 1; 1; 1; 1; 2; 3; 0]); %! assert_equal (bw, 2.004975185874807, 1e-12); %! [~, ~, bw] = ksdensity ([5; 5; 5; 5; 5; 5; 5; 5; 5; 9]); %! assert_equal (bw, 2.673300247833076, 1e-12); %!test # MATLAB parity: data with no spread at all takes a bandwidth of one %! [~, ~, bw] = ksdensity (repmat (2, 10, 1)); %! assert_equal (bw, 1); %!test # MATLAB parity: default bandwidth, grid size and range %! x = [2.1 0.3 1.2 -0.7 0.9 1.5 2.8 0.1 0.4 1.1 3.2 0.6 2.0 0.9 1.7]; %! [~, xi, bw] = ksdensity (x); %! assert_equal (bw, 0.6396, 5e-4); %! assert_equal (numel (xi), 100); %! assert_equal ([xi(1), xi(end)], [-2.6187, 5.1187], 1e-3); %!test # MATLAB parity: pdf (normal and box kernels) and cdf at fixed points %! x = [2.1 0.3 1.2 -0.7 0.9 1.5 2.8 0.1 0.4 1.1 3.2 0.6 2.0 0.9 1.7]; %! pts = [-0.5 0 0.5 1 1.5 2 2.5 3]; %! assert_equal (ksdensity (x, pts), ... %! [0.1214 0.2141 0.3051 0.3394 0.3061 0.2375 0.1697 0.1166], 2e-3); %! assert_equal (ksdensity (x, pts, "Kernel", "box"), ... %! [0.1505 0.2407 0.2708 0.3611 0.3009 0.2708 0.1805 0.1204], 2e-3); %! assert_equal (ksdensity (x, pts, "Function", "cdf"), ... %! [0.0710 0.1538 0.2850 0.4492 0.6128 0.7494 0.8506 0.9217], 2e-3); %!test # MATLAB parity: positive support (log) bandwidth, grid and pdf %! y = [0.2 0.5 0.7 1.1 1.4 2 2.6 3.3 4.1 5.5]; %! ypts = [0.1 0.3 0.6 1 2 3 4 5]; %! [~, xy, by] = ksdensity (y, "Support", "positive"); %! assert_equal (by, 0.7682, 5e-4); %! assert_equal ([xy(1), xy(end)], [0.0200, 55.1111], 1e-3); %! assert_equal (ksdensity (y, ypts, "Support", "positive"), ... %! [0.4300 0.4620 0.3620 0.2740 0.1570 0.0990 0.0660 0.0450], 2e-3); %!test # MATLAB parity: positive support with reflection boundary correction %! y = [0.2 0.5 0.7 1.1 1.4 2 2.6 3.3 4.1 5.5]; %! ypts = [0.1 0.3 0.6 1 2 3 4 5]; %! f = ksdensity (y, ypts, "Support", "positive", ... %! "BoundaryCorrection", "reflection"); %! assert_equal (f, [0.2920 0.2900 0.2810 0.2620 0.2010 0.1470 0.1070 0.0740], 2e-3); %!test # bounded support integrates to ~1 and vanishes outside the bounds %! y = [0.2 0.5 0.7 1.1 1.4 2 2.6 3.3]; %! [f, xi] = ksdensity (y, "Support", [0 4], "NumPoints", 4000); %! assert_equal (trapz (xi, f), 1, 5e-3); %! assert_equal (all (xi >= 0 & xi <= 4), true); %!test # reflection density is confined to the support %! y = [0.2 0.5 0.7 1.1 1.4 2 2.6 3.3]; %! f = ksdensity (y, [-1 -0.1 5], "Support", "positive", ... %! "BoundaryCorrection", "reflection"); %! assert_equal (f(1:2), [0 0]); %!test %! ## The lower tail with an upper bound only, below the resolution of 1 - cdf %! f = ksdensity (1:5, -60, 'Support', [-Inf, 10], 'Function', 'cdf'); %! I = integral (@(t) ksdensity (1:5, t, 'Support', [-Inf, 10]), -Inf, -60); %! assert_equal (f, I, -1e-8); ## Test input validation %!error ksdensity () %!error ksdensity (ones (3, 3)) %!error ... %! ksdensity (5) %!error ... %! ksdensity (1:10, "Kernel", "cosine") %!error ... %! ksdensity (1:10, "Function", "hazard") %!error ... %! ksdensity (1:10, "Bandwidth", -1) %!error ... %! ksdensity (1:10, "Weights", [1 2 3]) %!error ... %! ksdensity (1:10, "Support", "half") %!error ... %! ksdensity (1:10, "Support", [2 1]) %!error ... %! ksdensity ([-1 1 2 3], "Support", "positive") %!error ... %! ksdensity (1:10, "Support", "positive", "BoundaryCorrection", "linear") %!error ... %! ksdensity (1:10, "Censoring", ones (1, 10)) statistics-release-2.1.2/inst/Descriptive_Statistics/limitsOfAgreement.m000066400000000000000000000231771527311104300265770ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{LoA} =} limitsOfAgreement (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{LoA} =} limitsOfAgreement (@var{X}, @var{Y}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{LoA}, @var{stats}] =} limitsOfAgreement (@dots{}) ## ## Bland-Altman limits of agreement between two measurement methods. ## ## @code{@var{LoA} = limitsOfAgreement (@var{X}, @var{Y})} compares two ## methods that measured the same subjects, the measurements of the first in ## the vector @var{X} and of the second, in the same order, in @var{Y}. With ## @code{d = @var{X} - @var{Y}} the differences, @code{dbar} their mean and ## @code{s} their standard deviation, the bias of the first method against ## the second is @code{dbar} and the limits of agreement are ## @code{dbar - 1.96 * s} and @code{dbar + 1.96 * s}, between which about 95% ## of the differences between the methods fall when the differences are ## normal. Two methods ## agree well enough to be used in place of one another when the limits are ## too close to matter in practice, a judgement the data cannot make. ## ## @var{LoA} is a table with the rows @qcode{Bias}, @qcode{LowerLimit} and ## @qcode{UpperLimit} and the variables @qcode{Estimate} and ## @qcode{ConfidenceIntervals}, each row's confidence interval at the level ## @math{100 (1 - alpha)} percent: @code{dbar + [-1, 1] * t * s / sqrt (n)} ## for the bias and @code{[-1, 1] * t * sqrt (3 * s^2 / n)} about each limit, ## @code{t} being the @math{1 - alpha / 2} quantile of Student's t ## distribution on @math{n - 1} degrees of freedom and @math{n} the number of ## subjects (Bland and Altman, 1986). @var{X} and @var{Y} are vectors of ## type double or single, of the same length, and a subject with a missing ## value (@qcode{NaN}) in either is left out. Where either is single, so are ## the results. ## ## The following @qcode{Name-Value} pairs are supported: ## ## @multitable @columnfractions 0.2 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Multiplier'} @tab The number of standard deviations from ## the bias to each limit, a positive scalar. The default is 1.96, which ## Bland and Altman use. ## ## @item @qcode{'Alpha'} @tab The significance level of the confidence ## intervals, a scalar in the range @math{(0,1)}. The default is ## @math{0.05}. ## @end multitable ## ## @code{[@var{LoA}, @var{stats}] = limitsOfAgreement (@dots{})} also returns ## a structure with the fields @qcode{sd}, the standard deviation of the ## differences, @qcode{n}, the number of subjects, @qcode{differences} and ## @qcode{means}, the difference and the mean of the two measurements of each ## subject, which @code{blandAltmanPlot} draws. ## ## The bias with its interval is the paired mean difference of ## @code{meanEffectSize}. MATLAB has no limits of agreement, so ## @code{limitsOfAgreement} is specific to Octave. ## ## Reference: J. M. Bland and D. G. Altman. Statistical methods for ## assessing agreement between two methods of clinical measurement. ## @emph{The Lancet}, 327(8476):307--310, 1986. ## ## @seealso{blandAltmanPlot, meanEffectSize, cohenKappa} ## @end deftypefn function [LoA, stats] = limitsOfAgreement (X, Y, varargin) if (nargin < 2) print_usage (); endif if (! (isfloat (X) && isreal (X) && isvector (X))) error (strcat ("limitsOfAgreement: X must be a real vector of type", ... " double or single.")); endif if (! (isfloat (Y) && isreal (Y) && isvector (Y))) error (strcat ("limitsOfAgreement: Y must be a real vector of type", ... " double or single.")); endif if (numel (X) != numel (Y)) error ("limitsOfAgreement: X and Y must have the same length."); endif ## Parse optional Name-Value paired arguments optNames = {'Multiplier', 'Alpha'}; dfValues = {1.96, 0.05}; [mult, alpha, rest] = parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (rest)) if (mod (numel (rest), 2) == 0 && ischar (rest{1})) error ("limitsOfAgreement: unknown parameter name '%s'.", rest{1}); endif error ("limitsOfAgreement: optional arguments must be Name-Value pairs."); endif if (! (isfloat (mult) && isreal (mult) && isscalar (mult) && mult > 0 && isfinite (mult))) error ("limitsOfAgreement: 'Multiplier' must be a positive finite scalar."); endif if (! (isfloat (alpha) && isreal (alpha) && isscalar (alpha) && alpha > 0 && alpha < 1)) error ("limitsOfAgreement: 'Alpha' must be a scalar between 0 and 1."); endif outclass = 'double'; if (isa (X, 'single') || isa (Y, 'single')) outclass = 'single'; endif x = double (X(:)); y = double (Y(:)); keep = ! (isnan (x) | isnan (y)); x = x(keep); y = y(keep); if (any (isinf (x)) || any (isinf (y))) error ("limitsOfAgreement: X and Y must not hold infinite values."); endif n = numel (x); if (n < 2) error ("limitsOfAgreement: X and Y must hold at least two subjects."); endif ## Bias, limits and their intervals (Bland and Altman, 1986) d = x - y; bias = mean (d); s = std (d); t = tinv (1 - alpha / 2, n - 1); E = bias + [0; -1; 1] * mult * s; h = t * [s / sqrt(n); sqrt(3 * s ^ 2 / n); sqrt(3 * s ^ 2 / n)]; CI = E + [-1, 1] .* h; LoA = table (cast (E, outclass), cast (CI, outclass), 'VariableNames', ... {'Estimate', 'ConfidenceIntervals'}, 'RowNames', ... {'Bias'; 'LowerLimit'; 'UpperLimit'}); stats = struct ('sd', cast (s, outclass), 'n', n, 'differences', ... cast (d, outclass), 'means', cast ((x + y) / 2, outclass)); endfunction %!demo %! ## Two meters measured the peak expiratory flow of 17 subjects %! x = [494, 395, 516, 434, 476, 557, 413, 442, 650, 433, 417, 656, 267, ... %! 478, 178, 423, 427]; %! y = [512, 430, 520, 428, 500, 600, 364, 380, 658, 445, 432, 626, 260, ... %! 477, 259, 350, 451]; %! LoA = limitsOfAgreement (x, y) ## Test output ## Values checked against R 4.5.0, BlandAltmanLeh 0.3.1 (bland.altman.stats) %!shared x, y %! x = [494, 395, 516, 434, 476, 557, 413, 442, 650, 433, 417, 656, 267, ... %! 478, 178, 423, 427]; %! y = [512, 430, 520, 428, 500, 600, 364, 380, 658, 445, 432, 626, 260, ... %! 477, 259, 350, 451]; %!test %! [L, s] = limitsOfAgreement (x, y); %! assert_equal (L.Properties.RowNames, {'Bias'; 'LowerLimit'; 'UpperLimit'}); %! assert_equal (L.Estimate, [-2.11764705882353; -78.097301611094; ... %! 73.8620074934469], 1e-12); %! assert_equal (L.ConfidenceIntervals, ... %! [-22.0488376966452, 17.8135435789981; ... %! -112.619136451142, -43.5754667710458; ... %! 39.3401726533987, 108.383842333495], 1e-12); %! assert_equal (s.sd, 38.7651298736074, 1e-12); %! assert_equal (s.n, 17); %! assert_equal (s.differences, (x - y)'); %! assert_equal (s.means, ((x + y) / 2)'); %!test %! L = limitsOfAgreement ([x, NaN], [y, 3], 'Multiplier', 2, 'Alpha', 0.1); %! assert_equal (L.Estimate, [-2.11764705882353; -79.6479068060383; ... %! 75.4126126883912], 1e-12); %! assert_equal (L.ConfidenceIntervals, ... %! [-18.5323144480251, 14.2970203303781; ... %! -108.078944713479, -51.2168688985971; ... %! 46.9815747809501, 103.843650595832], 1e-12); %!test %! L = limitsOfAgreement (x, y); %! T = meanEffectSize (x, y, 'Paired', true); %! assert_equal (L.Estimate(1), T.Effect, 1e-12); %! assert_equal (L.ConfidenceIntervals(1,:), T.ConfidenceIntervals, 1e-12); %!test %! [L, s] = limitsOfAgreement (single (x), y); %! assert_equal (class (L.Estimate), 'single'); %! assert_equal (class (L.ConfidenceIntervals), 'single'); %! assert_equal (class (s.sd), 'single'); %! assert_equal (double (L.Estimate), [-2.11764705882353; -78.097301611094; ... %! 73.8620074934469], 1e-4); ## Test input validation %!error limitsOfAgreement (1) %!error ... %! limitsOfAgreement (int8 ([1, 2]), [1, 2]) %!error ... %! limitsOfAgreement ([1, 2], [1, 2] + 1i) %!error ... %! limitsOfAgreement ([1, 2, 3], [1, 2]) %!error ... %! limitsOfAgreement (x, y, 'Foo', 1) %!error ... %! limitsOfAgreement (x, y, 'Alpha') %!error ... %! limitsOfAgreement (x, y, 'Multiplier', 0) %!error ... %! limitsOfAgreement (x, y, 'Alpha', 0) %!error ... %! limitsOfAgreement ([1, Inf, 3], [1, 2, 3]) %!error ... %! limitsOfAgreement ([1, NaN], [2, 3]) statistics-release-2.1.2/inst/Descriptive_Statistics/mvksdensity.m000066400000000000000000000343351527311104300255370ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{f} =} mvksdensity (@var{x}, @var{pts}, @var{Name}, @var{Value}) ## ## Multivariate kernel smoothing density estimate. ## ## @code{@var{f} = mvksdensity (@var{x}, @var{pts})} computes a probability ## density estimate of the sample in the @math{N*D} matrix @var{x}, evaluated at ## the points in the @math{M*D} matrix @var{pts}. Each row of @var{x} is a ## single @math{D}-dimensional observation, and each row of @var{pts} is a point ## at which to evaluate the estimate. The result @var{f} is an @math{M*1} ## vector, with one density value per row of @var{pts}. Rows of @var{x} holding ## a @qcode{NaN}, and rows whose weight is @qcode{NaN}, are removed first, so ## the default bandwidth, the weights and the sample size come from the ## remaining rows. A single observation is valid. ## ## The density estimate uses a product kernel: the multivariate kernel is the ## product of the univariate kernels applied to each dimension, each with its ## own bandwidth. ## ## The following @qcode{Name-Value} pairs are supported: ## ## @multitable @columnfractions 0.18 0.82 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Bandwidth'} @tab The kernel bandwidth, either a positive scalar ## applied to every dimension or a @math{1*D} vector of positive values, one per ## dimension. The default is a diagonal normal-reference (Silverman) rule ## computed from @var{x}, for any number of columns, although MATLAB requires a ## bandwidth from three columns up. ## ## @item @qcode{'Kernel'} @tab The smoothing kernel applied in each dimension: ## @qcode{'normal'} (default), @qcode{'box'}, @qcode{'triangle'}, or ## @qcode{'epanechnikov'}. ## ## @item @qcode{'Function'} @tab The function to estimate: @qcode{'pdf'} ## (default) or @qcode{'cdf'}. ## ## @item @qcode{'Weights'} @tab A vector of non-negative weights, one for each ## row of @var{x}, with a positive weight among the rows kept. The default ## weights are all equal. ## @end multitable ## ## @seealso{ksdensity} ## @end deftypefn function f = mvksdensity (x, pts, varargin) if (nargin < 2) print_usage (); endif if (! (isnumeric (x) && isreal (x) && ismatrix (x) && ndims (x) == 2)) error ("mvksdensity: X must be a matrix of real values."); endif n = rows (x); d = columns (x); if (! (isnumeric (pts) && isreal (pts) && ismatrix (pts) && ndims (pts) == 2)) error ("mvksdensity: PTS must be a matrix of real values."); endif if (columns (pts) != d) error ("mvksdensity: PTS must have the same number of columns as X."); endif ## Defaults and Name-Value parsing. kernel = 'normal'; bw = []; func = 'pdf'; weights = []; if (mod (numel (varargin), 2) != 0) error ("mvksdensity: optional arguments must be Name-Value pairs."); endif for k = 1:2:numel (varargin) if (! ischar (varargin{k})) error ("mvksdensity: parameter names must be character vectors."); endif switch (lower (varargin{k})) case 'bandwidth' bw = varargin{k+1}; case 'kernel' kernel = varargin{k+1}; case 'function' func = lower (varargin{k+1}); case 'weights' weights = varargin{k+1}; otherwise error ("mvksdensity: unknown parameter name '%s'.", varargin{k}); endswitch endfor ## Validate the kernel. if (! (ischar (kernel) && any (strcmpi (kernel, ... {'normal', 'box', 'triangle', 'epanechnikov'})))) error ("mvksdensity: unrecognised 'Kernel' value."); endif kernel = lower (kernel); if (! any (strcmp (func, {'pdf', 'cdf'}))) error ("mvksdensity: unrecognised 'Function' value."); endif ## Weights (normalised to sum to one). Rows with a NaN, in X or in the ## weights, are dropped. if (isempty (weights)) weights = ones (n, 1); elseif (! (isnumeric (weights) && isreal (weights) && isvector (weights) && numel (weights) == n && ! any (weights < 0))) error (strcat ("mvksdensity: 'Weights' must be a non-negative vector", ... " with one element per observation in X.")); endif weights = weights(:); keep = ! (any (isnan (x), 2) | isnan (weights)); if (! any (keep)) error ("mvksdensity: X has no data after removing NaNs."); endif if (! any (weights(keep) > 0)) error ("mvksdensity: 'Weights' has no positive value in the rows kept."); endif x = x(keep,:); n = rows (x); w = weights(keep) / sum (weights(keep)); ## Bandwidth: normal-reference rule per dimension unless supplied. if (isempty (bw)) bw = default_bw (x, n, d); elseif (isnumeric (bw) && isreal (bw) && isscalar (bw) && bw > 0) bw = repmat (bw, 1, d); elseif (! (isnumeric (bw) && isreal (bw) && isvector (bw) && numel (bw) == d && all (bw > 0))) error (strcat ("mvksdensity: 'Bandwidth' must be a positive scalar", ... " or a vector with one element per column of X.")); else bw = bw(:)'; endif ## Evaluate the product-kernel estimate at each query point. m = rows (pts); f = zeros (m, 1); ispdf = strcmp (func, 'pdf'); for j = 1:m ## Standardized distance from every observation to this query point. z = (pts(j,:) - x) ./ bw; if (ispdf) ## Product of the per-dimension kernels, normalised once by prod (bw). k = prod_dim (kernelpdf (z, kernel), d) ./ prod (bw); f(j) = sum (w .* k); else c = prod_dim (kernelcdf (z, kernel), d); f(j) = sum (w .* c); endif endfor endfunction ## Diagonal normal-reference (Silverman) bandwidth, one element per dimension. ## The per-column spread is a robust estimate of the standard deviation, the ## same rule ksdensity uses, and a column whose median absolute deviation is ## zero falls back to its RANGE rather than to its standard deviation. A ## column with no range either takes the whole bandwidth to ones, not just its ## own element: MATLAB abandons the rule for every dimension once one of them ## is degenerate, and the other columns' bandwidths change with it. Measured ## against R2024a, which agrees to 3.2e-14 on ordinary data and differed by 86% ## and 99% on the two degenerate cases before this. function bw = default_bw (x, n, d) sigma = median (abs (x - median (x, 1)), 1) / 0.6745; bad = ! (sigma > 0); if (any (bad)) r = max (x, [], 1) - min (x, [], 1); sigma(bad) = r(bad); endif if (any (! (sigma > 0))) bw = ones (1, d); return; endif bw = sigma * (4 / ((d + 2) * n)) ^ (1 / (d + 4)); endfunction ## Row-wise product across the D columns of a per-dimension kernel matrix. function p = prod_dim (k, d) if (d == 1) p = k; else p = prod (k, 2); endif endfunction %!demo %! ## Bivariate kernel density estimate over a grid, drawn as a contour plot. %! rng (42); %! x = [randn(60, 2); randn(40, 2) + 3]; %! [gx, gy] = meshgrid (linspace (-4, 7, 60)); %! f = mvksdensity (x, [gx(:), gy(:)]); %! contourf (gx, gy, reshape (f, size (gx))); %! hold on; plot (x(:,1), x(:,2), 'k.'); hold off; %! title ('Bivariate kernel density estimate'); %!shared X, pts %! X = [1 1; 2 1; 1 2; 3 2; 2 3; 4 3; 3 4; 5 4]; %! pts = [2 2; 3 3; 1 1; 4 4]; %!test ## MATLAB parity: default (robust normal-reference) bandwidth %! assert_equal (mvksdensity (X, pts), ... %! [0.0569998; 0.0520004; 0.0448930; 0.0382812], 1e-6); %!test ## MATLAB parity: fixed vector and scalar bandwidth (normal kernel) %! assert_equal (mvksdensity (X, pts, "Bandwidth", [1 1]), ... %! [0.0589; 0.0535; 0.0474; 0.0395], 1e-3); %! assert_equal (mvksdensity (X, pts, "Bandwidth", 1), ... %! mvksdensity (X, pts, "Bandwidth", [1 1])); %!test ## MATLAB parity: box and epanechnikov product kernels %! assert_equal (mvksdensity (X, pts, "Bandwidth", [1 1], "Kernel", "box"), ... %! [0.0521; 0.0417; 0.0313; 0.0313], 1e-3); %! assert_equal (mvksdensity (X, pts, "Bandwidth", [1 1], ... %! "Kernel", "epanechnikov"), [0.0585; 0.0523; 0.0411; 0.0383], 1e-3); %!test ## MATLAB parity: cumulative distribution and weighted estimate %! assert_equal (mvksdensity (X, pts, "Bandwidth", [1 1], "Function", "cdf"), ... %! [0.2144; 0.4504; 0.0520; 0.6893], 1e-3); %! assert_equal (mvksdensity (X, pts, "Bandwidth", [1 1], ... %! "Weights", [2 1 1 1 1 1 1 1]), [0.0588; 0.0479; 0.0598; 0.0351], 1e-3); %!test ## the density integrates to ~1 over a wide grid %! [gx, gy] = meshgrid (linspace (-6, 11, 220)); %! f = mvksdensity (X, [gx(:), gy(:)]); %! dx = gx(1,2) - gx(1,1); %! assert_equal (sum (f) * dx ^ 2, 1, 1e-2); ## MATLAB parity: the default bandwidth where a column's median absolute ## deviation is zero. The fallback is that column's RANGE, not its standard ## deviation, which is the same rule ksdensity follows. Before this the two ## differed by 86%. %!test %! xz = [1, 2.3; 1, 3.1; 1, 4.8; 1, 5.5; 1, 6.1; 3, 7.9; 5, 8.2; 9, 9.4]; %! f = mvksdensity (xz, [3, 4; 5, 6]); %! assert_equal (f, [0.006520360257529; 0.006588943731769], 1e-12); ## MATLAB parity: a column with no range at all takes the WHOLE bandwidth to ## ones, not merely its own element, so the other columns' bandwidths move ## with it. Before this the two differed by 99%. %!test %! xc = [2, 2.3; 2, 3.1; 2, 4.8; 2, 5.5; 2, 6.1; 2, 7.9; 2, 8.2; 2, 9.4]; %! f = mvksdensity (xc, [3, 4; 5, 6]); %! assert_equal (f, [0.024910428037417; 0.000582734811254], 1e-12); %! assert_equal (f, mvksdensity (xc, [3, 4; 5, 6], 'Bandwidth', [1, 1]), 1e-12); ## An ordinary column is untouched by either fallback. %!test %! xh = [1.1, 2.3; 2.4, 3.1; 3.9, 4.8; 4.2, 5.5; 5.7, 6.1; 6.3, 7.9; ... %! 7.1, 8.2; 8.8, 9.4]; %! assert_equal (mvksdensity (xh, [3, 4; 5, 6]), ... %! [0.014366107896069; 0.016985761603336], 1e-12); ## Three or more predictors take a default bandwidth here. R2024a cannot: ## its default path yields a two-element bandwidth whatever the dimension and ## it raises "Bandwidth must be a scalar or a vector with 3 elements" on any ## three-column call, healthy data included. Documented deviation. %!test %! x3 = [1.1, 5.2, 2.3; 2.4, 6.1, 3.1; 3.9, 4.4, 4.8; 4.2, 7.7, 5.5; ... %! 5.7, 5.9, 6.1; 6.3, 8.3, 7.9; 7.1, 6.6, 8.2; 8.8, 9.1, 9.4]; %! f = mvksdensity (x3, [3, 6, 4; 5, 7, 6]); %! assert_equal (size (f), [2, 1]); %! assert_equal (all (f > 0), true); ## Rows with a NaN are dropped before the estimate is computed. %!test %! xn = X; %! xn(3,1) = NaN; %! assert_equal (mvksdensity (xn, pts, "Bandwidth", [1 1]), ... %! [0.053502; 0.05931; 0.040328; 0.045141], 1e-6); ## A NaN in any column removes the whole row, including all-NaN rows. %!test %! xn = X; %! xn(3,1) = NaN; %! xn = [xn; 2 NaN; NaN NaN]; %! assert_equal (mvksdensity (xn, pts, "Bandwidth", [1 1]), ... %! mvksdensity (X([1:2, 4:8],:), pts, "Bandwidth", [1 1]), 1e-12); ## The default bandwidth is computed from the rows that remain. %!test %! xn = X; %! xn(3,1) = NaN; %! assert_equal (mvksdensity (xn, pts), ... %! mvksdensity (X([1:2, 4:8],:), pts), 1e-12); ## A NaN weight drops its row; the other weights are renormalised. %!test %! xr = X([1:2, 4:8],:); %! wr = [2 1 1 1 1 1 3]; %! wn = wr; %! wn(3) = NaN; %! assert_equal (mvksdensity (xr, pts, "Bandwidth", [1 1], "Weights", wn), ... %! mvksdensity (xr([1:2, 4:7],:), pts, "Bandwidth", [1 1], ... %! "Weights", wr([1:2, 4:7])), 1e-12); ## A single observation is valid, also when it is what remains after dropping. %!test %! f = mvksdensity ([1 1], pts(1:3,:), "Bandwidth", [1 1]); %! assert_equal (f, [0.05855; 0.002915; 0.15915], 1e-5); %! assert_equal (mvksdensity ([1 1], pts(1:3,:)), f); %! assert_equal (mvksdensity ([1 1; NaN 2], pts(1:3,:), ... %! "Bandwidth", [1 1]), f); ## From three columns up the default bandwidth follows the same rule. %!test %! Xt = [1 2 3; 2 1 4; 4 3 5; 3 4 2; 4 2 6; 5 5 5]; %! Qt = [2 2 3; 3 3 4]; %! bw = median (abs (Xt - median (Xt, 1)), 1) / 0.6745; %! bw *= (4 / (5 * 6)) ^ (1 / 7); %! assert_equal (mvksdensity (Xt, Qt), ... %! mvksdensity (Xt, Qt, "Bandwidth", bw), 1e-14); ## Test input validation %!error mvksdensity (ones (3, 2)) %!error ... %! mvksdensity (ones (2, 2, 2), [1 1]) %!error ... %! mvksdensity ([NaN 1; NaN 2], [1 1]) %!error ... %! mvksdensity ([1 2; 3 4], [1 1], "Weights", [NaN NaN]) %!error ... %! mvksdensity ([NaN 1; 2 2; 3 3], [1 1], "Weights", [1 0 0]) %!error ... %! mvksdensity ([1 1; 2 2], [1 1], "Weights", [0 0]) %!error ... %! mvksdensity ([1 1; 2 2; 3 3], [1 1], "Weights", [0 0 NaN]) %!error ... %! mvksdensity ([1 1; 2 2], [1 1 1]) %!error ... %! mvksdensity ([1 1; 2 2], [1 1], "Bandwidth") %!error ... %! mvksdensity ([1 1; 2 2], [1 1], "Kernel", "cosine") %!error ... %! mvksdensity ([1 1; 2 2], [1 1], "Function", "icdf") %!error ... %! mvksdensity ([1 1; 2 2], [1 1], "Bandwidth", [1 2 3]) %!error ... %! mvksdensity ([1 1; 2 2], [1 1], "Bandwidth", -1) %!error ... %! mvksdensity ([1 1; 2 2], [1 1], "Weights", [1 2 3]) statistics-release-2.1.2/inst/Descriptive_Statistics/nancov.m000066400000000000000000000204101527311104300244300ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{c} =} nancov (@var{x}) ## @deftypefnx {statistics} {@var{c} =} nancov (@var{x}, @var{y}) ## @deftypefnx {statistics} {@var{c} =} nancov (@dots{}, @var{normalization}) ## @deftypefnx {statistics} {@var{c} =} nancov (@dots{}, @var{method}) ## ## Compute the covariance matrix while handling NaN values. ## ## @code{@var{c} = nancov (@var{x})} returns the covariance matrix of the ## columns of @var{x}, treating each row as an observation. Rows containing ## @qcode{NaN} values are removed before computing the covariance. If @var{x} ## is a vector, the scalar variance of its non-@qcode{NaN} elements is returned. ## ## @code{@var{c} = nancov (@var{x}, @var{y})}, where @var{x} and @var{y} are of ## equal length, is equivalent to @code{nancov ([@var{x}(:), @var{y}(:)])} and ## returns the 2-by-2 covariance matrix. ## ## @code{@var{c} = nancov (@dots{}, @var{normalization})} specifies the ## normalization. When @var{normalization} is 0 (default), the covariance is ## normalized by @math{N-1}, where @math{N} is the number of observations used. ## When it is 1, it is normalized by @math{N}. ## ## @code{@var{c} = nancov (@dots{}, @var{method})} selects how @qcode{NaN} ## values are handled. With @qcode{"complete"} (the default), any row of the ## data that contains a @qcode{NaN} value is removed before the covariance is ## computed. With @qcode{"pairwise"}, each element @code{@var{c}(i,j)} is ## computed using all rows in which both column @var{i} and column @var{j} are ## non-@qcode{NaN}; the resulting matrix may fail to be positive semidefinite. ## ## @var{x} and @var{y} must be real floating point, although MATLAB accepts ## logical too. ## ## @seealso{cov, nanvar, nanstd, nanmean} ## @end deftypefn function c = nancov (varargin) if (nargin < 1) print_usage (); endif x = varargin{1}; if (! (isfloat (x) && isreal (x))) error ("nancov: X must be a real floating point matrix or vector."); endif args = varargin(2:end); ## Separate a trailing method string from the numeric arguments method = 'complete'; strmask = cellfun (@ischar, args); if (any (strmask)) sopt = args(strmask); if (numel (sopt) > 1) error ("nancov: only one METHOD option may be specified."); endif method = lower (sopt{1}); if (! any (strcmp (method, {'complete', 'pairwise'}))) error ("nancov: METHOD must be 'complete' or 'pairwise'."); endif args = args(! strmask); endif ## Remaining numeric arguments: an optional Y and/or a normalization flag y = []; y_given = false; nrm = 0; if (numel (args) == 1) a = args{1}; if (isscalar (a) && (a == 0 || a == 1)) nrm = a; else y = a; y_given = true; endif elseif (numel (args) == 2) y = args{1}; y_given = true; nrm = args{2}; if (! (isscalar (nrm) && (nrm == 0 || nrm == 1))) error ("nancov: normalization flag must be 0 or 1."); endif elseif (numel (args) > 2) error ("nancov: too many input arguments."); endif if (ndims (x) > 2 || (y_given && ndims (y) > 2)) error ("nancov: Inputs must be 2-D."); endif ## Assemble the data matrix (observations in rows, variables in columns) if (y_given) if (! (isfloat (y) && isreal (y))) error ("nancov: Y must be a real floating point matrix or vector."); endif if (numel (x) != numel (y)) error ("nancov: X and Y must have the same number of elements."); endif X = [x(:), y(:)]; else if (isequal (size (x), [0, 0])) c = NaN (class (x)); return; endif if (isvector (x)) X = x(:); else X = x; endif endif p = columns (X); if (strcmp (method, 'complete')) good = all (! isnan (X), 2); Xc = X(good, :); n = rows (Xc); if (n == 0) c = NaN (p, p, class (X)); return; endif Xd = Xc - mean (Xc, 1); if (nrm == 1 || n == 1) d = n; else d = n - 1; endif c = (Xd' * Xd) / d; else c = zeros (p, p, class (X)); for i = 1:p for j = i:p rc = ! isnan (X(:,i)) & ! isnan (X(:,j)); xi = X(rc, i); xj = X(rc, j); n = numel (xi); if (n == 0) cij = NaN; else xi -= mean (xi); xj -= mean (xj); if (nrm == 1 || n == 1) d = n; else d = n - 1; endif cij = sum (xi .* xj) / d; endif c(i,j) = cij; c(j,i) = cij; endfor endfor endif endfunction %!demo %! ## Covariance matrix of a data set with missing values (complete-case). %! %! x = [1 2 3; 4 5 NaN; 7 NaN 9; 10 11 12; NaN 14 15] %! c = nancov (x) %!demo %! ## The same data set using pairwise deletion of missing values. %! %! x = [1 2 3; 4 5 NaN; 7 NaN 9; 10 11 12; NaN 14 15] %! c = nancov (x, 'pairwise') ## Test output %!assert_equal (nancov ([1 2 3; 4 5 NaN; 7 NaN 9; 10 11 12; NaN 14 15]), ... %! 40.5 * ones (3)) %!assert_equal (nancov ([1 2 3; 4 5 NaN; 7 NaN 9; 10 11 12; NaN 14 15], 1), ... %! 20.25 * ones (3)) %!assert_equal (nancov ([1 2 3; 4 5 NaN; 7 NaN 9; 10 11 12; NaN 14 15], ... %! 'pairwise'), [15, 21, 21; 21, 30, 39; 21, 39, 26.25]) %!assert_equal (nancov ([1 2 3; 4 5 NaN; 7 NaN 9; 10 11 12; NaN 14 15], 1, ... %! 'pairwise'), [11.25, 14, 14; 14, 22.5, 26; 14, 26, 19.6875]) %!assert_equal (nancov ([1 2 3 NaN 5]', [2 NaN 6 8 10]'), [4, 8; 8, 16]) %!assert_equal (nancov ([1 2 3 4 5]'), 2.5) %!assert_equal (nancov (5), 0) %!assert_equal (nancov (NaN (3, 2)), NaN (2, 2)) %!assert_equal (nancov ([], []), NaN (2, 2)) %!test %! c = nancov (single ([1 2; 3 5; 4 9]), 'pairwise'); %! assert_equal (class (c), 'single'); %! assert_equal (c, single ([2.3333, 5.1667; 5.1667, 12.333]), 1e-3); %!test %! c = nancov (single (NaN (3, 2)), 'pairwise'); %! assert_equal (class (c), 'single'); %! assert_equal (c, single (NaN (2, 2))); %!test %! c = nancov (single (zeros (0, 3))); %! assert_equal (class (c), 'single'); %! assert_equal (c, single (NaN (3, 3))); %!assert_equal (class (nancov (single ([]))), 'single') ## Test input validation %!error nancov () %!error nancov ({1}) %!error ... %! nancov (int8 ([1 2; 3 5])) %!error ... %! nancov (int32 ([1 2; 3 5]), 'pairwise') %!error ... %! nancov (true (2)) %!error ... %! nancov (logical ([])) %!error ... %! nancov ([1 2; 3 4] + 1i) %!error ... %! nancov ([1 2 3], int8 ([1 2 3])) %!error ... %! nancov ([1 2 3], [true false true]) %!error ... %! nancov ([1 2; 3 4], 'bogus') %!error ... %! nancov ([1 2 3], [1 2]) %!error nancov ([1 2], [3 4], 0, 1) ## Test edge cases %!assert_equal (nancov ([]), NaN) %!assert_equal (nancov (zeros (0, 3)), NaN (3, 3)) %!assert_equal (nancov (zeros (3, 0)), zeros (0, 0)) %!assert_equal (nancov ([NaN, NaN, NaN]), NaN) %!assert_equal (nancov ([1, NaN, 3; NaN, NaN, NaN]), NaN (3, 3)) %!assert_equal (nancov (zeros (0, 3), zeros (0, 3)), NaN (2, 2)) %!error nancov (ones (2, 0, 3, 2)) statistics-release-2.1.2/inst/Descriptive_Statistics/nanmax.m000066400000000000000000000265751527311104300244500ustar00rootroot00000000000000## Copyright (C) 2001 Paul Kienzle ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{v} =} nanmax (@var{x}) ## @deftypefnx {statistics} {@var{v} =} nanmax (@var{x}, [], @var{dim}) ## @deftypefnx {statistics} {[@var{v}, @var{idx}] =} nanmax (@dots{}) ## @deftypefnx {statistics} {@var{v} =} nanmax (@var{x}, [], @qcode{'all'}) ## @deftypefnx {statistics} {@var{v} =} nanmax (@var{x}, [], @var{vecdim}) ## @deftypefnx {statistics} {@var{v} =} nanmax (@var{x}, @var{y}) ## ## Find the maximum while ignoring NaN values. ## ## @code{@var{v} = nanmax (@var{x})} returns the maximum of @var{x}, after ## removing @qcode{NaN} values. If @var{x} is a vector, a scalar maximum value ## is returned. If @var{x} is a matrix, a row vector of column maxima is ## returned. If @var{x} is a multidimensional array, the @code{nanmax} operates ## along the first nonsingleton dimension. If all values in a column are ## @qcode{NaN}, the maximum is returned as @qcode{NaN} rather than @qcode{[]}. ## ## @code{@var{v} = nanmax (@var{x}, [], @var{dim})} operates along the dimension ## @var{dim} of @var{x}. A logical @var{dim} or @var{vecdim} is an error, ## although MATLAB accepts @code{true} as dimension 1. ## ## @code{[@var{v}, @var{idx}] = nanmax (@dots{})} also returns the row indices ## of the maximum values for each column in the vector @var{idx}. When @var{x} ## is a vector, then @var{idx} is a scalar value as @var{v}. ## ## @code{@var{v} = nanmax (@var{x}, [], @qcode{'all'})} returns the maximum of ## all elements of @var{x}, after removing @qcode{NaN} values. It is the ## equivalent of @code{nanmax (@var{x}(:))}. The optional flag @qcode{'all'} ## cannot be used together with @var{dim} or @var{vecdim} input arguments. ## ## @code{@var{v} = nanmax (@var{x}, [], @var{vecdim})} returns the maximum over ## the dimensions specified in the vector @var{vecdim}. Each element of ## @var{vecdim} represents a dimension of the input array @var{x} and the output ## @var{v} has length 1 in the specified operating dimensions. The lengths of ## the other dimensions are the same for @var{x} and @var{y}. For example, if ## @var{x} is a 2-by-3-by-4 array, then @code{nanmax (@var{x}, [1 2])} returns a ## 1-by-1-by-4 array. Each element of the output array is the maximum of the ## elements on the corresponding page of @var{x}. If @var{vecdim} indexes all ## dimensions of @var{x}, then it is equivalent to ## @code{nanmax (@var{x}, @qcode{'all'})}. Any dimension in @var{vecdim} ## greater than @code{ndims (@var{x})} is ignored. ## ## @seealso{max, nanmin, nansum} ## @end deftypefn function [v, idx] = nanmax (x, y, dim) if (nargin < 1 || nargin > 3) print_usage; elseif (nargin == 1 || (nargin == 2 && isempty (y))) nanvals = isnan (x); x(nanvals) = -Inf; [v, idx] = max (x); if (any (nanvals(:))) v(all (nanvals)) = NaN; endif elseif (nargin == 3 && strcmpi (dim, 'all') && isempty (y)) x = x(:); nanvals = isnan (x); x(nanvals) = -Inf; [v, idx] = max (x); if (any (nanvals(:))) v(all (nanvals)) = NaN; endif elseif (nargin == 3 && isempty (y)) if (isscalar (dim)) if (! isnumeric (dim) || ! isfinite (dim) || fix (dim) != dim || dim <= 0) error ("nanmax: DIM must be a positive integer."); endif nanvals = isnan (x); x(nanvals) = -Inf; [v, idx] = max (x, [], dim); if (any (nanvals(:))) v(all (nanvals, dim)) = NaN; endif else if (! (isnumeric (dim) && isvector (dim))) error ("nanmax: invalid DIM argument."); elseif (any (! isfinite (dim)) || any (fix (dim) != dim) || any (dim <= 0)) error ("nanmax: VECDIM must be a vector of positive integers."); endif vecdim = sort (dim); if (! all (diff (vecdim))) error ("nanmax: VECDIM must contain non-repeating positive integers."); endif ## Ignore dimensions in VECDIM larger than actual array vecdim(find (vecdim > ndims (x))) = []; if (isempty (vecdim)) v = x; if (nargout > 1) idx = reshape ([1:numel(x)], size (x)); endif else ## Calculate permutation vector szx = size (x); remdims = 1:ndims (x); # All dimensions remdims(vecdim) = []; # Delete dimensions specified by vecdim nremd = numel (remdims); ## If all dimensions are given, it is equivalent to 'all' flag if (nremd == 0) x = x(:); nanvals = isnan (x); x(nanvals) = -Inf; [v, idx] = max (x); if (any (nanvals(:))) v(all (nanvals)) = NaN; endif else ## Permute to push vecdims to back perm = [remdims, vecdim]; x = permute (x, perm); ## Reshape to squash all vecdims in final dimension sznew = [szx(remdims), prod(szx(vecdim))]; x = reshape (x, sznew); ## Calculate nanmax on final dimension dim = nremd + 1; nanvals = isnan (x); x(nanvals) = -Inf; [v, idx] = max (x, [], dim); if (any (nanvals(:))) v(all (nanvals, dim)) = NaN; endif ## Inverse permute back to correct dimensions v = ipermute (v, perm); idx = ipermute (idx, perm); endif endif endif else if (nargout > 1) error ("nanmax: a second output is not supported with this syntax."); endif Xnan = isnan (x); Ynan = isnan (y); x(Xnan) = -Inf; y(Ynan) = -Inf; v = max (x, y); if (any (Xnan(:)) && any (Ynan(:))) v(Xnan & Ynan) = NaN; endif endif endfunction %!demo %! ## Find the column maximum values and their indices %! ## for matrix data with missing values. %! %! x = magic (3); %! x([1, 6:9]) = NaN %! [y, ind] = nanmax (x) %!demo %! ## Find the maximum of all the values in an array, ignoring missing values. %! ## Create a 2-by-5-by-3 array x with some missing values. %! %! x = reshape (1:30, [2, 5, 3]); %! x([10:12, 25]) = NaN %! %! ## Find the maximum of the elements of x. %! %! y = nanmax (x, [], 'all') ## Test output %!assert_equal (nanmax ([]), []) %!assert_equal (nanmax (zeros (0, 3)), zeros (0, 3)) %!assert_equal (nanmax (zeros (3, 0)), zeros (1, 0)) %!assert_equal (size (nanmax (ones (2, 0, 3, 2))), [1, 0, 3, 2]) %!assert_equal (nanmax ([], [], 1), []) %!assert_equal (nanmax ([], [], 2), []) %!assert_equal (size (nanmax (ones (2, 0, 3, 2), [], 1)), [1, 0, 3, 2]) %!assert_equal (size (nanmax (ones (2, 0, 3, 2), [], 2)), [2, 0, 3, 2]) %!assert_equal (size (nanmax (ones (2, 0, 3, 2), [], 'all')), [0, 1]) %!assert_equal (size (nanmax (ones (2, 0, 3, 2), [], [1, 2])), [0, 1, 3, 2]) %!assert_equal (nanmax ([NaN, NaN, NaN]), NaN) %!assert_equal (nanmax ([2, 4, NaN, 7]), 7) %!assert_equal (nanmax ([2, 4, NaN, Inf]), Inf) %!assert_equal (nanmax ([1, NaN, 3; NaN, 5, 6; 7, 8, NaN]), [7, 8, 6]) %!assert_equal (nanmax ([1, NaN, 3; NaN, 5, 6; 7, 8, NaN]'), [3, 6, 8]) %!assert_equal (nanmax (single ([1, NaN, 3; NaN, 5, 6; 7, 8, NaN])), single ([7, 8, 6])) %!shared x, y %! x(:,:,1) = [1.77, -0.005, NaN, -2.95; NaN, 0.34, NaN, 0.19]; %! x(:,:,2) = [1.77, -0.005, NaN, -2.95; NaN, 0.34, NaN, 0.19] + 5; %! y = x; %! y(2,3,1) = 0.51; %!assert_equal (nanmax (x, [], [1, 2])(:), [1.77;6.77]) %!assert_equal (nanmax (x, [], [1, 3])(:), [6.77;5.34;NaN;5.19]) %!assert_equal (nanmax (x, [], [2, 3])(:), [6.77;5.34]) %!assert_equal (nanmax (x, [], [1, 2, 3]), 6.77) %!assert_equal (nanmax (x, [], 'all'), 6.77) %!assert_equal (nanmax (y, [], [1, 3])(:), [6.77;5.34;0.51;5.19]) %!assert_equal (nanmax (x(1,:,1), x(2,:,1)), [1.77, 0.34, NaN, 0.19]) %!assert_equal (nanmax (x(1,:,2), x(2,:,2)), [6.77, 5.34, NaN, 5.19]) %!assert_equal (nanmax (y(1,:,1), y(2,:,1)), [1.77, 0.34, 0.51, 0.19]) %!assert_equal (nanmax (y(1,:,2), y(2,:,2)), [6.77, 5.34, NaN, 5.19]) ## Test dimension indexing with vecdim in N-dimensional arrays %!test %! xx = repmat ([1:20;6:25], [5 2 6 3]); %! assert_equal (size (nanmax (xx, [], [3, 2])), [10, 1, 1, 3]); %! assert_equal (size (nanmax (xx, [], [1, 2])), [1, 1, 6, 3]); %! assert_equal (size (nanmax (xx, [], [1, 2, 4])), [1, 1, 6]); %! assert_equal (size (nanmax (xx, [], [1, 4, 3])), [1, 40]); %! assert_equal (size (nanmax (xx, [], [1, 2, 3, 4])), [1, 1]); ## Test exceeding dimensions %!assert_equal (nanmax (ones (2), [], 3), ones (2, 2)) %!assert_equal (nanmax (ones (2, 2, 2), [], 99), ones (2, 2, 2)) %!assert_equal (nanmax (magic (3), [], 3), magic (3)) %!assert_equal (nanmax (magic (3), [], [1, 3]), [8, 9, 7]) %!assert_equal (nanmax (magic (3), [], [1, 99]), [8, 9, 7]) ## Test comparisons %!assert_equal (nanmax (ones (2), 3), 3 * ones (2,2)) ## Test logical input %!assert_equal (nanmax (logical ([0, 1, 0])), logical (1)) %!assert_equal (nanmax (logical ([0, 1; 1, 0])), logical ([1, 1])) %!assert_equal (nanmax (logical ([0, 1; 1, 0]), [], 2), logical ([1; 1])) %!assert_equal (nanmax (logical ([0, 1; 1, 0]), [], 'all'), logical (1)) %!assert_equal (nanmax (logical ([0, 1; 1, 0]), [], [1, 2]), true) %!assert_equal (nanmax (true (2, 2, 2), [], 3), true (2, 2)) %!assert_equal (nanmax (logical (zeros (0, 3))), logical (zeros (0, 3))) %!assert_equal (nthargout (2, @nanmax, logical ([0, 1, 0])), 2) %!assert_equal (nthargout (2, @nanmax, logical ([0, 1; 1, 0]), [], 2), [2; 1]) %!assert_equal (nanmax (logical ([0, 1; 1, 0]), true), true (2)) %!assert_equal (nanmax (logical ([0, 0]), NaN), [0, 0]) %!assert_equal (nanmax ([NaN, 0], logical ([0, 1])), [0, 1]) ## Test input validation %!error nanmax (ones (3), [], 0) %!error nanmax (ones (3), [], 1.5) %!error nanmax (ones (3), [], Inf) %!error ... %! nanmax (ones (3), [], [1, Inf]) %!error nanmax (ones (3), [], true) %!error ... %! nanmax (ones (3, 3, 3), [], [2, 2.5]) %!error ... %! nanmax (ones (3, 3, 3), [], [-1, 2]) %!error ... %! nanmax (y, [], [1, 1, 2]) %!error ... %! nanmax (true (2), [], [1, 1]) %!error nanmax (ones (3), [], "invalid") %!error nanmax (ones (3), [], ones (2)) %!error nanmax (ones (3), [], []) %!error ... %! nanmax (ones (3), [], [true, false]) %!error ... %! [v, idx] = nanmax (x, y, [1 2]) statistics-release-2.1.2/inst/Descriptive_Statistics/nanmean.m000066400000000000000000000173031527311104300245700ustar00rootroot00000000000000## Copyright (C) 2025 Leonardo Araujos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{s} =} nanmean (@var{x}) ## @deftypefnx {statistics} {@var{s} =} nanmean (@var{x}, @qcode{'all'}) ## @deftypefnx {statistics} {@var{s} =} nanmean (@var{x}, @var{dim}) ## @deftypefnx {statistics} {@var{s} =} nanmean (@var{x}, @var{vecdim}) ## ## Compute the mean while ignoring NaN values. ## ## @code{@var{s} = nanmean (@var{x})} returns the mean of @var{x} after removing ## @qcode{NaN} values. If @var{x} is a vector, a scalar value is returned. If ## @var{x} is a matrix, a row vector of column means is returned. If @var{x} is ## a multidimensional array, @code{nanmean} operates along the first ## nonsingleton dimension. If all values along a dimension are @qcode{NaN}, the ## mean is returned returned as @qcode{NaN}. ## ## @code{@var{s} = nanmean (@var{x}, @qcode{'all'})} returns the mean of all ## elements of @var{x}, after removing @qcode{NaN} values. It is the equivalent ## of @code{nanmean (@var{x}(:))}. ## ## @code{@var{s} = nanmean (@var{x}, @var{dim})} operates along the dimension ## @var{dim} of @var{x}. A logical @var{dim} or @var{vecdim} is an error, ## although MATLAB accepts @code{true} as dimension 1. ## ## @code{@var{s} = nanmean (@var{x}, @var{vecdim})} returns the mean over the ## dimensions specified in the vector @var{vecdim}. Each element of ## @var{vecdim} represents a dimension of the input array @var{x} and the output ## @var{s} has length 1 in the specified operating dimensions. The lengths of ## the other dimensions are the same for @var{x} and @var{y}. For example, if ## @var{x} is a 2-by-3-by-4 array, then @code{nanmean (@var{x}, [1 2])} returns ## a ## 1-by-1-by-4 array. Each element of the output array is the mean of the ## elements on the corresponding page of @var{x}. If @var{vecdim} indexes all ## dimensions of @var{x}, then it is equivalent to ## @code{nanmean (@var{x}, @qcode{'all'})}. Any dimension in @var{vecdim} ## greater than @code{ndims (@var{x})} is ignored. ## ## @code{nanmean} primarily operates on @qcode{single} and @qcode{double} ## numeric types, since they support @qcode{NaN} values, while preserving the ## data type. Nevertheless, it can also operate on integer types by treating ## them as @qcode{double} types. To avoid overflow on very large @qcode{int64} ## and @qcode{uint64} values, use the @code{mean} function, which applies ## special handling for such cases. ## ## @seealso{mean, nansum, nanmin, nanmax} ## @end deftypefn function y = nanmean (x, dim) if (nargin < 1 || nargin > 2) print_usage (); elseif (! isnumeric (x) && ! islogical (x)) error ("nanmean: X must be numeric or logical."); ## 0 by 0 and no DIM given elseif (nargin < 2 && isequal (size (x), [0, 0])) y = NaN; else ## Determine the first nonsingleton dimension to operate on sx = size (x); if (nargin < 2) dim = find (sx != 1, 1); if (isempty (dim)) # scalar dim = 1; endif elseif (ischar (dim) && strcmpi (dim, 'all')) x = x(:); dim = 1; elseif (isscalar (dim)) if (! isnumeric (dim) || ! isfinite (dim) || fix (dim) != dim || dim <= 0) error ("nanmean: DIM must be a positive integer."); endif elseif (isnumeric (dim) && isvector (dim)) if (any (! isfinite (dim)) || any (fix (dim) != dim) || any (dim <= 0)) error ("nanmean: VECDIM must be a vector of positive integers."); endif dim = sort (dim); if (! all (diff (dim))) error ("nanmean: VECDIM must contain non-repeating positive integers."); endif else error ("nanmean: invalid DIM argument."); endif ## A dimension past the last leaves logical and sparse X as they are if (isscalar (dim) && dim > ndims (x) && (islogical (x) || issparse (x))) y = x; return; endif na = isnan (x); x(na) = 0; na = ! na; if (isscalar (dim)) y = sum (x, dim) ./ sum (na, dim); else for i = numel (dim):-1:1, x = sum (x, dim(i)); na = sum (na, dim(i)); endfor y = x ./ na; endif endif endfunction %!demo %! ## Find the column means for a matrix with missing values., %! %! x = magic (3); %! x([1, 4, 7:9]) = NaN %! y = nanmean (x) %!demo %! ## Find the row means for a matrix with missing values., %! %! x = magic (3); %! x([1, 4, 7:9]) = NaN %! y = nanmean (x, 2) %!demo %! ## Find the mean of all the values in a multidimensional array %! ## with missing values. %! %! x = reshape (1:30, [2, 5, 3]); %! x([10:12, 25]) = NaN %! y = nanmean (x, 'all') %!demo %! ## Find the mean of a multidimensional array with missing values over %! ## multiple dimensions. %! %! x = reshape (1:30, [2, 5, 3]); %! x([10:12, 25]) = NaN %! y = nanmean (x, [2, 3]) ## Test output %!assert_equal (nanmean ([]), NaN) %!assert_equal (nanmean (zeros (0, 3)), [NaN, NaN, NaN]) %!assert_equal (nanmean (zeros (3, 0)), zeros (1, 0)) %!assert_equal (nanmean ([], 1), zeros (1, 0)) %!assert_equal (nanmean ([], 2), zeros (0, 1)) %!assert_equal (size (nanmean (ones (2, 0, 3, 2), 2)), [2, 1, 3, 2]) %!assert_equal (nanmean (NaN), NaN) %!assert_equal (nanmean (NaN (3)), [NaN, NaN, NaN]) %!assert_equal (nanmean ([3 2 NaN 7]), 4) %!assert_equal (nanmean ([2 4 NaN Inf]), Inf) %!assert_equal (nanmean ([1 NaN 3; NaN 4 6; 7 8 NaN]), [4 6 4.5]) %!assert_equal (nanmean ([1 NaN 3; NaN 5 6; 7 8 NaN], 2), [2; 5.5; 7.5]) %!assert_equal (nanmean (uint8 ([2 4 1 7])), 3.5) %!test %! x = magic (3); %! x([1 6:9]) = NaN; %! assert_equal (nanmean (x), [3.5, 3, NaN]) %! assert_equal (nanmean (x, 2), [1; 4; 4]) %!test %! x = reshape (1:24, [2, 4, 3]); %! x([5:6, 20]) = NaN; %! assert_equal (nanmean (x, 'all'), 269/21) %!test %! x = reshape (1:24,[2, 4, 3]); %! x([5:6, 20]) = NaN; %! assert_equal (squeeze (nanmean (x, [1, 2])), [25/6; 100/8; 144/7]) %! assert_equal (nanmean (x, [2, 3]), [139/11; 13]) ## Test logical input %!assert_equal (nanmean (logical ([1, 0, 1; 0, 0, 1])), [0.5, 0, 1]) %!assert_equal (nanmean (sparse ([1 2; 3 4]), 3), sparse ([1 2; 3 4])) %!assert_equal (nanmean (true (2), 3), true (2)) %!assert_equal (nanmean (int8 ([1 2]), 3), [1, 2]) ## Test input validation %!error nanmean () %!error nanmean ("str") %!error nanmean (ones (3), 0) %!error nanmean (ones (3), 1.5) %!error nanmean (ones (3), Inf) %!error ... %! nanmean (ones (3), [1, Inf]) %!error nanmean (ones (3), true) %!error nanmean (ones (3), [1, -1]) %!error ... %! nanmean (ones (3, 3, 3), [2, 2, 3]) %!error nanmean (ones (3), "invalid") %!error nanmean (ones (3), ones (2)) %!error nanmean (ones (3), []) statistics-release-2.1.2/inst/Descriptive_Statistics/nanmedian.m000066400000000000000000000143141527311104300251040ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{m} =} nanmedian (@var{x}) ## @deftypefnx {statistics} {@var{m} =} nanmedian (@var{x}, @qcode{'all'}) ## @deftypefnx {statistics} {@var{m} =} nanmedian (@var{x}, @var{dim}) ## @deftypefnx {statistics} {@var{m} =} nanmedian (@var{x}, @var{vecdim}) ## ## Compute the median while ignoring NaN values. ## ## @code{@var{m} = nanmedian (@var{x})} returns the median of @var{x}, after ## removing @qcode{NaN} values. If @var{x} is a vector, a scalar value is ## returned. If @var{x} is a matrix, a row vector of column medians is ## returned. If @var{x} is a multidimensional array, @code{nanmedian} operates ## along the first nonsingleton dimension. If all values along a dimension are ## @qcode{NaN}, the median is returned as @qcode{NaN}. As in @code{median}, ## @var{m} takes the class of @var{x}, so an integer median is rounded. ## ## @code{@var{m} = nanmedian (@var{x}, @qcode{'all'})} returns the median of all ## elements of @var{x}, after removing @qcode{NaN} values. It is the equivalent ## of @code{nanmedian (@var{x}(:))}. ## ## @code{@var{m} = nanmedian (@var{x}, @var{dim})} operates along the dimension ## @var{dim} of @var{x}. A logical @var{dim} or @var{vecdim} is an error, as in ## MATLAB. ## ## @code{@var{m} = nanmedian (@var{x}, @var{vecdim})} returns the median over ## the dimensions specified in the vector @var{vecdim}. Each element of ## @var{vecdim} represents a dimension of the input array @var{x} and the output ## @var{m} has length 1 in the specified operating dimensions. If @var{vecdim} ## indexes all dimensions of @var{x}, then it is equivalent to ## @code{nanmedian (@var{x}, @qcode{'all'})}. Any dimension in @var{vecdim} ## greater than @code{ndims (@var{x})} is ignored. ## ## @seealso{median, nanmean, nansum} ## @end deftypefn function m = nanmedian (x, dim) if (nargin < 1 || nargin > 2) print_usage (); endif if (! isnumeric (x) && ! islogical (x)) error ("nanmedian: X must be numeric or logical."); endif ## Operating dimension(s) if (nargin < 2) m = median (x, 'omitnan'); return; elseif (ischar (dim) && strcmpi (dim, 'all')) dim = 'all'; elseif (isscalar (dim)) if (! isnumeric (dim) || ! isfinite (dim) || fix (dim) != dim || dim <= 0) error ("nanmedian: DIM must be a positive integer."); endif elseif (isnumeric (dim) && isvector (dim)) if (any (! isfinite (dim)) || any (fix (dim) != dim) || any (dim <= 0)) error ("nanmedian: VECDIM must be a vector of positive integers."); endif if (! all (diff (sort (dim)))) error ("nanmedian: VECDIM must contain non-repeating positive integers."); endif else error ("nanmedian: invalid DIM argument."); endif m = median (x, dim, 'omitnan'); endfunction %!demo %! ## Find the column medians for a matrix with missing values. %! %! x = magic (3); %! x([1, 6:9]) = NaN %! m = nanmedian (x) %!demo %! ## Find the median of all elements, ignoring missing values. %! %! x = reshape (1:30, [2, 5, 3]); %! x([10:12, 25]) = NaN %! m = nanmedian (x, 'all') ## Test output %!assert_equal (nanmedian ([]), NaN) %!assert_equal (nanmedian (zeros (0, 3)), [NaN, NaN, NaN]) %!assert_equal (nanmedian (zeros (3, 0)), zeros (1, 0)) %!assert_equal (nanmedian ([], 1), zeros (1, 0)) %!assert_equal (nanmedian ([], 2), zeros (0, 1)) %!assert_equal (size (nanmedian (ones (2, 0, 3, 2), 2)), [2, 1, 3, 2]) %!assert_equal (nanmedian (ones (2, 0, 3, 2), 'all'), NaN) %!assert_equal (size (nanmedian (ones (2, 0, 3, 2), [1, 2])), [1, 1, 3, 2]) %!assert_equal (nanmedian (NaN), NaN) %!assert_equal (nanmedian (5), 5) %!assert_equal (nanmedian ([2, 4, NaN, 8]), 4) %!assert_equal (nanmedian ([1 2 NaN; 4 NaN 6; 7 8 9; 10 11 12]), [5.5, 8, 9]) %!assert_equal (nanmedian ([1 2 NaN; 4 NaN 6; 7 8 9; 10 11 12], 2), ... %! [1.5; 5; 8; 11]) %!assert_equal (nanmedian ([1 NaN; NaN NaN; 3 NaN]), [2, NaN]) %!assert_equal (nanmedian (reshape (1:12, [2, 3, 2])(:)), 6.5) %!assert_equal (nanmedian (single ([1 2 NaN 4])), single (2)) %!assert_equal (nanmedian (int8 ([2 5; 3 9])), int8 ([3, 7])) %!assert_equal (nanmedian (uint8 ([200 250])), uint8 (225)) %!assert_equal (nanmedian (intmax ('int64') - int64 ([1 3])), ... %! intmax ('int64') - int64 (2)) %!assert_equal (nanmedian ([true false]), true) %!assert_equal (nanmedian (int8 ([])), int8 (0)) %!assert_equal (nanmedian (sparse ([1 NaN 3])), sparse (2)) %!assert_equal (nanmedian (int8 ([1 2; 3 4]), 'all'), int8 (3)) %!assert_equal (nanmedian (single ([1 NaN; 3 4]), [1 2]), single (3)) %!test %! x = reshape (1:24, [2, 4, 3]); %! x([5:6, 20]) = NaN; %! assert_equal (nanmedian (x, 'all'), nanmedian (x(! isnan (x))(:))); %! assert_equal (nanmedian (x, [1, 2, 3]), nanmedian (x, 'all')); %!test %! x = magic (4); %! x([1, 6, 11, 16]) = NaN; %! assert_equal (nanmedian (x, 2), [3; 8; 9; 14]); ## Test input validation %!error nanmedian () %!error nanmedian ({3}) %!error nanmedian (ones (3), 0) %!error nanmedian (ones (3), 1.5) %!error nanmedian (ones (3), true) %!error nanmedian (ones (3), Inf) %!error ... %! nanmedian (ones (3), [1, Inf]) %!error ... %! nanmedian (ones (3, 3, 3), [2, 2, 3]) statistics-release-2.1.2/inst/Descriptive_Statistics/nanmin.m000066400000000000000000000267261527311104300244440ustar00rootroot00000000000000## Copyright (C) 2001 Paul Kienzle ## Copyright (C) 2003 Alois Schloegl ## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{v} =} nanmin (@var{x}) ## @deftypefnx {statistics} {@var{v} =} nanmin (@var{x}, [], @var{dim}) ## @deftypefnx {statistics} {[@var{v}, @var{idx}] =} nanmin (@dots{}) ## @deftypefnx {statistics} {@var{v} =} nanmin (@var{x}, [], @qcode{'all'}) ## @deftypefnx {statistics} {@var{v} =} nanmin (@var{x}, [], @var{vecdim}) ## @deftypefnx {statistics} {@var{v} =} nanmin (@var{x}, @var{y}) ## ## Find the minimum while ignoring NaN values. ## ## @code{@var{v} = nanmin (@var{x})} returns the minimum of @var{x}, after ## removing @qcode{NaN} values. If @var{x} is a vector, a scalar minimum value ## is returned. If @var{x} is a matrix, a row vector of column minima is ## returned. If @var{x} is a multidimensional array, the @code{nanmin} operates ## along the first nonsingleton dimension. If all values in a column are ## @qcode{NaN}, the minimum is returned as @qcode{NaN} rather than @qcode{[]}. ## ## @code{@var{v} = nanmin (@var{x}, [], @var{dim})} operates along the dimension ## @var{dim} of @var{x}. A logical @var{dim} or @var{vecdim} is an error, ## although MATLAB accepts @code{true} as dimension 1. ## ## @code{[@var{v}, @var{idx}] = nanmin (@dots{})} also returns the row indices ## of the minimum values for each column in the vector @var{idx}. When @var{x} ## is a vector, then @var{idx} is a scalar value as @var{v}. ## ## @code{@var{v} = nanmin (@var{x}, [], @qcode{'all'})} returns the minimum of ## all elements of @var{x}, after removing @qcode{NaN} values. It is the ## equivalent of @code{nanmin (@var{x}(:))}. The optional flag @qcode{'all'} ## cannot be used together with @var{dim} or @var{vecdim} input arguments. ## ## @code{@var{v} = nanmin (@var{x}, [], @var{vecdim})} returns the minimum over ## the dimensions specified in the vector @var{vecdim}. Each element of ## @var{vecdim} represents a dimension of the input array @var{x} and the output ## @var{v} has length 1 in the specified operating dimensions. The lengths of ## the other dimensions are the same for @var{x} and @var{y}. For example, if ## @var{x} is a 2-by-3-by-4 array, then @code{nanmin (@var{x}, [1 2])} returns a ## 1-by-1-by-4 array. Each element of the output array is the minimum of the ## elements on the corresponding page of @var{x}. If @var{vecdim} indexes all ## dimensions of @var{x}, then it is equivalent to ## @code{nanmin (@var{x}, @qcode{'all'})}. Any dimension in @var{vecdim} ## greater than @code{ndims (@var{x})} is ignored. ## ## @seealso{min, nanmax, nansum} ## @end deftypefn function [v, idx] = nanmin (x, y, dim) if (nargin < 1 || nargin > 3) print_usage; elseif (nargin == 1 || (nargin == 2 && isempty (y))) nanvals = isnan (x); x(nanvals) = Inf; [v, idx] = min (x); if (any (nanvals(:))) v(all (nanvals)) = NaN; endif elseif (nargin == 3 && strcmpi (dim, 'all') && isempty (y)) x = x(:); nanvals = isnan (x); x(nanvals) = Inf; [v, idx] = min (x); if (any (nanvals(:))) v(all (nanvals)) = NaN; endif elseif (nargin == 3 && isempty (y)) if (isscalar (dim)) if (! isnumeric (dim) || ! isfinite (dim) || fix (dim) != dim || dim <= 0) error ("nanmin: DIM must be a positive integer."); endif nanvals = isnan (x); x(nanvals) = Inf; [v, idx] = min (x, [], dim); if (any (nanvals(:))) v(all (nanvals, dim)) = NaN; endif else if (! (isnumeric (dim) && isvector (dim))) error ("nanmin: invalid DIM argument."); elseif (any (! isfinite (dim)) || any (fix (dim) != dim) || any (dim <= 0)) error ("nanmin: VECDIM must be a vector of positive integers."); endif vecdim = sort (dim); if (! all (diff (vecdim))) error ("nanmin: VECDIM must contain non-repeating positive integers."); endif ## Ignore dimensions in VECDIM larger than actual array vecdim(find (vecdim > ndims (x))) = []; if (isempty (vecdim)) v = x; if (nargout > 1) idx = reshape ([1:numel(x)], size (x)); endif else ## Calculate permutation vector szx = size (x); remdims = 1:ndims (x); # All dimensions remdims(vecdim) = []; # Delete dimensions specified by vecdim nremd = numel (remdims); ## If all dimensions are given, it is equivalent to 'all' flag if (nremd == 0) x = x(:); nanvals = isnan (x); x(nanvals) = Inf; [v, idx] = min (x); if (any (nanvals(:))) v(all (nanvals)) = NaN; endif else ## Permute to push vecdims to back perm = [remdims, vecdim]; x = permute (x, perm); ## Reshape to squash all vecdims in final dimension sznew = [szx(remdims), prod(szx(vecdim))]; x = reshape (x, sznew); ## Calculate nanmin on final dimension dim = nremd + 1; nanvals = isnan (x); x(nanvals) = Inf; [v, idx] = min (x, [], dim); if (any (nanvals(:))) v(all (nanvals, dim)) = NaN; endif ## Inverse permute back to correct dimensions v = ipermute (v, perm); idx = ipermute (idx, perm); endif endif endif else if (nargout > 1) error ("nanmin: a second output is not supported with this syntax."); endif Xnan = isnan (x); Ynan = isnan (y); x(Xnan) = Inf; y(Ynan) = Inf; v = min (x, y); if (any (Xnan(:)) && any (Ynan(:))) v(Xnan & Ynan) = NaN; endif endif endfunction %!demo %! ## Find the column minimum values and their indices %! ## for matrix data with missing values. %! %! x = magic (3); %! x([1, 6:9]) = NaN %! [y, ind] = nanmin (x) %!demo %! ## Find the minimum of all the values in an array, ignoring missing values. %! ## Create a 2-by-5-by-3 array x with some missing values. %! %! x = reshape (1:30, [2, 5, 3]); %! x([10:12, 25]) = NaN %! %! ## Find the minimum of the elements of x. %! %! y = nanmin (x, [], 'all') ## Test output %!assert_equal (nanmin ([]), []) %!assert_equal (nanmin (zeros (0, 3)), zeros (0, 3)) %!assert_equal (nanmin (zeros (3, 0)), zeros (1, 0)) %!assert_equal (size (nanmin (ones (2, 0, 3, 2))), [1, 0, 3, 2]) %!assert_equal (nanmin ([], [], 1), []) %!assert_equal (nanmin ([], [], 2), []) %!assert_equal (size (nanmin (ones (2, 0, 3, 2), [], 1)), [1, 0, 3, 2]) %!assert_equal (size (nanmin (ones (2, 0, 3, 2), [], 2)), [2, 0, 3, 2]) %!assert_equal (size (nanmin (ones (2, 0, 3, 2), [], 'all')), [0, 1]) %!assert_equal (size (nanmin (ones (2, 0, 3, 2), [], [1, 2])), [0, 1, 3, 2]) %!assert_equal (nanmin ([NaN, NaN, NaN]), NaN) %!assert_equal (nanmin ([2, 4, NaN, 7]), 2) %!assert_equal (nanmin ([2, 4, NaN, -Inf]), -Inf) %!assert_equal (nanmin ([1, NaN, 3; NaN, 5, 6; 7, 8, NaN]), [1, 5, 3]) %!assert_equal (nanmin ([1, NaN, 3; NaN, 5, 6; 7, 8, NaN]'), [1, 5, 7]) %!assert_equal (nanmin (single ([1, NaN, 3; NaN, 5, 6; 7, 8, NaN])), single ([1, 5, 3])) %!shared x, y %! x(:,:,1) = [1.77, -0.005, NaN, -2.95; NaN, 0.34, NaN, 0.19]; %! x(:,:,2) = [1.77, -0.005, NaN, -2.95; NaN, 0.34, NaN, 0.19] + 5; %! y = x; %! y(2,3,1) = 0.51; %!assert_equal (nanmin (x, [], [1, 2])(:), [-2.95; 2.05]) %!assert_equal (nanmin (x, [], [1, 3])(:), [1.77; -0.005; NaN; -2.95]) %!assert_equal (nanmin (x, [], [2, 3])(:), [-2.95; 0.19]) %!assert_equal (nanmin (x, [], [1, 2, 3]), -2.95) %!assert_equal (nanmin (x, [], 'all'), -2.95) %!assert_equal (nanmin (y, [], [1, 3])(:), [1.77; -0.005; 0.51; -2.95]) %!assert_equal (nanmin (x(1,:,1), x(2,:,1)), [1.77, -0.005, NaN, -2.95]) %!assert_equal (nanmin (x(1,:,2), x(2,:,2)), [6.77, 4.995, NaN, 2.05]) %!assert_equal (nanmin (y(1,:,1), y(2,:,1)), [1.77, -0.005, 0.51, -2.95]) %!assert_equal (nanmin (y(1,:,2), y(2,:,2)), [6.77, 4.995, NaN, 2.05]) ## Test dimension indexing with vecdim in N-dimensional arrays %!test %! xx = repmat ([1:20;6:25], [5 2 6 3]); %! assert_equal (size (nanmin (xx, [], [3, 2])), [10, 1, 1, 3]); %! assert_equal (size (nanmin (xx, [], [1, 2])), [1, 1, 6, 3]); %! assert_equal (size (nanmin (xx, [], [1, 2, 4])), [1, 1, 6]); %! assert_equal (size (nanmin (xx, [], [1, 4, 3])), [1, 40]); %! assert_equal (size (nanmin (xx, [], [1, 2, 3, 4])), [1, 1]); ## Test exceeding dimensions %!assert_equal (nanmin (ones (2), [], 3), ones (2, 2)) %!assert_equal (nanmin (ones (2, 2, 2), [], 99), ones (2, 2, 2)) %!assert_equal (nanmin (magic (3), [], 3), magic (3)) %!assert_equal (nanmin (magic (3), [], [1, 3]), [3, 1, 2]) %!assert_equal (nanmin (magic (3), [], [1, 99]), [3, 1, 2]) ## Test comparisons %!assert_equal (nanmin (ones (2), 3), ones (2,2)) ## Test logical input %!assert_equal (nanmin (logical ([1, 0, 1])), logical (0)) %!assert_equal (nanmin (logical ([0, 1; 1, 0])), logical ([0, 0])) %!assert_equal (nanmin (logical ([0, 1; 1, 0]), [], 2), logical ([0; 0])) %!assert_equal (nanmin (logical ([0, 1; 1, 0]), [], 'all'), logical (0)) %!assert_equal (nanmin (logical ([0, 1; 1, 0]), [], [1, 2]), false) %!assert_equal (nanmin (true (2, 2, 2), [], 3), true (2, 2)) %!assert_equal (nanmin (logical (zeros (0, 3))), logical (zeros (0, 3))) %!assert_equal (nthargout (2, @nanmin, logical ([1, 0, 1])), 2) %!assert_equal (nthargout (2, @nanmin, logical ([0, 1; 1, 0]), [], 2), [1; 2]) %!assert_equal (nanmin (logical ([0, 1; 1, 0]), false), false (2)) %!assert_equal (nanmin (logical ([1, 1]), NaN), [1, 1]) %!assert_equal (nanmin ([NaN, 1], logical ([1, 0])), [1, 0]) ## Test input validation %!error nanmin (ones (3), [], 0) %!error nanmin (ones (3), [], 1.5) %!error nanmin (ones (3), [], Inf) %!error ... %! nanmin (ones (3), [], [1, Inf]) %!error nanmin (ones (3), [], true) %!error ... %! nanmin (ones (3, 3, 3), [], [2, 2.5]) %!error ... %! nanmin (ones (3, 3, 3), [], [-1, 2]) %!error ... %! nanmin (y, [], [1, 1, 2]) %!error ... %! nanmin (true (2), [], [1, 1]) %!error nanmin (ones (3), [], "invalid") %!error nanmin (ones (3), [], ones (2)) %!error nanmin (ones (3), [], []) %!error ... %! nanmin (ones (3), [], [true, false]) %!error ... %! [v, idx] = nanmin (x, y, [1 2]) statistics-release-2.1.2/inst/Descriptive_Statistics/nanstd.m000066400000000000000000000111711527311104300244370ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{s} =} nanstd (@var{x}) ## @deftypefnx {statistics} {@var{s} =} nanstd (@var{x}, @var{w}) ## @deftypefnx {statistics} {@var{s} =} nanstd (@var{x}, @var{w}, @qcode{'all'}) ## @deftypefnx {statistics} {@var{s} =} nanstd (@var{x}, @var{w}, @var{dim}) ## @deftypefnx {statistics} {@var{s} =} nanstd (@var{x}, @var{w}, @var{vecdim}) ## ## Compute the standard deviation while ignoring NaN values. ## ## @code{@var{s} = nanstd (@var{x})} returns the standard deviation of @var{x}, ## after removing @qcode{NaN} values. If @var{x} is a vector, a scalar value is ## returned. If @var{x} is a matrix, a row vector of column standard deviations ## is returned. If @var{x} is a multidimensional array, @code{nanstd} operates ## along the first nonsingleton dimension. If a dimension contains fewer than ## two non-@qcode{NaN} values, the standard deviation is returned as 0 for a ## single value and as @qcode{NaN} when all values are @qcode{NaN}. @var{x} ## must be floating point, as for @code{std}, although MATLAB accepts logical ## too. ## ## @code{@var{s} = nanstd (@var{x}, @var{w})} specifies the normalization. When ## @var{w} is 0 (default), the standard deviation is normalized by @math{N-1}, ## where @math{N} is the number of non-@qcode{NaN} observations. When @var{w} ## is 1, it is normalized by @math{N}. @var{w} may also be a vector of ## nonnegative weights whose length matches the operating dimension, in which ## case the weighted standard deviation normalized by the sum of the weights is ## returned. ## ## @code{@var{s} = nanstd (@var{x}, @var{w}, @qcode{'all'})} returns the ## standard deviation of all elements of @var{x}, after removing @qcode{NaN} ## values. Use an empty value, @code{@var{w} = []}, to pass the default ## normalization. ## ## @code{@var{s} = nanstd (@var{x}, @var{w}, @var{dim})} operates along the ## dimension @var{dim} of @var{x}. A logical @var{dim} or @var{vecdim} is an ## error, although MATLAB accepts @code{true} as dimension 1. ## ## @code{@var{s} = nanstd (@var{x}, @var{w}, @var{vecdim})} returns the standard ## deviation over the dimensions specified in the vector @var{vecdim}. A weight ## vector is not supported together with @qcode{'all'} or @var{vecdim}. Any ## dimension in @var{vecdim} greater than @code{ndims (@var{x})} is ignored. ## ## @seealso{std, nanvar, nanmean, nansum} ## @end deftypefn function s = nanstd (x, varargin) if (nargin < 1 || nargin > 3) print_usage (); elseif (! isfloat (x)) error ("nanstd: X must be floating point."); endif s = sqrt (nanvar (x, varargin{:})); endfunction %!demo %! ## Find the column standard deviations for a matrix with missing values. %! %! x = magic (3); %! x([1, 6:9]) = NaN %! s = nanstd (x) %!demo %! ## Find the row standard deviations, normalized by N instead of N-1. %! %! x = magic (3); %! x([1, 6:9]) = NaN %! s = nanstd (x, 1, 2) ## Test output %!assert_equal (nanstd ([]), NaN) %!assert_equal (nanstd (NaN), NaN) %!assert_equal (nanstd (5), 0) %!assert_equal (nanstd ([1 2 NaN; 4 NaN 6; 7 8 9; 10 11 12]), ... %! sqrt ([15, 21, 9]), 1e-14) %!assert_equal (nanstd ([1 2 NaN; 4 NaN 6; 7 8 9; 10 11 12], 1), ... %! sqrt ([11.25, 14, 6]), 1e-14) %!assert_equal (nanstd ([1 2 NaN; 4 NaN 6; 7 8 9; 10 11 12], 0, 2), ... %! sqrt ([0.5; 2; 1; 1]), 1e-14) %!assert_equal (nanstd (NaN (2, 3)), [NaN, NaN, NaN]) %!assert_equal (nanstd (zeros (0, 3)), [NaN, NaN, NaN]) %!assert_equal (nanstd (zeros (3, 0)), zeros (1, 0)) %!assert_equal (nanstd ([], 1), NaN) %!assert_equal (nanstd ([], [], 1), zeros (1, 0)) %!assert_equal (size (nanstd (ones (2, 0, 3, 2), 0, 2)), [2, 1, 3, 2]) ## Test input validation %!error nanstd () %!error nanstd ({3}) %!error nanstd (int8 ([1 2; 3 5])) %!error nanstd (true (2)) statistics-release-2.1.2/inst/Descriptive_Statistics/nansum.m000066400000000000000000000201501527311104300244460ustar00rootroot00000000000000## Copyright (C) 2025 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{s} =} nansum (@var{x}) ## @deftypefnx {statistics} {@var{s} =} nansum (@var{x}, @qcode{'all'}) ## @deftypefnx {statistics} {@var{s} =} nansum (@var{x}, @var{dim}) ## @deftypefnx {statistics} {@var{s} =} nansum (@var{x}, @var{vecdim}) ## ## Compute the sum while ignoring NaN values. ## ## @code{@var{s} = nansum (@var{x})} returns the sum of @var{x}, after removing ## @qcode{NaN} values. If @var{x} is a vector, a scalar value is returned. If ## @var{x} is a matrix, a row vector of column sums is returned. If @var{x} is ## a multidimensional array, the @code{nansum} operates along the first ## nonsingleton dimension. If all values along a dimension are @qcode{NaN}, the ## sum is returned returned as 0. ## ## @code{@var{s} = nansum (@var{x}, @qcode{'all'})} returns the sum of all ## elements of @var{x}, after removing @qcode{NaN} values. It is the equivalent ## of @code{nansum (@var{x}(:))}. ## ## @code{@var{s} = nansum (@var{x}, @var{dim})} operates along the dimension ## @var{dim} of @var{x}. A logical @var{dim} or @var{vecdim} is an error, ## although MATLAB accepts @code{true} as dimension 1. ## ## @code{@var{s} = nansum (@var{x}, @var{vecdim})} returns the sum over the ## dimensions specified in the vector @var{vecdim}. Each element of ## @var{vecdim} represents a dimension of the input array @var{x} and the output ## @var{s} has length 1 in the specified operating dimensions. The lengths of ## the other dimensions are the same for @var{x} and @var{y}. For example, if ## @var{x} is a 2-by-3-by-4 array, then @code{nanmax (@var{x}, [1 2])} returns a ## 1-by-1-by-4 array. Each element of the output array is the maximum of the ## elements on the corresponding page of @var{x}. If @var{vecdim} indexes all ## dimensions of @var{x}, then it is equivalent to ## @code{nanmax (@var{x}, @qcode{'all'})}. Any dimension in @var{vecdim} ## greater than @code{ndims (@var{x})} is ignored. ## ## @seealso{sum, nanmin, nanmax} ## @end deftypefn function s = nansum (x, dim) if (nargin < 1 || nargin > 2) print_usage (); elseif (! (isnumeric (x) || islogical (x) || ischar (x))) error ("nansum: X must be numeric, logical or char."); ## 0 by 0 and no DIM given elseif (nargin < 2 && isequal (size (x), [0, 0])) s = 0; elseif (nargin == 1) nanvals = isnan (x); x(nanvals) = 0; s = sum (x); s(all (nanvals)) = 0; elseif (nargin == 2 && strcmpi (dim, 'all')) x = x(:); nanvals = isnan (x); x(nanvals) = 0; s = sum (x); s(all (nanvals)) = 0; else # DIM must be a numeric scalar or vector if (isscalar (dim)) if (! isnumeric (dim) || ! isfinite (dim) || fix (dim) != dim || dim <= 0) error ("nansum: DIM must be a positive integer."); endif nanvals = isnan (x); x(nanvals) = 0; s = sum (x, dim); s(all (nanvals, dim)) = 0; else if (! (isnumeric (dim) && isvector (dim))) error ("nansum: invalid DIM argument."); elseif (any (! isfinite (dim)) || any (fix (dim) != dim) || any (dim <= 0)) error ("nansum: VECDIM must be a vector of positive integers."); endif vecdim = sort (dim); if (! all (diff (vecdim))) error ("nansum: VECDIM must contain non-repeating positive integers."); endif ## Ignore dimensions in VECDIM larger than actual array vecdim(find (vecdim > ndims (x))) = []; if (isempty (vecdim)) s = x; else ## Calculate permutation vector szx = size (x); remdims = 1:ndims (x); # All dimensions remdims(vecdim) = []; # Delete dimensions specified by vecdim nremd = numel (remdims); ## If all dimensions are given, it is equivalent to 'all' flag if (nremd == 0) x = x(:); nanvals = isnan (x); x(nanvals) = 0; s = sum (x); s(all (nanvals)) = 0; else ## Permute to push vecdims to back perm = [remdims, vecdim]; x = permute (x, perm); ## Reshape to squash all vecdims in final dimension sznew = [szx(remdims), prod(szx(vecdim))]; x = reshape (x, sznew); ## Calculate nansum on final dimension dim = nremd + 1; nanvals = isnan (x); x(nanvals) = 0; s = sum (x, dim); s(all (nanvals, dim)) = 0; ## Inverse permute back to correct dimensions s = ipermute (s, perm); endif endif endif endif endfunction %!demo %! ## Find the column sums for a matrix with missing values., %! %! x = magic (3); %! x([1, 4, 7:9]) = NaN %! s = nansum (x) %!demo %! ## Find the row sums for a matrix with missing values., %! %! x = magic (3); %! x([1, 4, 7:9]) = NaN %! s = nansum (x, 2) %!demo %! ## Find the sum of all the values in a multidimensional array %! ## with missing values. %! %! x = reshape (1:30, [2, 5, 3]); %! x([10:12, 25]) = NaN %! s = nansum (x, 'all') %!demo %! ## Find the sum of a multidimensional array with missing values over %! ## multiple dimensions. %! %! x = reshape (1:30, [2, 5, 3]); %! x([10:12, 25]) = NaN %! s = nansum (x, [2, 3]) ## Test output %!assert_equal (nansum ([]), 0) %!assert_equal (nansum (zeros (0, 3)), [0, 0, 0]) %!assert_equal (nansum (zeros (3, 0)), zeros (1, 0)) %!assert_equal (nansum ([], 1), zeros (1, 0)) %!assert_equal (nansum ([], 2), zeros (0, 1)) %!assert_equal (size (nansum (ones (2, 0, 3, 2), 2)), [2, 1, 3, 2]) %!assert_equal (nansum (NaN), 0) %!assert_equal (nansum (NaN (3)), [0, 0, 0]) %!assert_equal (nansum ([2 4 NaN 7]), 13) %!assert_equal (nansum ([2 4 NaN Inf]), Inf) %!assert_equal (nansum ([1 NaN 3; NaN 5 6; 7 8 NaN]), [8 13 9]) %!assert_equal (nansum ([1 NaN 3; NaN 5 6; 7 8 NaN], 2), [4; 11; 15]) %!assert_equal (nansum (uint8 ([2 4 1 7])), 14) %!test %! x = magic (3); %! x([1 6:9]) = NaN; %! assert_equal (nansum (x), [7, 6, 0]) %! assert_equal (nansum (x, 2), [1; 8; 4]) %!test %! x = reshape (1:24, [2, 4, 3]); %! x([5:6, 20]) = NaN; %! assert_equal (nansum (x, 'all'), 269) %!test %! x = reshape (1:24,[2, 4, 3]); %! x([5:6, 20]) = NaN; %! assert_equal (squeeze (nansum (x, [1, 2])), [25; 100; 144]) %! assert_equal (nansum (x, [2, 3]), [139; 130]) ## Test logical input %!assert_equal (nansum (logical ([1, 0, 1; 0, 0, 1])), [1, 0, 2]) %!assert_equal (nansum ('abd'), 295) %!assert_equal (nansum ('abd', 1), [97, 98, 100]) %!assert_equal (nansum (''), 0) ## Test input validation %!error nansum ({3}) %!error nansum (ones (3), 0) %!error nansum (ones (3), true) %!error nansum (ones (3), 1.5) %!error nansum (ones (3), Inf) %!error ... %! nansum (ones (3), [1, Inf]) %!error ... %! nansum (ones (3, 3, 3), [2, 2.5]) %!error ... %! nansum (ones (3, 3, 3), [-1, 2]) %!error ... %! nansum (ones (3, 3, 3), [2, 2, 3]) %!error nansum (ones (3), "invalid") %!error nansum (ones (3), ones (2)) %!error nansum (ones (3), []) statistics-release-2.1.2/inst/Descriptive_Statistics/nanvar.m000066400000000000000000000177651527311104300244540ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{v} =} nanvar (@var{x}) ## @deftypefnx {statistics} {@var{v} =} nanvar (@var{x}, @var{w}) ## @deftypefnx {statistics} {@var{v} =} nanvar (@var{x}, @var{w}, @qcode{'all'}) ## @deftypefnx {statistics} {@var{v} =} nanvar (@var{x}, @var{w}, @var{dim}) ## @deftypefnx {statistics} {@var{v} =} nanvar (@var{x}, @var{w}, @var{vecdim}) ## ## Compute the variance while ignoring NaN values. ## ## @code{@var{v} = nanvar (@var{x})} returns the variance of @var{x}, after ## removing @qcode{NaN} values. If @var{x} is a vector, a scalar value is ## returned. If @var{x} is a matrix, a row vector of column variances is ## returned. If @var{x} is a multidimensional array, @code{nanvar} operates ## along the first nonsingleton dimension. If a dimension contains fewer than ## two non-@qcode{NaN} values, the variance is returned as 0 for a single value ## and as @qcode{NaN} when all values are @qcode{NaN}. @var{x} must be floating ## point, as for @code{var}, although MATLAB accepts logical too. ## ## @code{@var{v} = nanvar (@var{x}, @var{w})} specifies the normalization. When ## @var{w} is 0 (default), the variance is normalized by @math{N-1}, where ## @math{N} is the number of non-@qcode{NaN} observations. When @var{w} is 1, ## it is normalized by @math{N}. @var{w} may also be a vector of nonnegative ## weights whose length matches the operating dimension, in which case the ## weighted variance normalized by the sum of the weights is returned. ## ## @code{@var{v} = nanvar (@var{x}, @var{w}, @qcode{'all'})} returns the ## variance of all elements of @var{x}, after removing @qcode{NaN} values. Use ## an empty value, @code{@var{w} = []}, to pass the default normalization. ## ## @code{@var{v} = nanvar (@var{x}, @var{w}, @var{dim})} operates along the ## dimension @var{dim} of @var{x}. A logical @var{dim} or @var{vecdim} is an ## error, although MATLAB accepts @code{true} as dimension 1. ## ## @code{@var{v} = nanvar (@var{x}, @var{w}, @var{vecdim})} returns the variance ## over the dimensions specified in the vector @var{vecdim}. A weight vector is ## not supported together with @qcode{'all'} or @var{vecdim}. Any dimension in ## @var{vecdim} greater than @code{ndims (@var{x})} is ignored. ## ## @seealso{var, nanstd, nanmean, nansum} ## @end deftypefn function v = nanvar (x, w, dim) if (nargin < 1 || nargin > 3) print_usage (); endif if (! isfloat (x)) error ("nanvar: X must be floating point."); endif if (nargin < 3 && isequal (size (x), [0, 0])) v = NaN; return; endif ## Normalization / weight argument if (nargin < 2 || isempty (w)) w = 0; endif if (! isnumeric (w) || ! isreal (w)) error ("nanvar: W must be 0, 1, or a vector of nonnegative weights."); endif wvec = false; if (isscalar (w)) if (w != 0 && w != 1) error ("nanvar: W must be 0 or 1 when it is a scalar."); endif elseif (isvector (w)) if (any (w < 0) || any (isnan (w))) error ("nanvar: weight vector W must contain nonnegative values."); endif wvec = true; else error ("nanvar: W must be 0, 1, or a vector of nonnegative weights."); endif ## Operating dimension dimall = false; if (nargin < 3) dim = find (size (x) != 1, 1); if (isempty (dim)) dim = 1; endif elseif (ischar (dim) && strcmpi (dim, 'all')) dimall = true; elseif (isscalar (dim)) if (! isnumeric (dim) || ! isfinite (dim) || fix (dim) != dim || dim <= 0) error ("nanvar: DIM must be a positive integer."); endif elseif (isnumeric (dim) && isvector (dim)) if (any (! isfinite (dim)) || any (fix (dim) != dim) || any (dim <= 0)) error ("nanvar: VECDIM must be a vector of positive integers."); endif dim = sort (dim); if (! all (diff (dim))) error ("nanvar: VECDIM must contain non-repeating positive integers."); endif else error ("nanvar: invalid DIM argument."); endif if (dimall) dimarg = 'all'; else dimarg = dim; endif if (wvec) ## Weighted variance (biased, normalized by the sum of the weights) if (dimall || ! isscalar (dim)) error ("nanvar: a weight vector is supported only with a scalar DIM."); endif if (numel (w) != size (x, dim)) error ("nanvar: the length of W must match the operating dimension."); endif wsz = ones (1, max (ndims (x), dim)); wsz(dim) = numel (w); wr = reshape (w(:), wsz); mask = ! isnan (x); we = wr .* mask; sumw = sum (we, dim); x0 = x; x0(! mask) = 0; mu = sum (we .* x0, dim) ./ sumw; t = (x0 - mu) .^ 2; v = sum (we .* t, dim) ./ sumw; else mask = ! isnan (x); n = sum (mask, dimarg); x0 = x; x0(! mask) = 0; xm = sum (x0, dimarg) ./ n; d2 = (x - xm) .^ 2; d2(isnan (d2)) = 0; ss = sum (d2, dimarg); if (w == 1) v = ss ./ n; else v = ss ./ (n - 1); endif v(n == 1) = 0; v(n == 0) = NaN; endif endfunction %!demo %! ## Find the column variances for a matrix with missing values. %! %! x = magic (3); %! x([1, 6:9]) = NaN %! v = nanvar (x) %!demo %! ## Find the row variances, normalized by N instead of N-1. %! %! x = magic (3); %! x([1, 6:9]) = NaN %! v = nanvar (x, 1, 2) ## Test output %!assert_equal (nanvar ([]), NaN) %!assert_equal (nanvar (NaN), NaN) %!assert_equal (nanvar (5), 0) %!assert_equal (nanvar ([2, 4, NaN, 8]), 9.333333333333334, 1e-14) %!assert_equal (nanvar ([1 2 NaN; 4 NaN 6; 7 8 9; 10 11 12]), [15, 21, 9]) %!assert_equal (nanvar ([1 2 NaN; 4 NaN 6; 7 8 9; 10 11 12], 1), [11.25, 14, 6]) %!assert_equal (nanvar ([1 2 NaN; 4 NaN 6; 7 8 9; 10 11 12], 0, 2), ... %! [0.5; 2; 1; 1]) %!assert_equal (nanvar ([1 2 NaN; 4 NaN 6; 7 8 9; 10 11 12], [1 2 3 4]'), ... %! [9, 8.4375, 50/9], 1e-13) %!assert_equal (nanvar (NaN (2, 3)), [NaN, NaN, NaN]) %!assert_equal (nanvar (zeros (0, 3)), [NaN, NaN, NaN]) %!assert_equal (nanvar (zeros (3, 0)), zeros (1, 0)) %!assert_equal (nanvar ([], 1), NaN) %!assert_equal (nanvar ([], [], 1), zeros (1, 0)) %!assert_equal (size (nanvar (ones (2, 0, 3, 2), 0, 2)), [2, 1, 3, 2]) %!test %! x = reshape (1:24, [2, 4, 3]); %! x([5:6, 20]) = NaN; %! assert_equal (nanvar (x, 0, 'all'), nanvar (x(! isnan (x))(:)), 1e-12) ## Test input validation %!error nanvar () %!error nanvar ({3}) %!error nanvar (int8 ([1 2; 3 5])) %!error nanvar (true (2)) %!error nanvar (ones (3), 2) %!error ... %! nanvar (ones (1, 3), [1, -1, 2]) %!error nanvar (ones (3), 0, 1.5) %!error nanvar (ones (3), 0, Inf) %!error ... %! nanvar (ones (3), 0, [1, Inf]) %!error nanvar (ones (3), 0, true) %!error ... %! nanvar (ones (3, 3, 3), 0, [2, 2, 3]) %!error ... %! nanvar (ones (2, 3), [1, 2], 'all') %!error ... %! nanvar (ones (2, 3), [1, 2, 3], 1) statistics-release-2.1.2/inst/Descriptive_Statistics/partialcorr.m000066400000000000000000000404411527311104300254740ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{rho} =} partialcorr (@var{x}) ## @deftypefnx {statistics} {@var{rho} =} partialcorr (@var{x}, @var{z}) ## @deftypefnx {statistics} {@var{rho} =} partialcorr (@var{x}, @var{y}, @var{z}) ## @deftypefnx {statistics} {[@var{rho}, @var{pval}] =} partialcorr (@dots{}) ## @deftypefnx {statistics} {[@dots{}] =} partialcorr (@dots{}, @var{Name}, @var{Value}) ## ## Linear or rank partial correlation coefficients. ## ## @code{@var{rho} = partialcorr (@var{x})} returns the sample linear partial ## correlation coefficients between pairs of variables in the ## @math{n}-by-@math{p} matrix @var{x}, controlling for the remaining columns of ## @var{x}. Each element @code{@var{rho}(i,j)} is the partial correlation ## between @code{@var{x}(:,i)} and @code{@var{x}(:,j)}, adjusted for the other ## @math{p-2} columns. @var{rho} is a symmetric @math{p}-by-@math{p} matrix ## with ones on the diagonal. ## ## @code{@var{rho} = partialcorr (@var{x}, @var{z})} controls instead for the ## variables in the @math{n}-by-@math{q} matrix @var{z}, returning the ## @math{p}-by-@math{p} partial correlations among the columns of @var{x}. ## ## @code{@var{rho} = partialcorr (@var{x}, @var{y}, @var{z})} returns the ## @math{p1}-by-@math{p2} matrix of partial correlations between the columns of ## the @math{n}-by-@math{p1} matrix @var{x} and the @math{n}-by-@math{p2} matrix ## @var{y}, controlling for @var{z}. Element @code{@var{rho}(i,j)} is the ## partial correlation between @code{@var{x}(:,i)} and @code{@var{y}(:,j)}. ## ## @code{[@var{rho}, @var{pval}] = partialcorr (@dots{})} also returns ## @var{pval}, a matrix of p-values for testing the hypothesis of no partial ## correlation against the alternative selected by @qcode{'Tail'}. ## ## A coefficient is @code{NaN} where the controlling variables explain either ## of the two variables completely, since the partial correlation is then ## undefined: no variation is left to correlate. This covers a controlling ## variable that duplicates one of them and any set of them that spans it. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @table @asis ## @item @qcode{'Type'} ## @qcode{'Pearson'} (default) for linear partial correlation, or ## @qcode{'Spearman'} for rank partial correlation (computed on the ranks of the ## data). @qcode{'Kendall'} is @emph{not} supported and raises an error, as in ## @sc{matlab}. ## ## @item @qcode{'Rows'} ## @qcode{'all'} (default) uses all rows regardless of missing values (any ## @code{NaN} yields a @code{NaN} result); @qcode{'complete'} uses only the rows ## with no missing values across all supplied variables; @qcode{'pairwise'} ## uses, for each computed coefficient, the rows with no missing values among ## just the variables involved in that coefficient. ## ## @item @qcode{'Tail'} ## The alternative hypothesis for @var{pval}: @qcode{'both'} (default, nonzero ## correlation), @qcode{'right'} (greater than zero), or @qcode{'left'} (less ## than zero). ## @end table ## ## The partial correlation is computed by regressing each of the two variables ## on the controlling variables (with an intercept) and correlating the ## residuals. The p-value uses a Student's @math{t} statistic with ## @math{n - 2 - k} degrees of freedom, where @math{k} is the number of ## controlling variables and @math{n} the number of observations used. ## ## The data are centered, and the controlling variables rescaled, before the ## regression, which leaves the result unchanged in exact arithmetic and keeps ## it accurate in floating point. Shifting or rescaling a controlling ## variable, or giving the two variables very different scales, does not ## change the coefficient. MATLAB's coefficient does change in those cases: ## a large shift gives a different value, a small-scaled controlling variable ## is dropped, and very different scales overflow to @code{NaN}. ## ## @seealso{partialcorri, corr, corrcoef, tiedrank} ## @end deftypefn function [rho, pval] = partialcorr (varargin) if (nargin < 1) print_usage (); endif ## Separate the leading numeric matrices from the Name/Value options. nmat = 0; while (nmat < numel (varargin) && isnumeric (varargin{nmat+1})) nmat += 1; endwhile if (nmat < 1 || nmat > 3) error ("partialcorr: invalid number of input matrices."); endif args = varargin(1:nmat); [Type, Rows, Tail, rem] = parsePairedArguments ({'Type', 'Rows', 'Tail'}, ... {'pearson', 'all', 'both'}, varargin(nmat+1:end)); if (! isempty (rem)) error ("partialcorr: unknown or unpaired optional argument."); endif if (! (ischar (Type) && ischar (Rows) && ischar (Tail))) error ("partialcorr: 'Type', 'Rows', and 'Tail' values must be strings."); endif Type = lower (Type); Rows = lower (Rows); Tail = lower (Tail); if (strcmp (Type, 'kendall')) error ("partialcorr: cannot compute Kendall's partial rank correlation."); elseif (! any (strcmp (Type, {'pearson', 'spearman'}))) error ("partialcorr: '%s' is not a valid 'Type'.", Type); endif if (! any (strcmp (Rows, {'all', 'complete', 'pairwise'}))) error ("partialcorr: '%s' is not a valid 'Rows' option.", Rows); endif if (! any (strcmp (Tail, {'both', 'right', 'left'}))) error ("partialcorr: '%s' is not a valid 'Tail' option.", Tail); endif ## Assign the roles of the input matrices. ## 1 matrix : pairs of columns of X, controlling for the remaining columns. ## 2 matrix : pairs of columns of X, controlling for Z. ## 3 matrix : columns of X vs columns of Y, controlling for Z. A = args{1}; useRemaining = (nmat == 1); square = (nmat <= 2); if (nmat == 3) B = args{2}; else B = A; endif if (nmat >= 2) Zc = args{end}; else Zc = []; endif check_matrix ("partialcorr", A, "X"); n = rows (A); if (nmat == 3) check_matrix ("partialcorr", B, "Y"); if (rows (B) != n) error ("partialcorr: X and Y must have the same number of rows."); endif endif if (! isempty (Zc)) check_matrix ("partialcorr", Zc, "Z"); if (rows (Zc) != n) error ("partialcorr: Z must have the same number of rows as X."); endif endif p1 = columns (A); p2 = columns (B); ## Rows with no missing value across every supplied variable. completerows = ! any (isnan ([A, B, Zc]), 2); rho = NaN (p1, p2); pval = NaN (p1, p2); for i = 1:p1 for j = 1:p2 if (square && j < i) continue; # filled below by symmetry endif a = A(:,i); b = B(:,j); if (useRemaining) C = A(:, setdiff (1:p1, [i, j])); else C = Zc; endif [r, nu] = resid_partial (a, b, C, Type, Rows, completerows); rho(i,j) = r; pval(i,j) = student_pval (r, nu - 2 - columns (C), Tail); if (square) rho(j,i) = rho(i,j); pval(j,i) = pval(i,j); endif endfor endfor endfunction ## Validate that a matrix input is real, numeric, and 2-D. function check_matrix (fname, x, label) if (! (isnumeric (x) && isreal (x) && ismatrix (x) && ndims (x) == 2)) error ("%s: %s must be a real numeric matrix.", fname, label); endif endfunction ## Partial correlation of a and b controlling for C, with NaN handling. function [r, nu] = resid_partial (a, b, C, Type, Rows, completerows) n = numel (a); switch (Rows) case 'all' idx = true (n, 1); case 'complete' idx = completerows; case 'pairwise' idx = ! any (isnan ([a, b, C]), 2); endswitch nu = sum (idx); ## With 'all', any missing value propagates to a NaN coefficient. if (strcmp (Rows, 'all') && any (isnan ([a, b, C])(:))) r = NaN; nu = n; return; endif a = a(idx); b = b(idx); C = C(idx,:); if (strcmp (Type, 'spearman')) a = tiedrank (a); b = tiedrank (b); for c = 1:columns (C) C(:,c) = tiedrank (C(:,c)); endfor endif ## Use single precision only when all the data for this regression are ## single. In particular, do not let concatenation silently demote a double ## response when another input is single. use_single = isa (a, 'single') && isa (b, 'single') && ... (isempty (C) || isa (C, 'single')); if (! use_single) a = double (a); b = double (b); C = double (C); endif m = numel (a); Y = [a, b]; ## Center before fitting, then equilibrate the control columns. Translation ## and nonzero scaling do not change the regression subspace, but doing both ## keeps the intercept and controls well conditioned. Retain the intercept ## to absorb rounding in the calculated means. Zero columns need no scale. Yc = Y - mean (Y, 1); Cc = C - mean (C, 1); cscale = max (abs (Cc), [], 1); cscale(cscale == 0) = 1; Cc = Cc ./ cscale; M = [ones(m, 1, class (Yc)), Cc]; ## Fit both responses with the same factorization. Recenter the residuals ## to enforce their theoretical orthogonality to the intercept despite the ## final matrix multiplication and subtraction rounding. R = Yc - M * (M \ Yc); R = R - mean (R, 1); ra = R(:,1); rb = R(:,2); sa = norm (ra); sb = norm (rb); ## A residual at or below the precision the column is held in counts as ## zero. Scale by the vector's magnitude, not one element of it, so a large ## constant offset raises the tolerance as it raises the rounding error. tol_a = m * eps (norm (a)); tol_b = m * eps (norm (b)); if (sa <= tol_a || sb <= tol_b) r = NaN; else ## Normalize before multiplying to avoid overflow or underflow when the ## two responses have very different magnitudes. r = sum ((ra / sa) .* (rb / sb), 'extra'); r = max (-1, min (1, r)); endif endfunction ## p-value from a Student's t statistic for a correlation r with DOF degrees ## of freedom, under the requested tail. function p = student_pval (r, dof, Tail) if (dof <= 0 || isnan (r)) p = NaN; return; endif t = r .* sqrt (dof ./ (1 - r .^ 2)); switch (Tail) case 'both' p = 2 * tcdf (-abs (t), dof); case 'right' p = tcdf (-t, dof); case 'left' p = tcdf (t, dof); endswitch endfunction %!demo %! ## Partial correlations among four variables, each pair adjusted for the %! ## other two. %! x = [0.42 1.30 -0.85 0.11; 1.15 -0.47 0.33 1.82; -0.98 0.55 1.21 -0.34; ... %! 0.63 2.10 -0.19 0.48; 1.88 -1.02 0.74 0.05; -0.31 0.86 -1.44 1.29; ... %! 0.77 0.14 0.58 -0.71; -1.52 1.77 0.02 0.94; 0.29 -0.63 1.36 0.37]; %! rho = partialcorr (x) ## shared test data %!shared D, X, Y, Z %! D = [ 0.42 1.30 -0.85 0.11 2.04 %! 1.15 -0.47 0.33 1.82 -0.62 %! -0.98 0.55 1.21 -0.34 0.77 %! 0.63 2.10 -0.19 0.48 -1.15 %! 1.88 -1.02 0.74 0.05 0.39 %! -0.31 0.86 -1.44 1.29 0.92 %! 0.77 0.14 0.58 -0.71 1.63 %! -1.52 1.77 0.02 0.94 -0.28 %! 0.29 -0.63 1.36 0.37 0.51 %! 2.01 0.48 -0.77 -1.08 0.14 %! -0.44 1.05 0.91 0.66 -0.83 %! 0.90 -0.29 -0.36 1.47 1.22]; %! X = D(:,1:2); %! Y = D(:,3); %! Z = D(:,4:5); ## single-matrix form: symmetric, unit diagonal, controls for remaining columns %!test %! rho = partialcorr (D); %! assert_equal (diag (rho), ones (5, 1), 1e-12); %! assert_equal (rho, rho', 1e-12); %! assert_equal (rho(1,2), -0.8399, 1e-4); %! assert_equal (rho(1,5), -0.6172, 1e-4); %! assert_equal (rho(3,4), -0.6763, 1e-4); ## single-matrix form p-values: zero on the diagonal, symmetric off-diagonal %!test %! [rho, p] = partialcorr (D); %! assert_equal (diag (p), zeros (5, 1), 1e-12); %! assert_equal (p, p', 1e-12); %! assert_equal (p(1,2), 0.0046, 1e-4); %! assert_equal (p(1,5), 0.0766, 1e-4); ## two-matrix form: pairs of X controlling for Z, with p-values %!test %! [rho, p] = partialcorr (X, Z); %! assert_equal (rho, [1 -0.5888; -0.5888 1], 1e-4); %! assert_equal (p, [0 0.0733; 0.0733 0], 1e-4); ## three-matrix form: X vs Y controlling for Z, with p-values %!test %! [r, p] = partialcorr (X, Y, Z); %! assert_equal (r, [-0.2073; -0.5273], 1e-4); %! assert_equal (p, [0.5656; 0.1173], 1e-4); ## Spearman rank partial correlation %!test %! r = partialcorr (X, Y, Z, 'Type', 'Spearman'); %! assert_equal (r, [-0.3079; -0.4984], 1e-4); ## one-sided p-values %!test %! [~, pr] = partialcorr (X, Y, Z, 'Tail', 'right'); %! [~, pl] = partialcorr (X, Y, Z, 'Tail', 'left'); %! assert_equal (pr, [0.7172; 0.9414], 1e-4); %! assert_equal (pl, [0.2828; 0.0586], 1e-4); ## NaN handling: 'all' propagates, 'complete'/'pairwise' delete rows %!test %! DN = D; %! DN(3,2) = NaN; %! assert_equal (all (isnan (partialcorr (DN)), 'all'), true); %! rc = partialcorr (DN, 'Rows', 'complete'); %! assert_equal (rc(1,2), -0.8311, 1e-4); %! assert_equal (rc(4,5), -0.6163, 1e-4); ## 'pairwise' differs from 'complete' when entries use different columns %!test %! XN = X; %! XN(3,1) = NaN; %! rc = partialcorr (XN, Y, Z, 'Rows', 'complete'); %! [rp, pp] = partialcorr (XN, Y, Z, 'Rows', 'pairwise'); %! assert_equal (rc, [-0.0116; -0.5849], 1e-4); %! assert_equal (rp, [-0.0116; -0.5273], 1e-4); %! assert_equal (pp, [0.9764; 0.1173], 1e-4); ## a controlling variable that perfectly explains a column of X collapses ## its residual variance, so rho and pval are NaN %!test %! X = [1 2; 3 4; 5 5]; %! Z = [2; 4; 6]; %! [rho, pval] = partialcorr (X, Z); %! assert_equal (isnan (rho(1,:)), [true, true]); %! assert_equal (isnan (rho(:,1)), [true; true]); %! assert_equal (rho(2,2), 1); %! assert_equal (all (isnan (pval(:))), true); ## the tolerance keeps a residual that is small but genuinely resolvable %!test %! Z = [1; 2; 3; 4; 5; 6]; %! X = [Z + 1e-12 * [1; -2; 1; 0; 0; 0], [3; 1; 4; 1; 5; 9]]; %! rho = partialcorr (X, Z); %! assert_equal (isnan (rho(1,2)), false); ## and discards one that is below the precision the data is held in %!test %! Z = [1; 2; 3; 4; 5; 6]; %! X = [Z + 1e-16 * [1; -2; 1; 0; 0; 0], [3; 1; 4; 1; 5; 9]]; %! rho = partialcorr (X, Z); %! assert_equal (isnan (rho(1,2)), true); ## the tolerance follows the data, so a constant offset large enough to cost ## the column its low-order bits collapses the residual with it %!test %! Z = [1; 2; 3; 4; 5; 6]; %! v = 1e-3 * [1; -2; 1; 0; 0; 0]; %! Y = [3; 1; 4; 1; 5; 9]; %! assert_equal (isnan (partialcorr ([Z + v, Y], Z)(1,2)), false); %! assert_equal (isnan (partialcorr ([1e12 + Z + v, Y], Z)(1,2)), true); ## shifting a control does not change the coefficient, where MATLAB's changes %!test %! t = linspace (-1, 1, 50)'; %! X = [sin(3*t) + 0.2*cos(11*t), cos(5*t) - 0.1*sin(13*t)]; %! r0 = partialcorr (X, [t, t.^2]); %! rt = partialcorr (X, [1e8 + t, t.^2]); %! assert_equal (rt(1,2), r0(1,2), 1e-8); ## rescaling a control does not change the coefficient, where MATLAB drops it %!test %! t = linspace (-1, 1, 50)'; %! X = [sin(3*t) + 0.2*cos(11*t), cos(5*t) - 0.1*sin(13*t)]; %! r0 = partialcorr (X, [t, t.^2]); %! rs = partialcorr (X, [t, 1e-16 * t.^2]); %! assert_equal (rs(1,2), r0(1,2), 1e-12); ## residual normalization avoids overflow and underflow across response ## scales, where MATLAB gives NaN %!test %! t = linspace (-1, 1, 50)'; %! X = [sin(3*t) + 0.2*cos(11*t), cos(5*t) - 0.1*sin(13*t)]; %! Z = [t, t.^2]; %! r0 = partialcorr (X, Z); %! rs = partialcorr ([1e200 * X(:,1), 1e-200 * X(:,2)], Z); %! assert_equal (rs(1,2), r0(1,2), 1e-12); ## input validation %!error ... %! partialcorr (ones (5), ones (5), ones (5), ones (5)) %!error ... %! partialcorr (ones (10, 3), 'Type', 'Kendall') %!error partialcorr (ones (10, 3), 'Type', 'foo') %!error partialcorr (ones (10, 3), 'Rows', 'foo') %!error ... %! partialcorr (ones (10, 2), ones (8, 1), ones (10, 2)) statistics-release-2.1.2/inst/Descriptive_Statistics/partialcorri.m000066400000000000000000000161031527311104300256430ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{rho} =} partialcorri (@var{y}, @var{x}) ## @deftypefnx {statistics} {@var{rho} =} partialcorri (@var{y}, @var{x}, @var{z}) ## @deftypefnx {statistics} {[@var{rho}, @var{pval}] =} partialcorri (@dots{}) ## @deftypefnx {statistics} {[@dots{}] =} partialcorri (@dots{}, @var{Name}, @var{Value}) ## ## Partial correlation of each response with each predictor, adjusting for the ## remaining predictors. ## ## @code{@var{rho} = partialcorri (@var{y}, @var{x})} returns the sample partial ## correlation coefficients between the columns of the @math{n}-by-@math{p} ## response matrix @var{y} and the columns of the @math{n}-by-@math{q} predictor ## matrix @var{x}. Element @code{@var{rho}(i,j)} is the partial correlation ## between @code{@var{y}(:,i)} and @code{@var{x}(:,j)}, adjusted for the other ## columns of @var{x} (that is, all columns of @var{x} except the @math{j}-th). ## @var{rho} is a @math{p}-by-@math{q} matrix. ## ## @code{@var{rho} = partialcorri (@var{y}, @var{x}, @var{z})} additionally ## controls for the variables in the @math{n}-by-@math{r} matrix @var{z}, so ## that @code{@var{rho}(i,j)} is adjusted for both the other columns of @var{x} ## and all columns of @var{z}. ## ## @code{[@var{rho}, @var{pval}] = partialcorri (@dots{})} also returns ## @var{pval}, a matrix of p-values for testing the hypothesis of no partial ## correlation against the alternative selected by @qcode{'Tail'}. ## ## The @qcode{'Type'}, @qcode{'Rows'}, and @qcode{'Tail'} @var{Name}/@var{Value} ## options are accepted with the same meaning as in @code{partialcorr}. ## @qcode{'Kendall'} is @emph{not} supported and raises an error, as in ## @sc{matlab}. ## ## @seealso{partialcorr, corr, corrcoef, tiedrank} ## @end deftypefn function [rho, pval] = partialcorri (varargin) if (nargin < 2) print_usage (); endif ## Separate the leading numeric matrices from the Name/Value options. nmat = 0; while (nmat < numel (varargin) && isnumeric (varargin{nmat+1})) nmat += 1; endwhile if (nmat < 2 || nmat > 3) error ("partialcorri: invalid number of input matrices."); endif Yv = varargin{1}; Xv = varargin{2}; if (nmat == 3) Zc = varargin{3}; else Zc = []; endif ## Parse and validate options (errors on 'Kendall' under this function's name). [Type, Rows, Tail, rem] = parsePairedArguments ({'Type', 'Rows', 'Tail'}, ... {'pearson', 'all', 'both'}, varargin(nmat+1:end)); if (! isempty (rem)) error ("partialcorri: unknown or unpaired optional argument."); endif if (! (ischar (Type) && ischar (Rows) && ischar (Tail))) error ("partialcorri: 'Type', 'Rows', and 'Tail' values must be strings."); endif Type = lower (Type); Rows = lower (Rows); Tail = lower (Tail); if (strcmp (Type, 'kendall')) error ("partialcorri: cannot compute Kendall's partial rank correlation."); elseif (! any (strcmp (Type, {'pearson', 'spearman'}))) error ("partialcorri: '%s' is not a valid 'Type'.", Type); endif if (! any (strcmp (Rows, {'all', 'complete', 'pairwise'}))) error ("partialcorri: '%s' is not a valid 'Rows' option.", Rows); endif if (! any (strcmp (Tail, {'both', 'right', 'left'}))) error ("partialcorri: '%s' is not a valid 'Tail' option.", Tail); endif if (! (isnumeric (Yv) && isreal (Yv) && ismatrix (Yv))) error ("partialcorri: Y must be a real numeric matrix."); endif if (! (isnumeric (Xv) && isreal (Xv) && ismatrix (Xv))) error ("partialcorri: X must be a real numeric matrix."); endif n = rows (Yv); if (rows (Xv) != n) error ("partialcorri: X and Y must have the same number of rows."); endif if (! isempty (Zc) && rows (Zc) != n) error ("partialcorri: Z must have the same number of rows as Y."); endif qx = columns (Xv); py = columns (Yv); rho = NaN (py, qx); pval = NaN (py, qx); ## Each predictor column in turn: adjust for the other predictors (and Z), then ## reuse partialcorr's cross form (Y vs the single predictor, controlling for ## the rest). This shares the ranking, NaN, and p-value machinery. for j = 1:qx C = [Xv(:, [1:j-1, j+1:qx]), Zc]; [rj, pj] = partialcorr (Yv, Xv(:,j), C, ... 'Type', Type, 'Rows', Rows, 'Tail', Tail); rho(:,j) = rj; pval(:,j) = pj; endfor endfunction %!demo %! ## Partial correlation of a response with each of two predictors, each %! ## adjusted for the other predictor. %! y = [-0.85; 0.33; 1.21; -0.19; 0.74; -1.44; 0.58; 0.02; 1.36]; %! x = [0.42 1.30; 1.15 -0.47; -0.98 0.55; 0.63 2.10; 1.88 -1.02; ... %! -0.31 0.86; 0.77 0.14; -1.52 1.77; 0.29 -0.63]; %! rho = partialcorri (y, x) ## shared test data %!shared D, X, Y, Z %! D = [ 0.42 1.30 -0.85 0.11 2.04 %! 1.15 -0.47 0.33 1.82 -0.62 %! -0.98 0.55 1.21 -0.34 0.77 %! 0.63 2.10 -0.19 0.48 -1.15 %! 1.88 -1.02 0.74 0.05 0.39 %! -0.31 0.86 -1.44 1.29 0.92 %! 0.77 0.14 0.58 -0.71 1.63 %! -1.52 1.77 0.02 0.94 -0.28 %! 0.29 -0.63 1.36 0.37 0.51 %! 2.01 0.48 -0.77 -1.08 0.14 %! -0.44 1.05 0.91 0.66 -0.83 %! 0.90 -0.29 -0.36 1.47 1.22]; %! X = D(:,1:2); %! Y = D(:,3); %! Z = D(:,4:5); ## single response, two predictors, controlling for Z, with p-values %!test %! [r, p] = partialcorri (Y, X, Z); %! assert_equal (r, [-0.7539, -0.8212], 1e-4); %! assert_equal (p, [0.0189, 0.0066], 1e-4); ## matches the corresponding entries of the symmetric partialcorr matrix %!test %! rho = partialcorr (D); %! assert_equal (partialcorri (Y, X, Z), rho(3,1:2), 1e-12); ## Spearman rank partial correlation %!test %! r = partialcorri (Y, X, Z, 'Type', 'Spearman'); %! assert_equal (r, [-0.8381, -0.8677], 1e-4); ## multiple responses, with and without an extra control set %!test %! r1 = partialcorri ([D(:,3), D(:,4)], X, D(:,5)); %! assert_equal (r1, [-0.5444, -0.6714; -0.3517, -0.2830], 1e-4); %! r2 = partialcorri ([D(:,3), D(:,4)], X); %! assert_equal (r2, [-0.4740, -0.5784; -0.3098, -0.1934], 1e-4); ## input validation %!error ... %! partialcorri (ones (5), ones (5), ones (5), ones (5)) %!error ... %! partialcorri (ones (10, 1), ones (10, 2), 'Type', 'Kendall') %!error ... %! partialcorri (ones (10, 1), ones (8, 2)) statistics-release-2.1.2/inst/Descriptive_Statistics/private/000077500000000000000000000000001527311104300244435ustar00rootroot00000000000000statistics-release-2.1.2/inst/Descriptive_Statistics/private/kernelcdf.m000066400000000000000000000040401527311104300265540ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{c} =} kernelcdf (@var{z}, @var{kernel}) ## ## Cumulative distribution function of the unit-variance smoothing kernel, ## evaluated at the standardized distances @var{z}. For a named kernel this is ## the canonical cdf evaluated at @math{s*z}; @var{kernel} may instead be a ## function handle, whose cumulative is computed numerically over a fine grid. ## Internal helper shared by @code{ksdensity} and @code{mvksdensity}; not ## intended to be called directly. ## ## @end deftypefn function c = kernelcdf (z, kernel) if (is_function_handle (kernel)) ## Numeric cumulative for a custom kernel over a fine grid. g = linspace (-1e3, 1e3, 200001)'; C = cumtrapz (g, kernel (g)); C = C / C(end); c = reshape (interp1 (g, C, z(:), 'linear', 'extrap'), size (z)); return; endif z = kernelscale (kernel) * z; switch (kernel) case 'normal' c = 0.5 * erfc (-z / sqrt (2)); case 'box' c = min (max ((z + 1) / 2, 0), 1); case 'triangle' zc = max (min (z, 1), -1); c = (zc < 0) .* (0.5 * (zc + 1) .^ 2) ... + (zc >= 0) .* (0.5 + zc - 0.5 * zc .^ 2); case 'epanechnikov' zc = max (min (z, 1), -1); c = 0.75 * zc - 0.25 * zc .^ 3 + 0.5; endswitch endfunction statistics-release-2.1.2/inst/Descriptive_Statistics/private/kernelpdf.m000066400000000000000000000033371527311104300266010ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{k} =} kernelpdf (@var{z}, @var{kernel}) ## ## Unit-variance smoothing kernel density @math{K(z)} evaluated at the ## standardized distances @var{z}. For a named compact kernel @math{K(z) = ## s * K0(s*z)} with @math{K0} the canonical form and @math{s} its standard ## deviation; @var{kernel} may instead be a function handle, which is applied as ## supplied. Internal helper shared by @code{ksdensity} and @code{mvksdensity}; ## not intended to be called directly. ## ## @end deftypefn function k = kernelpdf (z, kernel) if (is_function_handle (kernel)) k = kernel (z); return; endif s = kernelscale (kernel); z = s * z; switch (kernel) case 'normal' k = exp (-0.5 * z .^ 2) / sqrt (2 * pi); case 'box' k = 0.5 * (abs (z) <= 1); case 'triangle' k = max (1 - abs (z), 0); case 'epanechnikov' k = 0.75 * max (1 - z .^ 2, 0); endswitch k = s * k; endfunction statistics-release-2.1.2/inst/Descriptive_Statistics/private/kernelscale.m000066400000000000000000000027341527311104300271170ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{s} =} kernelscale (@var{kernel}) ## ## Standardized-distance scale of a named smoothing kernel, i.e. the standard ## deviation of its canonical form. The compact kernels are rescaled by this ## factor so that each has unit variance and the bandwidth is comparable across ## kernels (as in MATLAB). Internal helper shared by @code{ksdensity} and ## @code{mvksdensity}; not intended to be called directly. ## ## @end deftypefn function s = kernelscale (kernel) switch (kernel) case 'box' s = 1 / sqrt (3); case 'triangle' s = 1 / sqrt (6); case 'epanechnikov' s = 1 / sqrt (5); otherwise s = 1; endswitch endfunction statistics-release-2.1.2/inst/Descriptive_Statistics/private/ratingcodes.m000066400000000000000000000114121527311104300271220ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{codes}, @var{cats}, @var{errmsg}] =} ratingcodes (@var{R}, @var{cats}) ## ## Code the ratings of several raters against one set of categories. ## ## @var{R} is a cell array holding the ratings of each rater, one vector ## each, all of the same kind and length: numeric or logical, categorical, or ## text (a character matrix with one rating per row, a cell array of ## character vectors, or a string array). @var{codes} has one column per ## rater, holding the index of each rating in @var{cats}, and @qcode{NaN} ## where the rating is missing: @qcode{NaN}, an undefined category, an empty ## text or a missing string. ## ## Given @var{cats} empty, the categories are the sorted values of numeric ## ratings, the categories of categorical ratings in their own order, unused ## ones included, and the sorted texts of text ratings. Otherwise @var{cats} ## lists them, in the order to be used; numeric for numeric ratings, text ## or categorical for the others. @var{cats} is returned as a numeric ## column or a cell array of character vectors. @var{errmsg} is empty, or ## says what is wrong with the input. ## @end deftypefn function [codes, cats, errmsg] = ratingcodes (R, cats) codes = []; errmsg = ""; ## The kind of each rater's ratings, and their number k = numel (R); kind = cell (1, k); n = zeros (1, k); for j = 1:k r = R{j}; if ((isnumeric (r) || islogical (r)) && isreal (r) && isvector (r)) kind{j} = 'numeric'; n(j) = numel (r); elseif (iscategorical (r) && isvector (r)) kind{j} = 'categorical'; n(j) = numel (r); elseif (ischar (r) && ndims (r) == 2) kind{j} = 'text'; n(j) = rows (r); elseif ((iscellstr (r) || isstring (r)) && isvector (r)) kind{j} = 'text'; n(j) = numel (r); else errmsg = "ratings must be numeric, logical, categorical or text vectors."; return; endif endfor if (! all (strcmp (kind, kind{1}))) errmsg = "ratings of every rater must be of the same kind."; return; endif if (any (n != n(1))) errmsg = "every rater must rate the same number of subjects."; return; endif ## All ratings in one column, rater after rater, with what is missing switch (kind{1}) case 'numeric' all_r = zeros (0, 1); for j = 1:k all_r = [all_r; double(R{j}(:))]; endfor miss = isnan (all_r); case 'categorical' all_r = R{1}(:); for j = 2:k all_r = [all_r; R{j}(:)]; endfor if (isempty (cats)) cats = categories (all_r); endif miss = isundefined (all_r); all_r = cellstr (all_r); case 'text' all_r = cell (0, 1); for j = 1:k r = R{j}; if (isstring (r)) m = ismissing (r(:)); r = cellstr (r(:)); r(m) = {''}; else r = cellstr (r); endif all_r = [all_r; r(:)]; endfor miss = cellfun (@isempty, all_r); endswitch ## The categories, given or found if (strcmp (kind{1}, 'numeric')) if (isempty (cats)) cats = unique (all_r(! miss)); elseif (! ((isnumeric (cats) || islogical (cats)) && isreal (cats) && isvector (cats) && ! any (isnan (cats)))) errmsg = "'Categories' must be a numeric vector for numeric ratings."; return; endif cats = double (cats(:)); else if (isempty (cats)) cats = unique (all_r(! miss)); elseif (iscategorical (cats) || isstring (cats) || ischar (cats)) cats = cellstr (cats); elseif (! iscellstr (cats)) errmsg = "'Categories' must be text for text or categorical ratings."; return; endif cats = cats(:); endif if (numel (unique (cats)) != numel (cats)) errmsg = "'Categories' must not repeat a category."; return; endif [tf, idx] = ismember (all_r, cats); if (any (! tf & ! miss)) errmsg = "a rating is not one of 'Categories'."; return; endif codes = idx; codes(miss) = NaN; codes = reshape (codes, n(1), k); endfunction statistics-release-2.1.2/inst/Descriptive_Statistics/tabulate.m000066400000000000000000000423251527311104300247560ustar00rootroot00000000000000## Copyright (C) 2003 Alberto Terruzzi ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} tabulate (@var{x}) ## @deftypefnx {statistics} {@var{tbl} =} tabulate (@var{x}) ## ## Create a frequency table of unique values in vector @var{x}. ## ## @code{tabulate (x)} displays a frequency table of the data in the vector ## @var{x}. The input @var{x} can be a numeric vector, a logical vector, a ## character matrix, a cell vector of character vectors, a categorical vector, ## a string vector, a datetime vector, or a duration vector. ## ## The table displays the value, the number of instances (count), and the ## percentage of that value in @var{x}. If no output argument is requested, ## the table is displayed in the command window. ## ## @code{@var{tbl} = tabulate (@var{x})} returns the frequency table, ## @var{tbl}, as a numeric matrix when @var{x} is numeric and as a cell array ## otherwise. ## ## If @var{x} is numeric, any missing values (@qcode{NaNs}) are ignored. ## Similarly, undefined elements in categorical arrays, missing elements in ## string arrays, @qcode{NaT} elements in datetime arrays, and @qcode{NaN} ## elements in duration arrays are ignored. The values of a datetime or ## duration vector are listed in sorted order. MATLAB tabulates each ## @qcode{NaN} duration instead, as a value of its own counted once. ## ## If all the elements of @var{x} are positive integers, then the frequency ## table includes 0 counts for the integers between 1 and @qcode{max (@var{x})} ## that do not appear in @var{x}. ## ## For categorical arrays, the frequency table includes 0 counts for any ## categories that are defined but do not appear in @var{x}. ## ## Missing values are not tabulated. The percentage column is ## @qcode{count / total * 100} taken literally, so a category with no ## observations out of none at all is @qcode{NaN} rather than zero: there is no ## total to take a proportion of. A @qcode{categorical} carries its categories ## independently of its data, so an all-undefined one still tabulates every ## category with a zero count; a string, datetime or duration array has no ## levels beyond those its data carries, so an all-missing one tabulates to an ## empty table. MATLAB agrees on the categorical case but returns a malformed ## @math{1}-by-@math{2} cell for an all-missing string or datetime array, ## lacking the label column its own documentation describes, while returning a ## well-formed @math{0}-by-@math{3} for an empty one. This implementation ## returns the empty table in both. ## ## @seealso{bar, pareto} ## @end deftypefn function tbl = tabulate (x) ## Check input for being numeric, logical, categorical, datetime, duration, ## or text. All types but char array must be vectors if (ischar (x) && ! ismatrix (x)) error ("tabulate: char array input must be a matrix."); elseif (! isvector (x) && ! isempty (x) && ! ischar (x)) error ("tabulate: input must be a vector except for char array."); elseif (! (isnumeric (x) || islogical (x) || iscellstr (x) || iscategorical (x) || isa (x, 'string') || isdatetime (x) || isduration (x)) && ! ischar (x)) error (strcat ("tabulate: X must be numeric, logical, categorical,", ... " datetime, duration, cellstring, string, or a char", ... " array.")); endif ## Ensure vector input if (! ischar (x)) x = x(:); endif if (iscategorical (x)) ## For categorical, we report ALL categories, even if count is 0 vals = categories (x); nc = length (vals); ## Count occurrences xi = double (x); ## Filter out undefined valid_mask = ! isnan (xi) & (xi >= 1) & (xi <= nc); xi = xi(valid_mask); if (isempty (xi)) counts = zeros (nc, 1); else counts = accumarray (xi, 1, [nc, 1]); endif ## The percentage is computed literally, so a category with no ## observations out of none at all is NaN rather than zero: there is no ## total to take a proportion of. MATLAB does the same. total = sum (counts); percents = 100 * counts ./ total; ## Output format: Cell array out = cell (length (vals), 3); out(:,1) = vals; out(:,2) = num2cell (counts); out(:,3) = num2cell (percents); elseif (isa (x, 'string') || isdatetime (x) || isduration (x)) ## Handle string, datetime and duration arrays: grp2idx keeps strings in ## order of first appearance and sorts the others x(ismissing (x)) = []; if (isempty (x)) out = cell (0, 3); else if (isa (x, 'string')) x = cellstr (x); endif [idx, vals] = grp2idx (x); counts = accumarray (idx, 1); total = sum (counts); percents = 100 * counts ./ total; out = cell (length (vals), 3); out(:,1) = vals; out(:,2) = num2cell (counts); out(:,3) = num2cell (percents); endif elseif (islogical (x)) ## Handle logical arrays [vals, ~, idx] = unique (x); if (isempty (x)) counts = []; percents = []; else counts = accumarray (idx, 1); total = sum (counts); percents = 100 * counts ./ total; endif vals_cell = cellstr (num2str (double (vals))); out = cell (length (vals), 3); out(:, 1) = vals_cell; out(:, 2) = num2cell (counts); out(:, 3) = num2cell (percents); elseif (isnumeric (x)) ## Handle numeric ## Remove missing values (NaNs) if numeric x(isnan (x)) = []; ## Handle positive integers separately if (! isempty (x) && all (x == fix (x)) && all (x > 0)) max_val = max (x); vals = (1:max_val)'; [counts, ~] = hist (x, vals); counts = counts(:); else [vals, ~, idx] = unique (x); if (isempty (x)) counts = []; else counts = accumarray (idx, 1); endif endif if (isempty (counts)) percents = []; else percents = 100 * counts ./ sum (counts); endif ## Output format: Numeric Matrix out = [vals, counts, percents]; else ## Handle char and cellstr if (ischar (x)) x = cellstr (x); endif [idx, vals] = grp2idx (x); if (isempty (idx)) counts = []; else counts = accumarray (idx, 1); endif if (isempty (counts)) percents = []; else percents = 100 * counts ./ sum (counts); endif out = cell (length (vals), 3); out(:,1) = vals; out(:,2) = num2cell (counts); out(:,3) = num2cell (percents); endif if (nargout == 0) ## Use table for display if no output requested if (isempty (out)) ## Handle empty case disp (' Value Count Percent'); return; endif if (isnumeric (out)) ## Numeric matrix case Value = out(:,1); Count = out(:,2); Percent = out(:,3); else ## Cell array case Value = out(:,1); Count = cell2mat (out(:,2)); Percent = cell2mat (out(:,3)); endif t = table (Value, Count, Percent, 'VariableNames', {'Value', 'Count', ... 'Percent'}); disp (t); else tbl = out; endif endfunction %!demo %! ## Generate a frequency table for a vector of data in a cell array %! load patients %! %! ## Display the first seven entries of the Gender variable %! gender = Gender(1:7) %! %! ## Compute the frequency table that shows the number and %! ## percentage of Male and Female patients %! tabulate (Gender) %!demo %! ## Create a frequency table for a vector of positive integers %! load patients %! %! ## Display the first seven entries of the Gender variable %! height = Height(1:7) %! %! ## Create a frequency table that shows, in its second and third columns, %! ## the number and percentage of patients with a particular height. %! table = tabulate (Height); %! %! ## Display the first and last seven entries of the frequency table %! first = table(1:7,:) %! %! last = table(end-6:end,:) %!demo %! ## Create a frequency table from a character array %! load carsmall %! %! ## Tabulate the data in the Origin variable, which shows the %! ## country of origin of each car in the data set %! tabulate (Origin) %!demo %! ## Create a frequency table from a numeric vector with NaN values %! load carsmall %! %! ## The carsmall dataset contains measurements of 100 cars %! total_cars = length (MPG) %! ## For six cars, the MPG value is missing %! missingMPG = length (MPG(isnan (MPG))) %! %! ## Create a frequency table using MPG %! tabulate (MPG) %! table = tabulate (MPG); %! %! ## Only 94 cars were used %! valid_cars = sum (table(:,2)) %!test %! load patients %! table = tabulate (Gender); %! assert_equal (table{1,1}, "Male"); %! assert_equal (table{2,1}, "Female"); %! assert_equal (table{1,2}, 47); %! assert_equal (table{2,2}, 53); %!test %! load patients %! table = tabulate (Height); %! assert_equal (table(end-4,:), [68, 15, 15]); %! assert_equal (table(end-3,:), [69, 8, 8]); %! assert_equal (table(end-2,:), [70, 11, 11]); %! assert_equal (table(end-1,:), [71, 10, 10]); %! assert_equal (table(end,:), [72, 4, 4]); %!test %! ## Test numeric vector including NaNs %! x = [1; 1; 2; 3; 1; NaN; 2]; %! tbl = tabulate (x); %! assert_equal (isnumeric (tbl), true); %! assert_equal (size (tbl), [3, 3]); %! assert_equal (tbl(:,1), [1; 2; 3]); %! assert_equal (tbl(:,2), [3; 2; 1]); %! assert_equal (tbl(:,3), [50; 33.3333; 16.6667], 3e-4); %!test %! ## Test positive integers with gaps %! x = [1; 3; 3]; %! tbl = tabulate (x); %! assert_equal (isnumeric (tbl), true); %! assert_equal (size (tbl), [3, 3]); %! assert_equal (tbl(:,1), [1; 2; 3]); %! assert_equal (tbl(:,2), [1; 0; 2]); %! assert_equal (tbl(:,3), [33.3333; 0; 66.6667], 3e-4); %!test %! ## Test logical inputs (should return cell array with '0'/'1') %! x = [true; false; true; true]; %! tbl = tabulate (x); %! assert_equal (iscell (tbl), true); %! assert_equal (size (tbl), [2, 3]); %! assert_equal (tbl(:,1), {'0'; '1'}); %! assert_equal ([tbl{:,2}]', [1; 3]); %! assert_equal ([tbl{:,3}]', [25; 75]); %!test %! ## Test character array %! x = ['a'; 'b'; 'a']; %! tbl = tabulate (x); %! assert_equal (iscell (tbl), true); %! assert_equal (size (tbl), [2, 3]); %! assert_equal (tbl(:,1), {'a'; 'b'}); %! assert_equal ([tbl{:,2}]', [2; 1]); %!test %! ## Test cell array of character vectors %! x = {'a', 'b', 'a'}; %! tbl = tabulate (x); %! assert_equal (iscell (tbl), true); %! assert_equal (size (tbl), [2, 3]); %! assert_equal (tbl(:,1), {'a'; 'b'}); %! assert_equal ([tbl{:,2}]', [2; 1]); %!test %! ## Test string array with missing values %! x = string ({'a', 'b', 'a'}); %! x(4) = missing; %! tbl = tabulate (x); %! assert_equal (iscell (tbl), true); %! assert_equal (size (tbl), [2, 3]); %! assert_equal (tbl(:,1), {'a'; 'b'}); %! assert_equal ([tbl{:,2}]', [2; 1]); %!test %! ## Test categorical array with undefined values and vacuous levels %! x = categorical ({'a', 'a', 'b'}, {'a', 'b', 'c'}); %! tbl = tabulate (x); %! assert_equal (iscell (tbl), true); %! assert_equal (size (tbl), [3, 3]); %! assert_equal (tbl(:,1), {'a'; 'b'; 'c'}); %! assert_equal ([tbl{:,2}]', [2; 1; 0]); %! assert_equal ([tbl{:,3}]', [66.6667; 33.3333; 0], 1e-3); %!test %! ## Test empty input %! tbl = tabulate ([]); %! assert_equal (isempty (tbl), true); %!test %! ## fisheriris (Categorical/CellStr) %! load fisheriris; %! tbl = tabulate (species); %! assert_equal (size (tbl), [3, 3]); %! assert_equal (tbl(:,1), {'setosa'; 'versicolor'; 'virginica'}); %! assert_equal ([tbl{:,2}]', [50; 50; 50]); %! assert_equal ([tbl{:,3}]', [33.3333; 33.3333; 33.3333], 1e-4); %!test %! ## carsmall (Char/CellStr) %! load carsmall; %! tbl = tabulate (Origin); %! origins = tbl(:,1); %! counts = [tbl{:,2}]; %! assert_equal (counts(strcmp (origins, 'USA')), 69); %! assert_equal (counts(strcmp (origins, 'Japan')), 15); %! assert_equal (counts(strcmp (origins, 'Germany')), 9); %! assert_equal (counts(strcmp (origins, 'France')), 4); %! assert_equal (counts(strcmp (origins, 'Sweden')), 2); %! assert_equal (counts(strcmp (origins, 'Italy')), 1); %!test %! ## patients (Logical) %! load patients; %! tbl = tabulate (Smoker); %! assert_equal (size (tbl), [2, 3]); %! assert_equal (tbl(:,1), {'0'; '1'}); %! assert_equal ([tbl{:,2}]', [66; 34]); %!test %! ## patients (String) %! load patients; %! tbl = tabulate (Gender); %! vals = tbl(:,1); %! counts = [tbl{:,2}]; %! assert_equal (counts(strcmp (vals, 'Male')), 47); %! assert_equal (counts(strcmp (vals, 'Female')), 53); ## Test categorical with all undefined values. The categories survive with ## zero counts, and the percentages are NaN: there is no total to take a ## proportion of. Values are R2024a's, measured 2026-08-17. %!test %! x = categorical ({'a','b','c'}); %! x(:) = categorical (missing); %! tbl = tabulate (x); %! assert_equal (iscell (tbl), true); %! assert_equal (size (tbl), [3, 3]); %! assert_equal ([tbl{:,2}]', [0; 0; 0]); %! assert_equal ([tbl{:,3}]', [NaN; NaN; NaN]); %!test %! ## a zero count against a nonzero total is 0 per cent, not NaN %! x = categorical ({'a','b','c','a'}); %! x(2) = categorical (missing); %! tbl = tabulate (x); %! assert_equal ([tbl{:,2}]', [2; 0; 1]); %! assert_equal ([tbl{:,3}]', [200/3; 0; 100/3], 1e-12); ## Test categorical with defined categories but no data %!test %! x = categorical ({}, {'low','med','high'}); %! tbl = tabulate (x); %! assert_equal (iscell (tbl), true); %! assert_equal ([tbl{:,2}]', [0; 0; 0]); %! assert_equal ([tbl{:,3}]', [NaN; NaN; NaN]); ## Test string array with all missing values. A string array has no levels ## beyond those its data carries, so an all-missing one tabulates to nothing. ## MATLAB returns a malformed 1-by-2 cell here, without the label column its ## own documentation describes, and returns a well-formed 0-by-3 for an empty ## string array; this is a deliberate divergence. Measured 2026-08-17. %!test %! x = string ({'a','b'}); %! x(:) = missing; %! tbl = tabulate (x); %! assert_equal (iscell (tbl), true); %! assert_equal (isempty (tbl), true); ## Test 2D character matrices. %!test %! x = ['yes'; 'no'; 'yes']; %! tbl = tabulate (x); %! assert_equal (iscell (tbl), true); %! assert_equal (size (tbl), [2, 3]); %! assert_equal (tbl(:,1), {'yes'; 'no'}); %! assert_equal ([tbl{:,2}]', [2; 1]); %! assert_equal ([tbl{:,3}]', [66.6667; 33.3333], 1e-4); ## Test datetime and duration vectors. The tables of d and h are R2024a's: ## groups are sorted, and a NaT is left out of both the counts and the total. ## A NaN duration and an all-missing vector deviate, as the help text states. %!shared d, h %! d = repmat ([datetime(2026, 1, 5); datetime(2025, 3, 1); ... %! datetime(2026, 10, 13)], 6, 1); %! d(4) = NaT; %! h = hours (repmat ([1; 2; 3], 6, 1)); %!test %! tbl = tabulate (d); %! assert_equal (tbl(:,1), {'01-Mar-2025'; '05-Jan-2026'; '13-Oct-2026'}); %! assert_equal ([tbl{:,2}]', [6; 5; 6]); %! assert_equal ([tbl{:,3}]', [35.2941; 29.4118; 35.2941], 1e-4); %!test %! tbl = tabulate (h); %! assert_equal (tbl(:,1), {'1 hr'; '2 hr'; '3 hr'}); %! assert_equal ([tbl{:,2}]', [6; 6; 6]); %! assert_equal ([tbl{:,3}]', [33.3333; 33.3333; 33.3333], 1e-4); %!test %! h(2) = NaN; %! tbl = tabulate (h); %! assert_equal ([tbl{:,2}]', [6; 5; 6]); %!test %! tbl = tabulate (d.'); %! assert_equal ([tbl{:,2}]', [6; 5; 6]); %!test %! tbl = tabulate (NaT (3, 1)); %! assert_equal (iscell (tbl), true); %! assert_equal (isempty (tbl), true); %!test %! tbl = tabulate (NaT (0, 1)); %! assert_equal (iscell (tbl), true); %! assert_equal (isempty (tbl), true); %!test %! tbl = tabulate (hours (NaN (3, 1))); %! assert_equal (size (tbl), [0, 3]); %!test %! out = evalc ('tabulate (d)'); %! assert_equal (isempty (strfind (out, '05-Jan-2026')), false); ## Test input validation %!error ... %! tabulate (repmat ('a', 3, 3, 3)) %!error ... %! tabulate ([3, 3; 3, 3]) %!error ... %! tabulate ({'3', '3'; '3', '3'}) %!error ... %! tabulate ([true, true; false, true]) %!error ... %! tabulate (categorical ([true, true; false, true])) %!error ... %! tabulate (string ({'a', 'b'; 'a', 'c'})) %!error ... %! tabulate (repmat (datetime (2026, 1, 1), 2, 2)) %!error ... %! tabulate (hours ([1, 2; 3, 4])) %!error ... %! tabulate ({3, 3, 3, 3}) statistics-release-2.1.2/inst/Descriptive_Statistics/trimmean.m000066400000000000000000000351101527311104300247630ustar00rootroot00000000000000## Copyright (C) 2001 Paul Kienzle ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{m} =} trimmean (@var{x}, @var{p}) ## @deftypefnx {statistics} {@var{m} =} trimmean (@var{x}, @var{p}, @var{flag}) ## @deftypefnx {statistics} {@var{m} =} trimmean (@dots{}, @qcode{'all'}) ## @deftypefnx {statistics} {@var{m} =} trimmean (@dots{}, @var{dim}) ## @deftypefnx {statistics} {@var{m} =} trimmean (@dots{}, @var{vecdim}) ## ## Compute the trimmed mean. ## ## The trimmed mean of @var{x} is defined as the mean of @var{x} excluding the ## highest and lowest @math{k} data values of @var{x}, calculated as ## @qcode{@var{k} = n * (@var{p} / 100) / 2)}, where @var{n} is the sample size. ## ## @code{@var{m} = trimmean (@var{x}, @var{p})} returns the mean of @var{x} ## after removing the outliers in @var{x} defined by @var{p} percent. ## @itemize ## @item If @var{x} is a vector, then @code{trimmean (@var{x}, @var{p})} is the ## mean of all the values of @var{x}, computed after removing the outliers. ## @item If @var{x} is a matrix, then @code{trimmean (@var{x}, @var{p})} is a ## row vector of column means, computed after removing the outliers. ## @item If @var{x} is a multidimensional array, then @code{trimmean} operates ## along the first nonsingleton dimension of @var{x}. ## @end itemize ## ## To specify the operating dimension(s) when @var{x} is a matrix or a ## multidimensional array, use the @var{dim} or @var{vecdim} input argument. ## ## @code{trimmean} treats @qcode{NaN} values in @var{x} as missing values and ## removes them. Integer, logical and character @var{x} is computed and ## returned in double precision, although MATLAB keeps the integer class and ## rounds under @qcode{'weighted'}. ## ## @code{@var{m} = trimmean (@var{x}, @var{p}, @var{flag})} specifies how to ## trim when @math{k}, i.e. half the number of outliers, is not an integer. ## @var{flag} can be specified as one of the following values: ## @multitable @columnfractions 0.2 0.75 ## @headitem Value @tab Description ## @item @qcode{'round'} @tab Round @math{k} to the nearest integer. This ## is the default. ## @item @qcode{'floor'} @tab Round @math{k} down to the next smaller ## integer. ## @item @qcode{'weighted'} @tab If @math{k = i + f}, where @math{i} is an ## integer and @math{f} is a fraction, compute a weighted mean with weight ## @math{(1 - f)} for the @math{(i + 1)}-th and @math{(n - i)}-th values, and ## full weight for the values between them. ## @end multitable ## ## @code{@var{m} = trimmean (@dots{}, @qcode{'all'})} returns the trimmed mean ## of all the values in @var{x} using any of the input argument combinations in ## the previous syntaxes. ## ## @code{@var{m} = trimmean (@dots{}, @var{dim})} returns the trimmed mean along ## the operating dimension @var{dim} specified as a positive integer scalar. If ## not specified, then the default value is the first nonsingleton dimension of ## @var{x}, i.e. whose size does not equal 1. If @var{dim} is greater than ## @qcode{ndims (@var{X})} or if @qcode{size (@var{x}, @var{dim})} is 1, then ## @code{trimmean} returns @var{x}. ## ## @code{@var{m} = trimmean (@dots{}, @var{vecdim})} returns the trimmed mean ## over the dimensions specified in the vector @var{vecdim}. For example, if ## @var{x} is a 2-by-3-by-4 array, then @code{mean (@var{x}, [1 2])} returns a ## 1-by-1-by-4 array. Each element of the output array is the mean of the ## elements on the corresponding page of @var{x}. If @var{vecdim} indexes all ## dimensions of @var{x}, then it is equivalent to @code{mean (@var{x}, "all")}. ## Any dimension in @var{vecdim} greater than @code{ndims (@var{x})} is ignored. ## ## @seealso{mean} ## @end deftypefn function m = trimmean (x, p, varargin) if (nargin < 2 || nargin > 4) print_usage; endif ## A non-scalar percentage used to slip through, because each half of the ## range test was then itself non-scalar and neither branch was taken: the ## trimming ran on a colon built from a vector and returned an untrimmed ## mean behind a warning. if (! isscalar (p) || ! isreal (p) || p < 0 || p >= 100) error ("trimmean: PERCENT must be a real scalar in the range [0, 100)."); endif ## Parse extra arguments if (nargin < 3) flag = []; dim = []; elseif (nargin < 4) if (ischar (varargin{1}) && ! strcmpi (varargin{1}, 'all')) flag = varargin{1}; dim = []; elseif (isnumeric (varargin{1}) || strcmpi (varargin{1}, 'all')) flag = []; dim = varargin{1}; endif else flag = varargin{1}; dim = varargin{2}; endif ## Compute in double unless X is floating point if (! isfloat (x)) x = double (x); endif ## Get size of X szx = size (x); ndx = ndims (x); ## Handle special case X = [] when DIM is not specified if (isempty (dim) && isequal (szx, [0, 0])) m = NaN (class (x)); return endif ## Check FLAG if (isempty (flag)) flag = 'round'; endif if (! any (strcmpi (flag, {'round', 'floor', 'weighted'}))) error ("trimmean: invalid FLAG argument."); endif ## Check DIM if (isempty (dim)) (dim = find (szx != 1, 1)) || (dim = 1); endif if (strcmpi (dim, 'all')) x = x(:); dim = 1; szx = size (x); ndx = 2; endif if (! (isvector (dim) && all (dim > 0) && all (rem (dim, 1) == 0))) error ("trimmean: DIM must be a positive integer scalar or vector."); endif vecdim_flag = false; if (numel (dim) > 1) dim = sort (dim); if (! all (diff (dim))) error ("trimmean: VECDIM must contain non-repeating positive integers."); endif vecdim_flag = true; endif ## If DIM is a scalar greater than ndims, return X if (isscalar (dim) && dim > ndx) m = x; return endif ## If DIM is a scalar and size (x, dim) == 1, return X if (isscalar (dim) && size (x, dim) == 1) m = x; return endif ## If DIM is a vector, ignore any value > ndims (x) if (numel (dim) > 1) dim(dim > ndx) = []; if (isempty (dim)) m = x; return endif endif ## Permute dim to simplify all operations along dim1. At func. end ipermute. if (numel (dim) > 1 || dim != 1) perm = 1:ndx; if (! vecdim_flag) ## Move dim to dim 1 perm([1, dim]) = [dim, 1]; x = permute (x, perm); szx([1, dim]) = szx([dim, 1]); dim = 1; else ## Move vecdims to front perm(dim) = []; perm = [dim, perm]; x = permute (x, perm); ## Reshape all vecdims into dim1 num_dim = prod (szx(dim)); szx(dim) = []; szx = [num_dim, ones(1, numel(dim)-1), szx]; x = reshape (x, szx); dim = 1; endif perm_flag = true; else perm_flag = false; endif ## Create output matrix sizem = size (x); sizem(dim) = 1; ## Sort X along 1st dimensions x = sort (x, 1); ## No missing data, all columns have the same length if (! any (isnan (x(:)))) if (isempty (x)) n = 0; else n = size (x, 1); endif m = trim (x, n, p, flag, sizem); m = reshape (m, sizem); ## With missing data, each column is computed separately else m = NaN (sizem, class (x)); for j = 1:prod (sizem(2:end)) n = find (! isnan (x(:,j)), 1, 'last'); m(j) = trim (x(:,j), n, p, flag, [1, 1]); endfor endif ## Inverse permute back to correct dimensions (if necessary) if (perm_flag) m = ipermute (m, perm); endif endfunction ## Help function for handling different flags function m = trim (x, n, p, flag, sizem) switch (lower (flag)) case 'round' k = n * p / 200; k0 = round (k - eps (k)); if (! isempty (n) && n > 0 && k0 < n / 2) m = mean (x((k0+1):(n-k0),:), 1); else m = NaN (sizem, class (x)); endif case 'floor' k0 = floor (n * p / 200); if (! isempty (n) && n > 0 && k0 < n / 2) m = mean (x((k0+1):(n-k0),:), 1); else m = NaN (sizem, class (x)); endif case 'weighted' k = n * p / 200; k0 = floor (k); fr = 1 + k0 - k; if (! isempty (n) && n > 0 && (k0 < n / 2 || fr > 0)) m = (sum (x((k0+2):(n-k0-1),:),1) + fr * x(k0+1,:) + fr * x(n-k0,:)) ... / (max (0, n - 2 * k0 - 2) + 2 * fr); else m = NaN (sizem, class (x)); endif endswitch endfunction ## Test output %!test %! x = reshape (1:40, [5, 4, 2]); %! x([3, 37]) = -100; %! assert_equal (trimmean (x, 10, 'all'), 19.4722, 1e-4); %!test %! x = reshape (1:40, [5, 4, 2]); %! x([3, 37]) = -100; %! out = trimmean (x, 10, [1, 2]); %! assert_equal (out(1,1,1), 10.3889, 1e-4); %! assert_equal (out(1,1,2), 29.6111, 1e-4); %!test %! x = reshape (1:40, [5, 4, 2]); %! x([3, 37]) = -100; %! x([4, 38]) = NaN; %! assert_equal (trimmean (x, 10, 'all'), 19.3824, 1e-4); %!test %! x = reshape (1:40, [5, 4, 2]); %! x([3, 37]) = -100; %! out = trimmean (x, 10, 1); %! assert_equal (out(:,:,1), [-17.6, 8, 13, 18]); %! assert_equal (out(:,:,2), [23, 28, 33, 10.6]); %!test %! x = reshape (1:40, [5, 4, 2]); %! x([3, 37]) = -100; %! x([4, 38]) = NaN; %! out = trimmean (x, 10, 1); %! assert_equal (out(:,:,1), [-23, 8, 13, 18]); %! assert_equal (out(:,:,2), [23, 28, 33, 3.75]); %!test %! x = reshape (1:40, [5, 4, 2]); %! x([3, 37]) = -100; %! out = trimmean (x, 10, 2); %! assert_equal (out(:,:,1), [8.5; 9.5; -15.25; 11.5; 12.5]); %! assert_equal (out(:,:,2), [28.5; -4.75; 30.5; 31.5; 32.5]); %!test %! x = reshape (1:40, [5, 4, 2]); %! x([3, 37]) = -100; %! x([4, 38]) = NaN; %! out = trimmean (x, 10, 2); %! assert_equal (out(:,:,1), [8.5; 9.5; -15.25; 14; 12.5]); %! assert_equal (out(:,:,2), [28.5; -4.75; 28; 31.5; 32.5]); %!test %! x = reshape (1:40, [5, 4, 2]); %! x([3, 37]) = -100; %! out = trimmean (x, 10, [1, 2, 3]); %! assert_equal (out, trimmean (x, 10, 'all')); ## Test N-D array with NaNs %!test %! x = reshape (1:40, [5, 4, 2]); %! x([3, 37]) = -100; %! x([4, 38]) = NaN; %! out = trimmean (x, 10, [1, 2]); %! assert_equal (out(1,1,1), 10.7647, 1e-4); %! assert_equal (out(1,1,2), 29.1176, 1e-4); %!test %! x = reshape (1:40, [5, 4, 2]); %! x([3, 37]) = -100; %! x([4, 38]) = NaN; %! out = trimmean (x, 10, [1, 3]); %! assert_equal (out, [2.5556, 18, 23, 11.6667], 1e-4); %!test %! x = reshape (1:40, [5, 4, 2]); %! x([3, 37]) = -100; %! x([4, 38]) = NaN; %! out = trimmean (x, 10, [2, 3]); %! assert_equal (out, [18.5; 2.3750; 3.2857; 24; 22.5], 1e-4); %!test %! x = reshape (1:40, [5, 4, 2]); %! x([3, 37]) = -100; %! x([4, 38]) = NaN; %! out = trimmean (x, 10, [1, 2, 3]); %! assert_equal (out, trimmean (x, 10, 'all')); %!test %! x = reshape (1:40, [5, 4, 2]); %! x([3, 37]) = -100; %! x([4, 38]) = NaN; %! out = trimmean (x, 10, [2, 3, 5]); %! assert_equal (out, [18.5; 2.3750; 3.2857; 24; 22.5], 1e-4); ## Test bug reported in PR #381 %!assert_equal (trimmean ([1, 2, 3, 4, 5], 40), 3) ## Test special cases %!assert_equal (trimmean (reshape (1:40, [5, 4, 2]), 10, 4), reshape (1:40, [5, 4, 2])) %!assert_equal (trimmean ([], 10), NaN) %!assert_equal (trimmean ([1;2;3;4;5], 10, 2), [1;2;3;4;5]) %!assert_equal (trimmean ([1, 2, 3; 4, 5, 6], 10, [3, 4]), [1, 2, 3; 4, 5, 6]) %!test %! ## Row vector with explicit dimension 1 %! assert_equal (trimmean ([1, 2, 3], 10, 1), [1, 2, 3]); %!test %! ## Row vector with explicit dimension 2 %! assert_equal (trimmean ([1, 2, 3], 10, 2), 2); %!test %! ## Empty array with non-operating dimension preserved (dim=1) %! assert_equal (trimmean (zeros (0, 5), 10, 1), NaN (1, 5)); %!test %! ## Empty array with non-operating dimension preserved (dim=2) %! assert_equal (trimmean (zeros (2, 0), 10, 2), NaN (2, 1)); ## Test empty X with explicit DIM %!assert_equal (trimmean ([], 10, 1), zeros (1, 0)) %!assert_equal (trimmean ([], 10, 2), zeros (0, 1)) %!assert_equal (trimmean ([], 10, 'all'), NaN) %!assert_equal (trimmean ([], 10, 'weighted', 2), zeros (0, 1)) %!assert_equal (trimmean (single ([]), 10, 1), zeros (1, 0, 'single')) ## Test integer, logical and single X %!assert_equal (trimmean (int8 ([1 2; 3 4]), 10, 3), [1, 2; 3, 4]) %!assert_equal (trimmean (int8 ([1 2; 3 4]), 10, [3, 4]), [1, 2; 3, 4]) %!assert_equal (trimmean (int8 ([1 2; 3 4]), 10, 2), [1.5; 3.5]) %!assert_equal (trimmean (int8 (5), 10), 5) %!assert_equal (trimmean (true (2), 10, 3), ones (2)) %!assert_equal (trimmean ('abd', 10), 295 / 3, 1e-13) %!assert_equal (trimmean (uint8 ([1 2 3 4 5 100]), 30, 'weighted'), ... %! 5.738095238095237, 1e-14) %!assert_equal (trimmean (int8 ([1 2; 3 5; 4 9; 7 1; 2 8; 6 3]), 30, ... %! 'weighted'), [3.761904761904761, 4.523809523809524], 1e-14) %!assert_equal (trimmean (single ([1 2; 3 4]), 10, 3), single ([1 2; 3 4])) %!assert_equal (trimmean (int8 ([]), 10), NaN) %!assert_equal (trimmean (logical ([]), 10, 1), zeros (1, 0)) ## Test input validation %!error trimmean (1) %!error trimmean (1,2,3,4,5) %!error ... %! trimmean ([1 2 3 4], -10) %!error ... %! trimmean ([1 2 3 4], 100) %!error ... %! trimmean ([1 2 3 4], [10, 20]) %!error ... %! trimmean ([1 2 3 4], []) %!error ... %! trimmean ([1 2 3 4], 10 + 2i) %!error trimmean ([1 2 3 4], 10, 'flag') %!error trimmean ([1 2 3 4], 10, 'flag', 1) %!error ... %! trimmean ([1 2 3 4], 10, -1) %!error ... %! trimmean ([1 2 3 4], 10, 'floor', -1) %!error ... %! trimmean (reshape (1:40, [5, 4, 2]), 10, [-1, 2]) %!error ... %! trimmean (reshape (1:40, [5, 4, 2]), 10, [1, 2, 2]) statistics-release-2.1.2/inst/Dimensionality_Reduction/000077500000000000000000000000001527311104300233025ustar00rootroot00000000000000statistics-release-2.1.2/inst/Dimensionality_Reduction/ReconstructionICA.m000066400000000000000000000431271527311104300270250ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftp {statistics} {} ReconstructionICA ## ## Reconstruction independent component analysis (RICA) feature-extraction ## model. ## ## A @qcode{ReconstructionICA} object stores the transformation learned by ## @code{rica} for extracting features from data. Create one with @code{rica}; ## apply it to data with the @code{transform} method. ## ## @seealso{rica, sparsefilt} ## @end deftp classdef ReconstructionICA properties (SetAccess = protected) ## -*- texinfo -*- ## @deftp {ReconstructionICA} {property} ModelParameters ## ## Options the fit used ## ## A scalar structure holding the options the fit ran with: ## @qcode{IterationLimit}, @qcode{Lambda}, @qcode{Standardize}, ## @qcode{ContrastFcn}, @qcode{InitialTransformWeights}, ## @qcode{GradientTolerance}, @qcode{StepTolerance}, @qcode{Solver} and ## @qcode{NonGaussianityIndicator}. ## This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {ReconstructionICA} {property} NumPredictors ## ## Number of input predictors ## ## A positive integer @math{P}, the number of columns of the training ## data. This property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {ReconstructionICA} {property} NumLearnedFeatures ## ## Number of learned features ## ## A positive integer @math{Q}, the number of features the learned ## transformation produces. This property is read-only. ## ## @end deftp NumLearnedFeatures = []; ## -*- texinfo -*- ## @deftp {ReconstructionICA} {property} Mu ## ## Predictor means used when standardizing ## ## A column vector with one entry per predictor, the mean of each ## column of the training data. It is empty unless @qcode{'Standardize'} ## was true. This property is read-only. ## ## @end deftp Mu = []; ## -*- texinfo -*- ## @deftp {ReconstructionICA} {property} Sigma ## ## Predictor standard deviations used when standardizing ## ## A column vector with one entry per predictor, the standard deviation ## of each column of the training data. It is empty unless ## @qcode{'Standardize'} was true. This property is read-only. ## ## @end deftp Sigma = []; ## -*- texinfo -*- ## @deftp {ReconstructionICA} {property} FitInfo ## ## History of the fit ## ## A scalar structure with the fields @qcode{Iteration} and ## @qcode{Objective}, both column vectors of the same length. ## @qcode{Iteration} counts from zero and @qcode{Objective(1)} is the ## objective at the starting weights, so the last entry of each is the ## solution the fit returned. This property is read-only. ## ## The trajectory is this implementation's own. The default ## @qcode{'quasinewton'} solver minimises through Octave's @code{fminunc}, ## and @qcode{'Solver', 'lbfgs'} selects the limited-memory BFGS solver ## MATLAB uses. Either way the steps taken from the same starting weights ## differ from MATLAB's, so the length of the history and the iteration ## counts differ, and on an objective this far from convex the optimum ## reached need not be MATLAB's either. ## ## @end deftp FitInfo = []; ## -*- texinfo -*- ## @deftp {ReconstructionICA} {property} TransformWeights ## ## Learned feature transformation weights ## ## A @math{P}-by-@math{Q} matrix of learned weights, its columns of unit ## length. The @code{transform} method applies it to data. This property ## is read-only. ## ## @end deftp TransformWeights = []; ## -*- texinfo -*- ## @deftp {ReconstructionICA} {property} InitialTransformWeights ## ## Starting feature transformation weights ## ## A @math{P}-by-@math{Q} matrix, the weights the fit started from. It ## is the matrix given as @qcode{'InitialTransformWeights'} when one was ## given, and the random start the fit drew otherwise. This property is ## read-only. ## ## @end deftp InitialTransformWeights = []; ## -*- texinfo -*- ## @deftp {ReconstructionICA} {property} NonGaussianityIndicator ## ## Non-Gaussianity of each learned feature ## ## A @math{Q}-by-1 vector of @math{+1} and @math{-1}, one per learned ## feature: @math{+1} where the feature is taken to be super-Gaussian and ## @math{-1} where it is taken to be sub-Gaussian. The entry sets the ## sign its feature's contrast term carries in the objective, so the fit ## seeks a sparse feature where the entry is @math{+1} and a spread one ## where it is @math{-1}. The default is all @math{+1}. This property is ## read-only. ## ## @end deftp NonGaussianityIndicator = []; endproperties methods ## -*- texinfo -*- ## @deftypefn {ReconstructionICA} {@var{Mdl} =} ReconstructionICA (@var{X}, @var{Q}, @dots{}) ## Fit a reconstruction ICA model. This constructor is invoked by ## @code{rica}; see @code{help rica} for the arguments. ## @end deftypefn function this = ReconstructionICA (X, Q, varargin) if (nargin == 0) return; endif [n, p] = size (X); ## Options opts = struct ("IterationLimit", 1000, "Lambda", 1, ... "Standardize", false, "ContrastFcn", "logcosh", ... "InitialTransformWeights", [], ... "GradientTolerance", 1e-6, "StepTolerance", 1e-6, ... "Solver", "quasinewton", ... "NonGaussianityIndicator", []); for k = 1:2:numel (varargin) name = varargin{k}; val = varargin{k+1}; switch (lower (name)) case 'iterationlimit' opts.IterationLimit = val; case 'lambda' opts.Lambda = val; case 'standardize' opts.Standardize = val; case 'contrastfcn' opts.ContrastFcn = lower (val); case 'initialtransformweights' opts.InitialTransformWeights = val; case 'gradienttolerance' opts.GradientTolerance = val; case 'steptolerance' opts.StepTolerance = val; case 'nongaussianityindicator' opts.NonGaussianityIndicator = val; case 'solver' if (! ischar (val) || ... ! any (strcmpi (val, {'quasinewton', 'lbfgs'}))) error ("rica: 'Solver' must be 'quasinewton' or 'lbfgs'."); endif opts.Solver = lower (val); otherwise error ("rica: unknown parameter name '%s'.", name); endswitch endfor if (! any (strcmp (opts.ContrastFcn, {'logcosh', 'exp', 'sqrt'}))) error ("rica: 'ContrastFcn' must be 'logcosh', 'exp', or 'sqrt'."); endif ## One sign per learned feature, all of them super-Gaussian by default if (isempty (opts.NonGaussianityIndicator)) opts.NonGaussianityIndicator = ones (Q, 1); else sigma = opts.NonGaussianityIndicator; if (! (isnumeric (sigma) && isreal (sigma) && isvector (sigma) && numel (sigma) == Q && all (abs (sigma(:)) == 1))) error (strcat ("rica: 'NonGaussianityIndicator' must be a real", ... " vector of Q elements, each +1 or -1.")); endif opts.NonGaussianityIndicator = sigma(:); endif ## Standardize (center and scale) the data if requested if (opts.Standardize) ## MU and SIGMA are held as columns, one entry per predictor, as ## MATLAB holds them, and transposed where the data is scaled. this.Mu = mean (X)'; this.Sigma = std (X)'; sig = this.Sigma'; sig(sig == 0) = 1; X = (X - this.Mu') ./ sig; endif ## Initial weights if (isempty (opts.InitialTransformWeights)) W0 = randn (p, Q); else W0 = opts.InitialTransformWeights; endif this.InitialTransformWeights = W0; ## Minimize the RICA objective lambda = opts.Lambda; sigma = opts.NonGaussianityIndicator; ofun = @(wv) __rica_objective__ (wv, X, p, Q, lambda, ... opts.ContrastFcn, sigma); if (strcmpi (opts.Solver, 'lbfgs')) ## LossTolerance is switched off because MATLAB has no such option ## here: the fit stops on the gradient or the step, never on the ## objective's own value. ## The history is sized to the problem rather than left at the ## engine's default ten. A transform carries only p * Q parameters, ## and once the stored pairs reach that count the recursion is full ## BFGS, which converges further here and in fewer iterations. The ## cap bounds the memory when Q is large. lbopts = struct ("IterationLimit", opts.IterationLimit, ... "GradientTolerance", opts.GradientTolerance, ... "StepTolerance", opts.StepTolerance, ... "LossTolerance", -Inf, ... "HistorySize", min (max (10, numel (W0)), 50)); [wv, lbinfo] = __lbfgs__ (ofun, W0(:), lbopts); fval = lbinfo.Fval; ## The engine records from the first step, where MATLAB's trajectory ## starts at the initial weights. f0 = ofun (W0(:)); obj = [f0; lbinfo.History.Fval]; else objective_history ("reset"); fmopts = optimset ("GradObj", "on", "MaxIter", opts.IterationLimit, ... "TolFun", 1e-10, "TolX", 1e-10, "Display", "off", ... "OutputFcn", @objective_history_fcn); [wv, fval, ~, output] = fminunc (ofun, W0(:), fmopts); obj = objective_history ("get"); endif W = reshape (wv, p, Q); W = W ./ sqrt (sum (W .^ 2, 1)); ## unit-length columns this.TransformWeights = W; this.NumPredictors = p; this.NumLearnedFeatures = Q; this.NonGaussianityIndicator = opts.NonGaussianityIndicator; ## MATLAB reports the whole trajectory: a column with the objective at ## the starting weights first, one entry per iteration after it, and ## Iteration the matching 0-based index. fminunc does not call its ## OutputFcn for the step it returns on, so the final value is appended. if (isempty (obj) || obj(end) != fval) obj(end+1, 1) = fval; endif this.FitInfo = struct ("Iteration", (0:numel (obj) - 1)', ... "Objective", obj); this.ModelParameters = opts; endfunction ## -*- texinfo -*- ## @deftypefn {ReconstructionICA} {@var{Z} =} transform (@var{Mdl}, @var{X}) ## Transform data @var{X} into the learned feature space, returning the ## @math{N * Q} matrix @var{Z} of features. ## @end deftypefn function Z = transform (this, X) if (nargin != 2) print_usage (); endif if (! isempty (this.Mu)) sig = this.Sigma'; sig(sig == 0) = 1; X = (X - this.Mu') ./ sig; endif Z = X * this.TransformWeights; endfunction endmethods endclassdef ## RICA objective and gradient with respect to the raw (un-normalized) weights. ## SQRT_EPS smooths abs (z) at the origin for the 'sqrt' contrast; the value is ## MATLAB's own, measured on R2024a against a fixture carrying a zero, which is ## the only place it is visible. function [f, g] = __rica_objective__ (wv, X, p, Q, lambda, contrast, sigma) SQRT_EPS = 1e-8; W = reshape (wv, p, Q); nrm = sqrt (sum (W .^ 2, 1)); Wn = W ./ nrm; Z = X * Wn; E = X * (Wn * Wn') - X; switch (contrast) case 'logcosh' C = 0.5 * log (cosh (2 * Z)); case 'exp' C = -exp (-Z .^ 2 / 2); case 'sqrt' C = sqrt (Z .^ 2 + SQRT_EPS); endswitch f = lambda * sum (E(:) .^ 2) + sum (C * sigma); if (nargout > 1) switch (contrast) case 'logcosh' dC = tanh (2 * Z); case 'exp' dC = Z .* exp (-Z .^ 2 / 2); case 'sqrt' dC = Z ./ sqrt (Z .^ 2 + SQRT_EPS); endswitch gWn = lambda * 2 * (X' * E * Wn + E' * X * Wn) + X' * (dC .* sigma'); g = zeros (p, Q); for j = 1:Q wnj = Wn(:,j); g(:,j) = (gWn(:,j) - wnj * (wnj' * gWn(:,j))) / nrm(j); endfor g = g(:); endif endfunction %!shared X, W0 %! X = reshape (mod ((1:60)*7, 13), 12, 5) - 6; %! W0 = reshape (mod ((1:15)*3, 7), 5, 3) - 3; %!test %! ## Construct the object directly and check its properties. %! Mdl = ReconstructionICA (X, 3, "InitialTransformWeights", W0, ... %! "Lambda", 1, "IterationLimit", 500); %! assert_equal (isa (Mdl, "ReconstructionICA"), true); %! assert_equal (Mdl.NumPredictors, 5); %! assert_equal (Mdl.NumLearnedFeatures, 3); %! assert_equal (size (Mdl.TransformWeights), [5, 3]); %! assert_equal (Mdl.InitialTransformWeights, W0); %! assert_equal (isfield (Mdl.FitInfo, "Objective"), true); %!test %! ## The transform method projects onto the (unit-column) weights. %! Mdl = ReconstructionICA (X, 3, "InitialTransformWeights", W0, ... %! "IterationLimit", 500); %! Z = transform (Mdl, X); %! assert_equal (Z, X * Mdl.TransformWeights, 1e-12); %! assert_equal (sqrt (sum (Mdl.TransformWeights .^ 2, 1)), [1, 1, 1], 1e-10); %!test %! ## The transform method standardizes new data when the model does. %! Mdl = ReconstructionICA (X, 2, "Standardize", true, "IterationLimit", 100, ... %! "InitialTransformWeights", W0(:,1:2)); %! Z = transform (Mdl, X); %! Xs = (X - Mdl.Mu') ./ Mdl.Sigma'; %! assert_equal (Z, Xs * Mdl.TransformWeights, 1e-12); %!test %! ## Mu and Sigma are columns, one entry per predictor, as MATLAB reports them %! Mdl = ReconstructionICA (X, 2, "Standardize", true, "IterationLimit", 100, ... %! "InitialTransformWeights", W0(:,1:2)); %! assert_equal (size (Mdl.Mu), [5, 1]); %! assert_equal (size (Mdl.Sigma), [5, 1]); %! assert_equal (Mdl.Mu, mean (X)', 1e-12); %! assert_equal (Mdl.Sigma, std (X)', 1e-12); %!test %! ## The default constructor returns an empty model. %! Mdl = ReconstructionICA (); %! assert_equal (isempty (Mdl.TransformWeights), true); %!test %! ## NonGaussianityIndicator defaults to one per feature, all +1. %! Mdl = ReconstructionICA (X, 3, "InitialTransformWeights", W0, ... %! "IterationLimit", 100); %! assert_equal (Mdl.NonGaussianityIndicator, [1; 1; 1]); %!test %! ## A given indicator is held as a column, whatever shape it arrives in. %! Mdl = ReconstructionICA (X, 3, "InitialTransformWeights", W0, ... %! "IterationLimit", 100, ... %! "NonGaussianityIndicator", [1, -1, 1]); %! assert_equal (Mdl.NonGaussianityIndicator, [1; -1; 1]); %!test %! ## The indicator signs each feature's contrast term, so it moves the fit. %! args = {"InitialTransformWeights", W0, "IterationLimit", 200}; %! Mdl = ReconstructionICA (X, 3, args{:}); %! Neg = ReconstructionICA (X, 3, args{:}, ... %! "NonGaussianityIndicator", [-1; 1; 1]); %! assert_equal (isequal (Mdl.TransformWeights, Neg.TransformWeights), false); %!test %! ## All +1 is the default, so it must reproduce the default fit exactly. %! args = {"InitialTransformWeights", W0, "IterationLimit", 200}; %! Mdl = ReconstructionICA (X, 3, args{:}); %! Pos = ReconstructionICA (X, 3, args{:}, ... %! "NonGaussianityIndicator", [1; 1; 1]); %! assert_equal (Pos.TransformWeights, Mdl.TransformWeights); %!test %! ## Objective against MATLAB R2024a: 801.1816174574 and 197.5588279632. %! t = (1:80)'; %! Xr = double ([mod(t*7,11)-5, mod(t*13,17)-8, mod(t*5,7)-3]); %! args = {"InitialTransformWeights", [1, 0; 0, 1; 1, 1], "Lambda", 1, ... %! "ContrastFcn", "logcosh", "IterationLimit", 1000, ... %! "Solver", "lbfgs", "GradientTolerance", 1e-10, ... %! "StepTolerance", 1e-10}; %! Mdl = ReconstructionICA (Xr, 2, args{:}); %! Neg = ReconstructionICA (Xr, 2, args{:}, ... %! "NonGaussianityIndicator", [-1; 1]); %! assert_equal (Mdl.FitInfo.Objective(end), 801.1816174574, 1e-6); %! assert_equal (Neg.FitInfo.Objective(end), 197.5588279632, 1e-6); %!error ... %! ReconstructionICA (ones (6, 5), 2, "NonGaussianityIndicator", [1; 1; 1]) %!error ... %! ReconstructionICA (ones (6, 5), 2, "NonGaussianityIndicator", [1; 0]) %!error ... %! ReconstructionICA (ones (6, 5), 2, "NonGaussianityIndicator", {1, -1}) %!error ... %! ReconstructionICA (ones (6, 5), 2, "ContrastFcn", "bogus") %!error ... %! ReconstructionICA (ones (6, 5), 2, "bogus", 1) statistics-release-2.1.2/inst/Dimensionality_Reduction/SparseFiltering.m000066400000000000000000000303441527311104300265650ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftp {statistics} {} SparseFiltering ## ## Sparse filtering feature-extraction model. ## ## A @qcode{SparseFiltering} object stores the transformation learned by ## @code{sparsefilt} for extracting features from data. Create one with ## @code{sparsefilt}; apply it to data with the @code{transform} method. ## ## @seealso{sparsefilt, rica} ## @end deftp classdef SparseFiltering properties (SetAccess = protected) ## -*- texinfo -*- ## @deftp {SparseFiltering} {property} ModelParameters ## ## Options the fit used ## ## A scalar structure holding the options the fit ran with: ## @qcode{IterationLimit}, @qcode{Lambda}, @qcode{Standardize}, ## @qcode{InitialTransformWeights}, @qcode{GradientTolerance}, ## @qcode{StepTolerance} and @qcode{Solver}. ## This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {SparseFiltering} {property} NumPredictors ## ## Number of input predictors ## ## A positive integer @math{P}, the number of columns of the training ## data. This property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {SparseFiltering} {property} NumLearnedFeatures ## ## Number of learned features ## ## A positive integer @math{Q}, the number of features the learned ## transformation produces. This property is read-only. ## ## @end deftp NumLearnedFeatures = []; ## -*- texinfo -*- ## @deftp {SparseFiltering} {property} Mu ## ## Predictor means used when standardizing ## ## A column vector with one entry per predictor, the mean of each ## column of the training data. It is empty unless @qcode{'Standardize'} ## was true. This property is read-only. ## ## @end deftp Mu = []; ## -*- texinfo -*- ## @deftp {SparseFiltering} {property} Sigma ## ## Predictor standard deviations used when standardizing ## ## A column vector with one entry per predictor, the standard deviation ## of each column of the training data. It is empty unless ## @qcode{'Standardize'} was true. This property is read-only. ## ## @end deftp Sigma = []; ## -*- texinfo -*- ## @deftp {SparseFiltering} {property} FitInfo ## ## History of the fit ## ## A scalar structure with the fields @qcode{Iteration} and ## @qcode{Objective}, both column vectors of the same length. ## @qcode{Iteration} counts from zero and @qcode{Objective(1)} is the ## objective at the starting weights, so the last entry of each is the ## solution the fit returned. This property is read-only. ## ## The trajectory is this implementation's own. The default ## @qcode{'quasinewton'} solver minimises through Octave's @code{fminunc}, ## and @qcode{'Solver', 'lbfgs'} selects the limited-memory BFGS solver ## MATLAB uses. Either way the steps taken from the same starting weights ## differ from MATLAB's, so the length of the history and the iteration ## counts differ, and on an objective this far from convex the optimum ## reached need not be MATLAB's either. ## ## @end deftp FitInfo = []; ## -*- texinfo -*- ## @deftp {SparseFiltering} {property} TransformWeights ## ## Learned feature transformation weights ## ## A @math{P}-by-@math{Q} matrix of learned weights. The @code{transform} ## method applies it to data. This property is read-only. ## ## @end deftp TransformWeights = []; ## -*- texinfo -*- ## @deftp {SparseFiltering} {property} InitialTransformWeights ## ## Starting feature transformation weights ## ## A @math{P}-by-@math{Q} matrix, the weights the fit started from. It ## is the matrix given as @qcode{'InitialTransformWeights'} when one was ## given, and the random start the fit drew otherwise. This property is ## read-only. ## ## @end deftp InitialTransformWeights = []; endproperties methods ## -*- texinfo -*- ## @deftypefn {SparseFiltering} {@var{Mdl} =} SparseFiltering (@var{X}, @var{Q}, @dots{}) ## Fit a sparse filtering model. This constructor is invoked by ## @code{sparsefilt}; see @code{help sparsefilt} for the arguments. ## @end deftypefn function this = SparseFiltering (X, Q, varargin) if (nargin == 0) return; endif [n, p] = size (X); opts = struct ("IterationLimit", 1000, "Lambda", 1, ... "Standardize", false, ... "InitialTransformWeights", [], ... "GradientTolerance", 1e-6, "StepTolerance", 1e-6, ... "Solver", "quasinewton"); for k = 1:2:numel (varargin) name = varargin{k}; val = varargin{k+1}; switch (lower (name)) case 'iterationlimit' opts.IterationLimit = val; case 'lambda' opts.Lambda = val; case 'standardize' opts.Standardize = val; case 'initialtransformweights' opts.InitialTransformWeights = val; case 'gradienttolerance' opts.GradientTolerance = val; case 'steptolerance' opts.StepTolerance = val; case 'solver' if (! ischar (val) || ... ! any (strcmpi (val, {'quasinewton', 'lbfgs'}))) error ("sparsefilt: 'Solver' must be 'quasinewton' or 'lbfgs'."); endif opts.Solver = lower (val); otherwise error ("sparsefilt: unknown parameter name '%s'.", name); endswitch endfor if (opts.Standardize) ## MU and SIGMA are held as columns, one entry per predictor, as ## MATLAB holds them, and transposed where the data is scaled. this.Mu = mean (X)'; this.Sigma = std (X)'; sig = this.Sigma'; sig(sig == 0) = 1; X = (X - this.Mu') ./ sig; endif if (isempty (opts.InitialTransformWeights)) W0 = randn (p, Q); else W0 = opts.InitialTransformWeights; endif this.InitialTransformWeights = W0; lambda = opts.Lambda; ofun = @(wv) __sparsefilt_objective__ (wv, X, p, Q, lambda); if (strcmpi (opts.Solver, 'lbfgs')) ## LossTolerance is switched off because MATLAB has no such option ## here: the fit stops on the gradient or the step, never on the ## objective's own value. ## The history is sized to the problem rather than left at the ## engine's default ten. A transform carries only p * Q parameters, ## and once the stored pairs reach that count the recursion is full ## BFGS, which converges further here and in fewer iterations. The ## cap bounds the memory when Q is large. lbopts = struct ("IterationLimit", opts.IterationLimit, ... "GradientTolerance", opts.GradientTolerance, ... "StepTolerance", opts.StepTolerance, ... "LossTolerance", -Inf, ... "HistorySize", min (max (10, numel (W0)), 50)); [wv, lbinfo] = __lbfgs__ (ofun, W0(:), lbopts); fval = lbinfo.Fval; ## The engine records from the first step, where MATLAB's trajectory ## starts at the initial weights. f0 = ofun (W0(:)); obj = [f0; lbinfo.History.Fval]; else objective_history ("reset"); fmopts = optimset ("GradObj", "on", "MaxIter", opts.IterationLimit, ... "TolFun", 1e-10, "TolX", 1e-10, "Display", "off", ... "OutputFcn", @objective_history_fcn); [wv, fval, ~, output] = fminunc (ofun, W0(:), fmopts); obj = objective_history ("get"); endif this.TransformWeights = reshape (wv, p, Q); this.NumPredictors = p; this.NumLearnedFeatures = Q; ## MATLAB reports the whole trajectory: a column with the objective at ## the starting weights first, one entry per iteration after it, and ## Iteration the matching 0-based index. fminunc does not call its ## OutputFcn for the step it returns on, so the final value is appended. if (isempty (obj) || obj(end) != fval) obj(end+1, 1) = fval; endif this.FitInfo = struct ("Iteration", (0:numel (obj) - 1)', ... "Objective", obj); this.ModelParameters = opts; endfunction ## -*- texinfo -*- ## @deftypefn {SparseFiltering} {@var{Z} =} transform (@var{Mdl}, @var{X}) ## Transform data @var{X} into the learned feature space, returning the ## @math{N * Q} matrix @var{Z} of sparse features. ## @end deftypefn function Z = transform (this, X) if (nargin != 2) print_usage (); endif if (! isempty (this.Mu)) sig = this.Sigma'; sig(sig == 0) = 1; X = (X - this.Mu') ./ sig; endif Z = __sparsefilt_features__ (X, this.TransformWeights); endfunction endmethods endclassdef ## Soft-absolute features, normalized across examples then across features. function F = __sparsefilt_features__ (X, W) A = X * W; F = sqrt (A .^ 2 + 1e-8); F = F ./ sqrt (sum (F .^ 2, 1) + 1e-8); ## per-feature (column) normalization F = F ./ sqrt (sum (F .^ 2, 2) + 1e-8); ## per-example (row) normalization endfunction ## Sparse filtering objective (sum of features + L2 penalty) and its gradient. function [f, g] = __sparsefilt_objective__ (wv, X, p, Q, lambda) W = reshape (wv, p, Q); A = X * W; F = sqrt (A .^ 2 + 1e-8); cn = sqrt (sum (F .^ 2, 1) + 1e-8); Ft = F ./ cn; rn = sqrt (sum (Ft .^ 2, 2) + 1e-8); Fh = Ft ./ rn; f = sum (Fh(:)) + lambda * sum (W(:) .^ 2); if (nargout > 1) dFh = ones (size (Fh)); dFt = (dFh - sum (dFh .* Fh, 2) .* Fh) ./ rn; dF = (dFt - sum (dFt .* Ft, 1) .* Ft) ./ cn; dA = dF .* (A ./ F); g = X' * dA + 2 * lambda * W; g = g(:); endif endfunction %!shared X, W0 %! X = reshape (mod ((1:60)*7, 13), 12, 5) - 6; %! W0 = reshape (mod ((1:15)*3, 7), 5, 3) - 3; %!test %! ## Construct the object directly and check its properties. %! Mdl = SparseFiltering (X, 3, "InitialTransformWeights", W0, ... %! "Lambda", 1, "IterationLimit", 500); %! assert_equal (isa (Mdl, "SparseFiltering"), true); %! assert_equal (Mdl.NumPredictors, 5); %! assert_equal (Mdl.NumLearnedFeatures, 3); %! assert_equal (size (Mdl.TransformWeights), [5, 3]); %! assert_equal (Mdl.InitialTransformWeights, W0); %! assert_equal (isfield (Mdl.FitInfo, "Objective"), true); %!test %! ## The transform method returns nonnegative features in [0, 1]. %! Mdl = SparseFiltering (X, 3, "InitialTransformWeights", W0, ... %! "IterationLimit", 500); %! Z = transform (Mdl, X); %! assert_equal (size (Z), [12, 3]); %! assert_equal (all (Z(:) >= 0) && all (Z(:) <= 1 + 1e-12), true); %!test %! ## The transform method standardizes new data when the model does. %! Mdl = SparseFiltering (X, 2, "Standardize", true, "IterationLimit", 100, ... %! "InitialTransformWeights", W0(:,1:2)); %! Z = transform (Mdl, X); %! assert_equal (size (Z), [12, 2]); %! assert_equal (isempty (Mdl.Mu), false); %! assert_equal (size (Mdl.Mu), [5, 1]); %! assert_equal (size (Mdl.Sigma), [5, 1]); %!test %! ## The default constructor returns an empty model. %! Mdl = SparseFiltering (); %! assert_equal (isempty (Mdl.TransformWeights), true); %!error ... %! SparseFiltering (ones (6, 5), 2, "bogus", 1) statistics-release-2.1.2/inst/Dimensionality_Reduction/canoncorr.m000066400000000000000000000160011527311104300254420ustar00rootroot00000000000000## Copyright (C) 2016-2019 by Nir Krakauer ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{A}, @var{B}, @var{r}, @var{U}, @var{V}] =} canoncorr (@var{X}, @var{Y}) ## ## Canonical correlation analysis. ## ## Given @var{X} (size @var{k}*@var{m}) and @var{Y} (@var{k}*@var{n}), returns ## projection matrices of canonical coefficients @var{A} (size @var{m}*@var{d}, ## where @var{d} is the smallest of @var{m}, @var{n}, @var{d}) and @var{B} ## (size @var{m}*@var{d}); the canonical correlations @var{r} (1*@var{d}, ## arranged in decreasing order); the canonical variables @var{U}, @var{V} ## (both @var{k}*@var{d}, with orthonormal columns); and @var{stats}, ## a structure containing results from Bartlett's chi-square and Rao's F tests ## of significance. ## ## @seealso{princomp} ## @end deftypefn function [A,B,r,U,V,stats] = canoncorr (X,Y) k = size (X, 1); # should also be size (Y, 1) m = size (X, 2); n = size (Y, 2); X = center (X); Y = center (Y); ## Factor with column pivoting and work in the numerical rank. Without this ## a rank-deficient input is solved against a singular triangular factor: ## the coefficients run away to 1e15 and the canonical correlations come back ## wrong rather than merely imprecise. [Qx, Rx, px] = qr (X, 0); rankX = rank_of (Rx, k, m); if (rankX == 0) error ("canoncorr: X must contain at least one non-constant column."); elseif (rankX < m) warning ("canoncorr:NotFullRank", "canoncorr: X is not full rank."); Qx = Qx(:, 1:rankX); Rx = Rx(1:rankX, 1:rankX); endif [Qy, Ry, py] = qr (Y, 0); rankY = rank_of (Ry, k, n); if (rankY == 0) error ("canoncorr: Y must contain at least one non-constant column."); elseif (rankY < n) warning ("canoncorr:NotFullRank", "canoncorr: Y is not full rank."); Qy = Qy(:, 1:rankY); Ry = Ry(1:rankY, 1:rankY); endif d = min (rankX, rankY); [U S V] = svd (Qx' * Qy, 'econ'); A = Rx \ U(:, 1:d); B = Ry \ V(:, 1:d); ## A, B are scaled to make the covariance matrices of the outputs U, V ## identity matrices f = sqrt (k-1); A .*= f; B .*= f; ## Put the coefficients back to their full height in the original column ## order, the dropped columns contributing nothing. A(px, :) = [A; zeros(m - rankX, d)]; B(py, :) = [B; zeros(n - rankY, d)]; if (nargout > 2) r = max (0, min (diag (S)(1:d), 1))'; endif if (nargout > 3) U = X * A; endif if (nargout > 4) V = Y * B; endif if (nargout > 5) ## The degrees of freedom count the dimensions actually fitted, which is ## the rank rather than the number of columns supplied. Wilks = fliplr (cumprod (fliplr ((1 - r .^ 2)))); chisq = - (k - 1 - (rankX + rankY + 1)/2) * log (Wilks); df1 = (rankX - (1:d) + 1) .* (rankY - (1:d) + 1); pChisq = chi2cdf (chisq, df1, 'upper'); s = sqrt ((df1.^2 - 4) ./ ((rankX - (1:d) + 1).^2 + ... (rankY - (1:d) + 1).^2 - 5)); df2 = (k - 1 - (rankX + rankY + 1)/2) * s - df1/2 + 1; ls = Wilks .^ (1 ./ s); F = (1 ./ ls - 1) .* (df2 ./ df1); pF = fcdf (F, df1, df2, 'upper'); stats.Wilks = Wilks; stats.df1 = df1; stats.df2 = df2; stats.F = F; stats.pF = pF; stats.chisq = chisq; stats.pChisq = pChisq; endif endfunction ## Numerical rank of a triangular QR factor, on the scale of its leading entry. function rk = rank_of (R, nrows, ncols) if (isempty (R)) rk = 0; else rk = sum (abs (diag (R)) > eps (abs (R(1))) * max (nrows, ncols)); endif endfunction %!shared X, Y, A, B, r, U, V, k, Cuv %! k = 10; %! X = [1:k; sin(1:k); cos(1:k)]'; %! Y = [tan(1:k); tanh((1:k)/k)]'; %! [A, B, r, U, V, stats] = canoncorr (X, Y); %! Cuv = (U' * V) / (k - 1); %!assert_equal (diag (Cuv)', r, 10 * eps); %!assert_equal (diag (diag (Cuv)), Cuv, 2 * eps); %!assert_equal (r, [0.99590, 0.26754], 1E-5); %!assert_equal (U, center (X) * A, 10 * eps); %!assert_equal (V, center (Y) * B, 10 * eps); %!assert_equal (cov (U), eye (size (U, 2)), 10 * eps); %!assert_equal (cov (V), eye (size (V, 2)), 10 * eps); %! rand ('state', 1); [A, B, r] = canoncorr (rand (5, 10), rand (5, 20)); %! ## Four, not five: centring a five-row matrix leaves rank at most four, so %! ## there is no fifth canonical correlation to report. The count used to %! ## come from the number of rows rather than the rank. %!assert_equal (r, ones (1, 4), 10*eps); ## Rank-deficient input is reduced to its rank rather than solved against a ## singular factor. Verified against MATLAB R2024a. %!test %! Xr = [X(:,1), X(:,1), X(:,2)]; %! warning ("off", "canoncorr:NotFullRank", "local"); %! [Ar, Br, rr] = canoncorr (Xr, Y); %! ## the duplicated column contributes nothing %! assert_equal (Ar(2,:), zeros (1, columns (Ar))); %! ## and the coefficients stay finite, where they used to reach 1e15 %! assert_equal (all (isfinite (Ar(:))), true); %! assert_equal (max (abs (Ar(:))) < 1e3, true); %! ## dropping the duplicate leaves the same fit as never having had it %! [A2, B2, r2] = canoncorr (X(:,1:2), Y); %! assert_equal (rr, r2, 1e-12); %! assert_equal (Ar([1, 3], :), A2, 1e-10); %!test # a constant column carries no information and is dropped %! Xc = [X(:,1:2), ones(rows (X), 1)]; %! warning ("off", "canoncorr:NotFullRank", "local"); %! [Ac, Bc, rc] = canoncorr (Xc, Y); %! assert_equal (Ac(3,:), zeros (1, columns (Ac))); %! [A2, B2, r2] = canoncorr (X(:,1:2), Y); %! assert_equal (rc, r2, 1e-12); %!test # the deficiency is reported %! Xr = [X(:,1), X(:,1), X(:,2)]; %! fail ("canoncorr (Xr, Y)", "warning", "X is not full rank"); %!test # the returned coefficients satisfy the definition whatever the rank %! Xr = [X(:,1), X(:,1), X(:,2)]; %! warning ("off", "canoncorr:NotFullRank", "local"); %! [Ar, Br, rr, Ur, Vr] = canoncorr (Xr, Y); %! kk = rows (Xr); %! assert_equal ((Ur' * Ur) / (kk - 1), eye (numel (rr)), 1e-10); %! assert_equal ((Vr' * Vr) / (kk - 1), eye (numel (rr)), 1e-10); %! assert_equal ((Ur' * Vr) / (kk - 1), diag (rr), 1e-10); %!test %! ## Below the resolution of 1 - chi2cdf and 1 - fcdf, values from MATLAB %! ## R2024a %! u = (1:30)'; %! [~, ~, ~, ~, ~, st] = canoncorr (u, u + 0.01 * sin (u)); %! assert_equal ([st.pChisq, st.pF], ... %! [6.27210601481829e-87, 6.68262095310285e-88], -1e-7); %!error ... %! canoncorr (ones (10, 2), [tan(1:10); tanh((1:10)/10)]') statistics-release-2.1.2/inst/Dimensionality_Reduction/cmdscale.m000066400000000000000000000272451527311104300252450ustar00rootroot00000000000000## Copyright (C) 2014 JD Walsh ## Copyright (C) 2026 Avanish Salunke ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Y} =} cmdscale (@var{D}) ## @deftypefnx {statistics} {[@var{Y}, @var{e}] =} cmdscale (@var{D}) ## @deftypefnx {statistics} {@var{Y} =} cmdscale (@var{D}, @var{p}) ## @deftypefnx {statistics} {[@var{Y}, @var{e}] =} cmdscale (@var{D}, @var{p}) ## ## Classical multidimensional scaling of a matrix. ## ## Takes an @var{n} by @var{n} distance (or difference, similarity, or ## dissimilarity) matrix @var{D}. Returns @var{Y}, a matrix of @var{n} points ## with coordinates in @var{p} dimensional space which approximate those ## distances (or differences, similarities, or dissimilarities). Also returns ## the eigenvalues @var{e} of ## @code{@var{B} = -1/2 * @var{J} * (@var{D}.^2) * @var{J}}, where ## @code{J = eye(@var{n}) - ones(@var{n},@var{n})/@var{n}}. @var{p}, the number ## of columns of @var{Y}, is equal to the number of positive real eigenvalues of ## @var{B}, where an eigenvalue counts as positive only if it exceeds ## @code{eps (class (@var{D})) ^ (3/4)} times the largest eigenvalue magnitude, ## so that eigenvalues which are numerically zero in the precision of @var{D} ## are discarded. If @var{B} has no positive ## eigenvalues, which is the case when every point in @var{D} is coincident, ## @var{Y} is a single column of zeros. ## ## The optional argument @var{p} is a positive integer between 1 and @var{n} ## that specifies the maximum dimensionality of the desired embedding @var{Y}. ## If specified, @var{Y} will have at most @var{p} columns, and the returned ## eigenvalues @var{e} will be a vector of exactly length @var{p}. Specifying ## @var{p} can be useful for reducing dimensions for visualization (e.g., ## @code{@var{p} = ## 2}). ## ## @var{D} can be a full or sparse matrix or a vector of length ## @code{@var{n}*(@var{n}-1)/2} containing the upper triangular elements (like ## the output of the @code{pdist} function). It must be symmetric with ## non-negative entries whose values are further restricted by the type of ## matrix being represented: ## ## * If @var{D} is either a distance, dissimilarity, or difference matrix, then ## it must have zero entries along the main diagonal. In this case the points ## @var{Y} equal or approximate the distances given by @var{D}. ## ## * If @var{D} is a similarity matrix, the elements must all be less than or ## equal to one, with ones along the main diagonal. In this case the points ## @var{Y} equal or approximate the distances given by ## @code{@var{D} = sqrt(ones(@var{n},@var{n})-@var{D})}. ## ## @var{D} is a Euclidean matrix if and only if @var{B} is positive ## semi-definite. When this is the case, then @var{Y} is an exact representation ## of the distances given in @var{D}. If @var{D} is non-Euclidean, @var{Y} only ## approximates the distance given in @var{D}. The approximation used by ## @code{cmdscale} minimizes the statistical loss function known as ## @var{strain}. ## ## The returned @var{Y} is an @var{n} by @var{p} matrix showing possible ## coordinates of the points in @var{p} dimensional space ## (@code{@var{p} < @var{n}}). The columns correspond to the positive ## eigenvalues of @var{B} in descending order, or, when @var{B} has none, to ## its single zero eigenvalue. A translation, rotation, or reflection of the ## coordinates given by @var{Y} will satisfy the same distance matrix up to the ## limits of machine precision. ## ## For any @code{@var{k} <= @var{p}}, if the largest @var{k} positive ## eigenvalues of @var{B} are significantly greater in absolute magnitude than ## its other eigenvalues, the first @var{k} columns of @var{Y} provide a ## @var{k}-dimensional reduction of @var{Y} which approximates the distances ## given by @var{D}. The optional return @var{e} can be used to consider various ## values of @var{k}, or to evaluate the accuracy of specific dimension ## reductions (e.g., @code{@var{k} = 2}). ## ## Reference: Ingwer Borg and Patrick J.F. Groenen (2005), Modern ## Multidimensional Scaling, Second Edition, Springer, ISBN: 978-0-387-25150-9 ## (Print) 978-0-387-28981-6 (Online) ## ## @seealso{pdist} ## @end deftypefn function [Y, e] = cmdscale (D, p) ## Check for matrix input and valid number of arguments if (nargin < 1 || nargin > 2 || ! isnumeric (D) || ! ismatrix (D)) error ("cmdscale: input must be a numeric vector or matrix."); endif ## If vector, convert to matrix; otherwise, check for square symmetric input if (isvector (D)) D = squareform (D); elseif (! issquare (D) || norm (D - D', 1) > 0) error ("cmdscale: matrix input must be square symmetric."); endif n = size (D, 1); if (nargin > 1 && ! isempty (p)) if (! isscalar (p) || ! isreal (p) || p != round (p) || p < 1 || p > n) error ("cmdscale: p must be an integer between 1 and %d.", n); endif endif ## Check for valid format and if similarity matrix, convert if (any (any (D < 0))) error ("cmdscale: entries must be nonnegative."); elseif (trace (D) != 0) if (! all (diag (D) == 1) || ! all (D(:) <= 1)) error ("cmdscale: input must be a distance vector or matrix."); endif D = sqrt (ones (n,n) - D); endif ## Build centering matrix, perform double centering J = eye (n) - ones (n, n) / n; B = -1 / 2 * J * (D .^ 2) * J; B = (B + B') / 2; ## extract and sort eigenpairs [Q, e] = eig (B); etmp = diag (e); [etmp, ord] = sort (etmp, 'descend'); Q = Q(:, ord); ## determine total strictly positive eigenvalues tol = eps (class (etmp)) ^ (3/4) * max (abs (etmp)); n_pos = sum (etmp > tol); if (n_pos == n) n_pos = n - 1; elseif (n_pos == 0) n_pos = 1; endif if (nargin > 1 && ! isempty (p)) ## if p is given, eigenvalue output length is exactly p e = etmp(1:p); k = min (p, n_pos); else e = etmp; k = n_pos; endif ## build output matrix Y by safely slicing the top k elements etmp = etmp(1:k); Q = Q(:, 1:k); Y = Q * diag (sqrt (etmp)); [~, maxind] = max (abs (Y), [], 1); d = size (Y, 2); idx = maxind + (0 : n : (d - 1) * n); colsign = sign (Y(idx)); colsign(colsign == 0) = 1; Y = Y .* colsign; endfunction %!test %! m = randi (100) + 1; %! n = randi (100) + 1; %! X = rand (m, n); %! D = pdist (X); %! assert_equal (norm (pdist (cmdscale (D))), norm (D), sqrt (eps)); %! assert_equal (norm (pdist (cmdscale (squareform (D)))), norm (D), sqrt (eps)); %!test %! ## test output %! X = [ %! 0.8147, 0.1576, 0.6557, 0.7060; %! 0.9058, 0.9706, 0.0357, 0.0318; %! 0.1270, 0.9572, 0.8491, 0.2769; %! 0.9134, 0.4854, 0.9340, 0.0462; %! 0.6324, 0.8003, 0.6787, 0.0971 %! ]; %! D = pdist (X); %! p = 2; %! [Y, e] = cmdscale (D, p); %! expected_Y = [ %! 0.635444598081665, -0.209808014423477; %! -0.558655450609184, -0.457908993032377; %! -0.158680352453745, 0.622280326562354; %! 0.222509398493731, -0.047804408953240; %! -0.140618193512467, 0.093241089846740 %! ]; %! expected_e = [0.810349112746116; 0.651912015993974]; %! assert_equal (Y, expected_Y, 1e-14); %! assert_equal (e, expected_e, 1e-14); %!test %! ## basic dimentionality reduction %! D = [0 2 3; 2 0 4; 3 4 0]; %! [Y, e] = cmdscale (D, 2); %! assert_equal (size (Y, 2), 2); %! assert_equal (length (e), 2); %!test %! ## oversized dimension %! X = [0 0; 1 0; 0 1; 1 1]; %! D = pdist (X); %! [Y, e] = cmdscale (D, 3); %! assert_equal (size (Y, 2), 2); %! assert_equal (length (e), 3); %!test %! ## non euclidean distance. %! X = [1 2; 3 4; 5 6; 7 8; 9 10]; %! D = pdist (X, 'cityblock'); %! [Y, e] = cmdscale (D, 2); %! assert_equal (size (Y, 2), 1); %! assert_equal (length (e), 2); %!test %! ## compatability with p %! X = rand (10, 4); %! D = pdist (X); %! [Y, e] = cmdscale (D, 3); %! assert_equal (size (Y, 2), 3); %! assert_equal (length (e), 3); %! assert_equal (size (Y, 1), 10); %!test %! ## sign convention. %! rng (0, 'twister'); %! X = rand (10, 3); %! D = pdist (X); %! Y = cmdscale (D); %! [~, maxind] = max (abs (Y), [], 1); %! d = size (Y, 2); %! n = size (Y, 1); %! idx = maxind + (0 : n : (d - 1) * n); %! assert_equal (all (Y(idx) >= 0), true); %!test %! ## testing with p = n and without p %! rng (1, 'twister'); %! X = rand (10, 4); %! D = pdist (X); %! n_points = size (X, 1); %! [Y1, e1] = cmdscale (D); %! [Y2, e2] = cmdscale (D, n_points); %! assert_equal (size (Y1, 2), size (Y2, 2)); %!test %! ## Test that all coincident points (n_pos == 0) still give a single %! ## zero-filled column instead of an n-by-0 empty matrix %! D = zeros (4, 4); %! [Y, e] = cmdscale (D); %! assert_equal (size (Y), [4, 1]); %! assert_equal (Y, zeros (4, 1)); %! assert_equal (e, zeros (4, 1)); %!test %! ## Same boundary case at the smallest possible input, a single point %! [Y, e] = cmdscale (0); %! assert_equal (Y, zeros (2, 1)); %! assert_equal (e, zeros (2, 1)); %!test %! ## Coincident points give one column whatever p asks for %! [Y, e] = cmdscale (zeros (4, 4), 2); %! assert_equal (size (Y), [4, 1]); %! assert_equal (size (e), [2, 1]); %!test %! ## A similarity matrix of all ones is coincident after conversion %! [Y, e] = cmdscale (ones (3, 3)); %! assert_equal (Y, zeros (3, 1)); %! assert_equal (e, zeros (3, 1)); %!test %! ## A numerically flat dimension is not counted as a positive eigenvalue %! X = [0 0 0; 1 0 0; 0 1 0; 1 1 1e-7; 2 1 -1e-7; 1 3 2e-7]; %! D = pdist (X); %! [Y, e] = cmdscale (D); %! assert_equal (size (Y), [6, 2]); %! assert_equal (length (e), 6); %!test %! ## The cutoff lies between 1.78e-12 and 2.0e-12 times the largest eigenvalue %! X = [0 0 0; 1 0 0; 0 1 0; 1 1 1; 2 1 -1; 1 3 2]; %! Y1 = cmdscale (pdist ([X(:,1:2), 2.36477e-6 * X(:,3)])); %! Y2 = cmdscale (pdist ([X(:,1:2), 2.50489e-6 * X(:,3)])); %! assert_equal ([columns(Y1), columns(Y2)], [2, 3]); %!test %! ## The cutoff is relative to the largest eigenvalue magnitude, which here %! ## is a negative one %! H = hadamard (16); %! z = sqrt (2.6e-12) * H(:,11); %! X = [H(:,2:10), z]; %! D = sqrt (max (pdist (X) .^ 2 - pdist (sqrt (1.9) * H(:,12)) .^ 2, 0)); %! Y = cmdscale (D); %! assert_equal (columns (Y), 9); %!test %! ## The cutoff does not depend on the scale of D or the number of points %! X = [0 0 0; 1 0 0; 0 1 0; 1 1 1; 2 1 -1; 1 3 2]; %! X(:,3) = 3.16228e-6 * X(:,3); %! Y = cmdscale (pdist (1e3 * [X; X + [5 5 0]])); %! assert_equal (columns (Y), 2); %!test %! ## The cutoff follows the precision of single D, as in MATLAB R2024a %! X = [0 0 0; 1 0 0; 0 1 0; 1 1 3e-3]; %! Y1 = cmdscale (single (pdist (X))); %! X(4,3) = 1e-2; %! Y2 = cmdscale (single (pdist (X))); %! assert_equal (class (Y1), 'single'); %! assert_equal ([columns(Y1), columns(Y2)], [2, 3]); %!error cmdscale ({'not', 'a', 'matrix'}) %!error cmdscale (rand (3, 4)) %!error cmdscale (-ones (3)) %!error

cmdscale (eye (3), 0) %!error

cmdscale (eye (3), 4) %!error

cmdscale (eye (3), 1.5) %!error

cmdscale (eye (3), [1, 2]) %!error

cmdscale (eye (3), 2 + 1i) statistics-release-2.1.2/inst/Dimensionality_Reduction/doc-cache000066400000000000000000001717451527311104300250520ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 40 # name: # type: sq_string # elements: 1 # length: 17 ReconstructionICA # name: # type: sq_string # elements: 1 # length: 307 statistics: ReconstructionICA Reconstruction independent component analysis (RICA) feature-extraction model. A ReconstructionICA object stores the transformation learned by rica for extracting features from data. Create one with rica; apply it to data with the transform method. See also: rica, sparsefilt # name: # type: sq_string # elements: 1 # length: 78 Reconstruction independent component analysis (RICA) feature-extraction model. # name: # type: sq_string # elements: 1 # length: 25 ReconstructionICA.FitInfo # name: # type: sq_string # elements: 1 # length: 750 ReconstructionICA: property FitInfo History of the fit A scalar structure with the fields Iteration and Objective, both column vectors of the same length. Iteration counts from zero and Objective(1) is the objective at the starting weights, so the last entry of each is the solution the fit returned. This property is read-only. The trajectory is this implementation's own. The default 'quasinewton' solver minimises through Octave's fminunc, and 'Solver', 'lbfgs' selects the limited-memory BFGS solver MATLAB uses. Either way the steps taken from the same starting weights differ from MATLAB's, so the length of the history and the iteration counts differ, and on an objective this far from convex the optimum reached need not be MATLAB's either. # name: # type: sq_string # elements: 1 # length: 18 History of the fit # name: # type: sq_string # elements: 1 # length: 41 ReconstructionICA.InitialTransformWeights # name: # type: sq_string # elements: 1 # length: 288 ReconstructionICA: property InitialTransformWeights Starting feature transformation weights A P-by-Q matrix, the weights the fit started from. It is the matrix given as 'InitialTransformWeights' when one was given, and the random start the fit drew otherwise. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Starting feature transformation weights # name: # type: sq_string # elements: 1 # length: 33 ReconstructionICA.ModelParameters # name: # type: sq_string # elements: 1 # length: 295 ReconstructionICA: property ModelParameters Options the fit used A scalar structure holding the options the fit ran with: IterationLimit, Lambda, Standardize, ContrastFcn, InitialTransformWeights, GradientTolerance, StepTolerance, Solver and NonGaussianityIndicator. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Options the fit used # name: # type: sq_string # elements: 1 # length: 20 ReconstructionICA.Mu # name: # type: sq_string # elements: 1 # length: 234 ReconstructionICA: property Mu Predictor means used when standardizing A column vector with one entry per predictor, the mean of each column of the training data. It is empty unless 'Standardize' was true. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Predictor means used when standardizing # name: # type: sq_string # elements: 1 # length: 41 ReconstructionICA.NonGaussianityIndicator # name: # type: sq_string # elements: 1 # length: 461 ReconstructionICA: property NonGaussianityIndicator Non-Gaussianity of each learned feature A Q-by-1 vector of +1 and -1, one per learned feature: +1 where the feature is taken to be super-Gaussian and -1 where it is taken to be sub-Gaussian. The entry sets the sign its feature's contrast term carries in the objective, so the fit seeks a sparse feature where the entry is +1 and a spread one where it is -1. The default is all +1. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Non-Gaussianity of each learned feature # name: # type: sq_string # elements: 1 # length: 36 ReconstructionICA.NumLearnedFeatures # name: # type: sq_string # elements: 1 # length: 184 ReconstructionICA: property NumLearnedFeatures Number of learned features A positive integer Q, the number of features the learned transformation produces. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Number of learned features # name: # type: sq_string # elements: 1 # length: 31 ReconstructionICA.NumPredictors # name: # type: sq_string # elements: 1 # length: 163 ReconstructionICA: property NumPredictors Number of input predictors A positive integer P, the number of columns of the training data. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Number of input predictors # name: # type: sq_string # elements: 1 # length: 35 ReconstructionICA.ReconstructionICA # name: # type: sq_string # elements: 1 # length: 156 ReconstructionICA: Mdl = ReconstructionICA (X, Q, ...) Fit a reconstruction ICA model. This constructor is invoked by rica; see help rica for the arguments. # name: # type: sq_string # elements: 1 # length: 31 Fit a reconstruction ICA model. # name: # type: sq_string # elements: 1 # length: 23 ReconstructionICA.Sigma # name: # type: sq_string # elements: 1 # length: 265 ReconstructionICA: property Sigma Predictor standard deviations used when standardizing A column vector with one entry per predictor, the standard deviation of each column of the training data. It is empty unless 'Standardize' was true. This property is read-only. # name: # type: sq_string # elements: 1 # length: 53 Predictor standard deviations used when standardizing # name: # type: sq_string # elements: 1 # length: 34 ReconstructionICA.TransformWeights # name: # type: sq_string # elements: 1 # length: 217 ReconstructionICA: property TransformWeights Learned feature transformation weights A P-by-Q matrix of learned weights, its columns of unit length. The transform method applies it to data. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Learned feature transformation weights # name: # type: sq_string # elements: 1 # length: 27 ReconstructionICA.transform # name: # type: sq_string # elements: 1 # length: 132 ReconstructionICA: Z = transform (Mdl, X) Transform data X into the learned feature space, returning the N x Q matrix Z of features. # name: # type: sq_string # elements: 1 # length: 90 Transform data X into the learned feature space, returning the N * Q matrix Z of features. # name: # type: sq_string # elements: 1 # length: 15 SparseFiltering # name: # type: sq_string # elements: 1 # length: 279 statistics: SparseFiltering Sparse filtering feature-extraction model. A SparseFiltering object stores the transformation learned by sparsefilt for extracting features from data. Create one with sparsefilt; apply it to data with the transform method. See also: sparsefilt, rica # name: # type: sq_string # elements: 1 # length: 42 Sparse filtering feature-extraction model. # name: # type: sq_string # elements: 1 # length: 23 SparseFiltering.FitInfo # name: # type: sq_string # elements: 1 # length: 748 SparseFiltering: property FitInfo History of the fit A scalar structure with the fields Iteration and Objective, both column vectors of the same length. Iteration counts from zero and Objective(1) is the objective at the starting weights, so the last entry of each is the solution the fit returned. This property is read-only. The trajectory is this implementation's own. The default 'quasinewton' solver minimises through Octave's fminunc, and 'Solver', 'lbfgs' selects the limited-memory BFGS solver MATLAB uses. Either way the steps taken from the same starting weights differ from MATLAB's, so the length of the history and the iteration counts differ, and on an objective this far from convex the optimum reached need not be MATLAB's either. # name: # type: sq_string # elements: 1 # length: 18 History of the fit # name: # type: sq_string # elements: 1 # length: 39 SparseFiltering.InitialTransformWeights # name: # type: sq_string # elements: 1 # length: 286 SparseFiltering: property InitialTransformWeights Starting feature transformation weights A P-by-Q matrix, the weights the fit started from. It is the matrix given as 'InitialTransformWeights' when one was given, and the random start the fit drew otherwise. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Starting feature transformation weights # name: # type: sq_string # elements: 1 # length: 31 SparseFiltering.ModelParameters # name: # type: sq_string # elements: 1 # length: 255 SparseFiltering: property ModelParameters Options the fit used A scalar structure holding the options the fit ran with: IterationLimit, Lambda, Standardize, InitialTransformWeights, GradientTolerance, StepTolerance and Solver. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Options the fit used # name: # type: sq_string # elements: 1 # length: 18 SparseFiltering.Mu # name: # type: sq_string # elements: 1 # length: 232 SparseFiltering: property Mu Predictor means used when standardizing A column vector with one entry per predictor, the mean of each column of the training data. It is empty unless 'Standardize' was true. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Predictor means used when standardizing # name: # type: sq_string # elements: 1 # length: 34 SparseFiltering.NumLearnedFeatures # name: # type: sq_string # elements: 1 # length: 182 SparseFiltering: property NumLearnedFeatures Number of learned features A positive integer Q, the number of features the learned transformation produces. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Number of learned features # name: # type: sq_string # elements: 1 # length: 29 SparseFiltering.NumPredictors # name: # type: sq_string # elements: 1 # length: 161 SparseFiltering: property NumPredictors Number of input predictors A positive integer P, the number of columns of the training data. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Number of input predictors # name: # type: sq_string # elements: 1 # length: 21 SparseFiltering.Sigma # name: # type: sq_string # elements: 1 # length: 263 SparseFiltering: property Sigma Predictor standard deviations used when standardizing A column vector with one entry per predictor, the standard deviation of each column of the training data. It is empty unless 'Standardize' was true. This property is read-only. # name: # type: sq_string # elements: 1 # length: 53 Predictor standard deviations used when standardizing # name: # type: sq_string # elements: 1 # length: 31 SparseFiltering.SparseFiltering # name: # type: sq_string # elements: 1 # length: 162 SparseFiltering: Mdl = SparseFiltering (X, Q, ...) Fit a sparse filtering model. This constructor is invoked by sparsefilt; see help sparsefilt for the arguments. # name: # type: sq_string # elements: 1 # length: 29 Fit a sparse filtering model. # name: # type: sq_string # elements: 1 # length: 32 SparseFiltering.TransformWeights # name: # type: sq_string # elements: 1 # length: 187 SparseFiltering: property TransformWeights Learned feature transformation weights A P-by-Q matrix of learned weights. The transform method applies it to data. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Learned feature transformation weights # name: # type: sq_string # elements: 1 # length: 25 SparseFiltering.transform # name: # type: sq_string # elements: 1 # length: 137 SparseFiltering: Z = transform (Mdl, X) Transform data X into the learned feature space, returning the N x Q matrix Z of sparse features. # name: # type: sq_string # elements: 1 # length: 97 Transform data X into the learned feature space, returning the N * Q matrix Z of sparse features. # name: # type: sq_string # elements: 1 # length: 9 canoncorr # name: # type: sq_string # elements: 1 # length: 489 statistics: [A, B, r, U, V] = canoncorr (X, Y) Canonical correlation analysis. Given X (size k*m) and Y (k*n), returns projection matrices of canonical coefficients A (size m*d, where d is the smallest of m, n, d) and B (size m*d); the canonical correlations r (1*d, arranged in decreasing order); the canonical variables U, V (both k*d, with orthonormal columns); and stats, a structure containing results from Bartlett's chi-square and Rao's F tests of significance. See also: princomp # name: # type: sq_string # elements: 1 # length: 31 Canonical correlation analysis. # name: # type: sq_string # elements: 1 # length: 8 cmdscale # name: # type: sq_string # elements: 1 # length: 3219 statistics: Y = cmdscale (D) statistics: [Y, e] = cmdscale (D) statistics: Y = cmdscale (D, p) statistics: [Y, e] = cmdscale (D, p) Classical multidimensional scaling of a matrix. Takes an n by n distance (or difference, similarity, or dissimilarity) matrix D. Returns Y, a matrix of n points with coordinates in p dimensional space which approximate those distances (or differences, similarities, or dissimilarities). Also returns the eigenvalues e of B = -1/2 * J * (D.^2) * J, where J = eye(n) - ones(n,n)/n. p, the number of columns of Y, is equal to the number of positive real eigenvalues of B, where an eigenvalue counts as positive only if it exceeds eps (class (D)) ^ (3/4) times the largest eigenvalue magnitude, so that eigenvalues which are numerically zero in the precision of D are discarded. If B has no positive eigenvalues, which is the case when every point in D is coincident, Y is a single column of zeros. The optional argument p is a positive integer between 1 and n that specifies the maximum dimensionality of the desired embedding Y. If specified, Y will have at most p columns, and the returned eigenvalues e will be a vector of exactly length p. Specifying p can be useful for reducing dimensions for visualization (e.g., p = 2). D can be a full or sparse matrix or a vector of length n*(n-1)/2 containing the upper triangular elements (like the output of the pdist function). It must be symmetric with non-negative entries whose values are further restricted by the type of matrix being represented: * If D is either a distance, dissimilarity, or difference matrix, then it must have zero entries along the main diagonal. In this case the points Y equal or approximate the distances given by D. * If D is a similarity matrix, the elements must all be less than or equal to one, with ones along the main diagonal. In this case the points Y equal or approximate the distances given by D = sqrt(ones(n,n)-D). D is a Euclidean matrix if and only if B is positive semi-definite. When this is the case, then Y is an exact representation of the distances given in D. If D is non-Euclidean, Y only approximates the distance given in D. The approximation used by cmdscale minimizes the statistical loss function known as strain. The returned Y is an n by p matrix showing possible coordinates of the points in p dimensional space (p < n). The columns correspond to the positive eigenvalues of B in descending order, or, when B has none, to its single zero eigenvalue. A translation, rotation, or reflection of the coordinates given by Y will satisfy the same distance matrix up to the limits of machine precision. For any k <= p, if the largest k positive eigenvalues of B are significantly greater in absolute magnitude than its other eigenvalues, the first k columns of Y provide a k-dimensional reduction of Y which approximates the distances given by D. The optional return e can be used to consider various values of k, or to evaluate the accuracy of specific dimension reductions (e.g., k = 2). Reference: Ingwer Borg and Patrick J.F. Groenen (2005), Modern Multidimensional Scaling, Second Edition, Springer, ISBN: 978-0-387-25150-9 (Print) 978-0-387-28981-6 (Online) See also: pdist # name: # type: sq_string # elements: 1 # length: 47 Classical multidimensional scaling of a matrix. # name: # type: sq_string # elements: 1 # length: 8 factoran # name: # type: sq_string # elements: 1 # length: 4746 statistics: lambda = factoran (X, m) statistics: [lambda, psi] = factoran (X, m) statistics: [lambda, psi, T] = factoran (X, m) statistics: [lambda, psi, T, stats] = factoran (X, m) statistics: [lambda, psi, T, stats, F] = factoran (X, m) statistics: [...] = factoran (..., Name, Value) Common factor analysis. lambda = factoran (X, m) fits the common factor model with m common factors to the N-by-P data matrix X, whose rows are observations and columns are variables, and returns the P-by-m matrix lambda of factor loadings. The model is x = mu + lambda * f + e where f are the common factors and e the variable-specific errors, uncorrelated with each other and with f. The analysis is carried out on the correlation matrix, so the loadings are in standardized units and a variable's communality sum (lambda(i,:) .^ 2) and its specific variance psi(i) sum to one. [lambda, psi] = factoran (...) also returns the P-by-1 vector psi of specific variances. [lambda, psi, T] = factoran (...) also returns the m-by-m rotation matrix T that was applied to the loadings. It is the identity when 'Rotate' is 'none'. [lambda, psi, T, stats] = factoran (...) also returns a structure stats with the fields Field Description loglike the maximized log-likelihood, up to a constant. dfe the error degrees of freedom, ((p - m)^2 - p - m) / 2. chisq the likelihood ratio statistic testing m common factors against an unrestricted covariance. p the significance of chisq. The last two are present only when they can be computed: they are omitted when the degrees of freedom are not positive, when a specific variance has reached its lower bound (a Heywood case, where the likelihood is on the boundary), and for the 'paf' extraction, which does not maximize a likelihood. Test for them with isfield before using them. [lambda, psi, T, stats, F] = factoran (...) also returns the N-by-m matrix F of predicted factor scores. Scores are not available from a covariance matrix, only from data. Name-Value pairs Name Value 'Extraction' How the loadings are estimated, either 'ml' (default) for maximum likelihood or 'paf' for principal axis factoring. An Octave extension; MATLAB fits by maximum likelihood only. See the note below. 'Xtype' Whether X holds 'data' (default) or a 'covariance' (or correlation) matrix. 'Nobs' The number of observations behind a covariance matrix. Required for stats when 'Xtype' is 'covariance'. 'Delta' The lower bound on the specific variances, a scalar in [0, 1) (default 0.005). Bounding them away from zero keeps the likelihood finite; a solution that reaches the bound is a Heywood case and is reported by a warning. 'Rotate' The rotation applied to the loadings, passed to rotatefactors: 'varimax' (default), 'none', 'quartimax', 'equamax', 'parsimax', 'orthomax', or 'promax'. 'Normalize' Whether the rotation normalizes the rows of the loadings (Kaiser normalization), 'on' (default) or 'off'. 'Power' The exponent of the 'promax' target, a scalar not less than 1 (default 4). 'Scores' How F is predicted, either 'wls' (default, also named 'Bartlett') or 'regression' (also named 'Thomson'). 'Maxit' The iteration limit of the extraction (default 500). 'Tolerance' The convergence tolerance of the extraction (default 1e-8). Choosing the extraction The two extractions fit the same model but estimate it differently, and they answer to different circumstances. 'ml' maximizes the likelihood of a multivariate normal, and is what MATLAB's factoran does. Use it when you want the likelihood ratio test in stats to decide how many factors the data support, and when the data are plausibly normal. It can fail to converge, or push a specific variance to zero, when the model asks for more factors than the data hold. 'paf' iterates communalities on the reduced correlation matrix. It makes no distributional assumption and is stable where maximum likelihood struggles, which is why it remains available here, but it provides no likelihood and therefore no test: stats carries only dfe. The two agree closely when the model fits the data well and diverge when it does not, so a large difference between them is itself informative. Number of factors m must leave the model identified, that is (p - m)^2 >= p + m. With six variables at most three factors can be fitted, and only the smaller counts leave degrees of freedom to test. References Lawley, D. N., and Maxwell, A. E., Factor Analysis as a Statistical Method, 2nd Edition, Butterworths, 1971. Joreskog, K. G., "Some contributions to maximum likelihood factor analysis", Psychometrika 32(4), 443-482, 1967. Harman, H. H., Modern Factor Analysis, 3rd Edition, University of Chicago Press, 1976. See also: rotatefactors, pca, pcacov, princomp, barttest # name: # type: sq_string # elements: 1 # length: 23 Common factor analysis. # name: # type: sq_string # elements: 1 # length: 7 mdscale # name: # type: sq_string # elements: 1 # length: 3303 statistics: Y = mdscale (D, p) statistics: [Y, stress] = mdscale (D, p) statistics: [Y, stress, disparities] = mdscale (D, p) statistics: [...] = mdscale (..., Name, Value) Nonclassical (metric and nonmetric) multidimensional scaling. Y = mdscale (D, p) takes a matrix of dissimilarities D and returns a configuration Y of n points in p dimensions (an n x p matrix) whose interpoint distances approximate D, by minimizing a stress criterion. D may be given either as a full n x n symmetric matrix with zero diagonal, or as the vector of the n (n - 1) / 2 upper-triangle dissimilarities returned by pdist. [Y, stress, disparities] = mdscale (...) also returns the final value of the stress criterion and the disparities (the transformed dissimilarities the distances are fitted to). For the nonmetric criteria the disparities are the monotone (isotonic) regression of the dissimilarities onto the distances; for the metric criteria they are the dissimilarities themselves. Name/Value pairs: 'Criterion' The goodness-of-fit criterion to minimize, one of: 'stress' (default) Kruskal's normalized stress-1, sqrt (sum ((d - dhat)^2) / sum (d^2)), computed from disparities dhat (nonmetric). 'sstress' Squared stress, sqrt (sum ((d^2 - dhat^2)^2) / sum (d^4)) (nonmetric). 'metricstress' Metric stress, sqrt (sum ((d - delta)^2) / sum (delta^2)), fitting the dissimilarities delta directly. 'metricsstress' Metric squared stress, sqrt (sum ((d^2 - delta^2)^2) / sum (delta^4)). 'sammon' Sammon's nonlinear mapping criterion, (1 / sum (delta)) sum ((d - delta)^2 / delta). 'strain' The classical scaling criterion; equivalent to cmdscale. 'Weights' A matrix or vector of nonnegative weights, the same size as D, weighting each dissimilarity in the criterion. 'Start' The initial configuration: 'cmdscale' (default, classical scaling), 'random', or an explicit n x p matrix. 'Replicates' The number of times to repeat the minimization from different starting points, keeping the best (lowest-stress) result. The default is 1. 'Options' A structure of algorithm options (as returned by statset) whose MaxIter, TolFun, and TolX fields control the iterative minimization. Non-uniqueness of the solution A stress-minimizing configuration is defined only up to a translation, rotation, and reflection, because these leave all interpoint distances (and hence the stress) unchanged. mdscale removes this ambiguity by returning Y centred at the origin and rotated to its principal axes (with the largest-magnitude coordinate on each axis made positive), matching the convention used by MATLAB. Beyond that rigid ambiguity, the nonmetric criteria ('stress' and 'sstress') are non-convex and typically have several local minima; the one reached depends on the starting configuration and the details of the optimizer. As a result the returned configuration for these criteria may differ from the one another program (including MATLAB) reports even when the stress value agrees, and different runs may find configurations with slightly different stress. Use 'Replicates' with a 'random' start to search for a lower-stress solution. The metric criteria and 'strain' have an essentially unique solution and are reproducible up to the rigid ambiguity above. See also: cmdscale, pdist, squareform, procrustes, statset # name: # type: sq_string # elements: 1 # length: 61 Nonclassical (metric and nonmetric) multidimensional scaling. # name: # type: string # elements: 1 # length: 4 nnmf # name: # type: sq_string # elements: 1 # length: 1720 statistics: [W, H] = nnmf (A, K) statistics: [W, H, D] = nnmf (A, K) statistics: [...] = nnmf (..., Name, Value) Nonnegative matrix factorization. [W, H] = nnmf (A, K) factors the nonnegative N x M matrix A into nonnegative factors W (N x K) and H (K x M) whose product approximates A, by minimizing the root-mean-square residual between A and W * H. K, the number of factors, is typically smaller than N and M. [W, H, D] = nnmf (...) also returns the root-mean-square residual D, that is norm (A - W * H, "fro") / sqrt (N * M). The factorization is not unique: the returned factors are normalized so that the rows of H have unit length, and the columns of W (and the corresponding rows of H) are ordered by decreasing length of the columns of W. Because the objective is not convex, the iteration converges to a local minimum that depends on the starting point; use 'Replicates' to try several random starts and keep the best. Name/Value pairs: 'Algorithm' 'als' (default) for alternating least squares, or 'mult' for multiplicative updates. Alternating least squares usually converges faster and more reliably; multiplicative updates are more sensitive to the starting point. 'W0' An N x K initial value for W. 'H0' A K x M initial value for H. 'Replicates' The number of times to repeat the factorization from new random starting points, keeping the result with the smallest residual. The default is 1. Ignored for a starting point fixed by both 'W0' and 'H0'. 'Options' A structure of algorithm options (as returned by statset) whose MaxIter, TolFun, and TolX fields control the iteration. A MaxIter of 0 returns the starting factors, normalized, with their residual, where MATLAB fails. See also: pca, statset # name: # type: sq_string # elements: 1 # length: 33 Nonnegative matrix factorization. # name: # type: sq_string # elements: 1 # length: 3 pca # name: # type: sq_string # elements: 1 # length: 2761 statistics: coeff = pca (x) statistics: coeff = pca (x, Name, Value) statistics: [coeff, score, latent] = pca (...) statistics: [coeff, score, latent, tsquared] = pca (...) statistics: [coeff, score, latent, tsquared, explained, mu] = pca (...) Performs a principal component analysis on a data matrix. A principal component analysis of a data matrix of N observations in a D dimensional space returns a DxD transformation matrix, to perform a change of basis on the data. The first component of the new basis is the direction that maximizes the variance of the projected data. Input argument: x : a NxD data matrix The following Name, Value pair arguments can be used: 'Algorithm' defines the algorithm to use: 'svd' (default), for singular value decomposition 'eig' for eigenvalue decomposition 'Centered' is a boolean indicator for centering the observation data. It is true by default. 'Economy' is a boolean indicator for the economy size output. It is true by default. Hence, pca returns only the elements of latent that are not necessarily zero, and the corresponding columns of coeff and score, that is, when N <= D, only the first N - 1. 'NumComponents' defines the number of components k to return. If k < p, then only the first k columns of coeff and score are returned. 'Rows' defines how to handle missing values: 'complete' (default), missing values are removed before computation. 'pairwise' (only valid when 'Algorithm' is 'eig'), the covariance of rows with missing data is computed using the available data, but the covariance matrix could be not positive definite, which triggers the termination of pca. 'all', missing values are not allowed, pca terminates with an error if there are any. 'Weights' defines observation weights as a vector of positive values of length N. 'VariableWeights' defines variable weights: a vector of positive values of length D. the string 'variance' to use the sample variance as weights. Return values: coeff : the principal component coefficients, a DxD transformation matrix score : the principal component scores, the representation of x in the principal component space latent : the principal component variances, i.e., the eigenvalues of the covariance matrix of x tsquared : Hotelling's T-squared Statistic for each observation in x explained : the percentage of the variance explained by each principal component mu : the estimated mean of each variable of x, it is zero if the data are not centered Matlab compatibility note: the alternating least square method 'als' and associated options 'Coeff0', 'Score0', and 'Options' are not yet implemented References Jolliffe, I. T., Principal Component Analysis, 2nd Edition, Springer, 2002 See also: barttest, factoran, pcacov, pcares # name: # type: sq_string # elements: 1 # length: 57 Performs a principal component analysis on a data matrix. # name: # type: sq_string # elements: 1 # length: 6 pcacov # name: # type: sq_string # elements: 1 # length: 1296 statistics: coeff = pcacov (K) statistics: [coeff, latent] = pcacov (K) statistics: [coeff, latent, explained] = pcacov (K) Perform principal component analysis on covariance matrix coeff = pcacov (K) performs principal component analysis on the square covariance matrix K and returns the principal component coefficients, also known as loadings. The columns are in order of decreasing component variance. [coeff, latent] = pcacov (K) also returns a vector with the principal component variances, i.e. the eigenvalues of K. latent has a length of size (coeff, 1). [coeff, latent, explained] = pcacov (K) also returns a vector with the percentage of the total variance explained by each principal component. explained has the same size as latent. The entries in explained range from 0 (none of the variance is explained) to 100 (all of the variance is explained). pcacov does not standardize K to have unit variances. In order to perform principal component analysis on standardized variables, use the correlation matrix R = K ./ (SD * SD'), where SD = sqrt (diag (K)), in place of K. To perform principal component analysis directly on the data matrix, use pca. References Jolliffe, I. T., Principal Component Analysis, 2nd Edition, Springer, 2002 See also: barttest, factoran, pcares, pca # name: # type: sq_string # elements: 1 # length: 57 Perform principal component analysis on covariance matrix # name: # type: sq_string # elements: 1 # length: 6 pcares # name: # type: sq_string # elements: 1 # length: 1142 statistics: residuals = pcares (x, ndim) statistics: [residuals, reconstructed] = pcares (x, ndim) Calculate residuals from principal component analysis. residuals = pcares (x, ndim) returns the residuals obtained by retaining ndim principal components of the NxD matrix x. Rows of x correspond to observations, columns of x correspond to variables. ndim is a scalar and must be less than or equal to D. residuals is a matrix of the same size as x. Use the data matrix, not the covariance matrix, with this function. [residuals, reconstructed] = pcares (x, ndim) returns the reconstructed observations, i.e. the approximation to x obtained by retaining its first ndim principal components. pcares does not normalize the columns of x. Use pcares (zscore (x), ndim) in order to perform the principal components analysis based on standardized variables, i.e. based on correlations. Use pcacov in order to perform principal components analysis directly on a covariance or correlation matrix without constructing residuals. References Jolliffe, I. T., Principal Component Analysis, 2nd Edition, Springer, 2002 See also: factoran, pcacov, pca # name: # type: sq_string # elements: 1 # length: 54 Calculate residuals from principal component analysis. # name: # type: sq_string # elements: 1 # length: 4 ppca # name: # type: sq_string # elements: 1 # length: 2149 statistics: coeff = ppca (Y, K) statistics: [coeff, score] = ppca (Y, K) statistics: [coeff, score, pcvar] = ppca (Y, K) statistics: [coeff, score, pcvar, mu] = ppca (Y, K) statistics: [coeff, score, pcvar, mu, v] = ppca (Y, K) statistics: [coeff, score, pcvar, mu, v, S] = ppca (Y, K) statistics: [...] = ppca (..., Name, Value) Probabilistic principal component analysis. coeff = ppca (Y, K) fits a probabilistic principal component analysis (PPCA) model with K components to the N x P data matrix Y (rows are observations, columns are variables) and returns the P x K matrix coeff of orthonormal principal component coefficients, ordered by decreasing component variance. Y may contain NaN values marking missing observations; the model is fitted by an expectation-maximization algorithm that accounts for them. K must be a positive integer smaller than P. [coeff, score, pcvar, mu, v, S] = ppca (...) returns further outputs: score The N x K principal component scores (the data projected onto the components; missing entries are reconstructed from the model before projection). pcvar A K x 1 vector of the principal component variances (the variance explained by each component). mu A 1 x P vector of the estimated mean of Y. v The estimated residual (isotropic noise) variance. S A structure with the fitted model details: the loadings W, the expected scores Xexp, the reconstruction Recon, the number of iterations NumIter, and the root-mean-square residual RMSResid. Name/Value pairs control the fit: 'W0' A P x K initial value for the loadings used by the expectation-maximization algorithm (missing-data case). 'Options' A structure of algorithm options, as returned by statset, whose MaxIter, TolFun, and TolX fields set the maximum number of iterations and the convergence tolerances of the expectation-maximization algorithm. When Y has no missing values the model is fitted directly from the eigendecomposition of its covariance matrix; coeff, pcvar, and v are then the principal component directions, the leading variances, and the mean of the trailing variances, respectively. See also: pca, pcacov, pcares, factoran, barttest # name: # type: sq_string # elements: 1 # length: 43 Probabilistic principal component analysis. # name: # type: sq_string # elements: 1 # length: 8 princomp # name: # type: sq_string # elements: 1 # length: 1041 statistics: COEFF = princomp (X) statistics: [COEFF, SCORE] = princomp (X) statistics: [COEFF, SCORE, latent] = princomp (X) statistics: [COEFF, SCORE, latent, tsquare] = princomp (X) statistics: [...] = princomp (X, "econ") Performs a principal component analysis on a NxP data matrix X. COEFF : returns the principal component coefficients SCORE : returns the principal component scores, the representation of X in the principal component space LATENT : returns the principal component variances, i.e., the eigenvalues of the covariance matrix X. TSQUARE : returns Hotelling's T-squared Statistic for each observation in X [...] = princomp(X,'econ') returns only the elements of latent that are not necessarily zero, and the corresponding columns of COEFF and SCORE, that is, when n <= p, only the first n-1. This can be significantly faster when p is much larger than n. In this case the svd will be applied on the transpose of the data matrix X References Jolliffe, I. T., Principal Component Analysis, 2nd Edition, Springer, 2002 # name: # type: sq_string # elements: 1 # length: 63 Performs a principal component analysis on a NxP data matrix X. # name: # type: sq_string # elements: 1 # length: 10 procrustes # name: # type: sq_string # elements: 1 # length: 2164 statistics: d = procrustes (X, Y) statistics: d = procrustes (X, Y, param1, value1, ...) statistics: [d, Z] = procrustes (...) statistics: [d, Z, transform] = procrustes (...) Procrustes Analysis. d = procrustes (X, Y) computes a linear transformation of the points in the matrix Y to best conform them to the points in the matrix X by minimizing the sum of squared errors, as the goodness of fit criterion, which is returned in d as a dissimilarity measure. d is standardized by a measure of the scale of X, given by sum (sum ((X - repmat (mean (X, 1), size (X, 1), 1)) .^ 2, 1)) i.e., the sum of squared elements of a centered version of X. However, if X comprises repetitions of the same point, the sum of squared errors is not standardized. X and Y must have the same number of points (rows) and procrustes matches the i-th point in Y to the i-th point in X. Points in Y can have smaller dimensions (columns) than those in X, but not the opposite. Missing dimensions in Y are added with padding columns of zeros as necessary to match the the dimensions in X. [d, Z] = procrustes (X, Y) also returns the transformed values in Y. [d, Z, transform] = procrustes (X, Y) also returns the transformation that maps Y to Z. transform is a structure with fields: c the translation component T the orthogonal rotation and reflection component b the scale component So that Z = transform.b * Y * transform.T + transform.c procrustes can take two optional parameters as Name-Value pairs. [...] = procrustes (..., 'Scaling', false) computes a transformation that does not include scaling, that is transform.b = 1. Setting 'Scaling' to true includes a scaling component, which is the default. [...] = procrustes (..., 'Reflection', false) computes a transformation that does not include a reflection component, that is transform.T = 1. Setting 'Reflection' to true forces the solution to include a reflection component in the computed transformation, that is transform.T = -1. [...] = procrustes (..., 'Reflection', 'best') computes the best fit procrustes solution, which may or may not include a reflection component, which is the default. See also: cmdscale # name: # type: sq_string # elements: 1 # length: 20 Procrustes Analysis. # name: # type: string # elements: 1 # length: 7 relieff # name: # type: sq_string # elements: 1 # length: 5176 statistics: idx = relieff (X, Y, k) statistics: idx = relieff (X, Y, k, name, value) statistics: [idx, weights] = relieff (...) Rank the importance of predictors with the ReliefF or RReliefF algorithm. idx = relieff (X, Y, k) ranks the predictors, the columns of the matrix X, by how well they tell apart observations with different responses Y, judged by the k nearest neighbors of each observation. idx is a row vector of column indices, from the most important predictor to the least. X is a matrix of type double or single, with one row per observation. Y holds one response per row of X: a numeric vector, a logical vector, a categorical vector, a cell array of character vectors, a string array, or a character matrix with one row per observation. A numeric Y is a regression problem and any other type a classification problem, unless 'method' says otherwise. Rows with a missing value (NaN) in X or a missing response (NaN, an empty or blank name, or an undefined category) are left out. k is a positive integer. [idx, weights] = relieff (...) also returns the weight of each predictor, a row vector in the order of the columns of X, of type double. A larger weight marks a more important predictor, and weights range from -1 to 1. idx sorts weights in descending order. Predictors are compared on their range: for continuous predictors, the difference between two observations in predictor j is |x_rj - x_qj| divided by the range of predictor j, and the distance between them is the sum of these differences over all predictors (city block distance). A predictor holding a single value is left out of the distance and has weight NaN, ranked last. For categorical predictors the difference is 0 when two observations share a value and 1 otherwise. The neighbors of an observation are the k observations nearest to it, ties between equal distances going to the observation that comes first in X. Each neighbor counts with a weight that falls with its rank i among them as exp (-(i / sigma)^2), normalized to sum to 1 over the neighbors. For classification (ReliefF), each observation r has k nearest neighbors in its own class (hits) and k in each of the other classes (misses), or the whole class when it holds fewer. The weight of predictor j falls by the weighted difference to the hits and rises by the weighted difference to the misses of class c, scaled by p_c / (1 - p_r), p being the prior probabilities of the classes; each observation adds 1/m of its own share, m being the number of observations used. For regression (RReliefF), with dy the difference of two responses divided by the range of Y and dj the difference in predictor j, the weighted sums of dy, dj and dy dj over the k nearest neighbors of each observation are accumulated as W_dy, W_dj and W_dydj, and the weight of predictor j is W_dydj / W_dy - (W_dj - W_dydj) / (m - W_dy). The following Name-Value pairs are supported: Name Value 'method' 'classification' or 'regression'. The default is 'regression' for a numeric Y and 'classification' otherwise. A numeric Y may be classified, its distinct values being the classes, but only a numeric Y can be regressed. 'prior' The prior probabilities of the classes, for classification only: 'empirical', the class frequencies in Y, which is the default; 'uniform', equal for every class; a numeric vector with one value per class; or a structure with the fields group, the class names, and prob, their probabilities. A numeric vector follows the order of the classes in Y: sorted values for a numeric, logical or categorical Y, and order of first appearance for text. A structure must name every class of Y and may name others, which are ignored. The probabilities are normalized to sum to 1, and the observations of a class with prior probability 0 are left out. 'updates' The number of observations, drawn at random without replacement, for which neighbors are sought: a positive integer, or 'all', the default, for every observation. A number at least as large as the number of observations uses them all. 'categoricalx' 'on' to treat every predictor as categorical, or 'off', the default, to treat every predictor as continuous. 'sigma' The distance scaling factor of the rank weights of the neighbors, a positive scalar. The default is Inf for classification, weighing all neighbors alike, and 50 for regression. When X is single, the weights are computed in single precision and returned as double. The option names are not case sensitive but must be given in full, where MATLAB accepts any shortened name. Where MATLAB accepts a non-integer k or 'updates', answers for complex or infinite values in X, gives weights for a single class of responses, and returns NaN for a constant response, relieff refuses them. It also accepts numeric class names in a 'prior' structure, which MATLAB refuses. References: I. Kononenko, E. Simec and M. Robnik-Sikonja. Overcoming the myopia of inductive learning algorithms with RELIEFF. Applied Intelligence, 7(1):39--55, 1997. M. Robnik-Sikonja and I. Kononenko. Theoretical and empirical analysis of ReliefF and RReliefF. Machine Learning, 53(1-2):23--69, 2003. See also: pca, fitcknn, knnsearch # name: # type: sq_string # elements: 1 # length: 73 Rank the importance of predictors with the ReliefF or RReliefF algorithm. # name: # type: sq_string # elements: 1 # length: 4 rica # name: # type: sq_string # elements: 1 # length: 2676 statistics: Mdl = rica (X, Q) statistics: Mdl = rica (X, Q, Name, Value) Reconstruction independent component analysis (RICA) for feature extraction. Mdl = rica (X, Q) learns Q features from the N x P data matrix X (rows are observations, columns are predictors) and returns a ReconstructionICA object Mdl. Apply the learned transformation to data with transform (Mdl, X), which returns X * Mdl.TransformWeights. The P x Q weight matrix (with unit-length columns) minimizes the objective Lambda * ||X * W * W' - X||_F^2 + sum (sum (g (X * W))) over the transformation weights W, combining a reconstruction cost with a sparsity contrast g applied elementwise and selected by 'ContrastFcn'. Name/Value pairs: 'IterationLimit' Maximum number of iterations (default 1000). 'Lambda' Weight of the reconstruction term (default 1). 'Standardize' Logical; center and scale each predictor before fitting (default false). 'ContrastFcn' The sparsity contrast g applied to each element z of X x W: 'logcosh' (default), which is 0.5 x log (cosh (2 x z)); 'exp', which is -exp (-z^2 / 2); or 'sqrt', which is sqrt (z^2 + 1e-8), a smooth stand-in for abs (z). 'InitialTransformWeights' A P x Q initial value for the weights. The default is random. 'NonGaussianityIndicator' A Q-element vector of +1 and -1, one per learned feature, setting the sign its contrast term carries in the objective: +1 seeks a super-Gaussian (sparse) feature and -1 a sub-Gaussian (spread) one. The default is all +1. 'GradientTolerance', 'StepTolerance' Stop once the gradient's or the step's infinity norm falls to or below the given value (default 1e-6 each). They govern the fit only under 'Solver', 'lbfgs'; the 'quasinewton' solver runs to its own tighter internal tolerances and records these without acting on them. 'Solver' 'quasinewton' (default) minimizes through Octave's fminunc, which carries a full inverse Hessian. 'lbfgs' selects the limited-memory BFGS solver MATLAB uses, holding as many curvature pairs as the transform has parameters, and is several times faster here. It stops where 'GradientTolerance' and 'StepTolerance' say to, so a value tighter than the default carries it further. Note on reproducibility The RICA objective is not convex and is minimized by a quasi-Newton solver, so the learned weights depend on the starting point and the solver, and are only defined up to a permutation and sign of the feature columns. Different runs (or different software, including MATLAB) may return different weights that nonetheless describe an equally valid feature transformation. Fix 'InitialTransformWeights' for a reproducible result. See also: ReconstructionICA, sparsefilt, pca # name: # type: sq_string # elements: 1 # length: 76 Reconstruction independent component analysis (RICA) for feature extraction. # name: # type: sq_string # elements: 1 # length: 13 rotatefactors # name: # type: sq_string # elements: 1 # length: 2717 statistics: B = rotatefactors (A) statistics: B = rotatefactors (A, Name, Value, ...) statistics: [B, T] = rotatefactors (...) Rotate a factor-loading matrix. B = rotatefactors (A) rotates the D x M factor loadings matrix A (D observed variables, M factors) to the 'varimax' criterion and returns the rotated loadings B, the same size as A. [B, T] = rotatefactors (...) also returns the M x M rotation matrix T, so that B = A * T. For the orthogonal methods T is orthonormal (T' * T is the identity); for the oblique methods ('promax' and oblique 'procrustes') it is a general invertible matrix. The rotation is controlled by Name/Value pairs: 'Method' The rotation criterion, one of: 'varimax' (default) Orthomax with a criterion coefficient of 1; maximizes the variance of the squared loadings within each factor. 'quartimax' Orthomax with a coefficient of 0; simplifies the description of each variable. 'equamax' Orthomax with a coefficient of M / 2. 'parsimax' Orthomax with a coefficient of D (M - 1) / (D + M - 2). 'orthomax' General orthomax with the coefficient given by 'Coeff'. 'promax' Oblique rotation obtained by fitting an oblique transformation to a target built from a 'varimax' solution raised to the power 'Power'. 'procrustes' Rotation towards the 'Target' matrix, either orthogonal or oblique according to 'Type'. 'Normalize' 'on' (default) applies Kaiser normalization (each row of A is scaled to unit length before the orthomax rotation and unscaled afterwards); 'off' disables it. Ignored by 'procrustes'. 'Reltol' Relative convergence tolerance for the iterative orthomax rotation. The default is sqrt (eps). 'Maxit' Maximum number of iterations for the iterative orthomax rotation. The default is 250. 'Coeff' The orthomax coefficient used when 'Method' is 'orthomax'. The default is 1 (equivalent to 'varimax'). 'Power' The power used to build the 'promax' target, a scalar greater than or equal to 1. The default is 4. 'Target' The target loadings matrix for 'procrustes', the same size as A. Required for that method. 'Type' 'oblique' (default) or 'orthogonal', selecting the kind of 'procrustes' rotation. The default follows MATLAB, whose 'procrustes' rotation is oblique unless told otherwise. Note on the orthomax family: for coefficients up to 1 ('varimax', 'quartimax', and small 'orthomax') the rotation follows the same successive-SVD iteration as MATLAB and stops at the same relative tolerance. For larger coefficients ('equamax', 'parsimax') that iteration does not converge, so a monotonically convergent pairwise algorithm is used instead; it reaches the same optimum as MATLAB to that solution's own convergence precision. See also: factoran, pca, pcacov, procrustes # name: # type: sq_string # elements: 1 # length: 31 Rotate a factor-loading matrix. # name: # type: string # elements: 1 # length: 12 sequentialfs # name: # type: sq_string # elements: 1 # length: 5198 statistics: tf = sequentialfs (fun, X, y) statistics: tf = sequentialfs (fun, X1, ..., XN) statistics: tf = sequentialfs (..., name, value) statistics: [tf, history] = sequentialfs (...) Sequential feature selection. tf = sequentialfs (fun, X, y) selects the columns of X that best predict y, adding one column at a time, each time the one that lowers the cross-validated criterion computed by fun the most, until no column lowers it enough. tf is a logical row vector, true for each selected column of X. By default the criterion is estimated by 10-fold cross-validation, on a random partition of the rows that is not stratified. fun is a function handle called as criterion = fun (XTRAIN, ytrain, XTEST, ytest), where XTRAIN and ytrain are the training rows of X, restricted to the candidate columns, and of y, and XTEST and ytest the test rows. It returns a real scalar, which is typically a loss summed over the test rows: the values fun returns for every test set are summed and divided by the total number of test rows, so a loss averaged over the test rows must be multiplied by their number. Lower values are better. tf = sequentialfs (fun, X1, ..., XN) partitions the rows of every data argument alike, all of which must have the same number of rows, at least two, and selects columns of X1 only. fun is then called with the training parts of all of them followed by the test parts, fun (X1TRAIN, ..., XNTRAIN, X1TEST, ..., XNTEST). The data arguments are the leading arguments up to the first option name. The following Name-Value pairs are supported: Name Value 'CV' How the criterion is validated: a positive integer k, greater than 1 and not greater than the number of rows, for k-fold cross-validation on a random partition that is not stratified; a cvpartition object; 'resubstitution', calling fun (X, y, X, y) on all rows and dividing by their number; or 'none', calling fun (X, y) on all rows and taking its value as it is. The default is 10. 'MCReps' The number of Monte Carlo repetitions of the cross-validation, a positive integer, each on a new random partition, the criterion being summed over all of them before it is divided by the total number of test rows. The default is 1. It is set to 1, with a warning, when 'CV' is 'none' or 'resubstitution', or a cvpartition object for leave-one-out, resubstitution or a custom partition, none of which can be drawn anew. 'Direction' 'forward', the default, starts from no column and adds one at each step; 'backward' starts from all columns and removes one at each step. 'KeepIn' Columns always selected, given as a logical vector with one element per column of X or as column indices. The default is none. 'KeepOut' Columns never selected, given as 'KeepIn'. A column cannot be in both. 'NFeatures' The number of columns to select, a positive integer. The search then goes on until that many columns are selected, whatever the criterion and MaxIter. The default selects by the criterion alone. 'NullModel' true to let the selection hold no column, in which case fun must accept data arguments with no column; the default is false. A forward search then starts by evaluating the empty selection, and a backward search may remove the last column. 'Options' A structure, such as statset ('sequentialfs') returns, with the fields Display, 'off' (the default), 'final' or 'iter'; MaxIter, the largest number of steps, Inf by default; TolFun, the tolerance on the criterion, 1e-6 for a forward search and 0 for a backward search by default; and TolTypeFun, 'rel' (the default) or 'abs'. Missing or empty fields take their defaults. At each step every remaining column is tried and the one giving the lowest criterion is taken, the first of them in the order of the columns on a tie; a criterion of NaN is never taken over a number. The search stops when no column is left, after MaxIter steps, or when the new criterion c is greater than c_0 - TolFun (|c_0| + sqrt (eps)) for a relative tolerance, or than c_0 - TolFun for an absolute one, c_0 being the criterion before the step, so a step that leaves the criterion unchanged is taken when TolFun is 0. A criterion of NaN on either side never stops the search. The first column of a forward search is always added, unless 'NullModel' or 'KeepIn' gives a starting selection to compare it with. [tf, history] = sequentialfs (...) also returns a structure with the fields In, a logical matrix whose row i marks the columns selected after step i, and Crit, a row vector of the criterion after each step. The starting selection is the first step when it is evaluated: always for a backward search, and for a forward search when 'KeepIn' or 'NullModel' is given. Both fields are empty when nothing is evaluated. The option names are not case sensitive but must be given in full, where MATLAB accepts any shortened name. sequentialfs refuses an 'NFeatures' of 0, a 'CV' larger than the number of rows, a 'NullModel' other than true or false, and a criterion that is not a real number, all of which MATLAB accepts. Parallel computation is not supported, and the UseParallel, UseSubstreams and Streams options are ignored. See also: relieff, cvpartition, crossval, statset, stepwiselm # name: # type: sq_string # elements: 1 # length: 29 Sequential feature selection. # name: # type: sq_string # elements: 1 # length: 10 sparsefilt # name: # type: sq_string # elements: 1 # length: 2141 statistics: Mdl = sparsefilt (X, Q) statistics: Mdl = sparsefilt (X, Q, Name, Value) Sparse filtering for feature extraction. Mdl = sparsefilt (X, Q) learns Q features from the N x P data matrix X (rows are observations, columns are predictors) and returns a SparseFiltering object Mdl. Apply the learned transformation to data with transform (Mdl, X). The N x Q features returned by transform are the soft-absolute activations sqrt ((X * W) .^ 2 + 1e-8), normalized first across observations (each feature) and then across features (each observation). The P x Q weight matrix W minimizes the sum of those features plus an L2 penalty Lambda * ||W ||_F^2, driving the features to be sparse. Name/Value pairs: 'IterationLimit' Maximum number of iterations (default 1000). 'Lambda' Weight of the L2 penalty on the transform weights (default 1). 'Standardize' Logical; center and scale each predictor before fitting (default false). 'InitialTransformWeights' A P x Q initial value for the weights. The default is random. 'GradientTolerance', 'StepTolerance' Stop once the gradient's or the step's infinity norm falls to or below the given value (default 1e-6 each). They govern the fit only under 'Solver', 'lbfgs'; the 'quasinewton' solver runs to its own tighter internal tolerances and records these without acting on them. 'Solver' 'quasinewton' (default) minimizes through Octave's fminunc, which carries a full inverse Hessian. 'lbfgs' selects the limited-memory BFGS solver MATLAB uses, holding as many curvature pairs as the transform has parameters, and is several times faster here. It stops where 'GradientTolerance' and 'StepTolerance' say to, so a value tighter than the default carries it further. Note on reproducibility The sparse filtering objective is not convex and is minimized by a quasi-Newton solver, so the learned weights depend on the starting point and the solver. Different runs (or different software, including MATLAB) may return different weights that nonetheless describe an equally valid feature transformation. Fix 'InitialTransformWeights' for a reproducible result. See also: SparseFiltering, rica, pca # name: # type: sq_string # elements: 1 # length: 40 Sparse filtering for feature extraction. # name: # type: string # elements: 1 # length: 4 tsne # name: # type: sq_string # elements: 1 # length: 2976 statistics: Y = tsne (X) statistics: [Y, loss] = tsne (X) statistics: [...] = tsne (..., Name, Value) t-distributed stochastic neighbor embedding (t-SNE). Y = tsne (X) embeds the N x P data matrix X (rows are observations) into a low-dimensional space and returns the N x NumDimensions matrix Y of embedded points, whose pairwise (Student-t) affinities approximate the Gaussian affinities of the rows of X. [Y, loss] = tsne (...) also returns the Kullback-Leibler divergence loss between the two affinity distributions at the returned embedding. Name/Value pairs: 'Algorithm' 'exact' (default) forms the affinities and the gradient over every pair of points, which costs O(N^2) in time and memory at each iteration. 'barneshut' approximates both: the high-dimensional affinities are kept only over each point's 3 x Perplexity nearest neighbours, and the repulsive part of the gradient is summed over a space-partitioning tree of the embedding, giving O(N log N). Use it when N is large enough that the exact algorithm is slow or cannot allocate; on this machine the two cost the same at about N = 500 and 'barneshut' is eight times faster at N = 2000. The two do not return the same embedding, and their loss values are not comparable either: the divergence is summed over the pairs that carry an affinity, and 'barneshut' keeps far fewer of them. 'Theta' The tree opening criterion for 'barneshut', a non-negative scalar (default 0.5). A cell of the tree is collapsed to its centre of mass when its width is smaller than Theta times its distance from the point being pushed, so a larger value is faster and coarser. 0 collapses nothing and makes the repulsion exact, at O(N^2); note that this still leaves the affinities sparse, so it does not reproduce 'exact'. Ignored by 'exact'. 'Distance' The distance metric used for the high-dimensional affinities, as accepted by pdist (default 'euclidean'). 'NumDimensions' The dimension of the embedding Y (default min (P, 2)). 'NumPCAComponents' If positive, reduce X to this many principal components before embedding (default 0, no reduction). 'Standardize' Logical; center and scale each column of X before embedding (default false). 'Perplexity' The effective number of local neighbors (default 30). It must be smaller than N. 'Exaggeration' Tightness factor applied to the high-dimensional affinities for the first 100 iterations (default 4, no less than 1). 'LearnRate' The learning rate of the optimization (default 500). 'InitialY' An N x NumDimensions initial embedding (default 1e-4 * randn). 'Options' A structure (as returned by statset) whose MaxIter (default 1000) and TolFun (default 1e-10) fields control the optimization. A MaxIter of 0 returns the initial embedding with its loss, where MATLAB returns a loss of 0 or -Inf. The embedding is not unique: it depends on the initial configuration and the random state. Set 'InitialY' (or the random seed) for a reproducible result. See also: pca, pdist, statset # name: # type: sq_string # elements: 1 # length: 52 t-distributed stochastic neighbor embedding (t-SNE). statistics-release-2.1.2/inst/Dimensionality_Reduction/factoran.m000066400000000000000000000733521527311104300252670ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{lambda} =} factoran (@var{X}, @var{m}) ## @deftypefnx {statistics} {[@var{lambda}, @var{psi}] =} factoran (@var{X}, @var{m}) ## @deftypefnx {statistics} {[@var{lambda}, @var{psi}, @var{T}] =} factoran (@var{X}, @var{m}) ## @deftypefnx {statistics} {[@var{lambda}, @var{psi}, @var{T}, @var{stats}] =} factoran (@var{X}, @var{m}) ## @deftypefnx {statistics} {[@var{lambda}, @var{psi}, @var{T}, @var{stats}, @var{F}] =} factoran (@var{X}, @var{m}) ## @deftypefnx {statistics} {[@dots{}] =} factoran (@dots{}, @var{Name}, @var{Value}) ## ## Common factor analysis. ## ## @code{@var{lambda} = factoran (@var{X}, @var{m})} fits the common factor ## model with @var{m} common factors to the @math{N}-by-@math{P} data matrix ## @var{X}, whose rows are observations and columns are variables, and returns ## the @math{P}-by-@var{m} matrix @var{lambda} of factor loadings. The model is ## ## @example ## @var{x} = @var{mu} + @var{lambda} * @var{f} + @var{e} ## @end example ## ## @noindent ## where @var{f} are the common factors and @var{e} the variable-specific ## errors, uncorrelated with each other and with @var{f}. The analysis is ## carried out on the correlation matrix, so the loadings are in standardized ## units and a variable's communality @code{sum (@var{lambda}(i,:) .^ 2)} and ## its specific variance @code{@var{psi}(i)} sum to one. ## ## @code{[@var{lambda}, @var{psi}] = factoran (@dots{})} also returns the ## @math{P}-by-1 vector @var{psi} of specific variances. ## ## @code{[@var{lambda}, @var{psi}, @var{T}] = factoran (@dots{})} also returns ## the @var{m}-by-@var{m} rotation matrix @var{T} that was applied to the ## loadings. It is the identity when @qcode{'Rotate'} is @qcode{'none'}. ## ## @code{[@var{lambda}, @var{psi}, @var{T}, @var{stats}] = factoran (@dots{})} ## also returns a structure @var{stats} with the fields ## ## @multitable @columnfractions 0.15 0.8 ## @headitem Field @tab Description ## @item @qcode{loglike} @tab the maximized log-likelihood, up to a constant. ## @item @qcode{dfe} @tab the error degrees of freedom, ## @code{((@var{p} - @var{m})^2 - @var{p} - @var{m}) / 2}. ## @item @qcode{chisq} @tab the likelihood ratio statistic testing @var{m} ## common factors against an unrestricted covariance. ## @item @qcode{p} @tab the significance of @var{chisq}. ## @end multitable ## ## @noindent ## The last two are present only when they can be computed: they are omitted ## when the degrees of freedom are not positive, when a specific variance has ## reached its lower bound (a Heywood case, where the likelihood is on the ## boundary), and for the @qcode{'paf'} extraction, which does not maximize a ## likelihood. Test for them with @code{isfield} before using them. ## ## @code{[@var{lambda}, @var{psi}, @var{T}, @var{stats}, @var{F}] = factoran ## (@dots{})} also returns the @math{N}-by-@var{m} matrix @var{F} of predicted ## factor scores. Scores are not available from a covariance matrix, only from ## data. ## ## @subheading Name-Value pairs ## ## @multitable @columnfractions 0.18 0.77 ## @headitem Name @tab Value ## @item @qcode{'Extraction'} @tab How the loadings are estimated, either ## @qcode{'ml'} (default) for maximum likelihood or @qcode{'paf'} for principal ## axis factoring. @strong{An Octave extension}; MATLAB fits by maximum ## likelihood only. See the note below. ## ## @item @qcode{'Xtype'} @tab Whether @var{X} holds @qcode{'data'} (default) or ## a @qcode{'covariance'} (or correlation) matrix. ## ## @item @qcode{'Nobs'} @tab The number of observations behind a covariance ## matrix. Required for @var{stats} when @qcode{'Xtype'} is ## @qcode{'covariance'}. ## ## @item @qcode{'Delta'} @tab The lower bound on the specific variances, a ## scalar in @code{[0, 1)} (default @code{0.005}). Bounding them away from zero ## keeps the likelihood finite; a solution that reaches the bound is a Heywood ## case and is reported by a warning. ## ## @item @qcode{'Rotate'} @tab The rotation applied to the loadings, passed to ## @code{rotatefactors}: @qcode{'varimax'} (default), @qcode{'none'}, ## @qcode{'quartimax'}, @qcode{'equamax'}, @qcode{'parsimax'}, ## @qcode{'orthomax'}, or @qcode{'promax'}. ## ## @item @qcode{'Normalize'} @tab Whether the rotation normalizes the rows of ## the loadings (Kaiser normalization), @qcode{'on'} (default) or @qcode{'off'}. ## ## @item @qcode{'Power'} @tab The exponent of the @qcode{'promax'} target, a ## scalar not less than 1 (default 4). ## ## @item @qcode{'Scores'} @tab How @var{F} is predicted, either @qcode{'wls'} ## (default, also named @qcode{'Bartlett'}) or @qcode{'regression'} (also named ## @qcode{'Thomson'}). ## ## @item @qcode{'Maxit'} @tab The iteration limit of the extraction (default ## @code{500}). ## ## @item @qcode{'Tolerance'} @tab The convergence tolerance of the extraction ## (default @code{1e-8}). ## @end multitable ## ## @subheading Choosing the extraction ## ## The two extractions fit the same model but estimate it differently, and they ## answer to different circumstances. ## ## @qcode{'ml'} maximizes the likelihood of a multivariate normal, and is what ## MATLAB's @code{factoran} does. Use it when you want the likelihood ratio ## test in @var{stats} to decide how many factors the data support, and when ## the data are plausibly normal. It can fail to converge, or push a specific ## variance to zero, when the model asks for more factors than the data hold. ## ## @qcode{'paf'} iterates communalities on the reduced correlation matrix. It ## makes no distributional assumption and is stable where maximum likelihood ## struggles, which is why it remains available here, but it provides no ## likelihood and therefore no test: @var{stats} carries only @qcode{dfe}. ## ## The two agree closely when the model fits the data well and diverge when it ## does not, so a large difference between them is itself informative. ## ## @subheading Number of factors ## ## @var{m} must leave the model identified, that is ## @code{(@var{p} - @var{m})^2 >= @var{p} + @var{m}}. With six variables at ## most three factors can be fitted, and only the smaller counts leave degrees ## of freedom to test. ## ## @subheading References ## @enumerate ## @item ## Lawley, D. N., and Maxwell, A. E., @cite{Factor Analysis as a Statistical ## Method}, 2nd Edition, Butterworths, 1971. ## ## @item ## Joreskog, K. G., "Some contributions to maximum likelihood factor analysis", ## @cite{Psychometrika} 32(4), 443-482, 1967. ## ## @item ## Harman, H. H., @cite{Modern Factor Analysis}, 3rd Edition, University of ## Chicago Press, 1976. ## @end enumerate ## ## @seealso{rotatefactors, pca, pcacov, princomp, barttest} ## @end deftypefn function [lambda, psi, T, stats, F] = factoran (X, m, varargin) if (nargin < 2) print_usage (); endif ## ------------------------------------------------------------------ ## ## Options ## ------------------------------------------------------------------ ## extraction = "ml"; xtype = "data"; nobs = []; delta = 0.005; rotate = "varimax"; normalize = "on"; power = 4; scores = "wls"; maxit = 500; tolerance = 1e-8; if (mod (numel (varargin), 2) != 0) error ("factoran: Name-Value arguments must come in pairs."); endif for k = 1:2:numel (varargin) name = varargin{k}; if (! ischar (name)) error ("factoran: parameter name must be a character vector."); endif val = varargin{k+1}; switch (lower (name)) case "extraction" extraction = check_choice ("Extraction", val, {"ml", "paf"}); case "xtype" xtype = check_choice ("Xtype", val, {"data", "covariance"}); case "nobs" nobs = val; case "delta" delta = val; case "rotate" rotate = check_choice ("Rotate", val, {"none", "varimax", ... "quartimax", "equamax", "parsimax", "orthomax", "promax"}); case "normalize" normalize = check_choice ("Normalize", val, {"on", "off"}); case "power" power = val; case "scores" scores = check_choice ("Scores", val, ... {"wls", "bartlett", "regression", "thomson"}); case "maxit" maxit = val; case "tolerance" tolerance = val; otherwise error ("factoran: unknown parameter name '%s'.", name); endswitch endfor if (! (isscalar (delta) && isnumeric (delta) && isreal (delta) ... && delta >= 0 && delta < 1)) error ("factoran: DELTA must be a scalar in the range [0, 1)."); endif if (! (isscalar (maxit) && isnumeric (maxit) && maxit >= 1)) error ("factoran: MAXIT must be a positive integer."); endif if (! (isscalar (tolerance) && isnumeric (tolerance) && tolerance > 0)) error ("factoran: TOLERANCE must be a positive scalar."); endif ## The two spellings of each score predictor are the same thing if (strcmp (scores, "bartlett")) scores = "wls"; elseif (strcmp (scores, "thomson")) scores = "regression"; endif ## ------------------------------------------------------------------ ## ## Data or covariance ## ------------------------------------------------------------------ ## if (! (isnumeric (X) && isreal (X) && ismatrix (X) && ndims (X) == 2)) error ("factoran: X must be a real numeric matrix."); endif isdata = strcmp (xtype, "data"); if (isdata) if (any (isnan (X(:)))) error ("factoran: X must not contain missing values."); endif [n, p] = size (X); if (n < 2) error ("factoran: X must have at least two observations."); endif S = corr_from_data (X); else [p, pc] = size (X); if (p != pc) error ("factoran: a covariance matrix must be square."); endif if (any (any (abs (X - X') > sqrt (eps) * max (1, max (abs (X(:))))))) error ("factoran: a covariance matrix must be symmetric."); endif d = sqrt (diag (X)); if (any (d <= 0)) error ("factoran: a covariance matrix must have positive diagonal."); endif S = X ./ (d * d'); S = (S + S') / 2; n = nobs; endif if (! (isscalar (m) && isnumeric (m) && isreal (m) && m >= 0 ... && m == fix (m))) error ("factoran: M must be a non-negative integer."); endif dfe = ((p - m) ^ 2 - p - m) / 2; if (dfe < 0) error (strcat ("factoran: M is too large for %d variables; the model", ... " is not identified beyond %d factors."), ... p, max_factors (p)); endif ## ------------------------------------------------------------------ ## ## Extraction ## ------------------------------------------------------------------ ## heywood = false; if (m == 0) ## No common factors: the model is a diagonal covariance, which on a ## correlation matrix is the identity, so the fit tests independence. L = zeros (p, 0); psi = ones (p, 1); fmin = -log (det (S)); converged = true; elseif (strcmp (extraction, "ml")) [L, psi, fmin, converged] = ml_extract (S, m, delta, maxit, tolerance); if (! converged) warning (strcat ("factoran: maximum likelihood did not converge in", ... " %d iterations."), maxit); endif heywood = any (psi <= delta * (1 + 1e-8)); else [L, psi, converged] = paf_extract (S, m, maxit, tolerance); if (! converged) warning (strcat ("factoran: principal axis factoring did not", ... " converge in %d iterations."), maxit); endif fmin = NaN; heywood = any (psi <= 0); endif if (heywood) warning (strcat ("factoran: some specific variances are at their lower", ... " bound; the fit is a Heywood case.")); endif ## ------------------------------------------------------------------ ## ## Statistics. chisq and p exist only for a likelihood fit that is inside ## the parameter space with degrees of freedom to spare. ## ------------------------------------------------------------------ ## if (nargout > 3) stats = struct ("loglike", -fmin, "dfe", dfe); if ((strcmp (extraction, "ml") || m == 0) && dfe > 0 && ! heywood) if (isempty (n)) warning (strcat ("factoran: NOBS is needed to test the fit when X", ... " is a covariance matrix.")); else stats.chisq = (n - 1 - (2 * p + 5) / 6 - 2 * m / 3) * fmin; stats.p = chi2cdf (stats.chisq, dfe, 'upper'); endif endif endif ## ------------------------------------------------------------------ ## ## Rotation ## ------------------------------------------------------------------ ## if (strcmp (rotate, "none") || m <= 1) lambda = L; T = eye (m); else rargs = {"Method", rotate, "Normalize", normalize}; if (strcmp (rotate, "promax")) rargs = [rargs, {"Power", power}]; endif [lambda, T] = rotatefactors (L, rargs{:}); endif ## ------------------------------------------------------------------ ## ## Scores ## ------------------------------------------------------------------ ## if (nargout > 4) if (! isdata) error ("factoran: factor scores need data, not a covariance matrix."); endif Z = zscore_cols (X); if (strcmp (scores, "wls")) ## Bartlett: weighted least squares in the specific variances W = lambda ./ psi; F = Z * W / (lambda' * W); else ## Thomson: regression of the factors on the variables Sigma = lambda * lambda' + diag (psi); F = Z * (Sigma \ lambda); endif endif endfunction ## Largest number of factors leaving the model identified function mx = max_factors (p) mx = 0; for k = 1:p if (((p - k) ^ 2 - p - k) / 2 >= 0) mx = k; endif endfor endfunction ## Validate a string-valued option against its accepted values function out = check_choice (name, val, choices) if (! ischar (val)) error ("factoran: %s must be a character vector.", name); endif idx = find (strcmpi (val, choices), 1); if (isempty (idx)) error ("factoran: '%s' is not a valid value for %s.", val, name); endif out = choices{idx}; endfunction ## Correlation matrix of the columns of X function S = corr_from_data (X) Z = zscore_cols (X); S = (Z' * Z) / (rows (X) - 1); S = (S + S') / 2; S(1:(columns (S) + 1):end) = 1; endfunction ## Centre and scale the columns to unit variance function Z = zscore_cols (X) s = std (X, 0, 1); if (any (s == 0)) error ("factoran: X must not have a constant column."); endif Z = (X - mean (X, 1)) ./ s; endfunction ## The profile discrepancy of the maximum likelihood factor model at a given ## vector of specific variances, and the loadings it implies. Concentrating ## the likelihood on PSI this way is Joreskog's formulation: for a fixed PSI ## the loadings follow from the eigenvectors of the scaled correlation matrix, ## so only PSI has to be searched over. function [f, L] = ml_discrepancy (psi, S, m) sq = sqrt (psi(:)); Sstar = S ./ (sq * sq'); Sstar = (Sstar + Sstar') / 2; [V, D] = eig (Sstar); [theta, idx] = sort (diag (D), "descend"); V = V(:, idx); theta = max (theta, eps); f = sum (theta(m+1:end) - log (theta(m+1:end)) - 1); if (nargout > 1) L = (sq * ones (1, m)) .* V(:, 1:m) .* sqrt (max (theta(1:m)' - 1, 0)); endif endfunction ## Maximum likelihood extraction: minimize the profile discrepancy over the ## specific variances, held inside [DELTA, 1] by a logistic reparametrization ## so the search itself is unconstrained. function [L, psi, fmin, converged] = ml_extract (S, m, delta, maxit, tol) p = rows (S); ## Joreskog's starting values Sinv = pinv (S); psi0 = (1 - 0.5 * m / p) ./ diag (Sinv); psi0 = min (max (psi0, delta + 1e-6), 1 - 1e-6); u0 = logit ((psi0 - delta) / (1 - delta)); obj = @(u) ml_discrepancy (delta + (1 - delta) * sigmoid (u), S, m); opts = optimset ("MaxIter", maxit * 20, "MaxFunEvals", maxit * 200, ... "TolX", tol, "TolFun", tol); [uhat, fmin, flag] = fminsearch (obj, u0, opts); converged = (flag == 1); psi = delta + (1 - delta) * sigmoid (uhat); psi = min (max (psi(:), delta), 1); ## Polish on the stationarity condition of the model, that the fitted ## covariance reproduces the unit diagonal. The simplex search lands close ## to the optimum but leaves the condition satisfied only to about 1e-9, ## which shows up as the communalities and the specific variances not quite ## summing to one; iterating the condition drives it to machine precision. for it = 1:maxit [~, L] = ml_discrepancy (psi, S, m); psinew = min (max (1 - sum (L .^ 2, 2), delta), 1); if (max (abs (psinew - psi)) < eps * 8) psi = psinew; break; endif psi = psinew; endfor [fmin, L] = ml_discrepancy (psi, S, m); L = sign_convention (L); endfunction function y = sigmoid (u) y = 1 ./ (1 + exp (-u)); endfunction function u = logit (y) y = min (max (y, 1e-12), 1 - 1e-12); u = log (y ./ (1 - y)); endfunction ## Principal axis factoring: iterate the communalities on the reduced ## correlation matrix until they stop moving. function [L, psi, converged] = paf_extract (S, m, maxit, tol) p = rows (S); h2 = ones (p, 1); converged = false; L = zeros (p, m); for it = 1:maxit Rstar = S - diag (1 - h2); [V, D] = eig ((Rstar + Rstar') / 2); [ev, idx] = sort (diag (D), "descend"); V = V(:, idx); L = V(:, 1:m) .* sqrt (max (ev(1:m)', 0)); h2new = sum (L .^ 2, 2); if (max (abs (h2new - h2)) < tol) converged = true; h2 = h2new; break; endif h2 = h2new; endfor psi = max (1 - h2, 0); L = sign_convention (L); endfunction ## Make each column's largest-magnitude loading positive, so that a solution ## does not change sign between runs for no reason. function L = sign_convention (L) [~, idx] = max (abs (L), [], 1); for j = 1:columns (L) if (L(idx(j), j) < 0) L(:, j) = -L(:, j); endif endfor endfunction %!demo %! ## Six measured variables built from two underlying factors, plus noise. %! ## Factor analysis recovers the structure without being told it: the first %! ## three variables load on one factor and the last three on the other. %! rng (42); %! F = randn (300, 2); %! X = F * [0.8 0.1; 0.7 0.2; 0.75 0.15; 0.15 0.8; 0.2 0.7; 0.1 0.75]' ... %! + 0.6 * randn (300, 6); %! lambda = factoran (X, 2); %! printf ("loadings on the two rotated factors:\n"); %! disp (round (lambda * 1000) / 1000); %!demo %! ## How many factors do the data support? The likelihood ratio test in %! ## stats answers it. These data were built from two factors, and the test %! ## rejects one factor while accepting two. Three factors leave no degrees %! ## of freedom, so there is nothing left to test with. %! rng (42); %! F = randn (300, 2); %! X = F * [0.8 0.1; 0.7 0.2; 0.75 0.15; 0.15 0.8; 0.2 0.7; 0.1 0.75]' ... %! + 0.6 * randn (300, 6); %! for m = 1:3 %! [~, ~, ~, stats] = factoran (X, m); %! if (isfield (stats, "p")) %! printf ("%d factor(s): chisq = %8.3f, dfe = %d, p = %.4f\n", ... %! m, stats.chisq, stats.dfe, stats.p); %! else %! printf ("%d factor(s): nothing to test against (dfe = %d)\n", ... %! m, stats.dfe); %! endif %! endfor %!demo %! ## Rotation decides how a fit is presented, not how good it is. The %! ## unrotated solution puts most of the variance on a general first factor; %! ## varimax turns it so each variable loads mainly on one factor, which is %! ## easier to read. The specific variances are untouched either way. %! rng (42); %! F = randn (300, 2); %! X = F * [0.8 0.1; 0.7 0.2; 0.75 0.15; 0.15 0.8; 0.2 0.7; 0.1 0.75]' ... %! + 0.6 * randn (300, 6); %! [Lnone, psi_none] = factoran (X, 2, "Rotate", "none"); %! [Lvari, psi_vari, T] = factoran (X, 2, "Rotate", "varimax"); %! printf ("unrotated:\n"); disp (round (Lnone * 100) / 100); %! printf ("varimax:\n"); disp (round (Lvari * 100) / 100); %! printf ("the rotation matrix takes one to the other: %d\n", ... %! max (max (abs (Lnone * T - Lvari))) < 1e-10); %! printf ("specific variances unchanged: %d\n", ... %! max (abs (psi_none - psi_vari)) < 1e-10); %!demo %! ## Factor scores place each observation on the factors, so they can be %! ## plotted or used as inputs downstream. The two predictors optimise %! ## different things and are not equal, but they agree closely on the %! ## ordering of observations. %! rng (42); %! F = randn (300, 2); %! X = F * [0.8 0.1; 0.7 0.2; 0.75 0.15; 0.15 0.8; 0.2 0.7; 0.1 0.75]' ... %! + 0.6 * randn (300, 6); %! [~, ~, ~, ~, Fwls] = factoran (X, 2, "Scores", "wls"); %! [~, ~, ~, ~, Freg] = factoran (X, 2, "Scores", "regression"); %! printf ("first three observations, weighted least squares:\n"); %! disp (round (Fwls(1:3,:) * 1000) / 1000); %! printf ("first three observations, regression:\n"); %! disp (round (Freg(1:3,:) * 1000) / 1000); %! printf ("the two agree on factor 1 to a correlation of %.4f\n", ... %! corr (Fwls(:,1), Freg(:,1))); %!demo %! ## Two ways to estimate the same model. Maximum likelihood is the default %! ## and is what MATLAB does; principal axis factoring is an Octave %! ## extension that assumes no distribution. They agree closely when the %! ## model fits, so a large gap between them is a warning about the fit. %! ## Only maximum likelihood carries a test. %! rng (42); %! F = randn (300, 2); %! X = F * [0.8 0.1; 0.7 0.2; 0.75 0.15; 0.15 0.8; 0.2 0.7; 0.1 0.75]' ... %! + 0.6 * randn (300, 6); %! Lml = factoran (X, 2, "Extraction", "ml"); %! Lpaf = factoran (X, 2, "Extraction", "paf"); %! printf ("largest loading difference between the extractions: %.4f\n", ... %! max (abs (abs (Lml(:)) - abs (Lpaf(:))))); %! [~, ~, ~, sml] = factoran (X, 2, "Extraction", "ml"); %! [~, ~, ~, spaf] = factoran (X, 2, "Extraction", "paf"); %! printf ("ml reports: %s\n", strjoin (fieldnames (sml)', ", ")); %! printf ("paf reports: %s\n", strjoin (fieldnames (spaf)', ", ")); ## Reference values below are MATLAB R2024a's, on data generated by a ## deterministic stream so that both engines see identical bytes. %!shared X, Lref, Pref, Tref %! s = 7; u = zeros (3200, 1); %! for k = 1:3200 %! s = mod (16807 * s, 2147483647); %! u(k) = s / 2147483647; %! endfor %! u = min (max (u, 1e-12), 1 - 1e-12); %! a = u(1:2:end); b = u(2:2:end); r = sqrt (-2 * log (a)); %! z = zeros (3200, 1); %! z(1:2:end) = r .* cos (2 * pi * b); %! z(2:2:end) = r .* sin (2 * pi * b); %! Z = reshape (z(1:1600), 200, 8); %! X = Z(:,1:2) * [0.7 0.1; 0.6 0.2; 0.65 0.15; ... %! 0.15 0.7; 0.2 0.65; 0.1 0.6]' + 0.75 * Z(:,3:8); %! Lref = [0.23744646899123, 0.62572191026907; ... %! 0.1948126299518, 0.57314527421474; ... %! 0.038102881086025, 0.63721347868091; ... %! 0.76535682403077, 0.15782050760078; ... %! 0.54869037618166, 0.1041708573065; ... %! 0.52204369118599, 0.15800651296731]; %! Pref = [0.55209126537284; 0.63355253385654; 0.59250715304029; ... %! 0.38932161929025; 0.68808730357355; 0.70250432635276]; %! Tref = [0.78348287808137, 0.62141337268628; ... %! -0.62141337268628, 0.78348287808137]; %!test # the maximum likelihood fit, its rotation and its statistics %! [L, psi, T, stats] = factoran (X, 2); %! assert_equal (L, Lref, 1e-6); %! assert_equal (psi, Pref, 1e-6); %! assert_equal (T, Tref, 1e-6); %! assert_equal (stats.loglike, -0.0043677316938693, 1e-9); %! assert_equal (stats.dfe, 4); %! assert_equal (stats.chisq, 0.8509797250222, 1e-6); %! assert_equal (stats.p, 0.93148578937045, 1e-8); %!test # a single factor, where the model does not fit and the test says so %! [L, psi, T, stats] = factoran (X, 1); %! assert_equal (L, [0.58734985138995; 0.52528039923063; 0.42927173838555; ... %! 0.5943390974845; 0.47167200636347; ... %! 0.49418848792238], 1e-6); %! assert_equal (stats.dfe, 9); %! assert_equal (stats.chisq, 51.130175056732, 1e-5); %! assert_equal (stats.p < 1e-6, true); %! assert_equal (T, 1); %!test # rotation changes the loadings but not the fit %! [Ln, psin, Tn] = factoran (X, 2, "Rotate", "none"); %! assert_equal (Ln, [0.57486720553951, 0.34268999200789; ... %! 0.50879249789022, 0.32799033558027; ... %! 0.42582593184473, 0.47556821038442; ... %! 0.69771574115811, -0.3519532998141; ... %! 0.49462267888081, -0.25934745412885; ... %! 0.50719965378403, -0.20060953329425], 1e-6); %! assert_equal (Tn, eye (2)); %! assert_equal (psin, Pref, 1e-6); %! [~, psiv] = factoran (X, 2, "Rotate", "varimax"); %! assert_equal (psiv, psin, 1e-10); %!test # the rotation matrix is the one that was applied %! [Ln, ~, ~] = factoran (X, 2, "Rotate", "none"); %! [Lv, ~, T] = factoran (X, 2, "Rotate", "varimax"); %! assert_equal (Ln * T, Lv, 1e-8); %!test # quartimax, another orthogonal rotation %! L = factoran (X, 2, "Rotate", "quartimax"); %! assert_equal (L, [0.24417499900219, 0.62312703719984; ... %! 0.20097710825146, 0.57101284407823; ... %! 0.044966808556795, 0.63676591702752; ... %! 0.76701294807206, 0.14956442825637; ... %! 0.54978098993064, 0.098252529419005; ... %! 0.52371594497243, 0.15237218456401], 1e-5); %!test # communality and specific variance partition the unit variance %! [L, psi] = factoran (X, 2); %! assert_equal (sum (L .^ 2, 2) + psi, ones (6, 1), 1e-10); %!test # both score predictors, and the default is the weighted least squares %! [~, ~, ~, ~, Fw] = factoran (X, 2, "Scores", "wls"); %! assert_equal (Fw(1:2,:), [0.8685910477393, 3.3629807100209; ... %! -0.03713794537533, -1.9311992150794], 1e-5); %! [~, ~, ~, ~, Fr] = factoran (X, 2, "Scores", "regression"); %! assert_equal (Fr(1:2,:), [0.94397703443356, 2.2276228684448; ... %! -0.22623256144569, -1.2312105486235], 1e-5); %! [~, ~, ~, ~, Fd] = factoran (X, 2); %! assert_equal (Fd, Fw, 1e-12); %! [~, ~, ~, ~, Fb] = factoran (X, 2, "Scores", "Bartlett"); %! assert_equal (Fb, Fw, 1e-12); %! [~, ~, ~, ~, Ft] = factoran (X, 2, "Scores", "Thomson"); %! assert_equal (Ft, Fr, 1e-12); %!test # a correlation matrix gives the same fit as the data behind it %! [L, psi, T, stats] = factoran (corr (X), 2, "Xtype", "covariance", ... %! "Nobs", 200); %! assert_equal (L, Lref, 1e-6); %! assert_equal (psi, Pref, 1e-6); %! assert_equal (stats.chisq, 0.85097972502237, 1e-6); %!test # principal axis factoring is available and agrees where the fit is good %! Lml = factoran (X, 2, "Extraction", "ml"); %! Lpaf = factoran (X, 2, "Extraction", "paf"); %! assert_equal (size (Lpaf), [6, 2]); %! assert_equal (max (abs (abs (Lml(:)) - abs (Lpaf(:)))) < 0.15, true); %!test # but it reports no likelihood, so no test comes with it %! [~, ~, ~, s] = factoran (X, 2, "Extraction", "paf"); %! assert_equal (isnan (s.loglike), true); %! assert_equal (s.dfe, 4); %! assert_equal (isfield (s, "chisq"), false); %! assert_equal (isfield (s, "p"), false); %!test # the degrees of freedom follow the variable and factor counts %! for m = 1:3 %! [~, ~, ~, s] = factoran (X, m); %! assert_equal (s.dfe, ((6 - m) ^ 2 - 6 - m) / 2); %! endfor %!test # with no degrees of freedom there is nothing to test %! [~, ~, ~, s] = factoran (X, 3); %! assert_equal (s.dfe, 0); %! assert_equal (isfield (s, "chisq"), false); %!test # no common factors is a model too, and it tests independence %! [L, psi, T, stats] = factoran (X, 0); %! assert_equal (size (L), [6, 0]); %! assert_equal (psi, ones (6, 1)); %! assert_equal (stats.dfe, 15); %! assert_equal (stats.loglike, log (det (corr (X))), 1e-10); %! assert_equal (stats.p < 1e-6, true); %!test # the loadings do not change sign between runs %! L1 = factoran (X, 2); %! L2 = factoran (X, 2); %! assert_equal (L1, L2); ## Test input validation %!error factoran (1) %!error ... %! factoran (rand (20, 5), 1.5) %!error ... %! factoran (rand (20, 5), -1) %!error factoran (rand (30, 6), 4) %!error factoran ("abc", 1) %!error ... %! factoran ([rand(20, 4); NaN(1, 4)], 1) %!error ... %! factoran ([rand(20, 3), ones(20, 1)], 1) %!error ... %! factoran (rand (1, 5), 1) %!error ... %! factoran (rand (20, 5), 1, "Rotate") %!error ... %! factoran (rand (20, 5), 1, "bogus", 1) %!error <'nosuch' is not a valid value for Extraction.> ... %! factoran (rand (20, 5), 1, "Extraction", "nosuch") %!error <'nosuch' is not a valid value for Rotate.> ... %! factoran (rand (20, 5), 1, "Rotate", "nosuch") %!error <'nosuch' is not a valid value for Scores.> ... %! factoran (rand (20, 5), 1, "Scores", "nosuch") %!error ... %! factoran (rand (20, 5), 1, "Delta", 1) %!error ... %! factoran (rand (5, 4), 1, "Xtype", "covariance") %!error ... %! [a, b, c, d, e] = factoran (corr (rand (20, 5)), 1, "Xtype", "covariance"); statistics-release-2.1.2/inst/Dimensionality_Reduction/mdscale.m000066400000000000000000000546101527311104300250760ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Y} =} mdscale (@var{D}, @var{p}) ## @deftypefnx {statistics} {[@var{Y}, @var{stress}] =} mdscale (@var{D}, @var{p}) ## @deftypefnx {statistics} {[@var{Y}, @var{stress}, @var{disparities}] =} mdscale (@var{D}, @var{p}) ## @deftypefnx {statistics} {[@dots{}] =} mdscale (@dots{}, @var{Name}, @var{Value}) ## ## Nonclassical (metric and nonmetric) multidimensional scaling. ## ## @code{@var{Y} = mdscale (@var{D}, @var{p})} takes a matrix of dissimilarities ## @var{D} and returns a configuration @var{Y} of @var{n} points in @var{p} ## dimensions (an @math{@var{n} * @var{p} } matrix) whose interpoint distances ## approximate @var{D}, by minimizing a stress criterion. @var{D} may be given ## either as a full @math{@var{n} * @var{n}} symmetric matrix with zero ## diagonal, or as the vector of the @math{@var{n} (@var{n} - 1) / 2} ## upper-triangle dissimilarities returned by @code{pdist}. ## ## @code{[@var{Y}, @var{stress}, @var{disparities}] = mdscale (@dots{})} also ## returns the final value of the @var{stress} criterion and the ## @var{disparities} (the transformed dissimilarities the distances are fitted ## to). For the nonmetric criteria the disparities are the monotone ## (isotonic) regression of the dissimilarities onto the distances; for the ## metric criteria they are the dissimilarities themselves. ## ## Name/Value pairs: ## ## @table @asis ## @item @qcode{'Criterion'} ## The goodness-of-fit criterion to minimize, one of: ## ## @table @asis ## @item @qcode{'stress'} (default) ## Kruskal's normalized stress-1, @math{sqrt (sum ((d - dhat)^2) / sum (d^2))}, ## computed from disparities @var{dhat} (nonmetric). ## ## @item @qcode{'sstress'} ## Squared stress, @math{sqrt (sum ((d^2 - dhat^2)^2) / sum (d^4))} (nonmetric). ## ## @item @qcode{'metricstress'} ## Metric stress, @math{sqrt (sum ((d - delta)^2) / sum (delta^2))}, fitting the ## dissimilarities @var{delta} directly. ## ## @item @qcode{'metricsstress'} ## Metric squared stress, @math{sqrt (sum ((d^2 - delta^2)^2) / sum (delta^4))}. ## ## @item @qcode{'sammon'} ## Sammon's nonlinear mapping criterion, ## @math{(1 / sum (delta)) sum ((d - delta)^2 / delta)}. ## ## @item @qcode{'strain'} ## The classical scaling criterion; equivalent to @code{cmdscale}. ## @end table ## ## @item @qcode{'Weights'} ## A matrix or vector of nonnegative weights, the same size as @var{D}, ## weighting each dissimilarity in the criterion. ## ## @item @qcode{'Start'} ## The initial configuration: @qcode{'cmdscale'} (default, classical scaling), ## @qcode{'random'}, or an explicit @math{@var{n} * @var{p} } matrix. ## ## @item @qcode{'Replicates'} ## The number of times to repeat the minimization from different starting ## points, keeping the best (lowest-stress) result. The default is 1. ## ## @item @qcode{'Options'} ## A structure of algorithm options (as returned by @code{statset}) whose ## @qcode{MaxIter}, @qcode{TolFun}, and @qcode{TolX} fields control the ## iterative minimization. ## @end table ## ## @subheading Non-uniqueness of the solution ## ## A stress-minimizing configuration is defined only up to a translation, ## rotation, and reflection, because these leave all interpoint distances (and ## hence the stress) unchanged. @code{mdscale} removes this ambiguity by ## returning @var{Y} centred at the origin and rotated to its principal axes ## (with the largest-magnitude coordinate on each axis made positive), matching ## the convention used by MATLAB. ## ## Beyond that rigid ambiguity, the @strong{nonmetric} criteria ## (@qcode{'stress'} and @qcode{'sstress'}) are non-convex and typically have ## several local minima; the one reached depends on the starting configuration ## and the details of the optimizer. As a result the returned configuration for ## these criteria may differ from the one another program (including MATLAB) ## reports even when the @strong{stress value} agrees, and different runs may ## find configurations with slightly different stress. Use @qcode{'Replicates'} ## with a @qcode{'random'} start to search for a lower-stress solution. The ## metric criteria and @qcode{'strain'} have an essentially unique solution and ## are reproducible up to the rigid ambiguity above. ## ## @seealso{cmdscale, pdist, squareform, procrustes, statset} ## @end deftypefn function [Y, stress, disparities] = mdscale (D, p, varargin) ## Input validation if (nargin < 2) print_usage (); endif ## Resolve D into the vector of dissimilarities DELTA and the count N if (isvector (D)) delta = D(:); m = numel (delta); n = (1 + sqrt (1 + 8 * m)) / 2; if (n != fix (n)) error (strcat ("mdscale: the length of a dissimilarity vector D must", ... " be N*(N-1)/2 for some integer N.")); endif elseif (ismatrix (D) && ndims (D) == 2 && rows (D) == columns (D)) n = rows (D); delta = D(tril (true (n), -1)); m = numel (delta); else error ("mdscale: D must be a dissimilarity vector or a square matrix."); endif if (! (isnumeric (delta) && isreal (delta)) || any (delta < 0)) error ("mdscale: D must contain real, nonnegative dissimilarities."); endif if (! (isscalar (p) && isnumeric (p) && p >= 1 && p == fix (p) && p < n)) error ("mdscale: P must be a positive integer smaller than N."); endif ## Defaults and Name/Value parsing Criterion = "stress"; Weights = []; Start = "cmdscale"; Replicates = 1; opts = statset ("mdscale"); args = varargin; if (mod (numel (args), 2) != 0) error ("mdscale: Name/Value arguments must come in pairs."); endif for k = 1:2:numel (args) name = args{k}; val = args{k+1}; if (! (ischar (name) && isrow (name))) error ("mdscale: parameter names must be character vectors."); endif switch (lower (name)) case 'criterion' Criterion = lower (val); case 'weights' Weights = val; case 'start' Start = val; case 'replicates' Replicates = val; case 'options' if (! isstruct (val)) error ("mdscale: 'Options' must be a structure."); endif opts = statset (opts, val); otherwise error ("mdscale: unknown parameter name '%s'.", name); endswitch endfor critlist = {'stress', 'sstress', 'metricstress', 'metricsstress', ... 'sammon', 'strain'}; if (! (ischar (Criterion) && any (strcmp (Criterion, critlist)))) error ("mdscale: unknown 'Criterion' '%s'.", Criterion); endif nonmetric = any (strcmp (Criterion, {'stress', 'sstress'})); ## Weights vector aligned with DELTA if (isempty (Weights)) w = ones (m, 1); elseif (isvector (Weights) && numel (Weights) == m) w = Weights(:); elseif (ismatrix (Weights) && isequal (size (Weights), [n, n])) w = Weights(tril (true (n), -1)); else error ("mdscale: 'Weights' must match the size of D."); endif if (! (isnumeric (w) && isreal (w)) || any (w < 0)) error ("mdscale: 'Weights' must be real and nonnegative."); endif if (! (isscalar (Replicates) && Replicates >= 1 && Replicates == fix (Replicates))) error ("mdscale: 'Replicates' must be a positive integer."); endif ## The 'strain' criterion is classical scaling: a direct eigen-solution. if (strcmp (Criterion, "strain")) Y = classical_scaling (delta, n, p); Y = orient (Y); stress = strain_value (delta, n, Y); disparities = delta; return; endif ## Pair index list, in pdist (upper-triangle) order P = zeros (m, 2); r = 1; for i = 1:n-1 for j = i+1:n P(r,:) = [i, j]; r++; endfor endfor ## Map the criterion name to the id used by crit_grad/eval_stress critmap = {'metricstress', 'metricsstress', 'sammon', 'stress', 'sstress'}; critid = find (strcmp (Criterion, critmap)); ## Run the minimization once per replicate, keeping the best result bestY = []; bestf = Inf; for rep = 1:Replicates Y0 = initial_config (Start, delta, n, p, rep); [Yr, fr] = minimize_stress (Y0, critid, delta, w, n, p, P, nonmetric, opts); if (fr < bestf) bestf = fr; bestY = Yr; endif endfor ## A non-finite criterion value compares false against every bound, so a ## replicate returning one leaves bestY empty rather than raising. Catch it ## here: otherwise the empty configuration reaches eval_stress and fails as ## an out-of-bound index, naming nothing that led to it. if (isempty (bestY)) error (strcat ("mdscale: no replicate produced a finite criterion", ... " value; the optimization did not converge.")); endif Y = orient (bestY); [stress, disparities] = eval_stress (Y, critid, delta, w, n, p, P, nonmetric); endfunction ## --------------------------------------------------------------------------- ## Build the starting configuration. function Y0 = initial_config (Start, delta, n, p, rep) if (ischar (Start)) switch (lower (Start)) case 'cmdscale' if (rep == 1) Y0 = classical_scaling (delta, n, p); else Y0 = randn (n, p); endif case 'random' Y0 = randn (n, p); otherwise error ("mdscale: unknown 'Start' '%s'.", Start); endswitch elseif (isnumeric (Start) && isequal (size (Start), [n, p])) Y0 = Start; if (rep > 1) Y0 += randn (n, p); endif else error ("mdscale: 'Start' must be 'cmdscale', 'random', or an N-by-P matrix."); endif endfunction ## --------------------------------------------------------------------------- ## Classical scaling (the 'strain' solution and default start). function Y = classical_scaling (delta, n, p) Dsq = squareform (delta) .^ 2; J = eye (n) - ones (n) / n; B = -0.5 * J * Dsq * J; [V, L] = eig ((B + B') / 2); [ev, idx] = sort (diag (L), "descend"); V = V(:,idx); ev = max (ev(1:p), 0); Y = V(:,1:p) .* sqrt (ev)'; endfunction ## --------------------------------------------------------------------------- ## The 'strain' criterion value. function s = strain_value (delta, n, Y) Dsq = squareform (delta) .^ 2; s = sqrt (sum (sumsq (-0.5 * Dsq - Y * Y'))); endfunction ## --------------------------------------------------------------------------- ## Centre at the origin and rotate to principal axes with a sign convention. function Y = orient (Y) Y = Y - mean (Y); [~, ~, V] = svd (Y, 0); Y = Y * V; for j = 1:columns (Y) [~, im] = max (abs (Y(:,j))); if (Y(im,j) < 0) Y(:,j) = -Y(:,j); endif endfor endfunction ## --------------------------------------------------------------------------- ## Isotonic (monotone) regression of X in the order of DELTA, with primary ## tie handling (ties in DELTA broken by X), by pool-adjacent-violators. function dhat = pav (x, delta) [~, ord] = sortrows ([delta, x]); xo = x(ord); m = numel (xo); vals = xo; wts = ones (m, 1); cnt = ones (m, 1); K = m; k = 1; while (k < K) if (vals(k) > vals(k+1)) vals(k) = (wts(k) * vals(k) + wts(k+1) * vals(k+1)) / (wts(k) + wts(k+1)); wts(k) += wts(k+1); cnt(k) += cnt(k+1); vals(k+1) = []; wts(k+1) = []; cnt(k+1) = []; K--; if (k > 1) k--; endif else k++; endif endwhile xexp = zeros (m, 1); pos = 1; for b = 1:K xexp(pos:pos+cnt(b)-1) = vals(b); pos += cnt(b); endfor dhat = zeros (m, 1); dhat(ord) = xexp; endfunction ## --------------------------------------------------------------------------- ## Criterion value and gradient at a configuration. For the nonmetric criteria ## the disparities are recomputed and held fixed for the gradient (Kruskal). function [f, g] = crit_grad (Yv, critid, delta, w, n, p, P, nonmetric) Y = reshape (Yv, n, p); dif = Y(P(:,1),:) - Y(P(:,2),:); d = sqrt (sum (dif .^ 2, 2)); d(d == 0) = eps; switch (critid) case 1 ## metricstress T = sum (w .* delta .^ 2); res = d - delta; N = sum (w .* res .^ 2); f = sqrt (N / T); dfdd = (w .* res) / (f * T); case 2 ## metricsstress T = sum (w .* delta .^ 4); res = d .^ 2 - delta .^ 2; N = sum (w .* res .^ 2); f = sqrt (N / T); dfdd = (2 * w .* d .* res) / (f * T); case 3 ## sammon S = sum (w .* delta); res = d - delta; f = sum (w .* res .^ 2 ./ delta) / S; dfdd = (2 * w .* res ./ delta) / S; case 4 ## stress (nonmetric) dh = pav (d, delta); res = d - dh; T = sum (w .* d .^ 2); N = sum (w .* res .^ 2); f = sqrt (N / T); ## A nonmetric fit needs only monotonicity, so distances already ordered ## like the dissimilarities give res == 0 exactly, hence f == 0 and a 0/0 ## gradient. That configuration is the global minimum: the gradient is ## zero there. Only exact zero is singular -- the expression has a finite ## limit for any res != 0, however small. if (f == 0) dfdd = zeros (size (d)); else dfdd = (w .* res * T - N * (w .* d)) / (f * T ^ 2); endif case 5 ## sstress (nonmetric) dh = pav (d, delta); res = d .^ 2 - dh .^ 2; T = sum (w .* d .^ 4); N = sum (w .* res .^ 2); f = sqrt (N / T); ## Zero stress is the global minimum; see the note in case 4. if (f == 0) dfdd = zeros (size (d)); else dfdd = (2 * w .* d .* res * T - N * (2 * w .* d .^ 3)) / (f * T ^ 2); endif endswitch gg = (dfdd ./ d) .* dif; g = zeros (n, p); for k = 1:size (P, 1) g(P(k,1),:) += gg(k,:); g(P(k,2),:) -= gg(k,:); endfor g = g(:); endfunction ## --------------------------------------------------------------------------- ## Gradient descent with backtracking (Armijo) line search. function [Y, f] = minimize_stress (Y0, critid, delta, w, n, p, P, nonmetric, opts) Yv = Y0(:); [f, g] = crit_grad (Yv, critid, delta, w, n, p, P, nonmetric); al = 1; for it = 1:opts.MaxIter al = min (al * 2, 1e3); gg = g' * g; fn = f; Yn = Yv; for ls = 1:60 Yn = Yv - al * g; fn = crit_grad (Yn, critid, delta, w, n, p, P, nonmetric); if (fn < f - 1e-4 * al * gg) break; endif al /= 2; endfor step = max (abs (Yv - Yn)); Yv = Yn; [f, g] = crit_grad (Yv, critid, delta, w, n, p, P, nonmetric); if (abs (fn - f) <= opts.TolFun * max (1, f) && step <= opts.TolX) break; endif endfor Y = reshape (Yv, n, p); endfunction ## --------------------------------------------------------------------------- ## Final stress value and disparities at the returned configuration. function [stress, disparities] = eval_stress (Y, critid, delta, w, n, p, P, nonmetric) dif = Y(P(:,1),:) - Y(P(:,2),:); d = sqrt (sum (dif .^ 2, 2)); if (nonmetric) disparities = pav (d, delta); else disparities = delta; endif dd = disparities; switch (critid) case {1} stress = sqrt (sum (w .* (d - dd) .^ 2) / sum (w .* dd .^ 2)); case {2} stress = sqrt (sum (w .* (d.^2 - dd.^2) .^ 2) / sum (w .* dd .^ 4)); case 3 stress = sum (w .* (d - dd) .^ 2 ./ dd) / sum (w .* dd); case 4 stress = sqrt (sum (w .* (d - dd) .^ 2) / sum (w .* d .^ 2)); case 5 stress = sqrt (sum (w .* (d.^2 - dd.^2) .^ 2) / sum (w .* d .^ 4)); endswitch endfunction %!demo %! ## Recover a 2-D map of 8 points from their pairwise distances. %! rng = [0 0; 5 0; 5 4; 0 4; 8 2; -3 2; 2 7; 2 -3]; %! D = pdist (rng); %! [Y, stress] = mdscale (D, 2, 'Criterion', 'metricstress'); %! stress %! ## Y reproduces the pairwise distances closely. %! max (abs (pdist (Y)' - D')) ## Reference values below are from MATLAB R2023b, for ## C = [0 0 0; 5 1 2; 2 6 1; 1 2 7; 7 3 4; 3 8 5; 6 4 9; 4 9 3]; ## D = pdist (C); Y0 = [1 -1;-1 1;2 2;-2 -2;3 1;1 3;-3 -1;-1 -3]; ## The metric criteria and 'strain' reproduce MATLAB's configuration; the ## nonmetric criteria reproduce its stress value (see the non-uniqueness note). %!shared C, D, Y0, opt %! C = [0 0 0; 5 1 2; 2 6 1; 1 2 7; 7 3 4; 3 8 5; 6 4 9; 4 9 3]; %! D = pdist (C); %! Y0 = [1 -1; -1 1; 2 2; -2 -2; 3 1; 1 3; -3 -1; -1 -3]; %! opt = struct ("MaxIter", 1000, "TolFun", 1e-10, "TolX", 1e-10); %!test %! [Y, s] = mdscale (D, 2, "Criterion", "metricstress", "Start", Y0, "Options", opt); %! Yref = [ 6.810767239317778, -0.733620649959338; ... %! 3.071301239764592, 1.094132882710339; ... %! 1.065313739387755, -3.760130869234625; ... %! 0.623478915333739, 4.891508042741564; ... %! -0.674391648191895, 1.443596654184263; ... %! -3.493671563292435, -2.348293046078109; ... %! -4.559656269176627, 3.674896334304512; ... %! -2.843141653142908, -4.262089348668606]; %! assert_equal (Y, Yref, 1e-4); %! assert_equal (s, 0.144654108154698, 1e-8); %!test %! [Y, s] = mdscale (D, 2, "Criterion", "metricsstress", "Start", Y0, "Options", opt); %! Yref = [ 6.635327214692762, 0.792856204849063; ... %! 3.228563095536443, -1.334104353307981; ... %! 1.003358563030293, 3.760704781755182; ... %! 0.699645585641474, -3.784555012990314; ... %! -1.079052104978016, -2.376723256450389; ... %! -3.223960839101638, 2.563657848313004; ... %! -4.268551283289842, -3.733948071746559; ... %! -2.995330231531478, 4.112111859577994]; %! assert_equal (Y, Yref, 1e-4); %! assert_equal (s, 0.190937534497622, 1e-8); %!test %! [Y, s] = mdscale (D, 2, "Criterion", "sammon", "Start", Y0, "Options", opt); %! Yref = [ 6.933279805420950, -0.419508440326684; ... %! 3.008163425596839, 1.373357387532562; ... %! 1.273453983125238, -3.707332917059666; ... %! 0.101742369038229, 5.027798678525714; ... %! -0.450738847427944, 1.372181450811530; ... %! -3.390706238135306, -2.442042299704407; ... %! -4.908058626481512, 3.317873521951261; ... %! -2.567135871136496, -4.522327381730312]; %! assert_equal (Y, Yref, 1e-4); %! assert_equal (s, 0.023119816166763, 1e-8); %!test %! ## 'strain' is classical scaling: configuration and value are reproducible. %! [Y, s] = mdscale (D, 2, "Criterion", "strain", "Start", Y0, "Options", opt); %! Yref = [ 6.572163580404339, -0.496338025399502; ... %! 2.719997306646783, 1.496763413554223; ... %! 1.036596577326041, -3.598999625596409; ... %! 0.538361534877868, 2.777882896586096; ... %! -0.658048085951181, 1.901764558503386; ... %! -3.170613455894927, -2.220122324409574; ... %! -4.005398518313616, 4.052005597122571; ... %! -3.033058939095310, -3.912956490360791]; %! assert_equal (Y, Yref, 1e-3); %! assert_equal (s, 1.930074461959919e+02, 1e-6); %!test %! ## Nonmetric 'stress': the stress value matches MATLAB (configuration is %! ## only defined up to the local minimum -- see the non-uniqueness note). %! [Y, s, dsp] = mdscale (D, 2, "Criterion", "stress", "Start", Y0, "Options", opt); %! assert_equal (s, 0.078131057459269, 1e-6); %! assert_equal (size (Y), [8, 2]); %! assert_equal (numel (dsp), 28); %! ## disparities are monotone in the dissimilarities %! [~, ord] = sort (D(:)); %! assert_equal (all (diff (dsp(ord)) >= -1e-9), true); %!test %! ## Nonmetric 'sstress' stress value. %! [Y, s] = mdscale (D, 2, "Criterion", "sstress", "Start", Y0, "Options", opt); %! assert_equal (s <= 0.089488341028430 + 1e-6, true); %!test %! ## Near-collinear data: the nonmetric fit is perfect, so the residuals are %! ## exactly zero and the criterion gradient is 0/0. R2024a returns %! ## 1.122356541711999e-16 here and does not raise. %! A = [1, 2; 2, 4.1; 3, 5.9; 4, 8.2; 5, 9.8; 6, 12.1; 7, 13.9; 8, 16.2]; %! [Y, s] = mdscale (pdist (A), 2, 'Criterion', 'stress'); %! assert_equal (size (Y), [8, 2]); %! assert_equal (s < 1e-9, true); %!test %! ## The same degeneracy under 'sstress'; R2024a returns 2.0818624071994747e-16. %! A = [1, 2; 2, 4.1; 3, 5.9; 4, 8.2; 5, 9.8; 6, 12.1; 7, 13.9; 8, 16.2]; %! [Y, s] = mdscale (pdist (A), 2, 'Criterion', 'sstress'); %! assert_equal (size (Y), [8, 2]); %! assert_equal (s < 1e-9, true); %!test %! ## The default criterion is 'stress'. No other block calls mdscale without %! ## naming a criterion, so this is the only cover of the default path. %! A = [1, 2; 2, 4.1; 3, 5.9; 4, 8.2; 5, 9.8; 6, 12.1; 7, 13.9; 8, 16.2]; %! [Y1, s1] = mdscale (pdist (A), 2); %! [Y2, s2] = mdscale (pdist (A), 2, 'Criterion', 'stress'); %! assert_equal (s1, s2, 1e-12); %!test %! ## Accepts a square dissimilarity matrix as well as a vector. %! [Y1, s1] = mdscale (D, 2, "Criterion", "metricstress", "Start", Y0, "Options", opt); %! [Y2, s2] = mdscale (squareform (D), 2, "Criterion", "metricstress", ... %! "Start", Y0, "Options", opt); %! assert_equal (Y1, Y2, 1e-12); %!test %! ## Returned configuration is centred and on principal axes. %! Y = mdscale (D, 2, "Criterion", "metricstress", "Start", Y0, "Options", opt); %! assert_equal (mean (Y), [0, 0], 1e-10); %! c = cov (Y); %! assert_equal (c(1,2), 0, 1e-8); ## Test input validation %!error mdscale (D) %!error mdscale (1:4, 2) %!error ... %! mdscale (ones (3, 4), 2) %!error ... %! mdscale ([1, -2, 3], 1) %!error mdscale (D, 8) %!error mdscale (D, 0) %!error mdscale (D, 2, "Criterion", "foo") %!error mdscale (D, 2, "bogus", 1) %!error ... %! mdscale (D, 2, "Weights", [1, 2, 3]) %!error ... %! mdscale (D, 2, "Replicates", 0) %!error mdscale (D, 2, "Start", "bad") %!error mdscale (D, 2, "Options", 5) statistics-release-2.1.2/inst/Dimensionality_Reduction/nnmf.m000066400000000000000000000270041527311104300244210ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{W}, @var{H}] =} nnmf (@var{A}, @var{K}) ## @deftypefnx {statistics} {[@var{W}, @var{H}, @var{D}] =} nnmf (@var{A}, @var{K}) ## @deftypefnx {statistics} {[@dots{}] =} nnmf (@dots{}, @var{Name}, @var{Value}) ## ## Nonnegative matrix factorization. ## ## @code{[@var{W}, @var{H}] = nnmf (@var{A}, @var{K})} factors the nonnegative ## @math{N * M} matrix @var{A} into nonnegative factors @var{W} (@math{N * ## @var{K}}) and @var{H} (@math{@var{K} * M}) whose product approximates ## @var{A}, by minimizing the root-mean-square residual between @var{A} and ## @code{@var{W} * @var{H}}. @var{K}, the number of factors, is typically ## smaller than @math{N} and @math{M}. ## ## @code{[@var{W}, @var{H}, @var{D}] = nnmf (@dots{})} also returns the ## root-mean-square residual @var{D}, that is @code{norm (@var{A} - @var{W} * ## @var{H}, "fro") / sqrt (N * M)}. ## ## The factorization is not unique: the returned factors are normalized so that ## the rows of @var{H} have unit length, and the columns of @var{W} (and the ## corresponding rows of @var{H}) are ordered by decreasing length of the ## columns of @var{W}. Because the objective is not convex, the iteration ## converges to a local minimum that depends on the starting point; use ## @qcode{'Replicates'} to try several random starts and keep the best. ## ## Name/Value pairs: ## ## @table @asis ## @item @qcode{'Algorithm'} ## @qcode{'als'} (default) for alternating least squares, or @qcode{'mult'} for ## multiplicative updates. Alternating least squares usually converges faster ## and more reliably; multiplicative updates are more sensitive to the starting ## point. ## ## @item @qcode{'W0'} ## An @math{N * @var{K}} initial value for @var{W}. ## ## @item @qcode{'H0'} ## A @math{@var{K} * M} initial value for @var{H}. ## ## @item @qcode{'Replicates'} ## The number of times to repeat the factorization from new random starting ## points, keeping the result with the smallest residual. The default is 1. ## Ignored for a starting point fixed by both @qcode{'W0'} and @qcode{'H0'}. ## ## @item @qcode{'Options'} ## A structure of algorithm options (as returned by @code{statset}) whose ## @qcode{MaxIter}, @qcode{TolFun}, and @qcode{TolX} fields control the ## iteration. A @qcode{MaxIter} of 0 returns the starting factors, normalized, ## with their residual, where MATLAB fails. ## @end table ## ## @seealso{pca, statset} ## @end deftypefn function [W, H, D] = nnmf (A, K, varargin) ## Input validation if (nargin < 2) print_usage (); endif if (! (isnumeric (A) && ismatrix (A) && ndims (A) == 2)) error ("nnmf: A must be a numeric matrix."); endif if (! (isreal (A) && all (isfinite (A(:))))) error ("nnmf: A must be real and finite."); endif [n, m] = size (A); if (! (isscalar (K) && isnumeric (K))) error ("nnmf: K must be a numeric scalar."); elseif (K < 1 || K != fix (K)) error ("nnmf: K must be a positive integer."); elseif (K > min (n, m)) error ("nnmf: K must be no larger than the number of rows or columns in A."); endif ## Defaults and Name/Value parsing algorithm = "als"; W0 = []; H0 = []; Replicates = 1; opts = statset ("nnmf"); args = varargin; if (mod (numel (args), 2) != 0) error ("nnmf: Name/Value arguments must come in pairs."); endif for k = 1:2:numel (args) name = args{k}; val = args{k+1}; if (! (ischar (name) && isrow (name))) error ("nnmf: parameter names must be character vectors."); endif switch (lower (name)) case 'algorithm' algorithm = lower (val); case 'w0' W0 = val; case 'h0' H0 = val; case 'replicates' Replicates = val; case 'options' if (! isstruct (val)) error ("nnmf: 'Options' must be a structure."); endif opts = statset (opts, val); otherwise error ("nnmf: unknown parameter name '%s'.", name); endswitch endfor if (! any (strcmp (algorithm, {'als', 'mult'}))) error ("nnmf: 'Algorithm' must be 'als' or 'mult'."); endif if (! isempty (W0) && ! isequal (size (W0), [n, K])) error ("nnmf: 'W0' must be an %d-by-%d matrix.", n, K); endif if (! isempty (H0) && ! isequal (size (H0), [K, m])) error ("nnmf: 'H0' must be an %d-by-%d matrix.", K, m); endif if (! (isscalar (Replicates) && Replicates >= 1 && Replicates == fix (Replicates))) error ("nnmf: 'Replicates' must be a positive integer."); endif ## With both factors fixed there is a single starting point to run fixedStart = ! isempty (W0) && ! isempty (H0); if (fixedStart) Replicates = 1; endif ## Run the factorization once per replicate, keeping the best bestD = Inf; bestW = []; bestH = []; scal = sqrt (mean (A(:)) / K); for rep = 1:Replicates if (isempty (W0)) W = rand (n, K) * scal; else W = W0; endif if (isempty (H0)) H = rand (K, m) * scal; else H = H0; endif [W, H, d] = factorize (A, W, H, algorithm, opts, n, m); if (d < bestD) bestD = d; bestW = W; bestH = H; endif endfor ## Normalize: unit-length rows of H, columns ordered by decreasing W length hlen = sqrt (sum (bestH .^ 2, 2)); hlen(hlen == 0) = 1; H = bestH ./ hlen; W = bestW .* hlen'; [~, idx] = sort (sqrt (sum (W .^ 2, 1)), "descend"); W = W(:,idx); H = H(idx,:); D = bestD; endfunction ## --------------------------------------------------------------------------- ## Iterate one algorithm from a starting point to convergence. function [W, H, dnorm] = factorize (A, W, H, algorithm, opts, n, m) sqrteps = sqrt (eps); dnorm0 = Inf; ## The residual of the starting point, returned when no iteration runs dnorm = norm (A - W * H, "fro") / sqrt (n * m); for iter = 1:opts.MaxIter Wp = W; Hp = H; if (strcmp (algorithm, "als")) H = max (0, W \ A); W = max (0, A / H); else H = H .* (W' * A) ./ ((W' * W) * H + eps); W = W .* (A * H') ./ (W * (H * H') + eps); endif dnorm = norm (A - W * H, "fro") / sqrt (n * m); if (iter > 1) dw = max (max (abs (W - Wp))) / (sqrteps + max (max (abs (Wp)))); dh = max (max (abs (H - Hp))) / (sqrteps + max (max (abs (Hp)))); if (max (dw, dh) <= opts.TolX) break; endif if (dnorm0 - dnorm <= opts.TolFun * max (1, dnorm0)) break; endif endif dnorm0 = dnorm; endfor endfunction %!demo %! ## Factor a nonnegative matrix into two rank-2 nonnegative factors. %! A = [1, 2, 3; 2, 4, 6; 3, 5, 7; 4, 8, 12]; %! [W, H, D] = nnmf (A, 2); %! D %! ## W * H approximates A. %! W * H ## Reference values below are from MATLAB R2023b for ## A = reshape (mod ((1:20)*7, 13), 5, 4) + 1, with fixed starting factors. %!shared A, W0, H0, opt %! A = reshape (mod ((1:20)*7, 13), 5, 4) + 1; %! W0 = reshape (mod ((1:10)*3, 7), 5, 2) + 1; %! H0 = reshape (mod ((1:8)*3, 7), 2, 4) + 1; %! opt = struct ("MaxIter", 500, "TolFun", 1e-12, "TolX", 1e-12); %!test %! [W, H, D] = nnmf (A, 2, "Algorithm", "als", "W0", W0, "H0", H0, "Options", opt); %! Wref = [11.808229123411982, 9.049993647167147; ... %! 3.212266148150507, 12.092875848976247; ... %! 13.853561873197318, 1.116057457895120; ... %! 4.618299368228214, 13.176478328920602; ... %! 15.259595093275024, 2.199659937839474]; %! Href = [0.657913952127586, 0.187993469755446, 0.140048330155150, 0.715677407877266; ... %! 0, 0.730839581652543, 0.680241675322826, 0.056078240378338]; %! assert_equal (W, Wref, 1e-4); %! assert_equal (H, Href, 1e-5); %! assert_equal (D, 1.882172868745145, 1e-8); %!test %! [W, H, D] = nnmf (A, 2, "Algorithm", "mult", "W0", W0, "H0", H0, "Options", opt); %! Wref = [11.221742977568461, 8.713876463682844; ... %! 2.176215667360621, 12.316408085448018; ... %! 14.030193052240865, 0.411019822854590; ... %! 3.511868529923804, 13.360184914158390; ... %! 15.365861312580710, 1.454761030110911]; %! Href = [0.647958006689051, 0.222457058550383, 0.172654918585345, 0.707710080299095; ... %! 0.057102252901681, 0.727300231504821, 0.673915159707622, 0.116670748188381]; %! assert_equal (W, Wref, 1e-3); %! assert_equal (H, Href, 1e-4); %! assert_equal (D, 1.882172868753092, 1e-8); %!test %! ## Factors are nonnegative, correctly sized, and H rows are unit length. %! [W, H] = nnmf (A, 2, "W0", W0, "H0", H0, "Options", opt); %! assert_equal (size (W), [5, 2]); %! assert_equal (size (H), [2, 4]); %! assert_equal (all (W(:) >= 0) && all (H(:) >= 0), true); %! assert_equal (sqrt (sum (H .^ 2, 2)), [1; 1], 1e-10); %!test %! ## Columns of W are ordered by decreasing length. %! [W, H] = nnmf (A, 2, "W0", W0, "H0", H0, "Options", opt); %! assert_equal (norm (W(:,1)) >= norm (W(:,2)), true); %!test %! ## The residual D matches the direct computation. %! [W, H, D] = nnmf (A, 2, "W0", W0, "H0", H0, "Options", opt); %! assert_equal (D, norm (A - W * H, "fro") / sqrt (numel (A)), 1e-12); %!test %! ## No iteration returns the starting point with its residual. %! [W, H, D] = nnmf (A, 2, "W0", W0, "H0", H0, ... %! "Options", struct ("MaxIter", 0)); %! assert_equal (W * H, W0 * H0, 1e-12); %! assert_equal (D, norm (A - W0 * H0, "fro") / sqrt (numel (A)), 1e-12); ## Test input validation %!error nnmf (ones (4, 3)) %!error nnmf ({1, 2}, 1) %!error nnmf ([1, Inf; 2, 3], 1) %!error nnmf ([1+2i, 3; 4, 5], 1) %!error nnmf (ones (4, 3), 'd') %!error nnmf (ones (4, 3), [1, 2]) %!error nnmf (ones (4, 3), 1.5) %!error nnmf (ones (4, 3), 0) %!error nnmf (ones (4, 3), -1) %!error ... %! nnmf (ones (4, 3), 5) %!error ... %! nnmf (ones (4, 3), 4) %!error ... %! nnmf (ones (4, 3), 2, "Algorithm", "foo") %!error ... %! nnmf (ones (4, 3), 2, "W0", ones (3, 3)) %!error ... %! nnmf (ones (4, 3), 2, "H0", ones (2, 2)) %!error ... %! nnmf (ones (4, 3), 2, "Replicates", 0) %!error nnmf (ones (4, 3), 2, "bogus", 1) %!error nnmf (ones (4, 3), 2, "Options", 5) ## Edge cases with empty arrays %!error ... %! nnmf ([], 1) %!error ... %! nnmf (zeros (0, 3), 1) statistics-release-2.1.2/inst/Dimensionality_Reduction/pca.m000066400000000000000000000622261527311104300242330ustar00rootroot00000000000000## Copyright (C) 2013-2019 Fernando Damian Nieuwveldt ## Copyright (C) 2021 Stefano Guidoni ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{coeff} =} pca (@var{x}) ## @deftypefnx {statistics} {@var{coeff} =} pca (@var{x}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{coeff}, @var{score}, @var{latent}] =} pca (@dots{}) ## @deftypefnx {statistics} {[@var{coeff}, @var{score}, @var{latent}, @var{tsquared}] =} pca (@dots{}) ## @deftypefnx {statistics} {[@var{coeff}, @var{score}, @var{latent}, @var{tsquared}, @var{explained}, @var{mu}] =} pca (@dots{}) ## ## Performs a principal component analysis on a data matrix. ## ## A principal component analysis of a data matrix of @math{N} observations in a ## @math{D} dimensional space returns a @math{D*D} transformation matrix, to ## perform a change of basis on the data. The first component of the new basis ## is the direction that maximizes the variance of the projected data. ## ## Input argument: ## @itemize @bullet ## @item ## @var{x} : a @math{N*D} data matrix ## @end itemize ## ## The following @var{Name}, @var{Value} pair arguments can be used: ## @itemize @bullet ## @item ## @qcode{'Algorithm'} defines the algorithm to use: ## @itemize ## @item @qcode{'svd'} (default), for singular value decomposition ## @item @qcode{'eig'} for eigenvalue decomposition ## @end itemize ## ## @item ## @qcode{'Centered'} is a boolean indicator for centering the observation data. ## It is @code{true} by default. ## @item ## ## @qcode{'Economy'} is a boolean indicator for the economy size output. It is ## @code{true} by default. Hence, @code{pca} returns only the elements of ## @var{latent} that are not necessarily zero, and the corresponding columns of ## @var{coeff} and @var{score}, that is, when @math{N <= D}, only the first ## @math{N - 1}. ## ## @item ## @qcode{'NumComponents'} defines the number of components @math{k} to return. ## If @math{k < p}, then only the first @math{k} columns of @var{coeff} and ## @var{score} are returned. ## ## @item ## @qcode{'Rows'} defines how to handle missing values: ## @itemize ## @item @qcode{'complete'} (default), missing values are removed before ## computation. ## @item @qcode{'pairwise'} (only valid when @qcode{'Algorithm'} is ## @qcode{'eig'}), the covariance of rows with missing data is computed using ## the available data, but the covariance matrix could be not positive definite, ## which triggers the termination of @code{pca}. ## @item @qcode{'all'}, missing values are not allowed, @code{pca} ## terminates with an error if there are any. ## @end itemize ## ## @item ## @qcode{'Weights'} defines observation weights as a vector of positive values ## of length @math{N}. ## ## @item ## @qcode{'VariableWeights'} defines variable weights: ## @itemize ## @item a @var{vector} of positive values of length @math{D}. ## @item the string @qcode{'variance'} to use the sample variance as weights. ## @end itemize ## @end itemize ## ## Return values: ## @itemize @bullet ## @item ## @var{coeff} : the principal component coefficients, a @math{D*D} ## transformation matrix ## @item ## @var{score} : the principal component scores, the representation of @var{x} ## in the principal component space ## @item ## @var{latent} : the principal component variances, i.e., the eigenvalues of ## the covariance matrix of @var{x} ## @item ## @var{tsquared} : Hotelling's T-squared Statistic for each observation in ## @var{x} ## @item ## @var{explained} : the percentage of the variance explained by each principal ## component ## @item ## @var{mu} : the estimated mean of each variable of @var{x}, it is zero if the ## data are not centered ## @end itemize ## ## Matlab compatibility note: the alternating least square method 'als' and ## associated options 'Coeff0', 'Score0', and 'Options' are not yet implemented ## ## @subheading References ## @enumerate ## @item ## Jolliffe, I. T., Principal Component Analysis, 2nd Edition, Springer, 2002 ## @end enumerate ## ## @seealso{barttest, factoran, pcacov, pcares} ## @end deftypefn function [coeff, score, latent, tsquared, explained, mu] = pca (x, varargin) if (nargin < 1) print_usage (); endif [nobs, nvars] = size (x); ## default options optAlgorithmS = 'svd'; optCenteredB = true; optEconomyB = true; optNumComponentsI = nvars; optWeights = []; optVariableWeights = []; optRowsB = false; TF = []; ## parse parameters pair_index = 1; while (pair_index <= (nargin - 1)) switch (lower (varargin{pair_index})) ## decomposition algorithm: singular value decomposition, eigenvalue ## decomposition or (currently unavailable) alternating least square case 'algorithm' optAlgorithmS = varargin{pair_index + 1}; switch (optAlgorithmS) case {'svd', 'eig'} ; case 'als' error ("pca: alternating least square algorithm not implemented."); otherwise error ("pca: invalid algorithm '%s'.", optAlgorithmS); endswitch ## centering of the columns, around the mean case 'centered' if (isbool (varargin{pair_index + 1})) optCenteredB = varargin{pair_index + 1}; else error ("pca: 'centered' requires a boolean value."); endif ## limit the size of the output to the degrees of freedom, when a smaller ## number than the number of variables case 'economy' if (isbool (varargin{pair_index + 1})) optEconomyB = varargin{pair_index + 1}; else error ("pca: 'economy' requires a boolean value."); endif ## choose the number of components to show case 'numcomponents' optNumComponentsI = varargin{pair_index + 1}; if ((! isscalar (optNumComponentsI)) || (! isnumeric (optNumComponentsI)) || optNumComponentsI != floor (optNumComponentsI) || optNumComponentsI <= 0 || optNumComponentsI > nvars) error (strcat ("pca: the number of components must be a positive", ... " integernumber smaller or equal to the number of", ... " variables.")); endif ## observation weights: some observations can be more accurate than others case 'weights' optWeights = varargin{pair_index + 1}; if ((! isvector (optWeights)) || length (optWeights) != nobs || any (optWeights < 0)) error ("pca: weights must be a numerical array of positive numbers."); endif if (rows (optWeights) == 1 ) optWeights = transpose (optWeights); endif ## variable weights: weights used for the variables case 'variableweights' optVariableWeights = varargin{pair_index + 1}; if (ischar (optVariableWeights) && strcmpi (optVariableWeights, 'variance')) optVariableWeights = 'variance'; # take care of this later elseif ((! isvector (optVariableWeights)) || length (optVariableWeights) != nvars || (! isnumeric (optVariableWeights)) || any (optVariableWeights < 0)) error (strcat ("pca: variable weights must be a numerical array", ... " of positive numbers or the string 'variance',")); else optVariableWeights = 1 ./ sqrt (optVariableWeights); ## it is used as a row vector if (columns (optVariableWeights) == 1 ) optVariableWeights = transpose (optVariableWeights); endif endif ## rows: policy for missing values case 'rows' switch (varargin{pair_index + 1}) case 'complete' optRowsB = false; case 'pairwise' optRowsB = true; case 'all' if (any (isnan (x), 'all')) error (strcat ("pca: when all rows are requested the", ... " dataset cannot include NaN values")); endif otherwise error ("pca: %s is an invalid value for rows", ... varargin{pair_index + 1}); endswitch case {'coeff0', 'score0', 'options'} error (strcat ("pca: parameter %s is only valid with the 'als'", ... " method, which is not yet implemented."), ... varargin{pair_index}); otherwise error ("pca: unknown property '%s'.", varargin{pair_index}); endswitch pair_index += 2; endwhile if (nobs == 0 || nvars == 0) coeff = zeros (nvars, 0); score = zeros (nobs, 0); latent = zeros (0, 1); tsquared = zeros (nobs, 1); explained = zeros (0, 0); mu = zeros (0, 0); return; endif ## Preparing the dataset according to the chosen policy for missing values if (optRowsB) if (! strcmp (optAlgorithmS, 'eig')) optAlgorithmS = 'eig'; warning (strcat ("pca: setting algorithm to 'eig' because", ... " 'rows' option is set to 'pairwise'.")); endif TF = isnan (x); missingRows = zeros (nobs, 1); nmissing = 0; else ## "complete": remove all the rows with missing values TF = isnan (x); missingRows = any (TF, 2); nmissing = sum (missingRows); endif ## indices of the available rows ridcs = find (missingRows == 0); ## Center the columns to mean zero if requested if (optCenteredB) if (isempty (optWeights) && nmissing == 0 && ! optRowsB) ## no weights and no missing values mu = mean (x); elseif (nmissing == 0 && ! optRowsB) ## weighted observations: some observations are more valuable, i.e. they ## can be trusted more mu = sum (optWeights .* x) ./ sum (optWeights); else ## missing values: the mean is computed column by column mu = zeros (1, nvars); if (isempty (optWeights)) for iter = 1 : nvars valid = ! TF(:, iter); mu(iter) = mean (x(valid, iter)); endfor else ## weighted mean with missing data for iter = 1 : nvars valid = ! TF(:, iter); w = optWeights(valid); mu(iter) = sum (x(valid, iter) .* w) ./ sum (w); endfor endif endif Xc = x - mu; else Xc = x; ## The mean of the variables of the original dataset: ## return zero if the dataset is not centered mu = zeros (1, nvars); endif ## Change the columns according to the variable weights if (! isempty (optVariableWeights)) if (ischar (optVariableWeights)) if (isempty (optWeights)) sqrtBias = 1; # see below optVariableWeights = std (x); else ## unbiased variance estimation: the bias when using reliability weights ## is 1 - var(weights) / std(weights)^2 sqrtBias = sqrt (1 - (sumsq (optWeights) / sum (optWeights) ^ 2)); optVariableWeights = std (x, optWeights) / sqrtBias; endif endif Xc = Xc ./ optVariableWeights; endif ## Compute the observation weight matrix if (isempty (optWeights)) Wd = eye (nobs - nmissing); else Wd = diag (optWeights) ./ sum (optWeights); endif ## Compute the coefficients switch (optAlgorithmS) case 'svd' ## Check if there are more variables than observations if (nvars <= nobs) [U, S, coeff] = svd (sqrt (Wd) * Xc(ridcs,:), 'econ'); else ## Calculate the svd on the transpose matrix, much faster if (optEconomyB) [coeff, S, V] = svd (Xc(ridcs,:)' * sqrt (Wd), 'econ'); else [coeff, S, V] = svd (Xc(ridcs,:)' * sqrt (Wd)); endif endif case 'eig' ## this method requires the computation of the sample covariance matrix if (optRowsB) ## pairwise: ## in this case the degrees of freedom for each element of the matrix ## are equal to the number of valid rows for the couple of columns ## used to compute the element Xpairwise = Xc; Xpairwise(isnan (Xc)) = 0; Ndegrees = (nobs - 1) * ones (nvars, nvars); for i_iter = 1 : nvars for j_iter = i_iter : nvars Ndegrees(i_iter, j_iter) = Ndegrees(i_iter, j_iter) - ... sum (any (TF(:,[i_iter j_iter]), 2)); Ndegrees(j_iter, i_iter) = Ndegrees(i_iter, j_iter); endfor endfor Mcov = Xpairwise' * Wd * Xpairwise ./ Ndegrees; else ## the degrees of freedom are not really important here ndegrees = nobs - nmissing - 1; Mcov = Xc(ridcs, :)' * Wd * Xc(ridcs, :) / ndegrees; endif [coeff, S] = eigs (Mcov, nvars); endswitch ## Change the coefficients according to the variable weights if (! isempty (optVariableWeights)) coeff = coeff .* transpose (optVariableWeights); endif ## MATLAB compatibility: the sign convention is that the ## greatest absolute value for each column is positive switchSignV = find (max (coeff) < abs (min (coeff))); if (! isempty (switchSignV)) coeff(:, switchSignV) = -1 * coeff(:, switchSignV); endif ## Compute the scores if (nargout > 1) ## This is for the score when using variable weights, it is not really ## a new definition of Xc if (! isempty (optVariableWeights)) Xc = Xc ./ optVariableWeights; endif ## Get the Scores score = Xc(ridcs,:) * coeff; ## Get the rank of the score matrix r = rank (score); ## If there is missing data, put it back if (nmissing) scoretmp = zeros (nobs, nvars); scoretmp(! missingRows, :) = score; scoretmp(missingRows, :) = NaN; score = scoretmp; endif ## Only use the first r columns, pad rest with zeros if economy != true score = score(:, 1:r) ; if (! optEconomyB) score = [score, (zeros (nobs , nvars-r))]; else coeff = coeff(: , 1:r); endif endif ## Compute the variances if (nargout > 2) ## degrees of freedom: n - 1 for centered data if (optCenteredB) dof = size (Xc(ridcs,:), 1) - 1; else dof = size (Xc(ridcs,:), 1); endif ## This is the same as the eigenvalues of the covariance matrix of x if (strcmp (optAlgorithmS, 'eig')) latent = diag (S, 0); else latent = (diag (S'*S) / dof)(1:r); endif ## If observation weights were used, we need to scale back these values if (! isempty (optWeights)) latent = latent .* sum (optWeights(ridcs)); endif if (! optEconomyB) latent= [latent; (zeros (nvars - r, 1))]; endif endif ## Compute the Hotelling T-square statistics ## MATLAB compatibility: when using weighted observations the T-square ## statistics differ by some rounding error if (nargout > 3) ## Calculate the Hotelling T-Square statistic for the observations ## formally: tsquared = sumsq (zscore (score(:, 1:r)),2); ## No component counts at one degree of freedom or fewer: two centered ## observations define a single component passing exactly through both, so ## no Mahalanobis distance is estimable and T-square is 0 -- where the ## formula returns 0.5 for every such pair, whatever the data. Uncentered ## the same two observations carry two degrees of freedom and do count. ## Measured against R2024a, which reports 0 and 2 respectively. dof = numel (ridcs) - optCenteredB; tsquared = NaN (nobs, 1); if (dof <= 1) tsquared(ridcs) = 0; elseif (! isempty (optWeights)) if (r > 0) standardized_scores = score(ridcs, 1:r) ./ sqrt (latent(1:r)'); tsquared(ridcs) = sum (standardized_scores .^ 2, 2); endif else if (r > 0) tsquared(ridcs) = mahal (score(ridcs, 1:r), score(ridcs, 1:r)); endif endif endif ## Compute the variance explained by each principal component if (nargout > 4) explained = 100 * latent / sum (latent); endif ## When a number of components is chosen, the coefficients and score matrix ## only show that number of columns if (optNumComponentsI != nvars) coeff = coeff(:, 1:optNumComponentsI); endif if (optNumComponentsI != nvars && nargout > 1) score = score(:, 1:optNumComponentsI); endif endfunction %!shared COEFF,SCORE,latent,tsquare,m,x,R,V,lambda,i,S,F ## NIST Engineering Statistics Handbook example (6.5.5.2) %!test %! x = [7, 4, 3; 4, 1, 8; 6, 3, 5; 8, 6, 1; 8, 5, 7; ... %! 7, 2, 9; 5, 3, 3; 9, 5, 8; 7, 4, 5; 8, 2, 2]; %! R = corrcoef (x); %! [V, lambda] = eig (R); %! [~, i] = sort (diag (lambda), 'descend'); #arrange largest PC first %! S = V(:, i) * diag (sqrt (diag (lambda)(i))); %!assert_equal (diag (S(:, 1:2) * S(:, 1:2)'), [0.8662; 0.8420; 0.9876], 1E-4); %! B = V(:, i) * diag ( 1./ sqrt (diag (lambda)(i))); %! F = zscore (x) * B; %! [COEFF, SCORE, latent, tsquare] = pca (zscore (x, 1)); %!assert_equal (tsquare, sumsq (F, 2), 1E4*eps); %!test %! x = [1, 2, 3; 2, 1, 3]'; %! [COEFF, SCORE, latent, tsquare] = pca (x, 'Economy', false); %! m = [sqrt(2), sqrt(2); sqrt(2), -sqrt(2); -2*sqrt(2), 0] / 2; %! m(:,1) = m(:,1) * sign (COEFF(1,1)); %! m(:,2) = m(:,2) * sign (COEFF(1,2)); %!assert_equal (COEFF, m(1:2,:), 10*eps); %!assert_equal (SCORE, -m, 10*eps); %!assert_equal (latent, [1.5;.5], 10*eps); %!assert_equal (tsquare, [4;4;4]/3, 10*eps); ## Test with observation weights (using Matlab's results as a reference) %! [COEFF, SCORE, latent, tsquare] = pca (x, 'Economy', false, 'weights', ... %! [1 2 1], 'variableweights', ... %! 'variance'); %!assert_equal (COEFF, [0.632455532033676, -0.632455532033676; ... %! 0.741619848709566, 0.741619848709566], 10 * eps); %!assert_equal (SCORE, [-0.622019449426284, 0.959119380657905; ... %! -0.505649896847432, -0.505649896847431; %! 1.633319243121148, 0.052180413036957], 10 * eps); %!assert_equal (latent, [1.783001790889027; 0.716998209110974], 10 * eps); %!test assert_equal (tsquare, [1.5; 0.5; 1.5], 10 * eps); %!test %! x = [1,2,3;2,1,3]'; %! [COEFF, SCORE, latent, tsquare] = pca (x, 'Economy', false, 'weights', ... %! [2 1 2], 'variableweights', ... %! 'variance'); %! COEFF_exp = [0.7906, 0.7906; 0.6614, -0.6614]; %! SCORE_exp = [-0.7836, -0.4813; -0.9071, 0.9071; 1.2372, 0.0277]; %! latent_exp = [2.5562; 0.6438]; %! tsquare_exp = [0.6000; 1.6000; 0.6000]; %! assert_equal (COEFF, COEFF_exp, 1e-4); %! assert_equal (SCORE, SCORE_exp, 1e-4); %! assert_equal (latent, latent_exp, 1e-4); %! assert_equal (tsquare, tsquare_exp, 1e-4); %!test %! x = [1,2,3;2,1,3]'; %! [COEFF, SCORE, latent, tsquare] = pca (x, 'Economy', false, 'weights', ... %! [1 3 2], 'variableweights', ... %! 'variance'); %! COEFF_exp = [0.6216, -0.6216; 0.8118, 0.8118]; %! SCORE_exp = [-0.8358, 1.0411; -0.6473, -0.3792; 1.3889, 0.0482]; %! latent_exp = [2.9067; 0.7599]; %! tsquare_exp = [1.6667; 0.3333; 0.6667]; %! assert_equal (COEFF, COEFF_exp, 1e-4); %! assert_equal (SCORE, SCORE_exp, 1e-4); %! assert_equal (latent, latent_exp, 1e-4); %! assert_equal (tsquare, tsquare_exp, 1e-4); %!test %! x = [1,2,3;2,1,3]'; %! [COEFF, SCORE, latent, tsquare] = pca (x, 'Economy', false, 'weights', ... %! [1 0.5 1.5], 'variableweights', ... %! 'variance'); %! COEFF_exp = [0.8118, 0.8118; 0.6742, -0.6742]; %! SCORE_exp = [-0.9657, -0.4713; -1.0915, 0.8862; 1.0076, 0.0188]; %! latent_exp = [1.5257; 0.3077]; %! tsquare_exp = [1.3333; 3.3333; 0.6667]; %! assert_equal (COEFF, COEFF_exp, 1e-4); %! assert_equal (SCORE, SCORE_exp, 1e-4); %! assert_equal (latent, latent_exp, 1e-4); %! assert_equal (tsquare, tsquare_exp, 1e-4); %!test %! x = [1,2,3;2,1,3]'; %! [COEFF, SCORE, latent, tsquare] = pca (x, 'Economy', true, 'weights', ... %! [2 1 2], 'variableweights', ... %! 'variance'); %! COEFF_exp = [0.7906, 0.7906; 0.6614, -0.6614]; %! SCORE_exp = [-0.7836, -0.4813; -0.9071, 0.9071; 1.2372, 0.0277]; %! latent_exp = [2.5562; 0.6438]; %! tsquare_exp = [0.6000; 1.6000; 0.6000]; %! assert_equal (COEFF, COEFF_exp, 1e-4); %! assert_equal (SCORE, SCORE_exp, 1e-4); %! assert_equal (latent, latent_exp, 1e-4); %! assert_equal (tsquare, tsquare_exp, 1e-4); %!test %! x = [1,2,3;2,1,3]'; %! [COEFF, SCORE, latent, tsquare] = pca (x, 'Economy', true, 'weights', ... %! [1 3 2], 'variableweights', ... %! 'variance'); %! COEFF_exp = [0.6216, -0.6216; 0.8118, 0.8118]; %! SCORE_exp = [-0.8358, 1.0411; -0.6473, -0.3792; 1.3889, 0.0482]; %! latent_exp = [2.9067; 0.7599]; %! tsquare_exp = [1.6667; 0.3333; 0.6667]; %! assert_equal (COEFF, COEFF_exp, 1e-4); %! assert_equal (SCORE, SCORE_exp, 1e-4); %! assert_equal (latent, latent_exp, 1e-4); %! assert_equal (tsquare, tsquare_exp, 1e-4); %!test %! x = [1,2,3;2,1,3]'; %! [COEFF, SCORE, latent, tsquare] = pca (x, 'Economy', true, 'weights', ... %! [1 0.5 1.5], 'variableweights', ... %! 'variance'); %! COEFF_exp = [0.8118, 0.8118; 0.6742, -0.6742]; %! SCORE_exp = [-0.9657, -0.4713; -1.0915, 0.8862; 1.0076, 0.0188]; %! latent_exp = [1.5257; 0.3077]; %! tsquare_exp = [1.3333; 3.3333; 0.6667]; %! assert_equal (COEFF, COEFF_exp, 1e-4); %! assert_equal (SCORE, SCORE_exp, 1e-4); %! assert_equal (latent, latent_exp, 1e-4); %! assert_equal (tsquare, tsquare_exp, 1e-4); %!test %! x = x'; %! [COEFF, SCORE, latent, tsquare] = pca (x, 'Economy', false); %! m = [sqrt(2), sqrt(2), 0; -sqrt(2), sqrt(2), 0; 0, 0, 2] / 2; %! m(:,1) = m(:,1) * sign (COEFF(1,1)); %! m(:,2) = m(:,2) * sign (COEFF(1,2)); %! m(:,3) = m(:,3) * sign (COEFF(3,3)); %!assert_equal (COEFF, m, 10*eps); %!assert_equal (SCORE(:,1), -m(1:2,1), 10*eps); %!assert_equal (SCORE(:,2:3), zeros (2), 10*eps); %!assert_equal (latent, [1;0;0], 10*eps); %!## two centered observations carry one degree of freedom, so no component %!## counts and T-square is 0, as R2024a returns it %!assert_equal (tsquare, [0;0], 10*eps) %!test %! [COEFF, SCORE, latent, tsquare] = pca (x); %!assert_equal (COEFF, m(:, 1), 10*eps); %!assert_equal (SCORE, -m(1:2,1), 10*eps); %!assert_equal (latent, [1], 10*eps); %!assert_equal (tsquare, [0;0], 10*eps) %!test %! ## Complex missing data test %! x = [ 0.8147 0.2785 0.9575 %! 0.9058 0.5469 0.9649 %! 0.1270 0.9575 0.1576 %! 0.9134 0.9649 0.9706 %! 0.6324 0.1576 0.9572 %! 0.0975 0.9706 0.4854 %! 0.2785 0.9575 0.8003 %! 0.5469 0.1419 0.1419 ]; %! x_nan = x; %! x_nan(2, 3) = NaN; %! x_nan(5, 1) = NaN; %! [COEFF, SCORE, latent, tsquare] = pca (x_nan, 'Economy', false); %! %! ## Verify NaNs are correctly placed in SCORE and tsquare %! assert_equal (all (isnan (SCORE(2, :))), true); %! assert_equal (all (isnan (SCORE(5, :))), true); %! assert_equal (isnan (tsquare(2)), true); %! assert_equal (isnan (tsquare(5)), true); %! %! ## Verify other rows do not have NaNs %! assert_equal (any (isnan (SCORE([1 3 4 6 7 8], :))(:)), false); %! assert_equal (any (isnan (tsquare([1 3 4 6 7 8]))), false); %!error pca ([1 2; 3 4], 'Algorithm', 'xxx') %!error pca ([1 NaN; 3 4], 'Rows', 'all') %!error <'centered' requires a boolean value> pca ([1 2; 3 4], 'Centered', 'xxx') %!error pca ([1 2; 3 4], 'NumComponents', -4) %!error pca ([1 2; 3 4], 'Rows', 1) %!error pca ([1 2; 3 4], 'Weights', [1 2 3]) %!error pca ([1 2; 3 4], 'Weights', [-1 2]) %!error pca ([1 2; 3 4], 'VariableWeights', [-1 2]) %!error pca ([1 2; 3 4], 'VariableWeights', 'xxx') %!error pca ([1 2; 3 4], 'XXX', 1) ## Edge cases with empty arrays %!test %! [c, s, l, t, e, m] = pca ([]); %! assert_equal (size (c), [0 0]); %! assert_equal (size (s), [0 0]); %! assert_equal (size (l), [0 1]); %! assert_equal (size (t), [0 1]); %! assert_equal (size (e), [0 0]); %! assert_equal (size (m), [0 0]); %!test %! [c, s, l, t, e, m] = pca (zeros (0, 3)); %! assert_equal (size (c), [3 0]); %! assert_equal (size (s), [0 0]); %! assert_equal (size (l), [0 1]); %! assert_equal (size (t), [0 1]); %! assert_equal (size (e), [0 0]); %! assert_equal (size (m), [0 0]); %!test %! [c, s, l, t, e, m] = pca (zeros (3, 0)); %! assert_equal (size (c), [0 0]); %! assert_equal (size (s), [3 0]); %! assert_equal (size (l), [0 1]); %! assert_equal (size (t), [3 1]); %! assert_equal (size (e), [0 0]); %! assert_equal (size (m), [0 0]); statistics-release-2.1.2/inst/Dimensionality_Reduction/pcacov.m000066400000000000000000000117311527311104300247360ustar00rootroot00000000000000## Copyright (C) 2013-2019 Fernando Damian Nieuwveldt ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{coeff} =} pcacov (@var{K}) ## @deftypefnx {statistics} {[@var{coeff}, @var{latent}] =} pcacov (@var{K}) ## @deftypefnx {statistics} {[@var{coeff}, @var{latent}, @var{explained}] =} pcacov (@var{K}) ## ## Perform principal component analysis on covariance matrix ## ## @code{@var{coeff} = pcacov (@var{K})} performs principal component analysis ## on the square covariance matrix @var{K} and returns the principal component ## coefficients, also known as loadings. The columns are in order of decreasing ## component variance. ## ## @code{[@var{coeff}, @var{latent}] = pcacov (@var{K})} also returns a vector ## with the principal component variances, i.e. the eigenvalues of @var{K}. ## @var{latent} has a length of @qcode{size (@var{coeff}, 1)}. ## ## @code{[@var{coeff}, @var{latent}, @var{explained}] = pcacov (@var{K})} also ## returns a vector with the percentage of the total variance explained by each ## principal component. @var{explained} has the same size as @var{latent}. ## The entries in @var{explained} range from 0 (none of the variance is ## explained) to 100 (all of the variance is explained). ## ## @code{pcacov} does not standardize @var{K} to have unit variances. In order ## to perform principal component analysis on standardized variables, use the ## correlation matrix @qcode{@var{R} = @var{K} ./ (@var{SD} * @var{SD}')}, where ## @qcode{@var{SD} = sqrt (diag (@var{K}))}, in place of @var{K}. To perform ## principal component analysis directly on the data matrix, use @code{pca}. ## ## @subheading References ## @enumerate ## @item ## Jolliffe, I. T., Principal Component Analysis, 2nd Edition, Springer, 2002 ## @end enumerate ## ## @seealso{barttest, factoran, pcares, pca} ## @end deftypefn function [coeff, latent, explained] = pcacov (K) ## Check X being a square matrix if (ndims (K) != 2 || size (K, 1) != size (K, 2)) error ("pcacov: K must be a square matrix."); endif ## Check X being a symmetric matrix if (! issymmetric (K)) error ("pcacov: K must be a symmetric matrix."); endif [U, S, V] = svd (K); s_vals = diag (S); col_dot_prod = sum (real (conj (U) .* V), 1); tol = size (K, 1) * eps (max (s_vals)); is_negative = col_dot_prod < -0.9; is_significant = s_vals' > tol; ## Check for positive semi-definiteness if (any (is_negative & is_significant)) error ("pcacov: K must be a positive semi-definite matrix."); endif ## Force a sign convention on the coefficients so that ## the largest element in each column has a positive sign [row, col] = size (U); [~, m_ind] = max (abs (U), [], 1); csign = sign (U(m_ind + (0:row:(col - 1) * row))); coeff = bsxfun (@times, U, csign); ## Compute extra output arguments if (nargout > 1) latent = diag (S); endif if (nargout > 2) explained = 100 * latent ./ sum (latent); endif endfunction %!demo %! x = [ 7 26 6 60; %! 1 29 15 52; %! 11 56 8 20; %! 11 31 8 47; %! 7 52 6 33; %! 11 55 9 22; %! 3 71 17 6; %! 1 31 22 44; %! 2 54 18 22; %! 21 47 4 26; %! 1 40 23 34; %! 11 66 9 12; %! 10 68 8 12 %! ]; %! Kxx = cov (x); %! [coeff, latent, explained] = pcacov (Kxx) ## Test output %!test %! load hald %! Kxx = cov (ingredients); %! [coeff,latent,explained] = pcacov (Kxx); %! c_out = [-0.0678, -0.6460, 0.5673, 0.5062; ... %! -0.6785, -0.0200, -0.5440, 0.4933; ... %! 0.0290, 0.7553, 0.4036, 0.5156; ... %! 0.7309, -0.1085, -0.4684, 0.4844]; %! l_out = [517.7969; 67.4964; 12.4054; 0.2372]; %! e_out = [ 86.5974; 11.2882; 2.0747; 0.0397]; %! assert_equal (coeff, c_out, 1e-4); %! assert_equal (latent, l_out, 1e-4); %! assert_equal (explained, e_out, 1e-4); ## Test input validation %!error pcacov (ones (2, 3)) %!error pcacov (ones (3, 3, 3)) %!error pcacov ([1, 2; 0, 1]) %!error pcacov ([10, 0; 0, -1]) statistics-release-2.1.2/inst/Dimensionality_Reduction/pcares.m000066400000000000000000000103661527311104300247430ustar00rootroot00000000000000## Copyright (C) 2013-2019 Fernando Damian Nieuwveldt ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{residuals} =} pcares (@var{x}, @var{ndim}) ## @deftypefnx {statistics} {[@var{residuals}, @var{reconstructed}] =} pcares (@var{x}, @var{ndim}) ## ## Calculate residuals from principal component analysis. ## ## @code{@var{residuals} = pcares (@var{x}, @var{ndim})} returns the residuals ## obtained by retaining @var{ndim} principal components of the @math{N*D} ## matrix @var{x}. Rows of @var{x} correspond to observations, columns of ## @var{x} correspond to variables. @var{ndim} is a scalar and must be less ## than or equal to @math{D}. @var{residuals} is a matrix of the same size as ## @var{x}. Use the data matrix, not the covariance matrix, with this function. ## ## @code{[@var{residuals}, @var{reconstructed}] = pcares (@var{x}, @var{ndim})} ## returns the reconstructed observations, i.e. the approximation to @var{x} ## obtained by retaining its first @var{ndim} principal components. ## ## @code{pcares} does not normalize the columns of @var{x}. Use ## @qcode{pcares (zscore (@var{x}), @var{ndim})} in order to perform the ## principal components analysis based on standardized variables, i.e. based on ## correlations. Use @code{pcacov} in order to perform principal components ## analysis directly on a covariance or correlation matrix without constructing ## residuals. ## ## @subheading References ## @enumerate ## @item ## Jolliffe, I. T., Principal Component Analysis, 2nd Edition, Springer, 2002 ## @end enumerate ## ## @seealso{factoran, pcacov, pca} ## @end deftypefn function [residuals, reconstructed] = pcares (x, ndim) ## Check input arguments if (nargin < 2) error ("pcares: too few input arguments."); endif if (ndim > size (x, 2)) error ("pcares: NDIM must be less than or equal to the column of X."); endif ## Mean center data Xcentered = bsxfun (@minus, x, mean (x)); ## Apply svd to get the principal component coefficients [U, S, V] = svd (Xcentered); ## Use only the first ndim PCA components v = V(:,1:ndim); ## Calculate the residuals residuals = Xcentered - Xcentered * (v * v'); ## Compute extra output arguments if (nargout > 1) ## Reconstructed data using ndim PCA components reconstructed = x - residuals; endif endfunction %!demo %! x = [ 7 26 6 60; %! 1 29 15 52; %! 11 56 8 20; %! 11 31 8 47; %! 7 52 6 33; %! 11 55 9 22; %! 3 71 17 6; %! 1 31 22 44; %! 2 54 18 22; %! 21 47 4 26; %! 1 40 23 34; %! 11 66 9 12; %! 10 68 8 12]; %! %! ## As we increase the number of principal components, the norm %! ## of the residuals matrix will decrease %! r1 = pcares (x,1); %! n1 = norm (r1) %! r2 = pcares (x,2); %! n2 = norm (r2) %! r3 = pcares (x,3); %! n3 = norm (r3) %! r4 = pcares (x,4); %! n4 = norm (r4) ## Test output %!test %! load hald %! r1 = pcares (ingredients,1); %! r2 = pcares (ingredients,2); %! r3 = pcares (ingredients,3); %! assert_equal (r1(1,:), [2.0350, 2.8304, -6.8378, 3.0879], 1e-4); %! assert_equal (r2(1,:), [-2.4037, 2.6930, -1.6482, 2.3425], 1e-4); %! assert_equal (r3(1,:), [ 0.2008, 0.1957, 0.2045, 0.1921], 1e-4); ## Test input validation %!error pcares (ones (20, 3)) %!error ... %! pcares (ones (30, 2), 3) statistics-release-2.1.2/inst/Dimensionality_Reduction/ppca.m000066400000000000000000000305531527311104300244110ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{coeff} =} ppca (@var{Y}, @var{K}) ## @deftypefnx {statistics} {[@var{coeff}, @var{score}] =} ppca (@var{Y}, @var{K}) ## @deftypefnx {statistics} {[@var{coeff}, @var{score}, @var{pcvar}] =} ppca (@var{Y}, @var{K}) ## @deftypefnx {statistics} {[@var{coeff}, @var{score}, @var{pcvar}, @var{mu}] =} ppca (@var{Y}, @var{K}) ## @deftypefnx {statistics} {[@var{coeff}, @var{score}, @var{pcvar}, @var{mu}, @var{v}] =} ppca (@var{Y}, @var{K}) ## @deftypefnx {statistics} {[@var{coeff}, @var{score}, @var{pcvar}, @var{mu}, @var{v}, @var{S}] =} ppca (@var{Y}, @var{K}) ## @deftypefnx {statistics} {[@dots{}] =} ppca (@dots{}, @var{Name}, @var{Value}) ## ## Probabilistic principal component analysis. ## ## @code{@var{coeff} = ppca (@var{Y}, @var{K})} fits a probabilistic principal ## component analysis (PPCA) model with @var{K} components to the @math{N * P} ## data matrix @var{Y} (rows are observations, columns are variables) and ## returns the @math{P * @var{K}} matrix @var{coeff} of orthonormal principal ## component coefficients, ordered by decreasing component variance. @var{Y} ## may contain @code{NaN} values marking missing observations; the model is ## fitted by an expectation-maximization algorithm that accounts for them. ## @var{K} must be a positive integer smaller than @math{P}. ## ## @code{[@var{coeff}, @var{score}, @var{pcvar}, @var{mu}, @var{v}, @var{S}] = ## ppca (@dots{})} returns further outputs: ## ## @table @var ## @item score ## The @math{N * @var{K}} principal component scores (the data projected onto ## the components; missing entries are reconstructed from the model before ## projection). ## ## @item pcvar ## A @math{@var{K} * 1} vector of the principal component variances (the ## variance explained by each component). ## ## @item mu ## A @math{1 * P} vector of the estimated mean of @var{Y}. ## ## @item v ## The estimated residual (isotropic noise) variance. ## ## @item S ## A structure with the fitted model details: the loadings @qcode{W}, the ## expected scores @qcode{Xexp}, the reconstruction @qcode{Recon}, the number of ## iterations @qcode{NumIter}, and the root-mean-square residual ## @qcode{RMSResid}. ## @end table ## ## Name/Value pairs control the fit: ## ## @table @asis ## @item @qcode{'W0'} ## A @math{P * @var{K}} initial value for the loadings used by the ## expectation-maximization algorithm (missing-data case). ## ## @item @qcode{'Options'} ## A structure of algorithm options, as returned by @code{statset}, whose ## @qcode{MaxIter}, @qcode{TolFun}, and @qcode{TolX} fields set the maximum ## number of iterations and the convergence tolerances of the ## expectation-maximization algorithm. ## @end table ## ## When @var{Y} has no missing values the model is fitted directly from the ## eigendecomposition of its covariance matrix; @var{coeff}, @var{pcvar}, and ## @var{v} are then the principal component directions, the leading variances, ## and the mean of the trailing variances, respectively. ## ## @seealso{pca, pcacov, pcares, factoran, barttest} ## @end deftypefn function [coeff, score, pcvar, mu, v, S] = ppca (Y, K, varargin) ## Input validation if (nargin < 2) print_usage (); endif if (! (isnumeric (Y) && ismatrix (Y) && ndims (Y) == 2)) error ("ppca: Y must be a numeric matrix."); endif if (! isreal (Y)) error ("ppca: Y must be real."); endif [n, p] = size (Y); if (! (isnumeric (K) && isscalar (K) && isreal (K) && K >= 1 ... && K == fix (K))) error ("ppca: K must be a positive integer."); endif if (K >= p) error ("ppca: K must be smaller than the number of variables."); endif ## Defaults and Name/Value / Options parsing opts = statset ("ppca"); W0 = []; args = varargin; if (mod (numel (args), 2) != 0) error ("ppca: Name/Value arguments must come in pairs."); endif for k = 1:2:numel (args) name = args{k}; val = args{k+1}; if (! (ischar (name) && isrow (name))) error ("ppca: parameter names must be character vectors."); endif switch (lower (name)) case 'w0' W0 = val; case 'options' if (! isstruct (val)) error ("ppca: 'Options' must be a structure."); endif opts = statset (opts, val); otherwise error ("ppca: unknown parameter name '%s'.", name); endswitch endfor if (! isempty (W0) && ! isequal (size (W0), [p, K])) error ("ppca: 'W0' must be a %d-by-%d matrix.", p, K); endif ## Fit the model if (any (isnan (Y(:)))) [coeff, W, ex, mu, v, numiter] = ppca_em (Y, K, W0, opts); else [coeff, W, ex, mu, v, numiter] = ppca_full (Y, K); endif ## Sign convention: the element of largest magnitude in each column is positive for j = 1:K [~, im] = max (abs (coeff(:,j))); if (coeff(im,j) < 0) coeff(:,j) = -coeff(:,j); endif endfor ## Reconstruct missing entries from the model and project onto the components obs = ! isnan (Y); Yhat = Y; Recon = ex * W' + mu; Yhat(! obs) = Recon(! obs); score = (Yhat - mu) * coeff; pcvar = var (score, 0)'(:); if (nargout > 5) resid = Yhat - (score * coeff' + mu); S = struct ("W", coeff .* sqrt (max (pcvar - v, 0))', ... "Xexp", score, "Recon", score * coeff' + mu, ... "NumIter", numiter, ... "RMSResid", sqrt (mean (resid(obs) .^ 2))); endif endfunction ## Direct fit for fully observed data via the covariance eigendecomposition. function [coeff, W, ex, mu, v, numiter] = ppca_full (Y, K) [n, p] = size (Y); mu = mean (Y); C = cov (Y); [U, L] = eig ((C + C') / 2); [lambda, idx] = sort (diag (L), "descend"); U = U(:,idx); coeff = U(:,1:K); v = mean (lambda(K+1:end)); ## ML loadings and expected scores (posterior mean) for the S struct W = coeff .* sqrt (max (lambda(1:K) - v, 0))'; M = W' * W + v * eye (K); ex = (Y - mu) * W / M; numiter = 0; endfunction ## Expectation-maximization fit for data with missing (NaN) entries. function [coeff, W, ex, mu, v, iter] = ppca_em (Y, K, W0, opts) [n, p] = size (Y); obs = ! isnan (Y); ## Initialize the mean, a filled data matrix, and the loadings mu = zeros (1, p); for j = 1:p mu(j) = mean (Y(obs(:,j), j)); endfor Yf = Y; for j = 1:p Yf(! obs(:,j), j) = mu(j); endfor if (isempty (W0)) [~, Sv, V] = svd (Yf - mu, 0); W = V(:,1:K) * Sv(1:K,1:K) / sqrt (n); else W = W0; endif v = 1; ex = zeros (n, K); Exx = zeros (K, K, n); for iter = 1:opts.MaxIter Wold = W; muold = mu; vold = v; ## E-step: expected scores and second moments per observation for i = 1:n o = obs(i,:); Wo = W(o,:); yo = Y(i,o) - mu(o); Minv = inv (v * eye (K) + Wo' * Wo); ex(i,:) = (Minv * Wo' * yo')'; Exx(:,:,i) = v * Minv + ex(i,:)' * ex(i,:); endfor ## M-step: update the mean and loadings per variable, then the noise for j = 1:p r = obs(:,j); exj = ex(r,:); yj = Y(r,j); ZtZ = [sum(Exx(:,:,r), 3), sum(exj, 1)'; sum(exj, 1), sum(r)]; sol = ZtZ \ [exj' * yj; sum(yj)]; W(j,:) = sol(1:K)'; mu(j) = sol(K+1); endfor num = 0; cnt = 0; for i = 1:n o = obs(i,:); Wo = W(o,:); yo = Y(i,o) - mu(o); num += yo * yo' - 2 * ex(i,:) * Wo' * yo' ... + trace (Wo' * Wo * Exx(:,:,i)); cnt += sum (o); endfor v = num / cnt; if (max (abs (W(:) - Wold(:))) < opts.TolX ... && max (abs (mu - muold)) < opts.TolX && abs (v - vold) < opts.TolX) break; endif endfor ## Orthonormalize the loadings into principal component directions [Uw, ~, ~] = svd (W, 0); coeff = Uw(:,1:K); endfunction %!demo %! ## Fit a two-component PPCA model and reconstruct the data. %! Y = [ 1.0, 2.0, 0.5; 2.1, 3.9, 1.2; ... %! -1.0, -2.2, -0.4; -2.0, -3.8, -1.1; ... %! 0.5, 1.1, 0.9; 1.6, 2.8, -0.2]; %! [coeff, score, pcvar, mu, v] = ppca (Y, 2); %! coeff %! pcvar %! ## The scores reconstruct the data through the coefficients. %! max (abs (vec (score * coeff' + mu - Y))) ## Reference values below are from MATLAB R2023b for ## Y = reshape (mod ((1:40)*7, 17), 10, 4) - 8. %!test %! Y = reshape (mod ((1:40)*7, 17), 10, 4) - 8; %! opt = struct ("MaxIter", 2000, "TolFun", 1e-12, "TolX", 1e-12); %! [coeff, score, pcvar, mu, v] = ppca (Y, 2, "Options", opt); %! coeff_ref = [ 0.543716564044483, -0.391810693220729; ... %! 0.599330273587830, 0.314784124203745; ... %! 0.302509942024958, 0.761619894252268; ... %! -0.503649934101907, 0.409060475365608]; %! assert_equal (coeff, coeff_ref, 1e-6); %! assert_equal (pcvar, [49.558237292069599; 30.960532427249241], 1e-4); %! assert_equal (mu, [-0.1, 0.2, 0.5, -0.9], 1e-10); %! assert_equal (v, 10.623960883864141, 1e-4); %!test %! Y = reshape (mod ((1:40)*7, 17), 10, 4) - 8; %! [coeff, score] = ppca (Y, 2); %! score_ref = [ -2.225140840148114, 4.921963749319239; ... %! 7.787588722827117, -7.324026640834169; ... %! -5.050861878994857, 1.641002156881092; ... %! 10.104537612540948, 2.342550721115434; ... %! -7.876582917841600, -1.639959435557055; ... %! -1.283233827199200, 6.015617613465288; ... %! -1.459120725735530, -11.581703199267448; ... %! -4.108954866045941, 2.734656021027141; ... %! 11.046444625489862, 3.436204585261484; ... %! -6.934675904892686, -0.546305571411006]; %! assert_equal (score, score_ref, 1e-4); %!test %! ## coeff is orthonormal and score is the projection of the centred data. %! Y = reshape (mod ((1:40)*7, 17), 10, 4) - 8; %! [coeff, score, pcvar, mu] = ppca (Y, 2); %! assert_equal (coeff' * coeff, eye (2), 1e-10); %! assert_equal (score, (Y - mu) * coeff, 1e-10); %!test %! ## Missing-data (NaN) case fitted by expectation-maximization. %! Y = reshape (mod ((1:40)*7, 17), 10, 4) - 8; %! Y(3,2) = NaN; %! Y(7,4) = NaN; %! opt = struct ("MaxIter", 5000, "TolFun", 1e-12, "TolX", 1e-12); %! [coeff, score, pcvar, mu, v] = ppca (Y, 2, "Options", opt); %! coeff_ref = [ 0.489878122099491, -0.455706110813571; ... %! 0.631344835584810, 0.254329922477782; ... %! 0.331227809703440, 0.763974559487245; ... %! -0.501708343709497, 0.379461596944783]; %! assert_equal (coeff, coeff_ref, 1e-4); %! assert_equal (pcvar, [49.932943652722741; 28.917799526089862], 1e-3); %! assert_equal (mu, [-0.099999999999924, 0.225205294055629, ... %! 0.500000000000008, -0.683793705409387], 1e-4); %! assert_equal (v, 10.684669221332555, 1e-3); %!test %! ## Output sizes. %! Y = reshape (mod ((1:40)*7, 17), 10, 4) - 8; %! [coeff, score, pcvar, mu, v, S] = ppca (Y, 3); %! assert_equal (size (coeff), [4, 3]); %! assert_equal (size (score), [10, 3]); %! assert_equal (size (pcvar), [3, 1]); %! assert_equal (size (mu), [1, 4]); %! assert_equal (isscalar (v), true); %! assert_equal (isfield (S, "W") && isfield (S, "Recon"), true); ## Test input validation %!error ppca (ones (5, 3)) %!error ppca ({1, 2}, 1) %!error ppca ([1+2i, 3; 4, 5], 1) %!error ppca (ones (5, 3), 0) %!error ppca (ones (5, 3), 1.5) %!error ppca (ones (5, 3), 3) %!error ppca (ones (5, 3), 1, "W0") %!error ppca (ones (5, 3), 1, "foo", 1) %!error ppca (ones (5, 3), 1, "W0", ones (2, 2)) %!error ppca (ones (5, 3), 1, "Options", 5) statistics-release-2.1.2/inst/Dimensionality_Reduction/princomp.m000066400000000000000000000130321527311104300253060ustar00rootroot00000000000000## Copyright (C) 2013-2019 Fernando Damian Nieuwveldt ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{COEFF} =} princomp (@var{X}) ## @deftypefnx {statistics} {[@var{COEFF}, @var{SCORE}] =} princomp (@var{X}) ## @deftypefnx {statistics} {[@var{COEFF}, @var{SCORE}, @var{latent}] =} princomp (@var{X}) ## @deftypefnx {statistics} {[@var{COEFF}, @var{SCORE}, @var{latent}, @var{tsquare}] =} princomp (@var{X}) ## @deftypefnx {statistics} {[@dots{}] =} princomp (@var{X}, "econ") ## ## Performs a principal component analysis on a NxP data matrix X. ## ## @itemize @bullet ## @item ## @var{COEFF} : returns the principal component coefficients ## @item ## @var{SCORE} : returns the principal component scores, the representation of X ## in the principal component space ## @item ## @var{LATENT} : returns the principal component variances, i.e., the ## eigenvalues of the covariance matrix X. ## @item ## @var{TSQUARE} : returns Hotelling's T-squared Statistic for each observation ## in X ## @item ## [...] = princomp(X,'econ') returns only the elements of latent that are not ## necessarily zero, and the corresponding columns of COEFF and SCORE, that is, ## when n <= p, only the first n-1. This can be significantly faster when p is ## much larger than n. In this case the svd will be applied on the transpose of ## the data matrix X ## ## @end itemize ## ## @subheading References ## ## @enumerate ## @item ## Jolliffe, I. T., Principal Component Analysis, 2nd Edition, Springer, 2002 ## ## @end enumerate ## @end deftypefn function [COEFF, SCORE, latent, tsquare] = princomp (X, varargin) if (nargin < 1 || nargin > 2) print_usage (); endif if (nargin == 2 && ! strcmpi (varargin{:}, 'econ')) error (strcat ("princomp: if a second input argument is present,", ... " it must be the string 'econ'.")); endif [nobs nvars] = size (X); # Center the columns to mean zero Xcentered = bsxfun (@minus,X,mean (X)); # Check if there are more variables then observations if nvars <= nobs [U,S,COEFF] = svd (Xcentered, 'econ'); else # Calculate the svd on the transpose matrix, much faster if (nargin == 2 && strcmpi (varargin{:} , 'econ')) [COEFF,S,V] = svd (Xcentered' , 'econ'); else [COEFF,S,V] = svd (Xcentered'); endif endif if nargout > 1 # Get the Scores SCORE = Xcentered * COEFF; # Get the rank of the SCORE matrix r = rank (SCORE); # Only use the first r columns, pad rest with zeros if economy != 'econ' SCORE = SCORE(:,1:r) ; if ! (nargin == 2 && strcmpi (varargin{:} , 'econ')) SCORE = [SCORE, zeros(nobs , nvars-r)]; else COEFF = COEFF(: , 1:r); endif endif if nargout > 2 # This is the same as the eigenvalues of the covariance matrix of X latent = (diag (S) .^ 2 / (size (Xcentered, 1) - 1))(1:r); if ! (nargin == 2 && strcmpi (varargin{:} , 'econ')) latent= [latent; zeros(nvars-r,1)]; endif endif if (nargout > 3) # Calculate the Hotelling T-Square statistic for the observations tsquare = sumsq (zscore (SCORE(:,1:r)), 2); endif endfunction %!shared COEFF,SCORE,latent,tsquare,m,x,R,V,lambda,i,S,F #NIST Engineering Statistics Handbook example (6.5.5.2) %!test %! x=[7 4 3 %! 4 1 8 %! 6 3 5 %! 8 6 1 %! 8 5 7 %! 7 2 9 %! 5 3 3 %! 9 5 8 %! 7 4 5 %! 8 2 2]; %! R = corrcoef (x); %! [V, lambda] = eig (R); %! [~, i] = sort (diag (lambda), 'descend'); #arrange largest PC first %! S = V(:, i) * diag (sqrt (diag (lambda)(i))); %! ## contribution of first 2 PCs to each original variable %!assert_equal (diag (S(:, 1:2)*S(:, 1:2)'), [0.8662; 0.8420; 0.9876], 1E-4); %! B = V(:, i) * diag ( 1./ sqrt (diag (lambda)(i))); %! F = zscore (x)*B; %! [COEFF,SCORE,latent,tsquare] = princomp (zscore (x, 1)); %!assert_equal (tsquare,sumsq (F, 2),1E4*eps); %!test %! x=[1,2,3;2,1,3]'; %! [COEFF,SCORE,latent,tsquare] = princomp (x); %! m=[sqrt(2),sqrt(2);sqrt(2),-sqrt(2);-2*sqrt(2),0]/2; %! m(:,1) = m(:,1)*sign (COEFF(1,1)); %! m(:,2) = m(:,2)*sign (COEFF(1,2)); %!assert_equal (COEFF,m(1:2,:),10*eps); %!assert_equal (SCORE,-m,10*eps); %!assert_equal (latent,[1.5;.5],10*eps); %!assert_equal (tsquare,[4;4;4]/3,10*eps); %!test %! x=x'; %! [COEFF,SCORE,latent,tsquare] = princomp (x); %! m=[sqrt(2),sqrt(2),0;-sqrt(2),sqrt(2),0;0,0,2]/2; %! m(:,1) = m(:,1)*sign (COEFF(1,1)); %! m(:,2) = m(:,2)*sign (COEFF(1,2)); %! m(:,3) = m(:,3)*sign (COEFF(3,3)); %!assert_equal (COEFF,m,10*eps); %!assert_equal (SCORE(:,1),-m(1:2,1),10*eps); %!assert_equal (SCORE(:,2:3),zeros (2),10*eps); %!assert_equal (latent,[1;0;0],10*eps); %!assert_equal (tsquare,[0.5;0.5],10*eps) %!test %! [COEFF,SCORE,latent,tsquare] = princomp (x, 'econ'); %!assert_equal (COEFF,m(:, 1),10*eps); %!assert_equal (SCORE,-m(1:2,1),10*eps); %!assert_equal (latent,[1],10*eps); %!assert_equal (tsquare,[0.5;0.5],10*eps) statistics-release-2.1.2/inst/Dimensionality_Reduction/private/000077500000000000000000000000001527311104300247545ustar00rootroot00000000000000statistics-release-2.1.2/inst/Dimensionality_Reduction/private/objective_history.m000066400000000000000000000034531527311104300306720ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{out} =} objective_history (@var{action}, @var{val}) ## ## Collect the objective values @code{fminunc} walks through. ## ## @var{action} is @qcode{'reset'} to begin a fit, @qcode{'add'} to append ## @var{val}, or @qcode{'get'} to return everything gathered since the reset. ## Only @qcode{'get'} returns anything, the others leaving @var{out} empty. ## ## @code{fminunc} offers no history of its own and its @code{OutputFcn} cannot ## return one, so the values are gathered here and reported as ## @code{FitInfo.Objective}, which would otherwise hold the final value alone. ## One fit runs at a time, which is what lets a single buffer serve both ## classes. ## ## @end deftypefn function out = objective_history (action, val) persistent buf out = []; switch (action) case 'reset' buf = zeros (0, 1); case 'add' buf(end+1, 1) = val; case 'get' out = buf; otherwise error ("objective_history: unknown action '%s'.", action); endswitch endfunction statistics-release-2.1.2/inst/Dimensionality_Reduction/private/objective_history_fcn.m000066400000000000000000000026471527311104300315240ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{stop} =} objective_history_fcn (@var{x}, @var{optimValues}, @var{state}) ## ## Append the current objective to the history, as @code{fminunc}'s ## @code{OutputFcn}. ## ## @var{stop} is always false, so a fit is never halted from here, and @var{x} ## is unused. The @qcode{'init'} call carries the objective at the starting ## weights, which is the first entry of the history MATLAB reports. ## ## @end deftypefn function stop = objective_history_fcn (x, optimValues, state) stop = false; if (any (strcmp (state, {"init", "iter"}))) objective_history ("add", optimValues.fval); endif endfunction statistics-release-2.1.2/inst/Dimensionality_Reduction/procrustes.m000066400000000000000000000312311527311104300256710ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{d} =} procrustes (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{d} =} procrustes (@var{X}, @var{Y}, @var{param1}, @var{value1}, @dots{}) ## @deftypefnx {statistics} {[@var{d}, @var{Z}] =} procrustes (@dots{}) ## @deftypefnx {statistics} {[@var{d}, @var{Z}, @var{transform}] =} procrustes (@dots{}) ## ## Procrustes Analysis. ## ## @code{@var{d} = procrustes (@var{X}, @var{Y})} computes a linear ## transformation of the points in the matrix @var{Y} to best conform them to ## the points in the matrix @var{X} by minimizing the sum of squared errors, as ## the goodness of fit criterion, which is returned in @var{d} as a ## dissimilarity measure. @var{d} is standardized by a measure of the scale of ## @var{X}, given by ## @itemize ## @item @qcode{sum (sum ((X - repmat (mean (X, 1), size (X, 1), 1)) .^ 2, 1))} ## @end itemize ## i.e., the sum of squared elements of a centered version of @var{X}. However, ## if @var{X} comprises repetitions of the same point, the sum of squared errors ## is not standardized. ## ## @var{X} and @var{Y} must have the same number of points (rows) and ## @qcode{procrustes} matches the @math{i}-th point in @var{Y} to the ## @math{i}-th point in @var{X}. Points in @var{Y} can have smaller dimensions ## (columns) than those in @var{X}, but not the opposite. Missing dimensions in ## @var{Y} are added with padding columns of zeros as necessary to match the ## the dimensions in @var{X}. ## ## @code{[@var{d}, @var{Z}] = procrustes (@var{X}, @var{Y})} also returns the ## transformed values in @var{Y}. ## ## @code{[@var{d}, @var{Z}, @var{transform}] = procrustes (@var{X}, @var{Y})} ## also returns the transformation that maps @var{Y} to @var{Z}. ## ## @var{transform} is a structure with fields: ## ## @multitable @columnfractions 0.1 0.8 ## @item @qcode{c} @tab the translation component ## @item @qcode{T} @tab the orthogonal rotation and reflection ## component ## @item @qcode{b} @tab the scale component ## @end multitable ## ## So that @code{@var{Z} = @var{transform}.@qcode{b} * @var{Y} * ## @var{transform}.@qcode{T} + @var{transform}.@qcode{c}} ## ## @qcode{procrustes} can take two optional parameters as Name-Value pairs. ## ## @code{[@dots{}] = procrustes (@dots{}, @qcode{'Scaling'}, @qcode{false})} ## computes a transformation that does not include scaling, that is ## @var{transform}.@qcode{b} = 1. Setting @qcode{'Scaling'} to @qcode{true} ## includes a scaling component, which is the default. ## ## @code{[@dots{}] = procrustes (@dots{}, @qcode{'Reflection'}, @qcode{false})} ## computes a transformation that does not include a reflection component, that ## is @var{transform}.@qcode{T} = 1. Setting @qcode{'Reflection'} to ## @qcode{true} forces the solution to include a reflection component in the ## computed transformation, that is @var{transform}.@qcode{T} = -1. ## ## @code{[@dots{}] = procrustes (@dots{}, @qcode{'Reflection'}, @qcode{'best'})} ## computes the best fit procrustes solution, which may or may not include a ## reflection component, which is the default. ## ## @seealso{cmdscale} ## @end deftypefn function [d, Z, transform] = procrustes (X, Y, varargin) if (nargin < 2 || nargin > 6) print_usage (); endif ## Check X and Y for appropriate input if (isempty (X) || ! ismatrix (X) || ndims (X) != 2 || ... isempty (Y) || ! ismatrix (Y) || ndims (Y) != 2) error ("procrustes: X and Y must be 2-dimensional matrices."); endif if (any (isnan (X(:))) || any (isinf (X(:))) || iscomplex (X) || ... any (isnan (Y(:))) || any (isinf (Y(:))) || iscomplex (Y)) error ("procrustes: values in X and Y must be real."); endif [Xp, Xd] = size (X); [Yp, Yd] = size (Y); if (Yp != Xp) error ("procrustes: X and Y must have equal number of rows."); elseif (Yd > Xd) error ("procrustes: X must have at least as many columns as Y."); endif ## Add defaults and parse optional arguments scaling = true; reflection = 'best'; if (nargin > 2) params = numel (varargin); if ((params / 2) != fix (params / 2)) error ("procrustes: optional arguments must be in Name-Value pairs.") endif for idx = 1:2:params name = varargin{idx}; value = varargin{idx+1}; switch (lower (name)) case 'scaling' scaling = value; if (! (isscalar (scaling) && islogical (scaling))) error ("procrustes: invalid value for scaling."); endif case 'reflection' reflection = value; if (! (strcmpi (reflection, 'best') || islogical (reflection))) error ("procrustes: invalid value for reflection."); endif otherwise error ("procrustes: invalid name for optional arguments."); endswitch endfor endif ## Center at the origin. Xmu = mean (X, 1); Ymu = mean (Y, 1); X_0 = X - repmat (Xmu, Xp, 1); Y_0 = Y - repmat (Ymu, Xp, 1); ## Get centroid size and check for X or Y having identical points Xsumsq = sum (X_0 .^ 2, 1); Ysumsq = sum (Y_0 .^ 2, 1); constX = all (Xsumsq <= abs (eps (class (X)) * Xp * Xmu) .^ 2); constY = all (Ysumsq <= abs (eps (class (X)) * Xp * Ymu) .^ 2); Xsumsq = sum (Xsumsq); Ysumsq = sum (Ysumsq); if (! constX && ! constY) ## Scale to "centered" Frobenius norm. normX = sqrt (Xsumsq); normY = sqrt (Ysumsq); X_0 = X_0 / normX; Y_0 = Y_0 / normY; ## Fix dimension space (if necessary) if (Yd < Xd) Y_0 = [Y_0 zeros(Xp, Xd-Yd)]; endif ## Find optimal rotation matrix of Y A = X_0' * Y_0; [U, S, V] = svd (A); T = V * U'; ## Handle reflection only if 'true' or 'false' was given if (! strcmpi (reflection, 'best')) is_reflection = (det (T) < 0); ## Force a reflection if data and reflection option disagree if (reflection != is_reflection) V(:,end) = -V(:,end); S(end,end) = -S(end,end); T = V * U'; endif endif ## Apply scaling (if requested) traceTA = sum (diag (S)); if (scaling) b = traceTA * normX / normY; d = 1 - traceTA .^ 2; if (nargout > 1) Z = normX * traceTA * Y_0 * T + repmat (Xmu, Xp, 1); endif else b = 1; d = 1 + Ysumsq / Xsumsq - 2 * traceTA * normY / normX; if (nargout > 1) Z = normY * Y_0 * T + repmat (Xmu, Xp, 1); endif endif ## 3rd output argument if (nargout > 2) if (Yd < Xd) T = T(1:Yd,:); endif c = Xmu - b * Ymu * T; transform = struct ('T', T, 'b', b, 'c', repmat (c, Xp, 1)); endif ## Special cases elseif constX # Identical points in X d = 0; Z = repmat (Xmu, Xp, 1); T = eye (Yd, Xd); transform = struct ('T', T, 'b', 0, 'c', Z); else # Identical points in Y d = 1; Z = repmat (Xmu, Xp, 1); T = eye (Yd, Xd); transform = struct ('T', T, 'b', 0, 'c', Z); endif endfunction %!demo %! ## Create some random points in two dimensions %! rng (42); %! n = 10; %! X = normrnd (0, 1, [n, 2]); %! %! ## Those same points, rotated, scaled, translated, plus some noise %! S = [0.5, -sqrt(3)/2; sqrt(3)/2, 0.5]; # rotate 60 degrees %! Y = normrnd (0.5*X*S + 2, 0.05, n, 2); %! %! ## Conform Y to X, plot original X and Y, and transformed Y %! [d, Z] = procrustes (X, Y); %! plot (X(:,1), X(:,2), 'rx', Y(:,1), Y(:,2), 'b.', Z(:,1), Z(:,2), 'bx'); %!demo %! ## Find Procrustes distance and plot superimposed shape %! %! X = [40 88; 51 88; 35 78; 36 75; 39 72; 44 71; 48 71; 52 74; 55 77]; %! Y = [36 43; 48 42; 31 26; 33 28; 37 30; 40 31; 45 30; 48 28; 51 24]; %! plot (X(:,1),X(:,2),'x'); %! hold on %! plot (Y(:,1),Y(:,2),'o'); %! xlim ([0 100]); %! ylim ([0 100]); %! legend ('Target shape (X)', 'Source shape (Y)'); %! [d, Z] = procrustes (X, Y) %! plot (Z(:,1), Z(:,2), 's'); %! legend ('Target shape (X)', 'Source shape (Y)', 'Transformed shape (Z)'); %! hold off %!demo %! ## Apply Procrustes transformation to larger set of points %! %! ## Create matrices with landmark points for two triangles %! X = [5, 0; 5, 5; 8, 5]; # target %! Y = [0, 0; 1, 0; 1, 1]; # source %! %! ## Create a matrix with more points on the source triangle %! Y_mp = [linspace(Y(1,1),Y(2,1),10)', linspace(Y(1,2),Y(2,2),10)'; ... %! linspace(Y(2,1),Y(3,1),10)', linspace(Y(2,2),Y(3,2),10)'; ... %! linspace(Y(3,1),Y(1,1),10)', linspace(Y(3,2),Y(1,2),10)']; %! %! ## Plot both shapes, including the larger set of points for the source shape %! plot ([X(:,1); X(1,1)], [X(:,2); X(1,2)], 'bx-'); %! hold on %! plot ([Y(:,1); Y(1,1)], [Y(:,2); Y(1,2)], 'ro-', 'MarkerFaceColor', 'r'); %! plot (Y_mp(:,1), Y_mp(:,2), 'ro'); %! xlim ([-1 10]); %! ylim ([-1 6]); %! legend ('Target shape (X)', 'Source shape (Y)', ... %! 'More points on Y', 'Location', 'northwest'); %! hold off %! %! ## Obtain the Procrustes transformation %! [d, Z, transform] = procrustes (X, Y) %! %! ## Use the Procrustes transformation to superimpose the more points (Y_mp) %! ## on the source shape onto the target shape, and then visualize the results. %! Z_mp = transform.b * Y_mp * transform.T + transform.c(1,:); %! figure %! plot ([X(:,1); X(1,1)], [X(:,2); X(1,2)], 'bx-'); %! hold on %! plot ([Y(:,1); Y(1,1)], [Y(:,2); Y(1,2)], 'ro-', 'MarkerFaceColor', 'r'); %! plot (Y_mp(:,1), Y_mp(:,2), 'ro'); %! xlim ([-1 10]); %! ylim ([-1 6]); %! plot ([Z(:,1); Z(1,1)],[Z(:,2); Z(1,2)],'ks-','MarkerFaceColor','k'); %! plot (Z_mp(:,1),Z_mp(:,2),'ks'); %! legend ('Target shape (X)', 'Source shape (Y)', ... %! 'More points on Y', 'Transformed source shape (Z)', ... %! 'Transformed additional points', 'Location', 'northwest'); %! hold off %!demo %! ## Compare shapes without reflection %! %! T = [33, 93; 33, 87; 33, 80; 31, 72; 32, 65; 32, 58; 30, 72; ... %! 28, 72; 25, 69; 22, 64; 23, 59; 26, 57; 30, 57]; %! S = [48, 83; 48, 77; 48, 70; 48, 65; 49, 59; 49, 56; 50, 66; ... %! 52, 66; 56, 65; 58, 61; 57, 57; 54, 56; 51, 55]; %! plot (T(:,1), T(:,2), 'x-'); %! hold on %! plot (S(:,1), S(:,2), 'o-'); %! legend ('Target shape (d)', 'Source shape (b)'); %! hold off %! d_false = procrustes (T, S, 'reflection', false); %! printf ("Procrustes distance without reflection: %f\n", d_false); %! d_true = procrustes (T, S, 'reflection', true); %! printf ("Procrustes distance with reflection: %f\n", d_true); %! d_best = procrustes (T, S, 'reflection', 'best'); %! printf ("Procrustes distance with best fit: %f\n", d_true); ## Test input validation %!error procrustes (); %!error procrustes (1); %!error procrustes (1, 2, 3, 4, 5, 6, 7); %!error ... %! procrustes (ones (2, 2, 2), ones (2, 2, 2)); %!error ... %! procrustes ([1, 2; -3, 4; 2, 3], [1, 2; -3, 4; 2, 3+i]); %!error ... %! procrustes ([1, 2; -3, 4; 2, 3], [1, 2; -3, 4; 2, NaN]); %!error ... %! procrustes ([1, 2; -3, 4; 2, 3], [1, 2; -3, 4; 2, Inf]); %!error ... %! procrustes (ones (10 ,3), ones (11, 3)); %!error ... %! procrustes (ones (10 ,3), ones (10, 4)); %!error ... %! procrustes (ones (10 ,3), ones (10, 3), 'reflection'); %!error ... %! procrustes (ones (10 ,3), ones (10, 3), true); %!error ... %! procrustes (ones (10 ,3), ones (10, 3), 'scaling', 0); %!error ... %! procrustes (ones (10 ,3), ones (10, 3), 'scaling', [true true]); %!error ... %! procrustes (ones (10 ,3), ones (10, 3), 'reflection', 1); %!error ... %! procrustes (ones (10 ,3), ones (10, 3), 'reflection', 'some'); %!error ... %! procrustes (ones (10 ,3), ones (10, 3), 'param1', 'some'); statistics-release-2.1.2/inst/Dimensionality_Reduction/relieff.m000066400000000000000000001024641527311104300251030ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{idx} =} relieff (@var{X}, @var{Y}, @var{k}) ## @deftypefnx {statistics} {@var{idx} =} relieff (@var{X}, @var{Y}, @var{k}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{idx}, @var{weights}] =} relieff (@dots{}) ## ## Rank the importance of predictors with the ReliefF or RReliefF algorithm. ## ## @code{@var{idx} = relieff (@var{X}, @var{Y}, @var{k})} ranks the ## predictors, the columns of the matrix @var{X}, by how well they tell apart ## observations with different responses @var{Y}, judged by the @var{k} ## nearest neighbors of each observation. @var{idx} is a row vector of ## column indices, from the most important predictor to the least. ## ## @var{X} is a matrix of type double or single, with one row per observation. ## @var{Y} holds one response per row of @var{X}: a numeric vector, a logical ## vector, a categorical vector, a cell array of character vectors, a string ## array, or a character matrix with one row per observation. A numeric ## @var{Y} is a regression problem and any other type a classification ## problem, unless @qcode{'method'} says otherwise. Rows with a missing value ## (@qcode{NaN}) in @var{X} or a missing response (@qcode{NaN}, an empty or ## blank name, or an undefined category) are left out. @var{k} is a positive ## integer. ## ## @code{[@var{idx}, @var{weights}] = relieff (@dots{})} also returns the ## weight of each predictor, a row vector in the order of the columns of ## @var{X}, of type double. A larger weight marks a more important predictor, ## and weights range from -1 to 1. @var{idx} sorts @var{weights} in ## descending order. ## ## Predictors are compared on their range: for continuous predictors, the ## difference between two observations in predictor @math{j} is ## @math{|x_rj - x_qj|} divided by the range of predictor @math{j}, and the ## distance between them is the sum of these differences over all predictors ## (city block distance). A predictor holding a single value is left out of ## the distance and has weight @qcode{NaN}, ranked last. For categorical ## predictors the difference is 0 when two observations share a value and 1 ## otherwise. The neighbors of an observation are the @var{k} observations ## nearest to it, ties between equal distances going to the observation that ## comes first in @var{X}. Each neighbor counts with a weight that falls with ## its rank @math{i} among them as @math{exp (-(i / sigma)^2)}, normalized to ## sum to 1 over the neighbors. ## ## For classification (ReliefF), each observation @math{r} has @var{k} ## nearest neighbors in its own class (hits) and @var{k} in each of the other ## classes (misses), or the whole class when it holds fewer. The weight of ## predictor @math{j} falls by the weighted difference to the hits and rises ## by the weighted difference to the misses of class @math{c}, scaled by ## @math{p_c / (1 - p_r)}, @math{p} being the prior probabilities of the ## classes; each observation adds @math{1/m} of its own share, @math{m} being ## the number of observations used. For regression (RReliefF), with ## @math{dy} the difference of two responses divided by the range of @var{Y} ## and @math{dj} the difference in predictor @math{j}, the weighted sums of ## @math{dy}, @math{dj} and @math{dy dj} over the @var{k} nearest neighbors ## of each observation are accumulated as @math{W_dy}, @math{W_dj} and ## @math{W_dydj}, and the weight of predictor @math{j} is ## @math{W_dydj / W_dy - (W_dj - W_dydj) / (m - W_dy)}. ## ## The following @qcode{Name-Value} pairs are supported: ## ## @multitable @columnfractions 0.2 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'method'} @tab @qcode{'classification'} or ## @qcode{'regression'}. The default is @qcode{'regression'} for a numeric ## @var{Y} and @qcode{'classification'} otherwise. A numeric @var{Y} may be ## classified, its distinct values being the classes, but only a numeric ## @var{Y} can be regressed. ## ## @item @qcode{'prior'} @tab The prior probabilities of the classes, for ## classification only: @qcode{'empirical'}, the class frequencies in @var{Y}, ## which is the default; @qcode{'uniform'}, equal for every class; a numeric ## vector with one value per class; or a structure with the fields ## @qcode{group}, the class names, and @qcode{prob}, their probabilities. A ## numeric vector follows the order of the classes in @var{Y}: sorted values ## for a numeric, logical or categorical @var{Y}, and order of first ## appearance for text. A structure must name every class of @var{Y} and may ## name others, which are ignored. The probabilities are normalized to sum ## to 1, and the observations of a class with prior probability 0 are left ## out. ## ## @item @qcode{'updates'} @tab The number of observations, drawn at random ## without replacement, for which neighbors are sought: a positive integer, or ## @qcode{'all'}, the default, for every observation. A number at least as ## large as the number of observations uses them all. ## ## @item @qcode{'categoricalx'} @tab @qcode{'on'} to treat every predictor as ## categorical, or @qcode{'off'}, the default, to treat every predictor as ## continuous. ## ## @item @qcode{'sigma'} @tab The distance scaling factor of the rank weights ## of the neighbors, a positive scalar. The default is @code{Inf} for ## classification, weighing all neighbors alike, and 50 for regression. ## @end multitable ## ## When @var{X} is single, the weights are computed in single precision and ## returned as double. The option names are not case sensitive but must be ## given in full, where MATLAB accepts any shortened name. Where MATLAB ## accepts a non-integer @var{k} or @qcode{'updates'}, answers for complex or ## infinite values in @var{X}, gives weights for a single class of responses, ## and returns @qcode{NaN} for a constant response, @code{relieff} refuses ## them. It also accepts numeric class names in a @qcode{'prior'} structure, ## which MATLAB refuses. ## ## References: ## @enumerate ## @item ## I. Kononenko, E. @v{S}imec and M. Robnik-@v{S}ikonja. Overcoming the ## myopia of inductive learning algorithms with RELIEFF. ## @emph{Applied Intelligence}, 7(1):39--55, 1997. ## @item ## M. Robnik-@v{S}ikonja and I. Kononenko. Theoretical and empirical ## analysis of ReliefF and RReliefF. @emph{Machine Learning}, ## 53(1-2):23--69, 2003. ## @end enumerate ## ## @seealso{pca, fitcknn, knnsearch} ## @end deftypefn function [idx, weights] = relieff (X, Y, k, varargin) if (nargin < 3) print_usage (); endif if (! (isfloat (X) && isreal (X) && ismatrix (X) && ! isempty (X))) error ("relieff: X must be a real matrix of type double or single."); endif if (ischar (Y)) nY = rows (Y); elseif ((isnumeric (Y) || islogical (Y) || iscategorical (Y) || iscellstr (Y) || isstring (Y)) && isvector (Y)) nY = numel (Y); else error ("relieff: Y must be a vector of responses or class names."); endif if (rows (X) != nY) error ("relieff: X must have as many rows as Y has observations."); endif if (! (isnumeric (k) && isreal (k) && isscalar (k) && isfinite (k) && k > 0 && k == fix (k))) error ("relieff: K must be a positive integer scalar."); endif ## Parse optional Name-Value paired arguments optNames = {'method', 'prior', 'updates', 'categoricalx', 'sigma'}; dfValues = {[], [], 'all', 'off', []}; [method, prior, updates, catX, sigma, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (args)) if (mod (numel (args), 2) == 0 && ischar (args{1})) error ("relieff: unknown parameter name '%s'.", args{1}); endif error ("relieff: optional arguments must be Name-Value pairs."); endif if (isempty (method)) if (isnumeric (Y)) method = 'regression'; else method = 'classification'; endif elseif (! ((ischar (method) && isrow (method)) || (isstring (method) && isscalar (method)))) error ("relieff: 'method' must be a character vector."); endif method = lower (char (method)); if (! any (strcmp (method, {'classification', 'regression'}))) error ("relieff: unknown method '%s'.", method); endif isreg = strcmp (method, 'regression'); if (isreg && ! isnumeric (Y)) error ("relieff: regression needs a numeric Y."); endif if (isreg && ! isempty (prior)) error ("relieff: 'prior' applies to classification only."); endif if (isempty (prior)) prior = 'empirical'; endif if (isstring (updates) && isscalar (updates)) updates = char (updates); endif if (ischar (updates)) if (! strcmpi (updates, 'all')) error ("relieff: 'updates' must be 'all' or a positive integer."); endif updates = Inf; elseif (! (isnumeric (updates) && isreal (updates) && isscalar (updates) && isfinite (updates) && updates > 0 && updates == fix (updates))) error ("relieff: 'updates' must be 'all' or a positive integer."); endif if (isstring (catX) && isscalar (catX)) catX = char (catX); endif if (! (ischar (catX) && any (strcmpi (catX, {'on', 'off'})))) error ("relieff: 'categoricalx' must be 'on' or 'off'."); endif catX = strcmpi (catX, 'on'); if (isempty (sigma)) if (isreg) sigma = 50; else sigma = Inf; endif elseif (! (isnumeric (sigma) && isreal (sigma) && isscalar (sigma) && sigma > 0)) error ("relieff: 'sigma' must be a positive scalar."); endif ## Responses as values or class indices, with the missing ones as NaN if (isreg) y = double (Y(:)); if (any (isinf (y))) error ("relieff: Y must not contain infinite values."); endif else [y, gn, gl] = grp2idx (Y); endif if (any (isinf (X(:)))) error ("relieff: X must not contain infinite values."); endif keep = ! (any (isnan (X), 2) | isnan (y)); if (! isreg) ## Classes present in the data, in the order of grp2idx held = unique (y(keep)); [~, y] = ismember (y, held); gn = gn(held); gl = gl(held); nc = numel (held); if (ischar (prior) || (isstring (prior) && isscalar (prior))) prior = lower (char (prior)); if (strcmp (prior, 'empirical')) prior = accumarray (y(keep), 1, [nc, 1])'; elseif (strcmp (prior, 'uniform')) prior = ones (1, nc); else error ("relieff: unknown 'prior' value '%s'.", prior); endif elseif (isstruct (prior)) if (! (isscalar (prior) && all (isfield (prior, {'group', 'prob'})))) error (strcat ("relieff: a 'prior' structure must have the", ... " fields 'group' and 'prob'.")); endif group = prior.group; prob = prior.prob; if (! (isnumeric (prob) && isreal (prob) && isvector (prob))) error ("relieff: 'prob' in 'prior' must be a numeric vector."); endif if ((isnumeric (group) || islogical (group)) && (isnumeric (Y) || islogical (Y))) group = double (group(:)); gl = double (gl); elseif (ischar (group)) group = cellstr (group); gl = gn; elseif (iscellstr (group) || isstring (group) || iscategorical (group)) group = cellstr (group(:)); gl = gn; else error (strcat ("relieff: 'group' in 'prior' must name the", ... " classes of Y.")); endif if (numel (group) != numel (prob)) error (strcat ("relieff: 'group' and 'prob' in 'prior' must", ... " have the same length.")); endif [found, at] = ismember (gl, group); if (! all (found)) error ("relieff: 'prior' has no probability for class '%s'.", ... gn{find (! found, 1)}); endif prior = prob(at); elseif (! (isnumeric (prior) && isreal (prior) && isvector (prior) && numel (prior) == nc)) error ("relieff: 'prior' must be a vector of %d probabilities.", nc); endif prior = double (prior(:)'); if (! (all (isfinite (prior)) && all (prior >= 0) && any (prior > 0))) error (strcat ("relieff: prior probabilities must be finite,", ... " non-negative and not all zero.")); endif prior = prior / sum (prior); ## The observations of a class with prior probability 0 are left out keep &= ismember (y, find (prior > 0)); if (sum (prior > 0) < 2) error (strcat ("relieff: Y must hold at least two classes with", ... " positive prior probability.")); endif endif X = X(keep,:); y = y(keep); [n, p] = size (X); if (n < 2) error ("relieff: X and Y must hold at least two complete observations."); endif ## Differences between observations, scaled by the range of each predictor if (catX) diffs = @(r) cast (X != X(r,:), class (X)); used = true (1, p); else w = 1 ./ (max (X, [], 1) - min (X, [], 1)); diffs = @(r) abs (X - X(r,:)) .* w; used = isfinite (w); endif ## Observations whose neighbors are sought if (updates >= n) upd = 1:n; else upd = randperm (n, updates); endif m = numel (upd); if (isreg) yrange = max (y) - min (y); if (yrange == 0) error ("relieff: Y must not be constant."); endif Wdy = 0; Wdj = zeros (1, p, class (X)); Wdydj = zeros (1, p, class (X)); for r = upd D = diffs (r); cand = [1:r-1, r+1:n]'; [~, o] = sort (sum (D(cand, used), 2)); nb = cand(o(1:min (k, n - 1))); ## Normalized rank weights of the neighbors dw = exp (- ((1:numel (nb))' / sigma) .^ 2); dw /= sum (dw); dy = abs (y(nb) - y(r)) / yrange; Wdy += sum (dy .* dw); Wdj += sum (D(nb,:) .* dw, 1); Wdydj += sum (D(nb,:) .* (dy .* dw), 1); endfor weights = Wdydj / Wdy - (Wdj - Wdydj) / (m - Wdy); else weights = zeros (1, p, class (X)); members = arrayfun (@(c) find (y == c), 1:nc, 'UniformOutput', false); for r = upd D = diffs (r); for c = 1:nc cand = members{c}; cand(cand == r) = []; if (isempty (cand)) continue; endif [~, o] = sort (sum (D(cand, used), 2)); nb = cand(o(1:min (k, numel (cand)))); dw = exp (- ((1:numel (nb))' / sigma) .^ 2); share = sum (D(nb,:) .* (dw / sum (dw)), 1) / m; if (c == y(r)) weights -= share; else weights += prior(c) / (1 - prior(y(r))) * share; endif endfor endfor endif weights = double (weights); ## Most important first, a NaN weight last [~, idx] = sort (- weights); endfunction %!demo %! ## Rank the four measurements of the iris flowers by how well they tell %! ## the three species apart %! load fisheriris %! [idx, weights] = relieff (meas, species, 10); %! bar (weights(idx)); %! xticks (1:4); %! xticklabels (cellstr (num2str (idx'))); %! xlabel ("Predictor (column of meas), most important first"); %! ylabel ("Predictor importance weight"); ## Test output ## Values checked against MATLAB R2024a %!test %! load fisheriris %! [idx, w] = relieff (meas, species, 10); %! assert_equal (idx, [4, 3, 1, 2]); %! assert_equal (w, [0.13990740740740726, 0.12258333333333336, ... %! 0.35898870056497195, 0.37541666666666723], 1e-14); %!test %! load fisheriris %! [~, w] = relieff (meas, species, 60); %! assert_equal (w, [0.15579758125472401, 0.067501360544217723, ... %! 0.38647167070217914, 0.40577256235827669], 1e-14); %!test %! load fisheriris %! y = strcmp (species(51:150), 'virginica'); %! [~, w] = relieff (meas(51:150,:), y, 10); %! assert_equal (w, [0.045166666666666674, 0.017999999999999992, ... %! 0.14182051282051272, 0.2323333333333332], 1e-14); %!test %! load fisheriris %! [~, w] = relieff (meas, char (species), 10); %! assert_equal (w, [0.13990740740740731, 0.12258333333333328, ... %! 0.35898870056497167, 0.37541666666666712], 1e-14); %!test %! load fisheriris %! [~, w] = relieff (meas, string (species), 10); %! assert_equal (w, [0.13990740740740737, 0.12258333333333331, ... %! 0.35898870056497195, 0.37541666666666701], 1e-14); %!test %! load fisheriris %! s = [1:53, 101:150]; %! P = struct ('group', {{'virginica', 'setosa', 'versicolor'}}, ... %! 'prob', [0.5, 0.2, 0.3]); %! [~, w] = relieff (meas(s,:), species(s), 5, 'prior', P); %! assert_equal (w, [0.23459855139466784, 0.12308504602193938, ... %! 0.41972930722395935, 0.45912390198797975], 1e-14); %!test %! load fisheriris %! [~, w] = relieff (meas(1:20,2:4), meas(1:20,1), 3); %! assert_equal (w, [0.039264168421865836, 0.009725964602631948, ... %! -0.0081765673957266277], 1e-14); %!test %! load carbig %! X = [grp2idx(Mfg), grp2idx(Model), grp2idx(Origin)]; %! [idx, w] = relieff (X, MPG, 10, 'categoricalx', 'on'); %! assert_equal (idx, [2, 3, 1]); %! assert_equal (w, [-0.0018716957152787317, 0.050096007760873751, ... %! 0.011404762142927625], 1e-14); %!test %! load carbig %! X = [grp2idx(Mfg), grp2idx(Model), grp2idx(Origin)]; %! [~, w] = relieff (X(1:60,:), Origin(1:60,:), 5, 'categoricalx', 'on'); %! assert_equal (w, [0.73055555555555629, 0.077777777777777751, ... %! 1.0000000000000013], 1e-13); %!shared X, y %! X = [0, 0; 0.5, 0.5; 0.9, 0; 1, 1; 0.2, 0.9; 0.8, 0.3; 0.1, 0.6; 0.6, 0.9]; %! y = [1; 1; 2; 2; 1; 2; 1; 2]; %!test %! [~, w] = relieff (X, y, 1, 'method', 'classification'); %! assert_equal (w, [0.34999999999999998, -0.16249999999999998], 1e-15); %!test %! [~, w] = relieff (X, y, 2, 'method', 'classification'); %! assert_equal (w, [0.29375000000000001, -0.17499999999999999], 1e-15); %!test %! [~, w] = relieff (X, y, 3, 'method', 'classification'); %! assert_equal (w, [0.34583333333333333, -0.21666666666666667], 1e-15); %!test %! [~, w] = relieff (X, y, 2, 'method', 'classification', 'sigma', 1); %! assert_equal (w, [0.34466458926752375, -0.16368564682943915], 1e-15); %!test %! X = [1, 2; 1, 2; 2, 3; 3, 3; 1, 1; 2, 2; 3, 1; 1, 3]; %! [idx, w] = relieff (X, [1; 1; 1; 1; 2; 2; 2; 2] > 1, 2); %! assert_equal (idx, [2, 1]); %! assert_equal (w, [-0.25, 0.0625], 1e-15); %!shared XA, yA, gA, XB, yB, gB %! XA = [0.22665944695472717, 0.048830259591341019, 0.25411459803581238; ... %! 0.2114872932434082, 0.77445060014724731, 0.53388506174087524; ... %! 0.33685088157653809, 0.34994497895240784, 0.47933411598205566; ... %! 0.056142527610063553, 0.34475591778755188, 0.27004486322402954; ... %! 0.6719900369644165, 0.21916352212429047, 0.965262770652771; ... %! 0.43299207091331482, 0.28173792362213135, 0.96468651294708252; ... %! 0.59951025247573853, 0.25370854139328003, 0.90369051694869995; ... %! 0.61985927820205688, 0.29071399569511414, 0.25213038921356201; ... %! 0.008284449577331543, 0.14870639145374298, 0.8450893759727478; ... %! 0.55917549133300781, 0.58620524406433105, 0.29522070288658142; ... %! 0.57303112745285034, 0.71820592880249023, 0.43747693300247192; ... %! 0.38736355304718018, 0.98570030927658081, 0.9433753490447998; ... %! 0.72225898504257202, 0.32017061114311218, 0.2949681282043457; ... %! 0.23183286190032959, 0.81152999401092529, 0.79241269826889038; ... %! 0.085462123155593872, 0.64490348100662231, 0.016459200531244278; ... %! 0.12298344820737839, 0.52148056030273438, 0.55653321743011475; ... %! 0.55263209342956543, 0.87742847204208374, 0.13704623281955719; ... %! 0.40711727738380432, 0.19621142745018005, 0.90734457969665527; ... %! 0.61790573596954346, 0.8348698616027832, 0.016985762864351273; ... %! 0.76110851764678955, 0.46426743268966675, 0.20533750951290131; ... %! 0.90961301326751709, 0.54733520746231079, 0.81192338466644287; ... %! 0.31863677501678467, 0.44641050696372986, 0.61500537395477295; ... %! 0.23273782432079315, 0.73465442657470703, 0.74490165710449219; ... %! 0.18065030872821808, 0.39856430888175964, 0.81240725517272949; ... %! 0.1189243420958519, 0.20111806690692902, 0.59000658988952637; ... %! 0.32951122522354126, 0.56738686561584473, 0.39614564180374146; ... %! 0.91682279109954834, 0.52208811044692993, 0.78768277168273926; ... %! 0.1428496241569519, 0.74294054508209229, 0.99268949031829834; ... %! 0.97257781028747559, 0.54679542779922485, 0.62209063768386841; ... %! 0.48548838496208191, 0.87875497341156006, 0.41486015915870667; ... %! 0.39223554730415344, 0.62517315149307251, 0.44906029105186462; ... %! 0.29200407862663269, 0.85335928201675415, 0.31995701789855957; ... %! 0.77067995071411133, 0.64253586530685425, 0.18106210231781006; ... %! 0.54787242412567139, 0.67721915245056152, 0.72045046091079712; ... %! 0.12052302062511444, 0.69361585378646851, 0.053895667195320129; ... %! 0.66175878047943115, 0.90023434162139893, 0.12591096758842468; ... %! 0.32526770234107971, 0.26533603668212891, 0.059136319905519485; ... %! 0.92174309492111206, 0.10128919035196304, 0.5817381739616394; ... %! 0.96077883243560791, 0.85762202739715576, 0.90069860219955444; ... %! 0.26504355669021606, 0.51390761137008667, 0.94491356611251831]; %! yA = [4.9158596992492676; 5.7109177112579346; 0.63560187816619873; ... %! 0.31373497098684311; 4.1956847906112671; 1.6540294885635376; ... %! 9.9510228633880615; 7.8833210468292236; 7.1206676959991455; ... %! 6.3018262386322021; 7.5598502159118652; 8.6826205253601074; ... %! 9.9508476257324219; 9.4992786645889282; 2.8919506072998047; ... %! 3.5654160380363464; 1.446286141872406; 4.3914371728897095; ... %! 4.0492373704910278; 1.2478844821453094; 4.1050773859024048; ... %! 4.5266264677047729; 1.7514462769031525; 3.5887551307678223; ... %! 3.4448394179344177; 1.9565299153327942; 1.4055664837360382; ... %! 2.7674135565757751; 0.063248542137444019; 2.8539523482322693; ... %! 5.6255972385406494; 9.9377048015594482; 6.2569814920425415; ... %! 3.4007266163825989; 4.4449138641357422; 8.6494183540344238; ... %! 9.2109048366546631; 2.5252458453178406; 0.74270054697990417; ... %! 4.1986265778541565]; %! gA = mod ((1:40)', 3) + 1; %! XB = [0, 8; 8, 0; 1, 2; 0, 7; 7, 1; 5, 3; 4, 4; 4, 8; 5, 7; 3, 6; ... %! 1, 2; 3, 4; 3, 3; 4, 8; 8, 1; 2, 8; 7, 1; 7, 1; 0, 2; 7, 7; ... %! 7, 0; 6, 4; 7, 6; 1, 3]; %! yB = [0; 16; 3; 12; 6; 0; 4; 11; 11; 5; 14; 6; 4; 5; 15; 8; 13; 3; ... %! 13; 15; 2; 3; 4; 3]; %! gB = mod ((1:24)', 2) + 1; %!test %! [~, w] = relieff (XA, yA, 5); %! assert_equal (w, [0.01388223198910471, -0.0092574193195629761, ... %! -0.012105763609689901], 1e-15); %!test %! [~, w] = relieff (XA, yA, 5, 'sigma', 2); %! assert_equal (w, [-0.0041728199442636477, -0.014809858819979496, ... %! -0.010613408476370462], 1e-15); %!test %! [~, w] = relieff (XA, yA, 2); %! assert_equal (w, [-0.0042147304959920528, -0.01845014821865984, ... %! -0.0021988211394621537], 1e-15); %!test %! [~, w] = relieff (XA(1:6,:), yA(1:6), 10); %! assert_equal (w, [-0.053827585946063905, 0.079324770706013403, ... %! -0.13727902710184209], 1e-15); %!test %! [~, w] = relieff (XA, yA, 5, 'updates', 100); %! assert_equal (w, [0.01388223198910471, -0.0092574193195630039, ... %! -0.012105763609689846], 1e-15); %!test %! [~, w] = relieff (XA, gA, 5, 'method', 'classification'); %! assert_equal (w, [0.0021386292049166023, 0.026522833631013005, ... %! -0.033770330323774092], 1e-15); %!test %! [~, w] = relieff (XA, gA, 5, 'method', 'classification', 'sigma', 2); %! assert_equal (w, [0.0024473915815581577, 0.0047736034538774725, ... %! -0.028887639163964078], 1e-15); %!test %! g = gA; %! g(5) = 4; %! [~, w] = relieff (XA, g, 5, 'method', 'classification'); %! assert_equal (w, [0.0085623364208488022, 0.039399610632522876, ... %! -0.0025562241901765361], 1e-15); %!test %! [~, w] = relieff (XA, gA, 5, 'method', 'classification', ... %! 'prior', [0, 0.5, 0.5]); %! assert_equal (w, [0.019765058234956511, 0.035556998164938798, ... %! -0.064207251594005171], 1e-15); %!test %! [~, w] = relieff (XA, gA, 5, 'method', 'classification', ... %! 'prior', [2, 3, 5]); %! assert_equal (w, [-0.00017399953766534648, 0.028017907734860202, ... %! -0.037361123235406815], 1e-15); %!test %! P = struct ('group', [3, 1, 2], 'prob', [0.5, 0.2, 0.3]); %! [~, w] = relieff (XA, gA, 5, 'method', 'classification', 'prior', P); %! assert_equal (w, [-0.00017399953766534387, 0.028017907734860195, ... %! -0.037361123235406808], 1e-15); %!test %! P = struct ('group', {{'3', '1', '2', '7'}}, 'prob', [0.5, 0.2, 0.3, 0.4]); %! [~, w] = relieff (XA, cellstr (num2str (gA)), 5, 'prior', P); %! assert_equal (w, [-0.00017399953766534387, 0.028017907734860188, ... %! -0.037361123235406787], 1e-15); %!test %! P = struct ('group', {{'3', '1', '2'}}, 'prob', [5, 2, 3]); %! [~, w] = relieff (XA, cellstr (num2str (gA)), 5, 'prior', P); %! assert_equal (w, [-0.00017399953766534561, 0.028017907734860195, ... %! -0.037361123235406801], 1e-15); %!test %! [~, w] = relieff (XA, cellstr (num2str (gA)), 5, 'prior', [0.2; 0.3; 0.5]); %! assert_equal (w, [0.002374388536664899, 0.029954823922265453, ... %! -0.028297242346431235], 1e-15); %!test %! [~, w] = relieff (XA, categorical (gA, 1:4), 5); %! assert_equal (w, [0.002138629204916604, 0.026522833631012988, ... %! -0.033770330323774085], 1e-15); %!test %! [~, w] = relieff (XA, categorical (gA, 1:4), 5, 'prior', 'uniform'); %! assert_equal (w, [0.0020399923619240142, 0.02693029952279568, ... %! -0.033439605784320946], 1e-15); %!test %! [~, w] = relieff (XA, categorical (gA, 1:4), 5, 'prior', [0.2, 0.3, 0.5]); %! assert_equal (w, [-0.00017399953766534474, 0.028017907734860192, ... %! -0.037361123235406801], 1e-15); %!test %! X = XA; %! X([3, 7], 2) = NaN; %! y = yA; %! y(10) = NaN; %! [~, w] = relieff (X, y, 5); %! assert_equal (w, [0.0096723878723309897, -0.0050429741167224706, ... %! -0.014019835775775619], 1e-15); %!test %! X = XA; %! X([3, 7], 2) = NaN; %! y = cellstr (num2str (gA)); %! y{12} = ''; %! [~, w] = relieff (X, y, 5); %! assert_equal (w, [-0.0048661229809348686, 0.045375869235758988, ... %! -0.019229982110778903], 1e-15); %!test %! [idx, w] = relieff ([XA(:,1:2), 3 * ones(40, 1)], yA, 5); %! assert_equal (idx, [1, 2, 3]); %! assert_equal (w, [0.0060575278856132564, -0.011272620704472172, NaN], ... %! 1e-15); %!test %! [idx, w] = relieff ([XA(:,1:2), 3 * ones(40, 1)], gA, 5, ... %! 'method', 'classification'); %! assert_equal (idx, [1, 2, 3]); %! assert_equal (w, [0.015455906238505155, -0.0058458986874681575, NaN], ... %! 1e-15); %!test %! [idx, w] = relieff (single (XA), yA, 5); %! assert_equal (class (idx), 'double'); %! assert_equal (class (w), 'double'); %! assert_equal (w, [0.013882221976705744, -0.0092574315350100744, ... %! -0.012105737249683085], 1e-6); %!test %! [~, w] = relieff (single (XA), gA, 5, 'method', 'classification'); %! assert_equal (w, [0.0021386349108070135, 0.026522833853960037, ... %! -0.033770337700843811], 1e-6); %!test %! [~, w] = relieff (XB, yB, 4); %! assert_equal (w, [-0.015833802328636792, -0.017033417536778783], 1e-15); %!test %! [~, w] = relieff (XB, yB, 4, 'sigma', 1); %! assert_equal (w, [-0.0062979535245773766, -0.026549633818311213], 1e-15); %!test %! [~, w] = relieff (XB, yB, 4, 'sigma', Inf); %! assert_equal (w, [-0.015822377582084102, -0.017017026629918081], 1e-15); %!test %! [~, w] = relieff (XB, gB, 3, 'method', 'classification'); %! assert_equal (w, [-0.020833333333333318, -0.012152777777777783], 1e-15); %!test %! [~, w] = relieff (XB, gB, 3, 'method', 'classification', 'sigma', 1); %! assert_equal (w, [-0.064720206045157824, -0.0019737829821077271], 1e-15); %!test %! [~, w] = relieff (XB, yB, 4, 'categoricalx', 'on'); %! assert_equal (w, [-0.0021677478940700379, 0.036294175269704521], 1e-15); %!test %! [~, w] = relieff (XB, yB, 4, 'categoricalx', 'on', 'sigma', 1); %! assert_equal (w, [-0.095389338861245093, -0.003446685860238996], 1e-15); %!test %! [~, w] = relieff (XB, gB, 3, 'method', 'classification', ... %! 'categoricalx', 'on'); %! assert_equal (w, [-0.1666666666666666, -0.013888888888888895], 1e-15); %!test %! rand ('seed', 1); %! [idx, w] = relieff (XA, yA, 5, 'updates', 10); %! assert_equal (sort (idx), [1, 2, 3]); %! assert_equal (all (isfinite (w)), true); ## Test input validation %!error relieff (XA, yA) %!error ... %! relieff (int8 (XB), yB, 4) %!error ... %! relieff (XA + 1i, yA, 5) %!error ... %! relieff (XA, [yA, yA], 5) %!error ... %! relieff (XA, yA(1:30), 5) %!error relieff (XA, yA, 0) %!error relieff (XA, yA, 1.5) %!error relieff (XA, yA, [5, 6]) %!error relieff (XA, yA, 5, 'foo', 1) %!error ... %! relieff (XA, yA, 5, 'method') %!error ... %! relieff (XA, yA, 5, 'method', 1) %!error relieff (XA, yA, 5, 'method', 'foo') %!error ... %! relieff (XA, cellstr (num2str (gA)), 5, 'method', 'regression') %!error ... %! relieff (XA, yA, 5, 'prior', 'uniform') %!error ... %! relieff (XA, yA, 5, 'updates', 0) %!error ... %! relieff (XA, yA, 5, 'updates', 1.5) %!error ... %! relieff (XA, yA, 5, 'updates', 'some') %!error ... %! relieff (XA, yA, 5, 'categoricalx', 'yes') %!error ... %! relieff (XA, yA, 5, 'categoricalx', true) %!error ... %! relieff (XA, yA, 5, 'sigma', 0) %!error ... %! relieff (XA, yA, 5, 'sigma', [1, 2]) %!error ... %! relieff (XA, [Inf; yA(2:end)], 5) %!error ... %! relieff ([Inf, 0, 0; XA(2:end,:)], yA, 5) %!error ... %! relieff (XA, gA, 5, 'method', 'classification', 'prior', 'foo') %!error ... %! relieff (XA, gA, 5, 'method', 'classification', 'prior', struct ('a', 1)) %!error ... %! relieff (XA, gA, 5, 'method', 'classification', 'prior', ... %! struct ('group', [1, 2, 3], 'prob', 'abc')) %!error ... %! relieff (XA, cellstr (num2str (gA)), 5, 'prior', ... %! struct ('group', [1, 2, 3], 'prob', [1, 1, 1])) %!error ... %! relieff (XA, gA, 5, 'method', 'classification', 'prior', ... %! struct ('group', [1, 2, 3], 'prob', [1, 1])) %!error ... %! relieff (XA, cellstr (num2str (gA)), 5, 'prior', ... %! struct ('group', {{'3', '1'}}, 'prob', [0.5, 0.2])) %!error ... %! relieff (XA, gA, 5, 'method', 'classification', 'prior', [0.5, 0.5]) %!error ... %! relieff (XA, gA, 5, 'method', 'classification', 'prior', [0.2, 0.3, NaN]) %!error ... %! relieff (XA, gA, 5, 'method', 'classification', 'prior', [-1, 1, 1]) %!error ... %! relieff (XA, gA, 5, 'method', 'classification', 'prior', [0, 0, 0]) %!error ... %! relieff (XA, gA, 5, 'method', 'classification', 'prior', [1, 0, 0]) %!error ... %! relieff (XA, ones (40, 1), 5, 'method', 'classification') %!error ... %! relieff (XA(1,:), yA(1), 5) %!error relieff (XA, ones (40, 1), 5) statistics-release-2.1.2/inst/Dimensionality_Reduction/rica.m000066400000000000000000000300341527311104300243760ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} rica (@var{X}, @var{Q}) ## @deftypefnx {statistics} {@var{Mdl} =} rica (@var{X}, @var{Q}, @var{Name}, @var{Value}) ## ## Reconstruction independent component analysis (RICA) for feature extraction. ## ## @code{@var{Mdl} = rica (@var{X}, @var{Q})} learns @var{Q} features from the ## @math{N * P} data matrix @var{X} (rows are observations, columns are ## predictors) and returns a @qcode{ReconstructionICA} object @var{Mdl}. Apply ## the learned transformation to data with ## @code{transform (@var{Mdl}, @var{X})}, which returns ## @code{@var{X} * @var{Mdl}.TransformWeights}. ## ## The @math{P * @var{Q}} weight matrix (with unit-length columns) minimizes the ## objective ## ## @example ## @var{Lambda} * ||@var{X} * @var{W} * @var{W}' - @var{X}||_F^2 ## + sum (sum (@var{g} (@var{X} * @var{W}))) ## @end example ## ## @noindent ## over the transformation weights @var{W}, combining a reconstruction cost with ## a sparsity contrast @var{g} applied elementwise and selected by ## @qcode{'ContrastFcn'}. ## ## Name/Value pairs: ## ## @table @asis ## @item @qcode{'IterationLimit'} ## Maximum number of iterations (default 1000). ## ## @item @qcode{'Lambda'} ## Weight of the reconstruction term (default 1). ## ## @item @qcode{'Standardize'} ## Logical; center and scale each predictor before fitting (default ## @qcode{false}). ## ## @item @qcode{'ContrastFcn'} ## The sparsity contrast @var{g} applied to each element @math{z} of ## @math{@var{X} * @var{W}}: @qcode{'logcosh'} (default), which is ## @math{0.5 * log (cosh (2 * z))}; @qcode{'exp'}, which is ## @math{-exp (-z^2 / 2)}; or @qcode{'sqrt'}, which is ## @math{sqrt (z^2 + 1e-8)}, a smooth stand-in for @math{abs (z)}. ## ## @item @qcode{'InitialTransformWeights'} ## A @math{P * @var{Q}} initial value for the weights. The default is random. ## ## @item @qcode{'NonGaussianityIndicator'} ## A @var{Q}-element vector of @math{+1} and @math{-1}, one per learned ## feature, setting the sign its contrast term carries in the objective: ## @math{+1} seeks a super-Gaussian (sparse) feature and @math{-1} a ## sub-Gaussian (spread) one. The default is all @math{+1}. ## ## @item @qcode{'GradientTolerance'}, @qcode{'StepTolerance'} ## Stop once the gradient's or the step's infinity norm falls to or below the ## given value (default @qcode{1e-6} each). They govern the fit only under ## @qcode{'Solver', 'lbfgs'}; the @qcode{'quasinewton'} solver runs to its own ## tighter internal tolerances and records these without acting on them. ## ## @item @qcode{'Solver'} ## @qcode{'quasinewton'} (default) minimizes through Octave's @code{fminunc}, ## which carries a full inverse Hessian. @qcode{'lbfgs'} selects the ## limited-memory BFGS solver MATLAB uses, holding as many curvature pairs as ## the transform has parameters, and is several times faster here. It stops ## where @qcode{'GradientTolerance'} and @qcode{'StepTolerance'} say to, so a ## value tighter than the default carries it further. ## @end table ## ## @subheading Note on reproducibility ## ## The RICA objective is not convex and is minimized by a quasi-Newton solver, ## so the learned weights depend on the starting point and the solver, and are ## only defined up to a permutation and sign of the feature columns. Different ## runs (or different software, including MATLAB) may return different weights ## that nonetheless describe an equally valid feature transformation. Fix ## @qcode{'InitialTransformWeights'} for a reproducible result. ## ## @seealso{ReconstructionICA, sparsefilt, pca} ## @end deftypefn function Mdl = rica (X, Q, varargin) ## Input validation if (nargin < 2) print_usage (); endif if (! (isnumeric (X) && ismatrix (X) && ndims (X) == 2)) error ("rica: X must be a numeric matrix."); endif if (! (isreal (X) && all (isfinite (X(:))))) error ("rica: X must be real and finite."); endif [n, p] = size (X); if (! (isscalar (Q) && isnumeric (Q) && Q >= 1 && Q == fix (Q))) error ("rica: Q must be a positive integer."); endif if (mod (numel (varargin), 2) != 0) error ("rica: Name/Value arguments must come in pairs."); endif ## Validate a supplied initial-weights matrix up front for a clear message for k = 1:2:numel (varargin) if (ischar (varargin{k}) && strcmpi (varargin{k}, "InitialTransformWeights")) W0 = varargin{k+1}; if (! (isnumeric (W0) && isequal (size (W0), [p, Q]))) error ("rica: 'InitialTransformWeights' must be a %d-by-%d matrix.", p, Q); endif endif endfor Mdl = ReconstructionICA (X, Q, varargin{:}); endfunction %!demo %! ## Learn two features from data with the default (random) start. %! X = [1, 2, 3, 4; 2, 3, 4, 5; -1, 0, 1, 2; 3, 1, 4, 1; 0, 2, 1, 3]; %! Mdl = rica (X, 2, "IterationLimit", 200); %! Z = transform (Mdl, X) ## The RICA objective is verified against MATLAB R2023b for ## X = reshape (mod ((1:60)*7, 13), 12, 5) - 6 with fixed initial weights. %!shared X, W0 %! X = reshape (mod ((1:60)*7, 13), 12, 5) - 6; %! W0 = reshape (mod ((1:15)*3, 7), 5, 3) - 3; %!test %! Mdl = rica (X, 3, "InitialTransformWeights", W0, "Lambda", 1, ... %! "Standardize", false, "IterationLimit", 1000); %! ## transform is X * TransformWeights, with unit-length weight columns %! assert_equal (transform (Mdl, X), X * Mdl.TransformWeights, 1e-12); %! assert_equal (sqrt (sum (Mdl.TransformWeights .^ 2, 1)), [1, 1, 1], 1e-10); %! assert_equal (size (Mdl.TransformWeights), [5, 3]); %!test %! ## The solver reaches a low objective value (MATLAB's is 194.5156). %! Mdl = rica (X, 3, "InitialTransformWeights", W0, "Lambda", 1, ... %! "Standardize", false, "IterationLimit", 1000); %! assert_equal (Mdl.FitInfo.Objective(end) < 195, true); %!test %! ## The objective value stored in FitInfo matches a direct evaluation. %! Mdl = rica (X, 3, "InitialTransformWeights", W0, "Lambda", 1, ... %! "Standardize", false, "IterationLimit", 1000); %! W = Mdl.TransformWeights; %! Z = X * W; %! f = sum (sumsq (X * (W * W') - X)) + sum (sum (0.5 * log (cosh (2 * Z)))); %! assert_equal (Mdl.FitInfo.Objective(end), f, 1e-6); %!test %! ## The 'exp' contrast objective matches a direct evaluation. %! Mdl = rica (X, 3, "InitialTransformWeights", W0, "Lambda", 1, ... %! "Standardize", false, "IterationLimit", 200, ... %! "ContrastFcn", "exp"); %! W = Mdl.TransformWeights; %! Z = X * W; %! f = sum (sumsq (X * (W * W') - X)) + sum (sum (-exp (-Z .^ 2 / 2))); %! assert_equal (Mdl.FitInfo.Objective(end), f, 1e-6); %!test %! ## The 'sqrt' contrast objective matches a direct evaluation, the smoothing %! ## constant included. %! Mdl = rica (X, 3, "InitialTransformWeights", W0, "Lambda", 1, ... %! "Standardize", false, "IterationLimit", 200, ... %! "ContrastFcn", "sqrt"); %! W = Mdl.TransformWeights; %! Z = X * W; %! f = sum (sumsq (X * (W * W') - X)) + sum (sum (sqrt (Z .^ 2 + 1e-8))); %! assert_equal (Mdl.FitInfo.Objective(end), f, 1e-6); %!test %! ## The smoothing constant is visible only where a projection is zero, so it %! ## is pinned there: one column of X projected onto a weight orthogonal to it. %! Xz = [1 0; -2 0; 3 0; 0 0]; %! Wz = [0; 1]; %! M = rica (Xz, 1, "InitialTransformWeights", Wz, "IterationLimit", 0, ... %! "Standardize", false, "ContrastFcn", "sqrt"); %! assert_equal (M.FitInfo.Objective(1), 14 + 4 * sqrt (1e-8), 1e-12); %!test %! ## Each contrast gives its own objective; they are not interchangeable. %! args = {"InitialTransformWeights", W0, "Standardize", false, ... %! "IterationLimit", 0}; %! f1 = rica (X, 3, args{:}, "ContrastFcn", "logcosh").FitInfo.Objective(1); %! f2 = rica (X, 3, args{:}, "ContrastFcn", "exp").FitInfo.Objective(1); %! f3 = rica (X, 3, args{:}, "ContrastFcn", "sqrt").FitInfo.Objective(1); %! assert_equal (f1 != f2 && f2 != f3 && f1 != f3, true); %!test %! ## Standardize centers and scales the data before transforming. %! Mdl = rica (X, 2, "Standardize", true, "IterationLimit", 100, ... %! "InitialTransformWeights", W0(:,1:2)); %! assert_equal (size (Mdl.Mu), [5, 1]); %! assert_equal (size (Mdl.Sigma), [5, 1]); %!test %! ## FitInfo carries the whole trajectory, not just its final value: two %! ## columns of equal length, Iteration the 0-based index, Objective the %! ## objective at the starting weights first and the solution last %! Mdl = rica (X, 3, "InitialTransformWeights", W0, "Lambda", 1, ... %! "Standardize", false, "IterationLimit", 1000); %! it = Mdl.FitInfo.Iteration; %! ob = Mdl.FitInfo.Objective; %! assert_equal (columns (it), 1); %! assert_equal (size (ob), size (it)); %! assert_equal (it, (0:numel (ob) - 1)'); %! assert_equal (all (diff (ob) <= 1e-10), true); %! ## the first entry is the objective at the starting weights, which is what %! ## a fit allowed no iterations at all reports %! M0 = rica (X, 3, "InitialTransformWeights", W0, "Lambda", 1, ... %! "Standardize", false, "IterationLimit", 0); %! assert_equal (numel (M0.FitInfo.Objective), 1); %! assert_equal (ob(1), M0.FitInfo.Objective(1), 1e-12); %!test %! ## a capped run stops with one entry per iteration plus the starting point %! Mdl = rica (X, 3, "InitialTransformWeights", W0, "Lambda", 1, ... %! "Standardize", false, "IterationLimit", 5); %! assert_equal (numel (Mdl.FitInfo.Objective) <= 6, true); %! assert_equal (Mdl.FitInfo.Iteration(1), 0); ## Test input validation %!test %! ## 'lbfgs' reaches the optimum the default solver reaches %! args = {"InitialTransformWeights", W0, "Lambda", 1, ... %! "Standardize", false, "IterationLimit", 1000}; %! Mq = rica (X, 3, args{:}); %! Ml = rica (X, 3, args{:}, "Solver", "lbfgs"); %! assert_equal (Ml.FitInfo.Objective(end), Mq.FitInfo.Objective(end), 1e-6); %!test %! ## The trajectory starts at the initial weights whichever solver ran %! args = {"InitialTransformWeights", W0, "IterationLimit", 20}; %! Mq = rica (X, 3, args{:}); %! Ml = rica (X, 3, args{:}, "Solver", "lbfgs"); %! assert_equal (Ml.FitInfo.Iteration(1), 0); %! assert_equal (Ml.FitInfo.Objective(1), Mq.FitInfo.Objective(1), 1e-10); %!test %! ## The solver that ran is recorded %! Mdl = rica (X, 3, "InitialTransformWeights", W0, "Solver", "lbfgs"); %! assert_equal (Mdl.ModelParameters.Solver, "lbfgs"); %!error ... %! rica (X, 3, "Solver", "bogus") %!error ... %! rica (X, 3, "Solver", 5) %!error rica (ones (5, 3)) %!error rica ({1, 2}, 1) %!error rica ([1, Inf; 2, 3], 1) %!error rica (ones (5, 3), 0) %!error rica (ones (5, 3), 1.5) %!error ... %! rica (ones (4, 5), 2, "InitialTransformWeights", ones (3, 3)) %!test %! ## The NonGaussianityIndicator pair reaches the model through rica. %! Mdl = rica (X, 3, "InitialTransformWeights", W0, "IterationLimit", 100, ... %! "NonGaussianityIndicator", [1; -1; 1]); %! assert_equal (Mdl.NonGaussianityIndicator, [1; -1; 1]); %!error ... %! rica (ones (4, 5), 2, "ContrastFcn", "bogus", "InitialTransformWeights", ones (5, 2)) %!error ... %! rica (ones (4, 5), 2, "bogus", 1) statistics-release-2.1.2/inst/Dimensionality_Reduction/rotatefactors.m000066400000000000000000000471061527311104300263500ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{B} =} rotatefactors (@var{A}) ## @deftypefnx {statistics} {@var{B} =} rotatefactors (@var{A}, @var{Name}, @var{Value}, @dots{}) ## @deftypefnx {statistics} {[@var{B}, @var{T}] =} rotatefactors (@dots{}) ## ## Rotate a factor-loading matrix. ## ## @code{@var{B} = rotatefactors (@var{A})} rotates the @math{D * M} factor ## loadings matrix @var{A} (@math{D} observed variables, @math{M} factors) to ## the @qcode{'varimax'} criterion and returns the rotated loadings @var{B}, the ## same size as @var{A}. ## ## @code{[@var{B}, @var{T}] = rotatefactors (@dots{})} also returns the ## @math{M * M} rotation matrix @var{T}, so that ## @code{@var{B} = @var{A} * @var{T}}. For the orthogonal methods @var{T} is ## orthonormal (@code{@var{T}' * @var{T}} is the identity); for the oblique ## methods (@qcode{'promax'} and oblique @qcode{'procrustes'}) it is a general ## invertible matrix. ## ## The rotation is controlled by @var{Name}/@var{Value} pairs: ## ## @table @asis ## @item @qcode{'Method'} ## The rotation criterion, one of: ## ## @table @asis ## @item @qcode{'varimax'} (default) ## Orthomax with a criterion coefficient of 1; maximizes the variance of the ## squared loadings within each factor. ## ## @item @qcode{'quartimax'} ## Orthomax with a coefficient of 0; simplifies the description of each ## variable. ## ## @item @qcode{'equamax'} ## Orthomax with a coefficient of @math{M / 2}. ## ## @item @qcode{'parsimax'} ## Orthomax with a coefficient of @math{D (M - 1) / (D + M - 2)}. ## ## @item @qcode{'orthomax'} ## General orthomax with the coefficient given by @qcode{'Coeff'}. ## ## @item @qcode{'promax'} ## Oblique rotation obtained by fitting an oblique transformation to a target ## built from a @qcode{'varimax'} solution raised to the power @qcode{'Power'}. ## ## @item @qcode{'procrustes'} ## Rotation towards the @qcode{'Target'} matrix, either orthogonal or oblique ## according to @qcode{'Type'}. ## @end table ## ## @item @qcode{'Normalize'} ## @qcode{'on'} (default) applies Kaiser normalization (each row of @var{A} is ## scaled to unit length before the orthomax rotation and unscaled afterwards); ## @qcode{'off'} disables it. Ignored by @qcode{'procrustes'}. ## ## @item @qcode{'Reltol'} ## Relative convergence tolerance for the iterative orthomax rotation. The ## default is @code{sqrt (eps)}. ## ## @item @qcode{'Maxit'} ## Maximum number of iterations for the iterative orthomax rotation. The ## default is 250. ## ## @item @qcode{'Coeff'} ## The orthomax coefficient used when @qcode{'Method'} is @qcode{'orthomax'}. ## The default is 1 (equivalent to @qcode{'varimax'}). ## ## @item @qcode{'Power'} ## The power used to build the @qcode{'promax'} target, a scalar greater than or ## equal to 1. The default is 4. ## ## @item @qcode{'Target'} ## The target loadings matrix for @qcode{'procrustes'}, the same size as ## @var{A}. Required for that method. ## ## @item @qcode{'Type'} ## @qcode{'oblique'} (default) or @qcode{'orthogonal'}, selecting the kind of ## @qcode{'procrustes'} rotation. The default follows MATLAB, whose ## @qcode{'procrustes'} rotation is oblique unless told otherwise. ## @end table ## ## @strong{Note on the orthomax family:} for coefficients up to 1 ## (@qcode{'varimax'}, @qcode{'quartimax'}, and small @qcode{'orthomax'}) the ## rotation follows the same successive-SVD iteration as MATLAB and stops at the ## same relative tolerance. For larger coefficients (@qcode{'equamax'}, ## @qcode{'parsimax'}) that iteration does not converge, so a monotonically ## convergent pairwise algorithm is used instead; it reaches the same optimum as ## MATLAB to that solution's own convergence precision. ## ## @seealso{factoran, pca, pcacov, procrustes} ## @end deftypefn function [B, T] = rotatefactors (A, varargin) ## Input validation if (nargin < 1) print_usage (); endif if (! (isnumeric (A) && ismatrix (A) && ndims (A) == 2)) error ("rotatefactors: A must be a numeric matrix."); endif if (! isreal (A)) error ("rotatefactors: A must be real."); endif [d, m] = size (A); ## Parse Name/Value options optNames = {'Method', 'Normalize', 'Reltol', 'Maxit', 'Coeff', ... 'Power', 'Target', 'Type'}; dfValues = {'varimax', 'on', sqrt(eps), 250, 1, 4, [], 'oblique'}; [Method, Normalize, Reltol, Maxit, Coeff, Power, Target, Type, rem] = ... parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (rem)) error ("rotatefactors: unknown or unpaired optional argument."); endif if (! (ischar (Method) && isrow (Method))) error ("rotatefactors: 'Method' must be a character vector."); endif Method = lower (Method); if (! any (strcmp (Method, {'varimax', 'quartimax', 'equamax', ... 'parsimax', 'orthomax', 'promax', 'procrustes'}))) error ("rotatefactors: unknown 'Method' '%s'.", Method); endif if (! (ischar (Normalize) && isrow (Normalize) ... && any (strcmpi (Normalize, {'on', 'off'})))) error ("rotatefactors: 'Normalize' must be 'on' or 'off'."); endif normalize = strcmpi (Normalize, 'on'); if (! (isnumeric (Reltol) && isscalar (Reltol) && isreal (Reltol) ... && Reltol > 0)) error ("rotatefactors: 'Reltol' must be a positive scalar."); endif if (! (isnumeric (Maxit) && isscalar (Maxit) && isreal (Maxit) ... && Maxit >= 1 && Maxit == fix (Maxit))) error ("rotatefactors: 'Maxit' must be a positive integer."); endif ## Dispatch on the requested method switch (Method) case 'varimax' [B, T] = orthomaxrotate (A, 1, normalize, Reltol, Maxit); case 'quartimax' [B, T] = orthomaxrotate (A, 0, normalize, Reltol, Maxit); case 'equamax' [B, T] = orthomaxrotate (A, m / 2, normalize, Reltol, Maxit); case 'parsimax' [B, T] = orthomaxrotate (A, d * (m - 1) / (d + m - 2), ... normalize, Reltol, Maxit); case 'orthomax' if (! (isnumeric (Coeff) && isscalar (Coeff) && isreal (Coeff))) error ("rotatefactors: 'Coeff' must be a real scalar."); endif [B, T] = orthomaxrotate (A, Coeff, normalize, Reltol, Maxit); case 'promax' if (! (isnumeric (Power) && isscalar (Power) && isreal (Power) ... && Power >= 1)) error ("rotatefactors: 'Power' must be a scalar >= 1."); endif [B, T] = promaxrotate (A, Power, normalize, Reltol, Maxit); case 'procrustes' if (isempty (Target)) error ("rotatefactors: 'Target' is required for 'procrustes'."); endif if (! (isnumeric (Target) && isreal (Target) ... && isequal (size (Target), [d, m]))) error (strcat ("rotatefactors: 'Target' must be a real matrix", ... " the same size as A.")); endif if (! (ischar (Type) && isrow (Type) ... && any (strcmpi (Type, {'orthogonal', 'oblique'})))) error ("rotatefactors: 'Type' must be 'orthogonal' or 'oblique'."); endif [B, T] = procrustesrotate (A, Target, lower (Type)); endswitch endfunction ## Orthomax criterion (to be maximized) of a loadings matrix L function v = orthomaxcrit (L, gamma, d) cs = sum (L .^ 2, 1); v = sum (sum (L .^ 4)) - (gamma / d) * sum (cs .^ 2); endfunction ## Orthomax rotation: successive-SVD iteration with a pairwise fallback for the ## coefficients where the SVD iteration fails to ascend the criterion. function [B, T] = orthomaxrotate (A, gamma, normalize, reltol, maxit) [d, m] = size (A); if (m < 2) B = A; T = eye (m); return; endif if (normalize) h = sqrt (sum (A .^ 2, 2)); An = A ./ h; else An = A; endif ## Successive-SVD (gradient-projection) iteration. Each step replaces T by ## the orthonormal polar factor of the criterion gradient. Whenever a step ## would fail to increase the criterion, the iteration is abandoned in favour ## of the always-monotone pairwise algorithm. T = eye (m); B = An; sPrev = 0; oscillated = false; for iter = 1:maxit D = diag (sum (B .^ 2, 1)); G = An' * (B .^ 3 - (gamma / d) * B * D); fCur = orthomaxcrit (B, gamma, d); [U, S, V] = svd (G); Tnew = U * V'; if (orthomaxcrit (An * Tnew, gamma, d) < fCur - 1e-12 * max (1, abs (fCur))) oscillated = true; break; endif T = Tnew; B = An * T; sCur = sum (diag (S)); if (iter > 1 && abs (sCur - sPrev) < reltol * sCur) break; endif sPrev = sCur; endfor if (oscillated) [B, T] = pairwiserotate (An, gamma, reltol, maxit); endif B = A * T; endfunction ## Pairwise (Jacobi) orthomax rotation: sweep over factor pairs, applying the ## planar rotation that maximizes the criterion, until every angle is negligible. function [B, T] = pairwiserotate (An, gamma, reltol, maxit) [n, m] = size (An); T = eye (m); B = An; for iter = 1:maxit converged = true; for p = 1:m-1 for q = p+1:m x = B(:,p); y = B(:,q); u = x .^ 2 - y .^ 2; v = 2 * x .* y; As = sum (u); Bs = sum (v); Cs = sum (u .^ 2 - v .^ 2); Ds = sum (2 * u .* v); num = Ds - (2 * gamma / n) * As * Bs; den = Cs - (gamma / n) * (As ^ 2 - Bs ^ 2); theta = atan2 (num, den) / 4; if (abs (theta) > reltol) converged = false; endif c = cos (theta); s = sin (theta); B(:,p) = c * x + s * y; B(:,q) = -s * x + c * y; R = eye (m); R(p,p) = c; R(q,q) = c; R(p,q) = -s; R(q,p) = s; T = T * R; endfor endfor if (converged) break; endif endfor endfunction ## Promax rotation: oblique fit to a power target built from a varimax solution. function [B, T] = promaxrotate (A, power, normalize, reltol, maxit) [d, m] = size (A); if (m < 2) B = A; T = eye (m); return; endif ## Start from a varimax (orthomax coefficient 1) rotation. [Bv, Tv] = orthomaxrotate (A, 1, normalize, reltol, maxit); ## Build the power target and fit an oblique transformation to it, normalized ## so the implied factor correlation matrix has a unit diagonal. BStar = sign (Bv) .* abs (Bv) .^ power; Q = Bv \ BStar; Q = Q .* sqrt (diag (inv (Q' * Q)))'; T = Tv * Q; B = A * T; endfunction ## Procrustes rotation towards a target, orthogonal or oblique. function [B, T] = procrustesrotate (A, Target, type) if (strcmp (type, 'orthogonal')) [U, S, V] = svd (A' * Target); T = U * V'; else ## Oblique: least-squares fit, columns normalized so the implied factor ## correlation matrix has a unit diagonal. T = A \ Target; T = T .* sqrt (diag (inv (T' * T)))'; endif B = A * T; endfunction %!demo %! ## Rotate a three-factor loading matrix to the varimax criterion and %! ## recover the rotation matrix. %! A = [ 0.8, 0.2, 0.1; 0.7, 0.3, 0.0; ... %! 0.1, 0.9, 0.2; 0.2, 0.8, 0.1; ... %! 0.1, 0.2, 0.9; 0.0, 0.1, 0.8]; %! [B, T] = rotatefactors (A, 'Method', 'varimax'); %! B %! ## T is orthonormal and reconstructs B from A. %! max (abs (vec (A * T - B))) ## Reference values below are from MATLAB R2023b for ## A = reshape (mod ((1:18)*5, 11), 6, 3) - 5. %!test %! A = reshape (mod ((1:18)*5, 11), 6, 3) - 5; %! B = rotatefactors (A, 'Method', 'varimax'); %! Bref = [ -0.590343326670958, -5.399542278328798, 2.120480592034490; ... %! 5.301191898681203, 1.349821193720584, -0.274494441363606; ... %! -1.789611374449118, -5.388780549360982, 0.870824505437868; ... %! 4.101923850903043, 1.360582922688400, -1.524150527960228; ... %! -2.988879422227277, -5.378018820393167, -0.378831581158754; ... %! 2.902655803124883, 1.371344651656216, -2.773806614556851]; %! assert_equal (B, Bref, 1e-10); %!test %! A = reshape (mod ((1:18)*5, 11), 6, 3) - 5; %! [B, T] = rotatefactors (A, 'Method', 'varimax'); %! assert_equal (A * T, B, 1e-12); %! assert_equal (T' * T, eye (3), 1e-12); %!test %! A = reshape (mod ((1:18)*5, 11), 6, 3) - 5; %! B = rotatefactors (A, 'Method', 'quartimax'); %! Bref = [ -1.079871632250411, 1.331921409298459, 5.573137591816050; ... %! 5.135643864717125, 1.382567861335108, -1.309071504386449; ... %! -1.872731015323443, -0.201056474219368, 5.427011593724494; ... %! 4.342784481644092, -0.150410022182719, -1.455197502478006; ... %! -2.665590398396474, -1.734034357737195, 5.280885595632937; ... %! 3.549925098571060, -1.683387905700547, -1.601323500569563]; %! assert_equal (B, Bref, 1e-10); %!test %! A = reshape (mod ((1:18)*5, 11), 6, 3) - 5; %! B = rotatefactors (A, 'Method', 'equamax'); %! Bref = [ -0.163528146620418, -5.392883888956456, 2.211348436021980; ... %! 4.856598298899028, 0.925912265845965, -2.357146461102300; ... %! -1.752970474391914, -5.249881056555078, 1.538129841051886; ... %! 3.267155971127525, 1.068915098247334, -3.030365056072396; ... %! -3.342412802163413, -5.106878224153711, 0.864911246081789; ... %! 1.677713643356028, 1.211917930648702, -3.703583651042488]; %! assert_equal (B, Bref, 1e-6); %!test %! A = reshape (mod ((1:18)*5, 11), 6, 3) - 5; %! B = rotatefactors (A, 'Method', 'parsimax'); %! Bref = [ -0.209779706669808, -5.381270194314001, 2.235603625524279; ... %! 4.824100779801576, 0.835974617312815, -2.455442547795920; ... %! -1.809538067356745, -5.214183987228857, 1.593065387896242; ... %! 3.224342419114638, 1.003060824397959, -3.097980785423958; ... %! -3.409296428043682, -5.047097780143712, 0.950527150268203; ... %! 1.624584058427703, 1.170147031483100, -3.740519023051994]; %! assert_equal (B, Bref, 1e-6); %!test %! ## orthomax with Coeff 1 equals varimax. %! A = reshape (mod ((1:18)*5, 11), 6, 3) - 5; %! B1 = rotatefactors (A, 'Method', 'orthomax', 'Coeff', 1); %! B2 = rotatefactors (A, 'Method', 'varimax'); %! assert_equal (B1, B2, 1e-12); %!test %! ## orthomax with Coeff 0 equals quartimax. %! A = reshape (mod ((1:18)*5, 11), 6, 3) - 5; %! B1 = rotatefactors (A, 'Method', 'orthomax', 'Coeff', 0); %! B2 = rotatefactors (A, 'Method', 'quartimax'); %! assert_equal (B1, B2, 1e-12); %!test %! ## Kaiser normalization 'off'. %! A = reshape (mod ((1:18)*5, 11), 6, 3) - 5; %! B = rotatefactors (A, 'Method', 'orthomax', 'Coeff', 0.5, 'Normalize', 'off'); %! Bref = [ -0.018655295494772, -5.634517554136197, 1.500621175407379; ... %! 5.076579678301866, 2.027215443018021, 0.344581365488290; ... %! -1.109307774177549, -5.634304393877173, 0.155081460160809; ... %! 3.985927199619089, 2.027428603277046, -1.000958349758279; ... %! -2.199960252860326, -5.634091233618149, -1.190458255085760; ... %! 2.895274720936312, 2.027641763536070, -2.346498065004848]; %! assert_equal (B, Bref, 1e-10); %!test %! A = reshape (mod ((1:18)*5, 11), 6, 3) - 5; %! [B, T] = rotatefactors (A, 'Method', 'promax', 'Power', 3); %! Bref = [ 1.224548928356429, -5.243438188632538, 2.071368824506429; ... %! 5.402710851790721, 0.067482009256478, -0.106948752887073; ... %! -0.495720593244403, -5.226782837470801, 0.630342425004671; ... %! 3.682441330189890, 0.084137360418215, -1.547975152388831; ... %! -2.215990114845235, -5.210127486309064, -0.810683974497087; ... %! 1.962171808589059, 0.100792711579952, -2.989001551890589]; %! assert_equal (B, Bref, 1e-10); %! assert_equal (A * T, B, 1e-10); %!test %! A = reshape (mod ((1:18)*5, 11), 6, 3) - 5; %! Target = reshape (mod ((1:18)*3, 7), 6, 3) - 3; %! [B, T] = rotatefactors (A, 'Method', 'procrustes', 'Target', Target, ... %! 'Type', 'orthogonal'); %! Bref = [ -1.222902114166934, 5.164166701208014, -2.415759239100699; ... %! 5.403127504320427, -0.893247624515622, -0.091224192807273; ... %! -2.313297220899559, 5.148157805250635, -1.070106153619982; ... %! 4.312732397587801, -0.909256520473001, 1.254428892673443; ... %! -3.403692327632185, 5.132148909293257, 0.275546931860735; ... %! 3.222337290855177, -0.925265416430380, 2.600081978154161]; %! assert_equal (B, Bref, 1e-10); %! assert_equal (T' * T, eye (3), 1e-12); %!test %! A = reshape (mod ((1:18)*5, 11), 6, 3) - 5; %! Target = reshape (mod ((1:18)*3, 7), 6, 3) - 3; %! ## 'oblique' is the default, so naming it must change nothing %! B = rotatefactors (A, 'Method', 'procrustes', 'Target', Target, ... %! 'Type', 'oblique'); %! assert_equal (B, rotatefactors (A, 'Method', 'procrustes', ... %! 'Target', Target), 1e-10); %! Bref = [ 0.000000000000002, -4.619308411881804, -10.370742303151150; ... %! 94.640566010004221, -97.005476649517561, -1.152304700350133; ... %! -31.546855336668067, 36.954467295054300, 0.000000000000000; ... %! 63.093710673336162, -55.431700942581458, 9.218437602801021; ... %! -63.093710673336133, 78.528243001990404, 10.370742303151154; ... %! 31.546855336668081, -13.857925235645340, 19.589179905952172]; %! assert_equal (B, Bref, 1e-8); %!test %! ## Single-factor input is returned unchanged. %! A = [1; 2; 3; 4]; %! [B, T] = rotatefactors (A); %! assert_equal (B, A); %! assert_equal (T, 1); %!test %! ## Default method is varimax. %! A = reshape (mod ((1:18)*5, 11), 6, 3) - 5; %! assert_equal (rotatefactors (A), rotatefactors (A, 'Method', 'varimax')); ## Test input validation %!error rotatefactors () %!error rotatefactors ({1, 2}) %!error rotatefactors (ones (2, 2, 2)) %!error rotatefactors ([1+2i; 3]) %!error rotatefactors (ones (3), 'Method', 'foo') %!error ... %! rotatefactors (ones (3), 'Method', 5) %!error ... %! rotatefactors (ones (3), 'Normalize', 'yes') %!error ... %! rotatefactors (ones (3), 'Reltol', -1) %!error ... %! rotatefactors (ones (3), 'Maxit', 2.5) %!error ... %! rotatefactors (ones (3), 'Method', 'orthomax', 'Coeff', [1, 2]) %!error ... %! rotatefactors (ones (3), 'Method', 'promax', 'Power', 0.5) %!error ... %! rotatefactors (ones (3), 'Method', 'procrustes') %!error ... %! rotatefactors (ones (6, 3), 'Method', 'procrustes', 'Target', ones (4, 2)) %!error ... %! rotatefactors (ones (6, 3), 'Method', 'procrustes', 'Target', ones (6, 3), 'Type', 'x') %!error ... %! rotatefactors (ones (3), 'Bogus', 1) statistics-release-2.1.2/inst/Dimensionality_Reduction/sequentialfs.m000066400000000000000000001177461527311104300262030ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{tf} =} sequentialfs (@var{fun}, @var{X}, @var{y}) ## @deftypefnx {statistics} {@var{tf} =} sequentialfs (@var{fun}, @var{X1}, @dots{}, @var{XN}) ## @deftypefnx {statistics} {@var{tf} =} sequentialfs (@dots{}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{tf}, @var{history}] =} sequentialfs (@dots{}) ## ## Sequential feature selection. ## ## @code{@var{tf} = sequentialfs (@var{fun}, @var{X}, @var{y})} selects the ## columns of @var{X} that best predict @var{y}, adding one column at a time, ## each time the one that lowers the cross-validated criterion computed by ## @var{fun} the most, until no column lowers it enough. @var{tf} is a ## logical row vector, true for each selected column of @var{X}. By default ## the criterion is estimated by 10-fold cross-validation, on a random ## partition of the rows that is not stratified. ## ## @var{fun} is a function handle called as ## @code{@var{criterion} = @var{fun} (@var{XTRAIN}, @var{ytrain}, @var{XTEST}, ## @var{ytest})}, where @var{XTRAIN} and @var{ytrain} are the training rows of ## @var{X}, restricted to the candidate columns, and of @var{y}, and ## @var{XTEST} and @var{ytest} the test rows. It returns a real scalar, which ## is typically a loss summed over the test rows: the values @var{fun} ## returns for every test set are summed and divided by the total number of ## test rows, so a loss averaged over the test rows must be multiplied by ## their number. Lower values are better. ## ## @code{@var{tf} = sequentialfs (@var{fun}, @var{X1}, @dots{}, @var{XN})} ## partitions the rows of every data argument alike, all of which must have ## the same number of rows, at least two, and selects columns of @var{X1} ## only. @var{fun} is then called with the training parts of all of them ## followed by the test parts, @code{@var{fun} (@var{X1TRAIN}, @dots{}, ## @var{XNTRAIN}, @var{X1TEST}, @dots{}, @var{XNTEST})}. The data arguments ## are the leading arguments up to the first option name. ## ## The following @qcode{Name-Value} pairs are supported: ## ## @multitable @columnfractions 0.2 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'CV'} @tab How the criterion is validated: a positive ## integer @math{k}, greater than 1 and not greater than the number of rows, ## for @math{k}-fold cross-validation on a random partition that is not ## stratified; a @code{cvpartition} object; @qcode{'resubstitution'}, calling ## @code{@var{fun} (@var{X}, @var{y}, @var{X}, @var{y})} on all rows and ## dividing by their number; or @qcode{'none'}, calling @code{@var{fun} ## (@var{X}, @var{y})} on all rows and taking its value as it is. The ## default is 10. ## ## @item @qcode{'MCReps'} @tab The number of Monte Carlo repetitions of the ## cross-validation, a positive integer, each on a new random partition, the ## criterion being summed over all of them before it is divided by the total ## number of test rows. The default is 1. It is set to 1, with a warning, ## when @qcode{'CV'} is @qcode{'none'} or @qcode{'resubstitution'}, or a ## @code{cvpartition} object for leave-one-out, resubstitution or a custom ## partition, none of which can be drawn anew. ## ## @item @qcode{'Direction'} @tab @qcode{'forward'}, the default, starts from ## no column and adds one at each step; @qcode{'backward'} starts from all ## columns and removes one at each step. ## ## @item @qcode{'KeepIn'} @tab Columns always selected, given as a logical ## vector with one element per column of @var{X} or as column indices. The ## default is none. ## ## @item @qcode{'KeepOut'} @tab Columns never selected, given as ## @qcode{'KeepIn'}. A column cannot be in both. ## ## @item @qcode{'NFeatures'} @tab The number of columns to select, a positive ## integer. The search then goes on until that many columns are selected, ## whatever the criterion and @qcode{MaxIter}. The default selects by the ## criterion alone. ## ## @item @qcode{'NullModel'} @tab @qcode{true} to let the selection hold no ## column, in which case @var{fun} must accept data arguments with no column; ## the default is @qcode{false}. A forward search then starts by evaluating ## the empty selection, and a backward search may remove the last column. ## ## @item @qcode{'Options'} @tab A structure, such as @code{statset ## ('sequentialfs')} returns, with the fields @qcode{Display}, @qcode{'off'} ## (the default), @qcode{'final'} or @qcode{'iter'}; @qcode{MaxIter}, the ## largest number of steps, @code{Inf} by default; @qcode{TolFun}, the ## tolerance on the criterion, 1e-6 for a forward search and 0 for a ## backward search by default; and @qcode{TolTypeFun}, @qcode{'rel'} (the ## default) or @qcode{'abs'}. Missing or empty fields take their defaults. ## @end multitable ## ## At each step every remaining column is tried and the one giving the ## lowest criterion is taken, the first of them in the order of the columns ## on a tie; a criterion of @qcode{NaN} is never taken over a number. The ## search stops when no column is left, after @qcode{MaxIter} steps, or when ## the new criterion @math{c} is greater than @math{c_0 - TolFun (|c_0| + ## sqrt (eps))} for a relative tolerance, or than @math{c_0 - TolFun} for an ## absolute one, @math{c_0} being the criterion before the step, so a step ## that leaves the criterion unchanged is taken when @qcode{TolFun} is 0. A ## criterion of @qcode{NaN} on either side never stops the search. The ## first column of a forward search is always added, unless ## @qcode{'NullModel'} or @qcode{'KeepIn'} gives a starting selection to ## compare it with. ## ## @code{[@var{tf}, @var{history}] = sequentialfs (@dots{})} also returns a ## structure with the fields @qcode{In}, a logical matrix whose row @math{i} ## marks the columns selected after step @math{i}, and @qcode{Crit}, a row ## vector of the criterion after each step. The starting selection is the ## first step when it is evaluated: always for a backward search, and for a ## forward search when @qcode{'KeepIn'} or @qcode{'NullModel'} is given. Both ## fields are empty when nothing is evaluated. ## ## The option names are not case sensitive but must be given in full, where ## MATLAB accepts any shortened name. @code{sequentialfs} refuses an ## @qcode{'NFeatures'} of 0, a @qcode{'CV'} larger than the number of rows, ## a @qcode{'NullModel'} other than true or false, and a criterion that is ## not a real number, all of which MATLAB accepts. Parallel computation is ## not supported, and the @qcode{UseParallel}, @qcode{UseSubstreams} and ## @qcode{Streams} options are ignored. ## ## @seealso{relieff, cvpartition, crossval, statset, stepwiselm} ## @end deftypefn function [tf, history] = sequentialfs (fun, varargin) if (nargin < 2) print_usage (); endif if (! is_function_handle (fun)) error ("sequentialfs: FUN must be a function handle."); endif ## Data arguments, up to the first option name nd = numel (varargin); for i = 1:numel (varargin) if ((ischar (varargin{i}) && rows (varargin{i}) <= 1) || (isstring (varargin{i}) && isscalar (varargin{i}))) nd = i - 1; break; endif endfor if (nd == 0) print_usage (); endif data = varargin(1:nd); n = size (data{1}, 1); if (any (cellfun (@(d) size (d, 1), data) != n)) error (strcat ("sequentialfs: the data arguments must have the same", ... " number of rows.")); endif if (n < 2) error ("sequentialfs: the data arguments must have at least two rows."); endif p = size (data{1}, 2); ## Parse optional Name-Value paired arguments optNames = {'CV', 'MCReps', 'Direction', 'KeepIn', 'KeepOut', ... 'NFeatures', 'NullModel', 'Options'}; dfValues = {10, 1, 'forward', [], [], [], false, []}; [cv, mcreps, direction, keepin, keepout, nfeat, nullmodel, options, ... args] = parsePairedArguments (optNames, dfValues, varargin(nd+1:end)); if (! isempty (args)) if (mod (numel (args), 2) == 0 && ischar (args{1})) error ("sequentialfs: unknown parameter name '%s'.", args{1}); endif error ("sequentialfs: optional arguments must be Name-Value pairs."); endif if (! ((ischar (direction) && isrow (direction)) || (isstring (direction) && isscalar (direction)))) error ("sequentialfs: 'Direction' must be a character vector."); endif direction = lower (char (direction)); if (! any (strcmp (direction, {'forward', 'backward'}))) error ("sequentialfs: unknown direction '%s'.", direction); endif forward = strcmp (direction, 'forward'); if (! (isnumeric (mcreps) && isreal (mcreps) && isscalar (mcreps) && isfinite (mcreps) && mcreps >= 1 && mcreps == fix (mcreps))) error ("sequentialfs: 'MCReps' must be a positive integer."); endif if (isstring (cv) && isscalar (cv)) cv = char (cv); endif if (isa (cv, 'cvpartition')) if (cv.NumObservations != n) error (strcat ("sequentialfs: the cvpartition object must have as", ... " many observations as the data arguments have rows.")); endif if (mcreps > 1 && (cv.IsCustom || any (strcmpi (cv.Type, {'leaveout', ... 'resubstitution'})))) warning (strcat ("sequentialfs: 'MCReps' is set to 1 for a", ... " leave-one-out, resubstitution or custom", ... " partition.")); mcreps = 1; endif validation = 'partition'; elseif (ischar (cv) && any (strcmpi (cv, {'resubstitution', 'none'}))) validation = lower (cv); if (mcreps > 1) warning (strcat ("sequentialfs: 'MCReps' is set to 1 when 'CV' is", ... " 'none' or 'resubstitution'.")); mcreps = 1; endif elseif (isnumeric (cv) && isreal (cv) && isscalar (cv) && isfinite (cv) && cv > 1 && cv == fix (cv)) if (cv > n) error (strcat ("sequentialfs: 'CV' must not be greater than the", ... " number of rows.")); endif validation = 'partition'; cv = cvpartition (n, 'KFold', cv); else error (strcat ("sequentialfs: 'CV' must be a cvpartition object, an", ... " integer greater than 1, 'resubstitution' or 'none'.")); endif [keepin, errmsg] = column_mask (keepin, p, 'KeepIn'); if (! isempty (errmsg)) error (errmsg); endif [keepout, errmsg] = column_mask (keepout, p, 'KeepOut'); if (! isempty (errmsg)) error (errmsg); endif if (any (keepin & keepout)) error ("sequentialfs: a column cannot be in both 'KeepIn' and 'KeepOut'."); endif if (! isempty (nfeat)) if (! (isnumeric (nfeat) && isreal (nfeat) && isscalar (nfeat) && nfeat >= 1 && nfeat <= p && nfeat == fix (nfeat))) error (strcat ("sequentialfs: 'NFeatures' must be a positive integer", ... " not greater than the number of columns of X.")); endif if (nfeat < sum (keepin)) error (strcat ("sequentialfs: 'NFeatures' must not be smaller than", ... " the number of columns in 'KeepIn'.")); endif if (nfeat > p - sum (keepout)) error (strcat ("sequentialfs: 'NFeatures' must not be greater than", ... " the number of columns not in 'KeepOut'.")); endif endif if (! ((islogical (nullmodel) || isnumeric (nullmodel)) && isscalar (nullmodel) && (nullmodel == 0 || nullmodel == 1))) error ("sequentialfs: 'NullModel' must be true or false."); endif nullmodel = logical (nullmodel); ## Search options, missing or empty fields taking their defaults display = 'off'; maxiter = Inf; tolfun = 0; if (forward) tolfun = 1e-6; endif reltol = true; if (! isempty (options)) if (! (isstruct (options) && isscalar (options))) error ("sequentialfs: 'Options' must be a structure."); endif if (isfield (options, 'Display') && ! isempty (options.Display)) display = options.Display; if (isstring (display) && isscalar (display)) display = char (display); endif if (! (ischar (display) && any (strcmpi (display, {'off', 'final', 'iter'})))) error ("sequentialfs: unknown 'Display' value."); endif display = lower (display); endif if (isfield (options, 'MaxIter') && ! isempty (options.MaxIter)) maxiter = options.MaxIter; if (! (isnumeric (maxiter) && isreal (maxiter) && isscalar (maxiter) && maxiter >= 0 && maxiter == fix (maxiter))) error (strcat ("sequentialfs: 'MaxIter' must be a non-negative", ... " integer or Inf.")); endif endif if (isfield (options, 'TolFun') && ! isempty (options.TolFun)) tolfun = options.TolFun; if (! (isnumeric (tolfun) && isreal (tolfun) && isscalar (tolfun) && tolfun >= 0 && isfinite (tolfun))) error ("sequentialfs: 'TolFun' must be a non-negative scalar."); endif endif if (isfield (options, 'TolTypeFun') && ! isempty (options.TolTypeFun)) toltype = options.TolTypeFun; if (isstring (toltype) && isscalar (toltype)) toltype = char (toltype); endif if (! (ischar (toltype) && any (strcmpi (toltype, {'abs', 'rel'})))) error ("sequentialfs: 'TolTypeFun' must be 'abs' or 'rel'."); endif reltol = strcmpi (toltype, 'rel'); endif endif ## Training and test rows of every repetition of the partition trainrows = testrows = {}; if (strcmp (validation, 'partition')) for r = 1:mcreps if (r > 1) cv = repartition (cv); endif for i = 1:cv.NumTestSets trainrows{end+1} = training (cv, i); testrows{end+1} = test (cv, i); endfor endfor endif ## Starting selection if (forward) in = keepin; else in = ! keepout; endif H = false (0, p); C = zeros (1, 0); verbose = ! strcmp (display, 'off'); if (verbose) if (forward) printf ("Start forward sequential feature selection:\n"); printf ("Initial columns included: %s\n", column_list (in)); printf ("Columns that can not be included: %s\n", ... column_list (keepout)); else printf ("Start backward sequential feature selection:\n"); printf ("Initial columns included: %s\n", column_list (in)); printf ("Columns that must be included: %s\n", column_list (keepin)); endif endif crit = NaN; if ((forward && (any (in) || nullmodel)) || (! forward && any (in))) crit = criterion (fun, data, in, validation, trainrows, testrows, n); H(end+1,:) = in; C(end+1) = crit; if (verbose) printf ("Step %d, used initial columns, criterion value %g\n", ... numel (C), crit); endif endif ## Add or remove one column at each step steps = 0; while (! isempty (nfeat) || steps < maxiter) if (forward) cand = find (! in & ! keepout); if (! isempty (nfeat) && sum (in) >= nfeat) break; endif else cand = find (in & ! keepin); if ((! nullmodel && sum (in) <= 1) || (! isempty (nfeat) && sum (in) <= nfeat)) break; endif endif if (isempty (cand)) break; endif c = zeros (1, numel (cand)); for j = 1:numel (cand) trial = in; trial(cand(j)) = forward; c(j) = criterion (fun, data, trial, validation, trainrows, testrows, n); endfor [newcrit, best] = min (c); if (isempty (nfeat)) if (reltol) limit = crit - tolfun * (abs (crit) + sqrt (eps)); else limit = crit - tolfun; endif if (newcrit > limit) break; endif endif in(cand(best)) = forward; crit = newcrit; steps += 1; H(end+1,:) = in; C(end+1) = crit; if (strcmp (display, 'iter')) if (forward) printf ("Step %d, added column %d, criterion value %g\n", ... numel (C), cand(best), crit); else printf ("Step %d, removed column %d, criterion value %g\n", ... numel (C), cand(best), crit); endif endif endwhile if (verbose) printf ("Final columns included: %s\n", column_list (in)); endif tf = in; if (isempty (C)) history = struct ('In', false (0, 0), 'Crit', []); else history = struct ('In', H, 'Crit', C); endif endfunction ## A column selection as a logical row, from a logical vector or indices function [mask, errmsg] = column_mask (v, p, name) mask = false (1, p); errmsg = ''; if (isempty (v)) return; endif if (islogical (v) && isvector (v) && numel (v) == p) mask = v(:)'; elseif (isnumeric (v) && isreal (v) && isvector (v) && all (v == fix (v)) && all (v >= 1) && all (v <= p)) mask(v) = true; else errmsg = sprintf (strcat ("sequentialfs: '%s' must be a logical", ... " vector with one element per column of X,", ... " or a vector of column indices."), name); endif endfunction ## The selected columns as text, as the display shows them function txt = column_list (mask) if (! any (mask)) txt = 'none'; elseif (all (mask)) txt = 'all'; else txt = sprintf ('%d ', find (mask)); endif endfunction ## The criterion of the columns in MASK, validated as asked function crit = criterion (fun, data, mask, validation, trainrows, testrows, n) sel = data; sel{1} = data{1}(:, mask); switch (validation) case 'none' crit = checked (fun (sel{:})); case 'resubstitution' crit = checked (fun (sel{:}, sel{:})) / n; otherwise total = 0; ntest = 0; for i = 1:numel (trainrows) tr = cellfun (@(d) d(trainrows{i},:), sel, 'UniformOutput', false); te = cellfun (@(d) d(testrows{i},:), sel, 'UniformOutput', false); total += checked (fun (tr{:}, te{:})); ntest += sum (testrows{i}); endfor crit = total / ntest; endswitch endfunction ## The value FUN returned, which must be a real scalar function v = checked (v) if (! ((isnumeric (v) || islogical (v)) && isreal (v) && isscalar (v))) error ("sequentialfs: FUN must return a real scalar."); endif v = double (v); endfunction %!demo %! ## Select the ingredients of cement that predict the heat it gives off, %! ## judging each selection by its sum of squared errors under %! ## leave-one-out cross-validation %! load hald %! sse = @(XT, yT, Xt, yt) sum ((yt - [ones(rows (Xt), 1), Xt] * ... %! ([ones(rows (XT), 1), XT] \ yT)) .^ 2); %! cv = cvpartition (13, 'LeaveOut'); %! opts = statset ('Display', 'iter'); %! tf = sequentialfs (sse, ingredients, heat, 'CV', cv, 'Options', opts) %!demo %! ## Remove ingredients one at a time from the full model instead %! load hald %! sse = @(XT, yT, Xt, yt) sum ((yt - [ones(rows (Xt), 1), Xt] * ... %! ([ones(rows (XT), 1), XT] \ yT)) .^ 2); %! cv = cvpartition (13, 'LeaveOut'); %! [tf, history] = sequentialfs (sse, ingredients, heat, 'CV', cv, ... %! 'Direction', 'backward') ## Test output ## Values checked against MATLAB R2024a. Column j of X holds the value j, ## so the criterion F looks up the selected columns in the table T. %!shared X, y, m %! X = repmat (1:3, 10, 1); %! y = (1:10)'; %! m = @(Z) 1 + sum (2 .^ (Z(1,:) - 1)); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! [tf, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none'); %! assert_equal (tf, true (1, 3)); %! assert_equal (h.In, logical ([1, 0, 0; 1, 1, 0; 1, 1, 1])); %! assert_equal (h.Crit, [10, 9, 8]); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! [tf, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none', ... %! 'Options', statset ('TolFun', 0.1)); %! assert_equal (tf, logical ([1, 0, 0])); %! assert_equal (h.Crit, 10); %!test %! T = [100, 10, 50, 8.99999999]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X(:,1:2), y, 'CV', 'none', ... %! 'Options', statset ('TolFun', 0.1)); %! assert_equal (h.Crit, [10, 8.99999999]); %!test %! T = [100, 10, 50, 8.999999999]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X(:,1:2), y, 'CV', 'none', ... %! 'Options', statset ('TolFun', 0.1)); %! assert_equal (h.Crit, 10); %!test %! T = [100, 0, 50, -0.05]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X(:,1:2), y, 'CV', 'none', ... %! 'Options', statset ('TolFun', 0.1)); %! assert_equal (h.Crit, [0, -0.05]); %!test %! T = [100, 10, 20, 9.05, 30, 9.5, 25, 9.05]; %! [tf, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none', ... %! 'Options', statset ('TolFun', 0)); %! assert_equal (tf, true (1, 3)); %! assert_equal (h.Crit, [10, 9.05, 9.05]); %!test %! T = [100, 10, 20, 9.05, 30, 9.5, 25, 8]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none', ... %! 'Options', statset ('TolFun', 0.95, ... %! 'TolTypeFun', 'abs')); %! assert_equal (h.Crit, [10, 9.05, 8]); %!test %! T = [100, 10, 20, 9.05, 30, 9.5, 25, 8]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none', ... %! 'Options', statset ('TolFun', 0.96, ... %! 'TolTypeFun', 'abs')); %! assert_equal (h.Crit, 10); %!test %! T = [100, 10, 10, 9, 20, 9, 25, 8]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none'); %! assert_equal (h.In, logical ([1, 0, 0; 1, 1, 0; 1, 1, 1])); %!test %! T = [100, NaN, 20, 9, 30, 9.5, 25, 8]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none'); %! assert_equal (h.In, logical ([0, 1, 0; 1, 1, 0; 1, 1, 1])); %! assert_equal (h.Crit, [20, 9, 8]); %!test %! T = [100, NaN, NaN, 9, NaN, 9.5, 25, 8]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none'); %! assert_equal (h.In, logical ([1, 0, 0; 1, 1, 0; 1, 1, 1])); %! assert_equal (h.Crit, [NaN, 9, 8]); %!test %! T = [100, 10, 50, NaN]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X(:,1:2), y, 'CV', 'none'); %! assert_equal (h.Crit, [10, NaN]); %!test %! T = [5, 10, 20, 9, 30, 9.5, 25, 8]; %! [tf, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none', ... %! 'NullModel', true); %! assert_equal (tf, false (1, 3)); %! assert_equal (h.In, false (1, 3)); %! assert_equal (h.Crit, 5); %!test %! T = [10.5, 10, 20, 9, 30, 9.5, 25, 8]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none', ... %! 'NullModel', true); %! assert_equal (h.In, logical ([0, 0, 0; 1, 0, 0; 1, 1, 0; 1, 1, 1])); %! assert_equal (h.Crit, [10.5, 10, 9, 8]); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none', 'KeepIn', 3); %! assert_equal (h.In, logical ([0, 0, 1; 1, 0, 1; 1, 1, 1])); %! assert_equal (h.Crit, [30, 9.5, 8]); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! [tf, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none', ... %! 'KeepOut', [false, true, false]); %! assert_equal (tf, logical ([1, 0, 1])); %! assert_equal (h.Crit, [10, 9.5]); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! [tf, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none', ... %! 'KeepOut', [1, 2, 3]); %! assert_equal (tf, false (1, 3)); %! assert_equal (size (h.In), [0, 0]); %! assert_equal (size (h.Crit), [0, 0]); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none', ... %! 'NFeatures', 3, 'Options', statset ('TolFun', 0.5)); %! assert_equal (h.Crit, [10, 9, 8]); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none', ... %! 'NFeatures', 2, 'Options', statset ('MaxIter', 1)); %! assert_equal (h.Crit, [10, 9]); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none', ... %! 'Options', statset ('MaxIter', 2)); %! assert_equal (h.Crit, [10, 9]); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none', ... %! 'NullModel', true, 'Options', statset ('MaxIter', 1)); %! assert_equal (h.In, logical ([0, 0, 0; 1, 0, 0])); %! assert_equal (h.Crit, [100, 10]); %!test %! T = [100, 9, 8, 10, 7, 12, 10, 10]; %! [tf, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none', ... %! 'Direction', 'backward'); %! assert_equal (tf, logical ([0, 0, 1])); %! assert_equal (h.In, logical ([1, 1, 1; 0, 1, 1; 0, 0, 1])); %! assert_equal (h.Crit, [10, 10, 7]); %!test %! T = [100, 50, 10.000000000000002, 10]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X(:,1:2), y, 'CV', 'none', ... %! 'Direction', 'backward'); %! assert_equal (h.Crit, 10); %!test %! T = [100, 50, 9, 10]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X(:,1:2), y, 'CV', 'none', ... %! 'Direction', 'backward', ... %! 'Options', statset ('TolFun', 0.1)); %! assert_equal (h.Crit, 10); %!test %! T = [100, 50, 8.99, 10]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X(:,1:2), y, 'CV', 'none', ... %! 'Direction', 'backward', ... %! 'Options', statset ('TolFun', 0.1)); %! assert_equal (h.Crit, [10, 8.99]); %!test %! T = [100, 50, 9, 10]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X(:,1:2), y, 'CV', 'none', ... %! 'Direction', 'backward', ... %! 'Options', statset ('TolFun', 1, ... %! 'TolTypeFun', 'abs')); %! assert_equal (h.Crit, [10, 9]); %!test %! T = [100, 50, NaN, 10]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X(:,1:2), y, 'CV', 'none', ... %! 'Direction', 'backward'); %! assert_equal (h.Crit, 10); %!test %! T = [6, 50, 7, 10]; %! [tf, h] = sequentialfs (@(Z, w) T(m (Z)), X(:,1:2), y, 'CV', 'none', ... %! 'Direction', 'backward', 'NullModel', true); %! assert_equal (tf, false (1, 2)); %! assert_equal (h.In, logical ([1, 1; 0, 1; 0, 0])); %! assert_equal (h.Crit, [10, 7, 6]); %!test %! T = [100, 9, 8, 10.5, 7, 12, 10, 10]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none', ... %! 'Direction', 'backward', 'KeepIn', 2); %! assert_equal (h.In, logical ([1, 1, 1; 0, 1, 1; 0, 1, 0])); %! assert_equal (h.Crit, [10, 10, 8]); %!test %! T = [100, 9, 8, 10.5, 7, 12, 10, 10]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none', ... %! 'Direction', 'backward', 'KeepOut', 3); %! assert_equal (h.In, logical ([1, 1, 0; 0, 1, 0])); %! assert_equal (h.Crit, [10.5, 8]); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! [~, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none', ... %! 'Direction', 'backward', 'KeepIn', 3, ... %! 'NFeatures', 1); %! assert_equal (h.Crit, [8, 9.5, 30]); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! [tf, h] = sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV', 'none', ... %! 'Direction', 'backward', ... %! 'Options', struct ('MaxIter', 0)); %! assert_equal (tf, true (1, 3)); %! assert_equal (h.Crit, 8); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! [~, h] = sequentialfs (@(A, b, C, d) T(m (A)), X, y, 'CV', 5); %! assert_equal (h.Crit, [5, 4.5, 4]); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! [~, h] = sequentialfs (@(A, b, C, d) T(m (A)), X, y, 'CV', 5, 'MCReps', 3); %! assert_equal (h.Crit, [5, 4.5, 4]); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! [~, h] = sequentialfs (@(A, b, C, d) T(m (A)), X, y); %! assert_equal (h.Crit, [10, 9, 8]); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! [~, h] = sequentialfs (@(A, b, C, d) T(m (A)), X, y, ... %! 'CV', 'resubstitution'); %! assert_equal (h.Crit, [1, 0.9, 0.8], eps); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! [~, h] = sequentialfs (@(A, b, C, d) T(m (A)), X, y, ... %! 'CV', cvpartition (10, 'Holdout', 3), 'MCReps', 4); %! assert_equal (h.Crit, [10, 9, 8] / 3, eps); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! q = @(A, B, c, D, E, z) T(m (A)) + columns (B) / 1000 + rows (E) / 1e6; %! [~, h] = sequentialfs (q, X, [y, y], y, 'CV', 'resubstitution'); %! assert_equal (h.Crit, [1.000201, 0.900201, 0.800201], 1e-15); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! [tf, h] = sequentialfs (@(A) T(m (A)), X, 'CV', 'none'); %! assert_equal (h.Crit, [10, 9, 8]); %!test %! T = [100, 9, 8, 10.5, 7, 12, 10, 10]; %! out = evalc (strcat ("sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV',", ... %! " 'none', 'Options', statset ('Display', 'iter'));")); %! assert_equal (out, ["Start forward sequential feature selection:\n", ... %! "Initial columns included: none\n", ... %! "Columns that can not be included: none\n", ... %! "Step 1, added column 3, criterion value 7\n", ... %! "Final columns included: 3 \n"]); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! out = evalc (strcat ("sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV',", ... %! " 'none', 'KeepIn', [1, 3], 'Options',", ... %! " statset ('Display', 'iter'));")); %! assert_equal (out, ["Start forward sequential feature selection:\n", ... %! "Initial columns included: 1 3 \n", ... %! "Columns that can not be included: none\n", ... %! "Step 1, used initial columns, ", ... %! "criterion value 9.5\n", ... %! "Step 2, added column 2, criterion value 8\n", ... %! "Final columns included: all\n"]); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! out = evalc (strcat ("sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV',", ... %! " 'none', 'KeepOut', [1, 3], 'Options',", ... %! " statset ('Display', 'iter'));")); %! assert_equal (out, ["Start forward sequential feature selection:\n", ... %! "Initial columns included: none\n", ... %! "Columns that can not be included: 1 3 \n", ... %! "Step 1, added column 2, criterion value 20\n", ... %! "Final columns included: 2 \n"]); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! out = evalc (strcat ("sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV',", ... %! " 'none', 'NullModel', true, 'Options',", ... %! " statset ('Display', 'final'));")); %! assert_equal (out, ["Start forward sequential feature selection:\n", ... %! "Initial columns included: none\n", ... %! "Columns that can not be included: none\n", ... %! "Step 1, used initial columns, ", ... %! "criterion value 100\n", ... %! "Final columns included: all\n"]); %!test %! T = [100, 9, 8, 10.5, 7, 12, 10, 10]; %! out = evalc (strcat ("sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV',", ... %! " 'none', 'Direction', 'backward', 'Options',", ... %! " statset ('Display', 'iter'));")); %! assert_equal (out, ["Start backward sequential feature selection:\n", ... %! "Initial columns included: all\n", ... %! "Columns that must be included: none\n", ... %! "Step 1, used initial columns, criterion value 10\n", ... %! "Step 2, removed column 1, criterion value 10\n", ... %! "Step 3, removed column 2, criterion value 7\n", ... %! "Final columns included: 3 \n"]); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! out = evalc (strcat ("sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV',", ... %! " 'none', 'Direction', 'backward', 'KeepOut', 2,", ... %! " 'Options', statset ('Display', 'final'));")); %! assert_equal (out, ["Start backward sequential feature selection:\n", ... %! "Initial columns included: 1 3 \n", ... %! "Columns that must be included: none\n", ... %! "Step 1, used initial columns, ", ... %! "criterion value 9.5\n", ... %! "Final columns included: 1 3 \n"]); %!test %! T = [100, 10, 20, 9, 30, 9.5, 25, 8]; %! out = evalc (strcat ("sequentialfs (@(Z, w) T(m (Z)), X, y, 'CV',", ... %! " 'none', 'Direction', 'backward', 'KeepOut',", ... %! " [1, 2, 3], 'Options',", ... %! " statset ('Display', 'iter'));")); %! assert_equal (out, ["Start backward sequential feature selection:\n", ... %! "Initial columns included: none\n", ... %! "Columns that must be included: none\n", ... %! "Final columns included: none\n"]); %!shared sse, cv %! sse = @(XT, yT, Xt, yt) sum ((yt - [ones(rows (Xt), 1), Xt] * ... %! ([ones(rows (XT), 1), XT] \ yT)) .^ 2); %! cv = cvpartition (13, 'LeaveOut'); %!test %! load hald %! [tf, h] = sequentialfs (sse, ingredients, heat, 'CV', cv); %! assert_equal (tf, logical ([1, 1, 0, 1])); %! assert_equal (h.In, logical ([0, 0, 0, 1; 1, 0, 0, 1; 1, 1, 0, 1])); %! assert_equal (h.Crit, [91.86293870821909, 9.3249533115070999, ... %! 6.5654708620637523], 1e-10); %!test %! load hald %! [tf, h] = sequentialfs (sse, ingredients, heat, 'CV', cv, ... %! 'Direction', 'backward'); %! assert_equal (tf, logical ([1, 1, 0, 1])); %! assert_equal (h.Crit, [8.4881966849412365, 6.5654708620637523], 1e-10); %!test %! load hald %! [~, h] = sequentialfs (sse, ingredients, heat, 'CV', cv, 'NullModel', true); %! assert_equal (h.Crit, [245.17305555555561, 91.86293870821909, ... %! 9.3249533115070999, 6.5654708620637523], 1e-10); %!test %! load hald %! [tf, h] = sequentialfs (sse, ingredients, heat, 'CV', 'resubstitution', ... %! 'Direction', 'backward', 'NullModel', true); %! assert_equal (tf, true (1, 4)); %! assert_equal (h.Crit, 3.6818184115768386, 1e-12); %!warning ... %! sequentialfs (@(A, b) 1, [1, 2; 3, 4], [1; 2], 'CV', 'none', 'MCReps', 3); %!warning ... %! sequentialfs (@(A, b, C, d) 1, [1, 2; 3, 4], [1; 2], ... %! 'CV', cvpartition (2, 'LeaveOut'), 'MCReps', 3); ## Test input validation %!shared X, y, f %! X = repmat (1:3, 10, 1); %! y = (1:10)'; %! f = @(Z, w) sum (Z(1,:)); %!error sequentialfs (f) %!error sequentialfs (f, 'CV', 'none') %!error sequentialfs (3, X, y) %!error ... %! sequentialfs (f, X, y(1:9), 'CV', 'none') %!error ... %! sequentialfs (f, X(1,:), y(1), 'CV', 'none') %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'Foo', 1) %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'dir', 'backward') %!error ... %! sequentialfs (f, X, y, 'CV') %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'Direction', 1) %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'Direction', 'foo') %!error ... %! sequentialfs (f, X, y, 'MCReps', 0) %!error ... %! sequentialfs (f, X, y, 'MCReps', 1.5) %!error ... %! sequentialfs (f, X, y, 'CV', cvpartition (9, 'LeaveOut')) %!error ... %! sequentialfs (f, X, y, 'CV', 11) %!error ... %! sequentialfs (f, X, y, 'CV', 'foo') %!error ... %! sequentialfs (f, X, y, 'CV', 1) %!error ... %! sequentialfs (f, X, y, 'CV', 2.5) %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'KeepIn', 4) %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'KeepIn', [true, false]) %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'KeepIn', 1.5) %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'KeepOut', 0) %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'KeepIn', 1, 'KeepOut', 1) %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'NFeatures', 4) %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'NFeatures', 0) %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'NFeatures', 1.5) %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'KeepIn', [1, 2], 'NFeatures', 1) %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'KeepOut', [1, 2], 'NFeatures', 2) %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'NullModel', 2) %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'NullModel', 'on') %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'Options', 1) %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'Options', struct ('Display', 'on')) %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'Options', struct ('MaxIter', -1)) %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'Options', struct ('TolFun', -1)) %!error ... %! sequentialfs (f, X, y, 'CV', 'none', 'Options', struct ('TolTypeFun', 'foo')) %!error ... %! sequentialfs (@(Z, w) [1, 2], X, y, 'CV', 'none') %!error ... %! sequentialfs (@(Z, w) 'a', X, y, 'CV', 'none') %!error ... %! sequentialfs (@(Z, w) 1i, X, y, 'CV', 'none') %!error ... %! sequentialfs (@(Z, w) [], X, y, 'CV', 'none') statistics-release-2.1.2/inst/Dimensionality_Reduction/sparsefilt.m000066400000000000000000000225331527311104300256410ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} sparsefilt (@var{X}, @var{Q}) ## @deftypefnx {statistics} {@var{Mdl} =} sparsefilt (@var{X}, @var{Q}, @var{Name}, @var{Value}) ## ## Sparse filtering for feature extraction. ## ## @code{@var{Mdl} = sparsefilt (@var{X}, @var{Q})} learns @var{Q} features from ## the @math{N * P} data matrix @var{X} (rows are observations, columns are ## predictors) and returns a @qcode{SparseFiltering} object @var{Mdl}. Apply ## the learned transformation to data with ## @code{transform (@var{Mdl}, @var{X})}. ## ## The @math{N * @var{Q}} features returned by @code{transform} are the ## soft-absolute activations @code{sqrt ((@var{X} * @var{W}) .^ 2 + 1e-8)}, ## normalized first across observations (each feature) and then across features ## (each observation). The @math{P * @var{Q}} weight matrix @var{W} minimizes ## the sum of those features plus an L2 penalty @code{@var{Lambda} * ||@var{W} ## ||_F^2}, driving the features to be sparse. ## ## Name/Value pairs: ## ## @table @asis ## @item @qcode{'IterationLimit'} ## Maximum number of iterations (default 1000). ## ## @item @qcode{'Lambda'} ## Weight of the L2 penalty on the transform weights (default 1). ## ## @item @qcode{'Standardize'} ## Logical; center and scale each predictor before fitting (default ## @qcode{false}). ## ## @item @qcode{'InitialTransformWeights'} ## A @math{P * @var{Q}} initial value for the weights. The default is random. ## ## @item @qcode{'GradientTolerance'}, @qcode{'StepTolerance'} ## Stop once the gradient's or the step's infinity norm falls to or below the ## given value (default @qcode{1e-6} each). They govern the fit only under ## @qcode{'Solver', 'lbfgs'}; the @qcode{'quasinewton'} solver runs to its own ## tighter internal tolerances and records these without acting on them. ## ## @item @qcode{'Solver'} ## @qcode{'quasinewton'} (default) minimizes through Octave's @code{fminunc}, ## which carries a full inverse Hessian. @qcode{'lbfgs'} selects the ## limited-memory BFGS solver MATLAB uses, holding as many curvature pairs as ## the transform has parameters, and is several times faster here. It stops ## where @qcode{'GradientTolerance'} and @qcode{'StepTolerance'} say to, so a ## value tighter than the default carries it further. ## @end table ## ## @subheading Note on reproducibility ## ## The sparse filtering objective is not convex and is minimized by a ## quasi-Newton solver, so the learned weights depend on the starting point and ## the solver. Different runs (or different software, including MATLAB) may ## return different weights that nonetheless describe an equally valid feature ## transformation. Fix @qcode{'InitialTransformWeights'} for a reproducible ## result. ## ## @seealso{SparseFiltering, rica, pca} ## @end deftypefn function Mdl = sparsefilt (X, Q, varargin) ## Input validation if (nargin < 2) print_usage (); endif if (! (isnumeric (X) && ismatrix (X) && ndims (X) == 2)) error ("sparsefilt: X must be a numeric matrix."); endif if (! (isreal (X) && all (isfinite (X(:))))) error ("sparsefilt: X must be real and finite."); endif [n, p] = size (X); if (! (isscalar (Q) && isnumeric (Q) && Q >= 1 && Q == fix (Q))) error ("sparsefilt: Q must be a positive integer."); endif if (mod (numel (varargin), 2) != 0) error ("sparsefilt: Name/Value arguments must come in pairs."); endif for k = 1:2:numel (varargin) if (ischar (varargin{k}) && strcmpi (varargin{k}, "InitialTransformWeights")) W0 = varargin{k+1}; if (! (isnumeric (W0) && isequal (size (W0), [p, Q]))) error ("sparsefilt: 'InitialTransformWeights' must be a %d-by-%d matrix.", ... p, Q); endif endif endfor Mdl = SparseFiltering (X, Q, varargin{:}); endfunction %!demo %! ## Learn two sparse features from data with the default (random) start. %! X = [1, 2, 3, 4; 2, 3, 4, 5; -1, 0, 1, 2; 3, 1, 4, 1; 0, 2, 1, 3]; %! Mdl = sparsefilt (X, 2, "IterationLimit", 200); %! Z = transform (Mdl, X) ## The sparse filtering objective is verified against MATLAB R2023b for ## X = reshape (mod ((1:60)*7, 13), 12, 5) - 6 with fixed initial weights. %!shared X, W0 %! X = reshape (mod ((1:60)*7, 13), 12, 5) - 6; %! W0 = reshape (mod ((1:15)*3, 7), 5, 3) - 3; %!test %! Mdl = sparsefilt (X, 3, "InitialTransformWeights", W0, "Lambda", 1, ... %! "Standardize", false, "IterationLimit", 1000); %! Z = transform (Mdl, X); %! ## features are in [0, 1] and each observation (row) is essentially unit length %! assert_equal (size (Z), [12, 3]); %! assert_equal (all (Z(:) >= 0) && all (Z(:) <= 1 + 1e-12), true); %! ## rows are unit length up to the 1e-8 normalization regularizer %! assert_equal (sqrt (sum (Z .^ 2, 2)), ones (12, 1), 1e-3); %!test %! ## The objective at the initial weights matches MATLAB (73.171833). %! A = X * W0; %! F = sqrt (A .^ 2 + 1e-8); %! F = F ./ sqrt (sum (F .^ 2, 1) + 1e-8); %! F = F ./ sqrt (sum (F .^ 2, 2) + 1e-8); %! f0 = sum (F(:)) + sum (W0(:) .^ 2); %! assert_equal (f0, 73.171833042485730, 1e-9); %!test %! ## The solver drives the objective well below its value at the start %! ## (73.17) -- reaching a good local minimum of the sparse filtering cost. %! Mdl = sparsefilt (X, 3, "InitialTransformWeights", W0, "Lambda", 1, ... %! "Standardize", false, "IterationLimit", 1000); %! assert_equal (Mdl.FitInfo.Objective(end) < 20, true); %!test %! ## Lambda is an L2 penalty on the weights: FitInfo.Objective is %! ## sum(features) + Lambda * ||W||^2 at the solution. %! Mdl = sparsefilt (X, 3, "InitialTransformWeights", W0, "Lambda", 1, ... %! "Standardize", false, "IterationLimit", 1000); %! W = Mdl.TransformWeights; %! Z = transform (Mdl, X); %! assert_equal (Mdl.FitInfo.Objective(end), sum (Z(:)) + sum (W(:) .^ 2), 1e-6); %!test %! ## FitInfo carries the whole trajectory, not just its final value %! Mdl = sparsefilt (X, 3, "InitialTransformWeights", W0, "Lambda", 1, ... %! "Standardize", false, "IterationLimit", 1000); %! it = Mdl.FitInfo.Iteration; %! ob = Mdl.FitInfo.Objective; %! assert_equal (columns (it), 1); %! assert_equal (size (ob), size (it)); %! assert_equal (it, (0:numel (ob) - 1)'); %! assert_equal (all (diff (ob) <= 1e-10), true); %!test %! ## the comparison broadcasts over the history, as MATLAB's does %! Mdl = sparsefilt (X, 3, "InitialTransformWeights", W0, "Lambda", 1, ... %! "Standardize", false, "IterationLimit", 1000); %! assert_equal (size (Mdl.FitInfo.Objective < 20), size (Mdl.FitInfo.Objective)); ## Test input validation %!test %! ## The lbfgs solver reaches a low objective value (MATLAB's is 12.2532). %! ## It does not land where the default solver lands: this objective is far %! ## from convex and the two take different steps from the same weights. %! ## Both tolerances are tightened because the step reaches 1e-6 here while %! ## the gradient is still well above it, so the default stops the fit %! ## early; that is StepTolerance doing exactly what it says. %! Mdl = sparsefilt (X, 3, "InitialTransformWeights", W0, "Lambda", 1, ... %! "Standardize", false, "IterationLimit", 1000, ... %! "Solver", "lbfgs", "GradientTolerance", 1e-8, ... %! "StepTolerance", 1e-8); %! assert_equal (Mdl.FitInfo.Objective(end) < 12.5, true); %!test %! ## The trajectory starts at the initial weights whichever solver ran %! args = {"InitialTransformWeights", W0, "IterationLimit", 20}; %! Mq = sparsefilt (X, 3, args{:}); %! Ml = sparsefilt (X, 3, args{:}, "Solver", "lbfgs"); %! assert_equal (Ml.FitInfo.Iteration(1), 0); %! assert_equal (Ml.FitInfo.Objective(1), Mq.FitInfo.Objective(1), 1e-10); %!test %! ## The solver that ran is recorded %! Mdl = sparsefilt (X, 3, "InitialTransformWeights", W0, "Solver", "lbfgs"); %! assert_equal (Mdl.ModelParameters.Solver, "lbfgs"); %!error ... %! sparsefilt (X, 3, "Solver", "bogus") %!error ... %! sparsefilt (X, 3, "Solver", 5) %!error sparsefilt (ones (5, 3)) %!error sparsefilt ({1, 2}, 1) %!error sparsefilt ([1, Inf; 2, 3], 1) %!error sparsefilt (ones (5, 3), 0) %!error sparsefilt (ones (5, 3), 1.5) %!error ... %! sparsefilt (ones (4, 5), 2, "InitialTransformWeights", ones (3, 3)) %!error ... %! sparsefilt (ones (4, 5), 2, "bogus", 1) statistics-release-2.1.2/inst/Dimensionality_Reduction/tsne.m000066400000000000000000000577351527311104300244520ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Y} =} tsne (@var{X}) ## @deftypefnx {statistics} {[@var{Y}, @var{loss}] =} tsne (@var{X}) ## @deftypefnx {statistics} {[@dots{}] =} tsne (@dots{}, @var{Name}, @var{Value}) ## ## t-distributed stochastic neighbor embedding (t-SNE). ## ## @code{@var{Y} = tsne (@var{X})} embeds the @math{N * P} data matrix @var{X} ## (rows are observations) into a low-dimensional space and returns the ## @math{N * @var{NumDimensions}} matrix @var{Y} of embedded points, whose ## pairwise (Student-t) affinities approximate the Gaussian affinities of the ## rows of @var{X}. ## ## @code{[@var{Y}, @var{loss}] = tsne (@dots{})} also returns the ## Kullback-Leibler divergence @var{loss} between the two affinity ## distributions at the returned embedding. ## ## Name/Value pairs: ## ## @table @asis ## @item @qcode{'Algorithm'} ## @qcode{'exact'} (default) forms the affinities and the gradient over every ## pair of points, which costs @math{O(N^2)} in time and memory at each ## iteration. @qcode{'barneshut'} approximates both: the high-dimensional ## affinities are kept only over each point's @math{3 * Perplexity} nearest ## neighbours, and the repulsive part of the gradient is summed over a ## space-partitioning tree of the embedding, giving @math{O(N log N)}. Use it ## when @math{N} is large enough that the exact algorithm is slow or cannot ## allocate; on this machine the two cost the same at about @math{N = 500} and ## @qcode{'barneshut'} is eight times faster at @math{N = 2000}. ## ## The two do not return the same embedding, and their @var{loss} values are ## not comparable either: the divergence is summed over the pairs that carry an ## affinity, and @qcode{'barneshut'} keeps far fewer of them. ## ## @item @qcode{'Theta'} ## The tree opening criterion for @qcode{'barneshut'}, a non-negative scalar ## (default 0.5). A cell of the tree is collapsed to its centre of mass when ## its width is smaller than @var{Theta} times its distance from the point ## being pushed, so a larger value is faster and coarser. @qcode{0} collapses ## nothing and makes the repulsion exact, at @math{O(N^2)}; note that this ## still leaves the affinities sparse, so it does not reproduce ## @qcode{'exact'}. Ignored by @qcode{'exact'}. ## ## @item @qcode{'Distance'} ## The distance metric used for the high-dimensional affinities, as accepted by ## @code{pdist} (default @qcode{'euclidean'}). ## ## @item @qcode{'NumDimensions'} ## The dimension of the embedding @var{Y} (default @code{min (P, 2)}). ## ## @item @qcode{'NumPCAComponents'} ## If positive, reduce @var{X} to this many principal components before ## embedding (default 0, no reduction). ## ## @item @qcode{'Standardize'} ## Logical; center and scale each column of @var{X} before embedding (default ## @qcode{false}). ## ## @item @qcode{'Perplexity'} ## The effective number of local neighbors (default 30). It must be smaller ## than @math{N}. ## ## @item @qcode{'Exaggeration'} ## Tightness factor applied to the high-dimensional affinities for the first 100 ## iterations (default 4, no less than 1). ## ## @item @qcode{'LearnRate'} ## The learning rate of the optimization (default 500). ## ## @item @qcode{'InitialY'} ## An @math{N * @var{NumDimensions}} initial embedding (default ## @code{1e-4 * randn}). ## ## @item @qcode{'Options'} ## A structure (as returned by @code{statset}) whose @qcode{MaxIter} (default ## 1000) and @qcode{TolFun} (default @code{1e-10}) fields control the ## optimization. A @qcode{MaxIter} of 0 returns the initial embedding with ## its loss, where MATLAB returns a loss of 0 or @code{-Inf}. ## @end table ## ## The embedding is not unique: it depends on the initial configuration and the ## random state. Set @qcode{'InitialY'} (or the random seed) for a reproducible ## result. ## ## @seealso{pca, pdist, statset} ## @end deftypefn function [Y, loss] = tsne (X, varargin) if (nargin < 1) print_usage (); endif if (! (isnumeric (X) && ismatrix (X) && ndims (X) == 2)) error ("tsne: X must be a numeric matrix."); endif if (! (isreal (X) && all (! isinf (X(:))))) error ("tsne: X must be real and finite."); endif [N, p] = size (X); ## Defaults and Name/Value parsing Algorithm = "exact"; Distance = "euclidean"; ydims = []; numPCA = 0; Standardize = false; Perplexity = 30; Exaggeration = 4; LearnRate = 500; InitialY = []; Theta = 0.5; opts = statset ("tsne"); if (mod (numel (varargin), 2) != 0) error ("tsne: Name/Value arguments must come in pairs."); endif for k = 1:2:numel (varargin) name = varargin{k}; val = varargin{k+1}; switch (lower (name)) case 'algorithm' Algorithm = lower (val); case 'distance' Distance = val; case 'numdimensions' ydims = val; case 'numpcacomponents' numPCA = val; case 'standardize' Standardize = val; case 'perplexity' Perplexity = val; case 'exaggeration' Exaggeration = val; case 'learnrate' LearnRate = val; case 'initialy' InitialY = val; case 'theta' Theta = val; case 'options' if (! isstruct (val)) error ("tsne: 'Options' must be a structure."); endif opts = statset (opts, val); otherwise error ("tsne: unknown parameter name '%s'.", name); endswitch endfor if (! any (strcmp (Algorithm, {'exact', 'barneshut'}))) error ("tsne: 'Algorithm' must be 'exact' or 'barneshut'."); endif if (! (isscalar (Theta) && isnumeric (Theta) && isreal (Theta) && isfinite (Theta) && Theta >= 0)) error ("tsne: 'Theta' must be a non-negative scalar."); endif if (! (isscalar (Perplexity) && Perplexity > 0 && Perplexity < N)) error ("tsne: 'Perplexity' must be a positive scalar smaller than N."); endif if (! (isscalar (Exaggeration) && Exaggeration >= 1)) error ("tsne: 'Exaggeration' must be a scalar not less than 1."); endif if (! (isscalar (LearnRate) && LearnRate > 0)) error ("tsne: 'LearnRate' must be a positive scalar."); endif ## An empty value falls back to the default below. Anything else has to be a ## dimension the embedding can actually have: a non-positive one used to ## return an N-by-0 embedding rather than complain, and a fractional or ## non-numeric one leaked an internal conversion error. if (! isempty (ydims)) if (! (isnumeric (ydims) && isscalar (ydims) && isreal (ydims) && isfinite (ydims) && ydims > 0 && fix (ydims) == ydims)) error ("tsne: 'NumDimensions' must be a positive integer."); elseif (ydims > p) error (strcat ("tsne: 'NumDimensions' must not be greater than the", ... " number of columns of X.")); endif endif ## Initial embedding if (isempty (InitialY)) if (isempty (ydims)) ydims = min (p, 2); endif Y = 1e-4 * randn (N, ydims); else if (! isequal (rows (InitialY), N)) error ("tsne: 'InitialY' must have N rows."); endif ydims = columns (InitialY); Y = InitialY; endif ## The Barnes-Hut tree is a 2^D-ary subdivision of the embedding, which is ## only built for the dimensions t-SNE is used in. if (strcmp (Algorithm, "barneshut") && ydims > 3) error (strcat ("tsne: 'NumDimensions' must not be greater than 3 for", ... " the 'barneshut' Algorithm.")); endif ## Standardize and optionally reduce with PCA if (Standardize) sig = std (X, 0, 1); sig(range (X, 1) == 0) = 1; X = (X - mean (X, 1)) ./ sig; endif if (numPCA > 0) [~, X] = pca (X, "Centered", false, "NumComponents", numPCA); endif ## High-dimensional affinities P. The exact algorithm forms them for every ## pair; Barnes-Hut keeps only each point's nearest neighbours, which is the ## half of the approximation that has nothing to do with the tree. if (strcmp (Algorithm, "exact")) D = squareform (pdist (X, Distance)) .^ 2; P = binary_search_variance (D, Perplexity, N); P(1:N+1:end) = 0; P = (P + P') / (2 * N); P = max (P, realmin); else P = neighbour_affinities (X, Distance, Perplexity, N); endif ## Optimize the embedding [Y, loss] = tsne_embedding (Y, P, Exaggeration, LearnRate, opts, N, ydims, ... Algorithm, Theta); endfunction ## --------------------------------------------------------------------------- ## Binary search for the per-point variance giving the target perplexity. function condP = binary_search_variance (D, perplexity, N) condP = zeros (N); beta = ones (N, 1); H = log (perplexity); for i = 1:N a = -Inf; c = Inf; for iter = 1:100 Pi = exp (-D(i,:) * beta(i)); Pi(i) = 0; si = max (sum (Pi), realmin); Pi = Pi / si; Hi = log (si) + beta(i) * sum (D(i,:) .* Pi); fval = Hi - H; if (abs (fval) < 1e-5) break; endif if (fval > 0) a = beta(i); if (isinf (c)) beta(i) = 2 * beta(i); else beta(i) = 0.5 * (beta(i) + c); endif else c = beta(i); if (isinf (a)) beta(i) = 0.5 * beta(i); else beta(i) = 0.5 * (a + beta(i)); endif endif endfor condP(i,:) = Pi; endfor endfunction ## --------------------------------------------------------------------------- ## Sparse high-dimensional affinities over each point's nearest neighbours. ## Barnes-Hut t-SNE keeps 3 * Perplexity of them, the count at which the ## conditional distribution has essentially no mass left, and runs the same ## per-point variance search over that row instead of over all N. function P = neighbour_affinities (X, Distance, perplexity, N) K = min (N - 1, max (1, round (3 * perplexity))); ## Neighbours a block of rows at a time. The exact algorithm forms the whole ## N-by-N distance matrix, which is the memory wall this path exists to get ## around, so the block is sized to a fixed budget rather than to N. A block ## against all of X is what pdist2 is for; knnsearch would serve too and is ## two orders of magnitude slower at this K. blk = max (1, min (N, floor (1e7 / N))); idx = zeros (N, K); D2 = zeros (N, K); for lo = 1:blk:N hi = min (lo + blk - 1, N); Db = pdist2 (X(lo:hi,:), X, Distance) .^ 2; ## A point is its own nearest neighbour. Removing it by index rather than ## by distance keeps a duplicated row, which is a neighbour at distance ## zero and belongs in the list. for r = lo:hi Db(r - lo + 1, r) = Inf; endfor [sv, so] = sort (Db, 2); idx(lo:hi,:) = so(:,1:K); D2(lo:hi,:) = sv(:,1:K); endfor ## The same per-point variance search the exact algorithm runs, over the ## neighbour row instead of over all N. condP = zeros (N, K); H = log (perplexity); for i = 1:N beta = 1; a = -Inf; c = Inf; for iter = 1:100 Pi = exp (-D2(i,:) * beta); si = max (sum (Pi), realmin); Pi = Pi / si; Hi = log (si) + beta * sum (D2(i,:) .* Pi); fval = Hi - H; if (abs (fval) < 1e-5) break; endif if (fval > 0) a = beta; if (isinf (c)) beta = 2 * beta; else beta = 0.5 * (beta + c); endif else c = beta; if (isinf (a)) beta = 0.5 * beta; else beta = 0.5 * (a + beta); endif endif endfor condP(i,:) = Pi; endfor rowi = repmat ((1:N)', 1, K); P = sparse (rowi(:), idx(:), condP(:), N, N); P = (P + P') / (2 * N); endfunction ## --------------------------------------------------------------------------- ## Gradient of the t-SNE cost and the low-dimensional affinities Q. function [grad, Q] = tsne_gradient (P, Y, N) sumY = sum (Y .^ 2, 2); num = 1 ./ (1 + sumY + sumY' - 2 * (Y * Y')); num(1:N+1:end) = 0; Q = max (num ./ sum (num(:)), realmin); L = num .* (P - Q); grad = 4 * (diag (sum (L, 1)) - L) * Y; endfunction ## --------------------------------------------------------------------------- ## Gradient of the t-SNE cost by Barnes-Hut summation. The attraction runs ## over the stored pairs alone and is formed here; the repulsion runs over ## every pair and is summed by the tree in __bhtsne__. The loss comes with it ## because both its terms are already to hand. function [grad, loss] = tsne_gradient_bh (P, Y, N, ydims, theta) [ii, jj, pv] = find (P); dY = Y(ii,:) - Y(jj,:); qn = 1 ./ (1 + sum (dY .^ 2, 2)); Fattr = zeros (N, ydims); w = pv .* qn; for m = 1:ydims Fattr(:,m) = accumarray (ii, w .* dY(:,m), [N, 1]); endfor [Frep, Z] = __bhtsne__ (Y, theta); grad = 4 * (Fattr - Frep / Z); if (nargout > 1) q = max (qn / Z, realmin); loss = sum (pv .* log (max (pv, realmin))) - sum (pv .* log (q)); endif endfunction ## --------------------------------------------------------------------------- ## Gradient descent with adaptive learning rate (Jacobi) and momentum. function [Y, loss] = tsne_embedding (Y, P, exaggeration, learnrate, opts, ... N, ydims, algorithm, theta) adp = ones (N, ydims); minRate = 0.01; momentums = [0.5, 0.8]; momentumChange = 250; exaggerationStop = 100; kk = 0.15; phi = 0.85; exact = strcmp (algorithm, "exact"); P = exaggeration * P; Ychange = zeros (N, ydims); Q = []; lossbh = []; for iter = 1:opts.MaxIter if (iter == exaggerationStop) P = P / exaggeration; exaggeration = 1; endif if (exact) [grad, Q] = tsne_gradient (P, Y, N); else [grad, lossbh] = tsne_gradient_bh (P, Y, N, ydims, theta); endif ops = sign (grad) != sign (Ychange); adp(ops) += kk; adp(! ops) *= phi; adpRate = learnrate * max (minRate, adp); if (iter < momentumChange) Ychange = momentums(1) * Ychange - adpRate .* grad; else Ychange = momentums(2) * Ychange - adpRate .* grad; endif Y = Y + Ychange; if (norm (grad, Inf) < opts.TolFun) break; endif endfor if (nargout > 1) ## With no iteration run, the loss of the initial embedding if (exact && isempty (Q)) [~, Q] = tsne_gradient (P, Y, N); elseif (! exact && isempty (lossbh)) [~, lossbh] = tsne_gradient_bh (P, Y, N, ydims, theta); endif if (exact) loss = P(:)' * log (P(:)) - P(:)' * log (Q(:)); else loss = lossbh; endif endif endfunction %!demo %! ## Embed a small five-dimensional data set into two dimensions. %! rng (42); %! X = [randn(20, 5); randn(20, 5) + 5]; %! Y = tsne (X, "Perplexity", 10); %! plot (Y(1:20,1), Y(1:20,2), "bo", Y(21:end,1), Y(21:end,2), "rx"); %! title ("t-SNE embedding"); ## Verified against MATLAB R2023b (exact algorithm) for ## X = reshape (mod ((1:60)*7, 13), 12, 5) - 6 with a fixed initial embedding. %!test %! X = reshape (mod ((1:60)*7, 13), 12, 5) - 6; %! Y0 = reshape (mod ((1:24)*3, 7), 12, 2) - 3; %! opt = struct ("MaxIter", 20, "TolFun", 0); %! [Y, loss] = tsne (X, "Algorithm", "exact", "Distance", "euclidean", ... %! "NumDimensions", 2, "Perplexity", 3, "Exaggeration", 1, ... %! "LearnRate", 100, "Standardize", false, "InitialY", Y0, ... %! "Options", opt); %! Yref = [-11.087464047262014, 26.776776669616648; ... %! 4.015876079561814, 4.314538936119215; ... %! -10.791976288780955, -28.890539694879799; ... %! -6.197553982245900, 11.136429372335288; ... %! -4.791764519072724, -29.162528702965172; ... %! -0.506807464921708, 13.209994463821561; ... %! -1.097171761767460, -24.062824347518902; ... %! 0.108706666197103, 21.076444386535410; ... %! 1.270590904095503, -17.709227609503525; ... %! 7.840024872376860, 19.937127571156172; ... %! 6.043642805354078, -19.531582453041814; ... %! 13.753383923495731, 22.678754729129128]; %! assert_equal (Y, Yref, 1e-8); %! assert_equal (loss, 0.298069770937047, 1e-10); %!test %! ## Output sizes and reproducibility with a fixed initial embedding. %! X = reshape (mod ((1:60)*7, 13), 12, 5) - 6; %! Y0 = reshape (mod ((1:24)*3, 7), 12, 2) - 3; %! Y1 = tsne (X, "Perplexity", 3, "InitialY", Y0, ... %! "Options", struct ("MaxIter", 50)); %! Y2 = tsne (X, "Perplexity", 3, "InitialY", Y0, ... %! "Options", struct ("MaxIter", 50)); %! assert_equal (size (Y1), [12, 2]); %! assert_equal (Y1, Y2, 1e-12); %!test %! ## NumDimensions controls the embedding dimension. %! X = reshape (mod ((1:60)*7, 13), 12, 5) - 6; %! Y = tsne (X, "NumDimensions", 3, "Perplexity", 3, ... %! "Options", struct ("MaxIter", 20)); %! assert_equal (size (Y), [12, 3]); %!test %! ## The Barnes-Hut tree reproduces the exact pairwise repulsion when nothing %! ## is collapsed, which is what pins the summation without an oracle. %! Y = reshape (mod ((1:60)*7, 13), 30, 2) - 6; %! [F, Z] = __bhtsne__ (Y, 0); %! n = rows (Y); %! Fe = zeros (n, 2); %! Ze = 0; %! for i = 1:n %! d = Y(i,:) - Y; %! q = 1 ./ (1 + sum (d .^ 2, 2)); %! q(i) = 0; %! Ze += sum (q); %! Fe(i,:) = sum ((q .^ 2) .* d, 1); %! endfor %! assert_equal (F, Fe, 1e-12); %! assert_equal (Z, Ze, 1e-10); %!test %! ## A larger Theta collapses cells, so it approximates rather than reproduces. %! Y = reshape (mod ((1:60)*7, 13), 30, 2) - 6; %! [F0, Z0] = __bhtsne__ (Y, 0); %! [F5, Z5] = __bhtsne__ (Y, 0.5); %! assert_equal (norm (F5 - F0, "fro") / norm (F0, "fro") < 0.1, true); %! assert_equal (abs (Z5 - Z0) / Z0 < 0.1, true); %! assert_equal (isequal (F5, F0), false); %!test %! ## The tree handles coincident points, which an embedding starts out full %! ## of and which a subdivision to one point per cell never separates. %! Y = zeros (12, 2); %! [F, Z] = __bhtsne__ (Y, 0.5); %! assert_equal (F, zeros (12, 2), 1e-12); %! assert_equal (Z, 132, 1e-10); %!test %! ## 'barneshut' returns an embedding of the right shape and a finite loss. %! X = [reshape(mod((1:100)*7, 13), 20, 5) - 6; ... %! reshape(mod((1:100)*7, 13), 20, 5) + 30]; %! [Y, loss] = tsne (X, "Algorithm", "barneshut", "Perplexity", 3, ... %! "NumDimensions", 2, "Options", struct ("MaxIter", 200)); %! assert_equal (size (Y), [40, 2]); %! assert_equal (all (isfinite (Y(:))), true); %! assert_equal (isfinite (loss) && loss > 0, true); %!test %! ## It optimizes: the divergence falls as the iterations run. %! X = [reshape(mod((1:100)*7, 13), 20, 5) - 6; ... %! reshape(mod((1:100)*7, 13), 20, 5) + 30]; %! args = {"Algorithm", "barneshut", "Perplexity", 3, ... %! "InitialY", reshape(mod((1:80)*3, 7), 40, 2) - 3}; %! [~, l1] = tsne (X, args{:}, "Options", struct ("MaxIter", 120)); %! [~, l2] = tsne (X, args{:}, "Options", struct ("MaxIter", 400)); %! assert_equal (l2 < l1, true); %!test %! ## It tracks the exact algorithm it approximates, from the same start. %! X = [reshape(mod((1:100)*7, 13), 20, 5) - 6; ... %! reshape(mod((1:100)*7, 13), 20, 5) + 30]; %! o = struct ("MaxIter", 400); %! args = {"Perplexity", 3, "InitialY", reshape(mod((1:80)*3, 7), 40, 2) - 3, ... %! "Options", o}; %! [~, lb] = tsne (X, args{:}, "Algorithm", "barneshut"); %! [~, le] = tsne (X, args{:}, "Algorithm", "exact"); %! assert_equal (abs (lb - le) / le < 0.3, true); %!test %! ## Theta reaches the fit: two values give two embeddings. %! X = reshape (mod ((1:100)*7, 13), 20, 5) - 6; %! o = struct ("MaxIter", 50); %! args = {"Algorithm", "barneshut", "Perplexity", 4, "NumDimensions", 2, ... %! "InitialY", reshape(mod((1:40)*3, 7), 20, 2) - 3, "Options", o}; %! Ya = tsne (X, args{:}, "Theta", 0); %! Yb = tsne (X, args{:}, "Theta", 0.8); %! assert_equal (isequal (Ya, Yb), false); %!test %! ## 'barneshut' takes the same NumDimensions the tree is built for. %! X = reshape (mod ((1:100)*7, 13), 20, 5) - 6; %! for d = 1:3 %! Y = tsne (X, "Algorithm", "barneshut", "NumDimensions", d, ... %! "Perplexity", 4, "Options", struct ("MaxIter", 20)); %! assert_equal (size (Y), [20, d]); %! endfor %!test %! ## No iteration returns the initial embedding with its loss, which a %! ## single iteration also reports, the loss coming before each step. %! X = reshape (mod ((1:60)*7, 13), 12, 5) - 6; %! Y0 = reshape (mod ((1:24)*3, 7), 12, 2) - 3; %! args = {"Algorithm", "exact", "Distance", "euclidean", "Perplexity", 3, ... %! "InitialY", Y0, "Standardize", false}; %! [Y, loss] = tsne (X, args{:}, "Options", struct ("MaxIter", 0)); %! [~, l1] = tsne (X, args{:}, "Options", struct ("MaxIter", 1, "TolFun", 0)); %! assert_equal (Y, Y0); %! assert_equal (loss, l1); %!test %! X = reshape (mod ((1:60)*7, 13), 12, 5) - 6; %! Y0 = reshape (mod ((1:24)*3, 7), 12, 2) - 3; %! args = {"Algorithm", "barneshut", "Perplexity", 3, "InitialY", Y0}; %! [Y, loss] = tsne (X, args{:}, "Options", struct ("MaxIter", 0)); %! [~, l1] = tsne (X, args{:}, "Options", struct ("MaxIter", 1, "TolFun", 0)); %! assert_equal (Y, Y0); %! assert_equal (loss, l1); ## Test input validation %!error tsne () %!error tsne ({1, 2}) %!error tsne ([1, Inf; 2, 3]) %!error ... %! tsne (ones (5, 3), "Algorithm", "bogus") %!error ... %! tsne (ones (5, 3), "Algorithm", "barneshut", "Theta", -1) %!error ... %! tsne (ones (5, 3), "Algorithm", "barneshut", "Theta", [1, 2]) %!error ... %! tsne (ones (8, 5), "Algorithm", "barneshut", "NumDimensions", 4, "Perplexity", 2) %!error ... %! tsne (ones (5, 3), "Perplexity", 5) %!error ... %! tsne (ones (5, 3), "Perplexity", 2, "Exaggeration", 0) %!error ... %! tsne (ones (5, 3), "Perplexity", 2, "LearnRate", -1) %!error tsne (ones (5, 3), "bogus", 1) %!error ... %! tsne (ones (5, 3), "Perplexity", 2, "NumDimensions", 0) %!error ... %! tsne (ones (5, 3), "Perplexity", 2, "NumDimensions", -1) %!error ... %! tsne (ones (5, 3), "Perplexity", 2, "NumDimensions", 2.5) %!error ... %! tsne (ones (5, 3), "Perplexity", 2, "NumDimensions", [2, 3]) %!error ... %! tsne (ones (5, 3), "Perplexity", 2, "NumDimensions", "a") %!error ... %! tsne (ones (5, 3), "Perplexity", 2, "NumDimensions", true) %!error ... %! tsne (ones (5, 3), "Perplexity", 2, "NumDimensions", Inf) %!error ... %! tsne (ones (5, 3), "Perplexity", 2, "NumDimensions", 4) ## An empty NumDimensions falls back to the default, and an integer-typed one ## is accepted, both as in MATLAB. %!test %! X = [1 2 3; 2 4 6; 3 5 9; 4 8 11; 5 9 14; 6 11 17]; %! Y = tsne (X, "Perplexity", 2, "NumDimensions", []); %! assert_equal (columns (Y), 2); %! Y = tsne (X, "Perplexity", 2, "NumDimensions", int32 (2)); %! assert_equal (columns (Y), 2); %! Y = tsne (X, "Perplexity", 2, "NumDimensions", 3); %! assert_equal (columns (Y), 3); statistics-release-2.1.2/inst/Distribution_Classes/000077500000000000000000000000001527311104300224325ustar00rootroot00000000000000statistics-release-2.1.2/inst/Distribution_Classes/+prob/000077500000000000000000000000001527311104300234475ustar00rootroot00000000000000statistics-release-2.1.2/inst/Distribution_Classes/+prob/BetaDistribution.m000066400000000000000000001201561527311104300271050ustar00rootroot00000000000000## Copyright (C) 2024-2025 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef BetaDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.BetaDistribution ## ## Beta probability distribution object. ## ## A @code{prob.BetaDistribution} object consists of parameters, a model ## description, and sample data for a beta probability distribution. ## ## The beta distribution is a family of continuous probability distributions ## defined on the interval @math{[0, 1]} in terms of two positive parameters, ## denoted by alpha @qcode{(@var{a})} and beta @qcode{(@var{b})}, that appear ## as exponents of the variable and its complement to 1, respectively, and ## control the shape of the distribution. ## ## There are several ways to create a @code{prob.BetaDistribution} object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor @qcode{prob.BetaDistribution (@var{a}, @var{b})} ## to create a beta distribution with fixed parameter values @var{a} and ## @var{b}. ## @item Use the static method @qcode{prob.BetaDistribution.fit (@var{x}, ## @var{alpha}, @var{freq}, @var{options})} to fit a distribution to the data ## in @var{x} using the same input arguments as the @code{betafit} function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the beta distribution can be found at ## @url{https://en.wikipedia.org/wiki/Beta_distribution} ## ## @seealso{fitdist, makedist, betacdf, betainv, betapdf, betarnd, betafit, ## betalike, betastat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.BetaDistribution} {property} a ## ## First shape parameter ## ## A positive scalar value characterizing the shape of the beta ## distribution. You can access the @qcode{a} property using dot name ## assignment. ## ## @end deftp a ## -*- texinfo -*- ## @deftp {prob.BetaDistribution} {property} b ## ## Second shape parameter ## ## A positive scalar value characterizing the shape of the beta ## distribution. You can access the @qcode{b} property using dot name ## assignment. ## ## @end deftp b endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.BetaDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Beta'; ## -*- texinfo -*- ## @deftp {prob.BetaDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 2; ## -*- texinfo -*- ## @deftp {prob.BetaDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'a', 'b'}; ## -*- texinfo -*- ## @deftp {prob.BetaDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'First shape parameter', 'Second shape parameter'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = false; DistributionCode = 'beta'; ParameterRange = [realmin, realmin; Inf, Inf]; ParameterLogCI = [true, true]; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.BetaDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{2*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{a} and @qcode{b} ## properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.BetaDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.BetaDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.BetaDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{2*2} numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only ## meaningful when the distribution was fitted to data. If the distribution ## object was created with fixed parameters, or a parameter of a fitted ## distribution is modified, then all elements of the variance-covariance ## are zero. This property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.BetaDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*2} logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.BetaDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data} : a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens} : an empty array, since @qcode{prob.BetaDistribution} ## does not allow censoring. ## @item @qcode{frequency} : a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.BetaDistribution} {@var{pd} =} BetaDistribution (@var{a}, @var{b}) ## @deftypefnx {prob.BetaDistribution} {@var{pd} =} BetaDistribution () ## ## Create a @code{prob.BetaDistribution} object. ## ## @var{a} and @var{b} are the distribution parameters, which the class help ## describes. Called with no arguments the parameters take their defaults, ## @var{a} 1 and @var{b} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = BetaDistribution (a, b) if (nargin == 0) a = 1; b = 1; endif checkparams (a, b); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [a, b]; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'beta distribution'); endfunction function disp (this) __disp__ (this, 'beta distribution'); endfunction function this = set.a (this, a) checkparams (a, this.b); this.InputData = []; this.ParameterValues(1) = a; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function a = get.a (this) a = this.ParameterValues(1); endfunction function this = set.b (this, b) checkparams (this.a, b); this.InputData = []; this.ParameterValues(2) = b; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function b = get.b (this) b = this.ParameterValues(2); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.BetaDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.BetaDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = betacdf (x, this.a, this.b); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= betacdf (lx, this.a, this.b); p(! (lb | ub)) /= diff (betacdf ([lx, ux], this.a, this.b)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BetaDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = betacdf (this.Truncation(1), this.a, this.b); up = betacdf (this.Truncation(2), this.a, this.b); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = betainv (np, this.a, this.b); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = betainv (p, this.a, this.b); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BetaDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BetaDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = betastat (this.a, this.b); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BetaDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = betacdf ([lx, ux], this.a, this.b); m = betainv (sum (Fa_b) / 2, this.a, this.b); else m = betainv (0.5, this.a, this.b); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BetaDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood ## of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = betalike ([this.a, this.b], this.InputData.data, ... this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BetaDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.BetaDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the ## confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BetaDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = betapdf (x, this.a, this.b); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (betacdf ([lx, ux], this.a, this.b)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BetaDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.BetaDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.BetaDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BetaDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.BetaDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.BetaDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.BetaDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.BetaDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.BetaDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the beta distribution, @qcode{@var{pnum} = 1} selects the parameter ## @qcode{a} and @qcode{@var{pnum} = 2} selects the parameter @qcode{b}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BetaDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.BetaDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.BetaDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.BetaDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{betarnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) sz = [varargin{:}]; ps = prod (sz); ## Get an estimate of how many more random numbers we need to randomly ## pick the appropriate size from lx = this.Truncation(1); ux = this.Truncation(2); ratio = 1 / diff (betacdf ([lx, ux], this.a, this.b)); nsize = fix (2 * ratio * ps); # times 2 to be on the safe side ## Generate the numbers and remove out-of-bound random samples r = betarnd (this.a, this.b, nsize, 1); r(r < lx | r > ux) = []; ## Randomly select the required size and reshape to requested dimensions idx = randperm (numel (r), ps); r = reshape (r(idx), sz); else r = betarnd (this.a, this.b, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BetaDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BetaDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, ## and upper limit, @var{upper}. If @var{pd} is fitted to data with ## @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BetaDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = betastat (this.a, this.b); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) freq = []; else freq = varargin{2}; endif if (nargin < 4) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else options = varargin{3}; endif ## Fit data [phat, pci] = betafit (x, alpha, freq, options); [~, acov] = betalike (phat, x, freq); ## Create fitted distribution object pd = prob.BetaDistribution.makeFitted (phat, pci, acov, x, freq); endfunction function pd = makeFitted (phat, pci, acov, x, freq) a = phat(1); b = phat(2); pd = prob.BetaDistribution (a, b); pd.ParameterCI = pci; pd.ParameterIsFixed = [false, false]; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', [], 'freq', freq); endfunction endmethods endclassdef function checkparams (a, b) if (! (isscalar (a) && isnumeric (a) && isreal (a) && isfinite (a) && a > 0)) error ("BetaDistribution: A must be a positive real scalar.") endif if (! (isscalar (b) && isnumeric (b) && isreal (b) && isfinite (b) && b > 0)) error ("BetaDistribution: B must be a positive real scalar.") endif endfunction %!demo %! ## Generate a data set of 5000 random samples from a Beta distribution with %! ## parameters a = 2 and b = 5. Fit a Beta distribution to this data and plot %! ## a PDF of the fitted distribution superimposed on a histogram of the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ('Beta', 'a', 2, 'b', 5) %! data = random (pd_fixed, 5000, 1); %! pd_fitted = fitdist (data, 'Beta') %! plot (pd_fitted) %! msg = 'Fitted Beta distribution with a = %0.2f and b = %0.2f'; %! title (sprintf (msg, pd_fitted.a, pd_fitted.b)) ## Test output %!shared pd, t %! pd = prob.BetaDistribution; %! t = truncate (pd, 0.2, 0.8); %!assert_equal (cdf (pd, [0:0.2:1]), [0, 0.2, 0.4, 0.6, 0.8, 1], 1e-4); %!assert_equal (cdf (t, [0:0.2:1]), [0, 0, 0.3333, 0.6667, 1, 1], 1e-4); %!assert_equal (cdf (pd, [-1, 1, NaN]), [0, 1, NaN], 1e-4); %!assert_equal (cdf (t, [-1, 1, NaN]), [0, 1, NaN], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [0, 0.2, 0.4, 0.6, 0.8, 1], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [0.2, 0.32, 0.44, 0.56, 0.68, 0.8], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 0.4, 0.6, 0.8, 1, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 0.44, 0.56, 0.68, 0.8, NaN], 1e-4); %!assert_equal (iqr (pd), 0.5, 1e-4); %!assert_equal (iqr (t), 0.3, 1e-4); %!assert_equal (mean (pd), 0.5); %!assert_equal (mean (t), 0.5, 1e-6); %!assert_equal (median (pd), 0.5); %!assert_equal (median (t), 0.5, 1e-6); %!assert_equal (pdf (pd, [0:0.2:1]), [1, 1, 1, 1, 1, 1], 1e-4); %!assert_equal (pdf (t, [0:0.2:1]), [0, 1.6667, 1.6667, 1.6667, 1.6667, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1, NaN]), [0, 1, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1, NaN]), [0, 0, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 0.2), false); %!assert_equal (any (random (t, 1000, 1) > 0.8), false); %!assert_equal (std (pd), 0.2887, 1e-4); %!assert_equal (std (t), 0.1732, 1e-4); %!assert_equal (var (pd), 0.0833, 1e-4); %!assert_equal (var (t), 0.0300, 1e-4); %!test %! ## The profile over the first free parameter: 21 grid values, one row of %! ## OTHER per value, and the likelihood peaking at the fitted estimate. %! x = [0.2; 0.5; 0.7; 0.3; 0.8; 0.4; 0.6; 0.55; 0.25; 0.75; ... %! 0.35; 0.65; 0.45; 0.15; 0.85; 0.5; 0.6; 0.3; 0.7; 0.4]; %! pd = fitdist (x, 'Beta'); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (size (param), [1, 21]); %! assert_equal (size (other), [21, 1]); %! assert_equal (proflik (pd), nlogL); %! [~, imax] = max (nlogL); %! assert_equal (abs (param(imax) - pd.ParameterValues(1)) <= param(2) - param(1), true); ## Test input validation ## 'prob.BetaDistribution' constructor %!error ... %! prob.BetaDistribution (0, 1) %!error ... %! prob.BetaDistribution (Inf, 1) %!error ... %! prob.BetaDistribution (i, 1) %!error ... %! prob.BetaDistribution ('a', 1) %!error ... %! prob.BetaDistribution ([1, 2], 1) %!error ... %! prob.BetaDistribution (NaN, 1) %!error ... %! prob.BetaDistribution (1, 0) %!error ... %! prob.BetaDistribution (1, -1) %!error ... %! prob.BetaDistribution (1, Inf) %!error ... %! prob.BetaDistribution (1, i) %!error ... %! prob.BetaDistribution (1, 'a') %!error ... %! prob.BetaDistribution (1, [1, 2]) %!error ... %! prob.BetaDistribution (1, NaN) ## 'cdf' method %!error ... %! cdf (prob.BetaDistribution, 2, 'uper') %!error ... %! cdf (prob.BetaDistribution, 2, 3) ## 'paramci' method %!shared x %! randg ('seed', 1); %! x = betarnd (1, 1, [100, 1]); %!error ... %! paramci (prob.BetaDistribution.fit (x), 'alpha') %!error ... %! paramci (prob.BetaDistribution.fit (x), 'alpha', 0) %!error ... %! paramci (prob.BetaDistribution.fit (x), 'alpha', 1) %!error ... %! paramci (prob.BetaDistribution.fit (x), 'alpha', [0.5 2]) %!error ... %! paramci (prob.BetaDistribution.fit (x), 'alpha', '') %!error ... %! paramci (prob.BetaDistribution.fit (x), 'alpha', {0.05}) %!error ... %! paramci (prob.BetaDistribution.fit (x), 'parameter', 'a', 'alpha', {0.05}) %!error ... %! paramci (prob.BetaDistribution.fit (x), 'parameter', {'a', 'b', 'param'}) %!error ... %! paramci (prob.BetaDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', {'a', 'b', 'param'}) %!error ... %! paramci (prob.BetaDistribution.fit (x), 'parameter', 'param') %!error ... %! paramci (prob.BetaDistribution.fit (x), 'alpha', 0.01, 'parameter', 'param') %!error ... %! paramci (prob.BetaDistribution.fit (x), 'NAME', 'value') %!error ... %! paramci (prob.BetaDistribution.fit (x), 'alpha', 0.01, 'NAME', 'value') %!error ... %! paramci (prob.BetaDistribution.fit (x), 'alpha', 0.01, 'parameter', 'a', ... %! 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.BetaDistribution, 'Parent') %!error ... %! plot (prob.BetaDistribution, 'PlotType', 12) %!error ... %! plot (prob.BetaDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.BetaDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.BetaDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.BetaDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.BetaDistribution, 'Discrete', {true}) %!error ... %! plot (prob.BetaDistribution, 'Parent', 12) %!error ... %! plot (prob.BetaDistribution, 'Parent', 'hax') %!error ... %! plot (prob.BetaDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.BetaDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.BetaDistribution, 2) %!error ... %! proflik (prob.BetaDistribution.fit (x), 3) %!error ... %! proflik (prob.BetaDistribution.fit (x), [1, 2]) %!error ... %! proflik (prob.BetaDistribution.fit (x), {1}) %!error ... %! proflik (prob.BetaDistribution.fit (x), 1, ones (2)) %!error ... %! proflik (prob.BetaDistribution.fit (x), 1, 'Display') %!error ... %! proflik (prob.BetaDistribution.fit (x), 1, 'Display', 1) %!error ... %! proflik (prob.BetaDistribution.fit (x), 1, 'Display', {1}) %!error ... %! proflik (prob.BetaDistribution.fit (x), 1, 'Display', {'on'}) %!error ... %! proflik (prob.BetaDistribution.fit (x), 1, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.BetaDistribution.fit (x), 1, 'Display', 'onnn') %!error ... %! proflik (prob.BetaDistribution.fit (x), 1, 'NAME', 'on') %!error ... %! proflik (prob.BetaDistribution.fit (x), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.BetaDistribution.fit (x), 1, {[1 2 3 4]}, 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.BetaDistribution) %!error ... %! truncate (prob.BetaDistribution, 2) %!error ... %! truncate (prob.BetaDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.BetaDistribution (1, 1); %! pd(2) = prob.BetaDistribution (1, 3); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/BinomialDistribution.m000066400000000000000000001242351527311104300277660ustar00rootroot00000000000000## Copyright (C) 2024-2025 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef BinomialDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.BinomialDistribution ## ## Binomial probability distribution object. ## ## A @code{prob.BinomialDistribution} object consists of parameters, a model ## description, and sample data for a binomial probability distribution. ## ## The binomial distribution is a discrete probability distribution that ## models the number of successes in a sequence of @var{N} independent trials, ## each with a probability of success @var{p}. ## ## There are several ways to create a @code{prob.BinomialDistribution} object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor ## @qcode{prob.BinomialDistribution (@var{N}, @var{p})} to create a binomial ## distribution with fixed parameter values @var{N} and @var{p}. ## @item Use the static method @qcode{prob.BinomialDistribution.fit (@var{x}, ## @var{ntrials}, @var{alpha})} to fit a distribution to the data in @var{x} ## using the same input arguments as the @code{binofit} function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the binomial distribution can be found at ## @url{https://en.wikipedia.org/wiki/Binomial_distribution} ## ## @seealso{fitdist, makedist, binocdf, binoinv, binopdf, binornd, binofit, ## binolike, binostat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.BinomialDistribution} {property} N ## ## Number of trials ## ## A positive integer value characterizing the number of trials in the ## binomial distribution. You can access the @qcode{N} property using dot ## name assignment. ## ## @end deftp N ## -*- texinfo -*- ## @deftp {prob.BinomialDistribution} {property} p ## ## Probability of success ## ## A scalar value in the range @math{[0, 1]} characterizing the probability ## of success in each trial of the binomial distribution. You can access ## the @qcode{p} property using dot name assignment. ## ## @end deftp p endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.BinomialDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Binomial'; ## -*- texinfo -*- ## @deftp {prob.BinomialDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 2; ## -*- texinfo -*- ## @deftp {prob.BinomialDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'N', 'p'}; ## -*- texinfo -*- ## @deftp {prob.BinomialDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Number of trials', 'Probability of success'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = false; DistributionCode = 'bino'; ParameterRange = [realmin, realmin; Inf, 1]; ParameterLogCI = [true, true]; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.BinomialDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{2*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{N} and @qcode{p} ## properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.BinomialDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.BinomialDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.BinomialDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{2*2} numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only ## meaningful when the distribution was fitted to data. If the distribution ## object was created with fixed parameters, or a parameter of a fitted ## distribution is modified, then all elements of the variance-covariance ## are zero. This property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.BinomialDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*2} logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.BinomialDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data} : a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens} : an empty array, since ## @qcode{prob.BinomialDistribution} does not allow censoring. ## @item @qcode{frequency} : a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.BinomialDistribution} {@var{pd} =} BinomialDistribution (@var{N}, @var{p}) ## @deftypefnx {prob.BinomialDistribution} {@var{pd} =} BinomialDistribution () ## ## Create a @code{prob.BinomialDistribution} object. ## ## @var{N} and @var{p} are the distribution parameters, which the class help ## describes. Called with no arguments the parameters take their defaults, ## @var{N} 1 and @var{p} 0.5. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = BinomialDistribution (N, p) if (nargin == 0) N = 1; p = 0.5; endif checkparams (N, p); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [N, p]; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'binomial distribution'); endfunction function disp (this) __disp__ (this, 'binomial distribution'); endfunction function this = set.N (this, N) checkparams (N, this.p); this.InputData = []; this.ParameterValues(1) = N; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function N = get.N (this) N = this.ParameterValues(1); endfunction function this = set.p (this, p) checkparams (this.N, p); this.InputData = []; this.ParameterValues(2) = p; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function p = get.p (this) p = this.ParameterValues(2); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.BinomialDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.BinomialDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = binocdf (x, this.N, this.p); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= binocdf (lx - 1, this.N, this.p); p(! (lb | ub)) /= diff (binocdf ([lx-1, ux], this.N, this.p)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BinomialDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif umax = binoinv (1, this.N, this.p); if (this.IsTruncated && this.Truncation(2) >= umax) ## Find out of range p values is_nan = p < 0 | p > 1; ## Get lower and upper boundaries lx = ceil (this.Truncation(1)); ux = floor (this.Truncation(2)); ux = min (ux, umax); lp = binocdf (lx - 1, this.N, this.p); up = binocdf (ux, this.N, this.p); p = lp + p * (up - lp); p(is_nan) = NaN; endif x = binoinv (p, this.N, this.p); if (this.IsTruncated) x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BinomialDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BinomialDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif m = binostat (this.N, this.p); if (this.IsTruncated) lx = ceil (this.Truncation(1)); ux = floor (this.Truncation(2)); ux = min (ux, binoinv (1, this.N, this.p)); x = [lx:ux]; m = sum (x .* pdf (this, x)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BinomialDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = binocdf ([lx, ux], this.N, this.p); m = binoinv (sum (Fa_b) / 2, this.N, this.p); else m = binoinv (0.5, this.N, this.p); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BinomialDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood ## of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = binolike ([this.N, this.p], this.InputData.data); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BinomialDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.BinomialDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the ## confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BinomialDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = binopdf (x, this.N, this.p); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (binocdf ([lx-1, ux], this.N, this.p)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BinomialDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.BinomialDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.BinomialDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, true, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BinomialDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.BinomialDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.BinomialDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.BinomialDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.BinomialDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.BinomialDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the binomial distribution, @qcode{@var{pnum} = 1} selects the ## parameter @qcode{N} and @qcode{@var{pnum} = 2} selects the parameter ## @qcode{p}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BinomialDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.BinomialDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.BinomialDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.BinomialDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{binornd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) sz = [varargin{:}]; ps = prod (sz); ## Get an estimate of how many more random numbers we need to randomly ## pick the appropriate size from lx = this.Truncation(1); ux = this.Truncation(2); ratio = 1 / diff (binocdf ([lx-1, ux], this.N, this.p)); nsize = fix (2 * ratio * ps); # times 2 to be on the safe side ## Generate the numbers and remove out-of-bound random samples r = binornd (this.N, this.p, nsize, 1); r(r < lx | r > ux) = []; ## Randomly select the required size and reshape to requested dimensions idx = randperm (numel (r), ps); r = reshape (r(idx), sz); else r = binornd (this.N, this.p, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BinomialDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BinomialDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, ## and upper limit, @var{upper}. If @var{pd} is fitted to data with ## @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); endif ## Check boundaries and constrain within support: Natural numbers lower = round (lower); upper = round (upper); lower(lower < 0) = 0; if (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BinomialDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) ## Calculate untruncated mean and variance [um, uv] = binostat (this.N, this.p); ## Calculate truncated mean m = mean (this); ## Get lower and upper boundaries lx = ceil (this.Truncation(1)); ux = floor (this.Truncation(2)); ux = min (ux, binoinv (1, this.N, this.p)); ## Handle infinite support on the right if (isequal (ux, Inf)) ratio = 1 / diff (binocdf ([lx-1, ux], this.N, this.p)); x = 0:lx-1; v = ratio * (uv + (um - m) ^ 2 - sum (((x - m) .^ 2) .* ... binopdf (x, this.N, this.p))); else x = lx:ux; v = sum (((x - m) .^ 2) .* pdf (this, x)); endif else [~, v] = binostat (this.N, this.p); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, ntrials, varargin) ## Check input arguments if (nargin < 3) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 4) freq = ones (size (x)); else freq = varargin{2}; endif ## Fit data [pshat, psci] = mle (x, 'distribution', 'binomial', 'alpha', alpha, ... 'ntrials', ntrials, 'frequency', freq); phat = [ntrials, pshat]; pci = [[ntrials; ntrials], psci(:)]; [~, acov] = binolike (phat, x, freq); ## Create fitted distribution object pd = prob.BinomialDistribution.makeFitted (phat, pci, acov, x, freq); endfunction function pd = makeFitted (phat, pci, acov, x, freq) N = phat(1); p = phat(2); pd = prob.BinomialDistribution (N, p); pd.ParameterCI = pci; pd.ParameterIsFixed = [true, false]; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', [], 'freq', freq); endfunction endmethods endclassdef function checkparams (N, p) if (! (isscalar (N) && isnumeric (N) && isreal (N) && isfinite (N) && N > 0 && fix (N) == N)) error ("BinomialDistribution: N must be a positive integer scalar.") endif if (! (isscalar (p) && isnumeric (p) && isreal (p) && isfinite (p) && p >= 0 && p <= 1)) error (strcat ("BinomialDistribution: p must be a real", ... " scalar bounded in the range [0, 1].")) endif endfunction ## Test output %!shared pd, t, t_inf %! pd = prob.BinomialDistribution (5, 0.5); %! t = truncate (pd, 2, 4); %! t_inf = truncate (pd, 2, Inf); %!assert_equal (cdf (pd, [0:5]), [0.0312, 0.1875, 0.5, 0.8125, 0.9688, 1], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0.4, 0.8, 1, 1], 1e-4); %!assert_equal (cdf (t_inf, [0:5]), [0, 0, 0.3846, 0.7692, 0.9615, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4, NaN]), [0.1875, 0.5, 0.8125, 0.9688, NaN], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4, NaN]), [0, 0.4, 0.8, 1, NaN], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [0, 2, 2, 3, 3, 5], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2, 2, 3, 3, 4], 1e-4); %!assert_equal (icdf (t_inf, [0:0.2:1]), [2, 2, 3, 3, 4, 5], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 2, 3, 3, 5, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2, 3, 3, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 1); %!assert_equal (iqr (t), 1); %!assert_equal (mean (pd), 2.5, 1e-10); %!assert_equal (mean (t), 2.8, 1e-10); %!assert_equal (mean (t_inf), 2.8846, 1e-4); ## The median is the smallest attainable value whose CDF reaches 0.5, so it is ## in the support: 2 for Binomial (5, 0.5), as R2024a returns. Measured ## 2026-08-17. %!assert_equal (median (pd), 2); %!assert_equal (median (prob.BinomialDistribution (7, 0.5)), 3); %!assert_equal (median (prob.BinomialDistribution (10, 0.5)), 5); %!assert_equal (icdf (pd, 0.5), 2); %!assert_equal (median (t), 3); %!assert_equal (pdf (pd, [0:5]), [0.0312, 0.1562, 0.3125, 0.3125, 0.1562, 0.0312], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 0.4, 0.4, 0.2, 0], 1e-4); %!assert_equal (pdf (t_inf, [0:5]), [0, 0, 0.3846, 0.3846, 0.1923, 0.0385], 1e-4); %!assert_equal (pdf (pd, [-1, 1.5, NaN]), [0, 0, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1.5, NaN]), [0, 0, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 1.1180, 1e-4); %!assert_equal (std (t), 0.7483, 1e-4); %!assert_equal (std (t_inf), 0.8470, 1e-4); %!assert_equal (var (pd), 1.2500, 1e-4); %!assert_equal (var (t), 0.5600, 1e-4); %!assert_equal (var (t_inf), 0.7175, 1e-4); %!test %! ## paramci reports one column per parameter, N held at its own value, as %! ## MATLAB does; it used to return a single 1x2 row, which stopped proflik. %! x = [3; 1; 4; 1; 5; 2; 6; 5; 3; 5; 2; 4; 1; 3; 2; 4; 6; 2; 3; 1]; %! pd = fitdist (x, 'Binomial', 'NTrials', 8); %! assert_equal (paramci (pd), [8, 0.317538; 8, 0.473975], 1e-6); %! assert_equal (paramci (pd, 'Parameter', 'p'), [0.317538; 0.473975], 1e-6); %! assert_equal (paramci (pd, 'Parameter', 'N'), [8; 8]); %!test %! ## The profile over p, the only estimated parameter, takes 101 grid values. %! ## OTHER reports the fixed N at its own value; MATLAB documents that and %! ## then returns 0 there, so this follows its documentation, not its code. %! x = [3; 1; 4; 1; 5; 2; 6; 5; 3; 5; 2; 4; 1; 3; 2; 4; 6; 2; 3; 1]; %! pd = fitdist (x, 'Binomial', 'NTrials', 8); %! [nlogL, param, other] = proflik (pd, 2); %! assert_equal (size (param), [1, 101]); %! assert_equal (size (other), [101, 1]); %! assert_equal (unique (other), 8); %! assert_equal (proflik (pd), nlogL); ## Test input validation ## 'prob.BinomialDistribution' constructor %!error ... %! prob.BinomialDistribution (Inf, 0.5) %!error ... %! prob.BinomialDistribution (i, 0.5) %!error ... %! prob.BinomialDistribution ('a', 0.5) %!error ... %! prob.BinomialDistribution ([1, 2], 0.5) %!error ... %! prob.BinomialDistribution (NaN, 0.5) %!error ... %! prob.BinomialDistribution (1, 1.01) %!error ... %! prob.BinomialDistribution (1, -0.01) %!error ... %! prob.BinomialDistribution (1, Inf) %!error ... %! prob.BinomialDistribution (1, i) %!error ... %! prob.BinomialDistribution (1, 'a') %!error ... %! prob.BinomialDistribution (1, [1, 2]) %!error ... %! prob.BinomialDistribution (1, NaN) ## 'cdf' method %!error ... %! cdf (prob.BinomialDistribution, 2, 'uper') %!error ... %! cdf (prob.BinomialDistribution, 2, 3) ## 'paramci' method %!shared x %! rand ('seed', 2); %! x = binornd (5, 0.5, [1, 100]); %!error ... %! paramci (prob.BinomialDistribution.fit (x, 6), 'alpha') %!error ... %! paramci (prob.BinomialDistribution.fit (x, 6), 'alpha', 0) %!error ... %! paramci (prob.BinomialDistribution.fit (x, 6), 'alpha', 1) %!error ... %! paramci (prob.BinomialDistribution.fit (x, 6), 'alpha', [0.5 2]) %!error ... %! paramci (prob.BinomialDistribution.fit (x, 6), 'alpha', '') %!error ... %! paramci (prob.BinomialDistribution.fit (x, 6), 'alpha', {0.05}) %!error ... %! paramci (prob.BinomialDistribution.fit (x, 6), 'parameter', 'p', ... %! 'alpha', {0.05}) %!error ... %! paramci (prob.BinomialDistribution.fit (x, 6), ... %! 'parameter', {'N', 'p', 'param'}) %!error ... %! paramci (prob.BinomialDistribution.fit (x, 6), 'alpha', 0.01, ... %! 'parameter', {'N', 'p', 'param'}) %!error ... %! paramci (prob.BinomialDistribution.fit (x, 6), 'parameter', 'param') %!error ... %! paramci (prob.BinomialDistribution.fit (x, 6), 'alpha', 0.01, ... %! 'parameter', 'param') %!error ... %! paramci (prob.BinomialDistribution.fit (x, 6), 'NAME', 'value') %!error ... %! paramci (prob.BinomialDistribution.fit (x, 6), 'alpha', 0.01, ... %! 'NAME', 'value') %!error ... %! paramci (prob.BinomialDistribution.fit (x, 6), 'alpha', 0.01, ... %! 'parameter', 'p', 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.BinomialDistribution, 'Parent') %!error ... %! plot (prob.BinomialDistribution, 'PlotType', 12) %!error ... %! plot (prob.BinomialDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.BinomialDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.BinomialDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.BinomialDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.BinomialDistribution, 'Discrete', {true}) %!error ... %! plot (prob.BinomialDistribution, 'Parent', 12) %!error ... %! plot (prob.BinomialDistribution, 'Parent', 'hax') %!error ... %! plot (prob.BinomialDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.BinomialDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.BinomialDistribution, 2) %!error ... %! proflik (prob.BinomialDistribution.fit (x, 6), 3) %!error ... %! proflik (prob.BinomialDistribution.fit (x, 6), [1, 2]) %!error ... %! proflik (prob.BinomialDistribution.fit (x, 6), {1}) %!error ... %! proflik (prob.BinomialDistribution.fit (x, 6), 2, ones (2)) %!error ... %! proflik (prob.BinomialDistribution.fit (x, 6), 2, 'Display') %!error ... %! proflik (prob.BinomialDistribution.fit (x, 6), 2, 'Display', 1) %!error ... %! proflik (prob.BinomialDistribution.fit (x, 6), 2, 'Display', {1}) %!error ... %! proflik (prob.BinomialDistribution.fit (x, 6), 2, 'Display', {'on'}) %!error ... %! proflik (prob.BinomialDistribution.fit (x, 6), 2, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.BinomialDistribution.fit (x, 6), 2, 'Display', 'onnn') %!error ... %! proflik (prob.BinomialDistribution.fit (x, 6), 2, 'NAME', 'on') %!error ... %! proflik (prob.BinomialDistribution.fit (x, 6), 2, {'NAME'}, 'on') %!error ... %! proflik (prob.BinomialDistribution.fit (x, 6), 2, {[1 2 3]}, 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.BinomialDistribution) %!error ... %! truncate (prob.BinomialDistribution, 2) %!error ... %! truncate (prob.BinomialDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.BinomialDistribution (1, 0.5); %! pd(2) = prob.BinomialDistribution (1, 0.6); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/BirnbaumSaundersDistribution.m000066400000000000000000001252101527311104300314720ustar00rootroot00000000000000## Copyright (C) 2024-2025 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef BirnbaumSaundersDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.BirnbaumSaundersDistribution ## ## Birnbaum-Saunders probability distribution object. ## ## A @code{prob.BirnbaumSaundersDistribution} object consists of parameters, a ## model description, and sample data for a Birnbaum-Saunders probability ## distribution. ## ## The Birnbaum-Saunders distribution is a continuous probability distribution ## that models the time to failure of materials subjected to cyclic loading. ## It is defined by scale parameter @var{beta} and shape parameter ## @var{gamma}. ## ## There are several ways to create a @code{prob.BirnbaumSaundersDistribution} ## object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor ## @qcode{prob.BirnbaumSaundersDistribution (@var{beta}, @var{gamma})} to ## create a Birnbaum-Saunders distribution with fixed parameter values ## @var{beta} and @var{gamma}. ## @item Use the static method @qcode{prob.BirnbaumSaundersDistribution.fit ## (@var{x}, @var{alpha}, @var{censor}, @var{freq}, @var{options})} to fit a ## distribution to the data in @var{x} using the same input arguments as the ## @code{bisafit} function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the Birnbaum-Saunders distribution can be found ## at ## @url{https://en.wikipedia.org/wiki/Birnbaum%E2%80%93Saunders_distribution} ## ## @seealso{fitdist, makedist, bisacdf, bisainv, bisapdf, bisarnd, bisafit, ## bisalike, bisastat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.BirnbaumSaundersDistribution} {property} beta ## ## Scale parameter ## ## A positive scalar value characterizing the scale of the ## Birnbaum-Saunders distribution. You can access the @qcode{beta} ## property using dot name assignment. ## ## @end deftp beta ## -*- texinfo -*- ## @deftp {prob.BirnbaumSaundersDistribution} {property} gamma ## ## Shape parameter ## ## A positive scalar value characterizing the shape of the ## Birnbaum-Saunders distribution. You can access the @qcode{gamma} ## property using dot name assignment. ## ## @end deftp gamma endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.BirnbaumSaundersDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Birnbaum-Saunders'; ## -*- texinfo -*- ## @deftp {prob.BirnbaumSaundersDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 2; ## -*- texinfo -*- ## @deftp {prob.BirnbaumSaundersDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'beta', 'gamma'}; ## -*- texinfo -*- ## @deftp {prob.BirnbaumSaundersDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Scale', 'Shape'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = true; DistributionCode = 'bisa'; ParameterRange = [realmin, realmin; Inf, Inf]; ParameterLogCI = [true, true]; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.BirnbaumSaundersDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{2*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{beta} and @qcode{gamma} ## properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.BirnbaumSaundersDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.BirnbaumSaundersDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.BirnbaumSaundersDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{2*2} numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter, and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only meaningful ## when the distribution was fitted to data. If the distribution object was ## created with fixed parameters, or a parameter of a fitted distribution is ## modified, then all elements of the variance-covariance are zero. This ## property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.BirnbaumSaundersDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*2} logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.BirnbaumSaundersDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: a numeric vector of logical values indicating ## censoring information corresponding to the elements of the data used for ## distribution fitting. If no censoring vector was used for distribution ## fitting, then this field defaults to an empty array. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.BirnbaumSaundersDistribution} {@var{pd} =} BirnbaumSaundersDistribution (@var{beta}, @var{gamma}) ## @deftypefnx {prob.BirnbaumSaundersDistribution} {@var{pd} =} BirnbaumSaundersDistribution () ## ## Create a @code{prob.BirnbaumSaundersDistribution} object. ## ## @var{beta} and @var{gamma} are the distribution parameters, which the ## class help describes. Called with no arguments the parameters take their ## defaults, @var{beta} 1 and @var{gamma} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = BirnbaumSaundersDistribution (beta, gamma) if (nargin == 0) beta = 1; gamma = 1; endif checkparams (beta, gamma); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [beta, gamma]; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'Birnbaum-Saunders distribution'); endfunction function disp (this) __disp__ (this, 'Birnbaum-Saunders distribution'); endfunction function this = set.beta (this, beta) checkparams (beta, this.gamma); this.InputData = []; this.ParameterValues(1) = beta; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function beta = get.beta (this) beta = this.ParameterValues(1); endfunction function this = set.gamma (this, gamma) checkparams (this.beta, gamma); this.InputData = []; this.ParameterValues(2) = gamma; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function gamma = get.gamma (this) gamma = this.ParameterValues(2); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.BirnbaumSaundersDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.BirnbaumSaundersDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = bisacdf (x, this.beta, this.gamma); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= bisacdf (lx, this.beta, this.gamma); p(! (lb | ub)) /= diff (bisacdf ([lx, ux], this.beta, this.gamma)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BirnbaumSaundersDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = bisacdf (this.Truncation(1), this.beta, this.gamma); up = bisacdf (this.Truncation(2), this.beta, this.gamma); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = bisainv (np, this.beta, this.gamma); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = bisainv (p, this.beta, this.gamma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BirnbaumSaundersDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BirnbaumSaundersDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = bisastat (this.beta, this.gamma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BirnbaumSaundersDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = bisacdf ([lx, ux], this.beta, this.gamma); m = bisainv (sum (Fa_b) / 2, this.beta, this.gamma); else m = bisainv (0.5, this.beta, this.gamma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BirnbaumSaundersDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = bisalike ([this.beta, this.gamma], this.InputData.data, ... this.InputData.cens, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BirnbaumSaundersDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.BirnbaumSaundersDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the ## confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BirnbaumSaundersDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = bisapdf (x, this.beta, this.gamma); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (bisacdf ([lx, ux], this.beta, this.gamma)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BirnbaumSaundersDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.BirnbaumSaundersDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.BirnbaumSaundersDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BirnbaumSaundersDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.BirnbaumSaundersDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.BirnbaumSaundersDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.BirnbaumSaundersDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.BirnbaumSaundersDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.BirnbaumSaundersDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the Birnbaum-Saunders distribution, @qcode{@var{pnum} = 1} selects ## the parameter @qcode{beta} and @qcode{@var{pnum} = 2} selects the ## parameter @qcode{gamma}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BirnbaumSaundersDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.BirnbaumSaundersDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.BirnbaumSaundersDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.BirnbaumSaundersDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{bisarnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) sz = [varargin{:}]; ps = prod (sz); ## Get an estimate of how many more random numbers we need to randomly ## pick the appropriate size from lx = this.Truncation(1); ux = this.Truncation(2); ratio = 1 / diff (bisacdf ([lx, ux], this.beta, this.gamma)); nsize = fix (2 * ratio * ps); # times 2 to be on the safe side ## Generate the numbers and remove out-of-bound random samples r = bisarnd (this.beta, this.gamma, nsize, 1); r(r < lx | r > ux) = []; ## Randomly select the required size and reshape to requested dimensions idx = randperm (numel (r), ps); r = reshape (r(idx), sz); else r = bisarnd (this.beta, this.gamma, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BirnbaumSaundersDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BirnbaumSaundersDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, ## and upper limit, @var{upper}. If @var{pd} is fitted to data with ## @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BirnbaumSaundersDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = bisastat (this.beta, this.gamma); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) censor = []; else censor = varargin{2}; endif if (nargin < 4) freq = []; else freq = varargin{3}; endif if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else options = varargin{4}; endif ## Fit data [phat, pci] = bisafit (x, alpha, censor, freq, options); [~, acov] = bisalike (phat, x, censor, freq); ## Create fitted distribution object pd = prob.BirnbaumSaundersDistribution.makeFitted ... (phat, pci, acov, x, censor, freq); endfunction function pd = makeFitted (phat, pci, acov, x, censor, freq) beta = phat(1); gamma = phat(2); pd = prob.BirnbaumSaundersDistribution (beta, gamma); pd.ParameterCI = pci; pd.ParameterIsFixed = [false, false]; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', censor, 'freq', freq); endfunction endmethods endclassdef function checkparams (beta, gamma) if (! (isscalar (beta) && isnumeric (beta) && isreal (beta) && isfinite (beta) && beta > 0)) error ("BirnbaumSaundersDistribution: BETA must be a positive real scalar.") endif if (! (isscalar (gamma) && isnumeric (gamma) && isreal (gamma) && isfinite (gamma) && gamma > 0)) error ("BirnbaumSaundersDistribution: GAMMA must be a positive real scalar.") endif endfunction %!demo %! ## Generate a data set of 5000 random samples from a Birnbaum-Saunders %! ## distribution with parameters β = 1 and γ = 0.5. Fit a Birnbaum-Saunders %! ## distribution to this data and plot a PDF of the fitted distribution %! ## superimposed on a histogram of the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ('BirnbaumSaunders', 'beta', 1, 'gamma', 0.5) %! data = random (pd_fixed, 5000, 1); %! pd_fitted = fitdist (data, 'BirnbaumSaunders') %! plot (pd_fitted) %! msg = 'Fitted Birnbaum-Saunders distribution with beta = %0.2f and gamma = %0.2f'; %! title (sprintf (msg, pd_fitted.beta, pd_fitted.gamma)) ## Test output %!shared pd, t %! pd = prob.BirnbaumSaundersDistribution; %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0:5]), [0, 0.5, 0.7602, 0.8759, 0.9332, 0.9632], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0, 0.6687, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4, NaN]), [0.6585, 0.7602, 0.8759, 0.9332, NaN], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4, NaN]), [0, 0, 0.6687, 1, NaN], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [0, 0.4411, 0.7767, 1.2875, 2.2673, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.2293, 2.5073, 2.8567, 3.3210, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 0.7767, 1.2875, 2.2673, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.5073, 2.8567, 3.3210, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 1.4236, 1e-4); %!assert_equal (iqr (t), 0.8968, 1e-4); %!assert_equal (mean (pd), 1.5, eps); %!assert_equal (mean (t), 2.7723, 1e-4); %!assert_equal (median (pd), 1, 1e-4); %!assert_equal (median (t), 2.6711, 1e-4); %!assert_equal (pdf (pd, [0:5]), [0, 0.3989, 0.1648, 0.0788, 0.0405, 0.0216], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 0.9528, 0.4559, 0.2340, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1.5, NaN]), [0, 0.2497, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1.5, NaN]), [0, 0, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 1.5, eps); %!assert_equal (std (t), 0.5528, 1e-4); %!assert_equal (var (pd), 2.25, eps); %!assert_equal (var (t), 0.3056, 1e-4); %!test %! ## The profile over the first free parameter: 21 grid values, one row of %! ## OTHER per value, and the likelihood peaking at the fitted estimate. %! x = [1.2; 0.4; 3.1; 0.7; 2.5; 1.8; 0.3; 4.2; 1.1; 0.9; ... %! 2.2; 0.6; 1.5; 3.7; 0.8; 2.9; 1.3; 0.5; 2.0; 1.6]; %! pd = fitdist (x, 'BirnbaumSaunders'); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (size (param), [1, 21]); %! assert_equal (size (other), [21, 1]); %! assert_equal (proflik (pd), nlogL); %! [~, imax] = max (nlogL); %! assert_equal (abs (param(imax) - pd.ParameterValues(1)) <= param(2) - param(1), true); ## Test input validation ## 'prob.BirnbaumSaundersDistribution' constructor %!error ... %! prob.BirnbaumSaundersDistribution (0, 1) %!error ... %! prob.BirnbaumSaundersDistribution (Inf, 1) %!error ... %! prob.BirnbaumSaundersDistribution (i, 1) %!error ... %! prob.BirnbaumSaundersDistribution ('beta', 1) %!error ... %! prob.BirnbaumSaundersDistribution ([1, 2], 1) %!error ... %! prob.BirnbaumSaundersDistribution (NaN, 1) %!error ... %! prob.BirnbaumSaundersDistribution (1, 0) %!error ... %! prob.BirnbaumSaundersDistribution (1, -1) %!error ... %! prob.BirnbaumSaundersDistribution (1, Inf) %!error ... %! prob.BirnbaumSaundersDistribution (1, i) %!error ... %! prob.BirnbaumSaundersDistribution (1, 'beta') %!error ... %! prob.BirnbaumSaundersDistribution (1, [1, 2]) %!error ... %! prob.BirnbaumSaundersDistribution (1, NaN) ## 'cdf' method %!error ... %! cdf (prob.BirnbaumSaundersDistribution, 2, 'uper') %!error ... %! cdf (prob.BirnbaumSaundersDistribution, 2, 3) ## 'paramci' method %!shared x %! rand ('seed', 5); %! x = bisarnd (1, 1, [100, 1]); %!error ... %! paramci (prob.BirnbaumSaundersDistribution.fit (x), 'alpha') %!error ... %! paramci (prob.BirnbaumSaundersDistribution.fit (x), 'alpha', 0) %!error ... %! paramci (prob.BirnbaumSaundersDistribution.fit (x), 'alpha', 1) %!error ... %! paramci (prob.BirnbaumSaundersDistribution.fit (x), 'alpha', [0.5 2]) %!error ... %! paramci (prob.BirnbaumSaundersDistribution.fit (x), 'alpha', '') %!error ... %! paramci (prob.BirnbaumSaundersDistribution.fit (x), 'alpha', {0.05}) %!error ... %! paramci (prob.BirnbaumSaundersDistribution.fit (x), 'parameter', ... %! 'beta', 'alpha', {0.05}) %!error ... %! paramci (prob.BirnbaumSaundersDistribution.fit (x), ... %! 'parameter', {'beta', 'gamma', 'param'}) %!error ... %! paramci (prob.BirnbaumSaundersDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', {'beta', 'gamma', 'param'}) %!error ... %! paramci (prob.BirnbaumSaundersDistribution.fit (x), 'parameter', 'param') %!error ... %! paramci (prob.BirnbaumSaundersDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', 'param') %!error ... %! paramci (prob.BirnbaumSaundersDistribution.fit (x), 'NAME', 'value') %!error ... %! paramci (prob.BirnbaumSaundersDistribution.fit (x), 'alpha', 0.01, ... %! 'NAME', 'value') %!error ... %! paramci (prob.BirnbaumSaundersDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', 'beta', 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.BirnbaumSaundersDistribution, 'Parent') %!error ... %! plot (prob.BirnbaumSaundersDistribution, 'PlotType', 12) %!error ... %! plot (prob.BirnbaumSaundersDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.BirnbaumSaundersDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.BirnbaumSaundersDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.BirnbaumSaundersDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.BirnbaumSaundersDistribution, 'Discrete', {true}) %!error ... %! plot (prob.BirnbaumSaundersDistribution, 'Parent', 12) %!error ... %! plot (prob.BirnbaumSaundersDistribution, 'Parent', 'hax') %!error ... %! plot (prob.BirnbaumSaundersDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.BirnbaumSaundersDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.BirnbaumSaundersDistribution, 2) %!error ... %! proflik (prob.BirnbaumSaundersDistribution.fit (x), 3) %!error ... %! proflik (prob.BirnbaumSaundersDistribution.fit (x), [1, 2]) %!error ... %! proflik (prob.BirnbaumSaundersDistribution.fit (x), {1}) %!error ... %! proflik (prob.BirnbaumSaundersDistribution.fit (x), 1, ones (2)) %!error ... %! proflik (prob.BirnbaumSaundersDistribution.fit (x), 1, 'Display') %!error ... %! proflik (prob.BirnbaumSaundersDistribution.fit (x), 1, 'Display', 1) %!error ... %! proflik (prob.BirnbaumSaundersDistribution.fit (x), 1, 'Display', {1}) %!error ... %! proflik (prob.BirnbaumSaundersDistribution.fit (x), 1, 'Display', {'on'}) %!error ... %! proflik (prob.BirnbaumSaundersDistribution.fit (x), 1, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.BirnbaumSaundersDistribution.fit (x), 1, 'Display', 'onnn') %!error ... %! proflik (prob.BirnbaumSaundersDistribution.fit (x), 1, 'NAME', 'on') %!error ... %! proflik (prob.BirnbaumSaundersDistribution.fit (x), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.BirnbaumSaundersDistribution.fit (x), 1, {[1 2 3 4]}, 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.BirnbaumSaundersDistribution) %!error ... %! truncate (prob.BirnbaumSaundersDistribution, 2) %!error ... %! truncate (prob.BirnbaumSaundersDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.BirnbaumSaundersDistribution (1, 1); %! pd(2) = prob.BirnbaumSaundersDistribution (1, 3); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/BurrDistribution.m000066400000000000000000001270761527311104300271540ustar00rootroot00000000000000## Copyright (C) 2024-2025 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef BurrDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.BurrDistribution ## ## Burr probability distribution object. ## ## A @code{prob.BurrDistribution} object consists of parameters, a model ## description, and sample data for a Burr probability distribution. ## ## The Burr distribution is a continuous probability distribution that models ## a non-negative random variable, commonly used to model household income. ## It is defined by a scale parameter @var{alpha} and two shape parameters ## @var{c} and @var{k}. ## ## There are several ways to create a @code{prob.BurrDistribution} object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor ## @qcode{prob.BurrDistribution (@var{alpha}, @var{c}, @var{k})} to create a ## Burr distribution with fixed parameter values @var{alpha}, @var{c}, and ## @var{k}. ## @item Use the static method @qcode{prob.BurrDistribution.fit (@var{x}, ## @var{alpha}, @var{censor}, @var{freq}, @var{options})} to fit a ## distribution to the data in @var{x} using the same input arguments as the ## @code{burrfit} function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the Burr distribution can be found at ## @url{https://en.wikipedia.org/wiki/Burr_distribution} ## ## @seealso{fitdist, makedist, burrcdf, burrinv, burrpdf, burrrnd, burrfit, ## burrlike, burrstat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.BurrDistribution} {property} alpha ## ## Scale parameter ## ## A positive scalar value characterizing the scale of the Burr ## distribution. You can access the @qcode{alpha} property using dot name ## assignment. ## ## @end deftp alpha ## -*- texinfo -*- ## @deftp {prob.BurrDistribution} {property} c ## ## First shape parameter ## ## A positive scalar value characterizing the first shape parameter of the ## Burr distribution. You can access the @qcode{c} property using dot name ## assignment. ## ## @end deftp c ## -*- texinfo -*- ## @deftp {prob.BurrDistribution} {property} k ## ## Second shape parameter ## ## A positive scalar value characterizing the second shape parameter of the ## Burr distribution. You can access the @qcode{k} property using dot name ## assignment. ## ## @end deftp k endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.BurrDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Burr'; ## -*- texinfo -*- ## @deftp {prob.BurrDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 3; ## -*- texinfo -*- ## @deftp {prob.BurrDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{3*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'alpha', 'c', 'k'}; ## -*- texinfo -*- ## @deftp {prob.BurrDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{3*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Scale', '1st shape', '2nd shape'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = false; DistributionCode = 'burr'; ParameterRange = [realmin, realmin, realmin; Inf, Inf, Inf]; ParameterLogCI = [false, true, false]; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.BurrDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{3*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{alpha}, @qcode{c}, and ## @qcode{k} properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.BurrDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.BurrDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.BurrDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{3*3} numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter, and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only meaningful ## when the distribution was fitted to data. If the distribution object was ## created with fixed parameters, or a parameter of a fitted distribution is ## modified, then all elements of the variance-covariance are zero. This ## property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.BurrDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*3} logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.BurrDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: an empty array, since @qcode{prob.BurrDistribution} ## does not allow censoring. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.BurrDistribution} {@var{pd} =} BurrDistribution (@var{alpha}, @var{c}, @var{k}) ## @deftypefnx {prob.BurrDistribution} {@var{pd} =} BurrDistribution () ## ## Create a @code{prob.BurrDistribution} object. ## ## @var{alpha}, @var{c} and @var{k} are the distribution parameters, which ## the class help describes. Called with no arguments the parameters take ## their defaults, @var{alpha} 1, @var{c} 1 and @var{k} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = BurrDistribution (alpha, c, k) if (nargin == 0) alpha = 1; c = 1; k = 1; endif checkparams (alpha, c, k); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [alpha, c, k]; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'Burr distribution'); endfunction function disp (this) __disp__ (this, 'Burr distribution'); endfunction function this = set.alpha (this, alpha) checkparams (alpha, this.c, this.k); this.InputData = []; this.ParameterValues(1) = alpha; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function alpha = get.alpha (this) alpha = this.ParameterValues(1); endfunction function this = set.c (this, c) checkparams (this.alpha, c, this.k); this.InputData = []; this.ParameterValues(2) = c; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function c = get.c (this) c = this.ParameterValues(2); endfunction function this = set.k (this, k) checkparams (this.alpha, this.c, k); this.InputData = []; this.ParameterValues(3) = k; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function k = get.k (this) k = this.ParameterValues(3); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.BurrDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.BurrDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = burrcdf (x, this.alpha, this.c, this.k); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= burrcdf (lx, this.alpha, this.c, this.k); p(! (lb | ub)) /= diff (burrcdf ([lx, ux], this.alpha, this.c, this.k)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BurrDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = burrcdf (this.Truncation(1), this.alpha, this.c, this.k); up = burrcdf (this.Truncation(2), this.alpha, this.c, this.k); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = burrinv (np, this.alpha, this.c, this.k); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = burrinv (p, this.alpha, this.c, this.k); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BurrDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BurrDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = burrstat (this.alpha, this.c, this.k); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BurrDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = burrcdf ([lx, ux], this.alpha, this.c, this.k); m = burrinv (sum (Fa_b) / 2, this.alpha, this.c, this.k); else m = burrinv (0.5, this.alpha, this.c, this.k); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BurrDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = burrlike ([this.alpha, this.c, this.k], this.InputData.data, ... this.InputData.cens, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BurrDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.BurrDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the ## confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BurrDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = burrpdf (x, this.alpha, this.c, this.k); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (burrcdf ([lx, ux], this.alpha, this.c, this.k)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BurrDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.BurrDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.BurrDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BurrDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.BurrDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.BurrDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.BurrDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.BurrDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.BurrDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the Burr distribution, @qcode{@var{pnum} = 1} selects the parameter ## @qcode{alpha}, @qcode{@var{pnum} = 2} selects the parameter @qcode{c}, ## and @qcode{@var{pnum} = 3} selects the parameter @qcode{k}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BurrDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.BurrDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.BurrDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.BurrDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{burrrnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) sz = [varargin{:}]; ps = prod (sz); ## Get an estimate of how many more random numbers we need to randomly ## pick the appropriate size from lx = this.Truncation(1); ux = this.Truncation(2); ratio = 1 / diff (burrcdf ([lx, ux], this.alpha, this.c, this.k)); nsize = fix (2 * ratio * ps); # times 2 to be on the safe side ## Generate the numbers and remove out-of-bound random samples r = burrrnd (this.alpha, this.c, this.k, nsize, 1); r(r < lx | r > ux) = []; ## Randomly select the required size and reshape to requested dimensions idx = randperm (numel (r), ps); r = reshape (r(idx), sz); else r = burrrnd (this.alpha, this.c, this.k, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.BurrDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BurrDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, ## and upper limit, @var{upper}. If @var{pd} is fitted to data with ## @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.BurrDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = burrstat (this.alpha, this.c, this.k); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) censor = []; else censor = varargin{2}; endif if (nargin < 4) freq = []; else freq = varargin{3}; endif if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else options = varargin{4}; endif ## Fit data [phat, pci] = burrfit (x, alpha, censor, freq, options); [~, acov] = burrlike (phat, x, censor, freq); ## Create fitted distribution object pd = prob.BurrDistribution.makeFitted (phat, pci, acov, x, censor, freq); endfunction function pd = makeFitted (phat, pci, acov, x, censor, freq) alpha = phat(1); c = phat(2); k = phat(3); pd = prob.BurrDistribution (alpha, c, k); pd.ParameterCI = pci; pd.ParameterIsFixed = [false, false, false]; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', censor, 'freq', freq); endfunction endmethods endclassdef function checkparams (alpha, c, k) if (! (isscalar (alpha) && isnumeric (alpha) && isreal (alpha) && isfinite (alpha) && alpha > 0)) error ("BurrDistribution: ALPHA must be a positive real scalar.") endif if (! (isscalar (c) && isnumeric (c) && isreal (c) && isfinite (c) && c > 0)) error ("BurrDistribution: C must be a positive real scalar.") endif if (! (isscalar (k) && isnumeric (k) && isreal (k) && isfinite (k) && k > 0)) error ("BurrDistribution: K must be a positive real scalar.") endif endfunction %!demo %! ## Generate a data set of 5000 random samples from a Burr type XII %! ## distribution with parameters alpha = 1, c = 2, and k = 1. Fit a Burr type %! ## XII distribution to this data and plot a PDF of the fitted distribution %! ## superimposed on a histogram of the data %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ('Burr', 'alpha', 1, 'c', 2, 'k', 1) %! data = random (pd, 5000, 1); %! pd = fitdist (data, 'Burr') %! plot (pd) %! msg = strcat ("Fitted Burr type XII distribution with", ... %! " alpha = %0.2f, c = %0.2f, and k = %0.2f"); %! title (sprintf (msg, pd.alpha, pd.c, pd.k)) ## Test output %!shared pd, t %! pd = prob.BurrDistribution; %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0:5]), [0, 0.5, 0.6667, 0.75, 0.8, 0.8333], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0, 0.625, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4]), [0.6, 0.6667, 0.75, 0.8], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4]), [0, 0, 0.625, 1], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [0, 0.25, 0.6667, 1.5, 4, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.2609, 2.5714, 2.9474, 3.4118, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 0.6667, 1.5, 4, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.5714, 2.9474, 3.4118, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 2.6667, 1e-4); %!assert_equal (iqr (t), 0.9524, 1e-4); %!assert_equal (mean (pd), Inf); %!assert_equal (mean (t), 2.8312, 1e-4); %!assert_equal (median (pd), 1, 1e-4); %!assert_equal (median (t), 2.75, 1e-4); %!assert_equal (pdf (pd, [0:5]), [1, 0.25, 0.1111, 0.0625, 0.04, 0.0278], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 0.8333, 0.4687, 0.3, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1:4, NaN]), [0, 0.25, 0.1111, 0.0625, 0.04, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1:4, NaN]), [0, 0, 0.8333, 0.4687, 0.3, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), Inf); %!assert_equal (std (t), 0.5674, 1e-4); %!assert_equal (var (pd), Inf); %!assert_equal (var (t), 0.3220, 1e-4); %!test %! ## The profile over the first free parameter: 21 grid values, one row of %! ## OTHER per value, and the likelihood peaking at the fitted estimate. The %! ## sample is Burr, drawn from burrinv at fixed quantiles. %! x = [0.160128; 0.284747; 0.377964; 0.460566; 0.538816; 0.615882; ... %! 0.693889; 0.774597; 0.859727; 0.951190; 1.051310; 1.163160; ... %! 1.290990; 1.441150; 1.623690; 1.855920; 2.171240; 2.645750; ... %! 3.511880; 6.245000]; %! pd = fitdist (x, 'Burr'); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (size (param), [1, 21]); %! assert_equal (size (other), [21, 2]); %! assert_equal (proflik (pd), nlogL); %! [~, imax] = max (nlogL); %! assert_equal (abs (param(imax) - pd.ParameterValues(1)) <= param(2) - param(1), true); %!test %! ## The profiled-out parameters are passed to burrlike as frequencies, not as %! ## censoring: reading them as censoring marks every observation censored and %! ## the profile runs away as the second shape parameter goes to zero. %! x = [0.160128; 0.284747; 0.377964; 0.460566; 0.538816; 0.615882; ... %! 0.693889; 0.774597; 0.859727; 0.951190; 1.051310; 1.163160; ... %! 1.290990; 1.441150; 1.623690; 1.855920; 2.171240; 2.645750; ... %! 3.511880; 6.245000]; %! pd = fitdist (x, 'Burr'); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (max (nlogL) <= -burrlike (pd.ParameterValues, x), true); %! [~, imax] = max (nlogL); %! assert_equal (-burrlike ([param(imax), other(imax,:)], x), nlogL(imax), 1e-6); ## Test input validation ## 'prob.BurrDistribution' constructor %!error ... %! prob.BurrDistribution (0, 1, 1) %!error ... %! prob.BurrDistribution (-1, 1, 1) %!error ... %! prob.BurrDistribution (Inf, 1, 1) %!error ... %! prob.BurrDistribution (i, 1, 1) %!error ... %! prob.BurrDistribution ('a', 1, 1) %!error ... %! prob.BurrDistribution ([1, 2], 1, 1) %!error ... %! prob.BurrDistribution (NaN, 1, 1) %!error ... %! prob.BurrDistribution (1, 0, 1) %!error ... %! prob.BurrDistribution (1, -1, 1) %!error ... %! prob.BurrDistribution (1, Inf, 1) %!error ... %! prob.BurrDistribution (1, i, 1) %!error ... %! prob.BurrDistribution (1, 'a', 1) %!error ... %! prob.BurrDistribution (1, [1, 2], 1) %!error ... %! prob.BurrDistribution (1, NaN, 1) %!error ... %! prob.BurrDistribution (1, 1, 0) %!error ... %! prob.BurrDistribution (1, 1, -1) %!error ... %! prob.BurrDistribution (1, 1, Inf) %!error ... %! prob.BurrDistribution (1, 1, i) %!error ... %! prob.BurrDistribution (1, 1, 'a') %!error ... %! prob.BurrDistribution (1, 1, [1, 2]) %!error ... %! prob.BurrDistribution (1, 1, NaN) ## 'cdf' method %!error ... %! cdf (prob.BurrDistribution, 2, 'uper') %!error ... %! cdf (prob.BurrDistribution, 2, 3) ## 'paramci' method %!shared x %! rand ('seed', 4); %! x = burrrnd (1, 1, 1, [1, 100]); %!error ... %! paramci (prob.BurrDistribution.fit (x), 'alpha') %!error ... %! paramci (prob.BurrDistribution.fit (x), 'alpha', 0) %!error ... %! paramci (prob.BurrDistribution.fit (x), 'alpha', 1) %!error ... %! paramci (prob.BurrDistribution.fit (x), 'alpha', [0.5 2]) %!error ... %! paramci (prob.BurrDistribution.fit (x), 'alpha', '') %!error ... %! paramci (prob.BurrDistribution.fit (x), 'alpha', {0.05}) %!error ... %! paramci (prob.BurrDistribution.fit (x), 'parameter', 'c', 'alpha', {0.05}) %!error ... %! paramci (prob.BurrDistribution.fit (x), 'parameter', {'alpha', 'c', 'k', 'param'}) %!error ... %! paramci (prob.BurrDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', {'alpha', 'c', 'k', 'param'}) %!error ... %! paramci (prob.BurrDistribution.fit (x), 'parameter', 'param') %!error ... %! paramci (prob.BurrDistribution.fit (x), 'alpha', 0.01, 'parameter', 'param') %!error ... %! paramci (prob.BurrDistribution.fit (x), 'NAME', 'value') %!error ... %! paramci (prob.BurrDistribution.fit (x), 'alpha', 0.01, 'NAME', 'value') %!error ... %! paramci (prob.BurrDistribution.fit (x), 'alpha', 0.01, 'parameter', 'c', ... %! 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.BurrDistribution, 'Parent') %!error ... %! plot (prob.BurrDistribution, 'PlotType', 12) %!error ... %! plot (prob.BurrDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.BurrDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.BurrDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.BurrDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.BurrDistribution, 'Discrete', {true}) %!error ... %! plot (prob.BurrDistribution, 'Parent', 12) %!error ... %! plot (prob.BurrDistribution, 'Parent', 'hax') %!error ... %! plot (prob.BurrDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.BurrDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.BurrDistribution, 2) %!error ... %! proflik (prob.BurrDistribution.fit (x), 4) %!error ... %! proflik (prob.BurrDistribution.fit (x), [1, 2]) %!error ... %! proflik (prob.BurrDistribution.fit (x), {1}) %!error ... %! proflik (prob.BurrDistribution.fit (x), 1, ones (2)) %!error ... %! proflik (prob.BurrDistribution.fit (x), 1, 'Display') %!error ... %! proflik (prob.BurrDistribution.fit (x), 1, 'Display', 1) %!error ... %! proflik (prob.BurrDistribution.fit (x), 1, 'Display', {1}) %!error ... %! proflik (prob.BurrDistribution.fit (x), 1, 'Display', {'on'}) %!error ... %! proflik (prob.BurrDistribution.fit (x), 1, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.BurrDistribution.fit (x), 1, 'Display', 'onnn') %!error ... %! proflik (prob.BurrDistribution.fit (x), 1, 'NAME', 'on') %!error ... %! proflik (prob.BurrDistribution.fit (x), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.BurrDistribution.fit (x), 1, {[1 2 3 4]}, 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.BurrDistribution) %!error ... %! truncate (prob.BurrDistribution, 2) %!error ... %! truncate (prob.BurrDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.BurrDistribution (1, 1, 1); %! pd(2) = prob.BurrDistribution (1, 3, 1); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/ExponentialDistribution.m000066400000000000000000001157701527311104300305260ustar00rootroot00000000000000## Copyright (C) 2024-2025 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ExponentialDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.ExponentialDistribution ## ## Exponential probability distribution object. ## ## A @code{prob.ExponentialDistribution} object consists of parameters, a ## model description, and sample data for a exponential probability ## distribution. ## ## The exponential distribution is a continuous probability distribution with ## mean parameter @var{mu} that models the time between events in a Poisson ## process. ## ## There are several ways to create a @code{prob.ExponentialDistribution} ## object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor @qcode{prob.ExponentialDistribution (@var{mu})} ## to create a exponential distribution with fixed parameter value @var{mu}. ## @item Use the static method @qcode{prob.ExponentialDistribution.fit ## (@var{x}, @var{alpha}, @var{censor}, @var{freq}, @var{options})} to fit a ## distribution to the data in @var{x} using the same input arguments as the ## @code{expfit} function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the exponential distribution can be found at ## @url{https://en.wikipedia.org/wiki/Exponential_distribution} ## ## @seealso{fitdist, makedist, expcdf, expinv, exppdf, exprnd, expfit, ## explike, expstat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.ExponentialDistribution} {property} mu ## ## Mean parameter ## ## A positive scalar value characterizing the mean of the ## exponential distribution. You can access the @qcode{mu} ## property using dot name assignment. ## ## @end deftp mu endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.ExponentialDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Exponential'; ## -*- texinfo -*- ## @deftp {prob.ExponentialDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 1; ## -*- texinfo -*- ## @deftp {prob.ExponentialDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{1*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'mu'}; ## -*- texinfo -*- ## @deftp {prob.ExponentialDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{1*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Mean'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = true; DistributionCode = 'exp'; ParameterRange = [realmin; Inf]; ParameterLogCI = true; endproperties properties(GetAccess = public , SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.ExponentialDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{1*1} numeric vector containing the value of the distribution ## parameter. This property is read-only. You can change the distribution ## parameter by assigning a new value to the @qcode{mu} ## property. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.ExponentialDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.ExponentialDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.ExponentialDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A scalar numeric value containing the variance-covariance of the ## parameter estimate. The covariance matrix is only meaningful ## when the distribution was fitted to data. If the distribution object was ## created with fixed parameters, or a parameter of a fitted distribution is ## modified, then the variance-covariance is zero. This ## property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.ExponentialDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*1} logical vector specifying whether the parameter is fixed or ## estimated. @qcode{true} value corresponds to fixed parameter, ## @qcode{false} value corresponds to parameter estimate. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.ExponentialDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: a numeric vector of logical values indicating ## censoring information corresponding to the elements of the data used for ## distribution fitting. If no censoring vector was used for distribution ## fitting, then this field defaults to an empty array. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.ExponentialDistribution} {@var{pd} =} ExponentialDistribution (@var{mu}) ## @deftypefnx {prob.ExponentialDistribution} {@var{pd} =} ExponentialDistribution () ## ## Create a @code{prob.ExponentialDistribution} object. ## ## @var{mu} is the distribution parameter, which the class help describes. ## Called with no arguments the parameter takes its default, @var{mu} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = ExponentialDistribution (mu) if (nargin == 0) mu = 1; endif checkparams (mu); this.InputData = []; this.IsTruncated = false; this.ParameterValues = mu; this.ParameterIsFixed = true; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'exponential distribution'); endfunction function disp (this) __disp__ (this, 'exponential distribution'); endfunction function this = set.mu (this, mu) checkparams (mu); this.InputData = []; this.ParameterValues(1) = mu; this.ParameterIsFixed = true; this.ParameterCovariance = zeros (this.NumParameters); endfunction function mu = get.mu (this) mu = this.ParameterValues(1); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.ExponentialDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.ExponentialDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = expcdf (x, this.mu); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= expcdf (lx, this.mu); p(! (lb | ub)) /= diff (expcdf ([lx, ux], this.mu)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExponentialDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = expcdf (this.Truncation(1), this.mu); up = expcdf (this.Truncation(2), this.mu); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = expinv (np, this.mu); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = expinv (p, this.mu); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExponentialDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExponentialDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = expstat (this.mu); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExponentialDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = expcdf ([lx, ux], this.mu); m = expinv (sum (Fa_b) / 2, this.mu); else m = this.mu .* log (2); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExponentialDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = explike (this.mu, this.InputData.data, ... this.InputData.cens, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExponentialDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.ExponentialDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the ## confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExponentialDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = exppdf (x, this.mu); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (expcdf ([lx, ux], this.mu)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExponentialDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.ExponentialDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.ExponentialDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExponentialDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.ExponentialDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.ExponentialDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.ExponentialDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.ExponentialDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.ExponentialDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the exponential distribution, @qcode{@var{pnum} = 1} selects the ## parameter @qcode{mu}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExponentialDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.ExponentialDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.ExponentialDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.ExponentialDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{betarnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) sz = [varargin{:}]; ps = prod (sz); ## Get an estimate of how many more random numbers we need to randomly ## pick the appropriate size from lx = this.Truncation(1); ux = this.Truncation(2); ratio = 1 / diff (expcdf ([lx, ux], this.mu)); nsize = fix (2 * ratio * ps); # times 2 to be on the safe side ## Generate the numbers and remove out-of-bound random samples r = exprnd (this.mu, nsize, 1); r(r < lx | r > ux) = []; ## Randomly select the required size and reshape to requested dimensions idx = randperm (numel (r), ps); r = reshape (r(idx), sz); else r = exprnd (this.mu, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExponentialDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExponentialDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, ## and upper limit, @var{upper}. If @var{pd} is fitted to data with ## @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif ## Check boundaries and constrain within support [0, Inf) lower(lower < 0) = 0; this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = true; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExponentialDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = expstat (this.mu); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) censor = []; else censor = varargin{2}; endif if (nargin < 4) freq = []; else freq = varargin{3}; endif ## Fit data [phat, pci] = expfit (x, alpha, censor, freq); [~, acov] = explike (phat, x, censor, freq); ## Create fitted distribution object pd = prob.ExponentialDistribution.makeFitted ... (phat, pci, acov, x, censor, freq); endfunction function pd = makeFitted (phat, pci, acov, x, censor, freq) mu = phat(1); pd = prob.ExponentialDistribution (mu); pd.ParameterCI = pci; pd.ParameterIsFixed = false; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', censor, 'freq', freq); endfunction endmethods endclassdef function checkparams (mu) if (! (isscalar (mu) && isnumeric (mu) && isreal (mu) && isfinite (mu) && mu > 0)) error ("ExponentialDistribution: MU must be a positive real scalar.") endif endfunction ## Test output %!shared pd, t %! pd = prob.ExponentialDistribution (1); %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0:5]), [0, 0.6321, 0.8647, 0.9502, 0.9817, 0.9933], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0, 0.7311, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4]), [0.7769, 0.8647, 0.9502, 0.9817], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4]), [0, 0, 0.7311, 1], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [0, 0.2231, 0.5108, 0.9163, 1.6094, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.1899, 2.4244, 2.7315, 3.1768, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 0.5108, 0.9163, 1.6094, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.4244, 2.7315, 3.1768, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 1.0986, 1e-4); %!assert_equal (iqr (t), 0.8020, 1e-4); %!assert_equal (mean (pd), 1); %!assert_equal (mean (t), 2.6870, 1e-4); %!assert_equal (median (pd), 0.6931, 1e-4); %!assert_equal (median (t), 2.5662, 1e-4); %!assert_equal (pdf (pd, [0:5]), [1, 0.3679, 0.1353, 0.0498, 0.0183, 0.0067], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 1.1565, 0.4255, 0.1565, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1:4, NaN]), [0, 0.3679, 0.1353, 0.0498, 0.0183, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1:4, NaN]), [0, 0, 1.1565, 0.4255, 0.1565, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 1); %!assert_equal (std (t), 0.5253, 1e-4); %!assert_equal (var (pd), 1); %!assert_equal (var (t), 0.2759, 1e-4); %!test %! ## The default grid takes 101 values over the 98% confidence interval when %! ## the selected parameter is the only one estimated. Verified against MATLAB. %! x = [1.2; 0.4; 3.1; 0.7; 2.5; 1.8; 0.3; 4.2; 1.1; 0.9; ... %! 2.2; 0.6; 1.5; 3.7; 0.8; 2.9; 1.3; 0.5; 2.0; 1.6]; %! [nlogL, param, other] = proflik (prob.ExponentialDistribution.fit (x), 1); %! assert_equal (size (param), [1, 101]); %! assert_equal ([param(1), param(end)], [1.0457, 3.0048], 1e-4); %! assert_equal (size (other), [101, 0]); %!test %! ## PNUM defaults to the first parameter that is not fixed. %! x = [1.2; 0.4; 3.1; 0.7; 2.5; 1.8; 0.3; 4.2; 1.1; 0.9; ... %! 2.2; 0.6; 1.5; 3.7; 0.8; 2.9; 1.3; 0.5; 2.0; 1.6]; %! pdf = prob.ExponentialDistribution.fit (x); %! assert_equal (proflik (pdf), proflik (pdf, 1)); ## Test input validation ## 'prob.ExponentialDistribution' constructor %!error ... %! prob.ExponentialDistribution (0) %!error ... %! prob.ExponentialDistribution (-1) %!error ... %! prob.ExponentialDistribution (Inf) %!error ... %! prob.ExponentialDistribution (i) %!error ... %! prob.ExponentialDistribution ('a') %!error ... %! prob.ExponentialDistribution ([1, 2]) %!error ... %! prob.ExponentialDistribution (NaN) ## 'cdf' method %!error ... %! cdf (prob.ExponentialDistribution, 2, 'uper') %!error ... %! cdf (prob.ExponentialDistribution, 2, 3) ## 'paramci' method %!shared x %! x = exprnd (1, [100, 1]); %!error ... %! paramci (prob.ExponentialDistribution.fit (x), 'alpha') %!error ... %! paramci (prob.ExponentialDistribution.fit (x), 'alpha', 0) %!error ... %! paramci (prob.ExponentialDistribution.fit (x), 'alpha', 1) %!error ... %! paramci (prob.ExponentialDistribution.fit (x), 'alpha', [0.5 2]) %!error ... %! paramci (prob.ExponentialDistribution.fit (x), 'alpha', '') %!error ... %! paramci (prob.ExponentialDistribution.fit (x), 'alpha', {0.05}) %!error ... %! paramci (prob.ExponentialDistribution.fit (x), 'parameter', 'mu', ... %! 'alpha', {0.05}) %!error ... %! paramci (prob.ExponentialDistribution.fit (x), 'parameter', {'mu', 'param'}) %!error ... %! paramci (prob.ExponentialDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', {'mu', 'param'}) %!error ... %! paramci (prob.ExponentialDistribution.fit (x), 'parameter', 'param') %!error ... %! paramci (prob.ExponentialDistribution.fit (x), 'alpha', 0.01, 'parameter', 'parm') %!error ... %! paramci (prob.ExponentialDistribution.fit (x), 'NAME', 'value') %!error ... %! paramci (prob.ExponentialDistribution.fit (x), 'alpha', 0.01, 'NAME', 'value') %!error ... %! paramci (prob.ExponentialDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', 'mu', 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.ExponentialDistribution, 'Parent') %!error ... %! plot (prob.ExponentialDistribution, 'PlotType', 12) %!error ... %! plot (prob.ExponentialDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.ExponentialDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.ExponentialDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.ExponentialDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.ExponentialDistribution, 'Discrete', {true}) %!error ... %! plot (prob.ExponentialDistribution, 'Parent', 12) %!error ... %! plot (prob.ExponentialDistribution, 'Parent', 'hax') %!error ... %! plot (prob.ExponentialDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.ExponentialDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.ExponentialDistribution, 2) %!error ... %! proflik (prob.ExponentialDistribution.fit (x), 3) %!error ... %! proflik (prob.ExponentialDistribution.fit (x), [1, 2]) %!error ... %! proflik (prob.ExponentialDistribution.fit (x), {1}) %!error ... %! proflik (prob.ExponentialDistribution.fit (x), 1, ones (2)) %!error ... %! proflik (prob.ExponentialDistribution.fit (x), 1, 'Display') %!error ... %! proflik (prob.ExponentialDistribution.fit (x), 1, 'Display', 1) %!error ... %! proflik (prob.ExponentialDistribution.fit (x), 1, 'Display', {1}) %!error ... %! proflik (prob.ExponentialDistribution.fit (x), 1, 'Display', {'on'}) %!error ... %! proflik (prob.ExponentialDistribution.fit (x), 1, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.ExponentialDistribution.fit (x), 1, 'Display', 'onnn') %!error ... %! proflik (prob.ExponentialDistribution.fit (x), 1, 'NAME', 'on') %!error ... %! proflik (prob.ExponentialDistribution.fit (x), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.ExponentialDistribution.fit (x), 1, {[1 2 3 4]}, 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.ExponentialDistribution) %!error ... %! truncate (prob.ExponentialDistribution, 2) %!error ... %! truncate (prob.ExponentialDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.ExponentialDistribution (1); %! pd(2) = prob.ExponentialDistribution (3); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!test %! ## negloglik returns the (positive) negative log-likelihood. %! xdat = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! pdfit = prob.ExponentialDistribution.fit (xdat'); %! assert_equal (negloglik (pdfit), -sum (log (pdf (pdfit, xdat'))), 1e-9); %! assert_equal (negloglik (pdfit) > 0, true); %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/ExtremeValueDistribution.m000066400000000000000000001220321527311104300306330ustar00rootroot00000000000000## Copyright (C) 2024-2025 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ExtremeValueDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.ExtremeValueDistribution ## ## Extreme value probability distribution object. ## ## A @code{prob.ExtremeValueDistribution} object consists of parameters, a ## model description, and sample data for an extreme value probability ## distribution. ## ## The extreme value distribution is also known as the Gumbel distribution for ## maxima, and it is a limiting distribution for the maximum of a large number ## of samples from a continuous distribution. It is defined by location ## parameter @var{mu} and scale parameter @var{sigma}. ## ## There are several ways to create a @code{prob.ExtremeValueDistribution} ## object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with specified parameter values using the ## @code{makedist} function. ## @item Use the constructor @qcode{prob.ExtremeValueDistribution (@var{mu}, ## @var{sigma})} to create an extreme value distribution with specified ## parameter values. ## @item Use the static method @qcode{prob.ExtremeValueDistribution.fit ## (@var{x}, @var{alpha}, @var{censor}, @var{freq}, @var{options})} to fit a ## distribution to the data in @var{x} using the same input arguments as the ## @code{evfit} function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the ## constructor and the aforementioned static method. ## ## Further information about the Gumbel distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gumbel_distribution} ## ## @seealso{fitdist, makedist, evcdf, evinv, evpdf, evrnd, evfit, ## evlike, evstat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.ExtremeValueDistribution} {property} mu ## ## Location parameter ## ## A scalar value characterizing the location of the ## extreme value distribution. You can access the @qcode{mu} ## property using dot name assignment. ## ## @end deftp mu ## -*- texinfo -*- ## @deftp {prob.ExtremeValueDistribution} {property} sigma ## ## Scale parameter ## ## A positive scalar value characterizing the scale of the ## extreme value distribution. You can access the @qcode{sigma} ## property using dot name assignment. ## ## @end deftp sigma endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.ExtremeValueDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Extreme Value'; ## -*- texinfo -*- ## @deftp {prob.ExtremeValueDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 2; ## -*- texinfo -*- ## @deftp {prob.ExtremeValueDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'mu', 'sigma'}; ## -*- texinfo -*- ## @deftp {prob.ExtremeValueDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Location', 'Scale'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = true; DistributionCode = 'ev'; ParameterRange = [-Inf, realmin; Inf, Inf]; ParameterLogCI = [true, true]; endproperties properties(GetAccess = public , SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.ExtremeValueDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{2*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{mu} and @qcode{sigma} ## properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.ExtremeValueDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.ExtremeValueDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.ExtremeValueDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{2*2} numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter, and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only meaningful ## when the distribution was fitted to data. If the distribution object was ## created with fixed parameters, or a parameter of a fitted distribution is ## modified, then all elements of the variance-covariance are zero. This ## property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.ExtremeValueDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*2} logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.ExtremeValueDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: a numeric vector of logical values indicating ## censoring information corresponding to the elements of the data used for ## distribution fitting. If no censoring vector was used for distribution ## fitting, then this field defaults to an empty array. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.ExtremeValueDistribution} {@var{pd} =} ExtremeValueDistribution (@var{mu}, @var{sigma}) ## @deftypefnx {prob.ExtremeValueDistribution} {@var{pd} =} ExtremeValueDistribution () ## ## Create a @code{prob.ExtremeValueDistribution} object. ## ## @var{mu} and @var{sigma} are the distribution parameters, which the class ## help describes. Called with no arguments the parameters take their ## defaults, @var{mu} 0 and @var{sigma} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = ExtremeValueDistribution (mu, sigma) if (nargin == 0) mu = 0; sigma = 1; endif checkparams (mu, sigma); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [mu, sigma]; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'extreme value distribution'); endfunction function disp (this) __disp__ (this, 'extreme value distribution'); endfunction function this = set.mu (this, mu) checkparams (mu, this.sigma); this.InputData = []; this.ParameterValues(1) = mu; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function mu = get.mu (this) mu = this.ParameterValues(1); endfunction function this = set.sigma (this, sigma) checkparams (this.mu, sigma); this.InputData = []; this.ParameterValues(2) = sigma; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function sigma = get.sigma (this) sigma = this.ParameterValues(2); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.ExtremeValueDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.ExtremeValueDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = evcdf (x, this.mu, this.sigma); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= evcdf (lx, this.mu, this.sigma); p(! (lb | ub)) /= diff (evcdf ([lx, ux], this.mu, this.sigma)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExtremeValueDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = evcdf (this.Truncation(1), this.mu, this.sigma); up = evcdf (this.Truncation(2), this.mu, this.sigma); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = evinv (np, this.mu, this.sigma); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = evinv (p, this.mu, this.sigma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExtremeValueDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExtremeValueDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = evstat (this.mu, this.sigma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExtremeValueDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = evcdf ([lx, ux], this.mu, this.sigma); m = evinv (sum (Fa_b) / 2, this.mu, this.sigma); else m = evinv (0.5, this.mu, this.sigma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExtremeValueDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = evlike ([this.mu, this.sigma], this.InputData.data, ... this.InputData.cens, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExtremeValueDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.ExtremeValueDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the ## confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExtremeValueDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = evpdf (x, this.mu, this.sigma); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (evcdf ([lx, ux], this.mu, this.sigma)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExtremeValueDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.ExtremeValueDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.ExtremeValueDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExtremeValueDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.ExtremeValueDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.ExtremeValueDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.ExtremeValueDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.ExtremeValueDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.ExtremeValueDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the extreme value distribution, @qcode{@var{pnum} = 1} selects the ## parameter @qcode{mu} and @qcode{@var{pnum} = 2} selects the parameter ## @qcode{sigma}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExtremeValueDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.ExtremeValueDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.ExtremeValueDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.ExtremeValueDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{betarnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) sz = [varargin{:}]; ps = prod (sz); ## Get an estimate of how many more random numbers we need to randomly ## pick the appropriate size from lx = this.Truncation(1); ux = this.Truncation(2); ratio = 1 / diff (evcdf ([lx, ux], this.mu, this.sigma)); nsize = fix (2 * ratio * ps); # times 2 to be on the safe side ## Generate the numbers and remove out-of-bound random samples r = evrnd (this.mu, this.sigma, nsize, 1); r(r < lx | r > ux) = []; ## Randomly select the required size and reshape to requested dimensions idx = randperm (numel (r), ps); r = reshape (r(idx), sz); else r = evrnd (this.mu, this.sigma, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExtremeValueDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExtremeValueDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, ## and upper limit, @var{upper}. If @var{pd} is fitted to data with ## @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.ExtremeValueDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = evstat (this.mu, this.sigma); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) censor = []; else censor = varargin{2}; endif if (nargin < 4) freq = []; else freq = varargin{3}; endif if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else options = varargin{4}; endif ## Fit data [phat, pci] = evfit (x, alpha, censor, freq, options); [~, acov] = evlike (phat, x, censor, freq); ## Create fitted distribution object pd = prob.ExtremeValueDistribution.makeFitted ... (phat, pci, acov, x, censor, freq); endfunction function pd = makeFitted (phat, pci, acov, x, censor, freq) mu = phat(1); sigma = phat(2); pd = prob.ExtremeValueDistribution (mu, sigma); pd.ParameterCI = pci; pd.ParameterIsFixed = [false, false]; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', censor, 'freq', freq); endfunction endmethods endclassdef function checkparams (mu, sigma) if (! (isscalar (mu) && isnumeric (mu) && isreal (mu) && isfinite (mu))) error ("ExtremeValueDistribution: MU must be a real scalar.") endif if (! (isscalar (sigma) && isnumeric (sigma) && isreal (sigma) && isfinite (sigma) && sigma > 0)) error ("ExtremeValueDistribution: SIGMA must be a positive real scalar.") endif endfunction ## Test output %!shared pd, t %! pd = prob.ExtremeValueDistribution (0, 1); %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0:5]), [0.6321, 0.9340, 0.9994, 1, 1, 1], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0, 1, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4]), [0.9887, 0.9994, 1, 1], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4]), [0, 0, 1, 1], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [-Inf, -1.4999, -0.6717, -0.0874, 0.4759, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.0298, 2.0668, 2.1169, 2.1971, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, -0.6717, -0.0874, 0.4759, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.0668, 2.1169, 2.1971, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 1.5725, 1e-4); %!assert_equal (iqr (t), 0.1338, 1e-4); %!assert_equal (mean (pd), -0.5772, 1e-4); %!assert_equal (mean (t), 2.1206, 1e-4); %!assert_equal (median (pd), -0.3665, 1e-4); %!assert_equal (median (t), 2.0897, 1e-4); %!assert_equal (pdf (pd, [0:5]), [0.3679, 0.1794, 0.0046, 0, 0, 0], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 7.3891, 0.0001, 0, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1:4, NaN]), [0.2546, 0.1794, 0.0046, 0, 0, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1:4, NaN]), [0, 0, 7.3891, 0.0001, 0, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 1.2825, 1e-4); %!assert_equal (std (t), 0.1091, 1e-4); %!assert_equal (var (pd), 1.6449, 1e-4); %!assert_equal (var (t), 0.0119, 1e-4); %!test %! ## The profile over the first free parameter: 21 grid values, one row of %! ## OTHER per value, and the likelihood peaking at the fitted estimate. %! x = [0.3; -1.2; 0.8; 1.5; -0.4; 0.2; -0.9; 1.1; 0.6; -0.3; ... %! 1.8; -1.5; 0.4; 0.9; -0.7; 1.2; -0.2; 0.5; -1.1; 0.7]; %! pd = fitdist (x, 'ExtremeValue'); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (size (param), [1, 21]); %! assert_equal (size (other), [21, 1]); %! assert_equal (proflik (pd), nlogL); %! [~, imax] = max (nlogL); %! assert_equal (abs (param(imax) - pd.ParameterValues(1)) <= param(2) - param(1), true); ## Test input validation ## 'prob.ExtremeValueDistribution' constructor %!error ... %! prob.ExtremeValueDistribution (Inf, 1) %!error ... %! prob.ExtremeValueDistribution (i, 1) %!error ... %! prob.ExtremeValueDistribution ('a', 1) %!error ... %! prob.ExtremeValueDistribution ([1, 2], 1) %!error ... %! prob.ExtremeValueDistribution (NaN, 1) %!error ... %! prob.ExtremeValueDistribution (1, 0) %!error ... %! prob.ExtremeValueDistribution (1, -1) %!error ... %! prob.ExtremeValueDistribution (1, Inf) %!error ... %! prob.ExtremeValueDistribution (1, i) %!error ... %! prob.ExtremeValueDistribution (1, 'a') %!error ... %! prob.ExtremeValueDistribution (1, [1, 2]) %!error ... %! prob.ExtremeValueDistribution (1, NaN) ## 'cdf' method %!error ... %! cdf (prob.ExtremeValueDistribution, 2, 'uper') %!error ... %! cdf (prob.ExtremeValueDistribution, 2, 3) ## 'paramci' method %!shared x %! rand ('seed', 1); %! x = evrnd (1, 1, [1000, 1]); %!error ... %! paramci (prob.ExtremeValueDistribution.fit (x), 'alpha') %!error ... %! paramci (prob.ExtremeValueDistribution.fit (x), 'alpha', 0) %!error ... %! paramci (prob.ExtremeValueDistribution.fit (x), 'alpha', 1) %!error ... %! paramci (prob.ExtremeValueDistribution.fit (x), 'alpha', [0.5 2]) %!error ... %! paramci (prob.ExtremeValueDistribution.fit (x), 'alpha', '') %!error ... %! paramci (prob.ExtremeValueDistribution.fit (x), 'alpha', {0.05}) %!error ... %! paramci (prob.ExtremeValueDistribution.fit (x), ... %! 'parameter', 'mu', 'alpha', {0.05}) %!error ... %! paramci (prob.ExtremeValueDistribution.fit (x), ... %! 'parameter', {'mu', 'sigma', 'param'}) %!error ... %! paramci (prob.ExtremeValueDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', {'mu', 'sigma', 'param'}) %!error ... %! paramci (prob.ExtremeValueDistribution.fit (x), 'parameter', 'param') %!error ... %! paramci (prob.ExtremeValueDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', 'param') %!error ... %! paramci (prob.ExtremeValueDistribution.fit (x), 'NAME', 'value') %!error ... %! paramci (prob.ExtremeValueDistribution.fit (x), 'alpha', 0.01, 'NAME', 'value') %!error ... %! paramci (prob.ExtremeValueDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', 'mu', 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.ExtremeValueDistribution, 'Parent') %!error ... %! plot (prob.ExtremeValueDistribution, 'PlotType', 12) %!error ... %! plot (prob.ExtremeValueDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.ExtremeValueDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.ExtremeValueDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.ExtremeValueDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.ExtremeValueDistribution, 'Discrete', {true}) %!error ... %! plot (prob.ExtremeValueDistribution, 'Parent', 12) %!error ... %! plot (prob.ExtremeValueDistribution, 'Parent', 'hax') %!error ... %! plot (prob.ExtremeValueDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.ExtremeValueDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.ExtremeValueDistribution, 2) %!error ... %! proflik (prob.ExtremeValueDistribution.fit (x), 3) %!error ... %! proflik (prob.ExtremeValueDistribution.fit (x), [1, 2]) %!error ... %! proflik (prob.ExtremeValueDistribution.fit (x), {1}) %!error ... %! proflik (prob.ExtremeValueDistribution.fit (x), 1, ones (2)) %!error ... %! proflik (prob.ExtremeValueDistribution.fit (x), 1, 'Display') %!error ... %! proflik (prob.ExtremeValueDistribution.fit (x), 1, 'Display', 1) %!error ... %! proflik (prob.ExtremeValueDistribution.fit (x), 1, 'Display', {1}) %!error ... %! proflik (prob.ExtremeValueDistribution.fit (x), 1, 'Display', {'on'}) %!error ... %! proflik (prob.ExtremeValueDistribution.fit (x), 1, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.ExtremeValueDistribution.fit (x), 1, 'Display', 'onnn') %!error ... %! proflik (prob.ExtremeValueDistribution.fit (x), 1, 'NAME', 'on') %!error ... %! proflik (prob.ExtremeValueDistribution.fit (x), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.ExtremeValueDistribution.fit (x), 1, {[1 2 3 4]}, 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.ExtremeValueDistribution) %!error ... %! truncate (prob.ExtremeValueDistribution, 2) %!error ... %! truncate (prob.ExtremeValueDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.ExtremeValueDistribution (1, 1); %! pd(2) = prob.ExtremeValueDistribution (1, 3); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/GammaDistribution.m000066400000000000000000001202221527311104300272460ustar00rootroot00000000000000## Copyright (C) 2024-2025 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef GammaDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.GammaDistribution ## ## Gamma probability distribution object. ## ## A @code{prob.GammaDistribution} object consists of parameters, a model ## description, and sample data for a gamma probability distribution. ## ## The gamma distribution is a continuous probability distribution that models ## the time to failure of a process. It is defined by shape parameter @var{a} ## and scale parameter @var{b}. ## ## There are several ways to create a @code{prob.GammaDistribution} object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor @qcode{prob.GammaDistribution (@var{a}, @var{b})} ## to create a gamma distribution with fixed parameter values @var{a} and ## @var{b}. ## @item Use the static method @qcode{prob.GammaDistribution.fit (@var{x}, ## @var{alpha}, @var{censor}, @var{freq}, @var{options})} to fit a ## distribution to the data in @var{x} using the same input arguments as the ## @code{gamfit} function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the gamma distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gamma_distribution} ## ## @seealso{fitdist, makedist, gamcdf, gaminv, gampdf, gamrnd, gamfit, ## gamlike, gamstat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.GammaDistribution} {property} a ## ## Shape parameter ## ## A positive scalar value characterizing the shape of the ## gamma distribution. You can access the @qcode{a} ## property using dot name assignment. ## ## @end deftp a ## -*- texinfo -*- ## @deftp {prob.GammaDistribution} {property} b ## ## Scale parameter ## ## A positive scalar value characterizing the scale of the ## gamma distribution. You can access the @qcode{b} ## property using dot name assignment. ## ## @end deftp b endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.GammaDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Gamma'; ## -*- texinfo -*- ## @deftp {prob.GammaDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 2; ## -*- texinfo -*- ## @deftp {prob.GammaDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'a', 'b'}; ## -*- texinfo -*- ## @deftp {prob.GammaDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Shape', 'Scale'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = true; DistributionCode = 'gam'; ParameterRange = [realmin, realmin; Inf, Inf]; ParameterLogCI = [true, true]; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.GammaDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{2*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{a} and @qcode{b} ## properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.GammaDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.GammaDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.GammaDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{2*2} numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter, and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only meaningful ## when the distribution was fitted to data. If the distribution object was ## created with fixed parameters, or a parameter of a fitted distribution is ## modified, then all elements of the variance-covariance are zero. This ## property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.GammaDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*2} logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.GammaDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: a numeric vector of logical values indicating ## censoring information corresponding to the elements of the data used for ## distribution fitting. If no censoring vector was used for distribution ## fitting, then this field defaults to an empty array. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.GammaDistribution} {@var{pd} =} GammaDistribution (@var{a}, @var{b}) ## @deftypefnx {prob.GammaDistribution} {@var{pd} =} GammaDistribution () ## ## Create a @code{prob.GammaDistribution} object. ## ## @var{a} and @var{b} are the distribution parameters, which the class help ## describes. Called with no arguments the parameters take their defaults, ## @var{a} 1 and @var{b} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = GammaDistribution (a, b) if (nargin == 0) a = 1; b = 1; endif checkparams (a, b); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [a, b]; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'gamma distribution'); endfunction function disp (this) __disp__ (this, 'gamma distribution'); endfunction function this = set.a (this, a) checkparams (a, this.b); this.InputData = []; this.ParameterValues(1) = a; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function a = get.a (this) a = this.ParameterValues(1); endfunction function this = set.b (this, b) checkparams (this.a, b); this.InputData = []; this.ParameterValues(2) = b; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function b = get.b (this) b = this.ParameterValues(2); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.GammaDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.GammaDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = gamcdf (x, this.a, this.b); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= gamcdf (lx, this.a, this.b); p(! (lb | ub)) /= diff (gamcdf ([lx, ux], this.a, this.b)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GammaDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = gamcdf (this.Truncation(1), this.a, this.b); up = gamcdf (this.Truncation(2), this.a, this.b); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = gaminv (np, this.a, this.b); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = gaminv (p, this.a, this.b); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GammaDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.GammaDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = gamstat (this.a, this.b); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GammaDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = gamcdf ([lx, ux], this.a, this.b); m = gaminv (sum (Fa_b) / 2, this.a, this.b); else m = gaminv (0.5, this.a, this.b); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GammaDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = gamlike ([this.a, this.b], this.InputData.data, ... this.InputData.cens, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.GammaDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.GammaDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the ## confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GammaDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = gampdf (x, this.a, this.b); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (gamcdf ([lx, ux], this.a, this.b)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GammaDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.GammaDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.GammaDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GammaDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.GammaDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.GammaDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.GammaDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.GammaDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.GammaDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the gamma distribution, @qcode{@var{pnum} = 1} selects the parameter ## @qcode{a} and @qcode{@var{pnum} = 2} selects the parameter @qcode{b}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.GammaDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.GammaDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.GammaDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.GammaDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{gamrnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) sz = [varargin{:}]; ps = prod (sz); ## Get an estimate of how many more random numbers we need to randomly ## pick the appropriate size from lx = this.Truncation(1); ux = this.Truncation(2); ratio = 1 / diff (gamcdf ([lx, ux], this.a, this.b)); nsize = fix (2 * ratio * ps); # times 2 to be on the safe side ## Generate the numbers and remove out-of-bound random samples r = gamrnd (this.a, this.b, nsize, 1); r(r < lx | r > ux) = []; ## Randomly select the required size and reshape to requested dimensions idx = randperm (numel (r), ps); r = reshape (r(idx), sz); else r = gamrnd (this.a, this.b, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GammaDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.GammaDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, ## and upper limit, @var{upper}. If @var{pd} is fitted to data with ## @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.GammaDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = gamstat (this.a, this.b); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) censor = []; else censor = varargin{2}; endif if (nargin < 4) freq = []; else freq = varargin{3}; endif if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else options = varargin{4}; endif ## Fit data [phat, pci] = gamfit (x, alpha, censor, freq, options); [~, acov] = gamlike (phat, x, censor, freq); ## Create fitted distribution object pd = prob.GammaDistribution.makeFitted (phat, pci, acov, x, censor, freq); endfunction function pd = makeFitted (phat, pci, acov, x, censor, freq) a = phat(1); b = phat(2); pd = prob.GammaDistribution (a, b); pd.ParameterCI = pci; pd.ParameterIsFixed = [false, false]; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', censor, 'freq', freq); endfunction endmethods endclassdef function checkparams (a, b) if (! (isscalar (a) && isnumeric (a) && isreal (a) && isfinite (a) && a > 0)) error ("GammaDistribution: A must be a positive real scalar.") endif if (! (isscalar (b) && isnumeric (b) && isreal (b) && isfinite (b) && b > 0)) error ("GammaDistribution: B must be a positive real scalar.") endif endfunction ## Test output %!shared pd, t %! pd = prob.GammaDistribution (1, 1); %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0:5]), [0, 0.6321, 0.8647, 0.9502, 0.9817, 0.9933], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0, 0.7311, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4]), [0.7769, 0.8647, 0.9502, 0.9817], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4]), [0, 0, 0.7311, 1], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [0, 0.2231, 0.5108, 0.9163, 1.6094, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.1899, 2.4244, 2.7315, 3.1768, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 0.5108, 0.9163, 1.6094, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.4244, 2.7315, 3.1768, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 1.0986, 1e-4); %!assert_equal (iqr (t), 0.8020, 1e-4); %!assert_equal (mean (pd), 1); %!assert_equal (mean (t), 2.6870, 1e-4); %!assert_equal (median (pd), 0.6931, 1e-4); %!assert_equal (median (t), 2.5662, 1e-4); %!assert_equal (pdf (pd, [0:5]), [1, 0.3679, 0.1353, 0.0498, 0.0183, 0.0067], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 1.1565, 0.4255, 0.1565, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1:4, NaN]), [0, 0.3679, 0.1353, 0.0498, 0.0183, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1:4, NaN]), [0, 0, 1.1565, 0.4255, 0.1565, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 1); %!assert_equal (std (t), 0.5253, 1e-4); %!assert_equal (var (pd), 1); %!assert_equal (var (t), 0.2759, 1e-4); %!test %! ## The profile over the first free parameter: 21 grid values, one row of %! ## OTHER per value, and the likelihood peaking at the fitted estimate. %! x = [1.2; 0.4; 3.1; 0.7; 2.5; 1.8; 0.3; 4.2; 1.1; 0.9; ... %! 2.2; 0.6; 1.5; 3.7; 0.8; 2.9; 1.3; 0.5; 2.0; 1.6]; %! pd = fitdist (x, 'Gamma'); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (size (param), [1, 21]); %! assert_equal (size (other), [21, 1]); %! assert_equal (proflik (pd), nlogL); %! [~, imax] = max (nlogL); %! assert_equal (abs (param(imax) - pd.ParameterValues(1)) <= param(2) - param(1), true); ## Test input validation ## 'prob.GammaDistribution' constructor %!error ... %! prob.GammaDistribution (0, 1) %!error ... %! prob.GammaDistribution (Inf, 1) %!error ... %! prob.GammaDistribution (i, 1) %!error ... %! prob.GammaDistribution ('a', 1) %!error ... %! prob.GammaDistribution ([1, 2], 1) %!error ... %! prob.GammaDistribution (NaN, 1) %!error ... %! prob.GammaDistribution (1, 0) %!error ... %! prob.GammaDistribution (1, -1) %!error ... %! prob.GammaDistribution (1, Inf) %!error ... %! prob.GammaDistribution (1, i) %!error ... %! prob.GammaDistribution (1, 'a') %!error ... %! prob.GammaDistribution (1, [1, 2]) %!error ... %! prob.GammaDistribution (1, NaN) ## 'cdf' method %!error ... %! cdf (prob.GammaDistribution, 2, 'uper') %!error ... %! cdf (prob.GammaDistribution, 2, 3) ## 'paramci' method %!shared x %! x = gamrnd (1, 1, [100, 1]); %!error ... %! paramci (prob.GammaDistribution.fit (x), 'alpha') %!error ... %! paramci (prob.GammaDistribution.fit (x), 'alpha', 0) %!error ... %! paramci (prob.GammaDistribution.fit (x), 'alpha', 1) %!error ... %! paramci (prob.GammaDistribution.fit (x), 'alpha', [0.5 2]) %!error ... %! paramci (prob.GammaDistribution.fit (x), 'alpha', '') %!error ... %! paramci (prob.GammaDistribution.fit (x), 'alpha', {0.05}) %!error ... %! paramci (prob.GammaDistribution.fit (x), 'parameter', 'a', 'alpha', {0.05}) %!error ... %! paramci (prob.GammaDistribution.fit (x), 'parameter', {'a', 'b', 'param'}) %!error ... %! paramci (prob.GammaDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', {'a', 'b', 'param'}) %!error ... %! paramci (prob.GammaDistribution.fit (x), 'parameter', 'param') %!error ... %! paramci (prob.GammaDistribution.fit (x), 'alpha', 0.01, 'parameter', 'param') %!error ... %! paramci (prob.GammaDistribution.fit (x), 'NAME', 'value') %!error ... %! paramci (prob.GammaDistribution.fit (x), 'alpha', 0.01, 'NAME', 'value') %!error ... %! paramci (prob.GammaDistribution.fit (x), 'alpha', 0.01, 'parameter', 'a', ... %! 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.GammaDistribution, 'Parent') %!error ... %! plot (prob.GammaDistribution, 'PlotType', 12) %!error ... %! plot (prob.GammaDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.GammaDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.GammaDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.GammaDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.GammaDistribution, 'Discrete', {true}) %!error ... %! plot (prob.GammaDistribution, 'Parent', 12) %!error ... %! plot (prob.GammaDistribution, 'Parent', 'hax') %!error ... %! plot (prob.GammaDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.GammaDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.GammaDistribution, 2) %!error ... %! proflik (prob.GammaDistribution.fit (x), 3) %!error ... %! proflik (prob.GammaDistribution.fit (x), [1, 2]) %!error ... %! proflik (prob.GammaDistribution.fit (x), {1}) %!error ... %! proflik (prob.GammaDistribution.fit (x), 1, ones (2)) %!error ... %! proflik (prob.GammaDistribution.fit (x), 1, 'Display') %!error ... %! proflik (prob.GammaDistribution.fit (x), 1, 'Display', 1) %!error ... %! proflik (prob.GammaDistribution.fit (x), 1, 'Display', {1}) %!error ... %! proflik (prob.GammaDistribution.fit (x), 1, 'Display', {'on'}) %!error ... %! proflik (prob.GammaDistribution.fit (x), 1, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.GammaDistribution.fit (x), 1, 'Display', 'onnn') %!error ... %! proflik (prob.GammaDistribution.fit (x), 1, 'NAME', 'on') %!error ... %! proflik (prob.GammaDistribution.fit (x), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.GammaDistribution.fit (x), 1, {[1 2 3 4]}, 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.GammaDistribution) %!error ... %! truncate (prob.GammaDistribution, 2) %!error ... %! truncate (prob.GammaDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.GammaDistribution (1, 1); %! pd(2) = prob.GammaDistribution (1, 3); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!test %! ## negloglik returns the (positive) negative log-likelihood. %! xdat = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! pdfit = prob.GammaDistribution.fit (xdat'); %! assert_equal (negloglik (pdfit), -sum (log (pdf (pdfit, xdat'))), 1e-9); %! assert_equal (negloglik (pdfit) > 0, true); %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/GeneralizedExtremeValueDistribution.m000066400000000000000000001306551527311104300330170ustar00rootroot00000000000000## Copyright (C) 2024-2025 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef GeneralizedExtremeValueDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.GeneralizedExtremeValueDistribution ## ## Generalized extreme value probability distribution object. ## ## A @code{prob.GeneralizedExtremeValueDistribution} object consists of ## parameters, a model description, and sample data for a generalized extreme ## value probability distribution. ## ## The generalized extreme value distribution is a continuous probability ## distribution that models extreme values. It is defined by shape parameter ## @var{k}, scale parameter @var{sigma}, and location parameter @var{mu}. ## ## There are several ways to create a ## @code{prob.GeneralizedExtremeValueDistribution} object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor @qcode{prob.GeneralizedExtremeValueDistribution ## (@var{k}, @var{sigma}, @var{mu})} to create a generalized extreme value ## distribution with fixed parameter values @var{k}, @var{sigma}, and ## @var{mu}. ## @item Use the static method ## @qcode{prob.GeneralizedExtremeValueDistribution.fit (@var{x}, @var{alpha}, ## @var{freq}, @var{options})} to fit a distribution to the data in @var{x} ## using the same input arguments as the @code{gevfit} function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the generalized extreme value distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Generalized_extreme_value_distribution} ## ## @seealso{fitdist, makedist, gevcdf, gevinv, gevpdf, gevrnd, gevfit, ## gevlike, gevstat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.GeneralizedExtremeValueDistribution} {property} k ## ## Shape parameter ## ## A scalar value characterizing the shape of the generalized extreme value ## distribution. You can access the @qcode{k} property using dot name ## assignment. ## ## @end deftp k ## -*- texinfo -*- ## @deftp {prob.GeneralizedExtremeValueDistribution} {property} sigma ## ## Scale parameter ## ## A positive scalar value characterizing the scale of the generalized ## extreme value distribution. You can access the @qcode{sigma} property ## using dot name assignment. ## ## @end deftp sigma ## -*- texinfo -*- ## @deftp {prob.GeneralizedExtremeValueDistribution} {property} mu ## ## Location parameter ## ## A scalar value characterizing the location of the generalized extreme ## value distribution. You can access the @qcode{mu} property using dot name ## assignment. ## ## @end deftp mu endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.GeneralizedExtremeValueDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Generalized Extreme Value'; ## -*- texinfo -*- ## @deftp {prob.GeneralizedExtremeValueDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 3; ## -*- texinfo -*- ## @deftp {prob.GeneralizedExtremeValueDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{3*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'k', 'sigma', 'mu'}; ## -*- texinfo -*- ## @deftp {prob.GeneralizedExtremeValueDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{3*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Shape', 'Scale', 'Location'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = false; DistributionCode = 'gev'; ParameterRange = [-Inf, realmin, -Inf; Inf, Inf, Inf]; ParameterLogCI = [false, true, false]; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.GeneralizedExtremeValueDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{3*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{k}, @qcode{sigma}, and ## @qcode{mu} properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.GeneralizedExtremeValueDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.GeneralizedExtremeValueDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.GeneralizedExtremeValueDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{3*3} numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter, and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only meaningful ## when the distribution was fitted to data. If the distribution object was ## created with fixed parameters, or a parameter of a fitted distribution is ## modified, then all elements of the variance-covariance are zero. This ## property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.GeneralizedExtremeValueDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*3} logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.GeneralizedExtremeValueDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: a numeric vector of logical values indicating ## censoring information corresponding to the elements of the data used for ## distribution fitting. If no censoring vector was used for distribution ## fitting, then this field defaults to an empty array. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedExtremeValueDistribution} {@var{pd} =} GeneralizedExtremeValueDistribution (@var{k}, @var{sigma}, @var{mu}) ## @deftypefnx {prob.GeneralizedExtremeValueDistribution} {@var{pd} =} GeneralizedExtremeValueDistribution () ## ## Create a @code{prob.GeneralizedExtremeValueDistribution} object. ## ## @var{k}, @var{sigma} and @var{mu} are the distribution parameters, which ## the class help describes. Called with no arguments the parameters take ## their defaults, @var{k} 0, @var{sigma} 1 and @var{mu} 0. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = GeneralizedExtremeValueDistribution (k, sigma, mu) if (nargin == 0) k = 0; sigma = 1; mu = 0; endif checkparams (k, sigma, mu); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [k, sigma, mu]; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'Generalized Extreme Value distribution'); endfunction function disp (this) __disp__ (this, 'Generalized Extreme Value distribution'); endfunction function this = set.k (this, k) checkparams (k, this.sigma, this.mu); this.InputData = []; this.ParameterValues(1) = k; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function k = get.k (this) k = this.ParameterValues(1); endfunction function this = set.sigma (this, sigma) checkparams (this.k, sigma, this.mu); this.InputData = []; this.ParameterValues(2) = sigma; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function sigma = get.sigma (this) sigma = this.ParameterValues(2); endfunction function this = set.mu (this, mu) checkparams (this.k, this.sigma, mu); this.InputData = []; this.ParameterValues(3) = mu; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function mu = get.mu (this) mu = this.ParameterValues(3); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedExtremeValueDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.GeneralizedExtremeValueDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = gevcdf (x, this.k, this.sigma, this.mu); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= gevcdf (lx, this.k, this.sigma, this.mu); p(! (lb | ub)) /= diff (gevcdf ([lx, ux], this.k, this.sigma, this.mu)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedExtremeValueDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = gevcdf (this.Truncation(1), this.k, this.sigma, this.mu); up = gevcdf (this.Truncation(2), this.k, this.sigma, this.mu); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = gevinv (np, this.k, this.sigma, this.mu); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = gevinv (p, this.k, this.sigma, this.mu); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedExtremeValueDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedExtremeValueDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = gevstat (this.k, this.sigma, this.mu); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedExtremeValueDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = gevcdf ([lx, ux], this.k, this.sigma, this.mu); m = gevinv (sum (Fa_b) / 2, this.k, this.sigma, this.mu); else m = gevinv (0.5, this.k, this.sigma, this.mu); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedExtremeValueDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = gevlike ([this.k, this.sigma, this.mu], ... this.InputData.data, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedExtremeValueDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.GeneralizedExtremeValueDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the ## confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedExtremeValueDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = gevpdf (x, this.k, this.sigma, this.mu); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (gevcdf ([lx, ux], this.k, this.sigma, this.mu)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedExtremeValueDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.GeneralizedExtremeValueDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.GeneralizedExtremeValueDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedExtremeValueDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.GeneralizedExtremeValueDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.GeneralizedExtremeValueDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.GeneralizedExtremeValueDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.GeneralizedExtremeValueDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.GeneralizedExtremeValueDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the generalized extreme value distribution, @qcode{@var{pnum} = 1} ## selects the parameter @qcode{k}, @qcode{@var{pnum} = 2} selects the ## parameter @qcode{sigma}, and @qcode{@var{pnum} = 3} selects the ## parameter @qcode{mu}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedExtremeValueDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.GeneralizedExtremeValueDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.GeneralizedExtremeValueDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.GeneralizedExtremeValueDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{gevrnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) sz = [varargin{:}]; ps = prod (sz); ## Get an estimate of how many more random numbers we need to randomly ## pick the appropriate size from lx = this.Truncation(1); ux = this.Truncation(2); ratio = 1 / diff (gevcdf ([lx, ux], this.k, this.sigma, this.mu)); nsize = fix (2 * ratio * ps); # times 2 to be on the safe side ## Generate the numbers and remove out-of-bound random samples r = gevrnd (this.k, this.sigma, this.mu, nsize, 1); r(r < lx | r > ux) = []; ## Randomly select the required size and reshape to requested dimensions idx = randperm (numel (r), ps); r = reshape (r(idx), sz); else r = gevrnd (this.k, this.sigma, this.mu, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedExtremeValueDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedExtremeValueDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, ## and upper limit, @var{upper}. If @var{pd} is fitted to data with ## @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedExtremeValueDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = gevstat (this.k, this.sigma, this.mu); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) freq = []; else freq = varargin{2}; endif if (nargin < 4) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else options = varargin{3}; endif ## Fit data [phat, pci] = gevfit (x, alpha, freq, options); [~, acov] = gevlike (phat, x, freq); ## Create fitted distribution object pd = prob.GeneralizedExtremeValueDistribution.makeFitted ... (phat, pci, acov, x, freq); endfunction function pd = makeFitted (phat, pci, acov, x, freq) k = phat(1); sigma = phat(2); mu = phat(3); pd = prob.GeneralizedExtremeValueDistribution (k, sigma, mu); pd.ParameterCI = pci; pd.ParameterIsFixed = [false, false, false]; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', [], 'freq', freq); endfunction endmethods endclassdef function checkparams (k, sigma, mu) if (! (isscalar (k) && isnumeric (k) && isreal (k) && isfinite (k))) error ("GeneralizedExtremeValueDistribution: K must be a real scalar.") endif if (! (isscalar (sigma) && isnumeric (sigma) && isreal (sigma) && isfinite (sigma) && sigma > 0)) error (strcat ("GeneralizedExtremeValueDistribution: SIGMA must be", ... " a positive real scalar.")) endif if (! (isscalar (mu) && isnumeric (mu) && isreal (mu) && isfinite (mu))) error ("GeneralizedExtremeValueDistribution: MU must be a real scalar.") endif endfunction ## Test output %!shared pd, t %! pd = prob.GeneralizedExtremeValueDistribution; %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0:5]), [0.3679, 0.6922, 0.8734, 0.9514, 0.9819, 0.9933], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0, 0.7195, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4]), [0.8, 0.8734, 0.9514, 0.9819], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4]), [0, 0, 0.7195, 1], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [-Inf, -0.4759, 0.0874, 0.6717, 1.4999, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.1999, 2.4433, 2.7568, 3.2028, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 0.0874, 0.6717, 1.4999, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.4433, 2.7568, 3.2028, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 1.5725, 1e-4); %!assert_equal (iqr (t), 0.8164, 1e-4); %!assert_equal (mean (pd), 0.5772, 1e-4); %!assert_equal (mean (t), 2.7043, 1e-4); %!assert_equal (median (pd), 0.3665, 1e-4); %!assert_equal (median (t), 2.5887, 1e-4); %!assert_equal (pdf (pd, [0:5]), [0.3679, 0.2546, 0.1182, 0.0474, 0.0180, 0.0067], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 1.0902, 0.4369, 0.1659, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1:4, NaN]), [0.1794, 0.2546, 0.1182, 0.0474, 0.0180, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1:4, NaN]), [0, 0, 1.0902, 0.4369, 0.1659, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 1.2825, 1e-4); %!assert_equal (std (t), 0.5289, 1e-4); %!assert_equal (var (pd), 1.6449, 1e-4); %!assert_equal (var (t), 0.2798, 1e-4); %!test %! ## The profile over the first free parameter: 21 grid values, one row of %! ## OTHER per value, and the likelihood peaking at the fitted estimate. %! x = [0.3; -1.2; 0.8; 1.5; -0.4; 0.2; -0.9; 1.1; 0.6; -0.3; ... %! 1.8; -1.5; 0.4; 0.9; -0.7; 1.2; -0.2; 0.5; -1.1; 0.7]; %! pd = fitdist (x, 'GeneralizedExtremeValue'); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (size (param), [1, 21]); %! assert_equal (size (other), [21, 2]); %! assert_equal (proflik (pd), nlogL); %! [~, imax] = max (nlogL); %! assert_equal (abs (param(imax) - pd.ParameterValues(1)) <= param(2) - param(1), true); ## Test input validation ## 'prob.GeneralizedExtremeValueDistribution' constructor %!error ... %! prob.GeneralizedExtremeValueDistribution (Inf, 1, 1) %!error ... %! prob.GeneralizedExtremeValueDistribution (i, 1, 1) %!error ... %! prob.GeneralizedExtremeValueDistribution ('a', 1, 1) %!error ... %! prob.GeneralizedExtremeValueDistribution ([1, 2], 1, 1) %!error ... %! prob.GeneralizedExtremeValueDistribution (NaN, 1, 1) %!error ... %! prob.GeneralizedExtremeValueDistribution (1, 0, 1) %!error ... %! prob.GeneralizedExtremeValueDistribution (1, -1, 1) %!error ... %! prob.GeneralizedExtremeValueDistribution (1, Inf, 1) %!error ... %! prob.GeneralizedExtremeValueDistribution (1, i, 1) %!error ... %! prob.GeneralizedExtremeValueDistribution (1, 'a', 1) %!error ... %! prob.GeneralizedExtremeValueDistribution (1, [1, 2], 1) %!error ... %! prob.GeneralizedExtremeValueDistribution (1, NaN, 1) %!error ... %! prob.GeneralizedExtremeValueDistribution (1, 1, Inf) %!error ... %! prob.GeneralizedExtremeValueDistribution (1, 1, i) %!error ... %! prob.GeneralizedExtremeValueDistribution (1, 1, 'a') %!error ... %! prob.GeneralizedExtremeValueDistribution (1, 1, [1, 2]) %!error ... %! prob.GeneralizedExtremeValueDistribution (1, 1, NaN) ## 'cdf' method %!error ... %! cdf (prob.GeneralizedExtremeValueDistribution, 2, 'uper') %!error ... %! cdf (prob.GeneralizedExtremeValueDistribution, 2, 3) ## 'paramci' method %!shared x %! x = gevrnd (1, 1, 1, [1, 100]); %!error ... %! paramci (prob.GeneralizedExtremeValueDistribution.fit (x), 'alpha') %!error ... %! paramci (prob.GeneralizedExtremeValueDistribution.fit (x), 'alpha', 0) %!error ... %! paramci (prob.GeneralizedExtremeValueDistribution.fit (x), 'alpha', 1) %!error ... %! paramci (prob.GeneralizedExtremeValueDistribution.fit (x), 'alpha', [0.5 2]) %!error ... %! paramci (prob.GeneralizedExtremeValueDistribution.fit (x), 'alpha', '') %!error ... %! paramci (prob.GeneralizedExtremeValueDistribution.fit (x), 'alpha', {0.05}) %!error ... %! paramci (prob.GeneralizedExtremeValueDistribution.fit (x), ... %! 'parameter', 'sigma', 'alpha', {0.05}) %!error ... %! paramci (prob.GeneralizedExtremeValueDistribution.fit (x), ... %! 'parameter', {'k', 'sigma', 'mu', 'param'}) %!error ... %! paramci (prob.GeneralizedExtremeValueDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', {'k', 'sigma', 'mu', 'param'}) %!error ... %! paramci (prob.GeneralizedExtremeValueDistribution.fit (x), 'parameter', 'param') %!error ... %! paramci (prob.GeneralizedExtremeValueDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', 'param') %!error ... %! paramci (prob.GeneralizedExtremeValueDistribution.fit (x), 'NAME', 'value') %!error ... %! paramci (prob.GeneralizedExtremeValueDistribution.fit (x), 'alpha', 0.01, ... %! 'NAME', 'value') %!error ... %! paramci (prob.GeneralizedExtremeValueDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', 'sigma', 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.GeneralizedExtremeValueDistribution, 'Parent') %!error ... %! plot (prob.GeneralizedExtremeValueDistribution, 'PlotType', 12) %!error ... %! plot (prob.GeneralizedExtremeValueDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.GeneralizedExtremeValueDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.GeneralizedExtremeValueDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.GeneralizedExtremeValueDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.GeneralizedExtremeValueDistribution, 'Discrete', {true}) %!error ... %! plot (prob.GeneralizedExtremeValueDistribution, 'Parent', 12) %!error ... %! plot (prob.GeneralizedExtremeValueDistribution, 'Parent', 'hax') %!error ... %! plot (prob.GeneralizedExtremeValueDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.GeneralizedExtremeValueDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.GeneralizedExtremeValueDistribution, 2) %!error ... %! proflik (prob.GeneralizedExtremeValueDistribution.fit (x), 4) %!error ... %! proflik (prob.GeneralizedExtremeValueDistribution.fit (x), [1, 2]) %!error ... %! proflik (prob.GeneralizedExtremeValueDistribution.fit (x), {1}) %!error ... %! proflik (prob.GeneralizedExtremeValueDistribution.fit (x), 1, ones (2)) %!error ... %! proflik (prob.GeneralizedExtremeValueDistribution.fit (x), 1, 'Display') %!error ... %! proflik (prob.GeneralizedExtremeValueDistribution.fit (x), 1, 'Display', 1) %!error ... %! proflik (prob.GeneralizedExtremeValueDistribution.fit (x), 1, 'Display', {1}) %!error ... %! proflik (prob.GeneralizedExtremeValueDistribution.fit (x), 1, 'Display', {'on'}) %!error ... %! proflik (prob.GeneralizedExtremeValueDistribution.fit (x), 1, ... %! 'Display', ['on'; 'on']) %!error ... %! proflik (prob.GeneralizedExtremeValueDistribution.fit (x), 1, 'Display', 'onnn') %!error ... %! proflik (prob.GeneralizedExtremeValueDistribution.fit (x), 1, 'NAME', 'on') %!error ... %! proflik (prob.GeneralizedExtremeValueDistribution.fit (x), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.GeneralizedExtremeValueDistribution.fit (x), 1, {[1 2 3 4]}, ... %! 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.GeneralizedExtremeValueDistribution) %!error ... %! truncate (prob.GeneralizedExtremeValueDistribution, 2) %!error ... %! truncate (prob.GeneralizedExtremeValueDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.GeneralizedExtremeValueDistribution (1, 1, 1); %! pd(2) = prob.GeneralizedExtremeValueDistribution (1, 3, 1); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/GeneralizedParetoDistribution.m000066400000000000000000001302311527311104300316310ustar00rootroot00000000000000## Copyright (C) 2024-2025 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef GeneralizedParetoDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.GeneralizedParetoDistribution ## ## Generalized Pareto probability distribution object. ## ## A @code{prob.GeneralizedParetoDistribution} object consists of parameters, ## a model description, and sample data for a Generalized Pareto probability ## distribution. ## ## The Generalized Pareto distribution is a continuous probability ## distribution that models the tail behavior of other distributions, commonly ## used for extreme value analysis. It is defined by shape parameter @var{k}, ## scale parameter @var{sigma}, and location parameter @var{theta}. ## ## There are several ways to create a ## @code{prob.GeneralizedParetoDistribution} object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor @qcode{prob.GeneralizedParetoDistribution ## (@var{k}, @var{sigma}, @var{theta})} to create a Generalized Pareto ## distribution with fixed parameter values @var{k}, @var{sigma}, and ## @var{theta}. ## @item Use the static method @qcode{prob.GeneralizedParetoDistribution.fit ## (@var{x}, @var{theta}, @var{alpha}, @var{freq}, @var{options})} to fit a ## distribution to the data in @var{x} using the same input arguments as the ## @code{gpfit} function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the Generalized Pareto distribution can be found ## at ## @url{https://en.wikipedia.org/wiki/Generalized_Pareto_distribution} ## ## @seealso{fitdist, makedist, gpcdf, gpinv, gppdf, gprnd, gpfit, ## gplike, gpstat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.GeneralizedParetoDistribution} {property} k ## ## Shape parameter ## ## A scalar value characterizing the shape of the Generalized Pareto ## distribution. You can access the @qcode{k} property using dot name ## assignment. ## ## @end deftp k ## -*- texinfo -*- ## @deftp {prob.GeneralizedParetoDistribution} {property} sigma ## ## Scale parameter ## ## A positive scalar value characterizing the scale of the Generalized ## Pareto distribution. You can access the @qcode{sigma} property using dot ## name assignment. ## ## @end deftp sigma ## -*- texinfo -*- ## @deftp {prob.GeneralizedParetoDistribution} {property} theta ## ## Location parameter ## ## A scalar value characterizing the location of the Generalized Pareto ## distribution. You can access the @qcode{theta} property using dot name ## assignment. ## ## @end deftp theta endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.GeneralizedParetoDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Generalized Pareto'; ## -*- texinfo -*- ## @deftp {prob.GeneralizedParetoDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 3; ## -*- texinfo -*- ## @deftp {prob.GeneralizedParetoDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{3*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'k', 'sigma', 'theta'}; ## -*- texinfo -*- ## @deftp {prob.GeneralizedParetoDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{3*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Shape', 'Scale', 'Location'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = false; DistributionCode = 'gp'; ParameterRange = [-Inf, realmin, -Inf; Inf, Inf, Inf]; ParameterLogCI = [false, true, false]; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.GeneralizedParetoDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{3*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{k}, @qcode{sigma}, and ## @qcode{theta} properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.GeneralizedParetoDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.GeneralizedParetoDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.GeneralizedParetoDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{3*3} numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter, and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only meaningful ## when the distribution was fitted to data. If the distribution object was ## created with fixed parameters, or a parameter of a fitted distribution is ## modified, then all elements of the variance-covariance are zero. This ## property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.GeneralizedParetoDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*3} logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.GeneralizedParetoDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: a numeric vector of logical values indicating ## censoring information corresponding to the elements of the data used for ## distribution fitting. If no censoring vector was used for distribution ## fitting, then this field defaults to an empty array. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedParetoDistribution} {@var{pd} =} GeneralizedParetoDistribution (@var{k}, @var{sigma}, @var{theta}) ## @deftypefnx {prob.GeneralizedParetoDistribution} {@var{pd} =} GeneralizedParetoDistribution () ## ## Create a @code{prob.GeneralizedParetoDistribution} object. ## ## @var{k}, @var{sigma} and @var{theta} are the distribution parameters, ## which the class help describes. Called with no arguments the parameters ## take their defaults, @var{k} 1, @var{sigma} 1 and @var{theta} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = GeneralizedParetoDistribution (k, sigma, theta) if (nargin == 0) k = 1; sigma = 1; theta = 1; endif checkparams (k, sigma, theta); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [k, sigma, theta]; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'Generalized Pareto distribution'); endfunction function disp (this) __disp__ (this, 'Generalized Pareto distribution'); endfunction function this = set.k (this, k) checkparams (k, this.sigma, this.theta); this.InputData = []; this.ParameterValues(1) = k; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function k = get.k (this) k = this.ParameterValues(1); endfunction function this = set.sigma (this, sigma) checkparams (this.k, sigma, this.theta); this.InputData = []; this.ParameterValues(2) = sigma; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function sigma = get.sigma (this) sigma = this.ParameterValues(2); endfunction function this = set.theta (this, theta) checkparams (this.k, this.sigma, theta); this.InputData = []; this.ParameterValues(3) = theta; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function theta = get.theta (this) theta = this.ParameterValues(3); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedParetoDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.GeneralizedParetoDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = gpcdf (x, this.k, this.sigma, this.theta); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= gpcdf (lx, this.k, this.sigma, this.theta); p(! (lb | ub)) /= diff (gpcdf ([lx, ux], this.k, this.sigma, this.theta)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedParetoDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = gpcdf (this.Truncation(1), this.k, this.sigma, this.theta); up = gpcdf (this.Truncation(2), this.k, this.sigma, this.theta); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = gpinv (np, this.k, this.sigma, this.theta); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = gpinv (p, this.k, this.sigma, this.theta); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedParetoDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedParetoDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = gpstat (this.k, this.sigma, this.theta); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedParetoDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = gpcdf ([lx, ux], this.k, this.sigma, this.theta); m = gpinv (sum (Fa_b) / 2, this.k, this.sigma, this.theta); else m = gpinv (0.5, this.k, this.sigma, this.theta); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedParetoDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif ## GPLIKE assumes a zero location, so shift the data by THETA. nlogL = gplike ([this.k, this.sigma], ... this.InputData.data - this.theta, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedParetoDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.GeneralizedParetoDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the ## confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedParetoDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = gppdf (x, this.k, this.sigma, this.theta); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (gpcdf ([lx, ux], this.k, this.sigma, this.theta)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedParetoDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.GeneralizedParetoDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.GeneralizedParetoDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedParetoDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.GeneralizedParetoDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.GeneralizedParetoDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.GeneralizedParetoDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.GeneralizedParetoDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.GeneralizedParetoDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the Generalized Pareto distribution, @qcode{@var{pnum} = 1} selects ## the parameter @qcode{k}, @qcode{@var{pnum} = 2} selects the parameter ## @qcode{sigma}, and @qcode{@var{pnum} = 3} selects the parameter ## @qcode{theta}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedParetoDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.GeneralizedParetoDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.GeneralizedParetoDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.GeneralizedParetoDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{random} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) sz = [varargin{:}]; ps = prod (sz); ## Get an estimate of how many more random numbers we need to randomly ## pick the appropriate size from lx = this.Truncation(1); ux = this.Truncation(2); ratio = 1 / diff (gpcdf ([lx, ux], this.k, this.sigma, this.theta)); nsize = fix (2 * ratio * ps); # times 2 to be on the safe side ## Generate the numbers and remove out-of-bound random samples r = gprnd (this.k, this.sigma, this.theta, nsize, 1); r(r < lx | r > ux) = []; ## Randomly select the required size and reshape to requested dimensions idx = randperm (numel (r), ps); r = reshape (r(idx), sz); else r = gprnd (this.k, this.sigma, this.theta, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedParetoDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedParetoDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, ## and upper limit, @var{upper}. If @var{pd} is fitted to data with ## @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.GeneralizedParetoDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = gpstat (this.k, this.sigma, this.theta); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, theta, varargin) ## Check input arguments if (nargin < 3) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 4) freq = []; else freq = varargin{2}; endif if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else options = varargin{3}; endif ## Fit data. GPFIT assumes a zero location, as MATLAB's does, so the ## known THETA is shifted out of the data and put back into the estimates. [phat, pci] = gpfit (x - theta, alpha, options, freq); phat = [phat, theta]; pci = [pci, [theta; theta]]; ## GPLIKE covers the two estimated parameters; THETA is fixed, so the ## covariance carries a zero row and column for it, as MATLAB's does. [~, acov] = gplike (phat(1:2), x - theta, freq); acov = [acov, [0; 0]; 0, 0, 0]; ## Create fitted distribution object pd = prob.GeneralizedParetoDistribution.makeFitted (phat, pci, acov, x, freq); endfunction function pd = makeFitted (phat, pci, acov, x, freq) k = phat(1); sigma = phat(2); theta = phat(3); pd = prob.GeneralizedParetoDistribution (k, sigma, theta); pd.ParameterCI = pci; pd.ParameterIsFixed = [false, false, true]; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', [], 'freq', freq); endfunction endmethods endclassdef function checkparams (k, sigma, theta) if (! (isscalar (k) && isnumeric (k) && isreal (k) && isfinite (k))) error ("GeneralizedParetoDistribution: K must be a real scalar.") endif if (! (isscalar (sigma) && isnumeric (sigma) && isreal (sigma) && isfinite (sigma) && sigma > 0)) error ("GeneralizedParetoDistribution: SIGMA must be a positive real scalar.") endif if (! (isscalar (theta) && isnumeric (theta) && isreal (theta) && isfinite (theta))) error ("GeneralizedParetoDistribution: THETA must be a real scalar.") endif endfunction ## Test output %!shared pd, t %! pd = prob.GeneralizedParetoDistribution (1, 1, 1); %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0:5]), [0, 0, 0.5, 0.6667, 0.75, 0.8], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0, 0.6667, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4]), [0.3333, 0.5, 0.6667, 0.75], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4]), [0, 0, 0.6667, 1], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [1, 1.25, 1.6667, 2.5, 5, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.2222, 2.5, 2.8571, 3.3333, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 1.6667, 2.5, 5, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.5, 2.8571, 3.3333, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 2.6667, 1e-4); %!assert_equal (iqr (t), 0.9143, 1e-4); %!assert_equal (mean (pd), Inf); %!assert_equal (mean (t), 2.7726, 1e-4); %!assert_equal (median (pd), 2); %!assert_equal (median (t), 2.6667, 1e-4); %!assert_equal (pdf (pd, [0:5]), [0, 1, 0.25, 0.1111, 0.0625, 0.04], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 1, 0.4444, 0.25, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1:4, NaN]), [0, 1, 0.25, 0.1111, 0.0625, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1:4, NaN]), [0, 0, 1, 0.4444, 0.25, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), Inf); %!assert_equal (std (t), 0.5592, 1e-4); %!assert_equal (var (pd), Inf); %!assert_equal (var (t), 0.3128, 1e-4); %!test %! ## The profile over the first free parameter: 21 grid values, one row of %! ## OTHER per value, and the likelihood peaking at the fitted estimate. %! x = [1.2; 0.4; 3.1; 0.7; 2.5; 1.8; 0.3; 4.2; 1.1; 0.9; ... %! 2.2; 0.6; 1.5; 3.7; 0.8; 2.9; 1.3; 0.5; 2.0; 1.6] - 0.3 + 1e-8; %! pd = fitdist (x, 'GeneralizedPareto', 'theta', 0); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (size (param), [1, 21]); %! assert_equal (size (other), [21, 2]); %! assert_equal (proflik (pd), nlogL); %! [~, imax] = max (nlogL); %! assert_equal (abs (param(imax) - pd.ParameterValues(1)) <= param(2) - param(1), true); ## Test input validation ## 'prob.GeneralizedParetoDistribution' constructor %!error ... %! prob.GeneralizedParetoDistribution (Inf, 1, 1) %!error ... %! prob.GeneralizedParetoDistribution (i, 1, 1) %!error ... %! prob.GeneralizedParetoDistribution ('a', 1, 1) %!error ... %! prob.GeneralizedParetoDistribution ([1, 2], 1, 1) %!error ... %! prob.GeneralizedParetoDistribution (NaN, 1, 1) %!error ... %! prob.GeneralizedParetoDistribution (1, 0, 1) %!error ... %! prob.GeneralizedParetoDistribution (1, -1, 1) %!error ... %! prob.GeneralizedParetoDistribution (1, Inf, 1) %!error ... %! prob.GeneralizedParetoDistribution (1, i, 1) %!error ... %! prob.GeneralizedParetoDistribution (1, 'a', 1) %!error ... %! prob.GeneralizedParetoDistribution (1, [1, 2], 1) %!error ... %! prob.GeneralizedParetoDistribution (1, NaN, 1) %!error ... %! prob.GeneralizedParetoDistribution (1, 1, Inf) %!error ... %! prob.GeneralizedParetoDistribution (1, 1, i) %!error ... %! prob.GeneralizedParetoDistribution (1, 1, 'a') %!error ... %! prob.GeneralizedParetoDistribution (1, 1, [1, 2]) %!error ... %! prob.GeneralizedParetoDistribution (1, 1, NaN) ## 'cdf' method %!error ... %! cdf (prob.GeneralizedParetoDistribution, 2, 'uper') %!error ... %! cdf (prob.GeneralizedParetoDistribution, 2, 3) ## 'paramci' method %!shared x %! x = gprnd (1, 1, 1, [1, 100]); %!error ... %! paramci (prob.GeneralizedParetoDistribution.fit (x, 1), 'alpha') %!error ... %! paramci (prob.GeneralizedParetoDistribution.fit (x, 1), 'alpha', 0) %!error ... %! paramci (prob.GeneralizedParetoDistribution.fit (x, 1), 'alpha', 1) %!error ... %! paramci (prob.GeneralizedParetoDistribution.fit (x, 1), 'alpha', [0.5 2]) %!error ... %! paramci (prob.GeneralizedParetoDistribution.fit (x, 1), 'alpha', '') %!error ... %! paramci (prob.GeneralizedParetoDistribution.fit (x, 1), 'alpha', {0.05}) %!error ... %! paramci (prob.GeneralizedParetoDistribution.fit (x, 1), ... %! 'parameter', 'sigma', 'alpha', {0.05}) %!error ... %! paramci (prob.GeneralizedParetoDistribution.fit (x, 1), ... %! 'parameter', {'k', 'sigma', 'theta', 'param'}) %!error ... %! paramci (prob.GeneralizedParetoDistribution.fit (x, 1), 'alpha', 0.01, ... %! 'parameter', {'k', 'sigma', 'theta', 'param'}) %!error ... %! paramci (prob.GeneralizedParetoDistribution.fit (x, 1), 'parameter', 'param') %!error ... %! paramci (prob.GeneralizedParetoDistribution.fit (x, 1), 'alpha', 0.01, ... %! 'parameter', 'param') %!error ... %! paramci (prob.GeneralizedParetoDistribution.fit (x, 1), 'NAME', 'value') %!error ... %! paramci (prob.GeneralizedParetoDistribution.fit (x, 1), 'alpha', 0.01, ... %! 'NAME', 'value') %!error ... %! paramci (prob.GeneralizedParetoDistribution.fit (x, 1), 'alpha', 0.01, ... %! 'parameter', 'sigma', 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.GeneralizedParetoDistribution, 'Parent') %!error ... %! plot (prob.GeneralizedParetoDistribution, 'PlotType', 12) %!error ... %! plot (prob.GeneralizedParetoDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.GeneralizedParetoDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.GeneralizedParetoDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.GeneralizedParetoDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.GeneralizedParetoDistribution, 'Discrete', {true}) %!error ... %! plot (prob.GeneralizedParetoDistribution, 'Parent', 12) %!error ... %! plot (prob.GeneralizedParetoDistribution, 'Parent', 'hax') %!error ... %! plot (prob.GeneralizedParetoDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.GeneralizedParetoDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.GeneralizedParetoDistribution, 2) %!error ... %! proflik (prob.GeneralizedParetoDistribution.fit (x, 1), 3) %!error ... %! proflik (prob.GeneralizedParetoDistribution.fit (x, 1), [1, 2]) %!error ... %! proflik (prob.GeneralizedParetoDistribution.fit (x, 1), {1}) %!error ... %! proflik (prob.GeneralizedParetoDistribution.fit (x, 1), 1, ones (2)) %!error ... %! proflik (prob.GeneralizedParetoDistribution.fit (x, 1), 1, 'Display') %!error ... %! proflik (prob.GeneralizedParetoDistribution.fit (x, 1), 1, 'Display', 1) %!error ... %! proflik (prob.GeneralizedParetoDistribution.fit (x, 1), 1, 'Display', {1}) %!error ... %! proflik (prob.GeneralizedParetoDistribution.fit (x, 1), 1, 'Display', {'on'}) %!error ... %! proflik (prob.GeneralizedParetoDistribution.fit (x, 1), 1, ... %! 'Display', ['on'; 'on']) %!error ... %! proflik (prob.GeneralizedParetoDistribution.fit (x, 1), 1, 'Display', 'onnn') %!error ... %! proflik (prob.GeneralizedParetoDistribution.fit (x, 1), 1, 'NAME', 'on') %!error ... %! proflik (prob.GeneralizedParetoDistribution.fit (x, 1), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.GeneralizedParetoDistribution.fit (x, 1), 1, {[1 2 3 4]}, ... %! 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.GeneralizedParetoDistribution) %!error ... %! truncate (prob.GeneralizedParetoDistribution, 2) %!error ... %! truncate (prob.GeneralizedParetoDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.GeneralizedParetoDistribution (1, 1, 1); %! pd(2) = prob.GeneralizedParetoDistribution (1, 3, 1); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/HalfNormalDistribution.m000066400000000000000000001210121527311104300302450ustar00rootroot00000000000000## Copyright (C) 2024-2025 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef HalfNormalDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.HalfNormalDistribution ## ## Half-normal probability distribution object. ## ## A @code{prob.HalfNormalDistribution} object consists of parameters, a model ## description, and sample data for a half-normal probability distribution. ## ## The half-normal distribution is a continuous probability distribution that ## models the time to failure of materials subjected to cyclic loading. It is ## defined by location parameter @var{mu} and scale parameter @var{sigma}. ## ## There are several ways to create a @code{prob.HalfNormalDistribution} ## object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor @qcode{prob.HalfNormalDistribution (@var{mu}, ## @var{sigma})} to create a half-normal distribution with fixed parameter ## values @var{mu} and @var{sigma}. ## @item Use the static method ## @qcode{prob.HalfNormalDistribution.fit (@var{x}, @var{mu}, @var{freq})} to ## fit a distribution to the data in @var{x} using the same input arguments as ## the @code{hnfit} function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the half-normal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Half-normal_distribution} ## ## @seealso{fitdist, makedist, hncdf, hninv, hnpdf, hnrnd, hnfit, ## hnlike, hnstat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.HalfNormalDistribution} {property} mu ## ## Location parameter ## ## A scalar value characterizing the location of the ## half-normal distribution. You can access the @qcode{mu} ## property using dot name assignment. ## ## @end deftp mu ## -*- texinfo -*- ## @deftp {prob.HalfNormalDistribution} {property} sigma ## ## Scale parameter ## ## A positive scalar value characterizing the scale of the ## half-normal distribution. You can access the @qcode{sigma} ## property using dot name assignment. ## ## @end deftp sigma endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.HalfNormalDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Half Normal'; ## -*- texinfo -*- ## @deftp {prob.HalfNormalDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 2; ## -*- texinfo -*- ## @deftp {prob.HalfNormalDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'mu', 'sigma'}; ## -*- texinfo -*- ## @deftp {prob.HalfNormalDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Location', 'Scale'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = false; DistributionCode = 'hn'; ParameterRange = [-Inf, realmin; Inf, Inf]; ParameterLogCI = [true, true]; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.HalfNormalDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{2*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{mu} and @qcode{sigma} ## properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.HalfNormalDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.HalfNormalDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.HalfNormalDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{2*2} numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter, and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only ## meaningful when the distribution was fitted to data. If the distribution ## object was created with fixed parameters, or a parameter of a fitted ## distribution is modified, then all elements of the variance-covariance ## are zero. This property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.HalfNormalDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*2} logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.HalfNormalDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: a numeric vector of logical values indicating ## censoring information corresponding to the elements of the data used for ## distribution fitting. If no censoring vector was used for distribution ## fitting, then this field defaults to an empty array. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.HalfNormalDistribution} {@var{pd} =} HalfNormalDistribution (@var{mu}, @var{sigma}) ## @deftypefnx {prob.HalfNormalDistribution} {@var{pd} =} HalfNormalDistribution () ## ## Create a @code{prob.HalfNormalDistribution} object. ## ## @var{mu} and @var{sigma} are the distribution parameters, which the class ## help describes. Called with no arguments the parameters take their ## defaults, @var{mu} 0 and @var{sigma} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = HalfNormalDistribution (mu, sigma) if (nargin == 0) mu = 0; sigma = 1; endif checkparams (mu, sigma); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [mu, sigma]; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'Half-normal distribution'); endfunction function disp (this) __disp__ (this, 'Half-normal distribution'); endfunction function this = set.mu (this, mu) checkparams (mu, this.sigma); this.InputData = []; this.ParameterValues(1) = mu; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function mu = get.mu (this) mu = this.ParameterValues(1); endfunction function this = set.sigma (this, sigma) checkparams (this.mu, sigma); this.InputData = []; this.ParameterValues(2) = sigma; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function sigma = get.sigma (this) sigma = this.ParameterValues(2); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.HalfNormalDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.HalfNormalDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = hncdf (x, this.mu, this.sigma); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= hncdf (lx, this.mu, this.sigma); p(! (lb | ub)) /= diff (hncdf ([lx, ux], this.mu, this.sigma)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.HalfNormalDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = hncdf (this.Truncation(1), this.mu, this.sigma); up = hncdf (this.Truncation(2), this.mu, this.sigma); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = hninv (np, this.mu, this.sigma); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = hninv (p, this.mu, this.sigma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.HalfNormalDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.HalfNormalDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = hnstat (this.mu, this.sigma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.HalfNormalDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = hncdf ([lx, ux], this.mu, this.sigma); m = hninv (sum (Fa_b) / 2, this.mu, this.sigma); else m = hninv (0.5, this.mu, this.sigma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.HalfNormalDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = hnlike ([this.mu, this.sigma], ... this.InputData.data, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.HalfNormalDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.HalfNormalDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.HalfNormalDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = hnpdf (x, this.mu, this.sigma); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (hncdf ([lx, ux], this.mu, this.sigma)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.HalfNormalDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.HalfNormalDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.HalfNormalDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.HalfNormalDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.HalfNormalDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.HalfNormalDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.HalfNormalDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.HalfNormalDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.HalfNormalDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the Half-normal distribution, @qcode{@var{pnum} = 1} selects ## the parameter @qcode{mu} and @qcode{@var{pnum} = 2} selects the ## parameter @qcode{sigma}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.HalfNormalDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.HalfNormalDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.HalfNormalDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.HalfNormalDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{hnrnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) sz = [varargin{:}]; ps = prod (sz); ## Get an estimate of how many more random numbers we need to randomly ## pick the appropriate size from lx = this.Truncation(1); ux = this.Truncation(2); ratio = 1 / diff (hncdf ([lx, ux], this.mu, this.sigma)); nsize = fix (2 * ratio * ps); # times 2 to be on the safe side ## Generate the numbers and remove out-of-bound random samples r = hnrnd (this.mu, this.sigma, nsize, 1); r(r < lx | r > ux) = []; ## Randomly select the required size and reshape to requested dimensions idx = randperm (numel (r), ps); r = reshape (r(idx), sz); else r = hnrnd (this.mu, this.sigma, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.HalfNormalDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.HalfNormalDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, and upper limit, @var{upper}. If @var{pd} is fitted to data ## with @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.HalfNormalDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = hnstat (this.mu, this.sigma); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, mu, varargin) ## Check input arguments if (nargin < 3) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 4) freq = []; else freq = varargin{2}; endif ## Fit data [phat, pci] = hnfit (x, mu, alpha, freq); [~, acov] = hnlike (phat, x, freq); ## Create fitted distribution object pd = prob.HalfNormalDistribution.makeFitted (phat, pci, acov, x, freq); endfunction function pd = makeFitted (phat, pci, acov, x, freq) mu = phat(1); sigma = phat(2); pd = prob.HalfNormalDistribution (mu, sigma); pd.ParameterCI = pci; pd.ParameterIsFixed = [true, false]; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', [], 'freq', freq); endfunction endmethods endclassdef function checkparams (mu, sigma) if (! (isscalar (mu) && isnumeric (mu) && isreal (mu) && isfinite (mu))) error ("HalfNormalDistribution: MU must be a real scalar.") endif if (! (isscalar (sigma) && isnumeric (sigma) && isreal (sigma) && isfinite (sigma) && sigma > 0)) error ("HalfNormalDistribution: SIGMA must be a positive real scalar.") endif endfunction ## Test output %!shared pd, t %! pd = prob.HalfNormalDistribution (0, 1); %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0:5]), [0, 0.6827, 0.9545, 0.9973, 0.9999, 1], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0, 0.9420, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4]), [0.8664, 0.9545, 0.9973, 0.9999], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4]), [0, 0, 0.9420, 1], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [0, 0.2533, 0.5244, 0.8416, 1.2816, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.0923, 2.2068, 2.3607, 2.6064, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 0.5244, 0.8416, 1.2816, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.2068, 2.3607, 2.6064, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 0.8317, 1e-4); %!assert_equal (iqr (t), 0.4111, 1e-4); %!assert_equal (mean (pd), 0.7979, 1e-4); %!assert_equal (mean (t), 2.3706, 1e-4); %!assert_equal (median (pd), 0.6745, 1e-4); %!assert_equal (median (t), 2.2771, 1e-4); %!assert_equal (pdf (pd, [0:5]), [0.7979, 0.4839, 0.1080, 0.0089, 0.0003, 0], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 2.3765, 0.1951, 0.0059, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1:4, NaN]), [0, 0.4839, 0.1080, 0.0089, 0.0003, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1:4, NaN]), [0, 0, 2.3765, 0.1951, 0.0059, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 0.6028, 1e-4); %!assert_equal (std (t), 0.3310, 1e-4); %!assert_equal (var (pd), 0.3634, 1e-4); %!assert_equal (var (t), 0.1096, 1e-4); %!test %! ## A fixed parameter is skipped by the default PNUM and reported in OTHER at %! ## its own value. Verified against MATLAB. %! x = [1.2; 0.4; 3.1; 0.7; 2.5; 1.8; 0.3; 4.2; 1.1; 0.9; ... %! 2.2; 0.6; 1.5; 3.7; 0.8; 2.9; 1.3; 0.5; 2.0; 1.6]; %! pdh = prob.HalfNormalDistribution.fit (x, 0); %! [nlogL, param, other] = proflik (pdh, 2); %! assert_equal (size (param), [1, 101]); %! assert_equal (size (other), [101, 1]); %! assert_equal (unique (other), 0); %! assert_equal (proflik (pdh), nlogL); ## Test input validation ## 'prob.HalfNormalDistribution' constructor %!error ... %! prob.HalfNormalDistribution (Inf, 1) %!error ... %! prob.HalfNormalDistribution (i, 1) %!error ... %! prob.HalfNormalDistribution ('a', 1) %!error ... %! prob.HalfNormalDistribution ([1, 2], 1) %!error ... %! prob.HalfNormalDistribution (NaN, 1) %!error ... %! prob.HalfNormalDistribution (1, 0) %!error ... %! prob.HalfNormalDistribution (1, -1) %!error ... %! prob.HalfNormalDistribution (1, Inf) %!error ... %! prob.HalfNormalDistribution (1, i) %!error ... %! prob.HalfNormalDistribution (1, 'a') %!error ... %! prob.HalfNormalDistribution (1, [1, 2]) %!error ... %! prob.HalfNormalDistribution (1, NaN) ## 'cdf' method %!error ... %! cdf (prob.HalfNormalDistribution, 2, 'uper') %!error ... %! cdf (prob.HalfNormalDistribution, 2, 3) ## 'paramci' method %!shared x %! x = hnrnd (1, 1, [1, 100]); %!error ... %! paramci (prob.HalfNormalDistribution.fit (x, 1), 'alpha') %!error ... %! paramci (prob.HalfNormalDistribution.fit (x, 1), 'alpha', 0) %!error ... %! paramci (prob.HalfNormalDistribution.fit (x, 1), 'alpha', 1) %!error ... %! paramci (prob.HalfNormalDistribution.fit (x, 1), 'alpha', [0.5 2]) %!error ... %! paramci (prob.HalfNormalDistribution.fit (x, 1), 'alpha', '') %!error ... %! paramci (prob.HalfNormalDistribution.fit (x, 1), 'alpha', {0.05}) %!error ... %! paramci (prob.HalfNormalDistribution.fit (x, 1), 'parameter', 'sigma', ... %! 'alpha', {0.05}) %!error ... %! paramci (prob.HalfNormalDistribution.fit (x, 1), ... %! 'parameter', {'mu', 'sigma', 'param'}) %!error ... %! paramci (prob.HalfNormalDistribution.fit (x, 1), 'alpha', 0.01, ... %! 'parameter', {'mu', 'sigma', 'param'}) %!error ... %! paramci (prob.HalfNormalDistribution.fit (x, 1), 'parameter', 'param') %!error ... %! paramci (prob.HalfNormalDistribution.fit (x, 1), 'alpha', 0.01, ... %! 'parameter', 'param') %!error ... %! paramci (prob.HalfNormalDistribution.fit (x, 1),'NAME', 'value') %!error ... %! paramci (prob.HalfNormalDistribution.fit (x, 1), 'alpha', 0.01, ... %! 'NAME', 'value') %!error ... %! paramci (prob.HalfNormalDistribution.fit (x, 1), 'alpha', 0.01, ... %! 'parameter', 'sigma', 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.HalfNormalDistribution, 'Parent') %!error ... %! plot (prob.HalfNormalDistribution, 'PlotType', 12) %!error ... %! plot (prob.HalfNormalDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.HalfNormalDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.HalfNormalDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.HalfNormalDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.HalfNormalDistribution, 'Discrete', {true}) %!error ... %! plot (prob.HalfNormalDistribution, 'Parent', 12) %!error ... %! plot (prob.HalfNormalDistribution, 'Parent', 'hax') %!error ... %! plot (prob.HalfNormalDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.HalfNormalDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.HalfNormalDistribution, 2) %!error ... %! proflik (prob.HalfNormalDistribution.fit (x, 1), 3) %!error ... %! proflik (prob.HalfNormalDistribution.fit (x, 1), [1, 2]) %!error ... %! proflik (prob.HalfNormalDistribution.fit (x, 1), {1}) %!error ... %! proflik (prob.HalfNormalDistribution.fit (x, 1), 1) %!error ... %! proflik (prob.HalfNormalDistribution.fit (x, 1), 2, ones (2)) %!error ... %! proflik (prob.HalfNormalDistribution.fit (x, 1), 2, 'Display') %!error ... %! proflik (prob.HalfNormalDistribution.fit (x, 1), 2, 'Display', 1) %!error ... %! proflik (prob.HalfNormalDistribution.fit (x, 1), 2, 'Display', {1}) %!error ... %! proflik (prob.HalfNormalDistribution.fit (x, 1), 2, 'Display', {'on'}) %!error ... %! proflik (prob.HalfNormalDistribution.fit (x, 1), 2, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.HalfNormalDistribution.fit (x, 1), 2, 'Display', 'onnn') %!error ... %! proflik (prob.HalfNormalDistribution.fit (x, 1), 2, 'NAME', 'on') %!error ... %! proflik (prob.HalfNormalDistribution.fit (x, 1), 2, {'NAME'}, 'on') %!error ... %! proflik (prob.HalfNormalDistribution.fit (x, 1), 2, {[1 2 3 4]}, ... %! 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.HalfNormalDistribution) %!error ... %! truncate (prob.HalfNormalDistribution, 2) %!error ... %! truncate (prob.HalfNormalDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.HalfNormalDistribution (1, 1); %! pd(2) = prob.HalfNormalDistribution (1, 3); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/InverseGaussianDistribution.m000066400000000000000000001233241527311104300313400ustar00rootroot00000000000000## Copyright (C) 2024-2025 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef InverseGaussianDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.InverseGaussianDistribution ## ## Inverse Gaussian probability distribution object. ## ## A @code{prob.InverseGaussianDistribution} object consists of parameters, a ## model description, and sample data for a Inverse Gaussian probability ## distribution. ## ## The Inverse Gaussian distribution is a continuous probability distribution, ## which is often used to model non-negative positively skewed data. Is is ## defined by mean parameter @var{mu} and shape parameter @var{lambda}. ## ## There are several ways to create a @code{prob.InverseGaussianDistribution} ## object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor ## @qcode{prob.InverseGaussianDistribution (@var{mu}, @var{lambda})} to create ## a Inverse Gaussian distribution with fixed parameter values @var{mu} and ## @var{lambda}. ## @item Use the static method @qcode{prob.InverseGaussianDistribution.fit ## (@var{x}, @var{alpha}, @var{censor}, @var{freq}, @var{options})} to fit a ## distribution to the data in @var{x} using the same input arguments as the ## @code{invgfit} function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the Inverse Gaussian distribution can be found at ## @url{https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution} ## ## @seealso{fitdist, makedist, invgcdf, invginv, invgpdf, invgrnd, invgfit, ## invglike, invgstat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.InverseGaussianDistribution} {property} mu ## ## Mean parameter ## ## A positive scalar value characterizing the mean of the ## Inverse Gaussian distribution. You can access the @qcode{mu} ## property using dot name assignment. ## ## @end deftp mu ## -*- texinfo -*- ## @deftp {prob.InverseGaussianDistribution} {property} lambda ## ## Shape parameter ## ## A positive scalar value characterizing the shape of the ## Inverse Gaussian distribution. You can access the @qcode{lambda} ## property using dot name assignment. ## ## @end deftp lambda endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.InverseGaussianDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Inverse Gaussian'; ## -*- texinfo -*- ## @deftp {prob.InverseGaussianDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 2; ## -*- texinfo -*- ## @deftp {prob.InverseGaussianDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'mu', 'lambda'}; ## -*- texinfo -*- ## @deftp {prob.InverseGaussianDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Mean', 'Shape'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = true; DistributionCode = 'invg'; ParameterRange = [realmin, realmin; Inf, Inf]; ParameterLogCI = [true, true]; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.InverseGaussianDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{2*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{mu} and @qcode{lambda} ## properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.InverseGaussianDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.InverseGaussianDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.InverseGaussianDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{2*2} numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter, and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only meaningful ## when the distribution was fitted to data. If the distribution object was ## created with fixed parameters, or a parameter of a fitted distribution is ## modified, then all elements of the variance-covariance are zero. This ## property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.InverseGaussianDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*2} logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.InverseGaussianDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: a numeric vector of logical values indicating ## censoring information corresponding to the elements of the data used for ## distribution fitting. If no censoring vector was used for distribution ## fitting, then this field defaults to an empty array. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.InverseGaussianDistribution} {@var{pd} =} InverseGaussianDistribution (@var{mu}, @var{lambda}) ## @deftypefnx {prob.InverseGaussianDistribution} {@var{pd} =} InverseGaussianDistribution () ## ## Create a @code{prob.InverseGaussianDistribution} object. ## ## @var{mu} and @var{lambda} are the distribution parameters, which the ## class help describes. Called with no arguments the parameters take their ## defaults, @var{mu} 1 and @var{lambda} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = InverseGaussianDistribution (mu, lambda) if (nargin == 0) mu = 1; lambda = 1; endif checkparams (mu, lambda); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [mu, lambda]; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'Inverse Gaussian distribution'); endfunction function disp (this) __disp__ (this, 'Inverse Gaussian distribution'); endfunction function this = set.mu (this, mu) checkparams (mu, this.lambda); this.InputData = []; this.ParameterValues(1) = mu; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function mu = get.mu (this) mu = this.ParameterValues(1); endfunction function this = set.lambda (this, lambda) checkparams (this.mu, lambda); this.InputData = []; this.ParameterValues(2) = lambda; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function lambda = get.lambda (this) lambda = this.ParameterValues(2); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.InverseGaussianDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.InverseGaussianDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = invgcdf (x, this.mu, this.lambda); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= invgcdf (lx, this.mu, this.lambda); p(! (lb | ub)) /= diff (invgcdf ([lx, ux], this.mu, this.lambda)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.InverseGaussianDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = invgcdf (this.Truncation(1), this.mu, this.lambda); up = invgcdf (this.Truncation(2), this.mu, this.lambda); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = invginv (np, this.mu, this.lambda); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = invginv (p, this.mu, this.lambda); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.InverseGaussianDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.InverseGaussianDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = invgstat (this.mu, this.lambda); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.InverseGaussianDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = invgcdf ([lx, ux], this.mu, this.lambda); m = invginv (sum (Fa_b) / 2, this.mu, this.lambda); else m = invginv (0.5, this.mu, this.lambda); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.InverseGaussianDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = invglike ([this.mu, this.lambda], this.InputData.data, ... this.InputData.cens, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.InverseGaussianDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.InverseGaussianDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the ## confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.InverseGaussianDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = invgpdf (x, this.mu, this.lambda); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (invgcdf ([lx, ux], this.mu, this.lambda)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.InverseGaussianDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.InverseGaussianDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.InverseGaussianDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.InverseGaussianDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.InverseGaussianDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.InverseGaussianDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.InverseGaussianDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.InverseGaussianDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.InverseGaussianDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the Inverse Gaussian distribution, @qcode{@var{pnum} = 1} selects ## the parameter @qcode{mu} and @qcode{@var{pnum} = 2} selects the ## parameter @qcode{lambda}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.InverseGaussianDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.InverseGaussianDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.InverseGaussianDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.InverseGaussianDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{invgrnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) sz = [varargin{:}]; ps = prod (sz); ## Get an estimate of how many more random numbers we need to randomly ## pick the appropriate size from lx = this.Truncation(1); ux = this.Truncation(2); ratio = 1 / diff (invgcdf ([lx, ux], this.mu, this.lambda)); nsize = fix (2 * ratio * ps); # times 2 to be on the safe side ## Generate the numbers and remove out-of-bound random samples r = invgrnd (this.mu, this.lambda, nsize, 1); r(r < lx | r > ux) = []; ## Randomly select the required size and reshape to requested dimensions idx = randperm (numel (r), ps); r = reshape (r(idx), sz); else r = invgrnd (this.mu, this.lambda, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.InverseGaussianDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.InverseGaussianDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, ## and upper limit, @var{upper}. If @var{pd} is fitted to data with ## @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.InverseGaussianDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = invgstat (this.mu, this.lambda); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) censor = []; else censor = varargin{2}; endif if (nargin < 4) freq = []; else freq = varargin{3}; endif if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else options = varargin{4}; endif ## Fit data [phat, pci] = invgfit (x, alpha, censor, freq, options); [~, acov] = invglike (phat, x, censor, freq); ## Create fitted distribution object pd = prob.InverseGaussianDistribution.makeFitted ... (phat, pci, acov, x, censor, freq); endfunction function pd = makeFitted (phat, pci, acov, x, censor, freq) mu = phat(1); lambda = phat(2); pd = prob.InverseGaussianDistribution (mu, lambda); pd.ParameterCI = pci; pd.ParameterIsFixed = [false, false]; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', censor, 'freq', freq); endfunction endmethods endclassdef function checkparams (mu, lambda) if (! (isscalar (mu) && isnumeric (mu) && isreal (mu) && isfinite (mu) && mu > 0)) error ("InverseGaussianDistribution: MU must be a positive real scalar.") endif if (! (isscalar (lambda) && isnumeric (lambda) && isreal (lambda) && isfinite (lambda) && lambda > 0)) error ("InverseGaussianDistribution: LAMBDA must be a positive real scalar.") endif endfunction ## Test output %!shared pd, t %! pd = prob.InverseGaussianDistribution (1, 1); %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0:5]), [0, 0.6681, 0.8855, 0.9532, 0.9791, 0.9901], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0, 0.7234, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4]), [0.8108, 0.8855, 0.9532, 0.9791], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4]), [0, 0, 0.7234, 1], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [0, 0.3320, 0.5411, 0.8483, 1.4479, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.1889, 2.4264, 2.7417, 3.1993, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 0.5411, 0.8483, 1.4479, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.4264, 2.7417, 3.1993, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 0.8643, 1e-4); %!assert_equal (iqr (t), 0.8222, 1e-4); %!assert_equal (mean (pd), 1); %!assert_equal (mean (t), 2.6953, 1e-4); %!assert_equal (median (pd), 0.6758, 1e-4); %!assert_equal (median (t), 2.5716, 1e-4); %!assert_equal (pdf (pd, [0:5]), [0, 0.3989, 0.1098, 0.0394, 0.0162, 0.0072], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 1.1736, 0.4211, 0.1730, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1:4, NaN]), [0, 0.3989, 0.1098, 0.0394, 0.0162, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1:4, NaN]), [0, 0, 1.1736, 0.4211, 0.1730, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 1); %!assert_equal (std (t), 0.5332, 1e-4); %!assert_equal (var (pd), 1); %!assert_equal (var (t), 0.2843, 1e-4); %!test %! ## The profile over the first free parameter: 21 grid values, one row of %! ## OTHER per value, and the likelihood peaking at the fitted estimate. %! x = [1.2; 0.4; 3.1; 0.7; 2.5; 1.8; 0.3; 4.2; 1.1; 0.9; ... %! 2.2; 0.6; 1.5; 3.7; 0.8; 2.9; 1.3; 0.5; 2.0; 1.6]; %! pd = fitdist (x, 'InverseGaussian'); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (size (param), [1, 21]); %! assert_equal (size (other), [21, 1]); %! assert_equal (proflik (pd), nlogL); %! [~, imax] = max (nlogL); %! assert_equal (abs (param(imax) - pd.ParameterValues(1)) <= param(2) - param(1), true); ## Test input validation ## 'prob.InverseGaussianDistribution' constructor %!error ... %! prob.InverseGaussianDistribution (0, 1) %!error ... %! prob.InverseGaussianDistribution (Inf, 1) %!error ... %! prob.InverseGaussianDistribution (i, 1) %!error ... %! prob.InverseGaussianDistribution ('a', 1) %!error ... %! prob.InverseGaussianDistribution ([1, 2], 1) %!error ... %! prob.InverseGaussianDistribution (NaN, 1) %!error ... %! prob.InverseGaussianDistribution (1, 0) %!error ... %! prob.InverseGaussianDistribution (1, -1) %!error ... %! prob.InverseGaussianDistribution (1, Inf) %!error ... %! prob.InverseGaussianDistribution (1, i) %!error ... %! prob.InverseGaussianDistribution (1, 'a') %!error ... %! prob.InverseGaussianDistribution (1, [1, 2]) %!error ... %! prob.InverseGaussianDistribution (1, NaN) ## 'cdf' method %!error ... %! cdf (prob.InverseGaussianDistribution, 2, 'uper') %!error ... %! cdf (prob.InverseGaussianDistribution, 2, 3) ## 'paramci' method %!shared x %! x = invgrnd (1, 1, [1, 100]); %!error ... %! paramci (prob.InverseGaussianDistribution.fit (x), 'alpha') %!error ... %! paramci (prob.InverseGaussianDistribution.fit (x), 'alpha', 0) %!error ... %! paramci (prob.InverseGaussianDistribution.fit (x), 'alpha', 1) %!error ... %! paramci (prob.InverseGaussianDistribution.fit (x), 'alpha', [0.5 2]) %!error ... %! paramci (prob.InverseGaussianDistribution.fit (x), 'alpha', '') %!error ... %! paramci (prob.InverseGaussianDistribution.fit (x), 'alpha', {0.05}) %!error ... %! paramci (prob.InverseGaussianDistribution.fit (x), 'parameter', 'mu', ... %! 'alpha', {0.05}) %!error ... %! paramci (prob.InverseGaussianDistribution.fit (x), ... %! 'parameter', {'mu', 'lambda', 'param'}) %!error ... %! paramci (prob.InverseGaussianDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', {'mu', 'lambda', 'param'}) %!error ... %! paramci (prob.InverseGaussianDistribution.fit (x), 'parameter', 'param') %!error ... %! paramci (prob.InverseGaussianDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', 'param') %!error ... %! paramci (prob.InverseGaussianDistribution.fit (x), 'NAME', 'value') %!error ... %! paramci (prob.InverseGaussianDistribution.fit (x), 'alpha', 0.01, 'NAME', 'value') %!error ... %! paramci (prob.InverseGaussianDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', 'mu', 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.InverseGaussianDistribution, 'Parent') %!error ... %! plot (prob.InverseGaussianDistribution, 'PlotType', 12) %!error ... %! plot (prob.InverseGaussianDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.InverseGaussianDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.InverseGaussianDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.InverseGaussianDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.InverseGaussianDistribution, 'Discrete', {true}) %!error ... %! plot (prob.InverseGaussianDistribution, 'Parent', 12) %!error ... %! plot (prob.InverseGaussianDistribution, 'Parent', 'hax') %!error ... %! plot (prob.InverseGaussianDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.InverseGaussianDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.InverseGaussianDistribution, 2) %!error ... %! proflik (prob.InverseGaussianDistribution.fit (x), 3) %!error ... %! proflik (prob.InverseGaussianDistribution.fit (x), [1, 2]) %!error ... %! proflik (prob.InverseGaussianDistribution.fit (x), {1}) %!error ... %! proflik (prob.InverseGaussianDistribution.fit (x), 1, ones (2)) %!error ... %! proflik (prob.InverseGaussianDistribution.fit (x), 1, 'Display') %!error ... %! proflik (prob.InverseGaussianDistribution.fit (x), 1, 'Display', 1) %!error ... %! proflik (prob.InverseGaussianDistribution.fit (x), 1, 'Display', {1}) %!error ... %! proflik (prob.InverseGaussianDistribution.fit (x), 1, 'Display', {'on'}) %!error ... %! proflik (prob.InverseGaussianDistribution.fit (x), 1, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.InverseGaussianDistribution.fit (x), 1, 'Display', 'onnn') %!error ... %! proflik (prob.InverseGaussianDistribution.fit (x), 1, 'NAME', 'on') %!error ... %! proflik (prob.InverseGaussianDistribution.fit (x), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.InverseGaussianDistribution.fit (x), 1, {[1 2 3]}, 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.InverseGaussianDistribution) %!error ... %! truncate (prob.InverseGaussianDistribution, 2) %!error ... %! truncate (prob.InverseGaussianDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.InverseGaussianDistribution (1, 1); %! pd(2) = prob.InverseGaussianDistribution (1, 3); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/KernelDistribution.m000066400000000000000000000734311527311104300274550ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef KernelDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.KernelDistribution ## ## Kernel probability distribution object. ## ## A @code{prob.KernelDistribution} object consists of a nonparametric kernel ## smoothing density estimate fitted to sample data, together with a model ## description. Unlike the parametric distribution objects, it has no ## estimated parameters; the fitted distribution is defined entirely by the ## data, the smoothing kernel, and the bandwidth. ## ## A @code{prob.KernelDistribution} object can only be created by fitting a ## kernel smoothing distribution to data with the @code{fitdist} function. ## Unlike the parametric distributions, it cannot be created with the ## @code{makedist} function, since it is not parametric and requires data. ## ## Further information about the kernel density estimation can be found at ## @url{https://en.wikipedia.org/wiki/Kernel_density_estimation} ## ## @seealso{fitdist, ksdensity, mvksdensity} ## @end deftp properties (GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.KernelDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Kernel'; endproperties properties (Dependent = true) ## -*- texinfo -*- ## @deftp {prob.KernelDistribution} {property} Kernel ## ## Kernel smoothing function ## ## A character vector specifying the type of smoothing kernel used for the ## density estimate. It is one of @qcode{'normal'}, @qcode{'box'}, ## @qcode{'triangle'}, or @qcode{'epanechnikov'}. You can access the ## @qcode{Kernel} property using dot name assignment. ## ## @end deftp Kernel ## -*- texinfo -*- ## @deftp {prob.KernelDistribution} {property} Bandwidth ## ## Bandwidth of the smoothing kernel ## ## A positive scalar value specifying the bandwidth of the smoothing kernel. ## You can access the @qcode{Bandwidth} property using dot name assignment. ## ## @end deftp Bandwidth endproperties properties (GetAccess = public, Constant = true, Hidden) CensoringAllowed = false; DistributionCode = 'kernel'; NumParameters = 0; ParameterNames = {}; ParameterDescription = {}; endproperties properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.KernelDistribution} {property} Support ## ## Support of the probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{range}: either the character vector @qcode{'unbounded'} or ## @qcode{'positive'}, or a two-element numeric vector @math{[L, U]} with ## the lower and upper bounds of the support. ## @item @qcode{closedbound}: a two-element logical vector specifying ## whether each bound is closed. ## @item @qcode{iscontinuous}: a logical scalar, always @qcode{true} for a ## kernel distribution. ## @end itemize ## ## This property is read-only. ## ## @end deftp Support ## -*- texinfo -*- ## @deftp {prob.KernelDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.KernelDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.KernelDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: an empty array, since censoring is not supported for ## a kernel distribution. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties (GetAccess = public, SetAccess = protected, Hidden) KernelName BandwidthValue endproperties methods (Hidden) ## -*- texinfo -*- ## @deftypefn {prob.KernelDistribution} {@var{pd} =} KernelDistribution (@var{data}, @var{kernel}, @var{bw}, @var{support}, @var{freq}) ## @deftypefnx {prob.KernelDistribution} {@var{pd} =} KernelDistribution () ## ## Create a @code{prob.KernelDistribution} object. ## ## @var{data}, @var{kernel}, @var{bw}, @var{support} and @var{freq} are the ## distribution parameters, which the class help describes. Called with no ## arguments it fits the data @code{[0; 1]} with a normal kernel over an ## unbounded support, taking the bandwidth from the data. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = KernelDistribution (data, kernel, bw, support, freq) if (nargin == 0) data = [0; 1]; kernel = 'normal'; support = make_support ('unbounded'); freq = []; bw = default_bandwidth (data, kernel, support, freq); endif this.KernelName = kernel; this.BandwidthValue = bw; this.Support = support; this.IsTruncated = false; this.Truncation = []; this.InputData = struct ('data', data(:), 'cens', [], 'freq', freq); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'kernel distribution'); endfunction function disp (this) __disp__ (this, 'kernel distribution'); endfunction function this = set.Kernel (this, kernel) if (! (ischar (kernel) && isrow (kernel) && any (strcmpi (kernel, ... {'normal', 'box', 'triangle', 'epanechnikov'})))) error (strcat ("KernelDistribution: 'Kernel' must be 'normal',", ... " 'box', 'triangle', or 'epanechnikov'.")); endif this.KernelName = lower (kernel); endfunction function kernel = get.Kernel (this) kernel = this.KernelName; endfunction function this = set.Bandwidth (this, bw) if (! (isnumeric (bw) && isscalar (bw) && isreal (bw) && bw > 0)) error ("KernelDistribution: 'Bandwidth' must be a positive scalar."); endif this.BandwidthValue = bw; endfunction function bw = get.Bandwidth (this) bw = this.BandwidthValue; endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {prob.KernelDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.KernelDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @var{x} must be @qcode{double} or @qcode{single}; integer, logical, and ## character arrays are rejected. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif if (! isfloat (x)) error ("cdf: X must be double or single."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = basecdf (this, x); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; Fab = basecdf (this, [lx, ux]); p(! (lb | ub)) -= Fab(1); p(! (lb | ub)) /= diff (Fab); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.KernelDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @var{p} must be @qcode{double} or @qcode{single}; integer, logical, and ## character arrays are rejected. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (! isfloat (p)) error ("icdf: P must be double or single."); endif if (this.IsTruncated) Fab = basecdf (this, this.Truncation); lp = Fab(1); up = Fab(2); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = baseicdf (this, np); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = baseicdf (this, p); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.KernelDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.KernelDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated || ! is_unbounded (this)) [lb, ub] = integration_limits (this); fm = @(x) x .* pdf (this, x); m = integral (fm, lb, ub, 'ArrayValued', 1); else w = weights (this); m = sum (w .* this.InputData.data(:)'); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.KernelDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif m = icdf (this, 0.5); endfunction ## -*- texinfo -*- ## @deftypefn {prob.KernelDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif data = this.InputData.data(:); f = pdf (this, data); if (isempty (this.InputData.freq)) nlogL = - sum (log (f)); else nlogL = - sum (this.InputData.freq(:) .* log (f)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.KernelDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability density function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @var{x} must be @qcode{double} or @qcode{single}; integer, logical, and ## character arrays are rejected. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif if (! isfloat (x)) error ("pdf: X must be double or single."); endif y = basepdf (this, x); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (basecdf (this, [lx, ux])); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.KernelDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.KernelDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.KernelDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}, superimposed over a histogram ## of the data used to fit it. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.82 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF) ## superimposed on a histogram of the data. @qcode{'cdf'} plots the ## cumulative distribution function (CDF) superimposed over an empirical ## CDF. @qcode{'probability'} plots a probability plot using a CDF of the ## data and a CDF of the fitted probability distribution. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If not ## specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.KernelDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.KernelDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.KernelDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.KernelDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{random} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif u = unifrnd (0, 1, varargin{:}); r = icdf (this, u); endfunction ## -*- texinfo -*- ## @deftypefn {prob.KernelDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif s = sqrt (var (this)); endfunction ## -*- texinfo -*- ## @deftypefn {prob.KernelDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, and upper limit, @var{upper}. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; endfunction ## -*- texinfo -*- ## @deftypefn {prob.KernelDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated || ! is_unbounded (this)) [lb, ub] = integration_limits (this); m = mean (this); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, lb, ub, 'ArrayValued', 1); else w = weights (this); data = this.InputData.data(:)'; mu = sum (w .* data); v = sum (w .* (data - mu) .^ 2) + this.Bandwidth ^ 2; endif endfunction endmethods methods (Access = private) ## Untruncated PDF, delegating to the shipped ksdensity engine. The query ## values are flattened to a vector for ksdensity and reshaped back, since ## ksdensity only accepts vector query points. function y = basepdf (this, x) ## ksdensity reads an empty query as "no points given" and falls back to ## its default grid, so short-circuit before it is called. if (isempty (x)) y = zeros (size (x)); return; endif y = reshape (ksdensity (this.InputData.data, x(:), ksargs (this){:}, ... 'Function', 'pdf'), size (x)); endfunction ## Untruncated CDF, delegating to the shipped ksdensity engine. function p = basecdf (this, x) if (isempty (x)) p = zeros (size (x)); return; endif p = reshape (ksdensity (this.InputData.data, x(:), ksargs (this){:}, ... 'Function', 'cdf'), size (x)); endfunction ## Untruncated inverse CDF, delegating to the shipped ksdensity engine. function x = baseicdf (this, p) if (isempty (p)) x = zeros (size (p)); return; endif x = reshape (ksdensity (this.InputData.data, p(:), ksargs (this){:}, ... 'Function', 'icdf'), size (p)); endfunction ## Common Name-Value arguments forwarded to ksdensity. function args = ksargs (this) args = {'Kernel', this.Kernel, 'Bandwidth', this.Bandwidth, ... 'Support', support_arg(this.Support)}; if (! isempty (this.InputData.freq)) args = [args, {'Weights', this.InputData.freq}]; endif endfunction ## Normalised weights of the sample points. function w = weights (this) n = numel (this.InputData.data); if (isempty (this.InputData.freq)) w = ones (1, n) / n; else f = this.InputData.freq(:)'; w = f / sum (f); endif endfunction ## Finite integration limits for numeric moment computation. function [lb, ub] = integration_limits (this) if (this.IsTruncated) lb = this.Truncation(1); ub = this.Truncation(2); else lb = baseicdf (this, 1e-10); ub = baseicdf (this, 1 - 1e-10); endif endfunction ## True for an unbounded (whole real line) support. function tf = is_unbounded (this) tf = ischar (this.Support.range) ... && strcmp (this.Support.range, 'unbounded'); endfunction endmethods methods (Static, Hidden) function pd = fit (x, kernel, support, width, freq) if (nargin < 2 || isempty (kernel)) kernel = 'normal'; endif if (nargin < 3 || isempty (support)) support = 'unbounded'; endif if (nargin < 4) width = []; endif if (nargin < 5) freq = []; endif ## A trivial (all-ones) frequency vector is treated as no weighting if (! isempty (freq) && all (freq(:) == 1)) freq = []; endif ## Validate the kernel if (! (ischar (kernel) && isrow (kernel) && any (strcmpi (kernel, ... {'normal', 'box', 'triangle', 'epanechnikov'})))) error (strcat ("KernelDistribution: 'Kernel' must be 'normal',", ... " 'box', 'triangle', or 'epanechnikov'.")); endif kernel = lower (kernel); S = make_support (support); ## Bandwidth: use the supplied value or the ksdensity default rule if (isempty (width)) bw = default_bandwidth (x, kernel, S, freq); elseif (isnumeric (width) && isscalar (width) && isreal (width) && width > 0) bw = width; else error ("KernelDistribution: 'Width' must be a positive scalar."); endif ## Create fitted distribution object pd = prob.KernelDistribution (x, kernel, bw, S, freq); endfunction endmethods endclassdef ## Build the Support structure from a keyword or a two-element vector. function S = make_support (support) if (ischar (support) && isrow (support)) switch (lower (support)) case 'unbounded' S = struct ('range', 'unbounded', 'closedbound', ... [false, false], 'iscontinuous', true); case 'positive' S = struct ('range', 'positive', 'closedbound', ... [false, false], 'iscontinuous', true); otherwise error (strcat ("KernelDistribution: 'Support' must be", ... " 'unbounded', 'positive', or [L U].")); endswitch elseif (isnumeric (support) && isreal (support) && numel (support) == 2 && support(1) < support(2)) S = struct ('range', [support(1), support(2)], 'closedbound', ... [false, false], 'iscontinuous', true); else error (strcat ("KernelDistribution: 'Support' must be", ... " 'unbounded', 'positive', or [L U].")); endif endfunction ## Convert a Support structure to the argument expected by ksdensity. function arg = support_arg (S) if (ischar (S.range)) arg = S.range; else arg = S.range; endif endfunction ## Default bandwidth via the ksdensity normal-reference rule. function bw = default_bandwidth (x, kernel, S, freq) args = {'Kernel', kernel, 'Support', support_arg(S)}; if (! isempty (freq)) args = [args, {'Weights', freq}]; endif [~, ~, bw] = ksdensity (x, args{:}); endfunction %!demo %! ## Fit a kernel distribution to a sample and plot its PDF over a histogram. %! load patients %! pd = fitdist (Weight, 'Kernel'); %! plot (pd) %! title ('Kernel distribution fitted to patient weights') ## Test output %!shared x, pd, pdbox, t %! x = [2.1 0.3 1.2 -0.7 0.9 1.5 2.8 0.1 0.4 1.1 3.2 0.6 2.0 0.9 1.7]'; %! pd = fitdist (x, 'Kernel'); %! pdbox = fitdist (x, 'Kernel', 'Kernel', 'box', 'Width', 0.5); %! t = truncate (pd, 0, 3); %!assert_equal (pd.DistributionName, 'Kernel'); %!assert_equal (pd.Kernel, 'normal'); %!assert_equal (pd.NumParameters, 0); %!assert_equal (pd.ParameterNames, {}); %!assert_equal (pd.IsTruncated, false); %!assert_equal (pd.Bandwidth, 0.639566, 1e-4); %!assert_equal (pd.Support.range, 'unbounded'); %!assert_equal (pdf (pd, [-1 0 0.5 1 1.5 2 2.5 3]), ... %! [0.0589 0.2141 0.3051 0.3394 0.3061 0.2375 0.1697 0.1166], 2e-3); %!assert_equal (cdf (pd, [-1 0 0.5 1 1.5 2 2.5 3]), ... %! [0.0272 0.1538 0.2850 0.4492 0.6128 0.7494 0.8506 0.9217], 2e-3); %!assert_equal (icdf (pd, [0.1 0.25 0.5 0.75 0.9]), ... %! [-0.2909 0.3820 1.1504 2.0027 2.8265], 5e-3); %!assert_equal (size (pdf (pd, [])), [0, 0]) %!error pdf (pd, int32 (1)) %!error pdf (pd, true) %!error pdf (pd, 'a') %!error cdf (pd, int32 (1)) %!error icdf (pd, int32 (1)) %!assert_equal (size (cdf (pd, [])), [0, 0]) %!assert_equal (size (icdf (pd, [])), [0, 0]) %!assert_equal (size (random (pd, -1)), [0, 0]) %!assert_equal (size (random (pd, 2, -1, 5)), [2, 0, 5]) %!assert_equal (mean (pd), 1.2067, 1e-4); %!assert_equal (std (pd), 1.1918, 1e-3); %!assert_equal (var (pd), 1.4203, 1e-3); %!assert_equal (median (pd), 1.1504, 5e-3); %!assert_equal (iqr (pd), 1.6207, 5e-3); %!assert_equal (negloglik (pd), 21.5835, 1e-3); %!assert_equal (pdbox.Kernel, 'box'); %!assert_equal (pdbox.Bandwidth, 0.5); %!assert_equal (pdf (pdbox, [0 1 2]), [0.1925 0.3464 0.2309], 2e-3); %!assert_equal (pdf (t, [-1 0 1 2 3 4]), ... %! [0 0.2788 0.4420 0.3093 0.1518 0], 2e-3); %!assert_equal (cdf (t, [-1 0 1 2 3 4]), ... %! [0 0 0.3846 0.7755 1 1], 2e-3); %!assert_equal (mean (t), 1.3293, 1e-3); %!test ## positive support (log boundary correction) %! y = [0.2 0.5 0.7 1.1 1.4 2 2.6 3.3 4.1 5.5]'; %! pdpos = fitdist (y, 'Kernel', 'Support', 'positive'); %! assert_equal (pdpos.Bandwidth, 0.768201, 1e-4); %! assert_equal (pdpos.Support.range, 'positive'); %! assert_equal (pdf (pdpos, [0.1 0.5 1 2 3 5]), ... %! [0.4302 0.3919 0.2738 0.1572 0.0999 0.0463], 2e-3); %!test ## random values honour truncation bounds %! x = [2.1 0.3 1.2 -0.7 0.9 1.5 2.8 0.1 0.4 1.1 3.2 0.6 2.0 0.9 1.7]'; %! pd = fitdist (x, 'Kernel'); %! t = truncate (pd, 0, 3); %! r = random (t, 1000, 1); %! assert_equal (all (r >= 0 & r <= 3), true); %! assert_equal (size (random (pd, 10, 5)), [10, 5]); ## Test input validation %!error ... %! cdf (fitdist ([1 2 3 4 5]', 'Kernel'), 2, 'uper') %!error ... %! truncate (fitdist ([1 2 3 4 5]', 'Kernel')) %!error ... %! truncate (fitdist ([1 2 3 4 5]', 'Kernel'), 4, 2) %!error ... %! fitdist ([1 2 3 4 5]', 'Kernel', 'Kernel', 'cosine') %!error ... %! fitdist ([1 2 3 4 5]', 'Kernel', 'Support', 'half') %!error ... %! fitdist ([1 2 3 4 5]', 'Kernel', 'Width', -1) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.KernelDistribution (); %! pd(2) = prob.KernelDistribution (); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error pdf (pd, 1) %!error plot (pd) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/LogisticDistribution.m000066400000000000000000001200571527311104300300070ustar00rootroot00000000000000## Copyright (C) 2024-2025 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef LogisticDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.LogisticDistribution ## ## Logistic probability distribution object. ## ## A @code{prob.LogisticDistribution} object consists of parameters, a model ## description, and sample data for a logistic probability distribution. ## ## The logistic distribution is a continuous probability distribution, which ## is commonly used in logistic regression and feedforward neural networks. ## It is defined by location parameter @var{mu} and scale parameter ## @var{sigma}. ## ## There are several ways to create a @code{prob.LogisticDistribution} ## object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor @qcode{prob.LogisticDistribution (@var{mu}, ## @var{sigma})} to create a logistic distribution with fixed parameter values ## @var{mu} and @var{sigma}. ## @item Use the static method @qcode{prob.LogisticDistribution.fit (@var{x}, ## @var{alpha}, @var{censor}, @var{freq}, @var{options})} to fit a ## distribution to the data in @var{x} using the same input arguments as the ## @code{logifit} function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the logistic distribution can be found at ## @url{https://en.wikipedia.org/wiki/Logistic_distribution} ## ## @seealso{fitdist, makedist, logicdf, logiinv, logipdf, logirnd, logifit, ## logilike, logistat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.LogisticDistribution} {property} mu ## ## Location parameter ## ## A scalar value characterizing the location of the ## logistic distribution. You can access the @qcode{mu} ## property using dot name assignment. ## ## @end deftp mu ## -*- texinfo -*- ## @deftp {prob.LogisticDistribution} {property} sigma ## ## Scale parameter ## ## A positive scalar value characterizing the scale of the ## logistic distribution. You can access the @qcode{sigma} ## property using dot name assignment. ## ## @end deftp sigma endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.LogisticDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Logistic'; ## -*- texinfo -*- ## @deftp {prob.LogisticDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 2; ## -*- texinfo -*- ## @deftp {prob.LogisticDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'mu', 'sigma'}; ## -*- texinfo -*- ## @deftp {prob.LogisticDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Location', 'Scale'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = true; DistributionCode = 'logi'; ParameterRange = [-Inf, realmin; Inf, Inf]; ParameterLogCI = [true, true]; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.LogisticDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{2*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{mu} and @qcode{sigma} ## properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.LogisticDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.LogisticDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.LogisticDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{2*2} numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter, and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only meaningful ## when the distribution was fitted to data. If the distribution object was ## created with fixed parameters, or a parameter of a fitted distribution is ## modified, then all elements of the variance-covariance are zero. This ## property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.LogisticDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*2} logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.LogisticDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: a numeric vector of logical values indicating ## censoring information corresponding to the elements of the data used for ## distribution fitting. If no censoring vector was used for distribution ## fitting, then this field defaults to an empty array. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.LogisticDistribution} {@var{pd} =} LogisticDistribution (@var{mu}, @var{sigma}) ## @deftypefnx {prob.LogisticDistribution} {@var{pd} =} LogisticDistribution () ## ## Create a @code{prob.LogisticDistribution} object. ## ## @var{mu} and @var{sigma} are the distribution parameters, which the class ## help describes. Called with no arguments the parameters take their ## defaults, @var{mu} 0 and @var{sigma} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = LogisticDistribution (mu, sigma) if (nargin == 0) mu = 0; sigma = 1; endif checkparams (mu, sigma); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [mu, sigma]; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'logistic distribution'); endfunction function disp (this) __disp__ (this, 'logistic distribution'); endfunction function this = set.mu (this, mu) checkparams (mu, this.sigma); this.InputData = []; this.ParameterValues(1) = mu; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function mu = get.mu (this) mu = this.ParameterValues(1); endfunction function this = set.sigma (this, sigma) checkparams (this.mu, sigma); this.InputData = []; this.ParameterValues(2) = sigma; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function sigma = get.sigma (this) sigma = this.ParameterValues(2); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.LogisticDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.LogisticDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = logicdf (x, this.mu, this.sigma); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= logicdf (lx, this.mu, this.sigma); p(! (lb | ub)) /= diff (logicdf ([lx, ux], this.mu, this.sigma)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LogisticDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = logicdf (this.Truncation(1), this.mu, this.sigma); up = logicdf (this.Truncation(2), this.mu, this.sigma); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = logiinv (np, this.mu, this.sigma); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = logiinv (p, this.mu, this.sigma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LogisticDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LogisticDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = logistat (this.mu, this.sigma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LogisticDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = logicdf ([lx, ux], this.mu, this.sigma); m = logiinv (sum (Fa_b) / 2, this.mu, this.sigma); else m = this.mu; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LogisticDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = logilike ([this.mu, this.sigma], this.InputData.data, ... this.InputData.cens, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LogisticDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.LogisticDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the ## confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LogisticDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = logipdf (x, this.mu, this.sigma); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (logicdf ([lx, ux], this.mu, this.sigma)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LogisticDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.LogisticDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.LogisticDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LogisticDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.LogisticDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.LogisticDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.LogisticDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.LogisticDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.LogisticDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the logistic distribution, @qcode{@var{pnum} = 1} selects ## the parameter @qcode{mu} and @qcode{@var{pnum} = 2} selects the ## parameter @qcode{sigma}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LogisticDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.LogisticDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.LogisticDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.LogisticDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{bisarnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = logicdf (this.Truncation(1), this.mu, this.sigma); up = logicdf (this.Truncation(2), this.mu, this.sigma); u = unifrnd (lp, up, varargin{:}); r = - log (1 ./ u - 1) .* this.sigma + this.mu; else r = logirnd (this.mu, this.sigma, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LogisticDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LogisticDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, ## and upper limit, @var{upper}. If @var{pd} is fitted to data with ## @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LogisticDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = logistat (this.mu, this.sigma); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) censor = []; else censor = varargin{2}; endif if (nargin < 4) freq = []; else freq = varargin{3}; endif if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else options = varargin{4}; endif ## Fit data [phat, pci] = logifit (x, alpha, censor, freq, options); [~, acov] = logilike (phat, x, censor, freq); ## Create fitted distribution object pd = prob.LogisticDistribution.makeFitted ... (phat, pci, acov, x, censor, freq); endfunction function pd = makeFitted (phat, pci, acov, x, censor, freq) mu = phat(1); sigma = phat(2); pd = prob.LogisticDistribution (mu, sigma); pd.ParameterCI = pci; pd.ParameterIsFixed = [false, false]; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', censor, 'freq', freq); endfunction endmethods endclassdef function checkparams (mu, sigma) if (! (isscalar (mu) && isnumeric (mu) && isreal (mu) && isfinite (mu))) error ("LogisticDistribution: MU must be a finite real scalar.") endif if (! (isscalar (sigma) && isnumeric (sigma) && isreal (sigma) && isfinite (sigma) && sigma > 0)) error ("LogisticDistribution: SIGMA must be a positive real scalar.") endif endfunction ## Test output %!shared pd, t %! pd = prob.LogisticDistribution (0, 1); %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0:5]), [0.5, 0.7311, 0.8808, 0.9526, 0.9820, 0.9933], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0, 0.7091, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4]), [0.8176, 0.8808, 0.9526, 0.9820], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4]), [0, 0, 0.7091, 1], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [-Inf, -1.3863, -0.4055, 0.4055, 1.3863, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.2088, 2.4599, 2.7789, 3.2252, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, -0.4055, 0.4055, 1.3863, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.4599, 2.7789, 3.2252, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 2.1972, 1e-4); %!assert_equal (iqr (t), 0.8286, 1e-4); %!assert_equal (mean (pd), 0, 1e-4); %!assert_equal (mean (t), 2.7193, 1e-4); %!assert_equal (median (pd), 0); %!assert_equal (median (t), 2.6085, 1e-4); %!assert_equal (pdf (pd, [0:5]), [0.25, 0.1966, 0.1050, 0.0452, 0.0177, 0.0066], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 1.0373, 0.4463, 0.1745, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1:4, NaN]), [0.1966, 0.1966, 0.1050, 0.0452, 0.0177, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1:4, NaN]), [0, 0, 1.0373, 0.4463, 0.1745, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 1.8138, 1e-4); %!assert_equal (std (t), 0.5320, 1e-4); %!assert_equal (var (pd), 3.2899, 1e-4); %!assert_equal (var (t), 0.2830, 1e-4); %!test %! ## The profile over the first free parameter: 21 grid values, one row of %! ## OTHER per value, and the likelihood peaking at the fitted estimate. %! x = [0.3; -1.2; 0.8; 1.5; -0.4; 0.2; -0.9; 1.1; 0.6; -0.3; ... %! 1.8; -1.5; 0.4; 0.9; -0.7; 1.2; -0.2; 0.5; -1.1; 0.7]; %! pd = fitdist (x, 'Logistic'); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (size (param), [1, 21]); %! assert_equal (size (other), [21, 1]); %! assert_equal (proflik (pd), nlogL); %! [~, imax] = max (nlogL); %! assert_equal (abs (param(imax) - pd.ParameterValues(1)) <= param(2) - param(1), true); ## Test input validation ## 'prob.LogisticDistribution' constructor %!error ... %! prob.LogisticDistribution (Inf, 1) %!error ... %! prob.LogisticDistribution (i, 1) %!error ... %! prob.LogisticDistribution ('a', 1) %!error ... %! prob.LogisticDistribution ([1, 2], 1) %!error ... %! prob.LogisticDistribution (NaN, 1) %!error ... %! prob.LogisticDistribution (1, 0) %!error ... %! prob.LogisticDistribution (1, -1) %!error ... %! prob.LogisticDistribution (1, Inf) %!error ... %! prob.LogisticDistribution (1, i) %!error ... %! prob.LogisticDistribution (1, 'a') %!error ... %! prob.LogisticDistribution (1, [1, 2]) %!error ... %! prob.LogisticDistribution (1, NaN) ## 'cdf' method %!error ... %! cdf (prob.LogisticDistribution, 2, 'uper') %!error ... %! cdf (prob.LogisticDistribution, 2, 3) ## 'paramci' method %!shared x %! x = logirnd (1, 1, [1, 100]); %!error ... %! paramci (prob.LogisticDistribution.fit (x), 'alpha') %!error ... %! paramci (prob.LogisticDistribution.fit (x), 'alpha', 0) %!error ... %! paramci (prob.LogisticDistribution.fit (x), 'alpha', 1) %!error ... %! paramci (prob.LogisticDistribution.fit (x), 'alpha', [0.5 2]) %!error ... %! paramci (prob.LogisticDistribution.fit (x), 'alpha', '') %!error ... %! paramci (prob.LogisticDistribution.fit (x), 'alpha', {0.05}) %!error ... %! paramci (prob.LogisticDistribution.fit (x), 'parameter', 'mu', 'alpha', {0.05}) %!error ... %! paramci (prob.LogisticDistribution.fit (x), 'parameter', {'mu', 'sigma', 'param'}) %!error ... %! paramci (prob.LogisticDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', {'mu', 'sigma', 'param'}) %!error ... %! paramci (prob.LogisticDistribution.fit (x), 'parameter', 'param') %!error ... %! paramci (prob.LogisticDistribution.fit (x), 'alpha', 0.01, 'parameter', 'param') %!error ... %! paramci (prob.LogisticDistribution.fit (x), 'NAME', 'value') %!error ... %! paramci (prob.LogisticDistribution.fit (x), 'alpha', 0.01, 'NAME', 'value') %!error ... %! paramci (prob.LogisticDistribution.fit (x), 'alpha', 0.01, 'parameter', 'mu', ... %! 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.LogisticDistribution, 'Parent') %!error ... %! plot (prob.LogisticDistribution, 'PlotType', 12) %!error ... %! plot (prob.LogisticDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.LogisticDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.LogisticDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.LogisticDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.LogisticDistribution, 'Discrete', {true}) %!error ... %! plot (prob.LogisticDistribution, 'Parent', 12) %!error ... %! plot (prob.LogisticDistribution, 'Parent', 'hax') %!error ... %! plot (prob.LogisticDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.LogisticDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.LogisticDistribution, 2) %!error ... %! proflik (prob.LogisticDistribution.fit (x), 3) %!error ... %! proflik (prob.LogisticDistribution.fit (x), [1, 2]) %!error ... %! proflik (prob.LogisticDistribution.fit (x), {1}) %!error ... %! proflik (prob.LogisticDistribution.fit (x), 1, ones (2)) %!error ... %! proflik (prob.LogisticDistribution.fit (x), 1, 'Display') %!error ... %! proflik (prob.LogisticDistribution.fit (x), 1, 'Display', 1) %!error ... %! proflik (prob.LogisticDistribution.fit (x), 1, 'Display', {1}) %!error ... %! proflik (prob.LogisticDistribution.fit (x), 1, 'Display', {'on'}) %!error ... %! proflik (prob.LogisticDistribution.fit (x), 1, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.LogisticDistribution.fit (x), 1, 'Display', 'onnn') %!error ... %! proflik (prob.LogisticDistribution.fit (x), 1, 'NAME', 'on') %!error ... %! proflik (prob.LogisticDistribution.fit (x), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.LogisticDistribution.fit (x), 1, {[1 2 3 4]}, 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.LogisticDistribution) %!error ... %! truncate (prob.LogisticDistribution, 2) %!error ... %! truncate (prob.LogisticDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.LogisticDistribution (1, 1); %! pd(2) = prob.LogisticDistribution (1, 3); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/LoglogisticDistribution.m000066400000000000000000001224241527311104300305110ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef LoglogisticDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.LoglogisticDistribution ## ## Log-logistic probability distribution object. ## ## A @code{prob.LoglogisticDistribution} object consists of parameters, a ## model description, and sample data for a log-logistic probability ## distribution. ## ## The log-logistic distribution is a continuous probability distribution that ## models non-negative random variables whose logarithm follows the logistic ## distribution. It is defined by location parameter @var{mu} and scale ## parameter @var{sigma}. ## ## There are several ways to create a @code{prob.LoglogisticDistribution} ## object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor @qcode{prob.LoglogisticDistribution (@var{mu}, ## @var{sigma})} to create a log-logistic distribution with fixed parameter ## values @var{mu} and @var{sigma}. ## @item Use the static method @qcode{prob.LoglogisticDistribution.fit ## (@var{x}, @var{censor}, @var{freq}, @var{options})} to fit a distribution ## to the data in @var{x} using the same input arguments as the @code{loglfit} ## function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the log-logistic distribution can be found at ## @url{https://en.wikipedia.org/wiki/Log-logistic_distribution} ## ## @seealso{fitdist, makedist, loglcdf, loglinv, loglpdf, loglrnd, loglfit, ## logllike, loglstat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.LoglogisticDistribution} {property} mu ## ## Mean of logarithmic values ## ## A scalar value characterizing the mean of the logarithmic values of the ## log-logistic distribution. You can access the @qcode{mu} ## property using dot name assignment. ## ## @end deftp mu ## -*- texinfo -*- ## @deftp {prob.LoglogisticDistribution} {property} sigma ## ## Scale of logarithmic values ## ## A positive scalar value characterizing the scale of the logarithmic ## values of the log-logistic distribution. You can access the @qcode{sigma} ## property using dot name assignment. ## ## @end deftp sigma endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.LoglogisticDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Log-Logistic'; ## -*- texinfo -*- ## @deftp {prob.LoglogisticDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 2; ## -*- texinfo -*- ## @deftp {prob.LoglogisticDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'mu', 'sigma'}; ## -*- texinfo -*- ## @deftp {prob.LoglogisticDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Mean of logarithmic values', ... 'Scale of logarithmic values'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = true; DistributionCode = 'logl'; ParameterRange = [0, realmin; Inf, Inf]; ParameterLogCI = [true, true]; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.LoglogisticDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{2*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{mu} and @qcode{sigma} ## properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.LoglogisticDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.LoglogisticDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.LoglogisticDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{2*2} numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter, and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only ## meaningful when the distribution was fitted to data. If the distribution ## object was created with fixed parameters, or a parameter of a fitted ## distribution is modified, then all elements of the variance-covariance ## are zero. This property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.LoglogisticDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*2} logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.LoglogisticDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: a numeric vector of logical values indicating ## censoring information corresponding to the elements of the data used for ## distribution fitting. If no censoring vector was used for distribution ## fitting, then this field defaults to an empty array. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.LoglogisticDistribution} {@var{pd} =} LoglogisticDistribution (@var{mu}, @var{sigma}) ## @deftypefnx {prob.LoglogisticDistribution} {@var{pd} =} LoglogisticDistribution () ## ## Create a @code{prob.LoglogisticDistribution} object. ## ## @var{mu} and @var{sigma} are the distribution parameters, which the class ## help describes. Called with no arguments the parameters take their ## defaults, @var{mu} 0 and @var{sigma} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = LoglogisticDistribution (mu, sigma) if (nargin == 0) mu = 0; sigma = 1; endif checkparams (mu, sigma); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [mu, sigma]; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'Log-Logistic distribution'); endfunction function disp (this) __disp__ (this, 'Log-Logistic distribution'); endfunction function this = set.mu (this, mu) checkparams (mu, this.sigma); this.InputData = []; this.ParameterValues(1) = mu; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function mu = get.mu (this) mu = this.ParameterValues(1); endfunction function this = set.sigma (this, sigma) checkparams (this.mu, sigma); this.InputData = []; this.ParameterValues(2) = sigma; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function sigma = get.sigma (this) sigma = this.ParameterValues(2); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.LoglogisticDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.LoglogisticDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = loglcdf (x, this.mu, this.sigma); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= loglcdf (lx, this.mu, this.sigma); p(! (lb | ub)) /= diff (loglcdf ([lx, ux], this.mu, this.sigma)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoglogisticDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = loglcdf (this.Truncation(1), this.mu, this.sigma); up = loglcdf (this.Truncation(2), this.mu, this.sigma); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = loglinv (np, this.mu, this.sigma); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = loglinv (p, this.mu, this.sigma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoglogisticDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoglogisticDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = loglstat (this.mu, this.sigma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoglogisticDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = loglcdf ([lx, ux], this.mu, this.sigma); m = loglinv (sum (Fa_b) / 2, this.mu, this.sigma); else m = exp (this.mu); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoglogisticDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = logllike ([this.mu, this.sigma], this.InputData.data, ... this.InputData.cens, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoglogisticDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.LoglogisticDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoglogisticDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = loglpdf (x, this.mu, this.sigma); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (loglcdf ([lx, ux], this.mu, this.sigma)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoglogisticDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.LoglogisticDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.LoglogisticDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoglogisticDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.LoglogisticDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.LoglogisticDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.LoglogisticDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.LoglogisticDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.LoglogisticDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the Log-logistic distribution, @qcode{@var{pnum} = 1} selects ## the parameter @qcode{mu} and @qcode{@var{pnum} = 2} selects the ## parameter @qcode{sigma}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoglogisticDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.LoglogisticDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.LoglogisticDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.LoglogisticDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{random} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif a = exp (this.mu); b = 1 / this.sigma; if (this.IsTruncated) lp = loglcdf (this.Truncation(1), this.mu, this.sigma); up = loglcdf (this.Truncation(2), this.mu, this.sigma); u = unifrnd (lp, up, varargin{:}); r = exp (this.mu) .* (u ./ (1 - u)) .^ (this.sigma); else r = loglrnd (this.mu, this.sigma, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoglogisticDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoglogisticDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, and upper limit, @var{upper}. If @var{pd} is fitted to data ## with @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoglogisticDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = loglstat (this.mu, this.sigma); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) censor = []; else censor = varargin{2}; endif if (nargin < 4) freq = []; else freq = varargin{3}; endif if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else options = varargin{4}; endif ## Fit data [phat, pci] = loglfit (x, alpha, censor, freq, options); [~, acov] = logllike (phat, x, censor, freq); ## Create fitted distribution object pd = prob.LoglogisticDistribution.makeFitted ... (phat, pci, acov, x, censor, freq); endfunction function pd = makeFitted (phat, pci, acov, x, censor, freq) mu = phat(1); sigma = phat(2); pd = prob.LoglogisticDistribution (mu, sigma); pd.ParameterCI = pci; pd.ParameterIsFixed = [false, false]; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', censor, 'freq', freq); endfunction endmethods endclassdef function checkparams (mu, sigma) if (! (isscalar (mu) && isnumeric (mu) && isreal (mu) && isfinite (mu) && mu >= 0)) error ("LoglogisticDistribution: MU must be a nonnegative real scalar.") endif if (! (isscalar (sigma) && isnumeric (sigma) && isreal (sigma) && isfinite (sigma) && sigma > 0)) error ("LoglogisticDistribution: SIGMA must be a positive real scalar.") endif endfunction %!demo %! ## Generate a data set of 5000 random samples from a Log-logistic %! ## distribution with parameters mu = 0 and sigma = 1. Fit a Log-logistic %! ## distribution to this data and plot a PDF of the fitted distribution %! ## superimposed on a histogram of the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ('Loglogistic', 'mu', 0, 'sigma', 1) %! data = random (pd_fixed, 5000, 1); %! pd_fitted = fitdist (data, 'Loglogistic') %! plot (pd_fitted) %! msg = 'Fitted Log-logistic distribution with mu = %0.2f and sigma = %0.2f'; %! title (sprintf (msg, pd_fitted.mu, pd_fitted.sigma)) ## Test output %!shared pd, t %! pd = prob.LoglogisticDistribution; %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0:5]), [0, 0.5, 0.6667, 0.75, 0.8, 0.8333], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0, 0.625, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4]), [0.6, 0.6667, 0.75, 0.8], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4]), [0, 0, 0.625, 1], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [0, 0.25, 0.6667, 1.5, 4, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.2609, 2.5714, 2.9474, 3.4118, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 0.6667, 1.5, 4, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.5714, 2.9474, 3.4118, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 2.6667, 1e-4); %!assert_equal (iqr (t), 0.9524, 1e-4); %!assert_equal (mean (pd), Inf); %!assert_equal (mean (t), 2.8312, 1e-4); %!assert_equal (median (pd), 1, 1e-4); %!assert_equal (median (t), 2.75, 1e-4); %!assert_equal (pdf (pd, [0:5]), [0, 0.25, 0.1111, 0.0625, 0.04, 0.0278], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 0.8333, 0.4687, 0.3, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1:4, NaN]), [0, 0.25, 0.1111, 0.0625, 0.04, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1:4, NaN]), [0, 0, 0.8333, 0.4687, 0.3, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), Inf); %!assert_equal (std (t), 0.5674, 1e-4); %!assert_equal (var (pd), Inf); %!assert_equal (var (t), 0.3220, 1e-4); %!test %! ## The profile over the first free parameter: 21 grid values, one row of %! ## OTHER per value, and the likelihood peaking at the fitted estimate. %! x = [1.2; 0.4; 3.1; 0.7; 2.5; 1.8; 0.3; 4.2; 1.1; 0.9; ... %! 2.2; 0.6; 1.5; 3.7; 0.8; 2.9; 1.3; 0.5; 2.0; 1.6]; %! pd = fitdist (x, 'Loglogistic'); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (size (param), [1, 21]); %! assert_equal (size (other), [21, 1]); %! assert_equal (proflik (pd), nlogL); %! [~, imax] = max (nlogL); %! assert_equal (abs (param(imax) - pd.ParameterValues(1)) <= param(2) - param(1), true); ## Test input validation ## 'prob.LoglogisticDistribution' constructor %!error ... %! prob.LoglogisticDistribution (Inf, 1) %!error ... %! prob.LoglogisticDistribution (i, 1) %!error ... %! prob.LoglogisticDistribution ('a', 1) %!error ... %! prob.LoglogisticDistribution ([1, 2], 1) %!error ... %! prob.LoglogisticDistribution (NaN, 1) %!error ... %! prob.LoglogisticDistribution (1, 0) %!error ... %! prob.LoglogisticDistribution (1, -1) %!error ... %! prob.LoglogisticDistribution (1, Inf) %!error ... %! prob.LoglogisticDistribution (1, i) %!error ... %! prob.LoglogisticDistribution (1, 'a') %!error ... %! prob.LoglogisticDistribution (1, [1, 2]) %!error ... %! prob.LoglogisticDistribution (1, NaN) ## 'cdf' method %!error ... %! cdf (prob.LoglogisticDistribution, 2, 'uper') %!error ... %! cdf (prob.LoglogisticDistribution, 2, 3) ## 'paramci' method %!shared x %! x = loglrnd (1, 1, [1, 100]); %!error ... %! paramci (prob.LoglogisticDistribution.fit (x), 'alpha') %!error ... %! paramci (prob.LoglogisticDistribution.fit (x), 'alpha', 0) %!error ... %! paramci (prob.LoglogisticDistribution.fit (x), 'alpha', 1) %!error ... %! paramci (prob.LoglogisticDistribution.fit (x), 'alpha', [0.5 2]) %!error ... %! paramci (prob.LoglogisticDistribution.fit (x), 'alpha', '') %!error ... %! paramci (prob.LoglogisticDistribution.fit (x), 'alpha', {0.05}) %!error ... %! paramci (prob.LoglogisticDistribution.fit (x), 'parameter', 'mu', 'alpha', {0.05}) %!error ... %! paramci (prob.LoglogisticDistribution.fit (x), 'parameter', {'mu', 'sigma', 'pa'}) %!error ... %! paramci (prob.LoglogisticDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', {'mu', 'sigma', 'param'}) %!error ... %! paramci (prob.LoglogisticDistribution.fit (x), 'parameter', 'param') %!error ... %! paramci (prob.LoglogisticDistribution.fit (x), 'alpha', 0.01, 'parameter', 'parm') %!error ... %! paramci (prob.LoglogisticDistribution.fit (x), 'NAME', 'value') %!error ... %! paramci (prob.LoglogisticDistribution.fit (x), 'alpha', 0.01, 'NAME', 'value') %!error ... %! paramci (prob.LoglogisticDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', 'mu', 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.LoglogisticDistribution, 'Parent') %!error ... %! plot (prob.LoglogisticDistribution, 'PlotType', 12) %!error ... %! plot (prob.LoglogisticDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.LoglogisticDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.LoglogisticDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.LoglogisticDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.LoglogisticDistribution, 'Discrete', {true}) %!error ... %! plot (prob.LoglogisticDistribution, 'Parent', 12) %!error ... %! plot (prob.LoglogisticDistribution, 'Parent', 'hax') %!error ... %! plot (prob.LoglogisticDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.LoglogisticDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.LoglogisticDistribution, 2) %!error ... %! proflik (prob.LoglogisticDistribution.fit (x), 3) %!error ... %! proflik (prob.LoglogisticDistribution.fit (x), [1, 2]) %!error ... %! proflik (prob.LoglogisticDistribution.fit (x), {1}) %!error ... %! proflik (prob.LoglogisticDistribution.fit (x), 1, ones (2)) %!error ... %! proflik (prob.LoglogisticDistribution.fit (x), 1, 'Display') %!error ... %! proflik (prob.LoglogisticDistribution.fit (x), 1, 'Display', 1) %!error ... %! proflik (prob.LoglogisticDistribution.fit (x), 1, 'Display', {1}) %!error ... %! proflik (prob.LoglogisticDistribution.fit (x), 1, 'Display', {'on'}) %!error ... %! proflik (prob.LoglogisticDistribution.fit (x), 1, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.LoglogisticDistribution.fit (x), 1, 'Display', 'onnn') %!error ... %! proflik (prob.LoglogisticDistribution.fit (x), 1, 'NAME', 'on') %!error ... %! proflik (prob.LoglogisticDistribution.fit (x), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.LoglogisticDistribution.fit (x), 1, {[1 2 3 4]}, 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.LoglogisticDistribution) %!error ... %! truncate (prob.LoglogisticDistribution, 2) %!error ... %! truncate (prob.LoglogisticDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.LoglogisticDistribution (1, 1); %! pd(2) = prob.LoglogisticDistribution (1, 3); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/LognormalDistribution.m000066400000000000000000001217371527311104300301720ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef LognormalDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.LognormalDistribution ## ## Lognormal probability distribution object. ## ## A @code{prob.LognormalDistribution} object consists of parameters, a model ## description, and sample data for a lognormal probability distribution. ## ## The lognormal distribution is a continuous probability distribution whose ## logarithm is normally distributed. It is defined by mean parameter ## @var{mu} and standard deviation parameter @var{sigma} of the logarithmic ## values. ## ## There are several ways to create a @code{prob.LognormalDistribution} ## object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor @qcode{prob.LognormalDistribution (@var{mu}, ## @var{sigma})} to create a lognormal distribution with fixed parameter ## values @var{mu} and @var{sigma}. ## @item Use the static method @qcode{prob.LognormalDistribution.fit (@var{x}, ## @var{censor}, @var{freq}, @var{options})} to fit a distribution to the ## data in @var{x} using the same input arguments as the @code{lognfit} ## function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the lognormal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Log-normal_distribution} ## ## @seealso{fitdist, makedist, logncdf, logninv, lognpdf, lognrnd, lognfit, ## lognlike, lognstat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.LognormalDistribution} {property} mu ## ## Mean of logarithmic values ## ## A scalar value characterizing the mean of the logarithmic values of the ## lognormal distribution. You can access the @qcode{mu} ## property using dot name assignment. ## ## @end deftp mu ## -*- texinfo -*- ## @deftp {prob.LognormalDistribution} {property} sigma ## ## Standard deviation of logarithmic values ## ## A positive scalar value characterizing the standard deviation of the ## logarithmic values of the lognormal distribution. You can access the ## @qcode{sigma} property using dot name assignment. ## ## @end deftp sigma endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.LognormalDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Lognormal'; ## -*- texinfo -*- ## @deftp {prob.LognormalDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 2; ## -*- texinfo -*- ## @deftp {prob.LognormalDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'mu', 'sigma'}; ## -*- texinfo -*- ## @deftp {prob.LognormalDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Mean of logarithmic values', ... 'Standard deviation of logarithmic values'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = true; DistributionCode = 'logn'; ParameterRange = [-Inf, realmin; Inf, Inf]; ParameterLogCI = [true, true]; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.LognormalDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{2*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{mu} and @qcode{sigma} ## properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.LognormalDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.LognormalDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.LognormalDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{2*2} numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter, and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only ## meaningful when the distribution was fitted to data. If the distribution ## object was created with fixed parameters, or a parameter of a fitted ## distribution is modified, then all elements of the variance-covariance ## are zero. This property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.LognormalDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*2} logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.LognormalDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: a numeric vector of logical values indicating ## censoring information corresponding to the elements of the data used for ## distribution fitting. If no censoring vector was used for distribution ## fitting, then this field defaults to an empty array. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.LognormalDistribution} {@var{pd} =} LognormalDistribution (@var{mu}, @var{sigma}) ## @deftypefnx {prob.LognormalDistribution} {@var{pd} =} LognormalDistribution () ## ## Create a @code{prob.LognormalDistribution} object. ## ## @var{mu} and @var{sigma} are the distribution parameters, which the class ## help describes. Called with no arguments the parameters take their ## defaults, @var{mu} 0 and @var{sigma} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = LognormalDistribution (mu, sigma) if (nargin == 0) mu = 0; sigma = 1; endif checkparams (mu, sigma); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [mu, sigma]; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'Lognormal distribution'); endfunction function disp (this) __disp__ (this, 'Lognormal distribution'); endfunction function this = set.mu (this, mu) checkparams (mu, this.sigma); this.InputData = []; this.ParameterValues(1) = mu; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function mu = get.mu (this) mu = this.ParameterValues(1); endfunction function this = set.sigma (this, sigma) checkparams (this.mu, sigma); this.InputData = []; this.ParameterValues(2) = sigma; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function sigma = get.sigma (this) sigma = this.ParameterValues(2); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.LognormalDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.LognormalDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = logncdf (x, this.mu, this.sigma); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= logncdf (lx, this.mu, this.sigma); p(! (lb | ub)) /= diff (logncdf ([lx, ux], this.mu, this.sigma)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LognormalDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = logncdf (this.Truncation(1), this.mu, this.sigma); up = logncdf (this.Truncation(2), this.mu, this.sigma); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = logninv (np, this.mu, this.sigma); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = logninv (p, this.mu, this.sigma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LognormalDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LognormalDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = lognstat (this.mu, this.sigma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LognormalDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = logncdf ([lx, ux], this.mu, this.sigma); m = logninv (sum (Fa_b) / 2, this.mu, this.sigma); else m = logninv (0.5, this.mu, this.sigma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LognormalDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = lognlike ([this.mu, this.sigma], this.InputData.data, ... this.InputData.cens, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LognormalDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.LognormalDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LognormalDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = lognpdf (x, this.mu, this.sigma); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (logncdf ([lx, ux], this.mu, this.sigma)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LognormalDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.LognormalDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.LognormalDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LognormalDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.LognormalDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.LognormalDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.LognormalDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.LognormalDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.LognormalDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the Lognormal distribution, @qcode{@var{pnum} = 1} selects ## the parameter @qcode{mu} and @qcode{@var{pnum} = 2} selects the ## parameter @qcode{sigma}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LognormalDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.LognormalDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.LognormalDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.LognormalDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{lognrnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = logncdf (this.Truncation(1), this.mu, this.sigma); up = logncdf (this.Truncation(2), this.mu, this.sigma); u = unifrnd (lp, up, varargin{:}); r = exp (this.mu + this.sigma .* (-sqrt (2) * erfcinv (2 * u))); else r = lognrnd (this.mu, this.sigma, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LognormalDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LognormalDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, and upper limit, @var{upper}. If @var{pd} is fitted to data ## with @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LognormalDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = lognstat (this.mu, this.sigma); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) censor = []; else censor = varargin{2}; endif if (nargin < 4) freq = []; else freq = varargin{3}; endif if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else options = varargin{4}; endif ## Fit data [phat, pci] = lognfit (x, alpha, censor, freq, options); [~, acov] = lognlike (phat, x, censor, freq); ## Create fitted distribution object pd = prob.LognormalDistribution.makeFitted ... (phat, pci, acov, x, censor, freq); endfunction function pd = makeFitted (phat, pci, acov, x, censor, freq) mu = phat(1); sigma = phat(2); pd = prob.LognormalDistribution (mu, sigma); pd.ParameterCI = pci; pd.ParameterIsFixed = [false, false]; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', censor, 'freq', freq); endfunction endmethods endclassdef function checkparams (mu, sigma) if (! (isscalar (mu) && isnumeric (mu) && isreal (mu) && isfinite (mu))) error ("LognormalDistribution: MU must be a real scalar.") endif if (! (isscalar (sigma) && isnumeric (sigma) && isreal (sigma) && isfinite (sigma) && sigma > 0)) error ("LognormalDistribution: SIGMA must be a positive real scalar.") endif endfunction %!demo %! ## Generate a data set of 5000 random samples from a Lognormal distribution with %! ## parameters mu = 0 and sigma = 1. Fit a Lognormal distribution to this data and plot %! ## a PDF of the fitted distribution superimposed on a histogram of the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ('Lognormal', 'mu', 0, 'sigma', 1) %! data = random (pd_fixed, 5000, 1); %! pd_fitted = fitdist (data, 'Lognormal') %! plot (pd_fitted) %! msg = 'Fitted Lognormal distribution with mu = %0.2f and sigma = %0.2f'; %! title (sprintf (msg, pd_fitted.mu, pd_fitted.sigma)) ## Test output %!shared pd, t %! pd = prob.LognormalDistribution; %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0:5]), [0, 0.5, 0.7559, 0.8640, 0.9172, 0.9462], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0, 0.6705, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4]), [0.6574, 0.7559, 0.8640, 0.9172], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4]), [0, 0, 0.6705, 1], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [0, 0.4310, 0.7762, 1.2883, 2.3201, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.2256, 2.5015, 2.8517, 3.3199, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 0.7762, 1.2883, 2.3201, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.5015, 2.8517, 3.3199, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 1.4536, 1e-4); %!assert_equal (iqr (t), 0.8989, 1e-4); %!assert_equal (mean (pd), 1.6487, 1e-4); %!assert_equal (mean (t), 2.7692, 1e-4); %!assert_equal (median (pd), 1, 1e-4); %!assert_equal (median (t), 2.6653, 1e-4); %!assert_equal (pdf (pd, [0:5]), [0, 0.3989, 0.1569, 0.0727, 0.0382, 0.0219], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 0.9727, 0.4509, 0.2366, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1:4, NaN]), [0, 0.3989, 0.1569, 0.0727, 0.0382, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1:4, NaN]), [0, 0, 0.9727, 0.4509, 0.2366, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 2.1612, 1e-4); %!assert_equal (std (t), 0.5540, 1e-4); %!assert_equal (var (pd), 4.6708, 1e-4); %!assert_equal (var (t), 0.3069, 1e-4); %!test %! ## The profile over the first free parameter: 21 grid values, one row of %! ## OTHER per value, and the likelihood peaking at the fitted estimate. %! x = [1.2; 0.4; 3.1; 0.7; 2.5; 1.8; 0.3; 4.2; 1.1; 0.9; ... %! 2.2; 0.6; 1.5; 3.7; 0.8; 2.9; 1.3; 0.5; 2.0; 1.6]; %! pd = fitdist (x, 'Lognormal'); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (size (param), [1, 21]); %! assert_equal (size (other), [21, 1]); %! assert_equal (proflik (pd), nlogL); %! [~, imax] = max (nlogL); %! assert_equal (abs (param(imax) - pd.ParameterValues(1)) <= param(2) - param(1), true); ## Test input validation ## 'prob.LognormalDistribution' constructor %!error ... %! prob.LognormalDistribution (Inf, 1) %!error ... %! prob.LognormalDistribution (i, 1) %!error ... %! prob.LognormalDistribution ('a', 1) %!error ... %! prob.LognormalDistribution ([1, 2], 1) %!error ... %! prob.LognormalDistribution (NaN, 1) %!error ... %! prob.LognormalDistribution (1, 0) %!error ... %! prob.LognormalDistribution (1, -1) %!error ... %! prob.LognormalDistribution (1, Inf) %!error ... %! prob.LognormalDistribution (1, i) %!error ... %! prob.LognormalDistribution (1, 'a') %!error ... %! prob.LognormalDistribution (1, [1, 2]) %!error ... %! prob.LognormalDistribution (1, NaN) ## 'cdf' method %!error ... %! cdf (prob.LognormalDistribution, 2, 'uper') %!error ... %! cdf (prob.LognormalDistribution, 2, 3) ## 'paramci' method %!shared x %! randn ('seed', 1); %! x = lognrnd (1, 1, [1, 100]); %!error ... %! paramci (prob.LognormalDistribution.fit (x), 'alpha') %!error ... %! paramci (prob.LognormalDistribution.fit (x), 'alpha', 0) %!error ... %! paramci (prob.LognormalDistribution.fit (x), 'alpha', 1) %!error ... %! paramci (prob.LognormalDistribution.fit (x), 'alpha', [0.5 2]) %!error ... %! paramci (prob.LognormalDistribution.fit (x), 'alpha', '') %!error ... %! paramci (prob.LognormalDistribution.fit (x), 'alpha', {0.05}) %!error ... %! paramci (prob.LognormalDistribution.fit (x), 'parameter', 'mu', 'alpha', {0.05}) %!error ... %! paramci (prob.LognormalDistribution.fit (x), 'parameter', {'mu', 'sigma', 'parm'}) %!error ... %! paramci (prob.LognormalDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', {'mu', 'sigma', 'param'}) %!error ... %! paramci (prob.LognormalDistribution.fit (x), 'parameter', 'param') %!error ... %! paramci (prob.LognormalDistribution.fit (x), 'alpha', 0.01, 'parameter', 'param') %!error ... %! paramci (prob.LognormalDistribution.fit (x), 'NAME', 'value') %!error ... %! paramci (prob.LognormalDistribution.fit (x), 'alpha', 0.01, 'NAME', 'value') %!error ... %! paramci (prob.LognormalDistribution.fit (x), 'alpha', 0.01, 'parameter', 'mu', ... %! 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.LognormalDistribution, 'Parent') %!error ... %! plot (prob.LognormalDistribution, 'PlotType', 12) %!error ... %! plot (prob.LognormalDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.LognormalDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.LognormalDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.LognormalDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.LognormalDistribution, 'Discrete', {true}) %!error ... %! plot (prob.LognormalDistribution, 'Parent', 12) %!error ... %! plot (prob.LognormalDistribution, 'Parent', 'hax') %!error ... %! plot (prob.LognormalDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.LognormalDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.LognormalDistribution, 2) %!error ... %! proflik (prob.LognormalDistribution.fit (x), 3) %!error ... %! proflik (prob.LognormalDistribution.fit (x), [1, 2]) %!error ... %! proflik (prob.LognormalDistribution.fit (x), {1}) %!error ... %! proflik (prob.LognormalDistribution.fit (x), 1, ones (2)) %!error ... %! proflik (prob.LognormalDistribution.fit (x), 1, 'Display') %!error ... %! proflik (prob.LognormalDistribution.fit (x), 1, 'Display', 1) %!error ... %! proflik (prob.LognormalDistribution.fit (x), 1, 'Display', {1}) %!error ... %! proflik (prob.LognormalDistribution.fit (x), 1, 'Display', {'on'}) %!error ... %! proflik (prob.LognormalDistribution.fit (x), 1, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.LognormalDistribution.fit (x), 1, 'Display', 'onnn') %!error ... %! proflik (prob.LognormalDistribution.fit (x), 1, 'NAME', 'on') %!error ... %! proflik (prob.LognormalDistribution.fit (x), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.LognormalDistribution.fit (x), 1, {[1 2 3 4]}, 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.LognormalDistribution) %!error ... %! truncate (prob.LognormalDistribution, 2) %!error ... %! truncate (prob.LognormalDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.LognormalDistribution (1, 1); %! pd(2) = prob.LognormalDistribution (1, 3); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/LoguniformDistribution.m000066400000000000000000000643711527311104300303610ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef LoguniformDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.LoguniformDistribution ## ## Log-uniform probability distribution object. ## ## A @code{prob.LoguniformDistribution} object consists of parameters and a ## model description for a log-uniform probability distribution. ## ## The log-uniform distribution is a continuous probability distribution that ## is constant between locations @var{Lower} and @var{Upper} on a logarithmic ## scale. ## ## There are several ways to create a @code{prob.LoguniformDistribution} ## object. ## ## @itemize ## @item Create a distribution with specified parameter values using the ## @code{makedist} function. ## @item Use the constructor @qcode{prob.LoguniformDistribution (@var{Lower}, ## @var{Upper})} to create a log-uniform distribution with specified parameter ## values @var{Lower} and @var{Upper}. ## @end itemize ## ## It is highly recommended to use @code{makedist} function to create ## probability distribution objects, instead of the class constructor. ## ## Further information about the log-uniform distribution can be found at ## @url{https://en.wikipedia.org/wiki/Reciprocal_distribution} ## ## @seealso{makedist} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.LoguniformDistribution} {property} Lower ## ## Lower limit ## ## A positive scalar value characterizing the lower limit of the ## log-uniform distribution. You can access the @qcode{Lower} ## property using dot name assignment. ## ## @end deftp Lower ## -*- texinfo -*- ## @deftp {prob.LoguniformDistribution} {property} Upper ## ## Upper limit ## ## A positive scalar value characterizing the upper limit of the ## log-uniform distribution. You can access the @qcode{Upper} ## property using dot name assignment. ## ## @end deftp Upper endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.LoguniformDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Loguniform'; ## -*- texinfo -*- ## @deftp {prob.LoguniformDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 2; ## -*- texinfo -*- ## @deftp {prob.LoguniformDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'Lower', 'Upper'}; ## -*- texinfo -*- ## @deftp {prob.LoguniformDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Lower limit', 'Upper limit'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = false; DistributionCode = 'logu'; ParameterRange = [realmin, realmin; Inf, Inf]; ParameterLogCI = [false, false]; endproperties properties(GetAccess = public , SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.LoguniformDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{2*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{Lower} and @qcode{Upper} ## properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.LoguniformDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.LoguniformDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.LoguniformDistribution} {@var{pd} =} LoguniformDistribution (@var{Lower}, @var{Upper}) ## @deftypefnx {prob.LoguniformDistribution} {@var{pd} =} LoguniformDistribution () ## ## Create a @code{prob.LoguniformDistribution} object. ## ## @var{Lower} and @var{Upper} are the distribution parameters, which the ## class help describes. Called with no arguments the parameters take their ## defaults, @var{Lower} 1 and @var{Upper} 4. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = LoguniformDistribution (Lower, Upper) if (nargin == 0) Lower = 1; Upper = 4; endif checkparams (Lower, Upper); this.IsTruncated = false; this.ParameterValues = [Lower, Upper]; this.Truncation = []; endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'Log-uniform distribution'); endfunction function disp (this) __disp__ (this, 'Log-uniform distribution'); endfunction function this = set.Lower (this, Lower) checkparams (Lower, this.Upper); this.ParameterValues(1) = Lower; endfunction function Lower = get.Lower (this) Lower = this.ParameterValues(1); endfunction function this = set.Upper (this, Upper) checkparams (this.Lower, Upper); this.ParameterValues(2) = Upper; endfunction function Upper = get.Upper (this) Upper = this.ParameterValues(2); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.LoguniformDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.LoguniformDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## ## @var{x} must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif if (! isfloat (x)) error ("cdf: X must be double or single."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations d = log (this.Upper / this.Lower); p = log (x / this.Lower) / d; p(xthis.Upper) = 1; if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= log (lx / this.Lower) / d; p(! (lb | ub)) /= diff (log ([lx, ux] ./ this.Lower) ./ d); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoguniformDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## ## @var{p} must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (! isfloat (p)) error ("icdf: P must be double or single."); endif ## Do the computations is_nan = p < 0 | p > 1 | isnan (p); is_val = p >= 0 & p <= 1; if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); d = log (this.Upper / this.Lower); lp = log (lx / this.Lower) / d; up = log (ux / this.Lower) / d; ## Adjust p values within range of p @ lower limit and p @ upper limit p = lp + (up - lp) .* p; is_nan = p < lp | p > up | isnan (p); is_val = p >= lp & p <= up; endif x = p; x(is_nan) = NaN; x(is_val) = (this.Upper .^ p(is_val)) ./ (this.Lower .^ (p(is_val) - 1)); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoguniformDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoguniformDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = (this.Upper - this.Lower) / log (this.Upper / this.Lower); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoguniformDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif m = icdf (this, 0.5); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoguniformDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## ## @var{x} must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif if (! isfloat (x)) error ("pdf: X must be double or single."); endif d = log (this.Upper / this.Lower); y = 1 ./ (x .* d); y(x < this.Lower | x > this.Upper) = 0; if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (log ([lx, ux] ./ this.Lower) ./ d); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoguniformDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.LoguniformDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.LoguniformDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). ## @qcode{'cdf'} plots the cumulative density function (CDF). ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoguniformDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.LoguniformDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.LoguniformDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.LoguniformDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{random} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif r = icdf (this, unifrnd (0, 1, varargin{:})); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoguniformDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoguniformDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, and upper limit, @var{upper}. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; endfunction ## -*- texinfo -*- ## @deftypefn {prob.LoguniformDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else a = this.Lower; b = this.Upper; l = log (b / a); v = (b ^ 2 - a ^2) / (2 * l) - ((b - a) / l) ^ 2; endif endfunction endmethods endclassdef function checkparams (Lower, Upper) if (! (isscalar (Lower) && isnumeric (Lower) && isreal (Lower) && isfinite (Lower) && Lower > 0)) error ("LoguniformDistribution: LOWER must be a positive real scalar.") endif if (! (isscalar (Upper) && isnumeric (Upper) && isreal (Upper) && isfinite (Upper))) error ("LoguniformDistribution: UPPER must be a real scalar.") endif if (! (Lower < Upper)) error ("LoguniformDistribution: LOWER must be less than UPPER.") endif endfunction %!demo %! ## Generate a data set of 5000 random samples from a Log-uniform distribution with %! ## parameters Lower = 1 and Upper = 10. Plot a PDF of the distribution superimposed %! ## on a histogram of the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ('Loguniform', 'Lower', 1, 'Upper', 10); %! data = random (pd_fixed, 5000, 1); %! plot (pd_fixed) %! hold on %! hist (data, 50) %! hold off %! msg = 'Log-uniform distribution with Lower = %0.2f and Upper = %0.2f'; %! title (sprintf (msg, pd_fixed.Lower, pd_fixed.Upper)) ## Test output %!shared pd, t %! pd = prob.LoguniformDistribution (1, 4); %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0, 1, 2, 3, 4, 5]), [0, 0, 0.5, 0.7925, 1, 1], 1e-4); %!assert_equal (cdf (t, [0, 1, 2, 3, 4, 5]), [0, 0, 0, 0.5850, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4]), [0.2925, 0.5, 0.7925, 1], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4]), [0, 0, 0.5850, 1], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [1, 1.3195, 1.7411, 2.2974, 3.0314, 4], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.2974, 2.6390, 3.0314, 3.4822, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 1.7411, 2.2974, 3.0314, 4, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.6390, 3.0314, 3.4822, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 1.4142, 1e-4); %!assert_equal (iqr (t), 0.9852, 1e-4); %!assert_equal (mean (pd), 2.1640, 1e-4); %!assert_equal (mean (t), 2.8854, 1e-4); %!assert_equal (median (pd), 2); %!assert_equal (median (t), 2.8284, 1e-4); %!assert_equal (pdf (pd, [0, 1, 2, 3, 4, 5]), [0, 0.7213, 0.3607, 0.2404, 0.1803, 0], 1e-4); %!assert_equal (pdf (t, [0, 1, 2, 3, 4, 5]), [0, 0, 0.7213, 0.4809, 0.3607, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1, 2, 3, 4, NaN]), [0, 0.7213, 0.3607, 0.2404, 0.1803, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1, 2, 3, 4, NaN]), [0, 0, 0.7213, 0.4809, 0.3607, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (size (random (pd)), [1, 1]) %!error pdf (pd, int32 (2)) %!error pdf (pd, true) %!error cdf (pd, int32 (2)) %!error icdf (pd, int32 (1)) %!assert_equal (size (random (pd, 3)), [3, 3]) %!assert_equal (size (random (pd, -1)), [0, 0]) %!assert_equal (size (random (pd, 2, -1, 5)), [2, 0, 5]) %!assert_equal (any (random (pd, 1000, 1) < 1), false); %!assert_equal (any (random (pd, 1000, 1) > 4), false); %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 0.8527, 1e-4); %!assert_equal (std (t), 0.5751, 1e-4); %!assert_equal (var (pd), 0.7270, 1e-4); %!assert_equal (var (t), 0.3307, 1e-4); ## Test input validation ## 'prob.LoguniformDistribution' constructor %!error ... %! prob.LoguniformDistribution (i, 1) %!error ... %! prob.LoguniformDistribution (Inf, 1) %!error ... %! prob.LoguniformDistribution ([1, 2], 1) %!error ... %! prob.LoguniformDistribution ('a', 1) %!error ... %! prob.LoguniformDistribution (NaN, 1) %!error ... %! prob.LoguniformDistribution (1, i) %!error ... %! prob.LoguniformDistribution (1, Inf) %!error ... %! prob.LoguniformDistribution (1, [1, 2]) %!error ... %! prob.LoguniformDistribution (1, 'a') %!error ... %! prob.LoguniformDistribution (1, NaN) %!error ... %! prob.LoguniformDistribution (2, 1) ## 'cdf' method %!error ... %! cdf (prob.LoguniformDistribution, 2, 'uper') %!error ... %! cdf (prob.LoguniformDistribution, 2, 3) ## 'plot' method %!error ... %! plot (prob.LoguniformDistribution, 'Parent') %!error ... %! plot (prob.LoguniformDistribution, 'PlotType', 12) %!error ... %! plot (prob.LoguniformDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.LoguniformDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.LoguniformDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.LoguniformDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.LoguniformDistribution, 'Discrete', {true}) %!error ... %! plot (prob.LoguniformDistribution, 'Parent', 12) %!error ... %! plot (prob.LoguniformDistribution, 'Parent', 'hax') %!error ... %! plot (prob.LoguniformDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.LoguniformDistribution, 'PlotType', 'probability') ## 'truncate' method %!error ... %! truncate (prob.LoguniformDistribution) %!error ... %! truncate (prob.LoguniformDistribution, 2) %!error ... %! truncate (prob.LoguniformDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.LoguniformDistribution (1, 4); %! pd(2) = prob.LoguniformDistribution (2, 5); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error pdf (pd, 1) %!error plot (pd) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/MultinomialDistribution.m000066400000000000000000000723601527311104300305270ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef MultinomialDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.MultinomialDistribution ## ## Multinomial probability distribution object. ## ## A @code{prob.MultinomialDistribution} object consists of parameters, a ## model description, and sample data for a multinomial probability ## distribution. ## ## The multinomial distribution is a discrete probability distribution that ## models the outcomes of n independent trials of a k-category system, where ## each trial has a probability of falling into each category. It is ## defined by the vector of probabilities for each outcome. ## ## There are several ways to create a @code{prob.MultinomialDistribution} ## object. ## ## @itemize ## @item Create a distribution with specified parameter values using the ## @code{makedist} function. ## @item Use the constructor ## @qcode{prob.MultinomialDistribution ## (@var{Probabilities})} ## to create a multinomial distribution with specified parameter values. ## @end itemize ## ## It is highly recommended to use the @code{makedist} function to create ## probability distribution objects, instead of the constructor. ## ## Further information about the multinomial distribution can be found at ## @url{https://en.wikipedia.org/wiki/Multinomial_distribution} ## ## @seealso{makedist, mnpdf, mnrnd} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.MultinomialDistribution} {property} Probabilities ## ## Outcome probabilities ## ## A row vector of probabilities for each outcome. You can access the ## @qcode{Probabilities} property using dot name assignment. ## ## @end deftp Probabilities endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.MultinomialDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Multinomial'; ## -*- texinfo -*- ## @deftp {prob.MultinomialDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 1; ## -*- texinfo -*- ## @deftp {prob.MultinomialDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{1*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'Probabilities'}; ## -*- texinfo -*- ## @deftp {prob.MultinomialDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{1*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Outcome probabilities'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = false; DistributionCode = 'mn'; endproperties properties(GetAccess = public , SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.MultinomialDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{Probabilities} ## property. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.MultinomialDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.MultinomialDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.MultinomialDistribution} {@var{pd} =} MultinomialDistribution (@var{Probabilities}) ## @deftypefnx {prob.MultinomialDistribution} {@var{pd} =} MultinomialDistribution () ## ## Create a @code{prob.MultinomialDistribution} object. ## ## @var{Probabilities} is the distribution parameter, which the class help ## describes. Called with no arguments the parameter takes its default, ## @var{Probabilities} @code{[0.5, 0.5]}. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = MultinomialDistribution (Probabilities) if (nargin == 0) Probabilities = [0.5, 0.5]; endif checkparams (Probabilities); this.IsTruncated = false; this.ParameterValues = {Probabilities(:)'}; endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'multinomial distribution'); endfunction function disp (this) __disp__ (this, 'multinomial distribution'); endfunction function this = set.Probabilities (this, Probabilities) checkparams (Probabilities); this.ParameterValues = {Probabilities(:)'}; endfunction function Probabilities = get.Probabilities (this) Probabilities = this.ParameterValues{1}; endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.MultinomialDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.MultinomialDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @var{x} must be @qcode{double}, @qcode{single}, or an integer type; ## logical and character arrays are rejected. Integer input is promoted ## to @qcode{double}, so the result is always a probability. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif if (! isnumeric (x)) error ("cdf: X must be double, single, or integer."); endif if (isinteger (x)) x = double (x); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Check input data if (! isreal (x)) error ("cdf: X must be real."); endif probs = this.Probabilities; ## Check for truncation and normalize truncated probabilities vector if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); probs = this.Probabilities([lx:ux]); probs = probs .* (1 / sum (probs)); x = x - lx + 1; endif ## Do the computations is_nan = isnan (x); sz = size (x); xf = floor (x); pk = length (probs); ## Create cumulative probability vector pc = cumsum (probs); pc(end) = 1; # Force last element to 1 xf(xf > pk) = pk; xf(xf < 1) = 1; xf(is_nan) = 1; p = pc(xf); p(x < 1) = 0; p(is_nan) = NaN; p = reshape (p, sz); ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.MultinomialDistribution} {@var{p} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{p} = icdf (@var{pd}, @var{x})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{x}. ## ## @var{p} must be @qcode{double} or @qcode{single}; integer, logical, and ## character arrays are rejected. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (! isfloat (p)) error ("icdf: P must be double or single."); endif probs = this.Probabilities; ## Do the computations sz = size (p); p = p(:); x = zeros (numel (p), 1); pc = cumsum (this.Probabilities); pc = [0 pc(1:(end-1))]; is_one = p == 0; is_nan = isnan (p) | p > 1 | p < 0; for i = 1:length (pc) x(p > pc(i)) = i; endfor x(is_one) = 1; x(is_nan) = NaN; ## Check for truncation and clip edges if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); lp = pc(lx); up = pc(ux); lb = p >= 0 & p <= lp; ub = p <= 1 & p >= up; x(lb) = lx; x(ub) = ux; endif x = reshape (x, sz); endfunction ## -*- texinfo -*- ## @deftypefn {prob.MultinomialDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.MultinomialDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif probs = this.Probabilities; if (this.IsTruncated) x = 1:numel (probs); w = x >= this.Truncation(1) & x <= this.Truncation(2); x = x(w); y = pdf (this, x); m = sum (y .* x); else m = sum (this.Probabilities .* (1:numel (probs))); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.MultinomialDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif m = icdf (this, 0.5); endfunction ## -*- texinfo -*- ## @deftypefn {prob.MultinomialDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @var{x} must be @qcode{double}, @qcode{single}, or an integer type; ## logical and character arrays are rejected. Integer input is promoted ## to @qcode{double}, so the result is always a probability. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif if (! isnumeric (x)) error ("pdf: X must be double, single, or integer."); endif if (isinteger (x)) x = double (x); endif probs = this.Probabilities; size_x = size (x); is_nan = isnan (x); if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); is_out = x < lx | x > ux | (x-floor (x)) > 0 | is_nan; tprobs = probs([lx:ux]); tprobs = tprobs .* (1 / sum (tprobs)); probs([lx:ux]) = tprobs; copy_x = x; copy_x(is_out) = 1; y = probs(copy_x); y(is_out) = 0; y(is_nan) = NaN; y = reshape (y, size_x); return else is_out = x < 1 | x > length (probs) | (x-floor (x)) > 0 | is_nan; copy_x = x; copy_x(is_out) = 1; y = probs(copy_x); y(is_out) = 0; y(is_nan) = NaN; y = reshape (y, size_x); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.MultinomialDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.MultinomialDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.MultinomialDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd}} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, true, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.MultinomialDistribution} {@var{y} =} random (@var{pd}) ## @deftypefnx {prob.MultinomialDistribution} {@var{y} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.MultinomialDistribution} {@var{y} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.MultinomialDistribution} {@var{y} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{mnrnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif u = unifrnd (0, 1, varargin{:}); sz = size (u); if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); tprobs = pdf (this, [1:numel(this.Probabilities)]); tprobs = tprobs([lx:ux]); cp = cumsum (tprobs); else cp = cumsum (this.Probabilities); endif bins = min ([0, cp], 1); bins(end) = 1; [~, r] = histc (u(:), bins); r = reshape (r, sz); if (this.IsTruncated) r = r + lx - 1; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.MultinomialDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.MultinomialDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd})} returns a probability distribution ## @var{t}, which is the probability distribution @var{pd} truncated to the ## specified interval with lower limit, @var{lower}, and upper limit, ## @var{upper}. If @var{pd} is fitted to data with @code{fitdist}, the ## returned probability distribution @var{t} is not fitted, does not contain ## any data or estimated values, and it is as it has been created with the ## @var{makedist} function, but it includes the truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: is_nan input argument."); endif ## Constrain within the length of Probabilities vector lower = round (lower); upper = round (upper); k = numel (this.Probabilities); lower(lower < 1) = 1; upper(upper > k) = k; if (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; endfunction ## -*- texinfo -*- ## @deftypefn {prob.MultinomialDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif probs = this.Probabilities; if (this.IsTruncated) x = 1:numel (probs); w = x >= this.Truncation(1) & x <= this.Truncation(2); x = x(w); y = pdf (this, x); m = sum (y .* x); v = sum (y .* (x - m) .^ 2); else v = sum (probs .* (1:numel (probs)) .^ 2) - mean (this) ^ 2; endif endfunction endmethods endclassdef function checkparams (Probabilities) if (! (isvector (Probabilities) && isnumeric (Probabilities) && isreal (Probabilities) && isfinite (Probabilities) && abs (sum (Probabilities) - 1) < eps * 100)) error (strcat ("MultinomialDistribution: PROBABILITIES must be a vector", " of positive real scalars that sum up to 1.")) endif endfunction %!demo %! ## Generate a data set of 5000 random samples from a Multinomial distribution %! ## with parameters Probabilities = [0.1, 0.2, 0.3, 0.2, 0.1, 0.1]. Create %! ## the distribution and plot the PDF superimposed on a histogram of the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! probs = [0.1, 0.2, 0.3, 0.2, 0.1, 0.1]; %! pd = makedist ('Multinomial', 'Probabilities', probs); %! data = random (pd, 5000, 1); %! hist (data, length (probs)); %! hold on %! x = 1:length (probs); %! y = pdf (pd, x) * 5000; %! stem (x, y, 'r', 'LineWidth', 2); %! hold off %! msg = 'Multinomial distribution with Probabilities = [%s]'; %! probs_str = num2str (probs, '%0.1f '); %! title (sprintf (msg, probs_str)) ## Test output %!shared pd, t %! pd = prob.MultinomialDistribution ([0.1, 0.2, 0.3, 0.2, 0.1, 0.1]); %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [2, 3, 4]), [0.3, 0.6, 0.8], eps); %!assert_equal (cdf (t, [2, 3, 4]), [0.2857, 0.7143, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4]), [0.1, 0.3, 0.6, 0.8], eps); %!assert_equal (cdf (pd, [1.5, 2-eps, 3, 4]), [0.1, 0.1, 0.6, 0.8], eps); %!assert_equal (cdf (t, [1.5, 2, 3, 4]), [0, 0.2857, 0.7143, 1], 1e-4); %!assert_equal (cdf (t, [1.5, 2-eps, 3, 4]), [0, 0, 0.7143, 1], 1e-4); %!assert_equal (cdf (pd, [1, 2.5, 4, 6]), [0.1, 0.3, 0.8, 1], eps); %!assert_equal (icdf (pd, [0, 0.2857, 0.7143, 1]), [1, 2, 4, 6]); %!assert_equal (icdf (t, [0, 0.2857, 0.7143, 1]), [2, 2, 4, 4]); %!assert_equal (icdf (t, [0, 0.35, 0.7143, 1]), [2, 3, 4, 4]); %!assert_equal (icdf (t, [0, 0.35, 0.7143, 1, NaN]), [2, 3, 4, 4, NaN]); %!assert_equal (icdf (t, [-0.5, 0, 0.35, 0.7143, 1, NaN]), [NaN, 2, 3, 4, 4, NaN]); %!assert_equal (icdf (pd, [-0.5, 0, 0.35, 0.7143, 1, NaN]), [NaN, 1, 3, 4, 6, NaN]); %!assert_equal (iqr (pd), 2); %!assert_equal (iqr (t), 2); %!assert_equal (mean (pd), 3.3, 1e-14); %!assert_equal (mean (t), 3, eps); %!assert_equal (median (pd), 3); %!assert_equal (median (t), 3); %!assert_equal (pdf (pd, [-5, 1, 2.5, 4, 6, NaN, 9]), [0, 0.1, 0, 0.2, 0.1, NaN, 0]); %!assert_equal (pdf (pd, [-5, 1, 2, 3, 4, 6, NaN, 9]), ... %! [0, 0.1, 0.2, 0.3, 0.2, 0.1, NaN, 0]); %!assert_equal (pdf (t, [-5, 1, 2, 3, 4, 6, NaN, 0]), ... %! [0, 0, 0.2857, 0.4286, 0.2857, 0, NaN, 0], 1e-4); %!assert_equal (pdf (t, [-5, 1, 2, 4, 6, NaN, 0]), ... %! [0, 0, 0.2857, 0.2857, 0, NaN, 0], 1e-4); %!assert_equal (size (random (pd)), [1, 1]) %!assert_equal (class (pdf (pd, int32 (2))), 'double') %!assert_equal (class (cdf (pd, int32 (2))), 'double') %!assert_equal (pdf (pd, int32 (2)), pdf (pd, 2)) %!error pdf (pd, true) %!error pdf (pd, 'a') %!error icdf (pd, int32 (1)) %!assert_equal (size (random (pd, 3)), [3, 3]) %!assert_equal (size (random (pd, -1)), [0, 0]) %!assert_equal (size (random (pd, 2, -1, 5)), [2, 0, 5]) %!assert_equal (unique (random (pd, 1000, 5)), [1, 2, 3, 4, 5, 6]'); %!assert_equal (unique (random (t, 1000, 5)), [2, 3, 4]'); %!assert_equal (std (pd), 1.4177, 1e-4); %!assert_equal (std (t), 0.7559, 1e-4); %!assert_equal (var (pd), 2.0100, 1e-4); %!assert_equal (var (t), 0.5714, 1e-4); ## Test input validation ## 'prob.MultinomialDistribution' constructor %!error ... %! prob.MultinomialDistribution (0) %!error ... %! prob.MultinomialDistribution (-1) %!error ... %! prob.MultinomialDistribution (Inf) %!error ... %! prob.MultinomialDistribution (i) %!error ... %! prob.MultinomialDistribution ('a') %!error ... %! prob.MultinomialDistribution ([1, 2]) %!error ... %! prob.MultinomialDistribution (NaN) ## 'cdf' method %!error ... %! cdf (prob.MultinomialDistribution, 2, 'uper') %!error ... %! cdf (prob.MultinomialDistribution, 2, 3) %!error ... %! cdf (prob.MultinomialDistribution, i) ## 'plot' method %!error ... %! plot (prob.MultinomialDistribution, 'Parent') %!error ... %! plot (prob.MultinomialDistribution, 'PlotType', 12) %!error ... %! plot (prob.MultinomialDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.MultinomialDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.MultinomialDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.MultinomialDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.MultinomialDistribution, 'Discrete', {true}) %!error ... %! plot (prob.MultinomialDistribution, 'Parent', 12) %!error ... %! plot (prob.MultinomialDistribution, 'Parent', 'hax') %!error ... %! plot (prob.MultinomialDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.MultinomialDistribution, 'PlotType', 'probability') ## 'truncate' method %!error ... %! truncate (prob.MultinomialDistribution) %!error ... %! truncate (prob.MultinomialDistribution, 2) %!error ... %! truncate (prob.MultinomialDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.MultinomialDistribution ([0.1, 0.2, 0.3, 0.4]); %! pd(2) = prob.MultinomialDistribution ([0.1, 0.2, 0.3, 0.4]); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error pdf (pd, 1) %!error plot (pd) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) ## ParameterValues holds one entry per parameter, as MATLAB reports it. %!test %! pd = makedist ('Multinomial', 'Probabilities', [0.2, 0.3, 0.5]); %! assert_equal (iscell (pd.ParameterValues), true); %! assert_equal (size (pd.ParameterValues), [1, 1]); %! assert_equal (pd.ParameterValues{1}, [0.2, 0.3, 0.5]); %! assert_equal (numel (pd.ParameterValues), pd.NumParameters); %! assert_equal (size (pd.ParameterValues), size (pd.ParameterNames)); ## The entry is a row whichever orientation the parameter was given in. %!test %! pd = makedist ('Multinomial', 'Probabilities', [0.2; 0.3; 0.5]); %! assert_equal (size (pd.ParameterValues{1}), [1, 3]); %! pd.Probabilities = [0.5; 0.5]; %! assert_equal (pd.ParameterValues{1}, [0.5, 0.5]); statistics-release-2.1.2/inst/Distribution_Classes/+prob/NakagamiDistribution.m000066400000000000000000001225031527311104300277400ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef NakagamiDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.NakagamiDistribution ## ## Nakagami probability distribution object. ## ## A @code{prob.NakagamiDistribution} object consists of parameters, a model ## description, and sample data for a Nakagami probability distribution. ## ## The Nakagami distribution is a continuous probability distribution that ## models the amplitude of received signals after maximum ratio diversity ## combining. It is defined by shape parameter @var{mu} and spread parameter ## @var{omega}. ## ## There are several ways to create a @code{prob.NakagamiDistribution} object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor @qcode{prob.NakagamiDistribution (@var{mu}, ## @var{omega})} to create a Nakagami distribution with fixed parameter ## values @var{mu} and @var{omega}. ## @item Use the static method @qcode{prob.NakagamiDistribution.fit (@var{x}, ## @var{censor}, @var{freq}, @var{options})} to fit a distribution to the data ## in @var{x} using the same input arguments as the @code{nakafit} function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the Nakagami distribution can be found at ## @url{https://en.wikipedia.org/wiki/Nakagami_distribution} ## ## @seealso{fitdist, makedist, nakacdf, nakainv, nakapdf, nakarnd, nakafit, ## nakalike, nakastat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.NakagamiDistribution} {property} mu ## ## Shape parameter ## ## A positive scalar value characterizing the shape of the ## Nakagami distribution. You can access the @qcode{mu} ## property using dot name assignment. ## ## @end deftp mu ## -*- texinfo -*- ## @deftp {prob.NakagamiDistribution} {property} omega ## ## Spread parameter ## ## A positive scalar value characterizing the spread of the ## Nakagami distribution. You can access the @qcode{omega} ## property using dot name assignment. ## ## @end deftp omega endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.NakagamiDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Nakagami'; ## -*- texinfo -*- ## @deftp {prob.NakagamiDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 2; ## -*- texinfo -*- ## @deftp {prob.NakagamiDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'mu', 'omega'}; ## -*- texinfo -*- ## @deftp {prob.NakagamiDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Shape', 'Spread'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = true; DistributionCode = 'naka'; ParameterRange = [0.5, realmin; Inf, Inf]; ParameterLogCI = [true, true]; endproperties properties(GetAccess = public , SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.NakagamiDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{2*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{mu} and @qcode{omega} ## properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.NakagamiDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.NakagamiDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.NakagamiDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{2*2} numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter, and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only ## meaningful when the distribution was fitted to data. If the distribution ## object was created with fixed parameters, or a parameter of a fitted ## distribution is modified, then all elements of the variance-covariance ## are zero. This property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.NakagamiDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*2} logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.NakagamiDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: a numeric vector of logical values indicating ## censoring information corresponding to the elements of the data used for ## distribution fitting. If no censoring vector was used for distribution ## fitting, then this field defaults to an empty array. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.NakagamiDistribution} {@var{pd} =} NakagamiDistribution (@var{mu}, @var{omega}) ## @deftypefnx {prob.NakagamiDistribution} {@var{pd} =} NakagamiDistribution () ## ## Create a @code{prob.NakagamiDistribution} object. ## ## @var{mu} and @var{omega} are the distribution parameters, which the class ## help describes. Called with no arguments the parameters take their ## defaults, @var{mu} 1 and @var{omega} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = NakagamiDistribution (mu, omega) if (nargin == 0) mu = 1; omega = 1; endif checkparams (mu, omega); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [mu, omega]; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'Nakagami distribution'); endfunction function disp (this) __disp__ (this, 'Nakagami distribution'); endfunction function this = set.mu (this, mu) checkparams (mu, this.omega); this.InputData = []; this.ParameterValues(1) = mu; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function mu = get.mu (this) mu = this.ParameterValues(1); endfunction function this = set.omega (this, omega) checkparams (this.mu, omega); this.InputData = []; this.ParameterValues(2) = omega; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function omega = get.omega (this) omega = this.ParameterValues(2); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.NakagamiDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.NakagamiDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = nakacdf (x, this.mu, this.omega); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= nakacdf (lx, this.mu, this.omega); p(! (lb | ub)) /= diff (nakacdf ([lx, ux], this.mu, this.omega)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NakagamiDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = nakacdf (this.Truncation(1), this.mu, this.omega); up = nakacdf (this.Truncation(2), this.mu, this.omega); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = nakainv (np, this.mu, this.omega); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = nakainv (p, this.mu, this.omega); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NakagamiDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.NakagamiDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = nakastat (this.mu, this.omega); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NakagamiDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = nakacdf ([lx, ux], this.mu, this.omega); m = nakainv (sum (Fa_b) / 2, this.mu, this.omega); else m = nakainv (0.5, this.mu, this.omega); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NakagamiDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = nakalike ([this.mu, this.omega], this.InputData.data, ... this.InputData.cens, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.NakagamiDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.NakagamiDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NakagamiDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = nakapdf (x, this.mu, this.omega); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (nakacdf ([lx, ux], this.mu, this.omega)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NakagamiDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.NakagamiDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.NakagamiDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NakagamiDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.NakagamiDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.NakagamiDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.NakagamiDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.NakagamiDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.NakagamiDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the Nakagami distribution, @qcode{@var{pnum} = 1} selects ## the parameter @qcode{mu} and @qcode{@var{pnum} = 2} selects the ## parameter @qcode{omega}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.NakagamiDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.NakagamiDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.NakagamiDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.NakagamiDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{betarnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) sz = [varargin{:}]; ps = prod (sz); ## Get an estimate of how many more random numbers we need to randomly ## pick the appropriate size from lx = this.Truncation(1); ux = this.Truncation(2); ratio = 1 / diff (nakacdf ([lx, ux], this.mu, this.omega)); nsize = fix (2 * ratio * ps); # times 2 to be on the safe side ## Generate the numbers and remove out-of-bound random samples r = nakarnd (this.mu, this.omega, nsize, 1); r(r < lx | r > ux) = []; ## Randomly select the required size and reshape to requested dimensions idx = randperm (numel (r), ps); r = reshape (r(idx), sz); else r = nakarnd (this.mu, this.omega, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NakagamiDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.NakagamiDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, and upper limit, @var{upper}. If @var{pd} is fitted to data ## with @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); endif ## Check boundaries and constrain within support: Natural numbers lower = round (lower); upper = round (upper); lower(lower < 0) = 0; if (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.NakagamiDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = nakastat (this.mu, this.omega); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) censor = []; else censor = varargin{2}; endif if (nargin < 4) freq = []; else freq = varargin{3}; endif if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else options = varargin{4}; endif ## Fit data [phat, pci] = nakafit (x, alpha, censor, freq, options); [~, acov] = nakalike (phat, x, censor, freq); ## Create fitted distribution object pd = prob.NakagamiDistribution.makeFitted (phat, pci, acov, x, censor, freq); endfunction function pd = makeFitted (phat, pci, acov, x, censor, freq) mu = phat(1); omega = phat(2); pd = prob.NakagamiDistribution (mu, omega); pd.ParameterCI = pci; pd.ParameterIsFixed = [false, false]; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', censor, 'freq', freq); endfunction endmethods endclassdef function checkparams (mu, omega) if (! (isscalar (mu) && isnumeric (mu) && isreal (mu) && isfinite (mu) && mu >= 0.5)) error ("NakagamiDistribution: MU must be a real scalar of at least 0.5.") endif if (! (isscalar (omega) && isnumeric (omega) && isreal (omega) && isfinite (omega) && omega > 0)) error ("NakagamiDistribution: OMEGA must be a positive real scalar.") endif endfunction %!demo %! ## Generate a data set of 5000 random samples from a Nakagami distribution with %! ## parameters mu = 1 and omega = 1. Fit a Nakagami distribution to this data and plot %! ## a PDF of the fitted distribution superimposed on a histogram of the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ('Nakagami', 'mu', 1, 'omega', 1) %! data = random (pd_fixed, 5000, 1); %! pd_fitted = fitdist (data, 'Nakagami') %! plot (pd_fitted) %! msg = 'Fitted Nakagami distribution with mu = %0.2f and omega = %0.2f'; %! title (sprintf (msg, pd_fitted.mu, pd_fitted.omega)) ## Test output %!shared pd, t %! pd = prob.NakagamiDistribution; %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0:5]), [0, 0.6321, 0.9817, 0.9999, 1, 1], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0, 0.9933, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4]), [0.8946, 0.9817, 0.9999, 1], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4]), [0, 0, 0.9933, 1], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [0, 0.4724, 0.7147, 0.9572, 1.2686, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.0550, 2.1239, 2.2173, 2.3684, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 0.7147, 0.9572, 1.2686, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.1239, 2.2173, 2.3684, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 0.6411, 1e-4); %!assert_equal (iqr (t), 0.2502, 1e-4); %!assert_equal (mean (pd), 0.8862, 1e-4); %!assert_equal (mean (t), 2.2263, 1e-4); %!assert_equal (median (pd), 0.8326, 1e-4); %!assert_equal (median (t), 2.1664, 1e-4); %!assert_equal (pdf (pd, [0:5]), [0, 0.7358, 0.0733, 0.0007, 0, 0], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 4, 0.0404, 0, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1:4, NaN]), [0, 0.7358, 0.0733, 0.0007, 0, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1:4, NaN]), [0, 0, 4, 0.0404, 0, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 0.4633, 1e-4); %!assert_equal (std (t), 0.2083, 1e-4); %!assert_equal (var (pd), 0.2146, 1e-4); %!assert_equal (var (t), 0.0434, 1e-4); %!test %! ## The profile over the first free parameter: 21 grid values, one row of %! ## OTHER per value, and the likelihood peaking at the fitted estimate. %! x = [1.2; 0.4; 3.1; 0.7; 2.5; 1.8; 0.3; 4.2; 1.1; 0.9; ... %! 2.2; 0.6; 1.5; 3.7; 0.8; 2.9; 1.3; 0.5; 2.0; 1.6]; %! pd = fitdist (x, 'Nakagami'); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (size (param), [1, 21]); %! assert_equal (size (other), [21, 1]); %! assert_equal (proflik (pd), nlogL); %! [~, imax] = max (nlogL); %! assert_equal (abs (param(imax) - pd.ParameterValues(1)) <= param(2) - param(1), true); ## Test input validation ## 'prob.NakagamiDistribution' constructor %!error ... %! prob.NakagamiDistribution (Inf, 1) %!error ... %! prob.NakagamiDistribution (i, 1) %!error ... %! prob.NakagamiDistribution ('a', 1) %!error ... %! prob.NakagamiDistribution ([1, 2], 1) %!error ... %! prob.NakagamiDistribution (NaN, 1) %!error ... %! prob.NakagamiDistribution (1, 0) %!error ... %! prob.NakagamiDistribution (1, -1) %!error ... %! prob.NakagamiDistribution (1, Inf) %!error ... %! prob.NakagamiDistribution (1, i) %!error ... %! prob.NakagamiDistribution (1, 'a') %!error ... %! prob.NakagamiDistribution (1, [1, 2]) %!error ... %! prob.NakagamiDistribution (1, NaN) ## 'cdf' method %!error ... %! cdf (prob.NakagamiDistribution, 2, 'uper') %!error ... %! cdf (prob.NakagamiDistribution, 2, 3) ## 'paramci' method %!shared x %! x = nakarnd (1, 0.5, [1, 100]); %!error ... %! paramci (prob.NakagamiDistribution.fit (x), 'alpha') %!error ... %! paramci (prob.NakagamiDistribution.fit (x), 'alpha', 0) %!error ... %! paramci (prob.NakagamiDistribution.fit (x), 'alpha', 1) %!error ... %! paramci (prob.NakagamiDistribution.fit (x), 'alpha', [0.5 2]) %!error ... %! paramci (prob.NakagamiDistribution.fit (x), 'alpha', '') %!error ... %! paramci (prob.NakagamiDistribution.fit (x), 'alpha', {0.05}) %!error ... %! paramci (prob.NakagamiDistribution.fit (x), 'parameter', 'mu', 'alpha', {0.05}) %!error ... %! paramci (prob.NakagamiDistribution.fit (x), 'parameter', {'mu', 'omega', 'param'}) %!error ... %! paramci (prob.NakagamiDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', {'mu', 'omega', 'param'}) %!error ... %! paramci (prob.NakagamiDistribution.fit (x), 'parameter', 'param') %!error ... %! paramci (prob.NakagamiDistribution.fit (x), 'alpha', 0.01, 'parameter', 'param') %!error ... %! paramci (prob.NakagamiDistribution.fit (x), 'NAME', 'value') %!error ... %! paramci (prob.NakagamiDistribution.fit (x), 'alpha', 0.01, 'NAME', 'value') %!error ... %! paramci (prob.NakagamiDistribution.fit (x), 'alpha', 0.01, 'parameter', 'mu', ... %! 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.NakagamiDistribution, 'Parent') %!error ... %! plot (prob.NakagamiDistribution, 'PlotType', 12) %!error ... %! plot (prob.NakagamiDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.NakagamiDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.NakagamiDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.NakagamiDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.NakagamiDistribution, 'Discrete', {true}) %!error ... %! plot (prob.NakagamiDistribution, 'Parent', 12) %!error ... %! plot (prob.NakagamiDistribution, 'Parent', 'hax') %!error ... %! plot (prob.NakagamiDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.NakagamiDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.NakagamiDistribution, 2) %!error ... %! proflik (prob.NakagamiDistribution.fit (x), 3) %!error ... %! proflik (prob.NakagamiDistribution.fit (x), [1, 2]) %!error ... %! proflik (prob.NakagamiDistribution.fit (x), {1}) %!error ... %! proflik (prob.NakagamiDistribution.fit (x), 1, ones (2)) %!error ... %! proflik (prob.NakagamiDistribution.fit (x), 1, 'Display') %!error ... %! proflik (prob.NakagamiDistribution.fit (x), 1, 'Display', 1) %!error ... %! proflik (prob.NakagamiDistribution.fit (x), 1, 'Display', {1}) %!error ... %! proflik (prob.NakagamiDistribution.fit (x), 1, 'Display', {'on'}) %!error ... %! proflik (prob.NakagamiDistribution.fit (x), 1, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.NakagamiDistribution.fit (x), 1, 'Display', 'onnn') %!error ... %! proflik (prob.NakagamiDistribution.fit (x), 1, 'NAME', 'on') %!error ... %! proflik (prob.NakagamiDistribution.fit (x), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.NakagamiDistribution.fit (x), 1, {[1 2 3 4]}, 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.NakagamiDistribution) %!error ... %! truncate (prob.NakagamiDistribution, 2) %!error ... %! truncate (prob.NakagamiDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.NakagamiDistribution (1, 0.5); %! pd(2) = prob.NakagamiDistribution (1, 0.6); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/NegativeBinomialDistribution.m000066400000000000000000001271741527311104300314560ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef NegativeBinomialDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.NegativeBinomialDistribution ## ## Negative binomial probability distribution object. ## ## A @code{prob.NegativeBinomialDistribution} object consists of parameters, a ## model description, and sample data for a negative binomial probability ## distribution. ## ## The negative binomial distribution is a discrete probability distribution ## that models the number of failures in a sequence of independent and ## identically distributed Bernoulli trials before a specified (non-random) ## number of successes occurs. It is defined by the number of successes ## @var{R} and the probability of success @var{P}. ## ## There are several ways to create a @code{prob.NegativeBinomialDistribution} ## object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor ## @qcode{prob.NegativeBinomialDistribution (@var{R}, @var{P})} to create a ## negative binomial distribution with fixed parameter values @var{R} and ## @var{P}. ## @item Use the static method ## @qcode{prob.NegativeBinomialDistribution.fit ## (@var{x}, ## @var{freq}, @var{options})} to fit a distribution to the data in @var{x} ## using the same input arguments as the @code{nbinfit} function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the negative binomial distribution can be found ## at @url{https://en.wikipedia.org/wiki/Negative_binomial_distribution} ## ## @seealso{fitdist, makedist, nbincdf, nbininv, nbinpdf, nbinrnd, nbinfit, ## nbinlike, nbinstat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.NegativeBinomialDistribution} {property} R ## ## Number of successes ## ## A scalar value characterizing the number of successes in the ## negative binomial distribution. You can access the @qcode{R} ## property using dot name assignment. ## ## @end deftp R ## -*- texinfo -*- ## @deftp {prob.NegativeBinomialDistribution} {property} P ## ## Probability of success ## ## A scalar value characterizing the probability of success in the ## negative binomial distribution. You can access the @qcode{P} ## property using dot name assignment. ## ## @end deftp P endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.NegativeBinomialDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Negative Binomial'; ## -*- texinfo -*- ## @deftp {prob.NegativeBinomialDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 2; ## -*- texinfo -*- ## @deftp {prob.NegativeBinomialDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'R', 'P'}; ## -*- texinfo -*- ## @deftp {prob.NegativeBinomialDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Number of successes', 'Probability of success'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = false; DistributionCode = 'nbin'; ParameterRange = [realmin, realmin; Inf, 1]; ParameterLogCI = [true, true]; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.NegativeBinomialDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{2*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{R} and @qcode{P} ## properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.NegativeBinomialDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.NegativeBinomialDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.NegativeBinomialDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{2*2} numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter, and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only ## meaningful when the distribution was fitted to data. If the distribution ## object was created with fixed parameters, or a parameter of a fitted ## distribution is modified, then all elements of the variance-covariance ## are zero. This property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.NegativeBinomialDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*2} logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.NegativeBinomialDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: a numeric vector of logical values indicating ## censoring information corresponding to the elements of the data used for ## distribution fitting. If no censoring vector was used for distribution ## fitting, then this field defaults to an empty array. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.NegativeBinomialDistribution} {@var{pd} =} NegativeBinomialDistribution (@var{R}, @var{P}) ## @deftypefnx {prob.NegativeBinomialDistribution} {@var{pd} =} NegativeBinomialDistribution () ## ## Create a @code{prob.NegativeBinomialDistribution} object. ## ## @var{R} and @var{P} are the distribution parameters, which the class help ## describes. Called with no arguments the parameters take their defaults, ## @var{R} 1 and @var{P} 0.5. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = NegativeBinomialDistribution (R, P) if (nargin == 0) R = 1; P = 0.5; endif checkparams (R, P); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [R, P]; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'negative binomial distribution'); endfunction function disp (this) __disp__ (this, 'negative binomial distribution'); endfunction function this = set.R (this, R) checkparams (R, this.P); this.InputData = []; this.ParameterValues(1) = R; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function R = get.R (this) R = this.ParameterValues(1); endfunction function this = set.P (this, P) checkparams (this.R, P); this.InputData = []; this.ParameterValues(2) = P; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function P = get.P (this) P = this.ParameterValues(2); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.NegativeBinomialDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.NegativeBinomialDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = nbincdf (x, this.R, this.P); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= nbincdf (lx - 1, this.R, this.P); p(! (lb | ub)) /= diff (nbincdf ([lx-1, ux], this.R, this.P)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NegativeBinomialDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif umax = nbininv (1, this.R, this.P); if (this.IsTruncated) ## Find out of range p values is_nan = p < 0 | p > 1; ## Get lower and upper boundaries lx = ceil (this.Truncation(1)); ux = floor (this.Truncation(2)); ux = min (ux, umax); lp = nbincdf (lx - 1, this.R, this.P); up = nbincdf (ux, this.R, this.P); p = lp + p * (up - lp); p(is_nan) = NaN; endif x = nbininv (p, this.R, this.P); if (this.IsTruncated) x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NegativeBinomialDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.NegativeBinomialDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif [um, uv] = nbinstat (this.R, this.P); if (this.IsTruncated) lx = ceil (this.Truncation(1)); ux = floor (this.Truncation(2)); ux = min (ux, nbininv (1, this.R, this.P)); ## Handle infinite support on the right if (isequal (ux, Inf)) ratio = 1 / diff (nbincdf ([lx-1, ux], this.R, this.P)); x = 0:lx-1; m = ratio * (um - sum (x .* nbinpdf (x, this.R, this.P))); else x = lx:ux; m = sum (x .* pdf (this, x)); endif else m = um; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NegativeBinomialDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = nbincdf ([lx, ux], this.R, this.P); m = nbininv (sum (Fa_b) / 2, this.R, this.P); else m = nbininv (0.5, this.R, this.P); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NegativeBinomialDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = nbinlike ([this.R, this.P], this.InputData.data); endfunction ## -*- texinfo -*- ## @deftypefn {prob.NegativeBinomialDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.NegativeBinomialDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NegativeBinomialDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = nbinpdf (x, this.R, this.P); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (nbincdf ([lx-1, ux], this.R, this.P)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NegativeBinomialDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.NegativeBinomialDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.NegativeBinomialDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, true, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NegativeBinomialDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.NegativeBinomialDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.NegativeBinomialDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.NegativeBinomialDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.NegativeBinomialDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.NegativeBinomialDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the negative binomial distribution, @qcode{@var{pnum} = 1} selects ## the parameter @qcode{R} and @qcode{@var{pnum} = 2} selects the ## parameter @qcode{P}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.NegativeBinomialDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.NegativeBinomialDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.NegativeBinomialDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.NegativeBinomialDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{nbindrnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) sz = [varargin{:}]; ps = prod (sz); ## Get an estimate of how many more random numbers we need to randomly ## pick the appropriate size from lx = this.Truncation(1); ux = this.Truncation(2); ratio = 1 / diff (nbincdf ([lx-1, ux], this.R, this.P)); nsize = fix (2 * ratio * ps); # times 2 to be on the safe side ## Generate the numbers and remove out-of-bound random samples r = nbinrnd (this.R, this.P, nsize, 1); r(r < lx | r > ux) = []; ## Randomly select the required size and reshape to requested dimensions idx = randperm (numel (r), ps); r = reshape (r(idx), sz); else r = nbinrnd (this.R, this.P, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NegativeBinomialDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.NegativeBinomialDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, and upper limit, @var{upper}. If @var{pd} is fitted to data ## with @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); endif ## Check boundaries and constrain within support: Natural numbers lower = round (lower); upper = round (upper); lower(lower < 0) = 0; if (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.NegativeBinomialDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) ## Calculate untruncated mean and variance [um, uv] = nbinstat (this.R, this.P); ## Calculate truncated mean m = mean (this); ## Get lower and upper boundaries lx = ceil (this.Truncation(1)); ux = floor (this.Truncation(2)); ux = min (ux, nbininv (1, this.R, this.P)); ## Handle infinite support on the right if (isequal (ux, Inf)) ratio = 1 / diff (nbincdf ([lx-1, ux], this.R, this.P)); x = 0:lx-1; v = ratio * (uv + (um - m) ^ 2 - sum (((x - m) .^ 2) .* ... nbinpdf (x, this.R, this.P))); else x = lx:ux; v = sum (((x - m) .^ 2) .* pdf (this, x)); endif else [~, v] = nbinstat (this.R, this.P); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) freq = []; else freq = varargin{2}; endif if (nargin < 4) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else options = varargin{3}; endif ## Fit data [phat, pci] = nbinfit (x, alpha, freq, options); [~, acov] = nbinlike (phat, x, freq); ## Create fitted distribution object pd = prob.NegativeBinomialDistribution.makeFitted (phat, pci, acov, x, freq); endfunction function pd = makeFitted (phat, pci, acov, x, freq) R = phat(1); P = phat(2); pd = prob.NegativeBinomialDistribution (R, P); pd.ParameterCI = pci; pd.ParameterIsFixed = [false, false]; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', [], 'freq', freq); endfunction endmethods endclassdef function checkparams (R, P) if (! (isscalar (R) && isnumeric (R) && isreal (R) && isfinite (R) && R > 0)) error ("NegativeBinomialDistribution: R must be a positive scalar.") endif if (! (isscalar (P) && isnumeric (P) && isreal (P) && isfinite (P) && P > 0 && P <= 1)) error (strcat ("NegativeBinomialDistribution: P must be a real", ... " scalar bounded in the range (0, 1].")) endif endfunction %!demo %! ## Generate a data set of 5000 random samples from a Negative Binomial %! ## distribution with parameters R = 5 and P = 0.5. Fit a Negative Binomial %! ## distribution to this data and plot a PDF of the fitted distribution %! ## superimposed on a histogram of the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ('NegativeBinomial', 'R', 5, 'P', 0.5) %! data = random (pd_fixed, 5000, 1); %! pd_fitted = fitdist (data, 'NegativeBinomial') %! plot (pd_fitted) %! msg = 'Fitted Negative Binomial distribution with R = %0.2f and P = %0.2f'; %! title (sprintf (msg, pd_fitted.R, pd_fitted.P)) ## Test output %!shared pd, t, t_inf %! pd = prob.NegativeBinomialDistribution (5, 0.5); %! t = truncate (pd, 2, 4); %! t_inf = truncate (pd, 2, Inf); %!assert_equal (cdf (pd, [0:5]), [0.0312, 0.1094, 0.2266, 0.3633, 0.5, 0.6230], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0.3, 0.65, 1, 1], 1e-4); %!assert_equal (cdf (t_inf, [0:5]), [0, 0, 0.1316, 0.2851, 0.4386, 0.5768], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4]), [0.1094, 0.2266, 0.3633, 0.5000], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4]), [0, 0.3, 0.65, 1], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [0, 2, 4, 5, 7, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2, 3, 3, 4, 4], 1e-4); %!assert_equal (icdf (t_inf, [0:0.2:1]), [2, 3, 4, 6, 8, Inf], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 4, 5, 7, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 3, 3, 4, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 4); %!assert_equal (iqr (t), 2); %!assert_equal (mean (pd), 5); %!assert_equal (mean (t), 3.0500, 1e-4); %!assert_equal (mean (t_inf), 5.5263, 1e-4); %!assert_equal (median (pd), 4); %!assert_equal (median (t), 3); %!assert_equal (pdf (pd, [0:5]), [0.0312, 0.0781, 0.1172, 0.1367, 0.1367, 0.1230], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 0.3, 0.35, 0.35, 0], 1e-4); %!assert_equal (pdf (t_inf, [0:5]), [0, 0, 0.1316, 0.1535, 0.1535, 0.1382], 1e-4); %!assert_equal (pdf (pd, [-1, 1:4, NaN]), [0, 0.0781, 0.1172, 0.1367, 0.1367, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1:4, NaN]), [0, 0, 0.3, 0.35, 0.35, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 3.1623, 1e-4); %!assert_equal (std (t), 0.8047, 1e-4); %!assert_equal (std (t_inf), 2.9445, 1e-4); %!assert_equal (var (pd), 10); %!assert_equal (var (t), 0.6475, 1e-4); %!assert_equal (var (t_inf), 8.6704, 1e-4); %!test %! ## The profile over the first free parameter: 21 grid values, one row of %! ## OTHER per value, and the likelihood peaking at the fitted estimate. %! x = [8; 2; 14; 1; 5; 22; 6; 5; 3; 9; 2; 17; 1; 3; 2; 11; 6; 2; 30; 1]; %! pd = fitdist (x, 'NegativeBinomial'); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (size (param), [1, 21]); %! assert_equal (size (other), [21, 1]); %! assert_equal (proflik (pd), nlogL); %! [~, imax] = max (nlogL); %! assert_equal (abs (param(imax) - pd.ParameterValues(1)) <= param(2) - param(1), true); ## Test input validation ## 'prob.NegativeBinomialDistribution' constructor %!error ... %! prob.NegativeBinomialDistribution (Inf, 1) %!error ... %! prob.NegativeBinomialDistribution (i, 1) %!error ... %! prob.NegativeBinomialDistribution ('a', 1) %!error ... %! prob.NegativeBinomialDistribution ([1, 2], 1) %!error ... %! prob.NegativeBinomialDistribution (NaN, 1) %!error ... %! prob.NegativeBinomialDistribution (1, 0) %!error ... %! prob.NegativeBinomialDistribution (1, -1) %!error ... %! prob.NegativeBinomialDistribution (1, Inf) %!error ... %! prob.NegativeBinomialDistribution (1, i) %!error ... %! prob.NegativeBinomialDistribution (1, 'a') %!error ... %! prob.NegativeBinomialDistribution (1, [1, 2]) %!error ... %! prob.NegativeBinomialDistribution (1, NaN) %!error ... %! prob.NegativeBinomialDistribution (1, 1.2) ## 'cdf' method %!error ... %! cdf (prob.NegativeBinomialDistribution, 2, 'uper') %!error ... %! cdf (prob.NegativeBinomialDistribution, 2, 3) ## 'paramci' method %!shared x %! x = nbinrnd (1, 0.5, [1, 100]); %!error ... %! paramci (prob.NegativeBinomialDistribution.fit (x), 'alpha') %!error ... %! paramci (prob.NegativeBinomialDistribution.fit (x), 'alpha', 0) %!error ... %! paramci (prob.NegativeBinomialDistribution.fit (x), 'alpha', 1) %!error ... %! paramci (prob.NegativeBinomialDistribution.fit (x), 'alpha', [0.5 2]) %!error ... %! paramci (prob.NegativeBinomialDistribution.fit (x), 'alpha', '') %!error ... %! paramci (prob.NegativeBinomialDistribution.fit (x), 'alpha', {0.05}) %!error ... %! paramci (prob.NegativeBinomialDistribution.fit (x), 'parameter', 'R', ... %! 'alpha', {0.05}) %!error ... %! paramci (prob.NegativeBinomialDistribution.fit (x), ... %! 'parameter', {'R', 'P', 'param'}) %!error ... %! paramci (prob.NegativeBinomialDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', {'R', 'P', 'param'}) %!error ... %! paramci (prob.NegativeBinomialDistribution.fit (x), 'parameter', 'param') %!error ... %! paramci (prob.NegativeBinomialDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', 'param') %!error ... %! paramci (prob.NegativeBinomialDistribution.fit (x), 'NAME', 'value') %!error ... %! paramci (prob.NegativeBinomialDistribution.fit (x), 'alpha', 0.01, ... %! 'NAME', 'value') %!error ... %! paramci (prob.NegativeBinomialDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', 'R', 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.NegativeBinomialDistribution, 'Parent') %!error ... %! plot (prob.NegativeBinomialDistribution, 'PlotType', 12) %!error ... %! plot (prob.NegativeBinomialDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.NegativeBinomialDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.NegativeBinomialDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.NegativeBinomialDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.NegativeBinomialDistribution, 'Discrete', {true}) %!error ... %! plot (prob.NegativeBinomialDistribution, 'Parent', 12) %!error ... %! plot (prob.NegativeBinomialDistribution, 'Parent', 'hax') %!error ... %! plot (prob.NegativeBinomialDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.NegativeBinomialDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.NegativeBinomialDistribution, 2) %!error ... %! proflik (prob.NegativeBinomialDistribution.fit (x), 3) %!error ... %! proflik (prob.NegativeBinomialDistribution.fit (x), [1, 2]) %!error ... %! proflik (prob.NegativeBinomialDistribution.fit (x), {1}) %!error ... %! proflik (prob.NegativeBinomialDistribution.fit (x), 1, ones (2)) %!error ... %! proflik (prob.NegativeBinomialDistribution.fit (x), 1, 'Display') %!error ... %! proflik (prob.NegativeBinomialDistribution.fit (x), 1, 'Display', 1) %!error ... %! proflik (prob.NegativeBinomialDistribution.fit (x), 1, 'Display', {1}) %!error ... %! proflik (prob.NegativeBinomialDistribution.fit (x), 1, 'Display', {'on'}) %!error ... %! proflik (prob.NegativeBinomialDistribution.fit (x), 1, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.NegativeBinomialDistribution.fit (x), 1, 'Display', 'onnn') %!error ... %! proflik (prob.NegativeBinomialDistribution.fit (x), 1, 'NAME', 'on') %!error ... %! proflik (prob.NegativeBinomialDistribution.fit (x), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.NegativeBinomialDistribution.fit (x), 1, {[1 2 3]}, 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.NegativeBinomialDistribution) %!error ... %! truncate (prob.NegativeBinomialDistribution, 2) %!error ... %! truncate (prob.NegativeBinomialDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.NegativeBinomialDistribution (1, 0.5); %! pd(2) = prob.NegativeBinomialDistribution (1, 0.6); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/NormalDistribution.m000066400000000000000000001243531527311104300274650ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef NormalDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.NormalDistribution ## ## Normal probability distribution object. ## ## A @code{prob.NormalDistribution} object consists of parameters, a model ## description, and sample data for a normal probability distribution. ## ## The normal distribution is a continuous probability distribution that is ## symmetric about the mean, @var{mu}, showing that data near the mean are ## more frequent in occurrence than data far from the mean. It is defined by ## location parameter @var{mu} and scale parameter @var{sigma}. ## ## There are several ways to create a @code{prob.NormalDistribution} object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor ## @qcode{prob.NormalDistribution (@var{mu}, ## @var{sigma})} ## to create a normal distribution with fixed parameter values @var{mu} and ## @var{sigma}. ## @item Use the static method @qcode{prob.NormalDistribution.fit (@var{x}, ## @var{censor}, @var{freq}, @var{options})} to fit a distribution to data ## @var{x}. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the normal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Normal_distribution} ## ## @seealso{fitdist, makedist, normcdf, norminv, normpdf, normrnd, normfit, ## normlike, normstat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.NormalDistribution} {property} mu ## ## Location parameter ## ## A scalar value characterizing the location of the normal distribution. ## You can access the @qcode{mu} property using dot name assignment. ## ## @end deftp mu ## -*- texinfo -*- ## @deftp {prob.NormalDistribution} {property} sigma ## ## Scale parameter ## ## A positive scalar value characterizing the scale of the normal ## distribution. You can access the @qcode{sigma} property using dot name ## assignment. ## ## @end deftp sigma endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.NormalDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Normal'; ## -*- texinfo -*- ## @deftp {prob.NormalDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 2; ## -*- texinfo -*- ## @deftp {prob.NormalDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'mu', 'sigma'}; ## -*- texinfo -*- ## @deftp {prob.NormalDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Mean', 'Standard Deviation'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = true; DistributionCode = 'norm'; ParameterRange = [-Inf, realmin; Inf, Inf]; ParameterLogCI = [true, true]; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.NormalDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{2*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{mu} and @qcode{sigma} ## properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.NormalDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.NormalDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.NormalDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{2*2} numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter, and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only meaningful ## when the distribution was fitted to data. If the distribution object was ## created with fixed parameters, or a parameter of a fitted distribution is ## modified, then all elements of the variance-covariance are zero. This ## property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.NormalDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*2} logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.NormalDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: a numeric vector of logical values indicating ## censoring information corresponding to the elements of the data used for ## distribution fitting. If no censoring vector was used for distribution ## fitting, then this field defaults to an empty array. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.NormalDistribution} {@var{pd} =} NormalDistribution (@var{mu}, @var{sigma}) ## @deftypefnx {prob.NormalDistribution} {@var{pd} =} NormalDistribution () ## ## Create a @code{prob.NormalDistribution} object. ## ## @var{mu} and @var{sigma} are the distribution parameters, which the class ## help describes. Called with no arguments the parameters take their ## defaults, @var{mu} 0 and @var{sigma} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = NormalDistribution (mu, sigma) if (nargin == 0) mu = 0; sigma = 1; endif checkparams (mu, sigma); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [mu, sigma]; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'normal distribution'); endfunction function disp (this) __disp__ (this, 'normal distribution'); endfunction function this = set.mu (this, mu) checkparams (mu, this.sigma); this.InputData = []; this.ParameterValues(1) = mu; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function mu = get.mu (this) mu = this.ParameterValues(1); endfunction function this = set.sigma (this, sigma) checkparams (this.mu, sigma); this.InputData = []; this.ParameterValues(2) = sigma; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function sigma = get.sigma (this) sigma = this.ParameterValues(2); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.NormalDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.NormalDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = normcdf (x, this.mu, this.sigma); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= normcdf (lx, this.mu, this.sigma); p(! (lb | ub)) /= diff (normcdf ([lx, ux], this.mu, this.sigma)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NormalDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = normcdf (this.Truncation(1), this.mu, this.sigma); up = normcdf (this.Truncation(2), this.mu, this.sigma); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = norminv (np, this.mu, this.sigma); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = norminv (p, this.mu, this.sigma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NormalDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.NormalDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = normstat (this.mu, this.sigma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NormalDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = normcdf ([lx, ux], this.mu, this.sigma); m = norminv (sum (Fa_b) / 2, this.mu, this.sigma); else m = norminv (0.5, this.mu, this.sigma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NormalDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = normlike ([this.mu, this.sigma], this.InputData.data, ... this.InputData.cens, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.NormalDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.NormalDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NormalDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = normpdf (x, this.mu, this.sigma); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (normcdf ([lx, ux], this.mu, this.sigma)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NormalDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.NormalDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.NormalDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NormalDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.NormalDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.NormalDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.NormalDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.NormalDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.NormalDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the normal distribution, @qcode{@var{pnum} = 1} selects the ## parameter @qcode{mu} and @qcode{@var{pnum} = 2} selects the parameter ## @qcode{sigma}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.NormalDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.NormalDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.NormalDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.NormalDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{normrnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = normcdf (this.Truncation(1), this.mu, this.sigma); up = normcdf (this.Truncation(2), this.mu, this.sigma); u = unifrnd (lp, up, varargin{:}); r = this.mu + this.sigma .* (-sqrt (2) * erfcinv (2 * u)); else r = normrnd (this.mu, this.sigma, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.NormalDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.NormalDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, and upper limit, @var{upper}. If @var{pd} is fitted to data ## with @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.NormalDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = normstat (this.mu, this.sigma); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) censor = []; else censor = varargin{2}; endif if (nargin < 4) freq = []; else freq = varargin{3}; endif if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else options = varargin{4}; endif ## Fit data [muhat, sigmahat, muci, sigmaci] = normfit ... (x, alpha, censor, freq, options); phat = [muhat, sigmahat]; pci = [muci(:), sigmaci(:)]; [~, acov] = normlike (phat, x, censor, freq); ## Create fitted distribution object pd = prob.NormalDistribution.makeFitted ... (phat, pci, acov, x, censor, freq); endfunction function pd = makeFitted (phat, pci, acov, x, censor, freq) mu = phat(1); sigma = phat(2); pd = prob.NormalDistribution (mu, sigma); pd.ParameterCI = pci; pd.ParameterIsFixed = [false, false]; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', censor, 'freq', freq); endfunction endmethods endclassdef function checkparams (mu, sigma) if (! (isscalar (mu) && isnumeric (mu) && isreal (mu) && isfinite (mu))) error ("NormalDistribution: MU must be a real scalar.") endif if (! (isscalar (sigma) && isnumeric (sigma) && isreal (sigma) && isfinite (sigma) && sigma > 0)) error ("NormalDistribution: SIGMA must be a positive real scalar.") endif endfunction %!demo %! ## Generate a data set of 5000 random samples from a Normal distribution with %! ## parameters mu = 0 and sigma = 1. Fit a Normal distribution to this data and plot %! ## a PDF of the fitted distribution superimposed on a histogram of the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ('Normal', 'mu', 0, 'sigma', 1) %! data = random (pd_fixed, 5000, 1); %! pd_fitted = fitdist (data, 'Normal') %! plot (pd_fitted) %! msg = 'Fitted Normal distribution with mu = %0.2f and sigma = %0.2f'; %! title (sprintf (msg, pd_fitted.mu, pd_fitted.sigma)) ## Test output %!shared pd, t %! pd = prob.NormalDistribution; %! t = truncate (pd, -2, 2); %!assert_equal (cdf (pd, [0:5]), [0.5, 0.8413, 0.9772, 0.9987, 1, 1], 1e-4); %!assert_equal (cdf (t, [0:5]), [0.5, 0.8576, 1, 1, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4]), [0.9332, 0.9772, 0.9987, 1], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4]), [0.9538, 1, 1, 1], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [-Inf, -0.8416, -0.2533, 0.2533, 0.8416, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [-2, -0.7938, -0.2416, 0.2416, 0.7938, 2], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, -0.2533, 0.2533, 0.8416, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, -0.2416, 0.2416, 0.7938, 2, NaN], 1e-4); %!assert_equal (iqr (pd), 1.3490, 1e-4); %!assert_equal (iqr (t), 1.2782, 1e-4); %!assert_equal (mean (pd), 0); %!assert_equal (mean (t), 0, 3e-16); %!assert_equal (median (pd), 0); %!assert_equal (median (t), 0, 3e-16); %!assert_equal (pdf (pd, [0:5]), [0.3989, 0.2420, 0.0540, 0.0044, 0.0001, 0], 1e-4); %!assert_equal (pdf (t, [0:5]), [0.4180, 0.2535, 0.0566, 0, 0, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1:4, NaN]), [0.2420, 0.2420, 0.0540, 0.0044, 0.0001, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1:4, NaN]), [0.2535, 0.2535, 0.0566, 0, 0, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < -2), false); %!assert_equal (any (random (t, 1000, 1) > 2), false); %!assert_equal (std (pd), 1); %!assert_equal (std (t), 0.8796, 1e-4); %!assert_equal (var (pd), 1); %!assert_equal (var (t), 0.7737, 1e-4); %!test %! ## With a further parameter to profile out the default grid takes 21 values. %! z = [0.3; -1.2; 0.8; 1.5; -0.4; 0.2; -0.9; 1.1; 0.6; -0.3; ... %! 1.8; -1.5; 0.4; 0.9; -0.7; 1.2; -0.2; 0.5; -1.1; 0.7]; %! [nlogL, param, other] = proflik (prob.NormalDistribution.fit (z), 1); %! assert_equal (size (param), [1, 21]); %! assert_equal (size (other), [21, 1]); %!test %! ## OTHER holds the profiled-out parameter maximizing the likelihood at each %! ## value of PARAM. Verified against MATLAB. %! z = [0.3; -1.2; 0.8; 1.5; -0.4; 0.2; -0.9; 1.1; 0.6; -0.3; ... %! 1.8; -1.5; 0.4; 0.9; -0.7; 1.2; -0.2; 0.5; -1.1; 0.7]; %! [nlogL, param, other] = proflik (prob.NormalDistribution.fit (z), 1, [-0.2, 0, 0.2]); %! assert_equal (other, [0.9937; 0.9346; 0.9162], 1e-4); ## Test input validation ## 'prob.NormalDistribution' constructor %!error ... %! prob.NormalDistribution (Inf, 1) %!error ... %! prob.NormalDistribution (i, 1) %!error ... %! prob.NormalDistribution ('a', 1) %!error ... %! prob.NormalDistribution ([1, 2], 1) %!error ... %! prob.NormalDistribution (NaN, 1) %!error ... %! prob.NormalDistribution (1, 0) %!error ... %! prob.NormalDistribution (1, -1) %!error ... %! prob.NormalDistribution (1, Inf) %!error ... %! prob.NormalDistribution (1, i) %!error ... %! prob.NormalDistribution (1, 'a') %!error ... %! prob.NormalDistribution (1, [1, 2]) %!error ... %! prob.NormalDistribution (1, NaN) ## 'cdf' method %!error ... %! cdf (prob.NormalDistribution, 2, 'uper') %!error ... %! cdf (prob.NormalDistribution, 2, 3) ## 'paramci' method %!shared x %! x = normrnd (1, 1, [1, 100]); %!error ... %! paramci (prob.NormalDistribution.fit (x), 'alpha') %!error ... %! paramci (prob.NormalDistribution.fit (x), 'alpha', 0) %!error ... %! paramci (prob.NormalDistribution.fit (x), 'alpha', 1) %!error ... %! paramci (prob.NormalDistribution.fit (x), 'alpha', [0.5 2]) %!error ... %! paramci (prob.NormalDistribution.fit (x), 'alpha', '') %!error ... %! paramci (prob.NormalDistribution.fit (x), 'alpha', {0.05}) %!error ... %! paramci (prob.NormalDistribution.fit (x), 'parameter', 'mu', 'alpha', {0.05}) %!error ... %! paramci (prob.NormalDistribution.fit (x), 'parameter', {'mu', 'sigma', 'param'}) %!error ... %! paramci (prob.NormalDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', {'mu', 'sigma', 'param'}) %!error ... %! paramci (prob.NormalDistribution.fit (x), 'parameter', 'param') %!error ... %! paramci (prob.NormalDistribution.fit (x), 'alpha', 0.01, 'parameter', 'param') %!error ... %! paramci (prob.NormalDistribution.fit (x), 'NAME', 'value') %!error ... %! paramci (prob.NormalDistribution.fit (x), 'alpha', 0.01, 'NAME', 'value') %!error ... %! paramci (prob.NormalDistribution.fit (x), 'alpha', 0.01, 'parameter', 'mu', ... %! 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.NormalDistribution, 'Parent') %!error ... %! plot (prob.NormalDistribution, 'PlotType', 12) %!error ... %! plot (prob.NormalDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.NormalDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.NormalDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.NormalDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.NormalDistribution, 'Discrete', {true}) %!error ... %! plot (prob.NormalDistribution, 'Parent', 12) %!error ... %! plot (prob.NormalDistribution, 'Parent', 'hax') %!error ... %! plot (prob.NormalDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.NormalDistribution, 'PlotType', 'probability') ## 'proflik' method %!test %! ## Profile log-likelihood values verified against MATLAB (nuisance %! ## parameters are profiled out, not held fixed). %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! pd = prob.NormalDistribution.fit (x'); %! [ll, param] = proflik (pd, 1, 2.6:0.2:4.2); %! ref = [-20.3706543783445, -19.2807332475991, -18.3460903001355, ... %! -17.6848714616926, -17.4098041938949, -17.5763530516294, ... %! -18.1502877476524, -19.0282120496015, -20.0892153912834]; %! assert_equal (ll, ref, 1e-6); %! assert_equal (param, 2.6:0.2:4.2, 1e-12); %!test %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! pd = prob.NormalDistribution.fit (x'); %! ll = proflik (pd, 2, 0.7:0.1:1.6); %! ref = [-19.7905304181301, -18.3358679076856, -17.6533683093276, ... %! -17.4185123984561, -17.4530093494964, -17.6534891355391, ... %! -17.9574383057938, -18.3257710746453, -18.7333992513096, ... %! -19.1638879806549]; %! assert_equal (ll, ref, 1e-6); %!test %! ## Default grid spans the 98% CI with 21 points (matching MATLAB). %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! pd = prob.NormalDistribution.fit (x'); %! [ll, param] = proflik (pd, 1); %! assert_equal (numel (param), 21); %! assert_equal ([param(1), param(end)], ... %! [2.57917008770431, 4.27082991229569], 1e-6); %!error ... %! proflik (prob.NormalDistribution, 2) %!error ... %! proflik (prob.NormalDistribution.fit (x), 3) %!error ... %! proflik (prob.NormalDistribution.fit (x), [1, 2]) %!error ... %! proflik (prob.NormalDistribution.fit (x), {1}) %!error ... %! proflik (prob.NormalDistribution.fit (x), 1, ones (2)) %!error ... %! proflik (prob.NormalDistribution.fit (x), 1, 'Display') %!error ... %! proflik (prob.NormalDistribution.fit (x), 1, 'Display', 1) %!error ... %! proflik (prob.NormalDistribution.fit (x), 1, 'Display', {1}) %!error ... %! proflik (prob.NormalDistribution.fit (x), 1, 'Display', {'on'}) %!error ... %! proflik (prob.NormalDistribution.fit (x), 1, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.NormalDistribution.fit (x), 1, 'Display', 'onnn') %!error ... %! proflik (prob.NormalDistribution.fit (x), 1, 'NAME', 'on') %!error ... %! proflik (prob.NormalDistribution.fit (x), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.NormalDistribution.fit (x), 1, {[1 2 3 4]}, 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.NormalDistribution) %!error ... %! truncate (prob.NormalDistribution, 2) %!error ... %! truncate (prob.NormalDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.NormalDistribution (1, 1); %! pd(2) = prob.NormalDistribution (1, 3); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!test %! ## negloglik returns the (positive) negative log-likelihood. %! xdat = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! pdfit = prob.NormalDistribution.fit (xdat'); %! assert_equal (negloglik (pdfit), -sum (log (pdf (pdfit, xdat'))), 1e-9); %! assert_equal (negloglik (pdfit) > 0, true); %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/PiecewiseLinearDistribution.m000066400000000000000000000724351527311104300313100ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef PiecewiseLinearDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.PiecewiseLinearDistribution ## ## Piecewise linear probability distribution object. ## ## A @code{prob.PiecewiseLinearDistribution} object consists of parameters, a ## model description, and sample data for a piecewise linear probability ## distribution. ## ## The piecewise linear distribution is a continuous probability distribution ## that is defined by a set of points where the cumulative distribution ## function (CDF) changes slope. It is defined by a vector of @math{x} values ## and a corresponding vector of CDF values @var{Fx}. ## ## There are several ways to create a @code{prob.PiecewiseLinearDistribution} ## object. ## ## @itemize ## @item Create a distribution with specified parameter values using the ## @code{makedist} function. ## @item Use the constructor @qcode{prob.PiecewiseLinearDistribution (@var{x}, ## @var{Fx})} to create a piecewise linear distribution with specified ## parameter values @var{x} and @var{Fx}. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the piecewise linear distribution can be found at ## @url{https://en.wikipedia.org/wiki/Piecewise_linear_function} ## ## @seealso{makedist, plcdf, plinv, plpdf, plrnd, plstat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.PiecewiseLinearDistribution} {property} x ## ## Vector of x values ## ## A numeric row vector of @math{x} values at which the CDF changes slope, ## reported as a row whichever way it was given. You can access the ## @qcode{x} property using dot name assignment. ## ## @end deftp x ## -*- texinfo -*- ## @deftp {prob.PiecewiseLinearDistribution} {property} Fx ## ## Vector of CDF values ## ## A numeric row vector of CDF values that correspond to each value in ## @math{x}, reported as a row whichever way it was given. You can access ## the @qcode{Fx} property using dot name assignment. ## ## @end deftp Fx endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.PiecewiseLinearDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Piecewise Linear'; ## -*- texinfo -*- ## @deftp {prob.PiecewiseLinearDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 2; ## -*- texinfo -*- ## @deftp {prob.PiecewiseLinearDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'x', 'Fx'}; ## -*- texinfo -*- ## @deftp {prob.PiecewiseLinearDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'x', 'cdf = F(x)'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = false; DistributionCode = 'pl'; ParameterRange = [-Inf, Inf; -Inf, Inf; 0, 1; 0, 1]; ParameterLogCI = [false, false, false, false]; endproperties properties(GetAccess = public , SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.PiecewiseLinearDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{2*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{x} and @qcode{Fx} ## properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.PiecewiseLinearDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.PiecewiseLinearDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.PiecewiseLinearDistribution} {@var{pd} =} PiecewiseLinearDistribution (@var{x}, @var{Fx}) ## @deftypefnx {prob.PiecewiseLinearDistribution} {@var{pd} =} PiecewiseLinearDistribution () ## ## Create a @code{prob.PiecewiseLinearDistribution} object. ## ## @var{x} and @var{Fx} are the distribution parameters, which the class ## help describes. Called with no arguments the parameters take their ## defaults, @var{x} @code{[0; 1]} and @var{Fx} @code{[0; 1]}. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = PiecewiseLinearDistribution (x, Fx) if (nargin == 0) x = [0; 1]; Fx = [0; 1]; else x = x(:); Fx = Fx(:); endif checkparams (x, Fx); this.IsTruncated = false; this.ParameterValues = {x(:)', Fx(:)'}; endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'Piecewise Linear distribution'); endfunction function disp (this) __disp__ (this, 'Piecewise Linear distribution'); endfunction ## X and FX are reported as rows, as MATLAB reports them, whichever way ## they were given; ParameterValues holds one entry per parameter, so ## each is a row of its own. function this = set.x (this, x) checkparams (x(:), this.Fx(:)); this.ParameterValues{1} = x(:)'; endfunction function x = get.x (this) x = this.ParameterValues{1}; endfunction function this = set.Fx (this, Fx) checkparams (this.x(:), Fx(:)); this.ParameterValues{2} = Fx(:)'; endfunction function Fx = get.Fx (this) Fx = this.ParameterValues{2}; endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.PiecewiseLinearDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.PiecewiseLinearDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = plcdf (x, this.x, this.Fx); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= plcdf (lx, this.x, this.Fx); p(! (lb | ub)) /= diff (plcdf ([lx, ux], this.x, this.Fx)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.PiecewiseLinearDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = plcdf (this.Truncation(1), this.x, this.Fx); up = plcdf (this.Truncation(2), this.x, this.Fx); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = plinv (np, this.x, this.Fx); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = plinv (p, this.x, this.Fx); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.PiecewiseLinearDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.PiecewiseLinearDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) m = truncated_moments (this.x, this.Fx, this.Truncation(1), ... this.Truncation(2)); else m = plstat (this.x, this.Fx); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.PiecewiseLinearDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = plcdf ([lx, ux], this.x, this.Fx); m = plinv (sum (Fa_b) / 2, this.x, this.Fx); else m = plinv (0.5, this.x, this.Fx); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.PiecewiseLinearDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = plpdf (x, this.x, this.Fx); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (plcdf ([lx, ux], this.x, this.Fx)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.PiecewiseLinearDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.PiecewiseLinearDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.PiecewiseLinearDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.PiecewiseLinearDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.PiecewiseLinearDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.PiecewiseLinearDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.PiecewiseLinearDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{betarnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) x = this.x(:)'; Fx = this.Fx(:)'; lp = plcdf (this.Truncation(1), x, Fx); up = plcdf (this.Truncation(2), x, Fx); u = unifrnd (lp, up, varargin{:}); r = zeros (size (u)); [~, bin] = histc (u(:)', Fx); r0 = x(bin); dx = diff (x); dF = diff (Fx); dr = (u(:)' - Fx(bin)) .* dx(bin) ./ dF(bin); r(:) = r0 + dr; else r = plrnd (this.x, this.Fx, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.PiecewiseLinearDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.PiecewiseLinearDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd})} returns a probability distribution ## @var{t}, which is the probability distribution @var{pd} truncated to the ## specified interval with lower limit, @var{lower}, and upper limit, ## @var{upper}. If @var{pd} is fitted to data with @code{fitdist}, the ## returned probability distribution @var{t} is not fitted, does not contain ## any data or estimated values, and it is as it has been created with the ## @var{makedist} function, but it includes the truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; endfunction ## -*- texinfo -*- ## @deftypefn {prob.PiecewiseLinearDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) [m, m2] = truncated_moments (this.x, this.Fx, this.Truncation(1), ... this.Truncation(2)); v = m2 - m ^ 2; else [~, v] = plstat (this.x, this.Fx); endif endfunction endmethods endclassdef ## First and second raw moments over the truncation interval, taken segment by ## segment. The pdf of a piecewise-linear cdf is piecewise constant, so ## x^k * pdf(x) is a polynomial on each segment and integrates exactly; ## adaptive quadrature instead sees a jump at every knot and costs four to ## five digits, which is what this replaced. function [m1, m2] = truncated_moments (x, Fx, lx, ux) x = x(:); Fx = Fx(:); Z = plcdf (ux, x, Fx) - plcdf (lx, x, Fx); m1 = 0; m2 = 0; for i = 1:numel (x) - 1 a = max (x(i), lx); b = min (x(i+1), ux); if (b <= a) continue; endif d = (Fx(i+1) - Fx(i)) / (x(i+1) - x(i)); m1 += d * (b ^ 2 - a ^ 2) / 2; m2 += d * (b ^ 3 - a ^ 3) / 3; endfor m1 /= Z; m2 /= Z; endfunction function checkparams (x, Fx) if (! (isvector (x) && isnumeric (x) && isreal (x) && isfinite (x))) error ("PiecewiseLinearDistribution: X must be a real vector.") endif if (! (isvector (Fx) && isnumeric (Fx) && isreal (Fx) && isfinite (Fx))) error ("PiecewiseLinearDistribution: Fx must be a real vector.") endif if (! isvector (x) || ! isvector (Fx) || ! isequal (size (x), size (Fx))) error (strcat ("PiecewiseLinearDistribution: X and FX must", ... " be vectors of equal size.")); endif if (length (x) < 2 || length (Fx) < 2) error (strcat ("PiecewiseLinearDistribution: X and FX must", ... " be at least two-elements long.")); endif if (any (Fx < 0) || any (Fx > 1)) error (strcat ("PiecewiseLinearDistribution: FX must be", ... " bounded in the range [0, 1].")); endif endfunction %!demo %! ## Generate a data set of 5000 random samples from a Beta distribution with %! ## parameters a = 2 and b = 5 scaled to [0,10]. %! ## Compute empirical CDF, subsample, create prob.PiecewiseLinearDistribution, %! ## and plot the PDF superimposed on a histogram of the data. %! %! rng (42); %! randg ('state', 42); %! data = betarnd (2, 5, 5000, 1) * 10; %! [f, x] = ecdf (data); %! f = f(1:5:end); %! x = x(1:5:end); %! pd = prob.PiecewiseLinearDistribution (x, f); %! [counts, centers] = hist (data, 50); %! bin_width = centers(2) - centers(1); %! bar (centers, counts / (sum (counts) * bin_width), 1); %! hold on %! vals = min (data):0.1:max (data); %! y = pdf (pd, vals); %! plot (vals, y, '-r', 'LineWidth', 2) %! hold off %! title ('Piecewise Linear approximation to scaled Beta(2,5) data') %! legend ('Histogram', 'Piecewise PDF') ## Test output %!shared pd, t %! load patients %! [f, x] = ecdf (Weight); %! f = f(1:5:end); %! x = x(1:5:end); %! pd = prob.PiecewiseLinearDistribution (x, f); %! t = truncate (pd, 130, 180); %!assert_equal (cdf (pd, [120, 130, 140, 150, 200]), [0.0767, 0.25, 0.4629, 0.5190, 0.9908], 1e-4); %!assert_equal (cdf (t, [120, 130, 140, 150, 200]), [0, 0, 0.4274, 0.5403, 1], 1e-4); %!assert_equal (cdf (pd, [100, 250, NaN]), [0, 1, NaN], 1e-4); %!assert_equal (cdf (t, [115, 290, NaN]), [0, 1, NaN], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [111, 127.5, 136.62, 169.67, 182.17, 202], 1e-2); %!assert_equal (icdf (t, [0:0.2:1]), [130, 134.15, 139.26, 162.5, 173.99, 180], 1e-2); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NA, 136.62, 169.67, 182.17, 202, NA], 1e-2); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NA, 139.26, 162.5, 173.99, 180, NA], 1e-2); %!assert_equal (iqr (pd), 50.0833, 1e-4); %!assert_equal (iqr (t), 36.8077, 1e-4); %!assert_equal (mean (pd), 153.61, 1e-10); %!assert_equal (mean (t), 152.30321285140542, 1e-10); %!assert_equal (median (pd), 142, 1e-10); %!assert_equal (median (t), 141.9462, 1e-4); %!assert_equal (pdf (pd, [120, 130, 140, 150, 200]), [0.0133, 0.0240, 0.0186, 0.0024, 0.0004], 6e-3); %!assert_equal (pdf (t, [120, 130, 140, 150, 200]), [0, 0.0482, 0.0373, 0.0048, 0], 1e-4); %!assert_equal (pdf (pd, [100, 250, NaN]), [0, 0, NaN], 1e-4); %!assert_equal (pdf (t, [100, 250, NaN]), [0, 0, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 130), false); %!assert_equal (any (random (t, 1000, 1) > 180), false); %!assert_equal (std (pd), 26.5196, 1e-4); %!assert_equal (std (t), 18.293981947282326, 1e-10); %!assert_equal (var (pd), 703.2879, 1e-4); %!assert_equal (var (t), 334.66977548749168, 1e-10); ## Test input validation ## 'prob.PiecewiseLinearDistribution' constructor %!error ... %! prob.PiecewiseLinearDistribution ([0, i], [0, 1]) %!error ... %! prob.PiecewiseLinearDistribution ([0, Inf], [0, 1]) %!error ... %! prob.PiecewiseLinearDistribution (['a', 'c'], [0, 1]) %!error ... %! prob.PiecewiseLinearDistribution ([NaN, 1], [0, 1]) %!error ... %! prob.PiecewiseLinearDistribution ([0, 1], [0, i]) %!error ... %! prob.PiecewiseLinearDistribution ([0, 1], [0, Inf]) %!error ... %! prob.PiecewiseLinearDistribution ([0, 1], ['a', 'c']) %!error ... %! prob.PiecewiseLinearDistribution ([0, 1], [NaN, 1]) %!error ... %! prob.PiecewiseLinearDistribution ([0, 1], [0, 0.5, 1]) %!error ... %! prob.PiecewiseLinearDistribution ([0], [1]) %!error ... %! prob.PiecewiseLinearDistribution ([0, 0.5, 1], [0, 1, 1.5]) ## 'cdf' method %!error ... %! cdf (prob.PiecewiseLinearDistribution, 2, 'uper') %!error ... %! cdf (prob.PiecewiseLinearDistribution, 2, 3) ## 'plot' method %!error ... %! plot (prob.PiecewiseLinearDistribution, 'Parent') %!error ... %! plot (prob.PiecewiseLinearDistribution, 'PlotType', 12) %!error ... %! plot (prob.PiecewiseLinearDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.PiecewiseLinearDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.PiecewiseLinearDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.PiecewiseLinearDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.PiecewiseLinearDistribution, 'Discrete', {true}) %!error ... %! plot (prob.PiecewiseLinearDistribution, 'Parent', 12) %!error ... %! plot (prob.PiecewiseLinearDistribution, 'Parent', 'hax') %!error ... %! plot (prob.PiecewiseLinearDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.PiecewiseLinearDistribution, 'PlotType', 'probability') ## 'truncate' method %!error ... %! truncate (prob.PiecewiseLinearDistribution) %!error ... %! truncate (prob.PiecewiseLinearDistribution, 2) %!error ... %! truncate (prob.PiecewiseLinearDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.PiecewiseLinearDistribution (); %! pd(2) = prob.PiecewiseLinearDistribution (); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error pdf (pd, 1) %!error plot (pd) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) ## ParameterValues holds one entry per parameter, as MATLAB reports it. %!test %! pd = makedist ('PiecewiseLinear', 'x', [0, 1, 2], 'Fx', [0, 0.5, 1]); %! assert_equal (iscell (pd.ParameterValues), true); %! assert_equal (size (pd.ParameterValues), [1, 2]); %! assert_equal (pd.ParameterValues{1}, [0, 1, 2]); %! assert_equal (pd.ParameterValues{2}, [0, 0.5, 1]); %! assert_equal (numel (pd.ParameterValues), pd.NumParameters); %! assert_equal (size (pd.ParameterValues), size (pd.ParameterNames)); ## Each entry is a row whichever orientation the parameter was given in. %!test %! pd = makedist ('PiecewiseLinear', 'x', [0; 1; 2], 'Fx', [0; 0.5; 1]); %! assert_equal (size (pd.ParameterValues{1}), [1, 3]); %! assert_equal (size (pd.ParameterValues{2}), [1, 3]); %! pd.x = [0; 2; 4]; %! assert_equal (pd.ParameterValues{1}, [0, 2, 4]); %! assert_equal (pd.x, [0, 2, 4]); statistics-release-2.1.2/inst/Distribution_Classes/+prob/PoissonDistribution.m000066400000000000000000001176561527311104300276770ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef PoissonDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.PoissonDistribution ## ## Poisson probability distribution object. ## ## A @code{prob.PoissonDistribution} object consists of parameters, a model ## description, and sample data for a Poisson probability distribution. ## ## The Poisson distribution is a discrete probability distribution that ## models the number of events occurring in a fixed interval of time or space, ## given a constant average rate of occurrence. It is defined by the rate ## parameter @var{lambda}. ## ## There are several ways to create a @code{prob.PoissonDistribution} object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor @qcode{prob.PoissonDistribution (@var{lambda})} ## to create a Poisson distribution with fixed parameter value @var{lambda}. ## @item Use the static method @qcode{prob.PoissonDistribution.fit (@var{x}, ## @var{freq})} to fit a distribution to the data in @var{x} using ## the same input arguments as the @code{poissfit} function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the Poisson distribution can be found at ## @url{https://en.wikipedia.org/wiki/Poisson_distribution} ## ## @seealso{fitdist, makedist, poisscdf, poissinv, poisspdf, poissrnd, ## poissfit, poisslike, poisstat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.PoissonDistribution} {property} lambda ## ## Rate parameter ## ## A positive scalar value characterizing the rate of the ## Poisson distribution. You can access the @qcode{lambda} ## property using dot name assignment. ## ## @end deftp lambda endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.PoissonDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Poisson'; ## -*- texinfo -*- ## @deftp {prob.PoissonDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 1; ## -*- texinfo -*- ## @deftp {prob.PoissonDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{1*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'lambda'}; ## -*- texinfo -*- ## @deftp {prob.PoissonDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{1*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Rate'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = false; DistributionCode = 'poiss'; ParameterRange = [realmin; Inf]; ParameterLogCI = true; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.PoissonDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{1*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{lambda} property. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.PoissonDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. The first element contains the lower boundary, ## the second element contains the upper boundary. This property is ## read-only. You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.PoissonDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.PoissonDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{1*1} numeric matrix containing the variance of the parameter ## estimate. This matrix is only meaningful when the distribution was fitted ## to data. If the distribution object was created with fixed parameters, ## or a parameter of a fitted distribution is modified, then the ## variance is zero. This property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.PoissonDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A logical scalar specifying whether the parameter is fixed or estimated. ## A @qcode{true} value corresponds to a fixed parameter, a @qcode{false} ## value corresponds to a parameter estimate. This property is read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.PoissonDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: a numeric vector of logical values indicating ## censoring information corresponding to the elements of the data used for ## distribution fitting. If no censoring vector was used for distribution ## fitting, then this field defaults to an empty array. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.PoissonDistribution} {@var{pd} =} PoissonDistribution (@var{lambda}) ## @deftypefnx {prob.PoissonDistribution} {@var{pd} =} PoissonDistribution () ## ## Create a @code{prob.PoissonDistribution} object. ## ## @var{lambda} is the distribution parameter, which the class help ## describes. Called with no arguments the parameter takes its default, ## @var{lambda} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = PoissonDistribution (lambda) if (nargin == 0) lambda = 1; endif checkparams (lambda); this.InputData = []; this.IsTruncated = false; this.ParameterValues = lambda; this.ParameterIsFixed = true; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'Poisson distribution'); endfunction function disp (this) __disp__ (this, 'Poisson distribution'); endfunction function this = set.lambda (this, lambda) checkparams (lambda); this.InputData = []; this.ParameterValues(1) = lambda; this.ParameterIsFixed = true; this.ParameterCovariance = zeros (this.NumParameters); endfunction function lambda = get.lambda (this) lambda = this.ParameterValues(1); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.PoissonDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.PoissonDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = poisscdf (x, this.lambda); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= poisscdf (lx - 1, this.lambda); p(! (lb | ub)) /= diff (poisscdf ([lx-1, ux], this.lambda)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.PoissonDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif umax = poissinv (1, this.lambda); if (this.IsTruncated) ## Find out of range p values is_nan = p < 0 | p > 1; ## Get lower and upper boundaries lx = ceil (this.Truncation(1)); ux = floor (this.Truncation(2)); ux = min (ux, umax); lp = poisscdf (lx - 1, this.lambda); up = poisscdf (ux, this.lambda); p = lp + p * (up - lp); p(is_nan) = NaN; endif x = poissinv (p, this.lambda); if (this.IsTruncated) x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.PoissonDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.PoissonDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif [um, uv] = poisstat (this.lambda); if (this.IsTruncated) lx = ceil (this.Truncation(1)); ux = floor (this.Truncation(2)); ux = min (ux, poissinv (1, this.lambda)); ## Handle infinite support on the right if (isequal (ux, Inf)) ratio = 1 / diff (poisscdf ([lx-1, ux], this.lambda)); x = 0:lx-1; m = ratio * (um - sum (x .* poisspdf (x, this.lambda))); else x = lx:ux; m = sum (x .* pdf (this, x)); endif else m = um; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.PoissonDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = poisscdf ([lx-1, ux], this.lambda); m = poissinv (sum (Fa_b) / 2, this.lambda); else m = poissinv (0.5, this.lambda); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.PoissonDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = poisslike (this.lambda, this.InputData.data, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.PoissonDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.PoissonDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.PoissonDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = poisspdf (x, this.lambda); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (poisscdf ([lx-1, ux], this.lambda)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.PoissonDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.PoissonDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.PoissonDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. ## ## @item @qcode{'Parent'} @tab An axes graphics object for the plot. ## If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, true, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.PoissonDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.PoissonDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.PoissonDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.PoissonDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.PoissonDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.PoissonDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the Poisson distribution, @qcode{@var{pnum} = 1} selects the ## parameter @qcode{lambda}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the user-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.PoissonDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.PoissonDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.PoissonDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.PoissonDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{poissrnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) sz = [varargin{:}]; ps = prod (sz); ## Get an estimate of how many more random numbers we need to randomly ## pick the appropriate size from lx = this.Truncation(1); ux = this.Truncation(2); ratio = 1 / diff (poisscdf ([lx, ux], this.lambda)); nsize = fix (2 * ratio * ps); # times 2 to be on the safe side ## Generate the numbers and remove out-of-bound random samples r = poissrnd (this.lambda, nsize, 1); r(r < lx | r > ux) = []; ## Randomly select the required size and reshape to requested dimensions idx = randperm (numel (r), ps); r = reshape (r(idx), sz); else r = poissrnd (this.lambda, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.PoissonDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.PoissonDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, and upper limit, @var{upper}. If @var{pd} is fitted to data ## with @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); endif ## Check boundaries and constrain within support: Natural numbers lower = round (lower); upper = round (upper); lower(lower < 0) = 0; if (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = true; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.PoissonDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) ## Calculate untruncated mean and variance [um, uv] = poisstat (this.lambda); ## Calculate truncated mean m = mean (this); ## Get lower and upper boundaries lx = ceil (this.Truncation(1)); ux = floor (this.Truncation(2)); ux = min (ux, poissinv (1, this.lambda)); ## Handle infinite support on the right if (isequal (ux, Inf)) ratio = 1 / diff (poisscdf ([lx-1, ux], this.lambda)); x = 0:lx-1; v = ratio * (uv + (um - m) ^ 2 - sum (((x - m) .^ 2) .* ... poisspdf (x, this.lambda))); else x = lx:ux; v = sum (((x - m) .^ 2) .* pdf (this, x)); endif else [~, v] = poisstat (this.lambda); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) freq = []; else freq = varargin{2}; endif ## Fit data [phat, pci] = poissfit (x, alpha, freq); [~, acov] = poisslike (phat, x, freq); ## Create fitted distribution object pd = prob.PoissonDistribution.makeFitted ... (phat, pci, acov, x, freq); endfunction function pd = makeFitted (phat, pci, acov, x, freq) lambda = phat(1); pd = prob.PoissonDistribution (lambda); pd.ParameterCI = pci; pd.ParameterIsFixed = false; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', [], 'freq', freq); endfunction endmethods endclassdef function checkparams (lambda) if (! (isscalar (lambda) && isnumeric (lambda) && isreal (lambda) && isfinite (lambda) && lambda > 0)) error ("PoissonDistribution: LAMBDA must be a positive real scalar.") endif endfunction %!demo %! ## Generate a data set of 5000 random samples from a Poisson distribution with %! ## parameter lambda = 5. Fit a Poisson distribution to this data and plot %! ## a PDF of the fitted distribution superimposed on a histogram of the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ('Poisson', 'lambda', 5) %! data = random (pd_fixed, 5000, 1); %! pd_fitted = fitdist (data, 'Poisson') %! plot (pd_fitted) %! msg = 'Fitted Poisson distribution with lambda = %0.2f'; %! title (sprintf (msg, pd_fitted.lambda)) ## Test output %!shared pd, t, t_inf %! pd = prob.PoissonDistribution; %! t = truncate (pd, 2, 4); %! t_inf = truncate (pd, 2, Inf); %!assert_equal (cdf (pd, [0:5]), [0.3679, 0.7358, 0.9197, 0.9810, 0.9963, 0.9994], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0.7059, 0.9412, 1, 1], 1e-4); %!assert_equal (cdf (t_inf, [0:5]), [0, 0, 0.6961, 0.9281, 0.9861, 0.9978], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4]), [0.7358, 0.9197, 0.9810, 0.9963], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4]), [0, 0.7059, 0.9412, 1], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [0, 0, 1, 1, 2, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2, 2, 2, 3, 4], 1e-4); %!assert_equal (icdf (t_inf, [0:0.2:1]), [2, 2, 2, 2, 3, Inf], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 1, 1, 2, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2, 2, 3, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 2); %!assert_equal (iqr (t), 1); %!assert_equal (mean (pd), 1); %!assert_equal (mean (t), 2.3529, 1e-4); %!assert_equal (mean (t_inf), 2.3922, 1e-4); %!assert_equal (median (pd), 1); %!assert_equal (median (t), 2); %!assert_equal (median (t_inf), 2); %!assert_equal (pdf (pd, [0:5]), [0.3679, 0.3679, 0.1839, 0.0613, 0.0153, 0.0031], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 0.7059, 0.2353, 0.0588, 0], 1e-4); %!assert_equal (pdf (t_inf, [0:5]), [0, 0, 0.6961, 0.2320, 0.0580, 0.0116], 1e-4); %!assert_equal (pdf (pd, [-1, 1:4, NaN]), [0, 0.3679, 0.1839, 0.0613, 0.0153, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1:4, NaN]), [0, 0, 0.7059, 0.2353, 0.0588, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 1); %!assert_equal (std (t), 0.5882, 1e-4); %!assert_equal (std (t_inf), 0.6738, 1e-4); %!assert_equal (var (pd), 1); %!assert_equal (var (t), 0.3460, 1e-4); %!assert_equal (var (t_inf), 0.4540, 1e-4); %!test %! ## The profile over the first free parameter: 101 grid values, one row of %! ## OTHER per value, and the likelihood peaking at the fitted estimate. %! x = [3; 1; 4; 1; 5; 2; 6; 5; 3; 5; 2; 4; 1; 3; 2; 4; 6; 2; 3; 1]; %! pd = fitdist (x, 'Poisson'); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (size (param), [1, 101]); %! assert_equal (size (other), [101, 0]); %! assert_equal (proflik (pd), nlogL); %! [~, imax] = max (nlogL); %! assert_equal (abs (param(imax) - pd.ParameterValues(1)) <= param(2) - param(1), true); ## Test input validation ## 'prob.PoissonDistribution' constructor %!error ... %! prob.PoissonDistribution (0) %!error ... %! prob.PoissonDistribution (-1) %!error ... %! prob.PoissonDistribution (Inf) %!error ... %! prob.PoissonDistribution (i) %!error ... %! prob.PoissonDistribution ('a') %!error ... %! prob.PoissonDistribution ([1, 2]) %!error ... %! prob.PoissonDistribution (NaN) ## 'cdf' method %!error ... %! cdf (prob.PoissonDistribution, 2, 'uper') %!error ... %! cdf (prob.PoissonDistribution, 2, 3) ## 'paramci' method %!shared x %! x = poissrnd (1, [1, 100]); %!error ... %! paramci (prob.PoissonDistribution.fit (x), 'alpha') %!error ... %! paramci (prob.PoissonDistribution.fit (x), 'alpha', 0) %!error ... %! paramci (prob.PoissonDistribution.fit (x), 'alpha', 1) %!error ... %! paramci (prob.PoissonDistribution.fit (x), 'alpha', [0.5 2]) %!error ... %! paramci (prob.PoissonDistribution.fit (x), 'alpha', '') %!error ... %! paramci (prob.PoissonDistribution.fit (x), 'alpha', {0.05}) %!error ... %! paramci (prob.PoissonDistribution.fit (x), 'parameter', 'lambda', 'alpha', {0.05}) %!error ... %! paramci (prob.PoissonDistribution.fit (x), 'parameter', {'lambda', 'param'}) %!error ... %! paramci (prob.PoissonDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', {'lambda', 'param'}) %!error ... %! paramci (prob.PoissonDistribution.fit (x), 'parameter', 'param') %!error ... %! paramci (prob.PoissonDistribution.fit (x), 'alpha', 0.01, 'parameter', 'param') %!error ... %! paramci (prob.PoissonDistribution.fit (x), 'NAME', 'value') %!error ... %! paramci (prob.PoissonDistribution.fit (x), 'alpha', 0.01, 'NAME', 'value') %!error ... %! paramci (prob.PoissonDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', 'lambda', 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.PoissonDistribution, 'Parent') %!error ... %! plot (prob.PoissonDistribution, 'PlotType', 12) %!error ... %! plot (prob.PoissonDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.PoissonDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.PoissonDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.PoissonDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.PoissonDistribution, 'Discrete', {true}) %!error ... %! plot (prob.PoissonDistribution, 'Parent', 12) %!error ... %! plot (prob.PoissonDistribution, 'Parent', 'hax') %!error ... %! plot (prob.PoissonDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.PoissonDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.PoissonDistribution, 2) %!error ... %! proflik (prob.PoissonDistribution.fit (x), 3) %!error ... %! proflik (prob.PoissonDistribution.fit (x), [1, 2]) %!error ... %! proflik (prob.PoissonDistribution.fit (x), {1}) %!error ... %! proflik (prob.PoissonDistribution.fit (x), 1, ones (2)) %!error ... %! proflik (prob.PoissonDistribution.fit (x), 1, 'Display') %!error ... %! proflik (prob.PoissonDistribution.fit (x), 1, 'Display', 1) %!error ... %! proflik (prob.PoissonDistribution.fit (x), 1, 'Display', {1}) %!error ... %! proflik (prob.PoissonDistribution.fit (x), 1, 'Display', {'on'}) %!error ... %! proflik (prob.PoissonDistribution.fit (x), 1, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.PoissonDistribution.fit (x), 1, 'Display', 'onnn') %!error ... %! proflik (prob.PoissonDistribution.fit (x), 1, 'NAME', 'on') %!error ... %! proflik (prob.PoissonDistribution.fit (x), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.PoissonDistribution.fit (x), 1, {[1 2 3 4]}, 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.PoissonDistribution) %!error ... %! truncate (prob.PoissonDistribution, 2) %!error ... %! truncate (prob.PoissonDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.PoissonDistribution (1); %! pd(2) = prob.PoissonDistribution (3); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/ProbabilityDistribution.m000066400000000000000000000675601527311104300305230ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftp {statistics} prob.ProbabilityDistribution ## ## Abstract base class of the probability distribution objects. ## ## It holds the behaviour every distribution object shares -- how it is ## displayed, how its parameter confidence intervals are computed, how it is ## plotted and how a profile likelihood is taken -- as protected methods, so ## the 29 distribution classes inherit one implementation and no part of it is ## reachable from outside them. ## ## These helpers were ordinary files in a @file{private} directory until the ## classes moved into the @code{prob} namespace. Octave does not resolve a ## @file{private} directory from inside a package directory, for a classdef or ## for a plain function, so the only way to keep them out of the public ## interface is to make them protected methods of a shared base. ## ## @end deftp classdef (Abstract) ProbabilityDistribution methods (Access = protected) function __disp__ (pd, distname) if (isscalar (pd)) ## Handle special case of prob.PiecewiseLinearDistribution if (isa (pd, 'prob.PiecewiseLinearDistribution')) ## Print distribution header fprintf (" %s\n\n", pd.DistributionName); ## Print parameter values for i = 1:numel (pd.x) fprintf ("F(%g) = %g\n", pd.x(i), pd.Fx(i)); endfor ## Print truncation interval if applicable if (pd.IsTruncated) fprintf (" Truncated to the interval [%g, %g]\n\n", pd.Truncation); else fprintf ("\n"); endif ## Handle special case of prob.MultinomialDistribution elseif (isa (pd, 'prob.MultinomialDistribution')) ## Print distribution header fprintf (" %s\n\n", pd.DistributionName); ## Print parameter values fprintf (" Probabilities:\n"); fprintf (" %0.4f", pd.Probabilities); fprintf ("\n\n"); ## Print truncation interval if applicable if (pd.IsTruncated) fprintf (" Truncated to the interval [%g, %g]\n\n", pd.Truncation); endif ## Handle special case of prob.KernelDistribution elseif (isa (pd, 'prob.KernelDistribution')) ## Print distribution header fprintf (" %s\n\n", pd.DistributionName); ## Print kernel, bandwidth, and support fprintf (" Kernel = %s\n", pd.Kernel); fprintf (" Bandwidth = %g\n", pd.Bandwidth); if (ischar (pd.Support.range)) fprintf (" Support = %s\n", pd.Support.range); else fprintf (" Support = [%g, %g]\n", pd.Support.range); endif ## Print truncation interval if applicable if (pd.IsTruncated) fprintf (" Truncated to the interval [%g, %g]\n\n", pd.Truncation); else fprintf ("\n"); endif ## Handle all other cases else ## Get required length for parameter values PVlen = max (arrayfun (@(x) numel (sprintf ("%g", x)), ... pd.ParameterValues)); PVstr = sprintf ("%%%dg", PVlen); ## Prepare template for fitted and not fitted distributions pat1 = [' %+7s = ', PVstr, " [%g, %g]\n"]; pat2 = [' %+7s = ', PVstr, "\n"]; ## Grad distributions that are non fittable if (any (strcmpi (pd.DistributionCode, {'unif', 'tri', 'logu'}))) fitted = false; ParameterIsFixed = true; elseif (all (pd.ParameterIsFixed)) fitted = false; ParameterIsFixed = pd.ParameterIsFixed; else fitted = true; ParameterIsFixed = pd.ParameterIsFixed; endif ## Print distribution header fprintf (" %s\n\n", pd.DistributionName); fprintf (" %s\n", distname); ## Print parameter values for i = 1:pd.NumParameters if (fitted && ! ParameterIsFixed(i)) fprintf (pat1, pd.ParameterNames{i}, pd.ParameterValues(i), ... pd.ParameterCI(1,i), pd.ParameterCI(2,i)); else fprintf (pat2, pd.ParameterNames{i}, pd.ParameterValues(i)); endif endfor ## Print truncation interval if applicable if (pd.IsTruncated) fprintf (" Truncated to the interval [%g, %g]\n\n", pd.Truncation); else fprintf ("\n"); endif endif else fprintf ("%dx%d %s array\n", size (pd), class (pd)); endif endfunction function ci = __paramci__ (pd, varargin) ## Get Distribution specific info distname = pd.DistributionCode; parnames = pd.ParameterNames; ## Add defaults and parse optional arguments alpha = 0.05; param = logical ([1:numel(parnames)]); if (mod (numel (varargin), 2) != 0) error ("paramci: optional arguments must be in NAME-VALUE pairs."); endif while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'alpha' alpha = varargin{2}; if (! isscalar (alpha) || ! isnumeric (alpha) || ... alpha <= 0 || alpha >= 1) error ("paramci: invalid VALUE for 'Alpha' argument."); endif case 'parameter' if (! isvector (varargin{2}) || ((iscellstr (varargin{2}) || isnumeric (varargin{2})) && numel (varargin{2}) > numel (parnames))) error ("paramci: invalid VALUE size for 'Parameter' argument."); endif if (iscellstr (varargin{2})) tmp = cellfun (@(x) strcmpi (x, parnames), varargin{2}, ... 'UniformOutput', false); param = or (tmp{:}); elseif (isnumeric (varargin{2})) param = ismember (parnames, varargin{2}); else # assume it is a character vector param = strcmpi (varargin{2}, parnames); endif if (! any (param)) error ("paramci: unknown distribution parameter."); endif case {'type', 'logflag'} printf ("paramci: '%s' argument not supported yet.", varargin{1}); otherwise error ("paramci: invalid NAME for optional argument."); endswitch varargin([1:2]) = []; endwhile ## Get confidence intervals for all parameters from selected distribution if (strcmpi (distname, 'bino')) ntrials = pd.N; [~, ci] = mle (pd.InputData.data, 'distribution', distname, ... 'alpha', alpha, 'ntrials', ntrials, ... 'frequency', pd.InputData.freq); elseif (strcmpi (distname, 'gp')) theta = pd.theta; [~, ci] = mle (pd.InputData.data, 'distribution', distname, ... 'alpha', alpha, 'theta', theta, ... 'frequency', pd.InputData.freq); elseif (strcmpi (distname, 'hn')) mu = pd.mu; [~, ci] = mle (pd.InputData.data, 'distribution', distname, ... 'alpha', alpha, 'mu', mu, 'frequency', pd.InputData.freq); elseif (! pd.CensoringAllowed) [~, ci] = mle (pd.InputData.data, 'distribution', distname, ... 'alpha', alpha, 'frequency', pd.InputData.freq); else [~, ci] = mle (pd.InputData.data, 'distribution', distname, ... 'alpha', alpha, 'censoring', pd.InputData.cens, ... 'frequency', pd.InputData.freq); endif ## MLE reports intervals only for the estimated parameters of some ## distributions. Report one column per parameter, as MATLAB does, holding a ## fixed parameter at its own value. if (columns (ci) != numel (pd.ParameterValues)) fullci = [pd.ParameterValues; pd.ParameterValues]; fullci(:, ! pd.ParameterIsFixed) = ci; ci = fullci; endif ## Return ci only for requested parameters ci(:, ! param) = []; endfunction function h = __plot__ (pd, DistType, varargin) ## Add defaults (Discrete is passed by the calling method) ax = []; PlotType = 'pdf'; Discrete = DistType; ## Parse optional arguments if (mod (numel (varargin), 2) != 0) error ("plot: optional arguments must be in NAME-VALUE pairs."); endif while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'plottype' ValidTypes = {'pdf', 'cdf', 'probability'}; try selected_T = strcmpi (varargin{2}, ValidTypes); catch error ("plot: invalid VALUE size for 'Parameter' argument."); end_try_catch if (! any (selected_T) || sum (selected_T) > 1) error ("plot: invalid VALUE for 'PlotType' argument."); endif PlotType = ValidTypes{selected_T}; case 'discrete' if (! (islogical (varargin{2}) && isscalar (varargin{2}))) error ("plot: invalid VALUE for 'Discrete' argument."); endif ## Only for discrete distributions this can be changed by the user if (DistType) Discrete = varargin{2}; endif case 'parent' if (! isaxes (varargin{2})) error ("plot: invalid VALUE for 'Parent' argument."); endif ax = varargin{2}; otherwise error ("plot: invalid NAME for optional argument."); endswitch varargin([1:2]) = []; endwhile ## Check for invalid cases of probability type before creating new axes if (strcmpi (PlotType, 'probability')) if (! isprop (pd, 'InputData')) msg = 'plot: ''probability'' PlotType is not supported for ''%s''.'; error (sprintf (msg, pd.DistributionName)); endif if (isempty (pd.InputData)) error ("plot: no fitted DATA to plot a probability plot."); endif endif ## Get current axes or create new ones if (isempty (ax)) ax = gca (); endif ## Switch to PlotType switch (PlotType) case 'pdf' h = plot_pdf (pd, ax, DistType, Discrete); case 'cdf' h = plot_cdf (pd, ax, DistType, Discrete); case 'probability' h = plot_prob (pd, ax, DistType, Discrete); endswitch endfunction function [nlogl, param, other] = __proflik__ (pd, pnum, varargin) ## Default to the first non-fixed parameter npvec = find (pd.ParameterIsFixed == false); if (nargin < 2 || isempty (pnum)) pnum = npvec(1); endif ## Check for non-fixed pnum if (! (isnumeric (pnum) && isscalar (pnum) && ismember (pnum, npvec))) error (strcat ("proflik: PNUM must be a scalar number", ... " indexing a non-fixed parameter.")); endif ## Add defaults and parse optional arguments param = []; Display = false; while (numel (varargin) > 0) if (isnumeric (varargin{1})) if (! isvector (varargin{1})) error ("proflik: SETPARAM must be a numeric vector."); endif param = varargin{1}; varargin(1) = []; elseif (ischar (varargin{1})) if (strcmpi (varargin{1}, 'display')) if (numel (varargin) < 2) error ("proflik: missing VALUE for 'Display' argument."); endif if (! ischar (varargin{2})) error ("proflik: invalid VALUE type for 'Display' argument."); endif if (size (varargin{2}, 1) != 1) error ("proflik: invalid VALUE size for 'Display' argument."); endif if (strcmpi (varargin{2}, 'off')) Display = false; elseif (strcmpi (varargin{2}, 'on')) Display = true; else error ("proflik: invalid VALUE for 'Display' argument."); endif varargin([1:2]) = []; else error ("proflik: invalid NAME for optional arguments."); endif else error ("proflik: invalid optional argument."); endif endwhile ## Optimal parameter values and the free parameters to profile out (the ## non-fixed parameters other than the selected one) optpar = pd.ParameterValues; fname = sprintf ("%slike", pd.DistributionCode); freeidx = npvec(npvec != pnum); ## Create parameter vector pname = pd.ParameterNames{pnum}; if (isempty (param)) ## Default range: equally spaced values over the 98% confidence interval, ## restricted to the non-fixed range. MATLAB takes 101 values when the ## selected parameter is the only one estimated and 21 when the others must ## be profiled out at each of them. ci = paramci (pd, "Alpha", 0.02); if (any (isnan (ci(:, pnum)))) error (strcat ("proflik: no confidence interval is defined for '%s',", ... " so the default range cannot be built; supply", ... " SETPARAM instead."), pd.ParameterNames{pnum}); endif lower = max (ci(1, pnum), pd.ParameterRange(1, pnum)); upper = min (ci(2, pnum), pd.ParameterRange(2, pnum)); if (isempty (freeidx)) param = linspace (lower, upper, 101); else param = linspace (lower, upper, 21); endif else ## Restrict user defined parameter range within non-fixed range param(param < pd.ParameterRange(1,pnum)) = []; param(param > pd.ParameterRange(2,pnum)) = []; endif ## Compute the profile log likelihood: at each value of the selected ## parameter, maximize the log likelihood over the remaining free parameters params = pd.ParameterValues; opts = optimset ("Display", "off", "TolX", 1e-6, "TolFun", 1e-6); nlogl = zeros (1, numel (param)); ## Each row of OTHER holds every parameter but the selected one, at the ## values maximizing the likelihood; a fixed parameter keeps its own value otheridx = [1:numel(params)]; otheridx(pnum) = []; other = zeros (numel (param), numel (otheridx)); for i = 1:numel (param) p0 = params; p0(pnum) = param(i); if (isempty (freeidx)) nlogl(i) = - like_value (fname, p0, pd); else objfun = @(pf) like_free (pf, p0, freeidx, fname, pd); [pfhat, fval] = fminsearch (objfun, params(freeidx), opts); nlogl(i) = - fval; p0(freeidx) = pfhat; endif other(i,:) = p0(otheridx); endfor optnll = - like_value (fname, optpar, pd); ## Plot the profile log likelihood against the selected parameter, marking ## the estimate and the 95% profile-likelihood confidence threshold if (Display) nll_conf = optnll - 0.5 * chi2inv (0.95, 1); plot (optpar(pnum), optnll, 'ok;Estimate;', ... param, nlogl, '-r;Profile log likelihood;', ... param, repmat (nll_conf, size (param)), ':b;95% confidence;'); xlabel (pname); ylabel ('log likelihood'); xlim ([param(1), param(end)]); endif endfunction endmethods endclassdef function x = expand_freq (data, freq) x = []; for i = 1:numel (freq) x = [x, repmat(data(i), 1, freq(i))]; endfor endfunction function [lb, ub, xmin, xmax] = compute_boundaries (pd) ## Compute moments to determine plot boundaries m = mean (pd); s = std (pd); lb = m - 3 * s; ub = m + 3 * s; xmin = m - 3.5 * s; xmax = m + 3.5 * s; ## Fix boundaries for specific distributions PD = {'bino', 'bisa', 'exp', 'gam', 'invg', 'logl', 'logn', ... 'naka', 'nbin', 'poiss', 'rayl', 'rice', 'wbl'}; if (strcmpi (pd.DistributionCode, 'beta')) lb = xmin = 0; ub = xmax = 1; elseif (strcmpi (pd.DistributionCode, 'burr')) lb = xmin = 0; ub = xmax = m + 3 * iqr (pd); elseif (any (strcmpi (pd.DistributionCode, PD))) lb = max (m - 3 * s, 0); xmin = max (m - 3.5 * s, 0); elseif (strcmpi (pd.DistributionCode, 'gev')) elseif (strcmpi (pd.DistributionCode, 'gp')) elseif (strcmpi (pd.DistributionCode, 'hn')) lb = max (m - 3 * s, m); xmin = max (m - 3.5 * s, m); elseif (strcmpi (pd.DistributionCode, 'kernel')) ## Clamp the plotting range to a bounded kernel support if (ischar (pd.Support.range)) if (strcmp (pd.Support.range, 'positive')) lb = xmin = max (m - 3 * s, 0); endif else lb = xmin = max (m - 3 * s, pd.Support.range(1)); ub = xmax = min (m + 3 * s, pd.Support.range(2)); endif endif endfunction function h = plot_pdf (pd, ax, DistType, Discrete) ## Handle special case of multinomial distribution if (strcmpi (pd.DistributionCode, 'mn')) y = pd.ParameterValues{1}(:); x = [1:numel(y)]'; if (Discrete) h = stem (ax, x, y, 'color', 'b'); else h = plot (ax, x, y, ';;b-'); endif xlim (ax, [0.5, max(x)+0.5]); xlabel ('Data'); ylabel ('Probability'); return endif ## Handle special case of piecewise linear distribution if (strcmpi (pd.DistributionCode, 'pl')) x = pd.ParameterValues{1}(:); y = pd.ParameterValues{2}(:); h = plot (ax, x, y, ';;b-'); xgap = (x(end) - x(1)) * 0.1; xlim (ax, [x(1)-xgap, x(end)+xgap]); xlabel ('Data'); ylabel ('Probability'); return endif ## Handle special case of triangular distribution if (strcmpi (pd.DistributionCode, 'tri')) lb = pd.A; ub = pd.C; xmin = lb - (ub - lb) * 0.1; xmax = ub + (ub - lb) * 0.1; x = [lb:(ub-lb)/100:ub]'; y = pdf (pd, x); h = plot (ax, x, y, ';;r-', 'linewidth', 2); xlim (ax, [xmin, xmax]); xlabel ('Data'); ylabel ('PDF'); return endif ## Handle special case of log-uniform and uniform distributions if (any (strcmpi (pd.DistributionCode, {'logu', 'unif'}))) lb = pd.Lower; ub = pd.Upper; xmin = lb - (ub - lb) * 0.1; xmax = ub + (ub - lb) * 0.1; x = [lb:(ub-lb)/100:ub]'; y = pdf (pd, x); h = plot (ax, x, y, ';;r-', 'linewidth', 2); xlim (ax, [xmin, xmax]); xlabel ('Data'); ylabel ('PDF'); return endif ## Check for fitted distribution if (isempty (pd.InputData)) # fixed parameters, no data ## Compute plot boundaries [lb, ub, xmin, xmax] = compute_boundaries (pd); ## Compute stem or line for PDF if (DistType) x = [floor(lb):ceil(ub)]'; y = pdf (pd, x); else x = [lb:(ub-lb)/100:ub]'; y = pdf (pd, x); endif ## Plot if (Discrete) xlim (ax, [min(x)-0.5, max(x)+0.5]); h = stem (ax, x, y, 'color', 'r'); #xlim (ax, [min(x)-0.5, max(x)+0.5]); # before Octave 11 this emits an error xlabel ('Data'); ylabel ('Probability'); else h = plot (ax, x, y, ';;r-', 'linewidth', 2); xlim (ax, [xmin, xmax]); xlabel ('Data'); ylabel ('PDF'); endif else # fitted distribution, data available ## Expand frequency vector (if necessary) if (any (pd.InputData.freq != 1)) x = expand_freq (pd.InputData.data, pd.InputData.freq); else x = pd.InputData.data; endif ## Keep data within plotting boundaries [lb, ub, xmin, xmax] = compute_boundaries (pd); x(x < lb | x > ub) = []; ## Compute the patch or histogram for data xsize = numel (x); if (DistType) binwidth = 1; xmin = min (x) - 1; xmax = max (x) + 1; [binsize, bincenter] = hist (x, [xmin:xmax]); else nbins = ceil (sqrt (xsize)); [binsize, bincenter] = hist (x, nbins); binwidth = max (diff (bincenter)); xmin = min (x) - binwidth / 2; xmax = max (x) + binwidth / 2; endif ## Compute stem or line for PDF if (Discrete) x = [min(x):max(x)]'; y = pdf (pd, x); else x = [xmin:(xmax-xmin)/100:xmax]'; y = pdf (pd, x); endif ## Normalize density line y = xsize * y * binwidth; ## Plot if (DistType) h(2) = patch (ax, bincenter, binsize, 1, 'facecolor', 'b'); xlim (ax, [xmin, xmax]); hold on; if (Discrete) h(1) = stem (ax, x, y, 'color', 'r'); else h(1) = plot (ax, x, y, ';;r-'); endif xlabel ('Data'); ylabel ('Probability'); hold off; else h(2) = bar (ax, bincenter, binsize, 1, 'facecolor', 'b'); hold on; h(1) = plot (ax, x, y, ';;r-', 'linewidth', 2); xlim (ax, [xmin, xmax]); xlabel ('Data'); ylabel ('PDF'); hold off; endif endif endfunction function h = plot_cdf (pd, ax, DistType, Discrete) ## Handle special case of multinomial distribution if (strcmpi (pd.DistributionCode, 'mn')) y = pd.ParameterValues{1}(:); x = [1:numel(y)]'; xlim (ax, [0.5, max(x)+0.5]); if (Discrete) h = stem (ax, x, y, 'color', 'b'); else h = plot (ax, x, y, ';;b-'); endif xlabel ('Data'); ylabel ('Probability'); return endif ## Handle special case of piecewise linear distribution if (strcmpi (pd.DistributionCode, 'pl')) x = pd.ParameterValues{1}(:); y = pd.ParameterValues{2}(:); h = plot (ax, x, y, ';;b-'); xgap = (x(end) - x(1)) * 0.1; xlim (ax, [x(1)-xgap, x(end)+xgap]); xlabel ('Data'); ylabel ('Probability'); return endif ## Handle special case of triangular distribution if (strcmpi (pd.DistributionCode, 'tri')) lb = pd.A; ub = pd.C; xmin = lb - (ub - lb) * 0.1; xmax = ub + (ub - lb) * 0.1; x = [lb:(ub-lb)/100:ub]'; y = pdf (pd, x); h = plot (ax, x, y, ';;r-', 'linewidth', 2); xlim (ax, [xmin, xmax]); xlabel ('Data'); ylabel ('PDF'); return endif ## Handle special case of log-uniform and uniform distributions if (any (strcmpi (pd.DistributionCode, {'logu', 'unif'}))) lb = pd.Lower; ub = pd.Upper; xmin = lb - (ub - lb) * 0.1; xmax = ub + (ub - lb) * 0.1; x = [lb:(ub-lb)/100:ub]'; y = pdf (pd, x); h = plot (ax, x, y, ';;r-', 'linewidth', 2); xlim (ax, [xmin, xmax]); xlabel ('Data'); ylabel ('PDF'); return endif ## Compute plot boundaries [lb, ub, xmin, xmax] = compute_boundaries (pd); ## Check for fitted distribution if (isempty (pd.InputData)) # fixed parameters, no data ## Compute stem or line for PDF if (DistType) x = [floor(lb):ceil(ub)]'; p = cdf (pd, x); else x = [lb:(ub-lb)/100:ub]'; p = cdf (pd, x); endif ## Plot if (Discrete) h = stairs (ax, x, p, 'color', 'r'); xlim (ax, [lb-0.5, ub+0.5]); ylim (ax, [0, 1]); xlabel ('Data'); ylabel ('CDF'); else h = plot (ax, x, p, ';;r-', 'linewidth', 2); xlim (ax, [xmin, xmax]); ylim (ax, [0, 1]); xlabel ('Data'); ylabel ('CDF'); endif else # fitted distribution, data available ## Expand frequency vector (if necessary) if (any (pd.InputData.freq != 1)) x = expand_freq (pd.InputData.data, pd.InputData.freq); else x = pd.InputData.data; endif ## Compute the stairs for data [yy, xx, ~, ~, eid] = cdfcalc (x); n = length (xx); ## Create vectors for plotting nidx = reshape (repmat (1:n, 2, 1), 2*n, 1); xCDF = [-Inf; xx(nidx); Inf]; yCDF = [0; 0; yy(1+nidx)]; ## Compute stairs or line for CDF if (DistType) x = [min(x):max(x)]'; p = cdf (pd, x); else x = [xmin:(xmax-xmin)/100:xmax]'; p = cdf (pd, x); endif ## Plot if (DistType) h(2) = plot (ax, xCDF, yCDF, ';;b-'); xlim (ax, [xmin, xmax]); ylim (ax, [0, 1]); hold on; if (Discrete) h(1) = stem (ax, x, p, 'color', 'r'); else h(1) = plot (ax, x, p, ';;r-'); endif xlabel ('Data'); ylabel ('CDF'); hold off; else h(2) = plot (ax, xCDF, yCDF, ';;b-'); xlim (ax, [xmin, xmax]); ylim (ax, [0, 1]); hold on; h(1) = plot (ax, x, p, ';;r-', 'linewidth', 2); xlabel ('Data'); ylabel ('CDF'); hold off; endif endif endfunction function h = plot_prob (pd, ax, DistType, Discrete) ## Expand frequency vector (if necessary) if (any (pd.InputData.freq != 1)) x = expand_freq (pd.InputData.data, pd.InputData.freq); else x = pd.InputData.data; endif ## Compute the probabilities for data n = rows (x); y = icdf (pd, ([1:n]' - 0.5) / n); x = sort (x); ## Plot reference line X = Y = [x(1); x(end)]; h(2) = line (ax, X, Y, 'LineStyle', '-.', 'Marker', 'none', 'color', 'red'); hold on; h(1) = plot (ax, x, y, 'LineStyle', 'none', 'Marker', '+', 'color', 'blue'); ## Plot labels ylabel 'Probability' xlabel 'Data' ## Plot grid p = [0.001, 0.005, 0.01, 0.02, 0.05, 0.10, 0.25, 0.5, ... 0.75, 0.90, 0.95, 0.98, 0.99, 0.995, 0.999]; label = {'0.001', '0.005', '0.01', '0.02', '0.05', '0.10', '0.25', '0.50', ... '0.75', '0.90', '0.95', '0.98', '0.99', '0.995', '0.999'}; tick = icdf (pd, p); set (ax, 'ytick', tick, 'yticklabel', label); ## Compute plot boundaries [~, ~, xmin, xmax] = compute_boundaries (pd); ## Set view range with a bit of space around data ymin = icdf (pd, 0.25 ./ n); ymax = icdf (pd, (n - 0.25) ./ n); set (ax, 'ylim', [ymin, ymax], 'xlim', [xmin, xmax]); grid (ax, 'on'); box (ax, 'off'); hold off; endfunction ## Evaluate the family negative log likelihood at a full parameter vector. ## The family function takes (params, x, censor, freq) or (params, x, freq); ## dispatch on what it declares rather than on the distribution's own ## CensoringAllowed, since Burr does not allow censoring yet still takes the ## four-argument form, and passing the frequencies positionally into the ## censoring slot marks every observation censored. function nll = like_value (fname, params, pd) if (strcmp (fname, 'gplike')) ## GPLIKE takes the two estimated parameters and assumes a zero location, ## as MATLAB's does, so the fixed THETA is shifted out of the data instead ## of being passed with them. nll = feval (fname, params(1:2), pd.InputData.data - params(3), ... pd.InputData.freq); elseif (nargin (fname) > 3) nll = feval (fname, params, pd.InputData.data, ... pd.InputData.cens, pd.InputData.freq); else nll = feval (fname, params, pd.InputData.data, pd.InputData.freq); endif endfunction ## Negative log likelihood as a function of the free parameters only. The ## search is unconstrained, so parameters outside their own range are rejected ## here: the family likelihood may well return a finite value there, and for ## some families it grows without bound as they run away. function nll = like_free (pf, p0, freeidx, fname, pd) if (any (pf(:)' < pd.ParameterRange(1,freeidx)) || any (pf(:)' > pd.ParameterRange(2,freeidx))) nll = Inf; return; endif p = p0; p(freeidx) = pf; nll = like_value (fname, p, pd); if (! isreal (nll) || ! isfinite (nll)) nll = Inf; endif endfunction %!test ## A no-op test to avoid reporting this abstact class in the pkg test summary ## as a file with no tests, since an abstract class is by definition uncallable ## and thus untestable except through its consumers. statistics-release-2.1.2/inst/Distribution_Classes/+prob/RayleighDistribution.m000066400000000000000000001151661527311104300300030ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef RayleighDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.RayleighDistribution ## ## Rayleigh probability distribution object. ## ## A @code{prob.RayleighDistribution} object consists of parameters, a model ## description, and sample data for a Rayleigh probability distribution. ## ## The Rayleigh distribution is a continuous probability distribution for ## nonnegative random variables. It is often used to model the magnitude of ## a vector in two dimensions where the components are normally distributed ## with zero mean and equal variance. It is defined by scale parameter ## @var{B}. ## ## @var{B} is the @math{sigma} of the usual mathematical notation. The ## @code{rayl*} functions name the same quantity @var{sigma}; this class ## follows MATLAB. ## ## There are several ways to create a @code{prob.RayleighDistribution} object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor @qcode{prob.RayleighDistribution (@var{B})} ## to create a Rayleigh distribution with fixed parameter value @var{B}. ## @item Use the static method @qcode{prob.RayleighDistribution.fit (@var{x}, ## @var{censor}, @var{freq})} to fit a distribution to the data in @var{x} ## using the same input arguments as the @code{raylfit} function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the Rayleigh distribution can be found at ## @url{https://en.wikipedia.org/wiki/Rayleigh_distribution} ## ## @seealso{fitdist, makedist, raylcdf, raylinv, raylpdf, raylrnd, raylfit, ## rayllike, raylstat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.RayleighDistribution} {property} B ## ## Scale parameter ## ## A positive scalar value characterizing the scale of the ## Rayleigh distribution. You can access the @qcode{B} ## property using dot name assignment. ## ## @end deftp B endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.RayleighDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Rayleigh'; ## -*- texinfo -*- ## @deftp {prob.RayleighDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 1; ## -*- texinfo -*- ## @deftp {prob.RayleighDistribution} {property} ParameterNames ## ## Names of parameters ## ## A cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'B'}; ## -*- texinfo -*- ## @deftp {prob.RayleighDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Scale'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = true; DistributionCode = 'rayl'; ParameterRange = [realmin; Inf]; ParameterLogCI = true; endproperties properties(GetAccess = public , SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.RayleighDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{B} ## property. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.RayleighDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.RayleighDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.RayleighDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter, and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only ## meaningful when the distribution was fitted to data. If the distribution ## object was created with fixed parameters, or a parameter of a fitted ## distribution is modified, then all elements of the variance-covariance ## are zero. This property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.RayleighDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.RayleighDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: a numeric vector of logical values indicating ## censoring information corresponding to the elements of the data used for ## distribution fitting. If no censoring vector was used for distribution ## fitting, then this field defaults to an empty array. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.RayleighDistribution} {@var{pd} =} RayleighDistribution (@var{B}) ## @deftypefnx {prob.RayleighDistribution} {@var{pd} =} RayleighDistribution () ## ## Create a @code{prob.RayleighDistribution} object. ## ## @var{B} is the distribution parameter, which the class help describes. ## Called with no arguments the parameter takes its default, @var{B} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = RayleighDistribution (B) if (nargin == 0) B = 1; endif checkparams (B); this.InputData = []; this.IsTruncated = false; this.ParameterValues = B; this.ParameterIsFixed = true; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'Rayleigh distribution'); endfunction function disp (this) __disp__ (this, 'Rayleigh distribution'); endfunction function this = set.B (this, B) checkparams (B); this.InputData = []; this.ParameterValues(1) = B; this.ParameterIsFixed = true; this.ParameterCovariance = zeros (this.NumParameters); endfunction function B = get.B (this) B = this.ParameterValues(1); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.RayleighDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.RayleighDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = raylcdf (x, this.B); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= raylcdf (lx, this.B); p(! (lb | ub)) /= diff (raylcdf ([lx, ux], this.B)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.RayleighDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = raylcdf (this.Truncation(1), this.B); up = raylcdf (this.Truncation(2), this.B); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = raylinv (np, this.B); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = raylinv (p, this.B); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.RayleighDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.RayleighDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = raylstat (this.B); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.RayleighDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = raylcdf ([lx, ux], this.B); m = raylinv (sum (Fa_b) / 2, this.B); else m = this.B .* sqrt (2 * log (2)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.RayleighDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = rayllike (this.B, this.InputData.data, ... this.InputData.cens, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.RayleighDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.RayleighDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.RayleighDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = raylpdf (x, this.B); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (raylcdf ([lx, ux], this.B)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.RayleighDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.RayleighDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.RayleighDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.RayleighDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.RayleighDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.RayleighDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.RayleighDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.RayleighDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.RayleighDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the Rayleigh distribution, @qcode{@var{pnum} = 1} selects ## the parameter @qcode{B}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.RayleighDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.RayleighDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.RayleighDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.RayleighDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{betarnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = raylcdf (this.Truncation(1), this.B); up = raylcdf (this.Truncation(2), this.B); u = unifrnd (lp, up, varargin{:}); r = sqrt (-2 .* log (1 - u) .* this.B .^ 2); else r = raylrnd (this.B, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.RayleighDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.RayleighDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, and upper limit, @var{upper}. If @var{pd} is fitted to data ## with @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif ## Check boundaries and constrain within support [0, Inf) lower(lower < 0) = 0; this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = true; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.RayleighDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = raylstat (this.B); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) censor = []; else censor = varargin{2}; endif if (nargin < 4) freq = []; else freq = varargin{3}; endif ## Fit data [phat, pci] = raylfit (x, alpha, censor, freq); [~, acov] = rayllike (phat, x, censor, freq); ## Create fitted distribution object pd = prob.RayleighDistribution.makeFitted ... (phat, pci, acov, x, censor, freq); endfunction function pd = makeFitted (phat, pci, acov, x, censor, freq) B = phat(1); pd = prob.RayleighDistribution (B); pd.ParameterCI = pci; pd.ParameterIsFixed = false; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', censor, 'freq', freq); endfunction endmethods endclassdef function checkparams (B) if (! (isscalar (B) && isnumeric (B) && isreal (B) && isfinite (B) && B > 0)) error ("RayleighDistribution: SIGMA must be a positive real scalar.") endif endfunction %!demo %! ## Generate a data set of 5000 random samples from a Rayleigh distribution with %! ## parameter B = 2. Fit a Rayleigh distribution to this data and plot %! ## a PDF of the fitted distribution superimposed on a histogram of the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ('Rayleigh', 'B', 2) %! data = random (pd_fixed, 5000, 1); %! pd_fitted = fitdist (data, 'Rayleigh') %! plot (pd_fitted) %! msg = 'Fitted Rayleigh distribution with B = %0.2f'; %! title (sprintf (msg, pd_fitted.B)) ## Test output %!shared pd, t %! pd = prob.RayleighDistribution; %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0:5]), [0, 0.3935, 0.8647, 0.9889, 0.9997, 1], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0, 0.9202, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4, NaN]), [0.6753, 0.8647, 0.9889, 0.9997, NaN], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4, NaN]), [0, 0, 0.9202, 1, NaN], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [0, 0.6680, 1.0108, 1.3537, 1.7941, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.1083, 2.2402, 2.4135, 2.6831, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 1.0108, 1.3537, 1.7941, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.2402, 2.4135, 2.6831, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 0.9066, 1e-4); %!assert_equal (iqr (t), 0.4609, 1e-4); %!assert_equal (mean (pd), 1.2533, 1e-4); %!assert_equal (mean (t), 2.4169, 1e-4); %!assert_equal (median (pd), 1.1774, 1e-4); %!assert_equal (median (t), 2.3198, 1e-4); %!assert_equal (pdf (pd, [0:5]), [0, 0.6065, 0.2707, 0.0333, 0.0013, 0], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 2.0050, 0.2469, 0.0099, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1.5, NaN]), [0, 0.4870, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1.5, NaN]), [0, 0, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 0.6551, 1e-4); %!assert_equal (std (t), 0.3591, 1e-4); %!assert_equal (var (pd), 0.4292, 1e-4); %!assert_equal (var (t), 0.1290, 1e-4); %!test %! ## The profile over the first free parameter: 101 grid values, one row of %! ## OTHER per value, and the likelihood peaking at the fitted estimate. %! x = [1.2; 0.4; 3.1; 0.7; 2.5; 1.8; 0.3; 4.2; 1.1; 0.9; ... %! 2.2; 0.6; 1.5; 3.7; 0.8; 2.9; 1.3; 0.5; 2.0; 1.6]; %! pd = fitdist (x, 'Rayleigh'); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (size (param), [1, 101]); %! assert_equal (size (other), [101, 0]); %! assert_equal (proflik (pd), nlogL); %! [~, imax] = max (nlogL); %! assert_equal (abs (param(imax) - pd.ParameterValues(1)) <= param(2) - param(1), true); ## Test input validation ## 'prob.RayleighDistribution' constructor %!error ... %! prob.RayleighDistribution (0) %!error ... %! prob.RayleighDistribution (-1) %!error ... %! prob.RayleighDistribution (Inf) %!error ... %! prob.RayleighDistribution (i) %!error ... %! prob.RayleighDistribution ('a') %!error ... %! prob.RayleighDistribution ([1, 2]) %!error ... %! prob.RayleighDistribution (NaN) ## 'cdf' method %!error ... %! cdf (prob.RayleighDistribution, 2, 'uper') %!error ... %! cdf (prob.RayleighDistribution, 2, 3) ## 'paramci' method %!shared x %! x = raylrnd (1, [1, 100]); %!error ... %! paramci (prob.RayleighDistribution.fit (x), 'alpha') %!error ... %! paramci (prob.RayleighDistribution.fit (x), 'alpha', 0) %!error ... %! paramci (prob.RayleighDistribution.fit (x), 'alpha', 1) %!error ... %! paramci (prob.RayleighDistribution.fit (x), 'alpha', [0.5 2]) %!error ... %! paramci (prob.RayleighDistribution.fit (x), 'alpha', '') %!error ... %! paramci (prob.RayleighDistribution.fit (x), 'alpha', {0.05}) %!error ... %! paramci (prob.RayleighDistribution.fit (x), 'parameter', 'B', 'alpha', {0.05}) %!error ... %! paramci (prob.RayleighDistribution.fit (x), 'parameter', {'B', 'param'}) %!error ... %! paramci (prob.RayleighDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', {'B', 'param'}) %!error ... %! paramci (prob.RayleighDistribution.fit (x), 'parameter', 'param') %!error ... %! paramci (prob.RayleighDistribution.fit (x), 'alpha', 0.01, 'parameter', 'param') %!error ... %! paramci (prob.RayleighDistribution.fit (x), 'NAME', 'value') %!error ... %! paramci (prob.RayleighDistribution.fit (x), 'alpha', 0.01, 'NAME', 'value') %!error ... %! paramci (prob.RayleighDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', 'B', 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.RayleighDistribution, 'Parent') %!error ... %! plot (prob.RayleighDistribution, 'PlotType', 12) %!error ... %! plot (prob.RayleighDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.RayleighDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.RayleighDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.RayleighDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.RayleighDistribution, 'Discrete', {true}) %!error ... %! plot (prob.RayleighDistribution, 'Parent', 12) %!error ... %! plot (prob.RayleighDistribution, 'Parent', 'hax') %!error ... %! plot (prob.RayleighDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.RayleighDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.RayleighDistribution, 2) %!error ... %! proflik (prob.RayleighDistribution.fit (x), 3) %!error ... %! proflik (prob.RayleighDistribution.fit (x), [1, 2]) %!error ... %! proflik (prob.RayleighDistribution.fit (x), {1}) %!error ... %! proflik (prob.RayleighDistribution.fit (x), 1, ones (2)) %!error ... %! proflik (prob.RayleighDistribution.fit (x), 1, 'Display') %!error ... %! proflik (prob.RayleighDistribution.fit (x), 1, 'Display', 1) %!error ... %! proflik (prob.RayleighDistribution.fit (x), 1, 'Display', {1}) %!error ... %! proflik (prob.RayleighDistribution.fit (x), 1, 'Display', {'on'}) %!error ... %! proflik (prob.RayleighDistribution.fit (x), 1, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.RayleighDistribution.fit (x), 1, 'Display', 'onnn') %!error ... %! proflik (prob.RayleighDistribution.fit (x), 1, 'NAME', 'on') %!error ... %! proflik (prob.RayleighDistribution.fit (x), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.RayleighDistribution.fit (x), 1, {[1 2 3 4]}, 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.RayleighDistribution) %!error ... %! truncate (prob.RayleighDistribution, 2) %!error ... %! truncate (prob.RayleighDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.RayleighDistribution (1); %! pd(2) = prob.RayleighDistribution (3); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/RicianDistribution.m000066400000000000000000001223741527311104300274430ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef RicianDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.RicianDistribution ## ## Rician probability distribution object. ## ## A @code{prob.RicianDistribution} object consists of parameters, a model ## description, and sample data for a Rician probability distribution. ## ## The Rician distribution is a continuous probability distribution that ## models the magnitude of a signal in the presence of Gaussian noise. It is ## defined by noncentrality parameter @var{s} and scale parameter @var{sigma}. ## ## There are several ways to create a @code{prob.RicianDistribution} object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor ## @qcode{prob.RicianDistribution (@var{s}, @var{sigma})} to create a Rician ## distribution with fixed parameter values @var{s} and @var{sigma}. ## @item Use the static method @qcode{prob.RicianDistribution.fit (@var{x}, ## @var{censor}, @var{freq}, @var{options})} to fit a distribution to data ## @var{x}. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the Rician distribution can be found at ## @url{https://en.wikipedia.org/wiki/Rice_distribution} ## ## @seealso{fitdist, makedist, ricecdf, riceinv, ricepdf, ricernd, ricefit, ## ricelike, ricestat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.RicianDistribution} {property} s ## ## Noncentrality parameter ## ## A non-negative scalar value characterizing the noncentrality of the ## Rician distribution. You can access the @qcode{s} property using dot ## name assignment. ## ## @end deftp s ## -*- texinfo -*- ## @deftp {prob.RicianDistribution} {property} sigma ## ## Scale parameter ## ## A positive scalar value characterizing the scale of the Rician ## distribution. You can access the @qcode{sigma} property using dot name ## assignment. ## ## @end deftp sigma endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.RicianDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Rician'; ## -*- texinfo -*- ## @deftp {prob.RicianDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 2; ## -*- texinfo -*- ## @deftp {prob.RicianDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'s', 'sigma'}; ## -*- texinfo -*- ## @deftp {prob.RicianDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Noncentrality', 'Scale'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = true; DistributionCode = 'rice'; ParameterRange = [0, realmin; Inf, Inf]; ParameterLogCI = [true, true]; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.RicianDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{2*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{s} and @qcode{sigma} ## properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.RicianDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.RicianDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.RicianDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{2*2} numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter, and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only ## meaningful when the distribution was fitted to data. If the distribution ## object was created with fixed parameters, or a parameter of a fitted ## distribution is modified, then all elements of the variance-covariance ## are zero. This property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.RicianDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*2} logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.RicianDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: a numeric vector of logical values indicating ## censoring information corresponding to the elements of the data used for ## distribution fitting. If no censoring vector was used for distribution ## fitting, then this field defaults to an empty array. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.RicianDistribution} {@var{pd} =} RicianDistribution (@var{s}, @var{sigma}) ## @deftypefnx {prob.RicianDistribution} {@var{pd} =} RicianDistribution () ## ## Create a @code{prob.RicianDistribution} object. ## ## @var{s} and @var{sigma} are the distribution parameters, which the class ## help describes. Called with no arguments the parameters take their ## defaults, @var{s} 1 and @var{sigma} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = RicianDistribution (s, sigma) if (nargin == 0) s = 1; sigma = 1; endif checkparams (s, sigma); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [s, sigma]; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'Rician distribution'); endfunction function disp (this) __disp__ (this, 'Rician distribution'); endfunction function this = set.s (this, s) checkparams (s, this.sigma); this.InputData = []; this.ParameterValues(1) = s; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function s = get.s (this) s = this.ParameterValues(1); endfunction function this = set.sigma (this, sigma) checkparams (this.s, sigma); this.InputData = []; this.ParameterValues(2) = sigma; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function sigma = get.sigma (this) sigma = this.ParameterValues(2); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.RicianDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.RicianDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = ricecdf (x, this.s, this.sigma); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= ricecdf (lx, this.s, this.sigma); p(! (lb | ub)) /= diff (ricecdf ([lx, ux], this.s, this.sigma)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.RicianDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = ricecdf (this.Truncation(1), this.s, this.sigma); up = ricecdf (this.Truncation(2), this.s, this.sigma); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = riceinv (np, this.s, this.sigma); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = riceinv (p, this.s, this.sigma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.RicianDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.RicianDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = ricestat (this.s, this.sigma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.RicianDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = ricecdf ([lx, ux], this.s, this.sigma); m = riceinv (sum (Fa_b) / 2, this.s, this.sigma); else m = riceinv (0.5, this.s, this.sigma); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.RicianDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = ricelike ([this.s, this.sigma], this.InputData.data, ... this.InputData.cens, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.RicianDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.RicianDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.RicianDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = ricepdf (x, this.s, this.sigma); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (ricecdf ([lx, ux], this.s, this.sigma)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.RicianDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.RicianDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.RicianDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.RicianDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.RicianDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.RicianDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.RicianDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.RicianDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.RicianDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the Rician distribution, @qcode{@var{pnum} = 1} selects the ## parameter @qcode{s} and @qcode{@var{pnum} = 2} selects the parameter ## @qcode{sigma}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.RicianDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.RicianDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.RicianDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.RicianDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{ricernd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) sz = [varargin{:}]; ps = prod (sz); ## Get an estimate of how many more random numbers we need to randomly ## pick the appropriate size from lx = this.Truncation(1); ux = this.Truncation(2); ratio = 1 / diff (ricecdf ([lx, ux], this.s, this.sigma)); nsize = fix (2 * ratio * ps); # times 2 to be on the safe side ## Generate the numbers and remove out-of-bound random samples r = ricernd (this.s, this.sigma, nsize, 1); r(r < lx | r > ux) = []; ## Randomly select the required size and reshape to requested dimensions idx = randperm (numel (r), ps); r = reshape (r(idx), sz); else r = ricernd (this.s, this.sigma, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.RicianDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.RicianDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, and upper limit, @var{upper}. If @var{pd} is fitted to data ## with @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.RicianDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = ricestat (this.s, this.sigma); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) censor = []; else censor = varargin{2}; endif if (nargin < 4) freq = []; else freq = varargin{3}; endif if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else options = varargin{4}; endif ## Fit data [phat, pci] = ricefit (x, alpha, censor, freq, options); [~, acov] = ricelike (phat, x, censor, freq); ## Create fitted distribution object pd = prob.RicianDistribution.makeFitted ... (phat, pci, acov, x, censor, freq); endfunction function pd = makeFitted (phat, pci, acov, x, censor, freq) s = phat(1); sigma = phat(2); pd = prob.RicianDistribution (s, sigma); pd.ParameterCI = pci; pd.ParameterIsFixed = [false, false]; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', censor, 'freq', freq); endfunction endmethods endclassdef function checkparams (s, sigma) if (! (isscalar (s) && isnumeric (s) && isreal (s) && isfinite (s) && s >= 0 )) error ("RicianDistribution: S must be a non-negative real scalar.") endif if (! (isscalar (sigma) && isnumeric (sigma) && isreal (sigma) && isfinite (sigma) && sigma > 0)) error ("RicianDistribution: SIGMA must be a positive real scalar.") endif endfunction %!demo %! ## Generate a data set of 5000 random samples from a Rician distribution with %! ## parameters s = 2 and sigma = 1. Fit a Rician distribution to this data and %! ## plot a PDF of the fitted distribution superimposed on a histogram of the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ('Rician', 's', 2, 'sigma', 1) %! data = random (pd_fixed, 5000, 1); %! pd_fitted = fitdist (data, 'Rician') %! plot (pd_fitted) %! msg = 'Fitted Rician distribution with s = %0.2f and sigma = %0.2f'; %! title (sprintf (msg, pd_fitted.s, pd_fitted.sigma)) ## Test output %!shared pd, t %! pd = prob.RicianDistribution; %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0:5]), [0, 0.2671, 0.7310, 0.9563, 0.9971, 0.9999], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0, 0.8466, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4, NaN]), [0.5120, 0.7310, 0.9563, 0.9971, NaN], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4, NaN]), [0, 0, 0.8466, 1, NaN], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [0, 0.8501, 1.2736, 1.6863, 2.2011, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.1517, 2.3296, 2.5545, 2.8868, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 1.2736, 1.6863, 2.2011, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.3296, 2.5545, 2.8868, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 1.0890, 1e-4); %!assert_equal (iqr (t), 0.5928, 1e-4); %!assert_equal (mean (pd), 1.5486, 1e-4); %!assert_equal (mean (t), 2.5380, 1e-4); %!assert_equal (median (pd), 1.4755, 1e-4); %!assert_equal (median (t), 2.4341, 1e-4); %!assert_equal (pdf (pd, [0:5]), [0, 0.4658, 0.3742, 0.0987, 0.0092, 0.0003], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 1.4063, 0.3707, 0.0346, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1.5, NaN]), [0, 0.4864, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1.5, NaN]), [0, 0, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 0.7758, 1e-4); %!assert_equal (std (t), 0.4294, 1e-4); %!assert_equal (var (pd), 0.6019, 1e-4); %!assert_equal (var (t), 0.1844, 1e-4); %!test %! ## The profile over the first free parameter: 21 grid values, one row of %! ## OTHER per value, and the likelihood peaking at the fitted estimate. The %! ## sample is Rician: fitting arbitrary positive data drives s to its own %! ## boundary, and the confidence interval it profiles over degenerates. %! x = [0.584700; 0.962174; 1.201400; 1.388590; 1.548120; 1.690860; ... %! 1.822800; 1.947740; 2.068370; 2.186810; 2.304930; 2.424520; ... %! 2.547550; 2.676370; 2.814120; 2.965420; 3.137930; 3.346330; ... %! 3.626640; 4.133390]; %! pd = fitdist (x, 'Rician'); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (size (param), [1, 21]); %! assert_equal (size (other), [21, 1]); %! assert_equal (proflik (pd), nlogL); %! [~, imax] = max (nlogL); %! assert_equal (abs (param(imax) - pd.ParameterValues(1)) <= param(2) - param(1), true); ## Test input validation ## 'prob.RicianDistribution' constructor %!error ... %! prob.RicianDistribution (-eps, 1) %!error ... %! prob.RicianDistribution (-1, 1) %!error ... %! prob.RicianDistribution (Inf, 1) %!error ... %! prob.RicianDistribution (i, 1) %!error ... %! prob.RicianDistribution ('a', 1) %!error ... %! prob.RicianDistribution ([1, 2], 1) %!error ... %! prob.RicianDistribution (NaN, 1) %!error ... %! prob.RicianDistribution (1, 0) %!error ... %! prob.RicianDistribution (1, -1) %!error ... %! prob.RicianDistribution (1, Inf) %!error ... %! prob.RicianDistribution (1, i) %!error ... %! prob.RicianDistribution (1, 'a') %!error ... %! prob.RicianDistribution (1, [1, 2]) %!error ... %! prob.RicianDistribution (1, NaN) ## 'cdf' method %!error ... %! cdf (prob.RicianDistribution, 2, 'uper') %!error ... %! cdf (prob.RicianDistribution, 2, 3) ## 'paramci' method %!shared x %! x = gevrnd (1, 1, 1, [1, 100]); %!error ... %! paramci (prob.RicianDistribution.fit (x), 'alpha') %!error ... %! paramci (prob.RicianDistribution.fit (x), 'alpha', 0) %!error ... %! paramci (prob.RicianDistribution.fit (x), 'alpha', 1) %!error ... %! paramci (prob.RicianDistribution.fit (x), 'alpha', [0.5 2]) %!error ... %! paramci (prob.RicianDistribution.fit (x), 'alpha', '') %!error ... %! paramci (prob.RicianDistribution.fit (x), 'alpha', {0.05}) %!error ... %! paramci (prob.RicianDistribution.fit (x), 'parameter', 's', 'alpha', {0.05}) %!error ... %! paramci (prob.RicianDistribution.fit (x), 'parameter', {'s', 'sigma', 'param'}) %!error ... %! paramci (prob.RicianDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', {'s', 'sigma', 'param'}) %!error ... %! paramci (prob.RicianDistribution.fit (x), 'parameter', 'param') %!error ... %! paramci (prob.RicianDistribution.fit (x), 'alpha', 0.01, 'parameter', 'param') %!error ... %! paramci (prob.RicianDistribution.fit (x), 'NAME', 'value') %!error ... %! paramci (prob.RicianDistribution.fit (x), 'alpha', 0.01, 'NAME', 'value') %!error ... %! paramci (prob.RicianDistribution.fit (x), 'alpha', 0.01, 'parameter', 's', ... %! 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.RicianDistribution, 'Parent') %!error ... %! plot (prob.RicianDistribution, 'PlotType', 12) %!error ... %! plot (prob.RicianDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.RicianDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.RicianDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.RicianDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.RicianDistribution, 'Discrete', {true}) %!error ... %! plot (prob.RicianDistribution, 'Parent', 12) %!error ... %! plot (prob.RicianDistribution, 'Parent', 'hax') %!error ... %! plot (prob.RicianDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.RicianDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.RicianDistribution, 2) %!error ... %! proflik (prob.RicianDistribution.fit (x), 3) %!error ... %! proflik (prob.RicianDistribution.fit (x), [1, 2]) %!error ... %! proflik (prob.RicianDistribution.fit (x), {1}) %!error ... %! proflik (prob.RicianDistribution.fit (x), 1, ones (2)) %!error ... %! proflik (prob.RicianDistribution.fit (x), 1, 'Display') %!error ... %! proflik (prob.RicianDistribution.fit (x), 1, 'Display', 1) %!error ... %! proflik (prob.RicianDistribution.fit (x), 1, 'Display', {1}) %!error ... %! proflik (prob.RicianDistribution.fit (x), 1, 'Display', {'on'}) %!error ... %! proflik (prob.RicianDistribution.fit (x), 1, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.RicianDistribution.fit (x), 1, 'Display', 'onnn') %!error ... %! proflik (prob.RicianDistribution.fit (x), 1, 'NAME', 'on') %!error ... %! proflik (prob.RicianDistribution.fit (x), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.RicianDistribution.fit (x), 1, {[1 2 3 4]}, 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.RicianDistribution) %!error ... %! truncate (prob.RicianDistribution, 2) %!error ... %! truncate (prob.RicianDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.RicianDistribution (1, 1); %! pd(2) = prob.RicianDistribution (1, 3); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/StableDistribution.m000066400000000000000000001026561527311104300274510ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef StableDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.StableDistribution ## ## Stable probability distribution object. ## ## A @code{prob.StableDistribution} object consists of parameters, a model ## description, and sample data for a stable probability distribution. ## ## The stable distribution is a continuous probability distribution family ## closed under linear combinations, generalizing the normal, Cauchy, and Levy ## distributions. It is parameterized, in the Nolan @qcode{S0} ## parameterization, by a tail index (first shape parameter) @var{alpha} in ## @math{(0, 2]}, a skewness (second shape parameter) @var{beta} in ## @math{[-1, 1]}, a scale parameter @var{gam} greater than zero, and a ## location parameter @var{delta}. ## ## There are several ways to create a @code{prob.StableDistribution} object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor @qcode{prob.StableDistribution (@var{alpha}, ## @var{beta}, @var{gam}, @var{delta})} to create a stable distribution with ## fixed parameter values @var{alpha}, @var{beta}, @var{gam}, and @var{delta}. ## @item Use the static method @qcode{prob.StableDistribution.fit (@var{x}, ## @var{alpha}, @var{freq}, @var{options})} to fit a distribution to the data ## in @var{x} using the same input arguments as the @code{stblfit} function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Fitting is by maximum likelihood. Because the stable density has no closed ## form, it is evaluated by numerical inversion of the characteristic ## function, which makes fitting considerably slower than for the closed-form ## distributions. ## ## Further information about the stable distribution can be found at ## @url{https://en.wikipedia.org/wiki/Stable_distribution} ## ## @seealso{fitdist, makedist, stblpdf, stblcdf, stblinv, stblrnd, stblfit, ## stbllike} ## @end deftp properties (Dependent = true) ## -*- texinfo -*- ## @deftp {prob.StableDistribution} {property} alpha ## ## Tail index (first shape parameter) ## ## A scalar value in the range @math{(0, 2]} characterizing the tail ## behaviour of the stable distribution. You can access the @qcode{alpha} ## property using dot name assignment. ## ## @end deftp alpha ## -*- texinfo -*- ## @deftp {prob.StableDistribution} {property} beta ## ## Skewness (second shape parameter) ## ## A scalar value in the range @math{[-1, 1]} characterizing the skewness of ## the stable distribution. You can access the @qcode{beta} property using ## dot name assignment. ## ## @end deftp beta ## -*- texinfo -*- ## @deftp {prob.StableDistribution} {property} gam ## ## Scale parameter ## ## A positive scalar value characterizing the scale of the stable ## distribution. You can access the @qcode{gam} property using dot name ## assignment. ## ## @end deftp gam ## -*- texinfo -*- ## @deftp {prob.StableDistribution} {property} delta ## ## Location parameter ## ## A scalar value characterizing the location of the stable distribution. ## You can access the @qcode{delta} property using dot name assignment. ## ## @end deftp delta endproperties properties (GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.StableDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Stable'; ## -*- texinfo -*- ## @deftp {prob.StableDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 4; ## -*- texinfo -*- ## @deftp {prob.StableDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{4*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {"alpha", "beta", "gam", "delta"}; ## -*- texinfo -*- ## @deftp {prob.StableDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{4*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {"First shape parameter", ... "Second shape parameter", "Scale", "Location"}; endproperties properties (GetAccess = public, Constant = true, Hidden) CensoringAllowed = false; DistributionCode = "stbl"; ParameterRange = [realmin, -1, realmin, -Inf; 2, 1, Inf, Inf]; ParameterLogCI = [false, false, false, false]; endproperties properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.StableDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{4*1} numeric vector containing the values of the distribution ## parameters, matching the order in @qcode{ParameterNames}. This property ## is read-only; use dot name assignment on the @qcode{alpha}, @qcode{beta}, ## @qcode{gam}, and @qcode{delta} properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.StableDistribution} {property} Truncation ## ## Truncation interval ## ## A two-element numeric vector with the truncation interval, if the ## distribution is truncated. This property is read-only. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.StableDistribution} {property} IsTruncated ## ## Flag for truncated distribution ## ## A logical scalar that is true when the distribution is truncated. This ## property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.StableDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{4*4} numeric matrix containing the variance-covariance of the ## distribution parameters. This property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.StableDistribution} {property} ParameterIsFixed ## ## Flags for fixed parameters ## ## A @math{4*1} logical vector specifying which parameters are held fixed ## rather than estimated. This property is read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.StableDistribution} {property} InputData ## ## Data used for fitting the distribution ## ## A structure containing the data used to fit the distribution. It is ## empty unless the distribution was fitted with @code{fitdist} or the ## static @code{fit} method. This property is read-only. ## ## @end deftp InputData endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods (Hidden) ## -*- texinfo -*- ## @deftypefn {prob.StableDistribution} {@var{pd} =} StableDistribution (@var{alpha}, @var{beta}, @var{gam}, @var{delta}) ## @deftypefnx {prob.StableDistribution} {@var{pd} =} StableDistribution () ## ## Create a @code{prob.StableDistribution} object. ## ## @var{alpha}, @var{beta}, @var{gam} and @var{delta} are the distribution ## parameters, which the class help describes. Called with no arguments the ## parameters take their defaults, @var{alpha} 2, @var{beta} 0, @var{gam} 1 ## and @var{delta} 0. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = StableDistribution (alpha, beta, gam, delta) if (nargin == 0) alpha = 2; beta = 0; gam = 1; delta = 0; endif checkparams (alpha, beta, gam, delta); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [alpha, beta, gam, delta]; this.ParameterIsFixed = [true, true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) __disp__ (this, "Stable distribution"); endfunction function disp (this) __disp__ (this, "Stable distribution"); endfunction function this = set.alpha (this, alpha) checkparams (alpha, this.beta, this.gam, this.delta); this.InputData = []; this.ParameterValues(1) = alpha; this.ParameterIsFixed = [true, true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function alpha = get.alpha (this) alpha = this.ParameterValues(1); endfunction function this = set.beta (this, beta) checkparams (this.alpha, beta, this.gam, this.delta); this.InputData = []; this.ParameterValues(2) = beta; this.ParameterIsFixed = [true, true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function beta = get.beta (this) beta = this.ParameterValues(2); endfunction function this = set.gam (this, gam) checkparams (this.alpha, this.beta, gam, this.delta); this.InputData = []; this.ParameterValues(3) = gam; this.ParameterIsFixed = [true, true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function gam = get.gam (this) gam = this.ParameterValues(3); endfunction function this = set.delta (this, delta) checkparams (this.alpha, this.beta, this.gam, delta); this.InputData = []; this.ParameterValues(4) = delta; this.ParameterIsFixed = [true, true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function delta = get.delta (this) delta = this.ParameterValues(4); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {prob.StableDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.StableDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{"upper"}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. The optional @qcode{"upper"} flag computes the upper tail ## probability. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif if (nargin > 2 && strcmpi (uflag, "upper")) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, "upper")) error ("cdf: invalid argument for upper tail."); else utail = false; endif p = stblcdf (x, this.alpha, this.beta, this.gam, this.delta); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= stblcdf (lx, this.alpha, this.beta, this.gam, ... this.delta); p(! (lb | ub)) /= diff (stblcdf ([lx, ux], this.alpha, this.beta, ... this.gam, this.delta)); endif if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.StableDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = stblcdf (this.Truncation(1), this.alpha, this.beta, this.gam, ... this.delta); up = stblcdf (this.Truncation(2), this.alpha, this.beta, this.gam, ... this.delta); p(p < 0 | p > 1) = NaN; np = lp + (up - lp) .* p; x = stblinv (np, this.alpha, this.beta, this.gam, this.delta); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = stblinv (p, this.alpha, this.beta, this.gam, this.delta); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.StableDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.StableDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. The mean is @qcode{NaN} for ## @code{@var{alpha} <= 1}, where it is undefined. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); elseif (this.alpha == 2) m = this.delta; elseif (this.alpha > 1) m = this.delta - this.beta .* this.gam .* tan (pi .* this.alpha ./ 2); else m = NaN; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.StableDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = stblcdf ([lx, ux], this.alpha, this.beta, this.gam, this.delta); m = stblinv (sum (Fa_b) / 2, this.alpha, this.beta, this.gam, ... this.delta); else m = stblinv (0.5, this.alpha, this.beta, this.gam, this.delta); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.StableDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. It ## returns an empty value when @var{pd} is not fitted to data. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = stbllike ([this.alpha, this.beta, this.gam, this.delta], ... this.InputData.data, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.StableDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.StableDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise the parameter values are returned in both rows. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.StableDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability density function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = stblpdf (x, this.alpha, this.beta, this.gam, this.delta); if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); y(x < lx | x > ux) = 0; y /= diff (stblcdf ([lx, ux], this.alpha, this.beta, this.gam, ... this.delta)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.StableDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.StableDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.StableDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots the probability density function (PDF) of ## the probability distribution object @var{pd}. Name-value pair arguments ## select the plotted function and its appearance, as documented in ## @code{__plot__}. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.StableDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.StableDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.StableDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.StableDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.StableDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.StableDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the stable distribution, @qcode{@var{pnum} = 1} selects the tail ## index @qcode{alpha}, @qcode{@var{pnum} = 2} selects the skewness ## @qcode{beta}, @qcode{@var{pnum} = 3} selects the scale @qcode{gam}, and ## @qcode{@var{pnum} = 4} selects the location @qcode{delta}. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.StableDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.StableDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.StableDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.StableDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}, following the size conventions of ## @code{stblrnd}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) sz = [varargin{:}]; ps = prod (sz); lx = this.Truncation(1); ux = this.Truncation(2); ratio = 1 / diff (stblcdf ([lx, ux], this.alpha, this.beta, ... this.gam, this.delta)); nsize = fix (2 * ratio * ps); r = stblrnd (this.alpha, this.beta, this.gam, this.delta, nsize, 1); r(r < lx | r > ux) = []; while (numel (r) < ps) r = [r; stblrnd(this.alpha, this.beta, this.gam, this.delta, ... nsize, 1)]; r(r < lx | r > ux) = []; endwhile idx = randperm (numel (r), ps); r = reshape (r(idx), sz); else r = stblrnd (this.alpha, this.beta, this.gam, this.delta, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.StableDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. It is @qcode{NaN} for ## @code{@var{alpha} < 2}, where the variance is infinite. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif s = sqrt (var (this)); endfunction ## -*- texinfo -*- ## @deftypefn {prob.StableDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns ## the probability distribution @var{pd} truncated to the interval with ## lower limit @var{lower} and upper limit @var{upper}. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.StableDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the probability ## distribution object, @var{pd}. It is @qcode{NaN} for @code{@var{alpha} < ## 2}, where the variance is infinite. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); elseif (this.alpha == 2) v = 2 .* this.gam .^ 2; else v = NaN; endif endfunction endmethods methods (Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) freq = []; else freq = varargin{2}; endif if (nargin < 4) options.Display = "off"; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else options = varargin{3}; endif ## Fit data [phat, pci] = stblfit (x, alpha, freq, options); [~, acov] = stbllike (phat, x, freq); ## Create fitted distribution object pd = prob.StableDistribution.makeFitted (phat, pci, acov, x, freq); endfunction function pd = makeFitted (phat, pci, acov, x, freq) alpha = phat(1); beta = phat(2); gam = phat(3); delta = phat(4); pd = prob.StableDistribution (alpha, beta, gam, delta); pd.ParameterCI = pci; pd.ParameterIsFixed = [false, false, false, false]; pd.ParameterCovariance = acov; pd.InputData = struct ("data", x, "cens", [], "freq", freq); endfunction endmethods endclassdef function checkparams (alpha, beta, gam, delta) if (! (isscalar (alpha) && isnumeric (alpha) && isreal (alpha) ... && alpha > 0 && alpha <= 2)) error ("StableDistribution: ALPHA must be a real scalar in (0, 2]."); endif if (! (isscalar (beta) && isnumeric (beta) && isreal (beta) ... && beta >= -1 && beta <= 1)) error ("StableDistribution: BETA must be a real scalar in [-1, 1]."); endif if (! (isscalar (gam) && isnumeric (gam) && isreal (gam) ... && isfinite (gam) && gam > 0)) error ("StableDistribution: GAM must be a positive real scalar."); endif if (! (isscalar (delta) && isnumeric (delta) && isreal (delta) ... && isfinite (delta))) error ("StableDistribution: DELTA must be a real scalar."); endif endfunction %!demo %! ## Create a stable distribution and plot its pdf %! pd = makedist ("Stable", "alpha", 1.5, "beta", 0.5, "gam", 1, "delta", 0); %! plot (pd); %! title ("Stable distribution, alpha = 1.5, beta = 0.5"); ## Test output against MATLAB (created via makedist) %!shared pd %! pd = makedist ("Stable", "alpha", 1.5, "beta", 0.5, "gam", 1, "delta", 0); %!test %! assert_equal (pd.alpha, 1.5); %! assert_equal (pd.beta, 0.5); %! assert_equal (pd.gam, 1); %! assert_equal (pd.delta, 0); %! assert_equal (pd.DistributionName, "Stable"); %! assert_equal (pd.NumParameters, 4); %!test %! x = -5:5; %! exp_p = [0.00961772128347771, 0.0143422747723476, 0.0257902242195547, ... %! 0.0657154294128386, 0.201576145758624, 0.462186560100778, ... %! 0.712063555515659, 0.855535196378772, 0.921201224725992, ... %! 0.951409668616683, 0.966845678836178]; %! assert_equal (cdf (pd, x), exp_p, 1e-8); %!test %! assert_equal (icdf (pd, [0.1, 0.5, 0.9]), ... %! [-1.63127009138493, 0.133853042315326, 2.58231785139714], 1e-6); %!test # mean, variance, median of a skewed stable (alpha = 1.5) %! assert_equal (mean (pd), 0.5, 1e-12); %! assert_equal (isnan (var (pd)), true); %! assert_equal (median (pd), 0.133853042315326, 1e-6); ## alpha = 2 is the normal distribution with variance 2*gam^2 %!test %! pn = makedist ("Stable", "alpha", 2, "beta", 0, "gam", 1, "delta", 0); %! assert_equal (mean (pn), 0); %! assert_equal (var (pn), 2, 1e-12); %! assert_equal (std (pn), sqrt (2), 1e-12); %! assert_equal (pdf (pn, 0:2), normpdf (0:2, 0, sqrt (2)), 1e-12); ## alpha <= 1 has no finite mean %!test %! pc = makedist ("Stable", "alpha", 0.8, "beta", 0.5, "gam", 1, "delta", 0); %! assert_equal (isnan (mean (pc)), true); %! assert_equal (isnan (var (pc)), true); %! assert_equal (median (pc), 0.250487323305453, 1e-5); ## Scale and location, and a truncated distribution %!test %! ps = makedist ("Stable", "alpha", 1.5, "beta", 0.5, "gam", 2, "delta", 3); %! assert_equal (mean (ps), 4, 1e-12); %! assert_equal (median (ps), 3.26770608463065, 1e-6); %!test # truncation renormalizes and bounds the support %! pt = truncate (pd, -1, 3); %! assert_equal (pt.IsTruncated, true); %! assert_equal (cdf (pt, [-2, 3]), [0, 1]); %! assert_equal (isfinite (mean (pt)), true); %! r = random (pt, 100, 1); %! assert_equal (all (r >= -1 & r <= 3, 'all'), true); %!test %! ## The profile over alpha: 21 grid values, one row of OTHER per value, and %! ## the likelihood peaking at the fitted estimate. The sample is stable. %! x = [-4.481370; -2.439510; -1.787770; -1.391000; -1.095410; -0.852136; ... %! -0.639452; -0.445603; -0.263227; -0.087082; 0.087082; 0.263227; ... %! 0.445603; 0.639452; 0.852136; 1.095410; 1.391000; 1.787770; ... %! 2.439510; 4.481370]; %! pd = fitdist (x, 'Stable'); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (size (param), [1, 21]); %! assert_equal (size (other), [21, 3]); %! assert_equal (proflik (pd), nlogL); %! [~, imax] = max (nlogL); %! assert_equal (abs (param(imax) - pd.ParameterValues(1)) <= param(2) - param(1), true); ## Test input validation %!error ... %! proflik (fitdist ([0.3; -1.2; 0.8; 1.5; -0.4; 0.2; -0.9; 1.1; 0.6; -0.3; ... %! 1.8; -1.5; 0.4; 0.9; -0.7; 1.2; -0.2; 0.5; -1.1; 0.7], ... %! 'Stable'), 1) %!error ... %! prob.StableDistribution (2.5, 0, 1, 0) %!error ... %! prob.StableDistribution (1.5, 2, 1, 0) %!error ... %! prob.StableDistribution (1.5, 0, 0, 0) %!error ... %! prob.StableDistribution (1.5, 0, 1, Inf) %!error ... %! cdf ([pd, pd], 1) statistics-release-2.1.2/inst/Distribution_Classes/+prob/TriangularDistribution.m000066400000000000000000000675621527311104300303550ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef TriangularDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.TriangularDistribution ## ## Triangular probability distribution object. ## ## A @code{prob.TriangularDistribution} object consists of parameters, a model ## description, and sample data for a triangular probability distribution. ## ## The triangular distribution uses the following parameters. ## ## @multitable @columnfractions 0.25 0.48 0.27 ## @headitem @var{Parameter} @tab @var{Description} @tab @var{Support} ## ## @item @qcode{A} @tab Lower limit @tab @math{-Inf < A < Inf} ## @item @qcode{B} @tab Peak location @tab @math{A <= B <= C} ## @item @qcode{C} @tab Upper limit @tab @math{C > A} ## @end multitable ## ## There are several ways to create a @code{prob.TriangularDistribution} ## object. ## ## @itemize ## @item Create a distribution with specified parameter values using the ## @code{makedist} function. ## @item Use the constructor ## @qcode{prob.TriangularDistribution (@var{A}, @var{B}, @var{C})} to create a ## triangular distribution with specified parameter values @var{A}, @var{B}, ## and @var{C}. ## @end itemize ## ## It is highly recommended to use @code{makedist} function to create ## probability distribution objects, instead of the constructor. ## ## Further information about the triangular distribution can be found ## at @url{https://en.wikipedia.org/wiki/Triangular_distribution} ## ## @seealso{makedist, tricdf, triinv, tripdf, trirnd, tristat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.TriangularDistribution} {property} A ## ## Lower limit parameter ## ## A scalar value characterizing the lower limit of the ## triangular distribution. You can access the @qcode{A} ## property using dot name assignment. ## ## @end deftp A ## -*- texinfo -*- ## @deftp {prob.TriangularDistribution} {property} B ## ## Peak location parameter ## ## A scalar value characterizing the peak location of the ## triangular distribution. You can access the @qcode{B} ## property using dot name assignment. ## ## @end deftp B ## -*- texinfo -*- ## @deftp {prob.TriangularDistribution} {property} C ## ## Upper limit parameter ## ## A scalar value characterizing the upper limit of the ## triangular distribution. You can access the @qcode{C} ## property using dot name assignment. ## ## @end deftp C endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.TriangularDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Triangular'; ## -*- texinfo -*- ## @deftp {prob.TriangularDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 3; ## -*- texinfo -*- ## @deftp {prob.TriangularDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{3*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'A', 'B', 'C'}; ## -*- texinfo -*- ## @deftp {prob.TriangularDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{3*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Lower limit', 'Peak location', 'Upper limit'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = false; DistributionCode = 'tri'; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.TriangularDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{3*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{A}, @qcode{B}, and ## @qcode{C} properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.TriangularDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.TriangularDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.TriangularDistribution} {@var{pd} =} TriangularDistribution (@var{A}, @var{B}, @var{C}) ## @deftypefnx {prob.TriangularDistribution} {@var{pd} =} TriangularDistribution () ## ## Create a @code{prob.TriangularDistribution} object. ## ## @var{A}, @var{B} and @var{C} are the distribution parameters, which the ## class help describes. Called with no arguments the parameters take their ## defaults, @var{A} 0, @var{B} 0.5 and @var{C} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = TriangularDistribution (A, B, C) if (nargin == 0) A = 0; B = 0.5; C = 1; endif checkparams (A, B, C); this.IsTruncated = false; this.ParameterValues = [A, B, C]; endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'Triangular distribution'); endfunction function disp (this) __disp__ (this, 'Triangular distribution'); endfunction function this = set.A (this, A) checkparams (A, this.B, this.C); this.ParameterValues(1) = A; endfunction function A = get.A (this) A = this.ParameterValues(1); endfunction function this = set.B (this, B) checkparams (this.A, B, this.C); this.ParameterValues(2) = B; endfunction function B = get.B (this) B = this.ParameterValues(2); endfunction function this = set.C (this, C) checkparams (this.A, this.B, C); this.ParameterValues(3) = C; endfunction function C = get.C (this) C = this.ParameterValues(3); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.TriangularDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.TriangularDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = tricdf (x, this.A, this.B, this.C); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= tricdf (lx, this.A, this.B, this.C); p(! (lb | ub)) /= diff (tricdf ([lx, ux], this.A, this.B, this.C)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.TriangularDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = tricdf (this.Truncation(1), this.A, this.B, this.C); up = tricdf (this.Truncation(2), this.A, this.B, this.C); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = triinv (np, this.A, this.B, this.C); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = triinv (p, this.A, this.B, this.C); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.TriangularDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.TriangularDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = tristat (this.A, this.B, this.C); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.TriangularDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = tricdf ([lx, ux], this.A, this.B, this.C); m = triinv (sum (Fa_b) / 2, this.A, this.B, this.C); else m = triinv (0.5, this.A, this.B, this.C); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.TriangularDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability density function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = tripdf (x, this.A, this.B, this.C); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (tricdf ([lx, ux], this.A, this.B, this.C)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.TriangularDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.TriangularDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.TriangularDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). ## @qcode{'cdf'} plots the cumulative density function (CDF). ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, this option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for the plot. ## If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.TriangularDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.TriangularDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.TriangularDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.TriangularDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{trirnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) sz = [varargin{:}]; ps = prod (sz); ## Get an estimate of how many more random numbers we need to randomly ## pick the appropriate size from lx = this.Truncation(1); ux = this.Truncation(2); ratio = 1 / diff (tricdf ([lx, ux], this.A, this.B, this.C)); nsize = fix (2 * ratio * ps); # times 2 to be on the safe side ## Generate the numbers and remove out-of-bound random samples r = trirnd (this.A, this.B, this.C, nsize, 1); r(r < lx | r > ux) = []; ## Randomly select the required size and reshape to requested dimensions idx = randperm (numel (r), ps); r = reshape (r(idx), sz); else r = trirnd (this.A, this.B, this.C, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.TriangularDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.TriangularDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, and upper limit, @var{upper}. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif ## Check boundaries and constrain within support [A, C] lower(lower < this.A) = this.A; upper(upper > this.C) = this.C; this.Truncation = [lower, upper]; this.IsTruncated = true; endfunction ## -*- texinfo -*- ## @deftypefn {prob.TriangularDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = tristat (this.A, this.B, this.C); endif endfunction endmethods endclassdef function checkparams (A, B, C) if (! (isscalar (A) && isnumeric (A) && isreal (A) && isfinite (A))) error ("TriangularDistribution: lower limit A must be a real scalar.") endif if (! (isscalar (B) && isnumeric (B) && isreal (B) && isfinite (B))) error ("TriangularDistribution: mode B must be a real scalar.") endif if (! (isscalar (C) && isnumeric (C) && isreal (C) && isfinite (C))) error ("TriangularDistribution: upper limit C must be a real scalar.") endif if (! (A < C)) error (strcat ("TriangularDistribution: lower limit A must", ... " be less than upper limit C.")) endif if (! (A <= B && B <= C)) error (strcat ("TriangularDistribution: mode B must be within", ... " lower limit A and upper limit C.")) endif endfunction %!demo %! ## Generate a data set of 5000 random samples from a Triangular distribution %! ## with parameters A = 0, B = 1, C = 2. Fit a Triangular distribution to %! ## this data and plot a PDF of the fitted distribution superimposed on a %! ## histogram of the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ('Triangular', 'A', 0, 'B', 1, 'C', 2); %! data = random (pd_fixed, 5000, 1); %! A = min (data); %! C = mean (data); %! B = max (data); %! %! [counts, centers] = hist (data, 50); %! bin_width = centers(2) - centers(1); %! normalized_counts = counts / (sum (counts) * bin_width); %! bar (centers, normalized_counts, 1); %! hold on; %! %! x = linspace (A, B, 100); %! y = (2 * (x - A) / (C - A) .* (x <= C)) + (2 * (B - x) / (B - C) .* (x > C)); %! %! plot (x, y, 'r-', 'LineWidth', 2); %! %! msg = sprintf ("Fitted Triangular distribution with A = %0.2f, C = %0.2f, B = %0.2f", A, C, B); %! title (msg); %! %! hold off; ## Test output %!shared pd, t %! pd = prob.TriangularDistribution (0, 3, 5); %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0:5]), [0, 0.0667, 0.2667, 0.6000, 0.9000, 1], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0, 0.5263, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4, NaN]), [0.1500, 0.2667, 0.6, 0.9, NaN], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4, NaN]), [0, 0, 0.5263, 1, NaN], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [0, 1.7321, 2.4495, 3, 3.5858, 5], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.4290, 2.7928, 3.1203, 3.4945, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 2.4495, 3, 3.5858, 5, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.7928, 3.1203, 3.4945, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 1.4824, 1e-4); %!assert_equal (iqr (t), 0.8678, 1e-4); %!assert_equal (mean (pd), 2.6667, 1e-4); %!assert_equal (mean (t), 2.9649, 1e-4); %!assert_equal (median (pd), 2.7386, 1e-4); %!assert_equal (median (t), 2.9580, 1e-4); %!assert_equal (pdf (pd, [0:5]), [0, 0.1333, 0.2667, 0.4, 0.2, 0], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 0.4211, 0.6316, 0.3158, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1.5, NaN]), [0, 0.2, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1.5, NaN]), [0, 0, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 1.0274, 1e-4); %!assert_equal (std (t), 0.5369, 1e-4); %!assert_equal (var (pd), 1.0556, 1e-4); %!assert_equal (var (t), 0.2882, 1e-4); ## Test input validation ## 'prob.TriangularDistribution' constructor %!error ... %! prob.TriangularDistribution (i, 1, 2) %!error ... %! prob.TriangularDistribution (Inf, 1, 2) %!error ... %! prob.TriangularDistribution ([1, 2], 1, 2) %!error ... %! prob.TriangularDistribution ('a', 1, 2) %!error ... %! prob.TriangularDistribution (NaN, 1, 2) %!error ... %! prob.TriangularDistribution (1, i, 2) %!error ... %! prob.TriangularDistribution (1, Inf, 2) %!error ... %! prob.TriangularDistribution (1, [1, 2], 2) %!error ... %! prob.TriangularDistribution (1, 'a', 2) %!error ... %! prob.TriangularDistribution (1, NaN, 2) %!error ... %! prob.TriangularDistribution (1, 2, i) %!error ... %! prob.TriangularDistribution (1, 2, Inf) %!error ... %! prob.TriangularDistribution (1, 2, [1, 2]) %!error ... %! prob.TriangularDistribution (1, 2, 'a') %!error ... %! prob.TriangularDistribution (1, 2, NaN) %!error ... %! prob.TriangularDistribution (1, 1, 1) %!error ... %! prob.TriangularDistribution (1, 0.5, 2) ## 'cdf' method %!error ... %! cdf (prob.TriangularDistribution, 2, 'uper') %!error ... %! cdf (prob.TriangularDistribution, 2, 3) ## 'plot' method %!error ... %! plot (prob.TriangularDistribution, 'Parent') %!error ... %! plot (prob.TriangularDistribution, 'PlotType', 12) %!error ... %! plot (prob.TriangularDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.TriangularDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.TriangularDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.TriangularDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.TriangularDistribution, 'Discrete', {true}) %!error ... %! plot (prob.TriangularDistribution, 'Parent', 12) %!error ... %! plot (prob.TriangularDistribution, 'Parent', 'hax') %!error ... %! plot (prob.TriangularDistribution, 'invalidNAME', 'pdf') %!error <'probability' PlotType is not supported for 'Triangular'.> ... %! plot (prob.TriangularDistribution, 'PlotType', 'probability') ## 'truncate' method %!error ... %! truncate (prob.TriangularDistribution) %!error ... %! truncate (prob.TriangularDistribution, 2) %!error ... %! truncate (prob.TriangularDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.TriangularDistribution (0, 1, 2); %! pd(2) = prob.TriangularDistribution (0, 1, 2); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error pdf (pd, 1) %!error plot (pd) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/UniformDistribution.m000066400000000000000000000641531527311104300276550ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef UniformDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.UniformDistribution ## ## Continuous uniform probability distribution object. ## ## A @code{prob.UniformDistribution} object consists of parameters, a model ## description, and sample data for a uniform probability distribution. ## ## The uniform distribution is a continuous probability distribution that ## models random variables that are equally likely to take any value within a ## specified interval defined by the lower limit @var{Lower} and upper limit ## @var{Upper}. ## ## There are several ways to create a @code{prob.UniformDistribution} object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor @qcode{prob.UniformDistribution (@var{Lower}, ## @var{Upper})} to create a uniform distribution with fixed parameter ## values @var{Lower} and @var{Upper}. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor. ## ## Further information about the continuous uniform distribution can be found ## at @url{https://en.wikipedia.org/wiki/Continuous_uniform_distribution} ## ## @seealso{fitdist, makedist, unifcdf, unifinv, unifpdf, unifrnd, unifit, ## unifstat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.UniformDistribution} {property} Lower ## ## Lower limit parameter ## ## A scalar value characterizing the lower bound of the uniform ## distribution. You can access the @qcode{Lower} property using dot ## name assignment. ## ## @end deftp Lower ## -*- texinfo -*- ## @deftp {prob.UniformDistribution} {property} Upper ## ## Upper limit parameter ## ## A scalar value characterizing the upper bound of the uniform ## distribution. You can access the @qcode{Upper} property using dot ## name assignment. ## ## @end deftp Upper endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.UniformDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Uniform'; ## -*- texinfo -*- ## @deftp {prob.UniformDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 2; ## -*- texinfo -*- ## @deftp {prob.UniformDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'Lower', 'Upper'}; ## -*- texinfo -*- ## @deftp {prob.UniformDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Lower limit', 'Upper limit'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = false; DistributionCode = 'unif'; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.UniformDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{2*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{Lower} and @qcode{Upper} ## properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.UniformDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.UniformDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.UniformDistribution} {@var{pd} =} UniformDistribution (@var{Lower}, @var{Upper}) ## @deftypefnx {prob.UniformDistribution} {@var{pd} =} UniformDistribution () ## ## Create a @code{prob.UniformDistribution} object. ## ## @var{Lower} and @var{Upper} are the distribution parameters, which the ## class help describes. Called with no arguments the parameters take their ## defaults, @var{Lower} 0 and @var{Upper} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = UniformDistribution (Lower, Upper) if (nargin == 0) Lower = 0; Upper = 1; endif checkparams (Lower, Upper); this.IsTruncated = false; this.ParameterValues = [Lower, Upper]; endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'Uniform distribution (continuous)'); endfunction function disp (this) __disp__ (this, 'Uniform distribution (continuous)'); endfunction function this = set.Lower (this, Lower) checkparams (Lower, this.Upper); this.ParameterValues(1) = Lower; endfunction function Lower = get.Lower (this) Lower = this.ParameterValues(1); endfunction function this = set.Upper (this, Upper) checkparams (this.Lower, Upper); this.ParameterValues(2) = Upper; endfunction function Upper = get.Upper (this) Upper = this.ParameterValues(2); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.UniformDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.UniformDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = unifcdf (x, this.Lower, this.Upper); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= unifcdf (lx, this.Lower, this.Upper); p(! (lb | ub)) /= diff (unifcdf ([lx, ux], this.Lower, this.Upper)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.UniformDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = unifcdf (this.Truncation(1), this.Lower, this.Upper); up = unifcdf (this.Truncation(2), this.Lower, this.Upper); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = unifinv (np, this.Lower, this.Upper); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = unifinv (p, this.Lower, this.Upper); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.UniformDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.UniformDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = unifstat (this.Lower, this.Upper); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.UniformDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = unifcdf ([lx, ux], this.Lower, this.Upper); m = unifinv (sum (Fa_b) / 2, this.Lower, this.Upper); else m = unifstat (this.Lower, this.Upper); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.UniformDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = unifpdf (x, this.Lower, this.Upper); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (unifcdf ([lx, ux], this.Lower, this.Upper)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.UniformDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.UniformDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.UniformDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.UniformDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.UniformDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.UniformDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.UniformDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{unifrnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) r = unifrnd (this.Truncation(1), this.Truncation(2), varargin{:}); else r = unifrnd (this.Lower, this.Upper, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.UniformDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.UniformDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, and upper limit, @var{upper}. If @var{pd} is fitted to data ## with @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif ## Check boundaries and constrain within support [Lower, Upper] lower(lower < this.Lower) = this.Lower; upper(upper > this.Upper) = this.Upper; this.Truncation = [lower, upper]; this.IsTruncated = true; endfunction ## -*- texinfo -*- ## @deftypefn {prob.UniformDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = unifstat (this.Lower, this.Upper); endif endfunction endmethods endclassdef function checkparams (Lower, Upper) if (! (isscalar (Lower) && isnumeric (Lower) && isreal (Lower) && isfinite (Lower))) error ("UniformDistribution: LOWER must be a real scalar.") endif if (! (isscalar (Upper) && isnumeric (Upper) && isreal (Upper) && isfinite (Upper))) error ("UniformDistribution: UPPER must be a real scalar.") endif if (! (Lower < Upper)) error ("UniformDistribution: LOWER must be less than UPPER.") endif endfunction %!demo %! ## Generate a data set of 5000 random samples from a Uniform distribution with %! ## parameters Lower = 0 and Upper = 10. Create a Uniform distribution with these %! ## parameters and plot its PDF superimposed on a histogram of the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ('Uniform', 'Lower', 0, 'Upper', 10); %! data = random (pd, 5000, 1); %! %! x = linspace (pd.Lower - 1, pd.Upper + 1, 500); %! y = pdf (pd, x); %! plot (x, y, 'r-', 'LineWidth', 2); %! hold on; %! %! [counts, centers] = hist (data, 50); %! bin_width = centers(2) - centers(1); %! normalized_counts = counts / (sum (counts) * bin_width); %! bar (centers, normalized_counts, 1); %! %! msg = 'Uniform distribution with Lower = %0.2f and Upper = %0.2f'; %! title (sprintf (msg, pd.Lower, pd.Upper)); %! legend ('PDF', 'Histogram', 'location', 'northeast'); %! %! hold off; ## Test output %!shared pd, t %! pd = prob.UniformDistribution (0, 5); %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0:5]), [0, 0.2, 0.4, 0.6, 0.8, 1], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0, 0.5, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4, NaN]), [0.3, 0.4, 0.6, 0.8, NaN], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4, NaN]), [0, 0, 0.5, 1, NaN], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [0, 1, 2, 3, 4, 5], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.4, 2.8, 3.2, 3.6, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 2, 3, 4, 5, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.8, 3.2, 3.6, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 2.5, 1e-14); %!assert_equal (iqr (t), 1, 1e-14); %!assert_equal (mean (pd), 2.5, 1e-14); %!assert_equal (mean (t), 3, 1e-14); %!assert_equal (median (pd), 2.5, 1e-14); %!assert_equal (median (t), 3, 1e-14); %!assert_equal (pdf (pd, [0:5]), [0.2, 0.2, 0.2, 0.2, 0.2, 0.2], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 0.5, 0.5, 0.5, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1.5, NaN]), [0, 0.2, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1.5, NaN]), [0, 0, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 1.4434, 1e-4); %!assert_equal (std (t), 0.5774, 1e-4); %!assert_equal (var (pd), 2.0833, 1e-4); %!assert_equal (var (t), 0.3333, 1e-4); ## Test input validation ## 'prob.UniformDistribution' constructor %!error ... %! prob.UniformDistribution (i, 1) %!error ... %! prob.UniformDistribution (Inf, 1) %!error ... %! prob.UniformDistribution ([1, 2], 1) %!error ... %! prob.UniformDistribution ('a', 1) %!error ... %! prob.UniformDistribution (NaN, 1) %!error ... %! prob.UniformDistribution (1, i) %!error ... %! prob.UniformDistribution (1, Inf) %!error ... %! prob.UniformDistribution (1, [1, 2]) %!error ... %! prob.UniformDistribution (1, 'a') %!error ... %! prob.UniformDistribution (1, NaN) %!error ... %! prob.UniformDistribution (2, 1) ## 'cdf' method %!error ... %! cdf (prob.UniformDistribution, 2, 'uper') %!error ... %! cdf (prob.UniformDistribution, 2, 3) ## 'plot' method %!error ... %! plot (prob.UniformDistribution, 'Parent') %!error ... %! plot (prob.UniformDistribution, 'PlotType', 12) %!error ... %! plot (prob.UniformDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.UniformDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.UniformDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.UniformDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.UniformDistribution, 'Discrete', {true}) %!error ... %! plot (prob.UniformDistribution, 'Parent', 12) %!error ... %! plot (prob.UniformDistribution, 'Parent', 'hax') %!error ... %! plot (prob.UniformDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.UniformDistribution, 'PlotType', 'probability') ## 'truncate' method %!error ... %! truncate (prob.UniformDistribution) %!error ... %! truncate (prob.UniformDistribution, 2) %!error ... %! truncate (prob.UniformDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.UniformDistribution (0, 1); %! pd(2) = prob.UniformDistribution (0, 2); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error pdf (pd, 1) %!error plot (pd) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/WeibullDistribution.m000066400000000000000000001221241527311104300276320ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef WeibullDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.WeibullDistribution ## ## Weibull probability distribution object. ## ## A @code{prob.WeibullDistribution} object consists of parameters, a model ## description, and sample data for a Weibull probability distribution. ## ## The Weibull distribution is a continuous probability distribution that ## models the time to failure of materials or the lifetime of mechanical ## systems. It is defined by scale parameter @var{A} and shape ## parameter @var{B}. ## ## @var{A} is the @math{lambda} of the usual mathematical notation and ## @var{B} is its @math{k}. The @code{wbl*} functions name the same two ## quantities @var{lambda} and @var{k}; this class follows MATLAB. ## ## There are several ways to create a @code{prob.WeibullDistribution} object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor @qcode{prob.WeibullDistribution (@var{A}, ## @var{B})} to create a Weibull distribution with fixed parameter ## values @var{A} and @var{B}. ## @item Use the static method @qcode{prob.WeibullDistribution.fit (@var{x}, ## @var{alpha}, @var{censor}, @var{freq})} to fit a distribution to the ## data in @var{x} using the same input arguments as the @code{wblfit} ## function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the Weibull distribution can be found at ## @url{https://en.wikipedia.org/wiki/Weibull_distribution} ## ## @seealso{fitdist, makedist, wblcdf, wblinv, wblpdf, wblrnd, wblfit, ## wbllike, wblstat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.WeibullDistribution} {property} A ## ## Scale parameter ## ## A positive scalar value characterizing the scale of the ## Weibull distribution. You can access the @qcode{A} ## property using dot name assignment. ## ## @end deftp A ## -*- texinfo -*- ## @deftp {prob.WeibullDistribution} {property} B ## ## Shape parameter ## ## A positive scalar value characterizing the shape of the ## Weibull distribution. You can access the @qcode{B} ## property using dot name assignment. ## ## @end deftp B endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.WeibullDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 'Weibull'; ## -*- texinfo -*- ## @deftp {prob.WeibullDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 2; ## -*- texinfo -*- ## @deftp {prob.WeibullDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'A', 'B'}; ## -*- texinfo -*- ## @deftp {prob.WeibullDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{2*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Scale', 'Shape'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = true; DistributionCode = 'wbl'; ParameterRange = [realmin, realmin; Inf, Inf]; ParameterLogCI = [true, true]; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.WeibullDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{2*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{A} and @qcode{B} ## properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.WeibullDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.WeibullDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.WeibullDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{2*2} numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter, and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only ## meaningful when the distribution was fitted to data. If the distribution ## object was created with fixed parameters, or a parameter of a fitted ## distribution is modified, then all elements of the variance-covariance ## are zero. This property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.WeibullDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*2} logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.WeibullDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: a numeric vector of logical values indicating ## censoring information corresponding to the elements of the data used for ## distribution fitting. If no censoring vector was used for distribution ## fitting, then this field defaults to an empty array. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.WeibullDistribution} {@var{pd} =} WeibullDistribution (@var{A}, @var{B}) ## @deftypefnx {prob.WeibullDistribution} {@var{pd} =} WeibullDistribution () ## ## Create a @code{prob.WeibullDistribution} object. ## ## @var{A} and @var{B} are the distribution parameters, which the class help ## describes. Called with no arguments the parameters take their defaults, ## @var{A} 1 and @var{B} 1. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = WeibullDistribution (A, B) if (nargin == 0) A = 1; B = 1; endif checkparams (A, B); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [A, B]; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 'Weibull distribution'); endfunction function disp (this) __disp__ (this, 'Weibull distribution'); endfunction function this = set.A (this, A) checkparams (A, this.B); this.InputData = []; this.ParameterValues(1) = A; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function A = get.A (this) A = this.ParameterValues(1); endfunction function this = set.B (this, B) checkparams (this.A, B); this.InputData = []; this.ParameterValues(2) = B; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function B = get.B (this) B = this.ParameterValues(2); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.WeibullDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.WeibullDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = wblcdf (x, this.A, this.B); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= wblcdf (lx, this.A, this.B); p(! (lb | ub)) /= diff (wblcdf ([lx, ux], this.A, this.B)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.WeibullDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = wblcdf (this.Truncation(1), this.A, this.B); up = wblcdf (this.Truncation(2), this.A, this.B); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = wblinv (np, this.A, this.B); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = wblinv (p, this.A, this.B); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.WeibullDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.WeibullDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = wblstat (this.A, this.B); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.WeibullDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = wblcdf ([lx, ux], this.A, this.B); m = wblinv (sum (Fa_b) / 2, this.A, this.B); else m = wblinv (0.5, this.A, this.B); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.WeibullDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = wbllike ([this.A, this.B], this.InputData.data, ... this.InputData.cens, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.WeibullDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.WeibullDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.WeibullDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = wblpdf (x, this.A, this.B); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (wblcdf ([lx, ux], this.A, this.B)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.WeibullDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.WeibullDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.WeibullDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.WeibullDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.WeibullDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.WeibullDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.WeibullDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.WeibullDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.WeibullDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the Weibull distribution, @qcode{@var{pnum} = 1} selects the ## parameter @qcode{A} and @qcode{@var{pnum} = 2} selects the ## parameter @qcode{B}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.WeibullDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.WeibullDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.WeibullDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.WeibullDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{wblrnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) sz = [varargin{:}]; ps = prod (sz); ## Get an estimate of how many more random numbers we need to randomly ## pick the appropriate size from lx = this.Truncation(1); ux = this.Truncation(2); ratio = 1 / diff (wblcdf ([lx, ux], this.A, this.B)); nsize = fix (2 * ratio * ps); # times 2 to be on the safe side ## Generate the numbers and remove out-of-bound random samples r = wblrnd (this.A, this.B, nsize, 1); r(r < lx | r > ux) = []; ## Randomly select the required size and reshape to requested dimensions idx = randperm (numel (r), ps); r = reshape (r(idx), sz); else r = wblrnd (this.A, this.B, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.WeibullDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.WeibullDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, and upper limit, @var{upper}. If @var{pd} is fitted to data ## with @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.WeibullDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = wblstat (this.A, this.B); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) censor = []; else censor = varargin{2}; endif if (nargin < 4) freq = []; else freq = varargin{3}; endif if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else options = varargin{4}; endif ## Fit data [phat, pci] = wblfit (x, alpha, censor, freq, options); [~, acov] = wbllike (phat, x, censor, freq); ## Create fitted distribution object pd = prob.WeibullDistribution.makeFitted ... (phat, pci, acov, x, censor, freq); endfunction function pd = makeFitted (phat, pci, acov, x, censor, freq) A = phat(1); B = phat(2); pd = prob.WeibullDistribution (A, B); pd.ParameterCI = pci; pd.ParameterIsFixed = [false, false]; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', censor, 'freq', freq); endfunction endmethods endclassdef function checkparams (A, B) if (! (isscalar (A) && isnumeric (A) && isreal (A) && isfinite (A) && A > 0 )) error ("WeibullDistribution: LAMBDA must be a positive real scalar.") endif if (! (isscalar (B) && isnumeric (B) && isreal (B) && isfinite (B) && B > 0)) error ("WeibullDistribution: K must be a positive real scalar.") endif endfunction %!demo %! ## Generate a data set of 5000 random samples from a Weibull distribution with %! ## parameters A = 1 and B = 2. Fit a Weibull distribution to this data and plot %! ## a PDF of the fitted distribution superimposed on a histogram of a data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ('Weibull', 'A', 1, 'B', 2) %! data = random (pd_fixed, 5000, 1); %! pd_fitted = fitdist (data, 'Weibull') %! plot (pd_fitted) %! msg = 'Fitted Weibull distribution with A = %0.2f and B = %0.2f'; %! title (sprintf (msg, pd_fitted.A, pd_fitted.B)) ## Test output %!shared pd, t %! pd = prob.WeibullDistribution; %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0:5]), [0, 0.6321, 0.8647, 0.9502, 0.9817, 0.9933], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0, 0.7311, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4, NaN]), [0.7769, 0.8647, 0.9502, 0.9817, NaN], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4, NaN]), [0, 0, 0.7311, 1, NaN], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [0, 0.2231, 0.5108, 0.9163, 1.6094, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.1899, 2.4244, 2.7315, 3.1768, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, 0.5108, 0.9163, 1.6094, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.4244, 2.7315, 3.1768, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 1.0986, 1e-4); %!assert_equal (iqr (t), 0.8020, 1e-4); %!assert_equal (mean (pd), 1, 1e-14); %!assert_equal (mean (t), 2.6870, 1e-4); %!assert_equal (median (pd), 0.6931, 1e-4); %!assert_equal (median (t), 2.5662, 1e-4); %!assert_equal (pdf (pd, [0:5]), [1, 0.3679, 0.1353, 0.0498, 0.0183, 0.0067], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 1.1565, 0.4255, 0.1565, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1.5, NaN]), [0, 0.2231, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1.5, NaN]), [0, 0, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 1, 1e-14); %!assert_equal (std (t), 0.5253, 1e-4); %!assert_equal (var (pd), 1, 1e-14); %!assert_equal (var (t), 0.2759, 1e-4); %!test %! ## The profile over the first free parameter: 21 grid values, one row of %! ## OTHER per value, and the likelihood peaking at the fitted estimate. %! x = [1.2; 0.4; 3.1; 0.7; 2.5; 1.8; 0.3; 4.2; 1.1; 0.9; ... %! 2.2; 0.6; 1.5; 3.7; 0.8; 2.9; 1.3; 0.5; 2.0; 1.6]; %! pd = fitdist (x, 'Weibull'); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (size (param), [1, 21]); %! assert_equal (size (other), [21, 1]); %! assert_equal (proflik (pd), nlogL); %! [~, imax] = max (nlogL); %! assert_equal (abs (param(imax) - pd.ParameterValues(1)) <= param(2) - param(1), true); ## Test input validation ## 'prob.WeibullDistribution' constructor %!error ... %! prob.WeibullDistribution (0, 1) %!error ... %! prob.WeibullDistribution (-1, 1) %!error ... %! prob.WeibullDistribution (Inf, 1) %!error ... %! prob.WeibullDistribution (i, 1) %!error ... %! prob.WeibullDistribution ('a', 1) %!error ... %! prob.WeibullDistribution ([1, 2], 1) %!error ... %! prob.WeibullDistribution (NaN, 1) %!error ... %! prob.WeibullDistribution (1, 0) %!error ... %! prob.WeibullDistribution (1, -1) %!error ... %! prob.WeibullDistribution (1, Inf) %!error ... %! prob.WeibullDistribution (1, i) %!error ... %! prob.WeibullDistribution (1, 'a') %!error ... %! prob.WeibullDistribution (1, [1, 2]) %!error ... %! prob.WeibullDistribution (1, NaN) ## 'cdf' method %!error ... %! cdf (prob.WeibullDistribution, 2, 'uper') %!error ... %! cdf (prob.WeibullDistribution, 2, 3) ## 'paramci' method %!shared x %! x = wblrnd (1, 1, [1, 100]); %!error ... %! paramci (prob.WeibullDistribution.fit (x), 'alpha') %!error ... %! paramci (prob.WeibullDistribution.fit (x), 'alpha', 0) %!error ... %! paramci (prob.WeibullDistribution.fit (x), 'alpha', 1) %!error ... %! paramci (prob.WeibullDistribution.fit (x), 'alpha', [0.5 2]) %!error ... %! paramci (prob.WeibullDistribution.fit (x), 'alpha', '') %!error ... %! paramci (prob.WeibullDistribution.fit (x), 'alpha', {0.05}) %!error ... %! paramci (prob.WeibullDistribution.fit (x), 'parameter', 'B', 'alpha', {0.05}) %!error ... %! paramci (prob.WeibullDistribution.fit (x), 'parameter', {'A', 'B', 'param'}) %!error ... %! paramci (prob.WeibullDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', {'A', 'B', 'param'}) %!error ... %! paramci (prob.WeibullDistribution.fit (x), 'parameter', 'param') %!error ... %! paramci (prob.WeibullDistribution.fit (x), 'alpha', 0.01, 'parameter', 'param') %!error ... %! paramci (prob.WeibullDistribution.fit (x), 'NAME', 'value') %!error ... %! paramci (prob.WeibullDistribution.fit (x), 'alpha', 0.01, 'NAME', 'value') %!error ... %! paramci (prob.WeibullDistribution.fit (x), 'alpha', 0.01, 'parameter', 'B', ... %! 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.WeibullDistribution, 'Parent') %!error ... %! plot (prob.WeibullDistribution, 'PlotType', 12) %!error ... %! plot (prob.WeibullDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.WeibullDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.WeibullDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.WeibullDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.WeibullDistribution, 'Discrete', {true}) %!error ... %! plot (prob.WeibullDistribution, 'Parent', 12) %!error ... %! plot (prob.WeibullDistribution, 'Parent', 'hax') %!error ... %! plot (prob.WeibullDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.WeibullDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.WeibullDistribution, 2) %!error ... %! proflik (prob.WeibullDistribution.fit (x), 3) %!error ... %! proflik (prob.WeibullDistribution.fit (x), [1, 2]) %!error ... %! proflik (prob.WeibullDistribution.fit (x), {1}) %!error ... %! proflik (prob.WeibullDistribution.fit (x), 1, ones (2)) %!error ... %! proflik (prob.WeibullDistribution.fit (x), 1, 'Display') %!error ... %! proflik (prob.WeibullDistribution.fit (x), 1, 'Display', 1) %!error ... %! proflik (prob.WeibullDistribution.fit (x), 1, 'Display', {1}) %!error ... %! proflik (prob.WeibullDistribution.fit (x), 1, 'Display', {'on'}) %!error ... %! proflik (prob.WeibullDistribution.fit (x), 1, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.WeibullDistribution.fit (x), 1, 'Display', 'onnn') %!error ... %! proflik (prob.WeibullDistribution.fit (x), 1, 'NAME', 'on') %!error ... %! proflik (prob.WeibullDistribution.fit (x), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.WeibullDistribution.fit (x), 1, {[1 2 3 4]}, 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.WeibullDistribution) %!error ... %! truncate (prob.WeibullDistribution, 2) %!error ... %! truncate (prob.WeibullDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.WeibullDistribution (1, 1); %! pd(2) = prob.WeibullDistribution (1, 3); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/+prob/tLocationScaleDistribution.m000066400000000000000000001306161527311104300311400ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef tLocationScaleDistribution < prob.ProbabilityDistribution ## -*- texinfo -*- ## @deftp {statistics} prob.tLocationScaleDistribution ## ## Location-Scale Student's T probability distribution object. ## ## A @code{prob.tLocationScaleDistribution} object consists of parameters, a ## model description, and sample data for a location-scale Student's T ## probability distribution. ## ## The location-scale Student's T distribution is a continuous probability ## distribution that generalizes the standard Student's T distribution by ## including location and scale parameters. It is defined by location ## parameter ## @var{mu}, scale parameter @var{sigma}, and degrees of freedom @var{nu}. ## ## There are several ways to create a @code{prob.tLocationScaleDistribution} ## object. ## ## @itemize ## @item Fit a distribution to data using the @code{fitdist} function. ## @item Create a distribution with fixed parameter values using the ## @code{makedist} function. ## @item Use the constructor @qcode{prob.tLocationScaleDistribution (@var{mu}, ## @var{sigma}, @var{nu})} to create a location-scale Student's T distribution ## with fixed parameter values @var{mu}, @var{sigma}, and @var{nu}. ## @item Use the static method @qcode{prob.tLocationScaleDistribution.fit ## (@var{x}, @var{censor}, @var{freq}, @var{options})} to fit a distribution ## to the data in @var{x} using the same input arguments as the @code{tlsfit} ## function. ## @end itemize ## ## It is highly recommended to use @code{fitdist} and @code{makedist} ## functions to create probability distribution objects, instead of the class ## constructor or the aforementioned static method. ## ## Further information about the location-scale Student's T distribution can ## be found at ## @url{https://en.wikipedia.org/wiki/Student%27s_t-distribution#Location-scale_t_distribution} ## ## @seealso{fitdist, makedist, tlscdf, tlsinv, tlspdf, tlsrnd, tlsfit, ## tlslike, tlsstat} ## @end deftp properties(Dependent = true) ## -*- texinfo -*- ## @deftp {prob.tLocationScaleDistribution} {property} mu ## ## Location parameter ## ## A scalar value characterizing the location of the ## location-scale Student's T distribution. You can access the @qcode{mu} ## property using dot name assignment. ## ## @end deftp mu ## -*- texinfo -*- ## @deftp {prob.tLocationScaleDistribution} {property} sigma ## ## Scale parameter ## ## A positive scalar value characterizing the scale of the location-scale ## Student's T distribution. You can access the @qcode{sigma} property using ## dot name assignment. ## ## @end deftp sigma ## -*- texinfo -*- ## @deftp {prob.tLocationScaleDistribution} {property} nu ## ## Degrees of freedom ## ## A positive scalar value characterizing the degrees of freedom of the ## location-scale Student's T distribution. You can access the @qcode{nu} ## property using dot name assignment. ## ## @end deftp nu endproperties properties(GetAccess = public, Constant = true) ## -*- texinfo -*- ## @deftp {prob.tLocationScaleDistribution} {property} DistributionName ## ## Probability distribution name ## ## A character vector specifying the name of the probability distribution ## object. This property is read-only. ## ## @end deftp DistributionName = 't Location-Scale'; ## -*- texinfo -*- ## @deftp {prob.tLocationScaleDistribution} {property} NumParameters ## ## Number of parameters ## ## A scalar integer value specifying the number of parameters characterizing ## the probability distribution. This property is read-only. ## ## @end deftp NumParameters = 3; ## -*- texinfo -*- ## @deftp {prob.tLocationScaleDistribution} {property} ParameterNames ## ## Names of parameters ## ## A @math{3*1} cell array of character vectors with each element containing ## the name of a distribution parameter. This property is read-only. ## ## @end deftp ParameterNames = {'mu', 'sigma', 'nu'}; ## -*- texinfo -*- ## @deftp {prob.tLocationScaleDistribution} {property} ParameterDescription ## ## Description of parameters ## ## A @math{3*1} cell array of character vectors with each element containing ## a short description of a distribution parameter. This property is ## read-only. ## ## @end deftp ParameterDescription = {'Location', 'Scale', 'Degrees of Freedom'}; endproperties properties(GetAccess = public, Constant = true, Hidden) CensoringAllowed = true; DistributionCode = 'tls'; ParameterRange = [-Inf, realmin, realmin; Inf, Inf, Inf]; ParameterLogCI = [false, true, true]; endproperties properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {prob.tLocationScaleDistribution} {property} ParameterValues ## ## Distribution parameter values ## ## A @math{3*1} numeric vector containing the values of the distribution ## parameters. This property is read-only. You can change the distribution ## parameters by assigning new values to the @qcode{mu}, @qcode{sigma}, and ## @qcode{nu} properties. ## ## @end deftp ParameterValues ## -*- texinfo -*- ## @deftp {prob.tLocationScaleDistribution} {property} Truncation ## ## Truncation interval ## ## A @math{1*2} numeric vector specifying the truncation interval for the ## probability distribution. First element contains the lower boundary, ## second element contains the upper boundary. This property is read-only. ## You can only truncate a probability distribution with the ## @qcode{truncate} method. ## ## @end deftp Truncation ## -*- texinfo -*- ## @deftp {prob.tLocationScaleDistribution} {property} IsTruncated ## ## Flag for truncated probability distribution ## ## A logical scalar value specifying whether a probability distribution is ## truncated or not. This property is read-only. ## ## @end deftp IsTruncated ## -*- texinfo -*- ## @deftp {prob.tLocationScaleDistribution} {property} ParameterCovariance ## ## Covariance matrix of the parameter estimates ## ## A @math{3*3} numeric matrix containing the variance-covariance of the ## parameter estimates. Diagonal elements contain the variance of each ## estimated parameter, and non-diagonal elements contain the covariance ## between the parameter estimates. The covariance matrix is only ## meaningful when the distribution was fitted to data. If the distribution ## object was created with fixed parameters, or a parameter of a fitted ## distribution is modified, then all elements of the variance-covariance ## are zero. This property is read-only. ## ## @end deftp ParameterCovariance ## -*- texinfo -*- ## @deftp {prob.tLocationScaleDistribution} {property} ParameterIsFixed ## ## Flag for fixed parameters ## ## A @math{1*3} logical vector specifying which parameters are fixed and ## which are estimated. @qcode{true} values correspond to fixed parameters, ## @qcode{false} values correspond to parameter estimates. This property is ## read-only. ## ## @end deftp ParameterIsFixed ## -*- texinfo -*- ## @deftp {prob.tLocationScaleDistribution} {property} InputData ## ## Data used for fitting a probability distribution ## ## A scalar structure containing the following fields: ## @itemize ## @item @qcode{data}: a numeric vector containing the data used for ## distribution fitting. ## @item @qcode{cens}: a numeric vector of logical values indicating ## censoring information corresponding to the elements of the data used for ## distribution fitting. If no censoring vector was used for distribution ## fitting, then this field defaults to an empty array. ## @item @qcode{freq}: a numeric vector of non-negative integer values ## containing the frequency information corresponding to the elements of the ## data used for distribution fitting. If no frequency vector was used for ## distribution fitting, then this field defaults to an empty array. ## @end itemize ## ## @end deftp InputData endproperties properties(GetAccess = public, SetAccess = protected, Hidden) ParameterCI endproperties methods(Hidden) ## -*- texinfo -*- ## @deftypefn {prob.tLocationScaleDistribution} {@var{pd} =} tLocationScaleDistribution (@var{mu}, @var{sigma}, @var{nu}) ## @deftypefnx {prob.tLocationScaleDistribution} {@var{pd} =} tLocationScaleDistribution () ## ## Create a @code{prob.tLocationScaleDistribution} object. ## ## @var{mu}, @var{sigma} and @var{nu} are the distribution parameters, which ## the class help describes. Called with no arguments the parameters take ## their defaults, @var{mu} 0, @var{sigma} 1 and @var{nu} 5. ## ## @code{makedist} is the usual way to create a distribution object. ## ## @end deftypefn function this = tLocationScaleDistribution (mu, sigma, nu) if (nargin == 0) mu = 0; sigma = 1; nu = 5; endif checkparams (mu, sigma, nu); this.InputData = []; this.IsTruncated = false; this.ParameterValues = [mu, sigma, nu]; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function display (this) fprintf ("%s =\n", inputname (1)); __disp__ (this, 't Location-Scale distribution'); endfunction function disp (this) __disp__ (this, 't Location-Scale distribution'); endfunction function this = set.mu (this, mu) checkparams (mu, this.sigma, this.nu); this.InputData = []; this.ParameterValues(1) = mu; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function mu = get.mu (this) mu = this.ParameterValues(1); endfunction function this = set.sigma (this, sigma) checkparams (this.mu, sigma, this.nu); this.InputData = []; this.ParameterValues(2) = sigma; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function sigma = get.sigma (this) sigma = this.ParameterValues(2); endfunction function this = set.nu (this, nu) checkparams (this.mu, this.sigma, nu); this.InputData = []; this.ParameterValues(3) = nu; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction function nu = get.nu (this) nu = this.ParameterValues(3); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {prob.tLocationScaleDistribution} {@var{p} =} cdf (@var{pd}, @var{x}) ## @deftypefnx {prob.tLocationScaleDistribution} {@var{p} =} cdf (@var{pd}, @var{x}, @qcode{'upper'}) ## ## Compute the cumulative distribution function (CDF). ## ## @code{@var{p} = cdf (@var{pd}, @var{x})} computes the CDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of ## the CDF of the probability distribution object, @var{pd}, evaluated at ## the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x, uflag) if (! isscalar (this)) error ("cdf: requires a scalar probability distribution."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("cdf: invalid argument for upper tail."); else utail = false; endif ## Do the computations p = tlscdf (x, this.mu, this.sigma, this.nu); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; p(lb) = 0; p(ub) = 1; p(! (lb | ub)) -= tlscdf (lx, this.mu, this.sigma, this.nu); p(! (lb | ub)) /= diff (tlscdf ([lx, ux], this.mu, this.sigma, this.nu)); endif ## Apply uflag if (utail) p = 1 - p; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.tLocationScaleDistribution} {@var{x} =} icdf (@var{pd}, @var{p}) ## ## Compute the inverse cumulative distribution function (iCDF). ## ## @code{@var{x} = icdf (@var{pd}, @var{p})} computes the quantile (the ## inverse of the CDF) of the probability distribution object, @var{pd}, ## evaluated at the values in @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (! isscalar (this)) error ("icdf: requires a scalar probability distribution."); endif if (this.IsTruncated) lp = tlscdf (this.Truncation(1), this.mu, this.sigma, this.nu); up = tlscdf (this.Truncation(2), this.mu, this.sigma, this.nu); ## Adjust p values within range of p @ lower limit and p @ upper limit is_nan = p < 0 | p > 1; p(is_nan) = NaN; np = lp + (up - lp) .* p; x = tlsinv (np, this.mu, this.sigma, this.nu); x(x < this.Truncation(1)) = this.Truncation(1); x(x > this.Truncation(2)) = this.Truncation(2); else x = tlsinv (p, this.mu, this.sigma, this.nu); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.tLocationScaleDistribution} {@var{r} =} iqr (@var{pd}) ## ## Compute the interquartile range of a probability distribution. ## ## @code{@var{r} = iqr (@var{pd})} computes the interquartile range of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function r = iqr (this) if (! isscalar (this)) error ("iqr: requires a scalar probability distribution."); endif r = diff (icdf (this, [0.25, 0.75])); endfunction ## -*- texinfo -*- ## @deftypefn {prob.tLocationScaleDistribution} {@var{m} =} mean (@var{pd}) ## ## Compute the mean of a probability distribution. ## ## @code{@var{m} = mean (@var{pd})} computes the mean of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = mean (this) if (! isscalar (this)) error ("mean: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); else m = tlsstat (this.mu, this.sigma, this.nu); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.tLocationScaleDistribution} {@var{m} =} median (@var{pd}) ## ## Compute the median of a probability distribution. ## ## @code{@var{m} = median (@var{pd})} computes the median of the probability ## distribution object, @var{pd}. ## ## @end deftypefn function m = median (this) if (! isscalar (this)) error ("median: requires a scalar probability distribution."); endif if (this.IsTruncated) lx = this.Truncation(1); ux = this.Truncation(2); Fa_b = tlscdf ([lx, ux], this.mu, this.sigma, this.nu); m = tlsinv (sum (Fa_b) / 2, this.mu, this.sigma, this.nu); else m = tlsstat (this.mu, this.sigma, this.nu); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.tLocationScaleDistribution} {@var{nlogL} =} negloglik (@var{pd}) ## ## Compute the negative loglikelihood of a probability distribution. ## ## @code{@var{nlogL} = negloglik (@var{pd})} computes the negative ## loglikelihood of the probability distribution object, @var{pd}. ## ## @end deftypefn function nlogL = negloglik (this) if (! isscalar (this)) error ("negloglik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) nlogL = []; return endif nlogL = tlslike ([this.mu, this.sigma, this.nu], this.InputData.data, ... this.InputData.cens, this.InputData.freq); endfunction ## -*- texinfo -*- ## @deftypefn {prob.tLocationScaleDistribution} {@var{ci} =} paramci (@var{pd}) ## @deftypefnx {prob.tLocationScaleDistribution} {@var{ci} =} paramci (@var{pd}, @var{Name}, @var{Value}) ## ## Compute the confidence intervals for probability distribution parameters. ## ## @code{@var{ci} = paramci (@var{pd})} computes the lower and upper ## boundaries of the 95% confidence interval for each parameter of the ## probability distribution object, @var{pd}. ## ## @code{@var{ci} = paramci (@var{pd}, @var{Name}, @var{Value})} computes ## the confidence intervals with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab A scalar value in the range @math{(0,1)} ## specifying the significance level for the confidence interval. The ## default value 0.05 corresponds to a 95% confidence interval. ## ## @item @qcode{'Parameter'} @tab A character vector or a cell array of ## character vectors specifying the parameter names for which to compute ## confidence intervals. By default, @code{paramci} computes confidence ## intervals for all distribution parameters. ## @end multitable ## ## @code{paramci} is meaningful only when @var{pd} is fitted to data, ## otherwise an empty array, @qcode{[]}, is returned. ## ## @end deftypefn function ci = paramci (this, varargin) if (! isscalar (this)) error ("paramci: requires a scalar probability distribution."); endif if (isempty (this.InputData)) ci = [this.ParameterValues; this.ParameterValues]; else ci = __paramci__ (this, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.tLocationScaleDistribution} {@var{y} =} pdf (@var{pd}, @var{x}) ## ## Compute the probability distribution function (PDF). ## ## @code{@var{y} = pdf (@var{pd}, @var{x})} computes the PDF of the ## probability distribution object, @var{pd}, evaluated at the values in ## @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (! isscalar (this)) error ("pdf: requires a scalar probability distribution."); endif y = tlspdf (x, this.mu, this.sigma, this.nu); if (this.IsTruncated) lx = this.Truncation(1); lb = x < lx; ux = this.Truncation(2); ub = x > ux; y(lb | ub) = 0; y(! (lb | ub)) /= diff (tlscdf ([lx, ux], this.mu, this.sigma, this.nu)); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.tLocationScaleDistribution} {} plot (@var{pd}) ## @deftypefnx {prob.tLocationScaleDistribution} {} plot (@var{pd}, @var{Name}, @var{Value}) ## @deftypefnx {prob.tLocationScaleDistribution} {@var{h} =} plot (@dots{}) ## ## Plot a probability distribution object. ## ## @code{plot (@var{pd})} plots a probability density function (PDF) of the ## probability distribution object @var{pd}. If @var{pd} contains data, ## which have been fitted by @code{fitdist}, the PDF is superimposed over a ## histogram of the data. ## ## @code{plot (@var{pd}, @var{Name}, @var{Value})} specifies additional ## options with the @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PlotType'} @tab A character vector specifying the plot ## type. @qcode{'pdf'} plots the probability density function (PDF). When ## @var{pd} is fit to data, the PDF is superimposed on a histogram of the ## data. @qcode{'cdf'} plots the cumulative density function (CDF). When ## @var{pd} is fit to data, the CDF is superimposed over an empirical CDF. ## @qcode{'probability'} plots a probability plot using a CDF of the data ## and a CDF of the fitted probability distribution. This option is ## available only when @var{pd} is fitted to data. ## ## @item @qcode{'Discrete'} @tab A logical scalar to specify whether to ## plot the PDF or CDF of a discrete distribution object as a line plot or a ## stem plot, by specifying @qcode{false} or @qcode{true}, respectively. By ## default, it is @qcode{true} for discrete distributions and @qcode{false} ## for continuous distributions. When @var{pd} is a continuous distribution ## object, option is ignored. ## ## @item @qcode{'Parent'} @tab An axes graphics object for plot. If ## not specified, the @code{plot} function plots into the current axes or ## creates a new axes object if one does not exist. ## @end multitable ## ## @code{@var{h} = plot (@dots{})} returns a graphics handle to the plotted ## objects. ## ## @end deftypefn function [varargout] = plot (this, varargin) if (! isscalar (this)) error ("plot: requires a scalar probability distribution."); endif h = __plot__ (this, false, varargin{:}); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.tLocationScaleDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}) ## @deftypefnx {prob.tLocationScaleDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.tLocationScaleDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}) ## @deftypefnx {prob.tLocationScaleDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}, @var{pnum}, @var{setparam}, @qcode{'Display'}, @var{display}) ## @deftypefnx {prob.tLocationScaleDistribution} {[@var{nlogL}, @var{param}] =} proflik (@var{pd}) ## @deftypefnx {prob.tLocationScaleDistribution} {[@var{nlogL}, @var{param}, @var{other}] =} proflik (@dots{}) ## ## Profile likelihood function for a probability distribution object. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum})} ## returns a vector @var{nlogL} of negative loglikelihood values and a ## vector @var{param} of corresponding parameter values for the parameter in ## the position indicated by @var{pnum}. By default, @code{proflik} uses ## the lower and upper bounds of the 98% confidence interval and computes ## 101 equispaced values for the selected parameter when it is the only one ## being estimated, and 21 values otherwise. @var{pd} must be fitted to ## data. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @qcode{'Display'}, @qcode{'on'})} also plots the profile likelihood ## against the default range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam})} defines a user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd}, @var{pnum}, ## @var{setparam}, @qcode{'Display'}, @qcode{'on'})} also plots the profile ## likelihood against the user-defined range of the selected parameter. ## ## @code{[@var{nlogL}, @var{param}] = proflik (@var{pd})} selects the ## first parameter that is not fixed. ## ## @code{[@var{nlogL}, @var{param}, @var{other}] = proflik (@dots{})} also ## returns a matrix @var{other} holding, in each row, the values of the ## remaining parameters that maximize the likelihood at the corresponding ## value of @var{param}. A fixed parameter keeps its own value. ## ## For the location-scale Student's T distribution, @qcode{@var{pnum} = 1} ## selects the parameter @qcode{mu}, @qcode{@var{pnum} = 2} selects the ## parameter @qcode{sigma}, and @qcode{@var{pnum} = 3} selects the ## parameter @qcode{nu}. ## ## When opted to display the profile likelihood plot, @code{proflik} also ## plots the baseline loglikelihood computed at the lower bound of the 95% ## confidence interval and estimated maximum likelihood. The latter might ## not be observable if it is outside of the used-defined range of parameter ## values. ## ## @end deftypefn function [varargout] = proflik (this, pnum, varargin) if (! isscalar (this)) error ("proflik: requires a scalar probability distribution."); endif if (isempty (this.InputData)) error ("proflik: no fitted data available."); endif if (nargin < 2) pnum = []; endif [varargout{1:nargout}] = __proflik__ (this, pnum, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {prob.tLocationScaleDistribution} {@var{r} =} random (@var{pd}) ## @deftypefnx {prob.tLocationScaleDistribution} {@var{r} =} random (@var{pd}, @var{rows}) ## @deftypefnx {prob.tLocationScaleDistribution} {@var{r} =} random (@var{pd}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {prob.tLocationScaleDistribution} {@var{r} =} random (@var{pd}, [@var{sz}]) ## ## Generate random arrays from the probability distribution object. ## ## @code{@var{r} = random (@var{pd})} returns a random number from the ## distribution object @var{pd}. ## ## When called with a single size argument, @code{tlsrnd} returns a square ## matrix with the dimension specified. When called with more than one ## scalar argument, the first two arguments are taken as the number of rows ## and columns and any further arguments specify additional matrix ## dimensions. The size may also be specified with a row vector of ## dimensions, @var{sz}. ## ## @end deftypefn function r = random (this, varargin) if (! isscalar (this)) error ("random: requires a scalar probability distribution."); endif if (this.IsTruncated) sz = [varargin{:}]; ps = prod (sz); ## Get an estimate of how many more random numbers we need to randomly ## pick the appropriate size from lx = this.Truncation(1); ux = this.Truncation(2); ratio = 1 / diff (tlscdf ([lx, ux], this.mu, this.sigma, this.nu)); nsize = fix (2 * ratio * ps); # times 2 to be on the safe side ## Generate the numbers and remove out-of-bound random samples r = tlsrnd (this.mu, this.sigma, this.nu, nsize, 1); r(r < lx | r > ux) = []; ## Randomly select the required size and reshape to requested dimensions idx = randperm (numel (r), ps); r = reshape (r(idx), sz); else r = tlsrnd (this.mu, this.sigma, this.nu, varargin{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {prob.tLocationScaleDistribution} {@var{s} =} std (@var{pd}) ## ## Compute the standard deviation of a probability distribution. ## ## @code{@var{s} = std (@var{pd})} computes the standard deviation of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function s = std (this) if (! isscalar (this)) error ("std: requires a scalar probability distribution."); endif v = var (this); s = sqrt (v); endfunction ## -*- texinfo -*- ## @deftypefn {prob.tLocationScaleDistribution} {@var{t} =} truncate (@var{pd}, @var{lower}, @var{upper}) ## ## Truncate a probability distribution. ## ## @code{@var{t} = truncate (@var{pd}, @var{lower}, @var{upper})} returns a ## probability distribution @var{t}, which is the probability distribution ## @var{pd} truncated to the specified interval with lower limit, ## @var{lower}, and upper limit, @var{upper}. If @var{pd} is fitted to data ## with @code{fitdist}, the returned probability distribution @var{t} is not ## fitted, does not contain any data or estimated values, and it is as it ## has been created with the @var{makedist} function, but it includes the ## truncation interval. ## ## @end deftypefn function this = truncate (this, lower, upper) if (! isscalar (this)) error ("truncate: requires a scalar probability distribution."); endif if (nargin < 3) error ("truncate: missing input argument."); elseif (lower >= upper) error ("truncate: invalid lower upper limits."); endif this.Truncation = [lower, upper]; this.IsTruncated = true; this.InputData = []; this.ParameterIsFixed = [true, true, true]; this.ParameterCovariance = zeros (this.NumParameters); endfunction ## -*- texinfo -*- ## @deftypefn {prob.tLocationScaleDistribution} {@var{v} =} var (@var{pd}) ## ## Compute the variance of a probability distribution. ## ## @code{@var{v} = var (@var{pd})} computes the variance of the ## probability distribution object, @var{pd}. ## ## @end deftypefn function v = var (this) if (! isscalar (this)) error ("var: requires a scalar probability distribution."); endif if (this.IsTruncated) fm = @(x) x .* pdf (this, x); m = integral (fm, this.Truncation(1), this.Truncation(2)); fv = @(x) ((x - m) .^ 2) .* pdf (this, x); v = integral (fv, this.Truncation(1), this.Truncation(2)); else [~, v] = tlsstat (this.mu, this.sigma, this.nu); endif endfunction endmethods methods(Static, Hidden) function pd = fit (x, varargin) ## Check input arguments if (nargin < 2) alpha = 0.05; else alpha = varargin{1}; endif if (nargin < 3) censor = []; else censor = varargin{2}; endif if (nargin < 4) freq = []; else freq = varargin{3}; endif if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else options = varargin{4}; endif ## Fit data [phat, pci] = tlsfit (x, alpha, censor, freq, options); [~, acov] = tlslike (phat, x, censor, freq); ## Create fitted distribution object pd = prob.tLocationScaleDistribution.makeFitted ... (phat, pci, acov, x, censor, freq); endfunction function pd = makeFitted (phat, pci, acov, x, censor, freq) mu = phat(1); sigma = phat(2); nu = phat(3); pd = prob.tLocationScaleDistribution (mu, sigma, nu); pd.ParameterCI = pci; pd.ParameterIsFixed = [false, false, false]; pd.ParameterCovariance = acov; pd.InputData = struct ('data', x, 'cens', censor, 'freq', freq); endfunction endmethods endclassdef function checkparams (mu, sigma, nu) if (! (isscalar (mu) && isnumeric (mu) && isreal (mu) && isfinite (mu))) error ("tLocationScaleDistribution: MU must be a real scalar.") endif if (! (isscalar (sigma) && isnumeric (sigma) && isreal (sigma) && isfinite (sigma) && sigma > 0)) error ("tLocationScaleDistribution: SIGMA must be a positive real scalar.") endif if (! (isscalar (nu) && isnumeric (nu) && isreal (nu) && isfinite (nu) && nu > 0)) error ("tLocationScaleDistribution: NU must be a positive real scalar.") endif endfunction %!demo %! ## Generate a data set of 5000 random samples from a t Location-Scale distribution %! ## with parameters mu = 0, sigma = 1, and nu = 5. Fit a t Location-Scale %! ## distribution to this data and plot a PDF of the fitted distribution %! ## superimposed on a histogram of the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ('tLocationScale', 'mu', 0, 'sigma', 1, 'nu', 5); %! data = random (pd_fixed, 5000, 1); %! pd_fitted = fitdist (data, 'tLocationScale'); %! plot (pd_fitted); %! msg = 'Fitted t Location-Scale distribution with mu = %0.2f, sigma = %0.2f, nu = %0.2f'; %! title (sprintf (msg, pd_fitted.mu, pd_fitted.sigma, pd_fitted.nu)); ## Test output %!shared pd, t %! pd = prob.tLocationScaleDistribution; %! t = truncate (pd, 2, 4); %!assert_equal (cdf (pd, [0:5]), [0.5, 0.8184, 0.9490, 0.9850, 0.9948, 0.9979], 1e-4); %!assert_equal (cdf (t, [0:5]), [0, 0, 0, 0.7841, 1, 1], 1e-4); %!assert_equal (cdf (pd, [1.5, 2, 3, 4, NaN]), [0.9030, 0.9490, 0.9850, 0.9948, NaN], 1e-4); %!assert_equal (cdf (t, [1.5, 2, 3, 4, NaN]), [0, 0, 0.7841, 1, NaN], 1e-4); %!assert_equal (icdf (pd, [0:0.2:1]), [-Inf, -0.9195, -0.2672, 0.2672, 0.9195, Inf], 1e-4); %!assert_equal (icdf (t, [0:0.2:1]), [2, 2.1559, 2.3533, 2.6223, 3.0432, 4], 1e-4); %!assert_equal (icdf (pd, [-1, 0.4:0.2:1, NaN]), [NaN, -0.2672, 0.2672, 0.9195, Inf, NaN], 1e-4); %!assert_equal (icdf (t, [-1, 0.4:0.2:1, NaN]), [NaN, 2.3533, 2.6223, 3.0432, 4, NaN], 1e-4); %!assert_equal (iqr (pd), 1.4534, 1e-4); %!assert_equal (iqr (t), 0.7139, 1e-4); %!assert_equal (mean (pd), 0, eps); %!assert_equal (mean (t), 2.6099, 1e-4); %!assert_equal (median (pd), 0, eps); %!assert_equal (median (t), 2.4758, 1e-4); %!assert_equal (pdf (pd, [0:5]), [0.3796, 0.2197, 0.0651, 0.0173, 0.0051, 0.0018], 1e-4); %!assert_equal (pdf (t, [0:5]), [0, 0, 1.4209, 0.3775, 0.1119, 0], 1e-4); %!assert_equal (pdf (pd, [-1, 1.5, NaN]), [0.2197, 0.1245, NaN], 1e-4); %!assert_equal (pdf (t, [-1, 1.5, NaN]), [0, 0, NaN], 1e-4); %!assert_equal (isequal (size (random (pd, 100, 50)), [100, 50]), true) %!assert_equal (any (random (t, 1000, 1) < 2), false); %!assert_equal (any (random (t, 1000, 1) > 4), false); %!assert_equal (std (pd), 1.2910, 1e-4); %!assert_equal (std (t), 0.4989, 1e-4); %!assert_equal (var (pd), 1.6667, 1e-4); %!assert_equal (var (t), 0.2489, 1e-4); %!test %! ## The profile over the first free parameter: 21 grid values, one row of %! ## OTHER per value, and the likelihood peaking at the fitted estimate. %! x = [0.3; -1.2; 0.8; 1.5; -0.4; 0.2; -0.9; 1.1; 0.6; -0.3; ... %! 1.8; -1.5; 0.4; 0.9; -0.7; 1.2; -0.2; 0.5; -1.1; 0.7]; %! pd = fitdist (x, 'tLocationScale'); %! [nlogL, param, other] = proflik (pd, 1); %! assert_equal (size (param), [1, 21]); %! assert_equal (size (other), [21, 2]); %! assert_equal (proflik (pd), nlogL); %! [~, imax] = max (nlogL); %! assert_equal (abs (param(imax) - pd.ParameterValues(1)) <= param(2) - param(1), true); ## Test input validation ## 'prob.tLocationScaleDistribution' constructor %!error ... %! prob.tLocationScaleDistribution (i, 1, 1) %!error ... %! prob.tLocationScaleDistribution (Inf, 1, 1) %!error ... %! prob.tLocationScaleDistribution ([1, 2], 1, 1) %!error ... %! prob.tLocationScaleDistribution ('a', 1, 1) %!error ... %! prob.tLocationScaleDistribution (NaN, 1, 1) %!error ... %! prob.tLocationScaleDistribution (0, 0, 1) %!error ... %! prob.tLocationScaleDistribution (0, -1, 1) %!error ... %! prob.tLocationScaleDistribution (0, Inf, 1) %!error ... %! prob.tLocationScaleDistribution (0, i, 1) %!error ... %! prob.tLocationScaleDistribution (0, 'a', 1) %!error ... %! prob.tLocationScaleDistribution (0, [1, 2], 1) %!error ... %! prob.tLocationScaleDistribution (0, NaN, 1) %!error ... %! prob.tLocationScaleDistribution (0, 1, 0) %!error ... %! prob.tLocationScaleDistribution (0, 1, -1) %!error ... %! prob.tLocationScaleDistribution (0, 1, Inf) %!error ... %! prob.tLocationScaleDistribution (0, 1, i) %!error ... %! prob.tLocationScaleDistribution (0, 1, 'a') %!error ... %! prob.tLocationScaleDistribution (0, 1, [1, 2]) %!error ... %! prob.tLocationScaleDistribution (0, 1, NaN) ## 'cdf' method %!error ... %! cdf (prob.tLocationScaleDistribution, 2, 'uper') %!error ... %! cdf (prob.tLocationScaleDistribution, 2, 3) ## 'paramci' method %!shared x %! x = tlsrnd (0, 1, 1, [1, 100]); %!error ... %! paramci (prob.tLocationScaleDistribution.fit (x), 'alpha') %!error ... %! paramci (prob.tLocationScaleDistribution.fit (x), 'alpha', 0) %!error ... %! paramci (prob.tLocationScaleDistribution.fit (x), 'alpha', 1) %!error ... %! paramci (prob.tLocationScaleDistribution.fit (x), 'alpha', [0.5 2]) %!error ... %! paramci (prob.tLocationScaleDistribution.fit (x), 'alpha', '') %!error ... %! paramci (prob.tLocationScaleDistribution.fit (x), 'alpha', {0.05}) %!error ... %! paramci (prob.tLocationScaleDistribution.fit (x), 'parameter', 'mu', ... %! 'alpha', {0.05}) %!error ... %! paramci (prob.tLocationScaleDistribution.fit (x), ... %! 'parameter', {'mu', 'sigma', 'nu', 'param'}) %!error ... %! paramci (prob.tLocationScaleDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', {'mu', 'sigma', 'nu', 'param'}) %!error ... %! paramci (prob.tLocationScaleDistribution.fit (x), 'parameter', 'param') %!error ... %! paramci (prob.tLocationScaleDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', 'param') %!error ... %! paramci (prob.tLocationScaleDistribution.fit (x), 'NAME', 'value') %!error ... %! paramci (prob.tLocationScaleDistribution.fit (x), 'alpha', 0.01, 'NAME', 'value') %!error ... %! paramci (prob.tLocationScaleDistribution.fit (x), 'alpha', 0.01, ... %! 'parameter', 'mu', 'NAME', 'value') ## 'plot' method %!error ... %! plot (prob.tLocationScaleDistribution, 'Parent') %!error ... %! plot (prob.tLocationScaleDistribution, 'PlotType', 12) %!error ... %! plot (prob.tLocationScaleDistribution, 'PlotType', {'pdf', 'cdf'}) %!error ... %! plot (prob.tLocationScaleDistribution, 'PlotType', 'pdfcdf') %!error ... %! plot (prob.tLocationScaleDistribution, 'Discrete', 'pdfcdf') %!error ... %! plot (prob.tLocationScaleDistribution, 'Discrete', [1, 0]) %!error ... %! plot (prob.tLocationScaleDistribution, 'Discrete', {true}) %!error ... %! plot (prob.tLocationScaleDistribution, 'Parent', 12) %!error ... %! plot (prob.tLocationScaleDistribution, 'Parent', 'hax') %!error ... %! plot (prob.tLocationScaleDistribution, 'invalidNAME', 'pdf') %!error ... %! plot (prob.tLocationScaleDistribution, 'PlotType', 'probability') ## 'proflik' method %!error ... %! proflik (prob.tLocationScaleDistribution, 2) %!error ... %! proflik (prob.tLocationScaleDistribution.fit (x), 4) %!error ... %! proflik (prob.tLocationScaleDistribution.fit (x), [1, 2]) %!error ... %! proflik (prob.tLocationScaleDistribution.fit (x), {1}) %!error ... %! proflik (prob.tLocationScaleDistribution.fit (x), 1, ones (2)) %!error ... %! proflik (prob.tLocationScaleDistribution.fit (x), 1, 'Display') %!error ... %! proflik (prob.tLocationScaleDistribution.fit (x), 1, 'Display', 1) %!error ... %! proflik (prob.tLocationScaleDistribution.fit (x), 1, 'Display', {1}) %!error ... %! proflik (prob.tLocationScaleDistribution.fit (x), 1, 'Display', {'on'}) %!error ... %! proflik (prob.tLocationScaleDistribution.fit (x), 1, 'Display', ['on'; 'on']) %!error ... %! proflik (prob.tLocationScaleDistribution.fit (x), 1, 'Display', 'onnn') %!error ... %! proflik (prob.tLocationScaleDistribution.fit (x), 1, 'NAME', 'on') %!error ... %! proflik (prob.tLocationScaleDistribution.fit (x), 1, {'NAME'}, 'on') %!error ... %! proflik (prob.tLocationScaleDistribution.fit (x), 1, {[1 2 3 4]}, 'Display', 'on') ## 'truncate' method %!error ... %! truncate (prob.tLocationScaleDistribution) %!error ... %! truncate (prob.tLocationScaleDistribution, 2) %!error ... %! truncate (prob.tLocationScaleDistribution, 4, 2) ## Catch errors when using array of probability objects with available methods %!shared pd %! pd = prob.tLocationScaleDistribution (0, 1, 1); %! pd(2) = prob.tLocationScaleDistribution (0, 1, 3); %!error cdf (pd, 1) %!error icdf (pd, 0.5) %!error iqr (pd) %!error mean (pd) %!error median (pd) %!error negloglik (pd) %!error paramci (pd) %!error pdf (pd, 1) %!error plot (pd) %!error proflik (pd, 2) %!error random (pd) %!error std (pd) %!error ... %! truncate (pd, 2, 4) %!error var (pd) statistics-release-2.1.2/inst/Distribution_Classes/doc-cache000066400000000000000000021727521527311104300242030ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 792 # name: # type: sq_string # elements: 1 # length: 11 paretotails # name: # type: sq_string # elements: 1 # length: 1259 statistics: paretotails Piecewise distribution with generalized Pareto tails. A paretotails object is a piecewise probability distribution fit to sample data. A generalized Pareto distribution (GPD) is fit to each tail of the data, below a lower quantile and above an upper quantile, while the middle of the distribution is described by the empirical cumulative distribution function of the data. This gives a smooth model for the tails, useful for extreme value analysis, together with a nonparametric description of the central region. Create a paretotails object with the constructor pt = paretotails (x, pl, pu), where x is the sample data and pl and pu are the cumulative probabilities at the lower and upper tail boundaries. Data at or below the pl quantile form the lower tail, data at or above the pu quantile form the upper tail, and the rest form the middle segment. Query the fitted object with the methods cdf, pdf, icdf, random, boundary, nsegments, segment, lowerparams, and upperparams. Note: the kernel-smoothed middle option of MATLAB (paretotails (x, pl, pu, "kernel")) is not yet supported; only the default empirical ("ecdf") middle is available. See also: gpfit, gpcdf, gppdf, gpinv, ecdf, fitdist, prob.GeneralizedParetoDistribution # name: # type: sq_string # elements: 1 # length: 53 Piecewise distribution with generalized Pareto tails. # name: # type: sq_string # elements: 1 # length: 25 paretotails.NumParameters # name: # type: sq_string # elements: 1 # length: 108 paretotails: property NumParameters Number of estimated parameters (two per fitted generalized Pareto tail). # name: # type: sq_string # elements: 1 # length: 72 Number of estimated parameters (two per fitted generalized Pareto tail). # name: # type: sq_string # elements: 1 # length: 23 paretotails.NumSegments # name: # type: sq_string # elements: 1 # length: 138 paretotails: property NumSegments Number of segments in the piecewise distribution (a lower tail, a middle, and an upper tail give three). # name: # type: sq_string # elements: 1 # length: 104 Number of segments in the piecewise distribution (a lower tail, a middle, and an upper tail give three). # name: # type: sq_string # elements: 1 # length: 20 paretotails.boundary # name: # type: sq_string # elements: 1 # length: 141 paretotails: [p, q] = boundary (pt) Boundary probabilities p and quantiles q of the segments of the paretotails object pt, as column vectors. # name: # type: sq_string # elements: 1 # length: 105 Boundary probabilities p and quantiles q of the segments of the paretotails object pt, as column vectors. # name: # type: sq_string # elements: 1 # length: 15 paretotails.cdf # name: # type: sq_string # elements: 1 # length: 120 paretotails: p = cdf (pt, x) Cumulative distribution function of the paretotails object pt evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 91 Cumulative distribution function of the paretotails object pt evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 16 paretotails.icdf # name: # type: sq_string # elements: 1 # length: 153 paretotails: x = icdf (pt, p) Inverse cumulative distribution function (quantile function) of the paretotails object pt evaluated at the probabilities p. # name: # type: sq_string # elements: 1 # length: 123 Inverse cumulative distribution function (quantile function) of the paretotails object pt evaluated at the probabilities p. # name: # type: sq_string # elements: 1 # length: 23 paretotails.lowerparams # name: # type: sq_string # elements: 1 # length: 144 paretotails: params = lowerparams (pt) Shape and scale parameters [k, sigma] of the generalized Pareto distribution fit to the lower tail of pt. # name: # type: sq_string # elements: 1 # length: 105 Shape and scale parameters [k, sigma] of the generalized Pareto distribution fit to the lower tail of pt. # name: # type: sq_string # elements: 1 # length: 21 paretotails.nsegments # name: # type: sq_string # elements: 1 # length: 80 paretotails: n = nsegments (pt) Number of segments in the paretotails object pt. # name: # type: sq_string # elements: 1 # length: 48 Number of segments in the paretotails object pt. # name: # type: sq_string # elements: 1 # length: 23 paretotails.paretotails # name: # type: sq_string # elements: 1 # length: 539 paretotails: pt = paretotails (x, pl, pu) paretotails: pt = paretotails (x, pl, pu, cdffun) Fit a piecewise distribution with generalized Pareto tails to x. pl and pu are the cumulative probabilities of the lower and upper tail boundaries, with 0 <= pl < pu <= 1. A generalized Pareto distribution is fit by maximum likelihood to the exceedances in each tail; the middle segment uses the empirical cumulative distribution of x. cdffun selects the middle segment and defaults to "ecdf"; the "kernel" option of MATLAB is not yet supported. # name: # type: sq_string # elements: 1 # length: 64 Fit a piecewise distribution with generalized Pareto tails to x. # name: # type: sq_string # elements: 1 # length: 15 paretotails.pdf # name: # type: sq_string # elements: 1 # length: 116 paretotails: y = pdf (pt, x) Probability density function of the paretotails object pt evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 87 Probability density function of the paretotails object pt evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 18 paretotails.random # name: # type: sq_string # elements: 1 # length: 216 paretotails: r = random (pt) paretotails: r = random (pt, sz) paretotails: r = random (pt, m, n, ...) Random values drawn from the paretotails object pt, by inverse transform sampling. The size arguments follow rand. # name: # type: sq_string # elements: 1 # length: 82 Random values drawn from the paretotails object pt, by inverse transform sampling. # name: # type: sq_string # elements: 1 # length: 19 paretotails.segment # name: # type: sq_string # elements: 1 # length: 243 paretotails: s = segment (pt, x, p) Segment indices for the paretotails object pt. Supply the data values in x (with p empty) or the cumulative probabilities in p (with x empty). Segment 1 is the lower tail, 2 the middle, and 3 the upper tail. # name: # type: sq_string # elements: 1 # length: 46 Segment indices for the paretotails object pt. # name: # type: sq_string # elements: 1 # length: 23 paretotails.upperparams # name: # type: sq_string # elements: 1 # length: 144 paretotails: params = upperparams (pt) Shape and scale parameters [k, sigma] of the generalized Pareto distribution fit to the upper tail of pt. # name: # type: sq_string # elements: 1 # length: 105 Shape and scale parameters [k, sigma] of the generalized Pareto distribution fit to the upper tail of pt. # name: # type: sq_string # elements: 1 # length: 21 prob.BetaDistribution # name: # type: sq_string # elements: 1 # length: 1373 statistics: prob.BetaDistribution Beta probability distribution object. A prob.BetaDistribution object consists of parameters, a model description, and sample data for a beta probability distribution. The beta distribution is a family of continuous probability distributions defined on the interval [0, 1] in terms of two positive parameters, denoted by alpha (a) and beta (b), that appear as exponents of the variable and its complement to 1, respectively, and control the shape of the distribution. There are several ways to create a prob.BetaDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.BetaDistribution (a, b) to create a beta distribution with fixed parameter values a and b. Use the static method prob.BetaDistribution.fit (x, alpha, freq, options) to fit a distribution to the data in x using the same input arguments as the betafit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the beta distribution can be found at https://en.wikipedia.org/wiki/Beta_distribution See also: fitdist, makedist, betacdf, betainv, betapdf, betarnd, betafit, betalike, betastat # name: # type: sq_string # elements: 1 # length: 37 Beta probability distribution object. # name: # type: sq_string # elements: 1 # length: 38 prob.BetaDistribution.BetaDistribution # name: # type: sq_string # elements: 1 # length: 346 prob.BetaDistribution: pd = BetaDistribution (a, b) prob.BetaDistribution: pd = BetaDistribution () Create a prob.BetaDistribution object. a and b are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, a 1 and b 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 38 Create a prob.BetaDistribution object. # name: # type: sq_string # elements: 1 # length: 38 prob.BetaDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 186 prob.BetaDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 31 prob.BetaDistribution.InputData # name: # type: sq_string # elements: 1 # length: 568 prob.BetaDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data : a numeric vector containing the data used for distribution fitting. cens : an empty array, since prob.BetaDistribution does not allow censoring. frequency : a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 33 prob.BetaDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 206 prob.BetaDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 35 prob.BetaDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 199 prob.BetaDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 41 prob.BetaDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 611 prob.BetaDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 2x2 numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 42 prob.BetaDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 223 prob.BetaDistribution: property ParameterDescription Description of parameters A 2x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 38 prob.BetaDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 279 prob.BetaDistribution: property ParameterIsFixed Flag for fixed parameters A 1x2 logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 36 prob.BetaDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 200 prob.BetaDistribution: property ParameterNames Names of parameters A 2x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 37 prob.BetaDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 275 prob.BetaDistribution: property ParameterValues Distribution parameter values A 2x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the a and b properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 32 prob.BetaDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 343 prob.BetaDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 23 prob.BetaDistribution.a # name: # type: sq_string # elements: 1 # length: 188 prob.BetaDistribution: property a First shape parameter A positive scalar value characterizing the shape of the beta distribution. You can access the a property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 21 First shape parameter # name: # type: sq_string # elements: 1 # length: 23 prob.BetaDistribution.b # name: # type: sq_string # elements: 1 # length: 189 prob.BetaDistribution: property b Second shape parameter A positive scalar value characterizing the shape of the beta distribution. You can access the b property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 22 Second shape parameter # name: # type: sq_string # elements: 1 # length: 25 prob.BetaDistribution.cdf # name: # type: sq_string # elements: 1 # length: 378 prob.BetaDistribution: p = cdf (pd, x) prob.BetaDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 26 prob.BetaDistribution.icdf # name: # type: sq_string # elements: 1 # length: 239 prob.BetaDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 25 prob.BetaDistribution.iqr # name: # type: sq_string # elements: 1 # length: 189 prob.BetaDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 26 prob.BetaDistribution.mean # name: # type: sq_string # elements: 1 # length: 161 prob.BetaDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 28 prob.BetaDistribution.median # name: # type: sq_string # elements: 1 # length: 169 prob.BetaDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.BetaDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 215 prob.BetaDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 29 prob.BetaDistribution.paramci # name: # type: sq_string # elements: 1 # length: 969 prob.BetaDistribution: ci = paramci (pd) prob.BetaDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 25 prob.BetaDistribution.pdf # name: # type: sq_string # elements: 1 # length: 199 prob.BetaDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 26 prob.BetaDistribution.plot # name: # type: sq_string # elements: 1 # length: 1510 prob.BetaDistribution: plot (pd) prob.BetaDistribution: plot (pd, Name, Value) prob.BetaDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 29 prob.BetaDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2013 prob.BetaDistribution: [nlogL, param] = proflik (pd, pnum) prob.BetaDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.BetaDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.BetaDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.BetaDistribution: [nlogL, param] = proflik (pd) prob.BetaDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the beta distribution, pnum = 1 selects the parameter a and pnum = 2 selects the parameter b. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 28 prob.BetaDistribution.random # name: # type: sq_string # elements: 1 # length: 675 prob.BetaDistribution: r = random (pd) prob.BetaDistribution: r = random (pd, rows) prob.BetaDistribution: r = random (pd, rows, cols, ...) prob.BetaDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, betarnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 25 prob.BetaDistribution.std # name: # type: sq_string # elements: 1 # length: 187 prob.BetaDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 30 prob.BetaDistribution.truncate # name: # type: sq_string # elements: 1 # length: 530 prob.BetaDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 25 prob.BetaDistribution.var # name: # type: sq_string # elements: 1 # length: 167 prob.BetaDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 25 prob.BinomialDistribution # name: # type: sq_string # elements: 1 # length: 1285 statistics: prob.BinomialDistribution Binomial probability distribution object. A prob.BinomialDistribution object consists of parameters, a model description, and sample data for a binomial probability distribution. The binomial distribution is a discrete probability distribution that models the number of successes in a sequence of N independent trials, each with a probability of success p. There are several ways to create a prob.BinomialDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.BinomialDistribution (N, p) to create a binomial distribution with fixed parameter values N and p. Use the static method prob.BinomialDistribution.fit (x, ntrials, alpha) to fit a distribution to the data in x using the same input arguments as the binofit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the binomial distribution can be found at https://en.wikipedia.org/wiki/Binomial_distribution See also: fitdist, makedist, binocdf, binoinv, binopdf, binornd, binofit, binolike, binostat # name: # type: sq_string # elements: 1 # length: 41 Binomial probability distribution object. # name: # type: sq_string # elements: 1 # length: 46 prob.BinomialDistribution.BinomialDistribution # name: # type: sq_string # elements: 1 # length: 368 prob.BinomialDistribution: pd = BinomialDistribution (N, p) prob.BinomialDistribution: pd = BinomialDistribution () Create a prob.BinomialDistribution object. N and p are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, N 1 and p 0.5. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 42 Create a prob.BinomialDistribution object. # name: # type: sq_string # elements: 1 # length: 42 prob.BinomialDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 190 prob.BinomialDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 35 prob.BinomialDistribution.InputData # name: # type: sq_string # elements: 1 # length: 576 prob.BinomialDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data : a numeric vector containing the data used for distribution fitting. cens : an empty array, since prob.BinomialDistribution does not allow censoring. frequency : a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 37 prob.BinomialDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 210 prob.BinomialDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 27 prob.BinomialDistribution.N # name: # type: sq_string # elements: 1 # length: 203 prob.BinomialDistribution: property N Number of trials A positive integer value characterizing the number of trials in the binomial distribution. You can access the N property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 16 Number of trials # name: # type: sq_string # elements: 1 # length: 39 prob.BinomialDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 203 prob.BinomialDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 45 prob.BinomialDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 615 prob.BinomialDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 2x2 numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 46 prob.BinomialDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 227 prob.BinomialDistribution: property ParameterDescription Description of parameters A 2x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 42 prob.BinomialDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 283 prob.BinomialDistribution: property ParameterIsFixed Flag for fixed parameters A 1x2 logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 40 prob.BinomialDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 204 prob.BinomialDistribution: property ParameterNames Names of parameters A 2x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 41 prob.BinomialDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 279 prob.BinomialDistribution: property ParameterValues Distribution parameter values A 2x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the N and p properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 36 prob.BinomialDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 347 prob.BinomialDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 29 prob.BinomialDistribution.cdf # name: # type: sq_string # elements: 1 # length: 386 prob.BinomialDistribution: p = cdf (pd, x) prob.BinomialDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 30 prob.BinomialDistribution.icdf # name: # type: sq_string # elements: 1 # length: 243 prob.BinomialDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 29 prob.BinomialDistribution.iqr # name: # type: sq_string # elements: 1 # length: 193 prob.BinomialDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 30 prob.BinomialDistribution.mean # name: # type: sq_string # elements: 1 # length: 165 prob.BinomialDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.BinomialDistribution.median # name: # type: sq_string # elements: 1 # length: 173 prob.BinomialDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 35 prob.BinomialDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 219 prob.BinomialDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 27 prob.BinomialDistribution.p # name: # type: sq_string # elements: 1 # length: 239 prob.BinomialDistribution: property p Probability of success A scalar value in the range [0, 1] characterizing the probability of success in each trial of the binomial distribution. You can access the p property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 22 Probability of success # name: # type: sq_string # elements: 1 # length: 33 prob.BinomialDistribution.paramci # name: # type: sq_string # elements: 1 # length: 977 prob.BinomialDistribution: ci = paramci (pd) prob.BinomialDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 29 prob.BinomialDistribution.pdf # name: # type: sq_string # elements: 1 # length: 203 prob.BinomialDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 30 prob.BinomialDistribution.plot # name: # type: sq_string # elements: 1 # length: 1522 prob.BinomialDistribution: plot (pd) prob.BinomialDistribution: plot (pd, Name, Value) prob.BinomialDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 33 prob.BinomialDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2041 prob.BinomialDistribution: [nlogL, param] = proflik (pd, pnum) prob.BinomialDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.BinomialDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.BinomialDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.BinomialDistribution: [nlogL, param] = proflik (pd) prob.BinomialDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the binomial distribution, pnum = 1 selects the parameter N and pnum = 2 selects the parameter p. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 32 prob.BinomialDistribution.random # name: # type: sq_string # elements: 1 # length: 691 prob.BinomialDistribution: r = random (pd) prob.BinomialDistribution: r = random (pd, rows) prob.BinomialDistribution: r = random (pd, rows, cols, ...) prob.BinomialDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, binornd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 29 prob.BinomialDistribution.std # name: # type: sq_string # elements: 1 # length: 191 prob.BinomialDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 34 prob.BinomialDistribution.truncate # name: # type: sq_string # elements: 1 # length: 534 prob.BinomialDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 29 prob.BinomialDistribution.var # name: # type: sq_string # elements: 1 # length: 171 prob.BinomialDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 33 prob.BirnbaumSaundersDistribution # name: # type: sq_string # elements: 1 # length: 1443 statistics: prob.BirnbaumSaundersDistribution Birnbaum-Saunders probability distribution object. A prob.BirnbaumSaundersDistribution object consists of parameters, a model description, and sample data for a Birnbaum-Saunders probability distribution. The Birnbaum-Saunders distribution is a continuous probability distribution that models the time to failure of materials subjected to cyclic loading. It is defined by scale parameter beta and shape parameter gamma. There are several ways to create a prob.BirnbaumSaundersDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.BirnbaumSaundersDistribution (beta, gamma) to create a Birnbaum-Saunders distribution with fixed parameter values beta and gamma. Use the static method prob.BirnbaumSaundersDistribution.fit (x, alpha, censor, freq, options) to fit a distribution to the data in x using the same input arguments as the bisafit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the Birnbaum-Saunders distribution can be found at https://en.wikipedia.org/wiki/Birnbaum%E2%80%93Saunders_distribution See also: fitdist, makedist, bisacdf, bisainv, bisapdf, bisarnd, bisafit, bisalike, bisastat # name: # type: sq_string # elements: 1 # length: 50 Birnbaum-Saunders probability distribution object. # name: # type: sq_string # elements: 1 # length: 62 prob.BirnbaumSaundersDistribution.BirnbaumSaundersDistribution # name: # type: sq_string # elements: 1 # length: 427 prob.BirnbaumSaundersDistribution: pd = BirnbaumSaundersDistribution (beta, gamma) prob.BirnbaumSaundersDistribution: pd = BirnbaumSaundersDistribution () Create a prob.BirnbaumSaundersDistribution object. beta and gamma are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, beta 1 and gamma 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 50 Create a prob.BirnbaumSaundersDistribution object. # name: # type: sq_string # elements: 1 # length: 50 prob.BirnbaumSaundersDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 198 prob.BirnbaumSaundersDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 43 prob.BirnbaumSaundersDistribution.InputData # name: # type: sq_string # elements: 1 # length: 745 prob.BirnbaumSaundersDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: a numeric vector of logical values indicating censoring information corresponding to the elements of the data used for distribution fitting. If no censoring vector was used for distribution fitting, then this field defaults to an empty array. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 45 prob.BirnbaumSaundersDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 218 prob.BirnbaumSaundersDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 47 prob.BirnbaumSaundersDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 211 prob.BirnbaumSaundersDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 53 prob.BirnbaumSaundersDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 624 prob.BirnbaumSaundersDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 2x2 numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter, and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 54 prob.BirnbaumSaundersDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 235 prob.BirnbaumSaundersDistribution: property ParameterDescription Description of parameters A 2x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 50 prob.BirnbaumSaundersDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 291 prob.BirnbaumSaundersDistribution: property ParameterIsFixed Flag for fixed parameters A 1x2 logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 48 prob.BirnbaumSaundersDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 212 prob.BirnbaumSaundersDistribution: property ParameterNames Names of parameters A 2x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 49 prob.BirnbaumSaundersDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 294 prob.BirnbaumSaundersDistribution: property ParameterValues Distribution parameter values A 2x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the beta and gamma properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 44 prob.BirnbaumSaundersDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 355 prob.BirnbaumSaundersDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 38 prob.BirnbaumSaundersDistribution.beta # name: # type: sq_string # elements: 1 # length: 213 prob.BirnbaumSaundersDistribution: property beta Scale parameter A positive scalar value characterizing the scale of the Birnbaum-Saunders distribution. You can access the beta property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Scale parameter # name: # type: sq_string # elements: 1 # length: 37 prob.BirnbaumSaundersDistribution.cdf # name: # type: sq_string # elements: 1 # length: 402 prob.BirnbaumSaundersDistribution: p = cdf (pd, x) prob.BirnbaumSaundersDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 39 prob.BirnbaumSaundersDistribution.gamma # name: # type: sq_string # elements: 1 # length: 215 prob.BirnbaumSaundersDistribution: property gamma Shape parameter A positive scalar value characterizing the shape of the Birnbaum-Saunders distribution. You can access the gamma property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Shape parameter # name: # type: sq_string # elements: 1 # length: 38 prob.BirnbaumSaundersDistribution.icdf # name: # type: sq_string # elements: 1 # length: 251 prob.BirnbaumSaundersDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 37 prob.BirnbaumSaundersDistribution.iqr # name: # type: sq_string # elements: 1 # length: 201 prob.BirnbaumSaundersDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 38 prob.BirnbaumSaundersDistribution.mean # name: # type: sq_string # elements: 1 # length: 173 prob.BirnbaumSaundersDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 40 prob.BirnbaumSaundersDistribution.median # name: # type: sq_string # elements: 1 # length: 181 prob.BirnbaumSaundersDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 43 prob.BirnbaumSaundersDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 227 prob.BirnbaumSaundersDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 41 prob.BirnbaumSaundersDistribution.paramci # name: # type: sq_string # elements: 1 # length: 993 prob.BirnbaumSaundersDistribution: ci = paramci (pd) prob.BirnbaumSaundersDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 37 prob.BirnbaumSaundersDistribution.pdf # name: # type: sq_string # elements: 1 # length: 211 prob.BirnbaumSaundersDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 38 prob.BirnbaumSaundersDistribution.plot # name: # type: sq_string # elements: 1 # length: 1546 prob.BirnbaumSaundersDistribution: plot (pd) prob.BirnbaumSaundersDistribution: plot (pd, Name, Value) prob.BirnbaumSaundersDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 41 prob.BirnbaumSaundersDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2105 prob.BirnbaumSaundersDistribution: [nlogL, param] = proflik (pd, pnum) prob.BirnbaumSaundersDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.BirnbaumSaundersDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.BirnbaumSaundersDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.BirnbaumSaundersDistribution: [nlogL, param] = proflik (pd) prob.BirnbaumSaundersDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the Birnbaum-Saunders distribution, pnum = 1 selects the parameter beta and pnum = 2 selects the parameter gamma. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 40 prob.BirnbaumSaundersDistribution.random # name: # type: sq_string # elements: 1 # length: 723 prob.BirnbaumSaundersDistribution: r = random (pd) prob.BirnbaumSaundersDistribution: r = random (pd, rows) prob.BirnbaumSaundersDistribution: r = random (pd, rows, cols, ...) prob.BirnbaumSaundersDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, bisarnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 37 prob.BirnbaumSaundersDistribution.std # name: # type: sq_string # elements: 1 # length: 199 prob.BirnbaumSaundersDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 42 prob.BirnbaumSaundersDistribution.truncate # name: # type: sq_string # elements: 1 # length: 542 prob.BirnbaumSaundersDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 37 prob.BirnbaumSaundersDistribution.var # name: # type: sq_string # elements: 1 # length: 179 prob.BirnbaumSaundersDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 21 prob.BurrDistribution # name: # type: sq_string # elements: 1 # length: 1319 statistics: prob.BurrDistribution Burr probability distribution object. A prob.BurrDistribution object consists of parameters, a model description, and sample data for a Burr probability distribution. The Burr distribution is a continuous probability distribution that models a non-negative random variable, commonly used to model household income. It is defined by a scale parameter alpha and two shape parameters c and k. There are several ways to create a prob.BurrDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.BurrDistribution (alpha, c, k) to create a Burr distribution with fixed parameter values alpha, c, and k. Use the static method prob.BurrDistribution.fit (x, alpha, censor, freq, options) to fit a distribution to the data in x using the same input arguments as the burrfit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the Burr distribution can be found at https://en.wikipedia.org/wiki/Burr_distribution See also: fitdist, makedist, burrcdf, burrinv, burrpdf, burrrnd, burrfit, burrlike, burrstat # name: # type: sq_string # elements: 1 # length: 37 Burr probability distribution object. # name: # type: sq_string # elements: 1 # length: 38 prob.BurrDistribution.BurrDistribution # name: # type: sq_string # elements: 1 # length: 369 prob.BurrDistribution: pd = BurrDistribution (alpha, c, k) prob.BurrDistribution: pd = BurrDistribution () Create a prob.BurrDistribution object. alpha, c and k are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, alpha 1, c 1 and k 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 38 Create a prob.BurrDistribution object. # name: # type: sq_string # elements: 1 # length: 38 prob.BurrDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 186 prob.BurrDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 31 prob.BurrDistribution.InputData # name: # type: sq_string # elements: 1 # length: 560 prob.BurrDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: an empty array, since prob.BurrDistribution does not allow censoring. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 33 prob.BurrDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 206 prob.BurrDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 35 prob.BurrDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 199 prob.BurrDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 41 prob.BurrDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 612 prob.BurrDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 3x3 numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter, and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 42 prob.BurrDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 223 prob.BurrDistribution: property ParameterDescription Description of parameters A 3x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 38 prob.BurrDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 279 prob.BurrDistribution: property ParameterIsFixed Flag for fixed parameters A 1x3 logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 36 prob.BurrDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 200 prob.BurrDistribution: property ParameterNames Names of parameters A 3x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 37 prob.BurrDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 283 prob.BurrDistribution: property ParameterValues Distribution parameter values A 3x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the alpha, c, and k properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 32 prob.BurrDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 343 prob.BurrDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 27 prob.BurrDistribution.alpha # name: # type: sq_string # elements: 1 # length: 190 prob.BurrDistribution: property alpha Scale parameter A positive scalar value characterizing the scale of the Burr distribution. You can access the alpha property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Scale parameter # name: # type: sq_string # elements: 1 # length: 23 prob.BurrDistribution.c # name: # type: sq_string # elements: 1 # length: 204 prob.BurrDistribution: property c First shape parameter A positive scalar value characterizing the first shape parameter of the Burr distribution. You can access the c property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 21 First shape parameter # name: # type: sq_string # elements: 1 # length: 25 prob.BurrDistribution.cdf # name: # type: sq_string # elements: 1 # length: 378 prob.BurrDistribution: p = cdf (pd, x) prob.BurrDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 26 prob.BurrDistribution.icdf # name: # type: sq_string # elements: 1 # length: 239 prob.BurrDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 25 prob.BurrDistribution.iqr # name: # type: sq_string # elements: 1 # length: 189 prob.BurrDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 23 prob.BurrDistribution.k # name: # type: sq_string # elements: 1 # length: 206 prob.BurrDistribution: property k Second shape parameter A positive scalar value characterizing the second shape parameter of the Burr distribution. You can access the k property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 22 Second shape parameter # name: # type: sq_string # elements: 1 # length: 26 prob.BurrDistribution.mean # name: # type: sq_string # elements: 1 # length: 161 prob.BurrDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 28 prob.BurrDistribution.median # name: # type: sq_string # elements: 1 # length: 169 prob.BurrDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.BurrDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 215 prob.BurrDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 29 prob.BurrDistribution.paramci # name: # type: sq_string # elements: 1 # length: 969 prob.BurrDistribution: ci = paramci (pd) prob.BurrDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 25 prob.BurrDistribution.pdf # name: # type: sq_string # elements: 1 # length: 199 prob.BurrDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 26 prob.BurrDistribution.plot # name: # type: sq_string # elements: 1 # length: 1510 prob.BurrDistribution: plot (pd) prob.BurrDistribution: plot (pd, Name, Value) prob.BurrDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 29 prob.BurrDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2052 prob.BurrDistribution: [nlogL, param] = proflik (pd, pnum) prob.BurrDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.BurrDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.BurrDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.BurrDistribution: [nlogL, param] = proflik (pd) prob.BurrDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the Burr distribution, pnum = 1 selects the parameter alpha, pnum = 2 selects the parameter c, and pnum = 3 selects the parameter k. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 28 prob.BurrDistribution.random # name: # type: sq_string # elements: 1 # length: 675 prob.BurrDistribution: r = random (pd) prob.BurrDistribution: r = random (pd, rows) prob.BurrDistribution: r = random (pd, rows, cols, ...) prob.BurrDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, burrrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 25 prob.BurrDistribution.std # name: # type: sq_string # elements: 1 # length: 187 prob.BurrDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 30 prob.BurrDistribution.truncate # name: # type: sq_string # elements: 1 # length: 530 prob.BurrDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 25 prob.BurrDistribution.var # name: # type: sq_string # elements: 1 # length: 167 prob.BurrDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 28 prob.ExponentialDistribution # name: # type: sq_string # elements: 1 # length: 1286 statistics: prob.ExponentialDistribution Exponential probability distribution object. A prob.ExponentialDistribution object consists of parameters, a model description, and sample data for a exponential probability distribution. The exponential distribution is a continuous probability distribution with mean parameter mu that models the time between events in a Poisson process. There are several ways to create a prob.ExponentialDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.ExponentialDistribution (mu) to create a exponential distribution with fixed parameter value mu. Use the static method prob.ExponentialDistribution.fit (x, alpha, censor, freq, options) to fit a distribution to the data in x using the same input arguments as the expfit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the exponential distribution can be found at https://en.wikipedia.org/wiki/Exponential_distribution See also: fitdist, makedist, expcdf, expinv, exppdf, exprnd, expfit, explike, expstat # name: # type: sq_string # elements: 1 # length: 44 Exponential probability distribution object. # name: # type: sq_string # elements: 1 # length: 45 prob.ExponentialDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 193 prob.ExponentialDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 52 prob.ExponentialDistribution.ExponentialDistribution # name: # type: sq_string # elements: 1 # length: 362 prob.ExponentialDistribution: pd = ExponentialDistribution (mu) prob.ExponentialDistribution: pd = ExponentialDistribution () Create a prob.ExponentialDistribution object. mu is the distribution parameter, which the class help describes. Called with no arguments the parameter takes its default, mu 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 45 Create a prob.ExponentialDistribution object. # name: # type: sq_string # elements: 1 # length: 38 prob.ExponentialDistribution.InputData # name: # type: sq_string # elements: 1 # length: 740 prob.ExponentialDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: a numeric vector of logical values indicating censoring information corresponding to the elements of the data used for distribution fitting. If no censoring vector was used for distribution fitting, then this field defaults to an empty array. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 40 prob.ExponentialDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 213 prob.ExponentialDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 42 prob.ExponentialDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 206 prob.ExponentialDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 48 prob.ExponentialDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 453 prob.ExponentialDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A scalar numeric value containing the variance-covariance of the parameter estimate. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then the variance-covariance is zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 49 prob.ExponentialDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 230 prob.ExponentialDistribution: property ParameterDescription Description of parameters A 1x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 45 prob.ExponentialDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 277 prob.ExponentialDistribution: property ParameterIsFixed Flag for fixed parameters A 1x1 logical vector specifying whether the parameter is fixed or estimated. true value corresponds to fixed parameter, false value corresponds to parameter estimate. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 43 prob.ExponentialDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 207 prob.ExponentialDistribution: property ParameterNames Names of parameters A 1x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 44 prob.ExponentialDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 273 prob.ExponentialDistribution: property ParameterValues Distribution parameter values A 1x1 numeric vector containing the value of the distribution parameter. This property is read-only. You can change the distribution parameter by assigning a new value to the mu property. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 39 prob.ExponentialDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 350 prob.ExponentialDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 32 prob.ExponentialDistribution.cdf # name: # type: sq_string # elements: 1 # length: 392 prob.ExponentialDistribution: p = cdf (pd, x) prob.ExponentialDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 33 prob.ExponentialDistribution.icdf # name: # type: sq_string # elements: 1 # length: 246 prob.ExponentialDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 32 prob.ExponentialDistribution.iqr # name: # type: sq_string # elements: 1 # length: 196 prob.ExponentialDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 33 prob.ExponentialDistribution.mean # name: # type: sq_string # elements: 1 # length: 168 prob.ExponentialDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 35 prob.ExponentialDistribution.median # name: # type: sq_string # elements: 1 # length: 176 prob.ExponentialDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.ExponentialDistribution.mu # name: # type: sq_string # elements: 1 # length: 196 prob.ExponentialDistribution: property mu Mean parameter A positive scalar value characterizing the mean of the exponential distribution. You can access the mu property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 14 Mean parameter # name: # type: sq_string # elements: 1 # length: 38 prob.ExponentialDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 222 prob.ExponentialDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 36 prob.ExponentialDistribution.paramci # name: # type: sq_string # elements: 1 # length: 983 prob.ExponentialDistribution: ci = paramci (pd) prob.ExponentialDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 32 prob.ExponentialDistribution.pdf # name: # type: sq_string # elements: 1 # length: 206 prob.ExponentialDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 33 prob.ExponentialDistribution.plot # name: # type: sq_string # elements: 1 # length: 1531 prob.ExponentialDistribution: plot (pd) prob.ExponentialDistribution: plot (pd, Name, Value) prob.ExponentialDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 36 prob.ExponentialDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2026 prob.ExponentialDistribution: [nlogL, param] = proflik (pd, pnum) prob.ExponentialDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.ExponentialDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.ExponentialDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.ExponentialDistribution: [nlogL, param] = proflik (pd) prob.ExponentialDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the exponential distribution, pnum = 1 selects the parameter mu. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 35 prob.ExponentialDistribution.random # name: # type: sq_string # elements: 1 # length: 703 prob.ExponentialDistribution: r = random (pd) prob.ExponentialDistribution: r = random (pd, rows) prob.ExponentialDistribution: r = random (pd, rows, cols, ...) prob.ExponentialDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, betarnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 32 prob.ExponentialDistribution.std # name: # type: sq_string # elements: 1 # length: 194 prob.ExponentialDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 37 prob.ExponentialDistribution.truncate # name: # type: sq_string # elements: 1 # length: 537 prob.ExponentialDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.ExponentialDistribution.var # name: # type: sq_string # elements: 1 # length: 174 prob.ExponentialDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 29 prob.ExtremeValueDistribution # name: # type: sq_string # elements: 1 # length: 1399 statistics: prob.ExtremeValueDistribution Extreme value probability distribution object. A prob.ExtremeValueDistribution object consists of parameters, a model description, and sample data for an extreme value probability distribution. The extreme value distribution is also known as the Gumbel distribution for maxima, and it is a limiting distribution for the maximum of a large number of samples from a continuous distribution. It is defined by location parameter mu and scale parameter sigma. There are several ways to create a prob.ExtremeValueDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with specified parameter values using the makedist function. Use the constructor prob.ExtremeValueDistribution (mu, sigma) to create an extreme value distribution with specified parameter values. Use the static method prob.ExtremeValueDistribution.fit (x, alpha, censor, freq, options) to fit a distribution to the data in x using the same input arguments as the evfit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the constructor and the aforementioned static method. Further information about the Gumbel distribution can be found at https://en.wikipedia.org/wiki/Gumbel_distribution See also: fitdist, makedist, evcdf, evinv, evpdf, evrnd, evfit, evlike, evstat # name: # type: sq_string # elements: 1 # length: 46 Extreme value probability distribution object. # name: # type: sq_string # elements: 1 # length: 46 prob.ExtremeValueDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 194 prob.ExtremeValueDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 54 prob.ExtremeValueDistribution.ExtremeValueDistribution # name: # type: sq_string # elements: 1 # length: 401 prob.ExtremeValueDistribution: pd = ExtremeValueDistribution (mu, sigma) prob.ExtremeValueDistribution: pd = ExtremeValueDistribution () Create a prob.ExtremeValueDistribution object. mu and sigma are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, mu 0 and sigma 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 46 Create a prob.ExtremeValueDistribution object. # name: # type: sq_string # elements: 1 # length: 39 prob.ExtremeValueDistribution.InputData # name: # type: sq_string # elements: 1 # length: 741 prob.ExtremeValueDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: a numeric vector of logical values indicating censoring information corresponding to the elements of the data used for distribution fitting. If no censoring vector was used for distribution fitting, then this field defaults to an empty array. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 41 prob.ExtremeValueDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 214 prob.ExtremeValueDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 43 prob.ExtremeValueDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 207 prob.ExtremeValueDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 49 prob.ExtremeValueDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 620 prob.ExtremeValueDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 2x2 numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter, and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 50 prob.ExtremeValueDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 231 prob.ExtremeValueDistribution: property ParameterDescription Description of parameters A 2x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 46 prob.ExtremeValueDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 287 prob.ExtremeValueDistribution: property ParameterIsFixed Flag for fixed parameters A 1x2 logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 44 prob.ExtremeValueDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 208 prob.ExtremeValueDistribution: property ParameterNames Names of parameters A 2x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 45 prob.ExtremeValueDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 288 prob.ExtremeValueDistribution: property ParameterValues Distribution parameter values A 2x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the mu and sigma properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 40 prob.ExtremeValueDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 351 prob.ExtremeValueDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 33 prob.ExtremeValueDistribution.cdf # name: # type: sq_string # elements: 1 # length: 394 prob.ExtremeValueDistribution: p = cdf (pd, x) prob.ExtremeValueDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 34 prob.ExtremeValueDistribution.icdf # name: # type: sq_string # elements: 1 # length: 247 prob.ExtremeValueDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 33 prob.ExtremeValueDistribution.iqr # name: # type: sq_string # elements: 1 # length: 197 prob.ExtremeValueDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 34 prob.ExtremeValueDistribution.mean # name: # type: sq_string # elements: 1 # length: 169 prob.ExtremeValueDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 36 prob.ExtremeValueDistribution.median # name: # type: sq_string # elements: 1 # length: 177 prob.ExtremeValueDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.ExtremeValueDistribution.mu # name: # type: sq_string # elements: 1 # length: 198 prob.ExtremeValueDistribution: property mu Location parameter A scalar value characterizing the location of the extreme value distribution. You can access the mu property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 18 Location parameter # name: # type: sq_string # elements: 1 # length: 39 prob.ExtremeValueDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 223 prob.ExtremeValueDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 37 prob.ExtremeValueDistribution.paramci # name: # type: sq_string # elements: 1 # length: 985 prob.ExtremeValueDistribution: ci = paramci (pd) prob.ExtremeValueDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 33 prob.ExtremeValueDistribution.pdf # name: # type: sq_string # elements: 1 # length: 207 prob.ExtremeValueDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 34 prob.ExtremeValueDistribution.plot # name: # type: sq_string # elements: 1 # length: 1534 prob.ExtremeValueDistribution: plot (pd) prob.ExtremeValueDistribution: plot (pd, Name, Value) prob.ExtremeValueDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 37 prob.ExtremeValueDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2075 prob.ExtremeValueDistribution: [nlogL, param] = proflik (pd, pnum) prob.ExtremeValueDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.ExtremeValueDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.ExtremeValueDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.ExtremeValueDistribution: [nlogL, param] = proflik (pd) prob.ExtremeValueDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the extreme value distribution, pnum = 1 selects the parameter mu and pnum = 2 selects the parameter sigma. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 36 prob.ExtremeValueDistribution.random # name: # type: sq_string # elements: 1 # length: 707 prob.ExtremeValueDistribution: r = random (pd) prob.ExtremeValueDistribution: r = random (pd, rows) prob.ExtremeValueDistribution: r = random (pd, rows, cols, ...) prob.ExtremeValueDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, betarnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 35 prob.ExtremeValueDistribution.sigma # name: # type: sq_string # elements: 1 # length: 207 prob.ExtremeValueDistribution: property sigma Scale parameter A positive scalar value characterizing the scale of the extreme value distribution. You can access the sigma property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Scale parameter # name: # type: sq_string # elements: 1 # length: 33 prob.ExtremeValueDistribution.std # name: # type: sq_string # elements: 1 # length: 195 prob.ExtremeValueDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 38 prob.ExtremeValueDistribution.truncate # name: # type: sq_string # elements: 1 # length: 538 prob.ExtremeValueDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 33 prob.ExtremeValueDistribution.var # name: # type: sq_string # elements: 1 # length: 175 prob.ExtremeValueDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 22 prob.GammaDistribution # name: # type: sq_string # elements: 1 # length: 1251 statistics: prob.GammaDistribution Gamma probability distribution object. A prob.GammaDistribution object consists of parameters, a model description, and sample data for a gamma probability distribution. The gamma distribution is a continuous probability distribution that models the time to failure of a process. It is defined by shape parameter a and scale parameter b. There are several ways to create a prob.GammaDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.GammaDistribution (a, b) to create a gamma distribution with fixed parameter values a and b. Use the static method prob.GammaDistribution.fit (x, alpha, censor, freq, options) to fit a distribution to the data in x using the same input arguments as the gamfit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the gamma distribution can be found at https://en.wikipedia.org/wiki/Gamma_distribution See also: fitdist, makedist, gamcdf, gaminv, gampdf, gamrnd, gamfit, gamlike, gamstat # name: # type: sq_string # elements: 1 # length: 38 Gamma probability distribution object. # name: # type: sq_string # elements: 1 # length: 39 prob.GammaDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 187 prob.GammaDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 40 prob.GammaDistribution.GammaDistribution # name: # type: sq_string # elements: 1 # length: 351 prob.GammaDistribution: pd = GammaDistribution (a, b) prob.GammaDistribution: pd = GammaDistribution () Create a prob.GammaDistribution object. a and b are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, a 1 and b 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 39 Create a prob.GammaDistribution object. # name: # type: sq_string # elements: 1 # length: 32 prob.GammaDistribution.InputData # name: # type: sq_string # elements: 1 # length: 734 prob.GammaDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: a numeric vector of logical values indicating censoring information corresponding to the elements of the data used for distribution fitting. If no censoring vector was used for distribution fitting, then this field defaults to an empty array. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 34 prob.GammaDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 207 prob.GammaDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 36 prob.GammaDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 200 prob.GammaDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 42 prob.GammaDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 613 prob.GammaDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 2x2 numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter, and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 43 prob.GammaDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 224 prob.GammaDistribution: property ParameterDescription Description of parameters A 2x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 39 prob.GammaDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 280 prob.GammaDistribution: property ParameterIsFixed Flag for fixed parameters A 1x2 logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 37 prob.GammaDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 201 prob.GammaDistribution: property ParameterNames Names of parameters A 2x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 38 prob.GammaDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 276 prob.GammaDistribution: property ParameterValues Distribution parameter values A 2x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the a and b properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 33 prob.GammaDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 344 prob.GammaDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 24 prob.GammaDistribution.a # name: # type: sq_string # elements: 1 # length: 184 prob.GammaDistribution: property a Shape parameter A positive scalar value characterizing the shape of the gamma distribution. You can access the a property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Shape parameter # name: # type: sq_string # elements: 1 # length: 24 prob.GammaDistribution.b # name: # type: sq_string # elements: 1 # length: 184 prob.GammaDistribution: property b Scale parameter A positive scalar value characterizing the scale of the gamma distribution. You can access the b property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Scale parameter # name: # type: sq_string # elements: 1 # length: 26 prob.GammaDistribution.cdf # name: # type: sq_string # elements: 1 # length: 380 prob.GammaDistribution: p = cdf (pd, x) prob.GammaDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 27 prob.GammaDistribution.icdf # name: # type: sq_string # elements: 1 # length: 240 prob.GammaDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 26 prob.GammaDistribution.iqr # name: # type: sq_string # elements: 1 # length: 190 prob.GammaDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 27 prob.GammaDistribution.mean # name: # type: sq_string # elements: 1 # length: 162 prob.GammaDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 29 prob.GammaDistribution.median # name: # type: sq_string # elements: 1 # length: 170 prob.GammaDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.GammaDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 216 prob.GammaDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 30 prob.GammaDistribution.paramci # name: # type: sq_string # elements: 1 # length: 971 prob.GammaDistribution: ci = paramci (pd) prob.GammaDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 26 prob.GammaDistribution.pdf # name: # type: sq_string # elements: 1 # length: 200 prob.GammaDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 27 prob.GammaDistribution.plot # name: # type: sq_string # elements: 1 # length: 1513 prob.GammaDistribution: plot (pd) prob.GammaDistribution: plot (pd, Name, Value) prob.GammaDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 30 prob.GammaDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2020 prob.GammaDistribution: [nlogL, param] = proflik (pd, pnum) prob.GammaDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.GammaDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.GammaDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.GammaDistribution: [nlogL, param] = proflik (pd) prob.GammaDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the gamma distribution, pnum = 1 selects the parameter a and pnum = 2 selects the parameter b. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 29 prob.GammaDistribution.random # name: # type: sq_string # elements: 1 # length: 678 prob.GammaDistribution: r = random (pd) prob.GammaDistribution: r = random (pd, rows) prob.GammaDistribution: r = random (pd, rows, cols, ...) prob.GammaDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, gamrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 26 prob.GammaDistribution.std # name: # type: sq_string # elements: 1 # length: 188 prob.GammaDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.GammaDistribution.truncate # name: # type: sq_string # elements: 1 # length: 531 prob.GammaDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 26 prob.GammaDistribution.var # name: # type: sq_string # elements: 1 # length: 168 prob.GammaDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 40 prob.GeneralizedExtremeValueDistribution # name: # type: sq_string # elements: 1 # length: 1480 statistics: prob.GeneralizedExtremeValueDistribution Generalized extreme value probability distribution object. A prob.GeneralizedExtremeValueDistribution object consists of parameters, a model description, and sample data for a generalized extreme value probability distribution. The generalized extreme value distribution is a continuous probability distribution that models extreme values. It is defined by shape parameter k, scale parameter sigma, and location parameter mu. There are several ways to create a prob.GeneralizedExtremeValueDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.GeneralizedExtremeValueDistribution (k, sigma, mu) to create a generalized extreme value distribution with fixed parameter values k, sigma, and mu. Use the static method prob.GeneralizedExtremeValueDistribution.fit (x, alpha, freq, options) to fit a distribution to the data in x using the same input arguments as the gevfit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the generalized extreme value distribution can be found at https://en.wikipedia.org/wiki/Generalized_extreme_value_distribution See also: fitdist, makedist, gevcdf, gevinv, gevpdf, gevrnd, gevfit, gevlike, gevstat # name: # type: sq_string # elements: 1 # length: 58 Generalized extreme value probability distribution object. # name: # type: sq_string # elements: 1 # length: 57 prob.GeneralizedExtremeValueDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 205 prob.GeneralizedExtremeValueDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 76 prob.GeneralizedExtremeValueDistribution.GeneralizedExtremeValueDistribution # name: # type: sq_string # elements: 1 # length: 467 prob.GeneralizedExtremeValueDistribution: pd = GeneralizedExtremeValueDistribution (k, sigma, mu) prob.GeneralizedExtremeValueDistribution: pd = GeneralizedExtremeValueDistribution () Create a prob.GeneralizedExtremeValueDistribution object. k, sigma and mu are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, k 0, sigma 1 and mu 0. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 57 Create a prob.GeneralizedExtremeValueDistribution object. # name: # type: sq_string # elements: 1 # length: 50 prob.GeneralizedExtremeValueDistribution.InputData # name: # type: sq_string # elements: 1 # length: 752 prob.GeneralizedExtremeValueDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: a numeric vector of logical values indicating censoring information corresponding to the elements of the data used for distribution fitting. If no censoring vector was used for distribution fitting, then this field defaults to an empty array. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 52 prob.GeneralizedExtremeValueDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 225 prob.GeneralizedExtremeValueDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 54 prob.GeneralizedExtremeValueDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 218 prob.GeneralizedExtremeValueDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 60 prob.GeneralizedExtremeValueDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 631 prob.GeneralizedExtremeValueDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 3x3 numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter, and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 61 prob.GeneralizedExtremeValueDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 242 prob.GeneralizedExtremeValueDistribution: property ParameterDescription Description of parameters A 3x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 57 prob.GeneralizedExtremeValueDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 298 prob.GeneralizedExtremeValueDistribution: property ParameterIsFixed Flag for fixed parameters A 1x3 logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 55 prob.GeneralizedExtremeValueDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 219 prob.GeneralizedExtremeValueDistribution: property ParameterNames Names of parameters A 3x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 56 prob.GeneralizedExtremeValueDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 303 prob.GeneralizedExtremeValueDistribution: property ParameterValues Distribution parameter values A 3x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the k, sigma, and mu properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 51 prob.GeneralizedExtremeValueDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 362 prob.GeneralizedExtremeValueDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 44 prob.GeneralizedExtremeValueDistribution.cdf # name: # type: sq_string # elements: 1 # length: 416 prob.GeneralizedExtremeValueDistribution: p = cdf (pd, x) prob.GeneralizedExtremeValueDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 45 prob.GeneralizedExtremeValueDistribution.icdf # name: # type: sq_string # elements: 1 # length: 258 prob.GeneralizedExtremeValueDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 44 prob.GeneralizedExtremeValueDistribution.iqr # name: # type: sq_string # elements: 1 # length: 208 prob.GeneralizedExtremeValueDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 42 prob.GeneralizedExtremeValueDistribution.k # name: # type: sq_string # elements: 1 # length: 213 prob.GeneralizedExtremeValueDistribution: property k Shape parameter A scalar value characterizing the shape of the generalized extreme value distribution. You can access the k property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Shape parameter # name: # type: sq_string # elements: 1 # length: 45 prob.GeneralizedExtremeValueDistribution.mean # name: # type: sq_string # elements: 1 # length: 180 prob.GeneralizedExtremeValueDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 47 prob.GeneralizedExtremeValueDistribution.median # name: # type: sq_string # elements: 1 # length: 188 prob.GeneralizedExtremeValueDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 43 prob.GeneralizedExtremeValueDistribution.mu # name: # type: sq_string # elements: 1 # length: 221 prob.GeneralizedExtremeValueDistribution: property mu Location parameter A scalar value characterizing the location of the generalized extreme value distribution. You can access the mu property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 18 Location parameter # name: # type: sq_string # elements: 1 # length: 50 prob.GeneralizedExtremeValueDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 234 prob.GeneralizedExtremeValueDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 48 prob.GeneralizedExtremeValueDistribution.paramci # name: # type: sq_string # elements: 1 # length: 1007 prob.GeneralizedExtremeValueDistribution: ci = paramci (pd) prob.GeneralizedExtremeValueDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 44 prob.GeneralizedExtremeValueDistribution.pdf # name: # type: sq_string # elements: 1 # length: 218 prob.GeneralizedExtremeValueDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 45 prob.GeneralizedExtremeValueDistribution.plot # name: # type: sq_string # elements: 1 # length: 1567 prob.GeneralizedExtremeValueDistribution: plot (pd) prob.GeneralizedExtremeValueDistribution: plot (pd, Name, Value) prob.GeneralizedExtremeValueDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 48 prob.GeneralizedExtremeValueDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2188 prob.GeneralizedExtremeValueDistribution: [nlogL, param] = proflik (pd, pnum) prob.GeneralizedExtremeValueDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.GeneralizedExtremeValueDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.GeneralizedExtremeValueDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.GeneralizedExtremeValueDistribution: [nlogL, param] = proflik (pd) prob.GeneralizedExtremeValueDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the generalized extreme value distribution, pnum = 1 selects the parameter k, pnum = 2 selects the parameter sigma, and pnum = 3 selects the parameter mu. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 47 prob.GeneralizedExtremeValueDistribution.random # name: # type: sq_string # elements: 1 # length: 750 prob.GeneralizedExtremeValueDistribution: r = random (pd) prob.GeneralizedExtremeValueDistribution: r = random (pd, rows) prob.GeneralizedExtremeValueDistribution: r = random (pd, rows, cols, ...) prob.GeneralizedExtremeValueDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, gevrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 46 prob.GeneralizedExtremeValueDistribution.sigma # name: # type: sq_string # elements: 1 # length: 230 prob.GeneralizedExtremeValueDistribution: property sigma Scale parameter A positive scalar value characterizing the scale of the generalized extreme value distribution. You can access the sigma property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Scale parameter # name: # type: sq_string # elements: 1 # length: 44 prob.GeneralizedExtremeValueDistribution.std # name: # type: sq_string # elements: 1 # length: 206 prob.GeneralizedExtremeValueDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 49 prob.GeneralizedExtremeValueDistribution.truncate # name: # type: sq_string # elements: 1 # length: 549 prob.GeneralizedExtremeValueDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 44 prob.GeneralizedExtremeValueDistribution.var # name: # type: sq_string # elements: 1 # length: 186 prob.GeneralizedExtremeValueDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 34 prob.GeneralizedParetoDistribution # name: # type: sq_string # elements: 1 # length: 1484 statistics: prob.GeneralizedParetoDistribution Generalized Pareto probability distribution object. A prob.GeneralizedParetoDistribution object consists of parameters, a model description, and sample data for a Generalized Pareto probability distribution. The Generalized Pareto distribution is a continuous probability distribution that models the tail behavior of other distributions, commonly used for extreme value analysis. It is defined by shape parameter k, scale parameter sigma, and location parameter theta. There are several ways to create a prob.GeneralizedParetoDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.GeneralizedParetoDistribution (k, sigma, theta) to create a Generalized Pareto distribution with fixed parameter values k, sigma, and theta. Use the static method prob.GeneralizedParetoDistribution.fit (x, theta, alpha, freq, options) to fit a distribution to the data in x using the same input arguments as the gpfit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the Generalized Pareto distribution can be found at https://en.wikipedia.org/wiki/Generalized_Pareto_distribution See also: fitdist, makedist, gpcdf, gpinv, gppdf, gprnd, gpfit, gplike, gpstat # name: # type: sq_string # elements: 1 # length: 51 Generalized Pareto probability distribution object. # name: # type: sq_string # elements: 1 # length: 51 prob.GeneralizedParetoDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 199 prob.GeneralizedParetoDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 64 prob.GeneralizedParetoDistribution.GeneralizedParetoDistribution # name: # type: sq_string # elements: 1 # length: 446 prob.GeneralizedParetoDistribution: pd = GeneralizedParetoDistribution (k, sigma, theta) prob.GeneralizedParetoDistribution: pd = GeneralizedParetoDistribution () Create a prob.GeneralizedParetoDistribution object. k, sigma and theta are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, k 1, sigma 1 and theta 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 51 Create a prob.GeneralizedParetoDistribution object. # name: # type: sq_string # elements: 1 # length: 44 prob.GeneralizedParetoDistribution.InputData # name: # type: sq_string # elements: 1 # length: 746 prob.GeneralizedParetoDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: a numeric vector of logical values indicating censoring information corresponding to the elements of the data used for distribution fitting. If no censoring vector was used for distribution fitting, then this field defaults to an empty array. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 46 prob.GeneralizedParetoDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 219 prob.GeneralizedParetoDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 48 prob.GeneralizedParetoDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 212 prob.GeneralizedParetoDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 54 prob.GeneralizedParetoDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 625 prob.GeneralizedParetoDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 3x3 numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter, and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 55 prob.GeneralizedParetoDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 236 prob.GeneralizedParetoDistribution: property ParameterDescription Description of parameters A 3x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 51 prob.GeneralizedParetoDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 292 prob.GeneralizedParetoDistribution: property ParameterIsFixed Flag for fixed parameters A 1x3 logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 49 prob.GeneralizedParetoDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 213 prob.GeneralizedParetoDistribution: property ParameterNames Names of parameters A 3x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 50 prob.GeneralizedParetoDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 300 prob.GeneralizedParetoDistribution: property ParameterValues Distribution parameter values A 3x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the k, sigma, and theta properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 45 prob.GeneralizedParetoDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 356 prob.GeneralizedParetoDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 38 prob.GeneralizedParetoDistribution.cdf # name: # type: sq_string # elements: 1 # length: 404 prob.GeneralizedParetoDistribution: p = cdf (pd, x) prob.GeneralizedParetoDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 39 prob.GeneralizedParetoDistribution.icdf # name: # type: sq_string # elements: 1 # length: 252 prob.GeneralizedParetoDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 38 prob.GeneralizedParetoDistribution.iqr # name: # type: sq_string # elements: 1 # length: 202 prob.GeneralizedParetoDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 36 prob.GeneralizedParetoDistribution.k # name: # type: sq_string # elements: 1 # length: 200 prob.GeneralizedParetoDistribution: property k Shape parameter A scalar value characterizing the shape of the Generalized Pareto distribution. You can access the k property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Shape parameter # name: # type: sq_string # elements: 1 # length: 39 prob.GeneralizedParetoDistribution.mean # name: # type: sq_string # elements: 1 # length: 174 prob.GeneralizedParetoDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 41 prob.GeneralizedParetoDistribution.median # name: # type: sq_string # elements: 1 # length: 182 prob.GeneralizedParetoDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 44 prob.GeneralizedParetoDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 228 prob.GeneralizedParetoDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 42 prob.GeneralizedParetoDistribution.paramci # name: # type: sq_string # elements: 1 # length: 995 prob.GeneralizedParetoDistribution: ci = paramci (pd) prob.GeneralizedParetoDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 38 prob.GeneralizedParetoDistribution.pdf # name: # type: sq_string # elements: 1 # length: 212 prob.GeneralizedParetoDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 39 prob.GeneralizedParetoDistribution.plot # name: # type: sq_string # elements: 1 # length: 1549 prob.GeneralizedParetoDistribution: plot (pd) prob.GeneralizedParetoDistribution: plot (pd, Name, Value) prob.GeneralizedParetoDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 42 prob.GeneralizedParetoDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2148 prob.GeneralizedParetoDistribution: [nlogL, param] = proflik (pd, pnum) prob.GeneralizedParetoDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.GeneralizedParetoDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.GeneralizedParetoDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.GeneralizedParetoDistribution: [nlogL, param] = proflik (pd) prob.GeneralizedParetoDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the Generalized Pareto distribution, pnum = 1 selects the parameter k, pnum = 2 selects the parameter sigma, and pnum = 3 selects the parameter theta. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 41 prob.GeneralizedParetoDistribution.random # name: # type: sq_string # elements: 1 # length: 726 prob.GeneralizedParetoDistribution: r = random (pd) prob.GeneralizedParetoDistribution: r = random (pd, rows) prob.GeneralizedParetoDistribution: r = random (pd, rows, cols, ...) prob.GeneralizedParetoDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, random returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 40 prob.GeneralizedParetoDistribution.sigma # name: # type: sq_string # elements: 1 # length: 217 prob.GeneralizedParetoDistribution: property sigma Scale parameter A positive scalar value characterizing the scale of the Generalized Pareto distribution. You can access the sigma property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Scale parameter # name: # type: sq_string # elements: 1 # length: 38 prob.GeneralizedParetoDistribution.std # name: # type: sq_string # elements: 1 # length: 200 prob.GeneralizedParetoDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 40 prob.GeneralizedParetoDistribution.theta # name: # type: sq_string # elements: 1 # length: 214 prob.GeneralizedParetoDistribution: property theta Location parameter A scalar value characterizing the location of the Generalized Pareto distribution. You can access the theta property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 18 Location parameter # name: # type: sq_string # elements: 1 # length: 43 prob.GeneralizedParetoDistribution.truncate # name: # type: sq_string # elements: 1 # length: 543 prob.GeneralizedParetoDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 38 prob.GeneralizedParetoDistribution.var # name: # type: sq_string # elements: 1 # length: 180 prob.GeneralizedParetoDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 27 prob.HalfNormalDistribution # name: # type: sq_string # elements: 1 # length: 1330 statistics: prob.HalfNormalDistribution Half-normal probability distribution object. A prob.HalfNormalDistribution object consists of parameters, a model description, and sample data for a half-normal probability distribution. The half-normal distribution is a continuous probability distribution that models the time to failure of materials subjected to cyclic loading. It is defined by location parameter mu and scale parameter sigma. There are several ways to create a prob.HalfNormalDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.HalfNormalDistribution (mu, sigma) to create a half-normal distribution with fixed parameter values mu and sigma. Use the static method prob.HalfNormalDistribution.fit (x, mu, freq) to fit a distribution to the data in x using the same input arguments as the hnfit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the half-normal distribution can be found at https://en.wikipedia.org/wiki/Half-normal_distribution See also: fitdist, makedist, hncdf, hninv, hnpdf, hnrnd, hnfit, hnlike, hnstat # name: # type: sq_string # elements: 1 # length: 44 Half-normal probability distribution object. # name: # type: sq_string # elements: 1 # length: 44 prob.HalfNormalDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 192 prob.HalfNormalDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 50 prob.HalfNormalDistribution.HalfNormalDistribution # name: # type: sq_string # elements: 1 # length: 391 prob.HalfNormalDistribution: pd = HalfNormalDistribution (mu, sigma) prob.HalfNormalDistribution: pd = HalfNormalDistribution () Create a prob.HalfNormalDistribution object. mu and sigma are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, mu 0 and sigma 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 44 Create a prob.HalfNormalDistribution object. # name: # type: sq_string # elements: 1 # length: 37 prob.HalfNormalDistribution.InputData # name: # type: sq_string # elements: 1 # length: 739 prob.HalfNormalDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: a numeric vector of logical values indicating censoring information corresponding to the elements of the data used for distribution fitting. If no censoring vector was used for distribution fitting, then this field defaults to an empty array. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 39 prob.HalfNormalDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 212 prob.HalfNormalDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 41 prob.HalfNormalDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 205 prob.HalfNormalDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 47 prob.HalfNormalDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 618 prob.HalfNormalDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 2x2 numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter, and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 48 prob.HalfNormalDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 229 prob.HalfNormalDistribution: property ParameterDescription Description of parameters A 2x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 44 prob.HalfNormalDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 285 prob.HalfNormalDistribution: property ParameterIsFixed Flag for fixed parameters A 1x2 logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 42 prob.HalfNormalDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 206 prob.HalfNormalDistribution: property ParameterNames Names of parameters A 2x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 43 prob.HalfNormalDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 286 prob.HalfNormalDistribution: property ParameterValues Distribution parameter values A 2x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the mu and sigma properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 38 prob.HalfNormalDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 349 prob.HalfNormalDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 31 prob.HalfNormalDistribution.cdf # name: # type: sq_string # elements: 1 # length: 390 prob.HalfNormalDistribution: p = cdf (pd, x) prob.HalfNormalDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 32 prob.HalfNormalDistribution.icdf # name: # type: sq_string # elements: 1 # length: 245 prob.HalfNormalDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 31 prob.HalfNormalDistribution.iqr # name: # type: sq_string # elements: 1 # length: 195 prob.HalfNormalDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.HalfNormalDistribution.mean # name: # type: sq_string # elements: 1 # length: 167 prob.HalfNormalDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 34 prob.HalfNormalDistribution.median # name: # type: sq_string # elements: 1 # length: 175 prob.HalfNormalDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 30 prob.HalfNormalDistribution.mu # name: # type: sq_string # elements: 1 # length: 194 prob.HalfNormalDistribution: property mu Location parameter A scalar value characterizing the location of the half-normal distribution. You can access the mu property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 18 Location parameter # name: # type: sq_string # elements: 1 # length: 37 prob.HalfNormalDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 221 prob.HalfNormalDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 35 prob.HalfNormalDistribution.paramci # name: # type: sq_string # elements: 1 # length: 981 prob.HalfNormalDistribution: ci = paramci (pd) prob.HalfNormalDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 31 prob.HalfNormalDistribution.pdf # name: # type: sq_string # elements: 1 # length: 205 prob.HalfNormalDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 32 prob.HalfNormalDistribution.plot # name: # type: sq_string # elements: 1 # length: 1528 prob.HalfNormalDistribution: plot (pd) prob.HalfNormalDistribution: plot (pd, Name, Value) prob.HalfNormalDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 35 prob.HalfNormalDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2061 prob.HalfNormalDistribution: [nlogL, param] = proflik (pd, pnum) prob.HalfNormalDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.HalfNormalDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.HalfNormalDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.HalfNormalDistribution: [nlogL, param] = proflik (pd) prob.HalfNormalDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the Half-normal distribution, pnum = 1 selects the parameter mu and pnum = 2 selects the parameter sigma. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 34 prob.HalfNormalDistribution.random # name: # type: sq_string # elements: 1 # length: 697 prob.HalfNormalDistribution: r = random (pd) prob.HalfNormalDistribution: r = random (pd, rows) prob.HalfNormalDistribution: r = random (pd, rows, cols, ...) prob.HalfNormalDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, hnrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 33 prob.HalfNormalDistribution.sigma # name: # type: sq_string # elements: 1 # length: 203 prob.HalfNormalDistribution: property sigma Scale parameter A positive scalar value characterizing the scale of the half-normal distribution. You can access the sigma property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Scale parameter # name: # type: sq_string # elements: 1 # length: 31 prob.HalfNormalDistribution.std # name: # type: sq_string # elements: 1 # length: 193 prob.HalfNormalDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 36 prob.HalfNormalDistribution.truncate # name: # type: sq_string # elements: 1 # length: 536 prob.HalfNormalDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.HalfNormalDistribution.var # name: # type: sq_string # elements: 1 # length: 173 prob.HalfNormalDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.InverseGaussianDistribution # name: # type: sq_string # elements: 1 # length: 1413 statistics: prob.InverseGaussianDistribution Inverse Gaussian probability distribution object. A prob.InverseGaussianDistribution object consists of parameters, a model description, and sample data for a Inverse Gaussian probability distribution. The Inverse Gaussian distribution is a continuous probability distribution, which is often used to model non-negative positively skewed data. Is is defined by mean parameter mu and shape parameter lambda. There are several ways to create a prob.InverseGaussianDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.InverseGaussianDistribution (mu, lambda) to create a Inverse Gaussian distribution with fixed parameter values mu and lambda. Use the static method prob.InverseGaussianDistribution.fit (x, alpha, censor, freq, options) to fit a distribution to the data in x using the same input arguments as the invgfit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the Inverse Gaussian distribution can be found at https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution See also: fitdist, makedist, invgcdf, invginv, invgpdf, invgrnd, invgfit, invglike, invgstat # name: # type: sq_string # elements: 1 # length: 49 Inverse Gaussian probability distribution object. # name: # type: sq_string # elements: 1 # length: 49 prob.InverseGaussianDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 197 prob.InverseGaussianDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 42 prob.InverseGaussianDistribution.InputData # name: # type: sq_string # elements: 1 # length: 744 prob.InverseGaussianDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: a numeric vector of logical values indicating censoring information corresponding to the elements of the data used for distribution fitting. If no censoring vector was used for distribution fitting, then this field defaults to an empty array. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 60 prob.InverseGaussianDistribution.InverseGaussianDistribution # name: # type: sq_string # elements: 1 # length: 419 prob.InverseGaussianDistribution: pd = InverseGaussianDistribution (mu, lambda) prob.InverseGaussianDistribution: pd = InverseGaussianDistribution () Create a prob.InverseGaussianDistribution object. mu and lambda are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, mu 1 and lambda 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 49 Create a prob.InverseGaussianDistribution object. # name: # type: sq_string # elements: 1 # length: 44 prob.InverseGaussianDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 217 prob.InverseGaussianDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 46 prob.InverseGaussianDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 210 prob.InverseGaussianDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 52 prob.InverseGaussianDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 623 prob.InverseGaussianDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 2x2 numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter, and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 53 prob.InverseGaussianDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 234 prob.InverseGaussianDistribution: property ParameterDescription Description of parameters A 2x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 49 prob.InverseGaussianDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 290 prob.InverseGaussianDistribution: property ParameterIsFixed Flag for fixed parameters A 1x2 logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 47 prob.InverseGaussianDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 211 prob.InverseGaussianDistribution: property ParameterNames Names of parameters A 2x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 48 prob.InverseGaussianDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 292 prob.InverseGaussianDistribution: property ParameterValues Distribution parameter values A 2x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the mu and lambda properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 43 prob.InverseGaussianDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 354 prob.InverseGaussianDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 36 prob.InverseGaussianDistribution.cdf # name: # type: sq_string # elements: 1 # length: 400 prob.InverseGaussianDistribution: p = cdf (pd, x) prob.InverseGaussianDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 37 prob.InverseGaussianDistribution.icdf # name: # type: sq_string # elements: 1 # length: 250 prob.InverseGaussianDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 36 prob.InverseGaussianDistribution.iqr # name: # type: sq_string # elements: 1 # length: 200 prob.InverseGaussianDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 39 prob.InverseGaussianDistribution.lambda # name: # type: sq_string # elements: 1 # length: 215 prob.InverseGaussianDistribution: property lambda Shape parameter A positive scalar value characterizing the shape of the Inverse Gaussian distribution. You can access the lambda property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Shape parameter # name: # type: sq_string # elements: 1 # length: 37 prob.InverseGaussianDistribution.mean # name: # type: sq_string # elements: 1 # length: 172 prob.InverseGaussianDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 39 prob.InverseGaussianDistribution.median # name: # type: sq_string # elements: 1 # length: 180 prob.InverseGaussianDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 35 prob.InverseGaussianDistribution.mu # name: # type: sq_string # elements: 1 # length: 205 prob.InverseGaussianDistribution: property mu Mean parameter A positive scalar value characterizing the mean of the Inverse Gaussian distribution. You can access the mu property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 14 Mean parameter # name: # type: sq_string # elements: 1 # length: 42 prob.InverseGaussianDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 226 prob.InverseGaussianDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 40 prob.InverseGaussianDistribution.paramci # name: # type: sq_string # elements: 1 # length: 991 prob.InverseGaussianDistribution: ci = paramci (pd) prob.InverseGaussianDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 36 prob.InverseGaussianDistribution.pdf # name: # type: sq_string # elements: 1 # length: 210 prob.InverseGaussianDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 37 prob.InverseGaussianDistribution.plot # name: # type: sq_string # elements: 1 # length: 1543 prob.InverseGaussianDistribution: plot (pd) prob.InverseGaussianDistribution: plot (pd, Name, Value) prob.InverseGaussianDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 40 prob.InverseGaussianDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2097 prob.InverseGaussianDistribution: [nlogL, param] = proflik (pd, pnum) prob.InverseGaussianDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.InverseGaussianDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.InverseGaussianDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.InverseGaussianDistribution: [nlogL, param] = proflik (pd) prob.InverseGaussianDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the Inverse Gaussian distribution, pnum = 1 selects the parameter mu and pnum = 2 selects the parameter lambda. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 39 prob.InverseGaussianDistribution.random # name: # type: sq_string # elements: 1 # length: 719 prob.InverseGaussianDistribution: r = random (pd) prob.InverseGaussianDistribution: r = random (pd, rows) prob.InverseGaussianDistribution: r = random (pd, rows, cols, ...) prob.InverseGaussianDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, invgrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 36 prob.InverseGaussianDistribution.std # name: # type: sq_string # elements: 1 # length: 198 prob.InverseGaussianDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 41 prob.InverseGaussianDistribution.truncate # name: # type: sq_string # elements: 1 # length: 541 prob.InverseGaussianDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 36 prob.InverseGaussianDistribution.var # name: # type: sq_string # elements: 1 # length: 178 prob.InverseGaussianDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 23 prob.KernelDistribution # name: # type: sq_string # elements: 1 # length: 839 statistics: prob.KernelDistribution Kernel probability distribution object. A prob.KernelDistribution object consists of a nonparametric kernel smoothing density estimate fitted to sample data, together with a model description. Unlike the parametric distribution objects, it has no estimated parameters; the fitted distribution is defined entirely by the data, the smoothing kernel, and the bandwidth. A prob.KernelDistribution object can only be created by fitting a kernel smoothing distribution to data with the fitdist function. Unlike the parametric distributions, it cannot be created with the makedist function, since it is not parametric and requires data. Further information about the kernel density estimation can be found at https://en.wikipedia.org/wiki/Kernel_density_estimation See also: fitdist, ksdensity, mvksdensity # name: # type: sq_string # elements: 1 # length: 39 Kernel probability distribution object. # name: # type: sq_string # elements: 1 # length: 33 prob.KernelDistribution.Bandwidth # name: # type: sq_string # elements: 1 # length: 217 prob.KernelDistribution: property Bandwidth Bandwidth of the smoothing kernel A positive scalar value specifying the bandwidth of the smoothing kernel. You can access the Bandwidth property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 33 Bandwidth of the smoothing kernel # name: # type: sq_string # elements: 1 # length: 40 prob.KernelDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 188 prob.KernelDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 33 prob.KernelDistribution.InputData # name: # type: sq_string # elements: 1 # length: 568 prob.KernelDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: an empty array, since censoring is not supported for a kernel distribution. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 35 prob.KernelDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 208 prob.KernelDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 30 prob.KernelDistribution.Kernel # name: # type: sq_string # elements: 1 # length: 280 prob.KernelDistribution: property Kernel Kernel smoothing function A character vector specifying the type of smoothing kernel used for the density estimate. It is one of 'normal', 'box', 'triangle', or 'epanechnikov'. You can access the Kernel property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 25 Kernel smoothing function # name: # type: sq_string # elements: 1 # length: 42 prob.KernelDistribution.KernelDistribution # name: # type: sq_string # elements: 1 # length: 469 prob.KernelDistribution: pd = KernelDistribution (data, kernel, bw, support, freq) prob.KernelDistribution: pd = KernelDistribution () Create a prob.KernelDistribution object. data, kernel, bw, support and freq are the distribution parameters, which the class help describes. Called with no arguments it fits the data [0; 1] with a normal kernel over an unbounded support, taking the bandwidth from the data. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 40 Create a prob.KernelDistribution object. # name: # type: sq_string # elements: 1 # length: 31 prob.KernelDistribution.Support # name: # type: sq_string # elements: 1 # length: 467 prob.KernelDistribution: property Support Support of the probability distribution A scalar structure containing the following fields: range: either the character vector 'unbounded' or 'positive', or a two-element numeric vector [L, U] with the lower and upper bounds of the support. closedbound: a two-element logical vector specifying whether each bound is closed. iscontinuous: a logical scalar, always true for a kernel distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Support of the probability distribution # name: # type: sq_string # elements: 1 # length: 34 prob.KernelDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 345 prob.KernelDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 27 prob.KernelDistribution.cdf # name: # type: sq_string # elements: 1 # length: 464 prob.KernelDistribution: p = cdf (pd, x) prob.KernelDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. x must be double or single; integer, logical, and character arrays are rejected. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 28 prob.KernelDistribution.icdf # name: # type: sq_string # elements: 1 # length: 323 prob.KernelDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. p must be double or single; integer, logical, and character arrays are rejected. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 27 prob.KernelDistribution.iqr # name: # type: sq_string # elements: 1 # length: 191 prob.KernelDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 28 prob.KernelDistribution.mean # name: # type: sq_string # elements: 1 # length: 163 prob.KernelDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 30 prob.KernelDistribution.median # name: # type: sq_string # elements: 1 # length: 171 prob.KernelDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 33 prob.KernelDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 217 prob.KernelDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 27 prob.KernelDistribution.pdf # name: # type: sq_string # elements: 1 # length: 278 prob.KernelDistribution: y = pdf (pd, x) Compute the probability density function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. x must be double or single; integer, logical, and character arrays are rejected. # name: # type: sq_string # elements: 1 # length: 47 Compute the probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 28 prob.KernelDistribution.plot # name: # type: sq_string # elements: 1 # length: 1008 prob.KernelDistribution: plot (pd) prob.KernelDistribution: plot (pd, Name, Value) prob.KernelDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd, superimposed over a histogram of the data used to fit it. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF) superimposed on a histogram of the data. 'cdf' plots the cumulative distribution function (CDF) superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 30 prob.KernelDistribution.random # name: # type: sq_string # elements: 1 # length: 682 prob.KernelDistribution: r = random (pd) prob.KernelDistribution: r = random (pd, rows) prob.KernelDistribution: r = random (pd, rows, cols, ...) prob.KernelDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, random returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 27 prob.KernelDistribution.std # name: # type: sq_string # elements: 1 # length: 189 prob.KernelDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.KernelDistribution.truncate # name: # type: sq_string # elements: 1 # length: 290 prob.KernelDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 27 prob.KernelDistribution.var # name: # type: sq_string # elements: 1 # length: 169 prob.KernelDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 25 prob.LogisticDistribution # name: # type: sq_string # elements: 1 # length: 1344 statistics: prob.LogisticDistribution Logistic probability distribution object. A prob.LogisticDistribution object consists of parameters, a model description, and sample data for a logistic probability distribution. The logistic distribution is a continuous probability distribution, which is commonly used in logistic regression and feedforward neural networks. It is defined by location parameter mu and scale parameter sigma. There are several ways to create a prob.LogisticDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.LogisticDistribution (mu, sigma) to create a logistic distribution with fixed parameter values mu and sigma. Use the static method prob.LogisticDistribution.fit (x, alpha, censor, freq, options) to fit a distribution to the data in x using the same input arguments as the logifit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the logistic distribution can be found at https://en.wikipedia.org/wiki/Logistic_distribution See also: fitdist, makedist, logicdf, logiinv, logipdf, logirnd, logifit, logilike, logistat # name: # type: sq_string # elements: 1 # length: 41 Logistic probability distribution object. # name: # type: sq_string # elements: 1 # length: 42 prob.LogisticDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 190 prob.LogisticDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 35 prob.LogisticDistribution.InputData # name: # type: sq_string # elements: 1 # length: 737 prob.LogisticDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: a numeric vector of logical values indicating censoring information corresponding to the elements of the data used for distribution fitting. If no censoring vector was used for distribution fitting, then this field defaults to an empty array. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 37 prob.LogisticDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 210 prob.LogisticDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 46 prob.LogisticDistribution.LogisticDistribution # name: # type: sq_string # elements: 1 # length: 381 prob.LogisticDistribution: pd = LogisticDistribution (mu, sigma) prob.LogisticDistribution: pd = LogisticDistribution () Create a prob.LogisticDistribution object. mu and sigma are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, mu 0 and sigma 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 42 Create a prob.LogisticDistribution object. # name: # type: sq_string # elements: 1 # length: 39 prob.LogisticDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 203 prob.LogisticDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 45 prob.LogisticDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 616 prob.LogisticDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 2x2 numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter, and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 46 prob.LogisticDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 227 prob.LogisticDistribution: property ParameterDescription Description of parameters A 2x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 42 prob.LogisticDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 283 prob.LogisticDistribution: property ParameterIsFixed Flag for fixed parameters A 1x2 logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 40 prob.LogisticDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 204 prob.LogisticDistribution: property ParameterNames Names of parameters A 2x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 41 prob.LogisticDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 284 prob.LogisticDistribution: property ParameterValues Distribution parameter values A 2x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the mu and sigma properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 36 prob.LogisticDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 347 prob.LogisticDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 29 prob.LogisticDistribution.cdf # name: # type: sq_string # elements: 1 # length: 386 prob.LogisticDistribution: p = cdf (pd, x) prob.LogisticDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 30 prob.LogisticDistribution.icdf # name: # type: sq_string # elements: 1 # length: 243 prob.LogisticDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 29 prob.LogisticDistribution.iqr # name: # type: sq_string # elements: 1 # length: 193 prob.LogisticDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 30 prob.LogisticDistribution.mean # name: # type: sq_string # elements: 1 # length: 165 prob.LogisticDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.LogisticDistribution.median # name: # type: sq_string # elements: 1 # length: 173 prob.LogisticDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 28 prob.LogisticDistribution.mu # name: # type: sq_string # elements: 1 # length: 189 prob.LogisticDistribution: property mu Location parameter A scalar value characterizing the location of the logistic distribution. You can access the mu property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 18 Location parameter # name: # type: sq_string # elements: 1 # length: 35 prob.LogisticDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 219 prob.LogisticDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 33 prob.LogisticDistribution.paramci # name: # type: sq_string # elements: 1 # length: 977 prob.LogisticDistribution: ci = paramci (pd) prob.LogisticDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 29 prob.LogisticDistribution.pdf # name: # type: sq_string # elements: 1 # length: 203 prob.LogisticDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 30 prob.LogisticDistribution.plot # name: # type: sq_string # elements: 1 # length: 1522 prob.LogisticDistribution: plot (pd) prob.LogisticDistribution: plot (pd, Name, Value) prob.LogisticDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 33 prob.LogisticDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2046 prob.LogisticDistribution: [nlogL, param] = proflik (pd, pnum) prob.LogisticDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.LogisticDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.LogisticDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.LogisticDistribution: [nlogL, param] = proflik (pd) prob.LogisticDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the logistic distribution, pnum = 1 selects the parameter mu and pnum = 2 selects the parameter sigma. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 32 prob.LogisticDistribution.random # name: # type: sq_string # elements: 1 # length: 691 prob.LogisticDistribution: r = random (pd) prob.LogisticDistribution: r = random (pd, rows) prob.LogisticDistribution: r = random (pd, rows, cols, ...) prob.LogisticDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, bisarnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 31 prob.LogisticDistribution.sigma # name: # type: sq_string # elements: 1 # length: 198 prob.LogisticDistribution: property sigma Scale parameter A positive scalar value characterizing the scale of the logistic distribution. You can access the sigma property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Scale parameter # name: # type: sq_string # elements: 1 # length: 29 prob.LogisticDistribution.std # name: # type: sq_string # elements: 1 # length: 191 prob.LogisticDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 34 prob.LogisticDistribution.truncate # name: # type: sq_string # elements: 1 # length: 534 prob.LogisticDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 29 prob.LogisticDistribution.var # name: # type: sq_string # elements: 1 # length: 171 prob.LogisticDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 28 prob.LoglogisticDistribution # name: # type: sq_string # elements: 1 # length: 1389 statistics: prob.LoglogisticDistribution Log-logistic probability distribution object. A prob.LoglogisticDistribution object consists of parameters, a model description, and sample data for a log-logistic probability distribution. The log-logistic distribution is a continuous probability distribution that models non-negative random variables whose logarithm follows the logistic distribution. It is defined by location parameter mu and scale parameter sigma. There are several ways to create a prob.LoglogisticDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.LoglogisticDistribution (mu, sigma) to create a log-logistic distribution with fixed parameter values mu and sigma. Use the static method prob.LoglogisticDistribution.fit (x, censor, freq, options) to fit a distribution to the data in x using the same input arguments as the loglfit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the log-logistic distribution can be found at https://en.wikipedia.org/wiki/Log-logistic_distribution See also: fitdist, makedist, loglcdf, loglinv, loglpdf, loglrnd, loglfit, logllike, loglstat # name: # type: sq_string # elements: 1 # length: 45 Log-logistic probability distribution object. # name: # type: sq_string # elements: 1 # length: 45 prob.LoglogisticDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 193 prob.LoglogisticDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 38 prob.LoglogisticDistribution.InputData # name: # type: sq_string # elements: 1 # length: 740 prob.LoglogisticDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: a numeric vector of logical values indicating censoring information corresponding to the elements of the data used for distribution fitting. If no censoring vector was used for distribution fitting, then this field defaults to an empty array. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 40 prob.LoglogisticDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 213 prob.LoglogisticDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 52 prob.LoglogisticDistribution.LoglogisticDistribution # name: # type: sq_string # elements: 1 # length: 396 prob.LoglogisticDistribution: pd = LoglogisticDistribution (mu, sigma) prob.LoglogisticDistribution: pd = LoglogisticDistribution () Create a prob.LoglogisticDistribution object. mu and sigma are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, mu 0 and sigma 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 45 Create a prob.LoglogisticDistribution object. # name: # type: sq_string # elements: 1 # length: 42 prob.LoglogisticDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 206 prob.LoglogisticDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 48 prob.LoglogisticDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 619 prob.LoglogisticDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 2x2 numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter, and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 49 prob.LoglogisticDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 230 prob.LoglogisticDistribution: property ParameterDescription Description of parameters A 2x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 45 prob.LoglogisticDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 286 prob.LoglogisticDistribution: property ParameterIsFixed Flag for fixed parameters A 1x2 logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 43 prob.LoglogisticDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 207 prob.LoglogisticDistribution: property ParameterNames Names of parameters A 2x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 44 prob.LoglogisticDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 287 prob.LoglogisticDistribution: property ParameterValues Distribution parameter values A 2x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the mu and sigma properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 39 prob.LoglogisticDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 350 prob.LoglogisticDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 32 prob.LoglogisticDistribution.cdf # name: # type: sq_string # elements: 1 # length: 392 prob.LoglogisticDistribution: p = cdf (pd, x) prob.LoglogisticDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 33 prob.LoglogisticDistribution.icdf # name: # type: sq_string # elements: 1 # length: 246 prob.LoglogisticDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 32 prob.LoglogisticDistribution.iqr # name: # type: sq_string # elements: 1 # length: 196 prob.LoglogisticDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 33 prob.LoglogisticDistribution.mean # name: # type: sq_string # elements: 1 # length: 168 prob.LoglogisticDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 35 prob.LoglogisticDistribution.median # name: # type: sq_string # elements: 1 # length: 176 prob.LoglogisticDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.LoglogisticDistribution.mu # name: # type: sq_string # elements: 1 # length: 226 prob.LoglogisticDistribution: property mu Mean of logarithmic values A scalar value characterizing the mean of the logarithmic values of the log-logistic distribution. You can access the mu property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 26 Mean of logarithmic values # name: # type: sq_string # elements: 1 # length: 38 prob.LoglogisticDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 222 prob.LoglogisticDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 36 prob.LoglogisticDistribution.paramci # name: # type: sq_string # elements: 1 # length: 983 prob.LoglogisticDistribution: ci = paramci (pd) prob.LoglogisticDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 32 prob.LoglogisticDistribution.pdf # name: # type: sq_string # elements: 1 # length: 206 prob.LoglogisticDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 33 prob.LoglogisticDistribution.plot # name: # type: sq_string # elements: 1 # length: 1531 prob.LoglogisticDistribution: plot (pd) prob.LoglogisticDistribution: plot (pd, Name, Value) prob.LoglogisticDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 36 prob.LoglogisticDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2068 prob.LoglogisticDistribution: [nlogL, param] = proflik (pd, pnum) prob.LoglogisticDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.LoglogisticDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.LoglogisticDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.LoglogisticDistribution: [nlogL, param] = proflik (pd) prob.LoglogisticDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the Log-logistic distribution, pnum = 1 selects the parameter mu and pnum = 2 selects the parameter sigma. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 35 prob.LoglogisticDistribution.random # name: # type: sq_string # elements: 1 # length: 702 prob.LoglogisticDistribution: r = random (pd) prob.LoglogisticDistribution: r = random (pd, rows) prob.LoglogisticDistribution: r = random (pd, rows, cols, ...) prob.LoglogisticDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, random returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 34 prob.LoglogisticDistribution.sigma # name: # type: sq_string # elements: 1 # length: 243 prob.LoglogisticDistribution: property sigma Scale of logarithmic values A positive scalar value characterizing the scale of the logarithmic values of the log-logistic distribution. You can access the sigma property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 27 Scale of logarithmic values # name: # type: sq_string # elements: 1 # length: 32 prob.LoglogisticDistribution.std # name: # type: sq_string # elements: 1 # length: 194 prob.LoglogisticDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 37 prob.LoglogisticDistribution.truncate # name: # type: sq_string # elements: 1 # length: 537 prob.LoglogisticDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.LoglogisticDistribution.var # name: # type: sq_string # elements: 1 # length: 174 prob.LoglogisticDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 26 prob.LognormalDistribution # name: # type: sq_string # elements: 1 # length: 1345 statistics: prob.LognormalDistribution Lognormal probability distribution object. A prob.LognormalDistribution object consists of parameters, a model description, and sample data for a lognormal probability distribution. The lognormal distribution is a continuous probability distribution whose logarithm is normally distributed. It is defined by mean parameter mu and standard deviation parameter sigma of the logarithmic values. There are several ways to create a prob.LognormalDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.LognormalDistribution (mu, sigma) to create a lognormal distribution with fixed parameter values mu and sigma. Use the static method prob.LognormalDistribution.fit (x, censor, freq, options) to fit a distribution to the data in x using the same input arguments as the lognfit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the lognormal distribution can be found at https://en.wikipedia.org/wiki/Log-normal_distribution See also: fitdist, makedist, logncdf, logninv, lognpdf, lognrnd, lognfit, lognlike, lognstat # name: # type: sq_string # elements: 1 # length: 42 Lognormal probability distribution object. # name: # type: sq_string # elements: 1 # length: 43 prob.LognormalDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 191 prob.LognormalDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 36 prob.LognormalDistribution.InputData # name: # type: sq_string # elements: 1 # length: 738 prob.LognormalDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: a numeric vector of logical values indicating censoring information corresponding to the elements of the data used for distribution fitting. If no censoring vector was used for distribution fitting, then this field defaults to an empty array. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 38 prob.LognormalDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 211 prob.LognormalDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 48 prob.LognormalDistribution.LognormalDistribution # name: # type: sq_string # elements: 1 # length: 386 prob.LognormalDistribution: pd = LognormalDistribution (mu, sigma) prob.LognormalDistribution: pd = LognormalDistribution () Create a prob.LognormalDistribution object. mu and sigma are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, mu 0 and sigma 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 43 Create a prob.LognormalDistribution object. # name: # type: sq_string # elements: 1 # length: 40 prob.LognormalDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 204 prob.LognormalDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 46 prob.LognormalDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 617 prob.LognormalDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 2x2 numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter, and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 47 prob.LognormalDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 228 prob.LognormalDistribution: property ParameterDescription Description of parameters A 2x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 43 prob.LognormalDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 284 prob.LognormalDistribution: property ParameterIsFixed Flag for fixed parameters A 1x2 logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 41 prob.LognormalDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 205 prob.LognormalDistribution: property ParameterNames Names of parameters A 2x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 42 prob.LognormalDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 285 prob.LognormalDistribution: property ParameterValues Distribution parameter values A 2x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the mu and sigma properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 37 prob.LognormalDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 348 prob.LognormalDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 30 prob.LognormalDistribution.cdf # name: # type: sq_string # elements: 1 # length: 388 prob.LognormalDistribution: p = cdf (pd, x) prob.LognormalDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 31 prob.LognormalDistribution.icdf # name: # type: sq_string # elements: 1 # length: 244 prob.LognormalDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 30 prob.LognormalDistribution.iqr # name: # type: sq_string # elements: 1 # length: 194 prob.LognormalDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.LognormalDistribution.mean # name: # type: sq_string # elements: 1 # length: 166 prob.LognormalDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 33 prob.LognormalDistribution.median # name: # type: sq_string # elements: 1 # length: 174 prob.LognormalDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 29 prob.LognormalDistribution.mu # name: # type: sq_string # elements: 1 # length: 221 prob.LognormalDistribution: property mu Mean of logarithmic values A scalar value characterizing the mean of the logarithmic values of the lognormal distribution. You can access the mu property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 26 Mean of logarithmic values # name: # type: sq_string # elements: 1 # length: 36 prob.LognormalDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 220 prob.LognormalDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 34 prob.LognormalDistribution.paramci # name: # type: sq_string # elements: 1 # length: 979 prob.LognormalDistribution: ci = paramci (pd) prob.LognormalDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 30 prob.LognormalDistribution.pdf # name: # type: sq_string # elements: 1 # length: 204 prob.LognormalDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 31 prob.LognormalDistribution.plot # name: # type: sq_string # elements: 1 # length: 1525 prob.LognormalDistribution: plot (pd) prob.LognormalDistribution: plot (pd, Name, Value) prob.LognormalDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 34 prob.LognormalDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2053 prob.LognormalDistribution: [nlogL, param] = proflik (pd, pnum) prob.LognormalDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.LognormalDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.LognormalDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.LognormalDistribution: [nlogL, param] = proflik (pd) prob.LognormalDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the Lognormal distribution, pnum = 1 selects the parameter mu and pnum = 2 selects the parameter sigma. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 33 prob.LognormalDistribution.random # name: # type: sq_string # elements: 1 # length: 695 prob.LognormalDistribution: r = random (pd) prob.LognormalDistribution: r = random (pd, rows) prob.LognormalDistribution: r = random (pd, rows, cols, ...) prob.LognormalDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, lognrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 32 prob.LognormalDistribution.sigma # name: # type: sq_string # elements: 1 # length: 264 prob.LognormalDistribution: property sigma Standard deviation of logarithmic values A positive scalar value characterizing the standard deviation of the logarithmic values of the lognormal distribution. You can access the sigma property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 40 Standard deviation of logarithmic values # name: # type: sq_string # elements: 1 # length: 30 prob.LognormalDistribution.std # name: # type: sq_string # elements: 1 # length: 192 prob.LognormalDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 35 prob.LognormalDistribution.truncate # name: # type: sq_string # elements: 1 # length: 535 prob.LognormalDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 30 prob.LognormalDistribution.var # name: # type: sq_string # elements: 1 # length: 172 prob.LognormalDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 27 prob.LoguniformDistribution # name: # type: sq_string # elements: 1 # length: 939 statistics: prob.LoguniformDistribution Log-uniform probability distribution object. A prob.LoguniformDistribution object consists of parameters and a model description for a log-uniform probability distribution. The log-uniform distribution is a continuous probability distribution that is constant between locations Lower and Upper on a logarithmic scale. There are several ways to create a prob.LoguniformDistribution object. Create a distribution with specified parameter values using the makedist function. Use the constructor prob.LoguniformDistribution (Lower, Upper) to create a log-uniform distribution with specified parameter values Lower and Upper. It is highly recommended to use makedist function to create probability distribution objects, instead of the class constructor. Further information about the log-uniform distribution can be found at https://en.wikipedia.org/wiki/Reciprocal_distribution See also: makedist # name: # type: sq_string # elements: 1 # length: 44 Log-uniform probability distribution object. # name: # type: sq_string # elements: 1 # length: 44 prob.LoguniformDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 192 prob.LoguniformDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 39 prob.LoguniformDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 212 prob.LoguniformDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 50 prob.LoguniformDistribution.LoguniformDistribution # name: # type: sq_string # elements: 1 # length: 400 prob.LoguniformDistribution: pd = LoguniformDistribution (Lower, Upper) prob.LoguniformDistribution: pd = LoguniformDistribution () Create a prob.LoguniformDistribution object. Lower and Upper are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, Lower 1 and Upper 4. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 44 Create a prob.LoguniformDistribution object. # name: # type: sq_string # elements: 1 # length: 33 prob.LoguniformDistribution.Lower # name: # type: sq_string # elements: 1 # length: 205 prob.LoguniformDistribution: property Lower Lower limit A positive scalar value characterizing the lower limit of the log-uniform distribution. You can access the Lower property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 11 Lower limit # name: # type: sq_string # elements: 1 # length: 41 prob.LoguniformDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 205 prob.LoguniformDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 48 prob.LoguniformDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 229 prob.LoguniformDistribution: property ParameterDescription Description of parameters A 2x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 42 prob.LoguniformDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 206 prob.LoguniformDistribution: property ParameterNames Names of parameters A 2x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 43 prob.LoguniformDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 289 prob.LoguniformDistribution: property ParameterValues Distribution parameter values A 2x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the Lower and Upper properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 38 prob.LoguniformDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 349 prob.LoguniformDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 33 prob.LoguniformDistribution.Upper # name: # type: sq_string # elements: 1 # length: 205 prob.LoguniformDistribution: property Upper Upper limit A positive scalar value characterizing the upper limit of the log-uniform distribution. You can access the Upper property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 11 Upper limit # name: # type: sq_string # elements: 1 # length: 31 prob.LoguniformDistribution.cdf # name: # type: sq_string # elements: 1 # length: 472 prob.LoguniformDistribution: p = cdf (pd, x) prob.LoguniformDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. x must be double or single; integer, logical, and character arrays are rejected. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 32 prob.LoguniformDistribution.icdf # name: # type: sq_string # elements: 1 # length: 327 prob.LoguniformDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. p must be double or single; integer, logical, and character arrays are rejected. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 31 prob.LoguniformDistribution.iqr # name: # type: sq_string # elements: 1 # length: 195 prob.LoguniformDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.LoguniformDistribution.mean # name: # type: sq_string # elements: 1 # length: 167 prob.LoguniformDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 34 prob.LoguniformDistribution.median # name: # type: sq_string # elements: 1 # length: 175 prob.LoguniformDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.LoguniformDistribution.pdf # name: # type: sq_string # elements: 1 # length: 287 prob.LoguniformDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. x must be double or single; integer, logical, and character arrays are rejected. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 32 prob.LoguniformDistribution.plot # name: # type: sq_string # elements: 1 # length: 1101 prob.LoguniformDistribution: plot (pd) prob.LoguniformDistribution: plot (pd, Name, Value) prob.LoguniformDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). 'cdf' plots the cumulative density function (CDF). 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 34 prob.LoguniformDistribution.random # name: # type: sq_string # elements: 1 # length: 698 prob.LoguniformDistribution: r = random (pd) prob.LoguniformDistribution: r = random (pd, rows) prob.LoguniformDistribution: r = random (pd, rows, cols, ...) prob.LoguniformDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, random returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 31 prob.LoguniformDistribution.std # name: # type: sq_string # elements: 1 # length: 193 prob.LoguniformDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 36 prob.LoguniformDistribution.truncate # name: # type: sq_string # elements: 1 # length: 294 prob.LoguniformDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.LoguniformDistribution.var # name: # type: sq_string # elements: 1 # length: 173 prob.LoguniformDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 28 prob.MultinomialDistribution # name: # type: sq_string # elements: 1 # length: 1080 statistics: prob.MultinomialDistribution Multinomial probability distribution object. A prob.MultinomialDistribution object consists of parameters, a model description, and sample data for a multinomial probability distribution. The multinomial distribution is a discrete probability distribution that models the outcomes of n independent trials of a k-category system, where each trial has a probability of falling into each category. It is defined by the vector of probabilities for each outcome. There are several ways to create a prob.MultinomialDistribution object. Create a distribution with specified parameter values using the makedist function. Use the constructor prob.MultinomialDistribution (Probabilities) to create a multinomial distribution with specified parameter values. It is highly recommended to use the makedist function to create probability distribution objects, instead of the constructor. Further information about the multinomial distribution can be found at https://en.wikipedia.org/wiki/Multinomial_distribution See also: makedist, mnpdf, mnrnd # name: # type: sq_string # elements: 1 # length: 44 Multinomial probability distribution object. # name: # type: sq_string # elements: 1 # length: 45 prob.MultinomialDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 193 prob.MultinomialDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 40 prob.MultinomialDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 213 prob.MultinomialDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 52 prob.MultinomialDistribution.MultinomialDistribution # name: # type: sq_string # elements: 1 # length: 404 prob.MultinomialDistribution: pd = MultinomialDistribution (Probabilities) prob.MultinomialDistribution: pd = MultinomialDistribution () Create a prob.MultinomialDistribution object. Probabilities is the distribution parameter, which the class help describes. Called with no arguments the parameter takes its default, Probabilities [0.5, 0.5]. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 45 Create a prob.MultinomialDistribution object. # name: # type: sq_string # elements: 1 # length: 42 prob.MultinomialDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 206 prob.MultinomialDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 49 prob.MultinomialDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 230 prob.MultinomialDistribution: property ParameterDescription Description of parameters A 1x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 43 prob.MultinomialDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 207 prob.MultinomialDistribution: property ParameterNames Names of parameters A 1x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 44 prob.MultinomialDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 282 prob.MultinomialDistribution: property ParameterValues Distribution parameter values A numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the Probabilities property. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 42 prob.MultinomialDistribution.Probabilities # name: # type: sq_string # elements: 1 # length: 192 prob.MultinomialDistribution: property Probabilities Outcome probabilities A row vector of probabilities for each outcome. You can access the Probabilities property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 21 Outcome probabilities # name: # type: sq_string # elements: 1 # length: 39 prob.MultinomialDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 350 prob.MultinomialDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 32 prob.MultinomialDistribution.cdf # name: # type: sq_string # elements: 1 # length: 558 prob.MultinomialDistribution: p = cdf (pd, x) prob.MultinomialDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. x must be double, single, or an integer type; logical and character arrays are rejected. Integer input is promoted to double, so the result is always a probability. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 33 prob.MultinomialDistribution.icdf # name: # type: sq_string # elements: 1 # length: 328 prob.MultinomialDistribution: p = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). p = icdf (pd, x) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in x. p must be double or single; integer, logical, and character arrays are rejected. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 32 prob.MultinomialDistribution.iqr # name: # type: sq_string # elements: 1 # length: 196 prob.MultinomialDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 33 prob.MultinomialDistribution.mean # name: # type: sq_string # elements: 1 # length: 168 prob.MultinomialDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 35 prob.MultinomialDistribution.median # name: # type: sq_string # elements: 1 # length: 176 prob.MultinomialDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.MultinomialDistribution.pdf # name: # type: sq_string # elements: 1 # length: 372 prob.MultinomialDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. x must be double, single, or an integer type; logical and character arrays are rejected. Integer input is promoted to double, so the result is always a probability. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 33 prob.MultinomialDistribution.plot # name: # type: sq_string # elements: 1 # length: 1530 prob.MultinomialDistribution: plot (pd) prob.MultinomialDistribution: plot (pd, Name, Value) prob.MultinomialDistribution: h = plot (...) Plot a probability distribution object. plot (pd plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 35 prob.MultinomialDistribution.random # name: # type: sq_string # elements: 1 # length: 701 prob.MultinomialDistribution: y = random (pd) prob.MultinomialDistribution: y = random (pd, rows) prob.MultinomialDistribution: y = random (pd, rows, cols, ...) prob.MultinomialDistribution: y = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, mnrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 32 prob.MultinomialDistribution.std # name: # type: sq_string # elements: 1 # length: 194 prob.MultinomialDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 37 prob.MultinomialDistribution.truncate # name: # type: sq_string # elements: 1 # length: 523 prob.MultinomialDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.MultinomialDistribution.var # name: # type: sq_string # elements: 1 # length: 184 prob.MultinomialDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 25 prob.NakagamiDistribution # name: # type: sq_string # elements: 1 # length: 1342 statistics: prob.NakagamiDistribution Nakagami probability distribution object. A prob.NakagamiDistribution object consists of parameters, a model description, and sample data for a Nakagami probability distribution. The Nakagami distribution is a continuous probability distribution that models the amplitude of received signals after maximum ratio diversity combining. It is defined by shape parameter mu and spread parameter omega. There are several ways to create a prob.NakagamiDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.NakagamiDistribution (mu, omega) to create a Nakagami distribution with fixed parameter values mu and omega. Use the static method prob.NakagamiDistribution.fit (x, censor, freq, options) to fit a distribution to the data in x using the same input arguments as the nakafit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the Nakagami distribution can be found at https://en.wikipedia.org/wiki/Nakagami_distribution See also: fitdist, makedist, nakacdf, nakainv, nakapdf, nakarnd, nakafit, nakalike, nakastat # name: # type: sq_string # elements: 1 # length: 41 Nakagami probability distribution object. # name: # type: sq_string # elements: 1 # length: 42 prob.NakagamiDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 190 prob.NakagamiDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 35 prob.NakagamiDistribution.InputData # name: # type: sq_string # elements: 1 # length: 737 prob.NakagamiDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: a numeric vector of logical values indicating censoring information corresponding to the elements of the data used for distribution fitting. If no censoring vector was used for distribution fitting, then this field defaults to an empty array. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 37 prob.NakagamiDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 210 prob.NakagamiDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 46 prob.NakagamiDistribution.NakagamiDistribution # name: # type: sq_string # elements: 1 # length: 381 prob.NakagamiDistribution: pd = NakagamiDistribution (mu, omega) prob.NakagamiDistribution: pd = NakagamiDistribution () Create a prob.NakagamiDistribution object. mu and omega are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, mu 1 and omega 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 42 Create a prob.NakagamiDistribution object. # name: # type: sq_string # elements: 1 # length: 39 prob.NakagamiDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 203 prob.NakagamiDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 45 prob.NakagamiDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 616 prob.NakagamiDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 2x2 numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter, and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 46 prob.NakagamiDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 227 prob.NakagamiDistribution: property ParameterDescription Description of parameters A 2x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 42 prob.NakagamiDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 283 prob.NakagamiDistribution: property ParameterIsFixed Flag for fixed parameters A 1x2 logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 40 prob.NakagamiDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 204 prob.NakagamiDistribution: property ParameterNames Names of parameters A 2x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 41 prob.NakagamiDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 284 prob.NakagamiDistribution: property ParameterValues Distribution parameter values A 2x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the mu and omega properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 36 prob.NakagamiDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 347 prob.NakagamiDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 29 prob.NakagamiDistribution.cdf # name: # type: sq_string # elements: 1 # length: 386 prob.NakagamiDistribution: p = cdf (pd, x) prob.NakagamiDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 30 prob.NakagamiDistribution.icdf # name: # type: sq_string # elements: 1 # length: 243 prob.NakagamiDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 29 prob.NakagamiDistribution.iqr # name: # type: sq_string # elements: 1 # length: 193 prob.NakagamiDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 30 prob.NakagamiDistribution.mean # name: # type: sq_string # elements: 1 # length: 165 prob.NakagamiDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.NakagamiDistribution.median # name: # type: sq_string # elements: 1 # length: 173 prob.NakagamiDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 28 prob.NakagamiDistribution.mu # name: # type: sq_string # elements: 1 # length: 192 prob.NakagamiDistribution: property mu Shape parameter A positive scalar value characterizing the shape of the Nakagami distribution. You can access the mu property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Shape parameter # name: # type: sq_string # elements: 1 # length: 35 prob.NakagamiDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 219 prob.NakagamiDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.NakagamiDistribution.omega # name: # type: sq_string # elements: 1 # length: 200 prob.NakagamiDistribution: property omega Spread parameter A positive scalar value characterizing the spread of the Nakagami distribution. You can access the omega property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 16 Spread parameter # name: # type: sq_string # elements: 1 # length: 33 prob.NakagamiDistribution.paramci # name: # type: sq_string # elements: 1 # length: 977 prob.NakagamiDistribution: ci = paramci (pd) prob.NakagamiDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 29 prob.NakagamiDistribution.pdf # name: # type: sq_string # elements: 1 # length: 203 prob.NakagamiDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 30 prob.NakagamiDistribution.plot # name: # type: sq_string # elements: 1 # length: 1522 prob.NakagamiDistribution: plot (pd) prob.NakagamiDistribution: plot (pd, Name, Value) prob.NakagamiDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 33 prob.NakagamiDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2046 prob.NakagamiDistribution: [nlogL, param] = proflik (pd, pnum) prob.NakagamiDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.NakagamiDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.NakagamiDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.NakagamiDistribution: [nlogL, param] = proflik (pd) prob.NakagamiDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the Nakagami distribution, pnum = 1 selects the parameter mu and pnum = 2 selects the parameter omega. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 32 prob.NakagamiDistribution.random # name: # type: sq_string # elements: 1 # length: 691 prob.NakagamiDistribution: r = random (pd) prob.NakagamiDistribution: r = random (pd, rows) prob.NakagamiDistribution: r = random (pd, rows, cols, ...) prob.NakagamiDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, betarnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 29 prob.NakagamiDistribution.std # name: # type: sq_string # elements: 1 # length: 191 prob.NakagamiDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 34 prob.NakagamiDistribution.truncate # name: # type: sq_string # elements: 1 # length: 534 prob.NakagamiDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 29 prob.NakagamiDistribution.var # name: # type: sq_string # elements: 1 # length: 171 prob.NakagamiDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 33 prob.NegativeBinomialDistribution # name: # type: sq_string # elements: 1 # length: 1511 statistics: prob.NegativeBinomialDistribution Negative binomial probability distribution object. A prob.NegativeBinomialDistribution object consists of parameters, a model description, and sample data for a negative binomial probability distribution. The negative binomial distribution is a discrete probability distribution that models the number of failures in a sequence of independent and identically distributed Bernoulli trials before a specified (non-random) number of successes occurs. It is defined by the number of successes R and the probability of success P. There are several ways to create a prob.NegativeBinomialDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.NegativeBinomialDistribution (R, P) to create a negative binomial distribution with fixed parameter values R and P. Use the static method prob.NegativeBinomialDistribution.fit (x, freq, options) to fit a distribution to the data in x using the same input arguments as the nbinfit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the negative binomial distribution can be found at https://en.wikipedia.org/wiki/Negative_binomial_distribution See also: fitdist, makedist, nbincdf, nbininv, nbinpdf, nbinrnd, nbinfit, nbinlike, nbinstat # name: # type: sq_string # elements: 1 # length: 50 Negative binomial probability distribution object. # name: # type: sq_string # elements: 1 # length: 50 prob.NegativeBinomialDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 198 prob.NegativeBinomialDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 43 prob.NegativeBinomialDistribution.InputData # name: # type: sq_string # elements: 1 # length: 745 prob.NegativeBinomialDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: a numeric vector of logical values indicating censoring information corresponding to the elements of the data used for distribution fitting. If no censoring vector was used for distribution fitting, then this field defaults to an empty array. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 45 prob.NegativeBinomialDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 218 prob.NegativeBinomialDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 62 prob.NegativeBinomialDistribution.NegativeBinomialDistribution # name: # type: sq_string # elements: 1 # length: 408 prob.NegativeBinomialDistribution: pd = NegativeBinomialDistribution (R, P) prob.NegativeBinomialDistribution: pd = NegativeBinomialDistribution () Create a prob.NegativeBinomialDistribution object. R and P are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, R 1 and P 0.5. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 50 Create a prob.NegativeBinomialDistribution object. # name: # type: sq_string # elements: 1 # length: 47 prob.NegativeBinomialDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 211 prob.NegativeBinomialDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 35 prob.NegativeBinomialDistribution.P # name: # type: sq_string # elements: 1 # length: 222 prob.NegativeBinomialDistribution: property P Probability of success A scalar value characterizing the probability of success in the negative binomial distribution. You can access the P property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 22 Probability of success # name: # type: sq_string # elements: 1 # length: 53 prob.NegativeBinomialDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 624 prob.NegativeBinomialDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 2x2 numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter, and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 54 prob.NegativeBinomialDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 235 prob.NegativeBinomialDistribution: property ParameterDescription Description of parameters A 2x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 50 prob.NegativeBinomialDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 291 prob.NegativeBinomialDistribution: property ParameterIsFixed Flag for fixed parameters A 1x2 logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 48 prob.NegativeBinomialDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 212 prob.NegativeBinomialDistribution: property ParameterNames Names of parameters A 2x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 49 prob.NegativeBinomialDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 287 prob.NegativeBinomialDistribution: property ParameterValues Distribution parameter values A 2x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the R and P properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 35 prob.NegativeBinomialDistribution.R # name: # type: sq_string # elements: 1 # length: 216 prob.NegativeBinomialDistribution: property R Number of successes A scalar value characterizing the number of successes in the negative binomial distribution. You can access the R property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 19 Number of successes # name: # type: sq_string # elements: 1 # length: 44 prob.NegativeBinomialDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 355 prob.NegativeBinomialDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 37 prob.NegativeBinomialDistribution.cdf # name: # type: sq_string # elements: 1 # length: 402 prob.NegativeBinomialDistribution: p = cdf (pd, x) prob.NegativeBinomialDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 38 prob.NegativeBinomialDistribution.icdf # name: # type: sq_string # elements: 1 # length: 251 prob.NegativeBinomialDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 37 prob.NegativeBinomialDistribution.iqr # name: # type: sq_string # elements: 1 # length: 201 prob.NegativeBinomialDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 38 prob.NegativeBinomialDistribution.mean # name: # type: sq_string # elements: 1 # length: 173 prob.NegativeBinomialDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 40 prob.NegativeBinomialDistribution.median # name: # type: sq_string # elements: 1 # length: 181 prob.NegativeBinomialDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 43 prob.NegativeBinomialDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 227 prob.NegativeBinomialDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 41 prob.NegativeBinomialDistribution.paramci # name: # type: sq_string # elements: 1 # length: 993 prob.NegativeBinomialDistribution: ci = paramci (pd) prob.NegativeBinomialDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 37 prob.NegativeBinomialDistribution.pdf # name: # type: sq_string # elements: 1 # length: 211 prob.NegativeBinomialDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 38 prob.NegativeBinomialDistribution.plot # name: # type: sq_string # elements: 1 # length: 1546 prob.NegativeBinomialDistribution: plot (pd) prob.NegativeBinomialDistribution: plot (pd, Name, Value) prob.NegativeBinomialDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 41 prob.NegativeBinomialDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2098 prob.NegativeBinomialDistribution: [nlogL, param] = proflik (pd, pnum) prob.NegativeBinomialDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.NegativeBinomialDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.NegativeBinomialDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.NegativeBinomialDistribution: [nlogL, param] = proflik (pd) prob.NegativeBinomialDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the negative binomial distribution, pnum = 1 selects the parameter R and pnum = 2 selects the parameter P. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 40 prob.NegativeBinomialDistribution.random # name: # type: sq_string # elements: 1 # length: 724 prob.NegativeBinomialDistribution: r = random (pd) prob.NegativeBinomialDistribution: r = random (pd, rows) prob.NegativeBinomialDistribution: r = random (pd, rows, cols, ...) prob.NegativeBinomialDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, nbindrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 37 prob.NegativeBinomialDistribution.std # name: # type: sq_string # elements: 1 # length: 199 prob.NegativeBinomialDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 42 prob.NegativeBinomialDistribution.truncate # name: # type: sq_string # elements: 1 # length: 542 prob.NegativeBinomialDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 37 prob.NegativeBinomialDistribution.var # name: # type: sq_string # elements: 1 # length: 179 prob.NegativeBinomialDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 23 prob.NormalDistribution # name: # type: sq_string # elements: 1 # length: 1304 statistics: prob.NormalDistribution Normal probability distribution object. A prob.NormalDistribution object consists of parameters, a model description, and sample data for a normal probability distribution. The normal distribution is a continuous probability distribution that is symmetric about the mean, mu, showing that data near the mean are more frequent in occurrence than data far from the mean. It is defined by location parameter mu and scale parameter sigma. There are several ways to create a prob.NormalDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.NormalDistribution (mu, sigma) to create a normal distribution with fixed parameter values mu and sigma. Use the static method prob.NormalDistribution.fit (x, censor, freq, options) to fit a distribution to data x. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the normal distribution can be found at https://en.wikipedia.org/wiki/Normal_distribution See also: fitdist, makedist, normcdf, norminv, normpdf, normrnd, normfit, normlike, normstat # name: # type: sq_string # elements: 1 # length: 39 Normal probability distribution object. # name: # type: sq_string # elements: 1 # length: 40 prob.NormalDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 188 prob.NormalDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 33 prob.NormalDistribution.InputData # name: # type: sq_string # elements: 1 # length: 735 prob.NormalDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: a numeric vector of logical values indicating censoring information corresponding to the elements of the data used for distribution fitting. If no censoring vector was used for distribution fitting, then this field defaults to an empty array. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 35 prob.NormalDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 208 prob.NormalDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 42 prob.NormalDistribution.NormalDistribution # name: # type: sq_string # elements: 1 # length: 371 prob.NormalDistribution: pd = NormalDistribution (mu, sigma) prob.NormalDistribution: pd = NormalDistribution () Create a prob.NormalDistribution object. mu and sigma are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, mu 0 and sigma 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 40 Create a prob.NormalDistribution object. # name: # type: sq_string # elements: 1 # length: 37 prob.NormalDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 201 prob.NormalDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 43 prob.NormalDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 614 prob.NormalDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 2x2 numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter, and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 44 prob.NormalDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 225 prob.NormalDistribution: property ParameterDescription Description of parameters A 2x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 40 prob.NormalDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 281 prob.NormalDistribution: property ParameterIsFixed Flag for fixed parameters A 1x2 logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 38 prob.NormalDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 202 prob.NormalDistribution: property ParameterNames Names of parameters A 2x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 39 prob.NormalDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 282 prob.NormalDistribution: property ParameterValues Distribution parameter values A 2x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the mu and sigma properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 34 prob.NormalDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 345 prob.NormalDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 27 prob.NormalDistribution.cdf # name: # type: sq_string # elements: 1 # length: 382 prob.NormalDistribution: p = cdf (pd, x) prob.NormalDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 28 prob.NormalDistribution.icdf # name: # type: sq_string # elements: 1 # length: 241 prob.NormalDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 27 prob.NormalDistribution.iqr # name: # type: sq_string # elements: 1 # length: 191 prob.NormalDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 28 prob.NormalDistribution.mean # name: # type: sq_string # elements: 1 # length: 163 prob.NormalDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 30 prob.NormalDistribution.median # name: # type: sq_string # elements: 1 # length: 171 prob.NormalDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 26 prob.NormalDistribution.mu # name: # type: sq_string # elements: 1 # length: 185 prob.NormalDistribution: property mu Location parameter A scalar value characterizing the location of the normal distribution. You can access the mu property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 18 Location parameter # name: # type: sq_string # elements: 1 # length: 33 prob.NormalDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 217 prob.NormalDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.NormalDistribution.paramci # name: # type: sq_string # elements: 1 # length: 973 prob.NormalDistribution: ci = paramci (pd) prob.NormalDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 27 prob.NormalDistribution.pdf # name: # type: sq_string # elements: 1 # length: 201 prob.NormalDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 28 prob.NormalDistribution.plot # name: # type: sq_string # elements: 1 # length: 1516 prob.NormalDistribution: plot (pd) prob.NormalDistribution: plot (pd, Name, Value) prob.NormalDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 31 prob.NormalDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2032 prob.NormalDistribution: [nlogL, param] = proflik (pd, pnum) prob.NormalDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.NormalDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.NormalDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.NormalDistribution: [nlogL, param] = proflik (pd) prob.NormalDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the normal distribution, pnum = 1 selects the parameter mu and pnum = 2 selects the parameter sigma. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 30 prob.NormalDistribution.random # name: # type: sq_string # elements: 1 # length: 683 prob.NormalDistribution: r = random (pd) prob.NormalDistribution: r = random (pd, rows) prob.NormalDistribution: r = random (pd, rows, cols, ...) prob.NormalDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, normrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 29 prob.NormalDistribution.sigma # name: # type: sq_string # elements: 1 # length: 194 prob.NormalDistribution: property sigma Scale parameter A positive scalar value characterizing the scale of the normal distribution. You can access the sigma property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Scale parameter # name: # type: sq_string # elements: 1 # length: 27 prob.NormalDistribution.std # name: # type: sq_string # elements: 1 # length: 189 prob.NormalDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.NormalDistribution.truncate # name: # type: sq_string # elements: 1 # length: 532 prob.NormalDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 27 prob.NormalDistribution.var # name: # type: sq_string # elements: 1 # length: 169 prob.NormalDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.PiecewiseLinearDistribution # name: # type: sq_string # elements: 1 # length: 1178 statistics: prob.PiecewiseLinearDistribution Piecewise linear probability distribution object. A prob.PiecewiseLinearDistribution object consists of parameters, a model description, and sample data for a piecewise linear probability distribution. The piecewise linear distribution is a continuous probability distribution that is defined by a set of points where the cumulative distribution function (CDF) changes slope. It is defined by a vector of x values and a corresponding vector of CDF values Fx. There are several ways to create a prob.PiecewiseLinearDistribution object. Create a distribution with specified parameter values using the makedist function. Use the constructor prob.PiecewiseLinearDistribution (x, Fx) to create a piecewise linear distribution with specified parameter values x and Fx. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the piecewise linear distribution can be found at https://en.wikipedia.org/wiki/Piecewise_linear_function See also: makedist, plcdf, plinv, plpdf, plrnd, plstat # name: # type: sq_string # elements: 1 # length: 49 Piecewise linear probability distribution object. # name: # type: sq_string # elements: 1 # length: 49 prob.PiecewiseLinearDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 197 prob.PiecewiseLinearDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 35 prob.PiecewiseLinearDistribution.Fx # name: # type: sq_string # elements: 1 # length: 242 prob.PiecewiseLinearDistribution: property Fx Vector of CDF values A numeric row vector of CDF values that correspond to each value in x, reported as a row whichever way it was given. You can access the Fx property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 20 Vector of CDF values # name: # type: sq_string # elements: 1 # length: 44 prob.PiecewiseLinearDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 217 prob.PiecewiseLinearDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 46 prob.PiecewiseLinearDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 210 prob.PiecewiseLinearDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 53 prob.PiecewiseLinearDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 234 prob.PiecewiseLinearDistribution: property ParameterDescription Description of parameters A 2x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 47 prob.PiecewiseLinearDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 211 prob.PiecewiseLinearDistribution: property ParameterNames Names of parameters A 2x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 48 prob.PiecewiseLinearDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 287 prob.PiecewiseLinearDistribution: property ParameterValues Distribution parameter values A 2x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the x and Fx properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 60 prob.PiecewiseLinearDistribution.PiecewiseLinearDistribution # name: # type: sq_string # elements: 1 # length: 414 prob.PiecewiseLinearDistribution: pd = PiecewiseLinearDistribution (x, Fx) prob.PiecewiseLinearDistribution: pd = PiecewiseLinearDistribution () Create a prob.PiecewiseLinearDistribution object. x and Fx are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, x [0; 1] and Fx [0; 1]. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 49 Create a prob.PiecewiseLinearDistribution object. # name: # type: sq_string # elements: 1 # length: 43 prob.PiecewiseLinearDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 354 prob.PiecewiseLinearDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 36 prob.PiecewiseLinearDistribution.cdf # name: # type: sq_string # elements: 1 # length: 400 prob.PiecewiseLinearDistribution: p = cdf (pd, x) prob.PiecewiseLinearDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 37 prob.PiecewiseLinearDistribution.icdf # name: # type: sq_string # elements: 1 # length: 250 prob.PiecewiseLinearDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 36 prob.PiecewiseLinearDistribution.iqr # name: # type: sq_string # elements: 1 # length: 200 prob.PiecewiseLinearDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 37 prob.PiecewiseLinearDistribution.mean # name: # type: sq_string # elements: 1 # length: 172 prob.PiecewiseLinearDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 39 prob.PiecewiseLinearDistribution.median # name: # type: sq_string # elements: 1 # length: 180 prob.PiecewiseLinearDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 36 prob.PiecewiseLinearDistribution.pdf # name: # type: sq_string # elements: 1 # length: 210 prob.PiecewiseLinearDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 37 prob.PiecewiseLinearDistribution.plot # name: # type: sq_string # elements: 1 # length: 1543 prob.PiecewiseLinearDistribution: plot (pd) prob.PiecewiseLinearDistribution: plot (pd, Name, Value) prob.PiecewiseLinearDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 39 prob.PiecewiseLinearDistribution.random # name: # type: sq_string # elements: 1 # length: 719 prob.PiecewiseLinearDistribution: r = random (pd) prob.PiecewiseLinearDistribution: r = random (pd, rows) prob.PiecewiseLinearDistribution: r = random (pd, rows, cols, ...) prob.PiecewiseLinearDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, betarnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 36 prob.PiecewiseLinearDistribution.std # name: # type: sq_string # elements: 1 # length: 198 prob.PiecewiseLinearDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 41 prob.PiecewiseLinearDistribution.truncate # name: # type: sq_string # elements: 1 # length: 527 prob.PiecewiseLinearDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 36 prob.PiecewiseLinearDistribution.var # name: # type: sq_string # elements: 1 # length: 188 prob.PiecewiseLinearDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 34 prob.PiecewiseLinearDistribution.x # name: # type: sq_string # elements: 1 # length: 232 prob.PiecewiseLinearDistribution: property x Vector of x values A numeric row vector of x values at which the CDF changes slope, reported as a row whichever way it was given. You can access the x property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 18 Vector of x values # name: # type: sq_string # elements: 1 # length: 24 prob.PoissonDistribution # name: # type: sq_string # elements: 1 # length: 1328 statistics: prob.PoissonDistribution Poisson probability distribution object. A prob.PoissonDistribution object consists of parameters, a model description, and sample data for a Poisson probability distribution. The Poisson distribution is a discrete probability distribution that models the number of events occurring in a fixed interval of time or space, given a constant average rate of occurrence. It is defined by the rate parameter lambda. There are several ways to create a prob.PoissonDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.PoissonDistribution (lambda) to create a Poisson distribution with fixed parameter value lambda. Use the static method prob.PoissonDistribution.fit (x, freq) to fit a distribution to the data in x using the same input arguments as the poissfit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the Poisson distribution can be found at https://en.wikipedia.org/wiki/Poisson_distribution See also: fitdist, makedist, poisscdf, poissinv, poisspdf, poissrnd, poissfit, poisslike, poisstat # name: # type: sq_string # elements: 1 # length: 40 Poisson probability distribution object. # name: # type: sq_string # elements: 1 # length: 41 prob.PoissonDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 189 prob.PoissonDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 34 prob.PoissonDistribution.InputData # name: # type: sq_string # elements: 1 # length: 736 prob.PoissonDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: a numeric vector of logical values indicating censoring information corresponding to the elements of the data used for distribution fitting. If no censoring vector was used for distribution fitting, then this field defaults to an empty array. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 36 prob.PoissonDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 209 prob.PoissonDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 38 prob.PoissonDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 202 prob.PoissonDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 44 prob.PoissonDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 415 prob.PoissonDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 1x1 numeric matrix containing the variance of the parameter estimate. This matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then the variance is zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 45 prob.PoissonDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 226 prob.PoissonDistribution: property ParameterDescription Description of parameters A 1x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 41 prob.PoissonDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 277 prob.PoissonDistribution: property ParameterIsFixed Flag for fixed parameters A logical scalar specifying whether the parameter is fixed or estimated. A true value corresponds to a fixed parameter, a false value corresponds to a parameter estimate. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 39 prob.PoissonDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 203 prob.PoissonDistribution: property ParameterNames Names of parameters A 1x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 40 prob.PoissonDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 275 prob.PoissonDistribution: property ParameterValues Distribution parameter values A 1x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the lambda property. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 44 prob.PoissonDistribution.PoissonDistribution # name: # type: sq_string # elements: 1 # length: 354 prob.PoissonDistribution: pd = PoissonDistribution (lambda) prob.PoissonDistribution: pd = PoissonDistribution () Create a prob.PoissonDistribution object. lambda is the distribution parameter, which the class help describes. Called with no arguments the parameter takes its default, lambda 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 41 Create a prob.PoissonDistribution object. # name: # type: sq_string # elements: 1 # length: 35 prob.PoissonDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 354 prob.PoissonDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. The first element contains the lower boundary, the second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 28 prob.PoissonDistribution.cdf # name: # type: sq_string # elements: 1 # length: 384 prob.PoissonDistribution: p = cdf (pd, x) prob.PoissonDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 29 prob.PoissonDistribution.icdf # name: # type: sq_string # elements: 1 # length: 242 prob.PoissonDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 28 prob.PoissonDistribution.iqr # name: # type: sq_string # elements: 1 # length: 192 prob.PoissonDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.PoissonDistribution.lambda # name: # type: sq_string # elements: 1 # length: 196 prob.PoissonDistribution: property lambda Rate parameter A positive scalar value characterizing the rate of the Poisson distribution. You can access the lambda property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 14 Rate parameter # name: # type: sq_string # elements: 1 # length: 29 prob.PoissonDistribution.mean # name: # type: sq_string # elements: 1 # length: 164 prob.PoissonDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.PoissonDistribution.median # name: # type: sq_string # elements: 1 # length: 172 prob.PoissonDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 34 prob.PoissonDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 218 prob.PoissonDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.PoissonDistribution.paramci # name: # type: sq_string # elements: 1 # length: 975 prob.PoissonDistribution: ci = paramci (pd) prob.PoissonDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 28 prob.PoissonDistribution.pdf # name: # type: sq_string # elements: 1 # length: 202 prob.PoissonDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 29 prob.PoissonDistribution.plot # name: # type: sq_string # elements: 1 # length: 1459 prob.PoissonDistribution: plot (pd) prob.PoissonDistribution: plot (pd, Name, Value) prob.PoissonDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. 'Parent' An axes graphics object for the plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 32 prob.PoissonDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2002 prob.PoissonDistribution: [nlogL, param] = proflik (pd, pnum) prob.PoissonDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.PoissonDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.PoissonDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.PoissonDistribution: [nlogL, param] = proflik (pd) prob.PoissonDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the Poisson distribution, pnum = 1 selects the parameter lambda. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the user-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 31 prob.PoissonDistribution.random # name: # type: sq_string # elements: 1 # length: 688 prob.PoissonDistribution: r = random (pd) prob.PoissonDistribution: r = random (pd, rows) prob.PoissonDistribution: r = random (pd, rows, cols, ...) prob.PoissonDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, poissrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 28 prob.PoissonDistribution.std # name: # type: sq_string # elements: 1 # length: 190 prob.PoissonDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 33 prob.PoissonDistribution.truncate # name: # type: sq_string # elements: 1 # length: 533 prob.PoissonDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 28 prob.PoissonDistribution.var # name: # type: sq_string # elements: 1 # length: 170 prob.PoissonDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 28 prob.ProbabilityDistribution # name: # type: sq_string # elements: 1 # length: 757 statistics: prob.ProbabilityDistribution Abstract base class of the probability distribution objects. It holds the behaviour every distribution object shares -- how it is displayed, how its parameter confidence intervals are computed, how it is plotted and how a profile likelihood is taken -- as protected methods, so the 29 distribution classes inherit one implementation and no part of it is reachable from outside them. These helpers were ordinary files in a private directory until the classes moved into the prob namespace. Octave does not resolve a private directory from inside a package directory, for a classdef or for a plain function, so the only way to keep them out of the public interface is to make them protected methods of a shared base. # name: # type: sq_string # elements: 1 # length: 60 Abstract base class of the probability distribution objects. # name: # type: sq_string # elements: 1 # length: 25 prob.RayleighDistribution # name: # type: sq_string # elements: 1 # length: 1512 statistics: prob.RayleighDistribution Rayleigh probability distribution object. A prob.RayleighDistribution object consists of parameters, a model description, and sample data for a Rayleigh probability distribution. The Rayleigh distribution is a continuous probability distribution for nonnegative random variables. It is often used to model the magnitude of a vector in two dimensions where the components are normally distributed with zero mean and equal variance. It is defined by scale parameter B. B is the sigma of the usual mathematical notation. The rayl* functions name the same quantity sigma; this class follows MATLAB. There are several ways to create a prob.RayleighDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.RayleighDistribution (B) to create a Rayleigh distribution with fixed parameter value B. Use the static method prob.RayleighDistribution.fit (x, censor, freq) to fit a distribution to the data in x using the same input arguments as the raylfit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the Rayleigh distribution can be found at https://en.wikipedia.org/wiki/Rayleigh_distribution See also: fitdist, makedist, raylcdf, raylinv, raylpdf, raylrnd, raylfit, rayllike, raylstat # name: # type: sq_string # elements: 1 # length: 41 Rayleigh probability distribution object. # name: # type: sq_string # elements: 1 # length: 27 prob.RayleighDistribution.B # name: # type: sq_string # elements: 1 # length: 190 prob.RayleighDistribution: property B Scale parameter A positive scalar value characterizing the scale of the Rayleigh distribution. You can access the B property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Scale parameter # name: # type: sq_string # elements: 1 # length: 42 prob.RayleighDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 190 prob.RayleighDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 35 prob.RayleighDistribution.InputData # name: # type: sq_string # elements: 1 # length: 737 prob.RayleighDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: a numeric vector of logical values indicating censoring information corresponding to the elements of the data used for distribution fitting. If no censoring vector was used for distribution fitting, then this field defaults to an empty array. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 37 prob.RayleighDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 210 prob.RayleighDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 39 prob.RayleighDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 203 prob.RayleighDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 45 prob.RayleighDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 612 prob.RayleighDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter, and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 46 prob.RayleighDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 223 prob.RayleighDistribution: property ParameterDescription Description of parameters A cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 42 prob.RayleighDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 279 prob.RayleighDistribution: property ParameterIsFixed Flag for fixed parameters A logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 40 prob.RayleighDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 200 prob.RayleighDistribution: property ParameterNames Names of parameters A cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 41 prob.RayleighDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 267 prob.RayleighDistribution: property ParameterValues Distribution parameter values A numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the B property. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 46 prob.RayleighDistribution.RayleighDistribution # name: # type: sq_string # elements: 1 # length: 344 prob.RayleighDistribution: pd = RayleighDistribution (B) prob.RayleighDistribution: pd = RayleighDistribution () Create a prob.RayleighDistribution object. B is the distribution parameter, which the class help describes. Called with no arguments the parameter takes its default, B 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 42 Create a prob.RayleighDistribution object. # name: # type: sq_string # elements: 1 # length: 36 prob.RayleighDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 347 prob.RayleighDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 29 prob.RayleighDistribution.cdf # name: # type: sq_string # elements: 1 # length: 386 prob.RayleighDistribution: p = cdf (pd, x) prob.RayleighDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 30 prob.RayleighDistribution.icdf # name: # type: sq_string # elements: 1 # length: 243 prob.RayleighDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 29 prob.RayleighDistribution.iqr # name: # type: sq_string # elements: 1 # length: 193 prob.RayleighDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 30 prob.RayleighDistribution.mean # name: # type: sq_string # elements: 1 # length: 165 prob.RayleighDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.RayleighDistribution.median # name: # type: sq_string # elements: 1 # length: 173 prob.RayleighDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 35 prob.RayleighDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 219 prob.RayleighDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 33 prob.RayleighDistribution.paramci # name: # type: sq_string # elements: 1 # length: 977 prob.RayleighDistribution: ci = paramci (pd) prob.RayleighDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 29 prob.RayleighDistribution.pdf # name: # type: sq_string # elements: 1 # length: 203 prob.RayleighDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 30 prob.RayleighDistribution.plot # name: # type: sq_string # elements: 1 # length: 1522 prob.RayleighDistribution: plot (pd) prob.RayleighDistribution: plot (pd, Name, Value) prob.RayleighDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 33 prob.RayleighDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2004 prob.RayleighDistribution: [nlogL, param] = proflik (pd, pnum) prob.RayleighDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.RayleighDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.RayleighDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.RayleighDistribution: [nlogL, param] = proflik (pd) prob.RayleighDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the Rayleigh distribution, pnum = 1 selects the parameter B. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 32 prob.RayleighDistribution.random # name: # type: sq_string # elements: 1 # length: 691 prob.RayleighDistribution: r = random (pd) prob.RayleighDistribution: r = random (pd, rows) prob.RayleighDistribution: r = random (pd, rows, cols, ...) prob.RayleighDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, betarnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 29 prob.RayleighDistribution.std # name: # type: sq_string # elements: 1 # length: 191 prob.RayleighDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 34 prob.RayleighDistribution.truncate # name: # type: sq_string # elements: 1 # length: 534 prob.RayleighDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 29 prob.RayleighDistribution.var # name: # type: sq_string # elements: 1 # length: 171 prob.RayleighDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 23 prob.RicianDistribution # name: # type: sq_string # elements: 1 # length: 1246 statistics: prob.RicianDistribution Rician probability distribution object. A prob.RicianDistribution object consists of parameters, a model description, and sample data for a Rician probability distribution. The Rician distribution is a continuous probability distribution that models the magnitude of a signal in the presence of Gaussian noise. It is defined by noncentrality parameter s and scale parameter sigma. There are several ways to create a prob.RicianDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.RicianDistribution (s, sigma) to create a Rician distribution with fixed parameter values s and sigma. Use the static method prob.RicianDistribution.fit (x, censor, freq, options) to fit a distribution to data x. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the Rician distribution can be found at https://en.wikipedia.org/wiki/Rice_distribution See also: fitdist, makedist, ricecdf, riceinv, ricepdf, ricernd, ricefit, ricelike, ricestat # name: # type: sq_string # elements: 1 # length: 39 Rician probability distribution object. # name: # type: sq_string # elements: 1 # length: 40 prob.RicianDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 188 prob.RicianDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 33 prob.RicianDistribution.InputData # name: # type: sq_string # elements: 1 # length: 735 prob.RicianDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: a numeric vector of logical values indicating censoring information corresponding to the elements of the data used for distribution fitting. If no censoring vector was used for distribution fitting, then this field defaults to an empty array. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 35 prob.RicianDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 208 prob.RicianDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 37 prob.RicianDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 201 prob.RicianDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 43 prob.RicianDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 614 prob.RicianDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 2x2 numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter, and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 44 prob.RicianDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 225 prob.RicianDistribution: property ParameterDescription Description of parameters A 2x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 40 prob.RicianDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 281 prob.RicianDistribution: property ParameterIsFixed Flag for fixed parameters A 1x2 logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 38 prob.RicianDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 202 prob.RicianDistribution: property ParameterNames Names of parameters A 2x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 39 prob.RicianDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 281 prob.RicianDistribution: property ParameterValues Distribution parameter values A 2x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the s and sigma properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 42 prob.RicianDistribution.RicianDistribution # name: # type: sq_string # elements: 1 # length: 368 prob.RicianDistribution: pd = RicianDistribution (s, sigma) prob.RicianDistribution: pd = RicianDistribution () Create a prob.RicianDistribution object. s and sigma are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, s 1 and sigma 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 40 Create a prob.RicianDistribution object. # name: # type: sq_string # elements: 1 # length: 34 prob.RicianDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 345 prob.RicianDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 27 prob.RicianDistribution.cdf # name: # type: sq_string # elements: 1 # length: 382 prob.RicianDistribution: p = cdf (pd, x) prob.RicianDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 28 prob.RicianDistribution.icdf # name: # type: sq_string # elements: 1 # length: 241 prob.RicianDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 27 prob.RicianDistribution.iqr # name: # type: sq_string # elements: 1 # length: 191 prob.RicianDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 28 prob.RicianDistribution.mean # name: # type: sq_string # elements: 1 # length: 163 prob.RicianDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 30 prob.RicianDistribution.median # name: # type: sq_string # elements: 1 # length: 171 prob.RicianDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 33 prob.RicianDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 217 prob.RicianDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.RicianDistribution.paramci # name: # type: sq_string # elements: 1 # length: 973 prob.RicianDistribution: ci = paramci (pd) prob.RicianDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 27 prob.RicianDistribution.pdf # name: # type: sq_string # elements: 1 # length: 201 prob.RicianDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 28 prob.RicianDistribution.plot # name: # type: sq_string # elements: 1 # length: 1516 prob.RicianDistribution: plot (pd) prob.RicianDistribution: plot (pd, Name, Value) prob.RicianDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 31 prob.RicianDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2031 prob.RicianDistribution: [nlogL, param] = proflik (pd, pnum) prob.RicianDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.RicianDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.RicianDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.RicianDistribution: [nlogL, param] = proflik (pd) prob.RicianDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the Rician distribution, pnum = 1 selects the parameter s and pnum = 2 selects the parameter sigma. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 30 prob.RicianDistribution.random # name: # type: sq_string # elements: 1 # length: 683 prob.RicianDistribution: r = random (pd) prob.RicianDistribution: r = random (pd, rows) prob.RicianDistribution: r = random (pd, rows, cols, ...) prob.RicianDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, ricernd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 25 prob.RicianDistribution.s # name: # type: sq_string # elements: 1 # length: 206 prob.RicianDistribution: property s Noncentrality parameter A non-negative scalar value characterizing the noncentrality of the Rician distribution. You can access the s property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 23 Noncentrality parameter # name: # type: sq_string # elements: 1 # length: 29 prob.RicianDistribution.sigma # name: # type: sq_string # elements: 1 # length: 194 prob.RicianDistribution: property sigma Scale parameter A positive scalar value characterizing the scale of the Rician distribution. You can access the sigma property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Scale parameter # name: # type: sq_string # elements: 1 # length: 27 prob.RicianDistribution.std # name: # type: sq_string # elements: 1 # length: 189 prob.RicianDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.RicianDistribution.truncate # name: # type: sq_string # elements: 1 # length: 532 prob.RicianDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 27 prob.RicianDistribution.var # name: # type: sq_string # elements: 1 # length: 169 prob.RicianDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 23 prob.StableDistribution # name: # type: sq_string # elements: 1 # length: 1756 statistics: prob.StableDistribution Stable probability distribution object. A prob.StableDistribution object consists of parameters, a model description, and sample data for a stable probability distribution. The stable distribution is a continuous probability distribution family closed under linear combinations, generalizing the normal, Cauchy, and Levy distributions. It is parameterized, in the Nolan S0 parameterization, by a tail index (first shape parameter) alpha in (0, 2], a skewness (second shape parameter) beta in [-1, 1], a scale parameter gam greater than zero, and a location parameter delta. There are several ways to create a prob.StableDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.StableDistribution (alpha, beta, gam, delta) to create a stable distribution with fixed parameter values alpha, beta, gam, and delta. Use the static method prob.StableDistribution.fit (x, alpha, freq, options) to fit a distribution to the data in x using the same input arguments as the stblfit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Fitting is by maximum likelihood. Because the stable density has no closed form, it is evaluated by numerical inversion of the characteristic function, which makes fitting considerably slower than for the closed-form distributions. Further information about the stable distribution can be found at https://en.wikipedia.org/wiki/Stable_distribution See also: fitdist, makedist, stblpdf, stblcdf, stblinv, stblrnd, stblfit, stbllike # name: # type: sq_string # elements: 1 # length: 39 Stable probability distribution object. # name: # type: sq_string # elements: 1 # length: 40 prob.StableDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 188 prob.StableDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 33 prob.StableDistribution.InputData # name: # type: sq_string # elements: 1 # length: 259 prob.StableDistribution: property InputData Data used for fitting the distribution A structure containing the data used to fit the distribution. It is empty unless the distribution was fitted with fitdist or the static fit method. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Data used for fitting the distribution # name: # type: sq_string # elements: 1 # length: 35 prob.StableDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 172 prob.StableDistribution: property IsTruncated Flag for truncated distribution A logical scalar that is true when the distribution is truncated. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Flag for truncated distribution # name: # type: sq_string # elements: 1 # length: 37 prob.StableDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 201 prob.StableDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 43 prob.StableDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 215 prob.StableDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 4x4 numeric matrix containing the variance-covariance of the distribution parameters. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 44 prob.StableDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 225 prob.StableDistribution: property ParameterDescription Description of parameters A 4x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 40 prob.StableDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 193 prob.StableDistribution: property ParameterIsFixed Flags for fixed parameters A 4x1 logical vector specifying which parameters are held fixed rather than estimated. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Flags for fixed parameters # name: # type: sq_string # elements: 1 # length: 38 prob.StableDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 202 prob.StableDistribution: property ParameterNames Names of parameters A 4x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 39 prob.StableDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 292 prob.StableDistribution: property ParameterValues Distribution parameter values A 4x1 numeric vector containing the values of the distribution parameters, matching the order in ParameterNames. This property is read-only; use dot name assignment on the alpha, beta, gam, and delta properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 42 prob.StableDistribution.StableDistribution # name: # type: sq_string # elements: 1 # length: 417 prob.StableDistribution: pd = StableDistribution (alpha, beta, gam, delta) prob.StableDistribution: pd = StableDistribution () Create a prob.StableDistribution object. alpha, beta, gam and delta are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, alpha 2, beta 0, gam 1 and delta 0. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 40 Create a prob.StableDistribution object. # name: # type: sq_string # elements: 1 # length: 34 prob.StableDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 186 prob.StableDistribution: property Truncation Truncation interval A two-element numeric vector with the truncation interval, if the distribution is truncated. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 29 prob.StableDistribution.alpha # name: # type: sq_string # elements: 1 # length: 233 prob.StableDistribution: property alpha Tail index (first shape parameter) A scalar value in the range (0, 2] characterizing the tail behaviour of the stable distribution. You can access the alpha property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 34 Tail index (first shape parameter) # name: # type: sq_string # elements: 1 # length: 28 prob.StableDistribution.beta # name: # type: sq_string # elements: 1 # length: 225 prob.StableDistribution: property beta Skewness (second shape parameter) A scalar value in the range [-1, 1] characterizing the skewness of the stable distribution. You can access the beta property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 33 Skewness (second shape parameter) # name: # type: sq_string # elements: 1 # length: 27 prob.StableDistribution.cdf # name: # type: sq_string # elements: 1 # length: 313 prob.StableDistribution: p = cdf (pd, x) prob.StableDistribution: p = cdf (pd, x, "upper") Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. The optional "upper" flag computes the upper tail probability. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 29 prob.StableDistribution.delta # name: # type: sq_string # elements: 1 # length: 191 prob.StableDistribution: property delta Location parameter A scalar value characterizing the location of the stable distribution. You can access the delta property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 18 Location parameter # name: # type: sq_string # elements: 1 # length: 27 prob.StableDistribution.gam # name: # type: sq_string # elements: 1 # length: 190 prob.StableDistribution: property gam Scale parameter A positive scalar value characterizing the scale of the stable distribution. You can access the gam property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Scale parameter # name: # type: sq_string # elements: 1 # length: 28 prob.StableDistribution.icdf # name: # type: sq_string # elements: 1 # length: 241 prob.StableDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 27 prob.StableDistribution.iqr # name: # type: sq_string # elements: 1 # length: 191 prob.StableDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 28 prob.StableDistribution.mean # name: # type: sq_string # elements: 1 # length: 218 prob.StableDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. The mean is NaN for alpha <= 1, where it is undefined. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 30 prob.StableDistribution.median # name: # type: sq_string # elements: 1 # length: 171 prob.StableDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 33 prob.StableDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 274 prob.StableDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. It returns an empty value when pd is not fitted to data. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.StableDistribution.paramci # name: # type: sq_string # elements: 1 # length: 988 prob.StableDistribution: ci = paramci (pd) prob.StableDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise the parameter values are returned in both rows. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 27 prob.StableDistribution.pdf # name: # type: sq_string # elements: 1 # length: 196 prob.StableDistribution: y = pdf (pd, x) Compute the probability density function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 47 Compute the probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 28 prob.StableDistribution.plot # name: # type: sq_string # elements: 1 # length: 362 prob.StableDistribution: plot (pd) prob.StableDistribution: plot (pd, Name, Value) prob.StableDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots the probability density function (PDF) of the probability distribution object pd. Name-value pair arguments select the plotted function and its appearance, as documented in __plot__. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 31 prob.StableDistribution.proflik # name: # type: sq_string # elements: 1 # length: 1814 prob.StableDistribution: [nlogL, param] = proflik (pd, pnum) prob.StableDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.StableDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.StableDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.StableDistribution: [nlogL, param] = proflik (pd) prob.StableDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the stable distribution, pnum = 1 selects the tail index alpha, pnum = 2 selects the skewness beta, pnum = 3 selects the scale gam, and pnum = 4 selects the location delta. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 30 prob.StableDistribution.random # name: # type: sq_string # elements: 1 # length: 374 prob.StableDistribution: r = random (pd) prob.StableDistribution: r = random (pd, rows) prob.StableDistribution: r = random (pd, rows, cols, ...) prob.StableDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd, following the size conventions of stblrnd. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 27 prob.StableDistribution.std # name: # type: sq_string # elements: 1 # length: 246 prob.StableDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. It is NaN for alpha < 2, where the variance is infinite. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.StableDistribution.truncate # name: # type: sq_string # elements: 1 # length: 238 prob.StableDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns the probability distribution pd truncated to the interval with lower limit lower and upper limit upper. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 27 prob.StableDistribution.var # name: # type: sq_string # elements: 1 # length: 226 prob.StableDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. It is NaN for alpha < 2, where the variance is infinite. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 27 prob.TriangularDistribution # name: # type: sq_string # elements: 1 # length: 997 statistics: prob.TriangularDistribution Triangular probability distribution object. A prob.TriangularDistribution object consists of parameters, a model description, and sample data for a triangular probability distribution. The triangular distribution uses the following parameters. Parameter Description Support A Lower limit -Inf < A < Inf B Peak location A <= B <= C C Upper limit C > A There are several ways to create a prob.TriangularDistribution object. Create a distribution with specified parameter values using the makedist function. Use the constructor prob.TriangularDistribution (A, B, C) to create a triangular distribution with specified parameter values A, B, and C. It is highly recommended to use makedist function to create probability distribution objects, instead of the constructor. Further information about the triangular distribution can be found at https://en.wikipedia.org/wiki/Triangular_distribution See also: makedist, tricdf, triinv, tripdf, trirnd, tristat # name: # type: sq_string # elements: 1 # length: 43 Triangular probability distribution object. # name: # type: sq_string # elements: 1 # length: 29 prob.TriangularDistribution.A # name: # type: sq_string # elements: 1 # length: 197 prob.TriangularDistribution: property A Lower limit parameter A scalar value characterizing the lower limit of the triangular distribution. You can access the A property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 21 Lower limit parameter # name: # type: sq_string # elements: 1 # length: 29 prob.TriangularDistribution.B # name: # type: sq_string # elements: 1 # length: 201 prob.TriangularDistribution: property B Peak location parameter A scalar value characterizing the peak location of the triangular distribution. You can access the B property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 23 Peak location parameter # name: # type: sq_string # elements: 1 # length: 29 prob.TriangularDistribution.C # name: # type: sq_string # elements: 1 # length: 197 prob.TriangularDistribution: property C Upper limit parameter A scalar value characterizing the upper limit of the triangular distribution. You can access the C property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 21 Upper limit parameter # name: # type: sq_string # elements: 1 # length: 44 prob.TriangularDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 192 prob.TriangularDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 39 prob.TriangularDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 212 prob.TriangularDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 41 prob.TriangularDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 205 prob.TriangularDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 48 prob.TriangularDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 229 prob.TriangularDistribution: property ParameterDescription Description of parameters A 3x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 42 prob.TriangularDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 206 prob.TriangularDistribution: property ParameterNames Names of parameters A 3x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 43 prob.TriangularDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 285 prob.TriangularDistribution: property ParameterValues Distribution parameter values A 3x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the A, B, and C properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 50 prob.TriangularDistribution.TriangularDistribution # name: # type: sq_string # elements: 1 # length: 389 prob.TriangularDistribution: pd = TriangularDistribution (A, B, C) prob.TriangularDistribution: pd = TriangularDistribution () Create a prob.TriangularDistribution object. A, B and C are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, A 0, B 0.5 and C 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 44 Create a prob.TriangularDistribution object. # name: # type: sq_string # elements: 1 # length: 38 prob.TriangularDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 349 prob.TriangularDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 31 prob.TriangularDistribution.cdf # name: # type: sq_string # elements: 1 # length: 390 prob.TriangularDistribution: p = cdf (pd, x) prob.TriangularDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 32 prob.TriangularDistribution.icdf # name: # type: sq_string # elements: 1 # length: 245 prob.TriangularDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 31 prob.TriangularDistribution.iqr # name: # type: sq_string # elements: 1 # length: 195 prob.TriangularDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.TriangularDistribution.mean # name: # type: sq_string # elements: 1 # length: 167 prob.TriangularDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 34 prob.TriangularDistribution.median # name: # type: sq_string # elements: 1 # length: 175 prob.TriangularDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.TriangularDistribution.pdf # name: # type: sq_string # elements: 1 # length: 200 prob.TriangularDistribution: y = pdf (pd, x) Compute the probability density function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 47 Compute the probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 32 prob.TriangularDistribution.plot # name: # type: sq_string # elements: 1 # length: 1110 prob.TriangularDistribution: plot (pd) prob.TriangularDistribution: plot (pd, Name, Value) prob.TriangularDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). 'cdf' plots the cumulative density function (CDF). 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, this option is ignored. 'Parent' An axes graphics object for the plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 34 prob.TriangularDistribution.random # name: # type: sq_string # elements: 1 # length: 698 prob.TriangularDistribution: r = random (pd) prob.TriangularDistribution: r = random (pd, rows) prob.TriangularDistribution: r = random (pd, rows, cols, ...) prob.TriangularDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, trirnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 31 prob.TriangularDistribution.std # name: # type: sq_string # elements: 1 # length: 193 prob.TriangularDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 36 prob.TriangularDistribution.truncate # name: # type: sq_string # elements: 1 # length: 294 prob.TriangularDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.TriangularDistribution.var # name: # type: sq_string # elements: 1 # length: 173 prob.TriangularDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 24 prob.UniformDistribution # name: # type: sq_string # elements: 1 # length: 1154 statistics: prob.UniformDistribution Continuous uniform probability distribution object. A prob.UniformDistribution object consists of parameters, a model description, and sample data for a uniform probability distribution. The uniform distribution is a continuous probability distribution that models random variables that are equally likely to take any value within a specified interval defined by the lower limit Lower and upper limit Upper. There are several ways to create a prob.UniformDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.UniformDistribution (Lower, Upper) to create a uniform distribution with fixed parameter values Lower and Upper. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor. Further information about the continuous uniform distribution can be found at https://en.wikipedia.org/wiki/Continuous_uniform_distribution See also: fitdist, makedist, unifcdf, unifinv, unifpdf, unifrnd, unifit, unifstat # name: # type: sq_string # elements: 1 # length: 51 Continuous uniform probability distribution object. # name: # type: sq_string # elements: 1 # length: 41 prob.UniformDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 189 prob.UniformDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 36 prob.UniformDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 209 prob.UniformDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 30 prob.UniformDistribution.Lower # name: # type: sq_string # elements: 1 # length: 199 prob.UniformDistribution: property Lower Lower limit parameter A scalar value characterizing the lower bound of the uniform distribution. You can access the Lower property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 21 Lower limit parameter # name: # type: sq_string # elements: 1 # length: 38 prob.UniformDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 202 prob.UniformDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 45 prob.UniformDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 226 prob.UniformDistribution: property ParameterDescription Description of parameters A 2x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 39 prob.UniformDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 203 prob.UniformDistribution: property ParameterNames Names of parameters A 2x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 40 prob.UniformDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 286 prob.UniformDistribution: property ParameterValues Distribution parameter values A 2x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the Lower and Upper properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 35 prob.UniformDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 346 prob.UniformDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 44 prob.UniformDistribution.UniformDistribution # name: # type: sq_string # elements: 1 # length: 385 prob.UniformDistribution: pd = UniformDistribution (Lower, Upper) prob.UniformDistribution: pd = UniformDistribution () Create a prob.UniformDistribution object. Lower and Upper are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, Lower 0 and Upper 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 41 Create a prob.UniformDistribution object. # name: # type: sq_string # elements: 1 # length: 30 prob.UniformDistribution.Upper # name: # type: sq_string # elements: 1 # length: 199 prob.UniformDistribution: property Upper Upper limit parameter A scalar value characterizing the upper bound of the uniform distribution. You can access the Upper property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 21 Upper limit parameter # name: # type: sq_string # elements: 1 # length: 28 prob.UniformDistribution.cdf # name: # type: sq_string # elements: 1 # length: 384 prob.UniformDistribution: p = cdf (pd, x) prob.UniformDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 29 prob.UniformDistribution.icdf # name: # type: sq_string # elements: 1 # length: 242 prob.UniformDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 28 prob.UniformDistribution.iqr # name: # type: sq_string # elements: 1 # length: 192 prob.UniformDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 29 prob.UniformDistribution.mean # name: # type: sq_string # elements: 1 # length: 164 prob.UniformDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.UniformDistribution.median # name: # type: sq_string # elements: 1 # length: 172 prob.UniformDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 28 prob.UniformDistribution.pdf # name: # type: sq_string # elements: 1 # length: 202 prob.UniformDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 29 prob.UniformDistribution.plot # name: # type: sq_string # elements: 1 # length: 1519 prob.UniformDistribution: plot (pd) prob.UniformDistribution: plot (pd, Name, Value) prob.UniformDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 31 prob.UniformDistribution.random # name: # type: sq_string # elements: 1 # length: 687 prob.UniformDistribution: r = random (pd) prob.UniformDistribution: r = random (pd, rows) prob.UniformDistribution: r = random (pd, rows, cols, ...) prob.UniformDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, unifrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 28 prob.UniformDistribution.std # name: # type: sq_string # elements: 1 # length: 190 prob.UniformDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 33 prob.UniformDistribution.truncate # name: # type: sq_string # elements: 1 # length: 533 prob.UniformDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 28 prob.UniformDistribution.var # name: # type: sq_string # elements: 1 # length: 170 prob.UniformDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 24 prob.WeibullDistribution # name: # type: sq_string # elements: 1 # length: 1459 statistics: prob.WeibullDistribution Weibull probability distribution object. A prob.WeibullDistribution object consists of parameters, a model description, and sample data for a Weibull probability distribution. The Weibull distribution is a continuous probability distribution that models the time to failure of materials or the lifetime of mechanical systems. It is defined by scale parameter A and shape parameter B. A is the lambda of the usual mathematical notation and B is its k. The wbl* functions name the same two quantities lambda and k; this class follows MATLAB. There are several ways to create a prob.WeibullDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.WeibullDistribution (A, B) to create a Weibull distribution with fixed parameter values A and B. Use the static method prob.WeibullDistribution.fit (x, alpha, censor, freq) to fit a distribution to the data in x using the same input arguments as the wblfit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the Weibull distribution can be found at https://en.wikipedia.org/wiki/Weibull_distribution See also: fitdist, makedist, wblcdf, wblinv, wblpdf, wblrnd, wblfit, wbllike, wblstat # name: # type: sq_string # elements: 1 # length: 40 Weibull probability distribution object. # name: # type: sq_string # elements: 1 # length: 26 prob.WeibullDistribution.A # name: # type: sq_string # elements: 1 # length: 188 prob.WeibullDistribution: property A Scale parameter A positive scalar value characterizing the scale of the Weibull distribution. You can access the A property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Scale parameter # name: # type: sq_string # elements: 1 # length: 26 prob.WeibullDistribution.B # name: # type: sq_string # elements: 1 # length: 188 prob.WeibullDistribution: property B Shape parameter A positive scalar value characterizing the shape of the Weibull distribution. You can access the B property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Shape parameter # name: # type: sq_string # elements: 1 # length: 41 prob.WeibullDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 189 prob.WeibullDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 34 prob.WeibullDistribution.InputData # name: # type: sq_string # elements: 1 # length: 736 prob.WeibullDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: a numeric vector of logical values indicating censoring information corresponding to the elements of the data used for distribution fitting. If no censoring vector was used for distribution fitting, then this field defaults to an empty array. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 36 prob.WeibullDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 209 prob.WeibullDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 38 prob.WeibullDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 202 prob.WeibullDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 44 prob.WeibullDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 615 prob.WeibullDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 2x2 numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter, and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 45 prob.WeibullDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 226 prob.WeibullDistribution: property ParameterDescription Description of parameters A 2x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 41 prob.WeibullDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 282 prob.WeibullDistribution: property ParameterIsFixed Flag for fixed parameters A 1x2 logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 39 prob.WeibullDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 203 prob.WeibullDistribution: property ParameterNames Names of parameters A 2x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 40 prob.WeibullDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 278 prob.WeibullDistribution: property ParameterValues Distribution parameter values A 2x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the A and B properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 35 prob.WeibullDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 346 prob.WeibullDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 44 prob.WeibullDistribution.WeibullDistribution # name: # type: sq_string # elements: 1 # length: 361 prob.WeibullDistribution: pd = WeibullDistribution (A, B) prob.WeibullDistribution: pd = WeibullDistribution () Create a prob.WeibullDistribution object. A and B are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, A 1 and B 1. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 41 Create a prob.WeibullDistribution object. # name: # type: sq_string # elements: 1 # length: 28 prob.WeibullDistribution.cdf # name: # type: sq_string # elements: 1 # length: 384 prob.WeibullDistribution: p = cdf (pd, x) prob.WeibullDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 29 prob.WeibullDistribution.icdf # name: # type: sq_string # elements: 1 # length: 242 prob.WeibullDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 28 prob.WeibullDistribution.iqr # name: # type: sq_string # elements: 1 # length: 192 prob.WeibullDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 29 prob.WeibullDistribution.mean # name: # type: sq_string # elements: 1 # length: 164 prob.WeibullDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.WeibullDistribution.median # name: # type: sq_string # elements: 1 # length: 172 prob.WeibullDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 34 prob.WeibullDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 218 prob.WeibullDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 32 prob.WeibullDistribution.paramci # name: # type: sq_string # elements: 1 # length: 975 prob.WeibullDistribution: ci = paramci (pd) prob.WeibullDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 28 prob.WeibullDistribution.pdf # name: # type: sq_string # elements: 1 # length: 202 prob.WeibullDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 29 prob.WeibullDistribution.plot # name: # type: sq_string # elements: 1 # length: 1519 prob.WeibullDistribution: plot (pd) prob.WeibullDistribution: plot (pd, Name, Value) prob.WeibullDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 32 prob.WeibullDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2034 prob.WeibullDistribution: [nlogL, param] = proflik (pd, pnum) prob.WeibullDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.WeibullDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.WeibullDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.WeibullDistribution: [nlogL, param] = proflik (pd) prob.WeibullDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the Weibull distribution, pnum = 1 selects the parameter A and pnum = 2 selects the parameter B. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 31 prob.WeibullDistribution.random # name: # type: sq_string # elements: 1 # length: 686 prob.WeibullDistribution: r = random (pd) prob.WeibullDistribution: r = random (pd, rows) prob.WeibullDistribution: r = random (pd, rows, cols, ...) prob.WeibullDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, wblrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 28 prob.WeibullDistribution.std # name: # type: sq_string # elements: 1 # length: 190 prob.WeibullDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 33 prob.WeibullDistribution.truncate # name: # type: sq_string # elements: 1 # length: 533 prob.WeibullDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 28 prob.WeibullDistribution.var # name: # type: sq_string # elements: 1 # length: 170 prob.WeibullDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. # name: # type: sq_string # elements: 1 # length: 31 prob.tLocationScaleDistribution # name: # type: sq_string # elements: 1 # length: 1536 statistics: prob.tLocationScaleDistribution Location-Scale Student's T probability distribution object. A prob.tLocationScaleDistribution object consists of parameters, a model description, and sample data for a location-scale Student's T probability distribution. The location-scale Student's T distribution is a continuous probability distribution that generalizes the standard Student's T distribution by including location and scale parameters. It is defined by location parameter mu, scale parameter sigma, and degrees of freedom nu. There are several ways to create a prob.tLocationScaleDistribution object. Fit a distribution to data using the fitdist function. Create a distribution with fixed parameter values using the makedist function. Use the constructor prob.tLocationScaleDistribution (mu, sigma, nu) to create a location-scale Student's T distribution with fixed parameter values mu, sigma, and nu. Use the static method prob.tLocationScaleDistribution.fit (x, censor, freq, options) to fit a distribution to the data in x using the same input arguments as the tlsfit function. It is highly recommended to use fitdist and makedist functions to create probability distribution objects, instead of the class constructor or the aforementioned static method. Further information about the location-scale Student's T distribution can be found at https://en.wikipedia.org/wiki/Student%27s_t-distribution#Location-scale_t_distribution See also: fitdist, makedist, tlscdf, tlsinv, tlspdf, tlsrnd, tlsfit, tlslike, tlsstat # name: # type: sq_string # elements: 1 # length: 59 Location-Scale Student's T probability distribution object. # name: # type: sq_string # elements: 1 # length: 48 prob.tLocationScaleDistribution.DistributionName # name: # type: sq_string # elements: 1 # length: 196 prob.tLocationScaleDistribution: property DistributionName Probability distribution name A character vector specifying the name of the probability distribution object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Probability distribution name # name: # type: sq_string # elements: 1 # length: 41 prob.tLocationScaleDistribution.InputData # name: # type: sq_string # elements: 1 # length: 743 prob.tLocationScaleDistribution: property InputData Data used for fitting a probability distribution A scalar structure containing the following fields: data: a numeric vector containing the data used for distribution fitting. cens: a numeric vector of logical values indicating censoring information corresponding to the elements of the data used for distribution fitting. If no censoring vector was used for distribution fitting, then this field defaults to an empty array. freq: a numeric vector of non-negative integer values containing the frequency information corresponding to the elements of the data used for distribution fitting. If no frequency vector was used for distribution fitting, then this field defaults to an empty array. # name: # type: sq_string # elements: 1 # length: 48 Data used for fitting a probability distribution # name: # type: sq_string # elements: 1 # length: 43 prob.tLocationScaleDistribution.IsTruncated # name: # type: sq_string # elements: 1 # length: 216 prob.tLocationScaleDistribution: property IsTruncated Flag for truncated probability distribution A logical scalar value specifying whether a probability distribution is truncated or not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Flag for truncated probability distribution # name: # type: sq_string # elements: 1 # length: 45 prob.tLocationScaleDistribution.NumParameters # name: # type: sq_string # elements: 1 # length: 209 prob.tLocationScaleDistribution: property NumParameters Number of parameters A scalar integer value specifying the number of parameters characterizing the probability distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of parameters # name: # type: sq_string # elements: 1 # length: 51 prob.tLocationScaleDistribution.ParameterCovariance # name: # type: sq_string # elements: 1 # length: 622 prob.tLocationScaleDistribution: property ParameterCovariance Covariance matrix of the parameter estimates A 3x3 numeric matrix containing the variance-covariance of the parameter estimates. Diagonal elements contain the variance of each estimated parameter, and non-diagonal elements contain the covariance between the parameter estimates. The covariance matrix is only meaningful when the distribution was fitted to data. If the distribution object was created with fixed parameters, or a parameter of a fitted distribution is modified, then all elements of the variance-covariance are zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Covariance matrix of the parameter estimates # name: # type: sq_string # elements: 1 # length: 52 prob.tLocationScaleDistribution.ParameterDescription # name: # type: sq_string # elements: 1 # length: 233 prob.tLocationScaleDistribution: property ParameterDescription Description of parameters A 3x1 cell array of character vectors with each element containing a short description of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Description of parameters # name: # type: sq_string # elements: 1 # length: 48 prob.tLocationScaleDistribution.ParameterIsFixed # name: # type: sq_string # elements: 1 # length: 289 prob.tLocationScaleDistribution: property ParameterIsFixed Flag for fixed parameters A 1x3 logical vector specifying which parameters are fixed and which are estimated. true values correspond to fixed parameters, false values correspond to parameter estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for fixed parameters # name: # type: sq_string # elements: 1 # length: 46 prob.tLocationScaleDistribution.ParameterNames # name: # type: sq_string # elements: 1 # length: 210 prob.tLocationScaleDistribution: property ParameterNames Names of parameters A 3x1 cell array of character vectors with each element containing the name of a distribution parameter. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Names of parameters # name: # type: sq_string # elements: 1 # length: 47 prob.tLocationScaleDistribution.ParameterValues # name: # type: sq_string # elements: 1 # length: 295 prob.tLocationScaleDistribution: property ParameterValues Distribution parameter values A 3x1 numeric vector containing the values of the distribution parameters. This property is read-only. You can change the distribution parameters by assigning new values to the mu, sigma, and nu properties. # name: # type: sq_string # elements: 1 # length: 29 Distribution parameter values # name: # type: sq_string # elements: 1 # length: 42 prob.tLocationScaleDistribution.Truncation # name: # type: sq_string # elements: 1 # length: 353 prob.tLocationScaleDistribution: property Truncation Truncation interval A 1x2 numeric vector specifying the truncation interval for the probability distribution. First element contains the lower boundary, second element contains the upper boundary. This property is read-only. You can only truncate a probability distribution with the truncate method. # name: # type: sq_string # elements: 1 # length: 19 Truncation interval # name: # type: sq_string # elements: 1 # length: 35 prob.tLocationScaleDistribution.cdf # name: # type: sq_string # elements: 1 # length: 398 prob.tLocationScaleDistribution: p = cdf (pd, x) prob.tLocationScaleDistribution: p = cdf (pd, x, 'upper') Compute the cumulative distribution function (CDF). p = cdf (pd, x) computes the CDF of the probability distribution object, pd, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 51 Compute the cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 36 prob.tLocationScaleDistribution.icdf # name: # type: sq_string # elements: 1 # length: 249 prob.tLocationScaleDistribution: x = icdf (pd, p) Compute the inverse cumulative distribution function (iCDF). x = icdf (pd, p) computes the quantile (the inverse of the CDF) of the probability distribution object, pd, evaluated at the values in p. # name: # type: sq_string # elements: 1 # length: 60 Compute the inverse cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 35 prob.tLocationScaleDistribution.iqr # name: # type: sq_string # elements: 1 # length: 199 prob.tLocationScaleDistribution: r = iqr (pd) Compute the interquartile range of a probability distribution. r = iqr (pd) computes the interquartile range of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 62 Compute the interquartile range of a probability distribution. # name: # type: sq_string # elements: 1 # length: 36 prob.tLocationScaleDistribution.mean # name: # type: sq_string # elements: 1 # length: 171 prob.tLocationScaleDistribution: m = mean (pd) Compute the mean of a probability distribution. m = mean (pd) computes the mean of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 47 Compute the mean of a probability distribution. # name: # type: sq_string # elements: 1 # length: 38 prob.tLocationScaleDistribution.median # name: # type: sq_string # elements: 1 # length: 179 prob.tLocationScaleDistribution: m = median (pd) Compute the median of a probability distribution. m = median (pd) computes the median of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 49 Compute the median of a probability distribution. # name: # type: sq_string # elements: 1 # length: 34 prob.tLocationScaleDistribution.mu # name: # type: sq_string # elements: 1 # length: 213 prob.tLocationScaleDistribution: property mu Location parameter A scalar value characterizing the location of the location-scale Student's T distribution. You can access the mu property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 18 Location parameter # name: # type: sq_string # elements: 1 # length: 41 prob.tLocationScaleDistribution.negloglik # name: # type: sq_string # elements: 1 # length: 225 prob.tLocationScaleDistribution: nlogL = negloglik (pd) Compute the negative loglikelihood of a probability distribution. nlogL = negloglik (pd) computes the negative loglikelihood of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 65 Compute the negative loglikelihood of a probability distribution. # name: # type: sq_string # elements: 1 # length: 34 prob.tLocationScaleDistribution.nu # name: # type: sq_string # elements: 1 # length: 232 prob.tLocationScaleDistribution: property nu Degrees of freedom A positive scalar value characterizing the degrees of freedom of the location-scale Student's T distribution. You can access the nu property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 18 Degrees of freedom # name: # type: sq_string # elements: 1 # length: 39 prob.tLocationScaleDistribution.paramci # name: # type: sq_string # elements: 1 # length: 989 prob.tLocationScaleDistribution: ci = paramci (pd) prob.tLocationScaleDistribution: ci = paramci (pd, Name, Value) Compute the confidence intervals for probability distribution parameters. ci = paramci (pd) computes the lower and upper boundaries of the 95% confidence interval for each parameter of the probability distribution object, pd. ci = paramci (pd, Name, Value) computes the confidence intervals with additional options specified by Name-Value pair arguments listed below. Name Value 'Alpha' A scalar value in the range (0,1) specifying the significance level for the confidence interval. The default value 0.05 corresponds to a 95% confidence interval. 'Parameter' A character vector or a cell array of character vectors specifying the parameter names for which to compute confidence intervals. By default, paramci computes confidence intervals for all distribution parameters. paramci is meaningful only when pd is fitted to data, otherwise an empty array, [], is returned. # name: # type: sq_string # elements: 1 # length: 73 Compute the confidence intervals for probability distribution parameters. # name: # type: sq_string # elements: 1 # length: 35 prob.tLocationScaleDistribution.pdf # name: # type: sq_string # elements: 1 # length: 209 prob.tLocationScaleDistribution: y = pdf (pd, x) Compute the probability distribution function (PDF). y = pdf (pd, x) computes the PDF of the probability distribution object, pd, evaluated at the values in x. # name: # type: sq_string # elements: 1 # length: 52 Compute the probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 36 prob.tLocationScaleDistribution.plot # name: # type: sq_string # elements: 1 # length: 1540 prob.tLocationScaleDistribution: plot (pd) prob.tLocationScaleDistribution: plot (pd, Name, Value) prob.tLocationScaleDistribution: h = plot (...) Plot a probability distribution object. plot (pd) plots a probability density function (PDF) of the probability distribution object pd. If pd contains data, which have been fitted by fitdist, the PDF is superimposed over a histogram of the data. plot (pd, Name, Value) specifies additional options with the Name-Value pair arguments listed below. Name Value 'PlotType' A character vector specifying the plot type. 'pdf' plots the probability density function (PDF). When pd is fit to data, the PDF is superimposed on a histogram of the data. 'cdf' plots the cumulative density function (CDF). When pd is fit to data, the CDF is superimposed over an empirical CDF. 'probability' plots a probability plot using a CDF of the data and a CDF of the fitted probability distribution. This option is available only when pd is fitted to data. 'Discrete' A logical scalar to specify whether to plot the PDF or CDF of a discrete distribution object as a line plot or a stem plot, by specifying false or true, respectively. By default, it is true for discrete distributions and false for continuous distributions. When pd is a continuous distribution object, option is ignored. 'Parent' An axes graphics object for plot. If not specified, the plot function plots into the current axes or creates a new axes object if one does not exist. h = plot (...) returns a graphics handle to the plotted objects. # name: # type: sq_string # elements: 1 # length: 39 Plot a probability distribution object. # name: # type: sq_string # elements: 1 # length: 39 prob.tLocationScaleDistribution.proflik # name: # type: sq_string # elements: 1 # length: 2136 prob.tLocationScaleDistribution: [nlogL, param] = proflik (pd, pnum) prob.tLocationScaleDistribution: [nlogL, param] = proflik (pd, pnum, 'Display', display) prob.tLocationScaleDistribution: [nlogL, param] = proflik (pd, pnum, setparam) prob.tLocationScaleDistribution: [nlogL, param] = proflik (pd, pnum, setparam, 'Display', display) prob.tLocationScaleDistribution: [nlogL, param] = proflik (pd) prob.tLocationScaleDistribution: [nlogL, param, other] = proflik (...) Profile likelihood function for a probability distribution object. [nlogL, param] = proflik (pd, pnum) returns a vector nlogL of negative loglikelihood values and a vector param of corresponding parameter values for the parameter in the position indicated by pnum. By default, proflik uses the lower and upper bounds of the 98% confidence interval and computes 101 equispaced values for the selected parameter when it is the only one being estimated, and 21 values otherwise. pd must be fitted to data. [nlogL, param] = proflik (pd, pnum, 'Display', 'on') also plots the profile likelihood against the default range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam) defines a user-defined range of the selected parameter. [nlogL, param] = proflik (pd, pnum, setparam, 'Display', 'on') also plots the profile likelihood against the user-defined range of the selected parameter. [nlogL, param] = proflik (pd) selects the first parameter that is not fixed. [nlogL, param, other] = proflik (...) also returns a matrix other holding, in each row, the values of the remaining parameters that maximize the likelihood at the corresponding value of param. A fixed parameter keeps its own value. For the location-scale Student's T distribution, pnum = 1 selects the parameter mu, pnum = 2 selects the parameter sigma, and pnum = 3 selects the parameter nu. When opted to display the profile likelihood plot, proflik also plots the baseline loglikelihood computed at the lower bound of the 95% confidence interval and estimated maximum likelihood. The latter might not be observable if it is outside of the used-defined range of parameter values. # name: # type: sq_string # elements: 1 # length: 66 Profile likelihood function for a probability distribution object. # name: # type: sq_string # elements: 1 # length: 38 prob.tLocationScaleDistribution.random # name: # type: sq_string # elements: 1 # length: 714 prob.tLocationScaleDistribution: r = random (pd) prob.tLocationScaleDistribution: r = random (pd, rows) prob.tLocationScaleDistribution: r = random (pd, rows, cols, ...) prob.tLocationScaleDistribution: r = random (pd, [sz]) Generate random arrays from the probability distribution object. r = random (pd) returns a random number from the distribution object pd. When called with a single size argument, tlsrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. # name: # type: sq_string # elements: 1 # length: 64 Generate random arrays from the probability distribution object. # name: # type: sq_string # elements: 1 # length: 37 prob.tLocationScaleDistribution.sigma # name: # type: sq_string # elements: 1 # length: 222 prob.tLocationScaleDistribution: property sigma Scale parameter A positive scalar value characterizing the scale of the location-scale Student's T distribution. You can access the sigma property using dot name assignment. # name: # type: sq_string # elements: 1 # length: 15 Scale parameter # name: # type: sq_string # elements: 1 # length: 35 prob.tLocationScaleDistribution.std # name: # type: sq_string # elements: 1 # length: 197 prob.tLocationScaleDistribution: s = std (pd) Compute the standard deviation of a probability distribution. s = std (pd) computes the standard deviation of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 61 Compute the standard deviation of a probability distribution. # name: # type: sq_string # elements: 1 # length: 58 prob.tLocationScaleDistribution.tLocationScaleDistribution # name: # type: sq_string # elements: 1 # length: 425 prob.tLocationScaleDistribution: pd = tLocationScaleDistribution (mu, sigma, nu) prob.tLocationScaleDistribution: pd = tLocationScaleDistribution () Create a prob.tLocationScaleDistribution object. mu, sigma and nu are the distribution parameters, which the class help describes. Called with no arguments the parameters take their defaults, mu 0, sigma 1 and nu 5. makedist is the usual way to create a distribution object. # name: # type: sq_string # elements: 1 # length: 48 Create a prob.tLocationScaleDistribution object. # name: # type: sq_string # elements: 1 # length: 40 prob.tLocationScaleDistribution.truncate # name: # type: sq_string # elements: 1 # length: 540 prob.tLocationScaleDistribution: t = truncate (pd, lower, upper) Truncate a probability distribution. t = truncate (pd, lower, upper) returns a probability distribution t, which is the probability distribution pd truncated to the specified interval with lower limit, lower, and upper limit, upper. If pd is fitted to data with fitdist, the returned probability distribution t is not fitted, does not contain any data or estimated values, and it is as it has been created with the makedist function, but it includes the truncation interval. # name: # type: sq_string # elements: 1 # length: 36 Truncate a probability distribution. # name: # type: sq_string # elements: 1 # length: 35 prob.tLocationScaleDistribution.var # name: # type: sq_string # elements: 1 # length: 177 prob.tLocationScaleDistribution: v = var (pd) Compute the variance of a probability distribution. v = var (pd) computes the variance of the probability distribution object, pd. # name: # type: sq_string # elements: 1 # length: 51 Compute the variance of a probability distribution. statistics-release-2.1.2/inst/Distribution_Classes/paretotails.m000066400000000000000000000452701527311104300251470ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef paretotails ## -*- texinfo -*- ## @deftp {statistics} paretotails ## ## Piecewise distribution with generalized Pareto tails. ## ## A @code{paretotails} object is a piecewise probability distribution fit to ## sample data. A generalized Pareto distribution (GPD) is fit to each tail ## of the data, below a lower quantile and above an upper quantile, while the ## middle of the distribution is described by the empirical cumulative ## distribution function of the data. This gives a smooth model for the ## tails, useful for extreme value analysis, together with a nonparametric ## description of the central region. ## ## Create a @code{paretotails} object with the constructor ## @code{@var{pt} = paretotails (@var{x}, @var{pl}, @var{pu})}, where @var{x} ## is the sample data and @var{pl} and @var{pu} are the cumulative ## probabilities at the lower and upper tail boundaries. Data at or below the ## @var{pl} quantile form the lower tail, data at or above the @var{pu} ## quantile form the upper tail, and the rest form the middle segment. ## ## Query the fitted object with the methods @code{cdf}, @code{pdf}, ## @code{icdf}, @code{random}, @code{boundary}, @code{nsegments}, ## @code{segment}, @code{lowerparams}, and @code{upperparams}. ## ## @strong{Note:} the kernel-smoothed middle option of @sc{matlab} ## (@code{paretotails (@var{x}, @var{pl}, @var{pu}, "kernel")}) is not yet ## supported; only the default empirical (@qcode{"ecdf"}) middle is available. ## ## @seealso{gpfit, gpcdf, gppdf, gpinv, ecdf, fitdist, ## prob.GeneralizedParetoDistribution} ## @end deftp properties (SetAccess = private) ## -*- texinfo -*- ## @deftp {paretotails} {property} NumSegments ## ## Number of segments in the piecewise distribution (a lower tail, a middle, ## and an upper tail give three). ## ## @end deftp NumSegments = 3 ## -*- texinfo -*- ## @deftp {paretotails} {property} NumParameters ## ## Number of estimated parameters (two per fitted generalized Pareto tail). ## ## @end deftp NumParameters = 4 endproperties properties (Access = private) lowerP = [] # [k, sigma] of the lower-tail GPD, or [] if no tail upperP = [] # [k, sigma] of the upper-tail GPD, or [] if no tail pl = 0 # cumulative probability at the lower boundary pu = 1 # cumulative probability at the upper boundary ql = -Inf # lower boundary quantile qu = Inf # upper boundary quantile xknot = [] # middle-segment interpolation knots (x) pknot = [] # middle-segment interpolation knots (cumulative prob) nobs = 0 # number of (non-NaN) observations method = "ecdf" # description of the middle segment endproperties methods (Hidden) ## Custom display of the segment summary. function disp (this) printf (" Piecewise distribution with %d segments fit to %d ", ... this.NumSegments, this.nobs); printf ("observations:\n\n"); if (this.pl > 0) printf (" P = [0, %g]: lower tail, generalized Pareto ", this.pl); printf ("(k = %g, sigma = %g)\n", this.lowerP(1), this.lowerP(2)); endif printf (" P = [%g, %g]: middle, %s\n", this.pl, this.pu, this.method); if (this.pu < 1) printf (" P = [%g, 1]: upper tail, generalized Pareto ", this.pu); printf ("(k = %g, sigma = %g)\n", this.upperP(1), this.upperP(2)); endif endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {paretotails} {@var{pt} =} paretotails (@var{x}, @var{pl}, @var{pu}) ## @deftypefnx {paretotails} {@var{pt} =} paretotails (@var{x}, @var{pl}, @var{pu}, @var{cdffun}) ## ## Fit a piecewise distribution with generalized Pareto tails to @var{x}. ## ## @var{pl} and @var{pu} are the cumulative probabilities of the lower and ## upper tail boundaries, with @code{0 <= @var{pl} < @var{pu} <= 1}. A ## generalized Pareto distribution is fit by maximum likelihood to the ## exceedances in each tail; the middle segment uses the empirical ## cumulative distribution of @var{x}. ## ## @var{cdffun} selects the middle segment and defaults to @qcode{"ecdf"}; ## the @qcode{"kernel"} option of @sc{matlab} is not yet supported. ## ## @end deftypefn function this = paretotails (x, pl, pu, cdffun) if (nargin < 3) print_usage (); endif if (! (isnumeric (x) && isreal (x) && isvector (x))) error ("paretotails: X must be a numeric vector."); endif x = x(:); x(isnan (x)) = []; if (numel (x) < 2) error ("paretotails: X must contain at least two non-NaN values."); endif if (! (isnumeric (pl) && isscalar (pl) && isreal (pl) ... && pl >= 0 && pl <= 1)) error ("paretotails: PL must be a scalar in the range [0, 1]."); endif if (! (isnumeric (pu) && isscalar (pu) && isreal (pu) ... && pu >= 0 && pu <= 1)) error ("paretotails: PU must be a scalar in the range [0, 1]."); endif if (pl >= pu) error ("paretotails: PL must be less than PU."); endif if (nargin > 3) if (! ischar (cdffun)) error ("paretotails: CDFFUN must be 'ecdf' or 'kernel'."); endif switch (lower (cdffun)) case "ecdf" ## default case "kernel" error (strcat ("paretotails: the 'kernel' middle segment is", ... " not yet supported; use the default 'ecdf'.")); otherwise error ("paretotails: CDFFUN must be 'ecdf' or 'kernel'."); endswitch endif xs = sort (x); n = numel (xs); pp = ((1:n)' - 0.5) ./ n; this.pl = pl; this.pu = pu; this.nobs = n; this.ql = paretotails.quantile_pp (xs, pp, pl); this.qu = paretotails.quantile_pp (xs, pp, pu); ## Fit the generalized Pareto tails to the exceedances, which are already ## measured from their threshold and so have the zero location gpfit ## assumes. if (pl > 0) this.lowerP = gpfit (this.ql - xs(xs < this.ql)); endif if (pu < 1) this.upperP = gpfit (xs(xs > this.qu) - this.qu); endif ## Middle-segment interpolation knots: the interior data points, with the ## two boundary points prepended and appended so the piecewise-linear cdf ## maps [ql, qu] onto [pl, pu] exactly. mid = xs > this.ql & xs < this.qu; xk = xs(mid); pk = pp(mid); if (pl > 0) xk = [this.ql; xk]; pk = [pl; pk]; endif if (pu < 1) xk = [xk; this.qu]; pk = [pk; pu]; endif ## Drop any repeated abscissae so interp1 sees a strictly increasing grid keep = [true; diff(xk) > 0]; this.xknot = xk(keep); this.pknot = pk(keep); this.NumSegments = 1 + (pl > 0) + (pu < 1); this.NumParameters = 2 .* (pl > 0) + 2 .* (pu < 1); endfunction ## -*- texinfo -*- ## @deftypefn {paretotails} {@var{p} =} cdf (@var{pt}, @var{x}) ## ## Cumulative distribution function of the @code{paretotails} object ## @var{pt} evaluated at the values in @var{x}. ## ## @end deftypefn function p = cdf (this, x) if (nargin != 2) print_usage (); endif p = nan (size (x)); lo = x < this.ql; hi = x > this.qu; mid = ! lo & ! hi; if (any (lo(:))) p(lo) = this.pl .* gpcdf (this.ql - x(lo), ... this.lowerP(1), this.lowerP(2), 0, 'upper'); endif if (any (hi(:))) p(hi) = this.pu + (1 - this.pu) .* gpcdf (x(hi) - this.qu, ... this.upperP(1), this.upperP(2), 0); endif if (any (mid(:))) p(mid) = interp1 (this.xknot, this.pknot, x(mid), "linear"); endif endfunction ## -*- texinfo -*- ## @deftypefn {paretotails} {@var{y} =} pdf (@var{pt}, @var{x}) ## ## Probability density function of the @code{paretotails} object @var{pt} ## evaluated at the values in @var{x}. ## ## @end deftypefn function y = pdf (this, x) if (nargin != 2) print_usage (); endif y = nan (size (x)); lo = x < this.ql; hi = x > this.qu; mid = ! lo & ! hi; if (any (lo(:))) y(lo) = this.pl .* gppdf (this.ql - x(lo), ... this.lowerP(1), this.lowerP(2), 0); endif if (any (hi(:))) y(hi) = (1 - this.pu) .* gppdf (x(hi) - this.qu, ... this.upperP(1), this.upperP(2), 0); endif if (any (mid(:))) ## The middle cdf is piecewise linear, so its density is the piecewise ## constant slope of the segment each point falls in. slope = diff (this.pknot) ./ diff (this.xknot); idx = paretotails.bin_index (this.xknot, x(mid)); y(mid) = slope(idx); endif endfunction ## -*- texinfo -*- ## @deftypefn {paretotails} {@var{x} =} icdf (@var{pt}, @var{p}) ## ## Inverse cumulative distribution function (quantile function) of the ## @code{paretotails} object @var{pt} evaluated at the probabilities ## @var{p}. ## ## @end deftypefn function x = icdf (this, p) if (nargin != 2) print_usage (); endif x = nan (size (p)); valid = p >= 0 & p <= 1; lo = valid & p < this.pl; hi = valid & p > this.pu; mid = valid & ! lo & ! hi; if (any (lo(:))) x(lo) = this.ql - gpinv (1 - p(lo) ./ this.pl, ... this.lowerP(1), this.lowerP(2), 0); endif if (any (hi(:))) x(hi) = this.qu + gpinv ((p(hi) - this.pu) ./ (1 - this.pu), ... this.upperP(1), this.upperP(2), 0); endif if (any (mid(:))) x(mid) = interp1 (this.pknot, this.xknot, p(mid), "linear"); endif endfunction ## -*- texinfo -*- ## @deftypefn {paretotails} {@var{r} =} random (@var{pt}) ## @deftypefnx {paretotails} {@var{r} =} random (@var{pt}, @var{sz}) ## @deftypefnx {paretotails} {@var{r} =} random (@var{pt}, @var{m}, @var{n}, @dots{}) ## ## Random values drawn from the @code{paretotails} object @var{pt}, by ## inverse transform sampling. The size arguments follow @code{rand}. ## ## @end deftypefn function r = random (this, varargin) ## Negative dimensions are treated as zero, as in core Octave and MATLAB szargs = cellfun (@(x) max (x, 0), varargin, 'UniformOutput', false); r = this.icdf (rand (szargs{:})); endfunction ## -*- texinfo -*- ## @deftypefn {paretotails} {[@var{p}, @var{q}] =} boundary (@var{pt}) ## ## Boundary probabilities @var{p} and quantiles @var{q} of the segments of ## the @code{paretotails} object @var{pt}, as column vectors. ## ## @end deftypefn function [p, q] = boundary (this) p = []; q = []; if (this.pl > 0) p = [p; this.pl]; q = [q; this.ql]; endif if (this.pu < 1) p = [p; this.pu]; q = [q; this.qu]; endif endfunction ## -*- texinfo -*- ## @deftypefn {paretotails} {@var{n} =} nsegments (@var{pt}) ## ## Number of segments in the @code{paretotails} object @var{pt}. ## ## @end deftypefn function n = nsegments (this) n = this.NumSegments; endfunction ## -*- texinfo -*- ## @deftypefn {paretotails} {@var{params} =} lowerparams (@var{pt}) ## ## Shape and scale parameters @code{[@var{k}, @var{sigma}]} of the ## generalized Pareto distribution fit to the lower tail of @var{pt}. ## ## @end deftypefn function params = lowerparams (this) if (isempty (this.lowerP)) error ("paretotails: the distribution has no lower tail."); endif params = this.lowerP(:)'; endfunction ## -*- texinfo -*- ## @deftypefn {paretotails} {@var{params} =} upperparams (@var{pt}) ## ## Shape and scale parameters @code{[@var{k}, @var{sigma}]} of the ## generalized Pareto distribution fit to the upper tail of @var{pt}. ## ## @end deftypefn function params = upperparams (this) if (isempty (this.upperP)) error ("paretotails: the distribution has no upper tail."); endif params = this.upperP(:)'; endfunction ## -*- texinfo -*- ## @deftypefn {paretotails} {@var{s} =} segment (@var{pt}, @var{x}, @var{p}) ## ## Segment indices for the @code{paretotails} object @var{pt}. Supply the ## data values in @var{x} (with @var{p} empty) or the cumulative ## probabilities in @var{p} (with @var{x} empty). Segment @code{1} is the ## lower tail, @code{2} the middle, and @code{3} the upper tail. ## ## @end deftypefn function s = segment (this, x, p) if (nargin < 3) p = []; endif if (nargin < 2) x = []; endif if (isempty (x) && ! isempty (p)) s = 2 .* ones (size (p)); s(p < this.pl) = 1; s(p > this.pu) = 3; else s = 2 .* ones (size (x)); s(x < this.ql) = 1; s(x > this.qu) = 3; endif endfunction endmethods methods (Static, Access = private) ## Empirical quantile by linear interpolation of the plotting positions ## (i-0.5)/n, clamped to the data range (the default of MATLAB's quantile). function q = quantile_pp (xs, pp, p) if (p <= pp(1)) q = xs(1); elseif (p >= pp(end)) q = xs(end); else q = interp1 (pp, xs, p, "linear"); endif endfunction ## Index of the interpolation bin containing each value of x, clamped to the ## valid range of the knot vector xk. function idx = bin_index (xk, x) idx = zeros (size (x)); for i = 1:numel (x) j = find (xk <= x(i), 1, "last"); if (isempty (j)) j = 1; elseif (j >= numel (xk)) j = numel (xk) - 1; endif idx(i) = j; endfor endfunction endmethods endclassdef %!demo %! ## Fit Pareto tails to a normal sample and compare the tail cdf to the data %! x = norminv (((1:100) - 0.5) / 100); %! pt = paretotails (x, 0.1, 0.9); %! lowerparams (pt) %! [p, q] = boundary (pt) %! cdf (pt, [-2.5, 0, 2.5]) ## Shared probe data and MATLAB reference values (x = norminv of 100 plotting ## positions; pl = 0.1, pu = 0.9). %!shared x, pt %! x = norminv (((1:100) - 0.5) / 100); %! pt = paretotails (x, 0.1, 0.9); %!test %! assert_equal (lowerparams (pt), ... %! [-0.381277950146653, 0.652296030248444], 1e-5); %! assert_equal (upperparams (pt), ... %! [-0.381277950146658, 0.652296030248448], 1e-5); %!test %! [p, q] = boundary (pt); %! assert_equal (p, [0.1; 0.9], 1e-12); %! assert_equal (q, [-1.28207227531929; 1.28207227531929], 1e-10); %!test %! assert_equal (nsegments (pt), 3); %! assert_equal (pt.NumSegments, 3); %!test %! xq = [-2.8, -2.3, -1.8, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 1.8, 2.3, 2.8]; %! ref = [0.000326503983434112, 0.00934245750239852, 0.0388388580428329, ... %! 0.0699512617277107, 0.158702922884361, 0.308553672872447, 0.5, ... %! 0.691446327127553, 0.84129707711564, 0.930048738272289, ... %! 0.961161141957167, 0.990657542497602, 0.999673496016566]; %! assert_equal (cdf (pt, xq), ref, 1e-5); %!test %! xq = [-2.8, -2.3, -1.8, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 1.8, 2.3, 2.8]; %! ref = [0.00443959395367343, 0.0353636322020254, 0.0853936076680005, ... %! 0.122892916352678, 0.243260728154123, 0.352775902406303, ... %! 0.398931835816165, 0.352775902406307, 0.243260728154123, ... %! 0.122892916352677, 0.0853936076680006, 0.0353636322020256, ... %! 0.00443959395367328]; %! assert_equal (pdf (pt, xq), ref, 1e-5); %!test %! pq = [0.005, 0.01, 0.02, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.98, ... %! 0.99, 0.995]; %! ref = [-2.44694213045116, -2.28179640154802, -2.06669489811039, ... %! -1.67939673030492, -1.28207227531929, -0.674573258334612, 0, ... %! 0.67457325833461, 1.28207227531929, 1.67939673030492, ... %! 2.06669489811039, 2.28179640154802, 2.44694213045116]; %! assert_equal (icdf (pt, pq), ref, 1e-5); %!test %! xq = [-2.8, -2.3, -1.8, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 1.8, 2.3, 2.8]; %! ref = [1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3]; %! assert_equal (segment (pt, xq, []), ref); ## cdf and icdf are inverses in the middle and tails %!test %! p = [0.02, 0.2, 0.5, 0.8, 0.98]; %! assert_equal (cdf (pt, icdf (pt, p)), p, 1e-6); ## random returns values of the requested size within the support %!test %! r = random (pt, 3, 4); %! assert_equal (size (r), [3, 4]); %! assert_equal (size (random (pt, -1)), [0, 0]); %! assert_equal (size (random (pt, 2, -1, 5)), [2, 0, 5]); ## Boundary probabilities outside [0,1] give NaN from icdf %!test %! assert_equal (icdf (pt, [-0.1, 1.1]), [NaN, NaN]); %!test %! ## The lower tail far below the resolution of 1 - gpcdf %! pd = paretotails (tan (pi * ((1:200)' / 201 - 0.5)), 0.1, 0.9); %! [pb, qb] = boundary (pd); %! lp = lowerparams (pd); %! q = qb(1) - 1e20; %! assert_equal (cdf (pd, q), ... %! pb(1) * (1 + lp(1) * (qb(1) - q) / lp(2)) ^ (-1 / lp(1)), ... %! -1e-12); ## Test input validation %!error paretotails (1) %!error paretotails ("a", 0.1, 0.9) %!error ... %! paretotails (1, 0.1, 0.9) %!error ... %! paretotails (1:10, -0.1, 0.9) %!error ... %! paretotails (1:10, 0.1, 1.5) %!error paretotails (1:10, 0.9, 0.1) %!error ... %! paretotails (1:10, 0.1, 0.9, "kernel") %!error ... %! paretotails (1:10, 0.1, 0.9, "foo") statistics-release-2.1.2/inst/Distribution_Fitting/000077500000000000000000000000001527311104300224415ustar00rootroot00000000000000statistics-release-2.1.2/inst/Distribution_Fitting/betafit.m000066400000000000000000000253251527311104300242440ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{paramhat} =} betafit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} betafit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} betafit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} betafit (@var{x}, @var{alpha}, @var{freq}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} betafit (@var{x}, @var{alpha}, @var{options}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} betafit (@var{x}, @var{alpha}, @var{freq}, @var{options}) ## ## Estimate parameters and confidence intervals for the Beta distribution. ## ## @code{@var{paramhat} = betafit (@var{x})} returns the maximum likelihood ## estimates of the parameters of the Beta distribution given the data in vector ## @var{x}. @qcode{@var{paramhat}([1, 2])} corresponds to the @math{α} and ## @math{β} shape parameters, respectively. Missing values, @qcode{NaNs}, are ## ignored. ## ## @code{[@var{paramhat}, @var{paramci}] = betafit (@var{x})} returns the 95% ## confidence intervals for the parameter estimates. ## ## @code{[@dots{}] = betafit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals of the estimated ## parameter. By default, the optional argument @var{alpha} is 0.05 ## corresponding to 95% confidence intervals. ## ## @code{[@dots{}] = betafit (@var{params}, @var{x}, @var{freq})} accepts a ## frequency vector, @var{freq}, of the same size as @var{x}. @var{freq} ## must contain non-negative integer frequencies for the corresponding elements ## in @var{x}. By default, or if left empty, ## @qcode{@var{freq} = ones (size (@var{x}))}. ## ## @code{[@var{paramhat}, @var{paramci}] = betafit (@var{x}, @var{alpha}, ## @var{options})} specifies control parameters for the iterative algorithm used ## to compute ML estimates with the @code{fminsearch} function. @var{options} ## is a structure with the following fields and their default values: ## @itemize ## @item @qcode{@var{options}.Display = "off"} ## @item @qcode{@var{options}.MaxFunEvals = 400} ## @item @qcode{@var{options}.MaxIter = 200} ## @item @qcode{@var{options}.TolX = 1e-6} ## @end itemize ## ## The Beta distribution is defined on the open interval @math{(0,1)}. However, ## @code{betafit} can also compute the unbounded beta likelihood function for ## data that include exact zeros or ones. In such cases, zeros and ones are ## treated as if they were values that have been left-censored at ## @qcode{sqrt (realmin)} or right-censored at @qcode{1 - eps/2}, respectively. ## ## Further information about the Beta distribution can be found at ## @url{https://en.wikipedia.org/wiki/Beta_distribution} ## ## @seealso{betacdf, betainv, betapdf, betarnd, betalike, betastat} ## @end deftypefn function [paramhat, paramci] = betafit (x, alpha, varargin) ## Check X for being a vector if (isempty (x)) phat = nan (1, 2, class (x)); pci = nan (2, 2, class (x)); return elseif (! isvector (x) || ! isreal (x)) error ("betafit: X must be a vector of real values."); endif ## Check that X contains values in the range [0,1] if (any (x < 0) || any (x > 1)) error ("betafit: X must be in the range [0,1]."); endif ## Check X being a constant vector if (min (x) == max (x)) error ("betafit: X must contain distinct values."); endif ## Check ALPHA if (nargin < 2 || isempty (alpha)) alpha = 0.05; else if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("betafit: wrong value for ALPHA."); endif endif ## Add defaults freq = ones (size (x)); options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; ## Check extra arguments for FREQ vector and/or 'options' structure if (nargin > 2) if (numel (varargin) == 1 && isstruct (varargin{1})) options = varargin{1}; elseif (numel (varargin) == 1 && isnumeric (varargin{1})) freq = varargin{1}; elseif (numel (varargin) == 2) freq = varargin{1}; options = varargin{2}; endif if (isempty (freq)) freq = ones (size (x)); endif ## Check for valid freq vector if (! isequal (size (x), size (freq))) error ("betafit: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("betafit: FREQ must not contain negative values."); elseif (any (fix (freq) != freq)) error ("betafit: FREQ must contain integer values."); endif ## Check for valid options structure if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("betafit: 'options' argument must be a", ... " structure with 'Display', 'MaxFunEvals',", ... " 'MaxIter', and 'TolX' fields present.")); endif endif ## Remove missing values remove = isnan (x) | isnan (freq); x(remove) = []; freq(remove) = []; ## Expand frequency if (! all (freq == 1)) xf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; endfor x = xf; endif ## Estimate initial parameters numx = length (x); tmp1 = prod ((1 - x) .^ (1 / numx)); tmp2 = prod (x .^ (1 / numx)); tmp3 = (1 - tmp1 - tmp2); ahat = 0.5 * (1 - tmp1) / tmp3; bhat = 0.5 * (1 - tmp2) / tmp3; init = log ([ahat, bhat]); ## Add tolerance for boundary conditions x_lo = sqrt (realmin (class (x))); x_hi = 1 - eps (class (x)) / 2; ## All values are strictly within the interval (0,1) if (all (x > x_lo) && all (x < x_hi)) sumlogx = sum (log (x)); sumlog1px = sum (log1p (-x)); paramhat = fminsearch (@cont_negloglike, init, options); paramhat = exp (paramhat); ## Find boundary elements and process them separately else num0 = sum (x < x_lo); num1 = sum (x > x_hi); x_ct = x(x > x_lo & x < x_hi); numx = length (x_ct); sumlogx = sum (log (x_ct)); sumlog1px = sum (log1p (-x_ct)); paramhat = fminsearch (@mixed_negloglike, init, options); paramhat = exp (paramhat); endif ## Compute confidence intervals if (nargout == 2) [~, acov] = betalike (paramhat,x); logphat = log (paramhat); serrlog = sqrt (diag (acov))' ./ paramhat; p_int = [alpha/2; 1-alpha/2]; paramci = exp (norminv ([p_int p_int], ... [logphat; logphat], [serrlog; serrlog])); endif ## Continuous Negative log-likelihood function function nll = cont_negloglike (params) params = exp (params); nll = numx * betaln (params(1), params(2)) - (params(1) - 1) ... * sumlogx - (params(2) - 1) * sumlog1px; endfunction ## Unbounded Negative log-likelihood function function nll = mixed_negloglike (params) params = exp (params); nll = numx * betaln (params(1), params(2)) - (params(1) - 1) ... * sumlogx - (params(2) - 1) * sumlog1px; ## Handle zeros if (num0 > 0) nll = nll - num0 * log (betainc (x_lo, params(1), params(2), 'lower')); endif ## Handle ones if (num1 > 0) nll = nll - num1 * log (betainc (x_hi, params(1), params(2), 'upper')); endif endfunction endfunction %!demo %! ## Sample 2 populations from different Beta distributions %! randg ('state', 42); %! r1 = betarnd (2, 5, 500, 1); %! r2 = betarnd (2, 2, 500, 1); %! r = [r1, r2]; %! %! ## Plot them normalized and fix their colors %! hist (r, 12, 15); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! hold on %! %! ## Estimate their shape parameters %! a_b_A = betafit (r(:,1)); %! a_b_B = betafit (r(:,2)); %! %! ## Plot their estimated PDFs %! x = [min(r(:)):0.01:max(r(:))]; %! y = betapdf (x, a_b_A(1), a_b_A(2)); %! plot (x, y, '-pr'); %! y = betapdf (x, a_b_B(1), a_b_B(2)); %! plot (x, y, '-sg'); %! ylim ([0, 4]) %! legend ({'Normalized HIST of sample 1 with α=2 and β=5', ... %! 'Normalized HIST of sample 2 with α=2 and β=2', ... %! sprintf("PDF for sample 1 with estimated α=%0.2f and β=%0.2f", ... %! a_b_A(1), a_b_A(2)), ... %! sprintf("PDF for sample 2 with estimated α=%0.2f and β=%0.2f", ... %! a_b_B(1), a_b_B(2))}) %! title ('Two population samples from different Beta distributions') %! hold off ## Test output %!test %! x = 0.01:0.02:0.99; %! [paramhat, paramci] = betafit (x); %! paramhat_out = [1.0199, 1.0199]; %! paramci_out = [0.6947, 0.6947; 1.4974, 1.4974]; %! assert_equal (paramhat, paramhat_out, 1e-4); %! assert_equal (paramci, paramci_out, 1e-4); %!test %! x = 0.01:0.02:0.99; %! [paramhat, paramci] = betafit (x, 0.01); %! paramci_out = [0.6157, 0.6157; 1.6895, 1.6895]; %! assert_equal (paramci, paramci_out, 1e-4); %!test %! x = 0.00:0.02:1; %! [paramhat, paramci] = betafit (x); %! paramhat_out = [0.0875, 0.1913]; %! paramci_out = [0.0822, 0.1490; 0.0931, 0.2455]; %! assert_equal (paramhat, paramhat_out, 1e-4); %! assert_equal (paramci, paramci_out, 1e-4); ## Test input validation %!error betafit ([0.2, 0.5+i]); %!error betafit (ones (2,2) * 0.5); %!error betafit ([0.5, 1.2]); %!error betafit ([0.1, 0.1]); %!error betafit ([0.01:0.1:0.99], 1.2); %!error ... %! betafit ([0.01:0.01:0.05], 0.05, [1, 2, 3, 2]); %!error ... %! betafit ([0.01:0.01:0.05], 0.05, [1, 2, 3, 2, -1]); %!error ... %! betafit ([0.01:0.01:0.05], 0.05, [1, 2, 3, 2, 1.5]); %!error ... %! betafit ([0.01:0.01:0.05], 0.05, struct ('option', 234)); %!error ... %! betafit ([0.01:0.01:0.05], 0.05, ones (1,5), struct ('option', 234)); statistics-release-2.1.2/inst/Distribution_Fitting/betalike.m000066400000000000000000000160171527311104300244040ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} betalike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{avar}] =} betalike (@var{params}, @var{x}) ## ## Negative log-likelihood for the Beta distribution. ## ## @code{@var{nlogL} = betalike (@var{params}, @var{x})} returns the negative ## log likelihood of the data in @var{x} corresponding to the Beta distribution ## with (1) shape parameter @math{α} and (2) shape parameter @math{β} given in ## the two-element vector @var{params}. Both parameters must be positive real ## numbers and the data in the range @math{[0,1]}. Out of range parameters or ## data return @qcode{NaN}. ## ## @code{[@var{nlogL}, @var{avar}] = betalike (@var{params}, @var{x})} returns ## the inverse of Fisher's information matrix, @var{avar}. If the input ## parameter values in @var{params} are the maximum likelihood estimates, the ## diagonal elements of @var{params} are their asymptotic variances. ## ## @code{[@dots{}] = betalike (@var{params}, @var{x}, @var{freq})} accepts a ## frequency vector, @var{freq}, of the same size as @var{x}. @var{freq} ## must contain non-negative integer frequencies for the corresponding elements ## in @var{x}. By default, or if left empty, ## @qcode{@var{freq} = ones (size (@var{x}))}. ## ## The Beta distribution is defined on the open interval @math{(0,1)}. However, ## @code{betafit} can also compute the unbounded beta likelihood function for ## data that include exact zeros or ones. In such cases, zeros and ones are ## treated as if they were values that have been left-censored at ## @qcode{sqrt (realmin)} or right-censored at @qcode{1 - eps/2}, respectively. ## ## Further information about the Beta distribution can be found at ## @url{https://en.wikipedia.org/wiki/Beta_distribution} ## ## @seealso{betacdf, betainv, betapdf, betarnd, betafit, betastat} ## @end deftypefn function [nlogL, avar] = betalike (params, x, freq) ## Check input arguments and add defaults if (nargin < 2) error ("betalike: function called with too few input arguments."); endif if (numel (params) != 2) error ("betalike: wrong parameters length."); endif if (nargin < 3 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("betalike: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("betalike: FREQ must not contain negative values."); elseif (any (fix (freq) != freq)) error ("betalike: FREQ must contain integer values."); endif ## Expand frequency if (! all (freq == 1)) xf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; endfor x = xf; endif ## Get α and β parameters a = params(1); b = params(2); ## Force X to column vector x = x(:); ## Return NaN for out of range parameters or data. a(a <= 0) = NaN; b(b <= 0) = NaN; xmin = min (x); xmax = max (x); x(! (0 <= x & x <= 1)) = NaN; ## Add tolerance for boundary conditions x_lo = sqrt (realmin (class (x))); x_hi = 1 - eps (class (x)) / 2; ## All values are strictly within the interval (0,1) if (all (x > x_lo) && all (x < x_hi)) num0 = 0; num1 = 0; x_ct = x; numx = length (x_ct); ## Find boundary elements and process them separately else num0 = sum (x < x_lo); num1 = sum (x > x_hi); x_ct = x(x > x_lo & x < x_hi); numx = length (x_ct); endif ## Compute continuous log likelihood logx = log (x_ct); log1px = log1p (-x_ct); sumlogx = sum (logx); sumlog1px = sum (log1px); nlogL = numx * betaln (a, b) - (a - 1) * sumlogx - (b - 1) * sumlog1px; ## Include log likelihood for zeros if (num0 > 0) nlogL = nlogL - num0 * log (betainc (x_lo, a, b, 'lower')); endif ## Include log likelihood for ones if (num1 > 0) nlogL = nlogL - num1 * log (betainc (x_hi, a, b, 'upper')); endif ## Compute the asymptotic covariance if (nargout > 1) if (numel (x) < 2) error ("betalike: not enough data in X."); endif ## Compute the Jacobian of the likelihood for values (0,1) psiab = psi (a + b); psi_a = psi (a); psi_b = psi (b); J = [logx+psiab-psi_a, log1px+psiab-psi_b]; ## Add terms into the Jacobian for the zero and one values. if (num0 > 0 || num1 > 0) dd = sqrt (eps (class (x))); aa = a + a * dd * [1, -1]; bb = b + b * dd * [1, -1]; ad = 2 * a *dd; bd = 2 * b *dd; if (num0 > 0) da = diff (log (betainc (x_lo, aa, b, 'lower'))) / ad; db = diff (log (betainc (x_lo, a, bb, 'lower'))) / bd; J = [J; repmat([da, db], num0, 1)]; endif if num1 > 0 da = diff (log (betainc (x_hi, aa, b, 'upper'))) / ad; db = diff (log (betainc (x_hi, a, bb, 'upper'))) / bd; J = [J; repmat([da, db], num1, 1)]; endif endif ## Invert the inner product of the Jacobian to get the asymptotic covariance [~, R] = qr (J, 0); if (any (isnan (R(:)))) avar = [NaN, NaN; NaN, NaN]; else Rinv = R \ eye (2); avar = Rinv * Rinv'; endif endif endfunction ## Test output %!test %! x = 0.01:0.02:0.99; %! [nlogL, avar] = betalike ([2.3, 1.2], x); %! avar_out = [0.03691678, 0.02803056; 0.02803056, 0.03965629]; %! assert_equal (nlogL, 17.873477715879040, 3e-14); %! assert_equal (avar, avar_out, 1e-7); %!test %! x = 0.01:0.02:0.99; %! [nlogL, avar] = betalike ([1, 4], x); %! avar_out = [0.02793282, 0.02717274; 0.02717274, 0.03993361]; %! assert_equal (nlogL, 79.648061114839550, 1e-13); %! assert_equal (avar, avar_out, 1e-7); %!test %! x = 0.00:0.02:1; %! [nlogL, avar] = betalike ([1, 4], x); %! avar_out = [0.00000801564765, 0.00000131397245; ... %! 0.00000131397245, 0.00070827639442]; %! assert_equal (nlogL, 573.2008434477486, 1e-10); %! assert_equal (avar, avar_out, 1e-14); ## Test input validation %!error ... %! betalike ([12, 15]); %!error betalike ([12, 15, 3], [1:50]); %!error ... %! betalike ([12, 15], ones (10, 1), ones (8,1)) %!error ... %! betalike ([12, 15], ones (1, 8), [1 1 1 1 1 1 1 -1]) %!error ... %! betalike ([12, 15], ones (1, 8), [1 1 1 1 1 1 1 1.5]) statistics-release-2.1.2/inst/Distribution_Fitting/binofit.m000066400000000000000000000142451527311104300242570ustar00rootroot00000000000000## Copyright (C) 2023-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{pshat} =} binofit (@var{x}, @var{n}) ## @deftypefnx {statistics} {[@var{pshat}, @var{psci}] =} binofit (@var{x}, @var{n}) ## @deftypefnx {statistics} {[@var{pshat}, @var{psci}] =} binofit (@var{x}, @var{n}, @var{alpha}) ## ## Estimate parameter and confidence intervals for the binomial distribution. ## ## @code{@var{pshat} = binofit (@var{x}, @var{n})} returns the maximum ## likelihood estimate (MLE) of the probability of success for the binomial ## distribution. @var{x} and @var{n} are scalars containing the number of ## successes and the number of trials, respectively. If @var{x} and @var{n} are ## vectors, @code{binofit} returns a vector of estimates whose @math{i}-th ## element is the parameter estimate for @var{x}(i) and @var{n}(i). A scalar ## value for @var{x} or @var{n} is expanded to the same size as the other input. ## ## @code{[@var{pshat}, @var{psci}] = binofit (@var{x}, @var{n}, @var{alpha})} ## also returns the @qcode{100 * (1 - @var{alpha})} percent confidence intervals ## of the estimated parameter. By default, the optional argument @var{alpha} ## is 0.05 corresponding to 95% confidence intervals. ## ## @code{binofit} treats a vector @var{x} as a collection of measurements from ## separate samples, and returns a vector of estimates. If you want to treat ## @var{x} as a single sample and compute a single parameter estimate and ## confidence interval, use @qcode{binofit (sum (@var{x}), sum (@var{n}))} when ## @var{n} is a vector, and ## @qcode{binofit (sum (@var{x}), @var{n} * length (@var{x}))} when @var{n} is a ## scalar. ## ## Further information about the binomial distribution can be found at ## @url{https://en.wikipedia.org/wiki/Binomial_distribution} ## ## @seealso{binocdf, binoinv, binopdf, binornd, binolike, binostat} ## @end deftypefn function [pshat, psci] = binofit (x, n, alpha) ## Check input arguments if (nargin < 2) error ("binofit: function called with too few input arguments."); endif if (any (x < 0)) error ("binofit: X cannot have negative values."); endif if (! isvector (x)) error ("binofit: X must be a vector."); endif if (any (n < 0) || any (n != round (n)) || any (isinf (n))) error ("binofit: N must be a non-negative integer."); endif if (! (isscalar (n) || isequal (size (n), size (x)))) error ("binofit: N must be a scalar or the same size as X."); endif if (any (x > n)) error ("binofit: N must be at least as large as X."); endif if (nargin < 3 || isempty (alpha)) alpha = 0.05; else if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("binofit: wrong value for ALPHA."); endif endif ## Compute pshat pshat = x ./ n; ## Compute lower confidence interval nu1 = 2 * x; nu2 = 2 * (n - x + 1); F = finv (alpha / 2, nu1, nu2); lb = (nu1 .* F) ./ (nu2 + nu1 .* F); x0 = find (x == 0); if (! isempty (x0)) lb(x0) = 0; endif ## Compute upper confidence interval nu1 = 2 * (x + 1); nu2 = 2 * (n - x); F = finv (1 - alpha / 2, nu1, nu2); ub = (nu1 .* F) ./ (nu2 + nu1 .* F); xn = find (x == n); if (! isempty (xn)) ub(xn) = 1; endif psci = [lb(:), ub(:)]; endfunction %!demo %! ## Sample 2 populations from different binomial distributions %! rng (42); %! r1 = binornd (50, 0.15, 1000, 1); %! r2 = binornd (100, 0.5, 1000, 1); %! r = [r1, r2]; %! %! ## Plot them normalized and fix their colors %! hist (r, 23, 0.35); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! hold on %! %! ## Estimate their probability of success %! pshatA = binofit (r(:,1), 50); %! pshatB = binofit (r(:,2), 100); %! %! ## Plot their estimated PDFs %! x = [min(r(:,1)):max(r(:,1))]; %! y = binopdf (x, 50, mean (pshatA)); %! plot (x, y, '-pg'); %! x = [min(r(:,2)):max(r(:,2))]; %! y = binopdf (x, 100, mean (pshatB)); %! plot (x, y, '-sc'); %! ylim ([0, 0.2]) %! legend ({'Normalized HIST of sample 1 with ps=0.15', ... %! 'Normalized HIST of sample 2 with ps=0.50', ... %! sprintf("PDF for sample 1 with estimated ps=%0.2f", ... %! mean (pshatA)), ... %! sprintf("PDF for sample 2 with estimated ps=%0.2f", ... %! mean (pshatB))}) %! title ('Two population samples from different binomial distributions') %! hold off ## Test output %!test %! x = 0:3; %! [pshat, psci] = binofit (x, 3); %! assert_equal (pshat, [0, 0.3333, 0.6667, 1], 1e-4); %! assert_equal (psci(1,:), [0, 0.7076], 1e-4); %! assert_equal (psci(2,:), [0.0084, 0.9057], 1e-4); %! assert_equal (psci(3,:), [0.0943, 0.9916], 1e-4); %! assert_equal (psci(4,:), [0.2924, 1.0000], 1e-4); ## Test input validation %!error ... %! binofit ([1 2 3 4]) %!error ... %! binofit ([-1, 4, 3, 2], [1, 2, 3, 3]) %!error binofit (ones (2), [1, 2, 3, 3]) %!error ... %! binofit ([1, 4, 3, 2], [1, 2, -1, 3]) %!error ... %! binofit ([1, 4, 3, 2], [5, 5, 5]) %!error ... %! binofit ([1, 4, 3, 2], [5, 3, 5, 5]) %!error binofit ([1, 2, 1], 3, 1.2); %!error binofit ([1, 2, 1], 3, 0); %!error binofit ([1, 2, 1], 3, 'alpha'); statistics-release-2.1.2/inst/Distribution_Fitting/binolike.m000066400000000000000000000131601527311104300244140ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} binolike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} binolike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} binolike (@var{params}, @var{x}, @var{freq}) ## ## Negative log-likelihood for the binomial distribution. ## ## @code{@var{nlogL} = binolike (@var{params}, @var{x})} returns the negative ## log likelihood of the binomial distribution with (1) parameter @var{n} and ## (2) parameter @var{ps}, given in the two-element vector @var{params}, where ## @var{n} is the number of trials and @var{ps} is the probability of success, ## given the number of successes in @var{x}. Unlike @code{binofit}, which ## handles each element in @var{x} independently, @code{binolike} returns the ## negative log likelihood of the entire vector @var{x}. ## ## @code{[@var{nlogL}, @var{acov}] = binolike (@var{params}, @var{x})} also ## returns the inverse of Fisher's information matrix, @var{acov}. If the input ## parameter values in @var{params} are the maximum likelihood estimates, the ## diagonal elements of @var{params} are their asymptotic variances. ## ## @code{[@dots{}] = binolike (@var{params}, @var{x}, @var{freq})} accepts a ## frequency vector, @var{freq}, of the same size as @var{x}. @var{freq} ## typically contains integer frequencies for the corresponding elements in ## @var{x}, but it can contain any non-integer non-negative values. By default, ## or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## Further information about the binomial distribution can be found at ## @url{https://en.wikipedia.org/wiki/Binomial_distribution} ## ## @seealso{binocdf, binoinv, binopdf, binornd, binofit, binostat} ## @end deftypefn function [nlogL, acov] = binolike (params, x, freq) ## Check input arguments if (nargin < 2) error ("binolike: function called with too few input arguments."); endif if (! isvector (x)) error ("binolike: X must be a vector."); endif if (length (params) != 2) error ("binolike: PARAMS must be a two-element vector."); endif if (params(1) < 0 || params(1) != round (params(1)) || isinf (params(1))) error (strcat ("binolike: number of trials, PARAMS(1), must be a", ... " finite non-negative integer.")); endif if (params(2) < 0 || params(2) > 1) error (strcat ("binolike: probability of success, PARAMS(2), must be", ... " in the range [0,1].")); endif ## Parse FREQ argument or add default if (nargin < 3 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("binolike: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("binolike: FREQ must not contain negative values."); endif ## Expand frequency vector (if necessary) if (! all (freq == 1)) ## Remove NaNs and zeros remove = isnan (freq) | freq == 0; x(remove) = []; freq(remove) = []; xf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; endfor x = xf; endif if (any (x < 0)) error ("binolike: X cannot have negative values."); endif if (any (x > params(1))) error (strcat ("binolike: number of successes, X, must be at least", ... " as large as the number of trials, N.")); endif ## Compute negative log-likelihood and asymptotic covariance n = params(1); ps = params(2); numx = length (x); nlogL = -sum (log (binopdf (x, n, ps))); tmp = ps * (1 - ps) / (n * numx); acov = [0, 0; 0, tmp]; endfunction ## Test output %!assert_equal (binolike ([3, 0.333], [0:3]), 6.8302, 1e-4) %!assert_equal (binolike ([3, 0.333], 0), 1.2149, 1e-4) %!assert_equal (binolike ([3, 0.333], 1), 0.8109, 1e-4) %!assert_equal (binolike ([3, 0.333], 2), 1.5056, 1e-4) %!assert_equal (binolike ([3, 0.333], 3), 3.2988, 1e-4) %!test %! [nlogL, acov] = binolike ([3, 0.333], 3); %! assert_equal (acov(4), 0.0740, 1e-4) ## Test input validation %!error binolike (3.25) %!error binolike ([5, 0.2], ones (2)) %!error ... %! binolike ([1, 0.2, 3], [1, 3, 5, 7]) %!error binolike ([1.5, 0.2], 1) %!error binolike ([-1, 0.2], 1) %!error binolike ([Inf, 0.2], 1) %!error binolike ([5, 1.2], [3, 5]) %!error binolike ([5, -0.2], [3, 5]) %!error ... %! binolike ([5, 0.5], ones (10, 1), ones (8,1)) %!error ... %! binolike ([5, 0.5], ones (1, 8), [1 1 1 1 1 1 1 -1]) %!error binolike ([5, 0.2], [-1, 3]) %!error binolike ([5, 0.2], [3, 5, 7]) statistics-release-2.1.2/inst/Distribution_Fitting/bisafit.m000066400000000000000000000222741527311104300242470ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{paramhat} =} bisafit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} bisafit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} bisafit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@dots{}] =} bisafit (@var{x}, @var{alpha}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} bisafit (@var{x}, @var{alpha}, @var{censor}, @var{freq}) ## @deftypefnx {statistics} {[@dots{}] =} bisafit (@var{x}, @var{alpha}, @var{censor}, @var{freq}, @var{options}) ## ## Estimate mean and confidence intervals for the Birnbaum-Saunders ## distribution. ## ## @code{@var{muhat} = bisafit (@var{x})} returns the maximum likelihood ## estimates of the parameters of the Birnbaum-Saunders distribution given the ## data in @var{x}. @qcode{@var{paramhat}(1)} is the scale parameter, ## @var{beta}, and @qcode{@var{paramhat}(2)} is the shape parameter, ## @var{gamma}. ## ## @code{[@var{paramhat}, @var{paramci}] = bisafit (@var{x})} returns the 95% ## confidence intervals for the parameter estimates. ## ## @code{[@dots{}] = bisafit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals for the ## parameter estimates. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = bisafit (@var{x}, @var{alpha}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = bisafit (@var{x}, @var{alpha}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## @code{[@dots{}] = bisafit (@dots{}, @var{options})} specifies control ## parameters for the iterative algorithm used to compute ML estimates with the ## @code{fminsearch} function. @var{options} is a structure with the following ## fields and their default values: ## @itemize ## @item @qcode{@var{options}.Display = "off"} ## @item @qcode{@var{options}.MaxFunEvals = 400} ## @item @qcode{@var{options}.MaxIter = 200} ## @item @qcode{@var{options}.TolX = 1e-6} ## @end itemize ## ## Further information about the Birnbaum-Saunders distribution can be found at ## @url{https://en.wikipedia.org/wiki/Birnbaum%E2%80%93Saunders_distribution} ## ## @seealso{bisacdf, bisainv, bisapdf, bisarnd, bisalike, bisastat} ## @end deftypefn function [paramhat, paramci] = bisafit (x, alpha, censor, freq, options) ## Check input arguments if (! isvector (x)) error ("bisafit: X must be a vector."); elseif (any (x <= 0)) error ("bisafit: X must contain only positive values."); endif ## Check alpha if (nargin < 2 || isempty (alpha)) alpha = 0.05; else if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("bisafit: wrong value for ALPHA."); endif endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("bisafit: X and CENSOR vectors mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("bisafit: X and FREQ vectors mismatch."); endif ## Get options structure or add defaults if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("bisafit: 'options' 5th argument must be a", ... " structure with 'Display', 'MaxFunEvals',", ... " 'MaxIter', and 'TolX' fields present.")); endif endif ## Starting points as suggested by Birnbaum and Saunders x_uncensored = x(censor==0); xubar = mean (x_uncensored); xuinv = mean (1 ./ x_uncensored); beta = sqrt (xubar ./ xuinv); gamma = 2 .* sqrt (sqrt (xubar .* xuinv) - 1); x0 = [beta, gamma]; ## Minimize negative log-likelihood to estimate parameters f = @(params) bisalike (params, x, censor, freq); [paramhat, ~, err, output] = fminsearch (f, x0, options); ## Force positive parameter values paramhat = abs (paramhat); ## Handle errors if (err == 0) if (output.funcCount >= options.MaxFunEvals) warning ("bisafit: maximum number of function evaluations are exceeded."); elseif (output.iterations >= options.MaxIter) warning ("bisafit: maximum number of iterations are exceeded."); endif elseif (err < 0) error ("bisafit: no solution."); endif ## Compute CIs using a log normal approximation for parameters. if (nargout > 1) ## Compute asymptotic covariance [~, acov] = bisalike (paramhat, x, censor, freq); ## Get standard errors stderr = sqrt (diag (acov))'; stderr = stderr ./ paramhat; ## Apply log transform phatlog = log (paramhat); ## Compute normal quantiles z = norminv (alpha / 2); ## Compute CI paramci = [phatlog; phatlog] + [stderr; stderr] .* [z, z; -z, -z]; ## Inverse log transform paramci = exp (paramci); endif endfunction %!demo %! ## Sample 3 populations from different Birnbaum-Saunders distributions %! rng (42); %! r1 = bisarnd (1, 0.5, 2000, 1); %! r2 = bisarnd (2, 0.3, 2000, 1); %! r3 = bisarnd (4, 0.5, 2000, 1); %! r = [r1, r2, r3]; %! %! ## Plot them normalized and fix their colors %! hist (r, 80, 4.2); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! set (h(3), 'facecolor', 'r'); %! ylim ([0, 1.1]); %! xlim ([0, 8]); %! hold on %! %! ## Estimate their α and β parameters %! beta_gammaA = bisafit (r(:,1)); %! beta_gammaB = bisafit (r(:,2)); %! beta_gammaC = bisafit (r(:,3)); %! %! ## Plot their estimated PDFs %! x = [0:0.1:8]; %! y = bisapdf (x, beta_gammaA(1), beta_gammaA(2)); %! plot (x, y, '-pr'); %! y = bisapdf (x, beta_gammaB(1), beta_gammaB(2)); %! plot (x, y, '-sg'); %! y = bisapdf (x, beta_gammaC(1), beta_gammaC(2)); %! plot (x, y, '-^c'); %! hold off %! legend ({'Normalized HIST of sample 1 with β=1 and γ=0.5', ... %! 'Normalized HIST of sample 2 with β=2 and γ=0.3', ... %! 'Normalized HIST of sample 3 with β=4 and γ=0.5', ... %! sprintf("PDF for sample 1 with estimated β=%0.2f and γ=%0.2f", ... %! beta_gammaA(1), beta_gammaA(2)), ... %! sprintf("PDF for sample 2 with estimated β=%0.2f and γ=%0.2f", ... %! beta_gammaB(1), beta_gammaB(2)), ... %! sprintf("PDF for sample 3 with estimated β=%0.2f and γ=%0.2f", ... %! beta_gammaC(1), beta_gammaC(2))}) %! title ('Three population samples from different Birnbaum-Saunders distributions') %! hold off ## Test output %!test %! paramhat = bisafit ([1:50]); %! paramhat_out = [16.2649, 1.0156]; %! assert_equal (paramhat, paramhat_out, 1e-4); %!test %! paramhat = bisafit ([1:5]); %! paramhat_out = [2.5585, 0.5839]; %! assert_equal (paramhat, paramhat_out, 1e-4); ## Test input validation %!error bisafit (ones (2,5)); %!error bisafit ([-1 2 3 4]); %!error bisafit ([1, 2, 3, 4, 5], 1.2); %!error bisafit ([1, 2, 3, 4, 5], 0); %!error bisafit ([1, 2, 3, 4, 5], 'alpha'); %!error ... %! bisafit ([1, 2, 3, 4, 5], 0.05, [1 1 0]); %!error ... %! bisafit ([1, 2, 3, 4, 5], [], [1 1 0 1 1]'); %!error ... %! bisafit ([1, 2, 3, 4, 5], 0.05, zeros (1,5), [1 1 0]); %!error ... %! bisafit ([1, 2, 3, 4, 5], [], [], [1 1 0 1 1]'); %!error ... %! bisafit ([1, 2, 3, 4, 5], 0.05, [], [], 2); statistics-release-2.1.2/inst/Distribution_Fitting/bisalike.m000066400000000000000000000153521527311104300244100ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} bisalike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} bisalike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} bisalike (@var{params}, @var{x}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} bisalike (@var{params}, @var{x}, @var{censor}, @var{freq}) ## ## Negative log-likelihood for the Birnbaum-Saunders distribution. ## ## @code{@var{nlogL} = bisalike (@var{params}, @var{x})} returns the negative ## log likelihood of the data in @var{x} corresponding to the Birnbaum-Saunders ## distribution with (1) scale parameter @var{beta} and (2) shape parameter ## @var{gamma} given in the two-element vector @var{params}. ## ## @code{[@var{nlogL}, @var{acov}] = bisalike (@var{params}, @var{x})} also ## returns the inverse of Fisher's information matrix, @var{acov}. If the input ## parameter values in @var{params} are the maximum likelihood estimates, the ## diagonal elements of @var{params} are their asymptotic variances. ## ## @code{[@dots{}] = bisalike (@var{params}, @var{x}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = bisalike (@var{params}, @var{x}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## Further information about the Birnbaum-Saunders distribution can be found at ## @url{https://en.wikipedia.org/wiki/Birnbaum%E2%80%93Saunders_distribution} ## ## @seealso{bisacdf, bisainv, bisapdf, bisarnd, bisafit, bisastat} ## @end deftypefn function [nlogL, acov] = bisalike (params, x, censor, freq) ## Check input arguments if (nargin < 2) error ("bisalike: function called with too few input arguments."); endif if (! isvector (x)) error ("bisalike: X must be a vector."); endif if (any (x < 0)) error ("bisalike: X cannot have negative values."); endif if (length (params) != 2) error ("bisalike: PARAMS must be a two-element vector."); endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("bisalike: X and CENSOR vector mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("bisalike: X and FREQ vector mismatch."); endif beta = params(1); gamma = params(2); z = (sqrt (x ./ beta) - sqrt (beta ./ x)) ./ gamma; w = (sqrt (x ./ beta) + sqrt (beta ./ x)) ./ gamma; L = -0.5 .* (z .^ 2 + log (2 .* pi)) + log (w) - log (2 .* x); n_censored = sum (freq .* censor); if (n_censored > 0) censored = (censor == 1); z_censored = z(censored); Scen = 0.5 * erfc (z_censored ./ sqrt (2)); L(censored) = log (Scen); endif nlogL = -sum (freq .* L); ## Compute asymptotic covariance if (nargout > 1) ## Compute first order central differences of the log-likelihood gradient dp = 0.0001 .* max (abs (params), 1); ngrad_p1 = bisa_ngrad (params + [dp(1), 0], x, censor, freq); ngrad_m1 = bisa_ngrad (params - [dp(1), 0], x, censor, freq); ngrad_p2 = bisa_ngrad (params + [0, dp(2)], x, censor, freq); ngrad_m2 = bisa_ngrad (params - [0, dp(2)], x, censor, freq); ## Compute negative Hessian by normalizing the differences by the increment nH = [(ngrad_p1(:) - ngrad_m1(:))./(2 * dp(1)), ... (ngrad_p2(:) - ngrad_m2(:))./(2 * dp(2))]; ## Force neg Hessian being symmetric nH = 0.5 .* (nH + nH'); ## Check neg Hessian is positive definite [R, p] = chol (nH); if (p > 0) warning ("bisalike: non positive definite Hessian matrix."); acov = NaN (2); return endif ## ACOV estimate is the negative inverse of the Hessian. Rinv = inv (R); acov = Rinv * Rinv; endif endfunction ## Helper function for computing negative gradient function ngrad = bisa_ngrad (params, x, censor, freq) beta = params(1); gamma = params(2); z = (sqrt (x ./ beta) - sqrt (beta ./ x)) ./ gamma; w = (sqrt (x ./ beta) + sqrt (beta ./ x)) ./ gamma; logphi = -0.5 .* (z .^ 2 + log (2 .* pi)); n_censored = sum (freq .* censor); if (n_censored > 0) censored = (censor == 1); z_censored = z(censored); Scen = 0.5 * erfc (z_censored ./ sqrt (2)); endif dL1 = (w .^ 2 - 1) .* 0.5 .* z ./ (w .* beta); dL2 = (z .^ 2 - 1) ./ gamma; if (n_censored > 0) phi_censored = exp (logphi(censored)); wcen = w(censored); d1Scen = phi_censored .* 0.5 .* wcen ./ beta; d2Scen = phi_censored .* z_censored ./ gamma; dL1(censored) = d1Scen ./ Scen; dL2(censored) = d2Scen ./ Scen; endif ngrad = -[sum(freq .* dL1), sum(freq .* dL2)]; endfunction ## Test results %!test %! nlogL = bisalike ([16.2649, 1.0156], [1:50]); %! assert_equal (nlogL, 215.5905, 1e-4); %!test %! nlogL = bisalike ([2.5585, 0.5839], [1:5]); %! assert_equal (nlogL, 8.9950, 1e-4); ## Test input validation %!error bisalike (3.25) %!error bisalike ([5, 0.2], ones (2)) %!error bisalike ([5, 0.2], [-1, 3]) %!error ... %! bisalike ([1, 0.2, 3], [1, 3, 5, 7]) %!error ... %! bisalike ([1.5, 0.2], [1:5], [0, 0, 0]) %!error ... %! bisalike ([1.5, 0.2], [1:5], [0, 0, 0, 0, 0], [1, 1, 1]) %!error ... %! bisalike ([1.5, 0.2], [1:5], [], [1, 1, 1]) statistics-release-2.1.2/inst/Distribution_Fitting/burrfit.m000066400000000000000000000352151527311104300243020ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{paramhat} =} burrfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} burrfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} burrfit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@dots{}] =} burrfit (@var{x}, @var{alpha}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} burrfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}) ## @deftypefnx {statistics} {[@dots{}] =} burrfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}, @var{options}) ## ## Estimate mean and confidence intervals for the Burr type XII distribution. ## ## @code{@var{muhat} = burrfit (@var{x})} returns the maximum likelihood ## estimates of the parameters of the Burr type XII distribution given the data ## in @var{x}. @qcode{@var{paramhat}(1)} is the scale parameter, @var{lambda}, ## @qcode{@var{paramhat}(2)} is the first shape parameter, @var{c}, and ## @qcode{@var{paramhat}(3)} is the second shape parameter, @var{k} ## ## @code{[@var{paramhat}, @var{paramci}] = burrfit (@var{x})} returns the 95% ## confidence intervals for the parameter estimates. ## ## @code{[@dots{}] = burrfit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals for the ## parameter estimates. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = burrfit (@var{x}, @var{alpha}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = burrfit (@var{x}, @var{alpha}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## @code{[@dots{}] = burrfit (@dots{}, @var{options})} specifies control ## parameters for the iterative algorithm used to compute the maximum likelihood ## estimates. @var{options} is a structure with the following field and its ## default value: ## @itemize ## @item @qcode{@var{options}.Display = "off"} ## @item @qcode{@var{options}.MaxFunEvals = 400} ## @item @qcode{@var{options}.MaxIter = 200} ## @item @qcode{@var{options}.TolX = 1e-6} ## @end itemize ## ## Further information about the Burr type XII distribution can be found at ## @url{https://en.wikipedia.org/wiki/Burr_distribution} ## ## @seealso{burrcdf, burrinv, burrpdf, burrrnd, burrlike, burrstat} ## @end deftypefn function [paramhat, paramci] = burrfit (x, alpha, censor, freq, options) ## Check input arguments if (! isvector (x)) error ("burrfit: X must be a vector."); elseif (any (x <= 0)) error ("burrfit: X must contain only positive values."); endif ## Check alpha if (nargin < 2 || isempty (alpha)) alpha = 0.05; else if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("burrfit: wrong value for ALPHA."); endif endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("burrfit: X and CENSOR vectors mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("burrfit: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("burrfit: FREQ must not contain negative values."); endif ## Get options structure or add defaults if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("burrfit: 'options' 5th argument must be a", ... " structure with 'Display', 'MaxFunEvals',", ... " 'MaxIter', and 'TolX' fields present.")); endif endif ## Force censoring vector into logical notc = ! censor; cens = ! notc; ## Check for identical data in X if (! isscalar (x) && max (abs (diff (x)) ./ x(2:end)) <= sqrt (eps)) warning ("burrfit: X must not contain identical data."); ## Return some sensical values for estimated parameters lambda = x(1); c = Inf; k = sum (notc .* freq) / sum (freq) / log (2); paramhat = [lambda, c, k]; if (nargout > 1) paramci = [paramhat; paramhat]; endif return endif ## Fit a Pareto distribution [paramhat_prt, nlogL_prt] = prtfit (x, cens, freq); ## Fit a Weibull distribution paramhat_wbl = wblfit (x, alpha, cens, freq); nlogL_wbl = wbllike (paramhat_wbl, x, cens, freq); ## Calculate the discriminator x_lambda = x ./ paramhat_wbl(1); x_lambdk = x_lambda .^ paramhat_wbl(2); discrimi = sum (freq .* (0.5 * x_lambdk .^ 2 - x_lambdk .* notc)); ## Compute Burr distribution if (discrimi > 0) ## Expand data (if necessary) if (any (freq != 1)) ## Preserve class x_expand = zeros (1, sum (freq), class (x)); id0 = 1; for idx = 1:numel (x) x_expand(id0:id0 + freq(idx) - 1) = x_lambda(idx); id0 += freq(idx); endfor else x_expand = x_lambda; endif ## Calculate median and 3rd quartile to estimate LAMBDA and C parameters Q = prctile (x_expand); xl_median = Q(3); xl_upperq = Q(4); ## Avoid median and upper quartile being too close together IRDdist = sqrt (eps (xl_median)) * xl_median; if ((xl_upperq - xl_median) < IRDdist) if (any (x_lambda > xl_upperq)) xl_upperq = min (x_lambda(x_lambda > xl_upperq)); elseif (any (x_lambda < xl_upperq)) xl_median = max (x_lambda(x_lambda < xl_median)); endif endif ## Compute starting LAMBDA and C, either directly or by minimization if (xl_median >= xl_upperq / xl_median) l0 = xl_median; c0 = log (3)/log (xl_upperq/xl_median); else l0 = 1; opts = optimset ('fzero'); opts = optimset (opts, 'Display', 'off'); cmax = log (realmax)/(2*log (xl_upperq/xl_median)); c0 = fzero (@(c)(xl_upperq/xl_median).^c-xl_median.^c-2, [0, cmax], opts); endif ## Calculate starting K from other starting parameters and scaled data k0 = exp (compute_logk (x_lambda, l0, c0, censor, freq)); ## Estimate parameters by minimizing the negative log-likelihood function f = @(params) burrlike (params, x_lambda, censor, freq); [paramhat, ~, err, output] = fminsearch (f, [l0, c0, k0], options); ## Force positive parameter values paramhat = abs (paramhat); ## Handle errors if (err == 0) if (output.funcCount >= options.MaxFunEvals) warning (strcat ("burrfit: maximum number of function", ... " evaluations are exceeded.")); elseif (output.iterations >= options.MaxIter) warning ("burrfit: maximum number of iterations are exceeded."); endif endif ## Scale back LAMBDA parameter paramhat(1) = paramhat(1) * paramhat_wbl(1); ## Compute negative log-likelihood with estimated parameters nlogL_burr = burrlike (paramhat, x, censor, freq); ## Check if fitting a Burr distribution is better than fitting a Pareto ## according to step 5 of the algorithmic implementation in Shao, 2004 if (paramhat(3) > 1e-6 && nlogL_burr < nlogL_prt) ## Compute CIs using a log normal approximation for phat. if (nargout > 1) ## Compute asymptotic covariance [~, acov] = burrlike (paramhat, x, censor, freq); ## Get standard errors stderr = sqrt (diag (acov))'; stderr = stderr ./ paramhat; ## Apply log transform phatlog = log (paramhat); ## Compute normal quantiles z = norminv (alpha / 2); ## Compute CI paramci = [phatlog; phatlog] + ... [stderr; stderr] .* [z, z, z; -z, -z, -z]; ## Inverse log transform paramci = exp (paramci); endif else if (nlogL_prt < nlogL_wbl) error ("burrfit: Pareto distribution fits better in X."); else error ("burrfit: Weibull distribution fits better in X."); endif endif else if (nlogL_prt < nlogL_wbl) error ("burrfit: Pareto distribution fits better in X."); else error ("burrfit: Weibull distribution fits better in X."); endif endif endfunction ## Helper function for fitting a Pareto distribution function [paramhat, nlogL] = prtfit (x, censor, freq) ## Force censoring vector into logical notc = ! censor; cens = ! notc; ## Compute MLE for x_m xm = x(notc); xm = min (xm); ## Handle case with all data censored if (all (cens)) paramhat = [max(x), NaN]; nlogL_prt = 0; return endif ## Compute some values logx = log (x); suml = sum (freq .* (logx - log (xm)) .* (x > xm)); sumf = sum (freq .* notc); ## Compute MLE for alpha a = sumf ./ suml; ## Add MLEs to returning vector paramhat = [xm, a]; ## Compute negative log-likelihood nlogL = a .* suml + sum (freq .* notc .* logx) - log (a) .* sumf; endfunction ## Helper function for computing K from X, LAMBDA, and C function logk = compute_logk (x, lambda, c, censor, freq) ## Force censoring vector into logical notc = ! censor; cens = ! notc; ## Precalculate some values xl = x ./ lambda; l1_xlc = log1p (xl .^ c); ## Avoid realmax overflow by approximation is_inf = isinf (l1_xlc); l1_xlc(is_inf) = c .* log (xl(is_inf)); if (sum (freq .* l1_xlc) < eps) lsxc = log (sum (freq .* (x .^ c))); if (isinf (lsxc)) [maxx, idx] = max (x); lsxc = c * log (freq(idx) * maxx); endif logk = log (sum (freq .* notc)) + c * log (lambda) - lsxc; else logk = log (sum (freq .* notc)) - log (sum (freq .* l1_xlc)); endif endfunction %!demo %! ## Sample 3 populations from different Burr type XII distributions %! rng (42); %! r1 = burrrnd (3.5, 2, 2.5, 10000, 1); %! r2 = burrrnd (1, 3, 1, 10000, 1); %! r3 = burrrnd (0.5, 2, 3, 10000, 1); %! r = [r1, r2, r3]; %! %! ## Plot them normalized and fix their colors %! hist (r, [0.1:0.2:20], [18, 5, 3]); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! set (h(3), 'facecolor', 'r'); %! ylim ([0, 3]); %! xlim ([0, 5]); %! hold on %! %! ## Estimate their α and β parameters %! lambda_c_kA = burrfit (r(:,1)); %! lambda_c_kB = burrfit (r(:,2)); %! lambda_c_kC = burrfit (r(:,3)); %! %! ## Plot their estimated PDFs %! x = [0.01:0.15:15]; %! y = burrpdf (x, lambda_c_kA(1), lambda_c_kA(2), lambda_c_kA(3)); %! plot (x, y, '-pr'); %! y = burrpdf (x, lambda_c_kB(1), lambda_c_kB(2), lambda_c_kB(3)); %! plot (x, y, '-sg'); %! y = burrpdf (x, lambda_c_kC(1), lambda_c_kC(2), lambda_c_kC(3)); %! plot (x, y, '-^c'); %! hold off %! legend ({'Normalized HIST of sample 1 with λ=3.5, c=2, and k=2.5', ... %! 'Normalized HIST of sample 2 with λ=1, c=3, and k=1', ... %! 'Normalized HIST of sample 3 with λ=0.5, c=2, and k=3', ... %! sprintf("PDF for sample 1 with estimated λ=%0.2f, c=%0.2f, and k=%0.2f", ... %! lambda_c_kA(1), lambda_c_kA(2), lambda_c_kA(3)), ... %! sprintf("PDF for sample 2 with estimated λ=%0.2f, c=%0.2f, and k=%0.2f", ... %! lambda_c_kB(1), lambda_c_kB(2), lambda_c_kB(3)), ... %! sprintf("PDF for sample 3 with estimated λ=%0.2f, c=%0.2f, and k=%0.2f", ... %! lambda_c_kC(1), lambda_c_kC(2), lambda_c_kC(3))}) %! title ('Three population samples from different Burr type XII distributions') %! hold off ## Test output %!test %! l = 1; c = 2; k = 3; %! r = burrrnd (l, c, k, 100000, 1); %! lambda_c_kA = burrfit (r); %! assert_equal (lambda_c_kA(1), l, 0.2); %! assert_equal (lambda_c_kA(2), c, 0.2); %! assert_equal (lambda_c_kA(3), k, 0.3); %!test %! l = 0.5; c = 1; k = 3; %! r = burrrnd (l, c, k, 100000, 1); %! lambda_c_kA = burrfit (r); %! assert_equal (lambda_c_kA(1), l, 0.2); %! assert_equal (lambda_c_kA(2), c, 0.2); %! assert_equal (lambda_c_kA(3), k, 0.3); %!test %! l = 1; c = 3; k = 1; %! r = burrrnd (l, c, k, 100000, 1); %! lambda_c_kA = burrfit (r); %! assert_equal (lambda_c_kA(1), l, 0.2); %! assert_equal (lambda_c_kA(2), c, 0.2); %! assert_equal (lambda_c_kA(3), k, 0.3); %!test %! l = 3; c = 2; k = 1; %! r = burrrnd (l, c, k, 100000, 1); %! lambda_c_kA = burrfit (r); %! assert_equal (lambda_c_kA(1), l, 0.2); %! assert_equal (lambda_c_kA(2), c, 0.2); %! assert_equal (lambda_c_kA(3), k, 0.3); %!test %! l = 4; c = 2; k = 4; %! r = burrrnd (l, c, k, 100000, 1); %! lambda_c_kA = burrfit (r); %! assert_equal (lambda_c_kA(1), l, 0.2); %! assert_equal (lambda_c_kA(2), c, 0.2); %! assert_equal (lambda_c_kA(3), k, 0.3); ## Test input validation %!error burrfit (ones (2,5)); %!error burrfit ([-1 2 3 4]); %!error burrfit ([1, 2, 3, 4, 5], 1.2); %!error burrfit ([1, 2, 3, 4, 5], 0); %!error burrfit ([1, 2, 3, 4, 5], 'alpha'); %!error ... %! burrfit ([1, 2, 3, 4, 5], 0.05, [1 1 0]); %!error ... %! burrfit ([1, 2, 3, 4, 5], [], [1 1 0 1 1]'); %!error %! burrfit ([1, 2, 3, 4, 5], 0.05, [], [1, 1, 5]) %!error %! burrfit ([1, 2, 3, 4, 5], 0.05, [], [1, 5, 1, 1, -1]) %!error ... %! burrfit ([1:10], 0.05, [], [], 5) statistics-release-2.1.2/inst/Distribution_Fitting/burrlike.m000066400000000000000000000153361527311104300244460ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} burrlike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} burrlike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} burrlike (@var{params}, @var{x}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} burrlike (@var{params}, @var{x}, @var{censor}, @var{freq}) ## ## Negative log-likelihood for the Burr type XII distribution. ## ## @code{@var{nlogL} = burrlike (@var{params}, @var{x})} returns the negative ## log likelihood of the data in @var{x} corresponding to the Burr type XII ## distribution with (1) scale parameter @var{lambda}, (2) first shape parameter ## @var{c}, and (3) second shape parameter @var{k} given in the three-element ## vector @var{params}. ## ## @code{[@var{nlogL}, @var{acov}] = burrlike (@var{params}, @var{x})} also ## returns the inverse of Fisher's information matrix, @var{acov}. If the input ## parameter values in @var{params} are the maximum likelihood estimates, the ## diagonal elements of @var{acov} are their asymptotic variances. ## ## @code{[@dots{}] = burrlike (@var{params}, @var{x}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = burrlike (@var{params}, @var{x}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## Further information about the Burr type XII distribution can be found at ## @url{https://en.wikipedia.org/wiki/Burr_distribution} ## ## @seealso{burrcdf, burrinv, burrpdf, burrrnd, burrfit, burrstat} ## @end deftypefn function [nlogL, acov] = burrlike (params, x, censor, freq) ## Check input arguments if (nargin < 2) error ("burrlike: function called with too few input arguments."); endif if (! isvector (x)) error ("burrlike: X must be a vector."); endif if (any (x < 0)) error ("burrlike: X cannot have negative values."); endif if (length (params) != 3) error ("burrlike: PARAMS must be a three-element vector."); endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("burrlike: X and CENSOR vector mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("burrlike: X and FREQ vector mismatch."); endif ## Get parameters lambda = params(1); c = params(2); k = params(3); ## Precalculate some values xl = x ./ lambda; log_xl = log (xl); l1_xlc = log1p (xl .^ c); ## Avoid realmax overflow by approximation is_inf = isinf (l1_xlc); l1_xlc(is_inf) = c .* log (xl(is_inf)); ## Force censoring vector into logical notc = ! censor; cens = ! notc; ## Compute neg-loglikelihood likeL = zeros (size (x)); likeL(notc) = (c - 1) .* log_xl(notc) - (k + 1) .* l1_xlc(notc); likeL(cens) = -k .* l1_xlc(cens); nlogL = sum (freq(notc)) * log (lambda / k / c) - sum (freq .* likeL); ## Compute asymptotic covariance if (nargout > 1) ## Preallocate variables nH = zeros (3); d2V1 = zeros (size (x)); d2V2 = d2V1; ## Precalculate some more values xlc = xl .^ c; log_xl = log (xl); xlc1 = (1 + xlc); xlc1sq = xlc1.^2; invxlc1sq = (1 + 1./xlc).^2; ## Find realmax overflow is_inf = isinf (xlc); is_fin = ! is_inf; ## Compute each element of the negative Hessian d2V1(is_fin) = -((1 + c) ./ xlc(is_fin) + 1) ./ invxlc1sq(is_fin); d2V1(is_inf) = -1; d2V2(notc) = d2V1(notc) .* (k + 1) + 1; d2V2(cens) = d2V1(cens) .* k; nH(1,1) = c ./ lambda .^ 2 .* sum (freq .* d2V2); d2V1(is_fin) = xlc(is_fin) .* (c .* log_xl(is_fin) + xlc1(is_fin)) ... ./ xlc1sq(is_fin); d2V1(is_inf) = 1; d2V2(notc) = (k + 1) .* d2V1(notc) - 1; d2V2(cens) = k .* d2V1(cens); nH(1,2) = sum (freq ./ lambda .* d2V2); nH(2,1) = nH(1,2); d2V1(is_fin) = xlc(is_fin) .* log_xl(is_fin) .^ 2 ./ xlc1sq(is_fin); d2V1(is_inf) = 0; d2V2(notc) = d2V1(notc) .* k + d2V1(notc); d2V2(cens) = d2V1(cens) .* k; nH(2,2) = -(sum (freq(notc))) ./ c .^ 2 - sum (freq .* d2V2); d2V1(is_fin) = xlc(is_fin) ./ xlc1(is_fin); d2V1(is_inf) = 1; nH(1,3) = (c ./ lambda) .* sum (freq .* d2V1); nH(3,1) = nH(1,3); nH(2,3) = -sum (freq .* d2V1 .* log_xl); nH(3,2) = nH(2,3); nH(3,3) = -(sum (freq(notc))) ./ k .^ 2; nH = -nH; ## Check negative Hessian is positive definite [R, p] = chol (nH); if (p > 0) warning ("burrlike: non positive definite Hessian matrix."); acov = NaN (3); return endif ## ACOV estimate is the negative inverse of the Hessian. Rinv = inv (R); acov = Rinv * Rinv; endif endfunction ## Test output ## Test input validation %!error burrlike (3.25) %!error burrlike ([1, 2, 3], ones (2)) %!error burrlike ([1, 2, 3], [-1, 3]) %!error ... %! burrlike ([1, 2], [1, 3, 5, 7]) %!error ... %! burrlike ([1, 2, 3, 4], [1, 3, 5, 7]) %!error ... %! burrlike ([1, 2, 3], [1:5], [0, 0, 0]) %!error ... %! burrlike ([1, 2, 3], [1:5], [0, 0, 0, 0, 0], [1, 1, 1]) %!error ... %! burrlike ([1, 2, 3], [1:5], [], [1, 1, 1]) statistics-release-2.1.2/inst/Distribution_Fitting/copulafit.m000066400000000000000000000257151527311104300246170ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{rho} =} copulafit (@qcode{"Gaussian"}, @var{u}) ## @deftypefnx {statistics} {[@var{rho}, @var{nu}] =} copulafit (@qcode{"t"}, @var{u}) ## @deftypefnx {statistics} {[@var{param}, @var{ci}] =} copulafit (@var{family}, @var{u}) ## @deftypefnx {statistics} {[@dots{}] =} copulafit (@dots{}, @qcode{"alpha"}, @var{a}) ## ## Fit a copula to data. ## ## @code{copulafit (@var{family}, @var{u})} returns the maximum-likelihood ## estimate of the parameter of a copula of the family @var{family}, fit to the ## data in @var{u}. The rows of @var{u} are observations and its columns are ## variables; all entries must lie strictly inside the unit interval ## @math{(0,1)}, as produced for example by a probability-integral transform or ## by @code{ecdf}/@code{ksdensity}. ## ## @var{family} is the copula family name. It can be @qcode{"Gaussian"} for the ## Gaussian family, @qcode{"t"} for the Student's t family, @qcode{"Clayton"} ## for the Clayton family, @qcode{"Gumbel"} for the Gumbel-Hougaard family, or ## @qcode{"Frank"} for the Frank family. ## ## The returned value depends on the family: ## ## @itemize @bullet ## @item For @qcode{"Gaussian"}, @code{@var{rho} = copulafit ("Gaussian", ## @var{u})} returns the estimated linear correlation matrix @var{rho}, computed ## as the sample correlation of the normal scores @code{norminv (@var{u})}. The ## data may have two or more columns. ## ## @item For @qcode{"t"}, @code{copulafit ("t", @var{u})} returns the estimated ## correlation matrix @var{rho} and the degrees of freedom @var{nu} as ## @code{[@var{rho}, @var{nu}]}, obtained by maximizing the copula ## log-likelihood. Only bivariate data (two columns) are supported. ## ## @item For the Archimedean families @qcode{"Clayton"}, @qcode{"Gumbel"}, and ## @qcode{"Frank"}, @code{[@var{param}, @var{ci}] = copulafit (@var{family}, ## @var{u})} returns the scalar copula parameter @var{param} and, optionally, a ## two-element vector @var{ci} with the lower and upper confidence bounds. Only ## bivariate data are supported. ## @end itemize ## ## @code{copulafit (@dots{}, @qcode{"alpha"}, @var{a})} sets the significance ## level for the confidence interval to @var{a}, so that @var{ci} has coverage ## @code{100 * (1 - @var{a})} percent. The default is @code{@var{a} = 0.05}. ## The confidence interval is a Wald interval whose standard error is obtained ## from the outer-product-of-gradients estimate of the information. ## ## @seealso{copulastat, copulaparam, copulacdf, copulapdf, copularnd} ## @end deftypefn function varargout = copulafit (family, u, varargin) ## Check arguments if (nargin < 2) print_usage (); endif if (! ischar (family)) error (strcat ("copulafit: FAMILY must be one of 'Gaussian',", ... " 't', 'Clayton', 'Gumbel', and 'Frank'.")); endif if (! isnumeric (u) || ! isreal (u) || ! ismatrix (u) || isempty (u)) error ("copulafit: U must be a nonempty numeric matrix."); endif if (any (u(:) <= 0) || any (u(:) >= 1)) error ("copulafit: U must have all values in the open interval (0, 1)."); endif [n, d] = size (u); if (d < 2) error ("copulafit: U must have at least two columns."); endif ## Parse the 'alpha' option alpha = 0.05; if (numel (varargin) > 0) if (numel (varargin) != 2 || ! ischar (varargin{1}) || ... ! strcmpi (varargin{1}, 'alpha')) error ("copulafit: invalid optional argument."); endif alpha = varargin{2}; if (! (isnumeric (alpha) && isscalar (alpha) && isreal (alpha) ... && alpha > 0 && alpha < 1)) error ("copulafit: ALPHA must be a scalar in the range (0, 1)."); endif endif lower_family = lower (family); switch (lower_family) case 'gaussian' if (nargout > 1) error (strcat ("copulafit: the Gaussian copula fit returns only", ... " the correlation matrix.")); endif varargout{1} = correlation_of_scores (norminv (u)); case 't' if (d != 2) error (strcat ("copulafit: the t copula fit supports bivariate", ... " data only.")); endif if (nargout > 2) error (strcat ("copulafit: confidence intervals are not supported", ... " for the t copula fit.")); endif [rho, nu] = fit_t (u); varargout{1} = [1, rho; rho, 1]; varargout{2} = nu; case {'clayton', 'gumbel', 'frank'} if (d != 2) error (strcat ("copulafit: the %s copula fit supports bivariate", ... " data only."), family); endif [param, ci] = fit_archimedean (lower_family, u, alpha); varargout{1} = param; if (nargout > 1) varargout{2} = ci; endif otherwise error ("copulafit: unknown copula family '%s'.", family); endswitch endfunction ## Sample correlation matrix of the columns of z (mean-removed, unit-normalised) function R = correlation_of_scores (z) R = corr (z); ## Guard the diagonal against round-off so it is exactly one R(1 : (rows (R) + 1) : end) = 1; endfunction ## Maximum-likelihood fit of the bivariate Student's t copula. Optimises the ## correlation and the degrees of freedom jointly on an unconstrained scale ## (rho = tanh (a), nu = exp (b)). function [rho, nu] = fit_t (u) rho0 = corr (norminv (u))(1, 2); p0 = [atanh(rho0); log(4)]; opts = optimset ("TolX", 1e-8, "TolFun", 1e-8, ... "MaxFunEvals", 20000, "MaxIter", 20000); p = fminsearch (@(p) t_copula_nll (u, tanh (p(1)), exp (p(2))), p0, opts); rho = tanh (p(1)); nu = exp (p(2)); endfunction ## Negative log-likelihood of the bivariate t copula function v = t_copula_nll (u, rho, nu) if (abs (rho) >= 1 || nu <= 0) v = Inf; return; endif R = [1, rho; rho, 1]; t = tinv (u, nu); c = mvtpdf (t, R, nu) ./ (tpdf (t(:, 1), nu) .* tpdf (t(:, 2), nu)); if (any (! isfinite (c)) || any (c <= 0)) v = Inf; return; endif v = -sum (log (c)); endfunction ## Maximum-likelihood fit of a bivariate Archimedean copula with a Wald ## confidence interval whose standard error uses the outer-product-of-gradients ## (BHHH) estimate of the information. function [param, ci] = fit_archimedean (family, u, alpha) a0 = archimedean_start (family); opts = optimset ("TolX", 1e-10, "TolFun", 1e-10, ... "MaxFunEvals", 10000, "MaxIter", 10000); param = fminsearch (@(a) -sum (log (copulapdf (family, u, a))), a0, opts); ## Per-observation score by central difference, then OPG information h = max (1e-6, abs (param) .* 1e-5); logc = @(a) log (copulapdf (family, u, a)); score = (logc (param + h) - logc (param - h)) ./ (2 .* h); se = 1 ./ sqrt (sum (score .^ 2)); z = norminv (1 - alpha ./ 2); ci = [param - z .* se, param + z .* se]; endfunction ## A robust starting value for the Archimedean maximum-likelihood search function a0 = archimedean_start (family) switch (family) case 'clayton' a0 = 1; case 'gumbel' a0 = 1.5; case 'frank' a0 = 1; endswitch endfunction %!demo %! ## Fit a Clayton copula to data and recover a confidence interval %! rng (42); %! randg ('state', 42); %! u = copularnd ("Clayton", 2, 500); %! [alpha, ci] = copulafit ("Clayton", u) %!demo %! ## Fit a Gaussian copula and report the correlation matrix %! rng (42); %! randg ('state', 42); %! u = copularnd ("Gaussian", 0.6, 500); %! rho = copulafit ("Gaussian", u) ## Shared probe data (near-Gaussian bivariate sample), reference values from ## MATLAB's copulafit. %!shared u %! u = [0.08,0.12; 0.17,0.25; 0.23,0.19; 0.31,0.42; 0.39,0.35; 0.46,0.51; ... %! 0.52,0.48; 0.58,0.63; 0.64,0.59; 0.71,0.68; 0.77,0.82; 0.83,0.79; ... %! 0.88,0.91; 0.93,0.87; 0.97,0.95]; ## Gaussian: sample correlation of the normal scores (exact match to MATLAB) %!test %! rho = copulafit ("Gaussian", u); %! assert_equal (rho, [1, 0.979591430658725; 0.979591430658725, 1], 1e-12); ## Archimedean point estimates and Wald confidence intervals (match MATLAB) %!test %! [a, ci] = copulafit ("Clayton", u); %! assert_equal (a, 8.70842970823662, 1e-6); %! assert_equal (ci, [4.48832722027934, 12.9285321961939], 1e-5); %!test %! [a, ci] = copulafit ("Frank", u); %! assert_equal (a, 29.7092786222299, 1e-5); %! assert_equal (ci, [7.05441865208815, 52.3641385923717], 1e-4); %!test %! [a, ci] = copulafit ("Gumbel", u); %! assert_equal (a, 6.14597585483989, 1e-6); %! assert_equal (ci, [2.94928782240414, 9.34266388727563], 1e-5); ## The t copula fit recovers the correlation (its nu is weakly identified for ## near-Gaussian data, so only rho is checked tightly). %!test %! [rho, nu] = copulafit ("t", u); %! assert_equal (rho, [1, 0.98084; 0.98084, 1], 2e-3); %! assert_equal (nu > 10, true); ## The confidence level widens the interval as alpha shrinks %!test %! [~, ci95] = copulafit ("Clayton", u); %! [~, ci99] = copulafit ("Clayton", u, "alpha", 0.01); %! assert_equal (ci99(1) < ci95(1) && ci99(2) > ci95(2), true); ## Round trip: fit recovers a parameter close to the generating one %!test %! rng (42); %! u = copularnd ("Clayton", 2, 2000); %! a = copulafit ("Clayton", u); %! assert_equal (a, 2, 0.3); ## Test input validation %!error ... %! copulafit (5, [0.2, 0.3]) %!error ... %! copulafit ("Gaussian", "foo") %!error ... %! copulafit ("Gaussian", [0.2, 0.3; 1.2, 0.5]) %!error ... %! copulafit ("Gaussian", [0.2; 0.3; 0.4]) %!error ... %! [a, b] = copulafit ("Gaussian", [0.2, 0.3; 0.4, 0.5]); %!error ... %! copulafit ("t", [0.2, 0.3, 0.4; 0.5, 0.6, 0.7]) %!error ... %! copulafit ("Clayton", [0.2, 0.3, 0.4; 0.5, 0.6, 0.7]) %!error ... %! copulafit ("Clayton", [0.2, 0.3; 0.4, 0.5], "foo") %!error ... %! copulafit ("Clayton", [0.2, 0.3; 0.4, 0.5], "alpha", 2) %!error ... %! copulafit ("Foo", [0.2, 0.3; 0.4, 0.5]) statistics-release-2.1.2/inst/Distribution_Fitting/doc-cache000066400000000000000000003345631527311104300242100ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 53 # name: # type: sq_string # elements: 1 # length: 7 betafit # name: # type: sq_string # elements: 1 # length: 2039 statistics: paramhat = betafit (x) statistics: [paramhat, paramci] = betafit (x) statistics: [paramhat, paramci] = betafit (x, alpha) statistics: [paramhat, paramci] = betafit (x, alpha, freq) statistics: [paramhat, paramci] = betafit (x, alpha, options) statistics: [paramhat, paramci] = betafit (x, alpha, freq, options) Estimate parameters and confidence intervals for the Beta distribution. paramhat = betafit (x) returns the maximum likelihood estimates of the parameters of the Beta distribution given the data in vector x. paramhat([1, 2]) corresponds to the α and β shape parameters, respectively. Missing values, NaNs, are ignored. [paramhat, paramci] = betafit (x) returns the 95% confidence intervals for the parameter estimates. [...] = betafit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals of the estimated parameter. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. [...] = betafit (params, x, freq) accepts a frequency vector, freq, of the same size as x. freq must contain non-negative integer frequencies for the corresponding elements in x. By default, or if left empty, freq = ones (size (x)). [paramhat, paramci] = betafit (x, alpha, options) specifies control parameters for the iterative algorithm used to compute ML estimates with the fminsearch function. options is a structure with the following fields and their default values: options.Display = "off" options.MaxFunEvals = 400 options.MaxIter = 200 options.TolX = 1e-6 The Beta distribution is defined on the open interval (0,1). However, betafit can also compute the unbounded beta likelihood function for data that include exact zeros or ones. In such cases, zeros and ones are treated as if they were values that have been left-censored at sqrt (realmin) or right-censored at 1 - eps/2, respectively. Further information about the Beta distribution can be found at https://en.wikipedia.org/wiki/Beta_distribution See also: betacdf, betainv, betapdf, betarnd, betalike, betastat # name: # type: sq_string # elements: 1 # length: 71 Estimate parameters and confidence intervals for the Beta distribution. # name: # type: sq_string # elements: 1 # length: 8 betalike # name: # type: sq_string # elements: 1 # length: 1467 statistics: nlogL = betalike (params, x) statistics: [nlogL, avar] = betalike (params, x) Negative log-likelihood for the Beta distribution. nlogL = betalike (params, x) returns the negative log likelihood of the data in x corresponding to the Beta distribution with (1) shape parameter α and (2) shape parameter β given in the two-element vector params. Both parameters must be positive real numbers and the data in the range [0,1]. Out of range parameters or data return NaN. [nlogL, avar] = betalike (params, x) returns the inverse of Fisher's information matrix, avar. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of params are their asymptotic variances. [...] = betalike (params, x, freq) accepts a frequency vector, freq, of the same size as x. freq must contain non-negative integer frequencies for the corresponding elements in x. By default, or if left empty, freq = ones (size (x)). The Beta distribution is defined on the open interval (0,1). However, betafit can also compute the unbounded beta likelihood function for data that include exact zeros or ones. In such cases, zeros and ones are treated as if they were values that have been left-censored at sqrt (realmin) or right-censored at 1 - eps/2, respectively. Further information about the Beta distribution can be found at https://en.wikipedia.org/wiki/Beta_distribution See also: betacdf, betainv, betapdf, betarnd, betafit, betastat # name: # type: sq_string # elements: 1 # length: 50 Negative log-likelihood for the Beta distribution. # name: # type: sq_string # elements: 1 # length: 7 binofit # name: # type: sq_string # elements: 1 # length: 1371 statistics: pshat = binofit (x, n) statistics: [pshat, psci] = binofit (x, n) statistics: [pshat, psci] = binofit (x, n, alpha) Estimate parameter and confidence intervals for the binomial distribution. pshat = binofit (x, n) returns the maximum likelihood estimate (MLE) of the probability of success for the binomial distribution. x and n are scalars containing the number of successes and the number of trials, respectively. If x and n are vectors, binofit returns a vector of estimates whose i-th element is the parameter estimate for x(i) and n(i). A scalar value for x or n is expanded to the same size as the other input. [pshat, psci] = binofit (x, n, alpha) also returns the 100 * (1 - alpha) percent confidence intervals of the estimated parameter. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. binofit treats a vector x as a collection of measurements from separate samples, and returns a vector of estimates. If you want to treat x as a single sample and compute a single parameter estimate and confidence interval, use binofit (sum (x), sum (n)) when n is a vector, and binofit (sum (x), n * length (x)) when n is a scalar. Further information about the binomial distribution can be found at https://en.wikipedia.org/wiki/Binomial_distribution See also: binocdf, binoinv, binopdf, binornd, binolike, binostat # name: # type: sq_string # elements: 1 # length: 74 Estimate parameter and confidence intervals for the binomial distribution. # name: # type: sq_string # elements: 1 # length: 8 binolike # name: # type: sq_string # elements: 1 # length: 1320 statistics: nlogL = binolike (params, x) statistics: [nlogL, acov] = binolike (params, x) statistics: [...] = binolike (params, x, freq) Negative log-likelihood for the binomial distribution. nlogL = binolike (params, x) returns the negative log likelihood of the binomial distribution with (1) parameter n and (2) parameter ps, given in the two-element vector params, where n is the number of trials and ps is the probability of success, given the number of successes in x. Unlike binofit, which handles each element in x independently, binolike returns the negative log likelihood of the entire vector x. [nlogL, acov] = binolike (params, x) also returns the inverse of Fisher's information matrix, acov. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of params are their asymptotic variances. [...] = binolike (params, x, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). Further information about the binomial distribution can be found at https://en.wikipedia.org/wiki/Binomial_distribution See also: binocdf, binoinv, binopdf, binornd, binofit, binostat # name: # type: sq_string # elements: 1 # length: 54 Negative log-likelihood for the binomial distribution. # name: # type: sq_string # elements: 1 # length: 7 bisafit # name: # type: sq_string # elements: 1 # length: 2031 statistics: paramhat = bisafit (x) statistics: [paramhat, paramci] = bisafit (x) statistics: [paramhat, paramci] = bisafit (x, alpha) statistics: [...] = bisafit (x, alpha, censor) statistics: [...] = bisafit (x, alpha, censor, freq) statistics: [...] = bisafit (x, alpha, censor, freq, options) Estimate mean and confidence intervals for the Birnbaum-Saunders distribution. muhat = bisafit (x) returns the maximum likelihood estimates of the parameters of the Birnbaum-Saunders distribution given the data in x. paramhat(1) is the scale parameter, beta, and paramhat(2) is the shape parameter, gamma. [paramhat, paramci] = bisafit (x) returns the 95% confidence intervals for the parameter estimates. [...] = bisafit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals for the parameter estimates. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = bisafit (x, alpha, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = bisafit (x, alpha, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). [...] = bisafit (..., options) specifies control parameters for the iterative algorithm used to compute ML estimates with the fminsearch function. options is a structure with the following fields and their default values: options.Display = "off" options.MaxFunEvals = 400 options.MaxIter = 200 options.TolX = 1e-6 Further information about the Birnbaum-Saunders distribution can be found at https://en.wikipedia.org/wiki/Birnbaum%E2%80%93Saunders_distribution See also: bisacdf, bisainv, bisapdf, bisarnd, bisalike, bisastat # name: # type: sq_string # elements: 1 # length: 78 Estimate mean and confidence intervals for the Birnbaum-Saunders distribution. # name: # type: sq_string # elements: 1 # length: 8 bisalike # name: # type: sq_string # elements: 1 # length: 1490 statistics: nlogL = bisalike (params, x) statistics: [nlogL, acov] = bisalike (params, x) statistics: [...] = bisalike (params, x, censor) statistics: [...] = bisalike (params, x, censor, freq) Negative log-likelihood for the Birnbaum-Saunders distribution. nlogL = bisalike (params, x) returns the negative log likelihood of the data in x corresponding to the Birnbaum-Saunders distribution with (1) scale parameter beta and (2) shape parameter gamma given in the two-element vector params. [nlogL, acov] = bisalike (params, x) also returns the inverse of Fisher's information matrix, acov. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of params are their asymptotic variances. [...] = bisalike (params, x, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = bisalike (params, x, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). Further information about the Birnbaum-Saunders distribution can be found at https://en.wikipedia.org/wiki/Birnbaum%E2%80%93Saunders_distribution See also: bisacdf, bisainv, bisapdf, bisarnd, bisafit, bisastat # name: # type: sq_string # elements: 1 # length: 63 Negative log-likelihood for the Birnbaum-Saunders distribution. # name: # type: sq_string # elements: 1 # length: 7 burrfit # name: # type: sq_string # elements: 1 # length: 2034 statistics: paramhat = burrfit (x) statistics: [paramhat, paramci] = burrfit (x) statistics: [paramhat, paramci] = burrfit (x, alpha) statistics: [...] = burrfit (x, alpha, censor) statistics: [...] = burrfit (x, alpha, censor, freq) statistics: [...] = burrfit (x, alpha, censor, freq, options) Estimate mean and confidence intervals for the Burr type XII distribution. muhat = burrfit (x) returns the maximum likelihood estimates of the parameters of the Burr type XII distribution given the data in x. paramhat(1) is the scale parameter, lambda, paramhat(2) is the first shape parameter, c, and paramhat(3) is the second shape parameter, k [paramhat, paramci] = burrfit (x) returns the 95% confidence intervals for the parameter estimates. [...] = burrfit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals for the parameter estimates. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = burrfit (x, alpha, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = burrfit (x, alpha, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). [...] = burrfit (..., options) specifies control parameters for the iterative algorithm used to compute the maximum likelihood estimates. options is a structure with the following field and its default value: options.Display = "off" options.MaxFunEvals = 400 options.MaxIter = 200 options.TolX = 1e-6 Further information about the Burr type XII distribution can be found at https://en.wikipedia.org/wiki/Burr_distribution See also: burrcdf, burrinv, burrpdf, burrrnd, burrlike, burrstat # name: # type: sq_string # elements: 1 # length: 74 Estimate mean and confidence intervals for the Burr type XII distribution. # name: # type: sq_string # elements: 1 # length: 8 burrlike # name: # type: sq_string # elements: 1 # length: 1492 statistics: nlogL = burrlike (params, x) statistics: [nlogL, acov] = burrlike (params, x) statistics: [...] = burrlike (params, x, censor) statistics: [...] = burrlike (params, x, censor, freq) Negative log-likelihood for the Burr type XII distribution. nlogL = burrlike (params, x) returns the negative log likelihood of the data in x corresponding to the Burr type XII distribution with (1) scale parameter lambda, (2) first shape parameter c, and (3) second shape parameter k given in the three-element vector params. [nlogL, acov] = burrlike (params, x) also returns the inverse of Fisher's information matrix, acov. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of acov are their asymptotic variances. [...] = burrlike (params, x, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = burrlike (params, x, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). Further information about the Burr type XII distribution can be found at https://en.wikipedia.org/wiki/Burr_distribution See also: burrcdf, burrinv, burrpdf, burrrnd, burrfit, burrstat # name: # type: sq_string # elements: 1 # length: 59 Negative log-likelihood for the Burr type XII distribution. # name: # type: sq_string # elements: 1 # length: 9 copulafit # name: # type: sq_string # elements: 1 # length: 1869 statistics: rho = copulafit ("Gaussian", u) statistics: [rho, nu] = copulafit ("t", u) statistics: [param, ci] = copulafit (family, u) statistics: [...] = copulafit (..., "alpha", a) Fit a copula to data. copulafit (family, u) returns the maximum-likelihood estimate of the parameter of a copula of the family family, fit to the data in u. The rows of u are observations and its columns are variables; all entries must lie strictly inside the unit interval (0,1), as produced for example by a probability-integral transform or by ecdf/ksdensity. family is the copula family name. It can be "Gaussian" for the Gaussian family, "t" for the Student's t family, "Clayton" for the Clayton family, "Gumbel" for the Gumbel-Hougaard family, or "Frank" for the Frank family. The returned value depends on the family: For "Gaussian", rho = copulafit ("Gaussian", u) returns the estimated linear correlation matrix rho, computed as the sample correlation of the normal scores norminv (u). The data may have two or more columns. For "t", copulafit ("t", u) returns the estimated correlation matrix rho and the degrees of freedom nu as [rho, nu], obtained by maximizing the copula log-likelihood. Only bivariate data (two columns) are supported. For the Archimedean families "Clayton", "Gumbel", and "Frank", [param, ci] = copulafit (family, u) returns the scalar copula parameter param and, optionally, a two-element vector ci with the lower and upper confidence bounds. Only bivariate data are supported. copulafit (..., "alpha", a) sets the significance level for the confidence interval to a, so that ci has coverage 100 * (1 - a) percent. The default is a = 0.05. The confidence interval is a Wald interval whose standard error is obtained from the outer-product-of-gradients estimate of the information. See also: copulastat, copulaparam, copulacdf, copulapdf, copularnd # name: # type: sq_string # elements: 1 # length: 21 Fit a copula to data. # name: # type: sq_string # elements: 1 # length: 5 evfit # name: # type: sq_string # elements: 1 # length: 2308 statistics: paramhat = evfit (x) statistics: [paramhat, paramci] = evfit (x) statistics: [paramhat, paramci] = evfit (x, alpha) statistics: [...] = evfit (x, alpha, censor) statistics: [...] = evfit (x, alpha, censor, freq) statistics: [...] = evfit (x, alpha, censor, freq, options) Estimate parameters and confidence intervals for the extreme value distribution. paramhat = evfit (x) returns the maximum likelihood estimates of the parameters of the extreme value distribution (also known as the Gumbel or the type I generalized extreme value distribution) given the data in x. paramhat(1) is the location parameter, mu, and paramhat(2) is the scale parameter, sigma. [paramhat, paramci] = evfit (x) returns the 95% confidence intervals for the parameter estimates. [...] = evfit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals for the parameter estimates. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = evfit (x, alpha, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = evfit (x, alpha, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). [...] = evfit (..., options) specifies control parameters for the iterative algorithm used to compute the maximum likelihood estimates. options is a structure with the following field and its default value: options.Display = "off" options.MaxFunEvals = 400 options.MaxIter = 200 options.TolX = 1e-6 The Gumbel distribution is used to model the distribution of the maximum (or the minimum) of a number of samples of various distributions. This version is suitable for modeling minima. For modeling maxima, use the alternative Gumbel fitting function, gumbelfit. Further information about the Gumbel distribution can be found at https://en.wikipedia.org/wiki/Gumbel_distribution See also: evcdf, evinv, evpdf, evrnd, evlike, evstat, gumbelfit # name: # type: sq_string # elements: 1 # length: 80 Estimate parameters and confidence intervals for the extreme value distribution. # name: # type: sq_string # elements: 1 # length: 6 evlike # name: # type: sq_string # elements: 1 # length: 1782 statistics: nlogL = evlike (params, x) statistics: [nlogL, acov] = evlike (params, x) statistics: [...] = evlike (params, x, censor) statistics: [...] = evlike (params, x, censor, freq) Negative log-likelihood for the extreme value distribution. nlogL = evlike (params, x) returns the negative log likelihood of the data in x corresponding to the extreme value distribution (also known as the Gumbel or the type I generalized extreme value distribution) with (1) location parameter mu and (2) scale parameter sigma given in the two-element vector params. [nlogL, acov] = evlike (params, x) also returns the inverse of Fisher's information matrix, acov. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of acov are their asymptotic variances. [...] = evlike (params, x, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = evlike (params, x, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). The Gumbel distribution is used to model the distribution of the maximum (or the minimum) of a number of samples of various distributions. This version is suitable for modeling minima. For modeling maxima, use the alternative Gumbel likelihood function, gumbellike. Further information about the Gumbel distribution can be found at https://en.wikipedia.org/wiki/Gumbel_distribution See also: evcdf, evinv, evpdf, evrnd, evfit, evstat, gumbellike # name: # type: sq_string # elements: 1 # length: 59 Negative log-likelihood for the extreme value distribution. # name: # type: sq_string # elements: 1 # length: 6 expfit # name: # type: sq_string # elements: 1 # length: 2622 statistics: muhat = expfit (x) statistics: [muhat, muci] = expfit (x) statistics: [muhat, muci] = expfit (x, alpha) statistics: [...] = expfit (x, alpha, censor) statistics: [...] = expfit (x, alpha, censor, freq) Estimate mean and confidence intervals for the exponential distribution. muhat = expfit (x) returns the maximum likelihood estimate of the mean parameter, muhat, of the exponential distribution given the data in x. x is expected to be a non-negative vector. If x is an array, the mean will be computed for each column of x. If any elements of x are NaN, that vector's mean will be returned as NaN. [muhat, muci] = expfit (x) returns the 95% confidence intervals for the parameter estimate. If x is a vector, muci is a two element column vector. If x is an array, each column of data will have a confidence interval returned as a two-row array. [...] = evfit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals for the parameter estimates. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. Any invalid values for alpha will return NaN for both CI bounds. [...] = expfit (x, alpha, censor) accepts a logical or numeric array, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. Any non-zero elements are regarded as 1s. By default, or if left empty, censor = zeros (size (x)). [...] = expfit (x, alpha, censor, freq) accepts a frequency array, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). Matlab incompatibility: Matlab's expfit produces unpredictable results for some cases with higher dimensions (specifically 1 x m x n x ... arrays). Octave's implementation allows for nxD arrays, consistently performing calculations on individual column vectors. Additionally, censor and freq can be used with arrays of any size, whereas Matlab only allows their use when x is a vector. A common alternative parameterization of the exponential distribution is to use the parameter λ defined as the mean number of events in an interval as opposed to the parameter μ, which is the mean wait time for an event to occur. λ and μ are reciprocals, i.e. μ = 1 / λ. Further information about the exponential distribution can be found at https://en.wikipedia.org/wiki/Exponential_distribution See also: expcdf, expinv, exppdf, exprnd, explike, expstat # name: # type: sq_string # elements: 1 # length: 72 Estimate mean and confidence intervals for the exponential distribution. # name: # type: sq_string # elements: 1 # length: 7 explike # name: # type: sq_string # elements: 1 # length: 1644 statistics: nlogL = explike (mu, x) statistics: [nlogL, avar] = explike (mu, x) statistics: [...] = explike (mu, x, censor) statistics: [...] = explike (mu, x, censor, freq) Negative log-likelihood for the exponential distribution. nlogL = explike (mu, x) returns the negative log likelihood of the data in x corresponding to the exponential distribution with mean parameter mu. x must be a vector of non-negative values, otherwise NaN is returned. [nlogL, avar] = explike (mu, x) also returns the inverse of Fisher's information matrix, avar. If the input mean parameter, mu, is the maximum likelihood estimate, avar is its asymptotic variance. [...] = explike (mu, x, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = explike (mu, x, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). A common alternative parameterization of the exponential distribution is to use the parameter λ defined as the mean number of events in an interval as opposed to the parameter μ, which is the mean wait time for an event to occur. λ and μ are reciprocals, i.e. μ = 1 / λ. Further information about the exponential distribution can be found at https://en.wikipedia.org/wiki/Exponential_distribution See also: expcdf, expinv, exppdf, exprnd, expfit, expstat # name: # type: sq_string # elements: 1 # length: 57 Negative log-likelihood for the exponential distribution. # name: # type: sq_string # elements: 1 # length: 6 gamfit # name: # type: sq_string # elements: 1 # length: 2330 statistics: paramhat = gamfit (x) statistics: [paramhat, paramci] = gamfit (x) statistics: [paramhat, paramci] = gamfit (x, alpha) statistics: [...] = gamfit (x, alpha, censor) statistics: [...] = gamfit (x, alpha, censor, freq) statistics: [...] = gamfit (x, alpha, censor, freq, options) Estimate parameters and confidence intervals for the Gamma distribution. paramhat = gamfit (x) returns the maximum likelihood estimates of the parameters of the Gamma distribution given the data in x. paramhat(1) is the shape parameter, a, and paramhat(2) is the scale parameter, b. [paramhat, paramci] = gamfit (x) returns the 95% confidence intervals for the parameter estimates. [...] = gamfit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals for the parameter estimates. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = gamfit (x, alpha, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = gamfit (x, alpha, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). [...] = gamfit (..., options) specifies control parameters for the iterative algorithm used to compute the maximum likelihood estimates. options is a structure with the following field and its default value: options.Display = "off" options.MaxFunEvals = 400 options.MaxIter = 200 options.TolX = 1e-6 OCTAVE/MATLAB use the alternative parameterization given by the pair α, β, i.e. shape a and scale b. In Wikipedia, the two common parameterizations use the pairs k, θ, as shape and scale, and α, β, as shape and rate, respectively. The parameter names a and b used here (for MATLAB compatibility) correspond to the parameter notation k, θ instead of the α, β as reported in Wikipedia. Further information about the Gamma distribution can be found at https://en.wikipedia.org/wiki/Gamma_distribution See also: gamcdf, gampdf, gaminv, gamrnd, gamlike # name: # type: sq_string # elements: 1 # length: 72 Estimate parameters and confidence intervals for the Gamma distribution. # name: # type: sq_string # elements: 1 # length: 7 gamlike # name: # type: sq_string # elements: 1 # length: 1795 statistics: nlogL = gamlike (params, x) statistics: [nlogL, acov] = gamlike (params, x) statistics: [...] = gamlike (params, x, censor) statistics: [...] = gamlike (params, x, censor, freq) Negative log-likelihood for the Gamma distribution. nlogL = gamlike (params, x) returns the negative log likelihood of the data in x corresponding to the Gamma distribution with (1) shape parameter a and (2) scale parameter b given in the two-element vector params. [nlogL, acov] = gamlike (params, x) also returns the inverse of Fisher's information matrix, acov. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of acov are their asymptotic variances. [...] = gamlike (params, x, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = gamlike (params, x, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). OCTAVE/MATLAB use the alternative parameterization given by the pair α, β, i.e. shape a and scale b. In Wikipedia, the two common parameterizations use the pairs k, θ, as shape and scale, and α, β, as shape and rate, respectively. The parameter names a and b used here (for MATLAB compatibility) correspond to the parameter notation k, θ instead of the α, β as reported in Wikipedia. Further information about the Gamma distribution can be found at https://en.wikipedia.org/wiki/Gamma_distribution See also: gamcdf, gampdf, gaminv, gamrnd, gamfit # name: # type: sq_string # elements: 1 # length: 51 Negative log-likelihood for the Gamma distribution. # name: # type: sq_string # elements: 1 # length: 6 geofit # name: # type: sq_string # elements: 1 # length: 1241 statistics: pshat = geofit (x) statistics: [pshat, psci] = geofit (x) statistics: [pshat, psci] = geofit (x, alpha) statistics: [pshat, psci] = geofit (x, alpha, freq) Estimate parameter and confidence intervals for the geometric distribution. pshat = geofit (x) returns the maximum likelihood estimate (MLE) of the probability of success for the geometric distribution. x must be a vector. [pshat, psci] = geofit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals of the estimated parameter. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = geofit (x, alpha, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). The geometric distribution models the number of failures (x) of a Bernoulli trial with probability ps before the first success. Further information about the geometric distribution can be found at https://en.wikipedia.org/wiki/Geometric_distribution See also: geocdf, geoinv, geopdf, geornd, geostat # name: # type: sq_string # elements: 1 # length: 75 Estimate parameter and confidence intervals for the geometric distribution. # name: # type: sq_string # elements: 1 # length: 6 gevfit # name: # type: sq_string # elements: 1 # length: 2481 statistics: paramhat = gevfit (x) statistics: [paramhat, paramci] = gevfit (x) statistics: [paramhat, paramci] = gevfit (x, alpha) statistics: [paramhat, paramci] = gevfit (x, alpha, freq) statistics: [paramhat, paramci] = gevfit (x, alpha, options) statistics: [paramhat, paramci] = gevfit (x, alpha, freq, options) Estimate parameters and confidence intervals for the generalized extreme value (GEV) distribution. paramhat = gevfit (x) returns the maximum likelihood estimates of the parameters of the GEV distribution given the data in x. paramhat(1) is the shape parameter, k, and paramhat(2) is the scale parameter, sigma, and paramhat(3) is the location parameter, mu. [paramhat, paramci] = gevfit (x) returns the 95% confidence intervals for the parameter estimates. [...] = gevfit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals for the parameter estimates. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = gevfit (params, x, freq) accepts a frequency vector, freq, of the same size as x. freq must contain non-negative integer frequencies for the corresponding elements in x. By default, or if left empty, freq = ones (size (x)). [paramhat, paramci] = gevfit (x, alpha, options) specifies control parameters for the iterative algorithm used to compute ML estimates with the fminsearch function. options is a structure with the following fields and their default values: options.Display = "off" options.MaxFunEvals = 400 options.MaxIter = 200 options.TolX = 1e-6 When k < 0, the GEV is the type III extreme value distribution. When k > 0, the GEV distribution is the type II, or Frechet, extreme value distribution. If W has a Weibull distribution as computed by the wblcdf function, then -W has a type III extreme value distribution and 1/W has a type II extreme value distribution. In the limit as k approaches 0, the GEV is the mirror image of the type I extreme value distribution as computed by the evcdf function. The mean of the GEV distribution is not finite when k >= 1, and the variance is not finite when k >= 1/2. The GEV distribution has positive density only for values of x such that k * (x - mu) / sigma > -1. Further information about the generalized extreme value distribution can be found at https://en.wikipedia.org/wiki/Generalized_extreme_value_distribution See also: gevcdf, gevinv, gevpdf, gevrnd, gevlike, gevstat # name: # type: sq_string # elements: 1 # length: 98 Estimate parameters and confidence intervals for the generalized extreme value (GEV) distribution. # name: # type: sq_string # elements: 1 # length: 11 gevfit_lmom # name: # type: sq_string # elements: 1 # length: 930 statistics: [paramhat, paramci] = gevfit_lmom (data) Find an estimator (paramhat) of the generalized extreme value (GEV) distribution fitting data using the method of L-moments. Arguments data is the vector of given values. Return values paramhat is the 3-parameter maximum-likelihood parameter vector [k; sigma; mu], where k is the shape parameter of the GEV distribution, sigma is the scale parameter of the GEV distribution, and mu is the location parameter of the GEV distribution. paramci has the approximate 95% confidence intervals of the parameter values (currently not implemented). Examples data = gevrnd (0.1, 1, 0, 100, 1); [pfit, pci] = gevfit_lmom (data); p1 = gevcdf (data,pfit(1),pfit(2),pfit(3)); [f, x] = ecdf (data); plot(data, p1, 's', x, f) References Ailliot, P.; Thompson, C. & Thomson, P. Mixed methods for fitting the GEV distribution, Water Resources Research, 2011, 47, W05551 See also: gevfit # name: # type: sq_string # elements: 1 # length: 124 Find an estimator (paramhat) of the generalized extreme value (GEV) distribution fitting data using the method of L-moments. # name: # type: sq_string # elements: 1 # length: 7 gevlike # name: # type: sq_string # elements: 1 # length: 2315 statistics: nlogL = gevlike (params, x) statistics: [nlogL, acov] = gevlike (params, x) statistics: [nlogL, acov] = gevlike (params, x, freq) Negative log-likelihood for the generalized extreme value (GEV) distribution. nlogL = gevlike (params, x) returns the negative log likelihood of the data in x corresponding to the GEV distribution with (1) shape parameter k, (2) scale parameter sigma, and (3) location parameter mu given in the three-element vector params. [nlogL, acov] = gevlike (params, x) also returns the inverse of Fisher's information matrix, acov. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of acov are their asymptotic variances. [...] = gevlike (params, x, freq) accepts a frequency vector, freq, of the same size as x. freq must contain non-negative integer frequencies for the corresponding elements in x. By default, or if left empty, freq = ones (size (x)). When k < 0, the GEV is the type III extreme value distribution. When k > 0, the GEV distribution is the type II, or Frechet, extreme value distribution. If W has a Weibull distribution as computed by the wblcdf function, then -W has a type III extreme value distribution and 1/W has a type II extreme value distribution. In the limit as k approaches 0, the GEV is the mirror image of the type I extreme value distribution as computed by the evcdf function. MATLAB compatibility. At exactly k = 0 the returned ACOV deviates from MATLAB's deliberately. Both implementations agree to thirteen digits for any k != 0, and both their k != 0 expressions converge on the values returned here as k approaches 0; MATLAB's own k = 0 branch returns something else entirely, which its k != 0 branch therefore contradicts. The Gumbel-limit expressions used here are the limits of the general ones, so ACOV is continuous at 0. This is independent of the sample size. The mean of the GEV distribution is not finite when k >= 1, and the variance is not finite when k >= 1/2. The GEV distribution has positive density only for values of x such that k * (x - mu) / sigma > -1. Further information about the generalized extreme value distribution can be found at https://en.wikipedia.org/wiki/Generalized_extreme_value_distribution See also: gevcdf, gevinv, gevpdf, gevrnd, gevfit, gevstat # name: # type: sq_string # elements: 1 # length: 77 Negative log-likelihood for the generalized extreme value (GEV) distribution. # name: # type: sq_string # elements: 1 # length: 5 gpfit # name: # type: sq_string # elements: 1 # length: 3136 statistics: paramhat = gpfit (x) statistics: [paramhat, paramci] = gpfit (x) statistics: [paramhat, paramci] = gpfit (x, alpha) statistics: [paramhat, paramci] = gpfit (x, alpha, options) statistics: [paramhat, paramci] = gpfit (x, alpha, options, freq) Estimate parameters and confidence intervals for the generalized Pareto distribution. paramhat = gpfit (x) returns the maximum likelihood estimates of the parameters of the generalized Pareto distribution given the data in x. paramhat(1) is the shape parameter, k, and paramhat(2) is the scale parameter, sigma. gpfit does not estimate the location parameter theta and assumes it to be zero, so x must not contain negative values. To fit data with a known nonzero theta, subtract it from x before calling gpfit; the estimates of k and sigma are unchanged by the shift. [paramhat, paramci] = gpfit (x) returns the 95% confidence intervals for the estimated parameters k and sigma as a 2-by-2 matrix whose first row holds the lower bounds and whose second row holds the upper bounds. [...] = gpfit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals for the parameter estimates. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = gpfit (x, alpha, options) specifies control parameters for the iterative algorithm used to compute ML estimates with the fminsearch function. options is a structure with the following fields and their default values: options.Display = "off" options.MaxFunEvals = 400 options.MaxIter = 200 options.TolX = 1e-6 [...] = gpfit (x, alpha, options, freq) accepts a vector of the same size as x giving the number of times each element of x was observed. This fourth argument is an Octave extension; MATLAB's gpfit takes three inputs at most. When the shape parameter falls below -1 the likelihood is unbounded: the density at the upper endpoint of the support diverges as that endpoint closes onto the largest observation, so no maximum likelihood estimate exists and whatever is returned is an arbitrary point on that ridge. gpfit warns in this case and returns NaN confidence intervals. The estimate it does return always keeps every observation strictly inside the fitted support, since the likelihood is infinite outside it. This is a deliberate deviation: MATLAB has been measured returning parameters for such data under which the largest observation has zero density and its own gplike returns Inf. When k = 0 and theta = 0, the Generalized Pareto is equivalent to the exponential distribution. When k > 0 and theta = sigma / k the Generalized Pareto is equivalent to the Pareto distribution. The mean of the Generalized Pareto is not finite when k >= 1 and the variance is not finite when k >= 1/2. When k >= 0, the Generalized Pareto has positive density for x > theta, or, when theta < 0, for 0 <= (x - theta) / sigma <= -1 / k. Further information about the generalized Pareto distribution can be found at https://en.wikipedia.org/wiki/Generalized_Pareto_distribution See also: gpcdf, gpinv, gppdf, gprnd, gplike, gpstat # name: # type: sq_string # elements: 1 # length: 85 Estimate parameters and confidence intervals for the generalized Pareto distribution. # name: # type: sq_string # elements: 1 # length: 6 gplike # name: # type: sq_string # elements: 1 # length: 1892 statistics: nlogL = gplike (params, x) statistics: [nlogL, acov] = gplike (params, x) statistics: [...] = gplike (params, x, freq) Negative log-likelihood for the generalized Pareto distribution. nlogL = gplike (params, x) returns the negative log-likelihood of the data in x corresponding to the generalized Pareto distribution with (1) shape parameter k and (2) scale parameter sigma given in the two-element vector params. gplike does not accept a location parameter theta and assumes it to be zero. If the location is known to be nonzero, subtract it from x before calling gplike. [nlogL, acov] = gplike (params, x) returns the inverse of Fisher's information matrix, acov, a 2-by-2 matrix. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of acov are their asymptotic variances. acov is based on the observed Fisher's information, not the expected information. [...] = gplike (params, x, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). This third argument is an Octave extension; MATLAB's gplike takes two inputs. When k = 0 and mu = 0, the Generalized Pareto CDF is equivalent to the exponential distribution. When k > 0 and mu = k / k the Generalized Pareto is equivalent to the Pareto distribution. The mean of the Generalized Pareto is not finite when k >= 1 and the variance is not finite when k >= 1/2. When k >= 0, the Generalized Pareto has positive density for x > mu, or, when mu < 0, for 0 <= (x - mu) / sigma <= -1 / k. Further information about the generalized Pareto distribution can be found at https://en.wikipedia.org/wiki/Generalized_Pareto_distribution See also: gpcdf, gpinv, gppdf, gprnd, gpfit, gpstat # name: # type: sq_string # elements: 1 # length: 64 Negative log-likelihood for the generalized Pareto distribution. # name: # type: sq_string # elements: 1 # length: 9 gumbelfit # name: # type: sq_string # elements: 1 # length: 2358 statistics: paramhat = gumbelfit (x) statistics: [paramhat, paramci] = gumbelfit (x) statistics: [paramhat, paramci] = gumbelfit (x, alpha) statistics: [...] = gumbelfit (x, alpha, censor) statistics: [...] = gumbelfit (x, alpha, censor, freq) statistics: [...] = gumbelfit (x, alpha, censor, freq, options) Estimate parameters and confidence intervals for Gumbel distribution. paramhat = gumbelfit (x) returns the maximum likelihood estimates of the parameters of the Gumbel distribution (also known as the extreme value or the type I generalized extreme value distribution) given in x. paramhat(1) is the location parameter, mu, and paramhat(2) is the scale parameter, beta. [paramhat, paramci] = gumbelfit (x) returns the 95% confidence intervals for the parameter estimates. [...] = gumbelfit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals for the parameter estimates. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = gumbelfit (x, alpha, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = gumbelfit (x, alpha, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). [...] = gumbelfit (..., options) specifies control parameters for the iterative algorithm used to compute the maximum likelihood estimates. options is a structure with the following field and its default value: options.Display = "off" options.MaxFunEvals = 400 options.MaxIter = 200 options.TolX = 1e-6 The Gumbel distribution is used to model the distribution of the maximum (or the minimum) of a number of samples of various distributions. This version is suitable for modeling maxima. For modeling minima, use the alternative extreme value fitting function, evfit. Further information about the Gumbel distribution can be found at https://en.wikipedia.org/wiki/Gumbel_distribution See also: gumbelcdf, gumbelinv, gumbelpdf, gumbelrnd, gumbellike, gumbelstat, evfit # name: # type: sq_string # elements: 1 # length: 69 Estimate parameters and confidence intervals for Gumbel distribution. # name: # type: sq_string # elements: 1 # length: 10 gumbellike # name: # type: sq_string # elements: 1 # length: 1836 statistics: nlogL = gumbellike (params, x) statistics: [nlogL, avar] = gumbellike (params, x) statistics: [...] = gumbellike (params, x, censor) statistics: [...] = gumbellike (params, x, censor, freq) Negative log-likelihood for the extreme value distribution. nlogL = gumbellike (params, x) returns the negative log likelihood of the data in x corresponding to the Gumbel distribution (also known as the extreme value or the type I generalized extreme value distribution) with (1) location parameter mu and (2) scale parameter beta given in the two-element vector params. [nlogL, acov] = gumbellike (params, x) also returns the inverse of Fisher's information matrix, acov. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of acov are their asymptotic variances. [...] = gumbellike (params, x, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = gumbellike (params, x, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). The Gumbel distribution is used to model the distribution of the maximum (or the minimum) of a number of samples of various distributions. This version is suitable for modeling maxima. For modeling minima, use the alternative extreme value likelihood function, evlike. Further information about the Gumbel distribution can be found at https://en.wikipedia.org/wiki/Gumbel_distribution See also: gumbelcdf, gumbelinv, gumbelpdf, gumbelrnd, gumbelfit, gumbelstat, evlike # name: # type: sq_string # elements: 1 # length: 59 Negative log-likelihood for the extreme value distribution. # name: # type: sq_string # elements: 1 # length: 5 hnfit # name: # type: sq_string # elements: 1 # length: 1581 statistics: [paramhat, paramci] = hnfit (x, mu) statistics: [paramhat, paramci] = hnfit (x, mu, alpha) statistics: [paramhat, paramci] = hnfit (x, mu, alpha, freq) Estimate parameters and confidence intervals for the half-normal distribution. paramhat = hnfit (x, mu) returns the maximum likelihood estimates of the parameters of the half-normal distribution given the data in vector x and the location parameter mu. paramhat(1) is the location parameter, mu, and paramhat(2) is the scale parameter, sigma. Although mu is returned in the estimated paramhat, hnfit does not estimate the location parameter mu, and it must be assumed to be known, given as a fixed parameter in input argument mu. [paramhat, paramci] = hnfit (x, mu) returns the 95% confidence intervals for the estimated scale parameter sigma. The first column of paramci includes the location parameter mu without any confidence bounds. [...] = hnfit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals of the estimated scale parameter. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. [...] = hnfit (params, x, freq) accepts a frequency vector, freq, of the same size as x. freq must contain non-negative integer frequencies for the corresponding elements in x. By default, or if left empty, freq = ones (size (x)). The half-normal CDF is only defined for x >= mu. Further information about the half-normal distribution can be found at https://en.wikipedia.org/wiki/Half-normal_distribution See also: hncdf, hninv, hnpdf, hnrnd, hnlike, hnstat # name: # type: sq_string # elements: 1 # length: 78 Estimate parameters and confidence intervals for the half-normal distribution. # name: # type: sq_string # elements: 1 # length: 6 hnlike # name: # type: sq_string # elements: 1 # length: 1170 statistics: nlogL = hnlike (params, x) statistics: [nlogL, acov] = hnlike (params, x) statistics: [nlogL, acov] = hnlike (params, x, freq) Negative log-likelihood for the half-normal distribution. nlogL = hnlike (params, x) returns the negative log likelihood of the data in x corresponding to the half-normal distribution with (1) location parameter mu and (2) scale parameter sigma given in the two-element vector params. [nlogL, acov] = hnlike (params, x) returns the inverse of Fisher's information matrix, acov. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of acov are their asymptotic variances. [...] = hnlike (params, x, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). The half-normal CDF is only defined for x >= mu. Further information about the half-normal distribution can be found at https://en.wikipedia.org/wiki/Half-normal_distribution See also: hncdf, hninv, hnpdf, hnrnd, hnfit, hnstat # name: # type: sq_string # elements: 1 # length: 57 Negative log-likelihood for the half-normal distribution. # name: # type: sq_string # elements: 1 # length: 7 invgfit # name: # type: sq_string # elements: 1 # length: 2016 statistics: paramhat = invgfit (x) statistics: [paramhat, paramci] = invgfit (x) statistics: [paramhat, paramci] = invgfit (x, alpha) statistics: [...] = invgfit (x, alpha, censor) statistics: [...] = invgfit (x, alpha, censor, freq) statistics: [...] = invgfit (x, alpha, censor, freq, options) Estimate mean and confidence intervals for the inverse Gaussian distribution. mu0 = invgfit (x) returns the maximum likelihood estimates of the parameters of the inverse Gaussian distribution given the data in x. paramhat(1) is the scale parameter, mu, and paramhat(2) is the shape parameter, lambda. [paramhat, paramci] = invgfit (x) returns the 95% confidence intervals for the parameter estimates. [...] = invgfit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals for the parameter estimates. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = invgfit (x, alpha, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = invgfit (x, alpha, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). [...] = invgfit (..., options) specifies control parameters for the iterative algorithm used to compute ML estimates with the fminsearch function. options is a structure with the following fields and their default values: options.Display = "off" options.MaxFunEvals = 400 options.MaxIter = 200 options.TolX = 1e-6 Further information about the inverse Gaussian distribution can be found at https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution See also: invgcdf, invginv, invgpdf, invgrnd, invglike, invgstat # name: # type: sq_string # elements: 1 # length: 77 Estimate mean and confidence intervals for the inverse Gaussian distribution. # name: # type: sq_string # elements: 1 # length: 8 invglike # name: # type: sq_string # elements: 1 # length: 1477 statistics: nlogL = invglike (params, x) statistics: [nlogL, acov] = invglike (params, x) statistics: [...] = invglike (params, x, censor) statistics: [...] = invglike (params, x, censor, freq) Negative log-likelihood for the inverse Gaussian distribution. nlogL = invglike (params, x) returns the negative log likelihood of the data in x corresponding to the inverse Gaussian distribution with (1) scale parameter mu and (2) shape parameter lambda given in the two-element vector params. [nlogL, acov] = invglike (params, x) also returns the inverse of Fisher's information matrix, acov. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of params are their asymptotic variances. [...] = invglike (params, x, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = invglike (params, x, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). Further information about the inverse Gaussian distribution can be found at https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution See also: invgcdf, invginv, invgpdf, invgrnd, invgfit, invgstat # name: # type: sq_string # elements: 1 # length: 62 Negative log-likelihood for the inverse Gaussian distribution. # name: # type: sq_string # elements: 1 # length: 7 logifit # name: # type: sq_string # elements: 1 # length: 1979 statistics: paramhat = logifit (x) statistics: [paramhat, paramci] = logifit (x) statistics: [paramhat, paramci] = logifit (x, alpha) statistics: [...] = logifit (x, alpha, censor) statistics: [...] = logifit (x, alpha, censor, freq) statistics: [...] = logifit (x, alpha, censor, freq, options) Estimate mean and confidence intervals for the logistic distribution. mu0 = logifit (x) returns the maximum likelihood estimates of the parameters of the logistic distribution given the data in x. paramhat(1) is the scale parameter, mu, and paramhat(2) is the shape parameter, s. [paramhat, paramci] = logifit (x) returns the 95% confidence intervals for the parameter estimates. [...] = logifit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals for the parameter estimates. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = logifit (x, alpha, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = logifit (x, alpha, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). [...] = logifit (..., options) specifies control parameters for the iterative algorithm used to compute ML estimates with the fminsearch function. options is a structure with the following fields and their default values: options.Display = "off" options.MaxFunEvals = 400 options.MaxIter = 200 options.TolX = 1e-6 Further information about the logistic distribution can be found at https://en.wikipedia.org/wiki/Logistic_distribution See also: logicdf, logiinv, logipdf, logirnd, logilike, logistat # name: # type: sq_string # elements: 1 # length: 69 Estimate mean and confidence intervals for the logistic distribution. # name: # type: sq_string # elements: 1 # length: 8 logilike # name: # type: sq_string # elements: 1 # length: 1447 statistics: nlogL = logilike (params, x) statistics: [nlogL, acov] = logilike (params, x) statistics: [...] = logilike (params, x, censor) statistics: [...] = logilike (params, x, censor, freq) Negative log-likelihood for the logistic distribution. nlogL = logilike (params, x) returns the negative log likelihood of the data in x corresponding to the logistic distribution with (1) location parameter mu and (2) scale parameter sigma given in the two-element vector params. [nlogL, acov] = logilike (params, x) also returns the inverse of Fisher's information matrix, acov. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of params are their asymptotic variances. [...] = logilike (params, x, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = logilike (params, x, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). Further information about the logistic distribution can be found at https://en.wikipedia.org/wiki/Logistic_distribution See also: logicdf, logiinv, logipdf, logirnd, logifit, logistat # name: # type: sq_string # elements: 1 # length: 54 Negative log-likelihood for the logistic distribution. # name: # type: sq_string # elements: 1 # length: 7 loglfit # name: # type: sq_string # elements: 1 # length: 2241 statistics: paramhat = loglfit (x) statistics: [paramhat, paramci] = loglfit (x) statistics: [paramhat, paramci] = loglfit (x, alpha) statistics: [...] = loglfit (x, alpha, censor) statistics: [...] = loglfit (x, alpha, censor, freq) statistics: [...] = loglfit (x, alpha, censor, freq, options) Estimate mean and confidence intervals for the log-logistic distribution. mu0 = loglfit (x) returns the maximum likelihood estimates of the parameters of the log-logistic distribution given the data in x. paramhat(1) is the mean parameter, mu, and paramhat(2) is the scale parameter, sigma. [paramhat, paramci] = loglfit (x) returns the 95% confidence intervals for the parameter estimates. [...] = loglfit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals for the parameter estimates. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = loglfit (x, alpha, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = loglfit (x, alpha, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). [...] = loglfit (..., options) specifies control parameters for the iterative algorithm used to compute ML estimates with the fminsearch function. options is a structure with the following fields and their default values: options.Display = "off" options.MaxFunEvals = 400 options.MaxIter = 200 options.TolX = 1e-6 Further information about the loglogistic distribution can be found at https://en.wikipedia.org/wiki/Log-logistic_distribution OCTAVE/MATLAB use an alternative parameterization given by the pair μ, σ, i.e. mu and sigma, in analogy with the logistic distribution. Their relation to the α and b parameters used in Wikipedia are given below: mu = log (a) sigma = 1 / a See also: loglcdf, loglinv, loglpdf, loglrnd, logllike, loglstat # name: # type: sq_string # elements: 1 # length: 73 Estimate mean and confidence intervals for the log-logistic distribution. # name: # type: sq_string # elements: 1 # length: 8 logllike # name: # type: sq_string # elements: 1 # length: 1698 statistics: nlogL = logllike (params, x) statistics: [nlogL, acov] = logllike (params, x) statistics: [...] = logllike (params, x, censor) statistics: [...] = logllike (params, x, censor, freq) Negative log-likelihood for the log-logistic distribution. nlogL = logllike (params, x) returns the negative log likelihood of the data in x corresponding to the log-logistic distribution with (1) scale parameter a and (2) shape parameter b given in the two-element vector params. [nlogL, acov] = logllike (params, x) also returns the inverse of Fisher's information matrix, acov. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of params are their asymptotic variances. [...] = logllike (params, x, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = logllike (params, x, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). Further information about the loglogistic distribution can be found at https://en.wikipedia.org/wiki/Log-logistic_distribution OCTAVE/MATLAB use an alternative parameterization given by the pair μ, σ, i.e. mu and sigma, in analogy with the logistic distribution. Their relation to the α and b parameters used in Wikipedia are given below: mu = log (a) sigma = 1 / a See also: loglcdf, loglinv, loglpdf, loglrnd, loglfit, loglstat # name: # type: sq_string # elements: 1 # length: 58 Negative log-likelihood for the log-logistic distribution. # name: # type: sq_string # elements: 1 # length: 7 lognfit # name: # type: sq_string # elements: 1 # length: 2374 statistics: paramhat = lognfit (x) statistics: [paramhat, paramci] = lognfit (x) statistics: [paramhat, paramci] = lognfit (x, alpha) statistics: [...] = lognfit (x, alpha, censor) statistics: [...] = lognfit (x, alpha, censor, freq) statistics: [...] = lognfit (x, alpha, censor, freq, options) Estimate parameters and confidence intervals for the lognormal distribution. paramhat = lognfit (x) returns the maximum likelihood estimates of the parameters of the lognormal distribution given the data in vector x. paramhat([1, 2]) corresponds to the mean and standard deviation, respectively, of the associated normal distribution. If a random variable follows this distribution, its logarithm is normally distributed with mean mu and standard deviation sigma. [paramhat, paramci] = lognfit (x) returns the 95% confidence intervals for the parameter estimates. [...] = lognfit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals for the parameter estimates. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = lognfit (x, alpha, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = lognfit (x, alpha, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). [...] = lognfit (..., options) specifies control parameters for the iterative algorithm used to compute ML estimates with the fminsearch function. options is a structure with the following fields and their default values: options.Display = "off" options.MaxFunEvals = 400 options.MaxIter = 200 options.TolX = 1e-6 With no censor, the estimate of the standard deviation, paramhat(2), is the square root of the unbiased estimate of the variance of log (x). With censored data, the maximum likelihood estimate is returned. Further information about the lognormal distribution can be found at https://en.wikipedia.org/wiki/Log-normal_distribution See also: logncdf, logninv, lognpdf, lognrnd, lognlike, lognstat # name: # type: sq_string # elements: 1 # length: 76 Estimate parameters and confidence intervals for the lognormal distribution. # name: # type: sq_string # elements: 1 # length: 8 lognlike # name: # type: sq_string # elements: 1 # length: 1835 statistics: nlogL = lognlike (params, x) statistics: [nlogL, avar] = lognlike (params, x) statistics: [...] = lognlike (params, x, censor) statistics: [...] = lognlike (params, x, censor, freq) Negative log-likelihood for the lognormal distribution. nlogL = lognlike (params, x) returns the negative log-likelihood of the data in x corresponding to the lognormal distribution with (1) location parameter mu and (2) scale parameter sigma given in the two-element vector params, which correspond to the mean and standard deviation of the associated normal distribution. Missing values, NaNs, are ignored. Negative values of x are treated as missing values. If a random variable follows this distribution, its logarithm is normally distributed with mean mu and standard deviation sigma. [nlogL, avar] = lognlike (params, x) returns the inverse of Fisher's information matrix, avar. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of avar are their asymptotic variances. avar is based on the observed Fisher's information, not the expected information. [...] = lognlike (params, x, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = lognlike (params, x, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). Further information about the lognormal distribution can be found at https://en.wikipedia.org/wiki/Log-normal_distribution See also: logncdf, logninv, lognpdf, lognrnd, lognfit, lognstat # name: # type: sq_string # elements: 1 # length: 55 Negative log-likelihood for the lognormal distribution. # name: # type: sq_string # elements: 1 # length: 7 nakafit # name: # type: sq_string # elements: 1 # length: 1936 statistics: paramhat = nakafit (x) statistics: [paramhat, paramci] = nakafit (x) statistics: [paramhat, paramci] = nakafit (x, alpha) statistics: [...] = nakafit (x, alpha, censor) statistics: [...] = nakafit (x, alpha, censor, freq) statistics: [...] = nakafit (x, alpha, censor, freq, options) Estimate mean and confidence intervals for the Nakagami distribution. mu0 = nakafit (x) returns the maximum likelihood estimates of the parameters of the Nakagami distribution given the data in x. paramhat(1) is the shape parameter, mu, and paramhat(2) is the spread parameter, omega. [paramhat, paramci] = nakafit (x) returns the 95% confidence intervals for the parameter estimates. [...] = nakafit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals for the parameter estimates. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = nakafit (x, alpha, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = nakafit (params, x, censor, freq) accepts a frequency vector, freq, of the same size as x. freq must contain non-negative integer frequencies for the corresponding elements in x. By default, or if left empty, freq = ones (size (x)). [...] = nakafit (..., options) specifies control parameters for the iterative algorithm used to compute ML estimates with the fminsearch function. options is a structure with the following fields and their default values: options.Display = "off" options.MaxFunEvals = 400 options.MaxIter = 200 options.TolX = 1e-6 Further information about the Nakagami distribution can be found at https://en.wikipedia.org/wiki/Nakagami_distribution See also: nakacdf, nakainv, nakapdf, nakarnd, nakalike, nakastat # name: # type: sq_string # elements: 1 # length: 69 Estimate mean and confidence intervals for the Nakagami distribution. # name: # type: sq_string # elements: 1 # length: 8 nakalike # name: # type: sq_string # elements: 1 # length: 1396 statistics: nlogL = nakalike (params, x) statistics: [nlogL, acov] = nakalike (params, x) statistics: [...] = nakalike (params, x, censor) statistics: [...] = nakalike (params, x, censor, freq) Negative log-likelihood for the Nakagami distribution. nlogL = nakalike (params, x) returns the negative log likelihood of the data in x corresponding to the Nakagami distribution with (1) shape parameter mu and (2) spread parameter omega given in the two-element vector params. [nlogL, acov] = nakalike (params, x) also returns the inverse of Fisher's information matrix, acov. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of params are their asymptotic variances. [...] = nakalike (params, x, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = nakalike (params, x, censor, freq) accepts a frequency vector, freq, of the same size as x. freq must contain non-negative integer frequencies for the corresponding elements in x. By default, or if left empty, freq = ones (size (x)). Further information about the Nakagami distribution can be found at https://en.wikipedia.org/wiki/Nakagami_distribution See also: nakacdf, nakainv, nakapdf, nakarnd, nakafit, nakastat # name: # type: sq_string # elements: 1 # length: 54 Negative log-likelihood for the Nakagami distribution. # name: # type: sq_string # elements: 1 # length: 7 nbinfit # name: # type: sq_string # elements: 1 # length: 2904 statistics: paramhat = nbinfit (x) statistics: [paramhat, paramci] = nbinfit (x) statistics: [paramhat, paramci] = nbinfit (x, alpha) statistics: [paramhat, paramci] = nbinfit (x, alpha, freq) statistics: [paramhat, paramci] = nbinfit (x, alpha, options) statistics: [paramhat, paramci] = nbinfit (x, alpha, freq, options) Estimate parameter and confidence intervals for the negative binomial distribution. paramhat = nbinfit (x) returns the maximum likelihood estimates of the parameters of the negative binomial distribution given the data in vector x. paramhat(1) is the number of successes until the experiment is stopped, r, and paramhat(2) is the probability of success in each experiment, ps. [paramhat, paramci] = nbinfit (x) returns the 95% confidence intervals for the parameter estimates. [paramhat, paramci] = nbinfit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals of the estimated parameter. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. [...] = nbinlike (params, x, freq) accepts a frequency vector, freq, of the same size as x. freq must contain non-negative integer frequencies for the corresponding elements in x. By default, or if left empty, freq = ones (size (x)). [paramhat, paramci] = nbinfit (x, alpha, options) specifies control parameters for the iterative algorithm used to compute ML estimates with the fminsearch function. options is a structure with the following fields and their default values: options.Display = "off" options.MaxFunEvals = 400 options.MaxIter = 200 options.TolX = 1e-6 When r is an integer, the negative binomial distribution is also known as the Pascal distribution and it models the number of failures in x before a specified number of successes is reached in a series of independent, identical trials. Its parameters are the probability of success in a single trial, ps, and the number of successes, r. A special case of the negative binomial distribution, when r = 1, is the geometric distribution, which models the number of failures before the first success. r can also have non-integer positive values, in which form the negative binomial distribution, also known as the Polya distribution, has no interpretation in terms of repeated trials, but, like the Poisson distribution, it is useful in modeling count data. The negative binomial distribution is more general than the Poisson distribution because it has a variance that is greater than its mean, making it suitable for count data that do not meet the assumptions of the Poisson distribution. In the limit, as r increases to infinity, the negative binomial distribution approaches the Poisson distribution. Further information about the negative binomial distribution can be found at https://en.wikipedia.org/wiki/Negative_binomial_distribution See also: nbincdf, nbininv, nbinpdf, nbinrnd, nbinlike, nbinstat # name: # type: sq_string # elements: 1 # length: 83 Estimate parameter and confidence intervals for the negative binomial distribution. # name: # type: sq_string # elements: 1 # length: 8 nbinlike # name: # type: sq_string # elements: 1 # length: 2339 statistics: nlogL = nbinlike (params, x) statistics: [nlogL, avar] = nbinlike (params, x) statistics: [nlogL, avar] = nbinlike (params, x, freq) Negative log-likelihood for the negative binomial distribution. nlogL = nbinlike (params, x) returns the negative log likelihood of the negative binomial distribution with (1) parameter r and (2) parameter ps, given in the two-element vector params, where r is the number of successes until the experiment is stopped and ps is the probability of success in each experiment, given the number of failures in x. [nlogL, avar] = nbinlike (params, x) also returns the inverse of Fisher's information matrix, avar. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of params are their asymptotic variances. [...] = nbinlike (params, x, freq) accepts a frequency vector, freq, of the same size as x. freq must contain non-negative integer frequencies for the corresponding elements in x. By default, or if left empty, freq = ones (size (x)). When r is an integer, the negative binomial distribution is also known as the Pascal distribution and it models the number of failures in x before a specified number of successes is reached in a series of independent, identical trials. Its parameters are the probability of success in a single trial, ps, and the number of successes, r. A special case of the negative binomial distribution, when r = 1, is the geometric distribution, which models the number of failures before the first success. r can also have non-integer positive values, in which form the negative binomial distribution, also known as the Polya distribution, has no interpretation in terms of repeated trials, but, like the Poisson distribution, it is useful in modeling count data. The negative binomial distribution is more general than the Poisson distribution because it has a variance that is greater than its mean, making it suitable for count data that do not meet the assumptions of the Poisson distribution. In the limit, as r increases to infinity, the negative binomial distribution approaches the Poisson distribution. Further information about the negative binomial distribution can be found at https://en.wikipedia.org/wiki/Negative_binomial_distribution See also: nbincdf, nbininv, nbinpdf, nbinrnd, nbinfit, nbinstat # name: # type: sq_string # elements: 1 # length: 63 Negative log-likelihood for the negative binomial distribution. # name: # type: sq_string # elements: 1 # length: 7 normfit # name: # type: sq_string # elements: 1 # length: 3196 statistics: muhat = normfit (x) statistics: [muhat, sigmahat] = normfit (x) statistics: [muhat, sigmahat, muci] = normfit (x) statistics: [muhat, sigmahat, muci, sigmaci] = normfit (x) statistics: [...] = normfit (x, alpha) statistics: [...] = normfit (x, alpha, censor) statistics: [...] = normfit (x, alpha, censor, freq) statistics: [...] = normfit (x, alpha, censor, freq, options) Estimate parameters and confidence intervals for the normal distribution. [muhat, sigmahat] = normfit (x) estimates the parameters of the normal distribution given the data in x. muhat is an estimate of the mean, and sigmahat is an estimate of the standard deviation. [muhat, sigmahat, muci, sigmaci] = normfit (x) returns the 95% confidence intervals for the mean and standard deviation estimates in the arrays muci and sigmaci, respectively. x can be a vector or a matrix. When x is a matrix, the parameter estimates and their confidence intervals are computed for each column. In this case, normfit supports only 2 input arguments, x and alpha. Optional arguments censor, freq, and options can be used only when x is a vector. alpha is a scalar value in the range (0,1) specifying the confidence level for the confidence intervals calculated as 100x(1 - alpha)%. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. censor is a logical vector of the same length as x specifying whether each value in x is right-censored or not. 1 indicates observations that are right-censored and 0 indicates observations that are fully observed. With censoring, muhat and sigmahat are the maximum likelihood estimates (MLEs). If empty, the default is an array of 0s, meaning that all observations are fully observed. freq is a vector of the same length as x and it typically contains non-negative integer counts of the corresponding elements in x. If empty, the default is an array of 1s, meaning one observation per element of x. To obtain the weighted MLEs for a data set with censoring, specify weights of observations, normalized to the number of observations in x. However, when there is no censored data (default), the returned estimate for standard deviation is not exactly the WMLE. To compute the weighted MLE, multiply the value returned in sigmahat by sqrt ((sum (freq) - 1) / sum (freq)). The square root is needed because the factor corrects a variance, while sigmahat is a standard deviation. This correction is needed because normfit normally computes sigmahat using an unbiased variance estimator when there is no censored data. When there is censoring in the data, the correction is not needed, since normfit does not use the unbiased variance estimator in that case. options is a structure with the control parameters for fminsearch which is used internally to compute MLEs for censored data. By default, it uses the following options: options.Display = "off" options.MaxFunEvals = 400 options.MaxIter = 200 options.TolX = 1e-6 Further information about the normal distribution can be found at https://en.wikipedia.org/wiki/Normal_distribution See also: normcdf, norminv, normpdf, normrnd, normlike, normstat # name: # type: sq_string # elements: 1 # length: 73 Estimate parameters and confidence intervals for the normal distribution. # name: # type: sq_string # elements: 1 # length: 8 normlike # name: # type: sq_string # elements: 1 # length: 1411 statistics: nlogL = normlike (params, x) statistics: [nlogL, avar] = normlike (params, x) statistics: [...] = normlike (params, x, censor) statistics: [...] = normlike (params, x, censor, freq) Negative log-likelihood for the normal distribution. nlogL = normlike (params, x) returns the negative log-likelihood for the normal distribution, evaluated at parameters params(1) = mean and params(2) = standard deviation, given x. nlogL is a scalar. [nlogL, avar] = normlike (params, x) returns the inverse of Fisher's information matrix, avar. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of avar are their asymptotic variances. avar is based on the observed Fisher's information, not the expected information. [...] = normlike (params, x, censor) accepts a boolean vector of the same size as x that is 1 for observations that are right-censored and 0 for observations that are observed exactly. [...] = normlike (params, x, censor, freq) accepts a frequency vector of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it may contain any non-integer non-negative values. Pass in [] for censor to use its default value. Further information about the normal distribution can be found at https://en.wikipedia.org/wiki/Normal_distribution See also: normcdf, norminv, normpdf, normrnd, normfit, normstat # name: # type: sq_string # elements: 1 # length: 52 Negative log-likelihood for the normal distribution. # name: # type: sq_string # elements: 1 # length: 8 poissfit # name: # type: sq_string # elements: 1 # length: 1255 statistics: lambdahat = poissfit (x) statistics: [lambdahat, lambdaci] = poissfit (x) statistics: [lambdahat, lambdaci] = poissfit (x, alpha) statistics: [lambdahat, lambdaci] = poissfit (x, alpha, freq) Estimate parameter and confidence intervals for the Poisson distribution. lambdahat = poissfit (x) returns the maximum likelihood estimate of the rate parameter, lambda, of the Poisson distribution given the data in x. x must be a vector of non-negative values. [lambdahat, lambdaci] = poissfit (x) returns the 95% confidence intervals for the parameter estimate. [lambdahat, lambdaci] = poissfit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals of the estimated parameter. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = poissfit (x, alpha, freq) accepts a frequency vector or matrix, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x. freq cannot contain negative values. Further information about the Poisson distribution can be found at https://en.wikipedia.org/wiki/Poisson_distribution See also: poisscdf, poissinv, poisspdf, poissrnd, poisslike, poisstat # name: # type: sq_string # elements: 1 # length: 73 Estimate parameter and confidence intervals for the Poisson distribution. # name: # type: sq_string # elements: 1 # length: 9 poisslike # name: # type: sq_string # elements: 1 # length: 1072 statistics: nlogL = poisslike (lambda, x) statistics: [nlogL, avar] = poisslike (lambda, x) statistics: [...] = poisslike (lambda, x, freq) Negative log-likelihood for the Poisson distribution. nlogL = poisslike (lambda, x) returns the negative log likelihood of the data in x corresponding to the Poisson distribution with rate parameter lambda. x must be a vector of non-negative values. [nlogL, avar] = poisslike (lambda, x) also returns the inverse of Fisher's information matrix, avar. If the input rate parameter, lambda, is the maximum likelihood estimate, avar is its asymptotic variance. [...] = poisslike (lambda, x, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). Further information about the Poisson distribution can be found at https://en.wikipedia.org/wiki/Poisson_distribution See also: poisscdf, poissinv, poisspdf, poissrnd, poissfit, poisstat # name: # type: sq_string # elements: 1 # length: 53 Negative log-likelihood for the Poisson distribution. # name: # type: sq_string # elements: 1 # length: 7 raylfit # name: # type: sq_string # elements: 1 # length: 1624 statistics: sigmaA = raylfit (x) statistics: [sigmaA, sigmaci] = raylfit (x) statistics: [sigmaA, sigmaci] = raylfit (x, alpha) statistics: [sigmaA, sigmaci] = raylfit (x, alpha, censor) statistics: [sigmaA, sigmaci] = raylfit (x, alpha, censor, freq) Estimate parameter and confidence intervals for the Rayleigh distribution. sigmaA = raylfit (x) returns the maximum likelihood estimate of the rate parameter, lambda, of the Rayleigh distribution given the data in x. x must be a vector of non-negative values. [sigmaA, sigmaci] = raylfit (x) returns the 95% confidence intervals for the parameter estimate. [sigmaA, sigmaci] = raylfit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals of the estimated parameter. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = raylfit (x, alpha, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = raylfit (x, alpha, censor, freq) accepts a frequency vector or matrix, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x. freq cannot contain negative values. Further information about the Rayleigh distribution can be found at https://en.wikipedia.org/wiki/Rayleigh_distribution The prob.RayleighDistribution class names this same parameter B, after MATLAB. See also: raylcdf, raylinv, raylpdf, raylrnd, rayllike, raylstat # name: # type: sq_string # elements: 1 # length: 74 Estimate parameter and confidence intervals for the Rayleigh distribution. # name: # type: sq_string # elements: 1 # length: 8 rayllike # name: # type: sq_string # elements: 1 # length: 1137 statistics: nlogL = rayllike (sigma, x) statistics: [nlogL, acov] = rayllike (sigma, x) statistics: [...] = rayllike (sigma, x, freq) Negative log-likelihood for the Rayleigh distribution. nlogL = rayllike (sigma, x) returns the negative log likelihood of the data in x corresponding to the Rayleigh distribution with rate parameter sigma. x must be a vector of non-negative values. [nlogL, acov] = rayllike (sigma, x) also returns the inverse of Fisher's information matrix, acov. If the input rate parameter, sigma, is the maximum likelihood estimate, acov is its asymptotic variance. [...] = rayllike (sigma, x, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). Further information about the Rayleigh distribution can be found at https://en.wikipedia.org/wiki/Rayleigh_distribution The prob.RayleighDistribution class names this same parameter B, after MATLAB. See also: raylcdf, raylinv, raylpdf, raylrnd, raylfit, raylstat # name: # type: sq_string # elements: 1 # length: 54 Negative log-likelihood for the Rayleigh distribution. # name: # type: sq_string # elements: 1 # length: 7 ricefit # name: # type: sq_string # elements: 1 # length: 1991 statistics: paramhat = ricefit (x) statistics: [paramhat, paramci] = ricefit (x) statistics: [paramhat, paramci] = ricefit (x, alpha) statistics: [...] = ricefit (x, alpha, censor) statistics: [...] = ricefit (x, alpha, censor, freq) statistics: [...] = ricefit (x, alpha, censor, freq, options) Estimate parameters and confidence intervals for the Rician distribution. paramhat = ricefit (x) returns the maximum likelihood estimates of the parameters of the Rician distribution given the data in x. paramhat(1) is the non-centrality (distance) parameter, s, and paramhat(2) is the scale parameter, sigma. [paramhat, paramci] = ricefit (x) returns the 95% confidence intervals for the parameter estimates. [...] = ricefit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals for the parameter estimates. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = ricefit (x, alpha, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = ricefit (x, alpha, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). [...] = ricefit (..., options) specifies control parameters for the iterative algorithm used to compute the maximum likelihood estimates. options is a structure with the following field and its default value: options.Display = "off" options.MaxFunEvals = 1000 options.MaxIter = 500 options.TolX = 1e-6 Further information about the Rician distribution can be found at https://en.wikipedia.org/wiki/Rice_distribution See also: ricecdf, ricepdf, riceinv, ricernd, ricelike, ricestat # name: # type: sq_string # elements: 1 # length: 73 Estimate parameters and confidence intervals for the Rician distribution. # name: # type: sq_string # elements: 1 # length: 8 ricelike # name: # type: sq_string # elements: 1 # length: 1453 statistics: nlogL = ricelike (params, x) statistics: [nlogL, acov] = ricelike (params, x) statistics: [...] = ricelike (params, x, censor) statistics: [...] = ricelike (params, x, censor, freq) Negative log-likelihood for the Rician distribution. nlogL = ricelike (params, x) returns the negative log likelihood of the data in x corresponding to the Rician distribution with (1) non-centrality (distance) parameter s and (2) scale parameter sigma given in the two-element vector params. [nlogL, acov] = ricelike (params, x) also returns the inverse of Fisher's information matrix, acov. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of params are their asymptotic variances. [...] = ricelike (params, x, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = ricelike (params, x, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). Further information about the Rician distribution can be found at https://en.wikipedia.org/wiki/Rice_distribution See also: ricecdf, riceinv, ricepdf, ricernd, ricefit, ricestat # name: # type: sq_string # elements: 1 # length: 52 Negative log-likelihood for the Rician distribution. # name: # type: sq_string # elements: 1 # length: 7 stblfit # name: # type: sq_string # elements: 1 # length: 2476 statistics: paramhat = stblfit (x) statistics: [paramhat, paramci] = stblfit (x) statistics: [paramhat, paramci] = stblfit (x, alpha) statistics: [paramhat, paramci] = stblfit (x, alpha, freq) statistics: [paramhat, paramci] = stblfit (x, alpha, options) statistics: [paramhat, paramci] = stblfit (x, alpha, freq, options) Estimate parameters and confidence intervals for the stable distribution. paramhat = stblfit (x) returns the maximum likelihood estimates of the parameters of the stable distribution, in the Nolan S0 parameterization, given the data in x. paramhat(1) is the tail index alpha, paramhat(2) is the skewness beta, paramhat(3) is the scale gam, and paramhat(4) is the location delta. [paramhat, paramci] = stblfit (x) returns the 95% confidence intervals for the parameter estimates. The intervals are Wald intervals from the observed Fisher information. [...] = stblfit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals for the parameter estimates. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default value. [...] = stblfit (x, alpha, freq) accepts a frequency vector, freq, of the same size as x. freq must contain non-negative integer frequencies for the corresponding elements in x. By default, or if left empty, freq = ones (size (x)). [paramhat, paramci] = stblfit (x, alpha, options) specifies control parameters for the iterative algorithm used to compute the ML estimates with the fminsearch function. options is a structure with the following fields and their default values: options.Display = "off" options.MaxFunEvals = 400 options.MaxIter = 200 options.TolX = 1e-6 The stable density has no closed form; it is evaluated by numerical inversion of the characteristic function, which makes fitting considerably slower than for the closed-form distributions. Censoring is not supported. The estimates are the maximum-likelihood estimates under the mathematically exact density. MATLAB fits an interpolation-based approximation of the stable density, whose maximum-likelihood estimates deviate from the exact ones by about 10^{-2} (and the resulting confidence intervals by up to roughly 20%); stblfit returns the exact (more accurate) estimates. Further information about the stable distribution can be found at https://en.wikipedia.org/wiki/Stable_distribution See also: stbllike, stblpdf, stblcdf, stblinv, stblrnd, fitdist, makedist # name: # type: sq_string # elements: 1 # length: 73 Estimate parameters and confidence intervals for the stable distribution. # name: # type: sq_string # elements: 1 # length: 8 stbllike # name: # type: sq_string # elements: 1 # length: 1268 statistics: nlogL = stbllike (params, x) statistics: [nlogL, acov] = stbllike (params, x) statistics: [nlogL, acov] = stbllike (params, x, freq) Negative log-likelihood for the stable distribution. nlogL = stbllike (params, x) returns the negative log-likelihood of the data in x corresponding to the stable distribution, in the Nolan S0 parameterization, with (1) tail index alpha, (2) skewness beta, (3) scale gam, and (4) location delta given in the four-element vector params. [nlogL, acov] = stbllike (params, x) also returns the inverse of the observed Fisher information matrix, acov. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of acov are their asymptotic variances. acov is based on the numerically evaluated Hessian of the negative log-likelihood, since the stable density has no closed form. [...] = stbllike (params, x, freq) accepts a frequency vector, freq, of the same size as x. freq must contain non-negative integer frequencies for the corresponding elements in x. By default, or if left empty, freq = ones (size (x)). Further information about the stable distribution can be found at https://en.wikipedia.org/wiki/Stable_distribution See also: stblfit, stblpdf, stblcdf, stblinv, stblrnd # name: # type: sq_string # elements: 1 # length: 52 Negative log-likelihood for the stable distribution. # name: # type: sq_string # elements: 1 # length: 6 tlsfit # name: # type: sq_string # elements: 1 # length: 2098 statistics: paramhat = tlsfit (x) statistics: [paramhat, paramci] = tlsfit (x) statistics: [paramhat, paramci] = tlsfit (x, alpha) statistics: [...] = tlsfit (x, alpha, censor) statistics: [...] = tlsfit (x, alpha, censor, freq) statistics: [...] = tlsfit (x, alpha, censor, freq, options) Estimate parameters and confidence intervals for the Location-scale Student's T distribution. muhat = tlsfit (x) returns the maximum likelihood estimates of the parameters of the location-scale T distribution given the data in x. paramhat(1) is the location parameter, mu, paramhat(2) is the scale parameter, sigma, and paramhat(3) is the degrees of freedom, nu. [paramhat, paramci] = tlsfit (x) returns the 95% confidence intervals for the parameter estimates. [...] = tlsfit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals for the parameter estimates. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = tlsfit (x, alpha, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = tlsfit (x, alpha, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). [...] = tlsfit (..., options) specifies control parameters for the iterative algorithm used to compute ML estimates with the fminsearch function. options is a structure with the following fields and their default values: options.Display = "off" options.MaxFunEvals = 400 options.MaxIter = 200 options.TolX = 1e-6 Further information about the location-scale Student's T distribution can be found at https://en.wikipedia.org/wiki/Student%27s_t-distribution#Location-scale_t_distribution See also: tlscdf, tlsinv, tlspdf, tlsrnd, tlslike, tlsstat # name: # type: sq_string # elements: 1 # length: 93 Estimate parameters and confidence intervals for the Location-scale Student's T distribution. # name: # type: sq_string # elements: 1 # length: 7 tlslike # name: # type: sq_string # elements: 1 # length: 1629 statistics: nlogL = tlslike (params, x) statistics: [nlogL, acov] = tlslike (params, x) statistics: [...] = tlslike (params, x, alpha, censor) statistics: [...] = tlslike (params, x, alpha, censor, freq) Negative log-likelihood for the location-scale Student's T distribution. nlogL = tlslike (params, x) returns the negative log-likelihood of the x in x corresponding to the location-scale T distribution with (1) location parameter mu, (2) scale parameter sigma and (3) degrees of freedom nu given in the three-element vector params. [nlogL, acov] = tlslike (params, x) also returns the inverse of Fisher's information matrix, acov. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of acov are their asymptotic variances. acov is based on the observed Fisher's information, not the expected information. [...] = tlslike (params, x, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = tlslike (params, x, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but may contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). Further information about the location-scale Student's T distribution can be found at https://en.wikipedia.org/wiki/Student%27s_t-distribution#Location-scale_t_distribution See also: tlscdf, tlsinv, tlspdf, tlsrnd, tlsfit, tlsstat # name: # type: sq_string # elements: 1 # length: 72 Negative log-likelihood for the location-scale Student's T distribution. # name: # type: sq_string # elements: 1 # length: 7 unidfit # name: # type: sq_string # elements: 1 # length: 1144 statistics: Nhat = unidfit (x) statistics: [Nhat, Nci] = unidfit (x) statistics: [Nhat, Nci] = unidfit (x, alpha) statistics: [Nhat, Nci] = unidfit (x, alpha, freq) Estimate parameter and confidence intervals for the discrete uniform distribution. Nhat = unidfit (x) returns the maximum likelihood estimate (MLE) of the maximum observable value for the discrete uniform distribution. x must be a vector. [Nhat, Nci] = unidfit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals of the estimated parameter. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = unidfit (x, alpha, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). Further information about the discrete uniform distribution can be found at https://en.wikipedia.org/wiki/Discrete_uniform_distribution See also: unidcdf, unidinv, unidpdf, unidrnd, unidstat # name: # type: sq_string # elements: 1 # length: 82 Estimate parameter and confidence intervals for the discrete uniform distribution. # name: # type: sq_string # elements: 1 # length: 6 unifit # name: # type: sq_string # elements: 1 # length: 1878 statistics: ahat = unifit (x) statistics: [ahat, bhat] = unifit (x) statistics: [ahat, bhat, aci, bci] = unifit (x) statistics: [...] = unifit (x, alpha) statistics: [...] = unifit (x, alpha, freq) Estimate parameters and confidence intervals for the continuous uniform distribution. [ahat, bhat] = unifit (x) returns the maximum likelihood estimates of the lower and upper endpoints, a and b, of the continuous uniform distribution given the data in x. Each estimate is returned as a separate output. x may be a vector, which is fitted as a single sample, or a matrix, which is fitted column by column. For a matrix of n columns ahat and bhat are 1-by-n row vectors and aci and bci are 2-by-n. [ahat, bhat, aci, bci] = unifit (x) also returns the 95% confidence intervals of the two estimates, one column per column of x, with the lower bound in the first row and the upper bound in the second. ahat is the upper bound of aci and bhat the lower bound of bci, since no sample can fall outside the fitted range. [...] = unifit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals of the estimated parameters. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = unifit (x, alpha, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). This third argument is an Octave extension; MATLAB's unifit takes two inputs at most, and freq is accepted for a vector x only. Further information about the continuous uniform distribution can be found at https://en.wikipedia.org/wiki/Continuous_uniform_distribution See also: unifcdf, unifinv, unifpdf, unifrnd, unifstat # name: # type: sq_string # elements: 1 # length: 85 Estimate parameters and confidence intervals for the continuous uniform distribution. # name: # type: sq_string # elements: 1 # length: 6 wblfit # name: # type: sq_string # elements: 1 # length: 2079 statistics: paramhat = wblfit (x) statistics: [paramhat, paramci] = wblfit (x) statistics: [paramhat, paramci] = wblfit (x, alpha) statistics: [...] = wblfit (x, alpha, censor) statistics: [...] = wblfit (x, alpha, censor, freq) statistics: [...] = wblfit (x, alpha, censor, freq, options) Estimate parameters and confidence intervals for the Weibull distribution. muhat = wblfit (x) returns the maximum likelihood estimates of the parameters of the Weibull distribution given the data in x. paramhat(1) is the scale parameter, lambda, and paramhat(2) is the shape parameter, k. [paramhat, paramci] = wblfit (x) returns the 95% confidence intervals for the parameter estimates. [...] = wblfit (x, alpha) also returns the 100 * (1 - alpha) percent confidence intervals for the parameter estimates. By default, the optional argument alpha is 0.05 corresponding to 95% confidence intervals. Pass in [] for alpha to use the default values. [...] = wblfit (x, alpha, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = wblfit (x, alpha, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but it can contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). [...] = wblfit (..., options) specifies control parameters for the iterative algorithm used to compute the maximum likelihood estimates. options is a structure with the following field and its default value: options.Display = "off" options.MaxFunEvals = 400 options.MaxIter = 200 options.TolX = 1e-6 Further information about the Weibull distribution can be found at https://en.wikipedia.org/wiki/Weibull_distribution The prob.WeibullDistribution class names these same two parameters A and B, after MATLAB. lambda is its A and k is its B. See also: wblcdf, wblinv, wblpdf, wblrnd, wbllike, wblstat # name: # type: sq_string # elements: 1 # length: 74 Estimate parameters and confidence intervals for the Weibull distribution. # name: # type: sq_string # elements: 1 # length: 7 wbllike # name: # type: sq_string # elements: 1 # length: 1652 statistics: nlogL = wbllike (params, x) statistics: [nlogL, acov] = wbllike (params, x) statistics: [...] = wbllike (params, x, alpha, censor) statistics: [...] = wbllike (params, x, alpha, censor, freq) Negative log-likelihood for the Weibull distribution. nlogL = wbllike (params, data) returns the negative log-likelihood of the data in x corresponding to the Weibull distribution with (1) scale parameter lambda and (2) shape parameter k given in the two-element vector params. [nlogL, acov] = wbllike (params, data) also returns the inverse of Fisher's information matrix, acov. If the input parameter values in params are the maximum likelihood estimates, the diagonal elements of acov are their asymptotic variances. acov is based on the observed Fisher's information, not the expected information. [...] = wbllike (params, data, censor) accepts a boolean vector, censor, of the same size as x with 1s for observations that are right-censored and 0s for observations that are observed exactly. By default, or if left empty, censor = zeros (size (x)). [...] = wbllike (params, data, censor, freq) accepts a frequency vector, freq, of the same size as x. freq typically contains integer frequencies for the corresponding elements in x, but may contain any non-integer non-negative values. By default, or if left empty, freq = ones (size (x)). Further information about the Weibull distribution can be found at https://en.wikipedia.org/wiki/Weibull_distribution The prob.WeibullDistribution class names these same two parameters A and B, after MATLAB. lambda is its A and k is its B. See also: wblcdf, wblinv, wblpdf, wblrnd, wblfit, wblstat # name: # type: sq_string # elements: 1 # length: 53 Negative log-likelihood for the Weibull distribution. statistics-release-2.1.2/inst/Distribution_Fitting/evfit.m000066400000000000000000000335511527311104300237430ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## Copyright (C) 2022 Andrew Penn ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{paramhat} =} evfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} evfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} evfit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@dots{}] =} evfit (@var{x}, @var{alpha}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} evfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}) ## @deftypefnx {statistics} {[@dots{}] =} evfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}, @var{options}) ## ## Estimate parameters and confidence intervals for the extreme value ## distribution. ## ## @code{@var{paramhat} = evfit (@var{x})} returns the maximum likelihood ## estimates of the parameters of the extreme value distribution (also known as ## the Gumbel or the type I generalized extreme value distribution) given the ## data in @var{x}. @qcode{@var{paramhat}(1)} is the location parameter, ## @var{mu}, and @qcode{@var{paramhat}(2)} is the scale parameter, @var{sigma}. ## ## @code{[@var{paramhat}, @var{paramci}] = evfit (@var{x})} returns the 95% ## confidence intervals for the parameter estimates. ## ## @code{[@dots{}] = evfit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals for the ## parameter estimates. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = evfit (@var{x}, @var{alpha}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = evfit (@var{x}, @var{alpha}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## @code{[@dots{}] = evfit (@dots{}, @var{options})} specifies control ## parameters for the iterative algorithm used to compute the maximum likelihood ## estimates. @var{options} is a structure with the following field and its ## default value: ## @itemize ## @item @qcode{@var{options}.Display = "off"} ## @item @qcode{@var{options}.MaxFunEvals = 400} ## @item @qcode{@var{options}.MaxIter = 200} ## @item @qcode{@var{options}.TolX = 1e-6} ## @end itemize ## ## The Gumbel distribution is used to model the distribution of the maximum (or ## the minimum) of a number of samples of various distributions. This version ## is suitable for modeling minima. For modeling maxima, use the alternative ## Gumbel fitting function, @code{gumbelfit}. ## ## Further information about the Gumbel distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gumbel_distribution} ## ## @seealso{evcdf, evinv, evpdf, evrnd, evlike, evstat, gumbelfit} ## @end deftypefn function [paramhat, paramci] = evfit (x, alpha, censor, freq, options) ## Check X for being a double precision vector if (! isvector (x) || ! isa (x, 'double')) error ("evfit: X must be a double-precision vector."); endif ## Check that X does not contain missing values (NaNs) if (any (isnan (x))) error ("evfit: X must NOT contain missing values (NaNs)."); endif ## Check alpha if (nargin < 2 || isempty (alpha)) alpha = 0.05; else if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("evfit: wrong value for ALPHA."); endif endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("evfit: X and CENSOR vectors mismatch."); endif ## Parse FREQ argument or add default if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("evfit: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("evfit: FREQ must not contain negative values."); endif ## Get options structure or add defaults if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("evfit: 'options' 5th argument must be a", ... " structure with 'Display', 'MaxFunEvals',", ... " 'MaxIter', and 'TolX' fields present.")); endif endif ## Remove zeros and NaNs from frequency vector (if necessary) if (! all (freq == 1)) remove = freq == 0 | isnan (freq); x(remove) = []; censor(remove) = []; freq(remove) = []; endif ## If X is a column vector, make X, CENSOR, and FREQ row vectors if (size (x, 1) > 1) x = x(:)'; censor = censor(:)'; freq = freq(:)'; endif ## Censor x and get number of samples sample_size = sum (freq); censored_sample_size = sum (freq .* censor); uncensored_sample_size = sample_size - censored_sample_size; x_range = range (x); x_max = max (x); ## Check cases that cannot make a fit. ## 1. All observations are censored if (sample_size == 0 || uncensored_sample_size == 0 || ! isfinite (x_range)) paramhat = NaN (1, 2); paramci = NaN (2, 2); return endif ## 2. Constant x in X if (censored_sample_size == 0 && x_range == 0) paramhat = [x(1), 0]; if (sample_size == 1) paramci = [-Inf, 0; Inf, Inf]; else paramci = [paramhat, paramhat]; endif return elseif (censored_sample_size == 0 && x_range != 0) ## Data can fit, so preprocess them to make likelihood eqn more stable. ## Shift x to max(x) == 0, min(x) = -1. x_0 = (x - x_max) ./ x_range; ## Get a rough initial estimate for scale parameter initial_sigma_parm = (sqrt (6) * std (x_0)) / pi; uncensored_weights = sum (freq .* x_0) ./ sample_size; endif ## 3. All uncensored observations are equal and greater than all censored ones uncensored_x_range = range (x(censor == 0)); uncensored_x = x(censor == 0); if (censored_sample_size > 0 && uncensored_x_range == 0 ... && uncensored_x(1) >= x_max) paramhat = [uncensored_x(1), 0]; if uncensored_sample_size == 1 paramci = [-Inf, 0; Inf, Inf]; else paramci = [paramhat; paramhat]; endif return else ## Data can fit, so preprocess them to make likelihood eqn more stable. ## Shift x to max(x) == 0, min(x) = -1. x_0 = (x - x_max) ./ x_range; ## Get a rough initial estimate for scale parameter if (uncensored_x_range > 0) [F_y, y] = ecdf (x_0, 'censoring', censor', 'frequency', freq'); pmid = (F_y(1:(end-1)) + F_y(2:end)) / 2; linefit = polyfit (log (- log (1 - pmid)), y(2:end), 1); initial_sigma_parm = linefit(1); else initial_sigma_parm = 1; endif uncensored_weights = sum (freq .* x_0 .* (1 - censor)) ./ ... uncensored_sample_size; endif ## Find lower and upper boundaries for bracketing the likelihood equation for ## the extreme value scale parameter if (evscale_lkeq (initial_sigma_parm, x_0, freq, uncensored_weights) > 0) upper = initial_sigma_parm; lower = 0.5 * upper; while (evscale_lkeq (lower, x_0, freq, uncensored_weights) > 0) upper = lower; lower = 0.5 * upper; if (lower <= realmin ('double')) error ("evfit: no solution for maximum likelihood estimates."); endif endwhile boundaries = [lower, upper]; else lower = initial_sigma_parm; upper = 2 * lower; while (evscale_lkeq (upper, x_0, freq, uncensored_weights) < 0) lower = upper; upper = 2 * lower; if (upper > realmax ('double')) error ("evfit: no solution for maximum likelihood estimates."); endif endwhile boundaries = [lower, upper]; endif ## Compute maximum likelihood for scale parameter as the root of the equation ## Custom code for finding the value within the boundaries [lower, upper] that ## evscale_lkeq function returns zero ## First check that there is a root within the boundaries new_lo = boundaries(1); new_up = boundaries(2); v_lower = evscale_lkeq (new_lo, x_0, freq, uncensored_weights); v_upper = evscale_lkeq (new_up, x_0, freq, uncensored_weights); if (! (sign (v_lower) * sign (v_upper) <= 0)) error ("evfit: no solution for maximum likelihood estimates."); endif ## Get a value at mid boundary range old_sigma = new_lo; new_sigma = (new_lo + new_up) / 2; new_fzero = evscale_lkeq (new_sigma, x_0, freq, uncensored_weights); ## Start searching cur_iter = 0; max_iter = 1e+3; while (cur_iter < max_iter && abs (old_sigma - new_sigma) > options.TolX) cur_iter++; if (new_fzero < 0) old_sigma = new_sigma; new_lo = new_sigma; new_sigma = (new_lo + new_up) / 2; new_fzero = evscale_lkeq (new_sigma, x_0, freq, uncensored_weights); else old_sigma = new_sigma; new_up = new_sigma; new_sigma = (new_lo + new_up) / 2; new_fzero = evscale_lkeq (new_sigma, x_0, freq, uncensored_weights); endif endwhile ## Check for maximum number of iterations if (cur_iter == max_iter) warning (strcat ("evfit: maximum number of function ", ... " evaluations (1e+4) has been reached.")); endif ## Compute MU muhat = new_sigma .* log (sum (freq .* exp (x_0 ./ new_sigma)) ./ ... uncensored_sample_size); ## Transform MU and SIGMA back to original location and scale paramhat = [(x_range*muhat)+x_max, x_range*new_sigma]; ## Compute the CI for MU and SIGMA if (nargout == 2) probs = [alpha/2; 1-alpha/2]; [~, acov] = evlike (paramhat, x, censor, freq); transfhat = [paramhat(1), log(paramhat(2))]; se = sqrt (diag (acov))'; se(2) = se(2) ./ paramhat(2); paramci = norminv ([probs, probs], [transfhat; transfhat], [se; se]); paramci(:,2) = exp (paramci(:,2)); endif endfunction ## Likelihood equation for the extreme value scale parameter. function v = evscale_lkeq (sigma, x, freq, x_weighted_uncensored) freq = freq .* exp (x ./ sigma); v = sigma + x_weighted_uncensored - sum (x .* freq) / sum (freq); endfunction %!demo %! ## Sample 3 populations from different extreme value distributions %! rng (42); %! r1 = evrnd (2, 5, 400, 1); %! r2 = evrnd (-5, 3, 400, 1); %! r3 = evrnd (14, 8, 400, 1); %! r = [r1, r2, r3]; %! %! ## Plot them normalized and fix their colors %! hist (r, 25, 0.4); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! set (h(3), 'facecolor', 'r'); %! ylim ([0, 0.28]) %! xlim ([-30, 30]); %! hold on %! %! ## Estimate their MU and SIGMA parameters %! mu_sigmaA = evfit (r(:,1)); %! mu_sigmaB = evfit (r(:,2)); %! mu_sigmaC = evfit (r(:,3)); %! %! ## Plot their estimated PDFs %! x = [min(r(:)):max(r(:))]; %! y = evpdf (x, mu_sigmaA(1), mu_sigmaA(2)); %! plot (x, y, '-pr'); %! y = evpdf (x, mu_sigmaB(1), mu_sigmaB(2)); %! plot (x, y, '-sg'); %! y = evpdf (x, mu_sigmaC(1), mu_sigmaC(2)); %! plot (x, y, '-^c'); %! legend ({'Normalized HIST of sample 1 with μ=2 and σ=5', ... %! 'Normalized HIST of sample 2 with μ=-5 and σ=3', ... %! 'Normalized HIST of sample 3 with μ=14 and σ=8', ... %! sprintf("PDF for sample 1 with estimated μ=%0.2f and σ=%0.2f", ... %! mu_sigmaA(1), mu_sigmaA(2)), ... %! sprintf("PDF for sample 2 with estimated μ=%0.2f and σ=%0.2f", ... %! mu_sigmaB(1), mu_sigmaB(2)), ... %! sprintf("PDF for sample 3 with estimated μ=%0.2f and σ=%0.2f", ... %! mu_sigmaC(1), mu_sigmaC(2))}) %! title ('Three population samples from different extreme value distributions') %! hold off ## Test output %!test %! x = 1:50; %! [paramhat, paramci] = evfit (x); %! paramhat_out = [32.6811, 13.0509]; %! paramci_out = [28.8504, 10.5294; 36.5118, 16.1763]; %! assert_equal (paramhat, paramhat_out, 1e-4); %! assert_equal (paramci, paramci_out, 1e-4); %!test %! x = 1:50; %! [paramhat, paramci] = evfit (x, 0.01); %! paramci_out = [27.6468, 9.8426; 37.7155, 17.3051]; %! assert_equal (paramci, paramci_out, 1e-4); ## Test input validation %!error evfit (ones (2,5)); %!error evfit (single (ones (1,5))); %!error evfit ([1, 2, 3, 4, NaN]); %!error evfit ([1, 2, 3, 4, 5], 1.2); %!error %! evfit ([1 2 3], 0.05, [], [1 5]) %!error %! evfit ([1 2 3], 0.05, [], [1 5 -1]) %!error ... %! evfit ([1:10], 0.05, [], [], 5) statistics-release-2.1.2/inst/Distribution_Fitting/evlike.m000066400000000000000000000136461527311104300241100ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} evlike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} evlike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} evlike (@var{params}, @var{x}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} evlike (@var{params}, @var{x}, @var{censor}, @var{freq}) ## ## Negative log-likelihood for the extreme value distribution. ## ## @code{@var{nlogL} = evlike (@var{params}, @var{x})} returns the negative ## log likelihood of the data in @var{x} corresponding to the extreme value ## distribution (also known as the Gumbel or the type I generalized extreme ## value distribution) with (1) location parameter @var{mu} and (2) scale ## parameter @var{sigma} given in the two-element vector @var{params}. ## ## @code{[@var{nlogL}, @var{acov}] = evlike (@var{params}, @var{x})} also ## returns the inverse of Fisher's information matrix, @var{acov}. If the input ## parameter values in @var{params} are the maximum likelihood estimates, the ## diagonal elements of @var{acov} are their asymptotic variances. ## ## @code{[@dots{}] = evlike (@var{params}, @var{x}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = evlike (@var{params}, @var{x}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## The Gumbel distribution is used to model the distribution of the maximum (or ## the minimum) of a number of samples of various distributions. This version ## is suitable for modeling minima. For modeling maxima, use the alternative ## Gumbel likelihood function, @code{gumbellike}. ## ## Further information about the Gumbel distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gumbel_distribution} ## ## @seealso{evcdf, evinv, evpdf, evrnd, evfit, evstat, gumbellike} ## @end deftypefn function [nlogL, acov] = evlike (params, x, censor, freq) ## Check input arguments and add defaults if (nargin < 2) error ("evlike: function called with too few input arguments."); endif if (numel (params) != 2) error ("evlike: wrong parameters length."); endif if (! isvector (x)) error ("evlike: X must be a vector."); endif if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("evlike: X and CENSOR vectors mismatch."); endif if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (isequal (size (x), size (freq))) nulls = find (freq == 0); if (numel (nulls) > 0) x(nulls) = []; censor(nulls) = []; freq(nulls) = []; endif else error ("evlike: X and FREQ vectors mismatch."); endif ## Get mu and sigma values mu = params(1); sigma = params(2); ## sigma must be positive, otherwise make it NaN if (sigma <= 0) sigma = NaN; endif ## Compute the individual log-likelihood terms z = (x - mu) ./ sigma; expz = exp (z); L = (z - log (sigma)) .* (1 - censor) - expz; ## Force a log(0)==-Inf for X from extreme right tail L(z == Inf) = -Inf; ## Neg-log-likelihood is the sum of the individual contributions nlogL = -sum (freq .* L); ## Compute the negative hessian at the parameter values. ## Invert to get the observed information matrix. if (nargout == 2) unc = (1 - censor); nH11 = sum (freq .* expz); nH12 = sum (freq .* ((z + 1) .* expz - unc)); nH22 = sum (freq .* (z .* (z+2) .* expz - ((2 .* z + 1) .* unc))); acov = (sigma .^ 2) * ... [nH22 -nH12; -nH12 nH11] / (nH11 * nH22 - nH12 * nH12); endif endfunction ## Test output %!test %! x = 1:50; %! [nlogL, acov] = evlike ([2.3, 1.2], x); %! avar_out = [-1.2778e-13, 3.1859e-15; 3.1859e-15, -7.9430e-17]; %! assert_equal (nlogL, 3.242264755689906e+17, 1e-14); %! assert_equal (acov, avar_out, 1e-3); %!test %! x = 1:50; %! [nlogL, acov] = evlike ([2.3, 1.2], x * 0.5); %! avar_out = [-7.6094e-05, 3.9819e-06; 3.9819e-06, -2.0836e-07]; %! assert_equal (nlogL, 481898704.0472211, 1e-6); %! assert_equal (acov, avar_out, 1e-3); %!test %! x = 1:50; %! [nlogL, acov] = evlike ([21, 15], x); %! avar_out = [11.73913876598908, -5.9546128523121216; ... %! -5.954612852312121, 3.708060045170236]; %! assert_equal (nlogL, 223.7612479380652, 1e-13); %! assert_equal (acov, avar_out, 1e-14); ## Test input validation %!error evlike ([12, 15]) %!error evlike ([12, 15, 3], [1:50]) %!error evlike ([12, 3], ones (10, 2)) %!error ... %! evlike ([12, 15], [1:50], [1, 2, 3]) %!error ... %! evlike ([12, 15], [1:50], [], [1, 2, 3]) statistics-release-2.1.2/inst/Distribution_Fitting/expfit.m000066400000000000000000000320271527311104300241220ustar00rootroot00000000000000## Copyright (C) 2021 Nicholas R. Jankowski ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{muhat} =} expfit (@var{x}) ## @deftypefnx {statistics} {[@var{muhat}, @var{muci}] =} expfit (@var{x}) ## @deftypefnx {statistics} {[@var{muhat}, @var{muci}] =} expfit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@dots{}] =} expfit (@var{x}, @var{alpha}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} expfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}) ## ## Estimate mean and confidence intervals for the exponential distribution. ## ## @code{@var{muhat} = expfit (@var{x})} returns the maximum likelihood estimate ## of the mean parameter, @var{muhat}, of the exponential distribution given the ## data in @var{x}. @var{x} is expected to be a non-negative vector. If @var{x} ## is an array, the mean will be computed for each column of @var{x}. If any ## elements of @var{x} are NaN, that vector's mean will be returned as NaN. ## ## @code{[@var{muhat}, @var{muci}] = expfit (@var{x})} returns the 95% ## confidence intervals for the parameter estimate. If @var{x} is a vector, ## @var{muci} is a two element column vector. If @var{x} is an array, each ## column of data will have a confidence interval returned as a two-row array. ## ## @code{[@dots{}] = evfit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals for the ## parameter estimates. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. Any invalid values for @var{alpha} ## will return NaN for both CI bounds. ## ## @code{[@dots{}] = expfit (@var{x}, @var{alpha}, @var{censor})} accepts a ## logical or numeric array, @var{censor}, of the same size as @var{x} with ## @qcode{1}s for observations that are right-censored and @qcode{0}s for ## observations that are observed exactly. Any non-zero elements are regarded ## as @qcode{1}s. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = expfit (@var{x}, @var{alpha}, @var{censor}, @var{freq})} ## accepts a frequency array, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## Matlab incompatibility: Matlab's @code{expfit} produces unpredictable results ## for some cases with higher dimensions (specifically 1 x m x n x ... arrays). ## Octave's implementation allows for @math{n*D} arrays, consistently performing ## calculations on individual column vectors. Additionally, @var{censor} and ## @var{freq} can be used with arrays of any size, whereas Matlab only allows ## their use when @var{x} is a vector. ## ## A common alternative parameterization of the exponential distribution is to ## use the parameter @math{λ} defined as the mean number of events in an ## interval as opposed to the parameter @math{μ}, which is the mean wait time ## for an event to occur. @math{λ} and @math{μ} are reciprocals, ## i.e. @math{μ = 1 / λ}. ## ## Further information about the exponential distribution can be found at ## @url{https://en.wikipedia.org/wiki/Exponential_distribution} ## ## @seealso{expcdf, expinv, exppdf, exprnd, explike, expstat} ## @end deftypefn function [muhat, muci] = expfit (x, alpha = 0.05, censor = [], freq = []) ## Check arguments if (nargin == 0 || nargin > 4 || nargout > 2) print_usage (); endif if (! (isnumeric (x) || islogical (x))) x = double (x); endif ## Guarantee working with column vectors if (isvector (x)) x = x(:); endif if (any (x(:) < 0)) error ("expfit: X cannot be negative."); endif sz_s = size (x); if (isempty (alpha)) alpha = 0.05; elseif (! (isscalar (alpha))) error ("expfit: ALPHA must be a scalar quantity."); endif if (isempty (censor) && isempty (freq)) ## Simple case without freq or censor, shortcut other validations muhat = mean (x, 1); if (nargout == 2) X = sum (x, 1); muci = [2*X./chi2inv(1 - alpha / 2, 2 * sz_s(1));... 2*X./chi2inv(alpha / 2, 2 * sz_s(1))]; endif else ## Input validation for censor and freq if (isempty (censor)) ## Expand to full censor with values that don't affect results censor = zeros (sz_s); elseif (! (isnumeric (censor) || islogical (censor))) ## Check for incorrect freq type error ("expfit: CENSOR must be a numeric or logical array.") elseif (isvector (censor)) ## Guarantee working with a column vector censor = censor(:); endif if (isempty (freq)) ## Expand to full censor with values that don't affect results freq = ones (sz_s); elseif (! (isnumeric (freq) || islogical (freq))) ## Check for incorrect freq type error ("expfit: FREQ must be a numeric or logical array.") elseif (isvector (freq)) ## Guarantee working with a column vector freq = freq(:); endif ## Check that size of censor and freq match x if (! (isequal (size (censor), sz_s))) error ("expfit: X and CENSOR vectors mismatch."); elseif (! isequal (size (freq), sz_s)) error ("expfit: X and FREQ vectors mismatch."); endif ## Trivial case where censor and freq have no effect if (all (censor(:) == 0 & freq(:) == 1)) muhat = mean (x, 1); if (nargout == 2) X = sum (x, 1); muci = [2*X./chi2inv(1 - alpha / 2, 2 * sz_s(1));... 2*X./chi2inv(alpha / 2, 2 * sz_s(1))]; endif ## No censoring, just adjust sample counts for freq elseif (all (censor(:) == 0)) X = sum (x.*freq, 1); n = sum (freq, 1); muhat = X ./ n; if (nargout == 2) muci = [2*X./chi2inv(1 - alpha / 2, 2 * n);... 2*X./chi2inv(alpha / 2, 2 * n)]; endif ## Censoring, but no sample counts adjustment elseif (all (freq(:) == 1)) censor = logical (censor); # convert any numeric censor'x to 0s and 1s X = sum (x, 1); r = sz_s(1) - sum (censor, 1); muhat = X ./ r; if (nargout == 2) muci = [2*X./chi2inv(1 - alpha / 2, 2 * r);... 2*X./chi2inv(alpha / 2, 2 * r)]; endif ## Both censoring and sample count adjustment else censor = logical (censor); # convert any numeric censor'x to 0s and 1s X = sum (x .* freq , 1); r = sum (freq .* (! censor), 1); muhat = X ./ r; if (nargout == 2) muci = [2*X./chi2inv(1 - alpha / 2, 2 * r);... 2*X./chi2inv(alpha / 2, 2 * r)]; endif endif ## compatibility check, NaN for columns where all censor's or freq's remove ## all samples null_columns = all (censor) | ! all (freq); muhat(null_columns) = NaN; if (nargout == 2) muci(:,null_columns) = NaN; endif endif endfunction %!demo %! ## Sample 3 populations from 3 different exponential distributions %! rande ('state', 42); %! r1 = exprnd (2, 4000, 1); %! r2 = exprnd (5, 4000, 1); %! r3 = exprnd (12, 4000, 1); %! r = [r1, r2, r3]; %! %! ## Plot them normalized and fix their colors %! hist (r, 48, 0.52); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! set (h(3), 'facecolor', 'r'); %! hold on %! %! ## Estimate their mu parameter %! muhat = expfit (r); %! %! ## Plot their estimated PDFs %! x = [0:max(r(:))]; %! y = exppdf (x, muhat(1)); %! plot (x, y, '-pr'); %! y = exppdf (x, muhat(2)); %! plot (x, y, '-sg'); %! y = exppdf (x, muhat(3)); %! plot (x, y, '-^c'); %! ylim ([0, 0.6]) %! xlim ([0, 40]) %! legend ({'Normalized HIST of sample 1 with μ=2', ... %! 'Normalized HIST of sample 2 with μ=5', ... %! 'Normalized HIST of sample 3 with μ=12', ... %! sprintf("PDF for sample 1 with estimated μ=%0.2f", muhat(1)), ... %! sprintf("PDF for sample 2 with estimated μ=%0.2f", muhat(2)), ... %! sprintf("PDF for sample 3 with estimated μ=%0.2f", muhat(3))}) %! title ('Three population samples from different exponential distributions') %! hold off ## Tests for mean %!assert_equal (expfit (1), 1) %!assert_equal (expfit (1:3), 2) %!assert_equal (expfit ([1:3]'), 2) %!assert_equal (expfit (1:3, []), 2) %!assert_equal (expfit (1:3, [], [], []), 2) %!assert_equal (expfit (magic (3)), [5 5 5]) %!assert_equal (expfit (cat (3, magic (3), 2*magic (3))), cat (3,[5 5 5], [10 10 10])) %!assert_equal (expfit (1:3, 0.1, [0 0 0], [1 1 1]), 2) %!assert_equal (expfit ([1:3]', 0.1, [0 0 0]', [1 1 1]'), 2) %!assert_equal (expfit (1:3, 0.1, [0 0 0]', [1 1 1]'), 2) %!assert_equal (expfit (1:3, 0.1, [1 0 0], [1 1 1]), 3) %!assert_equal (expfit (1:3, 0.1, [0 0 0], [4 1 1]), 1.5) %!assert_equal (expfit (1:3, 0.1, [1 0 0], [4 1 1]), 4.5) %!assert_equal (expfit (1:3, 0.1, [1 0 1], [4 1 1]), 9) %!assert_equal (expfit (1:3, 0.1, [], [-1 1 1]), 4) %!assert_equal (expfit (1:3, 0.1, [], [0.5 1 1]), 2.2) %!assert_equal (expfit (1:3, 0.1, [1 1 1]), NaN) %!assert_equal (expfit (1:3, 0.1, [], [0 0 0]), NaN) %!assert_equal (expfit (reshape (1:9, [3 3])), [2 5 8]) %!assert_equal (expfit (reshape (1:9, [3 3]), [], eye (3)), [3 7.5 12]) %!assert_equal (expfit (reshape (1:9, [3 3]), [], 2*eye (3)), [3 7.5 12]) %!assert_equal (expfit (reshape (1:9, [3 3]), [], [], [2 2 2; 1 1 1; 1 1 1]), ... %! [1.75 4.75 7.75]) %!assert_equal (expfit (reshape (1:9, [3 3]), [], [], [2 2 2; 1 1 1; 1 1 1]), ... %! [1.75 4.75 7.75]) %!assert_equal (expfit (reshape (1:9, [3 3]), [], eye (3), [2 2 2; 1 1 1; 1 1 1]), ... %! [3.5 19/3 31/3]) ## Tests for confidence intervals %!assert_equal ([~,muci] = expfit (1:3, 0), [0; Inf]) %!assert_equal ([~,muci] = expfit (1:3, 2), [Inf; 0]) %!assert_equal ([~,muci] = expfit (1:3, 0.1, [1 1 1]), [NaN; NaN]) %!assert_equal ([~,muci] = expfit (1:3, 0.1, [], [0 0 0]), [NaN; NaN]) %!assert_equal ([~,muci] = expfit (1:3, -1), [NaN; NaN]) %!assert_equal ([~,muci] = expfit (1:3, 5), [NaN; NaN]) #!assert_equal ([~,muci] = expfit ([1:3;1:3], -1), NaN (2, 3)] #!assert_equal ([~,muci] = expfit ([1:3;1:3], 5), NaN (2, 3)] %!assert_equal ([~,muci] = expfit (1:3), [0.830485728373393; 9.698190330474096], ... %! 1000*eps) %!assert_equal ([~,muci] = expfit (1:3, 0.1), ... %! [0.953017262058213; 7.337731146400207], 1000*eps) %!assert_equal ([~,muci] = expfit ([1:3;2:4]), ... %! [0.538440777613095, 0.897401296021825, 1.256361814430554; ... %! 12.385982973214016, 20.643304955356694, 28.900626937499371], ... %! 1000*eps) %!assert_equal ([~,muci] = expfit ([1:3;2:4], [], [1 1 1; 0 0 0]), ... %! 100*[0.008132550920455, 0.013554251534091, 0.018975952147727; ... %! 1.184936706156216, 1.974894510260360, 2.764852314364504], ... %! 1000*eps) %!assert_equal ([~,muci] = expfit ([1:3;2:4], [], [], [3 3 3; 1 1 1]), ... %! [0.570302756652583, 1.026544961974649, 1.482787167296715; ... %! 4.587722594914109, 8.257900670845396, 11.928078746776684], ... %! 1000*eps) %!assert_equal ([~,muci] = expfit ([1:3;2:4], [], [0 0 0; 1 1 1], [3 3 3; 1 1 1]), ... %! [0.692071440311161, 1.245728592560089, 1.799385744809018; ... %! 8.081825275395081, 14.547285495711145, 21.012745716027212], ... %! 1000*eps) %!test %! x = reshape (1:8, [4 2]); %! x(4) = NaN; %! [muhat,muci] = expfit (x); %! assert_equal ({muhat, muci}, {[NaN, 6.5], ... %! [NaN, 2.965574334593430;NaN, 23.856157493553368]}, 1000*eps); %!test %! x = magic (3); %! censor = [0 1 0; 0 1 0; 0 1 0]; %! freq = [1 1 0; 1 1 0; 1 1 0]; %! [muhat,muci] = expfit (x, [], censor, freq); %! assert_equal ({muhat, muci}, {[5 NaN NaN], ... %! [[2.076214320933482; 24.245475826185242],NaN(2)]}, 1000*eps); ## Test input validation %!error expfit () %!error expfit (1,2,3,4,5) %!error [a b censor] = expfit (1) %!error expfit (1, [1 2]) %!error expfit ([-1 2 3 4 5]) %!error expfit ([1:5], [], 'test') %!error expfit ([1:5], [], [], 'test') %!error expfit ([1:5], [], [0 0 0 0]) %!error expfit ([1:5], [], [], [1 1 1 1]) statistics-release-2.1.2/inst/Distribution_Fitting/explike.m000066400000000000000000000123611527311104300242630ustar00rootroot00000000000000## Copyright (C) 2021 Nir Krakauer ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} explike (@var{mu}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{avar}] =} explike (@var{mu}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} explike (@var{mu}, @var{x}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} explike (@var{mu}, @var{x}, @var{censor}, @var{freq}) ## ## Negative log-likelihood for the exponential distribution. ## ## @code{@var{nlogL} = explike (@var{mu}, @var{x})} returns the negative ## log likelihood of the data in @var{x} corresponding to the exponential ## distribution with mean parameter @var{mu}. @var{x} must be a vector of ## non-negative values, otherwise @qcode{NaN} is returned. ## ## @code{[@var{nlogL}, @var{avar}] = explike (@var{mu}, @var{x})} also ## returns the inverse of Fisher's information matrix, @var{avar}. If the input ## mean parameter, @var{mu}, is the maximum likelihood estimate, @var{avar} is ## its asymptotic variance. ## ## @code{[@dots{}] = explike (@var{mu}, @var{x}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = explike (@var{mu}, @var{x}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## A common alternative parameterization of the exponential distribution is to ## use the parameter @math{λ} defined as the mean number of events in an ## interval as opposed to the parameter @math{μ}, which is the mean wait time ## for an event to occur. @math{λ} and @math{μ} are reciprocals, ## i.e. @math{μ = 1 / λ}. ## ## Further information about the exponential distribution can be found at ## @url{https://en.wikipedia.org/wiki/Exponential_distribution} ## ## @seealso{expcdf, expinv, exppdf, exprnd, expfit, expstat} ## @end deftypefn function [nlogL, avar] = explike (mu, x, censor, freq) ## Check input arguments if (nargin < 2) error ("explike: function called with too few input arguments."); endif if (! isvector (x)) error ("explike: X must be a vector."); endif if (numel (mu) != 1) error ("explike: MU must be a scalar."); endif ## Return NaNs for non-positive MU or negative values in X if (mu <= 0 || any (x(:) < 0)) nlogL = NaN; if (nargout > 1) avar = NaN; endif return endif if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("explike: X and CENSOR vectors mismatch."); endif if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (isequal (size (x), size (freq))) nulls = find (freq == 0); if (numel (nulls) > 0) x(nulls) = []; censor(nulls) = []; freq(nulls) = []; endif else error ("explike: X and FREQ vectors mismatch."); endif ## Start processing numx = numel (x); sumz = sum (x .* freq) / mu; numc = numx - sum (freq .* censor); ## Calculate negative log likelihood nlogL = sumz + numc * log (mu); ## Optionally calculate the inverse (reciprocal) of the second derivative ## of the negative log likelihood with respect to parameter if (nargout > 1) avar = (mu ^ 2) ./ (2 * sumz - numc); endif endfunction %!test %! x = 12; %! beta = 5; %! [L, V] = explike (beta, x); %! expected_L = 4.0094; %! expected_V = 6.5789; %! assert_equal (L, expected_L, 0.001); %! assert_equal (V, expected_V, 0.001); %!test %! x = 1:5; %! beta = 2; %! [L, V] = explike (beta, x); %! expected_L = 10.9657; %! expected_V = 0.4; %! assert_equal (L, expected_L, 0.001); %! assert_equal (V, expected_V, 0.001); ## Test input validation %!error explike () %!error explike (2) %!error explike ([12, 3], [1:50]) %!error explike (3, ones (10, 2)) %!error ... %! explike (3, [1:50], [1, 2, 3]) %!error ... %! explike (3, [1:50], [], [1, 2, 3]) statistics-release-2.1.2/inst/Distribution_Fitting/gamfit.m000066400000000000000000000401141527311104300240660ustar00rootroot00000000000000## Copyright (C) 2019 Nir Krakauer ## Copyright (C) 2023-2024 Andreas Bertsatos ## Based on previous work by Martijn van Oosterhout ## originally granted to the public domain. ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{paramhat} =} gamfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} gamfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} gamfit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@dots{}] =} gamfit (@var{x}, @var{alpha}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} gamfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}) ## @deftypefnx {statistics} {[@dots{}] =} gamfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}, @var{options}) ## ## Estimate parameters and confidence intervals for the Gamma distribution. ## ## @code{@var{paramhat} = gamfit (@var{x})} returns the maximum likelihood ## estimates of the parameters of the Gamma distribution given the data in ## @var{x}. @qcode{@var{paramhat}(1)} is the shape parameter, @var{a}, and ## @qcode{@var{paramhat}(2)} is the scale parameter, @var{b}. ## ## @code{[@var{paramhat}, @var{paramci}] = gamfit (@var{x})} returns the 95% ## confidence intervals for the parameter estimates. ## ## @code{[@dots{}] = gamfit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals for the ## parameter estimates. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = gamfit (@var{x}, @var{alpha}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = gamfit (@var{x}, @var{alpha}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## @code{[@dots{}] = gamfit (@dots{}, @var{options})} specifies control ## parameters for the iterative algorithm used to compute the maximum likelihood ## estimates. @var{options} is a structure with the following field and its ## default value: ## @itemize ## @item @qcode{@var{options}.Display = "off"} ## @item @qcode{@var{options}.MaxFunEvals = 400} ## @item @qcode{@var{options}.MaxIter = 200} ## @item @qcode{@var{options}.TolX = 1e-6} ## @end itemize ## ## OCTAVE/MATLAB use the alternative parameterization given by the pair ## @math{α, β}, i.e. shape @var{a} and scale @var{b}. In Wikipedia, the two ## common parameterizations use the pairs @math{k, θ}, as shape and scale, and ## @math{α, β}, as shape and rate, respectively. The parameter names @var{a} ## and @var{b} used here (for MATLAB compatibility) correspond to the parameter ## notation @math{k, θ} instead of the @math{α, β} as reported in Wikipedia. ## ## Further information about the Gamma distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gamma_distribution} ## ## @seealso{gamcdf, gampdf, gaminv, gamrnd, gamlike} ## @end deftypefn function [paramhat, paramci] = gamfit (x, alpha, censor, freq, options) ## Check input arguments if (! isvector (x)) error ("gamfit: X must be a vector."); endif ## Check alpha if (nargin < 2 || isempty (alpha)) alpha = 0.05; else if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("gamfit: wrong value for ALPHA."); endif endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("gamfit: X and CENSOR vectors mismatch."); endif ## Parse FREQ argument or add default if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("gamfit: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("gamfit: FREQ must not contain negative values."); endif ## Get options structure or add defaults if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("gamfit: 'options' 5th argument must be a", ... " structure with 'Display', 'MaxFunEvals',", ... " 'MaxIter', and 'TolX' fields present.")); endif endif ## Remove zeros and NaNs from frequency vector (if necessary) if (! all (freq == 1)) remove = freq == 0 | isnan (freq); x(remove) = []; censor(remove) = []; freq(remove) = []; endif ## Get sample size and data type cls = class (x); szx = sum (freq); ncen = sum (freq .* censor); nunc = szx - ncen; ## Check for illegal value in X if (ncen == 0 && any (x < 0)) error ("gamfit: X cannot contain negative values."); endif if (ncen > 0 && any (x <= 0)) error ("gamfit: X must contain positive values when censored."); endif ## Handle ill-conditioned cases: no data or all censored if (szx == 0 || nunc == 0 || any (! isfinite (x))) paramhat = nan (1, 2, cls); paramci = nan (2, cls); return endif ## Check for identical data in X if (! isscalar (x) && max (abs (diff (x)) ./ x(2:end)) <= sqrt (eps)) paramhat = cast ([Inf, 0], cls); paramci = cast ([Inf, 0; Inf, 0], cls); return endif ## When CENSOR and FREQ are default if (all (censor == 0) && all (freq == 1)) ## Optimize with respect to log(a), since both A and B must be positive meanx = mean (x); x0 = 0; ## Minimize negative log-likelihood to estimate parameters f = @(loga) gamfit_search (loga, meanx, x); [loga, ~, err, output] = fminsearch (f, x0, options); ## Inverse log(a) a = exp (loga); b = meanx / a; paramhat = [a, b]; ## Handle errors if (err == 0) if (output.funcCount >= options.MaxFunEvals) warning (strcat ("gamfit: maximum number of function", ... " evaluations are exceeded.")); elseif (output.iterations >= options.MaxIter) warning ("gamfit: maximum number of iterations are exceeded."); endif elseif (err < 0) error ("gamfit: NoSolution."); endif endif ## No censoring if (all (censor == 0)) ## Scale data to allow parameter estimation ## for extremely large or small values scale = sum (freq .* x) / szx; ## Check for all data being ~zero if (scale < realmin (cls)) paramhat = cast ([NaN, 0], cls); paramci = cast ([NaN, 0; NaN, 0], cls); return endif scaledx = x / scale; ## Use Method of Moments for initial estimates meansqx = sum (freq .* (scaledx - 1) .^ 2) / szx; b = meansqx * szx / (szx - 1); a = 1 / b; ## Ensure that MLEs is possible, otherwise return initial estimates if (any (scaledx == 0)) paramhat = [a, b*scale]; paramci = nan (2, cls); warning ("gamfit: X contains zeros."); return ## Compute MLEs else ## Bracket the root of the scale parameter likelihood equation sumlogx = sum (freq .* log (scaledx)); bracket = sumlogx / szx; if (lkeqn (a, bracket) > 0) upper = a; lower = 0.5 * upper; while (lkeqn (lower, bracket) > 0) upper = lower; lower = 0.5 * upper; if (lower < realmin (cls)) error ("gamfit: no solution"); endif endwhile else lower = a; upper = 2 * lower; while (lkeqn (upper, bracket) < 0) lower = upper; upper = 2 * lower; if (upper > realmax (cls)) error ("gamfit: no solution"); endif endwhile endif bounds = [lower upper]; ## Find the root of the likelihood equation. opts = optimset ('fzero'); opts = optimset (opts, 'Display', 'off'); f = @(a) lkeqn (a, bracket); [a, lkeqnval, err] = fzero (f, bounds, opts); ## Rescale B paramhat = [a, (1/a)*scale]; endif ## With censoring else ## Get uncensored data notc = ! censor; xunc = x(notc); freq_notc = freq(notc); ## Ensure that MLEs is possible and get initial estimates xuncbar = sum (freq_notc .* xunc) / nunc; s2unc = sum (freq_notc .* (xunc - xuncbar) .^ 2) / nunc; if s2unc <= 100.*eps (xuncbar.^2) ## When all uncensored observations are equal and greater than all ## the censored observations, the likelihood surface becomes infinite if (max (xunc) == max (x)) paramhat = cast ([Inf, 0], cls); if (nunc > 1) paramci = cast ([Inf, 0; Inf, 0], cls); else paramci = cast ([0, 0; Inf, Inf], cls); endif return endif ## Set some default parameter estimates. x0 = [2, xuncbar./2]; else ## Fit a Weibull distribution and equate the parameter estimates ## into a Gamma distribution wblphat = wblfit (x, alpha, censor, freq); [m, v] = wblstat (wblphat(1), wblphat(2)); x0 = [m.*m./v, v./m]; endif ## Minimize negative log-likelihood to estimate parameters f = @(params) gamlike (params, x, censor, freq); [paramhat, ~, err, output] = fminsearch (f, x0, options); ## Force positive parameter values paramhat = abs (paramhat); ## Handle errors if (err == 0) if (output.funcCount >= options.MaxFunEvals) warning (strcat ("gamfit: maximum number of function", ... " evaluations are exceeded.")); elseif (output.iterations >= options.MaxIter) warning ("gamfit: maximum number of iterations are exceeded."); endif elseif (err < 0) error ("gamfit: no solution."); endif endif ## Compute CIs using a log normal approximation for parameters. if (nargout > 1) ## Compute asymptotic covariance [~, acov] = gamlike (paramhat, x, censor, freq); ## Get standard errors stderr = sqrt (diag (acov))'; stderr = stderr ./ paramhat; ## Apply log transform phatlog = log (paramhat); ## Compute normal quantiles z = probit (alpha / 2); ## Compute CI paramci = [phatlog; phatlog] + [stderr; stderr] .* [z, z; -z, -z]; ## Inverse log transform paramci = exp (paramci); endif endfunction ## Helper function so we only have to minimize for one variable. function nlogL = gamfit_search (loga, meanx, x) a = exp (loga); b = meanx / a; nlogL = gamlike ([a, b], x); endfunction ## Helper function for MLE with no censoring function v = lkeqn (a, bracket) v = -bracket - log (a) + psi (a); endfunction %!demo %! ## Sample 3 populations from different Gamma distributions %! randg ('state', 42); %! r1 = gamrnd (1, 2, 2000, 1); %! r2 = gamrnd (2, 2, 2000, 1); %! r3 = gamrnd (7.5, 1, 2000, 1); %! r = [r1, r2, r3]; %! %! ## Plot them normalized and fix their colors %! hist (r, 75, 4); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! set (h(3), 'facecolor', 'r'); %! ylim ([0, 0.62]); %! xlim ([0, 12]); %! hold on %! %! ## Estimate their α and β parameters %! a_bA = gamfit (r(:,1)); %! a_bB = gamfit (r(:,2)); %! a_bC = gamfit (r(:,3)); %! %! ## Plot their estimated PDFs %! x = [0.01,0.1:0.2:18]; %! y = gampdf (x, a_bA(1), a_bA(2)); %! plot (x, y, '-pr'); %! y = gampdf (x, a_bB(1), a_bB(2)); %! plot (x, y, '-sg'); %! y = gampdf (x, a_bC(1), a_bC(2)); %! plot (x, y, '-^c'); %! hold off %! legend ({'Normalized HIST of sample 1 with α=1 and β=2', ... %! 'Normalized HIST of sample 2 with α=2 and β=2', ... %! 'Normalized HIST of sample 3 with α=7.5 and β=1', ... %! sprintf("PDF for sample 1 with estimated α=%0.2f and β=%0.2f", ... %! a_bA(1), a_bA(2)), ... %! sprintf("PDF for sample 2 with estimated α=%0.2f and β=%0.2f", ... %! a_bB(1), a_bB(2)), ... %! sprintf("PDF for sample 3 with estimated α=%0.2f and β=%0.2f", ... %! a_bC(1), a_bC(2))}) %! title ('Three population samples from different Gamma distributions') %! hold off ## Test output %!shared x %! x = [1.2 1.6 1.7 1.8 1.9 2.0 2.2 2.6 3.0 3.5 4.0 4.8 5.6 6.6 7.6]; %!test %! [paramhat, paramci] = gamfit (x); %! assert_equal (paramhat, [3.4248, 0.9752], 1e-4); %! assert_equal (paramci, [1.7287, 0.4670; 6.7852, 2.0366], 1e-4); %!test %! [paramhat, paramci] = gamfit (x, 0.01); %! assert_equal (paramhat, [3.4248, 0.9752], 1e-4); %! assert_equal (paramci, [1.3945, 0.3705; 8.4113, 2.5668], 1e-4); %!test %! freq = [1 1 1 1 2 1 1 1 1 2 1 1 1 1 2]; %! [paramhat, paramci] = gamfit (x, [], [], freq); %! assert_equal (paramhat, [3.3025, 1.0615], 1e-4); %! assert_equal (paramci, [1.7710, 0.5415; 6.1584, 2.0806], 1e-4); %!test %! [paramhat, paramci] = gamfit (x, [], [], [1:15]); %! assert_equal (paramhat, [4.4484, 0.9689], 1e-4); %! assert_equal (paramci, [3.4848, 0.7482; 5.6785, 1.2546], 1e-4); %!test %! [paramhat, paramci] = gamfit (x, 0.01, [], [1:15]); %! assert_equal (paramhat, [4.4484, 0.9689], 1e-4); %! assert_equal (paramci, [3.2275, 0.6899; 6.1312, 1.3608], 1e-4); %!test %! cens = [0 0 0 0 1 0 0 0 0 0 0 0 0 0 0]; %! [paramhat, paramci] = gamfit (x, [], cens, [1:15]); %! assert_equal (paramhat, [4.7537, 0.9308], 1e-4); %! assert_equal (paramci, [3.7123, 0.7162; 6.0872, 1.2097], 1e-4); %!test %! cens = [0 0 0 0 1 0 0 0 0 0 0 0 0 0 0]; %! freq = [1 1 1 1 2 1 1 1 1 2 1 1 1 1 2]; %! [paramhat, paramci] = gamfit (x, [], cens, freq); %! assert_equal (paramhat, [3.4736, 1.0847], 1e-4); %! assert_equal (paramci, [1.8286, 0.5359; 6.5982, 2.1956], 1e-4); ## Test edge cases %!test %! [paramhat, paramci] = gamfit ([1 1 1 1 1 1]); %! assert_equal (paramhat, [Inf, 0]); %! assert_equal (paramci, [Inf, 0; Inf, 0]); %!test %! [paramhat, paramci] = gamfit ([1 1 1 1 1 1], [], [1 1 1 1 1 1]); %! assert_equal (paramhat, [NaN, NaN]); %! assert_equal (paramci, [NaN, NaN; NaN, NaN]); %!test %! [paramhat, paramci] = gamfit ([1 1 1 1 1 1], [], [], [1 1 1 1 1 1]); %! assert_equal (paramhat, [Inf, 0]); %! assert_equal (paramci, [Inf, 0; Inf, 0]); ## Test class of input preserved %!assert_equal (class (gamfit (single (x))), "single") ## Test input validation %!error gamfit (ones (2)) %!error gamfit (x, 1) %!error gamfit (x, -1) %!error gamfit (x, {0.05}) %!error gamfit (x, 'a') %!error gamfit (x, i) %!error gamfit (x, [0.01 0.02]) %!error %! gamfit ([1 2 3], 0.05, [], [1 5]) %!error %! gamfit ([1 2 3], 0.05, [], [1 5 -1]) %!error ... %! gamfit ([1:10], 0.05, [], [], 5) %!error gamfit ([1 2 3 -4]) %!error ... %! gamfit ([1 2 0], [], [1 0 0]) statistics-release-2.1.2/inst/Distribution_Fitting/gamlike.m000066400000000000000000000246521527311104300242410ustar00rootroot00000000000000## Copyright (C) 2022-2024 Andreas Bertsatos ## Based on previous work by Martijn van Oosterhout ## originally granted to the public domain. ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} gamlike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} gamlike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} gamlike (@var{params}, @var{x}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} gamlike (@var{params}, @var{x}, @var{censor}, @var{freq}) ## ## Negative log-likelihood for the Gamma distribution. ## ## @code{@var{nlogL} = gamlike (@var{params}, @var{x})} returns the negative ## log likelihood of the data in @var{x} corresponding to the Gamma distribution ## with (1) shape parameter @var{a} and (2) scale parameter @var{b} given in the ## two-element vector @var{params}. ## ## @code{[@var{nlogL}, @var{acov}] = gamlike (@var{params}, @var{x})} also ## returns the inverse of Fisher's information matrix, @var{acov}. If the input ## parameter values in @var{params} are the maximum likelihood estimates, the ## diagonal elements of @var{acov} are their asymptotic variances. ## ## @code{[@dots{}] = gamlike (@var{params}, @var{x}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = gamlike (@var{params}, @var{x}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## OCTAVE/MATLAB use the alternative parameterization given by the pair ## @math{α, β}, i.e. shape @var{a} and scale @var{b}. In Wikipedia, the two ## common parameterizations use the pairs @math{k, θ}, as shape and scale, and ## @math{α, β}, as shape and rate, respectively. The parameter names @var{a} ## and @var{b} used here (for MATLAB compatibility) correspond to the parameter ## notation @math{k, θ} instead of the @math{α, β} as reported in Wikipedia. ## ## Further information about the Gamma distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gamma_distribution} ## ## @seealso{gamcdf, gampdf, gaminv, gamrnd, gamfit} ## @end deftypefn function [nlogL, acov] = gamlike (params, x, censor, freq) ## Check input arguments and add defaults if (nargin < 2) error ("gamlike: function called with too few input arguments."); endif if (numel (params) != 2) error ("gamlike: wrong parameters length."); endif if (! isvector (x)) error ("gamlike: X must be a vector."); endif if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("gamlike: X and CENSOR vectors mismatch."); endif if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (isequal (size (x), size (freq))) nulls = find (freq == 0); if (numel (nulls) > 0) x(nulls) = []; censor(nulls) = []; freq(nulls) = []; endif else error ("gamlike: X and FREQ vectors mismatch."); endif ## Get K and THETA values a = params(1); b = params(2); ## Parameters K and THETA must be positive, otherwise make them NaN a(a <= 0) = NaN; b(b <= 0) = NaN; ## Data in X must be positive, otherwise make it NaN x(x <= 0) = NaN; ## Compute the individual log-likelihood terms z = x ./ b; L = (a - 1) .* log (z) - z - gammaln (a) - log (b); n_censored = sum (freq .* censor); if (n_censored > 0) z_censored = z(logical (censor)); Scen = gammainc (z_censored, a, 'upper'); L(logical (censor)) = log (Scen); endif ## Force a log(0)==-Inf for X from extreme right tail L(z == Inf) = -Inf; ## Neg-log-likelihood is the sum of the individual contributions nlogL = -sum (freq .* L); ## Compute the negative hessian at the parameter values. ## Invert to get the observed information matrix. if (nargout == 2) ## Calculate all data dL11 = -psi (1, a) * ones (size (z), 'like', z); dL12 = -(1 ./ b) * ones (size (z), 'like', z); dL22 = -(2 .* z - a) ./ (b .^ 2); ## Calculate censored data if (n_censored > 0) ## Compute derivatives [y, dy, d2y] = dgammainc (z_censored, a); dlnS = dy ./ y; d2lnS = d2y ./ y - dlnS.*dlnS; #[dlnS,d2lnS] = dlngamsf(z_censored,a); logzcen = log (z_censored); tmp = exp (a .* logzcen - z_censored - gammaln (a) - log (b)) ./ Scen; dL11(logical (censor)) = d2lnS; dL12(logical (censor)) = tmp .* (logzcen - dlnS - psi (0,a)); dL22(logical (censor)) = tmp .* ((z_censored-1-a)./b - tmp); endif nH11 = -sum (freq .* dL11); nH12 = -sum (freq .* dL12); nH22 = -sum (freq .* dL22); nH = [nH11 nH12; nH12 nH22]; if (any (isnan (nH(:)))) acov = nan (2, 'like', nH); else acov = inv (nH); endif endif endfunction ## Compute the incomplete Gamma function with its 1st and 2nd derivatives function [y, dy, d2y] = dgammainc (x, a) ## Initialize return variables y = nan (size (x)); dy = y; d2y = y; ## Use approximation for K > 2^20 ulim = 2^20; is_lim = find (a > ulim); if (! isempty (is_lim)) x(is_lim) = max (ulim - 1/3 + sqrt (ulim ./ a(is_lim)) .* ... (x(is_lim) - (a(is_lim) - 1/3)), 0); a(is_lim) = ulim; endif ## For x < a+1 is_lo = find (x < a + 1 & x != 0); if (! isempty (is_lo)) x_lo = x(is_lo); k_lo = a(is_lo); k_1 = k_lo; step = 1; d1st = 0; d2st = 0; stsum = step; d1sum = d1st; d2sum = d2st; while norm (step, 'inf') >= 100 * eps (norm (stsum, 'inf')) k_1 += 1; step = step .* x_lo ./ k_1; d1st = (d1st .* x_lo - step) ./ k_1; d2st = (d2st .* x_lo - 2 .* d1st) ./ k_1; stsum = stsum + step; d1sum = d1sum + d1st; d2sum = d2sum + d2st; endwhile fklo = exp (-x_lo + k_lo .* log (x_lo) - gammaln (k_lo + 1)); y_lo = fklo .* stsum; ## Fix very small a y_lo(x_lo > 0 & y_lo > 1) = 1; ## Compute 1st derivative dlogfklo = (log (x_lo) - psi (k_lo + 1)); d1fklo = fklo .* dlogfklo; d1y_lo = d1fklo .* stsum + fklo .* d1sum; ## Compute 2nd derivative d2fklo = d1fklo .* dlogfklo - fklo .* psi (1, k_lo + 1); d2y_lo = d2fklo .* stsum + 2 .* d1fklo .* d1sum + fklo .* d2sum; ## Considering the upper tail y(is_lo) = 1 - y_lo; dy(is_lo) = -d1y_lo; d2y(is_lo) = -d2y_lo; endif ## For x >= a+1 is_hi = find (x >= a+1); if (! isempty (is_hi)) x_hi = x(is_hi); k_hi = a(is_hi); zc = 0; k0 = 0; k1 = k_hi; x0 = 1; x1 = x_hi; d1k0 = 0; d1k1 = 1; d1x0 = 0; d1x1 = 0; d2k0 = 0; d2k1 = 0; d2x0 = 0; d2x2 = 0; kx = k_hi ./ x_hi; d1kx = 1 ./ x_hi; d2kx = 0; start = 1; while norm (d2kx - start, 'Inf') > 100 * eps (norm (d2kx, 'Inf')) rescale = 1 ./ x1; zc += 1; n_k = zc - k_hi; d2k0 = (d2k1 + d2k0 .* n_k - 2 .* d1k0) .* rescale; d2x0 = (d2x2 + d2x0 .* n_k - 2 .* d1x0) .* rescale; d1k0 = (d1k1 + d1k0 .* n_k - k0) .* rescale; d1x0 = (d1x1 + d1x0 .* n_k - x0) .* rescale; k0 = (k1 + k0 .* n_k) .* rescale; x0 = 1 + (x0 .* n_k) .* rescale; nrescale = zc .* rescale; d2k1 = d2k0 .* x_hi + d2k1 .* nrescale; d2x2 = d2x0 .* x_hi + d2x2 .* nrescale; d1k1 = d1k0 .* x_hi + d1k1 .* nrescale; d1x1 = d1x0 .* x_hi + d1x1 .* nrescale; k1 = k0 .* x_hi + k1 .* nrescale; x1 = x0 .* x_hi + zc; start = d2kx; kx = k1 ./ x1; d1kx = (d1k1 - kx .* d1x1) ./ x1; d2kx = (d2k1 - d1kx .* d1x1 - kx .* d2x2 - d1kx .* d1x1) ./ x1; endwhile fkhi = exp (-x_hi + k_hi .* log (x_hi) - gammaln (k_hi + 1)); y_hi = fkhi .* kx; ## Compute 1st derivative dlogfkhi = (log (x_hi) - psi (k_hi + 1)); d1fkhi = fkhi .* dlogfkhi; d1y_hi = d1fkhi .* kx + fkhi .* d1kx; ## Compute 2nd derivative d2fkhi = d1fkhi .* dlogfkhi - fkhi .* psi (1, k_hi + 1); d2y_hi = d2fkhi .* kx + 2 .* d1fkhi .* d1kx + fkhi .* d2kx; ## Considering the upper tail y(is_hi) = y_hi; dy(is_hi) = d1y_hi; d2y(is_hi) = d2y_hi; endif ## Handle x == 0 is_x0 = find (x == 0); if (! isempty (is_x0)) ## Considering the upper tail y(is_x0) = 1; dy(is_x0) = 0; d2y(is_x0) = 0; endif ## Handle a == 0 is_k0 = find (a == 0); if (! isempty (is_k0)) is_k0x0 = find (a == 0 & x == 0); ## Considering the upper tail y(is_k0) = 0; dy(is_k0x0) = Inf; d2y(is_k0x0) = -Inf; endif endfunction ## Test output %!test %! [nlogL, acov] = gamlike ([2, 3], [2, 3, 4, 5, 6, 7, 8, 9]); %! assert_equal (nlogL, 19.4426, 1e-4); %! assert_equal (acov, [2.7819, -5.0073; -5.0073, 9.6882], 1e-4); %!test %! [nlogL, acov] = gamlike ([2, 3], [5:45]); %! assert_equal (nlogL, 305.8070, 1e-4); %! assert_equal (acov, [0.0423, -0.0087; -0.0087, 0.0167], 1e-4); %!test %! [nlogL, acov] = gamlike ([2, 13], [5:45]); %! assert_equal (nlogL, 163.2261, 1e-4); %! assert_equal (acov, [0.2362, -1.6631; -1.6631, 13.9440], 1e-4); ## Test input validation %!error ... %! gamlike ([12, 15]) %!error gamlike ([12, 15, 3], [1:50]) %!error gamlike ([12, 3], ones (10, 2)) %!error ... %! gamlike ([12, 15], [1:50], [1, 2, 3]) %!error ... %! gamlike ([12, 15], [1:50], [], [1, 2, 3]) statistics-release-2.1.2/inst/Distribution_Fitting/geofit.m000066400000000000000000000131251527311104300240760ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{pshat} =} geofit (@var{x}) ## @deftypefnx {statistics} {[@var{pshat}, @var{psci}] =} geofit (@var{x}) ## @deftypefnx {statistics} {[@var{pshat}, @var{psci}] =} geofit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@var{pshat}, @var{psci}] =} geofit (@var{x}, @var{alpha}, @var{freq}) ## ## Estimate parameter and confidence intervals for the geometric distribution. ## ## @code{@var{pshat} = geofit (@var{x})} returns the maximum likelihood estimate ## (MLE) of the probability of success for the geometric distribution. @var{x} ## must be a vector. ## ## @code{[@var{pshat}, @var{psci}] = geofit (@var{x}, @var{alpha})} also returns ## the @qcode{100 * (1 - @var{alpha})} percent confidence intervals of the ## estimated parameter. By default, the optional argument @var{alpha} is 0.05 ## corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = geofit (@var{x}, @var{alpha}, @var{freq})} accepts a ## frequency vector, @var{freq}, of the same size as @var{x}. @var{freq} ## typically contains integer frequencies for the corresponding elements in ## @var{x}, but it can contain any non-integer non-negative values. By default, ## or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## The geometric distribution models the number of failures (@var{x}) of a ## Bernoulli trial with probability @var{ps} before the first success. ## ## Further information about the geometric distribution can be found at ## @url{https://en.wikipedia.org/wiki/Geometric_distribution} ## ## @seealso{geocdf, geoinv, geopdf, geornd, geostat} ## @end deftypefn function [pshat, psci] = geofit (x, alpha, freq) ## Check input arguments if (nargin < 1) error ("geofit: function called with too few input arguments."); endif ## Check data in X if (any (x < 0)) error ("geofit: X cannot have negative values."); endif if (! isvector (x)) error ("geofit: X must be a vector."); endif ## Check ALPHA if (nargin < 2 || isempty (alpha)) alpha = 0.05; elseif (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("geofit: wrong value for ALPHA."); endif ## Check frequency vector if (nargin < 3 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("geofit: X and FREQ vector mismatch."); endif ## Expand frequency and censor vectors (if necessary) if (! all (freq == 1)) xf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; endfor x = xf; freq = ones (size (x)); endif ## Compute PS estimate pshat = 1 ./ (1 + mean (x)); ## Compute confidence interval of PS if (nargout > 1) sz = numel (x); serr = pshat .* sqrt ((1 - pshat) ./ sz); psci = norminv ([alpha/2; 1-alpha/2], [pshat; pshat], [serr; serr]); endif endfunction %!demo %! ## Sample 2 populations from different geometric distributions %! rande ('state', 42); %! r1 = geornd (0.15, 1000, 1); %! r2 = geornd (0.5, 1000, 1); %! r = [r1, r2]; %! %! ## Plot them normalized and fix their colors %! hist (r, 0:0.5:20.5, 1); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! hold on %! %! ## Estimate their probability of success %! pshatA = geofit (r(:,1)); %! pshatB = geofit (r(:,2)); %! %! ## Plot their estimated PDFs %! x = [0:15]; %! y = geopdf (x, pshatA); %! plot (x, y, '-pg'); %! y = geopdf (x, pshatB); %! plot (x, y, '-sc'); %! xlim ([0, 15]) %! ylim ([0, 0.6]) %! legend ({'Normalized HIST of sample 1 with ps=0.15', ... %! 'Normalized HIST of sample 2 with ps=0.50', ... %! sprintf("PDF for sample 1 with estimated ps=%0.2f", ... %! mean (pshatA)), ... %! sprintf("PDF for sample 2 with estimated ps=%0.2f", ... %! mean (pshatB))}) %! title ('Two population samples from different geometric distributions') %! hold off ## Test output %!test %! x = 0:5; %! [pshat, psci] = geofit (x); %! assert_equal (pshat, 0.2857, 1e-4); %! assert_equal (psci, [0.092499; 0.478929], 1e-5); %!test %! x = 0:5; %! [pshat, psci] = geofit (x, [], [1 1 1 1 1 1]); %! assert_equal (pshat, 0.2857, 1e-4); %! assert_equal (psci, [0.092499; 0.478929], 1e-5); %!assert_equal (geofit ([1 1 2 3]), geofit ([1 2 3], [] ,[2 1 1])) ## Test input validation %!error geofit () %!error geofit (-1, [1 2 3 3]) %!error geofit (1, 0) %!error geofit (1, 1.2) %!error geofit (1, [0.02 0.05]) %!error ... %! geofit ([1.5, 0.2], [], [0, 0, 0, 0, 0]) %!error ... %! geofit ([1.5, 0.2], [], [1, 1, 1]) statistics-release-2.1.2/inst/Distribution_Fitting/gevfit.m000066400000000000000000000274761527311104300241230ustar00rootroot00000000000000## Copyright (C) 2012-2021 Nir Krakauer ## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{paramhat} =} gevfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} gevfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} gevfit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} gevfit (@var{x}, @var{alpha}, @var{freq}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} gevfit (@var{x}, @var{alpha}, @var{options}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} gevfit (@var{x}, @var{alpha}, @var{freq}, @var{options}) ## ## Estimate parameters and confidence intervals for the generalized extreme ## value (GEV) distribution. ## ## @code{@var{paramhat} = gevfit (@var{x})} returns the maximum likelihood ## estimates of the parameters of the GEV distribution given the data in ## @var{x}. @qcode{@var{paramhat}(1)} is the shape parameter, @var{k}, and ## @qcode{@var{paramhat}(2)} is the scale parameter, @var{sigma}, and ## @qcode{@var{paramhat}(3)} is the location parameter, @var{mu}. ## ## @code{[@var{paramhat}, @var{paramci}] = gevfit (@var{x})} returns the 95% ## confidence intervals for the parameter estimates. ## ## @code{[@dots{}] = gevfit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals for the ## parameter estimates. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = gevfit (@var{params}, @var{x}, @var{freq})} accepts a ## frequency vector, @var{freq}, of the same size as @var{x}. @var{freq} ## must contain non-negative integer frequencies for the corresponding elements ## in @var{x}. By default, or if left empty, ## @qcode{@var{freq} = ones (size (@var{x}))}. ## ## @code{[@var{paramhat}, @var{paramci}] = gevfit (@var{x}, @var{alpha}, ## @var{options})} specifies control parameters for the iterative algorithm used ## to compute ML estimates with the @code{fminsearch} function. @var{options} ## is a structure with the following fields and their default values: ## @itemize ## @item @qcode{@var{options}.Display = "off"} ## @item @qcode{@var{options}.MaxFunEvals = 400} ## @item @qcode{@var{options}.MaxIter = 200} ## @item @qcode{@var{options}.TolX = 1e-6} ## @end itemize ## ## When @qcode{@var{k} < 0}, the GEV is the type III extreme value distribution. ## When @qcode{@var{k} > 0}, the GEV distribution is the type II, or Frechet, ## extreme value distribution. If @var{W} has a Weibull distribution as ## computed by the @code{wblcdf} function, then @qcode{-@var{W}} has a type III ## extreme value distribution and @qcode{1/@var{W}} has a type II extreme value ## distribution. In the limit as @var{k} approaches @qcode{0}, the GEV is the ## mirror image of the type I extreme value distribution as computed by the ## @code{evcdf} function. ## ## The mean of the GEV distribution is not finite when @qcode{@var{k} >= 1}, and ## the variance is not finite when @qcode{@var{k} >= 1/2}. The GEV distribution ## has positive density only for values of @var{x} such that ## @qcode{@var{k} * (@var{x} - @var{mu}) / @var{sigma} > -1}. ## ## Further information about the generalized extreme value distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Generalized_extreme_value_distribution} ## ## @seealso{gevcdf, gevinv, gevpdf, gevrnd, gevlike, gevstat} ## @end deftypefn function [paramhat, paramci] = gevfit (x, alpha, varargin) ## Check X is vector if (! isvector (x)) error ("gevfit: X must be a vector."); endif ## Get X type and convert to double for computation is_type = class (x); if (strcmpi (is_type, 'single')) x = double (x); endif ## Check that X is not constant and does not contain NaNs sample_size = length (x); if (sample_size == 0 || any (isnan (x))) paramhat = NaN (1,3, is_type); paramci = NaN (2,3, is_type); warning ("gevfit: X contains NaNs."); return elseif (numel (unique (x)) == 1) paramhat = cast ([0, 0, unique(x)], is_type); if (length (x) == 1) paramci = cast ([-Inf, 0, -Inf; Inf, Inf, Inf], is_type); else paramci = [paramhat; paramhat]; endif warning ("gevfit: X is a constant vector."); return endif ## Check ALPHA if (nargin < 2 || isempty (alpha)) alpha = 0.05; else if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("gevfit: wrong value for ALPHA."); endif endif ## Add defaults freq = []; options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; ## Check extra arguments for FREQ vector and/or 'options' structure if (nargin > 2) if (numel (varargin) == 1 && isstruct (varargin{1})) options = varargin{1}; elseif (numel (varargin) == 1 && isnumeric (varargin{1})) freq = varargin{1}; elseif (numel (varargin) == 2) freq = varargin{1}; options = varargin{2}; endif if (isempty (freq)) freq = ones (size (x)); endif ## Check for valid freq vector if (! isequal (size (x), size (freq))) error ("gevfit: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("gevfit: FREQ must not contain negative values."); elseif (any (fix (freq) != freq)) error ("gevfit: FREQ must contain integer values."); endif ## Check for valid options structure if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("gevfit: 'options' argument must be a", ... " structure with 'Display', 'MaxFunEvals',", ... " 'MaxIter', and 'TolX' fields present.")); endif endif ## Expand frequency if (! all (freq == 1)) xf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; endfor x = xf; endif ## Force to column vector x = x(:); ## Compute initial parameters F = (0.5:1:(sample_size - 0.5))' ./ sample_size; k_0 = fminsearch (@(k) 1 - corr (x, gevinv (F, k, 1, 0)), 0); paramguess = [k_0, polyfit(gevinv(F,k_0,1,0),x',1)]; ## Check if x support initial parameters or fall back to unbounded evfit if (k_0 < 0 && (max (x) > - paramguess(2) / k_0 + paramguess(3)) || ... k_0 > 0 && (min (x) < - paramguess(2) / k_0 + paramguess(3))) paramguess = [evfit(x), 0]; paramguess = flip (paramguess); endif ## Minimize the negative log-likelihood according to initial parameters paramguess(2) = log (paramguess(2)); fhandle = @(paramguess) nll (paramguess, x); [paramhat, ~, exitflag, output] = fminsearch (fhandle, paramguess, options); paramhat(2) = exp (paramhat(2)); ## Display errors and warnings if any if (exitflag == 0) if (output.funcCount >= output.iterations) warning ("gevfit: maximum number of evaluations reached"); else warning ("gevfit: reached iteration limit"); endif elseif (exitflag == -1) error ("gevfit: No solution"); endif ## Return a row vector for Matlab compatibility paramhat = paramhat(:)'; ## Check for second output argument if (nargout > 1) [~, acov] = gevlike (paramhat, x); param_se = sqrt (diag (acov))'; if (any (iscomplex (param_se))) warning (strcat ("gevfit: Fisher information matrix not positive", ... " definite; parameter optimization likely did not", ... " converge")); paramci = NaN (2, 3, is_type); else p_vals = [alpha/2; 1-alpha/2]; k_ci = norminv (p_vals, paramhat(1), param_se(1)); s_ci = exp (norminv (p_vals, log (paramhat(2)), param_se(2) ./ paramhat(2))); m_ci = norminv (p_vals, paramhat(3), param_se(3)); paramci = [k_ci, s_ci, m_ci]; endif endif endfunction ## Negative log-likelihood for the GEV (log(sigma) parameterization) function out = nll (parms, x) k_0 = parms(1); log_sigma = parms(2); sigma = exp (log_sigma); mu = parms(3); n = numel (x); z = (x - mu) ./ sigma; if abs (k_0) > eps u = 1 + k_0.*z; if min (u) > 0 lnu = log1p (k_0 .* z); out = n * log_sigma + sum (exp ((-1 / k_0) * lnu)) + ... (1 + 1 / k_0) * sum (lnu); else out = Inf; endif else out = n * log_sigma + sum (exp (-z) + z); endif endfunction %!demo %! ## Sample 2 populations from 2 different exponential distributions %! rng (42); %! r1 = gevrnd (-0.5, 1, 2, 5000, 1); %! r2 = gevrnd (0, 1, -4, 5000, 1); %! r = [r1, r2]; %! %! ## Plot them normalized and fix their colors %! hist (r, 50, 5); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! hold on %! %! ## Estimate their k, sigma, and mu parameters %! k_sigma_muA = gevfit (r(:,1)); %! k_sigma_muB = gevfit (r(:,2)); %! %! ## Plot their estimated PDFs %! x = [-10:0.5:20]; %! y = gevpdf (x, k_sigma_muA(1), k_sigma_muA(2), k_sigma_muA(3)); %! plot (x, y, '-pr'); %! y = gevpdf (x, k_sigma_muB(1), k_sigma_muB(2), k_sigma_muB(3)); %! plot (x, y, '-sg'); %! ylim ([0, 0.7]) %! xlim ([-7, 5]) %! legend ({'Normalized HIST of sample 1 with k=-0.5, σ=1, μ=2', ... %! 'Normalized HIST of sample 2 with k=0, σ=1, μ=-4', %! sprintf("PDF for sample 1 with estimated k=%0.2f, σ=%0.2f, μ=%0.2f", ... %! k_sigma_muA(1), k_sigma_muA(2), k_sigma_muA(3)), ... %! sprintf("PDF for sample 3 with estimated k=%0.2f, σ=%0.2f, μ=%0.2f", ... %! k_sigma_muB(1), k_sigma_muB(2), k_sigma_muB(3))}) %! title ('Two population samples from different exponential distributions') %! hold off ## Test output %!test %! x = 1:50; %! [pfit, pci] = gevfit (x); %! pfit_out = [-0.4407, 15.1923, 21.5309]; %! pci_out = [-0.7532, 11.5878, 16.5686; -0.1282, 19.9183, 26.4926]; %! assert_equal (pfit, pfit_out, 1e-3); %! assert_equal (pci, pci_out, 1e-3); %!test %! x = 1:2:50; %! [pfit, pci] = gevfit (x); %! pfit_out = [-0.4434, 15.2024, 21.0532]; %! pci_out = [-0.8904, 10.3439, 14.0168; 0.0035, 22.3429, 28.0896]; %! assert_equal (pfit, pfit_out, 1e-3); %! assert_equal (pci, pci_out, 1e-3); ## Test input validation %!error gevfit (ones (2,5)); %!error gevfit ([1, 2, 3, 4, 5], 1.2); %!error gevfit ([1, 2, 3, 4, 5], 0); %!error gevfit ([1, 2, 3, 4, 5], 'alpha'); %!error ... %! gevfit ([1, 2, 3, 4, 5], 0.05, [1, 2, 3, 2]); %!error ... %! gevfit ([1, 2, 3, 4, 5], 0.05, [1, 2, 3, 2, -1]); %!error ... %! gevfit ([1, 2, 3, 4, 5], 0.05, [1, 2, 3, 2, 1.5]); %!error ... %! gevfit ([1, 2, 3, 4, 5], 0.05, struct ('option', 234)); %!error ... %! gevfit ([1, 2, 3, 4, 5], 0.05, ones (1,5), struct ('option', 234)); statistics-release-2.1.2/inst/Distribution_Fitting/gevfit_lmom.m000066400000000000000000000067221527311104300251360ustar00rootroot00000000000000## Copyright (C) 2012 Nir Krakauer ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{paramhat}, @var{paramci}] =} gevfit_lmom (@var{data}) ## ## Find an estimator (@var{paramhat}) of the generalized extreme value (GEV) ## distribution fitting @var{data} using the method of L-moments. ## ## @subheading Arguments ## ## @itemize @bullet ## @item ## @var{data} is the vector of given values. ## @end itemize ## ## @subheading Return values ## ## @itemize @bullet ## @item ## @var{paramhat} is the 3-parameter maximum-likelihood parameter vector ## [@var{k}; @var{sigma}; @var{mu}], where @var{k} is the shape parameter of the ## GEV distribution, @var{sigma} is the scale parameter of the GEV distribution, ## and @var{mu} is the location parameter of the GEV distribution. ## @item ## @var{paramci} has the approximate 95% confidence intervals of the parameter ## values (currently not implemented). ## ## @end itemize ## ## @subheading Examples ## ## @example ## @group ## data = gevrnd (0.1, 1, 0, 100, 1); ## [pfit, pci] = gevfit_lmom (data); ## p1 = gevcdf (data,pfit(1),pfit(2),pfit(3)); ## [f, x] = ecdf (data); ## plot(data, p1, 's', x, f) ## @end group ## @end example ## @seealso{gevfit} ## @subheading References ## ## @enumerate ## @item ## Ailliot, P.; Thompson, C. & Thomson, P. Mixed methods for fitting the GEV ## distribution, Water Resources Research, 2011, 47, W05551 ## ## @end enumerate ## @end deftypefn function [paramhat, paramci] = gevfit_lmom (data) # Check arguments if (nargin < 1) print_usage; endif # find the L-moments data = sort (data(:))'; n = numel (data); L1 = mean (data); L2 = sum (data .* (2*(1:n) - n - 1)) / (2*nchoosek (n, 2)); # or mean(triu(data' - data, 1, 'pack')) / 2; b = bincoeff ((1:n) - 1, 2); L3 = sum (data .* (b - 2 * ((1:n) - 1) .* (n - (1:n)) + fliplr (b))) / (3*nchoosek (n, 3)); #match the moments to the GEV distribution #first find k based on L3/L2 f = @(k) (L3/L2 + 3)/2 - limdiv ((1 - 3^(k)), (1 - 2^(k))); k = fzero (f, 0); #next find sigma and mu given k if abs (k) < 1E-8 sigma = L2 / log (2); eg = 0.57721566490153286; #Euler-Mascheroni constant mu = L1 - sigma * eg; else sigma = -k*L2 / (gamma (1 - k) * (1 - 2^(k))); mu = L1 - sigma * ((gamma (1 - k) - 1) / k); endif paramhat = [k; sigma; mu]; if nargout > 1 paramci = NaN; endif endfunction #internal function to accurately evaluate (1 - 3^k)/(1 - 2^k) in the limit as k --> 0 function c = limdiv (a, b) # c = ifelse (abs(b) < 1E-8, log(3)/log(2), a ./ b); if abs (b) < 1E-8 c = log (3)/log (2); else c = a / b; endif endfunction %!xtest <31070> %! data = 1:50; %! [pfit, pci] = gevfit_lmom (data); %! expected_p = [-0.28 15.01 20.22]'; %! assert_equal (pfit, expected_p, 0.1); statistics-release-2.1.2/inst/Distribution_Fitting/gevlike.m000066400000000000000000000354651527311104300242620ustar00rootroot00000000000000## Copyright (C) 2012 Nir Krakauer ## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} gevlike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} gevlike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} gevlike (@var{params}, @var{x}, @var{freq}) ## ## Negative log-likelihood for the generalized extreme value (GEV) distribution. ## ## @code{@var{nlogL} = gevlike (@var{params}, @var{x})} returns the negative ## log likelihood of the data in @var{x} corresponding to the GEV distribution ## with (1) shape parameter @var{k}, (2) scale parameter @var{sigma}, and (3) ## location parameter @var{mu} given in the three-element vector @var{params}. ## ## @code{[@var{nlogL}, @var{acov}] = gevlike (@var{params}, @var{x})} also ## returns the inverse of Fisher's information matrix, @var{acov}. If the input ## parameter values in @var{params} are the maximum likelihood estimates, the ## diagonal elements of @var{acov} are their asymptotic variances. ## ## @code{[@dots{}] = gevlike (@var{params}, @var{x}, @var{freq})} accepts a ## frequency vector, @var{freq}, of the same size as @var{x}. @var{freq} ## must contain non-negative integer frequencies for the corresponding elements ## in @var{x}. By default, or if left empty, ## @qcode{@var{freq} = ones (size (@var{x}))}. ## ## When @qcode{@var{k} < 0}, the GEV is the type III extreme value distribution. ## When @qcode{@var{k} > 0}, the GEV distribution is the type II, or Frechet, ## extreme value distribution. If @var{W} has a Weibull distribution as ## computed by the @code{wblcdf} function, then @qcode{-@var{W}} has a type III ## extreme value distribution and @qcode{1/@var{W}} has a type II extreme value ## distribution. In the limit as @var{k} approaches @qcode{0}, the GEV is the ## mirror image of the type I extreme value distribution as computed by the ## @code{evcdf} function. ## ## @strong{MATLAB compatibility.} At exactly @qcode{@var{k} = 0} the returned ## @var{ACOV} deviates from MATLAB's deliberately. Both implementations agree ## to thirteen digits for any @qcode{@var{k} != 0}, and both their ## @qcode{@var{k} != 0} expressions converge on the values returned here as ## @var{k} approaches @qcode{0}; MATLAB's own @qcode{@var{k} = 0} branch ## returns something else entirely, which its @qcode{@var{k} != 0} branch ## therefore contradicts. The Gumbel-limit expressions used here are the ## limits of the general ones, so @var{ACOV} is continuous at @qcode{0}. This ## is independent of the sample size. ## ## The mean of the GEV distribution is not finite when @qcode{@var{k} >= 1}, and ## the variance is not finite when @qcode{@var{k} >= 1/2}. The GEV distribution ## has positive density only for values of @var{x} such that ## @qcode{@var{k} * (@var{x} - @var{mu}) / @var{sigma} > -1}. ## ## Further information about the generalized extreme value distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Generalized_extreme_value_distribution} ## ## @seealso{gevcdf, gevinv, gevpdf, gevrnd, gevfit, gevstat} ## @end deftypefn function [nlogL, acov] = gevlike (params, x, freq) ## Check input arguments if (nargin < 2) error ("gevlike: function called with too few input arguments."); endif if (! isvector (x)) error ("gevlike: X must be a vector."); endif if (length (params) != 3) error ("gevlike: PARAMS must be a three-element vector."); endif if (nargin < 3 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("gevlike: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("gevlike: FREQ must not contain negative values."); elseif (any (fix (freq) != freq)) error ("gevlike: FREQ must contain integer values."); endif ## Expand frequency if (! all (freq == 1)) xf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; endfor x = xf; endif k = params(1); sigma = params(2); mu = params(3); ## Calculate negative log likelihood [nll, k_terms] = gevnll (x, k, sigma, mu); nlogL = sum (nll(:)); ## Optionally calculate the first and second derivatives of the negative log ## likelihood with respect to parameters if (nargout > 1) [Grad, kk_terms] = gevgrad (x, k, sigma, mu, k_terms); FIM = gevfim (x, k, sigma, mu, k_terms, kk_terms); acov = inv (FIM); endif endfunction ## Internal function to calculate negative log likelihood for gevlike function [nlogL, k_terms] = gevnll (x, k, sigma, mu) k_terms = []; a = (x - mu) ./ sigma; if (all (k == 0)) nlogL = exp (-a) + a + log (sigma); else aa = k .* a; ## Use a series expansion to find the log likelihood more accurately ## when k is small if (min (abs (aa)) < 1E-3 && max (abs (aa)) < 0.5) k_terms = 1; sgn = 1; i = 0; while 1 sgn = -sgn; i++; newterm = (sgn / (i + 1)) * (aa .^ i); k_terms = k_terms + newterm; if (max (abs (newterm)) <= eps) break endif endwhile nlogL = exp (-a .* k_terms) + a .* (k + 1) .* k_terms + log (sigma); else b = 1 + aa; nlogL = b .^ (-1 ./ k) + (1 + 1 ./ k) .* log (b) + log (sigma); nlogL(b <= 0) = Inf; endif endif endfunction ## Calculate the gradient of the negative log likelihood of x with respect ## to the parameters of the generalized extreme value distribution for gevlike function [G, kk_terms] = gevgrad (x, k, sigma, mu, k_terms) kk_terms = []; G = ones (3, 1); ## Use the expressions for first derivatives that are the limits as k --> 0 if (k == 0) a = (x - mu) ./ sigma; f = exp (-a) - 1; ## k g = a .* (1 + a .* f / 2); G(1) = sum (g(:)); ## sigma g = (a .* f + 1) ./ sigma; G(2) = sum (g(:)); ## mu g = f ./ sigma; G(3) = sum (g(:)); return endif a = (x - mu) ./ sigma; b = 1 + k .* a; ## Negative log likelihood is locally infinite if (any (b <= 0)) G(:) = 0; return endif ## k c = log (b); d = 1 ./ k + 1; ## Use a series expansion to find the gradient more accurately when k is small if (nargin > 4 && ! isempty (k_terms)) aa = k .* a; f = exp (-a .* k_terms); kk_terms = 0.5; sgn = 1; i = 0; while 1 sgn = -sgn; i++; newterm = (sgn * (i + 1) / (i + 2)) * (aa .^ i); kk_terms = kk_terms + newterm; if (max (abs (newterm)) <= eps) break endif endwhile g = a .* ((a .* kk_terms) .* (f - 1 - k) + k_terms); else g = (c ./ k - a ./ b) ./ (k .* b .^ (1/k)) - c ./ (k .^ 2) + a .* d ./ b; endif G(1) = sum (g(:)); ## sigma ## Use a series expansion to find the gradient more accurately when k is small if nargin > 4 && ! isempty (k_terms) g = (1 - a .* (a .* k .* kk_terms - k_terms) .* (f - k - 1)) ./ sigma; else g = (a .* b .^ (-d) - (k + 1) .* a ./ b + 1) ./ sigma; endif G(2) = sum (g(:)); ## mu ## Use a series expansion to find the gradient more accurately when k is small if (nargin > 4 && ! isempty (k_terms)) g = - (a .* k .* kk_terms - k_terms) .* (f - k - 1) ./ sigma; else g = (b .^ (-d) - (k + 1) ./ b) ./ sigma; endif G(3) = sum (g(:)); endfunction ## Internal function to calculate the Fisher information matrix for gevlike function ACOV = gevfim (x, k, sigma, mu, k_terms, kk_terms) ACOV = ones (3); ## Use the expressions for second derivatives that are the limits as k --> 0. ## MATLAB's k == 0 branch returns a different matrix, and is wrong: walking ## either library's k != 0 expressions toward zero converges on the values ## below, MATLAB's included. Deliberate deviation, see the docstring. if (k == 0) ## k, k a = (x - mu) ./ sigma; f = exp (-a); der = (a .^ 2) .* (a .* (a/4 - 2/3) .* f + 2/3 * a - 1); ACOV(1, 1) = sum (der(:)); ## sigma, sigma der = (sigma .^ -2) .* (a .* ((a - 2) .* f + 2) - 1); ACOV(2, 2) = sum (der(:)); ## mu, mu der = (sigma .^ -2) .* f; ACOV(3, 3) = sum (der(:)); ## k, sigma der = (-a ./ sigma) .* (a .* (1 - a/2) .* f - a + 1); ACOV(1, 2) = ACOV(2, 1) = sum (der(:)); ## k, mu der = (-1 ./ sigma) .* (a .* (1 - a/2) .* f - a + 1); ACOV(1, 3) = ACOV(3, 1) = sum (der(:)); ## sigma, mu der = (1 + (a - 1) .* f) ./ (sigma .^ 2); ACOV(2, 3) = ACOV(3, 2) = sum (der(:)); return endif ## General case z = 1 + k .* (x - mu) ./ sigma; ## k, k a = (x - mu) ./ sigma; b = k .* a + 1; c = log (b); d = 1 ./ k + 1; ## Use a series expansion to find the derivatives more accurately ## when k is small if (nargin > 5 && ! isempty (kk_terms)) aa = k .* a; f = exp (-a .* k_terms); kkk_terms = 2/3; sgn = 1; i = 0; while 1 sgn = -sgn; i++; newterm = (sgn * (i + 1) * (i + 2) / (i + 3)) * (aa .^ i); kkk_terms = kkk_terms + newterm; if (max (abs (newterm)) <= eps) break endif endwhile der = (a .^ 2) .* (a .* (a .* kk_terms .^ 2 - kkk_terms) .* ... f + a .* (1 + k) .* kkk_terms - 2 * kk_terms); else der = ((((c ./ k.^2) - (a ./ (k .* b))) .^ 2) ./ (b .^ (1 ./ k))) + ... ((-2*c ./ k.^3) + (2*a ./ (k.^2 .* b)) + ((a ./ b) .^ 2 ./ k)) ./ ... (b .^ (1 ./ k)) + 2*c ./ k.^3 - (2*a ./ (k.^2 .* b)) - (d .* (a ./ b) .^ 2); endif der(z <= 0) = 0; # no probability mass in this region ACOV(1, 1) = sum (der(:)); ## sigma, sigma ## Use a series expansion to find the derivatives more accurately ## when k is small if (nargin > 5 && ! isempty (kk_terms)) der = ((-2*a .* k_terms + 4 * a .^ 2 .* k .* kk_terms - a .^ 3 .* ... (k .^ 2) .* kkk_terms) .* (f - k - 1) + f .* ((a .* ... (k_terms - a .* k .* kk_terms)) .^ 2) - 1) ./ (sigma .^ 2); else der = (sigma .^ -2) .* (-2 * a .* b .^ (-d) + d .* k .* a .^ 2 .* ... (b .^ (-d-1)) + 2 .* d .* k .* a ./ b - d .* (k .* a ./ b) .^ 2 - 1); endif der(z <= 0) = 0; # no probability mass in this region ACOV(2, 2) = sum (der(:)); ## mu, mu ## Use a series expansion to find the derivatives more accurately ## when k is small if (nargin > 5 && ! isempty (kk_terms)) der = (f .* (a .* k .* kk_terms - k_terms) .^ 2 - a .* k .^ 2 .* ... kkk_terms .* (f - k - 1)) ./ (sigma .^ 2); else der = (d .* (sigma .^ -2)) .* (k .* (b .^ (-d-1)) - (k ./ b) .^ 2); endif der(z <= 0) = 0; # no probability mass in this region ACOV(3, 3) = sum (der(:)); ## k, mu ## Use a series expansion to find the derivatives more accurately ## when k is small if (nargin > 5 && ! isempty (kk_terms)) der = 2 * a .* kk_terms .* (f - 1 - k) - a .^ 2 .* k_terms .* ... kk_terms .* f + k_terms; der = -der ./ sigma; else der = ((b .^ (-d)) .* (c ./ k - a ./ b) ./ k - a .* (b .^ (-d-1)) + ... ((1 ./ k) - d) ./ b + a .* k .* d ./ (b .^ 2)) ./ sigma; endif der(z <= 0) = 0; # no probability mass in this region ACOV(1, 3) = ACOV(3, 1) = sum (der(:)); ## k, sigma der = a .* der; der(z <= 0) = 0; # no probability mass in this region ACOV(1, 2) = ACOV(2, 1) = sum (der(:)); ## sigma, mu ## Use a series expansion to find the derivatives more accurately ## when k is small if (nargin > 5 && ! isempty (kk_terms)) der = ((-k_terms + 3 * a .* k .* kk_terms - (a .* k) .^ 2 .* ... kkk_terms) .* (f - k - 1) + a .* (k_terms - a .* k .* ... kk_terms) .^ 2 .* f) ./ (sigma .^ 2); else der = (-(b .^ (-d)) + a .* k .* d .* (b .^ (-d-1)) + ... (d .* k ./ b) - a .* (k./b).^2 .* d) ./ (sigma .^ 2); endif der(z <= 0) = 0; # no probability mass in this region ACOV(2, 3) = ACOV(3, 2) = sum (der(:)); endfunction ## Test output %!test %! x = 1; %! k = 0.2; %! sigma = 0.3; %! mu = 0.5; %! [L, C] = gevlike ([k sigma mu], x); %! expected_L = 0.75942; %! expected_C = [-0.12547 1.77884 1.06731; 1.77884 16.40761 8.48877; 1.06731 8.48877 0.27979]; %! assert_equal (L, expected_L, 0.001); %! assert_equal (C, inv (expected_C), 0.001); %!test %! x = 1; %! k = 0; %! sigma = 0.3; %! mu = 0.5; %! [L, C] = gevlike ([k sigma mu], x); %! expected_L = 0.65157; %! expected_C = [0.090036 3.41229 2.047337; 3.412229 24.760027 12.510190; 2.047337 12.510190 2.098618]; %! assert_equal (L, expected_L, 0.001); %! assert_equal (C, inv (expected_C), 0.001); %!test %! ## ACOV is continuous at k = 0: the Gumbel-limit branch agrees with the %! ## general expressions evaluated just off zero. MATLAB's k = 0 branch does %! ## not, returning ACOV(1,1) = -0.3977 against the limit's -2.8133. %! [~, C0] = gevlike ([0, 0.3, 0.5], 1); %! [~, Ce] = gevlike ([1e-5, 0.3, 0.5], 1); %! assert_equal (C0, Ce, 1e-3); %! assert_equal (C0(1,1), -2.813275839387, 1e-9); %!test %! x = -5:-1; %! k = -0.2; %! sigma = 0.3; %! mu = 0.5; %! [L, C] = gevlike ([k sigma mu], x); %! expected_L = 3786.4; %! expected_C = [1.6802e-07, 4.6110e-06, 8.7297e-05; ... %! 4.6110e-06, 7.5693e-06, 1.2034e-05; ... %! 8.7297e-05, 1.2034e-05, -0.0019125]; %! assert_equal (L, expected_L, -0.001); %! assert_equal (C, expected_C, -0.001); %!test %! x = -5:0; %! k = -0.2; %! sigma = 0.3; %! mu = 0.5; %! [L, C] = gevlike ([k sigma mu], x, [1, 1, 1, 1, 1, 0]); %! expected_L = 3786.4; %! expected_C = [1.6802e-07, 4.6110e-06, 8.7297e-05; ... %! 4.6110e-06, 7.5693e-06, 1.2034e-05; ... %! 8.7297e-05, 1.2034e-05, -0.0019125]; %! assert_equal (L, expected_L, -0.001); %! assert_equal (C, expected_C, -0.001); ## Test input validation %!error gevlike (3.25) %!error gevlike ([1, 2, 3], ones (2)) %!error ... %! gevlike ([1, 2], [1, 3, 5, 7]) %!error ... %! gevlike ([1, 2, 3, 4], [1, 3, 5, 7]) %!error ... %! gevlike ([5, 0.2, 1], ones (10, 1), ones (8,1)) %!error ... %! gevlike ([5, 0.2, 1], ones (1, 8), [1 1 1 1 1 1 1 -1]) %!error ... %! gevlike ([5, 0.2, 1], ones (1, 8), [1 1 1 1 1 1 1 1.5]) statistics-release-2.1.2/inst/Distribution_Fitting/gpfit.m000066400000000000000000000360351527311104300237370ustar00rootroot00000000000000## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{paramhat} =} gpfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} gpfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} gpfit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} gpfit (@var{x}, @var{alpha}, @var{options}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} gpfit (@var{x}, @var{alpha}, @var{options}, @var{freq}) ## ## Estimate parameters and confidence intervals for the generalized Pareto ## distribution. ## ## @code{@var{paramhat} = gpfit (@var{x})} returns the maximum likelihood ## estimates of the parameters of the generalized Pareto distribution given the ## data in @var{x}. @qcode{@var{paramhat}(1)} is the shape parameter, @var{k}, ## and @qcode{@var{paramhat}(2)} is the scale parameter, @var{sigma}. ## ## @code{gpfit} does not estimate the location parameter @var{theta} and assumes ## it to be zero, so @var{x} must not contain negative values. To fit data with ## a known nonzero @var{theta}, subtract it from @var{x} before calling ## @code{gpfit}; the estimates of @var{k} and @var{sigma} are unchanged by the ## shift. ## ## @code{[@var{paramhat}, @var{paramci}] = gpfit (@var{x})} returns the 95% ## confidence intervals for the estimated parameters @var{k} and @var{sigma} as ## a @math{2}-by-@math{2} matrix whose first row holds the lower bounds and ## whose second row holds the upper bounds. ## ## @code{[@dots{}] = gpfit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals for the ## parameter estimates. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = gpfit (@var{x}, @var{alpha}, @var{options})} ## specifies control parameters for the iterative algorithm used to compute ML ## estimates with the @code{fminsearch} function. @var{options} is a structure ## with the following fields and their default values: ## @itemize ## @item @qcode{@var{options}.Display = "off"} ## @item @qcode{@var{options}.MaxFunEvals = 400} ## @item @qcode{@var{options}.MaxIter = 200} ## @item @qcode{@var{options}.TolX = 1e-6} ## @end itemize ## ## @code{[@dots{}] = gpfit (@var{x}, @var{alpha}, @var{options}, @var{freq})} ## accepts a vector of the same size as @var{x} giving the number of times each ## element of @var{x} was observed. This fourth argument is an Octave ## extension; MATLAB's @code{gpfit} takes three inputs at most. ## ## When the shape parameter falls below @math{-1} the likelihood is unbounded: ## the density at the upper endpoint of the support diverges as that endpoint ## closes onto the largest observation, so no maximum likelihood estimate ## exists and whatever is returned is an arbitrary point on that ridge. ## @code{gpfit} warns in this case and returns @code{NaN} confidence intervals. ## The estimate it does return always keeps every observation strictly inside ## the fitted support, since the likelihood is infinite outside it. This is a ## deliberate deviation: MATLAB has been measured returning parameters for such ## data under which the largest observation has zero density and its own ## @code{gplike} returns @code{Inf}. ## ## When @qcode{@var{k} = 0} and @qcode{@var{theta} = 0}, the Generalized Pareto ## is equivalent to the exponential distribution. When @qcode{@var{k} > 0} and ## @code{@var{theta} = @var{sigma} / @var{k}} the Generalized Pareto is ## equivalent to the Pareto distribution. The mean of the Generalized Pareto is ## not finite when @qcode{@var{k} >= 1} and the variance is not finite when ## @qcode{@var{k} >= 1/2}. When @qcode{@var{k} >= 0}, the Generalized Pareto ## has positive density for @qcode{@var{x} > @var{theta}}, or, when ## @qcode{@var{theta} < 0}, for ## @qcode{0 <= (@var{x} - @var{theta}) / @var{sigma} <= -1 / @var{k}}. ## ## Further information about the generalized Pareto distribution can be found at ## @url{https://en.wikipedia.org/wiki/Generalized_Pareto_distribution} ## ## @seealso{gpcdf, gpinv, gppdf, gprnd, gplike, gpstat} ## @end deftypefn function [paramhat, paramci] = gpfit (x, alpha, options, freq) ## Check for valid number of input arguments if (nargin < 1) error ("gpfit: function called with too few input arguments."); endif ## Check X for being a vector if (isempty (x)) paramhat = nan (1, 2, class (x)); paramci = nan (2, 2, class (x)); return elseif (! isvector (x) || ! isreal (x)) error ("gpfit: X must be a vector of real values."); endif ## The location parameter is assumed to be zero, so no observation may fall ## below it. Data with a known nonzero location is shifted by the caller. if (any (x < 0)) error ("gpfit: X must not contain negative values."); endif ## Parse ALPHA argument or add default if (nargin < 2 || isempty (alpha)) alpha = 0.05; elseif (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("gpfit: wrong value for ALPHA."); endif ## Parse FREQ argument or add default if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("gpfit: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("gpfit: FREQ must not contain negative values."); endif ## Expand frequency vector (if necessary) if (! all (freq == 1)) xf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; endfor x = xf; endif ## Get options structure or add defaults if (nargin < 3 || isempty (options)) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("gpfit: 'options' 3rd argument must be a", ... " structure with 'Display', 'MaxFunEvals',", ... " 'MaxIter', and 'TolX' fields present.")); endif endif ## Non-finite data is not removed: it propagates into the estimates, as it ## does in MATLAB and in every iterative fitter of this package. Dropping ## missing observations belongs to the wrappers a user hands raw data to, ## FITDIST and MLE, not to the estimator. ## Get sample size, max and range of X x_max = max (x); x_size = length (x); x_range = range (x); ## Check for appropriate sample size or all observations being equal if (x_size == 0) paramhat = NaN (1, 2); paramci = NaN (2, 2); warning ("gpfit: X contains no data."); return elseif (x_range < realmin (class (x))) paramhat = cast ([NaN, 0], class (x)); paramci = [paramhat; paramhat]; warning ("gpfit: X contains constant data."); return endif ## Make an initial guess x_mean = mean (x); x_var = var (x); k0 = -0.5 .* (x_mean .^ 2 ./ x_var - 1); s0 = 0.5 .* x_mean .* (x_mean .^ 2 ./ x_var + 1); ## If initial guess fails, start with an exponential fit if (k0 < 0 && (x_max >= -s0 / k0)) k0 = 0; s0 = x_mean; endif paramhat = [k0, log(s0)]; ## Maximize the log-likelihood with respect to shape and log_scale. f = @(paramhat) negloglike (paramhat, x); [paramhat, ~, err, output] = fminsearch (f, paramhat, options); paramhat(2) = exp (paramhat(2)); ## Check output of fminsearch and produce warnings or errors if applicable if (err == 0) if (output.funcCount >= options.MaxFunEvals) warning ("gpfit: reached evaluation limit."); else warning ("gpfit: reached iteration limit."); endif elseif (err < 0) error ("gpfit: no solution."); endif ## Check if converged to boundaries if ((paramhat(1) < 0) && (x_max > -paramhat(2)/paramhat(1) - options.TolX)) warning (strcat ("gpfit: the fitted upper bound of the support has", ... " closed onto the largest observation, a boundary of", ... " the parameter space, so the estimates are unreliable", ... " and no confidence intervals are computed.")); reachedBnd = true; elseif (paramhat(1) <= -1 / 2) warning (strcat ("gpfit: the shape parameter has converged to", ... " K <= -1/2, where the maximum likelihood estimator is", ... " not regular, so standard errors and confidence", ... " intervals cannot be computed reliably.")); reachedBnd = true; else reachedBnd = false; endif ## If second output argument is requested if (nargout > 1) if (! reachedBnd) probs = [alpha/2; 1-alpha/2]; [~, acov] = gplike (paramhat, x); se = sqrt (diag (acov))'; ## Compute the CI for shape using a normal distribution for khat. kci = norminv (probs, paramhat(1), se(1)); ## Compute the CI for scale using a normal approximation for ## log(sigmahat), and transform back to the original scale. lnsigci = norminv (probs, log (paramhat(2)), se(2) ./ paramhat(2)); paramci = [kci, exp(lnsigci)]; else paramci = [NaN, NaN; NaN, NaN]; endif endif endfunction ## Negative log-likelihood for the GP function nll = negloglike (paramhat, data) shape = paramhat(1); log_scale = paramhat(2); scale = exp (log_scale); sample_size = numel (data); z = data ./ scale; if (abs (shape) > eps) if (shape > 0 || max (z) < -1 / shape) nll = sample_size * log_scale + (1 + 1/shape) * sum (log1p (shape .* z)); else nll = Inf; endif else nll = sample_size * log_scale + sum (z); endif endfunction %!demo %! ## Sample 2 populations from different generalized Pareto distributions %! ## Assume location parameter θ is known %! rng (42); %! theta = 0; %! r1 = gprnd (1, 2, theta, 20000, 1); %! r2 = gprnd (3, 1, theta, 20000, 1); %! r = [r1, r2]; %! %! ## Plot them normalized and fix their colors %! hist (r, [0.1:0.2:100], 5); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'r'); %! set (h(2), 'facecolor', 'c'); %! ylim ([0, 1]); %! xlim ([0, 5]); %! hold on %! %! ## Estimate their α and β parameters %! k_sigmaA = gpfit (r(:,1)); %! k_sigmaB = gpfit (r(:,2)); %! %! ## Plot their estimated PDFs %! x = [0.01, 0.1:0.2:18]; %! y = gppdf (x, k_sigmaA(1), k_sigmaA(2), theta); %! plot (x, y, '-pc'); %! y = gppdf (x, k_sigmaB(1), k_sigmaB(2), theta); %! plot (x, y, '-sr'); %! hold off %! legend ({'Normalized HIST of sample 1 with k=1 and σ=2', ... %! 'Normalized HIST of sample 2 with k=2 and σ=2', ... %! sprintf("PDF for sample 1 with estimated k=%0.2f and σ=%0.2f", ... %! k_sigmaA(1), k_sigmaA(2)), ... %! sprintf("PDF for sample 3 with estimated k=%0.2f and σ=%0.2f", ... %! k_sigmaB(1), k_sigmaB(2))}) %! title ('Two population samples from different generalized Pareto distributions') %! text (2, 0.7, 'Known location parameter θ = 0') %! hold off ## Test output ## Values below are R2024a's, measured 2026-08-17. The estimates agree to the ## convergence tolerance of fminsearch, which is what the 1e-4 covers. %!shared x %! x = [2.2196, 11.9301, 4.3673, 1.0949, 6.5626, ... %! 1.2109, 1.8576, 1.0039, 12.7917, 2.2590]; %!test %! [hat, ci] = gpfit (x); %! assert_equal (hat, [-0.163107819293798, 5.305483917184919], 1e-4); %! assert_equal (ci, [-1.174106637867584, 1.627748133572634; ... %! 0.847890999279987, 17.292699659699391], 1e-4); %!test %! [hat, ci] = gpfit (x, 0.10); %! assert_equal (ci, [-1.011564773958192, 1.968276868273005; ... %! 0.685349135370595, 14.300914698146826], 1e-4); %!test %! ## a known location is fitted by shifting the data, and only shifts the fit %! [hat, ci] = gpfit (x - 1); %! assert_equal (hat, [0.893710299404345, 1.322962458731574], 1e-6); %! assert_equal (ci, [-0.774991092191746, 0.243695078371714; ... %! 2.562411691000436, 7.182047659343478], 1e-5); %!assert_equal (size (gpfit (x)), [1, 2]) %!test %! [~, ci] = gpfit (x); %! assert_equal (size (ci), [2, 2]); %!test %! ## the default confidence level is 95% %! [h1, c1] = gpfit (x); %! [h2, c2] = gpfit (x, 0.05); %! assert_equal (h1, h2); %! assert_equal (c1, c2); %!test %! ## FREQ counts repeated observations %! assert_equal (gpfit (x, [], [], [2, ones(1,9)]), gpfit ([x(1), x]), 1e-10); %!test %! ## non-finite data propagates into the estimates instead of being dropped %! assert_equal (gpfit ([x, NaN]), [NaN, NaN]); %!test %! assert_equal (gpfit ([x, Inf]), [NaN, NaN]); %!test %! assert_equal (gpfit ([x, NaN, Inf]), [NaN, NaN]); %!test %! ## below a shape of -1 the likelihood is unbounded, but the estimate still %! ## keeps every observation strictly inside the fitted support %! xb = [1.2 2.3 0.5 3.1 2.2 1.8 0.9 2.7 1.1 3.3]; %! warning ('off', 'all'); %! p = gpfit (xb); %! warning ('on', 'all'); %! assert_equal (p(1) < -1, true); %! assert_equal (max (xb) < -p(2) / p(1), true); %! assert_equal (isfinite (gplike (p, xb)), true); %!test %! ## the confidence intervals are withheld there %! warning ('off', 'all'); %! [~, ci] = gpfit ([1.2 2.3 0.5 3.1 2.2 1.8 0.9 2.7 1.1 3.3]); %! warning ('on', 'all'); %! assert_equal (ci, [NaN, NaN; NaN, NaN]); %!warning ... %! gpfit ([1.2 2.3 0.5 3.1 2.2 1.8 0.9 2.7 1.1 3.3]); ## Test input validation %!error gpfit () %!error gpfit ([0.2, 0.5+i]); %!error gpfit (ones (2,2) * 0.5); %!error gpfit ([-1, 2, 3]); %!error gpfit ([0.01:0.1:0.99], 1.2); %!error gpfit ([0.01:0.1:0.99], i); %!error gpfit ([0.01:0.1:0.99], -1); %!error gpfit ([0.01:0.1:0.99], [0.05, 0.01]); %!error ... %! gpfit ([1 2 3], [], [], [1 5]) %!error ... %! gpfit ([1 2 3], [], [], [1 5 -1]) %!error ... %! gpfit ([1:10], 0.05, 5) statistics-release-2.1.2/inst/Distribution_Fitting/gplike.m000066400000000000000000000165401527311104300241000ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} gplike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} gplike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} gplike (@var{params}, @var{x}, @var{freq}) ## ## Negative log-likelihood for the generalized Pareto distribution. ## ## @code{@var{nlogL} = gplike (@var{params}, @var{x})} returns the negative ## log-likelihood of the data in @var{x} corresponding to the generalized Pareto ## distribution with (1) shape parameter @var{k} and (2) scale parameter ## @var{sigma} given in the two-element vector @var{params}. ## ## @code{gplike} does not accept a location parameter @var{theta} and assumes it ## to be zero. If the location is known to be nonzero, subtract it from ## @var{x} before calling @code{gplike}. ## ## @code{[@var{nlogL}, @var{acov}] = gplike (@var{params}, @var{x})} returns ## the inverse of Fisher's information matrix, @var{acov}, a ## @math{2}-by-@math{2} matrix. If the input parameter values in @var{params} ## are the maximum likelihood estimates, the diagonal elements of @var{acov} are ## their asymptotic variances. @var{acov} is based on the observed Fisher's ## information, not the expected information. ## ## @code{[@dots{}] = gplike (@var{params}, @var{x}, @var{freq})} accepts a ## frequency vector, @var{freq}, of the same size as @var{x}. @var{freq} ## typically contains integer frequencies for the corresponding elements in ## @var{x}, but it can contain any non-integer non-negative values. By default, ## or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. This third ## argument is an Octave extension; MATLAB's @code{gplike} takes two inputs. ## ## When @qcode{@var{k} = 0} and @qcode{@var{mu} = 0}, the Generalized Pareto CDF ## is equivalent to the exponential distribution. When @qcode{@var{k} > 0} and ## @code{@var{mu} = @var{k} / @var{k}} the Generalized Pareto is equivalent to ## the Pareto distribution. The mean of the Generalized Pareto is not finite ## when @qcode{@var{k} >= 1} and the variance is not finite when ## @qcode{@var{k} >= 1/2}. When @qcode{@var{k} >= 0}, the Generalized Pareto ## has positive density for @qcode{@var{x} > @var{mu}}, or, when ## @qcode{@var{mu} < 0}, for ## @qcode{0 <= (@var{x} - @var{mu}) / @var{sigma} <= -1 / @var{k}}. ## ## Further information about the generalized Pareto distribution can be found at ## @url{https://en.wikipedia.org/wiki/Generalized_Pareto_distribution} ## ## @seealso{gpcdf, gpinv, gppdf, gprnd, gpfit, gpstat} ## @end deftypefn function [nlogL, acov] = gplike (params, x, freq) ## Check input arguments if (nargin < 2) error ("gplike: function called with too few input arguments."); endif if (! isvector (x)) error ("gplike: X must be a vector."); endif if (numel (params) != 2) error ("gplike: PARAMS must be a two-element vector."); endif ## Parse FREQ argument or add default if (nargin < 3 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("gplike: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("gplike: FREQ must not contain negative values."); endif ## Expand frequency vector (if necessary) if (! all (freq == 1)) xf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; endfor x = xf; endif ## Get K and SIGMA parameters. The location is assumed to be zero, the ## caller having shifted the data by any known value. k = params(1); sigma = params(2); ## Get sample size and sigma x sz = numel (x); z = x ./ sigma; ## For K > 0 if (abs (k) > eps) if (k > 0 || max (z) < -1 / k) sumLn = sum (log1p (k .* z)); nlogL = sz * log (sigma) + (1 + 1 / k) .* sumLn; if (nargout > 1) z_kz = z ./ (1 + k .* z); sumv = sum (z_kz); sumvsq = sum (z_kz .^ 2); nH11 = 2 * sumLn ./ k ^ 3 - ... 2 * sumv ./ k ^ 2 - (1 + 1 / k) .* sumvsq; nH12 = (-sumv + (k + 1) .* sumvsq) ./ sigma; nH22 = (-sz + 2 * (k + 1) .* sumv - ... k * (k + 1) .* sumvsq) ./ sigma ^ 2; acov = [nH22, -nH12; -nH12, nH11] / (nH11 * nH22 - nH12 * nH12); endif else ## The support of the GP when k<0 is 0 < y < abs(sigma/k) nlogL = Inf; if (nargout > 1) acov = [NaN, NaN; NaN, NaN]; endif endif else # For k = 0 nlogL = sz * log (sigma) + sum (z); if (nargout > 1) sumz = sum (z); sumzsq = sum (z .^ 2); sumzcb = sum (z .^ 3); nH11 = (2 / 3) * sumzcb - sumzsq; nH12 = (-sumz + sumzsq) ./ sigma; nH22 = (-sz + 2 * sumz) ./ sigma ^ 2; acov = [nH22, -nH12; -nH12, nH11] / (nH11 * nH22 - nH12 * nH12); endif endif endfunction ## Test output ## The first block's values are R2024a's, measured 2026-08-17. %!test %! k = 0.893710299404345; sigma = 1.322962458731574; %! x = [2.2196, 11.9301, 4.3673, 1.0949, 6.5626, ... %! 1.2109, 1.8576, 1.0039, 12.7917, 2.2590] - 1; %! [nlogL, acov] = gplike ([k, sigma], x); %! assert_equal (nlogL, 21.735838309709596, 1e-12); %! assert_equal (acov, [ 0.724871582460845, -0.735076013984010; ... %! -0.735076013984010, 1.303920812049488], 1e-12); %!test %! ## the covariance covers the two estimated parameters only %! [~, acov] = gplike ([2, 3], [1:10]); %! assert_equal (size (acov), [2, 2]); %!test %! ## a known location is handled by shifting the data %! assert_equal (gplike ([2, 3], ([1:10] + 2) - 2), ... %! gplike ([2, 3], [1:10]), 1e-14); %!assert_equal (gplike ([2, 3], 4), 3.047536764863501, 1e-14) %!assert_equal (gplike ([1, 2], 4), 2.890371757896165, 1e-14) %!assert_equal (gplike ([2, 3], [1:10]), 32.57864322725392, 1e-14) %!assert_equal (gplike ([1, 2], [1:10]), 31.65666282460443, 1e-14) %!assert_equal (gplike ([2, 3], [1:10], ones (1,10)), 32.57864322725392, 1e-14) %!assert_equal (gplike ([1, 2], [1:10], ones (1,10)), 31.65666282460443, 1e-14) %!assert_equal (gplike ([1, NaN], [1:10]), NaN) ## Test input validation %!error gplike () %!error gplike (1) %!error gplike ([1, 2], []) %!error gplike ([1, 2], ones (2)) %!error gplike (2, [1:10]) %!error gplike ([1, 2, 0], [1:10]) %!error ... %! gplike ([1, 2], ones (10, 1), ones (8,1)) %!error ... %! gplike ([1, 2], ones (1, 8), [1 1 1 1 1 1 1 -1]) statistics-release-2.1.2/inst/Distribution_Fitting/gumbelfit.m000066400000000000000000000221701527311104300245770ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{paramhat} =} gumbelfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} gumbelfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} gumbelfit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@dots{}] =} gumbelfit (@var{x}, @var{alpha}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} gumbelfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}) ## @deftypefnx {statistics} {[@dots{}] =} gumbelfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}, @var{options}) ## ## Estimate parameters and confidence intervals for Gumbel distribution. ## ## @code{@var{paramhat} = gumbelfit (@var{x})} returns the maximum likelihood ## estimates of the parameters of the Gumbel distribution (also known as ## the extreme value or the type I generalized extreme value distribution) given ## in @var{x}. @qcode{@var{paramhat}(1)} is the location parameter, @var{mu}, ## and @qcode{@var{paramhat}(2)} is the scale parameter, @var{beta}. ## ## @code{[@var{paramhat}, @var{paramci}] = gumbelfit (@var{x})} returns the 95% ## confidence intervals for the parameter estimates. ## ## @code{[@dots{}] = gumbelfit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals for the ## parameter estimates. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = gumbelfit (@var{x}, @var{alpha}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = gumbelfit (@var{x}, @var{alpha}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## @code{[@dots{}] = gumbelfit (@dots{}, @var{options})} specifies control ## parameters for the iterative algorithm used to compute the maximum likelihood ## estimates. @var{options} is a structure with the following field and its ## default value: ## @itemize ## @item @qcode{@var{options}.Display = "off"} ## @item @qcode{@var{options}.MaxFunEvals = 400} ## @item @qcode{@var{options}.MaxIter = 200} ## @item @qcode{@var{options}.TolX = 1e-6} ## @end itemize ## ## The Gumbel distribution is used to model the distribution of the maximum (or ## the minimum) of a number of samples of various distributions. This version ## is suitable for modeling maxima. For modeling minima, use the alternative ## extreme value fitting function, @code{evfit}. ## ## Further information about the Gumbel distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gumbel_distribution} ## ## @seealso{gumbelcdf, gumbelinv, gumbelpdf, gumbelrnd, gumbellike, gumbelstat, ## evfit} ## @end deftypefn function [paramhat, paramci] = gumbelfit (x, alpha, censor, freq, options) ## Check X for being a double precision vector if (! isvector (x) || ! isa (x, 'double')) error ("gumbelfit: X must be a double-precision vector."); endif ## Check that X does not contain missing values (NaNs) if (any (isnan (x))) error ("gumbelfit: X must NOT contain missing values (NaNs)."); endif ## Check alpha if (nargin < 2 || isempty (alpha)) alpha = 0.05; else if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("gumbelfit: wrong value for ALPHA."); endif endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("gumbelfit: X and CENSOR vectors mismatch."); endif ## Parse FREQ argument or add default if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("gumbelfit: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("gumbelfit: FREQ must not contain negative values."); endif ## Get options structure or add defaults if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("gumbelfit: 'options' 5th argument must be a", ... " structure with 'Display', 'MaxFunEvals',", ... " 'MaxIter', and 'TolX' fields present.")); endif endif ## Remove zeros and NaNs from frequency vector (if necessary) if (! all (freq == 1)) remove = freq == 0 | isnan (freq); x(remove) = []; censor(remove) = []; freq(remove) = []; endif ## If X is a column vector, make X, CENSOR, and FREQ row vectors if (size (x, 1) > 1) x = x(:)'; censor = censor(:)'; freq = freq(:)'; endif ## Call evfit to do the actual computation on the negative X try [paramhat, paramci] = evfit (-x, alpha, censor, freq, options); catch error ("gumbelfit: no solution for maximum likelihood estimates."); end_try_catch ## Flip sign on estimated parameter MU paramhat(1) = -paramhat(1); ## Flip sign on confidence intervals of parameter MU paramci(:,1) = -flip (paramci(:,1)); endfunction %!demo %! ## Sample 3 populations from different Gumbel distributions %! rng (42); %! r1 = gumbelrnd (2, 5, 400, 1); %! r2 = gumbelrnd (-5, 3, 400, 1); %! r3 = gumbelrnd (14, 8, 400, 1); %! r = [r1, r2, r3]; %! %! ## Plot them normalized and fix their colors %! hist (r, 25, 0.32); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! set (h(3), 'facecolor', 'r'); %! ylim ([0, 0.28]) %! xlim ([-11, 50]); %! hold on %! %! ## Estimate their MU and BETA parameters %! mu_betaA = gumbelfit (r(:,1)); %! mu_betaB = gumbelfit (r(:,2)); %! mu_betaC = gumbelfit (r(:,3)); %! %! ## Plot their estimated PDFs %! x = [min(r(:)):max(r(:))]; %! y = gumbelpdf (x, mu_betaA(1), mu_betaA(2)); %! plot (x, y, '-pr'); %! y = gumbelpdf (x, mu_betaB(1), mu_betaB(2)); %! plot (x, y, '-sg'); %! y = gumbelpdf (x, mu_betaC(1), mu_betaC(2)); %! plot (x, y, '-^c'); %! legend ({'Normalized HIST of sample 1 with μ=2 and β=5', ... %! 'Normalized HIST of sample 2 with μ=-5 and β=3', ... %! 'Normalized HIST of sample 3 with μ=14 and β=8', ... %! sprintf("PDF for sample 1 with estimated μ=%0.2f and β=%0.2f", ... %! mu_betaA(1), mu_betaA(2)), ... %! sprintf("PDF for sample 2 with estimated μ=%0.2f and β=%0.2f", ... %! mu_betaB(1), mu_betaB(2)), ... %! sprintf("PDF for sample 3 with estimated μ=%0.2f and β=%0.2f", ... %! mu_betaC(1), mu_betaC(2))}) %! title ('Three population samples from different Gumbel distributions') %! hold off ## Test output %!test %! x = 1:50; %! [paramhat, paramci] = gumbelfit (x); %! paramhat_out = [18.3188, 13.0509]; %! paramci_out = [14.4882, 10.5294; 22.1495, 16.1763]; %! assert_equal (paramhat, paramhat_out, 1e-4); %! assert_equal (paramci, paramci_out, 1e-4); %!test %! x = 1:50; %! [paramhat, paramci] = gumbelfit (x, 0.01); %! paramci_out = [13.2845, 9.8426; 23.3532, 17.3051]; %! assert_equal (paramci, paramci_out, 1e-4); ## Test input validation %!error gumbelfit (ones (2,5)); %!error ... %! gumbelfit (single (ones (1,5))); %!error ... %! gumbelfit ([1, 2, 3, 4, NaN]); %!error gumbelfit ([1, 2, 3, 4, 5], 1.2); %!error ... %! gumbelfit ([1, 2, 3, 4, 5], 0.05, [1 1 0]); %!error ... %! gumbelfit ([1, 2, 3, 4, 5], 0.05, [], [1 1 0]); %!error %! gamfit ([1, 2, 3], 0.05, [], [1 5 -1]) %!error ... %! gumbelfit ([1, 2, 3, 4, 5], 0.05, [], [], 2); statistics-release-2.1.2/inst/Distribution_Fitting/gumbellike.m000066400000000000000000000137621527311104300247500ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} gumbellike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{avar}] =} gumbellike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} gumbellike (@var{params}, @var{x}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} gumbellike (@var{params}, @var{x}, @var{censor}, @var{freq}) ## ## Negative log-likelihood for the extreme value distribution. ## ## @code{@var{nlogL} = gumbellike (@var{params}, @var{x})} returns the negative ## log likelihood of the data in @var{x} corresponding to the Gumbel ## distribution (also known as the extreme value or the type I generalized ## extreme value distribution) with (1) location parameter @var{mu} and (2) ## scale parameter @var{beta} given in the two-element vector @var{params}. ## ## @code{[@var{nlogL}, @var{acov}] = gumbellike (@var{params}, @var{x})} also ## returns the inverse of Fisher's information matrix, @var{acov}. If the input ## parameter values in @var{params} are the maximum likelihood estimates, the ## diagonal elements of @var{acov} are their asymptotic variances. ## ## @code{[@dots{}] = gumbellike (@var{params}, @var{x}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = gumbellike (@var{params}, @var{x}, @var{censor}, ## @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## The Gumbel distribution is used to model the distribution of the maximum (or ## the minimum) of a number of samples of various distributions. This version ## is suitable for modeling maxima. For modeling minima, use the alternative ## extreme value likelihood function, @code{evlike}. ## ## Further information about the Gumbel distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gumbel_distribution} ## ## @seealso{gumbelcdf, gumbelinv, gumbelpdf, gumbelrnd, gumbelfit, gumbelstat, ## evlike} ## @end deftypefn function [nlogL, avar] = gumbellike (params, x, censor, freq) ## Check input arguments and add defaults if (nargin < 2) error ("gumbellike: too few input arguments."); endif if (numel (params) != 2) error ("gumbellike: wrong parameters length."); endif if (! isvector (x)) error ("gumbellike: X must be a vector."); endif if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("gumbellike: X and CENSOR vectors mismatch."); endif if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (isequal (size (x), size (freq))) nulls = find (freq == 0); if (numel (nulls) > 0) x(nulls) = []; censor(nulls) = []; freq(nulls) = []; endif else error ("gumbellike: X and FREQ vectors mismatch."); endif ## Get mu and sigma values mu = params(1); sigma = params(2); ## sigma must be positive, otherwise make it NaN if (sigma <= 0) sigma = NaN; endif ## Compute the individual log-likelihood terms. Force a log(0)==-Inf for ## x from extreme right tail, instead of getting exp(Inf-Inf)==NaN. z = (x - mu) ./ sigma; expz = exp (z); L = (z - log (sigma)) .* (1 - censor) - expz; L(z == Inf) = -Inf; ## Neg-log-like is the sum of the individual contributions nlogL = -sum (freq .* L); ## Compute the negative hessian at the parameter values. ## Invert to get the observed information matrix. if (nargout == 2) unc = (1-censor); nH11 = sum (freq .* expz); nH12 = sum (freq .* ((z + 1) .* expz - unc)); nH22 = sum (freq .* (z .* (z+2) .* expz - ((2 .* z + 1) .* unc))); avar = (sigma .^ 2) * ... [nH22 -nH12; -nH12 nH11] / (nH11 * nH22 - nH12 * nH12); endif endfunction ## Test output %!test %! x = 1:50; %! [nlogL, avar] = gumbellike ([2.3, 1.2], x); %! avar_out = [-1.2778e-13, 3.1859e-15; 3.1859e-15, -7.9430e-17]; %! assert_equal (nlogL, 3.242264755689906e+17, 1e-14); %! assert_equal (avar, avar_out, 1e-3); %!test %! x = 1:50; %! [nlogL, avar] = gumbellike ([2.3, 1.2], x * 0.5); %! avar_out = [-7.6094e-05, 3.9819e-06; 3.9819e-06, -2.0836e-07]; %! assert_equal (nlogL, 481898704.0472211, 1e-6); %! assert_equal (avar, avar_out, 1e-3); %!test %! x = 1:50; %! [nlogL, avar] = gumbellike ([21, 15], x); %! avar_out = [11.73913876598908, -5.9546128523121216; ... %! -5.954612852312121, 3.708060045170236]; %! assert_equal (nlogL, 223.7612479380652, 1e-13); %! assert_equal (avar, avar_out, 1e-14); ## Test input validation %!error gumbellike ([12, 15]); %!error gumbellike ([12, 15, 3], [1:50]); %!error gumbellike ([12, 3], ones (10, 2)); %!error gumbellike ([12, 15], [1:50], [1, 2, 3]); %!error gumbellike ([12, 15], [1:50], [], [1, 2, 3]); statistics-release-2.1.2/inst/Distribution_Fitting/hnfit.m000066400000000000000000000161621527311104300237350ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{paramhat}, @var{paramci}] =} hnfit (@var{x}, @var{mu}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} hnfit (@var{x}, @var{mu}, @var{alpha}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} hnfit (@var{x}, @var{mu}, @var{alpha}, @var{freq}) ## ## Estimate parameters and confidence intervals for the half-normal ## distribution. ## ## @code{@var{paramhat} = hnfit (@var{x}, @var{mu})} returns the maximum ## likelihood estimates of the parameters of the half-normal distribution given ## the data in vector @var{x} and the location parameter @var{mu}. ## @qcode{@var{paramhat}(1)} is the location parameter, @var{mu}, and ## @qcode{@var{paramhat}(2)} is the scale parameter, @var{sigma}. Although ## @var{mu} is returned in the estimated @var{paramhat}, @code{hnfit} does not ## estimate the location parameter @var{mu}, and it must be assumed to be known, ## given as a fixed parameter in input argument @var{mu}. ## ## @code{[@var{paramhat}, @var{paramci}] = hnfit (@var{x}, @var{mu})} returns ## the 95% confidence intervals for the estimated scale parameter @var{sigma}. ## The first column of @var{paramci} includes the location parameter @var{mu} ## without any confidence bounds. ## ## @code{[@dots{}] = hnfit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals of the estimated ## scale parameter. By default, the optional argument @var{alpha} is 0.05 ## corresponding to 95% confidence intervals. ## ## @code{[@dots{}] = hnfit (@var{params}, @var{x}, @var{freq})} accepts a ## frequency vector, @var{freq}, of the same size as @var{x}. @var{freq} ## must contain non-negative integer frequencies for the corresponding elements ## in @var{x}. By default, or if left empty, ## @qcode{@var{freq} = ones (size (@var{x}))}. ## ## The half-normal CDF is only defined for @qcode{@var{x} >= @var{mu}}. ## ## Further information about the half-normal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Half-normal_distribution} ## ## @seealso{hncdf, hninv, hnpdf, hnrnd, hnlike, hnstat} ## @end deftypefn function [paramhat, paramci] = hnfit (x, mu, alpha, freq) ## Check for valid number of input arguments if (nargin < 2) error ("hnfit: function called with too few input arguments."); endif ## Check X for being a vector if (isempty (x)) phat = nan (1, 2, class (x)); pci = nan (2, 2, class (x)); return elseif (! isvector (x) || ! isreal (x)) error ("hnfit: X must be a vector of real values."); endif ## Check for MU being a scalar real value if (! isscalar (mu) || ! isreal (mu)) error ("hnfit: MU must be a real scalar value."); endif ## Check X >= MU if (any (x < mu)) error ("hnfit: X cannot contain values less than MU."); endif ## Parse ALPHA argument or add default if (nargin < 3 || isempty (alpha)) alpha = 0.05; elseif (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("hnfit: wrong value for ALPHA."); endif ## Parse FREQ argument or add default if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("hnfit: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("hnfit: FREQ must not contain negative values."); endif ## Expand frequency vector (if necessary) if (! all (freq == 1)) xf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; endfor x = xf; endif ## Estimate parameters sz = numel (x); x = x - mu; sigmahat = sqrt (sum (x .* x) ./ sz); paramhat = [mu, sigmahat]; ## Compute confidence intervals if (nargout == 2) chi2cr = chi2inv ([alpha/2, 1-alpha/2], sz); shatlo = sigmahat * sqrt (sz / chi2inv (1 - alpha / 2, sz)); shathi = sigmahat * sqrt (sz / chi2inv (alpha / 2, sz)); paramci = [mu, shatlo; mu, shathi]; endif endfunction %!demo %! ## Sample 2 populations from different half-normal distributions %! rng (42); %! r1 = hnrnd (0, 5, 5000, 1); %! r2 = hnrnd (0, 2, 5000, 1); %! r = [r1, r2]; %! %! ## Plot them normalized and fix their colors %! hist (r, [0.5:20], 1); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! hold on %! %! ## Estimate their shape parameters %! mu_sigmaA = hnfit (r(:,1), 0); %! mu_sigmaB = hnfit (r(:,2), 0); %! %! ## Plot their estimated PDFs %! x = [0:0.2:10]; %! y = hnpdf (x, mu_sigmaA(1), mu_sigmaA(2)); %! plot (x, y, '-pr'); %! y = hnpdf (x, mu_sigmaB(1), mu_sigmaB(2)); %! plot (x, y, '-sg'); %! xlim ([0, 10]) %! ylim ([0, 0.5]) %! legend ({'Normalized HIST of sample 1 with μ=0 and σ=5', ... %! 'Normalized HIST of sample 2 with μ=0 and σ=2', ... %! sprintf("PDF for sample 1 with estimated μ=%0.2f and σ=%0.2f", ... %! mu_sigmaA(1), mu_sigmaA(2)), ... %! sprintf("PDF for sample 2 with estimated μ=%0.2f and σ=%0.2f", ... %! mu_sigmaB(1), mu_sigmaB(2))}) %! title ('Two population samples from different half-normal distributions') %! hold off ## Test output %!test %! x = 1:20; %! [paramhat, paramci] = hnfit (x, 0); %! assert_equal (paramhat, [0, 11.9791], 1e-4); %! assert_equal (paramci, [0, 9.1648; 0, 17.2987], 1e-4); %!test %! x = 1:20; %! [paramhat, paramci] = hnfit (x, 0, 0.01); %! assert_equal (paramci, [0, 8.4709; 0, 19.6487], 1e-4); ## Test input validation %!error hnfit () %!error hnfit (1) %!error hnfit ([0.2, 0.5+i], 0); %!error hnfit (ones (2,2) * 0.5, 0); %!error ... %! hnfit ([0.5, 1.2], [0, 1]); %!error ... %! hnfit ([0.5, 1.2], 5+i); %!error ... %! hnfit ([1:5], 2); %!error hnfit ([0.01:0.1:0.99], 0, 1.2); %!error hnfit ([0.01:0.1:0.99], 0, i); %!error hnfit ([0.01:0.1:0.99], 0, -1); %!error hnfit ([0.01:0.1:0.99], 0, [0.05, 0.01]); %!error %! hnfit ([1 2 3], 0, [], [1 5]) %!error %! hnfit ([1 2 3], 0, [], [1 5 -1]) statistics-release-2.1.2/inst/Distribution_Fitting/hnlike.m000066400000000000000000000114051527311104300240720ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} hnlike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} hnlike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} hnlike (@var{params}, @var{x}, @var{freq}) ## ## Negative log-likelihood for the half-normal distribution. ## ## @code{@var{nlogL} = hnlike (@var{params}, @var{x})} returns the negative ## log likelihood of the data in @var{x} corresponding to the half-normal ## distribution with (1) location parameter @var{mu} and (2) scale parameter ## @var{sigma} given in the two-element vector @var{params}. ## ## @code{[@var{nlogL}, @var{acov}] = hnlike (@var{params}, @var{x})} returns ## the inverse of Fisher's information matrix, @var{acov}. If the input ## parameter values in @var{params} are the maximum likelihood estimates, the ## diagonal elements of @var{acov} are their asymptotic variances. ## ## @code{[@dots{}] = hnlike (@var{params}, @var{x}, @var{freq})} accepts a ## frequency vector, @var{freq}, of the same size as @var{x}. @var{freq} ## typically contains integer frequencies for the corresponding elements in ## @var{x}, but it can contain any non-integer non-negative values. By default, ## or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## The half-normal CDF is only defined for @qcode{@var{x} >= @var{mu}}. ## ## Further information about the half-normal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Half-normal_distribution} ## ## @seealso{hncdf, hninv, hnpdf, hnrnd, hnfit, hnstat} ## @end deftypefn function [nlogL, acov] = hnlike (params, x, freq) ## Check input arguments and add defaults if (nargin < 2) error ("hnlike: function called with too few input arguments."); endif if (numel (params) != 2) error ("hnlike: wrong parameters length."); endif ## Check X for being a vector if (isempty (x)) phat = nan (1, 2, class (x)); pci = nan (2, 2, class (x)); return elseif (! isvector (x) || ! isreal (x)) error ("hnlike: X must be a vector of real values."); endif ## Parse FREQ argument or add default if (nargin < 3 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("hnlike: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("hnlike: FREQ must not contain negative values."); endif ## Expand frequency vector (if necessary) if (! all (freq == 1)) xf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; endfor x = xf; endif ## Get MU and SIGMA parameters mu = params(1); sigma = params(2); ## Force X to column vector x = x(:); ## Return NaN for out of range parameters or data. sigma(sigma <= 0) = NaN; x(x < mu) = NaN; z = (x - mu) ./ sigma; ## Sum up the individual log-likelihood terms nlogL = -sum (-0.5 .* z .* z - log (sqrt (pi ./ 2) .* sigma)); ## Compute asymptotic covariance (if requested) if (nargout == 2) nH = -sum (1 - 3 .* z .* z); avar = (sigma .^ 2) ./ nH; acov = [0, 0; 0, avar]; endif endfunction ## Test output %!test %! x = 1:20; %! paramhat = hnfit (x, 0); %! [nlogL, acov] = hnlike (paramhat, x); %! assert_equal (nlogL, 64.179177404891300, 1e-14); %!test %! x = 1:20; %! paramhat = hnfit (x, 0); %! [nlogL, acov] = hnlike (paramhat, x, ones (1, 20)); %! assert_equal (nlogL, 64.179177404891300, 1e-14); ## Test input validation %!error ... %! hnlike ([12, 15]); %!error hnlike ([12, 15, 3], [1:50]); %!error hnlike ([3], [1:50]); %!error ... %! hnlike ([0, 3], ones (2)); %!error ... %! hnlike ([0, 3], [1, 2, 3, 4, 5+i]); %!error ... %! hnlike ([1, 2], ones (10, 1), ones (8,1)) %!error ... %! hnlike ([1, 2], ones (1, 8), [1 1 1 1 1 1 1 -1]) statistics-release-2.1.2/inst/Distribution_Fitting/invgfit.m000066400000000000000000000225771527311104300243020ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{paramhat} =} invgfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} invgfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} invgfit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@dots{}] =} invgfit (@var{x}, @var{alpha}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} invgfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}) ## @deftypefnx {statistics} {[@dots{}] =} invgfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}, @var{options}) ## ## Estimate mean and confidence intervals for the inverse Gaussian distribution. ## ## @code{@var{mu0} = invgfit (@var{x})} returns the maximum likelihood ## estimates of the parameters of the inverse Gaussian distribution given the ## data in @var{x}. @qcode{@var{paramhat}(1)} is the scale parameter, @var{mu}, ## and @qcode{@var{paramhat}(2)} is the shape parameter, @var{lambda}. ## ## @code{[@var{paramhat}, @var{paramci}] = invgfit (@var{x})} returns the 95% ## confidence intervals for the parameter estimates. ## ## @code{[@dots{}] = invgfit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals for the ## parameter estimates. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = invgfit (@var{x}, @var{alpha}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = invgfit (@var{x}, @var{alpha}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## @code{[@dots{}] = invgfit (@dots{}, @var{options})} specifies control ## parameters for the iterative algorithm used to compute ML estimates with the ## @code{fminsearch} function. @var{options} is a structure with the following ## fields and their default values: ## @itemize ## @item @qcode{@var{options}.Display = "off"} ## @item @qcode{@var{options}.MaxFunEvals = 400} ## @item @qcode{@var{options}.MaxIter = 200} ## @item @qcode{@var{options}.TolX = 1e-6} ## @end itemize ## ## Further information about the inverse Gaussian distribution can be found at ## @url{https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution} ## ## @seealso{invgcdf, invginv, invgpdf, invgrnd, invglike, invgstat} ## @end deftypefn function [paramhat, paramci] = invgfit (x, alpha, censor, freq, options) ## Check input arguments if (! isvector (x)) error ("invgfit: X must be a vector."); elseif (any (x <= 0)) error ("invgfit: X must contain only positive values."); endif ## Check alpha if (nargin < 2 || isempty (alpha)) alpha = 0.05; else if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("invgfit: wrong value for ALPHA."); endif endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("invgfit: X and CENSOR vectors mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("invgfit: X and FREQ vectors mismatch."); endif ## Get options structure or add defaults if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("invgfit: 'options' 5th argument must be a", ... " structure with 'Display', 'MaxFunEvals',", ... " 'MaxIter', and 'TolX' fields present.")); endif endif n_censored = sum (freq .* censor); ## Compute parameters for uncensored data if (n_censored == 0) ## Expand frequency vector (MATLAB does not do this, in R2018 at least) xf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; endfor xbar = mean (xf); paramhat = [xbar, (1 ./ mean (1 ./ x - 1 ./ xbar))]; else ## Use MLEs of the uncensored data as initial searching values x_uncensored = x(censor == 0); mu0 = mean (x_uncensored); lambda0 = 1 ./ mean (1 ./ x_uncensored - 1 ./ mu0); x0 = [mu0, lambda0]; ## Minimize negative log-likelihood to estimate parameters f = @(params) invglike (params, x, censor, freq); [paramhat, ~, err, output] = fminsearch (f, x0, options); ## Force positive parameter values paramhat = abs (paramhat); ## Handle errors if (err == 0) if (output.funcCount >= options.MaxFunEvals) msg = 'invgfit: maximum number of function evaluations are exceeded.'; warning (msg); elseif (output.iterations >= options.MaxIter) warning ("invgfit: maximum number of iterations are exceeded."); endif elseif (err < 0) error ("invgfit: no solution."); endif endif ## Compute CIs using a log normal approximation for parameters. if (nargout > 1) ## Compute asymptotic covariance [~, acov] = invglike (paramhat, x, censor, freq); ## Get standard errors stderr = sqrt (diag (acov))'; ## Get normal quantiles probs = [alpha/2; 1-alpha/2]; ## Compute CI paramci = norminv ([probs, probs], [paramhat; paramhat], [stderr; stderr]); endif endfunction %!demo %! ## Sample 3 populations from different inverse Gaussian distributions %! rng (42); %! r1 = invgrnd (1, 0.2, 2000, 1); %! r2 = invgrnd (1, 3, 2000, 1); %! r3 = invgrnd (3, 1, 2000, 1); %! r = [r1, r2, r3]; %! %! ## Plot them normalized and fix their colors %! hist (r, [0.1:0.1:3.2], 9); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! set (h(3), 'facecolor', 'r'); %! ylim ([0, 3]); %! xlim ([0, 3]); %! hold on %! %! ## Estimate their MU and LAMBDA parameters %! mu_lambdaA = invgfit (r(:,1)); %! mu_lambdaB = invgfit (r(:,2)); %! mu_lambdaC = invgfit (r(:,3)); %! %! ## Plot their estimated PDFs %! x = [0:0.1:3]; %! y = invgpdf (x, mu_lambdaA(1), mu_lambdaA(2)); %! plot (x, y, '-pr'); %! y = invgpdf (x, mu_lambdaB(1), mu_lambdaB(2)); %! plot (x, y, '-sg'); %! y = invgpdf (x, mu_lambdaC(1), mu_lambdaC(2)); %! plot (x, y, '-^c'); %! hold off %! legend ({'Normalized HIST of sample 1 with μ=1 and λ=0.5', ... %! 'Normalized HIST of sample 2 with μ=2 and λ=0.3', ... %! 'Normalized HIST of sample 3 with μ=4 and λ=0.5', ... %! sprintf("PDF for sample 1 with estimated μ=%0.2f and λ=%0.2f", ... %! mu_lambdaA(1), mu_lambdaA(2)), ... %! sprintf("PDF for sample 2 with estimated μ=%0.2f and λ=%0.2f", ... %! mu_lambdaB(1), mu_lambdaB(2)), ... %! sprintf("PDF for sample 3 with estimated μ=%0.2f and λ=%0.2f", ... %! mu_lambdaC(1), mu_lambdaC(2))}) %! title ('Three population samples from different inverse Gaussian distributions') %! hold off ## Test output %!test %! paramhat = invgfit ([1:50]); %! paramhat_out = [25.5, 19.6973]; %! assert_equal (paramhat, paramhat_out, 1e-4); %!test %! paramhat = invgfit ([1:5]); %! paramhat_out = [3, 8.1081]; %! assert_equal (paramhat, paramhat_out, 1e-4); ## Test input validation %!error invgfit (ones (2,5)); %!error invgfit ([-1 2 3 4]); %!error invgfit ([1, 2, 3, 4, 5], 1.2); %!error invgfit ([1, 2, 3, 4, 5], 0); %!error invgfit ([1, 2, 3, 4, 5], 'alpha'); %!error ... %! invgfit ([1, 2, 3, 4, 5], 0.05, [1 1 0]); %!error ... %! invgfit ([1, 2, 3, 4, 5], [], [1 1 0 1 1]'); %!error ... %! invgfit ([1, 2, 3, 4, 5], 0.05, zeros (1,5), [1 1 0]); %!error ... %! invgfit ([1, 2, 3, 4, 5], [], [], [1 1 0 1 1]'); %!error ... %! invgfit ([1, 2, 3, 4, 5], 0.05, [], [], 2); statistics-release-2.1.2/inst/Distribution_Fitting/invglike.m000066400000000000000000000163271527311104300244400ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} invglike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} invglike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} invglike (@var{params}, @var{x}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} invglike (@var{params}, @var{x}, @var{censor}, @var{freq}) ## ## Negative log-likelihood for the inverse Gaussian distribution. ## ## @code{@var{nlogL} = invglike (@var{params}, @var{x})} returns the negative ## log likelihood of the data in @var{x} corresponding to the inverse Gaussian ## distribution with (1) scale parameter @var{mu} and (2) shape parameter ## @var{lambda} given in the two-element vector @var{params}. ## ## @code{[@var{nlogL}, @var{acov}] = invglike (@var{params}, @var{x})} also ## returns the inverse of Fisher's information matrix, @var{acov}. If the input ## parameter values in @var{params} are the maximum likelihood estimates, the ## diagonal elements of @var{params} are their asymptotic variances. ## ## @code{[@dots{}] = invglike (@var{params}, @var{x}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = invglike (@var{params}, @var{x}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## Further information about the inverse Gaussian distribution can be found at ## @url{https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution} ## ## @seealso{invgcdf, invginv, invgpdf, invgrnd, invgfit, invgstat} ## @end deftypefn function [nlogL, acov] = invglike (params, x, censor, freq) ## Check input arguments if (nargin < 2) error ("invglike: function called with too few input arguments."); endif if (! isvector (x)) error ("invglike: X must be a vector."); endif if (any (x < 0)) error ("invglike: X must have positive values."); endif if (length (params) != 2) error ("invglike: PARAMS must be a two-element vector."); endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("invglike: X and CENSOR vector mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("invglike: X and FREQ vector mismatch."); endif ## Get parameters mu = params(1); lambda = params(2); L = 0.5 .* log (lambda ./ (2 * pi)) - 1.5 .* log (x) ... -lambda .* (x ./ mu - 1) .^ 2 ./ (2 .* x); n_censored = sum (freq .* censor); ## Handle censored data if (n_censored > 0) censored = (censor == 1); x_censored = x(censored); sqrt_lx = sqrt (lambda ./ x_censored); z_censored = -(x_censored ./ mu - 1) .* sqrt_lx; w_censored = -(x_censored ./ mu + 1) .* sqrt_lx; Fz = 0.5 .* erfc (-z_censored ./ sqrt (2)); Fw = 0.5 .* erfc (-w_censored ./ sqrt (2)); S_censored = Fz - exp (2 .* lambda ./ mu) .* Fw; L(censored) = log (S_censored); endif ## Sum up the neg log likelihood nlogL = -sum (freq .* L); ## Compute asymptotic covariance if (nargout > 1) ## Compute first order central differences of the log-likelihood gradient dp = 0.0001 .* max (abs (params), 1); ngrad_p1 = invg_grad (params + [dp(1), 0], x, censor, freq); ngrad_m1 = invg_grad (params - [dp(1), 0], x, censor, freq); ngrad_p2 = invg_grad (params + [0, dp(2)], x, censor, freq); ngrad_m2 = invg_grad (params - [0, dp(2)], x, censor, freq); ## Compute negative Hessian by normalizing the differences by the increment nH = [(ngrad_p1(:) - ngrad_m1(:))./(2 * dp(1)), ... (ngrad_p2(:) - ngrad_m2(:))./(2 * dp(2))]; ## Force neg Hessian being symmetric nH = 0.5 .* (nH + nH'); ## Check neg Hessian is positive definite [R, p] = chol (nH); if (p > 0) warning ("invglike: non positive definite Hessian matrix."); acov = NaN (2); return endif ## ACOV estimate is the negative inverse of the Hessian. Rinv = inv (R); acov = Rinv * Rinv; endif endfunction ## Helper function for computing negative gradient function ngrad = invg_grad (params, x, censor, freq) mu = params(1); lambda = params(2); dL1 = lambda .* (x - mu) ./ mu .^ 3; dL2 = 1 ./ (2 .* lambda) - (x ./ mu - 1) .^ 2 ./ (2 .* x); n_censored = sum (freq .* censor); if (n_censored > 0) censored = (censor == 1); x_censored = x(censored); sqrt_lx = sqrt (lambda ./ x_censored); exp_lmu = exp (2 .* lambda ./ mu); z_censored = -(x_censored ./ mu - 1) .* sqrt_lx; w_censored = -(x_censored ./ mu + 1) .* sqrt_lx; Fw = 0.5 .* erfc (-w_censored ./ sqrt (2)); fz = exp (-0.5 .* z_censored .^ 2) ./ sqrt (2 .* pi); fw = exp (-0.5 .* w_censored .^ 2) ./ sqrt (2 .* pi); dS1cen = (fz - exp_lmu .* fw) .* (x_censored ./ mu .^ 2) .* sqrt_lx ... + 2 .* Fw .* exp_lmu .* lambda ./ mu .^ 2; dS2cen = 0.5 .* (fz .* z_censored - exp_lmu .* fw .* w_censored) ... ./ lambda - 2 .* Fw .* exp_lmu ./ mu; dL1(censored) = dS1cen ./ Scen; dL2(censored) = dS2cen ./ Scen; endif ngrad = -[sum(freq .* dL1), sum(freq .* dL2)]; endfunction ## Test results %!test %! nlogL = invglike ([25.5, 19.6973], [1:50]); %! assert_equal (nlogL, 219.1516, 1e-4); %!test %! nlogL = invglike ([3, 8.1081], [1:5]); %! assert_equal (nlogL, 9.0438, 1e-4); ## Test input validation %!error invglike (3.25) %!error invglike ([5, 0.2], ones (2)) %!error invglike ([5, 0.2], [-1, 3]) %!error ... %! invglike ([1, 0.2, 3], [1, 3, 5, 7]) %!error ... %! invglike ([1.5, 0.2], [1:5], [0, 0, 0]) %!error ... %! invglike ([1.5, 0.2], [1:5], [0, 0, 0, 0, 0], [1, 1, 1]) %!error ... %! invglike ([1.5, 0.2], [1:5], [], [1, 1, 1]) statistics-release-2.1.2/inst/Distribution_Fitting/logifit.m000066400000000000000000000225311527311104300242570ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{paramhat} =} logifit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} logifit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} logifit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@dots{}] =} logifit (@var{x}, @var{alpha}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} logifit (@var{x}, @var{alpha}, @var{censor}, @var{freq}) ## @deftypefnx {statistics} {[@dots{}] =} logifit (@var{x}, @var{alpha}, @var{censor}, @var{freq}, @var{options}) ## ## Estimate mean and confidence intervals for the logistic distribution. ## ## @code{@var{mu0} = logifit (@var{x})} returns the maximum likelihood ## estimates of the parameters of the logistic distribution given the data in ## @var{x}. @qcode{@var{paramhat}(1)} is the scale parameter, @var{mu}, and ## @qcode{@var{paramhat}(2)} is the shape parameter, @var{s}. ## ## @code{[@var{paramhat}, @var{paramci}] = logifit (@var{x})} returns the 95% ## confidence intervals for the parameter estimates. ## ## @code{[@dots{}] = logifit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals for the ## parameter estimates. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = logifit (@var{x}, @var{alpha}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = logifit (@var{x}, @var{alpha}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## @code{[@dots{}] = logifit (@dots{}, @var{options})} specifies control ## parameters for the iterative algorithm used to compute ML estimates with the ## @code{fminsearch} function. @var{options} is a structure with the following ## fields and their default values: ## @itemize ## @item @qcode{@var{options}.Display = "off"} ## @item @qcode{@var{options}.MaxFunEvals = 400} ## @item @qcode{@var{options}.MaxIter = 200} ## @item @qcode{@var{options}.TolX = 1e-6} ## @end itemize ## ## Further information about the logistic distribution can be found at ## @url{https://en.wikipedia.org/wiki/Logistic_distribution} ## ## @seealso{logicdf, logiinv, logipdf, logirnd, logilike, logistat} ## @end deftypefn function [paramhat, paramci] = logifit (x, alpha, censor, freq, options) ## Check input arguments if (! isvector (x)) error ("logifit: X must be a vector."); endif ## Check alpha if (nargin < 2 || isempty (alpha)) alpha = 0.05; else if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("logifit: wrong value for ALPHA."); endif endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("logifit: X and CENSOR vectors mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("logifit: X and FREQ vectors mismatch."); endif ## Get options structure or add defaults if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("logifit: 'options' 5th argument must be a", ... " structure with 'Display', 'MaxFunEvals',", ... " 'MaxIter', and 'TolX' fields present.")); endif endif ## Expand frequency and censor vectors (if necessary) if (! all (freq == 1)) xf = []; cf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; cf = [cf, repmat(censor(i), 1, freq(i))]; endfor x = xf; freq = ones (size (x)); censor = cf; endif ## Use MLEs of the uncensored data as initial searching values x_uncensored = x(censor == 0); mu0 = mean (x_uncensored); s0 = std (x_uncensored) .* sqrt (3) ./ pi; x0 = [mu0, s0]; ## Minimize negative log-likelihood to estimate parameters f = @(params) logilike (params, x, censor, freq); [paramhat, ~, err, output] = fminsearch (f, x0, options); ## Handle errors if (err == 0) if (output.funcCount >= options.MaxFunEvals) msg = 'logifit: maximum number of function evaluations are exceeded.'; warning (msg); elseif (output.iterations >= options.MaxIter) warning ("logifit: maximum number of iterations are exceeded."); endif elseif (err < 0) error ("logifit: no solution."); endif ## Compute CIs using a log normal approximation for parameters. if (nargout > 1) ## Compute asymptotic covariance [~, acov] = logilike (paramhat, x, censor, freq); ## Get standard errors se = sqrt (diag (acov))'; ## Get normal quantiles probs = [alpha/2; 1-alpha/2]; ## Compute muci using a normal approximation paramci(:,1) = norminv (probs, paramhat(1), se(1)); ## Compute sci using a normal approximation for log (s) and transform back paramci(:,2) = exp (norminv (probs, log (paramhat(2)), se(2) / paramhat(2))); endif endfunction %!demo %! ## Sample 3 populations from different logistic distributions %! rng (42); %! r1 = logirnd (2, 1, 2000, 1); %! r2 = logirnd (5, 2, 2000, 1); %! r3 = logirnd (9, 4, 2000, 1); %! r = [r1, r2, r3]; %! %! ## Plot them normalized and fix their colors %! hist (r, [-6:20], 1); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! set (h(3), 'facecolor', 'r'); %! ylim ([0, 0.3]); %! xlim ([-5, 20]); %! hold on %! %! ## Estimate their MU and LAMBDA parameters %! mu_sA = logifit (r(:,1)); %! mu_sB = logifit (r(:,2)); %! mu_sC = logifit (r(:,3)); %! %! ## Plot their estimated PDFs %! x = [-5:0.5:20]; %! y = logipdf (x, mu_sA(1), mu_sA(2)); %! plot (x, y, '-pr'); %! y = logipdf (x, mu_sB(1), mu_sB(2)); %! plot (x, y, '-sg'); %! y = logipdf (x, mu_sC(1), mu_sC(2)); %! plot (x, y, '-^c'); %! hold off %! legend ({'Normalized HIST of sample 1 with μ=1 and s=0.5', ... %! 'Normalized HIST of sample 2 with μ=2 and s=0.3', ... %! 'Normalized HIST of sample 3 with μ=4 and s=0.5', ... %! sprintf("PDF for sample 1 with estimated μ=%0.2f and s=%0.2f", ... %! mu_sA(1), mu_sA(2)), ... %! sprintf("PDF for sample 2 with estimated μ=%0.2f and s=%0.2f", ... %! mu_sB(1), mu_sB(2)), ... %! sprintf("PDF for sample 3 with estimated μ=%0.2f and s=%0.2f", ... %! mu_sC(1), mu_sC(2))}) %! title ('Three population samples from different logistic distributions') %! hold off ## Test output %!test %! paramhat = logifit ([1:50]); %! paramhat_out = [25.5, 8.7724]; %! assert_equal (paramhat, paramhat_out, 1e-4); %!test %! paramhat = logifit ([1:5]); %! paramhat_out = [3, 0.8645]; %! assert_equal (paramhat, paramhat_out, 1e-4); %!test %! paramhat = logifit ([1:6], [], [], [1 1 1 1 1 0]); %! paramhat_out = [3, 0.8645]; %! assert_equal (paramhat, paramhat_out, 1e-4); %!test %! paramhat = logifit ([1:5], [], [], [1 1 1 1 2]); %! paramhat_out = logifit ([1:5, 5]); %! assert_equal (paramhat, paramhat_out, 1e-4); ## Test input validation %!error logifit (ones (2,5)); %!error logifit ([1, 2, 3, 4, 5], 1.2); %!error logifit ([1, 2, 3, 4, 5], 0); %!error logifit ([1, 2, 3, 4, 5], 'alpha'); %!error ... %! logifit ([1, 2, 3, 4, 5], 0.05, [1 1 0]); %!error ... %! logifit ([1, 2, 3, 4, 5], [], [1 1 0 1 1]'); %!error ... %! logifit ([1, 2, 3, 4, 5], 0.05, zeros (1,5), [1 1 0]); %!error ... %! logifit ([1, 2, 3, 4, 5], [], [], [1 1 0 1 1]'); %!error ... %! logifit ([1, 2, 3, 4, 5], 0.05, [], [], 2); statistics-release-2.1.2/inst/Distribution_Fitting/logilike.m000066400000000000000000000153431527311104300244240ustar00rootroot00000000000000## Copyright (C) 2023-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} logilike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} logilike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} logilike (@var{params}, @var{x}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} logilike (@var{params}, @var{x}, @var{censor}, @var{freq}) ## ## Negative log-likelihood for the logistic distribution. ## ## @code{@var{nlogL} = logilike (@var{params}, @var{x})} returns the negative ## log likelihood of the data in @var{x} corresponding to the logistic ## distribution with (1) location parameter @var{mu} and (2) scale parameter ## @var{sigma} given in the two-element vector @var{params}. ## ## @code{[@var{nlogL}, @var{acov}] = logilike (@var{params}, @var{x})} also ## returns the inverse of Fisher's information matrix, @var{acov}. If the input ## parameter values in @var{params} are the maximum likelihood estimates, the ## diagonal elements of @var{params} are their asymptotic variances. ## ## @code{[@dots{}] = logilike (@var{params}, @var{x}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = logilike (@var{params}, @var{x}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## Further information about the logistic distribution can be found at ## @url{https://en.wikipedia.org/wiki/Logistic_distribution} ## ## @seealso{logicdf, logiinv, logipdf, logirnd, logifit, logistat} ## @end deftypefn function [nlogL, acov] = logilike (params, x, censor, freq) ## Check input arguments if (nargin < 2) error ("logilike: function called with too few input arguments."); endif if (! isvector (x)) error ("logilike: X must be a vector."); endif if (length (params) != 2) error ("logilike: PARAMS must be a two-element vector."); endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("logilike: X and CENSOR vector mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("logilike: X and FREQ vector mismatch."); endif ## Expand frequency and censor vectors (if necessary) if (! all (freq == 1)) xf = []; cf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; cf = [cf, repmat(censor(i), 1, freq(i))]; endfor x = xf; freq = ones (size (x)); censor = cf; endif ## Compute the negative loglikelihood nlogL = loginll (params, x, censor, freq); ## Compute the negative hessian and invert to get the information matrix if (nargout > 1) ei = zeros (1, 2); ej = zeros (1, 2); nH = zeros (2, 2); dp = (eps ^ (1/4)) .* max (abs (params), 1); for i = 1:2 ei(i) = dp(i); for j = 1:(i-1) ej(j) = dp(j); ## Four-point central difference for mixed second partials nH(i,j) = loginll (params+ei+ej, x, censor, freq) ... - loginll (params+ei-ej, x, censor, freq) ... - loginll (params-ei+ej, x, censor, freq) ... + loginll (params-ei-ej, x, censor, freq); ej(j) = 0; endfor ## Five-point central difference for pure second partial nH(i,i) = - loginll (params+2*ei, x, censor, freq) ... + 16 * loginll (params+ei, x, censor, freq) - 30 * nlogL ... + 16 * loginll (params-ei, x, censor, freq) ... - loginll (params-2*ei, x, censor, freq); ei(i) = 0; endfor ## Fill in the upper triangle nH = nH + triu (nH', 1); ## Normalize the second differences to get derivative estimates nH = nH ./ (4 .* dp(:) * dp(:)' + diag (8 * dp(:) .^ 2)); ## Check neg Hessian is positive definite [R, p] = chol (nH); if (p > 0) warning ("logilike: non positive definite Hessian matrix."); acov = NaN (2); return endif ## ACOV estimate is the negative inverse of the Hessian Rinv = inv (R); acov = Rinv * Rinv'; endif endfunction ## Helper function for computing negative loglikelihood function nlogL = loginll (params, x, censor, freq) ## Get parameters mu = params(1); sigma = params(2); ## Compute intermediate values z = (x - mu) ./ sigma; logclogitz = log (1 ./ (1 + exp (z))); k = (z > 700); if (any (k)) logclogitz(k) = z(k); endif L = z + 2 .* logclogitz - log (sigma); n_censored = sum (freq .* censor); ## Handle censored data if (n_censored > 0) censored = (censor == 1); L(censored) = logclogitz(censored); endif ## Sum up the neg log likelihood if (sigma < 0) nlogL = Inf; else nlogL = -sum (freq .* L); endif endfunction ## Test results %!test %! nlogL = logilike ([25.5, 8.7725], [1:50]); %! assert_equal (nlogL, 206.6769, 1e-4); %!test %! nlogL = logilike ([3, 0.8645], [1:5]); %! assert_equal (nlogL, 9.0699, 1e-4); ## Test input validation %!error logilike (3.25) %!error logilike ([5, 0.2], ones (2)) %!error ... %! logilike ([1, 0.2, 3], [1, 3, 5, 7]) %!error ... %! logilike ([1.5, 0.2], [1:5], [0, 0, 0]) %!error ... %! logilike ([1.5, 0.2], [1:5], [0, 0, 0, 0, 0], [1, 1, 1]) %!error ... %! logilike ([1.5, 0.2], [1:5], [], [1, 1, 1]) statistics-release-2.1.2/inst/Distribution_Fitting/loglfit.m000066400000000000000000000237031527311104300242640ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{paramhat} =} loglfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} loglfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} loglfit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@dots{}] =} loglfit (@var{x}, @var{alpha}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} loglfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}) ## @deftypefnx {statistics} {[@dots{}] =} loglfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}, @var{options}) ## ## Estimate mean and confidence intervals for the log-logistic distribution. ## ## @code{@var{mu0} = loglfit (@var{x})} returns the maximum likelihood ## estimates of the parameters of the log-logistic distribution given the data ## in @var{x}. @qcode{@var{paramhat}(1)} is the mean parameter, @var{mu}, and ## @qcode{@var{paramhat}(2)} is the scale parameter, @var{sigma}. ## ## @code{[@var{paramhat}, @var{paramci}] = loglfit (@var{x})} returns the 95% ## confidence intervals for the parameter estimates. ## ## @code{[@dots{}] = loglfit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals for the ## parameter estimates. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = loglfit (@var{x}, @var{alpha}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = loglfit (@var{x}, @var{alpha}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## @code{[@dots{}] = loglfit (@dots{}, @var{options})} specifies control ## parameters for the iterative algorithm used to compute ML estimates with the ## @code{fminsearch} function. @var{options} is a structure with the following ## fields and their default values: ## @itemize ## @item @qcode{@var{options}.Display = "off"} ## @item @qcode{@var{options}.MaxFunEvals = 400} ## @item @qcode{@var{options}.MaxIter = 200} ## @item @qcode{@var{options}.TolX = 1e-6} ## @end itemize ## ## Further information about the loglogistic distribution can be found at ## @url{https://en.wikipedia.org/wiki/Log-logistic_distribution} ## ## OCTAVE/MATLAB use an alternative parameterization given by the pair ## @math{μ, σ}, i.e. @var{mu} and @var{sigma}, in analogy with the logistic ## distribution. Their relation to the @math{α} and @math{b} parameters used ## in Wikipedia are given below: ## ## @itemize ## @item @qcode{@var{mu} = log (@var{a})} ## @item @qcode{@var{sigma} = 1 / @var{a}} ## @end itemize ## ## @seealso{loglcdf, loglinv, loglpdf, loglrnd, logllike, loglstat} ## @end deftypefn function [paramhat, paramci] = loglfit (x, alpha, censor, freq, options) ## Check input arguments if (! isvector (x)) error ("loglfit: X must be a vector."); endif ## Check alpha if (nargin < 2 || isempty (alpha)) alpha = 0.05; else if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("loglfit: wrong value for ALPHA."); endif endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("loglfit: X and CENSOR vectors mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("loglfit: X and FREQ vectors mismatch."); endif ## Get options structure or add defaults if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("loglfit: 'options' 5th argument must be a", ... " structure with 'Display', 'MaxFunEvals',", ... " 'MaxIter', and 'TolX' fields present.")); endif endif ## Expand frequency and censor vectors (if necessary) if (! all (freq == 1)) xf = []; cf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; cf = [cf, repmat(censor(i), 1, freq(i))]; endfor x = xf; freq = ones (size (x)); censor = cf; endif ## Use MLEs of the uncensored data as initial searching values logx_uncensored = log (x(censor == 0)); a0 = mean (logx_uncensored); b0 = 1 ./ (std (logx_uncensored) .* sqrt (3) ./ pi); x0 = [a0, b0]; ## Minimize negative log-likelihood to estimate parameters f = @(params) logllike (params, x, censor, freq); [paramhat, ~, err, output] = fminsearch (f, x0, options); ## Force positive parameter values paramhat = abs (paramhat); ## Handle errors if (err == 0) if (output.funcCount >= options.MaxFunEvals) msg = 'loglfit: maximum number of function evaluations are exceeded.'; warning (msg); elseif (output.iterations >= options.MaxIter) warning ("loglfit: maximum number of iterations are exceeded."); endif elseif (err < 0) error ("loglfit: no solution."); endif ## Compute CIs using a log normal approximation for parameters. if (nargout > 1) ## Compute asymptotic covariance [~, acov] = logllike (paramhat, x, censor, freq); ## Get standard errors se = sqrt (diag (acov))'; ## Get normal quantiles probs = [alpha/2; 1-alpha/2]; ## Compute muci using a normal approximation paramci(:,1) = norminv (probs, paramhat(1), se(1)); ## Compute sci using a normal approximation for log (s) and transform back paramci(:,2) = exp (norminv (probs, log (paramhat(2)), se(2)/paramhat(2))); endif endfunction %!demo %! ## Sample 3 populations from different log-logistic distributions %! rng (42); %! r1 = loglrnd (0, 1, 2000, 1); %! r2 = loglrnd (0, 0.5, 2000, 1); %! r3 = loglrnd (0, 0.125, 2000, 1); %! r = [r1, r2, r3]; %! %! ## Plot them normalized and fix their colors %! hist (r, [0.05:0.1:2.5], 10); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! set (h(3), 'facecolor', 'r'); %! ylim ([0, 3.5]); %! xlim ([0, 2.0]); %! hold on %! %! ## Estimate their MU and LAMBDA parameters %! a_bA = loglfit (r(:,1)); %! a_bB = loglfit (r(:,2)); %! a_bC = loglfit (r(:,3)); %! %! ## Plot their estimated PDFs %! x = [0.01:0.1:2.01]; %! y = loglpdf (x, a_bA(1), a_bA(2)); %! plot (x, y, '-pr'); %! y = loglpdf (x, a_bB(1), a_bB(2)); %! plot (x, y, '-sg'); %! y = loglpdf (x, a_bC(1), a_bC(2)); %! plot (x, y, '-^c'); %! legend ({'Normalized HIST of sample 1 with α=1 and β=1', ... %! 'Normalized HIST of sample 2 with α=1 and β=2', ... %! 'Normalized HIST of sample 3 with α=1 and β=8', ... %! sprintf("PDF for sample 1 with estimated α=%0.2f and β=%0.2f", ... %! a_bA(1), a_bA(2)), ... %! sprintf("PDF for sample 2 with estimated α=%0.2f and β=%0.2f", ... %! a_bB(1), a_bB(2)), ... %! sprintf("PDF for sample 3 with estimated α=%0.2f and β=%0.2f", ... %! a_bC(1), a_bC(2))}) %! title ('Three population samples from different log-logistic distributions') %! hold off ## Test output %!test %! [paramhat, paramci] = loglfit ([1:50]); %! paramhat_out = [3.09717, 0.468525]; %! paramci_out = [2.87261, 0.370616; 3.32174, 0.5923]; %! assert_equal (paramhat, paramhat_out, 1e-5); %! assert_equal (paramci, paramci_out, 1e-5); %!test %! paramhat = loglfit ([1:5]); %! paramhat_out = [1.01124, 0.336449]; %! assert_equal (paramhat, paramhat_out, 1e-5); %!test %! paramhat = loglfit ([1:6], [], [], [1 1 1 1 1 0]); %! paramhat_out = [1.01124, 0.336449]; %! assert_equal (paramhat, paramhat_out, 1e-4); %!test %! paramhat = loglfit ([1:5], [], [], [1 1 1 1 2]); %! paramhat_out = loglfit ([1:5, 5]); %! assert_equal (paramhat, paramhat_out, 1e-4); ## Test input validation %!error loglfit (ones (2,5)); %!error loglfit ([1, 2, 3, 4, 5], 1.2); %!error loglfit ([1, 2, 3, 4, 5], 0); %!error loglfit ([1, 2, 3, 4, 5], 'alpha'); %!error ... %! loglfit ([1, 2, 3, 4, 5], 0.05, [1 1 0]); %!error ... %! loglfit ([1, 2, 3, 4, 5], [], [1 1 0 1 1]'); %!error ... %! loglfit ([1, 2, 3, 4, 5], 0.05, zeros (1,5), [1 1 0]); %!error ... %! loglfit ([1, 2, 3, 4, 5], [], [], [1 1 0 1 1]'); %!error ... %! loglfit ([1, 2, 3, 4, 5], 0.05, [], [], 2); statistics-release-2.1.2/inst/Distribution_Fitting/logllike.m000066400000000000000000000163351527311104300244310ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} logllike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} logllike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} logllike (@var{params}, @var{x}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} logllike (@var{params}, @var{x}, @var{censor}, @var{freq}) ## ## Negative log-likelihood for the log-logistic distribution. ## ## @code{@var{nlogL} = logllike (@var{params}, @var{x})} returns the negative ## log likelihood of the data in @var{x} corresponding to the log-logistic ## distribution with (1) scale parameter @var{a} and (2) shape parameter @var{b} ## given in the two-element vector @var{params}. ## ## @code{[@var{nlogL}, @var{acov}] = logllike (@var{params}, @var{x})} also ## returns the inverse of Fisher's information matrix, @var{acov}. If the input ## parameter values in @var{params} are the maximum likelihood estimates, the ## diagonal elements of @var{params} are their asymptotic variances. ## ## @code{[@dots{}] = logllike (@var{params}, @var{x}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = logllike (@var{params}, @var{x}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## Further information about the loglogistic distribution can be found at ## @url{https://en.wikipedia.org/wiki/Log-logistic_distribution} ## ## OCTAVE/MATLAB use an alternative parameterization given by the pair ## @math{μ, σ}, i.e. @var{mu} and @var{sigma}, in analogy with the logistic ## distribution. Their relation to the @math{α} and @math{b} parameters used ## in Wikipedia are given below: ## ## @itemize ## @item @qcode{@var{mu} = log (@var{a})} ## @item @qcode{@var{sigma} = 1 / @var{a}} ## @end itemize ## ## @seealso{loglcdf, loglinv, loglpdf, loglrnd, loglfit, loglstat} ## @end deftypefn function [nlogL, acov] = logllike (params, x, censor, freq) ## Check input arguments if (nargin < 2) error ("logllike: function called with too few input arguments."); endif if (! isvector (x)) error ("logllike: X must be a vector."); endif if (length (params) != 2) error ("logllike: PARAMS must be a two-element vector."); endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("logllike: X and CENSOR vector mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("logllike: X and FREQ vector mismatch."); endif ## Expand frequency and censor vectors (if necessary) if (! all (freq == 1)) xf = []; cf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; cf = [cf, repmat(censor(i), 1, freq(i))]; endfor x = xf; freq = ones (size (x)); censor = cf; endif ## Compute the negative loglikelihood nlogL = loglnll (params, x, censor, freq); ## Compute the negative hessian and invert to get the information matrix if (nargout > 1) ei = zeros (1, 2); ej = zeros (1, 2); nH = zeros (2, 2); dp = (eps ^ (1/4)) .* max (abs (params), 1); for i = 1:2 ei(i) = dp(i); for j = 1:(i-1) ej(j) = dp(j); ## Four-point central difference for mixed second partials nH(i,j) = loglnll (params+ei+ej, x, censor, freq) ... - loglnll (params+ei-ej, x, censor, freq) ... - loglnll (params-ei+ej, x, censor, freq) ... + loglnll (params-ei-ej, x, censor, freq); ej(j) = 0; endfor ## Five-point central difference for pure second partial nH(i,i) = - loglnll (params+2*ei, x, censor, freq) ... + 16 * loglnll (params+ei, x, censor, freq) - 30 * nlogL ... + 16 * loglnll (params-ei, x, censor, freq) ... - loglnll (params-2*ei, x, censor, freq); ei(i) = 0; endfor ## Fill in the upper triangle nH = nH + triu (nH', 1); ## Normalize the second differences to get derivative estimates nH = nH ./ (4 .* dp(:) * dp(:)' + diag (8 * dp(:) .^ 2)); ## Check neg Hessian is positive definite [R, p] = chol (nH); if (p > 0) warning ("logllike: non positive definite Hessian matrix."); acov = NaN (2); return endif ## ACOV estimate is the negative inverse of the Hessian Rinv = inv (R); acov = Rinv * Rinv'; endif endfunction ## Helper function for computing negative loglikelihood function nlogL = loglnll (params, x, censor, freq) ## Get parameters mu = params(1); sigma = params(2); ## Compute intermediate values z = (log (x) - mu) ./ sigma; logclogitz = log (1 ./ (1 + exp (z))); k = (z > 708); if (any (k)) logclogitz(k) = z(k); endif L = z + 2 .* logclogitz - log (sigma) - log (x); n_censored = sum (freq .* censor); ## Handle censored data if (n_censored > 0) censored = (censor == 1); L(censored) = logclogitz(censored); endif ## Sum up the neg log likelihood nlogL = -sum (freq .* L); endfunction ## Test output %!test %! [nlogL, acov] = logllike ([3.09717, 0.468525], [1:50]); %! assert_equal (nlogL, 211.2965, 1e-4); %! assert_equal (acov, [0.0131, -0.0007; -0.0007, 0.0031], 1e-4); %!test %! [nlogL, acov] = logllike ([1.01124, 0.336449], [1:5]); %! assert_equal (nlogL, 9.2206, 1e-4); %! assert_equal (acov, [0.0712, -0.0032; -0.0032, 0.0153], 1e-4); ## Test input validation %!error logllike (3.25) %!error logllike ([5, 0.2], ones (2)) %!error ... %! logllike ([1, 0.2, 3], [1, 3, 5, 7]) %!error ... %! logllike ([1.5, 0.2], [1:5], [0, 0, 0]) %!error ... %! logllike ([1.5, 0.2], [1:5], [0, 0, 0, 0, 0], [1, 1, 1]) %!error ... %! logllike ([1.5, 0.2], [1:5], [], [1, 1, 1]) statistics-release-2.1.2/inst/Distribution_Fitting/lognfit.m000066400000000000000000000207721527311104300242710ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{paramhat} =} lognfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} lognfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} lognfit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@dots{}] =} lognfit (@var{x}, @var{alpha}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} lognfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}) ## @deftypefnx {statistics} {[@dots{}] =} lognfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}, @var{options}) ## ## Estimate parameters and confidence intervals for the lognormal distribution. ## ## @code{@var{paramhat} = lognfit (@var{x})} returns the maximum likelihood ## estimates of the parameters of the lognormal distribution given the data in ## vector @var{x}. @qcode{@var{paramhat}([1, 2])} corresponds to the mean and ## standard deviation, respectively, of the associated normal distribution. ## ## If a random variable follows this distribution, its logarithm is normally ## distributed with mean @var{mu} and standard deviation @var{sigma}. ## ## @code{[@var{paramhat}, @var{paramci}] = lognfit (@var{x})} returns the 95% ## confidence intervals for the parameter estimates. ## ## @code{[@dots{}] = lognfit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals for the ## parameter estimates. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = lognfit (@var{x}, @var{alpha}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = lognfit (@var{x}, @var{alpha}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## @code{[@dots{}] = lognfit (@dots{}, @var{options})} specifies control ## parameters for the iterative algorithm used to compute ML estimates with the ## @code{fminsearch} function. @var{options} is a structure with the following ## fields and their default values: ## @itemize ## @item @qcode{@var{options}.Display = "off"} ## @item @qcode{@var{options}.MaxFunEvals = 400} ## @item @qcode{@var{options}.MaxIter = 200} ## @item @qcode{@var{options}.TolX = 1e-6} ## @end itemize ## ## With no censor, the estimate of the standard deviation, ## @qcode{@var{paramhat}(2)}, is the square root of the unbiased estimate of the ## variance of @qcode{log (@var{x})}. With censored data, the maximum ## likelihood estimate is returned. ## ## Further information about the lognormal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Log-normal_distribution} ## ## @seealso{logncdf, logninv, lognpdf, lognrnd, lognlike, lognstat} ## @end deftypefn function [paramhat, paramci] = lognfit (x, alpha, censor, freq, options) ## Check X for valid data if (! isvector (x) || ! isnumeric (x) || any (x <= 0)) error ("lognfit: X must be a numeric vector of positive values."); endif ## Check alpha if (nargin < 2 || isempty (alpha)) alpha = 0.05; else if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("lognfit: wrong value for ALPHA."); endif endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = []; elseif (! isequal (size (x), size (censor))) error ("lognfit: X and CENSOR vectors mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = []; elseif (! isequal (size (x), size (freq))) error ("lognfit: X and FREQ vectors mismatch."); endif ## Check options structure or add defaults if (nargin > 4 && ! isempty (options)) if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("lognfit: 'options' 5th argument must be a", ... " structure with 'Display', 'MaxFunEvals',", ... " 'MaxIter', and 'TolX' fields present.")); endif else options = []; endif ## Fit a normal distribution to the logged data if (nargout <= 1) [muhat, sigmahat] = normfit (log (x), alpha, censor, freq, options); paramhat = [muhat, sigmahat]; else [muhat, sigmahat, muci, sigmaci] = normfit (log (x), alpha, ... censor, freq, options); paramhat = [muhat, sigmahat]; paramci = [muci, sigmaci]; endif endfunction %!demo %! ## Sample 3 populations from 3 different log-normal distributions %! rng (42); %! r1 = lognrnd (0, 0.25, 1000, 1); %! r2 = lognrnd (0, 0.5, 1000, 1); %! r3 = lognrnd (0, 1, 1000, 1); %! r = [r1, r2, r3]; %! %! ## Plot them normalized and fix their colors %! hist (r, 30, 2); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! set (h(3), 'facecolor', 'r'); %! hold on %! %! ## Estimate their mu and sigma parameters %! mu_sigmaA = lognfit (r(:,1)); %! mu_sigmaB = lognfit (r(:,2)); %! mu_sigmaC = lognfit (r(:,3)); %! %! ## Plot their estimated PDFs %! x = [0:0.1:6]; %! y = lognpdf (x, mu_sigmaA(1), mu_sigmaA(2)); %! plot (x, y, '-pr'); %! y = lognpdf (x, mu_sigmaB(1), mu_sigmaB(2)); %! plot (x, y, '-sg'); %! y = lognpdf (x, mu_sigmaC(1), mu_sigmaC(2)); %! plot (x, y, '-^c'); %! ylim ([0, 2]) %! xlim ([0, 6]) %! hold off %! legend ({'Normalized HIST of sample 1 with mu=0, σ=0.25', ... %! 'Normalized HIST of sample 2 with mu=0, σ=0.5', ... %! 'Normalized HIST of sample 3 with mu=0, σ=1', ... %! sprintf("PDF for sample 1 with estimated mu=%0.2f and σ=%0.2f", ... %! mu_sigmaA(1), mu_sigmaA(2)), ... %! sprintf("PDF for sample 2 with estimated mu=%0.2f and σ=%0.2f", ... %! mu_sigmaB(1), mu_sigmaB(2)), ... %! sprintf("PDF for sample 3 with estimated mu=%0.2f and σ=%0.2f", ... %! mu_sigmaC(1), mu_sigmaC(2))}, 'location', 'northeast') %! title ('Three population samples from different log-normal distributions') %! hold off ## Test output %!test %! randn ('seed', 1); %! x = lognrnd (3, 5, [1000, 1]); %! [paramhat, paramci] = lognfit (x, 0.01); %! assert_equal (paramci(1,1) < 3, true); %! assert_equal (paramci(1,2) > 3, true); %! assert_equal (paramci(2,1) < 5, true); %! assert_equal (paramci(2,2) > 5, true); ## Test input validation %!error ... %! lognfit (ones (20,3)) %!error ... %! lognfit ({1, 2, 3, 4, 5}) %!error ... %! lognfit ([-1, 2, 3, 4, 5]) %!error lognfit (ones (20,1), 0) %!error lognfit (ones (20,1), -0.3) %!error lognfit (ones (20,1), 1.2) %!error lognfit (ones (20,1), [0.05, 0.1]) %!error lognfit (ones (20,1), 0.02+i) %!error ... %! lognfit (ones (20,1), [], zeros (15,1)) %!error ... %! lognfit (ones (20,1), [], zeros (20,1), ones (25,1)) %!error lognfit (ones (20,1), [], zeros (20,1), ones (20,1), 'options') statistics-release-2.1.2/inst/Distribution_Fitting/lognlike.m000066400000000000000000000142231527311104300244250ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} lognlike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{avar}] =} lognlike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} lognlike (@var{params}, @var{x}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} lognlike (@var{params}, @var{x}, @var{censor}, @var{freq}) ## ## Negative log-likelihood for the lognormal distribution. ## ## @code{@var{nlogL} = lognlike (@var{params}, @var{x})} returns the negative ## log-likelihood of the data in @var{x} corresponding to the lognormal ## distribution with (1) location parameter @var{mu} and (2) scale parameter ## @var{sigma} given in the two-element vector @var{params}, which correspond to ## the mean and standard deviation of the associated normal distribution. ## Missing values, @qcode{NaNs}, are ignored. Negative values of @var{x} are ## treated as missing values. ## ## If a random variable follows this distribution, its logarithm is normally ## distributed with mean @var{mu} and standard deviation @var{sigma}. ## ## @code{[@var{nlogL}, @var{avar}] = lognlike (@var{params}, @var{x})} ## returns the inverse of Fisher's information matrix, @var{avar}. If the input ## parameter values in @var{params} are the maximum likelihood estimates, the ## diagonal elements of @var{avar} are their asymptotic variances. @var{avar} ## is based on the observed Fisher's information, not the expected information. ## ## @code{[@dots{}] = lognlike (@var{params}, @var{x}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = lognlike (@var{params}, @var{x}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## Further information about the lognormal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Log-normal_distribution} ## ## @seealso{logncdf, logninv, lognpdf, lognrnd, lognfit, lognstat} ## @end deftypefn function [nlogL, avar] = lognlike (params, x, censor, freq) ## Check input arguments if (nargin < 2) error ("lognlike: function called with too few input arguments."); endif if (! isvector (x)) error ("lognlike: X must be a vector."); endif if (numel (params) != 2) error ("lognlike: PARAMS must be a two-element vector."); endif if (nargin < 3 || isempty (censor)) censor = []; elseif (! isequal (size (x), size (censor))) error ("lognlike: X and CENSOR vectors mismatch."); endif if nargin < 4 || isempty (freq) freq = []; elseif (isequal (size (x), size (freq))) nulls = find (freq == 0); if (numel (nulls) > 0) x(nulls) = []; if (numel (censor) == numel (freq)) censor(nulls) = []; endif freq(nulls) = []; endif else error ("lognlike: X and FREQ vectors mismatch."); endif ## Treat negative data in X as missing values x(x < 0) = NaN; ## Calculate on log data logx = log (x); if (nargout <= 1) nlogL = normlike (params, logx, censor, freq); else [nlogL, avar] = normlike (params, logx, censor, freq); endif ## Compute censored and frequency if (isempty (freq)) freq = 1; endif if (isempty (censor)) censor = 0; endif nlogL = nlogL + sum (freq .* logx .* (1 - censor)); endfunction ## Test output %!test %! x = 1:50; %! [nlogL, avar] = lognlike ([0, 0.25], x); %! avar_out = [-5.4749e-03, 2.8308e-04; 2.8308e-04, -1.1916e-05]; %! assert_equal (nlogL, 3962.330333301793, 1e-10); %! assert_equal (avar, avar_out, 1e-7); %!test %! x = 1:50; %! [nlogL, avar] = lognlike ([0, 0.25], x * 0.5); %! avar_out = [-7.6229e-03, 4.8722e-04; 4.8722e-04, -2.6754e-05]; %! assert_equal (nlogL, 2473.183051225747, 1e-10); %! assert_equal (avar, avar_out, 1e-7); %!test %! x = 1:50; %! [nlogL, avar] = lognlike ([0, 0.5], x); %! avar_out = [-2.1152e-02, 2.2017e-03; 2.2017e-03, -1.8535e-04]; %! assert_equal (nlogL, 1119.072424020455, 1e-12); %! assert_equal (avar, avar_out, 1e-6); %!test %! x = 1:50; %! censor = ones (1, 50); %! censor([2, 4, 6, 8, 12, 14]) = 0; %! [nlogL, avar] = lognlike ([0, 0.5], x, censor); %! avar_out = [-1.9823e-02, 2.0370e-03; 2.0370e-03, -1.6618e-04]; %! assert_equal (nlogL, 1091.746371145497, 1e-12); %! assert_equal (avar, avar_out, 1e-6); %!test %! x = 1:50; %! censor = ones (1, 50); %! censor([2, 4, 6, 8, 12, 14]) = 0; %! [nlogL, avar] = lognlike ([0, 1], x, censor); %! avar_out = [-6.8634e-02, 1.3968e-02; 1.3968e-02, -2.1664e-03]; %! assert_equal (nlogL, 349.3969104144271, 1e-12); %! assert_equal (avar, avar_out, 1e-6); ## Test input validation %!error ... %! lognlike ([12, 15]); %!error lognlike ([12, 15], ones (2)); %!error ... %! lognlike ([12, 15, 3], [1:50]); %!error ... %! lognlike ([12, 15], [1:50], [1, 2, 3]); %!error ... %! lognlike ([12, 15], [1:50], [], [1, 2, 3]); statistics-release-2.1.2/inst/Distribution_Fitting/nakafit.m000066400000000000000000000223451527311104300242420ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{paramhat} =} nakafit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} nakafit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} nakafit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@dots{}] =} nakafit (@var{x}, @var{alpha}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} nakafit (@var{x}, @var{alpha}, @var{censor}, @var{freq}) ## @deftypefnx {statistics} {[@dots{}] =} nakafit (@var{x}, @var{alpha}, @var{censor}, @var{freq}, @var{options}) ## ## Estimate mean and confidence intervals for the Nakagami distribution. ## ## @code{@var{mu0} = nakafit (@var{x})} returns the maximum likelihood ## estimates of the parameters of the Nakagami distribution given the data in ## @var{x}. @qcode{@var{paramhat}(1)} is the shape parameter, @var{mu}, and ## @qcode{@var{paramhat}(2)} is the spread parameter, @var{omega}. ## ## @code{[@var{paramhat}, @var{paramci}] = nakafit (@var{x})} returns the 95% ## confidence intervals for the parameter estimates. ## ## @code{[@dots{}] = nakafit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals for the ## parameter estimates. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = nakafit (@var{x}, @var{alpha}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = nakafit (@var{params}, @var{x}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} must contain non-negative integer frequencies for the ## corresponding elements in @var{x}. By default, or if left empty, ## @qcode{@var{freq} = ones (size (@var{x}))}. ## ## @code{[@dots{}] = nakafit (@dots{}, @var{options})} specifies control ## parameters for the iterative algorithm used to compute ML estimates with the ## @code{fminsearch} function. @var{options} is a structure with the following ## fields and their default values: ## @itemize ## @item @qcode{@var{options}.Display = "off"} ## @item @qcode{@var{options}.MaxFunEvals = 400} ## @item @qcode{@var{options}.MaxIter = 200} ## @item @qcode{@var{options}.TolX = 1e-6} ## @end itemize ## ## Further information about the Nakagami distribution can be found at ## @url{https://en.wikipedia.org/wiki/Nakagami_distribution} ## ## @seealso{nakacdf, nakainv, nakapdf, nakarnd, nakalike, nakastat} ## @end deftypefn function [paramhat, paramci] = nakafit (x, alpha, censor, freq, options) ## Check input arguments if (! isvector (x)) error ("nakafit: X must be a vector."); endif ## Check alpha if (nargin < 2 || isempty (alpha)) alpha = 0.05; else if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("nakafit: wrong value for ALPHA."); endif endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("nakafit: X and CENSOR vectors mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("nakafit: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("nakafit: FREQ must not contain negative values."); elseif (any (fix (freq) != freq)) error ("nakafit: FREQ must contain integer values."); endif ## Get options structure or add defaults if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("nakafit: 'options' 5th argument must be a", ... " structure with 'Display', 'MaxFunEvals',", ... " 'MaxIter', and 'TolX' fields present.")); endif endif ## Expand frequency and censor vectors (if necessary) if (! all (freq == 1)) xf = []; cf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; cf = [cf, repmat(censor(i), 1, freq(i))]; endfor x = xf; freq = ones (size (x)); censor = cf; endif ## Get parameter estimates from the Gamma distribution paramhat = gamfit (x .^ 2, alpha, censor, freq, options); ## Transform back to Nakagami parameters paramhat(2) = paramhat(1) .* paramhat(2); ## Compute CIs using a log normal approximation for parameters. if (nargout > 1) ## Compute asymptotic covariance [~, acov] = nakalike (paramhat, x, censor, freq); ## Get standard errors se = sqrt (diag (acov))'; ## Get normal quantiles probs = [alpha/2; 1-alpha/2]; ## Compute muci using a normal approximation paramci(:,1) = norminv (probs, paramhat(1), se(1)); ## Compute omegaci using a normal approximation for log (omega) paramci(:,2) = exp (norminv (probs, log (paramhat(2)), log (se(2)))); endif endfunction %!demo %! ## Sample 3 populations from different Nakagami distributions %! randg ('state', 42); %! r1 = nakarnd (0.5, 1, 2000, 1); %! r2 = nakarnd (5, 1, 2000, 1); %! r3 = nakarnd (2, 2, 2000, 1); %! r = [r1, r2, r3]; %! %! ## Plot them normalized and fix their colors %! hist (r, [0.05:0.1:3.5], 10); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! set (h(3), 'facecolor', 'r'); %! ylim ([0, 2.5]); %! xlim ([0, 3.0]); %! hold on %! %! ## Estimate their MU and LAMBDA parameters %! mu_omegaA = nakafit (r(:,1)); %! mu_omegaB = nakafit (r(:,2)); %! mu_omegaC = nakafit (r(:,3)); %! %! ## Plot their estimated PDFs %! x = [0.01:0.1:3.01]; %! y = nakapdf (x, mu_omegaA(1), mu_omegaA(2)); %! plot (x, y, '-pr'); %! y = nakapdf (x, mu_omegaB(1), mu_omegaB(2)); %! plot (x, y, '-sg'); %! y = nakapdf (x, mu_omegaC(1), mu_omegaC(2)); %! plot (x, y, '-^c'); %! legend ({'Normalized HIST of sample 1 with μ=0.5 and ω=1', ... %! 'Normalized HIST of sample 2 with μ=5 and ω=1', ... %! 'Normalized HIST of sample 3 with μ=2 and ω=2', ... %! sprintf("PDF for sample 1 with estimated μ=%0.2f and ω=%0.2f", ... %! mu_omegaA(1), mu_omegaA(2)), ... %! sprintf("PDF for sample 2 with estimated μ=%0.2f and ω=%0.2f", ... %! mu_omegaB(1), mu_omegaB(2)), ... %! sprintf("PDF for sample 3 with estimated μ=%0.2f and ω=%0.2f", ... %! mu_omegaC(1), mu_omegaC(2))}) %! title ('Three population samples from different Nakagami distributions') %! hold off ## Test output %!test %! paramhat = nakafit ([1:50]); %! paramhat_out = [0.7355, 858.5]; %! assert_equal (paramhat, paramhat_out, 1e-4); %!test %! paramhat = nakafit ([1:5]); %! paramhat_out = [1.1740, 11]; %! assert_equal (paramhat, paramhat_out, 1e-4); %!test %! paramhat = nakafit ([1:6], [], [], [1 1 1 1 1 0]); %! paramhat_out = [1.1740, 11]; %! assert_equal (paramhat, paramhat_out, 1e-4); %!test %! paramhat = nakafit ([1:5], [], [], [1 1 1 1 2]); %! paramhat_out = nakafit ([1:5, 5]); %! assert_equal (paramhat, paramhat_out, 1e-4); ## Test input validation %!error nakafit (ones (2,5)); %!error nakafit ([1, 2, 3, 4, 5], 1.2); %!error nakafit ([1, 2, 3, 4, 5], 0); %!error nakafit ([1, 2, 3, 4, 5], 'alpha'); %!error ... %! nakafit ([1, 2, 3, 4, 5], 0.05, [1 1 0]); %!error ... %! nakafit ([1, 2, 3, 4, 5], [], [1 1 0 1 1]'); %!error ... %! nakafit ([1, 2, 3, 4, 5], 0.05, zeros (1,5), [1 1 0]); %!error ... %! nakafit ([1, 2, 3, 4, 5], [], [], [1 1 0 1 1]'); %!error ... %! nakafit ([1, 2, 3, 4, 5], [], [], [1 1 -1 1 1]); %!error ... %! nakafit ([1, 2, 3, 4, 5], [], [], [1 1 1.5 1 1]); %!error ... %! nakafit ([1, 2, 3, 4, 5], 0.05, [], [], 2); statistics-release-2.1.2/inst/Distribution_Fitting/nakalike.m000066400000000000000000000256421527311104300244070ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} nakalike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} nakalike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} nakalike (@var{params}, @var{x}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} nakalike (@var{params}, @var{x}, @var{censor}, @var{freq}) ## ## Negative log-likelihood for the Nakagami distribution. ## ## @code{@var{nlogL} = nakalike (@var{params}, @var{x})} returns the negative ## log likelihood of the data in @var{x} corresponding to the Nakagami ## distribution with (1) shape parameter @var{mu} and (2) spread parameter ## @var{omega} given in the two-element vector @var{params}. ## ## @code{[@var{nlogL}, @var{acov}] = nakalike (@var{params}, @var{x})} also ## returns the inverse of Fisher's information matrix, @var{acov}. If the input ## parameter values in @var{params} are the maximum likelihood estimates, the ## diagonal elements of @var{params} are their asymptotic variances. ## ## @code{[@dots{}] = nakalike (@var{params}, @var{x}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = nakalike (@var{params}, @var{x}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} must contain non-negative integer frequencies for the ## corresponding elements in @var{x}. By default, or if left empty, ## @qcode{@var{freq} = ones (size (@var{x}))}. ## ## Further information about the Nakagami distribution can be found at ## @url{https://en.wikipedia.org/wiki/Nakagami_distribution} ## ## @seealso{nakacdf, nakainv, nakapdf, nakarnd, nakafit, nakastat} ## @end deftypefn function [nlogL, acov] = nakalike (params, x, censor, freq) ## Check input arguments if (nargin < 2) error ("nakalike: function called with too few input arguments."); endif if (! isvector (x)) error ("nakalike: X must be a vector."); endif if (length (params) != 2) error ("nakalike: PARAMS must be a two-element vector."); endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("nakalike: X and CENSOR vector mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("nakalike: X and FREQ vector mismatch."); elseif (any (freq < 0)) error ("nakalike: FREQ must not contain negative values."); elseif (any (fix (freq) != freq)) error ("nakafit: FREQ must contain integer values."); endif ## Expand frequency and censor vectors (if necessary) if (! all (freq == 1)) xf = []; cf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; cf = [cf, repmat(censor(i), 1, freq(i))]; endfor x = xf; freq = ones (size (x)); censor = cf; endif ## Get parameters mu = params(1); omega = params(2); log_a = gammaln (mu); log_b = log (omega / mu); z = x .^ 2 ./ (omega / mu); log_z = log (z); L = (mu - 1) .* log_z - z - log_a - log_b + log (2 .* x); ## Handle censored data n_censored = sum (freq .* censor); if (n_censored > 0) censored = (censor == 1); z_censored = z(censored); [S, dS] = dgammainc (z_censored, mu); L(censored) = log (S); endif ## Sum up the neg log likelihood nlogL = -sum (freq .* L); ## Compute asymptotic covariance if (nargout > 1) ## Compute first order central differences of the log-likelihood gradient dp = 0.0001 .* max (abs (params), 1); ngrad_p1 = logl_grad (params + [dp(1), 0], x, censor, freq); ngrad_m1 = logl_grad (params - [dp(1), 0], x, censor, freq); ngrad_p2 = logl_grad (params + [0, dp(2)], x, censor, freq); ngrad_m2 = logl_grad (params - [0, dp(2)], x, censor, freq); ## Compute negative Hessian by normalizing the differences by the increment nH = [(ngrad_p1(:) - ngrad_m1(:))./(2 * dp(1)), ... (ngrad_p2(:) - ngrad_m2(:))./(2 * dp(2))]; ## Force neg Hessian being symmetric nH = 0.5 .* (nH + nH'); ## Check neg Hessian is positive definite [R, p] = chol (nH); if (p > 0) warning ("nakalike: non positive definite Hessian matrix."); acov = NaN (2); return endif ## ACOV estimate is the negative inverse of the Hessian. Rinv = inv (R); acov = Rinv * Rinv; endif endfunction ## Helper function for computing negative gradient function ngrad = logl_grad (params, x, censor, freq) mu = params(1); omega = params(2); ## Transform to Gamma parameters log_a = gammaln (mu); log_b = log (omega / mu); z = x .^ 2 ./ (omega / mu); log_z = log (z); dL1 = log_z - psi (mu); dL2 = (z - mu) ./ (omega / mu); ## Handle censored data n_censored = sum (freq .* censor); if (n_censored > 0) censored = (censor == 1); z_censored = z(censored); [S, dS] = dgammainc (z_censored, a); dL1(censored) = dS ./ S; tmp = mu .* log_z(censored) - log_b - z_censored - log_a; dL2(censored) = exp (tmp) ./ S; endif ngrad = -[sum(freq .* dL1), sum(freq .* dL2)]; ## Transform back to Nakagami parameters ngrad = ngrad * [1, 0; (-omega ./ (mu .^ 2)), (1 ./ mu)]; endfunction ## Compute the incomplete Gamma function with its 1st and 2nd derivatives function [y, dy, d2y] = dgammainc (x, k) ## Initialize return variables y = nan (size (x)); dy = y; d2y = y; ## Use approximation for K > 2^20 ulim = 2^20; is_lim = find (k > ulim); if (! isempty (is_lim)) x(is_lim) = max (ulim - 1/3 + sqrt (ulim ./ k(is_lim)) .* ... (x(is_lim) - (k(is_lim) - 1/3)), 0); k(is_lim) = ulim; endif ## For x < k+1 is_lo = find (x < k + 1 & x != 0); if (! isempty (is_lo)) x_lo = x(is_lo); k_lo = k(is_lo); k_1 = k_lo; step = 1; d1st = 0; d2st = 0; stsum = step; d1sum = d1st; d2sum = d2st; while norm (step, 'inf') >= 100 * eps (norm (stsum, 'inf')) k_1 += 1; step = step .* x_lo ./ k_1; d1st = (d1st .* x_lo - step) ./ k_1; d2st = (d2st .* x_lo - 2 .* d1st) ./ k_1; stsum = stsum + step; d1sum = d1sum + d1st; d2sum = d2sum + d2st; endwhile fklo = exp (-x_lo + k_lo .* log (x_lo) - gammaln (k_lo + 1)); y_lo = fklo .* stsum; ## Fix very small k y_lo(x_lo > 0 & y_lo > 1) = 1; ## Compute 1st derivative dlogfklo = (log (x_lo) - psi (k_lo + 1)); d1fklo = fklo .* dlogfklo; d1y_lo = d1fklo .* stsum + fklo .* d1sum; ## Compute 2nd derivative d2fklo = d1fklo .* dlogfklo - fklo .* psi (1, k_lo + 1); d2y_lo = d2fklo .* stsum + 2 .* d1fklo .* d1sum + fklo .* d2sum; ## Considering the upper tail y(is_lo) = 1 - y_lo; dy(is_lo) = -d1y_lo; d2y(is_lo) = -d2y_lo; endif ## For x >= k+1 is_hi = find (x >= k+1); if (! isempty (is_hi)) x_hi = x(is_hi); k_hi = k(is_hi); zc = 0; k0 = 0; k1 = k_hi; x0 = 1; x1 = x_hi; d1k0 = 0; d1k1 = 1; d1x0 = 0; d1x1 = 0; d2k0 = 0; d2k1 = 0; d2x0 = 0; d2x2 = 0; kx = k_hi ./ x_hi; d1kx = 1 ./ x_hi; d2kx = 0; start = 1; while norm (d2kx - start, 'Inf') > 100 * eps (norm (d2kx, 'Inf')) rescale = 1 ./ x1; zc += 1; n_k = zc - k_hi; d2k0 = (d2k1 + d2k0 .* n_k - 2 .* d1k0) .* rescale; d2x0 = (d2x2 + d2x0 .* n_k - 2 .* d1x0) .* rescale; d1k0 = (d1k1 + d1k0 .* n_k - k0) .* rescale; d1x0 = (d1x1 + d1x0 .* n_k - x0) .* rescale; k0 = (k1 + k0 .* n_k) .* rescale; x0 = 1 + (x0 .* n_k) .* rescale; nrescale = zc .* rescale; d2k1 = d2k0 .* x_hi + d2k1 .* nrescale; d2x2 = d2x0 .* x_hi + d2x2 .* nrescale; d1k1 = d1k0 .* x_hi + d1k1 .* nrescale; d1x1 = d1x0 .* x_hi + d1x1 .* nrescale; k1 = k0 .* x_hi + k1 .* nrescale; x1 = x0 .* x_hi + zc; start = d2kx; kx = k1 ./ x1; d1kx = (d1k1 - kx .* d1x1) ./ x1; d2kx = (d2k1 - d1kx .* d1x1 - kx .* d2x2 - d1kx .* d1x1) ./ x1; endwhile fkhi = exp (-x_hi + k_hi .* log (x_hi) - gammaln (k_hi + 1)); y_hi = fkhi .* kx; ## Compute 1st derivative dlogfkhi = (log (x_hi) - psi (k_hi + 1)); d1fkhi = fkhi .* dlogfkhi; d1y_hi = d1fkhi .* kx + fkhi .* d1kx; ## Compute 2nd derivative d2fkhi = d1fkhi .* dlogfkhi - fkhi .* psi (1, k_hi + 1); d2y_hi = d2fkhi .* kx + 2 .* d1fkhi .* d1kx + fkhi .* d2kx; ## Considering the upper tail y(is_hi) = y_hi; dy(is_hi) = d1y_hi; d2y(is_hi) = d2y_hi; endif ## Handle x == 0 is_x0 = find (x == 0); if (! isempty (is_x0)) ## Considering the upper tail y(is_x0) = 1; dy(is_x0) = 0; d2y(is_x0) = 0; endif ## Handle k == 0 is_k0 = find (k == 0); if (! isempty (is_k0)) is_k0x0 = find (k == 0 & x == 0); ## Considering the upper tail y(is_k0) = 0; dy(is_k0x0) = Inf; d2y(is_k0x0) = -Inf; endif endfunction ## Test output %!test %! nlogL = nakalike ([0.735504, 858.5], [1:50]); %! assert_equal (nlogL, 202.8689, 1e-4); %!test %! nlogL = nakalike ([1.17404, 11], [1:5]); %! assert_equal (nlogL, 8.6976, 1e-4); %!test %! nlogL = nakalike ([1.17404, 11], [1:5], [], [1, 1, 1, 1, 1]); %! assert_equal (nlogL, 8.6976, 1e-4); %!test %! nlogL = nakalike ([1.17404, 11], [1:6], [], [1, 1, 1, 1, 1, 0]); %! assert_equal (nlogL, 8.6976, 1e-4); ## Test input validation %!error nakalike (3.25) %!error nakalike ([5, 0.2], ones (2)) %!error ... %! nakalike ([1, 0.2, 3], [1, 3, 5, 7]) %!error ... %! nakalike ([1.5, 0.2], [1:5], [0, 0, 0]) %!error ... %! nakalike ([1.5, 0.2], [1:5], [0, 0, 0, 0, 0], [1, 1, 1]) %!error ... %! nakalike ([1.5, 0.2], [1:5], [], [1, 1, 1]) %!error ... %! nakalike ([1.5, 0.2], [1:5], [], [1, 1, 1, 1, -1]) statistics-release-2.1.2/inst/Distribution_Fitting/nbinfit.m000066400000000000000000000306731527311104300242610ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{paramhat} =} nbinfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} nbinfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} nbinfit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} nbinfit (@var{x}, @var{alpha}, @var{freq}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} nbinfit (@var{x}, @var{alpha}, @var{options}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} nbinfit (@var{x}, @var{alpha}, @var{freq}, @var{options}) ## ## Estimate parameter and confidence intervals for the negative binomial ## distribution. ## ## @code{@var{paramhat} = nbinfit (@var{x})} returns the maximum likelihood ## estimates of the parameters of the negative binomial distribution given the ## data in vector @var{x}. @qcode{@var{paramhat}(1)} is the number of successes ## until the experiment is stopped, @var{r}, and @qcode{@var{paramhat}(2)} is ## the probability of success in each experiment, @var{ps}. ## ## @code{[@var{paramhat}, @var{paramci}] = nbinfit (@var{x})} returns the 95% ## confidence intervals for the parameter estimates. ## ## @code{[@var{paramhat}, @var{paramci}] = nbinfit (@var{x}, @var{alpha})} also ## returns the @qcode{100 * (1 - @var{alpha})} percent confidence intervals of ## the estimated parameter. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. ## ## @code{[@dots{}] = nbinlike (@var{params}, @var{x}, @var{freq})} accepts a ## frequency vector, @var{freq}, of the same size as @var{x}. @var{freq} ## must contain non-negative integer frequencies for the corresponding elements ## in @var{x}. By default, or if left empty, ## @qcode{@var{freq} = ones (size (@var{x}))}. ## ## @code{[@var{paramhat}, @var{paramci}] = nbinfit (@var{x}, @var{alpha}, ## @var{options})} specifies control parameters for the iterative algorithm used ## to compute ML estimates with the @code{fminsearch} function. @var{options} ## is a structure with the following fields and their default values: ## @itemize ## @item @qcode{@var{options}.Display = "off"} ## @item @qcode{@var{options}.MaxFunEvals = 400} ## @item @qcode{@var{options}.MaxIter = 200} ## @item @qcode{@var{options}.TolX = 1e-6} ## @end itemize ## ## When @var{r} is an integer, the negative binomial distribution is also known ## as the Pascal distribution and it models the number of failures in @var{x} ## before a specified number of successes is reached in a series of independent, ## identical trials. Its parameters are the probability of success in a single ## trial, @var{ps}, and the number of successes, @var{r}. A special case of the ## negative binomial distribution, when @qcode{@var{r} = 1}, is the geometric ## distribution, which models the number of failures before the first success. ## ## @var{r} can also have non-integer positive values, in which form the negative ## binomial distribution, also known as the Polya distribution, has no ## interpretation in terms of repeated trials, but, like the Poisson ## distribution, it is useful in modeling count data. The negative binomial ## distribution is more general than the Poisson distribution because it has a ## variance that is greater than its mean, making it suitable for count data ## that do not meet the assumptions of the Poisson distribution. In the limit, ## as @var{r} increases to infinity, the negative binomial distribution ## approaches the Poisson distribution. ## ## Further information about the negative binomial distribution can be found at ## @url{https://en.wikipedia.org/wiki/Negative_binomial_distribution} ## ## @seealso{nbincdf, nbininv, nbinpdf, nbinrnd, nbinlike, nbinstat} ## @end deftypefn function [paramhat, paramci] = nbinfit (x, alpha, varargin) ## Check data in X if (any (x < 0)) error ("nbinfit: X cannot have negative values."); endif if (! isvector (x)) error ("nbinfit: X must be a vector."); endif if (any (x < 0) || any (x != round (x)) || any (isinf (x))) error ("nbinfit: X must be a non-negative integer."); endif ## Check ALPHA if (nargin < 2 || isempty (alpha)) alpha = 0.05; elseif (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("nbinfit: wrong value for ALPHA."); endif ## Add defaults freq = []; options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; ## Check extra arguments for FREQ vector and/or 'options' structure if (nargin > 2) if (numel (varargin) == 1 && isstruct (varargin{1})) options = varargin{1}; elseif (numel (varargin) == 1 && isnumeric (varargin{1})) freq = varargin{1}; elseif (numel (varargin) == 2) freq = varargin{1}; options = varargin{2}; endif if (isempty (freq)) freq = ones (size (x)); endif ## Check for valid freq vector if (! isequal (size (x), size (freq))) error ("nbinfit: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("nbinfit: FREQ must not contain negative values."); elseif (any (fix (freq) != freq)) error ("nbinfit: FREQ must contain integer values."); endif ## Check for valid options structure if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("nbinfit: 'options' argument must be a", ... " structure with 'Display', 'MaxFunEvals',", ... " 'MaxIter', and 'TolX' fields present.")); endif endif ## Expand frequency if (! all (freq == 1)) xf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; endfor x = xf; endif ## Ensure that a negative binomial fit is valid. xbar = mean (x); varx = var (x); if (varx <= xbar) paramhat = cast ([Inf, 1.0], class (x)); paramci = cast ([Inf, 1; Inf, 1], class (x)); fprintf ("warning: nbinfit: mean exceeds variance.\n"); return endif ## Use Method of Moments estimates as starting point for MLEs. rhat = (xbar .* xbar) ./ (varx - xbar); ## Minimize negative log-likelihood to estimate parameters by parameterizing ## with mu=r(1-p)/p, so it becomes 1-parameter search for rhat. f = @(rhat) nbinfit_search (rhat, x, numel (x), sum (x), options.TolX); [rhat, ~, err, output] = fminsearch (f, rhat, options); ## Handle errors if (err == 0) if (output.funcCount >= options.MaxFunEvals) warning (strcat ("nbinfit: maximum number of function", ... " evaluations are exceeded.")); elseif (output.iterations >= options.MaxIter) warning ("nbinfit: maximum number of iterations are exceeded."); endif elseif (err < 0) error ("nbinfit: NoSolution."); endif ## Compute parameter estimates pshat = rhat ./ (xbar + rhat); paramhat = [rhat, pshat]; ## Compute confidence interval if (nargout > 1) [~, avar] = nbinlike (paramhat, x); ## Get standard errors sigma = sqrt (diag (avar)); ## Get normal quantiles probs = [alpha/2; 1-alpha/2]; ## Compute paramci using a normal approximation. The bounds are not ## restricted to the parameter space, as MATLAB does not restrict them: a ## bound outside it is what tells the caller the normal approximation has ## broken down, and clamping it away leaves an interval that looks ordinary ## and is not. paramci = norminv ([probs, probs], [paramhat; paramhat], [sigma'; sigma']); endif endfunction ## Helper function for minimizing the negative log-likelihood function nll = nbinfit_search (r, x, nx, sx, tol) if (r < tol) nll = Inf; else xbar = sx / nx; nll = -sum (gammaln (r +x )) + nx * gammaln (r) ... -nx * r * log (r / (xbar + r)) - sx * log (xbar / (xbar + r)); endif endfunction %!demo %! ## Sample 2 populations from different negative binomial distributions %! randg ('state', 42); %! randp ('state', 42); %! r1 = nbinrnd (2, 0.15, 5000, 1); %! r2 = nbinrnd (5, 0.2, 5000, 1); %! r = [r1, r2]; %! %! ## Plot them normalized and fix their colors %! hist (r, [0:51], 1); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! hold on %! %! ## Estimate their probability of success %! r_psA = nbinfit (r(:,1)); %! r_psB = nbinfit (r(:,2)); %! %! ## Plot their estimated PDFs %! x = [0:40]; %! y = nbinpdf (x, r_psA(1), r_psA(2)); %! plot (x, y, '-pg'); %! x = [min(r(:,2)):max(r(:,2))]; %! y = nbinpdf (x, r_psB(1), r_psB(2)); %! plot (x, y, '-sc'); %! ylim ([0, 0.1]) %! xlim ([0, 50]) %! legend ({'Normalized HIST of sample 1 with r=2 and ps=0.15', ... %! 'Normalized HIST of sample 2 with r=5 and ps=0.2', ... %! sprintf("PDF for sample 1 with estimated r=%0.2f and ps=%0.2f", ... %! r_psA(1), r_psA(2)), ... %! sprintf("PDF for sample 2 with estimated r=%0.2f and ps=%0.2f", ... %! r_psB(1), r_psB(2))}) %! title ('Two population samples from negative different binomial distributions') %! hold off ## Test output %!test %! [paramhat, paramci] = nbinfit ([1:50]); %! assert_equal (paramhat, [2.420857, 0.086704], 1e-6); %! assert_equal (paramci(:,1), [1.382702; 3.459012], 1e-6); %! assert_equal (paramci(:,2), [0.049676; 0.123732], 1e-6); %!test %! [paramhat, paramci] = nbinfit ([1:20]); %! assert_equal (paramhat, [3.588233, 0.254697], 1e-6); %! ## The interval is a normal approximation over a numerical Hessian, and %! ## agrees with R2024a to a few parts in a million rather than exactly. %! assert_equal (paramci(:,1), [0.451693; 6.724772], 1e-5); %! assert_equal (paramci(:,2), [0.081143; 0.428251], 1e-5); %!test %! [paramhat, paramci] = nbinfit ([1:10]); %! assert_equal (paramhat, [8.8067, 0.6156], 1e-4); %! assert_equal (paramci(:,1), [-13.0934; 30.7068], 1e-4); %! assert_equal (paramci(:,2), [0.0217; 1.2094], 1e-4); %!test %! [paramhat, paramci] = nbinfit ([1:10], 0.05, ones (1, 10)); %! assert_equal (paramhat, [8.8067, 0.6156], 1e-4); %! assert_equal (paramci(:,1), [-13.0934; 30.7068], 1e-4); %! assert_equal (paramci(:,2), [0.0217; 1.2094], 1e-4); %!test %! [paramhat, paramci] = nbinfit ([1:11], 0.05, [ones(1, 10), 0]); %! assert_equal (paramhat, [8.8067, 0.6156], 1e-4); %! assert_equal (paramci(:,1), [-13.0934; 30.7068], 1e-4); %! assert_equal (paramci(:,2), [0.0217; 1.2094], 1e-4); ## Values below are R2024a's, measured 2026-08-17. %!test %! ## a bound outside the parameter space is reported, not clamped away %! [~, paramci] = nbinfit ([1:10]); %! assert_equal (paramci(1,1) < 0, true); %! assert_equal (paramci(2,2) > 1, true); %! assert_equal (paramci(1,1), -13.093353591937408, 1e-5); %! assert_equal (paramci(2,2), 1.209414934623099, 1e-5); ## Test input validation %!error nbinfit ([-1 2 3 3]) %!error nbinfit (ones (2)) %!error nbinfit ([1 2 1.2 3]) %!error nbinfit ([1 2 3], 0) %!error nbinfit ([1 2 3], 1.2) %!error nbinfit ([1 2 3], [0.02 0.05]) %!error ... %! nbinfit ([1, 2, 3, 4, 5], 0.05, [1, 2, 3, 2]); %!error ... %! nbinfit ([1, 2, 3, 4, 5], 0.05, [1, 2, 3, 2, -1]); %!error ... %! nbinfit ([1, 2, 3, 4, 5], 0.05, [1, 2, 3, 2, 1.5]); %!error ... %! nbinfit ([1, 2, 3, 4, 5], 0.05, struct ('option', 234)); %!error ... %! nbinfit ([1, 2, 3, 4, 5], 0.05, ones (1,5), struct ('option', 234)); statistics-release-2.1.2/inst/Distribution_Fitting/nbinlike.m000066400000000000000000000154551527311104300244240ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} nbinlike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{avar}] =} nbinlike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{avar}] =} nbinlike (@var{params}, @var{x}, @var{freq}) ## ## Negative log-likelihood for the negative binomial distribution. ## ## @code{@var{nlogL} = nbinlike (@var{params}, @var{x})} returns the negative ## log likelihood of the negative binomial distribution with (1) parameter ## @var{r} and (2) parameter @var{ps}, given in the two-element vector ## @var{params}, where @var{r} is the number of successes until the experiment ## is stopped and @var{ps} is the probability of success in each experiment, ## given the number of failures in @var{x}. ## ## @code{[@var{nlogL}, @var{avar}] = nbinlike (@var{params}, @var{x})} also ## returns the inverse of Fisher's information matrix, @var{avar}. If the input ## parameter values in @var{params} are the maximum likelihood estimates, the ## diagonal elements of @var{params} are their asymptotic variances. ## ## @code{[@dots{}] = nbinlike (@var{params}, @var{x}, @var{freq})} accepts a ## frequency vector, @var{freq}, of the same size as @var{x}. @var{freq} ## must contain non-negative integer frequencies for the corresponding elements ## in @var{x}. By default, or if left empty, ## @qcode{@var{freq} = ones (size (@var{x}))}. ## ## When @var{r} is an integer, the negative binomial distribution is also known ## as the Pascal distribution and it models the number of failures in @var{x} ## before a specified number of successes is reached in a series of independent, ## identical trials. Its parameters are the probability of success in a single ## trial, @var{ps}, and the number of successes, @var{r}. A special case of the ## negative binomial distribution, when @qcode{@var{r} = 1}, is the geometric ## distribution, which models the number of failures before the first success. ## ## @var{r} can also have non-integer positive values, in which form the negative ## binomial distribution, also known as the Polya distribution, has no ## interpretation in terms of repeated trials, but, like the Poisson ## distribution, it is useful in modeling count data. The negative binomial ## distribution is more general than the Poisson distribution because it has a ## variance that is greater than its mean, making it suitable for count data ## that do not meet the assumptions of the Poisson distribution. In the limit, ## as @var{r} increases to infinity, the negative binomial distribution ## approaches the Poisson distribution. ## ## Further information about the negative binomial distribution can be found at ## @url{https://en.wikipedia.org/wiki/Negative_binomial_distribution} ## ## @seealso{nbincdf, nbininv, nbinpdf, nbinrnd, nbinfit, nbinstat} ## @end deftypefn function [nlogL, avar] = nbinlike (params, x, freq) ## Check input arguments if (nargin < 2) error ("nbinlike: function called with too few input arguments."); endif if (! isvector (x)) error ("nbinlike: X must be a vector."); endif if (any (x < 0)) error ("nbinlike: X cannot have negative values."); endif if (any (x != fix (x))) error ("nbinlike: number of failures, X, must be integers."); endif if (length (params) != 2) error ("nbinlike: PARAMS must be a two-element vector."); endif if (params(1) <= 0) error (strcat ("nbinlike: number of successes, PARAMS(1), must be", ... " a real positive value.")); endif if (params(2) < 0 || params(2) > 1) error (strcat ("nbinlike: probability of success, PARAMS(2), must be", ... " in the range [0,1].")); endif if (nargin < 3 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("nbinlike: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("nbinlike: FREQ must not contain negative values."); elseif (any (fix (freq) != freq)) error ("nbinlike: FREQ must contain integer values."); endif ## Expand frequency if (! all (freq == 1)) xf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; endfor x = xf; endif ## Compute negative log-likelihood and asymptotic variance r = params(1); ps = params(2); nx = numel (x); glnr = gammaln (r + x) - gammaln (x + 1) - gammaln (r); sumx = sum (x); nlogL = -(sum (glnr) + nx * r * log (ps)) - sumx * log (1 - ps); if (nargout == 2) dL11 = sum (psi (1, r + x) - psi (1, r)); dL12 = nx ./ ps; dL22 = -nx .*r ./ ps .^ 2 - sumx ./ (1 - ps) .^ 2; nH = -[dL11, dL12; dL12, dL22]; if (any (isnan (nH(:)))) avar = [NaN, NaN; NaN, NaN]; else avar = inv (nH); endif endif endfunction ## Test output %!assert_equal (nbinlike ([2.42086, 0.0867043], [1:50]), 205.5942, 1e-4) %!assert_equal (nbinlike ([3.58823, 0.254697], [1:20]), 63.6435, 1e-4) %!assert_equal (nbinlike ([8.80671, 0.615565], [1:10]), 24.7410, 1e-4) %!assert_equal (nbinlike ([22.1756, 0.831306], [1:8]), 17.9528, 1e-4) %!assert_equal (nbinlike ([22.1756, 0.831306], [1:9], [ones(1,8), 0]), 17.9528, 1e-4) ## Test input validation %!error nbinlike (3.25) %!error nbinlike ([5, 0.2], ones (2)) %!error nbinlike ([5, 0.2], [-1, 3]) %!error ... %! nbinlike ([1, 0.2, 3], [1, 3, 5, 7]) %!error nbinlike ([-5, 0.2], [1:15]) %!error nbinlike ([0, 0.2], [1:15]) %!error nbinlike ([5, 1.2], [3, 5]) %!error nbinlike ([5, -0.2], [3, 5]) %!error ... %! nbinlike ([5, 0.2], ones (10, 1), ones (8,1)) %!error ... %! nbinlike ([5, 0.2], ones (1, 8), [1 1 1 1 1 1 1 -1]) %!error ... %! nbinlike ([5, 0.2], ones (1, 8), [1 1 1 1 1 1 1 1.5]) statistics-release-2.1.2/inst/Distribution_Fitting/normfit.m000066400000000000000000000416031527311104300243010ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{muhat} =} normfit (@var{x}) ## @deftypefnx {statistics} {[@var{muhat}, @var{sigmahat}] =} normfit (@var{x}) ## @deftypefnx {statistics} {[@var{muhat}, @var{sigmahat}, @var{muci}] =} normfit (@var{x}) ## @deftypefnx {statistics} {[@var{muhat}, @var{sigmahat}, @var{muci}, @var{sigmaci}] =} normfit (@var{x}) ## @deftypefnx {statistics} {[@dots{}] =} normfit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@dots{}] =} normfit (@var{x}, @var{alpha}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} normfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}) ## @deftypefnx {statistics} {[@dots{}] =} normfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}, @var{options}) ## ## Estimate parameters and confidence intervals for the normal distribution. ## ## @code{[@var{muhat}, @var{sigmahat}] = normfit (@var{x})} estimates the ## parameters of the normal distribution given the data in @var{x}. @var{muhat} ## is an estimate of the mean, and @var{sigmahat} is an estimate of the standard ## deviation. ## ## @code{[@var{muhat}, @var{sigmahat}, @var{muci}, @var{sigmaci}] = normfit ## (@var{x})} returns the 95% confidence intervals for the mean and standard ## deviation estimates in the arrays @var{muci} and @var{sigmaci}, respectively. ## ## @itemize ## @item ## @var{x} can be a vector or a matrix. When @var{x} is a matrix, the parameter ## estimates and their confidence intervals are computed for each column. In ## this case, @code{normfit} supports only 2 input arguments, @var{x} and ## @var{alpha}. Optional arguments @var{censor}, @var{freq}, and @var{options} ## can be used only when @var{x} is a vector. ## ## @item ## @var{alpha} is a scalar value in the range @math{(0,1)} specifying the ## confidence level for the confidence intervals calculated as ## @math{100*(1 - alpha)%}. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @item ## @var{censor} is a logical vector of the same length as @var{x} specifying ## whether each value in @var{x} is right-censored or not. 1 indicates ## observations that are right-censored and 0 indicates observations that are ## fully observed. With censoring, @var{muhat} and @var{sigmahat} are the ## maximum likelihood estimates (MLEs). If empty, the default is an array of ## 0s, meaning that all observations are fully observed. ## ## @item ## @var{freq} is a vector of the same length as @var{x} and it typically ## contains non-negative integer counts of the corresponding elements in ## @var{x}. If empty, the default is an array of 1s, meaning one observation ## per element of @var{x}. To obtain the weighted MLEs for a data set with ## censoring, specify weights of observations, normalized to the number of ## observations in @var{x}. However, when there is no censored data (default), ## the returned estimate for standard deviation is not exactly the WMLE. To ## compute the weighted MLE, multiply the value returned in @var{sigmahat} by ## @code{sqrt ((sum (@var{freq}) - 1) / sum (@var{freq}))}. The square root is ## needed because the factor corrects a variance, while @var{sigmahat} is a ## standard deviation. This correction is needed because @code{normfit} ## normally computes @var{sigmahat} using an unbiased variance estimator when ## there is no censored data. When there is censoring ## in the data, the correction is not needed, since @code{normfit} does not use ## the unbiased variance estimator in that case. ## ## @item ## @var{options} is a structure with the control parameters for ## @code{fminsearch} which is used internally to compute MLEs for censored data. ## By default, it uses the following options: ## @itemize ## @item @qcode{@var{options}.Display = "off"} ## @item @qcode{@var{options}.MaxFunEvals = 400} ## @item @qcode{@var{options}.MaxIter = 200} ## @item @qcode{@var{options}.TolX = 1e-6} ## @end itemize ## @end itemize ## ## Further information about the normal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Normal_distribution} ## ## @seealso{normcdf, norminv, normpdf, normrnd, normlike, normstat} ## @end deftypefn function [muhat, sigmahat, muci, sigmaci] = normfit (x, alpha, censor, freq, options) ## Check for valid number of input arguments narginchk (1, 5); ## Check X for being a vector or a matrix if (ndims (x) != 2) error ("normfit: X must not be a multi-dimensional array."); endif if (! isvector (x)) if (nargin < 3) [n, ncols] = size (x); else error ("normfit: matrix data acceptable only under 2-arg syntax."); endif else n = numel (x); ncols = 1; endif ## Check alpha if (nargin < 2 || isempty (alpha)) alpha = 0.05; else if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("normfit: wrong value for ALPHA."); endif endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = 0; elseif (! isequal (size (x), size (censor))) error ("normfit: X and CENSOR vectors mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = 1; elseif (any (freq < 0)) error ("normfit: FREQ must not contain negative values."); elseif (isequal (size (x), size (freq))) n = sum (freq); is_zero = find (freq == 0); if (numel (is_zero) > 0) x(is_zero) = []; if (numel (censor) == numel (freq)) censor(is_zero) = []; endif freq(is_zero) = []; endif else error ("normfit: X and FREQ vectors mismatch."); endif ## Check options structure or add defaults if (nargin > 4 && ! isempty (options)) if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("normfit: 'options' 5th argument must be a", ... " structure with 'Display', 'MaxFunEvals',", ... " 'MaxIter', and 'TolX' fields present.")); endif else options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; endif ## Get number of censored and uncensored elements n_censored = sum (freq.*censor); # a scalar in all cases n_uncensored = n - n_censored; # a scalar in all cases ## Compute total sum in X totalsum = sum (freq.*x); ## Check cases that cannot make a fit. ## 1. Handle Infs and NaNs if (! isfinite (totalsum)) muhat = totalsum; sigmahat = NaN ('like', x); muci = NaN (2, 1, 'like', x); sigmaci = NaN (2, 1, 'like', x); return endif ## 2. All observations are censored or empty data if (n == 0 || n_uncensored == 0) muhat = NaN (1, ncols,'like',x); sigmahat = NaN (1, ncols,'like',x); muci = NaN (2, ncols,'like',x); sigmaci = NaN (2, ncols,'like',x); return endif ## 3. No censored values, compute parameter estimates explicitly. if (n_censored == 0) muhat = totalsum ./ n; if (n > 1) if numel (muhat) == 1 # X is a vector xc = x - muhat; else # X is a matrix xc = x - repmat (muhat, [n, 1]); endif sigmahat = sqrt (sum (conj (xc) .* xc .* freq) ./ (n - 1)); else sigmahat = zeros (1, ncols, 'like', x); endif if (nargout > 2) if (n > 1) paramhat = [muhat; sigmahat]; ci = norm_ci (paramhat, [], alpha, x, [], freq); muci = ci(:,:,1); sigmaci = ci(:,:,2); else muci = [-Inf; Inf] * ones (1, ncols, 'like', x); sigmaci = [0; Inf] * ones (1, ncols, 'like', x); endif endif return endif ## 4. All uncensored observations equal and greater than all the ## censored observations x_uncensored = x(censor == 0); range_x_uncensored = range (x_uncensored); if (range_x_uncensored < realmin (class (x))) if (x_uncensored(1) == max (x)) muhat = x_uncensored(1); sigmahat = zeros ('like',x); if (n_uncensored > 1) muci = [muhat; muhat]; sigmaci = zeros (2, 1, 'like', x); else muci = cast ([-Inf; Inf], 'like', x); sigmaci = cast ([0; Inf], 'like', x); endif return endif endif ## Get an initial estimate for parameters using the "least squares" method if (range_x_uncensored > 0) if (numel (freq) == numel (x)) [p,q] = ecdf (x, 'censoring', censor, 'frequency', freq); else [p,q] = ecdf (x, 'censoring', censor); endif pmid = (p(1:(end-1)) + p(2:end)) / 2; linefit = polyfit (-sqrt (2) * erfcinv (2 * pmid), q(2:end), 1); paramhat = linefit([2 1]); else # only one uncensored element in X paramhat = [x_uncensored(1) 1]; endif ## Optimize the parameters as doubles, regardless of input data type paramhat = cast (paramhat, 'double'); ## Search for parameter that minimizes the negative log likelihood function [paramhat, ~, err, output] = fminsearch ... (@(ph) norm_nlogl (ph, x, censor, freq), paramhat, options); ## Handle errors if (err == 0) if (output.funcCount >= options.MaxFunEvals) warning ("normfit: maximum number of function evaluations are exceeded."); elseif (output.iterations >= options.MaxIter) warning ("normfit: maximum number of iterations are exceeded."); endif elseif (err < 0) error ("normfit: NoSolution."); endif ## Make sure the outputs match the input data type muhat = cast (paramhat(1), 'like', x); sigmahat = cast (paramhat(2), 'like', x); if (nargout > 2) paramhat = paramhat(:); if (numel (freq) == numel (x)) [~, avar] = normlike (paramhat, x, censor, freq); else [~, avar] = normlike (paramhat, x, censor); endif ci = norm_ci (paramhat, avar, alpha, x, censor, freq); muci = ci(:,:,1); sigmaci = ci(:,:,2); endif endfunction ## Negative log-likelihood function and gradient for normal distribution. function [nlogL, avar] = norm_nlogl (params, x, censor, freq) ## Get mu and sigma values mu = params(1); sigma = params(2); ## Compute the individual log-likelihood terms. Force a log(0)==-Inf for ## data from extreme right tail, instead of getting exp(Inf-Inf)==NaN. z = (x - mu) ./ sigma; L = -0.5 .* z .^ 2 - log (sqrt (2 .* pi) .* sigma); if (any (censor)) censored = censor == 1; z_censor = z(censored); S_censor = 0.5 * erfc (z_censor / sqrt (2)); L(censored) = log (S_censor); endif ## Neg-log-like is the sum of the individual contributions nlogL = -sum (freq .* L); ## Compute the negative hessian at the parameter values. ## Invert to get the observed information matrix. if (nargout == 2) dL11 = -ones (size (z), class (z)); dL12 = -2 .* z; dL22 = 1 - 3 .* z .^ 2; if (any (censor)) dlogScen = exp (-0.5 .* z_censor .^ 2) ./ (sqrt (2 * pi) .* S_censor); d2logScen = dlogScen .* (dlogScen - z_censor); dL11(censored) = -d2logScen; dL12(censored) = -dlogScen - z_censor .* d2logScen; dL22(censored) = -z_censor .* (2 .* dlogScen + z_censor .* d2logScen); endif nH11 = -sum (freq .* dL11); nH12 = -sum (freq .* dL12); nH22 = -sum (freq .* dL22); avar = (sigma .^ 2) * [nH22, -nH12; -nH12, nH11] / ... (nH11 * nH22 - nH12 * nH12); endif endfunction ## Confidence intervals for normal distribution function ci = norm_ci (paramhat, cv, alpha, x, censor, freq) ## Check for missing input arguments if (nargin < 6 || isempty (freq)) freq = ones (size (x)); endif if (nargin < 5 || isempty (censor)) censor = false (size (x)); endif if (isvector (paramhat)) paramhat = paramhat(:); endif muhat = paramhat(1,:); sigmahat = paramhat(2,:); ## Get number of elements if (isempty (freq) || isequal (freq, 1)) if (isvector (x)) n = length (x); else n = size (x, 1); endif else n = sum (freq); endif ## Get number of censored and uncensored elements n_censored = sum (freq .* censor); n_uncensored = n - n_censored; ## Just in case if (any (censor) && (n == 0 || n_uncensored == 0 || ! isfinite (paramhat(1)))) ## X is a vector muci = NaN (2,1); sigmaci = NaN (2,1); ci = cast (cat (3, muci, sigmaci), 'like', x); return endif ## Get confidence intervals for each parameter if ((isempty (censor) || ! any (censor(:))) && ! isequal (cv,zeros (2,2))) ## Use exact formulas tcrit = tinv ([alpha/2, 1-alpha/2], n-1); muci = [muhat+tcrit(1)*sigmahat/sqrt(n); muhat+tcrit(2)*sigmahat/sqrt(n)]; chi2crit = chi2inv ([alpha/2, 1-alpha/2], n-1); sigmaci = [sigmahat*sqrt((n-1)./chi2crit(2)); ... sigmahat*sqrt((n-1)./chi2crit(1))]; else ## Use normal approximation probs = [alpha/2; 1-alpha/2]; se = sqrt (diag (cv))'; z = norminv (probs); ## Compute the CI for mu using a normal distribution for muhat. muci = muhat + se(1) .* z; ## Compute the CI for sigma using a normal approximation for ## log(sigmahat), and transform back to the original scale. logsigci = log (sigmahat) + (se(2) ./ sigmahat) .* z; sigmaci = exp (logsigci); endif ## Return as a single array ci = cat (3, muci, sigmaci); endfunction %!demo %! ## Sample 3 populations from 3 different normal distributions %! rng (42); %! r1 = normrnd (2, 5, 5000, 1); %! r2 = normrnd (5, 2, 5000, 1); %! r3 = normrnd (9, 4, 5000, 1); %! r = [r1, r2, r3]; %! %! ## Plot them normalized and fix their colors %! hist (r, 15, 0.4); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! set (h(3), 'facecolor', 'r'); %! hold on %! %! ## Estimate their mu and sigma parameters %! [muhat, sigmahat] = normfit (r); %! %! ## Plot their estimated PDFs %! x = [min(r(:)):max(r(:))]; %! y = normpdf (x, muhat(1), sigmahat(1)); %! plot (x, y, '-pr'); %! y = normpdf (x, muhat(2), sigmahat(2)); %! plot (x, y, '-sg'); %! y = normpdf (x, muhat(3), sigmahat(3)); %! plot (x, y, '-^c'); %! ylim ([0, 0.5]) %! xlim ([-20, 20]) %! hold off %! legend ({'Normalized HIST of sample 1 with mu=2, σ=5', ... %! 'Normalized HIST of sample 2 with mu=5, σ=2', ... %! 'Normalized HIST of sample 3 with mu=9, σ=4', ... %! sprintf("PDF for sample 1 with estimated mu=%0.2f and σ=%0.2f", ... %! muhat(1), sigmahat(1)), ... %! sprintf("PDF for sample 2 with estimated mu=%0.2f and σ=%0.2f", ... %! muhat(2), sigmahat(2)), ... %! sprintf("PDF for sample 3 with estimated mu=%0.2f and σ=%0.2f", ... %! muhat(3), sigmahat(3))}, 'location', 'northwest') %! title ('Three population samples from different normal distributions') %! hold off ## Test output %!test %! load lightbulb %! idx = find (lightbulb(:,2) == 0); %! censoring = lightbulb(idx,3) == 1; %! [muHat, sigmaHat] = normfit (lightbulb(idx,1), [], censoring); %! assert_equal (muHat, 9496.59586737857, 1e-11); %! assert_equal (sigmaHat, 3064.021012796456, 2e-12); %!test %! randn ('seed', 234); %! x = normrnd (3, 5, [1000, 1]); %! [muHat, sigmaHat, muCI, sigmaCI] = normfit (x, 0.01); %! assert_equal (muCI(1) < 3, true); %! assert_equal (muCI(2) > 3, true); %! assert_equal (sigmaCI(1) < 5, true); %! assert_equal (sigmaCI(2) > 5, true); ## Test input validation %!error ... %! normfit (ones (3,3,3)) %!error ... %! normfit (ones (20,3), [], zeros (20,1)) %!error normfit (ones (20,1), 0) %!error normfit (ones (20,1), -0.3) %!error normfit (ones (20,1), 1.2) %!error normfit (ones (20,1), [0.05 0.1]) %!error normfit (ones (20,1), 0.02+i) %!error ... %! normfit (ones (20,1), [], zeros (15,1)) %!error ... %! normfit (ones (20,1), [], zeros (20,1), ones (25,1)) %!error ... %! normfit (ones (5,1), [], zeros (5,1), [1, 2, 1, 2, -1]') %!error normfit (ones (20,1), [], zeros (20,1), ones (20,1), 'options') statistics-release-2.1.2/inst/Distribution_Fitting/normlike.m000066400000000000000000000154551527311104300244510ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} normlike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{avar}] =} normlike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} normlike (@var{params}, @var{x}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} normlike (@var{params}, @var{x}, @var{censor}, @var{freq}) ## ## Negative log-likelihood for the normal distribution. ## ## @code{@var{nlogL} = normlike (@var{params}, @var{x})} returns the negative ## log-likelihood for the normal distribution, evaluated at parameters ## @var{params(1)} = mean and @var{params(2)} = standard deviation, given ## @var{x}. @var{nlogL} is a scalar. ## ## @code{[@var{nlogL}, @var{avar}] = normlike (@var{params}, @var{x})} ## returns the inverse of Fisher's information matrix, @var{avar}. If the input ## parameter values in @var{params} are the maximum likelihood estimates, the ## diagonal elements of @var{avar} are their asymptotic variances. @var{avar} ## is based on the observed Fisher's information, not the expected information. ## ## @code{[@dots{}] = normlike (@var{params}, @var{x}, @var{censor})} accepts ## a boolean vector of the same size as @var{x} that is 1 for observations ## that are right-censored and 0 for observations that are observed exactly. ## ## @code{[@dots{}] = normlike (@var{params}, @var{x}, @var{censor}, ## @var{freq})} accepts a frequency vector of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it may contain any non-integer non-negative ## values. Pass in [] for @var{censor} to use its default value. ## ## Further information about the normal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Normal_distribution} ## ## @seealso{normcdf, norminv, normpdf, normrnd, normfit, normstat} ## @end deftypefn function [nlogL, avar] = normlike (params, x, censor, freq) ## Check input arguments if (nargin < 2) error ("normlike: too few input arguments."); endif if (! isvector (x)) error ("normlike: X must be a vector."); endif if (numel (params) != 2) error ("normlike: PARAMS must be a two-element vector."); endif if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("normlike: X and CENSOR vectors mismatch."); endif if nargin < 4 || isempty (freq) freq = ones (size (x)); elseif (any (freq < 0)) error ("normlike: FREQ must not contain negative values."); elseif (isequal (size (x), size (freq))) nulls = find (freq == 0); if (numel (nulls) > 0) x(nulls) = []; censor(nulls) = []; freq(nulls) = []; endif else error ("normlike: X and FREQ vectors mismatch."); endif ## Get mu and sigma values mu = params(1); sigma = params(2); ## sigma must be positive, otherwise make it NaN if (sigma <= 0) sigma = NaN; endif ## Compute the individual log-likelihood terms. Force a log(0)==-Inf for ## x from extreme right tail, instead of getting exp(Inf-Inf)==NaN. z = (x - mu) ./ sigma; L = -0.5 .* z .^ 2 - log (sqrt (2 .* pi) .* sigma); if (any (censor)) censored = censor == 1; z_censor = z(censored); S_censor = 0.5 * erfc (z_censor / sqrt (2)); L(censored) = log (S_censor); endif ## Neg-log-like is the sum of the individual contributions nlogL = -sum (freq .* L); ## Compute the negative hessian at the parameter values. ## Invert to get the observed information matrix. if (nargout == 2) dL11 = -ones (size (z), class (z)); dL12 = -2 .* z; dL22 = 1 - 3 .* z .^ 2; if (any (censor)) dlogScen = exp (-0.5 .* z_censor .^ 2) ./ (sqrt (2 * pi) .* S_censor); d2logScen = dlogScen .* (dlogScen - z_censor); dL11(censored) = -d2logScen; dL12(censored) = -dlogScen - z_censor .* d2logScen; dL22(censored) = -z_censor .* (2 .* dlogScen + z_censor .* d2logScen); endif nH11 = -sum (freq .* dL11); nH12 = -sum (freq .* dL12); nH22 = -sum (freq .* dL22); avar = (sigma .^ 2) * [nH22, -nH12; -nH12, nH11] / ... (nH11 * nH22 - nH12 * nH12); endif endfunction ## Test input validation %!error normlike ([12, 15]); %!error normlike ([12, 15], ones (2)); %!error ... %! normlike ([12, 15, 3], [1:50]); %!error ... %! normlike ([12, 15], [1:50], [1, 2, 3]); %!error ... %! normlike ([12, 15], [1:50], [], [1, 2, 3]); %!error ... %! normlike ([12, 15], [1:5], [], [1, 2, 3, 2, -1]); ## Results compared with Matlab %!test %! x = 1:50; %! [nlogL, avar] = normlike ([2.3, 1.2], x); %! avar_out = [7.5767e-01, -1.8850e-02; -1.8850e-02, 4.8750e-04]; %! assert_equal (nlogL, 13014.95883783327, 1e-10); %! assert_equal (avar, avar_out, 1e-4); %!test %! x = 1:50; %! [nlogL, avar] = normlike ([2.3, 1.2], x * 0.5); %! avar_out = [3.0501e-01, -1.5859e-02; -1.5859e-02, 9.1057e-04]; %! assert_equal (nlogL, 2854.802587833265, 1e-10); %! assert_equal (avar, avar_out, 1e-4); %!test %! x = 1:50; %! [nlogL, avar] = normlike ([21, 15], x); %! avar_out = [5.460474308300396, -1.600790513833993; ... %! -1.600790513833993, 2.667984189723321]; %! assert_equal (nlogL, 206.738325604233, 1e-12); %! assert_equal (avar, avar_out, 1e-14); %!test %! x = 1:50; %! censor = ones (1, 50); %! censor([2, 4, 6, 8, 12, 14]) = 0; %! [nlogL, avar] = normlike ([2.3, 1.2], x, censor); %! avar_out = [3.0501e-01, -1.5859e-02; -1.5859e-02, 9.1057e-04]; %! assert_equal (nlogL, Inf); %! assert_equal (avar, [NaN, NaN; NaN, NaN]); %!test %! x = 1:50; %! censor = ones (1, 50); %! censor([2, 4, 6, 8, 12, 14]) = 0; %! [nlogL, avar] = normlike ([21, 15], x, censor); %! avar_out = [24.4824488866131, -10.6649544179636; ... %! -10.6649544179636, 6.22827849965737]; %! assert_equal (nlogL, 86.9254371829733, 1e-12); %! assert_equal (avar, avar_out, 8e-14); statistics-release-2.1.2/inst/Distribution_Fitting/poissfit.m000066400000000000000000000143511527311104300244630ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{lambdahat} =} poissfit (@var{x}) ## @deftypefnx {statistics} {[@var{lambdahat}, @var{lambdaci}] =} poissfit (@var{x}) ## @deftypefnx {statistics} {[@var{lambdahat}, @var{lambdaci}] =} poissfit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@var{lambdahat}, @var{lambdaci}] =} poissfit (@var{x}, @var{alpha}, @var{freq}) ## ## Estimate parameter and confidence intervals for the Poisson distribution. ## ## @code{@var{lambdahat} = poissfit (@var{x})} returns the maximum likelihood ## estimate of the rate parameter, @var{lambda}, of the Poisson distribution ## given the data in @var{x}. @var{x} must be a vector of non-negative values. ## ## @code{[@var{lambdahat}, @var{lambdaci}] = poissfit (@var{x})} returns the 95% ## confidence intervals for the parameter estimate. ## ## @code{[@var{lambdahat}, @var{lambdaci}] = poissfit (@var{x}, @var{alpha})} ## also returns the @qcode{100 * (1 - @var{alpha})} percent confidence intervals ## of the estimated parameter. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = poissfit (@var{x}, @var{alpha}, @var{freq})} accepts a ## frequency vector or matrix, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}. @var{freq} cannot contain negative values. ## ## Further information about the Poisson distribution can be found at ## @url{https://en.wikipedia.org/wiki/Poisson_distribution} ## ## @seealso{poisscdf, poissinv, poisspdf, poissrnd, poisslike, poisstat} ## @end deftypefn function [lambdahat, lambdaci] = poissfit (x, alpha, freq) ## Check input arguments if (any (x < 0)) error ("poissfit: X cannot have negative values."); endif if (nargin < 2 || isempty (alpha)) alpha = 0.05; elseif (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("poissfit: wrong value for ALPHA."); endif if (nargin < 3 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("poissfit: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("poissfit: FREQ must not contain negative values."); endif if (isvector (x)) x = x(:); freq = freq(:); endif ## Compute lambdahat n = sum (freq, 1); lambdahat = double (sum (x .* freq) ./ n); ## Compute confidence intervals lambdasum = n .* lambdahat; ## Select elements for exact method or normal approximation k = (lambdasum < 100); if (any (k)) # exact method lb(k) = chi2inv (alpha / 2, 2 * lambdasum(k)) / 2; ub(k) = chi2inv (1 - alpha / 2, 2 * (lambdasum(k) + 1)) / 2; endif k = ! k; if (any (k)) # normal approximation lb(k) = norminv (alpha / 2, lambdasum(k), sqrt (lambdasum(k))); ub(k) = norminv (1 - alpha / 2, lambdasum(k), sqrt (lambdasum(k))); endif lambdaci = [lb; ub] / n; endfunction %!demo %! ## Sample 3 populations from 3 different Poisson distributions %! rng (42); %! randp ('state', 42); %! r1 = poissrnd (1, 1000, 1); %! r2 = poissrnd (4, 1000, 1); %! r3 = poissrnd (10, 1000, 1); %! r = [r1, r2, r3]; %! %! ## Plot them normalized and fix their colors %! hist (r, [0:20], 1); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! set (h(3), 'facecolor', 'r'); %! hold on %! %! ## Estimate their lambda parameter %! lambdahat = poissfit (r); %! %! ## Plot their estimated PDFs %! x = [0:20]; %! y = poisspdf (x, lambdahat(1)); %! plot (x, y, '-pr'); %! y = poisspdf (x, lambdahat(2)); %! plot (x, y, '-sg'); %! y = poisspdf (x, lambdahat(3)); %! plot (x, y, '-^c'); %! xlim ([0, 20]) %! ylim ([0, 0.4]) %! legend ({'Normalized HIST of sample 1 with λ=1', ... %! 'Normalized HIST of sample 2 with λ=4', ... %! 'Normalized HIST of sample 3 with λ=10', ... %! sprintf("PDF for sample 1 with estimated λ=%0.2f", ... %! lambdahat(1)), ... %! sprintf("PDF for sample 2 with estimated λ=%0.2f", ... %! lambdahat(2)), ... %! sprintf("PDF for sample 3 with estimated λ=%0.2f", ... %! lambdahat(3))}) %! title ('Three population samples from different Poisson distributions') %! hold off ## Test output %!test %! x = [1 3 2 4 5 4 3 4]; %! [lhat, lci] = poissfit (x); %! assert_equal (lhat, 3.25) %! assert_equal (lci, [2.123007901949543; 4.762003010390628], 1e-14) %!test %! x = [1 3 2 4 5 4 3 4]; %! [lhat, lci] = poissfit (x, 0.01); %! assert_equal (lhat, 3.25) %! assert_equal (lci, [1.842572740234582; 5.281369033298528], 1e-14) %!test %! x = [1 2 3 4 5]; %! f = [1 1 2 3 1]; %! [lhat, lci] = poissfit (x, [], f); %! assert_equal (lhat, 3.25) %! assert_equal (lci, [2.123007901949543; 4.762003010390628], 1e-14) %!test %! x = [1 2 3 4 5]; %! f = [1 1 2 3 1]; %! [lhat, lci] = poissfit (x, 0.01, f); %! assert_equal (lhat, 3.25) %! assert_equal (lci, [1.842572740234582; 5.281369033298528], 1e-14) ## Test input validation %!error poissfit ([1 2 -1 3]) %!error poissfit ([1 2 3], 0) %!error poissfit ([1 2 3], 1.2) %!error poissfit ([1 2 3], [0.02 0.05]) %!error %! poissfit ([1 2 3], [], [1 5]) %!error %! poissfit ([1 2 3], [], [1 5 -1]) statistics-release-2.1.2/inst/Distribution_Fitting/poisslike.m000066400000000000000000000076041527311104300246300ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} poisslike (@var{lambda}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{avar}] =} poisslike (@var{lambda}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} poisslike (@var{lambda}, @var{x}, @var{freq}) ## ## Negative log-likelihood for the Poisson distribution. ## ## @code{@var{nlogL} = poisslike (@var{lambda}, @var{x})} returns the negative ## log likelihood of the data in @var{x} corresponding to the Poisson ## distribution with rate parameter @var{lambda}. @var{x} must be a vector of ## non-negative values. ## ## @code{[@var{nlogL}, @var{avar}] = poisslike (@var{lambda}, @var{x})} also ## returns the inverse of Fisher's information matrix, @var{avar}. If the input ## rate parameter, @var{lambda}, is the maximum likelihood estimate, @var{avar} ## is its asymptotic variance. ## ## @code{[@dots{}] = poisslike (@var{lambda}, @var{x}, @var{freq})} accepts a ## frequency vector, @var{freq}, of the same size as @var{x}. @var{freq} ## typically contains integer frequencies for the corresponding elements in ## @var{x}, but it can contain any non-integer non-negative values. By default, ## or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## Further information about the Poisson distribution can be found at ## @url{https://en.wikipedia.org/wiki/Poisson_distribution} ## ## @seealso{poisscdf, poissinv, poisspdf, poissrnd, poissfit, poisstat} ## @end deftypefn function [nlogL, avar] = poisslike (lambda, x, freq) ## Check input arguments if (nargin < 2) error ("poisslike: function called with too few input arguments."); endif if (! isscalar (lambda) || ! isnumeric (lambda) || lambda <= 0) error ("poisslike: LAMBDA must be a positive scalar."); endif if (! isvector (x) || any (x < 0)) error ("poisslike: X must be a vector of non-negative values."); endif if (nargin < 3 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("poisslike: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("poisslike: FREQ must not contain negative values."); endif ## Compute negative log-likelihood and asymptotic covariance n = sum (freq); nlogL = - sum (freq .* log (poisspdf (x, lambda))); avar = lambda / n; endfunction ## Test output %!test %! x = [1 3 2 4 5 4 3 4]; %! [nlogL, avar] = poisslike (3.25, x); %! assert_equal (nlogL, 13.9533, 1e-4) %!test %! x = [1 2 3 4 5]; %! f = [1 1 2 3 1]; %! [nlogL, avar] = poisslike (3.25, x, f); %! assert_equal (nlogL, 13.9533, 1e-4) ## Test input validation %!error poisslike (1) %!error poisslike ([1 2 3], [1 2]) %!error ... %! poisslike (3.25, ones (10, 2)) %!error ... %! poisslike (3.25, [1 2 3 -4 5]) %!error ... %! poisslike (3.25, ones (10, 1), ones (8,1)) %!error ... %! poisslike (3.25, ones (1, 8), [1 1 1 1 1 1 1 -1]) statistics-release-2.1.2/inst/Distribution_Fitting/private/000077500000000000000000000000001527311104300241135ustar00rootroot00000000000000statistics-release-2.1.2/inst/Distribution_Fitting/private/__stable_pdf__.m000066400000000000000000000107221527311104300271720ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{y} =} __stable_pdf__ (@var{x}, @var{alpha}, @var{beta}, @var{gam}, @var{delta}) ## ## Fast stable probability density for the fitting functions. ## ## Computes the stable density in the Nolan @qcode{S0} parameterization at all ## the points in @var{x} by a single shared-grid inversion of the characteristic ## function. Unlike @code{stblpdf}, which invokes an adaptive @code{quadgk} per ## point, the characteristic function is evaluated once on a common quadrature ## grid and combined against every data point at once, which is roughly an order ## of magnitude faster. This makes it suitable as the inner density of the ## maximum-likelihood loop in @code{stblfit}/@code{stbllike}, where accuracy of a ## few parts in @math{10^{-8}} across the bulk is ample for the estimator. ## ## The closed forms are used for the normal (@var{alpha} equal to @code{2}) and ## the Cauchy (@var{alpha} equal to @code{1} with @var{beta} equal to @code{0}) ## special cases. The parameters must be valid scalars; no checking is done ## here, as this is a private helper. ## ## @seealso{stblpdf, stblfit, stbllike} ## @end deftypefn function y = __stable_pdf__ (x, alpha, beta, gam, delta) z = (x - delta) ./ gam; y = nan (size (z)); ok = ! isnan (z); zk = z(ok)(:); if (isempty (zk)) return; endif if (alpha == 2) ## Normal with variance 2 v = exp (-zk .^ 2 ./ 4) ./ (2 .* sqrt (pi)); elseif (alpha == 1 && beta == 0) ## Cauchy v = 1 ./ (pi .* (1 + zk .^ 2)); else v = cf_invert (zk, alpha, beta); endif y(ok) = max (v, 0) ./ gam; endfunction ## Density of the standard stable S(alpha, beta, 1, 0) at the points Z by ## Gil-Pelaez inversion of the characteristic function on a shared grid. function v = cf_invert (z, alpha, beta) ## Upper limit: |phi(t)| = exp (-t^alpha), so t^alpha = L makes it negligible. L = 30; # exp (-30) ~ 9e-14 T = L ^ (1 / alpha); ## Uniform grid. The inversion integrand is the slowly decaying envelope ## |phi(t)| = exp (-t^alpha) times the oscillation exp (-i t z), whose ## frequency |z| is constant in t; a uniform mesh therefore resolves it most ## efficiently. The node count scales with the peak total phase over [0, T], ## i.e. with the largest |z| in the data, with a floor that also covers the ## mild t^alpha cusp at the origin when alpha < 1. The driving |z| is capped: ## far-tail points carry negligible density, so leaving their oscillation ## under-resolved keeps N bounded for heavy-tailed samples without affecting ## the likelihood. zmax = min (max (abs (z)), 30); N = ceil (6 * (zmax + 1) * T) + 512; N = min (N, 200000); t = linspace (0, T, N)'; phi = __stable_cf__ (t, alpha, beta); ## Uniform trapezoidal weights dt = T / (N - 1); w = dt .* ones (N, 1); w(1) /= 2; w(end) /= 2; ## Vectorized inversion: real (exp (-i t z') .* phi) integrated over t, per z M = real (exp (-1i .* (t * z(:)')) .* phi); v = (w' * M)' ./ pi; endfunction ## Characteristic function of the standard stable S(alpha, beta, 1, 0) in the ## Nolan S0 parameterization. Local copy of Distribution_Functions/private/ ## __stable_cf__.m, which stblpdf and stblcdf need and a private/ directory ## cannot share across folders. A closed form -- keep the two identical. function phi = __stable_cf__ (t, alpha, beta) at = abs (t); st = sign (t); if (abs (alpha - 1) < eps) ## The |t|*log|t| term vanishes at the origin lt = at .* log (at); lt(at == 0) = 0; phi = exp (-at - 1i .* beta .* (2 ./ pi) .* st .* lt); else phi = exp (-at .^ alpha - 1i .* beta .* tan (pi .* alpha ./ 2) ... .* st .* (at - at .^ alpha)); endif endfunction statistics-release-2.1.2/inst/Distribution_Fitting/raylfit.m000066400000000000000000000170341527311104300242760ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{sigmaA} =} raylfit (@var{x}) ## @deftypefnx {statistics} {[@var{sigmaA}, @var{sigmaci}] =} raylfit (@var{x}) ## @deftypefnx {statistics} {[@var{sigmaA}, @var{sigmaci}] =} raylfit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@var{sigmaA}, @var{sigmaci}] =} raylfit (@var{x}, @var{alpha}, @var{censor}) ## @deftypefnx {statistics} {[@var{sigmaA}, @var{sigmaci}] =} raylfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}) ## ## Estimate parameter and confidence intervals for the Rayleigh distribution. ## ## @code{@var{sigmaA} = raylfit (@var{x})} returns the maximum likelihood ## estimate of the rate parameter, @var{lambda}, of the Rayleigh distribution ## given the data in @var{x}. @var{x} must be a vector of non-negative values. ## ## @code{[@var{sigmaA}, @var{sigmaci}] = raylfit (@var{x})} returns the 95% ## confidence intervals for the parameter estimate. ## ## @code{[@var{sigmaA}, @var{sigmaci}] = raylfit (@var{x}, @var{alpha})} ## also returns the @qcode{100 * (1 - @var{alpha})} percent confidence intervals ## of the estimated parameter. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = raylfit (@var{x}, @var{alpha}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = raylfit (@var{x}, @var{alpha}, @var{censor}, @var{freq})} ## accepts a frequency vector or matrix, @var{freq}, of the same size as ## @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}. @var{freq} cannot contain negative values. ## ## Further information about the Rayleigh distribution can be found at ## @url{https://en.wikipedia.org/wiki/Rayleigh_distribution} ## ## The @code{prob.RayleighDistribution} class names this same parameter ## @qcode{B}, after MATLAB. ## @seealso{raylcdf, raylinv, raylpdf, raylrnd, rayllike, raylstat} ## @end deftypefn function [sigmaA, sigmaci] = raylfit (x, alpha, censor, freq) ## Check input arguments if (any (x < 0)) error ("raylfit: X cannot have negative values."); endif if (! isvector (x)) error ("raylfit: X must be a vector."); endif ## Check alpha if (nargin < 2 || isempty (alpha)) alpha = 0.05; elseif (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("raylfit: wrong value for ALPHA."); endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("raylfit: X and CENSOR vectors mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("raylfit: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("raylfit: FREQ must not contain negative values."); endif ## Remove any censored data censored = censor == 1; freq(censored) = []; x(censored) = []; ## Expand frequency vector (if necessary) if (! all (freq == 1)) xf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; endfor x = xf; endif ## Compute sigmaA sigmaA = sqrt (0.5 * mean (x .^ 2)); ## Compute confidence intervals (based on chi-squared) if (nargout > 1) sx = 2 * numel (x); ci = [1-alpha/2; alpha/2]; sigmaci = sqrt (sx * sigmaA .^ 2 ./ chi2inv (ci, sx)); endif endfunction %!demo %! ## Sample 3 populations from 3 different Rayleigh distributions %! rng (42); %! r1 = raylrnd (1, 1000, 1); %! r2 = raylrnd (2, 1000, 1); %! r3 = raylrnd (4, 1000, 1); %! r = [r1, r2, r3]; %! %! ## Plot them normalized and fix their colors %! hist (r, [0.5:0.5:10.5], 2); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! set (h(3), 'facecolor', 'r'); %! hold on %! %! ## Estimate their lambda parameter %! sigmaA = raylfit (r(:,1)); %! sigmaB = raylfit (r(:,2)); %! sigmaC = raylfit (r(:,3)); %! %! ## Plot their estimated PDFs %! x = [0:0.1:10]; %! y = raylpdf (x, sigmaA); %! plot (x, y, '-pr'); %! y = raylpdf (x, sigmaB); %! plot (x, y, '-sg'); %! y = raylpdf (x, sigmaC); %! plot (x, y, '-^c'); %! xlim ([0, 10]) %! ylim ([0, 0.7]) %! legend ({'Normalized HIST of sample 1 with σ=1', ... %! 'Normalized HIST of sample 2 with σ=2', ... %! 'Normalized HIST of sample 3 with σ=4', ... %! sprintf("PDF for sample 1 with estimated σ=%0.2f", ... %! sigmaA), ... %! sprintf("PDF for sample 2 with estimated σ=%0.2f", ... %! sigmaB), ... %! sprintf("PDF for sample 3 with estimated σ=%0.2f", ... %! sigmaC)}) %! title ('Three population samples from different Rayleigh distributions') %! hold off ## Test output %!test %! x = [1 3 2 4 5 4 3 4]; %! [shat, sci] = raylfit (x); %! assert_equal (shat, 2.4495, 1e-4) %! assert_equal (sci, [1.8243; 3.7279], 1e-4) %!test %! x = [1 3 2 4 5 4 3 4]; %! [shat, sci] = raylfit (x, 0.01); %! assert_equal (shat, 2.4495, 1e-4) %! assert_equal (sci, [1.6738; 4.3208], 1e-4) %!test %! x = [1 2 3 4 5]; %! f = [1 1 2 3 1]; %! [shat, sci] = raylfit (x, [], [], f); %! assert_equal (shat, 2.4495, 1e-4) %! assert_equal (sci, [1.8243; 3.7279], 1e-4) %!test %! x = [1 2 3 4 5]; %! f = [1 1 2 3 1]; %! [shat, sci] = raylfit (x, 0.01, [], f); %! assert_equal (shat, 2.4495, 1e-4) %! assert_equal (sci, [1.6738; 4.3208], 1e-4) %!test %! x = [1 2 3 4 5 6]; %! c = [0 0 0 0 0 1]; %! f = [1 1 2 3 1 1]; %! [shat, sci] = raylfit (x, 0.01, c, f); %! assert_equal (shat, 2.4495, 1e-4) %! assert_equal (sci, [1.6738; 4.3208], 1e-4) ## Test input validation %!error raylfit (ones (2,5)); %!error raylfit ([1 2 -1 3]) %!error raylfit ([1 2 3], 0) %!error raylfit ([1 2 3], 1.2) %!error raylfit ([1 2 3], [0.02 0.05]) %!error ... %! raylfit ([1, 2, 3, 4, 5], 0.05, [1 1 0]); %!error ... %! raylfit ([1, 2, 3, 4, 5], [], [1 1 0 1 1]'); %!error ... %! raylfit ([1, 2, 3, 4, 5], 0.05, zeros (1,5), [1 1 0]); %!error ... %! raylfit ([1, 2, 3, 4, 5], [], [], [1 1 0 1 1]'); %!error %! raylfit ([1 2 3], [], [], [1 5]) %!error %! raylfit ([1 2 3], [], [], [1 5 -1]) statistics-release-2.1.2/inst/Distribution_Fitting/rayllike.m000066400000000000000000000121071527311104300244340ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} rayllike (@var{sigma}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} rayllike (@var{sigma}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} rayllike (@var{sigma}, @var{x}, @var{freq}) ## ## Negative log-likelihood for the Rayleigh distribution. ## ## @code{@var{nlogL} = rayllike (@var{sigma}, @var{x})} returns the negative ## log likelihood of the data in @var{x} corresponding to the Rayleigh ## distribution with rate parameter @var{sigma}. @var{x} must be a vector of ## non-negative values. ## ## @code{[@var{nlogL}, @var{acov}] = rayllike (@var{sigma}, @var{x})} also ## returns the inverse of Fisher's information matrix, @var{acov}. If the input ## rate parameter, @var{sigma}, is the maximum likelihood estimate, @var{acov} ## is its asymptotic variance. ## ## @code{[@dots{}] = rayllike (@var{sigma}, @var{x}, @var{freq})} accepts a ## frequency vector, @var{freq}, of the same size as @var{x}. @var{freq} ## typically contains integer frequencies for the corresponding elements in ## @var{x}, but it can contain any non-integer non-negative values. By default, ## or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## Further information about the Rayleigh distribution can be found at ## @url{https://en.wikipedia.org/wiki/Rayleigh_distribution} ## ## The @code{prob.RayleighDistribution} class names this same parameter ## @qcode{B}, after MATLAB. ## @seealso{raylcdf, raylinv, raylpdf, raylrnd, raylfit, raylstat} ## @end deftypefn function [nlogL, acov] = rayllike (sigma, x, censor, freq) ## Check input arguments if (nargin < 2) error ("rayllike: function called with too few input arguments."); endif if (! isscalar (sigma) || ! isnumeric (sigma) || sigma <= 0) error ("rayllike: SIGMA must be a positive scalar."); endif if (! isvector (x) || any (x < 0)) error ("rayllike: X must be a vector of non-negative values."); endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("rayllike: X and CENSOR vectors mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("rayllike: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("rayllike: FREQ must not contain negative values."); endif ## Compute negative log-likelihood and asymptotic covariance zsq = (x / sigma) .^ 2; logfz = -2 * log (sigma) - zsq / 2 + log (x); dlogfz = (zsq - 2) / sigma; logS = - zsq / 2; dlogS = - zsq * 3 / sigma ^ 2; logfz(censor == 1) = logS(censor == 1); nlogL = - sum (freq .* logfz); if (nargout > 1) d2 = (2 - 3 * zsq) / sigma ^ 2; d2(censor == 1) = - 3 * zsq(censor == 1) / sigma ^ 2; acov = - sum (freq .* d2); endif endfunction ## Test output %!test %! x = [1 3 2 4 5 4 3 4]; %! [nlogL, acov] = rayllike (3.25, x); %! assert_equal (nlogL, 14.7442, 1e-4) %!test %! x = [1 2 3 4 5]; %! f = [1 1 2 3 1]; %! [nlogL, acov] = rayllike (3.25, x, [], f); %! assert_equal (nlogL, 14.7442, 1e-4) %!test %! x = [1 2 3 4 5 6]; %! f = [1 1 2 3 1 0]; %! [nlogL, acov] = rayllike (3.25, x, [], f); %! assert_equal (nlogL, 14.7442, 1e-4) %!test %! x = [1 2 3 4 5 6]; %! c = [0 0 0 0 0 1]; %! f = [1 1 2 3 1 0]; %! [nlogL, acov] = rayllike (3.25, x, c, f); %! assert_equal (nlogL, 14.7442, 1e-4) ## Test input validation %!error rayllike (1) %!error rayllike ([1 2 3], [1 2]) %!error ... %! rayllike (3.25, ones (10, 2)) %!error ... %! rayllike (3.25, [1 2 3 -4 5]) %!error ... %! rayllike (3.25, [1, 2, 3, 4, 5], [1 1 0]); %!error ... %! rayllike (3.25, [1, 2, 3, 4, 5], [1 1 0 1 1]'); %!error ... %! rayllike (3.25, [1, 2, 3, 4, 5], zeros (1,5), [1 1 0]); %!error ... %! rayllike (3.25, [1, 2, 3, 4, 5], [], [1 1 0 1 1]'); %!error ... %! rayllike (3.25, ones (1, 8), [], [1 1 1 1 1 1 1 -1]) statistics-release-2.1.2/inst/Distribution_Fitting/ricefit.m000066400000000000000000000255571527311104300242620ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{paramhat} =} ricefit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} ricefit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} ricefit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@dots{}] =} ricefit (@var{x}, @var{alpha}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} ricefit (@var{x}, @var{alpha}, @var{censor}, @var{freq}) ## @deftypefnx {statistics} {[@dots{}] =} ricefit (@var{x}, @var{alpha}, @var{censor}, @var{freq}, @var{options}) ## ## Estimate parameters and confidence intervals for the Rician distribution. ## ## @code{@var{paramhat} = ricefit (@var{x})} returns the maximum likelihood ## estimates of the parameters of the Rician distribution given the data in ## @var{x}. @qcode{@var{paramhat}(1)} is the non-centrality (distance) ## parameter, @math{s}, and @qcode{@var{paramhat}(2)} is the scale parameter, ## @math{sigma}. ## ## @code{[@var{paramhat}, @var{paramci}] = ricefit (@var{x})} returns the 95% ## confidence intervals for the parameter estimates. ## ## @code{[@dots{}] = ricefit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals for the ## parameter estimates. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = ricefit (@var{x}, @var{alpha}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = ricefit (@var{x}, @var{alpha}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## @code{[@dots{}] = ricefit (@dots{}, @var{options})} specifies control ## parameters for the iterative algorithm used to compute the maximum likelihood ## estimates. @var{options} is a structure with the following field and its ## default value: ## @itemize ## @item @qcode{@var{options}.Display = "off"} ## @item @qcode{@var{options}.MaxFunEvals = 1000} ## @item @qcode{@var{options}.MaxIter = 500} ## @item @qcode{@var{options}.TolX = 1e-6} ## @end itemize ## ## Further information about the Rician distribution can be found at ## @url{https://en.wikipedia.org/wiki/Rice_distribution} ## ## @seealso{ricecdf, ricepdf, riceinv, ricernd, ricelike, ricestat} ## @end deftypefn function [paramhat, paramci] = ricefit (x, alpha, censor, freq, options) ## Check input arguments if (! isvector (x)) error ("ricefit: X must be a vector."); endif ## Check alpha if (nargin < 2 || isempty (alpha)) alpha = 0.05; else if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("ricefit: wrong value for ALPHA."); endif endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("ricefit: X and CENSOR vectors mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("ricefit: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("ricefit: FREQ cannot have negative values."); endif ## Get options structure or add defaults if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("ricefit: 'options' 5th argument must be a", ... " structure with 'Display', 'MaxFunEvals',", ... " 'MaxIter', and 'TolX' fields present.")); endif endif ## Get sample size and data type cls = class (x); szx = sum (freq); ncen = sum (freq .* censor); nunc = szx - ncen; ## Check for illegal value in X if (any (x <= 0)) error ("ricefit: X must contain positive values."); endif ## Handle ill-conditioned cases: no data or all censored if (szx == 0 || nunc == 0 || any (! isfinite (x))) paramhat = nan (1, 2, cls); paramci = nan (2, cls); return endif ## Check for identical data in X if (! isscalar (x) && max (abs (diff (x)) ./ x(2:end)) <= sqrt (eps)) paramhat = cast ([Inf, 0], cls); paramci = cast ([Inf, 0; Inf, 0], cls); return endif ## Use 2nd and 4th Moment Estimators of uncensored data as starting point xsq_uncensored = x(censor == 0) .^ 2; meanxsq = mean (xsq_uncensored); meanx_4 = mean (xsq_uncensored .^ 2); if (meanxsq ^ 2 < meanx_4 && meanx_4 < 2 * meanxsq ^ 2) nu_4 = 2 * meanxsq ^ 2 - meanx_4; nusq = sqrt (nu_4); sigmasq = 0.5 * (meanxsq - nusq); params = cast ([sqrt(nusq), sqrt(sigmasq)], cls); else params = cast ([1, 1], cls); endif ## Minimize negative log-likelihood to estimate parameters f = @(params) ricelike (params, x, censor, freq); [paramhat, ~, err, output] = fminsearch (f, params, options); ## Force positive parameter values paramhat = abs (paramhat); ## Handle errors if (err == 0) if (output.funcCount >= options.MaxFunEvals) warning (strcat ("ricefit: maximum number of function", ... " evaluations are exceeded.")); elseif (output.iterations >= options.MaxIter) warning ("ricefit: maximum number of iterations are exceeded."); endif elseif (err < 0) error ("ricefit: no solution."); endif ## Compute CIs using a log normal approximation for parameters. if (nargout > 1) ## Compute asymptotic covariance [~, acov] = ricelike (paramhat, x, censor, freq); ## Get standard errors stderr = sqrt (diag (acov))'; stderr = stderr ./ paramhat; ## Apply log transform phatlog = log (paramhat); ## Compute normal quantiles z = probit (alpha / 2); ## Compute CI paramci = [phatlog; phatlog] + [stderr; stderr] .* [z, z; -z, -z]; ## Inverse log transform paramci = exp (paramci); endif endfunction %!demo %! ## Sample 3 populations from different Gamma distributions %! randg ('state', 42); %! randp ('state', 42); %! r1 = ricernd (1, 2, 3000, 1); %! r2 = ricernd (2, 4, 3000, 1); %! r3 = ricernd (7.5, 1, 3000, 1); %! r = [r1, r2, r3]; %! %! ## Plot them normalized and fix their colors %! hist (r, 75, 4); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! set (h(3), 'facecolor', 'r'); %! ylim ([0, 0.7]); %! xlim ([0, 12]); %! hold on %! %! ## Estimate their α and β parameters %! s_sigmaA = ricefit (r(:,1)); %! s_sigmaB = ricefit (r(:,2)); %! s_sigmaC = ricefit (r(:,3)); %! %! ## Plot their estimated PDFs %! x = [0.01,0.1:0.2:18]; %! y = ricepdf (x, s_sigmaA(1), s_sigmaA(2)); %! plot (x, y, '-pr'); %! y = ricepdf (x, s_sigmaB(1), s_sigmaB(2)); %! plot (x, y, '-sg'); %! y = ricepdf (x, s_sigmaC(1), s_sigmaC(2)); %! plot (x, y, '-^c'); %! hold off %! legend ({'Normalized HIST of sample 1 with s=1 and σ=2', ... %! 'Normalized HIST of sample 2 with s=2 and σ=4', ... %! 'Normalized HIST of sample 3 with s=7.5 and σ=1', ... %! sprintf("PDF for sample 1 with estimated s=%0.2f and σ=%0.2f", ... %! s_sigmaA(1), s_sigmaA(2)), ... %! sprintf("PDF for sample 2 with estimated s=%0.2f and σ=%0.2f", ... %! s_sigmaB(1), s_sigmaB(2)), ... %! sprintf("PDF for sample 3 with estimated s=%0.2f and σ=%0.2f", ... %! s_sigmaC(1), s_sigmaC(2))}) %! title ('Three population samples from different Rician distributions') %! hold off ## Test output %!test %! [paramhat, paramci] = ricefit ([1:50]); %! assert_equal (paramhat, [15.3057, 17.6668], 1e-4); %! assert_equal (paramci, [9.5468, 11.7802; 24.5383, 26.4952], 1e-4); %!test %! [paramhat, paramci] = ricefit ([1:50], 0.01); %! assert_equal (paramhat, [15.3057, 17.6668], 1e-4); %! assert_equal (paramci, [8.2309, 10.3717; 28.4615, 30.0934], 1e-4); %!test %! [paramhat, paramci] = ricefit ([1:5]); %! assert_equal (paramhat, [2.3123, 1.6812], 1e-4); %! assert_equal (paramci, [1.0819, 0.6376; 4.9424, 4.4331], 1e-4); %!test %! [paramhat, paramci] = ricefit ([1:5], 0.01); %! assert_equal (paramhat, [2.3123, 1.6812], 1e-4); %! assert_equal (paramci, [0.8521, 0.4702; 6.2747, 6.0120], 1e-4); %!test %! freq = [1 1 1 1 5]; %! [paramhat, paramci] = ricefit ([1:5], [], [], freq); %! assert_equal (paramhat, [3.5181, 1.5565], 1e-4); %! assert_equal (paramci, [2.5893, 0.9049; 4.7801, 2.6772], 1e-4); %!test %! censor = [1 0 0 0 0]; %! [paramhat, paramci] = ricefit ([1:5], [], censor); %! assert_equal (paramhat, [3.2978, 1.1527], 1e-4); %! assert_equal (paramci, [2.3192, 0.5476; 4.6895, 2.4261], 1e-4); ## Test class of input preserved %!assert_equal (class (ricefit (single ([1:50]))), "single") ## Test input validation %!error ricefit (ones (2)) %!error ricefit ([1:50], 1) %!error ricefit ([1:50], -1) %!error ricefit ([1:50], {0.05}) %!error ricefit ([1:50], 'k') %!error ricefit ([1:50], i) %!error ricefit ([1:50], [0.01 0.02]) %!error ricefit ([1:50], [], [1 1]) %!error ricefit ([1:50], [], [], [1 1]) %!error ... %! ricefit ([1:5], [], [], [1, 1, 2, 1, -1]) %!error ricefit ([1 2 3 -4]) %!error ricefit ([1 2 0], [], [1 0 0]) statistics-release-2.1.2/inst/Distribution_Fitting/ricelike.m000066400000000000000000000206031527311104300244070ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} ricelike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} ricelike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} ricelike (@var{params}, @var{x}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} ricelike (@var{params}, @var{x}, @var{censor}, @var{freq}) ## ## Negative log-likelihood for the Rician distribution. ## ## @code{@var{nlogL} = ricelike (@var{params}, @var{x})} returns the negative ## log likelihood of the data in @var{x} corresponding to the Rician ## distribution with (1) non-centrality (distance) parameter @math{s} and (2) ## scale parameter @math{sigma} given in the two-element vector @var{params}. ## ## @code{[@var{nlogL}, @var{acov}] = ricelike (@var{params}, @var{x})} also ## returns the inverse of Fisher's information matrix, @var{acov}. If the input ## parameter values in @var{params} are the maximum likelihood estimates, the ## diagonal elements of @var{params} are their asymptotic variances. ## ## @code{[@dots{}] = ricelike (@var{params}, @var{x}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = ricelike (@var{params}, @var{x}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## Further information about the Rician distribution can be found at ## @url{https://en.wikipedia.org/wiki/Rice_distribution} ## ## @seealso{ricecdf, riceinv, ricepdf, ricernd, ricefit, ricestat} ## @end deftypefn function [nlogL, acov] = ricelike (params, x, censor, freq) ## Check input arguments if (nargin < 2) error ("ricelike: function called with too few input arguments."); endif if (! isvector (x)) error ("ricelike: X must be a vector."); endif if (length (params) != 2) error ("ricelike: PARAMS must be a two-element vector."); endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("ricelike: X and CENSOR vector mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("ricelike: X and FREQ vector mismatch."); elseif (any (freq < 0)) error ("ricelike: FREQ must not contain negative values."); endif ## Expand frequency and censor vectors (if necessary) if (! all (freq == 1)) xf = []; cf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; cf = [cf, repmat(censor(i), 1, freq(i))]; endfor x = xf; freq = ones (size (x)); censor = cf; endif ## Get parameters nu = params(1); sigma = params(2); theta = nu ./ sigma; xsigma = x ./ sigma; xstheta = xsigma.*theta; I_0 = besseli (0, xstheta, 1); XNS = (xsigma .^ 2 + theta .^ 2) ./ 2; ## Compute log likelihood L = -XNS + log (I_0) + log (xsigma ./ sigma) + xstheta; ## Handle censored data n_censored = sum (freq .* censor); if (n_censored > 0) censored = (censor == 1); xsigma_censored = xsigma(censored); Q = marcumQ1 (theta, xsigma_censored); L(censored) = log (Q); endif ## Sum up the neg log likelihood nlogL = -sum (freq .* L); ## Compute asymptotic covariance if (nargout > 1) ## Compute first order central differences of the log-likelihood gradient dp = 0.0001 .* max (abs (params), 1); ngrad_p1 = rice_grad (params + [dp(1), 0], x, censor, freq); ngrad_m1 = rice_grad (params - [dp(1), 0], x, censor, freq); ngrad_p2 = rice_grad (params + [0, dp(2)], x, censor, freq); ngrad_m2 = rice_grad (params - [0, dp(2)], x, censor, freq); ## Compute negative Hessian by normalizing the differences by the increment nH = [(ngrad_p1(:) - ngrad_m1(:))./(2 * dp(1)), ... (ngrad_p2(:) - ngrad_m2(:))./(2 * dp(2))]; ## Force neg Hessian being symmetric nH = 0.5 .* (nH + nH'); ## Check neg Hessian is positive definite [R, p] = chol (nH); if (p > 0) warning ("ricelike: non positive definite Hessian matrix."); acov = NaN (2); return endif ## ACOV estimate is the negative inverse of the Hessian. Rinv = inv (R); acov = Rinv * Rinv; endif endfunction ## Helper function for computing negative gradient function ngrad = rice_grad (params, x, censor, freq) ## Get parameters nu = params(1); sigma = params(2); theta = nu ./ sigma; xsigma = x ./ sigma; xstheta = xsigma.*theta; I_0 = besseli (0, xstheta, 1); XNS = (xsigma .^ 2 + theta .^ 2) ./ 2; ## Compute derivatives I_1 = besseli (1, xstheta, 1); dII = I_1 ./ I_0; dL1 = (-theta + dII .* xsigma) ./ sigma; dL2 = -2 * (1 - XNS + dII .* xstheta) ./ sigma; ## Handle censored data n_censored = sum (freq .* censor); if (n_censored > 0) censored = (censor == 1); xsigma_censored = xsigma(censored); Q = marcumQ1 (theta, xsigma_censored); expt = exp (-XNS(censored) + xstheta(censored)); dQdtheta = xsigma_censored .* I_1(censored) .* expt; dQdz = -xsigma_censored .* I_0(censored) .* expt; dtheta1 = 1 ./ sigma; dtheta2 = -theta ./ sigma; dz2 = -xsigma_censored ./ sigma; dL1(censored) = dQdtheta .* dtheta1 ./ Q; dL2(censored) = (dQdtheta .* dtheta2 + dQdz .* dz2) ./ Q; endif ## Compute gradient ngrad = -[sum(freq .* dL1) sum(freq .* dL2)]; endfunction ## Marcum's "Q" function of order 1 function Q = marcumQ1 (a, b) ## Prepare output matrix if (isa (a, 'single') || isa (b, 'single')) Q = NaN (size (b), 'single'); else Q = NaN (size (b)); endif ## Force marginal cases Q(a != Inf & b == 0) = 1; Q(a != Inf & b == Inf) = 0; Q(a == Inf & b != Inf) = 1; z = isnan (Q) & a == 0 & b != Inf; if (any (z)) Q(z) = exp ((-b(z) .^ 2) ./ 2); endif ## Compute the remaining cases z = isnan (Q) & ! isnan (a) & ! isnan (b); if (any (z(:))) aa = (a(z) .^ 2) ./ 2; bb = (b(z) .^ 2) ./ 2; eA = exp (-aa); eB = bb .* exp (-bb); h = eA; d = eB .* h; s = d; j = (d > s.*eps (class (d))); k = 1; while (any (j)) eA = aa .* eA ./ k; h = h + eA; eB = bb .* eB ./ (k + 1); d = eB .* h; s(j) = s(j) + d(j); j = (d > s .* eps (class (d))); k = k + 1; endwhile Q(z) = 1 - s; endif endfunction ## Test output %!test %! nlogL = ricelike ([15.3057344, 17.6668458], [1:50]); %! assert_equal (nlogL, 204.5230311010569, 1e-12); %!test %! nlogL = ricelike ([2.312346885, 1.681228265], [1:5]); %! assert_equal (nlogL, 8.65562164930058, 1e-12); ## Test input validation %!error ricelike (3.25) %!error ricelike ([5, 0.2], ones (2)) %!error ... %! ricelike ([1, 0.2, 3], [1, 3, 5, 7]) %!error ... %! ricelike ([1.5, 0.2], [1:5], [0, 0, 0]) %!error ... %! ricelike ([1.5, 0.2], [1:5], [0, 0, 0, 0, 0], [1, 1, 1]) %!error ... %! ricelike ([1.5, 0.2], [1:5], [], [1, 1, 1]) %!error ... %! ricelike ([1.5, 0.2], [1:5], [], [1, 1, 1, 0, -1]) statistics-release-2.1.2/inst/Distribution_Fitting/stblfit.m000066400000000000000000000262421527311104300242740ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{paramhat} =} stblfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} stblfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} stblfit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} stblfit (@var{x}, @var{alpha}, @var{freq}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} stblfit (@var{x}, @var{alpha}, @var{options}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} stblfit (@var{x}, @var{alpha}, @var{freq}, @var{options}) ## ## Estimate parameters and confidence intervals for the stable distribution. ## ## @code{@var{paramhat} = stblfit (@var{x})} returns the maximum likelihood ## estimates of the parameters of the stable distribution, in the Nolan ## @qcode{S0} parameterization, given the data in @var{x}. ## @qcode{@var{paramhat}(1)} is the tail index @var{alpha}, ## @qcode{@var{paramhat}(2)} is the skewness @var{beta}, ## @qcode{@var{paramhat}(3)} is the scale @var{gam}, and ## @qcode{@var{paramhat}(4)} is the location @var{delta}. ## ## @code{[@var{paramhat}, @var{paramci}] = stblfit (@var{x})} returns the 95% ## confidence intervals for the parameter estimates. The intervals are Wald ## intervals from the observed Fisher information. ## ## @code{[@dots{}] = stblfit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals for the ## parameter estimates. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default value. ## ## @code{[@dots{}] = stblfit (@var{x}, @var{alpha}, @var{freq})} accepts a ## frequency vector, @var{freq}, of the same size as @var{x}. @var{freq} ## must contain non-negative integer frequencies for the corresponding elements ## in @var{x}. By default, or if left empty, ## @qcode{@var{freq} = ones (size (@var{x}))}. ## ## @code{[@var{paramhat}, @var{paramci}] = stblfit (@var{x}, @var{alpha}, ## @var{options})} specifies control parameters for the iterative algorithm used ## to compute the ML estimates with the @code{fminsearch} function. ## @var{options} is a structure with the following fields and their default ## values: ## @itemize ## @item @qcode{@var{options}.Display = "off"} ## @item @qcode{@var{options}.MaxFunEvals = 400} ## @item @qcode{@var{options}.MaxIter = 200} ## @item @qcode{@var{options}.TolX = 1e-6} ## @end itemize ## ## The stable density has no closed form; it is evaluated by numerical inversion ## of the characteristic function, which makes fitting considerably slower than ## for the closed-form distributions. Censoring is not supported. ## ## The estimates are the maximum-likelihood estimates under the mathematically ## exact density. MATLAB fits an interpolation-based approximation of the ## stable density, whose maximum-likelihood estimates deviate from the exact ## ones by about @math{10^{-2}} (and the resulting confidence intervals by up to ## roughly 20%); @code{stblfit} returns the exact (more accurate) estimates. ## ## Further information about the stable distribution can be found at ## @url{https://en.wikipedia.org/wiki/Stable_distribution} ## ## @seealso{stbllike, stblpdf, stblcdf, stblinv, stblrnd, fitdist, makedist} ## @end deftypefn function [paramhat, paramci] = stblfit (x, alpha, varargin) ## Check X is a vector if (! isvector (x)) error ("stblfit: X must be a vector."); endif ## Get X type and convert to double for computation is_type = class (x); if (strcmpi (is_type, "single")) x = double (x); endif ## Check that X does not contain NaNs and is not constant if (any (isnan (x))) error ("stblfit: X must not contain NaN values."); elseif (numel (unique (x)) == 1) error ("stblfit: X must contain at least two distinct values."); endif ## Check ALPHA (significance level) if (nargin < 2 || isempty (alpha)) alpha = 0.05; elseif (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("stblfit: wrong value for ALPHA."); endif ## Add defaults freq = []; options.Display = "off"; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; ## Check extra arguments for FREQ vector and/or 'options' structure if (nargin > 2) if (numel (varargin) == 1 && isstruct (varargin{1})) options = varargin{1}; elseif (numel (varargin) == 1 && isnumeric (varargin{1})) freq = varargin{1}; elseif (numel (varargin) == 2) freq = varargin{1}; options = varargin{2}; endif if (isempty (freq)) freq = ones (size (x)); endif if (! isequal (size (x), size (freq))) error ("stblfit: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("stblfit: FREQ must not contain negative values."); elseif (any (fix (freq) != freq)) error ("stblfit: FREQ must contain integer values."); endif if (! isstruct (options) || ! isfield (options, "Display") || ! isfield (options, "MaxFunEvals") || ! isfield (options, "MaxIter") || ! isfield (options, "TolX")) error (strcat ("stblfit: 'options' argument must be a structure with", ... " 'Display', 'MaxFunEvals', 'MaxIter', and 'TolX'", ... " fields present.")); endif endif if (isempty (freq)) freq = ones (size (x)); endif ## Force column vectors and drop zero-frequency observations x = x(:); freq = freq(:); keep = freq > 0; x = x(keep); freq = freq(keep); ## Objective: negative log-likelihood in the unconstrained parameter space fhandle = @(u) stbl_nll (to_nat (u), x, freq); ## Initial estimate: pick the best of a small grid of quantile-scaled starts, ## then refine. The grid spans the tail index and skewness, while the scale ## and location are matched to the sample interquartile range and median. q = quantile (x, [0.25, 0.5, 0.75]); iqr_x = q(3) - q(1); best_nll = Inf; u0 = []; for a0 = [0.7, 1.1, 1.5, 1.9] for b0 = [-0.5, 0, 0.5] ## Standardized interquartile range for this shape siqr = diff (stblinv ([0.25, 0.75], a0, b0, 1, 0)); gam0 = iqr_x / siqr; delta0 = q(2) - gam0 * stblinv (0.5, a0, b0, 1, 0); p0 = [a0, b0, gam0, delta0]; nll0 = stbl_nll (p0, x, freq); if (nll0 < best_nll) best_nll = nll0; u0 = to_unc (p0); endif endfor endfor ## Minimize the negative log-likelihood [uhat, ~, exitflag, output] = fminsearch (fhandle, u0, options); paramhat = to_nat (uhat); ## Display warnings if the optimizer did not converge if (exitflag == 0) if (output.funcCount >= output.iterations) warning ("stblfit: maximum number of function evaluations reached."); else warning ("stblfit: reached iteration limit."); endif endif ## Return a row vector for MATLAB compatibility paramhat = paramhat(:)'; ## Check for second output argument if (nargout > 1) [~, acov] = stbllike (paramhat, x, freq); param_se = sqrt (diag (acov))'; if (any (! isfinite (param_se))) warning (strcat ("stblfit: could not compute confidence intervals;", ... " the Fisher information matrix is not positive", ... " definite.")); paramci = NaN (2, 4, is_type); else ## Linear Wald intervals for all four parameters (matching MATLAB, which ## does not place the scale parameter on a log scale here) p_vals = [alpha/2; 1-alpha/2]; a_ci = norminv (p_vals, paramhat(1), param_se(1)); b_ci = norminv (p_vals, paramhat(2), param_se(2)); g_ci = norminv (p_vals, paramhat(3), param_se(3)); d_ci = norminv (p_vals, paramhat(4), param_se(4)); paramci = [a_ci, b_ci, g_ci, d_ci]; endif endif endfunction ## Map the unconstrained vector U to the natural parameters function p = to_nat (u) p = [2 ./ (1 + exp (-u(1))), tanh(u(2)), exp(u(3)), u(4)]; endfunction ## Map the natural parameters P to the unconstrained vector function u = to_unc (p) u = [log(p(1) ./ (2 - p(1))), atanh(p(2)), log(p(3)), p(4)]; endfunction ## Negative log-likelihood of the stable density at parameter vector P function nll = stbl_nll (p, x, freq) y = __stable_pdf__ (x, p(1), p(2), p(3), p(4)); y(y <= 0) = realmin; nll = -sum (freq .* log (y)); if (! isreal (nll) || isnan (nll)) nll = Inf; endif endfunction %!demo %! ## Fit a stable distribution to simulated data %! rng (42); %! x = stblrnd (1.5, 0.5, 2, 1, 150, 1); %! [paramhat, paramci] = stblfit (x) ## Test output ## The stable density has no closed form, so fitting is by numerical inversion ## of the characteristic function and is slow; the sample sizes below are kept ## modest to bound the test time, with correspondingly loose tolerances. %!test # recovery + MATLAB parity %! # Our fast integrator reproduces the exact stblpdf MLE to ~2e-5. MATLAB's %! # stable density is a Nolan interpolation approximation, so its fit %! # deviates from the exact MLE by ~1e-2; we ship the exact (more accurate) %! # estimate and document the deviation (as for the copula family). %! rand ("seed", 2718); %! randn ("seed", 2718); %! x = stblrnd (1.5, 0.5, 2, 1, 150, 1); %! [phat, pci] = stblfit (x); %! ## Exact-density estimate on this sample (recovers the generating [1.5 0.5 2 1]) %! assert_equal (phat, [1.5469000, 0.4732298, 2.0097077, 1.1640279], 1e-3); %! ## MATLAB fitdist (x, 'Stable') on the same data agrees to ~1.5e-2 %! assert_equal (phat, [1.5449145, 0.4693139, 2.0000225, 1.1646526], 1.5e-2); %! ## Confidence intervals bracket the estimate; gam CI is positive %! assert_equal (all (pci(1,:) <= phat, 'all') ... %! && all (pci(2,:) >= phat, 'all'), true); %! assert_equal (pci(1,3) > 0, true); ## Test input validation %!error stblfit (ones (2, 2)) %!error stblfit ([1, 2, NaN, 4]) %!error ... %! stblfit ([2, 2, 2, 2]) %!error stblfit ([1, 2, 3, 4], 1.5) %!error stblfit ([1, 2, 3, 4], -0.5) %!error ... %! stblfit ([1, 2, 3, 4], 0.05, [1, 2, 3]) %!error ... %! stblfit ([1, 2, 3, 4], 0.05, [1, -1, 2, 1]) %!error ... %! stblfit ([1, 2, 3, 4], 0.05, [1, 1.5, 2, 1]) statistics-release-2.1.2/inst/Distribution_Fitting/stbllike.m000066400000000000000000000153461527311104300244410ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} stbllike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} stbllike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} stbllike (@var{params}, @var{x}, @var{freq}) ## ## Negative log-likelihood for the stable distribution. ## ## @code{@var{nlogL} = stbllike (@var{params}, @var{x})} returns the negative ## log-likelihood of the data in @var{x} corresponding to the stable ## distribution, in the Nolan @qcode{S0} parameterization, with (1) tail index ## @var{alpha}, (2) skewness @var{beta}, (3) scale @var{gam}, and (4) location ## @var{delta} given in the four-element vector @var{params}. ## ## @code{[@var{nlogL}, @var{acov}] = stbllike (@var{params}, @var{x})} also ## returns the inverse of the observed Fisher information matrix, @var{acov}. ## If the input parameter values in @var{params} are the maximum likelihood ## estimates, the diagonal elements of @var{acov} are their asymptotic ## variances. @var{acov} is based on the numerically evaluated Hessian of the ## negative log-likelihood, since the stable density has no closed form. ## ## @code{[@dots{}] = stbllike (@var{params}, @var{x}, @var{freq})} accepts a ## frequency vector, @var{freq}, of the same size as @var{x}. @var{freq} must ## contain non-negative integer frequencies for the corresponding elements in ## @var{x}. By default, or if left empty, @qcode{@var{freq} = ones (size ## (@var{x}))}. ## ## Further information about the stable distribution can be found at ## @url{https://en.wikipedia.org/wiki/Stable_distribution} ## ## @seealso{stblfit, stblpdf, stblcdf, stblinv, stblrnd} ## @end deftypefn function [nlogL, acov] = stbllike (params, x, freq) ## Check input arguments if (nargin < 2) error ("stbllike: function called with too few input arguments."); endif if (! isvector (x)) error ("stbllike: X must be a vector."); endif if (numel (params) != 4) error ("stbllike: PARAMS must be a four-element vector."); endif if (nargin < 3 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("stbllike: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("stbllike: FREQ must not contain negative values."); elseif (any (fix (freq) != freq)) error ("stbllike: FREQ must contain integer values."); endif ## Force column vectors x = x(:); freq = freq(:); ## Negative log-likelihood at the given parameters nlogL = stbl_nll (params, x, freq); ## Optionally return the asymptotic covariance from the observed Fisher ## information, computed as the inverse of the numeric Hessian if (nargout > 1) theta = params(:).'; hstep = (eps ^ (1/4)) .* max (abs (theta), 1); H = num_hessian (@(t) stbl_nll (t, x, freq), theta, hstep); H = (H + H') / 2; [~, notpd] = chol (H); if (notpd != 0) warning (strcat ("stbllike: Fisher information matrix not positive", ... " definite; returning NaN covariance.")); acov = NaN (4); else acov = inv (H); acov = (acov + acov') / 2; endif endif endfunction ## Negative log-likelihood of the stable density at parameter vector THETA. ## Out-of-range parameters return Inf so that the enclosing optimizers (e.g. ## the profile-likelihood search in proflik) stay within the feasible region. function nll = stbl_nll (theta, x, freq) alpha = theta(1); beta = theta(2); gam = theta(3); if (alpha <= 0 || alpha > 2 || abs (beta) > 1 || gam <= 0) nll = Inf; return endif y = __stable_pdf__ (x, theta(1), theta(2), theta(3), theta(4)); y(y <= 0) = realmin; nll = -sum (freq .* log (y)); endfunction ## Central finite-difference Hessian of NLLFUN at THETA with per-parameter step function H = num_hessian (nllfun, theta, hstep) p = numel (theta); H = zeros (p); f0 = nllfun (theta); for i = 1:p ei = zeros (1, p); ei(i) = hstep(i); fpi = nllfun (theta + ei); fmi = nllfun (theta - ei); H(i,i) = (fpi - 2 * f0 + fmi) / (hstep(i) ^ 2); for j = (i + 1):p ej = zeros (1, p); ej(j) = hstep(j); fpp = nllfun (theta + ei + ej); fpm = nllfun (theta + ei - ej); fmp = nllfun (theta - ei + ej); fmm = nllfun (theta - ei - ej); H(i,j) = (fpp - fpm - fmp + fmm) / (4 * hstep(i) * hstep(j)); H(j,i) = H(i,j); endfor endfor endfunction %!demo %! ## Negative log-likelihood of a stable fit to simulated data %! rng (42); %! x = stblrnd (1.5, 0.5, 1, 0, 150, 1); %! phat = stblfit (x); %! nlogL = stbllike (phat, x) ## Test output %!test # matches -sum (log (pdf)); stbllike uses a fast CF-inversion density %! # that differs from stblpdf by a few parts in 1e-5 %! x = [-2.3, -0.9, 0.1, 0.4, 1.2, 2.8, 5.1]; %! nlogL = stbllike ([1.5, 0.5, 1, 0], x); %! assert_equal (nlogL, - sum (log (stblpdf (x, 1.5, 0.5, 1, 0))), 1e-3); %!test # frequency weights replicate observations %! x = [-1, 0.5, 2]; %! f = [2, 1, 3]; %! xr = [-1, -1, 0.5, 2, 2, 2]; %! assert_equal (stbllike ([1.2, 0, 1, 0], x, f), ... %! stbllike ([1.2, 0, 1, 0], xr), 1e-6); %!test # acov is symmetric positive (co)variance at a sensible parameter %! rand ("seed", 1); %! x = stblrnd (1.6, 0, 1, 0, 150, 1); %! [~, acov] = stbllike ([1.6, 0, 1, 0], x); %! assert_equal (issymmetric (acov, 1e-10), true); %! assert_equal (all (diag (acov) > 0, 'all'), true); ## Test input validation %!error ... %! stbllike ([1.5, 0, 1, 0]) %!error stbllike ([1.5, 0, 1, 0], ones (2, 2)) %!error ... %! stbllike ([1.5, 0, 1], [1, 2, 3]) %!error ... %! stbllike ([1.5, 0, 1, 0], [1, 2, 3], [1, 2]) %!error ... %! stbllike ([1.5, 0, 1, 0], [1, 2, 3], [1, -1, 2]) %!error ... %! stbllike ([1.5, 0, 1, 0], [1, 2, 3], [1, 1.5, 2]) statistics-release-2.1.2/inst/Distribution_Fitting/tlsfit.m000066400000000000000000000252401527311104300241270ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{paramhat} =} tlsfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} tlsfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} tlsfit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@dots{}] =} tlsfit (@var{x}, @var{alpha}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} tlsfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}) ## @deftypefnx {statistics} {[@dots{}] =} tlsfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}, @var{options}) ## ## Estimate parameters and confidence intervals for the Location-scale Student's ## T distribution. ## ## @code{@var{muhat} = tlsfit (@var{x})} returns the maximum likelihood ## estimates of the parameters of the location-scale T distribution ## given the data in @var{x}. @qcode{@var{paramhat}(1)} is the location ## parameter, @math{mu}, @qcode{@var{paramhat}(2)} is the scale parameter, ## @math{sigma}, and @qcode{@var{paramhat}(3)} is the degrees of freedom, ## @math{nu}. ## ## @code{[@var{paramhat}, @var{paramci}] = tlsfit (@var{x})} returns the 95% ## confidence intervals for the parameter estimates. ## ## @code{[@dots{}] = tlsfit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals for the ## parameter estimates. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = tlsfit (@var{x}, @var{alpha}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = tlsfit (@var{x}, @var{alpha}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## @code{[@dots{}] = tlsfit (@dots{}, @var{options})} specifies control ## parameters for the iterative algorithm used to compute ML estimates with the ## @code{fminsearch} function. @var{options} is a structure with the following ## fields and their default values: ## @itemize ## @item @qcode{@var{options}.Display = "off"} ## @item @qcode{@var{options}.MaxFunEvals = 400} ## @item @qcode{@var{options}.MaxIter = 200} ## @item @qcode{@var{options}.TolX = 1e-6} ## @end itemize ## ## Further information about the location-scale Student's T distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Student%27s_t-distribution#Location-scale_t_distribution} ## ## @seealso{tlscdf, tlsinv, tlspdf, tlsrnd, tlslike, tlsstat} ## @end deftypefn function [paramhat, paramci] = tlsfit (x, alpha, censor, freq, options) ## Check input arguments if (! isvector (x)) error ("tlsfit: X must be a vector."); endif ## Check alpha if (nargin < 2 || isempty (alpha)) alpha = 0.05; else if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("tlsfit: wrong value for ALPHA."); endif endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("tlsfit: X and CENSOR vectors mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("tlsfit: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("tlsfit: FREQ cannot have negative values."); endif ## Get options structure or add defaults if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("tlsfit: 'options' 5th argument must be a structure", ... " with 'Display', 'MaxFunEvals', 'MaxIter', and", ... " 'TolX' fields present.")); endif endif ## Starting points as robust estimators for MU and SIGMA, and method ## of moments for DF x_uncensored = x(censor==0); mu = median (x_uncensored); sigma = 1.253 * mad (x_uncensored); mom = kurtosis (x_uncensored); mom(mom < 4) = 4; nu = 2 * (2 * mom - 3) / (mom - 3); x0 = [mu, sigma, nu]; ## Minimize negative log-likelihood to estimate parameters f = @(params) tlslike (params, x, censor, freq); [paramhat, ~, err, output] = fminsearch (f, x0, options); ## Handle errors if (err == 0) if (output.funcCount >= options.MaxFunEvals) warning (strcat ("tlsfit: maximum number of function", ... " evaluations are exceeded.")); elseif (output.iterations >= options.MaxIter) warning ("tlsfit: maximum number of iterations are exceeded."); endif elseif (err < 0) error ("tlsfit: no solution."); endif ## Compute CIs using a log normal approximation for parameters. if (nargout > 1) ## Compute asymptotic covariance [~, acov] = tlslike (paramhat, x, censor, freq); ## Get standard errors stderr = sqrt (diag (acov))'; mu_se = stderr(1); sigma_se = stderr(2) ./ paramhat(2); df_se = stderr(3) ./ paramhat(3); ## Apply log transform to SIGMA and DF sigma_log = log (paramhat(2)); df_log = log (paramhat(3)); ## Compute normal quantiles z = norminv (alpha / 2); ## Compute CI muci = [paramhat(1); paramhat(1)] + [mu_se; mu_se] .* [z; -z]; sigmaci = [sigma_log; sigma_log] + [sigma_se; sigma_se] .* [z; -z]; dfci = [df_log; df_log] + [df_se; df_se] .* [z; -z]; ## Inverse log transform paramci = [muci, exp([sigmaci, dfci])]; endif endfunction %!demo %! ## Sample 3 populations from 3 different location-scale T distributions %! rng (42); %! randg ('state', 42); %! r1 = tlsrnd (-4, 3, 1, 2000, 1); %! r2 = tlsrnd (0, 3, 1, 2000, 1); %! r3 = tlsrnd (5, 5, 4, 2000, 1); %! r = [r1, r2, r3]; %! %! ## Plot them normalized and fix their colors %! hist (r, [-21:21], [1, 1, 1]); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! set (h(3), 'facecolor', 'r'); %! ylim ([0, 0.25]); %! xlim ([-20, 20]); %! hold on %! %! ## Estimate their lambda parameter %! mu_sigma_nuA = tlsfit (r(:,1)); %! mu_sigma_nuB = tlsfit (r(:,2)); %! mu_sigma_nuC = tlsfit (r(:,3)); %! %! ## Plot their estimated PDFs %! x = [-20:0.1:20]; %! y = tlspdf (x, mu_sigma_nuA(1), mu_sigma_nuA(2), mu_sigma_nuA(3)); %! plot (x, y, '-pr'); %! y = tlspdf (x, mu_sigma_nuB(1), mu_sigma_nuB(2), mu_sigma_nuB(3)); %! plot (x, y, '-sg'); %! y = tlspdf (x, mu_sigma_nuC(1), mu_sigma_nuC(2), mu_sigma_nuC(3)); %! plot (x, y, '-^c'); %! hold off %! legend ({'Normalized HIST of sample 1 with μ=0, σ=2 and nu=1', ... %! 'Normalized HIST of sample 2 with μ=5, σ=2 and nu=1', ... %! 'Normalized HIST of sample 3 with μ=3, σ=4 and nu=3', ... %! sprintf("PDF for sample 1 with estimated μ=%0.2f, σ=%0.2f, and ν=%0.2f", ... %! mu_sigma_nuA(1), mu_sigma_nuA(2), mu_sigma_nuA(3)), ... %! sprintf("PDF for sample 2 with estimated μ=%0.2f, σ=%0.2f, and ν=%0.2f", ... %! mu_sigma_nuB(1), mu_sigma_nuB(2), mu_sigma_nuB(3)), ... %! sprintf("PDF for sample 3 with estimated μ=%0.2f, σ=%0.2f, and ν=%0.2f", ... %! mu_sigma_nuC(1), mu_sigma_nuC(2), mu_sigma_nuC(3))}) %! title ('Three population samples from different location-scale T distributions') %! hold off ## Test output %!test %! x = [-1.2352, -0.2741, 0.1726, 7.4356, 1.0392, 16.4165]; %! [paramhat, paramci] = tlsfit (x); %! paramhat_out = [0.035893, 0.862711, 0.649261]; %! paramci_out = [-0.949034, 0.154655, 0.181080; 1.02082, 4.812444, 2.327914]; %! assert_equal (paramhat, paramhat_out, 1e-6); %! assert_equal (paramci, paramci_out, 1e-5); %!test %! x = [-1.2352, -0.2741, 0.1726, 7.4356, 1.0392, 16.4165]; %! [paramhat, paramci] = tlsfit (x, 0.01); %! paramci_out = [-1.2585, 0.0901, 0.1212; 1.3303, 8.2591, 3.4771]; %! assert_equal (paramci, paramci_out, 1e-4); ## Test input validation %!error tlsfit (ones (2,5)); %!error tlsfit ([1, 2, 3, 4, 5], 1.2); %!error tlsfit ([1, 2, 3, 4, 5], 0); %!error tlsfit ([1, 2, 3, 4, 5], 'alpha'); %!error ... %! tlsfit ([1, 2, 3, 4, 5], 0.05, [1 1 0]); %!error ... %! tlsfit ([1, 2, 3, 4, 5], [], [1 1 0 1 1]'); %!error ... %! tlsfit ([1, 2, 3, 4, 5], 0.05, zeros (1,5), [1 1 0]); %!error ... %! tlsfit ([1, 2, 3, 4, 5], [], [], [1 1 0 1 1]'); %!error ... %! tlsfit ([1, 2, 3, 4, 5], [], [], [1 1 0 1 -1]); %!error ... %! tlsfit ([1, 2, 3, 4, 5], 0.05, [], [], 2); ## A fit that stops at a limit warns; it used to die reading an option that ## nothing set. %!warning ... %! opt = struct ('Display', 'off', 'MaxFunEvals', 1, 'MaxIter', 200, ... %! 'TolX', 1e-6); %! tlsfit ([1, 2, 3, 4, 5, 6, 7, 8], 0.05, [], [], opt); %!warning ... %! opt = struct ('Display', 'off', 'MaxFunEvals', 400, 'MaxIter', 1, ... %! 'TolX', 1e-6); %! tlsfit ([1, 2, 3, 4, 5, 6, 7, 8], 0.05, [], [], opt); statistics-release-2.1.2/inst/Distribution_Fitting/tlslike.m000066400000000000000000000161321527311104300242710ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} tlslike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} tlslike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} tlslike (@var{params}, @var{x}, @var{alpha}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} tlslike (@var{params}, @var{x}, @var{alpha}, @var{censor}, @var{freq}) ## ## Negative log-likelihood for the location-scale Student's T distribution. ## ## @code{@var{nlogL} = tlslike (@var{params}, @var{x})} returns the negative ## log-likelihood of the x in @var{x} corresponding to the location-scale T ## distribution with (1) location parameter @math{mu}, (2) scale parameter ## @math{sigma} and (3) degrees of freedom @math{nu} given in the three-element ## vector @var{params}. ## ## @code{[@var{nlogL}, @var{acov}] = tlslike (@var{params}, @var{x})} also ## returns the inverse of Fisher's information matrix, @var{acov}. If the input ## parameter values in @var{params} are the maximum likelihood estimates, the ## diagonal elements of @var{acov} are their asymptotic variances. @var{acov} ## is based on the observed Fisher's information, not the expected information. ## ## @code{[@dots{}] = tlslike (@var{params}, @var{x}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = tlslike (@var{params}, @var{x}, @var{censor}, ## @var{freq})} accepts a frequency vector, @var{freq}, of the same size as ## @var{x}. @var{freq} typically contains integer frequencies for the ## corresponding elements in @var{x}, but may contain any non-integer ## non-negative values. By default, or if left empty, ## @qcode{@var{freq} = ones (size (@var{x}))}. ## ## Further information about the location-scale Student's T distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Student%27s_t-distribution#Location-scale_t_distribution} ## ## @seealso{tlscdf, tlsinv, tlspdf, tlsrnd, tlsfit, tlsstat} ## @end deftypefn function [nlogL, acov] = tlslike (params, x, censor, freq) ## Check input arguments and add defaults if (nargin < 2) error ("tlslike: too few input arguments."); endif if (numel (params) != 3) error ("tlslike: wrong parameters length."); endif if (! isvector (x)) error ("tlslike: X must be a vector."); endif if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("tlslike: X and CENSOR vectors mismatch."); endif if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (any (freq < 0)) error ("tlslike: FREQ cannot have negative values."); elseif (isequal (size (x), size (freq))) nulls = find (freq == 0); if (numel (nulls) > 0) x(nulls) = []; censor(nulls) = []; freq(nulls) = []; endif else error ("tlslike: X and FREQ vectors mismatch."); endif ## Compute the negative log-likelihood nlogL = tlsnll (x, params, censor, freq); ## Compute the negative hessian and invert to get the information matrix if (nargout > 1) ei = zeros (1, 3); ej = zeros (1, 3); nH = zeros (3, 3); dp = (eps ^ (1/4)) .* max (abs (params), 1); for i = 1:3 ei(i) = dp(i); for j = 1:(i-1) ej(j) = dp(j); ## Four-point central difference for mixed second partials nH(i,j) = tlsnll (x, params+ei+ej, censor, freq) ... - tlsnll (x, params+ei-ej, censor, freq) ... - tlsnll (x, params-ei+ej, censor, freq) ... + tlsnll (x, params-ei-ej, censor, freq); ej(j) = 0; endfor ## Five-point central difference for pure second partial nH(i,i) = - tlsnll (x, params+2*ei, censor, freq) ... + 16 * tlsnll (x, params+ei, censor, freq) - 30 * nlogL ... + 16 * tlsnll (x, params-ei, censor, freq) ... - tlsnll (x, params-2*ei, censor, freq); ei(i) = 0; endfor ## Fill in the upper triangle nH = nH + triu (nH', 1); ## Normalize the second differences to get derivative estimates nH = nH ./ (4 .* dp(:) * dp(:)' + diag (8 * dp(:) .^ 2)); ## Check neg Hessian is positive definite [R, p] = chol (nH); if (p > 0) warning ("tlslike: non positive definite Hessian matrix."); acov = NaN (3); return endif ## ACOV estimate is the negative inverse of the Hessian Rinv = inv (R); acov = Rinv * Rinv'; endif endfunction ## Internal function to calculate negative log likelihood for tlslike function nlogL = tlsnll (x, params, censor, freq) mu = params(1); sigma = params(2); sigma(sigma <= 0) = NaN; nu = params(3); nu(nu <= 0) = NaN; z = (x - mu) ./ sigma; w = nu + (z .^ 2); logw = log (w); L = - 0.5 .* (nu + 1) .* logw + gammaln (0.5 .* (nu + 1)) ... - gammaln (0.5 .* nu) + 0.5 .* nu .* log (nu) ... - log (sigma) - 0.5 .* log (pi); n_censored = sum (freq .* censor); if (n_censored > 0) censored = (censor == 1); if (nu < 1e7) # Use incomplete beta function S_censored = betainc (nu ./ w(censored), 0.5 .* nu, 0.5) ./ 2; S_censored(z(censored) < 0) = 1 - S_censored(z(censored) < 0); else # Use a normal approximation S_censored = log (0.5 * erfc (z(censored) ./ sqrt (2))); endif L(censored) = log (S_censored); endif nlogL = - sum (freq .* L); endfunction ## Test output %!test %! x = [-1.2352, -0.2741, 0.1726, 7.4356, 1.0392, 16.4165]; %! [nlogL, acov] = tlslike ([0.035893, 0.862711, 0.649261], x); %! acov_out = [0.2525, 0.0670, 0.0288; ... %! 0.0670, 0.5724, 0.1786; ... %! 0.0288, 0.1786, 0.1789]; %! assert_equal (nlogL, 17.9979636579, 1e-10); %! assert_equal (acov, acov_out, 1e-4); ## Test input validation %!error tlslike ([12, 15, 1]); %!error tlslike ([12, 15], [1:50]); %!error tlslike ([12, 3, 1], ones (10, 2)); %!error tlslike ([12, 15, 1], [1:50], [1, 2, 3]); %!error tlslike ([12, 15, 1], [1:50], [], [1, 2, 3]); %!error tlslike ([12, 15, 1], [1:3], [], [1, 2, -3]); statistics-release-2.1.2/inst/Distribution_Fitting/unidfit.m000066400000000000000000000126721527311104300242710ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Nhat} =} unidfit (@var{x}) ## @deftypefnx {statistics} {[@var{Nhat}, @var{Nci}] =} unidfit (@var{x}) ## @deftypefnx {statistics} {[@var{Nhat}, @var{Nci}] =} unidfit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@var{Nhat}, @var{Nci}] =} unidfit (@var{x}, @var{alpha}, @var{freq}) ## ## Estimate parameter and confidence intervals for the discrete uniform ## distribution. ## ## @code{@var{Nhat} = unidfit (@var{x})} returns the maximum likelihood estimate ## (MLE) of the maximum observable value for the discrete uniform distribution. ## @var{x} must be a vector. ## ## @code{[@var{Nhat}, @var{Nci}] = unidfit (@var{x}, @var{alpha})} also ## returns the @qcode{100 * (1 - @var{alpha})} percent confidence intervals of ## the estimated parameter. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = unidfit (@var{x}, @var{alpha}, @var{freq})} accepts a ## frequency vector, @var{freq}, of the same size as @var{x}. @var{freq} ## typically contains integer frequencies for the corresponding elements in ## @var{x}, but it can contain any non-integer non-negative values. By default, ## or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## Further information about the discrete uniform distribution can be found at ## @url{https://en.wikipedia.org/wiki/Discrete_uniform_distribution} ## ## @seealso{unidcdf, unidinv, unidpdf, unidrnd, unidstat} ## @end deftypefn function [Nhat, Nci] = unidfit (x, alpha, freq) ## Check input arguments if (nargin < 1) error ("unidfit: function called with too few input arguments."); endif ## Check data in X if (any (x < 0)) error ("unidfit: X cannot have negative values."); endif if (! isvector (x)) error ("unidfit: X must be a vector."); endif ## Check ALPHA if (nargin < 2 || isempty (alpha)) alpha = 0.05; elseif (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("unidfit: wrong value for ALPHA."); endif ## Check frequency vector if (nargin < 3 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("unidfit: X and FREQ vector mismatch."); elseif (any (freq < 0)) error ("unidfit: FREQ cannot have negative values."); endif ## Expand frequency and censor vectors (if necessary) if (! all (freq == 1)) xf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; endfor x = xf; freq = ones (size (x)); endif ## Compute N estimate Nhat = max (x); ## Compute confidence interval of N if (nargout > 1) Nci = [Nhat; ceil(Nhat ./ alpha .^ (1 ./ numel (x)))]; endif endfunction %!demo %! ## Sample 2 populations from different discrete uniform distributions %! rng (42); %! r1 = unidrnd (5, 1000, 1); %! r2 = unidrnd (9, 1000, 1); %! r = [r1, r2]; %! %! ## Plot them normalized and fix their colors %! hist (r, 0:0.5:20.5, 1); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! hold on %! %! ## Estimate their probability of success %! NhatA = unidfit (r(:,1)); %! NhatB = unidfit (r(:,2)); %! %! ## Plot their estimated PDFs %! x = [0:10]; %! y = unidpdf (x, NhatA); %! plot (x, y, '-pg'); %! y = unidpdf (x, NhatB); %! plot (x, y, '-sc'); %! xlim ([0, 10]) %! ylim ([0, 0.4]) %! legend ({'Normalized HIST of sample 1 with N=5', ... %! 'Normalized HIST of sample 2 with N=9', ... %! sprintf("PDF for sample 1 with estimated N=%0.2f", NhatA), ... %! sprintf("PDF for sample 2 with estimated N=%0.2f", NhatB)}) %! title ('Two population samples from different discrete uniform distributions') %! hold off ## Test output %!test %! x = 0:5; %! [Nhat, Nci] = unidfit (x); %! assert_equal (Nhat, 5); %! assert_equal (Nci, [5; 9]); %!test %! x = 0:5; %! [Nhat, Nci] = unidfit (x, [], [1 1 1 1 1 1]); %! assert_equal (Nhat, 5); %! assert_equal (Nci, [5; 9]); %!assert_equal (unidfit ([1 1 2 3]), unidfit ([1 2 3], [] ,[2 1 1])) ## Test input validation %!error unidfit () %!error unidfit (-1, [1 2 3 3]) %!error unidfit (1, 0) %!error unidfit (1, 1.2) %!error unidfit (1, [0.02 0.05]) %!error ... %! unidfit ([1.5, 0.2], [], [0, 0, 0, 0, 0]) %!error ... %! unidfit ([1.5, 0.2], [], [1, 1, 1]) %!error ... %! unidfit ([1.5, 0.2], [], [1, -1]) statistics-release-2.1.2/inst/Distribution_Fitting/unifit.m000066400000000000000000000212011527311104300241110ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{ahat} =} unifit (@var{x}) ## @deftypefnx {statistics} {[@var{ahat}, @var{bhat}] =} unifit (@var{x}) ## @deftypefnx {statistics} {[@var{ahat}, @var{bhat}, @var{aci}, @var{bci}] =} unifit (@var{x}) ## @deftypefnx {statistics} {[@dots{}] =} unifit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@dots{}] =} unifit (@var{x}, @var{alpha}, @var{freq}) ## ## Estimate parameters and confidence intervals for the continuous uniform ## distribution. ## ## @code{[@var{ahat}, @var{bhat}] = unifit (@var{x})} returns the maximum ## likelihood estimates of the lower and upper endpoints, @var{a} and @var{b}, ## of the continuous uniform distribution given the data in @var{x}. Each ## estimate is returned as a separate output. ## ## @var{x} may be a vector, which is fitted as a single sample, or a matrix, ## which is fitted column by column. For a matrix of @math{n} columns ## @var{ahat} and @var{bhat} are @math{1}-by-@math{n} row vectors and @var{aci} ## and @var{bci} are @math{2}-by-@math{n}. ## ## @code{[@var{ahat}, @var{bhat}, @var{aci}, @var{bci}] = unifit (@var{x})} also ## returns the 95% confidence intervals of the two estimates, one column per ## column of @var{x}, with the lower bound in the first row and the upper bound ## in the second. @var{ahat} is the upper bound of @var{aci} and @var{bhat} the ## lower bound of @var{bci}, since no sample can fall outside the fitted range. ## ## @code{[@dots{}] = unifit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals of the ## estimated parameters. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = unifit (@var{x}, @var{alpha}, @var{freq})} accepts a ## frequency vector, @var{freq}, of the same size as @var{x}. @var{freq} ## typically contains integer frequencies for the corresponding elements in ## @var{x}, but it can contain any non-integer non-negative values. By default, ## or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. This third ## argument is an Octave extension; MATLAB's @code{unifit} takes two inputs at ## most, and @var{freq} is accepted for a vector @var{x} only. ## ## Further information about the continuous uniform distribution can be found at ## @url{https://en.wikipedia.org/wiki/Continuous_uniform_distribution} ## ## @seealso{unifcdf, unifinv, unifpdf, unifrnd, unifstat} ## @end deftypefn function [ahat, bhat, aci, bci] = unifit (x, alpha, freq) ## Check input arguments if (nargin < 1) error ("unifit: function called with too few input arguments."); endif if (! isnumeric (x) || ! isreal (x)) error ("unifit: X must be a vector or matrix of real values."); endif ## Check ALPHA if (nargin < 2 || isempty (alpha)) alpha = 0.05; elseif (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("unifit: wrong value for ALPHA."); endif ## Check frequency vector if (nargin > 2 && ! isempty (freq)) if (! isvector (x)) error ("unifit: FREQ is supported for a vector X only."); elseif (! isequal (size (x), size (freq))) error ("unifit: X and FREQ vector mismatch."); elseif (any (freq < 0)) error ("unifit: FREQ cannot have negative values."); endif ## Expand frequency vector if (! all (freq == 1)) xf = []; for i = 1:numel (freq) xf = [xf, repmat(x(i), 1, freq(i))]; endfor x = xf; endif endif if (isempty (x)) ahat = []; bhat = []; aci = []; bci = []; return endif ## A vector is one sample whichever way it lies; a matrix is fitted by column if (isvector (x)) x = x(:); endif ## Compute A and B estimates ahat = min (x, [], 1); bhat = max (x, [], 1); ## Compute the confidence intervals of A and B. No sample can fall outside ## the fitted range, so AHAT bounds ACI from above and BHAT bounds BCI from ## below. if (nargout > 2) tmp = (bhat - ahat) ./ alpha .^ (1 ./ rows (x)); aci = [bhat - tmp; ahat]; bci = [bhat; ahat + tmp]; endif endfunction %!demo %! ## Sample 2 populations from different continuous uniform distributions %! rng (42); %! r1 = unifrnd (2, 5, 2000, 1); %! r2 = unifrnd (3, 9, 2000, 1); %! r = [r1, r2]; %! %! ## Plot them normalized and fix their colors %! hist (r, 0:0.5:10, 2); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! hold on %! %! ## Estimate their probability of success %! a_bA = unifit (r(:,1)); %! a_bB = unifit (r(:,2)); %! %! ## Plot their estimated PDFs %! x = [0:10]; %! y = unifpdf (x, a_bA(1), a_bA(2)); %! plot (x, y, '-pg'); %! y = unifpdf (x, a_bB(1), a_bB(2)); %! plot (x, y, '-sc'); %! xlim ([1, 10]) %! ylim ([0, 0.5]) %! legend ({'Normalized HIST of sample 1 with a=2 and b=5', ... %! 'Normalized HIST of sample 2 with a=3 and b=9', ... %! sprintf("PDF for sample 1 with estimated a=%0.2f and b=%0.2f", ... %! a_bA(1), a_bA(2)), ... %! sprintf("PDF for sample 2 with estimated a=%0.2f and b=%0.2f", ... %! a_bB(1), a_bB(2))}) %! title ('Two population samples from different continuous uniform distributions') %! hold off ## Test output ## Values below are R2024a's, measured 2026-08-17. %!test %! [ahat, bhat] = unifit (0:5); %! assert_equal (ahat, 0); %! assert_equal (bhat, 5); %!test %! [ahat, bhat, aci, bci] = unifit (0:5); %! assert_equal (aci, [-3.237744862210329; 0], 1e-12); %! assert_equal (bci, [5; 8.237744862210329], 1e-12); %!test %! [~, ~, aci, bci] = unifit (0:5, 0.10); %! assert_equal (aci, [-2.338996338110347; 0], 1e-12); %! assert_equal (bci, [5; 7.338996338110347], 1e-12); %!test %! ## a column vector is the same single sample as a row %! [ahat, bhat, aci, bci] = unifit ((0:5)'); %! assert_equal ([ahat, bhat], [0, 5]); %! assert_equal (aci, [-3.237744862210329; 0], 1e-12); %!test %! ## a matrix is fitted column by column %! [ahat, bhat, aci, bci] = unifit ([0 10; 1 11; 2 12; 3 13; 4 14; 5 15]); %! assert_equal (ahat, [0, 10]); %! assert_equal (bhat, [5, 15]); %! assert_equal (aci, [-3.237744862210329, 6.762255137789671; 0, 10], 1e-12); %! assert_equal (bci, [5, 15; 8.237744862210329, 18.237744862210327], 1e-12); %!test %! ## negative data is ordinary for a uniform distribution %! [ahat, bhat, aci, bci] = unifit ([-2, -1, 0, 1, 2]); %! assert_equal ([ahat, bhat], [-2, 2]); %! assert_equal (aci, [-5.282256812104322; -2], 1e-12); %! assert_equal (bci, [2; 5.282256812104322], 1e-12); %!test %! ## a one-element sample has no width %! [ahat, bhat, aci, bci] = unifit (5); %! assert_equal ([ahat, bhat], [5, 5]); %! assert_equal (aci, [5; 5]); %! assert_equal (bci, [5; 5]); %!test %! ## empty data gives empty estimates %! [ahat, bhat, aci, bci] = unifit ([]); %! assert_equal (isempty (ahat), true); %! assert_equal (isempty (bhat), true); %! assert_equal (isempty (aci), true); %!test %! ## the endpoints ignore NaN %! [ahat, bhat] = unifit ([0 1 NaN 3]); %! assert_equal ([ahat, bhat], [0, 3]); %!test %! ## FREQ counts repeated observations %! [a1, b1] = unifit ([1 1 2 3]); %! [a2, b2] = unifit ([1 2 3], [], [2 1 1]); %! assert_equal ([a1, b1], [a2, b2]); ## Test input validation %!error unifit () %!error unifit ({1, 2}) %!error unifit (1+2i) %!error unifit (1, 0) %!error unifit (1, 1.2) %!error unifit (1, [0.02 0.05]) %!error ... %! unifit ([1 2; 3 4], [], [1 1; 1 1]) %!error ... %! unifit ([1.5, 0.2], [], [0, 0, 0, 0, 0]) %!error ... %! unifit ([1.5, 0.2], [], [1, -1]) statistics-release-2.1.2/inst/Distribution_Fitting/wblfit.m000066400000000000000000000207541527311104300241160ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{paramhat} =} wblfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} wblfit (@var{x}) ## @deftypefnx {statistics} {[@var{paramhat}, @var{paramci}] =} wblfit (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@dots{}] =} wblfit (@var{x}, @var{alpha}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} wblfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}) ## @deftypefnx {statistics} {[@dots{}] =} wblfit (@var{x}, @var{alpha}, @var{censor}, @var{freq}, @var{options}) ## ## Estimate parameters and confidence intervals for the Weibull distribution. ## ## @code{@var{muhat} = wblfit (@var{x})} returns the maximum likelihood ## estimates of the parameters of the Weibull distribution given the data in ## @var{x}. @qcode{@var{paramhat}(1)} is the scale parameter, @math{lambda}, ## and @qcode{@var{paramhat}(2)} is the shape parameter, @math{k}. ## ## @code{[@var{paramhat}, @var{paramci}] = wblfit (@var{x})} returns the 95% ## confidence intervals for the parameter estimates. ## ## @code{[@dots{}] = wblfit (@var{x}, @var{alpha})} also returns the ## @qcode{100 * (1 - @var{alpha})} percent confidence intervals for the ## parameter estimates. By default, the optional argument @var{alpha} is ## 0.05 corresponding to 95% confidence intervals. Pass in @qcode{[]} for ## @var{alpha} to use the default values. ## ## @code{[@dots{}] = wblfit (@var{x}, @var{alpha}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = wblfit (@var{x}, @var{alpha}, @var{censor}, @var{freq})} ## accepts a frequency vector, @var{freq}, of the same size as @var{x}. ## @var{freq} typically contains integer frequencies for the corresponding ## elements in @var{x}, but it can contain any non-integer non-negative values. ## By default, or if left empty, @qcode{@var{freq} = ones (size (@var{x}))}. ## ## @code{[@dots{}] = wblfit (@dots{}, @var{options})} specifies control ## parameters for the iterative algorithm used to compute the maximum likelihood ## estimates. @var{options} is a structure with the following field and its ## default value: ## @itemize ## @item @qcode{@var{options}.Display = "off"} ## @item @qcode{@var{options}.MaxFunEvals = 400} ## @item @qcode{@var{options}.MaxIter = 200} ## @item @qcode{@var{options}.TolX = 1e-6} ## @end itemize ## ## Further information about the Weibull distribution can be found at ## @url{https://en.wikipedia.org/wiki/Weibull_distribution} ## ## The @code{prob.WeibullDistribution} class names these same two parameters ## @qcode{A} and @qcode{B}, after MATLAB. @var{lambda} is its @qcode{A} and ## @var{k} is its @qcode{B}. ## @seealso{wblcdf, wblinv, wblpdf, wblrnd, wbllike, wblstat} ## @end deftypefn function [paramhat, paramci] = wblfit (x, alpha, censor, freq, options) ## Check input arguments if (! isvector (x)) error ("wblfit: X must be a vector."); elseif (any (x <= 0)) error ("wblfit: X must contain only positive values."); endif ## Check alpha if (nargin < 2 || isempty (alpha)) alpha = 0.05; else if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("wblfit: wrong value for ALPHA."); endif endif ## Check censor vector if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("wblfit: X and CENSOR vectors mismatch."); endif ## Check frequency vector if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error ("wblfit: X and FREQ vectors mismatch."); elseif (any (freq < 0)) error ("wblfit: FREQ cannot have negative values."); endif ## Get options structure or add defaults if (nargin < 5) options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; else if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("wblfit: 'options' 5th argument must be a", ... " structure with 'Display', 'MaxFunEvals',", ... " 'MaxIter', and 'TolX' fields present.")); endif endif ## Fit an extreme value distribution to the logged data, then transform to ## the Weibull parameter scales. [paramhatEV, paramciEV] = evfit (log (x), alpha, censor, freq, options); paramhat = [exp(paramhatEV(1)), 1./paramhatEV(2)]; if (nargout > 1) paramci = [exp(paramciEV(:,1)) 1./paramciEV([2 1],2)]; endif endfunction %!demo %! ## Sample 3 populations from 3 different Weibull distributions %! rande ('state', 42); %! r1 = wblrnd (2, 4, 2000, 1); %! r2 = wblrnd (5, 2, 2000, 1); %! r3 = wblrnd (1, 5, 2000, 1); %! r = [r1, r2, r3]; %! %! ## Plot them normalized and fix their colors %! hist (r, 30, [2.5 2.1 3.2]); %! h = findobj (gca, 'Type', 'patch'); %! set (h(1), 'facecolor', 'c'); %! set (h(2), 'facecolor', 'g'); %! set (h(3), 'facecolor', 'r'); %! ylim ([0, 2]); %! xlim ([0, 10]); %! hold on %! %! ## Estimate their lambda parameter %! lambda_kA = wblfit (r(:,1)); %! lambda_kB = wblfit (r(:,2)); %! lambda_kC = wblfit (r(:,3)); %! %! ## Plot their estimated PDFs %! x = [0:0.1:15]; %! y = wblpdf (x, lambda_kA(1), lambda_kA(2)); %! plot (x, y, '-pr'); %! y = wblpdf (x, lambda_kB(1), lambda_kB(2)); %! plot (x, y, '-sg'); %! y = wblpdf (x, lambda_kC(1), lambda_kC(2)); %! plot (x, y, '-^c'); %! hold off %! legend ({'Normalized HIST of sample 1 with λ=2 and k=4', ... %! 'Normalized HIST of sample 2 with λ=5 and k=2', ... %! 'Normalized HIST of sample 3 with λ=1 and k=5', ... %! sprintf("PDF for sample 1 with estimated λ=%0.2f and k=%0.2f", ... %! lambda_kA(1), lambda_kA(2)), ... %! sprintf("PDF for sample 2 with estimated λ=%0.2f and k=%0.2f", ... %! lambda_kB(1), lambda_kB(2)), ... %! sprintf("PDF for sample 3 with estimated λ=%0.2f and k=%0.2f", ... %! lambda_kC(1), lambda_kC(2))}) %! title ('Three population samples from different Weibull distributions') %! hold off ## Test output %!test %! x = 1:50; %! [paramhat, paramci] = wblfit (x); %! paramhat_out = [28.3636, 1.7130]; %! paramci_out = [23.9531, 1.3551; 33.5861, 2.1655]; %! assert_equal (paramhat, paramhat_out, 1e-4); %! assert_equal (paramci, paramci_out, 1e-4); %!test %! x = 1:50; %! [paramhat, paramci] = wblfit (x, 0.01); %! paramci_out = [22.7143, 1.2589; 35.4179, 2.3310]; %! assert_equal (paramci, paramci_out, 1e-4); ## Test input validation %!error wblfit (ones (2,5)); %!error wblfit ([-1 2 3 4]); %!error wblfit ([1, 2, 3, 4, 5], 1.2); %!error wblfit ([1, 2, 3, 4, 5], 0); %!error wblfit ([1, 2, 3, 4, 5], 'alpha'); %!error ... %! wblfit ([1, 2, 3, 4, 5], 0.05, [1 1 0]); %!error ... %! wblfit ([1, 2, 3, 4, 5], [], [1 1 0 1 1]'); %!error ... %! wblfit ([1, 2, 3, 4, 5], 0.05, zeros (1,5), [1 1 0]); %!error ... %! wblfit ([1, 2, 3, 4, 5], [], [], [1 1 0 -1 1]); %!error ... %! wblfit ([1, 2, 3, 4, 5], [], [], [1 1 0 1 1]'); %!error ... %! wblfit ([1, 2, 3, 4, 5], 0.05, [], [], 2); statistics-release-2.1.2/inst/Distribution_Fitting/wbllike.m000066400000000000000000000135561527311104300242620ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} wbllike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{acov}] =} wbllike (@var{params}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} wbllike (@var{params}, @var{x}, @var{alpha}, @var{censor}) ## @deftypefnx {statistics} {[@dots{}] =} wbllike (@var{params}, @var{x}, @var{alpha}, @var{censor}, @var{freq}) ## ## Negative log-likelihood for the Weibull distribution. ## ## @code{@var{nlogL} = wbllike (@var{params}, @var{data})} returns the negative ## log-likelihood of the data in @var{x} corresponding to the Weibull ## distribution with (1) scale parameter @math{lambda} and (2) shape parameter ## @math{k} given in the two-element vector @var{params}. ## ## @code{[@var{nlogL}, @var{acov}] = wbllike (@var{params}, @var{data})} also ## returns the inverse of Fisher's information matrix, @var{acov}. If the input ## parameter values in @var{params} are the maximum likelihood estimates, the ## diagonal elements of @var{acov} are their asymptotic variances. @var{acov} ## is based on the observed Fisher's information, not the expected information. ## ## @code{[@dots{}] = wbllike (@var{params}, @var{data}, @var{censor})} accepts a ## boolean vector, @var{censor}, of the same size as @var{x} with @qcode{1}s for ## observations that are right-censored and @qcode{0}s for observations that are ## observed exactly. By default, or if left empty, ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @code{[@dots{}] = wbllike (@var{params}, @var{data}, @var{censor}, ## @var{freq})} accepts a frequency vector, @var{freq}, of the same size as ## @var{x}. @var{freq} typically contains integer frequencies for the ## corresponding elements in @var{x}, but may contain any non-integer ## non-negative values. By default, or if left empty, ## @qcode{@var{freq} = ones (size (@var{x}))}. ## ## Further information about the Weibull distribution can be found at ## @url{https://en.wikipedia.org/wiki/Weibull_distribution} ## ## The @code{prob.WeibullDistribution} class names these same two parameters ## @qcode{A} and @qcode{B}, after MATLAB. @var{lambda} is its @qcode{A} and ## @var{k} is its @qcode{B}. ## @seealso{wblcdf, wblinv, wblpdf, wblrnd, wblfit, wblstat} ## @end deftypefn function [nlogL, acov] = wbllike (params, x, censor, freq) ## Check input arguments and add defaults if (nargin < 2) error ("wbllike: too few input arguments."); endif if (numel (params) != 2) error ("wbllike: wrong parameters length."); endif if (! isvector (x)) error ("wbllike: X must be a vector."); endif if (nargin < 3 || isempty (censor)) censor = zeros (size (x)); elseif (! isequal (size (x), size (censor))) error ("wbllike: X and CENSOR vectors mismatch."); endif if (nargin < 4 || isempty (freq)) freq = ones (size (x)); elseif (any (freq < 0)) error ("wbllike: FREQ cannot have negative values."); elseif (isequal (size (x), size (freq))) nulls = find (freq == 0); if (numel (nulls) > 0) x(nulls) = []; censor(nulls) = []; freq(nulls) = []; endif else error ("wbllike: X and FREQ vectors mismatch."); endif ## Get lambda and k parameter values l = params(1); k = params(2); ## Force NaNs for out of range parameters or x. l(l <= 0) = NaN; k(k <= 0) = NaN; x(x < 0) = NaN; ## Compute the individual log-likelihood terms z = x ./ l; logz = log (z); expz = exp (k .* logz); ilogL = ((k - 1) .* logz + log (k ./ l)) .* (1 - censor) - expz; ilogL(z == Inf) = -Inf; ## Sum up the individual log-likelihood contributions nlogL = -sum (freq .* ilogL); ## Compute the negative hessian and invert to get the information matrix. if (nargout > 1) ucen = (1 - censor); nH11 = sum (freq .* (k .* ((1 + k) .* expz - ucen))) ./ l .^ 2; nH12 = -sum (freq .* (((1 + k .* logz) .* expz - ucen))) ./ l; nH22 = sum (freq .* ((logz .^ 2) .* expz + ucen ./ k .^ 2)); acov = [nH22, -nH12; -nH12, nH11] / (nH11 * nH22 - nH12 * nH12); endif endfunction ## Test output %!test %! x = 1:50; %! [nlogL, acov] = wbllike ([2.3, 1.2], x); %! avar_out = [0.0250, 0.0062; 0.0062, 0.0017]; %! assert_equal (nlogL, 945.9589180651594, 1e-12); %! assert_equal (acov, avar_out, 1e-4); %!test %! x = 1:50; %! [nlogL, acov] = wbllike ([2.3, 1.2], x * 0.5); %! avar_out = [-0.3238, -0.1112; -0.1112, -0.0376]; %! assert_equal (nlogL, 424.9879809704742, 6e-14); %! assert_equal (acov, avar_out, 1e-4); %!test %! x = 1:50; %! [nlogL, acov] = wbllike ([21, 15], x); %! avar_out = [-0.00001236, -0.00001166; -0.00001166, -0.00001009]; %! assert_equal (nlogL, 1635190.328991511, 1e-8); %! assert_equal (acov, avar_out, 1e-8); ## Test input validation %!error wbllike ([12, 15]); %!error wbllike ([12, 15, 3], [1:50]); %!error wbllike ([12, 3], ones (10, 2)); %!error wbllike ([12, 15], [1:50], [1, 2, 3]); %!error wbllike ([12, 15], [1:50], [], [1, 2, 3]); %!error ... %! wbllike ([12, 15], [1:5], [], [1, 2, 3, -1, 0]); statistics-release-2.1.2/inst/Distribution_Functions/000077500000000000000000000000001527311104300230055ustar00rootroot00000000000000statistics-release-2.1.2/inst/Distribution_Functions/betacdf.m000066400000000000000000000157671527311104300245730ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} betacdf (@var{x}, @var{a}, @var{b}) ## @deftypefnx {statistics} {@var{p} =} betacdf (@var{x}, @var{a}, @var{b}, @qcode{'upper'}) ## ## Beta cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function of ## the Beta distribution with shape parameters @var{a} and @var{b}. The size of ## @var{p} is the common size of @var{x}, @var{a}, and @var{b}. A scalar input ## functions as a constant matrix of the same size as the other inputs. ## ## @code{@var{p} = betacdf (@var{x}, @var{a}, @var{b}, "upper")} computes the ## upper tail probability of the Beta distribution with parameters @var{a} and ## @var{b}, at the values in @var{x}. ## ## Further information about the Beta distribution can be found at ## @url{https://en.wikipedia.org/wiki/Beta_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{betainv, betapdf, betarnd, betafit, betalike, betastat} ## @end deftypefn function p = betacdf (x, a, b, uflag) ## Check for valid number of input arguments if (nargin < 3) error ("betacdf: function called with too few input arguments."); endif ## Check for valid "upper" flag if (nargin > 3) if (! strcmpi (uflag, 'upper')) error ("betacdf: invalid argument for upper tail."); else uflag = true; endif else uflag = false; endif ## Check for common size of X, A, and B if (! isscalar (x) || ! isscalar (a) || ! isscalar (b)) [err, x, a, b] = common_size (x, a, b); if (err > 0) error ("betacdf: X, A, and B must be of common size or scalars."); endif endif ## Check for X, A, and B being double or single if (! (isfloat (x) && isfloat (a) && isfloat (b))) error ("betacdf: X, A, and B must be double or single."); endif ## Check for X, A, and B being reals if (iscomplex (x) || iscomplex (a) || iscomplex (b)) error ("betacdf: X, A, and B must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (a, 'single') || isa (b, 'single')) is_type = 'single'; else is_type = 'double'; endif ## Find valid values in parameters and data okPARAM = (0 < a & a < Inf) & (0 < b & b < Inf); okDATA = (okPARAM & (0 <= x & x <= 1)); all_OK = all (okDATA(:)); ## Force NaNs for out of range parameters. ## Fill in edges cases when X is outside 0 or 1. if (! all_OK) p = NaN (size (okDATA), is_type); if (uflag) p(okPARAM & x <= 0) = 1; p(okPARAM & x >= 1) = 0; else p(okPARAM & x < 0) = 0; p(okPARAM & x > 1) = 1; endif ## Remove the out of range/edge cases. Return, if there's nothing left. if (any (okDATA(:))) if (numel (x) > 1) x = x(okDATA); endif if (numel (a) > 1) a = a(okDATA); endif if (numel (b) > 1) b = b(okDATA); endif else return; endif endif ## Call betainc for the actual work if (uflag) pk = betainc (x, a, b, 'upper'); else pk = betainc (x, a, b); endif ## Relocate the values to the correct places if necessary. if all_OK p = pk; else p(okDATA) = pk; endif endfunction %!demo %! ## Plot various CDFs from the Beta distribution %! x = 0:0.005:1; %! p1 = betacdf (x, 0.5, 0.5); %! p2 = betacdf (x, 5, 1); %! p3 = betacdf (x, 1, 3); %! p4 = betacdf (x, 2, 2); %! p5 = betacdf (x, 2, 5); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', x, p4, '-c', x, p5, '-m') %! grid on %! legend ({'α = β = 0.5', 'α = 5, β = 1', 'α = 1, β = 3', ... %! 'α = 2, β = 2', 'α = 2, β = 5'}, 'location', 'northwest') %! title ('Beta CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, y, x1, x2 %! x = [-1 0 0.5 1 2]; %! y = [0 0 0.75 1 1]; %!assert_equal (betacdf (x, ones (1, 5), 2 * ones (1, 5)), y) %!assert_equal (betacdf (x, 1, 2 * ones (1, 5)), y) %!assert_equal (betacdf (x, ones (1, 5), 2), y) %!assert_equal (betacdf (x, [0 1 NaN 1 1], 2), [NaN 0 NaN 1 1]) %!assert_equal (betacdf (x, 1, 2 * [0 1 NaN 1 1]), [NaN 0 NaN 1 1]) %!assert_equal (betacdf ([x(1:2) NaN x(4:5)], 1, 2), [y(1:2) NaN y(4:5)]) %! x1 = [0.1:0.2:0.9]; %!assert_equal (betacdf (x1, 2, 2), [0.028, 0.216, 0.5, 0.784, 0.972], 1e-14); %!assert_equal (betacdf (x1, 2, 2, 'upper'), 1 - [0.028, 0.216, 0.5, 0.784, 0.972],... %! 1e-14); %! x2 = [1, 2, 3]; %!assert_equal (betacdf (0.5, x2, x2), [0.5, 0.5, 0.5], 1e-14); %!assert_equal (betacdf ([x, NaN], 1, 2), [y, NaN]) ## Test class of input preserved %!assert_equal (betacdf (single ([x, NaN]), 1, 2), single ([y, NaN])) %!assert_equal (betacdf ([x, NaN], single (1), 2), single ([y, NaN])) %!assert_equal (betacdf ([x, NaN], 1, single (2)), single ([y, NaN])) ## Test input validation %!error betacdf () %!error betacdf (1) %!error betacdf (1, 2) %!error betacdf (1, 2, 3, 4, 5) %!error betacdf (1, 2, 3, 'tail') %!error betacdf (1, 2, 3, 4) %!error ... %! betacdf (ones (3), ones (2), ones (2)) %!error ... %! betacdf (ones (2), ones (3), ones (2)) %!error ... %! betacdf (ones (2), ones (2), ones (3)) %!error betacdf (int32 (2), 2, 2) %!error betacdf (true, 2, 2) %!error betacdf ('a', 2, 2) %!error betacdf (i, 2, 2) %!error betacdf (2, i, 2) %!error betacdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/betainv.m000066400000000000000000000235131527311104300246170ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} betainv (@var{p}, @var{a}, @var{b}) ## ## Inverse of the Beta distribution (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) ## of the Beta distribution with shape parameters @var{a} and @var{b}. The size ## of @var{x} is the common size of @var{x}, @var{a}, and @var{b}. A scalar ## input functions as a constant matrix of the same size as the other inputs. ## ## Further information about the Beta distribution can be found at ## @url{https://en.wikipedia.org/wiki/Beta_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{betacdf, betapdf, betarnd, betafit, betalike, betastat} ## @end deftypefn function x = betainv (p, a, b) ## Check for valid number of input arguments if (nargin < 3) error ("betainv: function called with too few input arguments."); endif ## Check for common size of P, A, and B if (! isscalar (p) || ! isscalar (a) || ! isscalar (b)) [retval, p, a, b] = common_size (p, a, b); if (retval > 0) error ("betainv: P, A, and B must be of common size or scalars."); endif endif ## Check for P, A, and B being double or single if (! (isfloat (p) && isfloat (a) && isfloat (b))) error ("betainv: P, A, and B must be double or single."); endif ## Check for P, A, and B being reals if (iscomplex (p) || iscomplex (a) || iscomplex (b)) error ("betainv: P, A, and B must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (a, 'single') || isa (b, 'single')) x = zeros (size (p), 'single'); else x = zeros (size (p)); endif k = (p < 0) | (p > 1) | ! (a > 0) | ! (b > 0) | isnan (p); x(k) = NaN; k = (p == 1) & (a > 0) & (b > 0); x(k) = 1; k = find ((p > 0) & (p < 1) & (a > 0) & (b > 0)); if (! isempty (k)) if (isscalar (a)) a = a * ones (size (k)); else a = a(k); endif if (isscalar (b)) b = b * ones (size (k)); else b = b(k); endif x(k) = bInverse (double (p(k)(:)), double (a(:)), double (b(:))); endif endfunction ## The quantiles, solved by Newton's method on betainc kept inside a bracket. ## Below the median the lower tail is matched to P, above it the upper tail ## to 1 - P, which is exact there, so neither tail loses its digits to a ## subtraction from 1. The steps stop on a relative tolerance, so a ## quantile of 1e-20 is resolved as closely as one of 0.5. function x = bInverse (p, a, b) up = p > 0.5; q = p; q(up) = 1 - p(up); lbeta = betaln (a, b); ## Start from the leading term of the tail: x^a / (a B) below the median, ## (1 - x)^b / (b B) above it, and from the mean where that falls outside x = exp ((log (q) + log (a) + lbeta) ./ a); x(up) = -expm1 ((log (q(up)) + log (b(up)) + lbeta(up)) ./ b(up)); bad = ! (x > 0 & x < 1); x(bad) = a(bad) ./ (a(bad) + b(bad)); lo = zeros (size (x)); hi = ones (size (x)); todo = true (size (x)); for iter = 1:200 i = find (todo); if (isempty (i)) break; endif xi = x(i); ## The tail matched, signed so that it grows with x g = betainc (xi, a(i), b(i)) - q(i); u = up(i); g(u) = q(i)(u) - betainc (xi(u), a(i)(u), b(i)(u), 'upper'); ## The root lies below where the tail is too large over = g > 0; hi(i(over)) = xi(over); lo(i(! over)) = xi(! over); ## Newton's step, and bisection where it leaves the bracket; geometric ## bisection where the bracket spans more than a factor of 4 dens = exp ((a(i) - 1) .* log (xi) + (b(i) - 1) .* log1p (-xi) - lbeta(i)); xn = xi - g ./ dens; xn(g == 0) = xi(g == 0); out = ! (xn >= lo(i) & xn <= hi(i)); geo = out & lo(i) > 0 & hi(i) > 4 * lo(i); xn(geo) = sqrt (lo(i)(geo) .* hi(i)(geo)); fromzero = out & lo(i) == 0; xn(fromzero) = hi(i)(fromzero) / 16; mid = out & ! geo & ! fromzero; xn(mid) = (lo(i)(mid) + hi(i)(mid)) / 2; x(i) = xn; ## Near a root betainc is noisy by some tens of units in the last place ## of its value, below which no step can improve the answer done = abs (g) <= 64 * eps (q(i)) | abs (xn - xi) <= 8 * eps (xn) ... | (hi(i) - lo(i)) <= 64 * eps (lo(i)); todo(i(done)) = false; endfor ## Values still stepping have reached betainc's own noise; only a residual ## well above it means the solution was not found if (any (todo)) j = find (todo); if (any (abs (bResidual (x(j), q(j), a(j), b(j), up(j))) > 1e-10 * q(j))) warning ("betainv: calculation failed to converge for some values."); endif endif ## Step to a neighbouring double for as long as it matches its tail better i = (1:numel (x))'; for step = 1:64 gx = abs (bResidual (x(i), q(i), a(i), b(i), up(i))); moved = false (size (i)); for d = [-1, 1] y = x(i) + d * eps (x(i)); ok = y >= 0 & y <= 1; y = min (max (y, 0), 1); gy = abs (bResidual (y, q(i), a(i), b(i), up(i))); better = ok & gy < gx; x(i(better)) = y(better); gx(better) = gy(better); moved |= better; endfor i = i(moved); if (isempty (i)) break; endif endfor endfunction ## How far the tail matched at X is from its target. function g = bResidual (x, q, a, b, up) g = betainc (x, a, b) - q; g(up) = q(up) - betainc (x(up), a(up), b(up), 'upper'); endfunction %!demo %! ## Plot various iCDFs from the Beta distribution %! p = 0.001:0.001:0.999; %! x1 = betainv (p, 0.5, 0.5); %! x2 = betainv (p, 5, 1); %! x3 = betainv (p, 1, 3); %! x4 = betainv (p, 2, 2); %! x5 = betainv (p, 2, 5); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', p, x4, '-c', p, x5, '-m') %! grid on %! legend ({'α = β = 0.5', 'α = 5, β = 1', 'α = 1, β = 3', ... %! 'α = 2, β = 2', 'α = 2, β = 5'}, 'location', 'southeast') %! title ('Beta iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p %! p = [-1 0 0.75 1 2]; %!assert_equal (betainv (p, ones (1,5), 2*ones (1,5)), [NaN 0 0.5 1 NaN], eps) %!assert_equal (betainv (p, 1, 2*ones (1,5)), [NaN 0 0.5 1 NaN], eps) %!assert_equal (betainv (p, ones (1,5), 2), [NaN 0 0.5 1 NaN], eps) %!assert_equal (betainv (p, [1 0 NaN 1 1], 2), [NaN NaN NaN 1 NaN]) %!assert_equal (betainv (p, 1, 2*[1 0 NaN 1 1]), [NaN NaN NaN 1 NaN]) %!assert_equal (betainv ([p(1:2) NaN p(4:5)], 1, 2), [NaN 0 NaN 1 NaN]) ## Closed forms, deep into both tails %!test %! pp = [1e-300, 1e-20, 0.025, 0.5, 1 - 1e-12]; %! assert_equal (betainv (pp, 1, 1000), -expm1 (log1p (-pp) / 1000), -1e-14); %!assert_equal (betainv ([1e-300, 1e-20, 0.3], 30, 1), ... %! [1e-300, 1e-20, 0.3] .^ (1/30), -1e-14) %!assert_equal (betainv (2e-10, 0.5, 0.5), sin (pi * 1e-10) ^ 2, -1e-14) ## Expected values from MATLAB R2024a, among them the upper tail of a small ## first shape, where core's betaincinv misses %!assert_equal (betainv (1e-10, 0.5, 50), 1.578669844608891e-22, -1e-12) %!assert_equal (betainv (0.999, 0.5, 50), 0.103102263418713, -1e-13) %!assert_equal (betainv (1 - 1e-9, 0.5, 1000), 0.018493954158234, -1e-13) %!assert_equal (betainv (0.3, 2.5, 7.3), 0.170751332194546, -1e-14) %!assert_equal (betainv (1e-6, 30, 200), 0.048760009765902, -5e-14) %!assert_equal (betainv (1e-20, 2, 3), 4.082482904749744e-11, -1e-14) %!assert_equal (betainv (0.975, 0.5, 0.5), 0.998458666866564, -1e-14) %!assert_equal (size (betainv (0.3 * ones (2, 3), 2, 5)), [2, 3]) ## Test class of input preserved %!assert_equal (betainv ([p, NaN], 1, 2), [NaN 0 0.5 1 NaN NaN], eps) %!assert_equal (betainv (single ([p, NaN]), 1, 2), single ([NaN 0 0.5 1 NaN NaN])) %!assert_equal (betainv ([p, NaN], single (1), 2), single ([NaN 0 0.5 1 NaN NaN]), eps ('single')) %!assert_equal (betainv ([p, NaN], 1, single (2)), single ([NaN 0 0.5 1 NaN NaN]), eps ('single')) ## Test input validation %!error betainv () %!error betainv (1) %!error betainv (1,2) %!error betainv (1,2,3,4) %!error ... %! betainv (ones (3), ones (2), ones (2)) %!error ... %! betainv (ones (2), ones (3), ones (2)) %!error ... %! betainv (ones (2), ones (2), ones (3)) %!error betainv (int32 (2), 2, 2) %!error betainv (true, 2, 2) %!error betainv ('a', 2, 2) %!error betainv (i, 2, 2) %!error betainv (2, i, 2) %!error betainv (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/betapdf.m000066400000000000000000000147611527311104300246010ustar00rootroot00000000000000## Copyright (C) 2010 Christos Dimitrakakis ## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} betapdf (@var{x}, @var{a}, @var{b}) ## ## Beta probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the Beta distribution with shape parameters @var{a} and @var{b}. The size ## of @var{y} is the common size of @var{x}, @var{a}, and @var{b}. A scalar ## input functions as a constant matrix of the same size as the other inputs. ## ## Further information about the Beta distribution can be found at ## @url{https://en.wikipedia.org/wiki/Beta_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{betacdf, betainv, betarnd, betafit, betalike, betastat} ## @end deftypefn function y = betapdf (x, a, b) ## Check for valid number of input arguments if (nargin < 3) error ("betapdf: function called with too few input arguments."); endif ## Check for common size of X, A, and B if (! isscalar (x) || ! isscalar (a) || ! isscalar (b)) [retval, x, a, b] = common_size (x, a, b); if (retval > 0) error ("betapdf: X, A, and B must be of common size or scalars."); endif endif ## Check for X, A, and B being double or single if (! (isfloat (x) && isfloat (a) && isfloat (b))) error ("betapdf: X, A, and B must be double or single."); endif ## Check for X, A, and B being reals if (iscomplex (x) || iscomplex (a) || iscomplex (b)) error ("betapdf: X, A, and B must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (a, 'single') || isa (b, 'single')); y = zeros (size (x), 'single'); else y = zeros (size (x)); endif k = ! (a > 0) | ! (b > 0) | isnan (x); y(k) = NaN; k = (x > 0) & (x < 1) & (a > 0) & (b > 0) & ((a != 1) | (b != 1)); if (isscalar (a) && isscalar (b)) y(k) = exp ((a - 1) * log (x(k)) + (b - 1) * log (1 - x(k)) + gammaln (a + b) - gammaln (a) - gammaln (b)); else y(k) = exp ((a(k) - 1) .* log (x(k)) + (b(k) - 1) .* log (1 - x(k)) + gammaln (a(k) + b(k)) - gammaln (a(k)) - gammaln (b(k))); endif ## Most important special cases when the density is finite. k = (x == 0) & (a == 1) & (b > 0) & (b != 1); if (isscalar (a) && isscalar (b)) y(k) = exp (gammaln (a + b) - gammaln (a) - gammaln (b)); else y(k) = exp (gammaln (a(k) + b(k)) - gammaln (a(k)) - gammaln (b(k))); endif k = (x == 1) & (b == 1) & (a > 0) & (a != 1); if (isscalar (a) && isscalar (b)) y(k) = exp (gammaln (a + b) - gammaln (a) - gammaln (b)); else y(k) = exp (gammaln (a(k) + b(k)) - gammaln (a(k)) - gammaln (b(k))); endif k = (x >= 0) & (x <= 1) & (a == 1) & (b == 1); y(k) = 1; ## Other special case when the density at the boundary is infinite. k = (x == 0) & (a < 1); y(k) = Inf; k = (x == 1) & (b < 1); y(k) = Inf; endfunction %!demo %! ## Plot various PDFs from the Beta distribution %! x = 0.001:0.001:0.999; %! y1 = betapdf (x, 0.5, 0.5); %! y2 = betapdf (x, 5, 1); %! y3 = betapdf (x, 1, 3); %! y4 = betapdf (x, 2, 2); %! y5 = betapdf (x, 2, 5); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', x, y4, '-c', x, y5, '-m') %! grid on %! ylim ([0, 2.5]) %! legend ({'α = β = 0.5', 'α = 5, β = 1', 'α = 1, β = 3', ... %! 'α = 2, β = 2', 'α = 2, β = 5'}, 'location', 'north') %! title ('Beta PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x, y %! x = [-1 0 0.5 1 2]; %! y = [0 2 1 0 0]; %!assert_equal (betapdf (x, ones (1, 5), 2 * ones (1, 5)), y) %!assert_equal (betapdf (x, 1, 2 * ones (1, 5)), y) %!assert_equal (betapdf (x, ones (1, 5), 2), y) %!assert_equal (betapdf (x, [0 NaN 1 1 1], 2), [NaN NaN y(3:5)]) %!assert_equal (betapdf (x, 1, 2 * [0 NaN 1 1 1]), [NaN NaN y(3:5)]) %!assert_equal (betapdf ([x, NaN], 1, 2), [y, NaN]) ## Test class of input preserved %!assert_equal (betapdf (single ([x, NaN]), 1, 2), single ([y, NaN])) %!assert_equal (betapdf ([x, NaN], single (1), 2), single ([y, NaN])) %!assert_equal (betapdf ([x, NaN], 1, single (2)), single ([y, NaN])) ## Beta (1/2,1/2) == arcsine distribution %!test %! x = rand (10,1); %! y = 1 ./ (pi * sqrt (x .* (1 - x))); %! assert_equal (betapdf (x, 1/2, 1/2), y, 1e-12); ## Test large input values to betapdf %!assert_equal (betapdf (0.5, 1000, 1000), 35.678, 1e-3) ## Test input validation %!error betapdf () %!error betapdf (1) %!error betapdf (1,2) %!error betapdf (1,2,3,4) %!error ... %! betapdf (ones (3), ones (2), ones (2)) %!error ... %! betapdf (ones (2), ones (3), ones (2)) %!error ... %! betapdf (ones (2), ones (2), ones (3)) %!error betapdf (int32 (2), 2, 2) %!error betapdf (true, 2, 2) %!error betapdf ('a', 2, 2) %!error betapdf (i, 2, 2) %!error betapdf (2, i, 2) %!error betapdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/betarnd.m000066400000000000000000000163721527311104300246130ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} betarnd (@var{a}, @var{b}) ## @deftypefnx {statistics} {@var{r} =} betarnd (@var{a}, @var{b}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} betarnd (@var{a}, @var{b}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} betarnd (@var{a}, @var{b}, [@var{sz}]) ## ## Random arrays from the Beta distribution. ## ## @code{@var{r} = betarnd (@var{a}, @var{b})} returns an array of random ## numbers chosen from the Beta distribution with shape parameters @var{a} and ## @var{b}. The size of @var{r} is the common size of @var{a} and @var{b}. ## A scalar input functions as a constant matrix of the same size as the other ## inputs. ## ## When called with a single size argument, @code{betarnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the Beta distribution can be found at ## @url{https://en.wikipedia.org/wiki/Beta_distribution} ## ## @seealso{betacdf, betainv, betapdf, betafit, betalike, betastat} ## @end deftypefn function r = betarnd (a, b, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("betarnd: function called with too few input arguments."); endif ## Check for common size of A and B if (! isscalar (a) || ! isscalar (b)) [retval, a, b] = common_size (a, b); if (retval > 0) error ("betarnd: A and B must be of common size or scalars."); endif endif ## Check for A and B being reals if (iscomplex (a) || iscomplex (b)) error ("betarnd: A and B must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (a); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("betarnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("betarnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (a) && ! isequal (size (a), size (ones (sz)))) error ("betarnd: A and B must be scalars or of size SZ."); endif ## Check for class type if (isa (a, 'single') || isa (b, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from Beta distribution if (isscalar (a) && isscalar (b)) if ((a > 0) && (a < Inf) && (b > 0) && (b < Inf)) tmpr = randg (a, sz, cls); r = tmpr ./ (tmpr + randg (b, sz, cls)); else r = NaN (sz, cls); endif else r = NaN (sz, cls); k = (a > 0) & (a < Inf) & (b > 0) & (b < Inf); tmpr = randg (a(k), cls); r(k) = tmpr ./ (tmpr + randg (b(k), cls)); endif endfunction ## Test output %!assert_equal (size (betarnd (2, 1/2)), [1 1]) %!assert_equal (size (betarnd (2 * ones (2, 1), 1/2)), [2, 1]) %!assert_equal (size (betarnd (2 * ones (2, 2), 1/2)), [2, 2]) %!assert_equal (size (betarnd (2, 1/2 * ones (2, 1))), [2, 1]) %!assert_equal (size (betarnd (1, 1/2 * ones (2, 2))), [2, 2]) %!assert_equal (size (betarnd (ones (2, 1), 1)), [2, 1]) %!assert_equal (size (betarnd (ones (2, 2), 1)), [2, 2]) %!assert_equal (size (betarnd (2, 1/2, 3)), [3, 3]) %!assert_equal (size (betarnd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (betarnd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (betarnd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (betarnd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (betarnd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (betarnd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (betarnd (1, 1, [])), [0, 0]) %!assert_equal (size (betarnd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (betarnd (1, 1/2, -1)), [0, 0]) %!assert_equal (size (betarnd (1, 1/2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (betarnd (1, 1/2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (betarnd (1, 1)), "double") %!assert_equal (class (betarnd (1, single (0))), "single") %!assert_equal (class (betarnd (1, single ([0, 0]))), "single") %!assert_equal (class (betarnd (1, single (1), 2)), "single") %!assert_equal (class (betarnd (1, single ([1, 1]), 1, 2)), "single") %!assert_equal (class (betarnd (single (1), 1, 2)), "single") %!assert_equal (class (betarnd (single ([1, 1]), 1, 1, 2)), "single") ## Test input validation %!error betarnd () %!error betarnd (1) %!error ... %! betarnd (ones (3), ones (2)) %!error ... %! betarnd (ones (2), ones (3)) %!error betarnd (i, 2) %!error betarnd (1, i) %!error ... %! betarnd (1, 1/2, 1.2) %!error ... %! betarnd (1, 1/2, ones (2)) %!error ... %! betarnd (1, 1/2, [2 0 2.5]) %!error ... %! betarnd (1, 1/2, 2, 1.5, 5) %!error ... %! betarnd (2, 1/2 * ones (2), 3) %!error ... %! betarnd (2, 1/2 * ones (2), [3, 2]) %!error ... %! betarnd (2, 1/2 * ones (2), 3, 2) %!error ... %! betarnd (2 * ones (2), 1/2, 3) %!error ... %! betarnd (2 * ones (2), 1/2, [3, 2]) %!error ... %! betarnd (2 * ones (2), 1/2, 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/binocdf.m000066400000000000000000000165521527311104300246000ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} binocdf (@var{x}, @var{n}, @var{ps}) ## @deftypefnx {statistics} {@var{p} =} binocdf (@var{x}, @var{n}, @var{ps}, @qcode{'upper'}) ## ## Binomial cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the binomial distribution with parameters @var{n} and @var{ps}, ## where @var{n} is the number of trials and @var{ps} is the probability of ## success. The size of @var{p} is the common size of @var{x}, @var{n}, and ## @var{ps}. A scalar input functions as a constant matrix of the same size as ## the other inputs. ## ## @code{@var{p} = binocdf (@var{x}, @var{n}, @var{ps}, "upper")} computes the ## upper tail probability of the binomial distribution with parameters ## @var{n} and @var{ps}, at the values in @var{x}. ## ## Further information about the binomial distribution can be found at ## @url{https://en.wikipedia.org/wiki/Binomial_distribution} ## ## Input arguments must be @qcode{double}, @qcode{single}, or an integer type; ## logical and character arrays are rejected. Integer input is promoted to ## @qcode{double}, so the result is always a probability. MATLAB is ## inconsistent here: for several of the discrete distributions it returns the ## result in the integer class of the input, truncating a probability to ## @math{0} or @math{1}. ## ## @seealso{binoinv, binopdf, binornd, binofit, binolike, binostat, binotest} ## @end deftypefn function p = binocdf (x, n, ps, uflag) ## Check for valid number of input arguments if (nargin < 3) error ("binocdf: function called with too few input arguments."); endif ## Check for valid "upper" flag if (nargin == 4) if (strcmp (uflag, 'upper')) uflag = true; else error ("binocdf: invalid argument for upper tail."); endif else uflag = false; endif ## Check for common size of X, N, and PS if (! isscalar (x) || ! isscalar (n) || ! isscalar (ps)) [retval, x, n, ps] = common_size (x, n, ps); if (retval > 0) error ("binocdf: X, N, and PS must be of common size or scalars."); endif endif ## Check for X, N, and PS being double, single, or integer if (! (isnumeric (x) && isnumeric (n) && isnumeric (ps))) error ("binocdf: X, N, and PS must be double, single, or integer."); endif ## Integer input is promoted to double, so the result is a probability ## rather than a value truncated to the input's integer type. if (isinteger (x)) x = double (x); endif if (isinteger (n)) n = double (n); endif if (isinteger (ps)) ps = double (ps); endif ## Check for X, N, and PS being reals if (iscomplex (x) || iscomplex (n) || iscomplex (ps)) error ("binocdf: X, N, and PS must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (n, 'single') || isa (ps, 'single')); p = nan (size (x), 'single'); else p = nan (size (x)); endif k = (x >= n) & (n >= 0) & (n == fix (n) & (ps >= 0) & (ps <= 1)); p(k) = ! uflag; k = (x < 0) & (n >= 0) & (n == fix (n) & (ps >= 0) & (ps <= 1)); p(k) = uflag; k = (x >= 0) & (x < n) & (n == fix (n)) & (ps >= 0) & (ps <= 1); tmp = floor (x(k)); if (! uflag) if (isscalar (n) && isscalar (ps)) p(k) = betainc (1 - ps, n - tmp, tmp + 1); else p(k) = betainc (1 - ps(k), n(k) - tmp, tmp + 1); endif else if (isscalar (n) && isscalar (ps)); p(k) = betainc (ps, tmp + 1, n - tmp); else p(k) = betainc (ps(k), tmp + 1, n(k) - tmp); endif endif endfunction %!demo %! ## Plot various CDFs from the binomial distribution %! x = 0:40; %! p1 = binocdf (x, 20, 0.5); %! p2 = binocdf (x, 20, 0.7); %! p3 = binocdf (x, 40, 0.5); %! plot (x, p1, '*b', x, p2, '*g', x, p3, '*r') %! grid on %! legend ({'n = 20, ps = 0.5', 'n = 20, ps = 0.7', ... %! 'n = 40, ps = 0.5'}, 'location', 'southeast') %! title ('Binomial CDF') %! xlabel ('values in x (number of successes)') %! ylabel ('probability') ## Test output %!shared x, p, p1 %! x = [-1 0 1 2 3]; %! p = [0 1/4 3/4 1 1]; %! p1 = 1 - p; %!assert_equal (binocdf (x, 2 * ones (1, 5), 0.5 * ones (1, 5)), p, eps) %!assert_equal (binocdf (x, 2, 0.5 * ones (1, 5)), p, eps) %!assert_equal (binocdf (x, 2 * ones (1, 5), 0.5), p, eps) %!assert_equal (binocdf (x, 2 * [0 -1 NaN 1.1 1], 0.5), [0 NaN NaN NaN 1]) %!assert_equal (binocdf (x, 2, 0.5 * [0 -1 NaN 3 1]), [0 NaN NaN NaN 1]) %!assert_equal (binocdf ([x(1:2) NaN x(4:5)], 2, 0.5), [p(1:2) NaN p(4:5)], eps) %!assert_equal (binocdf (99, 100, 0.1, 'upper'), 1e-100, 1e-112); %!assert_equal (binocdf (x, 2 * ones (1, 5), 0.5*ones (1,5), 'upper'), p1, eps) %!assert_equal (binocdf (x, 2, 0.5 * ones (1, 5), 'upper'), p1, eps) %!assert_equal (binocdf (x, 2 * ones (1, 5), 0.5, 'upper'), p1, eps) %!assert_equal (binocdf (x, 2 * [0 -1 NaN 1.1 1], 0.5, 'upper'), [1 NaN NaN NaN 0]) %!assert_equal (binocdf (x, 2, 0.5 * [0 -1 NaN 3 1], 'upper'), [1 NaN NaN NaN 0]) %!assert_equal (binocdf ([x(1:2) NaN x(4:5)], 2, 0.5, 'upper'), [p1(1:2) NaN p1(4:5)]) %!assert_equal (binocdf ([x, NaN], 2, 0.5), [p, NaN], eps) ## Test class of input preserved %!assert_equal (binocdf (single ([x, NaN]), 2, 0.5), single ([p, NaN])) %!assert_equal (binocdf ([x, NaN], single (2), 0.5), single ([p, NaN])) %!assert_equal (binocdf ([x, NaN], 2, single (0.5)), single ([p, NaN])) ## Test input validation %!error binocdf () %!error binocdf (1) %!error binocdf (1, 2) %!error binocdf (1, 2, 3, 4, 5) %!error binocdf (1, 2, 3, 'tail') %!error binocdf (1, 2, 3, 4) %!error ... %! binocdf (ones (3), ones (2), ones (2)) %!error ... %! binocdf (ones (2), ones (3), ones (2)) %!error ... %! binocdf (ones (2), ones (2), ones (3)) %!error binocdf (true, 2, 2) %!error binocdf ('a', 2, 2) %!assert_equal (class (binocdf (int32 (2), 2, 2)), 'double') %!error binocdf (i, 2, 2) %!error binocdf (2, i, 2) %!error binocdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/binoinv.m000066400000000000000000000263141527311104300246350ustar00rootroot00000000000000## Copyright (C) 2016-2017 Lachlan Andrew ## Copyright (C) 2012-2016 Rik Wehbring ## Copyright (C) 1995-2012 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} binoinv (@var{p}, @var{n}, @var{ps}) ## ## Inverse of the Binomial cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the binomial distribution with parameters @var{n} and @var{ps}, where @var{n} ## is the number of trials and @var{ps} is the probability of success. The size ## of @var{x} is the common size of @var{p}, @var{n}, and @var{ps}. A scalar ## input functions as a constant matrix of the same size as the other inputs. ## ## Further information about the binomial distribution can be found at ## @url{https://en.wikipedia.org/wiki/Binomial_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{binocdf, binopdf, binornd, binofit, binolike, binostat, binotest} ## @end deftypefn function x = binoinv (p, n, ps) ## Check for valid number of input arguments if (nargin < 3) error ("binoinv: function called with too few input arguments."); endif ## Check for common size of P, N, and PS if (! isscalar (n) || ! isscalar (ps)) [retval, p, n, ps] = common_size (p, n, ps); if (retval > 0) error ("binoinv: P, N, and PS must be of common size or scalars."); endif endif ## Check for P, N, and PS being double or single if (! (isfloat (p) && isfloat (n) && isfloat (ps))) error ("binoinv: P, N, and PS must be double or single."); endif ## Check for P, N, and PS being reals if (iscomplex (p) || iscomplex (n) || iscomplex (ps)) error ("binoinv: P, N, and PS must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (n, 'single') || isa (ps, 'single')); x = zeros (size (p), 'single'); else x = zeros (size (p)); endif k = (! (p >= 0) | ! (p <= 1) | ! (n >= 0) | (n != fix (n)) | ! (ps >= 0) | ... ! (ps <= 1)); x(k) = NaN; k = find ((p >= 0) & (p <= 1) & (n >= 0) & (n == fix (n) ... & (ps >= 0) & (ps <= 1))); if (! isempty (k)) pk = p(k)(:); if (isscalar (n) && isscalar (ps)) [xk, unfinished] = scalar_binoinv (pk, n, ps); if (! isempty (unfinished)) xk(unfinished) = bin_search_binoinv (pk(unfinished), n, ps); endif xk = tie_correct (xk, pk, n, ps); ## P of 1 attains N. The CDF saturates to 1 well below it, reading 1 at ## 206 for 500 trials at 0.25, so no search can locate it. xk(pk == 1) = n; else nk = n(k)(:); psk = ps(k)(:); [xk, unfinished] = vector_binoinv (pk, nk, psk); if (! isempty (unfinished)) xk(unfinished) = bin_search_binoinv (pk(unfinished), nk(unfinished), ... psk(unfinished)); endif xk = tie_correct (xk, pk, nk, psk); xk(pk == 1) = nk(pk == 1); endif x(k) = xk; endif endfunction ## Core algorithm to calculate the inverse binomial, for n and ps real scalars ## and x a column vector, and for which the output is not NaN or Inf. ## Compute CDF in batches of doubling size until CDF > p, or answer > 500 ## Return the locations of unfinished cases in k. function [m, k] = scalar_binoinv (p, n, ps) k = 1:length (p); m = zeros (size (p)); prev_limit = 0; limit = 10; cdf = 0; v = 0; do cdf = binocdf (prev_limit:limit-1, n, ps); r = bsxfun (@le, p(k), cdf); [v, m(k)] = max (r, [], 2); # find first instance of p <= cdf m(k) += prev_limit - 1; k = k(v == 0); prev_limit = limit; limit += limit; until (isempty (k) || limit >= 1000) endfunction ## Core algorithm to calculate the inverse binomial, for n, ps, and x column ## vectors, and for which the output is not NaN or Inf. ## Compute CDF in batches of doubling size until CDF > p, or answer > 500 ## Return the locations of unfinished cases in k. ## Calculates CDF by summing PDF, which is faster than calls to binocdf. function [m, k] = vector_binoinv (p, n, ps) k = 1:length (p); m = zeros (size (p)); prev_limit = 0; limit = 10; cdf = 0; v = 0; do xx = repmat (prev_limit:limit-1, [length(k), 1]); nn = kron (ones (1, limit-prev_limit), n(k)); pp = kron (ones (1, limit-prev_limit), ps(k)); pdf = binopdf (xx, nn, pp); pdf(:,1) += cdf(v==0, end); cdf = cumsum (pdf, 2); r = bsxfun (@le, p(k), cdf); [v, m(k)] = max (r, [], 2); # find first instance of p <= cdf m(k) += prev_limit - 1; k = k(v == 0); prev_limit = limit; limit += min (limit, max (1e4/numel (k), 10)); # limit memory use until (isempty (k) || limit >= 1000) endfunction ## Vectorized binary search. ## Can handle vectors n and ps, and is faster than the scalar case when the ## answer is large. ## Could be optimized to call binocdf only for a subset of the p at each stage, ## but care must be taken to handle both scalar and vector n, ps. Bookkeeping ## may cost more than the extra computations. function m = bin_search_binoinv (p, n, ps) ## binocdf returns a column, so a row P would broadcast into a full matrix. p = p(:); k = 1:length (p); lower = zeros (size (p)); limit = 500; # lower bound on point at which prev phase finished while (any (k) && limit < 1e100) cdf = binocdf (limit, n, ps); k = (p > cdf); lower(k) = limit; limit += limit; endwhile upper = max (2*lower, 1); k = find (lower != limit/2); # elements for which above loop finished for i = 1:ceil (log2 (max (lower))) mid = (upper + lower)/2; cdf = binocdf (floor (mid(:)), n, ps); r = (p <= cdf); upper(r) = mid(r); lower(! r) = mid(! r); endfor m = ceil (lower); m(p > binocdf (m(:), n, ps)) += 1; # fix off-by-one errors from binary search endfunction ## Step the answer back onto M-1 wherever P lies within the error of the CDF ## there. The CDF carries a relative error that grows with N, so an exactly ## attained probability reads short and sends the search past its answer: ## binocdf (2, 5, 0.5) is 0.49999999999999989 where the exact value, 16/32, ## is representable, and the shortfall reaches 2355 eps by 2001 trials. The ## slack is capped at half the probability of M so that it can never cross a ## real step of the distribution and round P to the wrong side of one. function m = tie_correct (m, p, n, ps) j = find (m > 0); if (isempty (j)) return; endif mj = m(j); if (isscalar (n)) nj = n; else nj = n(j); endif if (isscalar (ps)) psj = ps; else psj = ps(j); endif lo = binocdf (mj - 1, nj, psj); tol = min (32 * nj .* eps (lo), 0.5 * binopdf (mj, nj, psj)); m(j(p(j) <= lo + tol)) -= 1; endfunction %!demo %! ## Plot various iCDFs from the binomial distribution %! p = 0.001:0.001:0.999; %! x1 = binoinv (p, 20, 0.5); %! x2 = binoinv (p, 20, 0.7); %! x3 = binoinv (p, 40, 0.5); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r') %! grid on %! legend ({'n = 20, ps = 0.5', 'n = 20, ps = 0.7', ... %! 'n = 40, ps = 0.5'}, 'location', 'southeast') %! title ('Binomial iCDF') %! xlabel ('probability') %! ylabel ('values in x (number of successes)') ## Test output %!shared p %! p = [-1 0 0.5 1 2]; %!assert_equal (binoinv (p, 2*ones (1,5), 0.5*ones (1,5)), [NaN 0 1 2 NaN]) %!assert_equal (binoinv (p, 2, 0.5*ones (1,5)), [NaN 0 1 2 NaN]) %!assert_equal (binoinv (p, 2*ones (1,5), 0.5), [NaN 0 1 2 NaN]) %!assert_equal (binoinv (p, 2*[0 -1 NaN 1.1 1], 0.5), [NaN NaN NaN NaN NaN]) %!assert_equal (binoinv (p, 2, 0.5*[0 -1 NaN 3 1]), [NaN NaN NaN NaN NaN]) %!assert_equal (binoinv ([p(1:2) NaN p(4:5)], 2, 0.5), [NaN 0 NaN 2 NaN]) ## Test class of input preserved %!assert_equal (binoinv ([p, NaN], 2, 0.5), [NaN 0 1 2 NaN NaN]) %!assert_equal (binoinv (single ([p, NaN]), 2, 0.5), single ([NaN 0 1 2 NaN NaN])) %!assert_equal (binoinv ([p, NaN], single (2), 0.5), single ([NaN 0 1 2 NaN NaN])) %!assert_equal (binoinv ([p, NaN], 2, single (0.5)), single ([NaN 0 1 2 NaN NaN])) ## Test accuracy against a round trip through the CDF %!shared x %! x = magic (3) + 1; %!assert_equal (binoinv (binocdf (1:10, 11, 0.1), 11, 0.1), 1:10) %!assert_equal (binoinv (binocdf (1:10, 2*(1:10), 0.1), 2*(1:10), 0.1), 1:10) %!assert_equal (binoinv (binocdf (x, 2*x, 1./x), 2*x, 1./x), x) ## A symmetric binomial with an odd number of trials attains 0.5 exactly at ## its median, which the CDF reads short of by an error growing with N. %!assert_equal (binoinv (0.5, 5, 0.5), 2) %!assert_equal (binoinv (0.5, 7, 0.5), 3) %!assert_equal (binoinv (0.5, 501, 0.5), 250) %!assert_equal (binoinv (0.5, 2001, 0.5), 1000) %!assert_equal (binoinv (0.5, 100001, 0.5), 50000) ## P of 1 attains N even where the CDF saturates to 1 far below it. %!assert_equal (binoinv (1, 500, 0.25), 500) %!assert_equal (binoinv (1, [50, 500], [0.5, 0.1]), [50, 500]) ## Answers above 500 take the binary search, where the CDF is a column and P ## keeps the orientation it was given. %!assert_equal (binoinv ([0.5, 0.9], 2001, 0.5), [1000, 1029]) %!assert_equal (binoinv ([0.5; 0.9], 2001, 0.5), [1000; 1029]) %!assert_equal (binoinv ([NaN, 0.5], 2001, 0.5), [NaN, 1000]) %!assert_equal (binoinv ([2, 0.5], [2001, 2001], [0.5, 0.5]), [NaN, 1000]) ## Test input validation %!error binoinv () %!error binoinv (1) %!error binoinv (1,2) %!error binoinv (1,2,3,4) %!error ... %! binoinv (ones (3), ones (2), ones (2)) %!error ... %! binoinv (ones (2), ones (3), ones (2)) %!error ... %! binoinv (ones (2), ones (2), ones (3)) %!error binoinv (int32 (2), 2, 2) %!error binoinv (true, 2, 2) %!error binoinv ('a', 2, 2) %!error binoinv (i, 2, 2) %!error binoinv (2, i, 2) %!error binoinv (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/binopdf.m000066400000000000000000000316521527311104300246130ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2021 Nicholas R. Jankowski ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} binopdf (@var{x}, @var{n}, @var{ps}) ## ## Binomial probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the binomial distribution with parameters @var{n} and @var{ps}, where ## @var{n} is the number of trials and @var{ps} is the probability of success. ## The size of @var{y} is the common size of @var{x}, @var{n}, and @var{ps}. A ## scalar input functions as a constant matrix of the same size as the other ## inputs. ## ## Matlab incompatibility: Octave's @code{binopdf} does not allow complex ## input values. Matlab 2021b returns values for complex inputs despite the ## documentation indicates integer and real value inputs are required. ## ## Further information about the binomial distribution can be found at ## @url{https://en.wikipedia.org/wiki/Binomial_distribution} ## ## Input arguments must be @qcode{double}, @qcode{single}, or an integer type; ## logical and character arrays are rejected. Integer input is promoted to ## @qcode{double}, so the result is always a probability. MATLAB is ## inconsistent here: for several of the discrete distributions it returns the ## result in the integer class of the input, truncating a probability to ## @math{0} or @math{1}. ## ## @seealso{binocdf, binoinv, binornd, binofit, binolike, binostat, binotest} ## @end deftypefn function y = binopdf (x, n, ps) ## Check for valid number of input arguments if (nargin < 3) error ("binopdf: function called with too few input arguments."); endif ## Check for common size of X, N, and PS if (! isscalar (x) || ! isscalar (n) || ! isscalar (ps)) [retval, x, n, ps] = common_size (x, n, ps); if (retval > 0) error ("binopdf: X, N, and PS must be of common size or scalars."); endif endif ## Check for X, N, and PS being double, single, or integer if (! (isnumeric (x) && isnumeric (n) && isnumeric (ps))) error ("binopdf: X, N, and PS must be double, single, or integer."); endif ## Integer input is promoted to double, so the result is a probability ## rather than a value truncated to the input's integer type. if (isinteger (x)) x = double (x); endif if (isinteger (n)) n = double (n); endif if (isinteger (ps)) ps = double (ps); endif ## Check for X, N, and PS being reals if (iscomplex (x) || iscomplex (n) || iscomplex (ps)) error ("binopdf: X, N, and PS must not be complex."); endif sz_x = size (x); # save original size for reshape later x = x(:); n = n(:); ps = ps(:); # columns for easier vectorization ## Initialize output, preserve class of output if any are singles if (isa (x, 'single') || isa (n, 'single') || isa (ps, 'single')); y = zeros (numel (x), 1, 'single'); else y = zeros (numel (x), 1); endif ## k - index of array locations needing calculation k = (x == fix (x)) & (n == fix (n)) & (n >= 0) & (ps >= 0) & (ps <= 1) ... & (x >= 0) & (x <= n); nx = n - x; q = 1 - ps; ## Catch special cases ahead of calculations: ## Matlab incompatibility: Matlab 2021b returns values for complex inputs ## despite documentation indicating integer and real value inputs required. ## Octave chooses to return an NaN instead. catch_special = (iscomplex (x) | iscomplex (n) | iscomplex (ps)); k(catch_special) = false; y(catch_special) = NaN; ## x = 0 and x = n cases where ps != 0 or 1, respectively ## remove them from k, use alternate calculation to avoid /0 catch_special = (x == 0)& (! catch_special); k(catch_special) = false; y(catch_special) = exp (n(catch_special) .* log (q(catch_special))); catch_special = (nx == 0) & (! catch_special); k(catch_special) = false; y(catch_special) = exp (n(catch_special) .* log (ps(catch_special))); ## Perform Loader pdf calculation on non-trivial elements if (any (k)) y(k) = loader_expansion (x(k), n(k), ps(k), nx(k), q(k)); endif ## Trivial case special outputs: ksp = ((ps == 0) & (x == 0)) | (ps == 1) & (x == n); y(ksp) = 1; ## Input NaN, n not pos int, or ps outside [0,1], ## set output to NaN (overrides 0 or 1) ksp = (n != fix (n)) | (n < 0) | (ps < 0) | (ps > 1) | isnan (x) ... | isnan (n) | isnan (ps); y(ksp) = NaN; y = reshape (y, sz_x); ## restore output to input shape endfunction function y = loader_expansion (x, n, ps, nx, q) ## Precalculated constants, d_n from n = 0 to 30 ## extended from Loader using octave symbolic vpa ## out to n = 30 d_n = [ 0.08106146679532725821967026359438236013860, 0.04134069595540929409382208140711750802535, 0.02767792568499833914878929274624466659538, 0.02079067210376509311152277176784865633309, 0.01664469118982119216319486537359339114739, 0.01387612882307074799874572702376290856175, 0.01189670994589177009505572411765943862013, 0.01041126526197209649747856713253462919952, 0.00925546218271273291772863663310013611743, 0.00833056343336287125646931865962855220929, 0.00757367548795184079497202421159508389293, 0.00694284010720952986566415266347536265992, 0.00640899418800420706843963108297831257520, 0.00595137011275884773562441604646945832642, 0.00555473355196280137103868995979228464907, 0.00520765591960964044071799685790189865099, 0.00490139594843473786071681819096755442865, 0.00462915374933402859242721316419232323878, 0.00438556024923232426828773634861946570116, 0.00416631969199692245746292338221831613633, 0.00396795421864085961728763680734281467287, 0.00378761806844443457786667706893349200129, 0.00362296022468309470738119836390285473489, 0.00347202138297876696294511542270952959204, 0.00333315563672809287580701911737271025035, 0.00320497022805503801118415655381541759643, 0.00308627868260877706325624133564397946129, 0.00297606398355040882602116255686080370692, 0.00287344936235246638755235148906672207372, 0.00277767492975269360359490376220667282839 ]; stored_dn = numel (d_n); ## Indices for precalculated vs to-be-calculated values n_precalc = (n > 0) & (n < stored_dn); x_precalc = (x > 0) & (x < stored_dn); nx_precalc = (nx > 0) & (nx < stored_dn); [delta_n, delta_x, delta_nx] = deal (zeros (size (x))); ## Fetch precalculated values delta_n(n_precalc) = d_n(n(n_precalc)); delta_x(x_precalc) = d_n(x(x_precalc)); delta_nx(nx_precalc) = d_n(nx(nx_precalc)); ## Calculate any other d(n) values delta_n(! n_precalc) = delta_fn (n(! n_precalc)); delta_x(! x_precalc) = delta_fn (x(! x_precalc)); delta_nx(! nx_precalc) = delta_fn (nx(! nx_precalc)); ## Calculate exp(log(pdf)); y = exp ((delta_n - delta_x - delta_nx - ... deviance (x, n .* ps) - ... deviance (nx, n .* q)) - ... 0.5 * (log (2*pi) + log (x) + log (1-x./n))); endfunction function y = delta_fn (n) ## Stirling formula error term approximations based on Loader paper. ## exact expression, n^n overflows to Inf for n > ~145: ## = log (n!*exp(n)/(sqrt(2pi*n)*n^n)); ## ## Rewritten to avoid overflow out to n> 1e305. accurate to ~10^-12 ## = n + gammaln (n+1) - (n+0.5) * log(n) - log(2*pi)/2; ## ## Approximated as: ## accurate to ~10^-16 for n=30. underflow to 0 at n~10^309 ## = 1/(12n)-1/(360n^3)+1/(1260n^5)- 1/(1680n.^7)+1/(1188n^9) + O(n^-11); ## ## Factored to reduced operation count. Used by Loader and in R: ## 25% faster than unfactored form. ## =(1/12-(1/360-(1/1260-(1/1680-(1/1188)/n^2)/n^2)/n^2)/n^2)/n; nn = n.^2; y = (0.08333333333333333333333333333333333333333 - ... (0.00277777777777777777777777777777777777778 - ... (0.00079365079365079365079365079365079365079 - ... (0.00059523809523809523809523809523809523810 - ... (0.00084175084175084175084175084175084175084)./nn)./nn)./nn)./nn)./n; endfunction function D = deviance (x, np) ## requires equal length column inputs epsilon = x ./ np; v = (epsilon - 1) ./ (epsilon + 1); vtest = abs (v) < 0.1; if (any (vtest)) ## For abs(v) < 0.1, do taylor expansion for higher precision. Expansion ## term: v^(2j+1)/(2j+1). For abs(v)< 0.1, term drops slowest for max ## abs(v) = 0.1. (n+1)th term is <= 10. jmax = 12; two_jpone = 2 * [1:jmax] + 1; # sum term 2*j+1 (row vector expansion) D = zeros (numel (epsilon), 1); ## D = (x-np)*v + 2*x*sum_over_j(v^2j+1 / 2j+1) D(vtest) = (x(vtest) - np(vtest)) .* v(vtest) + 2 .* x(vtest) .* ... sum (v(vtest).^(two_jpone) ./ two_jpone, 2); D(! vtest) = x(! vtest) .* (log (epsilon(! vtest)) - 1) + np(! vtest); else D = x.* (log (epsilon) - 1) + np; endif endfunction %!demo %! ## Plot various PDFs from the binomial distribution %! x = 0:40; %! y1 = binopdf (x, 20, 0.5); %! y2 = binopdf (x, 20, 0.7); %! y3 = binopdf (x, 40, 0.5); %! plot (x, y1, '*b', x, y2, '*g', x, y3, '*r') %! grid on %! ylim ([0, 0.25]) %! legend ({'n = 20, ps = 0.5', 'n = 20, ps = 0.7', ... %! 'n = 40, ps = 0.5'}, 'location', 'northeast') %! title ('Binomial PDF') %! xlabel ('values in x (number of successes)') %! ylabel ('density') ## Test output %!shared x, y %! x = [-1 0 1 2 3]; %! y = [0 1/4 1/2 1/4 0]; %!assert_equal (binopdf (x, 2 * ones (1, 5), 0.5 * ones (1, 5)), y, eps) %!assert_equal (binopdf (x, 2, 0.5 * ones (1, 5)), y, eps) %!assert_equal (binopdf (x, 2 * ones (1, 5), 0.5), y, eps) %!assert_equal (binopdf (x, 2 * [0 -1 NaN 1.1 1], 0.5), [0 NaN NaN NaN 0]) %!assert_equal (binopdf (x, 2, 0.5 * [0 -1 NaN 3 1]), [0 NaN NaN NaN 0]) %!assert_equal (binopdf ([x, NaN], 2, 0.5), [y, NaN], eps) %!assert_equal (binopdf (cat (3, x, x), 2, 0.5), cat (3, y, y), eps) ## Test Special input values %!assert_equal (binopdf (1, 1, 1), 1) %!assert_equal (binopdf (0, 3, 0), 1) %!assert_equal (binopdf (2, 2, 1), 1) %!assert_equal (binopdf (1, 2, 1), 0) %!assert_equal (binopdf (0, 1.1, 0), NaN) %!assert_equal (binopdf (1, 2, -1), NaN) %!assert_equal (binopdf (1, 2, 1.5), NaN) ## Test empty inputs %!assert_equal (binopdf ([], 1, 1), []) %!assert_equal (binopdf (1, [], 1), []) %!assert_equal (binopdf (1, 1, []), []) %!assert_equal (binopdf (ones (1, 0), 2, .5), ones (1, 0)) %!assert_equal (binopdf (ones (0, 1), 2, .5), ones (0, 1)) %!assert_equal (binopdf (ones (0, 1, 2), 2, .5), ones (0, 1, 2)) %!assert_equal (binopdf (1, ones (0, 1, 2), .5), ones (0, 1, 2)) %!assert_equal (binopdf (1, 2, ones (0, 1, 2)), ones (0, 1, 2)) %!assert_equal (binopdf (ones (1, 0, 2), 2, .5), ones (1, 0, 2)) %!assert_equal (binopdf (ones (1, 2, 0), 2, .5), ones (1, 2, 0)) %!assert_equal (binopdf (ones (0, 1, 2), NaN, .5), ones (0, 1, 2)) %!assert_equal (binopdf (ones (0, 1, 2), 2, NaN), ones (0, 1, 2)) ## Test class of input preserved %!assert_equal (binopdf (single ([x, NaN]), 2, 0.5), single ([y, NaN])) %!assert_equal (binopdf ([x, NaN], single (2), 0.5), single ([y, NaN])) %!assert_equal (binopdf ([x, NaN], 2, single (0.5)), single ([y, NaN])) ## Test input validation %!error binopdf () %!error binopdf (1) %!error binopdf (1, 2) %!error binopdf (1, 2, 3, 4) %!error ... %! binopdf (ones (3), ones (2), ones (2)) %!error ... %! binopdf (ones (2), ones (3), ones (2)) %!error ... %! binopdf (ones (2), ones (2), ones (3)) %!error binopdf (true, 2, 2) %!error binopdf ('a', 2, 2) %!assert_equal (class (binopdf (int32 (2), 2, 2)), 'double') %!error binopdf (i, 2, 2) %!error binopdf (2, i, 2) %!error binopdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/binornd.m000066400000000000000000000175611527311104300246300ustar00rootroot00000000000000## Copyright (C) 2015 Michael Leitner ## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} binornd (@var{n}, @var{ps}) ## @deftypefnx {statistics} {@var{r} =} binornd (@var{n}, @var{ps}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} binornd (@var{n}, @var{ps}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} binornd (@var{n}, @var{ps}, [@var{sz}]) ## ## Random arrays from the Binomial distribution. ## ## @code{@var{r} = binornd (@var{n}, @var{ps})} returns a matrix of random ## samples from the binomial distribution with parameters @var{n} and @var{ps}, ## where @var{n} is the number of trials and @var{ps} is the probability of ## success. The size of @var{r} is the common size of @var{n} and @var{ps}. ## A scalar input functions as a constant matrix of the same size as the other ## inputs. ## ## When called with a single size argument, @code{binornd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the binomial distribution can be found at ## @url{https://en.wikipedia.org/wiki/Binomial_distribution} ## ## @seealso{binocdf, binoinv, binopdf, binofit, binolike, binostat, binotest} ## @end deftypefn function r = binornd (n, ps, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("binornd: function called with too few input arguments."); endif ## Check for common size of N and PS if (! isscalar (n) || ! isscalar (ps)) [retval, n, ps] = common_size (n, ps); if (retval > 0) error ("binornd: N and PS must be of common size or scalars."); endif endif ## Check for N and PS being reals if (iscomplex (n) || iscomplex (ps)) error ("binornd: N and PS must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (n); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("binornd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("binornd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (n) && ! isequal (size (n), size (ones (sz)))) error ("binornd: N and PS must be scalars or of size SZ."); endif ## Check for class type if (isa (n, 'single') || isa (ps, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from binomial distribution if (isscalar (n) && isscalar (ps)) if ((n > 0) && (n < Inf) && (n == fix (n)) && (ps >= 0) && (ps <= 1)) nel = prod (sz); tmp = rand (n, nel); r = sum (tmp < ps, 1); r = reshape (r, sz); if (strcmp (cls, 'single')) r = single (r); endif elseif ((n == 0) && (ps >= 0) && (ps <= 1)) r = zeros (sz, cls); else r = NaN (sz, cls); endif else r = zeros (sz, cls); k = ! (n >= 0) | ! (n < Inf) | ! (n == fix (n)) | ! (ps >= 0) | ! (ps <= 1); r(k) = NaN; k = (n > 0) & (n < Inf) & (n == fix (n)) & (ps >= 0) & (ps <= 1); if (any (k(:))) L = sum (k(:)); ind = repelems ((1 : L), [(1 : L); n(k)(:)'])'; p_ext = ps(k)(ind)(:); r(k) = accumarray (ind, rand (sum (n(k)(:)), 1) < p_ext); endif endif endfunction ## Test output %!assert_equal (size (binornd (2, 1/2)), [1 1]) %!assert_equal (size (binornd (2 * ones (2, 1), 1/2)), [2, 1]) %!assert_equal (size (binornd (2 * ones (2, 2), 1/2)), [2, 2]) %!assert_equal (size (binornd (2, 1/2 * ones (2, 1))), [2, 1]) %!assert_equal (size (binornd (1, 1/2 * ones (2, 2))), [2, 2]) %!assert_equal (size (binornd (ones (2, 1), 1)), [2, 1]) %!assert_equal (size (binornd (ones (2, 2), 1)), [2, 2]) %!assert_equal (size (binornd (2, 1/2, 3)), [3, 3]) %!assert_equal (size (binornd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (binornd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (binornd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (binornd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (binornd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (binornd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (binornd (1, 1, [])), [0, 0]) %!assert_equal (size (binornd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (binornd (1, 1/2, -1)), [0, 0]) %!assert_equal (size (binornd (1, 1/2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (binornd (1, 1/2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (binornd (1, 1)), "double") %!assert_equal (class (binornd (1, single (0))), "single") %!assert_equal (class (binornd (1, single ([0, 0]))), "single") %!assert_equal (class (binornd (1, single (1), 2)), "single") %!assert_equal (class (binornd (1, single ([1, 1]), 1, 2)), "single") %!assert_equal (class (binornd (single (1), 1, 2)), "single") %!assert_equal (class (binornd (single ([1, 1]), 1, 1, 2)), "single") ## Test input validation %!error binornd () %!error binornd (1) %!error ... %! binornd (ones (3), ones (2)) %!error ... %! binornd (ones (2), ones (3)) %!error binornd (i, 2) %!error binornd (1, i) %!error ... %! binornd (1, 1/2, 1.2) %!error ... %! binornd (1, 1/2, ones (2)) %!error ... %! binornd (1, 1/2, [2 0 2.5]) %!error ... %! binornd (1, 1/2, 2, 1.5, 5) %!error ... %! binornd (2, 1/2 * ones (2), 3) %!error ... %! binornd (2, 1/2 * ones (2), [3, 2]) %!error ... %! binornd (2, 1/2 * ones (2), 3, 2) %!error ... %! binornd (2 * ones (2), 1/2, 3) %!error ... %! binornd (2 * ones (2), 1/2, [3, 2]) %!error ... %! binornd (2 * ones (2), 1/2, 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/bisacdf.m000066400000000000000000000171151527311104300245630ustar00rootroot00000000000000## Copyright (C) 1995-2015 Kurt Hornik ## Copyright (C) 2016 Dag Lyberg ## Copyright (C) 2018 John Donoghue ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} bisacdf (@var{x}, @var{beta}, @var{gamma}) ## @deftypefnx {statistics} {@var{p} =} bisacdf (@var{x}, @var{beta}, @var{gamma}, @qcode{'upper'}) ## ## Birnbaum-Saunders cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the Birnbaum-Saunders distribution with scale parameter @var{beta} ## and shape parameter @var{gamma}. The size of @var{p} is the common size of ## @var{x}, @var{beta} and @var{gamma}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## @code{@var{p} = bisacdf (@var{x}, @var{beta}, @var{gamma}, "upper")} ## computes the upper tail probability of the Birnbaum-Saunders distribution ## with parameters @var{beta} and @var{gamma}, at the values in @var{x}. ## ## Further information about the Birnbaum-Saunders distribution can be found at ## @url{https://en.wikipedia.org/wiki/Birnbaum%E2%80%93Saunders_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{bisainv, bisapdf, bisarnd, bisafit, bisalike, bisastat} ## @end deftypefn function p = bisacdf (x, beta, gamma, uflag) ## Check for valid number of input arguments if (nargin < 3) error ("bisacdf: function called with too few input arguments."); endif ## Check for valid "upper" flag if (nargin > 3) if (! strcmpi (uflag, 'upper')) error ("bisacdf: invalid argument for upper tail."); else uflag = true; endif else uflag = false; endif ## Check for common size of X, BETA, and GAMMA if (! isscalar (x) || ! isscalar (beta) || ! isscalar (gamma)) [retval, x, beta, gamma] = common_size (x, beta, gamma); if (retval > 0) error (strcat ("bisacdf: X, BETA, and GAMMA must be of", ... " common size or scalars.")); endif endif ## Check for X, BETA, and GAMMA being double or single if (! (isfloat (x) && isfloat (beta) && isfloat (gamma))) error ("bisacdf: X, BETA, and GAMMA must be double or single."); endif ## Check for X, BETA, and GAMMA being reals if (iscomplex (x) || iscomplex (beta) || iscomplex (gamma)) error ("bisacdf: X, BETA, and GAMMA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (beta, 'single') || isa (gamma, 'single')) p = zeros (size (x), 'single'); else p = zeros (size (x)); endif ## Force NaNs for out of range parameters. k = isnan (x) | ! (beta > 0) | ! (beta < Inf) ... | ! (gamma > 0) | ! (gamma < Inf); p(k) = NaN; ## Find valid values in parameters and data k = (x > 0) & (x <= Inf) & (beta > 0) & (beta < Inf) ... & (gamma > 0) & (gamma < Inf); xk = x(k); ## Compute Birnbaum-Saunders CDF if (isscalar (beta) && isscalar (gamma)) if (uflag) z = (-sqrt (xk ./ beta) + sqrt (beta ./ xk)) ./ gamma; else z = (sqrt (xk ./ beta) - sqrt (beta ./ xk)) ./ gamma; endif p(k) = 0.5 * erfc (-z ./ sqrt (2)); else if (uflag) z = (-sqrt (xk ./ beta(k)) + sqrt (beta(k) ./ xk)) ./ gamma(k); else z = (sqrt (xk ./ beta(k)) - sqrt (beta(k) ./ xk)) ./ gamma(k); endif p(k) = 0.5 * erfc (-z ./ sqrt (2)); endif endfunction %!demo %! ## Plot various CDFs from the Birnbaum-Saunders distribution %! x = 0.01:0.01:4; %! p1 = bisacdf (x, 1, 0.5); %! p2 = bisacdf (x, 1, 1); %! p3 = bisacdf (x, 1, 2); %! p4 = bisacdf (x, 1, 5); %! p5 = bisacdf (x, 1, 10); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', x, p4, '-c', x, p5, '-m') %! grid on %! legend ({'β = 1, γ = 0.5', 'β = 1, γ = 1', 'β = 1, γ = 2', ... %! 'β = 1, γ = 5', 'β = 1, γ = 10'}, 'location', 'southeast') %! title ('Birnbaum-Saunders CDF') %! xlabel ('values in x') %! ylabel ('probability') %!demo %! ## Plot various CDFs from the Birnbaum-Saunders distribution %! x = 0.01:0.01:6; %! p1 = bisacdf (x, 1, 0.3); %! p2 = bisacdf (x, 2, 0.3); %! p3 = bisacdf (x, 1, 0.5); %! p4 = bisacdf (x, 3, 0.5); %! p5 = bisacdf (x, 5, 0.5); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', x, p4, '-c', x, p5, '-m') %! grid on %! legend ({'β = 1, γ = 0.3', 'β = 2, γ = 0.3', 'β = 1, γ = 0.5', ... %! 'β = 3, γ = 0.5', 'β = 5, γ = 0.5'}, 'location', 'southeast') %! title ('Birnbaum-Saunders CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, y %! x = [-1, 0, 1, 2, Inf]; %! y = [0, 0, 1/2, 0.76024993890652337, 1]; %!assert_equal (bisacdf (x, ones (1,5), ones (1,5)), y, eps) %!assert_equal (bisacdf (x, 1, 1), y, eps) %!assert_equal (bisacdf (x, 1, ones (1,5)), y, eps) %!assert_equal (bisacdf (x, ones (1,5), 1), y, eps) %!assert_equal (bisacdf (x, 1, 1), y, eps) %!assert_equal (bisacdf (x, 1, [1, 1, NaN, 1, 1]), [y(1:2), NaN, y(4:5)], eps) %!assert_equal (bisacdf (x, [1, 1, NaN, 1, 1], 1), [y(1:2), NaN, y(4:5)], eps) %!assert_equal (bisacdf ([x, NaN], 1, 1), [y, NaN], eps) ## Test class of input preserved %!assert_equal (bisacdf (single ([x, NaN]), 1, 1), single ([y, NaN]), eps ('single')) %!assert_equal (bisacdf ([x, NaN], 1, single (1)), single ([y, NaN]), eps ('single')) %!assert_equal (bisacdf ([x, NaN], single (1), 1), single ([y, NaN]), eps ('single')) ## Test input validation %!error bisacdf () %!error bisacdf (1) %!error bisacdf (1, 2) %!error ... %! bisacdf (1, 2, 3, 4, 5) %!error bisacdf (1, 2, 3, 'tail') %!error bisacdf (1, 2, 3, 4) %!error ... %! bisacdf (ones (3), ones (2), ones (2)) %!error ... %! bisacdf (ones (2), ones (3), ones (2)) %!error ... %! bisacdf (ones (2), ones (2), ones (3)) %!error bisacdf (int32 (2), 4, 3) %!error bisacdf (true, 4, 3) %!error bisacdf ('a', 4, 3) %!error bisacdf (i, 4, 3) %!error bisacdf (1, i, 3) %!error bisacdf (1, 4, i) statistics-release-2.1.2/inst/Distribution_Functions/bisainv.m000066400000000000000000000155701527311104300246260ustar00rootroot00000000000000## Copyright (C) 1995-2015 Kurt Hornik ## Copyright (C) 2016 Dag Lyberg ## Copyright (C) 2018 John Donoghue ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} bisainv (@var{p}, @var{beta}, @var{gamma}) ## ## Inverse of the Birnbaum-Saunders cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the Birnbaum-Saunders distribution with scale parameter @var{beta} and shape ## parameter @var{gamma}. The size of @var{x} is the common size of @var{p}, ## @var{beta}, and @var{gamma}. A scalar input functions as a constant matrix ## of the same size as the other inputs. ## ## Further information about the Birnbaum-Saunders distribution can be found at ## @url{https://en.wikipedia.org/wiki/Birnbaum%E2%80%93Saunders_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{bisacdf, bisapdf, bisarnd, bisafit, bisalike, bisastat} ## @end deftypefn function x = bisainv (p, beta, gamma) ## Check for valid number of input arguments if (nargin < 3) error ("bisainv: function called with too few input arguments."); endif ## Check for common size of X, BETA, and GAMMA if (! isscalar (p) || ! isscalar (beta) || ! isscalar (gamma)) [retval, p, beta, gamma] = common_size (p, beta, gamma); if (retval > 0) error (strcat ("bisainv: P, BETA, and GAMMA must be of", ... " common size or scalars.")); endif endif ## Check for P, BETA, and GAMMA being double or single if (! (isfloat (p) && isfloat (beta) && isfloat (gamma))) error ("bisainv: P, BETA, and GAMMA must be double or single."); endif ## Check for X, BETA, and GAMMA being reals if (iscomplex (p) || iscomplex (beta) || iscomplex (gamma)) error ("bisainv: P, BETA, and GAMMA must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (beta, 'single') || isa (gamma, 'single')) x = zeros (size (p), 'single'); else x = zeros (size (p)); endif ## Force NaNs for out of range parameters kn = isnan (p) | (p < 0) | (p > 1) | ! (beta > 0) | ! (beta < Inf) ... | ! (gamma > 0) | ! (gamma < Inf); x(kn) = NaN; ## Find valid values in parameters kv = (beta > 0) & (beta < Inf) & (gamma > 0) & (gamma < Inf); ## Handle edge cases k0 = (p == 0) & kv; x(k0) = 0; k1 = (p == 1) & kv; x(k1) = Inf; ## Handle all other valid cases k = (p > 0) & (p < 1) & kv; if (isscalar (beta) && isscalar (gamma)) z = -sqrt (2) .* erfcinv (2 .* p(k)) .* gamma; x(k) = 0.25 .* beta .* (z + sqrt (4 + z .^ 2)) .^ 2; else z = -sqrt (2) .* erfcinv (2 .* p(k)) .* gamma(k); x(k) = 0.25 .* beta(k) .* (z + sqrt (4 + z .^ 2)) .^ 2; endif endfunction %!demo %! ## Plot various iCDFs from the Birnbaum-Saunders distribution %! p = 0.001:0.001:0.999; %! x1 = bisainv (p, 1, 0.5); %! x2 = bisainv (p, 1, 1); %! x3 = bisainv (p, 1, 2); %! x4 = bisainv (p, 1, 5); %! x5 = bisainv (p, 1, 10); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', p, x4, '-c', p, x5, '-m') %! grid on %! ylim ([0, 10]) %! legend ({'β = 1, γ = 0.5', 'β = 1, γ = 1', 'β = 1, γ = 2', ... %! 'β = 1, γ = 5', 'β = 1, γ = 10'}, 'location', 'northwest') %! title ('Birnbaum-Saunders iCDF') %! xlabel ('probability') %! ylabel ('values in x') %!demo %! ## Plot various iCDFs from the Birnbaum-Saunders distribution %! p = 0.001:0.001:0.999; %! x1 = bisainv (p, 1, 0.3); %! x2 = bisainv (p, 2, 0.3); %! x3 = bisainv (p, 1, 0.5); %! x4 = bisainv (p, 3, 0.5); %! x5 = bisainv (p, 5, 0.5); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', p, x4, '-c', p, x5, '-m') %! grid on %! ylim ([0, 10]) %! legend ({'β = 1, γ = 0.3', 'β = 2, γ = 0.3', 'β = 1, γ = 0.5', ... %! 'β = 3, γ = 0.5', 'β = 5, γ = 0.5'}, 'location', 'northwest') %! title ('Birnbaum-Saunders iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p, y, f %! f = @(p,b,c) (b * (c * norminv (p) + sqrt (4 + (c * norminv (p))^2))^2) / 4; %! p = [-1, 0, 1/4, 1/2, 1, 2]; %! y = [NaN, 0, f(1/4, 1, 1), 1, Inf, NaN]; %!assert_equal (bisainv (p, ones (1,6), ones (1,6)), y) %!assert_equal (bisainv (p, 1, ones (1,6)), y) %!assert_equal (bisainv (p, ones (1,6), 1), y) %!assert_equal (bisainv (p, 1, 1), y) %!assert_equal (bisainv (p, 1, [1, 1, 1, NaN, 1, 1]), [y(1:3), NaN, y(5:6)]) %!assert_equal (bisainv (p, [1, 1, 1, NaN, 1, 1], 1), [y(1:3), NaN, y(5:6)]) %!assert_equal (bisainv ([p, NaN], 1, 1), [y, NaN]) ## Test class of input preserved %!assert_equal (bisainv (single ([p, NaN]), 1, 1), single ([y, NaN]), eps ('single')) %!assert_equal (bisainv ([p, NaN], 1, single (1)), single ([y, NaN]), eps ('single')) %!assert_equal (bisainv ([p, NaN], single (1), 1), single ([y, NaN]), eps ('single')) ## Test input validation %!error bisainv () %!error bisainv (1) %!error bisainv (1, 2) %!error bisainv (1, 2, 3, 4) %!error ... %! bisainv (ones (3), ones (2), ones (2)) %!error ... %! bisainv (ones (2), ones (3), ones (2)) %!error ... %! bisainv (ones (2), ones (2), ones (3)) %!error bisainv (int32 (2), 4, 3) %!error bisainv (true, 4, 3) %!error bisainv ('a', 4, 3) %!error bisainv (i, 4, 3) %!error bisainv (1, i, 3) %!error bisainv (1, 4, i) statistics-release-2.1.2/inst/Distribution_Functions/bisapdf.m000066400000000000000000000155061527311104300246020ustar00rootroot00000000000000## Copyright (C) 2018 John Donoghue ## Copyright (C) 2016 Dag Lyberg ## Copyright (C) 1995-2015 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} bisapdf (@var{x}, @var{beta}, @var{gamma}) ## ## Birnbaum-Saunders probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the Birnbaum-Saunders distribution with scale parameter @var{beta} and ## shape parameter @var{gamma}. The size of @var{y} is the common size of ## @var{x}, @var{beta}, and @var{gamma}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## Further information about the Birnbaum-Saunders distribution can be found at ## @url{https://en.wikipedia.org/wiki/Birnbaum%E2%80%93Saunders_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{bisacdf, bisainv, bisarnd, bisafit, bisalike, bisastat} ## @end deftypefn function y = bisapdf (x, beta, gamma) ## Check for valid number of input arguments if (nargin < 3) error ("bisapdf: function called with too few input arguments."); endif ## Check for common size of X, BETA and GAMMA if (! isscalar (x) || ! isscalar (beta) || ! isscalar (gamma)) [retval, x, beta, gamma] = common_size (x, beta, gamma); if (retval > 0) error (strcat ("bisapdf: X, BETA, and GAMMA must be of", ... " common size or scalars.")); endif endif ## Check for X, BETA, and GAMMA being double or single if (! (isfloat (x) && isfloat (beta) && isfloat (gamma))) error ("bisapdf: X, BETA, and GAMMA must be double or single."); endif ## Check for X, BETA and GAMMA being reals if (iscomplex (x) || iscomplex (beta) || iscomplex (gamma)) error ("bisapdf: X, BETA, and GAMMA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (beta, 'single') || isa (gamma, 'single')) y = zeros (size (x), 'single'); else y = zeros (size (x)); endif ## Force NaNs for out of range parameters. k = isnan (x) | ! (beta > 0) | ! (beta < Inf) ... | ! (gamma > 0) | ! (gamma < Inf); y(k) = NaN; ## Find valid values in parameters and data k = (x > 0) & (x < Inf) & (beta > 0) & (beta < Inf) ... & (gamma > 0) & (gamma < Inf); xk = x(k); if (isscalar (beta) && isscalar (gamma)) z = (sqrt (xk ./ beta) - sqrt (beta ./ xk)) ./ gamma; w = (sqrt (xk ./ beta) + sqrt (beta ./ xk)) ./ gamma; y(k) = (exp (-0.5 .* z .^ 2) ./ sqrt (2 .* pi)) .* w ./ (2.*xk); else z = (sqrt (xk ./ beta(k)) - sqrt (beta(k) ./ xk)) ./ gamma(k); w = (sqrt (xk ./ beta(k)) + sqrt (beta(k) ./ xk)) ./ gamma(k); y(k) = (exp (-0.5 .* z .^ 2) ./ sqrt (2 .* pi)) .* w ./ (2 .* xk); endif endfunction %!demo %! ## Plot various PDFs from the Birnbaum-Saunders distribution %! x = 0.01:0.01:4; %! y1 = bisapdf (x, 1, 0.5); %! y2 = bisapdf (x, 1, 1); %! y3 = bisapdf (x, 1, 2); %! y4 = bisapdf (x, 1, 5); %! y5 = bisapdf (x, 1, 10); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', x, y4, '-c', x, y5, '-m') %! grid on %! ylim ([0, 1.5]) %! legend ({'β = 1 ,γ = 0.5', 'β = 1, γ = 1', 'β = 1, γ = 2', ... %! 'β = 1, γ = 5', 'β = 1, γ = 10'}, 'location', 'northeast') %! title ('Birnbaum-Saunders PDF') %! xlabel ('values in x') %! ylabel ('density') %!demo %! ## Plot various PDFs from the Birnbaum-Saunders distribution %! x = 0.01:0.01:6; %! y1 = bisapdf (x, 1, 0.3); %! y2 = bisapdf (x, 2, 0.3); %! y3 = bisapdf (x, 1, 0.5); %! y4 = bisapdf (x, 3, 0.5); %! y5 = bisapdf (x, 5, 0.5); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', x, y4, '-c', x, y5, '-m') %! grid on %! ylim ([0, 1.5]) %! legend ({'β = 1, γ = 0.3', 'β = 2, γ = 0.3', 'β = 1, γ = 0.5', ... %! 'β = 3, γ = 0.5', 'β = 5, γ = 0.5'}, 'location', 'northeast') %! title ('Birnbaum-Saunders CDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x, y %! x = [-1, 0, 1, 2, Inf]; %! y = [0, 0, 0.3989422804014327, 0.1647717335503959, 0]; %!assert_equal (bisapdf (x, ones (1,5), ones (1,5)), y, eps) %!assert_equal (bisapdf (x, 1, 1), y, eps) %!assert_equal (bisapdf (x, 1, ones (1,5)), y, eps) %!assert_equal (bisapdf (x, ones (1,5), 1), y, eps) %!assert_equal (bisapdf (x, 1, [1, 1, NaN, 1, 1]), [y(1:2), NaN, y(4:5)], eps) %!assert_equal (bisapdf (x, [1, 1, NaN, 1, 1], 1), [y(1:2), NaN, y(4:5)], eps) %!assert_equal (bisapdf ([x, NaN], 1, 1), [y, NaN], eps) ## Test class of input preserved %!assert_equal (bisapdf (single ([x, NaN]), 1, 1), single ([y, NaN]), eps ('single')) %!assert_equal (bisapdf ([x, NaN], 1, single (1)), single ([y, NaN]), eps ('single')) %!assert_equal (bisapdf ([x, NaN], single (1), 1), single ([y, NaN]), eps ('single')) ## Test input validation %!error bisapdf () %!error bisapdf (1) %!error bisapdf (1, 2) %!error bisapdf (1, 2, 3, 4) %!error ... %! bisapdf (ones (3), ones (2), ones (2)) %!error ... %! bisapdf (ones (2), ones (3), ones (2)) %!error ... %! bisapdf (ones (2), ones (2), ones (3)) %!error bisapdf (int32 (2), 4, 3) %!error bisapdf (true, 4, 3) %!error bisapdf ('a', 4, 3) %!error bisapdf (i, 4, 3) %!error bisapdf (1, i, 3) %!error bisapdf (1, 4, i) statistics-release-2.1.2/inst/Distribution_Functions/bisarnd.m000066400000000000000000000160701527311104300246110ustar00rootroot00000000000000## Copyright (C) 2018 John Donoghue ## Copyright (C) 2016 Dag Lyberg ## Copyright (C) 1995-2015 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} bisarnd (@var{beta}, @var{gamma}) ## @deftypefnx {statistics} {@var{r} =} bisarnd (@var{beta}, @var{gamma}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} bisarnd (@var{beta}, @var{gamma}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} bisarnd (@var{beta}, @var{gamma}, [@var{sz}]) ## ## Random arrays from the Birnbaum-Saunders distribution. ## ## @code{@var{r} = bisarnd (@var{beta}, @var{gamma})} returns an array of ## random numbers chosen from the Birnbaum-Saunders distribution with scale ## parameter @var{beta} and shape parameter @var{gamma}. The size of @var{r} is ## the common size of @var{beta} and @var{gamma}. A scalar input functions as a ## constant matrix of the same size as the other inputs. ## ## When called with a single size argument, @code{bisarnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the Birnbaum-Saunders distribution can be found at ## @url{https://en.wikipedia.org/wiki/Birnbaum%E2%80%93Saunders_distribution} ## ## @seealso{bisacdf, bisainv, bisapdf, bisafit, bisalike, bisastat} ## @end deftypefn function r = bisarnd (beta, gamma, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("bisarnd: function called with too few input arguments."); endif ## Check for common size of BETA and GAMMA if (! isscalar (beta) || ! isscalar (gamma)) [retval, beta, gamma] = common_size (beta, gamma); if (retval > 0) error ("bisarnd: BETA and GAMMA must be of common size or scalars."); endif endif ## Check for BETA and GAMMA being reals if (iscomplex (beta) || iscomplex (gamma)) error ("bisarnd: BETA and GAMMA must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (beta); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("bisarnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("bisarnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (beta) && ! isequal (size (beta), size (ones (sz)))) error ("bisarnd: BETA and GAMMA must be scalars or of size SZ."); endif ## Check for class type if (isa (beta, 'single') || isa (gamma, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from Birnbaum-Saunders distribution if (isscalar (beta) && isscalar (gamma)) if ((beta > 0) && (beta < Inf) && (gamma > 0) && (gamma < Inf)) r = rand (sz, cls); y = gamma * norminv (r); r = beta * (y + sqrt (4 + y .^ 2)) .^ 2 / 4; else r = NaN (sz, cls); endif else r = NaN (sz, cls); k = (beta > 0) & (beta < Inf) & (gamma > 0) & (gamma < Inf); r(k) = rand (sum (k(:)),1); y = gamma(k) .* norminv (r(k)); r(k) = beta(k) .* (y + sqrt (4 + y.^2)).^2 / 4; endif endfunction ## Test output %!assert_equal (size (bisarnd (1, 1)), [1 1]) %!assert_equal (size (bisarnd (1, ones (2,1))), [2, 1]) %!assert_equal (size (bisarnd (1, ones (2,2))), [2, 2]) %!assert_equal (size (bisarnd (ones (2,1), 1)), [2, 1]) %!assert_equal (size (bisarnd (ones (2,2), 1)), [2, 2]) %!assert_equal (size (bisarnd (1, 1, 3)), [3, 3]) %!assert_equal (size (bisarnd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (bisarnd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (bisarnd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (bisarnd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (bisarnd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (bisarnd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (bisarnd (1, 1, [])), [0, 0]) %!assert_equal (size (bisarnd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (bisarnd (1, 2, -1)), [0, 0]) %!assert_equal (size (bisarnd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (bisarnd (1, 2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (bisarnd (1, 1)), "double") %!assert_equal (class (bisarnd (1, single (1))), "single") %!assert_equal (class (bisarnd (1, single ([1, 1]))), "single") %!assert_equal (class (bisarnd (single (1), 1)), "single") %!assert_equal (class (bisarnd (single ([1, 1]), 1)), "single") ## Test input validation %!error bisarnd () %!error bisarnd (1) %!error ... %! bisarnd (ones (3), ones (2)) %!error ... %! bisarnd (ones (2), ones (3)) %!error bisarnd (i, 2, 3) %!error bisarnd (1, i, 3) %!error ... %! bisarnd (1, 2, 1.2) %!error ... %! bisarnd (1, 2, ones (2)) %!error ... %! bisarnd (1, 2, [2 0 2.5]) %!error ... %! bisarnd (1, 2, 2, 1.5, 5) %!error ... %! bisarnd (2, ones (2), 3) %!error ... %! bisarnd (2, ones (2), [3, 2]) %!error ... %! bisarnd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/burrcdf.m000066400000000000000000000166731527311104300246270ustar00rootroot00000000000000## Copyright (C) 1995-2015 Kurt Hornik ## Copyright (C) 2016 Dag Lyberg ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} burrcdf (@var{x}, @var{lambda}, @var{c}, @var{k}) ## @deftypefnx {statistics} {@var{p} =} burrcdf (@var{x}, @var{lambda}, @var{c}, @var{k}, @qcode{'upper'}) ## ## Burr type XII cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the Burr type XII distribution with scale parameter @var{lambda}, ## first shape parameter @var{c}, and second shape parameter @var{k}. The size ## of @var{p} is the common size of @var{x}, @var{lambda}, @var{c}, and @var{k}. ## A scalar input functions as a constant matrix of the same size as the other ## inputs. ## ## @code{@var{p} = burrcdf (@var{x}, @var{lambda}, @var{c}, @var{k}, "upper")} ## computes the upper tail probability of the Burr type XII distribution with ## parameters @var{lambda}, @var{c} and @var{k}, at the values in @var{x}. ## ## Further information about the Burr distribution can be found at ## @url{https://en.wikipedia.org/wiki/Burr_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{burrinv, burrpdf, burrrnd, burrfit, burrlike, burrstat} ## @end deftypefn function p = burrcdf (x, lambda, c, k, uflag) ## Check for valid number of input arguments if (nargin < 4) error ("burrcdf: function called with too few input arguments."); endif ## Check for valid "upper" flag if (nargin > 4) if (! strcmpi (uflag, 'upper')) error ("burrcdf: invalid argument for upper tail."); else uflag = true; endif else uflag = false; endif ## Check for common size of X, LANBDA, C and K if (! isscalar (x) || ! isscalar (lambda) || ! isscalar (c) || ! isscalar (k)) [retval, x, lambda, c, k] = common_size (x, lambda, c, k); if (retval > 0) error ("burrcdf: X, LAMBDA, C, and K must be of common size or scalars."); endif endif ## Check for X, LAMBDA, C, and K being double or single if (! (isfloat (x) && isfloat (lambda) && isfloat (c) && isfloat (k))) error ("burrcdf: X, LAMBDA, C, and K must be double or single."); endif ## Check for X, LANBDA, C and K being reals if (iscomplex (x) || iscomplex (lambda) || iscomplex (c) || iscomplex (k)) error ("burrcdf: X, LAMBDA, C, and K must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (lambda, 'single') || isa (c, 'single') ... || isa (k, 'single')) p = zeros (size (x), 'single'); else p = zeros (size (x)); endif ## Force NaNs for out of range parameters j = isnan (x) | ! (lambda > 0) | ! (c > 0) | ! (k > 0); p(j) = NaN; ## Find valid values in parameters and data j = (x > 0) & (lambda > 0) & (lambda < Inf) & (c > 0) & (c < Inf) ... & (k > 0) & (k < Inf); ## Compute Burr CDF if (isscalar (lambda) && isscalar (c) && isscalar (k)) if (uflag) p(j) = (1 + (x(j) / lambda) .^ c) .^ (-k); else p(j) = 1 - (1 + (x(j) / lambda) .^ c) .^ (-k); endif else if (uflag) p(j) = (1 + (x(j) ./ lambda(j)) .^ c(j)) .^ (-k(j)); else p(j) = 1 - (1 + (x(j) ./ lambda(j)) .^ c(j)) .^ (-k(j)); endif endif endfunction %!demo %! ## Plot various CDFs from the Burr type XII distribution %! x = 0.001:0.001:5; %! p1 = burrcdf (x, 1, 1, 1); %! p2 = burrcdf (x, 1, 1, 2); %! p3 = burrcdf (x, 1, 1, 3); %! p4 = burrcdf (x, 1, 2, 1); %! p5 = burrcdf (x, 1, 3, 1); %! p6 = burrcdf (x, 1, 0.5, 2); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', ... %! x, p4, '-c', x, p5, '-m', x, p6, '-k') %! grid on %! legend ({'λ = 1, c = 1, k = 1', 'λ = 1, c = 1, k = 2', ... %! 'λ = 1, c = 1, k = 3', 'λ = 1, c = 2, k = 1', ... %! 'λ = 1, c = 3, k = 1', 'λ = 1, c = 0.5, k = 2'}, ... %! 'location', 'southeast') %! title ('Burr type XII CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, y %! x = [-1, 0, 1, 2, Inf]; %! y = [0, 0, 1/2, 2/3, 1]; %!assert_equal (burrcdf (x, ones (1,5), ones (1,5), ones (1,5)), y, eps) %!assert_equal (burrcdf (x, 1, 1, 1), y, eps) %!assert_equal (burrcdf (x, [1, 1, NaN, 1, 1], 1, 1), [y(1:2), NaN, y(4:5)], eps) %!assert_equal (burrcdf (x, 1, [1, 1, NaN, 1, 1], 1), [y(1:2), NaN, y(4:5)], eps) %!assert_equal (burrcdf (x, 1, 1, [1, 1, NaN, 1, 1]), [y(1:2), NaN, y(4:5)], eps) %!assert_equal (burrcdf ([x, NaN], 1, 1, 1), [y, NaN], eps) ## Test class of input preserved %!assert_equal (burrcdf (single ([x, NaN]), 1, 1, 1), single ([y, NaN]), eps ('single')) %!assert_equal (burrcdf ([x, NaN], single (1), 1, 1), single ([y, NaN]), eps ('single')) %!assert_equal (burrcdf ([x, NaN], 1, single (1), 1), single ([y, NaN]), eps ('single')) %!assert_equal (burrcdf ([x, NaN], 1, 1, single (1)), single ([y, NaN]), eps ('single')) ## Test input validation %!error burrcdf () %!error burrcdf (1) %!error burrcdf (1, 2) %!error burrcdf (1, 2, 3) %!error ... %! burrcdf (1, 2, 3, 4, 5, 6) %!error burrcdf (1, 2, 3, 4, 'tail') %!error burrcdf (1, 2, 3, 4, 5) %!error ... %! burrcdf (ones (3), ones (2), ones (2), ones (2)) %!error ... %! burrcdf (ones (2), ones (3), ones (2), ones (2)) %!error ... %! burrcdf (ones (2), ones (2), ones (3), ones (2)) %!error ... %! burrcdf (ones (2), ones (2), ones (2), ones (3)) %!error burrcdf (int32 (2), 2, 3, 4) %!error burrcdf (true, 2, 3, 4) %!error burrcdf ('a', 2, 3, 4) %!error burrcdf (i, 2, 3, 4) %!error burrcdf (1, i, 3, 4) %!error burrcdf (1, 2, i, 4) %!error burrcdf (1, 2, 3, i) statistics-release-2.1.2/inst/Distribution_Functions/burrinv.m000066400000000000000000000173271527311104300246640ustar00rootroot00000000000000## Copyright (C) 1995-2015 Kurt Hornik ## Copyright (C) 2016 Dag Lyberg ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} burrinv (@var{p}, @var{lambda}, @var{c}, @var{k}) ## ## Inverse of the Burr type XII cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the Burr type XII distribution with scale parameter @var{lambda}, first shape ## parameter @var{c}, and second shape parameter @var{k}. The size of @var{x} ## is the common size of @var{p}, @var{lambda}, @var{c}, and @var{k}. A scalar ## input functions as a constant matrix of the same size as the other inputs. ## ## Further information about the Burr distribution can be found at ## @url{https://en.wikipedia.org/wiki/Burr_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{burrcdf, burrpdf, burrrnd, burrfit, burrlike, burrstat} ## @end deftypefn function x = burrinv (p, lambda, c, k) ## Check for valid number of input arguments if (nargin < 4) error ("burrinv: function called with too few input arguments."); endif ## Check for common size of P, LANBDA, C and K if (! isscalar (p) || ! isscalar (lambda) || ! isscalar (c) || ! isscalar (k)) [retval, p, lambda, c, k] = common_size (p, lambda, c, k); if (retval > 0) error ("burrinv: P, LAMBDA, C, and K must be of common size or scalars."); endif endif ## Check for P, LAMBDA, C, and K being double or single if (! (isfloat (p) && isfloat (lambda) && isfloat (c) && isfloat (k))) error ("burrinv: P, LAMBDA, C, and K must be double or single."); endif ## Check for P, LANBDA, C and K being reals if (iscomplex (p) || iscomplex (lambda) || iscomplex (c) || iscomplex (k)) error ("burrinv: P, LAMBDA, C, and K must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (lambda, 'single') || isa (c, 'single') ... || isa (k, 'single')) x = zeros (size (p), 'single'); else x = zeros (size (p)); endif ## Force NaNs for out of range parameters j = isnan (p) | (p < 0) | (p > 1) | ! (lambda > 0) | ! (c > 0) | ! (k > 0); x(j) = NaN; ## Handle edge cases j = (p == 1) & (lambda > 0) & (lambda < Inf) & (c > 0) & (c < Inf) ... & (k > 0) & (k < Inf); x(j) = Inf; ## Handle all other valid cases j = (0 < p) & (p < 1) & (0 < lambda) & (lambda < Inf) & (0 < c) & (c < Inf) ... & (0 < k) & (k < Inf); if (isscalar (lambda) && isscalar (c) && isscalar (k)) x(j) = lambda * ((1 - p(j)).^(-1 / k) - 1).^(1 / c); else x(j) = lambda(j) .* ((1 - p(j)).^(-1 ./ k(j)) - 1).^(1 ./ c(j)); endif endfunction %!demo %! ## Plot various iCDFs from the Burr type XII distribution %! p = 0.001:0.001:0.999; %! x1 = burrinv (p, 1, 1, 1); %! x2 = burrinv (p, 1, 1, 2); %! x3 = burrinv (p, 1, 1, 3); %! x4 = burrinv (p, 1, 2, 1); %! x5 = burrinv (p, 1, 3, 1); %! x6 = burrinv (p, 1, 0.5, 2); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', ... %! p, x4, '-c', p, x5, '-m', p, x6, '-k') %! grid on %! ylim ([0, 5]) %! legend ({'λ = 1, c = 1, k = 1', 'λ = 1, c = 1, k = 2', ... %! 'λ = 1, c = 1, k = 3', 'λ = 1, c = 2, k = 1', ... %! 'λ = 1, c = 3, k = 1', 'λ = 1, c = 0.5, k = 2'}, ... %! 'location', 'northwest') %! title ('Burr type XII iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p, y %! p = [-Inf, -1, 0, 1/2, 1, 2, Inf]; %! y = [NaN, NaN, 0, 1 , Inf, NaN, NaN]; %!assert_equal (burrinv (p, ones (1,7), ones (1,7), ones (1,7)), y, eps) %!assert_equal (burrinv (p, 1, 1, 1), y, eps) %!assert_equal (burrinv (p, [1, 1, 1, NaN, 1, 1, 1], 1, 1), [y(1:3), NaN, y(5:7)], eps) %!assert_equal (burrinv (p, 1, [1, 1, 1, NaN, 1, 1, 1], 1), [y(1:3), NaN, y(5:7)], eps) %!assert_equal (burrinv (p, 1, 1, [1, 1, 1, NaN, 1, 1, 1]), [y(1:3), NaN, y(5:7)], eps) %!assert_equal (burrinv ([p, NaN], 1, 1, 1), [y, NaN], eps) ## Test the scale parameter, which the quantile is multiplied by %!assert_equal (burrinv (0.5, 2, 1, 1), 2, eps) %!assert_equal (burrinv (0.5, 3, 0.5, 0.5), 27, eps) %!assert_equal (burrinv (0.5, 1.5, 2, 0.5), 1.5 * sqrt (3), eps) %!assert_equal (burrinv (0.5, 5, 2, 2), 5 * sqrt (sqrt (2) - 1), eps) ## Test that the quantile undoes the CDF, where the scale is not 1 %!assert_equal (burrcdf (burrinv (0.25, 2, 0.9, 4.2), 2, 0.9, 4.2), 0.25, 1e-12) %!assert_equal (burrcdf (burrinv (0.75, 3, 0.5, 0.5), 3, 0.5, 0.5), 0.75, 1e-12) %!assert_equal (burrcdf (burrinv (0.5, 0.4, 3, 0.2), 0.4, 3, 0.2), 0.5, 1e-12) %!test %! q = [0.05, 0.25, 0.5, 0.75, 0.95]; %! assert_equal (burrcdf (burrinv (q, 1.7, 1.3, 2.6), 1.7, 1.3, 2.6), q, 1e-12); ## Test that a scale below the probability still gives a real quantile %!assert_equal (isreal (burrinv (0.9, 0.5, 2, 3)), true) ## Test the scale taken elementwise %!assert_equal (burrinv ([0.5, 0.5], [2, 4], 1, 1), [2, 4], eps) ## Test class of input preserved %!assert_equal (burrinv (single ([p, NaN]), 1, 1, 1), single ([y, NaN]), eps ('single')) %!assert_equal (burrinv ([p, NaN], single (1), 1, 1), single ([y, NaN]), eps ('single')) %!assert_equal (burrinv ([p, NaN], 1, single (1), 1), single ([y, NaN]), eps ('single')) %!assert_equal (burrinv ([p, NaN], 1, 1, single (1)), single ([y, NaN]), eps ('single')) ## Test input validation %!error burrinv () %!error burrinv (1) %!error burrinv (1, 2) %!error burrinv (1, 2, 3) %!error ... %! burrinv (1, 2, 3, 4, 5) %!error ... %! burrinv (ones (3), ones (2), ones (2), ones (2)) %!error ... %! burrinv (ones (2), ones (3), ones (2), ones (2)) %!error ... %! burrinv (ones (2), ones (2), ones (3), ones (2)) %!error ... %! burrinv (ones (2), ones (2), ones (2), ones (3)) %!error burrinv (int32 (2), 2, 3, 4) %!error burrinv (true, 2, 3, 4) %!error burrinv ('a', 2, 3, 4) %!error burrinv (i, 2, 3, 4) %!error burrinv (1, i, 3, 4) %!error burrinv (1, 2, i, 4) %!error burrinv (1, 2, 3, i) statistics-release-2.1.2/inst/Distribution_Functions/burrpdf.m000066400000000000000000000151341527311104300246330ustar00rootroot00000000000000## Copyright (C) 1995-2015 Kurt Hornik ## Copyright (C) 2016 Dag Lyberg ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} burrpdf (@var{x}, @var{lambda}, @var{c}, @var{k}) ## ## Burr type XII probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the Burr type XII distribution with scale parameter @var{lambda}, first ## shape parameter @var{c}, and second shape parameter @var{k}. The size of ## @var{y} is the common size of @var{x}, @var{lambda}, @var{c}, and @var{k}. ## A scalar input functions as a constant matrix of the same size as the other ## inputs. ## ## Further information about the Burr distribution can be found at ## @url{https://en.wikipedia.org/wiki/Burr_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{burrcdf, burrinv, burrrnd, burrfit, burrlike, burrstat} ## @end deftypefn function y = burrpdf (x, lambda, c, k) ## Check for valid number of input arguments if (nargin < 4) error ("burrpdf: function called with too few input arguments."); endif ## Check for common size of X, LANBDA, C and K if (! isscalar (x) || ! isscalar (lambda) || ! isscalar (c) || ! isscalar (k)) [retval, x, lambda, c, k] = common_size (x, lambda, c, k); if (retval > 0) error ("burrpdf: X, LAMBDA, C, and K must be of common size or scalars."); endif endif ## Check for X, LAMBDA, C, and K being double or single if (! (isfloat (x) && isfloat (lambda) && isfloat (c) && isfloat (k))) error ("burrpdf: X, LAMBDA, C, and K must be double or single."); endif ## Check for X, LANBDA, C and K being reals if (iscomplex (x) || iscomplex (lambda) || iscomplex (c) || iscomplex (k)) error ("burrpdf: X, LAMBDA, C, and K must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (lambda, 'single') ... || isa (c, 'single') || isa (k, 'single')) y = zeros (size (x), 'single'); else y = zeros (size (x)); endif ## Force NaNs for out of range parameters j = isnan (x) | ! (lambda > 0) | ! (c > 0) | ! (k > 0); y(j) = NaN; ## Find valid values in parameters and data j = (x >= 0) & (0 < lambda) & (lambda < Inf) & (0 < c) & (c < Inf) ... & (0 < k) & (k < Inf); ## Compute Burr PDF if (isscalar (lambda) && isscalar (c) && isscalar (k)) y(j) = (c * k / lambda) .* (x(j) / lambda) .^ (c - 1) ./ ... (1 + (x(j) / lambda) .^ c) .^ (k + 1); else y(j) = (c(j) .* k(j) ./ lambda(j) ) .* x(j).^(c(j) - 1) ./ ... (1 + (x(j) ./ lambda(j) ) .^ c(j)) .^ (k(j) + 1); endif endfunction %!demo %! ## Plot various PDFs from the Burr type XII distribution %! x = 0.001:0.001:3; %! y1 = burrpdf (x, 1, 1, 1); %! y2 = burrpdf (x, 1, 1, 2); %! y3 = burrpdf (x, 1, 1, 3); %! y4 = burrpdf (x, 1, 2, 1); %! y5 = burrpdf (x, 1, 3, 1); %! y6 = burrpdf (x, 1, 0.5, 2); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', ... %! x, y4, '-c', x, y5, '-m', x, y6, '-k') %! grid on %! ylim ([0, 2]) %! legend ({'λ = 1, c = 1, k = 1', 'λ = 1, c = 1, k = 2', ... %! 'λ = 1, c = 1, k = 3', 'λ = 1, c = 2, k = 1', ... %! 'λ = 1, c = 3, k = 1', 'λ = 1, c = 0.5, k = 2'}, ... %! 'location', 'northeast') %! title ('Burr type XII PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x, y %! x = [-1, 0, 1, 2, Inf]; %! y = [0, 1, 1/4, 1/9, 0]; %!assert_equal (burrpdf (x, ones (1,5), ones (1,5), ones (1,5)), y) %!assert_equal (burrpdf (x, 1, 1, 1), y) %!assert_equal (burrpdf (x, [1, 1, NaN, 1, 1], 1, 1), [y(1:2), NaN, y(4:5)]) %!assert_equal (burrpdf (x, 1, [1, 1, NaN, 1, 1], 1), [y(1:2), NaN, y(4:5)]) %!assert_equal (burrpdf (x, 1, 1, [1, 1, NaN, 1, 1]), [y(1:2), NaN, y(4:5)]) %!assert_equal (burrpdf ([x, NaN], 1, 1, 1), [y, NaN]) ## Test class of input preserved %!assert_equal (burrpdf (single ([x, NaN]), 1, 1, 1), single ([y, NaN])) %!assert_equal (burrpdf ([x, NaN], single (1), 1, 1), single ([y, NaN])) %!assert_equal (burrpdf ([x, NaN], 1, single (1), 1), single ([y, NaN])) %!assert_equal (burrpdf ([x, NaN], 1, 1, single (1)), single ([y, NaN])) ## Test input validation %!error burrpdf () %!error burrpdf (1) %!error burrpdf (1, 2) %!error burrpdf (1, 2, 3) %!error ... %! burrpdf (1, 2, 3, 4, 5) %!error ... %! burrpdf (ones (3), ones (2), ones (2), ones (2)) %!error ... %! burrpdf (ones (2), ones (3), ones (2), ones (2)) %!error ... %! burrpdf (ones (2), ones (2), ones (3), ones (2)) %!error ... %! burrpdf (ones (2), ones (2), ones (2), ones (3)) %!error burrpdf (int32 (2), 2, 3, 4) %!error burrpdf (true, 2, 3, 4) %!error burrpdf ('a', 2, 3, 4) %!error burrpdf (i, 2, 3, 4) %!error burrpdf (1, i, 3, 4) %!error burrpdf (1, 2, i, 4) %!error burrpdf (1, 2, 3, i) statistics-release-2.1.2/inst/Distribution_Functions/burrrnd.m000066400000000000000000000162361527311104300246510ustar00rootroot00000000000000## Copyright (C) 1995-2015 Kurt Hornik ## Copyright (C) 2016 Dag Lyberg ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} burrrnd (@var{lambda}, @var{c}, @var{k}) ## @deftypefnx {statistics} {@var{r} =} burrrnd (@var{lambda}, @var{c}, @var{k}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} burrrnd (@var{lambda}, @var{c}, @var{k}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} burrrnd (@var{lambda}, @var{c}, @var{k}, [@var{sz}]) ## ## Random arrays from the Burr type XII distribution. ## ## @code{@var{r} = burrrnd (@var{lambda}, @var{c}, @var{k})} returns an array of ## random numbers chosen from the Burr type XII distribution with scale ## parameter @var{lambda}, first shape parameter @var{c}, and second shape ## parameter @var{k}. The size of @var{r} is the common size of @var{lambda}, ## @var{c}, and @var{k}. LAMBDA scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## When called with a single size argument, @code{burrrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the Burr distribution can be found at ## @url{https://en.wikipedia.org/wiki/Burr_distribution} ## ## @seealso{burrcdf, burrinv, burrpdf, burrfit, burrlike, burrstat} ## @end deftypefn function r = burrrnd (lambda, c, k, varargin) ## Check for valid number of input arguments if (nargin < 3) error ("burrrnd: function called with too few input arguments."); endif ## Check for common size of LAMBDA, C, and K if (! isscalar (lambda) || ! isscalar (c) || ! isscalar (k)) [retval, lambda, c, k] = common_size (lambda, c, k); if (retval > 0) error ("burrrnd: LAMBDA, C, and K must be of common size or scalars."); endif endif ## Check for LAMBDA, C, and K being reals if (iscomplex (lambda) || iscomplex (c) || iscomplex (k)) error ("burrrnd: LAMBDA, C, and K must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 3) sz = size (lambda); elseif (nargin == 4) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("burrrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 4) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("burrrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (lambda) && ! isequal (size (lambda), size (ones (sz)))) error ("burrrnd: LAMBDA, C, and K must be scalars or of size SZ."); endif ## Check for class type if (isa (lambda, 'single') || isa (c, 'single') || isa (k, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from Burr type XII distribution lambda(lambda <= 0) = NaN; c(c <= 0) = NaN; k(k <= 0) = NaN; r = lambda .* (((1 - rand (sz, cls)) .^ (-(1./k))) - 1) .^ (1./c); endfunction ## Test output %!assert_equal (size (burrrnd (1, 1, 1)), [1 1]) %!assert_equal (size (burrrnd (ones (2,1), 1, 1)), [2, 1]) %!assert_equal (size (burrrnd (ones (2,2), 1, 1)), [2, 2]) %!assert_equal (size (burrrnd (1, ones (2,1), 1)), [2, 1]) %!assert_equal (size (burrrnd (1, ones (2,2), 1)), [2, 2]) %!assert_equal (size (burrrnd (1, 1, ones (2,1))), [2, 1]) %!assert_equal (size (burrrnd (1, 1, ones (2,2))), [2, 2]) %!assert_equal (size (burrrnd (1, 1, 1, 3)), [3, 3]) %!assert_equal (size (burrrnd (1, 1, 1, [4 1])), [4, 1]) %!assert_equal (size (burrrnd (1, 1, 1, 4, 1)), [4, 1]) %!assert_equal (size (burrrnd (1, 1, 1, [])), [0, 0]) %!assert_equal (size (burrrnd (1, 1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (burrrnd (1, 2, 3, -1)), [0, 0]) %!assert_equal (size (burrrnd (1, 2, 3, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (burrrnd (1, 2, 3, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (burrrnd (1,1,1)), "double") %!assert_equal (class (burrrnd (single (1),1,1)), "single") %!assert_equal (class (burrrnd (single ([1 1]),1,1)), "single") %!assert_equal (class (burrrnd (1,single (1),1)), "single") %!assert_equal (class (burrrnd (1,single ([1 1]),1)), "single") %!assert_equal (class (burrrnd (1,1,single (1))), "single") %!assert_equal (class (burrrnd (1,1,single ([1 1]))), "single") ## Test input validation %!error burrrnd () %!error burrrnd (1) %!error burrrnd (1, 2) %!error ... %! burrrnd (ones (3), ones (2), ones (2)) %!error ... %! burrrnd (ones (2), ones (3), ones (2)) %!error ... %! burrrnd (ones (2), ones (2), ones (3)) %!error burrrnd (i, 2, 3) %!error burrrnd (1, i, 3) %!error burrrnd (1, 2, i) %!error ... %! burrrnd (1, 2, 3, 1.2) %!error ... %! burrrnd (1, 2, 3, ones (2)) %!error ... %! burrrnd (1, 2, 3, [2 0 2.5]) %!error ... %! burrrnd (1, 2, 3, 2, 1.5, 5) %!error ... %! burrrnd (2, ones (2), 2, 3) %!error ... %! burrrnd (2, ones (2), 2, [3, 2]) %!error ... %! burrrnd (2, ones (2), 2, 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/bvncdf.m000066400000000000000000000231521527311104300244300ustar00rootroot00000000000000## Copyright (C) 2022-2025 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} bvncdf (@var{x}, @var{mu}, @var{sigma}) ## @deftypefnx {statistics} {@var{p} =} bvncdf (@var{x}, [], @var{sigma}) ## ## Bivariate normal cumulative distribution function (CDF). ## ## @code{@var{p} = bvncdf (@var{x}, @var{mu}, @var{sigma})} will compute the ## bivariate normal cumulative distribution function of @var{x} given a mean ## parameter @var{mu} and a scale parameter @var{sigma}. ## ## @itemize ## @item @var{x} must be an @math{N*2} matrix with each variable as a column ## vector. ## @item @var{mu} can be either a scalar (common mean) or a two-element row ## vector (each element corresponds to a variable). If empty, a zero mean is ## assumed. ## @item @var{sigma} can be a scalar (common variance) or a @math{2*2} ## covariance matrix, which must be positive definite. ## @end itemize ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{mvncdf} ## @end deftypefn ## Code adapted from Thomas H. Jørgensen's work in BVNcdf.m function retrieved ## from https://www.tjeconomics.com/code/ function p = bvncdf (x, mu, sigma) ## Check input arguments and add defaults ## Check for X, MU, and SIGMA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (sigma))) error ("bvncdf: X, MU, and SIGMA must be double or single."); endif if (size (x, 2) != 2) error (strcat ("bvncdf: X must be an Nx2 matrix with each variable", ... " as a column vector.")); endif if (isempty (mu)) mu = [0, 0]; elseif (isscalar (mu)) mu = [mu, mu]; elseif (numel (mu) == 2); mu = mu(:)'; else error ("bvncdf: MU must be a scalar or a two-element vector."); endif if (numel (sigma) == 1) sigma = sigma * ones (2,2); sigma(1,1) = 1; sigma(2,2) = 1; elseif (numel (sigma) != 4) error (strcat ("bvncdf: the covariance matrix must be either a", ... " scalar or a 2x2 matrix.")); endif ## Test for symmetric positive definite covariance matrix [~, err] = chol (sigma); if (err != 0) error (strcat ("bvncdf: the covariance matrix is not positive", ... " definite and/or symmetric.")); endif dh = (x(:,1) - mu(:,1)) / sqrt (sigma(1,1)); dk = (x(:,2) - mu(:,2)) / sqrt (sigma(2,2)); r = sigma(1,2) / sqrt (sigma(1,1) * sigma(2,2)); p = NaN (size (dh)); ## Handle special cases for infinite integration limits p(dh == Inf & dk == Inf) = 1; ## Both limits are infinite: P(X1 ≤ ∞, X2 ≤ ∞) = 1 p(dk == Inf & dh != Inf) = 0.5 * erfc (- dh(dk == Inf & dh != Inf) / sqrt (2)); ## x2 → +∞ (finite x1): P(X1 ≤ x1, X2 ≤ ∞) = P(X1 ≤ x1) = Φ(dh) p(dh == Inf & dk != Inf) = 0.5 * erfc (- dk(dh == Inf & dk != Inf) / sqrt (2)); ## x1 → +∞ (finite x2): P(X1 ≤ ∞, X2 ≤ x2) = P(X2 ≤ x2) = Φ(dk) p(dh == -Inf | dk == -Inf) = 0; ## x1 → -∞ or x2 → -∞: P(X1 ≤ -∞, X2 ≤ x2) = P(X1 ≤ x1, X2 ≤ -∞) = 0 ind = (dh > -Inf & dh < Inf & dk > -Inf & dk < Inf); ## For p(x1 < dh, x2 < dk, r) if (sum (ind) > 0) p(ind) = calculate_bvncdf (-dh(ind), -dk(ind), r); endif endfunction function p = calculate_bvncdf (dh,dk,r) if (abs (r) < 0.3) lg = 3; ## Gauss Legendre points and weights, n = 6 w = [0.1713244923791705, 0.3607615730481384, 0.4679139345726904]; x = [0.9324695142031522, 0.6612093864662647, 0.2386191860831970]; elseif (abs (r) < 0.75) lg = 6; ## Gauss Legendre points and weights, n = 12 w = [.04717533638651177, 0.1069393259953183, 0.1600783285433464, ... 0.2031674267230659, 0.2334925365383547, 0.2491470458134029]; x = [0.9815606342467191, 0.9041172563704750, 0.7699026741943050, ... 0.5873179542866171, 0.3678314989981802, 0.1252334085114692]; else lg = 10; ## Gauss Legendre points and weights, n = 20 w = [.01761400713915212, .04060142980038694, .06267204833410906, ... .08327674157670475, 0.1019301198172404, 0.1181945319615184, ... 0.1316886384491766, 0.1420961093183821, 0.1491729864726037, ... 0.1527533871307259]; x = [0.9931285991850949, 0.9639719272779138, 0.9122344282513259, ... 0.8391169718222188, 0.7463319064601508, 0.6360536807265150, ... 0.5108670019508271, 0.3737060887154196, 0.2277858511416451, ... 0.07652652113349733]; endif dim1 = ones (size (dh, 1), 1); dim2 = ones (1, lg); hk = dh .* dk; bvn = dim1 * 0; phi_dh = 0.5 * erfc (dh / sqrt (2)); phi_dk = 0.5 * erfc (dk / sqrt (2)); if (abs (r) < 0.925) hs = (dh .* dh + dk .* dk) / 2; asr = asin (r); sn1 = sin (asr * (1 - x) / 2); sn2 = sin (asr * (1 + x) / 2); bvn = sum ((dim1 * w) .* exp (((dim1 * sn1) .* (hk * dim2) - ... hs * dim2) ./ (1 - dim1 * (sn1 .^ 2))) + ... (dim1 * w) .* exp (((dim1 * sn2) .* (hk * dim2) - ... hs * dim2) ./ (1 - dim1 * (sn2 .^ 2))), 2) * ... asr / (4 * pi) + phi_dh .* phi_dk; else twopi = 2 * pi; if r < 0 dk = -dk; hk = -hk; endif if abs (r) < 1 as = (1 - r) * (1 + r); a = sqrt (as); bs = (dh - dk) .^ 2; c = (4 - hk) / 8; d = (12 - hk) / 16; asr = - (bs ./ as + hk) / 2; ind = asr > -100; bvn(ind) = a * exp (asr(ind)) .* (1 - (c(ind) .* (bs(ind) - as)) ... .* (1 - d(ind) .* bs(ind) / 5) /3 ... + (c(ind) .* d(ind)) .* as .^ 2 / 5 ); ind = hk > -100; b = sqrt (bs); phi_ba = 0.5 * erfc ((b/a) / sqrt (2)); sp = sqrt (twopi) * phi_ba; bvn(ind) = bvn(ind) - (exp (-hk(ind) / 2) .* sp(ind)) ... .* b(ind) .* (1 - c(ind) .* bs(ind) ... .* (1 - d(ind) .* bs(ind) / 5) /3); a = a/2; for is = -1:2:1 xs = (a + a * is * x) .^ 2; rs = sqrt (1 - xs); asr1 = - ((bs * dim2) ./ (dim1 * xs) + hk * dim2) / 2; ind1 = (asr1 > -100); sp1 = (1 + (c * dim2) .* (dim1 * xs) .* ... (1 + (d * dim2) .* (dim1 * xs))); ep1 = exp (- (hk * dim2) .* (1 - dim1 * rs) ./ ... (2 * (1 + dim1 * rs))) ./ (dim1 * rs); bvn = bvn + sum (a .* (dim1 * w) .* exp (asr1 .* ind1) ... .* (ep1 .* ind1 - sp1 .* ind1), 2); endfor bvn = -bvn/twopi; endif if (r > 0) tmp = max (dh, dk); bvn = bvn + 0.5 * erfc (tmp / sqrt (2)); elseif (r < 0) phi_dh = 0.5 * erfc (dh / sqrt (2)); phi_dk = 0.5 * erfc (dk / sqrt (2)); bvn = - bvn + max (0, phi_dh - phi_dk); endif endif p = max (0, min (1, bvn)); endfunction %!demo %! mu = [1, -1]; %! sigma = [0.9, 0.4; 0.4, 0.3]; %! [X1, X2] = meshgrid (linspace (-1, 3, 25)', linspace (-3, 1, 25)'); %! x = [X1(:), X2(:)]; %! p = bvncdf (x, mu, sigma); %! Z = reshape (p, 25, 25); %! surf (X1, X2, Z); %! title ('Bivariate Normal Distribution'); %! ylabel 'X1' %! xlabel 'X2' ## Test output %!test %! mu = [1, -1]; %! sigma = [0.9, 0.4; 0.4, 0.3]; %! [X1,X2] = meshgrid (linspace (-1, 3, 25)', linspace (-3, 1, 25)'); %! x = [X1(:), X2(:)]; %! p = bvncdf (x, mu, sigma); %! p_out = [0.00011878988774500, 0.00034404112322371, ... %! 0.00087682502191813, 0.00195221905058185, ... %! 0.00378235566873474, 0.00638175749734415, ... %! 0.00943764224329656, 0.01239164888125426, ... %! 0.01472750274376648, 0.01623228313374828]'; %! assert_equal (p([1:10]), p_out, 1e-16); %!test %! mu = [1, -1]; %! sigma = [0.9, 0.4; 0.4, 0.3]; %! [X1,X2] = meshgrid (linspace (-1, 3, 25)', linspace (-3, 1, 25)'); %! x = [X1(:), X2(:)]; %! p = bvncdf (x, mu, sigma); %! p_out = [0.8180695783608276, 0.8854485749482751, ... %! 0.9308108777385832, 0.9579855743025508, ... %! 0.9722897881414742, 0.9788150170059926, ... %! 0.9813597788804785, 0.9821977956568989, ... %! 0.9824283794464095, 0.9824809345614861]'; %! assert_equal (p([616:625]), p_out, 3e-16); %!test %! ## Test infinite limits %! mu = [0, 0]; %! sigma = [1 0.5; 0.5 1]; %! assert_equal (bvncdf ([Inf, Inf], mu, sigma), 1); %! assert_equal (bvncdf ([-Inf, 2], mu, sigma), 0); %! assert_equal (bvncdf ([1, -Inf], mu, sigma), 0); %! assert_equal (bvncdf ([0.5, Inf], mu, sigma), normcdf (0.5), eps); %! assert_equal (bvncdf ([Inf, 0.5], mu, sigma), normcdf (0.5), eps); %!error bvncdf (int32 ([0, 0]), [0, 0], eye (2)) %!error bvncdf ([true, true], [0, 0], eye (2)) %!error bvncdf ('ab', [0, 0], eye (2)) %!error bvncdf (randn (25,3), [], [1, 1; 1, 1]); %!error bvncdf (randn (25,2), [], [1, 1; 1, 1]); %!error bvncdf (randn (25,2), [], ones (3, 2)); statistics-release-2.1.2/inst/Distribution_Functions/bvtcdf.m000066400000000000000000000160131527311104300244340ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} bvtcdf (@var{x}, @var{rho}, @var{df}) ## @deftypefnx {statistics} {@var{p} =} bvtcdf (@var{x}, @var{rho}, @var{df}, @var{Tol}) ## ## Bivariate Student's t cumulative distribution function (CDF). ## ## @code{@var{p} = bvtcdf (@var{x}, @var{rho}, @var{df})} will compute the ## bivariate student's t cumulative distribution function of @var{x}, which must ## be an @math{N*2} matrix, given a correlation coefficient @var{rho}, which ## must be a scalar, and @var{df} degrees of freedom, which can be a scalar or a ## vector of positive numbers commensurate with @var{x}. ## ## @var{Tol} is the tolerance for numerical integration and by default ## @code{@var{Tol} = 1e-8}. ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{mvtcdf} ## @end deftypefn function p = bvtcdf (x, rho, df, TolFun) narginchk (3,4); if (nargin < 4) TolFun = 1e-8; endif ## Check for X, RHO, and DF being double or single if (! (isfloat (x) && isfloat (rho) && isfloat (df))) error ("bvtcdf: X, RHO, and DF must be double or single."); endif if (isa (x, 'single') || isa (rho, 'single') || isa (df, 'single')) is_type = 'single'; else is_type = 'double'; endif if (abs (rho) < 1) largeNu = 1e4; general = ! (fix (df) == df & df < largeNu); if (isscalar (df)) if general p = generalDF (x, rho, repmat (df, size (x, 1), 1), TolFun); else p = integerDF (x, rho, df); endif else p = zeros (size (x, 1), 1, is_type); ## For large of non-integer df if (any (general)) p(general) = generalDF (x(general,:), rho, df(general), TolFun); endif ## For small integer df for i = find (! general(:)') p(i) = integerDF (x(i,:), rho, df(i)); endfor endif elseif (rho == 1) p = tcdf (min (x, [], 2), df); p(any (isnan ( x), 2)) = NaN; else p = tcdf (x(:,1), df) - tcdf (-x(:,2), df); endif endfunction ## CDF for the bivariate t with integer degrees of freedom function p = integerDF (x, rho, df) x1 = x(:,1); x2 = x(:,2); tau = 1 - rho .^ 2; x1rx2 = x1 - rho * x2; x2rx1 = x2 - rho * x1; sx1r2 = sign (x1rx2); sx2r1 = sign (x2rx1); dfx1s = df + x1 .^ 2; dfx2s = df + x2 .^ 2; tdfx1x2 = tau * dfx2s ./ x1rx2 .^ 2; tdfx2x1 = tau * dfx1s ./ x2rx1 .^ 2; x_tdf12 = 1 ./ (1 + tdfx1x2); x_tdf21 = 1 ./ (1 + tdfx2x1); y_tdf12 = 1 ./ (1 + 1 ./ tdfx1x2); y_tdf21 = 1 ./ (1 + 1 ./ tdfx2x1); sqrtDF = sqrt (df); halfDF = df/2; if (fix (halfDF) == halfDF) # for even DF p1 = atan2 (sqrt (tau), -rho) ./ (2 * pi); c1 = x1 ./ (4 * sqrt (dfx1s)); c2 = x2 ./ (4 * sqrt (dfx2s)); beta12 = 2 *atan2 (sqrt (x_tdf12), sqrt (y_tdf12)) / pi; beta21 = 2 *atan2 (sqrt (x_tdf21), sqrt (y_tdf21)) / pi; p2 = (1 + sx1r2 .* beta12) .* c2 + (1 + sx2r1 .* beta21) .* c1; betaT12 = 2 * sqrt (x_tdf12 .* y_tdf12) / pi; betaT21 = 2 * sqrt (x_tdf21 .* y_tdf21) / pi; for j = 2:halfDF fact = df * (j - 1.5) / (j - 1); c2 = c2 .* fact ./ dfx2s; c1 = c1 .* fact ./ dfx1s; beta12 = beta12 + betaT12; beta21 = beta21 + betaT21; p2 = p2 + (1 + sx1r2 .* beta12) .* c2 + (1 + sx2r1 .* beta21) .* c1; fact = 2 * (j - 1) / (2 * (j - 1) + 1); betaT12 = fact * betaT12 .* y_tdf12; betaT21 = fact * betaT21 .* y_tdf21; endfor else # for odd DF x1x2p = x1.*x2; x1x2s = x1 + x2; t1 = sqrt (x1 .^ 2 - 2 * rho * x1x2p + x2 .^ 2 + tau * df); t2 = x1x2p + rho * df; t3 = x1x2p - df; p1 = atan2 (sqrtDF .* (-x1x2s .* t2 - t3 .* t1), ... t3 .* t2 - df .* x1x2s .* t1) ./ (2 * pi); p1 = p1 + (p1 < 0); p2 = 0; if (df > 1) c1 = sqrtDF .* x1 ./ (2 * pi .* dfx1s); c2 = sqrtDF .* x2 ./ (2 * pi .* dfx2s); betaT12 = sqrt (x_tdf12); betaT21 = sqrt (x_tdf21); beta12 = betaT12; beta21 = betaT21; p2 = (1 + sx1r2 .* beta12) .* c2 + (1 + sx2r1 .* beta21) .* c1; for j = 2:(halfDF - 0.5) fact = df * (j - 1) / (j - 0.5); c2 = fact * c2 ./ dfx2s; c1 = fact * c1 ./ dfx1s; fact = 1 - 0.5 / (j - 1); betaT12 = fact * betaT12 .* y_tdf12; betaT21 = fact * betaT21 .* y_tdf21; beta12 = beta12 + betaT12; beta21 = beta21 + betaT21; p2 = p2 + (1 + sx1r2 .* beta12) .* c2 + (1 + sx2r1 .* beta21) .* c1; endfor endif endif p = p1 + p2; ## Fix limit cases large = 1e10; p(x1 < -large | x2 < -large) = 0; p(x1 > large) = tcdf (x2(x1 > large), df); p(x2 > large) = tcdf (x1(x2 > large), df); endfunction ## CDF for the bivariate t with arbitrary degrees of freedom. function p = generalDF (x, rho, df, TolFun) n = size (x, 1); if (rho >= 0) p1 = tcdf (min (x, [], 2), df); p1(any (isnan (x), 2)) = NaN; else p1 = tcdf (x(:,1), df) - tcdf (-x(:,2), df); p1(p1 < 0) = 0; endif lo = asin (rho); hi = (sign (rho) + (rho == 0)) .* pi ./ 2; p2 = zeros (size (p1), class (rho)); for i = 1:n b1 = x(i,1); b2 = x(i,2); v = df(i); if (isfinite (b1) && isfinite (b2)) p2(i) = quadgk (@bvtIntegrand, lo, hi, 'AbsTol', TolFun, 'RelTol', 0); endif endfor p = p1 - p2 ./ (2 .* pi); function integrand = bvtIntegrand (theta) st = sin (theta); ct2 = cos (theta).^2; integrand = (1 ./ (1 + ((b1 * st - b2) .^ 2 ./ ct2 + b1 .^ 2) / v)) ... .^ (v / 2); endfunction endfunction ## Test output %!test %! x = [1, 2]; %! rho = [1, 0.5; 0.5, 1]; %! df = 4; %! assert_equal (bvtcdf (x, rho(2), df), mvtcdf (x, rho, df), 1e-14); %!test %! x = [3, 2;2, 4;1, 5]; %! rho = [1, 0.5; 0.5, 1]; %! df = 4; %! assert_equal (bvtcdf (x, rho(2), df), mvtcdf (x, rho, df), 1e-14); ## Test input validation %!error bvtcdf (int32 ([0, 0]), 0.5, 5) %!error bvtcdf ([true, true], 0.5, 5) %!error bvtcdf ('ab', 0.5, 5) statistics-release-2.1.2/inst/Distribution_Functions/cauchycdf.m000066400000000000000000000147671527311104300251330ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} cauchycdf (@var{x}, @var{x0}, @var{gamma}) ## @deftypefnx {statistics} {@var{p} =} cauchycdf (@var{x}, @var{x0}, @var{gamma}, @qcode{'upper'}) ## ## Cauchy cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the Cauchy distribution with location parameter @var{x0} and scale ## parameter @var{gamma}. The size of @var{p} is the common size of @var{x}, ## @var{x0}, and @var{gamma}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## @code{@var{p} = cauchycdf (@var{x}, @var{x0}, @var{gamma}, "upper")} computes ## the upper tail probability of the Cauchy distribution with parameters ## @var{x0} and @var{gamma}, at the values in @var{x}. ## ## Further information about the Cauchy distribution can be found at ## @url{https://en.wikipedia.org/wiki/Cauchy_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{cauchyinv, cauchypdf, cauchyrnd} ## @end deftypefn function p = cauchycdf (x, x0, gamma, uflag) ## Check for valid number of input arguments if (nargin < 3) error ("cauchycdf: function called with too few input arguments."); endif ## Check for valid "upper" flag if (nargin > 3) if (! strcmpi (uflag, 'upper')) error ("cauchycdf: invalid argument for upper tail."); else uflag = true; endif else uflag = false; endif ## Check for common size of X, X0, and GAMMA if (! isscalar (x) || ! isscalar (x0) || ! isscalar (gamma)) [retval, x, x0, gamma] = common_size (x, x0, gamma); if (retval > 0) error (strcat ("cauchycdf: X, X0, and GAMMA must be of", ... " common size or scalars.")); endif endif ## Check for X, X0, and GAMMA being double or single if (! (isfloat (x) && isfloat (x0) && isfloat (gamma))) error ("cauchycdf: X, X0, and GAMMA must be double or single."); endif ## Check for X, X0, and GAMMA being reals if (iscomplex (x) || iscomplex (x0) || iscomplex (gamma)) error ("cauchycdf: X, X0, and GAMMA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (x0, 'single') || isa (gamma, 'single')); p = NaN (size (x), 'single'); else p = NaN (size (x)); endif ## Find valid values in parameters and data k = ! isinf (x0) & (gamma > 0) & (gamma < Inf); ## Compute Cauchy CDF if (isscalar (x0) && isscalar (gamma)) if (uflag) p = 0.5 + atan ((-x(k) + x0) / gamma) / pi; else p = 0.5 + atan ((x(k) - x0) / gamma) / pi; endif else if (uflag) p(k) = 0.5 + atan ((-x(k) + x0(k)) ./ gamma(k)) / pi; else p(k) = 0.5 + atan ((x(k) - x0(k)) ./ gamma(k)) / pi; endif endif endfunction %!demo %! ## Plot various CDFs from the Cauchy distribution %! x = -5:0.01:5; %! p1 = cauchycdf (x, 0, 0.5); %! p2 = cauchycdf (x, 0, 1); %! p3 = cauchycdf (x, 0, 2); %! p4 = cauchycdf (x, -2, 1); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', x, p4, '-c') %! grid on %! xlim ([-5, 5]) %! legend ({'x0 = 0, γ = 0.5', 'x0 = 0, γ = 1', ... %! 'x0 = 0, γ = 2', 'x0 = -2, γ = 1'}, 'location', 'southeast') %! title ('Cauchy CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, y %! x = [-1 0 0.5 1 2]; %! y = 1/pi * atan ((x-1) / 2) + 1/2; %!assert_equal (cauchycdf (x, ones (1,5), 2*ones (1,5)), y) %!assert_equal (cauchycdf (x, 1, 2*ones (1,5)), y) %!assert_equal (cauchycdf (x, ones (1,5), 2), y) %!assert_equal (cauchycdf (x, [-Inf 1 NaN 1 Inf], 2), [NaN y(2) NaN y(4) NaN]) %!assert_equal (cauchycdf (x, 1, 2*[0 1 NaN 1 Inf]), [NaN y(2) NaN y(4) NaN]) %!assert_equal (cauchycdf ([x(1:2) NaN x(4:5)], 1, 2), [y(1:2) NaN y(4:5)]) %!assert_equal (cauchycdf ([x, NaN], 1, 2), [y, NaN]) ## Test class of input preserved %!assert_equal (cauchycdf (single ([x, NaN]), 1, 2), single ([y, NaN]), eps ('single')) %!assert_equal (cauchycdf ([x, NaN], single (1), 2), single ([y, NaN]), eps ('single')) %!assert_equal (cauchycdf ([x, NaN], 1, single (2)), single ([y, NaN]), eps ('single')) ## Test input validation %!error cauchycdf () %!error cauchycdf (1) %!error ... %! cauchycdf (1, 2) %!error ... %! cauchycdf (1, 2, 3, 4, 5) %!error cauchycdf (1, 2, 3, 'tail') %!error cauchycdf (1, 2, 3, 4) %!error ... %! cauchycdf (ones (3), ones (2), ones (2)) %!error ... %! cauchycdf (ones (2), ones (3), ones (2)) %!error ... %! cauchycdf (ones (2), ones (2), ones (3)) %!error cauchycdf (int32 (2), 2, 2) %!error cauchycdf (true, 2, 2) %!error cauchycdf ('a', 2, 2) %!error cauchycdf (i, 2, 2) %!error cauchycdf (2, i, 2) %!error cauchycdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/cauchyinv.m000066400000000000000000000136031527311104300251570ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} cauchyinv (@var{p}, @var{x0}, @var{gamma}) ## ## Inverse of the Cauchy cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the Cauchy distribution with location parameter @var{x0} and scale parameter ## @var{gamma}. The size of @var{x} is the common size of @var{p}, @var{x0}, ## and @var{gamma}. A scalar input functions as a constant matrix of the same ## size as the other inputs. ## ## Further information about the Cauchy distribution can be found at ## @url{https://en.wikipedia.org/wiki/Cauchy_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{cauchycdf, cauchypdf, cauchyrnd} ## @end deftypefn function x = cauchyinv (p, x0, gamma) ## Check for valid number of input arguments if (nargin < 3) error ("cauchyinv: function called with too few input arguments."); endif ## Check for common size of P, X0, and GAMMA if (! isscalar (p) || ! isscalar (x0) || ! isscalar (gamma)) [retval, p, x0, gamma] = common_size (p, x0, gamma); if (retval > 0) error (strcat ("cauchyinv: P, X0, and GAMMA must be of", ... " common size or scalars.")); endif endif ## Check for P, X0, and GAMMA being double or single if (! (isfloat (p) && isfloat (x0) && isfloat (gamma))) error ("cauchyinv: P, X0, and GAMMA must be double or single."); endif ## Check for P, X0, and GAMMA being reals if (iscomplex (p) || iscomplex (x0) || iscomplex (gamma)) error ("cauchyinv: P, X0, and GAMMA must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (x0, 'single') || isa (gamma, 'single')) x = NaN (size (p), 'single'); else x = NaN (size (p)); endif ## Find valid values in parameters ok = ! isinf (x0) & (gamma > 0) & (gamma < Inf); ## Handle edge cases k0 = (p == 0) & ok; x(k0) = -Inf; k1 = (p == 1) & ok; x(k1) = Inf; ## Handle all other valid cases k = (p > 0) & (p < 1) & ok; if (isscalar (x0) && isscalar (gamma)) x(k) = x0 - gamma * cot (pi * p(k)); else x(k) = x0(k) - gamma(k) .* cot (pi * p(k)); endif endfunction %!demo %! ## Plot various iCDFs from the Cauchy distribution %! p = 0.001:0.001:0.999; %! x1 = cauchyinv (p, 0, 0.5); %! x2 = cauchyinv (p, 0, 1); %! x3 = cauchyinv (p, 0, 2); %! x4 = cauchyinv (p, -2, 1); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', p, x4, '-c') %! grid on %! ylim ([-5, 5]) %! legend ({'x0 = 0, γ = 0.5', 'x0 = 0, γ = 1', ... %! 'x0 = 0, γ = 2', 'x0 = -2, γ = 1'}, 'location', 'northwest') %! title ('Cauchy iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p %! p = [-1 0 0.5 1 2]; %!assert_equal (cauchyinv (p, ones (1,5), 2 * ones (1,5)), [NaN -Inf 1 Inf NaN], eps) %!assert_equal (cauchyinv (p, 1, 2 * ones (1,5)), [NaN -Inf 1 Inf NaN], eps) %!assert_equal (cauchyinv (p, ones (1,5), 2), [NaN -Inf 1 Inf NaN], eps) %!assert_equal (cauchyinv (p, [1 -Inf NaN Inf 1], 2), [NaN NaN NaN NaN NaN]) %!assert_equal (cauchyinv (p, 1, 2 * [1 0 NaN Inf 1]), [NaN NaN NaN NaN NaN]) %!assert_equal (cauchyinv ([p(1:2) NaN p(4:5)], 1, 2), [NaN -Inf NaN Inf NaN]) %!assert_equal (cauchyinv ([p, NaN], 1, 2), [NaN -Inf 1 Inf NaN NaN], eps) ## Test class of input preserved %!assert_equal (cauchyinv (single ([p, NaN]), 1, 2), ... %! single ([NaN -Inf 1 Inf NaN NaN]), eps ('single')) %!assert_equal (cauchyinv ([p, NaN], single (1), 2), ... %! single ([NaN -Inf 1 Inf NaN NaN]), eps ('single')) %!assert_equal (cauchyinv ([p, NaN], 1, single (2)), ... %! single ([NaN -Inf 1 Inf NaN NaN]), eps ('single')) ## Test input validation %!error cauchyinv () %!error cauchyinv (1) %!error ... %! cauchyinv (1, 2) %!error cauchyinv (1, 2, 3, 4) %!error ... %! cauchyinv (ones (3), ones (2), ones (2)) %!error ... %! cauchyinv (ones (2), ones (3), ones (2)) %!error ... %! cauchyinv (ones (2), ones (2), ones (3)) %!error cauchyinv (int32 (2), 4, 3) %!error cauchyinv (true, 4, 3) %!error cauchyinv ('a', 4, 3) %!error cauchyinv (i, 4, 3) %!error cauchyinv (1, i, 3) %!error cauchyinv (1, 4, i) statistics-release-2.1.2/inst/Distribution_Functions/cauchypdf.m000066400000000000000000000133161527311104300251350ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} cauchypdf (@var{x}, @var{x0}, @var{gamma}) ## ## Cauchy probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the Cauchy distribution with location parameter @var{x0} and scale ## parameter @var{gamma}. The size of @var{y} is the common size of @var{x}, ## @var{x0}, and @var{gamma}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## Further information about the Cauchy distribution can be found at ## @url{https://en.wikipedia.org/wiki/Cauchy_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{cauchycdf, cauchyinv, cauchyrnd} ## @end deftypefn function y = cauchypdf (x, x0, gamma) ## Check for valid number of input arguments if (nargin < 3) error ("cauchypdf: function called with too few input arguments."); endif ## Check for common size of X, X0, and GAMMA if (! isscalar (x) || ! isscalar (x0) || ! isscalar (gamma)) [retval, x, x0, gamma] = common_size (x, x0, gamma); if (retval > 0) error (strcat ("cauchypdf: X, X0, and GAMMA must be of", ... " common size or scalars.")); endif endif ## Check for X, X0, and GAMMA being double or single if (! (isfloat (x) && isfloat (x0) && isfloat (gamma))) error ("cauchypdf: X, X0, and GAMMA must be double or single."); endif ## Check for X, X0, and GAMMA being reals if (iscomplex (x) || iscomplex (x0) || iscomplex (gamma)) error ("cauchypdf: X, X0, and GAMMA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (x0, 'single') || isa (gamma, 'single')) y = NaN (size (x), 'single'); else y = NaN (size (x)); endif ## Find valid values in parameters k = ! isinf (x0) & (gamma > 0) & (gamma < Inf); if (isscalar (x0) && isscalar (gamma)) y(k) = ((1 ./ (1 + ((x(k) - x0) / gamma) .^ 2)) / pi / gamma); else y(k) = ((1 ./ (1 + ((x(k) - x0(k)) ./ gamma(k)) .^ 2)) / pi ./ gamma(k)); endif endfunction %!demo %! ## Plot various PDFs from the Cauchy distribution %! x = -5:0.01:5; %! y1 = cauchypdf (x, 0, 0.5); %! y2 = cauchypdf (x, 0, 1); %! y3 = cauchypdf (x, 0, 2); %! y4 = cauchypdf (x, -2, 1); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', x, y4, '-c') %! grid on %! xlim ([-5, 5]) %! ylim ([0, 0.7]) %! legend ({'x0 = 0, γ = 0.5', 'x0 = 0, γ = 1', ... %! 'x0 = 0, γ = 2', 'x0 = -2, γ = 1'}, 'location', 'northeast') %! title ('Cauchy PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x, y %! x = [-1 0 0.5 1 2]; %! y = 1/pi * ( 2 ./ ((x-1).^2 + 2^2) ); %!assert_equal (cauchypdf (x, ones (1,5), 2*ones (1,5)), y) %!assert_equal (cauchypdf (x, 1, 2*ones (1,5)), y) %!assert_equal (cauchypdf (x, ones (1,5), 2), y) %!assert_equal (cauchypdf (x, [-Inf 1 NaN 1 Inf], 2), [NaN y(2) NaN y(4) NaN]) %!assert_equal (cauchypdf (x, 1, 2*[0 1 NaN 1 Inf]), [NaN y(2) NaN y(4) NaN]) %!assert_equal (cauchypdf ([x, NaN], 1, 2), [y, NaN]) ## Test class of input preserved %!assert_equal (cauchypdf (single ([x, NaN]), 1, 2), single ([y, NaN]), eps ('single')) %!assert_equal (cauchypdf ([x, NaN], single (1), 2), single ([y, NaN]), eps ('single')) %!assert_equal (cauchypdf ([x, NaN], 1, single (2)), single ([y, NaN]), eps ('single')) ## Cauchy (0,1) == Student's T distribution with 1 DOF %!test %! x = rand (10, 1); %! assert_equal (cauchypdf (x, 0, 1), tpdf (x, 1), eps); ## Test input validation %!error cauchypdf () %!error cauchypdf (1) %!error ... %! cauchypdf (1, 2) %!error cauchypdf (1, 2, 3, 4) %!error ... %! cauchypdf (ones (3), ones (2), ones (2)) %!error ... %! cauchypdf (ones (2), ones (3), ones (2)) %!error ... %! cauchypdf (ones (2), ones (2), ones (3)) %!error cauchypdf (int32 (2), 4, 3) %!error cauchypdf (true, 4, 3) %!error cauchypdf ('a', 4, 3) %!error cauchypdf (i, 4, 3) %!error cauchypdf (1, i, 3) %!error cauchypdf (1, 4, i) statistics-release-2.1.2/inst/Distribution_Functions/cauchyrnd.m000066400000000000000000000156021527311104300251470ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} cauchyrnd (@var{x0}, @var{gamma}) ## @deftypefnx {statistics} {@var{r} =} cauchyrnd (@var{x0}, @var{gamma}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} cauchyrnd (@var{x0}, @var{gamma}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} cauchyrnd (@var{x0}, @var{gamma}, [@var{sz}]) ## ## Random arrays from the Cauchy distribution. ## ## @code{@var{r} = cauchyrnd (@var{x0}, @var{gamma})} returns an array of ## random numbers chosen from the Cauchy distribution with location parameter ## @var{x0} and scale parameter @var{gamma}. The size of @var{r} is the common ## size of @var{x0} and @var{gamma}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## When called with a single size argument, @code{cauchyrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the Cauchy distribution can be found at ## @url{https://en.wikipedia.org/wiki/Cauchy_distribution} ## ## @seealso{cauchycdf, cauchyinv, cauchypdf} ## @end deftypefn function r = cauchyrnd (x0, gamma, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("cauchyrnd: function called with too few input arguments."); endif ## Check for common size of X0 and GAMMA if (! isscalar (x0) || ! isscalar (gamma)) [retval, x0, gamma] = common_size (x0, gamma); if (retval > 0) error ("cauchyrnd: X0 and GAMMA must be of common size or scalars."); endif endif ## Check for X0 and GAMMA being reals if (iscomplex (x0) || iscomplex (gamma)) error ("cauchyrnd: X0 and GAMMA must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (x0); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("cauchyrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("cauchyrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (x0) && ! isequal (size (x0), size (ones (sz)))) error ("cauchyrnd: X0 and GAMMA must be scalars or of size SZ."); endif ## Check for class type if (isa (x0, 'single') || isa (gamma, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from Cauchy distribution if (isscalar (x0) && isscalar (gamma)) if (! isinf (x0) && (gamma > 0) && (gamma < Inf)) r = x0 - cot (pi * rand (sz, cls)) * gamma; else r = NaN (sz, cls); endif else r = NaN (sz, cls); k = ! isinf (x0) & (gamma > 0) & (gamma < Inf); r(k) = x0(k)(:) - cot (pi * rand (sum (k(:)), 1, cls)) .* gamma(k)(:); endif endfunction ## Test output %!assert_equal (size (cauchyrnd (1, 1)), [1 1]) %!assert_equal (size (cauchyrnd (1, ones (2,1))), [2, 1]) %!assert_equal (size (cauchyrnd (1, ones (2,2))), [2, 2]) %!assert_equal (size (cauchyrnd (ones (2,1), 1)), [2, 1]) %!assert_equal (size (cauchyrnd (ones (2,2), 1)), [2, 2]) %!assert_equal (size (cauchyrnd (1, 1, 3)), [3, 3]) %!assert_equal (size (cauchyrnd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (cauchyrnd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (cauchyrnd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (cauchyrnd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (cauchyrnd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (cauchyrnd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (cauchyrnd (1, 1, [])), [0, 0]) %!assert_equal (size (cauchyrnd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (cauchyrnd (1, 2, -1)), [0, 0]) %!assert_equal (size (cauchyrnd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (cauchyrnd (1, 2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (cauchyrnd (1, 1)), "double") %!assert_equal (class (cauchyrnd (1, single (1))), "single") %!assert_equal (class (cauchyrnd (1, single ([1, 1]))), "single") %!assert_equal (class (cauchyrnd (single (1), 1)), "single") %!assert_equal (class (cauchyrnd (single ([1, 1]), 1)), "single") ## Test input validation %!error cauchyrnd () %!error cauchyrnd (1) %!error ... %! cauchyrnd (ones (3), ones (2)) %!error ... %! cauchyrnd (ones (2), ones (3)) %!error cauchyrnd (i, 2, 3) %!error cauchyrnd (1, i, 3) %!error ... %! cauchyrnd (1, 2, 1.2) %!error ... %! cauchyrnd (1, 2, ones (2)) %!error ... %! cauchyrnd (1, 2, [2 0 2.5]) %!error ... %! cauchyrnd (1, 2, 2, 1.5, 5) %!error ... %! cauchyrnd (2, ones (2), 3) %!error ... %! cauchyrnd (2, ones (2), [3, 2]) %!error ... %! cauchyrnd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/chi2cdf.m000066400000000000000000000125271527311104300244740ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} chi2cdf (@var{x}, @var{df}) ## @deftypefnx {statistics} {@var{p} =} chi2cdf (@var{x}, @var{df}, @qcode{'upper'}) ## ## Chi-squared cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the chi-squared distribution with @var{df} degrees of freedom. The ## chi-squared density function with @var{df} degrees of freedom is the same as ## a gamma density function with parameters @qcode{@var{df}/2} and @qcode{2}. ## ## The size of @var{p} is the common size of @var{x} and @var{df}. A scalar ## input functions as a constant matrix of the same size as the other input. ## ## @code{@var{p} = chi2cdf (@var{x}, @var{df}, "upper")} computes the upper tail ## probability of the chi-squared distribution with @var{df} degrees of freedom, ## at the values in @var{x}. ## ## Further information about the chi-squared distribution can be found at ## @url{https://en.wikipedia.org/wiki/Chi-squared_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{chi2inv, chi2pdf, chi2rnd, chi2stat} ## @end deftypefn function p = chi2cdf (x, df, uflag) ## Check for valid number of input arguments if (nargin < 2) error ("chi2cdf: function called with too few input arguments."); endif ## Check for valid "upper" flag if (nargin > 2) if (! strcmpi (uflag, 'upper')) error ("chi2cdf: invalid argument for upper tail."); endif else uflag = []; endif ## Check for common size of X and DF if (! isscalar (x) || ! isscalar (df)) [err, x, df] = common_size (x, df); if (err > 0) error ("chi2cdf: X and DF must be of common size or scalars."); endif endif ## Check for X and DF being double or single if (! (isfloat (x) && isfloat (df))) error ("chi2cdf: X and DF must be double or single."); endif ## Check for X and DF being reals if (iscomplex (x) || iscomplex (df)) error ("chi2cdf: X and DF must not be complex."); endif ## Compute chi-squared CDF p = gamcdf (x, df/2, 2, uflag); endfunction %!demo %! ## Plot various CDFs from the chi-squared distribution %! x = 0:0.01:8; %! p1 = chi2cdf (x, 1); %! p2 = chi2cdf (x, 2); %! p3 = chi2cdf (x, 3); %! p4 = chi2cdf (x, 4); %! p5 = chi2cdf (x, 6); %! p6 = chi2cdf (x, 9); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', ... %! x, p4, '-c', x, p5, '-m', x, p6, '-y') %! grid on %! xlim ([0, 8]) %! legend ({'df = 1', 'df = 2', 'df = 3', ... %! 'df = 4', 'df = 6', 'df = 9'}, 'location', 'southeast') %! title ('Chi-squared CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, p, u %! x = [-1, 0, 0.5, 1, 2]; %! p = [0, (1 - exp (-x(2:end) / 2))]; %! u = [1, 0, NaN, 0.606530659712633, 0.367879441171442]; %!assert_equal (chi2cdf (x, 2 * ones (1,5)), p, eps) %!assert_equal (chi2cdf (x, 2), p, eps) %!assert_equal (chi2cdf (x, 2 * [1, 0, NaN, 1, 1]), [0, 1, NaN, p(4:5)], eps) %!assert_equal (chi2cdf (x, 2 * [1, 0, NaN, 1, 1], 'upper'), u, 3 * eps) %!assert_equal (chi2cdf ([x(1:2), NaN, x(4:5)], 2), [p(1:2), NaN, p(4:5)], eps) ## Test class of input preserved %!assert_equal (chi2cdf ([x, NaN], 2), [p, NaN], eps) %!assert_equal (chi2cdf (single ([x, NaN]), 2), single ([p, NaN]), eps ('single')) %!assert_equal (chi2cdf ([x, NaN], single (2)), single ([p, NaN]), eps ('single')) ## Test input validation %!error chi2cdf () %!error chi2cdf (1) %!error chi2cdf (1, 2, 3, 4) %!error chi2cdf (1, 2, 3) %!error chi2cdf (1, 2, 'uper') %!error ... %! chi2cdf (ones (3), ones (2)) %!error ... %! chi2cdf (ones (2), ones (3)) %!error chi2cdf (int32 (2), 2) %!error chi2cdf (true, 2) %!error chi2cdf ('a', 2) %!error chi2cdf (i, 2) %!error chi2cdf (2, i) statistics-release-2.1.2/inst/Distribution_Functions/chi2inv.m000066400000000000000000000107721527311104300245340ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} chi2inv (@var{p}, @var{df}) ## ## Inverse of the chi-squared cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the chi-squared distribution with @var{df} degrees of freedom. The size of ## @var{x} is the common size of @var{p} and @var{df}. A scalar input functions ## as a constant matrix of the same size as the other inputs. ## ## Further information about the chi-squared distribution can be found at ## @url{https://en.wikipedia.org/wiki/Chi-squared_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{chi2cdf, chi2pdf, chi2rnd, chi2stat} ## @end deftypefn function x = chi2inv (p, df) ## Check for valid number of input arguments if (nargin < 2) error ("chi2inv: function called with too few input arguments."); endif ## Check for common size of P and DF if (! isscalar (p) || ! isscalar (df)) [retval, p, df] = common_size (p, df); if (retval > 0) error ("chi2inv: P and DF must be of common size or scalars."); endif endif ## Check for P and DF being double or single if (! (isfloat (p) && isfloat (df))) error ("chi2inv: P and DF must be double or single."); endif ## Check for P and DF being reals if (iscomplex (p) || iscomplex (df)) error ("chi2inv: P and DF must not be complex."); endif ## Compute chi-squared iCDF x = gaminv (p, df/2, 2); endfunction %!demo %! ## Plot various iCDFs from the chi-squared distribution %! p = 0.001:0.001:0.999; %! x1 = chi2inv (p, 1); %! x2 = chi2inv (p, 2); %! x3 = chi2inv (p, 3); %! x4 = chi2inv (p, 4); %! x5 = chi2inv (p, 6); %! x6 = chi2inv (p, 9); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', ... %! p, x4, '-c', p, x5, '-m', p, x6, '-y') %! grid on %! ylim ([0, 8]) %! legend ({'df = 1', 'df = 2', 'df = 3', ... %! 'df = 4', 'df = 6', 'df = 9'}, 'location', 'northwest') %! title ('Chi-squared iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p %! p = [-1 0 0.3934693402873666 1 2]; %!assert_equal (chi2inv (p, 2*ones (1,5)), [NaN 0 1 Inf NaN], 5*eps) %!assert_equal (chi2inv (p, 2), [NaN 0 1 Inf NaN], 5*eps) %!assert_equal (chi2inv (p, 2*[0 1 NaN 1 1]), [NaN 0 NaN Inf NaN], 5*eps) %!assert_equal (chi2inv ([p(1:2) NaN p(4:5)], 2), [NaN 0 NaN Inf NaN], 5*eps) ## Test class of input preserved %!assert_equal (chi2inv ([p, NaN], 2), [NaN 0 1 Inf NaN NaN], 5*eps) %!assert_equal (chi2inv (single ([p, NaN]), 2), single ([NaN 0 1 Inf NaN NaN]), 5*eps ('single')) %!assert_equal (chi2inv ([p, NaN], single (2)), single ([NaN 0 1 Inf NaN NaN]), 5*eps ('single')) ## Test input validation %!error chi2inv () %!error chi2inv (1) %!error chi2inv (1,2,3) %!error ... %! chi2inv (ones (3), ones (2)) %!error ... %! chi2inv (ones (2), ones (3)) %!error chi2inv (int32 (2), 2) %!error chi2inv (true, 2) %!error chi2inv ('a', 2) %!error chi2inv (i, 2) %!error chi2inv (2, i) statistics-release-2.1.2/inst/Distribution_Functions/chi2pdf.m000066400000000000000000000105651527311104300245110ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} chi2pdf (@var{x}, @var{df}) ## ## Chi-squared probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the chi-squared distribution with @var{df} degrees of freedom. The size ## of @var{y} is the common size of @var{x} and @var{df}. A scalar input ## functions as a constant matrix of the same size as the other inputs. ## ## Further information about the chi-squared distribution can be found at ## @url{https://en.wikipedia.org/wiki/Chi-squared_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{chi2cdf, chi2inv, chi2rnd, chi2stat} ## @end deftypefn function y = chi2pdf (x, df) ## Check for valid number of input arguments if (nargin < 2) error ("chi2pdf: function called with too few input arguments."); endif ## Check for common size of X and DF if (! isscalar (x) || ! isscalar (df)) [retval, x, df] = common_size (x, df); if (retval > 0) error ("chi2pdf: X and DF must be of common size or scalars."); endif endif ## Check for X and DF being double or single if (! (isfloat (x) && isfloat (df))) error ("chi2pdf: X and DF must be double or single."); endif ## Check for X and DF being reals if (iscomplex (x) || iscomplex (df)) error ("chi2pdf: X and DF must not be complex."); endif ## Compute chi-squared PDF y = gampdf (x, df/2, 2); endfunction %!demo %! ## Plot various PDFs from the chi-squared distribution %! x = 0:0.01:8; %! y1 = chi2pdf (x, 1); %! y2 = chi2pdf (x, 2); %! y3 = chi2pdf (x, 3); %! y4 = chi2pdf (x, 4); %! y5 = chi2pdf (x, 6); %! y6 = chi2pdf (x, 9); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', ... %! x, y4, '-c', x, y5, '-m', x, y6, '-y') %! grid on %! xlim ([0, 8]) %! ylim ([0, 0.5]) %! legend ({'df = 1', 'df = 2', 'df = 3', ... %! 'df = 4', 'df = 6', 'df = 9'}, 'location', 'northeast') %! title ('Chi-squared PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x, y %! x = [-1 0 0.5 1 Inf]; %! y = [0, 1/2 * exp(-x(2:5)/2)]; %!assert_equal (chi2pdf (x, 2*ones (1,5)), y) %!assert_equal (chi2pdf (x, 2), y) %!assert_equal (chi2pdf (x, 2*[1 0 NaN 1 1]), [y(1) NaN NaN y(4:5)]) %!assert_equal (chi2pdf ([x, NaN], 2), [y, NaN]) ## Test for issue #203 (Github) %!assert_equal (chi2pdf (2, Inf), 0) ## Test class of input preserved %!assert_equal (chi2pdf (single ([x, NaN]), 2), single ([y, NaN])) %!assert_equal (chi2pdf ([x, NaN], single (2)), single ([y, NaN])) ## Test input validation %!error chi2pdf () %!error chi2pdf (1) %!error chi2pdf (1,2,3) %!error ... %! chi2pdf (ones (3), ones (2)) %!error ... %! chi2pdf (ones (2), ones (3)) %!error chi2pdf (int32 (2), 2) %!error chi2pdf (true, 2) %!error chi2pdf ('a', 2) %!error chi2pdf (i, 2) %!error chi2pdf (2, i) statistics-release-2.1.2/inst/Distribution_Functions/chi2rnd.m000066400000000000000000000131231527311104300245140ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} chi2rnd (@var{df}) ## @deftypefnx {statistics} {@var{r} =} chi2rnd (@var{df}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} chi2rnd (@var{df}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} chi2rnd (@var{df}, [@var{sz}]) ## ## Random arrays from the chi-squared distribution. ## ## @code{@var{r} = chi2rnd (@var{df})} returns an array of random numbers chosen ## from the chi-squared distribution with @var{df} degrees of freedom. The size ## of @var{r} is the size of @var{df}. ## ## When called with a single size argument, @code{chi2rnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the chi-squared distribution can be found at ## @url{https://en.wikipedia.org/wiki/Chi-squared_distribution} ## ## @seealso{chi2cdf, chi2inv, chi2pdf, chi2stat} ## @end deftypefn function r = chi2rnd (df, varargin) ## Check for valid number of input arguments if (nargin < 1) error ("chi2rnd: function called with too few input arguments."); endif ## Check for DF being reals if (iscomplex (df)) error ("chi2rnd: DF must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 1) sz = size (df); elseif (nargin == 2) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("chi2rnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 2) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("chi2rnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameter match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (df) && ! isequal (size (df), size (ones (sz)))) error ("chi2rnd: DF must be scalar or of size SZ."); endif ## Check for class type if (isa (df, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from chi-squared distribution if (isscalar (df)) if ((df > 0) && (df < Inf)) r = 2 * randg (df/2, sz, cls); else r = NaN (sz, cls); endif else r = NaN (sz, cls); k = (df > 0) | (df < Inf); r(k) = 2 * randg (df(k)/2, cls); endif endfunction ## Test output %!assert_equal (size (chi2rnd (2)), [1, 1]) %!assert_equal (size (chi2rnd (ones (2,1))), [2, 1]) %!assert_equal (size (chi2rnd (ones (2,2))), [2, 2]) %!assert_equal (size (chi2rnd (1, 3)), [3, 3]) %!assert_equal (size (chi2rnd (1, [4, 1])), [4, 1]) %!assert_equal (size (chi2rnd (1, 4, 1)), [4, 1]) %!assert_equal (size (chi2rnd (1, 4, 1)), [4, 1]) %!assert_equal (size (chi2rnd (1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (chi2rnd (1, 0, 1)), [0, 1]) %!assert_equal (size (chi2rnd (1, 1, 0)), [1, 0]) %!assert_equal (size (chi2rnd (1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (chi2rnd (1, [])), [0, 0]) %!assert_equal (size (chi2rnd (1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (chi2rnd (1, -1)), [0, 0]) %!assert_equal (size (chi2rnd (1, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (chi2rnd (1, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (chi2rnd (2)), "double") %!assert_equal (class (chi2rnd (single (2))), "single") %!assert_equal (class (chi2rnd (single ([2 2]))), "single") ## Test input validation %!error chi2rnd () %!error chi2rnd (i) %!error ... %! chi2rnd (1, 1.2) %!error ... %! chi2rnd (1, ones (2)) %!error ... %! chi2rnd (1, [2 0 2.5]) %!error ... %! chi2rnd (ones (2), ones (2)) %!error ... %! chi2rnd (1, 2, 1.5, 5) %!error chi2rnd (ones (2,2), 3) %!error chi2rnd (ones (2,2), [3, 2]) %!error chi2rnd (ones (2,2), 2, 3) statistics-release-2.1.2/inst/Distribution_Functions/copulacdf.m000066400000000000000000000242311527311104300251250ustar00rootroot00000000000000## Copyright (C) 2008 Arno Onken ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} copulacdf (@var{family}, @var{x}, @var{theta}) ## @deftypefnx {statistics} {@var{p} =} copulacdf ('t', @var{x}, @var{theta}, @var{df}) ## ## Copula family cumulative distribution functions (CDF). ## ## @subheading Arguments ## ## @itemize @bullet ## @item ## @var{family} is the copula family name. Currently, @var{family} can ## be @code{'Gaussian'} for the Gaussian family, @code{'t'} for the ## Student's t family, @code{'Clayton'} for the Clayton family, ## @code{'Gumbel'} for the Gumbel-Hougaard family, @code{'Frank'} for ## the Frank family, @code{'AMH'} for the Ali-Mikhail-Haq family, or ## @code{'FGM'} for the Farlie-Gumbel-Morgenstern family. ## ## @item ## @var{x} is the support where each row corresponds to an observation. ## ## @item ## @var{theta} is the parameter of the copula. For the Gaussian and ## Student's t copula, @var{theta} must be a correlation matrix. For ## bivariate copulas @var{theta} can also be a correlation coefficient. ## For the Clayton family, the Gumbel-Hougaard family, the Frank family, ## and the Ali-Mikhail-Haq family, @var{theta} must be a vector with the ## same number of elements as observations in @var{x} or be scalar. For ## the Farlie-Gumbel-Morgenstern family, @var{theta} must be a matrix of ## coefficients for the Farlie-Gumbel-Morgenstern polynomial where each ## row corresponds to one set of coefficients for an observation in ## @var{x}. A single row is expanded. The coefficients are in binary ## order. ## ## @item ## @var{df} is the degrees of freedom for the Student's t family. ## @var{df} must be a vector with the same number of elements as ## observations in @var{x} or be scalar. ## @end itemize ## ## @subheading Return values ## ## @itemize @bullet ## @item ## @var{p} is the cumulative distribution of the copula at each row of ## @var{x} and corresponding parameter @var{theta}. ## @end itemize ## ## @subheading Examples ## ## @example ## @group ## x = [0.2:0.2:0.6; 0.2:0.2:0.6]; ## theta = [1; 2]; ## p = copulacdf ("Clayton", x, theta) ## @end group ## ## @group ## x = [0.2:0.2:0.6; 0.2:0.1:0.4]; ## theta = [0.2, 0.1, 0.1, 0.05]; ## p = copulacdf ("FGM", x, theta) ## @end group ## @end example ## ## @subheading References ## ## @enumerate ## @item ## Roger B. Nelsen. @cite{An Introduction to Copulas}. Springer, ## New York, second edition, 2006. ## @end enumerate ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{copulapdf, copularnd} ## @end deftypefn function p = copulacdf (family, x, theta, df) ## Check arguments if (nargin != 3 && (nargin != 4 || ! strcmpi (family, 't'))) print_usage (); endif if (! ischar (family)) error (strcat ("copulacdf: family must be one of 'Gaussian',", ... " 't', 'Clayton', 'Gumbel', 'Frank', 'AMH', and 'FGM'.")); endif ## Check for X and THETA being double or single if (! (isfloat (x) && isfloat (theta))) error ("copulacdf: X and THETA must be double or single."); endif if (! isempty (x) && ! ismatrix (x)) error ("copulacdf: X must be a numeric matrix."); endif [n, d] = size (x); lower_family = lower (family); ## Check family and copula parameters switch (lower_family) case {'gaussian', 't'} ## Family with a covariance matrix if (d == 2 && isscalar (theta)) ## Expand a scalar to a correlation matrix theta = [1, theta; theta, 1]; endif if (any (size (theta) != [d, d]) || any (diag (theta) != 1) || ... any (any (theta != theta')) || min (eig (theta)) <= 0) error ("copulacdf: THETA must be a correlation matrix."); endif if (nargin == 4) ## Student's t family if (! isscalar (df) && (! isvector (df) || length (df) != n)) error (strcat ("copulacdf: DF must be a vector with the same", ... " number of rows as X or be scalar.")); endif df = df(:); endif case {'clayton', 'gumbel', 'frank', 'amh'} ## Archimedian one parameter family if (! isvector (theta) || (! isscalar (theta) && length (theta) != n)) error (strcat ("copulacdf: THETA must be a vector with the same", ... " number of rows as X or be scalar.")); endif theta = theta(:); if (n > 1 && isscalar (theta)) theta = repmat (theta, n, 1); endif case {'fgm'} ## Exponential number of parameters if (! ismatrix (theta) || size (theta, 2) != (2 .^ d - d - 1) || ... (size (theta, 1) != 1 && size (theta, 1) != n)) error (strcat ("copulacdf: THETA must be a row vector of length", ... " 2^d-d-1 or a matrix of size N x (2^d-d-1).")); endif if (n > 1 && size (theta, 1) == 1) theta = repmat (theta, n, 1); endif otherwise error ("copulacdf: unknown copula family '%s'.", family); endswitch if (n == 0) ## Input is empty p = zeros (0, 1); else ## Truncate input to unit hypercube x(x < 0) = 0; x(x > 1) = 1; ## Compute the cumulative distribution function according to family switch (lower_family) case {'gaussian'} ## The Gaussian family p = mvncdf (norminv (x), zeros (1, d), theta); ## No parameter bounds check k = []; case {'t'} ## The Student's t family p = mvtcdf (tinv (x, df), theta, df); ## No parameter bounds check k = []; case {'clayton'} ## The Clayton family p = exp (-log (max (sum (x .^ (repmat (-theta, 1, d)), 2) ... - d + 1, 0)) ./ theta); ## Product copula at columns where theta == 0 k = find (theta == 0); if (any (k)) p(k) = prod (x(k, :), 2); endif ## Check bounds if (d > 2) k = find (! (theta >= 0) | ! (theta < inf)); else k = find (! (theta >= -1) | ! (theta < inf)); endif case {'gumbel'} ## The Gumbel-Hougaard family p = exp (-(sum ((-log (x)) .^ repmat (theta, 1, d), 2)) ... .^ (1 ./ theta)); ## Check bounds k = find (! (theta >= 1) | ! (theta < inf)); case {'frank'} ## The Frank family p = -log (1 + (prod (expm1 (repmat (-theta, 1, d) .* x), 2)) ./ ... (expm1 (-theta) .^ (d - 1))) ./ theta; ## Product copula at columns where theta == 0 k = find (theta == 0); if (any (k)) p(k) = prod (x(k, :), 2); endif ## Check bounds if (d > 2) k = find (! (theta > 0) | ! (theta < inf)); else k = find (! (theta > -inf) | ! (theta < inf)); endif case {'amh'} ## The Ali-Mikhail-Haq family p = (theta - 1) ./ (theta - prod ((1 + repmat (theta, 1, d) ... .* (x - 1)) ./ x, 2)); ## Check bounds if (d > 2) k = find (! (theta >= 0) | ! (theta < 1)); else k = find (! (theta >= -1) | ! (theta < 1)); endif case {'fgm'} ## The Farlie-Gumbel-Morgenstern family ## All binary combinations bcomb = logical (floor (mod (((0:(2 .^ d - 1))' * 2 .^ ... ((1 - d):0)), 2))); ecomb = ones (size (bcomb)); ecomb(bcomb) = -1; ## Summation over all combinations of order >= 2 bcomb = bcomb(sum (bcomb, 2) >= 2, end:-1:1); ## Linear constraints matrix ac = zeros (size (ecomb, 1), size (bcomb, 1)); ## Matrix to compute p ap = zeros (size (x, 1), size (bcomb, 1)); for i = 1:size (bcomb, 1) ac(:, i) = -prod (ecomb(:, bcomb(i, :)), 2); ap(:, i) = prod (1 - x(:, bcomb(i, :)), 2); endfor p = prod (x, 2) .* (1 + sum (ap .* theta, 2)); ## Check linear constraints k = false (n, 1); for i = 1:n k(i) = any (ac * theta(i, :)' > 1); endfor endswitch ## Out of bounds parameters if (any (k)) p(k) = NaN; endif endif endfunction ## Test output %!test %! x = [0.2:0.2:0.6; 0.2:0.2:0.6]; %! theta = [1; 2]; %! p = copulacdf ('Clayton', x, theta); %! expected_p = [0.1395; 0.1767]; %! assert_equal (p, expected_p, 0.001); %!test %! x = [0.2:0.2:0.6; 0.2:0.2:0.6]; %! p = copulacdf ('Gumbel', x, 2); %! expected_p = [0.1464; 0.1464]; %! assert_equal (p, expected_p, 0.001); %!test %! x = [0.2:0.2:0.6; 0.2:0.2:0.6]; %! theta = [1; 2]; %! p = copulacdf ('Frank', x, theta); %! expected_p = [0.0699; 0.0930]; %! assert_equal (p, expected_p, 0.001); %!test %! x = [0.2:0.2:0.6; 0.2:0.2:0.6]; %! theta = [0.3; 0.7]; %! p = copulacdf ('AMH', x, theta); %! expected_p = [0.0629; 0.0959]; %! assert_equal (p, expected_p, 0.001); %!test %! x = [0.2:0.2:0.6; 0.2:0.1:0.4]; %! theta = [0.2, 0.1, 0.1, 0.05]; %! p = copulacdf ('FGM', x, theta); %! expected_p = [0.0558; 0.0293]; %! assert_equal (p, expected_p, 0.001); ## Test input validation %!error copulacdf ('Clayton', int32 ([0, 0]), 2) %!error copulacdf ('Clayton', [true, true], 2) %!error copulacdf ('Clayton', 'ab', 2) statistics-release-2.1.2/inst/Distribution_Functions/copulapdf.m000066400000000000000000000356201527311104300251460ustar00rootroot00000000000000## Copyright (C) 2008 Arno Onken ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} copulapdf (@var{family}, @var{x}, @var{theta}) ## @deftypefnx {statistics} {@var{y} =} copulapdf ('t', @var{x}, @var{theta}, @var{df}) ## ## Copula family probability density functions (PDF). ## ## @subheading Arguments ## ## @itemize @bullet ## @item ## @var{family} is the copula family name. Currently, @var{family} can ## be @code{'Gaussian'} for the Gaussian family, @code{'t'} for the ## Student's t family, @code{'Clayton'} for the Clayton family, ## @code{'Gumbel'} for the Gumbel-Hougaard family, @code{'Frank'} for the ## Frank family, @code{'AMH'} for the Ali-Mikhail-Haq family, or ## @code{'FGM'} for the Farlie-Gumbel-Morgenstern family. The last two are ## Octave extensions that MATLAB does not provide. ## ## @item ## @var{x} is the support where each row corresponds to an observation. ## ## @item ## @var{theta} is the parameter of the copula. For the Gaussian and ## Student's t families it is the linear correlation matrix, and a scalar ## is expanded to a bivariate one. For the remaining families the elements ## of @var{theta} must be greater than or equal to @code{-1} for the ## Clayton family, greater than or equal to @code{1} for the ## Gumbel-Hougaard family, arbitrary for the Frank family, and greater ## than or equal to @code{-1} and lower than @code{1} for the ## Ali-Mikhail-Haq family. Moreover, @var{theta} must be non-negative ## for dimensions greater than @code{2}. @var{theta} must be a column ## vector with the same number of rows as @var{x} or be scalar. The ## Farlie-Gumbel-Morgenstern family instead takes one parameter for every ## subset of the variables of order two or more, so @var{theta} is a row ## vector of length @code{2^d-d-1} or a matrix with one such row per ## observation; parameter sets violating the family's linear constraints ## give @code{NaN}. ## ## @item ## @var{df} is the degrees of freedom of the Student's t family, and is ## required by it. It must be a vector with the same number of rows as ## @var{x} or be scalar. ## @end itemize ## ## @subheading Return values ## ## @itemize @bullet ## @item ## @var{y} is the probability density of the copula at each row of ## @var{x} and corresponding parameter @var{theta}. ## @end itemize ## ## @subheading Examples ## ## @example ## @group ## x = [0.2:0.2:0.6; 0.2:0.2:0.6]; ## theta = [1; 2]; ## y = copulapdf ("Clayton", x, theta) ## @end group ## ## @group ## y = copulapdf ("Gumbel", x, 2) ## @end group ## @end example ## ## @subheading References ## ## @enumerate ## @item ## Roger B. Nelsen. @cite{An Introduction to Copulas}. Springer, ## New York, second edition, 2006. ## @end enumerate ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{copulacdf, copularnd} ## @end deftypefn function y = copulapdf (family, x, theta, df) ## Check arguments if (nargin != 3 && (nargin != 4 || ! strcmpi (family, 't'))) print_usage (); endif if (! ischar (family)) error (strcat ("copulapdf: family must be one of 'Gaussian',", ... " 't', 'Clayton', 'Gumbel', 'Frank', and 'AMH'.")); endif ## Check for X and THETA being double or single if (! (isfloat (x) && isfloat (theta))) error ("copulapdf: X and THETA must be double or single."); endif if (! isempty (x) && ! ismatrix (x)) error ("copulapdf: X must be a numeric matrix."); endif [n, d] = size (x); lower_family = lower (family); ## The Farlie-Gumbel-Morgenstern family carries one parameter per subset of ## the variables of order two or more, as it does in copulacdf. if (strcmp (lower_family, 'fgm')) if (! ismatrix (theta) || size (theta, 2) != (2 .^ d - d - 1) || ... (size (theta, 1) != 1 && size (theta, 1) != n)) error (strcat ("copulapdf: THETA must be a row vector of length", ... " 2^d-d-1 or a matrix of size N x (2^d-d-1).")); endif if (n > 1 && size (theta, 1) == 1) theta = repmat (theta, n, 1); endif endif ## The two elliptical families take a correlation matrix rather than a ## one-parameter THETA, and are validated the way copulacdf validates them. is_elliptical = any (strcmp (lower_family, {'gaussian', 't'})); if (is_elliptical) if (d == 2 && isscalar (theta)) ## Expand a scalar to a correlation matrix theta = [1, theta; theta, 1]; endif if (any (size (theta) != [d, d]) || any (diag (theta) != 1) || ... any (any (theta != theta')) || min (eig (theta)) <= 0) error ("copulapdf: THETA must be a correlation matrix."); endif if (nargin == 4) if (! isscalar (df) && (! isvector (df) || length (df) != n)) error (strcat ("copulapdf: DF must be a vector with the same", ... " number of rows as X or be scalar.")); endif df = df(:); endif elseif (! strcmp (lower_family, 'fgm') && (! isvector (theta) ... || (! isscalar (theta) && size (theta, 1) != n))) error (strcat ("copulapdf: THETA must be a column vector with the", ... " same number of rows as X or be scalar.")); endif if (n == 0) ## Input is empty y = zeros (0, 1); else if (n > 1 && isscalar (theta) && ! is_elliptical ... && ! strcmp (lower_family, 'fgm')) theta = repmat (theta, n, 1); endif ## Truncate input to unit hypercube x(x < 0) = 0; x(x > 1) = 1; ## Compute the density according to family lowerarg = lower_family; if (strcmp (lowerarg, 'gaussian')) ## The Gaussian family: the density of the correlated normal relative to ## the independent one, at the normal quantiles of X. z = norminv (x); y = exp (-0.5 * sum ((z * (inv (theta) - eye (d))) .* z, 2)) ... ./ sqrt (det (theta)); ## No parameter bounds check k = []; elseif (strcmp (lowerarg, 't')) ## The Student's t family: the multivariate t density at the t quantiles ## of X, divided by the univariate ones it would factor into. if (nargin < 4) error ("copulapdf: DF is required for the 't' copula family."); endif z = tinv (x, df); y = mvtpdf (z, theta, df) ./ prod (tpdf (z, df), 2); ## No parameter bounds check k = []; elseif (strcmp (lowerarg, 'clayton')) ## The Clayton family log_cdf = -log (max (sum (x .^ (repmat (-theta, 1, d)), 2) ... - d + 1, 0)) ./ theta; y = prod (repmat (theta, 1, d) .* repmat (0:(d - 1), n, 1) + 1, 2) ... .* exp ((1 + theta .* d) .* log_cdf - ... (theta + 1) .* sum (log (x), 2)); ## Product copula at columns where theta == 0 k = find (theta == 0); if (any (k)) y(k) = 1; endif ## Check theta if (d > 2) k = find (! (theta >= 0) | ! (theta < inf)); else k = find (! (theta >= -1) | ! (theta < inf)); endif elseif (strcmp (lowerarg, 'gumbel')) ## The Gumbel-Hougaard family g = sum ((-log (x)) .^ repmat (theta, 1, d), 2); c = exp (-g .^ (1 ./ theta)); y = ((prod (-log (x), 2)) .^ (theta - 1)) ./ prod (x, 2) .* c .* ... (g .^ (2 ./ theta - 2) + (theta - 1) .* g .^ (1 ./ theta - 2)); ## Check theta k = find (! (theta >= 1) | ! (theta < inf)); elseif (strcmp (lowerarg, 'frank')) ## The Frank family if (d != 2) error ("copulapdf: Frank copula PDF implemented as bivariate only."); endif y = (theta .* exp (theta .* (1 + sum (x, 2))) .* (exp (theta) - 1)) ./ ... (exp (theta) - exp (theta + theta .* x(:, 1)) + ... exp (theta .* sum (x, 2)) - exp (theta + theta .* x(:, 2))) .^ 2; ## Product copula at columns where theta == 0 k = find (theta == 0); if (any (k)) y(k) = 1; endif ## Check theta k = find (! (theta > -inf) | ! (theta < inf)); elseif (strcmp (lowerarg, 'amh')) ## The Ali-Mikhail-Haq family if (d != 2) error (strcat ("copulapdf: Ali-Mikhail-Haq copula PDF", ... " implemented as bivariate only.")); endif z = theta .* prod (x - 1, 2) - 1; y = (theta .* (1 - sum (x, 2) - prod (x, 2) - z) - 1) ./ (z .^ 3); ## Check theta k = find (! (theta >= -1) | ! (theta < 1)); elseif (strcmp (lowerarg, 'fgm')) ## The Farlie-Gumbel-Morgenstern family. Differentiating the ## distribution once in every variable turns each u_i (1 - u_i) factor ## into (1 - 2 u_i) and leaves the leading product at one. bcomb = logical (floor (mod (((0:(2 .^ d - 1))' * 2 .^ ... ((1 - d):0)), 2))); ecomb = ones (size (bcomb)); ecomb(bcomb) = -1; ## Summation over all combinations of order >= 2 bcomb = bcomb(sum (bcomb, 2) >= 2, end:-1:1); ## Linear constraints matrix ac = zeros (size (ecomb, 1), size (bcomb, 1)); ## Matrix to compute y ap = zeros (n, size (bcomb, 1)); for i = 1:size (bcomb, 1) ac(:, i) = -prod (ecomb(:, bcomb(i, :)), 2); ap(:, i) = prod (1 - 2 * x(:, bcomb(i, :)), 2); endfor y = 1 + sum (ap .* theta, 2); ## Check linear constraints k = false (n, 1); for i = 1:n k(i) = any (ac * theta(i, :)' > 1); endfor else error ("copulapdf: unknown copula family '%s'.", family); endif if (any (k)) y(k) = NaN; endif endif endfunction ## Test output %!test %! x = [0.2:0.2:0.6; 0.2:0.2:0.6]; %! theta = [1; 2]; %! y = copulapdf ('Clayton', x, theta); %! expected_p = [0.9872; 0.7295]; %! assert_equal (y, expected_p, 0.001); %!test %! x = [0.2:0.2:0.6; 0.2:0.2:0.6]; %! y = copulapdf ('Gumbel', x, 2); %! expected_p = [0.9468; 0.9468]; %! assert_equal (y, expected_p, 0.001); %!test %! x = [0.2, 0.6; 0.2, 0.6]; %! theta = [1; 2]; %! y = copulapdf ('Frank', x, theta); %! expected_p = [0.9378; 0.8678]; %! assert_equal (y, expected_p, 0.001); %!test %! x = [0.2, 0.6; 0.2, 0.6]; %! theta = [0.3; 0.7]; %! y = copulapdf ('AMH', x, theta); %! expected_p = [0.9540; 0.8577]; %! assert_equal (y, expected_p, 0.001); ## Test input validation %!error copulapdf ('Clayton', int32 ([0, 0]), 2) %!error copulapdf ('Clayton', [true, true], 2) %!error copulapdf ('Clayton', 'ab', 2) ## The Gaussian and Student's t families, verified against MATLAB R2024a. %!test %! x = [0.1, 0.2; 0.3, 0.6; 0.5, 0.4; 0.7, 0.9; 0.45, 0.55]; %! y = copulapdf ('Gaussian', x, 0.5); %! assert_equal (y, [1.60177371945198; 0.998741486235102; 1.14241401106385; ... %! 1.31299420633171; 1.13661012971028], 1e-12); %! y = copulapdf ('Gaussian', x, -0.3); %! assert_equal (y, [0.653989319149703; 1.07700187314878; 1.04496288532534; ... %! 0.76398020459843; 1.05211178116014], 1e-12); %!test # an uncorrelated Gaussian copula is the independence copula %! x = [0.1, 0.2; 0.3, 0.6; 0.5, 0.4; 0.7, 0.9; 0.45, 0.55]; %! assert_equal (copulapdf ('Gaussian', x, 0), ones (5, 1), 1e-12); %!test %! x = [0.1, 0.2; 0.3, 0.6; 0.5, 0.4; 0.7, 0.9; 0.45, 0.55]; %! y = copulapdf ('t', x, 0.5, 5); %! assert_equal (y, [1.66488234707408; 1.00205894407007; 1.24574194276063; ... %! 1.25091343011063; 1.24054754220011], 1e-12); %! y = copulapdf ('t', x, -0.3, 10); %! assert_equal (y, [0.644342340201519; 1.12031977908833; 1.09385787202575; ... %! 0.730314426588563; 1.10518920332876], 1e-12); %!test # both elliptical families take a correlation matrix beyond two columns %! R = [1, 0.4, 0.2; 0.4, 1, 0.3; 0.2, 0.3, 1]; %! x = [0.2, 0.4, 0.6; 0.5, 0.5, 0.5]; %! assert_equal (copulapdf ('Gaussian', x, R), ... %! [1.1162786093147; 1.14859096884849], 1e-12); %! assert_equal (copulapdf ('t', x, R, 8), ... %! [1.19326414606185; 1.37528246652052], 1e-12); %!test # the density integrates to one over the unit square %! g = ((1:40)' - 0.5) / 40; %! [A, B] = meshgrid (g, g); %! x = [A(:), B(:)]; %! assert_equal (sum (copulapdf ('Gaussian', x, 0.5)) / 1600, 1, 1e-3); %! assert_equal (sum (copulapdf ('t', x, 0.5, 6)) / 1600, 1, 1e-2); %!error ... %! copulapdf ('Gaussian', [0.2, 0.4], [1, 2; 2, 1]) %!error ... %! copulapdf ('t', [0.2, 0.4], 0.5) %!error copulapdf ('Gaussian', [0.2, 0.4], 0.5, 5) ## The Farlie-Gumbel-Morgenstern family, an Octave extension. Its density is ## checked against a finite difference of copulacdf, which is independent of it. %!test %! x = [0.35, 0.62]; %! h = 1e-5; %! for theta = [-1, -0.4, 0.7, 1] %! fd = (copulacdf ('FGM', [x(1)+h, x(2)+h], theta) ... %! - copulacdf ('FGM', [x(1)+h, x(2)-h], theta) ... %! - copulacdf ('FGM', [x(1)-h, x(2)+h], theta) ... %! + copulacdf ('FGM', [x(1)-h, x(2)-h], theta)) / (4 * h * h); %! assert_equal (copulapdf ('FGM', x, theta), fd, 1e-6); %! endfor %!test # the bivariate density in closed form %! x = [0.35, 0.62; 0.2, 0.3]; %! theta = 0.7; %! assert_equal (copulapdf ('FGM', x, theta), ... %! 1 + theta * (1 - 2 * x(:,1)) .* (1 - 2 * x(:,2)), 1e-14); %!test # a zero parameter gives the independence copula %! x = [0.35, 0.62; 0.2, 0.3; 0.9, 0.1]; %! assert_equal (copulapdf ('FGM', x, 0), ones (3, 1), 1e-14); %!test # one parameter per subset of order two or more beyond two variables %! x = [0.3, 0.5, 0.7]; %! theta = [0.1, 0.1, 0.1, 0.1]; %! assert_equal (copulapdf ('FGM', x, theta), 0.984, 1e-12); %!test # a parameter set outside the family's linear constraints gives NaN %! assert_equal (copulapdf ('FGM', [0.3, 0.5], 2), NaN); %!test # the density integrates to one over the unit square %! g = ((1:60)' - 0.5) / 60; %! [A, B] = meshgrid (g, g); %! x = [A(:), B(:)]; %! assert_equal (sum (copulapdf ('FGM', x, 0.7)) / 3600, 1, 1e-10); %!error ... %! copulapdf ('FGM', [0.3, 0.5, 0.7], [0.1, 0.1]) statistics-release-2.1.2/inst/Distribution_Functions/copularnd.m000066400000000000000000000363331527311104300251620ustar00rootroot00000000000000## Copyright (C) 2012 Arno Onken ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} copularnd (@var{family}, @var{theta}, @var{n}) ## @deftypefnx {statistics} {@var{r} =} copularnd (@var{family}, @var{theta}, @var{n}, @var{d}) ## @deftypefnx {statistics} {@var{r} =} copularnd ('t', @var{theta}, @var{df}, @var{n}) ## ## Random arrays from the copula family distributions. ## ## @subheading Arguments ## ## @itemize @bullet ## @item ## @var{family} is the copula family name. Currently, @var{family} can be ## @code{'Gaussian'} for the Gaussian family, @code{'t'} for the Student's t ## family, @code{'Clayton'} for the Clayton family, @code{'Frank'} for the ## Frank family, @code{'Gumbel'} for the Gumbel-Hougaard family, @code{'AMH'} ## for the Ali-Mikhail-Haq family, or @code{'FGM'} for the ## Farlie-Gumbel-Morgenstern family. The last two are Octave extensions that ## MATLAB does not provide. Every family but Clayton is generated as ## bivariate only. ## ## @item ## @var{theta} is the parameter of the copula. For the Gaussian and Student's t ## copula, @var{theta} must be a correlation matrix. For bivariate copulas ## @var{theta} can also be a correlation coefficient. For the Clayton, Frank ## and Gumbel-Hougaard families, @var{theta} must be a vector with the same ## number of elements as samples to be generated or be scalar. Values outside ## a family's range give @code{NaN} rows: at or above @code{1} for the ## Gumbel-Hougaard family, at or above @code{-1} for the bivariate Clayton ## family, and any finite value for the Frank family. The Ali-Mikhail-Haq ## family takes @var{theta} in @code{[-1, 1)} and the ## Farlie-Gumbel-Morgenstern family in @code{[-1, 1]}. ## ## @item ## @var{df} is the degrees of freedom for the Student's t family. @var{df} must ## be a vector with the same number of elements as samples to be generated or ## be scalar. ## ## @item ## @var{n} is the number of rows of the matrix to be generated. @var{n} must be ## a non-negative integer and corresponds to the number of samples to be ## generated. ## ## @item ## @var{d} is the number of columns of the matrix to be generated. @var{d} must ## be a positive integer and corresponds to the dimension of the copula. ## @end itemize ## ## @subheading Return values ## ## @itemize @bullet ## @item ## @var{r} is a matrix of random samples from the copula with @var{n} samples ## of distribution dimension @var{d}. ## @end itemize ## ## @subheading Examples ## ## @example ## @group ## theta = 0.5; ## r = copularnd ("Gaussian", theta); ## @end group ## ## @group ## theta = 0.5; ## df = 2; ## r = copularnd ("t", theta, df); ## @end group ## ## @group ## theta = 0.5; ## n = 2; ## r = copularnd ("Clayton", theta, n); ## @end group ## @end example ## ## @subheading References ## ## @enumerate ## @item ## Roger B. Nelsen. @cite{An Introduction to Copulas}. Springer, New York, ## second edition, 2006. ## @end enumerate ## @end deftypefn function r = copularnd (family, theta, df, n) ## Check arguments if (nargin < 2) print_usage (); endif if (! ischar (family)) error (strcat ("copularnd: family must be one of 'Gaussian',", ... " 't', 'Clayton', 'Frank', 'Gumbel', 'AMH', and 'FGM'.")); endif lower_family = lower (family); ## Check family and copula parameters switch (lower_family) case {'gaussian'} ## Gaussian family if (isscalar (theta)) ## Expand a scalar to a correlation matrix theta = [1, theta; theta, 1]; endif if (! ismatrix (theta) || any (diag (theta) != 1) || ... any (any (theta != theta')) || min (eig (theta)) <= 0) error ("copularnd: THETA must be a correlation matrix."); endif if (nargin > 3) d = n; if (! isscalar (d) || d != size (theta, 1)) error ("copularnd: D must correspond to dimension of theta."); endif else d = size (theta, 1); endif if (nargin < 3) n = 1; else n = df; if (! isscalar (n) || (n < 0) || round (n) != n) error ("copularnd: N must be a non-negative integer."); endif endif case {'t'} ## Student's t family if (nargin < 3) print_usage (); endif if (isscalar (theta)) ## Expand a scalar to a correlation matrix theta = [1, theta; theta, 1]; endif if (! ismatrix (theta) || any (diag (theta) != 1) || ... any (any (theta != theta')) || min (eig (theta)) <= 0) error ("copularnd: THETA must be a correlation matrix."); endif if (! isscalar (df) && (! isvector (df) || length (df) != n)) error (strcat ("copularnd: DF must be a vector with the same", ... " number of rows as r or be scalar.")); endif df = df(:); if (nargin < 4) n = 1; else if (! isscalar (n) || (n < 0) || round (n) != n) error ("copularnd: N must be a non-negative integer."); endif endif case {'clayton', 'frank', 'gumbel', 'amh', 'fgm'} ## Archimedian one parameter family if (nargin < 4) ## Default is bivariate d = 2; else d = n; if (! isscalar (d) || (d < 2) || round (d) != d) error ("copularnd: D must be an integer greater than 1."); endif endif ## Only the Clayton family is available for more than two dimensions. if (d != 2 && ! strcmp (lower_family, 'clayton')) error (strcat ("copularnd: the '%s' copula is implemented as", ... " bivariate only."), family); endif if (nargin < 3) ## Default is one sample n = 1; else n = df; if (! isscalar (n) || (n < 0) || round (n) != n) error ("copularnd: N must be a non-negative integer."); endif endif if (! isvector (theta) || (! isscalar (theta) && size (theta, 1) != n)) error (strcat ("copularnd: THETA must be a column vector with", ... " the number of rows equal to N or be scalar.")); endif if (n > 1 && isscalar (theta)) theta = repmat (theta, n, 1); endif otherwise error ("copularnd: unknown copula family '%s'.", family); endswitch if (n == 0) ## Input is empty r = zeros (0, d); else ## Draw random samples according to family switch (lower_family) case {'gaussian'} ## The Gaussian family r = normcdf (mvnrnd (zeros (1, d), theta, n), 0, 1); ## No parameter bounds check k = []; case {'t'} ## The Student's t family r = tcdf (mvtrnd (theta, df, n), df); ## No parameter bounds check k = []; case {'clayton'} ## The Clayton family u = rand (n, d); if (d == 2) r = zeros (n, 2); ## Conditional distribution method for the bivariate case which also ## works for theta < 0 r(:, 1) = u(:, 1); r(:, 2) = (1 + u(:, 1) .^ (-theta) .* (u(:, 2) .^ ... (-theta ./ (1 + theta)) - 1)) .^ (-1 ./ theta); else ## Apply the algorithm by Marshall and Olkin: ## Frailty distribution for Clayton copula is gamma y = randg (1 ./ theta, n, 1); r = (1 - log (u) ./ repmat (y, 1, d)) .^ (-1 ./ repmat (theta, 1, d)); endif k = find (theta == 0); if (any (k)) ## Product copula at columns k r(k, :) = u(k, :); endif ## Continue argument check if (d == 2) k = find (! (theta >= -1) | ! (theta < inf)); else k = find (! (theta >= 0) | ! (theta < inf)); endif case {'frank'} ## The Frank family, by inverting the conditional distribution. It has ## a closed form here, which the Gumbel family below does not. u = rand (n, 2); e = exp (-theta); eu = exp (-theta .* u(:, 1)); w = 1 + u(:, 2) .* (1 - e) ./ (u(:, 2) .* (eu - 1) - eu); u2 = -log (w) ./ theta; r = [u(:, 1), u2]; ## Product copula at rows where theta == 0 k = find (theta == 0); if (any (k)) r(k, :) = u(k, :); endif ## Check bounds k = find (! (theta > -inf) | ! (theta < inf)); case {'gumbel'} ## The Gumbel-Hougaard family, by the algorithm of Marshall and Olkin. ## Its frailty is positive stable with index 1 / THETA, drawn by ## Kanter's method; the conditional distribution cannot be inverted in ## closed form as Clayton's and Frank's can. a = 1 ./ theta; U = pi * rand (n, 1); W = -log (rand (n, 1)); aU = sin ((1 - a) .* U) .* (sin (a .* U) .^ (a ./ (1 - a))) ... ./ (sin (U) .^ (1 ./ (1 - a))); V = (aU ./ W) .^ ((1 - a) ./ a); E = -log (rand (n, 2)); r = exp (- (E ./ V) .^ a); ## The independence copula at theta == 1, where the frailty degenerates k = find (theta == 1); if (any (k)) r(k, :) = exp (-E(k, :)); endif ## Check bounds k = find (! (theta >= 1) | ! (theta < inf)); case {'amh'} ## The Ali-Mikhail-Haq family. Its conditional distribution is a ## quadratic in 1 - V, so it inverts in closed form. u = rand (n, 2); w = u(:, 2); b = 1 - u(:, 1); qa = theta - theta .^ 2 .* b .^ 2 .* w; qb = 2 .* theta .* b .* w - (1 + theta); qc = 1 - w; z = w; lin = abs (qa) <= eps; z(! lin) = (-qb(! lin) - sqrt (qb(! lin) .^ 2 ... - 4 .* qa(! lin) .* qc(! lin))) ./ (2 .* qa(! lin)); z(lin) = -qc(lin) ./ qb(lin); r = [u(:, 1), 1 - z]; ## Check bounds k = find (! (theta >= -1) | ! (theta < 1)); case {'fgm'} ## The Farlie-Gumbel-Morgenstern family, likewise a quadratic. u = rand (n, 2); w = u(:, 2); A = theta .* (1 - 2 .* u(:, 1)); v = w; nz = A != 0; v(nz) = ((1 + A(nz)) - sqrt ((1 + A(nz)) .^ 2 ... - 4 .* A(nz) .* w(nz))) ./ (2 .* A(nz)); r = [u(:, 1), v]; ## Check bounds k = find (! (theta >= -1) | ! (theta <= 1)); endswitch ## Out of bounds parameters if (any (k)) r(k, :) = NaN; endif endif endfunction ## Test output %!test %! theta = 0.5; %! r = copularnd ('Gaussian', theta); %! assert_equal (size (r), [1, 2]); %! assert_equal (all ((all ((r >= 0) & (r <= 1)))(:)), true); %!test %! theta = 0.5; %! df = 2; %! r = copularnd ('t', theta, df); %! assert_equal (size (r), [1, 2]); %! assert_equal (all ((all ((r >= 0) & (r <= 1)))(:)), true); %!test %! theta = 0.5; %! r = copularnd ('Clayton', theta); %! assert_equal (size (r), [1, 2]); %! assert_equal (all ((all ((r >= 0) & (r <= 1)))(:)), true); %!test %! theta = 0.5; %! n = 2; %! r = copularnd ('Clayton', theta, n); %! assert_equal (size (r), [n, 2]); %! assert_equal (all ((all ((r >= 0) & (r <= 1)))(:)), true); %!test %! theta = [1; 2]; %! n = 2; %! d = 3; %! r = copularnd ('Clayton', theta, n, d); %! assert_equal (size (r), [n, d]); %! assert_equal (all ((all ((r >= 0) & (r <= 1)))(:)), true); ## The Frank and Gumbel-Hougaard families. A generator cannot be checked ## against MATLAB value for value, so the sample's rank correlation is checked ## against the rank correlation the family is defined to have. %!test %! rand ("seed", 7); %! for theta = [-5, -2, 2, 5, 10] %! r = copularnd ("Frank", theta, 4000); %! assert_equal (size (r), [4000, 2]); %! assert_equal (all (r(:) >= 0 & r(:) <= 1), true); %! rho = corr (tiedrank (r(:,1)), tiedrank (r(:,2))); %! assert_equal (rho, copulastat ("Frank", theta, "type", "Spearman"), 0.05); %! endfor %!test %! rand ("seed", 11); %! for theta = [1.5, 2, 3, 5] %! r = copularnd ("Gumbel", theta, 4000); %! assert_equal (size (r), [4000, 2]); %! assert_equal (all (r(:) >= 0 & r(:) <= 1), true); %! rho = corr (tiedrank (r(:,1)), tiedrank (r(:,2))); %! assert_equal (rho, copulastat ("Gumbel", theta, "type", "Spearman"), 0.05); %! endfor %!test # each family degenerates to independence at its own boundary %! rand ("seed", 13); %! r = copularnd ("Frank", 0, 3000); %! assert_equal (corr (tiedrank (r(:,1)), tiedrank (r(:,2))), 0, 0.06); %! r = copularnd ("Gumbel", 1, 3000); %! assert_equal (corr (tiedrank (r(:,1)), tiedrank (r(:,2))), 0, 0.06); %!test # a parameter outside the family's range gives NaN rows %! assert_equal (copularnd ("Gumbel", 0.5, 2), NaN (2, 2)); %! assert_equal (copularnd ("Frank", Inf, 2), NaN (2, 2)); %!test # the default is a single bivariate draw %! rand ("seed", 3); %! assert_equal (size (copularnd ("Frank", 3)), [1, 2]); %! assert_equal (size (copularnd ("Gumbel", 2)), [1, 2]); %! assert_equal (size (copularnd ("Gumbel", 2, 5)), [5, 2]); %!error ... %! copularnd ("Frank", 3, 5, 3) %!error ... %! copularnd ("Gumbel", 2, 5, 3) ## The Ali-Mikhail-Haq and Farlie-Gumbel-Morgenstern families, Octave ## extensions. As with the other generators the sample's rank correlation is ## checked against the one the family is defined to have. %!test %! rand ("seed", 21); %! for theta = [-0.9, -0.5, 0.5, 0.9] %! r = copularnd ("AMH", theta, 4000); %! assert_equal (size (r), [4000, 2]); %! assert_equal (all (r(:) >= 0 & r(:) <= 1), true); %! rho = corr (tiedrank (r(:,1)), tiedrank (r(:,2))); %! assert_equal (rho, copulastat ("AMH", theta, "type", "Spearman"), 0.05); %! endfor %!test %! rand ("seed", 23); %! for theta = [-1, -0.5, 0.5, 1] %! r = copularnd ("FGM", theta, 4000); %! assert_equal (size (r), [4000, 2]); %! assert_equal (all (r(:) >= 0 & r(:) <= 1), true); %! rho = corr (tiedrank (r(:,1)), tiedrank (r(:,2))); %! assert_equal (rho, copulastat ("FGM", theta, "type", "Spearman"), 0.05); %! endfor %!test # both are the independence copula at a zero parameter %! rand ("seed", 29); %! r = copularnd ("AMH", 0, 4000); %! assert_equal (corr (tiedrank (r(:,1)), tiedrank (r(:,2))), 0, 0.05); %! r = copularnd ("FGM", 0, 4000); %! assert_equal (corr (tiedrank (r(:,1)), tiedrank (r(:,2))), 0, 0.05); %!test # a parameter outside the family's range gives NaN rows %! assert_equal (copularnd ("AMH", 1, 2), NaN (2, 2)); %! assert_equal (copularnd ("AMH", -1.5, 2), NaN (2, 2)); %! assert_equal (copularnd ("FGM", 1.5, 2), NaN (2, 2)); %!error ... %! copularnd ("AMH", 0.5, 5, 3) %!error ... %! copularnd ("FGM", 0.5, 5, 3) statistics-release-2.1.2/inst/Distribution_Functions/doc-cache000066400000000000000000010204371527311104300245450ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 180 # name: # type: sq_string # elements: 1 # length: 7 betacdf # name: # type: sq_string # elements: 1 # length: 975 statistics: p = betacdf (x, a, b) statistics: p = betacdf (x, a, b, 'upper') Beta cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function of the Beta distribution with shape parameters a and b. The size of p is the common size of x, a, and b. A scalar input functions as a constant matrix of the same size as the other inputs. p = betacdf (x, a, b, "upper") computes the upper tail probability of the Beta distribution with parameters a and b, at the values in x. Further information about the Beta distribution can be found at https://en.wikipedia.org/wiki/Beta_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: betainv, betapdf, betarnd, betafit, betalike, betastat # name: # type: sq_string # elements: 1 # length: 44 Beta cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 7 betainv # name: # type: sq_string # elements: 1 # length: 791 statistics: x = betainv (p, a, b) Inverse of the Beta distribution (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the Beta distribution with shape parameters a and b. The size of x is the common size of x, a, and b. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Beta distribution can be found at https://en.wikipedia.org/wiki/Beta_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: betacdf, betapdf, betarnd, betafit, betalike, betastat # name: # type: sq_string # elements: 1 # length: 40 Inverse of the Beta distribution (iCDF). # name: # type: sq_string # elements: 1 # length: 7 betapdf # name: # type: sq_string # elements: 1 # length: 792 statistics: y = betapdf (x, a, b) Beta probability density function (PDF). For each element of x, compute the probability density function (PDF) of the Beta distribution with shape parameters a and b. The size of y is the common size of x, a, and b. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Beta distribution can be found at https://en.wikipedia.org/wiki/Beta_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: betacdf, betainv, betarnd, betafit, betalike, betastat # name: # type: sq_string # elements: 1 # length: 40 Beta probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 7 betarnd # name: # type: sq_string # elements: 1 # length: 974 statistics: r = betarnd (a, b) statistics: r = betarnd (a, b, rows) statistics: r = betarnd (a, b, rows, cols, ...) statistics: r = betarnd (a, b, [sz]) Random arrays from the Beta distribution. r = betarnd (a, b) returns an array of random numbers chosen from the Beta distribution with shape parameters a and b. The size of r is the common size of a and b. A scalar input functions as a constant matrix of the same size as the other inputs. When called with a single size argument, betarnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the Beta distribution can be found at https://en.wikipedia.org/wiki/Beta_distribution See also: betacdf, betainv, betapdf, betafit, betalike, betastat # name: # type: sq_string # elements: 1 # length: 41 Random arrays from the Beta distribution. # name: # type: sq_string # elements: 1 # length: 7 binocdf # name: # type: sq_string # elements: 1 # length: 1146 statistics: p = binocdf (x, n, ps) statistics: p = binocdf (x, n, ps, 'upper') Binomial cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the binomial distribution with parameters n and ps, where n is the number of trials and ps is the probability of success. The size of p is the common size of x, n, and ps. A scalar input functions as a constant matrix of the same size as the other inputs. p = binocdf (x, n, ps, "upper") computes the upper tail probability of the binomial distribution with parameters n and ps, at the values in x. Further information about the binomial distribution can be found at https://en.wikipedia.org/wiki/Binomial_distribution Input arguments must be double, single, or an integer type; logical and character arrays are rejected. Integer input is promoted to double, so the result is always a probability. MATLAB is inconsistent here: for several of the discrete distributions it returns the result in the integer class of the input, truncating a probability to 0 or 1. See also: binoinv, binopdf, binornd, binofit, binolike, binostat, binotest # name: # type: sq_string # elements: 1 # length: 48 Binomial cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 7 binoinv # name: # type: sq_string # elements: 1 # length: 904 statistics: x = binoinv (p, n, ps) Inverse of the Binomial cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the binomial distribution with parameters n and ps, where n is the number of trials and ps is the probability of success. The size of x is the common size of p, n, and ps. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the binomial distribution can be found at https://en.wikipedia.org/wiki/Binomial_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: binocdf, binopdf, binornd, binofit, binolike, binostat, binotest # name: # type: sq_string # elements: 1 # length: 64 Inverse of the Binomial cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 7 binopdf # name: # type: sq_string # elements: 1 # length: 1156 statistics: y = binopdf (x, n, ps) Binomial probability density function (PDF). For each element of x, compute the probability density function (PDF) of the binomial distribution with parameters n and ps, where n is the number of trials and ps is the probability of success. The size of y is the common size of x, n, and ps. A scalar input functions as a constant matrix of the same size as the other inputs. Matlab incompatibility: Octave's binopdf does not allow complex input values. Matlab 2021b returns values for complex inputs despite the documentation indicates integer and real value inputs are required. Further information about the binomial distribution can be found at https://en.wikipedia.org/wiki/Binomial_distribution Input arguments must be double, single, or an integer type; logical and character arrays are rejected. Integer input is promoted to double, so the result is always a probability. MATLAB is inconsistent here: for several of the discrete distributions it returns the result in the integer class of the input, truncating a probability to 0 or 1. See also: binocdf, binoinv, binornd, binofit, binolike, binostat, binotest # name: # type: sq_string # elements: 1 # length: 44 Binomial probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 7 binornd # name: # type: sq_string # elements: 1 # length: 1064 statistics: r = binornd (n, ps) statistics: r = binornd (n, ps, rows) statistics: r = binornd (n, ps, rows, cols, ...) statistics: r = binornd (n, ps, [sz]) Random arrays from the Binomial distribution. r = binornd (n, ps) returns a matrix of random samples from the binomial distribution with parameters n and ps, where n is the number of trials and ps is the probability of success. The size of r is the common size of n and ps. A scalar input functions as a constant matrix of the same size as the other inputs. When called with a single size argument, binornd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the binomial distribution can be found at https://en.wikipedia.org/wiki/Binomial_distribution See also: binocdf, binoinv, binopdf, binofit, binolike, binostat, binotest # name: # type: sq_string # elements: 1 # length: 45 Random arrays from the Binomial distribution. # name: # type: sq_string # elements: 1 # length: 7 bisacdf # name: # type: sq_string # elements: 1 # length: 1110 statistics: p = bisacdf (x, beta, gamma) statistics: p = bisacdf (x, beta, gamma, 'upper') Birnbaum-Saunders cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the Birnbaum-Saunders distribution with scale parameter beta and shape parameter gamma. The size of p is the common size of x, beta and gamma. A scalar input functions as a constant matrix of the same size as the other inputs. p = bisacdf (x, beta, gamma, "upper") computes the upper tail probability of the Birnbaum-Saunders distribution with parameters beta and gamma, at the values in x. Further information about the Birnbaum-Saunders distribution can be found at https://en.wikipedia.org/wiki/Birnbaum%E2%80%93Saunders_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: bisainv, bisapdf, bisarnd, bisafit, bisalike, bisastat # name: # type: sq_string # elements: 1 # length: 57 Birnbaum-Saunders cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 7 bisainv # name: # type: sq_string # elements: 1 # length: 907 statistics: x = bisainv (p, beta, gamma) Inverse of the Birnbaum-Saunders cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the Birnbaum-Saunders distribution with scale parameter beta and shape parameter gamma. The size of x is the common size of p, beta, and gamma. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Birnbaum-Saunders distribution can be found at https://en.wikipedia.org/wiki/Birnbaum%E2%80%93Saunders_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: bisacdf, bisapdf, bisarnd, bisafit, bisalike, bisastat # name: # type: sq_string # elements: 1 # length: 73 Inverse of the Birnbaum-Saunders cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 7 bisapdf # name: # type: sq_string # elements: 1 # length: 888 statistics: y = bisapdf (x, beta, gamma) Birnbaum-Saunders probability density function (PDF). For each element of x, compute the probability density function (PDF) of the Birnbaum-Saunders distribution with scale parameter beta and shape parameter gamma. The size of y is the common size of x, beta, and gamma. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Birnbaum-Saunders distribution can be found at https://en.wikipedia.org/wiki/Birnbaum%E2%80%93Saunders_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: bisacdf, bisainv, bisarnd, bisafit, bisalike, bisastat # name: # type: sq_string # elements: 1 # length: 53 Birnbaum-Saunders probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 7 bisarnd # name: # type: sq_string # elements: 1 # length: 1098 statistics: r = bisarnd (beta, gamma) statistics: r = bisarnd (beta, gamma, rows) statistics: r = bisarnd (beta, gamma, rows, cols, ...) statistics: r = bisarnd (beta, gamma, [sz]) Random arrays from the Birnbaum-Saunders distribution. r = bisarnd (beta, gamma) returns an array of random numbers chosen from the Birnbaum-Saunders distribution with scale parameter beta and shape parameter gamma. The size of r is the common size of beta and gamma. A scalar input functions as a constant matrix of the same size as the other inputs. When called with a single size argument, bisarnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the Birnbaum-Saunders distribution can be found at https://en.wikipedia.org/wiki/Birnbaum%E2%80%93Saunders_distribution See also: bisacdf, bisainv, bisapdf, bisafit, bisalike, bisastat # name: # type: sq_string # elements: 1 # length: 54 Random arrays from the Birnbaum-Saunders distribution. # name: # type: sq_string # elements: 1 # length: 7 burrcdf # name: # type: sq_string # elements: 1 # length: 1101 statistics: p = burrcdf (x, lambda, c, k) statistics: p = burrcdf (x, lambda, c, k, 'upper') Burr type XII cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the Burr type XII distribution with scale parameter lambda, first shape parameter c, and second shape parameter k. The size of p is the common size of x, lambda, c, and k. A scalar input functions as a constant matrix of the same size as the other inputs. p = burrcdf (x, lambda, c, k, "upper") computes the upper tail probability of the Burr type XII distribution with parameters lambda, c and k, at the values in x. Further information about the Burr distribution can be found at https://en.wikipedia.org/wiki/Burr_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: burrinv, burrpdf, burrrnd, burrfit, burrlike, burrstat # name: # type: sq_string # elements: 1 # length: 53 Burr type XII cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 7 burrinv # name: # type: sq_string # elements: 1 # length: 898 statistics: x = burrinv (p, lambda, c, k) Inverse of the Burr type XII cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the Burr type XII distribution with scale parameter lambda, first shape parameter c, and second shape parameter k. The size of x is the common size of p, lambda, c, and k. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Burr distribution can be found at https://en.wikipedia.org/wiki/Burr_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: burrcdf, burrpdf, burrrnd, burrfit, burrlike, burrstat # name: # type: sq_string # elements: 1 # length: 69 Inverse of the Burr type XII cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 7 burrpdf # name: # type: sq_string # elements: 1 # length: 879 statistics: y = burrpdf (x, lambda, c, k) Burr type XII probability density function (PDF). For each element of x, compute the probability density function (PDF) of the Burr type XII distribution with scale parameter lambda, first shape parameter c, and second shape parameter k. The size of y is the common size of x, lambda, c, and k. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Burr distribution can be found at https://en.wikipedia.org/wiki/Burr_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: burrcdf, burrinv, burrrnd, burrfit, burrlike, burrstat # name: # type: sq_string # elements: 1 # length: 49 Burr type XII probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 7 burrrnd # name: # type: sq_string # elements: 1 # length: 1099 statistics: r = burrrnd (lambda, c, k) statistics: r = burrrnd (lambda, c, k, rows) statistics: r = burrrnd (lambda, c, k, rows, cols, ...) statistics: r = burrrnd (lambda, c, k, [sz]) Random arrays from the Burr type XII distribution. r = burrrnd (lambda, c, k) returns an array of random numbers chosen from the Burr type XII distribution with scale parameter lambda, first shape parameter c, and second shape parameter k. The size of r is the common size of lambda, c, and k. LAMBDA scalar input functions as a constant matrix of the same size as the other inputs. When called with a single size argument, burrrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the Burr distribution can be found at https://en.wikipedia.org/wiki/Burr_distribution See also: burrcdf, burrinv, burrpdf, burrfit, burrlike, burrstat # name: # type: sq_string # elements: 1 # length: 50 Random arrays from the Burr type XII distribution. # name: # type: sq_string # elements: 1 # length: 6 bvncdf # name: # type: sq_string # elements: 1 # length: 892 statistics: p = bvncdf (x, mu, sigma) statistics: p = bvncdf (x, [], sigma) Bivariate normal cumulative distribution function (CDF). p = bvncdf (x, mu, sigma) will compute the bivariate normal cumulative distribution function of x given a mean parameter mu and a scale parameter sigma. x must be an Nx2 matrix with each variable as a column vector. mu can be either a scalar (common mean) or a two-element row vector (each element corresponds to a variable). If empty, a zero mean is assumed. sigma can be a scalar (common variance) or a 2x2 covariance matrix, which must be positive definite. Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: mvncdf # name: # type: sq_string # elements: 1 # length: 56 Bivariate normal cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 6 bvtcdf # name: # type: sq_string # elements: 1 # length: 805 statistics: p = bvtcdf (x, rho, df) statistics: p = bvtcdf (x, rho, df, Tol) Bivariate Student's t cumulative distribution function (CDF). p = bvtcdf (x, rho, df) will compute the bivariate student's t cumulative distribution function of x, which must be an Nx2 matrix, given a correlation coefficient rho, which must be a scalar, and df degrees of freedom, which can be a scalar or a vector of positive numbers commensurate with x. Tol is the tolerance for numerical integration and by default Tol = 1e-8. Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: mvtcdf # name: # type: sq_string # elements: 1 # length: 61 Bivariate Student's t cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 9 cauchycdf # name: # type: sq_string # elements: 1 # length: 1022 statistics: p = cauchycdf (x, x0, gamma) statistics: p = cauchycdf (x, x0, gamma, 'upper') Cauchy cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the Cauchy distribution with location parameter x0 and scale parameter gamma. The size of p is the common size of x, x0, and gamma. A scalar input functions as a constant matrix of the same size as the other inputs. p = cauchycdf (x, x0, gamma, "upper") computes the upper tail probability of the Cauchy distribution with parameters x0 and gamma, at the values in x. Further information about the Cauchy distribution can be found at https://en.wikipedia.org/wiki/Cauchy_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: cauchyinv, cauchypdf, cauchyrnd # name: # type: sq_string # elements: 1 # length: 46 Cauchy cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 9 cauchyinv # name: # type: sq_string # elements: 1 # length: 831 statistics: x = cauchyinv (p, x0, gamma) Inverse of the Cauchy cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the Cauchy distribution with location parameter x0 and scale parameter gamma. The size of x is the common size of p, x0, and gamma. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Cauchy distribution can be found at https://en.wikipedia.org/wiki/Cauchy_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: cauchycdf, cauchypdf, cauchyrnd # name: # type: sq_string # elements: 1 # length: 62 Inverse of the Cauchy cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 9 cauchypdf # name: # type: sq_string # elements: 1 # length: 812 statistics: y = cauchypdf (x, x0, gamma) Cauchy probability density function (PDF). For each element of x, compute the probability density function (PDF) of the Cauchy distribution with location parameter x0 and scale parameter gamma. The size of y is the common size of x, x0, and gamma. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Cauchy distribution can be found at https://en.wikipedia.org/wiki/Cauchy_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: cauchycdf, cauchyinv, cauchyrnd # name: # type: sq_string # elements: 1 # length: 42 Cauchy probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 9 cauchyrnd # name: # type: sq_string # elements: 1 # length: 1024 statistics: r = cauchyrnd (x0, gamma) statistics: r = cauchyrnd (x0, gamma, rows) statistics: r = cauchyrnd (x0, gamma, rows, cols, ...) statistics: r = cauchyrnd (x0, gamma, [sz]) Random arrays from the Cauchy distribution. r = cauchyrnd (x0, gamma) returns an array of random numbers chosen from the Cauchy distribution with location parameter x0 and scale parameter gamma. The size of r is the common size of x0 and gamma. A scalar input functions as a constant matrix of the same size as the other inputs. When called with a single size argument, cauchyrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the Cauchy distribution can be found at https://en.wikipedia.org/wiki/Cauchy_distribution See also: cauchycdf, cauchyinv, cauchypdf # name: # type: sq_string # elements: 1 # length: 43 Random arrays from the Cauchy distribution. # name: # type: sq_string # elements: 1 # length: 7 chi2cdf # name: # type: sq_string # elements: 1 # length: 1116 statistics: p = chi2cdf (x, df) statistics: p = chi2cdf (x, df, 'upper') Chi-squared cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the chi-squared distribution with df degrees of freedom. The chi-squared density function with df degrees of freedom is the same as a gamma density function with parameters df/2 and 2. The size of p is the common size of x and df. A scalar input functions as a constant matrix of the same size as the other input. p = chi2cdf (x, df, "upper") computes the upper tail probability of the chi-squared distribution with df degrees of freedom, at the values in x. Further information about the chi-squared distribution can be found at https://en.wikipedia.org/wiki/Chi-squared_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: chi2inv, chi2pdf, chi2rnd, chi2stat # name: # type: sq_string # elements: 1 # length: 51 Chi-squared cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 7 chi2inv # name: # type: sq_string # elements: 1 # length: 812 statistics: x = chi2inv (p, df) Inverse of the chi-squared cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the chi-squared distribution with df degrees of freedom. The size of x is the common size of p and df. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the chi-squared distribution can be found at https://en.wikipedia.org/wiki/Chi-squared_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: chi2cdf, chi2pdf, chi2rnd, chi2stat # name: # type: sq_string # elements: 1 # length: 67 Inverse of the chi-squared cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 7 chi2pdf # name: # type: sq_string # elements: 1 # length: 793 statistics: y = chi2pdf (x, df) Chi-squared probability density function (PDF). For each element of x, compute the probability density function (PDF) of the chi-squared distribution with df degrees of freedom. The size of y is the common size of x and df. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the chi-squared distribution can be found at https://en.wikipedia.org/wiki/Chi-squared_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: chi2cdf, chi2inv, chi2rnd, chi2stat # name: # type: sq_string # elements: 1 # length: 47 Chi-squared probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 7 chi2rnd # name: # type: sq_string # elements: 1 # length: 874 statistics: r = chi2rnd (df) statistics: r = chi2rnd (df, rows) statistics: r = chi2rnd (df, rows, cols, ...) statistics: r = chi2rnd (df, [sz]) Random arrays from the chi-squared distribution. r = chi2rnd (df) returns an array of random numbers chosen from the chi-squared distribution with df degrees of freedom. The size of r is the size of df. When called with a single size argument, chi2rnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the chi-squared distribution can be found at https://en.wikipedia.org/wiki/Chi-squared_distribution See also: chi2cdf, chi2inv, chi2pdf, chi2stat # name: # type: sq_string # elements: 1 # length: 48 Random arrays from the chi-squared distribution. # name: # type: sq_string # elements: 1 # length: 9 copulacdf # name: # type: sq_string # elements: 1 # length: 2050 statistics: p = copulacdf (family, x, theta) statistics: p = copulacdf ('t', x, theta, df) Copula family cumulative distribution functions (CDF). Arguments family is the copula family name. Currently, family can be 'Gaussian' for the Gaussian family, 't' for the Student's t family, 'Clayton' for the Clayton family, 'Gumbel' for the Gumbel-Hougaard family, 'Frank' for the Frank family, 'AMH' for the Ali-Mikhail-Haq family, or 'FGM' for the Farlie-Gumbel-Morgenstern family. x is the support where each row corresponds to an observation. theta is the parameter of the copula. For the Gaussian and Student's t copula, theta must be a correlation matrix. For bivariate copulas theta can also be a correlation coefficient. For the Clayton family, the Gumbel-Hougaard family, the Frank family, and the Ali-Mikhail-Haq family, theta must be a vector with the same number of elements as observations in x or be scalar. For the Farlie-Gumbel-Morgenstern family, theta must be a matrix of coefficients for the Farlie-Gumbel-Morgenstern polynomial where each row corresponds to one set of coefficients for an observation in x. A single row is expanded. The coefficients are in binary order. df is the degrees of freedom for the Student's t family. df must be a vector with the same number of elements as observations in x or be scalar. Return values p is the cumulative distribution of the copula at each row of x and corresponding parameter theta. Examples x = [0.2:0.2:0.6; 0.2:0.2:0.6]; theta = [1; 2]; p = copulacdf ("Clayton", x, theta) x = [0.2:0.2:0.6; 0.2:0.1:0.4]; theta = [0.2, 0.1, 0.1, 0.05]; p = copulacdf ("FGM", x, theta) References Roger B. Nelsen. An Introduction to Copulas. Springer, New York, second edition, 2006. Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: copulapdf, copularnd # name: # type: sq_string # elements: 1 # length: 54 Copula family cumulative distribution functions (CDF). # name: # type: sq_string # elements: 1 # length: 9 copulapdf # name: # type: sq_string # elements: 1 # length: 2268 statistics: y = copulapdf (family, x, theta) statistics: y = copulapdf ('t', x, theta, df) Copula family probability density functions (PDF). Arguments family is the copula family name. Currently, family can be 'Gaussian' for the Gaussian family, 't' for the Student's t family, 'Clayton' for the Clayton family, 'Gumbel' for the Gumbel-Hougaard family, 'Frank' for the Frank family, 'AMH' for the Ali-Mikhail-Haq family, or 'FGM' for the Farlie-Gumbel-Morgenstern family. The last two are Octave extensions that MATLAB does not provide. x is the support where each row corresponds to an observation. theta is the parameter of the copula. For the Gaussian and Student's t families it is the linear correlation matrix, and a scalar is expanded to a bivariate one. For the remaining families the elements of theta must be greater than or equal to -1 for the Clayton family, greater than or equal to 1 for the Gumbel-Hougaard family, arbitrary for the Frank family, and greater than or equal to -1 and lower than 1 for the Ali-Mikhail-Haq family. Moreover, theta must be non-negative for dimensions greater than 2. theta must be a column vector with the same number of rows as x or be scalar. The Farlie-Gumbel-Morgenstern family instead takes one parameter for every subset of the variables of order two or more, so theta is a row vector of length 2^d-d-1 or a matrix with one such row per observation; parameter sets violating the family's linear constraints give NaN. df is the degrees of freedom of the Student's t family, and is required by it. It must be a vector with the same number of rows as x or be scalar. Return values y is the probability density of the copula at each row of x and corresponding parameter theta. Examples x = [0.2:0.2:0.6; 0.2:0.2:0.6]; theta = [1; 2]; y = copulapdf ("Clayton", x, theta) y = copulapdf ("Gumbel", x, 2) References Roger B. Nelsen. An Introduction to Copulas. Springer, New York, second edition, 2006. Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: copulacdf, copularnd # name: # type: sq_string # elements: 1 # length: 50 Copula family probability density functions (PDF). # name: # type: sq_string # elements: 1 # length: 9 copularnd # name: # type: sq_string # elements: 1 # length: 2080 statistics: r = copularnd (family, theta, n) statistics: r = copularnd (family, theta, n, d) statistics: r = copularnd ('t', theta, df, n) Random arrays from the copula family distributions. Arguments family is the copula family name. Currently, family can be 'Gaussian' for the Gaussian family, 't' for the Student's t family, 'Clayton' for the Clayton family, 'Frank' for the Frank family, 'Gumbel' for the Gumbel-Hougaard family, 'AMH' for the Ali-Mikhail-Haq family, or 'FGM' for the Farlie-Gumbel-Morgenstern family. The last two are Octave extensions that MATLAB does not provide. Every family but Clayton is generated as bivariate only. theta is the parameter of the copula. For the Gaussian and Student's t copula, theta must be a correlation matrix. For bivariate copulas theta can also be a correlation coefficient. For the Clayton, Frank and Gumbel-Hougaard families, theta must be a vector with the same number of elements as samples to be generated or be scalar. Values outside a family's range give NaN rows: at or above 1 for the Gumbel-Hougaard family, at or above -1 for the bivariate Clayton family, and any finite value for the Frank family. The Ali-Mikhail-Haq family takes theta in [-1, 1) and the Farlie-Gumbel-Morgenstern family in [-1, 1]. df is the degrees of freedom for the Student's t family. df must be a vector with the same number of elements as samples to be generated or be scalar. n is the number of rows of the matrix to be generated. n must be a non-negative integer and corresponds to the number of samples to be generated. d is the number of columns of the matrix to be generated. d must be a positive integer and corresponds to the dimension of the copula. Return values r is a matrix of random samples from the copula with n samples of distribution dimension d. Examples theta = 0.5; r = copularnd ("Gaussian", theta); theta = 0.5; df = 2; r = copularnd ("t", theta, df); theta = 0.5; n = 2; r = copularnd ("Clayton", theta, n); References Roger B. Nelsen. An Introduction to Copulas. Springer, New York, second edition, 2006. # name: # type: sq_string # elements: 1 # length: 51 Random arrays from the copula family distributions. # name: # type: sq_string # elements: 1 # length: 5 evcdf # name: # type: sq_string # elements: 1 # length: 2127 statistics: p = evcdf (x) statistics: p = evcdf (x, mu) statistics: p = evcdf (x, mu, sigma) statistics: p = evcdf (..., 'upper') statistics: [p, plo, pup] = evcdf (x, mu, sigma, pcov) statistics: [p, plo, pup] = evcdf (x, mu, sigma, pcov, alpha) statistics: [p, plo, pup] = evcdf (..., 'upper') Extreme value cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the extreme value distribution (also known as the Gumbel or the type I generalized extreme value distribution) at the values in x with location parameter mu and scale parameter sigma. The size of p is the common size of x, mu and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Default values are mu = 0 and sigma = 1. When called with three output arguments, i.e. [p, plo, pup], evcdf computes the confidence bounds for p when the input parameters mu and sigma are estimates. In such case, pcov, a 2x2 matrix containing the covariance matrix of the estimated parameters, is necessary. Optionally, alpha, which has a default value of 0.05, specifies the 100 * (1 - alpha) percent confidence bounds. plo and pup are arrays of the same size as p containing the lower and upper confidence bounds. [...] = evcdf (..., "upper") computes the upper tail probability of the extreme value distribution with parameters x0 and gamma, at the values in x. The Gumbel distribution is used to model the distribution of the maximum (or the minimum) of a number of samples of various distributions. This version is suitable for modeling minima. For modeling maxima, use the alternative Gumbel CDF, gumbelcdf. Further information about the Gumbel distribution can be found at https://en.wikipedia.org/wiki/Gumbel_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: evinv, evpdf, evrnd, evfit, evlike, evstat, gumbelcdf # name: # type: sq_string # elements: 1 # length: 53 Extreme value cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 5 evinv # name: # type: sq_string # elements: 1 # length: 1884 statistics: x = evinv (p) statistics: x = evinv (p, mu) statistics: x = evinv (p, mu, sigma) statistics: [x, xlo, xup] = evinv (p, mu, sigma, pcov) statistics: [x, xlo, xup] = evinv (p, mu, sigma, pcov, alpha) Inverse of the extreme value cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the extreme value distribution (also known as the Gumbel or the type I generalized extreme value distribution) with location parameter mu and scale parameter sigma. The size of x is the common size of p, mu and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Default values are mu = 0 and sigma = 1. When called with three output arguments, i.e. [x, xlo, xup], evinv computes the confidence bounds for x when the input parameters mu and sigma are estimates. In such case, pcov, a 2x2 matrix containing the covariance matrix of the estimated parameters, is necessary. Optionally, alpha, which has a default value of 0.05, specifies the 100 * (1 - alpha) percent confidence bounds. xlo and xup are arrays of the same size as x containing the lower and upper confidence bounds. The Gumbel distribution is used to model the distribution of the maximum (or the minimum) of a number of samples of various distributions. This version is suitable for modeling minima. For modeling maxima, use the alternative Gumbel iCDF, gumbelinv. Further information about the Gumbel distribution can be found at https://en.wikipedia.org/wiki/Gumbel_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: evcdf, evpdf, evrnd, evfit, evlike, evstat, gumbelinv # name: # type: sq_string # elements: 1 # length: 69 Inverse of the extreme value cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 5 evpdf # name: # type: sq_string # elements: 1 # length: 1272 statistics: y = evpdf (x) statistics: y = evpdf (x, mu) statistics: y = evpdf (x, mu, sigma) Extreme value probability density function (PDF). For each element of x, compute the probability density function (PDF) of the extreme value distribution (also known as the Gumbel or the type I generalized extreme value distribution) with location parameter mu and scale parameter sigma. The size of y is the common size of x, mu and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Default values are mu = 0 and sigma = 1. The Gumbel distribution is used to model the distribution of the maximum (or the minimum) of a number of samples of various distributions. This version is suitable for modeling minima. For modeling maxima, use the alternative Gumbel iCDF, gumbelinv. Further information about the Gumbel distribution can be found at https://en.wikipedia.org/wiki/Gumbel_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: evcdf, evinv, evrnd, evfit, evlike, evstat, gumbelpdf # name: # type: sq_string # elements: 1 # length: 49 Extreme value probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 5 evrnd # name: # type: sq_string # elements: 1 # length: 1356 statistics: r = evrnd (mu, sigma) statistics: r = evrnd (mu, sigma, rows) statistics: r = evrnd (mu, sigma, rows, cols, ...) statistics: r = evrnd (mu, sigma, [sz]) Random arrays from the extreme value distribution. r = evrnd (mu, sigma) returns an array of random numbers chosen from the extreme value distribution (also known as the Gumbel or the type I generalized extreme value distribution) with location parameter mu and scale parameter sigma. The size of r is the common size of mu and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. When called with a single size argument, evrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. The Gumbel distribution is used to model the distribution of the maximum (or the minimum) of a number of samples of various distributions. This version is suitable for modeling minima. For modeling maxima, use the alternative Gumbel iCDF, gumbelinv. Further information about the Gumbel distribution can be found at https://en.wikipedia.org/wiki/Gumbel_distribution See also: evcdf, evinv, evpdf, evfit, evlike, evstat # name: # type: sq_string # elements: 1 # length: 50 Random arrays from the extreme value distribution. # name: # type: sq_string # elements: 1 # length: 6 expcdf # name: # type: sq_string # elements: 1 # length: 1927 statistics: p = expcdf (x) statistics: p = expcdf (x, mu) statistics: p = expcdf (..., 'upper') statistics: [p, plo, pup] = expcdf (x, mu, pcov) statistics: [p, plo, pup] = expcdf (x, mu, pcov, alpha) statistics: [p, plo, pup] = expcdf (..., 'upper') Exponential cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the exponential distribution with mean parameter mu. The size of p is the common size of x and mu. A scalar input functions as a constant matrix of the same size as the other inputs. Default value is mu = 1. A common alternative parameterization of the exponential distribution is to use the parameter λ defined as the mean number of events in an interval as opposed to the parameter μ, which is the mean wait time for an event to occur. λ and μ are reciprocals, i.e. μ = 1 / λ. When called with three output arguments, i.e. [p, plo, pup], expcdf computes the confidence bounds for p when the input parameter mu is an estimate. In such case, pcov, a scalar value with the variance of the estimated parameter mu, is necessary. Optionally, alpha, which has a default value of 0.05, specifies the 100 * (1 - alpha) percent confidence bounds. plo and pup are arrays of the same size as p containing the lower and upper confidence bounds. [...] = expcdf (..., "upper") computes the upper tail probability of the exponential distribution with parameter mu, at the values in x. Further information about the exponential distribution can be found at https://en.wikipedia.org/wiki/Exponential_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: expinv, exppdf, exprnd, expfit, explike, expstat # name: # type: sq_string # elements: 1 # length: 51 Exponential cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 6 expinv # name: # type: sq_string # elements: 1 # length: 1702 statistics: x = expinv (p) statistics: x = expinv (p, mu) statistics: [x, xlo, xup] = expinv (p, mu, pcov) statistics: [x, xlo, xup] = expinv (p, mu, pcov, alpha) Inverse of the exponential cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the exponential distribution with mean mu. The size of x is the common size of p and mu. A scalar input functions as a constant matrix of the same size as the other inputs. Default value is mu = 1. A common alternative parameterization of the exponential distribution is to use the parameter λ defined as the mean number of events in an interval as opposed to the parameter μ, which is the mean wait time for an event to occur. λ and μ are reciprocals, i.e. μ = 1 / λ. When called with three output arguments, i.e. [x, xlo, xup], expinv computes the confidence bounds for x when the input parameter mu is an estimate. In such case, pcov, a scalar value with the variance of the estimated parameter mu, is necessary. Optionally, alpha, which has a default value of 0.05, specifies the 100 * (1 - alpha) percent confidence bounds. xlo and xup are arrays of the same size as x containing the lower and upper confidence bounds. Further information about the exponential distribution can be found at https://en.wikipedia.org/wiki/Exponential_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: expcdf, exppdf, exprnd, expfit, explike, expstat # name: # type: sq_string # elements: 1 # length: 67 Inverse of the exponential cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 6 exppdf # name: # type: sq_string # elements: 1 # length: 1133 statistics: y = exppdf (x) statistics: y = exppdf (x, mu) Exponential probability density function (PDF). For each element of x, compute the probability density function (PDF) of the exponential distribution with mean parameter mu. The size of y is the common size of x and mu. A scalar input functions as a constant matrix of the same size as the other inputs. Default value for mu = 1. A common alternative parameterization of the exponential distribution is to use the parameter λ defined as the mean number of events in an interval as opposed to the parameter μ, which is the mean wait time for an event to occur. λ and μ are reciprocals, i.e. μ = 1 / λ. Further information about the exponential distribution can be found at https://en.wikipedia.org/wiki/Exponential_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: expcdf, expinv, exprnd, expfit, explike, expstat # name: # type: sq_string # elements: 1 # length: 47 Exponential probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 6 exprnd # name: # type: sq_string # elements: 1 # length: 1155 statistics: r = exprnd (mu) statistics: r = exprnd (mu, rows) statistics: r = exprnd (mu, rows, cols, ...) statistics: r = exprnd (mu, [sz]) Random arrays from the exponential distribution. r = exprnd (mu) returns an array of random numbers chosen from the exponential distribution with mean parameter mu. The size of r is the size of mu. When called with a single size argument, exprnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. A common alternative parameterization of the exponential distribution is to use the parameter λ defined as the mean number of events in an interval as opposed to the parameter μ, which is the mean wait time for an event to occur. λ and μ are reciprocals, i.e. μ = 1 / λ. Further information about the exponential distribution can be found at https://en.wikipedia.org/wiki/Exponential_distribution See also: expcdf, expinv, exppdf, expfit, explike, expstat # name: # type: sq_string # elements: 1 # length: 48 Random arrays from the exponential distribution. # name: # type: sq_string # elements: 1 # length: 4 fcdf # name: # type: sq_string # elements: 1 # length: 960 statistics: p = fcdf (x, df1, df2) statistics: p = fcdf (x, df1, df2, 'upper') F-cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the F-distribution with df1 and df2 degrees of freedom. The size of p is the common size of x, df1, and df2. A scalar input functions as a constant matrix of the same size as the other inputs. p = fcdf (x, df1, df2, "upper") computes the upper tail probability of the F-distribution with df1 and df2 degrees of freedom, at the values in x. Further information about the F-distribution can be found at https://en.wikipedia.org/wiki/F-distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: finv, fpdf, frnd, fstat # name: # type: sq_string # elements: 1 # length: 41 F-cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 4 finv # name: # type: sq_string # elements: 1 # length: 1257 statistics: x = finv (p, df1, df2) Inverse of the F-cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the F-distribution with df1 and df2 degrees of freedom. The size of x is the common size of p, df1, and df2. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the F-distribution can be found at https://en.wikipedia.org/wiki/F-distribution Where one degree of freedom is so large against the other that the F-distribution cannot be told from its limit in double precision, the quantile is taken from the limit, chi2inv (p, df1) / df1 as df2 grows and df2 / chi2inv (1 - p, df2) as df1 grows. Once one degree of freedom reaches 10^8 and the other is small, MATLAB returns quantiles whose probability is off by about 3%: its finv (0.025, 10, 1e8) is 0.32227, whose probability is 0.0243, where the quantile is 0.32470. Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: fcdf, fpdf, frnd, fstat # name: # type: sq_string # elements: 1 # length: 57 Inverse of the F-cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 4 fpdf # name: # type: sq_string # elements: 1 # length: 760 statistics: y = fpdf (x, df1, df2) F-probability density function (PDF). For each element of x, compute the probability density function (PDF) of the F-distribution with df1 and df2 degrees of freedom. The size of y is the common size of x, df1, and df2. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the F-distribution can be found at https://en.wikipedia.org/wiki/F-distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: fcdf, finv, frnd, fstat # name: # type: sq_string # elements: 1 # length: 37 F-probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 4 frnd # name: # type: sq_string # elements: 1 # length: 943 statistics: r = frnd (df1, df2) statistics: r = frnd (df1, df2, rows) statistics: r = frnd (df1, df2, rows, cols, ...) statistics: r = frnd (df1, df2, [sz]) Random arrays from the F-distribution. r = frnd (df1, df2) returns an array of random numbers chosen from the F-distribution with df1 and df2 degrees of freedom. The size of r is the common size of df1 and df2. A scalar input functions as a constant matrix of the same size as the other inputs. When called with a single size argument, frnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the F-distribution can be found at https://en.wikipedia.org/wiki/F-distribution See also: fcdf, finv, fpdf, fstat # name: # type: sq_string # elements: 1 # length: 38 Random arrays from the F-distribution. # name: # type: sq_string # elements: 1 # length: 6 gamcdf # name: # type: sq_string # elements: 1 # length: 2101 statistics: p = gamcdf (x, a) statistics: p = gamcdf (x, a, b) statistics: p = gamcdf (..., 'upper') statistics: [p, plo, pup] = gamcdf (x, a, b, pcov) statistics: [p, plo, pup] = gamcdf (x, a, b, pcov, alpha) statistics: [p, plo, pup] = gamcdf (..., 'upper') Gamma cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the Gamma distribution with shape parameter a and scale parameter b. When called with only one parameter, then b defaults to 1. The size of p is the common size of x, a, and b. A scalar input functions as a constant matrix of the same size as the other inputs. When called with three output arguments, i.e. [p, plo, pup], gamcdf computes the confidence bounds for p when the input parameters a and b are estimates. In such case, pcov, a 2x2 matrix containing the covariance matrix of the estimated parameters, is necessary. Optionally, alpha, which has a default value of 0.05, specifies the 100 * (1 - alpha) percent confidence bounds. plo and pup are arrays of the same size as p containing the lower and upper confidence bounds. [...] = gamcdf (..., "upper") computes the upper tail probability of the Gamma distribution with parameters a and b, at the values in x. OCTAVE/MATLAB use the alternative parameterization given by the pair α, β, i.e. shape a and scale b. In Wikipedia, the two common parameterizations use the pairs k, θ, as shape and scale, and α, β, as shape and rate, respectively. The parameter names a and b used here (for MATLAB compatibility) correspond to the parameter notation k, θ instead of the α, β as reported in Wikipedia. Further information about the Gamma distribution can be found at https://en.wikipedia.org/wiki/Gamma_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: gaminv, gampdf, gamrnd, gamfit, gamlike, gamstat # name: # type: sq_string # elements: 1 # length: 45 Gamma cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 6 gaminv # name: # type: sq_string # elements: 1 # length: 1216 statistics: x = gaminv (p, a, b) Inverse of the Gamma cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the Gamma distribution with shape parameter a and scale parameter b. The size of x is the common size of p, a, and b. A scalar input functions as a constant matrix of the same size as the other inputs. OCTAVE/MATLAB use the alternative parameterization given by the pair α, β, i.e. shape a and scale b. In Wikipedia, the two common parameterizations use the pairs k, θ, as shape and scale, and α, β, as shape and rate, respectively. The parameter names a and b used here (for MATLAB compatibility) correspond to the parameter notation k, θ instead of the α, β as reported in Wikipedia. Further information about the Gamma distribution can be found at https://en.wikipedia.org/wiki/Gamma_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: gamcdf, gampdf, gamrnd, gamfit, gamlike, gamstat # name: # type: sq_string # elements: 1 # length: 61 Inverse of the Gamma cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 6 gampdf # name: # type: sq_string # elements: 1 # length: 1196 statistics: y = gampdf (x, a, b) Gamma probability density function (PDF). For each element of x, compute the probability density function (PDF) of the Gamma distribution with shape parameter a and scale parameter b. The size of y is the common size of x, a and b. A scalar input functions as a constant matrix of the same size as the other inputs. OCTAVE/MATLAB use the alternative parameterization given by the pair α, β, i.e. shape a and scale b. In Wikipedia, the two common parameterizations use the pairs k, θ, as shape and scale, and α, β, as shape and rate, respectively. The parameter names a and b used here (for MATLAB compatibility) correspond to the parameter notation k, θ instead of the α, β as reported in Wikipedia. Further information about the Gamma distribution can be found at https://en.wikipedia.org/wiki/Gamma_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: gamcdf, gaminv, gamrnd, gamfit, gamlike, gamstat # name: # type: sq_string # elements: 1 # length: 41 Gamma probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 6 gamrnd # name: # type: sq_string # elements: 1 # length: 1374 statistics: r = gamrnd (a, b) statistics: r = gamrnd (a, b, rows) statistics: r = gamrnd (a, b, rows, cols, ...) statistics: r = gamrnd (a, b, [sz]) Random arrays from the Gamma distribution. r = gamrnd (a, b) returns an array of random numbers chosen from the Gamma distribution with shape parameter a and scale parameter b. The size of r is the common size of a and b. A scalar input functions as a constant matrix of the same size as the other inputs. When called with a single size argument, gamrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. OCTAVE/MATLAB use the alternative parameterization given by the pair α, β, i.e. shape a and scale b. In Wikipedia, the two common parameterizations use the pairs k, θ, as shape and scale, and α, β, as shape and rate, respectively. The parameter names a and b used here (for MATLAB compatibility) correspond to the parameter notation k, θ instead of the α, β as reported in Wikipedia. Further information about the Gamma distribution can be found at https://en.wikipedia.org/wiki/Gamma_distribution See also: gamcdf, gaminv, gampdf, gamfit, gamlike, gamstat # name: # type: sq_string # elements: 1 # length: 42 Random arrays from the Gamma distribution. # name: # type: sq_string # elements: 1 # length: 6 geocdf # name: # type: sq_string # elements: 1 # length: 1178 statistics: p = geocdf (x, ps) statistics: p = geocdf (x, ps, 'upper') Geometric cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the geometric distribution with probability of success parameter ps. The size of p is the common size of x and ps. A scalar input functions as a constant matrix of the same size as the other inputs. p = geocdf (x, ps, "upper") computes the upper tail probability of the geometric distribution with parameter ps, at the values in x. The geometric distribution models the number of failures (x) of a Bernoulli trial with probability ps before the first success. Further information about the geometric distribution can be found at https://en.wikipedia.org/wiki/Geometric_distribution Input arguments must be double, single, or an integer type; logical and character arrays are rejected. Integer input is promoted to double, so the result is always a probability. MATLAB is inconsistent here: for several of the discrete distributions it returns the result in the integer class of the input, truncating a probability to 0 or 1. See also: geoinv, geopdf, geornd, geofit, geostat # name: # type: sq_string # elements: 1 # length: 49 Geometric cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 6 geoinv # name: # type: sq_string # elements: 1 # length: 950 statistics: x = geoinv (p, ps) Inverse of the geometric cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the geometric distribution with probability of success parameter ps. The size of x is the common size of p and ps. A scalar input functions as a constant matrix of the same size as the other inputs. The geometric distribution models the number of failures (p) of a Bernoulli trial with probability ps before the first success. Further information about the geometric distribution can be found at https://en.wikipedia.org/wiki/Geometric_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: geocdf, geopdf, geornd, geofit, geostat # name: # type: sq_string # elements: 1 # length: 65 Inverse of the geometric cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 6 geopdf # name: # type: sq_string # elements: 1 # length: 996 statistics: y = geopdf (x, ps) Geometric probability density function (PDF). For each element of x, compute the probability density function (PDF) of the geometric distribution with probability of success parameter ps. The size of y is the common size of x and ps. A scalar input functions as a constant matrix of the same size as the other inputs. The geometric distribution models the number of failures (x) of a Bernoulli trial with probability ps before the first success. Further information about the geometric distribution can be found at https://en.wikipedia.org/wiki/Geometric_distribution Input arguments must be double, single, or an integer type; logical and character arrays are rejected. Integer input is promoted to double, so the result is always a probability. MATLAB is inconsistent here: for several of the discrete distributions it returns the result in the integer class of the input, truncating a probability to 0 or 1. See also: geocdf, geoinv, geornd, geofit, geostat # name: # type: sq_string # elements: 1 # length: 45 Geometric probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 6 geornd # name: # type: sq_string # elements: 1 # length: 1015 statistics: r = geornd (ps) statistics: r = geornd (ps, rows) statistics: r = geornd (ps, rows, cols, ...) statistics: r = geornd (ps, [sz]) Random arrays from the geometric distribution. r = geornd (ps) returns an array of random numbers chosen from the Birnbaum-Saunders distribution with probability of success parameter ps. The size of r is the size of ps. When called with a single size argument, geornd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. The geometric distribution models the number of failures (x) of a Bernoulli trial with probability ps before the first success. Further information about the geometric distribution can be found at https://en.wikipedia.org/wiki/Geometric_distribution See also: geocdf, geoinv, geopdf, geofit, geostat # name: # type: sq_string # elements: 1 # length: 46 Random arrays from the geometric distribution. # name: # type: sq_string # elements: 1 # length: 6 gevcdf # name: # type: sq_string # elements: 1 # length: 1792 statistics: p = gevcdf (x, k, sigma, mu) statistics: p = gevcdf (x, k, sigma, mu, 'upper') Generalized extreme value (GEV) cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the GEV distribution with shape parameter k, scale parameter sigma, and location parameter mu. The size of p is the common size of x, k, sigma, and mu. A scalar input functions as a constant matrix of the same size as the other inputs. [...] = gevcdf (x, k, sigma, mu, "upper") computes the upper tail probability of the GEV distribution with parameters k, sigma, and mu, at the values in x. When k < 0, the GEV is the type III extreme value distribution. When k > 0, the GEV distribution is the type II, or Frechet, extreme value distribution. If W has a Weibull distribution as computed by the wblcdf function, then -W has a type III extreme value distribution and 1/W has a type II extreme value distribution. In the limit as k approaches 0, the GEV is the mirror image of the type I extreme value distribution as computed by the evcdf function. The mean of the GEV distribution is not finite when k >= 1, and the variance is not finite when k >= 1/2. The GEV distribution has positive density only for values of x such that k * (x - mu) / sigma > -1. Further information about the generalized extreme value distribution can be found at https://en.wikipedia.org/wiki/Generalized_extreme_value_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: gevinv, gevpdf, gevrnd, gevfit, gevlike, gevstat # name: # type: sq_string # elements: 1 # length: 71 Generalized extreme value (GEV) cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 6 gevinv # name: # type: sq_string # elements: 1 # length: 1596 statistics: x = gevinv (p, k, sigma, mu) Inverse of the generalized extreme value (GEV) cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the GEV distribution with shape parameter k, scale parameter sigma, and location parameter mu. The size of p is the common size of x, k, sigma, and mu. A scalar input functions as a constant matrix of the same size as the other inputs. When k < 0, the GEV is the type III extreme value distribution. When k > 0, the GEV distribution is the type II, or Frechet, extreme value distribution. If W has a Weibull distribution as computed by the wblcdf function, then -W has a type III extreme value distribution and 1/W has a type II extreme value distribution. In the limit as k approaches 0, the GEV is the mirror image of the type I extreme value distribution as computed by the evcdf function. The mean of the GEV distribution is not finite when k >= 1, and the variance is not finite when k >= 1/2. The GEV distribution has positive density only for values of x such that k * (x - mu) / sigma > -1. Further information about the generalized extreme value distribution can be found at https://en.wikipedia.org/wiki/Generalized_extreme_value_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: gevcdf, gevpdf, gevrnd, gevfit, gevlike, gevstat # name: # type: sq_string # elements: 1 # length: 87 Inverse of the generalized extreme value (GEV) cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 6 gevpdf # name: # type: sq_string # elements: 1 # length: 1577 statistics: y = gevpdf (x, k, sigma, mu) Generalized extreme value (GEV) probability density function (PDF). For each element of x, compute the probability density function (PDF) of the GEV distribution with shape parameter k, scale parameter sigma, and location parameter mu. The size of y is the common size of x, k, sigma, and mu. A scalar input functions as a constant matrix of the same size as the other inputs. When k < 0, the GEV is the type III extreme value distribution. When k > 0, the GEV distribution is the type II, or Frechet, extreme value distribution. If W has a Weibull distribution as computed by the wblcdf function, then -W has a type III extreme value distribution and 1/W has a type II extreme value distribution. In the limit as k approaches 0, the GEV is the mirror image of the type I extreme value distribution as computed by the evcdf function. The mean of the GEV distribution is not finite when k >= 1, and the variance is not finite when k >= 1/2. The GEV distribution has positive density only for values of x such that k * (x - mu) / sigma > -1. Further information about the generalized extreme value distribution can be found at https://en.wikipedia.org/wiki/Generalized_extreme_value_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: gevcdf, gevinv, gevrnd, gevfit, gevlike, gevstat # name: # type: sq_string # elements: 1 # length: 67 Generalized extreme value (GEV) probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 6 gevrnd # name: # type: sq_string # elements: 1 # length: 1786 statistics: r = gevrnd (k, sigma, mu) statistics: r = gevrnd (k, sigma, mu, rows) statistics: r = gevrnd (k, sigma, mu, rows, cols, ...) statistics: r = gevrnd (k, sigma, mu, [sz]) Random arrays from the generalized extreme value (GEV) distribution. r = gevrnd (k, sigma, mu returns an array of random numbers chosen from the GEV distribution with shape parameter k, scale parameter sigma, and location parameter mu. The size of r is the common size of k, sigma, and mu. A scalar input functions as a constant matrix of the same size as the other inputs. When called with a single size argument, gevrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. When k < 0, the GEV is the type III extreme value distribution. When k > 0, the GEV distribution is the type II, or Frechet, extreme value distribution. If W has a Weibull distribution as computed by the wblcdf function, then -W has a type III extreme value distribution and 1/W has a type II extreme value distribution. In the limit as k approaches 0, the GEV is the mirror image of the type I extreme value distribution as computed by the evcdf function. The mean of the GEV distribution is not finite when k >= 1, and the variance is not finite when k >= 1/2. The GEV distribution has positive density only for values of x such that k * (x - mu) / sigma > -1. Further information about the generalized extreme value distribution can be found at https://en.wikipedia.org/wiki/Generalized_extreme_value_distribution See also: gevcdf, gevinv, gevpdf, gevfit, gevlike, gevstat # name: # type: sq_string # elements: 1 # length: 68 Random arrays from the generalized extreme value (GEV) distribution. # name: # type: sq_string # elements: 1 # length: 5 gpcdf # name: # type: sq_string # elements: 1 # length: 1572 statistics: p = gpcdf (x, k, sigma, theta) statistics: p = gpcdf (x, k, sigma, theta, 'upper') Generalized Pareto cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the generalized Pareto distribution with shape parameter k, scale parameter sigma, and location parameter theta. The size of p is the common size of x, k, sigma, and theta. A scalar input functions as a constant matrix of the same size as the other inputs. [...] = gpcdf(x, k, sigma, theta, "upper") computes the upper tail probability of the generalized Pareto distribution with parameters k, sigma, and theta, at the values in x. When k = 0 and theta = 0, the Generalized Pareto is equivalent to the exponential distribution. When k > 0 and theta = sigma / k the Generalized Pareto is equivalent to the Pareto distribution. The mean of the Generalized Pareto is not finite when k >= 1 and the variance is not finite when k >= 1/2. When k >= 0, the Generalized Pareto has positive density for x > theta, or, when theta < 0, for 0 <= (x - theta) / sigma <= -1 / k. Further information about the generalized Pareto distribution can be found at https://en.wikipedia.org/wiki/Generalized_Pareto_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: gpinv, gppdf, gprnd, gpfit, gplike, gpstat # name: # type: sq_string # elements: 1 # length: 58 Generalized Pareto cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 5 gpinv # name: # type: sq_string # elements: 1 # length: 1355 statistics: x = gpinv (p, k, sigma, theta) Inverse of the generalized Pareto cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the generalized Pareto distribution with shape parameter k, scale parameter sigma, and location parameter theta. The size of x is the common size of p, k, sigma, and theta. A scalar input functions as a constant matrix of the same size as the other inputs. When k = 0 and theta = 0, the Generalized Pareto is equivalent to the exponential distribution. When k > 0 and theta = sigma / k the Generalized Pareto is equivalent to the Pareto distribution. The mean of the Generalized Pareto is not finite when k >= 1 and the variance is not finite when k >= 1/2. When k >= 0, the Generalized Pareto has positive density for x > theta, or, when theta < 0, for 0 <= (x - theta) / sigma <= -1 / k. Further information about the generalized Pareto distribution can be found at https://en.wikipedia.org/wiki/Generalized_Pareto_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: gpcdf, gppdf, gprnd, gpfit, gplike, gpstat # name: # type: sq_string # elements: 1 # length: 74 Inverse of the generalized Pareto cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 5 gppdf # name: # type: sq_string # elements: 1 # length: 1879 statistics: y = gppdf (x, k, sigma, theta) Generalized Pareto probability density function (PDF). For each element of x, compute the probability density function (PDF) of the generalized Pareto distribution with shape parameter k, scale parameter sigma, and location parameter theta. The size of y is the common size of p, k, sigma, and theta. A scalar input functions as a constant matrix of the same size as the other inputs. When k = 0 and theta = 0, the Generalized Pareto is equivalent to the exponential distribution. When k > 0 and theta = sigma / k the Generalized Pareto is equivalent to the Pareto distribution. The mean of the Generalized Pareto is not finite when k >= 1 and the variance is not finite when k >= 1/2. When k >= 0, the Generalized Pareto has positive density for x > theta, or, when theta < 0, for 0 <= (x - theta) / sigma <= -1 / k. Further information about the generalized Pareto distribution can be found at https://en.wikipedia.org/wiki/Generalized_Pareto_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. With a negative shape parameter the support is the closed interval [theta, theta - sigma/k], and the density at its upper endpoint follows the limit of the density there: 0 for -1 < k < 0, 1/sigma at k = -1, and unbounded for k < -1. MATLAB returns 0 at that endpoint whatever the shape, which contradicts its own unifpdf: the generalized Pareto with k = -1 is the uniform distribution on [theta, theta + sigma], for which MATLAB's unifpdf returns 1/sigma at the same point. This implementation returns the limit, and so agrees with unifpdf. See also: gpcdf, gpinv, gprnd, gpfit, gplike, gpstat # name: # type: sq_string # elements: 1 # length: 54 Generalized Pareto probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 5 gprnd # name: # type: sq_string # elements: 1 # length: 1553 statistics: r = gprnd (k, sigma, theta) statistics: r = gprnd (k, sigma, theta, rows) statistics: r = gprnd (k, sigma, theta, rows, cols, ...) statistics: r = gprnd (k, sigma, theta, [sz]) Random arrays from the generalized Pareto distribution. r = gprnd (k, sigma, theta) returns an array of random numbers chosen from the generalized Pareto distribution with shape parameter k, scale parameter sigma, and location parameter theta. The size of r is the common size of k, sigma, and theta. A scalar input functions as a constant matrix of the same size as the other inputs. When called with a single size argument, gprnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. When k = 0 and theta = 0, the Generalized Pareto is equivalent to the exponential distribution. When k > 0 and theta = sigma / k the Generalized Pareto is equivalent to the Pareto distribution. The mean of the Generalized Pareto is not finite when k >= 1 and the variance is not finite when k >= 1/2. When k >= 0, the Generalized Pareto has positive density for x > theta, or, when theta < 0, for 0 <= (x - theta) / sigma <= -1 / k. Further information about the generalized Pareto distribution can be found at https://en.wikipedia.org/wiki/Generalized_Pareto_distribution See also: gpcdf, gpinv, gppdf, gpfit, gplike, gpstat # name: # type: sq_string # elements: 1 # length: 55 Random arrays from the generalized Pareto distribution. # name: # type: sq_string # elements: 1 # length: 9 gumbelcdf # name: # type: sq_string # elements: 1 # length: 2259 statistics: p = gumbelcdf (x) statistics: p = gumbelcdf (x, mu) statistics: p = gumbelcdf (x, mu, beta) statistics: p = gumbelcdf (..., 'upper') statistics: [p, plo, pup] = gumbelcdf (x, mu, beta, pcov) statistics: [p, plo, pup] = gumbelcdf (x, mu, beta, pcov, alpha) statistics: [p, plo, pup] = gumbelcdf (..., 'upper') Gumbel cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the Gumbel distribution (also known as the extreme value or the type I generalized extreme value distribution) with location parameter mu and scale parameter beta. The size of p is the common size of x, mu and beta. A scalar input functions as a constant matrix of the same size as the other inputs. Default values are mu = 0 and beta = 1. When called with three output arguments, i.e. [p, plo, pup], gumbelcdf computes the confidence bounds for p when the input parameters mu and beta are estimates. In such case, pcov, a 2x2 matrix containing the covariance matrix of the estimated parameters, is necessary. Optionally, alpha, which has a default value of 0.05, specifies the 100 * (1 - alpha) percent confidence bounds. plo and pup are arrays of the same size as p containing the lower and upper confidence bounds. [...] = gumbelcdf (..., "upper") computes the upper tail probability of the Gumbel distribution with parameters mu and beta, at the values in x. The Gumbel distribution is used to model the distribution of the maximum (or the minimum) of a number of samples of various distributions. This version is suitable for modeling maxima. For modeling minima, use the alternative extreme value CDF, evcdf. [...] = gumbelcdf (..., "upper") computes the upper tail probability of the extreme value (Gumbel) distribution. Further information about the Gumbel distribution can be found at https://en.wikipedia.org/wiki/Gumbel_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: gumbelinv, gumbelpdf, gumbelrnd, gumbelfit, gumbellike, gumbelstat, evcdf # name: # type: sq_string # elements: 1 # length: 46 Gumbel cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 9 gumbelinv # name: # type: sq_string # elements: 1 # length: 1917 statistics: x = gumbelinv (p) statistics: x = gumbelinv (p, mu) statistics: x = gumbelinv (p, mu, beta) statistics: [x, xlo, xup] = gumbelinv (p, mu, beta, pcov) statistics: [x, xlo, xup] = gumbelinv (p, mu, beta, pcov, alpha) Inverse of the Gumbel cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the Gumbel distribution (also known as the extreme value or the type I generalized extreme value distribution) with location parameter mu and scale parameter beta. The size of x is the common size of p, mu and beta. A scalar input functions as a constant matrix of the same size as the other inputs. Default values are mu = 0 and beta = 1. When called with three output arguments, i.e. [x, xlo, xup], gumbelinv computes the confidence bounds for x when the input parameters mu and beta are estimates. In such case, pcov, a 2x2 matrix containing the covariance matrix of the estimated parameters, is necessary. Optionally, alpha, which has a default value of 0.05, specifies the 100 * (1 - alpha) percent confidence bounds. xlo and xup are arrays of the same size as x containing the lower and upper confidence bounds. The Gumbel distribution is used to model the distribution of the maximum (or the minimum) of a number of samples of various distributions. This version is suitable for modeling maxima. For modeling minima, use the alternative extreme value iCDF, evinv. Further information about the Gumbel distribution can be found at https://en.wikipedia.org/wiki/Gumbel_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: gumbelcdf, gumbelpdf, gumbelrnd, gumbelfit, gumbellike, gumbelstat, evinv # name: # type: sq_string # elements: 1 # length: 62 Inverse of the Gumbel cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 9 gumbelpdf # name: # type: sq_string # elements: 1 # length: 1296 statistics: y = gumbelpdf (x) statistics: y = gumbelpdf (x, mu) statistics: y = gumbelpdf (x, mu, beta) Gumbel probability density function (PDF). For each element of x, compute the probability density function (PDF) of the Gumbel distribution (also known as the extreme value or the type I generalized extreme value distribution) with location parameter mu and scale parameter beta. The size of y is the common size of x, mu and beta. A scalar input functions as a constant matrix of the same size as the other inputs. Default values are mu = 0 and beta = 1. The Gumbel distribution is used to model the distribution of the maximum (or the minimum) of a number of samples of various distributions. This version is suitable for modeling maxima. For modeling minima, use the alternative extreme value iCDF, evpdf. Further information about the Gumbel distribution can be found at https://en.wikipedia.org/wiki/Gumbel_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: gumbelcdf, gumbelinv, gumbelrnd, gumbelfit, gumbellike, gumbelstat, evpdf # name: # type: sq_string # elements: 1 # length: 42 Gumbel probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 9 gumbelrnd # name: # type: sq_string # elements: 1 # length: 1400 statistics: r = gumbelrnd (mu, beta) statistics: r = gumbelrnd (mu, beta, rows) statistics: r = gumbelrnd (mu, beta, rows, cols, ...) statistics: r = gumbelrnd (mu, beta, [sz]) Random arrays from the Gumbel distribution. r = gumbelrnd (mu, beta) returns an array of random numbers chosen from the Gumbel distribution (also known as the extreme value or the type I generalized extreme value distribution) with location parameter mu and scale parameter beta. The size of r is the common size of mu and beta. A scalar input functions as a constant matrix of the same size as the other inputs. When called with a single size argument, gumbelrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. The Gumbel distribution is used to model the distribution of the maximum (or the minimum) of a number of samples of various distributions. This version is suitable for modeling maxima. For modeling minima, use the alternative extreme value iCDF, evinv. Further information about the Gumbel distribution can be found at https://en.wikipedia.org/wiki/Gumbel_distribution See also: gumbelcdf, gumbelinv, gumbelpdf, gumbelfit, gumbellike, gumbelstat, evrnd # name: # type: sq_string # elements: 1 # length: 43 Random arrays from the Gumbel distribution. # name: # type: sq_string # elements: 1 # length: 5 hncdf # name: # type: sq_string # elements: 1 # length: 1099 statistics: p = hncdf (x, mu, sigma) statistics: p = hncdf (x, mu, sigma, 'upper') Half-normal cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the half-normal distribution with location parameter mu and scale parameter sigma. The size of p is the common size of x, mu and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. [...] = hncdf (x, mu, sigma, "upper") computes the upper tail probability of the half-normal distribution with parameters mu and sigma, at the values in x. The half-normal CDF is only defined for x >= mu. Further information about the half-normal distribution can be found at https://en.wikipedia.org/wiki/Half-normal_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: hninv, hnpdf, hnrnd, hnfit, hnlike, hnstat # name: # type: sq_string # elements: 1 # length: 51 Half-normal cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 5 hninv # name: # type: sq_string # elements: 1 # length: 858 statistics: x = hninv (p, mu, sigma) Inverse of the half-normal cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the half-normal distribution with location parameter mu and scale parameter sigma. The size of x is the common size of p, mu, and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the half-normal distribution can be found at https://en.wikipedia.org/wiki/Half-normal_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: hncdf, hnpdf, hnrnd, hnfit, hnlike, hnstat # name: # type: sq_string # elements: 1 # length: 67 Inverse of the half-normal cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 5 hnpdf # name: # type: sq_string # elements: 1 # length: 889 statistics: y = hnpdf (x, mu, sigma) Half-normal probability density function (PDF). For each element of x, compute the probability density function (PDF) of the half-normal distribution with location parameter mu and scale parameter sigma. The size of y is the common size of x, mu, and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. The half-normal CDF is only defined for x >= mu. Further information about the half-normal distribution can be found at https://en.wikipedia.org/wiki/Half-normal_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: hncdf, hninv, hnrnd, hnfit, hnlike, hnstat # name: # type: sq_string # elements: 1 # length: 47 Half-normal probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 5 hnrnd # name: # type: sq_string # elements: 1 # length: 1031 statistics: r = hnrnd (mu, sigma) statistics: r = hnrnd (mu, sigma, rows) statistics: r = hnrnd (mu, sigma, rows, cols, ...) statistics: r = hnrnd (mu, sigma, [sz]) Random arrays from the half-normal distribution. r = hnrnd (mu, sigma) returns an array of random numbers chosen from the half-normal distribution with location parameter mu and scale parameter sigma. The size of r is the common size of mu and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. When called with a single size argument, hnrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the half-normal distribution can be found at https://en.wikipedia.org/wiki/Half-normal_distribution See also: hncdf, hninv, hnpdf, hnfit, hnlike, hnstat # name: # type: sq_string # elements: 1 # length: 48 Random arrays from the half-normal distribution. # name: # type: sq_string # elements: 1 # length: 7 hygecdf # name: # type: sq_string # elements: 1 # length: 1381 statistics: p = hygecdf (x, m, k, n) statistics: p = hygecdf (x, m, k, n, 'upper') Hypergeometric cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the hypergeometric distribution with parameters m, k, and n. The size of p is the common size of x, m, k, and n. A scalar input functions as a constant matrix of the same size as the other inputs. This is the cumulative probability of obtaining not more than x marked items when randomly drawing a sample of size n without replacement from a population of total size m containing k marked items. The parameters m, k, and n must be positive integers with k and n not greater than m. [...] = hygecdf (x, m, k, n, "upper") computes the upper tail probability of the hypergeometric distribution with parameters m, k, and n, at the values in x. Further information about the hypergeometric distribution can be found at https://en.wikipedia.org/wiki/Hypergeometric_distribution Input arguments must be double, single, or an integer type; logical and character arrays are rejected. Integer input is promoted to double, so the result is always a probability. MATLAB is inconsistent here: for several of the discrete distributions it returns the result in the integer class of the input, truncating a probability to 0 or 1. See also: hygeinv, hygepdf, hygernd, hygestat # name: # type: sq_string # elements: 1 # length: 54 Hypergeometric cumulative distribution function (CDF). # name: # type: string # elements: 1 # length: 7 hygeinv # name: # type: sq_string # elements: 1 # length: 1312 statistics: x = hygeinv (p, m, k, n) Inverse of the hypergeometric cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the hypergeometric distribution with parameters m, k, and n. The size of x is the common size of p, m, k, and n. A scalar input functions as a constant matrix of the same size as the other inputs. This is the number of drawn marked items x given a probability p, when randomly drawing a sample of size n without replacement from a population of total size m containing k marked items. The parameters m, k, and n must be non-negative integers with k and n not greater than m, and m finite unless n is 0. For p = 0 and p = 1, x is the least and the greatest number of marked items the sample can hold, max (0, n - (m - k)) and min (n, k). MATLAB does not return for p = 0. Further information about the hypergeometric distribution can be found at https://en.wikipedia.org/wiki/Hypergeometric_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: hygecdf, hygepdf, hygernd, hygestat # name: # type: sq_string # elements: 1 # length: 70 Inverse of the hypergeometric cumulative distribution function (iCDF). # name: # type: string # elements: 1 # length: 7 hygepdf # name: # type: sq_string # elements: 1 # length: 1656 statistics: y = hygepdf (x, m, k, n) statistics: y = hygepdf (..., 'vectorexpand') Hypergeometric probability density function (PDF). For each element of x, compute the probability density function (PDF) of the hypergeometric distribution with parameters m, k, and n. The size of y is the common size of x, m, k, and n. A scalar input functions as a constant matrix of the same size as the other inputs. This is the probability of obtaining x marked items when randomly drawing a sample of size n without replacement from a population of total size m containing k marked items. The parameters m, k, and n must be non-negative integers with k and n not greater than m. A sample of size 0 contains no marked items, so x = 0 then has probability 1, even from an empty population. If the optional parameter vectorexpand is provided, x may be an array with size different from parameters m, k, and n (which must still be of a common size or scalar). Each element of x will be evaluated against each set of parameters m, k, and n in columnwise order. The output y will be an array of size r x s, where r = numel (m), and s = numel (x). Further information about the hypergeometric distribution can be found at https://en.wikipedia.org/wiki/Hypergeometric_distribution Input arguments must be double, single, or an integer type; logical and character arrays are rejected. Integer input is promoted to double, so the result is always a probability. MATLAB is inconsistent here: for several of the discrete distributions it returns the result in the integer class of the input, truncating a probability to 0 or 1. See also: hygecdf, hygeinv, hygernd, hygestat # name: # type: sq_string # elements: 1 # length: 50 Hypergeometric probability density function (PDF). # name: # type: string # elements: 1 # length: 7 hygernd # name: # type: sq_string # elements: 1 # length: 1164 statistics: r = hygernd (m, k, n) statistics: r = hygernd (m, k, n, rows) statistics: r = hygernd (m, k, n, rows, cols, ...) statistics: r = hygernd (m, k, n, [sz]) Random arrays from the hypergeometric distribution. r = hygernd (m, k, n) returns an array of random numbers chosen from the hypergeometric distribution with parameters m, k, and n. The size of r is the common size of m, k, and n. A scalar input functions as a constant matrix of the same size as the other inputs. The parameters m, k, and n must be non-negative integers with k and n not greater than m. A sample of size 0 contains no marked items, so r is then 0. When called with a single size argument, hygernd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the hypergeometric distribution can be found at https://en.wikipedia.org/wiki/Hypergeometric_distribution See also: hygecdf, hygeinv, hygepdf, hygestat # name: # type: sq_string # elements: 1 # length: 51 Random arrays from the hypergeometric distribution. # name: # type: sq_string # elements: 1 # length: 7 invgcdf # name: # type: sq_string # elements: 1 # length: 1160 statistics: p = invgcdf (x, mu, lambda) statistics: p = invgcdf (x, mu, lambda, 'upper') Inverse Gaussian cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the inverse Gaussian distribution with scale parameter mu and shape parameter lambda. The size of p is the common size of x, mu and lambda. A scalar input functions as a constant matrix of the same size as the other inputs. p = invgcdf (x, mu, lambda, "upper") computes the upper tail probability of the inverse Gaussian distribution with parameters mu and lambda, at the values in x. The inverse Gaussian CDF is only defined for mu > 0 and lambda > 0. Further information about the inverse Gaussian distribution can be found at https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: invginv, invgpdf, invgrnd, invgfit, invglike, invgstat # name: # type: sq_string # elements: 1 # length: 56 Inverse Gaussian cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 7 invginv # name: # type: sq_string # elements: 1 # length: 961 statistics: x = invginv (p, mu, lambda) Inverse of the inverse Gaussian cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the inverse Gaussian distribution with scale parameter mu and shape parameter lambda. The size of x is the common size of p, mu, and lambda. A scalar input functions as a constant matrix of the same size as the other inputs. The inverse Gaussian CDF is only defined for mu > 0 and lambda > 0. Further information about the inverse Gaussian distribution can be found at https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: invgcdf, invgpdf, invgrnd, invgfit, invglike, invgstat # name: # type: sq_string # elements: 1 # length: 72 Inverse of the inverse Gaussian cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 7 invgpdf # name: # type: sq_string # elements: 1 # length: 942 statistics: y = invgpdf (x, mu, lambda) Inverse Gaussian probability density function (PDF). For each element of x, compute the probability density function (PDF) of the inverse Gaussian distribution with scale parameter mu and shape parameter lambda. The size of y is the common size of x, mu, and lambda. A scalar input functions as a constant matrix of the same size as the other inputs. The inverse Gaussian CDF is only defined for mu > 0 and lambda > 0. Further information about the inverse Gaussian distribution can be found at https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: invgcdf, invginv, invgrnd, invgfit, invglike, invgstat # name: # type: sq_string # elements: 1 # length: 52 Inverse Gaussian probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 7 invgrnd # name: # type: sq_string # elements: 1 # length: 1151 statistics: r = invgrnd (mu, lambda) statistics: r = invgrnd (mu, lambda, rows) statistics: r = invgrnd (mu, lambda, rows, cols, ...) statistics: r = invgrnd (mu, lambda, [sz]) Random arrays from the inverse Gaussian distribution. r = invgrnd (mu, lambda) returns an array of random numbers chosen from the inverse Gaussian distribution with location parameter mu and scale parameter lambda. The size of r is the common size of mu and lambda. A scalar input functions as a constant matrix of the same size as the other inputs. When called with a single size argument, invgrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. The inverse Gaussian CDF is only defined for mu > 0 and lambda > 0. Further information about the inverse Gaussian distribution can be found at https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution See also: invgcdf, invginv, invgpdf, invgfit, invglike, invgstat # name: # type: sq_string # elements: 1 # length: 53 Random arrays from the inverse Gaussian distribution. # name: # type: sq_string # elements: 1 # length: 8 iwishpdf # name: # type: sq_string # elements: 1 # length: 897 statistics: y = iwishpdf (W, Tau, df, log_y=false) Compute the probability density function of the inverse Wishart distribution. Inputs: A p x p matrix W where to find the PDF and the p x p positive definite scale matrix Tau and scalar degrees of freedom parameter df characterizing the inverse Wishart distribution. (For the density to be finite, need df > (p - 1).) If the flag log_y is set, return the log probability density -- this helps avoid underflow when the numerical value of the density is very small. Output: y is the probability density of Wishart(Sigma, df) at W. Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: iwishrnd, wishpdf, wishrnd # name: # type: sq_string # elements: 1 # length: 77 Compute the probability density function of the inverse Wishart distribution. # name: # type: sq_string # elements: 1 # length: 8 iwishrnd # name: # type: sq_string # elements: 1 # length: 944 statistics: [W, DI] = iwishrnd (Tau, df, DI, n=1) Return a random matrix sampled from the inverse Wishart distribution with given parameters. Inputs: the p x p positive definite matrix Tau and scalar degrees of freedom parameter df (and optionally the transposed Cholesky factor DI of Sigma = inv(Tau)). df can be non-integer as long as df > d Output: a random p x p matrix W from the inverse Wishart(Tau, df) distribution. (inv(W) is from the Wishart(inv(Tau), df) distribution.) If n > 1, then W is p x p x n and holds n such random matrices. (Optionally, the transposed Cholesky factor DI of Sigma is also returned.) Averaged across many samples, the mean of W should approach Tau / (df - p - 1). References Yu-Cheng Ku and Peter Bloomfield (2010), Generating Random Wishart Matrices with Fractional Degrees of Freedom in OX, http://www.gwu.edu/~forcpgm/YuChengKu-030510final-WishartYu-ChengKu.pdf See also: iwishpdf, wishpdf, wishrnd # name: # type: sq_string # elements: 1 # length: 91 Return a random matrix sampled from the inverse Wishart distribution with given parameters. # name: # type: sq_string # elements: 1 # length: 6 jsucdf # name: # type: sq_string # elements: 1 # length: 920 statistics: p = jsucdf (x) statistics: p = jsucdf (x, alpha1) statistics: p = jsucdf (x, alpha1, alpha2) Johnson SU cumulative distribution function (CDF). For each element of x, return the cumulative distribution functions (CDF) at x of the Johnson SU distribution with shape parameters alpha1 and alpha2, which is normcdf (alpha1 + alpha2 * asinh (x)). The size of p is the common size of the input arguments x, alpha1, and alpha2. A scalar input functions as a constant matrix of the same size as the other inputs. Default values are alpha1 = 1, alpha2 = 1. alpha2 must be positive; where it is not, the result is NaN. Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: jsupdf # name: # type: sq_string # elements: 1 # length: 50 Johnson SU cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 6 jsupdf # name: # type: sq_string # elements: 1 # length: 938 statistics: y = jsupdf (x) statistics: y = jsupdf (x, alpha1) statistics: y = jsupdf (x, alpha1, alpha2) Johnson SU probability density function (PDF). For each element of x, compute the probability density function (PDF) at x of the Johnson SU distribution with shape parameters alpha1 and alpha2, which is alpha2 * normpdf (alpha1 + alpha2 * asinh (x)) / sqrt (x^2 + 1). The size of y is the common size of the input arguments x, alpha1, and alpha2. A scalar input functions as a constant matrix of the same size as the other inputs. Default values are alpha1 = 1, alpha2 = 1. alpha2 must be positive; where it is not, the result is NaN. Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: jsucdf # name: # type: sq_string # elements: 1 # length: 46 Johnson SU probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 10 laplacecdf # name: # type: sq_string # elements: 1 # length: 1123 statistics: p = laplacecdf (x, mu, beta) statistics: p = laplacecdf (x, mu, beta, 'upper') Laplace cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the Laplace distribution with location parameter mu and scale parameter (i.e. "diversity") beta. The size of p is the common size of x, mu, and beta. A scalar input functions as a constant matrix of the same size as the other inputs. Both parameters must be reals and beta > 0. For beta <= 0, NaN is returned. p = laplacecdf (x, mu, beta, "upper") computes the upper tail probability of the Laplace distribution with parameters mu and beta, at the values in x. Further information about the Laplace distribution can be found at https://en.wikipedia.org/wiki/Laplace_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: laplaceinv, laplacepdf, laplacernd # name: # type: sq_string # elements: 1 # length: 47 Laplace cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 10 laplaceinv # name: # type: sq_string # elements: 1 # length: 932 statistics: x = laplaceinv (p, mu, beta) Inverse of the Laplace cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the Laplace distribution with location parameter mu and scale parameter (i.e. "diversity") beta. The size of x is the common size of p, mu, and beta. A scalar input functions as a constant matrix of the same size as the other inputs. Both parameters must be reals and beta > 0. For beta <= 0, NaN is returned. Further information about the Laplace distribution can be found at https://en.wikipedia.org/wiki/Laplace_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: laplacecdf, laplacepdf, laplacernd # name: # type: sq_string # elements: 1 # length: 63 Inverse of the Laplace cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 10 laplacepdf # name: # type: sq_string # elements: 1 # length: 913 statistics: y = laplacepdf (x, mu, beta) Laplace probability density function (PDF). For each element of x, compute the probability density function (PDF) of the Laplace distribution with location parameter mu and scale parameter (i.e. "diversity") beta. The size of y is the common size of x, mu, and beta. A scalar input functions as a constant matrix of the same size as the other inputs. Both parameters must be reals and beta > 0. For beta <= 0, NaN is returned. Further information about the Laplace distribution can be found at https://en.wikipedia.org/wiki/Laplace_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: laplacecdf, laplaceinv, laplacernd # name: # type: sq_string # elements: 1 # length: 43 Laplace probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 10 laplacernd # name: # type: sq_string # elements: 1 # length: 1107 statistics: r = laplacernd (mu, beta) statistics: r = laplacernd (mu, beta, rows) statistics: r = laplacernd (mu, beta, rows, cols, ...) statistics: r = laplacernd (mu, beta, [sz]) Random arrays from the Laplace distribution. r = laplacernd (mu, beta) returns an array of random numbers chosen from the Laplace distribution with location parameter mu and scale parameter beta. The size of r is the common size of mu and beta. A scalar input functions as a constant matrix of the same size as the other inputs. Both parameters must be reals and beta > 0. For beta <= 0, NaN is returned. When called with a single size argument, laplacernd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the Laplace distribution can be found at https://en.wikipedia.org/wiki/Laplace_distribution See also: laplacecdf, laplaceinv, laplacepdf # name: # type: sq_string # elements: 1 # length: 44 Random arrays from the Laplace distribution. # name: # type: sq_string # elements: 1 # length: 7 logicdf # name: # type: sq_string # elements: 1 # length: 1128 statistics: p = logicdf (x, mu, sigma) statistics: p = logicdf (x, mu, sigma, 'upper') Logistic cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the logistic distribution with location parameter mu and scale parameter sigma. The size of p is the common size of x, mu, and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Both parameters must be reals and sigma > 0. For sigma <= 0, NaN is returned. p = logicdf (x, mu, sigma, "upper") computes the upper tail probability of the logistic distribution with parameters mu and sigma, at the values in x. Further information about the logistic distribution can be found at https://en.wikipedia.org/wiki/Logistic_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: logiinv, logipdf, logirnd, logifit, logilike, logistat # name: # type: sq_string # elements: 1 # length: 48 Logistic cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 7 logiinv # name: # type: sq_string # elements: 1 # length: 939 statistics: x = logiinv (p, mu, sigma) Inverse of the logistic cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the logistic distribution with location parameter mu and scale parameter sigma. The size of p is the common size of x, mu, and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Both parameters must be reals and sigma > 0. For sigma <= 0, NaN is returned. Further information about the logistic distribution can be found at https://en.wikipedia.org/wiki/Logistic_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: logicdf, logipdf, logirnd, logifit, logilike, logistat # name: # type: sq_string # elements: 1 # length: 64 Inverse of the logistic cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 7 logipdf # name: # type: sq_string # elements: 1 # length: 920 statistics: y = logipdf (x, mu, sigma) Logistic probability density function (PDF). For each element of x, compute the probability density function (PDF) of the logistic distribution with location parameter mu and scale parameter sigma. The size of p is the common size of x, mu, and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Both parameters must be reals and sigma > 0. For sigma <= 0, NaN is returned. Further information about the logistic distribution can be found at https://en.wikipedia.org/wiki/Logistic_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: logicdf, logiinv, logirnd, logifit, logilike, logistat # name: # type: sq_string # elements: 1 # length: 44 Logistic probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 7 logirnd # name: # type: sq_string # elements: 1 # length: 1122 statistics: r = logirnd (mu, sigma) statistics: r = logirnd (mu, sigma, rows) statistics: r = logirnd (mu, sigma, rows, cols, ...) statistics: r = logirnd (mu, sigma, [sz]) Random arrays from the logistic distribution. r = logirnd (mu, sigma) returns an array of random numbers chosen from the logistic distribution with location parameter mu and scale parameter sigma. The size of r is the common size of mu and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Both parameters must be reals and sigma > 0. For sigma <= 0, NaN is returned. When called with a single size argument, logirnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the logistic distribution can be found at https://en.wikipedia.org/wiki/Logistic_distribution See also: logicdf, logiinv, logipdf, logifit, logilike, logistat # name: # type: sq_string # elements: 1 # length: 45 Random arrays from the logistic distribution. # name: # type: sq_string # elements: 1 # length: 7 loglcdf # name: # type: sq_string # elements: 1 # length: 1514 statistics: p = loglcdf (x, mu, sigma) statistics: p = loglcdf (x, mu, sigma, 'upper') Loglogistic cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the loglogistic distribution with mean parameter mu and scale parameter sigma. The size of p is the common size of x, mu, and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Mean of logarithmic values mu must be a non-negative real value, scale parameter of logarithmic values sigma must be a positive real value and x is supported in the range [0,Inf), otherwise NaN is returned. p = loglcdf (x, mu, sigma, "upper") computes the upper tail probability of the log-logistic distribution with parameters mu and sigma, at the values in x. Further information about the loglogistic distribution can be found at https://en.wikipedia.org/wiki/Log-logistic_distribution OCTAVE/MATLAB use an alternative parameterization given by the pair μ, σ, i.e. mu and sigma, in analogy with the logistic distribution. Their relation to the α and b parameters used in Wikipedia are given below: mu = log (a) sigma = 1 / a Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: loglinv, loglpdf, loglrnd, loglfit, logllike, loglstat # name: # type: sq_string # elements: 1 # length: 51 Loglogistic cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 7 loglinv # name: # type: sq_string # elements: 1 # length: 1321 statistics: x = loglinv (p, mu, sigma) Inverse of the log-logistic cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the log-logistic distribution with mean parameter mu and scale parameter sigma. The size of x is the common size of p, mu, and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Mean of logarithmic values mu must be a non-negative real value, scale parameter of logarithmic values sigma must be a positive real value and p is supported in the range [0,1], otherwise NaN is returned. Further information about the loglogistic distribution can be found at https://en.wikipedia.org/wiki/Log-logistic_distribution OCTAVE/MATLAB use an alternative parameterization given by the pair μ, σ, i.e. mu and sigma, in analogy with the logistic distribution. Their relation to the α and b parameters used in Wikipedia are given below: mu = log (a) sigma = 1 / a Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: loglcdf, loglpdf, loglrnd, loglfit, logllike, loglstat # name: # type: sq_string # elements: 1 # length: 68 Inverse of the log-logistic cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 7 loglpdf # name: # type: sq_string # elements: 1 # length: 1300 statistics: y = loglpdf (x, mu, sigma) Loglogistic probability density function (PDF). For each element of x, compute the probability density function (PDF) of the loglogistic distribution with mean parameter mu and scale parameter sigma. The size of y is the common size of x, mu, and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Mean of logarithmic values mu must be a non-negative real value, scale parameter of logarithmic values sigma must be a positive real value and x is supported in the range [0,Inf), otherwise 0 is returned. Further information about the loglogistic distribution can be found at https://en.wikipedia.org/wiki/Log-logistic_distribution OCTAVE/MATLAB use an alternative parameterization given by the pair μ, σ, i.e. mu and sigma, in analogy with the logistic distribution. Their relation to the α and b parameters used in Wikipedia are given below: mu = log (a) sigma = 1 / a Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: loglcdf, loglinv, loglrnd, loglfit, logllike, loglstat # name: # type: sq_string # elements: 1 # length: 47 Loglogistic probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 7 loglrnd # name: # type: sq_string # elements: 1 # length: 1443 statistics: r = loglrnd (mu, sigma) statistics: r = loglrnd (mu, sigma, rows) statistics: r = loglrnd (mu, sigma, rows, cols, ...) statistics: r = loglrnd (mu, sigma, [sz]) Random arrays from the loglogistic distribution. r = loglrnd (mu, sigma) returns an array of random numbers chosen from the loglogistic distribution with mean parameter mu and scale parameter sigma. The size of r is the common size of mu and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Mean of logarithmic values mu must be a non-negative real value and scale parameter of logarithmic values sigma must be a positive real value. When called with mu single size argument, loglrnd returns mu square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with mu row vector of dimensions, sz. Further information about the loglogistic distribution can be found at https://en.wikipedia.org/wiki/Log-logistic_distribution OCTAVE/MATLAB use an alternative parameterization given by the pair μ, σ, i.e. mu and sigma, in analogy with the logistic distribution. Their relation to the α and b parameters used in Wikipedia are given below: mu = log (a) sigma = 1 / a See also: loglcdf, loglinv, loglpdf, loglfit, logllike, loglstat # name: # type: sq_string # elements: 1 # length: 48 Random arrays from the loglogistic distribution. # name: # type: sq_string # elements: 1 # length: 7 logncdf # name: # type: sq_string # elements: 1 # length: 2079 statistics: p = logncdf (x) statistics: p = logncdf (x, mu) statistics: p = logncdf (x, mu, sigma) statistics: p = logncdf (..., 'upper') statistics: [p, plo, pup] = logncdf (x, mu, sigma, pcov) statistics: [p, plo, pup] = logncdf (x, mu, sigma, pcov, alpha) statistics: [p, plo, pup] = logncdf (..., 'upper') Lognormal cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the lognormal distribution with mean parameter mu and standard deviation parameter sigma, each corresponding to the associated normal distribution. The size of p is the common size of x, mu, and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. If a random variable follows this distribution, its logarithm is normally distributed with mean mu and standard deviation sigma. Default parameter values are mu = 0 and sigma = 1. Both parameters must be reals and sigma > 0. For sigma <= 0, NaN is returned. When called with three output arguments, i.e. [p, plo, pup], logncdf computes the confidence bounds for p when the input parameters mu and sigma are estimates. In such case, pcov, a 2x2 matrix containing the covariance matrix of the estimated parameters, is necessary. Optionally, alpha, which has a default value of 0.05, specifies the 100 * (1 - alpha) percent confidence bounds. plo and pup are arrays of the same size as p containing the lower and upper confidence bounds. [...] = logncdf (..., "upper") computes the upper tail probability of the log-normal distribution with parameters mu and sigma, at the values in x. Further information about the lognormal distribution can be found at https://en.wikipedia.org/wiki/Log-normal_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: logninv, lognpdf, lognrnd, lognfit, lognlike, lognstat # name: # type: sq_string # elements: 1 # length: 49 Lognormal cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 7 logninv # name: # type: sq_string # elements: 1 # length: 1252 statistics: x = logninv (p) statistics: x = logninv (p, mu) statistics: x = logninv (p, mu, sigma) Inverse of the lognormal cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the lognormal distribution with mean parameter mu and standard deviation parameter sigma, each corresponding to the associated normal distribution. The size of x is the common size of p, mu, and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. If a random variable follows this distribution, its logarithm is normally distributed with mean mu and standard deviation sigma. Default parameter values are mu = 0 and sigma = 1. Both parameters must be reals and sigma > 0. For sigma <= 0, NaN is returned. Further information about the lognormal distribution can be found at https://en.wikipedia.org/wiki/Log-normal_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: logncdf, lognpdf, lognrnd, lognfit, lognlike, lognstat # name: # type: sq_string # elements: 1 # length: 65 Inverse of the lognormal cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 7 lognpdf # name: # type: sq_string # elements: 1 # length: 1233 statistics: y = lognpdf (x) statistics: y = lognpdf (x, mu) statistics: y = lognpdf (x, mu, sigma) Lognormal probability density function (PDF). For each element of x, compute the probability density function (PDF) of the lognormal distribution with mean parameter mu and standard deviation parameter sigma, each corresponding to the associated normal distribution. The size of y is the common size of p, mu, and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. If a random variable follows this distribution, its logarithm is normally distributed with mean mu and standard deviation sigma. Default parameter values are mu = 0 and sigma = 1. Both parameters must be reals and sigma > 0. For sigma <= 0, NaN is returned. Further information about the lognormal distribution can be found at https://en.wikipedia.org/wiki/Log-normal_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: logncdf, logninv, lognrnd, lognfit, lognlike, lognstat # name: # type: sq_string # elements: 1 # length: 45 Lognormal probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 7 lognrnd # name: # type: sq_string # elements: 1 # length: 1273 statistics: r = lognrnd (mu, sigma) statistics: r = lognrnd (mu, sigma, rows) statistics: r = lognrnd (mu, sigma, rows, cols, ...) statistics: r = lognrnd (mu, sigma, [sz]) Random arrays from the lognormal distribution. r = lognrnd (mu, sigma) returns an array of random numbers chosen from the lognormal distribution with mean parameter mu and standard deviation parameter sigma, each corresponding to the associated normal distribution. The size of r is the common size of mu, and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Both parameters must be reals and sigma > 0. For sigma <= 0, NaN is returned. Both parameters must be reals and sigma > 0. For sigma <= 0, NaN is returned. When called with a single size argument, lognrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the lognormal distribution can be found at https://en.wikipedia.org/wiki/Log-normal_distribution See also: logncdf, logninv, lognpdf, lognfit, lognlike, lognstat # name: # type: sq_string # elements: 1 # length: 46 Random arrays from the lognormal distribution. # name: # type: sq_string # elements: 1 # length: 5 mnpdf # name: # type: sq_string # elements: 1 # length: 1657 statistics: y = mnpdf (x, pk) Multinomial probability density function (PDF). Arguments x is vector with a single sample of a multinomial distribution with parameter pk or a matrix of random samples from multinomial distributions. In the latter case, each row of x is a sample from a multinomial distribution with the corresponding row of pk being its parameter. pk is a vector with the probabilities of the categories or a matrix with each row containing the probabilities of a multinomial sample. Return values y is a vector of probabilities of the random samples x from the multinomial distribution with corresponding parameter pk. The parameter n of the multinomial distribution is the sum of the elements of each row of x. The length of y is the number of columns of x. If a row of pk does not sum to 1, then the corresponding element of y will be NaN. Examples x = [1, 4, 2]; pk = [0.2, 0.5, 0.3]; y = mnpdf (x, pk); x = [1, 4, 2; 1, 0, 9]; pk = [0.2, 0.5, 0.3; 0.1, 0.1, 0.8]; y = mnpdf (x, pk); References Wendy L. Martinez and Angel R. Martinez. Computational Statistics Handbook with MATLAB. Appendix E, pages 547-557, Chapman & Hall/CRC, 2001. Merran Evans, Nicholas Hastings and Brian Peacock. Statistical Distributions. pages 134-136, Wiley, New York, third edition, 2000. Input arguments must be double, single, or an integer type; logical and character arrays are rejected. Integer input is promoted to double, so the result is always a probability. MATLAB is inconsistent here: for several of the discrete distributions it returns the result in the integer class of the input, truncating a probability to 0 or 1. See also: mnrnd # name: # type: sq_string # elements: 1 # length: 47 Multinomial probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 5 mnrnd # name: # type: sq_string # elements: 1 # length: 1763 statistics: r = mnrnd (n, pk) statistics: r = mnrnd (n, pk, s) Random arrays from the multinomial distribution. Arguments n is the first parameter of the multinomial distribution. n can be scalar or a vector containing the number of trials of each multinomial sample. The elements of n must be non-negative integers. pk is the second parameter of the multinomial distribution. pk can be a vector with the probabilities of the categories or a matrix with each row containing the probabilities of a multinomial sample. If pk has more than one row and n is non-scalar, then the number of rows of pk must match the number of elements of n. s is the number of multinomial samples to be generated. s must be a non-negative integer. If s is specified, then n must be scalar and pk must be a vector. Return values r is a matrix of random samples from the multinomial distribution with corresponding parameters n and pk. Each row corresponds to one multinomial sample. The number of columns, therefore, corresponds to the number of columns of pk. If s is not specified, then the number of rows of r is the maximum of the number of elements of n and the number of rows of pk. If a row of pk does not sum to 1, then the corresponding row of r will contain only NaN values. Examples n = 10; pk = [0.2, 0.5, 0.3]; r = mnrnd (n, pk); n = 10 * ones (3, 1); pk = [0.2, 0.5, 0.3]; r = mnrnd (n, pk); n = (1:2)'; pk = [0.2, 0.5, 0.3; 0.1, 0.1, 0.8]; r = mnrnd (n, pk); References Wendy L. Martinez and Angel R. Martinez. Computational Statistics Handbook with MATLAB. Appendix E, pages 547-557, Chapman & Hall/CRC, 2001. Merran Evans, Nicholas Hastings and Brian Peacock. Statistical Distributions. pages 134-136, Wiley, New York, third edition, 2000. See also: mnpdf # name: # type: sq_string # elements: 1 # length: 48 Random arrays from the multinomial distribution. # name: # type: sq_string # elements: 1 # length: 6 mvncdf # name: # type: sq_string # elements: 1 # length: 2829 statistics: p = mvncdf (x) statistics: p = mvncdf (x, mu, sigma) statistics: p = mvncdf (x_lo, x_up, mu, sigma) statistics: p = mvncdf (..., options) statistics: [p, err] = mvncdf (...) Multivariate normal cumulative distribution function (CDF). p = mvncdf (x) returns the cumulative probability of the multivariate normal distribution evaluated at each row of x with zero mean and an identity covariance matrix. The rows of matrix x correspond to observations and its columns to variables. The return argument p is a column vector with the same number of rows as in x. p = mvncdf (x, mu, sigma) returns cumulative probability of the multivariate normal distribution evaluated at each row of x with mean mu and a covariance matrix sigma. mu can be either a scalar (the same of every variable) or a row vector with the same number of elements as the number of variables in x. sigma covariance matrix may be specified a row vector if it only contains variances along its diagonal and zero covariances of the diagonal. In such a case, the diagonal vector sigma must have the same number of elements as the number of variables (columns) in x. If you only want to specify sigma, you can pass an empty matrix for mu. The multivariate normal cumulative probability at x is defined as the probability that a random vector V, distributed as multivariate normal, will fall within the semi-infinite rectangle with upper limits defined by x. Pr{V(1)<=X(1), V(2)<=X(2), ... V(D)<=X(D)}. p = mvncdf (x_lo, x_hi, mu, sigma) returns the multivariate normal cumulative probability evaluated over the rectangle (hyper-rectangle for multivariate data in x) with lower and upper limits defined by x_lo and x_hi, respectively. [p, err] = mvncdf (...) also returns an error estimate err in p. p = mvncdf (..., options) specifies the structure, which controls specific parameters for the numerical integration used to compute p, as statset makes it. A field that is missing or empty takes its default. The fields are: 'TolFun' Maximum absolute error tolerance. Default is 1e-8 for D < 4, or 1e-4 for D >= 4. Note that for bivariate normal cdf, the Octave implementation has a precision of more than 1e-10. 'MaxFunEvals' Maximum number of integrand evaluations. Default is 1e7 for D > 4. 'Display' Display options. Choices are 'off' (default), 'iter', which shows the probability and estimated error at each repetition, and 'final', which shows the final probability and related error after the integrand has converged successfully. Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: bvncdf, mvnpdf, mvnrnd # name: # type: sq_string # elements: 1 # length: 59 Multivariate normal cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 6 mvnpdf # name: # type: sq_string # elements: 1 # length: 1854 statistics: y = mvnpdf (x, mu, sigma) Multivariate normal probability density function (PDF). y = mvnpdf (x) returns the probability density of the multivariate normal distribution with zero mean and identity covariance matrix, evaluated at each row of x. Rows of the N-by-D matrix x correspond to observations orpoints, and columns correspond to variables or coordinates. y is an N-by-1 vector. y = mvnpdf (x, mu) returns the density of the multivariate normal distribution with mean MU and identity covariance matrix, evaluated at each row of x. mu is a 1-by-D vector, or an N-by-D matrix, in which case the density is evaluated for each row of x with the corresponding row of mu. mu can also be a scalar value, which MVNPDF replicates to match the size of x. y = mvnpdf (x, mu, sigma) returns the density of the multivariate normal distribution with mean mu and covariance sigma, evaluated at each row of x. sigma is a D-by-D matrix, or an D-by-D-by-N array, in which case the density is evaluated for each row of x with the corresponding page of sigma, i.e., mvnpdf computes y(i) using x(i,:) and sigma(:,:,i). If the covariance matrix is diagonal, containing variances along the diagonal and zero covariances off the diagonal, sigma may also be specified as a 1-by-D matrix or a 1-by-D-by-N array, containing just the diagonal. Pass in the empty matrix for mu to use its default value when you want to only specify sigma. If x is a 1-by-D vector, mvnpdf replicates it to match the leading dimension of mu or the trailing dimension of sigma. Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: mvncdf, mvnrnd # name: # type: sq_string # elements: 1 # length: 55 Multivariate normal probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 6 mvnrnd # name: # type: sq_string # elements: 1 # length: 1627 statistics: r = mvnrnd (mu, sigma) statistics: r = mvnrnd (mu, sigma, n) statistics: r = mvnrnd (mu, sigma, n, T) statistics: [r, T] = mvnrnd (...) Random vectors from the multivariate normal distribution. r = mvnrnd (mu, sigma) returns an N-by-D matrix r of random vectors chosen from the multivariate normal distribution with mean vector mu and covariance matrix sigma. mu is an N-by-D matrix, and mvnrnd generates each N of r using the corresponding N of mu. sigma is a D-by-D symmetric positive semi-definite matrix, or a D-by-D-by-N array. If sigma is an array, mvnrnd generates each N of r using the corresponding page of sigma, i.e., mvnrnd computes r(i,:) using mu(i,:) and sigma(:,:,i). If the covariance matrix is diagonal, containing variances along the diagonal and zero covariances off the diagonal, sigma may also be specified as a 1-by-D matrix or a 1-by-D-by-N array, containing just the diagonal. If mu is a 1-by-D vector, mvnrnd replicates it to match the trailing dimension of SIGMA. r = mvnrnd (mu, sigma, n) returns a N-by-D matrix R of random vectors chosen from the multivariate normal distribution with 1-by-D mean vector mu, and D-by-D covariance matrix sigma. r = mvnrnd (mu, sigma, n, T) supplies the Cholesky factor T of sigma, so that sigma(:,:,J) == T(:,:,J)'*T(:,:,J) if sigma is a 3D array or sigma == T'*T if sigma is a matrix. No error checking is done on T. [r, T] = mvnrnd (...) returns the Cholesky factor T, so it can be re-used to make later calls more efficient, although there are greater efficiency gains when SIGMA can be specified as a diagonal instead. See also: mvncdf, mvnpdf # name: # type: sq_string # elements: 1 # length: 57 Random vectors from the multivariate normal distribution. # name: # type: sq_string # elements: 1 # length: 6 mvtcdf # name: # type: sq_string # elements: 1 # length: 2582 statistics: p = mvtcdf (x, rho, df) statistics: p = mvtcdf (x_lo, x_up, rho, df) statistics: p = mvtcdf (..., options) statistics: [p, err] = mvtcdf (...) Multivariate Student's t cumulative distribution function (CDF). p = mvtcdf (x, rho, df) returns the cumulative probability of the multivariate student's t distribution with correlation parameters rho and degrees of freedom df, evaluated at each row of x. The rows of the NxD matrix x correspond to sample observations and its columns correspond to variables or coordinates. The return argument p is a column vector with the same number of rows as in x. rho is a symmetric, positive definite, DxD correlation matrix. dF is a scalar or a vector with N elements. Note: mvtcdf computes the CDF for the standard multivariate Student's t distribution, centered at the origin, with no scale parameters. If rho is a covariance matrix, i.e. diag(rho) is not all ones, mvtcdf rescales rho to transform it to a correlation matrix. mvtcdf does not rescale x, though. The multivariate Student's t cumulative probability at x is defined as the probability that a random vector T, distributed as multivariate normal, will fall within the semi-infinite rectangle with upper limits defined by x. Pr{T(1)<=X(1), T(2)<=X(2), ... T(D)<=X(D)}. p = mvtcdf (x_lo, x_hi, rho, df) returns the multivariate Student's t cumulative probability evaluated over the rectangle (hyper-rectangle for multivariate data in x) with lower and upper limits defined by x_lo and x_hi, respectively. [p, err] = mvtcdf (...) also returns an error estimate err in p. p = mvtcdf (..., options) specifies the structure, which controls specific parameters for the numerical integration used to compute p, as statset makes it. A field that is missing or empty takes its default. The fields are: 'TolFun' Maximum absolute error tolerance. Default is 1e-8 for D < 4, or 1e-4 for D >= 4. 'MaxFunEvals' Maximum number of integrand evaluations when D >= 4. Default is 1e7. Ignored when D < 4. 'Display' Display options. Choices are 'off' (default), 'iter', which shows the probability and estimated error at each repetition, and 'final', which shows the final probability and related error after the integrand has converged successfully. Ignored when D < 4. Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: bvtcdf, mvtpdf, mvtrnd # name: # type: sq_string # elements: 1 # length: 64 Multivariate Student's t cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 6 mvtpdf # name: # type: sq_string # elements: 1 # length: 1050 statistics: y = mvtpdf (x, rho, df) Multivariate Student's t probability density function (PDF). Arguments x are the points at which to find the probability, where each row corresponds to an observation. (NxD matrix) rho is the correlation matrix. (DxD symmetric positive definite matrix) df is the degrees of freedom. (scalar or vector of length N) The distribution is assumed to be centered (zero mean). Return values y is the probability density for each row of x. (Nx1 vector) Examples x = [1 2]; rho = [1.0 0.5; 0.5 1.0]; df = 4; y = mvtpdf (x, rho, df) References Michael Roth, On the Multivariate t Distribution, Technical report from Automatic Control at Linkoepings universitet, http://users.isy.liu.se/en/rt/roth/student.pdf Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: mvtcdf, mvtrnd # name: # type: sq_string # elements: 1 # length: 60 Multivariate Student's t probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 6 mvtrnd # name: # type: sq_string # elements: 1 # length: 1599 statistics: r = mvtrnd (rho, df) statistics: r = mvtrnd (rho, df, n) Random vectors from the multivariate Student's t distribution. Arguments rho is the matrix of correlation coefficients. If there are any non-unit diagonal elements then rho will be normalized, so that the resulting covariance of the obtained samples r follows: cov (r) = df/(df-2) * rho ./ (sqrt (diag (rho) * diag (rho))). In order to obtain samples distributed according to a standard multivariate student's t-distribution, rho must be equal to the identity matrix. To generate multivariate student's t-distribution samples r with arbitrary covariance matrix rho, the following scaling might be used: r = mvtrnd (rho, df, n) * diag (sqrt (diag (rho))). df is the degrees of freedom for the multivariate t-distribution. df must be a vector with the same number of elements as samples to be generated or be scalar. n is the number of rows of the matrix to be generated. n must be a non-negative integer and corresponds to the number of samples to be generated. Return values r is a matrix of random samples from the multivariate t-distribution with n row samples. Examples rho = [1, 0.5; 0.5, 1]; df = 3; n = 10; r = mvtrnd (rho, df, n); rho = [1, 0.5; 0.5, 1]; df = [2; 3]; n = 2; r = mvtrnd (rho, df, 2); References Wendy L. Martinez and Angel R. Martinez. Computational Statistics Handbook with MATLAB. Appendix E, pages 547-557, Chapman & Hall/CRC, 2001. Samuel Kotz and Saralees Nadarajah. Multivariate t Distributions and Their Applications. Cambridge University Press, Cambridge, 2004. See also: mvtcdf, mvtpdf # name: # type: sq_string # elements: 1 # length: 62 Random vectors from the multivariate Student's t distribution. # name: # type: sq_string # elements: 1 # length: 7 nakacdf # name: # type: sq_string # elements: 1 # length: 1146 statistics: p = nakacdf (x, mu, omega) statistics: p = nakacdf (x, mu, omega, 'upper') Nakagami cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the Nakagami distribution with shape parameter mu and spread parameter omega. The size of p is the common size of x, mu, and omega. A scalar input functions as a constant matrix of the same size as the other inputs. Both parameters must be positive reals and mu >= 0.5. For mu < 0.5 or omega <= 0, NaN is returned. p = nakacdf (x, mu, omega, "upper") computes the upper tail probability of the Nakagami distribution with parameters mu and beta, at the values in x. Further information about the Nakagami distribution can be found at https://en.wikipedia.org/wiki/Nakagami_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: nakainv, nakapdf, nakarnd, nakafit, nakalike, nakastat # name: # type: sq_string # elements: 1 # length: 48 Nakagami cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 7 nakainv # name: # type: sq_string # elements: 1 # length: 958 statistics: x = nakainv (p, mu, omega) Inverse of the Nakagami cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the Nakagami distribution with shape parameter mu and spread parameter omega. The size of x is the common size of p, mu, and omega. A scalar input functions as a constant matrix of the same size as the other inputs. Both parameters must be positive reals and mu >= 0.5. For mu < 0.5 or omega <= 0, NaN is returned. Further information about the Nakagami distribution can be found at https://en.wikipedia.org/wiki/Nakagami_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: nakacdf, nakapdf, nakarnd, nakafit, nakalike, nakastat # name: # type: sq_string # elements: 1 # length: 64 Inverse of the Nakagami cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 7 nakapdf # name: # type: sq_string # elements: 1 # length: 939 statistics: y = nakapdf (x, mu, omega) Nakagami probability density function (PDF). For each element of x, compute the probability density function (PDF) of the Nakagami distribution with shape parameter mu and spread parameter omega. The size of y is the common size of x, mu, and omega. A scalar input functions as a constant matrix of the same size as the other inputs. Both parameters must be positive reals and mu >= 0.5. For mu < 0.5 or omega <= 0, NaN is returned. Further information about the Nakagami distribution can be found at https://en.wikipedia.org/wiki/Nakagami_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: nakacdf, nakainv, nakarnd, nakafit, nakalike, nakastat # name: # type: sq_string # elements: 1 # length: 44 Nakagami probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 7 nakarnd # name: # type: sq_string # elements: 1 # length: 1141 statistics: r = nakarnd (mu, omega) statistics: r = nakarnd (mu, omega, rows) statistics: r = nakarnd (mu, omega, rows, cols, ...) statistics: r = nakarnd (mu, omega, [sz]) Random arrays from the Nakagami distribution. r = nakarnd (mu, omega) returns an array of random numbers chosen from the Nakagami distribution with shape parameter mu and spread parameter omega. The size of r is the common size of mu and omega. A scalar input functions as a constant matrix of the same size as the other inputs. Both parameters must be positive reals and mu >= 0.5. For mu < 0.5 or omega <= 0, NaN is returned. When called with a single size argument, nakarnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the Nakagami distribution can be found at https://en.wikipedia.org/wiki/Nakagami_distribution See also: nakacdf, nakainv, nakapdf, nakafit, nakalike, nakastat # name: # type: sq_string # elements: 1 # length: 45 Random arrays from the Nakagami distribution. # name: # type: sq_string # elements: 1 # length: 7 nbincdf # name: # type: sq_string # elements: 1 # length: 2437 statistics: p = nbincdf (x, r, ps) statistics: p = nbincdf (x, r, ps, 'upper') Negative binomial cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the negative binomial distribution with parameters r and ps, where r is the number of successes until the experiment is stopped and ps is the probability of success in each experiment, given the number of failures in x. The size of p is the common size of x, r, and ps. A scalar input functions as a constant matrix of the same size as the other inputs. The algorithm uses the cumulative sums of the binomial masses. p = nbincdf (x, r, ps, "upper") computes the upper tail probability of the negative binomial distribution with parameters r and ps, at the values in x. When r is an integer, the negative binomial distribution is also known as the Pascal distribution and it models the number of failures in x before a specified number of successes is reached in a series of independent, identical trials. Its parameters are the probability of success in a single trial, ps, and the number of successes, r. A special case of the negative binomial distribution, when r = 1, is the geometric distribution, which models the number of failures before the first success. r can also have non-integer positive values, in which form the negative binomial distribution, also known as the Polya distribution, has no interpretation in terms of repeated trials, but, like the Poisson distribution, it is useful in modeling count data. The negative binomial distribution is more general than the Poisson distribution because it has a variance that is greater than its mean, making it suitable for count data that do not meet the assumptions of the Poisson distribution. In the limit, as r increases to infinity, the negative binomial distribution approaches the Poisson distribution. Further information about the negative binomial distribution can be found at https://en.wikipedia.org/wiki/Negative_binomial_distribution Input arguments must be double, single, or an integer type; logical and character arrays are rejected. Integer input is promoted to double, so the result is always a probability. MATLAB is inconsistent here: for several of the discrete distributions it returns the result in the integer class of the input, truncating a probability to 0 or 1. See also: nbininv, nbinpdf, nbinrnd, nbinfit, nbinlike, nbinstat # name: # type: sq_string # elements: 1 # length: 57 Negative binomial cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 7 nbininv # name: # type: sq_string # elements: 1 # length: 2115 statistics: x = nbininv (p, r, ps) Inverse of the negative binomial cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the negative binomial distribution with parameters r and ps, where r is the number of successes until the experiment is stopped and ps is the probability of success in each experiment, given the probability in p. The size of x is the common size of p, r, and ps. A scalar input functions as a constant matrix of the same size as the other inputs. When r is an integer, the negative binomial distribution is also known as the Pascal distribution and it models the number of failures in x before a specified number of successes is reached in a series of independent, identical trials. Its parameters are the probability of success in a single trial, ps, and the number of successes, r. A special case of the negative binomial distribution, when r = 1, is the geometric distribution, which models the number of failures before the first success. r can also have non-integer positive values, in which form the negative binomial distribution, also known as the Polya distribution, has no interpretation in terms of repeated trials, but, like the Poisson distribution, it is useful in modeling count data. The negative binomial distribution is more general than the Poisson distribution because it has a variance that is greater than its mean, making it suitable for count data that do not meet the assumptions of the Poisson distribution. In the limit, as r increases to infinity, the negative binomial distribution approaches the Poisson distribution. Further information about the negative binomial distribution can be found at https://en.wikipedia.org/wiki/Negative_binomial_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: nbincdf, nbinpdf, nbinrnd, nbinfit, nbinlike, nbinstat # name: # type: sq_string # elements: 1 # length: 73 Inverse of the negative binomial cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 7 nbinpdf # name: # type: sq_string # elements: 1 # length: 2173 statistics: y = nbinpdf (x, r, ps) Negative binomial probability density function (PDF). For each element of x, compute the probability density function (PDF) at x of the negative binomial distribution with parameters r and ps, where r is the number of successes until the experiment is stopped and ps is the probability of success in each experiment, given the number of failures in x. The size of y is the common size of x, r, and ps. A scalar input functions as a constant matrix of the same size as the other inputs. When r is an integer, the negative binomial distribution is also known as the Pascal distribution and it models the number of failures in x before a specified number of successes is reached in a series of independent, identical trials. Its parameters are the probability of success in a single trial, ps, and the number of successes, r. A special case of the negative binomial distribution, when r = 1, is the geometric distribution, which models the number of failures before the first success. r can also have non-integer positive values, in which form the negative binomial distribution, also known as the Polya distribution, has no interpretation in terms of repeated trials, but, like the Poisson distribution, it is useful in modeling count data. The negative binomial distribution is more general than the Poisson distribution because it has a variance that is greater than its mean, making it suitable for count data that do not meet the assumptions of the Poisson distribution. In the limit, as r increases to infinity, the negative binomial distribution approaches the Poisson distribution. Further information about the negative binomial distribution can be found at https://en.wikipedia.org/wiki/Negative_binomial_distribution Input arguments must be double, single, or an integer type; logical and character arrays are rejected. Integer input is promoted to double, so the result is always a probability. MATLAB is inconsistent here: for several of the discrete distributions it returns the result in the integer class of the input, truncating a probability to 0 or 1. See also: nbincdf, nbininv, nbinrnd, nbinfit, nbinlike, nbinstat # name: # type: sq_string # elements: 1 # length: 53 Negative binomial probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 7 nbinrnd # name: # type: sq_string # elements: 1 # length: 2286 statistics: rnd = nbinrnd (r, ps) statistics: rnd = nbinrnd (r, ps, rows) statistics: rnd = nbinrnd (r, ps, rows, cols, ...) statistics: rnd = nbinrnd (r, ps, [sz]) Random arrays from the negative binomial distribution. rnd = nbinrnd (r, ps) returns an array of random numbers chosen from the negative binomial distribution with parameters r and ps, where r is the number of successes until the experiment is stopped and ps is the probability of success in each experiment, given the number of failures in x. The size of rnd is the common size of r and ps. A scalar input functions as a constant matrix of the same size as the other inputs. When called with a single size argument, return a square matrix with the dimension specified. When called with more than one scalar argument the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a vector of dimensions sz. When r is an integer, the negative binomial distribution is also known as the Pascal distribution and it models the number of failures in x before a specified number of successes is reached in a series of independent, identical trials. Its parameters are the probability of success in a single trial, ps, and the number of successes, r. A special case of the negative binomial distribution, when r = 1, is the geometric distribution, which models the number of failures before the first success. r can also have non-integer positive values, in which form the negative binomial distribution, also known as the Polya distribution, has no interpretation in terms of repeated trials, but, like the Poisson distribution, it is useful in modeling count data. The negative binomial distribution is more general than the Poisson distribution because it has a variance that is greater than its mean, making it suitable for count data that do not meet the assumptions of the Poisson distribution. In the limit, as r increases to infinity, the negative binomial distribution approaches the Poisson distribution. Further information about the negative binomial distribution can be found at https://en.wikipedia.org/wiki/Negative_binomial_distribution See also: nbincdf, nbininv, nbinpdf, nbinfit, nbinlike, nbinstat # name: # type: sq_string # elements: 1 # length: 54 Random arrays from the negative binomial distribution. # name: # type: sq_string # elements: 1 # length: 6 ncfcdf # name: # type: sq_string # elements: 1 # length: 1103 statistics: p = ncfcdf (x, df1, df2, lambda) statistics: p = ncfcdf (x, df1, df2, lambda, 'upper') Noncentral F-cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the noncentral F-distribution with df1 and df2 degrees of freedom and noncentrality parameter lambda. The size of p is the common size of x, df1, df2, and lambda. A scalar input functions as a constant matrix of the same size as the other inputs. p = ncfcdf (x, df1, df2, lambda, "upper") computes the upper tail probability of the noncentral F-distribution with parameters df1, df2, and lambda, at the values in x. Further information about the noncentral F-distribution can be found at https://en.wikipedia.org/wiki/Noncentral_F-distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: ncfinv, ncfpdf, ncfrnd, ncfstat, fcdf # name: # type: sq_string # elements: 1 # length: 52 Noncentral F-cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 6 ncfinv # name: # type: sq_string # elements: 1 # length: 948 statistics: x = ncfinv (p, df1, df2, lambda) Inverse of the noncentral F-cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the noncentral F-distribution with df1 and df2 degrees of freedom and noncentrality parameter lambda. The size of x is the common size of p, df1, df2, and lambda. A scalar input functions as a constant matrix of the same size as the other inputs. ncfinv uses Newton's method to converge to the solution. Further information about the noncentral F-distribution can be found at https://en.wikipedia.org/wiki/Noncentral_F-distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: ncfcdf, ncfpdf, ncfrnd, ncfstat, finv # name: # type: sq_string # elements: 1 # length: 68 Inverse of the noncentral F-cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 6 ncfpdf # name: # type: sq_string # elements: 1 # length: 871 statistics: y = ncfpdf (x, df1, df2, lambda) Noncentral F-probability density function (PDF). For each element of x, compute the probability density function (PDF) of the noncentral F-distribution with df1 and df2 degrees of freedom and noncentrality parameter lambda. The size of y is the common size of x, df1, df2, and lambda. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the noncentral F-distribution can be found at https://en.wikipedia.org/wiki/Noncentral_F-distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: ncfcdf, ncfinv, ncfrnd, ncfstat, fpdf # name: # type: sq_string # elements: 1 # length: 48 Noncentral F-probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 6 ncfrnd # name: # type: sq_string # elements: 1 # length: 1246 statistics: r = ncfrnd (df1, df2, lambda) statistics: r = ncfrnd (df1, df2, lambda, rows, cols, ...) statistics: r = ncfrnd (df1, df2, lambda, [sz]) Random arrays from the noncentral F-distribution. x = ncfrnd (p, df1, df2, lambda) returns an array of random numbers chosen from the noncentral F-distribution with df1 and df2 degrees of freedom and noncentrality parameter lambda. The size of r is the common size of df1, df2, and lambda. A scalar input functions as a constant matrix of the same size as the other input. ncfrnd generates values using the definition of a noncentral F random variable, as the ratio of a noncentral chi-squared distribution and a (central) chi-squared distribution. When called with a single size argument, ncfrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the noncentral F-distribution can be found at https://en.wikipedia.org/wiki/Noncentral_F-distribution See also: ncfcdf, ncfinv, ncfpdf, ncfstat, frnd, ncx2rnd, chi2rnd # name: # type: sq_string # elements: 1 # length: 49 Random arrays from the noncentral F-distribution. # name: # type: sq_string # elements: 1 # length: 6 nctcdf # name: # type: sq_string # elements: 1 # length: 1039 statistics: p = nctcdf (x, df, mu) statistics: p = nctcdf (x, df, mu, 'upper') Noncentral t-cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the noncentral t-distribution with df degrees of freedom and noncentrality parameter mu. The size of p is the common size of x, df, and mu. A scalar input functions as a constant matrix of the same size as the other inputs. p = nctcdf (x, df, mu, "upper") computes the upper tail probability of the noncentral t-distribution with parameters df and mu, at the values in x. Further information about the noncentral t-distribution can be found at https://en.wikipedia.org/wiki/Noncentral_t-distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: nctinv, nctpdf, nctrnd, nctstat, tcdf # name: # type: sq_string # elements: 1 # length: 52 Noncentral t-cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 6 nctinv # name: # type: sq_string # elements: 1 # length: 916 statistics: x = nctinv (p, df, mu) Inverse of the non-central t-cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the noncentral t-distribution with df degrees of freedom and noncentrality parameter mu. The size of x is the common size of p, df, and mu. A scalar input functions as a constant matrix of the same size as the other inputs. nctinv uses Newton's method to converge to the solution. Further information about the noncentral t-distribution can be found at https://en.wikipedia.org/wiki/Noncentral_t-distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: nctcdf, nctpdf, nctrnd, nctstat, tinv # name: # type: sq_string # elements: 1 # length: 69 Inverse of the non-central t-cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 6 nctpdf # name: # type: sq_string # elements: 1 # length: 838 statistics: y = nctpdf (x, df, mu) Noncentral t-probability density function (PDF). For each element of x, compute the probability density function (PDF) of the noncentral t-distribution with df degrees of freedom and noncentrality parameter mu. The size of y is the common size of x, df, and mu. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the noncentral t-distribution can be found at https://en.wikipedia.org/wiki/Noncentral_t-distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: nctcdf, nctinv, nctrnd, nctstat, tpdf # name: # type: sq_string # elements: 1 # length: 48 Noncentral t-probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 6 nctrnd # name: # type: sq_string # elements: 1 # length: 1187 statistics: r = nctrnd (df, mu) statistics: r = nctrnd (df, mu, rows, cols, ...) statistics: r = nctrnd (df, mu, [sz]) Random arrays from the noncentral t-distribution. x = nctrnd (p, df, mu) returns an array of random numbers chosen from the noncentral t-distribution with df degrees of freedom and noncentrality parameter mu. The size of r is the common size of df and mu. A scalar input functions as a constant matrix of the same size as the other input. nctrnd generates values using the definition of a noncentral t random variable, as the ratio of a normal distribution with non-zero mean and the sqrt of a chi-squared distribution. When called with a single size argument, nctrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the noncentral t-distribution can be found at https://en.wikipedia.org/wiki/Noncentral_t-distribution See also: nctcdf, nctinv, nctpdf, nctstat, trnd, normrnd, chi2rnd # name: # type: sq_string # elements: 1 # length: 49 Random arrays from the noncentral t-distribution. # name: # type: sq_string # elements: 1 # length: 7 ncx2cdf # name: # type: sq_string # elements: 1 # length: 1123 statistics: p = ncx2cdf (x, df, lambda) statistics: p = ncx2cdf (x, df, lambda, 'upper') Noncentral chi-squared cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the noncentral chi-squared distribution with df degrees of freedom and noncentrality parameter lambda. The size of p is the common size of x, df, and lambda. A scalar input functions as a constant matrix of the same size as the other inputs. p = ncx2cdf (x, df, lambda, "upper") computes the upper tail probability of the noncentral chi-squared distribution with parameters df and lambda, at the values in x. Further information about the noncentral chi-squared distribution can be found at https://en.wikipedia.org/wiki/Noncentral_chi-squared_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: ncx2inv, ncx2pdf, ncx2rnd, ncx2stat, chi2cdf # name: # type: sq_string # elements: 1 # length: 62 Noncentral chi-squared cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 7 ncx2inv # name: # type: sq_string # elements: 1 # length: 968 statistics: x = ncx2inv (p, df, lambda) Inverse of the noncentral chi-squared cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the noncentral chi-squared distribution with df degrees of freedom and noncentrality parameter mu. The size of x is the common size of p, df, and mu. A scalar input functions as a constant matrix of the same size as the other inputs. ncx2inv uses Newton's method to converge to the solution. Further information about the noncentral chi-squared distribution can be found at https://en.wikipedia.org/wiki/Noncentral_chi-squared_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: ncx2cdf, ncx2pdf, ncx2rnd, ncx2stat, chi2inv # name: # type: sq_string # elements: 1 # length: 78 Inverse of the noncentral chi-squared cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 7 ncx2pdf # name: # type: sq_string # elements: 1 # length: 903 statistics: y = ncx2pdf (x, df, lambda) Noncentral chi-squared probability distribution function (PDF). For each element of x, compute the probability density function (PDF) of the noncentral chi-squared distribution with df degrees of freedom and noncentrality parameter lambda. The size of y is the common size of x, df, and lambda. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the noncentral chi-squared distribution can be found at https://en.wikipedia.org/wiki/Noncentral_chi-squared_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: ncx2cdf, ncx2inv, ncx2rnd, ncx2stat, chi2pdf # name: # type: sq_string # elements: 1 # length: 63 Noncentral chi-squared probability distribution function (PDF). # name: # type: sq_string # elements: 1 # length: 7 ncx2rnd # name: # type: sq_string # elements: 1 # length: 1051 statistics: r = ncx2rnd (df, lambda) statistics: r = ncx2rnd (df, lambda, rows, cols, ...) statistics: r = ncx2rnd (df, lambda, [sz]) Random arrays from the noncentral chi-squared distribution. r = ncx2rnd (df, lambda) returns an array of random numbers chosen from the noncentral chi-squared distribution with df degrees of freedom and noncentrality parameter lambda. The size of r is the common size of df and lambda. A scalar input functions as a constant matrix of the same size as the other input. When called with a single size argument, ncx2rnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the noncentral chi-squared distribution can be found at https://en.wikipedia.org/wiki/Noncentral_chi-squared_distribution See also: ncx2cdf, ncx2inv, ncx2pdf, ncx2stat # name: # type: sq_string # elements: 1 # length: 59 Random arrays from the noncentral chi-squared distribution. # name: # type: sq_string # elements: 1 # length: 7 normcdf # name: # type: sq_string # elements: 1 # length: 1886 statistics: p = normcdf (x) statistics: p = normcdf (x, mu) statistics: p = normcdf (x, mu, sigma) statistics: p = normcdf (..., 'upper') statistics: [p, plo, pup] = normcdf (x, mu, sigma, pcov) statistics: [p, plo, pup] = normcdf (x, mu, sigma, pcov, alpha) statistics: [p, plo, pup] = normcdf (..., 'upper') Normal cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the normal distribution with mean mu and standard deviation sigma. The size of p is the common size of x, mu and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Default values are mu = 0, sigma = 1. When called with three output arguments, i.e. [p, plo, pup], normcdf computes the confidence bounds for p when the input parameters mu and sigma are estimates. In such case, pcov, a 2x2 matrix containing the covariance matrix of the estimated parameters, is necessary. Optionally, alpha, which has a default value of 0.05, specifies the 100 * (1 - alpha) percent confidence bounds. plo and pup are arrays of the same size as p containing the lower and upper confidence bounds. [...] = normcdf (..., "upper") computes the upper tail probability of the normal distribution with parameters mu and sigma, at the values in x. This can be used to compute a right-tailed p-value. To compute a two-tailed p-value, use 2 * normcdf (-abs (x), mu, sigma). Further information about the normal distribution can be found at https://en.wikipedia.org/wiki/Normal_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: norminv, normpdf, normrnd, normfit, normlike, normstat # name: # type: sq_string # elements: 1 # length: 46 Normal cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 7 norminv # name: # type: sq_string # elements: 1 # length: 1155 statistics: x = norminv (p) statistics: x = norminv (p, mu) statistics: x = norminv (p, mu, sigma) Inverse of the normal cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the normal distribution with mean mu and standard deviation sigma. The size of p is the common size of p, mu and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Default values are mu = 0, sigma = 1. The default values correspond to the standard normal distribution and computing its quantile function is also possible with the probit function, which is faster but it does not perform any input validation. Further information about the normal distribution can be found at https://en.wikipedia.org/wiki/Normal_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: normcdf, normpdf, normrnd, normfit, normlike, normstat, probit # name: # type: sq_string # elements: 1 # length: 62 Inverse of the normal cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 7 normpdf # name: # type: sq_string # elements: 1 # length: 920 statistics: y = normpdf (x) statistics: y = normpdf (x, mu) statistics: y = normpdf (x, mu, sigma) Normal probability density function (PDF). For each element of x, compute the probability density function (PDF) of the normal distribution with mean mu and standard deviation sigma. The size of y is the common size of p, mu and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Default values are mu = 0, sigma = 1. Further information about the normal distribution can be found at https://en.wikipedia.org/wiki/Normal_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: normcdf, norminv, normrnd, normfit, normlike, normstat # name: # type: sq_string # elements: 1 # length: 42 Normal probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 7 normrnd # name: # type: sq_string # elements: 1 # length: 1110 statistics: r = normrnd (mu, sigma) statistics: r = normrnd (mu, sigma, rows) statistics: r = normrnd (mu, sigma, rows, cols, ...) statistics: r = normrnd (mu, sigma, [sz]) Random arrays from the normal distribution. r = normrnd (mu, sigma) returns an array of random numbers chosen from the normal distribution with mean mu and standard deviation sigma. The size of r is the common size of mu and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Both parameters must be finite real numbers and sigma > 0, otherwise NaN is returned. When called with a single size argument, normrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the normal distribution can be found at https://en.wikipedia.org/wiki/Normal_distribution See also: normcdf, norminv, normpdf, normfit, normlike, normstat # name: # type: sq_string # elements: 1 # length: 43 Random arrays from the normal distribution. # name: # type: sq_string # elements: 1 # length: 5 plcdf # name: # type: sq_string # elements: 1 # length: 1094 statistics: p = plcdf (data, x, Fx) statistics: p = plcdf (data, x, Fx, 'upper') Piecewise linear cumulative distribution function (CDF). For each element of data, compute the cumulative distribution function (CDF) of the piecewise linear distribution with a vector of x values at which the CDF changes slope and a vector of CDF values Fx that correspond to each value in x. Both x and Fx must be vectors of the same size and at least 2-elements long. The size of p is the same as data. p = plcdf (data, x, Fx, "upper") computes the upper tail probability of the piecewise linear distribution with parameters x and Fx, at the values in data. Further information about the piecewise linear distribution can be found at https://en.wikipedia.org/wiki/Piecewise_linear_function Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: plinv, plpdf, plrnd, plstat # name: # type: sq_string # elements: 1 # length: 56 Piecewise linear cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 5 plinv # name: # type: sq_string # elements: 1 # length: 886 statistics: data = plinv (p, x, Fx) Inverse of the piecewise linear distribution (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the piecewise linear distribution with a vector of x values at which the CDF changes slope and a vector of CDF values Fx that correspond to each value in x. Both x and Fx must be vectors of the same_p size and at least 2-elements long.. The size of data is the same_p as p. Further information about the piecewise linear distribution can be found at https://en.wikipedia.org/wiki/Piecewise_linear_function Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: plcdf, plpdf, plrnd, plstat # name: # type: sq_string # elements: 1 # length: 52 Inverse of the piecewise linear distribution (iCDF). # name: # type: sq_string # elements: 1 # length: 5 plpdf # name: # type: sq_string # elements: 1 # length: 1053 statistics: y = plpdf (data, x, Fx) Piecewise linear probability density function (PDF). For each element of data, compute the probability density function (PDF) of the piecewise linear distribution with a vector of x values at which the CDF changes slope and a vector of CDF values Fx that correspond to each value in x. Both x and Fx must be vectors of the same size and at least 2-elements long. The size of p is the same as data. Further information about the piecewise linear distribution can be found at https://en.wikipedia.org/wiki/Piecewise_linear_function Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. MATLAB also accepts integer input here, returning the result in the integer class of the input; Octave rejects it, as it does for every other continuous distribution. See also: plcdf, plinv, plrnd, plstat # name: # type: sq_string # elements: 1 # length: 52 Piecewise linear probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 5 plrnd # name: # type: sq_string # elements: 1 # length: 1013 statistics: r = plrnd (x, Fx) statistics: r = plrnd (x, Fx, rows) statistics: r = plrnd (x, Fx, rows, cols, ...) statistics: r = plrnd (x, Fx, [sz]) Random arrays from the piecewise linear distribution. r = plrnd (x, Fx) returns a random number chosen from the piecewise linear distribution with a vector of x values at which the CDF changes slope and a vector of CDF values Fx that correspond to each value in x. Both x and Fx must be vectors of the same size and at least 2-elements long. When called with a single size argument, plrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the piecewise linear distribution can be found at https://en.wikipedia.org/wiki/Piecewise_linear_function See also: plcdf, plinv, plpdf, plstat # name: # type: sq_string # elements: 1 # length: 53 Random arrays from the piecewise linear distribution. # name: # type: sq_string # elements: 1 # length: 8 poisscdf # name: # type: sq_string # elements: 1 # length: 1071 statistics: p = poisscdf (x, lambda) statistics: p = poisscdf (x, lambda, 'upper') Poisson cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the Poisson distribution with rate parameter lambda. The size of p is the common size of x and lambda. A scalar input functions as a constant matrix of the same size as the other inputs. p = poisscdf (x, lambda, "upper") computes the upper tail probability of the Poisson distribution with parameter lambda, at the values in x. Further information about the Poisson distribution can be found at https://en.wikipedia.org/wiki/Poisson_distribution Input arguments must be double, single, or an integer type; logical and character arrays are rejected. Integer input is promoted to double, so the result is always a probability. MATLAB is inconsistent here: for several of the discrete distributions it returns the result in the integer class of the input, truncating a probability to 0 or 1. See also: poissinv, poisspdf, poissrnd, poissfit, poisslike, poisstat # name: # type: sq_string # elements: 1 # length: 47 Poisson cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 8 poissinv # name: # type: sq_string # elements: 1 # length: 829 statistics: x = poissinv (p, lambda) Inverse of the Poisson cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the Poisson distribution with rate parameter lambda. The size of x is the common size of p and lambda. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Poisson distribution can be found at https://en.wikipedia.org/wiki/Poisson_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: poisscdf, poisspdf, poissrnd, poissfit, poisslike, poisstat # name: # type: sq_string # elements: 1 # length: 63 Inverse of the Poisson cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 8 poisspdf # name: # type: sq_string # elements: 1 # length: 1163 statistics: y = poisspdf (x, lambda) Poisson probability density function (PDF). For each element of x, compute the probability density function (PDF) of the Poisson distribution with rate parameter lambda. The size of y is the common size of x and lambda. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Poisson distribution can be found at https://en.wikipedia.org/wiki/Poisson_distribution Input arguments must be double, single, or an integer type; logical and character arrays are rejected. Integer input is promoted to double, so the result is always a probability. MATLAB is inconsistent here: for several of the discrete distributions it returns the result in the integer class of the input, truncating a probability to 0 or 1. The density at an infinite abscissa is 0, no proper distribution placing mass there. MATLAB returns NaN here, as it does for raylpdf and for no other density, which is an inconsistency there rather than a convention: it returns 0 at Inf for every other distribution of the same support. See also: poisscdf, poissinv, poissrnd, poissfit, poisslike, poisstat # name: # type: sq_string # elements: 1 # length: 43 Poisson probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 8 poissrnd # name: # type: sq_string # elements: 1 # length: 1092 statistics: r = poissrnd (lambda) statistics: r = poissrnd (lambda, rows) statistics: r = poissrnd (lambda, rows, cols, ...) statistics: r = poissrnd (lambda, [sz]) Random arrays from the Poisson distribution. r = normrnd (lambda) returns an array of random numbers chosen from the Poisson distribution with rate parameter lambda. The size of r is the common size of lambda. A scalar input functions as a constant matrix of the same size as the other inputs. lambda must be a finite real number and greater or equal to 0, otherwise NaN is returned. When called with a single size argument, poissrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the Poisson distribution can be found at https://en.wikipedia.org/wiki/Poisson_distribution See also: poisscdf, poissinv, poisspdf, poissfit, poisslike, poisstat # name: # type: sq_string # elements: 1 # length: 44 Random arrays from the Poisson distribution. # name: # type: sq_string # elements: 1 # length: 7 raylcdf # name: # type: sq_string # elements: 1 # length: 1078 statistics: p = raylcdf (x, sigma) statistics: p = raylcdf (x, sigma, 'upper') Rayleigh cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the Rayleigh distribution with scale parameter sigma. The size of p is the common size of x and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. p = raylcdf (x, sigma, "upper") computes the upper tail probability of the Rayleigh distribution with parameter sigma, at the values in x. Further information about the Rayleigh distribution can be found at https://en.wikipedia.org/wiki/Rayleigh_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. The prob.RayleighDistribution class names this same parameter B, after MATLAB. See also: raylinv, raylpdf, raylrnd, raylfit, rayllike, raylstat # name: # type: sq_string # elements: 1 # length: 48 Rayleigh cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 7 raylinv # name: # type: sq_string # elements: 1 # length: 905 statistics: x = raylinv (p, sigma) Inverse of the Rayleigh cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the Rayleigh distribution with scale parameter sigma. The size of x is the common size of p and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Rayleigh distribution can be found at https://en.wikipedia.org/wiki/Rayleigh_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. The prob.RayleighDistribution class names this same parameter B, after MATLAB. See also: raylcdf, raylpdf, raylrnd, raylfit, rayllike, raylstat # name: # type: sq_string # elements: 1 # length: 64 Inverse of the Rayleigh cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 7 raylpdf # name: # type: sq_string # elements: 1 # length: 1175 statistics: y = raylpdf (x, sigma) Rayleigh probability density function (PDF). For each element of x, compute the probability density function (PDF) of the Rayleigh distribution with scale parameter sigma. The size of p is the common size of x and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Rayleigh distribution can be found at https://en.wikipedia.org/wiki/Rayleigh_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. The density at an infinite abscissa is 0, no proper distribution placing mass there. MATLAB returns NaN here, as it does for poisspdf and for no other density, which is an inconsistency there rather than a convention: it returns 0 at Inf for every other distribution of the same support. The prob.RayleighDistribution class names this same parameter B, after MATLAB. See also: raylcdf, raylinv, raylrnd, raylfit, rayllike, raylstat # name: # type: sq_string # elements: 1 # length: 44 Rayleigh probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 7 raylrnd # name: # type: sq_string # elements: 1 # length: 1141 statistics: r = raylrnd (sigma) statistics: r = raylrnd (sigma, rows) statistics: r = raylrnd (sigma, rows, cols, ...) statistics: r = raylrnd (sigma, [sz]) Random arrays from the Rayleigh distribution. r = raylrnd (sigma) returns an array of random numbers chosen from the Rayleigh distribution with scale parameter sigma. The size of r is the size of sigma. A scalar input functions as a constant matrix of the same size as the other inputs. sigma must be a finite real number greater than 0, otherwise NaN is returned. When called with a single size argument, raylrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the Rayleigh distribution can be found at https://en.wikipedia.org/wiki/Rayleigh_distribution The prob.RayleighDistribution class names this same parameter B, after MATLAB. See also: raylcdf, raylinv, raylpdf, raylfit, rayllike, raylstat # name: # type: sq_string # elements: 1 # length: 45 Random arrays from the Rayleigh distribution. # name: # type: sq_string # elements: 1 # length: 7 ricecdf # name: # type: sq_string # elements: 1 # length: 1048 statistics: p = ricecdf (x, s, sigma) statistics: p = ricecdf (x, s, sigma, 'upper') Rician cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the Rician distribution with non-centrality (distance) parameter s and scale parameter sigma. The size of p is the common size of x, s, and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. p = ricecdf (x, s, sigma, "upper") computes the upper tail probability of the Rician distribution with parameters s and sigma, at the values in x. Further information about the Rician distribution can be found at https://en.wikipedia.org/wiki/Rice_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: riceinv, ricepdf, ricernd, ricefit, ricelike, ricestat # name: # type: sq_string # elements: 1 # length: 46 Rician cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 7 riceinv # name: # type: sq_string # elements: 1 # length: 844 statistics: x = riceinv (p, s, sigma) Inverse of the Rician distribution (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the Rician distribution with non-centrality (distance) parameter s and scale parameter sigma. The size of x is the common size of x, s, and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Rician distribution can be found at https://en.wikipedia.org/wiki/Rice_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: ricecdf, ricepdf, ricernd, ricefit, ricelike, ricestat # name: # type: sq_string # elements: 1 # length: 42 Inverse of the Rician distribution (iCDF). # name: # type: sq_string # elements: 1 # length: 7 ricepdf # name: # type: sq_string # elements: 1 # length: 845 statistics: y = ricepdf (x, s, sigma) Rician probability density function (PDF). For each element of x, compute the probability density function (PDF) of the Rician distribution with non-centrality (distance) parameter s and scale parameter sigma. The size of y is the common size of x, s, and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Rician distribution can be found at https://en.wikipedia.org/wiki/Rice_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: ricecdf, riceinv, ricernd, ricefit, ricelike, ricestat # name: # type: sq_string # elements: 1 # length: 42 Rician probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 7 ricernd # name: # type: sq_string # elements: 1 # length: 1031 statistics: r = ricernd (s, sigma) statistics: r = ricernd (s, sigma, rows) statistics: r = ricernd (s, sigma, rows, cols, ...) statistics: r = ricernd (s, sigma, [sz]) Random arrays from the Rician distribution. r = ricernd (s, sigma) returns an array of random numbers chosen from the Rician distribution with noncentrality parameter s and scale parameter sigma. The size of r is the common size of s and sigma. A scalar input functions as a constant matrix of the same size as the other inputs. When called with a single size argument, ricernd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the Rician distribution can be found at https://en.wikipedia.org/wiki/Rice_distribution See also: ricecdf, riceinv, ricepdf, ricefit, ricelike, ricestat # name: # type: sq_string # elements: 1 # length: 43 Random arrays from the Rician distribution. # name: # type: sq_string # elements: 1 # length: 7 stblcdf # name: # type: sq_string # elements: 1 # length: 1068 statistics: p = stblcdf (x, alpha, beta, gam, delta) Stable cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the stable distribution with tail index (first shape parameter) alpha, skewness (second shape parameter) beta, scale parameter gam, and location parameter delta, in the Nolan S0 parameterization. The size of p is the size of x. alpha must be in the range (0, 2], beta in [-1, 1], gam positive, and delta real. The parameters must be scalars. The cumulative probability has a closed form for alpha equal to 2 (normal) and for 1 with beta equal to 0 (Cauchy); otherwise it is computed by numerical inversion of the characteristic function (the Gil-Pelaez formula). Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: stblpdf, stblinv, stblrnd, makedist # name: # type: sq_string # elements: 1 # length: 46 Stable cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 7 stblinv # name: # type: sq_string # elements: 1 # length: 1017 statistics: x = stblinv (p, alpha, beta, gam, delta) Inverse of the stable cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the stable distribution with tail index (first shape parameter) alpha, skewness (second shape parameter) beta, scale parameter gam, and location parameter delta, in the Nolan S0 parameterization. The size of x is the size of p. alpha must be in the range (0, 2], beta in [-1, 1], gam positive, and delta real. The parameters must be scalars. The quantile has a closed form for alpha equal to 2 (normal) and for 1 with beta equal to 0 (Cauchy); otherwise it is found by numerical inversion of stblcdf. Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: stblcdf, stblpdf, stblrnd, makedist # name: # type: sq_string # elements: 1 # length: 62 Inverse of the stable cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 7 stblpdf # name: # type: sq_string # elements: 1 # length: 1020 statistics: y = stblpdf (x, alpha, beta, gam, delta) Stable probability density function (PDF). For each element of x, compute the probability density function (PDF) of the stable distribution with tail index (first shape parameter) alpha, skewness (second shape parameter) beta, scale parameter gam, and location parameter delta, in the Nolan S0 parameterization. The size of y is the size of x. alpha must be in the range (0, 2], beta in [-1, 1], gam positive, and delta real. The parameters must be scalars. The density has a closed form for alpha equal to 2 (normal) and for 1 with beta equal to 0 (Cauchy); otherwise it is computed by numerical inversion of the characteristic function. Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: stblcdf, stblinv, stblrnd, makedist # name: # type: sq_string # elements: 1 # length: 42 Stable probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 7 stblrnd # name: # type: sq_string # elements: 1 # length: 915 statistics: r = stblrnd (alpha, beta, gam, delta) statistics: r = stblrnd (alpha, beta, gam, delta, m) statistics: r = stblrnd (alpha, beta, gam, delta, m, n, ...) statistics: r = stblrnd (alpha, beta, gam, delta, [m, n, ...]) Random arrays from the stable distribution. r = stblrnd (alpha, beta, gam, delta) returns a random value drawn from the stable distribution with tail index (first shape parameter) alpha, skewness (second shape parameter) beta, scale parameter gam, and location parameter delta, in the Nolan S0 parameterization. alpha must be in the range (0, 2], beta in [-1, 1], gam positive, and delta real. The parameters must be scalars. stblrnd (alpha, beta, gam, delta, m, n, ...) or stblrnd (..., [m, n, ...]) returns an m-by-n-by-... array, following the size conventions of rand. The values are generated with the Chambers-Mallows-Stuck method. See also: stblpdf, stblcdf, stblinv, makedist # name: # type: sq_string # elements: 1 # length: 43 Random arrays from the stable distribution. # name: # type: sq_string # elements: 1 # length: 7 stdrcdf # name: # type: sq_string # elements: 1 # length: 1601 statistics: p = stdrcdf (x, k, df) statistics: p = stdrcdf (x, k, df, 'upper') Studentized range cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the studentized range distribution for k groups and df degrees of freedom. The size of p is the common size of x, k and df. A scalar input functions as a constant matrix of the same size as the other inputs. The studentized range is the range of k independent standard normal variables divided by an independent sqrt (chi2 / df), where chi2 has a chi-square distribution with df degrees of freedom. It is the distribution behind Tukey's honestly significant difference and the Tukey-Kramer multiple comparison procedure. k must be an integer of at least 2 and df positive, Inf included, which gives the range of k standard normals; otherwise p is NaN. p = stdrcdf (x, k, df, "upper") computes the upper tail probability of the studentized range distribution at the values in x. Each tail is computed directly, so a small upper tail probability keeps its relative accuracy. The distribution is evaluated by numerical integration, accurate to about 1e-13 relative to the tail probability for df up to 1e4, and to about 1e-8 for df of 1e8. MATLAB has no public counterpart of this function. Further information about the studentized range distribution can be found at https://en.wikipedia.org/wiki/Studentized_range_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. See also: stdrinv, stdrpdf, stdrrnd, stdrstat, multcompare # name: # type: sq_string # elements: 1 # length: 57 Studentized range cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 7 stdrinv # name: # type: sq_string # elements: 1 # length: 1018 statistics: x = stdrinv (p, k, df) Inverse of the studentized range cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the studentized range distribution for k groups and df degrees of freedom. The size of x is the common size of p, k and df. A scalar input functions as a constant matrix of the same size as the other inputs. stdrinv (1 - alpha, k, df) is the critical value of Tukey's honestly significant difference at level alpha. k must be an integer of at least 2 and df positive, Inf included; otherwise x is NaN, as it is for p outside [0, 1]. The quantile is found to about 1e-13 relative accuracy. MATLAB has no public counterpart of this function. Further information about the studentized range distribution can be found at https://en.wikipedia.org/wiki/Studentized_range_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. See also: stdrcdf, stdrpdf, stdrrnd, stdrstat, multcompare # name: # type: sq_string # elements: 1 # length: 73 Inverse of the studentized range cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 7 stdrpdf # name: # type: sq_string # elements: 1 # length: 862 statistics: y = stdrpdf (x, k, df) Studentized range probability density function (PDF). For each element of x, compute the probability density function (PDF) of the studentized range distribution for k groups and df degrees of freedom. The size of y is the common size of x, k and df. A scalar input functions as a constant matrix of the same size as the other inputs. k must be an integer of at least 2 and df positive, Inf included; otherwise y is NaN. The density is zero for negative x and, for k greater than 2, at zero. MATLAB has no public counterpart of this function. Further information about the studentized range distribution can be found at https://en.wikipedia.org/wiki/Studentized_range_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. See also: stdrcdf, stdrinv, stdrrnd, stdrstat # name: # type: sq_string # elements: 1 # length: 53 Studentized range probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 7 stdrrnd # name: # type: sq_string # elements: 1 # length: 1251 statistics: r = stdrrnd (k, df) statistics: r = stdrrnd (k, df, rows) statistics: r = stdrrnd (k, df, rows, cols, ...) statistics: r = stdrrnd (k, df, [sz]) Random arrays from the studentized range distribution. r = stdrrnd (k, df) returns an array of random numbers chosen from the studentized range distribution for k groups and df degrees of freedom: the range of k standard normal variables divided by an independent sqrt (chi2 / df). The size of r is the common size of k and df. A scalar input functions as a constant matrix of the same size as the other input. k must be an integer of at least 2 and df positive, Inf included; otherwise NaN is returned. When called with a single size argument, stdrrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. MATLAB has no public counterpart of this function. Further information about the studentized range distribution can be found at https://en.wikipedia.org/wiki/Studentized_range_distribution See also: stdrcdf, stdrinv, stdrpdf, stdrstat # name: # type: sq_string # elements: 1 # length: 54 Random arrays from the studentized range distribution. # name: # type: sq_string # elements: 1 # length: 4 tcdf # name: # type: sq_string # elements: 1 # length: 968 statistics: p = tcdf (x, df) statistics: p = tcdf (x, df, 'upper') Student's T cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the Student's T distribution with df degrees of freedom. The size of p is the common size of x and df. A scalar input functions as a constant matrix of the same size as the other input. p = tcdf (x, df, "upper") computes the upper tail probability of the Student's T distribution with df degrees of freedom, at the values in x. Further information about the Student's T distribution can be found at https://en.wikipedia.org/wiki/Student%27s_t-distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: tinv, tpdf, trnd, tstat # name: # type: sq_string # elements: 1 # length: 51 Student's T cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 4 tinv # name: # type: sq_string # elements: 1 # length: 1228 statistics: x = tinv (p, df) Inverse of the Student's T cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the Student's T distribution with df degrees of freedom. The size of x is the common size of x and df. A scalar input functions as a constant matrix of the same size as the other input. This function is analogous to looking in a table for the t-value of a single-tailed distribution. For very large df (>10000), the inverse of the standard normal distribution is used. Further information about the Student's T distribution can be found at https://en.wikipedia.org/wiki/Student%27s_t-distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. Far in the tails the quantile stays accurate to the last few digits. MATLAB's does not with one degree of freedom, where it evaluates tan (pi (p - 0.5)) and p - 0.5 loses the digits that matter: at p = 10^{-10} it is off by 5 10^{-7}, relative. See also: tcdf, tpdf, trnd, tstat # name: # type: sq_string # elements: 1 # length: 67 Inverse of the Student's T cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 6 tlscdf # name: # type: sq_string # elements: 1 # length: 1192 statistics: p = tlscdf (x, mu, sigma, nu) statistics: p = tlscdf (x, mu, sigma, nu, 'upper') Location-scale Student's T cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the location-scale Student's T distribution with location parameter mu, scale parameter sigma, and nu degrees of freedom. The size of p is the common size of x, mu, sigma, and nu. A scalar input functions as a constant matrix of the same size as the other inputs. p = tlscdf (x, mu, sigma, nu, "upper") computes the upper tail probability of the location-scale Student's T distribution with parameters mu, sigma, and nu, at the values in x. Further information about the location-scale Student's T distribution can be found at https://en.wikipedia.org/wiki/Student%27s_t-distribution#Location-scale_t_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: tlsinv, tlspdf, tlsrnd, tlsfit, tlslike, tlsstat # name: # type: sq_string # elements: 1 # length: 66 Location-scale Student's T cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 6 tlsinv # name: # type: sq_string # elements: 1 # length: 974 statistics: x = tlsinv (p, mu, sigma, nu) Inverse of the location-scale Student's T cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the location-scale Student's T distribution with location parameter mu, scale parameter sigma, and nu degrees of freedom. The size of x is the common size of p, mu, sigma, and nu. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the location-scale Student's T distribution can be found at https://en.wikipedia.org/wiki/Student%27s_t-distribution#Location-scale_t_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: tlscdf, tlspdf, tlsrnd, tlsfit, tlslike, tlsstat # name: # type: sq_string # elements: 1 # length: 82 Inverse of the location-scale Student's T cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 6 tlspdf # name: # type: sq_string # elements: 1 # length: 955 statistics: p = tlspdf (x, mu, sigma, nu) Location-scale Student's T probability density function (PDF). For each element of x, compute the probability density function (PDF) of the location-scale Student's T distribution with location parameter mu, scale parameter sigma, and nu degrees of freedom. The size of y is the common size of x, mu, sigma, and nu. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the location-scale Student's T distribution can be found at https://en.wikipedia.org/wiki/Student%27s_t-distribution#Location-scale_t_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: tlscdf, tlsinv, tlsrnd, tlsfit, tlslike, tlsstat # name: # type: sq_string # elements: 1 # length: 62 Location-scale Student's T probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 6 tlsrnd # name: # type: sq_string # elements: 1 # length: 1320 statistics: r = tlsrnd (mu, sigma, nu) statistics: r = tlsrnd (mu, sigma, nu, rows) statistics: r = tlsrnd (mu, sigma, nu, rows, cols, ...) statistics: r = tlsrnd (mu, sigma, nu, [sz]) Random arrays from the location-scale Student's T distribution. Return a matrix of random samples from the location-scale Student's T distribution with location parameter mu, scale parameter sigma, and nu degrees of freedom. r = tlsrnd (nu) returns an array of random numbers chosen from the location-scale Student's T distribution with location parameter mu, scale parameter sigma, and nu degrees of freedom. The size of r is the common size of mu, sigma, and nu. A scalar input functions as a constant matrix of the same size as the other inputs. When called with a single size argument, tlsrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the location-scale Student's T distribution can be found at https://en.wikipedia.org/wiki/Student%27s_t-distribution#Location-scale_t_distribution See also: tlscdf, tlsinv, tlspdf, tlsfit, tlslike, tlsstat # name: # type: sq_string # elements: 1 # length: 63 Random arrays from the location-scale Student's T distribution. # name: # type: sq_string # elements: 1 # length: 4 tpdf # name: # type: sq_string # elements: 1 # length: 779 statistics: p = tpdf (x, df) Student's T probability density function (PDF). For each element of x, compute the probability density function (PDF) of the Student's T distribution with df degrees of freedom. The size of y is the common size of x and df. A scalar input functions as a constant matrix of the same size as the other input. Further information about the Student's T distribution can be found at https://en.wikipedia.org/wiki/Student%27s_t-distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: tcdf, tinv, trnd, tstat # name: # type: sq_string # elements: 1 # length: 47 Student's T probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 6 tricdf # name: # type: sq_string # elements: 1 # length: 1699 statistics: p = tricdf (x, a, b, c) statistics: p = tricdf (x, a, b, c, 'upper') Triangular cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the triangular distribution with lower limit parameter a, peak location (mode) parameter b, and upper limit parameter c. The size of p is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. p = tricdf (x, a, b, c, "upper") computes the upper tail probability of the triangular distribution with parameters a, b, and c, at the values in x. Note that the order of the parameter input arguments has been changed after statistics version 1.6.3 in order to be MATLAB compatible with the parameters used in the TriangularDistribution probability distribution object. More specifically, the positions of the parameters b and c have been swapped. As a result, the naming conventions no longer coincide with those used in Wikipedia, in which b denotes the upper limit and c denotes the mode or peak parameter. Further information about the triangular distribution can be found at https://en.wikipedia.org/wiki/Triangular_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. MATLAB also accepts integer input here, returning the result in the integer class of the input; Octave rejects it, as it does for every other continuous distribution. See also: triinv, tripdf, trirnd, tristat # name: # type: sq_string # elements: 1 # length: 50 Triangular cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 6 triinv # name: # type: sq_string # elements: 1 # length: 1347 statistics: x = triinv (p, a, b, c) Inverse of the triangular cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the triangular distribution with lower limit parameter a, peak location (mode) parameter b, and upper limit parameter c. The size of x is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Note that the order of the parameter input arguments has been changed after statistics version 1.6.3 in order to be MATLAB compatible with the parameters used in the TriangularDistribution probability distribution object. More specifically, the positions of the parameters b and c have been swapped. As a result, the naming conventions no longer coincide with those used in Wikipedia, in which b denotes the upper limit and c denotes the mode or peak parameter. Further information about the triangular distribution can be found at https://en.wikipedia.org/wiki/Triangular_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: tricdf, tripdf, trirnd, tristat # name: # type: sq_string # elements: 1 # length: 66 Inverse of the triangular cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 6 tripdf # name: # type: sq_string # elements: 1 # length: 1496 statistics: y = tripdf (x, a, b, c) Triangular probability density function (PDF). For each element of x, compute the probability density function (PDF) of the triangular distribution with lower limit parameter a, peak location (mode) parameter b, and upper limit parameter c. The size of y is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Note that the order of the parameter input arguments has been changed after statistics version 1.6.3 in order to be MATLAB compatible with the parameters used in the TriangularDistribution probability distribution object. More specifically, the positions of the parameters b and c have been swapped. As a result, the naming conventions no longer coincide with those used in Wikipedia, in which b denotes the upper limit and c denotes the mode or peak parameter. Further information about the triangular distribution can be found at https://en.wikipedia.org/wiki/Triangular_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. MATLAB also accepts integer input here, returning the result in the integer class of the input; Octave rejects it, as it does for every other continuous distribution. See also: tricdf, triinv, trirnd, tristat # name: # type: sq_string # elements: 1 # length: 46 Triangular probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 6 trirnd # name: # type: sq_string # elements: 1 # length: 1511 statistics: r = trirnd (a, b, c) statistics: r = trirnd (a, b, c, rows) statistics: r = trirnd (a, b, c, rows, cols, ...) statistics: r = trirnd (a, b, c, [sz]) Random arrays from the triangular distribution. r = trirnd (sigma) returns an array of random numbers chosen from the triangular distribution with lower limit parameter a, peak location (mode) parameter b, and upper limit parameter c. The size of r is the common size of a, b, and c. A scalar input functions as a constant matrix of the same size as the other inputs. When called with a single size argument, trirnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Note that the order of the parameter input arguments has been changed after statistics version 1.6.3 in order to be MATLAB compatible with the parameters used in the TriangularDistribution probability distribution object. More specifically, the positions of the parameters b and c have been swapped. As a result, the naming conventions no longer coincide with those used in Wikipedia, in which b denotes the upper limit and c denotes the mode or peak parameter. Further information about the triangular distribution can be found at https://en.wikipedia.org/wiki/Triangular_distribution See also: tricdf, triinv, tripdf, tristat # name: # type: sq_string # elements: 1 # length: 47 Random arrays from the triangular distribution. # name: # type: sq_string # elements: 1 # length: 4 trnd # name: # type: sq_string # elements: 1 # length: 1103 statistics: r = trnd (df) statistics: r = trnd (df, rows) statistics: r = trnd (df, rows, cols, ...) statistics: r = trnd (df, [sz]) Random arrays from the Student's T distribution. Return a matrix of random samples from the Students's T distribution with df degrees of freedom. r = trnd (df) returns an array of random numbers chosen from the Student's T distribution with df degrees of freedom. The size of r is the size of df. A scalar input functions as a constant matrix of the same size as the other inputs. df must be a finite real number greater than 0, otherwise NaN is returned. When called with a single size argument, trnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the Student's T distribution can be found at https://en.wikipedia.org/wiki/Student%27s_t-distribution See also: tcdf, tinv, tpdf, tstat # name: # type: sq_string # elements: 1 # length: 48 Random arrays from the Student's T distribution. # name: # type: sq_string # elements: 1 # length: 7 unidcdf # name: # type: sq_string # elements: 1 # length: 1445 statistics: p = unidcdf (x, N) statistics: p = unidcdf (x, N, 'upper') Discrete uniform cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of a discrete uniform distribution with parameter N, which corresponds to the maximum observable value. unidcdf assumes the integer values in the range [1,N] with equal probability. The size of p is the common size of x and N. A scalar input functions as a constant matrix of the same size as the other inputs. The maximum observable values in N must be positive integers, otherwise NaN is returned. [...] = unidcdf (x, N, "upper") computes the upper tail probability of the discrete uniform distribution with maximum observable value N, at the values in x. Warning: The underlying implementation uses the double class and will only be accurate for N < flintmax (2^{53} on IEEE 754 compatible systems). Further information about the discrete uniform distribution can be found at https://en.wikipedia.org/wiki/Discrete_uniform_distribution Input arguments must be double, single, or an integer type; logical and character arrays are rejected. Integer input is promoted to double, so the result is always a probability. MATLAB is inconsistent here: for several of the discrete distributions it returns the result in the integer class of the input, truncating a probability to 0 or 1. See also: unidinv, unidpdf, unidrnd, unidfit, unidstat # name: # type: sq_string # elements: 1 # length: 56 Discrete uniform cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 7 unidinv # name: # type: sq_string # elements: 1 # length: 1194 statistics: x = unidinv (p, N) Inverse of the discrete uniform cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the discrete uniform distribution with parameter N, which corresponds to the maximum observable value. unidinv assumes the integer values in the range [1,N] with equal probability. The size of x is the common size of p and N. A scalar input functions as a constant matrix of the same size as the other inputs. The maximum observable values in N must be positive integers, otherwise NaN is returned. Warning: The underlying implementation uses the double class and will only be accurate for N < flintmax (2^{53} on IEEE 754 compatible systems). Further information about the discrete uniform distribution can be found at https://en.wikipedia.org/wiki/Discrete_uniform_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: unidcdf, unidpdf, unidrnd, unidfit, unidstat # name: # type: sq_string # elements: 1 # length: 72 Inverse of the discrete uniform cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 7 unidpdf # name: # type: sq_string # elements: 1 # length: 1240 statistics: y = unidpdf (x, N) Discrete uniform probability density function (PDF). For each element of x, compute the probability density function (PDF) of the discrete uniform distribution with parameter N, which corresponds to the maximum observable value. unidpdf assumes the integer values in the range [1,N] with equal probability. The size of x is the common size of p and N. A scalar input functions as a constant matrix of the same size as the other inputs. The maximum observable values in N must be positive integers, otherwise NaN is returned. Warning: The underlying implementation uses the double class and will only be accurate for N < flintmax (2^{53} on IEEE 754 compatible systems). Further information about the discrete uniform distribution can be found at https://en.wikipedia.org/wiki/Discrete_uniform_distribution Input arguments must be double, single, or an integer type; logical and character arrays are rejected. Integer input is promoted to double, so the result is always a probability. MATLAB is inconsistent here: for several of the discrete distributions it returns the result in the integer class of the input, truncating a probability to 0 or 1. See also: unidcdf, unidinv, unidrnd, unidfit, unidstat # name: # type: sq_string # elements: 1 # length: 52 Discrete uniform probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 7 unidrnd # name: # type: sq_string # elements: 1 # length: 1336 statistics: r = unidrnd (N) statistics: r = unidrnd (N, rows) statistics: r = unidrnd (N, rows, cols, ...) statistics: r = unidrnd (N, [sz]) Random arrays from the discrete uniform distribution. r = unidrnd (N) returns an array of random numbers chosen from the discrete uniform distribution with parameter N, which corresponds to the maximum observable value. unidrnd assumes the integer values in the range [1,N] with equal probability. The size of r is the size of N. A scalar input functions as a constant matrix of the same size as the other inputs. The maximum observable values in N must be positive integers, otherwise NaN is returned. When called with a single size argument, unidrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Warning: The underlying implementation uses the double class and will only be accurate for N < flintmax (2^{53} on IEEE 754 compatible systems). Further information about the discrete uniform distribution can be found at https://en.wikipedia.org/wiki/Discrete_uniform_distribution See also: unidcdf, unidinv, unidpdf, unidfit, unidstat # name: # type: sq_string # elements: 1 # length: 53 Random arrays from the discrete uniform distribution. # name: # type: sq_string # elements: 1 # length: 7 unifcdf # name: # type: sq_string # elements: 1 # length: 1271 statistics: p = unifcdf (x, a, b) statistics: p = unifcdf (x, a, b, 'upper') Continuous uniform cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the continuous uniform distribution with parameters a and b, which define the lower and upper bounds of the interval [a, b]. The size of p is the common size of x, a, and b. A scalar input functions as a constant matrix of the same size as the other inputs. [...] = unifcdf (x, a, b, "upper") computes the upper tail probability of the continuous uniform distribution with parameters a, and b, at the values in x. Further information about the continuous uniform distribution can be found at https://en.wikipedia.org/wiki/Continuous_uniform_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. MATLAB also accepts integer input here, returning the result in the integer class of the input; Octave rejects it, as it does for every other continuous distribution. See also: unifinv, unifpdf, unifrnd, unifit, unifstat # name: # type: sq_string # elements: 1 # length: 58 Continuous uniform cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 7 unifinv # name: # type: sq_string # elements: 1 # length: 914 statistics: x = unifinv (p, a, b) Inverse of the continuous uniform cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the continuous uniform distribution with parameters a and b, which define the lower and upper bounds of the interval [a, b]. The size of x is the common size of p, a, and b. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the continuous uniform distribution can be found at https://en.wikipedia.org/wiki/Continuous_uniform_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: unifcdf, unifpdf, unifrnd, unifit, unifstat # name: # type: sq_string # elements: 1 # length: 74 Inverse of the continuous uniform cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 7 unifpdf # name: # type: sq_string # elements: 1 # length: 1063 statistics: y = unifpdf (x, a, b) Continuous uniform probability density function (PDF). For each element of x, compute the probability density function (PDF) of the continuous uniform distribution with parameters a and b, which define the lower and upper bounds of the interval [a, b]. The size of y is the common size of x, a, and b. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the continuous uniform distribution can be found at https://en.wikipedia.org/wiki/Continuous_uniform_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. MATLAB also accepts integer input here, returning the result in the integer class of the input; Octave rejects it, as it does for every other continuous distribution. See also: unifcdf, unifinv, unifrnd, unifit, unifstat # name: # type: sq_string # elements: 1 # length: 54 Continuous uniform probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 7 unifrnd # name: # type: sq_string # elements: 1 # length: 1077 statistics: r = unifrnd (a, b) statistics: r = unifrnd (a, b, rows) statistics: r = unifrnd (a, b, rows, cols, ...) statistics: r = unifrnd (a, b, [sz]) Random arrays from the continuous uniform distribution. r = unifrnd (a, b) returns an array of random numbers chosen from the continuous uniform distribution with parameters a and b, which define the lower and upper bounds of the interval [a, b]. The size of r is the common size of a and b. A scalar input functions as a constant matrix of the same size as the other inputs. When called with a single size argument, unifrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the continuous uniform distribution can be found at https://en.wikipedia.org/wiki/Continuous_uniform_distribution See also: unifcdf, unifinv, unifpdf, unifit, unifstat # name: # type: sq_string # elements: 1 # length: 55 Random arrays from the continuous uniform distribution. # name: # type: sq_string # elements: 1 # length: 5 vmcdf # name: # type: sq_string # elements: 1 # length: 1367 statistics: p = vmcdf (x, mu, k) statistics: p = vmcdf (x, mu, k, 'upper') Von Mises probability density function (PDF). For each element of x, compute the cumulative distribution function (CDF) of the von Mises distribution with location parameter mu and concentration parameter k on the interval [-pi,pi]. The size of p is the common size of x, mu, and k. A scalar input functions as a constant matrix of the same same size as the other inputs. p = vmcdf (x, mu, k, "upper") computes the upper tail probability of the von Mises distribution with parameters mu and k, at the values in x. Note: the CDF of the von Mises distribution is not analytic. Hence, it is calculated by integrating its probability density which is expressed as a series of Bessel functions. Balancing between performance and accuracy, the integration uses a step of 1e-5 on the interval [-pi,pi], which results to an accuracy of about 10 significant digits. Further information about the von Mises distribution can be found at https://en.wikipedia.org/wiki/Von_Mises_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: vminv, vmpdf, vmrnd # name: # type: sq_string # elements: 1 # length: 45 Von Mises probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 5 vminv # name: # type: sq_string # elements: 1 # length: 1182 statistics: x = vminv (p, mu, k) Inverse of the von Mises cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the von Mises distribution with location parameter mu and concentration parameter k on the interval [-pi,pi]. The size of x is the common size of p, mu, and k. A scalar input functions as a constant matrix of the same size as the other inputs. Note: the quantile of the von Mises distribution is not analytic. Hence, it is approximated by a custom searching algorithm using its CDF until it converges up to a tolerance of 1e-5 or 100 iterations. As a result, balancing between performance and accuracy, the accuracy is about 5e-5 for k = 1 and it drops to 5e-5 as k increases. Further information about the von Mises distribution can be found at https://en.wikipedia.org/wiki/Von_Mises_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: vmcdf, vmpdf, vmrnd # name: # type: sq_string # elements: 1 # length: 65 Inverse of the von Mises cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 5 vmpdf # name: # type: sq_string # elements: 1 # length: 830 statistics: y = vmpdf (x, mu, k) Von Mises probability density function (PDF). For each element of x, compute the probability density function (PDF) of the von Mises distribution with location parameter mu and concentration parameter k on the interval [-pi, pi]. The size of y is the common size of x, mu, and k. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the von Mises distribution can be found at https://en.wikipedia.org/wiki/Von_Mises_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: vmcdf, vminv, vmrnd # name: # type: sq_string # elements: 1 # length: 45 Von Mises probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 5 vmrnd # name: # type: sq_string # elements: 1 # length: 1085 statistics: r = vmrnd (mu, k) statistics: r = vmrnd (mu, k, rows) statistics: r = vmrnd (mu, k, rows, cols, ...) statistics: r = vmrnd (mu, k, [sz]) Random arrays from the von Mises distribution. r = vmrnd (mu, k) returns an array of random angles chosen from a von Mises distribution with location parameter mu and concentration parameter k on the interval [-pi, pi]. The size of r is the common size of mu and k. A scalar input functions as a constant matrix of the same size as the other inputs. Both parameters must be finite real numbers and k > 0, otherwise NaN is returned. When called with a single size argument, vmrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the von Mises distribution can be found at https://en.wikipedia.org/wiki/Von_Mises_distribution See also: vmcdf, vminv, vmpdf # name: # type: sq_string # elements: 1 # length: 46 Random arrays from the von Mises distribution. # name: # type: sq_string # elements: 1 # length: 6 wblcdf # name: # type: sq_string # elements: 1 # length: 1811 statistics: p = wblcdf (x) statistics: p = wblcdf (x, lambda) statistics: p = wblcdf (x, lambda, k) statistics: p = wblcdf (..., 'upper') statistics: [p, plo, pup] = wblcdf (x, lambda, k, pcov) statistics: [p, plo, pup] = wblcdf (x, lambda, k, pcov, alpha) statistics: [p, plo, pup] = wblcdf (..., 'upper') Weibull cumulative distribution function (CDF). For each element of x, compute the cumulative distribution function (CDF) of the Weibull distribution with scale parameter lambda and shape parameter k. The size of p is the common size of x, lambda and k. A scalar input functions as a constant matrix of the same size as the other inputs. Default values are lambda = 1, k = 1. When called with three output arguments, [p, plo, pup] it computes the confidence bounds for p when the input parameters lambda and k are estimates. In such case, pcov, a 2-by-2 matrix containing the covariance matrix of the estimated parameters, is necessary. Optionally, alpha has a default value of 0.05, and specifies 100 * (1 - alpha)% confidence bounds. plo and pup are arrays of the same size as p containing the lower and upper confidence bounds. [...] = wblcdf (..., "upper") computes the upper tail probability of the lognormal distribution. Further information about the Weibull distribution can be found at https://en.wikipedia.org/wiki/Weibull_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. The prob.WeibullDistribution class names these same two parameters A and B, after MATLAB. lambda is its A and k is its B. See also: wblinv, wblpdf, wblrnd, wblstat, wblplot # name: # type: sq_string # elements: 1 # length: 47 Weibull cumulative distribution function (CDF). # name: # type: sq_string # elements: 1 # length: 6 wblinv # name: # type: sq_string # elements: 1 # length: 1062 statistics: x = wblinv (p) statistics: x = wblinv (p, lambda) statistics: x = wblinv (p, lambda, k) Inverse of the Weibull cumulative distribution function (iCDF). For each element of p, compute the quantile (the inverse of the CDF) of the Weibull distribution with scale parameter lambda and shape parameter k. The size of x is the common size of p, lambda, and k. A scalar input functions as a constant matrix of the same size as the other inputs. Default values are lambda = 1, k = 1. Further information about the Weibull distribution can be found at https://en.wikipedia.org/wiki/Weibull_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. The prob.WeibullDistribution class names these same two parameters A and B, after MATLAB. lambda is its A and k is its B. See also: wblcdf, wblpdf, wblrnd, wblstat, wblplot # name: # type: sq_string # elements: 1 # length: 63 Inverse of the Weibull cumulative distribution function (iCDF). # name: # type: sq_string # elements: 1 # length: 6 wblpdf # name: # type: sq_string # elements: 1 # length: 1060 statistics: y = wblpdf (x) statistics: y = wblpdf (x, lambda) statistics: y = wblpdf (x, lambda, k) Weibull probability density function (PDF). For each element of x, compute the probability density function (PDF) of the Weibull distribution with scale parameter lambda and shape parameter k. The size of y is the common size of x, lambda, and k. A scalar input functions as a constant matrix of the same size as the other inputs. Default values are lambda = 1, k = 1. Further information about the Weibull distribution can be found at https://en.wikipedia.org/wiki/Weibull_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. The prob.WeibullDistribution class names these same two parameters A and B, after MATLAB. lambda is its A and k is its B. See also: wblcdf, wblinv, wblrnd, wblfit, wbllike, wblstat, wblplot # name: # type: sq_string # elements: 1 # length: 43 Weibull probability density function (PDF). # name: # type: sq_string # elements: 1 # length: 6 wblrnd # name: # type: sq_string # elements: 1 # length: 1196 statistics: r = wblrnd (lambda, k) statistics: r = wblrnd (lambda, k, rows) statistics: r = wblrnd (lambda, k, rows, cols, ...) statistics: r = wblrnd (lambda, k, [sz]) Random arrays from the Weibull distribution. r = wblrnd (lambda, k) returns an array of random numbers chosen from the Weibull distribution with scale parameter lambda and shape parameter k. The size of r is the common size of lambda and k. A scalar input functions as a constant matrix of the same size as the other inputs. Both parameters must be positive reals. When called with a single size argument, wblrnd returns a square matrix with the dimension specified. When called with more than one scalar argument, the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a row vector of dimensions, sz. Further information about the Weibull distribution can be found at https://en.wikipedia.org/wiki/Weibull_distribution The prob.WeibullDistribution class names these same two parameters A and B, after MATLAB. lambda is its A and k is its B. See also: wblcdf, wblinv, wblpdf, wblfit, wbllike, wblstat, wblplot # name: # type: sq_string # elements: 1 # length: 44 Random arrays from the Weibull distribution. # name: # type: sq_string # elements: 1 # length: 7 wienrnd # name: # type: sq_string # elements: 1 # length: 414 statistics: r = wienrnd (t, d, n) Return a simulated realization of the d-dimensional Wiener Process on the interval [0, t]. If d is omitted, d = 1 is used. The first column of the return matrix contains time, the remaining columns contain the Wiener process. The optional parameter n defines the number of summands used for simulating the process over an interval of length 1. If n is omitted, n = 1000 is used. # name: # type: sq_string # elements: 1 # length: 90 Return a simulated realization of the d-dimensional Wiener Process on the interval [0, t]. # name: # type: sq_string # elements: 1 # length: 7 wishpdf # name: # type: sq_string # elements: 1 # length: 875 statistics: y = wishpdf (W, Sigma, df, log_y=false) Compute the probability density function of the Wishart distribution Inputs: A p x p matrix W where to find the PDF. The p x p positive definite matrix Sigma and scalar degrees of freedom parameter df characterizing the Wishart distribution. (For the density to be finite, need df > (p - 1).) If the flag log_y is set, return the log probability density -- this helps avoid underflow when the numerical value of the density is very small Output: y is the probability density of Wishart(Sigma, df) at W. Input arguments must be double or single; integer, logical, and character arrays are rejected. MATLAB accepts a character array and evaluates it at the character codes, which Octave deliberately does not, since a character array is an integer type and integers are refused too. See also: wishrnd, iwishpdf, iwishrnd # name: # type: sq_string # elements: 1 # length: 68 Compute the probability density function of the Wishart distribution # name: # type: sq_string # elements: 1 # length: 7 wishrnd # name: # type: sq_string # elements: 1 # length: 945 statistics: [W, D] = wishrnd (Sigma, df, D, n=1) Return a random matrix sampled from the Wishart distribution with given parameters Inputs: the p x p positive definite matrix Sigma (or the lower-triangular Cholesky factor D of Sigma) and scalar degrees of freedom parameter df. df can be non-integer as long as df > p - 1 Output: a random p x p matrix W from the Wishart(Sigma, df) distribution. If n > 1, then W is p x p x n and holds n such random matrices. (Optionally, the lower-triangular Cholesky factor D of Sigma is also returned.) Averaged across many samples, the mean of W should approach df*Sigma, and the variance of each element W_ij should approach df*(Sigma_ij^2 + Sigma_ii*Sigma_jj) References Yu-Cheng Ku and Peter Bloomfield (2010), Generating Random Wishart Matrices with Fractional Degrees of Freedom in OX, http://www.gwu.edu/~forcpgm/YuChengKu-030510final-WishartYu-ChengKu.pdf See also: wishpdf, iwishpdf, iwishrnd # name: # type: sq_string # elements: 1 # length: 82 Return a random matrix sampled from the Wishart distribution with given parameters statistics-release-2.1.2/inst/Distribution_Functions/evcdf.m000066400000000000000000000231361527311104300242570ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} evcdf (@var{x}) ## @deftypefnx {statistics} {@var{p} =} evcdf (@var{x}, @var{mu}) ## @deftypefnx {statistics} {@var{p} =} evcdf (@var{x}, @var{mu}, @var{sigma}) ## @deftypefnx {statistics} {@var{p} =} evcdf (@dots{}, @qcode{'upper'}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} evcdf (@var{x}, @var{mu}, @var{sigma}, @var{pcov}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} evcdf (@var{x}, @var{mu}, @var{sigma}, @var{pcov}, @var{alpha}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} evcdf (@dots{}, @qcode{'upper'}) ## ## Extreme value cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the extreme value distribution (also known as the Gumbel or the type ## I generalized extreme value distribution) at the values in @var{x} with ## location parameter @var{mu} and scale parameter @var{sigma}. The size of ## @var{p} is the common size of @var{x}, @var{mu} and @var{sigma}. A scalar ## input functions as a constant matrix of the same size as the other inputs. ## ## Default values are @var{mu} = 0 and @var{sigma} = 1. ## ## When called with three output arguments, i.e. @qcode{[@var{p}, @var{plo}, ## @var{pup}]}, @code{evcdf} computes the confidence bounds for @var{p} when the ## input parameters @var{mu} and @var{sigma} are estimates. In such case, ## @var{pcov}, a @math{2*2} matrix containing the covariance matrix of the ## estimated parameters, is necessary. Optionally, @var{alpha}, which has a ## default value of 0.05, specifies the @qcode{100 * (1 - @var{alpha})} percent ## confidence bounds. @var{plo} and @var{pup} are arrays of the same size as ## @var{p} containing the lower and upper confidence bounds. ## ## @code{[@dots{}] = evcdf (@dots{}, "upper")} computes the upper tail ## probability of the extreme value distribution with parameters @var{x0} and ## @var{gamma}, at the values in @var{x}. ## ## The Gumbel distribution is used to model the distribution of the maximum (or ## the minimum) of a number of samples of various distributions. This version ## is suitable for modeling minima. For modeling maxima, use the alternative ## Gumbel CDF, @code{gumbelcdf}. ## ## Further information about the Gumbel distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gumbel_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{evinv, evpdf, evrnd, evfit, evlike, evstat, gumbelcdf} ## @end deftypefn function [varargout] = evcdf (x, varargin) ## Check for valid number of input arguments if (nargin < 1 || nargin > 6) error ("evcdf: invalid number of input arguments."); endif ## Check for 'upper' flag if (nargin > 1 && strcmpi (varargin{end}, 'upper')) uflag = true; varargin(end) = []; elseif (nargin > 1 && ischar (varargin{end}) && ... ! strcmpi (varargin{end}, 'upper')) error ("evcdf: invalid argument for upper tail."); else uflag = false; endif ## Get extra arguments (if they exist) or add defaults if (numel (varargin) > 0) mu = varargin{1}; else mu = 0; endif if (numel (varargin) > 1) sigma = varargin{2}; else sigma = 1; endif if (numel (varargin) > 2) pcov = varargin{3}; ## Check for valid covariance matrix 2x2 if (! isequal (size (pcov), [2, 2])) error ("evcdf: invalid size of covariance matrix."); endif else ## Check that cov matrix is provided if 3 output arguments are requested if (nargout > 1) error ("evcdf: covariance matrix is required for confidence bounds."); endif pcov = []; endif if (numel (varargin) > 3) alpha = varargin{4}; ## Check for valid alpha value if (! isnumeric (alpha) || numel (alpha) !=1 || alpha <= 0 || alpha >= 1) error ("evcdf: invalid value for alpha."); endif else alpha = 0.05; endif ## Check for common size of X, MU, and SIGMA if (! isscalar (x) || ! isscalar (mu) || ! isscalar (sigma)) [err, x, mu, sigma] = common_size (x, mu, sigma); if (err > 0) error ("evcdf: X, MU, and SIGMA must be of common size or scalars."); endif endif ## Check for X, MU, and SIGMA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (sigma))) error ("evcdf: X, MU, and SIGMA must be double or single."); endif ## Check for X, MU, and SIGMA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (sigma)) error ("evcdf: X, MU, and SIGMA must not be complex."); endif ## Return NaNs for out of range parameters. sigma(sigma <= 0) = NaN; ## Compute extreme value cdf z = (x - mu) ./ sigma; if (uflag) p = exp (-exp (z)); else p = -expm1 (-exp (z)); endif ## Check for appropriate class if (isa (x, 'single') || isa (mu, 'single') || isa (sigma, 'single')); is_class = 'single'; else is_class = 'double'; endif ## Prepare output varargout{1} = cast (p, is_class); if (nargout > 1) plo = NaN (size (z), is_class); pup = NaN (size (z), is_class); endif ## Check sigma if (isscalar (sigma)) if (sigma > 0) sigma_p = true (size (z)); else if (nargout == 3) varargout{2} = plo; varargout{3} = pup; endif return; endif else sigma_p = sigma > 0; endif ## Compute confidence bounds (if requested) if (nargout >= 2) zvar = (pcov(1,1) + 2 * pcov(1,2) * z(sigma_p) + ... pcov(2,2) * z(sigma_p) .^ 2) ./ (sigma .^ 2); if (any (zvar < 0)) error ("evcdf: bad covariance matrix."); endif normz = -probit (alpha / 2); halfwidth = normz * sqrt (zvar); zlo = z(sigma_p) - halfwidth; zup = z(sigma_p) + halfwidth; if (uflag) plo(sigma_p) = exp (-exp (zup)); pup(sigma_p) = exp (-exp (zlo)); else plo(sigma_p) = -expm1 (-exp (zlo)); pup(sigma_p) = -expm1 (-exp (zup)); endif varargout{2} = plo; varargout{3} = pup; endif endfunction %!demo %! ## Plot various CDFs from the extreme value distribution %! x = -10:0.01:10; %! p1 = evcdf (x, 0.5, 2); %! p2 = evcdf (x, 1.0, 2); %! p3 = evcdf (x, 1.5, 3); %! p4 = evcdf (x, 3.0, 4); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', x, p4, '-c') %! grid on %! legend ({'μ = 0.5, σ = 2', 'μ = 1.0, σ = 2', ... %! 'μ = 1.5, σ = 3', 'μ = 3.0, σ = 4'}, 'location', 'southeast') %! title ('Extreme value CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, y %! x = [-Inf, 1, 2, Inf]; %! y = [0, 0.6321, 0.9340, 1]; %!assert_equal (evcdf (x, ones (1,4), ones (1,4)), y, 1e-4) %!assert_equal (evcdf (x, 1, ones (1,4)), y, 1e-4) %!assert_equal (evcdf (x, ones (1,4), 1), y, 1e-4) %!assert_equal (evcdf (x, [0, -Inf, NaN, Inf], 1), [0, 1, NaN, NaN], 1e-4) %!assert_equal (evcdf (x, 1, [Inf, NaN, -1, 0]), [NaN, NaN, NaN, NaN], 1e-4) %!assert_equal (evcdf ([x(1:2), NaN, x(4)], 1, 1), [y(1:2), NaN, y(4)], 1e-4) %!assert_equal (evcdf (x, 'upper'), [1, 0.0660, 0.0006, 0], 1e-4) ## Test class of input preserved %!assert_equal (evcdf ([x, NaN], 1, 1), [y, NaN], 1e-4) %!assert_equal (evcdf (single ([x, NaN]), 1, 1), single ([y, NaN]), 1e-4) %!assert_equal (evcdf ([x, NaN], single (1), 1), single ([y, NaN]), 1e-4) %!assert_equal (evcdf ([x, NaN], 1, single (1)), single ([y, NaN]), 1e-4) ## Test input validation %!error evcdf () %!error evcdf (1,2,3,4,5,6,7) %!error evcdf (1, 2, 3, 4, 'uper') %!error ... %! evcdf (ones (3), ones (2), ones (2)) %!error evcdf (2, 3, 4, [1, 2]) %!error ... %! [p, plo, pup] = evcdf (1, 2, 3) %!error [p, plo, pup] = ... %! evcdf (1, 2, 3, [1, 0; 0, 1], 0) %!error [p, plo, pup] = ... %! evcdf (1, 2, 3, [1, 0; 0, 1], 1.22) %!error [p, plo, pup] = ... %! evcdf (1, 2, 3, [1, 0; 0, 1], 'alpha', 'upper') %!error evcdf (int32 (2), 2, 2) %!error evcdf (true, 2, 2) %!error evcdf ('a', 2, 2) %!error evcdf (i, 2, 2) %!error evcdf (2, i, 2) %!error evcdf (2, 2, i) %!error ... %! [p, plo, pup] = evcdf (1, 2, 3, [1, 0; 0, -inf], 0.04) statistics-release-2.1.2/inst/Distribution_Functions/evinv.m000066400000000000000000000202021527311104300243060ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} evinv (@var{p}) ## @deftypefnx {statistics} {@var{x} =} evinv (@var{p}, @var{mu}) ## @deftypefnx {statistics} {@var{x} =} evinv (@var{p}, @var{mu}, @var{sigma}) ## @deftypefnx {statistics} {[@var{x}, @var{xlo}, @var{xup}] =} evinv (@var{p}, @var{mu}, @var{sigma}, @var{pcov}) ## @deftypefnx {statistics} {[@var{x}, @var{xlo}, @var{xup}] =} evinv (@var{p}, @var{mu}, @var{sigma}, @var{pcov}, @var{alpha}) ## ## Inverse of the extreme value cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the extreme value distribution (also known as the Gumbel or the type I ## generalized extreme value distribution) with location parameter @var{mu} and ## scale parameter @var{sigma}. The size of @var{x} is the common size of ## @var{p}, @var{mu} and @var{sigma}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## Default values are @var{mu} = 0 and @var{sigma} = 1. ## ## When called with three output arguments, i.e. @qcode{[@var{x}, @var{xlo}, ## @var{xup}]}, @code{evinv} computes the confidence bounds for @var{x} when the ## input parameters @var{mu} and @var{sigma} are estimates. In such case, ## @var{pcov}, a @math{2*2} matrix containing the covariance matrix of the ## estimated parameters, is necessary. Optionally, @var{alpha}, which has a ## default value of 0.05, specifies the @qcode{100 * (1 - @var{alpha})} percent ## confidence bounds. @var{xlo} and @var{xup} are arrays of the same size as ## @var{x} containing the lower and upper confidence bounds. ## ## The Gumbel distribution is used to model the distribution of the maximum (or ## the minimum) of a number of samples of various distributions. This version ## is suitable for modeling minima. For modeling maxima, use the alternative ## Gumbel iCDF, @code{gumbelinv}. ## ## Further information about the Gumbel distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gumbel_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{evcdf, evpdf, evrnd, evfit, evlike, evstat, gumbelinv} ## @end deftypefn function [x, xlo, xup] = evinv (p, mu, sigma, pcov, alpha) ## Check for valid number of input arguments if (nargin < 1 || nargin > 5) error ("evinv: invalid number of input arguments."); endif ## Add defaults (if missing input arguments) if (nargin < 2) mu = 0; endif if (nargin < 3) sigma = 1; endif ## Check if PCOV is provided when confidence bounds are requested if (nargout > 2) if (nargin < 4) error ("evinv: covariance matrix is required for confidence bounds."); endif ## Check for valid covariance matrix 2x2 if (! isequal (size (pcov), [2, 2])) error ("evinv: invalid size of covariance matrix."); endif ## Check for valid alpha value if (nargin < 5) alpha = 0.05; elseif (! isnumeric (alpha) || numel (alpha) !=1 || alpha <= 0 || alpha >= 1) error ("evinv: invalid value for alpha."); endif endif ## Check for common size of P, MU, and SIGMA if (! isscalar (p) || ! isscalar (mu) || ! isscalar (sigma)) [err, p, mu, sigma] = common_size (p, mu, sigma); if (err > 0) error ("evinv: P, MU, and SIGMA must be of common size or scalars."); endif endif ## Check for P, MU, and SIGMA being double or single if (! (isfloat (p) && isfloat (mu) && isfloat (sigma))) error ("evinv: P, MU, and SIGMA must be double or single."); endif ## Check for P, MU, and SIGMA being reals if (iscomplex (p) || iscomplex (mu) || iscomplex (sigma)) error ("evinv: P, MU, and SIGMA must not be complex."); endif ## Check for appropriate class if (isa (p, 'single') || isa (mu, 'single') || isa (sigma, 'single')); is_class = 'single'; else is_class = 'double'; endif ## Compute inverse of type 1 extreme value cdf k = (eps <= p & p < 1); if (all (k(:))) q = log (-log (1 - p)); else q = zeros (size (p), is_class); q(k) = log (-log (1 - p(k))); ## Return -Inf for p = 0 and Inf for p = 1 q(p < eps) = -Inf; q(p == 1) = Inf; ## Return NaN for out of range values of P q(p < 0 | 1 < p | isnan (p)) = NaN; endif ## Return NaN for out of range values of SIGMA sigma(sigma <= 0) = NaN; x = sigma .* q + mu; ## Compute confidence bounds if requested. if (nargout >= 2) xvar = pcov(1,1) + 2 * pcov(1,2) * q + pcov(2,2) * q .^ 2; if (any (xvar < 0)) error ("evinv: bad covariance matrix."); endif z = -norminv (alpha / 2); halfwidth = z * sqrt (xvar); xlo = x - halfwidth; xup = x + halfwidth; endif endfunction %!demo %! ## Plot various iCDFs from the extreme value distribution %! p = 0.001:0.001:0.999; %! x1 = evinv (p, 0.5, 2); %! x2 = evinv (p, 1.0, 2); %! x3 = evinv (p, 1.5, 3); %! x4 = evinv (p, 3.0, 4); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', p, x4, '-c') %! grid on %! ylim ([-10, 10]) %! legend ({'μ = 0.5, σ = 2', 'μ = 1.0, σ = 2', ... %! 'μ = 1.5, σ = 3', 'μ = 3.0, σ = 4'}, 'location', 'northwest') %! title ('Extreme value iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p, x %! p = [0, 0.05, 0.5 0.95]; %! x = [-Inf, -2.9702, -0.3665, 1.0972]; %!assert_equal (evinv (p), x, 1e-4) %!assert_equal (evinv (p, zeros (1,4), ones (1,4)), x, 1e-4) %!assert_equal (evinv (p, 0, ones (1,4)), x, 1e-4) %!assert_equal (evinv (p, zeros (1,4), 1), x, 1e-4) %!assert_equal (evinv (p, [0, -Inf, NaN, Inf], 1), [-Inf, -Inf, NaN, Inf], 1e-4) %!assert_equal (evinv (p, 0, [Inf, NaN, -1, 0]), [-Inf, NaN, NaN, NaN], 1e-4) %!assert_equal (evinv ([p(1:2), NaN, p(4)], 0, 1), [x(1:2), NaN, x(4)], 1e-4) ## Test class of input preserved %!assert_equal (evinv ([p, NaN], 0, 1), [x, NaN], 1e-4) %!assert_equal (evinv (single ([p, NaN]), 0, 1), single ([x, NaN]), 1e-4) %!assert_equal (evinv ([p, NaN], single (0), 1), single ([x, NaN]), 1e-4) %!assert_equal (evinv ([p, NaN], 0, single (1)), single ([x, NaN]), 1e-4) ## Test input validation %!error evinv () %!error evinv (1,2,3,4,5,6) %!error ... %! evinv (ones (3), ones (2), ones (2)) %!error ... %! [p, plo, pup] = evinv (2, 3, 4, [1, 2]) %!error ... %! [p, plo, pup] = evinv (1, 2, 3) %!error [p, plo, pup] = ... %! evinv (1, 2, 3, [1, 0; 0, 1], 0) %!error [p, plo, pup] = ... %! evinv (1, 2, 3, [1, 0; 0, 1], 1.22) %!error evinv (int32 (2), 2, 2) %!error evinv (true, 2, 2) %!error evinv ('a', 2, 2) %!error evinv (i, 2, 2) %!error evinv (2, i, 2) %!error evinv (2, 2, i) %!error ... %! [p, plo, pup] = evinv (1, 2, 3, [-1, -10; -Inf, -Inf], 0.04) statistics-release-2.1.2/inst/Distribution_Functions/evpdf.m000066400000000000000000000117221527311104300242720ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} evpdf (@var{x}) ## @deftypefnx {statistics} {@var{y} =} evpdf (@var{x}, @var{mu}) ## @deftypefnx {statistics} {@var{y} =} evpdf (@var{x}, @var{mu}, @var{sigma}) ## ## Extreme value probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the extreme value distribution (also known as the Gumbel or the type I ## generalized extreme value distribution) with location parameter @var{mu} and ## scale parameter @var{sigma}. The size of @var{y} is the common size of ## @var{x}, @var{mu} and @var{sigma}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## Default values are @var{mu} = 0 and @var{sigma} = 1. ## ## The Gumbel distribution is used to model the distribution of the maximum (or ## the minimum) of a number of samples of various distributions. This version ## is suitable for modeling minima. For modeling maxima, use the alternative ## Gumbel iCDF, @code{gumbelinv}. ## ## Further information about the Gumbel distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gumbel_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{evcdf, evinv, evrnd, evfit, evlike, evstat, gumbelpdf} ## @end deftypefn function y = evpdf (x, mu, sigma) ## Check for valid number of input arguments if (nargin < 1) error ("evpdf: function called with too few input arguments."); endif ## Add defaults (if missing input arguments) if (nargin < 2) mu = 0; endif if (nargin < 3) sigma = 1; endif ## Check for common size of X, MU, and SIGMA if (! isscalar (x) || ! isscalar (mu) || ! isscalar (sigma)) [err, x, mu, sigma] = common_size (x, mu, sigma); if (err > 0) error ("evpdf: X, MU, and SIGMA must be of common size or scalars."); endif endif ## Check for X, MU, and SIGMA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (sigma))) error ("evpdf: X, MU, and SIGMA must be double or single."); endif ## Check for X, MU, and SIGMA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (sigma)) error ("evpdf: X, MU, and SIGMA must not be complex."); endif ## Return NaNs for out of range parameters sigma(sigma <= 0) = NaN; ## Compute pdf of type 1 extreme value distribution z = (x - mu) ./ sigma; y = exp (z - exp (z)) ./ sigma; ## Force 0 for extreme right tail, instead of getting exp (Inf - Inf) = NaN y(z == Inf) = 0; endfunction %!demo %! ## Plot various PDFs from the Extreme value distribution %! x = -10:0.001:10; %! y1 = evpdf (x, 0.5, 2); %! y2 = evpdf (x, 1.0, 2); %! y3 = evpdf (x, 1.5, 3); %! y4 = evpdf (x, 3.0, 4); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', x, y4, '-c') %! grid on %! ylim ([0, 0.2]) %! legend ({'μ = 0.5, σ = 2', 'μ = 1.0, σ = 2', ... %! 'μ = 1.5, σ = 3', 'μ = 3.0, σ = 4'}, 'location', 'northeast') %! title ('Extreme value PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x, y0, y1 %! x = [-5, 0, 1, 2, 3]; %! y0 = [0.0067, 0.3679, 0.1794, 0.0046, 0]; %! y1 = [0.0025, 0.2546, 0.3679, 0.1794, 0.0046]; %!assert_equal (evpdf (x), y0, 1e-4) %!assert_equal (evpdf (x, zeros (1,5), ones (1,5)), y0, 1e-4) %!assert_equal (evpdf (x, ones (1,5), ones (1,5)), y1, 1e-4) ## Test input validation %!error evpdf () %!error ... %! evpdf (ones (3), ones (2), ones (2)) %!error evpdf (int32 (2), 2, 2) %!error evpdf (true, 2, 2) %!error evpdf ('a', 2, 2) %!error evpdf (i, 2, 2) %!error evpdf (2, i, 2) %!error evpdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/evrnd.m000066400000000000000000000153731527311104300243120ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} evrnd (@var{mu}, @var{sigma}) ## @deftypefnx {statistics} {@var{r} =} evrnd (@var{mu}, @var{sigma}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} evrnd (@var{mu}, @var{sigma}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} evrnd (@var{mu}, @var{sigma}, [@var{sz}]) ## ## Random arrays from the extreme value distribution. ## ## @code{@var{r} = evrnd (@var{mu}, @var{sigma})} returns an array of random ## numbers chosen from the extreme value distribution (also known as the Gumbel ## or the type I generalized extreme value distribution) with location ## parameter @var{mu} and scale parameter @var{sigma}. The size of @var{r} is ## the common size of @var{mu} and @var{sigma}. A scalar input functions as a ## constant matrix of the same size as the other inputs. ## ## When called with a single size argument, @code{evrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## The Gumbel distribution is used to model the distribution of the maximum (or ## the minimum) of a number of samples of various distributions. This version ## is suitable for modeling minima. For modeling maxima, use the alternative ## Gumbel iCDF, @code{gumbelinv}. ## ## Further information about the Gumbel distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gumbel_distribution} ## ## @seealso{evcdf, evinv, evpdf, evfit, evlike, evstat} ## @end deftypefn function r = evrnd (mu, sigma, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("evrnd: function called with too few input arguments."); endif ## Check for common size of MU and SIGMA if (! isscalar (mu) || ! isscalar (sigma)) [retval, mu, sigma] = common_size (mu, sigma); if (retval > 0) error ("evrnd: MU and SIGMA must be of common size or scalars."); endif endif ## Check for MU and SIGMA being reals if (iscomplex (mu) || iscomplex (sigma)) error ("evrnd: MU and SIGMA must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (mu); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("evrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("evrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (mu) && ! isequal (size (mu), size (ones (sz)))) error ("evrnd: MU and SIGMA must be scalars or of size SZ."); endif ## Check for class type if (isa (mu, 'single') || isa (sigma, 'single')) cls = 'single'; else cls = 'double'; endif ## Return NaNs for out of range values of SIGMA sigma(sigma < 0) = NaN; ## Generate uniform random values, and apply the extreme value inverse CDF. r = log (-log (rand (sz, cls))) .* sigma + mu; endfunction ## Test output %!assert_equal (size (evrnd (1, 1)), [1 1]) %!assert_equal (size (evrnd (1, ones (2,1))), [2, 1]) %!assert_equal (size (evrnd (1, ones (2,2))), [2, 2]) %!assert_equal (size (evrnd (ones (2,1), 1)), [2, 1]) %!assert_equal (size (evrnd (ones (2,2), 1)), [2, 2]) %!assert_equal (size (evrnd (1, 1, 3)), [3, 3]) %!assert_equal (size (evrnd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (evrnd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (evrnd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (evrnd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (evrnd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (evrnd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (evrnd (1, 1, [])), [0, 0]) %!assert_equal (size (evrnd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (evrnd (1, 2, -1)), [0, 0]) %!assert_equal (size (evrnd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (evrnd (1, 2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (evrnd (1, 1)), "double") %!assert_equal (class (evrnd (1, single (1))), "single") %!assert_equal (class (evrnd (1, single ([1, 1]))), "single") %!assert_equal (class (evrnd (single (1), 1)), "single") %!assert_equal (class (evrnd (single ([1, 1]), 1)), "single") ## Test input validation %!error evrnd () %!error evrnd (1) %!error ... %! evrnd (ones (3), ones (2)) %!error ... %! evrnd (ones (2), ones (3)) %!error evrnd (i, 2, 3) %!error evrnd (1, i, 3) %!error ... %! evrnd (1, 2, 1.2) %!error ... %! evrnd (1, 2, ones (2)) %!error ... %! evrnd (1, 2, [2 0 2.5]) %!error ... %! evrnd (1, 2, 2, 1.5, 5) %!error ... %! evrnd (2, ones (2), 3) %!error ... %! evrnd (2, ones (2), [3, 2]) %!error ... %! evrnd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/expcdf.m000066400000000000000000000217041527311104300244400ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} expcdf (@var{x}) ## @deftypefnx {statistics} {@var{p} =} expcdf (@var{x}, @var{mu}) ## @deftypefnx {statistics} {@var{p} =} expcdf (@dots{}, @qcode{'upper'}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} expcdf (@var{x}, @var{mu}, @var{pcov}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} expcdf (@var{x}, @var{mu}, @var{pcov}, @var{alpha}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} expcdf (@dots{}, @qcode{'upper'}) ## ## Exponential cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the exponential distribution with mean parameter @var{mu}. The size ## of @var{p} is the common size of @var{x} and @var{mu}. A scalar input ## functions as a constant matrix of the same size as the other inputs. ## ## Default value is @var{mu} = 1. ## ## A common alternative parameterization of the exponential distribution is to ## use the parameter @math{λ} defined as the mean number of events in an ## interval as opposed to the parameter @math{μ}, which is the mean wait time ## for an event to occur. @math{λ} and @math{μ} are reciprocals, ## i.e. @math{μ = 1 / λ}. ## ## When called with three output arguments, i.e. @qcode{[@var{p}, @var{plo}, ## @var{pup}]}, @code{expcdf} computes the confidence bounds for @var{p} when ## the input parameter @var{mu} is an estimate. In such case, @var{pcov}, a ## scalar value with the variance of the estimated parameter @var{mu}, is ## necessary. Optionally, @var{alpha}, which has a default value of 0.05, ## specifies the @qcode{100 * (1 - @var{alpha})} percent confidence bounds. ## @var{plo} and @var{pup} are arrays of the same size as @var{p} containing the ## lower and upper confidence bounds. ## ## @code{[@dots{}] = expcdf (@dots{}, "upper")} computes the upper tail ## probability of the exponential distribution with parameter @var{mu}, at the ## values in @var{x}. ## ## Further information about the exponential distribution can be found at ## @url{https://en.wikipedia.org/wiki/Exponential_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{expinv, exppdf, exprnd, expfit, explike, expstat} ## @end deftypefn function [varargout] = expcdf (x, varargin) ## Check for valid number of input arguments if (nargin < 1 || nargin > 5) error ("expcdf: invalid number of input arguments."); endif ## Check for "upper" flag if (nargin > 1 && strcmpi (varargin{end}, 'upper')) uflag = true; varargin(end) = []; elseif (nargin > 1 && ischar (varargin{end}) && ... ! strcmpi (varargin{end}, 'upper')) error ("expcdf: invalid argument for upper tail."); else uflag = false; endif ## Get extra arguments (if they exist) or add defaults if (numel (varargin) > 0) mu = varargin{1}; else mu = 1; endif if (numel (varargin) > 1) pcov = varargin{2}; ## Check for variance being a scalar if (! isscalar (pcov)) error ("expcdf: invalid size of variance, PCOV must be a scalar."); endif if (pcov < 0) error ("expcdf: variance, PCOV, cannot be negative."); endif else ## Check that cov matrix is provided if 3 output arguments are requested if (nargout > 1) error ("expcdf: variance, PCOV, is required for confidence bounds."); endif pcov = []; endif if (numel (varargin) > 2) alpha = varargin{3}; ## Check for valid alpha value if (! isnumeric (alpha) || numel (alpha) != 1 || alpha <= 0 || alpha >= 1) error ("expcdf: invalid value for alpha."); endif else alpha = 0.05; endif ## Check for common size of X and MU if (! isscalar (x) || ! isscalar (mu)) [err, x, mu] = common_size (x, mu); if (err > 0) error ("expcdf: X and MU must be of common size or scalars."); endif endif ## Check for X and MU being double or single if (! (isfloat (x) && isfloat (mu))) error ("expcdf: X and MU must be double or single."); endif ## Check for X and MU being reals if (iscomplex (x) || iscomplex (mu)) error ("expcdf: X and MU must not be complex."); endif ## Check for appropriate class if (isa (x, 'single') || isa (mu, 'single')); is_class = 'single'; else is_class = 'double'; endif ## Return NaNs for out of range parameters. mu(mu <= 0) = NaN; ## Compute P value for exponential cdf z = x ./ mu; ## Force 0 for negative X z(z < 0) = 0; ## Check uflag if (uflag) p = exp (-z); else p = -expm1 (-z); endif ## Prepare output varargout{1} = cast (p, is_class); if (nargout > 1) plo = NaN (size (z), is_class); pup = NaN (size (z), is_class); endif ## Compute confidence bounds (if requested) if (nargout >= 2) ## Convert to log scale log_z = log (z); norm_z = -probit (alpha / 2); halfwidth = norm_z * sqrt (pcov ./ (mu .^ 2)); zlo = log_z - halfwidth; zup = log_z + halfwidth; ## Convert to original scale if (uflag) plo = exp (-exp (zup)); pup = exp (-exp (zlo)); else plo = - expm1 (-exp (zlo)); pup = - expm1 (-exp (zup)); endif ## Prepare output varargout{2} = plo; varargout{3} = pup; endif endfunction %!demo %! ## Plot various CDFs from the exponential distribution %! x = 0:0.01:5; %! p1 = expcdf (x, 2/3); %! p2 = expcdf (x, 1.0); %! p3 = expcdf (x, 2.0); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r') %! grid on %! legend ({'μ = 2/3', 'μ = 1', 'μ = 2'}, 'location', 'southeast') %! title ('Exponential CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, p %! x = [-1 0 0.5 1 Inf]; %! p = [0, 1 - exp(-x(2:end)/2)]; %!assert_equal (expcdf (x, 2 * ones (1, 5)), p, 1e-16) %!assert_equal (expcdf (x, 2), p, 1e-16) %!assert_equal (expcdf (x, 2 * [1, 0, NaN, 1, 1]), [0, NaN, NaN, p(4:5)], 1e-16) %!assert_equal (expcdf ([x, NaN], 2), [p, NaN], 1e-16) ## Test class of input preserved %!assert_equal (expcdf (single ([x, NaN]), 2), single ([p, NaN])) %!assert_equal (expcdf ([x, NaN], single (2)), single ([p, NaN])) ## Test values against MATLAB output %!test %! [p, plo, pup] = expcdf (1, 2, 3); %! assert_equal (p, 0.39346934028737, 1e-14); %! assert_equal (plo, 0.08751307220484, 1e-14); %! assert_equal (pup, 0.93476821257933, 1e-14); %!test %! [p, plo, pup] = expcdf (1, 2, 2, 0.1); %! assert_equal (p, 0.39346934028737, 1e-14); %! assert_equal (plo, 0.14466318041675, 1e-14); %! assert_equal (pup, 0.79808291849140, 1e-14); %!test %! [p, plo, pup] = expcdf (1, 2, 2, 0.1, 'upper'); %! assert_equal (p, 0.60653065971263, 1e-14); %! assert_equal (plo, 0.20191708150860, 1e-14); %! assert_equal (pup, 0.85533681958325, 1e-14); ## Test input validation %!error expcdf () %!error expcdf (1, 2 ,3 ,4 ,5, 6) %!error expcdf (1, 2, 3, 4, 'uper') %!error ... %! expcdf (ones (3), ones (2)) %!error ... %! expcdf (2, 3, [1, 2]) %!error ... %! [p, plo, pup] = expcdf (1, 2) %!error [p, plo, pup] = ... %! expcdf (1, 2, 3, 0) %!error [p, plo, pup] = ... %! expcdf (1, 2, 3, 1.22) %!error [p, plo, pup] = ... %! expcdf (1, 2, 3, 'alpha', 'upper') %!error expcdf (int32 (2), 2) %!error expcdf (true, 2) %!error expcdf ('a', 2) %!error expcdf (i, 2) %!error expcdf (2, i) %!error ... %! [p, plo, pup] = expcdf (1, 2, -1, 0.04) statistics-release-2.1.2/inst/Distribution_Functions/expinv.m000066400000000000000000000173421527311104300245030ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} expinv (@var{p}) ## @deftypefnx {statistics} {@var{x} =} expinv (@var{p}, @var{mu}) ## @deftypefnx {statistics} {[@var{x}, @var{xlo}, @var{xup}] =} expinv (@var{p}, @var{mu}, @var{pcov}) ## @deftypefnx {statistics} {[@var{x}, @var{xlo}, @var{xup}] =} expinv (@var{p}, @var{mu}, @var{pcov}, @var{alpha}) ## ## Inverse of the exponential cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the exponential distribution with mean @var{mu}. The size of @var{x} is the ## common size of @var{p} and @var{mu}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## Default value is @var{mu} = 1. ## ## A common alternative parameterization of the exponential distribution is to ## use the parameter @math{λ} defined as the mean number of events in an ## interval as opposed to the parameter @math{μ}, which is the mean wait time ## for an event to occur. @math{λ} and @math{μ} are reciprocals, ## i.e. @math{μ = 1 / λ}. ## ## When called with three output arguments, i.e. @qcode{[@var{x}, @var{xlo}, ## @var{xup}]}, @code{expinv} computes the confidence bounds for @var{x} when ## the input parameter @var{mu} is an estimate. In such case, @var{pcov}, a ## scalar value with the variance of the estimated parameter @var{mu}, is ## necessary. Optionally, @var{alpha}, which has a default value of 0.05, ## specifies the @qcode{100 * (1 - @var{alpha})} percent confidence bounds. ## @var{xlo} and @var{xup} are arrays of the same size as @var{x} containing the ## lower and upper confidence bounds. ## ## Further information about the exponential distribution can be found at ## @url{https://en.wikipedia.org/wiki/Exponential_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{expcdf, exppdf, exprnd, expfit, explike, expstat} ## @end deftypefn function [varargout] = expinv (p, varargin) ## Check for valid number of input arguments if (nargin < 1 || nargin > 4) error ("expinv: invalid number of input arguments."); endif ## Get extra arguments (if they exist) or add defaults if (numel (varargin) > 0) mu = varargin{1}; else mu = 1; endif if (numel (varargin) > 1) pcov = varargin{2}; ## Check for variance being a scalar if (! isscalar (pcov)) error ("expinv: invalid size of variance, PCOV must be a scalar."); endif if (pcov < 0) error ("expinv: variance, PCOV, cannot be negative."); endif else ## Check that cov matrix is provided if 3 output arguments are requested if (nargout > 1) error ("expinv: variance, PCOV, is required for confidence bounds."); endif pcov = []; endif if (numel (varargin) > 2) alpha = varargin{3}; ## Check for valid alpha value if (! isnumeric (alpha) || numel (alpha) != 1 || alpha <= 0 || alpha >= 1) error ("expinv: invalid value for alpha."); endif else alpha = 0.05; endif ## Check for common size of P and MU if (! isscalar (p) || ! isscalar (mu)) [retval, p, mu] = common_size (p, mu); if (retval > 0) error ("expinv: P and MU must be of common size or scalars."); endif endif ## Check for P and MU being double or single if (! (isfloat (p) && isfloat (mu))) error ("expinv: P and MU must be double or single."); endif ## Check for P and MU being reals if (iscomplex (p) || iscomplex (mu)) error ("expinv: P and MU must not be complex."); endif ## Check for appropriate class if (isa (p, 'single') || isa (mu, 'single')); is_class = 'single'; else is_class = 'double'; endif ## Create output matrix if (isa (p, 'single') || isa (mu, 'single')) x = NaN (size (p), 'single'); else x = NaN (size (p)); endif ## Handle edge cases k = (p == 1) & (mu > 0); x(k) = Inf; ## Handle valid cases k = (p >= 0) & (p < 1) & (mu > 0); if (isscalar (mu)) x(k) = - mu * log (1 - p(k)); else x(k) = - mu(k) .* log (1 - p(k)); endif ## Prepare output varargout{1} = cast (x, is_class); if (nargout > 1) xlo = NaN (size (z), is_class); xup = NaN (size (z), is_class); endif ## Compute confidence bounds (if requested) if (nargout >= 2) ## Convert to log scale log_x = log (x); z = -probit (alpha / 2); halfwidth = z * sqrt (pcov ./ (mu.^2)); ## Convert to original scale xlo = exp (log_x - halfwidth); xup = exp (log_x + halfwidth); ## Prepare output varargout{2} = plo; varargout{3} = pup; endif endfunction %!demo %! ## Plot various iCDFs from the exponential distribution %! p = 0.001:0.001:0.999; %! x1 = expinv (p, 2/3); %! x2 = expinv (p, 1.0); %! x3 = expinv (p, 2.0); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r') %! grid on %! ylim ([0, 5]) %! legend ({'μ = 2/3', 'μ = 1', 'μ = 2'}, 'location', 'northwest') %! title ('Exponential iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p %! p = [-1 0 0.3934693402873666 1 2]; %!assert_equal (expinv (p, 2*ones (1,5)), [NaN 0 1 Inf NaN], eps) %!assert_equal (expinv (p, 2), [NaN 0 1 Inf NaN], eps) %!assert_equal (expinv (p, 2*[1 0 NaN 1 1]), [NaN NaN NaN Inf NaN], eps) %!assert_equal (expinv ([p(1:2) NaN p(4:5)], 2), [NaN 0 NaN Inf NaN], eps) ## Test class of input preserved %!assert_equal (expinv ([p, NaN], 2), [NaN 0 1 Inf NaN NaN], eps) %!assert_equal (expinv (single ([p, NaN]), 2), single ([NaN 0 1 Inf NaN NaN]), eps) %!assert_equal (expinv ([p, NaN], single (2)), single ([NaN 0 1 Inf NaN NaN]), eps) ## Test input validation %!error expinv () %!error expinv (1, 2 ,3 ,4 ,5) %!error ... %! expinv (ones (3), ones (2)) %!error ... %! expinv (2, 3, [1, 2]) %!error ... %! [x, xlo, xup] = expinv (1, 2) %!error [x, xlo, xup] = ... %! expinv (1, 2, 3, 0) %!error [x, xlo, xup] = ... %! expinv (1, 2, 3, 1.22) %!error [x, xlo, xup] = ... %! expinv (1, 2, 3, [0.05, 0.1]) %!error expinv (int32 (2), 2) %!error expinv (true, 2) %!error expinv ('a', 2) %!error expinv (i, 2) %!error expinv (2, i) %!error ... %! [x, xlo, xup] = expinv (1, 2, -1, 0.04) statistics-release-2.1.2/inst/Distribution_Functions/exppdf.m000066400000000000000000000114261527311104300244550ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} exppdf (@var{x}) ## @deftypefnx {statistics} {@var{y} =} exppdf (@var{x}, @var{mu}) ## ## Exponential probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the exponential distribution with mean parameter @var{mu}. The size of ## @var{y} is the common size of @var{x} and @var{mu}. A scalar input functions ## as a constant matrix of the same size as the other inputs. ## ## Default value for @var{mu} = 1. ## ## A common alternative parameterization of the exponential distribution is to ## use the parameter @math{λ} defined as the mean number of events in an ## interval as opposed to the parameter @math{μ}, which is the mean wait time ## for an event to occur. @math{λ} and @math{μ} are reciprocals, ## i.e. @math{μ = 1 / λ}. ## ## Further information about the exponential distribution can be found at ## @url{https://en.wikipedia.org/wiki/Exponential_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{expcdf, expinv, exprnd, expfit, explike, expstat} ## @end deftypefn function y = exppdf (x, mu) ## Check for valid number of input arguments if (nargin < 1) error ("exppdf: function called with too few input arguments."); endif ## Add defaults (if missing input arguments) if (nargin < 2) mu = 0; endif ## Check for common size of X and MU if (! isscalar (x) || ! isscalar (mu)) [retval, x, mu] = common_size (x, mu); if (retval > 0) error ("exppdf: X and MU must be of common size or scalars."); endif endif ## Check for X and MU being double or single if (! (isfloat (x) && isfloat (mu))) error ("exppdf: X and MU must be double or single."); endif ## Check for X and MU being reals if (iscomplex (x) || iscomplex (mu)) error ("exppdf: X and MU must not be complex."); endif ## Check for appropriate class if (isa (x, 'single') || isa (mu, 'single')) y = zeros (size (x), 'single'); else y = zeros (size (x)); endif k = isnan (x) | ! (mu > 0); y(k) = NaN; k = (x >= 0) & (x < Inf) & (mu > 0); if (isscalar (mu)) y(k) = exp (-x(k) / mu) / mu; else y(k) = exp (-x(k) ./ mu(k)) ./ mu(k); endif endfunction %!demo %! ## Plot various PDFs from the exponential distribution %! x = 0:0.01:5; %! y1 = exppdf (x, 2/3); %! y2 = exppdf (x, 1.0); %! y3 = exppdf (x, 2.0); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r') %! grid on %! ylim ([0, 1.5]) %! legend ({'μ = 2/3', 'μ = 1', 'μ = 2'}, 'location', 'northeast') %! title ('Exponential PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x,y %! x = [-1 0 0.5 1 Inf]; %! y = gampdf (x, 1, 2); %!assert_equal (exppdf (x, 2*ones (1,5)), y) %!assert_equal (exppdf (x, 2*[1 0 NaN 1 1]), [y(1) NaN NaN y(4:5)]) %!assert_equal (exppdf ([x, NaN], 2), [y, NaN]) ## Test class of input preserved %!assert_equal (exppdf (single ([x, NaN]), 2), single ([y, NaN])) %!assert_equal (exppdf ([x, NaN], single (2)), single ([y, NaN])) ## Test input validation %!error exppdf () %!error exppdf (1,2,3) %!error ... %! exppdf (ones (3), ones (2)) %!error ... %! exppdf (ones (2), ones (3)) %!error exppdf (int32 (2), 2) %!error exppdf (true, 2) %!error exppdf ('a', 2) %!error exppdf (i, 2) %!error exppdf (2, i) statistics-release-2.1.2/inst/Distribution_Functions/exprnd.m000066400000000000000000000135741527311104300244750ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} exprnd (@var{mu}) ## @deftypefnx {statistics} {@var{r} =} exprnd (@var{mu}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} exprnd (@var{mu}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} exprnd (@var{mu}, [@var{sz}]) ## ## Random arrays from the exponential distribution. ## ## @code{@var{r} = exprnd (@var{mu})} returns an array of random numbers chosen ## from the exponential distribution with mean parameter @var{mu}. The size of ## @var{r} is the size of @var{mu}. ## ## When called with a single size argument, @code{exprnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## A common alternative parameterization of the exponential distribution is to ## use the parameter @math{λ} defined as the mean number of events in an ## interval as opposed to the parameter @math{μ}, which is the mean wait time ## for an event to occur. @math{λ} and @math{μ} are reciprocals, ## i.e. @math{μ = 1 / λ}. ## ## Further information about the exponential distribution can be found at ## @url{https://en.wikipedia.org/wiki/Exponential_distribution} ## ## @seealso{expcdf, expinv, exppdf, expfit, explike, expstat} ## @end deftypefn function r = exprnd (mu, varargin) ## Check for valid number of input arguments if (nargin < 1) error ("exprnd: function called with too few input arguments."); endif ## Check for MU being real if (iscomplex (mu)) error ("exprnd: MU must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 1) sz = size (mu); elseif (nargin == 2) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("exprnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 2) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("exprnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (mu) && ! isequal (size (mu), size (ones (sz)))) error ("exprnd: MU must be scalar or of size SZ."); endif ## Check for class type if (isa (mu, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from exponential distribution if (isscalar (mu)) if ((mu > 0) && (mu < Inf)) r = rande (sz, cls) * mu; else r = NaN (sz, cls); endif else r = NaN (sz, cls); k = (mu > 0) & (mu < Inf); r(k) = rande (sum (k(:)), 1, cls) .* mu(k)(:); endif endfunction ## Test output %!assert_equal (size (exprnd (2)), [1, 1]) %!assert_equal (size (exprnd (ones (2,1))), [2, 1]) %!assert_equal (size (exprnd (ones (2,2))), [2, 2]) %!assert_equal (size (exprnd (1, 3)), [3, 3]) %!assert_equal (size (exprnd (1, [4 1])), [4, 1]) %!assert_equal (size (exprnd (1, 4, 1)), [4, 1]) %!assert_equal (size (exprnd (1, 4, 1)), [4, 1]) %!assert_equal (size (exprnd (1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (exprnd (1, 0, 1)), [0, 1]) %!assert_equal (size (exprnd (1, 1, 0)), [1, 0]) %!assert_equal (size (exprnd (1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (exprnd (1, [])), [0, 0]) %!assert_equal (size (exprnd (1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (exprnd (1, -1)), [0, 0]) %!assert_equal (size (exprnd (1, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (exprnd (1, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (exprnd (2)), "double") %!assert_equal (class (exprnd (single (2))), "single") %!assert_equal (class (exprnd (single ([2 2]))), "single") ## Test input validation %!error exprnd () %!error exprnd (i) %!error ... %! exprnd (1, 1.2) %!error ... %! exprnd (1, ones (2)) %!error ... %! exprnd (1, [2 0 2.5]) %!error ... %! exprnd (ones (2), ones (2)) %!error ... %! exprnd (1, 2, 1.5, 5) %!error exprnd (ones (2,2), 3) %!error exprnd (ones (2,2), [3, 2]) %!error exprnd (ones (2,2), 2, 3) statistics-release-2.1.2/inst/Distribution_Functions/fcdf.m000066400000000000000000000164311527311104300240720ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} fcdf (@var{x}, @var{df1}, @var{df2}) ## @deftypefnx {statistics} {@var{p} =} fcdf (@var{x}, @var{df1}, @var{df2}, @qcode{'upper'}) ## ## @math{F}-cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the @math{F}-distribution with @var{df1} and @var{df2} degrees of ## freedom. The size of @var{p} is the common size of @var{x}, @var{df1}, and ## @var{df2}. A scalar input functions as a constant matrix of the same size as ## the other inputs. ## ## @code{@var{p} = fcdf (@var{x}, @var{df1}, @var{df2}, "upper")} computes the ## upper tail probability of the @math{F}-distribution with @var{df1} and ## @var{df2} degrees of freedom, at the values in @var{x}. ## ## Further information about the @math{F}-distribution can be found at ## @url{https://en.wikipedia.org/wiki/F-distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{finv, fpdf, frnd, fstat} ## @end deftypefn function p = fcdf (x, df1, df2, uflag) ## Check for valid number of input arguments if (nargin < 3) error ("fcdf: function called with too few input arguments."); endif ## Check for "upper" flag if (nargin > 3 && strcmpi (uflag, 'upper')) notnan = ! isnan (x); x(notnan) = 1 ./ max (0, x(notnan)); tmp=df1; df1=df2; df2=tmp; elseif (nargin > 3 && ! strcmpi (uflag, 'upper')) error ("fcdf: invalid argument for upper tail."); endif ## Check for common size of X, DF1, and DF2 if (! isscalar (x) || ! isscalar (df1) || ! isscalar (df2)) [err, x, df1, df2] = common_size (x, df1, df2); if (err > 0) error ("fcdf: X, DF1, and DF2 must be of common size or scalars."); endif endif ## Check for X, DF1, and DF2 being double or single if (! (isfloat (x) && isfloat (df1) && isfloat (df2))) error ("fcdf: X, DF1, and DF2 must be double or single."); endif ## Check for X, DF1, and DF2 being reals if (iscomplex (x) || iscomplex (df1) || iscomplex (df2)) error ("fcdf: X, DF1, and DF2 must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (df1, 'single') || isa (df2, 'single')) p = zeros (size (x), 'single'); else p = zeros (size (x)); endif ## Check X for NaNs while DFs <= 0 and make P = NaNs make_nan = (df1 <= 0 | df2 <= 0 | isnan (x) | isnan (df1) | isnan (df2)); p(make_nan) = NaN; ## Check remaining valid X for Inf values and make P = 1 is_inf = (x == Inf) & ! make_nan; if any (is_inf(:)) p(is_inf) = 1; make_nan = (make_nan | is_inf); endif ## Compute P when X > 0. k = find (x > 0 & ! make_nan & isfinite (df1) & isfinite (df2)); if (any (k)) k1 = (df2(k) <= x(k) .* df1(k)); if (any (k1)) kk = k(k1); xx = df2(kk) ./ (df2(kk) + x(kk) .* df1(kk)); p(kk) = betainc (xx, df2(kk)/2, df1(kk)/2, 'upper'); endif if (any (! k1)) kk = k(! k1); num = df1(kk) .* x(kk); xx = num ./ (num + df2(kk)); p(kk) = betainc (xx, df1(kk)/2, df2(kk)/2, 'lower'); endif endif if any (! isfinite (df1(:)) | ! isfinite (df2(:))) k = find (x > 0 & ! make_nan & isfinite (df1) & ! isfinite (df2) & df2 > 0); if (any (k)) p(k) = gammainc (df1(k) .* x(k) ./ 2, df1(k) ./ 2, 'lower'); endif k = find (x > 0 & ! make_nan & ! isfinite (df1) & df1 > 0 & isfinite (df2)); if (any (k)) p(k) = gammainc (df2(k) ./ x(k) ./ 2, df2(k) ./ 2, 'upper'); endif k = find (x > 0 & ! make_nan & ! isfinite (df1) & df1 > 0 & ... ! isfinite (df2) & df2 > 0); if (any (k)) if (nargin >= 4 && x(k) == 1) p(k) = 0; else p(k) = (x(k)>=1); endif endif endif endfunction %!demo %! ## Plot various CDFs from the F distribution %! x = 0.01:0.01:4; %! p1 = fcdf (x, 1, 2); %! p2 = fcdf (x, 2, 1); %! p3 = fcdf (x, 5, 2); %! p4 = fcdf (x, 10, 1); %! p5 = fcdf (x, 100, 100); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', x, p4, '-c', x, p5, '-m') %! grid on %! legend ({'df1 = 1, df2 = 2', 'df1 = 2, df2 = 1', ... %! 'df1 = 5, df2 = 2', 'df1 = 10, df2 = 1', ... %! 'df1 = 100, df2 = 100'}, 'location', 'southeast') %! title ('F CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, y %! x = [-1, 0, 0.5, 1, 2, Inf]; %! y = [0, 0, 1/3, 1/2, 2/3, 1]; %!assert_equal (fcdf (x, 2*ones (1,6), 2*ones (1,6)), y, eps) %!assert_equal (fcdf (x, 2, 2*ones (1,6)), y, eps) %!assert_equal (fcdf (x, 2*ones (1,6), 2), y, eps) %!assert_equal (fcdf (x, [0 NaN Inf 2 2 2], 2), [NaN NaN 0.1353352832366127 y(4:6)], eps) %!assert_equal (fcdf (x, 2, [0 NaN Inf 2 2 2]), [NaN NaN 0.3934693402873666 y(4:6)], eps) %!assert_equal (fcdf ([x(1:2) NaN x(4:6)], 2, 2), [y(1:2) NaN y(4:6)], eps) ## Test class of input preserved %!assert_equal (fcdf ([x, NaN], 2, 2), [y, NaN], eps) %!assert_equal (fcdf (single ([x, NaN]), 2, 2), single ([y, NaN]), eps ('single')) %!assert_equal (fcdf ([x, NaN], single (2), 2), single ([y, NaN]), eps ('single')) %!assert_equal (fcdf ([x, NaN], 2, single (2)), single ([y, NaN]), eps ('single')) ## Test input validation %!error fcdf () %!error fcdf (1) %!error fcdf (1, 2) %!error fcdf (1, 2, 3, 4) %!error fcdf (1, 2, 3, 'tail') %!error ... %! fcdf (ones (3), ones (2), ones (2)) %!error ... %! fcdf (ones (2), ones (3), ones (2)) %!error ... %! fcdf (ones (2), ones (2), ones (3)) %!error fcdf (int32 (2), 2, 2) %!error fcdf (true, 2, 2) %!error fcdf ('a', 2, 2) %!error fcdf (i, 2, 2) %!error fcdf (2, i, 2) %!error fcdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/finv.m000066400000000000000000000215251527311104300241320ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} finv (@var{p}, @var{df1}, @var{df2}) ## ## Inverse of the @math{F}-cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the @math{F}-distribution with @var{df1} and @var{df2} degrees of freedom. ## The size of @var{x} is the common size of @var{p}, @var{df1}, and @var{df2}. ## A scalar input functions as a constant matrix of the same size as the other ## inputs. ## ## Further information about the @math{F}-distribution can be found at ## @url{https://en.wikipedia.org/wiki/F-distribution} ## ## Where one degree of freedom is so large against the other that the ## @math{F}-distribution cannot be told from its limit in double precision, ## the quantile is taken from the limit, @code{chi2inv (@var{p}, @var{df1}) / ## @var{df1}} as @var{df2} grows and @code{@var{df2} / chi2inv (1 - @var{p}, ## @var{df2})} as @var{df1} grows. Once one degree of freedom reaches ## @math{10^8} and the other is small, MATLAB returns quantiles whose ## probability is off by about 3%: its @code{finv (0.025, 10, 1e8)} is ## 0.32227, whose probability is 0.0243, where the quantile is 0.32470. ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{fcdf, fpdf, frnd, fstat} ## @end deftypefn function x = finv (p, df1, df2) ## Check for valid number of input arguments if (nargin < 3) error ("finv: function called with too few input arguments."); endif ## Check for common size of P, DF1, and DF2 if (! isscalar (p) || ! isscalar (df1) || ! isscalar (df2)) [retval, p, df1, df2] = common_size (p, df1, df2); if (retval > 0) error ("finv: P, DF1, and DF2 must be of common size or scalars."); endif endif ## Check for P, DF1, and DF2 being double or single if (! (isfloat (p) && isfloat (df1) && isfloat (df2))) error ("finv: P, DF1, and DF2 must be double or single."); endif ## Check for P, DF1, and DF2 being reals if (iscomplex (p) || iscomplex (df1) || iscomplex (df2)) error ("finv: P, DF1, and DF2 must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (df1, 'single') || isa (df2, 'single')) x = NaN (size (p), 'single'); else x = NaN (size (p)); endif ## Handle both DFs being INF kz = df1 == Inf & df2 == Inf; k = p == 1 & df1 > 0 & df2 > 0; x(k) = Inf; ## A DF so large against the other that the Beta form loses more to ## rounding than the limit loses to the finite DF is taken as infinite; the ## two errors cross where the larger DF squared is 3e15 times the smaller big = max (df1, df2) .^ 2 > 3e15 * max (min (df1, df2), 1); inf1 = (df1 == Inf | (big & df1 > df2)) & df2 < Inf; inf2 = (df2 == Inf | (big & df2 > df1)) & df1 < Inf; ## Solve on the tail nearer the answer: the lower one as Beta (DF1/2, DF2/2) ## and the upper one as Beta (DF2/2, DF1/2), so that neither cancels k = (p >= 0) & (p < 1) & (df1 > 0) & (df2 > 0) & ! inf1 & ! inf2 & ! kz; kl = k & (p <= 0.5); z = betainv (p(kl), df1(kl) / 2, df2(kl) / 2); x(kl) = z ./ (1 - z) .* df2(kl) ./ df1(kl); ku = k & (p > 0.5); w = betainv (1 - p(ku), df2(ku) / 2, df1(ku) / 2); x(ku) = (1 - w) ./ w .* df2(ku) ./ df1(ku); ## Limits: DF1 * X is chi-square with DF1 as DF2 grows, and DF2 / X is ## chi-square with DF2 as DF1 grows k = (p >= 0) & (p < 1) & (df1 > 0) & inf2; x(k) = chi2inv (p(k), df1(k)) ./ df1(k); k = (p >= 0) & (p < 1) & (df2 > 0) & inf1; x(k) = df2(k) ./ (2 * gammaincinv (p(k), df2(k) / 2, 'upper')); ## Force instances with df1 = df2 = INF to 0 for p = 0 and to 1 for 0 < p <= 1 x(kz & p > 0 & p <= 1) = 1; x(kz & p == 0) = 0; endfunction %!demo %! ## Plot various iCDFs from the F distribution %! p = 0.001:0.001:0.999; %! x1 = finv (p, 1, 1); %! x2 = finv (p, 2, 1); %! x3 = finv (p, 5, 2); %! x4 = finv (p, 10, 1); %! x5 = finv (p, 100, 100); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', p, x4, '-c', p, x5, '-m') %! grid on %! ylim ([0, 4]) %! legend ({'df1 = 1, df2 = 2', 'df1 = 2, df2 = 1', ... %! 'df1 = 5, df2 = 2', 'df1 = 10, df2 = 1', ... %! 'df1 = 100, df2 = 100'}, 'location', 'northwest') %! title ('F iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p %! p = [-1 0 0.5 1 2]; %!assert_equal (finv (p, 2*ones (1,5), 2*ones (1,5)), [NaN 0 1 Inf NaN]) %!assert_equal (finv (p, 2, 2*ones (1,5)), [NaN 0 1 Inf NaN]) %!assert_equal (finv (p, 2*ones (1,5), 2), [NaN 0 1 Inf NaN]) %!assert_equal (finv (p, [2 -Inf NaN Inf 2], 2), [NaN NaN NaN Inf NaN]) %!assert_equal (finv (p, 2, [2 -Inf NaN Inf 2]), [NaN NaN NaN Inf NaN]) %!assert_equal (finv ([p(1:2) NaN p(4:5)], 2, 2), [NaN 0 NaN Inf NaN]) ## Test for bug #66034 (savannah) %!assert_equal (finv (0.025, 10, 1e6), 0.3247, 1e-4) %!assert_equal (finv (0.025, 10, 1e7), 0.3247, 1e-4) %!assert_equal (finv (0.025, 10, 1e10), 0.3247, 1e-4) %!assert_equal (finv (0.025, 10, 1e255), 0.3247, 1e-4) %!assert_equal (finv (0.025, 10, Inf), 0.3247, 1e-4) ## Test for issue #203 (Github) %!test %! x = finv (0.35, Inf, 4); %! assert_equal (x, 0.9014, 1e-4) %!test %! x = finv (0, Inf, 4); %! assert_equal (x, 0) %!test %! x = finv (1, Inf, 4); %! assert_equal (x, Inf) %!test %! x = finv (0.35, 4, Inf); %! assert_equal (x, 0.6175, 1e-4) %!test %! x = finv (0, 4, Inf); %! assert_equal (x, 0) %!test %! x = finv (1, 4, Inf); %! assert_equal (x, Inf) %!test %! x = finv ([0, 0.000001, 0.35, 1, 1.2], Inf, Inf); %! assert_equal (x, [0, 1, 1, 1, NaN]); ## Values from MATLAB R2024a %!assert_equal (finv (1e-8, 2, 2), 1.00000001e-08, -1e-14) %!assert_equal (finv (1e-12, 2, 2), 1.000000000001e-12, -1e-14) %!assert_equal (finv (1e-10, 1, 1), 2.46740110027235e-20, -1e-14) %!assert_equal (finv (1e-6, 10, 20), 0.0285838655483384, -1e-14) %!assert_equal (finv (0.975, 1e6, 1e6), 1.00392762317843, -1e-10) %!assert_equal (finv (0.975, 2e6, 2e6), 1.00277565345099, -1e-10) %!assert_equal (finv (0.975, 1e7, 1e7), 1.00124035874565, -1e-10) %!assert_equal (finv (0.975, 1e8, 1e8), 1.00039206963839, -1e-10) %!assert_equal (finv (0.975, 1e8, 1e10), 1.00027858274271, -1e-9) %!assert_equal (finv (0.35, Inf, 4), 0.901370019458443, -1e-14) %!assert_equal (finv (0.35, 4, Inf), 0.617521846868827, -1e-14) ## The limits, where MATLAB R2024a gives 0.322269543148685, 8.90372005876577 ## and 2.56334042013798e-05 %!assert_equal (finv (0.025, 10, 1e12), 0.32469727802368437, -1e-10) %!assert_equal (finv (0.975, 1e12, 4), 8.2573219821426864, -1e-10) %!assert_equal (finv (1e-10, 10, 1e12), 0.0052331065631905406, -1e-10) ## Test class of input preserved %!assert_equal (finv ([p, NaN], 2, 2), [NaN 0 1 Inf NaN NaN]) %!assert_equal (finv (single ([p, NaN]), 2, 2), single ([NaN 0 1 Inf NaN NaN])) %!assert_equal (finv ([p, NaN], single (2), 2), single ([NaN 0 1 Inf NaN NaN])) %!assert_equal (finv ([p, NaN], 2, single (2)), single ([NaN 0 1 Inf NaN NaN])) ## Test input validation %!error finv () %!error finv (1) %!error finv (1,2) %!error ... %! finv (ones (3), ones (2), ones (2)) %!error ... %! finv (ones (2), ones (3), ones (2)) %!error ... %! finv (ones (2), ones (2), ones (3)) %!error finv (int32 (2), 2, 2) %!error finv (true, 2, 2) %!error finv ('a', 2, 2) %!error finv (i, 2, 2) %!error finv (2, i, 2) %!error finv (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/fpdf.m000066400000000000000000000174461527311104300241160ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} fpdf (@var{x}, @var{df1}, @var{df2}) ## ## @math{F}-probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the @math{F}-distribution with @var{df1} and @var{df2} degrees of freedom. ## The size of @var{y} is the common size of @var{x}, @var{df1}, and @var{df2}. ## A scalar input functions as a constant matrix of the same size as the other ## inputs. ## ## Further information about the @math{F}-distribution can be found at ## @url{https://en.wikipedia.org/wiki/F-distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{fcdf, finv, frnd, fstat} ## @end deftypefn function y = fpdf (x, df1, df2) ## Check for valid number of input arguments if (nargin < 3) error ("fpdf: function called with too few input arguments."); endif ## Check for common size of X, DF1, and DF2 if (! isscalar (x) ||! isscalar (df1) || ! isscalar (df2)) [retval, x, df1, df2] = common_size (x, df1, df2); if (retval > 0) error ("fpdf: X, DF1, and DF2 must be of common size or scalars."); endif endif ## Check for X, DF1, and DF2 being double or single if (! (isfloat (x) && isfloat (df1) && isfloat (df2))) error ("fpdf: X, DF1, and DF2 must be double or single."); endif ## Check for X, DF1, and DF2 being reals if (iscomplex (x) || iscomplex (df1) || iscomplex (df2)) error ("fpdf: X, DF1, and DF2 must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (df1, 'single') || isa (df2, 'single')) y = zeros (size (x), 'single'); else y = zeros (size (x)); endif ## Handle both DFs being INF kz = df1 == Inf & df2 == Inf; ## Limit DFs to 5e10 to avoid numerical issues df1(df1 > 5e10) = 5e10; df2(df2 > 5e10) = 5e10; k = isnan (x) | ! (df1 > 0) | ! (df2 > 0); y(k) = NaN; #k = (x > 0) & (x < Inf) & (df1 > 0) & (df1 < Inf) & (df2 > 0) & (df2 < Inf); k = x > 0 & x < Inf & df1 > 0 & df2 > 0; if (isscalar (df1) && isscalar (df2)) tmp = df1 / df2 * x(k); y(k) = (exp ((df1/2 - 1) * log (tmp) ... - ((df1 + df2) / 2) * log (1 + tmp)) ... * (df1 / df2) ./ beta (df1/2, df2/2)); else tmp = df1(k) .* x(k) ./ df2(k); y(k) = (exp ((df1(k)/2 - 1) .* log (tmp) ... - ((df1(k) + df2(k)) / 2) .* log (1 + tmp)) ... .* (df1(k) ./ df2(k)) ./ beta (df1(k)/2, df2(k)/2)); endif ## Handle the origin, where the density has three regimes in DF1: it is ## unbounded for DF1 < 2, unity for DF1 = 2 (whatever DF2 is), and zero ## for DF1 > 2, which is the value Y already holds. The logarithmic form ## above cannot serve here, since it evaluates to 0 * -Inf at DF1 = 2. k = x == 0 & df1 > 0 & df2 > 0; y(k & df1 < 2) = Inf; y(k & df1 == 2) = 1; ## Force instances with df1 = df2 = INF to 0 for valid x y(kz & ! isnan (x)) = 0; endfunction %!demo %! ## Plot various PDFs from the F distribution %! x = 0.01:0.01:4; %! y1 = fpdf (x, 1, 1); %! y2 = fpdf (x, 2, 1); %! y3 = fpdf (x, 5, 2); %! y4 = fpdf (x, 10, 1); %! y5 = fpdf (x, 100, 100); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', x, y4, '-c', x, y5, '-m') %! grid on %! ylim ([0, 2.5]) %! legend ({'df1 = 1, df2 = 2', 'df1 = 2, df2 = 1', ... %! 'df1 = 5, df2 = 2', 'df1 = 10, df2 = 1', ... %! 'df1 = 100, df2 = 100'}, 'location', 'northeast') %! title ('F PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x, y %! x = [-1, 0, 0.5, 1, 2]; %! y = [0, 1, 4/9, 1/4, 1/9]; %!assert_equal (fpdf (x, 2*ones (1,5), 2*ones (1,5)), y, eps) %!assert_equal (fpdf (x, 2, 2*ones (1,5)), y, eps) %!assert_equal (fpdf (x, 2*ones (1,5), 2), y, eps) %!assert_equal (fpdf (x, [0, NaN, Inf, 2, 2], 2), [NaN, NaN, 0.5413, y(4:5)], 1e-4) %!assert_equal (fpdf (x, 2, [0, NaN, Inf, 2, 2]), [NaN, NaN, 0.6065, y(4:5)], 1e-4) %!assert_equal (fpdf ([x, NaN], 2, 2), [y, NaN], eps) ## Test the density at the origin, unbounded for DF1 < 2 and unity at DF1 = 2 %!assert_equal (fpdf (0, 0.5, 2), Inf) %!assert_equal (fpdf (0, 1, 1), Inf) %!assert_equal (fpdf (0, 1, 7), Inf) %!assert_equal (fpdf (0, 1.9, 3), Inf) %!assert_equal (fpdf (0, 2, 1), 1) %!assert_equal (fpdf (0, 2, 2), 1) %!assert_equal (fpdf (0, 2, 5), 1) %!assert_equal (fpdf (0, 2, 100), 1) %!assert_equal (fpdf (0, 2, Inf), 1) %!assert_equal (fpdf (0, 2.1, 3), 0) %!assert_equal (fpdf (0, 3, 3), 0) %!assert_equal (fpdf (0, 10, 10), 0) %!assert_equal (fpdf (0, Inf, 4), 0) %!assert_equal (fpdf (0, Inf, Inf), 0) %!assert_equal (fpdf (0, [0.5, 1, 2, 3, Inf], 2), [Inf, Inf, 1, 0, 0]) %!assert_equal (fpdf (0, 0, 2), NaN) %!assert_equal (fpdf (0, 2, 0), NaN) %!assert_equal (fpdf (0, NaN, 2), NaN) %!assert_equal (fpdf (0, 2, NaN), NaN) %!assert_equal (fpdf (single (0), 2, 2), single (1)) %!test #F (x, 1, df1) == T distribution (sqrt (x), df1) / sqrt (x) %! rand ('seed', 1234); # for reproducibility %! xr = rand (10,1); %! xr = xr(x > 0.1 & x < 0.9); %! yr = tpdf (sqrt (xr), 2) ./ sqrt (xr); %! assert_equal (fpdf (xr, 1, 2), yr, 5*eps); ## Test for issue #203 (Github) %!test %! yy = fpdf (2, 4, Inf); %! assert_equal (yy, 0.1465, 1e-4) %!test %! yy = fpdf (2, 4, 1000000000000000); %! assert_equal (yy, 0.1465, 1e-4) %!test %! yy = fpdf (2, Inf, 4); %! assert_equal (yy, 0.1839, 1e-4) %!test %! yy = fpdf (2, 10000000000000000, 4); %! assert_equal (yy, 0.1839, 1e-4) %!test %! yy = fpdf (2, Inf, Inf); %! assert_equal (yy, 0) %!test %! yy = fpdf (NaN, Inf, Inf); %! assert_equal (yy, NaN) ## Test class of input preserved %!assert_equal (fpdf (single ([x, NaN]), 2, 2), single ([y, NaN]), eps ('single')) %!assert_equal (fpdf ([x, NaN], single (2), 2), single ([y, NaN]), eps ('single')) %!assert_equal (fpdf ([x, NaN], 2, single (2)), single ([y, NaN]), eps ('single')) ## Test input validation %!error fpdf () %!error fpdf (1) %!error fpdf (1,2) %!error ... %! fpdf (ones (3), ones (2), ones (2)) %!error ... %! fpdf (ones (2), ones (3), ones (2)) %!error ... %! fpdf (ones (2), ones (2), ones (3)) %!error fpdf (int32 (2), 2, 2) %!error fpdf (true, 2, 2) %!error fpdf ('a', 2, 2) %!error fpdf (i, 2, 2) %!error fpdf (2, i, 2) %!error fpdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/frnd.m000066400000000000000000000151051527311104300241160ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} frnd (@var{df1}, @var{df2}) ## @deftypefnx {statistics} {@var{r} =} frnd (@var{df1}, @var{df2}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} frnd (@var{df1}, @var{df2}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} frnd (@var{df1}, @var{df2}, [@var{sz}]) ## ## Random arrays from the @math{F}-distribution. ## ## @code{@var{r} = frnd (@var{df1}, @var{df2})} returns an array of random ## numbers chosen from the @math{F}-distribution with @var{df1} and @var{df2} ## degrees of freedom. The size of @var{r} is the common size of @var{df1} and ## @var{df2}. A scalar input functions as a constant matrix of the same size as ## the other inputs. ## ## When called with a single size argument, @code{frnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the @math{F}-distribution can be found at ## @url{https://en.wikipedia.org/wiki/F-distribution} ## ## @seealso{fcdf, finv, fpdf, fstat} ## @end deftypefn function r = frnd (df1, df2, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("frnd: function called with too few input arguments."); endif ## Check for common size of DF1 and DF2 if (! isscalar (df1) || ! isscalar (df2)) [retval, df1, df2] = common_size (df1, df2); if (retval > 0) error ("frnd: DF1 and DF2 must be of common size or scalars."); endif endif ## Check for DF1 and DF2 being reals if (iscomplex (df1) || iscomplex (df2)) error ("frnd: DF1 and DF2 must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (df1); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("frnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("frnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (df1) && ! isequal (size (df1), size (ones (sz)))) error ("frnd: DF1 and DF2 must be scalars or of size SZ."); endif ## Check for class type if (isa (df1, 'single') || isa (df2, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from F distribution if (isscalar (df1) && isscalar (df2)) if ((df1 > 0) && (df1 < Inf) && (df2 > 0) && (df2 < Inf)) r = df2/df1 * randg (df1/2, sz, cls) ./ randg (df2/2, sz, cls); else r = NaN (sz, cls); endif else r = NaN (sz, cls); k = (df1 > 0) & (df1 < Inf) & (df2 > 0) & (df2 < Inf); r(k) = df2(k) ./ df1(k) .* randg (df1(k)/2, cls) ./ randg (df2(k)/2, cls); endif endfunction ## Test output %!assert_equal (size (frnd (1, 1)), [1 1]) %!assert_equal (size (frnd (1, ones (2,1))), [2, 1]) %!assert_equal (size (frnd (1, ones (2,2))), [2, 2]) %!assert_equal (size (frnd (ones (2,1), 1)), [2, 1]) %!assert_equal (size (frnd (ones (2,2), 1)), [2, 2]) %!assert_equal (size (frnd (1, 1, 3)), [3, 3]) %!assert_equal (size (frnd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (frnd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (frnd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (frnd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (frnd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (frnd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (frnd (1, 1, [])), [0, 0]) %!assert_equal (size (frnd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (frnd (1, 2, -1)), [0, 0]) %!assert_equal (size (frnd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (frnd (1, 2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (frnd (1, 1)), "double") %!assert_equal (class (frnd (1, single (1))), "single") %!assert_equal (class (frnd (1, single ([1, 1]))), "single") %!assert_equal (class (frnd (single (1), 1)), "single") %!assert_equal (class (frnd (single ([1, 1]), 1)), "single") ## Test input validation %!error frnd () %!error frnd (1) %!error ... %! frnd (ones (3), ones (2)) %!error ... %! frnd (ones (2), ones (3)) %!error frnd (i, 2, 3) %!error frnd (1, i, 3) %!error ... %! frnd (1, 2, 1.2) %!error ... %! frnd (1, 2, ones (2)) %!error ... %! frnd (1, 2, [2 0 2.5]) %!error ... %! frnd (1, 2, 2, 1.5, 5) %!error ... %! frnd (2, ones (2), 3) %!error ... %! frnd (2, ones (2), [3, 2]) %!error ... %! frnd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/gamcdf.m000066400000000000000000000322431527311104300244100ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} gamcdf (@var{x}, @var{a}) ## @deftypefnx {statistics} {@var{p} =} gamcdf (@var{x}, @var{a}, @var{b}) ## @deftypefnx {statistics} {@var{p} =} gamcdf (@dots{}, @qcode{'upper'}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} gamcdf (@var{x}, @var{a}, @var{b}, @var{pcov}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} gamcdf (@var{x}, @var{a}, @var{b}, @var{pcov}, @var{alpha}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} gamcdf (@dots{}, @qcode{'upper'}) ## ## Gamma cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the Gamma distribution with shape parameter @var{a} and scale ## parameter @var{b}. When called with only one parameter, then @var{b} ## defaults to 1. The size of @var{p} is the common size of @var{x}, @var{a}, ## and @var{b}. A scalar input functions as a constant matrix of the same ## size as the other inputs. ## ## When called with three output arguments, i.e. @qcode{[@var{p}, @var{plo}, ## @var{pup}]}, @code{gamcdf} computes the confidence bounds for @var{p} when ## the input parameters @var{a} and @var{b} are estimates. In such case, ## @var{pcov}, a @math{2*2} matrix containing the covariance matrix of the ## estimated parameters, is necessary. Optionally, @var{alpha}, which has a ## default value of 0.05, specifies the @qcode{100 * (1 - @var{alpha})} percent ## confidence bounds. @var{plo} and @var{pup} are arrays of the same size as ## @var{p} containing the lower and upper confidence bounds. ## ## @code{[@dots{}] = gamcdf (@dots{}, "upper")} computes the upper tail ## probability of the Gamma distribution with parameters @var{a} and ## @var{b}, at the values in @var{x}. ## ## OCTAVE/MATLAB use the alternative parameterization given by the pair ## @math{α, β}, i.e. shape @var{a} and scale @var{b}. In Wikipedia, the two ## common parameterizations use the pairs @math{k, θ}, as shape and scale, and ## @math{α, β}, as shape and rate, respectively. The parameter names @var{a} ## and @var{b} used here (for MATLAB compatibility) correspond to the parameter ## notation @math{k, θ} instead of the @math{α, β} as reported in Wikipedia. ## ## Further information about the Gamma distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gamma_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{gaminv, gampdf, gamrnd, gamfit, gamlike, gamstat} ## @end deftypefn function [varargout] = gamcdf (x, varargin) ## Check for valid number of input arguments if (nargin < 2 || nargin > 6) error ("gamcdf: invalid number of input arguments."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (varargin{end}, 'upper')) uflag = true; varargin(end) = []; elseif (nargin > 2 && ischar (varargin{end}) && ... ! strcmpi (varargin{end}, 'upper')) error ("gamcdf: invalid argument for upper tail."); elseif (nargin > 2 && isempty (varargin{end})) uflag = false; varargin(end) = []; else uflag = false; endif ## Get extra arguments (if they exist) or add defaults a = varargin{1}; if (numel (varargin) > 1) b = varargin{2}; else b = 1; endif if (numel (varargin) > 2) pcov = varargin{3}; ## Check for valid covariance matrix 2x2 if (! isequal (size (pcov), [2, 2])) error ("gamcdf: invalid size of covariance matrix."); endif else ## Check that cov matrix is provided if 3 output arguments are requested if (nargout > 1) error ("gamcdf: covariance matrix is required for confidence bounds."); endif pcov = []; endif if (numel (varargin) > 3) alpha = varargin{4}; ## Check for valid alpha value if (! isnumeric (alpha) || numel (alpha) !=1 || alpha <= 0 || alpha >= 1) error ("gamcdf: invalid value for alpha."); endif else alpha = 0.05; endif ## Check for common size of X, A, and B if (! isscalar (x) || ! isscalar (a) || ! isscalar (b)) [err, x, a, b] = common_size (x, a, b); if (err > 0) error ("gamcdf: X, A, and B must be of common size or scalars."); endif endif ## Check for X, A, and B being double or single if (! (isfloat (x) && isfloat (a) && isfloat (b))) error ("gamcdf: X, A, and B must be double or single."); endif ## Check for X, A, and B being reals if (iscomplex (x) || iscomplex (a) || iscomplex (b)) error ("gamcdf: X, A, and B must not be complex."); endif ## Prepare parameters so that gammainc returns NaN for out of range parameters a(a < 0) = NaN; b(b < 0) = NaN; ## Prepare data so that gammainc returns 0 for negative X x(x < 0) = 0; ## Compute gammainc z = x ./ b; if (uflag) p = gammainc (z, a, 'upper'); ## Fix NaNs to gammainc output when a == NaN p(isnan (a)) = NaN; else p = gammainc (z, a); ## Fix NaNs to gammainc output when a == NaN p(isnan (a)) = NaN; endif ## Check for appropriate class if (isa (x, 'single') || isa (a, 'single') || isa (b, 'single')); is_class = 'single'; else is_class = 'double'; endif ## Prepare output varargout{1} = cast (p, is_class); if (nargout > 1) plo = NaN (size (z), is_class); pup = NaN (size (z), is_class); endif ## Compute confidence bounds (if requested) if (nargout >= 2) ## Approximate the variance of p on the logit scale logitp = log (p ./ (1 - p)); dp = 1 ./ (p .* (1 - p)); dk = dgammainc (z, a) .* dp; dt = -exp (a .* log (z) - z - gammaln (a) - log (b)) .* dp; varLogitp = pcov(1,1) .* dk .^ 2 + 2 .* pcov(1,2) .* dk .* dt + ... pcov(2,2) .* dt .^ 2; if (any (varLogitp(:) < 0)) error ("gamcdf: bad covariance matrix."); endif ## Use a normal approximation on the logit scale, then transform back to ## the original CDF scale halfwidth = -norminv (alpha / 2) * sqrt (varLogitp); explogitplo = exp (logitp - halfwidth); explogitpup = exp (logitp + halfwidth); plo = explogitplo ./ (1 + explogitplo); pup = explogitpup ./ (1 + explogitpup); varargout{2} = plo; varargout{3} = pup; endif endfunction ## Compute 1st derivative of the incomplete Gamma function function dy = dgammainc (x, a) ## Initialize return variables dy = nan (size (x)); ## Use approximation for A > 2^20 ulim = 2^20; is_lim = find (a > ulim); if (! isempty (is_lim)) x(is_lim) = max (ulim - 1/3 + sqrt (ulim ./ a(is_lim)) .* ... (x(is_lim) - (a(is_lim) - 1/3)), 0); a(is_lim) = ulim; endif ## For x < a+1 is_lo = find (x < a + 1 & x != 0); if (! isempty (is_lo)) x_lo = x(is_lo); k_lo = a(is_lo); k_1 = k_lo; step = 1; d1st = 0; stsum = step; d1sum = d1st; while norm (step, 'inf') >= 100 * eps (norm (stsum, 'inf')) k_1 += 1; step = step .* x_lo ./ k_1; d1st = (d1st .* x_lo - step) ./ k_1; stsum = stsum + step; d1sum = d1sum + d1st; endwhile fklo = exp (-x_lo + k_lo .* log (x_lo) - gammaln (k_lo + 1)); ## Compute 1st derivative dlogfklo = (log (x_lo) - psi (k_lo + 1)); d1fklo = fklo .* dlogfklo; d1y_lo = d1fklo .* stsum + fklo .* d1sum; dy(is_lo) = d1y_lo; endif ## For x >= a+1 is_hi = find (x >= a+1); if (! isempty (is_hi)) x_hi = x(is_hi); k_hi = a(is_hi); zc = 0; k0 = 0; k1 = k_hi; x0 = 1; x1 = x_hi; d1k0 = 0; d1k1 = 1; d1x0 = 0; d1x1 = 0; kx = k_hi ./ x_hi; d1kx = 1 ./ x_hi; d2kx = 0; start = 1; while norm (d2kx - start, 'Inf') > 100 * eps (norm (d2kx, 'Inf')) rescale = 1 ./ x1; zc += 1; n_k = zc - k_hi; d1k0 = (d1k1 + d1k0 .* n_k - k0) .* rescale; d1x0 = (d1x1 + d1x0 .* n_k - x0) .* rescale; k0 = (k1 + k0 .* n_k) .* rescale; x0 = 1 + (x0 .* n_k) .* rescale; nrescale = zc .* rescale; d1k1 = d1k0 .* x_hi + d1k1 .* nrescale; d1x1 = d1x0 .* x_hi + d1x1 .* nrescale; k1 = k0 .* x_hi + k1 .* nrescale; x1 = x0 .* x_hi + zc; start = d2kx; kx = k1 ./ x1; d1kx = (d1k1 - kx .* d1x1) ./ x1; endwhile fkhi = exp (-x_hi + k_hi .* log (x_hi) - gammaln (k_hi+1)); ## Compute 1st derivative dlogfkhi = (log (x_hi) - psi (k_hi + 1)); d1fkhi = fkhi .* dlogfkhi; d1y_hi = d1fkhi .* kx + fkhi .* d1kx; dy(is_hi) = -d1y_hi; endif ## Handle x == 0 is_x0 = find (x == 0); if (! isempty (is_x0)) dy(is_x0) = 0; endif ## Handle a == 0 is_k0 = find (a == 0); if (! isempty (is_k0)) is_k0x0 = find (a == 0 & x == 0); dy(is_k0x0) = -Inf; endif endfunction %!demo %! ## Plot various CDFs from the Gamma distribution %! x = 0:0.01:20; %! p1 = gamcdf (x, 1, 2); %! p2 = gamcdf (x, 2, 2); %! p3 = gamcdf (x, 3, 2); %! p4 = gamcdf (x, 5, 1); %! p5 = gamcdf (x, 9, 0.5); %! p6 = gamcdf (x, 7.5, 1); %! p7 = gamcdf (x, 0.5, 1); %! plot (x, p1, '-r', x, p2, '-g', x, p3, '-y', x, p4, '-m', ... %! x, p5, '-k', x, p6, '-b', x, p7, '-c') %! grid on %! legend ({'α = 1, β = 2', 'α = 2, β = 2', 'α = 3, β = 2', ... %! 'α = 5, β = 1', 'α = 9, β = 0.5', 'α = 7.5, β = 1', ... %! 'α = 0.5, β = 1'}, 'location', 'southeast') %! title ('Gamma CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, y, u %! x = [-1, 0, 0.5, 1, 2, Inf]; %! y = [0, gammainc(x(2:end), 1)]; %! u = [0, NaN, NaN, 1, 0.1353352832366127, 0]; %!assert_equal (gamcdf (x, ones (1,6), ones (1,6)), y, eps) %!assert_equal (gamcdf (x, ones (1,6), ones (1,6), []), y, eps) %!assert_equal (gamcdf (x, 1, ones (1,6)), y, eps) %!assert_equal (gamcdf (x, ones (1,6), 1), y, eps) %!assert_equal (gamcdf (x, [0, -Inf, NaN, Inf, 1, 1], 1), [1, NaN, NaN, 0, y(5:6)], eps) %!assert_equal (gamcdf (x, [0, -Inf, NaN, Inf, 1, 1], 1, 'upper'), u, eps) %!assert_equal (gamcdf (x, 1, [0, -Inf, NaN, Inf, 1, 1]), [NaN, NaN, NaN, 0, y(5:6)], eps) %!assert_equal (gamcdf ([x(1:2), NaN, x(4:6)], 1, 1), [y(1:2), NaN, y(4:6)], eps) ## Test class of input preserved %!assert_equal (gamcdf ([x, NaN], 1, 1), [y, NaN]) %!assert_equal (gamcdf (single ([x, NaN]), 1, 1), single ([y, NaN]), eps ('single')) %!assert_equal (gamcdf ([x, NaN], single (1), 1), single ([y, NaN]), eps ('single')) %!assert_equal (gamcdf ([x, NaN], 1, single (1)), single ([y, NaN]), eps ('single')) ## Test input validation %!error gamcdf () %!error gamcdf (1) %!error gamcdf (1, 2, 3, 4, 5, 6, 7) %!error gamcdf (1, 2, 3, 'uper') %!error gamcdf (1, 2, 3, 4, 5, 'uper') %!error gamcdf (2, 3, 4, [1, 2]) %!error ... %! [p, plo, pup] = gamcdf (1, 2, 3) %!error ... %! [p, plo, pup] = gamcdf (1, 2, 3, 'upper') %!error [p, plo, pup] = ... %! gamcdf (1, 2, 3, [1, 0; 0, 1], 0) %!error [p, plo, pup] = ... %! gamcdf (1, 2, 3, [1, 0; 0, 1], 1.22) %!error [p, plo, pup] = ... %! gamcdf (1, 2, 3, [1, 0; 0, 1], 'alpha', 'upper') %!error ... %! gamcdf (ones (3), ones (2), ones (2)) %!error ... %! gamcdf (ones (2), ones (3), ones (2)) %!error ... %! gamcdf (ones (2), ones (2), ones (3)) %!error gamcdf (int32 (2), 2, 2) %!error gamcdf (true, 2, 2) %!error gamcdf ('a', 2, 2) %!error gamcdf (i, 2, 2) %!error gamcdf (2, i, 2) %!error gamcdf (2, 2, i) %!error ... %! [p, plo, pup] = gamcdf (1, 2, 3, [1, 0; 0, -inf], 0.04) statistics-release-2.1.2/inst/Distribution_Functions/gaminv.m000066400000000000000000000162471527311104300244560ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} gaminv (@var{p}, @var{a}, @var{b}) ## ## Inverse of the Gamma cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the Gamma distribution with shape parameter @var{a} and scale parameter ## @var{b}. The size of @var{x} is the common size of @var{p}, @var{a}, ## and @var{b}. A scalar input functions as a constant matrix of the same ## size as the other inputs. ## ## OCTAVE/MATLAB use the alternative parameterization given by the pair ## @math{α, β}, i.e. shape @var{a} and scale @var{b}. In Wikipedia, the two ## common parameterizations use the pairs @math{k, θ}, as shape and scale, and ## @math{α, β}, as shape and rate, respectively. The parameter names @var{a} ## and @var{b} used here (for MATLAB compatibility) correspond to the parameter ## notation @math{k, θ} instead of the @math{α, β} as reported in Wikipedia. ## ## Further information about the Gamma distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gamma_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{gamcdf, gampdf, gamrnd, gamfit, gamlike, gamstat} ## @end deftypefn function x = gaminv (p, a, b) ## Check for valid number of input arguments if (nargin < 3) error ("gaminv: function called with too few input arguments."); endif ## Check for common size of P, A, and B if (! isscalar (p) || ! isscalar (a) || ! isscalar (b)) [retval, p, a, b] = common_size (p, a, b); if (retval > 0) error ("gaminv: P, A, and B must be of common size or scalars."); endif endif ## Check for P, A, and B being double or single if (! (isfloat (p) && isfloat (a) && isfloat (b))) error ("gaminv: P, A, and B must be double or single."); endif ## Check for P, A, and B being reals if (iscomplex (p) || iscomplex (a) || iscomplex (b)) error ("gaminv: P, A, and B must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (a, 'single') || isa (b, 'single')) x = zeros (size (p), 'single'); else x = zeros (size (p)); endif ## Force NaNs for out of range parameters is_nan = ((p < 0) | (p > 1) | isnan (p) ... | ! (a > 0) | ! (a < Inf) | ! (b > 0) | ! (b < Inf)); x(is_nan) = NaN; ## Handle edge cases is_inf = (p == 1) & (a > 0) & (a < Inf) & (b > 0) & (b < Inf); x(is_inf) = Inf; ## Handle all other valid cases is_valid = find ((p > 0) & (p < 1) & (a > 0) & ... (a < Inf) & (b > 0) & (b < Inf)); if (! isempty (is_valid)) if (! isscalar (a) || ! isscalar (b)) a = a(is_valid); b = b(is_valid); y = a .* b; else y = a * b * ones (size (is_valid)); endif p = p(is_valid); ## Call GAMMAINCINV to find a root of GAMMAINC q = gammaincinv (p, a); tol = sqrt (eps (ones (1, 1, class (q)))); check_cdf = ((abs (gammainc (q, a) - p) ./ p) > tol); ## Check for any cdf being far off from tolerance if (any (check_cdf(:))) warning ("gaminv: calculation failed to converge for some values."); endif x(is_valid) = q .* b; endif endfunction %!demo %! ## Plot various iCDFs from the Gamma distribution %! p = 0.001:0.001:0.999; %! x1 = gaminv (p, 1, 2); %! x2 = gaminv (p, 2, 2); %! x3 = gaminv (p, 3, 2); %! x4 = gaminv (p, 5, 1); %! x5 = gaminv (p, 9, 0.5); %! x6 = gaminv (p, 7.5, 1); %! x7 = gaminv (p, 0.5, 1); %! plot (p, x1, '-r', p, x2, '-g', p, x3, '-y', p, x4, '-m', ... %! p, x5, '-k', p, x6, '-b', p, x7, '-c') %! ylim ([0, 20]) %! grid on %! legend ({'α = 1, β = 2', 'α = 2, β = 2', 'α = 3, β = 2', ... %! 'α = 5, β = 1', 'α = 9, β = 0.5', 'α = 7.5, β = 1', ... %! 'α = 0.5, β = 1'}, 'location', 'northwest') %! title ('Gamma iCDF') %! xlabel ('probability') %! ylabel ('x') ## Test output %!shared p %! p = [-1 0 0.63212055882855778 1 2]; %!assert_equal (gaminv (p, ones (1,5), ones (1,5)), [NaN 0 1 Inf NaN], eps) %!assert_equal (gaminv (p, 1, ones (1,5)), [NaN 0 1 Inf NaN], eps) %!assert_equal (gaminv (p, ones (1,5), 1), [NaN 0 1 Inf NaN], eps) %!assert_equal (gaminv (p, [1 -Inf NaN Inf 1], 1), [NaN NaN NaN NaN NaN]) %!assert_equal (gaminv (p, 1, [1 -Inf NaN Inf 1]), [NaN NaN NaN NaN NaN]) %!assert_equal (gaminv ([p(1:2) NaN p(4:5)], 1, 1), [NaN 0 NaN Inf NaN]) %!assert_equal (gaminv ([p(1:2) NaN p(4:5)], 1, 1), [NaN 0 NaN Inf NaN]) ## Test for accuracy when p is small. Results compared to Matlab %!assert_equal (gaminv (1e-16, 1, 1), 1e-16, eps) %!assert_equal (gaminv (1e-16, 1, 2), 2e-16, eps) %!assert_equal (gaminv (1e-20, 3, 5), 1.957434012161815e-06, eps) %!assert_equal (gaminv (1e-15, 1, 1), 1e-15, eps) %!assert_equal (gaminv (1e-35, 1, 1), 1e-35, eps) ## Test class of input preserved %!assert_equal (gaminv ([p, NaN], 1, 1), [NaN 0 1 Inf NaN NaN], eps) %!assert_equal (gaminv (single ([p, NaN]), 1, 1), single ([NaN 0 1 Inf NaN NaN]), ... %! eps ('single')) %!assert_equal (gaminv ([p, NaN], single (1), 1), single ([NaN 0 1 Inf NaN NaN]), ... %! eps ('single')) %!assert_equal (gaminv ([p, NaN], 1, single (1)), single ([NaN 0 1 Inf NaN NaN]), ... %! eps ('single')) ## Test input validation %!error gaminv () %!error gaminv (1) %!error gaminv (1,2) %!error ... %! gaminv (ones (3), ones (2), ones (2)) %!error ... %! gaminv (ones (2), ones (3), ones (2)) %!error ... %! gaminv (ones (2), ones (2), ones (3)) %!error gaminv (int32 (2), 2, 2) %!error gaminv (true, 2, 2) %!error gaminv ('a', 2, 2) %!error gaminv (i, 2, 2) %!error gaminv (2, i, 2) %!error gaminv (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/gampdf.m000066400000000000000000000152131527311104300244230ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} gampdf (@var{x}, @var{a}, @var{b}) ## ## Gamma probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the Gamma distribution with shape parameter @var{a} and scale parameter ## @var{b}. The size of @var{y} is the common size of @var{x}, @var{a} and ## @var{b}. A scalar input functions as a constant matrix of the same size ## as the other inputs. ## ## OCTAVE/MATLAB use the alternative parameterization given by the pair ## @math{α, β}, i.e. shape @var{a} and scale @var{b}. In Wikipedia, the two ## common parameterizations use the pairs @math{k, θ}, as shape and scale, and ## @math{α, β}, as shape and rate, respectively. The parameter names @var{a} ## and @var{b} used here (for MATLAB compatibility) correspond to the parameter ## notation @math{k, θ} instead of the @math{α, β} as reported in Wikipedia. ## ## Further information about the Gamma distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gamma_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{gamcdf, gaminv, gamrnd, gamfit, gamlike, gamstat} ## @end deftypefn function y = gampdf (x, a, b) ## Check for valid number of input arguments if (nargin < 3) error ("gampdf: function called with too few input arguments."); endif ## Check for common size of X, A, and B if (! isscalar (a) || ! isscalar (b)) [retval, x, a, b] = common_size (x, a, b); if (retval > 0) error ("gampdf: X, A, and B must be of common size or scalars."); endif endif ## Check for X, A, and B being double or single if (! (isfloat (x) && isfloat (a) && isfloat (b))) error ("gampdf: X, A, and B must be double or single."); endif ## Check for X, A, and B being reals if (iscomplex (x) || iscomplex (a) || iscomplex (b)) error ("gampdf: X, A, and B must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (a, 'single') || isa (b, 'single')) y = zeros (size (x), 'single'); else y = zeros (size (x)); endif ## Force NaNs for out of range parameters is_nan = ! (a > 0) | ! (b > 0) | isnan (x); y(is_nan) = NaN; ## Handle all other valid cases v = x >= 0 & a > 0 & a <= 1 & b > 0 & b < Inf; if (isscalar (a) && isscalar (b)) y(v) = (x(v) .^ (a - 1)) ... .* exp (- x(v) / b) / gamma (a) / (b ^ a); else y(v) = (x(v) .^ (a(v) - 1)) ... .* exp (- x(v) ./ b(v)) ./ gamma (a(v)) ./ (b(v) .^ a(v)); endif v = x >= 0 & a > 1 & a < Inf & b > 0 & b < Inf; if (isscalar (a) && isscalar (b)) y(v) = exp (- a * log (b) + (a-1) * log (x(v)) - x(v) / b - gammaln (a)); else y(v) = exp (- a(v) .* log (b(v)) + (a(v)-1) .* log (x(v)) - x(v) ./ b(v) - gammaln (a(v))); endif ## The density at an infinite abscissa is zero: no proper distribution ## places mass there. The expressions above reach it as Inf - Inf. y(isinf (x) & a > 0 & a < Inf & b > 0 & b < Inf) = 0; endfunction %!demo %! ## Plot various PDFs from the Gamma distribution %! x = 0:0.01:20; %! y1 = gampdf (x, 1, 2); %! y2 = gampdf (x, 2, 2); %! y3 = gampdf (x, 3, 2); %! y4 = gampdf (x, 5, 1); %! y5 = gampdf (x, 9, 0.5); %! y6 = gampdf (x, 7.5, 1); %! y7 = gampdf (x, 0.5, 1); %! plot (x, y1, '-r', x, y2, '-g', x, y3, '-y', x, y4, '-m', ... %! x, y5, '-k', x, y6, '-b', x, y7, '-c') %! grid on %! ylim ([0,0.5]) %! legend ({'α = 1, β = 2', 'α = 2, β = 2', 'α = 3, β = 2', ... %! 'α = 5, β = 1', 'α = 9, β = 0.5', 'α = 7.5, β = 1', ... %! 'α = 0.5, β = 1'}, 'location', 'northeast') %! title ('Gamma PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x, y %! x = [-1 0 0.5 1 Inf]; %! y = [0 exp(-x(2:end))]; %!assert_equal (gampdf (x, ones (1,5), ones (1,5)), y) %!assert_equal (gampdf (x, 1, ones (1,5)), y) %!assert_equal (gampdf (x, ones (1,5), 1), y) %!assert_equal (gampdf (x, [0 -Inf NaN Inf 1], 1), [NaN NaN NaN 0 y(5)]) %!assert_equal (gampdf (x, [0 Inf NaN Inf 1], 1), [NaN 0 NaN 0 y(5)]) %!assert_equal (gampdf (x, 1, [0 -Inf NaN Inf 1]), [NaN NaN NaN 0 y(5)]) %!assert_equal (gampdf ([x, NaN], 1, 1), [y, NaN]) ## Test for issue #203 (Github) %!assert_equal (gampdf (2, Inf, 4), 0) %!assert_equal (gampdf (2, 4, Inf), 0) %!assert_equal (gampdf (2, Inf, Inf), 0) ## Test class of input preserved %!assert_equal (gampdf (single ([x, NaN]), 1, 1), single ([y, NaN])) %!assert_equal (gampdf ([x, NaN], single (1), 1), single ([y, NaN])) %!assert_equal (gampdf ([x, NaN], 1, single (1)), single ([y, NaN])) ## Test input validation %!error gampdf () %!error gampdf (1) %!error gampdf (1,2) %!error ... %! gampdf (ones (3), ones (2), ones (2)) %!error ... %! gampdf (ones (2), ones (3), ones (2)) %!error ... %! gampdf (ones (2), ones (2), ones (3)) %!error gampdf (int32 (2), 2, 2) %!error gampdf (true, 2, 2) %!error gampdf ('a', 2, 2) %!error gampdf (i, 2, 2) %!error gampdf (2, i, 2) %!error gampdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/gamrnd.m000066400000000000000000000160101527311104300244310ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} gamrnd (@var{a}, @var{b}) ## @deftypefnx {statistics} {@var{r} =} gamrnd (@var{a}, @var{b}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} gamrnd (@var{a}, @var{b}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} gamrnd (@var{a}, @var{b}, [@var{sz}]) ## ## Random arrays from the Gamma distribution. ## ## @code{@var{r} = gamrnd (@var{a}, @var{b})} returns an array of random ## numbers chosen from the Gamma distribution with shape parameter @var{a} and ## scale parameter @var{b}. The size of @var{r} is the common size of ## @var{a} and @var{b}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## When called with a single size argument, @code{gamrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## OCTAVE/MATLAB use the alternative parameterization given by the pair ## @math{α, β}, i.e. shape @var{a} and scale @var{b}. In Wikipedia, the two ## common parameterizations use the pairs @math{k, θ}, as shape and scale, and ## @math{α, β}, as shape and rate, respectively. The parameter names @var{a} ## and @var{b} used here (for MATLAB compatibility) correspond to the parameter ## notation @math{k, θ} instead of the @math{α, β} as reported in Wikipedia. ## ## Further information about the Gamma distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gamma_distribution} ## ## @seealso{gamcdf, gaminv, gampdf, gamfit, gamlike, gamstat} ## @end deftypefn function r = gamrnd (a, b, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("gamrnd: function called with too few input arguments."); endif ## Check for common size of A and B if (! isscalar (a) || ! isscalar (b)) [retval, a, b] = common_size (a, b); if (retval > 0) error ("gamrnd: A and B must be of common size or scalars."); endif endif ## Check for A and B being reals if (iscomplex (a) || iscomplex (b)) error ("gamrnd: A and B must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (a); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("gamrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("gamrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (a) && ! isequal (size (a), size (ones (sz)))) error ("gamrnd: A and B must be scalars or of size SZ."); endif ## Check for class type if (isa (a, 'single') || isa (b, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from Gamma distribution if (isscalar (a) && isscalar (b)) if ((a > 0) && (a < Inf) && (b > 0) && (b < Inf)) r = b * randg (a, sz, cls); else r = NaN (sz, cls); endif else r = NaN (sz, cls); valid = (a > 0) & (a < Inf) & (b > 0) & (b < Inf); r(valid) = b(valid) .* randg (a(valid), cls); endif endfunction ## Test output %!assert_equal (size (gamrnd (1, 1)), [1 1]) %!assert_equal (size (gamrnd (1, ones (2,1))), [2, 1]) %!assert_equal (size (gamrnd (1, ones (2,2))), [2, 2]) %!assert_equal (size (gamrnd (ones (2,1), 1)), [2, 1]) %!assert_equal (size (gamrnd (ones (2,2), 1)), [2, 2]) %!assert_equal (size (gamrnd (1, 1, 3)), [3, 3]) %!assert_equal (size (gamrnd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (gamrnd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (gamrnd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (gamrnd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (gamrnd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (gamrnd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (gamrnd (1, 1, [])), [0, 0]) %!assert_equal (size (gamrnd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (gamrnd (1, 2, -1)), [0, 0]) %!assert_equal (size (gamrnd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (gamrnd (1, 2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (gamrnd (1, 1)), "double") %!assert_equal (class (gamrnd (1, single (1))), "single") %!assert_equal (class (gamrnd (1, single ([1, 1]))), "single") %!assert_equal (class (gamrnd (single (1), 1)), "single") %!assert_equal (class (gamrnd (single ([1, 1]), 1)), "single") ## Test input validation %!error gamrnd () %!error gamrnd (1) %!error ... %! gamrnd (ones (3), ones (2)) %!error ... %! gamrnd (ones (2), ones (3)) %!error gamrnd (i, 2, 3) %!error gamrnd (1, i, 3) %!error ... %! gamrnd (1, 2, 1.2) %!error ... %! gamrnd (1, 2, ones (2)) %!error ... %! gamrnd (1, 2, [2 0 2.5]) %!error ... %! gamrnd (1, 2, 2, 1.5, 5) %!error ... %! gamrnd (2, ones (2), 3) %!error ... %! gamrnd (2, ones (2), [3, 2]) %!error ... %! gamrnd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/geocdf.m000066400000000000000000000146021527311104300244150ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} geocdf (@var{x}, @var{ps}) ## @deftypefnx {statistics} {@var{p} =} geocdf (@var{x}, @var{ps}, @qcode{'upper'}) ## ## Geometric cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the geometric distribution with probability of success parameter ## @var{ps}. The size of @var{p} is the common size of @var{x} and @var{ps}. ## A scalar input functions as a constant matrix of the same size as the other ## inputs. ## ## @code{@var{p} = geocdf (@var{x}, @var{ps}, "upper")} computes the upper tail ## probability of the geometric distribution with parameter @var{ps}, at the ## values in @var{x}. ## ## The geometric distribution models the number of failures (@var{x}) of a ## Bernoulli trial with probability @var{ps} before the first success. ## ## Further information about the geometric distribution can be found at ## @url{https://en.wikipedia.org/wiki/Geometric_distribution} ## ## Input arguments must be @qcode{double}, @qcode{single}, or an integer type; ## logical and character arrays are rejected. Integer input is promoted to ## @qcode{double}, so the result is always a probability. MATLAB is ## inconsistent here: for several of the discrete distributions it returns the ## result in the integer class of the input, truncating a probability to ## @math{0} or @math{1}. ## ## @seealso{geoinv, geopdf, geornd, geofit, geostat} ## @end deftypefn function p = geocdf (x, ps, uflag) ## Check for valid number of input arguments if (nargin < 2) error ("geocdf: function called with too few input arguments."); endif ## Check for common size of X and PS if (! isscalar (x) || ! isscalar (ps)) [retval, x, ps] = common_size (x, ps); if (retval > 0) error ("geocdf: X and PS must be of common size or scalars."); endif endif ## Check for X and PS being double, single, or integer if (! (isnumeric (x) && isnumeric (ps))) error ("geocdf: X and PS must be double, single, or integer."); endif ## Integer input is promoted to double, so the result is a probability ## rather than a value truncated to the input's integer type. if (isinteger (x)) x = double (x); endif if (isinteger (ps)) ps = double (ps); endif ## Check for X and PS being reals if (iscomplex (x) || iscomplex (ps)) error ("geocdf: X and PS must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (ps, 'single')) p = zeros (size (x), 'single'); else p = zeros (size (x)); endif ## Return NaN for out of range parameters k = isnan (x) | ! (ps >= 0) | ! (ps <= 1); p(k) = NaN; ## Return 1 for valid range parameters when X = Inf k = (x == Inf) & (ps >= 0) & (ps <= 1); p(k) = 1; ## Return 0 for X < 0 x(x < 0) = -1; ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) uflag = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("geocdf: invalid argument for upper tail."); else uflag = false; endif ## Get valid instances k = (x >= 0) & (x < Inf) & (x == fix (x)) & (ps > 0) & (ps <= 1); ## Compute CDF if (uflag) if (any (k)) p(k) = betainc (ps(k), 1, (x(k)) + 1, 'upper'); endif else if (isscalar (ps)) p(k) = 1 - ((1 - ps) .^ (x(k) + 1)); else p(k) = 1 - ((1 - ps(k)) .^ (x(k) + 1)); endif endif endfunction %!demo %! ## Plot various CDFs from the geometric distribution %! x = 0:10; %! p1 = geocdf (x, 0.2); %! p2 = geocdf (x, 0.5); %! p3 = geocdf (x, 0.7); %! plot (x, p1, '*b', x, p2, '*g', x, p3, '*r') %! grid on %! xlim ([0, 10]) %! legend ({'ps = 0.2', 'ps = 0.5', 'ps = 0.7'}, 'location', 'southeast') %! title ('Geometric CDF') %! xlabel ('values in x (number of failures)') %! ylabel ('probability') ## Test output %!test %! p = geocdf ([1, 2, 3, 4], 0.25); %! assert_equal (p(1), 0.4375000000, 1e-14); %! assert_equal (p(2), 0.5781250000, 1e-14); %! assert_equal (p(3), 0.6835937500, 1e-14); %! assert_equal (p(4), 0.7626953125, 1e-14); %!test %! p = geocdf ([1, 2, 3, 4], 0.25, 'upper'); %! assert_equal (p(1), 0.5625000000, 1e-14); %! assert_equal (p(2), 0.4218750000, 1e-14); %! assert_equal (p(3), 0.3164062500, 1e-14); %! assert_equal (p(4), 0.2373046875, 1e-14); %!shared x, p %! x = [-1 0 1 Inf]; %! p = [0 0.5 0.75 1]; %!assert_equal (geocdf (x, 0.5*ones (1,4)), p) %!assert_equal (geocdf (x, 0.5), p) %!assert_equal (geocdf (x, 0.5*[-1 NaN 4 1]), [NaN NaN NaN p(4)]) %!assert_equal (geocdf ([x(1:2) NaN x(4)], 0.5), [p(1:2) NaN p(4)]) ## Test class of input preserved %!assert_equal (geocdf ([x, NaN], 0.5), [p, NaN]) %!assert_equal (geocdf (single ([x, NaN]), 0.5), single ([p, NaN])) %!assert_equal (geocdf ([x, NaN], single (0.5)), single ([p, NaN])) ## Test input validation %!error geocdf () %!error geocdf (1) %!error ... %! geocdf (ones (3), ones (2)) %!error ... %! geocdf (ones (2), ones (3)) %!error geocdf (true, 2) %!error geocdf ('a', 2) %!assert_equal (class (geocdf (int32 (2), 2)), 'double') %!error geocdf (i, 2) %!error geocdf (2, i) %!error geocdf (2, 3, 'tail') %!error geocdf (2, 3, 5) statistics-release-2.1.2/inst/Distribution_Functions/geoinv.m000066400000000000000000000114541527311104300244570ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} geoinv (@var{p}, @var{ps}) ## ## Inverse of the geometric cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the geometric distribution with probability of success parameter @var{ps}. ## The size of @var{x} is the common size of @var{p} and @var{ps}. A scalar ## input functions as a constant matrix of the same size as the other inputs. ## ## The geometric distribution models the number of failures (@var{p}) of a ## Bernoulli trial with probability @var{ps} before the first success. ## ## Further information about the geometric distribution can be found at ## @url{https://en.wikipedia.org/wiki/Geometric_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{geocdf, geopdf, geornd, geofit, geostat} ## @end deftypefn function x = geoinv (p, ps) ## Check for valid number of input arguments if (nargin < 2) error ("geoinv: function called with too few input arguments."); endif ## Check for common size of P and PS if (! isscalar (ps) || ! isscalar (ps)) [retval, p, ps] = common_size (p, ps); if (retval > 0) error ("geoinv: P and PS must be of common size or scalars."); endif endif ## Check for P and PS being double or single if (! (isfloat (p) && isfloat (ps))) error ("geoinv: P and PS must be double or single."); endif ## Check for P and PS being reals if (iscomplex (p) || iscomplex (ps)) error ("geoinv: P and PS must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (ps, 'single')) x = NaN (size (p), 'single'); else x = NaN (size (p)); endif ## Handle edge cases k = (p == 1) & (ps >= 0) & (ps <= 1); x(k) = Inf; ## Get valid instances k = (p >= 0) & (p < 1) & (ps > 0) & (ps <= 1); ## Compute iCDF if (isscalar (ps)) x(k) = max (ceil (log (1 - p(k)) / log (1 - ps)) - 1, 0); else x(k) = max (ceil (log (1 - p(k)) ./ log (1 - ps(k))) - 1, 0); endif endfunction %!demo %! ## Plot various iCDFs from the geometric distribution %! p = 0.001:0.001:0.999; %! x1 = geoinv (p, 0.2); %! x2 = geoinv (p, 0.5); %! x3 = geoinv (p, 0.7); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r') %! grid on %! ylim ([0, 10]) %! legend ({'ps = 0.2', 'ps = 0.5', 'ps = 0.7'}, 'location', 'northwest') %! title ('Geometric iCDF') %! xlabel ('probability') %! ylabel ('values in x (number of failures)') ## Test output %!shared p %! p = [-1 0 0.75 1 2]; %!assert_equal (geoinv (p, 0.5*ones (1,5)), [NaN 0 1 Inf NaN]) %!assert_equal (geoinv (p, 0.5), [NaN 0 1 Inf NaN]) %!assert_equal (geoinv (p, 0.5*[1 -1 NaN 4 1]), [NaN NaN NaN NaN NaN]) %!assert_equal (geoinv ([p(1:2) NaN p(4:5)], 0.5), [NaN 0 NaN Inf NaN]) ## Test class of input preserved %!assert_equal (geoinv ([p, NaN], 0.5), [NaN 0 1 Inf NaN NaN]) %!assert_equal (geoinv (single ([p, NaN]), 0.5), single ([NaN 0 1 Inf NaN NaN])) %!assert_equal (geoinv ([p, NaN], single (0.5)), single ([NaN 0 1 Inf NaN NaN])) ## Test input validation %!error geoinv () %!error geoinv (1) %!error ... %! geoinv (ones (3), ones (2)) %!error ... %! geoinv (ones (2), ones (3)) %!error ... %! geoinv (int32 (2), 2) %!error ... %! geoinv (true, 2) %!error ... %! geoinv ('a', 2) %!error ... %! geoinv (i, 2) %!error ... %! geoinv (2, i) statistics-release-2.1.2/inst/Distribution_Functions/geopdf.m000066400000000000000000000115521527311104300244330ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} geopdf (@var{x}, @var{ps}) ## ## Geometric probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the geometric distribution with probability of success parameter @var{ps}. ## The size of @var{y} is the common size of @var{x} and @var{ps}. A scalar ## input functions as a constant matrix of the same size as the other inputs. ## ## The geometric distribution models the number of failures (@var{x}) of a ## Bernoulli trial with probability @var{ps} before the first success. ## ## Further information about the geometric distribution can be found at ## @url{https://en.wikipedia.org/wiki/Geometric_distribution} ## ## Input arguments must be @qcode{double}, @qcode{single}, or an integer type; ## logical and character arrays are rejected. Integer input is promoted to ## @qcode{double}, so the result is always a probability. MATLAB is ## inconsistent here: for several of the discrete distributions it returns the ## result in the integer class of the input, truncating a probability to ## @math{0} or @math{1}. ## ## @seealso{geocdf, geoinv, geornd, geofit, geostat} ## @end deftypefn function y = geopdf (x, ps) ## Check for valid number of input arguments if (nargin < 2) error ("geopdf: function called with too few input arguments."); endif ## Check for common size of X and PS if (! isscalar (x) || ! isscalar (ps)) [retval, x, ps] = common_size (x, ps); if (retval > 0) error ("geopdf: X and PS must be of common size or scalars."); endif endif ## Check for X and PS being double, single, or integer if (! (isnumeric (x) && isnumeric (ps))) error ("geopdf: X and PS must be double, single, or integer."); endif ## Integer input is promoted to double, so the result is a probability ## rather than a value truncated to the input's integer type. if (isinteger (x)) x = double (x); endif if (isinteger (ps)) ps = double (ps); endif ## Check for X and PS being reals if (iscomplex (x) || iscomplex (ps)) error ("geopdf: X and PS must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (ps, 'single')) y = zeros (size (x), 'single'); else y = zeros (size (x)); endif ## Return NaN for out of range parameters k = isnan (x) | ! (ps >= 0) | ! (ps <= 1); y(k) = NaN; ## The density at an infinite abscissa is zero: no proper distribution ## places mass there. y(isinf (x) & (ps >= 0) & (ps <= 1)) = 0; ## Get valid instances k = (x >= 0) & (x < Inf) & (x == fix (x)) & (ps > 0) & (ps <= 1); ## Compute CDF if (isscalar (ps)) y(k) = ps * ((1 - ps) .^ x(k)); else y(k) = ps(k) .* ((1 - ps(k)) .^ x(k)); endif endfunction %!demo %! ## Plot various PDFs from the geometric distribution %! x = 0:10; %! y1 = geopdf (x, 0.2); %! y2 = geopdf (x, 0.5); %! y3 = geopdf (x, 0.7); %! plot (x, y1, '*b', x, y2, '*g', x, y3, '*r') %! grid on %! ylim ([0, 0.8]) %! legend ({'ps = 0.2', 'ps = 0.5', 'ps = 0.7'}, 'location', 'northeast') %! title ('Geometric PDF') %! xlabel ('values in x (number of failures)') %! ylabel ('density') ## Test output %!shared x, y %! x = [-1 0 1 Inf]; %! y = [0, 1/2, 1/4, 0]; %!assert_equal (geopdf (x, 0.5*ones (1,4)), y) %!assert_equal (geopdf (x, 0.5), y) %!assert_equal (geopdf (x, 0.5*[-1 NaN 4 1]), [NaN NaN NaN y(4)]) %!assert_equal (geopdf ([x, NaN], 0.5), [y, NaN]) ## Test class of input preserved %!assert_equal (geopdf (single ([x, NaN]), 0.5), single ([y, NaN]), 5*eps ('single')) %!assert_equal (geopdf ([x, NaN], single (0.5)), single ([y, NaN]), 5*eps ('single')) ## Test input validation %!error geopdf (true, 0.5) %!error geopdf ('a', 0.5) %!assert_equal (class (geopdf (int32 (2), 0.5)), 'double') %!error geopdf () %!error geopdf (1) %!error geopdf (1,2,3) %!error geopdf (ones (3), ones (2)) %!error geopdf (ones (2), ones (3)) %!error geopdf (i, 2) %!error geopdf (2, i) statistics-release-2.1.2/inst/Distribution_Functions/geornd.m000066400000000000000000000133651527311104300244510ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} geornd (@var{ps}) ## @deftypefnx {statistics} {@var{r} =} geornd (@var{ps}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} geornd (@var{ps}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} geornd (@var{ps}, [@var{sz}]) ## ## Random arrays from the geometric distribution. ## ## @code{@var{r} = geornd (@var{ps})} returns an array of random numbers chosen ## from the Birnbaum-Saunders distribution with probability of success parameter ## @var{ps}. The size of @var{r} is the size of @var{ps}. ## ## When called with a single size argument, @code{geornd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## The geometric distribution models the number of failures (@var{x}) of a ## Bernoulli trial with probability @var{ps} before the first success. ## ## Further information about the geometric distribution can be found at ## @url{https://en.wikipedia.org/wiki/Geometric_distribution} ## ## @seealso{geocdf, geoinv, geopdf, geofit, geostat} ## @end deftypefn function r = geornd (ps, varargin) ## Check for valid number of input arguments if (nargin < 1) error ("geornd: function called with too few input arguments."); endif ## Check for PS being reals if (iscomplex (ps)) error ("geornd: PS must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 1) sz = size (ps); elseif (nargin == 2) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("geornd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 2) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("geornd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameter match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (ps) && ! isequal (size (ps), size (ones (sz)))) error ("geornd: PS must be scalar or of size SZ."); endif ## Check for class type if (isa (ps, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from geometric distribution if (isscalar (ps)) if (ps > 0 && ps < 1); r = floor (- rande (sz, cls) ./ log (1 - ps)); elseif (ps == 0) r = Inf (sz, cls); elseif (ps == 1) r = zeros (sz, cls); elseif (ps < 0 || ps > 1) r = NaN (sz, cls); endif else r = floor (- rande (sz, cls) ./ log (1 - ps)); k = ! (ps >= 0) | ! (ps <= 1); r(k) = NaN; k = (ps == 0); r(k) = Inf; endif endfunction ## Test output %!assert_equal (size (geornd (0.5)), [1, 1]) %!assert_equal (size (geornd (0.5*ones (2,1))), [2, 1]) %!assert_equal (size (geornd (0.5*ones (2,2))), [2, 2]) %!assert_equal (size (geornd (0.5, 3)), [3, 3]) %!assert_equal (size (geornd (0.5, [4 1])), [4, 1]) %!assert_equal (size (geornd (0.5, 4, 1)), [4, 1]) %!assert_equal (size (geornd (0.5, [])), [0, 0]) %!assert_equal (size (geornd (0.5, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (geornd (1, -1)), [0, 0]) %!assert_equal (size (geornd (1, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (geornd (1, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (geornd (0.5)), "double") %!assert_equal (class (geornd (single (0.5))), "single") %!assert_equal (class (geornd (single ([0.5 0.5]))), "single") %!assert_equal (class (geornd (single (0))), "single") %!assert_equal (class (geornd (single (1))), "single") ## Test input validation %!error geornd () %!error geornd (i) %!error ... %! geornd (1, 1.2) %!error ... %! geornd (1, ones (2)) %!error ... %! geornd (1, [2 0 2.5]) %!error ... %! geornd (ones (2), ones (2)) %!error ... %! geornd (1, 2, 1.5, 5) %!error geornd (ones (2,2), 3) %!error geornd (ones (2,2), [3, 2]) %!error geornd (ones (2,2), 2, 3) statistics-release-2.1.2/inst/Distribution_Functions/gevcdf.m000066400000000000000000000201651527311104300244250ustar00rootroot00000000000000## Copyright (C) 2012 Nir Krakauer ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} gevcdf (@var{x}, @var{k}, @var{sigma}, @var{mu}) ## @deftypefnx {statistics} {@var{p} =} gevcdf (@var{x}, @var{k}, @var{sigma}, @var{mu}, @qcode{'upper'}) ## ## Generalized extreme value (GEV) cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the GEV distribution with shape parameter @var{k}, scale parameter ## @var{sigma}, and location parameter @var{mu}. The size of @var{p} is the ## common size of @var{x}, @var{k}, @var{sigma}, and @var{mu}. A scalar input ## functions as a constant matrix of the same size as the other inputs. ## ## @code{[@dots{}] = gevcdf (@var{x}, @var{k}, @var{sigma}, @var{mu}, "upper")} ## computes the upper tail probability of the GEV distribution with parameters ## @var{k}, @var{sigma}, and @var{mu}, at the values in @var{x}. ## ## When @qcode{@var{k} < 0}, the GEV is the type III extreme value distribution. ## When @qcode{@var{k} > 0}, the GEV distribution is the type II, or Frechet, ## extreme value distribution. If @var{W} has a Weibull distribution as ## computed by the @code{wblcdf} function, then @qcode{-@var{W}} has a type III ## extreme value distribution and @qcode{1/@var{W}} has a type II extreme value ## distribution. In the limit as @var{k} approaches @qcode{0}, the GEV is the ## mirror image of the type I extreme value distribution as computed by the ## @code{evcdf} function. ## ## The mean of the GEV distribution is not finite when @qcode{@var{k} >= 1}, and ## the variance is not finite when @qcode{@var{k} >= 1/2}. The GEV distribution ## has positive density only for values of @var{x} such that ## @qcode{@var{k} * (@var{x} - @var{mu}) / @var{sigma} > -1}. ## ## Further information about the generalized extreme value distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Generalized_extreme_value_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{gevinv, gevpdf, gevrnd, gevfit, gevlike, gevstat} ## @end deftypefn function p = gevcdf (x, k, sigma, mu, uflag) ## Check for valid number of input arguments if (nargin < 4) error ("gevcdf: function called with too few input arguments."); endif ## Check for valid "upper" flag if (nargin > 4) if (! strcmpi (uflag, 'upper')) error ("gevcdf: invalid argument for upper tail."); else uflag = true; endif else uflag = false; endif ## Check for common size of X, K, SIGMA, and MU if (! isscalar (x) || ! isscalar (k) || ! isscalar (sigma) || ! isscalar (mu)) [err, x, k, sigma, mu] = common_size (x, k, sigma, mu); if (err > 0) error ("gevcdf: X, K, SIGMA, and MU must be of common size or scalars."); endif endif ## Check for X, K, SIGMA, and MU being double or single if (! (isfloat (x) && isfloat (k) && isfloat (sigma) && isfloat (mu))) error ("gevcdf: X, K, SIGMA, and MU must be double or single."); endif ## Check for X, K, SIGMA, and MU being reals if (iscomplex (x) || iscomplex (k) || iscomplex (sigma) || iscomplex (mu)) error ("gevcdf: X, K, SIGMA, and MU must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (k, 'single') ... || isa (sigma, 'single') || isa (mu, 'single')); is_class = 'single'; else is_class = 'double'; endif ## Prepare output p = zeros (size (x), is_class); ## Return NaN for out of range parameter SIGMA. sigma(sigma <= 0) = NaN; ## Calculate z z = (x - mu) ./ sigma; ## Process k == 0 k_0 = (abs (k) < eps); if (uflag) p(k_0) = -expm1 (-exp (-z(k_0))); else p(k_0) = exp (-exp (-z(k_0))); endif ## Process k != 0 k_0 = ! k_0; t = z .* k; if (uflag) p(k_0) = -expm1 (-exp (-(1 ./ k(k_0)) .* log1p (t(k_0)))); else p(k_0) = exp (-exp (-(1 ./ k(k_0)) .* log1p (t(k_0)))); endif ## Return 0 or 1 for 1 + k.*(x-mu)/sigma > 0 k_1 = k_0 & (t<=-1); t(k_1) = 0; if uflag == true p(k_1) = (k(k_1) >= 0); else p(k_1) = (k(k_1) < 0); endif endfunction %!demo %! ## Plot various CDFs from the generalized extreme value distribution %! x = -1:0.001:10; %! p1 = gevcdf (x, 1, 1, 1); %! p2 = gevcdf (x, 0.5, 1, 1); %! p3 = gevcdf (x, 1, 1, 5); %! p4 = gevcdf (x, 1, 2, 5); %! p5 = gevcdf (x, 1, 5, 5); %! p6 = gevcdf (x, 1, 0.5, 5); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', ... %! x, p4, '-c', x, p5, '-m', x, p6, '-k') %! grid on %! xlim ([-1, 10]) %! legend ({'k = 1, σ = 1, μ = 1', 'k = 0.5, σ = 1, μ = 1', ... %! 'k = 1, σ = 1, μ = 5', 'k = 1, σ = 2, μ = 5', ... %! 'k = 1, σ = 5, μ = 5', 'k = 1, σ = 0.5, μ = 5'}, ... %! 'location', 'southeast') %! title ('Generalized extreme value CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!test %! x = 0:0.5:2.5; %! sigma = 1:6; %! k = 1; %! mu = 0; %! p = gevcdf (x, k, sigma, mu); %! expected_p = [0.36788, 0.44933, 0.47237, 0.48323, 0.48954, 0.49367]; %! assert_equal (p, expected_p, 0.001); %!test %! x = -0.5:0.5:2.5; %! sigma = 0.5; %! k = 1; %! mu = 0; %! p = gevcdf (x, k, sigma, mu); %! expected_p = [0, 0.36788, 0.60653, 0.71653, 0.77880, 0.81873, 0.84648]; %! assert_equal (p, expected_p, 0.001); %!test # check for continuity for k near 0 %! x = 1; %! sigma = 0.5; %! k = -0.03:0.01:0.03; %! mu = 0; %! p = gevcdf (x, k, sigma, mu); %! expected_p = [0.88062, 0.87820, 0.87580, 0.87342, 0.87107, 0.86874, 0.86643]; %! assert_equal (p, expected_p, 0.001); ## Test input validation %!error gevcdf () %!error gevcdf (1) %!error gevcdf (1, 2) %!error gevcdf (1, 2, 3) %!error ... %! gevcdf (1, 2, 3, 4, 5, 6) %!error gevcdf (1, 2, 3, 4, 'tail') %!error gevcdf (1, 2, 3, 4, 5) %!error ... %! gevcdf (ones (3), ones (2), ones (2), ones (2)) %!error ... %! gevcdf (ones (2), ones (3), ones (2), ones (2)) %!error ... %! gevcdf (ones (2), ones (2), ones (3), ones (2)) %!error ... %! gevcdf (ones (2), ones (2), ones (2), ones (3)) %!error gevcdf (int32 (2), 2, 3, 4) %!error gevcdf (true, 2, 3, 4) %!error gevcdf ('a', 2, 3, 4) %!error gevcdf (i, 2, 3, 4) %!error gevcdf (1, i, 3, 4) %!error gevcdf (1, 2, i, 4) %!error gevcdf (1, 2, 3, i) statistics-release-2.1.2/inst/Distribution_Functions/gevinv.m000066400000000000000000000152611527311104300244660ustar00rootroot00000000000000## Copyright (C) 2012 Nir Krakauer ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} gevinv (@var{p}, @var{k}, @var{sigma}, @var{mu}) ## ## Inverse of the generalized extreme value (GEV) cumulative distribution ## function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the GEV distribution with shape parameter @var{k}, scale parameter ## @var{sigma}, and location parameter @var{mu}. The size of @var{p} is the ## common size of @var{x}, @var{k}, @var{sigma}, and @var{mu}. A scalar input ## functions as a constant matrix of the same size as the other inputs. ## ## When @qcode{@var{k} < 0}, the GEV is the type III extreme value distribution. ## When @qcode{@var{k} > 0}, the GEV distribution is the type II, or Frechet, ## extreme value distribution. If @var{W} has a Weibull distribution as ## computed by the @code{wblcdf} function, then @qcode{-@var{W}} has a type III ## extreme value distribution and @qcode{1/@var{W}} has a type II extreme value ## distribution. In the limit as @var{k} approaches @qcode{0}, the GEV is the ## mirror image of the type I extreme value distribution as computed by the ## @code{evcdf} function. ## ## The mean of the GEV distribution is not finite when @qcode{@var{k} >= 1}, and ## the variance is not finite when @qcode{@var{k} >= 1/2}. The GEV distribution ## has positive density only for values of @var{x} such that ## @qcode{@var{k} * (@var{x} - @var{mu}) / @var{sigma} > -1}. ## ## Further information about the generalized extreme value distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Generalized_extreme_value_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{gevcdf, gevpdf, gevrnd, gevfit, gevlike, gevstat} ## @end deftypefn function x = gevinv (p, k, sigma, mu) ## Check for valid number of input arguments if (nargin < 4) error ("gevinv: function called with too few input arguments."); endif ## Check for common size of P, K, SIGMA, and MU [retval, p, k, sigma, mu] = common_size (p, k, sigma, mu); if (retval > 0) error ("gevinv: P, K, SIGMA, and MU must be of common size or scalars."); endif ## Check for P, K, SIGMA, and MU being double or single if (! (isfloat (p) && isfloat (k) && isfloat (sigma) && isfloat (mu))) error ("gevinv: P, K, SIGMA, and MU must be double or single."); endif ## Check for P, K, SIGMA, and MU being reals if (iscomplex (p) || iscomplex (k) || iscomplex (sigma) || iscomplex (mu)) error ("gevinv: P, K, SIGMA, and MU must not be complex."); endif is_neginf = p == 0; is_posinf = p == 1; is_nan = p < 0 | p > 1 | isnan (p); x = p; llP = log (-log (p)); kllP = k .* llP; ## Use the Taylor series expansion of the exponential to ## avoid roundoff error or dividing by zero when k is small ii = (abs (kllP) < 1E-4); x(ii) = mu(ii) - sigma(ii) .* llP(ii) .* (1 - kllP(ii) .* (1 - kllP(ii))); x(! ii) = mu(! ii) + (sigma(! ii) ./ k(! ii)) .* (exp (-kllP(! ii)) - 1); x(is_neginf) = -Inf; x(is_posinf) = Inf; x(is_nan) = NaN; endfunction %!demo %! ## Plot various iCDFs from the generalized extreme value distribution %! p = 0.001:0.001:0.999; %! x1 = gevinv (p, 1, 1, 1); %! x2 = gevinv (p, 0.5, 1, 1); %! x3 = gevinv (p, 1, 1, 5); %! x4 = gevinv (p, 1, 2, 5); %! x5 = gevinv (p, 1, 5, 5); %! x6 = gevinv (p, 1, 0.5, 5); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', ... %! p, x4, '-c', p, x5, '-m', p, x6, '-k') %! grid on %! ylim ([-1, 10]) %! legend ({'k = 1, σ = 1, μ = 1', 'k = 0.5, σ = 1, μ = 1', ... %! 'k = 1, σ = 1, μ = 5', 'k = 1, σ = 2, μ = 5', ... %! 'k = 1, σ = 5, μ = 5', 'k = 1, σ = 0.5, μ = 5'}, ... %! 'location', 'northwest') %! title ('Generalized extreme value iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!test %! p = 0.1:0.1:0.9; %! k = 0; %! sigma = 1; %! mu = 0; %! x = gevinv (p, k, sigma, mu); %! c = gevcdf (x, k, sigma, mu); %! assert_equal (c, p, 0.001); %!test %! p = 0.1:0.1:0.9; %! k = 1; %! sigma = 1; %! mu = 0; %! x = gevinv (p, k, sigma, mu); %! c = gevcdf (x, k, sigma, mu); %! assert_equal (c, p, 0.001); %!test %! p = 0.1:0.1:0.9; %! k = 0.3; %! sigma = 1; %! mu = 0; %! x = gevinv (p, k, sigma, mu); %! c = gevcdf (x, k, sigma, mu); %! assert_equal (c, p, 0.001); ## Test input validation %!error gevinv () %!error gevinv (1) %!error gevinv (1, 2) %!error gevinv (1, 2, 3) %!error ... %! gevinv (ones (3), ones (2), ones (2), ones (2)) %!error ... %! gevinv (ones (2), ones (3), ones (2), ones (2)) %!error ... %! gevinv (ones (2), ones (2), ones (3), ones (2)) %!error ... %! gevinv (ones (2), ones (2), ones (2), ones (3)) %!error gevinv (int32 (2), 2, 3, 4) %!error gevinv (true, 2, 3, 4) %!error gevinv ('a', 2, 3, 4) %!error gevinv (i, 2, 3, 4) %!error gevinv (1, i, 3, 4) %!error gevinv (1, 2, i, 4) %!error gevinv (1, 2, 3, i) statistics-release-2.1.2/inst/Distribution_Functions/gevpdf.m000066400000000000000000000154011527311104300244370ustar00rootroot00000000000000## Copyright (C) 2012 Nir Krakauer ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} gevpdf (@var{x}, @var{k}, @var{sigma}, @var{mu}) ## ## Generalized extreme value (GEV) probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the GEV distribution with shape parameter @var{k}, scale parameter ## @var{sigma}, and location parameter @var{mu}. The size of @var{y} is the ## common size of @var{x}, @var{k}, @var{sigma}, and @var{mu}. A scalar input ## functions as a constant matrix of the same size as the other inputs. ## ## When @qcode{@var{k} < 0}, the GEV is the type III extreme value distribution. ## When @qcode{@var{k} > 0}, the GEV distribution is the type II, or Frechet, ## extreme value distribution. If @var{W} has a Weibull distribution as ## computed by the @code{wblcdf} function, then @qcode{-@var{W}} has a type III ## extreme value distribution and @qcode{1/@var{W}} has a type II extreme value ## distribution. In the limit as @var{k} approaches @qcode{0}, the GEV is the ## mirror image of the type I extreme value distribution as computed by the ## @code{evcdf} function. ## ## The mean of the GEV distribution is not finite when @qcode{@var{k} >= 1}, and ## the variance is not finite when @qcode{@var{k} >= 1/2}. The GEV distribution ## has positive density only for values of @var{x} such that ## @qcode{@var{k} * (@var{x} - @var{mu}) / @var{sigma} > -1}. ## ## Further information about the generalized extreme value distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Generalized_extreme_value_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{gevcdf, gevinv, gevrnd, gevfit, gevlike, gevstat} ## @end deftypefn function y = gevpdf (x, k, sigma, mu) ## Check for valid number of input arguments if (nargin < 4) error ("gevpdf: function called with too few input arguments."); endif ## Check for common size of X, K, SIGMA, and MU [retval, x, k, sigma, mu] = common_size (x, k, sigma, mu); if (retval > 0) error ("gevpdf: X, K, SIGMA, and MU must be of common size or scalars."); endif ## Check for X, K, SIGMA, and MU being double or single if (! (isfloat (x) && isfloat (k) && isfloat (sigma) && isfloat (mu))) error ("gevpdf: X, K, SIGMA, and MU must be double or single."); endif ## Check for X, K, SIGMA, and MU being reals if (iscomplex (x) || iscomplex (k) || iscomplex (sigma) || iscomplex (mu)) error ("gevpdf: X, K, SIGMA, and MU must not be complex."); endif z = 1 + k .* (x - mu) ./ sigma; ## Calculate generalized extreme value PDF y = exp (-(z .^ (-1 ./ k))) .* (z .^ (-1 - 1 ./ k)) ./ sigma; y(z <= 0) = 0; ## Use a different formula if k is very close to zero inds = (abs (k) < (eps^0.7)); if (any (inds)) z = (mu(inds) - x(inds)) ./ sigma(inds); y(inds) = exp (z - exp (z)) ./ sigma(inds); endif endfunction %!demo %! ## Plot various PDFs from the generalized extreme value distribution %! x = -1:0.001:10; %! y1 = gevpdf (x, 1, 1, 1); %! y2 = gevpdf (x, 0.5, 1, 1); %! y3 = gevpdf (x, 1, 1, 5); %! y4 = gevpdf (x, 1, 2, 5); %! y5 = gevpdf (x, 1, 5, 5); %! y6 = gevpdf (x, 1, 0.5, 5); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', ... %! x, y4, '-c', x, y5, '-m', x, y6, '-k') %! grid on %! xlim ([-1, 10]) %! ylim ([0, 1.1]) %! legend ({'k = 1, σ = 1, μ = 1', 'k = 0.5, σ = 1, μ = 1', ... %! 'k = 1, σ = 1, μ = 5', 'k = 1, σ = 2, μ = 5', ... %! 'k = 1, σ = 5, μ = 5', 'k = 1, σ = 0.5, μ = 5'}, ... %! 'location', 'northeast') %! title ('Generalized extreme value PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!test %! x = 0:0.5:2.5; %! sigma = 1:6; %! k = 1; %! mu = 0; %! y = gevpdf (x, k, sigma, mu); %! expected_y = [0.367879 0.143785 0.088569 0.063898 0.049953 0.040997]; %! assert_equal (y, expected_y, 0.001); %!test %! x = -0.5:0.5:2.5; %! sigma = 0.5; %! k = 1; %! mu = 0; %! y = gevpdf (x, k, sigma, mu); %! expected_y = [0 0.735759 0.303265 0.159229 0.097350 0.065498 0.047027]; %! assert_equal (y, expected_y, 0.001); %!test # check for continuity for k near 0 %! x = 1; %! sigma = 0.5; %! k = -0.03:0.01:0.03; %! mu = 0; %! y = gevpdf (x, k, sigma, mu); %! expected_y = [0.23820 0.23764 0.23704 0.23641 0.23576 0.23508 0.23438]; %! assert_equal (y, expected_y, 0.001); ## Test input validation %!error gevpdf () %!error gevpdf (1) %!error gevpdf (1, 2) %!error gevpdf (1, 2, 3) %!error ... %! gevpdf (ones (3), ones (2), ones (2), ones (2)) %!error ... %! gevpdf (ones (2), ones (3), ones (2), ones (2)) %!error ... %! gevpdf (ones (2), ones (2), ones (3), ones (2)) %!error ... %! gevpdf (ones (2), ones (2), ones (2), ones (3)) %!error gevpdf (int32 (2), 2, 3, 4) %!error gevpdf (true, 2, 3, 4) %!error gevpdf ('a', 2, 3, 4) %!error gevpdf (i, 2, 3, 4) %!error gevpdf (1, i, 3, 4) %!error gevpdf (1, 2, i, 4) %!error gevpdf (1, 2, 3, i) statistics-release-2.1.2/inst/Distribution_Functions/gevrnd.m000066400000000000000000000174251527311104300244610ustar00rootroot00000000000000## Copyright (C) 2012 Nir Krakauer ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} gevrnd (@var{k}, @var{sigma}, @var{mu}) ## @deftypefnx {statistics} {@var{r} =} gevrnd (@var{k}, @var{sigma}, @var{mu}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} gevrnd (@var{k}, @var{sigma}, @var{mu}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} gevrnd (@var{k}, @var{sigma}, @var{mu}, [@var{sz}]) ## ## Random arrays from the generalized extreme value (GEV) distribution. ## ## @code{@var{r} = gevrnd (@var{k}, @var{sigma}, @var{mu}} returns an array of ## random numbers chosen from the GEV distribution with shape parameter @var{k}, ## scale parameter @var{sigma}, and location parameter @var{mu}. The size of ## @var{r} is the common size of @var{k}, @var{sigma}, and @var{mu}. A scalar ## input functions as a constant matrix of the same size as the other inputs. ## ## When called with a single size argument, @code{gevrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## When @qcode{@var{k} < 0}, the GEV is the type III extreme value distribution. ## When @qcode{@var{k} > 0}, the GEV distribution is the type II, or Frechet, ## extreme value distribution. If @var{W} has a Weibull distribution as ## computed by the @code{wblcdf} function, then @qcode{-@var{W}} has a type III ## extreme value distribution and @qcode{1/@var{W}} has a type II extreme value ## distribution. In the limit as @var{k} approaches @qcode{0}, the GEV is the ## mirror image of the type I extreme value distribution as computed by the ## @code{evcdf} function. ## ## The mean of the GEV distribution is not finite when @qcode{@var{k} >= 1}, and ## the variance is not finite when @qcode{@var{k} >= 1/2}. The GEV distribution ## has positive density only for values of @var{x} such that ## @qcode{@var{k} * (@var{x} - @var{mu}) / @var{sigma} > -1}. ## ## Further information about the generalized extreme value distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Generalized_extreme_value_distribution} ## ## @seealso{gevcdf, gevinv, gevpdf, gevfit, gevlike, gevstat} ## @end deftypefn function r = gevrnd (k, sigma, mu, varargin) ## Check for valid number of input arguments if (nargin < 3) error ("gevrnd: function called with too few input arguments."); endif ## Check for common size of K, SIGMA, and MU if (! isscalar (k) || ! isscalar (sigma) || ! isscalar (mu)) [retval, k, sigma, mu] = common_size (k, sigma, mu); if (retval > 0) error ("gevrnd: K, SIGMA, and MU must be of common size or scalars."); endif endif ## Check for K, SIGMA, and MU being reals if (iscomplex (k) || iscomplex (sigma) || iscomplex (mu)) error ("gevrnd: K, SIGMA, and MU must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 3) sz = size (k); elseif (nargin == 4) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("gevrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 4) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("gevrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (k) && ! isequal (size (k), size (ones (sz)))) error ("gevrnd: K, SIGMA, and MU must be scalars or of size SZ."); endif ## Check for class type if (isa (k, 'single') || isa (sigma, 'single') || isa (mu, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from Burr type XII distribution r = gevinv (rand (sz), k, sigma, mu); r = cast (r, cls); endfunction ## Test output %!assert_equal (size (gevrnd (1, 2, 1)), [1, 1]); %!assert_equal (size (gevrnd (ones (2, 1), 2, 1)), [2, 1]); %!assert_equal (size (gevrnd (ones (2, 2), 2, 1)), [2, 2]); %!assert_equal (size (gevrnd (1, 2 * ones (2, 1), 1)), [2, 1]); %!assert_equal (size (gevrnd (1, 2 * ones (2, 2), 1)), [2, 2]); %!assert_equal (size (gevrnd (1, 2, 1, 3)), [3, 3]); %!assert_equal (size (gevrnd (1, 2, 1, [4, 1])), [4, 1]); %!assert_equal (size (gevrnd (1, 2, 1, 4, 1)), [4, 1]); %!assert_equal (size (gevrnd (1, 2, 1, [])), [0, 0]) %!assert_equal (size (gevrnd (1, 2, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (gevrnd (1, 2, 3, -1)), [0, 0]) %!assert_equal (size (gevrnd (1, 2, 3, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (gevrnd (1, 2, 3, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (gevrnd (1,1,1)), "double") %!assert_equal (class (gevrnd (single (1),1,1)), "single") %!assert_equal (class (gevrnd (single ([1 1]),1,1)), "single") %!assert_equal (class (gevrnd (1,single (1),1)), "single") %!assert_equal (class (gevrnd (1,single ([1 1]),1)), "single") %!assert_equal (class (gevrnd (1,1,single (1))), "single") %!assert_equal (class (gevrnd (1,1,single ([1 1]))), "single") ## Test input validation %!error gevrnd () %!error gevrnd (1) %!error gevrnd (1, 2) %!error ... %! gevrnd (ones (3), ones (2), ones (2)) %!error ... %! gevrnd (ones (2), ones (3), ones (2)) %!error ... %! gevrnd (ones (2), ones (2), ones (3)) %!error gevrnd (i, 2, 3) %!error gevrnd (1, i, 3) %!error gevrnd (1, 2, i) %!error ... %! gevrnd (1, 2, 3, 1.2) %!error ... %! gevrnd (1, 2, 3, ones (2)) %!error ... %! gevrnd (1, 2, 3, [2 0 2.5]) %!error ... %! gevrnd (1, 2, 3, 2, 1.5, 5) %!error ... %! gevrnd (2, ones (2), 2, 3) %!error ... %! gevrnd (2, ones (2), 2, [3, 2]) %!error ... %! gevrnd (2, ones (2), 2, 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/gpcdf.m000066400000000000000000000310741527311104300242530ustar00rootroot00000000000000## Copyright (C) 1997-2015 Kurt Hornik ## Copyright (C) 2016 Dag Lyberg ## Copyright (C) 2018 John Donoghue ## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} gpcdf (@var{x}, @var{k}, @var{sigma}, @var{theta}) ## @deftypefnx {statistics} {@var{p} =} gpcdf (@var{x}, @var{k}, @var{sigma}, @var{theta}, @qcode{'upper'}) ## ## Generalized Pareto cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the generalized Pareto distribution with shape parameter @var{k}, ## scale parameter @var{sigma}, and location parameter @var{theta}. The size of ## @var{p} is the common size of @var{x}, @var{k}, @var{sigma}, and @var{theta}. ## A scalar input functions as a constant matrix of the same size as the other ## inputs. ## ## @code{[@dots{}] = gpcdf(@var{x}, @var{k}, @var{sigma}, @var{theta}, "upper")} ## computes the upper tail probability of the generalized Pareto distribution ## with parameters @var{k}, @var{sigma}, and @var{theta}, at the values in ## @var{x}. ## ## When @qcode{@var{k} = 0} and @qcode{@var{theta} = 0}, the Generalized Pareto ## is equivalent to the exponential distribution. When @qcode{@var{k} > 0} and ## @code{@var{theta} = @var{sigma} / @var{k}} the Generalized Pareto is ## equivalent to the Pareto distribution. The mean of the Generalized Pareto is ## not finite when @qcode{@var{k} >= 1} and the variance is not finite when ## @qcode{@var{k} >= 1/2}. When @qcode{@var{k} >= 0}, the Generalized Pareto ## has positive density for @qcode{@var{x} > @var{theta}}, or, when ## @qcode{@var{theta} < 0}, for ## @qcode{0 <= (@var{x} - @var{theta}) / @var{sigma} <= -1 / @var{k}}. ## ## Further information about the generalized Pareto distribution can be found at ## @url{https://en.wikipedia.org/wiki/Generalized_Pareto_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{gpinv, gppdf, gprnd, gpfit, gplike, gpstat} ## @end deftypefn function p = gpcdf (x, k, sigma, theta, uflag) ## Check for valid number of input arguments if (nargin < 4) error ("gpcdf: function called with too few input arguments."); endif ## Check for valid "upper" flag if (nargin > 4) if (! strcmpi (uflag, 'upper')) error ("gpcdf: invalid argument for upper tail."); else uflag = true; endif else uflag = false; endif ## Check for common size of X, K, SIGMA, and THETA if (! isscalar (x) || ! isscalar (k) || ! isscalar (sigma) || ! isscalar (theta)) [err, x, k, sigma, theta] = common_size (x, k, sigma, theta); if (err > 0) error ("gpcdf: X, K, SIGMA, and THETA must be of common size or scalars."); endif endif ## Check for X, K, SIGMA, and THETA being double or single if (! (isfloat (x) && isfloat (k) && isfloat (sigma) && isfloat (theta))) error ("gpcdf: X, K, SIGMA, and THETA must be double or single."); endif ## Check for X, K, SIGMA, and THETA being reals if (iscomplex (x) || iscomplex (k) || iscomplex (sigma) || iscomplex (theta)) error ("gpcdf: X, K, SIGMA, and THETA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (k, 'single') ... || isa (sigma, 'single') || isa (theta, 'single')); is_class = 'single'; else is_class = 'double'; endif ## Prepare output p = zeros (size (x), is_class); ## Return NaNs for out of range values of sigma parameter sigma(sigma <= 0) = NaN; ## Calculate (x-theta)/sigma => 0 and force zero below that z = (x - theta) ./ sigma; z(z < 0) = 0; ## Compute cases for SHAPE == 0 kz = (abs (k) < eps (is_class)); if (uflag) p(kz) = exp (-z(kz)); else p(kz) = -expm1 (-z(kz)); endif ## For SHAPE < 0, calculate 0 <= x/sigma <= -1/k and force zero below that t = z .* k; kt = (t <= -1 & k < -eps (is_class)); t(kt) = 0; ## Compute cases for SHAPE != 0 kz = ! kz; if (uflag) p(kz) = exp ((-1 ./ k(kz)) .* log1p (t(kz))); else p(kz) = -expm1 ((-1 ./ k(kz)) .* log1p (t(kz))); endif if (uflag) p(kt) = 0; else p(kt) = 1; endif ## For SHAPE == NaN force p = NaN p(isnan (k)) = NaN; endfunction %!demo %! ## Plot various CDFs from the generalized Pareto distribution %! x = 0:0.001:5; %! p1 = gpcdf (x, 1, 1, 0); %! p2 = gpcdf (x, 5, 1, 0); %! p3 = gpcdf (x, 20, 1, 0); %! p4 = gpcdf (x, 1, 2, 0); %! p5 = gpcdf (x, 5, 2, 0); %! p6 = gpcdf (x, 20, 2, 0); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', ... %! x, p4, '-c', x, p5, '-m', x, p6, '-k') %! grid on %! xlim ([0, 5]) %! legend ({'k = 1, σ = 1, θ = 0', 'k = 5, σ = 1, θ = 0', ... %! 'k = 20, σ = 1, θ = 0', 'k = 1, σ = 2, θ = 0', ... %! 'k = 5, σ = 2, θ = 0', 'k = 20, σ = 2, θ = 0'}, ... %! 'location', 'northwest') %! title ('Generalized Pareto CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, y1, y1u, y2, y2u, y3, y3u %! x = [-Inf, -1, 0, 1/2, 1, Inf]; %! y1 = [0, 0, 0, 0.3934693402873666, 0.6321205588285577, 1]; %! y1u = [1, 1, 1, 0.6065306597126334, 0.3678794411714423, 0]; %! y2 = [0, 0, 0, 1/3, 1/2, 1]; %! y2u = [1, 1, 1, 2/3, 1/2, 0]; %! y3 = [0, 0, 0, 1/2, 1, 1]; %! y3u = [1, 1, 1, 1/2, 0, 0]; %!assert_equal (gpcdf (x, zeros (1,6), ones (1,6), zeros (1,6)), y1, eps) %!assert_equal (gpcdf (x, 0, 1, zeros (1,6)), y1, eps) %!assert_equal (gpcdf (x, 0, ones (1,6), 0), y1, eps) %!assert_equal (gpcdf (x, zeros (1,6), 1, 0), y1, eps) %!assert_equal (gpcdf (x, 0, 1, 0), y1, eps) %!assert_equal (gpcdf (x, 0, 1, [0, 0, 0, NaN, 0, 0]), [y1(1:3), NaN, y1(5:6)], eps) %!assert_equal (gpcdf (x, 0, [1, 1, 1, NaN, 1, 1], 0), [y1(1:3), NaN, y1(5:6)], eps) %!assert_equal (gpcdf (x, [0, 0, 0, NaN, 0, 0], 1, 0), [y1(1:3), NaN, y1(5:6)], eps) %!assert_equal (gpcdf ([x(1:3), NaN, x(5:6)], 0, 1, 0), [y1(1:3), NaN, y1(5:6)], eps) %!assert_equal (gpcdf (x, zeros (1,6), ones (1,6), zeros (1,6), 'upper'), y1u, eps) %!assert_equal (gpcdf (x, 0, 1, zeros (1,6), 'upper'), y1u, eps) %!assert_equal (gpcdf (x, 0, ones (1,6), 0, 'upper'), y1u, eps) %!assert_equal (gpcdf (x, zeros (1,6), 1, 0, 'upper'), y1u, eps) %!assert_equal (gpcdf (x, 0, 1, 0, 'upper'), y1u, eps) %!assert_equal (gpcdf (x, ones (1,6), ones (1,6), zeros (1,6)), y2, eps) %!assert_equal (gpcdf (x, 1, 1, zeros (1,6)), y2, eps) %!assert_equal (gpcdf (x, 1, ones (1,6), 0), y2, eps) %!assert_equal (gpcdf (x, ones (1,6), 1, 0), y2, eps) %!assert_equal (gpcdf (x, 1, 1, 0), y2, eps) %!assert_equal (gpcdf (x, 1, 1, [0, 0, 0, NaN, 0, 0]), [y2(1:3), NaN, y2(5:6)], eps) %!assert_equal (gpcdf (x, 1, [1, 1, 1, NaN, 1, 1], 0), [y2(1:3), NaN, y2(5:6)], eps) %!assert_equal (gpcdf (x, [1, 1, 1, NaN, 1, 1], 1, 0), [y2(1:3), NaN, y2(5:6)], eps) %!assert_equal (gpcdf ([x(1:3), NaN, x(5:6)], 1, 1, 0), [y2(1:3), NaN, y2(5:6)], eps) %!assert_equal (gpcdf (x, ones (1,6), ones (1,6), zeros (1,6), 'upper'), y2u, eps) %!assert_equal (gpcdf (x, 1, 1, zeros (1,6), 'upper'), y2u, eps) %!assert_equal (gpcdf (x, 1, ones (1,6), 0, 'upper'), y2u, eps) %!assert_equal (gpcdf (x, ones (1,6), 1, 0, 'upper'), y2u, eps) %!assert_equal (gpcdf (x, 1, 1, 0, 'upper'), y2u, eps) %!assert_equal (gpcdf (x, 1, 1, [0, 0, 0, NaN, 0, 0], 'upper'), ... %! [y2u(1:3), NaN, y2u(5:6)], eps) %!assert_equal (gpcdf (x, 1, [1, 1, 1, NaN, 1, 1], 0, 'upper'), ... %! [y2u(1:3), NaN, y2u(5:6)], eps) %!assert_equal (gpcdf (x, [1, 1, 1, NaN, 1, 1], 1, 0, 'upper'), ... %! [y2u(1:3), NaN, y2u(5:6)], eps) %!assert_equal (gpcdf ([x(1:3), NaN, x(5:6)], 1, 1, 0, 'upper'), ... %! [y2u(1:3), NaN, y2u(5:6)], eps) %!assert_equal (gpcdf (x, -ones (1,6), ones (1,6), zeros (1,6)), y3, eps) %!assert_equal (gpcdf (x, -1, 1, zeros (1,6)), y3, eps) %!assert_equal (gpcdf (x, -1, ones (1,6), 0), y3, eps) %!assert_equal (gpcdf (x, -ones (1,6), 1, 0), y3, eps) %!assert_equal (gpcdf (x, -1, 1, 0), y3, eps) %!assert_equal (gpcdf (x, -1, 1, [0, 0, 0, NaN, 0, 0]), [y3(1:3), NaN, y3(5:6)], eps) %!assert_equal (gpcdf (x, -1, [1, 1, 1, NaN, 1, 1], 0), [y3(1:3), NaN, y3(5:6)], eps) %!assert_equal (gpcdf (x, [-1, -1, -1, NaN, -1, -1], 1, 0), [y3(1:3), NaN, y3(5:6)], eps) %!assert_equal (gpcdf ([x(1:3), NaN, x(5:6)], -1, 1, 0), [y3(1:3), NaN, y3(5:6)], eps) %!assert_equal (gpcdf (x, -ones (1,6), ones (1,6), zeros (1,6), 'upper'), y3u, eps) %!assert_equal (gpcdf (x, -1, 1, zeros (1,6), 'upper'), y3u, eps) %!assert_equal (gpcdf (x, -1, ones (1,6), 0, 'upper'), y3u, eps) %!assert_equal (gpcdf (x, -ones (1,6), 1, 0, 'upper'), y3u, eps) %!assert_equal (gpcdf (x, -1, 1, 0, 'upper'), y3u, eps) %!assert_equal (gpcdf (x, -1, 1, [0, 0, 0, NaN, 0, 0], 'upper'), ... %! [y3u(1:3), NaN, y3u(5:6)], eps) %!assert_equal (gpcdf (x, -1, [1, 1, 1, NaN, 1, 1], 0, 'upper'), ... %! [y3u(1:3), NaN, y3u(5:6)], eps) %!assert_equal (gpcdf (x, [-1, -1, -1, NaN, -1, -1], 1, 0, 'upper'), ... %! [y3u(1:3), NaN, y3u(5:6)], eps) %!assert_equal (gpcdf ([x(1:3), NaN, x(5:6)], -1, 1, 0, 'upper'), ... %! [y3u(1:3), NaN, y3u(5:6)], eps) ## Test class of input preserved %!assert_equal (gpcdf (single ([x, NaN]), 0, 1, 0), single ([y1, NaN]), eps ('single')) %!assert_equal (gpcdf ([x, NaN], 0, 1, single (0)), single ([y1, NaN]), eps ('single')) %!assert_equal (gpcdf ([x, NaN], 0, single (1), 0), single ([y1, NaN]), eps ('single')) %!assert_equal (gpcdf ([x, NaN], single (0), 1, 0), single ([y1, NaN]), eps ('single')) %!assert_equal (gpcdf (single ([x, NaN]), 1, 1, 0), single ([y2, NaN]), eps ('single')) %!assert_equal (gpcdf ([x, NaN], 1, 1, single (0)), single ([y2, NaN]), eps ('single')) %!assert_equal (gpcdf ([x, NaN], 1, single (1), 0), single ([y2, NaN]), eps ('single')) %!assert_equal (gpcdf ([x, NaN], single (1), 1, 0), single ([y2, NaN]), eps ('single')) %!assert_equal (gpcdf (single ([x, NaN]), -1, 1, 0), single ([y3, NaN]), eps ('single')) %!assert_equal (gpcdf ([x, NaN], -1, 1, single (0)), single ([y3, NaN]), eps ('single')) %!assert_equal (gpcdf ([x, NaN], -1, single (1), 0), single ([y3, NaN]), eps ('single')) %!assert_equal (gpcdf ([x, NaN], single (-1), 1, 0), single ([y3, NaN]), eps ('single')) ## Test input validation %!error gpcdf () %!error gpcdf (1) %!error gpcdf (1, 2) %!error gpcdf (1, 2, 3) %!error gpcdf (1, 2, 3, 4, 'tail') %!error gpcdf (1, 2, 3, 4, 5) %!error ... %! gpcdf (ones (3), ones (2), ones (2), ones (2)) %!error ... %! gpcdf (ones (2), ones (3), ones (2), ones (2)) %!error ... %! gpcdf (ones (2), ones (2), ones (3), ones (2)) %!error ... %! gpcdf (ones (2), ones (2), ones (2), ones (3)) %!error gpcdf (int32 (2), 2, 3, 4) %!error gpcdf (true, 2, 3, 4) %!error gpcdf ('a', 2, 3, 4) %!error gpcdf (i, 2, 3, 4) %!error gpcdf (1, i, 3, 4) %!error gpcdf (1, 2, i, 4) %!error gpcdf (1, 2, 3, i) statistics-release-2.1.2/inst/Distribution_Functions/gpinv.m000066400000000000000000000231711527311104300243120ustar00rootroot00000000000000## Copyright (C) 1997-2015 Kurt Hornik ## Copyright (C) 2016 Dag Lyberg ## Copyright (C) 2018 John Donoghue ## Copyright (C) 2023-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} gpinv (@var{p}, @var{k}, @var{sigma}, @var{theta}) ## ## Inverse of the generalized Pareto cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the generalized Pareto distribution with shape parameter @var{k}, scale ## parameter @var{sigma}, and location parameter @var{theta}. The size of ## @var{x} is the common size of @var{p}, @var{k}, @var{sigma}, and @var{theta}. ## A scalar input functions as a constant matrix of the same size as the other ## inputs. ## ## When @qcode{@var{k} = 0} and @qcode{@var{theta} = 0}, the Generalized Pareto ## is equivalent to the exponential distribution. When @qcode{@var{k} > 0} and ## @code{@var{theta} = @var{sigma} / @var{k}} the Generalized Pareto is ## equivalent to the Pareto distribution. The mean of the Generalized Pareto is ## not finite when @qcode{@var{k} >= 1} and the variance is not finite when ## @qcode{@var{k} >= 1/2}. When @qcode{@var{k} >= 0}, the Generalized Pareto ## has positive density for @qcode{@var{x} > @var{theta}}, or, when ## @qcode{@var{theta} < 0}, for ## @qcode{0 <= (@var{x} - @var{theta}) / @var{sigma} <= -1 / @var{k}}. ## ## Further information about the generalized Pareto distribution can be found at ## @url{https://en.wikipedia.org/wiki/Generalized_Pareto_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{gpcdf, gppdf, gprnd, gpfit, gplike, gpstat} ## @end deftypefn function x = gpinv (p, k, sigma, theta) ## Check for valid number of input arguments if (nargin < 4) error ("gpinv: function called with too few input arguments."); endif ## Check for common size of P, K, SIGMA, and THETA [retval, p, k, sigma, theta] = common_size (p, k, sigma, theta); if (retval > 0) error ("gpinv: P, K, SIGMA, and THETA must be of common size or scalars."); endif ## Check for P, K, SIGMA, and THETA being double or single if (! (isfloat (p) && isfloat (k) && isfloat (sigma) && isfloat (theta))) error ("gpinv: P, K, SIGMA, and THETA must be double or single."); endif ## Check for P, K, SIGMA, and THETA being reals if (iscomplex (p) || iscomplex (k) || iscomplex (sigma) || iscomplex (theta)) error ("gpinv: P, K, SIGMA, and THETA must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (theta, 'single') ... || isa (sigma, 'single') || isa (k, 'single')) x = zeros (size (p), 'single'); else x = zeros (size (p)); endif ## Return NaNs for out of range values of sigma parameter kx = isnan (p) | ! (0 <= p) | ! (p <= 1) ... | ! (-Inf < theta) | ! (theta < Inf) ... | ! (sigma > 0) | ! (sigma < Inf) ... | ! (-Inf < k) | ! (k < Inf); x(kx) = NaN; kx = (0 <= p) & (p <= 1) & (-Inf < theta) & (theta < Inf) ... & (sigma > 0) & (sigma < Inf) & (-Inf < k) & (k < Inf); if (isscalar (theta) && isscalar (sigma) && isscalar (k)) if (k == 0) x(kx) = -log (1 - p(kx)); x(kx) = sigma * x(kx) + theta; elseif (k > 0) x(kx) = (1 - p(kx)).^(-k) - 1; x(kx) = (sigma / k) * x(kx) + theta; elseif (k < 0) x(kx) = (1 - p(kx)).^(-k) - 1; x(kx) = (sigma / k) * x(kx) + theta; endif else j = kx & (k == 0); if (any (j)) x(j) = -log (1 - p(j)); x(j) = sigma(j) .* x(j) + theta(j); endif j = kx & (k > 0); if (any (j)) x(j) = (1 - p(j)).^(-k(j)) - 1; x(j) = (sigma(j) ./ k(j)) .* x(j) + theta(j); endif j = kx & (k < 0); if (any (j)) x(j) = (1 - p(j)).^(-k(j)) - 1; x(j) = (sigma(j) ./ k(j)) .* x(j) + theta(j); endif endif endfunction %!demo %! ## Plot various iCDFs from the generalized Pareto distribution %! p = 0.001:0.001:0.999; %! x1 = gpinv (p, 1, 1, 0); %! x2 = gpinv (p, 5, 1, 0); %! x3 = gpinv (p, 20, 1, 0); %! x4 = gpinv (p, 1, 2, 0); %! x5 = gpinv (p, 5, 2, 0); %! x6 = gpinv (p, 20, 2, 0); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', ... %! p, x4, '-c', p, x5, '-m', p, x6, '-k') %! grid on %! ylim ([0, 5]) %! legend ({'k = 1, σ = 1, θ = 0', 'k = 5, σ = 1, θ = 0', ... %! 'k = 20, σ = 1, θ = 0', 'k = 1, σ = 2, θ = 0', ... %! 'k = 5, σ = 2, θ = 0', 'k = 20, σ = 2, θ = 0'}, ... %! 'location', 'southeast') %! title ('Generalized Pareto iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p, y1, y2, y3 %! p = [-1, 0, 1/2, 1, 2]; %! y1 = [NaN, 0, 0.6931471805599453, Inf, NaN]; %! y2 = [NaN, 0, 1, Inf, NaN]; %! y3 = [NaN, 0, 1/2, 1, NaN]; %!assert_equal (gpinv (p, zeros (1,5), ones (1,5), zeros (1,5)), y1) %!assert_equal (gpinv (p, 0, 1, zeros (1,5)), y1) %!assert_equal (gpinv (p, 0, ones (1,5), 0), y1) %!assert_equal (gpinv (p, zeros (1,5), 1, 0), y1) %!assert_equal (gpinv (p, 0, 1, 0), y1) %!assert_equal (gpinv (p, 0, 1, [0, 0, NaN, 0, 0]), [y1(1:2), NaN, y1(4:5)]) %!assert_equal (gpinv (p, 0, [1, 1, NaN, 1, 1], 0), [y1(1:2), NaN, y1(4:5)]) %!assert_equal (gpinv (p, [0, 0, NaN, 0, 0], 1, 0), [y1(1:2), NaN, y1(4:5)]) %!assert_equal (gpinv ([p(1:2), NaN, p(4:5)], 0, 1, 0), [y1(1:2), NaN, y1(4:5)]) %!assert_equal (gpinv (p, ones (1,5), ones (1,5), zeros (1,5)), y2) %!assert_equal (gpinv (p, 1, 1, zeros (1,5)), y2) %!assert_equal (gpinv (p, 1, ones (1,5), 0), y2) %!assert_equal (gpinv (p, ones (1,5), 1, 0), y2) %!assert_equal (gpinv (p, 1, 1, 0), y2) %!assert_equal (gpinv (p, 1, 1, [0, 0, NaN, 0, 0]), [y2(1:2), NaN, y2(4:5)]) %!assert_equal (gpinv (p, 1, [1, 1, NaN, 1, 1], 0), [y2(1:2), NaN, y2(4:5)]) %!assert_equal (gpinv (p, [1, 1, NaN, 1, 1], 1, 0), [y2(1:2), NaN, y2(4:5)]) %!assert_equal (gpinv ([p(1:2), NaN, p(4:5)], 1, 1, 0), [y2(1:2), NaN, y2(4:5)]) %!assert_equal (gpinv (p, -ones (1,5), ones (1,5), zeros (1,5)), y3) %!assert_equal (gpinv (p, -1, 1, zeros (1,5)), y3) %!assert_equal (gpinv (p, -1, ones (1,5), 0), y3) %!assert_equal (gpinv (p, -ones (1,5), 1, 0), y3) %!assert_equal (gpinv (p, -1, 1, 0), y3) %!assert_equal (gpinv (p, -1, 1, [0, 0, NaN, 0, 0]), [y3(1:2), NaN, y3(4:5)]) %!assert_equal (gpinv (p, -1, [1, 1, NaN, 1, 1], 0), [y3(1:2), NaN, y3(4:5)]) %!assert_equal (gpinv (p, -[1, 1, NaN, 1, 1], 1, 0), [y3(1:2), NaN, y3(4:5)]) %!assert_equal (gpinv ([p(1:2), NaN, p(4:5)], -1, 1, 0), [y3(1:2), NaN, y3(4:5)]) ## Test class of input preserved %!assert_equal (gpinv (single ([p, NaN]), 0, 1, 0), single ([y1, NaN])) %!assert_equal (gpinv ([p, NaN], 0, 1, single (0)), single ([y1, NaN])) %!assert_equal (gpinv ([p, NaN], 0, single (1), 0), single ([y1, NaN])) %!assert_equal (gpinv ([p, NaN], single (0), 1, 0), single ([y1, NaN])) %!assert_equal (gpinv (single ([p, NaN]), 1, 1, 0), single ([y2, NaN])) %!assert_equal (gpinv ([p, NaN], 1, 1, single (0)), single ([y2, NaN])) %!assert_equal (gpinv ([p, NaN], 1, single (1), 0), single ([y2, NaN])) %!assert_equal (gpinv ([p, NaN], single (1), 1, 0), single ([y2, NaN])) %!assert_equal (gpinv (single ([p, NaN]), -1, 1, 0), single ([y3, NaN])) %!assert_equal (gpinv ([p, NaN], -1, 1, single (0)), single ([y3, NaN])) %!assert_equal (gpinv ([p, NaN], -1, single (1), 0), single ([y3, NaN])) %!assert_equal (gpinv ([p, NaN], single (-1), 1, 0), single ([y3, NaN])) ## Test input validation %!error gpinv () %!error gpinv (1) %!error gpinv (1, 2) %!error gpinv (1, 2, 3) %!error ... %! gpinv (ones (3), ones (2), ones (2), ones (2)) %!error ... %! gpinv (ones (2), ones (3), ones (2), ones (2)) %!error ... %! gpinv (ones (2), ones (2), ones (3), ones (2)) %!error ... %! gpinv (ones (2), ones (2), ones (2), ones (3)) %!error gpinv (int32 (2), 2, 3, 4) %!error gpinv (true, 2, 3, 4) %!error gpinv ('a', 2, 3, 4) %!error gpinv (i, 2, 3, 4) %!error gpinv (1, i, 3, 4) %!error gpinv (1, 2, i, 4) %!error gpinv (1, 2, 3, i) statistics-release-2.1.2/inst/Distribution_Functions/gppdf.m000066400000000000000000000270061527311104300242700ustar00rootroot00000000000000## Copyright (C) 1997-2015 Kurt Hornik ## Copyright (C) 2016 Dag Lyberg ## Copyright (C) 2018 John Donoghue ## Copyright (C) 2023-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} gppdf (@var{x}, @var{k}, @var{sigma}, @var{theta}) ## ## Generalized Pareto probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the generalized Pareto distribution with shape parameter @var{k}, scale ## parameter @var{sigma}, and location parameter @var{theta}. The size of ## @var{y} is the common size of @var{p}, @var{k}, @var{sigma}, and @var{theta}. ## A scalar input functions as a constant matrix of the same size as the other ## inputs. ## ## When @qcode{@var{k} = 0} and @qcode{@var{theta} = 0}, the Generalized Pareto ## is equivalent to the exponential distribution. When @qcode{@var{k} > 0} and ## @code{@var{theta} = @var{sigma} / @var{k}} the Generalized Pareto is ## equivalent to the Pareto distribution. The mean of the Generalized Pareto is ## not finite when @qcode{@var{k} >= 1} and the variance is not finite when ## @qcode{@var{k} >= 1/2}. When @qcode{@var{k} >= 0}, the Generalized Pareto ## has positive density for @qcode{@var{x} > @var{theta}}, or, when ## @qcode{@var{theta} < 0}, for ## @qcode{0 <= (@var{x} - @var{theta}) / @var{sigma} <= -1 / @var{k}}. ## ## Further information about the generalized Pareto distribution can be found at ## @url{https://en.wikipedia.org/wiki/Generalized_Pareto_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## With a negative shape parameter the support is the closed interval ## @math{[@var{theta}, @var{theta} - @var{sigma}/@var{k}]}, and the density at ## its upper endpoint follows the limit of the density there: @math{0} for ## @math{-1 < @var{k} < 0}, @math{1/@var{sigma}} at @math{@var{k} = -1}, and ## unbounded for @math{@var{k} < -1}. MATLAB returns @math{0} at that endpoint ## whatever the shape, which contradicts its own @code{unifpdf}: the ## generalized Pareto with @math{@var{k} = -1} @emph{is} the uniform ## distribution on @math{[@var{theta}, @var{theta} + @var{sigma}]}, for which ## MATLAB's @code{unifpdf} returns @math{1/@var{sigma}} at the same point. This ## implementation returns the limit, and so agrees with @code{unifpdf}. ## ## @seealso{gpcdf, gpinv, gprnd, gpfit, gplike, gpstat} ## @end deftypefn function y = gppdf (x, k, sigma, theta) ## Check for valid number of input arguments if (nargin < 4) error ("gppdf: function called with too few input arguments."); endif ## Check for common size of X, K, SIGMA, and THETA if (! isscalar (x) || ! isscalar (k) || ! isscalar (sigma) || ! isscalar (theta)) [err, x, k, sigma, theta] = common_size (x, k, sigma, theta); if (err > 0) error ("gppdf: X, K, SIGMA, and THETA must be of common size or scalars."); endif endif ## Check for X, K, SIGMA, and THETA being double or single if (! (isfloat (x) && isfloat (k) && isfloat (sigma) && isfloat (theta))) error ("gppdf: X, K, SIGMA, and THETA must be double or single."); endif ## Check for X, K, SIGMA, and THETA being reals if (iscomplex (x) || iscomplex (k) || iscomplex (sigma) || iscomplex (theta)) error ("gppdf: X, K, SIGMA, and THETA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (theta, 'single') || isa (sigma, 'single') ... || isa (k, 'single')) y = zeros (size (x), 'single'); else y = zeros (size (x)); endif ## Return NaNs for out of range values of sigma parameter ky = isnan (x) | ! (-Inf < theta) | ! (theta < Inf) | ... ! (sigma > 0) | ! (sigma < Inf) | ... ! (-Inf < k) | ! (k < Inf); y(ky) = NaN; ky = (-Inf < x) & (x < Inf) & (-Inf < theta) & (theta < Inf) & ... (sigma > 0) & (sigma < Inf) & (-Inf < k) & (k < Inf); if (isscalar (theta) && isscalar (sigma) && isscalar (k)) z = (x - theta) / sigma; j = ky & (k == 0) & (z >= 0); if (any (j)) y(j) = exp (-z(j)); endif j = ky & (k > 0) & (z >= 0); if (any (j)) y(j) = (k * z(j) + 1) .^ (-(k + 1) / k) ./ sigma; endif if (k < 0) j = ky & (k < 0) & (0 <= z) & (z <= -1. / k); if (any (j)) y(j) = (k * z(j) + 1) .^ (-(k + 1) / k) ./ sigma; endif endif else z = (x - theta) ./ sigma; j = ky & (k == 0) & (z >= 0); if (any (j)) y(j) = exp ( -z(j)); endif j = ky & (k > 0) & (z >= 0); if (any (j)) y(j) = (k(j) .* z(j) + 1) .^ (-(k(j) + 1) ./ k(j)) ... ./ sigma(j); endif if (any (k < 0)) j = ky & (k < 0) & (0 <= z) & (z <= -1 ./ k); if (any (j)) y(j) = (k(j) .* z(j) + 1) .^ (-(k(j) + 1) ./ k(j)) ... ./ sigma(j); endif endif endif endfunction %!demo %! ## Plot various PDFs from the generalized Pareto distribution %! x = 0:0.001:5; %! y1 = gppdf (x, 1, 1, 0); %! y2 = gppdf (x, 5, 1, 0); %! y3 = gppdf (x, 20, 1, 0); %! y4 = gppdf (x, 1, 2, 0); %! y5 = gppdf (x, 5, 2, 0); %! y6 = gppdf (x, 20, 2, 0); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', ... %! x, y4, '-c', x, y5, '-m', x, y6, '-k') %! grid on %! xlim ([0, 5]) %! ylim ([0, 1]) %! legend ({'k = 1, σ = 1, θ = 0', 'k = 5, σ = 1, θ = 0', ... %! 'k = 20, σ = 1, θ = 0', 'k = 1, σ = 2, θ = 0', ... %! 'k = 5, σ = 2, θ = 0', 'k = 20, σ = 2, θ = 0'}, ... %! 'location', 'northeast') %! title ('Generalized Pareto PDF') %! xlabel ('values in x') %! ylabel ('density') ## The upper endpoint of the support, measured against R2024a 2026-08-17, which ## returns 0 for all three shapes and so disagrees with its own unifpdf. %!test %! ## at k = -1 the distribution is uniform, and the density is 1/sigma %! assert_equal (gppdf (1, -1, 1, 0), 1); %! assert_equal (gppdf (1, -1, 1, 0), unifpdf (1, 0, 1)); %! assert_equal (gppdf (2, -1, 2, 0), 0.5); %! assert_equal (gppdf (2, -1, 2, 0), unifpdf (2, 0, 2)); %!test %! ## between -1 and 0 the density vanishes at the endpoint %! assert_equal (gppdf (2, -0.5, 1, 0), 0); %!test %! ## below -1 it diverges there %! assert_equal (gppdf (0.5, -2, 1, 0), Inf); %!test %! ## and the endpoint agrees with the values approaching it %! assert_equal (gppdf (1 - 1e-12, -1, 1, 0), 1); %! assert_equal (gppdf (1.5, -1, 1, 0), 0); ## Test output %!shared x, y1, y2, y3 %! x = [-Inf, -1, 0, 1/2, 1, Inf]; %! y1 = [0, 0, 1, 0.6065306597126334, 0.36787944117144233, 0]; %! y2 = [0, 0, 1, 4/9, 1/4, 0]; %! y3 = [0, 0, 1, 1, 1, 0]; %!assert_equal (gppdf (x, zeros (1,6), ones (1,6), zeros (1,6)), y1, eps) %!assert_equal (gppdf (x, 0, 1, zeros (1,6)), y1, eps) %!assert_equal (gppdf (x, 0, ones (1,6), 0), y1, eps) %!assert_equal (gppdf (x, zeros (1,6), 1, 0), y1, eps) %!assert_equal (gppdf (x, 0, 1, 0), y1, eps) %!assert_equal (gppdf (x, 0, 1, [0, 0, 0, NaN, 0, 0]), [y1(1:3), NaN, y1(5:6)]) %!assert_equal (gppdf (x, 0, [1, 1, 1, NaN, 1, 1], 0), [y1(1:3), NaN, y1(5:6)]) %!assert_equal (gppdf (x, [0, 0, 0, NaN, 0, 0], 1, 0), [y1(1:3), NaN, y1(5:6)]) %!assert_equal (gppdf ([x(1:3), NaN, x(5:6)], 0, 1, 0), [y1(1:3), NaN, y1(5:6)]) %!assert_equal (gppdf (x, ones (1,6), ones (1,6), zeros (1,6)), y2, eps) %!assert_equal (gppdf (x, 1, 1, zeros (1,6)), y2, eps) %!assert_equal (gppdf (x, 1, ones (1,6), 0), y2, eps) %!assert_equal (gppdf (x, ones (1,6), 1, 0), y2, eps) %!assert_equal (gppdf (x, 1, 1, 0), y2, eps) %!assert_equal (gppdf (x, 1, 1, [0, 0, 0, NaN, 0, 0]), [y2(1:3), NaN, y2(5:6)]) %!assert_equal (gppdf (x, 1, [1, 1, 1, NaN, 1, 1], 0), [y2(1:3), NaN, y2(5:6)]) %!assert_equal (gppdf (x, [1, 1, 1, NaN, 1, 1], 1, 0), [y2(1:3), NaN, y2(5:6)]) %!assert_equal (gppdf ([x(1:3), NaN, x(5:6)], 1, 1, 0), [y2(1:3), NaN, y2(5:6)]) %!assert_equal (gppdf (x, -ones (1,6), ones (1,6), zeros (1,6)), y3, eps) %!assert_equal (gppdf (x, -1, 1, zeros (1,6)), y3, eps) %!assert_equal (gppdf (x, -1, ones (1,6), 0), y3, eps) %!assert_equal (gppdf (x, -ones (1,6), 1, 0), y3, eps) %!assert_equal (gppdf (x, -1, 1, 0), y3, eps) %!assert_equal (gppdf (x, -1, 1, [0, 0, 0, NaN, 0, 0]), [y3(1:3), NaN, y3(5:6)]) %!assert_equal (gppdf (x, -1, [1, 1, 1, NaN, 1, 1], 0), [y3(1:3), NaN, y3(5:6)]) %!assert_equal (gppdf (x, [-1, -1, -1, NaN, -1, -1], 1, 0), [y3(1:3), NaN, y3(5:6)]) %!assert_equal (gppdf ([x(1:3), NaN, x(5:6)], -1, 1, 0), [y3(1:3), NaN, y3(5:6)]) ## Test class of input preserved %!assert_equal (gppdf (single ([x, NaN]), 0, 1, 0), single ([y1, NaN])) %!assert_equal (gppdf ([x, NaN], 0, 1, single (0)), single ([y1, NaN])) %!assert_equal (gppdf ([x, NaN], 0, single (1), 0), single ([y1, NaN])) %!assert_equal (gppdf ([x, NaN], single (0), 1, 0), single ([y1, NaN])) %!assert_equal (gppdf (single ([x, NaN]), 1, 1, 0), single ([y2, NaN])) %!assert_equal (gppdf ([x, NaN], 1, 1, single (0)), single ([y2, NaN])) %!assert_equal (gppdf ([x, NaN], 1, single (1), 0), single ([y2, NaN])) %!assert_equal (gppdf ([x, NaN], single (1), 1, 0), single ([y2, NaN])) %!assert_equal (gppdf (single ([x, NaN]), -1, 1, 0), single ([y3, NaN])) %!assert_equal (gppdf ([x, NaN], -1, 1, single (0)), single ([y3, NaN])) %!assert_equal (gppdf ([x, NaN], -1, single (1), 0), single ([y3, NaN])) %!assert_equal (gppdf ([x, NaN], single (-1), 1, 0), single ([y3, NaN])) ## Test input validation %!error gppdf (int32 (2), 1, 1, 0) %!error gppdf (true, 1, 1, 0) %!error gppdf ('a', 1, 1, 0) %!error gpcdf () %!error gpcdf (1) %!error gpcdf (1, 2) %!error gpcdf (1, 2, 3) %!error ... %! gpcdf (ones (3), ones (2), ones (2), ones (2)) %!error ... %! gpcdf (ones (2), ones (3), ones (2), ones (2)) %!error ... %! gpcdf (ones (2), ones (2), ones (3), ones (2)) %!error ... %! gpcdf (ones (2), ones (2), ones (2), ones (3)) %!error gpcdf (i, 2, 3, 4) %!error gpcdf (1, i, 3, 4) %!error gpcdf (1, 2, i, 4) %!error gpcdf (1, 2, 3, i) statistics-release-2.1.2/inst/Distribution_Functions/gprnd.m000066400000000000000000000225421527311104300243020ustar00rootroot00000000000000## Copyright (C) 1995-2015 Kurt Hornik ## Copyright (C) 2016 Dag Lyberg ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} gprnd (@var{k}, @var{sigma}, @var{theta}) ## @deftypefnx {statistics} {@var{r} =} gprnd (@var{k}, @var{sigma}, @var{theta}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} gprnd (@var{k}, @var{sigma}, @var{theta}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} gprnd (@var{k}, @var{sigma}, @var{theta}, [@var{sz}]) ## ## Random arrays from the generalized Pareto distribution. ## ## @code{@var{r} = gprnd (@var{k}, @var{sigma}, @var{theta})} returns an array ## of random numbers chosen from the generalized Pareto distribution with shape ## parameter @var{k}, scale parameter @var{sigma}, and location parameter ## @var{theta}. The size of @var{r} is the common size of @var{k}, @var{sigma}, ## and @var{theta}. A scalar input functions as a constant matrix of the same ## size as the other inputs. ## ## When called with a single size argument, @code{gprnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## When @qcode{@var{k} = 0} and @qcode{@var{theta} = 0}, the Generalized Pareto ## is equivalent to the exponential distribution. When @qcode{@var{k} > 0} and ## @code{@var{theta} = @var{sigma} / @var{k}} the Generalized Pareto is ## equivalent to the Pareto distribution. The mean of the Generalized Pareto is ## not finite when @qcode{@var{k} >= 1} and the variance is not finite when ## @qcode{@var{k} >= 1/2}. When @qcode{@var{k} >= 0}, the Generalized Pareto ## has positive density for @qcode{@var{x} > @var{theta}}, or, when ## @qcode{@var{theta} < 0}, for ## @qcode{0 <= (@var{x} - @var{theta}) / @var{sigma} <= -1 / @var{k}}. ## ## Further information about the generalized Pareto distribution can be found at ## @url{https://en.wikipedia.org/wiki/Generalized_Pareto_distribution} ## ## @seealso{gpcdf, gpinv, gppdf, gpfit, gplike, gpstat} ## @end deftypefn function r = gprnd (k, sigma, theta, varargin) ## Check for valid number of input arguments if (nargin < 3) error ("gprnd: function called with too few input arguments."); endif ## Check for common size of K, SIGMA, and THETA if (! isscalar (k) || ! isscalar (sigma) || ! isscalar (theta)) [retval, k, sigma, theta] = common_size (k, sigma, theta); if (retval > 0) error ("gprnd: K, SIGMA, and THETA must be of common size or scalars."); endif endif ## Check for K, SIGMA, and THETA being reals if (iscomplex (k) || iscomplex (sigma) || iscomplex (theta)) error ("gprnd: K, SIGMA, and THETA must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 3) sz = size (k); elseif (nargin == 4) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("gprnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 4) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("gprnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (k) && ! isequal (size (k), size (ones (sz)))) error ("gprnd: K, SIGMA, and THETA must be scalars or of size SZ."); endif ## Check for class type if (isa (k, 'single') || isa (sigma, 'single') || isa (theta, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from generalized Pareto distribution r = rand (sz, cls); ## Find valid parameters vr = (isfinite (r)) & (theta > -Inf) & (theta < Inf) ... & (sigma > 0) & (sigma < Inf) ... & (-Inf < k) & (k < Inf); ## Force invalid parameters to NaN r(! vr) = NaN; if (isscalar (k)) if (k == 0) r(vr) = theta - (sigma .* log (1 - r(vr))); else r(vr) = theta + ((sigma .* ((r(vr) .^ -k) - 1)) ./ k); endif else if (any (k == 0)) r(vr) = theta(vr) - (sigma(vr) .* log (1 - r(vr))); endif if (any (k < 0 | k > 0)) r(vr) = theta(vr) + ((sigma(vr) .* ((r(vr) .^ -k(vr)) - 1)) ./ k(vr)); endif endif endfunction ## Test output %!assert_equal (size (gprnd (0, 1, 0)), [1, 1]) %!assert_equal (size (gprnd (0, 1, zeros (2,1))), [2, 1]) %!assert_equal (size (gprnd (0, 1, zeros (2,2))), [2, 2]) %!assert_equal (size (gprnd (0, ones (2,1), 0)), [2, 1]) %!assert_equal (size (gprnd (0, ones (2,2), 0)), [2, 2]) %!assert_equal (size (gprnd (zeros (2,1), 1, 0)), [2, 1]) %!assert_equal (size (gprnd (zeros (2,2), 1, 0)), [2, 2]) %!assert_equal (size (gprnd (0, 1, 0, 3)), [3, 3]) %!assert_equal (size (gprnd (0, 1, 0, [4 1])), [4, 1]) %!assert_equal (size (gprnd (0, 1, 0, 4, 1)), [4, 1]) %!assert_equal (size (gprnd (1,1,0)), [1, 1]) %!assert_equal (size (gprnd (1, 1, zeros (2,1))), [2, 1]) %!assert_equal (size (gprnd (1, 1, zeros (2,2))), [2, 2]) %!assert_equal (size (gprnd (1, ones (2,1), 0)), [2, 1]) %!assert_equal (size (gprnd (1, ones (2,2), 0)), [2, 2]) %!assert_equal (size (gprnd (ones (2,1), 1, 0)), [2, 1]) %!assert_equal (size (gprnd (ones (2,2), 1, 0)), [2, 2]) %!assert_equal (size (gprnd (1, 1, 0, 3)), [3, 3]) %!assert_equal (size (gprnd (1, 1, 0, [4 1])), [4, 1]) %!assert_equal (size (gprnd (1, 1, 0, 4, 1)), [4, 1]) %!assert_equal (size (gprnd (-1, 1, 0)), [1, 1]) %!assert_equal (size (gprnd (-1, 1, zeros (2,1))), [2, 1]) %!assert_equal (size (gprnd (1, -1, zeros (2,2))), [2, 2]) %!assert_equal (size (gprnd (-1, ones (2,1), 0)), [2, 1]) %!assert_equal (size (gprnd (-1, ones (2,2), 0)), [2, 2]) %!assert_equal (size (gprnd (-ones (2,1), 1, 0)), [2, 1]) %!assert_equal (size (gprnd (-ones (2,2), 1, 0)), [2, 2]) %!assert_equal (size (gprnd (-1, 1, 0, 3)), [3, 3]) %!assert_equal (size (gprnd (-1, 1, 0, [4, 1])), [4, 1]) %!assert_equal (size (gprnd (-1, 1, 0, 4, 1)), [4, 1]) %!assert_equal (size (gprnd (-1, 1, 0, [])), [0, 0]) %!assert_equal (size (gprnd (-1, 1, 0, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (gprnd (1, 2, 3, -1)), [0, 0]) %!assert_equal (size (gprnd (1, 2, 3, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (gprnd (1, 2, 3, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (gprnd (0, 1, 0)), "double") %!assert_equal (class (gprnd (0, 1, single (0))), "single") %!assert_equal (class (gprnd (0, 1, single ([0, 0]))), "single") %!assert_equal (class (gprnd (0, single (1),0)), "single") %!assert_equal (class (gprnd (0, single ([1, 1]),0)), "single") %!assert_equal (class (gprnd (single (0), 1, 0)), "single") %!assert_equal (class (gprnd (single ([0, 0]), 1, 0)), "single") ## Test input validation %!error gprnd () %!error gprnd (1) %!error gprnd (1, 2) %!error ... %! gprnd (ones (3), ones (2), ones (2)) %!error ... %! gprnd (ones (2), ones (3), ones (2)) %!error ... %! gprnd (ones (2), ones (2), ones (3)) %!error gprnd (i, 2, 3) %!error gprnd (1, i, 3) %!error gprnd (1, 2, i) %!error ... %! gprnd (1, 2, 3, 1.2) %!error ... %! gprnd (1, 2, 3, ones (2)) %!error ... %! gprnd (1, 2, 3, [2 0 2.5]) %!error ... %! gprnd (1, 2, 3, 2, 1.5, 5) %!error ... %! gprnd (2, ones (2), 2, 3) %!error ... %! gprnd (2, ones (2), 2, [3, 2]) %!error ... %! gprnd (2, ones (2), 2, 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/gumbelcdf.m000066400000000000000000000236501527311104300251210ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} gumbelcdf (@var{x}) ## @deftypefnx {statistics} {@var{p} =} gumbelcdf (@var{x}, @var{mu}) ## @deftypefnx {statistics} {@var{p} =} gumbelcdf (@var{x}, @var{mu}, @var{beta}) ## @deftypefnx {statistics} {@var{p} =} gumbelcdf (@dots{}, @qcode{'upper'}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} gumbelcdf (@var{x}, @var{mu}, @var{beta}, @var{pcov}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} gumbelcdf (@var{x}, @var{mu}, @var{beta}, @var{pcov}, @var{alpha}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} gumbelcdf (@dots{}, @qcode{'upper'}) ## ## Gumbel cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the Gumbel distribution (also known as the extreme value or the type ## I generalized extreme value distribution) with location parameter @var{mu} ## and scale parameter @var{beta}. The size of @var{p} is the common size of ## @var{x}, @var{mu} and @var{beta}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## Default values are @var{mu} = 0 and @var{beta} = 1. ## ## When called with three output arguments, i.e. @code{[@var{p}, @var{plo}, ## @var{pup}]}, @code{gumbelcdf} computes the confidence bounds for @var{p} when ## the input parameters @var{mu} and @var{beta} are estimates. In such case, ## @var{pcov}, a @math{2*2} matrix containing the covariance matrix of the ## estimated parameters, is necessary. Optionally, @var{alpha}, which has a ## default value of 0.05, specifies the @qcode{100 * (1 - @var{alpha})} percent ## confidence bounds. @var{plo} and @var{pup} are arrays of the same size as ## @var{p} containing the lower and upper confidence bounds. ## ## @code{[@dots{}] = gumbelcdf (@dots{}, "upper")} computes the upper tail ## probability of the Gumbel distribution with parameters @var{mu} and ## @var{beta}, at the values in @var{x}. ## ## The Gumbel distribution is used to model the distribution of the maximum (or ## the minimum) of a number of samples of various distributions. This version ## is suitable for modeling maxima. For modeling minima, use the alternative ## extreme value CDF, @code{evcdf}. ## ## @code{[@dots{}] = gumbelcdf (@dots{}, "upper")} computes the upper tail ## probability of the extreme value (Gumbel) distribution. ## ## Further information about the Gumbel distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gumbel_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{gumbelinv, gumbelpdf, gumbelrnd, gumbelfit, gumbellike, gumbelstat, ## evcdf} ## @end deftypefn function [varargout] = gumbelcdf (x, varargin) ## Check for valid number of input arguments if (nargin < 1 || nargin > 6) error ("gumbelcdf: invalid number of input arguments."); endif ## Check for 'upper' flag if (nargin > 1 && strcmpi (varargin{end}, 'upper')) uflag = true; varargin(end) = []; elseif (nargin > 1 && ischar (varargin{end}) && ... ! strcmpi (varargin{end}, 'upper')) error ("gumbelcdf: invalid argument for upper tail."); else uflag = false; endif ## Get extra arguments (if they exist) or add defaults if (numel (varargin) > 0) mu = varargin{1}; else mu = 0; endif if (numel (varargin) > 1) beta = varargin{2}; else beta = 1; endif if (numel (varargin) > 2) pcov = varargin{3}; ## Check for valid covariance matrix 2x2 if (! isequal (size (pcov), [2, 2])) error ("gumbelcdf: invalid size of covariance matrix."); endif else ## Check that cov matrix is provided if 3 output arguments are requested if (nargout > 1) error ("gumbelcdf: covariance matrix is required for confidence bounds."); endif pcov = []; endif if (numel (varargin) > 3) alpha = varargin{4}; ## Check for valid alpha value if (! isnumeric (alpha) || numel (alpha) !=1 || alpha <= 0 || alpha >= 1) error ("gumbelcdf: invalid value for alpha."); endif else alpha = 0.05; endif ## Check for common size of X, MU, and BETA if (! isscalar (x) || ! isscalar (mu) || ! isscalar (beta)) [err, x, mu, beta] = common_size (x, mu, beta); if (err > 0) error ("gumbelcdf: X, MU, and BETA must be of common size or scalars."); endif endif ## Check for X, MU, and BETA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (beta))) error ("gumbelcdf: X, MU, and BETA must be double or single."); endif ## Check for X, MU, and BETA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (beta)) error ("gumbelcdf: X, MU, and BETA must not be complex."); endif ## Return NaNs for out of range parameters. beta(beta <= 0) = NaN; ## Compute extreme value cdf z = (x - mu) ./ beta; if (uflag) p = -expm1 (-exp (-z)); else p = exp (-exp (-z)); endif ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (beta, 'single')); is_class = 'single'; else is_class = 'double'; endif ## Prepare output varargout{1} = cast (p, is_class); if (nargout > 1) plo = NaN (size (z), is_class); pup = NaN (size (z), is_class); endif ## Check beta if (isscalar (beta)) if (beta > 0) sigma_p = true (size (z)); else if (nargout == 3) varargout{2} = plo; varargout{3} = pup; endif return; endif else sigma_p = beta > 0; endif ## Compute confidence bounds (if requested) if (nargout >= 2) zvar = (pcov(1,1) + 2 * pcov(1,2) * z(sigma_p) + ... pcov(2,2) * z(sigma_p) .^ 2) ./ (beta .^ 2); if (any (zvar < 0)) error ("gumbelcdf: bad covariance matrix."); endif normz = -norminv (alpha / 2); halfwidth = normz * sqrt (zvar); zlo = z(sigma_p) - halfwidth; zup = z(sigma_p) + halfwidth; if (uflag) plo(sigma_p) = -expm1 (-exp (-zup)); pup(sigma_p) = -expm1 (-exp (-zlo)); else plo(sigma_p) = exp (-exp (-zlo)); pup(sigma_p) = exp (-exp (-zup)); endif varargout{2} = plo; varargout{3} = pup; endif endfunction %!demo %! ## Plot various CDFs from the Gumbel distribution %! x = -5:0.01:20; %! p1 = gumbelcdf (x, 0.5, 2); %! p2 = gumbelcdf (x, 1.0, 2); %! p3 = gumbelcdf (x, 1.5, 3); %! p4 = gumbelcdf (x, 3.0, 4); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', x, p4, '-c') %! grid on %! legend ({'μ = 0.5, β = 2', 'μ = 1.0, β = 2', ... %! 'μ = 1.5, β = 3', 'μ = 3.0, β = 4'}, 'location', 'southeast') %! title ('Gumbel CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, y %! x = [-Inf, 1, 2, Inf]; %! y = [0, 0.3679, 0.6922, 1]; %!assert_equal (gumbelcdf (x, ones (1,4), ones (1,4)), y, 1e-4) %!assert_equal (gumbelcdf (x, 1, ones (1,4)), y, 1e-4) %!assert_equal (gumbelcdf (x, ones (1,4), 1), y, 1e-4) %!assert_equal (gumbelcdf (x, [0, -Inf, NaN, Inf], 1), [0, 1, NaN, NaN], 1e-4) %!assert_equal (gumbelcdf (x, 1, [Inf, NaN, -1, 0]), [NaN, NaN, NaN, NaN], 1e-4) %!assert_equal (gumbelcdf ([x(1:2), NaN, x(4)], 1, 1), [y(1:2), NaN, y(4)], 1e-4) %!assert_equal (gumbelcdf (x, 'upper'), [1, 0.3078, 0.1266, 0], 1e-4) ## Test class of input preserved %!assert_equal (gumbelcdf ([x, NaN], 1, 1), [y, NaN], 1e-4) %!assert_equal (gumbelcdf (single ([x, NaN]), 1, 1), single ([y, NaN]), 1e-4) %!assert_equal (gumbelcdf ([x, NaN], single (1), 1), single ([y, NaN]), 1e-4) %!assert_equal (gumbelcdf ([x, NaN], 1, single (1)), single ([y, NaN]), 1e-4) ## Test input validation %!error gumbelcdf () %!error gumbelcdf (1,2,3,4,5,6,7) %!error gumbelcdf (1, 2, 3, 4, 'uper') %!error ... %! gumbelcdf (ones (3), ones (2), ones (2)) %!error gumbelcdf (2, 3, 4, [1, 2]) %!error ... %! [p, plo, pup] = gumbelcdf (1, 2, 3) %!error [p, plo, pup] = ... %! gumbelcdf (1, 2, 3, [1, 0; 0, 1], 0) %!error [p, plo, pup] = ... %! gumbelcdf (1, 2, 3, [1, 0; 0, 1], 1.22) %!error [p, plo, pup] = ... %! gumbelcdf (1, 2, 3, [1, 0; 0, 1], 'alpha', 'upper') %!error gumbelcdf (int32 (2), 2, 2) %!error gumbelcdf (true, 2, 2) %!error gumbelcdf ('a', 2, 2) %!error gumbelcdf (i, 2, 2) %!error gumbelcdf (2, i, 2) %!error gumbelcdf (2, 2, i) %!error ... %! [p, plo, pup] = gumbelcdf (1, 2, 3, [1, 0; 0, -inf], 0.04) statistics-release-2.1.2/inst/Distribution_Functions/gumbelinv.m000066400000000000000000000207311527311104300251560ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} gumbelinv (@var{p}) ## @deftypefnx {statistics} {@var{x} =} gumbelinv (@var{p}, @var{mu}) ## @deftypefnx {statistics} {@var{x} =} gumbelinv (@var{p}, @var{mu}, @var{beta}) ## @deftypefnx {statistics} {[@var{x}, @var{xlo}, @var{xup}] =} gumbelinv (@var{p}, @var{mu}, @var{beta}, @var{pcov}) ## @deftypefnx {statistics} {[@var{x}, @var{xlo}, @var{xup}] =} gumbelinv (@var{p}, @var{mu}, @var{beta}, @var{pcov}, @var{alpha}) ## ## Inverse of the Gumbel cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the Gumbel distribution (also known as the extreme value or the type I ## generalized extreme value distribution) with location parameter @var{mu} and ## scale parameter @var{beta}. The size of @var{x} is the common size of ## @var{p}, @var{mu} and @var{beta}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## Default values are @var{mu} = 0 and @var{beta} = 1. ## ## When called with three output arguments, i.e. @qcode{[@var{x}, @var{xlo}, ## @var{xup}]}, @code{gumbelinv} computes the confidence bounds for @var{x} when ## the input parameters @var{mu} and @var{beta} are estimates. In such case, ## @var{pcov}, a @math{2*2} matrix containing the covariance matrix of the ## estimated parameters, is necessary. Optionally, @var{alpha}, which has a ## default value of 0.05, specifies the @qcode{100 * (1 - @var{alpha})} percent ## confidence bounds. @var{xlo} and @var{xup} are arrays of the same size as ## @var{x} containing the lower and upper confidence bounds. ## ## The Gumbel distribution is used to model the distribution of the maximum (or ## the minimum) of a number of samples of various distributions. This version ## is suitable for modeling maxima. For modeling minima, use the alternative ## extreme value iCDF, @code{evinv}. ## ## Further information about the Gumbel distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gumbel_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{gumbelcdf, gumbelpdf, gumbelrnd, gumbelfit, gumbellike, gumbelstat, ## evinv} ## @end deftypefn function [x, xlo, xup] = gumbelinv (p, mu, beta, pcov, alpha) ## Check for valid number of input arguments if (nargin < 1 || nargin > 5) error ("gumbelinv: invalid number of input arguments."); endif ## Add defaults (if missing input arguments) if (nargin < 2) mu = 0; endif if (nargin < 3) beta = 1; endif ## Check if PCOV is provided when confidence bounds are requested if (nargout > 2) if (nargin < 4) error ("gumbelinv: covariance matrix is required for confidence bounds."); endif ## Check for valid covariance matrix 2x2 if (! isequal (size (pcov), [2, 2])) error ("gumbelinv: invalid size of covariance matrix."); endif ## Check for valid alpha value if (nargin < 5) alpha = 0.05; elseif (! isnumeric (alpha) || numel (alpha) !=1 || alpha <= 0 || alpha >= 1) error ("gumbelinv: invalid value for alpha."); endif endif ## Check for common size of P, MU, and BETA if (! isscalar (p) || ! isscalar (mu) || ! isscalar (beta)) [err, p, mu, beta] = common_size (p, mu, beta); if (err > 0) error ("gumbelinv: P, MU, and BETA must be of common size or scalars."); endif endif ## Check for P, MU, and BETA being double or single if (! (isfloat (p) && isfloat (mu) && isfloat (beta))) error ("gumbelinv: P, MU, and BETA must be double or single."); endif ## Check for P, MU, and BETA being reals if (iscomplex (p) || iscomplex (mu) || iscomplex (beta)) error ("gumbelinv: P, MU, and BETA must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (mu, 'single') || isa (beta, 'single')); is_class = 'single'; else is_class = 'double'; endif ## Compute inverse of type 1 extreme value cdf k = (eps <= p & p < 1); if (all (k(:))) q = -log (-log (p)); else q = zeros (size (p), is_class); q(k) = -log (-log (p(k))); ## Return -Inf for p = 0 and Inf for p = 1 q(p < eps) = -Inf; q(p == 1) = Inf; ## Return NaN for out of range values of P q(p < 0 | 1 < p | isnan (p)) = NaN; endif ## Return NaN for out of range values of BETA beta(beta <= 0) = NaN; x = (beta .* q) + mu; ## Compute confidence bounds if requested. if (nargout >= 2) xvar = pcov(1,1) + 2 * pcov(1,2) * q + pcov(2,2) * q .^ 2; if (any (xvar < 0)) || any (isnan (xvar)) error ("gumbelinv: bad covariance matrix."); endif z = -norminv (alpha / 2); halfwidth = z * sqrt (xvar); xlo = x - halfwidth; xup = x + halfwidth; endif endfunction %!demo %! ## Plot various iCDFs from the Gumbel distribution %! p = 0.001:0.001:0.999; %! x1 = gumbelinv (p, 0.5, 2); %! x2 = gumbelinv (p, 1.0, 2); %! x3 = gumbelinv (p, 1.5, 3); %! x4 = gumbelinv (p, 3.0, 4); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', p, x4, '-c') %! grid on %! ylim ([-5, 20]) %! legend ({'μ = 0.5, β = 2', 'μ = 1.0, β = 2', ... %! 'μ = 1.5, β = 3', 'μ = 3.0, β = 4'}, 'location', 'northwest') %! title ('Gumbel iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p, x %! p = [0, 0.05, 0.5 0.95]; %! x = [-Inf, -1.0972, 0.3665, 2.9702]; %!assert_equal (gumbelinv (p), x, 1e-4) %!assert_equal (gumbelinv (p, zeros (1,4), ones (1,4)), x, 1e-4) %!assert_equal (gumbelinv (p, 0, ones (1,4)), x, 1e-4) %!assert_equal (gumbelinv (p, zeros (1,4), 1), x, 1e-4) %!assert_equal (gumbelinv (p, [0, -Inf, NaN, Inf], 1), [-Inf, -Inf, NaN, Inf], 1e-4) %!assert_equal (gumbelinv (p, 0, [Inf, NaN, -1, 0]), [-Inf, NaN, NaN, NaN], 1e-4) %!assert_equal (gumbelinv ([p(1:2), NaN, p(4)], 0, 1), [x(1:2), NaN, x(4)], 1e-4) ## Test class of input preserved %!assert_equal (gumbelinv ([p, NaN], 0, 1), [x, NaN], 1e-4) %!assert_equal (gumbelinv (single ([p, NaN]), 0, 1), single ([x, NaN]), 1e-4) %!assert_equal (gumbelinv ([p, NaN], single (0), 1), single ([x, NaN]), 1e-4) %!assert_equal (gumbelinv ([p, NaN], 0, single (1)), single ([x, NaN]), 1e-4) ## Test whether gumbelcdf is successfully inverted %! p = [0.05, 0.5, 0.95]; %! x = gumbelinv(p); %!assert_equal (gumbelcdf (x), p, 1e-4) ## Test input validation %!error gumbelinv () %!error gumbelinv (1,2,3,4,5,6) %!error ... %! gumbelinv (ones (3), ones (2), ones (2)) %!error ... %! [p, plo, pup] = gumbelinv (2, 3, 4, [1, 2]) %!error ... %! [p, plo, pup] = gumbelinv (1, 2, 3) %!error [p, plo, pup] = ... %! gumbelinv (1, 2, 3, [1, 0; 0, 1], 0) %!error [p, plo, pup] = ... %! gumbelinv (1, 2, 3, [1, 0; 0, 1], 1.22) %!error gumbelinv (int32 (2), 2, 2) %!error gumbelinv (true, 2, 2) %!error gumbelinv ('a', 2, 2) %!error gumbelinv (i, 2, 2) %!error gumbelinv (2, i, 2) %!error gumbelinv (2, 2, i) %!error ... %! [p, plo, pup] = gumbelinv (1, 2, 3, [-1, 10; -Inf, -Inf], 0.04) statistics-release-2.1.2/inst/Distribution_Functions/gumbelpdf.m000066400000000000000000000120261527311104300251310ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} gumbelpdf (@var{x}) ## @deftypefnx {statistics} {@var{y} =} gumbelpdf (@var{x}, @var{mu}) ## @deftypefnx {statistics} {@var{y} =} gumbelpdf (@var{x}, @var{mu}, @var{beta}) ## ## Gumbel probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the Gumbel distribution (also known as the extreme value or the type I ## generalized extreme value distribution) with location parameter @var{mu} and ## scale parameter @var{beta}. The size of @var{y} is the common size of ## @var{x}, @var{mu} and @var{beta}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## Default values are @var{mu} = 0 and @var{beta} = 1. ## ## The Gumbel distribution is used to model the distribution of the maximum (or ## the minimum) of a number of samples of various distributions. This version ## is suitable for modeling maxima. For modeling minima, use the alternative ## extreme value iCDF, @code{evpdf}. ## ## Further information about the Gumbel distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gumbel_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{gumbelcdf, gumbelinv, gumbelrnd, gumbelfit, gumbellike, gumbelstat, ## evpdf} ## @end deftypefn function y = gumbelpdf (x, mu, beta) ## Check for valid number of input arguments if (nargin < 1) error ("gumbelpdf: too few input arguments."); endif ## Add defaults (if missing input arguments) if (nargin < 2) mu = 0; endif if (nargin < 3) beta = 1; endif ## Check for common size of X, MU, and BETA if (! isscalar (x) || ! isscalar (mu) || ! isscalar (beta)) [err, x, mu, beta] = common_size (x, mu, beta); if (err > 0) error ("gumbelpdf: X, MU, and BETA must be of common size or scalars."); endif endif ## Check for X, MU, and BETA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (beta))) error ("gumbelpdf: X, MU, and BETA must be double or single."); endif ## Check for X, MU, and BETA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (beta)) error ("gumbelpdf: X, MU, and BETA must not be complex."); endif ## Return NaNs for out of range parameters beta(beta <= 0) = NaN; ## Compute pdf of type 1 extreme value distribution z = -(x - mu) ./ beta; y = exp (z - exp (z)) ./ beta; ## Force 0 for extreme right tail, instead of getting exp (Inf - Inf) = NaN y(z == Inf) = 0; endfunction %!demo %! ## Plot various PDFs from the Extreme value distribution %! x = -5:0.001:20; %! y1 = gumbelpdf (x, 0.5, 2); %! y2 = gumbelpdf (x, 1.0, 2); %! y3 = gumbelpdf (x, 1.5, 3); %! y4 = gumbelpdf (x, 3.0, 4); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', x, y4, '-c') %! grid on %! ylim ([0, 0.2]) %! legend ({'μ = 0.5, β = 2', 'μ = 1.0, β = 2', ... %! 'μ = 1.5, β = 3', 'μ = 3.0, β = 4'}, 'location', 'northeast') %! title ('Extreme value PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x, y0, y1 %! x = [-5, 0, 1, 2, 3]; %! y0 = [0, 0.3679, 0.2547, 0.1182, 0.0474]; %! y1 = [0, 0.1794, 0.3679, 0.2547, 0.1182]; %!assert_equal (gumbelpdf (x), y0, 1e-4) %!assert_equal (gumbelpdf (x, zeros (1,5), ones (1,5)), y0, 1e-4) %!assert_equal (gumbelpdf (x, ones (1,5), ones (1,5)), y1, 1e-4) ## Test input validation %!error gumbelpdf () %!error ... %! gumbelpdf (ones (3), ones (2), ones (2)) %!error gumbelpdf (int32 (2), 2, 2) %!error gumbelpdf (true, 2, 2) %!error gumbelpdf ('a', 2, 2) %!error gumbelpdf (i, 2, 2) %!error gumbelpdf (2, i, 2) %!error gumbelpdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/gumbelrnd.m000066400000000000000000000157541527311104300251560ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} gumbelrnd (@var{mu}, @var{beta}) ## @deftypefnx {statistics} {@var{r} =} gumbelrnd (@var{mu}, @var{beta}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} gumbelrnd (@var{mu}, @var{beta}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} gumbelrnd (@var{mu}, @var{beta}, [@var{sz}]) ## ## Random arrays from the Gumbel distribution. ## ## @code{@var{r} = gumbelrnd (@var{mu}, @var{beta})} returns an array of random ## numbers chosen from the Gumbel distribution (also known as the extreme value ## or the type I generalized extreme value distribution) with location ## parameter @var{mu} and scale parameter @var{beta}. The size of @var{r} is ## the common size of @var{mu} and @var{beta}. A scalar input functions as a ## constant matrix of the same size as the other inputs. ## ## When called with a single size argument, @code{gumbelrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## The Gumbel distribution is used to model the distribution of the maximum (or ## the minimum) of a number of samples of various distributions. This version ## is suitable for modeling maxima. For modeling minima, use the alternative ## extreme value iCDF, @code{evinv}. ## ## Further information about the Gumbel distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gumbel_distribution} ## ## @seealso{gumbelcdf, gumbelinv, gumbelpdf, gumbelfit, gumbellike, gumbelstat, ## evrnd} ## @end deftypefn function r = gumbelrnd (mu, beta, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("gumbelrnd: function called with too few input arguments."); endif ## Check for common size of MU and BETA if (! isscalar (mu) || ! isscalar (beta)) [retval, mu, beta] = common_size (mu, beta); if (retval > 0) error ("gumbelrnd: MU and BETA must be of common size or scalars."); endif endif ## Check for MU and BETA being reals if (iscomplex (mu) || iscomplex (beta)) error ("gumbelrnd: MU and BETA must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (mu); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("gumbelrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("gumbelrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (mu) && ! isequal (size (mu), size (ones (sz)))) error ("gumbelrnd: MU and BETA must be scalars or of size SZ."); endif ## Check for class type if (isa (mu, 'single') || isa (beta, 'single')) cls = 'single'; else cls = 'double'; endif ## Return NaNs for out of range values of BETA beta(beta < 0) = NaN; ## Generate uniform random values, and apply the extreme value inverse CDF. r = -log (-log (rand (sz, cls))) .* beta + mu; endfunction ## Test output %!assert_equal (size (gumbelrnd (1, 1)), [1 1]) %!assert_equal (size (gumbelrnd (1, ones (2,1))), [2, 1]) %!assert_equal (size (gumbelrnd (1, ones (2,2))), [2, 2]) %!assert_equal (size (gumbelrnd (ones (2,1), 1)), [2, 1]) %!assert_equal (size (gumbelrnd (ones (2,2), 1)), [2, 2]) %!assert_equal (size (gumbelrnd (1, 1, 3)), [3, 3]) %!assert_equal (size (gumbelrnd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (gumbelrnd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (gumbelrnd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (gumbelrnd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (gumbelrnd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (gumbelrnd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (gumbelrnd (1, 1, [])), [0, 0]) %!assert_equal (size (gumbelrnd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (gumbelrnd (1, 2, -1)), [0, 0]) %!assert_equal (size (gumbelrnd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (gumbelrnd (1, 2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (gumbelrnd (1, 1)), "double") %!assert_equal (class (gumbelrnd (1, single (1))), "single") %!assert_equal (class (gumbelrnd (1, single ([1, 1]))), "single") %!assert_equal (class (gumbelrnd (single (1), 1)), "single") %!assert_equal (class (gumbelrnd (single ([1, 1]), 1)), "single") ## Test input validation %!error gumbelrnd () %!error gumbelrnd (1) %!error ... %! gumbelrnd (ones (3), ones (2)) %!error ... %! gumbelrnd (ones (2), ones (3)) %!error gumbelrnd (i, 2, 3) %!error gumbelrnd (1, i, 3) %!error ... %! gumbelrnd (1, 2, 1.2) %!error ... %! gumbelrnd (1, 2, ones (2)) %!error ... %! gumbelrnd (1, 2, [2 0 2.5]) %!error ... %! gumbelrnd (1, 2, 2, 1.5, 5) %!error ... %! gumbelrnd (2, ones (2), 3) %!error ... %! gumbelrnd (2, ones (2), [3, 2]) %!error ... %! gumbelrnd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/hncdf.m000066400000000000000000000154511527311104300242530ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} hncdf (@var{x}, @var{mu}, @var{sigma}) ## @deftypefnx {statistics} {@var{p} =} hncdf (@var{x}, @var{mu}, @var{sigma}, @qcode{'upper'}) ## ## Half-normal cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the half-normal distribution with location parameter @var{mu} and ## scale parameter @var{sigma}. The size of @var{p} is the common size of ## @var{x}, @var{mu} and @var{sigma}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## @code{[@dots{}] = hncdf (@var{x}, @var{mu}, @var{sigma}, "upper")} computes ## the upper tail probability of the half-normal distribution with parameters ## @var{mu} and @var{sigma}, at the values in @var{x}. ## ## The half-normal CDF is only defined for @qcode{@var{x} >= @var{mu}}. ## ## Further information about the half-normal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Half-normal_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{hninv, hnpdf, hnrnd, hnfit, hnlike, hnstat} ## @end deftypefn function p = hncdf (x, mu, sigma, uflag) ## Check for valid number of input arguments if (nargin < 3) error ("hncdf: function called with too few input arguments."); endif ## Check for valid "upper" flag if (nargin > 3) if (! strcmpi (uflag, 'upper')) error ("hncdf: invalid argument for upper tail."); else uflag = true; endif else uflag = false; endif ## Check for common size of X, MU, and SIGMA if (! isscalar (x) || ! isscalar (mu) || ! isscalar (sigma)) [err, x, mu, sigma] = common_size (x, mu, sigma); if (err > 0) error ("hncdf: X, MU, and SIGMA must be of common size or scalars."); endif endif ## Check for X, MU, and SIGMA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (sigma))) error ("hncdf: X, MU, and SIGMA must be double or single."); endif ## Check for X, MU, and SIGMA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (sigma)) error ("hncdf: X, MU, and SIGMA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (sigma, 'single')) is_class = 'single'; else is_class = 'double'; endif ## Prepare output p = zeros (size (x), is_class); ## Return NaNs for out of range values of SIGMA parameter sigma(sigma <= 0) = NaN; ## Calculate (x-mu)/sigma => 0 and force zero below that z = (x - mu) ./ sigma; z(z < 0) = 0; if (uflag) p = erfc (z./sqrt (2)); else p = erf (z./sqrt (2)); endif endfunction %!demo %! ## Plot various CDFs from the half-normal distribution %! x = 0:0.001:10; %! p1 = hncdf (x, 0, 1); %! p2 = hncdf (x, 0, 2); %! p3 = hncdf (x, 0, 3); %! p4 = hncdf (x, 0, 5); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', x, p4, '-c') %! grid on %! xlim ([0, 10]) %! legend ({'μ = 0, σ = 1', 'μ = 0, σ = 2', ... %! 'μ = 0, σ = 3', 'μ = 0, σ = 5'}, 'location', 'southeast') %! title ('Half-normal CDF') %! xlabel ('values in x') %! ylabel ('probability') %!demo %! ## Plot half-normal against normal cumulative distribution function %! x = -5:0.001:5; %! p1 = hncdf (x, 0, 1); %! p2 = normcdf (x); %! plot (x, p1, '-b', x, p2, '-g') %! grid on %! xlim ([-5, 5]) %! legend ({'half-normal with μ = 0, σ = 1', ... %! 'standard normal (μ = 0, σ = 1)'}, 'location', 'southeast') %! title ('Half-normal against standard normal CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, p1, p1u, y2, y2u, y3, y3u %! x = [-Inf, -1, 0, 1/2, 1, Inf]; %! p1 = [0, 0, 0, 0.3829, 0.6827, 1]; %! p1u = [1, 1, 1, 0.6171, 0.3173, 0]; %!assert_equal (hncdf (x, zeros (1,6), ones (1,6)), p1, 1e-4) %!assert_equal (hncdf (x, 0, 1), p1, 1e-4) %!assert_equal (hncdf (x, 0, ones (1,6)), p1, 1e-4) %!assert_equal (hncdf (x, zeros (1,6), 1), p1, 1e-4) %!assert_equal (hncdf (x, 0, [1, 1, 1, NaN, 1, 1]), [p1(1:3), NaN, p1(5:6)], 1e-4) %!assert_equal (hncdf (x, [0, 0, 0, NaN, 0, 0], 1), [p1(1:3), NaN, p1(5:6)], 1e-4) %!assert_equal (hncdf ([x(1:3), NaN, x(5:6)], 0, 1), [p1(1:3), NaN, p1(5:6)], 1e-4) %!assert_equal (hncdf (x, zeros (1,6), ones (1,6), 'upper'), p1u, 1e-4) %!assert_equal (hncdf (x, 0, 1, 'upper'), p1u, 1e-4) %!assert_equal (hncdf (x, 0, ones (1,6), 'upper'), p1u, 1e-4) %!assert_equal (hncdf (x, zeros (1,6), 1, 'upper'), p1u, 1e-4) ## Test class of input preserved %!assert_equal (class (hncdf (single ([x, NaN]), 0, 1)), "single") %!assert_equal (class (hncdf ([x, NaN], 0, single (1))), "single") %!assert_equal (class (hncdf ([x, NaN], single (0), 1)), "single") ## Test input validation %!error hncdf () %!error hncdf (1) %!error hncdf (1, 2) %!error hncdf (1, 2, 3, 'tail') %!error hncdf (1, 2, 3, 5) %!error ... %! hncdf (ones (3), ones (2), ones (2)) %!error ... %! hncdf (ones (2), ones (3), ones (2)) %!error ... %! hncdf (ones (2), ones (2), ones (3)) %!error hncdf (int32 (2), 2, 3) %!error hncdf (true, 2, 3) %!error hncdf ('a', 2, 3) %!error hncdf (i, 2, 3) %!error hncdf (1, i, 3) %!error hncdf (1, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/hninv.m000066400000000000000000000116711527311104300243130ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} hninv (@var{p}, @var{mu}, @var{sigma}) ## ## Inverse of the half-normal cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the half-normal distribution with location parameter @var{mu} and scale ## parameter @var{sigma}. The size of @var{x} is the common size of @var{p}, ## @var{mu}, and @var{sigma}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## Further information about the half-normal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Half-normal_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{hncdf, hnpdf, hnrnd, hnfit, hnlike, hnstat} ## @end deftypefn function x = hninv (p, mu, sigma) ## Check for valid number of input arguments if (nargin < 3) error ("hninv: function called with too few input arguments."); endif ## Check for common size of P, MU, and SIGMA if (! isscalar (p) || ! isscalar (mu) || ! isscalar (sigma)) [retval, p, mu, sigma] = common_size (p, mu, sigma); if (retval > 0) error ("hninv: P, MU, and SIGMA must be of common size or scalars."); endif endif ## Check for P, MU, and SIGMA being double or single if (! (isfloat (p) && isfloat (mu) && isfloat (sigma))) error ("hninv: P, MU, and SIGMA must be double or single."); endif ## Check for X, MU, and SIGMA being reals if (iscomplex (p) || iscomplex (mu) || iscomplex (sigma)) error ("hninv: P, MU, and SIGMA must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (mu, 'single') || isa (sigma, 'single')); x = NaN (size (p), 'single'); else x = NaN (size (p)); endif ## Return NaNs for out of range values of SIGMA parameter sigma(sigma <= 0) = NaN; ## Return NaNs for out of range P values p(p < 0 | 1 < p) = NaN; ## Calculate the quantile of half-normal distribution x = sqrt (2) * sigma .* erfinv (p) + mu; endfunction %!demo %! ## Plot various iCDFs from the half-normal distribution %! p = 0.001:0.001:0.999; %! x1 = hninv (p, 0, 1); %! x2 = hninv (p, 0, 2); %! x3 = hninv (p, 0, 3); %! x4 = hninv (p, 0, 5); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', p, x4, '-c') %! grid on %! ylim ([0, 10]) %! legend ({'μ = 0, σ = 1', 'μ = 0, σ = 2', ... %! 'μ = 0, σ = 3', 'μ = 0, σ = 5'}, 'location', 'northwest') %! title ('Half-normal iCDF') %! xlabel ('probability') %! ylabel ('x') ## Test output %!shared p, x %! p = [0, 0.3829, 0.6827, 1]; %! x = [0, 1/2, 1, Inf]; %!assert_equal (hninv (p, 0, 1), x, 1e-4); %!assert_equal (hninv (p, 5, 1), x + 5, 1e-4); %!assert_equal (hninv (p, 0, ones (1,4)), x, 1e-4); %!assert_equal (hninv (p, 0, [-1, 0, 1, 1]), [NaN, NaN, x(3:4)], 1e-4) ## Test class of input preserved %!assert_equal (class (hninv (single ([p, NaN]), 0, 1)), "single") %!assert_equal (class (hninv ([p, NaN], single (0), 1)), "single") %!assert_equal (class (hninv ([p, NaN], 0, single (1))), "single") ## Test input validation %!error hninv (1) %!error hninv (1, 2) %!error ... %! hninv (1, ones (2), ones (3)) %!error ... %! hninv (ones (2), 1, ones (3)) %!error ... %! hninv (ones (2), ones (3), 1) %!error hninv (int32 (2), 2, 3) %!error hninv (true, 2, 3) %!error hninv ('a', 2, 3) %!error hninv (i, 2, 3) %!error hninv (1, i, 3) %!error hninv (1, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/hnpdf.m000066400000000000000000000127061527311104300242700ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} hnpdf (@var{x}, @var{mu}, @var{sigma}) ## ## Half-normal probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the half-normal distribution with location parameter @var{mu} and scale ## parameter @var{sigma}. The size of @var{y} is the common size of @var{x}, ## @var{mu}, and @var{sigma}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## The half-normal CDF is only defined for @qcode{@var{x} >= @var{mu}}. ## ## Further information about the half-normal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Half-normal_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{hncdf, hninv, hnrnd, hnfit, hnlike, hnstat} ## @end deftypefn function y = hnpdf (x, mu, sigma) ## Check for valid number of input arguments if (nargin < 3) error ("hnpdf: function called with too few input arguments."); endif ## Check for common size of X, MU, and SIGMA if (! isscalar (x) || ! isscalar (mu) || ! isscalar (sigma)) [retval, x, mu, sigma] = common_size (x, mu, sigma); if (retval > 0) error ("hnpdf: X, MU, and SIGMA must be of common size or scalars."); endif endif ## Check for X, MU, and SIGMA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (sigma))) error ("hnpdf: X, MU, and SIGMA must be double or single."); endif ## Check for X, MU, and SIGMA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (sigma)) error ("hnpdf: X, MU, and SIGMA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (sigma, 'single')); y = NaN (size (x), 'single'); else y = NaN (size (x)); endif ## Return NaNs for out of range values of SIGMA parameter sigma(sigma <= 0) = NaN; ## Compute half-normal PDF z = (x - mu) ./ sigma; y = sqrt (2 / pi) ./ sigma .* exp (-0.5 * z .^ 2); ## Force zero for unsupported X y(z < 0) = 0; endfunction %!demo %! ## Plot various PDFs from the half-normal distribution %! x = 0:0.001:10; %! y1 = hnpdf (x, 0, 1); %! y2 = hnpdf (x, 0, 2); %! y3 = hnpdf (x, 0, 3); %! y4 = hnpdf (x, 0, 5); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', x, y4, '-c') %! grid on %! xlim ([0, 10]) %! ylim ([0, 0.9]) %! legend ({'μ = 0, σ = 1', 'μ = 0, σ = 2', ... %! 'μ = 0, σ = 3', 'μ = 0, σ = 5'}, 'location', 'northeast') %! title ('Half-normal PDF') %! xlabel ('values in x') %! ylabel ('density') %!demo %! ## Plot half-normal against normal probability density function %! x = -5:0.001:5; %! y1 = hnpdf (x, 0, 1); %! y2 = normpdf (x); %! plot (x, y1, '-b', x, y2, '-g') %! grid on %! xlim ([-5, 5]) %! ylim ([0, 0.9]) %! legend ({'half-normal with μ = 0, σ = 1', ... %! 'standard normal (μ = 0, σ = 1)'}, 'location', 'northeast') %! title ('Half-normal against standard normal PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x, y %! x = [-Inf, -1, 0, 1/2, 1, Inf]; %! y = [0, 0, 0.7979, 0.7041, 0.4839, 0]; %!assert_equal (hnpdf ([x, NaN], 0, 1), [y, NaN], 1e-4) %!assert_equal (hnpdf (x, 0, [-2, -1, 0, 1, 1, 1]), [nan(1,3), y([4:6])], 1e-4) ## Test class of input preserved %!assert_equal (class (hncdf (single ([x, NaN]), 0, 1)), "single") %!assert_equal (class (hncdf ([x, NaN], 0, single (1))), "single") %!assert_equal (class (hncdf ([x, NaN], single (0), 1)), "single") ## Test input validation %!error hnpdf () %!error hnpdf (1) %!error hnpdf (1, 2) %!error ... %! hnpdf (1, ones (2), ones (3)) %!error ... %! hnpdf (ones (2), 1, ones (3)) %!error ... %! hnpdf (ones (2), ones (3), 1) %!error hnpdf (int32 (2), 2, 3) %!error hnpdf (true, 2, 3) %!error hnpdf ('a', 2, 3) %!error hnpdf (i, 2, 3) %!error hnpdf (1, i, 3) %!error hnpdf (1, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/hnrnd.m000066400000000000000000000150351527311104300243000ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} hnrnd (@var{mu}, @var{sigma}) ## @deftypefnx {statistics} {@var{r} =} hnrnd (@var{mu}, @var{sigma}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} hnrnd (@var{mu}, @var{sigma}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} hnrnd (@var{mu}, @var{sigma}, [@var{sz}]) ## ## Random arrays from the half-normal distribution. ## ## @code{@var{r} = hnrnd (@var{mu}, @var{sigma})} returns an array of random ## numbers chosen from the half-normal distribution with location parameter ## @var{mu} and scale parameter @var{sigma}. The size of @var{r} is the common ## size of @var{mu} and @var{sigma}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## When called with a single size argument, @code{hnrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the half-normal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Half-normal_distribution} ## ## @seealso{hncdf, hninv, hnpdf, hnfit, hnlike, hnstat} ## @end deftypefn function r = hnrnd (mu, sigma, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("hnrnd: function called with too few input arguments."); endif ## Check for common size of MU, and SIGMA if (! isscalar (mu) || ! isscalar (sigma)) [retval, mu, sigma] = common_size (mu, sigma); if (retval > 0) error ("hnrnd: MU and SIGMA must be of common size or scalars."); endif endif ## Check for X, MU, and SIGMA being reals if (iscomplex (mu) || iscomplex (sigma)) error ("hnrnd: MU and SIGMA must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (mu); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("hnrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("hnrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (mu) && ! isequal (size (mu), size (ones (sz)))) error ("hnrnd: MU and SIGMA must be scalars or of size SZ."); endif ## Check for class type if (isa (mu, 'single') || isa (sigma, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from half-normal distribution r = abs (randn (sz, cls)) .* sigma + mu; ## Force output to NaN for invalid parameter SIGMA <= 0 k = (sigma <= 0); r(k) = NaN; endfunction ## Test output %!assert_equal (size (hnrnd (1, 1, 1)), [1, 1]) %!assert_equal (size (hnrnd (1, 1, 2)), [2, 2]) %!assert_equal (size (hnrnd (1, 1, [2, 1])), [2, 1]) %!assert_equal (size (hnrnd (1, zeros (2, 2))), [2, 2]) %!assert_equal (size (hnrnd (1, ones (2, 1))), [2, 1]) %!assert_equal (size (hnrnd (1, ones (2, 2))), [2, 2]) %!assert_equal (size (hnrnd (ones (2, 1), 1)), [2, 1]) %!assert_equal (size (hnrnd (ones (2, 2), 1)), [2, 2]) %!assert_equal (size (hnrnd (1, 1, 3)), [3, 3]) %!assert_equal (size (hnrnd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (hnrnd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (hnrnd (1, 1, [])), [0, 0]) %!assert_equal (size (hnrnd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (hnrnd (1, 2, -1)), [0, 0]) %!assert_equal (size (hnrnd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (hnrnd (1, 2, 2, -1, 5)), [2, 0, 5]) %!test %! r = hnrnd (1, [1, 0, -1]); %! assert_equal (r([2:3]), [NaN, NaN]) ## Test class of input preserved %!assert_equal (class (hnrnd (1, 0)), "double") %!assert_equal (class (hnrnd (1, single (0))), "single") %!assert_equal (class (hnrnd (1, single ([0, 0]))), "single") %!assert_equal (class (hnrnd (1, single (1))), "single") %!assert_equal (class (hnrnd (1, single ([1, 1]))), "single") %!assert_equal (class (hnrnd (single (1), 1)), "single") %!assert_equal (class (hnrnd (single ([1, 1]), 1)), "single") ## Test input validation %!error hnrnd () %!error hnrnd (1) %!error ... %! hnrnd (ones (3), ones (2)) %!error ... %! hnrnd (ones (2), ones (3)) %!error hnrnd (i, 2, 3) %!error hnrnd (1, i, 3) %!error ... %! hnrnd (1, 2, 1.2) %!error ... %! hnrnd (1, 2, ones (2)) %!error ... %! hnrnd (1, 2, [2 0 2.5]) %!error ... %! hnrnd (1, 2, 2, 1.5, 5) %!error ... %! hnrnd (2, ones (2), 3) %!error ... %! hnrnd (2, ones (2), [3, 2]) %!error ... %! hnrnd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/hygecdf.m000066400000000000000000000233331527311104300246000ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1997-2016 Kurt Hornik ## Copyright (C) 2022 Nicholas R. Jankowski ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} hygecdf (@var{x}, @var{m}, @var{k}, @var{n}) ## @deftypefnx {statistics} {@var{p} =} hygecdf (@var{x}, @var{m}, @var{k}, @var{n}, @qcode{'upper'}) ## ## Hypergeometric cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the hypergeometric distribution with parameters @var{m}, @var{k}, ## and @var{n}. The size of @var{p} is the common size of @var{x}, @var{m}, ## @var{k}, and @var{n}. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## This is the cumulative probability of obtaining not more than @var{x} marked ## items when randomly drawing a sample of size @var{n} without replacement from ## a population of total size @var{m} containing @var{k} marked items. The ## parameters @var{m}, @var{k}, and @var{n} must be positive integers with ## @var{k} and @var{n} not greater than @var{m}. ## ## @code{[@dots{}] = hygecdf (@var{x}, @var{m}, @var{k}, @var{n}, "upper")} ## computes the upper tail probability of the hypergeometric distribution with ## parameters @var{m}, @var{k}, and @var{n}, at the values in @var{x}. ## ## Further information about the hypergeometric distribution can be found at ## @url{https://en.wikipedia.org/wiki/Hypergeometric_distribution} ## ## Input arguments must be @qcode{double}, @qcode{single}, or an integer type; ## logical and character arrays are rejected. Integer input is promoted to ## @qcode{double}, so the result is always a probability. MATLAB is ## inconsistent here: for several of the discrete distributions it returns the ## result in the integer class of the input, truncating a probability to ## @math{0} or @math{1}. ## ## @seealso{hygeinv, hygepdf, hygernd, hygestat} ## @end deftypefn function p = hygecdf (x, m, k, n, uflag) ## Check for valid number of input arguments if (nargin < 4) error ("hygecdf: function called with too few input arguments."); endif ## Check for common size of X, T, M, and N if (! isscalar (x) || ! isscalar (m) || ! isscalar (k) || ! isscalar (n)) [retval, x, m, k, n] = common_size (x, m, k, n); if (retval > 0) error ("hygecdf: X, T, M, and N must be of common size or scalars."); endif endif ## Check for X, T, M, and N being double, single, or integer if (! (isnumeric (x) && isnumeric (m) && isnumeric (k) && isnumeric (n))) error ("hygecdf: X, T, M, and N must be double, single, or integer."); endif ## Integer input is promoted to double, so the result is a probability ## rather than a value truncated to the input's integer type. if (isinteger (x)) x = double (x); endif if (isinteger (m)) m = double (m); endif if (isinteger (k)) k = double (k); endif if (isinteger (n)) n = double (n); endif ## Check for X, T, M, and N being reals if (iscomplex (x) || iscomplex (m) || iscomplex (k) || iscomplex (n)) error ("hygecdf: X, T, M, and N must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (m, 'single') || isa (k, 'single') || isa (n, 'single')) p = zeros (size (x), 'single'); else p = zeros (size (x)); endif ## Check for "upper" flag if (nargin > 4 && strcmpi (uflag, 'upper')) x = n - floor (x) - 1; k = m - k; elseif (nargin > 4 && ! strcmpi (uflag, 'upper')) error ("hygecdf: invalid argument for upper tail."); endif ## Force 1 where required is_1 = (x >= n | x >= k); p(is_1) = 1; ## Force NaNs where required is_nan = (isnan (x) | isnan (m) | isnan (k) | isnan (n) | ... m < 0 | k < 0 | n < 0 | round (m) != m | round (k) != k | ... round (n) != n | n > m | k > m); p(is_nan) = NaN; ## Get values for which P = 0 is_0 = (m - k - n + x + 1 <= 0 | x < 0); ok = ! (is_1 | is_nan | is_0); ## Compute hypergeometric CDF if (any (ok(:))) ## For improved accuracy, compute the upper tail 1-p instead ## of the lower tail pfor x values that are larger than the mean lo = (x <= k .* n ./ m); ok_lo = ok & lo; if (any (ok_lo(:))) p(ok_lo) = localPDF (floor (x(ok_lo)), m(ok_lo), k(ok_lo), n(ok_lo)); endif ok_hi = ok & ! lo; if (any (ok_hi(:))) p(ok_hi) = 1 - localPDF (n(ok_hi) - floor (x(ok_hi)) - 1, ... m(ok_hi), m(ok_hi) - k(ok_hi), n(ok_hi)); endif endif endfunction function p = localPDF (x, m, k, n) HPDF = hygepdf (x, m, k, n); ## Compute hygecdf(x,m,k,n)/hygepdf(x,m,k,n) with a series ## whose terms can be computed recursively, backwards. xmax = max (x(:)); ybig = repmat ((0:xmax)', 1, length (x)); xbig = repmat (x(:)', xmax + 1, 1); mbig = repmat (m(:)', xmax + 1, 1); kbig = repmat (k(:)', xmax + 1, 1); nbig = repmat (n(:)', xmax + 1, 1); terms = ((ybig+1) .* (mbig-kbig-nbig+ybig+1)) ./ ((nbig-ybig) .* (kbig-ybig)); terms(ybig >= xbig) = 1; terms = flip (cumprod (flip (terms))); terms(ybig > xbig) = 0; ratio = sum (terms,1); ratio = reshape (ratio,size (x)); p = ratio.*HPDF; ## Correct round-off errors p(p > 1) = 1; endfunction %!demo %! ## Plot various CDFs from the hypergeometric distribution %! x = 0:60; %! p1 = hygecdf (x, 500, 50, 100); %! p2 = hygecdf (x, 500, 60, 200); %! p3 = hygecdf (x, 500, 70, 300); %! plot (x, p1, '*b', x, p2, '*g', x, p3, '*r') %! grid on %! xlim ([0, 60]) %! legend ({'m = 500, k = 50, n = 100', 'm = 500, k = 60, n = 200', ... %! 'm = 500, k = 70, n = 300'}, 'location', 'southeast') %! title ('Hypergeometric CDF') %! xlabel ('values in x (number of successes)') %! ylabel ('probability') ## Test output %!shared x, y %! x = [-1 0 1 2 3]; %! y = [0 1/6 5/6 1 1]; %!assert_equal (hygecdf (x, 4*ones (1,5), 2, 2), y, 5*eps) %!assert_equal (hygecdf (x, 4, 2*ones (1,5), 2), y, 5*eps) %!assert_equal (hygecdf (x, 4, 2, 2*ones (1,5)), y, 5*eps) %!assert_equal (hygecdf (x, 4*[1 -1 NaN 1.1 1], 2, 2), [y(1) NaN NaN NaN y(5)], 5*eps) %!assert_equal (hygecdf (x, 4*[1 -1 NaN 1.1 1], 2, 2, 'upper'), ... %! [y(5) NaN NaN NaN y(1)], 5*eps) %!assert_equal (hygecdf (x, 4, 2*[1 -1 NaN 1.1 1], 2), [y(1) NaN NaN NaN y(5)], 5*eps) %!assert_equal (hygecdf (x, 4, 2*[1 -1 NaN 1.1 1], 2, 'upper'), ... %! [y(5) NaN NaN NaN y(1)], 5*eps) %!assert_equal (hygecdf (x, 4, 5, 2), [NaN NaN NaN NaN NaN]) %!assert_equal (hygecdf (x, 4, 2, 2*[1 -1 NaN 1.1 1]), [y(1) NaN NaN NaN y(5)], 5*eps) %!assert_equal (hygecdf (x, 4, 2, 2*[1 -1 NaN 1.1 1], 'upper'), ... %! [y(5) NaN NaN NaN y(1)], 5*eps) %!assert_equal (hygecdf (x, 4, 2, 5), [NaN NaN NaN NaN NaN]) %!assert_equal (hygecdf ([x(1:2) NaN x(4:5)], 4, 2, 2), [y(1:2) NaN y(4:5)], 5*eps) %!test %! p = hygecdf (x, 10, [1 2 3 4 5], 2, 'upper'); %! assert_equal (p, [1, 34/90, 2/30, 0, 0], 10*eps); %!test %! p = hygecdf (2*x, 10, [1 2 3 4 5], 2, 'upper'); %! assert_equal (p, [1, 34/90, 0, 0, 0], 10*eps); %!assert_equal (hygecdf (2132, 8658, 5491, 3533), ... %! 4.6997816650737797e-07, -1e-14) %!assert_equal (hygecdf (2300, 8658, 5491, 3533), 0.99672623672210792, -1e-14) %!assert_equal (hygecdf (24500, 200000, 100000, 50000), ... %! 1.2417441330510847e-07, -1e-14) ## Test class of input preserved %!assert_equal (hygecdf ([x, NaN], 4, 2, 2), [y, NaN], 5*eps) %!assert_equal (hygecdf (single ([x, NaN]), 4, 2, 2), single ([y, NaN]), ... %! eps ('single')) %!assert_equal (hygecdf ([x, NaN], single (4), 2, 2), single ([y, NaN]), ... %! eps ('single')) %!assert_equal (hygecdf ([x, NaN], 4, single (2), 2), single ([y, NaN]), ... %! eps ('single')) %!assert_equal (hygecdf ([x, NaN], 4, 2, single (2)), single ([y, NaN]), ... %! eps ('single')) ## Test input validation %!error hygecdf () %!error hygecdf (1) %!error hygecdf (1,2) %!error hygecdf (1,2,3) %!error hygecdf (1,2,3,4,5) %!error hygecdf (1,2,3,4,'uper') %!error ... %! hygecdf (ones (2), ones (3), 1, 1) %!error ... %! hygecdf (1, ones (2), ones (3), 1) %!error ... %! hygecdf (1, 1, ones (2), ones (3)) %!error hygecdf (true, 2, 2, 2) %!error hygecdf ('a', 2, 2, 2) %!assert_equal (class (hygecdf (int32 (2), 2, 2, 2)), 'double') %!error hygecdf (i, 2, 2, 2) %!error hygecdf (2, i, 2, 2) %!error hygecdf (2, 2, i, 2) %!error hygecdf (2, 2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/hygeinv.m000066400000000000000000000210241527311104300246330ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1997-2016 Kurt Hornik ## Copyright (C) 2022 Nicholas R. Jankowski ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} hygeinv (@var{p}, @var{m}, @var{k}, @var{n}) ## ## Inverse of the hypergeometric cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the hypergeometric distribution with parameters @var{m}, @var{k}, and ## @var{n}. The size of @var{x} is the common size of @var{p}, @var{m}, ## @var{k}, and @var{n}. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## This is the number of drawn marked items @var{x} given a probability @var{p}, ## when randomly drawing a sample of size @var{n} without replacement from a ## population of total size @var{m} containing @var{k} marked items. The ## parameters @var{m}, @var{k}, and @var{n} must be non-negative integers with ## @var{k} and @var{n} not greater than @var{m}, and @var{m} finite unless ## @var{n} is @qcode{0}. ## ## For @var{p} = 0 and @var{p} = 1, @var{x} is the least and the greatest ## number of marked items the sample can hold, @code{max (0, @var{n} - (@var{m} ## - @var{k}))} and @code{min (@var{n}, @var{k})}. MATLAB does not return for ## @var{p} = 0. ## ## Further information about the hypergeometric distribution can be found at ## @url{https://en.wikipedia.org/wiki/Hypergeometric_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{hygecdf, hygepdf, hygernd, hygestat} ## @end deftypefn function x = hygeinv (p, m, k, n) ## Check for valid number of input arguments if (nargin < 4) error ("hygeinv: function called with too few input arguments."); endif ## Check for common size of P, T, M, and N if (! isscalar (p) || ! isscalar (m) || ! isscalar (k) || ! isscalar (n)) [retval, p, m, k, n] = common_size (p, m, k, n); if (retval > 0) error ("hygeinv: P, T, M, and N must be of common size or scalars."); endif endif ## Check for P, T, M, and N being double or single if (! (isfloat (p) && isfloat (m) && isfloat (k) && isfloat (n))) error ("hygeinv: P, T, M, and N must be double or single."); endif ## Check for P, T, M, and N being reals if (iscomplex (p) || iscomplex (m) || iscomplex (k) || iscomplex (n)) error ("hygeinv: P, T, M, and N must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (m, 'single') || isa (k, 'single') || isa (n, 'single')) x = NaN (size (p), 'single'); else x = NaN (size (p)); endif ## An infinite population has no density, except for an empty sample ok = ((m >= 0) & (k >= 0) & (n >= 0) & (k <= m) & (n <= m) & (m == fix (m)) & (k == fix (k)) & (n == fix (n)) & (isfinite (m) | (n == 0))); ## P = 0 and P = 1 give the least and greatest number of marked items the ## sample can hold lo = max (0, n - (m - k)); hi = min (n, k); is_lo = ok & (p == 0); x(is_lo) = lo(is_lo); is_hi = ok & (p == 1); x(is_hi) = hi(is_hi); ok &= (p > 0 & p < 1); # P outside [0, 1] is left as NaN if (any (ok(:))) n = n(ok); v = 0 : max (n(:)); ## Manually perform discrete_inv to enable vectorizing with array input p_tmp = cumsum (hygepdf (v, m(ok), k(ok), n, 'vectorexpand'), 2); sz_p = size (p_tmp); end_locs = sub2ind (sz_p, [1 : numel(n)]', n(:) + 1); ## Manual row-wise vectorization of lookup, which returns index of element ## less than or equal to test value, zero if test value less than lowest ## number, and max index if greater than highest number. operated on ## flipped p_tmp, adjusting for different vector lengths in array rows. p_tmp = (p_tmp ./ p_tmp(end_locs))(:, end:-1:1) - p(ok)(:); p_tmp(p_tmp>=0) = NaN; [p_match, p_match_idx] = max (p_tmp, [], 2); p_match_idx(isnan (p_match)) = v(end) + 2; x(ok) = v(v(end) - p_match_idx + 3); endif endfunction %!demo %! ## Plot various iCDFs from the hypergeometric distribution %! p = 0.001:0.001:0.999; %! x1 = hygeinv (p, 500, 50, 100); %! x2 = hygeinv (p, 500, 60, 200); %! x3 = hygeinv (p, 500, 70, 300); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r') %! grid on %! ylim ([0, 60]) %! legend ({'m = 500, k = 50, n = 100', 'm = 500, k = 60, n = 200', ... %! 'm = 500, k = 70, n = 300'}, 'location', 'northwest') %! title ('Hypergeometric iCDF') %! xlabel ('probability') %! ylabel ('values in p (number of successes)') ## Test output %!shared p %! p = [-1 0 0.5 1 2]; %!assert_equal (hygeinv (p, 4*ones (1,5), 2*ones (1,5), 2*ones (1,5)), [NaN 0 1 2 NaN]) %!assert_equal (hygeinv (p, 4*ones (1,5), 2, 2), [NaN 0 1 2 NaN]) %!assert_equal (hygeinv (p, 4, 2*ones (1,5), 2), [NaN 0 1 2 NaN]) %!assert_equal (hygeinv (p, 4, 2, 2*ones (1,5)), [NaN 0 1 2 NaN]) %!assert_equal (hygeinv (p, 4*[1 -1 NaN 1.1 1], 2, 2), [NaN NaN NaN NaN NaN]) %!assert_equal (hygeinv (p, 4, 2*[1 -1 NaN 1.1 1], 2), [NaN NaN NaN NaN NaN]) %!assert_equal (hygeinv (p, 4, 5, 2), [NaN NaN NaN NaN NaN]) %!assert_equal (hygeinv (p, 4, 2, 2*[1 -1 NaN 1.1 1]), [NaN NaN NaN NaN NaN]) %!assert_equal (hygeinv (p, 4, 2, 5), [NaN NaN NaN NaN NaN]) %!assert_equal (hygeinv ([p(1:2) NaN p(4:5)], 4, 2, 2), [NaN 0 NaN 2 NaN]) %!assert_equal (hygeinv ([0 0.1 0.5 0.9 1], 10, 5, 0), [0 0 0 0 0]) %!assert_equal (hygeinv (0.5, 0, 0, 0), 0) %!assert_equal (hygeinv ([0.1 0.9], [10 0], [5 0], [0 0]), [0 0]) %!assert_equal (hygeinv (0.5, 10, 0, 3), 0) %!assert_equal (hygeinv ([0 1], 10, 0, 3), [0 0]) %!assert_equal (hygeinv (1, 10, 2, 5), 2) %!assert_equal (hygeinv (1, [10 10], [2 5], [5 5]), [2 5]) %!assert_equal (hygeinv (0, 10, 8, 5), 3) %!assert_equal (hygeinv (0, [10 10], [8 5], [5 3]), [3 0]) %!assert_equal (hygeinv (1e-300, 10, 8, 5), 3) %!assert_equal (hygeinv ([1e-300 0.5], [10 10], [8 8], [5 5]), [3 4]) %!assert_equal (hygeinv (0.5, Inf, 5, 3), NaN) %!assert_equal (hygeinv (0.5, Inf, 5, 0), 0) %!assert_equal (hygeinv (0.5, 10, 5, -1), NaN) ## Test class of input preserved %!assert_equal (hygeinv ([p, NaN], 4, 2, 2), [NaN 0 1 2 NaN NaN]) %!assert_equal (hygeinv (single ([p, NaN]), 4, 2, 2), single ([NaN 0 1 2 NaN NaN])) %!assert_equal (hygeinv ([p, NaN], single (4), 2, 2), single ([NaN 0 1 2 NaN NaN])) %!assert_equal (hygeinv ([p, NaN], 4, single (2), 2), single ([NaN 0 1 2 NaN NaN])) %!assert_equal (hygeinv ([p, NaN], 4, 2, single (2)), single ([NaN 0 1 2 NaN NaN])) ## Test input validation %!error hygeinv () %!error hygeinv (1) %!error hygeinv (1,2) %!error hygeinv (1,2,3) %!error ... %! hygeinv (ones (2), ones (3), 1, 1) %!error ... %! hygeinv (1, ones (2), ones (3), 1) %!error ... %! hygeinv (1, 1, ones (2), ones (3)) %!error hygeinv (int32 (2), 2, 2, 2) %!error hygeinv (true, 2, 2, 2) %!error hygeinv ('a', 2, 2, 2) %!error hygeinv (i, 2, 2, 2) %!error hygeinv (2, i, 2, 2) %!error hygeinv (2, 2, i, 2) %!error hygeinv (2, 2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/hygepdf.m000066400000000000000000000251071527311104300246160ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1996-2016 Kurt Hornik ## Copyright (C) 2022 Nicholas R. Jankowski ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} hygepdf (@var{x}, @var{m}, @var{k}, @var{n}) ## @deftypefnx {statistics} {@var{y} =} hygepdf (@dots{}, @qcode{'vectorexpand'}) ## ## Hypergeometric probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the hypergeometric distribution with parameters @var{m}, @var{k}, and ## @var{n}. The size of @var{y} is the common size of @var{x}, @var{m}, ## @var{k}, and @var{n}. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## This is the probability of obtaining @var{x} marked items when randomly ## drawing a sample of size @var{n} without replacement from a population of ## total size @var{m} containing @var{k} marked items. The parameters @var{m}, ## @var{k}, and @var{n} must be non-negative integers with @var{k} and @var{n} ## not greater than @var{m}. A sample of size @qcode{0} contains no marked ## items, so @var{x} = 0 then has probability 1, even from an empty population. ## ## If the optional parameter @qcode{vectorexpand} is provided, @var{x} may be an ## array with size different from parameters @var{m}, @var{k}, and @var{n} ## (which must still be of a common size or scalar). Each element of @var{x} ## will be evaluated against each set of parameters @var{m}, @var{k}, and ## @var{n} in columnwise order. The output @var{y} will be an array of size ## @qcode{@var{r} x @var{s}}, where @qcode{@var{r} = numel (@var{m})}, and ## @qcode{@var{s} = numel (@var{x})}. ## ## Further information about the hypergeometric distribution can be found at ## @url{https://en.wikipedia.org/wiki/Hypergeometric_distribution} ## ## Input arguments must be @qcode{double}, @qcode{single}, or an integer type; ## logical and character arrays are rejected. Integer input is promoted to ## @qcode{double}, so the result is always a probability. MATLAB is ## inconsistent here: for several of the discrete distributions it returns the ## result in the integer class of the input, truncating a probability to ## @math{0} or @math{1}. ## ## @seealso{hygecdf, hygeinv, hygernd, hygestat} ## @end deftypefn function y = hygepdf (x, m, k, n, vect_expand) ## Check for valid number of input arguments if (nargin < 4) error ("hygepdf: function called with too few input arguments."); endif ## Check for X, T, M, and N being double, single, or integer if (! (isnumeric (x) && isnumeric (m) && isnumeric (k) && isnumeric (n))) error ("hygepdf: X, T, M, and N must be double, single, or integer."); endif ## Integer input is promoted to double, so the result is a probability ## rather than a value truncated to the input's integer type. if (isinteger (x)) x = double (x); endif if (isinteger (m)) m = double (m); endif if (isinteger (k)) k = double (k); endif if (isinteger (n)) n = double (n); endif ## Check for X, T, M, and N being reals if (iscomplex (x) || iscomplex (m) || iscomplex (k) || iscomplex (n)) error ("hygepdf: X, T, M, and N must not be complex."); endif ## Check for 5th argument or add default if (nargin < 5) vect_expand = []; endif if strcmpi (vect_expand, 'vectorexpand') ## Expansion to improve vectorization of hyge calling functions. ## Project inputs over a 2D array with x(:) as a row vector and m,k,n as ## a column vector. each y(i,j) is hygepdf(x(j), m(i), k(i), n(i)) ## Following expansion, remainder of algorithm processes as normal. if (! isscalar (m) || ! isscalar (k) || ! isscalar (n)) [retval, m, k, n] = common_size (m, k, n); if (retval > 0) error ("hygepdf: T, M, and N must be of common size or scalars."); endif ## Ensure col vectors before expansion m = m(:); k = k(:); n = n(:); endif ## Expand x,m,k,n to arrays of size numel(m) x numel(x) sz = [numel(m), numel(x)]; x = x(:)'; # ensure row vector before expansion x = x(ones (sz(1), 1), :); m = m(:, ones (sz(2), 1)); k = k(:, ones (sz(2), 1)); n = n(:, ones (sz(2), 1)); else ## Check for common size of X, T, M, and N if (! isscalar (m) || ! isscalar (k) || ! isscalar (n)) [retval, x, m, k, n] = common_size (x, m, k, n); if (retval > 0) error ("hygepdf: X, T, M, and N must be of common size or scalars."); endif endif sz = size (x); endif ## Check for class type if (isa (x, 'single') || isa (m, 'single') || isa (k, 'single') || isa (n, 'single')) y = zeros (sz, 'single'); else y = zeros (sz); endif ## Everything in nel gives NaN nel = (isnan (x) | (m < 0) | (k < 0) | (n < 0) | (k > m) | (n > m) | (m != fix (m)) | (k != fix (k)) | (n != fix (n))); ## Everything in zel gives 0 unless in nel zel = ((x != fix (x)) | (x < 0) | (x > k) | (n < x) | (n-x > m-k)); y(nel) = NaN; ## An empty sample holds no marked items; computed below, an empty population ## would divide zero by zero y(! nel & ! zel & (n == 0)) = 1; ok = ! nel & ! zel & (n != 0); if (any (ok(:))) ## Loader's form: the ratio of three binomial densities with ps = n / m, ## whose powers of ps and 1 - ps cancel, each binomial density computed ## by binopdf with Loader's saddle point expansion. if (isscalar (m)) ps = n / m; y(ok) = binopdf (x(ok), k, ps) .* binopdf (n - x(ok), m - k, ps) ... ./ binopdf (n, m, ps); else ps = n(ok) ./ m(ok); y(ok) = binopdf (x(ok), k(ok), ps) ... .* binopdf (n(ok) - x(ok), m(ok) - k(ok), ps) ... ./ binopdf (n(ok), m(ok), ps); endif endif endfunction %!demo %! ## Plot various PDFs from the hypergeometric distribution %! x = 0:60; %! y1 = hygepdf (x, 500, 50, 100); %! y2 = hygepdf (x, 500, 60, 200); %! y3 = hygepdf (x, 500, 70, 300); %! plot (x, y1, '*b', x, y2, '*g', x, y3, '*r') %! grid on %! xlim ([0, 60]) %! ylim ([0, 0.18]) %! legend ({'m = 500, k = 50, μ = 100', 'm = 500, k = 60, μ = 200', ... %! 'm = 500, k = 70, μ = 300'}, 'location', 'northeast') %! title ('Hypergeometric PDF') %! xlabel ('values in x (number of successes)') %! ylabel ('density') ## Test output %!shared x, y %! x = [-1 0 1 2 3]; %! y = [0 1/6 4/6 1/6 0]; %!assert_equal (hygepdf (x, 4 * ones (1, 5), 2, 2), y, 3 * eps) %!assert_equal (hygepdf (x, 4, 2 * ones (1, 5), 2), y, 3 * eps) %!assert_equal (hygepdf (x, 4, 2, 2 * ones (1, 5)), y, 3 * eps) %!assert_equal (hygepdf (x, 4 * [1, -1, NaN, 1.1, 1], 2, 2), [0, NaN, NaN, NaN, 0]) %!assert_equal (hygepdf (x, 4, 2 * [1, -1, NaN, 1.1, 1], 2), [0, NaN, NaN, NaN, 0]) %!assert_equal (hygepdf (x, 4, 5, 2), [NaN, NaN, NaN, NaN, NaN], 3 * eps) %!assert_equal (hygepdf (x, 4, 2, 2 * [1, -1, NaN, 1.1, 1]), [0, NaN, NaN, NaN, 0]) %!assert_equal (hygepdf (x, 4, 2, 5), [NaN, NaN, NaN, NaN, NaN], 3 * eps) %!assert_equal (hygepdf ([x, NaN], 4, 2, 2), [y, NaN], 3 * eps) %!assert_equal (hygepdf (x, 10, 5, 0), [0, 1, 0, 0, 0]) %!assert_equal (hygepdf (0.5, 10, 5, 0), 0) %!assert_equal (hygepdf ([0, 1], 0, 0, 0), [1, 0]) %!assert_equal (hygepdf ([0, 1], 10, 5, [0, 0]), [1, 0]) %!assert_equal (hygepdf (0, [10, 10, 10], [0, 5, 10], 0), [1, 1, 1]) %!assert_equal (hygepdf (0, 10, 5, -1), NaN) %!assert_equal (hygepdf (2240, 8658, 5491, 3533), 0.018101314184210973, -1e-14) %!assert_equal (hygepdf (25000, 200000, 100000, 50000), ... %! 0.0041202358369094626, -1e-14) %!assert_equal (hygepdf (2, 1000000, 999990, 3), 2.9999460001620005e-05, -1e-14) %!assert_equal (hygepdf (1900, 8658, 5491, 3533), ... %! 3.8155743481754629e-54, -1e-12) ## Test class of input preserved %!assert_equal (hygepdf (single ([x, NaN]), 4, 2, 2), single ([y, NaN]), eps ('single')) %!assert_equal (hygepdf ([x, NaN], single (4), 2, 2), single ([y, NaN]), eps ('single')) %!assert_equal (hygepdf ([x, NaN], 4, single (2), 2), single ([y, NaN]), eps ('single')) %!assert_equal (hygepdf ([x, NaN], 4, 2, single (2)), single ([y, NaN]), eps ('single')) %!assert_equal (hygepdf (single (0), 10, 5, 0), single (1)) ## Test vector expansion %!test %! z = zeros (3,5); %! z([4,5,6,8,9,12]) = [1, 0.5, 1/6, 0.5, 2/3, 1/6]; %! assert_equal (hygepdf (x, 4, [0, 1, 2], 2, 'vectorexpand'), z, 3 * eps); %! assert_equal (hygepdf (x, 4, [0, 1, 2]', 2, 'vectorexpand'), z, 3 * eps); %! assert_equal (hygepdf (x', 4, [0, 1, 2], 2, 'vectorexpand'), z, 3 * eps); %! assert_equal (hygepdf (2, 4, [0 ,1, 2], 2, 'vectorexpand'), z(:,4), 3 * eps); %! assert_equal (hygepdf (x, 4, 1, 2, 'vectorexpand'), z(2,:), 3 *eps); %! assert_equal (hygepdf ([NaN, x], 4, [0 1 2]', 2, 'vectorexpand'), [NaN(3, 1), z], 3 * eps); ## Test input validation %!error hygepdf () %!error hygepdf (1) %!error hygepdf (1,2) %!error hygepdf (1,2,3) %!error ... %! hygepdf (1, ones (3), ones (2), ones (2)) %!error ... %! hygepdf (1, ones (2), ones (3), ones (2)) %!error ... %! hygepdf (1, ones (2), ones (2), ones (3)) %!error hygepdf (true, 2, 2, 2) %!error hygepdf ('a', 2, 2, 2) %!assert_equal (class (hygepdf (int32 (2), 2, 2, 2)), 'double') %!error hygepdf (i, 2, 2, 2) %!error hygepdf (2, i, 2, 2) %!error hygepdf (2, 2, i, 2) %!error hygepdf (2, 2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/hygernd.m000066400000000000000000000207471527311104300246350ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1997-2016 Kurt Hornik ## Copyright (C) 2022 Nicholas R. Jankowski ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} hygernd (@var{m}, @var{k}, @var{n}) ## @deftypefnx {statistics} {@var{r} =} hygernd (@var{m}, @var{k}, @var{n}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} hygernd (@var{m}, @var{k}, @var{n}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} hygernd (@var{m}, @var{k}, @var{n}, [@var{sz}]) ## ## Random arrays from the hypergeometric distribution. ## ## @code{@var{r} = hygernd (@var{m}, @var{k}, @var{n})} returns an array of ## random numbers chosen from the hypergeometric distribution with parameters ## @var{m}, @var{k}, and @var{n}. The size of @var{r} is the common size of ## @var{m}, @var{k}, and @var{n}. A scalar input functions as a constant matrix ## of the same size as the other inputs. ## ## The parameters @var{m}, @var{k}, and @var{n} must be non-negative integers ## with @var{k} and @var{n} not greater than @var{m}. A sample of size ## @qcode{0} contains no marked items, so @var{r} is then @qcode{0}. ## ## When called with a single size argument, @code{hygernd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the hypergeometric distribution can be found at ## @url{https://en.wikipedia.org/wiki/Hypergeometric_distribution} ## ## @seealso{hygecdf, hygeinv, hygepdf, hygestat} ## @end deftypefn function r = hygernd (m, k, n, varargin) ## Check for valid number of input arguments if (nargin < 3) error ("hygernd: function called with too few input arguments."); endif ## Check for common size of T, M, and N if (! isscalar (m) || ! isscalar (k) || ! isscalar (n)) [retval, m, k, n] = common_size (m, k, n); if (retval > 0) error ("hygernd: T, M, and N must be of common size or scalars."); endif endif ## Check for T, M, and N being reals if (iscomplex (m) || iscomplex (k) || iscomplex (n)) error ("hygernd: T, M, and N must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 3) sz = size (m); elseif (nargin == 4) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("hygernd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 4) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("hygernd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (m) && ! isequal (size (m), size (ones (sz)))) error ("hygernd: T, M, and N must be scalars or of size SZ."); endif ## Check for class type if (isa (m, 'single') || isa (k, 'single') || isa (n, 'single')) cls = 'single'; else cls = 'double'; endif ok = ((m >= 0) & (k >= 0) & (n >= 0) & (k <= m) & (n <= m) & (m == fix (m)) & (k == fix (k)) & (n == fix (n))); ## Generate random sample from the hypergeometric distribution if (isscalar (m)) if (ok) v = 0:n; p = hygepdf (v, m, k, n); r = v(lookup (cumsum (p(1:end-1)) / sum (p), rand (sz)) + 1); r = reshape (r, sz); if (strcmp (cls, 'single')) r = single (r); endif else r = NaN (sz, cls); endif else r = NaN (sz, cls); n = n(ok); num_n = numel (n); v = 0 : max (n(:)); p = cumsum (hygepdf (v, m(ok), k(ok), n, 'vectorexpand'), 2); ## Manual row-wise vectorization of lookup, which returns index of element ## less than or equal to test value, zero if test value is less than lowest ## number, and max index if greater than highest number. end_locs = sub2ind (size (p), [1 : num_n]', n(:) + 1); p = (p ./ p(end_locs)) - rand (num_n, 1); p(p>=0) = NaN; # NaN values ignored by max [p_match, p_match_idx] = max (p, [], 2); p_match_idx(isnan (p_match)) = 0; # rand < min(p) gives NaN, reset to 0 r(ok) = v(p_match_idx + 1); endif endfunction ## Test output %!assert_equal (size (hygernd (4, 2, 2)), [1, 1]) %!assert_equal (hygernd (10, 5, 0), 0) %!assert_equal (hygernd (10, 5, 0, 1, 3), [0, 0, 0]) %!assert_equal (hygernd (0, 0, 0), 0) %!assert_equal (hygernd ([10, 0], [5, 0], [0, 0]), [0, 0]) %!assert_equal (hygernd (10, 0, 3), 0) %!assert_equal (hygernd (10, 5, -1), NaN) %!assert_equal (size (hygernd (4 * ones (2, 1), 2,2)), [2, 1]) %!assert_equal (size (hygernd (4 * ones (2, 2), 2,2)), [2, 2]) %!assert_equal (size (hygernd (4, 2 * ones (2, 1), 2)), [2, 1]) %!assert_equal (size (hygernd (4, 2 * ones (2, 2), 2)), [2, 2]) %!assert_equal (size (hygernd (4, 2, 2 * ones (2, 1))), [2, 1]) %!assert_equal (size (hygernd (4, 2, 2 * ones (2, 2))), [2, 2]) %!assert_equal (size (hygernd (4, 2, 2, 3)), [3, 3]) %!assert_equal (size (hygernd (4, 2, 2, [4, 1])), [4, 1]) %!assert_equal (size (hygernd (4, 2, 2, 4, 1)), [4, 1]) %!assert_equal (size (hygernd (4, 2, 2, [])), [0, 0]) %!assert_equal (size (hygernd (4, 2, 2, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (hygernd (1, 2, 3, -1)), [0, 0]) %!assert_equal (size (hygernd (1, 2, 3, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (hygernd (1, 2, 3, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (hygernd (4, 2, 2)), "double") %!assert_equal (class (hygernd (single (4), 2, 2)), "single") %!assert_equal (class (hygernd (single ([4, 4]), 2, 2)), "single") %!assert_equal (class (hygernd (4, single (2), 2)), "single") %!assert_equal (class (hygernd (4, single ([2, 2]),2)), "single") %!assert_equal (class (hygernd (4, 2, single (2))), "single") %!assert_equal (class (hygernd (4, 2, single ([2, 2]))), "single") ## Test input validation %!error hygernd () %!error hygernd (1) %!error hygernd (1, 2) %!error ... %! hygernd (ones (3), ones (2), ones (2)) %!error ... %! hygernd (ones (2), ones (3), ones (2)) %!error ... %! hygernd (ones (2), ones (2), ones (3)) %!error hygernd (i, 2, 3) %!error hygernd (1, i, 3) %!error hygernd (1, 2, i) %!error ... %! hygernd (1, 2, 3, 1.2) %!error ... %! hygernd (1, 2, 3, ones (2)) %!error ... %! hygernd (1, 2, 3, [2 0 2.5]) %!error ... %! hygernd (1, 2, 3, 2, 1.5, 5) %!error ... %! hygernd (2, ones (2), 2, 3) %!error ... %! hygernd (2, ones (2), 2, [3, 2]) %!error ... %! hygernd (2, ones (2), 2, 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/invgcdf.m000066400000000000000000000161011527311104300246020ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} invgcdf (@var{x}, @var{mu}, @var{lambda}) ## @deftypefnx {statistics} {@var{p} =} invgcdf (@var{x}, @var{mu}, @var{lambda}, @qcode{'upper'}) ## ## Inverse Gaussian cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the inverse Gaussian distribution with scale parameter @var{mu} and ## shape parameter @var{lambda}. The size of @var{p} is the common size of ## @var{x}, @var{mu} and @var{lambda}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## @code{@var{p} = invgcdf (@var{x}, @var{mu}, @var{lambda}, "upper")} computes ## the upper tail probability of the inverse Gaussian distribution with ## parameters @var{mu} and @var{lambda}, at the values in @var{x}. ## ## The inverse Gaussian CDF is only defined for @qcode{@var{mu} > 0} and ## @qcode{@var{lambda} > 0}. ## ## Further information about the inverse Gaussian distribution can be found at ## @url{https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{invginv, invgpdf, invgrnd, invgfit, invglike, invgstat} ## @end deftypefn function p = invgcdf (x, mu, lambda, uflag) ## Check for valid number of input arguments if (nargin < 3) error ("invgcdf: function called with too few input arguments."); endif ## Check for valid "upper" flag if (nargin > 3) if (! strcmpi (uflag, 'upper')) error ("invgcdf: invalid argument for upper tail."); else uflag = true; endif else uflag = false; endif ## Check for common size of X, MU, and LAMBDA if (! isscalar (x) || ! isscalar (mu) || ! isscalar (lambda)) [err, x, mu, lambda] = common_size (x, mu, lambda); if (err > 0) error ("invgcdf: X, MU, and LAMBDA must be of common size or scalars."); endif endif ## Check for X, MU, and LAMBDA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (lambda))) error ("invgcdf: X, MU, and LAMBDA must be double or single."); endif ## Check for X, MU, and LAMBDA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (lambda)) error ("invgcdf: X, MU, and LAMBDA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (lambda, 'single')) is_class = 'single'; else is_class = 'double'; endif ## Prepare output p = zeros (size (x), is_class); ## Return NaNs for out of range values of MU and LAMBDA parameters mu(mu <= 0) = NaN; lambda(lambda <= 0) = NaN; ## Check for valid support of X is_zero = (x <= 0); x(is_zero) = realmin; is_inf = (x == Inf); ## Calculate z1, z2 z1 = sqrt (lambda ./ x) .* (x ./ mu - 1); z2 = -sqrt (lambda ./ x) .* (x ./ mu + 1); ## Compute the CDF if the inverse Gaussian if (uflag) p = 0.5 .* erfc (z1 ./ sqrt (2)) - ... exp (2 .* lambda ./ mu) .* 0.5 .* erfc (-z2 ./ sqrt (2)); p(is_zero) = 1; p(is_inf) = 0; else p = 0.5 .* erfc (-z1 ./ sqrt (2)) + ... exp (2 .* lambda ./ mu) .* 0.5 .* erfc (-z2 ./ sqrt (2)); p(is_zero) = 0; p(is_inf) = 1; endif endfunction %!demo %! ## Plot various CDFs from the inverse Gaussian distribution %! x = 0:0.001:3; %! p1 = invgcdf (x, 1, 0.2); %! p2 = invgcdf (x, 1, 1); %! p3 = invgcdf (x, 1, 3); %! p4 = invgcdf (x, 3, 0.2); %! p5 = invgcdf (x, 3, 1); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', x, p4, '-c', x, p5, '-y') %! grid on %! xlim ([0, 3]) %! legend ({'μ = 1, σ = 0.2', 'μ = 1, σ = 1', 'μ = 1, σ = 3', ... %! 'μ = 3, σ = 0.2', 'μ = 3, σ = 1'}, 'location', 'southeast') %! title ('Inverse Gaussian CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, p1, p1u, y2, y2u, y3, y3u %! x = [-Inf, -1, 0, 1/2, 1, Inf]; %! p1 = [0, 0, 0, 0.3650, 0.6681, 1]; %! p1u = [1, 1, 1, 0.6350, 0.3319, 0]; %!assert_equal (invgcdf (x, ones (1,6), ones (1,6)), p1, 1e-4) %!assert_equal (invgcdf (x, 1, 1), p1, 1e-4) %!assert_equal (invgcdf (x, 1, ones (1,6)), p1, 1e-4) %!assert_equal (invgcdf (x, ones (1,6), 1), p1, 1e-4) %!assert_equal (invgcdf (x, 1, [1, 1, 1, NaN, 1, 1]), [p1(1:3), NaN, p1(5:6)], 1e-4) %!assert_equal (invgcdf (x, [1, 1, 1, NaN, 1, 1], 1), [p1(1:3), NaN, p1(5:6)], 1e-4) %!assert_equal (invgcdf ([x(1:3), NaN, x(5:6)], 1, 1), [p1(1:3), NaN, p1(5:6)], 1e-4) %!assert_equal (invgcdf (x, ones (1,6), ones (1,6), 'upper'), p1u, 1e-4) %!assert_equal (invgcdf (x, 1, 1, 'upper'), p1u, 1e-4) %!assert_equal (invgcdf (x, 1, ones (1,6), 'upper'), p1u, 1e-4) %!assert_equal (invgcdf (x, ones (1,6), 1, 'upper'), p1u, 1e-4) ## Test class of input preserved %!assert_equal (class (invgcdf (single ([x, NaN]), 1, 1)), "single") %!assert_equal (class (invgcdf ([x, NaN], 1, single (1))), "single") %!assert_equal (class (invgcdf ([x, NaN], single (1), 1)), "single") ## Test input validation %!error invgcdf () %!error invgcdf (1) %!error invgcdf (1, 2) %!error invgcdf (1, 2, 3, 'tail') %!error invgcdf (1, 2, 3, 5) %!error ... %! invgcdf (ones (3), ones (2), ones (2)) %!error ... %! invgcdf (ones (2), ones (3), ones (2)) %!error ... %! invgcdf (ones (2), ones (2), ones (3)) %!error invgcdf (int32 (2), 2, 3) %!error invgcdf (true, 2, 3) %!error invgcdf ('a', 2, 3) %!error invgcdf (i, 2, 3) %!error invgcdf (1, i, 3) %!error invgcdf (1, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/invginv.m000066400000000000000000000165231527311104300246520ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} invginv (@var{p}, @var{mu}, @var{lambda}) ## ## Inverse of the inverse Gaussian cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the inverse Gaussian distribution with scale parameter @var{mu} and shape ## parameter @var{lambda}. The size of @var{x} is the common size of @var{p}, ## @var{mu}, and @var{lambda}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## The inverse Gaussian CDF is only defined for @qcode{@var{mu} > 0} and ## @qcode{@var{lambda} > 0}. ## ## Further information about the inverse Gaussian distribution can be found at ## @url{https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{invgcdf, invgpdf, invgrnd, invgfit, invglike, invgstat} ## @end deftypefn function x = invginv (p, mu, lambda) ## Check for valid number of input arguments if (nargin < 3) error ("invginv: function called with too few input arguments."); endif ## Check for common size of P, MU, and LAMBDA if (! isscalar (p) || ! isscalar (mu) || ! isscalar (lambda)) [retval, p, mu, lambda] = common_size (p, mu, lambda); vec = true; if (retval > 0) error ("invginv: P, MU, and LAMBDA must be of common size or scalars."); endif else vec = false; endif ## Check for P, MU, and LAMBDA being double or single if (! (isfloat (p) && isfloat (mu) && isfloat (lambda))) error ("invginv: P, MU, and LAMBDA must be double or single."); endif ## Check for X, MU, and LAMBDA being reals if (iscomplex (p) || iscomplex (mu) || iscomplex (lambda)) error ("invginv: P, MU, and LAMBDA must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (mu, 'single') || isa (lambda, 'single')); x = NaN (size (p), 'single'); else x = NaN (size (p)); endif ## Return NaNs for out of range values of MU and LAMBDA parameters mu(mu <= 0) = NaN; lambda(lambda <= 0) = NaN; ## Find valid parameters and p-values (handle edges cases below) validmulambda = (mu > 0) & (lambda > 0) & (lambda < Inf); validp_values = (validmulambda & (p > 0) & (p < 1)); valid_all = all (validp_values(:)); valid_any = any (validp_values(:)); ## Handle edges cases here if (! valid_all) x(p == 0 & validmulambda) = 0; x(p == 1 & validmulambda) = Inf; ## Keep valid cases (if any left) if (valid_any) if (vec) p = p(validp_values); mu = mu(validp_values); lambda = lambda(validp_values); endif else return; endif endif ## Apply Newton's Method to find a root of p = invgcdf(x,mu,lambda) ## Choose a starting guess for x0. Use quantiles from a lognormal ## distribution with the same mean (==1) and variance (==lambda0) lambda0 = lambda ./ mu; lognorm = log (1 ./ lambda0 + 1); mulnorm = -0.5 .* lognorm; x0 = exp (mulnorm - sqrt (2 .* lognorm) .* erfcinv (2 * p)); ## Set maximum iterations and tolerance for Newton's Method mit = 500; tol = eps (class (x0)) .^ (3/4); ## Get quantiles F = invgcdf (x0, 1, lambda0); dF = F - p; for it = 1:mit ## Compute the Newton step f = invgpdf (x0, 1, lambda0); h = dF ./ f; x0_1 = max (x0/10, min (10 * x0, x0 - h)); ## Check if tolerance is reached complete = (abs (h) <= tol * x0); if (all (complete(:))) x0 = x0_1; break endif ## Check for increasing error unless tolerance is reached dFold = dF; for j = 1:25 F = invgcdf (x0_1, 1, lambda0); dF = F - p; worse = (abs (dF) > abs (dFold)) & ! complete; if (! any (worse(:))) break endif x0_1(worse) = (x0(worse) + x0_1(worse)) / 2; endfor ## Update for next step x0 = x0_1; endfor ## Issue a warning for exceeding iterations or not converging to tolerance notconv = (abs (dF./F) > tol.^(2/3)); if (it > mit || any (notconv(:))) warning (strcat ("invginv: Newton's Method did not converge", ... " or exceeded maximum iterations.")); endif ## Apply the scale factor if (valid_all) x = x0 .* mu; else x(validp_values) = x0 .* mu; endif endfunction %!demo %! ## Plot various iCDFs from the inverse Gaussian distribution %! p = 0.001:0.001:0.999; %! x1 = invginv (p, 1, 0.2); %! x2 = invginv (p, 1, 1); %! x3 = invginv (p, 1, 3); %! x4 = invginv (p, 3, 0.2); %! x5 = invginv (p, 3, 1); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', p, x4, '-c', p, x5, '-y') %! grid on %! ylim ([0, 3]) %! legend ({'μ = 1, σ = 0.2', 'μ = 1, σ = 1', 'μ = 1, σ = 3', ... %! 'μ = 3, σ = 0.2', 'μ = 3, σ = 1'}, 'location', 'northwest') %! title ('Inverse Gaussian iCDF') %! xlabel ('probability') %! ylabel ('x') ## Test output %!shared p, x %! p = [0, 0.3829, 0.6827, 1]; %! x = [0, 0.5207, 1.0376, Inf]; %!assert_equal (invginv (p, 1, 1), x, 1e-4); %!assert_equal (invginv (p, 1, ones (1,4)), x, 1e-4); %!assert_equal (invginv (p, 1, [-1, 0, 1, 1]), [NaN, NaN, x(3:4)], 1e-4) %!assert_equal (invginv (p, [-1, 0, 1, 1], 1), [NaN, NaN, x(3:4)], 1e-4) ## Test class of input preserved %!assert_equal (class (invginv (single ([p, NaN]), 0, 1)), "single") %!assert_equal (class (invginv ([p, NaN], single (0), 1)), "single") %!assert_equal (class (invginv ([p, NaN], 0, single (1))), "single") ## Test input validation %!error invginv (1) %!error invginv (1, 2) %!error ... %! invginv (1, ones (2), ones (3)) %!error ... %! invginv (ones (2), 1, ones (3)) %!error ... %! invginv (ones (2), ones (3), 1) %!error invginv (int32 (2), 2, 3) %!error invginv (true, 2, 3) %!error invginv ('a', 2, 3) %!error invginv (i, 2, 3) %!error invginv (1, i, 3) %!error invginv (1, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/invgpdf.m000066400000000000000000000127451527311104300246310ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} invgpdf (@var{x}, @var{mu}, @var{lambda}) ## ## Inverse Gaussian probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the inverse Gaussian distribution with scale parameter @var{mu} and shape ## parameter @var{lambda}. The size of @var{y} is the common size of @var{x}, ## @var{mu}, and @var{lambda}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## The inverse Gaussian CDF is only defined for @qcode{@var{mu} > 0} and ## @qcode{@var{lambda} > 0}. ## ## Further information about the inverse Gaussian distribution can be found at ## @url{https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{invgcdf, invginv, invgrnd, invgfit, invglike, invgstat} ## @end deftypefn function y = invgpdf (x, mu, lambda) ## Check for valid number of input arguments if (nargin < 3) error ("invgpdf: function called with too few input arguments."); endif ## Check for common size of X, MU, and LAMBDA if (! isscalar (x) || ! isscalar (mu) || ! isscalar (lambda)) [retval, x, mu, lambda] = common_size (x, mu, lambda); if (retval > 0) error ("invgpdf: X, MU, and LAMBDA must be of common size or scalars."); endif endif ## Check for X, MU, and LAMBDA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (lambda))) error ("invgpdf: X, MU, and LAMBDA must be double or single."); endif ## Check for X, MU, and LAMBDA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (lambda)) error ("invgpdf: X, MU, and LAMBDA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (lambda, 'single')); is_class = 'single'; else is_class = 'double'; endif ## Return NaNs for out of range values of MU and LAMBDA parameters mu(mu <= 0) = NaN; lambda(lambda <= 0) = NaN; ## Check for valid support of X is_zero = (x <= 0); x(is_zero) = realmin; ## Compute inverse Gaussian PDF y = sqrt (lambda ./ (2 .* pi .* x .^ 3)) .* ... exp (-0.5 .* lambda .* (x ./ mu - 2 + mu ./ x) ./ mu); ## Force zero for unsupported X but valid parameters k0 = is_zero & mu > 0 & lambda > 0; y(k0) = 0; ## Cast to appropriate class y = cast (y, is_class); endfunction %!demo %! ## Plot various PDFs from the inverse Gaussian distribution %! x = 0:0.001:3; %! y1 = invgpdf (x, 1, 0.2); %! y2 = invgpdf (x, 1, 1); %! y3 = invgpdf (x, 1, 3); %! y4 = invgpdf (x, 3, 0.2); %! y5 = invgpdf (x, 3, 1); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', x, y4, '-c', x, y5, '-y') %! grid on %! xlim ([0, 3]) %! ylim ([0, 3]) %! legend ({'μ = 1, σ = 0.2', 'μ = 1, σ = 1', 'μ = 1, σ = 3', ... %! 'μ = 3, σ = 0.2', 'μ = 3, σ = 1'}, 'location', 'northeast') %! title ('Inverse Gaussian PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x, y %! x = [-Inf, -1, 0, 1/2, 1, Inf]; %! y = [0, 0, 0, 0.8788, 0.3989, 0]; %!assert_equal (invgpdf ([x, NaN], 1, 1), [y, NaN], 1e-4) %!assert_equal (invgpdf (x, 1, [-2, -1, 0, 1, 1, 1]), [nan(1,3), y([4:6])], 1e-4) ## Test class of input preserved %!assert_equal (class (hncdf (single ([x, NaN]), 1, 1)), "single") %!assert_equal (class (hncdf ([x, NaN], 1, single (1))), "single") %!assert_equal (class (hncdf ([x, NaN], single (1), 1)), "single") ## Test input validation %!error invgpdf () %!error invgpdf (1) %!error invgpdf (1, 2) %!error ... %! invgpdf (1, ones (2), ones (3)) %!error ... %! invgpdf (ones (2), 1, ones (3)) %!error ... %! invgpdf (ones (2), ones (3), 1) %!error invgpdf (int32 (2), 2, 3) %!error invgpdf (true, 2, 3) %!error invgpdf ('a', 2, 3) %!error invgpdf (i, 2, 3) %!error invgpdf (1, i, 3) %!error invgpdf (1, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/invgrnd.m000066400000000000000000000163011527311104300246330ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} invgrnd (@var{mu}, @var{lambda}) ## @deftypefnx {statistics} {@var{r} =} invgrnd (@var{mu}, @var{lambda}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} invgrnd (@var{mu}, @var{lambda}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} invgrnd (@var{mu}, @var{lambda}, [@var{sz}]) ## ## Random arrays from the inverse Gaussian distribution. ## ## @code{@var{r} = invgrnd (@var{mu}, @var{lambda})} returns an array of random ## numbers chosen from the inverse Gaussian distribution with location parameter ## @var{mu} and scale parameter @var{lambda}. The size of @var{r} is the common ## size of @var{mu} and @var{lambda}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## When called with a single size argument, @code{invgrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## The inverse Gaussian CDF is only defined for @qcode{@var{mu} > 0} and ## @qcode{@var{lambda} > 0}. ## ## Further information about the inverse Gaussian distribution can be found at ## @url{https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution} ## ## @seealso{invgcdf, invginv, invgpdf, invgfit, invglike, invgstat} ## @end deftypefn function r = invgrnd (mu, lambda, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("invgrnd: function called with too few input arguments."); endif ## Check for common size of MU, and LAMBDA if (! isscalar (mu) || ! isscalar (lambda)) [retval, mu, lambda] = common_size (mu, lambda); vec = true; if (retval > 0) error ("invgrnd: MU and LAMBDA must be of common size or scalars."); endif else vec = false; endif ## Check for X, MU, and LAMBDA being reals if (iscomplex (mu) || iscomplex (lambda)) error ("invgrnd: MU and LAMBDA must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (mu); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("invgrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("invgrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (mu) && ! isequal (size (mu), size (ones (sz)))) error ("invgrnd: MU and LAMBDA must be scalars or of size SZ."); endif ## Check for class type if (isa (mu, 'single') || isa (lambda, 'single')) cls = 'single'; else cls = 'double'; endif ## Expand parameters (if needed) if (! vec) mu = repmat (mu, sz); lambda = repmat (lambda, sz); endif ## Generate random sample from inverse Gaussian distribution v = randn (sz, cls); y = v .^ 2; r = mu + (mu .^ 2 .* y) ./ (2 .* lambda) - (mu ./ (2 .* lambda)) .* ... sqrt (4 * mu .* lambda .* y + mu .* mu .* y .* y); inver = (rand (sz) .* (mu + r) > mu); r(inver) = mu(inver) .^2 ./ r(inver); ## Force output to NaN for invalid parameters MU and LAMBDA k = (mu <= 0 | lambda <= 0); r(k) = NaN; endfunction ## Test results %!assert_equal (size (invgrnd (1, 1, 1)), [1, 1]) %!assert_equal (size (invgrnd (1, 1, 2)), [2, 2]) %!assert_equal (size (invgrnd (1, 1, [2, 1])), [2, 1]) %!assert_equal (size (invgrnd (1, zeros (2, 2))), [2, 2]) %!assert_equal (size (invgrnd (1, ones (2, 1))), [2, 1]) %!assert_equal (size (invgrnd (1, ones (2, 2))), [2, 2]) %!assert_equal (size (invgrnd (ones (2, 1), 1)), [2, 1]) %!assert_equal (size (invgrnd (ones (2, 2), 1)), [2, 2]) %!assert_equal (size (invgrnd (1, 1, 3)), [3, 3]) %!assert_equal (size (invgrnd (1, 1, [4 1])), [4, 1]) %!assert_equal (size (invgrnd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (invgrnd (1, 1, [])), [0, 0]) %!assert_equal (size (invgrnd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (invgrnd (1, 2, -1)), [0, 0]) %!assert_equal (size (invgrnd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (invgrnd (1, 2, 2, -1, 5)), [2, 0, 5]) %!test %! r = invgrnd (1, [1, 0, -1]); %! assert_equal (r([2:3]), [NaN, NaN]) ## Test class of input preserved %!assert_equal (class (invgrnd (1, 0)), "double") %!assert_equal (class (invgrnd (1, single (0))), "single") %!assert_equal (class (invgrnd (1, single ([0, 0]))), "single") %!assert_equal (class (invgrnd (1, single (1))), "single") %!assert_equal (class (invgrnd (1, single ([1, 1]))), "single") %!assert_equal (class (invgrnd (single (1), 1)), "single") %!assert_equal (class (invgrnd (single ([1, 1]), 1)), "single") ## Test input validation %!error invgrnd () %!error invgrnd (1) %!error ... %! invgrnd (ones (3), ones (2)) %!error ... %! invgrnd (ones (2), ones (3)) %!error invgrnd (i, 2, 3) %!error invgrnd (1, i, 3) %!error ... %! invgrnd (1, 2, 1.2) %!error ... %! invgrnd (1, 2, ones (2)) %!error ... %! invgrnd (1, 2, [2 0 2.5]) %!error ... %! invgrnd (1, 2, 2, 1.5, 5) %!error ... %! invgrnd (2, ones (2), 3) %!error ... %! invgrnd (2, ones (2), [3, 2]) %!error ... %! invgrnd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/iwishpdf.m000066400000000000000000000070321527311104300250020ustar00rootroot00000000000000## Copyright (C) 2013 Nir Krakauer ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} iwishpdf (@var{W}, @var{Tau}, @var{df}, @var{log_y}=false) ## ## Compute the probability density function of the inverse Wishart distribution. ## ## Inputs: A @var{p} x @var{p} matrix @var{W} where to find the PDF and the ## @var{p} x @var{p} positive definite scale matrix @var{Tau} and scalar degrees ## of freedom parameter @var{df} characterizing the inverse Wishart ## distribution. (For the density to be finite, need @var{df} > (@var{p} - 1).) ## If the flag @var{log_y} is set, return the log probability density -- this ## helps avoid underflow when the numerical value of the density is very small. ## ## Output: @var{y} is the probability density of Wishart(@var{Sigma}, @var{df}) ## at @var{W}. ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{iwishrnd, wishpdf, wishrnd} ## @end deftypefn function y = iwishpdf (W, Tau, df, log_y=false) if (nargin < 3) print_usage (); endif ## Check for W, TAU, and DF being double or single if (! (isfloat (W) && isfloat (Tau) && isfloat (df))) error ("iwishpdf: W, TAU, and DF must be double or single."); endif p = size (Tau, 1); if (df <= (p - 1)) error ("iwishpdf: DF too small, no finite densities exist."); endif ## Calculate the logarithm of G_d(df/2), the multivariate gamma function g = (p * (p - 1) / 4) * log (pi); for i = 1:p g = g + log (gamma ((df + (1 - i)) / 2)); endfor C = chol (W); ## Use formulas for determinant of positive definite matrix for better ## efficiency and numerical accuracy logdet_W = 2*sum (log (diag (C))); logdet_Tau = 2*sum (log (diag (chol (Tau)))); y = -(df * p) / 2 * log (2) + (df / 2) * logdet_Tau - g ... -((df + p + 1) / 2) * logdet_W - trace (Tau * chol2inv (C)) / 2; if (! log_y) y = exp (y); endif endfunction ## Test results cross-checked against diwish function in R MCMCpack library %!assert_equal (iwishpdf (4, 3, 3.1), 0.04226595, 1E-7); %!assert_equal (iwishpdf ([2 -0.3;-0.3 4], [1 0.3;0.3 1], 4), 1.60166e-05, 1E-10); %!assert_equal (iwishpdf ([6 2 5; 2 10 -5; 5 -5 25], ... %! [9 5 5; 5 10 -8; 5 -8 22], 5.1), 4.946831e-12, 1E-17); ## Test input validation %!error iwishpdf (int32 (eye (2)), eye (2), 3) %!error iwishpdf (true (2), eye (2), 3) %!error iwishpdf (['ab'; 'cd'], eye (2), 3) %!error iwishpdf () %!error iwishpdf (1, 2) %!error iwishpdf (1, 2, 0) statistics-release-2.1.2/inst/Distribution_Functions/iwishrnd.m000066400000000000000000000057141527311104300250210ustar00rootroot00000000000000## Copyright (C) 2013 Nir Krakauer ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{W}, @var{DI}] =} iwishrnd (@var{Tau}, @var{df}, @var{DI}, @var{n}=1) ## ## Return a random matrix sampled from the inverse Wishart distribution with ## given parameters. ## ## Inputs: the @math{p * p} positive definite matrix @var{Tau} and scalar ## degrees of freedom parameter @var{df} (and optionally the transposed Cholesky ## factor @var{DI} of @var{Sigma} = @code{inv(Tau)}). ## ## @var{df} can be non-integer as long as @math{@var{df} > d} ## ## Output: a random @math{p * p} matrix @var{W} from the inverse ## Wishart(@var{Tau}, @var{df}) distribution. (@code{inv(W)} is from the ## Wishart(@code{inv(Tau)}, @var{df}) distribution.) If @var{n} > 1, ## then @var{W} is @var{p} x @var{p} x @var{n} and holds @var{n} such random ## matrices. (Optionally, the transposed Cholesky factor @var{DI} of @var{Sigma} ## is also returned.) ## ## Averaged across many samples, the mean of @var{W} should approach ## @var{Tau} / (@var{df} - @var{p} - 1). ## ## @subheading References ## ## @enumerate ## @item ## Yu-Cheng Ku and Peter Bloomfield (2010), Generating Random Wishart Matrices ## with Fractional Degrees of Freedom in OX, ## http://www.gwu.edu/~forcpgm/YuChengKu-030510final-WishartYu-ChengKu.pdf ## @end enumerate ## ## @seealso{iwishpdf, wishpdf, wishrnd} ## @end deftypefn function [W, DI] = iwishrnd (Tau, df, DI, n = 1) if (nargin < 2) print_usage (); endif if (nargin < 3 || isempty (DI)) try D = chol (inv (Tau)); catch error (strcat ("iwishrnd: Cholesky decomposition failed;", ... " TAU probably not positive definite.")); end_try_catch DI = D'; else D = DI'; endif w = wishrnd ([], df, D, n); if (n > 1) p = size (D, 1); W = nan (p, p, n); endif for i = 1:n W(:, :, i) = inv (w(:, :, i)); endfor endfunction %!assert_equal (size (iwishrnd (1,2,1)), [1, 1]); %!assert_equal (size (iwishrnd ([],2,1)), [1, 1]); %!assert_equal (size (iwishrnd ([3 1; 1 3], 2.00001, [], 1)), [2, 2]); %!assert_equal (size (iwishrnd (eye (2), 2, [], 3)), [2, 2, 3]); ## Test input validation %!error iwishrnd () %!error iwishrnd (1) %!error iwishrnd ([-3 1; 1 3],1) %!error iwishrnd ([1; 1],1) statistics-release-2.1.2/inst/Distribution_Functions/jsucdf.m000066400000000000000000000074401527311104300244460ustar00rootroot00000000000000## Copyright (C) 2006 Frederick (Rick) A Niles ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} jsucdf (@var{x}) ## @deftypefnx {statistics} {@var{p} =} jsucdf (@var{x}, @var{alpha1}) ## @deftypefnx {statistics} {@var{p} =} jsucdf (@var{x}, @var{alpha1}, @var{alpha2}) ## ## Johnson SU cumulative distribution function (CDF). ## ## For each element of @var{x}, return the cumulative distribution functions ## (CDF) at @var{x} of the Johnson SU distribution with shape parameters ## @var{alpha1} and @var{alpha2}, which is ## @code{normcdf (@var{alpha1} + @var{alpha2} * asinh (@var{x}))}. The size of ## @var{p} is the common size of the input arguments @var{x}, @var{alpha1}, and ## @var{alpha2}. A scalar input functions as a constant matrix of the same size ## as the other inputs. ## ## Default values are @var{alpha1} = 1, @var{alpha2} = 1. @var{alpha2} must be ## positive; where it is not, the result is @qcode{NaN}. ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{jsupdf} ## @end deftypefn function p = jsucdf (x, alpha1, alpha2) if (nargin < 1 || nargin > 3) print_usage; endif if (nargin == 1) alpha1 = 1; alpha2 = 1; elseif (nargin == 2) alpha2 = 1; endif ## Check for X, ALPHA1, and ALPHA2 being double or single if (! (isfloat (x) && isfloat (alpha1) && isfloat (alpha2))) error ("jsucdf: X, ALPHA1, and ALPHA2 must be double or single."); endif if (! isscalar (x) || ! isscalar (alpha1) || ! isscalar (alpha2)) [retval, x, alpha1, alpha2] = common_size (x, alpha1, alpha2); if (retval > 0) error (strcat ("jsucdf: X, ALPHA1, and ALPHA2 must be of common", ... " size or scalars.")); endif endif p = normcdf (alpha1 + alpha2 .* asinh (x)); p((alpha2 <= 0) & true (size (p))) = NaN; endfunction %!assert_equal (jsucdf (0), normcdf (1), -1e-15) %!assert_equal (jsucdf (1, 0.5, 2), ... %! normcdf (0.5 + 2 * log (1 + sqrt (2))), -1e-14) %!assert_equal (jsucdf (sinh (-2), 0, 1), normcdf (-2), -1e-14) %!assert_equal (jsucdf (-1e8, 0, 1), normcdf (-log (2e8)), -1e-12) %!assert_equal (jsucdf ([-Inf, NaN, Inf]), [0, NaN, 1]) %!assert_equal (jsucdf (single (0)), single (normcdf (1)), -eps ('single')) %!assert_equal (jsucdf (1, 0, 0), NaN) %!assert_equal (jsucdf ([1, 2], 1, -1), [NaN, NaN]) %!assert_equal (jsucdf (1, 1, [1, 0]), [jsucdf(1), NaN]) %!error jsucdf (int32 (2), 1, 1) %!error jsucdf (true, 1, 1) %!error jsucdf ('a', 1, 1) %!error jsucdf () %!error jsucdf (1, 2, 3, 4) %!error ... %! jsucdf (1, ones (2), ones (3)) statistics-release-2.1.2/inst/Distribution_Functions/jsupdf.m000066400000000000000000000075151527311104300244660ustar00rootroot00000000000000## Copyright (C) 2006 Frederick (Rick) A Niles ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} jsupdf (@var{x}) ## @deftypefnx {statistics} {@var{y} =} jsupdf (@var{x}, @var{alpha1}) ## @deftypefnx {statistics} {@var{y} =} jsupdf (@var{x}, @var{alpha1}, @var{alpha2}) ## ## Johnson SU probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## at @var{x} of the Johnson SU distribution with shape parameters @var{alpha1} ## and @var{alpha2}, which is ## @code{@var{alpha2} * normpdf (@var{alpha1} + @var{alpha2} * asinh (@var{x})) ## / sqrt (@var{x}^2 + 1)}. The size of @var{y} is the common size of the input ## arguments @var{x}, @var{alpha1}, and @var{alpha2}. A scalar input functions ## as a constant matrix of the same size as the other inputs. ## ## Default values are @var{alpha1} = 1, @var{alpha2} = 1. @var{alpha2} must be ## positive; where it is not, the result is @qcode{NaN}. ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{jsucdf} ## @end deftypefn function y = jsupdf (x, alpha1, alpha2) if (nargin < 1 || nargin > 3) print_usage; endif if (nargin == 1) alpha1 = 1; alpha2 = 1; elseif (nargin == 2) alpha2 = 1; endif ## Check for X, ALPHA1, and ALPHA2 being double or single if (! (isfloat (x) && isfloat (alpha1) && isfloat (alpha2))) error ("jsupdf: X, ALPHA1, and ALPHA2 must be double or single."); endif if (! isscalar (x) || ! isscalar (alpha1) || ! isscalar (alpha2)) [retval, x, alpha1, alpha2] = common_size (x, alpha1, alpha2); if (retval > 0) error (strcat ("jsupdf: X, ALPHA1, and ALPHA2 must be of common", ... " size or scalars.")); endif endif y = alpha2 ./ hypot (x, 1) .* normpdf (alpha1 + alpha2 .* asinh (x)); y((alpha2 <= 0) & true (size (y))) = NaN; endfunction %!assert_equal (jsupdf (0), normpdf (1), -1e-15) %!assert_equal (jsupdf (1, 0.5, 2), ... %! sqrt (2) * normpdf (0.5 + 2 * log (1 + sqrt (2))), -1e-14) %!assert_equal (jsupdf ([-Inf, NaN, Inf]), [0, NaN, 0]) %!assert_equal (integral (@(x) jsupdf (x, 0.5, 2), -Inf, 1), ... %! jsucdf (1, 0.5, 2), -1e-9) %!assert_equal (jsupdf (single (0)), single (normpdf (1)), -eps ('single')) %!assert_equal (jsupdf (1, 0, 0), NaN) %!assert_equal (jsupdf ([1, 2], 1, -1), [NaN, NaN]) %!assert_equal (jsupdf (1, 1, [1, 0]), [jsupdf(1), NaN]) %!error jsupdf (int32 (2), 1, 1) %!error jsupdf (true, 1, 1) %!error jsupdf ('a', 1, 1) %!error jsupdf () %!error jsupdf (1, 2, 3, 4) %!error ... %! jsupdf (1, ones (2), ones (3)) statistics-release-2.1.2/inst/Distribution_Functions/laplacecdf.m000066400000000000000000000145421527311104300252470ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} laplacecdf (@var{x}, @var{mu}, @var{beta}) ## @deftypefnx {statistics} {@var{p} =} laplacecdf (@var{x}, @var{mu}, @var{beta}, @qcode{'upper'}) ## ## Laplace cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the Laplace distribution with location parameter @var{mu} and scale ## parameter (i.e. "diversity") @var{beta}. The size of @var{p} is the common ## size of @var{x}, @var{mu}, and @var{beta}. A scalar input functions as a ## constant matrix of the same size as the other inputs. ## ## Both parameters must be reals and @qcode{@var{beta} > 0}. ## For @qcode{@var{beta} <= 0}, @qcode{NaN} is returned. ## ## @code{@var{p} = laplacecdf (@var{x}, @var{mu}, @var{beta}, "upper")} computes ## the upper tail probability of the Laplace distribution with parameters ## @var{mu} and @var{beta}, at the values in @var{x}. ## ## Further information about the Laplace distribution can be found at ## @url{https://en.wikipedia.org/wiki/Laplace_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{laplaceinv, laplacepdf, laplacernd} ## @end deftypefn function p = laplacecdf (x, mu, beta, uflag) ## Check for valid number of input arguments if (nargin < 3) error ("laplacecdf: function called with too few input arguments."); endif ## Check for valid "upper" flag if (nargin > 3) if (! strcmpi (uflag, 'upper')) error ("laplacecdf: invalid argument for upper tail."); else uflag = true; endif else uflag = false; endif ## Check for common size of X, MU, and BETA if (! isscalar (x) || ! isscalar (mu) || ! isscalar (beta)) [retval, x, mu, beta] = common_size (x, mu, beta); if (retval > 0) error (strcat ("laplacecdf: X, MU, and BETA must be of", ... " common size or scalars.")); endif endif ## Check for X, MU, and BETA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (beta))) error ("laplacecdf: X, MU, and BETA must be double or single."); endif ## Check for X, MU, and BETA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (beta)) error ("laplacecdf: X, MU, and BETA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (beta, 'single')); p = NaN (size (x), 'single'); else p = NaN (size (x)); endif ## Find normal and edge cases k1 = (x == -Inf) & (beta > 0); k2 = (x == Inf) & (beta > 0); k = ! k1 & ! k2 & (beta > 0); ## Compute Laplace CDF if (uflag) p(k1) = 1; p(k2) = 0; p(k) = (1 + sign (-x(k) + mu(k)) .* ... (1 - exp (- abs (-x(k) + mu(k)) ./ beta(k)))) ./ 2; else p(k1) = 0; p(k2) = 1; p(k) = (1 + sign (x(k) - mu(k)) .* ... (1 - exp (- abs (x(k) - mu(k)) ./ beta(k)))) ./ 2; endif endfunction %!demo %! ## Plot various CDFs from the Laplace distribution %! x = -10:0.01:10; %! p1 = laplacecdf (x, 0, 1); %! p2 = laplacecdf (x, 0, 2); %! p3 = laplacecdf (x, 0, 4); %! p4 = laplacecdf (x, -5, 4); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', x, p4, '-c') %! grid on %! xlim ([-10, 10]) %! legend ({'μ = 0, β = 1', 'μ = 0, β = 2', ... %! 'μ = 0, β = 4', 'μ = -5, β = 4'}, 'location', 'southeast') %! title ('Laplace CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, y %! x = [-Inf, -log(2), 0, log(2), Inf]; %! y = [0, 1/4, 1/2, 3/4, 1]; %!assert_equal (laplacecdf ([x, NaN], 0, 1), [y, NaN]) %!assert_equal (laplacecdf (x, 0, [-2, -1, 0, 1, 2]), [nan(1, 3), 0.75, 1]) ## Test class of input preserved %!assert_equal (laplacecdf (single ([x, NaN]), 0, 1), single ([y, NaN]), eps ('single')) %!assert_equal (laplacecdf ([x, NaN], single (0), 1), single ([y, NaN]), eps ('single')) %!assert_equal (laplacecdf ([x, NaN], 0, single (1)), single ([y, NaN]), eps ('single')) ## Test input validation %!error laplacecdf () %!error laplacecdf (1) %!error ... %! laplacecdf (1, 2) %!error ... %! laplacecdf (1, 2, 3, 4, 5) %!error laplacecdf (1, 2, 3, 'tail') %!error laplacecdf (1, 2, 3, 4) %!error ... %! laplacecdf (ones (3), ones (2), ones (2)) %!error ... %! laplacecdf (ones (2), ones (3), ones (2)) %!error ... %! laplacecdf (ones (2), ones (2), ones (3)) %!error laplacecdf (int32 (2), 2, 2) %!error laplacecdf (true, 2, 2) %!error laplacecdf ('a', 2, 2) %!error laplacecdf (i, 2, 2) %!error laplacecdf (2, i, 2) %!error laplacecdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/laplaceinv.m000066400000000000000000000126031527311104300253030ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} laplaceinv (@var{p}, @var{mu}, @var{beta}) ## ## Inverse of the Laplace cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the Laplace distribution with location parameter @var{mu} and scale parameter ## (i.e. "diversity") @var{beta}. The size of @var{x} is the common size of ## @var{p}, @var{mu}, and @var{beta}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## Both parameters must be reals and @qcode{@var{beta} > 0}. ## For @qcode{@var{beta} <= 0}, @qcode{NaN} is returned. ## ## Further information about the Laplace distribution can be found at ## @url{https://en.wikipedia.org/wiki/Laplace_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{laplacecdf, laplacepdf, laplacernd} ## @end deftypefn function x = laplaceinv (p, mu, beta) ## Check for valid number of input arguments if (nargin < 3) error ("laplaceinv: function called with too few input arguments."); endif ## Check for common size of P, MU, and BETA if (! isscalar (p) || ! isscalar (mu) || ! isscalar (beta)) [retval, p, mu, beta] = common_size (p, mu, beta); if (retval > 0) error (strcat ("laplaceinv: P, MU, and BETA must be of", ... " common size or scalars.")); endif endif ## Check for P, MU, and BETA being double or single if (! (isfloat (p) && isfloat (mu) && isfloat (beta))) error ("laplaceinv: P, MU, and BETA must be double or single."); endif ## Check for X, MU, and BETA being reals if (iscomplex (p) || iscomplex (mu) || iscomplex (beta)) error ("laplaceinv: P, MU, and BETA must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (mu, 'single') || isa (beta, 'single')); x = NaN (size (p), 'single'); else x = NaN (size (p)); endif ## Compute Laplace iCDF k = (p >= 0) & (p <= 1) & (beta > 0); x(k) = mu(k) + beta(k) .* ((p(k) < 1/2) .* log (2 .* p(k)) - ... (p(k) > 1/2) .* log (2 .* (1 - p(k)))); endfunction %!demo %! ## Plot various iCDFs from the Laplace distribution %! p = 0.001:0.001:0.999; %! x1 = cauchyinv (p, 0, 1); %! x2 = cauchyinv (p, 0, 2); %! x3 = cauchyinv (p, 0, 4); %! x4 = cauchyinv (p, -5, 4); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', p, x4, '-c') %! grid on %! ylim ([-10, 10]) %! legend ({'μ = 0, β = 1', 'μ = 0, β = 2', ... %! 'μ = 0, β = 4', 'μ = -5, β = 4'}, 'location', 'northwest') %! title ('Laplace iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p, x %! p = [-1 0 0.5 1 2]; %! x = [NaN, -Inf, 0, Inf, NaN]; %!assert_equal (laplaceinv (p, 0, 1), x) %!assert_equal (laplaceinv (p, 0, [-2, -1, 0, 1, 2]), [nan(1, 3), Inf, NaN]) %!assert_equal (laplaceinv ([p, NaN], 0, 1), [x, NaN]) ## Test class of input preserved %!assert_equal (laplaceinv (single ([p, NaN]), 0, 1), single ([x, NaN])) %!assert_equal (laplaceinv ([p, NaN], single (0), 1), single ([x, NaN])) %!assert_equal (laplaceinv ([p, NaN], 0, single (1)), single ([x, NaN])) ## Test input validation %!error laplaceinv () %!error laplaceinv (1) %!error ... %! laplaceinv (1, 2) %!error laplaceinv (1, 2, 3, 4) %!error ... %! laplaceinv (1, ones (2), ones (3)) %!error ... %! laplaceinv (ones (2), 1, ones (3)) %!error ... %! laplaceinv (ones (2), ones (3), 1) %!error laplaceinv (int32 (2), 2, 3) %!error laplaceinv (true, 2, 3) %!error laplaceinv ('a', 2, 3) %!error laplaceinv (i, 2, 3) %!error laplaceinv (1, i, 3) %!error laplaceinv (1, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/laplacepdf.m000066400000000000000000000125231527311104300252610ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} laplacepdf (@var{x}, @var{mu}, @var{beta}) ## ## Laplace probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the Laplace distribution with location parameter @var{mu} and scale ## parameter (i.e. "diversity") @var{beta}. The size of @var{y} is the common ## size of @var{x}, @var{mu}, and @var{beta}. A scalar input functions as a ## constant matrix of the same size as the other inputs. ## ## Both parameters must be reals and @qcode{@var{beta} > 0}. ## For @qcode{@var{beta} <= 0}, @qcode{NaN} is returned. ## ## Further information about the Laplace distribution can be found at ## @url{https://en.wikipedia.org/wiki/Laplace_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{laplacecdf, laplaceinv, laplacernd} ## @end deftypefn function y = laplacepdf (x, mu, beta) ## Check for valid number of input arguments if (nargin < 3) error ("laplacepdf: function called with too few input arguments."); endif ## Check for common size of X, MU, and BETA if (! isscalar (x) || ! isscalar (mu) || ! isscalar (beta)) [retval, x, mu, beta] = common_size (x, mu, beta); if (retval > 0) error (strcat ("laplacepdf: X, MU, and BETA must be of", ... " common size or scalars.")); endif endif ## Check for X, MU, and BETA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (beta))) error ("laplacepdf: X, MU, and BETA must be double or single."); endif ## Check for X, MU, and BETA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (beta)) error ("laplacepdf: X, MU, and BETA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (beta, 'single')); y = NaN (size (x), 'single'); else y = NaN (size (x)); endif ## Compute Laplace PDF k1 = ((x == -Inf) & (beta > 0)) | ((x == Inf) & (beta > 0)); y(k1) = 0; k = ! k1 & (beta > 0); y(k) = exp (- abs (x(k) - mu(k)) ./ beta(k)) ./ (2 .* beta(k)); endfunction %!demo %! ## Plot various PDFs from the Laplace distribution %! x = -10:0.01:10; %! y1 = laplacepdf (x, 0, 1); %! y2 = laplacepdf (x, 0, 2); %! y3 = laplacepdf (x, 0, 4); %! y4 = laplacepdf (x, -5, 4); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', x, y4, '-c') %! grid on %! xlim ([-10, 10]) %! ylim ([0, 0.6]) %! legend ({'μ = 0, β = 1', 'μ = 0, β = 2', ... %! 'μ = 0, β = 4', 'μ = -5, β = 4'}, 'location', 'northeast') %! title ('Laplace PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test results %!shared x, y %! x = [-Inf -log(2) 0 log(2) Inf]; %! y = [0, 1/4, 1/2, 1/4, 0]; %!assert_equal (laplacepdf ([x, NaN], 0, 1), [y, NaN]) %!assert_equal (laplacepdf (x, 0, [-2, -1, 0, 1, 2]), [nan(1, 3), 0.25, 0]) ## Test class of input preserved %!assert_equal (laplacepdf (single ([x, NaN]), 0, 1), single ([y, NaN])) %!assert_equal (laplacepdf ([x, NaN], single (0), 1), single ([y, NaN])) %!assert_equal (laplacepdf ([x, NaN], 0, single (1)), single ([y, NaN])) ## Test input validation %!error laplacepdf () %!error laplacepdf (1) %!error ... %! laplacepdf (1, 2) %!error laplacepdf (1, 2, 3, 4) %!error ... %! laplacepdf (1, ones (2), ones (3)) %!error ... %! laplacepdf (ones (2), 1, ones (3)) %!error ... %! laplacepdf (ones (2), ones (3), 1) %!error laplacepdf (int32 (2), 2, 3) %!error laplacepdf (true, 2, 3) %!error laplacepdf ('a', 2, 3) %!error laplacepdf (i, 2, 3) %!error laplacepdf (1, i, 3) %!error laplacepdf (1, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/laplacernd.m000066400000000000000000000156601527311104300253000ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} laplacernd (@var{mu}, @var{beta}) ## @deftypefnx {statistics} {@var{r} =} laplacernd (@var{mu}, @var{beta}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} laplacernd (@var{mu}, @var{beta}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} laplacernd (@var{mu}, @var{beta}, [@var{sz}]) ## ## Random arrays from the Laplace distribution. ## ## @code{@var{r} = laplacernd (@var{mu}, @var{beta})} returns an array of ## random numbers chosen from the Laplace distribution with location parameter ## @var{mu} and scale parameter @var{beta}. The size of @var{r} is the common ## size of @var{mu} and @var{beta}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## Both parameters must be reals and @qcode{@var{beta} > 0}. ## For @qcode{@var{beta} <= 0}, @qcode{NaN} is returned. ## ## When called with a single size argument, @code{laplacernd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the Laplace distribution can be found at ## @url{https://en.wikipedia.org/wiki/Laplace_distribution} ## ## @seealso{laplacecdf, laplaceinv, laplacepdf} ## @end deftypefn function r = laplacernd (mu, beta, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("laplacernd: function called with too few input arguments."); endif ## Check for common size of MU, and BETA if (! isscalar (mu) || ! isscalar (beta)) [retval, mu, beta] = common_size (mu, beta); if (retval > 0) error ("laplacernd: MU and BETA must be of common size or scalars."); endif endif ## Check for X, MU, and BETA being reals if (iscomplex (mu) || iscomplex (beta)) error ("laplacernd: MU and BETA must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (mu); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("laplacernd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("laplacernd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (mu) && ! isequal (size (mu), size (ones (sz)))) error ("laplacernd: MU and BETA must be scalars or of size SZ."); endif ## Check for class type if (isa (mu, 'single') || isa (beta, 'single')) is_type = 'single'; else is_type = 'double'; endif ## Generate random sample from Laplace distribution tmp = rand (sz, is_type); r = ((tmp < 1/2) .* log (2 * tmp) - ... (tmp > 1/2) .* log (2 * (1 - tmp))) .* beta + mu; ## Force output to NaN for invalid parameter BETA <= 0 k = (beta <= 0); r(k) = NaN; endfunction ## Test output %!assert_equal (size (laplacernd (1, 1)), [1, 1]) %!assert_equal (size (laplacernd (1, ones (2, 1))), [2, 1]) %!assert_equal (size (laplacernd (1, ones (2, 2))), [2, 2]) %!assert_equal (size (laplacernd (ones (2, 1), 1)), [2, 1]) %!assert_equal (size (laplacernd (ones (2, 2), 1)), [2, 2]) %!assert_equal (size (laplacernd (1, 1, 3)), [3, 3]) %!assert_equal (size (laplacernd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (laplacernd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (laplacernd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (laplacernd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (laplacernd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (laplacernd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (laplacernd (1, 1, [])), [0, 0]) %!assert_equal (size (laplacernd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (laplacernd (1, 2, -1)), [0, 0]) %!assert_equal (size (laplacernd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (laplacernd (1, 2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (laplacernd (1, 1)), "double") %!assert_equal (class (laplacernd (1, single (1))), "single") %!assert_equal (class (laplacernd (1, single ([1, 1]))), "single") %!assert_equal (class (laplacernd (single (1), 1)), "single") %!assert_equal (class (laplacernd (single ([1, 1]), 1)), "single") ## Test input validation %!error laplacernd () %!error laplacernd (1) %!error ... %! laplacernd (ones (3), ones (2)) %!error ... %! laplacernd (ones (2), ones (3)) %!error laplacernd (i, 2, 3) %!error laplacernd (1, i, 3) %!error ... %! laplacernd (1, 2, 1.2) %!error ... %! laplacernd (1, 2, ones (2)) %!error ... %! laplacernd (1, 2, [2 0 2.5]) %!error ... %! laplacernd (1, 2, 2, 1.5, 5) %!error ... %! laplacernd (2, ones (2), 3) %!error ... %! laplacernd (2, ones (2), [3, 2]) %!error ... %! laplacernd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/logicdf.m000066400000000000000000000140641527311104300245770ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} logicdf (@var{x}, @var{mu}, @var{sigma}) ## @deftypefnx {statistics} {@var{p} =} logicdf (@var{x}, @var{mu}, @var{sigma}, @qcode{'upper'}) ## ## Logistic cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the logistic distribution with location parameter @var{mu} and scale ## parameter @var{sigma}. The size of @var{p} is the common size of @var{x}, ## @var{mu}, and @var{sigma}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## Both parameters must be reals and @qcode{@var{sigma} > 0}. ## For @qcode{@var{sigma} <= 0}, @qcode{NaN} is returned. ## ## @code{@var{p} = logicdf (@var{x}, @var{mu}, @var{sigma}, "upper")} computes ## the upper tail probability of the logistic distribution with parameters ## @var{mu} and @var{sigma}, at the values in @var{x}. ## ## Further information about the logistic distribution can be found at ## @url{https://en.wikipedia.org/wiki/Logistic_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{logiinv, logipdf, logirnd, logifit, logilike, logistat} ## @end deftypefn function p = logicdf (x, mu, sigma, uflag) ## Check for valid number of input arguments if (nargin < 3) error ("logicdf: function called with too few input arguments."); endif ## Check for valid "upper" flag if (nargin > 3) if (! strcmpi (uflag, 'upper')) error ("logicdf: invalid argument for upper tail."); else uflag = true; endif else uflag = false; endif ## Check for common size of X, MU, and SIGMA if (! isscalar (x) || ! isscalar (mu) || ! isscalar (sigma)) [retval, x, mu, sigma] = common_size (x, mu, sigma); if (retval > 0) error ("logicdf: X, MU, and SIGMA must be of common size or scalars."); endif endif ## Check for X, MU, and SIGMA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (sigma))) error ("logicdf: X, MU, and SIGMA must be double or single."); endif ## Check for X, MU, and SIGMA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (sigma)) error ("logicdf: X, MU, and SIGMA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (sigma, 'single')); p = NaN (size (x), 'single'); else p = NaN (size (x)); endif ## Find normal and edge cases k1 = (x == -Inf) & (sigma > 0); k2 = (x == Inf) & (sigma > 0); k = ! k1 & ! k2 & (sigma > 0); ## Compute logistic CDF if (uflag) p(k1) = 1; p(k2) = 0; p(k) = 1 ./ (1 + exp ((x(k) - mu(k)) ./ sigma(k))); else p(k1) = 0; p(k2) = 1; p(k) = 1 ./ (1 + exp (- (x(k) - mu(k)) ./ sigma(k))); endif endfunction %!demo %! ## Plot various CDFs from the logistic distribution %! x = -5:0.01:20; %! p1 = logicdf (x, 5, 2); %! p2 = logicdf (x, 9, 3); %! p3 = logicdf (x, 9, 4); %! p4 = logicdf (x, 6, 2); %! p5 = logicdf (x, 2, 1); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', x, p4, '-c', x, p5, '-m') %! grid on %! legend ({'μ = 5, σ = 2', 'μ = 9, σ = 3', 'μ = 9, σ = 4', ... %! 'μ = 6, σ = 2', 'μ = 2, σ = 1'}, 'location', 'southeast') %! title ('Logistic CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, y %! x = [-Inf -log(3) 0 log(3) Inf]; %! y = [0, 1/4, 1/2, 3/4, 1]; %!assert_equal (logicdf ([x, NaN], 0, 1), [y, NaN], eps) %!assert_equal (logicdf (x, 0, [-2, -1, 0, 1, 2]), [nan(1, 3), 0.75, 1], eps) ## Test class of input preserved %!assert_equal (logicdf (single ([x, NaN]), 0, 1), single ([y, NaN]), eps ('single')) %!assert_equal (logicdf ([x, NaN], single (0), 1), single ([y, NaN]), eps ('single')) %!assert_equal (logicdf ([x, NaN], 0, single (1)), single ([y, NaN]), eps ('single')) ## Test input validation %!error logicdf () %!error logicdf (1) %!error ... %! logicdf (1, 2) %!error logicdf (1, 2, 3, 'tail') %!error logicdf (1, 2, 3, 4) %!error ... %! logicdf (1, ones (2), ones (3)) %!error ... %! logicdf (ones (2), 1, ones (3)) %!error ... %! logicdf (ones (2), ones (3), 1) %!error logicdf (int32 (2), 2, 3) %!error logicdf (true, 2, 3) %!error logicdf ('a', 2, 3) %!error logicdf (i, 2, 3) %!error logicdf (1, i, 3) %!error logicdf (1, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/logiinv.m000066400000000000000000000125151527311104300246360ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} logiinv (@var{p}, @var{mu}, @var{sigma}) ## ## Inverse of the logistic cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the logistic distribution with location parameter @var{mu} and scale ## parameter @var{sigma}. The size of @var{p} is the common size of @var{x}, ## @var{mu}, and @var{sigma}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## Both parameters must be reals and @qcode{@var{sigma} > 0}. ## For @qcode{@var{sigma} <= 0}, @qcode{NaN} is returned. ## ## Further information about the logistic distribution can be found at ## @url{https://en.wikipedia.org/wiki/Logistic_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{logicdf, logipdf, logirnd, logifit, logilike, logistat} ## @end deftypefn function x = logiinv (p, mu, sigma) ## Check for valid number of input arguments if (nargin < 3) error ("logiinv: function called with too few input arguments."); endif ## Check for common size of P, MU, and SIGMA if (! isscalar (p) || ! isscalar (mu) || ! isscalar (sigma)) [retval, p, mu, sigma] = common_size (p, mu, sigma); if (retval > 0) error ("logiinv: P, MU, and SIGMA must be of common size or scalars."); endif endif ## Check for P, MU, and SIGMA being double or single if (! (isfloat (p) && isfloat (mu) && isfloat (sigma))) error ("logiinv: P, MU, and SIGMA must be double or single."); endif ## Check for X, MU, and SIGMA being reals if (iscomplex (p) || iscomplex (mu) || iscomplex (sigma)) error ("logiinv: P, MU, and SIGMA must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (mu, 'single') || isa (sigma, 'single')); x = NaN (size (p), 'single'); else x = NaN (size (p)); endif k = (p == 0) & (sigma > 0); x(k) = -Inf; k = (p == 1) & (sigma > 0); x(k) = Inf; k = (p > 0) & (p < 1) & (sigma > 0); x(k) = mu(k) + sigma(k) .* log (p(k) ./ (1 - p(k))); endfunction %!demo %! ## Plot various iCDFs from the logistic distribution %! p = 0.001:0.001:0.999; %! x1 = logiinv (p, 5, 2); %! x2 = logiinv (p, 9, 3); %! x3 = logiinv (p, 9, 4); %! x4 = logiinv (p, 6, 2); %! x5 = logiinv (p, 2, 1); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', p, x4, '-c', p, x5, '-m') %! grid on %! legend ({'μ = 5, σ = 2', 'μ = 9, σ = 3', 'μ = 9, σ = 4', ... %! 'μ = 6, σ = 2', 'μ = 2, σ = 1'}, 'location', 'southeast') %! title ('Logistic iCDF') %! xlabel ('probability') %! ylabel ('x') ## Test output %!test %! p = [0.01:0.01:0.99]; %! assert_equal (logiinv (p, 0, 1), log (p ./ (1-p)), 25*eps); %!shared p %! p = [-1 0 0.5 1 2]; %!assert_equal (logiinv (p, 0, 1), [NaN -Inf 0 Inf NaN]) %!assert_equal (logiinv (p, 0, [-1, 0, 1, 2, 3]), [NaN NaN 0 Inf NaN]) ## Test class of input preserved %!assert_equal (logiinv ([p, NaN], 0, 1), [NaN -Inf 0 Inf NaN NaN]) %!assert_equal (logiinv (single ([p, NaN]), 0, 1), single ([NaN -Inf 0 Inf NaN NaN])) %!assert_equal (logiinv ([p, NaN], single (0), 1), single ([NaN -Inf 0 Inf NaN NaN])) %!assert_equal (logiinv ([p, NaN], 0, single (1)), single ([NaN -Inf 0 Inf NaN NaN])) ## Test input validation %!error logiinv () %!error logiinv (1) %!error ... %! logiinv (1, 2) %!error ... %! logiinv (1, ones (2), ones (3)) %!error ... %! logiinv (ones (2), 1, ones (3)) %!error ... %! logiinv (ones (2), ones (3), 1) %!error logiinv (int32 (2), 2, 3) %!error logiinv (true, 2, 3) %!error logiinv ('a', 2, 3) %!error logiinv (i, 2, 3) %!error logiinv (1, i, 3) %!error logiinv (1, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/logipdf.m000066400000000000000000000123471527311104300246160ustar00rootroot00000000000000## Copyright (C) 1995-2017 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} logipdf (@var{x}, @var{mu}, @var{sigma}) ## ## Logistic probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the logistic distribution with location parameter @var{mu} and scale ## parameter @var{sigma}. The size of @var{p} is the common size of @var{x}, ## @var{mu}, and @var{sigma}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## Both parameters must be reals and @qcode{@var{sigma} > 0}. ## For @qcode{@var{sigma} <= 0}, @qcode{NaN} is returned. ## ## Further information about the logistic distribution can be found at ## @url{https://en.wikipedia.org/wiki/Logistic_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{logicdf, logiinv, logirnd, logifit, logilike, logistat} ## @end deftypefn function y = logipdf (x, mu, sigma) ## Check for valid number of input arguments if (nargin < 3) error ("logipdf: function called with too few input arguments."); endif ## Check for common size of X, MU, and SIGMA if (! isscalar (x) || ! isscalar (mu) || ! isscalar (sigma)) [retval, x, mu, sigma] = common_size (x, mu, sigma); if (retval > 0) error ("logipdf: X, MU, and SIGMA must be of common size or scalars."); endif endif ## Check for X, MU, and SIGMA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (sigma))) error ("logipdf: X, MU, and SIGMA must be double or single."); endif ## Check for X, MU, and SIGMA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (sigma)) error ("logipdf: X, MU, and SIGMA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (sigma, 'single')); y = NaN (size (x), 'single'); else y = NaN (size (x)); endif ## Compute logistic PDF k1 = ((x == -Inf) & (sigma > 0)) | ((x == Inf) & (sigma > 0)); y(k1) = 0; k = ! k1 & (sigma > 0); y(k) = (1 ./ (4 .* sigma(k))) .* ... (sech ((x(k) - mu(k)) ./ (2 .* sigma(k))) .^ 2); endfunction %!demo %! ## Plot various PDFs from the logistic distribution %! x = -5:0.01:20; %! y1 = logipdf (x, 5, 2); %! y2 = logipdf (x, 9, 3); %! y3 = logipdf (x, 9, 4); %! y4 = logipdf (x, 6, 2); %! y5 = logipdf (x, 2, 1); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', x, y4, '-c', x, y5, '-m') %! grid on %! ylim ([0, 0.3]) %! legend ({'μ = 5, σ = 2', 'μ = 9, σ = 3', 'μ = 9, σ = 4', ... %! 'μ = 6, σ = 2', 'μ = 2, σ = 1'}, 'location', 'northeast') %! title ('Logistic PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x, y %! x = [-Inf -log(4) 0 log(4) Inf]; %! y = [0, 0.16, 1/4, 0.16, 0]; %!assert_equal (logipdf ([x, NaN], 0, 1), [y, NaN], eps) %!assert_equal (logipdf (x, 0, [-2, -1, 0, 1, 2]), [nan(1, 3), y([4:5])], eps) ## Test class of input preserved %!assert_equal (logipdf (single ([x, NaN]), 0, 1), single ([y, NaN]), eps ('single')) %!assert_equal (logipdf ([x, NaN], single (0), 1), single ([y, NaN]), eps ('single')) %!assert_equal (logipdf ([x, NaN], 0, single (1)), single ([y, NaN]), eps ('single')) ## Test input validation %!error logipdf () %!error logipdf (1) %!error ... %! logipdf (1, 2) %!error ... %! logipdf (1, ones (2), ones (3)) %!error ... %! logipdf (ones (2), 1, ones (3)) %!error ... %! logipdf (ones (2), ones (3), 1) %!error logipdf (int32 (2), 2, 3) %!error logipdf (true, 2, 3) %!error logipdf ('a', 2, 3) %!error logipdf (i, 2, 3) %!error logipdf (1, i, 3) %!error logipdf (1, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/logirnd.m000066400000000000000000000153531527311104300246300ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} logirnd (@var{mu}, @var{sigma}) ## @deftypefnx {statistics} {@var{r} =} logirnd (@var{mu}, @var{sigma}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} logirnd (@var{mu}, @var{sigma}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} logirnd (@var{mu}, @var{sigma}, [@var{sz}]) ## ## Random arrays from the logistic distribution. ## ## @code{@var{r} = logirnd (@var{mu}, @var{sigma})} returns an array of ## random numbers chosen from the logistic distribution with location parameter ## @var{mu} and scale parameter @var{sigma}. The size of @var{r} is the common ## size ## of @var{mu} and @var{sigma}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## Both parameters must be reals and @qcode{@var{sigma} > 0}. ## For @qcode{@var{sigma} <= 0}, @qcode{NaN} is returned. ## ## When called with a single size argument, @code{logirnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the logistic distribution can be found at ## @url{https://en.wikipedia.org/wiki/Logistic_distribution} ## ## @seealso{logicdf, logiinv, logipdf, logifit, logilike, logistat} ## @end deftypefn function r = logirnd (mu, sigma, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("logirnd: function called with too few input arguments."); endif ## Check for common size of MU, and SIGMA if (! isscalar (mu) || ! isscalar (sigma)) [retval, mu, sigma] = common_size (mu, sigma); if (retval > 0) error ("logirnd: MU and SIGMA must be of common size or scalars."); endif endif ## Check for X, MU, and SIGMA being reals if (iscomplex (mu) || iscomplex (sigma)) error ("logirnd: MU and SIGMA must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (mu); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("logirnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("logirnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (mu) && ! isequal (size (mu), size (ones (sz)))) error ("logirnd: MU and SIGMA must be scalars or of size SZ."); endif ## Check for class type if (isa (mu, 'single') || isa (sigma, 'single')) is_type = 'single'; else is_type = 'double'; endif ## Generate random sample from logistic distribution r = - log (1 ./ rand (sz, is_type) - 1) .* sigma + mu; ## Force output to NaN for invalid parameter SIGMA <= 0 k = (sigma <= 0); r(k) = NaN; endfunction ## Test output %!assert_equal (size (logirnd (1, 1)), [1, 1]) %!assert_equal (size (logirnd (1, ones (2, 1))), [2, 1]) %!assert_equal (size (logirnd (1, ones (2, 2))), [2, 2]) %!assert_equal (size (logirnd (ones (2, 1), 1)), [2, 1]) %!assert_equal (size (logirnd (ones (2, 2), 1)), [2, 2]) %!assert_equal (size (logirnd (1, 1, 3)), [3, 3]) %!assert_equal (size (logirnd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (logirnd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (logirnd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (logirnd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (logirnd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (logirnd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (logirnd (1, 1, [])), [0, 0]) %!assert_equal (size (logirnd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (logirnd (1, 2, -1)), [0, 0]) %!assert_equal (size (logirnd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (logirnd (1, 2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (logirnd (1, 1)), "double") %!assert_equal (class (logirnd (1, single (1))), "single") %!assert_equal (class (logirnd (1, single ([1, 1]))), "single") %!assert_equal (class (logirnd (single (1), 1)), "single") %!assert_equal (class (logirnd (single ([1, 1]), 1)), "single") ## Test input validation %!error logirnd () %!error logirnd (1) %!error ... %! logirnd (ones (3), ones (2)) %!error ... %! logirnd (ones (2), ones (3)) %!error logirnd (i, 2, 3) %!error logirnd (1, i, 3) %!error ... %! logirnd (1, 2, 1.2) %!error ... %! logirnd (1, 2, ones (2)) %!error ... %! logirnd (1, 2, [2 0 2.5]) %!error ... %! logirnd (1, 2, 2, 1.5, 5) %!error ... %! logirnd (2, ones (2), 3) %!error ... %! logirnd (2, ones (2), [3, 2]) %!error ... %! logirnd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/loglcdf.m000066400000000000000000000163051527311104300246020ustar00rootroot00000000000000## Copyright (C) 2023-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} loglcdf (@var{x}, @var{mu}, @var{sigma}) ## @deftypefnx {statistics} {@var{p} =} loglcdf (@var{x}, @var{mu}, @var{sigma}, @qcode{'upper'}) ## ## Loglogistic cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the loglogistic distribution with mean parameter @var{mu} and scale ## parameter @var{sigma}. The size of @var{p} is the common size of @var{x}, ## @var{mu}, and @var{sigma}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## Mean of logarithmic values @var{mu} must be a non-negative real value, scale ## parameter of logarithmic values @var{sigma} must be a positive real value and ## @var{x} is supported in the range @math{[0,Inf)}, otherwise @qcode{NaN} is ## returned. ## ## @code{@var{p} = loglcdf (@var{x}, @var{mu}, @var{sigma}, "upper")} computes ## the upper tail probability of the log-logistic distribution with parameters ## @var{mu} and @var{sigma}, at the values in @var{x}. ## ## Further information about the loglogistic distribution can be found at ## @url{https://en.wikipedia.org/wiki/Log-logistic_distribution} ## ## OCTAVE/MATLAB use an alternative parameterization given by the pair ## @math{μ, σ}, i.e. @var{mu} and @var{sigma}, in analogy with the logistic ## distribution. Their relation to the @math{α} and @math{b} parameters used ## in Wikipedia are given below: ## ## @itemize ## @item @qcode{@var{mu} = log (@var{a})} ## @item @qcode{@var{sigma} = 1 / @var{a}} ## @end itemize ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{loglinv, loglpdf, loglrnd, loglfit, logllike, loglstat} ## @end deftypefn function p = loglcdf (x, mu, sigma, uflag) ## Check for valid number of input arguments if (nargin < 3) error ("loglcdf: function called with too few input arguments."); endif ## Check for valid "upper" flag if (nargin > 3) if (! strcmpi (uflag, 'upper')) error ("loglcdf: invalid argument for upper tail."); else uflag = true; endif else uflag = false; endif ## Check for common size of X, MU, and SIGMA if (! isscalar (x) || ! isscalar (mu) || ! isscalar (sigma)) [retval, x, mu, sigma] = common_size (x, mu, sigma); if (retval > 0) error ("loglcdf: X, MU, and SIGMA must be of common size or scalars."); endif endif ## Check for X, MU, and SIGMA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (sigma))) error ("loglcdf: X, MU, and SIGMA must be double or single."); endif ## Check for X, MU, and SIGMA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (sigma)) error ("loglcdf: X, MU, and SIGMA must not be complex."); endif ## Check for invalid points mu(mu < 0) = NaN; sigma(sigma <= 0) = NaN; x(x <= 0) = realmin; ## Compute log-logistic CDF z = (log (x) - mu) ./ sigma; if (uflag) p = 1 ./ (1 + exp (z)); else p = 1 ./ (1 + exp (-z)); endif ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (sigma, 'single')); p = cast (p, 'single'); endif endfunction %!demo %! ## Plot various CDFs from the log-logistic distribution %! x = 0:0.001:2; %! p1 = loglcdf (x, log (1), 1/0.5); %! p2 = loglcdf (x, log (1), 1); %! p3 = loglcdf (x, log (1), 1/2); %! p4 = loglcdf (x, log (1), 1/4); %! p5 = loglcdf (x, log (1), 1/8); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', x, p4, '-c', x, p5, '-m') %! legend ({'σ = 2 (β = 0.5)', 'σ = 1 (β = 1)', 'σ = 0.5 (β = 2)', ... %! 'σ = 0.25 (β = 4)', 'σ = 0.125 (β = 8)'}, 'location', 'northwest') %! grid on %! title ('Log-logistic CDF') %! xlabel ('values in x') %! ylabel ('probability') %! text (0.05, 0.64, 'μ = 0 (α = 1), values of σ (β) as shown in legend') ## Test output %!shared out1, out2 %! out1 = [0, 0.5, 0.66666667, 0.75, 0.8, 0.83333333]; %! out2 = [0, 0.4174, 0.4745, 0.5082, 0.5321, 0.5506]; %!assert_equal (loglcdf ([0:5], 0, 1), out1, 1e-8) %!assert_equal (loglcdf ([0:5], 0, 1, 'upper'), 1 - out1, 1e-8) %!assert_equal (loglcdf ([0:5], 0, 1), out1, 1e-8) %!assert_equal (loglcdf ([0:5], 0, 1, 'upper'), 1 - out1, 1e-8) %!assert_equal (loglcdf ([0:5], 1, 3), out2, 1e-4) %!assert_equal (loglcdf ([0:5], 1, 3, 'upper'), 1 - out2, 1e-4) ## Test class of input preserved %!assert_equal (class (loglcdf (single (1), 2, 3)), "single") %!assert_equal (class (loglcdf (1, single (2), 3)), "single") %!assert_equal (class (loglcdf (1, 2, single (3))), "single") ## Test input validation %!error loglcdf (1) %!error loglcdf (1, 2) %!error ... %! loglcdf (1, 2, 3, 4) %!error ... %! loglcdf (1, 2, 3, 'uper') %!error ... %! loglcdf (1, ones (2), ones (3)) %!error ... %! loglcdf (1, ones (2), ones (3), 'upper') %!error ... %! loglcdf (ones (2), 1, ones (3)) %!error ... %! loglcdf (ones (2), 1, ones (3), 'upper') %!error ... %! loglcdf (ones (2), ones (3), 1) %!error ... %! loglcdf (ones (2), ones (3), 1, 'upper') %!error loglcdf (int32 (2), 2, 3) %!error loglcdf (true, 2, 3) %!error loglcdf ('a', 2, 3) %!error loglcdf (i, 2, 3) %!error loglcdf (i, 2, 3, 'upper') %!error loglcdf (1, i, 3) %!error loglcdf (1, i, 3, 'upper') %!error loglcdf (1, 2, i) %!error loglcdf (1, 2, i, 'upper') statistics-release-2.1.2/inst/Distribution_Functions/loglinv.m000066400000000000000000000134111527311104300246350ustar00rootroot00000000000000## Copyright (C) 2023-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} loglinv (@var{p}, @var{mu}, @var{sigma}) ## ## Inverse of the log-logistic cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the log-logistic distribution with mean parameter @var{mu} and scale ## parameter @var{sigma}. The size of @var{x} is the common size of @var{p}, ## @var{mu}, and @var{sigma}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## Mean of logarithmic values @var{mu} must be a non-negative real value, scale ## parameter of logarithmic values @var{sigma} must be a positive real value and ## @var{p} is supported in the range @math{[0,1]}, otherwise @qcode{NaN} is ## returned. ## ## Further information about the loglogistic distribution can be found at ## @url{https://en.wikipedia.org/wiki/Log-logistic_distribution} ## ## OCTAVE/MATLAB use an alternative parameterization given by the pair ## @math{μ, σ}, i.e. @var{mu} and @var{sigma}, in analogy with the logistic ## distribution. Their relation to the @math{α} and @math{b} parameters used ## in Wikipedia are given below: ## ## @itemize ## @item @qcode{@var{mu} = log (@var{a})} ## @item @qcode{@var{sigma} = 1 / @var{a}} ## @end itemize ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{loglcdf, loglpdf, loglrnd, loglfit, logllike, loglstat} ## @end deftypefn function x = loglinv (p, mu, sigma) ## Check for valid number of input arguments if (nargin < 3) error ("loglinv: function called with too few input arguments."); endif ## Check for common size of P, MU, and SIGMA if (! isscalar (p) || ! isscalar (mu) || ! isscalar (sigma)) [retval, p, mu, sigma] = common_size (p, mu, sigma); if (retval > 0) error ("loglinv: P, MU, and SIGMA must be of common size or scalars."); endif endif ## Check for P, MU, and SIGMA being double or single if (! (isfloat (p) && isfloat (mu) && isfloat (sigma))) error ("loglinv: P, MU, and SIGMA must be double or single."); endif ## Check for X, MU, and SIGMA being reals if (iscomplex (p) || iscomplex (mu) || iscomplex (sigma)) error ("loglinv: P, MU, and SIGMA must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (mu, 'single') || isa (sigma, 'single')); x = NaN (size (p), 'single'); else x = NaN (size (p)); endif ## Check for valid points k = (p >= 0) & (p <= 1) & (mu >= 0) & (sigma > 0); ## Compute the log-logistic iCDF x(k) = exp (logit (p(k)) .* sigma(k) + mu(k)); endfunction %!demo %! ## Plot various iCDFs from the log-logistic distribution %! p = 0.001:0.001:0.999; %! x1 = loglinv (p, log (1), 1/0.5); %! x2 = loglinv (p, log (1), 1); %! x3 = loglinv (p, log (1), 1/2); %! x4 = loglinv (p, log (1), 1/4); %! x5 = loglinv (p, log (1), 1/8); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', p, x4, '-c', p, x5, '-m') %! ylim ([0, 20]) %! grid on %! legend ({'σ = 2 (β = 0.5)', 'σ = 1 (β = 1)', 'σ = 0.5 (β = 2)', ... %! 'σ = 0.25 (β = 4)', 'σ = 0.125 (β = 8)'}, 'location', 'northwest') %! title ('Log-logistic iCDF') %! xlabel ('probability') %! ylabel ('x') %! text (0.03, 12.5, 'μ = 0 (α = 1), values of σ (β) as shown in legend') ## Test output %!shared p, out1, out2 %! p = [-1, 0, 0.2, 0.5, 0.8, 0.95, 1, 2]; %! out1 = [NaN, 0, 0.25, 1, 4, 19, Inf, NaN]; %! out2 = [NaN, 0, 0.0424732, 2.718282, 173.970037, 18644.695061, Inf, NaN]; %!assert_equal (loglinv (p, 0, 1), out1, 1e-8) %!assert_equal (loglinv (p, 0, 1), out1, 1e-8) %!assert_equal (loglinv (p, 1, 3), out2, 1e-6) ## Test class of input preserved %!assert_equal (class (loglinv (single (1), 2, 3)), "single") %!assert_equal (class (loglinv (1, single (2), 3)), "single") %!assert_equal (class (loglinv (1, 2, single (3))), "single") ## Test input validation %!error loglinv (1) %!error loglinv (1, 2) %!error ... %! loglinv (1, ones (2), ones (3)) %!error ... %! loglinv (ones (2), 1, ones (3)) %!error ... %! loglinv (ones (2), ones (3), 1) %!error loglinv (int32 (2), 2, 3) %!error loglinv (true, 2, 3) %!error loglinv ('a', 2, 3) %!error loglinv (i, 2, 3) %!error loglinv (1, i, 3) %!error loglinv (1, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/loglpdf.m000066400000000000000000000134061527311104300246160ustar00rootroot00000000000000## Copyright (C) 2023-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} loglpdf (@var{x}, @var{mu}, @var{sigma}) ## ## Loglogistic probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the loglogistic distribution with mean parameter @var{mu} and scale ## parameter @var{sigma}. The size of @var{y} is the common size of @var{x}, ## @var{mu}, and @var{sigma}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## Mean of logarithmic values @var{mu} must be a non-negative real value, scale ## parameter of logarithmic values @var{sigma} must be a positive real value and ## @var{x} is supported in the range @math{[0,Inf)}, otherwise 0 is returned. ## ## Further information about the loglogistic distribution can be found at ## @url{https://en.wikipedia.org/wiki/Log-logistic_distribution} ## ## OCTAVE/MATLAB use an alternative parameterization given by the pair ## @math{μ, σ}, i.e. @var{mu} and @var{sigma}, in analogy with the logistic ## distribution. Their relation to the @math{α} and @math{b} parameters used ## in Wikipedia are given below: ## ## @itemize ## @item @qcode{@var{mu} = log (@var{a})} ## @item @qcode{@var{sigma} = 1 / @var{a}} ## @end itemize ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{loglcdf, loglinv, loglrnd, loglfit, logllike, loglstat} ## @end deftypefn function y = loglpdf (x, mu, sigma) ## Check for valid number of input arguments if (nargin < 3) error ("loglpdf: function called with too few input arguments."); endif ## Check for common size of X, MU, and SIGMA if (! isscalar (x) || ! isscalar (mu) || ! isscalar (sigma)) [retval, x, mu, sigma] = common_size (x, mu, sigma); if (retval > 0) error ("loglpdf: X, MU, and SIGMA must be of common size or scalars."); endif endif ## Check for X, MU, and SIGMA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (sigma))) error ("loglpdf: X, MU, and SIGMA must be double or single."); endif ## Check for X, MU, and SIGMA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (sigma)) error ("loglpdf: X, MU, and SIGMA must not be complex."); endif ## Check for invalid points mu(mu < 0) = NaN; sigma(sigma <= 0) = NaN; ## Compute log-logistic PDF a = exp (mu); b = 1./ sigma; y = ((b ./ a) .* (x ./ a) .^ (b - 1)) ./ ((1 + (x ./ a) .^ b) .^ 2); y(x <= 0) = 0; ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (sigma, 'single')); y = cast (y, 'single'); endif endfunction %!demo %! ## Plot various PDFs from the log-logistic distribution %! x = 0.001:0.001:2; %! y1 = loglpdf (x, log (1), 1/0.5); %! y2 = loglpdf (x, log (1), 1); %! y3 = loglpdf (x, log (1), 1/2); %! y4 = loglpdf (x, log (1), 1/4); %! y5 = loglpdf (x, log (1), 1/8); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', x, y4, '-c', x, y5, '-m') %! grid on %! ylim ([0,3]) %! legend ({'σ = 2 (β = 0.5)', 'σ = 1 (β = 1)', 'σ = 0.5 (β = 2)', ... %! 'σ = 0.25 (β = 4)', 'σ = 0.125 (β = 8)'}, 'location', 'northeast') %! title ('Log-logistic PDF') %! xlabel ('values in x') %! ylabel ('density') %! text (0.1, 2.8, 'μ = 0 (α = 1), values of σ (β) as shown in legend') ## Test output %!shared out1, out2 %! out1 = [0, 0, 1, 0.2500, 0.1111, 0.0625, 0.0400, 0.0278, 0]; %! out2 = [0, 0, 0.0811, 0.0416, 0.0278, 0.0207, 0.0165, 0]; %!assert_equal (loglpdf ([-1,0,realmin,1:5,Inf], 0, 1), out1, 1e-4) %!assert_equal (loglpdf ([-1,0,realmin,1:5,Inf], 0, 1), out1, 1e-4) %!assert_equal (loglpdf ([-1:5,Inf], 1, 3), out2, 1e-4) ## Test class of input preserved %!assert_equal (class (loglpdf (single (1), 2, 3)), "single") %!assert_equal (class (loglpdf (1, single (2), 3)), "single") %!assert_equal (class (loglpdf (1, 2, single (3))), "single") ## Test input validation %!error loglpdf (1) %!error loglpdf (1, 2) %!error ... %! loglpdf (1, ones (2), ones (3)) %!error ... %! loglpdf (ones (2), 1, ones (3)) %!error ... %! loglpdf (ones (2), ones (3), 1) %!error loglpdf (int32 (2), 2, 3) %!error loglpdf (true, 2, 3) %!error loglpdf ('a', 2, 3) %!error loglpdf (i, 2, 3) %!error loglpdf (1, i, 3) %!error loglpdf (1, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/loglrnd.m000066400000000000000000000161611527311104300246310ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} loglrnd (@var{mu}, @var{sigma}) ## @deftypefnx {statistics} {@var{r} =} loglrnd (@var{mu}, @var{sigma}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} loglrnd (@var{mu}, @var{sigma}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} loglrnd (@var{mu}, @var{sigma}, [@var{sz}]) ## ## Random arrays from the loglogistic distribution. ## ## @code{@var{r} = loglrnd (@var{mu}, @var{sigma})} returns an array of random ## numbers chosen from the loglogistic distribution with mean parameter @var{mu} ## and scale parameter @var{sigma}. The size of @var{r} is the common size of ## @var{mu} and @var{sigma}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## Mean of logarithmic values @var{mu} must be a non-negative real value and ## scale parameter of logarithmic values @var{sigma} must be a positive real ## value. ## ## When called with mu single size argument, @code{loglrnd} returns mu square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with mu row vector of dimensions, @var{sz}. ## ## Further information about the loglogistic distribution can be found at ## @url{https://en.wikipedia.org/wiki/Log-logistic_distribution} ## ## OCTAVE/MATLAB use an alternative parameterization given by the pair ## @math{μ, σ}, i.e. @var{mu} and @var{sigma}, in analogy with the logistic ## distribution. Their relation to the @math{α} and @math{b} parameters used ## in Wikipedia are given below: ## ## @itemize ## @item @qcode{@var{mu} = log (@var{a})} ## @item @qcode{@var{sigma} = 1 / @var{a}} ## @end itemize ## ## @seealso{loglcdf, loglinv, loglpdf, loglfit, logllike, loglstat} ## @end deftypefn function r = loglrnd (mu, sigma, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("loglrnd: function called with too few input arguments."); endif ## Check for common size of MU, and SIGMA if (! isscalar (mu) || ! isscalar (sigma)) [retval, mu, sigma] = common_size (mu, sigma); if (retval > 0) error ("loglrnd: MU and SIGMA must be of common size or scalars."); endif endif ## Check for X, MU, and SIGMA being reals if (iscomplex (mu) || iscomplex (sigma)) error ("loglrnd: MU and SIGMA must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (mu); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("loglrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("loglrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (mu) && ! isequal (size (mu), size (ones (sz)))) error ("loglrnd: MU and SIGMA must be scalars or of size SZ."); endif ## Check for class type if (isa (mu, 'single') || isa (sigma, 'single')) is_type = 'single'; else is_type = 'double'; endif ## Generate random sample from log-logistic distribution u = rand (sz, is_type); r = exp (mu) .* (u ./ (1 - u)) .^ (sigma); ## Force output to NaN for invalid parameters MU and SIGMA k = (mu < 0 | sigma <= 0); r(k) = NaN; endfunction ## Test output %!assert_equal (size (loglrnd (1, 1)), [1, 1]) %!assert_equal (size (loglrnd (1, ones (2, 1))), [2, 1]) %!assert_equal (size (loglrnd (1, ones (2, 2))), [2, 2]) %!assert_equal (size (loglrnd (ones (2, 1), 1)), [2, 1]) %!assert_equal (size (loglrnd (ones (2, 2), 1)), [2, 2]) %!assert_equal (size (loglrnd (1, 1, 3)), [3, 3]) %!assert_equal (size (loglrnd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (loglrnd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (loglrnd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (loglrnd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (loglrnd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (loglrnd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (loglrnd (1, 1, [])), [0, 0]) %!assert_equal (size (loglrnd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (loglrnd (1, 2, -1)), [0, 0]) %!assert_equal (size (loglrnd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (loglrnd (1, 2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (loglrnd (1, 1)), "double") %!assert_equal (class (loglrnd (1, single (1))), "single") %!assert_equal (class (loglrnd (1, single ([1, 1]))), "single") %!assert_equal (class (loglrnd (single (1), 1)), "single") %!assert_equal (class (loglrnd (single ([1, 1]), 1)), "single") ## Test input validation %!error loglrnd () %!error loglrnd (1) %!error ... %! loglrnd (ones (3), ones (2)) %!error ... %! loglrnd (ones (2), ones (3)) %!error loglrnd (i, 2, 3) %!error loglrnd (1, i, 3) %!error ... %! loglrnd (1, 2, 1.2) %!error ... %! loglrnd (1, 2, ones (2)) %!error ... %! loglrnd (1, 2, [2 0 2.5]) %!error ... %! loglrnd (1, 2, 2, 1.5, 5) %!error ... %! loglrnd (2, ones (2), 3) %!error ... %! loglrnd (2, ones (2), [3, 2]) %!error ... %! loglrnd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/logncdf.m000066400000000000000000000230461527311104300246040ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} logncdf (@var{x}) ## @deftypefnx {statistics} {@var{p} =} logncdf (@var{x}, @var{mu}) ## @deftypefnx {statistics} {@var{p} =} logncdf (@var{x}, @var{mu}, @var{sigma}) ## @deftypefnx {statistics} {@var{p} =} logncdf (@dots{}, @qcode{'upper'}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} logncdf (@var{x}, @var{mu}, @var{sigma}, @var{pcov}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} logncdf (@var{x}, @var{mu}, @var{sigma}, @var{pcov}, @var{alpha}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} logncdf (@dots{}, @qcode{'upper'}) ## ## Lognormal cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the lognormal distribution with mean parameter @var{mu} and ## standard deviation parameter @var{sigma}, each corresponding to the ## associated normal distribution. The size of @var{p} is the common size of ## @var{x}, @var{mu}, and @var{sigma}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## If a random variable follows this distribution, its logarithm is normally ## distributed with mean @var{mu} and standard deviation @var{sigma}. ## ## Default parameter values are @qcode{@var{mu} = 0} and ## @qcode{@var{sigma} = 1}. Both parameters must be reals and ## @qcode{@var{sigma} > 0}. For @qcode{@var{sigma} <= 0}, @qcode{NaN} is ## returned. ## ## When called with three output arguments, i.e. @qcode{[@var{p}, @var{plo}, ## @var{pup}]}, @code{logncdf} computes the confidence bounds for @var{p} when ## the input parameters @var{mu} and @var{sigma} are estimates. In such case, ## @var{pcov}, a @math{2*2} matrix containing the covariance matrix of the ## estimated parameters, is necessary. Optionally, @var{alpha}, which has a ## default value of 0.05, specifies the @qcode{100 * (1 - @var{alpha})} percent ## confidence bounds. @var{plo} and @var{pup} are arrays of the same size as ## @var{p} containing the lower and upper confidence bounds. ## ## @code{[@dots{}] = logncdf (@dots{}, "upper")} computes the upper tail ## probability of the log-normal distribution with parameters @var{mu} and ## @var{sigma}, at the values in @var{x}. ## ## Further information about the lognormal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Log-normal_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{logninv, lognpdf, lognrnd, lognfit, lognlike, lognstat} ## @end deftypefn function [varargout] = logncdf (x, varargin) ## Check for valid number of input arguments if (nargin < 1 || nargin > 6) error ("logncdf: invalid number of input arguments."); endif ## Check for "upper" flag if (nargin > 1 && strcmpi (varargin{end}, 'upper')) uflag = true; varargin(end) = []; elseif (nargin > 1 && ischar (varargin{end}) && ... ! strcmpi (varargin{end}, 'upper')) error ("logncdf: invalid argument for upper tail."); elseif (nargin > 2 && isempty (varargin{end})) uflag = false; varargin(end) = []; else uflag = false; endif ## Get extra arguments (if they exist) or add defaults if (numel (varargin) > 0) mu = varargin{1}; else mu = 0; endif if (numel (varargin) > 1) sigma = varargin{2}; else sigma = 1; endif if (numel (varargin) > 2) pcov = varargin{3}; ## Check for valid covariance matrix 2x2 if (! isequal (size (pcov), [2, 2])) error ("logncdf: invalid size of covariance matrix."); endif else ## Check that cov matrix is provided if 3 output arguments are requested if (nargout > 1) error ("logncdf: covariance matrix is required for confidence bounds."); endif pcov = []; endif if (numel (varargin) > 3) alpha = varargin{4}; ## Check for valid alpha value if (! isnumeric (alpha) || numel (alpha) !=1 || alpha <= 0 || alpha >= 1) error ("logncdf: invalid value for alpha."); endif else alpha = 0.05; endif ## Check for common size of X, MU, and SIGMA if (! isscalar (x) || ! isscalar (mu) || ! isscalar (sigma)) [err, x, mu, sigma] = common_size (x, mu, sigma); if (err > 0) error ("logncdf: X, MU, and SIGMA must be of common size or scalars."); endif endif ## Check for X, MU, and SIGMA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (sigma))) error ("logncdf: X, MU, and SIGMA must be double or single."); endif ## Check for X, MU, and SIGMA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (sigma)) error ("logncdf: X, MU, and SIGMA must not be complex."); endif ## Return NaN for out of range parameters. sigma(sigma <= 0) = NaN; ## Negative data would create complex values, which erfc cannot handle. x(x < 0) = 0; ## Compute lognormal cdf z = (log (x) - mu) ./ sigma; if (uflag) z = -z; endif p = 0.5 * erfc (-z ./ sqrt (2)); ## Compute confidence bounds (if requested) if (nargout >= 2) zvar = (pcov(1,1) + 2 * pcov(1,2) * z + pcov(2,2) * z .^ 2) ./ (sigma .^ 2); if (any (zvar(:) < 0)) error ("logncdf: bad covariance matrix."); endif normz = -norminv (alpha / 2); halfwidth = normz * sqrt (zvar); zlo = z - halfwidth; zup = z + halfwidth; plo = 0.5 * erfc (-zlo ./ sqrt (2)); pup = 0.5 * erfc (-zup ./ sqrt (2)); endif ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (sigma, 'single')); is_class = 'single'; else is_class = 'double'; endif ## Prepare output varargout{1} = cast (p, is_class); if (nargout > 1) varargout{2} = cast (plo, is_class); varargout{3} = cast (pup, is_class); endif endfunction %!demo %! ## Plot various CDFs from the log-normal distribution %! x = 0:0.01:3; %! p1 = logncdf (x, 0, 1); %! p2 = logncdf (x, 0, 0.5); %! p3 = logncdf (x, 0, 0.25); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r') %! grid on %! legend ({'μ = 0, σ = 1', 'μ = 0, σ = 0.5', 'μ = 0, σ = 0.25'}, ... %! 'location', 'southeast') %! title ('Log-normal CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, y %! x = [-1, 0, 1, e, Inf]; %! y = [0, 0, 0.5, 1/2+1/2*erf(1/2), 1]; %!assert_equal (logncdf (x, zeros (1,5), sqrt (2)*ones (1,5)), y, eps) %!assert_equal (logncdf (x, zeros (1,5), sqrt (2)*ones (1,5), []), y, eps) %!assert_equal (logncdf (x, 0, sqrt (2)*ones (1,5)), y, eps) %!assert_equal (logncdf (x, zeros (1,5), sqrt (2)), y, eps) %!assert_equal (logncdf (x, [0 1 NaN 0 1], sqrt (2)), [0 0 NaN y(4:5)], eps) %!assert_equal (logncdf (x, 0, sqrt (2)*[0 NaN Inf 1 1]), [NaN NaN y(3:5)], eps) %!assert_equal (logncdf ([x(1:3) NaN x(5)], 0, sqrt (2)), [y(1:3) NaN y(5)], eps) ## Test class of input preserved %!assert_equal (logncdf ([x, NaN], 0, sqrt (2)), [y, NaN], eps) %!assert_equal (logncdf (single ([x, NaN]), 0, sqrt (2)), single ([y, NaN]), eps ('single')) %!assert_equal (logncdf ([x, NaN], single (0), sqrt (2)), single ([y, NaN]), eps ('single')) %!assert_equal (logncdf ([x, NaN], 0, single (sqrt (2))), single ([y, NaN]), eps ('single')) ## Test input validation %!error logncdf () %!error logncdf (1,2,3,4,5,6,7) %!error logncdf (1, 2, 3, 4, 'uper') %!error ... %! logncdf (ones (3), ones (2), ones (2)) %!error logncdf (2, 3, 4, [1, 2]) %!error ... %! [p, plo, pup] = logncdf (1, 2, 3) %!error [p, plo, pup] = ... %! logncdf (1, 2, 3, [1, 0; 0, 1], 0) %!error [p, plo, pup] = ... %! logncdf (1, 2, 3, [1, 0; 0, 1], 1.22) %!error [p, plo, pup] = ... %! logncdf (1, 2, 3, [1, 0; 0, 1], 'alpha', 'upper') %!error logncdf (int32 (2), 2, 2) %!error logncdf (true, 2, 2) %!error logncdf ('a', 2, 2) %!error logncdf (i, 2, 2) %!error logncdf (2, i, 2) %!error logncdf (2, 2, i) %!error ... %! [p, plo, pup] =logncdf (1, 2, 3, [1, 0; 0, -inf], 0.04) statistics-release-2.1.2/inst/Distribution_Functions/logninv.m000066400000000000000000000131231527311104300246370ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} logninv (@var{p}) ## @deftypefnx {statistics} {@var{x} =} logninv (@var{p}, @var{mu}) ## @deftypefnx {statistics} {@var{x} =} logninv (@var{p}, @var{mu}, @var{sigma}) ## ## Inverse of the lognormal cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the lognormal distribution with mean parameter @var{mu} and standard ## deviation parameter @var{sigma}, each corresponding to the associated normal ## distribution. The size of @var{x} is the common size of @var{p}, @var{mu}, ## and @var{sigma}. A scalar input functions as a constant matrix of the same ## size as the other inputs. ## ## If a random variable follows this distribution, its logarithm is normally ## distributed with mean @var{mu} and standard deviation @var{sigma}. ## ## Default parameter values are @qcode{@var{mu} = 0} and ## @qcode{@var{sigma} = 1}. Both parameters must be reals and ## @qcode{@var{sigma} > 0}. For @qcode{@var{sigma} <= 0}, @qcode{NaN} is ## returned. ## ## Further information about the lognormal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Log-normal_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{logncdf, lognpdf, lognrnd, lognfit, lognlike, lognstat} ## @end deftypefn function x = logninv (p, mu = 0, sigma = 1) ## Check for valid number of input arguments if (nargin < 1 || nargin > 3) print_usage (); endif ## Check for common size of P, MU, and SIGMA if (! isscalar (p) || ! isscalar (mu) || ! isscalar (sigma)) [retval, p, mu, sigma] = common_size (p, mu, sigma); if (retval > 0) error ("logninv: X, MU, and SIGMA must be of common size or scalars."); endif endif ## Check for X, MU, and SIGMA being double or single if (! (isfloat (p) && isfloat (mu) && isfloat (sigma))) error ("logninv: X, MU, and SIGMA must be double or single."); endif ## Check for X, MU, and SIGMA being reals if (iscomplex (p) || iscomplex (mu) || iscomplex (sigma)) error ("logninv: X, MU, and SIGMA must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (mu, 'single') || isa (sigma, 'single')) x = NaN (size (p), 'single'); else x = NaN (size (p)); endif ## Compute lognormal iCDF k = ! (p >= 0) | ! (p <= 1) | ! (sigma > 0) | ! (sigma < Inf); x(k) = NaN; k = (p == 1) & (sigma > 0) & (sigma < Inf); x(k) = Inf; k = (p >= 0) & (p < 1) & (sigma > 0) & (sigma < Inf); if (isscalar (mu) && isscalar (sigma)) x(k) = exp (mu) .* exp (sigma .* (-sqrt (2) * erfcinv (2 * p(k)))); else x(k) = exp (mu(k)) .* exp (sigma(k) .* (-sqrt (2) * erfcinv (2 * p(k)))); endif endfunction %!demo %! ## Plot various iCDFs from the log-normal distribution %! p = 0.001:0.001:0.999; %! x1 = logninv (p, 0, 1); %! x2 = logninv (p, 0, 0.5); %! x3 = logninv (p, 0, 0.25); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r') %! grid on %! ylim ([0, 3]) %! legend ({'μ = 0, σ = 1', 'μ = 0, σ = 0.5', 'μ = 0, σ = 0.25'}, ... %! 'location', 'northwest') %! title ('Log-normal iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p %! p = [-1 0 0.5 1 2]; %!assert_equal (logninv (p, ones (1,5), ones (1,5)), [NaN 0 e Inf NaN], 2*eps) %!assert_equal (logninv (p, 1, ones (1,5)), [NaN 0 e Inf NaN], 2*eps) %!assert_equal (logninv (p, ones (1,5), 1), [NaN 0 e Inf NaN], 2*eps) %!assert_equal (logninv (p, [1 1 NaN 0 1], 1), [NaN 0 NaN Inf NaN]) %!assert_equal (logninv (p, 1, [1 0 NaN Inf 1]), [NaN NaN NaN NaN NaN]) %!assert_equal (logninv ([p(1:2) NaN p(4:5)], 1, 2), [NaN 0 NaN Inf NaN]) ## Test class of input preserved %!assert_equal (logninv ([p, NaN], 1, 1), [NaN 0 e Inf NaN NaN], 2*eps) %!assert_equal (logninv (single ([p, NaN]), 1, 1), single ([NaN 0 e Inf NaN NaN])) %!assert_equal (logninv ([p, NaN], single (1), 1), single ([NaN 0 e Inf NaN NaN])) %!assert_equal (logninv ([p, NaN], 1, single (1)), single ([NaN 0 e Inf NaN NaN])) ## Test input validation %!error logninv (int32 (2), 0, 1) %!error logninv (true, 0, 1) %!error logninv ('a', 0, 1) %!error logninv () %!error logninv (1,2,3,4) %!error logninv (ones (3), ones (2), ones (2)) %!error logninv (ones (2), ones (3), ones (2)) %!error logninv (ones (2), ones (2), ones (3)) %!error logninv (i, 2, 2) %!error logninv (2, i, 2) %!error logninv (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/lognpdf.m000066400000000000000000000130511527311104300246140ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} lognpdf (@var{x}) ## @deftypefnx {statistics} {@var{y} =} lognpdf (@var{x}, @var{mu}) ## @deftypefnx {statistics} {@var{y} =} lognpdf (@var{x}, @var{mu}, @var{sigma}) ## ## Lognormal probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the lognormal distribution with mean parameter @var{mu} and standard ## deviation parameter @var{sigma}, each corresponding to the associated normal ## distribution. The size of @var{y} is the common size of @var{p}, @var{mu}, ## and @var{sigma}. A scalar input functions as a constant matrix of the same ## size as the other inputs. ## ## If a random variable follows this distribution, its logarithm is normally ## distributed with mean @var{mu} and standard deviation @var{sigma}. ## ## Default parameter values are @qcode{@var{mu} = 0} and ## @qcode{@var{sigma} = 1}. Both parameters must be reals and ## @qcode{@var{sigma} > 0}. For @qcode{@var{sigma} <= 0}, @qcode{NaN} is ## returned. ## ## Further information about the lognormal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Log-normal_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{logncdf, logninv, lognrnd, lognfit, lognlike, lognstat} ## @end deftypefn function y = lognpdf (x, mu = 0, sigma = 1) ## Check for valid number of input arguments if (nargin < 1 || nargin > 3) print_usage (); endif ## Check for common size of P, MU, and SIGMA if (! isscalar (x) || ! isscalar (mu) || ! isscalar (sigma)) [retval, x, mu, sigma] = common_size (x, mu, sigma); if (retval > 0) error ("lognpdf: X, MU, and SIGMA must be of common size or scalars"); endif endif ## Check for X, MU, and SIGMA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (sigma))) error ("lognpdf: X, MU, and SIGMA must be double or single."); endif ## Check for X, MU, and SIGMA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (sigma))) error ("lognpdf: X, MU, and SIGMA must be double or single."); endif ## Check for X, MU, and SIGMA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (sigma)) error ("lognpdf: X, MU, and SIGMA must not be complex"); endif ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (sigma, 'single')) y = zeros (size (x), 'single'); else y = zeros (size (x)); endif ## Compute lognormal PDF k = isnan (x) | ! (sigma > 0) | ! (sigma < Inf); y(k) = NaN; k = (x > 0) & (x < Inf) & (sigma > 0) & (sigma < Inf); if (isscalar (mu) && isscalar (sigma)) y(k) = normpdf (log (x(k)), mu, sigma) ./ x(k); else y(k) = normpdf (log (x(k)), mu(k), sigma(k)) ./ x(k); endif endfunction %!demo %! ## Plot various PDFs from the log-normal distribution %! x = 0:0.01:5; %! y1 = lognpdf (x, 0, 1); %! y2 = lognpdf (x, 0, 0.5); %! y3 = lognpdf (x, 0, 0.25); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r') %! grid on %! ylim ([0, 2]) %! legend ({'μ = 0, σ = 1', 'μ = 0, σ = 0.5', 'μ = 0, σ = 0.25'}, ... %! 'location', 'northeast') %! title ('Log-normal PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x, y %! x = [-1 0 e Inf]; %! y = [0, 0, 1/(e*sqrt(2*pi)) * exp(-1/2), 0]; %!assert_equal (lognpdf (x, zeros (1,4), ones (1,4)), y, eps) %!assert_equal (lognpdf (x, 0, ones (1,4)), y, eps) %!assert_equal (lognpdf (x, zeros (1,4), 1), y, eps) %!assert_equal (lognpdf (x, [0 1 NaN 0], 1), [0 0 NaN y(4)], eps) %!assert_equal (lognpdf (x, 0, [0 NaN Inf 1]), [NaN NaN NaN y(4)], eps) %!assert_equal (lognpdf ([x, NaN], 0, 1), [y, NaN], eps) ## Test class of input preserved %!assert_equal (lognpdf (single ([x, NaN]), 0, 1), single ([y, NaN]), eps ('single')) %!assert_equal (lognpdf ([x, NaN], single (0), 1), single ([y, NaN]), eps ('single')) %!assert_equal (lognpdf ([x, NaN], 0, single (1)), single ([y, NaN]), eps ('single')) ## Test input validation %!error lognpdf (int32 (2), 0, 1) %!error lognpdf (true, 0, 1) %!error lognpdf ('a', 0, 1) %!error lognpdf () %!error lognpdf (1,2,3,4) %!error lognpdf (ones (3), ones (2), ones (2)) %!error lognpdf (ones (2), ones (3), ones (2)) %!error lognpdf (ones (2), ones (2), ones (3)) %!error lognpdf (i, 2, 2) %!error lognpdf (2, i, 2) %!error lognpdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/lognrnd.m000066400000000000000000000160611527311104300246320ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} lognrnd (@var{mu}, @var{sigma}) ## @deftypefnx {statistics} {@var{r} =} lognrnd (@var{mu}, @var{sigma}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} lognrnd (@var{mu}, @var{sigma}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} lognrnd (@var{mu}, @var{sigma}, [@var{sz}]) ## ## Random arrays from the lognormal distribution. ## ## @code{@var{r} = lognrnd (@var{mu}, @var{sigma})} returns an array of random ## numbers chosen from the lognormal distribution with mean parameter @var{mu} ## and standard deviation parameter @var{sigma}, each corresponding to the ## associated normal distribution. The size of @var{r} is the common size of ## @var{mu}, and @var{sigma}. A scalar input functions as a constant matrix of ## the same size as the other inputs. Both parameters must be reals and ## @qcode{@var{sigma} > 0}. For @qcode{@var{sigma} <= 0}, @qcode{NaN} is ## returned. ## ## Both parameters must be reals and @qcode{@var{sigma} > 0}. ## For @qcode{@var{sigma} <= 0}, @qcode{NaN} is returned. ## ## When called with a single size argument, @code{lognrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the lognormal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Log-normal_distribution} ## ## @seealso{logncdf, logninv, lognpdf, lognfit, lognlike, lognstat} ## @end deftypefn function r = lognrnd (mu, sigma, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("lognrnd: function called with too few input arguments."); endif ## Check for common size of P, MU, and SIGMA if (! isscalar (mu) || ! isscalar (sigma)) [retval, mu, sigma] = common_size (mu, sigma); if (retval > 0) error ("lognrnd: MU and SIGMA must be of common size or scalars."); endif endif ## Check for X, MU, and SIGMA being reals if (iscomplex (mu) || iscomplex (sigma)) error ("lognrnd: MU and SIGMA must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (mu); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("lognrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("lognrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (mu) && ! isequal (size (mu), size (ones (sz)))) error ("lognrnd: MU and SIGMA must be scalars or of size SZ."); endif ## Check for class type if (isa (mu, 'single') || isa (sigma, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from lognormal distribution if (isscalar (mu) && isscalar (sigma)) if ((sigma > 0) && (sigma < Inf)) r = exp (mu + sigma * randn (sz, cls)); else r = NaN (sz, cls); endif else r = exp (mu + sigma .* randn (sz, cls)); k = (sigma < 0) | (sigma == Inf); r(k) = NaN; endif endfunction ## Test output %!assert_equal (size (lognrnd (1, 1)), [1, 1]) %!assert_equal (size (lognrnd (1, ones (2, 1))), [2, 1]) %!assert_equal (size (lognrnd (1, ones (2, 2))), [2, 2]) %!assert_equal (size (lognrnd (ones (2, 1), 1)), [2, 1]) %!assert_equal (size (lognrnd (ones (2, 2), 1)), [2, 2]) %!assert_equal (size (lognrnd (1, 1, 3)), [3, 3]) %!assert_equal (size (lognrnd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (lognrnd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (lognrnd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (lognrnd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (lognrnd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (lognrnd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (lognrnd (1, 1, [])), [0, 0]) %!assert_equal (size (lognrnd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (lognrnd (1, 2, -1)), [0, 0]) %!assert_equal (size (lognrnd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (lognrnd (1, 2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (lognrnd (1, 1)), "double") %!assert_equal (class (lognrnd (1, single (1))), "single") %!assert_equal (class (lognrnd (1, single ([1, 1]))), "single") %!assert_equal (class (lognrnd (single (1), 1)), "single") %!assert_equal (class (lognrnd (single ([1, 1]), 1)), "single") ## Test input validation %!error lognrnd () %!error lognrnd (1) %!error ... %! lognrnd (ones (3), ones (2)) %!error ... %! lognrnd (ones (2), ones (3)) %!error lognrnd (i, 2, 3) %!error lognrnd (1, i, 3) %!error ... %! lognrnd (1, 2, 1.2) %!error ... %! lognrnd (1, 2, ones (2)) %!error ... %! lognrnd (1, 2, [2 0 2.5]) %!error ... %! lognrnd (1, 2, 2, 1.5, 5) %!error ... %! lognrnd (2, ones (2), 3) %!error ... %! lognrnd (2, ones (2), [3, 2]) %!error ... %! lognrnd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/mnpdf.m000066400000000000000000000121131527311104300242650ustar00rootroot00000000000000## Copyright (C) 2012 Arno Onken ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} mnpdf (@var{x}, @var{pk}) ## ## Multinomial probability density function (PDF). ## ## @subheading Arguments ## ## @itemize @bullet ## @item ## @var{x} is vector with a single sample of a multinomial distribution with ## parameter @var{pk} or a matrix of random samples from multinomial ## distributions. In the latter case, each row of @var{x} is a sample from a ## multinomial distribution with the corresponding row of @var{pk} being its ## parameter. ## ## @item ## @var{pk} is a vector with the probabilities of the categories or a matrix ## with each row containing the probabilities of a multinomial sample. ## @end itemize ## ## @subheading Return values ## ## @itemize @bullet ## @item ## @var{y} is a vector of probabilities of the random samples @var{x} from the ## multinomial distribution with corresponding parameter @var{pk}. The parameter ## @var{n} of the multinomial distribution is the sum of the elements of each ## row of @var{x}. The length of @var{y} is the number of columns of @var{x}. ## If a row of @var{pk} does not sum to @code{1}, then the corresponding element ## of @var{y} will be @code{NaN}. ## @end itemize ## ## @subheading Examples ## ## @example ## @group ## x = [1, 4, 2]; ## pk = [0.2, 0.5, 0.3]; ## y = mnpdf (x, pk); ## @end group ## ## @group ## x = [1, 4, 2; 1, 0, 9]; ## pk = [0.2, 0.5, 0.3; 0.1, 0.1, 0.8]; ## y = mnpdf (x, pk); ## @end group ## @end example ## ## @subheading References ## ## @enumerate ## @item ## Wendy L. Martinez and Angel R. Martinez. @cite{Computational Statistics ## Handbook with MATLAB}. Appendix E, pages 547-557, Chapman & Hall/CRC, 2001. ## ## @item ## Merran Evans, Nicholas Hastings and Brian Peacock. @cite{Statistical ## Distributions}. pages 134-136, Wiley, New York, third edition, 2000. ## @end enumerate ## ## Input arguments must be @qcode{double}, @qcode{single}, or an integer type; ## logical and character arrays are rejected. Integer input is promoted to ## @qcode{double}, so the result is always a probability. MATLAB is ## inconsistent here: for several of the discrete distributions it returns the ## result in the integer class of the input, truncating a probability to ## @math{0} or @math{1}. ## ## @seealso{mnrnd} ## @end deftypefn function y = mnpdf (x, pk) # Check arguments if (nargin != 2) print_usage (); endif ## Check for X and PK being double, single, or integer if (! (isnumeric (x) && isnumeric (pk))) error ("mnpdf: X and PK must be double, single, or integer."); endif ## Integer input is promoted to double, so the result is a probability ## rather than a value truncated to the input's integer type. if (isinteger (x)) x = double (x); endif if (isinteger (pk)) pk = double (pk); endif if (! ismatrix (x) || any (x(:) < 0 | round (x(:) != x(:)))) error ("mnpdf: X must be a matrix of non-negative integer values."); endif if (! ismatrix (pk) || any (pk(:) < 0)) error ("mnpdf: PK must be a non-empty matrix with rows of probabilities."); endif # Adjust input sizes if (! isvector (x) || ! isvector (pk)) if (isvector (x)) x = x(:)'; endif if (isvector (pk)) pk = pk(:)'; endif if (size (x, 1) == 1 && size (pk, 1) > 1) x = repmat (x, size (pk, 1), 1); elseif (size (x, 1) > 1 && size (pk, 1) == 1) pk = repmat (pk, size (x, 1), 1); endif endif # Continue argument check if (any (size (x) != size (pk))) error ("mnpdf: X and PK must have compatible sizes."); endif # Count total number of elements of each multinomial sample n = sum (x, 2); # Compute probability density function of the multinomial distribution t = x .* log (pk); t(x == 0) = 0; y = exp (gammaln (n+1) - sum (gammaln (x+1), 2) + sum (t, 2)); # Set invalid rows to NaN k = (abs (sum (pk, 2) - 1) > 1e-6); y(k) = NaN; endfunction %!test %! x = [1, 4, 2]; %! pk = [0.2, 0.5, 0.3]; %! y = mnpdf (x, pk); %! assert_equal (y, 0.11812, 0.001); %!test %! x = [1, 4, 2; 1, 0, 9]; %! pk = [0.2, 0.5, 0.3; 0.1, 0.1, 0.8]; %! y = mnpdf (x, pk); %! assert_equal (y, [0.11812; 0.13422], 0.001); ## Test input validation %!error mnpdf ([true, true], [0.3, 0.7]) %!error mnpdf ('ab', [0.3, 0.7]) %!assert_equal (class (mnpdf (int32 ([1, 2]), [0.3, 0.7])), 'double') statistics-release-2.1.2/inst/Distribution_Functions/mnrnd.m000066400000000000000000000140631527311104300243050ustar00rootroot00000000000000## Copyright (C) 2012 Arno Onken ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} mnrnd (@var{n}, @var{pk}) ## @deftypefnx {statistics} {@var{r} =} mnrnd (@var{n}, @var{pk}, @var{s}) ## ## Random arrays from the multinomial distribution. ## ## @subheading Arguments ## ## @itemize @bullet ## @item ## @var{n} is the first parameter of the multinomial distribution. @var{n} can ## be scalar or a vector containing the number of trials of each multinomial ## sample. The elements of @var{n} must be non-negative integers. ## ## @item ## @var{pk} is the second parameter of the multinomial distribution. @var{pk} ## can be a vector with the probabilities of the categories or a matrix with ## each row containing the probabilities of a multinomial sample. If @var{pk} ## has more than one row and @var{n} is non-scalar, then the number of rows of ## @var{pk} must match the number of elements of @var{n}. ## ## @item ## @var{s} is the number of multinomial samples to be generated. @var{s} must ## be a non-negative integer. If @var{s} is specified, then @var{n} must be ## scalar and @var{pk} must be a vector. ## @end itemize ## ## @subheading Return values ## ## @itemize @bullet ## @item ## @var{r} is a matrix of random samples from the multinomial distribution with ## corresponding parameters @var{n} and @var{pk}. Each row corresponds to one ## multinomial sample. The number of columns, therefore, corresponds to the ## number of columns of @var{pk}. If @var{s} is not specified, then the number ## of rows of @var{r} is the maximum of the number of elements of @var{n} and ## the number of rows of @var{pk}. If a row of @var{pk} does not sum to ## @code{1}, then the corresponding row of @var{r} will contain only @code{NaN} ## values. ## @end itemize ## ## @subheading Examples ## ## @example ## @group ## n = 10; ## pk = [0.2, 0.5, 0.3]; ## r = mnrnd (n, pk); ## @end group ## ## @group ## n = 10 * ones (3, 1); ## pk = [0.2, 0.5, 0.3]; ## r = mnrnd (n, pk); ## @end group ## ## @group ## n = (1:2)'; ## pk = [0.2, 0.5, 0.3; 0.1, 0.1, 0.8]; ## r = mnrnd (n, pk); ## @end group ## @end example ## ## @subheading References ## ## @enumerate ## @item ## Wendy L. Martinez and Angel R. Martinez. @cite{Computational Statistics ## Handbook with MATLAB}. Appendix E, pages 547-557, Chapman & Hall/CRC, 2001. ## ## @item ## Merran Evans, Nicholas Hastings and Brian Peacock. @cite{Statistical ## Distributions}. pages 134-136, Wiley, New York, third edition, 2000. ## @end enumerate ## ## @seealso{mnpdf} ## @end deftypefn function r = mnrnd (n, pk, s) # Check arguments if (nargin == 3) if (! isscalar (n) || n < 0 || round (n) != n) error ("mnrnd: N must be a non-negative integer."); endif if (! isvector (pk) || any (pk < 0 | pk > 1)) error ("mnrnd: PK must be a vector of probabilities."); endif if (! isscalar (s) || s < 0 || round (s) != s) error ("mnrnd: S must be a non-negative integer."); endif elseif (nargin == 2) if (isvector (pk) && size (pk, 1) > 1) pk = pk'; endif if (! isvector (n) || any (n < 0 | round (n) != n) || size (n, 2) > 1) error ("mnrnd: N must be a non-negative integer column vector."); endif if (! ismatrix (pk) || isempty (pk) || any (pk < 0 | pk > 1)) error (strcat ("mnrnd: PK must be a non-empty matrix with", ... " rows of probabilities.")); endif if (! isscalar (n) && size (pk, 1) > 1 && length (n) != size (pk, 1)) error ("mnrnd: the length of N must match the number of rows of PK."); endif else print_usage (); endif # Adjust input sizes if (nargin == 3) n = n * ones (s, 1); pk = repmat (pk(:)', s, 1); elseif (nargin == 2) if (isscalar (n) && size (pk, 1) > 1) n = n * ones (size (pk, 1), 1); elseif (size (pk, 1) == 1) pk = repmat (pk, length (n), 1); endif endif sz = size (pk); # Upper bounds of categories ub = cumsum (pk, 2); # Make sure that the greatest upper bound is 1 gub = ub(:, end); ub(:, end) = 1; # Lower bounds of categories lb = [zeros(sz(1), 1) ub(:, 1:(end-1))]; # Draw multinomial samples r = zeros (sz); for i = 1:sz(1) # Draw uniform random numbers r_tmp = repmat (rand (n(i), 1), 1, sz(2)); # Compare the random numbers of r_tmp to the cumulated probabilities of pk # and count the number of samples for each category r(i, :) = sum (r_tmp <= repmat (ub(i, :), n(i), 1) & ... r_tmp > repmat (lb(i, :), n(i), 1), 1); endfor # Set invalid rows to NaN k = (abs (gub - 1) > 1e-6); r(k, :) = NaN; endfunction %!test %! n = 10; %! pk = [0.2, 0.5, 0.3]; %! r = mnrnd (n, pk); %! assert_equal (size (r), size (pk)); %! assert_equal (all ((all (r >= 0))(:)), true); %! assert_equal (all ((all (round (r) == r))(:)), true); %! assert_equal (all ((sum (r) == n)(:)), true); %!test %! n = 10 * ones (3, 1); %! pk = [0.2, 0.5, 0.3]; %! r = mnrnd (n, pk); %! assert_equal (size (r), [length(n), length(pk)]); %! assert_equal (all ((all (r >= 0))(:)), true); %! assert_equal (all ((all (round (r) == r))(:)), true); %! assert_equal (all ((all (sum (r, 2) == n))(:)), true); %!test %! n = (1:2)'; %! pk = [0.2, 0.5, 0.3; 0.1, 0.1, 0.8]; %! r = mnrnd (n, pk); %! assert_equal (size (r), size (pk)); %! assert_equal (all ((all (r >= 0))(:)), true); %! assert_equal (all ((all (round (r) == r))(:)), true); %! assert_equal (all ((all (sum (r, 2) == n))(:)), true); statistics-release-2.1.2/inst/Distribution_Functions/mvncdf.m000066400000000000000000000426421527311104300244500ustar00rootroot00000000000000## Copyright (C) 2008 Arno Onken ## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} mvncdf (@var{x}) ## @deftypefnx {statistics} {@var{p} =} mvncdf (@var{x}, @var{mu}, @var{sigma}) ## @deftypefnx {statistics} {@var{p} =} mvncdf (@var{x_lo}, @var{x_up}, @var{mu}, @var{sigma}) ## @deftypefnx {statistics} {@var{p} =} mvncdf (@dots{}, @var{options}) ## @deftypefnx {statistics} {[@var{p}, @var{err}] =} mvncdf (@dots{}) ## ## Multivariate normal cumulative distribution function (CDF). ## ## @code{@var{p} = mvncdf (@var{x})} returns the cumulative probability of the ## multivariate normal distribution evaluated at each row of @var{x} with zero ## mean and an identity covariance matrix. The rows of matrix @var{x} ## correspond to observations and its columns to variables. The return argument ## @var{p} is a column vector with the same number of rows as in @var{x}. ## ## @code{@var{p} = mvncdf (@var{x}, @var{mu}, @var{sigma})} returns cumulative ## probability of the multivariate normal distribution evaluated at each row of ## @var{x} with mean @var{mu} and a covariance matrix @var{sigma}. @var{mu} can ## be either a scalar (the same of every variable) or a row vector with the same ## number of elements as the number of variables in @var{x}. @var{sigma} ## covariance matrix may be specified a row vector if it only contains variances ## along its diagonal and zero covariances of the diagonal. In such a case, the ## diagonal vector @var{sigma} must have the same number of elements as the ## number of variables (columns) in @var{x}. If you only want to specify sigma, ## you can pass an empty matrix for @var{mu}. ## ## The multivariate normal cumulative probability at @var{x} is defined as the ## probability that a random vector @math{V}, distributed as multivariate ## normal, will fall within the semi-infinite rectangle with upper limits ## defined by @var{x}. ## @itemize ## @item @math{Pr@{V(1)<=X(1), V(2)<=X(2), ... V(D)<=X(D)@}}. ## @end itemize ## ## @code{@var{p} = mvncdf (@var{x_lo}, @var{x_hi}, @var{mu}, @var{sigma})} ## returns the multivariate normal cumulative probability evaluated over the ## rectangle (hyper-rectangle for multivariate data in @var{x}) with lower and ## upper limits defined by @var{x_lo} and @var{x_hi}, respectively. ## ## @code{[@var{p}, @var{err}] = mvncdf (@dots{})} also returns an error estimate ## @var{err} in @var{p}. ## ## @code{@var{p} = mvncdf (@dots{}, @var{options})} specifies the structure, ## which controls specific parameters for the numerical integration used to ## compute @var{p}, as @code{statset} makes it. A field that is missing or ## empty takes its default. The fields are: ## ## @multitable @columnfractions 0.2 0.75 ## @item @qcode{'TolFun'} @tab Maximum absolute error tolerance. Default ## is 1e-8 for D < 4, or 1e-4 for D >= 4. Note that for bivariate normal cdf, ## the Octave implementation has a precision of more than 1e-10. ## ## @item @qcode{'MaxFunEvals'} @tab Maximum number of integrand ## evaluations. Default is 1e7 for D > 4. ## ## @item @qcode{'Display'} @tab Display options. Choices are @qcode{'off'} ## (default), @qcode{'iter'}, which shows the probability and estimated error at ## each repetition, and @qcode{'final'}, which shows the final probability and ## related error after the integrand has converged successfully. ## @end multitable ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{bvncdf, mvnpdf, mvnrnd} ## @end deftypefn function [p, err] = mvncdf (varargin) ## Check for valid number on input and output arguments narginchk (1,5); ## Check for X, MU, and SIGMA being double or single. The trailing ## 'options' struct, if present, is parsed separately below. numargs = varargin(! cellfun (@isstruct, varargin)); if (! all (cellfun (@isfloat, numargs))) error ("mvncdf: X, MU, and SIGMA must be double or single."); endif ## Defaults, which a field of the 'options' structure overrides where it ## is present and not empty, as a structure from statset leaves unset ## fields empty if (size (varargin{1}, 2) < 4) TolFun = 1e-8; elseif (size (varargin{1}, 2) < 26) TolFun = 1e-4; endif MaxFunEvals = 1e7; Display = 'off'; rem_nargin = nargin; if (isstruct (varargin{end})) opts = varargin{end}; if (isfield (opts, 'TolFun') && ! isempty (opts.TolFun)) TolFun = opts.TolFun; endif if (isfield (opts, 'MaxFunEvals') && ! isempty (opts.MaxFunEvals)) MaxFunEvals = opts.MaxFunEvals; endif if (isfield (opts, 'Display') && ! isempty (opts.Display)) Display = opts.Display; if (! any (strcmpi (Display, {'off', 'final', 'iter'}))) error ("mvncdf: 'Display' field in 'options' has invalid value."); endif endif rem_nargin = nargin - 1; endif ## Check for X of X_lo and X_up if (rem_nargin < 4) # MVNCDF(X_UP,MU,SIGMA) x_up_Only = true; x_up = varargin{1}; ## Check for x being a matrix if (! ismatrix (x_up)) error ("mvncdf: X must be a matrix."); endif ## Create x_lo according to data type of x_lo x_lo = - Inf (size (x_up)); if isa (x_up, 'single') x_lo = single (x_lo); endif ## Check for mu and sigma arguments if (rem_nargin > 1) mu = varargin{2}; else mu = []; endif if (rem_nargin > 2) sigma = varargin{3}; else sigma = []; endif else # MVNCDF(X_LO,X_UP,MU,SIGMA) x_up_Only = false; x_lo = varargin{1}; x_up = varargin{2}; mu = varargin{3}; sigma = varargin{4}; ## Check for x_lo and x_up being matrices of the same size ## and that they define increasing limits if (! ismatrix (x_lo) || ! ismatrix (x_up)) error ("mvncdf: X_LO and X_UP must be matrices."); endif if (size (x_lo) != size (x_up)) error ("mvncdf: X_LO and X_UP must have the same size."); endif if (any (any (x_lo > x_up))) error ("mvncdf: X_LO and X_UP must define increasing limits."); endif endif ## Check if data is single or double class is_type = 'double'; if (isa (x_lo, 'single')) is_type = 'single'; endif ## Get size of data [n_x, d_x] = size (x_lo); ## Center data according to mu if (isempty (mu)) # already centered XLo0 = x_lo; XUp0 = x_up; elseif (isscalar (mu)) # mu is a scalar XLo0 = x_lo - mu; XUp0 = x_up - mu; elseif (isvector (mu)) # mu is a vector ## Get size of mu vector [n_mu, d_mu] = size (mu); if (d_mu != d_x) error ("mvncdf: wrong size of MU vector."); endif if (n_mu == 1 || n_mu == n_x) XLo0 = x_lo - mu; XUp0 = x_up - mu; else error ("mvncdf: wrong size of MU vector."); endif else error ("mvncdf: MU must be either empty, a scalar, or a vector."); endif ## Check how sigma was parsed if (isempty (sigma)) # already standardized ## If x_lo and x_up are column vectors, transpose them to row vectors if (d_x == 1) XLo0 = XLo0'; XUp0 = XUp0'; [n_x, d_x] = size (XUp0); endif sigmaIsDiag = true; sigma = ones (1, d_x); else ## Check if sigma parsed as diagonal vector if (size (sigma, 1) == 1 && size (sigma, 2) > 1) sigmaIsDiag = true; else sigmaIsDiag = false; endif ## If x_lo and x_up are column vectors, transpose them to row vectors if (d_x == 1) if (isequal (size (sigma), [1, n_x])) XLo0 = XLo0'; XUp0 = XUp0'; [n_x, d_x] = size (XUp0); elseif (! isscalar (mu)) error ("mvncdf: MU must be a scalar if SIGMA is a vector."); endif endif ## Check for sigma being a valid covariance matrix if (! sigmaIsDiag && (size (sigma, 1) != size (sigma, 2))) error ("mvncdf: covariance matrix SIGMA is not square."); elseif (! sigmaIsDiag && (! all (size (sigma) == [d_x, d_x]))) error (strcat ("mvncdf: covariance matrix SIGMA does", ... " not match dimensions in data.")); else ## If sigma is a covariance matrix check that it is positive semi-definite if (! sigmaIsDiag) [~, err] = chol (sigma); if (err != 0) error (strcat ("mvncdf: covariance matrix SIGMA must be", ... " positive semi-definite.")); endif else if (any (sigma <= 0)) error ("mvncdf: invalid SIGMA diagonal vector."); endif endif endif endif ## Standardize sigma and x data if (sigmaIsDiag) XLo0 = XLo0 ./ sqrt (sigma); XUp0 = XUp0 ./ sqrt (sigma); else s = sqrt (diag (sigma))'; XLo0 = XLo0 ./ s; XUp0 = XUp0 ./ s; Rho = sigma ./ (s * s'); endif ## Compute the cdf from standardized values. if (d_x == 1) p = normcdf (XUp0, 0, 1) - normcdf (XLo0, 0, 1); if (nargout > 1) err = NaN (size (p), is_type); endif elseif (sigmaIsDiag) p = prod (normcdf (XUp0, 0, 1) - normcdf (XLo0, 0, 1), 2); if (nargout > 1) err = NaN (size (p), is_type); endif elseif (d_x < 4) if (x_up_Only) # upper limit only if (d_x == 2) p = bvncdf (x_up, mu, sigma); else p = tvncdf (XUp0, Rho([2 3 6]), TolFun); endif else # lower and upper limits present ## Compute the probability over the rectangle as sums and differences ## of integrals over semi-infinite half-rectangles. For degenerate ## rectangles, force an exact zero by making each piece exactly zero. equalLimits = (XUp0 == XLo0); XUp0(equalLimits) = -Inf; XLo0(equalLimits) = -Inf; ## For bvncdf x_up(equalLimits) = -Inf; x_lo(equalLimits) = -Inf; p = zeros (n_x, 1, is_type); for i = 0:d_x k = nchoosek (1:d_x, i); for j = 1:size (k, 1) X = XUp0; X(:,k(j,:)) = XLo0(:,k(j,:)); if d_x == 2 x = x_up; x(:,k(j,:)) = x_lo(:,k(j,:)); p = p + (-1) ^ i * bvncdf (x, mu, sigma); else p = p + (-1) ^ i * tvncdf (X, Rho([2 3 6]), TolFun / 8); endif endfor endfor endif if (nargout > 1) err = repmat (cast (TolFun, is_type), size (p)); endif elseif (d_x < 26) p = zeros (n_x, 1, is_type); err = zeros (n_x, 1, is_type); for i = 1:n_x [p(i), err(i)] = __mvtcdfqmc__ ("mvncdf", XLo0(i,:), XUp0(i,:), ... Rho, Inf, TolFun, MaxFunEvals, ... Display); endfor else error ("mvncdf: too many dimensions in data (limit = 25 columns)."); endif ## Bound p in range [0, 1] p(p < 0) = 0; p(p > 1) = 1; endfunction ## function for computing a trivariate normal cdf function p = tvncdf (x, rho, tol) ## Get size of data n = size (x,1); ## Check if data is single or double class is_type = 'double'; if (isa (x, 'single') || isa (rho, 'single')) is_type = 'single'; endif ## Find a permutation that makes rho_32 == max(rho) [dum,imax] = max (abs (rho)); if imax == 1 # swap 1 and 3 rho_21 = rho(3); rho_31 = rho(2); rho_32 = rho(1); x = x(:,[3 2 1]); elseif imax == 2 # swap 1 and 2 rho_21 = rho(1); rho_31 = rho(3); rho_32 = rho(2); x = x(:,[2 1 3]); else # imax == 3 rho_21 = rho(1); rho_31 = rho(2); rho_32 = rho(3); endif phi = 0.5 * erfc (- x(:,1) / sqrt (2)); p1 = phi .* bvncdf (x(:,2:3), [], rho_32); if abs (rho_21) > 0 loLimit = 0; hiLimit = asin (rho_21); rho_j1 = rho_21; rho_k1 = rho_31; p2 = zeros (size (p1), is_type); for i = 1:n b1 = x(i,1); bj = x(i,2); bk = x(i,3); if isfinite (b1) && isfinite (bj) && ! isnan (bk) p2(i) = quadgk (@tvnIntegrand,loLimit,hiLimit,'AbsTol',tol/3,'RelTol',0); endif endfor else p2 = zeros (size (p1), is_type); endif if abs (rho_31) > 0 loLimit = 0; hiLimit = asin (rho_31); rho_j1 = rho_31; rho_k1 = rho_21; p3 = zeros (size (p1), is_type); for i = 1:n b1 = x(i,1); bj = x(i,3); bk = x(i,2); if isfinite (b1) && isfinite (bj) && ! isnan (bk) p3(i) = quadgk (@tvnIntegrand,loLimit,hiLimit,'AbsTol',tol/3,'RelTol',0); endif endfor else p3 = zeros (size (p1), is_type); endif p = cast (p1 + (p2 + p3) ./ (2 .* pi), is_type); function integrand = tvnIntegrand (theta) # Integrand is exp( -(b1.^2 + bj.^2 - 2*b1*bj*sin(theta))/(2*cos(theta).^2)) sintheta = sin (theta); cossqtheta = cos (theta) .^ 2; expon = ((b1 * sintheta - bj) .^ 2 ./ cossqtheta + b1 .^ 2) / 2; sinphi = sintheta .* rho_k1 ./ rho_j1; numeru = bk .* cossqtheta - b1 .* (sinphi - rho_32 .* sintheta) ... - bj .* (rho_32 - sintheta .* sinphi); denomu = sqrt (cossqtheta .* (cossqtheta - sinphi .* sinphi ... - rho_32 .* (rho_32 - 2 .* sintheta .* sinphi))); phi = 0.5 * erfc (- (numeru ./ denomu) / sqrt (2)); integrand = exp (- expon) .* phi; endfunction endfunction %!demo %! mu = [1, -1]; %! Sigma = [0.9, 0.4; 0.4, 0.3]; %! [X1, X2] = meshgrid (linspace (-1, 3, 25)', linspace (-3, 1, 25)'); %! X = [X1(:), X2(:)]; %! p = mvncdf (X, mu, Sigma); %! Z = reshape (p, 25, 25); %! surf (X1, X2, Z); %! title ('Bivariate Normal Distribution'); %! ylabel 'X1' %! xlabel 'X2' %!demo %! mu = [0, 0]; %! Sigma = [0.25, 0.3; 0.3, 1]; %! p = mvncdf ([0 0], [1 1], mu, Sigma); %! x1 = -3:.2:3; %! x2 = -3:.2:3; %! [X1, X2] = meshgrid (x1, x2); %! X = [X1(:), X2(:)]; %! p = mvnpdf (X, mu, Sigma); %! p = reshape (p, length (x2), length (x1)); %! contour (x1, x2, p, [0.0001, 0.001, 0.01, 0.05, 0.15, 0.25, 0.35]); %! xlabel ('x'); %! ylabel ('p'); %! title ('Probability over Rectangular Region'); %! line ([0, 0, 1, 1, 0], [1, 0, 0, 1, 1], 'Linestyle', '--', 'Color', 'k'); %!test %! fD = (-2:2)'; %! X = repmat (fD, 1, 4); %! p = mvncdf (X); %! assert_equal (p, [0; 0.0006; 0.0625; 0.5011; 0.9121], ones (5, 1) * 1e-4); %!test %! mu = [1, -1]; %! Sigma = [0.9, 0.4; 0.4, 0.3]; %! [X1,X2] = meshgrid (linspace (-1, 3, 25)', linspace (-3, 1, 25)'); %! X = [X1(:), X2(:)]; %! p = mvncdf (X, mu, Sigma); %! p_out = [0.00011878988774500, 0.00034404112322371, ... %! 0.00087682502191813, 0.00195221905058185, ... %! 0.00378235566873474, 0.00638175749734415, ... %! 0.00943764224329656, 0.01239164888125426, ... %! 0.01472750274376648, 0.01623228313374828]'; %! assert_equal (p([1:10]), p_out, 1e-16); %!test %! mu = [1, -1]; %! Sigma = [0.9, 0.4; 0.4, 0.3]; %! [X1,X2] = meshgrid (linspace (-1, 3, 25)', linspace (-3, 1, 25)'); %! X = [X1(:), X2(:)]; %! p = mvncdf (X, mu, Sigma); %! p_out = [0.8180695783608276, 0.8854485749482751, ... %! 0.9308108777385832, 0.9579855743025508, ... %! 0.9722897881414742, 0.9788150170059926, ... %! 0.9813597788804785, 0.9821977956568989, ... %! 0.9824283794464095, 0.9824809345614861]'; %! assert_equal (p([616:625]), p_out, 3e-16); %!test %! mu = [0, 0]; %! Sigma = [0.25, 0.3; 0.3, 1]; %! [p, err] = mvncdf ([0, 0], [1, 1], mu, Sigma); %! assert_equal (p, 0.2097424404755626, 1e-16); %! assert_equal (err, 1e-08); %!test %! x = [1 2]; %! mu = [0.5 1.5]; %! sigma = [1.0, 0.5; 0.5, 1.0]; %! p = mvncdf (x, mu, sigma); %! assert_equal (p, 0.546244443857090, 1e-15); %!test %! x = [1 2]; %! mu = [0.5 1.5]; %! sigma = [1.0, 0.5; 0.5, 1.0]; %! a = [-inf 0]; %! p = mvncdf (a, x, mu, sigma); %! assert_equal (p, 0.482672935215631, 1e-15); %!test # MATLAB parity: an options field missing or empty takes its default %! rho = [1, 0.5; 0.5, 1]; %! p = 1 / 3; %! o = statset ('TolFun', 1e-4); %! assert_equal (mvncdf ([0, 0], [0, 0], rho, o), p, 1e-8); %! o = struct ('TolFun', 1e-4); %! assert_equal (mvncdf ([0, 0], [0, 0], rho, o), p, 1e-8); %! assert_equal (mvncdf ([0, 0], [0, 0], rho, struct ('Display', 'final')), ... %! p, 1e-8); %!error mvncdf (int32 ([0, 0])) %!error mvncdf ([true, true]) %!error mvncdf ('ab') %!error ... %! mvncdf ([0, 0], [0, 0], eye (2), struct ('Display', 'bogus')) %!error p = mvncdf (randn (25,26), [], eye (26)); %!error p = mvncdf (randn (25,8), [], eye (9)); %!error p = mvncdf (randn (25,4), randn (25,5), [], eye (4)); %!error p = mvncdf (randn (25,4), randn (25,4), [2, 3; 2, 3], eye (4)); %!error p = mvncdf (randn (25,4), randn (25,4), ones (1, 5), eye (4)); %!error p = mvncdf ([-inf, 0], [1, 2], [0.5, 1.5], [1.0, 0.5; 0.5, 1.0], option) statistics-release-2.1.2/inst/Distribution_Functions/mvnpdf.m000066400000000000000000000214531527311104300244620ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} mvnpdf (@var{x}, @var{mu}, @var{sigma}) ## ## Multivariate normal probability density function (PDF). ## ## @code{@var{y} = mvnpdf (@var{x})} returns the probability density of the ## multivariate normal distribution with zero mean and identity covariance ## matrix, evaluated at each row of @var{x}. Rows of the N-by-D matrix @var{x} ## correspond to observations orpoints, and columns correspond to variables or ## coordinates. @var{y} is an N-by-1 vector. ## ## @code{@var{y} = mvnpdf (@var{x}, @var{mu})} returns the density of the ## multivariate normal distribution with mean MU and identity covariance matrix, ## evaluated at each row of @var{x}. @var{mu} is a 1-by-D vector, or an N-by-D ## matrix, in which case the density is evaluated for each row of @var{x} with ## the corresponding row of @var{mu}. @var{mu} can also be a scalar value, ## which MVNPDF replicates to match the size of @var{x}. ## ## @code{@var{y} = mvnpdf (@var{x}, @var{mu}, @var{sigma})} returns the density ## of the multivariate normal distribution with mean @var{mu} and covariance ## @var{sigma}, evaluated at each row of @var{x}. @var{sigma} is a D-by-D ## matrix, or an D-by-D-by-N array, in which case the density is evaluated for ## each row of @var{x} with the corresponding page of @var{sigma}, i.e., ## @code{mvnpdf} computes @var{y(i)} using @var{x(i,:)} and @var{sigma(:,:,i)}. ## If the covariance matrix is diagonal, containing variances along the diagonal ## and zero covariances off the diagonal, @var{sigma} may also be specified as a ## 1-by-D matrix or a 1-by-D-by-N array, containing just the diagonal. Pass in ## the empty matrix for @var{mu} to use its default value when you want to only ## specify @var{sigma}. ## ## If @var{x} is a 1-by-D vector, @code{mvnpdf} replicates it to match the ## leading dimension of @var{mu} or the trailing dimension of @var{sigma}. ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{mvncdf, mvnrnd} ## @end deftypefn function y = mvnpdf (x, mu, sigma) ## Check for valid number of input arguments if (nargin < 1) error ("mvnpdf: too few input arguments."); endif if (nargin < 2) mu = []; endif if (nargin < 3) sigma = []; endif ## Check for X, MU, and SIGMA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (sigma))) error ("mvnpdf: X, MU, and SIGMA must be double or single."); endif ## Check for valid size of data [row, col] = size (x); if (col < 1) error ("mvnpdf: too few dimensions in X."); elseif (ndims (x) != 2) error ("mvnpdf: wrong dimensions in X."); endif ## Check for second input argument or assume zero mean if (nargin < 2 || isempty (mu)) xc = x; # already centered elseif (numel (mu) == 1) xc = x - mu; # mu is a scalar elseif (ndims (mu) == 2) [rm, cm] = size (mu); # mu is a vector if (cm != col) error ("mvnpdf: columns in X and MU mismatch."); elseif (rm == row) xc = x - mu; elseif (rm == 1 || row == 1) xc = bsxfun (@minus, x, mu); else error ("mvnpdf: rows in X and MU mismatch."); endif else error ("mvnpdf: wrong size of MU."); endif [row, col] = size (xc); ## Check for third input argument or assume identity covariance if (nargin < 2 || isempty (sigma)) ## already standardized if (col == 1 && row > 1) xRinv = xc'; # make row vector col == row; else xRinv = xc; col == row; endif lnSDS = 0; elseif (ndims (sigma) == 2) ## Single covariance matrix [rs, cs] = size (sigma); if (rs == 1 && cs > 1) rs = cs; # sigma passed as a diagonal is_diag = true; else is_diag = false; endif if (col == 1 && row > 1 && rs == row) xc = xc'; # make row vector col = row; endif ## Check sigma for correct size if (rs != cs) error ("mvnpdf: bad covariance matrix."); elseif (rs != col) error ("mvnpdf: covariance matrix mismatch."); else if (is_diag) ## Check sigma for invalid values if (any (sigma <= 0)) error ("mvnpdf: sigma diagonal contains negative or zero values."); endif R = sqrt (sigma); xRinv = bsxfun (@rdivide, xc, R); lnSDS = sum (log (R)); else ## Check for valid covariance matrix [R, err] = cholcov (sigma, 0); if (err != 0) error ("mvnpdf: invalid covariance matrix."); endif xRinv = xc / R; lnSDS = sum (log (diag (R))); endif endif elseif (ndims (sigma) == 3) ## Multiple covariance matrices sd = size (sigma); if (sd(1) == 1 && sd(2) > 1) sd(1) = sd(2); # sigma passed as a diagonal sigma = reshape (sigma, sd(2), sd(3))'; is_diag = true; else is_diag = false; endif if (col == 1 && row > 1 && sd(1) == row) xc = xc'; # make row vector [row, col] = size (xc); endif ## If X and MU are row vectors, match them with covariance if (row == 1) row = sd(3); xc = repmat (xc, row, 1); endif ## Check sigma for correct size if (sd(1) != sd(2)) error ("mvnpdf: bad multiple covariance matrix."); elseif (sd(1) != col || sd(2) != col) error ("mvnpdf: multiple covariance matrix mismatch."); elseif (sd(3) != row) error ("mvnpdf: multiple covariance pages mismatch."); else if (is_diag) ## Check sigma for invalid values if (any (any (sigma <= 0))) error ("mvnpdf: sigma diagonals contain negative or zero values."); endif R = sqrt (sigma); xRinv = xc ./ R; lnSDS = sum (log (R), 2); else ## Create arrays according to class type if (isa (x, 'single') || isa (mu, 'single') || isa (sigma, 'single')) xRinv = zeros (row, col,' single'); lnSDS = zeros (row, 1, 'single'); else xRinv = zeros (row, col); lnSDS = zeros (row, 1); endif for i = 1:row ## Check for valid covariance matrices [R, err] = cholcov (sigma(:,:,i), 0); if (err != 0) error ("mvnpdf:invalid multiple covariance matrix."); endif xRinv(i,:) = xc(i,:) / R; lnSDS(i) = sum (log (diag (R))); endfor endif endif else error ("mvnpdf: wrong dimensions in covariance matrix."); endif ## Compute the PDF y = exp (-0.5 * sum (xRinv .^ 2, 2) - lnSDS - col * log (2 * pi) / 2); endfunction %!demo %! mu = [1, -1]; %! sigma = [0.9, 0.4; 0.4, 0.3]; %! [X1, X2] = meshgrid (linspace (-1, 3, 25)', linspace (-3, 1, 25)'); %! x = [X1(:), X2(:)]; %! p = mvnpdf (x, mu, sigma); %! surf (X1, X2, reshape (p, 25, 25)); ## Input validation tests %!error mvnpdf (int32 ([0, 0]), [0, 0], eye (2)) %!error mvnpdf ([true, true], [0, 0], eye (2)) %!error mvnpdf ('ab', [0, 0], eye (2)) %!error y = mvnpdf (); %!error y = mvnpdf ([]); %!error y = mvnpdf (ones (3,3,3)); %!error ... %! y = mvnpdf (ones (10, 2), [4, 2, 3]); %!error ... %! y = mvnpdf (ones (10, 2), [4, 2; 3, 2]); %!error ... %! y = mvnpdf (ones (10, 2), ones (3, 3, 3)); ## Output validation tests %!shared x, mu, sigma %! x = [1, 2, 5, 4, 6]; %! mu = [2, 0, -1, 1, 4]; %! sigma = [2, 2, 2, 2, 2]; %!assert_equal (mvnpdf (x), 1.579343404440977e-20, 1e-30); %!assert_equal (mvnpdf (x, mu), 1.899325144348102e-14, 1e-25); %!assert_equal (mvnpdf (x, mu, sigma), 2.449062307156273e-09, 1e-20); statistics-release-2.1.2/inst/Distribution_Functions/mvnrnd.m000066400000000000000000000162641527311104300245000ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} mvnrnd (@var{mu}, @var{sigma}) ## @deftypefnx {statistics} {@var{r} =} mvnrnd (@var{mu}, @var{sigma}, @var{n}) ## @deftypefnx {statistics} {@var{r} =} mvnrnd (@var{mu}, @var{sigma}, @var{n}, @var{T}) ## @deftypefnx {statistics} {[@var{r}, @var{T}] =} mvnrnd (@dots{}) ## ## Random vectors from the multivariate normal distribution. ## ## @code{@var{r} = mvnrnd (@var{mu}, @var{sigma})} returns an N-by-D matrix ## @var{r} of random vectors chosen from the multivariate normal distribution ## with mean vector @var{mu} and covariance matrix @var{sigma}. @var{mu} is an ## N-by-D matrix, and @code{mvnrnd} generates each N of @var{r} using the ## corresponding N of @var{mu}. @var{sigma} is a D-by-D symmetric positive ## semi-definite matrix, or a D-by-D-by-N array. If @var{sigma} is an array, ## @code{mvnrnd} generates each N of @var{r} using the corresponding page of ## @var{sigma}, i.e., @code{mvnrnd} computes @var{r(i,:)} using @var{mu(i,:)} ## and @var{sigma(:,:,i)}. If the covariance matrix is diagonal, containing ## variances along the diagonal and zero covariances off the diagonal, ## @var{sigma} may also be specified as a 1-by-D matrix or a 1-by-D-by-N array, ## containing just the diagonal. If @var{mu} is a 1-by-D vector, @code{mvnrnd} ## replicates it to match the trailing dimension of SIGMA. ## ## @code{@var{r} = mvnrnd (@var{mu}, @var{sigma}, @var{n})} returns a N-by-D ## matrix R of random vectors chosen from the multivariate normal distribution ## with 1-by-D mean vector @var{mu}, and D-by-D covariance matrix @var{sigma}. ## ## @code{@var{r} = mvnrnd (@var{mu}, @var{sigma}, @var{n}, @var{T})} supplies ## the Cholesky factor @var{T} of @var{sigma}, so that @var{sigma(:,:,J)} == ## @var{T(:,:,J)}'*@var{T(:,:,J)} if @var{sigma} is a 3D array or @var{sigma} == ## @var{T}'*@var{T} if @var{sigma} is a matrix. No error checking is done on ## @var{T}. ## ## @code{[@var{r}, @var{T}] = mvnrnd (@dots{})} returns the Cholesky factor ## @var{T}, so it can be re-used to make later calls more efficient, although ## there are greater efficiency gains when SIGMA can be specified as a diagonal ## instead. ## ## @seealso{mvncdf, mvnpdf} ## @end deftypefn function [r, T] = mvnrnd (mu, sigma, N, T) ## Check input arguments if (nargin < 2 || isempty (mu) || isempty (sigma)) error ("mvnrnd: too few input arguments."); elseif (ndims (mu) > 2) error ("mvnrnd: wrong size of MU."); elseif (ndims (sigma) > 3) error ("mvnrnd: wrong size of SIGMA."); endif ## Get data type if (isa (mu, 'single') || isa (sigma, 'single')) is_class = 'single'; else is_class = 'double'; endif ## Check whether sigma is passed as a diagonal or a matrix sd = size (sigma); if (sd(1) == 1 && sd(2) > 1) sd(1) = sd(2); is_diag = true; else is_diag = false; endif ## Get size of mean vector [rm, cm] = size (mu); ## Make sure MU is a row vector if (cm == 1 && rm == sd(1)) mu = mu'; [rm, cm] = size (mu); endif ## Check for valid N input argument if (nargin < 3 || isempty (N)) N_empty = true; else N_empty = false; ## If MU is a row vector, rep it out to match N if (rm == 1) rm = N; mu = repmat (mu, rm, 1); elseif (rm != N) error ("mvnrnd: size mismatch of N and MU."); endif endif ## For single covariance matrix if (ndims (sigma) == 2) ## Check sigma for correct size if (sd(1) != sd(2)) error ("mvnpdf: bad covariance matrix."); elseif (! sd(1) == cm) error ("mvnpdf: covariance matrix mismatch."); endif ## Check for Cholesky factor T if (nargin > 3) r = randn (rm, size (T, 1), is_class) * T + mu; elseif (is_diag) ## Check sigma for invalid values if (any (sigma <= 0)) error ("mvnpdf: SIGMA diagonal contains negative or zero values."); endif t = sqrt (sigma); if (nargout > 1) T = diag (t); endif r = bsxfun (@times, randn (rm, cm, is_class), t) + mu; else ## Compute a Cholesky factorization [T, err] = cholcov (sigma); if (err != 0) error ("mvnrnd: covariance matrix is not positive definite."); endif r = randn (rm, size (T, 1), is_class) * T + mu; endif endif ## For multiple covariance matrices if (ndims (sigma) == 3) ## If MU is a row vector, rep it out to match sigma if (rm == 1 && N_empty) rm = sd(3); mu = repmat (mu, rm, 1); endif ## Check sigma for correct size if (sd(1) != sd(2)) error ("mvnpdf: bad multiple covariance matrix."); elseif (sd(1) != cm) error ("mvnpdf: multiple covariance matrix mismatch."); elseif (sd(3) != rm) error ("mvnpdf: multiple covariance pages mismatch."); endif ## Check for Cholesky factor T if (nargin < 4) # T not present if (nargout > 1) T = zeros (sd, is_class); endif if (is_diag) sigma = reshape (sigma,sd(2),sd(3))'; ## Check sigma for invalid values if (any (sigma(:) <= 0)) error ("mvnpdf: SIGMA diagonals contain negative or zero values."); endif R = sqrt (sigma); r = bsxfun (@times, randn (rm, cm, is_class), R) + mu; if (nargout > 1) for i = 1:rm T(:,:,i) = diag (R(i,:)); endfor endif else r = zeros (rm, cm, is_class); for i = 1:rm [R, err] = cholcov (sigma(:,:,i)); if (err != 0) error (strcat ("mvnrnd: multiple covariance matrix", ... " is not positive definite.")); endif Rrows = size (R,1); r(i,:) = randn (1, Rrows, is_class) * R + mu(i,:); if (nargout > 1) T(1:Rrows,:,i) = R; endif endfor endif else # T present r = zeros (rm, cm, is_class); for i = 1:rm r(i,:) = randn (1, cm, is_class) * T(:,:,i) + mu(i,:); endfor endif endif endfunction ## Test input validation %!error mvnrnd () %!error mvnrnd ([2, 3, 4]) %!error mvnrnd (ones (2, 2, 2), ones (1, 2, 3, 4)) %!error mvnrnd (ones (1, 3), ones (1, 2, 3, 4)) ## Output validation tests %!assert_equal (size (mvnrnd ([2, 3, 4], [2, 2, 2])), [1, 3]) %!assert_equal (size (mvnrnd ([2, 3, 4], [2, 2, 2], 10)), [10, 3]) statistics-release-2.1.2/inst/Distribution_Functions/mvtcdf.m000066400000000000000000000365371527311104300244640ustar00rootroot00000000000000## Copyright (C) 2008 Arno Onken ## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} mvtcdf (@var{x}, @var{rho}, @var{df}) ## @deftypefnx {statistics} {@var{p} =} mvtcdf (@var{x_lo}, @var{x_up}, @var{rho}, @var{df}) ## @deftypefnx {statistics} {@var{p} =} mvtcdf (@dots{}, @var{options}) ## @deftypefnx {statistics} {[@var{p}, @var{err}] =} mvtcdf (@dots{}) ## ## Multivariate Student's t cumulative distribution function (CDF). ## ## @code{@var{p} = mvtcdf (@var{x}, @var{rho}, @var{df})} returns the cumulative ## probability of the multivariate student's t distribution with correlation ## parameters @var{rho} and degrees of freedom @var{df}, evaluated at each row ## of @var{x}. The rows of the @math{N*D} matrix @var{x} correspond to sample ## observations and its columns correspond to variables or coordinates. The ## return argument @var{p} is a column vector with the same number of rows as in ## @var{x}. ## ## @var{rho} is a symmetric, positive definite, @math{D*D} correlation matrix. ## @var{dF} is a scalar or a vector with @math{N} elements. ## ## Note: @code{mvtcdf} computes the CDF for the standard multivariate Student's ## t distribution, centered at the origin, with no scale parameters. If ## @var{rho} is a covariance matrix, i.e. @code{diag(@var{rho})} is not all ## ones, @code{mvtcdf} rescales @var{rho} to transform it to a correlation ## matrix. @code{mvtcdf} does not rescale @var{x}, though. ## ## The multivariate Student's t cumulative probability at @var{x} is defined as ## the probability that a random vector T, distributed as multivariate normal, ## will fall within the semi-infinite rectangle with upper limits defined by ## @var{x}. ## @itemize ## @item @math{Pr@{T(1)<=X(1), T(2)<=X(2), ... T(D)<=X(D)@}}. ## @end itemize ## ## @code{@var{p} = mvtcdf (@var{x_lo}, @var{x_hi}, @var{rho}, @var{df})} returns ## the multivariate Student's t cumulative probability evaluated over the ## rectangle (hyper-rectangle for multivariate data in @var{x}) with lower and ## upper limits defined by @var{x_lo} and @var{x_hi}, respectively. ## ## @code{[@var{p}, @var{err}] = mvtcdf (@dots{})} also returns an error estimate ## @var{err} in @var{p}. ## ## @code{@var{p} = mvtcdf (@dots{}, @var{options})} specifies the structure, ## which controls specific parameters for the numerical integration used to ## compute @var{p}, as @code{statset} makes it. A field that is missing or ## empty takes its default. The fields are: ## ## @multitable @columnfractions 0.2 0.75 ## @item @qcode{'TolFun'} @tab Maximum absolute error tolerance. Default ## is 1e-8 for D < 4, or 1e-4 for D >= 4. ## ## @item @qcode{'MaxFunEvals'} @tab Maximum number of integrand evaluations ## when @math{D >= 4}. Default is 1e7. Ignored when @math{D < 4}. ## ## @item @qcode{'Display'} @tab Display options. Choices are @qcode{'off'} ## (default), @qcode{'iter'}, which shows the probability and estimated error at ## each repetition, and @qcode{'final'}, which shows the final probability and ## related error after the integrand has converged successfully. Ignored when ## @math{D < 4}. ## @end multitable ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{bvtcdf, mvtpdf, mvtrnd} ## @end deftypefn function [p, err] = mvtcdf (varargin) ## Check for valid number on input and output arguments narginchk (3,5); ## Check for X, RHO, and DF being double or single. The trailing ## 'options' struct, if present, is parsed separately below. numargs = varargin(! cellfun (@isstruct, varargin)); if (! all (cellfun (@isfloat, numargs))) error ("mvtcdf: X, RHO, and DF must be double or single."); endif ## Defaults, which a field of the 'options' structure overrides where it ## is present and not empty, as a structure from statset leaves unset ## fields empty if (size (varargin{1}, 2) < 4) TolFun = 1e-8; elseif (size (varargin{1}, 2) < 26) TolFun = 1e-4; endif MaxFunEvals = 1e7; Display = 'off'; rem_nargin = nargin; if (isstruct (varargin{end})) opts = varargin{end}; if (isfield (opts, 'TolFun') && ! isempty (opts.TolFun)) TolFun = opts.TolFun; endif if (isfield (opts, 'MaxFunEvals') && ! isempty (opts.MaxFunEvals)) MaxFunEvals = opts.MaxFunEvals; endif if (isfield (opts, 'Display') && ! isempty (opts.Display)) Display = opts.Display; if (! any (strcmpi (Display, {'off', 'final', 'iter'}))) error ("mvtcdf: 'Display' field in 'options' has invalid value."); endif endif rem_nargin = nargin - 1; endif ## Check for X of X_lo and X_up if (rem_nargin < 4) # MVTCDF(X_UP,SIGMA,DF) x_up_Only = true; x_up = varargin{1}; ## Check for x being a matrix if (! ismatrix (x_up)) error ("mvtcdf: X must be a matrix."); endif ## Create x_lo according to data type of x_lo x_lo = - Inf (size (x_up)); if isa (x_up, 'single') x_lo = single (x_lo); endif ## Get SIGMA and DF arguments rho = varargin{2}; df = varargin{3}; else # MVNCDF(X_LO,X_UP,SIGMA,DF) x_up_Only = false; x_lo = varargin{1}; x_up = varargin{2}; rho = varargin{3}; df = varargin{4}; ## Check for x_lo and x_up being matrices of the same size ## and that they define increasing limits if (! ismatrix (x_lo) || ! ismatrix (x_up)) error ("mvtcdf: X_LO and X_UP must be matrices."); endif if (any (size (x_lo) != size (x_up))) error ("mvtcdf: X_LO and X_UP must be of the same size."); endif if (any (any (x_lo > x_up))) error ("mvtcdf: X_LO and X_UP must define increasing limits."); endif endif ## Check if data is single or double class is_type = 'double'; if (isa (x_up, 'single') || isa (x_lo, 'single') || ... isa (rho, 'single') || isa (df, 'single')) is_type = 'single'; endif ## Get size of data [n_x, d_x] = size (x_lo); if (d_x < 1) error ("mvtcdf: too few dimensions in data."); endif ## Force univariate column vector into a row vector if ((d_x == 1) && (size (rho, 1) == n_x)) x_lo = x_lo'; x_up = x_up'; [n_x, d_x] = size (x_up); endif ## Check rho sz = size (rho); if (sz(1) != sz(2)) error ("mvtcdf: correlation matrix RHO is not square."); elseif (! isequal (sz, [d_x, d_x])) error (strcat ("mvtcdf: correlation matrix RHO does not", ... " match dimensions in data.")); endif ## Standardize rho to correlation if necessary (not the data) s = sqrt (diag (rho)); if (any (s != 1)) rho = rho ./ (s * s'); endif ## Continue checking rho for being a valid correlation matrix [~, err] = cholcov (rho, 0); if (err != 0) error (strcat ("mvtcdf: correlation matrix RHO must be", ... " positive semi-definite.")); endif ## Check df if (! isscalar (df) && ! (isvector (df) && length (df) == n_x)) error (strcat ("mvtcdf: DF must be a scalar or a vector with", ... " the same samples as in data.")); endif if (any (df <= 0) || ! isreal (df)) error ("mvtcdf: DF must contain only positive real numbers."); endif ## Compute the cdf if (d_x == 1) p = tcdf (x_up, df) - tcdf (x_lo, df); if (nargout > 1) err = NaN (size (p), is_type); endif elseif (d_x < 4) if (x_up_Only) # upper limit only if (d_x == 2) p = bvtcdf (x_up, rho(2), df, TolFun); else p = tvtcdf (x_up, rho([2 3 6]), df, TolFun); endif else # lower and upper limits present ## Compute the probability over the rectangle as sums and differences ## of integrals over semi-infinite half-rectangles. For degenerate ## rectangles, force an exact zero by making each piece exactly zero. equalLimits = (x_lo == x_up); x_lo(equalLimits) = -Inf; x_up(equalLimits) = -Inf; p = zeros (n_x, 1, is_type); for i = 0:d_x k = nchoosek (1:d_x, i); for j = 1:size (k, 1) X = x_up; X(:,k(j,:)) = x_lo(:,k(j,:)); if d_x == 2 p = p + (-1)^i * bvtcdf (X, rho(2), df, TolFun/4); else p = p + (-1)^i * tvtcdf (X, rho([2 3 6]), df, TolFun/8); endif endfor endfor endif if (nargout > 1) err = repmat (cast (TolFun, is_type), size (p)); endif elseif (d_x < 26) p = zeros (n_x, 1, is_type); err = zeros (n_x, 1, is_type); if (isscalar (df)) df = repmat (df, n_x, 1); endif for i = 1:n_x [p(i), err(i)] = __mvtcdfqmc__ ("mvtcdf", x_lo(i,:), x_up(i,:), ... rho, df(i), TolFun, MaxFunEvals, ... Display); endfor else error ("mvtcdf: too many dimensions in data (limit = 25 columns)."); endif ## Bound p in range [0, 1] p(p < 0) = 0; p(p > 1) = 1; endfunction ## CDF for the trivariate Student's T function p = tvtcdf (x, rho, df, TolFun) n_x = size (x, 1); if (isscalar (df)) df = repmat (df, n_x, 1); endif ## Find a permutation that makes rho_23 == max(rho) [~,imax] = max (abs (rho)); if (imax == 1) # swap 1 and 3 rho_12 = rho(3); rho_13 = rho(2); rho_23 = rho(1); x = x(:,[3 2 1]); elseif (imax == 2) # swap 1 and 2 rho_12 = rho(1); rho_13 = rho(3); rho_23 = rho(2); x = x(:,[2 1 3]); else # x already in correct order rho_12 = rho(1); rho_13 = rho(2); rho_23 = rho(3); endif if (rho_23 >= 0) p1 = bvtcdf ([x(:,1) min(x(:,2:3), [], 2)], 0, df, TolFun / 4); p1(any (isnan (x), 2)) = NaN; else p1 = bvtcdf (x(:,1:2), 0, df, TolFun /4) - ... bvtcdf ([x(:,1) -x(:,3)], 0, df, TolFun / 4); p1(p1 < 0) = 0; endif if (abs (rho_23) < 1) lo = asin (rho_23); hi = (sign (rho_23) + (rho_23 == 0)) .* pi ./ 2; p2 = zeros (size (p1), class (p1)); for i = 1:n_x x1 = x(i,1); x2 = x(i,2); x3 = x(i,3); if (isfinite (x2) && isfinite (x3) && isnan (x1)) v = df(i); p2(i) = quadgk (@tvtIntegr1, lo, hi, 'AbsTol', TolFun / 4, 'RelTol', 0); endif endfor else p2 = zeros (class (p1)); endif if (abs (rho_12) > 0) lo = 0; hi = asin (rho_12); rj = rho_12; rk = rho_13; p3 = zeros (size (p1), class (p1)); for i = 1:n_x x1 = x(i,1); xj = x(i,2); xk = x(i,3); if (isfinite (x1) && isfinite (xj) && ! isnan (xk)) v = df(i); p3(i) = quadgk (@tvtIntegr2, lo, hi, 'AbsTol', TolFun / 4, 'RelTol', 0); endif endfor else p3 = zeros (class (p1)); endif if (abs (rho_13) > 0) lo = 0; hi = asin (rho_13); rj = rho_13; rk = rho_12; p4 = zeros (size (p1), class (p1)); for i = 1:n_x x1 = x(i,1); xj = x(i,3); xk = x(i,2); if (isfinite (x1) && isfinite (xj) && ! isnan (xk)) v = df(i); p4(i) = quadgk (@tvtIntegr2, lo, hi, 'AbsTol', TolFun / 4, 'RelTol', 0); endif endfor else p4 = zeros (class (p1)); endif if (isa (x, 'single') || isa (rho, 'single') || isa (df, 'single')) p = cast (p1 + (-p2 + p3 + p4) ./ (2 .* pi), 'single'); else p = cast (p1 + (-p2 + p3 + p4) ./ (2 .* pi), 'double'); endif ## Functions to compute the integrands function integrand = tvtIntegr1 (theta) st = sin (theta); c2t = cos (theta) .^ 2; w = sqrt (1 ./ (1 + ((x2 * st - x3) .^ 2 ./ c2t + x2 .^ 2) / v)); integrand = w .^ v .* TCDF (x1 .* w, v); endfunction function integrand = tvtIntegr2 (theta) st = sin (theta); c2t = cos (theta) .^ 2; w = sqrt (1 ./ (1 + ((x1 *st - xj) .^ 2 ./ c2t + x1 .^ 2) / v)); integrand = w .^ v .* TCDF (uk (st, c2t) .* w, v); endfunction function uk = uk (st, c2t) sinphi = st .* rk ./ rj; numeru = xk .* c2t - x1 .* (sinphi - rho_23 .* st) ... - xj .* (rho_23 - st .* sinphi); denomu = sqrt (c2t .* (c2t - sinphi .* sinphi ... - rho_23 .* (rho_23 - 2 .* st .* sinphi))); uk = numeru ./ denomu; endfunction endfunction ## CDF for Student's T function p = TCDF (x, df) p = betainc (df ./ (df + x .^ 2), df / 2, 0.5) / 2; reflect = (x > 0); p(reflect) = 1 - p(reflect); endfunction %!demo %! ## Compute the cdf of a multivariate Student's t distribution with %! ## correlation parameters rho = [1, 0.4; 0.4, 1] and 2 degrees of freedom. %! %! rho = [1, 0.4; 0.4, 1]; %! df = 2; %! [X1, X2] = meshgrid (linspace (-2, 2, 25)', linspace (-2, 2, 25)'); %! X = [X1(:), X2(:)]; %! p = mvtcdf (X, rho, df); %! surf (X1, X2, reshape (p, 25, 25)); %! title ('Bivariate Student''s t cumulative distribution function'); ## Test output against MATLAB R2018 %!test %! x = [1, 2]; %! rho = [1, 0.5; 0.5, 1]; %! df = 4; %! a = [-1, 0]; %! assert_equal (mvtcdf (a, x, rho, df), 0.294196905339283, 1e-14); %!test %! x = [1, 2;2, 4;1, 5]; %! rho = [1, 0.5; 0.5, 1]; %! df = 4; %! p =[0.790285178602166; 0.938703291727784; 0.81222737321336]; %! assert_equal (mvtcdf (x, rho, df), p, 1e-14); %!test %! x = [1, 2, 2, 4, 1, 5]; %! rho = eye (6); %! rho(rho == 0) = 0.5; %! df = 4; %! assert_equal (mvtcdf (x, rho, df), 0.6874, 1e-4); %!test # MATLAB parity: an options field missing or empty takes its default %! rho = [1, 0.5; 0.5, 1]; %! p = 1 / 3; %! assert_equal (mvtcdf ([0, 0], rho, 5, statset ('TolFun', 1e-4)), p, 1e-8); %! assert_equal (mvtcdf ([0, 0], rho, 5, struct ('TolFun', 1e-4)), p, 1e-8); %! assert_equal (mvtcdf ([0, 0], rho, 5, struct ('Display', 'final')), p, 1e-8); %!error mvtcdf (int32 ([0, 0]), eye (2), 5) %!error mvtcdf ([true, true], eye (2), 5) %!error mvtcdf ('ab', eye (2), 5) %!error mvtcdf (1) %!error mvtcdf (1, 2) %!error ... %! mvtcdf ([0, 0], eye (2), 5, struct ('Display', 'bogus')) %!error ... %! mvtcdf (1, [2, 3; 3, 2], 1) %!error ... %! mvtcdf ([2, 3, 4], ones (2), 1) %!error ... %! mvtcdf ([1, 2, 3], [2, 3], ones (2), 1) %!error ... %! mvtcdf ([2, 3], ones (2), [1, 2, 3]) %!error ... %! mvtcdf ([2, 3], [1, 0.5; 0.5, 1], [1, 2, 3]) statistics-release-2.1.2/inst/Distribution_Functions/mvtpdf.m000066400000000000000000000121171527311104300244650ustar00rootroot00000000000000## Copyright (C) 2015 Nir Krakauer ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} mvtpdf (@var{x}, @var{rho}, @var{df}) ## ## Multivariate Student's t probability density function (PDF). ## ## @subheading Arguments ## ## @itemize @bullet ## @item ## @var{x} are the points at which to find the probability, where each row ## corresponds to an observation. (@math{N*D} matrix) ## ## @item ## @var{rho} is the correlation matrix. (@math{D*D} symmetric positive ## definite matrix) ## ## @item ## @var{df} is the degrees of freedom. (scalar or vector of length @math{N}) ## ## @end itemize ## ## The distribution is assumed to be centered (zero mean). ## ## @subheading Return values ## ## @itemize @bullet ## @item ## @var{y} is the probability density for each row of @var{x}. ## (@math{N*1} vector) ## @end itemize ## ## @subheading Examples ## ## @example ## @group ## x = [1 2]; ## rho = [1.0 0.5; 0.5 1.0]; ## df = 4; ## y = mvtpdf (x, rho, df) ## @end group ## @end example ## ## @subheading References ## ## @enumerate ## @item ## Michael Roth, On the Multivariate t Distribution, Technical report from ## Automatic Control at Linkoepings universitet, ## @url{http://users.isy.liu.se/en/rt/roth/student.pdf} ## @end enumerate ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{mvtcdf, mvtrnd} ## @end deftypefn function y = mvtpdf (x, rho, df) if (nargin != 3) print_usage (); endif ## Check for X, RHO, and DF being double or single if (! (isfloat (x) && isfloat (rho) && isfloat (df))) error ("mvtpdf: X, RHO, and DF must be double or single."); endif # Dimensions d = size (rho, 1); n = size (x, 1); # Check parameters if (size (x, 2) != d) error ("mvtpdf: x must have the same number of columns as rho."); endif if (! isscalar (df) && (! isvector (df) || numel (df) != n)) error (strcat ("mvtpdf: DF must be a scalar or a vector with the", ... " same number of rows as X.")); endif if (d < 1 || size (rho, 2) != d || ! issymmetric (rho)) error ("mvtpdf: SIGMA must be nonempty and symmetric."); endif try U = chol (rho); catch error ("mvtpdf: rho must be positive definite"); end_try_catch df = df(:); sqrt_det_sigma = prod (diag (U)); #square root of determinant of rho ## Scale factor for PDF c = (gamma ((df+d)/2) ./ gamma (df/2)) ./ (sqrt_det_sigma * (df*pi).^(d/2)); #note: sumsq(U' \ x') is equivalent to the quadratic form x*inv(rho)*x' y = c ./ ((1 + sumsq (U' \ x') ./ df') .^ ((df' + d)/2))'; endfunction %!demo %! ## Compute the pdf of a multivariate t distribution with correlation %! ## parameters rho = [1 .4; .4 1] and 2 degrees of freedom. %! %! rho = [1, 0.4; 0.4, 1]; %! df = 2; %! [X1, X2] = meshgrid (linspace (-2, 2, 25)', linspace (-2, 2, 25)'); %! X = [X1(:), X2(:)]; %! y = mvtpdf (X, rho, df); %! surf (X1, X2, reshape (y, 25, 25)); %! title ('Bivariate Student''s t probability density function'); ## Test results verified with R mvtnorm package dmvt function ## dmvt(x = c(0,0), rho = diag(2), log = FALSE) %!assert_equal (mvtpdf ([0 0], eye (2), 1), 0.1591549, 1E-7) ## dmvt(x = c(1,0), rho = matrix(c(1, 0.5, 0.5, 1), nrow=2, ncol=2), df = 2, log = FALSE) %!assert_equal (mvtpdf ([1 0], [1 0.5; 0.5 1], 2), 0.06615947, 1E-7) ## dmvt(x = c(1,0.4,0), rho = matrix(c(1, 0.5, 0.3, 0.5, 1, 0.6, 0.3, 0.6, ... ## 1), nrow=3, ncol=3), df = 5, log = FALSE); dmvt(x = c(1.2,0.5,0.5), ... ## rho = matrix(c(1, 0.5, 0.3, 0.5, 1, 0.6, 0.3, 0.6, 1), nrow=3, ncol=3), ... ## df = 6, log = FALSE); dmvt(x = c(1.4,0.6,1), rho = matrix(c(1, 0.5, 0.3,... ## 0.5, 1, 0.6, 0.3, 0.6, 1), nrow=3, ncol=3), df = 7, log = FALSE) %!assert_equal (mvtpdf ([1 0.4 0; 1.2 0.5 0.5; 1.4 0.6 1], ... %! [1 0.5 0.3; 0.5 1 0.6; 0.3 0.6 1], [5 6 7]), ... %! [0.04713313 0.03722421 0.02069011]', 1E-7) ## Test input validation %!error mvtpdf (int32 ([0, 0]), eye (2), 5) %!error mvtpdf ([true, true], eye (2), 5) %!error mvtpdf ('ab', eye (2), 5) statistics-release-2.1.2/inst/Distribution_Functions/mvtrnd.m000066400000000000000000000104361527311104300245010ustar00rootroot00000000000000## Copyright (C) 2012 Arno Onken ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} mvtrnd (@var{rho}, @var{df}) ## @deftypefnx {statistics} {@var{r} =} mvtrnd (@var{rho}, @var{df}, @var{n}) ## ## Random vectors from the multivariate Student's t distribution. ## ## @subheading Arguments ## ## @itemize @bullet ## @item ## @var{rho} is the matrix of correlation coefficients. If there are any ## non-unit diagonal elements then @var{rho} will be normalized, so that the ## resulting covariance of the obtained samples @var{r} follows: ## @code{cov (r) = df/(df-2) * rho ./ (sqrt (diag (rho) * diag (rho)))}. ## In order to obtain samples distributed according to a standard multivariate ## student's t-distribution, @var{rho} must be equal to the identity matrix. To ## generate multivariate student's t-distribution samples @var{r} with arbitrary ## covariance matrix @var{rho}, the following scaling might be used: ## @code{r = mvtrnd (rho, df, n) * diag (sqrt (diag (rho)))}. ## ## @item ## @var{df} is the degrees of freedom for the multivariate t-distribution. ## @var{df} must be a vector with the same number of elements as samples to be ## generated or be scalar. ## ## @item ## @var{n} is the number of rows of the matrix to be generated. @var{n} must be ## a non-negative integer and corresponds to the number of samples to be ## generated. ## @end itemize ## ## @subheading Return values ## ## @itemize @bullet ## @item ## @var{r} is a matrix of random samples from the multivariate t-distribution ## with @var{n} row samples. ## @end itemize ## ## @subheading Examples ## ## @example ## @group ## rho = [1, 0.5; 0.5, 1]; ## df = 3; ## n = 10; ## r = mvtrnd (rho, df, n); ## @end group ## ## @group ## rho = [1, 0.5; 0.5, 1]; ## df = [2; 3]; ## n = 2; ## r = mvtrnd (rho, df, 2); ## @end group ## @end example ## ## @subheading References ## ## @enumerate ## @item ## Wendy L. Martinez and Angel R. Martinez. @cite{Computational Statistics ## Handbook with MATLAB}. Appendix E, pages 547-557, Chapman & Hall/CRC, 2001. ## ## @item ## Samuel Kotz and Saralees Nadarajah. @cite{Multivariate t Distributions and ## Their Applications}. Cambridge University Press, Cambridge, 2004. ## @end enumerate ## ## @seealso{mvtcdf, mvtpdf} ## @end deftypefn function r = mvtrnd (rho, df, n) # Check arguments if (nargin < 2) print_usage (); endif [jnk, p] = cholcov (rho); # This is a more robust check for positive definite if (! ismatrix (rho) || any (any (rho != rho')) || (p != 0)) error ("mvtrnd: SIGMA must be a positive definite matrix."); endif if (! isvector (df) || any (df <= 0)) error ("mvtrnd: DF must be a positive scalar or vector."); endif df = df(:); if (nargin > 2) if (! isscalar (n) || n < 0 | round (n) != n) error ("mvtrnd: N must be a non-negative integer.") endif if (isscalar (df)) df = df * ones (n, 1); else if (length (df) != n) error ("mvtrnd: N must match the length of DF.") endif endif else n = length (df); endif # Normalize rho if (any (diag (rho) != 1)) rho = rho ./ sqrt (diag (rho) * diag (rho)'); endif # Dimension d = size (rho, 1); # Draw samples y = mvnrnd (zeros (1, d), rho, n); u = repmat (chi2rnd (df), 1, d); r = y .* sqrt (repmat (df, 1, d) ./ u); endfunction %!test %! rho = [1, 0.5; 0.5, 1]; %! df = 3; %! n = 10; %! r = mvtrnd (rho, df, n); %! assert_equal (size (r), [10, 2]); %!test %! rho = [1, 0.5; 0.5, 1]; %! df = [2; 3]; %! n = 2; %! r = mvtrnd (rho, df, 2); %! assert_equal (size (r), [2, 2]); statistics-release-2.1.2/inst/Distribution_Functions/nakacdf.m000066400000000000000000000153661527311104300245650ustar00rootroot00000000000000## Copyright (C) 2016 Dag Lyberg ## Copyright (C) 1995-2015 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} nakacdf (@var{x}, @var{mu}, @var{omega}) ## @deftypefnx {statistics} {@var{p} =} nakacdf (@var{x}, @var{mu}, @var{omega}, @qcode{'upper'}) ## ## Nakagami cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the Nakagami distribution with shape parameter @var{mu} and spread ## parameter @var{omega}. The size of @var{p} is the common size of @var{x}, ## @var{mu}, and @var{omega}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## Both parameters must be positive reals and @qcode{@var{mu} >= 0.5}. For ## @qcode{@var{mu} < 0.5} or @qcode{@var{omega} <= 0}, @qcode{NaN} is returned. ## ## @code{@var{p} = nakacdf (@var{x}, @var{mu}, @var{omega}, "upper")} computes ## the upper tail probability of the Nakagami distribution with parameters ## @var{mu} and @var{beta}, at the values in @var{x}. ## ## Further information about the Nakagami distribution can be found at ## @url{https://en.wikipedia.org/wiki/Nakagami_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{nakainv, nakapdf, nakarnd, nakafit, nakalike, nakastat} ## @end deftypefn function p = nakacdf (x, mu, omega, uflag) ## Check for valid number of input arguments if (nargin < 3) error ("nakacdf: function called with too few input arguments."); endif ## Check for valid "upper" flag if (nargin > 3) if (! strcmpi (uflag, 'upper')) error ("nakacdf: invalid argument for upper tail."); else uflag = true; endif else uflag = false; endif ## Check for common size of X, MU, and OMEGA if (! isscalar (x) || ! isscalar (mu) || ! isscalar (omega)) [retval, x, mu, omega] = common_size (x, mu, omega); if (retval > 0) error ("nakacdf: X, MU, and OMEGA must be of common size or scalars."); endif endif ## Check for X, MU, and OMEGA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (omega))) error ("nakacdf: X, MU, and OMEGA must be double or single."); endif ## Check for X, MU, and OMEGA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (omega)) error ("nakacdf: X, MU, and OMEGA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (omega, 'single')) p = zeros (size (x), 'single'); else p = zeros (size (x)); endif ## Force invalid parameters and missing data to NaN k1 = isnan (x) | ! (mu >= 0.5) | ! (omega > 0); p(k1) = NaN; ## Find normal and edge cases k2 = (x == Inf) & (mu >= 0.5) & (mu < Inf) & (omega > 0) & (omega < Inf); k = (x > 0) & (x < Inf) & (mu >= 0.5) & (mu < Inf) ... & (omega > 0) & (omega < Inf); ## Compute Nakagami CDF if (uflag) p(k2) = 0; left = mu .* ones (size (x)); right = (mu ./ omega) .* x .^ 2; p(k) = gammainc (right(k), left(k), 'upper'); else p(k2) = 1; left = mu .* ones (size (x)); right = (mu ./ omega) .* x .^ 2; p(k) = gammainc (right(k), left(k)); endif endfunction %!demo %! ## Plot various CDFs from the Nakagami distribution %! x = 0:0.01:3; %! p1 = nakacdf (x, 0.5, 1); %! p2 = nakacdf (x, 1, 1); %! p3 = nakacdf (x, 1, 2); %! p4 = nakacdf (x, 1, 3); %! p5 = nakacdf (x, 2, 1); %! p6 = nakacdf (x, 2, 2); %! p7 = nakacdf (x, 5, 1); %! plot (x, p1, '-r', x, p2, '-g', x, p3, '-y', x, p4, '-m', ... %! x, p5, '-k', x, p6, '-b', x, p7, '-c') %! grid on %! xlim ([0, 3]) %! legend ({'μ = 0.5, ω = 1', 'μ = 1, ω = 1', 'μ = 1, ω = 2', ... %! 'μ = 1, ω = 3', 'μ = 2, ω = 1', 'μ = 2, ω = 2', ... %! 'μ = 5, ω = 1'}, 'location', 'southeast') %! title ('Nakagami CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, y %! x = [-1, 0, 1, 2, Inf]; %! y = [0, 0, 0.63212055882855778, 0.98168436111126578, 1]; %!assert_equal (nakacdf (x, ones (1,5), ones (1,5)), y, eps) %!assert_equal (nakacdf (x, 1, 1), y, eps) %!assert_equal (nakacdf (x, [1, 1, NaN, 1, 1], 1), [y(1:2), NaN, y(4:5)]) %!assert_equal (nakacdf (x, 1, [1, 1, NaN, 1, 1]), [y(1:2), NaN, y(4:5)]) %!assert_equal (nakacdf ([x, NaN], 1, 1), [y, NaN], eps) ## Test class of input preserved %!assert_equal (nakacdf (single ([x, NaN]), 1, 1), single ([y, NaN]), eps ('single')) %!assert_equal (nakacdf ([x, NaN], single (1), 1), single ([y, NaN]), eps ('single')) %!assert_equal (nakacdf ([x, NaN], 1, single (1)), single ([y, NaN]), eps ('single')) ## Test input validation %!error nakacdf () %!error nakacdf (1) %!error nakacdf (1, 2) %!error nakacdf (1, 2, 3, 'tail') %!error nakacdf (1, 2, 3, 4) %!error ... %! nakacdf (ones (3), ones (2), ones (2)) %!error ... %! nakacdf (ones (2), ones (3), ones (2)) %!error ... %! nakacdf (ones (2), ones (2), ones (3)) %!error nakacdf (int32 (2), 2, 2) %!error nakacdf (true, 2, 2) %!error nakacdf ('a', 2, 2) %!error nakacdf (i, 2, 2) %!error nakacdf (2, i, 2) %!error nakacdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/nakainv.m000066400000000000000000000141701527311104300246150ustar00rootroot00000000000000## Copyright (C) 2016 Dag Lyberg ## Copyright (C) 1995-2015 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} nakainv (@var{p}, @var{mu}, @var{omega}) ## ## Inverse of the Nakagami cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the Nakagami distribution with shape parameter @var{mu} and spread parameter ## @var{omega}. The size of @var{x} is the common size of @var{p}, @var{mu}, ## and @var{omega}. A scalar input functions as a constant matrix of the same ## size as the other inputs. ## ## Both parameters must be positive reals and @qcode{@var{mu} >= 0.5}. For ## @qcode{@var{mu} < 0.5} or @qcode{@var{omega} <= 0}, @qcode{NaN} is returned. ## ## Further information about the Nakagami distribution can be found at ## @url{https://en.wikipedia.org/wiki/Nakagami_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{nakacdf, nakapdf, nakarnd, nakafit, nakalike, nakastat} ## @end deftypefn function x = nakainv (p, mu, omega) ## Check for valid number of input arguments if (nargin < 3) error ("nakainv: function called with too few input arguments."); endif ## Check for common size of P, MU, and OMEGA if (! isscalar (p) || ! isscalar (mu) || ! isscalar (omega)) [retval, p, mu, omega] = common_size (p, mu, omega); if (retval > 0) error ("nakainv: P, MU, and OMEGA must be of common size or scalars."); endif endif ## Check for P, MU, and OMEGA being double or single if (! (isfloat (p) && isfloat (mu) && isfloat (omega))) error ("nakainv: P, MU, and OMEGA must be double or single."); endif ## Check for P, MU, and OMEGA being reals if (iscomplex (p) || iscomplex (mu) || iscomplex (omega)) error ("nakainv: P, MU, and OMEGA must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (mu, 'single') || isa (omega, 'single')) x = zeros (size (p), 'single'); else x = zeros (size (p)); endif ## Force invalid parameters and missing data to NaN k = isnan (p) | ! (p >= 0) | ! (p <= 1) | ! (mu >= 0.5) | ! (omega > 0); x(k) = NaN; ## Handle edge cases k = (p == 1) & (mu >= 0.5) & (mu < Inf) & (omega > 0) & (omega < Inf); x(k) = Inf; ## Find normal cases k = (0 < p) & (p < 1) & (0.5 <= mu) & (mu < Inf) ... & (0 < omega) & (omega < Inf); ## Compute Nakagami iCDF if (isscalar (mu) && isscalar (omega)) m_gamma = mu; w_gamma = omega / mu; x(k) = gaminv (p(k), m_gamma, w_gamma); x(k) = sqrt (x(k)); else m_gamma = mu; w_gamma = omega ./ mu; x(k) = gaminv (p(k), m_gamma(k), w_gamma(k)); x(k) = sqrt (x(k)); endif endfunction %!demo %! ## Plot various iCDFs from the Nakagami distribution %! p = 0.001:0.001:0.999; %! x1 = nakainv (p, 0.5, 1); %! x2 = nakainv (p, 1, 1); %! x3 = nakainv (p, 1, 2); %! x4 = nakainv (p, 1, 3); %! x5 = nakainv (p, 2, 1); %! x6 = nakainv (p, 2, 2); %! x7 = nakainv (p, 5, 1); %! plot (p, x1, '-r', p, x2, '-g', p, x3, '-y', p, x4, '-m', ... %! p, x5, '-k', p, x6, '-b', p, x7, '-c') %! grid on %! ylim ([0, 3]) %! legend ({'μ = 0.5, ω = 1', 'μ = 1, ω = 1', 'μ = 1, ω = 2', ... %! 'μ = 1, ω = 3', 'μ = 2, ω = 1', 'μ = 2, ω = 2', ... %! 'μ = 5, ω = 1'}, 'location', 'northwest') %! title ('Nakagami iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p, y %! p = [-Inf, -1, 0, 1/2, 1, 2, Inf]; %! y = [NaN, NaN, 0, 0.83255461115769769, Inf, NaN, NaN]; %!assert_equal (nakainv (p, ones (1,7), ones (1,7)), y, eps) %!assert_equal (nakainv (p, 1, 1), y, eps) %!assert_equal (nakainv (p, [1, 1, 1, NaN, 1, 1, 1], 1), [y(1:3), NaN, y(5:7)], eps) %!assert_equal (nakainv (p, 1, [1, 1, 1, NaN, 1, 1, 1]), [y(1:3), NaN, y(5:7)], eps) %!assert_equal (nakainv ([p, NaN], 1, 1), [y, NaN], eps) ## Test class of input preserved %!assert_equal (nakainv (single ([p, NaN]), 1, 1), single ([y, NaN])) %!assert_equal (nakainv ([p, NaN], single (1), 1), single ([y, NaN])) %!assert_equal (nakainv ([p, NaN], 1, single (1)), single ([y, NaN])) ## Test input validation %!error nakainv () %!error nakainv (1) %!error nakainv (1, 2) %!error ... %! nakainv (ones (3), ones (2), ones (2)) %!error ... %! nakainv (ones (2), ones (3), ones (2)) %!error ... %! nakainv (ones (2), ones (2), ones (3)) %!error nakainv (int32 (2), 4, 3) %!error nakainv (true, 4, 3) %!error nakainv ('a', 4, 3) %!error nakainv (i, 4, 3) %!error nakainv (1, i, 3) %!error nakainv (1, 4, i) statistics-release-2.1.2/inst/Distribution_Functions/nakapdf.m000066400000000000000000000140211527311104300245650ustar00rootroot00000000000000## Copyright (C) 2016 Dag Lyberg ## Copyright (C) 1995-2015 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} nakapdf (@var{x}, @var{mu}, @var{omega}) ## ## Nakagami probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the Nakagami distribution with shape parameter @var{mu} and spread ## parameter @var{omega}. The size of @var{y} is the common size of @var{x}, ## @var{mu}, and @var{omega}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## Both parameters must be positive reals and @qcode{@var{mu} >= 0.5}. For ## @qcode{@var{mu} < 0.5} or @qcode{@var{omega} <= 0}, @qcode{NaN} is returned. ## ## Further information about the Nakagami distribution can be found at ## @url{https://en.wikipedia.org/wiki/Nakagami_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{nakacdf, nakainv, nakarnd, nakafit, nakalike, nakastat} ## @end deftypefn function y = nakapdf (x, mu, omega) ## Check for valid number of input arguments if (nargin < 3) error ("nakapdf: function called with too few input arguments."); endif ## Check for common size of X, MU, and OMEGA if (! isscalar (x) || ! isscalar (mu) || ! isscalar (omega)) [retval, x, mu, omega] = common_size (x, mu, omega); if (retval > 0) error ("nakapdf: X, MU, and OMEGA must be of common size or scalars."); endif endif ## Check for X, MU, and OMEGA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (omega))) error ("nakapdf: X, MU, and OMEGA must be double or single."); endif ## Check for X, MU, and OMEGA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (omega)) error ("nakapdf: X, MU, and OMEGA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (omega, 'single')) y = zeros (size (x), 'single'); else y = zeros (size (x)); endif ## Compute Nakagami PDF k = isnan (x) | ! (mu >= 0.5) | ! (omega > 0); y(k) = NaN; k = (0 < x) & (x < Inf) & (0.5 <= mu) & (mu < Inf) ... & (0 < omega) & (omega < Inf); if (isscalar (mu) && isscalar (omega)) y(k) = exp (log (2) + mu * log (mu) - log (gamma (mu)) - ... mu * log (omega) + (2 * mu-1) * ... log (x(k)) - (mu / omega) * x(k) .^ 2); else y(k) = exp (log (2) + mu(k) .* log (mu(k)) - log (gamma (mu(k))) - ... mu(k) .* log (omega(k)) + (2 * mu(k) - 1) ... .* log (x(k)) - (mu(k) ./ omega(k)) .* x(k) .^ 2); endif endfunction %!demo %! ## Plot various PDFs from the Nakagami distribution %! x = 0:0.01:3; %! y1 = nakapdf (x, 0.5, 1); %! y2 = nakapdf (x, 1, 1); %! y3 = nakapdf (x, 1, 2); %! y4 = nakapdf (x, 1, 3); %! y5 = nakapdf (x, 2, 1); %! y6 = nakapdf (x, 2, 2); %! y7 = nakapdf (x, 5, 1); %! plot (x, y1, '-r', x, y2, '-g', x, y3, '-y', x, y4, '-m', ... %! x, y5, '-k', x, y6, '-b', x, y7, '-c') %! grid on %! xlim ([0, 3]) %! ylim ([0, 2]) %! legend ({'μ = 0.5, ω = 1', 'μ = 1, ω = 1', 'μ = 1, ω = 2', ... %! 'μ = 1, ω = 3', 'μ = 2, ω = 1', 'μ = 2, ω = 2', ... %! 'μ = 5, ω = 1'}, 'location', 'northeast') %! title ('Nakagami PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x, y %! x = [-1, 0, 1, 2, Inf]; %! y = [0, 0, 0.73575888234288467, 0.073262555554936715, 0]; %!assert_equal (nakapdf (x, ones (1,5), ones (1,5)), y, eps) %!assert_equal (nakapdf (x, 1, 1), y, eps) %!assert_equal (nakapdf (x, [1, 1, NaN, 1, 1], 1), [y(1:2), NaN, y(4:5)], eps) %!assert_equal (nakapdf (x, 1, [1, 1, NaN, 1, 1]), [y(1:2), NaN, y(4:5)], eps) %!assert_equal (nakapdf ([x, NaN], 1, 1), [y, NaN], eps) ## Test class of input preserved %!assert_equal (nakapdf (single ([x, NaN]), 1, 1), single ([y, NaN])) %!assert_equal (nakapdf ([x, NaN], single (1), 1), single ([y, NaN])) %!assert_equal (nakapdf ([x, NaN], 1, single (1)), single ([y, NaN])) ## Test input validation %!error nakapdf () %!error nakapdf (1) %!error nakapdf (1, 2) %!error ... %! nakapdf (ones (3), ones (2), ones (2)) %!error ... %! nakapdf (ones (2), ones (3), ones (2)) %!error ... %! nakapdf (ones (2), ones (2), ones (3)) %!error nakapdf (int32 (2), 4, 3) %!error nakapdf (true, 4, 3) %!error nakapdf ('a', 4, 3) %!error nakapdf (i, 4, 3) %!error nakapdf (1, i, 3) %!error nakapdf (1, 4, i) statistics-release-2.1.2/inst/Distribution_Functions/nakarnd.m000066400000000000000000000161021527311104300246010ustar00rootroot00000000000000## Copyright (C) 2016 Dag Lyberg ## Copyright (C) 1995-2015 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} nakarnd (@var{mu}, @var{omega}) ## @deftypefnx {statistics} {@var{r} =} nakarnd (@var{mu}, @var{omega}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} nakarnd (@var{mu}, @var{omega}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} nakarnd (@var{mu}, @var{omega}, [@var{sz}]) ## ## Random arrays from the Nakagami distribution. ## ## @code{@var{r} = nakarnd (@var{mu}, @var{omega})} returns an array of random ## numbers chosen from the Nakagami distribution with shape parameter @var{mu} ## and spread parameter @var{omega}. The size of @var{r} is the common size of ## @var{mu} and @var{omega}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## Both parameters must be positive reals and @qcode{@var{mu} >= 0.5}. For ## @qcode{@var{mu} < 0.5} or @qcode{@var{omega} <= 0}, @qcode{NaN} is returned. ## ## When called with a single size argument, @code{nakarnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the Nakagami distribution can be found at ## @url{https://en.wikipedia.org/wiki/Nakagami_distribution} ## ## @seealso{nakacdf, nakainv, nakapdf, nakafit, nakalike, nakastat} ## @end deftypefn function r = nakarnd (mu, omega, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("nakarnd: function called with too few input arguments."); endif ## Check for common size of MU and OMEGA if (! isscalar (mu) || ! isscalar (omega)) [retval, mu, omega] = common_size (mu, omega); if (retval > 0) error ("nakarnd: MU and OMEGA must be of common size or scalars."); endif endif ## Check for MU and OMEGA being reals if (iscomplex (mu) || iscomplex (omega)) error ("nakarnd: MU and OMEGA must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (mu); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("nakarnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("nakarnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (mu) && ! isequal (size (mu), size (ones (sz)))) error ("nakarnd: MU and OMEGA must be scalars or of size SZ."); endif ## Check for class type if (isa (mu, 'single') || isa (omega, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from Nakagami distribution if (isscalar (mu) && isscalar (omega)) if ((0.5 <= mu) && (mu < Inf) && (0 < omega) && (omega < Inf)) m_gamma = mu; w_gamma = omega / mu; r = gamrnd (m_gamma, w_gamma, sz); r = sqrt (r); else r = NaN (sz, cls); endif else r = NaN (sz, cls); k = (0.5 <= mu) & (mu < Inf) & (0 < omega) & (omega < Inf); m_gamma = mu; w_gamma = omega ./ mu; r(k) = gamrnd (m_gamma(k), w_gamma(k)); r(k) = sqrt (r(k)); endif endfunction ## Test output %!assert_equal (size (nakarnd (1, 1)), [1, 1]) %!assert_equal (size (nakarnd (1, ones (2, 1))), [2, 1]) %!assert_equal (size (nakarnd (1, ones (2, 2))), [2, 2]) %!assert_equal (size (nakarnd (ones (2, 1), 1)), [2, 1]) %!assert_equal (size (nakarnd (ones (2, 2), 1)), [2, 2]) %!assert_equal (size (nakarnd (1, 1, 3)), [3, 3]) %!assert_equal (size (nakarnd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (nakarnd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (nakarnd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (nakarnd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (nakarnd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (nakarnd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (nakarnd (1, 1, [])), [0, 0]) %!assert_equal (size (nakarnd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (nakarnd (1, 2, -1)), [0, 0]) %!assert_equal (size (nakarnd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (nakarnd (1, 2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (nakarnd (1, 1)), "double") %!assert_equal (class (nakarnd (1, single (1))), "single") %!assert_equal (class (nakarnd (1, single ([1, 1]))), "single") %!assert_equal (class (nakarnd (single (1), 1)), "single") %!assert_equal (class (nakarnd (single ([1, 1]), 1)), "single") ## Test input validation %!error nakarnd () %!error nakarnd (1) %!error ... %! nakarnd (ones (3), ones (2)) %!error ... %! nakarnd (ones (2), ones (3)) %!error nakarnd (i, 2, 3) %!error nakarnd (1, i, 3) %!error ... %! nakarnd (1, 2, 1.2) %!error ... %! nakarnd (1, 2, ones (2)) %!error ... %! nakarnd (1, 2, [2 0 2.5]) %!error ... %! nakarnd (1, 2, 2, 1.5, 5) %!error ... %! nakarnd (2, ones (2), 3) %!error ... %! nakarnd (2, ones (2), [3, 2]) %!error ... %! nakarnd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/nbincdf.m000066400000000000000000000231251527311104300245710ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} nbincdf (@var{x}, @var{r}, @var{ps}) ## @deftypefnx {statistics} {@var{p} =} nbincdf (@var{x}, @var{r}, @var{ps}, @qcode{'upper'}) ## ## Negative binomial cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the negative binomial distribution with parameters @var{r} and ## @var{ps}, where @var{r} is the number of successes until the experiment is ## stopped and @var{ps} is the probability of success in each experiment, given ## the number of failures in @var{x}. The size of @var{p} is the common size of ## @var{x}, @var{r}, and @var{ps}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## The algorithm uses the cumulative sums of the binomial masses. ## ## @code{@var{p} = nbincdf (@var{x}, @var{r}, @var{ps}, "upper")} computes the ## upper tail probability of the negative binomial distribution with parameters ## @var{r} and @var{ps}, at the values in @var{x}. ## ## When @var{r} is an integer, the negative binomial distribution is also known ## as the Pascal distribution and it models the number of failures in @var{x} ## before a specified number of successes is reached in a series of independent, ## identical trials. Its parameters are the probability of success in a single ## trial, @var{ps}, and the number of successes, @var{r}. A special case of the ## negative binomial distribution, when @qcode{@var{r} = 1}, is the geometric ## distribution, which models the number of failures before the first success. ## ## @var{r} can also have non-integer positive values, in which form the negative ## binomial distribution, also known as the Polya distribution, has no ## interpretation in terms of repeated trials, but, like the Poisson ## distribution, it is useful in modeling count data. The negative binomial ## distribution is more general than the Poisson distribution because it has a ## variance that is greater than its mean, making it suitable for count data ## that do not meet the assumptions of the Poisson distribution. In the limit, ## as @var{r} increases to infinity, the negative binomial distribution ## approaches the Poisson distribution. ## ## Further information about the negative binomial distribution can be found at ## @url{https://en.wikipedia.org/wiki/Negative_binomial_distribution} ## ## Input arguments must be @qcode{double}, @qcode{single}, or an integer type; ## logical and character arrays are rejected. Integer input is promoted to ## @qcode{double}, so the result is always a probability. MATLAB is ## inconsistent here: for several of the discrete distributions it returns the ## result in the integer class of the input, truncating a probability to ## @math{0} or @math{1}. ## ## @seealso{nbininv, nbinpdf, nbinrnd, nbinfit, nbinlike, nbinstat} ## @end deftypefn function p = nbincdf (x, r, ps, uflag) ## Check for valid number of input arguments if (nargin < 3) error ("nbincdf: function called with too few input arguments."); endif ## Check for "upper" flag if (nargin == 4 && strcmpi (uflag, 'upper')) uflag = true; elseif (nargin == 4 && ! strcmpi (uflag, 'upper')) error ("nbincdf: invalid argument for upper tail."); else uflag = false; endif ## Check for R and PS being scalars scalarNPS = (isscalar (r) & isscalar (ps)); ## Check for common size of X, R, and PS if (! isscalar (x) || ! isscalar (r) || ! isscalar (ps)) [retval, x, r, ps] = common_size (x, r, ps); if (retval > 0) error ("nbincdf: X, R, and PS must be of common size or scalars."); endif endif ## Check for X, R, and PS being double, single, or integer if (! (isnumeric (x) && isnumeric (r) && isnumeric (ps))) error ("nbincdf: X, R, and PS must be double, single, or integer."); endif ## Integer input is promoted to double, so the result is a probability ## rather than a value truncated to the input's integer type. if (isinteger (x)) x = double (x); endif if (isinteger (r)) r = double (r); endif if (isinteger (ps)) ps = double (ps); endif ## Check for X, R, and PS being reals if (iscomplex (x) || iscomplex (r) || iscomplex (ps)) error ("nbincdf: X, R, and PS must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (r, 'single') || isa (ps, 'single')) p = zeros (size (x), 'single'); else p = zeros (size (x)); endif ## Force NaN for out of range or missing parameters and missing data NaN is_nan = (isnan (x) | isnan (r) | (r <= 0) | (r == Inf) | (ps < 0) | (ps > 1)); p(is_nan) = NaN; ## Compute P for X >= 0 xf = floor (x); k = find (xf >= 0 & ! is_nan); ## Return 1 for positive infinite values of X, unless "upper" is given: p = 0 k1 = find (isinf (xf(k))); if (any (k1)) if (uflag) p(k(k1)) = 0; else p(k(k1)) = 1; endif k(k1) = []; endif ## Return 1 when X < 0 and "upper" is given k1 = (x < 0 & ! is_nan); if (any (k1)) if (uflag) p(k1) = 1; endif endif ## Accumulate probabilities up to the maximum value in X if (any (k)) if (uflag) p(k) = betainc (ps(k), r(k), xf(k) + 1, 'upper'); else max_val = max (xf(k)); if (scalarNPS) tmp = cumsum (nbinpdf (0:max_val, r(1), ps(1))); p(k) = tmp(xf(k) + 1); else idx = (0:max_val)'; compare = idx(:, ones (size (k))); index = xf(k); index = index(:); index = index(:, ones (size (idx)))'; n_big = r(k); n_big = n_big(:); n_big = n_big(:, ones (size (idx)))'; ps_big = ps(k); ps_big = ps_big(:); ps_big = ps_big(:, ones (size (idx)))'; p0 = nbinpdf (compare, n_big, ps_big); indicator = find (compare > index); p0(indicator) = zeros (size (indicator)); p(k) = sum (p0,1); endif endif endif ## Prevent round-off errors p(p > 1) = 1; endfunction %!demo %! ## Plot various CDFs from the negative binomial distribution %! x = 0:50; %! p1 = nbincdf (x, 2, 0.15); %! p2 = nbincdf (x, 5, 0.2); %! p3 = nbincdf (x, 4, 0.4); %! p4 = nbincdf (x, 10, 0.3); %! plot (x, p1, '*r', x, p2, '*g', x, p3, '*k', x, p4, '*m') %! grid on %! xlim ([0, 40]) %! legend ({'r = 2, ps = 0.15', 'r = 5, ps = 0.2', 'r = 4, p = 0.4', ... %! 'r = 10, ps = 0.3'}, 'location', 'southeast') %! title ('Negative binomial CDF') %! xlabel ('values in x (number of failures)') %! ylabel ('probability') ## Test output %!shared x, y %! x = [-1 0 1 2 Inf]; %! y = [0 1/2 3/4 7/8 1]; %!assert_equal (nbincdf (x, ones (1,5), 0.5*ones (1,5)), y) %!assert_equal (nbincdf (x, 1, 0.5*ones (1,5)), y) %!assert_equal (nbincdf (x, ones (1,5), 0.5), y) %!assert_equal (nbincdf (x, ones (1,5), 0.5, 'upper'), 1 - y, eps) %!assert_equal (nbincdf ([x(1:3) 0 x(5)], [0 1 NaN 1.5 Inf], 0.5), ... %! [NaN 1/2 NaN nbinpdf(0,1.5,0.5) NaN], eps) %!assert_equal (nbincdf (x, 1, 0.5*[-1 NaN 4 1 1]), [NaN NaN NaN y(4:5)]) %!assert_equal (nbincdf ([x(1:2) NaN x(4:5)], 1, 0.5), [y(1:2) NaN y(4:5)]) ## A density that overflowed made the summed CDF read 1 from 308 failures on ## and NaN past the mode, where the value at the mode is 0.5 exactly. %!assert_equal (nbincdf (1000, 1001, 0.5), 0.5, 1e-11) %!assert (all (isfinite (nbincdf (0:2500, 1001, 0.5)))) %!assert (all (diff (nbincdf (0:2500, 1001, 0.5)) >= 0)) ## Test class of input preserved %!assert_equal (nbincdf ([x, NaN], 1, 0.5), [y, NaN]) %!assert_equal (nbincdf (single ([x, NaN]), 1, 0.5), single ([y, NaN])) %!assert_equal (nbincdf ([x, NaN], single (1), 0.5), single ([y, NaN])) %!assert_equal (nbincdf ([x, NaN], 1, single (0.5)), single ([y, NaN])) ## Test input validation %!error nbincdf () %!error nbincdf (1) %!error nbincdf (1, 2) %!error nbincdf (1, 2, 3, 4) %!error nbincdf (1, 2, 3, 'some') %!error ... %! nbincdf (ones (3), ones (2), ones (2)) %!error ... %! nbincdf (ones (2), ones (3), ones (2)) %!error ... %! nbincdf (ones (2), ones (2), ones (3)) %!error nbincdf (true, 2, 2) %!error nbincdf ('a', 2, 2) %!assert_equal (class (nbincdf (int32 (2), 2, 2)), 'double') %!error nbincdf (i, 2, 2) %!error nbincdf (2, i, 2) %!error nbincdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/nbininv.m000066400000000000000000000257601527311104300246400ustar00rootroot00000000000000## Copyright (C) 1995-2012 Kurt Hornik ## Copyright (C) 2012-2016 Rik Wehbring ## Copyright (C) 2016-2017 Lachlan Andrew ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} nbininv (@var{p}, @var{r}, @var{ps}) ## ## Inverse of the negative binomial cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the negative binomial distribution with parameters @var{r} and @var{ps}, ## where @var{r} is the number of successes until the experiment is stopped and ## @var{ps} is the probability of success in each experiment, given the ## probability in @var{p}. The size of @var{x} is the common size of @var{p}, ## @var{r}, and @var{ps}. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## When @var{r} is an integer, the negative binomial distribution is also known ## as the Pascal distribution and it models the number of failures in @var{x} ## before a specified number of successes is reached in a series of independent, ## identical trials. Its parameters are the probability of success in a single ## trial, @var{ps}, and the number of successes, @var{r}. A special case of the ## negative binomial distribution, when @qcode{@var{r} = 1}, is the geometric ## distribution, which models the number of failures before the first success. ## ## @var{r} can also have non-integer positive values, in which form the negative ## binomial distribution, also known as the Polya distribution, has no ## interpretation in terms of repeated trials, but, like the Poisson ## distribution, it is useful in modeling count data. The negative binomial ## distribution is more general than the Poisson distribution because it has a ## variance that is greater than its mean, making it suitable for count data ## that do not meet the assumptions of the Poisson distribution. In the limit, ## as @var{r} increases to infinity, the negative binomial distribution ## approaches the Poisson distribution. ## ## Further information about the negative binomial distribution can be found at ## @url{https://en.wikipedia.org/wiki/Negative_binomial_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{nbincdf, nbinpdf, nbinrnd, nbinfit, nbinlike, nbinstat} ## @end deftypefn function x = nbininv (p, r, ps) ## Check for valid number of input arguments if (nargin < 3) error ("nbininv: function called with too few input arguments."); endif ## Check for common size of P, R, and PS if (! isscalar (p) || ! isscalar (r) || ! isscalar (ps)) [retval, p, r, ps] = common_size (p, r, ps); if (retval > 0) error ("nbininv: P, R, and PS must be of common size or scalars."); endif endif ## Check for P, R, and PS being double or single if (! (isfloat (p) && isfloat (r) && isfloat (ps))) error ("nbininv: P, R, and PS must be double or single."); endif ## Check for P, R, and PS being reals if (iscomplex (p) || iscomplex (r) || iscomplex (ps)) error ("nbininv: P, R, and PS must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (r, 'single') || isa (ps, 'single')) x = zeros (size (p), 'single'); else x = zeros (size (p)); endif k = (isnan (p) | (p < 0) | (p > 1) | isnan (r) | (r < 1) | (r == Inf) | isnan (ps) | (ps < 0) | (ps > 1)); x(k) = NaN; k = (p == 1) & (r > 0) & (r < Inf) & (ps >= 0) & (ps <= 1); x(k) = Inf; k = find ((p >= 0) & (p < 1) & (r > 0) & (r < Inf) & (ps > 0) & (ps <= 1)); if (! isempty (k)) pk = p(k)(:); if (isscalar (r) && isscalar (ps)) [m, unfinished] = scalar_nbininv (pk, r, ps); if (! isempty (unfinished)) m(unfinished) = bin_search_nbininv (pk(unfinished), r, ps); endif m = tie_correct (m, pk, r, ps); else rk = r(k)(:); psk = ps(k)(:); m = bin_search_nbininv (pk, rk, psk); m = tie_correct (m, pk, rk, psk); endif x(k) = m; endif endfunction ## Core algorithm to calculate the inverse negative binomial, for r and ps real ## scalars and y a column vector, and for which the output is not NaN or Inf. ## Compute CDF in batches of doubling size until CDF > p, or answer > 500. ## Return the locations of unfinished cases in k. function [m, k] = scalar_nbininv (p, r, ps) k = 1:length (p); m = zeros (size (p)); prev_limit = 0; limit = 10; do cdf = nbincdf (prev_limit:limit, r, ps); rr = bsxfun (@le, p(k), cdf); [v, m(k)] = max (rr, [], 2); # find first instance of p <= cdf m(k) += prev_limit - 1; k = k(v == 0); prev_limit = limit; limit += limit; until (isempty (k) || limit >= 1000) endfunction ## Vectorized binary search. ## Can handle vectors r and ps, and is faster than the scalar case when the ## answer is large. ## Could be optimized to call nbincdf only for a subset of the p at each stage, ## but care must be taken to handle both scalar and vector r,ps. Bookkeeping ## may cost more than the extra computations. function m = bin_search_nbininv (p, r, ps) k = 1:length (p); lower = zeros (size (p)); limit = 1; while (any (k) && limit < 1e100) cdf = nbincdf (limit, r, ps); k = (p > cdf); lower(k) = limit; limit += limit; endwhile upper = max (2*lower, 1); k = find (lower != limit/2); # elements for which above loop finished for i = 1:ceil (log2 (max (lower))) mid = (upper + lower)/2; cdf = nbincdf (floor (mid), r, ps); rr = (p <= cdf); upper(rr) = mid(rr); lower(! rr) = mid(! rr); endfor m = ceil (lower); m(p > nbincdf (m, r, ps)) += 1; # fix off-by-one errors from binary search endfunction ## Step the answer back onto M-1 wherever P lies within the error of the CDF ## there. The lower tail is a sum of M+1 densities, so its error grows with ## the answer, and an exactly attained probability reads short and sends the ## search past it: nbincdf (100, 101, 0.5) is 215 eps below the 0.5 that the ## symmetry of a fair coin makes exact, and the shortfall reaches 5616 eps by ## 2001 successes. The slack is capped at half the probability of M so that ## it can never cross a real step of the distribution and round P to the wrong ## side of one. function m = tie_correct (m, p, r, ps) j = find (m > 0); if (isempty (j)) return; endif mj = m(j); if (isscalar (r)) rj = r; else rj = r(j); endif if (isscalar (ps)) psj = ps; else psj = ps(j); endif lo = nbincdf (mj - 1, rj, psj); tol = min (32 .* (mj + 1) .* eps (lo), 0.5 .* nbinpdf (mj, rj, psj)); m(j(p(j) <= lo + tol)) -= 1; endfunction %!demo %! ## Plot various iCDFs from the negative binomial distribution %! p = 0.001:0.001:0.999; %! x1 = nbininv (p, 2, 0.15); %! x2 = nbininv (p, 5, 0.2); %! x3 = nbininv (p, 4, 0.4); %! x4 = nbininv (p, 10, 0.3); %! plot (p, x1, '-r', p, x2, '-g', p, x3, '-k', p, x4, '-m') %! grid on %! ylim ([0, 40]) %! legend ({'r = 2, ps = 0.15', 'r = 5, ps = 0.2', 'r = 4, p = 0.4', ... %! 'r = 10, ps = 0.3'}, 'location', 'northwest') %! title ('Negative binomial iCDF') %! xlabel ('probability') %! ylabel ('values in x (number of failures)') ## Test output %!shared p %! p = [-1 0 3/4 1 2]; %!assert_equal (nbininv (p, ones (1,5), 0.5*ones (1,5)), [NaN 0 1 Inf NaN]) %!assert_equal (nbininv (p, 1, 0.5*ones (1,5)), [NaN 0 1 Inf NaN]) %!assert_equal (nbininv (p, ones (1,5), 0.5), [NaN 0 1 Inf NaN]) %!assert_equal (nbininv (p, [1 0 NaN Inf 1], 0.5), [NaN NaN NaN NaN NaN]) %!assert_equal (nbininv (p, [1 0 1.5 Inf 1], 0.5), [NaN NaN 2 NaN NaN]) %!assert_equal (nbininv (p, 1, 0.5*[1 -Inf NaN Inf 1]), [NaN NaN NaN NaN NaN]) %!assert_equal (nbininv ([p(1:2) NaN p(4:5)], 1, 0.5), [NaN 0 NaN Inf NaN]) ## Test class of input preserved %!assert_equal (nbininv ([p, NaN], 1, 0.5), [NaN 0 1 Inf NaN NaN]) %!assert_equal (nbininv (single ([p, NaN]), 1, 0.5), single ([NaN 0 1 Inf NaN NaN])) %!assert_equal (nbininv ([p, NaN], single (1), 0.5), single ([NaN 0 1 Inf NaN NaN])) %!assert_equal (nbininv ([p, NaN], 1, single (0.5)), single ([NaN 0 1 Inf NaN NaN])) ## Test accuracy, to within +/- 1 since it is a discrete distribution %!shared y, tol %! y = magic (3) + 1; %! tol = 1; %!assert_equal (nbininv (nbincdf (1:10, 3, 0.1), 3, 0.1), 1:10, tol) %!assert_equal (nbininv (nbincdf (1:10, 3./(1:10), 0.1), 3./(1:10), 0.1), 1:10, tol) %!assert_equal (nbininv (nbincdf (y, 3./y, 1./y), 3./y, 1./y), y, tol) ## Test an exactly attained probability, where the CDF reads short. For r ## successes at ps = 0.5 the median is r-1 exactly, a fair coin over 2r-1 ## trials giving at least r successes half the time. %!assert_equal (nbininv (0.5, 26, 0.5), 25) %!assert_equal (nbininv (0.5, 101, 0.5), 100) %!assert_equal (nbininv (0.5, 251, 0.5), 250) %!assert_equal (nbininv (0.5, 2001, 0.5), 2000) %!assert_equal (nbininv ([0.5, 0.9], 101, 0.5), [100, 120]) %!assert_equal (nbininv ([0.5; 0.9], 101, 0.5), [100; 120]) %!assert_equal (nbininv ([0.5, 0.5], [101, 251], 0.5), [100, 250]) %!assert_equal (nbininv ([NaN, 0.5], 101, 0.5), [NaN, 100]) %!assert_equal (nbininv ([2, 0.5], [101, 101], [0.5, 0.5]), [NaN, 100]) ## Test input validation %!error nbininv () %!error nbininv (1) %!error nbininv (1, 2) %!error ... %! nbininv (ones (3), ones (2), ones (2)) %!error ... %! nbininv (ones (2), ones (3), ones (2)) %!error ... %! nbininv (ones (2), ones (2), ones (3)) %!error nbininv (int32 (2), 2, 2) %!error nbininv (true, 2, 2) %!error nbininv ('a', 2, 2) %!error nbininv (i, 2, 2) %!error nbininv (2, i, 2) %!error nbininv (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/nbinpdf.m000066400000000000000000000210701527311104300246030ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} nbinpdf (@var{x}, @var{r}, @var{ps}) ## ## Negative binomial probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## at @var{x} of the negative binomial distribution with parameters @var{r} and ## @var{ps}, where @var{r} is the number of successes until the experiment is ## stopped and @var{ps} is the probability of success in each experiment, given ## the number of failures in @var{x}. The size of @var{y} is the common size of ## @var{x}, @var{r}, and @var{ps}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## When @var{r} is an integer, the negative binomial distribution is also known ## as the Pascal distribution and it models the number of failures in @var{x} ## before a specified number of successes is reached in a series of independent, ## identical trials. Its parameters are the probability of success in a single ## trial, @var{ps}, and the number of successes, @var{r}. A special case of the ## negative binomial distribution, when @qcode{@var{r} = 1}, is the geometric ## distribution, which models the number of failures before the first success. ## ## @var{r} can also have non-integer positive values, in which form the negative ## binomial distribution, also known as the Polya distribution, has no ## interpretation in terms of repeated trials, but, like the Poisson ## distribution, it is useful in modeling count data. The negative binomial ## distribution is more general than the Poisson distribution because it has a ## variance that is greater than its mean, making it suitable for count data ## that do not meet the assumptions of the Poisson distribution. In the limit, ## as @var{r} increases to infinity, the negative binomial distribution ## approaches the Poisson distribution. ## ## Further information about the negative binomial distribution can be found at ## @url{https://en.wikipedia.org/wiki/Negative_binomial_distribution} ## ## Input arguments must be @qcode{double}, @qcode{single}, or an integer type; ## logical and character arrays are rejected. Integer input is promoted to ## @qcode{double}, so the result is always a probability. MATLAB is ## inconsistent here: for several of the discrete distributions it returns the ## result in the integer class of the input, truncating a probability to ## @math{0} or @math{1}. ## ## @seealso{nbincdf, nbininv, nbinrnd, nbinfit, nbinlike, nbinstat} ## @end deftypefn function y = nbinpdf (x, r, ps) ## Check for valid number of input arguments if (nargin < 3) error ("nbinpdf: function called with too few input arguments."); endif ## Check for common size of X, R, and PS if (! isscalar (x) || ! isscalar (r) || ! isscalar (ps)) [retval, x, r, ps] = common_size (x, r, ps); if (retval > 0) error ("nbinpdf: X, R, and PS must be of common size or scalars."); endif endif ## Check for X, R, and PS being double, single, or integer if (! (isnumeric (x) && isnumeric (r) && isnumeric (ps))) error ("nbinpdf: X, R, and PS must be double, single, or integer."); endif ## Integer input is promoted to double, so the result is a probability ## rather than a value truncated to the input's integer type. if (isinteger (x)) x = double (x); endif if (isinteger (r)) r = double (r); endif if (isinteger (ps)) ps = double (ps); endif ## Check for X, R, and PS being reals if (iscomplex (x) || iscomplex (r) || iscomplex (ps)) error ("nbinpdf: X, R, and PS must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (r, 'single') || isa (ps, 'single')) y = NaN (size (x), 'single'); else y = NaN (size (x)); endif ok = (x < Inf) & (x == fix (x)) & (r > 0) & (r < Inf) & (ps >= 0) & (ps <= 1); k = (x < 0) & ok; y(k) = 0; k = (x >= 0) & ok; if (isscalar (r) && isscalar (ps)) y(k) = bincoeff (-r, x(k)) .* (ps ^ r) .* ((ps - 1) .^ x(k)); else y(k) = bincoeff (-r(k), x(k)) .* (ps(k) .^ r(k)) .* ((ps(k) - 1) .^ x(k)); endif ## The three factors leave the range of a double long before their product ## does: at 1001 successes bincoeff is Inf from 308 failures on, where the ## density is an ordinary number peaking at 0.0089. Recompute those through ## the log gamma form, which holds the whole product in the exponent. b = k & ! isfinite (y); if (any (b(:))) xb = x(b); if (isscalar (r)) rb = r; else rb = r(b); endif if (isscalar (ps)) psb = ps; else psb = ps(b); endif t = xb .* log1p (-psb); t(xb == 0) = 0; y(b) = exp (gammaln (xb + rb) - gammaln (rb) - gammaln (xb + 1) ... + rb .* log (psb) + t); endif ## The density at an infinite abscissa is zero: no proper distribution ## places mass there. y(isinf (x) & (r > 0) & (r < Inf) & (ps >= 0) & (ps <= 1)) = 0; endfunction %!demo %! ## Plot various PDFs from the negative binomial distribution %! x = 0:40; %! y1 = nbinpdf (x, 2, 0.15); %! y2 = nbinpdf (x, 5, 0.2); %! y3 = nbinpdf (x, 4, 0.4); %! y4 = nbinpdf (x, 10, 0.3); %! plot (x, y1, '*r', x, y2, '*g', x, y3, '*k', x, y4, '*m') %! grid on %! xlim ([0, 40]) %! ylim ([0, 0.12]) %! legend ({'r = 2, ps = 0.15', 'r = 5, ps = 0.2', 'r = 4, p = 0.4', ... %! 'r = 10, ps = 0.3'}, 'location', 'northeast') %! title ('Negative binomial PDF') %! xlabel ('values in x (number of failures)') %! ylabel ('density') ## Test output %!shared x, y %! x = [-1 0 1 2 Inf]; %! y = [0 1/2 1/4 1/8 0]; %!assert_equal (nbinpdf (x, ones (1,5), 0.5*ones (1,5)), y) %!assert_equal (nbinpdf (x, 1, 0.5*ones (1,5)), y) %!assert_equal (nbinpdf (x, ones (1,5), 0.5), y) %!assert_equal (nbinpdf (x, [0 1 NaN 1.5 Inf], 0.5), [NaN 1/2 NaN 1.875*0.5^1.5/4 NaN], eps) %!assert_equal (nbinpdf (Inf, 5, 0.4), 0) %!assert_equal (nbinpdf (x, 1, 0.5*[-1 NaN 4 1 1]), [NaN NaN NaN y(4:5)]) %!assert_equal (nbinpdf ([x, NaN], 1, 0.5), [y, NaN]) ## The three factors leave the range of a double before their product does: ## bincoeff (-1001, 1000) is Inf where the density peaks at 0.0089. ## The rescue path is exp (gammaln (...) - gammaln (...) - gammaln (...) + ...), ## so the value rides on the platform's lgamma and moves about 2e-12 relative ## between them. Assert the density, not the agreement of lgamma. %!assert_equal (nbinpdf (1000, 1001, 0.5), 0.0089195055729428853, -1e-10) %!assert_equal (nbinpdf (2000, 1001, 0.5), 1.2637737073869266e-76, -1e-10) %!assert (all (isfinite (nbinpdf (0:2500, 1001, 0.5)))) ## Test class of input preserved %!assert_equal (nbinpdf (single ([x, NaN]), 1, 0.5), single ([y, NaN])) %!assert_equal (nbinpdf ([x, NaN], single (1), 0.5), single ([y, NaN])) %!assert_equal (nbinpdf ([x, NaN], 1, single (0.5)), single ([y, NaN])) ## Test input validation %!error nbinpdf () %!error nbinpdf (1) %!error nbinpdf (1, 2) %!error ... %! nbinpdf (ones (3), ones (2), ones (2)) %!error ... %! nbinpdf (ones (2), ones (3), ones (2)) %!error ... %! nbinpdf (ones (2), ones (2), ones (3)) %!error nbinpdf (true, 2, 2) %!error nbinpdf ('a', 2, 2) %!assert_equal (class (nbinpdf (int32 (2), 2, 2)), 'double') %!error nbinpdf (i, 2, 2) %!error nbinpdf (2, i, 2) %!error nbinpdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/nbinrnd.m000066400000000000000000000203741527311104300246230ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{rnd} =} nbinrnd (@var{r}, @var{ps}) ## @deftypefnx {statistics} {@var{rnd} =} nbinrnd (@var{r}, @var{ps}, @var{rows}) ## @deftypefnx {statistics} {@var{rnd} =} nbinrnd (@var{r}, @var{ps}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{rnd} =} nbinrnd (@var{r}, @var{ps}, [@var{sz}]) ## ## Random arrays from the negative binomial distribution. ## ## @code{@var{rnd} = nbinrnd (@var{r}, @var{ps})} returns an array of random ## numbers chosen from the negative binomial distribution with parameters ## @var{r} and @var{ps}, where @var{r} is the number of successes until the ## experiment is stopped and @var{ps} is the probability of success in each ## experiment, given the number of failures in @var{x}. The size of @var{rnd} ## is the common size of @var{r} and @var{ps}. A scalar input functions as a ## constant matrix of the same size as the other inputs. ## ## When called with a single size argument, return a square matrix with ## the dimension specified. When called with more than one scalar argument the ## first two arguments are taken as the number of rows and columns and any ## further arguments specify additional matrix dimensions. The size may also ## be specified with a vector of dimensions @var{sz}. ## ## When @var{r} is an integer, the negative binomial distribution is also known ## as the Pascal distribution and it models the number of failures in @var{x} ## before a specified number of successes is reached in a series of independent, ## identical trials. Its parameters are the probability of success in a single ## trial, @var{ps}, and the number of successes, @var{r}. A special case of the ## negative binomial distribution, when @qcode{@var{r} = 1}, is the geometric ## distribution, which models the number of failures before the first success. ## ## @var{r} can also have non-integer positive values, in which form the negative ## binomial distribution, also known as the Polya distribution, has no ## interpretation in terms of repeated trials, but, like the Poisson ## distribution, it is useful in modeling count data. The negative binomial ## distribution is more general than the Poisson distribution because it has a ## variance that is greater than its mean, making it suitable for count data ## that do not meet the assumptions of the Poisson distribution. In the limit, ## as @var{r} increases to infinity, the negative binomial distribution ## approaches the Poisson distribution. ## ## Further information about the negative binomial distribution can be found at ## @url{https://en.wikipedia.org/wiki/Negative_binomial_distribution} ## ## @seealso{nbincdf, nbininv, nbinpdf, nbinfit, nbinlike, nbinstat} ## @end deftypefn function rnd = nbinrnd (r, ps, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("nbinrnd: function called with too few input arguments."); endif ## Check for common size R and PS if (! isscalar (r) || ! isscalar (ps)) [retval, r, ps] = common_size (r, ps); if (retval > 0) error ("nbinrnd: R and PS must be of common size or scalars."); endif endif ## Check for R and PS being reals if (iscomplex (r) || iscomplex (ps)) error ("nbinrnd: R and PS must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (r); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) rnd = []; return; else error (strcat ("nbinrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("nbinrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (r) && ! isequal (size (r), size (ones (sz)))) error ("nbinrnd: R and PS must be scalars or of size SZ."); endif ## Check for class type if (isa (r, 'single') || isa (ps, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from negative binomial distribution if (isscalar (r) && isscalar (ps)) if ((r > 0) && (r < Inf) && (ps > 0) && (ps <= 1)) rnd = randp ((1 - ps) ./ ps .* randg (r, sz, cls), cls); elseif ((r > 0) && (r < Inf) && (ps == 0)) rnd = zeros (sz, cls); else rnd = NaN (sz, cls); endif else rnd = NaN (sz, cls); k = (r > 0) & (r < Inf) & (ps == 0); rnd(k) = 0; k = (r > 0) & (r < Inf) & (ps > 0) & (ps <= 1); rnd(k) = randp ((1 - ps(k)) ./ ps(k) .* randg (r(k), cls)); endif endfunction ## Test output %!assert_equal (size (nbinrnd (1, 0.5)), [1, 1]) %!assert_equal (size (nbinrnd (1, 0.5 * ones (2, 1))), [2, 1]) %!assert_equal (size (nbinrnd (1, 0.5 * ones (2, 2))), [2, 2]) %!assert_equal (size (nbinrnd (ones (2, 1), 0.5)), [2, 1]) %!assert_equal (size (nbinrnd (ones (2, 2), 0.5)), [2, 2]) %!assert_equal (size (nbinrnd (1, 0.5, 3)), [3, 3]) %!assert_equal (size (nbinrnd (1, 0.5, [4, 1])), [4, 1]) %!assert_equal (size (nbinrnd (1, 0.5, 4, 1)), [4, 1]) %!assert_equal (size (nbinrnd (1, 0.5, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (nbinrnd (1, 0.5, 0, 1)), [0, 1]) %!assert_equal (size (nbinrnd (1, 0.5, 1, 0)), [1, 0]) %!assert_equal (size (nbinrnd (1, 0.5, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (nbinrnd (1, 0.5, [])), [0, 0]) %!assert_equal (size (nbinrnd (1, 0.5, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (nbinrnd (1, 2, -1)), [0, 0]) %!assert_equal (size (nbinrnd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (nbinrnd (1, 2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (nbinrnd (1, 0.5)), "double") %!assert_equal (class (nbinrnd (1, single (0.5))), "single") %!assert_equal (class (nbinrnd (1, single ([0.5, 0.5]))), "single") %!assert_equal (class (nbinrnd (single (1), 0.5)), "single") %!assert_equal (class (nbinrnd (single ([1, 1]), 0.5)), "single") ## Test input validation %!error nbinrnd () %!error nbinrnd (1) %!error ... %! nbinrnd (ones (3), ones (2)) %!error ... %! nbinrnd (ones (2), ones (3)) %!error nbinrnd (i, 2, 3) %!error nbinrnd (1, i, 3) %!error ... %! nbinrnd (1, 2, 1.2) %!error ... %! nbinrnd (1, 2, ones (2)) %!error ... %! nbinrnd (1, 2, [2 0 2.5]) %!error ... %! nbinrnd (1, 2, 2, 1.5, 5) %!error ... %! nbinrnd (2, ones (2), 3) %!error ... %! nbinrnd (2, ones (2), [3, 2]) %!error ... %! nbinrnd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/ncfcdf.m000066400000000000000000000243561527311104300244200ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} ncfcdf (@var{x}, @var{df1}, @var{df2}, @var{lambda}) ## @deftypefnx {statistics} {@var{p} =} ncfcdf (@var{x}, @var{df1}, @var{df2}, @var{lambda}, @qcode{'upper'}) ## ## Noncentral @math{F}-cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the noncentral @math{F}-distribution with @var{df1} and @var{df2} ## degrees of freedom and noncentrality parameter @var{lambda}. The size of ## @var{p} is the common size of @var{x}, @var{df1}, @var{df2}, and ## @var{lambda}. A scalar input functions as a constant matrix of the same size ## as the other inputs. ## ## @code{@var{p} = ncfcdf (@var{x}, @var{df1}, @var{df2}, @var{lambda}, ## "upper")} ## computes the upper tail probability of the noncentral @math{F}-distribution ## with parameters @var{df1}, @var{df2}, and @var{lambda}, at the values in ## @var{x}. ## ## Further information about the noncentral @math{F}-distribution can be found ## at @url{https://en.wikipedia.org/wiki/Noncentral_F-distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{ncfinv, ncfpdf, ncfrnd, ncfstat, fcdf} ## @end deftypefn function p = ncfcdf (x, df1, df2, lambda, uflag) ## Check for valid number of input arguments if (nargin < 4) error ("ncfcdf: function called with too few input arguments."); endif ## Check for valid "upper" flag if (nargin > 4) if (! strcmpi (uflag, 'upper')) error ("ncfcdf: invalid argument for upper tail."); else uflag = true; endif else uflag = false; endif ## Check for common size of X, DF1, DF2, and LAMBDA [err, x, df1, df2, lambda] = common_size (x, df1, df2, lambda); if (err > 0) error ("ncfcdf: X, DF1, DF2, and LAMBDA must be of common size or scalars."); endif ## Check for X, DF1, DF2, and LAMBDA being double or single if (! (isfloat (x) && isfloat (df1) && isfloat (df2) && isfloat (lambda))) error ("ncfcdf: X, DF1, DF2, and LAMBDA must be double or single."); endif ## Check for X, DF1, DF2, and LAMBDA being reals if (iscomplex (x) || iscomplex (df1) || iscomplex (df2) || iscomplex (lambda)) error ("ncfcdf: X, DF1, DF2, and LAMBDA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (df1, 'single') || ... isa (df2, 'single') || isa (lambda, 'single')) p = zeros (size (x), 'single'); c_eps = eps ('single') .^ (3/4); else p = zeros (size (x)); c_eps = eps .^ (3/4); endif ## Find NaNs in input arguments (if any) and propagate them to p is_nan = isnan (x) | isnan (df1) | isnan (df1) | isnan (lambda); p(is_nan) = NaN; ## For "upper" option, force p = 1 for x <= 0, and p = 0 for x == Inf, ## otherwise, force p = 1 for x == Inf. if (uflag) p(x == Inf & ! is_nan) = 0; p(x <= 0 & ! is_nan) = 1; else p(x == Inf & ! is_nan) = 1; endif ## Find invalid values of parameters and propagate them to p as NaN k = (df1 <= 0 | df2 <= 0 | lambda < 0); p(k) = NaN; ## Compute central distribution (lambda == 0) k0 = (lambda==0); if (any (k0(:))) if (uflag) p(k0) = fcdf (x(k0), df1(k0), df2(k0), 'upper'); else p(k0) = fcdf (x(k0), df1(k0), df2(k0)); endif endif ## Check if there are remaining elements and reset variables k1 = ! (k0 | k | x == Inf | x <= 0 | is_nan); if (! any (k1(:))) return; else x = x(k1); df1 = df1(k1); df2 = df2(k1); lambda = lambda(k1); endif ## Prepare variables x = x(:); df1 = df1(:) / 2; df2 = df2(:) / 2; lambda = lambda(:) / 2; ## Value passed to Beta distribution function. tmp = df1 .* x ./ (df2 + df1 .* x); logtmp = log (tmp); nu2const = df2 .* log (1 - tmp) - localgammaln (df2); ## Sum the series. The general idea is that we are going to sum terms ## of the form 'poisspdf(j,lambda) .* betacdf(tmp,j+df1,df2)' j0 = floor (lambda(:)); ## Compute Poisson pdf and beta cdf at the starting point if (uflag) bcdf0 = betainc (tmp, j0 + df1, df2, 'upper'); else bcdf0 = betacdf (tmp, j0 + df1, df2); endif ppdf0 = exp (-lambda + j0 .* log (lambda) - localgammaln (j0 + 1)); ## Set up for loop over values less than j0 y = ppdf0 .* bcdf0; ppdf = ppdf0; bcdf = bcdf0; olddy = zeros (size (lambda)); delty = zeros (size (lambda)); j = j0 - 1; ok = j >= 0; while (any (ok)) ## Use recurrence relation to compute new pdf and cdf ppdf(ok) = ppdf(ok) .* (j(ok) + 1) ./ lambda(ok); if (uflag) bcdf(ok) = betainc (tmp(ok), j(ok) + df1(ok), df2(ok), 'upper'); else db = exp ((j + df1) .* logtmp + nu2const + ... localgammaln (j + df1 + df2) - localgammaln (j + df1 + 1)); bcdf(ok) = bcdf(ok) + db(ok); endif delty(ok) = ppdf(ok) .* bcdf(ok); y(ok) = y(ok) + delty(ok); ## Convergence test: change must be small and not increasing ok = ok & (delty > y*c_eps | abs (delty) > olddy); j = j - 1; ok = ok & j >= 0; olddy(ok) = abs (delty(ok)); endwhile ## Set up again for loop upward from j0 ppdf = ppdf0; bcdf = bcdf0; olddy = zeros (size (lambda)); j = j0 + 1; ok = true (size (j)); ## Set up for loop to avoid endless loop for jj = 1:5000 ppdf = ppdf .* lambda ./ j; if (uflag) bcdf = betainc (tmp, j + df1, df2, 'upper'); else bcdf = bcdf - exp ((j + df1 - 1) .* logtmp + nu2const + ... localgammaln (j + df1 + df2 - 1) - localgammaln (j + df1)); endif delty = ppdf.*bcdf; ## ok = indices not converged y(ok) = y(ok) + delty(ok); ## Convergence test: change must be small and not increasing ok = ok & (delty>y*c_eps | abs (delty)>olddy); ## Break if all indices converged if (! any (ok)) break; endif olddy(ok) = abs (delty(ok)); j = j + 1; endfor if (jj == 5000) warning ("ncfcdf: no convergence."); endif ## Save returning p-value p(k1) = y; endfunction function x = localgammaln (y) x = Inf (size (y), class (y)); x(! (y < 0)) = gammaln (y(! (y < 0))); endfunction %!demo %! ## Plot various CDFs from the noncentral F distribution %! x = 0:0.01:5; %! p1 = ncfcdf (x, 2, 5, 1); %! p2 = ncfcdf (x, 2, 5, 2); %! p3 = ncfcdf (x, 5, 10, 1); %! p4 = ncfcdf (x, 10, 20, 10); %! plot (x, p1, '-r', x, p2, '-g', x, p3, '-k', x, p4, '-m') %! grid on %! xlim ([0, 5]) %! legend ({'df1 = 2, df2 = 5, λ = 1', 'df1 = 2, df2 = 5, λ = 2', ... %! 'df1 = 5, df2 = 10, λ = 1', 'df1 = 10, df2 = 20, λ = 10'}, ... %! 'location', 'southeast') %! title ('Noncentral F CDF') %! xlabel ('values in x') %! ylabel ('probability') %!demo %! ## Compare the noncentral F CDF with LAMBDA = 10 to the F CDF with the %! ## same number of numerator and denominator degrees of freedom (5, 20) %! %! x = 0.01:0.1:10.01; %! p1 = ncfcdf (x, 5, 20, 10); %! p2 = fcdf (x, 5, 20); %! plot (x, p1, '-', x, p2, '-'); %! grid on %! xlim ([0, 10]) %! legend ({'Noncentral F(5,20,10)', 'F(5,20)'}, 'location', 'southeast') %! title ('Noncentral F vs F CDFs') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!test %! x = -2:0.1:2; %! p = ncfcdf (x, 10, 1, 3); %! assert_equal (p([1:21]), zeros (1, 21), 1e-76); %! assert_equal (p(22), 0.004530737275319753, 1e-14); %! assert_equal (p(30), 0.255842099135669, 1e-14); %! assert_equal (p(41), 0.4379890998457305, 1e-14); %!test %! p = ncfcdf (12, 10, 3, 2); %! assert_equal (p, 0.9582287900447416, 1e-14); %!test %! p = ncfcdf (2, 3, 2, 1); %! assert_equal (p, 0.5731985522994989, 1e-14); %!test %! p = ncfcdf (2, 3, 2, 1, 'upper'); %! assert_equal (p, 0.4268014477004823, 1e-14); %!test %! p = ncfcdf ([3, 6], 3, 2, 5, 'upper'); %! assert_equal (p, [0.530248523596927, 0.3350482341323044], 1e-14); ## Test input validation %!error ncfcdf () %!error ncfcdf (1) %!error ncfcdf (1, 2) %!error ncfcdf (1, 2, 3) %!error ncfcdf (1, 2, 3, 4, 'tail') %!error ncfcdf (1, 2, 3, 4, 5) %!error ... %! ncfcdf (ones (3), ones (2), ones (2), ones (2)) %!error ... %! ncfcdf (ones (2), ones (3), ones (2), ones (2)) %!error ... %! ncfcdf (ones (2), ones (2), ones (3), ones (2)) %!error ... %! ncfcdf (ones (2), ones (2), ones (2), ones (3)) %!error ncfcdf (int32 (2), 2, 2, 2) %!error ncfcdf (true, 2, 2, 2) %!error ncfcdf ('a', 2, 2, 2) %!error ncfcdf (i, 2, 2, 2) %!error ncfcdf (2, i, 2, 2) %!error ncfcdf (2, 2, i, 2) %!error ncfcdf (2, 2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/ncfinv.m000066400000000000000000000207231527311104300244520ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} ncfinv (@var{p}, @var{df1}, @var{df2}, @var{lambda}) ## ## Inverse of the noncentral @math{F}-cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the noncentral @math{F}-distribution with @var{df1} and @var{df2} degrees of ## freedom and noncentrality parameter @var{lambda}. The size of @var{x} is the ## common size of @var{p}, @var{df1}, @var{df2}, and @var{lambda}. A scalar ## input functions as a constant matrix of the same size as the other inputs. ## ## @code{ncfinv} uses Newton's method to converge to the solution. ## ## Further information about the noncentral @math{F}-distribution can be found ## at @url{https://en.wikipedia.org/wiki/Noncentral_F-distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{ncfcdf, ncfpdf, ncfrnd, ncfstat, finv} ## @end deftypefn function x = ncfinv (p, df1, df2, lambda) ## Check for valid number of input arguments if (nargin < 4) error ("ncfinv: function called with too few input arguments."); endif ## Check for common size of P, DF1, DF2, and LAMBDA [err, p, df1, df2, lambda] = common_size (p, df1, df2, lambda); if (err > 0) error ("ncfinv: P, DF1, DF2, and LAMBDA must be of common size or scalars."); endif ## Check for P, DF1, DF2, and LAMBDA being double or single if (! (isfloat (p) && isfloat (df1) && isfloat (df2) && isfloat (lambda))) error ("ncfinv: P, DF1, DF2, and LAMBDA must be double or single."); endif ## Check for P, DF1, DF2, and LAMBDA being reals if (iscomplex (p) || iscomplex (df1) || iscomplex (df2) || iscomplex (lambda)) error ("ncfinv: P, DF1, DF2, and LAMBDA must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (df1, 'single') || ... isa (df2, 'single') || isa (lambda, 'single')) x = NaN (size (p), 'single'); crit = sqrt (eps ('single')); else x = NaN (size (p), 'double'); crit = sqrt (eps ('double')); endif ## For lambda == 0, call finv d0 = lambda == 0; if (any (d0(:))) x(d0) = finv (p(d0), df1(d0), df2(d0)); endif ## For lambda > 0 and valid dfs valid = df1 > 0 & df2 > 0 & lambda > 0; ## Force x = 0 for p == 0 ax = Inf for p ==1 x(p == 0 & valid) = 0; x(p == 1 & valid) = Inf; ## Find remaining valid cases within the range of 0 < p < 1 k = find (p > 0 & p < 1 & valid); ## Return if nothing left if isempty (k) return; endif ## Reset input variables to remaining cases p = p(k); df1 = df1(k); df2 = df2(k); lambda = lambda(k); ## Initialize counter count_limit = 100; count = 0; ## Start at the mean (if it exists) mu0 = df2.*(df1+lambda) ./ (df1.*max (1,df2-2)); next = mu0; prev = 0; F = ncfcdf (mu0, df1, df2, lambda); while (count < count_limit) count += 1; next = (F - p) ./ ncfpdf (mu0, df1, df2, lambda); ## Prevent oscillations if (length (next) == length (prev)) t = sign (next) == -sign (prev); next(t) = sign (next(t)) .* min (abs (next(t)), abs (prev(t))) / 2; endif ## Prepare for next step mu1 = max (mu0 / 5, min (5 * mu0, mu0 - next)); ## Check that next step improves, otherwise abort F1 = ncfcdf (mu1, df1, df2, lambda); while (true) worse = (abs (F1-p) > abs (F - p) * (1 + crit)) & ... (abs (mu0 - mu1) > crit * mu0); if (! any (worse)) break; endif mu1(worse) = 0.5 * (mu1(worse) + mu0(worse)); F1(worse) = ncfcdf (mu1(worse), df1(worse), df2(worse), lambda(worse)); endwhile x(k) = mu1; ## Find elements that are not converged yet next = mu0 - mu1; mask = (abs (next) > crit * abs (mu0)); if (! any (mask)) break; endif ## Save parameters for these elements only F = F1(mask); mu0 = mu1(mask); prev = next(mask); if (! all (mask)) df1 = df1(mask); df2 = df2(mask); lambda = lambda(mask); p = p(mask); k = k(mask); endif endwhile if (count == count_limit) warning ("ncfinv: did not converge."); endif endfunction %!demo %! ## Plot various iCDFs from the noncentral F distribution %! p = 0.001:0.001:0.999; %! x1 = ncfinv (p, 2, 5, 1); %! x2 = ncfinv (p, 2, 5, 2); %! x3 = ncfinv (p, 5, 10, 1); %! x4 = ncfinv (p, 10, 20, 10); %! plot (p, x1, '-r', p, x2, '-g', p, x3, '-k', p, x4, '-m') %! grid on %! ylim ([0, 5]) %! legend ({'df1 = 2, df2 = 5, λ = 1', 'df1 = 2, df2 = 5, λ = 2', ... %! 'df1 = 5, df2 = 10, λ = 1', 'df1 = 10, df2 = 20, λ = 10'}, ... %! 'location', 'northwest') %! title ('Noncentral F iCDF') %! xlabel ('probability') %! ylabel ('values in x') %!demo %! ## Compare the noncentral F iCDF with LAMBDA = 10 to the F iCDF with the %! ## same number of numerator and denominator degrees of freedom (5, 20) %! %! p = 0.001:0.001:0.999; %! x1 = ncfinv (p, 5, 20, 10); %! x2 = finv (p, 5, 20); %! plot (p, x1, '-', p, x2, '-'); %! grid on %! ylim ([0, 10]) %! legend ({'Noncentral F(5,20,10)', 'F(5,20)'}, 'location', 'northwest') %! title ('Noncentral F vs F quantile functions') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!test %! x = [0,0.1775,0.3864,0.6395,0.9564,1.3712,1.9471,2.8215,4.3679,8.1865,Inf]; %! assert_equal (ncfinv ([0:0.1:1], 2, 3, 1), x, 1e-4); %!test %! x = [0,0.7492,1.3539,2.0025,2.7658,3.7278,5.0324,6.9826,10.3955,18.7665,Inf]; %! assert_equal (ncfinv ([0:0.1:1], 2, 3, 5), x, 1e-4); %!test %! x = [0,0.2890,0.8632,1.5653,2.4088,3.4594,4.8442,6.8286,10.0983,17.3736,Inf]; %! assert_equal (ncfinv ([0:0.1:1], 1, 4, 3), x, 1e-4); %!test %! x = [0.078410, 0.212716, 0.288618, 0.335752, 0.367963, 0.391460]; %! assert_equal (ncfinv (0.05, [1, 2, 3, 4, 5, 6], 10, 3), x, 1e-6); %!test %! x = [0.2574, 0.2966, 0.3188, 0.3331, 0.3432, 0.3507]; %! assert_equal (ncfinv (0.05, 5, [1, 2, 3, 4, 5, 6], 3), x, 1e-4); %!test %! x = [1.6090, 1.8113, 1.9215, 1.9911, NaN, 2.0742]; %! assert_equal (ncfinv (0.05, 1, [1, 2, 3, 4, -1, 6], 10), x, 1e-4); %!test %! assert_equal (ncfinv (0.996, 3, 5, 8), 58.0912074080671, 4e-12); ## Test input validation %!error ncfinv () %!error ncfinv (1) %!error ncfinv (1, 2) %!error ncfinv (1, 2, 3) %!error ... %! ncfinv (ones (3), ones (2), ones (2), ones (2)) %!error ... %! ncfinv (ones (2), ones (3), ones (2), ones (2)) %!error ... %! ncfinv (ones (2), ones (2), ones (3), ones (2)) %!error ... %! ncfinv (ones (2), ones (2), ones (2), ones (3)) %!error ncfinv (int32 (2), 2, 2, 2) %!error ncfinv (true, 2, 2, 2) %!error ncfinv ('a', 2, 2, 2) %!error ncfinv (i, 2, 2, 2) %!error ncfinv (2, i, 2, 2) %!error ncfinv (2, 2, i, 2) %!error ncfinv (2, 2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/ncfpdf.m000066400000000000000000000406431527311104300244320ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} ncfpdf (@var{x}, @var{df1}, @var{df2}, @var{lambda}) ## ## Noncentral @math{F}-probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the noncentral @math{F}-distribution with @var{df1} and @var{df2} degrees ## of freedom and noncentrality parameter @var{lambda}. The size of @var{y} is ## the common size of @var{x}, @var{df1}, @var{df2}, and @var{lambda}. A scalar ## input functions as a constant matrix of the same size as the other inputs. ## ## Further information about the noncentral @math{F}-distribution can be found ## at @url{https://en.wikipedia.org/wiki/Noncentral_F-distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{ncfcdf, ncfinv, ncfrnd, ncfstat, fpdf} ## @end deftypefn function y = ncfpdf (x, df1, df2, lambda) ## Check for valid number of input arguments if (nargin < 4) error ("ncfpdf: function called with too few input arguments."); endif ## Check for common size of X, DF1, DF2, and LAMBDA [err, x, df1, df2, lambda] = common_size (x, df1, df2, lambda); if (err > 0) error ("ncfpdf: X, DF1, DF2, and LAMBDA must be of common size or scalars."); endif ## Check for X, DF1, DF2, and LAMBDA being double or single if (! (isfloat (x) && isfloat (df1) && isfloat (df2) && isfloat (lambda))) error ("ncfpdf: X, DF1, DF2, and LAMBDA must be double or single."); endif ## Check for X, DF1, DF2, and LAMBDA being reals if (iscomplex (x) || iscomplex (df1) || iscomplex (df2) || iscomplex (lambda)) error ("ncfpdf: X, DF1, DF2, and LAMBDA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (df1, 'single') || ... isa (df2, 'single') || isa (lambda, 'single')) y = zeros (size (x), 'single'); else y = zeros (size (x)); endif ## Find NaNs in input arguments (if any) and propagate them to p is_nan = isnan (x) | isnan (df1) | isnan (df2) | isnan (lambda); y(is_nan) = NaN; ## Force invalid parameter cases to NaN k1 = df1 <= 0 | df2 <= 0 | lambda < 0; y(k1) = NaN; ## Handle edge cases where x == 0 k2 = x == 0 & df1 < 2 & ! k1; y(k2) = Inf; k3 = x == 0 & df1 == 2 & ! k1; if (any (k3(:))) y(k3) = exp (-lambda(k3) / 2); endif ## Handle central distribution where lambda == 0 k4 = lambda == 0 & ! k1 & x > 0; if any (k4(:)) y(k4) = fpdf (x(k4), df1(k4), df2(k4)); endif ## Handle normal cases td = find (x > 0 & ! (k1 | k4)); ## Return if finished all normal cases if (isempty (td)) return; endif ## Reset input variables to remaining cases and pre-divide df1, df2 and lambda x = x(td); df1 = df1(td) / 2; df2 = df2(td) / 2; lambda = lambda(td) / 2; ## Use z and scaled x for convenience z = df1 .* x ./ (df1 .* x + df2); z1 = df2 ./ (df1 .* x + df2); xs = lambda .* z; ## Find max K at which we start the recursion series K = zeros (size (x)); termK = zeros (size (x)); rsum = zeros (size (x)); ## Handy constant lnsr2pi = 0.9189385332046727; ## Process integer and non-integer df2 separately df2int = df2 == floor (df2); if (any (df2int(:))) # integers smallx = xs <= df1 ./ df2; largex = xs >= df2 .* (df1 + df2 - 1) & ! smallx; K(df2int & largex) = df2(df2int & largex); ## Compute K idx = df2int & ! (smallx | largex); if (any (idx(:))) d = 0.5 * (1 - xs(idx) - df1(idx)); K(idx) = floor (d + sqrt (d .^ 2 + xs(idx) .* (df2(idx) + 1))); endif ## For K == df2 K_df2 = df2int & K == df2; idz1 = K_df2 & z < 0.9; termK(idz1) = (df1(idz1) + df2(idz1) - 1) .* log (z(idz1)); idz2 = K_df2 & ! idz1; termK(idz2) = (df1(idz2) + df2(idz2) - 1) .* log1p (-z1(idz2)); ## For K == 0 Kzero = df2int & (df1 + K) <= 1; termK(Kzero) = StirlingError (df1(Kzero) + df2(Kzero)) - ... StirlingError (df1(Kzero)) - StirlingError (df2(Kzero)) - ... BinoPoisson (df1(Kzero), ... (df1(Kzero) + df2(Kzero)) .* z(Kzero)) - ... BinoPoisson (df2(Kzero), ... (df1(Kzero) + df2(Kzero)) .* z1(Kzero)); ## For all other K K_all = df2int & ! (K_df2 | Kzero); termK(K_all) = StirlingError (df1(K_all) + df2(K_all) - 1) - ... StirlingError (df1(K_all) + K(K_all) -1) - ... StirlingError (df2(K_all) - K(K_all)) - ... BinoPoisson (df1(K_all) + K(K_all) - 1, ... (df1(K_all) + df2(K_all) - 1) .* z(K_all)) - ... BinoPoisson (df2(K_all) - K(K_all), ... (df1(K_all) + df2(K_all) - 1) .* z1(K_all)); ## Poisson density for the leading term x1 = lambda .* z1; smallk = df2int & K <= x1 * realmin; y(td(smallk)) = termK(smallk) - x1(smallk); otherk = df2int & ! smallk; y(td(otherk)) = termK(otherk) - lnsr2pi - 0.5 * log (K(otherk)) - ... StirlingError (K(otherk)) - ... BinoPoisson (K(otherk), x1(otherk)); ## Sum recursively downwards term = ones (size (x)); k = K; ok = df2int & k > 0; while (any (ok(:))) k(ok) = k(ok) - 1; term(ok) = term(ok) .* (k(ok) + 1) .* ... (k(ok) + df1(ok)) ./ (df2(ok) - k(ok)) ./ xs(ok); ok = ok & term >= eps (rsum); rsum(ok) = rsum(ok) + term(ok); endwhile ## Sum recursively upwards term = ones (size (x)); k = K; ok = df2int & k < df2; while any (ok(:)) term(ok) = term(ok) .* xs(ok) .* ... (df2(ok) - k(ok)) ./ (k(ok) + df1(ok)) ./ (k(ok) + 1); ok = ok & term >= eps (rsum); rsum(ok) = rsum(ok) + term(ok); k(ok) = k(ok) + 1; endwhile endif if (any (! df2int(:))) # non-integers ## Compute K largex = ! df2int & xs > df1 ./ (df1 + df2); d = 0.5 * (1 + xs(largex) - df1(largex)); K(largex) = floor (d + sqrt (d .^ 2 + xs(largex) .* ... (df1(largex) + df2(largex) - 1))); ## For K == 0 Kzero = ! df2int & (df1 + K) <= 1; termK(Kzero) = StirlingError (df1(Kzero) + df2(Kzero)) - ... StirlingError (df1(Kzero)) - ... StirlingError (df2(Kzero)) - ... BinoPoisson (df1(Kzero), ... (df1(Kzero) + df2(Kzero)) .* z(Kzero)) - ... BinoPoisson (df2(Kzero), ... (df1(Kzero) + df2(Kzero)) .* z1(Kzero)); ## For K != 0 K_all = ! df2int & ! Kzero; termK(K_all) = StirlingError (df1(K_all) + df2(K_all) + K(K_all) - 1) - ... StirlingError (df1(K_all) + K(K_all) - 1) - ... StirlingError (df2(K_all)) - ... BinoPoisson (df1(K_all) + K(K_all) - 1, ... (df1(K_all) + df2(K_all) + K(K_all) - 1) .* ... z(K_all)) - ... BinoPoisson (df2(K_all), ... (df1(K_all) + df2(K_all) + K(K_all) - 1) .* ... z1(K_all)); ## Poisson density for the leading term smallk = ! df2int & K <= lambda * realmin; y(td(smallk)) = termK(smallk) - lambda(smallk); K_all = ! df2int & ! smallk; y(td(K_all)) = termK(K_all) - lnsr2pi - 0.5 * log (K(K_all)) - ... StirlingError (K(K_all)) - ... BinoPoisson (K(K_all), lambda(K_all)); ## Sum recursively downwards term = ones (size (x)); k = K; ok = ! df2int & k > 0; while (any (ok(:))) k(ok) = k(ok) - 1; term(ok) = term(ok) .* (k(ok) + 1) .* (k(ok) + df1(ok)) ./ ... (k(ok) + df1(ok) + df2(ok)) ./ xs(ok); ok = ok & term >= eps (rsum); rsum(ok) = rsum(ok) + term(ok); endwhile ## Sum recursively upwards term = ones (size (x)); k = K; ok = ! df2int; while (any (ok(:))) term(ok) = term(ok) .* xs(ok) .* (k(ok) + df1(ok) + df2(ok)) ./ ... (k(ok) + df1(ok)) ./ (k(ok) + 1); ok = ok & term >= eps (rsum); rsum(ok) = rsum(ok) + term(ok); k(ok) = k(ok)+1; endwhile endif ## Compute density pi2 = 2 * pi; Kzero = (df1 + K) <= 1; y(td(Kzero)) = exp (y(td(Kzero))) .* (1 + rsum(Kzero)) .* ... sqrt (df1(Kzero) .* df2(Kzero) ./ ... (df1(Kzero) + df2(Kzero)) / pi2) ./ ... x(Kzero); K_df2 = ! Kzero & df2int & K == df2; y(td(K_df2)) = exp (y(td(K_df2))) .* (1 + rsum(K_df2)) .* ... df1(K_df2) .* z1(K_df2); idx = ! Kzero & df2int & ! K_df2; y(td(idx)) = exp (y(td(idx))) .* (1 + rsum(idx)) .* df1(idx) .* z1(idx) .* ... sqrt ((df1(idx) + df2(idx) - 1) ./ (df2(idx) - K(idx)) ./ ... (df1(idx) + K(idx) - 1) / pi2); idx = ! df2int & ! Kzero; y(td(idx)) = exp (y(td(idx))) .* (1 + rsum(idx)) .* df1(idx) .* z1(idx) .* ... sqrt ((df1(idx) + df2(idx) + K(idx) - 1) ./ ... df2(idx) ./ (df1(idx) + K(idx) - 1) / pi2); endfunction ## Error of Stirling-De Moivre approximation to n factorial. function lambda = StirlingError (n) is_class = class (n); lambda = zeros (size (n), is_class); nn = n .* n; ## Define S0=1/12 S1=1/360 S2=1/1260 S3=1/1680 S4=1/1188 S0 = 8.333333333333333e-02; S1 = 2.777777777777778e-03; S2 = 7.936507936507937e-04; S3 = 5.952380952380952e-04; S4 = 8.417508417508418e-04; ## Define lambda(n) for n<0:0.5:15 sfe=[ 0; 1.534264097200273e-01;... 8.106146679532726e-02; 5.481412105191765e-02;... 4.134069595540929e-02; 3.316287351993629e-02;... 2.767792568499834e-02; 2.374616365629750e-02;... 2.079067210376509e-02; 1.848845053267319e-02;... 1.664469118982119e-02; 1.513497322191738e-02;... 1.387612882307075e-02; 1.281046524292023e-02;... 1.189670994589177e-02; 1.110455975820868e-02;... 1.041126526197210e-02; 9.799416126158803e-03;... 9.255462182712733e-03; 8.768700134139385e-03;... 8.330563433362871e-03; 7.934114564314021e-03;... 7.573675487951841e-03; 7.244554301320383e-03;... 6.942840107209530e-03; 6.665247032707682e-03;... 6.408994188004207e-03; 6.171712263039458e-03;... 5.951370112758848e-03; 5.746216513010116e-03;... 5.554733551962801e-03]; k = find (n <= 15); if (any (k)) n1 = n(k); n2 = 2 * n1; if (all (n2 == round (n2))) lambda(k) = sfe(n2+1); else lnsr2pi = 0.9189385332046728; lambda(k) = gammaln (n1+1)-(n1+0.5).*log (n1)+n1-lnsr2pi; endif endif k = find (n > 15 & n <= 35); if (any (k)) lambda(k) = (S0 - (S1 - (S2 - (S3 - S4 ./ nn(k)) ./ nn(k)) ./ ... nn(k)) ./ nn(k)) ./ n(k); endif k = find (n > 35 & n <= 80); if (any (k)) lambda(k) = (S0 - (S1 - (S2 - S3 ./ nn(k)) ./ nn(k)) ./ nn(k)) ./ n(k); endif k = find (n > 80 & n <= 500); if (any (k)) lambda(k) = (S0 - (S1 - S2 ./ nn(k)) ./ nn(k)) ./ n(k); endif k = find (n > 500); if (any (k)) lambda(k) = (S0 - S1 ./ nn(k)) ./ n(k); endif endfunction ## Deviance term for binomial and Poisson probability calculation. function BP = BinoPoisson (x, np) if (isa (x,'single') || isa (np,'single')) BP = zeros (size (x), 'single'); else BP = zeros (size (x)); endif k = abs (x - np) < 0.1 * (x + np); if any (k(:)) s = (x(k) - np(k)) .* (x(k) - np(k)) ./ (x(k) + np(k)); v = (x(k) - np(k)) ./ (x(k) + np(k)); ej = 2 .* x(k) .* v; is_class = class (s); s1 = zeros (size (s), is_class); ok = true (size (s)); j = 0; while any (ok(:)) ej(ok) = ej(ok) .* v(ok) .* v(ok); j = j + 1; s1(ok) = s(ok) + ej(ok) ./ (2 * j + 1); ok = ok & s1 != s; s(ok) = s1(ok); endwhile BP(k) = s; endif k = ! k; if (any (k(:))) BP(k) = x(k) .* log (x(k) ./ np(k)) + np(k) - x(k); endif endfunction %!demo %! ## Plot various PDFs from the noncentral F distribution %! x = 0:0.01:5; %! y1 = ncfpdf (x, 2, 5, 1); %! y2 = ncfpdf (x, 2, 5, 2); %! y3 = ncfpdf (x, 5, 10, 1); %! y4 = ncfpdf (x, 10, 20, 10); %! plot (x, y1, '-r', x, y2, '-g', x, y3, '-k', x, y4, '-m') %! grid on %! xlim ([0, 5]) %! ylim ([0, 0.8]) %! legend ({'df1 = 2, df2 = 5, λ = 1', 'df1 = 2, df2 = 5, λ = 2', ... %! 'df1 = 5, df2 = 10, λ = 1', 'df1 = 10, df2 = 20, λ = 10'}, ... %! 'location', 'northeast') %! title ('Noncentral F PDF') %! xlabel ('values in x') %! ylabel ('density') %!demo %! ## Compare the noncentral F PDF with LAMBDA = 10 to the F PDF with the %! ## same number of numerator and denominator degrees of freedom (5, 20) %! %! x = 0.01:0.1:10.01; %! y1 = ncfpdf (x, 5, 20, 10); %! y2 = fpdf (x, 5, 20); %! plot (x, y1, '-', x, y2, '-'); %! grid on %! xlim ([0, 10]) %! ylim ([0, 0.8]) %! legend ({'Noncentral F(5,20,10)', 'F(5,20)'}, 'location', 'northeast') %! title ('Noncentral F vs F PDFs') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x1, df1, df2, lambda %! x1 = [-Inf, 2, NaN, 4, Inf]; %! df1 = [2, 0, -1, 1, 4]; %! df2 = [2, 4, 5, 6, 8]; %! lambda = [1, NaN, 3, -1, 2]; %!assert_equal (ncfpdf (x1, df1, df2, lambda), [0, NaN, NaN, NaN, NaN]); %!assert_equal (ncfpdf (x1, df1, df2, 1), [0, NaN, NaN, ... %! 0.05607937264237208, NaN], 1e-14); %!assert_equal (ncfpdf (x1, df1, df2, 3), [0, NaN, NaN, ... %! 0.080125760971946518, NaN], 1e-14); %!assert_equal (ncfpdf (x1, df1, df2, 2), [0, NaN, NaN, ... %! 0.0715902008258656, NaN], 1e-14); %!assert_equal (ncfpdf (x1, 3, 5, lambda), [0, NaN, NaN, NaN, NaN]); %!assert_equal (ncfpdf (2, df1, df2, lambda), [0.1254046999837947, NaN, NaN, ... %! NaN, 0.2152571783045893], 1e-14); %!assert_equal (ncfpdf (4, df1, df2, lambda), [0.05067089541001374, NaN, NaN, ... %! NaN, 0.05560846335398539], 1e-14); ## Test input validation %!error ncfpdf () %!error ncfpdf (1) %!error ncfpdf (1, 2) %!error ncfpdf (1, 2, 3) %!error ... %! ncfpdf (ones (3), ones (2), ones (2), ones (2)) %!error ... %! ncfpdf (ones (2), ones (3), ones (2), ones (2)) %!error ... %! ncfpdf (ones (2), ones (2), ones (3), ones (2)) %!error ... %! ncfpdf (ones (2), ones (2), ones (2), ones (3)) %!error ncfpdf (int32 (2), 2, 2, 2) %!error ncfpdf (true, 2, 2, 2) %!error ncfpdf ('a', 2, 2, 2) %!error ncfpdf (i, 2, 2, 2) %!error ncfpdf (2, i, 2, 2) %!error ncfpdf (2, 2, i, 2) %!error ncfpdf (2, 2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/ncfrnd.m000066400000000000000000000170441527311104300244430ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} ncfrnd (@var{df1}, @var{df2}, @var{lambda}) ## @deftypefnx {statistics} {@var{r} =} ncfrnd (@var{df1}, @var{df2}, @var{lambda}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} ncfrnd (@var{df1}, @var{df2}, @var{lambda}, [@var{sz}]) ## ## Random arrays from the noncentral @math{F}-distribution. ## ## @code{@var{x} = ncfrnd (@var{p}, @var{df1}, @var{df2}, @var{lambda})} returns ## an array of random numbers chosen from the noncentral @math{F}-distribution ## with ## @var{df1} and @var{df2} degrees of freedom and noncentrality parameter ## @var{lambda}. The size of @var{r} is the common size of @var{df1}, ## @var{df2}, and @var{lambda}. A scalar input functions as a constant matrix ## of the same size as the other input. ## ## @code{ncfrnd} generates values using the definition of a noncentral @math{F} ## random variable, as the ratio of a noncentral chi-squared distribution and a ## (central) chi-squared distribution. ## ## When called with a single size argument, @code{ncfrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the noncentral @math{F}-distribution can be found ## at @url{https://en.wikipedia.org/wiki/Noncentral_F-distribution} ## ## @seealso{ncfcdf, ncfinv, ncfpdf, ncfstat, frnd, ncx2rnd, chi2rnd} ## @end deftypefn function r = ncfrnd (df1, df2, lambda, varargin) ## Check for valid number of input arguments if (nargin < 3) error ("ncfrnd: function called with too few input arguments."); endif ## Check for common size of DF1, DF2, and LAMBDA if (! isscalar (df1) || ! isscalar (df2) || ! isscalar (lambda)) [retval, df1, df2, lambda] = common_size (df1, df2, lambda); if (retval > 0) error ("ncfrnd: DF1, DF2, and LAMBDA must be of common size or scalars."); endif endif ## Check for DF1, DF2, and LAMBDA being reals if (iscomplex (df1) || iscomplex (df2) || iscomplex (lambda)) error ("ncfrnd: DF1, DF2, and LAMBDA must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 3) sz = size (df1); elseif (nargin == 4) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("ncfrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 4) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("ncfrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (df1) && ! isequal (size (df1), size (ones (sz)))) error ("ncfrnd: DF1, DF2, and LAMBDA must be scalars or of size SZ."); endif ## Check for class type if (isa (df1, 'single') || isa (df2, 'single') || isa (lambda, 'single')); cls = 'single'; else cls = 'double'; endif ## Return NaNs for out of range values of DF1, DF2, and LAMBDA df1(df1 <= 0) = NaN; df2(df2 <= 0) = NaN; lambda(lambda <= 0) = NaN; ## Generate random sample from noncentral F distribution r = (ncx2rnd (df1, lambda, sz) ./ df1) ./ ... (2 .* randg (df2 ./ 2, sz) ./ df2); ## Cast to appropriate class r = cast (r, cls); endfunction ## Test output %!assert_equal (size (ncfrnd (1, 1, 1)), [1, 1]) %!assert_equal (size (ncfrnd (1, ones (2, 1), 1)), [2, 1]) %!assert_equal (size (ncfrnd (1, ones (2, 2), 1)), [2, 2]) %!assert_equal (size (ncfrnd (ones (2, 1), 1, 1)), [2, 1]) %!assert_equal (size (ncfrnd (ones (2, 2), 1, 1)), [2, 2]) %!assert_equal (size (ncfrnd (1, 1, 1, 3)), [3, 3]) %!assert_equal (size (ncfrnd (1, 1, 1, [4, 1])), [4, 1]) %!assert_equal (size (ncfrnd (1, 1, 1, 4, 1)), [4, 1]) %!assert_equal (size (ncfrnd (1, 1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (ncfrnd (1, 1, 1, 0, 1)), [0, 1]) %!assert_equal (size (ncfrnd (1, 1, 1, 1, 0)), [1, 0]) %!assert_equal (size (ncfrnd (1, 1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (ncfrnd (1, 1, 1, [])), [0, 0]) %!assert_equal (size (ncfrnd (1, 1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (ncfrnd (1, 2, 3, -1)), [0, 0]) %!assert_equal (size (ncfrnd (1, 2, 3, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (ncfrnd (1, 2, 3, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (ncfrnd (1, 1, 1)), "double") %!assert_equal (class (ncfrnd (1, single (1), 1)), "single") %!assert_equal (class (ncfrnd (1, 1, single (1))), "single") %!assert_equal (class (ncfrnd (1, single ([1, 1]), 1)), "single") %!assert_equal (class (ncfrnd (1, 1, single ([1, 1]))), "single") %!assert_equal (class (ncfrnd (single (1), 1, 1)), "single") %!assert_equal (class (ncfrnd (single ([1, 1]), 1, 1)), "single") ## Test input validation %!error ncfrnd () %!error ncfrnd (1) %!error ncfrnd (1, 2) %!error ... %! ncfrnd (ones (3), ones (2), ones (2)) %!error ... %! ncfrnd (ones (2), ones (3), ones (2)) %!error ... %! ncfrnd (ones (2), ones (2), ones (3)) %!error ncfrnd (i, 2, 3) %!error ncfrnd (1, i, 3) %!error ncfrnd (1, 2, i) %!error ... %! ncfrnd (1, 2, 3, 1.2) %!error ... %! ncfrnd (1, 2, 3, ones (2)) %!error ... %! ncfrnd (1, 2, 3, [2 0 2.5]) %!error ... %! ncfrnd (1, 2, 3, 2, 1.5, 5) %!error ... %! ncfrnd (2, ones (2), 2, 3) %!error ... %! ncfrnd (2, ones (2), 2, [3, 2]) %!error ... %! ncfrnd (2, ones (2), 2, 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/nctcdf.m000066400000000000000000000313561527311104300244340ustar00rootroot00000000000000## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} nctcdf (@var{x}, @var{df}, @var{mu}) ## @deftypefnx {statistics} {@var{p} =} nctcdf (@var{x}, @var{df}, @var{mu}, @qcode{'upper'}) ## ## Noncentral @math{t}-cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the noncentral @math{t}-distribution with @var{df} degrees of ## freedom and noncentrality parameter @var{mu}. The size of @var{p} is the ## common size of @var{x}, @var{df}, and @var{mu}. A scalar input functions ## as a constant matrix of the same size as the other inputs. ## ## @code{@var{p} = nctcdf (@var{x}, @var{df}, @var{mu}, "upper")} computes ## the upper tail probability of the noncentral @math{t}-distribution with ## parameters @var{df} and @var{mu}, at the values in @var{x}. ## ## Further information about the noncentral @math{t}-distribution can be found ## at @url{https://en.wikipedia.org/wiki/Noncentral_t-distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{nctinv, nctpdf, nctrnd, nctstat, tcdf} ## @end deftypefn function p = nctcdf (x, df, mu, uflag) ## Check for valid number of input arguments if (nargin < 3) error ("nctcdf: function called with too few input arguments."); endif ## Check for valid "upper" flag if (nargin > 3) if (! strcmpi (uflag, 'upper')) error ("nctcdf: invalid argument for upper tail."); else uflag = true; endif else uflag = false; endif ## Check for common size of X, DF, and MU [err, x, df, mu] = common_size (x, df, mu); if (err > 0) error ("nctcdf: X, DF, and MU must be of common size or scalars."); endif ## Check for X, DF, and MU being double or single if (! (isfloat (x) && isfloat (df) && isfloat (mu))) error ("nctcdf: X, DF, and MU must be double or single."); endif ## Check for X, DF, and MU being reals if (iscomplex (x) || iscomplex (df) || iscomplex (mu)) error ("nctcdf: X, DF, and MU must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (df, 'single') || isa (mu, 'single')) p = zeros (size (x), 'single'); c_eps = eps ('single'); else p = zeros (size (x)); c_eps = eps; endif ## Find NaNs in input arguments (if any) and propagate them to p is_nan = isnan (x) | isnan (df) | isnan (mu); p(is_nan) = NaN; ## Find special cases for mu==0 and x<0; and x = Inf. case_Dinf = (df <= 0 | isinf (mu)) & ! is_nan; case_Dzero = mu == 0 & ! case_Dinf & ! is_nan; case_Xzero = x < 0 & ! case_Dzero & ! case_Dinf & ! is_nan; case_Xinf = x == Inf & ! case_Dzero & ! case_Dinf & ! is_nan; case_DFbig = df > 2e6 & ! case_Dzero & ! case_Dinf & ! case_Xinf & ! is_nan; flag_Dinf = any (case_Dinf(:)); flag_Dzero = any (case_Dzero(:)); flag_Xzero = any (case_Xzero(:)); flag_Xinf = any (case_Xinf(:)); flag_DFbig = any (case_DFbig(:)); ## Handle special cases if (flag_Dinf || flag_Dzero || flag_Xzero || flag_Xinf || flag_DFbig) if (flag_Dinf) p(case_Dinf) = NaN; endif if (flag_Dzero) if (uflag) p(case_Dzero) = tcdf (x(case_Dzero), df(case_Dzero), 'upper'); else p(case_Dzero) = tcdf (x(case_Dzero), df(case_Dzero)); endif endif if (flag_Xinf) if (uflag) p(case_Xinf) = 0; else p(case_Xinf) = 1; endif endif if (flag_DFbig) s = 1 - 1 ./ (4 * df); d = sqrt (1 + x .^ 2 ./ (2 * df)); if (uflag) p(case_DFbig) = normcdf (x(case_DFbig) .* s(case_DFbig), ... mu(case_DFbig), d(case_DFbig), 'upper'); else p(case_DFbig) = normcdf (x(case_DFbig) .* s(case_DFbig), ... mu(case_DFbig), d(case_DFbig)); endif endif fp = ! (case_Dinf | case_Dzero | case_Xzero | case_Xinf | case_DFbig); if (any (fp(:))) if (uflag) p(fp) = nctcdf (x(fp), df(fp), mu(fp), 'upper'); else p(fp) = nctcdf (x(fp), df(fp), mu(fp)); endif endif if (flag_Xzero) if (uflag) p(case_Xzero) = nctcdf (-x(case_Xzero), df(case_Xzero), ... -mu(case_Xzero)); else p(case_Xzero) = nctcdf (-x(case_Xzero), df(case_Xzero), ... -mu(case_Xzero), 'upper'); endif endif return endif ## Compute value for betainc function. x_square = x .^ 2; denom = df + x_square; P = x_square ./ denom; Q = df ./ denom; ## Initialize infinite sum. d_square = mu .^ 2; ## Compute probability P[TD<0] (first term) if (uflag) x_zero = x == 0 & ! is_nan; if (any (x_zero(:))) fx = normcdf (- mu, 0, 1, 'upper'); p(x_zero)= fx(x_zero); endif else p(! is_nan) = normcdf (- mu(! is_nan), 0, 1); endif ## Compute probability P[0 (abs (subtotal(TD)) + c_eps) * c_eps); if (! any (TD)) break; endif ## Update for next iteration jj = jj+2; E1(TD) = E1(TD) .* d_square(TD) ./ (jj(TD)); E2(TD) = E2(TD) .* d_square(TD) ./ (jj(TD) + 1); if (uflag) B1(TD) = betainc (P(TD), (jj(TD) + 1) / 2, df(TD) / 2, 'upper'); B2(TD) = betainc (P(TD), (jj(TD) + 2) / 2, df(TD) / 2, 'upper'); else B1(TD) = B1(TD) - R1(TD); B2(TD) = B2(TD) - R2(TD); R1(TD) = R1(TD) .* P(TD) .* (jj(TD)+df(TD)-1) ./ (jj(TD)+1); R2(TD) = R2(TD) .* P(TD) .* (jj(TD)+df(TD) ) ./ (jj(TD)+2); endif endwhile ## Go back to the peak and start looping downward as far as necessary. E1 = E10; E2 = E20; B1 = B10; B2 = B20; R1 = R10; R2 = R20; jj = j0; TD = (jj > 0); while (any (TD)) JJ = jj(TD); E1(TD) = E1(TD) .* (JJ ) ./ d_square(TD); E2(TD) = E2(TD) .* (JJ+1) ./ d_square(TD); R1(TD) = R1(TD) .* (JJ+1) ./ ((JJ+df(TD)-1) .* P(TD)); R2(TD) = R2(TD) .* (JJ+2) ./ ((JJ+df(TD)) .* P(TD)); if (uflag) B1(TD) = betainc (P(TD), (JJ - 1) / 2, df(TD) / 2, 'upper'); B2(TD) = betainc (P(TD), JJ / 2, df(TD) / 2, 'upper'); else B1(TD) = B1(TD) + R1(TD); B2(TD) = B2(TD) + R2(TD); endif twoterms = E1(TD) .* B1(TD) + E2(TD) .* B2(TD); subtotal(TD) = subtotal(TD) + twoterms; jj = jj - 2; TD(TD) = (abs (twoterms) > (abs (subtotal(TD)) + c_eps) * c_eps) & ... (jj(TD) > 0); endwhile p(x_notzero) = min (1, max (0, p(x_notzero) + subtotal / 2)); endif endfunction %!demo %! ## Plot various CDFs from the noncentral T distribution %! x = -5:0.01:5; %! p1 = nctcdf (x, 1, 0); %! p2 = nctcdf (x, 4, 0); %! p3 = nctcdf (x, 1, 2); %! p4 = nctcdf (x, 4, 2); %! plot (x, p1, '-r', x, p2, '-g', x, p3, '-k', x, p4, '-m') %! grid on %! xlim ([-5, 5]) %! legend ({'df = 1, μ = 0', 'df = 4, μ = 0', ... %! 'df = 1, μ = 2', 'df = 4, μ = 2'}, 'location', 'southeast') %! title ('Noncentral T CDF') %! xlabel ('values in x') %! ylabel ('probability') %!demo %! ## Compare the noncentral T CDF with MU = 1 to the T CDF %! ## with the same number of degrees of freedom (10). %! %! x = -5:0.1:5; %! p1 = nctcdf (x, 10, 1); %! p2 = tcdf (x, 10); %! plot (x, p1, '-', x, p2, '-') %! grid on %! xlim ([-5, 5]) %! legend ({'Noncentral T(10,1)', 'T(10)'}, 'location', 'southeast') %! title ('Noncentral T vs T CDFs') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!test %! x = -2:0.1:2; %! p = nctcdf (x, 10, 1); %! assert_equal (p(1), 0.003302485766631558, 1e-14); %! assert_equal (p(2), 0.004084668193532631, 1e-14); %! assert_equal (p(3), 0.005052800319478737, 1e-14); %! assert_equal (p(41), 0.8076115625303751, 1e-14); %!test %! p = nctcdf (12, 10, 3); %! assert_equal (p, 0.9997719343243797, 1e-14); %!test %! p = nctcdf (2, 3, 2); %! assert_equal (p, 0.4430757822176028, 1e-14); %!test %! p = nctcdf (2, 3, 2, 'upper'); %! assert_equal (p, 0.5569242177823971, 1e-14); %!test %! p = nctcdf ([3, 6], 3, 2, 'upper'); %! assert_equal (p, [0.3199728259444777, 0.07064855592441913], 1e-14); ## Test input validation %!error nctcdf () %!error nctcdf (1) %!error nctcdf (1, 2) %!error nctcdf (1, 2, 3, 'tail') %!error nctcdf (1, 2, 3, 4) %!error ... %! nctcdf (ones (3), ones (2), ones (2)) %!error ... %! nctcdf (ones (2), ones (3), ones (2)) %!error ... %! nctcdf (ones (2), ones (2), ones (3)) %!error nctcdf (int32 (2), 2, 2) %!error nctcdf (true, 2, 2) %!error nctcdf ('a', 2, 2) %!error nctcdf (i, 2, 2) %!error nctcdf (2, i, 2) %!error nctcdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/nctinv.m000066400000000000000000000166151527311104300244750ustar00rootroot00000000000000## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} nctinv (@var{p}, @var{df}, @var{mu}) ## ## Inverse of the non-central @math{t}-cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the noncentral @math{t}-distribution with @var{df} degrees of freedom and ## noncentrality parameter @var{mu}. The size of @var{x} is the common size ## of @var{p}, @var{df}, and @var{mu}. A scalar input functions as a ## constant matrix of the same size as the other inputs. ## ## @code{nctinv} uses Newton's method to converge to the solution. ## ## Further information about the noncentral @math{t}-distribution can be found ## at @url{https://en.wikipedia.org/wiki/Noncentral_t-distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{nctcdf, nctpdf, nctrnd, nctstat, tinv} ## @end deftypefn function x = nctinv (p, df, mu) ## Check for valid number of input arguments if (nargin < 3) error ("nctinv: function called with too few input arguments."); endif ## Check for common size of P, DF, and MU [err, p, df, mu] = common_size (p, df, mu); if (err > 0) error ("nctinv: P, DF, and MU must be of common size or scalars."); endif ## Check for P, DF, and MU being double or single if (! (isfloat (p) && isfloat (df) && isfloat (mu))) error ("nctinv: P, DF, and MU must be double or single."); endif ## Check for P, DF, and MU being reals if (iscomplex (p) || iscomplex (df) || iscomplex (mu)) error ("nctinv: P, DF, and MU must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (df, 'single') || isa (mu, 'single')) x = NaN (size (p), 'single'); crit = sqrt (eps ('single')); else x = NaN (size (p), 'double'); crit = sqrt (eps ('double')); endif ## For mu == 0, call chi2inv m0 = mu == 0; if (any (m0(:))) x(m0) = tinv (p(m0), df(m0)); ## If mu == 0 for all entries, then return if (all (m0(:))) return; endif endif ## For all valid entries valid = df > 0 & ! isnan (mu) & ! isinf (mu); ## Force x = -Inf for p == 0 and x = Inf for p == 1 x(p == 0 & valid) = -Inf; x(p == 1 & valid) = Inf; ## Find valid samples within the range of 0 < p < 1 k = find (p > 0 & p < 1 & valid); p_k = p(k); df_k = df(k); mu_k = mu(k); ## Initialize counter count_limit = 100; count = 0; ## Supply a starting guess for the iteration with norminv x_k = norminv (p_k, mu_k, 1); h_k = ones (size (x_k), class (x_k)); ## Start iteration with a break out loop F = nctcdf (x_k, df_k, mu_k); while (any (abs (h_k) > crit * abs (x_k)) && ... max (abs (h_k)) > crit && count < count_limit) count = count + 1; h_k = (F - p_k) ./ nctpdf (x_k, df_k, mu_k); ## Prevent Infs - NaNs infnan = isinf (h_k) | isnan (h_k); if (any (infnan(:))) h_k(infnan) = x_k(infnan) / 10; endif ## Prepare for next step xnew = max (-5 * abs (x_k), min (5 * abs (x_k), x_k - h_k)); ## Check that next step improves, otherwise abort Fnew = nctcdf (xnew, df_k, mu_k); while (true) worse = (abs (Fnew - p_k) > abs (F - p_k) * (1 + crit)) & ... (abs (x_k - xnew) > crit * abs (x_k)); if (! any (worse)) break; endif xnew(worse) = 0.5 * (xnew(worse) + x_k(worse)); Fnew(worse) = nctcdf (xnew(worse), df_k(worse), mu_k(worse)); endwhile x_k = xnew; F = Fnew; endwhile ## Return the converged value(s). x(k) = x_k; if (count == count_limit) warning ("nctinv: did not converge."); endif endfunction %!demo %! ## Plot various iCDFs from the noncentral T distribution %! p = 0.001:0.001:0.999; %! x1 = nctinv (p, 1, 0); %! x2 = nctinv (p, 4, 0); %! x3 = nctinv (p, 1, 2); %! x4 = nctinv (p, 4, 2); %! plot (p, x1, '-r', p, x2, '-g', p, x3, '-k', p, x4, '-m') %! grid on %! ylim ([-5, 5]) %! legend ({'df = 1, μ = 0', 'df = 4, μ = 0', ... %! 'df = 1, μ = 2', 'df = 4, μ = 2'}, 'location', 'northwest') %! title ('Noncentral T iCDF') %! xlabel ('probability') %! ylabel ('values in x') %!demo %! ## Compare the noncentral T iCDF with MU = 1 to the T iCDF %! ## with the same number of degrees of freedom (10). %! %! p = 0.001:0.001:0.999; %! x1 = nctinv (p, 10, 1); %! x2 = tinv (p, 10); %! plot (p, x1, '-', p, x2, '-'); %! grid on %! ylim ([-5, 5]) %! legend ({'Noncentral T(10,1)', 'T(10)'}, 'location', 'northwest') %! title ('Noncentral T vs T quantile functions') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!test %! x = [-Inf,-0.3347,0.1756,0.5209,0.8279,1.1424,1.5021,1.9633,2.6571,4.0845,Inf]; %! assert_equal (nctinv ([0:0.1:1], 2, 1), x, 1e-4); %!test %! x = [-Inf,1.5756,2.0827,2.5343,3.0043,3.5406,4.2050,5.1128,6.5510,9.6442,Inf]; %! assert_equal (nctinv ([0:0.1:1], 2, 3), x, 1e-4); %!test %! x = [-Inf,2.2167,2.9567,3.7276,4.6464,5.8455,7.5619,10.3327,15.7569,31.8159,Inf]; %! assert_equal (nctinv ([0:0.1:1], 1, 4), x, 1e-4); %!test %! x = [1.7791 1.9368 2.0239 2.0801 2.1195 2.1489]; %! assert_equal (nctinv (0.05, [1, 2, 3, 4, 5, 6], 4), x, 1e-4); %!test %! x = [-0.7755, 0.3670, 1.2554, 2.0239, 2.7348, 3.4154]; %! assert_equal (nctinv (0.05, 3, [1, 2, 3, 4, 5, 6]), x, 1e-4); %!test %! x = [-0.7183, 0.3624, 1.2878, 2.1195, -3.5413, 3.6430]; %! assert_equal (nctinv (0.05, 5, [1, 2, 3, 4, -1, 6]), x, 1e-4); %!test %! assert_equal (nctinv (0.996, 5, 8), 30.02610554063658, 2e-11); ## Test input validation %!error nctinv () %!error nctinv (1) %!error nctinv (1, 2) %!error ... %! nctinv (ones (3), ones (2), ones (2)) %!error ... %! nctinv (ones (2), ones (3), ones (2)) %!error ... %! nctinv (ones (2), ones (2), ones (3)) %!error nctinv (int32 (2), 2, 2) %!error nctinv (true, 2, 2) %!error nctinv ('a', 2, 2) %!error nctinv (i, 2, 2) %!error nctinv (2, i, 2) %!error nctinv (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/nctpdf.m000066400000000000000000000156561527311104300244560ustar00rootroot00000000000000## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} nctpdf (@var{x}, @var{df}, @var{mu}) ## ## Noncentral @math{t}-probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the noncentral @math{t}-distribution with @var{df} degrees of freedom and ## noncentrality parameter @var{mu}. The size of @var{y} is the common size ## of @var{x}, @var{df}, and @var{mu}. A scalar input functions as a ## constant matrix of the same size as the other inputs. ## ## Further information about the noncentral @math{t}-distribution can be found ## at @url{https://en.wikipedia.org/wiki/Noncentral_t-distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{nctcdf, nctinv, nctrnd, nctstat, tpdf} ## @end deftypefn function y = nctpdf (x, df, mu) ## Check for valid number of input arguments if (nargin < 3) error ("nctpdf: function called with too few input arguments."); endif ## Check for common size of X, DF, and MU [err, x, df, mu] = common_size (x, df, mu); if (err > 0) error ("nctpdf: X, DF, and MU must be of common size or scalars."); endif ## Check for X, DF, and MU being double or single if (! (isfloat (x) && isfloat (df) && isfloat (mu))) error ("nctpdf: X, DF, and MU must be double or single."); endif ## Check for X, DF, and MU being reals if (iscomplex (x) || iscomplex (df) || iscomplex (mu)) error ("nctpdf: X, DF, and MU must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (df, 'single') || isa (mu, 'single')) y = zeros (size (x), 'single'); else y = zeros (size (x)); endif ## Find NaNs in input arguments (if any) and propagate them to p is_nan = isnan (x) | isnan (df) | isnan (mu); y(is_nan) = NaN; ## Force invalid parameter cases to NaN invalid = df <= 0 | ! isfinite (mu); y(invalid) = NaN; ## Use normal approximation for df > 1e6 bigDF = df > 1e6 & ! is_nan & ! invalid; if (any (bigDF(:))) s = 1 - 1 ./ (4 * df); d = sqrt (1 + x .^ 2 ./ (2 * df)); y(bigDF) = normpdf (x(bigDF) .* s(bigDF), mu(bigDF), d(bigDF)); endif ## For negative x use left tail cdf x_neg = find ((x < 0) & isfinite (x) & df <= 1e6 & ! is_nan & ! invalid); if (any (x_neg)) y(x_neg) = (df(x_neg) ./ x(x_neg)) .* ... (nctcdf (x(x_neg) .* sqrt ((df(x_neg) + 2) ./ df(x_neg)), ... df(x_neg) + 2, mu(x_neg)) - ... nctcdf (x(x_neg), df(x_neg), mu(x_neg))); endif ## For positive x reflect about zero and use left tail cdf x_pos = find ((x > 0) & isfinite (x) & df <= 1e6 & ! is_nan & ! invalid); if (any (x_pos)) y(x_pos) = (-df(x_pos) ./ x(x_pos)) .* ... (nctcdf (-x(x_pos) .* sqrt ((df(x_pos) + 2) ./ df(x_pos)), ... df(x_pos) + 2, -mu(x_pos)) - ... nctcdf (-x(x_pos), df(x_pos), -mu(x_pos))); endif ## For x == 0 use power series xzero = find ((x == 0) & df <= 1e6 & ! is_nan & ! invalid); if (any (xzero)) y(xzero) = exp (-0.5 * mu(xzero) .^ 2 - 0.5 * log (pi * df(xzero)) + ... gammaln (0.5 * (df(xzero) + 1)) - gammaln (0.5 * df(xzero))); endif endfunction %!demo %! ## Plot various PDFs from the noncentral T distribution %! x = -5:0.01:10; %! y1 = nctpdf (x, 1, 0); %! y2 = nctpdf (x, 4, 0); %! y3 = nctpdf (x, 1, 2); %! y4 = nctpdf (x, 4, 2); %! plot (x, y1, '-r', x, y2, '-g', x, y3, '-k', x, y4, '-m') %! grid on %! xlim ([-5, 10]) %! ylim ([0, 0.4]) %! legend ({'df = 1, μ = 0', 'df = 4, μ = 0', ... %! 'df = 1, μ = 2', 'df = 4, μ = 2'}, 'location', 'northeast') %! title ('Noncentral T PDF') %! xlabel ('values in x') %! ylabel ('density') %!demo %! ## Compare the noncentral T PDF with MU = 1 to the T PDF %! ## with the same number of degrees of freedom (10). %! %! x = -5:0.1:5; %! y1 = nctpdf (x, 10, 1); %! y2 = tpdf (x, 10); %! plot (x, y1, '-', x, y2, '-'); %! grid on %! xlim ([-5, 5]) %! ylim ([0, 0.4]) %! legend ({'Noncentral χ^2(4,2)', 'χ^2(4)'}, 'location', 'northwest') %! title ('Noncentral T vs T PDFs') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x1, df, mu %! x1 = [-Inf, 2, NaN, 4, Inf]; %! df = [2, 0, -1, 1, 4]; %! mu = [1, NaN, 3, -1, 2]; %!assert_equal (nctpdf (x1, df, mu), [0, NaN, NaN, 0.00401787561306999, 0], 1e-14); %!assert_equal (nctpdf (x1, df, 1), [0, NaN, NaN, 0.0482312135423008, 0], 1e-14); %!assert_equal (nctpdf (x1, df, 3), [0, NaN, NaN, 0.1048493126401585, 0], 1e-14); %!assert_equal (nctpdf (x1, df, 2), [0, NaN, NaN, 0.08137377919890307, 0], 1e-14); %!assert_equal (nctpdf (x1, 3, mu), [0, NaN, NaN, 0.001185305171654381, 0], 1e-14); %!assert_equal (nctpdf (2, df, mu), [0.1791097459405861, NaN, NaN, ... %! 0.0146500727180389, 0.3082302682110299], 1e-14); %!assert_equal (nctpdf (4, df, mu), [0.04467929612254971, NaN, NaN, ... %! 0.00401787561306999, 0.0972086534042828], 1e-14); ## Test input validation %!error nctpdf () %!error nctpdf (1) %!error nctpdf (1, 2) %!error ... %! nctpdf (ones (3), ones (2), ones (2)) %!error ... %! nctpdf (ones (2), ones (3), ones (2)) %!error ... %! nctpdf (ones (2), ones (2), ones (3)) %!error nctpdf (int32 (2), 2, 2) %!error nctpdf (true, 2, 2) %!error nctpdf ('a', 2, 2) %!error nctpdf (i, 2, 2) %!error nctpdf (2, i, 2) %!error nctpdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/nctrnd.m000066400000000000000000000153231527311104300244570ustar00rootroot00000000000000## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} nctrnd (@var{df}, @var{mu}) ## @deftypefnx {statistics} {@var{r} =} nctrnd (@var{df}, @var{mu}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} nctrnd (@var{df}, @var{mu}, [@var{sz}]) ## ## Random arrays from the noncentral @math{t}-distribution. ## ## @code{@var{x} = nctrnd (@var{p}, @var{df}, @var{mu})} returns an array of ## random numbers chosen from the noncentral @math{t}-distribution with @var{df} ## degrees of freedom and noncentrality parameter @var{mu}. The size of ## @var{r} is the common size of @var{df} and @var{mu}. A scalar input ## functions as a constant matrix of the same size as the other input. ## ## @code{nctrnd} generates values using the definition of a noncentral @math{t} ## random variable, as the ratio of a normal distribution with non-zero mean and ## the sqrt of a chi-squared distribution. ## ## When called with a single size argument, @code{nctrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the noncentral @math{t}-distribution can be found ## at @url{https://en.wikipedia.org/wiki/Noncentral_t-distribution} ## ## @seealso{nctcdf, nctinv, nctpdf, nctstat, trnd, normrnd, chi2rnd} ## @end deftypefn function r = nctrnd (df, mu, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("nctrnd: function called with too few input arguments."); endif ## Check for common size of DF and MU if (! isscalar (df) || ! isscalar (mu)) [retval, df, mu] = common_size (df, mu); if (retval > 0) error ("nctrnd: DF and MU must be of common size or scalars."); endif endif ## Check for DF and MU being reals if (iscomplex (df) || iscomplex (mu)) error ("nctrnd: DF and MU must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (df); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("nctrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("nctrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (df) && ! isequal (size (df), size (ones (sz)))) error ("nctrnd: DF and MU must be scalars or of size SZ."); endif ## Check for class type if (isa (df, 'single') || isa (mu, 'single')); cls = 'single'; else cls = 'double'; endif ## Return NaNs for out of range values of DF df(df <= 0) = NaN; ## Prevent Inf/Inf==NaN for the standardized chi-square in the denom. df(isinf (df)) = realmax; ## Generate random sample from noncentral F distribution r = (randn (sz) + mu) ./ sqrt (2 .* randg (df ./ 2, sz) ./ df); ## Cast to appropriate class r = cast (r, cls); endfunction ## Test output %!assert_equal (size (nctrnd (1, 1)), [1, 1]) %!assert_equal (size (nctrnd (1, ones (2, 1))), [2, 1]) %!assert_equal (size (nctrnd (1, ones (2, 2))), [2, 2]) %!assert_equal (size (nctrnd (ones (2, 1), 1)), [2, 1]) %!assert_equal (size (nctrnd (ones (2, 2), 1)), [2, 2]) %!assert_equal (size (nctrnd (1, 1, 3)), [3, 3]) %!assert_equal (size (nctrnd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (nctrnd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (nctrnd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (nctrnd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (nctrnd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (nctrnd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (nctrnd (1, 1, [])), [0, 0]) %!assert_equal (size (nctrnd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (nctrnd (1, 2, -1)), [0, 0]) %!assert_equal (size (nctrnd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (nctrnd (1, 2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (nctrnd (1, 1)), "double") %!assert_equal (class (nctrnd (1, single (1))), "single") %!assert_equal (class (nctrnd (1, single ([1, 1]))), "single") %!assert_equal (class (nctrnd (single (1), 1)), "single") %!assert_equal (class (nctrnd (single ([1, 1]), 1)), "single") ## Test input validation %!error nctrnd () %!error nctrnd (1) %!error ... %! nctrnd (ones (3), ones (2)) %!error ... %! nctrnd (ones (2), ones (3)) %!error nctrnd (i, 2) %!error nctrnd (1, i) %!error ... %! nctrnd (1, 2, 1.2) %!error ... %! nctrnd (1, 2, ones (2)) %!error ... %! nctrnd (1, 2, [2 0 2.5]) %!error ... %! nctrnd (1, 2, 2, 1.5, 5) %!error ... %! nctrnd (2, ones (2), 3) %!error ... %! nctrnd (2, ones (2), [3, 2]) %!error ... %! nctrnd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/ncx2cdf.m000066400000000000000000000247401527311104300245210ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} ncx2cdf (@var{x}, @var{df}, @var{lambda}) ## @deftypefnx {statistics} {@var{p} =} ncx2cdf (@var{x}, @var{df}, @var{lambda}, @qcode{'upper'}) ## ## Noncentral chi-squared cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the noncentral chi-squared distribution with @var{df} degrees of ## freedom and noncentrality parameter @var{lambda}. The size of @var{p} is the ## common size of @var{x}, @var{df}, and @var{lambda}. A scalar input functions ## as a constant matrix of the same size as the other inputs. ## ## @code{@var{p} = ncx2cdf (@var{x}, @var{df}, @var{lambda}, "upper")} computes ## the upper tail probability of the noncentral chi-squared distribution with ## parameters @var{df} and @var{lambda}, at the values in @var{x}. ## ## Further information about the noncentral chi-squared distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Noncentral_chi-squared_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{ncx2inv, ncx2pdf, ncx2rnd, ncx2stat, chi2cdf} ## @end deftypefn function p = ncx2cdf (x, df, lambda, uflag) ## Check for valid number of input arguments if (nargin < 3) error ("ncx2cdf: function called with too few input arguments."); endif ## Check for valid "upper" flag if (nargin > 3) if (! strcmpi (uflag, 'upper')) error ("ncx2cdf: invalid argument for upper tail."); else uflag = true; endif else uflag = false; endif ## Check for common size of X, DF, and LAMBDA [err, x, df, lambda] = common_size (x, df, lambda); if (err > 0) error ("ncx2cdf: X, DF, and LAMBDA must be of common size or scalars."); endif ## Check for X, DF, and LAMBDA being double or single if (! (isfloat (x) && isfloat (df) && isfloat (lambda))) error ("ncx2cdf: X, DF, and LAMBDA must be double or single."); endif ## Check for X, DF, and LAMBDA being reals if (iscomplex (x) || iscomplex (df) || iscomplex (lambda)) error ("ncx2cdf: X, DF, and LAMBDA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (df, 'single') || isa (lambda, 'single')) p = zeros (size (x), 'single'); c_eps = eps ('single'); c_min = realmin ('single'); else p = zeros (size (x)); c_eps = eps; c_min = realmin; endif ## Find NaNs in input arguments (if any) and propagate them to p is_nan = isnan (x) | isnan (df) | isnan (lambda); p(is_nan) = NaN; if (uflag) p(x == Inf & ! is_nan) = 0; p(x <= 0 & ! is_nan) = 1; else p(x == Inf & ! is_nan) = 1; endif ## Make P = NaN for negative values of noncentrality parameter and DF p(lambda < 0) = NaN; p(df < 0) = NaN; ## For DF == 0 at x == 0 k = df == 0 & x == 0 & lambda >= 0 & ! is_nan; if (uflag) p(k) = -expm1 (-lambda(k) / 2); else p(k) = exp (-lambda(k) / 2); endif ## Central chi2cdf k = df >= 0 & x > 0 & lambda == 0 & isfinite (x) & ! is_nan; if (uflag) p(k) = chi2cdf (x(k), df(k), 'upper'); else p(k) = chi2cdf (x(k), df(k)); endif ## Keep only valid samples td = find (df >= 0 & x > 0 & lambda > 0 & isfinite (x) & ! is_nan); lambda = lambda(td) / 2; df = df(td) / 2; x = x(td) / 2; ## Compute Chernoff bounds e0 = log (c_min); e1 = log (c_eps/4); t = 1 - (df + sqrt (df .^ 2 + 4 * lambda .* x)) ./ (2 * x); q = lambda .* t ./ (1 - t) - df .* log (1 - t) - t .* x; peq0 = x < lambda + df & q < e0; peq1 = x > lambda + df & q < e1; if (uflag) p(td(peq0)) = 1; else p(td(peq1)) = 1; endif td(peq0 | peq1) = []; x(peq0 | peq1) = []; df(peq0 | peq1) = []; lambda(peq0 | peq1) = []; ## Find index K of the maximal term in the summation series. ## K1 and K2 are lower and upper bounds for K, respectively. ## Indexing of terms in the summation series starts at 0. K1 = ceil ((sqrt ((df + x) .^ 2 + 4 * x .* lambda) - (df + x)) / 2); K = zeros (size (x)); k1above1 = find (K1 > 1); K2 = floor (lambda(k1above1) .* gammaincratio (x(k1above1), K1(k1above1))); fixK2 = isnan (K2) | isinf (K2); K2(fixK2) = K1(k1above1(fixK2)); K(k1above1) = K2; ## Find Poisson and Poisson*chi2cdf parts for the maximal terms in the ## summation series. if (uflag) k0 = (K==0 & df==0); K(k0) = 1; endif pois = poisspdf (K, lambda); if (uflag) full = pois .* gammainc (x, df + K, 'upper'); else full = pois .* gammainc (x, df + K); endif ## Sum the series. First go downward from K and then go upward. ## The term for K is added afterwards - it is not included in either sum. sumK = zeros (size (x)); ## Downward. poisspdf(k-1,lambda)/poisspdf(k,lambda) = k/lambda poisterm = pois; fullterm = full; keep = K > 0 & fullterm > 0; k = K; while any (keep) poisterm(keep) = poisterm(keep) .* k(keep) ./ lambda(keep); k(keep) = k(keep) - 1; if (uflag) fullterm(keep) = poisterm(keep) .* ... gammainc (x(keep), df(keep) + k(keep), 'upper'); else fullterm(keep) = poisterm(keep) .* ... gammainc (x(keep), df(keep) + k(keep)); endif sumK(keep) = sumK(keep) + fullterm(keep); keep = keep & k > 0 & fullterm > eps (sumK); endwhile ## Upward. poisspdf(k+1,lambda)/poisspdf(k,lambda) = lambda/(k+1) poisterm = pois; fullterm = full; keep = fullterm > 0; k = K; while any (keep) k(keep) = k(keep)+1; poisterm(keep) = poisterm(keep) .* lambda(keep) ./ k(keep); if (uflag) fullterm(keep) = poisterm(keep) .* ... gammainc (x(keep), df(keep) + k(keep), 'upper'); else fullterm(keep) = poisterm(keep) .* ... gammainc (x(keep), df(keep) + k(keep)); endif sumK(keep) = sumK(keep) + fullterm(keep); keep = keep & fullterm > eps (sumK); endwhile ## Get probabilities p(td) = full + sumK; p(p > 1) = 1; endfunction ## Ratio of incomplete gamma function values at S and S-1. function r = gammaincratio (x, s) ## Initialize r = zeros (size (s)); ## Finf small small = s < 2 | s <= x; ## For small S, use the ratio computed directly if (any (small(:))) r(small) = gammainc (x(small), s(small)) ./ ... gammainc (x(small), s(small) - 1); endif ## For large S, estimate numerator and denominator using 'scaledlower' option if (any (! small(:))) idx = find (! small); x = x(idx); s = s(idx); r(idx) = gammainc (x, s, 'scaledlower') ./ ... gammainc (x, s - 1, 'scaledlower') .* x ./ s; endif endfunction %!demo %! ## Plot various CDFs from the noncentral chi-squared distribution %! x = 0:0.1:10; %! p1 = ncx2cdf (x, 2, 1); %! p2 = ncx2cdf (x, 2, 2); %! p3 = ncx2cdf (x, 2, 3); %! p4 = ncx2cdf (x, 4, 1); %! p5 = ncx2cdf (x, 4, 2); %! p6 = ncx2cdf (x, 4, 3); %! plot (x, p1, '-r', x, p2, '-g', x, p3, '-k', ... %! x, p4, '-m', x, p5, '-c', x, p6, '-y') %! grid on %! xlim ([0, 10]) %! legend ({'df = 2, λ = 1', 'df = 2, λ = 2', ... %! 'df = 2, λ = 3', 'df = 4, λ = 1', ... %! 'df = 4, λ = 2', 'df = 4, λ = 3'}, 'location', 'southeast') %! title ('Noncentral chi-squared CDF') %! xlabel ('values in x') %! ylabel ('probability') %!demo %! ## Compare the noncentral chi-squared CDF with LAMBDA = 2 to the %! ## chi-squared CDF with the same number of degrees of freedom (4). %! %! x = 0:0.1:10; %! p1 = ncx2cdf (x, 4, 2); %! p2 = chi2cdf (x, 4); %! plot (x, p1, '-', x, p2, '-') %! grid on %! xlim ([0, 10]) %! legend ({'Noncentral χ^2(4,2)', 'χ^2(4)'}, 'location', 'northwest') %! title ('Noncentral chi-squared vs chi-squared CDFs') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!test %! x = -2:0.1:2; %! p = ncx2cdf (x, 10, 1); %! assert_equal (p([1:21]), zeros (1, 21), 3e-84); %! assert_equal (p(22), 1.521400636466575e-09, 1e-14); %! assert_equal (p(30), 6.665480510026046e-05, 1e-14); %! assert_equal (p(41), 0.002406447308399836, 1e-14); %!test %! p = ncx2cdf (12, 10, 3); %! assert_equal (p, 0.4845555602398649, 1e-14); %!test %! p = ncx2cdf (2, 3, 2); %! assert_equal (p, 0.2207330870741212, 1e-14); %!test %! p = ncx2cdf (2, 3, 2, 'upper'); %! assert_equal (p, 0.7792669129258789, 1e-14); %!test %! p = ncx2cdf ([3, 6], 3, 2, 'upper'); %! assert_equal (p, [0.6423318186400054, 0.3152299878943012], 1e-14); ## Test input validation %!error ncx2cdf () %!error ncx2cdf (1) %!error ncx2cdf (1, 2) %!error ncx2cdf (1, 2, 3, 'tail') %!error ncx2cdf (1, 2, 3, 4) %!error ... %! ncx2cdf (ones (3), ones (2), ones (2)) %!error ... %! ncx2cdf (ones (2), ones (3), ones (2)) %!error ... %! ncx2cdf (ones (2), ones (2), ones (3)) %!error ncx2cdf (int32 (2), 2, 2) %!error ncx2cdf (true, 2, 2) %!error ncx2cdf ('a', 2, 2) %!error ncx2cdf (i, 2, 2) %!error ncx2cdf (2, i, 2) %!error ncx2cdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/ncx2inv.m000066400000000000000000000176351527311104300245660ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} ncx2inv (@var{p}, @var{df}, @var{lambda}) ## ## Inverse of the noncentral chi-squared cumulative distribution function ## (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the noncentral chi-squared distribution with @var{df} degrees of freedom and ## noncentrality parameter @var{mu}. The size of @var{x} is the common size of ## @var{p}, @var{df}, and @var{mu}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## @code{ncx2inv} uses Newton's method to converge to the solution. ## ## Further information about the noncentral chi-squared distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Noncentral_chi-squared_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{ncx2cdf, ncx2pdf, ncx2rnd, ncx2stat, chi2inv} ## @end deftypefn function x = ncx2inv (p, df, lambda) ## Check for valid number of input arguments if (nargin < 3) error ("ncx2inv: function called with too few input arguments."); endif ## Check for common size of P, DF, and LAMBDA [err, p, df, lambda] = common_size (p, df, lambda); if (err > 0) error ("ncx2inv: P, DF, and LAMBDA must be of common size or scalars."); endif ## Check for P, DF, and LAMBDA being double or single if (! (isfloat (p) && isfloat (df) && isfloat (lambda))) error ("ncx2inv: P, DF, and LAMBDA must be double or single."); endif ## Check for P, DF, and LAMBDA being reals if (iscomplex (p) || iscomplex (df) || iscomplex (lambda)) error ("ncx2inv: P, DF, and LAMBDA must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (df, 'single') || isa (lambda, 'single')) x = NaN (size (p), 'single'); crit = sqrt (eps ('single')); else x = NaN (size (p), 'double'); crit = sqrt (eps ('double')); endif ## For lambda == 0, call chi2inv d0 = lambda == 0; if (any (d0(:))) x(d0) = chi2inv (p(d0), df(d0)); ## If lambda == 0 for all entries, then return if (all (d0(:))) return; endif endif ## CDF with 0 d.d0. has a step at x=0. ## Check if CDF at x=0 exceeds the requested p. df0 = df==0 & lambda > 0; if (any (df0(:))) p0 = zeros (size (p)); p0(df0) = ncx2cdf (0, df(df0), lambda(df0)); df0 = df0 & p0 >= p; x(df0) = 0; endif valid = ! df0 & df > 0 & lambda > 0; ## Force x = 0 for p == 0 and x = Inf for p == 1 x(p == 0 & valid) = 0; x(p == 1 & valid) = Inf; ## Find valid samples within the range of 0 < p < 1 k = find (p > 0 & p < 1 & valid); pk = p(k); ## Initialize counter count_limit = 100; count = 0; ## Supply a starting guess for the iteration. mn = df(k) + lambda(k); variance = 2 * (df(k) + 2 * lambda(k)); temp = log (variance + mn .^ 2); mu = 2 * log (mn) - 0.5 * temp; sigma = -2 * log (mn) + temp; xk = exp (norminv (pk, mu, sigma)); F = ncx2cdf (xk, df(k), lambda(k)); h = ones (size (xk), class (xk)); ## Start iteration with a break out loop while (count < count_limit) count = count + 1; h = (F - pk) ./ ncx2pdf (xk, df(k), lambda(k)); xnew = max (xk / 50, min (5 * xk, xk - h)); newF = ncx2cdf (xnew, df(k), lambda(k)); while (true) worse = (abs (newF - pk) > abs (F - pk) * (1 + crit)) & ... (abs (xk - xnew) > crit * xk); if (! any (worse)) break; endif xnew(worse) = 0.5 * (xnew(worse) + xk(worse)); newF(worse) = ncx2cdf (xnew(worse), df(k(worse)), lambda(k(worse))); endwhile h = xk - xnew; x(k) = xnew; mask = (abs (h) > crit * abs (xk)); if (! any (mask)) break; endif k = k(mask); xk = xnew(mask); F = newF(mask); pk = pk(mask); endwhile if (count == count_limit) warning ("ncx2inv: did not converge."); endif endfunction %!demo %! ## Plot various iCDFs from the noncentral chi-squared distribution %! p = 0.001:0.001:0.999; %! x1 = ncx2inv (p, 2, 1); %! x2 = ncx2inv (p, 2, 2); %! x3 = ncx2inv (p, 2, 3); %! x4 = ncx2inv (p, 4, 1); %! x5 = ncx2inv (p, 4, 2); %! x6 = ncx2inv (p, 4, 3); %! plot (p, x1, '-r', p, x2, '-g', p, x3, '-k', ... %! p, x4, '-m', p, x5, '-c', p, x6, '-y') %! grid on %! ylim ([0, 10]) %! legend ({'df = 2, λ = 1', 'df = 2, λ = 2', ... %! 'df = 2, λ = 3', 'df = 4, λ = 1', ... %! 'df = 4, λ = 2', 'df = 4, λ = 3'}, 'location', 'northwest') %! title ('Noncentral chi-squared iCDF') %! xlabel ('probability') %! ylabel ('values in x') %!demo %! ## Compare the noncentral chi-squared CDF with LAMBDA = 2 to the %! ## chi-squared CDF with the same number of degrees of freedom (4). %! %! p = 0.001:0.001:0.999; %! x1 = ncx2inv (p, 4, 2); %! x2 = chi2inv (p, 4); %! plot (p, x1, '-', p, x2, '-'); %! grid on %! ylim ([0, 10]) %! legend ({'Noncentral χ^2(4,2)', 'χ^2(4)'}, 'location', 'northwest') %! title ('Noncentral chi-squared vs chi-squared quantile functions') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!test %! x = [0,0.3443,0.7226,1.1440,1.6220,2.1770,2.8436,3.6854,4.8447,6.7701,Inf]; %! assert_equal (ncx2inv ([0:0.1:1], 2, 1), x, 1e-4); %!test %! x = [0,0.8295,1.6001,2.3708,3.1785,4.0598,5.0644,6.2765,7.8763,10.4199,Inf]; %! assert_equal (ncx2inv ([0:0.1:1], 2, 3), x, 1e-4); %!test %! x = [0,0.5417,1.3483,2.1796,3.0516,4.0003,5.0777,6.3726,8.0748,10.7686,Inf]; %! assert_equal (ncx2inv ([0:0.1:1], 1, 4), x, 1e-4); %!test %! x = [0.1808, 0.6456, 1.1842, 1.7650, 2.3760, 3.0105]; %! assert_equal (ncx2inv (0.05, [1, 2, 3, 4, 5, 6], 4), x, 1e-4); %!test %! x = [0.4887, 0.6699, 0.9012, 1.1842, 1.5164, 1.8927]; %! assert_equal (ncx2inv (0.05, 3, [1, 2, 3, 4, 5, 6]), x, 1e-4); %!test %! x = [1.3941, 1.6824, 2.0103, 2.3760, NaN, 3.2087]; %! assert_equal (ncx2inv (0.05, 5, [1, 2, 3, 4, -1, 6]), x, 1e-4); %!test %! assert_equal (ncx2inv (0.996, 5, 8), 35.51298862765576, 3e-13); ## Test input validation %!error ncx2inv () %!error ncx2inv (1) %!error ncx2inv (1, 2) %!error ... %! ncx2inv (ones (3), ones (2), ones (2)) %!error ... %! ncx2inv (ones (2), ones (3), ones (2)) %!error ... %! ncx2inv (ones (2), ones (2), ones (3)) %!error ncx2inv (int32 (2), 2, 2) %!error ncx2inv (true, 2, 2) %!error ncx2inv ('a', 2, 2) %!error ncx2inv (i, 2, 2) %!error ncx2inv (2, i, 2) %!error ncx2inv (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/ncx2pdf.m000066400000000000000000000320001527311104300245220ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} ncx2pdf (@var{x}, @var{df}, @var{lambda}) ## ## Noncentral chi-squared probability distribution function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the noncentral chi-squared distribution with @var{df} degrees of freedom ## and noncentrality parameter @var{lambda}. The size of @var{y} is the common ## size of @var{x}, @var{df}, and @var{lambda}. A scalar input functions as a ## constant matrix of the same size as the other inputs. ## ## Further information about the noncentral chi-squared distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Noncentral_chi-squared_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{ncx2cdf, ncx2inv, ncx2rnd, ncx2stat, chi2pdf} ## @end deftypefn function y = ncx2pdf (x, df, lambda) ## Check for valid number of input arguments if (nargin < 3) error ("ncx2pdf: function called with too few input arguments."); endif ## Check for common size of X, DF, and LAMBDA [err, x, df, lambda] = common_size (x, df, lambda); if (err > 0) error ("ncx2pdf: X, DF, and LAMBDA must be of common size or scalars."); endif ## Check for X, DF, and LAMBDA being double or single if (! (isfloat (x) && isfloat (df) && isfloat (lambda))) error ("ncx2pdf: X, DF, and LAMBDA must be double or single."); endif ## Check for X, DF, and LAMBDA being reals if (iscomplex (x) || iscomplex (df) || iscomplex (lambda)) error ("ncx2pdf: X, DF, and LAMBDA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (df, 'single') || isa (lambda, 'single')) y = zeros (size (x), 'single'); else y = zeros (size (x)); endif ## Find NaNs in input arguments (if any) and propagate them to p is_nan = isnan (x) | isnan (df) | isnan (lambda); y(is_nan) = NaN; ## Make input arguments column vectors and half DF x = x(:); df = df(:); df = df / 2; lambda = lambda(:); ## Handle special cases k1 = x == 0 & df == 1; y(k1) = 0.5 * exp (-0.5 * lambda(k1)); k2 = x == 0 & df < 1; y(k2) = Inf; y(lambda < 0) = NaN; y(df < 0) = NaN; k3 = lambda == 0 & df > 0; y(k3) = gampdf (x(k3), df(k3), 2); ## Handle normal cases td = find (x>0 & x0 & df>=0); ## Return if finished all normal cases if (isempty (td)) return; endif ## Reset input variables to remaining cases x = x(td); lambda = lambda(td); df = df(td) - 1; x_sqrt = sqrt (x); d_sqrt = sqrt (lambda); ## Upper Limit on density small_DF = df <= -0.5; large_DF = ! small_DF; ul = zeros (size (x)); ul(small_DF) = -0.5 * (lambda(small_DF) + x(small_DF)) + ... 0.5 * x_sqrt(small_DF) .* d_sqrt(small_DF) ./ ... (df(small_DF) + 1) + df(small_DF) .* ... (log (x(small_DF)) - log (2)) - log (2) - ... gammaln (df(small_DF) + 1); ul(large_DF) = -0.5 * (d_sqrt(large_DF) - x_sqrt(large_DF)) .^ 2 + ... df(large_DF) .* (log (x(large_DF)) - log (2)) - log (2) - ... gammaln (df(large_DF) + 1) + (df(large_DF) + 0.5) .* ... log ((df(large_DF) + 0.5) ./ (x_sqrt(large_DF) .* ... d_sqrt(large_DF) + df(large_DF) + 0.5)); ULunderflow = ul < log (realmin); y(td(ULunderflow)) = 0; td(ULunderflow) = []; ## Return if finished all normal cases if (isempty (td)) return; endif x(ULunderflow) = []; lambda(ULunderflow) = []; df(ULunderflow) = []; x_sqrt(ULunderflow) = []; d_sqrt(ULunderflow) = []; ## Try the scaled Bess function scaleB = besseli (df, d_sqrt .* x_sqrt, 1); use_SB = scaleB > 0 & scaleB < Inf; y(td(use_SB)) = exp (-log (2) -0.5 * (x_sqrt(use_SB) - ... d_sqrt(use_SB)) .^ 2 + df(use_SB) .* ... log (x_sqrt(use_SB) ./ d_sqrt(use_SB))) .* scaleB(use_SB); td(use_SB) = []; ## Return if finished all normal cases if (isempty (td)) return; endif x(use_SB) = []; lambda(use_SB) = []; df(use_SB) = []; x_sqrt(use_SB) = []; d_sqrt(use_SB) = []; ## Try the Bess function Bess = besseli (df, d_sqrt .* x_sqrt); useB = Bess > 0 & Bess < Inf; y(td(useB)) = exp (-log (2) - 0.5 * (x(useB) + lambda(useB)) + ... df(useB) .* log (x_sqrt(useB) ./ d_sqrt(useB))) .* Bess(useB); td(useB) = []; ## Return if finished all normal cases if isempty (td) return; endif x(useB) = []; lambda(useB) = []; df(useB) = []; ## If neither Bess function works, use recursion. When non-centrality ## parameter is very large, the initial values of the Poisson numbers used ## in the approximation are very small, smaller than epsilon. This would ## cause premature convergence. To avoid that, we start from the peak of the ## Poisson numbers, and go in both directions. lnsr2pi = 0.9189385332046727; # log(sqrt(2*pi)) dx = lambda .* x / 4; K = max (0, floor (0.5 * (sqrt (df .^ 2 + 4 * dx) - df))); lntK = zeros (size (K)); K0 = K == 0; lntK(K0) = -lnsr2pi -0.5 * (lambda(K0) + log (df(K0))) - ... StirlingError (df(K0)) - BinoPoisson (df(K0), x(K0) / 2); K0 = ! K0; lntK(K0) = -2 * lnsr2pi - 0.5 * (log (K(K0)) + log (df(K0) + K(K0))) - ... StirlingError (K(K0)) - StirlingError (df(K0) + K(K0)) - ... BinoPoisson (K(K0), lambda(K0) / 2) - ... BinoPoisson (df(K0) + K(K0), x(K0) / 2); sumK = ones (size (K)); keep = K>0; term = ones (size (K)); k = K; while (any (keep)) term(keep) = term(keep) .* (df(keep) + k(keep)) .* k(keep) ./ dx(keep); sumK(keep) = sumK(keep) + term(keep); keep = keep & k > 0 & term > eps (sumK); k = k - 1; endwhile keep = true (size (K)); term = ones (size (K)); k = K + 1; while (any (keep)) term(keep) = term(keep) ./ (df(keep) + k(keep)) ./ k(keep) .* dx(keep); sumK(keep) = sumK(keep) + term(keep); keep = keep & term > eps (sumK); k = k + 1; endwhile y(td) = 0.5 * exp (lntK + log (sumK)); endfunction ## Error of Stirling-De Moivre approximation to n factorial. function lambda = StirlingError (n) is_class = class (n); lambda = zeros (size (n), is_class); nn = n .* n; ## Define S0=1/12 S1=1/360 S2=1/1260 S3=1/1680 S4=1/1188 S0 = 8.333333333333333e-02; S1 = 2.777777777777778e-03; S2 = 7.936507936507937e-04; S3 = 5.952380952380952e-04; S4 = 8.417508417508418e-04; ## Define lambda(n) for n<0:0.5:15 sfe=[ 0; 1.534264097200273e-01;... 8.106146679532726e-02; 5.481412105191765e-02;... 4.134069595540929e-02; 3.316287351993629e-02;... 2.767792568499834e-02; 2.374616365629750e-02;... 2.079067210376509e-02; 1.848845053267319e-02;... 1.664469118982119e-02; 1.513497322191738e-02;... 1.387612882307075e-02; 1.281046524292023e-02;... 1.189670994589177e-02; 1.110455975820868e-02;... 1.041126526197210e-02; 9.799416126158803e-03;... 9.255462182712733e-03; 8.768700134139385e-03;... 8.330563433362871e-03; 7.934114564314021e-03;... 7.573675487951841e-03; 7.244554301320383e-03;... 6.942840107209530e-03; 6.665247032707682e-03;... 6.408994188004207e-03; 6.171712263039458e-03;... 5.951370112758848e-03; 5.746216513010116e-03;... 5.554733551962801e-03]; k = find (n <= 15); if (any (k)) n1 = n(k); n2 = 2 * n1; if (all (n2 == round (n2))) lambda(k) = sfe(n2+1); else lnsr2pi = 0.9189385332046728; lambda(k) = gammaln (n1+1)-(n1+0.5).*log (n1)+n1-lnsr2pi; endif endif k = find (n > 15 & n <= 35); if (any (k)) lambda(k) = (S0 - (S1 - (S2 - (S3 - S4 ./ nn(k)) ./ nn(k)) ./ ... nn(k)) ./ nn(k)) ./ n(k); endif k = find (n > 35 & n <= 80); if (any (k)) lambda(k) = (S0 - (S1 - (S2 - S3 ./ nn(k)) ./ nn(k)) ./ nn(k)) ./ n(k); endif k = find (n > 80 & n <= 500); if (any (k)) lambda(k) = (S0 - (S1 - S2 ./ nn(k)) ./ nn(k)) ./ n(k); endif k = find (n > 500); if (any (k)) lambda(k) = (S0 - S1 ./ nn(k)) ./ n(k); endif endfunction ## Deviance term for binomial and Poisson probability calculation. function BP = BinoPoisson (x, np) if (isa (x,'single') || isa (np,'single')) BP = zeros (size (x), 'single'); else BP = zeros (size (x)); endif k = abs (x - np) < 0.1 * (x + np); if any (k(:)) s = (x(k) - np(k)) .* (x(k) - np(k)) ./ (x(k) + np(k)); v = (x(k) - np(k)) ./ (x(k) + np(k)); ej = 2 .* x(k) .* v; is_class = class (s); s1 = zeros (size (s), is_class); ok = true (size (s)); j = 0; while any (ok(:)) ej(ok) = ej(ok) .* v(ok) .* v(ok); j = j + 1; s1(ok) = s(ok) + ej(ok) ./ (2 * j + 1); ok = ok & s1 != s; s(ok) = s1(ok); endwhile BP(k) = s; endif k = ! k; if (any (k(:))) BP(k) = x(k) .* log (x(k) ./ np(k)) + np(k) - x(k); endif endfunction %!demo %! ## Plot various PDFs from the noncentral chi-squared distribution %! x = 0:0.1:10; %! y1 = ncx2pdf (x, 2, 1); %! y2 = ncx2pdf (x, 2, 2); %! y3 = ncx2pdf (x, 2, 3); %! y4 = ncx2pdf (x, 4, 1); %! y5 = ncx2pdf (x, 4, 2); %! y6 = ncx2pdf (x, 4, 3); %! plot (x, y1, '-r', x, y2, '-g', x, y3, '-k', ... %! x, y4, '-m', x, y5, '-c', x, y6, '-y') %! grid on %! xlim ([0, 10]) %! ylim ([0, 0.32]) %! legend ({'df = 2, λ = 1', 'df = 2, λ = 2', ... %! 'df = 2, λ = 3', 'df = 4, λ = 1', ... %! 'df = 4, λ = 2', 'df = 4, λ = 3'}, 'location', 'northeast') %! title ('Noncentral chi-squared PDF') %! xlabel ('values in x') %! ylabel ('density') %!demo %! ## Compare the noncentral chi-squared PDF with LAMBDA = 2 to the %! ## chi-squared PDF with the same number of degrees of freedom (4). %! %! x = 0:0.1:10; %! y1 = ncx2pdf (x, 4, 2); %! y2 = chi2pdf (x, 4); %! plot (x, y1, '-', x, y2, '-'); %! grid on %! xlim ([0, 10]) %! ylim ([0, 0.32]) %! legend ({'Noncentral T(10,1)', 'T(10)'}, 'location', 'northwest') %! title ('Noncentral chi-squared vs chi-squared PDFs') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x1, df, d1 %! x1 = [-Inf, 2, NaN, 4, Inf]; %! df = [2, 0, -1, 1, 4]; %! d1 = [1, NaN, 3, -1, 2]; %!assert_equal (ncx2pdf (x1, df, d1), [0, NaN, NaN, NaN, 0]); %!assert_equal (ncx2pdf (x1, df, 1), [0, 0.07093996461786045, NaN, ... %! 0.06160064323277038, 0], 1e-14); %!assert_equal (ncx2pdf (x1, df, 3), [0, 0.1208364909271113, NaN, ... %! 0.09631299762429098, 0], 1e-14); %!assert_equal (ncx2pdf (x1, df, 2), [0, 0.1076346446244688, NaN, ... %! 0.08430464047296625, 0], 1e-14); %!assert_equal (ncx2pdf (x1, 2, d1), [0, NaN, NaN, NaN, 0]); %!assert_equal (ncx2pdf (2, df, d1), [0.1747201674611283, NaN, NaN, ... %! NaN, 0.1076346446244688], 1e-14); %!assert_equal (ncx2pdf (4, df, d1), [0.09355987820265799, NaN, NaN, ... %! NaN, 0.1192317192431485], 1e-14); ## Test input validation %!error ncx2pdf () %!error ncx2pdf (1) %!error ncx2pdf (1, 2) %!error ... %! ncx2pdf (ones (3), ones (2), ones (2)) %!error ... %! ncx2pdf (ones (2), ones (3), ones (2)) %!error ... %! ncx2pdf (ones (2), ones (2), ones (3)) %!error ncx2pdf (int32 (2), 2, 2) %!error ncx2pdf (true, 2, 2) %!error ncx2pdf ('a', 2, 2) %!error ncx2pdf (i, 2, 2) %!error ncx2pdf (2, i, 2) %!error ncx2pdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/ncx2rnd.m000066400000000000000000000155111527311104300245440ustar00rootroot00000000000000## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} ncx2rnd (@var{df}, @var{lambda}) ## @deftypefnx {statistics} {@var{r} =} ncx2rnd (@var{df}, @var{lambda}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} ncx2rnd (@var{df}, @var{lambda}, [@var{sz}]) ## ## Random arrays from the noncentral chi-squared distribution. ## ## @code{@var{r} = ncx2rnd (@var{df}, @var{lambda})} returns an array of random ## numbers chosen from the noncentral chi-squared distribution with @var{df} ## degrees of freedom and noncentrality parameter @var{lambda}. The size of ## @var{r} is the common size of @var{df} and @var{lambda}. A scalar input ## functions as a constant matrix of the same size as the other input. ## ## When called with a single size argument, @code{ncx2rnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the noncentral chi-squared distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Noncentral_chi-squared_distribution} ## ## @seealso{ncx2cdf, ncx2inv, ncx2pdf, ncx2stat} ## @end deftypefn function r = ncx2rnd (df, lambda, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("ncx2rnd: function called with too few input arguments."); endif ## Check for common size of DF and LAMBDA if (! isscalar (df) || ! isscalar (lambda)) [retval, df, lambda] = common_size (df, lambda); if (retval > 0) error ("ncx2rnd: DF and LAMBDA must be of common size or scalars."); endif endif ## Check for DF and LAMBDA being reals if (iscomplex (df) || iscomplex (lambda)) error ("ncx2rnd: DF and LAMBDA must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (df); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("ncx2rnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("ncx2rnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (df) && ! isequal (size (df), size (ones (sz)))) error ("ncx2rnd: DF and LAMBDA must be scalars or of size SZ."); endif ## Check for class type if (isa (df, 'single') || isa (lambda, 'single')); cls = 'single'; else cls = 'double'; endif ## Return NaNs for out of range values of DF and LAMBDA df(df <= 0) = NaN; lambda(lambda <= 0) = NaN; ## Force DF and LAMBDA into the same size as SZ (if necessary) if (isscalar (df)) df = repmat (df, sz); endif if (isscalar (lambda)) lambda = repmat (lambda, sz); endif ## Generate random sample from noncentral chi-squared distribution r = randp (lambda ./ 2); r(r > 0) = 2 * randg (r(r > 0)); r(df > 0) += 2 * randg (df(df > 0) / 2); ## Cast to appropriate class r = cast (r, cls); endfunction ## Test output %!assert_equal (size (ncx2rnd (1, 1)), [1, 1]) %!assert_equal (size (ncx2rnd (1, ones (2, 1))), [2, 1]) %!assert_equal (size (ncx2rnd (1, ones (2, 2))), [2, 2]) %!assert_equal (size (ncx2rnd (ones (2, 1), 1)), [2, 1]) %!assert_equal (size (ncx2rnd (ones (2, 2), 1)), [2, 2]) %!assert_equal (size (ncx2rnd (1, 1, 3)), [3, 3]) %!assert_equal (size (ncx2rnd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (ncx2rnd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (ncx2rnd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (ncx2rnd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (ncx2rnd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (ncx2rnd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (ncx2rnd (1, 1, [])), [0, 0]) %!assert_equal (size (ncx2rnd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (ncx2rnd (1, 2, -1)), [0, 0]) %!assert_equal (size (ncx2rnd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (ncx2rnd (1, 2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (ncx2rnd (1, 1)), "double") %!assert_equal (class (ncx2rnd (1, single (1))), "single") %!assert_equal (class (ncx2rnd (1, single ([1, 1]))), "single") %!assert_equal (class (ncx2rnd (single (1), 1)), "single") %!assert_equal (class (ncx2rnd (single ([1, 1]), 1)), "single") ## Test input validation %!error ncx2rnd () %!error ncx2rnd (1) %!error ... %! ncx2rnd (ones (3), ones (2)) %!error ... %! ncx2rnd (ones (2), ones (3)) %!error ncx2rnd (i, 2) %!error ncx2rnd (1, i) %!error ... %! ncx2rnd (1, 2, 1.2) %!error ... %! ncx2rnd (1, 2, ones (2)) %!error ... %! ncx2rnd (1, 2, [2 0 2.5]) %!error ... %! ncx2rnd (1, 2, 2, 1.5, 5) %!error ... %! ncx2rnd (2, ones (2), 3) %!error ... %! ncx2rnd (2, ones (2), [3, 2]) %!error ... %! ncx2rnd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/normcdf.m000066400000000000000000000242131527311104300246150ustar00rootroot00000000000000## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} normcdf (@var{x}) ## @deftypefnx {statistics} {@var{p} =} normcdf (@var{x}, @var{mu}) ## @deftypefnx {statistics} {@var{p} =} normcdf (@var{x}, @var{mu}, @var{sigma}) ## @deftypefnx {statistics} {@var{p} =} normcdf (@dots{}, @qcode{'upper'}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} normcdf (@var{x}, @var{mu}, @var{sigma}, @var{pcov}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} normcdf (@var{x}, @var{mu}, @var{sigma}, @var{pcov}, @var{alpha}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} normcdf (@dots{}, @qcode{'upper'}) ## ## Normal cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the normal distribution with mean @var{mu} and standard deviation ## @var{sigma}. The size of @var{p} is the common size of @var{x}, @var{mu} and ## @var{sigma}. A scalar input functions as a constant matrix of the same size ## as the other inputs. ## ## Default values are @var{mu} = 0, @var{sigma} = 1. ## ## When called with three output arguments, i.e. @qcode{[@var{p}, @var{plo}, ## @var{pup}]}, @code{normcdf} computes the confidence bounds for @var{p} when ## the input parameters @var{mu} and @var{sigma} are estimates. In such case, ## @var{pcov}, a @math{2*2} matrix containing the covariance matrix of the ## estimated parameters, is necessary. Optionally, @var{alpha}, which has a ## default value of 0.05, specifies the @qcode{100 * (1 - @var{alpha})} percent ## confidence bounds. @var{plo} and @var{pup} are arrays of the same size as ## @var{p} containing the lower and upper confidence bounds. ## ## @code{[@dots{}] = normcdf (@dots{}, "upper")} computes the upper tail ## probability of the normal distribution with parameters @var{mu} and ## @var{sigma}, at the values in @var{x}. This can be used to compute a ## right-tailed p-value. To compute a two-tailed p-value, use ## @code{2 * normcdf (-abs (@var{x}), @var{mu}, @var{sigma})}. ## ## Further information about the normal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Normal_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{norminv, normpdf, normrnd, normfit, normlike, normstat} ## @end deftypefn function [varargout] = normcdf (x, varargin) ## Check for valid number of input arguments if (nargin < 1 || nargin > 6) error ("normcdf: invalid number of input arguments."); endif ## Check for "upper" flag if (nargin > 1 && strcmpi (varargin{end}, 'upper')) uflag = true; varargin(end) = []; elseif (nargin > 1 && ischar (varargin{end}) && ... ! strcmpi (varargin{end}, 'upper')) error ("normcdf: invalid argument for upper tail."); else uflag = false; endif ## Get extra arguments (if they exist) or add defaults if (numel (varargin) > 0) mu = varargin{1}; else mu = 0; endif if (numel (varargin) > 1) sigma = varargin{2}; else sigma = 1; endif if (numel (varargin) > 2) pcov = varargin{3}; ## Check for valid covariance matrix 2x2 if (! isequal (size (pcov), [2, 2])) error ("normcdf: invalid size of covariance matrix."); endif else ## Check that cov matrix is provided if 3 output arguments are requested if (nargout > 1) error ("normcdf: covariance matrix is required for confidence bounds."); endif pcov = []; endif if (numel (varargin) > 3) alpha = varargin{4}; ## Check for valid alpha value if (! isnumeric (alpha) || numel (alpha) !=1 || alpha <= 0 || alpha >= 1) error ("normcdf: invalid value for alpha."); endif else alpha = 0.05; endif ## Check for common size of X, MU, and SIGMA if (! isscalar (mu) || ! isscalar (sigma)) [err, x, mu, sigma] = common_size (x, mu, sigma); if (err > 0) error ("normcdf: X, MU, and SIGMA must be of common size or scalars."); endif endif ## Check for X, MU, and SIGMA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (sigma))) error ("normcdf: X, MU, and SIGMA must be double or single."); endif ## Check for X, MU, and SIGMA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (sigma)) error ("normcdf: X, MU, and SIGMA must not be complex."); endif ## Compute normal CDF z = (x - mu) ./ sigma; if (uflag) z = -z; endif ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (sigma, 'single')); is_class = 'single'; else is_class = 'double'; endif ## Prepare output p = NaN (size (z), is_class); if (nargout > 1) plo = NaN (size (z), is_class); pup = NaN (size (z), is_class); endif ## Check SIGMA if (isscalar (sigma)) if (sigma > 0) sigma_p = true (size (z)); sigma_z = false (size (z)); elseif (sigma == 0) sigma_z = true (size (z)); sigma_p = false (size (z)); else if (nargout <= 1) varargout{1} = p; elseif (nargout == 3) varargout{1} = p; varargout{2} = plo; varargout{3} = pup; endif return; endif else sigma_p = sigma > 0; sigma_z = sigma == 0; endif ## Set edge cases when SIGMA = 0 if (uflag) p(sigma_z & x < mu) = 1; p(sigma_z & x >= mu) = 0; if (nargout > 1) plo(sigma_z & x < mu) = 1; plo(sigma_z & x >= mu) = 0; pup(sigma_z & x < mu) = 1; pup(sigma_z & x >= mu) = 0; endif else p(sigma_z & x < mu) = 0; p(sigma_z & x >= mu) = 1; if (nargout >= 2) plo(sigma_z & x < mu) = 0; plo(sigma_z & x >= mu) = 1; pup(sigma_z & x < mu) = 0; pup(sigma_z & x >= mu) = 1; endif endif ## Compute cases when SIGMA > 0 p(sigma_p) = 0.5 * erfc (-z(sigma_p) ./ sqrt (2)); varargout{1} = p; ## Compute confidence bounds (if requested) if (nargout >= 2) zvar = (pcov(1,1) + 2 * pcov(1,2) * z(sigma_p) + ... pcov(2,2) * z(sigma_p) .^ 2) ./ (sigma .^ 2); if (any (zvar < 0)) error ("normcdf: bad covariance matrix."); endif normz = -norminv (alpha / 2); halfwidth = normz * sqrt (zvar); zlo = z(sigma_p) - halfwidth; zup = z(sigma_p) + halfwidth; plo(sigma_p) = 0.5 * erfc (-zlo ./ sqrt (2)); pup(sigma_p) = 0.5 * erfc (-zup ./ sqrt (2)); varargout{2} = plo; varargout{3} = pup; endif endfunction %!demo %! ## Plot various CDFs from the normal distribution %! x = -5:0.01:5; %! p1 = normcdf (x, 0, 0.5); %! p2 = normcdf (x, 0, 1); %! p3 = normcdf (x, 0, 2); %! p4 = normcdf (x, -2, 0.8); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', x, p4, '-c') %! grid on %! xlim ([-5, 5]) %! legend ({'μ = 0, σ = 0.5', 'μ = 0, σ = 1', ... %! 'μ = 0, σ = 2', 'μ = -2, σ = 0.8'}, 'location', 'southeast') %! title ('Normal CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, y %! x = [-Inf 1 2 Inf]; %! y = [0, 0.5, 1/2*(1+erf(1/sqrt(2))), 1]; %!assert_equal (normcdf (x, ones (1,4), ones (1,4)), y) %!assert_equal (normcdf (x, 1, ones (1,4)), y) %!assert_equal (normcdf (x, ones (1,4), 1), y) %!assert_equal (normcdf (x, [0, -Inf, NaN, Inf], 1), [0, 1, NaN, NaN]) %!assert_equal (normcdf (x, 1, [Inf, NaN, -1, 0]), [NaN, NaN, NaN, 1]) %!assert_equal (normcdf ([x(1:2), NaN, x(4)], 1, 1), [y(1:2), NaN, y(4)]) %!assert_equal (normcdf (x, 'upper'), [1, 0.1587, 0.0228, 0], 1e-4) ## Test class of input preserved %!assert_equal (normcdf ([x, NaN], 1, 1), [y, NaN]) %!assert_equal (normcdf (single ([x, NaN]), 1, 1), single ([y, NaN]), eps ('single')) %!assert_equal (normcdf ([x, NaN], single (1), 1), single ([y, NaN]), eps ('single')) %!assert_equal (normcdf ([x, NaN], 1, single (1)), single ([y, NaN]), eps ('single')) ## Test input validation %!error normcdf () %!error normcdf (1,2,3,4,5,6,7) %!error normcdf (1, 2, 3, 4, 'uper') %!error ... %! normcdf (ones (3), ones (2), ones (2)) %!error normcdf (2, 3, 4, [1, 2]) %!error ... %! [p, plo, pup] = normcdf (1, 2, 3) %!error [p, plo, pup] = ... %! normcdf (1, 2, 3, [1, 0; 0, 1], 0) %!error [p, plo, pup] = ... %! normcdf (1, 2, 3, [1, 0; 0, 1], 1.22) %!error [p, plo, pup] = ... %! normcdf (1, 2, 3, [1, 0; 0, 1], 'alpha', 'upper') %!error normcdf (int32 (2), 2, 2) %!error normcdf (true, 2, 2) %!error normcdf ('a', 2, 2) %!error normcdf (i, 2, 2) %!error normcdf (2, i, 2) %!error normcdf (2, 2, i) %!error ... %! [p, plo, pup] =normcdf (1, 2, 3, [1, 0; 0, -inf], 0.04) statistics-release-2.1.2/inst/Distribution_Functions/norminv.m000066400000000000000000000137501527311104300246610ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} norminv (@var{p}) ## @deftypefnx {statistics} {@var{x} =} norminv (@var{p}, @var{mu}) ## @deftypefnx {statistics} {@var{x} =} norminv (@var{p}, @var{mu}, @var{sigma}) ## ## Inverse of the normal cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the normal distribution with mean @var{mu} and standard deviation ## @var{sigma}. The size of @var{p} is the common size of @var{p}, @var{mu} and ## @var{sigma}. A scalar input functions as a constant matrix of the same size ## as the other inputs. ## ## Default values are @var{mu} = 0, @var{sigma} = 1. ## ## The default values correspond to the standard normal distribution and ## computing its quantile function is also possible with the @code{probit} ## function, which is faster but it does not perform any input validation. ## ## Further information about the normal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Normal_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{normcdf, normpdf, normrnd, normfit, normlike, normstat, probit} ## @end deftypefn function x = norminv (p, mu, sigma) ## Check for valid number of input arguments if (nargin < 1) error ("norminv: function called with too few input arguments."); endif ## Add defaults (if missing input arguments) if (nargin < 2) mu = 0; endif if (nargin < 3) sigma = 1; endif ## Check for common size of P, MU, and SIGMA if (! isscalar (p) || ! isscalar (mu) || ! isscalar (sigma)) [retval, p, mu, sigma] = common_size (p, mu, sigma); if (retval > 0) error ("norminv: P, MU, and SIGMA must be of common size or scalars."); endif endif ## Check for P, MU, and SIGMA being double or single if (! (isfloat (p) && isfloat (mu) && isfloat (sigma))) error ("norminv: P, MU, and SIGMA must be double or single."); endif ## Check for P, MU, and SIGMA being reals if (iscomplex (p) || iscomplex (mu) || iscomplex (sigma)) error ("norminv: P, MU, and SIGMA must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (mu, 'single') || isa (sigma, 'single')) x = NaN (size (p), 'single'); else x = NaN (size (p)); endif ## Compute normal iCDF if (isscalar (mu) && isscalar (sigma)) if (isfinite (mu) && (sigma > 0) && (sigma < Inf)) x = mu + sigma * (-sqrt (2) * erfcinv (2 * p)); endif else k = isfinite (mu) & (sigma > 0) & (sigma < Inf); x(k) = mu(k) + sigma(k) .* (-sqrt (2) * erfcinv (2 * p(k))); endif endfunction %!demo %! ## Plot various iCDFs from the normal distribution %! p = 0.001:0.001:0.999; %! x1 = norminv (p, 0, 0.5); %! x2 = norminv (p, 0, 1); %! x3 = norminv (p, 0, 2); %! x4 = norminv (p, -2, 0.8); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', p, x4, '-c') %! grid on %! ylim ([-5, 5]) %! legend ({'μ = 0, σ = 0.5', 'μ = 0, σ = 1', ... %! 'μ = 0, σ = 2', 'μ = -2, σ = 0.8'}, 'location', 'northwest') %! title ('Normal iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p %! p = [-1 0 0.5 1 2]; %!assert_equal (norminv (p, ones (1,5), ones (1,5)), [NaN -Inf 1 Inf NaN]) %!assert_equal (norminv (p, 1, ones (1,5)), [NaN -Inf 1 Inf NaN]) %!assert_equal (norminv (p, ones (1,5), 1), [NaN -Inf 1 Inf NaN]) %!assert_equal (norminv (p, [1 -Inf NaN Inf 1], 1), [NaN NaN NaN NaN NaN]) %!assert_equal (norminv (p, 1, [1 0 NaN Inf 1]), [NaN NaN NaN NaN NaN]) %!assert_equal (norminv ([p(1:2) NaN p(4:5)], 1, 1), [NaN -Inf NaN Inf NaN]) %!assert_equal (norminv (p), probit (p)) %!assert_equal (norminv (0.31254), probit (0.31254)) ## Test class of input preserved %!assert_equal (norminv ([p, NaN], 1, 1), [NaN -Inf 1 Inf NaN NaN]) %!assert_equal (norminv (single ([p, NaN]), 1, 1), single ([NaN -Inf 1 Inf NaN NaN])) %!assert_equal (norminv ([p, NaN], single (1), 1), single ([NaN -Inf 1 Inf NaN NaN])) %!assert_equal (norminv ([p, NaN], 1, single (1)), single ([NaN -Inf 1 Inf NaN NaN])) ## Test input validation %!error norminv () %!error ... %! norminv (ones (3), ones (2), ones (2)) %!error ... %! norminv (ones (2), ones (3), ones (2)) %!error ... %! norminv (ones (2), ones (2), ones (3)) %!error norminv (int32 (2), 2, 2) %!error norminv (true, 2, 2) %!error norminv ('a', 2, 2) %!error norminv (i, 2, 2) %!error norminv (2, i, 2) %!error norminv (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/normpdf.m000066400000000000000000000134521527311104300246350ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} normpdf (@var{x}) ## @deftypefnx {statistics} {@var{y} =} normpdf (@var{x}, @var{mu}) ## @deftypefnx {statistics} {@var{y} =} normpdf (@var{x}, @var{mu}, @var{sigma}) ## ## Normal probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the normal distribution with mean @var{mu} and standard deviation ## @var{sigma}. The size of @var{y} is the common size of @var{p}, @var{mu} and ## @var{sigma}. A scalar input functions as a constant matrix of the same size ## as the other inputs. ## ## Default values are @var{mu} = 0, @var{sigma} = 1. ## ## Further information about the normal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Normal_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{normcdf, norminv, normrnd, normfit, normlike, normstat} ## @end deftypefn function y = normpdf (x, mu, sigma) ## Check for valid number of input arguments if (nargin < 1 || nargin > 3) error ("normpdf: function called with too few input arguments."); endif ## Add defaults (if missing input arguments) if (nargin < 2) mu = 0; endif if (nargin < 3) sigma = 1; endif ## Check for common size of X, MU, and SIGMA if (! isscalar (x) || ! isscalar (mu) || ! isscalar (sigma)) [retval, x, mu, sigma] = common_size (x, mu, sigma); if (retval > 0) error ("normpdf: X, MU, and SIGMA must be of common size or scalars."); endif endif ## Check for X, MU, and SIGMA being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (sigma))) error ("normpdf: X, MU, and SIGMA must be double or single."); endif ## Check for X, MU, and SIGMA being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (sigma)) error ("normpdf: X, MU, and SIGMA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (sigma, 'single')) y = zeros (size (x), 'single'); else y = zeros (size (x)); endif ## Compute normal PDF if (isscalar (mu) && isscalar (sigma)) if (isfinite (mu) && (sigma > 0) && (sigma < Inf)) y = stdnormal_pdf ((x - mu) / sigma) / sigma; else y = NaN (size (x), class (y)); endif else k = isinf (mu) | ! (sigma > 0) | ! (sigma < Inf); y(k) = NaN; k = ! isinf (mu) & (sigma > 0) & (sigma < Inf); y(k) = stdnormal_pdf ((x(k) - mu(k)) ./ sigma(k)) ./ sigma(k); endif endfunction function y = stdnormal_pdf (x) y = (2 * pi)^(- 1/2) * exp (- x .^ 2 / 2); endfunction %!demo %! ## Plot various PDFs from the normal distribution %! x = -5:0.01:5; %! y1 = normpdf (x, 0, 0.5); %! y2 = normpdf (x, 0, 1); %! y3 = normpdf (x, 0, 2); %! y4 = normpdf (x, -2, 0.8); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', x, y4, '-c') %! grid on %! xlim ([-5, 5]) %! ylim ([0, 0.9]) %! legend ({'μ = 0, σ = 0.5', 'μ = 0, σ = 1', ... %! 'μ = 0, σ = 2', 'μ = -2, σ = 0.8'}, 'location', 'northeast') %! title ('Normal PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x, y %! x = [-Inf, 1, 2, Inf]; %! y = 1 / sqrt (2 * pi) * exp (-(x - 1) .^ 2 / 2); %!assert_equal (normpdf (x, ones (1,4), ones (1,4)), y, eps) %!assert_equal (normpdf (x, 1, ones (1,4)), y, eps) %!assert_equal (normpdf (x, ones (1,4), 1), y, eps) %!assert_equal (normpdf (x, [0 -Inf NaN Inf], 1), [y(1) NaN NaN NaN], eps) %!assert_equal (normpdf (x, 1, [Inf NaN -1 0]), [NaN NaN NaN NaN], eps) %!assert_equal (normpdf ([x, NaN], 1, 1), [y, NaN], eps) ## Test class of input preserved %!assert_equal (normpdf (single ([x, NaN]), 1, 1), single ([y, NaN]), eps ('single')) %!assert_equal (normpdf ([x, NaN], single (1), 1), single ([y, NaN]), eps ('single')) %!assert_equal (normpdf ([x, NaN], 1, single (1)), single ([y, NaN]), eps ('single')) ## Test input validation %!error normpdf () %!error ... %! normpdf (ones (3), ones (2), ones (2)) %!error ... %! normpdf (ones (2), ones (3), ones (2)) %!error ... %! normpdf (ones (2), ones (2), ones (3)) %!error normpdf (int32 (2), 2, 2) %!error normpdf (true, 2, 2) %!error normpdf ('a', 2, 2) %!error normpdf (i, 2, 2) %!error normpdf (2, i, 2) %!error normpdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/normrnd.m000066400000000000000000000155151527311104300246510ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} normrnd (@var{mu}, @var{sigma}) ## @deftypefnx {statistics} {@var{r} =} normrnd (@var{mu}, @var{sigma}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} normrnd (@var{mu}, @var{sigma}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} normrnd (@var{mu}, @var{sigma}, [@var{sz}]) ## ## Random arrays from the normal distribution. ## ## @code{@var{r} = normrnd (@var{mu}, @var{sigma})} returns an array of random ## numbers chosen from the normal distribution with mean @var{mu} and standard ## deviation @var{sigma}. The size of @var{r} is the common size of @var{mu} ## and @var{sigma}. A scalar input functions as a constant matrix of the same ## size as the other inputs. Both parameters must be finite real numbers and ## @var{sigma} > 0, otherwise NaN is returned. ## ## When called with a single size argument, @code{normrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the normal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Normal_distribution} ## ## @seealso{normcdf, norminv, normpdf, normfit, normlike, normstat} ## @end deftypefn function r = normrnd (mu, sigma, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("normrnd: function called with too few input arguments."); endif ## Check for common size of MU and SIGMA if (! isscalar (mu) || ! isscalar (sigma)) [retval, mu, sigma] = common_size (mu, sigma); if (retval > 0) error ("normrnd: MU and SIGMA must be of common size or scalars."); endif endif ## Check for MU and SIGMA being reals if (iscomplex (mu) || iscomplex (sigma)) error ("normrnd: MU and SIGMA must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (mu); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("normrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("normrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (mu) && ! isequal (size (mu), size (ones (sz)))) error ("normrnd: MU and SIGMA must be scalars or of size SZ."); endif ## Check for class type if (isa (mu, 'single') || isa (sigma, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from normal distribution if (isscalar (mu) && isscalar (sigma)) if (isfinite (mu) && (sigma >= 0) && (sigma < Inf)) r = mu + sigma * randn (sz, cls); else r = NaN (sz, cls); endif else r = mu + sigma .* randn (sz, cls); k = ! isfinite (mu) | ! (sigma >= 0) | ! (sigma < Inf); r(k) = NaN; endif endfunction ## Test output %!assert_equal (size (normrnd (1, 1)), [1, 1]) %!assert_equal (size (normrnd (1, ones (2, 1))), [2, 1]) %!assert_equal (size (normrnd (1, ones (2, 2))), [2, 2]) %!assert_equal (size (normrnd (ones (2, 1), 1)), [2, 1]) %!assert_equal (size (normrnd (ones (2, 2), 1)), [2, 2]) %!assert_equal (size (normrnd (1, 1, 3)), [3, 3]) %!assert_equal (size (normrnd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (normrnd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (normrnd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (normrnd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (normrnd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (normrnd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (normrnd (1, 1, [])), [0, 0]) %!assert_equal (size (normrnd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (normrnd (1, 2, -1)), [0, 0]) %!assert_equal (size (normrnd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (normrnd (1, 2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (normrnd (1, 1)), "double") %!assert_equal (class (normrnd (1, single (1))), "single") %!assert_equal (class (normrnd (1, single ([1, 1]))), "single") %!assert_equal (class (normrnd (single (1), 1)), "single") %!assert_equal (class (normrnd (single ([1, 1]), 1)), "single") ## Test input validation %!error normrnd () %!error normrnd (1) %!error ... %! normrnd (ones (3), ones (2)) %!error ... %! normrnd (ones (2), ones (3)) %!error normrnd (i, 2, 3) %!error normrnd (1, i, 3) %!error ... %! normrnd (1, 2, 1.2) %!error ... %! normrnd (1, 2, ones (2)) %!error ... %! normrnd (1, 2, [2 0 2.5]) %!error ... %! normrnd (1, 2, 2, 1.5, 5) %!error ... %! normrnd (2, ones (2), 3) %!error ... %! normrnd (2, ones (2), [3, 2]) %!error ... %! normrnd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/plcdf.m000066400000000000000000000143361527311104300242620ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} plcdf (@var{data}, @var{x}, @var{Fx}) ## @deftypefnx {statistics} {@var{p} =} plcdf (@var{data}, @var{x}, @var{Fx}, @qcode{'upper'}) ## ## Piecewise linear cumulative distribution function (CDF). ## ## For each element of @var{data}, compute the cumulative distribution function ## (CDF) of the piecewise linear distribution with a vector of @var{x} values at ## which the CDF changes slope and a vector of CDF values @var{Fx} that ## correspond to each value in @var{x}. Both @var{x} and @var{Fx} must be ## vectors of the same size and at least 2-elements long. The size of @var{p} ## is the same as @var{data}. ## ## @code{@var{p} = plcdf (@var{data}, @var{x}, @var{Fx}, "upper")} computes ## the upper tail probability of the piecewise linear distribution with ## parameters @var{x} and @var{Fx}, at the values in @var{data}. ## ## Further information about the piecewise linear distribution can be found at ## @url{https://en.wikipedia.org/wiki/Piecewise_linear_function} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{plinv, plpdf, plrnd, plstat} ## @end deftypefn function p = plcdf (data, x, Fx, uflag) ## Check for valid number of input arguments if (nargin < 3) error ("plcdf: function called with too few input arguments."); endif ## Check for "upper" flag if (nargin == 4 && strcmpi (uflag, 'upper')) uflag = true; elseif (nargin == 4 && ! strcmpi (uflag, 'upper')) error ("plcdf: invalid argument for upper tail."); else uflag = false; endif ## Check for common size of X and FX if (! isvector (x) || ! isvector (Fx) || ! isequal (size (x), size (Fx))) error ("plcdf: X and FX must be vectors of equal size."); endif ## Check for X and FX being at least 2-elements long if (length (x) < 2 || length (Fx) < 2) error ("plcdf: X and FX must be at least two-elements long."); endif ## Check for Fx being bounded in [0, 1] if (any (Fx < 0) || any (Fx > 1)) error ("plcdf: FX must be bounded in the range [0, 1]."); endif ## Check for DATA, X, and FX being double or single if (! (isfloat (data) && isfloat (x) && isfloat (Fx))) error ("plcdf: DATA, X, and FX must be double or single."); endif ## Check for DATA, X, and FX being reals if (iscomplex (data) || iscomplex (x) || iscomplex (Fx)) error ("plcdf: DATA, X, and FX must not be complex."); endif ## Check for class type if (isa (data, 'single') || isa (x, 'single') || isa (Fx, 'single')); p = zeros (size (data), 'single'); else p = zeros (size (data)); endif ## Find data within supported range support = (data >= x(1) & data <= x(end)); p(support) = interp1 (x, Fx, data(support), 'linear'); ## Force right side outside support to 1 and invalid data to NaN p(data > x(end)) = 1; p(isnan (data)) = NaN; ## Return upper tail (if requested) if (uflag) p = 1 - p; endif endfunction %!demo %! ## Plot various CDFs from the Piecewise linear distribution %! data = 0:0.01:10; %! x1 = [0, 1, 3, 4, 7, 10]; %! Fx1 = [0, 0.2, 0.5, 0.6, 0.7, 1]; %! x2 = [0, 2, 5, 6, 7, 8]; %! Fx2 = [0, 0.1, 0.3, 0.6, 0.9, 1]; %! p1 = plcdf (data, x1, Fx1); %! p2 = plcdf (data, x2, Fx2); %! plot (data, p1, '-b', data, p2, 'g') %! grid on %! ylim ([0, 1]) %! xlim ([0, 10]) %! legend ({'x1, Fx1', 'x2, Fx2'}, 'location', 'southeast') %! title ('Piecewise linear CDF') %! xlabel ('values in data') %! ylabel ('probability') ## Test output %!test %! data = 0:0.2:1; %! p = plcdf (data, [0, 1], [0, 1]); %! assert_equal (p, data); %!test %! data = 0:0.2:1; %! p = plcdf (data, [0, 2], [0, 1]); %! assert_equal (p, 0.5 * data); %!test %! data = 0:0.2:1; %! p = plcdf (data, [0, 1], [0, 0.5]); %! assert_equal (p, 0.5 * data); %!test %! data = 0:0.2:1; %! p = plcdf (data, [0, 0.5], [0, 1]); %! assert_equal (p, [0, 0.4, 0.8, 1, 1, 1]); %!test %! data = 0:0.2:1; %! p = plcdf (data, [0, 1], [0, 1], 'upper'); %! assert_equal (p, 1 - data); ## Test input validation %!error plcdf () %!error plcdf (1) %!error plcdf (1, 2) %!error plcdf (1, 2, 3, 'uper') %!error plcdf (1, 2, 3, 4) %!error ... %! plcdf (1, [0, 1, 2], [0, 1]) %!error ... %! plcdf (1, [0], [1]) %!error ... %! plcdf (1, [0, 1, 2], [0, 1, 1.5]) %!error ... %! plcdf (1, [0, 1, 2], [0, i, 1]) %!error ... %! plcdf (int32 (2), [0, 1, 2], [0, 0.5, 1]) %!error ... %! plcdf (true, [0, 1, 2], [0, 0.5, 1]) %!error ... %! plcdf ('a', [0, 1, 2], [0, 0.5, 1]) %!error ... %! plcdf (i, [0, 1, 2], [0, 0.5, 1]) %!error ... %! plcdf (1, [0, i, 2], [0, 0.5, 1]) %!error ... %! plcdf (1, [0, 1, 2], [0, 0.5i, 1]) statistics-release-2.1.2/inst/Distribution_Functions/plinv.m000066400000000000000000000132031527311104300243120ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{data} =} plinv (@var{p}, @var{x}, @var{Fx}) ## ## Inverse of the piecewise linear distribution (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) ## of the piecewise linear distribution with a vector of @var{x} values at ## which the CDF changes slope and a vector of CDF values @var{Fx} that ## correspond to each value in @var{x}. Both @var{x} and @var{Fx} must be ## vectors of the same_p size and at least 2-elements long.. The size of ## @var{data} is the same_p as @var{p}. ## ## Further information about the piecewise linear distribution can be found at ## @url{https://en.wikipedia.org/wiki/Piecewise_linear_function} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{plcdf, plpdf, plrnd, plstat} ## @end deftypefn function data = plinv (p, x, Fx) ## Check for valid number of input arguments if (nargin < 3) error ("plinv: function called with too few input arguments."); endif ## Check for common size of X and FX if (! isvector (x) || ! isvector (Fx) || ! isequal (size (x), size (Fx))) error ("plinv: X and FX must be vectors of equal size."); endif ## Check for X and FX being at least 2-elements long if (length (x) < 2 || length (Fx) < 2) error ("plinv: X and FX must be at least two-elements long."); endif ## Check for Fx being bounded in [0, 1] if (any (Fx < 0) || any (Fx > 1)) error ("plinv: FX must be bounded in the range [0, 1]."); endif ## Check for P, X, and FX being double or single if (! (isfloat (p) && isfloat (x) && isfloat (Fx))) error ("plinv: P, X, and FX must be double or single."); endif ## Check for P, X, and FX being reals if (iscomplex (p) || iscomplex (x) || iscomplex (Fx)) error ("plinv: P, X, and FX must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (x, 'single') || isa (Fx, 'single')); data = zeros (size (p), 'single'); else data = zeros (size (p)); endif ## Remove consecutive bins with almost zero probability pw_diff = diff (Fx); if any (pw_diff==0) zero_p = 2 * eps (Fx); same_p = pw_diff <= zero_p(1:end-1); remove = same_p(1:end-1) & same_p(2:end); while (any (remove)) idx = find (remove); same_p(idx) = []; Fx(idx+1) = []; x(idx+1) = []; pw_diff = diff (Fx); remove = same_p(1:end-1) & same_p(2:end); endwhile idx = find (pw_diff==0); Fx(idx+1) = Fx(idx) + eps (Fx(idx)); endif p(p < 0 | 1 < p) = NaN; data = interp1 (Fx, x, p, 'linear'); endfunction %!demo %! ## Plot various iCDFs from the Piecewise linear distribution %! p = 0.001:0.001:0.999; %! x1 = [0, 1, 3, 4, 7, 10]; %! Fx1 = [0, 0.2, 0.5, 0.6, 0.7, 1]; %! x2 = [0, 2, 5, 6, 7, 8]; %! Fx2 = [0, 0.1, 0.3, 0.6, 0.9, 1]; %! data1 = plinv (p, x1, Fx1); %! data2 = plinv (p, x2, Fx2); %! plot (p, data1, '-b', p, data2, '-g') %! grid on %! legend ({'x1, Fx1', 'x2, Fx2'}, 'location', 'northwest') %! title ('Piecewise linear iCDF') %! xlabel ('probability') %! ylabel ('values in data') ## Test output %!test %! p = 0:0.2:1; %! data = plinv (p, [0, 1], [0, 1]); %! assert_equal (data, p); %!test %! p = 0:0.2:1; %! data = plinv (p, [0, 2], [0, 1]); %! assert_equal (data, 2 * p); %!test %! p = 0:0.2:1; %! data_out = 1:6; %! data = plinv (p, [0, 1], [0, 0.5]); %! assert_equal (data, [0, 0.4, 0.8, NA, NA, NA]); %!test %! p = 0:0.2:1; %! data_out = 1:6; %! data = plinv (p, [0, 0.5], [0, 1]); %! assert_equal (data, [0:0.1:0.5]); ## Test input validation %!error plinv () %!error plinv (1) %!error plinv (1, 2) %!error ... %! plinv (1, [0, 1, 2], [0, 1]) %!error ... %! plinv (1, [0], [1]) %!error ... %! plinv (1, [0, 1, 2], [0, 1, 1.5]) %!error ... %! plinv (1, [0, 1, 2], [0, i, 1]) %!error ... %! plinv (int32 (2), [0, 1, 2], [0, 0.5, 1]) %!error ... %! plinv (true, [0, 1, 2], [0, 0.5, 1]) %!error ... %! plinv ('a', [0, 1, 2], [0, 0.5, 1]) %!error ... %! plinv (i, [0, 1, 2], [0, 0.5, 1]) %!error ... %! plinv (1, [0, i, 2], [0, 0.5, 1]) %!error ... %! plinv (1, [0, 1, 2], [0, 0.5i, 1]) statistics-release-2.1.2/inst/Distribution_Functions/plpdf.m000066400000000000000000000131531527311104300242730ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} plpdf (@var{data}, @var{x}, @var{Fx}) ## ## Piecewise linear probability density function (PDF). ## ## For each element of @var{data}, compute the probability density function ## (PDF) of the piecewise linear distribution with a vector of @var{x} values at ## which the CDF changes slope and a vector of CDF values @var{Fx} that ## correspond to each value in @var{x}. Both @var{x} and @var{Fx} must be ## vectors of the same size and at least 2-elements long. The size of @var{p} ## is the same as @var{data}. ## ## Further information about the piecewise linear distribution can be found at ## @url{https://en.wikipedia.org/wiki/Piecewise_linear_function} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## MATLAB also accepts integer input here, returning the result in the integer ## class of the input; Octave rejects it, as it does for every other continuous ## distribution. ## ## @seealso{plcdf, plinv, plrnd, plstat} ## @end deftypefn function y = plpdf (data, x, Fx) ## Check for valid number of input arguments if (nargin < 3) error ("plpdf: function called with too few input arguments."); endif ## Check for common size of X and FX if (! isvector (x) || ! isvector (Fx) || ! isequal (size (x), size (Fx))) error ("plpdf: X and FX must be vectors of equal size."); endif ## Check for X and FX being at least 2-elements long if (length (x) < 2 || length (Fx) < 2) error ("plpdf: X and FX must be at least two-elements long."); endif ## Check for Fx being bounded in [0, 1] if (any (Fx < 0) || any (Fx > 1)) error ("plpdf: FX must be bounded in the range [0, 1]."); endif ## Check for DATA, X, and FX being double or single if (! (isfloat (data) && isfloat (x) && isfloat (Fx))) error ("plpdf: DATA, X, and FX must be double or single."); endif ## Check for DATA, X, and FX being reals if (iscomplex (data) || iscomplex (x) || iscomplex (Fx)) error ("plpdf: DATA, X, and FX must not be complex."); endif ## Force DATA, X, and FX into row vectors data = data(:)'; x = x(:)'; Fx = Fx(:)'; ## Check for class type if (isa (data, 'single') || isa (x, 'single') || isa (Fx, 'single')); y = zeros (size (data), 'single'); else y = zeros (size (data)); endif ## Bin data according to X [~, bin] = histc (data, [-Inf, x, Inf]); ## Compute piecewise densities dense = diff (Fx) ./ diff (x); bin_d = [0, dense, 0]; ## Fix densities xlen = length (x); bin(bin > xlen) = xlen + 1; y(bin>0) = bin_d(bin(bin>0)); ## Force invalid data to NaN y(isnan (data)) = NaN; endfunction %!demo %! ## Plot various PDFs from the Piecewise linear distribution %! data = 0:0.01:10; %! x1 = [0, 1, 3, 4, 7, 10]; %! Fx1 = [0, 0.2, 0.5, 0.6, 0.7, 1]; %! x2 = [0, 2, 5, 6, 7, 8]; %! Fx2 = [0, 0.1, 0.3, 0.6, 0.9, 1]; %! y1 = plpdf (data, x1, Fx1); %! y2 = plpdf (data, x2, Fx2); %! plot (data, y1, '-b', data, y2, 'g') %! grid on %! ylim ([0, 0.6]) %! xlim ([0, 10]) %! legend ({'x1, Fx1', 'x2, Fx2'}, 'location', 'northeast') %! title ('Piecewise linear CDF') %! xlabel ('values in data') %! ylabel ('density') ## Test output %!shared x, Fx %! x = [0, 1, 3, 4, 7, 10]; %! Fx = [0, 0.2, 0.5, 0.6, 0.7, 1]; %!assert_equal (plpdf (0.5, x, Fx), 0.2, eps); %!assert_equal (plpdf (1.5, x, Fx), 0.15, eps); %!assert_equal (plpdf (3.5, x, Fx), 0.1, eps); %!assert_equal (plpdf (5, x, Fx), 0.1/3, eps); %!assert_equal (plpdf (8, x, Fx), 0.1, eps); ## Test input validation %!error plpdf () %!error plpdf (1) %!error plpdf (1, 2) %!error ... %! plpdf (1, [0, 1, 2], [0, 1]) %!error ... %! plpdf (1, [0], [1]) %!error ... %! plpdf (1, [0, 1, 2], [0, 1, 1.5]) %!error ... %! plpdf (1, [0, 1, 2], [0, i, 1]) %!error ... %! plpdf (int32 (2), [0, 1, 2], [0, 0.5, 1]) %!error ... %! plpdf (true, [0, 1, 2], [0, 0.5, 1]) %!error ... %! plpdf ('a', [0, 1, 2], [0, 0.5, 1]) %!error ... %! plpdf (i, [0, 1, 2], [0, 0.5, 1]) %!error ... %! plpdf (1, [0, i, 2], [0, 0.5, 1]) %!error ... %! plpdf (1, [0, 1, 2], [0, 0.5i, 1]) statistics-release-2.1.2/inst/Distribution_Functions/plrnd.m000066400000000000000000000142501527311104300243040ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} plrnd (@var{x}, @var{Fx}) ## @deftypefnx {statistics} {@var{r} =} plrnd (@var{x}, @var{Fx}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} plrnd (@var{x}, @var{Fx}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} plrnd (@var{x}, @var{Fx}, [@var{sz}]) ## ## Random arrays from the piecewise linear distribution. ## ## @code{@var{r} = plrnd (@var{x}, @var{Fx})} returns a random number chosen ## from the piecewise linear distribution with a vector of @var{x} values at ## which the CDF changes slope and a vector of CDF values @var{Fx} that ## correspond to each value in @var{x}. Both @var{x} and @var{Fx} must be ## vectors of the same size and at least 2-elements long. ## ## When called with a single size argument, @code{plrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the piecewise linear distribution can be found at ## @url{https://en.wikipedia.org/wiki/Piecewise_linear_function} ## ## @seealso{plcdf, plinv, plpdf, plstat} ## @end deftypefn function r = plrnd (x, Fx, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("plrnd: function called with too few input arguments."); endif ## Check for common size of X and FX if (! isvector (x) || ! isvector (Fx) || ! isequal (size (x), size (Fx))) error ("plrnd: X and FX must be vectors of equal size."); endif ## Check for X and FX being at least 2-elements long if (length (x) < 2 || length (Fx) < 2) error ("plrnd: X and FX must be at least two-elements long."); endif ## Check for Fx being bounded in [0, 1] if (any (Fx < 0) || any (Fx > 1)) error ("plrnd: FX must be bounded in the range [0, 1]."); endif ## Check for X and FX being reals if (iscomplex (x) || iscomplex (Fx)) error ("plrnd: X and FX must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = 1; elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("plrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("plrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Force X and FX into row vectors x = x(:)'; Fx = Fx(:)'; ## Check for class type if (isa (x, 'single') || isa (Fx, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from the piecewise linear distribution u = rand (sz); r = zeros (sz); [~, bin] = histc (u(:)', Fx); r0 = x(bin); dx = diff (x); dF = diff (Fx); dr = (u(:)' - Fx(bin)) .* dx(bin) ./ dF(bin); r(:) = r0 + dr; ## Cast to appropriate class r = cast (r, cls); endfunction ## Test output %!shared x, Fx %! x = [0, 1, 3, 4, 7, 10]; %! Fx = [0, 0.2, 0.5, 0.6, 0.7, 1]; %!assert_equal (size (plrnd (x, Fx)), [1, 1]) %!assert_equal (size (plrnd (x, Fx, 3)), [3, 3]) %!assert_equal (size (plrnd (x, Fx, [4, 1])), [4, 1]) %!assert_equal (size (plrnd (x, Fx, 4, 1)), [4, 1]) %!assert_equal (size (plrnd (x, Fx, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (plrnd (x, Fx, 0, 1)), [0, 1]) %!assert_equal (size (plrnd (x, Fx, 1, 0)), [1, 0]) %!assert_equal (size (plrnd (x, Fx, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (plrnd (x, Fx, [])), [0, 0]) %!assert_equal (size (plrnd (x, Fx, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (plrnd (x, Fx, -1)), [0, 0]) %!assert_equal (size (plrnd (x, Fx, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (plrnd (x, Fx, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (plrnd (x, Fx)), "double") %!assert_equal (class (plrnd (x, single (Fx))), "single") %!assert_equal (class (plrnd (single (x), Fx)), "single") ## Test input validation %!error plrnd () %!error plrnd (1) %!error ... %! plrnd ([0, 1, 2], [0, 1]) %!error ... %! plrnd ([0], [1]) %!error ... %! plrnd ([0, 1, 2], [0, 1, 1.5]) %!error ... %! plrnd ([0, 1, 2], [0, i, 1]) %!error ... %! plrnd ([0, i, 2], [0, 0.5, 1]) %!error ... %! plrnd ([0, i, 2], [0, 0.5i, 1]) %!error ... %! plrnd (x, Fx, 1.2) %!error ... %! plrnd (x, Fx, ones (2)) %!error ... %! plrnd (x, Fx, [2 0 2.5]) %!error ... %! plrnd (x, Fx, 2, 1.5, 5) statistics-release-2.1.2/inst/Distribution_Functions/poisscdf.m000066400000000000000000000143761527311104300250100ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} poisscdf (@var{x}, @var{lambda}) ## @deftypefnx {statistics} {@var{p} =} poisscdf (@var{x}, @var{lambda}, @qcode{'upper'}) ## ## Poisson cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the Poisson distribution with rate parameter @var{lambda}. The ## size of @var{p} is the common size of @var{x} and @var{lambda}. A scalar ## input functions as a constant matrix of the same size as the other inputs. ## ## @code{@var{p} = poisscdf (@var{x}, @var{lambda}, "upper")} computes the ## upper tail probability of the Poisson distribution with parameter ## @var{lambda}, at the values in @var{x}. ## ## Further information about the Poisson distribution can be found at ## @url{https://en.wikipedia.org/wiki/Poisson_distribution} ## ## Input arguments must be @qcode{double}, @qcode{single}, or an integer type; ## logical and character arrays are rejected. Integer input is promoted to ## @qcode{double}, so the result is always a probability. MATLAB is ## inconsistent here: for several of the discrete distributions it returns the ## result in the integer class of the input, truncating a probability to ## @math{0} or @math{1}. ## ## @seealso{poissinv, poisspdf, poissrnd, poissfit, poisslike, poisstat} ## @end deftypefn function p = poisscdf (x, lambda, uflag) ## Check for valid number of input arguments if (nargin < 2) error ("poisscdf: function called with too few input arguments."); endif ## Check for "upper" flag if (nargin == 3 && strcmpi (uflag, 'upper')) uflag = true; elseif (nargin == 3 && ! strcmpi (uflag, 'upper')) error ("poisscdf: invalid argument for upper tail."); else uflag = false; endif ## Check for common size of X and LAMBDA if (! isscalar (x) || ! isscalar (lambda)) [retval, x, lambda] = common_size (x, lambda); if (retval > 0) error ("poisscdf: X and LAMBDA must be of common size or scalars."); endif endif ## Check for X and LAMBDA being double, single, or integer if (! (isnumeric (x) && isnumeric (lambda))) error ("poisscdf: X and LAMBDA must be double, single, or integer."); endif ## Integer input is promoted to double, so the result is a probability ## rather than a value truncated to the input's integer type. if (isinteger (x)) x = double (x); endif if (isinteger (lambda)) lambda = double (lambda); endif ## Check for X and LAMBDA being reals if (iscomplex (x) || iscomplex (lambda)) error ("poisscdf: X and LAMBDA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (lambda, 'single')) p = zeros (size (x), 'single'); else p = zeros (size (x)); endif ## Force NaN for out of range parameters or missing data NaN is_nan = isnan (x) | isnan (lambda) | (lambda < 0) ... | (isinf (x) & isinf (lambda)); p(is_nan) = NaN; ## Compute P for X >= 0 x = floor (x); k = x >= 0 & ! is_nan & isfinite (lambda); ## Return 1 for positive infinite values of X, unless "upper" is given: p = 0 k1 = isinf (x) & lambda > 0 & isfinite (lambda); if (any (k1)) if (uflag) p(k1) = 0; else p(k1) = 1; endif endif ## Return 1 when X < 0 and "upper" is given k1 = x < 0 & lambda > 0 & isfinite (lambda); if (any (k1)) if (uflag) p(k1) = 1; endif endif ## Compute Poisson CDF for remaining cases x = x(k); lambda = lambda(k); if (uflag) p(k) = gammainc (lambda, x + 1); else p(k) = gammainc (lambda, x + 1, 'upper'); endif endfunction %!demo %! ## Plot various CDFs from the Poisson distribution %! x = 0:20; %! p1 = poisscdf (x, 1); %! p2 = poisscdf (x, 4); %! p3 = poisscdf (x, 10); %! plot (x, p1, '*b', x, p2, '*g', x, p3, '*r') %! grid on %! ylim ([0, 1]) %! legend ({'λ = 1', 'λ = 4', 'λ = 10'}, 'location', 'southeast') %! title ('Poisson CDF') %! xlabel ('values in x (number of occurrences)') %! ylabel ('probability') ## Test output %!shared x, y %! x = [-1 0 1 2 Inf]; %! y = [0, gammainc(1, (x(2:4) +1), 'upper'), 1]; %!assert_equal (poisscdf (x, ones (1,5)), y) %!assert_equal (poisscdf (x, 1), y) %!assert_equal (poisscdf (x, [1 0 NaN 1 1]), [y(1) 1 NaN y(4:5)]) %!assert_equal (poisscdf ([x(1:2) NaN Inf x(5)], 1), [y(1:2) NaN 1 y(5)]) ## Test class of input preserved %!assert_equal (poisscdf ([x, NaN], 1), [y, NaN]) %!assert_equal (poisscdf (single ([x, NaN]), 1), single ([y, NaN]), eps ('single')) %!assert_equal (poisscdf ([x, NaN], single (1)), single ([y, NaN]), eps ('single')) ## Test input validation %!error poisscdf () %!error poisscdf (1) %!error poisscdf (1, 2, 3) %!error poisscdf (1, 2, 'tail') %!error ... %! poisscdf (ones (3), ones (2)) %!error ... %! poisscdf (ones (2), ones (3)) %!error poisscdf (true, 2) %!error poisscdf ('a', 2) %!assert_equal (class (poisscdf (int32 (2), 2)), 'double') %!error poisscdf (i, 2) %!error poisscdf (2, i) statistics-release-2.1.2/inst/Distribution_Functions/poissinv.m000066400000000000000000000201611527311104300250350ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 2014 Mike Giles ## Copyright (C) 2016 Lachlan Andrew ## Copyright (C) 1995-2017 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} poissinv (@var{p}, @var{lambda}) ## ## Inverse of the Poisson cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the Poisson distribution with rate parameter @var{lambda}. The size of ## @var{x} is the common size of @var{p} and @var{lambda}. A scalar input ## functions as a constant matrix of the same size as the other inputs. ## ## Further information about the Poisson distribution can be found at ## @url{https://en.wikipedia.org/wiki/Poisson_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{poisscdf, poisspdf, poissrnd, poissfit, poisslike, poisstat} ## @end deftypefn function x = poissinv (p, lambda) ## Check for valid number of input arguments if (nargin < 2) error ("poissinv: function called with too few input arguments."); endif ## Check for common size of P and LAMBDA if (! isscalar (p) || ! isscalar (lambda)) [retval, p, lambda] = common_size (p, lambda); if (retval > 0) error ("poissinv: P and LAMBDA must be of common size or scalars."); endif endif ## Check for P and LAMBDA being double or single if (! (isfloat (p) && isfloat (lambda))) error ("poissinv: P and LAMBDA must be double or single."); endif ## Check for P and LAMBDA being reals if (iscomplex (p) || iscomplex (lambda)) error ("poissinv: P and LAMBDA must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (lambda, 'single')) x = zeros (size (p), 'single'); else x = zeros (size (p)); endif ## Force NaN for out of range parameters or p-values k = (p < 0) | (p > 1) | isnan (p) | ! (lambda > 0); x(k) = NaN; k = (p == 1) & (lambda > 0); x(k) = Inf; k = (p > 0) & (p < 1) & (lambda > 0); if (any (k(:))) limit = 20; # After 'limit' iterations, use approx if (isscalar (lambda)) cdf = [(cumsum (poisspdf (0:limit-1,lambda))), 2]; y = p(:); # force to column r = bsxfun (@le, y(k), cdf); [~, x(k)] = max (r, [], 2); # find first instance of p <= cdf x(k) -= 1; else kk = find (k); cdf = exp (-lambda(kk)); for i = 1:limit m = find (cdf < p(kk)); if (isempty (m)) break; else x(kk(m)) += 1; cdf(m) += poisspdf (i, lambda(kk(m))); endif endfor endif ## Use Mike Giles's magic when x isn't < limit k &= (x == limit); if (any (k(:))) if (isscalar (lambda)) lam = repmat (lambda, size (p)); else lam = lambda; endif x(k) = analytic_approx (p(k), lam(k)); endif endif endfunction ## The following is based on Mike Giles's CUDA implementation, ## [http://people.maths.ox.ac.uk/gilesm/codes/poissinv/poissinv_cuda.h] ## which is copyright by the University of Oxford ## and is provided under the terms of the GNU GPLv3 license: ## http://www.gnu.org/licenses/gpl.html function x = analytic_approx (p, lambda) s = norminv (p, 0, 1) ./ sqrt (lambda); k = (s > -0.6833501) & (s < 1.777993); ## use polynomial approximations in central region if (any (k)) lam = lambda(k); if (isscalar (s)) sk = s; else sk = s(k); endif ## polynomial approximation to f^{-1}(s) - 1 rm = 2.82298751e-07; rm = -2.58136133e-06 + rm.*sk; rm = 1.02118025e-05 + rm.*sk; rm = -2.37996199e-05 + rm.*sk; rm = 4.05347462e-05 + rm.*sk; rm = -6.63730967e-05 + rm.*sk; rm = 0.000124762566 + rm.*sk; rm = -0.000256970731 + rm.*sk; rm = 0.000558953132 + rm.*sk; rm = -0.00133129194 + rm.*sk; rm = 0.00370367937 + rm.*sk; rm = -0.0138888706 + rm.*sk; rm = 0.166666667 + rm.*sk; rm = sk + sk.*(rm.*sk); ## polynomial approximation to correction c0(r) t = 1.86386867e-05; t = -0.000207319499 + t.*rm; t = 0.0009689451 + t.*rm; t = -0.00247340054 + t.*rm; t = 0.00379952985 + t.*rm; t = -0.00386717047 + t.*rm; t = 0.00346960934 + t.*rm; t = -0.00414125511 + t.*rm; t = 0.00586752093 + t.*rm; t = -0.00838583787 + t.*rm; t = 0.0132793933 + t.*rm; t = -0.027775536 + t.*rm; t = 0.333333333 + t.*rm; ## O(1/lam) correction y = -0.00014585224; y = 0.00146121529 + y.*rm; y = -0.00610328845 + y.*rm; y = 0.0138117964 + y.*rm; y = -0.0186988746 + y.*rm; y = 0.0168155118 + y.*rm; y = -0.013394797 + y.*rm; y = 0.0135698573 + y.*rm; y = -0.0155377333 + y.*rm; y = 0.0174065334 + y.*rm; y = -0.0198011178 + y.*rm; y ./= lam; x(k) = floor (lam + (y+t)+lam.*rm); endif k = ! k & (s > -sqrt (2)); if (any (k)) ## Newton iteration r = 1 + s(k); r2 = r + 1; while (any (abs (r - r2) > 1e-5)) t = log (r); r2 = r; s2 = sqrt (2 * ((1-r) + r.*t)); s2(r<1) *= -1; r = r2 - (s2 - s(k)) .* s2 ./ t; if (r < 0.1 * r2) r = 0.1 * r2; endif endwhile t = log (r); y = lambda(k) .* r + log (sqrt (2*r.*((1-r) + r.*t)) ./ abs (r-1)) ./ t; x(k) = floor (y - 0.0218 ./ (y + 0.065 * lambda(k))); endif endfunction %!demo %! ## Plot various iCDFs from the Poisson distribution %! p = 0.001:0.001:0.999; %! x1 = poissinv (p, 13); %! x2 = poissinv (p, 4); %! x3 = poissinv (p, 10); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r') %! grid on %! ylim ([0, 20]) %! legend ({'λ = 1', 'λ = 4', 'λ = 10'}, 'location', 'northwest') %! title ('Poisson iCDF') %! xlabel ('probability') %! ylabel ('values in x (number of occurrences)') ## Test output %!shared p %! p = [-1 0 0.5 1 2]; %!assert_equal (poissinv (p, ones (1,5)), [NaN 0 1 Inf NaN]) %!assert_equal (poissinv (p, 1), [NaN 0 1 Inf NaN]) %!assert_equal (poissinv (p, [1 0 NaN 1 1]), [NaN NaN NaN Inf NaN]) %!assert_equal (poissinv ([p(1:2) NaN p(4:5)], 1), [NaN 0 NaN Inf NaN]) ## Test class of input preserved %!assert_equal (poissinv ([p, NaN], 1), [NaN 0 1 Inf NaN NaN]) %!assert_equal (poissinv (single ([p, NaN]), 1), single ([NaN 0 1 Inf NaN NaN])) %!assert_equal (poissinv ([p, NaN], single (1)), single ([NaN 0 1 Inf NaN NaN])) ## Test input validation %!error poissinv () %!error poissinv (1) %!error ... %! poissinv (ones (3), ones (2)) %!error ... %! poissinv (ones (2), ones (3)) %!error poissinv (int32 (2), 2) %!error poissinv (true, 2) %!error poissinv ('a', 2) %!error poissinv (i, 2) %!error poissinv (2, i) statistics-release-2.1.2/inst/Distribution_Functions/poisspdf.m000066400000000000000000000125571527311104300250240ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} poisspdf (@var{x}, @var{lambda}) ## ## Poisson probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the Poisson distribution with rate parameter @var{lambda}. The size of ## @var{y} is the common size of @var{x} and @var{lambda}. A scalar input ## functions as a constant matrix of the same size as the other inputs. ## ## Further information about the Poisson distribution can be found at ## @url{https://en.wikipedia.org/wiki/Poisson_distribution} ## ## Input arguments must be @qcode{double}, @qcode{single}, or an integer type; ## logical and character arrays are rejected. Integer input is promoted to ## @qcode{double}, so the result is always a probability. MATLAB is ## inconsistent here: for several of the discrete distributions it returns the ## result in the integer class of the input, truncating a probability to ## @math{0} or @math{1}. ## ## ## The density at an infinite abscissa is @math{0}, no proper distribution ## placing mass there. MATLAB returns @qcode{NaN} here, as it does for ## @code{raylpdf} and for no other density, which is an inconsistency there ## rather than a convention: it returns @math{0} at @math{Inf} for every other ## distribution of the same support. ## @seealso{poisscdf, poissinv, poissrnd, poissfit, poisslike, poisstat} ## @end deftypefn function y = poisspdf (x, lambda) ## Check for valid number of input arguments if (nargin < 2) error ("poisspdf: function called with too few input arguments."); endif ## Check for common size of X and LAMBDA if (! isscalar (x) || ! isscalar (lambda)) [retval, x, lambda] = common_size (x, lambda); if (retval > 0) error ("poisspdf: X and LAMBDA must be of common size or scalars."); endif endif ## Check for X and LAMBDA being double, single, or integer if (! (isnumeric (x) && isnumeric (lambda))) error ("poisspdf: X and LAMBDA must be double, single, or integer."); endif ## Integer input is promoted to double, so the result is a probability ## rather than a value truncated to the input's integer type. if (isinteger (x)) x = double (x); endif if (isinteger (lambda)) lambda = double (lambda); endif ## Check for X and LAMBDA being reals if (iscomplex (x) || iscomplex (lambda)) error ("poisspdf: X and LAMBDA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (lambda, 'single')) y = zeros (size (x), 'single'); else y = zeros (size (x)); endif ## Force NaN for out of range parameters or missing data NaN k = isnan (x) | ! (lambda > 0); y(k) = NaN; k = (x >= 0) & (x < Inf) & (x == fix (x)) & (lambda > 0); if (isscalar (lambda)) y(k) = exp (x(k) * log (lambda) - lambda - gammaln (x(k) + 1)); else y(k) = exp (x(k) .* log (lambda(k)) - lambda(k) - gammaln (x(k) + 1)); endif endfunction %!demo %! ## Plot various PDFs from the Poisson distribution %! x = 0:20; %! y1 = poisspdf (x, 1); %! y2 = poisspdf (x, 4); %! y3 = poisspdf (x, 10); %! plot (x, y1, '*b', x, y2, '*g', x, y3, '*r') %! grid on %! ylim ([0, 0.4]) %! legend ({'λ = 1', 'λ = 4', 'λ = 10'}, 'location', 'northeast') %! title ('Poisson PDF') %! xlabel ('values in x (number of occurrences)') %! ylabel ('density') ## Test output %!shared x, y %! x = [-1 0 1 2 Inf]; %! y = [0, exp(-1)*[1 1 0.5], 0]; %!assert_equal (poisspdf (x, ones (1,5)), y, eps) %!assert_equal (poisspdf (x, 1), y, eps) %!assert_equal (poisspdf (x, [1 0 NaN 1 1]), [y(1) NaN NaN y(4:5)], eps) %!assert_equal (poisspdf ([x, NaN], 1), [y, NaN], eps) ## Test class of input preserved %!assert_equal (poisspdf (single ([x, NaN]), 1), single ([y, NaN]), eps ('single')) %!assert_equal (poisspdf ([x, NaN], single (1)), single ([y, NaN]), eps ('single')) ## Test input validation %!error poisspdf () %!error poisspdf (1) %!error ... %! poisspdf (ones (3), ones (2)) %!error ... %! poisspdf (ones (2), ones (3)) %!error poisspdf (true, 2) %!error poisspdf ('a', 2) %!assert_equal (class (poisspdf (int32 (2), 2)), 'double') %!error poisspdf (i, 2) %!error poisspdf (2, i) statistics-release-2.1.2/inst/Distribution_Functions/poissrnd.m000066400000000000000000000140211527311104300250220ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} poissrnd (@var{lambda}) ## @deftypefnx {statistics} {@var{r} =} poissrnd (@var{lambda}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} poissrnd (@var{lambda}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} poissrnd (@var{lambda}, [@var{sz}]) ## ## Random arrays from the Poisson distribution. ## ## @code{@var{r} = normrnd (@var{lambda})} returns an array of random numbers ## chosen from the Poisson distribution with rate parameter @var{lambda}. The ## size of @var{r} is the common size of @var{lambda}. A scalar input functions ## as a constant matrix of the same size as the other inputs. @var{lambda} must ## be a finite real number and greater or equal to 0, otherwise @qcode{NaN} is ## returned. ## ## When called with a single size argument, @code{poissrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the Poisson distribution can be found at ## @url{https://en.wikipedia.org/wiki/Poisson_distribution} ## ## @seealso{poisscdf, poissinv, poisspdf, poissfit, poisslike, poisstat} ## @end deftypefn function r = poissrnd (lambda, varargin) ## Check for valid number of input arguments if (nargin < 1) error ("poissrnd: function called with too few input arguments."); endif ## Check for LAMBDA being real if (iscomplex (lambda)) error ("poissrnd: LAMBDA must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 1) sz = size (lambda); elseif (nargin == 2) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("poissrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 2) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("poissrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (lambda) && ! isequal (size (lambda), size (ones (sz)))) error ("poissrnd: LAMBDA must be scalar or of size SZ."); endif ## Check for class type if (isa (lambda, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from Poisson distribution if (isscalar (lambda)) if (lambda >= 0 && lambda < Inf) r = randp (lambda, sz, cls); else r = NaN (sz, cls); endif else r = NaN (sz, cls); k = (lambda >= 0) & (lambda < Inf); r(k) = randp (lambda(k), cls); endif endfunction ## Test output %!assert_equal (size (poissrnd (2)), [1, 1]) %!assert_equal (size (poissrnd (ones (2, 1))), [2, 1]) %!assert_equal (size (poissrnd (ones (2, 2))), [2, 2]) %!assert_equal (size (poissrnd (1, 3)), [3, 3]) %!assert_equal (size (poissrnd (1, [4, 1])), [4, 1]) %!assert_equal (size (poissrnd (1, 4, 1)), [4, 1]) %!assert_equal (size (poissrnd (1, 4, 1)), [4, 1]) %!assert_equal (size (poissrnd (1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (poissrnd (1, 0, 1)), [0, 1]) %!assert_equal (size (poissrnd (1, 1, 0)), [1, 0]) %!assert_equal (size (poissrnd (1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (poissrnd (1, [])), [0, 0]) %!assert_equal (size (poissrnd (1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (poissrnd (1, -1)), [0, 0]) %!assert_equal (size (poissrnd (1, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (poissrnd (1, 2, -1, 5)), [2, 0, 5]) %!assert_equal (poissrnd (0, 1, 1), 0) %!assert_equal (poissrnd ([0, 0, 0], [1, 3]), [0 0 0]) ## Test class of input preserved %!assert_equal (class (poissrnd (2)), "double") %!assert_equal (class (poissrnd (single (2))), "single") %!assert_equal (class (poissrnd (single ([2 2]))), "single") ## Test input validation %!error poissrnd () %!error poissrnd (i) %!error ... %! poissrnd (1, 1.2) %!error ... %! poissrnd (1, ones (2)) %!error ... %! poissrnd (1, [2 0 2.5]) %!error ... %! poissrnd (ones (2), ones (2)) %!error ... %! poissrnd (1, 2, 1.5, 5) %!error poissrnd (ones (2,2), 3) %!error poissrnd (ones (2,2), [3, 2]) %!error poissrnd (ones (2,2), 2, 3) statistics-release-2.1.2/inst/Distribution_Functions/private/000077500000000000000000000000001527311104300244575ustar00rootroot00000000000000statistics-release-2.1.2/inst/Distribution_Functions/private/__mvtcdfqmc__.m000066400000000000000000000214721527311104300274230ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{p} =} __mvtcdfqmc__ (@var{fname}, @var{A}, @var{B}, @var{Rho}, @var{df}) ## @deftypefnx {Private Function} {@var{p} =} __mvtcdfqmc__ (@dots{}, @var{TolFun}) ## @deftypefnx {Private Function} {@var{p} =} __mvtcdfqmc__ (@dots{}, @var{TolFun}, @var{MaxFunEvals}) ## @deftypefnx {Private Function} {@var{p} =} __mvtcdfqmc__ (@dots{}, @var{TolFun}, @var{MaxFunEvals}, @var{Display}) ## @deftypefnx {Private Function} {[@var{p}, @var{err}] =} __mvtcdfqmc__ (@dots{}) ## @deftypefnx {Private Function} {[@var{p}, @var{err}, @var{FunEvals}] =} __mvtcdfqmc__ (@dots{}) ## ## Quasi-Monte-Carlo computation of the multivariate Student's T CDF. ## ## The QMC multivariate Student's t distribution is evaluated between the lower ## limit @var{A} and upper limit @var{B} of the hyper-rectangle with a ## correlation matrix @var{Rho} and degrees of freedom @var{df}. ## ## @var{fname} is the name of the calling function, which prefixes every error ## and warning this helper emits so that a user sees the function they called ## rather than this private one. ## ## @multitable @columnfractions 0.2 0.8 ## @item "TolFun" @tab --- Maximum absolute error tolerance. Default is 1e-4. ## @item "MaxFunEvals" @tab --- Maximum number of integrand evaluations. ## Default is 1e7 for D > 4. ## @item "Display" @tab --- Display options. Choices are "off" (default), ## "iter", which shows the probability and estimated error at each repetition, ## and "final", which shows the final probability and related error after the ## integrand has converged successfully. ## @end multitable ## ## @code{[@var{p}, @var{err}, @var{FunEvals}] = __mvtcdfqmc__ (@dots{})} returns ## the estimated probability, @var{p}, an estimate of the error, @var{err}, and ## the number of iterations until a successful convergence is met, unless the ## value in @var{MaxFunEvals} was reached. ## ## @seealso{mvtcdf, mvtpdf, mvtrnd} ## @end deftypefn function [p, err, FunEvals] = __mvtcdfqmc__ (fname, A, B, Rho, df, varargin) ## Check for input arguments narginchk (5,8); ## Add defaults TolFun = 1e-4; MaxFunEvals = 1e7; Display = 'off'; ## Parse optional arguments (TolFun, MaxFunEvals, Display) if (nargin > 5) TolFun = varargin{1}; if (! isscalar (TolFun) || ! isreal (TolFun)) error ("%s: TolFun must be a scalar.", fname); endif endif if (nargin > 6) MaxFunEvals = varargin{2}; if (! isscalar (MaxFunEvals) || ! isreal (MaxFunEvals)) error ("%s: MaxFunEvals must be a scalar.", fname); endif MaxFunEvals = floor (MaxFunEvals); endif if (nargin > 7) Display = varargin{3}; DispOptions = {'off', 'final', 'iter'}; if (sum (any (strcmpi (Display, DispOptions))) == 0) error ("%s: invalid value for 'Display' argument.", fname); endif endif ## Check if input is single or double class is_type = 'double'; if (isa (A, 'single') || isa (B, 'single') || isa (Rho, 'single')) is_type = 'single'; endif ## Check for appropriate lower upper limits and NaN values in data if (! all (A < B)) if (any (A > B)) error ("%s: inconsistent lower upper limits.", fname); elseif (any (isnan (A) | isnan (B))) warning ("%s: NaNs in data.", fname); p = NaN (is_type); err = NaN (is_type); else warning ("%s: zero distance between lower upper limits.", fname); p = zeros (is_type); err = zeros (is_type); endif FunEvals = 0; return; endif ## Ignore dimensions with infinite limits InfLim_idx = (A == -Inf) & (B == Inf); if (any (InfLim_idx)) if (all (InfLim_idx)) warning ("%s: infinite distance between lower upper limits.", fname); p = 1; err = 0; FunEvals = 0; return endif A(InfLim_idx) = []; B(InfLim_idx) = []; Rho(:,InfLim_idx) = []; Rho(InfLim_idx,:) = []; endif ## Get size of covariance matrix m = size (Rho, 1); ## Sort the order of integration according to increasing length of interval [~, ord] = sort (B - A); A = A(ord); B = B(ord); Rho = Rho(ord, ord); ## Check for highly correlated covariance matrix if any (any (abs (tril (Rho,-1)) > .999)) warning ("%s: highly correlated covariance matrix Rho.", fname); endif ## Scale the integration limits and the Cholesky factor of Rho C = chol (Rho); c = diag (C); A = A(:) ./ c; B = B(:) ./ c; C = C ./ repmat (c',m,1); ## Set repetitions fof Monte Carlo MCreps = 25; MCdims = m - isinf (df); ## Set initial output p = zeros (is_type); sigsq = Inf (is_type); FunEvals = 0; err = NaN; ## Initialize vector P = [31, 47, 73, 113, 173, 263, 397, 593, 907, 1361, 2053, 3079, 4621, ... 6947, 10427, 15641, 23473, 35221, 52837, 79259, 118891, 178349, ... 267523, 401287, 601942, 902933, 1354471, 2031713]; for i = 5:length (P); if ((FunEvals + 2*MCreps*P(i)) > MaxFunEvals) break; endif ## Compute the Niederreiter point set generator NRgen = 2 .^ ((1:MCdims) / (MCdims + 1)); ## Compute randomized quasi-Monte Carlo estimate with P points [THat,sigsqTHat] = estimate_mvtqmc (MCreps, P(i), NRgen, C, df, ... A, B, is_type); FunEvals = FunEvals + 2 * MCreps *P(i); ## Recursively update the estimate and the error estimate p = p + (THat - p) ./ (1 + sigsqTHat ./ sigsq); sigsq = sigsqTHat ./ (1 + sigsqTHat ./ sigsq); ## Compute a conservative estimate of error err = 3.5 * sqrt (sigsq); ## Display output for every iteration if (strcmpi (Display, 'iter')) printf ("%s: Probability estimate: %0.4f ", fname, p); printf ("Error estimate: %0.4e Iterations: %d\n", err, FunEvals); endif if (err < TolFun) if (strcmpi (Display, 'final')) printf ("%s: Successfully converged!\n", fname); printf ("Final probability estimate: %0.4f ",p); printf ("Final error estimate: %0.4e Iterations: %d\n", err, FunEvals); endif return endif endfor warning ("%s: Error tolerance did NOT converge!", fname); printf ("Error tolerance: %0.4f Total Iterations: %d\n", TolFun, MaxFunEvals); endfunction ## Randomized Quasi-Monte-Carlo estimate of the integral function [THat, sigsqTHat] = estimate_mvtqmc (MCreps, P, NRgen, C, df, A, ... B, is_type) qq = (1:P)' * NRgen; THat = zeros (MCreps,1,is_type); for rep = 1:MCreps ## Generate A new random lattice of P points. For MVT, this is in the ## m-dimensional unit hypercube, for MVN, in the (m-1)-dimensional unit ## hypercube. w = abs (2 * mod (qq + repmat (rand (size (NRgen), is_type), P, 1), 1) - 1); ## Compute the mean of the integrand over all P of the points, and all P ## of the antithetic points. THat(rep) = (F_qrsvn (A, B, C, df, w) + F_qrsvn (A, B, C, df, 1 - w)) ./ 2; endfor ## Return the MC mean and se^2 sigsqTHat = var (THat) ./ MCreps; THat = mean (THat); endfunction ## Integrand for computation of MVT probabilities function TBar = F_qrsvn (A, B, C, df, w) N = size (w, 1); # number of quasirandom points m = length (A); # number of dimensions if isinf (df) rho = 1; else rho = chi_inv (w(:,m), df) ./ sqrt (df); endif rA = norm_cdf (rho .* A(1)); # A is already scaled by diag(C) rB = norm_cdf (rho .* B(1)) - rA; # B is already scaled by diag(C) T = rB; Y = zeros (N, m, 'like', T); for i = 2:m z = min (max (rA + rB .* w(:,i-1), eps / 2), 1 - eps / 2); Y(:,i-1) = norm_inv (z); Ysum = Y * C(:,i); rA = norm_cdf (rho .* A(i) - Ysum); # A is already scaled by diag(C) rB = norm_cdf (rho .* B(i) - Ysum) - rA; # B is already scaled by diag(C) T = T .* rB; endfor TBar = sum (T, 1) ./ length (T); endfunction ## Normal cumulative distribution function function a = norm_cdf (b) a = 0.5 * erfc (- b ./ sqrt (2)); endfunction ## Inverse of normal cumulative distribution function function a = norm_inv (b) a = - sqrt (2) .* erfcinv (2 * b); endfunction ## Inverse of chi cumulative distribution function function a = chi_inv (b,df) a = sqrt (gammaincinv (b, df ./ 2) .* 2); endfunction statistics-release-2.1.2/inst/Distribution_Functions/private/__stable_cf__.m000066400000000000000000000033301527311104300273520ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{phi} =} __stable_cf__ (@var{t}, @var{alpha}, @var{beta}) ## ## Characteristic function of the standard stable distribution. ## ## Returns the characteristic function @var{phi} evaluated at the points @var{t} ## of the standard stable distribution @code{S(@var{alpha}, @var{beta}, 1, 0)} ## in the Nolan @qcode{S0} parameterization. This is a private helper for ## @code{stblpdf} and @code{stblcdf}, which recover the density and the ## cumulative probability by numerical inversion. ## ## @end deftypefn function phi = __stable_cf__ (t, alpha, beta) at = abs (t); st = sign (t); if (abs (alpha - 1) < eps) ## The |t|*log|t| term vanishes at the origin lt = at .* log (at); lt(at == 0) = 0; phi = exp (-at - 1i .* beta .* (2 ./ pi) .* st .* lt); else phi = exp (-at .^ alpha - 1i .* beta .* tan (pi .* alpha ./ 2) ... .* st .* (at - at .^ alpha)); endif endfunction statistics-release-2.1.2/inst/Distribution_Functions/private/__stable_checkparams__.m000066400000000000000000000035031527311104300312450ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{msg} =} __stable_checkparams__ (@var{alpha}, @var{beta}, @var{gam}, @var{delta}) ## ## Validate the parameters of a stable distribution. ## ## Returns an empty string if the tail index @var{alpha}, skewness @var{beta}, ## scale @var{gam}, and location @var{delta} are valid, otherwise a message body ## describing the first violation. The calling function prepends its own name. ## This is a private helper shared by the @code{stbl*} functions. ## ## @end deftypefn function msg = __stable_checkparams__ (alpha, beta, gam, delta) msg = ""; if (! (isscalar (alpha) && isreal (alpha) && alpha > 0 && alpha <= 2)) msg = "ALPHA must be a scalar in the range (0, 2]."; elseif (! (isscalar (beta) && isreal (beta) && beta >= -1 && beta <= 1)) msg = "BETA must be a scalar in the range [-1, 1]."; elseif (! (isscalar (gam) && isreal (gam) && gam > 0)) msg = "GAM must be a positive scalar."; elseif (! (isscalar (delta) && isreal (delta))) msg = "DELTA must be a real scalar."; endif endfunction statistics-release-2.1.2/inst/Distribution_Functions/private/__stdr__.m000066400000000000000000000161261527311104300264130ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{F}, @var{U}] =} __stdr__ (@var{x}, @var{k}, @var{df}, @qcode{'cdf'}) ## @deftypefnx {Private Function} {@var{y} =} __stdr__ (@var{x}, @var{k}, @var{df}, @qcode{'pdf'}) ## ## Studentized range distribution at positive finite values. ## ## @var{x} is a vector of positive finite values, @var{k} a scalar integer of ## at least 2 and @var{df} a positive scalar, possibly @code{Inf}. With ## @qcode{'cdf'}, @var{F} and @var{U} are the lower and upper tail ## probabilities, each computed directly so that neither loses its relative ## accuracy to the other; with @qcode{'pdf'}, @var{y} is the density. ## ## The range @math{W} of @var{k} standard normals has ## @math{P(W <= w) = k int phi(z) (Phi(z) - Phi(z - w))^(k-1) dz}, evaluated on ## a fixed composite Gauss-Legendre grid in @math{z}. The studentized range is ## @math{W / s} with @math{s^2} a chi-square over @var{df}, so a finite ## @var{df} adds an adaptive integral over @math{log s}, weighted by the ## density of @math{s}. ## @end deftypefn function [F, U] = __stdr__ (x, k, df, what) x = x(:)'; if (strcmp (what, 'pdf')) if (isinf (df)) F = rangepdf (x, k); else F = zeros (size (x)); for i = 1:numel (x) F(i) = outer (@(w) rangepdf (w, k), x(i), k, df, 1); endfor endif return; endif if (isinf (df)) [F, U] = rangecdf (x, k); return; endif ## Each tail is integrated on its own; the larger one is the complement of ## the smaller, which is where the accuracy matters. F = zeros (size (x)); U = zeros (size (x)); for i = 1:numel (x) U(i) = outer (@(w) nthargout (2, @rangecdf, w, k), x(i), k, df, 0); if (U(i) < 0.5) F(i) = 1 - U(i); else F(i) = outer (@(w) rangecdf (w, k), x(i), k, df, 0); U(i) = 1 - F(i); endif endfor endfunction ## The integral over t = log (s) of H (x * s) weighted by the density of s, ## times s ^ P. Its integrand peaks near t = 0, where the density of s does, ## near 0.5 * log (df / (df + x^2 / 2)) for the upper tail and near ## 0.5 * log ((df + k - 1) / df) for the lower tail, and no feature of it is ## narrower than 1 / sqrt (2 * df); the window reaches far enough past both ## peaks for the density of s to fall below exp (-40). A fixed grid of ## panels half that width is exact to rounding, where an adaptive rule loses ## the peak once df is large. function I = outer (H, x, k, df, P) ## The density of t is exp (C + df * (t - expm1 (2 * t) / 2)), written so ## that no two large terms cancel inside the integrand, and C from ## Stirling's series once df is large enough for the direct form to lose ## digits. a = df / 2; if (a > 1e3) C = log (2) + 0.5 * log (a / (2 * pi)) - 1 / (12 * a) ... + 1 / (360 * a ^ 3) - 1 / (1260 * a ^ 5); else C = log (2) + a * log (a) - gammaln (a) - a; endif sd = 1 / sqrt (2 * df); tu = 0.5 * log (df / (df + x ^ 2 / 2)); tl = 0.5 * log ((df + k - 1) / df); ## Left of both peaks the integrand is exp (df * t) times a constant, so ## panels there need only be 2 / df wide mid = min (tu, 0) - 12 * sd; lo = mid - 40 / df; hi = max (tl + 12 * sd, ... 0.5 * log ((df + k + 40 * sqrt (2 * (df + k)) + 80) / df)); [t1, w1] = legendre ((mid - lo) / max (sd / 2, 2 / df), lo, mid); [t2, w2] = legendre ((hi - mid) / (sd / 2), mid, hi); t = [t1, t2]; wt = [w1, w2]; I = sum (wt .* exp (C + df * (t - expm1 (2 * t) / 2) + P * t) ... .* H (x * exp (t))); endfunction ## Nodes and weights of a composite 16-point Gauss-Legendre rule over ## [LO, HI] in at least N panels, as row vectors. function [t, wt] = legendre (n, lo, hi) persistent xg wg; if (isempty (xg)) m = 16; b = 0.5 ./ sqrt (1 - (2 * (1:m-1)) .^ (-2)); [V, D] = eig (diag (b, 1) + diag (b, -1)); xg = diag (D); wg = 2 * V(1,:)' .^ 2; endif np = max (1, ceil (n)); h = (hi - lo) / np; c = lo + h * ((0:np-1) + 0.5); t = reshape (c + (h / 2) * xg, 1, []); wt = repmat ((h / 2) * wg', 1, np); endfunction ## Lower and upper tail probabilities of the range of K standard normals. function [F, U] = rangecdf (w, k) [z, wz, phi, a, Pz, Qz, Pzw] = grid (w, k); F = k * sum (wz .* phi .* a .^ (k - 1), 1); ## Phi(z)^(k-1) - a^(k-1), written so that no two close numbers are ## subtracted: a = Phi(z) - Phi(z-w). ## The ratio is at most 1 in exact arithmetic, but where w is tiny against z ## a libm whose erfc is not monotone in the last bit can push it above, ## and log1p would then turn complex. logPz = log (Pz); logPz(z > 0) = log1p (- Qz(z > 0)); r = min (Pzw ./ Pz, 1); d = exp ((k - 1) * logPz) .* (- expm1 ((k - 1) * log1p (- r))); U = k * sum (wz .* phi .* d, 1); ## Beyond w = 40 the upper tail is below exp (-400). far = (w > 40); F(far) = 1; U(far) = 0; F = reshape (F, size (w)); U = reshape (U, size (w)); endfunction ## Density of the range of K standard normals. function f = rangepdf (w, k) [z, wz, phi, a] = grid (w, k); phizw = exp (- (z - w(:)') .^ 2 / 2) / sqrt (2 * pi); f = k * (k - 1) * sum (wz .* phi .* phizw .* a .^ (k - 2), 1); f(w > 40) = 0; f = reshape (f, size (w)); endfunction ## The z grid, one column of nodes shared by every W, and the quantities ## on it. Panels are at most 4 / sqrt (k) wide, the width of the peak ## the integrands have for large K. function [z, wz, phi, a, Pz, Qz, Pzw] = grid (w, k) persistent xg wg; if (isempty (xg)) n = 16; b = 0.5 ./ sqrt (1 - (2 * (1:n-1)) .^ (-2)); [V, D] = eig (diag (b, 1) + diag (b, -1)); xg = diag (D); wg = 2 * V(1,:)' .^ 2; endif w = w(:)'; zlo = -9; zhi = min (29, max (9, max (w(w <= 40)) / 2 + 9)); if (isempty (zhi)) zhi = 9; endif np = ceil ((zhi - zlo) / min (1, 4 / sqrt (k))); h = (zhi - zlo) / np; c = zlo + h * ((0:np-1) + 0.5); z = reshape (c + (h / 2) * xg, [], 1); wz = repmat ((h / 2) * wg, np, 1); phi = exp (- z .^ 2 / 2) / sqrt (2 * pi); Pz = 0.5 * erfc (- z / sqrt (2)); Qz = 0.5 * erfc (z / sqrt (2)); zw = z - w; Pzw = 0.5 * erfc (- zw / sqrt (2)); ## a = Phi(z) - Phi(z-w) from whichever tail keeps it accurate a = Pz - Pzw; r = (zw > 0); Qzw = 0.5 * erfc (zw / sqrt (2)); Qzm = repmat (Qz, 1, numel (w)); a(r) = Qzw(r) - Qzm(r); s = (z > 0) & (zw <= 0); E = erf (z / sqrt (2)) - erf (zw / sqrt (2)); a(s) = 0.5 * E(s); a = max (a, 0); endfunction statistics-release-2.1.2/inst/Distribution_Functions/raylcdf.m000066400000000000000000000130761527311104300246160ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} raylcdf (@var{x}, @var{sigma}) ## @deftypefnx {statistics} {@var{p} =} raylcdf (@var{x}, @var{sigma}, @qcode{'upper'}) ## ## Rayleigh cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the Rayleigh distribution with scale parameter @var{sigma}. The ## size of @var{p} is the common size of @var{x} and @var{sigma}. A scalar ## input functions as a constant matrix of the same size as the other inputs. ## ## @code{@var{p} = raylcdf (@var{x}, @var{sigma}, "upper")} computes the upper ## tail probability of the Rayleigh distribution with parameter @var{sigma}, at ## the values in @var{x}. ## ## Further information about the Rayleigh distribution can be found at ## @url{https://en.wikipedia.org/wiki/Rayleigh_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## The @code{prob.RayleighDistribution} class names this same parameter ## @qcode{B}, after MATLAB. ## @seealso{raylinv, raylpdf, raylrnd, raylfit, rayllike, raylstat} ## @end deftypefn function p = raylcdf (x, sigma, uflag) ## Check for valid number of input arguments if (nargin < 2) error ("raylcdf: function called with too few input arguments."); endif ## Check for "upper" flag if (nargin == 3 && strcmpi (uflag, 'upper')) uflag = true; elseif (nargin == 3 && ! strcmpi (uflag, 'upper')) error ("raylcdf: invalid argument for upper tail."); else uflag = false; endif ## Check for common size of X and SIGMA if (! isscalar (x) || ! isscalar (sigma)) [retval, x, sigma] = common_size (x, sigma); if (retval > 0) error ("raylcdf: X and SIGMA must be of common size or scalars."); endif endif ## Check for X and SIGMA being double or single if (! (isfloat (x) && isfloat (sigma))) error ("raylcdf: X and SIGMA must be double or single."); endif ## Check for X and SIGMA being reals if (iscomplex (x) || iscomplex (sigma)) error ("raylcdf: X and SIGMA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (sigma, 'single')); p = zeros (size (x), 'single'); else p = zeros (size (x)); endif ## Force 1 for upper flag and X <= 0 k0 = sigma > 0 & x <= 0; if (uflag && any (k0(:))) p(k0) = 1; endif ## Calculate Rayleigh CDF for valid parameter and data range k = sigma > 0 & x > 0; if (any (k(:))) if (uflag) p(k) = exp (-x(k) .^ 2 ./ (2 * sigma(k) .^ 2)); else p(k) = - expm1 (-x(k) .^ 2 ./ (2 * sigma(k) .^ 2)); endif endif ## Continue argument check p(! (k0 | k)) = NaN; endfunction %!demo %! ## Plot various CDFs from the Rayleigh distribution %! x = 0:0.01:10; %! p1 = raylcdf (x, 0.5); %! p2 = raylcdf (x, 1); %! p3 = raylcdf (x, 2); %! p4 = raylcdf (x, 3); %! p5 = raylcdf (x, 4); %! plot (x, p1, '-b', x, p2, 'g', x, p3, '-r', x, p4, '-m', x, p5, '-k') %! grid on %! ylim ([0, 1]) %! legend ({'σ = 0.5', 'σ = 1', 'σ = 2', ... %! 'σ = 3', 'σ = 4'}, 'location', 'southeast') %! title ('Rayleigh CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!test %! x = 0:0.5:2.5; %! sigma = 1:6; %! p = raylcdf (x, sigma); %! expected_p = [0.0000, 0.0308, 0.0540, 0.0679, 0.0769, 0.0831]; %! assert_equal (p, expected_p, 0.001); %!test %! x = 0:0.5:2.5; %! p = raylcdf (x, 0.5); %! expected_p = [0.0000, 0.3935, 0.8647, 0.9889, 0.9997, 1.0000]; %! assert_equal (p, expected_p, 0.001); %!shared x, p %! x = [-1, 0, 1, 2, Inf]; %! p = [0, 0, 0.39346934028737, 0.86466471676338, 1]; %!assert_equal (raylcdf (x, 1), p, 1e-14) %!assert_equal (raylcdf (x, 1, 'upper'), 1 - p, 1e-14) ## Test input validation %!error raylcdf () %!error raylcdf (1) %!error raylcdf (1, 2, 'uper') %!error raylcdf (1, 2, 3) %!error ... %! raylcdf (ones (3), ones (2)) %!error ... %! raylcdf (ones (2), ones (3)) %!error raylcdf (int32 (2), 2) %!error raylcdf (true, 2) %!error raylcdf ('a', 2) %!error raylcdf (i, 2) %!error raylcdf (2, i) statistics-release-2.1.2/inst/Distribution_Functions/raylinv.m000066400000000000000000000106771527311104300246620ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} raylinv (@var{p}, @var{sigma}) ## ## Inverse of the Rayleigh cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the Rayleigh distribution with scale parameter @var{sigma}. The size of ## @var{x} is the common size of @var{p} and @var{sigma}. A scalar input ## functions as a constant matrix of the same size as the other inputs. ## ## Further information about the Rayleigh distribution can be found at ## @url{https://en.wikipedia.org/wiki/Rayleigh_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## The @code{prob.RayleighDistribution} class names this same parameter ## @qcode{B}, after MATLAB. ## @seealso{raylcdf, raylpdf, raylrnd, raylfit, rayllike, raylstat} ## @end deftypefn function x = raylinv (p, sigma) ## Check for valid number of input arguments if (nargin < 2) error ("raylinv: function called with too few input arguments."); endif ## Check for common size of P and SIGMA if (! isscalar (p) || ! isscalar (sigma)) [retval, p, sigma] = common_size (p, sigma); if (retval > 0) error ("raylinv: P and SIGMA must be of common size or scalars."); endif endif ## Check for P and SIGMA being double or single if (! (isfloat (p) && isfloat (sigma))) error ("raylinv: P and SIGMA must be double or single."); endif ## Check for X and SIGMA being reals if (iscomplex (p) || iscomplex (sigma)) error ("raylinv: P and SIGMA must not be complex."); endif ## Calculate Rayleigh iCDF x = sqrt (-2 .* log (1 - p) .* sigma .^ 2); ## Check for valid parameter and support k = find (p == 1); if (any (k)) x(k) = Inf; endif k = find (! (p >= 0) | ! (p <= 1) | ! (sigma > 0)); if (any (k)) x(k) = NaN; endif endfunction %!demo %! ## Plot various iCDFs from the Rayleigh distribution %! p = 0.001:0.001:0.999; %! x1 = raylinv (p, 0.5); %! x2 = raylinv (p, 1); %! x3 = raylinv (p, 2); %! x4 = raylinv (p, 3); %! x5 = raylinv (p, 4); %! plot (p, x1, '-b', p, x2, 'g', p, x3, '-r', p, x4, '-m', p, x5, '-k') %! grid on %! ylim ([0, 10]) %! legend ({'σ = 0,5', 'σ = 1', 'σ = 2', ... %! 'σ = 3', 'σ = 4'}, 'location', 'northwest') %! title ('Rayleigh iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!test %! p = 0:0.1:0.5; %! sigma = 1:6; %! x = raylinv (p, sigma); %! expected_x = [0.0000, 0.9181, 2.0041, 3.3784, 5.0538, 7.0645]; %! assert_equal (x, expected_x, 0.001); %!test %! p = 0:0.1:0.5; %! x = raylinv (p, 0.5); %! expected_x = [0.0000, 0.2295, 0.3340, 0.4223, 0.5054, 0.5887]; %! assert_equal (x, expected_x, 0.001); ## Test input validation %!error raylinv () %!error raylinv (1) %!error ... %! raylinv (ones (3), ones (2)) %!error ... %! raylinv (ones (2), ones (3)) %!error raylinv (int32 (2), 2) %!error raylinv (true, 2) %!error raylinv ('a', 2) %!error raylinv (i, 2) %!error raylinv (2, i) statistics-release-2.1.2/inst/Distribution_Functions/raylpdf.m000066400000000000000000000117541527311104300246340ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} raylpdf (@var{x}, @var{sigma}) ## ## Rayleigh probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the Rayleigh distribution with scale parameter @var{sigma}. The size of ## @var{p} is the common size of @var{x} and @var{sigma}. A scalar input ## functions as a constant matrix of the same size as the other inputs. ## ## Further information about the Rayleigh distribution can be found at ## @url{https://en.wikipedia.org/wiki/Rayleigh_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## ## The density at an infinite abscissa is @math{0}, no proper distribution ## placing mass there. MATLAB returns @qcode{NaN} here, as it does for ## @code{poisspdf} and for no other density, which is an inconsistency there ## rather than a convention: it returns @math{0} at @math{Inf} for every other ## distribution of the same support. ## ## The @code{prob.RayleighDistribution} class names this same parameter ## @qcode{B}, after MATLAB. ## @seealso{raylcdf, raylinv, raylrnd, raylfit, rayllike, raylstat} ## @end deftypefn function y = raylpdf (x, sigma) ## Check for valid number of input arguments if (nargin < 2) error ("raylpdf: function called with too few input arguments."); endif ## Check for common size of X and SIGMA if (! isscalar (x) || ! isscalar (sigma)) [retval, x, sigma] = common_size (x, sigma); if (retval > 0) error ("raylpdf: X and SIGMA must be of common size or scalars."); endif endif ## Check for X and SIGMA being double or single if (! (isfloat (x) && isfloat (sigma))) error ("raylpdf: X and SIGMA must be double or single."); endif ## Check for X and SIGMA being reals if (iscomplex (x) || iscomplex (sigma)) error ("raylpdf: X and SIGMA must not be complex."); endif ## Calculate Rayleigh PDF y = x .* exp ((-x .^ 2) ./ (2 .* sigma .^ 2)) ./ (sigma .^ 2); ## Continue argument check k = find (! isfinite (x) | ! (sigma > 0)); if (any (k)) y(k) = NaN; endif k = x < 0; if (any (k)) y(k) = 0; endif ## The density at an infinite abscissa is zero: no proper distribution ## places mass there. MATLAB returns NaN, as it does for POISSPDF and ## nowhere else, which is an inconsistency there rather than a convention. y(isinf (x) & (sigma > 0)) = 0; endfunction %!demo %! ## Plot various PDFs from the Rayleigh distribution %! x = 0:0.01:10; %! y1 = raylpdf (x, 0.5); %! y2 = raylpdf (x, 1); %! y3 = raylpdf (x, 2); %! y4 = raylpdf (x, 3); %! y5 = raylpdf (x, 4); %! plot (x, y1, '-b', x, y2, 'g', x, y3, '-r', x, y4, '-m', x, y5, '-k') %! grid on %! ylim ([0, 1.25]) %! legend ({'σ = 0,5', 'σ = 1', 'σ = 2', ... %! 'σ = 3', 'σ = 4'}, 'location', 'northeast') %! title ('Rayleigh PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!test %! x = 0:0.5:2.5; %! sigma = 1:6; %! y = raylpdf (x, sigma); %! expected_y = [0.0000, 0.1212, 0.1051, 0.0874, 0.0738, 0.0637]; %! assert_equal (y, expected_y, 0.001); %!test %! x = 0:0.5:2.5; %! y = raylpdf (x, 0.5); %! expected_y = [0.0000, 1.2131, 0.5413, 0.0667, 0.0027, 0.0000]; %! assert_equal (y, expected_y, 0.001); ## Test input validation %!error raylpdf () %!error raylpdf (1) %!error ... %! raylpdf (ones (3), ones (2)) %!error ... %! raylpdf (ones (2), ones (3)) %!error raylpdf (int32 (2), 2) %!error raylpdf (true, 2) %!error raylpdf ('a', 2) %!error raylpdf (i, 2) %!error raylpdf (2, i) statistics-release-2.1.2/inst/Distribution_Functions/raylrnd.m000066400000000000000000000136431527311104300246450ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} raylrnd (@var{sigma}) ## @deftypefnx {statistics} {@var{r} =} raylrnd (@var{sigma}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} raylrnd (@var{sigma}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} raylrnd (@var{sigma}, [@var{sz}]) ## ## Random arrays from the Rayleigh distribution. ## ## @code{@var{r} = raylrnd (@var{sigma})} returns an array of random numbers ## chosen from the Rayleigh distribution with scale parameter @var{sigma}. The ## size of @var{r} is the size of @var{sigma}. A scalar input functions as a ## constant matrix of the same size as the other inputs. @var{sigma} must be a ## finite real number greater than 0, otherwise @qcode{NaN} is returned. ## ## When called with a single size argument, @code{raylrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the Rayleigh distribution can be found at ## @url{https://en.wikipedia.org/wiki/Rayleigh_distribution} ## ## The @code{prob.RayleighDistribution} class names this same parameter ## @qcode{B}, after MATLAB. ## @seealso{raylcdf, raylinv, raylpdf, raylfit, rayllike, raylstat} ## @end deftypefn function r = raylrnd (sigma, varargin) ## Check for valid number of input arguments if (nargin < 1) error ("raylrnd: function called with too few input arguments."); endif ## Check for SIGMA being real if (iscomplex (sigma)) error ("raylrnd: SIGMA must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 1) sz = size (sigma); elseif (nargin == 2) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("raylrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 2) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("raylrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (sigma) && ! isequal (size (sigma), size (ones (sz)))) error ("raylrnd: SIGMA must be scalar or of size SZ."); endif ## Generate random sample from Rayleigh distribution r = sqrt (-2 .* log (1 - rand (sz)) .* sigma .^ 2); ## Check for valid parameter k = find (! (sigma > 0)); if (any (k)) r(k) = NaN; endif ## Cast into appropriate class if (isa (sigma, 'single')) r = cast (r, 'single'); endif endfunction ## Test output %!assert_equal (size (raylrnd (2)), [1, 1]) %!assert_equal (size (raylrnd (ones (2, 1))), [2, 1]) %!assert_equal (size (raylrnd (ones (2, 2))), [2, 2]) %!assert_equal (size (raylrnd (1, 3)), [3, 3]) %!assert_equal (size (raylrnd (1, [4, 1])), [4, 1]) %!assert_equal (size (raylrnd (1, 4, 1)), [4, 1]) %!assert_equal (size (raylrnd (1, 4, 1)), [4, 1]) %!assert_equal (size (raylrnd (1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (raylrnd (1, 0, 1)), [0, 1]) %!assert_equal (size (raylrnd (1, 1, 0)), [1, 0]) %!assert_equal (size (raylrnd (1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (raylrnd (1, [])), [0, 0]) %!assert_equal (size (raylrnd (1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (raylrnd (1, -1)), [0, 0]) %!assert_equal (size (raylrnd (1, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (raylrnd (1, 2, -1, 5)), [2, 0, 5]) %!assert_equal (raylrnd (0, 1, 1), NaN) %!assert_equal (raylrnd ([0, 0, 0], [1, 3]), [NaN, NaN, NaN]) ## Test class of input preserved %!assert_equal (class (raylrnd (2)), "double") %!assert_equal (class (raylrnd (single (2))), "single") %!assert_equal (class (raylrnd (single ([2, 2]))), "single") ## Test input validation %!error raylrnd () %!error raylrnd (i) %!error ... %! raylrnd (1, 1.2) %!error ... %! raylrnd (1, ones (2)) %!error ... %! raylrnd (1, [2 0 2.5]) %!error ... %! raylrnd (ones (2), ones (2)) %!error ... %! raylrnd (1, 2, 1.5, 5) %!error raylrnd (ones (2,2), 3) %!error raylrnd (ones (2,2), [3, 2]) %!error raylrnd (ones (2,2), 2, 3) statistics-release-2.1.2/inst/Distribution_Functions/ricecdf.m000066400000000000000000000175011527311104300245660ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} ricecdf (@var{x}, @var{s}, @var{sigma}) ## @deftypefnx {statistics} {@var{p} =} ricecdf (@var{x}, @var{s}, @var{sigma}, @qcode{'upper'}) ## ## Rician cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the Rician distribution with non-centrality (distance) parameter ## @var{s} and scale parameter @var{sigma}. The size of @var{p} is the common ## size of @var{x}, @var{s}, and @var{sigma}. A scalar input functions as a ## constant matrix of the same size as the other inputs. ## ## @code{@var{p} = ricecdf (@var{x}, @var{s}, @var{sigma}, "upper")} computes ## the upper tail probability of the Rician distribution with parameters ## @var{s} and @var{sigma}, at the values in @var{x}. ## ## Further information about the Rician distribution can be found at ## @url{https://en.wikipedia.org/wiki/Rice_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{riceinv, ricepdf, ricernd, ricefit, ricelike, ricestat} ## @end deftypefn function p = ricecdf (x, s, sigma, uflag) ## Check for valid number of input arguments if (nargin < 3) error ("ricecdf: function called with too few input arguments."); endif ## Check for "upper" flag if (nargin == 4 && strcmpi (uflag, 'upper')) uflag = true; elseif (nargin == 4 && ! strcmpi (uflag, 'upper')) error ("ricecdf: invalid argument for upper tail."); else uflag = false; endif ## Check for common size of X, S, and SIGMA if (! isscalar (x) || ! isscalar (s) || ! isscalar (sigma)) [retval, x, s, sigma] = common_size (x, s, sigma); if (retval > 0) error ("ricecdf: X, S, and SIGMA must be of common size or scalars."); endif endif ## Check for X, S, and SIGMA being double or single if (! (isfloat (x) && isfloat (s) && isfloat (sigma))) error ("ricecdf: X, S, and SIGMA must be double or single."); endif ## Check for X, S, and SIGMA being reals if (iscomplex (x) || iscomplex (s) || iscomplex (sigma)) error ("ricecdf: X, S, and SIGMA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (s, 'single') || isa (sigma, 'single')); p = zeros (size (x), 'single'); else p = zeros (size (x)); endif ## Force 1 for upper flag and X <= 0 k0 = s >= 0 & sigma > 0 & x < 0; if (uflag && any (k0(:))) p(k0) = 1; endif ## Calculate Rayleigh CDF for valid parameter and data range k = s >= 0 & sigma > 0 & x >= 0; if (any (k(:))) if (uflag) p(k) = marcumQ1 (s(k) ./ sigma(k), x(k) ./ sigma(k)); else p(k) = 1 - marcumQ1 (s(k) ./ sigma(k), x(k) ./ sigma(k)); endif endif ## Continue argument check p(! (k0 | k)) = NaN; endfunction ## Marcum's "Q" function of order 1 function Q = marcumQ1 (a, b) ## Prepare output matrix if (isa (a, 'single') || isa (b, 'single')) Q = NaN (size (b), 'single'); else Q = NaN (size (b)); endif ## Force marginal cases Q(a != Inf & b == 0) = 1; Q(a != Inf & b == Inf) = 0; Q(a == Inf & b != Inf) = 1; z = isnan (Q) & a == 0 & b != Inf; if (any (z)) Q(z) = exp ((-b(z) .^ 2) ./ 2); endif ## Compute the remaining cases z = isnan (Q) & ! isnan (a) & ! isnan (b); if (any (z(:))) aa = (a(z) .^ 2) ./ 2; bb = (b(z) .^ 2) ./ 2; eA = exp (-aa); eB = bb .* exp (-bb); h = eA; d = eB .* h; s = d; j = (d > s.*eps (class (d))); k = 1; while (any (j)) eA = aa .* eA ./ k; h = h + eA; eB = bb .* eB ./ (k + 1); d = eB .* h; s(j) = s(j) + d(j); j = (d > s .* eps (class (d))); k = k + 1; endwhile Q(z) = 1 - s; endif endfunction %!demo %! ## Plot various CDFs from the Rician distribution %! x = 0:0.01:10; %! p1 = ricecdf (x, 0, 1); %! p2 = ricecdf (x, 0.5, 1); %! p3 = ricecdf (x, 1, 1); %! p4 = ricecdf (x, 2, 1); %! p5 = ricecdf (x, 4, 1); %! plot (x, p1, '-b', x, p2, 'g', x, p3, '-r', x, p4, '-m', x, p5, '-k') %! grid on %! ylim ([0, 1]) %! xlim ([0, 8]) %! legend ({'s = 0, σ = 1', 's = 0.5, σ = 1', 's = 1, σ = 1', ... %! 's = 2, σ = 1', 's = 4, σ = 1'}, 'location', 'southeast') %! title ('Rician CDF') %! xlabel ('values in x') %! ylabel ('probability') %!demo %! ## Plot various CDFs from the Rician distribution %! x = 0:0.01:10; %! p1 = ricecdf (x, 0, 0.5); %! p2 = ricecdf (x, 0, 2); %! p3 = ricecdf (x, 0, 3); %! p4 = ricecdf (x, 2, 2); %! p5 = ricecdf (x, 4, 2); %! plot (x, p1, '-b', x, p2, 'g', x, p3, '-r', x, p4, '-m', x, p5, '-k') %! grid on %! ylim ([0, 1]) %! xlim ([0, 8]) %! legend ({'ν = 0, σ = 0.5', 'ν = 0, σ = 2', 'ν = 0, σ = 3', ... %! 'ν = 2, σ = 2', 'ν = 4, σ = 2'}, 'location', 'southeast') %! title ('Rician CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!test %! x = 0:0.5:2.5; %! s = 1:6; %! p = ricecdf (x, s, 1); %! expected_p = [0.0000, 0.0179, 0.0108, 0.0034, 0.0008, 0.0001]; %! assert_equal (p, expected_p, 0.001); %!test %! x = 0:0.5:2.5; %! sigma = 1:6; %! p = ricecdf (x, 1, sigma); %! expected_p = [0.0000, 0.0272, 0.0512, 0.0659, 0.0754, 0.0820]; %! assert_equal (p, expected_p, 0.001); %!test %! x = 0:0.5:2.5; %! p = ricecdf (x, 0, 1); %! expected_p = [0.0000, 0.1175, 0.3935, 0.6753, 0.8647, 0.9561]; %! assert_equal (p, expected_p, 0.001); %!test %! x = 0:0.5:2.5; %! p = ricecdf (x, 1, 1); %! expected_p = [0.0000, 0.0735, 0.2671, 0.5120, 0.7310, 0.8791]; %! assert_equal (p, expected_p, 0.001); %!shared x, p %! x = [-1, 0, 1, 2, Inf]; %! p = [0, 0, 0.26712019620318, 0.73098793996409, 1]; %!assert_equal (ricecdf (x, 1, 1), p, 1e-14) %!assert_equal (ricecdf (x, 1, 1, 'upper'), 1 - p, 1e-14) ## Test input validation %!error ricecdf () %!error ricecdf (1) %!error ricecdf (1, 2) %!error ricecdf (1, 2, 3, 'uper') %!error ricecdf (1, 2, 3, 4) %!error ... %! ricecdf (ones (3), ones (2), ones (2)) %!error ... %! ricecdf (ones (2), ones (3), ones (2)) %!error ... %! ricecdf (ones (2), ones (2), ones (3)) %!error ricecdf (int32 (2), 2, 3) %!error ricecdf (true, 2, 3) %!error ricecdf ('a', 2, 3) %!error ricecdf (i, 2, 3) %!error ricecdf (2, i, 3) %!error ricecdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/riceinv.m000066400000000000000000000126771527311104300246370ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} riceinv (@var{p}, @var{s}, @var{sigma}) ## ## Inverse of the Rician distribution (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) ## of the Rician distribution with non-centrality (distance) parameter @var{s} ## and scale parameter @var{sigma}. The size of @var{x} is the common size of ## @var{x}, @var{s}, and @var{sigma}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## Further information about the Rician distribution can be found at ## @url{https://en.wikipedia.org/wiki/Rice_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{ricecdf, ricepdf, ricernd, ricefit, ricelike, ricestat} ## @end deftypefn function x = riceinv (p, s, sigma) ## Check for valid number of input arguments if (nargin < 3) error ("riceinv: function called with too few input arguments."); endif ## Check for common size of P, S, and B if (! isscalar (p) || ! isscalar (s) || ! isscalar (sigma)) [retval, p, s, sigma] = common_size (p, s, sigma); if (retval > 0) error ("riceinv: P, S, and B must be of common size or scalars."); endif endif ## Check for P, S, and B being double or single if (! (isfloat (p) && isfloat (s) && isfloat (sigma))) error ("riceinv: P, S, and B must be double or single."); endif ## Check for P, S, and B being reals if (iscomplex (p) || iscomplex (s) || iscomplex (sigma)) error ("riceinv: P, S, and B must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (s, 'single') || isa (sigma, 'single')) x = zeros (size (p), 'single'); else x = zeros (size (p)); endif k = s < 0 | sigma <= 0 | p < 0 | p > 1 | ... isnan (p) | isnan (s) | isnan (sigma); x(k) = NaN; k = ! k; x(k) = sigma(k) .* sqrt (ncx2inv (p(k), 2, (s(k) ./ sigma(k)) .^ 2)); endfunction %!demo %! ## Plot various iCDFs from the Rician distribution %! p = 0.001:0.001:0.999; %! x1 = riceinv (p, 0, 1); %! x2 = riceinv (p, 0.5, 1); %! x3 = riceinv (p, 1, 1); %! x4 = riceinv (p, 2, 1); %! x5 = riceinv (p, 4, 1); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', p, x4, '-m', p, x5, '-k') %! grid on %! legend ({'s = 0, σ = 1', 's = 0.5, σ = 1', 's = 1, σ = 1', ... %! 's = 2, σ = 1', 's = 4, σ = 1'}, 'location', 'northwest') %! title ('Rician iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p %! p = [-1 0 0.75 1 2]; %!assert_equal (riceinv (p, ones (1,5), 2*ones (1,5)), [NaN 0 3.5354 Inf NaN], 1e-4) %!assert_equal (riceinv (p, 1, 2*ones (1,5)), [NaN 0 3.5354 Inf NaN], 1e-4) %!assert_equal (riceinv (p, ones (1,5), 2), [NaN 0 3.5354 Inf NaN], 1e-4) %!assert_equal (riceinv (p, [1 0 NaN 1 1], 2), [NaN 0 NaN Inf NaN]) %!assert_equal (riceinv (p, 1, 2*[1 0 NaN 1 1]), [NaN NaN NaN Inf NaN]) %!assert_equal (riceinv ([p(1:2) NaN p(4:5)], 1, 2), [NaN 0 NaN Inf NaN]) ## Test class of input preserved %!assert_equal (riceinv ([p, NaN], 1, 2), [NaN 0 3.5354 Inf NaN NaN], 1e-4) %!assert_equal (riceinv (single ([p, NaN]), 1, 2), ... %! single ([NaN 0 3.5354 Inf NaN NaN]), 1e-4) %!assert_equal (riceinv ([p, NaN], single (1), 2), ... %! single ([NaN 0 3.5354 Inf NaN NaN]), 1e-4) %!assert_equal (riceinv ([p, NaN], 1, single (2)), ... %! single ([NaN 0 3.5354 Inf NaN NaN]), 1e-4) ## Test input validation %!error riceinv () %!error riceinv (1) %!error riceinv (1,2) %!error riceinv (1,2,3,4) %!error ... %! riceinv (ones (3), ones (2), ones (2)) %!error ... %! riceinv (ones (2), ones (3), ones (2)) %!error ... %! riceinv (ones (2), ones (2), ones (3)) %!error riceinv (int32 (2), 2, 2) %!error riceinv (true, 2, 2) %!error riceinv ('a', 2, 2) %!error riceinv (i, 2, 2) %!error riceinv (2, i, 2) %!error riceinv (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/ricepdf.m000066400000000000000000000131271527311104300246030ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} ricepdf (@var{x}, @var{s}, @var{sigma}) ## ## Rician probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the Rician distribution with non-centrality (distance) parameter @var{s} ## and scale parameter @var{sigma}. The size of @var{y} is the common size of ## @var{x}, @var{s}, and @var{sigma}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## Further information about the Rician distribution can be found at ## @url{https://en.wikipedia.org/wiki/Rice_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{ricecdf, riceinv, ricernd, ricefit, ricelike, ricestat} ## @end deftypefn function y = ricepdf (x, s, sigma) ## Check for valid number of input arguments if (nargin < 3) error ("ricepdf: function called with too few input arguments."); endif ## Check for common size of X, S, and SIGMA if (! isscalar (x) || ! isscalar (s) || ! isscalar (sigma)) [retval, x, s, sigma] = common_size (x, s, sigma); if (retval > 0) error ("ricepdf: X, S, and SIGMA must be of common size or scalars."); endif endif ## Check for X, S, and SIGMA being double or single if (! (isfloat (x) && isfloat (s) && isfloat (sigma))) error ("ricepdf: X, S, and SIGMA must be double or single."); endif ## Check for X, S, and SIGMA being reals if (iscomplex (x) || iscomplex (s) || iscomplex (sigma)) error ("ricepdf: X, S, and SIGMA must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (s, 'single') || isa (sigma, 'single')); y = zeros (size (x), 'single'); else y = zeros (size (x)); endif k = s < 0 | sigma <= 0 | x < 0 | isnan (x) | isnan (s) | isnan (sigma); y(k) = NaN; k = ! k; ## Do the math x_k = x(k); n_k = s(k); s_sq = sigma(k) .^ 2; x_s2 = x_k ./ s_sq; xnsq = (x_k .^ 2 + n_k .^ 2) ./ (2 .* s_sq); epxt = xnsq - x_s2 .* n_k; term = exp (-epxt); y(k) = x_s2 .* term .* besseli (0, x_s2 .* n_k, 1); ## Fix arithmetic overflow due to exponent y(epxt > (log (realmax (class (y))))) = 0; ## Fix x < 0 -> 0 y(x < 0) = 0; endfunction %!demo %! ## Plot various PDFs from the Rician distribution %! x = 0:0.01:8; %! y1 = ricepdf (x, 0, 1); %! y2 = ricepdf (x, 0.5, 1); %! y3 = ricepdf (x, 1, 1); %! y4 = ricepdf (x, 2, 1); %! y5 = ricepdf (x, 4, 1); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', x, y4, '-m', x, y5, '-k') %! grid on %! ylim ([0, 0.65]) %! xlim ([0, 8]) %! legend ({'s = 0, σ = 1', 's = 0.5, σ = 1', 's = 1, σ = 1', ... %! 's = 2, σ = 1', 's = 4, σ = 1'}, 'location', 'northeast') %! title ('Rician PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x, y %! x = [-1 0 0.5 1 2]; %! y = [0 0 0.1073 0.1978 0.2846]; %!assert_equal (ricepdf (x, ones (1, 5), 2 * ones (1, 5)), y, 1e-4) %!assert_equal (ricepdf (x, 1, 2 * ones (1, 5)), y, 1e-4) %!assert_equal (ricepdf (x, ones (1, 5), 2), y, 1e-4) %!assert_equal (ricepdf (x, [0 NaN 1 1 1], 2), [0 NaN y(3:5)], 1e-4) %!assert_equal (ricepdf (x, 1, 2 * [0 NaN 1 1 1]), [0 NaN y(3:5)], 1e-4) %!assert_equal (ricepdf ([x, NaN], 1, 2), [y, NaN], 1e-4) ## Test class of input preserved %!assert_equal (ricepdf (single ([x, NaN]), 1, 2), single ([y, NaN]), 1e-4) %!assert_equal (ricepdf ([x, NaN], single (1), 2), single ([y, NaN]), 1e-4) %!assert_equal (ricepdf ([x, NaN], 1, single (2)), single ([y, NaN]), 1e-4) ## Test input validation %!error ricepdf () %!error ricepdf (1) %!error ricepdf (1,2) %!error ricepdf (1,2,3,4) %!error ... %! ricepdf (ones (3), ones (2), ones (2)) %!error ... %! ricepdf (ones (2), ones (3), ones (2)) %!error ... %! ricepdf (ones (2), ones (2), ones (3)) %!error ricepdf (int32 (2), 2, 2) %!error ricepdf (true, 2, 2) %!error ricepdf ('a', 2, 2) %!error ricepdf (i, 2, 2) %!error ricepdf (2, i, 2) %!error ricepdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/ricernd.m000066400000000000000000000165201527311104300246150ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} ricernd (@var{s}, @var{sigma}) ## @deftypefnx {statistics} {@var{r} =} ricernd (@var{s}, @var{sigma}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} ricernd (@var{s}, @var{sigma}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} ricernd (@var{s}, @var{sigma}, [@var{sz}]) ## ## Random arrays from the Rician distribution. ## ## @code{@var{r} = ricernd (@var{s}, @var{sigma})} returns an array of random ## numbers chosen from the Rician distribution with noncentrality parameter ## @var{s} and scale parameter @var{sigma}. The size of @var{r} is the common ## size of @var{s} and @var{sigma}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## When called with a single size argument, @code{ricernd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the Rician distribution can be found at ## @url{https://en.wikipedia.org/wiki/Rice_distribution} ## ## @seealso{ricecdf, riceinv, ricepdf, ricefit, ricelike, ricestat} ## @end deftypefn function r = ricernd (s, sigma, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("ricernd: function called with too few input arguments."); endif ## Check for common size of S and SIGMA if (! isscalar (s) || ! isscalar (sigma)) [retval, s, sigma] = common_size (s, sigma); if (retval > 0) error ("ricernd: S and SIGMA must be of common size or scalars."); endif endif ## Check for S and SIGMA being reals if (iscomplex (s) || iscomplex (sigma)) error ("ricernd: S and SIGMA must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (s); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("ricernd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("ricernd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (s) && ! isequal (size (s), size (ones (sz)))) error ("ricernd: S and SIGMA must be scalars or of size SZ."); endif ## Check for class type if (isa (s, 'single') || isa (sigma, 'single')) cls = 'single'; else cls = 'double'; endif ## Return NaNs for out of range values of S and SIGMA s(s < 0) = NaN; sigma(sigma <= 0) = NaN; ## Force S and SIGMA into the same size as SZ (if necessary) if (isscalar (s)) s = repmat (s, sz); endif if (isscalar (sigma)) sigma = repmat (sigma, sz); endif ## Generate random sample from the Rician distribution r = sigma .* sqrt (ncx2rnd (2, (s ./ sigma) .^ 2, sz)); ## Cast to appropriate class r = cast (r, cls); endfunction ## Test output %!assert_equal (size (ricernd (2, 1/2)), [1, 1]) %!assert_equal (size (ricernd (2 * ones (2, 1), 1/2)), [2, 1]) %!assert_equal (size (ricernd (2 * ones (2, 2), 1/2)), [2, 2]) %!assert_equal (size (ricernd (2, 1/2 * ones (2, 1))), [2, 1]) %!assert_equal (size (ricernd (1, 1/2 * ones (2, 2))), [2, 2]) %!assert_equal (size (ricernd (ones (2, 1), 1)), [2, 1]) %!assert_equal (size (ricernd (ones (2, 2), 1)), [2, 2]) %!assert_equal (size (ricernd (2, 1/2, 3)), [3, 3]) %!assert_equal (size (ricernd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (ricernd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (ricernd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (ricernd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (ricernd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (ricernd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (ricernd (1, 1, [])), [0, 0]) %!assert_equal (size (ricernd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (ricernd (1, 1/2, -1)), [0, 0]) %!assert_equal (size (ricernd (1, 1/2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (ricernd (1, 1/2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (ricernd (1, 1)), "double") %!assert_equal (class (ricernd (1, single (0))), "single") %!assert_equal (class (ricernd (1, single ([0, 0]))), "single") %!assert_equal (class (ricernd (1, single (1), 2)), "single") %!assert_equal (class (ricernd (1, single ([1, 1]), 1, 2)), "single") %!assert_equal (class (ricernd (single (1), 1, 2)), "single") %!assert_equal (class (ricernd (single ([1, 1]), 1, 1, 2)), "single") ## Test input validation %!error ricernd () %!error ricernd (1) %!error ... %! ricernd (ones (3), ones (2)) %!error ... %! ricernd (ones (2), ones (3)) %!error ricernd (i, 2) %!error ricernd (1, i) %!error ... %! ricernd (1, 1/2, 1.2) %!error ... %! ricernd (1, 1/2, ones (2)) %!error ... %! ricernd (1, 1/2, [2 0 2.5]) %!error ... %! ricernd (1, 1/2, 2, 1.5, 5) %!error ... %! ricernd (2, 1/2 * ones (2), 3) %!error ... %! ricernd (2, 1/2 * ones (2), [3, 2]) %!error ... %! ricernd (2, 1/2 * ones (2), 3, 2) %!error ... %! ricernd (2 * ones (2), 1/2, 3) %!error ... %! ricernd (2 * ones (2), 1/2, [3, 2]) %!error ... %! ricernd (2 * ones (2), 1/2, 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/stblcdf.m000066400000000000000000000135761527311104300246200ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} stblcdf (@var{x}, @var{alpha}, @var{beta}, @var{gam}, @var{delta}) ## ## Stable cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the stable distribution with tail index (first shape parameter) ## @var{alpha}, skewness (second shape parameter) @var{beta}, scale parameter ## @var{gam}, and location parameter @var{delta}, in the Nolan @qcode{S0} ## parameterization. The size of @var{p} is the size of @var{x}. ## ## @var{alpha} must be in the range @math{(0, 2]}, @var{beta} in @math{[-1, 1]}, ## @var{gam} positive, and @var{delta} real. The parameters must be scalars. ## ## The cumulative probability has a closed form for @var{alpha} equal to ## @code{2} (normal) and for @code{1} with @var{beta} equal to @code{0} ## (Cauchy); otherwise it is computed by numerical inversion of the ## characteristic function (the Gil-Pelaez formula). ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{stblpdf, stblinv, stblrnd, makedist} ## @end deftypefn function p = stblcdf (x, alpha, beta, gam, delta) if (nargin != 5) print_usage (); endif msg = __stable_checkparams__ (alpha, beta, gam, delta); if (! isempty (msg)) error ("stblcdf: %s", msg); endif ## Check for X, ALPHA, BETA, GAM, and DELTA being double or single if (! (isfloat (x) && isfloat (alpha) && isfloat (beta) && isfloat (gam) && isfloat (delta))) error ("stblcdf: X, ALPHA, BETA, GAM, and DELTA must be double or single."); endif if (! isreal (x)) error ("stblcdf: X must be real."); endif z = (x - delta) ./ gam; p = nan (size (z)); ok = ! isnan (z); if (alpha == 2) p(ok) = 0.5 .* erfc (-z(ok) ./ 2); elseif (alpha == 1 && beta == 0) p(ok) = 0.5 + atan (z(ok)) ./ pi; else zk = z(ok); v = zeros (numel (zk), 1); for i = 1:numel (zk) v(i) = 0.5 - (1 ./ pi) .* quadgk (@(t) imag (exp (-1i .* t .* zk(i)) ... .* __stable_cf__ (t, alpha, beta)) ./ t, 0, Inf, ... "AbsTol", 1e-12, "RelTol", 1e-10); endfor p(ok) = min (max (v, 0), 1); endif endfunction %!demo %! ## Stable cdf: Cauchy, a skewed stable, and the normal limit %! x = linspace (-6, 6, 200); %! plot (x, stblcdf (x, 1, 0, 1, 0), "-", ... %! x, stblcdf (x, 1.5, 0.5, 1, 0), "-", ... %! x, stblcdf (x, 2, 0, 1, 0), "-"); %! legend ("Cauchy", "alpha=1.5, beta=0.5", "normal", "location", "southeast"); ## Test output against MATLAB %!test %! x = -5:5; %! p = stblcdf (x, 1.5, 0.5, 1, 0); %! exp_p = [0.00961772128347771, 0.0143422747723476, 0.0257902242195547, ... %! 0.0657154294128386, 0.201576145758624, 0.462186560100778, ... %! 0.712063555515659, 0.855535196378772, 0.921201224725992, ... %! 0.951409668616683, 0.966845678836178]; %! assert_equal (p, exp_p, 1e-8); %!test %! x = -5:5; %! p = stblcdf (x, 0.8, 0.5, 1, 0); %! exp_p = [0.0427283102762096, 0.0504255041089544, 0.0624716830177048, ... %! 0.0849086757683013, 0.150275591315296, 0.431333711402679, ... %! 0.641248581720908, 0.74188789948898, 0.797926083610673, ... %! 0.833292740693924, 0.857610462691116]; %! assert_equal (p, exp_p, 1e-8); %!test # scaled and shifted (gam = 2, delta = 3) %! x = -5:5; %! p = stblcdf (x, 1.5, 0.5, 2, 3); %! exp_p = [0.0143422747723476, 0.0186030552365194, 0.0257902242195547, ... %! 0.0392075905274278, 0.0657154294128386, 0.116299801968237, ... %! 0.201576145758624, 0.321987153858349, 0.462186560100778, ... %! 0.598389078433622, 0.712063555515659]; %! assert_equal (p, exp_p, 1e-8); %!test # normal special case %! x = -5:5; %! assert_equal (stblcdf (x, 2, 0, 1, 0), normcdf (x, 0, sqrt (2)), 1e-12); %!test # Cauchy special case %! x = -5:5; %! assert_equal (stblcdf (x, 1, 0, 1, 0), 0.5 + atan (x) ./ pi, 1e-12); %!test # cdf is the integral of the pdf %! assert_equal (stblcdf (0.7, 1.3, -0.4, 1, 0) - ... %! stblcdf (-1.2, 1.3, -0.4, 1, 0), ... %! quadgk (@(x) stblpdf (x, 1.3, -0.4, 1, 0), -1.2, 0.7), 1e-8); ## Test input validation %!error stblcdf (int32 (2), 1.5, 0, 1, 0) %!error stblcdf (true, 1.5, 0, 1, 0) %!error stblcdf ('a', 1.5, 0, 1, 0) %!error stblcdf (1, 1.5, 0.5, 1) %!error ... %! stblcdf (1, 2.5, 0, 1, 0) %!error ... %! stblcdf (1, 1.5, 2, 1, 0) %!error stblcdf (1, 1.5, 0, 0, 0) %!error stblcdf (1, 1.5, 0, 1, 1i) %!error stblcdf (1i, 1.5, 0, 1, 0) statistics-release-2.1.2/inst/Distribution_Functions/stblinv.m000066400000000000000000000132501527311104300246450ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} stblinv (@var{p}, @var{alpha}, @var{beta}, @var{gam}, @var{delta}) ## ## Inverse of the stable cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the stable distribution with tail index (first shape parameter) @var{alpha}, ## skewness (second shape parameter) @var{beta}, scale parameter @var{gam}, and ## location parameter @var{delta}, in the Nolan @qcode{S0} parameterization. ## The size of @var{x} is the size of @var{p}. ## ## @var{alpha} must be in the range @math{(0, 2]}, @var{beta} in @math{[-1, 1]}, ## @var{gam} positive, and @var{delta} real. The parameters must be scalars. ## ## The quantile has a closed form for @var{alpha} equal to @code{2} (normal) and ## for @code{1} with @var{beta} equal to @code{0} (Cauchy); otherwise it is ## found by numerical inversion of @code{stblcdf}. ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{stblcdf, stblpdf, stblrnd, makedist} ## @end deftypefn function x = stblinv (p, alpha, beta, gam, delta) if (nargin != 5) print_usage (); endif msg = __stable_checkparams__ (alpha, beta, gam, delta); if (! isempty (msg)) error ("stblinv: %s", msg); endif ## Check for P, ALPHA, BETA, GAM, and DELTA being double or single if (! (isfloat (p) && isfloat (alpha) && isfloat (beta) && isfloat (gam) && isfloat (delta))) error ("stblinv: P, ALPHA, BETA, GAM, and DELTA must be double or single."); endif if (! (isreal (p) && all (p(:) >= 0 & p(:) <= 1 | isnan (p(:))))) error ("stblinv: P must contain real values in the range [0, 1]."); endif x = nan (size (p)); if (alpha == 2) x = delta + sqrt (2) .* gam .* (-sqrt (2) .* erfcinv (2 .* p)); elseif (alpha == 1 && beta == 0) x = delta + gam .* tan (pi .* (p - 0.5)); else for i = 1:numel (p) pp = p(i); if (isnan (pp)) continue; elseif (pp == 0) x(i) = -Inf; elseif (pp == 1) x(i) = Inf; else ## Root-find on the standardized cdf, started from the (heavy-tailed) ## Cauchy quantile so the search stays near the solution. z0 = tan (pi .* (pp - 0.5)); z = fzero (@(zz) stblcdf (zz, alpha, beta, 1, 0) - pp, z0); x(i) = delta + gam .* z; endif endfor endif endfunction %!demo %! ## Quantiles of a skewed stable distribution %! p = [0.1, 0.25, 0.5, 0.75, 0.9]; %! x = stblinv (p, 1.5, 0.5, 1, 0) ## Test output against MATLAB %!test %! p = [0.1, 0.25, 0.5, 0.75, 0.9]; %! x = stblinv (p, 1.5, 0.5, 1, 0); %! exp_x = [-1.63127009138493, -0.783313648587273, 0.133853042315326, ... %! 1.20341055131626, 2.58231785139714]; %! assert_equal (x, exp_x, 1e-6); %!test %! p = [0.1, 0.25, 0.5, 0.75, 0.9]; %! x = stblinv (p, 0.8, 0.5, 1, 0); %! exp_x = [-1.62200033048034, -0.553853652413272, 0.250487323305453, ... %! 2.11601429745393, 8.03924696271835]; %! assert_equal (x, exp_x, 1e-5); %!test # scaled and shifted (gam = 2, delta = 3) %! p = [0.1, 0.25, 0.5, 0.75, 0.9]; %! x = stblinv (p, 1.5, 0.5, 2, 3); %! exp_x = [-0.26254018276987, 1.43337270282545, 3.26770608463065, ... %! 5.40682110263252, 8.16463570279428]; %! assert_equal (x, exp_x, 1e-6); %!test # symmetric case (beta = 0): quantiles antisymmetric about delta %! p = [0.1, 0.25, 0.5, 0.75, 0.9]; %! x = stblinv (p, 1.5, 0, 1, 0); %! exp_x = [-2.06146263813919, -0.968933181710917, 0, ... %! 0.968933181710917, 2.06146263813919]; %! assert_equal (x, exp_x, 1e-6); %!test # normal and Cauchy special cases %! p = [0.1, 0.3, 0.5, 0.7, 0.9]; %! assert_equal (stblinv (p, 2, 0, 1, 0), norminv (p, 0, sqrt (2)), 1e-12); %! assert_equal (stblinv (p, 1, 0, 1, 0), tan (pi .* (p - 0.5)), 1e-12); %!test # inverts stblcdf %! x0 = [-3, -0.5, 0.8, 4]; %! p = stblcdf (x0, 1.4, 0.3, 1.5, -1); %! assert_equal (stblinv (p, 1.4, 0.3, 1.5, -1), x0, 1e-6); %!test # boundaries %! assert_equal (stblinv ([0, 1], 1.5, 0.5, 1, 0), [-Inf, Inf]); ## Test input validation %!error stblinv (int32 (2), 1.5, 0, 1, 0) %!error stblinv (true, 1.5, 0, 1, 0) %!error stblinv ('a', 1.5, 0, 1, 0) %!error stblinv (0.5, 1.5, 0.5, 1) %!error ... %! stblinv (0.5, 2.5, 0, 1, 0) %!error ... %! stblinv (1.2, 1.5, 0, 1, 0) %!error ... %! stblinv (0.5i, 1.5, 0, 1, 0) statistics-release-2.1.2/inst/Distribution_Functions/stblpdf.m000066400000000000000000000145421527311104300246270ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} stblpdf (@var{x}, @var{alpha}, @var{beta}, @var{gam}, @var{delta}) ## ## Stable probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the stable distribution with tail index (first shape parameter) ## @var{alpha}, skewness (second shape parameter) @var{beta}, scale parameter ## @var{gam}, and location parameter @var{delta}, in the Nolan @qcode{S0} ## parameterization. The size of @var{y} is the size of @var{x}. ## ## @var{alpha} must be in the range @math{(0, 2]}, @var{beta} in @math{[-1, 1]}, ## @var{gam} positive, and @var{delta} real. The parameters must be scalars. ## ## The density has a closed form for @var{alpha} equal to @code{2} (normal) and ## for @code{1} with @var{beta} equal to @code{0} (Cauchy); otherwise it is ## computed by numerical inversion of the characteristic function. ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{stblcdf, stblinv, stblrnd, makedist} ## @end deftypefn function y = stblpdf (x, alpha, beta, gam, delta) if (nargin != 5) print_usage (); endif msg = __stable_checkparams__ (alpha, beta, gam, delta); if (! isempty (msg)) error ("stblpdf: %s", msg); endif ## Check for X, ALPHA, BETA, GAM, and DELTA being double or single if (! (isfloat (x) && isfloat (alpha) && isfloat (beta) && isfloat (gam) && isfloat (delta))) error ("stblpdf: X, ALPHA, BETA, GAM, and DELTA must be double or single."); endif if (! isreal (x)) error ("stblpdf: X must be real."); endif ## Standardize z = (x - delta) ./ gam; y = nan (size (z)); ok = ! isnan (z); if (alpha == 2) ## Normal with variance 2 y(ok) = exp (-z(ok) .^ 2 ./ 4) ./ (2 .* sqrt (pi)); elseif (alpha == 1 && beta == 0) ## Cauchy y(ok) = 1 ./ (pi .* (1 + z(ok) .^ 2)); else zk = z(ok); v = zeros (numel (zk), 1); for i = 1:numel (zk) v(i) = (1 ./ pi) .* quadgk (@(t) real (exp (-1i .* t .* zk(i)) ... .* __stable_cf__ (t, alpha, beta)), 0, Inf, ... "AbsTol", 1e-12, "RelTol", 1e-10); endfor y(ok) = max (v, 0); endif ## Undo the scale (density transforms by 1 / gam) y = y ./ gam; endfunction %!demo %! ## Stable densities: Cauchy, a skewed stable, and the normal limit %! x = linspace (-6, 6, 200); %! plot (x, stblpdf (x, 1, 0, 1, 0), "-", ... %! x, stblpdf (x, 1.5, 0.5, 1, 0), "-", ... %! x, stblpdf (x, 2, 0, 1, 0), "-"); %! legend ("Cauchy", "alpha=1.5, beta=0.5", "normal"); ## Test output against MATLAB %!test %! x = -5:5; %! y = stblpdf (x, 1.5, 0.5, 1, 0); %! exp_y = [0.00330549826030791, 0.00673588721821526, 0.0190320671951022, ... %! 0.0729514702833168, 0.208194435543156, 0.284283800988578, ... %! 0.198573023913399, 0.0958317325744725, 0.0428461930184788, ... %! 0.0207819141087301, 0.0113306451818624]; %! assert_equal (y, exp_y, 1e-9); %!test %! x = -5:5; %! y = stblpdf (x, 0.8, 0.5, 1, 0); %! exp_y = [0.00634335874934447, 0.0093623044752761, 0.0155686941305108, ... %! 0.0326882516316453, 0.135673711418341, 0.298698147231422, ... %! 0.139071606104264, 0.0722555300098094, 0.0433943212392174, ... %! 0.0288186423689968, 0.020522989417733]; %! assert_equal (y, exp_y, 1e-9); %!test %! x = -5:5; %! y = stblpdf (x, 1.2, -0.5, 1, 0); %! exp_y = [0.0166464288580291, 0.0264743439008481, 0.0459799636082411, ... %! 0.0881296016218348, 0.177627321920986, 0.288106176914537, ... %! 0.196803906514695, 0.0520585692918225, 0.0173156565634881, ... %! 0.00836895075945555, 0.00490202402183536]; %! assert_equal (y, exp_y, 1e-9); %!test # scaled and shifted (gam = 2, delta = 3) %! x = -5:5; %! y = stblpdf (x, 1.5, 0.5, 2, 3); %! exp_y = [0.00336794360910763, 0.00537726811151198, 0.00951603359755111, ... %! 0.0184406959152125, 0.0364757351416584, 0.0666533040480966, ... %! 0.104097217771578, 0.134023248277231, 0.142141900494289, ... %! 0.127056343301115, 0.0992865119566997]; %! assert_equal (y, exp_y, 1e-9); %!test # normal special case (alpha = 2) %! x = -5:5; %! assert_equal (stblpdf (x, 2, 0, 1, 0), normpdf (x, 0, sqrt (2)), 1e-12); %!test # Cauchy special case (alpha = 1, beta = 0) %! x = -5:5; %! assert_equal (stblpdf (x, 1, 0, 1, 0), 1 ./ (pi .* (1 + x .^ 2)), 1e-12); %!test # Levy (alpha = 0.5, beta = 1): S0 support boundary at x = -1 %! x = -5:5; %! y = stblpdf (x, 0.5, 1, 1, 0); %! assert_equal (y(x < 0), zeros (1, 5), 1e-6); %! assert_equal (y(x == 0), 0.241970724519143, 1e-9); ## Test input validation %!error stblpdf (int32 (2), 1.5, 0, 1, 0) %!error stblpdf (true, 1.5, 0, 1, 0) %!error stblpdf ('a', 1.5, 0, 1, 0) %!error stblpdf (1, 1.5, 0.5, 1) %!error ... %! stblpdf (1, 2.5, 0, 1, 0) %!error ... %! stblpdf (1, 1.5, 2, 1, 0) %!error stblpdf (1, 1.5, 0, 0, 0) %!error stblpdf (1, 1.5, 0, 1, 1i) %!error stblpdf (1i, 1.5, 0, 1, 0) statistics-release-2.1.2/inst/Distribution_Functions/stblrnd.m000066400000000000000000000132301527311104300246320ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} stblrnd (@var{alpha}, @var{beta}, @var{gam}, @var{delta}) ## @deftypefnx {statistics} {@var{r} =} stblrnd (@var{alpha}, @var{beta}, @var{gam}, @var{delta}, @var{m}) ## @deftypefnx {statistics} {@var{r} =} stblrnd (@var{alpha}, @var{beta}, @var{gam}, @var{delta}, @var{m}, @var{n}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} stblrnd (@var{alpha}, @var{beta}, @var{gam}, @var{delta}, [@var{m}, @var{n}, @dots{}]) ## ## Random arrays from the stable distribution. ## ## @code{@var{r} = stblrnd (@var{alpha}, @var{beta}, @var{gam}, @var{delta})} ## returns a random value drawn from the stable distribution with tail index ## (first shape parameter) @var{alpha}, skewness (second shape parameter) ## @var{beta}, scale parameter @var{gam}, and location parameter @var{delta}, in ## the Nolan @qcode{S0} parameterization. ## ## @var{alpha} must be in the range @math{(0, 2]}, @var{beta} in @math{[-1, 1]}, ## @var{gam} positive, and @var{delta} real. The parameters must be scalars. ## ## @code{stblrnd (@var{alpha}, @var{beta}, @var{gam}, @var{delta}, @var{m}, ## @var{n}, @dots{})} or @code{stblrnd (@dots{}, [@var{m}, @var{n}, @dots{}])} ## returns an @var{m}-by-@var{n}-by-@dots{} array, following the size ## conventions of @code{rand}. ## ## The values are generated with the Chambers-Mallows-Stuck method. ## ## @seealso{stblpdf, stblcdf, stblinv, makedist} ## @end deftypefn function r = stblrnd (alpha, beta, gam, delta, varargin) if (nargin < 4) print_usage (); endif msg = __stable_checkparams__ (alpha, beta, gam, delta); if (! isempty (msg)) error ("stblrnd: %s", msg); endif ## Output size, following rand's conventions if (numel (varargin) == 0) sz = [1, 1]; elseif (numel (varargin) == 1) a = varargin{1}; if (isscalar (a)) sz = [a, a]; else sz = a(:)'; endif else sz = [varargin{:}]; endif if (! all (sz == fix (sz))) error ("stblrnd: dimensions must be integers."); endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Chambers-Mallows-Stuck: draw a uniform on (-pi/2, pi/2) and a unit ## exponential, form a standard S1 variate, then shift to S0 and scale. V = (rand (sz) - 0.5) .* pi; W = -log (rand (sz)); if (alpha == 1) X = (2 ./ pi) .* ((pi ./ 2 + beta .* V) .* tan (V) ... - beta .* log ((pi ./ 2 .* W .* cos (V)) ... ./ (pi ./ 2 + beta .* V))); r = gam .* X + delta; else ct = tan (pi .* alpha ./ 2); B = atan (beta .* ct) ./ alpha; S = (1 + beta .^ 2 .* ct .^ 2) .^ (1 ./ (2 .* alpha)); X = S .* sin (alpha .* (V + B)) ./ (cos (V)) .^ (1 ./ alpha) ... .* (cos (V - alpha .* (V + B)) ./ W) .^ ((1 - alpha) ./ alpha); r = gam .* (X - beta .* ct) + delta; endif endfunction %!demo %! ## Draw a large stable sample and overlay the theoretical density %! rng (42); %! r = stblrnd (1.5, 0.5, 1, 0, 1, 1e5); %! r = r(abs (r) < 15); %! hist (r, 100, 1); %! hold on; %! x = linspace (-15, 15, 400); %! plot (x, stblpdf (x, 1.5, 0.5, 1, 0), "r-", "linewidth", 2); %! hold off; ## The empirical cdf of a large sample matches stblcdf (seeded, repeatable) %!test %! rand ("state", 42); %! r = stblrnd (1.5, 0.5, 1, 0, 1, 200000); %! xs = [-2, -0.5, 0.5, 2]; %! ec = arrayfun (@(x) mean (r <= x), xs); %! assert_equal (ec, stblcdf (xs, 1.5, 0.5, 1, 0), 0.01); %!test # heavy-tailed alpha < 1, scaled and shifted %! rand ("state", 7); %! r = stblrnd (0.8, -0.3, 2, 1, 1, 200000); %! xs = [-3, 0, 1, 4]; %! ec = arrayfun (@(x) mean (r <= x), xs); %! assert_equal (ec, stblcdf (xs, 0.8, -0.3, 2, 1), 0.01); %!test # alpha = 1 with beta ~= 0 %! rand ("state", 99); %! r = stblrnd (1, 0.5, 1.5, -2, 1, 200000); %! xs = [-5, -2, 0, 3]; %! ec = arrayfun (@(x) mean (r <= x), xs); %! assert_equal (ec, stblcdf (xs, 1, 0.5, 1.5, -2), 0.01); %!test # alpha = 2 is normal with variance 2*gam^2 %! rand ("state", 1); %! r = stblrnd (2, 0, 1, 0, 1, 200000); %! assert_equal (mean (r), 0, 0.02); %! assert_equal (var (r), 2, 0.05); ## Size handling follows rand %!test %! assert_equal (size (stblrnd (1.5, 0.5, 1, 0, 3, 4)), [3, 4]); %! assert_equal (size (stblrnd (1.5, 0.5, 1, 0, [2, 5])), [2, 5]); %! assert_equal (isscalar (stblrnd (1.5, 0.5, 1, 0)), true); %! assert_equal (size (stblrnd (1.5, 0.5, 1, 0, -1)), [0, 0]); %! assert_equal (size (stblrnd (1.5, 0.5, 1, 0, 2, -1, 5)), [2, 0, 5]); ## Test input validation %!error stblrnd (1.5, 0.5, 1) %!error ... %! stblrnd (2.5, 0, 1, 0) %!error ... %! stblrnd (1.5, 2, 1, 0) %!error stblrnd (1.5, 0, 0, 0) %!error stblrnd (1.5, 0, 1, 1i) %!error ... %! stblrnd (1.5, 0, 1, 0, 2.5) statistics-release-2.1.2/inst/Distribution_Functions/stdrcdf.m000066400000000000000000000205101527311104300246120ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} stdrcdf (@var{x}, @var{k}, @var{df}) ## @deftypefnx {statistics} {@var{p} =} stdrcdf (@var{x}, @var{k}, @var{df}, @qcode{'upper'}) ## ## Studentized range cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the studentized range distribution for @var{k} groups and @var{df} ## degrees of freedom. The size of @var{p} is the common size of @var{x}, ## @var{k} and @var{df}. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## The studentized range is the range of @var{k} independent standard normal ## variables divided by an independent @math{sqrt (chi2 / df)}, where ## @math{chi2} has a chi-square distribution with @var{df} degrees of ## freedom. It is the distribution behind Tukey's honestly significant ## difference and the Tukey-Kramer multiple comparison procedure. @var{k} ## must be an integer of at least 2 and @var{df} positive, @code{Inf} ## included, which gives the range of @var{k} standard normals; otherwise ## @var{p} is @code{NaN}. ## ## @code{@var{p} = stdrcdf (@var{x}, @var{k}, @var{df}, "upper")} computes the ## upper tail probability of the studentized range distribution at the values ## in @var{x}. Each tail is computed directly, so a small upper tail ## probability keeps its relative accuracy. ## ## The distribution is evaluated by numerical integration, accurate to about ## 1e-13 relative to the tail probability for @var{df} up to 1e4, and to ## about 1e-8 for @var{df} of 1e8. ## ## MATLAB has no public counterpart of this function. ## ## Further information about the studentized range distribution can be found ## at @url{https://en.wikipedia.org/wiki/Studentized_range_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. ## ## @seealso{stdrinv, stdrpdf, stdrrnd, stdrstat, multcompare} ## @end deftypefn function p = stdrcdf (x, k, df, uflag) ## Check for valid number of input arguments if (nargin < 3) error ("stdrcdf: function called with too few input arguments."); endif ## Check for "upper" flag if (nargin > 3 && strcmpi (uflag, 'upper')) utail = true; elseif (nargin > 3 && ! strcmpi (uflag, 'upper')) error ("stdrcdf: invalid argument for upper tail."); else utail = false; endif ## Check for common size of X, K, and DF if (! isscalar (x) || ! isscalar (k) || ! isscalar (df)) [err, x, k, df] = common_size (x, k, df); if (err > 0) error ("stdrcdf: X, K, and DF must be of common size or scalars."); endif endif ## Check for X, K, and DF being double or single if (! (isfloat (x) && isfloat (k) && isfloat (df))) error ("stdrcdf: X, K, and DF must be double or single."); endif ## Check for X, K, and DF being reals if (iscomplex (x) || iscomplex (k) || iscomplex (df)) error ("stdrcdf: X, K, and DF must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (k, 'single') || isa (df, 'single')) cls = 'single'; else cls = 'double'; endif sz = size (x); x = double (x(:)); k = double (k(:)) .* ones (numel (x), 1); df = double (df(:)) .* ones (numel (x), 1); ## Invalid parameters or X give NaN valid = (k >= 2) & (k == fix (k)) & isfinite (k) & (df > 0); lower = zeros (numel (x), 1); lower(! valid | isnan (x)) = NaN; lower(valid & x == Inf) = 1; uppr = 1 - lower; ## Evaluate the positive finite values, one parameter pair at a time todo = valid & (x > 0) & isfinite (x); if (any (todo)) pairs = unique ([k(todo), df(todo)], 'rows'); for i = 1:rows (pairs) idx = todo & (k == pairs(i,1)) & (df == pairs(i,2)); [F, U] = __stdr__ (x(idx), pairs(i,1), pairs(i,2), 'cdf'); lower(idx) = F; uppr(idx) = U; endfor endif if (utail) p = uppr; else p = lower; endif p = cast (reshape (p, sz), cls); endfunction %!demo %! ## Plot various CDFs from the studentized range distribution %! x = 0:0.01:8; %! p1 = stdrcdf (x, 2, 5); %! p2 = stdrcdf (x, 3, 5); %! p3 = stdrcdf (x, 5, 5); %! p4 = stdrcdf (x, 5, Inf); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', x, p4, '-m') %! grid on %! ylim ([0, 1]) %! legend ({'k = 2, df = 5', 'k = 3, df = 5', 'k = 5, df = 5', ... %! 'k = 5, df = \infty'}, 'location', 'southeast') %! title ('Studentized range CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Two groups are sqrt (2) times the absolute value of a t variable %!shared x %! x = [0.5, 2, 4, 8]; %!assert_equal (stdrcdf (x, 2, 5), 1 - 2 * tcdf (-x / sqrt (2), 5), -1e-13) %!assert_equal (stdrcdf (x, 2, 5, 'upper'), 2 * tcdf (-x / sqrt (2), 5), -1e-13) %!assert_equal (stdrcdf (x, 2, Inf), erf (x / 2), -1e-13) %!assert_equal (stdrcdf (x, 2, Inf, 'upper'), erfc (x / 2), -1e-13) %!assert_equal (stdrcdf ([x, 900], 2, 1, 'upper'), ... %! 2 * tcdf (-[x, 900] / sqrt (2), 1), -1e-13) ## A far upper tail keeps its relative accuracy %!assert_equal (stdrcdf (20, 2, 30, 'upper'), ... %! betainc (200 / 230, 1/2, 15, 'upper'), -1e-12) ## Upper tail probabilities from R 4.5.0 ptukey, accurate to about 1e-6 for ## DF of 10 or more %!assert_equal (stdrcdf ([2, 4, 8], 3, 10, 'upper'), ... %! [0.37054467503555799, 0.043349349760161804, ... %! 0.00055885879221251322], -1e-5) %!assert_equal (stdrcdf ([2, 4, 8], 10, 30, 'upper'), ... %! [0.91315280898350459, 0.17214641499562477, ... %! 0.00013979917001072373], -1e-5) %!assert_equal (stdrcdf ([2, 4, 8], 20, Inf, 'upper'), ... %! [0.9976642515512063, 0.3360234394147692, ... %! 2.8901613927656555e-06], -1e-5) ## Upper tail probabilities from MATLAB R2024a, through the Tukey-Kramer ## p-values of multcompare, accurate to about 1e-3 above 1e-3 %!assert_equal (stdrcdf ([0.5, 1, 1.5], 3, 10, 'upper'), ... %! [0.93386416513356696, 0.76489080347916205, ... %! 0.55804816568706039], -1e-3) %!assert_equal (stdrcdf ([0.5, 1.5, 3, 5], 5, 30, 'upper'), ... %! [0.99646380721257077, 0.82481287273578452, ... %! 0.23767360358955125, 0.010890009004856521], -1e-3) ## Edge values and invalid parameters %!assert_equal (stdrcdf ([-1, 0, Inf, NaN], 3, 10), [0, 0, 1, NaN]) %!assert_equal (stdrcdf ([-1, 0, Inf, NaN], 3, 10, 'upper'), [1, 1, 0, NaN]) %!assert_equal (stdrcdf (2, [1, 2.5, NaN, Inf], 10), NaN (1, 4)) %!assert_equal (stdrcdf (2, 3, [0, -1, NaN]), NaN (1, 3)) ## Test class of input preserved %!assert_equal (class (stdrcdf (single (2), 3, 10)), 'single') %!assert_equal (class (stdrcdf (2, single (3), 10)), 'single') %!assert_equal (class (stdrcdf (2, 3, single (10))), 'single') ## Test input validation %!error stdrcdf () %!error stdrcdf (1, 2) %!error stdrcdf (1, 2, 3, 'uper') %!error stdrcdf (1, 2, 3, 4) %!error ... %! stdrcdf (ones (3), ones (2), 3) %!error ... %! stdrcdf (int32 (2), 3, 10) %!error ... %! stdrcdf (true, 3, 10) %!error ... %! stdrcdf ('a', 3, 10) %!error stdrcdf (i, 3, 10) %!error stdrcdf (2, i, 10) %!error stdrcdf (2, 3, i) statistics-release-2.1.2/inst/Distribution_Functions/stdrinv.m000066400000000000000000000167571527311104300246740ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} stdrinv (@var{p}, @var{k}, @var{df}) ## ## Inverse of the studentized range cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) ## of the studentized range distribution for @var{k} groups and @var{df} ## degrees of freedom. The size of @var{x} is the common size of @var{p}, ## @var{k} and @var{df}. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## @code{stdrinv (1 - @var{alpha}, @var{k}, @var{df})} is the critical value ## of Tukey's honestly significant difference at level @var{alpha}. @var{k} ## must be an integer of at least 2 and @var{df} positive, @code{Inf} ## included; otherwise @var{x} is @code{NaN}, as it is for @var{p} outside ## @math{[0, 1]}. The quantile is found to about 1e-13 relative accuracy. ## ## MATLAB has no public counterpart of this function. ## ## Further information about the studentized range distribution can be found ## at @url{https://en.wikipedia.org/wiki/Studentized_range_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. ## ## @seealso{stdrcdf, stdrpdf, stdrrnd, stdrstat, multcompare} ## @end deftypefn function x = stdrinv (p, k, df) ## Check for valid number of input arguments if (nargin < 3) error ("stdrinv: function called with too few input arguments."); endif ## Check for common size of P, K, and DF if (! isscalar (p) || ! isscalar (k) || ! isscalar (df)) [err, p, k, df] = common_size (p, k, df); if (err > 0) error ("stdrinv: P, K, and DF must be of common size or scalars."); endif endif ## Check for P, K, and DF being double or single if (! (isfloat (p) && isfloat (k) && isfloat (df))) error ("stdrinv: P, K, and DF must be double or single."); endif ## Check for P, K, and DF being reals if (iscomplex (p) || iscomplex (k) || iscomplex (df)) error ("stdrinv: P, K, and DF must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (k, 'single') || isa (df, 'single')) cls = 'single'; else cls = 'double'; endif sz = size (p); p = double (p(:)); k = double (k(:)) .* ones (numel (p), 1); df = double (df(:)) .* ones (numel (p), 1); valid = (k >= 2) & (k == fix (k)) & isfinite (k) & (df > 0); x = NaN (numel (p), 1); x(valid & p == 0) = 0; x(valid & p == 1) = Inf; for i = find (valid & p > 0 & p < 1)' x(i) = invert (p(i), k(i), df(i)); endfor x = cast (reshape (x, sz), cls); endfunction ## Newton's method on u = log (x), matching the log of whichever tail P ## leaves smaller so that either extreme keeps its digits, inside a bracket ## that is halved whenever a step would leave it. function q = invert (p, k, df) lower = (p <= 0.5); if (lower) target = log (p); else target = log1p (- p); endif ## Start from the Bonferroni bound over the k (k - 1) / 2 pairs u = log (sqrt (2) * tinv (1 - min (p, 1 - p) / (k * (k - 1)), df)); if (lower) u = log (sqrt (2) * tinv ((1 + p) / 2, df)); endif [h, dh] = gap (u, k, df, lower, target); a = -Inf; b = Inf; for iter = 1:100 if (h < 0) a = u; else b = u; endif step = - h / dh; un = u + step; if (! isfinite (un) || un <= a || un >= b) if (isinf (a)) un = b - 2; elseif (isinf (b)) un = a + 2; else un = (a + b) / 2; endif endif if (abs (un - u) <= 1e-14 * max (1, abs (u))) u = un; break; endif u = un; [h, dh] = gap (u, k, df, lower, target); if (h == 0) break; endif endfor q = exp (u); endfunction ## The log tail at exp (U) less its target, increasing in U, and its ## derivative in U. function [h, dh] = gap (u, k, df, lower, target) q = exp (u); f = stdrpdf (q, k, df); if (lower) T = stdrcdf (q, k, df); h = log (T) - target; dh = f * q / T; else T = stdrcdf (q, k, df, 'upper'); h = target - log (T); dh = f * q / T; endif endfunction %!demo %! ## Plot various iCDFs from the studentized range distribution %! p = 0.01:0.01:0.99; %! x1 = stdrinv (p, 2, 5); %! x2 = stdrinv (p, 3, 5); %! x3 = stdrinv (p, 5, Inf); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r') %! grid on %! legend ({'k = 2, df = 5', 'k = 3, df = 5', 'k = 5, df = \infty'}, ... %! 'location', 'northwest') %! title ('Studentized range iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Two groups are sqrt (2) times the absolute value of a t variable %!shared p %! p = [0.1, 0.5, 0.95, 0.999]; %!assert_equal (stdrinv (p, 2, 5), sqrt (2) * tinv ((1 + p) / 2, 5), -1e-12) %!assert_equal (stdrinv (p, 2, 1), sqrt (2) * tan (pi * p / 2), -1e-12) ## Quantiles from R 4.5.0 qtukey, accurate to about 1e-7 for DF of 10 or more %!assert_equal (stdrinv ([0.5, 0.9, 0.95, 0.99], 3, 10), ... %! [1.6446889006146324, 3.2703084031559371, ... %! 3.8767767491915595, 5.2701615370332799], -1e-6) %!assert_equal (stdrinv (0.95, 20, Inf), 5.0116887946867648, -1e-6) ## Quantiles from MATLAB R2024a, through the Tukey-Kramer limits of ## multcompare %!assert_equal (stdrinv (0.95, 5, 30), 4.1020790196264514, -1e-4) %!assert_equal (stdrinv (0.95, 3, 10), 3.8767767552295549, -1e-4) ## The quantile returns the probability it was found for, in either tail %!assert_equal (stdrcdf (stdrinv (0.975, 4, 7), 4, 7), 0.975, -1e-13) %!assert_equal (stdrcdf (stdrinv (1e-8, 4, 7), 4, 7), 1e-8, -1e-10) %!assert_equal (stdrcdf (stdrinv (1 - 1e-10, 3, 20), 3, 20, 'upper'), ... %! 1e-10, -1e-6) ## Edge values and invalid parameters %!assert_equal (stdrinv ([0, 1, -1, 2, NaN], 3, 10), [0, Inf, NaN, NaN, NaN]) %!assert_equal (stdrinv (0.5, [1, 2.5, NaN, Inf], 10), NaN (1, 4)) %!assert_equal (stdrinv (0.5, 3, [0, -1, NaN]), NaN (1, 3)) ## Test class of input preserved %!assert_equal (class (stdrinv (single (0.5), 3, 10)), 'single') %!assert_equal (class (stdrinv (0.5, 3, single (10))), 'single') ## Test input validation %!error stdrinv () %!error stdrinv (1, 2) %!error ... %! stdrinv (ones (3), ones (2), 3) %!error ... %! stdrinv (int32 (1), 3, 10) %!error ... %! stdrinv (true, 3, 10) %!error ... %! stdrinv ('a', 3, 10) %!error stdrinv (i, 3, 10) %!error stdrinv (0.5, i, 10) %!error stdrinv (0.5, 3, i) statistics-release-2.1.2/inst/Distribution_Functions/stdrpdf.m000066400000000000000000000133671527311104300246430ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} stdrpdf (@var{x}, @var{k}, @var{df}) ## ## Studentized range probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the studentized range distribution for @var{k} groups and @var{df} ## degrees of freedom. The size of @var{y} is the common size of @var{x}, ## @var{k} and @var{df}. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## @var{k} must be an integer of at least 2 and @var{df} positive, @code{Inf} ## included; otherwise @var{y} is @code{NaN}. The density is zero for ## negative @var{x} and, for @var{k} greater than 2, at zero. ## ## MATLAB has no public counterpart of this function. ## ## Further information about the studentized range distribution can be found ## at @url{https://en.wikipedia.org/wiki/Studentized_range_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. ## ## @seealso{stdrcdf, stdrinv, stdrrnd, stdrstat} ## @end deftypefn function y = stdrpdf (x, k, df) ## Check for valid number of input arguments if (nargin < 3) error ("stdrpdf: function called with too few input arguments."); endif ## Check for common size of X, K, and DF if (! isscalar (x) || ! isscalar (k) || ! isscalar (df)) [err, x, k, df] = common_size (x, k, df); if (err > 0) error ("stdrpdf: X, K, and DF must be of common size or scalars."); endif endif ## Check for X, K, and DF being double or single if (! (isfloat (x) && isfloat (k) && isfloat (df))) error ("stdrpdf: X, K, and DF must be double or single."); endif ## Check for X, K, and DF being reals if (iscomplex (x) || iscomplex (k) || iscomplex (df)) error ("stdrpdf: X, K, and DF must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (k, 'single') || isa (df, 'single')) cls = 'single'; else cls = 'double'; endif sz = size (x); x = double (x(:)); k = double (k(:)) .* ones (numel (x), 1); df = double (df(:)) .* ones (numel (x), 1); ## Invalid parameters or X give NaN valid = (k >= 2) & (k == fix (k)) & isfinite (k) & (df > 0); y = zeros (numel (x), 1); y(! valid | isnan (x)) = NaN; ## At zero only two groups have a density: their range is sqrt (2) times ## the absolute value of a t variable z = valid & (x == 0) & (k == 2); y(z) = sqrt (2) * tpdf (0, df(z)); ## Evaluate the positive finite values, one parameter pair at a time todo = valid & (x > 0) & isfinite (x); if (any (todo)) pairs = unique ([k(todo), df(todo)], 'rows'); for i = 1:rows (pairs) idx = todo & (k == pairs(i,1)) & (df == pairs(i,2)); y(idx) = __stdr__ (x(idx), pairs(i,1), pairs(i,2), 'pdf'); endfor endif y = cast (reshape (y, sz), cls); endfunction %!demo %! ## Plot various PDFs from the studentized range distribution %! x = 0:0.01:8; %! y1 = stdrpdf (x, 2, 5); %! y2 = stdrpdf (x, 3, 5); %! y3 = stdrpdf (x, 5, 5); %! y4 = stdrpdf (x, 5, Inf); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', x, y4, '-m') %! grid on %! legend ({'k = 2, df = 5', 'k = 3, df = 5', 'k = 5, df = 5', ... %! 'k = 5, df = \infty'}, 'location', 'northeast') %! title ('Studentized range PDF') %! xlabel ('values in x') %! ylabel ('density') ## Two groups are sqrt (2) times the absolute value of a t variable %!shared x %! x = [0.5, 2, 4, 8]; %!assert_equal (stdrpdf (x, 2, 5), sqrt (2) * tpdf (x / sqrt (2), 5), -1e-13) %!assert_equal (stdrpdf (x, 2, Inf), exp (-x .^ 2 / 4) / sqrt (pi), -1e-13) %!assert_equal (stdrpdf (0, 2, 5), sqrt (2) * tpdf (0, 5), -1e-13) %!assert_equal (stdrpdf (0, 3, 5), 0) ## The density is the derivative of the distribution function %!test %! h = 1e-4; %! d = (stdrcdf (3 + h, 5, 10) - stdrcdf (3 - h, 5, 10)) / (2 * h); %! assert_equal (stdrpdf (3, 5, 10), d, -1e-7); ## Edge values and invalid parameters %!assert_equal (stdrpdf ([-1, Inf, NaN], 3, 10), [0, 0, NaN]) %!assert_equal (stdrpdf (2, [1, 2.5, NaN, Inf], 10), NaN (1, 4)) %!assert_equal (stdrpdf (2, 3, [0, -1, NaN]), NaN (1, 3)) ## Test class of input preserved %!assert_equal (class (stdrpdf (single (2), 3, 10)), 'single') %!assert_equal (class (stdrpdf (2, 3, single (10))), 'single') ## Test input validation %!error stdrpdf () %!error stdrpdf (1, 2) %!error ... %! stdrpdf (ones (3), ones (2), 3) %!error ... %! stdrpdf (int32 (2), 3, 10) %!error ... %! stdrpdf (true, 3, 10) %!error ... %! stdrpdf ('a', 3, 10) %!error stdrpdf (i, 3, 10) %!error stdrpdf (2, i, 10) %!error stdrpdf (2, 3, i) statistics-release-2.1.2/inst/Distribution_Functions/stdrrnd.m000066400000000000000000000155331527311104300246520ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} stdrrnd (@var{k}, @var{df}) ## @deftypefnx {statistics} {@var{r} =} stdrrnd (@var{k}, @var{df}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} stdrrnd (@var{k}, @var{df}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} stdrrnd (@var{k}, @var{df}, [@var{sz}]) ## ## Random arrays from the studentized range distribution. ## ## @code{@var{r} = stdrrnd (@var{k}, @var{df})} returns an array of random ## numbers chosen from the studentized range distribution for @var{k} groups ## and @var{df} degrees of freedom: the range of @var{k} standard normal ## variables divided by an independent @math{sqrt (chi2 / df)}. The size of ## @var{r} is the common size of @var{k} and @var{df}. A scalar input functions ## as a constant matrix of the same size as the other input. @var{k} must be an ## integer of at least 2 and @var{df} positive, @code{Inf} included; otherwise ## @code{NaN} is returned. ## ## When called with a single size argument, @code{stdrrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## MATLAB has no public counterpart of this function. ## ## Further information about the studentized range distribution can be found ## at @url{https://en.wikipedia.org/wiki/Studentized_range_distribution} ## ## @seealso{stdrcdf, stdrinv, stdrpdf, stdrstat} ## @end deftypefn function r = stdrrnd (k, df, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("stdrrnd: function called with too few input arguments."); endif ## Check for common size of K and DF if (! isscalar (k) || ! isscalar (df)) [retval, k, df] = common_size (k, df); if (retval > 0) error ("stdrrnd: K and DF must be of common size or scalars."); endif endif ## Check for K and DF being reals if (iscomplex (k) || iscomplex (df)) error ("stdrrnd: K and DF must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (k); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("stdrrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("stdrrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (k) && ! isequal (size (k), size (ones (sz)))) error ("stdrrnd: K and DF must be scalars or of size SZ."); endif ## Check for class type if (isa (k, 'single') || isa (df, 'single')) cls = 'single'; else cls = 'double'; endif ## One draw per element: the range of k standard normals over the root of ## a chi-square on df degrees of freedom divided by df n = prod (sz); k = double (k(:)) .* ones (n, 1); df = double (df(:)) .* ones (n, 1); r = NaN (n, 1); valid = (k >= 2) & (k == fix (k)) & isfinite (k) & (df > 0); for kk = unique (k(valid))' idx = find (valid & k == kk); z = randn (kk, numel (idx)); w = max (z, [], 1)' - min (z, [], 1)'; s = ones (numel (idx), 1); fin = isfinite (df(idx)); s(fin) = sqrt (2 * randg (df(idx(fin)) / 2) ./ df(idx(fin))); r(idx) = w ./ s; endfor r = cast (reshape (r, sz), cls); endfunction %!demo %! ## Compare random samples with the density they are drawn from %! rng (42); %! r = stdrrnd (4, 10, 1, 10000); %! x = 0:0.05:10; %! hist (r, x, 1 / 0.05) %! hold on %! plot (x, stdrpdf (x, 4, 10), '-r', 'linewidth', 2) %! hold off %! xlim ([0, 10]) %! legend ({'10000 samples', 'k = 4, df = 10'}, 'location', 'northeast') %! title ('Studentized range random samples') %! xlabel ('values in r') %! ylabel ('density') ## Test output %!assert_equal (size (stdrrnd (3, 10)), [1, 1]) %!assert_equal (size (stdrrnd (3 * ones (2, 1), 10)), [2, 1]) %!assert_equal (size (stdrrnd (3, 10 * ones (2, 2))), [2, 2]) %!assert_equal (size (stdrrnd (3, 10, 3)), [3, 3]) %!assert_equal (size (stdrrnd (3, 10, [4, 1])), [4, 1]) %!assert_equal (size (stdrrnd (3, 10, 4, 1)), [4, 1]) %!assert_equal (size (stdrrnd (3, 10, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (stdrrnd (3, 10, 0, 1)), [0, 1]) %!assert_equal (size (stdrrnd (3, 10, [])), [0, 0]) %!assert_equal (size (stdrrnd (3, 10, [2, -1, 2])), [2, 0, 2]) %!assert_equal (stdrrnd ([1, 2.5, NaN], 10), [NaN, NaN, NaN]) %!assert_equal (stdrrnd (3, [0, -1, NaN]), [NaN, NaN, NaN]) %!assert_equal (all (stdrrnd (3, Inf, 1, 100) > 0), true) ## Test class of input preserved %!assert_equal (class (stdrrnd (3, 10)), 'double') %!assert_equal (class (stdrrnd (single (3), 10)), 'single') %!assert_equal (class (stdrrnd (3, single ([10, 10]))), 'single') ## Test input validation %!error stdrrnd () %!error stdrrnd (3) %!error ... %! stdrrnd (ones (3), ones (2)) %!error stdrrnd (i, 10) %!error stdrrnd (3, i) %!error ... %! stdrrnd (3, 10, 1.2) %!error ... %! stdrrnd (3, 10, ones (2)) %!error stdrrnd (3, 10, 2, 1.5) %!error ... %! stdrrnd (3 * ones (2), 10, 3) statistics-release-2.1.2/inst/Distribution_Functions/tcdf.m000066400000000000000000000207171527311104300241120ustar00rootroot00000000000000## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 2013-2017 Julien Bect ## Copyright (C) 2022-2025 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} tcdf (@var{x}, @var{df}) ## @deftypefnx {statistics} {@var{p} =} tcdf (@var{x}, @var{df}, @qcode{'upper'}) ## ## Student's T cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the Student's T distribution with @var{df} degrees of freedom. The ## size of @var{p} is the common size of @var{x} and @var{df}. A scalar input ## functions as a constant matrix of the same size as the other input. ## ## @code{@var{p} = tcdf (@var{x}, @var{df}, "upper")} computes the upper tail ## probability of the Student's T distribution with @var{df} degrees of freedom, ## at the values in @var{x}. ## ## Further information about the Student's T distribution can be found at ## @url{https://en.wikipedia.org/wiki/Student%27s_t-distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{tinv, tpdf, trnd, tstat} ## @end deftypefn function p = tcdf (x, df, uflag) ## Check for valid number of input arguments if (nargin < 2) error ("tcdf: function called with too few input arguments."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) x = -x; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("tcdf: invalid argument for upper tail."); endif ## Check for common size of X and DF if (! isscalar (x) || ! isscalar (df)) [err, x, df] = common_size (x, df); if (err > 0) error ("tcdf: X and DF must be of common size or scalars."); endif endif ## Check for X and DF being double or single if (! (isfloat (x) && isfloat (df))) error ("tcdf: X and DF must be double or single."); endif ## Check for X and DF being reals if (iscomplex (x) || iscomplex (df)) error ("tcdf: X and DF must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (df, 'single')) p = zeros (size (x), 'single'); else p = zeros (size (x)); endif ## Check for NaNs or DF <= 0 is_nan = isnan (x) | ! (df > 0); p(is_nan) = NaN; ## Check for Inf where DF > 0 (for -Inf is already 0) k = (x == Inf) & (df > 0); p(k) = 1; ## Find finite values in X where 0 < DF < Inf k = isfinite (x) & (df > 0) & (df < Inf); ## Distinguish between small and big abs(x) xx = x .^ 2; x_big_abs = (xx > df); ## Deal with the case "abs(x) big" kk = k & x_big_abs; if (isscalar (df)) p(kk) = betainc (df ./ (df + xx(kk)), df/2, 1/2) / 2; else p(kk) = betainc (df(kk) ./ (df(kk) + xx(kk)), df(kk)/2, 1/2) / 2; endif ## Deal with the case "abs(x) small" kk = k & ! x_big_abs; if (isscalar (df)) p(kk) = 0.5 * betainc (xx(kk) ./ (df + xx(kk)), 1/2, df/2, 'upper'); else p(kk) = 0.5 * betainc (xx(kk) ./ (df(kk) + xx(kk)), 1/2, df(kk)/2, ... 'upper'); endif ## For x > 0, F(x) = 1 - F(-|x|). k &= (x > 0); if (any (k(:))) p(k) = 1 - p(k); endif ## Special case for Cauchy distribution ## Use acot(-x) instead of the usual (atan x)/pi + 0.5 to avoid roundoff error xpos = (x > 0); c = (df == 1); p(c) = xpos(c) + acot (-x(c)) / pi; ## Special case for DF == Inf k = isfinite (x) & (df == Inf); p(k) = normcdf (x(k)); ## Make the result exact for the median p(x == 0 & ! is_nan) = 0.5; endfunction %!demo %! ## Plot various CDFs from the Student's T distribution %! x = -5:0.01:5; %! p1 = tcdf (x, 1); %! p2 = tcdf (x, 2); %! p3 = tcdf (x, 5); %! p4 = tcdf (x, Inf); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', x, p4, '-m') %! grid on %! xlim ([-5, 5]) %! ylim ([0, 1]) %! legend ({'df = 1', 'df = 2', ... %! 'df = 5', 'df = \infty'}, 'location', 'southeast') %! title ('Student''s T CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x,y %! x = [-Inf 0 1 Inf]; %! y = [0 1/2 3/4 1]; %!assert_equal (tcdf (x, ones (1,4)), y, eps) %!assert_equal (tcdf (x, 1), y, eps) %!assert_equal (tcdf (x, [0 1 NaN 1]), [NaN 1/2 NaN 1], eps) %!assert_equal (tcdf ([x(1:2) NaN x(4)], 1), [y(1:2) NaN y(4)], eps) %!assert_equal (tcdf (2, 3, 'upper'), 0.0697, 1e-4) %!assert_equal (tcdf (205, 5, 'upper'), 2.6206e-11, 1e-14) ## Test class of input preserved %!assert_equal (tcdf ([x, NaN], 1), [y, NaN], eps) %!assert_equal (tcdf (single ([x, NaN]), 1), single ([y, NaN]), eps ('single')) %!assert_equal (tcdf ([x, NaN], single (1)), single ([y, NaN]), eps ('single')) ## Test input validation %!error tcdf () %!error tcdf (1) %!error tcdf (1, 2, 'uper') %!error tcdf (1, 2, 3) %!error ... %! tcdf (ones (3), ones (2)) %!error ... %! tcdf (ones (3), ones (2)) %!error ... %! tcdf (ones (3), ones (2), 'upper') %!error tcdf (int32 (2), 2) %!error tcdf (true, 2) %!error tcdf ('a', 2) %!error tcdf (i, 2) %!error tcdf (2, i) ## Check some reference values %!shared tol_rel %! tol_rel = 10 * eps; ## check accuracy for small positive values %!assert_equal (tcdf (10^(-10), 2.5), 0.50000000003618087, -tol_rel) %!assert_equal (tcdf (10^(-11), 2.5), 0.50000000000361809, -tol_rel) %!assert_equal (tcdf (10^(-12), 2.5), 0.50000000000036181, -tol_rel) %!assert_equal (tcdf (10^(-13), 2.5), 0.50000000000003618, -tol_rel) %!assert_equal (tcdf (10^(-14), 2.5), 0.50000000000000362, -tol_rel) %!assert_equal (tcdf (10^(-15), 2.5), 0.50000000000000036, -tol_rel) %!assert_equal (tcdf (10^(-16), 2.5), 0.50000000000000004, -tol_rel) ## check accuracy for large negative values %!assert_equal (tcdf (-10^1, 2.5), 2.2207478836537124e-03, -tol_rel) %!assert_equal (tcdf (-10^2, 2.5), 7.1916492116661878e-06, -tol_rel) %!assert_equal (tcdf (-10^3, 2.5), 2.2747463948307452e-08, -tol_rel) %!assert_equal (tcdf (-10^4, 2.5), 7.1933970159922115e-11, -tol_rel) %!assert_equal (tcdf (-10^5, 2.5), 2.2747519231756221e-13, -tol_rel) ## Lower tail at whole DF, values from MATLAB R2024a %!assert_equal (tcdf (-17.138, 147), 3.59494847838787e-37, -1e-13) %!assert_equal (tcdf (17.138, 147, 'upper'), 3.59494847838787e-37, -1e-13) %!assert_equal (tcdf (-8, 147), 1.70674966444924e-13, -1e-13) %!assert_equal (tcdf (-1e10, 1), 3.18309886183791e-11, -1e-14) %!assert_equal (tcdf (-1e5, 2), 4.99999999925002e-11, -1e-14) %!assert_equal (tcdf (-1000, 3), 1.1026538212883e-09, -1e-13) ## # Reference values obtained using Python 2.7.4 and mpmath 0.17 ## ## from mpmath import * ## ## mp.dps = 100 ## ## def F(x_in, nu_in): ## x = mpf(x_in); ## nu = mpf(nu_in); ## t = nu / (nu + x*x) ## a = nu / 2 ## b = mpf(0.5) ## F = betainc(a, b, 0, t, regularized=True) / 2 ## if (x > 0): ## F = 1 - F ## return F ## ## nu = 2.5 ## ## for i in range(1, 6): ## x = - power(mpf(10), mpf(i)) ## print "%%!assert_equal (tcdf (-10^%d, 2.5), %s, -eps)" \ ## % (i, nstr(F(x, nu), 17)) ## ## for i in range(10, 17): ## x = power(mpf(10), -mpf(i)) ## print "%%!assert_equal (tcdf (10^(-%d), 2.5), %s, -eps)" \ ## % (i, nstr(F(x, nu), 17)) statistics-release-2.1.2/inst/Distribution_Functions/tinv.m000066400000000000000000000145321527311104300241500ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} tinv (@var{p}, @var{df}) ## ## Inverse of the Student's T cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the Student's T distribution with @var{df} degrees of freedom. The size of ## @var{x} is the common size of @var{x} and @var{df}. A scalar input functions ## as a constant matrix of the same size as the other input. ## ## This function is analogous to looking in a table for the t-value of a ## single-tailed distribution. For very large @var{df} (>10000), the inverse of ## the standard normal distribution is used. ## ## Further information about the Student's T distribution can be found at ## @url{https://en.wikipedia.org/wiki/Student%27s_t-distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## Far in the tails the quantile stays accurate to the last few digits. ## MATLAB's does not with one degree of freedom, where it evaluates ## @code{tan (pi (p - 0.5))} and @math{p - 0.5} loses the digits that ## matter: at @math{p = 10^{-10}} it is off by @math{5 10^{-7}}, relative. ## ## @seealso{tcdf, tpdf, trnd, tstat} ## @end deftypefn function x = tinv (p, df) ## Check for valid number of input arguments if (nargin < 2) error ("tinv: function called with too few input arguments."); endif ## Check for common size of P and DF if (! isscalar (p) || ! isscalar (df)) [retval, p, df] = common_size (p, df); if (retval > 0) error ("tinv: P and DF must be of common size or scalars."); endif endif ## Check for P and DF being double or single if (! (isfloat (p) && isfloat (df))) error ("tinv: P and DF must be double or single."); endif ## Check for P and DF being reals if (iscomplex (p) || iscomplex (df)) error ("tinv: P and DF must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (df, 'single')) x = NaN (size (p), 'single'); else x = NaN (size (p)); endif k = (p == 0) & (df > 0); x(k) = -Inf; k = (p == 1) & (df > 0); x(k) = Inf; if (isscalar (df)) k = (p > 0) & (p < 1); if ((df > 0) && (df < 10000)) x(k) = tQuantile (p(k), repmat (df, size (p(k)))); elseif (df >= 10000) ## For large df, use the quantiles of the standard normal x(k) = -sqrt (2) * erfcinv (2 * p(k)); endif else k = (p > 0) & (p < 1) & (df > 0) & (df < 10000); x(k) = tQuantile (p(k), df(k)); ## For large df, use the quantiles of the standard normal k = (p > 0) & (p < 1) & (df >= 10000); x(k) = -sqrt (2) * erfcinv (2 * p(k)); endif endfunction ## DF / (DF + T^2) is Beta (DF/2, 1/2) with lower tail Q, the probability of ## both tails beyond T; where Q is above one half, T^2 / (DF + T^2) is solved ## instead as Beta (1/2, DF/2) at the exact 1 - Q, which would otherwise ## cancel as T nears 0 function x = tQuantile (p, df) q = 2 * min (p, 1 - p); c = q > 0.5; t2 = zeros (size (p), class (p)); z = betainv (q(! c), df(! c) / 2, 1/2); t2(! c) = df(! c) .* (1 - z) ./ z; w = betainv (abs (1 - 2 * p(c)), 1/2, df(c) / 2); t2(c) = df(c) .* w ./ (1 - w); x = sign (p - 1/2) .* sqrt (t2); endfunction %!demo %! ## Plot various iCDFs from the Student's T distribution %! p = 0.001:0.001:0.999; %! x1 = tinv (p, 1); %! x2 = tinv (p, 2); %! x3 = tinv (p, 5); %! x4 = tinv (p, Inf); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', p, x4, '-m') %! grid on %! xlim ([0, 1]) %! ylim ([-5, 5]) %! legend ({'df = 1', 'df = 2', ... %! 'df = 5', 'df = \infty'}, 'location', 'northwest') %! title ('Student''s T iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p %! p = [-1 0 0.5 1 2]; %!assert_equal (tinv (p, ones (1,5)), [NaN -Inf 0 Inf NaN]) %!assert_equal (tinv (p, 1), [NaN -Inf 0 Inf NaN], eps) %!assert_equal (tinv (p, [1 0 NaN 1 1]), [NaN NaN NaN Inf NaN], eps) %!assert_equal (tinv ([p(1:2) NaN p(4:5)], 1), [NaN -Inf NaN Inf NaN]) %!test %! ## Deep in the tail, -cot (pi p); MATLAB gives -3183097229.936, the value %! ## of tan (pi (p - 0.5)), whose p - 0.5 loses the digits that matter %! assert_equal (tinv (1e-10, 1), -cot (pi * 1e-10), -1e-14); %!assert_equal (tinv (0.5 + 2^-30, 1), tan (pi * 2^-30), -1e-14) %!assert_equal (tinv (0.5 - 2^-30, 2), ... %! -2^-29 / sqrt (2 * (0.5 - 2^-30) * (0.5 + 2^-30)), -1e-14) ## Test class of input preserved %!assert_equal (tinv ([p, NaN], 1), [NaN -Inf 0 Inf NaN NaN], eps) %!assert_equal (tinv (single ([p, NaN]), 1), single ([NaN -Inf 0 Inf NaN NaN]), eps ('single')) %!assert_equal (tinv ([p, NaN], single (1)), single ([NaN -Inf 0 Inf NaN NaN]), eps ('single')) ## Test input validation %!error tinv () %!error tinv (1) %!error ... %! tinv (ones (3), ones (2)) %!error ... %! tinv (ones (2), ones (3)) %!error tinv (int32 (2), 2) %!error tinv (true, 2) %!error tinv ('a', 2) %!error tinv (i, 2) %!error tinv (2, i) statistics-release-2.1.2/inst/Distribution_Functions/tlscdf.m000066400000000000000000000160321527311104300244440ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} tlscdf (@var{x}, @var{mu}, @var{sigma}, @var{nu}) ## @deftypefnx {statistics} {@var{p} =} tlscdf (@var{x}, @var{mu}, @var{sigma}, @var{nu}, @qcode{'upper'}) ## ## Location-scale Student's T cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the location-scale Student's T distribution with location parameter ## @var{mu}, scale parameter @var{sigma}, and @var{nu} degrees of freedom. The ## size of @var{p} is the common size of @var{x}, @var{mu}, @var{sigma}, and ## @var{nu}. A scalar input functions as a constant matrix of the same size as ## the other inputs. ## ## @code{@var{p} = tlscdf (@var{x}, @var{mu}, @var{sigma}, @var{nu}, "upper")} ## computes the upper tail probability of the location-scale Student's T ## distribution with parameters @var{mu}, @var{sigma}, and @var{nu}, at the ## values in @var{x}. ## ## Further information about the location-scale Student's T distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Student%27s_t-distribution#Location-scale_t_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{tlsinv, tlspdf, tlsrnd, tlsfit, tlslike, tlsstat} ## @end deftypefn function p = tlscdf (x, mu, sigma, nu, uflag) ## Check for valid number of input arguments if (nargin < 4) error ("tlscdf: function called with too few input arguments."); endif ## Check for "upper" flag upper = false; if (nargin > 4 && strcmpi (uflag, 'upper')) upper = true; elseif (nargin > 4 && ! strcmpi (uflag, 'upper')) error ("tlscdf: invalid argument for upper tail."); endif ## Check for common size of X, MU, SIGMA, and NU if (! isscalar (x) || ! isscalar (mu) || ! isscalar (sigma) || ! isscalar (nu)) [err, x, mu, sigma, nu] = common_size (x, mu, sigma, nu); if (err > 0) error ("tlscdf: X, MU, SIGMA, and NU must be of common size or scalars."); endif endif ## Check for X, MU, SIGMA, and NU being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (sigma) && isfloat (nu))) error ("tlscdf: X, MU, SIGMA, and NU must be double or single."); endif ## Check for X, MU, SIGMA, and NU being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (sigma) || iscomplex (nu)) error ("tlscdf: X, MU, SIGMA, and NU must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (sigma, 'single') || isa (nu, 'single')) cls = 'single'; else cls = 'double'; endif ## Force invalid SIGMA parameter to NaN sigma(sigma <= 0) = NaN; ## Call tcdf to do the work if (upper) p = tcdf ((x - mu) ./ sigma, nu, 'upper'); else p = tcdf ((x - mu) ./ sigma, nu); endif ## Force class type p = cast (p, cls); endfunction %!demo %! ## Plot various CDFs from the location-scale Student's T distribution %! x = -8:0.01:8; %! p1 = tlscdf (x, 0, 1, 1); %! p2 = tlscdf (x, 0, 2, 2); %! p3 = tlscdf (x, 3, 2, 5); %! p4 = tlscdf (x, -1, 3, Inf); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', x, p4, '-m') %! grid on %! xlim ([-8, 8]) %! ylim ([0, 1]) %! legend ({'mu = 0, sigma = 1, nu = 1', 'mu = 0, sigma = 2, nu = 2', ... %! 'mu = 3, sigma = 2, nu = 5', 'mu = -1, sigma = 3, nu = \infty'}, ... %! 'location', 'northwest') %! title ('Location-scale Student''s T CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x,y %! x = [-Inf 0 1 Inf]; %! y = [0 1/2 3/4 1]; %!assert_equal (tlscdf (x, 0, 1, ones (1,4)), y, eps) %!assert_equal (tlscdf (x, 0, 1, 1), y, eps) %!assert_equal (tlscdf (x, 0, 1, [0 1 NaN 1]), [NaN 1/2 NaN 1], eps) %!assert_equal (tlscdf ([x(1:2) NaN x(4)], 0, 1, 1), [y(1:2) NaN y(4)], eps) %!assert_equal (tlscdf (2, 0, 1, 3, 'upper'), 0.0697, 1e-4) %!assert_equal (tlscdf (205, 0, 1, 5, 'upper'), 2.6206e-11, 1e-14) ## Test class of input preserved %!assert_equal (tlscdf ([x, NaN], 0, 1, 1), [y, NaN], eps) %!assert_equal (tlscdf (single ([x, NaN]), 0, 1, 1), single ([y, NaN]), eps ('single')) %!assert_equal (tlscdf ([x, NaN], single (0), 1, 1), single ([y, NaN]), eps ('single')) %!assert_equal (tlscdf ([x, NaN], 0, single (1), 1), single ([y, NaN]), eps ('single')) %!assert_equal (tlscdf ([x, NaN], 0, 1, single (1)), single ([y, NaN]), eps ('single')) ## Test input validation %!error tlscdf () %!error tlscdf (1) %!error tlscdf (1, 2) %!error tlscdf (1, 2, 3) %!error tlscdf (1, 2, 3, 4, 'uper') %!error tlscdf (1, 2, 3, 4, 5) %!error ... %! tlscdf (ones (3), ones (2), 1, 1) %!error ... %! tlscdf (ones (3), 1, ones (2), 1) %!error ... %! tlscdf (ones (3), 1, 1, ones (2)) %!error ... %! tlscdf (ones (3), ones (2), 1, 1, 'upper') %!error ... %! tlscdf (ones (3), 1, ones (2), 1, 'upper') %!error ... %! tlscdf (ones (3), 1, 1, ones (2), 'upper') %!error tlscdf (int32 (2), 2, 1, 1) %!error tlscdf (true, 2, 1, 1) %!error tlscdf ('a', 2, 1, 1) %!error tlscdf (i, 2, 1, 1) %!error tlscdf (2, i, 1, 1) %!error tlscdf (2, 1, i, 1) %!error tlscdf (2, 1, 1, i) statistics-release-2.1.2/inst/Distribution_Functions/tlsinv.m000066400000000000000000000133411527311104300245040ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} tlsinv (@var{p}, @var{mu}, @var{sigma}, @var{nu}) ## ## Inverse of the location-scale Student's T cumulative distribution function ## (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the location-scale Student's T distribution with location parameter @var{mu}, ## scale parameter @var{sigma}, and @var{nu} degrees of freedom. The size of ## @var{x} is the common size of @var{p}, @var{mu}, @var{sigma}, and @var{nu}. ## A scalar input functions as a constant matrix of the same size as the other ## inputs. ## ## Further information about the location-scale Student's T distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Student%27s_t-distribution#Location-scale_t_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{tlscdf, tlspdf, tlsrnd, tlsfit, tlslike, tlsstat} ## @end deftypefn function x = tlsinv (p, mu, sigma, nu) ## Check for valid number of input arguments if (nargin < 4) error ("tlsinv: function called with too few input arguments."); endif ## Check for common size of P, MU, SIGMA, and NU if (! isscalar (p) || ! isscalar (mu) || ! isscalar (sigma) || ! isscalar (nu)) [retval, p, mu, sigma, nu] = common_size (p, mu, sigma, nu); if (retval > 0) error ("tlsinv: P, MU, SIGMA, and NU must be of common size or scalars."); endif endif ## Check for P, MU, SIGMA, and NU being double or single if (! (isfloat (p) && isfloat (mu) && isfloat (sigma) && isfloat (nu))) error ("tlsinv: P, MU, SIGMA, and NU must be double or single."); endif ## Check for P, MU, SIGMA, and NU being reals if (iscomplex (p) || iscomplex (mu) || iscomplex (sigma) || iscomplex (nu)) error ("tlsinv: P, MU, SIGMA, and NU must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (mu, 'single') || isa (sigma, 'single') || isa (nu, 'single')) cls = 'single'; else cls = 'double'; endif ## Force invalid SIGMA parameter to NaN sigma(sigma <= 0) = NaN; ## Call tinv to do the work x = tinv (p, nu) .* sigma + mu; ## Force class type x = cast (x, cls); endfunction %!demo %! ## Plot various iCDFs from the location-scale Student's T distribution %! p = 0.001:0.001:0.999; %! x1 = tlsinv (p, 0, 1, 1); %! x2 = tlsinv (p, 0, 2, 2); %! x3 = tlsinv (p, 3, 2, 5); %! x4 = tlsinv (p, -1, 3, Inf); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', p, x4, '-m') %! grid on %! xlim ([0, 1]) %! ylim ([-8, 8]) %! legend ({'mu = 0, sigma = 1, nu = 1', 'mu = 0, sigma = 2, nu = 2', ... %! 'mu = 3, sigma = 2, nu = 5', 'mu = -1, sigma = 3, nu = \infty'}, ... %! 'location', 'southeast') %! title ('Location-scale Student''s T iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p %! p = [-1 0 0.5 1 2]; %!assert_equal (tlsinv (p, 0, 1, ones (1,5)), [NaN -Inf 0 Inf NaN]) %!assert_equal (tlsinv (p, 0, 1, 1), [NaN -Inf 0 Inf NaN], eps) %!assert_equal (tlsinv (p, 0, 1, [1 0 NaN 1 1]), [NaN NaN NaN Inf NaN], eps) %!assert_equal (tlsinv ([p(1:2) NaN p(4:5)], 0, 1, 1), [NaN -Inf NaN Inf NaN]) ## Test class of input preserved %!assert_equal (class (tlsinv ([p, NaN], 0, 1, 1)), "double") %!assert_equal (class (tlsinv (single ([p, NaN]), 0, 1, 1)), "single") %!assert_equal (class (tlsinv ([p, NaN], single (0), 1, 1)), "single") %!assert_equal (class (tlsinv ([p, NaN], 0, single (1), 1)), "single") %!assert_equal (class (tlsinv ([p, NaN], 0, 1, single (1))), "single") ## Test input validation %!error tlsinv () %!error tlsinv (1) %!error tlsinv (1, 2) %!error tlsinv (1, 2, 3) %!error ... %! tlsinv (ones (3), ones (2), 1, 1) %!error ... %! tlsinv (ones (2), 1, ones (3), 1) %!error ... %! tlsinv (ones (2), 1, 1, ones (3)) %!error tlsinv (int32 (2), 2, 3, 4) %!error tlsinv (true, 2, 3, 4) %!error tlsinv ('a', 2, 3, 4) %!error tlsinv (i, 2, 3, 4) %!error tlsinv (2, i, 3, 4) %!error tlsinv (2, 2, i, 4) %!error tlsinv (2, 2, 3, i) statistics-release-2.1.2/inst/Distribution_Functions/tlspdf.m000066400000000000000000000135541527311104300244670ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} tlspdf (@var{x}, @var{mu}, @var{sigma}, @var{nu}) ## ## Location-scale Student's T probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the location-scale Student's T distribution with location parameter ## @var{mu}, scale parameter @var{sigma}, and @var{nu} degrees of freedom. The ## size of @var{y} is the common size of @var{x}, @var{mu}, @var{sigma}, and ## @var{nu}. A scalar input functions as a constant matrix of the same size as ## the other inputs. ## ## Further information about the location-scale Student's T distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Student%27s_t-distribution#Location-scale_t_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{tlscdf, tlsinv, tlsrnd, tlsfit, tlslike, tlsstat} ## @end deftypefn function y = tlspdf (x, mu, sigma, nu) ## Check for valid number of input arguments if (nargin < 4) error ("tlspdf: function called with too few input arguments."); endif ## Check for common size of X, MU, SIGMA, and NU if (! isscalar (x) || ! isscalar (mu) || ! isscalar (sigma) || ! isscalar (nu)) [err, x, mu, sigma, nu] = common_size (x, mu, sigma, nu); if (err > 0) error ("tlspdf: X, MU, SIGMA, and NU must be of common size or scalars."); endif endif ## Check for X, MU, SIGMA, and NU being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (sigma) && isfloat (nu))) error ("tlspdf: X, MU, SIGMA, and NU must be double or single."); endif ## Check for X, MU, SIGMA, and NU being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (sigma) || iscomplex (nu)) error ("tlspdf: X, MU, SIGMA, and NU must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (sigma, 'single') || isa (nu, 'single')) cls = 'single'; else cls = 'double'; endif ## Force invalid SIGMA parameter to NaN sigma(sigma <= 0) = NaN; ## Call tpdf to do the work y = tpdf ((x - mu) ./ sigma, nu) ./ sigma; ## Force class type y = cast (y, cls); endfunction %!demo %! ## Plot various PDFs from the Student's T distribution %! x = -8:0.01:8; %! y1 = tlspdf (x, 0, 1, 1); %! y2 = tlspdf (x, 0, 2, 2); %! y3 = tlspdf (x, 3, 2, 5); %! y4 = tlspdf (x, -1, 3, Inf); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', x, y4, '-m') %! grid on %! xlim ([-8, 8]) %! ylim ([0, 0.41]) %! legend ({'mu = 0, sigma = 1, nu = 1', 'mu = 0, sigma = 2, nu = 2', ... %! 'mu = 3, sigma = 2, nu = 5', 'mu = -1, sigma = 3, nu = \infty'}, ... %! 'location', 'northwest') %! title ('Location-scale Student''s T PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!test %! x = rand (10,1); %! y = 1./(pi * (1 + x.^2)); %! assert_equal (tlspdf (x, 0, 1, 1), y, 5*eps); %! assert_equal (tlspdf (x+5, 5, 1, 1), y, 5*eps); %! assert_equal (tlspdf (x.*2, 0, 2, 1), y./2, 5*eps); %!shared x, y %! x = [-Inf 0 0.5 1 Inf]; %! y = 1./(pi * (1 + x.^2)); %!assert_equal (tlspdf (x, 0, 1, ones (1,5)), y, eps) %!assert_equal (tlspdf (x, 0, 1, 1), y, eps) %!assert_equal (tlspdf (x, 0, 1, [0 NaN 1 1 1]), [NaN NaN y(3:5)], eps) %!assert_equal (tlspdf (x, 0, 1, Inf), normpdf (x)) ## Test class of input preserved %!assert_equal (class (tlspdf ([x, NaN], 1, 1, 1)), "double") %!assert_equal (class (tlspdf (single ([x, NaN]), 1, 1, 1)), "single") %!assert_equal (class (tlspdf ([x, NaN], single (1), 1, 1)), "single") %!assert_equal (class (tlspdf ([x, NaN], 1, single (1), 1)), "single") %!assert_equal (class (tlspdf ([x, NaN], 1, 1, single (1))), "single") ## Test input validation %!error tlspdf () %!error tlspdf (1) %!error tlspdf (1, 2) %!error tlspdf (1, 2, 3) %!error ... %! tlspdf (ones (3), ones (2), 1, 1) %!error ... %! tlspdf (ones (2), 1, ones (3), 1) %!error ... %! tlspdf (ones (2), 1, 1, ones (3)) %!error tlspdf (int32 (2), 2, 1, 1) %!error tlspdf (true, 2, 1, 1) %!error tlspdf ('a', 2, 1, 1) %!error tlspdf (i, 2, 1, 1) %!error tlspdf (2, i, 1, 1) %!error tlspdf (2, 1, i, 1) %!error tlspdf (2, 1, 1, i) statistics-release-2.1.2/inst/Distribution_Functions/tlsrnd.m000066400000000000000000000173361527311104300245030ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} tlsrnd (@var{mu}, @var{sigma}, @var{nu}) ## @deftypefnx {statistics} {@var{r} =} tlsrnd (@var{mu}, @var{sigma}, @var{nu}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} tlsrnd (@var{mu}, @var{sigma}, @var{nu}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} tlsrnd (@var{mu}, @var{sigma}, @var{nu}, [@var{sz}]) ## ## Random arrays from the location-scale Student's T distribution. ## ## Return a matrix of random samples from the location-scale Student's T ## distribution with location parameter @var{mu}, scale parameter @var{sigma}, ## and @var{nu} degrees of freedom. ## ## @code{@var{r} = tlsrnd (@var{nu})} returns an array of random numbers chosen ## from the location-scale Student's T distribution with location parameter ## @var{mu}, scale parameter @var{sigma}, and @var{nu} degrees of freedom. The ## size of @var{r} is the common size of @var{mu}, @var{sigma}, and @var{nu}. A ## scalar input functions as a constant matrix of the same size as the other ## inputs. ## ## When called with a single size argument, @code{tlsrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the location-scale Student's T distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Student%27s_t-distribution#Location-scale_t_distribution} ## ## @seealso{tlscdf, tlsinv, tlspdf, tlsfit, tlslike, tlsstat} ## @end deftypefn function r = tlsrnd (mu, sigma, nu, varargin) ## Check for valid number of input arguments if (nargin < 3) error ("tlsrnd: function called with too few input arguments."); endif ## Check for common size of MU, SIGMA, and NU if (! isscalar (mu) || ! isscalar (sigma) || ! isscalar (nu)) [retval, mu, sigma, nu] = common_size (mu, sigma, nu); if (retval > 0) error ("tlsrnd: MU, SIGMA, and NU must be of common size or scalars."); endif endif ## Check for NU being real if (iscomplex (mu) || iscomplex (sigma) || iscomplex (nu)) error ("tlsrnd: MU, SIGMA, and NU must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 3) sz = size (nu); elseif (nargin == 4) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("tlsrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 4) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("tlsrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (nu) && ! isequal (size (nu), size (ones (sz)))) error ("tlsrnd: MU, SIGMA, and NU must be scalar or of size SZ."); endif ## Check for class type if (isa (mu, 'single') || isa (sigma, 'single') || isa (nu, 'single')) cls = 'single'; else cls = 'double'; endif ## Call trnd to do the work r = mu + sigma .* trnd (nu, sz); ## Force class type r = cast (r, cls); endfunction ## Test output %!assert_equal (size (tlsrnd (1, 2, 3)), [1, 1]) %!assert_equal (size (tlsrnd (ones (2, 1), 2, 3)), [2, 1]) %!assert_equal (size (tlsrnd (ones (2, 2), 2, 3)), [2, 2]) %!assert_equal (size (tlsrnd (1, 2, 3, 3)), [3, 3]) %!assert_equal (size (tlsrnd (1, 2, 3, [4, 1])), [4, 1]) %!assert_equal (size (tlsrnd (1, 2, 3, 4, 1)), [4, 1]) %!assert_equal (size (tlsrnd (1, 2, 3, 4, 1)), [4, 1]) %!assert_equal (size (tlsrnd (1, 2, 3, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (tlsrnd (1, 2, 3, 0, 1)), [0, 1]) %!assert_equal (size (tlsrnd (1, 2, 3, 1, 0)), [1, 0]) %!assert_equal (size (tlsrnd (1, 2, 3, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (tlsrnd (1, 2, 3, [])), [0, 0]) %!assert_equal (size (tlsrnd (1, 2, 3, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (tlsrnd (1, 2, 3, -1)), [0, 0]) %!assert_equal (size (tlsrnd (1, 2, 3, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (tlsrnd (1, 2, 3, 2, -1, 5)), [2, 0, 5]) %!assert_equal (tlsrnd (1, 2, 0, 1, 1), NaN) %!assert_equal (tlsrnd (1, 2, [0, 0, 0], [1, 3]), [NaN, NaN, NaN]) ## Test class of input preserved %!assert_equal (class (tlsrnd (1, 2, 3)), "double") %!assert_equal (class (tlsrnd (single (1), 2, 3)), "single") %!assert_equal (class (tlsrnd (single ([1, 1]), 2, 3)), "single") %!assert_equal (class (tlsrnd (1, single (2), 3)), "single") %!assert_equal (class (tlsrnd (1, single ([2, 2]), 3)), "single") %!assert_equal (class (tlsrnd (1, 2, single (3))), "single") %!assert_equal (class (tlsrnd (1, 2, single ([3, 3]))), "single") ## Test input validation %!error tlsrnd () %!error tlsrnd (1) %!error tlsrnd (1, 2) %!error ... %! tlsrnd (ones (3), ones (2), 1) %!error ... %! tlsrnd (ones (2), 1, ones (3)) %!error ... %! tlsrnd (1, ones (2), ones (3)) %!error tlsrnd (i, 2, 3) %!error tlsrnd (1, i, 3) %!error tlsrnd (1, 2, i) %!error ... %! tlsrnd (1, 2, 3, 1.2) %!error ... %! tlsrnd (1, 2, 3, ones (2)) %!error ... %! tlsrnd (1, 2, 3, [2 0 2.5]) %!error ... %! tlsrnd (ones (2), 2, 3, ones (2)) %!error ... %! tlsrnd (1, 2, 3, 2, 1.5, 5) %!error ... %! tlsrnd (ones (2,2), 2, 3, 3) %!error ... %! tlsrnd (1, ones (2,2), 3, 3) %!error ... %! tlsrnd (1, 2, ones (2,2), 3) %!error ... %! tlsrnd (1, 2, ones (2,2), [3, 3]) %!error ... %! tlsrnd (1, 2, ones (2,2), 2, 3) statistics-release-2.1.2/inst/Distribution_Functions/tpdf.m000066400000000000000000000111721527311104300241220ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} tpdf (@var{x}, @var{df}) ## ## Student's T probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the Student's T distribution with @var{df} degrees of freedom. The size ## of @var{y} is the common size of @var{x} and @var{df}. A scalar input ## functions as a constant matrix of the same size as the other input. ## ## Further information about the Student's T distribution can be found at ## @url{https://en.wikipedia.org/wiki/Student%27s_t-distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{tcdf, tinv, trnd, tstat} ## @end deftypefn function y = tpdf (x, df) ## Check for valid number of input arguments if (nargin < 2) error ("tpdf: function called with too few input arguments."); endif ## Check for common size of X and DF if (! isscalar (x) || ! isscalar (df)) [retval, x, df] = common_size (x, df); if (retval > 0) error ("tpdf: X and DF must be of common size or scalars."); endif endif ## Check for X and DF being double or single if (! (isfloat (x) && isfloat (df))) error ("tpdf: X and DF must be double or single."); endif ## Check for X and DF being reals if (iscomplex (x) || iscomplex (df)) error ("tpdf: X and DF must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (df, 'single')) y = zeros (size (x), 'single'); else y = zeros (size (x)); endif k = isnan (x) | ! (df > 0); y(k) = NaN; k = isfinite (x) & (df > 0) & (df < Inf); kinf = isfinite (x) & isinf (df); if (any (k)) y(k) = exp (- (df(k) + 1) .* log (1 + x(k) .^ 2 ./ df(k)) / 2) ./ ... (sqrt (df(k)) .* beta (df(k)/2, 1/2)); endif if (any (kinf)) y(kinf) = normpdf (x(kinf)); endif endfunction %!demo %! ## Plot various PDFs from the Student's T distribution %! x = -5:0.01:5; %! y1 = tpdf (x, 1); %! y2 = tpdf (x, 2); %! y3 = tpdf (x, 5); %! y4 = tpdf (x, Inf); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', x, y4, '-m') %! grid on %! xlim ([-5, 5]) %! ylim ([0, 0.41]) %! legend ({'df = 1', 'df = 2', ... %! 'df = 5', 'df = \infty'}, 'location', 'northeast') %! title ('Student''s T PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!test %! x = rand (10,1); %! y = 1./(pi * (1 + x.^2)); %! assert_equal (tpdf (x, 1), y, 5*eps); %!shared x, y %! x = [-Inf 0 0.5 1 Inf]; %! y = 1./(pi * (1 + x.^2)); %!assert_equal (tpdf (x, ones (1,5)), y, eps) %!assert_equal (tpdf (x, 1), y, eps) %!assert_equal (tpdf (x, [0 NaN 1 1 1]), [NaN NaN y(3:5)], eps) %!assert_equal (tpdf (x, Inf), normpdf (x)) ## Test class of input preserved %!assert_equal (tpdf ([x, NaN], 1), [y, NaN], eps) %!assert_equal (tpdf (single ([x, NaN]), 1), single ([y, NaN]), eps ('single')) %!assert_equal (tpdf ([x, NaN], single (1)), single ([y, NaN]), eps ('single')) ## Test input validation %!error tpdf () %!error tpdf (1) %!error ... %! tpdf (ones (3), ones (2)) %!error ... %! tpdf (ones (2), ones (3)) %!error tpdf (int32 (2), 2) %!error tpdf (true, 2) %!error tpdf ('a', 2) %!error tpdf (i, 2) %!error tpdf (2, i) statistics-release-2.1.2/inst/Distribution_Functions/tricdf.m000066400000000000000000000202251527311104300244370ustar00rootroot00000000000000## Copyright (C) 1997-2015 Kurt Hornik ## Copyright (C) 2016 Dag Lyberg ## Copyright (C) 2023-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} tricdf (@var{x}, @var{a}, @var{b}, @var{c}) ## @deftypefnx {statistics} {@var{p} =} tricdf (@var{x}, @var{a}, @var{b}, @var{c}, @qcode{'upper'}) ## ## Triangular cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the triangular distribution with lower limit parameter @var{a}, peak ## location (mode) parameter @var{b}, and upper limit parameter @var{c}. The ## size of @var{p} is the common size of the input arguments. A scalar input ## functions as a constant matrix of the same size as the other inputs. ## ## @code{@var{p} = tricdf (@var{x}, @var{a}, @var{b}, @var{c}, "upper")} ## computes the upper tail probability of the triangular distribution with ## parameters @var{a}, @var{b}, and @var{c}, at the values in @var{x}. ## ## Note that the order of the parameter input arguments has been changed after ## statistics version 1.6.3 in order to be MATLAB compatible with the parameters ## used in the TriangularDistribution probability distribution object. More ## specifically, the positions of the parameters @var{b} and @var{c} have been ## swapped. As a result, the naming conventions no longer coincide with those ## used in Wikipedia, in which @math{b} denotes the upper limit and @math{c} ## denotes the mode or peak parameter. ## ## Further information about the triangular distribution can be found at ## @url{https://en.wikipedia.org/wiki/Triangular_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## MATLAB also accepts integer input here, returning the result in the integer ## class of the input; Octave rejects it, as it does for every other continuous ## distribution. ## ## @seealso{triinv, tripdf, trirnd, tristat} ## @end deftypefn function p = tricdf (x, a, b, c, uflag) ## Check for valid number of input arguments if (nargin < 4) error ("tricdf: function called with too few input arguments."); endif ## Check for valid "upper" flag if (nargin > 4) if (! strcmpi (uflag, 'upper')) error ("tricdf: invalid argument for upper tail."); else uflag = true; endif else uflag = false; endif ## Check for common size of A, B, and C if (! isscalar (x) || ! isscalar (a) || ! isscalar (b) || ! isscalar (c)) [retval, x, a, b, c] = common_size (x, a, b, c); if (retval > 0) error ("tricdf: X, A, B, and C must be of common size or scalars."); endif endif ## Check for X, A, B, and C being double or single if (! (isfloat (x) && isfloat (a) && isfloat (b) && isfloat (c))) error ("tricdf: X, A, B, and C must be double or single."); endif ## Check for X, BETA, and GAMMA being reals if (iscomplex (x) || iscomplex (a) || iscomplex (b) || iscomplex (c)) error ("tricdf: X, A, B, and C must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (a, 'single') || isa (b, 'single') ... || isa (c, 'single')) p = zeros (size (x), 'single'); else p = zeros (size (x)); endif ## Force NaNs for out of range parameters. k = isnan (x) | ! (a < c) | ! (b >= a) | ! (b <= c); p(k) = NaN; ## Find valid values in parameters and data k = (a < c) & (a <= b) & (b <= c); k1 = (x <= a) & k; k2 = (x > a) & (x <= b) & k; k3 = (x > b) & (x < c) & k; k4 = (x >= c) & k; ## Compute triangular CDF if (uflag) p(k1) = 1; p(k2) = 1 - ((x(k2) - a(k2)) .^ 2) ./ ((c(k2) - a(k2)) .* (b(k2) - a(k2))); p(k3) = ((c(k3) - x(k3)) .^ 2) ./ ((c(k3) - a(k3)) .* (c(k3) - b(k3))); else p(k2) = ((x(k2) - a(k2)) .^ 2) ./ ((c(k2) - a(k2)) .* (b(k2) - a(k2))); p(k3) = 1 - ((c(k3) - x(k3)) .^ 2) ./ ((c(k3) - a(k3)) .* (c(k3) - b(k3))); p(k4) = 1; endif endfunction %!demo %! ## Plot various CDFs from the triangular distribution %! x = 0.001:0.001:10; %! p1 = tricdf (x, 3, 4, 6); %! p2 = tricdf (x, 1, 2, 5); %! p3 = tricdf (x, 2, 3, 9); %! p4 = tricdf (x, 2, 5, 9); %! plot (x, p1, '-b', x, p2, '-g', x, p3, '-r', x, p4, '-c') %! grid on %! xlim ([0, 10]) %! legend ({'a = 3, b = 4, c = 6', 'a = 1, b = 2, c = 5', ... %! 'a = 2, b = 3, c = 9', 'a = 2, b = 5, c = 9'}, ... %! 'location', 'southeast') %! title ('Triangular CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, y %! x = [-1, 0, 0.1, 0.5, 0.9, 1, 2] + 1; %! y = [0, 0, 0.02, 0.5, 0.98, 1 1]; %!assert_equal (tricdf (x, ones (1,7), 1.5 * ones (1, 7), 2 * ones (1, 7)), y, eps) %!assert_equal (tricdf (x, 1 * ones (1, 7), 1.5, 2), y, eps) %!assert_equal (tricdf (x, 1 * ones (1, 7), 1.5, 2, 'upper'), 1 - y, eps) %!assert_equal (tricdf (x, 1, 1.5, 2 * ones (1, 7)), y, eps) %!assert_equal (tricdf (x, 1, 1.5 * ones (1, 7), 2), y, eps) %!assert_equal (tricdf (x, 1, 1.5, 2), y, eps) %!assert_equal (tricdf (x, [1, 1, NaN, 1, 1, 1, 1], 1.5, 2), ... %! [y(1:2), NaN, y(4:7)], eps) %!assert_equal (tricdf (x, 1, 1.5, 2*[1, 1, NaN, 1, 1, 1, 1]), ... %! [y(1:2), NaN, y(4:7)], eps) %!assert_equal (tricdf (x, 1, 1.5, 2*[1, 1, NaN, 1, 1, 1, 1]), ... %! [y(1:2), NaN, y(4:7)], eps) %!assert_equal (tricdf ([x, NaN], 1, 1.5, 2), [y, NaN], eps) ## Test class of input preserved %!assert_equal (tricdf (single ([x, NaN]), 1, 1.5, 2), ... %! single ([y, NaN]), eps ('single')) %!assert_equal (tricdf ([x, NaN], single (1), 1.5, 2), ... %! single ([y, NaN]), eps ('single')) %!assert_equal (tricdf ([x, NaN], 1, single (1.5), 2), ... %! single ([y, NaN]), eps ('single')) %!assert_equal (tricdf ([x, NaN], 1, 1.5, single (2)), ... %! single ([y, NaN]), eps ('single')) ## Test input validation %!error tricdf () %!error tricdf (1) %!error tricdf (1, 2) %!error tricdf (1, 2, 3) %!error ... %! tricdf (1, 2, 3, 4, 5, 6) %!error tricdf (1, 2, 3, 4, 'tail') %!error tricdf (1, 2, 3, 4, 5) %!error ... %! tricdf (ones (3), ones (2), ones (2), ones (2)) %!error ... %! tricdf (ones (2), ones (3), ones (2), ones (2)) %!error ... %! tricdf (ones (2), ones (2), ones (3), ones (2)) %!error ... %! tricdf (ones (2), ones (2), ones (2), ones (3)) %!error tricdf (int32 (2), 2, 3, 4) %!error tricdf (true, 2, 3, 4) %!error tricdf ('a', 2, 3, 4) %!error tricdf (i, 2, 3, 4) %!error tricdf (1, i, 3, 4) %!error tricdf (1, 2, i, 4) %!error tricdf (1, 2, 3, i) statistics-release-2.1.2/inst/Distribution_Functions/triinv.m000066400000000000000000000156411527311104300245050ustar00rootroot00000000000000## Copyright (B) 1995-2015 Kurt Hornik ## Copyright (B) 2016 Dag Lyberg ## Copyright (B) 2023-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} triinv (@var{p}, @var{a}, @var{b}, @var{c}) ## ## Inverse of the triangular cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the triangular distribution with lower limit parameter @var{a}, peak ## location (mode) parameter @var{b}, and upper limit parameter @var{c}. The ## size of @var{x} is the common size of the input arguments. A scalar input ## functions as a constant matrix of the same size as the other inputs. ## ## Note that the order of the parameter input arguments has been changed after ## statistics version 1.6.3 in order to be MATLAB compatible with the parameters ## used in the TriangularDistribution probability distribution object. More ## specifically, the positions of the parameters @var{b} and @var{c} have been ## swapped. As a result, the naming conventions no longer coincide with those ## used in Wikipedia, in which @math{b} denotes the upper limit and @math{c} ## denotes the mode or peak parameter. ## ## Further information about the triangular distribution can be found at ## @url{https://en.wikipedia.org/wiki/Triangular_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{tricdf, tripdf, trirnd, tristat} ## @end deftypefn function x = triinv (p, a, b, c) ## Check for valid number of input arguments if (nargin < 4) error ("triinv: function called with too few input arguments."); endif ## Check for common size of P, A, B, and C if (! isscalar (p) || ! isscalar (a) || ! isscalar (b) || ! isscalar (c)) [retval, p, a, b, c] = common_size (p, a, b, c); if (retval > 0) error ("triinv: P, A, B, and C must be of common size or scalars."); endif endif ## Check for P, A, B, and C being double or single if (! (isfloat (p) && isfloat (a) && isfloat (b) && isfloat (c))) error ("triinv: P, A, B, and C must be double or single."); endif ## Check for P, A, B, and C being reals if (iscomplex (p) || iscomplex (a) || iscomplex (b) || iscomplex (c)) error ("triinv: P, A, B, and C must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (a, 'single') || isa (b, 'single') ... || isa (c, 'single')) x = NaN (size (p), 'single'); else x = NaN (size (p)); endif ## Force zeros for within range parameters. k = (p >= 0) & (p <= 1) & (a < c) & (a <= b) & (b <= c); x(k) = 0; ## Compute triangular iCDF h = 2 ./ (c-a); w = b - a; area1 = h .* w / 2; j = k & (p <= area1); x(j) += (2 * p(j) .* (w(j) ./ h(j))) .^ 0.5 + a(j); w = c - b; j = k & (area1 < p) & (p < 1); x(j) += c(j) - (2 * (1 - p(j)) .* (w(j) ./ h(j))) .^ 0.5; j = k & (p == 1); x(j) = c(j); endfunction %!demo %! ## Plot various iCDFs from the triangular distribution %! p = 0.001:0.001:0.999; %! x1 = triinv (p, 3, 6, 4); %! x2 = triinv (p, 1, 5, 2); %! x3 = triinv (p, 2, 9, 3); %! x4 = triinv (p, 2, 9, 5); %! plot (p, x1, '-b', p, x2, '-g', p, x3, '-r', p, x4, '-c') %! grid on %! ylim ([0, 10]) %! legend ({'a = 3, b = 6, c = 4', 'a = 1, b = 5, c = 2', ... %! 'a = 2, b = 9, c = 3', 'a = 2, b = 9, c = 5'}, ... %! 'location', 'northwest') %! title ('Triangular CDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p, y %! p = [-1, 0, 0.02, 0.5, 0.98, 1, 2]; %! y = [NaN, 0, 0.1, 0.5, 0.9, 1, NaN] + 1; %!assert_equal (triinv (p, ones (1, 7), 1.5 * ones (1, 7), 2 * ones (1, 7)), y, eps) %!assert_equal (triinv (p, 1 * ones (1, 7), 1.5, 2), y, eps) %!assert_equal (triinv (p, 1, 1.5, 2 * ones (1, 7)), y, eps) %!assert_equal (triinv (p, 1, 1.5*ones (1,7), 2), y, eps) %!assert_equal (triinv (p, 1, 1.5, 2), y, eps) %!assert_equal (triinv (p, [1, 1, NaN, 1, 1, 1, 1], 1.5, 2), [y(1:2), NaN, y(4:7)], eps) %!assert_equal (triinv (p, 1, 1.5 * [1, 1, NaN, 1, 1, 1, 1], 2), [y(1:2), NaN, y(4:7)], eps) %!assert_equal (triinv (p, 1, 1.5, 2 * [1, 1, NaN, 1, 1, 1, 1]), [y(1:2), NaN, y(4:7)], eps) %!assert_equal (triinv ([p, NaN], 1, 1.5, 2), [y, NaN], eps) ## Test class of input preserved %!assert_equal (triinv (single ([p, NaN]), 1, 1.5, 2), single ([y, NaN]), eps ('single')) %!assert_equal (triinv ([p, NaN], single (1), 1.5, 2), single ([y, NaN]), eps ('single')) %!assert_equal (triinv ([p, NaN], 1, single (1.5), 2), single ([y, NaN]), eps ('single')) %!assert_equal (triinv ([p, NaN], 1, 1.5, single (2)), single ([y, NaN]), eps ('single')) ## Test input validation %!error triinv () %!error triinv (1) %!error triinv (1, 2) %!error triinv (1, 2, 3) %!error ... %! triinv (1, 2, 3, 4, 5) %!error ... %! triinv (ones (3), ones (2), ones (2), ones (2)) %!error ... %! triinv (ones (2), ones (3), ones (2), ones (2)) %!error ... %! triinv (ones (2), ones (2), ones (3), ones (2)) %!error ... %! triinv (ones (2), ones (2), ones (2), ones (3)) %!error triinv (int32 (2), 2, 3, 4) %!error triinv (true, 2, 3, 4) %!error triinv ('a', 2, 3, 4) %!error triinv (i, 2, 3, 4) %!error triinv (1, i, 3, 4) %!error triinv (1, 2, i, 4) %!error triinv (1, 2, 3, i) statistics-release-2.1.2/inst/Distribution_Functions/tripdf.m000066400000000000000000000160471527311104300244630ustar00rootroot00000000000000## Copyright (C) 1997-2015 Kurt Hornik ## Copyright (C) 2016 Dag Lyberg ## Copyright (C) 2023-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} tripdf (@var{x}, @var{a}, @var{b}, @var{c}) ## ## Triangular probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the triangular distribution with lower limit parameter @var{a}, peak ## location (mode) parameter @var{b}, and upper limit parameter @var{c}. The ## size of @var{y} is the common size of the input arguments. A scalar input ## functions as a constant matrix of the same size as the other inputs. ## ## Note that the order of the parameter input arguments has been changed after ## statistics version 1.6.3 in order to be MATLAB compatible with the parameters ## used in the TriangularDistribution probability distribution object. More ## specifically, the positions of the parameters @var{b} and @var{c} have been ## swapped. As a result, the naming conventions no longer coincide with those ## used in Wikipedia, in which @math{b} denotes the upper limit and @math{c} ## denotes the mode or peak parameter. ## ## Further information about the triangular distribution can be found at ## @url{https://en.wikipedia.org/wiki/Triangular_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## MATLAB also accepts integer input here, returning the result in the integer ## class of the input; Octave rejects it, as it does for every other continuous ## distribution. ## ## @seealso{tricdf, triinv, trirnd, tristat} ## @end deftypefn function y = tripdf (x, a, b, c) ## Check for valid number of input arguments if (nargin < 4) error ("tripdf: function called with too few input arguments."); endif ## Check for common size of X, A, B, and C if (! isscalar (x) || ! isscalar (a) || ! isscalar (b) || ! isscalar (c)) [retval, x, a, b, c] = common_size (x, a, b, c); if (retval > 0) error ("tripdf: X, A, B, and C must be of common size or scalars."); endif endif ## Check for X, A, B, and C being double or single if (! (isfloat (x) && isfloat (a) && isfloat (b) && isfloat (c))) error ("tripdf: X, A, B, and C must be double or single."); endif ## Check for X, A, B, and C being reals if (iscomplex (x) || iscomplex (a) || iscomplex (b) || iscomplex (c)) error ("tripdf: X, A, B, and C must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (a, 'single') || isa (b, 'single') ... || isa (c, 'single')) y = zeros (size (x), 'single'); else y = zeros (size (x)); endif ## Force NaNs for out of range parameters. k = isnan (x) | ! (a < c) | ! (b >= a) | ! (b <= c); y(k) = NaN; k = (x >= a) & (x <= c) & (a < c) & (a <= b) & (b <= c); h = 2 ./ (c - a); j = k & (a <= x) & (x < b); y(j) = h(j) .* (x(j) - a(j)) ./ (b(j) - a(j)); j = k & (x == b); y(j) = h(j); j = k & (b < x) & (x <= c); y(j) = h(j) .* (c(j) - x(j)) ./ (c(j) - b(j)); endfunction %!demo %! ## Plot various CDFs from the triangular distribution %! x = 0.001:0.001:10; %! y1 = tripdf (x, 3, 4, 6); %! y2 = tripdf (x, 1, 2, 5); %! y3 = tripdf (x, 2, 3, 9); %! y4 = tripdf (x, 2, 5, 9); %! plot (x, y1, '-b', x, y2, '-g', x, y3, '-r', x, y4, '-c') %! grid on %! xlim ([0, 10]) %! legend ({'a = 3, b = 4, c = 6', 'a = 1, b = 2, c = 5', ... %! 'a = 2, b = 3, c = 9', 'a = 2, b = 5, c = 9'}, ... %! 'location', 'northeast') %! title ('Triangular CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, y, deps %! x = [-1, 0, 0.1, 0.5, 0.9, 1, 2] + 1; %! y = [0, 0, 0.4, 2, 0.4, 0, 0]; %! deps = 2*eps; %!assert_equal (tripdf (x, ones (1,7), 1.5*ones (1,7), 2*ones (1,7)), y, deps) %!assert_equal (tripdf (x, 1*ones (1,7), 1.5, 2), y, deps) %!assert_equal (tripdf (x, 1, 1.5, 2*ones (1,7)), y, deps) %!assert_equal (tripdf (x, 1, 1.5*ones (1,7), 2), y, deps) %!assert_equal (tripdf (x, 1, 1.5, 2), y, deps) %!assert_equal (tripdf (x, [1, 1, NaN, 1, 1, 1, 1], 1.5, 2), [y(1:2), NaN, y(4:7)], deps) %!assert_equal (tripdf (x, 1, 1.5, 2*[1, 1, NaN, 1, 1, 1, 1]), [y(1:2), NaN, y(4:7)], deps) %!assert_equal (tripdf (x, 1, 1.5*[1, 1, NaN, 1, 1, 1, 1], 2), [y(1:2), NaN, y(4:7)], deps) %!assert_equal (tripdf ([x, NaN], 1, 1.5, 2), [y, NaN], deps) ## Test class of input preserved %!assert_equal (tripdf (single ([x, NaN]), 1, 1.5, 2), single ([y, NaN]), eps ('single')) %!assert_equal (tripdf ([x, NaN], single (1), 1.5, 2), single ([y, NaN]), eps ('single')) %!assert_equal (tripdf ([x, NaN], 1, 1.5, single (2)), single ([y, NaN]), eps ('single')) %!assert_equal (tripdf ([x, NaN], 1, single (1.5), 2), single ([y, NaN]), eps ('single')) ## Test input validation %!error tripdf () %!error tripdf (1) %!error tripdf (1, 2) %!error tripdf (1, 2, 3) %!error ... %! tripdf (1, 2, 3, 4, 5) %!error ... %! tripdf (ones (3), ones (2), ones (2), ones (2)) %!error ... %! tripdf (ones (2), ones (3), ones (2), ones (2)) %!error ... %! tripdf (ones (2), ones (2), ones (3), ones (2)) %!error ... %! tripdf (ones (2), ones (2), ones (2), ones (3)) %!error tripdf (int32 (2), 2, 3, 4) %!error tripdf (true, 2, 3, 4) %!error tripdf ('a', 2, 3, 4) %!error tripdf (i, 2, 3, 4) %!error tripdf (1, i, 3, 4) %!error tripdf (1, 2, i, 4) %!error tripdf (1, 2, 3, i) statistics-release-2.1.2/inst/Distribution_Functions/trirnd.m000066400000000000000000000203631527311104300244710ustar00rootroot00000000000000## Copyright (C) 1997-2015 Kurt Hornik ## Copyright (C) 2016 Dag Lyberg ## Copyright (C) 2023-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} trirnd (@var{a}, @var{b}, @var{c}) ## @deftypefnx {statistics} {@var{r} =} trirnd (@var{a}, @var{b}, @var{c}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} trirnd (@var{a}, @var{b}, @var{c}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} trirnd (@var{a}, @var{b}, @var{c}, [@var{sz}]) ## ## Random arrays from the triangular distribution. ## ## @code{@var{r} = trirnd (@var{sigma})} returns an array of random numbers ## chosen from the triangular distribution with lower limit parameter @var{a}, ## peak location (mode) parameter @var{b}, and upper limit parameter @var{c}. ## The size of @var{r} is the common size of @var{a}, @var{b}, and @var{c}. A ## scalar input functions as a constant matrix of the same size as the other ## inputs. ## ## When called with a single size argument, @code{trirnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Note that the order of the parameter input arguments has been changed after ## statistics version 1.6.3 in order to be MATLAB compatible with the parameters ## used in the TriangularDistribution probability distribution object. More ## specifically, the positions of the parameters @var{b} and @var{c} have been ## swapped. As a result, the naming conventions no longer coincide with those ## used in Wikipedia, in which @math{b} denotes the upper limit and @math{c} ## denotes the mode or peak parameter. ## ## Further information about the triangular distribution can be found at ## @url{https://en.wikipedia.org/wiki/Triangular_distribution} ## ## @seealso{tricdf, triinv, tripdf, tristat} ## @end deftypefn function r = trirnd (a, b, c, varargin) ## Check for valid number of input arguments if (nargin < 3) error ("trirnd: function called with too few input arguments."); endif ## Check for common size of A, B, and C if (! isscalar (a) || ! isscalar (b) || ! isscalar (c)) [retval, a, b, c] = common_size (a, b, c); scalarABC = false; if (retval > 0) error ("trirnd: A, B, and C must be of common size or scalars."); endif else scalarABC = true; endif ## Check for A, B, and C being reals if (iscomplex (a) || iscomplex (b) || iscomplex (c)) error ("trirnd: A, B, and C must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 3) sz = size (a); elseif (nargin == 4) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("trirnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 4) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("trirnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (a) && ! isequal (size (a), size (ones (sz)))) error ("trirnd: A, B, and C must be scalar or of size SZ."); endif ## Check for class type if (isa (a, 'single') || isa (b, 'single') || isa (c, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from triangular distribution if (scalarABC) if ((-Inf < a) && (a < c) && (a <= b) && (b <= c) && (c < Inf)) w = c-a; left_width = b-a; right_width = c-b; h = 2 / w; left_area = h * left_width / 2; r = rand (sz, cls); idx = r < left_area; r(idx) = a + (r(idx) * w * left_width).^0.5; r(! idx) = c - ((1-r(! idx)) * w * right_width).^0.5; else r = NaN (sz, cls); endif else w = c-a; left_width = b-a; right_width = c-b; h = 2 ./ w; left_area = h .* left_width / 2; r = rand (sz, cls); k = r < left_area; r(k) = a(k) + (r(k) .* w(k) .* left_width(k)).^0.5; r(! k) = c(! k) - ((1-r(! k)) .* w(! k) .* right_width(! k)).^0.5; k = ! (-Inf < a) | ! (a < c) | ! (a <= b) | ! (b <= c) | ! (c < Inf); r(k) = NaN; endif endfunction ## Test results %!assert_equal (size (trirnd (1, 1.5, 2)), [1, 1]) %!assert_equal (size (trirnd (1 * ones (2, 1), 1.5, 2)), [2, 1]) %!assert_equal (size (trirnd (1 * ones (2, 2), 1.5, 2)), [2, 2]) %!assert_equal (size (trirnd (1, 1.5 * ones (2, 1), 2)), [2, 1]) %!assert_equal (size (trirnd (1, 1.5 * ones (2, 2), 2)), [2, 2]) %!assert_equal (size (trirnd (1, 1.5, 2 * ones (2, 1))), [2, 1]) %!assert_equal (size (trirnd (1, 1.5, 2 * ones (2, 2))), [2, 2]) %!assert_equal (size (trirnd (1, 1.5, 2, 3)), [3, 3]) %!assert_equal (size (trirnd (1, 1.5, 2, [4, 1])), [4, 1]) %!assert_equal (size (trirnd (1, 1.5, 2, 4, 1)), [4, 1]) %!assert_equal (size (trirnd (1, 1.5, 2, [])), [0, 0]) %!assert_equal (size (trirnd (1, 1.5, 2, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (trirnd (1, 5, 3, -1)), [0, 0]) %!assert_equal (size (trirnd (1, 5, 3, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (trirnd (1, 5, 3, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (trirnd (1, 1.5, 2)), "double") %!assert_equal (class (trirnd (single (1), 1.5, 2)), "single") %!assert_equal (class (trirnd (single ([1, 1]), 1.5, 2)), "single") %!assert_equal (class (trirnd (1, single (1.5), 2)), "single") %!assert_equal (class (trirnd (1, single ([1.5, 1.5]), 2)), "single") %!assert_equal (class (trirnd (1, 1.5, single (1.5))), "single") %!assert_equal (class (trirnd (1, 1.5, single ([2, 2]))), "single") ## Test input validation %!error trirnd () %!error trirnd (1) %!error trirnd (1, 2) %!error ... %! trirnd (ones (3), 5 * ones (2), ones (2)) %!error ... %! trirnd (ones (2), 5 * ones (3), ones (2)) %!error ... %! trirnd (ones (2), 5 * ones (2), ones (3)) %!error trirnd (i, 5, 3) %!error trirnd (1, 5+i, 3) %!error trirnd (1, 5, i) %!error ... %! trirnd (1, 5, 3, 1.2) %!error ... %! trirnd (1, 5, 3, ones (2)) %!error ... %! trirnd (1, 5, 3, [2 0 2.5]) %!error ... %! trirnd (1, 5, 3, 2, 1.5, 5) %!error ... %! trirnd (2, 5 * ones (2), 2, 3) %!error ... %! trirnd (2, 5 * ones (2), 2, [3, 2]) %!error ... %! trirnd (2, 5 * ones (2), 2, 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/trnd.m000066400000000000000000000137161527311104300241420ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} trnd (@var{df}) ## @deftypefnx {statistics} {@var{r} =} trnd (@var{df}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} trnd (@var{df}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} trnd (@var{df}, [@var{sz}]) ## ## Random arrays from the Student's T distribution. ## ## Return a matrix of random samples from the Students's T distribution with ## @var{df} degrees of freedom. ## ## @code{@var{r} = trnd (@var{df})} returns an array of random numbers chosen ## from the Student's T distribution with @var{df} degrees of freedom. The size ## of @var{r} is the size of @var{df}. A scalar input functions as a constant ## matrix of the same size as the other inputs. @var{df} must be a finite real ## number greater than 0, otherwise NaN is returned. ## ## When called with a single size argument, @code{trnd} returns a square matrix ## with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the Student's T distribution can be found at ## @url{https://en.wikipedia.org/wiki/Student%27s_t-distribution} ## ## @seealso{tcdf, tinv, tpdf, tstat} ## @end deftypefn function r = trnd (df, varargin) ## Check for valid number of input arguments if (nargin < 1) error ("trnd: function called with too few input arguments."); endif ## Check for DF being real if (iscomplex (df)) error ("trnd: DF must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 1) sz = size (df); elseif (nargin == 2) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("trnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 2) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("trnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (df) && ! isequal (size (df), size (ones (sz)))) error ("trnd: DF must be scalar or of size SZ."); endif ## Check for class type if (isa (df, 'single')) cls = 'single'; else cls = 'double'; endif if (isscalar (df)) if ((df > 0) && (df < Inf)) r = randn (sz, cls) ./ sqrt (2*randg (df/2, sz, cls) / df); elseif (isinf (df)) r = randn (sz, cls); else r = NaN (sz, cls); endif else r = NaN (sz, cls); k = (df > 0) & (df < Inf); kinf = isinf (df); r(k) = randn (sum (k(:)), 1, cls) ./ ... sqrt (2*randg (df(k)/2, cls) ./ df(k))(:); r(kinf) = randn (sum (kinf(:)), 1, cls); endif endfunction ## Test output %!assert_equal (size (trnd (2)), [1, 1]) %!assert_equal (size (trnd (ones (2, 1))), [2, 1]) %!assert_equal (size (trnd (ones (2, 2))), [2, 2]) %!assert_equal (size (trnd (1, 3)), [3, 3]) %!assert_equal (size (trnd (1, [4, 1])), [4, 1]) %!assert_equal (size (trnd (1, 4, 1)), [4, 1]) %!assert_equal (size (trnd (1, 4, 1)), [4, 1]) %!assert_equal (size (trnd (1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (trnd (1, 0, 1)), [0, 1]) %!assert_equal (size (trnd (1, 1, 0)), [1, 0]) %!assert_equal (size (trnd (1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (trnd (1, [])), [0, 0]) %!assert_equal (size (trnd (1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (trnd (1, -1)), [0, 0]) %!assert_equal (size (trnd (1, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (trnd (1, 2, -1, 5)), [2, 0, 5]) %!assert_equal (trnd (0, 1, 1), NaN) %!assert_equal (trnd ([0, 0, 0], [1, 3]), [NaN, NaN, NaN]) ## Test class of input preserved %!assert_equal (class (trnd (2)), "double") %!assert_equal (class (trnd (single (2))), "single") %!assert_equal (class (trnd (single ([2, 2]))), "single") ## Test input validation %!error trnd () %!error trnd (i) %!error ... %! trnd (1, 1.2) %!error ... %! trnd (1, ones (2)) %!error ... %! trnd (1, [2 0 2.5]) %!error ... %! trnd (ones (2), ones (2)) %!error ... %! trnd (1, 2, 1.5, 5) %!error trnd (ones (2,2), 3) %!error trnd (ones (2,2), [3, 2]) %!error trnd (ones (2,2), 2, 3) statistics-release-2.1.2/inst/Distribution_Functions/unidcdf.m000066400000000000000000000142501527311104300246010ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 2007-2016 David Bateman ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} unidcdf (@var{x}, @var{N}) ## @deftypefnx {statistics} {@var{p} =} unidcdf (@var{x}, @var{N}, @qcode{'upper'}) ## ## Discrete uniform cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of a discrete uniform distribution with parameter @var{N}, which ## corresponds to the maximum observable value. @code{unidcdf} assumes the ## integer values in the range @math{[1,N]} with equal probability. The size of ## @var{p} is the common size of @var{x} and @var{N}. A scalar input functions ## as a constant matrix of the same size as the other inputs. ## ## The maximum observable values in @var{N} must be positive integers, otherwise ## @qcode{NaN} is returned. ## ## @code{[@dots{}] = unidcdf (@var{x}, @var{N}, "upper")} computes the upper ## tail probability of the discrete uniform distribution with maximum observable ## value @var{N}, at the values in @var{x}. ## ## Warning: The underlying implementation uses the double class and will only ## be accurate for @var{N} < @code{flintmax} (@w{@math{2^{53}}} on ## IEEE 754 compatible systems). ## ## Further information about the discrete uniform distribution can be found at ## @url{https://en.wikipedia.org/wiki/Discrete_uniform_distribution} ## ## Input arguments must be @qcode{double}, @qcode{single}, or an integer type; ## logical and character arrays are rejected. Integer input is promoted to ## @qcode{double}, so the result is always a probability. MATLAB is ## inconsistent here: for several of the discrete distributions it returns the ## result in the integer class of the input, truncating a probability to ## @math{0} or @math{1}. ## ## @seealso{unidinv, unidpdf, unidrnd, unidfit, unidstat} ## @end deftypefn function p = unidcdf (x, N, uflag) ## Check for valid number of input arguments if (nargin < 2) error ("unidcdf: function called with too few input arguments."); endif ## Check for "upper" flag if (nargin > 2 && strcmpi (uflag, 'upper')) uflag = true; elseif (nargin > 2 && ! strcmpi (uflag, 'upper')) error ("unidcdf: invalid argument for upper tail."); else uflag = false; endif ## Check for common size of X and N if (! isscalar (x) || ! isscalar (N)) [retval, x, N] = common_size (x, N); if (retval > 0) error ("unidcdf: X and N must be of common size or scalars."); endif endif ## Check for X and N being double, single, or integer if (! (isnumeric (x) && isnumeric (N))) error ("unidcdf: X and N must be double, single, or integer."); endif ## Integer input is promoted to double, so the result is a probability ## rather than a value truncated to the input's integer type. if (isinteger (x)) x = double (x); endif if (isinteger (N)) N = double (N); endif ## Check for X and N being reals if (iscomplex (x) || iscomplex (N)) error ("unidcdf: X and N must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (N, 'single')) p = zeros (size (x), 'single'); else p = zeros (size (x)); endif ## Return 1 for X >= N p(x >= N) = 1; ## Floor X xf = floor (x); ## Compute uniform discrete CDF k = find (xf >= 1 & xf <= N); if any (k) p(k) = xf(k) ./ N(k); endif ## Check for NaNs or floored N <= 0 is_nan = isnan (x) | ! (N > 0 & N == fix (N)); if (any (is_nan(:))) p(is_nan) = NaN; endif p(N < 1 | round (N) != N) = NaN; if (uflag) # Compute upper tail p = 1 - unidcdf (x, N); endif endfunction %!demo %! ## Plot various CDFs from the discrete uniform distribution %! x = 0:10; %! p1 = unidcdf (x, 5); %! p2 = unidcdf (x, 9); %! plot (x, p1, '*b', x, p2, '*g') %! grid on %! xlim ([0, 10]) %! ylim ([0, 1]) %! legend ({'N = 5', 'N = 9'}, 'location', 'southeast') %! title ('Discrete uniform CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, y %! x = [0 1 2.5 10 11]; %! y = [0, 0.1 0.2 1.0 1.0]; %!assert_equal (unidcdf (x, 10*ones (1,5)), y) %!assert_equal (unidcdf (x, 10*ones (1,5), 'upper'), 1 - y) %!assert_equal (unidcdf (x, 10), y) %!assert_equal (unidcdf (x, 10, 'upper'), 1 - y) %!assert_equal (unidcdf (x, 10*[0 1 NaN 1 1]), [NaN 0.1 NaN y(4:5)]) %!assert_equal (unidcdf ([x(1:2) NaN Inf x(5)], 10), [y(1:2) NaN 1 y(5)]) ## Test class of input preserved %!assert_equal (unidcdf ([x, NaN], 10), [y, NaN]) %!assert_equal (unidcdf (single ([x, NaN]), 10), single ([y, NaN])) %!assert_equal (unidcdf ([x, NaN], single (10)), single ([y, NaN])) ## Test input validation %!error unidcdf () %!error unidcdf (1) %!error unidcdf (1, 2, 3) %!error unidcdf (1, 2, 'tail') %!error ... %! unidcdf (ones (3), ones (2)) %!error ... %! unidcdf (ones (2), ones (3)) %!error unidcdf (true, 2) %!error unidcdf ('a', 2) %!assert_equal (class (unidcdf (int32 (2), 2)), 'double') %!error unidcdf (i, 2) %!error unidcdf (2, i) statistics-release-2.1.2/inst/Distribution_Functions/unidinv.m000066400000000000000000000115451527311104300246450ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 2007-2016 David Bateman ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} unidinv (@var{p}, @var{N}) ## ## Inverse of the discrete uniform cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the discrete uniform distribution with parameter @var{N}, which corresponds ## to the maximum observable value. @code{unidinv} assumes the integer values ## in the range @math{[1,N]} with equal probability. The size of @var{x} is the ## common size of @var{p} and @var{N}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## The maximum observable values in @var{N} must be positive integers, otherwise ## @qcode{NaN} is returned. ## ## Warning: The underlying implementation uses the double class and will only ## be accurate for @var{N} < @code{flintmax} (@w{@math{2^{53}}} on ## IEEE 754 compatible systems). ## ## Further information about the discrete uniform distribution can be found at ## @url{https://en.wikipedia.org/wiki/Discrete_uniform_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{unidcdf, unidpdf, unidrnd, unidfit, unidstat} ## @end deftypefn function x = unidinv (p, N) ## Check for valid number of input arguments if (nargin < 2) error ("unidinv: function called with too few input arguments."); endif ## Check for common size of P and N if (! isscalar (p) || ! isscalar (N)) [retval, p, N] = common_size (p, N); if (retval > 0) error ("unidinv: P and N must be of common size or scalars."); endif endif ## Check for P and N being double or single if (! (isfloat (p) && isfloat (N))) error ("unidinv: P and N must be double or single."); endif ## Check for P and N being reals if (iscomplex (p) || iscomplex (N)) error ("unidinv: P and N must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (N, 'single')) x = NaN (size (p), 'single'); else x = NaN (size (p)); endif ## For Matlab compatibility, unidinv(0) = NaN k = (p > 0) & (p <= 1) & (N > 0 & N == fix (N)); x(k) = floor (p(k) .* N(k)); endfunction %!demo %! ## Plot various iCDFs from the discrete uniform distribution %! p = 0.001:0.001:0.999; %! x1 = unidinv (p, 5); %! x2 = unidinv (p, 9); %! plot (p, x1, '-b', p, x2, '-g') %! grid on %! xlim ([0, 1]) %! ylim ([0, 10]) %! legend ({'N = 5', 'N = 9'}, 'location', 'northwest') %! title ('Discrete uniform iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p %! p = [-1 0 0.5 1 2]; %!assert_equal (unidinv (p, 10*ones (1,5)), [NaN NaN 5 10 NaN], eps) %!assert_equal (unidinv (p, 10), [NaN NaN 5 10 NaN], eps) %!assert_equal (unidinv (p, 10*[0 1 NaN 1 1]), [NaN NaN NaN 10 NaN], eps) %!assert_equal (unidinv ([p(1:2) NaN p(4:5)], 10), [NaN NaN NaN 10 NaN], eps) ## Test class of input preserved %!assert_equal (unidinv ([p, NaN], 10), [NaN NaN 5 10 NaN NaN], eps) %!assert_equal (unidinv (single ([p, NaN]), 10), single ([NaN NaN 5 10 NaN NaN]), eps) %!assert_equal (unidinv ([p, NaN], single (10)), single ([NaN NaN 5 10 NaN NaN]), eps) ## Test input validation %!error unidinv () %!error unidinv (1) %!error ... %! unidinv (ones (3), ones (2)) %!error ... %! unidinv (ones (2), ones (3)) %!error unidinv (int32 (2), 2) %!error unidinv (true, 2) %!error unidinv ('a', 2) %!error unidinv (i, 2) %!error unidinv (2, i) statistics-release-2.1.2/inst/Distribution_Functions/unidpdf.m000066400000000000000000000120211527311104300246100ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 2007-2016 David Bateman ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} unidpdf (@var{x}, @var{N}) ## ## Discrete uniform probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the discrete uniform distribution with parameter @var{N}, which ## corresponds to the maximum observable value. @code{unidpdf} assumes the ## integer values in the range @math{[1,N]} with equal probability. The size of ## @var{x} is the common size of @var{p} and @var{N}. A scalar input functions ## as a constant matrix of the same size as the other inputs. ## ## The maximum observable values in @var{N} must be positive integers, otherwise ## @qcode{NaN} is returned. ## ## Warning: The underlying implementation uses the double class and will only ## be accurate for @var{N} < @code{flintmax} (@w{@math{2^{53}}} on ## IEEE 754 compatible systems). ## ## Further information about the discrete uniform distribution can be found at ## @url{https://en.wikipedia.org/wiki/Discrete_uniform_distribution} ## ## Input arguments must be @qcode{double}, @qcode{single}, or an integer type; ## logical and character arrays are rejected. Integer input is promoted to ## @qcode{double}, so the result is always a probability. MATLAB is ## inconsistent here: for several of the discrete distributions it returns the ## result in the integer class of the input, truncating a probability to ## @math{0} or @math{1}. ## ## @seealso{unidcdf, unidinv, unidrnd, unidfit, unidstat} ## @end deftypefn function y = unidpdf (x, N) ## Check for valid number of input arguments if (nargin < 2) error ("unidpdf: function called with too few input arguments."); endif ## Check for common size of X and N if (! isscalar (x) || ! isscalar (N)) [retval, x, N] = common_size (x, N); if (retval > 0) error ("unidpdf: X and N must be of common size or scalars."); endif endif ## Check for X and N being double, single, or integer if (! (isnumeric (x) && isnumeric (N))) error ("unidpdf: X and N must be double, single, or integer."); endif ## Integer input is promoted to double, so the result is a probability ## rather than a value truncated to the input's integer type. if (isinteger (x)) x = double (x); endif if (isinteger (N)) N = double (N); endif ## Check for X and N being reals if (iscomplex (x) || iscomplex (N)) error ("unidpdf: X and N must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (N, 'single')) y = zeros (size (x), 'single'); else y = zeros (size (x)); endif k = isnan (x) | ! (N > 0 & N == fix (N)); y(k) = NaN; k = ! k & (x >= 1) & (x <= N) & (x == fix (x)); y(k) = 1 ./ N(k); endfunction %!demo %! ## Plot various PDFs from the discrete uniform distribution %! x = 0:10; %! y1 = unidpdf (x, 5); %! y2 = unidpdf (x, 9); %! plot (x, y1, '*b', x, y2, '*g') %! grid on %! xlim ([0, 10]) %! ylim ([0, 0.25]) %! legend ({'N = 5', 'N = 9'}, 'location', 'northeast') %! title ('Discrete uniform PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x, y %! x = [-1 0 1 2 10 11]; %! y = [0 0 0.1 0.1 0.1 0]; %!assert_equal (unidpdf (x, 10*ones (1,6)), y) %!assert_equal (unidpdf (x, 10), y) %!assert_equal (unidpdf (x, 10*[0 NaN 1 1 1 1]), [NaN NaN y(3:6)]) %!assert_equal (unidpdf ([x, NaN], 10), [y, NaN]) ## Test class of input preserved %!assert_equal (unidpdf (single ([x, NaN]), 10), single ([y, NaN])) %!assert_equal (unidpdf ([x, NaN], single (10)), single ([y, NaN])) ## Test input validation %!error unidpdf () %!error unidpdf (1) %!error ... %! unidpdf (ones (3), ones (2)) %!error ... %! unidpdf (ones (2), ones (3)) %!error unidpdf (true, 2) %!error unidpdf ('a', 2) %!assert_equal (class (unidpdf (int32 (2), 2)), 'double') %!error unidpdf (i, 2) %!error unidpdf (2, i) statistics-release-2.1.2/inst/Distribution_Functions/unidrnd.m000066400000000000000000000142211527311104300246260ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 2005-2016 John W. Eaton ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} unidrnd (@var{N}) ## @deftypefnx {statistics} {@var{r} =} unidrnd (@var{N}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} unidrnd (@var{N}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} unidrnd (@var{N}, [@var{sz}]) ## ## Random arrays from the discrete uniform distribution. ## ## @code{@var{r} = unidrnd (@var{N})} returns an array of random numbers chosen ## from the discrete uniform distribution with parameter @var{N}, which ## corresponds to the maximum observable value. @code{unidrnd} assumes the ## integer values in the range @math{[1,N]} with equal probability. The size of ## @var{r} is the size of @var{N}. A scalar input functions as a constant ## matrix of the same size as the other inputs. ## ## The maximum observable values in @var{N} must be positive integers, otherwise ## @qcode{NaN} is returned. ## ## When called with a single size argument, @code{unidrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Warning: The underlying implementation uses the double class and will only ## be accurate for @var{N} < @code{flintmax} (@w{@math{2^{53}}} on ## IEEE 754 compatible systems). ## ## Further information about the discrete uniform distribution can be found at ## @url{https://en.wikipedia.org/wiki/Discrete_uniform_distribution} ## ## @seealso{unidcdf, unidinv, unidpdf, unidfit, unidstat} ## @end deftypefn function r = unidrnd (N, varargin) ## Check for valid number of input arguments if (nargin < 1) error ("unidrnd: function called with too few input arguments."); endif ## Check for N being real if (iscomplex (N)) error ("unidrnd: N must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 1) sz = size (N); elseif (nargin == 2) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("unidrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 2) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("unidrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (N) && ! isequal (size (N), size (ones (sz)))) error ("unidrnd: N must be scalar or of size SZ."); endif ## Check for class type if (isa (N, 'single')) cls = 'single'; else cls = 'double'; endif if (isscalar (N)) if (N > 0 && N == fix (N)) r = ceil (rand (sz, cls) * N); else r = NaN (sz, cls); endif else r = ceil (rand (sz, cls) .* N); k = ! (N > 0 & N == fix (N)); r(k) = NaN; endif endfunction ## Test output %!assert_equal (size (unidrnd (2)), [1, 1]) %!assert_equal (size (unidrnd (ones (2, 1))), [2, 1]) %!assert_equal (size (unidrnd (ones (2, 2))), [2, 2]) %!assert_equal (size (unidrnd (1, 3)), [3, 3]) %!assert_equal (size (unidrnd (1, [4, 1])), [4, 1]) %!assert_equal (size (unidrnd (1, 4, 1)), [4, 1]) %!assert_equal (size (unidrnd (1, 4, 1)), [4, 1]) %!assert_equal (size (unidrnd (1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (unidrnd (1, 0, 1)), [0, 1]) %!assert_equal (size (unidrnd (1, 1, 0)), [1, 0]) %!assert_equal (size (unidrnd (1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (unidrnd (1, [])), [0, 0]) %!assert_equal (size (unidrnd (1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (unidrnd (1, -1)), [0, 0]) %!assert_equal (size (unidrnd (1, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (unidrnd (1, 2, -1, 5)), [2, 0, 5]) %!assert_equal (unidrnd (0, 1, 1), NaN) %!assert_equal (unidrnd ([0, 0, 0], [1, 3]), [NaN, NaN, NaN]) ## Test class of input preserved %!assert_equal (class (unidrnd (2)), "double") %!assert_equal (class (unidrnd (single (2))), "single") %!assert_equal (class (unidrnd (single ([2, 2]))), "single") ## Test input validation %!error unidrnd () %!error unidrnd (i) %!error ... %! unidrnd (1, 1.2) %!error ... %! unidrnd (1, ones (2)) %!error ... %! unidrnd (1, [2 0 2.5]) %!error ... %! unidrnd (ones (2), ones (2)) %!error ... %! unidrnd (1, 2, 1.5, 5) %!error unidrnd (ones (2,2), 3) %!error unidrnd (ones (2,2), [3, 2]) %!error unidrnd (ones (2,2), 2, 3) statistics-release-2.1.2/inst/Distribution_Functions/unifcdf.m000066400000000000000000000153311527311104300246040ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} unifcdf (@var{x}, @var{a}, @var{b}) ## @deftypefnx {statistics} {@var{p} =} unifcdf (@var{x}, @var{a}, @var{b}, @qcode{'upper'}) ## ## Continuous uniform cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the continuous uniform distribution with parameters @var{a} and ## @var{b}, which define the lower and upper bounds of the interval ## @qcode{[@var{a}, @var{b}]}. The size of @var{p} is the common size of ## @var{x}, @var{a}, and @var{b}. A scalar input functions as a constant matrix ## of the same size as the other inputs. ## ## @code{[@dots{}] = unifcdf (@var{x}, @var{a}, @var{b}, "upper")} computes the ## upper tail probability of the continuous uniform distribution with parameters ## @var{a}, and @var{b}, at the values in @var{x}. ## ## Further information about the continuous uniform distribution can be found at ## @url{https://en.wikipedia.org/wiki/Continuous_uniform_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## MATLAB also accepts integer input here, returning the result in the integer ## class of the input; Octave rejects it, as it does for every other continuous ## distribution. ## ## @seealso{unifinv, unifpdf, unifrnd, unifit, unifstat} ## @end deftypefn function p = unifcdf (x, a, b, uflag) ## Check for valid number of input arguments if (nargin < 3) error ("unifcdf: function called with too few input arguments."); endif ## Check for "upper" flag if (nargin > 3 && strcmpi (uflag, 'upper')) uflag = true; elseif (nargin > 3 && ! strcmpi (uflag, 'upper')) error ("unifcdf: invalid argument for upper tail."); else uflag = false; endif ## Check for common size of X, A, and B if (! isscalar (x) || ! isscalar (a) || ! isscalar (b)) [retval, x, a, b] = common_size (x, a, b); if (retval > 0) error ("unifcdf: X, A, and B must be of common size or scalars."); endif endif ## Check for X, A, and B being double or single if (! (isfloat (x) && isfloat (a) && isfloat (b))) error ("unifcdf: X, A, and B must be double or single."); endif ## Check for X, A, and B being reals if (iscomplex (x) || iscomplex (a) || iscomplex (b)) error ("unifcdf: X, A, and B must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (a, 'single') || isa (b, 'single')) p = zeros (size (x), 'single'); else p = zeros (size (x)); endif ## Calculate continuous uniform CDF for valid parameter and data range k = find (x > a & x < b & a < b); if (uflag) p(x <= a & a < b) = 1; p(x >= b & a < b) = 0; if any (k) p(k) = (b(k)- x(k)) ./ (b(k) - a(k)); endif else p(x <= a & a < b) = 0; p(x >= b & a < b) = 1; if any (k) p(k) = (x(k) - a(k)) ./ (b(k) - a(k)); endif endif ## Continue argument check p(a >= b) = NaN; p(isnan (x) | isnan (a) | isnan (b)) = NaN; endfunction %!demo %! ## Plot various CDFs from the continuous uniform distribution %! x = 0:0.1:10; %! p1 = unifcdf (x, 2, 5); %! p2 = unifcdf (x, 3, 9); %! plot (x, p1, '-b', x, p2, '-g') %! grid on %! xlim ([0, 10]) %! ylim ([0, 1]) %! legend ({'a = 2, b = 5', 'a = 3, b = 9'}, 'location', 'southeast') %! title ('Continuous uniform CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, y %! x = [-1 0 0.5 1 2] + 1; %! y = [0 0 0.5 1 1]; %!assert_equal (unifcdf (x, ones (1,5), 2*ones (1,5)), y) %!assert_equal (unifcdf (x, ones (1,5), 2*ones (1,5), 'upper'), 1 - y) %!assert_equal (unifcdf (x, 1, 2*ones (1,5)), y) %!assert_equal (unifcdf (x, 1, 2*ones (1,5), 'upper'), 1 - y) %!assert_equal (unifcdf (x, ones (1,5), 2), y) %!assert_equal (unifcdf (x, ones (1,5), 2, 'upper'), 1 - y) %!assert_equal (unifcdf (x, [2 1 NaN 1 1], 2), [NaN 0 NaN 1 1]) %!assert_equal (unifcdf (x, [2 1 NaN 1 1], 2, 'upper'), 1 - [NaN 0 NaN 1 1]) %!assert_equal (unifcdf (x, 1, 2*[0 1 NaN 1 1]), [NaN 0 NaN 1 1]) %!assert_equal (unifcdf (x, 1, 2*[0 1 NaN 1 1], 'upper'), 1 - [NaN 0 NaN 1 1]) %!assert_equal (unifcdf ([x(1:2) NaN x(4:5)], 1, 2), [y(1:2) NaN y(4:5)]) %!assert_equal (unifcdf ([x(1:2) NaN x(4:5)], 1, 2, 'upper'), 1 - [y(1:2) NaN y(4:5)]) ## Test class of input preserved %!assert_equal (unifcdf ([x, NaN], 1, 2), [y, NaN]) %!assert_equal (unifcdf (single ([x, NaN]), 1, 2), single ([y, NaN])) %!assert_equal (unifcdf ([x, NaN], single (1), 2), single ([y, NaN])) %!assert_equal (unifcdf ([x, NaN], 1, single (2)), single ([y, NaN])) ## Test input validation %!error unifcdf () %!error unifcdf (1) %!error unifcdf (1, 2) %!error unifcdf (1, 2, 3, 4) %!error unifcdf (1, 2, 3, 'tail') %!error ... %! unifcdf (ones (3), ones (2), ones (2)) %!error ... %! unifcdf (ones (2), ones (3), ones (2)) %!error ... %! unifcdf (ones (2), ones (2), ones (3)) %!error unifcdf (int32 (2), 2, 2) %!error unifcdf (true, 2, 2) %!error unifcdf ('a', 2, 2) %!error unifcdf (i, 2, 2) %!error unifcdf (2, i, 2) %!error unifcdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/unifinv.m000066400000000000000000000122711527311104300246440ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} unifinv (@var{p}, @var{a}, @var{b}) ## ## Inverse of the continuous uniform cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the continuous uniform distribution with parameters @var{a} and @var{b}, ## which define the lower and upper bounds of the interval ## @qcode{[@var{a}, @var{b}]}. The size of @var{x} is the common size of ## @var{p}, @var{a}, and @var{b}. A scalar input functions as a constant matrix ## of the same size as the other inputs. ## ## Further information about the continuous uniform distribution can be found at ## @url{https://en.wikipedia.org/wiki/Continuous_uniform_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{unifcdf, unifpdf, unifrnd, unifit, unifstat} ## @end deftypefn function x = unifinv (p, a, b) ## Check for valid number of input arguments if (nargin < 3) error ("unifinv: function called with too few input arguments."); endif ## Check for common size of P, A, and B if (! isscalar (p) || ! isscalar (a) || ! isscalar (b)) [retval, p, a, b] = common_size (p, a, b); if (retval > 0) error ("unifinv: P, A, and B must be of common size or scalars."); endif endif ## Check for P, A, and B being double or single if (! (isfloat (p) && isfloat (a) && isfloat (b))) error ("unifinv: P, A, and B must be double or single."); endif ## Check for P, A, and B being reals if (iscomplex (p) || iscomplex (a) || iscomplex (b)) error ("unifinv: P, A, and B must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (a, 'single') || isa (b, 'single')) x = NaN (size (p), 'single'); else x = NaN (size (p)); endif ## Calculate continuous uniform iCDF for valid parameter and data range k = (p >= 0) & (p <= 1) & (a < b); x(k) = a(k) + p(k) .* (b(k) - a(k)); endfunction %!demo %! ## Plot various iCDFs from the continuous uniform distribution %! p = 0.001:0.001:0.999; %! x1 = unifinv (p, 2, 5); %! x2 = unifinv (p, 3, 9); %! plot (p, x1, '-b', p, x2, '-g') %! grid on %! xlim ([0, 1]) %! ylim ([0, 10]) %! legend ({'a = 2, b = 5', 'a = 3, b = 9'}, 'location', 'northwest') %! title ('Continuous uniform iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared p %! p = [-1 0 0.5 1 2]; %!assert_equal (unifinv (p, ones (1,5), 2*ones (1,5)), [NaN 1 1.5 2 NaN]) %!assert_equal (unifinv (p, 0, 1), [NaN 1 1.5 2 NaN] - 1) %!assert_equal (unifinv (p, 1, 2*ones (1,5)), [NaN 1 1.5 2 NaN]) %!assert_equal (unifinv (p, ones (1,5), 2), [NaN 1 1.5 2 NaN]) %!assert_equal (unifinv (p, [1 2 NaN 1 1], 2), [NaN NaN NaN 2 NaN]) %!assert_equal (unifinv (p, 1, 2*[1 0 NaN 1 1]), [NaN NaN NaN 2 NaN]) %!assert_equal (unifinv ([p(1:2) NaN p(4:5)], 1, 2), [NaN 1 NaN 2 NaN]) ## Test class of input preserved %!assert_equal (unifinv ([p, NaN], 1, 2), [NaN 1 1.5 2 NaN NaN]) %!assert_equal (unifinv (single ([p, NaN]), 1, 2), single ([NaN 1 1.5 2 NaN NaN])) %!assert_equal (unifinv ([p, NaN], single (1), 2), single ([NaN 1 1.5 2 NaN NaN])) %!assert_equal (unifinv ([p, NaN], 1, single (2)), single ([NaN 1 1.5 2 NaN NaN])) ## Test input validation %!error unifinv () %!error unifinv (1, 2) %!error ... %! unifinv (ones (3), ones (2), ones (2)) %!error ... %! unifinv (ones (2), ones (3), ones (2)) %!error ... %! unifinv (ones (2), ones (2), ones (3)) %!error unifinv (int32 (2), 2, 2) %!error unifinv (true, 2, 2) %!error unifinv ('a', 2, 2) %!error unifinv (i, 2, 2) %!error unifinv (2, i, 2) %!error unifinv (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/unifpdf.m000066400000000000000000000124471527311104300246260ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} unifpdf (@var{x}, @var{a}, @var{b}) ## ## Continuous uniform probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the continuous uniform distribution with parameters @var{a} and @var{b}, ## which define the lower and upper bounds of the interval ## @qcode{[@var{a}, @var{b}]}. The size of @var{y} is the common size of ## @var{x}, @var{a}, and @var{b}. A scalar input functions as a constant matrix ## of the same size as the other inputs. ## ## Further information about the continuous uniform distribution can be found at ## @url{https://en.wikipedia.org/wiki/Continuous_uniform_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## MATLAB also accepts integer input here, returning the result in the integer ## class of the input; Octave rejects it, as it does for every other continuous ## distribution. ## ## @seealso{unifcdf, unifinv, unifrnd, unifit, unifstat} ## @end deftypefn function y = unifpdf (x, a, b) ## Check for valid number of input arguments if (nargin < 3) error ("unifpdf: function called with too few input arguments."); endif ## Check for common size of X, A, and B if (! isscalar (x) || ! isscalar (a) || ! isscalar (b)) [retval, x, a, b] = common_size (x, a, b); if (retval > 0) error ("unifpdf: X, A, and B must be of common size or scalars."); endif endif ## Check for X, A, and B being double or single if (! (isfloat (x) && isfloat (a) && isfloat (b))) error ("unifpdf: X, A, and B must be double or single."); endif ## Check for X, A, and B being reals if (iscomplex (x) || iscomplex (a) || iscomplex (b)) error ("unifpdf: X, A, and B must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (a, 'single') || isa (b, 'single')) y = zeros (size (x), 'single'); else y = zeros (size (x)); endif ## Calculate continuous uniform PDF for valid parameter and data range k = isnan (x) | ! (a < b); y(k) = NaN; k = (x >= a) & (x <= b) & (a < b); y(k) = 1 ./ (b(k) - a(k)); endfunction %!demo %! ## Plot various PDFs from the continuous uniform distribution %! x = 0:0.001:10; %! y1 = unifpdf (x, 2, 5); %! y2 = unifpdf (x, 3, 9); %! plot (x, y1, '-b', x, y2, '-g') %! grid on %! xlim ([0, 10]) %! ylim ([0, 0.4]) %! legend ({'a = 2, b = 5', 'a = 3, b = 9'}, 'location', 'northeast') %! title ('Continuous uniform PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x, y %! x = [-1 0 0.5 1 2] + 1; %! y = [0 1 1 1 0]; %!assert_equal (unifpdf (x, ones (1,5), 2*ones (1,5)), y) %!assert_equal (unifpdf (x, 1, 2*ones (1,5)), y) %!assert_equal (unifpdf (x, ones (1,5), 2), y) %!assert_equal (unifpdf (x, [2 NaN 1 1 1], 2), [NaN NaN y(3:5)]) %!assert_equal (unifpdf (x, 1, 2*[0 NaN 1 1 1]), [NaN NaN y(3:5)]) %!assert_equal (unifpdf ([x, NaN], 1, 2), [y, NaN]) %!assert_equal (unifpdf (x, 0, 1), [1 1 0 0 0]) ## Test class of input preserved %!assert_equal (unifpdf (single ([x, NaN]), 1, 2), single ([y, NaN])) %!assert_equal (unifpdf (single ([x, NaN]), single (1), 2), single ([y, NaN])) %!assert_equal (unifpdf ([x, NaN], 1, single (2)), single ([y, NaN])) ## Test input validation %!error unifpdf () %!error unifpdf (1) %!error unifpdf (1, 2) %!error ... %! unifpdf (ones (3), ones (2), ones (2)) %!error ... %! unifpdf (ones (2), ones (3), ones (2)) %!error ... %! unifpdf (ones (2), ones (2), ones (3)) %!error unifpdf (int32 (2), 2, 2) %!error unifpdf (true, 2, 2) %!error unifpdf ('a', 2, 2) %!error unifpdf (i, 2, 2) %!error unifpdf (2, i, 2) %!error unifpdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/unifrnd.m000066400000000000000000000152551527311104300246400ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2019 Anthony Morast ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} unifrnd (@var{a}, @var{b}) ## @deftypefnx {statistics} {@var{r} =} unifrnd (@var{a}, @var{b}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} unifrnd (@var{a}, @var{b}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} unifrnd (@var{a}, @var{b}, [@var{sz}]) ## ## Random arrays from the continuous uniform distribution. ## ## @code{@var{r} = unifrnd (@var{a}, @var{b})} returns an array of random ## numbers chosen from the continuous uniform distribution with parameters ## @var{a} and @var{b}, which define the lower and upper bounds of the interval ## @qcode{[@var{a}, @var{b}]}. The size of @var{r} is the common size of ## @var{a} and @var{b}. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## When called with a single size argument, @code{unifrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the continuous uniform distribution can be found at ## @url{https://en.wikipedia.org/wiki/Continuous_uniform_distribution} ## ## @seealso{unifcdf, unifinv, unifpdf, unifit, unifstat} ## @end deftypefn function r = unifrnd (a, b, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("unifrnd: function called with too few input arguments."); endif ## Check for common size of A and B if (! isscalar (a) || ! isscalar (b)) [retval, a, b] = common_size (a, b); if (retval > 0) error ("unifrnd: A and B must be of common size or scalars."); endif endif ## Check for A and B being reals if (iscomplex (a) || iscomplex (b)) error ("unifrnd: A and B must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (a); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("unifrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("unifrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (a) && ! isequal (size (a), size (ones (sz)))) error ("unifrnd: A and B must be scalars or of size SZ."); endif ## Check for class type if (isa (a, 'single') || isa (b, 'single')) cls = 'single'; else cls = 'double'; endif if (isscalar (a) && isscalar (b)) if ((-Inf < a) && (a <= b) && (b < Inf)) r = a + (b - a) * rand (sz, cls); else r = NaN (sz, cls); endif else r = a + (b - a) .* rand (sz, cls); k = ! (-Inf < a) | ! (a <= b) | ! (b < Inf); r(k) = NaN; endif endfunction ## Test output %!assert_equal (size (unifrnd (1, 1)), [1 1]) %!assert_equal (size (unifrnd (1, ones (2,1))), [2, 1]) %!assert_equal (size (unifrnd (1, ones (2,2))), [2, 2]) %!assert_equal (size (unifrnd (ones (2,1), 1)), [2, 1]) %!assert_equal (size (unifrnd (ones (2,2), 1)), [2, 2]) %!assert_equal (size (unifrnd (1, 1, 3)), [3, 3]) %!assert_equal (size (unifrnd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (unifrnd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (unifrnd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (unifrnd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (unifrnd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (unifrnd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (unifrnd (1, 1, [])), [0, 0]) %!assert_equal (size (unifrnd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (unifrnd (1, 2, -1)), [0, 0]) %!assert_equal (size (unifrnd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (unifrnd (1, 2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (unifrnd (1, 1)), "double") %!assert_equal (class (unifrnd (1, single (1))), "single") %!assert_equal (class (unifrnd (1, single ([1, 1]))), "single") %!assert_equal (class (unifrnd (single (1), 1)), "single") %!assert_equal (class (unifrnd (single ([1, 1]), 1)), "single") ## Test input validation %!error unifrnd () %!error unifrnd (1) %!error ... %! unifrnd (ones (3), ones (2)) %!error ... %! unifrnd (ones (2), ones (3)) %!error unifrnd (i, 2, 3) %!error unifrnd (1, i, 3) %!error ... %! unifrnd (1, 2, 1.2) %!error ... %! unifrnd (1, 2, ones (2)) %!error ... %! unifrnd (1, 2, [2 0 2.5]) %!error ... %! unifrnd (1, 2, 2, 1.5, 5) %!error ... %! unifrnd (2, ones (2), 3) %!error ... %! unifrnd (2, ones (2), [3, 2]) %!error ... %! unifrnd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/vmcdf.m000066400000000000000000000144331527311104300242670ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} vmcdf (@var{x}, @var{mu}, @var{k}) ## @deftypefnx {statistics} {@var{p} =} vmcdf (@var{x}, @var{mu}, @var{k}, @qcode{'upper'}) ## ## Von Mises probability density function (PDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the von Mises distribution with location parameter @var{mu} and ## concentration parameter @var{k} on the interval @math{[-pi,pi]}. The size of ## @var{p} is the common size of @var{x}, @var{mu}, and @var{k}. A scalar input ## functions as a constant matrix of the same same size as the other inputs. ## ## @code{@var{p} = vmcdf (@var{x}, @var{mu}, @var{k}, "upper")} computes the ## upper tail probability of the von Mises distribution with parameters @var{mu} ## and @var{k}, at the values in @var{x}. ## ## Note: the CDF of the von Mises distribution is not analytic. Hence, it is ## calculated by integrating its probability density which is expressed as a ## series of Bessel functions. Balancing between performance and accuracy, the ## integration uses a step of @qcode{1e-5} on the interval @math{[-pi,pi]}, ## which results to an accuracy of about 10 significant digits. ## ## Further information about the von Mises distribution can be found at ## @url{https://en.wikipedia.org/wiki/Von_Mises_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{vminv, vmpdf, vmrnd} ## @end deftypefn function p = vmcdf (x, mu, k, uflag) ## Check for valid number of input arguments if (nargin < 3) error ("vmcdf: function called with too few input arguments."); endif ## Check for valid "upper" flag if (nargin > 3) if (! strcmpi (uflag, 'upper')) error ("vmcdf: invalid argument for upper tail."); else uflag = true; endif else uflag = false; endif ## Check for common size of X, MU, and K if (! isscalar (x) || ! isscalar (mu) || ! isscalar (k)) [retval, x, mu, k] = common_size (x, mu, k); if (retval > 0) error ("vmcdf: X, MU, and K must be of common size or scalars."); endif endif ## Check for X, MU, and K being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (k))) error ("vmcdf: X, MU, and K must be double or single."); endif ## Check for X, MU, and K being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (k)) error ("vmcdf: X, MU, and K must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (k, 'single')) p = zeros (size (x), 'single'); else p = zeros (size (x)); endif ## Evaluate Von Mises CDF by integrating from -PI to PI interval = linspace (-pi, pi, 1e5)'; # accurate to >10 significant digits f = exp (k .* cos (interval)) ./ (2 .* pi .* besseli (0, k)); c = cumtrapz (interval, f); p = diag (interp1 (interval, c, x - mu, 'spline'))'; ## Force Nan for negative K p(k < 0) = NaN; ## Apply upper flag (if required) if (uflag) p = 1 - p; endif endfunction %!demo %! ## Plot various CDFs from the von Mises distribution %! x1 = [-pi:0.1:pi]; %! p1 = vmcdf (x1, 0, 0.5); %! p2 = vmcdf (x1, 0, 1); %! p3 = vmcdf (x1, 0, 2); %! p4 = vmcdf (x1, 0, 4); %! plot (x1, p1, '-r', x1, p2, '-g', x1, p3, '-b', x1, p4, '-c') %! grid on %! xlim ([-pi, pi]) %! legend ({'μ = 0, k = 0.5', 'μ = 0, k = 1', ... %! 'μ = 0, k = 2', 'μ = 0, k = 4'}, 'location', 'northwest') %! title ('Von Mises CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, p0, p1 %! x = [-pi:pi/2:pi]; %! p0 = [0, 0.10975, 0.5, 0.89025, 1]; %! p1 = [0, 0.03752, 0.5, 0.99622, 1]; %!assert_equal (vmcdf (x, 0, 1), p0, 1e-5) %!assert_equal (vmcdf (x, 0, 1, 'upper'), 1 - p0, 1e-5) %!assert_equal (vmcdf (x, zeros (1,5), ones (1,5)), p0, 1e-5) %!assert_equal (vmcdf (x, zeros (1,5), ones (1,5), 'upper'), 1 - p0, 1e-5) %!assert_equal (vmcdf (x, 0, [1 2 3 4 5]), p1, 1e-5) %!assert_equal (vmcdf (x, 0, [1 2 3 4 5], 'upper'), 1 - p1, 1e-5) ## Test class of input preserved %!assert_equal (isa (vmcdf (single (pi), 0, 1), 'single'), true) %!assert_equal (isa (vmcdf (pi, single (0), 1), 'single'), true) %!assert_equal (isa (vmcdf (pi, 0, single (1)), 'single'), true) ## Test input validation %!error vmcdf () %!error vmcdf (1) %!error vmcdf (1, 2) %!error vmcdf (1, 2, 3, 'tail') %!error vmcdf (1, 2, 3, 4) %!error ... %! vmcdf (ones (3), ones (2), ones (2)) %!error ... %! vmcdf (ones (2), ones (3), ones (2)) %!error ... %! vmcdf (ones (2), ones (2), ones (3)) %!error vmcdf (int32 (2), 2, 2) %!error vmcdf (true, 2, 2) %!error vmcdf ('a', 2, 2) %!error vmcdf (i, 2, 2) %!error vmcdf (2, i, 2) %!error vmcdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/vminv.m000066400000000000000000000145111527311104300243240ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} vminv (@var{p}, @var{mu}, @var{k}) ## ## Inverse of the von Mises cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) of ## the von Mises distribution with location parameter @var{mu} and concentration ## parameter @var{k} on the interval @math{[-pi,pi]}. The size of @var{x} is ## the common size of @var{p}, @var{mu}, and @var{k}. A scalar input functions ## as a constant matrix of the same size as the other inputs. ## ## Note: the quantile of the von Mises distribution is not analytic. Hence, it ## is approximated by a custom searching algorithm using its CDF until it ## converges up to a tolerance of @qcode{1e-5} or 100 iterations. As a result, ## balancing between performance and accuracy, the accuracy is about ## @qcode{5e-5} for @qcode{@var{k} = 1} and it drops to @qcode{5e-5} as @var{k} ## increases. ## ## Further information about the von Mises distribution can be found at ## @url{https://en.wikipedia.org/wiki/Von_Mises_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{vmcdf, vmpdf, vmrnd} ## @end deftypefn function x = vminv (p, mu, k) ## Check for valid number of input arguments if (nargin < 3) error ("vminv: function called with too few input arguments."); endif ## Check for common size of P, MU, and K [err, p, mu, k] = common_size (p, mu, k); if (err > 0) error ("vminv: P, MU, and K must be of common size or scalars."); endif ## Check for P, MU, and K being double or single if (! (isfloat (p) && isfloat (mu) && isfloat (k))) error ("vminv: P, MU, and K must be double or single."); endif ## Check for P, MU, and K being reals if (iscomplex (p) || iscomplex (mu) || iscomplex (k)) error ("vminv: P, MU, and K must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (mu, 'single') || isa (k, 'single')) x = NaN (size (p), 'single'); else x = NaN (size (p), 'double'); endif ## Process edge cases p=0, p=0.5, p=1 p_0 = p < eps (class (x)) & k > 0 & isfinite (mu); x(p_0) = -pi + mu(p_0); p_5 = abs (p - 0.5) < eps (class (x)) & k > 0 & isfinite (mu); x(p_5) = mu(p_5); p_1 = 1 - p < eps (class (x)) & k > 0 & isfinite (mu); x(p_1) = pi + mu(p_1); ## Get remaining valid cases valc = p > 0 & p < 1 & ! p_5 & k > 0 & isfinite (mu); if (! any (valc)) return endif p = p(valc); mu = mu(valc); k = k(valc); ## Complement cases of p<0.5 to 1-p and keep track to invert them at the end comp = p < 0.5; p(comp) = 1 - p(comp); ## Initialize counter and threshold crit = 1e-5; count_limit = 100; count = 0; ## Supply a starting guess for the iteration by linear interpolation with k=0 x0 = 2 * pi .* p - pi; xz = zeros (size (p)); xa = xz; ## Compute p0 and compare to target p p0 = vmcdf (x0, 0, k); ## Solution is always 0 < x < x0. Search for x until p == p0 within threshold while (any (abs (p - p0) > crit) && count < count_limit) count = count + 1; xnew = xz + (abs (x0) - abs (xz)) .* 0.5; p0 = vmcdf (xnew, 0, k); ## Prepare for next step xdec = (p0 - p) > crit; xinc = (p - p0) > crit; if (any (xdec)) x0(xdec) = xnew(xdec); endif if (any (xinc)) xz(xinc) = xnew(xinc); endif endwhile ## Return the converged value(s). xnew(comp) = -xnew(comp); x(valc) = xnew + mu; if (count == count_limit) warning ("vminv: did not converge."); endif endfunction %!demo %! ## Plot various iCDFs from the von Mises distribution %! p1 = [0,0.005,0.01:0.01:0.1,0.15,0.2:0.1:0.8,0.85,0.9:0.01:0.99,0.995,1]; %! x1 = vminv (p1, 0, 0.5); %! x2 = vminv (p1, 0, 1); %! x3 = vminv (p1, 0, 2); %! x4 = vminv (p1, 0, 4); %! plot (p1, x1, '-r', p1, x2, '-g', p1, x3, '-b', p1, x4, '-c') %! grid on %! ylim ([-pi, pi]) %! legend ({'μ = 0, k = 0.5', 'μ = 0, k = 1', ... %! 'μ = 0, k = 2', 'μ = 0, k = 4'}, 'location', 'northwest') %! title ('Von Mises iCDF') %! xlabel ('probability') %! ylabel ('values in x') ## Test output %!shared x, p0, p1 %! x = [-pi:pi/2:pi]; %! p0 = [0, 0.10975, 0.5, 0.89025, 1]; %! p1 = [0, 0.03752, 0.5, 0.99622, 1]; %!assert_equal (vminv (p0, 0, 1), x, 5e-5) %!assert_equal (vminv (p0, zeros (1,5), ones (1,5)), x, 5e-5) %!assert_equal (vminv (p1, 0, [1 2 3 4 5]), x, [5e-5, 5e-4, 5e-5, 5e-4, 5e-5]) ## Test input validation %!error vminv () %!error vminv (1) %!error vminv (1, 2) %!error ... %! vminv (ones (3), ones (2), ones (2)) %!error ... %! vminv (ones (2), ones (3), ones (2)) %!error ... %! vminv (ones (2), ones (2), ones (3)) %!error vminv (int32 (2), 2, 2) %!error vminv (true, 2, 2) %!error vminv ('a', 2, 2) %!error vminv (i, 2, 2) %!error vminv (2, i, 2) %!error vminv (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/vmpdf.m000066400000000000000000000116361527311104300243060ustar00rootroot00000000000000## Copyright (C) 2009 Soren Hauberg ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} vmpdf (@var{x}, @var{mu}, @var{k}) ## ## Von Mises probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the von Mises distribution with location parameter @var{mu} and ## concentration parameter @var{k} on the interval [-pi, pi]. The size of ## @var{y} is the common size of @var{x}, @var{mu}, and @var{k}. A scalar input ## functions as a constant matrix of the same size as the other inputs. ## ## Further information about the von Mises distribution can be found at ## @url{https://en.wikipedia.org/wiki/Von_Mises_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{vmcdf, vminv, vmrnd} ## @end deftypefn function y = vmpdf (x, mu, k) ## Check for valid number of input arguments if (nargin < 3) error ("vmpdf: function called with too few input arguments."); endif ## Check for common size of X, MU, and K if (! isscalar (x) || ! isscalar (mu) || ! isscalar (k)) [retval, x, mu, k] = common_size (x, mu, k); if (retval > 0) error ("vmpdf: X, MU, and K must be of common size or scalars."); endif endif ## Check for X, MU, and K being double or single if (! (isfloat (x) && isfloat (mu) && isfloat (k))) error ("vmpdf: X, MU, and K must be double or single."); endif ## Check for X, MU, and K being reals if (iscomplex (x) || iscomplex (mu) || iscomplex (k)) error ("vmpdf: X, MU, and K must not be complex."); endif ## Evaluate Von Mises PDF Z = 2 .* pi .* besseli (0, k); y = exp (k .* cos (x - mu)) ./ Z; ## Force Nan for negative K y(k < 0) = NaN; ## Check for class type if (isa (x, 'single') || isa (mu, 'single') || isa (k, 'single')) y = cast (y, 'single'); else y = cast (y, 'double'); endif endfunction %!demo %! ## Plot various PDFs from the von Mises distribution %! x1 = [-pi:0.1:pi]; %! y1 = vmpdf (x1, 0, 0.5); %! y2 = vmpdf (x1, 0, 1); %! y3 = vmpdf (x1, 0, 2); %! y4 = vmpdf (x1, 0, 4); %! plot (x1, y1, '-r', x1, y2, '-g', x1, y3, '-b', x1, y4, '-c') %! grid on %! xlim ([-pi, pi]) %! ylim ([0, 0.8]) %! legend ({'μ = 0, k = 0.5', 'μ = 0, k = 1', ... %! 'μ = 0, k = 2', 'μ = 0, k = 4'}, 'location', 'northwest') %! title ('Von Mises PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x, y0, y1 %! x = [-pi:pi/2:pi]; %! y0 = [0.046245, 0.125708, 0.341710, 0.125708, 0.046245]; %! y1 = [0.046245, 0.069817, 0.654958, 0.014082, 0.000039]; %!assert_equal (vmpdf (x, 0, 1), y0, 1e-5) %!assert_equal (vmpdf (x, zeros (1,5), ones (1,5)), y0, 1e-6) %!assert_equal (vmpdf (x, 0, [1 2 3 4 5]), y1, 1e-6) ## Test class of input preserved %!assert_equal (isa (vmpdf (single (pi), 0, 1), 'single'), true) %!assert_equal (isa (vmpdf (pi, single (0), 1), 'single'), true) %!assert_equal (isa (vmpdf (pi, 0, single (1)), 'single'), true) ## Test input validation %!error vmpdf () %!error vmpdf (1) %!error vmpdf (1, 2) %!error ... %! vmpdf (ones (3), ones (2), ones (2)) %!error ... %! vmpdf (ones (2), ones (3), ones (2)) %!error ... %! vmpdf (ones (2), ones (2), ones (3)) %!error vmpdf (int32 (2), 2, 2) %!error vmpdf (true, 2, 2) %!error vmpdf ('a', 2, 2) %!error vmpdf (i, 2, 2) %!error vmpdf (2, i, 2) %!error vmpdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/vmrnd.m000066400000000000000000000166351527311104300243240ustar00rootroot00000000000000## Copyright (C) 2009 Soren Hauberg ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} vmrnd (@var{mu}, @var{k}) ## @deftypefnx {statistics} {@var{r} =} vmrnd (@var{mu}, @var{k}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} vmrnd (@var{mu}, @var{k}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} vmrnd (@var{mu}, @var{k}, [@var{sz}]) ## ## Random arrays from the von Mises distribution. ## ## @code{@var{r} = vmrnd (@var{mu}, @var{k})} returns an array of random angles ## chosen from a von Mises distribution with location parameter @var{mu} and ## concentration parameter @var{k} on the interval [-pi, pi]. The size of ## @var{r} is the common size of @var{mu} and @var{k}. A scalar input functions ## as a constant matrix of the same size as the other inputs. Both parameters ## must be finite real numbers and @var{k} > 0, otherwise NaN is returned. ## ## When called with a single size argument, @code{vmrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the von Mises distribution can be found at ## @url{https://en.wikipedia.org/wiki/Von_Mises_distribution} ## ## @seealso{vmcdf, vminv, vmpdf} ## @end deftypefn function r = vmrnd (mu, k, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("vmrnd: function called with too few input arguments."); endif ## Check for common size of MU and K if (! isscalar (mu) || ! isscalar (k)) [retval, mu, k] = common_size (mu, k); if (retval > 0) error ("vmrnd: MU and K must be of common size or scalars."); endif endif ## Check for MU and K being reals if (iscomplex (mu) || iscomplex (k)) error ("vmrnd: MU and K must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (mu); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("vmrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("vmrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (mu) && ! isequal (size (k), size (ones (sz)))) error ("vmrnd: MU and K must be scalars or of size SZ."); endif ## Check for class type if (isa (mu, 'single') || isa (k, 'single')) cls = 'single'; else cls = 'double'; endif ## Handle zero size dimensions if (any (sz == 0)) r = nan (sz, cls); return endif ## Simulate! if (all (k < 1e-6)) ## k is small: sample uniformly on circle r = mu + (2 * pi * rand (sz) - pi); else a = 1 + sqrt (1 + 4 .* k .^ 2); b = (a - sqrt (2 .* a)) ./ (2 .* k); r_tmp = (1 + b .^ 2) ./ (2 .* b); N = prod (sz); if (isscalar (k)) r_tmp = repmat (r_tmp, 1, N); k_tmp = repmat (k, 1, N); mu_rs = repmat (mu, 1, N); else r_tmp = reshape (r_tmp, 1, N); k_tmp = reshape (k, 1, N); mu_rs = reshape (mu, 1, N); endif notdone = true (N, 1); while (any (notdone)) u(:, notdone) = (rand (3, N))(:,notdone); z(notdone) = (cos (pi .* u(1, :)))(notdone); f(notdone) = ((1 + r_tmp(notdone) .* z(notdone)) ./ (r_tmp(notdone) + z(notdone))); c(notdone) = (k_tmp(notdone) .* (r_tmp(notdone) - f(notdone))); notdone = (u(2, :) >= c .* (2 - c)) & (log (c) - log (u(2, :)) + 1 - c < 0); #N = sum (notdone); endwhile r = mu_rs + sign (u(3, :) - 0.5) .* acos (f); r = reshape (r, sz); endif ## Cast to appropriate class r = cast (r, cls); endfunction ## Test output %!assert_equal (size (vmrnd (1, 1)), [1, 1]) %!assert_equal (size (vmrnd (1, ones (2, 1))), [2, 1]) %!assert_equal (size (vmrnd (1, ones (2, 2))), [2, 2]) %!assert_equal (size (vmrnd (ones (2, 1), 1)), [2, 1]) %!assert_equal (size (vmrnd (ones (2, 2), 1)), [2, 2]) %!assert_equal (size (vmrnd (1, 1, 3)), [3, 3]) %!assert_equal (size (vmrnd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (vmrnd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (vmrnd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (vmrnd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (vmrnd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (vmrnd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (vmrnd (1, 1, [])), [0, 0]) %!assert_equal (size (vmrnd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (vmrnd (1, 2, -1)), [0, 0]) %!assert_equal (size (vmrnd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (vmrnd (1, 2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (vmrnd (1, 1)), "double") %!assert_equal (class (vmrnd (1, single (1))), "single") %!assert_equal (class (vmrnd (1, single ([1, 1]))), "single") %!assert_equal (class (vmrnd (single (1), 1)), "single") %!assert_equal (class (vmrnd (single ([1, 1]), 1)), "single") ## Test input validation %!error vmrnd () %!error vmrnd (1) %!error ... %! vmrnd (ones (3), ones (2)) %!error ... %! vmrnd (ones (2), ones (3)) %!error vmrnd (i, 2, 3) %!error vmrnd (1, i, 3) %!error ... %! vmrnd (1, 2, 1.2) %!error ... %! vmrnd (1, 2, ones (2)) %!error ... %! vmrnd (1, 2, [2 0 2.5]) %!error ... %! vmrnd (1, 2, 2, 1.5, 5) %!error ... %! vmrnd (2, ones (2), 3) %!error ... %! vmrnd (2, ones (2), [3, 2]) %!error ... %! vmrnd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/wblcdf.m000066400000000000000000000231711527311104300244300ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} wblcdf (@var{x}) ## @deftypefnx {statistics} {@var{p} =} wblcdf (@var{x}, @var{lambda}) ## @deftypefnx {statistics} {@var{p} =} wblcdf (@var{x}, @var{lambda}, @var{k}) ## @deftypefnx {statistics} {@var{p} =} wblcdf (@dots{}, @qcode{'upper'}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} wblcdf (@var{x}, @var{lambda}, @var{k}, @var{pcov}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} wblcdf (@var{x}, @var{lambda}, @var{k}, @var{pcov}, @var{alpha}) ## @deftypefnx {statistics} {[@var{p}, @var{plo}, @var{pup}] =} wblcdf (@dots{}, @qcode{'upper'}) ## ## Weibull cumulative distribution function (CDF). ## ## For each element of @var{x}, compute the cumulative distribution function ## (CDF) of the Weibull distribution with scale parameter @var{lambda} and shape ## parameter @var{k}. The size of @var{p} is the common size of @var{x}, ## @var{lambda} and @var{k}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## Default values are @var{lambda} = 1, @var{k} = 1. ## ## When called with three output arguments, @code{[@var{p}, @var{plo}, ## @var{pup}]} it computes the confidence bounds for @var{p} when the input ## parameters @var{lambda} and @var{k} are estimates. In such case, @var{pcov}, ## a 2-by-2 matrix containing the covariance matrix of the estimated parameters, ## is necessary. Optionally, @var{alpha} has a default value of 0.05, and ## specifies 100 * (1 - @var{alpha})% confidence bounds. @var{plo} and @var{pup} ## are arrays of the same size as @var{p} containing the lower and upper ## confidence bounds. ## ## @code{[@dots{}] = wblcdf (@dots{}, "upper")} computes the upper tail ## probability of the lognormal distribution. ## ## Further information about the Weibull distribution can be found at ## @url{https://en.wikipedia.org/wiki/Weibull_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## The @code{prob.WeibullDistribution} class names these same two parameters ## @qcode{A} and @qcode{B}, after MATLAB. @var{lambda} is its @qcode{A} and ## @var{k} is its @qcode{B}. ## @seealso{wblinv, wblpdf, wblrnd, wblstat, wblplot} ## @end deftypefn function [varargout] = wblcdf (x, varargin) ## Check for valid number of input arguments if (nargin < 1 || nargin > 6) error ("wblcdf: invalid number of input arguments."); endif ## Check for "upper" flag if (nargin > 1 && strcmpi (varargin{end}, 'upper')) uflag = true; varargin(end) = []; elseif (nargin > 1 && ischar (varargin{end}) && ... ! strcmpi (varargin{end}, 'upper')) error ("wblcdf: invalid argument for upper tail."); elseif (nargin > 1 && isempty (varargin{end})) uflag = false; varargin(end) = []; else uflag = false; endif ## Get extra arguments (if they exist) or add defaults if (numel (varargin) > 0) lambda = varargin{1}; else lambda = 1; endif if (numel (varargin) > 1) k = varargin{2}; else k = 1; endif if (numel (varargin) > 2) pcov = varargin{3}; ## Check for valid covariance matrix 2x2 if (! isequal (size (pcov), [2, 2])) error ("wblcdf: invalid size of covariance matrix."); endif else ## Check that cov matrix is provided if 3 output arguments are requested if (nargout > 1) error ("wblcdf: covariance matrix is required for confidence bounds."); endif pcov = []; endif if (numel (varargin) > 3) alpha = varargin{4}; ## Check for valid alpha value if (! isnumeric (alpha) || numel (alpha) !=1 || alpha <= 0 || alpha >= 1) error ("wblcdf: invalid value for alpha."); endif else alpha = 0.05; endif ## Check for common size of X, LAMBDA, and K if (! isscalar (x) || ! isscalar (lambda) || ! isscalar (k)) [err, x, lambda, k] = common_size (x, lambda, k); if (err > 0) error ("wblcdf: X, LAMBDA, and K must be of common size or scalars."); endif endif ## Check for X, LAMBDA, and K being double or single if (! (isfloat (x) && isfloat (lambda) && isfloat (k))) error ("wblcdf: X, LAMBDA, and K must be double or single."); endif ## Check for X, LAMBDA, and K being reals if (iscomplex (x) || iscomplex (lambda) || iscomplex (k)) error ("wblcdf: X, LAMBDA, and K must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (lambda, 'single') || isa (k, 'single')); is_class = 'single'; else is_class = 'double'; endif ## Return NaN for out of range parameters. lambda(lambda <= 0) = NaN; k(k <= 0) = NaN; ## Force 0 for negative data x(x < 0) = 0; ## Compute z z = (x ./ lambda) .^ k; if (uflag) p = exp (-z); else p = -expm1 (-z); endif ## Compute confidence bounds (if requested) if (nargout >= 2) ## Work on log scale log_z = log (z); d_lambda = 1 ./ lambda; d_k = -1 ./ (k .^ 2); log_zvar = (pcov(1,1) .* d_lambda .^ 2 + ... 2 * pcov(1,2) .* d_lambda .* d_k .* log_z + ... pcov(2,2) .* (d_k .* log_z) .^ 2) .* (k .^ 2); if (any (log_zvar < 0)) error ("wblcdf: bad covariance matrix."); endif normz = -norminv (alpha / 2); halfwidth = normz * sqrt (log_zvar); zlo = log_z - halfwidth; zup = log_z + halfwidth; ## Convert back from log scale if uflag == true plo = exp (-exp (zup)); pup = exp (-exp (zlo)); else plo = -expm1 (-exp (zlo)); pup = -expm1 (-exp (zup)); endif endif ## Prepare output varargout{1} = cast (p, is_class); if (nargout > 1) varargout{2} = cast (plo, is_class); varargout{3} = cast (pup, is_class); endif endfunction %!demo %! ## Plot various CDFs from the Weibull distribution %! x = 0:0.001:2.5; %! p1 = wblcdf (x, 1, 0.5); %! p2 = wblcdf (x, 1, 1); %! p3 = wblcdf (x, 1, 1.5); %! p4 = wblcdf (x, 1, 5); %! plot (x, p1, '-b', x, p2, '-r', x, p3, '-m', x, p4, '-g') %! grid on %! legend ({'λ = 1, k = 0.5', 'λ = 1, k = 1', ... %! 'λ = 1, k = 1.5', 'λ = 1, k = 5'}, 'location', 'southeast') %! title ('Weibull CDF') %! xlabel ('values in x') %! ylabel ('probability') ## Test output %!shared x, y %! x = [-1 0 0.5 1 Inf]; %! y = [0, 1-exp(-x(2:4)), 1]; %!assert_equal (wblcdf (x, ones (1,5), ones (1,5)), y, 1e-16) %!assert_equal (wblcdf (x, ones (1,5), ones (1,5), 'upper'), 1 - y) %!assert_equal (wblcdf (x, 'upper'), 1 - y) %!assert_equal (wblcdf (x, 1, ones (1,5)), y, 1e-16) %!assert_equal (wblcdf (x, ones (1,5), 1), y, 1e-16) %!assert_equal (wblcdf (x, [0 1 NaN Inf 1], 1), [NaN 0 NaN 0 1]) %!assert_equal (wblcdf (x, [0 1 NaN Inf 1], 1, 'upper'), 1 - [NaN 0 NaN 0 1]) %!assert_equal (wblcdf (x, 1, [0 1 NaN Inf 1]), [NaN 0 NaN y(4:5)]) %!assert_equal (wblcdf (x, 1, [0 1 NaN Inf 1], 'upper'), 1 - [NaN 0 NaN y(4:5)]) %!assert_equal (wblcdf ([x(1:2) NaN x(4:5)], 1, 1), [y(1:2) NaN y(4:5)]) %!assert_equal (wblcdf ([x(1:2) NaN x(4:5)], 1, 1, 'upper'), 1 - [y(1:2) NaN y(4:5)]) ## Test class of input preserved %!assert_equal (wblcdf ([x, NaN], 1, 1), [y, NaN], 1e-16) %!assert_equal (wblcdf (single ([x, NaN]), 1, 1), single ([y, NaN])) %!assert_equal (wblcdf ([x, NaN], single (1), 1), single ([y, NaN])) %!assert_equal (wblcdf ([x, NaN], 1, single (1)), single ([y, NaN])) ## Test input validation %!error wblcdf () %!error wblcdf (1,2,3,4,5,6,7) %!error wblcdf (1, 2, 3, 4, 'uper') %!error ... %! wblcdf (ones (3), ones (2), ones (2)) %!error wblcdf (2, 3, 4, [1, 2]) %!error ... %! [p, plo, pup] = wblcdf (1, 2, 3) %!error [p, plo, pup] = ... %! wblcdf (1, 2, 3, [1, 0; 0, 1], 0) %!error [p, plo, pup] = ... %! wblcdf (1, 2, 3, [1, 0; 0, 1], 1.22) %!error [p, plo, pup] = ... %! wblcdf (1, 2, 3, [1, 0; 0, 1], 'alpha', 'upper') %!error wblcdf (int32 (2), 2, 2) %!error wblcdf (true, 2, 2) %!error wblcdf ('a', 2, 2) %!error wblcdf (i, 2, 2) %!error wblcdf (2, i, 2) %!error wblcdf (2, 2, i) %!error ... %! [p, plo, pup] =wblcdf (1, 2, 3, [1, 0; 0, -inf], 0.04) statistics-release-2.1.2/inst/Distribution_Functions/wblinv.m000066400000000000000000000140141527311104300244640ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} wblinv (@var{p}) ## @deftypefnx {statistics} {@var{x} =} wblinv (@var{p}, @var{lambda}) ## @deftypefnx {statistics} {@var{x} =} wblinv (@var{p}, @var{lambda}, @var{k}) ## ## Inverse of the Weibull cumulative distribution function (iCDF). ## ## For each element of @var{p}, compute the quantile (the inverse of the CDF) ## of the Weibull distribution with scale parameter @var{lambda} and shape ## parameter @var{k}. The size of @var{x} is the common size of @var{p}, ## @var{lambda}, and @var{k}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## Default values are @var{lambda} = 1, @var{k} = 1. ## ## Further information about the Weibull distribution can be found at ## @url{https://en.wikipedia.org/wiki/Weibull_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## The @code{prob.WeibullDistribution} class names these same two parameters ## @qcode{A} and @qcode{B}, after MATLAB. @var{lambda} is its @qcode{A} and ## @var{k} is its @qcode{B}. ## @seealso{wblcdf, wblpdf, wblrnd, wblstat, wblplot} ## @end deftypefn function x = wblinv (p, varargin) ## Check for valid number of input arguments if (nargin < 1 || nargin > 3) error ("wblinv: invalid number of input arguments."); endif ## Get extra arguments (if they exist) or add defaults if (numel (varargin) > 0) lambda = varargin{1}; else lambda = 1; endif if (numel (varargin) > 1) k = varargin{2}; else k = 1; endif ## Check for common size of P, LAMBDA, and K if (! isscalar (p) || ! isscalar (lambda) || ! isscalar (k)) [retval, p, lambda, k] = common_size (p, lambda, k); if (retval > 0) error ("wblinv: P, LAMBDA, and K must be of common size or scalars."); endif endif ## Check for P, LAMBDA, and K being double or single if (! (isfloat (p) && isfloat (lambda) && isfloat (k))) error ("wblinv: P, LAMBDA, and K must be double or single."); endif ## Check for P, LAMBDA, and K being reals if (iscomplex (p) || iscomplex (lambda) || iscomplex (k)) error ("wblinv: P, LAMBDA, and K must not be complex."); endif ## Check for class type if (isa (p, 'single') || isa (lambda, 'single') || isa (k, 'single')) x = NaN (size (p), 'single'); else x = NaN (size (p)); endif ok = (lambda > 0) & (lambda < Inf) & (k > 0) & (k < Inf); pk = (p == 0) & ok; x(pk) = 0; pk = (p == 1) & ok; x(pk) = Inf; pk = (p > 0) & (p < 1) & ok; if (isscalar (lambda) && isscalar (k)) x(pk) = lambda * (- log (1 - p(pk))) .^ (1 / k); else x(pk) = lambda(pk) .* (- log (1 - p(pk))) .^ (1 ./ k(pk)); endif endfunction %!demo %! ## Plot various iCDFs from the Weibull distribution %! p = 0.001:0.001:0.999; %! x1 = wblinv (p, 1, 0.5); %! x2 = wblinv (p, 1, 1); %! x3 = wblinv (p, 1, 1.5); %! x4 = wblinv (p, 1, 5); %! plot (p, x1, '-b', p, x2, '-r', p, x3, '-m', p, x4, '-g') %! ylim ([0, 2.5]) %! grid on %! legend ({'λ = 1, k = 0.5', 'λ = 1, k = 1', ... %! 'λ = 1, k = 1.5', 'λ = 1, k = 5'}, 'location', 'northwest') %! title ('Weibull iCDF') %! xlabel ('probability') %! ylabel ('x') ## Test output %!shared p %! p = [-1 0 0.63212055882855778 1 2]; %!assert_equal (wblinv (p, ones (1,5), ones (1,5)), [NaN 0 1 Inf NaN], eps) %!assert_equal (wblinv (p, 1, ones (1,5)), [NaN 0 1 Inf NaN], eps) %!assert_equal (wblinv (p, ones (1,5), 1), [NaN 0 1 Inf NaN], eps) %!assert_equal (wblinv (p, [1 -1 NaN Inf 1], 1), [NaN NaN NaN NaN NaN]) %!assert_equal (wblinv (p, 1, [1 -1 NaN Inf 1]), [NaN NaN NaN NaN NaN]) %!assert_equal (wblinv ([p(1:2) NaN p(4:5)], 1, 1), [NaN 0 NaN Inf NaN]) ## Test class of input preserved %!assert_equal (wblinv ([p, NaN], 1, 1), [NaN 0 1 Inf NaN NaN], eps) %!assert_equal (wblinv (single ([p, NaN]), 1, 1), single ([NaN 0 1 Inf NaN NaN]), eps ('single')) %!assert_equal (wblinv ([p, NaN], single (1), 1), single ([NaN 0 1 Inf NaN NaN]), eps ('single')) %!assert_equal (wblinv ([p, NaN], 1, single (1)), single ([NaN 0 1 Inf NaN NaN]), eps ('single')) ## Test input validation %!error wblinv () %!error wblinv (1,2,3,4) %!error ... %! wblinv (ones (3), ones (2), ones (2)) %!error ... %! wblinv (ones (2), ones (3), ones (2)) %!error ... %! wblinv (ones (2), ones (2), ones (3)) %!error wblinv (int32 (2), 2, 2) %!error wblinv (true, 2, 2) %!error wblinv ('a', 2, 2) %!error wblinv (i, 2, 2) %!error wblinv (2, i, 2) %!error wblinv (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/wblpdf.m000066400000000000000000000131431527311104300244430ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} wblpdf (@var{x}) ## @deftypefnx {statistics} {@var{y} =} wblpdf (@var{x}, @var{lambda}) ## @deftypefnx {statistics} {@var{y} =} wblpdf (@var{x}, @var{lambda}, @var{k}) ## ## Weibull probability density function (PDF). ## ## For each element of @var{x}, compute the probability density function (PDF) ## of the Weibull distribution with scale parameter @var{lambda} and shape ## parameter @var{k}. The size of @var{y} is the common size of @var{x}, ## @var{lambda}, and @var{k}. A scalar input functions as a constant matrix of ## the same size as the other inputs. ## ## Default values are @var{lambda} = 1, @var{k} = 1. ## ## Further information about the Weibull distribution can be found at ## @url{https://en.wikipedia.org/wiki/Weibull_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## The @code{prob.WeibullDistribution} class names these same two parameters ## @qcode{A} and @qcode{B}, after MATLAB. @var{lambda} is its @qcode{A} and ## @var{k} is its @qcode{B}. ## @seealso{wblcdf, wblinv, wblrnd, wblfit, wbllike, wblstat, wblplot} ## @end deftypefn function y = wblpdf (x, varargin) ## Check for valid number of input arguments if (nargin < 1 || nargin > 3) error ("wblpdf: invalid number of input arguments."); endif ## Get extra arguments (if they exist) or add defaults if (numel (varargin) > 0) lambda = varargin{1}; else lambda = 1; endif if (numel (varargin) > 1) k = varargin{2}; else k = 1; endif ## Check for common size of X, LAMBDA, and K if (! isscalar (lambda) || ! isscalar (k)) [retval, x, lambda, k] = common_size (x, lambda, k); if (retval > 0) error ("wblpdf: X, LAMBDA, and K must be of common size or scalars."); endif endif ## Check for X, LAMBDA, and K being double or single if (! (isfloat (x) && isfloat (lambda) && isfloat (k))) error ("wblpdf: X, LAMBDA, and K must be double or single."); endif ## Check for X, LAMBDA, and K being reals if (iscomplex (x) || iscomplex (lambda) || iscomplex (k)) error ("wblpdf: X, LAMBDA, and K must not be complex."); endif ## Check for class type if (isa (x, 'single') || isa (lambda, 'single') || isa (k, 'single')) y = NaN (size (x), 'single'); else y = NaN (size (x)); endif ok = ((lambda > 0) & (lambda < Inf) & (k > 0) & (k < Inf)); xk = (x < 0) & ok; y(xk) = 0; ## The density at an infinite abscissa is zero: no proper distribution ## places mass there. y(isinf (x) & (x > 0) & ok) = 0; xk = (x >= 0) & (x < Inf) & ok; if (isscalar (lambda) && isscalar (k)) y(xk) = (k * (lambda .^ -k) ... .* (x(xk) .^ (k - 1)) ... .* exp (- (x(xk) / lambda) .^ k)); else y(xk) = (k(xk) .* (lambda(xk) .^ -k(xk)) ... .* (x(xk) .^ (k(xk) - 1)) ... .* exp (- (x(xk) ./ lambda(xk)) .^ k(xk))); endif endfunction %!demo %! ## Plot various PDFs from the Weibull distribution %! x = 0:0.001:2.5; %! y1 = wblpdf (x, 1, 0.5); %! y2 = wblpdf (x, 1, 1); %! y3 = wblpdf (x, 1, 1.5); %! y4 = wblpdf (x, 1, 5); %! plot (x, y1, '-b', x, y2, '-r', x, y3, '-m', x, y4, '-g') %! grid on %! ylim ([0, 2.5]) %! legend ({'λ = 5, k = 0.5', 'λ = 9, k = 1', ... %! 'λ = 6, k = 1.5', 'λ = 2, k = 5'}, 'location', 'northeast') %! title ('Weibull PDF') %! xlabel ('values in x') %! ylabel ('density') ## Test output %!shared x,y %! x = [-1 0 0.5 1 Inf]; %! y = [0, exp(-x(2:4)), 0]; %!assert_equal (wblpdf (x, ones (1,5), ones (1,5)), y) %!assert_equal (wblpdf (x, 1, ones (1,5)), y) %!assert_equal (wblpdf (x, ones (1,5), 1), y) %!assert_equal (wblpdf (x, [0 NaN Inf 1 1], 1), [NaN NaN NaN y(4:5)]) %!assert_equal (wblpdf (x, 1, [0 NaN Inf 1 1]), [NaN NaN NaN y(4:5)]) %!assert_equal (wblpdf ([x, NaN], 1, 1), [y, NaN]) ## Test class of input preserved %!assert_equal (wblpdf (single ([x, NaN]), 1, 1), single ([y, NaN])) %!assert_equal (wblpdf ([x, NaN], single (1), 1), single ([y, NaN])) %!assert_equal (wblpdf ([x, NaN], 1, single (1)), single ([y, NaN])) ## Test input validation %!error wblpdf (int32 (2), 1, 1) %!error wblpdf (true, 1, 1) %!error wblpdf ('a', 1, 1) %!error wblpdf () %!error wblpdf (1,2,3,4) %!error wblpdf (ones (3), ones (2), ones (2)) %!error wblpdf (ones (2), ones (3), ones (2)) %!error wblpdf (ones (2), ones (2), ones (3)) %!error wblpdf (i, 2, 2) %!error wblpdf (2, i, 2) %!error wblpdf (2, 2, i) statistics-release-2.1.2/inst/Distribution_Functions/wblrnd.m000066400000000000000000000157161527311104300244650ustar00rootroot00000000000000## Copyright (C) 2012 Rik Wehbring ## Copyright (C) 1995-2016 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} wblrnd (@var{lambda}, @var{k}) ## @deftypefnx {statistics} {@var{r} =} wblrnd (@var{lambda}, @var{k}, @var{rows}) ## @deftypefnx {statistics} {@var{r} =} wblrnd (@var{lambda}, @var{k}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} wblrnd (@var{lambda}, @var{k}, [@var{sz}]) ## ## Random arrays from the Weibull distribution. ## ## @code{@var{r} = wblrnd (@var{lambda}, @var{k})} returns an array of random ## numbers chosen from the Weibull distribution with scale parameter ## @var{lambda} and shape parameter @var{k}. The size of @var{r} is the common ## size of @var{lambda} and @var{k}. A scalar input functions as a constant ## matrix of the same size as the other inputs. Both parameters must be ## positive reals. ## ## When called with a single size argument, @code{wblrnd} returns a square ## matrix with the dimension specified. When called with more than one scalar ## argument, the first two arguments are taken as the number of rows and columns ## and any further arguments specify additional matrix dimensions. The size may ## also be specified with a row vector of dimensions, @var{sz}. ## ## Further information about the Weibull distribution can be found at ## @url{https://en.wikipedia.org/wiki/Weibull_distribution} ## ## The @code{prob.WeibullDistribution} class names these same two parameters ## @qcode{A} and @qcode{B}, after MATLAB. @var{lambda} is its @qcode{A} and ## @var{k} is its @qcode{B}. ## @seealso{wblcdf, wblinv, wblpdf, wblfit, wbllike, wblstat, wblplot} ## @end deftypefn function r = wblrnd (lambda, k, varargin) ## Check for valid number of input arguments if (nargin < 2) error ("wblrnd: function called with too few input arguments."); endif ## Check for common size of LAMBDA and K if (! isscalar (lambda) || ! isscalar (k)) [retval, lambda, k] = common_size (lambda, k); if (retval > 0) error ("wblrnd: LAMBDA and K must be of common size or scalars."); endif endif ## Check for LAMBDA and K being reals if (iscomplex (lambda) || iscomplex (k)) error ("wblrnd: LAMBDA and K must not be complex."); endif ## Parse and check SIZE arguments if (nargin == 2) sz = size (lambda); elseif (nargin == 3) if (isscalar (varargin{1}) && varargin{1} == fix (varargin{1})) sz = [varargin{1}, varargin{1}]; elseif (isrow (varargin{1}) && all (varargin{1} == fix (varargin{1}))) sz = varargin{1}; elseif (isempty (varargin{1})) r = []; return; else error (strcat ("wblrnd: SZ must be a scalar or a row vector", ... " of integers.")); endif elseif (nargin > 3) notint = cellfun (@(x) (! isscalar (x) || x != fix (x)), varargin); if (any (notint)) error ("wblrnd: dimensions must be integers."); endif sz = [varargin{:}]; endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); ## Check that parameters match requested dimensions in size ## Use 'size (ones (sz))' to ignore any trailing singleton dimensions in SZ if (! isscalar (lambda) && ! isequal (size (lambda), size (ones (sz)))) error ("wblrnd: LAMBDA and K must be scalar or of size SZ."); endif ## Check for class type if (isa (lambda, 'single') || isa (k, 'single')) cls = 'single'; else cls = 'double'; endif ## Generate random sample from Weibull distribution if (isscalar (lambda) && isscalar (k)) if ((lambda > 0) && (lambda < Inf) && (k > 0) && (k < Inf)) r = lambda * rande (sz, cls) .^ (1/k); else r = NaN (sz, cls); endif else r = lambda .* rande (sz, cls) .^ (1./k); is_nan = (lambda <= 0) | (lambda == Inf) | (k <= 0) | (k == Inf); r(is_nan) = NaN; endif endfunction ## Test output %!assert_equal (size (wblrnd (1, 1)), [1, 1]) %!assert_equal (size (wblrnd (1, ones (2, 1))), [2, 1]) %!assert_equal (size (wblrnd (1, ones (2, 2))), [2, 2]) %!assert_equal (size (wblrnd (ones (2, 1), 1)), [2, 1]) %!assert_equal (size (wblrnd (ones (2, 2), 1)), [2, 2]) %!assert_equal (size (wblrnd (1, 1, 3)), [3, 3]) %!assert_equal (size (wblrnd (1, 1, [4, 1])), [4, 1]) %!assert_equal (size (wblrnd (1, 1, 4, 1)), [4, 1]) %!assert_equal (size (wblrnd (1, 1, 4, 1, 5)), [4, 1, 5]) %!assert_equal (size (wblrnd (1, 1, 0, 1)), [0, 1]) %!assert_equal (size (wblrnd (1, 1, 1, 0)), [1, 0]) %!assert_equal (size (wblrnd (1, 1, 1, 2, 0, 5)), [1, 2, 0, 5]) %!assert_equal (size (wblrnd (1, 1, [])), [0, 0]) %!assert_equal (size (wblrnd (1, 1, [2, 0, 2, 1])), [2, 0, 2]) %!assert_equal (size (wblrnd (1, 2, -1)), [0, 0]) %!assert_equal (size (wblrnd (1, 2, [2, -1, 2])), [2, 0, 2]) %!assert_equal (size (wblrnd (1, 2, 2, -1, 5)), [2, 0, 5]) ## Test class of input preserved %!assert_equal (class (wblrnd (1, 1)), "double") %!assert_equal (class (wblrnd (1, single (1))), "single") %!assert_equal (class (wblrnd (1, single ([1, 1]))), "single") %!assert_equal (class (wblrnd (single (1), 1)), "single") %!assert_equal (class (wblrnd (single ([1, 1]), 1)), "single") ## Test input validation %!error wblrnd () %!error wblrnd (1) %!error ... %! wblrnd (ones (3), ones (2)) %!error ... %! wblrnd (ones (2), ones (3)) %!error wblrnd (i, 2, 3) %!error wblrnd (1, i, 3) %!error ... %! wblrnd (1, 2, 1.2) %!error ... %! wblrnd (1, 2, ones (2)) %!error ... %! wblrnd (1, 2, [2 0 2.5]) %!error ... %! wblrnd (1, 2, 2, 1.5, 5) %!error ... %! wblrnd (2, ones (2), 3) %!error ... %! wblrnd (2, ones (2), [3, 2]) %!error ... %! wblrnd (2, ones (2), 3, 2) statistics-release-2.1.2/inst/Distribution_Functions/wienrnd.m000066400000000000000000000040641527311104300246350ustar00rootroot00000000000000## Copyright (C) 1995-2017 Friedrich Leisch ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} wienrnd (@var{t}, @var{d}, @var{n}) ## ## Return a simulated realization of the @var{d}-dimensional Wiener Process ## on the interval [0, @var{t}]. ## ## If @var{d} is omitted, @var{d} = 1 is used. The first column of the ## return matrix contains time, the remaining columns contain the Wiener ## process. ## ## The optional parameter @var{n} defines the number of summands used for ## simulating the process over an interval of length 1. If @var{n} is ## omitted, @var{n} = 1000 is used. ## @end deftypefn function r = wienrnd (t, d, n) if (nargin == 1) d = 1; n = 1000; elseif (nargin == 2) n = 1000; elseif (nargin > 3) print_usage (); endif if (! isscalar (t) || ! isscalar (d) || ! isscalar (n)) error ("wienrnd: T, D, and N must all be scalars."); endif if (! (fix (t) == t) || ! (fix (d) == d) || ! (fix (n) == n) || t <= 0 || d <= 0 || n <= 0) error ("wienrnd: T, D, and N must all be positive integers."); endif r = randn (n * t, d); r = cumsum (r) / sqrt (n); r = [((1: n*t)' / n), r]; endfunction %!error wienrnd (0) %!error wienrnd (1, 3, -50) %!error wienrnd (5, 0) %!error wienrnd (0.4, 3, 5) %!error wienrnd ([1 4], 3, 5) statistics-release-2.1.2/inst/Distribution_Functions/wishpdf.m000066400000000000000000000070041527311104300246300ustar00rootroot00000000000000## Copyright (C) 2013 Nir Krakauer ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} wishpdf (@var{W}, @var{Sigma}, @var{df}, @var{log_y}=false) ## ## Compute the probability density function of the Wishart distribution ## ## Inputs: A @var{p} x @var{p} matrix @var{W} where to find the PDF. The @var{p} ## x @var{p} positive definite matrix @var{Sigma} and scalar degrees of freedom ## parameter @var{df} characterizing the Wishart distribution. (For the density ## to be finite, need @var{df} > (@var{p} - 1).) ## ## If the flag @var{log_y} is set, return the log probability density -- this ## helps avoid underflow when the numerical value of the density is very small ## ## Output: @var{y} is the probability density of Wishart(@var{Sigma}, @var{df}) ## at @var{W}. ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. MATLAB accepts a character array and ## evaluates it at the character codes, which Octave deliberately does not, ## since a character array is an integer type and integers are refused too. ## ## @seealso{wishrnd, iwishpdf, iwishrnd} ## @end deftypefn function y = wishpdf (W, Sigma, df, log_y=false) if (nargin < 3) print_usage (); endif ## Check for W, SIGMA, and DF being double or single if (! (isfloat (W) && isfloat (Sigma) && isfloat (df))) error ("wishpdf: W, SIGMA, and DF must be double or single."); endif p = size (Sigma, 1); if (df <= (p - 1)) error ("wishpdf: DF too small, no finite densities exist."); endif ## calculate the logarithm of G_d(df/2), the multivariate gamma function g = (p * (p-1) / 4) * log (pi); for i = 1:p g = g + log (gamma ((df + (1 - i))/2)); endfor C = chol (Sigma); ## use formulas for determinant of positive definite matrix for better ## efficiency and numerical accuracy logdet_W = 2*sum (log (diag (chol (W)))); logdet_Sigma = 2*sum (log (diag (C))); y = -(df*p)/2 * log (2) - (df/2)*logdet_Sigma - g + ... ((df - p - 1)/2)*logdet_W - trace (chol2inv (C)*W)/2; if ! log_y y = exp (y); endif endfunction ##test results cross-checked against dwish function in R MCMCpack library %!assert_equal (wishpdf (4, 3, 3.1), 0.07702496, 1E-7); %!assert_equal (wishpdf ([2 -0.3;-0.3 4], [1 0.3;0.3 1], 4), 0.004529741, 1E-7); %!assert_equal (wishpdf ([6 2 5; 2 10 -5; 5 -5 25], [9 5 5; 5 10 -8; 5 -8 22], 5.1), 4.474865e-10, 1E-15); ## Test input validation %!error wishpdf (int32 (eye (2)), eye (2), 3) %!error wishpdf (true (2), eye (2), 3) %!error wishpdf (['ab'; 'cd'], eye (2), 3) %!error wishpdf () %!error wishpdf (1, 2) %!error wishpdf (1, 2, 0) %!error wishpdf (1, 2) statistics-release-2.1.2/inst/Distribution_Functions/wishrnd.m000066400000000000000000000100351527311104300246400ustar00rootroot00000000000000## Copyright (C) 2013-2019 Nir Krakauer ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{W}, @var{D}] =} wishrnd (@var{Sigma}, @var{df}, @var{D}, @var{n}=1) ## ## Return a random matrix sampled from the Wishart distribution with given ## parameters ## ## Inputs: the @math{p * p} positive definite matrix @var{Sigma} (or the ## lower-triangular Cholesky factor @var{D} of @var{Sigma}) and scalar degrees ## of freedom parameter @var{df}. ## ## @var{df} can be non-integer as long as @math{@var{df} > p - 1} ## ## Output: a random @math{p * p} matrix @var{W} from the ## Wishart(@var{Sigma}, @var{df}) distribution. If @var{n} > 1, then @var{W} is ## @var{p} x @var{p} x @var{n} and holds @var{n} such random matrices. ## (Optionally, the lower-triangular Cholesky factor @var{D} of @var{Sigma} is ## also returned.) ## ## Averaged across many samples, the mean of @var{W} should approach ## @var{df}*@var{Sigma}, and the variance of each element @var{W}_ij should ## approach @var{df}*(@var{Sigma}_ij^2 + @var{Sigma}_ii*@var{Sigma}_jj) ## ## @subheading References ## ## @enumerate ## @item ## Yu-Cheng Ku and Peter Bloomfield (2010), Generating Random Wishart Matrices ## with Fractional Degrees of Freedom in OX, ## http://www.gwu.edu/~forcpgm/YuChengKu-030510final-WishartYu-ChengKu.pdf ## @end enumerate ## ## @seealso{wishpdf, iwishpdf, iwishrnd} ## @end deftypefn function [W, D] = wishrnd (Sigma, df, D, n = 1) if (nargin < 2) print_usage (); endif if nargin < 3 || isempty (D) try D = chol (Sigma, 'lower'); catch error (strcat ("iwishrnd: Cholesky decomposition failed;", ... " SIGMA probably not positive definite.")); end_try_catch endif p = size (D, 1); ## Check for integer degrees of freedom. df_isint = (df == floor (df)); if (df < p) ## Truncate and warn only if the distribution is undefined. if (! df_isint && (df < (p - 1))) warning (strcat ("wishrnd: Wishart distribution undefined for", ... " non-integral df < p-1; truncating to floor(df).")); df = floor (df); df_isint = 1; ## Now it is an integer endif endif if (! df_isint) [ii, jj] = ind2sub ([p, p], 1:(p * p)); endif if (n > 1) W = nan (p, p, n); endif for i = 1:n if (df_isint) Z = D * randn (p, df); else Z = diag (sqrt (chi2rnd (df - (0:(p - 1))))); ##fill diagonal ## Note: chi2rnd(x) is equivalent to 2*randg(x/2), but the latter ## seems to offer no performance advantage Z(ii > jj) = randn (p * (p - 1) / 2, 1); #fill lower triangle Z = D * Z; endif W(:, :, i) = Z * Z'; endfor endfunction %!assert_equal (size (wishrnd (1,2)), [1, 1]); %!assert_equal (size (wishrnd (1,2,[])), [1, 1]); %!assert_equal (size (wishrnd (1,2,1)), [1, 1]); %!assert_equal (size (wishrnd ([],2,1)), [1, 1]); %!assert_equal (size (wishrnd ([3 1; 1 3], 2.00001, [], 1)), [2, 2]); %!assert_equal (size (wishrnd (eye (2), 2, [], 3)), [2, 2, 3]); ## Test input validation %!error wishrnd () %!error wishrnd (1) %!error wishrnd ([1; 1], 2) ## Test for non-integer df where p-1 < df < p (should not warn or truncate) %!test %! W = wishrnd (eye (3), 2.5); %! assert_equal (size (W), [3, 3]); ## Test that invalid non-integer df < p-1 triggers a warning %!warning wishrnd (eye (3), 1.5); statistics-release-2.1.2/inst/Distribution_Statistics/000077500000000000000000000000001527311104300231675ustar00rootroot00000000000000statistics-release-2.1.2/inst/Distribution_Statistics/betastat.m000066400000000000000000000101761527311104300251610ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} betastat (@var{a}, @var{b}) ## ## Compute statistics of the Beta distribution. ## ## @code{[@var{m}, @var{v}] = betastat (@var{a}, @var{b})} returns the mean ## and variance of the Beta distribution with shape parameters @var{a} and ## @var{b}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## Further information about the Beta distribution can be found at ## @url{https://en.wikipedia.org/wiki/Beta_distribution} ## ## @seealso{betacdf, betainv, betapdf, betarnd, betafit, betalike} ## @end deftypefn function [m, v] = betastat (a, b) ## Check for valid number of input arguments if (nargin < 2) error ("betastat: function called with too few input arguments."); endif ## Check for A and B being numeric if (! (isnumeric (a) && isnumeric (b))) error ("betastat: A and B must be numeric."); endif ## Check for A and B being real if (iscomplex (a) || iscomplex (b)) error ("betastat: A and B must not be complex."); endif ## Check for common size of A and B if (! isscalar (a) || ! isscalar (b)) [retval, a, b] = common_size (a, b); if (retval > 0) error ("betastat: A and B must be of common size or scalars."); endif endif ## Catch invalid parameters k = find (! (a > 0 & b > 0)); ## Calculate moments a_b = a + b; m = a ./ (a_b); m(k) = NaN; if (nargout > 1) v = (a .* b) ./ ((a_b .^ 2) .* (a_b + 1)); v(k) = NaN; endif endfunction ## Input validation tests %!error betastat () %!error betastat (1) %!error betastat ({}, 2) %!error betastat (1, '') %!error betastat (i, 2) %!error betastat (1, i) %!error ... %! betastat (ones (3), ones (2)) %!error ... %! betastat (ones (2), ones (3)) ## Output validation tests %!test %! a = -2:6; %! b = 0.4:0.2:2; %! [m, v] = betastat (a, b); %! expected_m = [NaN NaN NaN 1/2 2/3.2 3/4.4 4/5.6 5/6.8 6/8]; %! expected_v = [NaN NaN NaN 0.0833, 0.0558, 0.0402, 0.0309, 0.0250, 0.0208]; %! assert_equal (m, expected_m, eps*100); %! assert_equal (v, expected_v, 0.001); %!test %! a = -2:1:6; %! [m, v] = betastat (a, 1.5); %! expected_m = [NaN NaN NaN 1/2.5 2/3.5 3/4.5 4/5.5 5/6.5 6/7.5]; %! expected_v = [NaN NaN NaN 0.0686, 0.0544, 0.0404, 0.0305, 0.0237, 0.0188]; %! assert_equal (m, expected_m); %! assert_equal (v, expected_v, 0.001); %!test %! a = [14 Inf 10 NaN 10]; %! b = [12 9 NaN Inf 12]; %! [m, v] = betastat (a, b); %! expected_m = [14/26 NaN NaN NaN 10/22]; %! expected_v = [168/18252 NaN NaN NaN 120/11132]; %! assert_equal (m, expected_m); %! assert_equal (v, expected_v); %!assert_equal (nthargout (1:2, @betastat, 5, []), {[], []}) %!assert_equal (nthargout (1:2, @betastat, [], 5), {[], []}) %!assert_equal (size (betastat (rand (10, 5, 4), rand (10, 5, 4))), [10 5 4]) %!assert_equal (size (betastat (rand (10, 5, 4), 7)), [10 5 4]) statistics-release-2.1.2/inst/Distribution_Statistics/binostat.m000066400000000000000000000104131527311104300251670ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2015 Carnë Draug ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} binostat (@var{n}, @var{ps}) ## ## Compute statistics of the binomial distribution. ## ## @code{[@var{m}, @var{v}] = binostat (@var{n}, @var{ps})} returns the mean and ## variance of the binomial distribution with parameters @var{n} and @var{ps}, ## where @var{n} is the number of trials and @var{ps} is the probability of ## success. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## Further information about the binomial distribution can be found at ## @url{https://en.wikipedia.org/wiki/Binomial_distribution} ## ## @seealso{binocdf, binoinv, binopdf, binornd, binofit, binolike, binotest} ## @end deftypefn function [m, v] = binostat (n, ps) ## Check for valid number of input arguments if (nargin < 2) error ("binostat: function called with too few input arguments."); endif ## Check for N and PS being numeric if (! (isnumeric (n) && isnumeric (ps))) error ("binostat: N and PS must be numeric."); endif ## Check for N and PS being real if (iscomplex (n) || iscomplex (ps)) error ("binostat: N and PS must not be complex."); endif ## Check for common size of N and PS if (! isscalar (n) || ! isscalar (ps)) [retval, n, ps] = common_size (n, ps); if (retval > 0) error ("binostat: N and PS must be of common size or scalars."); endif endif ## Catch invalid parameters k = find (! (n > 0 & fix (n) == n & ps >= 0 & ps <= 1)); ## Calculate moments m = n .* ps; m(k) = NaN; if (nargout > 1) v = m .* (1 - ps); v(k) = NaN; endif endfunction ## Input validation tests %!error binostat () %!error binostat (1) %!error binostat ({}, 2) %!error binostat (1, '') %!error binostat (i, 2) %!error binostat (1, i) %!error ... %! binostat (ones (3), ones (2)) %!error ... %! binostat (ones (2), ones (3)) ## Output validation tests %!test %! n = 1:6; %! ps = 0:0.2:1; %! [m, v] = binostat (n, ps); %! expected_m = [0.00, 0.40, 1.20, 2.40, 4.00, 6.00]; %! expected_v = [0.00, 0.32, 0.72, 0.96, 0.80, 0.00]; %! assert_equal (m, expected_m, 0.001); %! assert_equal (v, expected_v, 0.001); %!test %! n = 1:6; %! [m, v] = binostat (n, 0.5); %! expected_m = [0.50, 1.00, 1.50, 2.00, 2.50, 3.00]; %! expected_v = [0.25, 0.50, 0.75, 1.00, 1.25, 1.50]; %! assert_equal (m, expected_m, 0.001); %! assert_equal (v, expected_v, 0.001); %!test %! n = [-Inf -3 5 0.5 3 NaN 100, Inf]; %! [m, v] = binostat (n, 0.5); %! assert_equal (isnan (m), [true true false true false true false false]) %! assert_equal (isnan (v), [true true false true false true false false]) %! assert_equal (m(end), Inf); %! assert_equal (v(end), Inf); %!assert_equal (nthargout (1:2, @binostat, 5, []), {[], []}) %!assert_equal (nthargout (1:2, @binostat, [], 5), {[], []}) %!assert_equal (size (binostat (randi (100, 10, 5, 4), rand (10, 5, 4))), [10 5 4]) %!assert_equal (size (binostat (randi (100, 10, 5, 4), 7)), [10 5 4]) statistics-release-2.1.2/inst/Distribution_Statistics/bisastat.m000066400000000000000000000075201527311104300251630ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} bisastat (@var{beta}, @var{gamma}) ## ## Compute statistics of the Birnbaum-Saunders distribution. ## ## @code{[@var{m}, @var{v}] = bisastat (@var{beta}, @var{gamma})} returns the ## mean and variance of the Birnbaum-Saunders distribution with scale parameter ## @var{beta} and shape parameter @var{gamma}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## Further information about the Birnbaum-Saunders distribution can be found at ## @url{https://en.wikipedia.org/wiki/Birnbaum%E2%80%93Saunders_distribution} ## ## @seealso{bisacdf, bisainv, bisapdf, bisarnd, bisafit, bisalike} ## @end deftypefn function [m, v] = bisastat (beta, gamma) ## Check for valid number of input arguments if (nargin < 2) error ("bisastat: function called with too few input arguments."); endif ## Check for BETA and GAMMA being numeric if (! (isnumeric (beta) && isnumeric (gamma))) error ("bisastat: BETA and GAMMA must be numeric."); endif ## Check for BETA and GAMMA being real if (iscomplex (beta) || iscomplex (gamma)) error ("bisastat: BETA and GAMMA must not be complex."); endif ## Check for common size of BETA and GAMMA if (! isscalar (beta) || ! isscalar (gamma)) [retval, beta, gamma] = common_size (beta, gamma); if (retval > 0) error ("bisastat: BETA and GAMMA must be of common size or scalars."); endif endif ## Calculate moments m = beta .* (1 + ((gamma .^ 2) ./ 2)); v = ((beta .* gamma) .^ 2) .* (1 + ((5 .* (gamma .^ 2)) ./ 4)); ## Continue argument check beta = find (! (beta > 0) | ! (beta < Inf) | ! (gamma > 0) | ! (gamma < Inf)); if (any (beta)) m(beta) = NaN; v(beta) = NaN; endif endfunction ## Input validation tests %!error bisastat () %!error bisastat (1) %!error bisastat ({}, 2) %!error bisastat (1, '') %!error bisastat (i, 2) %!error bisastat (1, i) %!error ... %! bisastat (ones (3), ones (2)) %!error ... %! bisastat (ones (2), ones (3)) ## Output validation tests %!test %! beta = 1:6; %! gamma = 1:0.2:2; %! [m, v] = bisastat (beta, gamma); %! expected_m = [1.50, 3.44, 5.94, 9.12, 13.10, 18]; %! expected_v = [2.25, 16.128, 60.858, 172.032, 409.050, 864]; %! assert_equal (m, expected_m, 1e-2); %! assert_equal (v, expected_v, 1e-3); %!test %! beta = 1:6; %! [m, v] = bisastat (beta, 1.5); %! expected_m = [2.125, 4.25, 6.375, 8.5, 10.625, 12.75]; %! expected_v = [8.5781, 34.3125, 77.2031, 137.2500, 214.4531, 308.8125]; %! assert_equal (m, expected_m, 1e-3); %! assert_equal (v, expected_v, 1e-4); statistics-release-2.1.2/inst/Distribution_Statistics/burrstat.m000066400000000000000000000105621527311104300252170ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} burrstat (@var{lambda}, @var{c}, @var{k}) ## ## Compute statistics of the Burr type XII distribution. ## ## @code{[@var{m}, @var{v}] = burrstat (@var{lambda}, @var{c}, @var{k})} returns ## the mean and variance of the Burr type XII distribution with scale parameter ## @var{lambda}, first shape parameter @var{c}, and second shape parameter ## @var{k}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the same ## size as the other inputs. ## ## Further information about the Burr distribution can be found at ## @url{https://en.wikipedia.org/wiki/Burr_distribution} ## ## @seealso{gevcdf, gevinv, gevpdf, gevrnd, gevfit, gevlike} ## @end deftypefn function [m, v] = burrstat (lambda, c, k) ## Check for is_val number of input arguments if (nargin < 3) error ("burrstat: function called with too few input arguments."); endif ## Check for LAMBDA, C, and K being numeric if (! (isnumeric (lambda) && isnumeric (c) && isnumeric (k))) error ("burrstat: LAMBDA, C, and K must be numeric."); endif ## Check for LAMBDA, C, and K being real if (iscomplex (lambda) || iscomplex (c) || iscomplex (k)) error ("burrstat: LAMBDA, C, and K must not be complex."); endif ## Check for common size of LAMBDA, C, and K if (! isscalar (lambda) || ! isscalar (c) || ! isscalar (k)) [retval, lambda, c, k] = common_size (lambda, c, k); if (retval > 0) error ("burrstat: LAMBDA, C, and K must be of common size or scalars."); endif endif ## Preallocate mean annd variance m = v = nan (size (lambda)); ## Precalculate some values c_i = 1 ./ c; l_c = lambda .* c_i; kci = k - c_i; ## Find valid vases c_k = c .* k; is_val = lambda > 0 & c > 0 & k > 0; ## Calculate 1st moment is_inf = is_val & c_k <= 1; m(is_inf) = Inf; no_inf = ! is_inf; m(no_inf) = l_c(no_inf) .* beta (c_i(no_inf), kci(no_inf)); ## Calculate 2nd moment is_inf = is_val & c_k <= 2; v(is_inf) = Inf; no_inf = ! is_inf; v(no_inf) = 2 * lambda(no_inf) .* l_c(no_inf) .* ... beta (c_i(no_inf) .* 2, kci(no_inf) - c_i(no_inf)) ... - m(no_inf) .^ 2; endfunction ## Input validation tests %!error burrstat () %!error burrstat (1) %!error burrstat (1, 2) %!error burrstat ({}, 2, 3) %!error burrstat (1, '', 3) %!error burrstat (1, 2, '') %!error burrstat (i, 2, 3) %!error burrstat (1, i, 3) %!error burrstat (1, 2, i) %!error ... %! burrstat (ones (3), ones (2), 3) %!error ... %! burrstat (ones (2), 2, ones (3)) %!error ... %! burrstat (1, ones (2), ones (3)) ## Output validation tests %!test %! [m, v] = burrstat (1, 2, 5); %! assert_equal (m, 0.4295, 1e-4); %! assert_equal (v, 0.0655, 1e-4); %!test %! [m, v] = burrstat (1, 1, 1); %! assert_equal (m, Inf); %! assert_equal (v, Inf); %!test %! [m, v] = burrstat (2, 4, 1); %! assert_equal (m, 2.2214, 1e-4); %! assert_equal (v, 1.3484, 1e-4); statistics-release-2.1.2/inst/Distribution_Statistics/chi2stat.m000066400000000000000000000046211527311104300250710ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} chi2stat (@var{df}) ## ## Compute statistics of the chi-squared distribution. ## ## @code{[@var{m}, @var{v}] = chi2stat (@var{df})} returns the mean and ## variance of the chi-squared distribution with @var{df} degrees of freedom. ## ## The size of @var{m} (mean) and @var{v} (variance) is the same size of the ## input argument. ## ## Further information about the chi-squared distribution can be found at ## @url{https://en.wikipedia.org/wiki/Chi-squared_distribution} ## ## @seealso{chi2cdf, chi2inv, chi2pdf, chi2rnd} ## @end deftypefn function [m, v] = chi2stat (df) ## Check for valid number of input arguments if (nargin < 1) error ("chi2stat: function called with too few input arguments."); endif ## Check for DF being numeric if (! isnumeric (df)) error ("chi2stat: DF must be numeric."); endif ## Check for DF being real if (iscomplex (df)) error ("chi2stat: DF must not be complex."); endif ## Calculate moments m = df; v = 2 .* df; ## Continue argument check k = find (! (df > 0) | ! (df < Inf)); if (any (k)) m(k) = NaN; v(k) = NaN; endif endfunction ## Input validation tests %!error chi2stat () %!error chi2stat ({}) %!error chi2stat ('') %!error chi2stat (i) ## Output validation tests %!test %! df = 1:6; %! [m, v] = chi2stat (df); %! assert_equal (m, df); %! assert_equal (v, [2, 4, 6, 8, 10, 12], 0.001); statistics-release-2.1.2/inst/Distribution_Statistics/copulaparam.m000066400000000000000000000263211527311104300256550ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{param} =} copulaparam (@var{family}, @var{r}) ## @deftypefnx {statistics} {@var{param} =} copulaparam (@dots{}, @qcode{"type"}, @var{type}) ## ## Copula parameter as a function of rank correlation. ## ## @code{@var{param} = copulaparam (@var{family}, @var{r})} returns the linear ## or copula parameter @var{param} corresponding to a copula of the family ## @var{family} that has Kendall's rank correlation @var{r}. It is the inverse ## of @code{copulastat}. ## ## @var{family} is the copula family name. It can be @qcode{"Gaussian"} for the ## Gaussian family, @qcode{"t"} for the Student's t family, @qcode{"Clayton"} ## for the Clayton family, @qcode{"Gumbel"} for the Gumbel-Hougaard family, ## @qcode{"Frank"} for the Frank family, @qcode{"AMH"} for the Ali-Mikhail-Haq ## family, or @qcode{"FGM"} for the Farlie-Gumbel-Morgenstern family. The last ## two are Octave extensions that MATLAB does not provide, and are treated as ## bivariate. Neither reaches the whole range of either rank correlation: the ## Ali-Mikhail-Haq family covers a Kendall's tau in ## @code{[(5-8*log (2))/3, 1/3]} and the Farlie-Gumbel-Morgenstern family one ## in @code{[-2/9, 2/9]}. ## ## For the Gaussian and Student's t families, @var{r} is a scalar rank ## correlation or a matrix of pairwise rank correlations, and @var{param} is the ## corresponding linear correlation of the same size. For the Clayton, ## Gumbel-Hougaard, and Frank families, @var{r} is a scalar rank correlation and ## @var{param} is the scalar copula parameter. The Gumbel-Hougaard family ## models positive dependence only, so @var{r} must be non-negative for that ## family. ## ## @code{@var{param} = copulaparam (@dots{}, @qcode{"type"}, @var{type})} ## selects the measure of rank correlation given in @var{r}. @var{type} can be ## @qcode{"Kendall"} (the default) for Kendall's tau, or @qcode{"Spearman"} for ## Spearman's rho. ## ## The Gaussian and Student's t families and the Kendall's tau of the Clayton ## and Gumbel-Hougaard families are inverted in closed form. The remaining ## cases are inverted numerically from @code{copulastat}. ## ## @strong{Note:} for the Archimedean families with @qcode{"Spearman"}, the ## underlying relationship is computed by exact numerical integration rather ## than the interpolated table used by @sc{matlab}, so results may differ from ## @sc{matlab} by up to about @math{10^{-4}}. See @code{copulastat}. ## ## @seealso{copulastat, copulafit, copulacdf, copulapdf, copularnd} ## @end deftypefn function param = copulaparam (family, r, varargin) ## Check arguments if (nargin < 2) print_usage (); endif if (! ischar (family)) error (strcat ("copulaparam: FAMILY must be one of 'Gaussian',", ... " 't', 'Clayton', 'Gumbel', 'Frank', 'AMH', and 'FGM'.")); endif if (! isnumeric (r) || ! isreal (r)) error ("copulaparam: R must be real."); endif ## Parse the 'type' option type = 'kendall'; if (numel (varargin) > 0) if (numel (varargin) != 2 || ! ischar (varargin{1}) || ... ! strcmpi (varargin{1}, 'type')) error ("copulaparam: invalid optional argument."); endif if (! ischar (varargin{2}) || ... ! any (strcmpi (varargin{2}, {'kendall', 'spearman'}))) error ("copulaparam: TYPE must be either 'Kendall' or 'Spearman'."); endif type = lower (varargin{2}); endif lower_family = lower (family); switch (lower_family) case {'gaussian', 't'} ## Elliptical families: closed-form inverse, applied elementwise if (any (abs (r(:)) > 1)) error ("copulaparam: R must be a correlation in the range -1 to 1."); endif if (strcmp (type, 'kendall')) param = sin (pi .* r ./ 2); else param = 2 .* sin (pi .* r ./ 6); endif case {'clayton', 'gumbel', 'frank'} ## Archimedean families: scalar rank correlation if (! isscalar (r)) error ("copulaparam: R must be a scalar for the %s family.", family); endif if (abs (r) >= 1) error ("copulaparam: R must be in the range -1 to 1."); endif if (strcmp (lower_family, 'gumbel') && r < 0) error (strcat ("copulaparam: R must be non-negative for the", ... " Gumbel family.")); endif switch (lower_family) case 'clayton' if (strcmp (type, 'kendall')) param = 2 .* r ./ (1 - r); else param = invert_stat (lower_family, r, type); endif case 'gumbel' if (strcmp (type, 'kendall')) param = 1 ./ (1 - r); else param = invert_stat (lower_family, r, type); endif case 'frank' param = invert_stat (lower_family, r, type); endswitch case {'amh', 'fgm'} ## Octave extensions that MATLAB does not have, both bivariate here. if (! isscalar (r)) error ("copulaparam: R must be a scalar for the %s family.", family); endif if (strcmp (lower_family, 'fgm')) ## Both measures are linear in the parameter, so the inverse is too. if (strcmp (type, 'kendall')) lim = 2 / 9; param = 9 .* r ./ 2; else lim = 1 / 3; param = 3 .* r; endif if (abs (r) > lim) error (strcat ("copulaparam: R must be in the range -%g to %g", ... " for the FGM family."), lim, lim); endif else ## The Ali-Mikhail-Haq family reaches only part of the range, and ## neither measure inverts in closed form. lo = copulastat ('AMH', -1, 'type', type); hi = copulastat ('AMH', 1 - eps, 'type', type); if (r < lo || r > hi) error (strcat ("copulaparam: R must be in the range %g to %g", ... " for the AMH family."), lo, hi); endif if (r == 0) param = 0; else param = fzero (@(a) copulastat ('AMH', a, 'type', type) - r, ... [-1, 1 - eps]); endif endif otherwise error ("copulaparam: unknown copula family '%s'.", family); endswitch endfunction ## Invert a rank correlation numerically from copulastat for an Archimedean ## family. The dependence measure is monotone increasing in the parameter, so ## we bracket the root and solve. function a = invert_stat (family, r, type) g = @(aa) copulastat (family, aa, 'type', type) - r; ## Parameter value at which the family is the independence copula if (strcmp (family, 'gumbel')) s0 = 1; else s0 = 0; endif if (r == 0) a = s0; return; elseif (r > 0) lo = s0; hi = s0 + 1; while (g (hi) < 0 && hi < 1e8) hi = s0 + (hi - s0) .* 2; endwhile else ## r < 0 (Clayton or Frank only) hi = s0; if (strcmp (family, 'clayton')) lo = -1 + 1e-8; else lo = s0 - 1; while (g (lo) > 0 && lo > -1e8) lo = s0 - (s0 - lo) .* 2; endwhile endif endif a = fzero (g, [lo, hi]); endfunction %!demo %! ## Copula parameter of a Gaussian copula with Kendall's tau 0.3 %! rho = copulaparam ("Gaussian", 0.3) %!demo %! ## copulaparam inverts copulastat %! alpha = copulaparam ("Clayton", 0.5) %! tau = copulastat ("Clayton", alpha) ## Test output against MATLAB %!test %! assert_equal (copulaparam ("Gaussian", 0.3), 0.453990499739547, 1e-14); %! assert_equal (copulaparam ("Gaussian", 0.3, "type", "Spearman"), ... %! 0.312868930080462, 1e-14); %! assert_equal (copulaparam ("t", 0.3), 0.453990499739547, 1e-14); %! assert_equal (copulaparam ("Clayton", 0.3), 0.857142857142857, 1e-14); %! assert_equal (copulaparam ("Frank", 0.3), 2.91743444592452, 1e-8); %! assert_equal (copulaparam ("Gumbel", 0.3), 1.42857142857143, 1e-13); ## copulaparam is the inverse of copulastat (round trip) %!test %! for tau = [0.1, 0.25, 0.5, 0.7] %! for fam = {"Clayton", "Gumbel", "Frank"} %! a = copulaparam (fam{1}, tau); %! assert_equal (copulastat (fam{1}, a), tau, 1e-8); %! endfor %! endfor %!test %! for rs = [0.1, 0.3, 0.6] %! for fam = {"Clayton", "Gumbel", "Frank"} %! a = copulaparam (fam{1}, rs, "type", "Spearman"); %! assert_equal (copulastat (fam{1}, a, "type", "Spearman"), rs, 1e-7); %! endfor %! endfor ## Negative dependence for the signed families %!test %! a = copulaparam ("Frank", -0.3); %! assert_equal (copulastat ("Frank", a), -0.3, 1e-8); %! a = copulaparam ("Clayton", -0.2); %! assert_equal (copulastat ("Clayton", a), -0.2, 1e-8); ## Elliptical families accept a correlation matrix and act elementwise %!test %! tau = [1, 0.3; 0.3, 1]; %! assert_equal (copulaparam ("Gaussian", tau), sin (pi .* tau ./ 2), 1e-14); ## Test input validation %!error ... %! copulaparam (5, 0.3) %!error copulaparam ("Gaussian", 2i) %!error ... %! copulaparam ("Gaussian", 0.3, "type", "Pearson") %!error ... %! copulaparam ("Gaussian", 0.3, "foo") %!error ... %! copulaparam ("Gaussian", 1.5) %!error ... %! copulaparam ("Clayton", [0.1, 0.2]) %!error ... %! copulaparam ("Clayton", 1) %!error ... %! copulaparam ("Gumbel", -0.3) %!error copulaparam ("Foo", 0.3) ## The Ali-Mikhail-Haq and Farlie-Gumbel-Morgenstern families, Octave ## extensions. Inverting copulastat must return the parameter it was given. %!test %! for ty = {'Kendall', 'Spearman'} %! for a = [-0.9, -0.5, -0.1, 0, 0.3, 0.7, 0.95] %! r = copulastat ('AMH', a, 'type', ty{1}); %! assert_equal (copulaparam ('AMH', r, 'type', ty{1}), a, 1e-9); %! endfor %! for a = [-1, -0.4, 0, 0.6, 1] %! r = copulastat ('FGM', a, 'type', ty{1}); %! assert_equal (copulaparam ('FGM', r, 'type', ty{1}), a, 1e-12); %! endfor %! endfor %!test # the FGM inverse is linear %! assert_equal (copulaparam ('FGM', 0.1), 0.45, 1e-14); %! assert_equal (copulaparam ('FGM', 0.1, 'type', 'Spearman'), 0.3, 1e-14); %!error ... %! copulaparam ('FGM', 0.5) %!error ... %! copulaparam ('AMH', 0.5) statistics-release-2.1.2/inst/Distribution_Statistics/copulastat.m000066400000000000000000000327311527311104300255320ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} copulastat (@var{family}, @var{param}) ## @deftypefnx {statistics} {@var{r} =} copulastat (@dots{}, @qcode{"type"}, @var{type}) ## ## Rank correlation for a copula family. ## ## @code{@var{r} = copulastat (@var{family}, @var{param})} returns Kendall's ## rank correlation @var{r} corresponding to a copula of the family ## @var{family} with linear or copula parameter @var{param}. ## ## @var{family} is the copula family name. It can be @qcode{"Gaussian"} for the ## Gaussian family, @qcode{"t"} for the Student's t family, @qcode{"Clayton"} ## for the Clayton family, @qcode{"Gumbel"} for the Gumbel-Hougaard family, ## @qcode{"Frank"} for the Frank family, @qcode{"AMH"} for the Ali-Mikhail-Haq ## family, or @qcode{"FGM"} for the Farlie-Gumbel-Morgenstern family. The last ## two are Octave extensions that MATLAB does not provide, and are treated as ## bivariate. Neither reaches the whole range of either rank correlation: the ## Ali-Mikhail-Haq family covers a Kendall's tau in ## @code{[(5-8*log (2))/3, 1/3]} and the Farlie-Gumbel-Morgenstern family one ## in @code{[-2/9, 2/9]}. ## ## For the Gaussian and Student's t families, @var{param} is a linear ## correlation coefficient @var{rho} in the range @math{[-1,1]}, or a ## @math{p}-by-@math{p} correlation matrix, in which case @var{r} has the same ## size and each element is computed elementwise. For the Clayton, ## Gumbel-Hougaard, and Frank families, @var{param} is the scalar copula ## parameter. ## ## @code{@var{r} = copulastat (@dots{}, @qcode{"type"}, @var{type})} selects the ## measure of rank correlation. @var{type} can be @qcode{"Kendall"} (the ## default) for Kendall's tau, or @qcode{"Spearman"} for Spearman's rho. ## ## The relationships are closed form for the Gaussian and Student's t families ## (@math{r = 2 \arcsin(\rho) / \pi} for Kendall's tau and ## @math{r = 6 \arcsin(\rho/2) / \pi} for Spearman's rho) and for the Kendall's ## tau of the Archimedean families. Spearman's rho of the Archimedean families ## has no closed form and is computed by accurate numerical integration of the ## copula. ## ## @strong{Note:} @sc{matlab} returns the Archimedean Spearman's rho by ## interpolating an internal precomputed table, whose values deviate from the ## true relationship by up to about @math{10^{-4}}. This implementation returns ## the mathematically exact value instead, so results for ## @code{copulastat (@var{family}, @var{param}, "type", "Spearman")} with an ## Archimedean @var{family} may differ from @sc{matlab} at that level. ## ## @seealso{copulaparam, copulafit, copulacdf, copulapdf, copularnd} ## @end deftypefn function r = copulastat (family, param, varargin) ## Check arguments if (nargin < 2) print_usage (); endif if (! ischar (family)) error (strcat ("copulastat: FAMILY must be one of 'Gaussian',", ... " 't', 'Clayton', 'Gumbel', 'Frank', 'AMH', and 'FGM'.")); endif if (! isnumeric (param) || ! isreal (param)) error ("copulastat: PARAM must be real."); endif ## Parse the 'type' option type = 'kendall'; if (numel (varargin) > 0) if (numel (varargin) != 2 || ! ischar (varargin{1}) || ... ! strcmpi (varargin{1}, 'type')) error ("copulastat: invalid optional argument."); endif if (! ischar (varargin{2}) || ... ! any (strcmpi (varargin{2}, {'kendall', 'spearman'}))) error ("copulastat: TYPE must be either 'Kendall' or 'Spearman'."); endif type = lower (varargin{2}); endif lower_family = lower (family); switch (lower_family) case {'gaussian', 't'} ## Elliptical families: closed form, applied elementwise if (any (abs (param(:)) > 1)) error ("copulastat: PARAM must be a correlation in the range -1 to 1."); endif if (strcmp (type, 'kendall')) r = 2 ./ pi .* asin (param); else r = 6 ./ pi .* asin (param ./ 2); endif case {'clayton', 'gumbel', 'frank'} ## Archimedean families: scalar copula parameter if (! isscalar (param)) error ("copulastat: PARAM must be a scalar for the %s family.", family); endif switch (lower_family) case 'clayton' if (param < -1) error (strcat ("copulastat: PARAM must be greater than or", ... " equal to -1 for the Clayton family.")); endif case 'gumbel' if (param < 1) error (strcat ("copulastat: PARAM must be greater than or", ... " equal to 1 for the Gumbel family.")); endif endswitch if (strcmp (type, 'kendall')) r = archimedean_kendall (lower_family, param); else r = archimedean_spearman (lower_family, param); endif case {'amh', 'fgm'} ## Octave extensions that MATLAB does not have. Both are bivariate here: ## the rank correlations are properties of a pair of variables, and the ## multivariate Farlie-Gumbel-Morgenstern family carries one parameter ## per subset rather than one overall. if (! isscalar (param)) error ("copulastat: PARAM must be a scalar for the %s family.", family); endif if (strcmp (lower_family, 'amh')) if (param < -1 || param >= 1) error (strcat ("copulastat: PARAM must be greater than or equal", ... " to -1 and less than 1 for the AMH family.")); endif if (strcmp (type, 'kendall')) r = amh_kendall (param); else r = amh_spearman (param); endif else if (param < -1 || param > 1) error (strcat ("copulastat: PARAM must be in the range -1 to 1", ... " for the FGM family.")); endif ## Both measures are linear in the parameter for this family if (strcmp (type, 'kendall')) r = 2 .* param ./ 9; else r = param ./ 3; endif endif otherwise error ("copulastat: unknown copula family '%s'.", family); endswitch endfunction ## Kendall's tau of the Ali-Mikhail-Haq copula. The limit at PARAM == 0 is ## 2/9 * PARAM, which is zero; the closed form is 0/0 there. function t = amh_kendall (a) if (a == 0) t = 0; else t = 1 - 2 .* ((1 - a) .^ 2 .* log (1 - a) + a) ./ (3 .* a .^ 2); endif endfunction ## Spearman's rho of the Ali-Mikhail-Haq copula, which needs the dilogarithm. function r = amh_spearman (a) if (a == 0) r = 0; else r = (12 .* (1 + a) .* dilog (a) - 24 .* (1 - a) .* log (1 - a)) ... ./ a .^ 2 - 3 .* (a + 12) ./ a; endif endfunction ## The dilogarithm, -int_0^z log (1-t)/t dt. Substituting t = z s moves the ## removable singularity off the end of the interval, which the direct form ## integrates badly for negative z. function d = dilog (z) if (z == 0) d = 0; else d = -integral (@(s) log (1 - z .* s) ./ s, 0, 1); endif endfunction ## Kendall's tau for the Archimedean families (closed form) function tau = archimedean_kendall (family, alpha) switch (family) case 'clayton' tau = alpha ./ (alpha + 2); case 'gumbel' tau = 1 - 1 ./ alpha; case 'frank' if (alpha == 0) tau = 0; else tau = 1 - 4 ./ alpha .* (1 - debye1 (alpha)); endif endswitch endfunction ## Spearman's rho for the Archimedean families by numerical integration of the ## copula: rho_s = 12 * integral over the unit square of C(u,v), minus 3. function rho = archimedean_spearman (family, alpha) if (family_is_independent (family, alpha)) rho = 0; return; endif [x, w] = gauss_legendre_unit (); [U, V] = meshgrid (x, x); switch (family) case 'clayton' C = (U .^ (-alpha) + V .^ (-alpha) - 1) .^ (-1 ./ alpha); case 'gumbel' C = exp (-((-log (U)) .^ alpha + (-log (V)) .^ alpha) .^ (1 ./ alpha)); case 'frank' C = -log (1 + (expm1 (-alpha .* U) .* expm1 (-alpha .* V)) ... ./ expm1 (-alpha)) ./ alpha; endswitch rho = 12 .* (w(:)' * C * w(:)) - 3; endfunction ## True when the parameter reduces the family to the independence copula function tf = family_is_independent (family, alpha) tf = ((strcmp (family, 'clayton') || strcmp (family, 'frank')) ... && alpha == 0) || (strcmp (family, 'gumbel') && alpha == 1); endfunction ## Debye function of the first order, D1(x) = (1/x) * integral_0^x t/(e^t-1) dt, ## valid for any real x (the removable singularity at t=0 is handled by expm1). function d = debye1 (x) d = integral (@(t) t ./ expm1 (t), 0, x) ./ x; endfunction ## Nodes and weights of a 64-point Gauss-Legendre rule mapped to [0,1], cached ## across calls (Golub-Welsch). function [x, w] = gauss_legendre_unit () persistent nodes weights if (isempty (nodes)) N = 64; beta = 0.5 ./ sqrt (1 - (2 .* (1:N-1)) .^ (-2)); T = diag (beta, 1) + diag (beta, -1); [V, D] = eig (T); [nodes, idx] = sort (diag (D)); weights = 2 .* (V(1, idx) .^ 2)'; nodes = (nodes + 1) ./ 2; weights = weights ./ 2; endif x = nodes; w = weights; endfunction %!demo %! ## Kendall's tau and Spearman's rho of a Gaussian copula with correlation 0.5 %! tau = copulastat ("Gaussian", 0.5) %! rho = copulastat ("Gaussian", 0.5, "type", "Spearman") %!demo %! ## Kendall's tau of a Clayton copula as its parameter grows %! alpha = [0.5, 1, 2, 5]; %! tau = arrayfun (@(a) copulastat ("Clayton", a), alpha) ## Test output against MATLAB %!test %! assert_equal (copulastat ("Gaussian", 0.5), 1/3, 1e-14); %! assert_equal (copulastat ("Gaussian", 0.5, "type", "Spearman"), ... %! 0.482583739530997, 1e-14); %! assert_equal (copulastat ("t", 0.5), 1/3, 1e-14); %! assert_equal (copulastat ("Clayton", 2), 0.5, 1e-14); %! assert_equal (copulastat ("Frank", 3), 0.307246959430723, 1e-12); %! assert_equal (copulastat ("Gumbel", 2), 0.5, 1e-14); ## Elliptical families accept a correlation matrix and act elementwise %!test %! rho = [1, 0.5; 0.5, 1]; %! assert_equal (copulastat ("Gaussian", rho), 2/pi .* asin (rho), 1e-14); ## Independence limits %!test %! assert_equal (copulastat ("Clayton", 0), 0, 1e-14); %! assert_equal (copulastat ("Frank", 0), 0, 1e-14); %! assert_equal (copulastat ("Gumbel", 1), 0, 1e-14); %! assert_equal (copulastat ("Clayton", 0, "type", "Spearman"), 0, 1e-14); %! assert_equal (copulastat ("Gumbel", 1, "type", "Spearman"), 0, 1e-14); ## Test input validation %!error ... %! copulastat (5, 0.5) %!error copulastat ("Gaussian", 2i) %!error ... %! copulastat ("Gaussian", 0.5, "type", "Pearson") %!error ... %! copulastat ("Gaussian", 0.5, "foo") %!error ... %! copulastat ("Gaussian", 1.5) %!error ... %! copulastat ("Clayton", [1, 2]) %!error ... %! copulastat ("Gumbel", 0.5) %!error copulastat ("Foo", 0.5) ## The Ali-Mikhail-Haq and Farlie-Gumbel-Morgenstern families, Octave ## extensions. Both rank correlations are checked against the integrals that ## define them, evaluated on copulacdf and copulapdf. %!test %! for a = [-0.9, -0.5, 0.5, 0.9] %! f = @(u, v) arrayfun (@(p, q) copulacdf ('AMH', [p, q], a), u, v); %! rho = 12 * integral2 (f, 0, 1, 0, 1, 'AbsTol', 1e-11) - 3; %! assert_equal (copulastat ('AMH', a, 'type', 'Spearman'), rho, 1e-8); %! endfor %!test %! for a = [-0.9, -0.5, 0.5, 0.9] %! f = @(u, v) arrayfun (@(p, q) copulacdf ('AMH', [p, q], a) ... %! * copulapdf ('AMH', [p, q], a), u, v); %! tau = 4 * integral2 (f, 0, 1, 0, 1, 'AbsTol', 1e-11) - 1; %! assert_equal (copulastat ('AMH', a), tau, 1e-8); %! endfor %!test # both measures are linear in the FGM parameter %! for a = [-1, -0.5, 0, 0.5, 1] %! assert_equal (copulastat ('FGM', a), 2 * a / 9, 1e-14); %! assert_equal (copulastat ('FGM', a, 'type', 'Spearman'), a / 3, 1e-14); %! endfor %!test # the independence copula at a zero parameter %! assert_equal (copulastat ('AMH', 0), 0); %! assert_equal (copulastat ('AMH', 0, 'type', 'Spearman'), 0); %! assert_equal (copulastat ('FGM', 0), 0); %!test # the extremes of the Ali-Mikhail-Haq range are the known ones %! assert_equal (copulastat ('AMH', -1), (5 - 8 * log (2)) / 3, 1e-12); %! assert_equal (copulastat ('AMH', 1 - eps), 1 / 3, 1e-9); %!error ... %! copulastat ('AMH', 1) %!error ... %! copulastat ('FGM', 1.5) statistics-release-2.1.2/inst/Distribution_Statistics/doc-cache000066400000000000000000001156641527311104300247350ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 38 # name: # type: sq_string # elements: 1 # length: 8 betastat # name: # type: sq_string # elements: 1 # length: 537 statistics: [m, v] = betastat (a, b) Compute statistics of the Beta distribution. [m, v] = betastat (a, b) returns the mean and variance of the Beta distribution with shape parameters a and b. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Beta distribution can be found at https://en.wikipedia.org/wiki/Beta_distribution See also: betacdf, betainv, betapdf, betarnd, betafit, betalike # name: # type: sq_string # elements: 1 # length: 44 Compute statistics of the Beta distribution. # name: # type: sq_string # elements: 1 # length: 8 binostat # name: # type: sq_string # elements: 1 # length: 630 statistics: [m, v] = binostat (n, ps) Compute statistics of the binomial distribution. [m, v] = binostat (n, ps) returns the mean and variance of the binomial distribution with parameters n and ps, where n is the number of trials and ps is the probability of success. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the binomial distribution can be found at https://en.wikipedia.org/wiki/Binomial_distribution See also: binocdf, binoinv, binopdf, binornd, binofit, binolike, binotest # name: # type: sq_string # elements: 1 # length: 48 Compute statistics of the binomial distribution. # name: # type: sq_string # elements: 1 # length: 8 bisastat # name: # type: sq_string # elements: 1 # length: 633 statistics: [m, v] = bisastat (beta, gamma) Compute statistics of the Birnbaum-Saunders distribution. [m, v] = bisastat (beta, gamma) returns the mean and variance of the Birnbaum-Saunders distribution with scale parameter beta and shape parameter gamma. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Birnbaum-Saunders distribution can be found at https://en.wikipedia.org/wiki/Birnbaum%E2%80%93Saunders_distribution See also: bisacdf, bisainv, bisapdf, bisarnd, bisafit, bisalike # name: # type: sq_string # elements: 1 # length: 57 Compute statistics of the Birnbaum-Saunders distribution. # name: # type: sq_string # elements: 1 # length: 8 burrstat # name: # type: sq_string # elements: 1 # length: 618 statistics: [m, v] = burrstat (lambda, c, k) Compute statistics of the Burr type XII distribution. [m, v] = burrstat (lambda, c, k) returns the mean and variance of the Burr type XII distribution with scale parameter lambda, first shape parameter c, and second shape parameter k. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Burr distribution can be found at https://en.wikipedia.org/wiki/Burr_distribution See also: gevcdf, gevinv, gevpdf, gevrnd, gevfit, gevlike # name: # type: sq_string # elements: 1 # length: 53 Compute statistics of the Burr type XII distribution. # name: # type: sq_string # elements: 1 # length: 8 chi2stat # name: # type: sq_string # elements: 1 # length: 452 statistics: [m, v] = chi2stat (df) Compute statistics of the chi-squared distribution. [m, v] = chi2stat (df) returns the mean and variance of the chi-squared distribution with df degrees of freedom. The size of m (mean) and v (variance) is the same size of the input argument. Further information about the chi-squared distribution can be found at https://en.wikipedia.org/wiki/Chi-squared_distribution See also: chi2cdf, chi2inv, chi2pdf, chi2rnd # name: # type: sq_string # elements: 1 # length: 51 Compute statistics of the chi-squared distribution. # name: # type: sq_string # elements: 1 # length: 11 copulaparam # name: # type: sq_string # elements: 1 # length: 2052 statistics: param = copulaparam (family, r) statistics: param = copulaparam (..., "type", type) Copula parameter as a function of rank correlation. param = copulaparam (family, r) returns the linear or copula parameter param corresponding to a copula of the family family that has Kendall's rank correlation r. It is the inverse of copulastat. family is the copula family name. It can be "Gaussian" for the Gaussian family, "t" for the Student's t family, "Clayton" for the Clayton family, "Gumbel" for the Gumbel-Hougaard family, "Frank" for the Frank family, "AMH" for the Ali-Mikhail-Haq family, or "FGM" for the Farlie-Gumbel-Morgenstern family. The last two are Octave extensions that MATLAB does not provide, and are treated as bivariate. Neither reaches the whole range of either rank correlation: the Ali-Mikhail-Haq family covers a Kendall's tau in [(5-8*log (2))/3, 1/3] and the Farlie-Gumbel-Morgenstern family one in [-2/9, 2/9]. For the Gaussian and Student's t families, r is a scalar rank correlation or a matrix of pairwise rank correlations, and param is the corresponding linear correlation of the same size. For the Clayton, Gumbel-Hougaard, and Frank families, r is a scalar rank correlation and param is the scalar copula parameter. The Gumbel-Hougaard family models positive dependence only, so r must be non-negative for that family. param = copulaparam (..., "type", type) selects the measure of rank correlation given in r. type can be "Kendall" (the default) for Kendall's tau, or "Spearman" for Spearman's rho. The Gaussian and Student's t families and the Kendall's tau of the Clayton and Gumbel-Hougaard families are inverted in closed form. The remaining cases are inverted numerically from copulastat. Note: for the Archimedean families with "Spearman", the underlying relationship is computed by exact numerical integration rather than the interpolated table used by MATLAB, so results may differ from MATLAB by up to about 10^{-4}. See copulastat. See also: copulastat, copulafit, copulacdf, copulapdf, copularnd # name: # type: sq_string # elements: 1 # length: 51 Copula parameter as a function of rank correlation. # name: # type: sq_string # elements: 1 # length: 10 copulastat # name: # type: sq_string # elements: 1 # length: 2149 statistics: r = copulastat (family, param) statistics: r = copulastat (..., "type", type) Rank correlation for a copula family. r = copulastat (family, param) returns Kendall's rank correlation r corresponding to a copula of the family family with linear or copula parameter param. family is the copula family name. It can be "Gaussian" for the Gaussian family, "t" for the Student's t family, "Clayton" for the Clayton family, "Gumbel" for the Gumbel-Hougaard family, "Frank" for the Frank family, "AMH" for the Ali-Mikhail-Haq family, or "FGM" for the Farlie-Gumbel-Morgenstern family. The last two are Octave extensions that MATLAB does not provide, and are treated as bivariate. Neither reaches the whole range of either rank correlation: the Ali-Mikhail-Haq family covers a Kendall's tau in [(5-8*log (2))/3, 1/3] and the Farlie-Gumbel-Morgenstern family one in [-2/9, 2/9]. For the Gaussian and Student's t families, param is a linear correlation coefficient rho in the range [-1,1], or a p-by-p correlation matrix, in which case r has the same size and each element is computed elementwise. For the Clayton, Gumbel-Hougaard, and Frank families, param is the scalar copula parameter. r = copulastat (..., "type", type) selects the measure of rank correlation. type can be "Kendall" (the default) for Kendall's tau, or "Spearman" for Spearman's rho. The relationships are closed form for the Gaussian and Student's t families (r = 2 \arcsin(\rho) / \pi for Kendall's tau and r = 6 \arcsin(\rho/2) / \pi for Spearman's rho) and for the Kendall's tau of the Archimedean families. Spearman's rho of the Archimedean families has no closed form and is computed by accurate numerical integration of the copula. Note: MATLAB returns the Archimedean Spearman's rho by interpolating an internal precomputed table, whose values deviate from the true relationship by up to about 10^{-4}. This implementation returns the mathematically exact value instead, so results for copulastat (family, param, "type", "Spearman") with an Archimedean family may differ from MATLAB at that level. See also: copulaparam, copulafit, copulacdf, copulapdf, copularnd # name: # type: sq_string # elements: 1 # length: 37 Rank correlation for a copula family. # name: # type: sq_string # elements: 1 # length: 6 evstat # name: # type: sq_string # elements: 1 # length: 957 statistics: [m, v] = evstat (mu, sigma) Compute statistics of the extreme value distribution. [m, v] = evstat (mu, sigma) returns the mean and variance of the extreme value distribution (also known as the Gumbel or the type I generalized extreme value distribution) with location parameter mu and scale parameter sigma. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. The type 1 extreme value distribution is also known as the Gumbel distribution. This version is suitable for modeling minima. The mirror image of this distribution can be used to model maxima by negating x. If y has a Weibull distribution, then x = log (y) has the type 1 extreme value distribution. Further information about the Gumbel distribution can be found at https://en.wikipedia.org/wiki/Gumbel_distribution See also: evcdf, evinv, evpdf, evrnd, evfit, evlike # name: # type: sq_string # elements: 1 # length: 53 Compute statistics of the extreme value distribution. # name: # type: sq_string # elements: 1 # length: 7 expstat # name: # type: sq_string # elements: 1 # length: 737 statistics: [m, v] = expstat (mu) Compute statistics of the exponential distribution. [m, v] = expstat (mu) returns the mean and variance of the exponential distribution with mean parameter mu. The size of m (mean) and v (variance) is the same size of the input argument. A common alternative parameterization of the exponential distribution is to use the parameter λ defined as the mean number of events in an interval as opposed to the parameter μ, which is the mean wait time for an event to occur. λ and μ are reciprocals, i.e. μ = 1 / λ. Further information about the exponential distribution can be found at https://en.wikipedia.org/wiki/Exponential_distribution See also: expcdf, expinv, exppdf, exprnd, expfit, explike # name: # type: sq_string # elements: 1 # length: 51 Compute statistics of the exponential distribution. # name: # type: sq_string # elements: 1 # length: 5 fstat # name: # type: sq_string # elements: 1 # length: 502 statistics: [m, v] = fstat (df1, df2) Compute statistics of the F-distribution. [m, v] = fstat (df1, df2) returns the mean and variance of the F-distribution with df1 and df2 degrees of freedom. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the F-distribution can be found at https://en.wikipedia.org/wiki/F-distribution See also: fcdf, finv, fpdf, frnd # name: # type: sq_string # elements: 1 # length: 41 Compute statistics of the F-distribution. # name: # type: sq_string # elements: 1 # length: 7 gamstat # name: # type: sq_string # elements: 1 # length: 941 statistics: [m, v] = gamstat (a, b) Compute statistics of the Gamma distribution. [m, v] = gamstat (a, b) returns the mean and variance of the Gamma distribution with shape parameter a and scale parameter b. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. OCTAVE/MATLAB use the alternative parameterization given by the pair α, β, i.e. shape a and scale b. In Wikipedia, the two common parameterizations use the pairs k, θ, as shape and scale, and α, β, as shape and rate, respectively. The parameter names a and b used here (for MATLAB compatibility) correspond to the parameter notation k, θ instead of the α, β as reported in Wikipedia. Further information about the Gamma distribution can be found at https://en.wikipedia.org/wiki/Gamma_distribution See also: gamcdf, gaminv, gampdf, gamrnd, gamfit, gamlike # name: # type: sq_string # elements: 1 # length: 45 Compute statistics of the Gamma distribution. # name: # type: sq_string # elements: 1 # length: 7 geostat # name: # type: sq_string # elements: 1 # length: 460 statistics: [m, v] = geostat (ps) Compute statistics of the geometric distribution. [m, v] = geostat (ps) returns the mean and variance of the geometric distribution with probability of success parameter ps. The size of m (mean) and v (variance) is the same size of the input argument. Further information about the geometric distribution can be found at https://en.wikipedia.org/wiki/Geometric_distribution See also: geocdf, geoinv, geopdf, geornd, geofit # name: # type: sq_string # elements: 1 # length: 49 Compute statistics of the geometric distribution. # name: # type: sq_string # elements: 1 # length: 7 gevstat # name: # type: sq_string # elements: 1 # length: 879 statistics: [m, v] = gevstat (k, sigma, mu) Compute statistics of the generalized extreme value distribution. [m, v] = gevstat (k, sigma, mu) returns the mean and variance of the generalized extreme value distribution with shape parameter k, scale parameter sigma, and location parameter mu. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. The mean of the GEV distribution is not finite when k >= 1, and the variance is not finite when k >= 1/2. The GEV distribution has positive density only for values of x such that k * (x - mu) / sigma > -1. Further information about the generalized extreme value distribution can be found at https://en.wikipedia.org/wiki/Generalized_extreme_value_distribution See also: gevcdf, gevinv, gevpdf, gevrnd, gevfit, gevlike # name: # type: sq_string # elements: 1 # length: 65 Compute statistics of the generalized extreme value distribution. # name: # type: sq_string # elements: 1 # length: 6 gpstat # name: # type: sq_string # elements: 1 # length: 1129 statistics: [m, v] = gpstat (k, sigma, theta) Compute statistics of the generalized Pareto distribution. [m, v] = gpstat (k, sigma, theta) returns the mean and variance of the generalized Pareto distribution with shape parameter k, scale parameter sigma, and location parameter theta. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. When k = 0 and theta = 0, the generalized Pareto distribution is equivalent to the exponential distribution. When k > 0 and theta = sigma / k, the generalized Pareto distribution is equivalent to the Pareto distribution. The mean of the generalized Pareto distribution is not finite when k >= 1, and the variance is not finite when k >= 1/2. When k >= 0, the generalized Pareto distribution has positive density for x > theta, or, when k < 0, for 0 <= (x - theta) / sigma <= -1 / k. Further information about the generalized Pareto distribution can be found at https://en.wikipedia.org/wiki/Generalized_Pareto_distribution See also: gpcdf, gpinv, gppdf, gprnd, gpfit, gplike # name: # type: sq_string # elements: 1 # length: 58 Compute statistics of the generalized Pareto distribution. # name: # type: sq_string # elements: 1 # length: 10 gumbelstat # name: # type: sq_string # elements: 1 # length: 1202 statistics: [m, v] = gumbelstat (mu, beta) Compute statistics of the Gumbel distribution. [m, v] = gumbelstat (mu, beta) returns the mean and variance of the Gumbel distribution (also known as the extreme value or the type I generalized extreme value distribution) with location parameter mu and scale parameter beta. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. The mean is mu + gamma * beta, where gamma is the Euler-Mascheroni constant, and the variance is pi^2 * beta^2 / 6. A non-positive beta returns NaN for both. The Gumbel distribution is used to model the distribution of the maximum (or the minimum) of a number of samples of various distributions. This version is suitable for modeling maxima. For modeling minima, use the alternative extreme value statistics, evstat. Further information about the Gumbel distribution can be found at https://en.wikipedia.org/wiki/Gumbel_distribution Input arguments must be double or single; integer, logical, and character arrays are rejected. See also: gumbelcdf, gumbelinv, gumbelpdf, gumbelrnd, gumbelfit, gumbellike, evstat # name: # type: sq_string # elements: 1 # length: 46 Compute statistics of the Gumbel distribution. # name: # type: sq_string # elements: 1 # length: 6 hnstat # name: # type: sq_string # elements: 1 # length: 599 statistics: [m, v] = hnstat (mu, sigma) Compute statistics of the half-normal distribution. [m, v] = hnstat (mu, sigma) returns the mean and variance of the half-normal distribution with non-centrality (distance) parameter mu and scale parameter sigma. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the half-normal distribution can be found at https://en.wikipedia.org/wiki/Half-normal_distribution See also: hncdf, hninv, hnpdf, hnrnd, hnfit, hnlike # name: # type: sq_string # elements: 1 # length: 51 Compute statistics of the half-normal distribution. # name: # type: string # elements: 1 # length: 8 hygestat # name: # type: sq_string # elements: 1 # length: 1187 statistics: [mn, v] = hygestat (m, k, n) Compute statistics of the hypergeometric distribution. [mn, v] = hygestat (m, k, n) returns the mean and variance of the hypergeometric distribution parameters m, k, and n. m is the total size of the population of the hypergeometric distribution. The elements of m must be non-negative integers. k is the number of marked items of the hypergeometric distribution. The elements of k must be natural numbers. n is the size of the drawn sample of the hypergeometric distribution. The elements of n must be non-negative integers. A sample of size 0 contains no marked items, so its mean and variance are 0, even for an empty population, where MATLAB returns NaN for both. A sample from a population of one always holds the same number of marked items, so its variance is 0, where MATLAB returns NaN. The size of mn (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the hypergeometric distribution can be found at https://en.wikipedia.org/wiki/Hypergeometric_distribution See also: hygecdf, hygeinv, hygepdf, hygernd # name: # type: sq_string # elements: 1 # length: 54 Compute statistics of the hypergeometric distribution. # name: # type: sq_string # elements: 1 # length: 8 invgstat # name: # type: sq_string # elements: 1 # length: 617 statistics: [m, v] = invgstat (mu, lambda) Compute statistics of the inverse Gaussian distribution. [m, v] = invgstat (mu, lambda) returns the mean and variance of the inverse Gaussian distribution with mean parameter mu and shape parameter lambda. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the inverse Gaussian distribution can be found at https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution See also: invgcdf, invginv, invgpdf, invgrnd, invgfit, invglike # name: # type: sq_string # elements: 1 # length: 56 Compute statistics of the inverse Gaussian distribution. # name: # type: sq_string # elements: 1 # length: 8 logistat # name: # type: sq_string # elements: 1 # length: 582 statistics: [m, v] = logistat (mu, sigma) Compute statistics of the logistic distribution. [m, v] = logistat (mu, sigma) returns the mean and variance of the logistic distribution with mean parameter mu and scale parameter sigma. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the logistic distribution can be found at https://en.wikipedia.org/wiki/Logistic_distribution See also: logicdf, logiinv, logipdf, logirnd, logifit, logilike # name: # type: sq_string # elements: 1 # length: 48 Compute statistics of the logistic distribution. # name: # type: sq_string # elements: 1 # length: 8 loglstat # name: # type: sq_string # elements: 1 # length: 839 statistics: [m, v] = loglstat (mu, sigma) Compute statistics of the loglogistic distribution. [m, v] = loglstat (mu, sigma) returns the mean and variance of the loglogistic distribution with mean parameter mu and scale parameter sigma. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the loglogistic distribution can be found at https://en.wikipedia.org/wiki/Log-logistic_distribution OCTAVE/MATLAB use an alternative parameterization given by the pair μ, σ, i.e. mu and sigma, in analogy with the logistic distribution. Their relation to the α and b parameters used in Wikipedia are given below: mu = log (a) sigma = 1 / a See also: logncdf, logninv, lognpdf, lognrnd, lognfit, lognlike # name: # type: sq_string # elements: 1 # length: 51 Compute statistics of the loglogistic distribution. # name: # type: sq_string # elements: 1 # length: 8 lognstat # name: # type: sq_string # elements: 1 # length: 658 statistics: [m, v] = lognstat (mu, sigma) Compute statistics of the lognormal distribution. [m, v] = lognstat (mu, sigma) returns the mean and variance of the lognormal distribution with mean parameter mu and standard deviation parameter sigma, each corresponding to the associated normal distribution. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the lognormal distribution can be found at https://en.wikipedia.org/wiki/Log-normal_distribution See also: logncdf, logninv, lognpdf, lognrnd, lognfit, lognlike # name: # type: sq_string # elements: 1 # length: 49 Compute statistics of the lognormal distribution. # name: # type: sq_string # elements: 1 # length: 8 nakastat # name: # type: sq_string # elements: 1 # length: 582 statistics: [m, v] = nakastat (mu, omega) Compute statistics of the Nakagami distribution. [m, v] = nakastat (mu, omega) returns the mean and variance of the Nakagami distribution with shape parameter mu and spread parameter omega. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Nakagami distribution can be found at https://en.wikipedia.org/wiki/Normal_distribution See also: nakacdf, nakainv, nakapdf, nakarnd, nakafit, nakalike # name: # type: sq_string # elements: 1 # length: 48 Compute statistics of the Nakagami distribution. # name: # type: sq_string # elements: 1 # length: 8 nbinstat # name: # type: sq_string # elements: 1 # length: 745 statistics: [m, v] = nbinstat (r, ps) Compute statistics of the negative binomial distribution. [m, v] = nbinstat (r, ps) returns the mean and variance of the negative binomial distribution with parameters r and ps, where r is the number of successes until the experiment is stopped and ps is the probability of success in each experiment, given the number of failures in x. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the negative binomial distribution can be found at https://en.wikipedia.org/wiki/Negative_binomial_distribution See also: nbincdf, nbininv, nbinpdf, nbinrnd, nbinfit, nbinlike # name: # type: sq_string # elements: 1 # length: 57 Compute statistics of the negative binomial distribution. # name: # type: sq_string # elements: 1 # length: 7 ncfstat # name: # type: sq_string # elements: 1 # length: 617 statistics: [m, v] = ncfstat (df1, df1, lambda) Compute statistics for the noncentral F-distribution. [m, v] = ncfstat (df1, df1, lambda) returns the mean and variance of the noncentral F-distribution with df1 and df2 degrees of freedom and noncentrality parameter lambda. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the noncentral F-distribution can be found at https://en.wikipedia.org/wiki/Noncentral_F-distribution See also: ncfcdf, ncfinv, ncfpdf, ncfrnd, fstat # name: # type: sq_string # elements: 1 # length: 53 Compute statistics for the noncentral F-distribution. # name: # type: sq_string # elements: 1 # length: 7 nctstat # name: # type: sq_string # elements: 1 # length: 584 statistics: [m, v] = nctstat (df, mu) Compute statistics for the noncentral t-distribution. [m, v] = nctstat (df, mu) returns the mean and variance of the noncentral t-distribution with df degrees of freedom and noncentrality parameter mu. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the noncentral t-distribution can be found at https://en.wikipedia.org/wiki/Noncentral_t-distribution See also: nctcdf, nctinv, nctpdf, nctrnd, tstat # name: # type: sq_string # elements: 1 # length: 53 Compute statistics for the noncentral t-distribution. # name: # type: sq_string # elements: 1 # length: 8 ncx2stat # name: # type: sq_string # elements: 1 # length: 635 statistics: [m, v] = ncx2stat (df, lambda) Compute statistics for the noncentral chi-squared distribution. [m, v] = ncx2stat (df, lambda) returns the mean and variance of the noncentral chi-squared distribution with df degrees of freedom and noncentrality parameter lambda. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the noncentral chi-squared distribution can be found at https://en.wikipedia.org/wiki/Noncentral_chi-squared_distribution See also: ncx2cdf, ncx2inv, ncx2pdf, ncx2rnd # name: # type: sq_string # elements: 1 # length: 63 Compute statistics for the noncentral chi-squared distribution. # name: # type: sq_string # elements: 1 # length: 8 normstat # name: # type: sq_string # elements: 1 # length: 595 statistics: [m, v] = normstat (mu, sigma) Compute statistics of the normal distribution. [m, v] = normstat (mu, sigma) returns the mean and variance of the normal distribution with non-centrality (distance) parameter mu and scale parameter sigma. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the normal distribution can be found at https://en.wikipedia.org/wiki/Normal_distribution See also: normcdf, norminv, normpdf, normrnd, normfit, normlike # name: # type: sq_string # elements: 1 # length: 46 Compute statistics of the normal distribution. # name: # type: sq_string # elements: 1 # length: 6 plstat # name: # type: sq_string # elements: 1 # length: 563 statistics: [m, v] = plstat (x, Fx) Compute statistics of the piecewise linear distribution. [m, v] = plstat (x, Fx) returns the mean, m, and variance, v, of the piecewise linear distribution with a vector of x values at which the CDF changes slope and a vector of CDF values Fx that correspond to each value in x. Both x and Fx must be vectors of the same size and at least 2-elements long. Further information about the piecewise linear distribution can be found at https://en.wikipedia.org/wiki/Piecewise_linear_function See also: plcdf, plinv, plpdf, plrnd # name: # type: sq_string # elements: 1 # length: 56 Compute statistics of the piecewise linear distribution. # name: # type: sq_string # elements: 1 # length: 8 poisstat # name: # type: sq_string # elements: 1 # length: 469 statistics: [m, v] = poisstat (lambda) Compute statistics of the Poisson distribution. [m, v] = poisstat (lambda) returns the mean and variance of the Poisson distribution with rate parameter lambda. The size of m (mean) and v (variance) is the same size of the input argument. Further information about the Poisson distribution can be found at https://en.wikipedia.org/wiki/Poisson_distribution See also: poisscdf, poissinv, poisspdf, poissrnd, poissfit, poisslike # name: # type: sq_string # elements: 1 # length: 47 Compute statistics of the Poisson distribution. # name: # type: sq_string # elements: 1 # length: 8 raylstat # name: # type: sq_string # elements: 1 # length: 545 statistics: [m, v] = raylstat (sigma) Compute statistics of the Rayleigh distribution. [m, v] = raylstat (sigma) returns the mean and variance of the Rayleigh distribution with scale parameter sigma. The size of m (mean) and v (variance) is the same size of the input argument. Further information about the Rayleigh distribution can be found at https://en.wikipedia.org/wiki/Rayleigh_distribution The prob.RayleighDistribution class names this same parameter B, after MATLAB. See also: raylcdf, raylinv, raylpdf, raylrnd, raylfit, rayllike # name: # type: sq_string # elements: 1 # length: 48 Compute statistics of the Rayleigh distribution. # name: # type: sq_string # elements: 1 # length: 8 ricestat # name: # type: sq_string # elements: 1 # length: 590 statistics: [m, v] = ricestat (s, sigma) Compute statistics of the Rician distribution. [m, v] = ricestat (s, sigma) returns the mean and variance of the Rician distribution with non-centrality (distance) parameter s and scale parameter sigma. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Rician distribution can be found at https://en.wikipedia.org/wiki/Rice_distribution See also: ricecdf, riceinv, ricepdf, ricernd, ricefit, ricelike # name: # type: sq_string # elements: 1 # length: 46 Compute statistics of the Rician distribution. # name: # type: sq_string # elements: 1 # length: 8 stdrstat # name: # type: sq_string # elements: 1 # length: 940 statistics: [m, v] = stdrstat (k, df) Compute statistics of the studentized range distribution. [m, v] = stdrstat (k, df) returns the mean and variance of the studentized range distribution for k groups and df degrees of freedom. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other input. k must be an integer of at least 2 and df positive, Inf included; otherwise both are NaN. The mean exists only for df greater than 1 and the variance only for df greater than 2; where either does not, it is NaN. For df equal to Inf they are the mean and variance of the range of k standard normal variables. MATLAB has no public counterpart of this function. Further information about the studentized range distribution can be found at https://en.wikipedia.org/wiki/Studentized_range_distribution See also: stdrcdf, stdrinv, stdrpdf, stdrrnd # name: # type: sq_string # elements: 1 # length: 57 Compute statistics of the studentized range distribution. # name: # type: sq_string # elements: 1 # length: 7 tlsstat # name: # type: sq_string # elements: 1 # length: 699 statistics: [m, v] = tlsstat (mu, sigma, nu) Compute statistics of the location-scale Student's T distribution. [m, v] = tlsstat (mu, sigma, nu) returns the mean and variance of the location-scale Student's T distribution with location parameter mu, scale parameter sigma, and nu degrees of freedom. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the location-scale Student's T distribution can be found at https://en.wikipedia.org/wiki/Student%27s_t-distribution#Location-scale_t_distribution See also: tlscdf, tlsinv, tlspdf, tlsrnd, tlsfit, tlslike # name: # type: sq_string # elements: 1 # length: 66 Compute statistics of the location-scale Student's T distribution. # name: # type: sq_string # elements: 1 # length: 7 tristat # name: # type: sq_string # elements: 1 # length: 1059 statistics: [m, v] = tristat (a, b, c) Compute statistics of the Triangular distribution. [m, v] = tristat (a, b, c) returns the mean and variance of the Triangular distribution with lower limit parameter a, peak location (mode) parameter b, and upper limit parameter c. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Note that the order of the parameter input arguments has been changed after statistics version 1.6.3 in order to be MATLAB compatible with the parameters used in the TriangularDistribution probability distribution object. More specifically, the positions of the parameters b and c have been swapped. As a result, the naming conventions no longer coincide with those used in Wikipedia, in which b denotes the upper limit and c denotes the mode or peak parameter. Further information about the triangular distribution can be found at https://en.wikipedia.org/wiki/Triangular_distribution See also: tcdf, tinv, tpdf, trnd # name: # type: sq_string # elements: 1 # length: 50 Compute statistics of the Triangular distribution. # name: # type: sq_string # elements: 1 # length: 5 tstat # name: # type: sq_string # elements: 1 # length: 436 statistics: [m, v] = tstat (df) Compute statistics of the Student's T distribution. [m, v] = tstat (df) returns the mean and variance of the Student's T distribution with df degrees of freedom. The size of m (mean) and v (variance) is the same size of the input argument. Further information about the Student's T distribution can be found at https://en.wikipedia.org/wiki/Student%27s_t-distribution See also: tcdf, tinv, tpdf, trnd # name: # type: sq_string # elements: 1 # length: 51 Compute statistics of the Student's T distribution. # name: # type: sq_string # elements: 1 # length: 8 unidstat # name: # type: sq_string # elements: 1 # length: 582 statistics: [m, v] = unidstat (df) Compute statistics of the discrete uniform cumulative distribution. [m, v] = unidstat (df) returns the mean and variance of the discrete uniform cumulative distribution with parameter N, which corresponds to the maximum observable value and must be a positive natural number. The size of m (mean) and v (variance) is the same size of the input argument. Further information about the discrete uniform distribution can be found at https://en.wikipedia.org/wiki/Discrete_uniform_distribution See also: unidcdf, unidinv, unidpdf, unidrnd, unidfit # name: # type: sq_string # elements: 1 # length: 67 Compute statistics of the discrete uniform cumulative distribution. # name: # type: sq_string # elements: 1 # length: 8 unifstat # name: # type: sq_string # elements: 1 # length: 658 statistics: [m, v] = unifstat (df) Compute statistics of the continuous uniform cumulative distribution. [m, v] = unifstat (df) returns the mean and variance of the continuous uniform cumulative distribution with parameters a and b, which define the lower and upper bounds of the interval [a, b]. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the continuous uniform distribution can be found at https://en.wikipedia.org/wiki/Continuous_uniform_distribution See also: unifcdf, unifinv, unifpdf, unifrnd, unifit # name: # type: sq_string # elements: 1 # length: 69 Compute statistics of the continuous uniform cumulative distribution. # name: # type: sq_string # elements: 1 # length: 7 wblstat # name: # type: sq_string # elements: 1 # length: 703 statistics: [m, v] = wblstat (lambda, k) Compute statistics of the Weibull distribution. [m, v] = wblstat (lambda, k) returns the mean and variance of the Weibull distribution with scale parameter lambda and shape parameter k. The size of m (mean) and v (variance) is the common size of the input arguments. A scalar input functions as a constant matrix of the same size as the other inputs. Further information about the Weibull distribution can be found at https://en.wikipedia.org/wiki/Weibull_distribution The prob.WeibullDistribution class names these same two parameters A and B, after MATLAB. lambda is its A and k is its B. See also: wblcdf, wblinv, wblpdf, wblrnd, wblfit, wbllike, wblplot # name: # type: sq_string # elements: 1 # length: 47 Compute statistics of the Weibull distribution. statistics-release-2.1.2/inst/Distribution_Statistics/evstat.m000066400000000000000000000073551527311104300246650ustar00rootroot00000000000000## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} evstat (@var{mu}, @var{sigma}) ## ## Compute statistics of the extreme value distribution. ## ## @code{[@var{m}, @var{v}] = evstat (@var{mu}, @var{sigma})} returns the mean ## and variance of the extreme value distribution (also known as the Gumbel ## or the type I generalized extreme value distribution) with location parameter ## @var{mu} and scale parameter @var{sigma}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## The type 1 extreme value distribution is also known as the Gumbel ## distribution. This version is suitable for modeling minima. The mirror image ## of this distribution can be used to model maxima by negating @var{x}. If ## @var{y} has a Weibull distribution, then @code{@var{x} = log (@var{y})} has ## the type 1 extreme value distribution. ## ## Further information about the Gumbel distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gumbel_distribution} ## ## @seealso{evcdf, evinv, evpdf, evrnd, evfit, evlike} ## @end deftypefn function [m, v] = evstat (mu, sigma) ## Check for valid number of input arguments if (nargin < 2) error ("evstat: function called with too few input arguments."); endif ## Check for MU and SIGMA being numeric if (! (isnumeric (mu) && isnumeric (sigma))) error ("evstat: MU and SIGMA must be numeric."); endif ## Check for MU and SIGMA being real if (iscomplex (mu) || iscomplex (sigma)) error ("evstat: MU and SIGMA must not be complex."); endif ## Check for common size of MU and SIGMA if (! isscalar (mu) || ! isscalar (sigma)) [retval, mu, sigma] = common_size (mu, sigma); if (retval > 0) error ("evstat: MU and SIGMA must be of common size or scalars."); endif endif ## Return NaNs for out of range values of SIGMA sigma(sigma <= 0) = NaN; ## Calculate mean and variance m = mu + psi (1) .* sigma; v = (pi .* sigma) .^ 2 ./ 6; endfunction ## Input validation tests %!error evstat () %!error evstat (1) %!error evstat ({}, 2) %!error evstat (1, '') %!error evstat (i, 2) %!error evstat (1, i) %!error ... %! evstat (ones (3), ones (2)) %!error ... %! evstat (ones (2), ones (3)) ## Output validation tests %!shared x, y0, y1 %! x = [-5, 0, 1, 2, 3]; %! y0 = [NaN, NaN, 0.4228, 0.8456, 1.2684]; %! y1 = [-5.5772, -3.4633, -3.0405, -2.6177, -2.1949]; %!assert_equal (evstat (x, x), y0, 1e-4) %!assert_equal (evstat (x, x+6), y1, 1e-4) %!assert_equal (evstat (x, x-6), NaN (1,5)) statistics-release-2.1.2/inst/Distribution_Statistics/expstat.m000066400000000000000000000053541527311104300250440ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} expstat (@var{mu}) ## ## Compute statistics of the exponential distribution. ## ## @code{[@var{m}, @var{v}] = expstat (@var{mu})} returns the mean and ## variance of the exponential distribution with mean parameter @var{mu}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the same size of the ## input argument. ## ## A common alternative parameterization of the exponential distribution is to ## use the parameter @math{λ} defined as the mean number of events in an ## interval as opposed to the parameter @math{μ}, which is the mean wait time ## for an event to occur. @math{λ} and @math{μ} are reciprocals, ## i.e. @math{μ = 1 / λ}. ## ## Further information about the exponential distribution can be found at ## @url{https://en.wikipedia.org/wiki/Exponential_distribution} ## ## @seealso{expcdf, expinv, exppdf, exprnd, expfit, explike} ## @end deftypefn function [m, v] = expstat (mu) ## Check for valid number of input arguments if (nargin < 1) error ("expstat: function called with too few input arguments."); endif ## Check for MU being numeric if (! isnumeric (mu)) error ("expstat: MU must be numeric."); endif ## Check for MU being real if (iscomplex (mu)) error ("expstat: MU must not be complex."); endif ## Calculate moments m = mu; v = m .^ 2; ## Continue argument check k = find (! (mu > 0) | ! (mu < Inf)); if (any (k)) m(k) = NaN; v(k) = NaN; endif endfunction ## Input validation tests %!error expstat () %!error expstat ({}) %!error expstat ('') %!error expstat (i) ## Output validation tests %!test %! mu = 1:6; %! [m, v] = expstat (mu); %! assert_equal (m, [1, 2, 3, 4, 5, 6], 0.001); %! assert_equal (v, [1, 4, 9, 16, 25, 36], 0.001); statistics-release-2.1.2/inst/Distribution_Statistics/fstat.m000066400000000000000000000073541527311104300244770ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} fstat (@var{df1}, @var{df2}) ## ## Compute statistics of the @math{F}-distribution. ## ## @code{[@var{m}, @var{v}] = fstat (@var{df1}, @var{df2})} returns the mean and ## variance of the @math{F}-distribution with @var{df1} and @var{df2} degrees ## of freedom. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## Further information about the @math{F}-distribution can be found at ## @url{https://en.wikipedia.org/wiki/F-distribution} ## ## @seealso{fcdf, finv, fpdf, frnd} ## @end deftypefn function [m, v] = fstat (df1, df2) ## Check for valid number of input arguments if (nargin < 2) error ("fstat: function called with too few input arguments."); endif ## Check for DF1 and DF2 being numeric if (! (isnumeric (df1) && isnumeric (df2))) error ("fstat: DF1 and DF2 must be numeric."); endif ## Check for DF1 and DF2 being real if (iscomplex (df1) || iscomplex (df2)) error ("fstat: DF1 and DF2 must not be complex."); endif ## Check for common size of DF1 and DF2 if (! isscalar (df1) || ! isscalar (df2)) [retval, df1, df2] = common_size (df1, df2); if (retval > 0) error ("fstat: DF1 and DF2 must be of common size or scalars."); endif endif ## Calculate moments m = df2 ./ (df2 - 2); v = (2 .* (df2 .^ 2) .* (df1 + df2 - 2)) ./ ... (df1 .* ((df2 - 2) .^ 2) .* (df2 - 4)); ## Continue argument check k = find (! (df1 > 0) | ! (df1 < Inf) | ! (df2 > 2) | ! (df2 < Inf)); if (any (k)) m(k) = NaN; v(k) = NaN; endif k = find (! (df2 > 4)); if (any (k)) v(k) = NaN; endif endfunction ## Input validation tests %!error fstat () %!error fstat (1) %!error fstat ({}, 2) %!error fstat (1, '') %!error fstat (i, 2) %!error fstat (1, i) %!error ... %! fstat (ones (3), ones (2)) %!error ... %! fstat (ones (2), ones (3)) ## Output validation tests %!test %! df1 = 1:6; %! df2 = 5:10; %! [m, v] = fstat (df1, df2); %! expected_mn = [1.6667, 1.5000, 1.4000, 1.3333, 1.2857, 1.2500]; %! expected_v = [22.2222, 6.7500, 3.4844, 2.2222, 1.5869, 1.2153]; %! assert_equal (m, expected_mn, 0.001); %! assert_equal (v, expected_v, 0.001); %!test %! df1 = 1:6; %! [m, v] = fstat (df1, 5); %! expected_mn = [1.6667, 1.6667, 1.6667, 1.6667, 1.6667, 1.6667]; %! expected_v = [22.2222, 13.8889, 11.1111, 9.7222, 8.8889, 8.3333]; %! assert_equal (m, expected_mn, 0.001); %! assert_equal (v, expected_v, 0.001); statistics-release-2.1.2/inst/Distribution_Statistics/gamstat.m000066400000000000000000000077611527311104300250200ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} gamstat (@var{a}, @var{b}) ## ## Compute statistics of the Gamma distribution. ## ## @code{[@var{m}, @var{v}] = gamstat (@var{a}, @var{b})} returns the mean ## and variance of the Gamma distribution with shape parameter @var{a} and ## scale parameter @var{b}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## OCTAVE/MATLAB use the alternative parameterization given by the pair ## @math{α, β}, i.e. shape @var{a} and scale @var{b}. In Wikipedia, the two ## common parameterizations use the pairs @math{k, θ}, as shape and scale, and ## @math{α, β}, as shape and rate, respectively. The parameter names @var{a} ## and @var{b} used here (for MATLAB compatibility) correspond to the parameter ## notation @math{k, θ} instead of the @math{α, β} as reported in Wikipedia. ## ## Further information about the Gamma distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gamma_distribution} ## ## @seealso{gamcdf, gaminv, gampdf, gamrnd, gamfit, gamlike} ## @end deftypefn function [m, v] = gamstat (a, b) ## Check for valid number of input arguments if (nargin < 2) error ("gamstat: function called with too few input arguments."); endif ## Check for A and B being numeric if (! (isnumeric (a) && isnumeric (b))) error ("gamstat: A and B must be numeric."); endif ## Check for A and B being real if (iscomplex (a) || iscomplex (b)) error ("gamstat: A and B must not be complex."); endif ## Check for common size of A and B if (! isscalar (a) || ! isscalar (b)) [retval, a, b] = common_size (a, b); if (retval > 0) error ("gamstat: A and B must be of common size or scalars."); endif endif ## Calculate moments m = a .* b; v = a .* (b .^ 2); ## Continue argument check a = find (! (a > 0) | ! (a < Inf) | ! (b > 0) | ! (b < Inf)); if (any (a)) m(a) = NaN; v(a) = NaN; endif endfunction ## Input validation tests %!error gamstat () %!error gamstat (1) %!error gamstat ({}, 2) %!error gamstat (1, '') %!error gamstat (i, 2) %!error gamstat (1, i) %!error ... %! gamstat (ones (3), ones (2)) %!error ... %! gamstat (ones (2), ones (3)) ## Output validation tests %!test %! a = 1:6; %! b = 1:0.2:2; %! [m, v] = gamstat (a, b); %! expected_m = [1.00, 2.40, 4.20, 6.40, 9.00, 12.00]; %! expected_v = [1.00, 2.88, 5.88, 10.24, 16.20, 24.00]; %! assert_equal (m, expected_m, 0.001); %! assert_equal (v, expected_v, 0.001); %!test %! a = 1:6; %! [m, v] = gamstat (a, 1.5); %! expected_m = [1.50, 3.00, 4.50, 6.00, 7.50, 9.00]; %! expected_v = [2.25, 4.50, 6.75, 9.00, 11.25, 13.50]; %! assert_equal (m, expected_m, 0.001); %! assert_equal (v, expected_v, 0.001); statistics-release-2.1.2/inst/Distribution_Statistics/geostat.m000066400000000000000000000047101527311104300250150ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} geostat (@var{ps}) ## ## Compute statistics of the geometric distribution. ## ## @code{[@var{m}, @var{v}] = geostat (@var{ps})} returns the mean and ## variance of the geometric distribution with probability of success parameter ## @var{ps}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the same size of the ## input argument. ## ## Further information about the geometric distribution can be found at ## @url{https://en.wikipedia.org/wiki/Geometric_distribution} ## ## @seealso{geocdf, geoinv, geopdf, geornd, geofit} ## @end deftypefn function [m, v] = geostat (ps) ## Check for valid number of input arguments if (nargin < 1) error ("geostat: function called with too few input arguments."); endif ## Check for PS being numeric if (! isnumeric (ps)) error ("geostat: PS must be numeric."); endif ## Check for PS being real if (iscomplex (ps)) error ("geostat: PS must not be complex."); endif ## Calculate moments q = 1 - ps; m = q ./ ps; v = q ./ (ps .^ 2); ## Continue argument check k = find (! (ps >= 0) | ! (ps <= 1)); if (any (k)) m(k) = NaN; v(k) = NaN; endif endfunction ## Input validation tests %!error geostat () %!error geostat ({}) %!error geostat ('') %!error geostat (i) ## Output validation tests %!test %! ps = 1 ./ (1:6); %! [m, v] = geostat (ps); %! assert_equal (m, [0, 1, 2, 3, 4, 5], 0.001); %! assert_equal (v, [0, 2, 6, 12, 20, 30], 0.001); statistics-release-2.1.2/inst/Distribution_Statistics/gevstat.m000066400000000000000000000113311527311104300250210ustar00rootroot00000000000000## Copyright (C) 2012 Nir Krakauer ## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} gevstat (@var{k}, @var{sigma}, @var{mu}) ## ## Compute statistics of the generalized extreme value distribution. ## ## @code{[@var{m}, @var{v}] = gevstat (@var{k}, @var{sigma}, @var{mu})} returns ## the mean and variance of the generalized extreme value distribution with ## shape parameter @var{k}, scale parameter @var{sigma}, and location parameter ## @var{mu}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## The mean of the GEV distribution is not finite when @qcode{@var{k} >= 1}, and ## the variance is not finite when @qcode{@var{k} >= 1/2}. The GEV distribution ## has positive density only for values of @var{x} such that ## @qcode{@var{k} * (@var{x} - @var{mu}) / @var{sigma} > -1}. ## ## Further information about the generalized extreme value distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Generalized_extreme_value_distribution} ## ## @seealso{gevcdf, gevinv, gevpdf, gevrnd, gevfit, gevlike} ## @end deftypefn function [m, v] = gevstat (k, sigma, mu) ## Check for valid number of input arguments if (nargin < 3) error ("gevstat: function called with too few input arguments."); endif ## Check for K, SIGMA, and MU being numeric if (! (isnumeric (k) && isnumeric (sigma) && isnumeric (mu))) error ("gevstat: K, SIGMA, and MU must be numeric."); endif ## Check for K, SIGMA, and MU being real if (iscomplex (k) || iscomplex (sigma) || iscomplex (mu)) error ("gevstat: K, SIGMA, and MU must not be complex."); endif ## Check for common size of K, SIGMA, and MU if (! isscalar (k) || ! isscalar (sigma) || ! isscalar (mu)) [retval, k, sigma, mu] = common_size (k, sigma, mu); if (retval > 0) error ("gevstat: K, SIGMA, and MU must be of common size or scalars."); endif endif ## Euler-Mascheroni constant eg = 0.57721566490153286; m = v = k; ## Find the mean m(k >= 1) = Inf; m(k == 0) = mu(k == 0) + eg*sigma(k == 0); m(k < 1 & k != 0) = mu(k < 1 & k != 0) + sigma(k < 1 & k != 0) .* ... (gamma (1-k(k < 1 & k != 0)) - 1) ./ k(k < 1 & k != 0); ## Find the variance v(k >= 0.5) = Inf; v(k == 0) = (pi^2 / 6) * sigma(k == 0) .^ 2; v(k < 0.5 & k != 0) = (gamma (1-2*k(k < 0.5 & k != 0)) - ... gamma (1-k(k < 0.5 & k != 0)).^2) .* ... (sigma(k < 0.5 & k != 0) ./ k(k < 0.5 & k != 0)) .^ 2; endfunction ## Input validation tests %!error gevstat () %!error gevstat (1) %!error gevstat (1, 2) %!error gevstat ({}, 2, 3) %!error gevstat (1, '', 3) %!error gevstat (1, 2, '') %!error gevstat (i, 2, 3) %!error gevstat (1, i, 3) %!error gevstat (1, 2, i) %!error ... %! gevstat (ones (3), ones (2), 3) %!error ... %! gevstat (ones (2), 2, ones (3)) %!error ... %! gevstat (1, ones (2), ones (3)) ## Output validation tests %!test %! k = [-1, -0.5, 0, 0.2, 0.4, 0.5, 1]; %! sigma = 2; %! mu = 1; %! [m, v] = gevstat (k, sigma, mu); %! expected_m = [1, 1.4551, 2.1544, 2.6423, 3.4460, 4.0898, Inf]; %! expected_v = [4, 3.4336, 6.5797, 13.3761, 59.3288, Inf, Inf]; %! assert_equal (m, expected_m, -0.001); %! assert_equal (v, expected_v, -0.001); statistics-release-2.1.2/inst/Distribution_Statistics/gpstat.m000066400000000000000000000131031527311104300246450ustar00rootroot00000000000000## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} gpstat (@var{k}, @var{sigma}, @var{theta}) ## ## Compute statistics of the generalized Pareto distribution. ## ## @code{[@var{m}, @var{v}] = gpstat (@var{k}, @var{sigma}, @var{theta})} ## returns the mean and variance of the generalized Pareto distribution with ## shape parameter @var{k}, scale parameter @var{sigma}, and location parameter ## @var{theta}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## When @var{k} = 0 and @var{theta} = 0, the generalized Pareto distribution is ## equivalent to the exponential distribution. When @code{@var{k} > 0} and ## @code{@var{theta} = @var{sigma} / @var{k}}, the generalized Pareto ## distribution is equivalent to the Pareto distribution. The mean of the ## generalized Pareto distribution is not finite when @code{@var{k} >= 1}, and ## the variance is not finite when @code{@var{k} >= 1/2}. When ## @code{@var{k} >= 0}, the generalized Pareto distribution has positive density ## for @code{@var{x} > @var{theta}}, or, when @code{@var{k} < 0}, for ## @code{0 <= (@var{x} - @var{theta}) / @var{sigma} <= -1 / @var{k}}. ## ## Further information about the generalized Pareto distribution can be found at ## @url{https://en.wikipedia.org/wiki/Generalized_Pareto_distribution} ## ## @seealso{gpcdf, gpinv, gppdf, gprnd, gpfit, gplike} ## @end deftypefn function [m, v] = gpstat (k, sigma, theta) ## Check for valid number of input arguments if (nargin < 3) error ("gpstat: function called with too few input arguments."); endif ## Check for K, SIGMA, and MU being numeric if (! (isnumeric (k) && isnumeric (sigma) && isnumeric (theta))) error ("gpstat: K, SIGMA, and MU must be numeric."); endif ## Check for K, SIGMA, and MU being real if (iscomplex (k) || iscomplex (sigma) || iscomplex (theta)) error ("gpstat: K, SIGMA, and MU must not be complex."); endif ## Check for common size of K, SIGMA, and MU if (! isscalar (k) || ! isscalar (sigma) || ! isscalar (theta)) [retval, k, sigma, theta] = common_size (k, sigma, theta); if (retval > 0) error ("gpstat: K, SIGMA, and MU must be of common size or scalars."); endif endif ## Return NaNs for out of range SCALE parameters. sigma(sigma <= 0) = NaN; ## Check for appropriate class if (isa (k, 'single') || isa (sigma, 'single') || isa (theta, 'single')); is_class = 'single'; else is_class = 'double'; endif ## Prepare output m = NaN (size (k), is_class); v = NaN (size (k), is_class); ## Compute cases for SHAPE == 0 knot0 = (abs (k) < eps (is_class)); m(knot0) = 1; v(knot0) = 1; ## Compute cases for SHAPE != 0 knot0 = ! knot0; ## SHAPE < 1 kless = knot0 & (k < 1); m(kless) = 1 ./ (1 - k(kless)); ## SHAPE > 1 m(k >= 1) = Inf; ## SHAPE < 1/2 ## Find the k~=0 cases and fill in the variance. kless = knot0 & (k < 1/2); v(kless) = 1 ./ ((1-k(kless)).^2 .* (1-2.*k(kless))); ## SHAPE > 1/2 v(k >= 1/2) = Inf; ## Compute mean and variance m = theta + sigma .* m; v = sigma .^ 2 .* v; endfunction ## Input validation tests %!error gpstat () %!error gpstat (1) %!error gpstat (1, 2) %!error gpstat ({}, 2, 3) %!error gpstat (1, '', 3) %!error gpstat (1, 2, '') %!error gpstat (i, 2, 3) %!error gpstat (1, i, 3) %!error gpstat (1, 2, i) %!error ... %! gpstat (ones (3), ones (2), 3) %!error ... %! gpstat (ones (2), 2, ones (3)) %!error ... %! gpstat (1, ones (2), ones (3)) ## Output validation tests %!shared x, y %! x = [-Inf, -1, 0, 1/2, 1, Inf]; %! y = [0, 0.5, 1, 2, Inf, Inf]; %!assert_equal (gpstat (x, ones (1,6), zeros (1,6)), y, eps) ## Test class of input preserved %!assert_equal (gpstat (single (x), 1, 0), single (y), eps ('single')) %!assert_equal (gpstat (x, single (1), 0), single (y), eps ('single')) %!assert_equal (gpstat (x, 1, single (0)), single (y), eps ('single')) %!assert_equal (gpstat (single ([x, NaN]), 1, 0), single ([y, NaN]), eps ('single')) %!assert_equal (gpstat ([x, NaN], single (1), 0), single ([y, NaN]), eps ('single')) %!assert_equal (gpstat ([x, NaN], 1, single (0)), single ([y, NaN]), eps ('single')) statistics-release-2.1.2/inst/Distribution_Statistics/gumbelstat.m000066400000000000000000000112501527311104300255130ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} gumbelstat (@var{mu}, @var{beta}) ## ## Compute statistics of the Gumbel distribution. ## ## @code{[@var{m}, @var{v}] = gumbelstat (@var{mu}, @var{beta})} returns the ## mean and variance of the Gumbel distribution (also known as the extreme value ## or the type I generalized extreme value distribution) with location ## parameter @var{mu} and scale parameter @var{beta}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## The mean is @code{@var{mu} + @var{gamma} * @var{beta}}, where @var{gamma} ## is the Euler-Mascheroni constant, and the variance is ## @code{pi^2 * @var{beta}^2 / 6}. A non-positive @var{beta} returns ## @code{NaN} for both. ## ## The Gumbel distribution is used to model the distribution of the maximum (or ## the minimum) of a number of samples of various distributions. This version ## is suitable for modeling maxima. For modeling minima, use the alternative ## extreme value statistics, @code{evstat}. ## ## Further information about the Gumbel distribution can be found at ## @url{https://en.wikipedia.org/wiki/Gumbel_distribution} ## ## Input arguments must be @qcode{double} or @qcode{single}; integer, logical, ## and character arrays are rejected. ## ## @seealso{gumbelcdf, gumbelinv, gumbelpdf, gumbelrnd, gumbelfit, gumbellike, ## evstat} ## @end deftypefn function [m, v] = gumbelstat (mu, beta) ## Check for valid number of input arguments if (nargin < 2) error ("gumbelstat: too few input arguments."); endif ## Check for common size of MU and BETA if (! isscalar (mu) || ! isscalar (beta)) [err, mu, beta] = common_size (mu, beta); if (err > 0) error ("gumbelstat: MU and BETA must be of common size or scalars."); endif endif ## Check for MU and BETA being double or single if (! (isfloat (mu) && isfloat (beta))) error ("gumbelstat: MU and BETA must be double or single."); endif ## Check for MU and BETA being reals if (iscomplex (mu) || iscomplex (beta)) error ("gumbelstat: MU and BETA must not be complex."); endif ## Return NaNs for out of range parameters beta(beta <= 0) = NaN; ## Calculate mean and variance; psi (1) is minus the Euler-Mascheroni ## constant m = mu - psi (1) .* beta; v = (pi .* beta) .^ 2 ./ 6; endfunction ## Test output %!test %! [m, v] = gumbelstat (0, 1); %! assert_equal (m, 0.577215664901533, 1e-15); %! assert_equal (v, pi ^ 2 / 6, eps); %!test %! [m, v] = gumbelstat ([-5, 0, 1, 2, 3], [0, 1, 2, -1, 3]); %! assert_equal (m, [NaN, 0.577215664901533, 2.154431329803066, NaN, ... %! 4.731646994704599], 1e-14); %! assert_equal (v, [NaN, pi^2/6, 2*pi^2/3, NaN, 3*pi^2/2], 1e-14); %!test %! [m, v] = gumbelstat (2, 3); %! f = @(x) gumbelpdf (x, 2, 3); %! assert_equal (m, integral (@(x) x .* f(x), -Inf, Inf), 1e-8); %! assert_equal (v, integral (@(x) (x - m) .^ 2 .* f(x), -Inf, Inf), 1e-8); %!test %! [m, v] = gumbelstat (1, 2); %! [me, ve] = evstat (-1, 2); %! assert_equal (m, -me, 1e-14); %! assert_equal (v, ve, 1e-14); %!test %! [m, v] = gumbelstat (0, single (1)); %! assert_equal (class (m), 'single'); %! assert_equal (class (v), 'single'); ## Test input validation %!error gumbelstat () %!error gumbelstat (1) %!error ... %! gumbelstat (ones (3), ones (2)) %!error ... %! gumbelstat (int32 (1), 2) %!error ... %! gumbelstat (1, true) %!error ... %! gumbelstat ('a', 2) %!error gumbelstat (i, 2) %!error gumbelstat (1, i) statistics-release-2.1.2/inst/Distribution_Statistics/hnstat.m000066400000000000000000000073701527311104300246550ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} hnstat (@var{mu}, @var{sigma}) ## ## Compute statistics of the half-normal distribution. ## ## @code{[@var{m}, @var{v}] = hnstat (@var{mu}, @var{sigma})} returns the mean ## and variance of the half-normal distribution with non-centrality (distance) ## parameter @var{mu} and scale parameter @var{sigma}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## Further information about the half-normal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Half-normal_distribution} ## ## @seealso{hncdf, hninv, hnpdf, hnrnd, hnfit, hnlike} ## @end deftypefn function [m, v] = hnstat (mu, sigma) ## Check for valid number of input arguments if (nargin < 2) error ("hnstat: function called with too few input arguments."); endif ## Check for MU and SIGMA being numeric if (! (isnumeric (mu) && isnumeric (sigma))) error ("hnstat: MU and SIGMA must be numeric."); endif ## Check for MU and SIGMA being real if (iscomplex (mu) || iscomplex (sigma)) error ("hnstat: MU and SIGMA must not be complex."); endif ## Check for common size of MU and SIGMA if (! isscalar (mu) || ! isscalar (sigma)) [retval, mu, sigma] = common_size (mu, sigma); if (retval > 0) error ("hnstat: MU and SIGMA must be of common size or scalars."); endif endif ## Calculate moments m = mu + (sigma .* sqrt (2)) ./ sqrt (pi); v = sigma .^ 2 .* (1 - 2 / pi); ## Continue argument check k = find (! (sigma > 0) | ! (sigma < Inf)); if (any (k)) m(k) = NaN; v(k) = NaN; endif endfunction ## Input validation tests %!error hnstat () %!error hnstat (1) %!error hnstat ({}, 2) %!error hnstat (1, '') %!error hnstat (i, 2) %!error hnstat (1, i) %!error ... %! hnstat (ones (3), ones (2)) %!error ... %! hnstat (ones (2), ones (3)) ## Output validation tests %!test %! [m, v] = hnstat (0, 1); %! assert_equal (m, 0.7979, 1e-4); %! assert_equal (v, 0.3634, 1e-4); %!test %! [m, v] = hnstat (2, 1); %! assert_equal (m, 2.7979, 1e-4); %! assert_equal (v, 0.3634, 1e-4); %!test %! [m, v] = hnstat (2, 2); %! assert_equal (m, 3.5958, 1e-4); %! assert_equal (v, 1.4535, 1e-4); %!test %! [m, v] = hnstat (2, 2.5); %! assert_equal (m, 3.9947, 1e-4); %! assert_equal (v, 2.2711, 1e-4); %!test %! [m, v] = hnstat (1.5, 0.5); %! assert_equal (m, 1.8989, 1e-4); %! assert_equal (v, 0.0908, 1e-4); %!test %! [m, v] = hnstat (-1.5, 0.5); %! assert_equal (m, -1.1011, 1e-4); %! assert_equal (v, 0.0908, 1e-4); statistics-release-2.1.2/inst/Distribution_Statistics/hygestat.m000066400000000000000000000132611527311104300252000ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{mn}, @var{v}] =} hygestat (@var{m}, @var{k}, @var{n}) ## ## Compute statistics of the hypergeometric distribution. ## ## @code{[@var{mn}, @var{v}] = hygestat (@var{m}, @var{k}, @var{n})} returns the ## mean and variance of the hypergeometric distribution parameters @var{m}, ## @var{k}, and @var{n}. ## ## @itemize ## @item ## @var{m} is the total size of the population of the hypergeometric ## distribution. The elements of @var{m} must be non-negative integers. ## ## @item ## @var{k} is the number of marked items of the hypergeometric distribution. ## The elements of @var{k} must be natural numbers. ## ## @item ## @var{n} is the size of the drawn sample of the hypergeometric ## distribution. The elements of @var{n} must be non-negative integers. ## @end itemize ## ## A sample of size @qcode{0} contains no marked items, so its mean and variance ## are @qcode{0}, even for an empty population, where MATLAB returns ## @qcode{NaN} for both. A sample from a population of one always holds the ## same number of marked items, so its variance is @qcode{0}, where MATLAB ## returns @qcode{NaN}. ## ## The size of @var{mn} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## Further information about the hypergeometric distribution can be found at ## @url{https://en.wikipedia.org/wiki/Hypergeometric_distribution} ## ## @seealso{hygecdf, hygeinv, hygepdf, hygernd} ## @end deftypefn function [mn, v] = hygestat (m, k, n) ## Check for valid number of input arguments if (nargin < 3) error ("hygestat: function called with too few input arguments."); endif ## Check for M, K, and N being numeric if (! (isnumeric (m) && isnumeric (k) && isnumeric (n))) error ("hygestat: M, K, and N must be numeric."); endif ## Check for M, K, and N being real if (iscomplex (m) || iscomplex (k) || iscomplex (n)) error ("hygestat: M, K, and N must not be complex."); endif ## Check for common size of M, K, and N if (! isscalar (m) || ! isscalar (k) || ! isscalar (n)) [retval, m, k, n] = common_size (m, k, n); if (retval > 0) error ("hygestat: M, K, and N must be of common size or scalars."); endif endif ## Calculate moments mn = (n .* k) ./ m; v = (n .* (k ./ m) .* (1 - k ./ m) .* (m - n)) ./ (m - 1); ## Continue argument check ## An empty sample holds no marked items, and a sample from a population of ## one a fixed number of them; the formulas above divide zero by zero there mn(n == 0) = 0; v(n == 0 | m == 1) = 0; is_nan = find (! (m >= 0) | ! (k >= 0) | ! (n >= 0) | ! (m == round (m)) | ... ! (k == round (k)) | ! (n == round (n)) | ! (k <= m) | ... ! (n <= m)); if (any (is_nan)) mn(is_nan) = NaN; v(is_nan) = NaN; endif endfunction ## Input validation tests %!error hygestat () %!error hygestat (1) %!error hygestat (1, 2) %!error hygestat ({}, 2, 3) %!error hygestat (1, '', 3) %!error hygestat (1, 2, '') %!error hygestat (i, 2, 3) %!error hygestat (1, i, 3) %!error hygestat (1, 2, i) %!error ... %! hygestat (ones (3), ones (2), 3) %!error ... %! hygestat (ones (2), 2, ones (3)) %!error ... %! hygestat (1, ones (2), ones (3)) ## Output validation tests %!test %! m = 4:9; %! k = 0:5; %! n = 1:6; %! [mn, v] = hygestat (m, k, n); %! expected_mn = [0.0000, 0.4000, 1.0000, 1.7143, 2.5000, 3.3333]; %! expected_v = [0.0000, 0.2400, 0.4000, 0.4898, 0.5357, 0.5556]; %! assert_equal (mn, expected_mn, 0.001); %! assert_equal (v, expected_v, 0.001); %!test %! [mn, v] = hygestat (10, 5, 0); %! assert_equal ([mn, v], [0, 0]); %!test %! [mn, v] = hygestat (0, 0, 0); %! assert_equal ([mn, v], [0, 0]); %!test %! [mn, v] = hygestat (1, 1, 1); %! assert_equal ([mn, v], [1, 0]); %!test %! [mn, v] = hygestat (1, 0, 1); %! assert_equal ([mn, v], [0, 0]); %!test %! [mn, v] = hygestat ([0, 10, 10], [0, 5, 5], [0, 0, -1]); %! assert_equal (mn, [0, 0, NaN]); %! assert_equal (v, [0, 0, NaN]); %!test %! m = 4:9; %! k = 0:5; %! [mn, v] = hygestat (m, k, 2); %! expected_mn = [0.0000, 0.4000, 0.6667, 0.8571, 1.0000, 1.1111]; %! expected_v = [0.0000, 0.2400, 0.3556, 0.4082, 0.4286, 0.4321]; %! assert_equal (mn, expected_mn, 0.001); %! assert_equal (v, expected_v, 0.001); statistics-release-2.1.2/inst/Distribution_Statistics/invgstat.m000066400000000000000000000070601527311104300252070ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} invgstat (@var{mu}, @var{lambda}) ## ## Compute statistics of the inverse Gaussian distribution. ## ## @code{[@var{m}, @var{v}] = invgstat (@var{mu}, @var{lambda})} returns the ## mean and variance of the inverse Gaussian distribution with mean parameter ## @var{mu} and shape parameter @var{lambda}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## Further information about the inverse Gaussian distribution can be found at ## @url{https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution} ## ## @seealso{invgcdf, invginv, invgpdf, invgrnd, invgfit, invglike} ## @end deftypefn function [m, v] = invgstat (mu, lambda) ## Check for valid number of input arguments if (nargin < 2) error ("invgstat: function called with too few input arguments."); endif ## Check for MU and LAMBDA being numeric if (! (isnumeric (mu) && isnumeric (lambda))) error ("invgstat: MU and LAMBDA must be numeric."); endif ## Check for MU and LAMBDA being real if (iscomplex (mu) || iscomplex (lambda)) error ("invgstat: MU and LAMBDA must not be complex."); endif ## Check for common size of MU and LAMBDA if (! isscalar (mu) || ! isscalar (lambda)) [retval, mu, lambda] = common_size (mu, lambda); if (retval > 0) error ("invgstat: MU and LAMBDA must be of common size or scalars."); endif endif ## Calculate moments m = mu; v = (mu .^ 3) ./ lambda; ## Continue argument check m(lambda <= 0 | mu <= 0) = NaN; v(lambda <= 0 | mu <= 0) = NaN; endfunction ## Input validation tests %!error invgstat () %!error invgstat (1) %!error invgstat ({}, 2) %!error invgstat (1, '') %!error invgstat (i, 2) %!error invgstat (1, i) %!error ... %! invgstat (ones (3), ones (2)) %!error ... %! invgstat (ones (2), ones (3)) ## Output validation tests %!test %! [m, v] = invgstat (1, 1); %! assert_equal (m, 1); %! assert_equal (v, 1); %!test %! [m, v] = invgstat (2, 1); %! assert_equal (m, 2); %! assert_equal (v, 8); %!test %! [m, v] = invgstat (2, 2); %! assert_equal (m, 2); %! assert_equal (v, 4); %!test %! [m, v] = invgstat (2, 2.5); %! assert_equal (m, 2); %! assert_equal (v, 3.2); %!test %! [m, v] = invgstat (1.5, 0.5); %! assert_equal (m, 1.5); %! assert_equal (v, 6.75); statistics-release-2.1.2/inst/Distribution_Statistics/logistat.m000066400000000000000000000070471527311104300252030ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} logistat (@var{mu}, @var{sigma}) ## ## Compute statistics of the logistic distribution. ## ## @code{[@var{m}, @var{v}] = logistat (@var{mu}, @var{sigma})} returns the mean ## and variance of the logistic distribution with mean parameter @var{mu} and ## scale parameter @var{sigma}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## Further information about the logistic distribution can be found at ## @url{https://en.wikipedia.org/wiki/Logistic_distribution} ## ## @seealso{logicdf, logiinv, logipdf, logirnd, logifit, logilike} ## @end deftypefn function [m, v] = logistat (mu, sigma) ## Check for valid number of input arguments if (nargin < 2) error ("logistat: function called with too few input arguments."); endif ## Check for MU and SIGMA being numeric if (! (isnumeric (mu) && isnumeric (sigma))) error ("logistat: MU and SIGMA must be numeric."); endif ## Check for MU and SIGMA being real if (iscomplex (mu) || iscomplex (sigma)) error ("logistat: MU and SIGMA must not be complex."); endif ## Check for common size of MU and SIGMA if (! isscalar (mu) || ! isscalar (sigma)) [retval, mu, sigma] = common_size (mu, sigma); if (retval > 0) error ("logistat: MU and SIGMA must be of common size or scalars."); endif endif ## Calculate moments m = mu; v = (sigma .^ 2 .* pi .^ 2) ./ 3; ## Continue argument check m(sigma <= 0) = NaN; v(sigma <= 0) = NaN; endfunction ## Input validation tests %!error logistat () %!error logistat (1) %!error logistat ({}, 2) %!error logistat (1, '') %!error logistat (i, 2) %!error logistat (1, i) %!error ... %! logistat (ones (3), ones (2)) %!error ... %! logistat (ones (2), ones (3)) ## Output validation tests %!test %! [m, v] = logistat (0, 1); %! assert_equal (m, 0); %! assert_equal (v, 3.2899, 0.001); %!test %! [m, v] = logistat (0, 0.8); %! assert_equal (m, 0); %! assert_equal (v, 2.1055, 0.001); %!test %! [m, v] = logistat (1, 0.6); %! assert_equal (m, 1); %! assert_equal (v, 1.1844, 0.001); %!test %! [m, v] = logistat (0, 0.4); %! assert_equal (m, 0); %! assert_equal (v, 0.5264, 0.001); %!test %! [m, v] = logistat (-1, 0.2); %! assert_equal (m, -1); %! assert_equal (v, 0.1316, 0.001); statistics-release-2.1.2/inst/Distribution_Statistics/loglstat.m000066400000000000000000000101671527311104300252030ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} loglstat (@var{mu}, @var{sigma}) ## ## Compute statistics of the loglogistic distribution. ## ## @code{[@var{m}, @var{v}] = loglstat (@var{mu}, @var{sigma})} returns the mean ## and variance of the loglogistic distribution with mean parameter @var{mu} and ## scale parameter @var{sigma}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## Further information about the loglogistic distribution can be found at ## @url{https://en.wikipedia.org/wiki/Log-logistic_distribution} ## ## OCTAVE/MATLAB use an alternative parameterization given by the pair ## @math{μ, σ}, i.e. @var{mu} and @var{sigma}, in analogy with the logistic ## distribution. Their relation to the @math{α} and @math{b} parameters used ## in Wikipedia are given below: ## ## @itemize ## @item @qcode{@var{mu} = log (@var{a})} ## @item @qcode{@var{sigma} = 1 / @var{a}} ## @end itemize ## ## @seealso{logncdf, logninv, lognpdf, lognrnd, lognfit, lognlike} ## @end deftypefn function [m, v] = loglstat (mu, sigma) ## Check for valid number of input arguments if (nargin < 2) error ("loglstat: function called with too few input arguments."); endif ## Check for MU and SIGMA being numeric if (! (isnumeric (mu) && isnumeric (sigma))) error ("loglstat: MU and SIGMA must be numeric."); endif ## Check for MU and SIGMA being real if (iscomplex (mu) || iscomplex (sigma)) error ("loglstat: MU and SIGMA must not be complex."); endif ## Check for common size of MU and SIGMA if (! isscalar (mu) || ! isscalar (sigma)) [retval, mu, sigma] = common_size (mu, sigma); if (retval > 0) error ("loglstat: MU and SIGMA must be of common size or scalars."); endif endif ## Calculate moments pib = pi .* sigma; m = (exp (mu) .* pib) ./ sin (pib); pib2 = 2 * pib; v = exp (mu) .^ 2 .* (pib2 ./ sin (pib2) - pib .^ 2 ./ sin (pib) .^ 2); ## Continue argument check m(sigma >= 1) = Inf; v(sigma >= 0.5) = Inf; m(sigma <= 0) = NaN; v(sigma <= 0) = NaN; endfunction ## Input validation tests %!error loglstat () %!error loglstat (1) %!error loglstat ({}, 2) %!error loglstat (1, '') %!error loglstat (i, 2) %!error loglstat (1, i) %!error ... %! loglstat (ones (3), ones (2)) %!error ... %! loglstat (ones (2), ones (3)) ## Output validation tests %!test %! [m, v] = loglstat (0, 1); %! assert_equal (m, Inf, 0.001); %! assert_equal (v, Inf, 0.001); %!test %! [m, v] = loglstat (0, 0.8); %! assert_equal (m, 4.2758, 0.001); %! assert_equal (v, Inf, 0.001); %!test %! [m, v] = loglstat (0, 0.6); %! assert_equal (m, 1.9820, 0.001); %! assert_equal (v, Inf, 0.001); %!test %! [m, v] = loglstat (0, 0.4); %! assert_equal (m, 1.3213, 0.001); %! assert_equal (v, 2.5300, 0.001); %!test %! [m, v] = loglstat (0, 0.2); %! assert_equal (m, 1.0690, 0.001); %! assert_equal (v, 0.1786, 0.001); statistics-release-2.1.2/inst/Distribution_Statistics/lognstat.m000066400000000000000000000075631527311104300252130ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} lognstat (@var{mu}, @var{sigma}) ## ## Compute statistics of the lognormal distribution. ## ## @code{[@var{m}, @var{v}] = lognstat (@var{mu}, @var{sigma})} returns the mean ## and variance of the lognormal distribution with mean parameter @var{mu} and ## standard deviation parameter @var{sigma}, each corresponding to the ## associated normal distribution. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## Further information about the lognormal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Log-normal_distribution} ## ## @seealso{logncdf, logninv, lognpdf, lognrnd, lognfit, lognlike} ## @end deftypefn function [m, v] = lognstat (mu, sigma) ## Check for valid number of input arguments if (nargin < 2) error ("lognstat: function called with too few input arguments."); endif ## Check for MU and SIGMA being numeric if (! (isnumeric (mu) && isnumeric (sigma))) error ("lognstat: MU and SIGMA must be numeric."); endif ## Check for MU and SIGMA being real if (iscomplex (mu) || iscomplex (sigma)) error ("lognstat: MU and SIGMA must not be complex."); endif ## Check for common size of MU and SIGMA if (! isscalar (mu) || ! isscalar (sigma)) [retval, mu, sigma] = common_size (mu, sigma); if (retval > 0) error ("lognstat: MU and SIGMA must be of common size or scalars."); endif endif ## Calculate moments m = exp (mu + (sigma .^ 2) ./ 2); v = (exp (sigma .^ 2) - 1) .* exp (2 .* mu + sigma .^ 2); ## Continue argument check k = find (! (sigma >= 0) | ! (sigma < Inf)); if (any (k)) m(k) = NaN; v(k) = NaN; endif endfunction ## Input validation tests %!error lognstat () %!error lognstat (1) %!error lognstat ({}, 2) %!error lognstat (1, '') %!error lognstat (i, 2) %!error lognstat (1, i) %!error ... %! lognstat (ones (3), ones (2)) %!error ... %! lognstat (ones (2), ones (3)) ## Output validation tests %!test %! mu = 0:0.2:1; %! sigma = 0.2:0.2:1.2; %! [m, v] = lognstat (mu, sigma); %! expected_m = [1.0202, 1.3231, 1.7860, 2.5093, 3.6693, 5.5845]; %! expected_v = [0.0425, 0.3038, 1.3823, 5.6447, 23.1345, 100.4437]; %! assert_equal (m, expected_m, 0.001); %! assert_equal (v, expected_v, 0.001); %!test %! sigma = 0.2:0.2:1.2; %! [m, v] = lognstat (0, sigma); %! expected_m = [1.0202, 1.0833, 1.1972, 1.3771, 1.6487, 2.0544]; %! expected_v = [0.0425, 0.2036, 0.6211, 1.7002, 4.6708, 13.5936]; %! assert_equal (m, expected_m, 0.001); %! assert_equal (v, expected_v, 0.001); statistics-release-2.1.2/inst/Distribution_Statistics/nakastat.m000066400000000000000000000070021527311104300251520ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} nakastat (@var{mu}, @var{omega}) ## ## Compute statistics of the Nakagami distribution. ## ## @code{[@var{m}, @var{v}] = nakastat (@var{mu}, @var{omega})} returns the mean ## and variance of the Nakagami distribution with shape parameter @var{mu} and ## spread parameter @var{omega}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## Further information about the Nakagami distribution can be found at ## @url{https://en.wikipedia.org/wiki/Normal_distribution} ## ## @seealso{nakacdf, nakainv, nakapdf, nakarnd, nakafit, nakalike} ## @end deftypefn function [m, v] = nakastat (mu, omega) ## Check for valid number of input arguments if (nargin < 2) error ("nakastat: function called with too few input arguments."); endif ## Check for MU and OMEGA being numeric if (! (isnumeric (mu) && isnumeric (omega))) error ("nakastat: MU and OMEGA must be numeric."); endif ## Check for MU and OMEGA being real if (iscomplex (mu) || iscomplex (omega)) error ("nakastat: MU and OMEGA must not be complex."); endif ## Check for common size of MU and OMEGA if (! isscalar (mu) || ! isscalar (omega)) [retval, mu, omega] = common_size (mu, omega); if (retval > 0) error ("nakastat: MU and OMEGA must be of common size or scalars."); endif endif ## Calculate moments g = gamma (mu + 0.5) ./ gamma (mu); m = g .* sqrt (omega ./ mu); v = omega .* (1 - ((1 ./ mu) .* (g .^ 2))); ## Continue argument check knan = mu < 0.5 | omega <= 0; m(knan) = NaN; v(knan) = NaN; endfunction ## Input validation tests %!error nakastat () %!error nakastat (1) %!error nakastat ({}, 2) %!error nakastat (1, '') %!error nakastat (i, 2) %!error nakastat (1, i) %!error ... %! nakastat (ones (3), ones (2)) %!error ... %! nakastat (ones (2), ones (3)) ## Output validation tests %!test %! [m, v] = nakastat (1, 1); %! assert_equal (m, 0.8862269254, 1e-10); %! assert_equal (v, 0.2146018366, 1e-10); %!test %! [m, v] = nakastat (1, 2); %! assert_equal (m, 1.25331413731, 1e-10); %! assert_equal (v, 0.42920367321, 1e-10); %!test %! [m, v] = nakastat (2, 1); %! assert_equal (m, 0.93998560299, 1e-10); %! assert_equal (v, 0.11642706618, 1e-10); statistics-release-2.1.2/inst/Distribution_Statistics/nbinstat.m000066400000000000000000000073751527311104300252030ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} nbinstat (@var{r}, @var{ps}) ## ## Compute statistics of the negative binomial distribution. ## ## @code{[@var{m}, @var{v}] = nbinstat (@var{r}, @var{ps})} returns the mean ## and variance of the negative binomial distribution with parameters @var{r} ## and @var{ps}, where @var{r} is the number of successes until the experiment ## is stopped and @var{ps} is the probability of success in each experiment, ## given the number of failures in @var{x}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## Further information about the negative binomial distribution can be found at ## @url{https://en.wikipedia.org/wiki/Negative_binomial_distribution} ## ## @seealso{nbincdf, nbininv, nbinpdf, nbinrnd, nbinfit, nbinlike} ## @end deftypefn function [m, v] = nbinstat (r, ps) ## Check for valid number of input arguments if (nargin < 2) error ("nbinstat: function called with too few input arguments."); endif ## Check for R and PS being numeric if (! (isnumeric (r) && isnumeric (ps))) error ("nbinstat: R and PS must be numeric."); endif ## Check for R and PS being real if (iscomplex (r) || iscomplex (ps)) error ("nbinstat: R and PS must not be complex."); endif ## Check for common size of R and PS if (! isscalar (r) || ! isscalar (ps)) [retval, r, ps] = common_size (r, ps); if (retval > 0) error ("nbinstat: R and PS must be of common size or scalars."); endif endif ## Calculate moments q = 1 - ps; m = r .* q ./ ps; v = r .* q ./ (ps .^ 2); ## Continue argument check k = find (! (r > 0) | ! (r < Inf) | ! (ps > 0) | ! (ps < 1)); if (any (k)) m(k) = NaN; v(k) = NaN; endif endfunction ## Input validation tests %!error nbinstat () %!error nbinstat (1) %!error nbinstat ({}, 2) %!error nbinstat (1, '') %!error nbinstat (i, 2) %!error nbinstat (1, i) %!error ... %! nbinstat (ones (3), ones (2)) %!error ... %! nbinstat (ones (2), ones (3)) ## Output validation tests %!test %! r = 1:4; %! ps = 0.2:0.2:0.8; %! [m, v] = nbinstat (r, ps); %! expected_m = [ 4.0000, 3.0000, 2.0000, 1.0000]; %! expected_v = [20.0000, 7.5000, 3.3333, 1.2500]; %! assert_equal (m, expected_m, 0.001); %! assert_equal (v, expected_v, 0.001); %!test %! r = 1:4; %! [m, v] = nbinstat (r, 0.5); %! expected_m = [1, 2, 3, 4]; %! expected_v = [2, 4, 6, 8]; %! assert_equal (m, expected_m, 0.001); %! assert_equal (v, expected_v, 0.001); statistics-release-2.1.2/inst/Distribution_Statistics/ncfstat.m000066400000000000000000000120161527311104300250070ustar00rootroot00000000000000## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} ncfstat (@var{df1}, @var{df1}, @var{lambda}) ## ## Compute statistics for the noncentral @math{F}-distribution. ## ## @code{[@var{m}, @var{v}] = ncfstat (@var{df1}, @var{df1}, @var{lambda})} ## returns the mean and variance of the noncentral @math{F}-distribution with ## @var{df1} and @var{df2} degrees of freedom and noncentrality parameter ## @var{lambda}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the same ## size as the other inputs. ## ## Further information about the noncentral @math{F}-distribution can be found ## at @url{https://en.wikipedia.org/wiki/Noncentral_F-distribution} ## ## @seealso{ncfcdf, ncfinv, ncfpdf, ncfrnd, fstat} ## @end deftypefn function [m, v] = ncfstat (df1, df2, lambda) ## Check for valid number of input arguments if (nargin < 3) error ("ncfstat: function called with too few input arguments."); endif ## Check for DF1, DF2, and LAMBDA being numeric if (! (isnumeric (df1) && isnumeric (df2) && isnumeric (lambda))) error ("ncfstat: DF1, DF2, and LAMBDA must be numeric."); endif ## Check for DF1, DF2, and LAMBDA being reals if (iscomplex (df1) || iscomplex (df2) || iscomplex (lambda)) error ("ncfstat: DF1, DF2, and LAMBDA must not be complex."); endif ## Check for common size of DF1, DF2, and LAMBDA if (! isscalar (df1) || ! isscalar (df2) || ! isscalar (lambda)) [retval, df1, df2, lambda] = common_size (df1, df2, lambda); if (retval > 0) error ("ncfstat: DF1, DF2, and LAMBDA must be of common size or scalars."); endif endif ## Initialize mean and variance if (isa (df1, 'single') || isa (df2, 'single') || isa (lambda, 'single')) m = zeros (size (df1), 'single'); v = m; else m = zeros (size (df1)); v = m; endif ## Return NaNs for invalid df2 parameters m(df2 <= 2) = NaN; v(df2 <= 4) = NaN; ## Compute mean and variance for valid parameter values. k = (df2 > 2); if (any (k(:))) m(k) = df2(k) .* (df1(k) + lambda(k)) ./ (df1(k) .* (df2(k) - 2)); endif k = (df2 > 4); if (any (k(:))) df1_idx = df1(k) + lambda(k); df2_idx = df2(k) - 2; df1_df2 = (df2(k) ./ df1(k)) .^ 2; v(k) = 2 * df1_df2 .* (df1_idx .^ 2 + (df1_idx + lambda(k)) .* ... df2_idx) ./ ((df2(k) - 4) .* df2_idx .^ 2); endif endfunction ## Input validation tests %!error ncfstat () %!error ncfstat (1) %!error ncfstat (1, 2) %!error ncfstat ({}, 2, 3) %!error ncfstat (1, '', 3) %!error ncfstat (1, 2, '') %!error ncfstat (i, 2, 3) %!error ncfstat (1, i, 3) %!error ncfstat (1, 2, i) %!error ... %! ncfstat (ones (3), ones (2), 3) %!error ... %! ncfstat (ones (2), 2, ones (3)) %!error ... %! ncfstat (1, ones (2), ones (3)) ## Output validation tests %!shared df1, df2, lambda %! df1 = [2, 0, -1, 1, 4, 5]; %! df2 = [2, 4, -1, 5, 6, 7]; %! lambda = [1, NaN, 3, 0, 2, -1]; %!assert_equal (ncfstat (df1, df2, lambda), [NaN, NaN, NaN, 1.6667, 2.25, 1.12], 1e-4); %!assert_equal (ncfstat (df1(4:6), df2(4:6), 1), [3.3333, 1.8750, 1.6800], 1e-4); %!assert_equal (ncfstat (df1(4:6), df2(4:6), 2), [5.0000, 2.2500, 1.9600], 1e-4); %!assert_equal (ncfstat (df1(4:6), df2(4:6), 3), [6.6667, 2.6250, 2.2400], 1e-4); %!assert_equal (ncfstat (2, [df2(1), df2(4:6)], 5), [NaN,5.8333,5.2500,4.9000], 1e-4); %!assert_equal (ncfstat (0, [df2(1), df2(4:6)], 5), [NaN, Inf, Inf, Inf]); %!assert_equal (ncfstat (1, [df2(1), df2(4:6)], 5), [NaN, 10, 9, 8.4], 1e-14); %!assert_equal (ncfstat (4, [df2(1), df2(4:6)], 5), [NaN, 3.75, 3.375, 3.15], 1e-14); statistics-release-2.1.2/inst/Distribution_Statistics/nctstat.m000066400000000000000000000102541527311104300250270ustar00rootroot00000000000000## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} nctstat (@var{df}, @var{mu}) ## ## Compute statistics for the noncentral @math{t}-distribution. ## ## @code{[@var{m}, @var{v}] = nctstat (@var{df}, @var{mu})} returns the mean ## and variance of the noncentral @math{t}-distribution with @var{df} degrees ## of freedom and noncentrality parameter @var{mu}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the same ## size as the other inputs. ## ## Further information about the noncentral @math{t}-distribution can be found ## at @url{https://en.wikipedia.org/wiki/Noncentral_t-distribution} ## ## @seealso{nctcdf, nctinv, nctpdf, nctrnd, tstat} ## @end deftypefn function [m, v] = nctstat (df, mu) ## Check for valid number of input arguments if (nargin < 2) error ("nctstat: function called with too few input arguments."); endif ## Check for DF and MU being numeric if (! (isnumeric (df) && isnumeric (mu))) error ("nctstat: DF and MU must be numeric."); endif ## Check for DF and MU being real if (iscomplex (df) || iscomplex (mu)) error ("nctstat: DF and MU must not be complex."); endif ## Check for common size of DF and MU if (! isscalar (df) || ! isscalar (mu)) [retval, df, mu] = common_size (df, mu); if (retval > 0) error ("nctstat: DF and MU must be of common size or scalars."); endif endif ## Initialize mean and variance if (isa (df, 'single') || isa (mu, 'single')) m = NaN (size (df), 'single'); v = m; else m = NaN (size (df)); v = m; endif ## Compute mean and variance for valid parameter values. mk = df > 1; if (any (mk(:))) m(mk) = mu(mk) .* sqrt ((df(mk) / 2)) .* ... gamma ((df(mk) - 1) / 2) ./ gamma (df(mk) / 2); endif vk = df > 2; if (any (vk(:))) v(vk) = (df(vk) ./ (df(vk) - 2)) .* ... (1 + mu(vk) .^2) - 0.5 * (df(vk) .* mu(vk) .^ 2) .* ... exp (2 * (gammaln ((df(vk) - 1) / 2) - gammaln (df(vk) / 2))); endif endfunction ## Input validation tests %!error nctstat () %!error nctstat (1) %!error nctstat ({}, 2) %!error nctstat (1, '') %!error nctstat (i, 2) %!error nctstat (1, i) %!error ... %! nctstat (ones (3), ones (2)) %!error ... %! nctstat (ones (2), ones (3)) ## Output validation tests %!shared df, mu %! df = [2, 0, -1, 1, 4]; %! mu = [1, NaN, 3, -1, 2]; %!assert_equal (nctstat (df, mu), [1.7725, NaN, NaN, NaN, 2.5066], 1e-4); %!assert_equal (nctstat ([df(1:2), df(4:5)], 1), [1.7725, NaN, NaN, 1.2533], 1e-4); %!assert_equal (nctstat ([df(1:2), df(4:5)], 3), [5.3174, NaN, NaN, 3.7599], 1e-4); %!assert_equal (nctstat ([df(1:2), df(4:5)], 2), [3.5449, NaN, NaN, 2.5066], 1e-4); %!assert_equal (nctstat (2, [mu(1), mu(3:5)]), [1.7725,5.3174,-1.7725,3.5449], 1e-4); %!assert_equal (nctstat (0, [mu(1), mu(3:5)]), [NaN, NaN, NaN, NaN]); %!assert_equal (nctstat (1, [mu(1), mu(3:5)]), [NaN, NaN, NaN, NaN]); %!assert_equal (nctstat (4, [mu(1), mu(3:5)]), [1.2533,3.7599,-1.2533,2.5066], 1e-4); statistics-release-2.1.2/inst/Distribution_Statistics/ncx2stat.m000066400000000000000000000077021527311104300251210ustar00rootroot00000000000000## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} ncx2stat (@var{df}, @var{lambda}) ## ## Compute statistics for the noncentral chi-squared distribution. ## ## @code{[@var{m}, @var{v}] = ncx2stat (@var{df}, @var{lambda})} returns the ## mean and variance of the noncentral chi-squared distribution with @var{df} ## degrees of freedom and noncentrality parameter @var{lambda}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## Further information about the noncentral chi-squared distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Noncentral_chi-squared_distribution} ## ## @seealso{ncx2cdf, ncx2inv, ncx2pdf, ncx2rnd} ## @end deftypefn function [m, v] = ncx2stat (df, lambda) ## Check for valid number of input arguments if (nargin < 2) error ("ncx2stat: function called with too few input arguments."); endif ## Check for DF and LAMBDA being numeric if (! (isnumeric (df) && isnumeric (lambda))) error ("ncx2stat: DF and LAMBDA must be numeric."); endif ## Check for DF and LAMBDA being real if (iscomplex (df) || iscomplex (lambda)) error ("ncx2stat: DF and LAMBDA must not be complex."); endif ## Check for common size of DF and LAMBDA if (! isscalar (df) || ! isscalar (lambda)) [retval, df, lambda] = common_size (df, lambda); if (retval > 0) error ("ncx2stat: DF and LAMBDA must be of common size or scalars."); endif endif ## Initialize mean and variance if (isa (df, 'single') || isa (lambda, 'single')) m = NaN (size (df), 'single'); v = m; else m = NaN (size (df)); v = m; endif ## Compute mean and variance for valid parameter values. k = (df > 0 & lambda >= 0); if (any (k(:))) m(k) = lambda(k) + df(k); v(k) = 2 * (df(k) + 2 * (lambda(k))); endif endfunction ## Input validation tests %!error ncx2stat () %!error ncx2stat (1) %!error ncx2stat ({}, 2) %!error ncx2stat (1, '') %!error ncx2stat (i, 2) %!error ncx2stat (1, i) %!error ... %! ncx2stat (ones (3), ones (2)) %!error ... %! ncx2stat (ones (2), ones (3)) ## Output validation tests %!shared df, d1 %! df = [2, 0, -1, 1, 4]; %! d1 = [1, NaN, 3, -1, 2]; %!assert_equal (ncx2stat (df, d1), [3, NaN, NaN, NaN, 6]); %!assert_equal (ncx2stat ([df(1:2), df(4:5)], 1), [3, NaN, 2, 5]); %!assert_equal (ncx2stat ([df(1:2), df(4:5)], 3), [5, NaN, 4, 7]); %!assert_equal (ncx2stat ([df(1:2), df(4:5)], 2), [4, NaN, 3, 6]); %!assert_equal (ncx2stat (2, [d1(1), d1(3:5)]), [3, 5, NaN, 4]); %!assert_equal (ncx2stat (0, [d1(1), d1(3:5)]), [NaN, NaN, NaN, NaN]); %!assert_equal (ncx2stat (1, [d1(1), d1(3:5)]), [2, 4, NaN, 3]); %!assert_equal (ncx2stat (4, [d1(1), d1(3:5)]), [5, 7, NaN, 6]); statistics-release-2.1.2/inst/Distribution_Statistics/normstat.m000066400000000000000000000071671527311104300252270ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} normstat (@var{mu}, @var{sigma}) ## ## Compute statistics of the normal distribution. ## ## @code{[@var{m}, @var{v}] = normstat (@var{mu}, @var{sigma})} returns the mean ## and variance of the normal distribution with non-centrality (distance) ## parameter @var{mu} and scale parameter @var{sigma}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## Further information about the normal distribution can be found at ## @url{https://en.wikipedia.org/wiki/Normal_distribution} ## ## @seealso{normcdf, norminv, normpdf, normrnd, normfit, normlike} ## @end deftypefn function [m, v] = normstat (mu, sigma) ## Check for valid number of input arguments if (nargin < 2) error ("normstat: function called with too few input arguments."); endif ## Check for MU and SIGMA being numeric if (! (isnumeric (mu) && isnumeric (sigma))) error ("normstat: MU and SIGMA must be numeric."); endif ## Check for MU and SIGMA being real if (iscomplex (mu) || iscomplex (sigma)) error ("normstat: MU and SIGMA must not be complex."); endif ## Check for common size of MU and SIGMA if (! isscalar (mu) || ! isscalar (sigma)) [retval, mu, sigma] = common_size (mu, sigma); if (retval > 0) error ("normstat: MU and SIGMA must be of common size or scalars."); endif endif ## Calculate moments m = mu; v = sigma .* sigma; ## Continue argument check k = find (! (sigma > 0) | ! (sigma < Inf)); if (any (k)) m(k) = NaN; v(k) = NaN; endif endfunction ## Input validation tests %!error normstat () %!error normstat (1) %!error normstat ({}, 2) %!error normstat (1, '') %!error normstat (i, 2) %!error normstat (1, i) %!error ... %! normstat (ones (3), ones (2)) %!error ... %! normstat (ones (2), ones (3)) ## Output validation tests %!test %! mu = 1:6; %! sigma = 0.2:0.2:1.2; %! [m, v] = normstat (mu, sigma); %! expected_v = [0.0400, 0.1600, 0.3600, 0.6400, 1.0000, 1.4400]; %! assert_equal (m, mu); %! assert_equal (v, expected_v, 0.001); %!test %! sigma = 0.2:0.2:1.2; %! [m, v] = normstat (0, sigma); %! expected_mn = [0, 0, 0, 0, 0, 0]; %! expected_v = [0.0400, 0.1600, 0.3600, 0.6400, 1.0000, 1.4400]; %! assert_equal (m, expected_mn, 0.001); %! assert_equal (v, expected_v, 0.001); statistics-release-2.1.2/inst/Distribution_Statistics/plstat.m000066400000000000000000000066031527311104300246610ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} plstat (@var{x}, @var{Fx}) ## ## Compute statistics of the piecewise linear distribution. ## ## @code{[@var{m}, @var{v}] = plstat (@var{x}, @var{Fx})} returns the mean, ## @var{m}, and variance, @var{v}, of the piecewise linear distribution with a ## vector of @var{x} values at which the CDF changes slope and a vector of CDF ## values @var{Fx} that correspond to each value in @var{x}. Both @var{x} and ## @var{Fx} must be vectors of the same size and at least 2-elements long. ## ## Further information about the piecewise linear distribution can be found at ## @url{https://en.wikipedia.org/wiki/Piecewise_linear_function} ## ## @seealso{plcdf, plinv, plpdf, plrnd} ## @end deftypefn function [m, v] = plstat (x, Fx) ## Check for valid number of input arguments if (nargin < 2) error ("plstat: function called with too few input arguments."); endif ## Check for common size of X and FX if (! isvector (x) || ! isvector (Fx) || ! isequal (size (x), size (Fx))) error ("plstat: X and FX must be vectors of equal size."); endif ## Check for X and FX being at least 2-elements long if (length (x) < 2 || length (Fx) < 2) error ("plstat: X and FX must be at least two-elements long."); endif ## Check for Fx being bounded in [0, 1] if (any (Fx < 0) || any (Fx > 1)) error ("plstat: FX must be bounded in the range [0, 1]."); endif ## Check for X and FX being reals if (iscomplex (x) || iscomplex (Fx)) error ("plstat: X and FX must not be complex."); endif ## Compute the mean and variance x_m = (x(1:end-1) + x(2:end)) / 2; dFx = diff (Fx); m = dot (dFx, x_m); x_v = diff (x) .^ 2 / 12; v = dot (dFx, x_v + (x_m - m) .^ 2); endfunction ## Test output %!shared x, Fx %! x = [0, 1, 3, 4, 7, 10]; %! Fx = [0, 0.2, 0.5, 0.6, 0.7, 1]; %!assert_equal (plstat (x, Fx), 4.15) %!test %! [m, v] = plstat (x, Fx); %! assert_equal (v, 10.3775, 1e-14) ## Test input validation %!error plstat () %!error plstat (1) %!error ... %! plstat ([0, 1, 2], [0, 1]) %!error ... %! plstat ([0], [1]) %!error ... %! plstat ([0, 1, 2], [0, 1, 1.5]) %!error ... %! plstat ([0, 1, 2], [0, i, 1]) %!error ... %! plstat ([0, i, 2], [0, 0.5, 1]) %!error ... %! plstat ([0, i, 2], [0, 0.5i, 1]) statistics-release-2.1.2/inst/Distribution_Statistics/poisstat.m000066400000000000000000000047151527311104300252220ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} poisstat (@var{lambda}) ## ## Compute statistics of the Poisson distribution. ## ## @code{[@var{m}, @var{v}] = poisstat (@var{lambda})} returns the mean and ## variance of the Poisson distribution with rate parameter @var{lambda}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the same size of the ## input argument. ## ## Further information about the Poisson distribution can be found at ## @url{https://en.wikipedia.org/wiki/Poisson_distribution} ## ## @seealso{poisscdf, poissinv, poisspdf, poissrnd, poissfit, poisslike} ## @end deftypefn function [m, v] = poisstat (lambda) ## Check for valid number of input arguments if (nargin < 1) error ("poisstat: function called with too few input arguments."); endif ## Check for LAMBDA being numeric if (! isnumeric (lambda)) error ("poisstat: LAMBDA must be numeric."); endif ## Check for LAMBDA being real if (iscomplex (lambda)) error ("poisstat: LAMBDA must not be complex."); endif ## Set moments m = lambda; v = lambda; ## Continue argument check k = find (! (lambda > 0) | ! (lambda < Inf)); if (any (k)) m(k) = NaN; v(k) = NaN; endif endfunction ## Input validation tests %!error poisstat () %!error poisstat ({}) %!error poisstat ('') %!error poisstat (i) ## Output validation tests %!test %! lambda = 1 ./ (1:6); %! [m, v] = poisstat (lambda); %! assert_equal (m, lambda); %! assert_equal (v, lambda); statistics-release-2.1.2/inst/Distribution_Statistics/raylstat.m000066400000000000000000000053161527311104300252150ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} raylstat (@var{sigma}) ## ## Compute statistics of the Rayleigh distribution. ## ## @code{[@var{m}, @var{v}] = raylstat (@var{sigma})} returns the mean and ## variance of the Rayleigh distribution with scale parameter @var{sigma}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the same size of the ## input argument. ## ## Further information about the Rayleigh distribution can be found at ## @url{https://en.wikipedia.org/wiki/Rayleigh_distribution} ## ## The @code{prob.RayleighDistribution} class names this same parameter ## @qcode{B}, after MATLAB. ## @seealso{raylcdf, raylinv, raylpdf, raylrnd, raylfit, rayllike} ## @end deftypefn function [m, v] = raylstat (sigma) ## Check for valid number of input arguments if (nargin < 1) error ("raylstat: function called with too few input arguments."); endif ## Check for SIGMA being numeric if (! isnumeric (sigma)) error ("raylstat: SIGMA must be numeric."); endif ## Check for SIGMA being real if (iscomplex (sigma)) error ("raylstat: SIGMA must not be complex."); endif ## Calculate moments m = sigma .* sqrt (pi ./ 2); v = (2 - pi ./ 2) .* sigma .^ 2; ## Continue argument check k = find (! (sigma > 0)); if (any (k)) m(k) = NaN; v(k) = NaN; endif endfunction ## Input validation tests %!error raylstat () %!error raylstat ({}) %!error raylstat ('') %!error raylstat (i) ## Output validation tests %!test %! sigma = 1:6; %! [m, v] = raylstat (sigma); %! expected_m = [1.2533, 2.5066, 3.7599, 5.0133, 6.2666, 7.5199]; %! expected_v = [0.4292, 1.7168, 3.8628, 6.8673, 10.7301, 15.4513]; %! assert_equal (m, expected_m, 0.001); %! assert_equal (v, expected_v, 0.001); statistics-release-2.1.2/inst/Distribution_Statistics/ricestat.m000066400000000000000000000104741527311104300251710ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} ricestat (@var{s}, @var{sigma}) ## ## Compute statistics of the Rician distribution. ## ## @code{[@var{m}, @var{v}] = ricestat (@var{s}, @var{sigma})} returns the mean ## and variance of the Rician distribution with non-centrality (distance) ## parameter @var{s} and scale parameter @var{sigma}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## Further information about the Rician distribution can be found at ## @url{https://en.wikipedia.org/wiki/Rice_distribution} ## ## @seealso{ricecdf, riceinv, ricepdf, ricernd, ricefit, ricelike} ## @end deftypefn function [m, v] = ricestat (s, sigma) ## Check for valid number of input arguments if (nargin < 2) error ("ricestat: function called with too few input arguments."); endif ## Check for S and SIGMA being numeric if (! (isnumeric (s) && isnumeric (sigma))) error ("ricestat: S and SIGMA must be numeric."); endif ## Check for S and SIGMA being real if (iscomplex (s) || iscomplex (sigma)) error ("ricestat: S and SIGMA must not be complex."); endif ## Check for common size of S and SIGMA if (! isscalar (s) || ! isscalar (sigma)) [retval, s, sigma] = common_size (s, sigma); if (retval > 0) error ("ricestat: S and SIGMA must be of common size or scalars."); endif endif ## Initialize mean and variance if (isa (s, 'single') || isa (sigma, 'single')) m = NaN (size (s), 'single'); v = m; else m = NaN (size (s)); v = m; endif ## Compute mean and variance for valid parameter values. k = (s >= 0 & sigma > 0); if (any (k(:))) thetasq = (s(k) .^ 2) ./ (sigma(k) .^ 2); L = Laguerre_half (-0.5 .* thetasq); m(k) = sigma(k) .* sqrt (pi / 2) .* L; v(k) = 2 * (sigma(k) .^ 2) + s(k) .^ 2 - ... (0.5 .* pi .* sigma(k) .^ 2) .* L .^ 2; endif endfunction function L = Laguerre_half (x) L = exp (x ./ 2) .* ((1 - x) .* besseli (0, -x./2) - x .* besseli (1, -x./2)); endfunction ## Input validation tests %!error ricestat () %!error ricestat (1) %!error ricestat ({}, 2) %!error ricestat (1, '') %!error ricestat (i, 2) %!error ricestat (1, i) %!error ... %! ricestat (ones (3), ones (2)) %!error ... %! ricestat (ones (2), ones (3)) ## Output validation tests %!shared s, sigma %! s = [2, 0, -1, 1, 4]; %! sigma = [1, NaN, 3, -1, 2]; %!assert_equal (ricestat (s, sigma), [2.2724, NaN, NaN, NaN, 4.5448], 1e-4); %!assert_equal (ricestat ([s(1:2), s(4:5)], 1), [2.2724, 1.2533, 1.5486, 4.1272], 1e-4); %!assert_equal (ricestat ([s(1:2), s(4:5)], 3), [4.1665, 3.7599, 3.8637, 5.2695], 1e-4); %!assert_equal (ricestat ([s(1:2), s(4:5)], 2), [3.0971, 2.5066, 2.6609, 4.5448], 1e-4); %!assert_equal (ricestat (2, [sigma(1), sigma(3:5)]), [2.2724, 4.1665, NaN, 3.0971], 1e-4); %!assert_equal (ricestat (0, [sigma(1), sigma(3:5)]), [1.2533, 3.7599, NaN, 2.5066], 1e-4); %!assert_equal (ricestat (1, [sigma(1), sigma(3:5)]), [1.5486, 3.8637, NaN, 2.6609], 1e-4); %!assert_equal (ricestat (4, [sigma(1), sigma(3:5)]), [4.1272, 5.2695, NaN, 4.5448], 1e-4); statistics-release-2.1.2/inst/Distribution_Statistics/stdrstat.m000066400000000000000000000126451527311104300252250ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} stdrstat (@var{k}, @var{df}) ## ## Compute statistics of the studentized range distribution. ## ## @code{[@var{m}, @var{v}] = stdrstat (@var{k}, @var{df})} returns the mean ## and variance of the studentized range distribution for @var{k} groups and ## @var{df} degrees of freedom. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the same ## size as the other input. @var{k} must be an integer of at least 2 and ## @var{df} positive, @code{Inf} included; otherwise both are @code{NaN}. The ## mean exists only for @var{df} greater than 1 and the variance only for ## @var{df} greater than 2; where either does not, it is @code{NaN}. For ## @var{df} equal to @code{Inf} they are the mean and variance of the range of ## @var{k} standard normal variables. ## ## MATLAB has no public counterpart of this function. ## ## Further information about the studentized range distribution can be found ## at @url{https://en.wikipedia.org/wiki/Studentized_range_distribution} ## ## @seealso{stdrcdf, stdrinv, stdrpdf, stdrrnd} ## @end deftypefn function [m, v] = stdrstat (k, df) ## Check for valid number of input arguments if (nargin < 2) error ("stdrstat: function called with too few input arguments."); endif ## Check for K and DF being numeric if (! (isnumeric (k) && isnumeric (df))) error ("stdrstat: K and DF must be numeric."); endif ## Check for K and DF being real if (iscomplex (k) || iscomplex (df)) error ("stdrstat: K and DF must not be complex."); endif ## Check for common size of K and DF if (! isscalar (k) || ! isscalar (df)) [retval, k, df] = common_size (k, df); if (retval > 0) error ("stdrstat: K and DF must be of common size or scalars."); endif endif sz = size (k); k = double (k(:)); df = double (df(:)) .* ones (numel (k), 1); m = NaN (numel (k), 1); v = NaN (numel (k), 1); valid = (k >= 2) & (k == fix (k)) & isfinite (k) & (df > 0); ## The moments of the range W of k standard normals, from its upper tail: ## E[W] = int U(w) dw and E[W^2] = 2 int w U(w) dw for kk = unique (k(valid & df > 1))' U = @(w) stdrcdf (w, kk, Inf, 'upper'); EW = integral (U, 0, 40, 'RelTol', 1e-13, 'AbsTol', 0); EW2 = 2 * integral (@(w) w .* U (w), 0, 40, 'RelTol', 1e-13, 'AbsTol', 0); ## E[1/s] with s^2 a chi-square over df, from its asymptotic series once ## the direct gamma ratio would lose digits i = valid & (k == kk) & (df > 1); n = df(i); Es = ones (size (n)); big = isfinite (n) & (n > 1e3); nb = n(big); Es(big) = 1 + 3 ./ (4 * nb) + 25 ./ (32 * nb .^ 2) + 105 ./ (128 * nb .^ 3); sml = (n <= 1e3); ns = n(sml); Es(sml) = sqrt (ns / 2) .* exp (gammaln ((ns - 1) / 2) - gammaln (ns / 2)); m(i) = EW * Es; ## E[1/s^2] = df / (df - 2) j = i & (df > 2); Es2 = df(j) ./ (df(j) - 2); Es2(isinf (df(j))) = 1; v(j) = EW2 * Es2 - m(j) .^ 2; endfor m = reshape (m, sz); v = reshape (v, sz); endfunction ## The range of two standard normals is sqrt (2) times a half-normal, and of ## three has mean 3 / sqrt (pi) %!test %! [m, v] = stdrstat (2, Inf); %! assert_equal (m, 2 / sqrt (pi), -1e-13); %! assert_equal (v, 2 - 4 / pi, -1e-12); %!assert_equal (stdrstat (3, Inf), 3 / sqrt (pi), -1e-13) ## Two groups on DF degrees of freedom are sqrt (2) times the absolute value ## of a t variable %!test %! df = 5; %! Et = 2 * sqrt (df) * exp (gammaln ((df + 1) / 2) - gammaln (df / 2)) ... %! / (sqrt (pi) * (df - 1)); %! [m, v] = stdrstat (2, df); %! assert_equal (m, sqrt (2) * Et, -1e-12); %! assert_equal (v, 2 * df / (df - 2) - 2 * Et ^ 2, -1e-11); ## The mean of the range of five standard normals, the control chart ## constant d2 %!assert_equal (stdrstat (5, Inf), 2.325929, 1e-6) ## Moments that do not exist, and invalid parameters %!test %! [m, v] = stdrstat (3, [1, 1.5, 2]); %! assert_equal (isnan (m), [true, false, false]); %! assert_equal (isnan (v), [true, true, true]); %!test %! [m, v] = stdrstat ([1, 2.5, NaN], 10); %! assert_equal ([m, v], NaN (1, 6)); ## Test input validation %!error stdrstat () %!error stdrstat (3) %!error stdrstat ({}, 10) %!error stdrstat (3, '') %!error stdrstat (i, 10) %!error stdrstat (3, i) %!error ... %! stdrstat (ones (3), ones (2)) statistics-release-2.1.2/inst/Distribution_Statistics/tlsstat.m000066400000000000000000000122551527311104300250500ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} tlsstat (@var{mu}, @var{sigma}, @var{nu}) ## ## Compute statistics of the location-scale Student's T distribution. ## ## @code{[@var{m}, @var{v}] = tlsstat (@var{mu}, @var{sigma}, @var{nu})} returns ## the mean and variance of the location-scale Student's T distribution with ## location parameter @var{mu}, scale parameter @var{sigma}, and @var{nu} ## degrees of freedom. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## Further information about the location-scale Student's T distribution can be ## found at ## @url{https://en.wikipedia.org/wiki/Student%27s_t-distribution#Location-scale_t_distribution} ## ## @seealso{tlscdf, tlsinv, tlspdf, tlsrnd, tlsfit, tlslike} ## @end deftypefn function [m, v] = tlsstat (mu, sigma, nu) ## Check for valid number of input arguments if (nargin < 3) error ("tlsstat: function called with too few input arguments."); endif ## Check for MU, SIGMA, and NU being numeric if (! (isnumeric (mu) && isnumeric (sigma) && isnumeric (nu))) error ("tlsstat: MU, SIGMA, and NU must be numeric."); endif ## Check for MU, SIGMA, and NU being real if (iscomplex (mu) || iscomplex (sigma) || iscomplex (nu)) error ("tlsstat: MU, SIGMA, and NU must not be complex."); endif ## Check for common size of MU, SIGMA, and NU if (! isscalar (mu) || ! isscalar (sigma) || ! isscalar (nu)) [retval, mu, sigma, nu] = common_size (mu, sigma, nu); if (retval > 0) error ("tlsstat: MU, SIGMA, and NU must be of common size or scalars."); endif endif ## Calculate moments m = zeros (size (nu)) + mu; v = sigma .* (nu ./ (nu - 2)); ## Continue argument check k = find (! (nu > 1) | ! (nu < Inf)); if (any (k)) m(k) = NaN; v(k) = NaN; endif k = find (! (nu > 2) & (nu < Inf)); if (any (k)) v(k) = NaN; endif endfunction ## Input validation tests %!error tlsstat () %!error tlsstat (1) %!error tlsstat (1, 2) %!error tlsstat ({}, 2, 3) %!error tlsstat (1, '', 3) %!error tlsstat (1, 2, ['d']) %!error tlsstat (i, 2, 3) %!error tlsstat (1, i, 3) %!error tlsstat (1, 2, i) %!error ... %! tlsstat (ones (3), ones (2), 1) %!error ... %! tlsstat (ones (2), 1, ones (3)) %!error ... %! tlsstat (1, ones (2), ones (3)) ## Output validation tests %!test %! [m, v] = tlsstat (0, 1, 0); %! assert_equal (m, NaN); %! assert_equal (v, NaN); %!test %! [m, v] = tlsstat (0, 1, 1); %! assert_equal (m, NaN); %! assert_equal (v, NaN); %!test %! [m, v] = tlsstat (2, 1, 1); %! assert_equal (m, NaN); %! assert_equal (v, NaN); %!test %! [m, v] = tlsstat (-2, 1, 1); %! assert_equal (m, NaN); %! assert_equal (v, NaN); %!test %! [m, v] = tlsstat (0, 1, 2); %! assert_equal (m, 0); %! assert_equal (v, NaN); %!test %! [m, v] = tlsstat (2, 1, 2); %! assert_equal (m, 2); %! assert_equal (v, NaN); %!test %! [m, v] = tlsstat (-2, 1, 2); %! assert_equal (m, -2); %! assert_equal (v, NaN); %!test %! [m, v] = tlsstat (0, 2, 2); %! assert_equal (m, 0); %! assert_equal (v, NaN); %!test %! [m, v] = tlsstat (2, 2, 2); %! assert_equal (m, 2); %! assert_equal (v, NaN); %!test %! [m, v] = tlsstat (-2, 2, 2); %! assert_equal (m, -2); %! assert_equal (v, NaN); %!test %! [m, v] = tlsstat (0, 1, 3); %! assert_equal (m, 0); %! assert_equal (v, 3); %!test %! [m, v] = tlsstat (0, 2, 3); %! assert_equal (m, 0); %! assert_equal (v, 6); %!test %! [m, v] = tlsstat (2, 1, 3); %! assert_equal (m, 2); %! assert_equal (v, 3); %!test %! [m, v] = tlsstat (2, 2, 3); %! assert_equal (m, 2); %! assert_equal (v, 6); %!test %! [m, v] = tlsstat (-2, 1, 3); %! assert_equal (m, -2); %! assert_equal (v, 3); %!test %! [m, v] = tlsstat (-2, 2, 3); %! assert_equal (m, -2); %! assert_equal (v, 6); statistics-release-2.1.2/inst/Distribution_Statistics/tristat.m000066400000000000000000000071601527311104300250430ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} tristat (@var{a}, @var{b}, @var{c}) ## ## Compute statistics of the Triangular distribution. ## ## @code{[@var{m}, @var{v}] = tristat (@var{a}, @var{b}, @var{c})} returns the ## mean and variance of the Triangular distribution with lower limit parameter ## @var{a}, peak location (mode) parameter @var{b}, and upper limit parameter ## @var{c}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## Note that the order of the parameter input arguments has been changed after ## statistics version 1.6.3 in order to be MATLAB compatible with the parameters ## used in the TriangularDistribution probability distribution object. More ## specifically, the positions of the parameters @var{b} and @var{c} have been ## swapped. As a result, the naming conventions no longer coincide with those ## used in Wikipedia, in which @math{b} denotes the upper limit and @math{c} ## denotes the mode or peak parameter. ## ## Further information about the triangular distribution can be found at ## @url{https://en.wikipedia.org/wiki/Triangular_distribution} ## ## @seealso{tcdf, tinv, tpdf, trnd} ## @end deftypefn function [m, v] = tristat (a, b, c) ## Check for valid number of input arguments if (nargin < 3) error ("tristat: function called with too few input arguments."); endif ## Check for A, B, and C being numeric if (! (isnumeric (a) && isnumeric (b) && isnumeric (c))) error ("tristat: A, B, and C must be numeric."); endif ## Check for A, B, and C being real if (! (isreal (a) && isreal (b) && isreal (c))) error ("tristat: A, B, and C must be real."); endif ## Calculate moments m = (a + b + c) ./ 3; v = (a .^ 2 + b .^ 2 + c .^ 2 - a .* b - a .* c - b .* c) ./ 18; ## Continue argument check k = find (! (a < c) | ! (a <= b & b <= c)); if (any (k)) m(k) = NaN; v(k) = NaN; endif endfunction ## Input validation tests %!error tristat () %!error tristat (1) %!error tristat (1, 2) %!error tristat ('i', 2, 1) %!error tristat (0, 'd', 1) %!error tristat (0, 3, {}) %!error tristat (i, 2, 1) %!error tristat (0, i, 1) %!error tristat (0, 3, i) ## Output validation tests %!test %! a = 1:5; %! b = 3:7; %! c = 5:9; %! [m, v] = tristat (a, b, c); %! expected_m = [3, 4, 5, 6, 7]; %! assert_equal (m, expected_m); %! assert_equal (v, ones (1, 5) * (2/3)); statistics-release-2.1.2/inst/Distribution_Statistics/tstat.m000066400000000000000000000050411527311104300245040ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} tstat (@var{df}) ## ## Compute statistics of the Student's T distribution. ## ## @code{[@var{m}, @var{v}] = tstat (@var{df})} returns the mean and variance of ## the Student's T distribution with @var{df} degrees of freedom. ## ## The size of @var{m} (mean) and @var{v} (variance) is the same size of the ## input argument. ## ## Further information about the Student's T distribution can be found at ## @url{https://en.wikipedia.org/wiki/Student%27s_t-distribution} ## ## @seealso{tcdf, tinv, tpdf, trnd} ## @end deftypefn function [m, v] = tstat (df) ## Check for valid number of input arguments if (nargin < 1) error ("tstat: function called with too few input arguments."); endif ## Check for DF being numeric if (! isnumeric (df)) error ("tstat: DF must be numeric."); endif ## Check for DF being real if (iscomplex (df)) error ("tstat: DF must not be complex."); endif ## Calculate moments m = zeros (size (df)); v = df ./ (df - 2); ## Continue argument check k = find (! (df > 1) | ! (df < Inf)); if (any (k)) m(k) = NaN; v(k) = NaN; endif k = find (! (df > 2) & (df < Inf)); if (any (k)) v(k) = Inf; endif endfunction ## Input validation tests %!error tstat () %!error tstat ({}) %!error tstat ('') %!error tstat (i) ## Output validation tests %!test %! df = 3:8; %! [m, v] = tstat (df); %! expected_m = [0, 0, 0, 0, 0, 0]; %! expected_v = [3.0000, 2.0000, 1.6667, 1.5000, 1.4000, 1.3333]; %! assert_equal (m, expected_m); %! assert_equal (v, expected_v, 0.001); statistics-release-2.1.2/inst/Distribution_Statistics/unidstat.m000066400000000000000000000053001527311104300251760ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} unidstat (@var{df}) ## ## Compute statistics of the discrete uniform cumulative distribution. ## ## @code{[@var{m}, @var{v}] = unidstat (@var{df})} returns the mean and variance ## of the discrete uniform cumulative distribution with parameter @var{N}, which ## corresponds to the maximum observable value and must be a positive natural ## number. ## ## The size of @var{m} (mean) and @var{v} (variance) is the same size of the ## input argument. ## ## Further information about the discrete uniform distribution can be found at ## @url{https://en.wikipedia.org/wiki/Discrete_uniform_distribution} ## ## @seealso{unidcdf, unidinv, unidpdf, unidrnd, unidfit} ## @end deftypefn function [m, v] = unidstat (N) ## Check for valid number of input arguments if (nargin < 1) error ("unidstat: function called with too few input arguments."); endif ## Check for N being numeric if (! isnumeric (N)) error ("unidstat: N must be numeric."); endif ## Check for N being real if (iscomplex (N)) error ("unidstat: N must not be complex."); endif ## Calculate moments m = (N + 1) ./ 2; v = ((N .^ 2) - 1) ./ 12; ## Continue argument check k = find (! (N > 0) | ! (N < Inf) | ! (N == round (N))); if (any (k)) m(k) = NaN; v(k) = NaN; endif endfunction ## Input validation tests %!error unidstat () %!error unidstat ({}) %!error unidstat ('') %!error unidstat (i) ## Output validation tests %!test %! N = 1:6; %! [m, v] = unidstat (N); %! expected_m = [1.0000, 1.5000, 2.0000, 2.5000, 3.0000, 3.5000]; %! expected_v = [0.0000, 0.2500, 0.6667, 1.2500, 2.0000, 2.9167]; %! assert_equal (m, expected_m, 0.001); %! assert_equal (v, expected_v, 0.001); statistics-release-2.1.2/inst/Distribution_Statistics/unifstat.m000066400000000000000000000073151527311104300252100ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} unifstat (@var{df}) ## ## Compute statistics of the continuous uniform cumulative distribution. ## ## @code{[@var{m}, @var{v}] = unifstat (@var{df})} returns the mean and variance ## of the continuous uniform cumulative distribution with parameters @var{a} and ## @var{b}, which define the lower and upper bounds of the interval ## @qcode{[@var{a}, @var{b}]}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## Further information about the continuous uniform distribution can be found at ## @url{https://en.wikipedia.org/wiki/Continuous_uniform_distribution} ## ## @seealso{unifcdf, unifinv, unifpdf, unifrnd, unifit} ## @end deftypefn function [m, v] = unifstat (a, b) ## Check for valid number of input arguments if (nargin < 2) error ("unifstat: function called with too few input arguments."); endif ## Check for A and B being numeric if (! (isnumeric (a) && isnumeric (b))) error ("unifstat: A and B must be numeric."); endif ## Check for A and B being real if (iscomplex (a) || iscomplex (b)) error ("unifstat: A and B must not be complex."); endif ## Check for common size of A and B if (! isscalar (a) || ! isscalar (b)) [retval, a, b] = common_size (a, b); if (retval > 0) error ("unifstat: A and B must be of common size or scalars."); endif endif ## Calculate moments m = (a + b) ./ 2; v = ((b - a) .^ 2) ./ 12; ## Continue argument check k = find (! (-Inf < a) | ! (a < b) | ! (b < Inf)); if (any (k)) m(k) = NaN; v(k) = NaN; endif endfunction ## Input validation tests %!error unifstat () %!error unifstat (1) %!error unifstat ({}, 2) %!error unifstat (1, '') %!error unifstat (i, 2) %!error unifstat (1, i) %!error ... %! unifstat (ones (3), ones (2)) %!error ... %! unifstat (ones (2), ones (3)) ## Output validation tests %!test %! a = 1:6; %! b = 2:2:12; %! [m, v] = unifstat (a, b); %! expected_m = [1.5000, 3.0000, 4.5000, 6.0000, 7.5000, 9.0000]; %! expected_v = [0.0833, 0.3333, 0.7500, 1.3333, 2.0833, 3.0000]; %! assert_equal (m, expected_m, 0.001); %! assert_equal (v, expected_v, 0.001); %!test %! a = 1:6; %! [m, v] = unifstat (a, 10); %! expected_m = [5.5000, 6.0000, 6.5000, 7.0000, 7.5000, 8.0000]; %! expected_v = [6.7500, 5.3333, 4.0833, 3.0000, 2.0833, 1.3333]; %! assert_equal (m, expected_m, 0.001); %! assert_equal (v, expected_v, 0.001); statistics-release-2.1.2/inst/Distribution_Statistics/wblstat.m000066400000000000000000000077071527311104300250400ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{m}, @var{v}] =} wblstat (@var{lambda}, @var{k}) ## ## Compute statistics of the Weibull distribution. ## ## @code{[@var{m}, @var{v}] = wblstat (@var{lambda}, @var{k})} returns the mean ## and variance of the Weibull distribution with scale parameter @var{lambda} ## and shape parameter @var{k}. ## ## The size of @var{m} (mean) and @var{v} (variance) is the common size of the ## input arguments. A scalar input functions as a constant matrix of the ## same size as the other inputs. ## ## Further information about the Weibull distribution can be found at ## @url{https://en.wikipedia.org/wiki/Weibull_distribution} ## ## The @code{prob.WeibullDistribution} class names these same two parameters ## @qcode{A} and @qcode{B}, after MATLAB. @var{lambda} is its @qcode{A} and ## @var{k} is its @qcode{B}. ## @seealso{wblcdf, wblinv, wblpdf, wblrnd, wblfit, wbllike, wblplot} ## @end deftypefn function [m, v] = wblstat (lambda, k) ## Check for valid number of input arguments if (nargin < 2) error ("wblstat: function called with too few input arguments."); endif ## Check for LAMBDA and K being numeric if (! (isnumeric (lambda) && isnumeric (k))) error ("wblstat: LAMBDA and K must be numeric."); endif ## Check for LAMBDA and K being real if (iscomplex (lambda) || iscomplex (k)) error ("wblstat: LAMBDA and K must not be complex."); endif ## Check for common size of LAMBDA and K if (! isscalar (lambda) || ! isscalar (k)) [retval, lambda, k] = common_size (lambda, k); if (retval > 0) error ("wblstat: LAMBDA and K must be of common size or scalars."); endif endif ## Calculate moments m = lambda .* gamma (1 + 1 ./ k); v = (lambda .^ 2) .* gamma (1 + 2 ./ k) - m .^ 2; ## Continue argument check is_nan = find (! (lambda > 0) | ! (lambda < Inf) | ! (k > 0) | ! (k < Inf)); if (any (is_nan)) m(is_nan) = NaN; v(is_nan) = NaN; endif endfunction ## Input validation tests %!error wblstat () %!error wblstat (1) %!error wblstat ({}, 2) %!error wblstat (1, '') %!error wblstat (i, 2) %!error wblstat (1, i) %!error ... %! wblstat (ones (3), ones (2)) %!error ... %! wblstat (ones (2), ones (3)) ## Output validation tests %!test %! lambda = 3:8; %! k = 1:6; %! [m, v] = wblstat (lambda, k); %! expected_m = [3.0000, 3.5449, 4.4649, 5.4384, 6.4272, 7.4218]; %! expected_v = [9.0000, 3.4336, 2.6333, 2.3278, 2.1673, 2.0682]; %! assert_equal (m, expected_m, 0.001); %! assert_equal (v, expected_v, 0.001); %!test %! k = 1:6; %! [m, v] = wblstat (6, k); %! expected_m = [ 6.0000, 5.3174, 5.3579, 5.4384, 5.5090, 5.5663]; %! expected_v = [36.0000, 7.7257, 3.7920, 2.3278, 1.5923, 1.1634]; %! assert_equal (m, expected_m, 0.001); %! assert_equal (v, expected_v, 0.001); statistics-release-2.1.2/inst/Distribution_Wrappers/000077500000000000000000000000001527311104300226405ustar00rootroot00000000000000statistics-release-2.1.2/inst/Distribution_Wrappers/cdf.m000066400000000000000000000423621527311104300235610ustar00rootroot00000000000000## Copyright (C) 2013 Pantxo Diribarne ## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} cdf (@var{name}, @var{x}, @var{A}) ## @deftypefnx {statistics} {@var{p} =} cdf (@var{name}, @var{x}, @var{A}, @var{B}) ## @deftypefnx {statistics} {@var{p} =} cdf (@var{name}, @var{x}, @var{A}, @var{B}, @var{C}) ## @deftypefnx {statistics} {@var{p} =} cdf (@dots{}, @qcode{'upper'}) ## ## Return the CDF of a univariate distribution evaluated at @var{x}. ## ## @code{cdf} is a wrapper for the univariate cumulative distribution functions ## available in the statistics package. See the corresponding functions' help ## to learn the signification of the parameters after @var{x}. ## ## @code{@var{p} = cdf (@var{name}, @var{x}, @var{A})} returns the CDF for the ## one-parameter distribution family specified by @var{name} and the ## distribution parameter @var{A}, evaluated at the values in @var{x}. ## ## @code{@var{p} = cdf (@var{name}, @var{x}, @var{A}, @var{B})} returns the CDF ## for the two-parameter distribution family specified by @var{name} and the ## distribution parameters @var{A} and @var{B}, evaluated at the values in ## @var{x}. ## ## @code{@var{p} = cdf (@var{name}, @var{x}, @var{A}, @var{B}, @var{C})} returns ## the CDF for the three-parameter distribution family specified by @var{name} ## and the distribution parameters @var{A}, @var{B}, and @var{C}, evaluated at ## the values in @var{x}. ## ## @code{@var{p} = cdf (@dots{}, @qcode{'upper'})} returns the complement of the ## CDF using an algorithm that more accurately computes the extreme upper-tail ## probabilities. @qcode{'upper'} can follow any of the input arguments in the ## previous syntaxes. ## ## @var{name} must be a char string of the name or the abbreviation of the ## desired cumulative distribution function as listed in the following table. ## The last column shows the number of required parameters that should be parsed ## after @var{x} to the desired CDF. The optional input argument ## @qcode{'upper'} does not count in the required number of parameters. ## ## @multitable @columnfractions 0.4 0.2 0.3 ## @headitem Distribution Name @tab Abbreviation @tab Input Parameters ## @item @qcode{'Beta'} @tab @qcode{'beta'} @tab 2 ## @item @qcode{'Binomial'} @tab @qcode{'bino'} @tab 2 ## @item @qcode{'Birnbaum-Saunders'} @tab @qcode{'bisa'} @tab 2 ## @item @qcode{'Burr'} @tab @qcode{'burr'} @tab 3 ## @item @qcode{'Cauchy'} @tab @qcode{'cauchy'} @tab 2 ## @item @qcode{'Chi-squared'} @tab @qcode{'chi2'} @tab 1 ## @item @qcode{'Extreme Value'} @tab @qcode{'ev'} @tab 2 ## @item @qcode{'Exponential'} @tab @qcode{'exp'} @tab 1 ## @item @qcode{'F-Distribution'} @tab @qcode{'f'} @tab 2 ## @item @qcode{'Gamma'} @tab @qcode{'gam'} @tab 2 ## @item @qcode{'Geometric'} @tab @qcode{'geo'} @tab 1 ## @item @qcode{'Generalized Extreme Value'} @tab @qcode{'gev'} @tab 3 ## @item @qcode{'Generalized Pareto'} @tab @qcode{'gp'} @tab 3 ## @item @qcode{'Gumbel'} @tab @qcode{'gumbel'} @tab 2 ## @item @qcode{'Half-normal'} @tab @qcode{'hn'} @tab 2 ## @item @qcode{'Hypergeometric'} @tab @qcode{'hyge'} @tab 3 ## @item @qcode{'Inverse Gaussian'} @tab @qcode{'invg'} @tab 2 ## @item @qcode{'Laplace'} @tab @qcode{'laplace'} @tab 2 ## @item @qcode{'Logistic'} @tab @qcode{'logi'} @tab 2 ## @item @qcode{'Log-Logistic'} @tab @qcode{'logl'} @tab 2 ## @item @qcode{'Lognormal'} @tab @qcode{'logn'} @tab 2 ## @item @qcode{'Nakagami'} @tab @qcode{'naka'} @tab 2 ## @item @qcode{'Negative Binomial'} @tab @qcode{'nbin'} @tab 2 ## @item @qcode{'Noncentral F-Distribution'} @tab @qcode{'ncf'} @tab 3 ## @item @qcode{'Noncentral Student T'} @tab @qcode{'nct'} @tab 2 ## @item @qcode{'Noncentral Chi-Squared'} @tab @qcode{'ncx2'} @tab 2 ## @item @qcode{'Normal'} @tab @qcode{'norm'} @tab 2 ## @item @qcode{'Poisson'} @tab @qcode{'poiss'} @tab 1 ## @item @qcode{'Rayleigh'} @tab @qcode{'rayl'} @tab 1 ## @item @qcode{'Rician'} @tab @qcode{'rice'} @tab 2 ## @item @qcode{'Student T'} @tab @qcode{'t'} @tab 1 ## @item @qcode{'location-scale T'} @tab @qcode{'tls'} @tab 3 ## @item @qcode{'Triangular'} @tab @qcode{'tri'} @tab 3 ## @item @qcode{'Discrete Uniform'} @tab @qcode{'unid'} @tab 1 ## @item @qcode{'Uniform'} @tab @qcode{'unif'} @tab 2 ## @item @qcode{'Von Mises'} @tab @qcode{'vm'} @tab 2 ## @item @qcode{'Weibull'} @tab @qcode{'wbl'} @tab 2 ## @end multitable ## ## Distribution names are matched ignoring case, spaces and hyphens, so that ## @qcode{'Extreme Value'}, @qcode{'ExtremeValue'} and @qcode{'extreme-value'} ## all select the same distribution, and the same set of names is accepted by ## @code{cdf}, @code{pdf}, @code{icdf}, @code{random}, @code{makedist}, ## @code{fitdist} and @code{mle}. ## ## This accepts more names than MATLAB. MATLAB takes the spaced and the ## squashed spelling but refuses the hyphenated one, so ## @qcode{'Birnbaum-Saunders'} and @qcode{'Log-Logistic'} are errors there; ## Octave has always accepted them and continues to. MATLAB also accepts ## @qcode{'tLocationScale'} in @code{makedist} while refusing it in ## @code{cdf} for the same distribution; Octave accepts it, and ## @qcode{'location-scale T'}, everywhere. Code written against MATLAB's ## names therefore runs unchanged, but code relying on these names will not ## port back. ## ## @seealso{icdf, pdf, cdf, betacdf, binocdf, bisacdf, burrcdf, cauchycdf, ## chi2cdf, evcdf, expcdf, fcdf, gamcdf, geocdf, gevcdf, gpcdf, gumbelcdf, ## hncdf, hygecdf, invgcdf, laplacecdf, logicdf, loglcdf, logncdf, nakacdf, ## nbincdf, ncfcdf, nctcdf, ncx2cdf, normcdf, poisscdf, raylcdf, ricecdf, tcdf, ## tlscdf, tricdf, unidcdf, unifcdf, vmcdf, wblcdf} ## @end deftypefn function p = cdf (name, x, varargin) ## implemented functions persistent allDF = { ... {'beta' , 'Beta'}, @betacdf, 2, ... {'bino' , 'Binomial'}, @binocdf, 2, ... {'bisa' , 'Birnbaum-Saunders'}, @bisacdf, 2, ... {'burr' , 'Burr'}, @burrcdf, 3, ... {'cauchy' , 'Cauchy'}, @cauchycdf, 2, ... {'chi2' , 'Chi-squared'}, @chi2cdf, 1, ... {'ev' , 'Extreme Value'}, @evcdf, 2, ... {'exp' , 'Exponential'}, @expcdf, 1, ... {'f' , 'F-Distribution'}, @fcdf, 2, ... {'gam' , 'Gamma'}, @gamcdf, 2, ... {'geo' , 'Geometric'}, @geocdf, 1, ... {'gev' , 'Generalized Extreme Value'}, @gevcdf, 3, ... {'gp' , 'Generalized Pareto'}, @gpcdf, 3, ... {'gumbel' , 'Gumbel'}, @gumbelcdf, 2, ... {'hn' , 'Half-normal'}, @hncdf, 2, ... {'hyge' , 'Hypergeometric'}, @hygecdf, 3, ... {'invg' , 'Inverse Gaussian'}, @invgcdf, 2, ... {'laplace' , 'Laplace'}, @laplacecdf, 2, ... {'logi' , 'Logistic'}, @logicdf, 2, ... {'logl' , 'Log-Logistic'}, @loglcdf, 2, ... {'logn' , 'Lognormal'}, @logncdf, 2, ... {'naka' , 'Nakagami'}, @nakacdf, 2, ... {'nbin' , 'Negative Binomial'}, @nbincdf, 2, ... {'ncf' , 'Noncentral F-Distribution'}, @ncfcdf, 3, ... {'nct' , 'Noncentral Student T'}, @nctcdf, 2, ... {'ncx2' , 'Noncentral Chi-squared'}, @ncx2cdf, 2, ... {'norm' , 'Normal'}, @normcdf, 2, ... {'poiss' , 'Poisson'}, @poisscdf, 1, ... {'rayl' , 'Rayleigh'}, @raylcdf, 1, ... {'rice' , 'Rician'}, @ricecdf, 2, ... {'t' , 'Student T'}, @tcdf, 1, ... {'tls', 'location-scale T', 'tLocationScale'}, @tlscdf, 3, ... {'tri' , 'Triangular'}, @tricdf, 3, ... {'unid' , 'Discrete Uniform'}, @unidcdf, 1, ... {'unif' , 'Uniform'}, @unifcdf, 2, ... {'vm' , 'Von Mises'}, @vmcdf, 2, ... {'wbl' , 'Weibull'}, @wblcdf, 2}; ## Check NAME being a char string if (! ischar (name)) error ("cdf: distribution NAME must be a char string."); endif ## Check X being numeric and real if (! isnumeric (x)) error ("cdf: X must be numeric."); elseif (! isreal (x)) error ("cdf: values in X must be real."); endif ## Get number of arguments nargs = numel (varargin); ## Get available functions cdfnames = allDF(1:3:end); cdfhandl = allDF(2:3:end); cdf_args = allDF(3:3:end); ## Search for CDF function ## Match on the folded key so that every spelling of a name resolves key = __distname_key__ (name); idx = cellfun (@(x) any (strcmp (key, cellfun (@__distname_key__, x, ... 'UniformOutput', false))), cdfnames); if (any (idx)) if (nargs == cdf_args{idx} + 1) ## Check for "upper" option if (! strcmpi (varargin{nargs}, 'upper')) error ("cdf: invalid argument for upper tail."); else ## Check that all remaining distribution parameters are numeric if (! all (cellfun (@(x)isnumeric (x), (varargin([1:nargs-1]))))) error ("cdf: distribution parameters must be numeric."); endif ## Call appropriate CDF with "upper" flag p = feval (cdfhandl{idx}, x, varargin{:}); endif elseif (nargs == cdf_args{idx}) ## Check that all distribution parameters are numeric if (! all (cellfun (@(x)isnumeric (x), (varargin)))) error ("cdf: distribution parameters must be numeric."); endif ## Call appropriate CDF without "upper" flag p = feval (cdfhandl{idx}, x, varargin{:}); else if (cdf_args{idx} == 1) error ("cdf: %s distribution requires 1 parameter.", name); else error ("cdf: %s distribution requires %d parameters.", ... name, cdf_args{idx}); endif endif else error ("cdf: %s distribution is not implemented in Statistics.", name); endif endfunction ## Test results %!shared x %! x = [1:5]; %!assert_equal (cdf ('Beta', x, 5, 2), betacdf (x, 5, 2)) %!assert_equal (cdf ('beta', x, 5, 2, 'upper'), betacdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Binomial', x, 5, 2), binocdf (x, 5, 2)) %!assert_equal (cdf ('bino', x, 5, 2, 'upper'), binocdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Birnbaum-Saunders', x, 5, 2), bisacdf (x, 5, 2)) %!assert_equal (cdf ('bisa', x, 5, 2, 'upper'), bisacdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Burr', x, 5, 2, 2), burrcdf (x, 5, 2, 2)) %!assert_equal (cdf ('burr', x, 5, 2, 2, 'upper'), burrcdf (x, 5, 2, 2, 'upper')) %!assert_equal (cdf ('Cauchy', x, 5, 2), cauchycdf (x, 5, 2)) %!assert_equal (cdf ('cauchy', x, 5, 2, 'upper'), cauchycdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Chi-squared', x, 5), chi2cdf (x, 5)) %!assert_equal (cdf ('chi2', x, 5, 'upper'), chi2cdf (x, 5, 'upper')) %!assert_equal (cdf ('Extreme Value', x, 5, 2), evcdf (x, 5, 2)) %!assert_equal (cdf ('ev', x, 5, 2, 'upper'), evcdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Exponential', x, 5), expcdf (x, 5)) %!assert_equal (cdf ('exp', x, 5, 'upper'), expcdf (x, 5, 'upper')) %!assert_equal (cdf ('F-Distribution', x, 5, 2), fcdf (x, 5, 2)) %!assert_equal (cdf ('f', x, 5, 2, 'upper'), fcdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Gamma', x, 5, 2), gamcdf (x, 5, 2)) %!assert_equal (cdf ('gam', x, 5, 2, 'upper'), gamcdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Geometric', x, 5), geocdf (x, 5)) %!assert_equal (cdf ('geo', x, 5, 'upper'), geocdf (x, 5, 'upper')) %!assert_equal (cdf ('Generalized Extreme Value', x, 5, 2, 2), gevcdf (x, 5, 2, 2)) %!assert_equal (cdf ('gev', x, 5, 2, 2, 'upper'), gevcdf (x, 5, 2, 2, 'upper')) %!assert_equal (cdf ('Generalized Pareto', x, 5, 2, 2), gpcdf (x, 5, 2, 2)) %!assert_equal (cdf ('gp', x, 5, 2, 2, 'upper'), gpcdf (x, 5, 2, 2, 'upper')) %!assert_equal (cdf ('Gumbel', x, 5, 2), gumbelcdf (x, 5, 2)) %!assert_equal (cdf ('gumbel', x, 5, 2, 'upper'), gumbelcdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Half-normal', x, 5, 2), hncdf (x, 5, 2)) %!assert_equal (cdf ('hn', x, 5, 2, 'upper'), hncdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Hypergeometric', x, 5, 2, 2), hygecdf (x, 5, 2, 2)) %!assert_equal (cdf ('hyge', x, 5, 2, 2, 'upper'), hygecdf (x, 5, 2, 2, 'upper')) %!assert_equal (cdf ('Inverse Gaussian', x, 5, 2), invgcdf (x, 5, 2)) %!assert_equal (cdf ('invg', x, 5, 2, 'upper'), invgcdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Laplace', x, 5, 2), laplacecdf (x, 5, 2)) %!assert_equal (cdf ('laplace', x, 5, 2, 'upper'), laplacecdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Logistic', x, 5, 2), logicdf (x, 5, 2)) %!assert_equal (cdf ('logi', x, 5, 2, 'upper'), logicdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Log-Logistic', x, 5, 2), loglcdf (x, 5, 2)) %!assert_equal (cdf ('logl', x, 5, 2, 'upper'), loglcdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Lognormal', x, 5, 2), logncdf (x, 5, 2)) %!assert_equal (cdf ('logn', x, 5, 2, 'upper'), logncdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Nakagami', x, 5, 2), nakacdf (x, 5, 2)) %!assert_equal (cdf ('naka', x, 5, 2, 'upper'), nakacdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Negative Binomial', x, 5, 2), nbincdf (x, 5, 2)) %!assert_equal (cdf ('nbin', x, 5, 2, 'upper'), nbincdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Noncentral F-Distribution', x, 5, 2, 2), ncfcdf (x, 5, 2, 2)) %!assert_equal (cdf ('ncf', x, 5, 2, 2, 'upper'), ncfcdf (x, 5, 2, 2, 'upper')) %!assert_equal (cdf ('Noncentral Student T', x, 5, 2), nctcdf (x, 5, 2)) %!assert_equal (cdf ('nct', x, 5, 2, 'upper'), nctcdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Noncentral Chi-Squared', x, 5, 2), ncx2cdf (x, 5, 2)) %!assert_equal (cdf ('ncx2', x, 5, 2, 'upper'), ncx2cdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Normal', x, 5, 2), normcdf (x, 5, 2)) %!assert_equal (cdf ('norm', x, 5, 2, 'upper'), normcdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Poisson', x, 5), poisscdf (x, 5)) %!assert_equal (cdf ('poiss', x, 5, 'upper'), poisscdf (x, 5, 'upper')) %!assert_equal (cdf ('Rayleigh', x, 5), raylcdf (x, 5)) %!assert_equal (cdf ('rayl', x, 5, 'upper'), raylcdf (x, 5, 'upper')) %!assert_equal (cdf ('Rician', x, 5, 1), ricecdf (x, 5, 1)) %!assert_equal (cdf ('rice', x, 5, 1, 'upper'), ricecdf (x, 5, 1, 'upper')) %!assert_equal (cdf ('Student T', x, 5), tcdf (x, 5)) %!assert_equal (cdf ('t', x, 5, 'upper'), tcdf (x, 5, 'upper')) %!assert_equal (cdf ('location-scale T', x, 5, 1, 2), tlscdf (x, 5, 1, 2)) %!assert_equal (cdf ('tls', x, 5, 1, 2, 'upper'), tlscdf (x, 5, 1, 2, 'upper')) %!assert_equal (cdf ('Triangular', x, 5, 2, 2), tricdf (x, 5, 2, 2)) %!assert_equal (cdf ('tri', x, 5, 2, 2, 'upper'), tricdf (x, 5, 2, 2, 'upper')) %!assert_equal (cdf ('Discrete Uniform', x, 5), unidcdf (x, 5)) %!assert_equal (cdf ('unid', x, 5, 'upper'), unidcdf (x, 5, 'upper')) %!assert_equal (cdf ('Uniform', x, 5, 2), unifcdf (x, 5, 2)) %!assert_equal (cdf ('unif', x, 5, 2, 'upper'), unifcdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Von Mises', x, 5, 2), vmcdf (x, 5, 2)) %!assert_equal (cdf ('vm', x, 5, 2, 'upper'), vmcdf (x, 5, 2, 'upper')) %!assert_equal (cdf ('Weibull', x, 5, 2), wblcdf (x, 5, 2)) %!assert_equal (cdf ('wbl', x, 5, 2, 'upper'), wblcdf (x, 5, 2, 'upper')) ## Test input validation %!test %! ## Every spelling of a name reaches the same distribution: case, spaces, %! ## hyphens and underscores are all ignored. %! for n = {'Extreme Value', 'ExtremeValue', 'extreme-value', 'EXTREME VALUE'} %! assert_equal (cdf (n{1}, 1, 2, 3), cdf ('ev', 1, 2, 3)); %! endfor %!test %! ## The name that makedist uses is accepted here too, and the reverse %! assert_equal (cdf ('tLocationScale', 1, 2, 3, 4), cdf ('tls', 1, 2, 3, 4)); %!error cdf (1) %!error cdf ({'beta'}) %!error cdf ('beta', {[1 2 3 4 5]}) %!error cdf ('beta', 'text') %!error cdf ('beta', 1+i) %!error ... %! cdf ('Beta', x, 'a', 2) %!error ... %! cdf ('Beta', x, 5, '') %!error ... %! cdf ('Beta', x, 5, {2}) %!error cdf ('chi2', x) %!error cdf ('Beta', x, 5) %!error cdf ('Burr', x, 5) %!error cdf ('Burr', x, 5, 2) statistics-release-2.1.2/inst/Distribution_Wrappers/doc-cache000066400000000000000000000661311527311104300244000ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 8 # name: # type: sq_string # elements: 1 # length: 3 cdf # name: # type: sq_string # elements: 1 # length: 3467 statistics: p = cdf (name, x, A) statistics: p = cdf (name, x, A, B) statistics: p = cdf (name, x, A, B, C) statistics: p = cdf (..., 'upper') Return the CDF of a univariate distribution evaluated at x. cdf is a wrapper for the univariate cumulative distribution functions available in the statistics package. See the corresponding functions' help to learn the signification of the parameters after x. p = cdf (name, x, A) returns the CDF for the one-parameter distribution family specified by name and the distribution parameter A, evaluated at the values in x. p = cdf (name, x, A, B) returns the CDF for the two-parameter distribution family specified by name and the distribution parameters A and B, evaluated at the values in x. p = cdf (name, x, A, B, C) returns the CDF for the three-parameter distribution family specified by name and the distribution parameters A, B, and C, evaluated at the values in x. p = cdf (..., 'upper') returns the complement of the CDF using an algorithm that more accurately computes the extreme upper-tail probabilities. 'upper' can follow any of the input arguments in the previous syntaxes. name must be a char string of the name or the abbreviation of the desired cumulative distribution function as listed in the following table. The last column shows the number of required parameters that should be parsed after x to the desired CDF. The optional input argument 'upper' does not count in the required number of parameters. Distribution Name Abbreviation Input Parameters 'Beta' 'beta' 2 'Binomial' 'bino' 2 'Birnbaum-Saunders' 'bisa' 2 'Burr' 'burr' 3 'Cauchy' 'cauchy' 2 'Chi-squared' 'chi2' 1 'Extreme Value' 'ev' 2 'Exponential' 'exp' 1 'F-Distribution' 'f' 2 'Gamma' 'gam' 2 'Geometric' 'geo' 1 'Generalized Extreme Value' 'gev' 3 'Generalized Pareto' 'gp' 3 'Gumbel' 'gumbel' 2 'Half-normal' 'hn' 2 'Hypergeometric' 'hyge' 3 'Inverse Gaussian' 'invg' 2 'Laplace' 'laplace' 2 'Logistic' 'logi' 2 'Log-Logistic' 'logl' 2 'Lognormal' 'logn' 2 'Nakagami' 'naka' 2 'Negative Binomial' 'nbin' 2 'Noncentral F-Distribution' 'ncf' 3 'Noncentral Student T' 'nct' 2 'Noncentral Chi-Squared' 'ncx2' 2 'Normal' 'norm' 2 'Poisson' 'poiss' 1 'Rayleigh' 'rayl' 1 'Rician' 'rice' 2 'Student T' 't' 1 'location-scale T' 'tls' 3 'Triangular' 'tri' 3 'Discrete Uniform' 'unid' 1 'Uniform' 'unif' 2 'Von Mises' 'vm' 2 'Weibull' 'wbl' 2 Distribution names are matched ignoring case, spaces and hyphens, so that 'Extreme Value', 'ExtremeValue' and 'extreme-value' all select the same distribution, and the same set of names is accepted by cdf, pdf, icdf, random, makedist, fitdist and mle. This accepts more names than MATLAB. MATLAB takes the spaced and the squashed spelling but refuses the hyphenated one, so 'Birnbaum-Saunders' and 'Log-Logistic' are errors there; Octave has always accepted them and continues to. MATLAB also accepts 'tLocationScale' in makedist while refusing it in cdf for the same distribution; Octave accepts it, and 'location-scale T', everywhere. Code written against MATLAB's names therefore runs unchanged, but code relying on these names will not port back. See also: icdf, pdf, cdf, betacdf, binocdf, bisacdf, burrcdf, cauchycdf, chi2cdf, evcdf, expcdf, fcdf, gamcdf, geocdf, gevcdf, gpcdf, gumbelcdf, hncdf, hygecdf, invgcdf, laplacecdf, logicdf, loglcdf, logncdf, nakacdf, nbincdf, ncfcdf, nctcdf, ncx2cdf, normcdf, poisscdf, raylcdf, ricecdf, tcdf, tlscdf, tricdf, unidcdf, unifcdf, vmcdf, wblcdf # name: # type: sq_string # elements: 1 # length: 59 Return the CDF of a univariate distribution evaluated at x. # name: # type: sq_string # elements: 1 # length: 7 fitdist # name: # type: sq_string # elements: 1 # length: 3095 statistics: pd = fitdist (x, distname) statistics: pd = fitdist (x, distname, Name, Value) statistics: [pdca, gn, gl] = fitdist (x, distname, 'By', groupvar) statistics: [pdca, gn, gl] = fitdist (x, distname, 'By', groupvar, Name, Value) Create probability distribution object. pd = fitdist (x, distname) creates a probability distribution object by fitting the distribution specified by distname to the data in vector x. pd = fitdist (x, distname, Name, Value) creates the probability distribution object with additional options specified by one or more Name-Value pair arguments listed below. Name Value 'distribution' A character vector specifying the distribution type for which to estimate parameters. 'Ntrials' A scalar specifying the number of trials for the corresponding element of x for the binomial distribution. 'theta' A scalar specifying the location parameter for the generalized Pareto distribution. It defaults to 0, as MATLAB assumes it, and is not estimated. 'mu' A scalar specifying the location parameter for the half-normal distribution. 'censoring' A vector of the same size as x indicating censored data in x. By default it is censor = zeros (size (x)). 'frequency' A vector of nonnegative integer counts of the same size as x used as frequency observations. By default it is freq = ones (size (x)). 'alpha' A scalar in the range (0,1), as the significance level for the confidence interval pci. By default it is 0.05 corresponding to 95% confidence intervals. 'options' A structure specifying the control parameters for the iterative algorithm used to compute ML estimates with the fminsearch function. [pdca, gn, gl] = fitdist (x, distname, 'By', groupvar) creates probability distribution objects by fitting the distribution specified by distname to the data in x based on the grouping variable groupvar. It returns a cell array of fitted probability distribution object, pdca, a cell array of group labels, gn, and a cell array of grouping variable levels, gl. [pdca, gn, gl] = fitdist (x, distname, 'By', groupvar, Name, Value) returns the same output arguments using additional options specified by one or more Name-Value pair arguments mentioned above. Note: calling fitdist without any input arguments will return a cell array of character vectors listing all supported distributions. Distribution names are matched ignoring case, spaces and hyphens, so that 'Extreme Value', 'ExtremeValue' and 'extreme-value' all select the same distribution, and the same set of names is accepted by cdf, pdf, icdf, random, makedist, fitdist and mle. This accepts more names than MATLAB. MATLAB takes the spaced and the squashed spelling but refuses the hyphenated one, so 'Birnbaum-Saunders' and 'Log-Logistic' are errors there; Octave has always accepted them and continues to. MATLAB also accepts 'tLocationScale' in makedist while refusing it in cdf for the same distribution; Octave accepts it, and 'location-scale T', everywhere. Code written against MATLAB's names therefore runs unchanged, but code relying on these names will not port back. See also: makedist # name: # type: sq_string # elements: 1 # length: 39 Create probability distribution object. # name: # type: sq_string # elements: 1 # length: 4 icdf # name: # type: sq_string # elements: 1 # length: 3151 statistics: x = icdf (name, p, A) statistics: x = icdf (name, p, A, B) statistics: x = icdf (name, p, A, B, C) Return the inverse CDF of a univariate distribution evaluated at p. icdf is a wrapper for the univariate quantile distribution functions (iCDF) available in the statistics package. See the corresponding functions' help to learn the signification of the parameters after p. x = icdf (name, p, A) returns the iCDF for the one-parameter distribution family specified by name and the distribution parameter A, evaluated at the values in p. x = icdf (name, p, A, B) returns the iCDF for the two-parameter distribution family specified by name and the distribution parameters A and B, evaluated at the values in p. x = icdf (name, p, A, B, C) returns the iCDF for the three-parameter distribution family specified by name and the distribution parameters A, B, and C, evaluated at the values in p. name must be a char string of the name or the abbreviation of the desired quantile distribution function as listed in the following table. The last column shows the number of required parameters that should be parsed after x to the desired iCDF. Distribution Name Abbreviation Input Parameters 'Beta' 'beta' 2 'Binomial' 'bino' 2 'Birnbaum-Saunders' 'bisa' 2 'Burr' 'burr' 3 'Cauchy' 'cauchy' 2 'Chi-squared' 'chi2' 1 'Extreme Value' 'ev' 2 'Exponential' 'exp' 1 'F-Distribution' 'f' 2 'Gamma' 'gam' 2 'Geometric' 'geo' 1 'Generalized Extreme Value' 'gev' 3 'Generalized Pareto' 'gp' 3 'Gumbel' 'gumbel' 2 'Half-normal' 'hn' 2 'Hypergeometric' 'hyge' 3 'Inverse Gaussian' 'invg' 2 'Laplace' 'laplace' 2 'Logistic' 'logi' 2 'Log-Logistic' 'logl' 2 'Lognormal' 'logn' 2 'Nakagami' 'naka' 2 'Negative Binomial' 'nbin' 2 'Noncentral F-Distribution' 'ncf' 3 'Noncentral Student T' 'nct' 2 'Noncentral Chi-Squared' 'ncx2' 2 'Normal' 'norm' 2 'Poisson' 'poiss' 1 'Rayleigh' 'rayl' 1 'Rician' 'rice' 2 'Student T' 't' 1 'location-scale T' 'tls' 3 'Triangular' 'tri' 3 'Discrete Uniform' 'unid' 1 'Uniform' 'unif' 2 'Von Mises' 'vm' 2 'Weibull' 'wbl' 2 Distribution names are matched ignoring case, spaces and hyphens, so that 'Extreme Value', 'ExtremeValue' and 'extreme-value' all select the same distribution, and the same set of names is accepted by cdf, pdf, icdf, random, makedist, fitdist and mle. This accepts more names than MATLAB. MATLAB takes the spaced and the squashed spelling but refuses the hyphenated one, so 'Birnbaum-Saunders' and 'Log-Logistic' are errors there; Octave has always accepted them and continues to. MATLAB also accepts 'tLocationScale' in makedist while refusing it in cdf for the same distribution; Octave accepts it, and 'location-scale T', everywhere. Code written against MATLAB's names therefore runs unchanged, but code relying on these names will not port back. See also: icdf, pdf, random, betainv, binoinv, bisainv, burrinv, cauchyinv, chi2inv, evinv, expinv, finv, gaminv, geoinv, gevinv, gpinv, gumbelinv, hninv, hygeinv, invginv, laplaceinv, logiinv, loglinv, logninv, nakainv, nbininv, ncfinv, nctinv, ncx2inv, norminv, poissinv, raylinv, riceinv, tinv, tlsinv, triinv, unidinv, unifinv, vminv, wblinv # name: # type: sq_string # elements: 1 # length: 67 Return the inverse CDF of a univariate distribution evaluated at p. # name: # type: sq_string # elements: 1 # length: 8 makedist # name: # type: sq_string # elements: 1 # length: 1369 statistics: pd = makedist (distname) statistics: pd = makedist (distname, Name, Value) statistics: list = makedist Create probability distribution object. pd = makedist (distname) creates a probability distribution object for the distribution specified in distname, using the default parameter values. pd = makedist (distname, Name, Value) also creates a probability distribution object with one or more distribution parameter values specified by Name-Value pair arguments. list = makedist returns a cell array, list, containing a list of the probability distributions that makedist can create. Distribution names are matched ignoring case, spaces and hyphens, so that 'Extreme Value', 'ExtremeValue' and 'extreme-value' all select the same distribution, and the same set of names is accepted by cdf, pdf, icdf, random, makedist, fitdist and mle. This accepts more names than MATLAB. MATLAB takes the spaced and the squashed spelling but refuses the hyphenated one, so 'Birnbaum-Saunders' and 'Log-Logistic' are errors there; Octave has always accepted them and continues to. MATLAB also accepts 'tLocationScale' in makedist while refusing it in cdf for the same distribution; Octave accepts it, and 'location-scale T', everywhere. Code written against MATLAB's names therefore runs unchanged, but code relying on these names will not port back. See also: fitdist # name: # type: sq_string # elements: 1 # length: 39 Create probability distribution object. # name: # type: sq_string # elements: 1 # length: 3 mle # name: # type: sq_string # elements: 1 # length: 4294 statistics: phat = mle (x) statistics: phat = mle (x, Name, Value) statistics: [phat, pci] = mle (...) Compute maximum likelihood estimates. phat = mle (x) returns the maximum likelihood estimates (MLEs) for the parameters of a normal distribution using the sample data in x, which must be a numeric vector of real values. phat = mle (x, Name, Value) returns the MLEs with additional options specified by Name-Value pair arguments listed below. Name Value 'distribution' A character vector specifying the distribution type for which to estimate parameters. 'Ntrials' A scalar specifying the number of trials for the corresponding element of x for the binomial distribution. 'theta' A scalar specifying the location parameter for the generalized Pareto distribution. It defaults to 0 and is not estimated: the data is shifted by it and only k and sigma are returned. 'mu' A scalar specifying the location parameter for the half-normal distribution. 'censoring' A vector of the same size as x indicating censored data in x. By default it is censor = zeros (size (x)). 'frequency' A vector of nonnegative integer counts of the same size as x used as frequency observations. By default it is freq = ones (size (x)). 'alpha' A scalar in the range (0,1), as the significance level for the confidence interval pci. By default it is 0.05 corresponding to 95% confidence intervals. 'options' A structure specifying the control parameters for the iterative algorithm used to compute ML estimates with the fminsearch function. 'pdf' A function handle @(data, p1, p2, ...) to the probability density of a custom distribution, whose parameters are then estimated by maximum likelihood. Requires 'start'. It is mutually exclusive with 'distribution' and with 'logpdf'/'nloglf'. 'cdf' A function handle to the cumulative distribution function of the custom distribution, with the same calling convention as 'pdf'. Required together with 'pdf' for censored or truncated data. 'logpdf' A function handle to the log probability density of a custom distribution, with the same calling convention as 'pdf'. Requires 'start'. 'logsf' A function handle to the log survivor function log (1 - cdf) of the custom distribution, with the same calling convention as 'pdf'. Required together with 'logpdf' for censored data. 'nloglf' A function handle @(params, data, cens, freq) returning the scalar negative log-likelihood of a custom distribution. Requires 'start'. 'start' A vector of initial parameter values for a custom-distribution fit. Required with 'pdf', 'logpdf', or 'nloglf'. 'lowerbound' A scalar or vector of lower bounds for the custom-distribution parameters. By default they are unbounded below. 'upperbound' A scalar or vector of upper bounds for the custom-distribution parameters. By default they are unbounded above. 'truncationbounds' A two-element vector [L U] giving the truncation interval of a custom distribution. Requires a 'cdf' function. 'optimfun' The optimizer for a custom-distribution fit. Only 'fminsearch' is supported; bounded fits are handled by internal reparameterization of the constrained parameters. When a custom distribution is specified through 'pdf', 'logpdf', or 'nloglf', the parameters are estimated by maximizing the likelihood with fminsearch, and the second output pci gives asymptotic normal (Wald) confidence intervals computed from the observed Fisher information at phat (see mlecov). Bounded parameters are estimated on an internally reparameterized unconstrained scale. Distribution names are matched ignoring case, spaces and hyphens, so that 'Extreme Value', 'ExtremeValue' and 'extreme-value' all select the same distribution, and the same set of names is accepted by cdf, pdf, icdf, random, makedist, fitdist and mle. This accepts more names than MATLAB. MATLAB takes the spaced and the squashed spelling but refuses the hyphenated one, so 'Birnbaum-Saunders' and 'Log-Logistic' are errors there; Octave has always accepted them and continues to. MATLAB also accepts 'tLocationScale' in makedist while refusing it in cdf for the same distribution; Octave accepts it, and 'location-scale T', everywhere. Code written against MATLAB's names therefore runs unchanged, but code relying on these names will not port back. See also: mlecov, fitdist, makedist # name: # type: sq_string # elements: 1 # length: 37 Compute maximum likelihood estimates. # name: # type: sq_string # elements: 1 # length: 6 mlecov # name: # type: sq_string # elements: 1 # length: 3651 statistics: acov = mlecov (params, data, Name, Value) Asymptotic covariance matrix of maximum likelihood estimators. acov = mlecov (params, data, ...) returns an approximation to the asymptotic covariance matrix of the maximum likelihood estimators of the parameters of a distribution, evaluated at the parameter values in params for the sample data in data. params is a numeric vector of parameter values (typically the estimates returned by mle or fitdist) and data is a numeric vector of the sample observations. acov is a pxp matrix, where p = numel (params). The distribution is not identified by name; instead it is supplied through Name-Value paired arguments that give function handles to its density, its log density, or its negative log-likelihood. Exactly one of the following three arguments must be specified: Name Value 'pdf' A function handle, f(data, p1, p2, ...), that accepts the sample data as its first argument and the distribution parameters as subsequent scalar arguments, and returns a vector of probability density values, one per observation. 'logpdf' A function handle, f(data, p1, p2, ...), with the same calling convention as 'pdf' but returning the logarithm of the density. 'nloglf' A function handle, nll(params, data, cens, freq), that returns the scalar negative log-likelihood of the whole sample. It receives the current parameter vector, the data, the censoring vector, and the frequency vector, and is responsible for incorporating censoring and frequency itself. 'cdf' A function handle to the cumulative distribution function, with the same calling convention as 'pdf'. It is required together with 'pdf' when the data are censored, so that censored observations can contribute their survival probability. 'logsf' A function handle to the logarithm of the survivor function log (1 - cdf), with the same calling convention as 'pdf'. It is required together with 'logpdf' when the data are censored. 'Censoring' A vector of the same size as data indicating censored observations (nonzero for right-censored). By default no observation is censored. 'Frequency' A vector of nonnegative integer counts of the same size as data, giving the number of times each observation was observed. By default it is ones (size (data)). 'Options' A structure that may contain a 'DerivStep' field specifying the relative finite-difference step used to approximate the Hessian (a positive scalar or a vector the same size as params). The default step is eps ^ (1/4). Computation and numerical behavior. mlecov approximates the covariance matrix as the inverse of the observed Fisher information, that is, the inverse of the Hessian of the aggregate negative log-likelihood of the sample, evaluated by central finite differences at params. The covariance is computed at the supplied params; mlecov does not refit the parameters, so params should be the maximum likelihood estimates for the result to be meaningful. Whichever of 'pdf', 'logpdf', or 'nloglf' is supplied, the Hessian is always formed by differencing the same aggregate negative log-likelihood rather than by differentiating the density itself. This makes the three input forms consistent with one another and is numerically far more stable than differentiating a density; as a consequence acov may differ from other implementations (including MATLAB) in ill-conditioned cases where those differentiate the density directly and return unreliable values or NaN. If the computed Hessian is not positive definite (for example when params is not at a likelihood maximum), a warning is issued and acov is returned as an all-NaN matrix. See also: mle, fitdist, makedist # name: # type: sq_string # elements: 1 # length: 62 Asymptotic covariance matrix of maximum likelihood estimators. # name: # type: sq_string # elements: 1 # length: 3 pdf # name: # type: sq_string # elements: 1 # length: 3129 statistics: y = pdf (name, x, A) statistics: y = pdf (name, x, A, B) statistics: y = pdf (name, x, A, B, C) Return the PDF of a univariate distribution evaluated at x. pdf is a wrapper for the univariate cumulative distribution functions available in the statistics package. See the corresponding functions' help to learn the signification of the parameters after x. y = pdf (name, x, A) returns the CDF for the one-parameter distribution family specified by name and the distribution parameter A, evaluated at the values in x. y = pdf (name, x, A, B) returns the CDF for the two-parameter distribution family specified by name and the distribution parameters A and B, evaluated at the values in x. y = pdf (name, x, A, B, C) returns the CDF for the three-parameter distribution family specified by name and the distribution parameters A, B, and C, evaluated at the values in x. name must be a char string of the name or the abbreviation of the desired cumulative distribution function as listed in the following table. The last column shows the number of required parameters that should be parsed after x to the desired PDF. Distribution Name Abbreviation Input Parameters 'Beta' 'beta' 2 'Binomial' 'bino' 2 'Birnbaum-Saunders' 'bisa' 2 'Burr' 'burr' 3 'Cauchy' 'cauchy' 2 'Chi-squared' 'chi2' 1 'Extreme Value' 'ev' 2 'Exponential' 'exp' 1 'F-Distribution' 'f' 2 'Gamma' 'gam' 2 'Geometric' 'geo' 1 'Generalized Extreme Value' 'gev' 3 'Generalized Pareto' 'gp' 3 'Gumbel' 'gumbel' 2 'Half-normal' 'hn' 2 'Hypergeometric' 'hyge' 3 'Inverse Gaussian' 'invg' 2 'Laplace' 'laplace' 2 'Logistic' 'logi' 2 'Log-Logistic' 'logl' 2 'Lognormal' 'logn' 2 'Nakagami' 'naka' 2 'Negative Binomial' 'nbin' 2 'Noncentral F-Distribution' 'ncf' 3 'Noncentral Student T' 'nct' 2 'Noncentral Chi-Squared' 'ncx2' 2 'Normal' 'norm' 2 'Poisson' 'poiss' 1 'Rayleigh' 'rayl' 1 'Rician' 'rice' 2 'Student T' 't' 1 'location-scale T' 'tls' 3 'Triangular' 'tri' 3 'Discrete Uniform' 'unid' 1 'Uniform' 'unif' 2 'Von Mises' 'vm' 2 'Weibull' 'wbl' 2 Distribution names are matched ignoring case, spaces and hyphens, so that 'Extreme Value', 'ExtremeValue' and 'extreme-value' all select the same distribution, and the same set of names is accepted by cdf, pdf, icdf, random, makedist, fitdist and mle. This accepts more names than MATLAB. MATLAB takes the spaced and the squashed spelling but refuses the hyphenated one, so 'Birnbaum-Saunders' and 'Log-Logistic' are errors there; Octave has always accepted them and continues to. MATLAB also accepts 'tLocationScale' in makedist while refusing it in cdf for the same distribution; Octave accepts it, and 'location-scale T', everywhere. Code written against MATLAB's names therefore runs unchanged, but code relying on these names will not port back. See also: cdf, icdf, random, betapdf, binopdf, bisapdf, burrpdf, cauchypdf, chi2pdf, evpdf, exppdf, fpdf, gampdf, geopdf, gevpdf, gppdf, gumbelpdf, hnpdf, hygepdf, invgpdf, laplacepdf, logipdf, loglpdf, lognpdf, nakapdf, nbinpdf, ncfpdf, nctpdf, ncx2pdf, normpdf, poisspdf, raylpdf, ricepdf, tpdf, tlspdf, tripdf, unidpdf, unifpdf, vmpdf, wblpdf # name: # type: sq_string # elements: 1 # length: 59 Return the PDF of a univariate distribution evaluated at x. # name: # type: sq_string # elements: 1 # length: 6 random # name: # type: sq_string # elements: 1 # length: 3299 statistics: r = random (name, A) statistics: r = random (name, A, B) statistics: r = random (name, A, B, C) statistics: r = random (name, ..., rows, cols) statistics: r = random (name, ..., rows, cols, ...) statistics: r = random (name, ..., [sz]) Random arrays from a given one-, two-, or three-parameter distribution. The variable name must be a string with the name of the distribution to sample from. If this distribution is a one-parameter distribution, A must be supplied, if it is a two-parameter distribution, B must also be supplied, and if it is a three-parameter distribution, C must also be supplied. Any arguments following the distribution parameters will determine the size of the result. When called with a single size argument, return a square matrix with the dimension specified. When called with more than one scalar argument the first two arguments are taken as the number of rows and columns and any further arguments specify additional matrix dimensions. The size may also be specified with a vector of dimensions sz. name must be a char string of the name or the abbreviation of the desired probability distribution function as listed in the following table. The last column shows the required number of parameters that must be passed to the desired *rnd distribution function. Distribution Name Abbreviation Input Parameters 'Beta' 'beta' 2 'Binomial' 'bino' 2 'Birnbaum-Saunders' 'bisa' 2 'Burr' 'burr' 3 'Cauchy' 'cauchy' 2 'Chi-squared' 'chi2' 1 'Extreme Value' 'ev' 2 'Exponential' 'exp' 1 'F-Distribution' 'f' 2 'Gamma' 'gam' 2 'Geometric' 'geo' 1 'Generalized Extreme Value' 'gev' 3 'Generalized Pareto' 'gp' 3 'Gumbel' 'gumbel' 2 'Half-normal' 'hn' 2 'Hypergeometric' 'hyge' 3 'Inverse Gaussian' 'invg' 2 'Laplace' 'laplace' 2 'Logistic' 'logi' 2 'Log-Logistic' 'logl' 2 'Lognormal' 'logn' 2 'Nakagami' 'naka' 2 'Negative Binomial' 'nbin' 2 'Noncentral F-Distribution' 'ncf' 3 'Noncentral Student T' 'nct' 2 'Noncentral Chi-Squared' 'ncx2' 2 'Normal' 'norm' 2 'Poisson' 'poiss' 1 'Rayleigh' 'rayl' 1 'Rician' 'rice' 2 'Student T' 't' 1 'location-scale T' 'tls' 3 'Triangular' 'tri' 3 'Discrete Uniform' 'unid' 1 'Uniform' 'unif' 2 'Von Mises' 'vm' 2 'Weibull' 'wbl' 2 Distribution names are matched ignoring case, spaces and hyphens, so that 'Extreme Value', 'ExtremeValue' and 'extreme-value' all select the same distribution, and the same set of names is accepted by cdf, pdf, icdf, random, makedist, fitdist and mle. This accepts more names than MATLAB. MATLAB takes the spaced and the squashed spelling but refuses the hyphenated one, so 'Birnbaum-Saunders' and 'Log-Logistic' are errors there; Octave has always accepted them and continues to. MATLAB also accepts 'tLocationScale' in makedist while refusing it in cdf for the same distribution; Octave accepts it, and 'location-scale T', everywhere. Code written against MATLAB's names therefore runs unchanged, but code relying on these names will not port back. See also: cdf, icdf, pdf, betarnd, binornd, bisarnd, burrrnd, cauchyrnd, chi2rnd, evrnd, exprnd, frnd, gamrnd, geornd, gevrnd, gprnd, gumbelrnd, hnrnd, hygernd, invgrnd, laplacernd, logirnd, loglrnd, lognrnd, nakarnd, nbinrnd, ncfrnd, nctrnd, ncx2rnd, normrnd, poissrnd, raylrnd, ricernd, trnd, tlsrnd, trirnd, unidrnd, unifrnd, vmrnd, wblrnd # name: # type: sq_string # elements: 1 # length: 71 Random arrays from a given one-, two-, or three-parameter distribution. statistics-release-2.1.2/inst/Distribution_Wrappers/fitdist.m000066400000000000000000001415721527311104300244760ustar00rootroot00000000000000## Copyright (C) 2024-2025 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{pd} =} fitdist (@var{x}, @var{distname}) ## @deftypefnx {statistics} {@var{pd} =} fitdist (@var{x}, @var{distname}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{pdca}, @var{gn}, @var{gl}] =} fitdist (@var{x}, @var{distname}, @qcode{'By'}, @var{groupvar}) ## @deftypefnx {statistics} {[@var{pdca}, @var{gn}, @var{gl}] =} fitdist (@var{x}, @var{distname}, @qcode{'By'}, @var{groupvar}, @var{Name}, @var{Value}) ## ## Create probability distribution object. ## ## @code{@var{pd} = fitdist (@var{x}, @var{distname})} creates a probability ## distribution object by fitting the distribution specified by ## @var{distname} to the data in vector @var{x}. ## ## @code{@var{pd} = fitdist (@var{x}, @var{distname}, @var{Name}, @var{Value})} ## creates the probability distribution object with additional options specified ## by one or more @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'distribution'} @tab A character vector specifying the ## distribution type for which to estimate parameters. ## ## @item @qcode{'Ntrials'} @tab A scalar specifying the number of trials ## for the corresponding element of @var{x} for the binomial distribution. ## ## @item @qcode{'theta'} @tab A scalar specifying the location parameter ## for the generalized Pareto distribution. It defaults to 0, as MATLAB ## assumes it, and is not estimated. ## ## @item @qcode{'mu'} @tab A scalar specifying the location parameter ## for the half-normal distribution. ## ## @item @qcode{'censoring'} @tab A vector of the same size as @var{x} ## indicating censored data in @var{x}. By default it is ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @item @qcode{'frequency'} @tab A vector of nonnegative integer counts of ## the same size as @var{x} used as frequency observations. By default it is ## @qcode{@var{freq} = ones (size (@var{x}))}. ## ## @item @qcode{'alpha'} @tab A scalar in the range @math{(0,1)}, as the ## significance level for the confidence interval @var{pci}. By default it is ## 0.05 corresponding to 95% confidence intervals. ## ## @item @qcode{'options'} @tab A structure specifying the control ## parameters for the iterative algorithm used to compute ML estimates with the ## @code{fminsearch} function. ## @end multitable ## ## @code{[@var{pdca}, @var{gn}, @var{gl}] = fitdist (@var{x}, @var{distname}, ## @qcode{'By'}, @var{groupvar})} creates probability distribution objects by ## fitting the distribution specified by @var{distname} to the data in @var{x} ## based on the grouping variable @var{groupvar}. It returns a cell array of ## fitted probability distribution object, @var{pdca}, a cell array of group ## labels, @var{gn}, and a cell array of grouping variable levels, @var{gl}. ## ## @code{[@var{pdca}, @var{gn}, @var{gl}] = fitdist (@var{x}, @var{distname}, ## @qcode{'By'}, @var{groupvar}, @var{Name}, @var{Value})} returns the same ## output arguments using additional options specified by one or more ## @qcode{Name-Value} pair arguments mentioned above. ## ## Note: calling @code{fitdist} without any input arguments will return a cell ## array of character vectors listing all supported distributions. ## ## Distribution names are matched ignoring case, spaces and hyphens, so that ## @qcode{'Extreme Value'}, @qcode{'ExtremeValue'} and @qcode{'extreme-value'} ## all select the same distribution, and the same set of names is accepted by ## @code{cdf}, @code{pdf}, @code{icdf}, @code{random}, @code{makedist}, ## @code{fitdist} and @code{mle}. ## ## This accepts more names than MATLAB. MATLAB takes the spaced and the ## squashed spelling but refuses the hyphenated one, so ## @qcode{'Birnbaum-Saunders'} and @qcode{'Log-Logistic'} are errors there; ## Octave has always accepted them and continues to. MATLAB also accepts ## @qcode{'tLocationScale'} in @code{makedist} while refusing it in ## @code{cdf} for the same distribution; Octave accepts it, and ## @qcode{'location-scale T'}, everywhere. Code written against MATLAB's ## names therefore runs unchanged, but code relying on these names will not ## port back. ## ## @seealso{makedist} ## @end deftypefn function [varargout] = fitdist (varargin) ## Add list of supported probability distribution objects PDO = {'Beta'; 'Binomial'; 'BirnbaumSaunders'; 'Burr'; 'Exponential'; ... 'ExtremeValue'; 'Gamma'; 'GeneralizedExtremeValue'; ... 'GeneralizedPareto'; 'HalfNormal'; 'InverseGaussian'; ... 'Kernel'; 'Logistic'; 'Loglogistic'; 'Lognormal'; 'Nakagami'; ... 'NegativeBinomial'; 'Normal'; 'Poisson'; 'Rayleigh'; 'Rician'; ... 'Stable'; 'tLocationScale'; 'Weibull'}; ABBR = {'bisa', 'ev', 'gev', 'gp', 'hn', 'invg', 'nbin', 'tls'}; ## Check for input arguments if (nargin == 0) varargout{1} = PDO; return elseif (nargin == 1) error ("fitdist: DISTNAME is required."); else x = varargin{1}; distname = varargin{2}; varargin([1:2]) = []; endif ## Check distribution name if (! (ischar (distname) && size (distname, 1) == 1)) error ("fitdist: DISTNAME must be a character vector."); elseif (! (any (strcmpi (distname, PDO)) || any (strcmpi (distname, ABBR)))) error ("fitdist: unrecognized distribution name."); endif ## Check data in X being a real vector if (! (isvector (x) && isnumeric (x) && isreal (x))) error ("fitdist: X must be a numeric vector of real values."); endif ## Add defaults groupvar = []; censor = zeros (size (x)); freq = ones (size (x)); alpha = 0.05; ntrials = 1; mu = 0; theta = 0; kernel = 'normal'; ksupport = 'unbounded'; kwidth = []; options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; ## Parse extra arguments if (mod (numel (varargin), 2) != 0) error ("fitdist: optional arguments must be in NAME-VALUE pairs."); endif while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'by' groupvar = varargin{2}; if (! isequal (size (x), size (groupvar)) && ! isempty (groupvar)) error (strcat ("fitdist: GROUPVAR argument must have the same", ... " size as the input data in X.")); endif case 'censoring' censor = varargin{2}; if (! isequal (size (x), size (censor))) error (strcat ("fitdist: 'censoring' argument must have the", ... " same size as the input data in X.")); endif case 'frequency' freq = varargin{2}; if (! isequal (size (x), size (freq))) error (strcat ("fitdist: 'frequency' argument must have the", ... " same size as the input data in X.")); endif if (any (freq != round (freq)) || any (freq < 0)) error (strcat ("fitdist: 'frequency' argument must contain", ... " non-negative integer values.")); endif case 'alpha' alpha = varargin{2}; if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("fitdist: invalid value for 'alpha' argument."); endif case 'ntrials' ntrials = varargin{2}; if (! (isscalar (ntrials) && isreal (ntrials) && ntrials > 0 && fix (ntrials) == ntrials)) error (strcat ("fitdist: 'ntrials' argument must be a positive", ... " integer scalar value.")); endif case {'mu'} mu = varargin{2}; case {'theta'} theta = varargin{2}; case 'options' options = varargin{2}; if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("fitdist: 'options' argument must be a", ... " structure compatible for 'fminsearch'.")); endif case 'kernel' kernel = varargin{2}; case 'support' ksupport = varargin{2}; case 'width' kwidth = varargin{2}; otherwise error ("fitdist: unknown parameter name."); endswitch varargin([1:2]) = []; endwhile ## Handle missing values is_nan = isnan (x) | isnan (censor) | isnan (freq); if (any (is_nan)) x(is_nan) = []; censor(is_nan) = []; freq(is_nan) = []; endif if (isempty (x)) error ("fitdist: no data in X to fit a '%s' distribution.", distname); endif ## Handle group variable if (isempty (groupvar) && nargout > 1) error ("fitdist: must define GROUPVAR for more than one output arguments."); endif if (! isempty (groupvar)) [g, gn, gl] = grp2idx (groupvar); groups = numel (gn); if (any (is_nan)) groupvar(is_nan) = []; g = grp2idx (groupvar); endif endif ## Warning message for no group data msg = 'fitdist: no data in group ''%s'' to fit a ''%s'' distribution.'; ## Switch to selected distribution switch (__distname_key__ (distname)) case 'beta' if (isempty (groupvar)) varargout{1} = prob.BetaDistribution.fit (x, alpha, freq, options); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.BetaDistribution.fit (x_i, alpha, f_i, options); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case 'binomial' if (any (x > ntrials)) error ("fitdist: invalid NTRIALS value for Binomial distribution.") endif if (isempty (groupvar)) varargout{1} = prob.BinomialDistribution.fit (x, ntrials, alpha, freq); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.BinomialDistribution.fit (x_i, ntrials, alpha, f_i); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case {'birnbaumsaunders', 'bisa'} if (isempty (groupvar)) varargout{1} = prob.BirnbaumSaundersDistribution.fit ... (x, alpha, censor, freq, options); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); c_i = censor(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.BirnbaumSaundersDistribution.fit ... (x_i, alpha, c_i, f_i, options); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case 'burr' if (isempty (groupvar)) varargout{1} = prob.BurrDistribution.fit (x, alpha, censor, freq, options); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); c_i = censor(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.BurrDistribution.fit (x_i, alpha, c_i, f_i, options); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case 'exponential' if (isempty (groupvar)) varargout{1} = prob.ExponentialDistribution.fit (x, alpha, censor, freq); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); c_i = censor(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.ExponentialDistribution.fit (x_i, alpha, c_i, f_i); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case {'extremevalue', 'ev'} if (isempty (groupvar)) varargout{1} = prob.ExtremeValueDistribution.fit ... (x, alpha, censor, freq, options); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); c_i = censor(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.ExtremeValueDistribution.fit (x_i, alpha, c_i, f_i, options); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case 'gamma' if (isempty (groupvar)) varargout{1} = prob.GammaDistribution.fit (x, alpha, censor, freq, options); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); c_i = censor(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.GammaDistribution.fit (x_i, alpha, c_i, f_i, options); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case {'generalizedextremevalue', 'gev'} if (isempty (groupvar)) varargout{1} = prob.GeneralizedExtremeValueDistribution.fit ... (x, alpha, freq, options); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.GeneralizedExtremeValueDistribution.fit ... (x_i, alpha, f_i, options); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case {'generalizedpareto', 'gp'} if (any (x - theta < 0)) error (strcat ("fitdist: invalid THETA value for generalized", ... " Pareto distribution.")); endif if (isempty (groupvar)) varargout{1} = prob.GeneralizedParetoDistribution.fit ... (x, theta, alpha, freq, options); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.GeneralizedParetoDistribution.fit ... (x_i, theta, alpha, f_i, options); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case {'halfnormal', 'hn'} if (any (x - mu < 0)) error ("fitdist: invalid MU value for half-normal distribution."); endif if (isempty (groupvar)) varargout{1} = prob.HalfNormalDistribution.fit (x, mu, alpha, freq); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.HalfNormalDistribution.fit (x_i, mu, alpha, f_i); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case {'inversegaussian', 'invg'} if (isempty (groupvar)) varargout{1} = prob.InverseGaussianDistribution.fit ... (x, alpha, censor, freq, options); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); c_i = censor(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.InverseGaussianDistribution.fit ... (x_i, alpha, c_i, f_i, options); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case 'kernel' if (any (censor != 0)) error ("fitdist: censoring is not supported for a 'Kernel' distribution."); endif if (isempty (groupvar)) varargout{1} = prob.KernelDistribution.fit ... (x, kernel, ksupport, kwidth, freq); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.KernelDistribution.fit ... (x_i, kernel, ksupport, kwidth, f_i); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case 'logistic' if (isempty (groupvar)) varargout{1} = prob.LogisticDistribution.fit ... (x, alpha, censor, freq, options); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); c_i = censor(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.LogisticDistribution.fit (x_i, alpha, c_i, f_i, options); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case 'loglogistic' if (isempty (groupvar)) varargout{1} = prob.LoglogisticDistribution.fit ... (x, alpha, censor, freq, options); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); c_i = censor(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.LoglogisticDistribution.fit (x_i, alpha, c_i, f_i, options); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case 'lognormal' if (isempty (groupvar)) varargout{1} = prob.LognormalDistribution.fit ... (x, alpha, censor, freq, options); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); c_i = censor(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.LognormalDistribution.fit (x_i, alpha, c_i, f_i, options); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case 'nakagami' if (isempty (groupvar)) varargout{1} = prob.NakagamiDistribution.fit ... (x, alpha, censor, freq, options); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); c_i = censor(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.NakagamiDistribution.fit (x_i, alpha, c_i, f_i, options); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case {'negativebinomial', 'nbin'} if (isempty (groupvar)) varargout{1} = prob.NegativeBinomialDistribution.fit ... (x, alpha, freq, options); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.NegativeBinomialDistribution.fit (x_i, alpha, f_i, options); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case 'normal' if (isempty (groupvar)) varargout{1} = prob.NormalDistribution.fit (x, alpha, censor, freq, options); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); c_i = censor(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.NormalDistribution.fit (x_i, alpha, c_i, f_i, options); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case 'poisson' if (isempty (groupvar)) varargout{1} = prob.PoissonDistribution.fit (x, alpha, freq); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.PoissonDistribution.fit (x_i, alpha, f_i); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case 'rayleigh' if (isempty (groupvar)) varargout{1} = prob.RayleighDistribution.fit (x, alpha, censor, freq); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); c_i = censor(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.RayleighDistribution.fit (x_i, alpha, c_i, f_i); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case 'rician' if (isempty (groupvar)) varargout{1} = prob.RicianDistribution.fit (x, alpha, censor, freq, options); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); c_i = censor(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.RicianDistribution.fit (x_i, alpha, c_i, f_i, options); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case 'stable' if (any (censor != 0)) error ("fitdist: censoring is not supported for a 'Stable' distribution."); endif if (isempty (groupvar)) varargout{1} = prob.StableDistribution.fit (x, alpha, freq, options); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.StableDistribution.fit (x_i, alpha, f_i, options); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case {'tlocationscale', 'tls'} if (isempty (groupvar)) varargout{1} = prob.tLocationScaleDistribution.fit ... (x, alpha, censor, freq, options); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); c_i = censor(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.tLocationScaleDistribution.fit ... (x_i, alpha, c_i, f_i, options); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif case 'weibull' if (isempty (groupvar)) varargout{1} = prob.WeibullDistribution.fit (x, alpha, censor, freq, options); else pd = cell (1, groups); for i = 1:groups x_i = x(g == i); c_i = censor(g == i); f_i = freq(g == i); if (isempty (x_i)) pd{i} = []; warning (msg, gn{i}, distname); else pd{i} = prob.WeibullDistribution.fit (x_i, alpha, c_i, f_i, options); endif endfor varargout{1} = pd; varargout{2} = gn; varargout{3} = gl; endif endswitch endfunction ## Test output %!test ## fitdist returns a fitted prob.KernelDistribution object %! x = [2.1 0.3 1.2 -0.7 0.9 1.5 2.8 0.1 0.4 1.1 3.2 0.6 2.0 0.9 1.7]'; %! pd = fitdist (x, 'Kernel'); %! assert_equal (class (pd), 'prob.KernelDistribution'); %! assert_equal (pd.Kernel, 'normal'); %! assert_equal (pd.Bandwidth, 0.639566, 1e-4); %! assert_equal (pd.InputData.data, x); %!test ## grouped kernel fit returns a cell of prob.KernelDistribution objects %! x = [2.1 0.3 1.2 -0.7 0.9 1.5 2.8 0.1 0.4 1.1 3.2 0.6 2.0 0.9 1.7]'; %! [pd, gn] = fitdist (x, 'Kernel', 'By', [ones(8, 1); 2*ones(7, 1)]); %! assert_equal (numel (pd), 2); %! assert_equal (class (pd{1}), 'prob.KernelDistribution'); %! assert_equal (class (pd{2}), 'prob.KernelDistribution'); %!test %! x = betarnd (1, 1, 100, 1); %! pd = fitdist (x, 'Beta'); %! [phat, pci] = betafit (x); %! assert_equal ([pd.a, pd.b], phat); %! assert_equal (paramci (pd), pci); %!test %! x1 = betarnd (1, 1, 100, 1); %! x2 = betarnd (5, 2, 100, 1); %! pd = fitdist ([x1; x2], 'Beta', 'By', [ones(100, 1); 2*ones(100, 1)]); %! [phat, pci] = betafit (x1); %! assert_equal ([pd{1}.a, pd{1}.b], phat); %! assert_equal (paramci (pd{1}), pci); %! [phat, pci] = betafit (x2); %! assert_equal ([pd{2}.a, pd{2}.b], phat); %! assert_equal (paramci (pd{2}), pci); %!warning ... %! fitdist ([betarnd(1, 1, 100, 1); nan(100, 1)], 'Beta', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! N = 1; %! x = binornd (N, 0.5, 100, 1); %! pd = fitdist (x, 'binomial'); %! [phat, pci] = binofit (sum (x), numel (x)); %! assert_equal ([pd.N, pd.p], [N, phat]); %! assert_equal (paramci (pd), [N, pci(1); N, pci(2)]); %!test %! N = 3; %! x = binornd (N, 0.4, 100, 1); %! pd = fitdist (x, 'binomial', 'ntrials', N); %! [phat, pci] = binofit (sum (x), numel (x) * N); %! assert_equal ([pd.N, pd.p], [N, phat]); %! assert_equal (paramci (pd), [N, pci(1); N, pci(2)]); %!test %! N = 1; %! x1 = binornd (N, 0.5, 100, 1); %! x2 = binornd (N, 0.7, 100, 1); %! pd = fitdist ([x1; x2], 'binomial', 'By', [ones(100, 1); 2*ones(100, 1)]); %! [phat, pci] = binofit (sum (x1), numel (x1)); %! assert_equal ([pd{1}.N, pd{1}.p], [N, phat]); %! assert_equal (paramci (pd{1}), [N, pci(1); N, pci(2)]); %! [phat, pci] = binofit (sum (x2), numel (x2)); %! assert_equal ([pd{2}.N, pd{2}.p], [N, phat]); %! assert_equal (paramci (pd{2}), [N, pci(1); N, pci(2)]); %!warning ... %! fitdist ([binornd(1, 0.5, 100, 1); nan(100, 1)], 'binomial', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! N = 5; %! x1 = binornd (N, 0.5, 100, 1); %! x2 = binornd (N, 0.8, 100, 1); %! pd = fitdist ([x1; x2], 'binomial', 'ntrials', N, ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %! [phat, pci] = binofit (sum (x1), numel (x1) * N); %! assert_equal ([pd{1}.N, pd{1}.p], [N, phat]); %! assert_equal (paramci (pd{1}), [N, pci(1); N, pci(2)]); %! [phat, pci] = binofit (sum (x2), numel (x2) * N); %! assert_equal ([pd{2}.N, pd{2}.p], [N, phat]); %! assert_equal (paramci (pd{2}), [N, pci(1); N, pci(2)]); %!warning ... %! fitdist ([binornd(5, 0.5, 100, 1); nan(100, 1)], 'binomial', 'ntrials', 5, ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! x = bisarnd (1, 1, 100, 1); %! pd = fitdist (x, 'BirnbaumSaunders'); %! [phat, pci] = bisafit (x); %! assert_equal ([pd.beta, pd.gamma], phat); %! assert_equal (paramci (pd), pci); %!test %! x1 = bisarnd (1, 1, 100, 1); %! x2 = bisarnd (5, 2, 100, 1); %! pd = fitdist ([x1; x2], 'bisa', 'By', [ones(100,1); 2*ones(100,1)]); %! [phat, pci] = bisafit (x1); %! assert_equal ([pd{1}.beta, pd{1}.gamma], phat); %! assert_equal (paramci (pd{1}), pci); %! [phat, pci] = bisafit (x2); %! assert_equal ([pd{2}.beta, pd{2}.gamma], phat); %! assert_equal (paramci (pd{2}), pci); %!warning ... %! fitdist ([bisarnd(1, 1, 100, 1); nan(100, 1)], 'bisa', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! x = burrrnd (1, 2, 1, 100, 1); %! pd = fitdist (x, 'Burr'); %! [phat, pci] = burrfit (x); %! assert_equal ([pd.alpha, pd.c, pd.k], phat); %! assert_equal (paramci (pd), pci); %!test %! rand ('seed', 4); # for reproducibility %! x1 = burrrnd (1, 2, 1, 100, 1); %! rand ('seed', 3); # for reproducibility %! x2 = burrrnd (1, 0.5, 2, 100, 1); %! pd = fitdist ([x1; x2], 'burr', 'By', [ones(100,1); 2*ones(100,1)]); %! [phat, pci] = burrfit (x1); %! assert_equal ([pd{1}.alpha, pd{1}.c, pd{1}.k], phat); %! assert_equal (paramci (pd{1}), pci); %! [phat, pci] = burrfit (x2); %! assert_equal ([pd{2}.alpha, pd{2}.c, pd{2}.k], phat); %! assert_equal (paramci (pd{2}), pci); %!warning ... %! fitdist ([burrrnd(1, 2, 1, 100, 1); nan(100, 1)], 'burr', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! x = exprnd (1, 100, 1); %! pd = fitdist (x, 'exponential'); %! [muhat, muci] = expfit (x); %! assert_equal ([pd.mu], muhat); %! assert_equal (paramci (pd), muci); %!test %! x1 = exprnd (1, 100, 1); %! x2 = exprnd (5, 100, 1); %! pd = fitdist ([x1; x2], 'exponential', 'By', [ones(100,1); 2*ones(100,1)]); %! [muhat, muci] = expfit (x1); %! assert_equal ([pd{1}.mu], muhat); %! assert_equal (paramci (pd{1}), muci); %! [muhat, muci] = expfit (x2); %! assert_equal ([pd{2}.mu], muhat); %! assert_equal (paramci (pd{2}), muci); %!warning ... %! fitdist ([exprnd(1, 100, 1); nan(100, 1)], 'exponential', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! x = evrnd (1, 1, 100, 1); %! pd = fitdist (x, 'ev'); %! [phat, pci] = evfit (x); %! assert_equal ([pd.mu, pd.sigma], phat); %! assert_equal (paramci (pd), pci); %!test %! x1 = evrnd (1, 1, 100, 1); %! x2 = evrnd (5, 2, 100, 1); %! pd = fitdist ([x1; x2], 'extremevalue', 'By', [ones(100,1); 2*ones(100,1)]); %! [phat, pci] = evfit (x1); %! assert_equal ([pd{1}.mu, pd{1}.sigma], phat); %! assert_equal (paramci (pd{1}), pci); %! [phat, pci] = evfit (x2); %! assert_equal ([pd{2}.mu, pd{2}.sigma], phat); %! assert_equal (paramci (pd{2}), pci); %!warning ... %! fitdist ([evrnd(1, 1, 100, 1); nan(100, 1)], 'extremevalue', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! x = gamrnd (1, 1, 100, 1); %! pd = fitdist (x, 'Gamma'); %! [phat, pci] = gamfit (x); %! assert_equal ([pd.a, pd.b], phat); %! assert_equal (paramci (pd), pci); %!test %! x1 = gamrnd (1, 1, 100, 1); %! x2 = gamrnd (5, 2, 100, 1); %! pd = fitdist ([x1; x2], 'Gamma', 'By', [ones(100,1); 2*ones(100,1)]); %! [phat, pci] = gamfit (x1); %! assert_equal ([pd{1}.a, pd{1}.b], phat); %! assert_equal (paramci (pd{1}), pci); %! [phat, pci] = gamfit (x2); %! assert_equal ([pd{2}.a, pd{2}.b], phat); %! assert_equal (paramci (pd{2}), pci); %!warning ... %! fitdist ([gamrnd(1, 1, 100, 1); nan(100, 1)], 'Gamma', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! rand ('seed', 4); # for reproducibility %! x = gevrnd (-0.5, 1, 2, 1000, 1); %! pd = fitdist (x, 'generalizedextremevalue'); %! [phat, pci] = gevfit (x); %! assert_equal ([pd.k, pd.sigma, pd.mu], phat); %! assert_equal (paramci (pd), pci); %!test %! rand ('seed', 5); # for reproducibility %! x1 = gevrnd (-0.5, 1, 2, 1000, 1); %! rand ('seed', 9); # for reproducibility %! x2 = gevrnd (0, 1, -4, 1000, 1); %! pd = fitdist ([x1; x2], 'gev', 'By', [ones(1000,1); 2*ones(1000,1)]); %! [phat, pci] = gevfit (x1); %! assert_equal ([pd{1}.k, pd{1}.sigma, pd{1}.mu], phat); %! assert_equal (paramci (pd{1}), pci); %! [phat, pci] = gevfit (x2); %! assert_equal ([pd{2}.k, pd{2}.sigma, pd{2}.mu], phat); %! assert_equal (paramci (pd{2}), pci); %!warning ... %! fitdist ([gevrnd(-0.5, 1, 2, 1000, 1); nan(1000, 1)], 'gev', ... %! 'By', [ones(1000, 1); 2*ones(1000, 1)]); %!test %! x = gprnd (1, 1, 1, 100, 1); %! pd = fitdist (x, 'GeneralizedPareto', 'theta', 1); %! [phat, pci] = gpfit (x - 1); %! assert_equal ([pd.k, pd.sigma, pd.theta], [phat, 1]); %! assert_equal (paramci (pd), [pci, [1; 1]]); %!test %! x = gprnd (1, 1, 2, 100, 1); %! pd = fitdist (x, 'GeneralizedPareto', 'theta', 2); %! [phat, pci] = gpfit (x - 2); %! assert_equal ([pd.k, pd.sigma, pd.theta], [phat, 2]); %! assert_equal (paramci (pd), [pci, [2; 2]]); %!test %! x1 = gprnd (1, 1, 1, 100, 1); %! x2 = gprnd (0, 2, 1, 100, 1); %! pd = fitdist ([x1; x2], 'gp', 'theta', 1, ... %! 'By', [ones(100,1); 2*ones(100,1)]); %! [phat, pci] = gpfit (x1 - 1); %! assert_equal ([pd{1}.k, pd{1}.sigma, pd{1}.theta], [phat, 1]); %! assert_equal (paramci (pd{1}), [pci, [1; 1]]); %! [phat, pci] = gpfit (x2 - 1); %! assert_equal ([pd{2}.k, pd{2}.sigma, pd{2}.theta], [phat, 1]); %! assert_equal (paramci (pd{2}), [pci, [1; 1]]); %!warning ... %! fitdist ([gprnd(1, 1, 1, 100, 1); nan(100, 1)], 'gp', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! x1 = gprnd (3, 2, 2, 100, 1); %! x2 = gprnd (2, 3, 2, 100, 1); %! pd = fitdist ([x1; x2], 'GeneralizedPareto', 'theta', 2, ... %! 'By', [ones(100,1); 2*ones(100,1)]); %! [phat, pci] = gpfit (x1 - 2); %! assert_equal ([pd{1}.k, pd{1}.sigma, pd{1}.theta], [phat, 2]); %! assert_equal (paramci (pd{1}), [pci, [2; 2]]); %! [phat, pci] = gpfit (x2 - 2); %! assert_equal ([pd{2}.k, pd{2}.sigma, pd{2}.theta], [phat, 2]); %! assert_equal (paramci (pd{2}), [pci, [2; 2]]); %!warning ... %! fitdist ([gprnd(3, 2, 2, 100, 1); nan(100, 1)], 'gp', 'theta', 2, ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! x = hnrnd (0, 1, 100, 1); %! pd = fitdist (x, 'HalfNormal'); %! [phat, pci] = hnfit (x, 0); %! assert_equal ([pd.mu, pd.sigma], phat); %! assert_equal (paramci (pd), pci); %!test %! x = hnrnd (1, 1, 100, 1); %! pd = fitdist (x, 'HalfNormal', 'mu', 1); %! [phat, pci] = hnfit (x, 1); %! assert_equal ([pd.mu, pd.sigma], phat); %! assert_equal (paramci (pd), pci); %!test %! x1 = hnrnd (0, 1, 100, 1); %! x2 = hnrnd (0, 2, 100, 1); %! pd = fitdist ([x1; x2], 'HalfNormal', 'By', [ones(100,1); 2*ones(100,1)]); %! [phat, pci] = hnfit (x1, 0); %! assert_equal ([pd{1}.mu, pd{1}.sigma], phat); %! assert_equal (paramci (pd{1}), pci); %! [phat, pci] = hnfit (x2, 0); %! assert_equal ([pd{2}.mu, pd{2}.sigma], phat); %! assert_equal (paramci (pd{2}), pci); %!warning ... %! fitdist ([hnrnd(0, 1, 100, 1); nan(100, 1)], 'HalfNormal', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! x1 = hnrnd (2, 1, 100, 1); %! x2 = hnrnd (2, 2, 100, 1); %! pd = fitdist ([x1; x2], 'HalfNormal', 'mu', 2, ... %! 'By', [ones(100,1); 2*ones(100,1)]); %! [phat, pci] = hnfit (x1, 2); %! assert_equal ([pd{1}.mu, pd{1}.sigma], phat); %! assert_equal (paramci (pd{1}), pci); %! [phat, pci] = hnfit (x2, 2); %! assert_equal ([pd{2}.mu, pd{2}.sigma], phat); %! assert_equal (paramci (pd{2}), pci); %!warning ... %! fitdist ([hnrnd(2, 1, 100, 1); nan(100, 1)], 'HalfNormal', 'mu', 2, ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! x = invgrnd (1, 1, 100, 1); %! pd = fitdist (x, 'InverseGaussian'); %! [phat, pci] = invgfit (x); %! assert_equal ([pd.mu, pd.lambda], phat); %! assert_equal (paramci (pd), pci); %!test %! x1 = invgrnd (1, 1, 100, 1); %! x2 = invgrnd (5, 2, 100, 1); %! pd = fitdist ([x1; x2], 'InverseGaussian', 'By', [ones(100,1); 2*ones(100,1)]); %! [phat, pci] = invgfit (x1); %! assert_equal ([pd{1}.mu, pd{1}.lambda], phat); %! assert_equal (paramci (pd{1}), pci); %! [phat, pci] = invgfit (x2); %! assert_equal ([pd{2}.mu, pd{2}.lambda], phat); %! assert_equal (paramci (pd{2}), pci); %!warning ... %! fitdist ([invgrnd(1, 1, 100, 1); nan(100, 1)], 'InverseGaussian', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! x = logirnd (1, 1, 100, 1); %! pd = fitdist (x, 'logistic'); %! [phat, pci] = logifit (x); %! assert_equal ([pd.mu, pd.sigma], phat); %! assert_equal (paramci (pd), pci); %!test %! x1 = logirnd (1, 1, 100, 1); %! x2 = logirnd (5, 2, 100, 1); %! pd = fitdist ([x1; x2], 'logistic', 'By', [ones(100,1); 2*ones(100,1)]); %! [phat, pci] = logifit (x1); %! assert_equal ([pd{1}.mu, pd{1}.sigma], phat); %! assert_equal (paramci (pd{1}), pci); %! [phat, pci] = logifit (x2); %! assert_equal ([pd{2}.mu, pd{2}.sigma], phat); %! assert_equal (paramci (pd{2}), pci); %!warning ... %! fitdist ([logirnd(1, 1, 100, 1); nan(100, 1)], 'logistic', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! x = loglrnd (1, 1, 100, 1); %! pd = fitdist (x, 'loglogistic'); %! [phat, pci] = loglfit (x); %! assert_equal ([pd.mu, pd.sigma], phat); %! assert_equal (paramci (pd), pci); %!test %! x1 = loglrnd (1, 1, 100, 1); %! x2 = loglrnd (5, 2, 100, 1); %! pd = fitdist ([x1; x2], 'loglogistic', 'By', [ones(100,1); 2*ones(100,1)]); %! [phat, pci] = loglfit (x1); %! assert_equal ([pd{1}.mu, pd{1}.sigma], phat); %! assert_equal (paramci (pd{1}), pci); %! [phat, pci] = loglfit (x2); %! assert_equal ([pd{2}.mu, pd{2}.sigma], phat); %! assert_equal (paramci (pd{2}), pci); %!warning ... %! fitdist ([loglrnd(1, 1, 100, 1); nan(100, 1)], 'loglogistic', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! x = lognrnd (1, 1, 100, 1); %! pd = fitdist (x, 'lognormal'); %! [phat, pci] = lognfit (x); %! assert_equal ([pd.mu, pd.sigma], phat); %! assert_equal (paramci (pd), pci); %!test %! x1 = lognrnd (1, 1, 100, 1); %! x2 = lognrnd (5, 2, 100, 1); %! pd = fitdist ([x1; x2], 'lognormal', 'By', [ones(100,1); 2*ones(100,1)]); %! [phat, pci] = lognfit (x1); %! assert_equal ([pd{1}.mu, pd{1}.sigma], phat); %! assert_equal (paramci (pd{1}), pci); %! [phat, pci] = lognfit (x2); %! assert_equal ([pd{2}.mu, pd{2}.sigma], phat); %! assert_equal (paramci (pd{2}), pci); %!warning ... %! fitdist ([lognrnd(1, 1, 100, 1); nan(100, 1)], 'lognormal', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! x = nakarnd (2, 0.5, 100, 1); %! pd = fitdist (x, 'Nakagami'); %! [phat, pci] = nakafit (x); %! assert_equal ([pd.mu, pd.omega], phat); %! assert_equal (paramci (pd), pci); %!test %! x1 = nakarnd (2, 0.5, 100, 1); %! x2 = nakarnd (5, 0.8, 100, 1); %! pd = fitdist ([x1; x2], 'Nakagami', 'By', [ones(100,1); 2*ones(100,1)]); %! [phat, pci] = nakafit (x1); %! assert_equal ([pd{1}.mu, pd{1}.omega], phat); %! assert_equal (paramci (pd{1}), pci); %! [phat, pci] = nakafit (x2); %! assert_equal ([pd{2}.mu, pd{2}.omega], phat); %! assert_equal (paramci (pd{2}), pci); %!warning ... %! fitdist ([nakarnd(2, 0.5, 100, 1); nan(100, 1)], 'Nakagami', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! randp ('seed', 123); %! randg ('seed', 321); %! x = nbinrnd (2, 0.5, 100, 1); %! pd = fitdist (x, 'negativebinomial'); %! [phat, pci] = nbinfit (x); %! assert_equal ([pd.R, pd.P], phat); %! assert_equal (paramci (pd), pci); %!test %! randp ('seed', 345); %! randg ('seed', 543); %! x1 = nbinrnd (2, 0.5, 100, 1); %! randp ('seed', 432); %! randg ('seed', 234); %! x2 = nbinrnd (5, 0.8, 100, 1); %! pd = fitdist ([x1; x2], 'nbin', 'By', [ones(100,1); 2*ones(100,1)]); %! [phat, pci] = nbinfit (x1); %! assert_equal ([pd{1}.R, pd{1}.P], phat); %! assert_equal (paramci (pd{1}), pci); %! [phat, pci] = nbinfit (x2); %! assert_equal ([pd{2}.R, pd{2}.P], phat); %! assert_equal (paramci (pd{2}), pci); %!warning ... %! fitdist ([nbinrnd(2, 0.5, 100, 1); nan(100, 1)], 'nbin', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! x = normrnd (1, 1, 100, 1); %! pd = fitdist (x, 'normal'); %! [muhat, sigmahat, muci, sigmaci] = normfit (x); %! assert_equal ([pd.mu, pd.sigma], [muhat, sigmahat]); %! assert_equal (paramci (pd), [muci, sigmaci]); %!test %! x1 = normrnd (1, 1, 100, 1); %! x2 = normrnd (5, 2, 100, 1); %! pd = fitdist ([x1; x2], 'normal', 'By', [ones(100,1); 2*ones(100,1)]); %! [muhat, sigmahat, muci, sigmaci] = normfit (x1); %! assert_equal ([pd{1}.mu, pd{1}.sigma], [muhat, sigmahat]); %! assert_equal (paramci (pd{1}), [muci, sigmaci]); %! [muhat, sigmahat, muci, sigmaci] = normfit (x2); %! assert_equal ([pd{2}.mu, pd{2}.sigma], [muhat, sigmahat]); %! assert_equal (paramci (pd{2}), [muci, sigmaci]); %!warning ... %! fitdist ([normrnd(1, 1, 100, 1); nan(100, 1)], 'normal', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! x = poissrnd (1, 100, 1); %! pd = fitdist (x, 'poisson'); %! [phat, pci] = poissfit (x); %! assert_equal (pd.lambda, phat); %! assert_equal (paramci (pd), pci); %!test %! x1 = poissrnd (1, 100, 1); %! x2 = poissrnd (5, 100, 1); %! pd = fitdist ([x1; x2], 'poisson', 'By', [ones(100,1); 2*ones(100,1)]); %! [phat, pci] = poissfit (x1); %! assert_equal (pd{1}.lambda, phat); %! assert_equal (paramci (pd{1}), pci); %! [phat, pci] = poissfit (x2); %! assert_equal (pd{2}.lambda, phat); %! assert_equal (paramci (pd{2}), pci); %!warning ... %! fitdist ([poissrnd(1, 100, 1); nan(100, 1)], 'poisson', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! x = raylrnd (1, 100, 1); %! pd = fitdist (x, 'rayleigh'); %! [phat, pci] = raylfit (x); %! assert_equal (pd.B, phat); %! assert_equal (paramci (pd), pci); %!test %! x1 = raylrnd (1, 100, 1); %! x2 = raylrnd (5, 100, 1); %! pd = fitdist ([x1; x2], 'rayleigh', 'By', [ones(100,1); 2*ones(100,1)]); %! [phat, pci] = raylfit (x1); %! assert_equal (pd{1}.B, phat); %! assert_equal (paramci (pd{1}), pci); %! [phat, pci] = raylfit (x2); %! assert_equal (pd{2}.B, phat); %! assert_equal (paramci (pd{2}), pci); %!warning ... %! fitdist ([raylrnd(1, 100, 1); nan(100, 1)], 'rayleigh', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! x = ricernd (1, 1, 100, 1); %! pd = fitdist (x, 'rician'); %! [phat, pci] = ricefit (x); %! assert_equal ([pd.s, pd.sigma], phat); %! assert_equal (paramci (pd), pci); %!test %! x1 = ricernd (1, 1, 100, 1); %! x2 = ricernd (5, 2, 100, 1); %! pd = fitdist ([x1; x2], 'rician', 'By', [ones(100,1); 2*ones(100,1)]); %! [phat, pci] = ricefit (x1); %! assert_equal ([pd{1}.s, pd{1}.sigma], phat); %! assert_equal (paramci (pd{1}), pci); %! [phat, pci] = ricefit (x2); %! assert_equal ([pd{2}.s, pd{2}.sigma], phat); %! assert_equal (paramci (pd{2}), pci); %!warning ... %! fitdist ([ricernd(1, 1, 100, 1); nan(100, 1)], 'rician', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test ## fitdist returns a fitted prob.StableDistribution object %! rand ("seed", 2718); %! randn ("seed", 2718); %! x = stblrnd (1.5, 0.5, 2, 1, 150, 1); %! pd = fitdist (x, 'Stable'); %! assert_equal (class (pd), 'prob.StableDistribution'); %! assert_equal (! any (pd.ParameterIsFixed, 'all'), true); %! assert_equal (pd.alpha, 1.5, 0.4); %! assert_equal (pd.gam, 2, 0.5); %!test %! x = tlsrnd (0, 1, 1, 100, 1); %! pd = fitdist (x, 'tlocationscale'); %! [phat, pci] = tlsfit (x); %! assert_equal ([pd.mu, pd.sigma, pd.nu], phat); %! assert_equal (paramci (pd), pci); %!test %! x1 = tlsrnd (0, 1, 1, 100, 1); %! x2 = tlsrnd (5, 2, 1, 100, 1); %! pd = fitdist ([x1; x2], 'tlocationscale', 'By', [ones(100,1); 2*ones(100,1)]); %! [phat, pci] = tlsfit (x1); %! assert_equal ([pd{1}.mu, pd{1}.sigma, pd{1}.nu], phat); %! assert_equal (paramci (pd{1}), pci); %! [phat, pci] = tlsfit (x2); %! assert_equal ([pd{2}.mu, pd{2}.sigma, pd{2}.nu], phat); %! assert_equal (paramci (pd{2}), pci); %!warning ... %! fitdist ([tlsrnd(0, 1, 1, 100, 1); nan(100, 1)], 'tlocationscale', ... %! 'By', [ones(100, 1); 2*ones(100, 1)]); %!test %! x = [1 2 3 4 5]; %! pd = fitdist (x, 'weibull'); %! [phat, pci] = wblfit (x); %! assert_equal ([pd.A, pd.B], phat); %! assert_equal (paramci (pd), pci); %!test %! x = [1 2 3 4 5 6 7 8 9 10]; %! pd = fitdist (x, 'weibull', 'By', [1 1 1 1 1 2 2 2 2 2]); %! [phat, pci] = wblfit (x(1:5)); %! assert_equal ([pd{1}.A, pd{1}.B], phat); %! assert_equal (paramci (pd{1}), pci); %! [phat, pci] = wblfit (x(6:10)); %! assert_equal ([pd{2}.A, pd{2}.B], phat); %! assert_equal (paramci (pd{2}), pci); %!warning ... %! fitdist ([1 2 3 4 5 NaN NaN NaN NaN NaN], 'weibull', 'By', [1 1 1 1 1 2 2 2 2 2]); ## Test input validation %!error fitdist (1) %!error fitdist (1, ['as';'sd']) %!error fitdist (1, 'some') %!error ... %! fitdist (ones (2), 'normal') %!error ... %! fitdist ([i, 2, 3], 'normal') %!error ... %! fitdist (['a', 's', 'd'], 'normal') %!error ... %! fitdist ([1, 2, 3], 'normal', 'By') %!error ... %! fitdist ([1, 2, 3], 'normal', 'By', [1, 2]) %!error ... %! fitdist ([1, 2, 3], 'normal', 'Censoring', [1, 2]) %!error ... %! fitdist ([1, 2, 3], 'normal', 'frequency', [1, 2]) %!error ... %! fitdist ([1, 2, 3], 'negativebinomial', 'frequency', [1, -2, 3]) %!error ... %! fitdist ([1, 2, 3], 'normal', 'alpha', [1, 2]) %!error ... %! fitdist ([1, 2, 3], 'normal', 'alpha', i) %!error ... %! fitdist ([1, 2, 3], 'normal', 'alpha', -0.5) %!error ... %! fitdist ([1, 2, 3], 'normal', 'alpha', 1.5) %!error ... %! fitdist ([1, 2, 3], 'normal', 'ntrials', [1, 2]) %!error ... %! fitdist ([1, 2, 3], 'normal', 'ntrials', 0) %!error ... %! fitdist ([1, 2, 3], 'normal', 'options', 0) %!error ... %! fitdist ([1, 2, 3], 'normal', 'options', struct ('options', 1)) %!error ... %! fitdist ([1, 2, 3]', 'kernel', 'Censoring', [1, 0, 0]'); %!error ... %! fitdist ([1, 2, 3]', 'Stable', 'Censoring', [1, 0, 0]'); %!error ... %! fitdist ([1, 2, 3], 'normal', 'param', struct ('options', 1)) %!error ... %! fitdist (nan (100,1), 'normal'); %!error ... %! [pdca, gn, gl] = fitdist ([1, 2, 3], 'normal'); %!error ... %! fitdist ([1, 2, 3], 'generalizedpareto', 'theta', 2); %!error ... %! fitdist ([1, 2, 3], 'halfnormal', 'mu', 2); statistics-release-2.1.2/inst/Distribution_Wrappers/icdf.m000066400000000000000000000373421527311104300237340ustar00rootroot00000000000000# Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} icdf (@var{name}, @var{p}, @var{A}) ## @deftypefnx {statistics} {@var{x} =} icdf (@var{name}, @var{p}, @var{A}, @var{B}) ## @deftypefnx {statistics} {@var{x} =} icdf (@var{name}, @var{p}, @var{A}, @var{B}, @var{C}) ## ## Return the inverse CDF of a univariate distribution evaluated at @var{p}. ## ## @code{icdf} is a wrapper for the univariate quantile distribution functions ## (iCDF) available in the statistics package. See the corresponding functions' ## help to learn the signification of the parameters after @var{p}. ## ## @code{@var{x} = icdf (@var{name}, @var{p}, @var{A})} returns the iCDF for the ## one-parameter distribution family specified by @var{name} and the ## distribution parameter @var{A}, evaluated at the values in @var{p}. ## ## @code{@var{x} = icdf (@var{name}, @var{p}, @var{A}, @var{B})} returns the ## iCDF for the two-parameter distribution family specified by @var{name} and ## the distribution parameters @var{A} and @var{B}, evaluated at the values in ## @var{p}. ## ## @code{@var{x} = icdf (@var{name}, @var{p}, @var{A}, @var{B}, @var{C})} ## returns the iCDF for the three-parameter distribution family specified by ## @var{name} and the distribution parameters @var{A}, @var{B}, and @var{C}, ## evaluated at the values in @var{p}. ## ## @var{name} must be a char string of the name or the abbreviation of the ## desired quantile distribution function as listed in the following table. ## The last column shows the number of required parameters that should be parsed ## after @var{x} to the desired iCDF. ## ## @multitable @columnfractions 0.4 0.2 0.3 ## @headitem Distribution Name @tab Abbreviation @tab Input Parameters ## @item @qcode{'Beta'} @tab @qcode{'beta'} @tab 2 ## @item @qcode{'Binomial'} @tab @qcode{'bino'} @tab 2 ## @item @qcode{'Birnbaum-Saunders'} @tab @qcode{'bisa'} @tab 2 ## @item @qcode{'Burr'} @tab @qcode{'burr'} @tab 3 ## @item @qcode{'Cauchy'} @tab @qcode{'cauchy'} @tab 2 ## @item @qcode{'Chi-squared'} @tab @qcode{'chi2'} @tab 1 ## @item @qcode{'Extreme Value'} @tab @qcode{'ev'} @tab 2 ## @item @qcode{'Exponential'} @tab @qcode{'exp'} @tab 1 ## @item @qcode{'F-Distribution'} @tab @qcode{'f'} @tab 2 ## @item @qcode{'Gamma'} @tab @qcode{'gam'} @tab 2 ## @item @qcode{'Geometric'} @tab @qcode{'geo'} @tab 1 ## @item @qcode{'Generalized Extreme Value'} @tab @qcode{'gev'} @tab 3 ## @item @qcode{'Generalized Pareto'} @tab @qcode{'gp'} @tab 3 ## @item @qcode{'Gumbel'} @tab @qcode{'gumbel'} @tab 2 ## @item @qcode{'Half-normal'} @tab @qcode{'hn'} @tab 2 ## @item @qcode{'Hypergeometric'} @tab @qcode{'hyge'} @tab 3 ## @item @qcode{'Inverse Gaussian'} @tab @qcode{'invg'} @tab 2 ## @item @qcode{'Laplace'} @tab @qcode{'laplace'} @tab 2 ## @item @qcode{'Logistic'} @tab @qcode{'logi'} @tab 2 ## @item @qcode{'Log-Logistic'} @tab @qcode{'logl'} @tab 2 ## @item @qcode{'Lognormal'} @tab @qcode{'logn'} @tab 2 ## @item @qcode{'Nakagami'} @tab @qcode{'naka'} @tab 2 ## @item @qcode{'Negative Binomial'} @tab @qcode{'nbin'} @tab 2 ## @item @qcode{'Noncentral F-Distribution'} @tab @qcode{'ncf'} @tab 3 ## @item @qcode{'Noncentral Student T'} @tab @qcode{'nct'} @tab 2 ## @item @qcode{'Noncentral Chi-Squared'} @tab @qcode{'ncx2'} @tab 2 ## @item @qcode{'Normal'} @tab @qcode{'norm'} @tab 2 ## @item @qcode{'Poisson'} @tab @qcode{'poiss'} @tab 1 ## @item @qcode{'Rayleigh'} @tab @qcode{'rayl'} @tab 1 ## @item @qcode{'Rician'} @tab @qcode{'rice'} @tab 2 ## @item @qcode{'Student T'} @tab @qcode{'t'} @tab 1 ## @item @qcode{'location-scale T'} @tab @qcode{'tls'} @tab 3 ## @item @qcode{'Triangular'} @tab @qcode{'tri'} @tab 3 ## @item @qcode{'Discrete Uniform'} @tab @qcode{'unid'} @tab 1 ## @item @qcode{'Uniform'} @tab @qcode{'unif'} @tab 2 ## @item @qcode{'Von Mises'} @tab @qcode{'vm'} @tab 2 ## @item @qcode{'Weibull'} @tab @qcode{'wbl'} @tab 2 ## @end multitable ## ## Distribution names are matched ignoring case, spaces and hyphens, so that ## @qcode{'Extreme Value'}, @qcode{'ExtremeValue'} and @qcode{'extreme-value'} ## all select the same distribution, and the same set of names is accepted by ## @code{cdf}, @code{pdf}, @code{icdf}, @code{random}, @code{makedist}, ## @code{fitdist} and @code{mle}. ## ## This accepts more names than MATLAB. MATLAB takes the spaced and the ## squashed spelling but refuses the hyphenated one, so ## @qcode{'Birnbaum-Saunders'} and @qcode{'Log-Logistic'} are errors there; ## Octave has always accepted them and continues to. MATLAB also accepts ## @qcode{'tLocationScale'} in @code{makedist} while refusing it in ## @code{cdf} for the same distribution; Octave accepts it, and ## @qcode{'location-scale T'}, everywhere. Code written against MATLAB's ## names therefore runs unchanged, but code relying on these names will not ## port back. ## ## @seealso{icdf, pdf, random, betainv, binoinv, bisainv, burrinv, cauchyinv, ## chi2inv, evinv, expinv, finv, gaminv, geoinv, gevinv, gpinv, gumbelinv, ## hninv, hygeinv, invginv, laplaceinv, logiinv, loglinv, logninv, nakainv, ## nbininv, ncfinv, nctinv, ncx2inv, norminv, poissinv, raylinv, riceinv, tinv, ## tlsinv, triinv, unidinv, unifinv, vminv, wblinv} ## @end deftypefn function x = icdf (name, p, varargin) ## implemented functions persistent allDF = { ... {'beta' , 'Beta'}, @betainv, 2, ... {'bino' , 'Binomial'}, @binoinv, 2, ... {'bisa' , 'Birnbaum-Saunders'}, @bisainv, 2, ... {'burr' , 'Burr'}, @burrinv, 3, ... {'cauchy' , 'Cauchy'}, @cauchyinv, 2, ... {'chi2' , 'Chi-squared'}, @chi2inv, 1, ... {'ev' , 'Extreme Value'}, @evinv, 2, ... {'exp' , 'Exponential'}, @expinv, 1, ... {'f' , 'F-Distribution'}, @finv, 2, ... {'gam' , 'Gamma'}, @gaminv, 2, ... {'geo' , 'Geometric'}, @geoinv, 1, ... {'gev' , 'Generalized Extreme Value'}, @gevinv, 3, ... {'gp' , 'Generalized Pareto'}, @gpinv, 3, ... {'gumbel' , 'Gumbel'}, @gumbelinv, 2, ... {'hn' , 'Half-normal'}, @hninv, 2, ... {'hyge' , 'Hypergeometric'}, @hygeinv, 3, ... {'invg' , 'Inverse Gaussian'}, @invginv, 2, ... {'laplace' , 'Laplace'}, @laplaceinv, 2, ... {'logi' , 'Logistic'}, @logiinv, 2, ... {'logl' , 'Log-Logistic'}, @loglinv, 2, ... {'logn' , 'Lognormal'}, @logninv, 2, ... {'naka' , 'Nakagami'}, @nakainv, 2, ... {'nbin' , 'Negative Binomial'}, @nbininv, 2, ... {'ncf' , 'Noncentral F-Distribution'}, @ncfinv, 3, ... {'nct' , 'Noncentral Student T'}, @nctinv, 2, ... {'ncx2' , 'Noncentral Chi-squared'}, @ncx2inv, 2, ... {'norm' , 'Normal'}, @norminv, 2, ... {'poiss' , 'Poisson'}, @poissinv, 1, ... {'rayl' , 'Rayleigh'}, @raylinv, 1, ... {'rice' , 'Rician'}, @riceinv, 2, ... {'t' , 'Student T'}, @tinv, 1, ... {'tls', 'location-scale T', 'tLocationScale'}, @tlsinv, 3, ... {'tri' , 'Triangular'}, @triinv, 3, ... {'unid' , 'Discrete Uniform'}, @unidinv, 1, ... {'unif' , 'Uniform'}, @unifinv, 2, ... {'vm' , 'Von Mises'}, @vminv, 2, ... {'wbl' , 'Weibull'}, @wblinv, 2}; if (! ischar (name)) error ("icdf: distribution NAME must be a char string."); endif ## Check P being numeric and real if (! isnumeric (p)) error ("icdf: P must be numeric."); elseif (! isreal (p)) error ("icdf: values in P must be real."); endif ## Get number of arguments nargs = numel (varargin); ## Get available functions icdfnames = allDF(1:3:end); icdfhandl = allDF(2:3:end); icdf_args = allDF(3:3:end); ## Search for iCDF function ## Match on the folded key so that every spelling of a name resolves key = __distname_key__ (name); idx = cellfun (@(x) any (strcmp (key, cellfun (@__distname_key__, x, ... 'UniformOutput', false))), icdfnames); if (any (idx)) if (nargs == icdf_args{idx}) ## Check that all distribution parameters are numeric if (! all (cellfun (@(x)isnumeric (x), (varargin)))) error ("icdf: distribution parameters must be numeric."); endif ## Call appropriate iCDF x = feval (icdfhandl{idx}, p, varargin{:}); else if (icdf_args{idx} == 1) error ("icdf: %s distribution requires 1 parameter.", name); else error ("icdf: %s distribution requires %d parameters.", ... name, icdf_args{idx}); endif endif else error ("icdf: %s distribution is not implemented in Statistics.", name); endif endfunction ## Test results %!shared p %! p = [0.05:0.05:0.5]; %!assert_equal (icdf ('Beta', p, 5, 2), betainv (p, 5, 2)) %!assert_equal (icdf ('beta', p, 5, 2), betainv (p, 5, 2)) %!assert_equal (icdf ('Binomial', p, 5, 2), binoinv (p, 5, 2)) %!assert_equal (icdf ('bino', p, 5, 2), binoinv (p, 5, 2)) %!assert_equal (icdf ('Birnbaum-Saunders', p, 5, 2), bisainv (p, 5, 2)) %!assert_equal (icdf ('bisa', p, 5, 2), bisainv (p, 5, 2)) %!assert_equal (icdf ('Burr', p, 5, 2, 2), burrinv (p, 5, 2, 2)) %!assert_equal (icdf ('burr', p, 5, 2, 2), burrinv (p, 5, 2, 2)) %!assert_equal (icdf ('Cauchy', p, 5, 2), cauchyinv (p, 5, 2)) %!assert_equal (icdf ('cauchy', p, 5, 2), cauchyinv (p, 5, 2)) %!assert_equal (icdf ('Chi-squared', p, 5), chi2inv (p, 5)) %!assert_equal (icdf ('chi2', p, 5), chi2inv (p, 5)) %!assert_equal (icdf ('Extreme Value', p, 5, 2), evinv (p, 5, 2)) %!assert_equal (icdf ('ev', p, 5, 2), evinv (p, 5, 2)) %!assert_equal (icdf ('Exponential', p, 5), expinv (p, 5)) %!assert_equal (icdf ('exp', p, 5), expinv (p, 5)) %!assert_equal (icdf ('F-Distribution', p, 5, 2), finv (p, 5, 2)) %!assert_equal (icdf ('f', p, 5, 2), finv (p, 5, 2)) %!assert_equal (icdf ('Gamma', p, 5, 2), gaminv (p, 5, 2)) %!assert_equal (icdf ('gam', p, 5, 2), gaminv (p, 5, 2)) %!assert_equal (icdf ('Geometric', p, 5), geoinv (p, 5)) %!assert_equal (icdf ('geo', p, 5), geoinv (p, 5)) %!assert_equal (icdf ('Generalized Extreme Value', p, 5, 2, 2), gevinv (p, 5, 2, 2)) %!assert_equal (icdf ('gev', p, 5, 2, 2), gevinv (p, 5, 2, 2)) %!assert_equal (icdf ('Generalized Pareto', p, 5, 2, 2), gpinv (p, 5, 2, 2)) %!assert_equal (icdf ('gp', p, 5, 2, 2), gpinv (p, 5, 2, 2)) %!assert_equal (icdf ('Gumbel', p, 5, 2), gumbelinv (p, 5, 2)) %!assert_equal (icdf ('gumbel', p, 5, 2), gumbelinv (p, 5, 2)) %!assert_equal (icdf ('Half-normal', p, 5, 2), hninv (p, 5, 2)) %!assert_equal (icdf ('hn', p, 5, 2), hninv (p, 5, 2)) %!assert_equal (icdf ('Hypergeometric', p, 5, 2, 2), hygeinv (p, 5, 2, 2)) %!assert_equal (icdf ('hyge', p, 5, 2, 2), hygeinv (p, 5, 2, 2)) %!assert_equal (icdf ('Inverse Gaussian', p, 5, 2), invginv (p, 5, 2)) %!assert_equal (icdf ('invg', p, 5, 2), invginv (p, 5, 2)) %!assert_equal (icdf ('Laplace', p, 5, 2), laplaceinv (p, 5, 2)) %!assert_equal (icdf ('laplace', p, 5, 2), laplaceinv (p, 5, 2)) %!assert_equal (icdf ('Logistic', p, 5, 2), logiinv (p, 5, 2)) %!assert_equal (icdf ('logi', p, 5, 2), logiinv (p, 5, 2)) %!assert_equal (icdf ('Log-Logistic', p, 5, 2), loglinv (p, 5, 2)) %!assert_equal (icdf ('logl', p, 5, 2), loglinv (p, 5, 2)) %!assert_equal (icdf ('Lognormal', p, 5, 2), logninv (p, 5, 2)) %!assert_equal (icdf ('logn', p, 5, 2), logninv (p, 5, 2)) %!assert_equal (icdf ('Nakagami', p, 5, 2), nakainv (p, 5, 2)) %!assert_equal (icdf ('naka', p, 5, 2), nakainv (p, 5, 2)) %!assert_equal (icdf ('Negative Binomial', p, 5, 2), nbininv (p, 5, 2)) %!assert_equal (icdf ('nbin', p, 5, 2), nbininv (p, 5, 2)) %!assert_equal (icdf ('Noncentral F-Distribution', p, 5, 2, 2), ncfinv (p, 5, 2, 2)) %!assert_equal (icdf ('ncf', p, 5, 2, 2), ncfinv (p, 5, 2, 2)) %!assert_equal (icdf ('Noncentral Student T', p, 5, 2), nctinv (p, 5, 2)) %!assert_equal (icdf ('nct', p, 5, 2), nctinv (p, 5, 2)) %!assert_equal (icdf ('Noncentral Chi-Squared', p, 5, 2), ncx2inv (p, 5, 2)) %!assert_equal (icdf ('ncx2', p, 5, 2), ncx2inv (p, 5, 2)) %!assert_equal (icdf ('Normal', p, 5, 2), norminv (p, 5, 2)) %!assert_equal (icdf ('norm', p, 5, 2), norminv (p, 5, 2)) %!assert_equal (icdf ('Poisson', p, 5), poissinv (p, 5)) %!assert_equal (icdf ('poiss', p, 5), poissinv (p, 5)) %!assert_equal (icdf ('Rayleigh', p, 5), raylinv (p, 5)) %!assert_equal (icdf ('rayl', p, 5), raylinv (p, 5)) %!assert_equal (icdf ('Rician', p, 5, 1), riceinv (p, 5, 1)) %!assert_equal (icdf ('rice', p, 5, 1), riceinv (p, 5, 1)) %!assert_equal (icdf ('Student T', p, 5), tinv (p, 5)) %!assert_equal (icdf ('t', p, 5), tinv (p, 5)) %!assert_equal (icdf ('location-scale T', p, 5, 1, 2), tlsinv (p, 5, 1, 2)) %!assert_equal (icdf ('tls', p, 5, 1, 2), tlsinv (p, 5, 1, 2)) %!assert_equal (icdf ('Triangular', p, 5, 2, 2), triinv (p, 5, 2, 2)) %!assert_equal (icdf ('tri', p, 5, 2, 2), triinv (p, 5, 2, 2)) %!assert_equal (icdf ('Discrete Uniform', p, 5), unidinv (p, 5)) %!assert_equal (icdf ('unid', p, 5), unidinv (p, 5)) %!assert_equal (icdf ('Uniform', p, 5, 2), unifinv (p, 5, 2)) %!assert_equal (icdf ('unif', p, 5, 2), unifinv (p, 5, 2)) %!assert_equal (icdf ('Von Mises', p, 5, 2), vminv (p, 5, 2)) %!assert_equal (icdf ('vm', p, 5, 2), vminv (p, 5, 2)) %!assert_equal (icdf ('Weibull', p, 5, 2), wblinv (p, 5, 2)) %!assert_equal (icdf ('wbl', p, 5, 2), wblinv (p, 5, 2)) ## Test input validation %!test %! ## Every spelling of a name reaches the same distribution: case, spaces, %! ## hyphens and underscores are all ignored. %! for n = {'Extreme Value', 'ExtremeValue', 'extreme-value', 'EXTREME VALUE'} %! assert_equal (icdf (n{1}, 0.5, 2, 3), icdf ('ev', 0.5, 2, 3)); %! endfor %!test %! ## The name that makedist uses is accepted here too, and the reverse %! assert_equal (icdf ('tLocationScale', 0.5, 2, 3, 4), ... %! icdf ('tls', 0.5, 2, 3, 4)); %!error icdf (1) %!error icdf ({'beta'}) %!error icdf ('beta', {[1 2 3 4 5]}) %!error icdf ('beta', 'text') %!error icdf ('beta', 1+i) %!error ... %! icdf ('Beta', p, 'a', 2) %!error ... %! icdf ('Beta', p, 5, '') %!error ... %! icdf ('Beta', p, 5, {2}) %!error icdf ('chi2', p) %!error icdf ('Beta', p, 5) %!error icdf ('Burr', p, 5) %!error icdf ('Burr', p, 5, 2) statistics-release-2.1.2/inst/Distribution_Wrappers/makedist.m000066400000000000000000001064611527311104300246270ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{pd} =} makedist (@var{distname}) ## @deftypefnx {statistics} {@var{pd} =} makedist (@var{distname}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {@var{list} =} makedist ## ## Create probability distribution object. ## ## @code{@var{pd} = makedist (@var{distname})} creates a probability ## distribution object for the distribution specified in @var{distname}, using ## the default parameter values. ## ## @code{@var{pd} = makedist (@var{distname}, @var{Name}, @var{Value})} also ## creates a probability distribution object with one or more distribution ## parameter values specified by @qcode{Name-Value} pair arguments. ## ## @code{@var{list} = makedist} returns a cell array, @var{list}, containing a ## list of the probability distributions that makedist can create. ## ## Distribution names are matched ignoring case, spaces and hyphens, so that ## @qcode{'Extreme Value'}, @qcode{'ExtremeValue'} and @qcode{'extreme-value'} ## all select the same distribution, and the same set of names is accepted by ## @code{cdf}, @code{pdf}, @code{icdf}, @code{random}, @code{makedist}, ## @code{fitdist} and @code{mle}. ## ## This accepts more names than MATLAB. MATLAB takes the spaced and the ## squashed spelling but refuses the hyphenated one, so ## @qcode{'Birnbaum-Saunders'} and @qcode{'Log-Logistic'} are errors there; ## Octave has always accepted them and continues to. MATLAB also accepts ## @qcode{'tLocationScale'} in @code{makedist} while refusing it in ## @code{cdf} for the same distribution; Octave accepts it, and ## @qcode{'location-scale T'}, everywhere. Code written against MATLAB's ## names therefore runs unchanged, but code relying on these names will not ## port back. ## ## @seealso{fitdist} ## @end deftypefn function pd = makedist (varargin) ## Add list of supported probability distribution objects PDO = {'Beta'; 'Binomial'; 'BirnbaumSaunders'; 'Burr'; 'Exponential'; ... 'ExtremeValue'; 'Gamma'; 'GeneralizedExtremeValue'; ... 'GeneralizedPareto'; 'HalfNormal'; 'InverseGaussian'; 'Kernel'; ... 'Logistic'; 'Loglogistic'; 'Lognormal'; 'Loguniform'; ... 'Multinomial'; 'Nakagami'; 'NegativeBinomial'; 'Normal'; ... 'PiecewiseLinear'; 'Poisson'; 'Rayleigh'; 'Rician'; ... 'Stable'; 'tLocationScale'; 'Triangular'; 'Uniform'; 'Weibull'}; ABBR = {'bisa', 'ev', 'gev', 'gp', 'hn', 'invg', 'nbin', 'tls', ... 'location-scale T'}; ## Check for input arguments if (nargin == 0) pd = PDO; return else distname = varargin{1}; varargin(1) = []; endif ## Check distribution name if (! (ischar (distname) && size (distname, 1) == 1)) error ("makedist: DISTNAME must be a character vector."); elseif (! (any (strcmp (__distname_key__ (distname), ... cellfun (@__distname_key__, PDO, ... "UniformOutput", false))) || any (strcmp (__distname_key__ (distname), ... cellfun (@__distname_key__, ABBR, ... "UniformOutput", false))))) error ("makedist: unrecognized distribution name."); endif ## Check for additional arguments being in pairs if (mod (numel (varargin), 2) != 0) error ("makedist: optional arguments must be in NAME-VALUE pairs."); endif ## Switch to selected distribution switch (__distname_key__ (distname)) case 'beta' ## Add default parameters a = 1; b = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'a' a = varargin{2}; case 'b' b = varargin{2}; otherwise error ("makedist: unknown parameter for 'Beta' distribution."); endswitch varargin([1:2]) = []; endwhile pd = prob.BetaDistribution (a, b); case 'binomial' N = 1; p = 0.5; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'n' N = varargin{2}; case 'p' p = varargin{2}; otherwise error ("makedist: unknown parameter for 'Binomial' distribution."); endswitch varargin([1:2]) = []; endwhile pd = prob.BinomialDistribution (N, p); case {'birnbaumsaunders', 'bisa'} beta = 1; gamma = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'beta' beta = varargin{2}; case 'gamma' gamma = varargin{2}; otherwise error (strcat ("makedist: unknown parameter for", ... " 'BirnbaumSaunders' distribution.")); endswitch varargin([1:2]) = []; endwhile pd = prob.BirnbaumSaundersDistribution (beta, gamma); case 'burr' alpha = 1; c = 1; k = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case {'lambda', 'alpha'} alpha = varargin{2}; case 'c' c = varargin{2}; case 'k' k = varargin{2}; otherwise error ("makedist: unknown parameter for 'Burr' distribution."); endswitch varargin([1:2]) = []; endwhile pd = prob.BurrDistribution (alpha, c, k); case 'exponential' mu = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'mu' mu = varargin{2}; otherwise error (strcat ("makedist: unknown parameter for", ... " 'Exponential' distribution.")); endswitch varargin([1:2]) = []; endwhile pd = prob.ExponentialDistribution (mu); case {'extremevalue', 'ev'} mu = 0; sigma = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'mu' mu = varargin{2}; case 'sigma' sigma = varargin{2}; otherwise error (strcat ("makedist: unknown parameter for", ... " 'ExtremeValue' distribution.")); endswitch varargin([1:2]) = []; endwhile pd = prob.ExtremeValueDistribution (mu, sigma); case 'gamma' a = 1; b = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'a' a = varargin{2}; case 'b' b = varargin{2}; otherwise error ("makedist: unknown parameter for 'Gamma' distribution."); endswitch varargin([1:2]) = []; endwhile pd = prob.GammaDistribution (a, b); case {'generalizedextremevalue', 'gev'} k = 0; sigma = 1; mu = 0; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'k' k = varargin{2}; case 'sigma' sigma = varargin{2}; case 'mu' mu = varargin{2}; otherwise error (strcat ("makedist: unknown parameter for", ... " 'GeneralizedExtremeValue' distribution.")); endswitch varargin([1:2]) = []; endwhile pd = prob.GeneralizedExtremeValueDistribution (k, sigma, mu); case {'generalizedpareto', 'gp'} k = 1; sigma = 1; theta = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'k' k = varargin{2}; case 'sigma' sigma = varargin{2}; case 'theta' theta = varargin{2}; otherwise error (strcat ("makedist: unknown parameter for", ... " 'GeneralizedPareto' distribution.")); endswitch varargin([1:2]) = []; endwhile pd = prob.GeneralizedParetoDistribution (k, sigma, theta); case {'halfnormal', 'hn'} mu = 0; sigma = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'mu' mu = varargin{2}; case 'sigma' sigma = varargin{2}; otherwise error (strcat ("makedist: unknown parameter for", ... " 'HalfNormal' distribution.")); endswitch varargin([1:2]) = []; endwhile pd = prob.HalfNormalDistribution (mu, sigma); case {'inversegaussian', 'invg'} mu = 1; lambda = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'mu' mu = varargin{2}; case 'lambda' lambda = varargin{2}; otherwise error (strcat ("makedist: unknown parameter for", ... " 'InverseGaussian' distribution.")); endswitch varargin([1:2]) = []; endwhile pd = prob.InverseGaussianDistribution (mu, lambda); case 'kernel' error (strcat ("makedist: the Kernel distribution is not parametric", ... " and cannot be created with makedist; use fitdist.")); case 'logistic' mu = 0; sigma = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'mu' mu = varargin{2}; case 'sigma' sigma = varargin{2}; otherwise error (strcat ("makedist: unknown parameter for", ... " 'Logistic' distribution.")); endswitch varargin([1:2]) = []; endwhile pd = prob.LogisticDistribution (mu, sigma); case 'loglogistic' mu = 0; sigma = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'mu' mu = varargin{2}; case 'sigma' sigma = varargin{2}; otherwise error (strcat ("makedist: unknown parameter for", ... " 'Loglogistic' distribution.")); endswitch varargin([1:2]) = []; endwhile pd = prob.LoglogisticDistribution (mu, sigma); case 'lognormal' mu = 0; sigma = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'mu' mu = varargin{2}; case 'sigma' sigma = varargin{2}; otherwise error (strcat ("makedist: unknown parameter for", ... " 'Lognormal' distribution.")); endswitch varargin([1:2]) = []; endwhile pd = prob.LognormalDistribution (mu, sigma); case 'loguniform' lower = 1; upper = 4; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'lower' lower = varargin{2}; case 'upper' upper = varargin{2}; otherwise error (strcat ("makedist: unknown parameter for", ... " 'Loguniform' distribution.")); endswitch varargin([1:2]) = []; endwhile pd = prob.LoguniformDistribution (lower, upper); case 'multinomial' probs = [0.5, 0.5]; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'probabilities' probs = varargin{2}; otherwise error (strcat ("makedist: unknown parameter for", ... " 'Multinomial' distribution.")); endswitch varargin([1:2]) = []; endwhile pd = prob.MultinomialDistribution (probs); case 'nakagami' mu = 1; omega = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'mu' mu = varargin{2}; case 'omega' omega = varargin{2}; otherwise error (strcat ("makedist: unknown parameter for", ... " 'Nakagami' distribution.")); endswitch varargin([1:2]) = []; endwhile pd = prob.NakagamiDistribution (mu, omega); case {'negativebinomial', 'nbin'} R = 1; P = 0.5; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'r' R = varargin{2}; case {'ps', 'p'} P = varargin{2}; otherwise error (strcat ("makedist: unknown parameter for", ... " 'NegativeBinomial' distribution.")); endswitch varargin([1:2]) = []; endwhile pd = prob.NegativeBinomialDistribution (R, P); case 'normal' mu = 0; sigma = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'mu' mu = varargin{2}; case 'sigma' sigma = varargin{2}; otherwise error ("makedist: unknown parameter for 'Normal' distribution."); endswitch varargin([1:2]) = []; endwhile pd = prob.NormalDistribution (mu, sigma); case 'piecewiselinear' x = [0, 1]; Fx = [0, 1]; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'x' x = varargin{2}; case 'fx' Fx = varargin{2}; otherwise error (strcat ("makedist: unknown parameter for", ... " 'PiecewiseLinear' distribution.")); endswitch varargin([1:2]) = []; endwhile pd = prob.PiecewiseLinearDistribution (x, Fx); case 'poisson' lambda = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'lambda' lambda = varargin{2}; otherwise error ("makedist: unknown parameter for 'Poisson' distribution."); endswitch varargin([1:2]) = []; endwhile pd = prob.PoissonDistribution (lambda); case 'rayleigh' sigma = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case {'sigma', 'b'} sigma = varargin{2}; otherwise error (strcat ("makedist: unknown parameter for", ... " 'Rayleigh' distribution.")); endswitch varargin([1:2]) = []; endwhile pd = prob.RayleighDistribution (sigma); case 'rician' s = 1; sigma = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 's' s = varargin{2}; case 'sigma' sigma = varargin{2}; otherwise error ("makedist: unknown parameter for 'Rician' distribution."); endswitch varargin([1:2]) = []; endwhile pd = prob.RicianDistribution (s, sigma); case 'stable' alpha = 2; beta = 0; gam = 1; delta = 0; while (numel (varargin) > 0) switch (tolower (varargin{1})) case {'alpha', 's'} alpha = varargin{2}; case 'beta' beta = varargin{2}; case 'gam' gam = varargin{2}; case 'delta' delta = varargin{2}; otherwise error ("makedist: unknown parameter for 'Stable' distribution."); endswitch varargin([1:2]) = []; endwhile pd = prob.StableDistribution (alpha, beta, gam, delta); case {'tlocationscale', 'tls', 'locationscalet'} mu = 0; sigma = 1; df = 5; while (numel (varargin) > 0) switch (tolower (varargin{1})) case {'mu', 's'} mu = varargin{2}; case 'sigma' sigma = varargin{2}; case {'df', 'nu'} df = varargin{2}; otherwise error (strcat ("makedist: unknown parameter for", ... " 'tLocationScale' distribution.")); endswitch varargin([1:2]) = []; endwhile pd = prob.tLocationScaleDistribution (mu, sigma, df); case 'triangular' A = 0; B = 0.5; C = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'a' A = varargin{2}; case 'b' B = varargin{2}; case 'c' C = varargin{2}; otherwise error (strcat ("makedist: unknown parameter for", ... " 'Triangular' distribution.")); endswitch varargin([1:2]) = []; endwhile pd = prob.TriangularDistribution (A, B, C); case 'uniform' Lower = 0; Upper = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'lower' Lower = varargin{2}; case 'upper' Upper = varargin{2}; otherwise error ("makedist: unknown parameter for 'Uniform' distribution."); endswitch varargin([1:2]) = []; endwhile pd = prob.UniformDistribution (Lower, Upper); case 'weibull' lambda = 1; k = 1; while (numel (varargin) > 0) switch (tolower (varargin{1})) case {'lambda', 'a'} lambda = varargin{2}; case {'k', 'b'} k = varargin{2}; otherwise error ("makedist: unknown parameter for 'Weibull' distribution."); endswitch varargin([1:2]) = []; endwhile pd = prob.WeibullDistribution (lambda, k); endswitch endfunction ## Test output %!test %! pd = makedist ('beta'); %! assert_equal (class (pd), "prob.BetaDistribution"); %! assert_equal (pd.a, 1); %! assert_equal (pd.b, 1); %!test %! pd = makedist ('beta', 'a', 5); %! assert_equal (pd.a, 5); %! assert_equal (pd.b, 1); %!test %! pd = makedist ('beta', 'b', 5); %! assert_equal (pd.a, 1); %! assert_equal (pd.b, 5); %!test %! pd = makedist ('beta', 'a', 3, 'b', 5); %! assert_equal (pd.a, 3); %! assert_equal (pd.b, 5); %!test %! pd = makedist ('binomial'); %! assert_equal (class (pd), "prob.BinomialDistribution"); %! assert_equal (pd.N, 1); %! assert_equal (pd.p, 0.5); %!test %! pd = makedist ('binomial', 'N', 5); %! assert_equal (pd.N, 5); %! assert_equal (pd.p, 0.5); %!test %! pd = makedist ('binomial', 'p', 0.2); %! assert_equal (pd.N, 1); %! assert_equal (pd.p, 0.2); %!test %! pd = makedist ('binomial', 'N', 3, 'p', 0.3); %! assert_equal (pd.N, 3); %! assert_equal (pd.p, 0.3); %!test %! pd = makedist ('birnbaumsaunders'); %! assert_equal (class (pd), "prob.BirnbaumSaundersDistribution"); %! assert_equal (pd.beta, 1); %! assert_equal (pd.gamma, 1); %!test %! pd = makedist ('birnbaumsaunders', 'beta', 5); %! assert_equal (pd.beta, 5); %! assert_equal (pd.gamma, 1); %!test %! pd = makedist ('birnbaumsaunders', 'gamma', 5); %! assert_equal (pd.beta, 1); %! assert_equal (pd.gamma, 5); %!test %! pd = makedist ('birnbaumsaunders', 'beta', 3, 'gamma', 5); %! assert_equal (pd.beta, 3); %! assert_equal (pd.gamma, 5); %!test %! pd = makedist ('burr'); %! assert_equal (class (pd), "prob.BurrDistribution"); %! assert_equal (pd.alpha, 1); %! assert_equal (pd.c, 1); %! assert_equal (pd.k, 1); %!test %! pd = makedist ('burr', 'k', 5); %! assert_equal (pd.alpha, 1); %! assert_equal (pd.c, 1); %! assert_equal (pd.k, 5); %!test %! pd = makedist ('burr', 'c', 5); %! assert_equal (pd.alpha, 1); %! assert_equal (pd.c, 5); %! assert_equal (pd.k, 1); %!test %! pd = makedist ('burr', 'alpha', 3, 'c', 5); %! assert_equal (pd.alpha, 3); %! assert_equal (pd.c, 5); %! assert_equal (pd.k, 1); %!test %! pd = makedist ('burr', 'k', 3, 'c', 5); %! assert_equal (pd.alpha, 1); %! assert_equal (pd.c, 5); %! assert_equal (pd.k, 3); %!test %! pd = makedist ('exponential'); %! assert_equal (class (pd), "prob.ExponentialDistribution"); %! assert_equal (pd.mu, 1); %!test %! pd = makedist ('exponential', 'mu', 5); %! assert_equal (pd.mu, 5); %!test %! pd = makedist ('extremevalue'); %! assert_equal (class (pd), "prob.ExtremeValueDistribution"); %! assert_equal (pd.mu, 0); %! assert_equal (pd.sigma, 1); %!test %! pd = makedist ('extremevalue', 'mu', 5); %! assert_equal (class (pd), "prob.ExtremeValueDistribution"); %! assert_equal (pd.mu, 5); %! assert_equal (pd.sigma, 1); %!test %! pd = makedist ('ev', 'sigma', 5); %! assert_equal (class (pd), "prob.ExtremeValueDistribution"); %! assert_equal (pd.mu, 0); %! assert_equal (pd.sigma, 5); %!test %! pd = makedist ('ev', 'mu', -3, 'sigma', 5); %! assert_equal (class (pd), "prob.ExtremeValueDistribution"); %! assert_equal (pd.mu, -3); %! assert_equal (pd.sigma, 5); %!test %! pd = makedist ('gamma'); %! assert_equal (class (pd), "prob.GammaDistribution"); %! assert_equal (pd.a, 1); %! assert_equal (pd.b, 1); %!test %! pd = makedist ('gamma', 'a', 5); %! assert_equal (pd.a, 5); %! assert_equal (pd.b, 1); %!test %! pd = makedist ('gamma', 'b', 5); %! assert_equal (pd.a, 1); %! assert_equal (pd.b, 5); %!test %! pd = makedist ('gamma', 'a', 3, 'b', 5); %! assert_equal (pd.a, 3); %! assert_equal (pd.b, 5); %!test %! pd = makedist ('GeneralizedExtremeValue'); %! assert_equal (class (pd), "prob.GeneralizedExtremeValueDistribution"); %! assert_equal (pd.k, 0); %! assert_equal (pd.sigma, 1); %! assert_equal (pd.mu, 0); %!test %! pd = makedist ('GeneralizedExtremeValue', 'k', 5); %! assert_equal (pd.k, 5); %! assert_equal (pd.sigma, 1); %! assert_equal (pd.mu, 0); %!test %! pd = makedist ('GeneralizedExtremeValue', 'sigma', 5); %! assert_equal (pd.k, 0); %! assert_equal (pd.sigma, 5); %! assert_equal (pd.mu, 0); %!test %! pd = makedist ('GeneralizedExtremeValue', 'k', 3, 'sigma', 5); %! assert_equal (pd.k, 3); %! assert_equal (pd.sigma, 5); %! assert_equal (pd.mu, 0); %!test %! pd = makedist ('GeneralizedExtremeValue', 'mu', 3, 'sigma', 5); %! assert_equal (pd.k, 0); %! assert_equal (pd.sigma, 5); %! assert_equal (pd.mu, 3); %!test %! pd = makedist ('GeneralizedPareto'); %! assert_equal (class (pd), "prob.GeneralizedParetoDistribution"); %! assert_equal (pd.k, 1); %! assert_equal (pd.sigma, 1); %! assert_equal (pd.theta, 1); %!test %! pd = makedist ('GeneralizedPareto', 'k', 5); %! assert_equal (pd.k, 5); %! assert_equal (pd.sigma, 1); %! assert_equal (pd.theta, 1); %!test %! pd = makedist ('GeneralizedPareto', 'sigma', 5); %! assert_equal (pd.k, 1); %! assert_equal (pd.sigma, 5); %! assert_equal (pd.theta, 1); %!test %! pd = makedist ('GeneralizedPareto', 'k', 3, 'sigma', 5); %! assert_equal (pd.k, 3); %! assert_equal (pd.sigma, 5); %! assert_equal (pd.theta, 1); %!test %! pd = makedist ('GeneralizedPareto', 'theta', 3, 'sigma', 5); %! assert_equal (pd.k, 1); %! assert_equal (pd.sigma, 5); %! assert_equal (pd.theta, 3); %!test %! pd = makedist ('HalfNormal'); %! assert_equal (class (pd), "prob.HalfNormalDistribution"); %! assert_equal (pd.mu, 0); %! assert_equal (pd.sigma, 1); %!test %! pd = makedist ('HalfNormal', 'mu', 5); %! assert_equal (pd.mu, 5); %! assert_equal (pd.sigma, 1); %!test %! pd = makedist ('HalfNormal', 'sigma', 5); %! assert_equal (pd.mu, 0); %! assert_equal (pd.sigma, 5); %!test %! pd = makedist ('HalfNormal', 'mu', 3, 'sigma', 5); %! assert_equal (pd.mu, 3); %! assert_equal (pd.sigma, 5); %!test %! pd = makedist ('InverseGaussian'); %! assert_equal (class (pd), "prob.InverseGaussianDistribution"); %! assert_equal (pd.mu, 1); %! assert_equal (pd.lambda, 1); %!test %! pd = makedist ('InverseGaussian', 'mu', 5); %! assert_equal (pd.mu, 5); %! assert_equal (pd.lambda, 1); %!test %! pd = makedist ('InverseGaussian', 'lambda', 5); %! assert_equal (pd.mu, 1); %! assert_equal (pd.lambda, 5); %!test %! pd = makedist ('InverseGaussian', 'mu', 3, 'lambda', 5); %! assert_equal (pd.mu, 3); %! assert_equal (pd.lambda, 5); %!test %! pd = makedist ('logistic'); %! assert_equal (class (pd), "prob.LogisticDistribution"); %! assert_equal (pd.mu, 0); %! assert_equal (pd.sigma, 1); %!test %! pd = makedist ('logistic', 'mu', 5); %! assert_equal (pd.mu, 5); %! assert_equal (pd.sigma, 1); %!test %! pd = makedist ('logistic', 'sigma', 5); %! assert_equal (pd.mu, 0); %! assert_equal (pd.sigma, 5); %!test %! pd = makedist ('logistic', 'mu', 3, 'sigma', 5); %! assert_equal (pd.mu, 3); %! assert_equal (pd.sigma, 5); %!test %! pd = makedist ('loglogistic'); %! assert_equal (class (pd), "prob.LoglogisticDistribution"); %! assert_equal (pd.mu, 0); %! assert_equal (pd.sigma, 1); %!test %! pd = makedist ('loglogistic', 'mu', 5); %! assert_equal (pd.mu, 5); %! assert_equal (pd.sigma, 1); %!test %! pd = makedist ('loglogistic', 'sigma', 5); %! assert_equal (pd.mu, 0); %! assert_equal (pd.sigma, 5); %!test %! pd = makedist ('loglogistic', 'mu', 3, 'sigma', 5); %! assert_equal (pd.mu, 3); %! assert_equal (pd.sigma, 5); %!test %! pd = makedist ('Lognormal'); %! assert_equal (class (pd), "prob.LognormalDistribution"); %! assert_equal (pd.mu, 0); %! assert_equal (pd.sigma, 1); %!test %! pd = makedist ('Lognormal', 'mu', 5); %! assert_equal (pd.mu, 5); %! assert_equal (pd.sigma, 1); %!test %! pd = makedist ('Lognormal', 'sigma', 5); %! assert_equal (pd.mu, 0); %! assert_equal (pd.sigma, 5); %!test %! pd = makedist ('Lognormal', 'mu', -3, 'sigma', 5); %! assert_equal (pd.mu, -3); %! assert_equal (pd.sigma, 5); %!test %! pd = makedist ('Loguniform'); %! assert_equal (class (pd), "prob.LoguniformDistribution"); %! assert_equal (pd.Lower, 1); %! assert_equal (pd.Upper, 4); %!test %! pd = makedist ('Loguniform', 'Lower', 2); %! assert_equal (pd.Lower, 2); %! assert_equal (pd.Upper, 4); %!test %! pd = makedist ('Loguniform', 'Lower', 1, 'Upper', 3); %! assert_equal (pd.Lower, 1); %! assert_equal (pd.Upper, 3); %!test %! pd = makedist ('Multinomial'); %! assert_equal (class (pd), "prob.MultinomialDistribution"); %! assert_equal (pd.Probabilities, [0.5, 0.5]); %!test %! pd = makedist ('Multinomial', 'Probabilities', [0.2, 0.3, 0.1, 0.4]); %! assert_equal (class (pd), "prob.MultinomialDistribution"); %! assert_equal (pd.Probabilities, [0.2, 0.3, 0.1, 0.4]); %!test %! pd = makedist ('Nakagami'); %! assert_equal (class (pd), "prob.NakagamiDistribution"); %! assert_equal (pd.mu, 1); %! assert_equal (pd.omega, 1); %!test %! pd = makedist ('Nakagami', 'mu', 5); %! assert_equal (class (pd), "prob.NakagamiDistribution"); %! assert_equal (pd.mu, 5); %! assert_equal (pd.omega, 1); %!test %! pd = makedist ('Nakagami', 'omega', 0.3); %! assert_equal (class (pd), "prob.NakagamiDistribution"); %! assert_equal (pd.mu, 1); %! assert_equal (pd.omega, 0.3); %!test %! pd = makedist ('NegativeBinomial'); %! assert_equal (class (pd), "prob.NegativeBinomialDistribution"); %! assert_equal (pd.R, 1); %! assert_equal (pd.P, 0.5); %!test %! pd = makedist ('NegativeBinomial', 'R', 5); %! assert_equal (class (pd), "prob.NegativeBinomialDistribution"); %! assert_equal (pd.R, 5); %! assert_equal (pd.P, 0.5); %!test %! pd = makedist ('NegativeBinomial', 'p', 0.3); %! assert_equal (class (pd), "prob.NegativeBinomialDistribution"); %! assert_equal (pd.R, 1); %! assert_equal (pd.P, 0.3); %!test %! pd = makedist ('Normal'); %! assert_equal (class (pd), "prob.NormalDistribution"); %! assert_equal (pd.mu, 0); %! assert_equal (pd.sigma, 1); %!test %! pd = makedist ('Normal', 'mu', 5); %! assert_equal (class (pd), "prob.NormalDistribution"); %! assert_equal (pd.mu, 5); %! assert_equal (pd.sigma, 1); %!test %! pd = makedist ('Normal', 'sigma', 5); %! assert_equal (class (pd), "prob.NormalDistribution"); %! assert_equal (pd.mu, 0); %! assert_equal (pd.sigma, 5); %!test %! pd = makedist ('Normal', 'mu', -3, 'sigma', 5); %! assert_equal (class (pd), "prob.NormalDistribution"); %! assert_equal (pd.mu, -3); %! assert_equal (pd.sigma, 5); %!test %! pd = makedist ('PiecewiseLinear'); %! assert_equal (class (pd), "prob.PiecewiseLinearDistribution"); %! assert_equal (pd.x, [0, 1]); %! assert_equal (pd.Fx, [0, 1]); %!test %! pd = makedist ('PiecewiseLinear', 'x', [0, 1, 2], 'Fx', [0, 0.5, 1]); %! assert_equal (pd.x, [0, 1, 2]); %! assert_equal (pd.Fx, [0, 0.5, 1]); %!test %! ## a column is stored the same way, as MATLAB stores it %! pd = makedist ('PiecewiseLinear', 'x', [0; 1; 2], 'Fx', [0; 0.5; 1]); %! assert_equal (pd.x, [0, 1, 2]); %! assert_equal (pd.Fx, [0, 0.5, 1]); %!test %! ## Multinomial already agreed with MATLAB and is unchanged %! pd = makedist ('Multinomial', 'Probabilities', [0.2, 0.3, 0.5]); %! assert_equal (pd.Probabilities, [0.2, 0.3, 0.5]); %! assert_equal (size (pd.Probabilities), [1, 3]); %!test %! pd = makedist ('Poisson'); %! assert_equal (class (pd), "prob.PoissonDistribution"); %! assert_equal (pd.lambda, 1); %!test %! pd = makedist ('Poisson', 'lambda', 5); %! assert_equal (pd.lambda, 5); %!test %! pd = makedist ('Rayleigh'); %! assert_equal (class (pd), "prob.RayleighDistribution"); %! assert_equal (pd.B, 1); %!test %! pd = makedist ('Rayleigh', 'sigma', 5); %! assert_equal (pd.B, 5); %!test %! pd = makedist ('Rician'); %! assert_equal (class (pd), "prob.RicianDistribution"); %! assert_equal (pd.s, 1); %! assert_equal (pd.sigma, 1); %!test %! pd = makedist ('Rician', 's', 3); %! assert_equal (pd.s, 3); %! assert_equal (pd.sigma, 1); %!test %! pd = makedist ('Rician', 'sigma', 3); %! assert_equal (pd.s, 1); %! assert_equal (pd.sigma, 3); %!test %! pd = makedist ('Rician', 's', 2, 'sigma', 3); %! assert_equal (pd.s, 2); %! assert_equal (pd.sigma, 3); %!test %! pd = makedist ('stable'); %! assert_equal (class (pd), "prob.StableDistribution"); %! assert_equal (pd.alpha, 2); %! assert_equal (pd.beta, 0); %!test %! pd = makedist ('tlocationscale'); %! assert_equal (class (pd), "prob.tLocationScaleDistribution"); %! assert_equal (pd.mu, 0); %! assert_equal (pd.sigma, 1); %! assert_equal (pd.nu, 5); %!test %! pd = makedist ('tlocationscale', 'mu', 5); %! assert_equal (pd.mu, 5); %! assert_equal (pd.sigma, 1); %! assert_equal (pd.nu, 5); %!test %! pd = makedist ('tlocationscale', 'sigma', 2); %! assert_equal (pd.mu, 0); %! assert_equal (pd.sigma, 2); %! assert_equal (pd.nu, 5); %!test %! pd = makedist ('tlocationscale', 'mu', 5, 'sigma', 2); %! assert_equal (pd.mu, 5); %! assert_equal (pd.sigma, 2); %! assert_equal (pd.nu, 5); %!test %! pd = makedist ('tlocationscale', 'nu', 1, 'sigma', 2); %! assert_equal (pd.mu, 0); %! assert_equal (pd.sigma, 2); %! assert_equal (pd.nu, 1); %!test %! pd = makedist ('tlocationscale', 'mu', -2, 'sigma', 3, 'nu', 1); %! assert_equal (pd.mu, -2); %! assert_equal (pd.sigma, 3); %! assert_equal (pd.nu, 1); %!test %! pd = makedist ('Triangular'); %! assert_equal (class (pd), "prob.TriangularDistribution"); %! assert_equal (pd.A, 0); %! assert_equal (pd.B, 0.5); %! assert_equal (pd.C, 1); %!test %! pd = makedist ('Triangular', 'A', -2); %! assert_equal (pd.A, -2); %! assert_equal (pd.B, 0.5); %! assert_equal (pd.C, 1); %!test %! pd = makedist ('Triangular', 'A', 0.5, 'B', 0.9); %! assert_equal (pd.A, 0.5); %! assert_equal (pd.B, 0.9); %! assert_equal (pd.C, 1); %!test %! pd = makedist ('Triangular', 'A', 1, 'B', 2, 'C', 5); %! assert_equal (pd.A, 1); %! assert_equal (pd.B, 2); %! assert_equal (pd.C, 5); %!test %! pd = makedist ('Uniform'); %! assert_equal (class (pd), "prob.UniformDistribution"); %! assert_equal (pd.Lower, 0); %! assert_equal (pd.Upper, 1); %!test %! pd = makedist ('Uniform', 'Lower', -2); %! assert_equal (pd.Lower, -2); %! assert_equal (pd.Upper, 1); %!test %! pd = makedist ('Uniform', 'Lower', 1, 'Upper', 3); %! assert_equal (pd.Lower, 1); %! assert_equal (pd.Upper, 3); %!test %! pd = makedist ('Weibull'); %! assert_equal (class (pd), "prob.WeibullDistribution"); %! assert_equal (pd.A, 1); %! assert_equal (pd.B, 1); %!test %! pd = makedist ('Weibull', 'lambda', 3); %! assert_equal (pd.A, 3); %! assert_equal (pd.B, 1); %!test %! pd = makedist ('Weibull', 'lambda', 3, 'k', 2); %! assert_equal (pd.A, 3); %! assert_equal (pd.B, 2); ## Test input validation %!error makedist (1) %!error makedist (['as';'sd']) %!error makedist ('some') %!error ... %! makedist ('Beta', 'a') %!error ... %! makedist ('Beta', 'a', 1, 'Q', 23) %!error ... %! makedist ('Binomial', 'N', 1, 'Q', 23) %!error ... %! makedist ('BirnbaumSaunders', 'N', 1) %!error ... %! makedist ('Burr', 'lambda', 1, 'sdfs', 34) %!error ... %! makedist ('extremevalue', 'mu', 1, 'sdfs', 34) %!error ... %! makedist ('exponential', 'mu', 1, 'sdfs', 34) %!error ... %! makedist ('Gamma', 'k', 1, 'sdfs', 34) %!error ... %! makedist ('GeneralizedExtremeValue', 'k', 1, 'sdfs', 34) %!error ... %! makedist ('GeneralizedPareto', 'k', 1, 'sdfs', 34) %!error ... %! makedist ('HalfNormal', 'k', 1, 'sdfs', 34) %!error ... %! makedist ('InverseGaussian', 'k', 1, 'sdfs', 34) %!error ... %! makedist ('Logistic', 'k', 1, 'sdfs', 34) %!error ... %! makedist ('Loglogistic', 'k', 1, 'sdfs', 34) %!error ... %! makedist ('Lognormal', 'k', 1, 'sdfs', 34) %!error ... %! makedist ('Loguniform', 'k', 1, 'sdfs', 34) %!error ... %! makedist ('Multinomial', 'k', 1, 'sdfs', 34) %!error ... %! makedist ('Nakagami', 'mu', 1, 'sdfs', 34) %!error ... %! makedist ('NegativeBinomial', 'mu', 1, 'sdfs', 34) %!error ... %! makedist ('Normal', 'mu', 1, 'sdfs', 34) %!error ... %! makedist ('PiecewiseLinear', 'mu', 1, 'sdfs', 34) %!error ... %! makedist ('Poisson', 'mu', 1, 'sdfs', 34) %!error ... %! makedist ('Rayleigh', 'mu', 1, 'sdfs', 34) %!error ... %! makedist ('Rician', 'mu', 1, 'sdfs', 34) %!error ... %! makedist ('Stable', 'mu', 1, 'sdfs', 34) %!error ... %! makedist ('tLocationScale', 'mu', 1, 'sdfs', 34) %!error ... %! makedist ('Triangular', 'mu', 1, 'sdfs', 34) %!error ... %! makedist ('Uniform', 'mu', 1, 'sdfs', 34) %!error ... %! makedist ('Weibull', 'mu', 1, 'sdfs', 34) statistics-release-2.1.2/inst/Distribution_Wrappers/mle.m000066400000000000000000001230711527311104300235770ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{phat} =} mle (@var{x}) ## @deftypefnx {statistics} {@var{phat} =} mle (@var{x}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{phat}, @var{pci}] =} mle (@dots{}) ## ## Compute maximum likelihood estimates. ## ## @code{@var{phat} = mle (@var{x})} returns the maximum likelihood estimates ## (MLEs) for the parameters of a normal distribution using the sample data in ## @var{x}, which must be a numeric vector of real values. ## ## @code{@var{phat} = mle (@var{x}, @var{Name}, @var{Value})} returns the MLEs ## with additional options specified by @qcode{Name-Value} pair arguments listed ## below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'distribution'} @tab A character vector specifying the ## distribution type for which to estimate parameters. ## ## @item @qcode{'Ntrials'} @tab A scalar specifying the number of trials ## for the corresponding element of @var{x} for the binomial distribution. ## ## @item @qcode{'theta'} @tab A scalar specifying the location parameter ## for the generalized Pareto distribution. It defaults to 0 and is not ## estimated: the data is shifted by it and only @var{k} and @var{sigma} are ## returned. ## ## @item @qcode{'mu'} @tab A scalar specifying the location parameter ## for the half-normal distribution. ## ## @item @qcode{'censoring'} @tab A vector of the same size as @var{x} ## indicating censored data in @var{x}. By default it is ## @qcode{@var{censor} = zeros (size (@var{x}))}. ## ## @item @qcode{'frequency'} @tab A vector of nonnegative integer counts of ## the same size as @var{x} used as frequency observations. By default it is ## @qcode{@var{freq} = ones (size (@var{x}))}. ## ## @item @qcode{'alpha'} @tab A scalar in the range @math{(0,1)}, as the ## significance level for the confidence interval @var{pci}. By default it is ## 0.05 corresponding to 95% confidence intervals. ## ## @item @qcode{'options'} @tab A structure specifying the control ## parameters for the iterative algorithm used to compute ML estimates with the ## @code{fminsearch} function. ## ## @item @qcode{'pdf'} @tab A function handle ## @code{@@(@var{data}, @var{p1}, @var{p2}, @dots{})} to the probability density ## of a @strong{custom} distribution, whose parameters are then estimated by ## maximum likelihood. Requires @qcode{'start'}. It is mutually exclusive with ## @qcode{'distribution'} and with @qcode{'logpdf'}/@qcode{'nloglf'}. ## ## @item @qcode{'cdf'} @tab A function handle to the cumulative distribution ## function of the custom distribution, with the same calling convention as ## @qcode{'pdf'}. Required together with @qcode{'pdf'} for censored or ## truncated data. ## ## @item @qcode{'logpdf'} @tab A function handle to the log probability density ## of a custom distribution, with the same calling convention as @qcode{'pdf'}. ## Requires @qcode{'start'}. ## ## @item @qcode{'logsf'} @tab A function handle to the log survivor function ## @math{log (1 - cdf)} of the custom distribution, with the same calling ## convention as @qcode{'pdf'}. Required together with @qcode{'logpdf'} for ## censored data. ## ## @item @qcode{'nloglf'} @tab A function handle ## @code{@@(@var{params}, @var{data}, @var{cens}, @var{freq})} returning the ## scalar negative log-likelihood of a custom distribution. Requires ## @qcode{'start'}. ## ## @item @qcode{'start'} @tab A vector of initial parameter values for a ## custom-distribution fit. Required with @qcode{'pdf'}, @qcode{'logpdf'}, or ## @qcode{'nloglf'}. ## ## @item @qcode{'lowerbound'} @tab A scalar or vector of lower bounds for the ## custom-distribution parameters. By default they are unbounded below. ## ## @item @qcode{'upperbound'} @tab A scalar or vector of upper bounds for the ## custom-distribution parameters. By default they are unbounded above. ## ## @item @qcode{'truncationbounds'} @tab A two-element vector @qcode{[L U]} ## giving the truncation interval of a custom distribution. Requires a ## @qcode{'cdf'} function. ## ## @item @qcode{'optimfun'} @tab The optimizer for a custom-distribution fit. ## Only @qcode{'fminsearch'} is supported; bounded fits are handled by internal ## reparameterization of the constrained parameters. ## @end multitable ## ## When a custom distribution is specified through @qcode{'pdf'}, ## @qcode{'logpdf'}, or @qcode{'nloglf'}, the parameters are estimated by ## maximizing the likelihood with @code{fminsearch}, and the second output ## @var{pci} gives asymptotic normal (Wald) confidence intervals computed from ## the observed Fisher information at @var{phat} (see @code{mlecov}). Bounded ## parameters are estimated on an internally reparameterized unconstrained ## scale. ## ## Distribution names are matched ignoring case, spaces and hyphens, so that ## @qcode{'Extreme Value'}, @qcode{'ExtremeValue'} and @qcode{'extreme-value'} ## all select the same distribution, and the same set of names is accepted by ## @code{cdf}, @code{pdf}, @code{icdf}, @code{random}, @code{makedist}, ## @code{fitdist} and @code{mle}. ## ## This accepts more names than MATLAB. MATLAB takes the spaced and the ## squashed spelling but refuses the hyphenated one, so ## @qcode{'Birnbaum-Saunders'} and @qcode{'Log-Logistic'} are errors there; ## Octave has always accepted them and continues to. MATLAB also accepts ## @qcode{'tLocationScale'} in @code{makedist} while refusing it in ## @code{cdf} for the same distribution; Octave accepts it, and ## @qcode{'location-scale T'}, everywhere. Code written against MATLAB's ## names therefore runs unchanged, but code relying on these names will not ## port back. ## ## @seealso{mlecov, fitdist, makedist} ## @end deftypefn function [phat, pci] = mle (x, varargin) ## Check data if (! (isvector (x) && isnumeric (x) && isreal (x))) error ("mle: X must be a numeric vector of real values."); endif ## Add defaults censor = []; freq = ones (size (x)); alpha = 0.05; ntrials = []; mu = 0; theta = 0; options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolX = 1e-6; distname = 'normal'; userdist = false; custpdf = []; custlogpdf = []; custnloglf = []; custcdf = []; custlogsf = []; start = []; lowbnd = []; uppbnd = []; truncbnd = []; optimfun = 'fminsearch'; ## Parse extra arguments if (mod (numel (varargin), 2) != 0) error ("mle: optional arguments must be in NAME-VALUE pairs."); endif while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'distribution' distname = varargin{2}; userdist = true; case 'censoring' censor = varargin{2}; if (! isequal (size (x), size (censor)) && ! isempty (censor)) error (strcat ("mle: 'censoring' argument must have the same", ... " size as the input data in X.")); endif case 'frequency' freq = varargin{2}; if (isempty (freq)) freq = ones (size (x)); elseif (! isequal (size (x), size (freq))) error (strcat ("mle: 'frequency' argument must have the same", ... " size as the input data in X.")); endif if (any (freq != round (freq)) || any (freq < 0)) error (strcat ("mle: 'frequency' argument must contain", ... " non-negative integer values.")); endif case 'alpha' alpha = varargin{2}; if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("mle: invalid value for 'alpha' argument."); endif case 'ntrials' ntrials = varargin{2}; if (! (isscalar (ntrials) && isreal (ntrials) && ntrials > 0 && fix (ntrials) == ntrials)) error (strcat ("mle: 'ntrials' argument must be a positive", ... " integer scalar value.")); endif case {'mu'} mu = varargin{2}; case {'theta'} theta = varargin{2}; case 'options' options = varargin{2}; if (! isstruct (options) || ! isfield (options, 'Display') || ! isfield (options, 'MaxFunEvals') || ! isfield (options, 'MaxIter') || ! isfield (options, 'TolX')) error (strcat ("mle: 'options' argument must be a structure", ... " compatible for 'fminsearch'.")); endif case 'pdf' custpdf = varargin{2}; case 'logpdf' custlogpdf = varargin{2}; case 'nloglf' custnloglf = varargin{2}; case 'cdf' custcdf = varargin{2}; case 'logsf' custlogsf = varargin{2}; case 'start' start = varargin{2}; case 'lowerbound' lowbnd = varargin{2}; case 'upperbound' uppbnd = varargin{2}; case 'truncationbounds' truncbnd = varargin{2}; case 'optimfun' optimfun = varargin{2}; otherwise error ("mle: unknown parameter name."); endswitch varargin([1:2]) = []; endwhile ## Custom-distribution fit through user-supplied likelihood functions. When a ## 'pdf', 'logpdf', or 'nloglf' handle is given, the parameters are estimated ## by maximum likelihood and the named-distribution path below is bypassed. ncust = (! isempty (custpdf)) + (! isempty (custlogpdf)) ... + (! isempty (custnloglf)); if (ncust > 0) if (userdist) error (strcat ("mle: the 'distribution' argument cannot be combined", ... " with a custom 'pdf', 'logpdf', or 'nloglf' function.")); endif if (ncust > 1) error (strcat ("mle: only one of the 'pdf', 'logpdf', or 'nloglf'", ... " arguments can be specified.")); endif [phat, pci] = mle_custom (x, custpdf, custlogpdf, custnloglf, custcdf, ... custlogsf, start, lowbnd, uppbnd, truncbnd, ... censor, freq, alpha, options, optimfun, nargout); return; endif ## Switch to known distributions switch (__distname_key__ (distname)) case 'bernoulli' if (! isempty (censor)) error (strcat ("mle: censoring is not supported for", ... " the Bernoulli distribution.")); elseif (any (x != 0 & x != 1)) error ("mle: invalid data for the Bernoulli distribution."); endif if (! isempty (freq)) x = expandFreq (x, freq); endif if (nargout < 2) phat = binofit (sum (x), numel (x)); else [phat, pci] = binofit (sum (x), numel (x), alpha); endif case 'beta' if (! isempty (censor)) error ("mle: censoring is not supported for the Beta distribution."); endif if (nargout < 2) phat = betafit (x, alpha, freq, options); else [phat, pci] = betafit (x, alpha, freq, options); endif case {'binomial', 'bino'} if (! isempty (censor)) error (strcat ("mle: censoring is not supported for", ... " the Binomial distribution.")); elseif (isempty (ntrials)) error (strcat ("mle: 'Ntrials' parameter is required", ... " for the Binomial distribution.")); endif if (nargout < 2) phat = binofit (sum (x .* freq), sum (freq) .* ntrials); else [phat, pci] = binofit (sum (x .* freq), sum (freq) .* ntrials, alpha); ## binofit reports the interval as a row; mle reports one column per ## parameter, as it does for every other distribution pci = pci(:); endif case {'bisa', 'birnbaumsaunders'} if (nargout < 2) phat = bisafit (x, alpha, censor, freq, options); else [phat, pci] = bisafit (x, alpha, censor, freq, options); endif case 'burr' if (nargout < 2) phat = burrfit (x, alpha, censor, freq, options); else [phat, pci] = burrfit (x, alpha, censor, freq, options); endif case {'ev', 'extremevalue'} if (nargout < 2) phat = evfit (x, alpha, censor, freq, options); else [phat, pci] = evfit (x, alpha, censor, freq, options); endif case {'exp', 'exponential'} if (nargout < 2) phat = expfit (x, alpha, censor, freq); else [phat, pci] = expfit (x, alpha, censor, freq); endif case {'gam', 'gamma'} if (nargout < 2) phat = gamfit (x, alpha, censor, freq, options); else [phat, pci] = gamfit (x, alpha, censor, freq, options); endif case {'geo', 'geometric'} if (! isempty (censor)) error (strcat ("mle: censoring is not supported for the", ... " Geometric distribution.")); endif if (nargout < 2) phat = geofit (x, alpha, freq); else [phat, pci] = geofit (x, alpha, freq); endif case {'gev', 'generalizedextremevalue'} if (! isempty (censor)) error (strcat ("mle: censoring is not supported for the", ... " Generalized Extreme Value distribution.")); endif if (nargout < 2) phat = gevfit (x, alpha, freq, options); else [phat, pci] = gevfit (x, alpha, freq, options); endif case {'gp', 'generalizedpareto'} if (! isempty (censor)) error (strcat ("mle: censoring is not supported for", ... " the Generalized Pareto distribution.")); endif if (any (x < theta)) error (strcat ("mle: invalid 'theta' location parameter", ... " for the Generalized Pareto distribution.")); endif ## GPFIT assumes a zero location, so shift the data by the known THETA, ## which leaves the estimates of K and SIGMA unchanged. if (nargout < 2) phat = gpfit (x - theta, alpha, options, freq); else [phat, pci] = gpfit (x - theta, alpha, options, freq); endif case 'gumbel' if (nargout < 2) phat = gumbelfit (x, alpha, censor, freq, options); else [phat, pci] = gumbelfit (x, alpha, censor, freq, options); endif case {'hn', 'halfnormal'} if (! isempty (censor)) error (strcat ("mle: censoring is not supported for", ... " the Half Normal distribution.")); endif if (any (x < mu)) error (strcat ("mle: invalid 'mu' location parameter", ... " for the Half Normal distribution.")); endif if (nargout < 2) phat = hnfit (x, mu, alpha, freq); else [phat, pci] = hnfit (x, mu, alpha, freq); endif case {'invg', 'inversegaussian'} if (nargout < 2) phat = invgfit (x, alpha, censor, freq, options); else [phat, pci] = invgfit (x, alpha, censor, freq, options); endif case {'logi', 'logistic'} if (nargout < 2) phat = logifit (x, alpha, censor, freq, options); else [phat, pci] = logifit (x, alpha, censor, freq, options); endif case {'logl', 'loglogistic'} if (nargout < 2) phat = loglfit (x, alpha, censor, freq, options); else [phat, pci] = loglfit (x, alpha, censor, freq, options); endif case {'logn', 'lognormal'} if (nargout < 2) phat = lognfit (x, alpha, censor, freq, options); else [phat, pci] = lognfit (x, alpha, censor, freq, options); endif phat(2) = mle_sigma (phat(2), censor, freq); case {'naka', 'nakagami'} if (nargout < 2) phat = nakafit (x, alpha, censor, freq, options); else [phat, pci] = nakafit (x, alpha, censor, freq, options); endif case {'nbin', 'negativebinomial'} if (! isempty (censor)) error (strcat ("mle: censoring is not supported for", ... " the Negative Binomial distribution.")); endif if (nargout < 2) phat = nbinfit (x, alpha, freq, options); else [phat, pci] = nbinfit (x, alpha, freq, options); endif case {'norm', 'normal'} if (nargout < 2) [muhat, sigmahat] = normfit (x, alpha, censor, freq, options); phat = [muhat, mle_sigma(sigmahat, censor, freq)]; else [muhat, sigmahat, muci, sigmaci] = normfit (x, alpha, censor, ... freq, options); phat = [muhat, mle_sigma(sigmahat, censor, freq)]; pci = [muci, sigmaci]; endif case {'poiss', 'poisson'} if (! isempty (censor)) error (strcat ("mle: censoring is not supported for", ... " the Poisson distribution.")); endif if (nargout < 2) phat = poissfit (x, alpha, freq); else [phat, pci] = poissfit (x, alpha, freq); endif case {'rayl', 'rayleigh'} if (nargout < 2) phat = raylfit (x, alpha, censor, freq); else [phat, pci] = raylfit (x, alpha, censor, freq); endif case {'rice', 'rician'} if (nargout < 2) phat = ricefit (x, alpha, censor, freq, options); else [phat, pci] = ricefit (x, alpha, censor, freq, options); endif case {'stbl', 'stable'} if (! isempty (censor)) error (strcat ("mle: censoring is not supported for", ... " the Stable distribution.")); endif if (nargout < 2) phat = stblfit (x, alpha, freq, options); else [phat, pci] = stblfit (x, alpha, freq, options); endif case {'tls', 'tlocationscale', 'locationscalet'} if (nargout < 2) phat = tlsfit (x, alpha, censor, freq, options); else [phat, pci] = tlsfit (x, alpha, censor, freq, options); endif case {'unid', 'uniformdiscrete', 'discreteuniform', 'discrete'} if (! isempty (censor)) error (strcat ("mle: censoring is not supported for", ... " the Discrete Uniform distribution.")); endif if (nargout < 2) phat = unidfit (x, alpha, freq); else [phat, pci] = unidfit (x, alpha, freq); endif case {'unif', 'uniform', 'continuousuniform'} if (! isempty (censor)) error (strcat ("mle: censoring is not supported for", ... " the Continuous Uniform distribution.")); endif ## UNIFIT returns one output per endpoint, as MATLAB's does, which MLE ## packs into its own parameter vector and interval matrix. if (nargout < 2) [ahat, bhat] = unifit (x, alpha, freq); phat = [ahat, bhat]; else [ahat, bhat, aci, bci] = unifit (x, alpha, freq); phat = [ahat, bhat]; pci = [aci, bci]; endif case {'wbl', 'weibull'} if (nargout < 2) phat = wblfit (x, alpha, censor, freq, options); else [phat, pci] = wblfit (x, alpha, censor, freq, options); endif otherwise error ("mle: unrecognized distribution name."); endswitch endfunction ## Helper function for expanding data according to frequency vector function [x, freq] = expandFreq (x, freq) ## Drop observations whose frequency is NaN remove = isnan (freq); x(remove) = []; freq(remove) = []; ## Repeat each observation according to its frequency. Building the ## expanded vector only when some frequency differed from 1 left it ## unassigned in exactly the default case, so every call without a frequency ## vector died on an undefined variable. repelem covers all of it, drops ## zero-frequency observations, and keeps the orientation of X. x = repelem (x, freq); freq = ones (size (x)); endfunction ## Maximum likelihood fit of a user-supplied custom distribution function [phat, pci] = mle_custom (x, custpdf, custlogpdf, custnloglf, ... custcdf, custlogsf, start, lowbnd, uppbnd, truncbnd, ... censor, freq, alpha, options, optimfun, nout) ## Only fminsearch is available in core Octave; bounded fits are handled by ## reparameterization, so a separate constrained optimizer is not required. if (! (ischar (optimfun) && strcmpi (optimfun, 'fminsearch'))) error (strcat ("mle: 'optimfun' only supports 'fminsearch'; bounded", ... " fits are handled by internal reparameterization.")); endif ## 'start' is required and sets the number of parameters if (isempty (start)) error (strcat ("mle: a 'start' vector of initial parameter values is", ... " required for a custom distribution fit.")); endif if (! (isvector (start) && isnumeric (start) && isreal (start))) error ("mle: 'start' must be a numeric vector of real values."); endif start = start(:).'; k = numel (start); ## Identify the input form and validate the supplied handles if (! isempty (custpdf)) form = 'pdf'; if (! is_function_handle (custpdf)) error ("mle: 'pdf' argument must be a function handle."); endif elseif (! isempty (custlogpdf)) form = 'logpdf'; if (! is_function_handle (custlogpdf)) error ("mle: 'logpdf' argument must be a function handle."); endif else form = 'nloglf'; if (! is_function_handle (custnloglf)) error ("mle: 'nloglf' argument must be a function handle."); endif endif if (! isempty (custcdf) && ! is_function_handle (custcdf)) error ("mle: 'cdf' argument must be a function handle."); endif if (! isempty (custlogsf) && ! is_function_handle (custlogsf)) error ("mle: 'logsf' argument must be a function handle."); endif ## Data, censoring, and frequency as columns x = x(:); n = numel (x); if (isempty (censor)) cens = false (n, 1); else cens = logical (censor(:)); endif freq = freq(:); docens = any (cens); ## Truncation bounds dotrunc = ! isempty (truncbnd); if (dotrunc && ! (isnumeric (truncbnd) && isreal (truncbnd) && numel (truncbnd) == 2 && truncbnd(1) < truncbnd(2))) error (strcat ("mle: 'truncationbounds' must be a two-element vector", ... " [L U] with L < U.")); endif ## Censoring and truncation need the complementary functions if (strcmp (form, 'pdf') && (docens || dotrunc) && isempty (custcdf)) error (strcat ("mle: a 'cdf' function handle is required for censored", ... " or truncated data when using the 'pdf' argument.")); endif if (strcmp (form, 'logpdf') && docens && isempty (custlogsf)) error (strcat ("mle: a 'logsf' function handle is required for", ... " censored data when using the 'logpdf' argument.")); endif if (strcmp (form, 'logpdf') && dotrunc && isempty (custcdf)) error (strcat ("mle: a 'cdf' function handle is required for truncated", ... " data when using the 'logpdf' argument.")); endif ## Parameter bounds, expanded to per-parameter row vectors if (isempty (lowbnd)) lb = -Inf (1, k); elseif (isscalar (lowbnd)) lb = lowbnd * ones (1, k); elseif (numel (lowbnd) == k) lb = lowbnd(:).'; else error ("mle: 'lowerbound' must be a scalar or match the size of 'start'."); endif if (isempty (uppbnd)) ub = Inf (1, k); elseif (isscalar (uppbnd)) ub = uppbnd * ones (1, k); elseif (numel (uppbnd) == k) ub = uppbnd(:).'; else error ("mle: 'upperbound' must be a scalar or match the size of 'start'."); endif if (any (lb >= ub)) error (strcat ("mle: each 'lowerbound' must be strictly less than its", ... " corresponding 'upperbound'.")); endif if (any (start <= lb) || any (start >= ub)) error ("mle: 'start' values must lie strictly within the given bounds."); endif ## Aggregate negative log-likelihood of the sample at a parameter row vector nllfun = @(th) custom_nll (th, form, custpdf, custlogpdf, custnloglf, ... custcdf, custlogsf, x, cens, freq, docens, ... dotrunc, truncbnd); ## Maximize the likelihood. Bounded parameters are optimized on an ## unconstrained transformed scale and mapped back. if (any (isfinite (lb)) || any (isfinite (ub))) obj = @(u) nllfun (to_con (u, lb, ub)); uopt = fminsearch (obj, to_uncon (start, lb, ub), options); phat = to_con (uopt, lb, ub); else phat = fminsearch (nllfun, start, options); endif ## Asymptotic (Wald) confidence intervals from the observed information if (nout > 1) acov = mlecov (phat, x, 'nloglf', @(pp, dd, cc, ff) nllfun (pp)); se = sqrt (diag (acov)).'; z = norminv (1 - alpha / 2); pci = [phat - z .* se; phat + z .* se]; else pci = []; endif endfunction ## Aggregate negative log-likelihood for a custom distribution function nll = custom_nll (th, form, cpdf, clogpdf, cnloglf, ccdf, clogsf, ... x, cens, freq, docens, dotrunc, tb) if (strcmp (form, 'nloglf')) nll = cnloglf (th, x, double (cens), freq); return; endif pc = num2cell (th); unc = ! cens; terms = zeros (size (x)); if (strcmp (form, 'pdf')) terms(unc) = log (cpdf (x(unc), pc{:})); if (docens) terms(cens) = log (1 - ccdf (x(cens), pc{:})); endif else terms(unc) = clogpdf (x(unc), pc{:}); if (docens) terms(cens) = clogsf (x(cens), pc{:}); endif endif if (dotrunc) logZ = log (ccdf (tb(2), pc{:}) - ccdf (tb(1), pc{:})); ## Right-censored survival is renormalized to the truncation interval if (docens) terms(cens) = log (ccdf (tb(2), pc{:}) - ccdf (x(cens), pc{:})); endif terms = terms - logZ; endif nll = -sum (freq .* terms); endfunction ## Map constrained parameters to an unconstrained scale for optimization function u = to_uncon (th, lb, ub) u = th; for i = 1:numel (th) if (isfinite (lb(i)) && isfinite (ub(i))) u(i) = log ((th(i) - lb(i)) / (ub(i) - th(i))); elseif (isfinite (lb(i))) u(i) = log (th(i) - lb(i)); elseif (isfinite (ub(i))) u(i) = log (ub(i) - th(i)); endif endfor endfunction ## Map unconstrained parameters back to the constrained scale function th = to_con (u, lb, ub) th = u; for i = 1:numel (u) if (isfinite (lb(i)) && isfinite (ub(i))) th(i) = lb(i) + (ub(i) - lb(i)) / (1 + exp (-u(i))); elseif (isfinite (lb(i))) th(i) = lb(i) + exp (u(i)); elseif (isfinite (ub(i))) th(i) = ub(i) - exp (u(i)); endif endfor endfunction ## Turn an unbiased standard deviation into the maximum likelihood estimate. ## normfit and lognfit deliberately return the unbiased estimator when the data ## are uncensored, and each documents the correction needed to recover the MLE; ## mle promises the MLE, so it applies that correction here. With censoring ## both fits maximise the likelihood directly and no correction is wanted. function s = mle_sigma (s, censor, freq) if (isempty (censor) || ! any (censor)) n = sum (freq); s = s .* sqrt ((n - 1) ./ n); endif endfunction %!demo %! ## Fit a custom (normal) distribution by maximum likelihood and return the %! ## asymptotic 95% confidence intervals of the estimates. %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! pdf = @(x, mu, sigma) normpdf (x, mu, sigma); %! [phat, pci] = mle (x, 'pdf', pdf, 'start', [mean(x), std(x)]) ## Two distribution families were dead. 'bernoulli' expanded its data through ## a helper that only assigned the expanded vector when some frequency ## differed from 1, so the default case died on an undefined variable, and ## 'unif' called a uniffit that does not exist -- the function is unifit. %!test %! x = [1 0 1 0 1 1 0 1]; %! assert_equal (mle (x, 'distribution', 'bernoulli'), mean (x), 1e-12); %!test %! x = [1 0 1 0 1 1 0 1]; %! [phat, pci] = mle (x, 'distribution', 'bernoulli'); %! [bp, bci] = binofit (sum (x), numel (x), 0.05); %! assert_equal (phat, bp); %! assert_equal (pci, bci); %!test %! ## a frequency vector expands the sample, and a zero frequency drops it %! assert_equal (mle ([1, 0], 'distribution', 'bernoulli', ... %! 'frequency', [3, 5]), 3/8, 1e-12); %! assert_equal (mle ([1, 0], 'distribution', 'bernoulli', ... %! 'frequency', [2, 0]), 1, 1e-12); %!test %! ## a column vector must work as well as a row %! assert_equal (mle ([1;0;1;1], 'distribution', 'bernoulli'), 0.75, 1e-12); %!test %! ## the uniform MLE is the sample range %! u = [0.2, 0.5, 0.7, 0.9, 0.35]; %! assert_equal (mle (u, 'distribution', 'unif'), [min(u), max(u)]); %! assert_equal (mle (u, 'distribution', 'uniform'), [min(u), max(u)]); %! assert_equal (mle (u, 'distribution', 'continuous uniform'), ... %! [min(u), max(u)]); %!test %! u = [0.2, 0.5, 0.7, 0.9, 0.35]; %! [phat, pci] = mle (u, 'distribution', 'uniform'); %! [ahat, bhat, aci, bci] = unifit (u, 0.05); %! assert_equal (phat, [ahat, bhat]); %! assert_equal (pci, [aci, bci]); ## mle returns the maximum likelihood estimate, so the scale parameter is ## std (x, 1) and not normfit's unbiased std (x, 0). It used to return the ## unbiased value, contradicting both its own name and its custom-pdf path. %!test %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]'; %! phat = mle (x, 'distribution', 'normal'); %! assert_equal (phat(1), mean (x), 1e-12); %! assert_equal (phat(2), std (x, 1), 1e-12); %! assert_equal (isequal (abs (phat(2) - std (x, 0)) < 1e-12, true), false); %!test %! ## the named path and the equivalent custom pdf must agree %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]'; %! a = mle (x, 'distribution', 'normal'); %! b = mle (x, 'pdf', @(v, m, s) normpdf (v, m, s), 'start', [3, 1]); %! assert_equal (a, b, 1e-4); %!test %! ## lognfit is a normal fit on the logs, so it carried the same bias %! x = [1.2, 2.4, 0.9, 3.2, 1.1, 2.8, 1.3, 4.7, 2.2, 0.6, 3.0, 1.5]'; %! phat = mle (x, 'distribution', 'lognormal'); %! assert_equal (phat(2), std (log (x), 1), 1e-12); %!test %! ## a frequency vector scales the effective sample size %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8]'; %! f = [1, 2, 1, 3, 1, 2]'; %! phat = mle (x, 'distribution', 'normal', 'frequency', f); %! xx = repelem (x, f); %! assert_equal (phat(2), std (xx, 1), 1e-10); %!test %! ## with censoring normfit already maximises the likelihood: no correction %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]'; %! c = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0]'; %! ref = normfit (x, 0.05, c); %! [~, s] = normfit (x, 0.05, c); %! phat = mle (x, 'distribution', 'normal', 'censoring', c); %! assert_equal (phat(2), s, 1e-12); %!test %! ## the confidence interval is normfit's and must not shift %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]'; %! [~, pci] = mle (x, 'distribution', 'normal'); %! [~, ~, muci, sci] = normfit (x); %! assert_equal (pci, [muci, sci], 1e-12); ## Test custom-distribution fitting (values verified against MATLAB) %!test %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! [phat, pci] = mle (x, 'pdf', @(x, mu, s) normpdf (x, mu, s), ... %! 'start', [mean(x), std(x)]); %! assert_equal (phat, [3.42499970800201, 1.03208912390818], 1e-4); %! assert_equal (pci, [2.8410510441517, 0.619175174501268; ... %! 4.00894837185232, 1.44500307331509], 1e-4); %!test %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! nll = @(p, d, c, f) -sum (log (normpdf (d, p(1), p(2)))); %! phat = mle (x, 'nloglf', nll, 'start', [3, 1]); %! assert_equal (phat, [3.42499959294639, 1.03208933560634], 1e-4); %!test %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! phat = mle (x, 'logpdf', @(x, mu, s) log (normpdf (x, mu, s)), ... %! 'start', [3, 1]); %! assert_equal (phat, [3.42499959294639, 1.03208933560634], 1e-4); %!test %! ## Alpha propagates into the Wald interval %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! [~, pci] = mle (x, 'pdf', @(x, mu, s) normpdf (x, mu, s), ... %! 'start', [mean(x), std(x)], 'alpha', 0.10); %! assert_equal (pci, [2.93493454085403, 0.685560813868748; ... %! 3.91506487514999, 1.37861743394761], 1e-4); %!test %! ## Frequency-weighted fit %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! f = [1, 2, 1, 1, 3, 1, 2, 1, 1, 1, 2, 1]; %! phat = mle (x, 'pdf', @(x, mu, s) normpdf (x, mu, s), ... %! 'start', [mean(x), std(x)], 'frequency', f); %! assert_equal (phat, [3.47058837030174, 0.902783454993327], 1e-4); %!test %! ## Right-censored fit (needs a cdf) %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! c = [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0]; %! phat = mle (x, 'pdf', @(x, mu, s) normpdf (x, mu, s), ... %! 'cdf', @(x, mu, s) normcdf (x, mu, s), ... %! 'start', [mean(x), std(x)], 'censoring', c); %! assert_equal (phat, [3.62195934926527, 1.03307264832117], 1e-4); %!test %! ## Bounded fit via reparameterization (lower bound inactive at the optimum) %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! phat = mle (x, 'pdf', @(x, mu) exppdf (x, mu), 'start', 3, 'lowerbound', 0); %! assert_equal (phat, 3.42499980926514, 1e-4); %!test %! ## Truncated fit on [1, 6] %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! phat = mle (x, 'pdf', @(x, mu, s) normpdf (x, mu, s), ... %! 'cdf', @(x, mu, s) normcdf (x, mu, s), ... %! 'start', [mean(x), std(x)], 'truncationbounds', [1, 6]); %! assert_equal (phat, [3.41148048015442, 1.12176348358835], 1e-4); %!test %! ## The BirnbaumSaunders distribution is reachable by its full name, not only %! ## by 'bisa'; the name is matched after tolower, so a mixed-case label was %! ## never matched by anything. %! x = [1.2; 0.4; 3.1; 0.7; 2.5; 1.8; 0.3; 4.2; 1.1; 0.9; ... %! 2.2; 0.6; 1.5; 3.7; 0.8; 2.9; 1.3; 0.5; 2.0; 1.6]; %! assert_equal (mle (x, 'distribution', 'BirnbaumSaunders'), ... %! mle (x, 'distribution', 'bisa')); %! assert_equal (mle (x, 'distribution', 'birnbaumsaunders'), ... %! mle (x, 'distribution', 'bisa')); ## Values below are R2024a's, measured 2026-08-17. %!test %! ## the Generalized Pareto location defaults to zero and is not returned %! x = [2.2196, 11.9301, 4.3673, 1.0949, 6.5626, ... %! 1.2109, 1.8576, 1.0039, 12.7917, 2.2590]; %! assert_equal (mle (x, 'distribution', 'gp'), ... %! [-0.163107819293798, 5.305483917184919], 1e-4); %!test %! ## a known location shifts the data, leaving two parameters estimated %! x = [2.2196, 11.9301, 4.3673, 1.0949, 6.5626, ... %! 1.2109, 1.8576, 1.0039, 12.7917, 2.2590]; %! assert_equal (mle (x, 'distribution', 'gp', 'theta', 1), ... %! [0.893710299404345, 1.322962458731574], 1e-6); %!test %! ## the Continuous Uniform interval holds one endpoint per column %! u = [0.2, 0.5, 0.7, 0.9, 0.35]; %! [phat, pci] = mle (u, 'distribution', 'unif'); %! assert_equal (phat, [0.2, 0.9], 1e-12); %! assert_equal (pci, [-0.374394942118256, 0.9; ... %! 0.2, 1.474394942118256], 1e-12); ## Test input validation %!error mle (ones (2)) %!error mle ('text') %!error mle ([1, 2, 3, i, 5]) %!error ... %! mle ([1:50], 'distribution') %!error ... %! mle ([1:50], 'censoring', logical ([1,0,1,0])) %!error ... %! mle ([1:50], 'frequency', [1,0,1,0]) %!error ... %! mle ([1 0 1 0], 'frequency', [-1 1 0 0]) %!error ... %! mle ([1 0 1 0], 'distribution', 'nbin', 'frequency', [-1 1 0 0]) %!error mle ([1:50], 'alpha', [0.05, 0.01]) %!error mle ([1:50], 'alpha', 1) %!error mle ([1:50], 'alpha', -1) %!error mle ([1:50], 'alpha', i) %!error ... %! mle ([1:50], 'ntrials', -1) %!error ... %! mle ([1:50], 'ntrials', [20, 50]) %!error ... %! mle ([1:50], 'ntrials', [20.3]) %!error ... %! mle ([1:50], 'ntrials', 3i) %!error ... %! mle ([1:50], 'options', 4) %!error ... %! mle ([1:50], 'options', struct ('x', 3)) %!error mle ([1:50], 'NAME', 'value') %!error ... %! mle ([1 0 1 0], 'distribution', 'bernoulli', 'censoring', [1 1 0 0]) %!error ... %! mle ([1 2 1 0], 'distribution', 'bernoulli') %!error ... %! mle ([1 0 1 0], 'distribution', 'beta', 'censoring', [1 1 0 0]) %!error ... %! mle ([1 0 1 0], 'distribution', 'bino', 'censoring', [1 1 0 0]) %!error ... %! mle ([1 0 1 0], 'distribution', 'bino') %!error ... %! mle ([1 0 1 0], 'distribution', 'geo', 'censoring', [1 1 0 0]) %!error ... %! mle ([1 0 1 0], 'distribution', 'gev', 'censoring', [1 1 0 0]) %!error ... %! mle ([1 0 1 0], 'distribution', 'gp', 'censoring', [1 1 0 0]) %!error ... %! mle ([1 0 -1 0], 'distribution', 'gp') %!error ... %! mle ([1 0 1 0], 'distribution', 'hn', 'censoring', [1 1 0 0]) %!error ... %! mle ([1 0 -1 0], 'distribution', 'hn') %!error ... %! mle ([1 0 1 0], 'distribution', 'nbin', 'censoring', [1 1 0 0]) %!error ... %! mle ([1 0 1 0], 'distribution', 'poisson', 'censoring', [1 1 0 0]) %!error ... %! mle ([1 0 1 0], 'distribution', 'unid', 'censoring', [1 1 0 0]) %!error ... %! mle ([1 0 1 0], 'distribution', 'unif', 'censoring', [1 1 0 0]) %!error mle ([1:50], 'distribution', 'value') %!error ... %! mle ([1 0 1 0], 'distribution', 'unif', 'censoring', [1 1 0 0]) %!error ... %! mle ([1:50], 'distribution', 'normal', 'pdf', @(x, a, b) normpdf (x, a, b)) %!error ... %! mle ([1:50], 'pdf', @sin, 'nloglf', @cos) %!error ... %! mle ([1:50], 'pdf', @(x, a, b) normpdf (x, a, b)) %!error ... %! mle ([1:50], 'pdf', @(x, a, b) normpdf (x, a, b), 'start', 'text') %!error ... %! mle ([1:50], 'pdf', 5, 'start', [0, 1]) %!error ... %! mle ([1:50], 'nloglf', 5, 'start', [0, 1]) %!error ... %! mle ([1:50], 'pdf', @(x, a, b) normpdf (x, a, b), 'cdf', 5, 'start', [0, 1]) %!error ... %! mle ([1:50], 'pdf', @(x, a, b) normpdf (x, a, b), 'start', [0, 1], ... %! 'censoring', [1, zeros(1, 49)]) %!error ... %! mle ([1:50], 'pdf', @(x, a, b) normpdf (x, a, b), ... %! 'cdf', @(x, a, b) normcdf (x, a, b), 'start', [0, 1], ... %! 'truncationbounds', [6, 1]) %!error ... %! mle ([1:50], 'pdf', @(x, a, b) normpdf (x, a, b), 'start', [0, 1], ... %! 'lowerbound', [0, 2], 'upperbound', [0, 5]) %!error ... %! mle ([1:50], 'pdf', @(x, a, b) normpdf (x, a, b), 'start', [0, 1], ... %! 'lowerbound', [1, 0]) %!error ... %! mle ([1:50], 'pdf', @(x, a, b) normpdf (x, a, b), 'start', [0, 1], ... %! 'optimfun', 'fmincon') statistics-release-2.1.2/inst/Distribution_Wrappers/mlecov.m000066400000000000000000000471541527311104300243160ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{acov} =} mlecov (@var{params}, @var{data}, @var{Name}, @var{Value}) ## ## Asymptotic covariance matrix of maximum likelihood estimators. ## ## @code{@var{acov} = mlecov (@var{params}, @var{data}, @dots{})} ## returns an approximation to the asymptotic covariance matrix of the maximum ## likelihood estimators of the parameters of a distribution, evaluated at the ## parameter values in @var{params} for the sample data in @var{data}. ## @var{params} is a numeric vector of parameter values (typically the estimates ## returned by @code{mle} or @code{fitdist}) and @var{data} is a numeric vector ## of the sample observations. @var{acov} is a @math{p*p} matrix, where ## @math{p = numel (@var{params})}. ## ## The distribution is not identified by name; instead it is supplied through ## @qcode{Name-Value} paired arguments that give function handles to its ## density, its log density, or its negative log-likelihood. Exactly ## @strong{one} of the following three arguments must be specified: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'pdf'} @tab A function handle, ## @code{@var{f}(@var{data}, @var{p1}, @var{p2}, @dots{})}, that accepts the ## sample data as its first argument and the distribution parameters as ## subsequent scalar arguments, and returns a vector of probability density ## values, one per observation. ## ## @item @qcode{'logpdf'} @tab A function handle, ## @code{@var{f}(@var{data}, @var{p1}, @var{p2}, @dots{})}, with the same ## calling convention as @qcode{'pdf'} but returning the @emph{logarithm} of ## the density. ## ## @item @qcode{'nloglf'} @tab A function handle, ## @code{@var{nll}(@var{params}, @var{data}, @var{cens}, @var{freq})}, that ## returns the scalar negative log-likelihood of the whole sample. It receives ## the current parameter vector, the data, the censoring vector, and the ## frequency vector, and is responsible for incorporating censoring and ## frequency itself. ## ## @item @qcode{'cdf'} @tab A function handle to the cumulative distribution ## function, with the same calling convention as @qcode{'pdf'}. It is ## @strong{required} together with @qcode{'pdf'} when the data are censored, so ## that censored observations can contribute their survival probability. ## ## @item @qcode{'logsf'} @tab A function handle to the logarithm of the survivor ## function @math{log (1 - cdf)}, with the same calling convention as ## @qcode{'pdf'}. It is @strong{required} together with @qcode{'logpdf'} when ## the data are censored. ## ## @item @qcode{'Censoring'} @tab A vector of the same size as @var{data} ## indicating censored observations (nonzero for right-censored). By default no ## observation is censored. ## ## @item @qcode{'Frequency'} @tab A vector of nonnegative integer counts of the ## same size as @var{data}, giving the number of times each observation was ## observed. By default it is @qcode{ones (size (@var{data}))}. ## ## @item @qcode{'Options'} @tab A structure that may contain a ## @qcode{'DerivStep'} field specifying the relative finite-difference step ## used to approximate the Hessian (a positive scalar or a vector the same size ## as @var{params}). The default step is @qcode{eps ^ (1/4)}. ## @end multitable ## ## @strong{Computation and numerical behavior.} @code{mlecov} approximates the ## covariance matrix as the inverse of the observed Fisher information, that is, ## the inverse of the Hessian of the @emph{aggregate} negative log-likelihood of ## the sample, evaluated by central finite differences at @var{params}. The ## covariance is computed @emph{at} the supplied @var{params}; @code{mlecov} ## does not refit the parameters, so @var{params} should be the maximum ## likelihood estimates for the result to be meaningful. ## ## Whichever of @qcode{'pdf'}, @qcode{'logpdf'}, or @qcode{'nloglf'} is ## supplied, the Hessian is always formed by differencing the same aggregate ## negative log-likelihood rather than by differentiating the density itself. ## This makes the three input forms consistent with one another and is ## numerically far more stable than differentiating a density; as a consequence ## @var{acov} may differ from other implementations (including MATLAB) in ## ill-conditioned cases where those differentiate the density directly and ## return unreliable values or @code{NaN}. If the computed Hessian is not ## positive definite (for example when @var{params} is not at a likelihood ## maximum), a warning is issued and @var{acov} is returned as an ## all-@code{NaN} matrix. ## ## @seealso{mle, fitdist, makedist} ## @end deftypefn function acov = mlecov (params, data, varargin) ## Check number of input arguments if (nargin < 3) print_usage (); endif ## Check PARAMS and DATA if (! (isvector (params) && isnumeric (params) && isreal (params) && ! isempty (params))) error ("mlecov: PARAMS must be a nonempty numeric vector of real values."); endif if (! (isvector (data) && isnumeric (data) && isreal (data) && ! isempty (data))) error ("mlecov: DATA must be a nonempty numeric vector of real values."); endif ## Add defaults pdf = []; logpdf = []; nloglf = []; cdf = []; logsf = []; censor = []; freq = []; derivstep = eps ^ (1/4); ## Parse optional arguments as NAME-VALUE pairs if (mod (numel (varargin), 2) != 0) error ("mlecov: optional arguments must be in NAME-VALUE pairs."); endif while (numel (varargin) > 0) name = varargin{1}; value = varargin{2}; if (! (ischar (name) && isrow (name))) error ("mlecov: NAME arguments must be character vectors."); endif switch (tolower (name)) case 'pdf' pdf = value; case 'logpdf' logpdf = value; case 'nloglf' nloglf = value; case 'cdf' cdf = value; case 'logsf' logsf = value; case 'censoring' censor = value; case 'frequency' freq = value; case 'options' if (! isstruct (value)) error ("mlecov: 'Options' argument must be a structure."); endif if (isfield (value, 'DerivStep') && ! isempty (value.DerivStep)) derivstep = value.DerivStep; endif otherwise error ("mlecov: unknown parameter name '%s'.", name); endswitch varargin([1:2]) = []; endwhile ## Exactly one distribution function must be specified nfun = (! isempty (pdf)) + (! isempty (logpdf)) + (! isempty (nloglf)); if (nfun == 0) error (strcat ("mlecov: a distribution must be specified with one of", ... " the 'pdf', 'logpdf', or 'nloglf' arguments.")); elseif (nfun > 1) error (strcat ("mlecov: only one of the 'pdf', 'logpdf', or 'nloglf'", ... " arguments can be specified.")); endif ## Check that supplied handles are function handles if (! isempty (pdf) && ! is_function_handle (pdf)) error ("mlecov: 'pdf' argument must be a function handle."); endif if (! isempty (logpdf) && ! is_function_handle (logpdf)) error ("mlecov: 'logpdf' argument must be a function handle."); endif if (! isempty (nloglf) && ! is_function_handle (nloglf)) error ("mlecov: 'nloglf' argument must be a function handle."); endif if (! isempty (cdf) && ! is_function_handle (cdf)) error ("mlecov: 'cdf' argument must be a function handle."); endif if (! isempty (logsf) && ! is_function_handle (logsf)) error ("mlecov: 'logsf' argument must be a function handle."); endif ## Add defaults and validate FREQUENCY and CENSORING if (isempty (freq)) freq = ones (size (data)); elseif (! isequal (size (data), size (freq))) error (strcat ("mlecov: 'Frequency' argument must have the same size", ... " as the input data in DATA.")); elseif (any (freq(:) < 0) || any (freq(:) != round (freq(:)))) error (strcat ("mlecov: 'Frequency' argument must contain non-negative", ... " integer values.")); endif if (isempty (censor)) censor = zeros (size (data)); elseif (! isequal (size (data), size (censor))) error (strcat ("mlecov: 'Censoring' argument must have the same size", ... " as the input data in DATA.")); endif docens = any (censor(:) != 0); ## Censored data need a survivor function for the 'pdf'/'logpdf' forms if (docens && ! isempty (pdf) && isempty (cdf)) error (strcat ("mlecov: a 'cdf' function handle is required for", ... " censored data when using the 'pdf' argument.")); endif if (docens && ! isempty (logpdf) && isempty (logsf)) error (strcat ("mlecov: a 'logsf' function handle is required for", ... " censored data when using the 'logpdf' argument.")); endif ## Validate DerivStep and build the per-parameter finite-difference step theta = params(:).'; p = numel (theta); if (! (isnumeric (derivstep) && isreal (derivstep) && all (derivstep(:) > 0) && (isscalar (derivstep) || numel (derivstep) == p))) error (strcat ("mlecov: 'DerivStep' must be a positive real scalar or", ... " a vector the same size as PARAMS.")); endif hstep = derivstep(:).' .* max (abs (theta), 1); ## Assemble the aggregate negative log-likelihood as a function of the ## parameter vector only, capturing data, censoring, and frequency if (! isempty (pdf)) form = 'pdf'; elseif (! isempty (logpdf)) form = 'logpdf'; else form = 'nloglf'; endif nllfun = @(t) aggregate_nll (t, form, pdf, cdf, logpdf, logsf, nloglf, ... data, censor, freq, docens); ## Central finite-difference Hessian of the negative log-likelihood H = num_hessian (nllfun, theta, hstep); H = (H + H') / 2; ## Invert only if the Hessian is positive definite; otherwise warn and NaN [~, notpd] = chol (H); if (notpd != 0) warning (strcat ("mlecov: unable to compute a covariance matrix", ... " because the computed Hessian matrix is not positive", ... " definite.")); acov = NaN (p); else acov = inv (H); acov = (acov + acov') / 2; endif endfunction ## Aggregate negative log-likelihood at parameter vector T function nll = aggregate_nll (t, form, pdf, cdf, logpdf, logsf, nloglf, ... data, censor, freq, docens) switch (form) case 'pdf' pc = num2cell (t); dens = pdf (data, pc{:}); if (docens) surv = 1 - cdf (data, pc{:}); terms = (censor == 0) .* log (dens) + (censor != 0) .* log (surv); else terms = log (dens); endif nll = -sum (freq(:) .* terms(:)); case 'logpdf' pc = num2cell (t); lpd = logpdf (data, pc{:}); if (docens) lsf = logsf (data, pc{:}); terms = (censor == 0) .* lpd + (censor != 0) .* lsf; else terms = lpd; endif nll = -sum (freq(:) .* terms(:)); case 'nloglf' nll = nloglf (t, data, censor, freq); endswitch endfunction ## Central finite-difference approximation of the Hessian of NLLFUN at THETA function H = num_hessian (nllfun, theta, hstep) p = numel (theta); H = zeros (p); f0 = nllfun (theta); for i = 1:p ei = zeros (1, p); ei(i) = hstep(i); fpi = nllfun (theta + ei); fmi = nllfun (theta - ei); H(i,i) = (fpi - 2 * f0 + fmi) / (hstep(i) ^ 2); for j = (i + 1):p ej = zeros (1, p); ej(j) = hstep(j); fpp = nllfun (theta + ei + ej); fpm = nllfun (theta + ei - ej); fmp = nllfun (theta - ei + ej); fmm = nllfun (theta - ei - ej); H(i,j) = (fpp - fpm - fmp + fmm) / (4 * hstep(i) * hstep(j)); H(j,i) = H(i,j); endfor endfor endfunction %!demo %! ## Asymptotic covariance matrix of the ML estimates for a normal fit. %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! phat = mle (x); %! acov = mlecov (phat, x, 'pdf', @(x, mu, sigma) normpdf (x, mu, sigma)) ## Reference values below were computed with MATLAB's mlecov using the reliable ## 'nloglf' input form (its 'pdf'/'logpdf' paths differentiate the density ## directly and return NaN or unreliable values on these inputs). %!test %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! phat = [mean(x), std(x, 1)]; %! nll = @(p, data, cens, freq) -sum (log (normpdf (data, p(1), p(2)))); %! acov = mlecov (phat, x, 'nloglf', nll); %! assert_equal (acov, [0.0887673606073251, 0; 0, 0.0443836789388774], 1e-6); %!test %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! phat = [3.5, 1.0]; %! nll = @(p, data, cens, freq) -sum (log (normpdf (data, p(1), p(2)))); %! acov = mlecov (phat, x, 'nloglf', nll); %! ref = [0.0841894975321553, 0.00570776282157731; ... %! 0.00570776282157731, 0.0380517511049636]; %! assert_equal (acov, ref, 1e-6); %!test %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! phat = gamfit (x); %! nll = @(p, data, cens, freq) -sum (log (gampdf (data, p(1), p(2)))); %! acov = mlecov (phat, x, 'nloglf', nll); %! ref = [17.6711714212941, -0.553235139352906; ... %! -0.553235139352906, 0.0181745610037496]; %! assert_equal (acov, ref, 5e-4); %!test %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! f = [1, 2, 1, 1, 3, 1, 2, 1, 1, 1, 2, 1]; %! phat = [mean(x), std(x, 1)]; %! nll = @(p, data, cens, freq) -sum (freq .* log (normpdf (data, p(1), p(2)))); %! acov = mlecov (phat, x, 'nloglf', nll, 'Frequency', f); %! ref = [0.0630373869314514, -0.00427967204059965; ... %! -0.00427967204059965, 0.0484445550668851]; %! assert_equal (acov, ref, 1e-6); %!test %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! c = [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0]; %! phat = [mean(x), std(x, 1)]; %! nll = @(p, data, cens, freq) -sum ((1 - cens) .* ... %! log (normpdf (data, p(1), p(2))) + ... %! cens .* log (1 - normcdf (data, p(1), p(2)))); %! acov = mlecov (phat, x, 'nloglf', nll, 'Censoring', c); %! ref = [0.100361972376949, -0.0148199117431384; ... %! -0.0148199117431384, 0.054254671056408]; %! assert_equal (acov, ref, 1e-6); ## The 'pdf', 'logpdf', and 'nloglf' forms differentiate the same aggregate ## negative log-likelihood, so they agree (unlike MATLAB, whose density-based ## 'pdf' path returns NaN for the normal and 51.84 for the exponential here). %!test %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! phat = [mean(x), std(x, 1)]; %! a_pdf = mlecov (phat, x, 'pdf', @(x, mu, s) normpdf (x, mu, s)); %! a_logpdf = mlecov (phat, x, 'logpdf', @(x, mu, s) log (normpdf (x, mu, s))); %! a_nloglf = mlecov (phat, x, 'nloglf', ... %! @(p, d, c, f) -sum (log (normpdf (d, p(1), p(2))))); %! assert_equal (a_pdf, a_nloglf, 1e-10); %! assert_equal (a_logpdf, a_nloglf, 1e-10); %! assert_equal (a_pdf, [0.0887673606073251, 0; 0, 0.0443836789388774], 1e-6); %!test %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! acov = mlecov (mean (x), x, 'pdf', @(x, mu) exppdf (x, mu)); %! assert_equal (acov, 0.977552156166645, 1e-6); ## Censored 'pdf' form (with a 'cdf' handle) matches the hand-built censored ## 'nloglf' likelihood. %!test %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! c = [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0]; %! phat = [mean(x), std(x, 1)]; %! acov = mlecov (phat, x, 'pdf', @(x, mu, s) normpdf (x, mu, s), ... %! 'cdf', @(x, mu, s) normcdf (x, mu, s), 'Censoring', c); %! ref = [0.100361972376949, -0.0148199117431384; ... %! -0.0148199117431384, 0.054254671056408]; %! assert_equal (acov, ref, 1e-6); ## A user-supplied 'DerivStep' still yields the correct covariance. %!test %! x = [2.1, 3.4, 1.9, 5.2, 4.1, 2.8, 3.3, 4.7, 2.2, 3.9, 3.0, 4.5]; %! phat = [mean(x), std(x, 1)]; %! nll = @(p, data, cens, freq) -sum (log (normpdf (data, p(1), p(2)))); %! acov = mlecov (phat, x, 'nloglf', nll, ... %! 'Options', struct ('DerivStep', 1e-4)); %! assert_equal (acov, [0.0887673606073251, 0; 0, 0.0443836789388774], 1e-5); ## A non-positive-definite Hessian yields a warning and an all-NaN matrix. %!warning ... %! mlecov (1, [1, 2, 3, 4, 5], 'nloglf', @(p, d, c, f) -sum (p(1) .* d)); %!test %! S = warning ("off", "all"); %! acov = mlecov (1, [1, 2, 3, 4, 5], 'nloglf', @(p, d, c, f) -sum (p(1) .* d)); %! warning (S); %! assert_equal (all (isnan (acov), 'all'), true); ## Test input validation %!error mlecov (1, [1, 2, 3]) %!error ... %! mlecov ([1, 2; 3, 4], [1, 2, 3], 'pdf', @(x, a) x) %!error ... %! mlecov ([1, 2i], [1, 2, 3], 'pdf', @(x, a, b) x) %!error ... %! mlecov ([1, 2], ones (2, 2), 'pdf', @(x, a, b) x) %!error ... %! mlecov ([1, 2], [1, 2, 3], 'pdf') %!error ... %! mlecov ([1, 2], [1, 2, 3], 5, @sin) %!error ... %! mlecov ([1, 2], [1, 2, 3], 'Frequency', [1, 1, 1]) %!error ... %! mlecov ([1, 2], [1, 2, 3], 'pdf', @sin, 'nloglf', @cos) %!error ... %! mlecov ([1, 2], [1, 2, 3], 'pdf', 5) %!error ... %! mlecov ([1, 2], [1, 2, 3], 'nloglf', 'text') %!error ... %! mlecov ([1, 2], [1, 2, 3], 'pdf', @sin, 'bogus', 1) %!error ... %! mlecov ([1, 2], [1, 2, 3], 'nloglf', @(varargin) 1, 'Frequency', [1, 1]) %!error ... %! mlecov ([1, 2], [1, 2, 3], 'nloglf', @(varargin) 1, 'Frequency', [1, 0.5, 1]) %!error ... %! mlecov ([1, 2], [1, 2, 3], 'nloglf', @(varargin) 1, 'Censoring', [1, 0]) %!error ... %! mlecov ([1, 2], [1, 2, 3], 'pdf', @(x, a, b) x, 'Censoring', [1, 0, 0]) %!error ... %! mlecov ([1, 2], [1, 2, 3], 'logpdf', @(x, a, b) x, 'Censoring', [1, 0, 0]) %!error ... %! mlecov ([1, 2], [1, 2, 3], 'nloglf', @(varargin) 1, 'Options', 5) %!error ... %! mlecov ([1, 2], [1, 2, 3], 'nloglf', @(varargin) 1, ... %! 'Options', struct ('DerivStep', -1)) statistics-release-2.1.2/inst/Distribution_Wrappers/pdf.m000066400000000000000000000371201527311104300235720ustar00rootroot00000000000000## Copyright (C) 2016 Andreas Stahel ## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{y} =} pdf (@var{name}, @var{x}, @var{A}) ## @deftypefnx {statistics} {@var{y} =} pdf (@var{name}, @var{x}, @var{A}, @var{B}) ## @deftypefnx {statistics} {@var{y} =} pdf (@var{name}, @var{x}, @var{A}, @var{B}, @var{C}) ## ## Return the PDF of a univariate distribution evaluated at @var{x}. ## ## @code{pdf} is a wrapper for the univariate cumulative distribution functions ## available in the statistics package. See the corresponding functions' help ## to learn the signification of the parameters after @var{x}. ## ## @code{@var{y} = pdf (@var{name}, @var{x}, @var{A})} returns the CDF for the ## one-parameter distribution family specified by @var{name} and the ## distribution parameter @var{A}, evaluated at the values in @var{x}. ## ## @code{@var{y} = pdf (@var{name}, @var{x}, @var{A}, @var{B})} returns the CDF ## for the two-parameter distribution family specified by @var{name} and the ## distribution parameters @var{A} and @var{B}, evaluated at the values in ## @var{x}. ## ## @code{@var{y} = pdf (@var{name}, @var{x}, @var{A}, @var{B}, @var{C})} returns ## the CDF for the three-parameter distribution family specified by @var{name} ## and the distribution parameters @var{A}, @var{B}, and @var{C}, evaluated at ## the values in @var{x}. ## ## @var{name} must be a char string of the name or the abbreviation of the ## desired cumulative distribution function as listed in the following table. ## The last column shows the number of required parameters that should be parsed ## after @var{x} to the desired PDF. ## ## @multitable @columnfractions 0.4 0.2 0.3 ## @headitem Distribution Name @tab Abbreviation @tab Input Parameters ## @item @qcode{'Beta'} @tab @qcode{'beta'} @tab 2 ## @item @qcode{'Binomial'} @tab @qcode{'bino'} @tab 2 ## @item @qcode{'Birnbaum-Saunders'} @tab @qcode{'bisa'} @tab 2 ## @item @qcode{'Burr'} @tab @qcode{'burr'} @tab 3 ## @item @qcode{'Cauchy'} @tab @qcode{'cauchy'} @tab 2 ## @item @qcode{'Chi-squared'} @tab @qcode{'chi2'} @tab 1 ## @item @qcode{'Extreme Value'} @tab @qcode{'ev'} @tab 2 ## @item @qcode{'Exponential'} @tab @qcode{'exp'} @tab 1 ## @item @qcode{'F-Distribution'} @tab @qcode{'f'} @tab 2 ## @item @qcode{'Gamma'} @tab @qcode{'gam'} @tab 2 ## @item @qcode{'Geometric'} @tab @qcode{'geo'} @tab 1 ## @item @qcode{'Generalized Extreme Value'} @tab @qcode{'gev'} @tab 3 ## @item @qcode{'Generalized Pareto'} @tab @qcode{'gp'} @tab 3 ## @item @qcode{'Gumbel'} @tab @qcode{'gumbel'} @tab 2 ## @item @qcode{'Half-normal'} @tab @qcode{'hn'} @tab 2 ## @item @qcode{'Hypergeometric'} @tab @qcode{'hyge'} @tab 3 ## @item @qcode{'Inverse Gaussian'} @tab @qcode{'invg'} @tab 2 ## @item @qcode{'Laplace'} @tab @qcode{'laplace'} @tab 2 ## @item @qcode{'Logistic'} @tab @qcode{'logi'} @tab 2 ## @item @qcode{'Log-Logistic'} @tab @qcode{'logl'} @tab 2 ## @item @qcode{'Lognormal'} @tab @qcode{'logn'} @tab 2 ## @item @qcode{'Nakagami'} @tab @qcode{'naka'} @tab 2 ## @item @qcode{'Negative Binomial'} @tab @qcode{'nbin'} @tab 2 ## @item @qcode{'Noncentral F-Distribution'} @tab @qcode{'ncf'} @tab 3 ## @item @qcode{'Noncentral Student T'} @tab @qcode{'nct'} @tab 2 ## @item @qcode{'Noncentral Chi-Squared'} @tab @qcode{'ncx2'} @tab 2 ## @item @qcode{'Normal'} @tab @qcode{'norm'} @tab 2 ## @item @qcode{'Poisson'} @tab @qcode{'poiss'} @tab 1 ## @item @qcode{'Rayleigh'} @tab @qcode{'rayl'} @tab 1 ## @item @qcode{'Rician'} @tab @qcode{'rice'} @tab 2 ## @item @qcode{'Student T'} @tab @qcode{'t'} @tab 1 ## @item @qcode{'location-scale T'} @tab @qcode{'tls'} @tab 3 ## @item @qcode{'Triangular'} @tab @qcode{'tri'} @tab 3 ## @item @qcode{'Discrete Uniform'} @tab @qcode{'unid'} @tab 1 ## @item @qcode{'Uniform'} @tab @qcode{'unif'} @tab 2 ## @item @qcode{'Von Mises'} @tab @qcode{'vm'} @tab 2 ## @item @qcode{'Weibull'} @tab @qcode{'wbl'} @tab 2 ## @end multitable ## ## Distribution names are matched ignoring case, spaces and hyphens, so that ## @qcode{'Extreme Value'}, @qcode{'ExtremeValue'} and @qcode{'extreme-value'} ## all select the same distribution, and the same set of names is accepted by ## @code{cdf}, @code{pdf}, @code{icdf}, @code{random}, @code{makedist}, ## @code{fitdist} and @code{mle}. ## ## This accepts more names than MATLAB. MATLAB takes the spaced and the ## squashed spelling but refuses the hyphenated one, so ## @qcode{'Birnbaum-Saunders'} and @qcode{'Log-Logistic'} are errors there; ## Octave has always accepted them and continues to. MATLAB also accepts ## @qcode{'tLocationScale'} in @code{makedist} while refusing it in ## @code{cdf} for the same distribution; Octave accepts it, and ## @qcode{'location-scale T'}, everywhere. Code written against MATLAB's ## names therefore runs unchanged, but code relying on these names will not ## port back. ## ## @seealso{cdf, icdf, random, betapdf, binopdf, bisapdf, burrpdf, cauchypdf, ## chi2pdf, evpdf, exppdf, fpdf, gampdf, geopdf, gevpdf, gppdf, gumbelpdf, ## hnpdf, hygepdf, invgpdf, laplacepdf, logipdf, loglpdf, lognpdf, nakapdf, ## nbinpdf, ncfpdf, nctpdf, ncx2pdf, normpdf, poisspdf, raylpdf, ricepdf, tpdf, ## tlspdf, tripdf, unidpdf, unifpdf, vmpdf, wblpdf} ## @end deftypefn function y = pdf (name, x, varargin) ## implemented functions persistent allDF = { ... {'beta' , 'Beta'}, @betapdf, 2, ... {'bino' , 'Binomial'}, @binopdf, 2, ... {'bisa' , 'Birnbaum-Saunders'}, @bisapdf, 2, ... {'burr' , 'Burr'}, @burrpdf, 3, ... {'cauchy' , 'Cauchy'}, @cauchypdf, 2, ... {'chi2' , 'Chi-squared'}, @chi2pdf, 1, ... {'ev' , 'Extreme Value'}, @evpdf, 2, ... {'exp' , 'Exponential'}, @exppdf, 1, ... {'f' , 'F-Distribution'}, @fpdf, 2, ... {'gam' , 'Gamma'}, @gampdf, 2, ... {'geo' , 'Geometric'}, @geopdf, 1, ... {'gev' , 'Generalized Extreme Value'}, @gevpdf, 3, ... {'gp' , 'Generalized Pareto'}, @gppdf, 3, ... {'gumbel' , 'Gumbel'}, @gumbelpdf, 2, ... {'hn' , 'Half-normal'}, @hnpdf, 2, ... {'hyge' , 'Hypergeometric'}, @hygepdf, 3, ... {'invg' , 'Inverse Gaussian'}, @invgpdf, 2, ... {'laplace' , 'Laplace'}, @laplacepdf, 2, ... {'logi' , 'Logistic'}, @logipdf, 2, ... {'logl' , 'Log-Logistic'}, @loglpdf, 2, ... {'logn' , 'Lognormal'}, @lognpdf, 2, ... {'naka' , 'Nakagami'}, @nakapdf, 2, ... {'nbin' , 'Negative Binomial'}, @nbinpdf, 2, ... {'ncf' , 'Noncentral F-Distribution'}, @ncfpdf, 3, ... {'nct' , 'Noncentral Student T'}, @nctpdf, 2, ... {'ncx2' , 'Noncentral Chi-squared'}, @ncx2pdf, 2, ... {'norm' , 'Normal'}, @normpdf, 2, ... {'poiss' , 'Poisson'}, @poisspdf, 1, ... {'rayl' , 'Rayleigh'}, @raylpdf, 1, ... {'rice' , 'Rician'}, @ricepdf, 2, ... {'t' , 'Student T'}, @tpdf, 1, ... {'tls', 'location-scale T', 'tLocationScale'}, @tlspdf, 3, ... {'tri' , 'Triangular'}, @tripdf, 3, ... {'unid' , 'Discrete Uniform'}, @unidpdf, 1, ... {'unif' , 'Uniform'}, @unifpdf, 2, ... {'vm' , 'Von Mises'}, @vmpdf, 2, ... {'wbl' , 'Weibull'}, @wblpdf, 2}; if (! ischar (name)) error ("pdf: distribution NAME must a char string."); endif ## Check X being numeric and real if (! isnumeric (x)) error ("pdf: X must be numeric."); elseif (! isreal (x)) error ("pdf: values in X must be real."); endif ## Get number of arguments nargs = numel (varargin); ## Get available functions pdfnames = allDF(1:3:end); pdfhandl = allDF(2:3:end); pdf_args = allDF(3:3:end); ## Search for PDF function ## Match on the folded key so that every spelling of a name resolves key = __distname_key__ (name); idx = cellfun (@(x) any (strcmp (key, cellfun (@__distname_key__, x, ... 'UniformOutput', false))), pdfnames); if (any (idx)) if (nargs == pdf_args{idx}) ## Check that all distribution parameters are numeric if (! all (cellfun (@(x)isnumeric (x), (varargin)))) error ("pdf: distribution parameters must be numeric."); endif ## Call appropriate iCDF y = feval (pdfhandl{idx}, x, varargin{:}); else if (pdf_args{idx} == 1) error ("pdf: %s distribution requires 1 parameter.", name); else error ("pdf: %s distribution requires %d parameters.", ... name, pdf_args{idx}); endif endif else error ("pdf: %s distribution is not implemented in Statistics.", name); endif endfunction ## Test results %!shared x %! x = [1:5]; %!assert_equal (pdf ('Beta', x, 5, 2), betapdf (x, 5, 2)) %!assert_equal (pdf ('beta', x, 5, 2), betapdf (x, 5, 2)) %!assert_equal (pdf ('Binomial', x, 5, 2), binopdf (x, 5, 2)) %!assert_equal (pdf ('bino', x, 5, 2), binopdf (x, 5, 2)) %!assert_equal (pdf ('Birnbaum-Saunders', x, 5, 2), bisapdf (x, 5, 2)) %!assert_equal (pdf ('bisa', x, 5, 2), bisapdf (x, 5, 2)) %!assert_equal (pdf ('Burr', x, 5, 2, 2), burrpdf (x, 5, 2, 2)) %!assert_equal (pdf ('burr', x, 5, 2, 2), burrpdf (x, 5, 2, 2)) %!assert_equal (pdf ('Cauchy', x, 5, 2), cauchypdf (x, 5, 2)) %!assert_equal (pdf ('cauchy', x, 5, 2), cauchypdf (x, 5, 2)) %!assert_equal (pdf ('Chi-squared', x, 5), chi2pdf (x, 5)) %!assert_equal (pdf ('chi2', x, 5), chi2pdf (x, 5)) %!assert_equal (pdf ('Extreme Value', x, 5, 2), evpdf (x, 5, 2)) %!assert_equal (pdf ('ev', x, 5, 2), evpdf (x, 5, 2)) %!assert_equal (pdf ('Exponential', x, 5), exppdf (x, 5)) %!assert_equal (pdf ('exp', x, 5), exppdf (x, 5)) %!assert_equal (pdf ('F-Distribution', x, 5, 2), fpdf (x, 5, 2)) %!assert_equal (pdf ('f', x, 5, 2), fpdf (x, 5, 2)) %!assert_equal (pdf ('Gamma', x, 5, 2), gampdf (x, 5, 2)) %!assert_equal (pdf ('gam', x, 5, 2), gampdf (x, 5, 2)) %!assert_equal (pdf ('Geometric', x, 5), geopdf (x, 5)) %!assert_equal (pdf ('geo', x, 5), geopdf (x, 5)) %!assert_equal (pdf ('Generalized Extreme Value', x, 5, 2, 2), gevpdf (x, 5, 2, 2)) %!assert_equal (pdf ('gev', x, 5, 2, 2), gevpdf (x, 5, 2, 2)) %!assert_equal (pdf ('Generalized Pareto', x, 5, 2, 2), gppdf (x, 5, 2, 2)) %!assert_equal (pdf ('gp', x, 5, 2, 2), gppdf (x, 5, 2, 2)) %!assert_equal (pdf ('Gumbel', x, 5, 2), gumbelpdf (x, 5, 2)) %!assert_equal (pdf ('gumbel', x, 5, 2), gumbelpdf (x, 5, 2)) %!assert_equal (pdf ('Half-normal', x, 5, 2), hnpdf (x, 5, 2)) %!assert_equal (pdf ('hn', x, 5, 2), hnpdf (x, 5, 2)) %!assert_equal (pdf ('Hypergeometric', x, 5, 2, 2), hygepdf (x, 5, 2, 2)) %!assert_equal (pdf ('hyge', x, 5, 2, 2), hygepdf (x, 5, 2, 2)) %!assert_equal (pdf ('Inverse Gaussian', x, 5, 2), invgpdf (x, 5, 2)) %!assert_equal (pdf ('invg', x, 5, 2), invgpdf (x, 5, 2)) %!assert_equal (pdf ('Laplace', x, 5, 2), laplacepdf (x, 5, 2)) %!assert_equal (pdf ('laplace', x, 5, 2), laplacepdf (x, 5, 2)) %!assert_equal (pdf ('Logistic', x, 5, 2), logipdf (x, 5, 2)) %!assert_equal (pdf ('logi', x, 5, 2), logipdf (x, 5, 2)) %!assert_equal (pdf ('Log-Logistic', x, 5, 2), loglpdf (x, 5, 2)) %!assert_equal (pdf ('logl', x, 5, 2), loglpdf (x, 5, 2)) %!assert_equal (pdf ('Lognormal', x, 5, 2), lognpdf (x, 5, 2)) %!assert_equal (pdf ('logn', x, 5, 2), lognpdf (x, 5, 2)) %!assert_equal (pdf ('Nakagami', x, 5, 2), nakapdf (x, 5, 2)) %!assert_equal (pdf ('naka', x, 5, 2), nakapdf (x, 5, 2)) %!assert_equal (pdf ('Negative Binomial', x, 5, 2), nbinpdf (x, 5, 2)) %!assert_equal (pdf ('nbin', x, 5, 2), nbinpdf (x, 5, 2)) %!assert_equal (pdf ('Noncentral F-Distribution', x, 5, 2, 2), ncfpdf (x, 5, 2, 2)) %!assert_equal (pdf ('ncf', x, 5, 2, 2), ncfpdf (x, 5, 2, 2)) %!assert_equal (pdf ('Noncentral Student T', x, 5, 2), nctpdf (x, 5, 2)) %!assert_equal (pdf ('nct', x, 5, 2), nctpdf (x, 5, 2)) %!assert_equal (pdf ('Noncentral Chi-Squared', x, 5, 2), ncx2pdf (x, 5, 2)) %!assert_equal (pdf ('ncx2', x, 5, 2), ncx2pdf (x, 5, 2)) %!assert_equal (pdf ('Normal', x, 5, 2), normpdf (x, 5, 2)) %!assert_equal (pdf ('norm', x, 5, 2), normpdf (x, 5, 2)) %!assert_equal (pdf ('Poisson', x, 5), poisspdf (x, 5)) %!assert_equal (pdf ('poiss', x, 5), poisspdf (x, 5)) %!assert_equal (pdf ('Rayleigh', x, 5), raylpdf (x, 5)) %!assert_equal (pdf ('rayl', x, 5), raylpdf (x, 5)) %!assert_equal (pdf ('Rician', x, 5, 1), ricepdf (x, 5, 1)) %!assert_equal (pdf ('rice', x, 5, 1), ricepdf (x, 5, 1)) %!assert_equal (pdf ('Student T', x, 5), tpdf (x, 5)) %!assert_equal (pdf ('t', x, 5), tpdf (x, 5)) %!assert_equal (pdf ('location-scale T', x, 5, 1, 2), tlspdf (x, 5, 1, 2)) %!assert_equal (pdf ('tls', x, 5, 1, 2), tlspdf (x, 5, 1, 2)) %!assert_equal (pdf ('Triangular', x, 5, 2, 2), tripdf (x, 5, 2, 2)) %!assert_equal (pdf ('tri', x, 5, 2, 2), tripdf (x, 5, 2, 2)) %!assert_equal (pdf ('Discrete Uniform', x, 5), unidpdf (x, 5)) %!assert_equal (pdf ('unid', x, 5), unidpdf (x, 5)) %!assert_equal (pdf ('Uniform', x, 5, 2), unifpdf (x, 5, 2)) %!assert_equal (pdf ('unif', x, 5, 2), unifpdf (x, 5, 2)) %!assert_equal (pdf ('Von Mises', x, 5, 2), vmpdf (x, 5, 2)) %!assert_equal (pdf ('vm', x, 5, 2), vmpdf (x, 5, 2)) %!assert_equal (pdf ('Weibull', x, 5, 2), wblpdf (x, 5, 2)) %!assert_equal (pdf ('wbl', x, 5, 2), wblpdf (x, 5, 2)) ## Test input validation %!test %! ## Every spelling of a name reaches the same distribution: case, spaces, %! ## hyphens and underscores are all ignored. %! for n = {'Extreme Value', 'ExtremeValue', 'extreme-value', 'EXTREME VALUE'} %! assert_equal (pdf (n{1}, 1, 2, 3), pdf ('ev', 1, 2, 3)); %! endfor %!test %! ## The name that makedist uses is accepted here too, and the reverse %! assert_equal (pdf ('tLocationScale', 1, 2, 3, 4), pdf ('tls', 1, 2, 3, 4)); %!error pdf (1) %!error pdf ({'beta'}) %!error pdf ('beta', {[1 2 3 4 5]}) %!error pdf ('beta', 'text') %!error pdf ('beta', 1+i) %!error ... %! pdf ('Beta', x, 'a', 2) %!error ... %! pdf ('Beta', x, 5, '') %!error ... %! pdf ('Beta', x, 5, {2}) %!error pdf ('chi2', x) %!error pdf ('Beta', x, 5) %!error pdf ('Burr', x, 5) %!error pdf ('Burr', x, 5, 2) statistics-release-2.1.2/inst/Distribution_Wrappers/private/000077500000000000000000000000001527311104300243125ustar00rootroot00000000000000statistics-release-2.1.2/inst/Distribution_Wrappers/private/__distname_key__.m000066400000000000000000000045351527311104300277470ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{key} =} __distname_key__ (@var{name}) ## ## Fold a distribution name to the key its wrapper looks up. ## ## The key is @var{name} in lower case with spaces and hyphens removed, so ## that every spelling of a distribution reaches the same distribution in ## every wrapper. @qcode{'Extreme Value'}, @qcode{'ExtremeValue'} and ## @qcode{'extreme-value'} all give @qcode{'extremevalue'}. Both separators ## occur in names the package already ships -- @qcode{'Birnbaum-Saunders'}, ## @qcode{'location-scale T'} -- which is why they are folded. ## ## The accepted set is deliberately a superset of MATLAB's, in two ways. ## ## MATLAB takes the spaced and the squashed spelling of a name but refuses ## the hyphenated one, so @qcode{'Birnbaum-Saunders'} and ## @qcode{'Log-Logistic'} are errors there. Octave has accepted those since ## they are the display names its own tables carry, and withdrawing them to ## match MATLAB would break working code for no gain. This is an extension, ## not a correction: MATLAB is not wrong to have a narrower set. ## ## MATLAB is, however, inconsistent with itself over ## @qcode{'tLocationScale'}: @code{makedist} accepts that name while ## @code{cdf} refuses it for the same distribution, which @code{cdf} calls ## @qcode{'location-scale T'}. Measured against R2024a on 2026-08-07. Every ## Octave wrapper accepts both names, so a name that builds a distribution ## also evaluates it. ## ## @end deftypefn function key = __distname_key__ (name) key = tolower (name); key(key == " " | key == "-") = []; endfunction statistics-release-2.1.2/inst/Distribution_Wrappers/random.m000066400000000000000000000434451527311104300243100ustar00rootroot00000000000000## Copyright (C) 2007 Soren Hauberg ## Copyright (C) 2023-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} random (@var{name}, @var{A}) ## @deftypefnx {statistics} {@var{r} =} random (@var{name}, @var{A}, @var{B}) ## @deftypefnx {statistics} {@var{r} =} random (@var{name}, @var{A}, @var{B}, @var{C}) ## @deftypefnx {statistics} {@var{r} =} random (@var{name}, @dots{}, @var{rows}, @var{cols}) ## @deftypefnx {statistics} {@var{r} =} random (@var{name}, @dots{}, @var{rows}, @var{cols}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} random (@var{name}, @dots{}, [@var{sz}]) ## ## Random arrays from a given one-, two-, or three-parameter distribution. ## ## The variable @var{name} must be a string with the name of the distribution to ## sample from. If this distribution is a one-parameter distribution, @var{A} ## must be supplied, if it is a two-parameter distribution, @var{B} must also be ## supplied, and if it is a three-parameter distribution, @var{C} must also be ## supplied. Any arguments following the distribution parameters will determine ## the size of the result. ## ## When called with a single size argument, return a square matrix with the ## dimension specified. When called with more than one scalar argument the ## first two arguments are taken as the number of rows and columns and any ## further arguments specify additional matrix dimensions. The size may also ## be specified with a vector of dimensions @var{sz}. ## ## @var{name} must be a char string of the name or the abbreviation of the ## desired probability distribution function as listed in the following table. ## The last column shows the required number of parameters that must be passed ## to the desired @qcode{*rnd} distribution function. ## ## @multitable @columnfractions 0.4 0.2 0.3 ## @headitem Distribution Name @tab Abbreviation @tab Input Parameters ## @item @qcode{'Beta'} @tab @qcode{'beta'} @tab 2 ## @item @qcode{'Binomial'} @tab @qcode{'bino'} @tab 2 ## @item @qcode{'Birnbaum-Saunders'} @tab @qcode{'bisa'} @tab 2 ## @item @qcode{'Burr'} @tab @qcode{'burr'} @tab 3 ## @item @qcode{'Cauchy'} @tab @qcode{'cauchy'} @tab 2 ## @item @qcode{'Chi-squared'} @tab @qcode{'chi2'} @tab 1 ## @item @qcode{'Extreme Value'} @tab @qcode{'ev'} @tab 2 ## @item @qcode{'Exponential'} @tab @qcode{'exp'} @tab 1 ## @item @qcode{'F-Distribution'} @tab @qcode{'f'} @tab 2 ## @item @qcode{'Gamma'} @tab @qcode{'gam'} @tab 2 ## @item @qcode{'Geometric'} @tab @qcode{'geo'} @tab 1 ## @item @qcode{'Generalized Extreme Value'} @tab @qcode{'gev'} @tab 3 ## @item @qcode{'Generalized Pareto'} @tab @qcode{'gp'} @tab 3 ## @item @qcode{'Gumbel'} @tab @qcode{'gumbel'} @tab 2 ## @item @qcode{'Half-normal'} @tab @qcode{'hn'} @tab 2 ## @item @qcode{'Hypergeometric'} @tab @qcode{'hyge'} @tab 3 ## @item @qcode{'Inverse Gaussian'} @tab @qcode{'invg'} @tab 2 ## @item @qcode{'Laplace'} @tab @qcode{'laplace'} @tab 2 ## @item @qcode{'Logistic'} @tab @qcode{'logi'} @tab 2 ## @item @qcode{'Log-Logistic'} @tab @qcode{'logl'} @tab 2 ## @item @qcode{'Lognormal'} @tab @qcode{'logn'} @tab 2 ## @item @qcode{'Nakagami'} @tab @qcode{'naka'} @tab 2 ## @item @qcode{'Negative Binomial'} @tab @qcode{'nbin'} @tab 2 ## @item @qcode{'Noncentral F-Distribution'} @tab @qcode{'ncf'} @tab 3 ## @item @qcode{'Noncentral Student T'} @tab @qcode{'nct'} @tab 2 ## @item @qcode{'Noncentral Chi-Squared'} @tab @qcode{'ncx2'} @tab 2 ## @item @qcode{'Normal'} @tab @qcode{'norm'} @tab 2 ## @item @qcode{'Poisson'} @tab @qcode{'poiss'} @tab 1 ## @item @qcode{'Rayleigh'} @tab @qcode{'rayl'} @tab 1 ## @item @qcode{'Rician'} @tab @qcode{'rice'} @tab 2 ## @item @qcode{'Student T'} @tab @qcode{'t'} @tab 1 ## @item @qcode{'location-scale T'} @tab @qcode{'tls'} @tab 3 ## @item @qcode{'Triangular'} @tab @qcode{'tri'} @tab 3 ## @item @qcode{'Discrete Uniform'} @tab @qcode{'unid'} @tab 1 ## @item @qcode{'Uniform'} @tab @qcode{'unif'} @tab 2 ## @item @qcode{'Von Mises'} @tab @qcode{'vm'} @tab 2 ## @item @qcode{'Weibull'} @tab @qcode{'wbl'} @tab 2 ## @end multitable ## ## Distribution names are matched ignoring case, spaces and hyphens, so that ## @qcode{'Extreme Value'}, @qcode{'ExtremeValue'} and @qcode{'extreme-value'} ## all select the same distribution, and the same set of names is accepted by ## @code{cdf}, @code{pdf}, @code{icdf}, @code{random}, @code{makedist}, ## @code{fitdist} and @code{mle}. ## ## This accepts more names than MATLAB. MATLAB takes the spaced and the ## squashed spelling but refuses the hyphenated one, so ## @qcode{'Birnbaum-Saunders'} and @qcode{'Log-Logistic'} are errors there; ## Octave has always accepted them and continues to. MATLAB also accepts ## @qcode{'tLocationScale'} in @code{makedist} while refusing it in ## @code{cdf} for the same distribution; Octave accepts it, and ## @qcode{'location-scale T'}, everywhere. Code written against MATLAB's ## names therefore runs unchanged, but code relying on these names will not ## port back. ## ## @seealso{cdf, icdf, pdf, betarnd, binornd, bisarnd, burrrnd, cauchyrnd, ## chi2rnd, evrnd, exprnd, frnd, gamrnd, geornd, gevrnd, gprnd, gumbelrnd, ## hnrnd, hygernd, invgrnd, laplacernd, logirnd, loglrnd, lognrnd, nakarnd, ## nbinrnd, ncfrnd, nctrnd, ncx2rnd, normrnd, poissrnd, raylrnd, ricernd, trnd, ## tlsrnd, trirnd, unidrnd, unifrnd, vmrnd, wblrnd} ## @end deftypefn function r = random (name, varargin) ## implemented functions persistent allDF = { ... {'beta' , 'Beta'}, @betarnd, 2, ... {'bino' , 'Binomial'}, @binornd, 2, ... {'bisa' , 'Birnbaum-Saunders'}, @bisarnd, 2, ... {'burr' , 'Burr'}, @burrrnd, 3, ... {'cauchy' , 'Cauchy'}, @cauchyrnd, 2, ... {'chi2' , 'Chi-squared'}, @chi2rnd, 1, ... {'ev' , 'Extreme Value'}, @evrnd, 2, ... {'exp' , 'Exponential'}, @exprnd, 1, ... {'f' , 'F-Distribution'}, @frnd, 2, ... {'gam' , 'Gamma'}, @gamrnd, 2, ... {'geo' , 'Geometric'}, @geornd, 1, ... {'gev' , 'Generalized Extreme Value'}, @gevrnd, 3, ... {'gp' , 'Generalized Pareto'}, @gprnd, 3, ... {'gumbel' , 'Gumbel'}, @gumbelrnd, 2, ... {'hn' , 'Half-normal'}, @hnrnd, 2, ... {'hyge' , 'Hypergeometric'}, @hygernd, 3, ... {'invg' , 'Inverse Gaussian'}, @invgrnd, 2, ... {'laplace' , 'Laplace'}, @laplacernd, 2, ... {'logi' , 'Logistic'}, @logirnd, 2, ... {'logl' , 'Log-Logistic'}, @loglrnd, 2, ... {'logn' , 'Lognormal'}, @lognrnd, 2, ... {'naka' , 'Nakagami'}, @nakarnd, 2, ... {'nbin' , 'Negative Binomial'}, @nbinrnd, 2, ... {'ncf' , 'Noncentral F-Distribution'}, @ncfrnd, 3, ... {'nct' , 'Noncentral Student T'}, @nctrnd, 2, ... {'ncx2' , 'Noncentral Chi-squared'}, @ncx2rnd, 2, ... {'norm' , 'Normal'}, @normrnd, 2, ... {'poiss' , 'Poisson'}, @poissrnd, 1, ... {'rayl' , 'Rayleigh'}, @raylrnd, 1, ... {'rice' , 'Rician'}, @ricernd, 2, ... {'t' , 'Student T'}, @trnd, 1, ... {'tls', 'location-scale T', 'tLocationScale'}, @tlsrnd, 3, ... {'tri' , 'Triangular'}, @trirnd, 3, ... {'unid' , 'Discrete Uniform'}, @unidrnd, 1, ... {'unif' , 'Uniform'}, @unifrnd, 2, ... {'vm' , 'Von Mises'}, @vmrnd, 2, ... {'wbl' , 'Weibull'}, @wblrnd, 2}; if (! ischar (name)) error ("random: distribution NAME must be a char string."); endif ## Get number of arguments nargs = numel (varargin); ## Get available functions rndnames = allDF(1:3:end); rndhandl = allDF(2:3:end); rnd_args = allDF(3:3:end); ## Search for RND function ## Match on the folded key so that every spelling of a name resolves key = __distname_key__ (name); idx = cellfun (@(x) any (strcmp (key, cellfun (@__distname_key__, x, ... 'UniformOutput', false))), rndnames); if (any (idx)) if (nargs == rnd_args{idx}) ## Check that all distribution parameters are numeric if (! all (cellfun (@(x)isnumeric (x), (varargin)))) error ("random: distribution parameters must be numeric."); endif ## Call appropriate RND r = feval (rndhandl{idx}, varargin{:}); elseif (nargs > rnd_args{idx}) ## Check that all distribution parameters are numeric if (! all (cellfun (@(x)isnumeric (x), (varargin(1:rnd_args{idx}))))) error ("random: distribution parameters must be numeric."); endif ## Call appropriate RND. SIZE arguments are checked by the RND function. r = feval (rndhandl{idx}, varargin{:}); else if (rnd_args{idx} == 1) error ("random: %s distribution requires 1 parameter.", name); else error ("random: %s distribution requires %d parameters.", ... name, rnd_args{idx}); endif endif else error ("random: %s distribution is not implemented in Statistics.", name); endif endfunction ## Test results %!assert_equal (size (random ('Beta', 5, 2, 2, 10)), size (betarnd (5, 2, 2, 10))) %!assert_equal (size (random ('beta', 5, 2, 2, 10)), size (betarnd (5, 2, 2, 10))) %!assert_equal (size (random ('Binomial', 5, 2, [10, 20])), size (binornd (5, 2, 10, 20))) %!assert_equal (size (random ('bino', 5, 2, [10, 20])), size (binornd (5, 2, 10, 20))) %!assert_equal (size (random ('Birnbaum-Saunders', 5, 2, [10, 20])), size (bisarnd (5, 2, 10, 20))) %!assert_equal (size (random ('bisa', 5, 2, [10, 20])), size (bisarnd (5, 2, 10, 20))) %!assert_equal (size (random ('Burr', 5, 2, 2, [10, 20])), size (burrrnd (5, 2, 2, 10, 20))) %!assert_equal (size (random ('burr', 5, 2, 2, [10, 20])), size (burrrnd (5, 2, 2, 10, 20))) %!assert_equal (size (random ('Cauchy', 5, 2, [10, 20])), size (cauchyrnd (5, 2, 10, 20))) %!assert_equal (size (random ('cauchy', 5, 2, [10, 20])), size (cauchyrnd (5, 2, 10, 20))) %!assert_equal (size (random ('Chi-squared', 5, [10, 20])), size (chi2rnd (5, 10, 20))) %!assert_equal (size (random ('chi2', 5, [10, 20])), size (chi2rnd (5, 10, 20))) %!assert_equal (size (random ('Extreme Value', 5, 2, [10, 20])), size (evrnd (5, 2, 10, 20))) %!assert_equal (size (random ('ev', 5, 2, [10, 20])), size (evrnd (5, 2, 10, 20))) %!assert_equal (size (random ('Exponential', 5, [10, 20])), size (exprnd (5, 10, 20))) %!assert_equal (size (random ('exp', 5, [10, 20])), size (exprnd (5, 10, 20))) %!assert_equal (size (random ('F-Distribution', 5, 2, [10, 20])), size (frnd (5, 2, 10, 20))) %!assert_equal (size (random ('f', 5, 2, [10, 20])), size (frnd (5, 2, 10, 20))) %!assert_equal (size (random ('Gamma', 5, 2, [10, 20])), size (gamrnd (5, 2, 10, 20))) %!assert_equal (size (random ('gam', 5, 2, [10, 20])), size (gamrnd (5, 2, 10, 20))) %!assert_equal (size (random ('Geometric', 5, [10, 20])), size (geornd (5, 10, 20))) %!assert_equal (size (random ('geo', 5, [10, 20])), size (geornd (5, 10, 20))) %!assert_equal (size (random ('Generalized Extreme Value', 5, 2, 2, [10, 20])), size (gevrnd (5, 2, 2, 10, 20))) %!assert_equal (size (random ('gev', 5, 2, 2, [10, 20])), size (gevrnd (5, 2, 2, 10, 20))) %!assert_equal (size (random ('Generalized Pareto', 5, 2, 2, [10, 20])), size (gprnd (5, 2, 2, 10, 20))) %!assert_equal (size (random ('gp', 5, 2, 2, [10, 20])), size (gprnd (5, 2, 2, 10, 20))) %!assert_equal (size (random ('Gumbel', 5, 2, [10, 20])), size (gumbelrnd (5, 2, 10, 20))) %!assert_equal (size (random ('gumbel', 5, 2, [10, 20])), size (gumbelrnd (5, 2, 10, 20))) %!assert_equal (size (random ('Half-normal', 5, 2, [10, 20])), size (hnrnd (5, 2, 10, 20))) %!assert_equal (size (random ('hn', 5, 2, [10, 20])), size (hnrnd (5, 2, 10, 20))) %!assert_equal (size (random ('Hypergeometric', 5, 2, 2, [10, 20])), size (hygernd (5, 2, 2, 10, 20))) %!assert_equal (size (random ('hyge', 5, 2, 2, [10, 20])), size (hygernd (5, 2, 2, 10, 20))) %!assert_equal (size (random ('Inverse Gaussian', 5, 2, [10, 20])), size (invgrnd (5, 2, 10, 20))) %!assert_equal (size (random ('invg', 5, 2, [10, 20])), size (invgrnd (5, 2, 10, 20))) %!assert_equal (size (random ('Laplace', 5, 2, [10, 20])), size (laplacernd (5, 2, 10, 20))) %!assert_equal (size (random ('laplace', 5, 2, [10, 20])), size (laplacernd (5, 2, 10, 20))) %!assert_equal (size (random ('Logistic', 5, 2, [10, 20])), size (logirnd (5, 2, 10, 20))) %!assert_equal (size (random ('logi', 5, 2, [10, 20])), size (logirnd (5, 2, 10, 20))) %!assert_equal (size (random ('Log-Logistic', 5, 2, [10, 20])), size (loglrnd (5, 2, 10, 20))) %!assert_equal (size (random ('logl', 5, 2, [10, 20])), size (loglrnd (5, 2, 10, 20))) %!assert_equal (size (random ('Lognormal', 5, 2, [10, 20])), size (lognrnd (5, 2, 10, 20))) %!assert_equal (size (random ('logn', 5, 2, [10, 20])), size (lognrnd (5, 2, 10, 20))) %!assert_equal (size (random ('Nakagami', 5, 2, [10, 20])), size (nakarnd (5, 2, 10, 20))) %!assert_equal (size (random ('naka', 5, 2, [10, 20])), size (nakarnd (5, 2, 10, 20))) %!assert_equal (size (random ('Negative Binomial', 5, 2, [10, 20])), size (nbinrnd (5, 2, 10, 20))) %!assert_equal (size (random ('nbin', 5, 2, [10, 20])), size (nbinrnd (5, 2, 10, 20))) %!assert_equal (size (random ('Noncentral F-Distribution', 5, 2, 2, [10, 20])), size (ncfrnd (5, 2, 2, 10, 20))) %!assert_equal (size (random ('ncf', 5, 2, 2, [10, 20])), size (ncfrnd (5, 2, 2, 10, 20))) %!assert_equal (size (random ('Noncentral Student T', 5, 2, [10, 20])), size (nctrnd (5, 2, 10, 20))) %!assert_equal (size (random ('nct', 5, 2, [10, 20])), size (nctrnd (5, 2, 10, 20))) %!assert_equal (size (random ('Noncentral Chi-Squared', 5, 2, [10, 20])), size (ncx2rnd (5, 2, 10, 20))) %!assert_equal (size (random ('ncx2', 5, 2, [10, 20])), size (ncx2rnd (5, 2, 10, 20))) %!assert_equal (size (random ('Normal', 5, 2, [10, 20])), size (normrnd (5, 2, 10, 20))) %!assert_equal (size (random ('norm', 5, 2, [10, 20])), size (normrnd (5, 2, 10, 20))) %!assert_equal (size (random ('Poisson', 5, [10, 20])), size (poissrnd (5, 10, 20))) %!assert_equal (size (random ('poiss', 5, [10, 20])), size (poissrnd (5, 10, 20))) %!assert_equal (size (random ('Rayleigh', 5, [10, 20])), size (raylrnd (5, 10, 20))) %!assert_equal (size (random ('rayl', 5, [10, 20])), size (raylrnd (5, 10, 20))) %!assert_equal (size (random ('Rician', 5, 1, [10, 20])), size (ricernd (5, 1, 10, 20))) %!assert_equal (size (random ('rice', 5, 1, [10, 20])), size (ricernd (5, 1, 10, 20))) %!assert_equal (size (random ('Student T', 5, [10, 20])), size (trnd (5, 10, 20))) %!assert_equal (size (random ('t', 5, [10, 20])), size (trnd (5, 10, 20))) %!assert_equal (size (random ('location-scale T', 5, 1, 2, [10, 20])), size (tlsrnd (5, 1, 2, 10, 20))) %!assert_equal (size (random ('tls', 5, 1, 2, [10, 20])), size (tlsrnd (5, 1, 2, 10, 20))) %!assert_equal (size (random ('Triangular', 5, 2, 2, [10, 20])), size (trirnd (5, 2, 2, 10, 20))) %!assert_equal (size (random ('tri', 5, 2, 2, [10, 20])), size (trirnd (5, 2, 2, 10, 20))) %!assert_equal (size (random ('Discrete Uniform', 5, [10, 20])), size (unidrnd (5, 10, 20))) %!assert_equal (size (random ('unid', 5, [10, 20])), size (unidrnd (5, 10, 20))) %!assert_equal (size (random ('Uniform', 5, 2, [10, 20])), size (unifrnd (5, 2, 10, 20))) %!assert_equal (size (random ('unif', 5, 2, [10, 20])), size (unifrnd (5, 2, 10, 20))) %!assert_equal (size (random ('Von Mises', 5, 2, [10, 20])), size (vmrnd (5, 2, 10, 20))) %!assert_equal (size (random ('vm', 5, 2, [10, 20])), size (vmrnd (5, 2, 10, 20))) %!assert_equal (size (random ('Weibull', 5, 2, [10, 20])), size (wblrnd (5, 2, 10, 20))) %!assert_equal (size (random ('wbl', 5, 2, [10, 20])), size (wblrnd (5, 2, 10, 20))) ## Test input validation %!error random (1) %!error random ({'beta'}) %!error ... %! random ('Beta', 'a', 2) %!error ... %! random ('Beta', 5, '') %!error ... %! random ('Beta', 5, {2}) %!error ... %! random ('Beta', 'a', 2, 2, 10) %!error ... %! random ('Beta', 5, '', 2, 10) %!error ... %! random ('Beta', 5, {2}, 2, 10) %!error ... %! random ('Beta', 5, '', 2, 10) %!error random ('chi2') %!error random ('Beta', 5) %!error random ('Burr', 5) %!error random ('Burr', 5, 2) statistics-release-2.1.2/inst/Experimental_Design/000077500000000000000000000000001527311104300222245ustar00rootroot00000000000000statistics-release-2.1.2/inst/Experimental_Design/doc-cache000066400000000000000000000432471527311104300237670ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 6 # name: # type: sq_string # elements: 1 # length: 4 ff2n # name: # type: sq_string # elements: 1 # length: 586 statistics: dFF2 = ff2n (n) Two-level full factorial design. dFF2 = ff2n (n) gives factor settings dFF2 for a two-level full factorial design with n factors. dFF2 is m-by-n, where m is the number of treatments in the full-factorial design. Each row of dFF2 corresponds to a single treatment. Each column contains the settings for a single factor, with values of 0 and 1 for the two levels. n must be a non-negative integer scalar. ff2n (0) returns a 1-by-0 matrix, a single treatment with no factors. An empty n is an error, whereas MATLAB returns an empty matrix. See also: fullfact # name: # type: sq_string # elements: 1 # length: 32 Two-level full factorial design. # name: # type: sq_string # elements: 1 # length: 8 fullfact # name: # type: sq_string # elements: 1 # length: 457 statistics: A = fullfact (levels) Full factorial design. A = fullfact (levels) returns a numeric matrix A with the treatments of a full factorial design specified by levels, which must be a numeric vector of real positive integer values with each value specifying the number of levels of each individual factor. Each row of A corresponds to a single treatment and each column to a single factor. For binary full factorial design, use ff2n. See also: ff2n # name: # type: sq_string # elements: 1 # length: 22 Full factorial design. # name: # type: sq_string # elements: 1 # length: 21 parseWilkinsonFormula # name: # type: sq_string # elements: 1 # length: 6751 statistics: terms = parseWilkinsonFormula (formula) statistics: result = parseWilkinsonFormula (formula, mode) statistics: [X, y, names] = parseWilkinsonFormula (formula, "model_matrix", data) statistics: S = parseWilkinsonFormula (formula, "mixed") statistics: ... = parseWilkinsonFormula (..., setOrder) Parse and expand statistical model formulae using the Wilkinson notation. This function implements the recursive-descent parser and expansion logic described by Wilkinson & Rogers (1973) for factorial models. It allows the symbolic specification of analysis of variance and regression models, converting strings into computational schemas or design matrices. It also supports multi-variable response specification on the Left-Hand Side (LHS) using lists or ranges. parseWilkinsonFormula accepts as its first input argument a Wilkinson notation string specified by formula either as a character vector or a string scalar with the following list of valid symbols: Right-Hand Side (Model) Operators The RHS specifies the independent variables (predictors) and the structural relationships between them, such as interactions and nesting. The parser expands these expressions into fundamental model terms following the standard statistical rules of marginality. Additionally, explicit nesting notation (e.g., B(A)) is supported to denote that factor B is nested within A. Operator Description Expansion Example + Addition (Union) A + B expands to A, B * Crossing A * B expands to A, B, A:B - Deletion A*B - A:B expands to A, B / Nesting A / B expands to A, A:B : Interaction A : B expands to A:B ^ Power (Limit) (A+B)^2 expands to A, B, A:B 1 Intercept y ~ A - 1 removes intercept Left-Hand Side (Response) Operators The LHS, separated by the ~ operator, defines the dependent variables. It natively supports multi-response syntaxes. Operator Description Usage Example ~ Formula separator y ~ x , List separator y1, y2 ~ x - Range operator T1 - T3 ~ x Processing Modes parseWilkinsonFormula (formula, mode) evaluates the formula string based on the selected mode: 'expand' (default) - Returns a structure containing response and model fields. Each field contains cell arrays of the expanded, fundamental terms. 'equation' - Generates a string representing the mathematical equation of the fitted model. Coefficients are represented generically as c1, c2, .... If multiple responses are specified, it returns a string array of equations. Formula String Equation Output y ~ x "y = c1 + c2*x" y ~ A * B "y = c1 + c2*A + c3*B + c4*A*B" y ~ School / Class "y = c1 + c2*School + c3*Class*School" y ~ x^2 "y = c1 + c2*x + c3*x^2" y1 - y2 ~ Trt ["y1 = c1 + c2*Trt", "y2 = ..."] 'matrix' - Returns a schema structure containing a binary matrix defining term membership, useful for internal algorithmic processing. 'model_matrix' - Constructs the numeric Design Matrix (X) and Response Matrix (y) directly from a provided data table. 'parse' - Returns the raw Abstract Syntax Tree (AST) structure. 'tokenize' - Returns the array of tokens generated by the lexer. 'mixed' - Decomposes a mixed-effects formula containing random-effects terms of the form (expr | group) into its fixed and random parts. See Mixed-Effects Formulae below. Order of the reported variables setOrder is 'sorted' or 'stable' and comes last, after every other argument, so that it may be given whatever else the mode takes. 'sorted' is the default and reports the variables in alphabetical order; 'stable' reports them in the order the formula wrote them, as unique uses the same two words. It applies wherever a mode reports variable names, which is 'matrix' and 'model_matrix', and the order carries into ResponseIdx and into the columns of Terms and of X beside it. Data Handling ('model_matrix' mode) When using the 'model_matrix' mode, a data argument must be provided as an Octave table. Categorical Variables: Cell arrays of strings in the table are automatically detected as categorical factors and undergo corner-point (reference) dummy coding. Numeric Variables: Standard numeric vectors are treated as continuous predictors or responses. Missing Data: Rows containing NaN values in any of the active variables are automatically omitted from the final matrices. Mixed-Effects Formulae ('mixed' mode) A random-effects term is written (expr | group), where expr is a Wilkinson design expression for the random intercept and slopes and group is the grouping factor (or an interaction of factors such as g1:g2). As with fixed effects an intercept is implicit; suppress it with 0 or -1 (for example (x - 1 | g) or (-1 + x | g) for a random slope with no random intercept). The 'mixed' mode returns a structure with the following fields: Response The response (LHS) as a character vector. FixedTerms A cell array of the expanded fixed-effects terms, excluding the intercept. FixedIntercept A logical flag, true when the fixed model includes an intercept. Random A struct array with one element per random-effects term, each with fields Expr (the raw expression), Terms (its expanded predictor terms), Intercept (logical), Group (the raw grouping spec), and GroupVars (its grouping variables as a cell array). HasRandom A logical flag, true when the formula contains any random-effects term. A formula with no random-effects term is still valid in 'mixed' mode: it returns HasRandom false and an empty Random array. Outputs setOrder Whether the variables are reported alphabetically, 'sorted', or in the order the formula wrote them, 'stable'. The default is 'sorted'. terms / result The processed model structure, string array, or cell array depending on the selected mode. X The generated numeric design matrix (Observations x Parameters). Includes a column of ones for the intercept unless - 1 is in the formula. y The numeric response matrix (Observations x K responses). names A cell array of character vectors containing the column names corresponding to the generated design matrix X. References Wilkinson, G. N. and Rogers, C. E. (1973). Symbolic Description of Factorial Models for Analysis of Variance. Applied Statistics, 22, 392-399. In "model_matrix" mode a categorical variable expands to indicator columns, one per level bar the reference level, which the intercept carries. The levels of a character or string column are taken in the order the data presents them, so the reference level is the one seen first; a categorical column uses its own category order. When the formula has no intercept, the first categorical variable is given an indicator for every one of its levels and any further categorical variable stays reference coded. MATLAB omits the reference level whether or not an intercept is present, and so cannot fit the reference group. # name: # type: sq_string # elements: 1 # length: 73 Parse and expand statistical model formulae using the Wilkinson notation. # name: # type: sq_string # elements: 1 # length: 11 sampsizepwr # name: # type: sq_string # elements: 1 # length: 5175 statistics: n = sampsizepwr (testtype, params, p1) statistics: n = sampsizepwr (testtype, params, p1, power) statistics: power = sampsizepwr (testtype, params, p1, [], n) statistics: p1 = sampsizepwr (testtype, params, [], power, n) statistics: [n1, n2] = sampsizepwr ('t2', params, p1, power) statistics: [...] = sampsizepwr (testtype, params, p1, power, n, name, value) Sample size and power calculation for hypothesis test. sampsizepwr computes the sample size, power, or alternative parameter value for a hypothesis test, given the other two values. For example, you can compute the sample size required to obtain a particular power for a hypothesis test, given the parameter value of the alternative hypothesis. n = sampsizepwr (testtype, params, p1) returns the sample size N required for a two-sided test of the specified type to have a power (probability of rejecting the null hypothesis when the alternative is true) of 0.90 when the significance level (probability of rejecting the null hypothesis when the null hypothesis is true) is 0.05. params specifies the parameter values under the null hypothesis. P1 specifies the value of the single parameter being tested under the alternative hypothesis. For the two-sample t-test, N is the value of the equal sample size for both samples, params specifies the parameter values of the first sample under the null and alternative hypotheses, and P1 specifies the value of the single parameter from the other sample under the alternative hypothesis. The following TESTTYPE values are available: "z" one-sample z-test for normally distributed data with known standard deviation. params is a two-element vector [MU0 SIGMA0] of the mean and standard deviation, respectively, under the null hypothesis. P1 is the value of the mean under the alternative hypothesis. "t" one-sample t-test or paired t-test for normally distributed data with unknown standard deviation. params is a two-element vector [MU0 SIGMA0] of the mean and standard deviation, respectively, under the null hypothesis. P1 is the value of the mean under the alternative hypothesis. "t2" two-sample pooled t-test (test for equal means) for normally distributed data with equal unknown standard deviations. params is a two-element vector [MU0 SIGMA0] of the mean and standard deviation of the first sample under the null and alternative hypotheses. P1 is the the mean of the second sample under the alternative hypothesis. "var" chi-square test of variance for normally distributed data. params is the variance under the null hypothesis. P1 is the variance under the alternative hypothesis. "p" test of the P parameter (success probability) for a binomial distribution. params is the value of P under the null hypothesis. P1 is the value of P under the alternative hypothesis. "r" test of the correlation coefficient parameter for significance. params is the value of r under the null hypothesis. P1 is the value of r under the alternative hypothesis. The "p" test for the binomial distribution is a discrete test for which increasing the sample size does not always increase the power. For N values larger than 200, there may be values smaller than the returned N value that also produce the desired power. n = sampsizepwr (testtype, params, p1, power) returns the sample size N such that the power is power for the parameter value P1. For the two-sample t-test, N is the equal sample size of both samples. [n1, n2] = sampsizepwr ("t2", params, p1, power) returns the sample sizes n1 and n2 for the two samples. These values are the same unless the "ratio" parameter, ratio = n2 / n2, is set to a value other than the default (See the name/value pair definition of ratio below). power = sampsizepwr (testtype, params, p1, [], n) returns the power achieved for a sample size of n when the true parameter value is p1. For the two-sample t-test, n is the smaller one of the two sample sizes. p1 = sampsizepwr (testtype, params, [], power, n) returns the parameter value detectable with the specified sample size n and power power. For the two-sample t-test, n is the smaller one of the two sample sizes. When computing p1 for the "p" test, if no alternative can be rejected for a given params, n and power value, the function displays a warning message and returns NaN. [...] = sampsizepwr (..., n, name, value) specifies one or more of the following name / value pairs: Name Value "alpha" significance level of the test (default is 0.05) "tail" the type of test which can be: "both" two-sided test for an alternative p1 not equal to params "right" one-sided test for an alternative p1 larger than params "left" one-sided test for an alternative p1 smaller than params "ratio" desired ratio n2 / n2 of the larger sample size n2 to the smaller sample size n1. Used only for the two-sample t-test. The value of ratio is greater than or equal to 1 (default is 1). sampsizepwr computes the sample size, power, or alternative hypothesis value given values for the other two. Specify one of these as [] to compute it. The remaining parameters (and ALPHA, RATIO) can be scalars or arrays of the same size. See also: vartest, ttest, ttest2, ztest, binocdf # name: # type: sq_string # elements: 1 # length: 54 Sample size and power calculation for hypothesis test. # name: # type: sq_string # elements: 1 # length: 9 sigma_pts # name: # type: sq_string # elements: 1 # length: 1007 statistics: pts = sigma_pts (n) statistics: pts = sigma_pts (n, m) statistics: pts = sigma_pts (n, m, K) statistics: pts = sigma_pts (n, m, K, l) Calculates 2*n+1 sigma points in n dimensions. Sigma points are used in the unscented transform to estimate the result of applying a given nonlinear transformation to a probability distribution that is characterized only in terms of a finite set of statistics. If only the dimension n is given the resulting points have zero mean and identity covariance matrix. If the mean m or the covariance matrix K are given, then the resulting points will have those statistics. The factor l scales the points away from the mean. It is useful to tune the accuracy of the unscented transform. There is no unique way of computing sigma points, this function implements the algorithm described in section 2.6 "The New Filter" pages 40-41 of Uhlmann, Jeffrey (1995). "Dynamic Map Building and Localization: New Theoretical Foundations". Ph.D. thesis. University of Oxford. # name: # type: sq_string # elements: 1 # length: 46 Calculates 2*n+1 sigma points in n dimensions. # name: # type: sq_string # elements: 1 # length: 4 x2fx # name: # type: sq_string # elements: 1 # length: 2412 statistics: [d, model, termstart, termend] = x2fx (x) statistics: [d, model, termstart, termend] = x2fx (x, model) statistics: [d, model, termstart, termend] = x2fx (x, model, categ) statistics: [d, model, termstart, termend] = x2fx (x, model, categ, catlevels) Convert predictors to design matrix. d = x2fx (x, model) converts a matrix of predictors x to a design matrix d for regression analysis. Distinct predictor variables should appear in different columns of x. The optional input model controls the regression model. By default, x2fx returns the design matrix for a linear additive model with a constant term. model can be any one of the following strings: "linear" Constant and linear terms (the default) "interaction" Constant, linear, and interaction terms "quadratic" Constant, linear, interaction, and squared terms "purequadratic" Constant, linear, and squared terms If x has n columns, the order of the columns of d for a full quadratic model is: The constant term. The linear terms (the columns of X, in order 1,2,...,n). The interaction terms (pairwise products of columns of x, in order (1,2), (1,3), ..., (1,n), (2,3), ..., (n-1,n). The squared terms (in the order 1,2,...,n). Other models use a subset of these terms, in the same order. Alternatively, MODEL can be a matrix specifying polynomial terms of arbitrary order. In this case, MODEL should have one column for each column in X and one r for each term in the model. The entries in any r of MODEL are powers for the corresponding columns of x. For example, if x has columns X1, X2, and X3, then a row [0 1 2] in model would specify the term (X1.^0).*(X2.^1).*(X3.^2). A row of all zeros in model specifies a constant term, which you can omit. d = x2fx (x, model, categ) treats columns with numbers listed in the vector categ as categorical variables. Terms involving categorical variables produce dummy variable columns in d. Dummy variables are computed under the assumption that possible categorical levels are completely enumerated by the unique values that appear in the corresponding column of x. d = x2fx (x, model, categ, catlevels) accepts a vector catlevels the same length as categ, specifying the number of levels in each categorical variable. In this case, values in the corresponding column of x must be integers in the range from 1 to the specified number of levels. Not all of the levels need to appear in x. # name: # type: sq_string # elements: 1 # length: 36 Convert predictors to design matrix. statistics-release-2.1.2/inst/Experimental_Design/ff2n.m000066400000000000000000000055761527311104300232520ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## based on public domain work by Paul Kienzle ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{dFF2} =} ff2n (@var{n}) ## ## Two-level full factorial design. ## ## @code{@var{dFF2} = ff2n (@var{n})} gives factor settings dFF2 for a two-level ## full factorial design with n factors. @var{dFF2} is m-by-n, where m is the ## number of treatments in the full-factorial design. Each row of @var{dFF2} ## corresponds to a single treatment. Each column contains the settings for a ## single factor, with values of 0 and 1 for the two levels. ## ## @var{n} must be a non-negative integer scalar. @code{ff2n (0)} returns a ## 1-by-0 matrix, a single treatment with no factors. An empty @var{n} is an ## error, whereas MATLAB returns an empty matrix. ## ## @seealso{fullfact} ## @end deftypefn function A = ff2n (n) if (nargin != 1) error ("ff2n: wrong number of input arguments."); endif if (! (isscalar (n) && (isnumeric (n) || islogical (n)) && isreal (n) && isfinite (n) && n >= 0 && fix (n) == n)) error ("ff2n: N must be a non-negative integer scalar."); endif if (n == 0) A = zeros (1, 0); else A = flip (fullfact (2 * ones (1, n)), 2) - 1; endif endfunction %!error ff2n (); %!error ff2n (2, 5); %!error ff2n ([]) %!error ff2n ([1, 2]) %!error ff2n ('a') %!error ff2n (2.5) %!error ff2n (-3) %!error ff2n (3+2i) %!error ff2n (Inf) %!error ff2n (NaN) %!assert_equal (ff2n (0), zeros (1, 0)) %!assert_equal (ff2n (true), [0; 1]) %!assert_equal (ff2n (int8 (2)), [0, 0; 0, 1; 1, 0; 1, 1]) %!test %! A = ff2n (3); %! assert_equal (A, [0, 0, 0; 0, 0, 1; 0, 1, 0; 0, 1, 1; ... %! 1, 0, 0; 1, 0, 1; 1, 1, 0; 1, 1, 1]); %!test %! A = ff2n (2); %! assert_equal (A, [0, 0; 0, 1; 1, 0; 1, 1]); statistics-release-2.1.2/inst/Experimental_Design/fullfact.m000066400000000000000000000105121527311104300242010ustar00rootroot00000000000000## Copyright (C) 2022-2025 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{A} =} fullfact (@var{levels}) ## ## Full factorial design. ## ## @code{@var{A} = fullfact (@var{levels})} returns a numeric matrix @var{A} ## with the treatments of a full factorial design specified by @var{levels}, ## which must be a numeric vector of real positive integer values with each ## value specifying the number of levels of each individual factor. ## ## Each row of @var{A} corresponds to a single treatment and each column to a ## single factor. For binary full factorial design, use @code{ff2n}. ## ## @seealso{ff2n} ## @end deftypefn function A = fullfact (levels) if (nargin != 1) error ("fullfact: one input argument is required."); endif if (! (isvector (levels) && isnumeric (levels) && isfinite (levels))) error ("fullfact: input argument must be a finite real numeric vector."); endif if (any (fix (levels) != levels) || any (levels < 1) || ! all (isreal (levels))) error ("fullfact: factor levels must be real positive integers."); endif rows = prod (levels); cols = numel (levels); A = zeros (rows, cols); n_seqs = rows; for i = 1:cols factor = [1:levels(i)]'; n_reps = rows / n_seqs; factor = repelem (factor, n_reps, 1); n_seqs = n_seqs / levels(i); factor = repmat (factor, n_seqs, 1); A(:,i) = factor; endfor endfunction %!demo %! ## Full factorial design with 3 ordinal variables %! fullfact ([2, 3, 4]) %!error fullfact (); %!error ... %! fullfact (Inf); %!error ... %! fullfact (NaN); %!error ... %! fullfact (ones (2)); %!error ... %! fullfact ([1, 2, NaN]); %!error ... %! fullfact ([1, 2, Inf]); %!error fullfact (2.5); %!error fullfact (0); %!error fullfact (-3); %!error fullfact (3+2i); %!error fullfact ([1, 2, -3]); %!error fullfact ([0, 1, 2]); %!test %! A = fullfact (1); %! assert_equal (A, 1); %!test %! A = fullfact (2); %! assert_equal (A, [1; 2]); %!test %!test %! A = fullfact (3); %! assert_equal (A, [1; 2; 3]); %!test %! A = fullfact ([1, 2, 4]); %! A_out = [1, 1, 1; 1, 2, 1; 1, 1, 2; 1, 2, 2; ... %! 1, 1, 3; 1, 2, 3; 1, 1, 4; 1, 2, 4]; %! assert_equal (A, A_out); %!test %! A = fullfact ([2, 2]); %! assert_equal (A, [1, 1; 2, 1; 1, 2; 2, 2]); %!test %! A = fullfact ([2, 2, 4]); %! A_out = [1, 1, 1; 2, 1, 1; 1, 2, 1; 2, 2, 1; ... %! 1, 1, 2; 2, 1, 2; 1, 2, 2; 2, 2, 2; ... %! 1, 1, 3; 2, 1, 3; 1, 2, 3; 2, 2, 3; ... %! 1, 1, 4; 2, 1, 4; 1, 2, 4; 2, 2, 4]; %! assert_equal (A, A_out); %!test %! A = fullfact ([3, 2, 4]); %! A_out = [1, 1, 1; 2, 1, 1; 3, 1, 1; 1, 2, 1; 2, 2, 1; 3, 2, 1; ... %! 1, 1, 2; 2, 1, 2; 3, 1, 2; 1, 2, 2; 2, 2, 2; 3, 2, 2; ... %! 1, 1, 3; 2, 1, 3; 3, 1, 3; 1, 2, 3; 2, 2, 3; 3, 2, 3; ... %! 1, 1, 4; 2, 1, 4; 3, 1, 4; 1, 2, 4; 2, 2, 4; 3, 2, 4]; %! assert_equal (A, A_out); %!test %! A = fullfact ([4, 2]); %! assert_equal (A, [1, 1; 2, 1; 3, 1; 4, 1; 1, 2; 2, 2; 3, 2; 4, 2]); statistics-release-2.1.2/inst/Experimental_Design/parseWilkinsonFormula.m000066400000000000000000002365061527311104300267540ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## Copyright (C) 2026 Avanish Salunke ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{terms} =} parseWilkinsonFormula (@var{formula}) ## @deftypefnx {statistics} {@var{result} =} parseWilkinsonFormula (@var{formula}, @var{mode}) ## @deftypefnx {statistics} {[@var{X}, @var{y}, @var{names}] =} parseWilkinsonFormula (@var{formula}, "model_matrix", @var{data}) ## @deftypefnx {statistics} {@var{S} =} parseWilkinsonFormula (@var{formula}, "mixed") ## @deftypefnx {statistics} {@dots{} =} parseWilkinsonFormula (@dots{}, @var{setOrder}) ## ## Parse and expand statistical model formulae using the Wilkinson notation. ## ## This function implements the recursive-descent parser and expansion logic ## described by Wilkinson & Rogers (1973) for factorial models. It allows the ## symbolic specification of analysis of variance and regression models, ## converting strings into computational schemas or design matrices. It also ## supports multi-variable response specification on the Left-Hand Side (LHS) ## using lists or ranges. ## ## @code{parseWilkinsonFormula} accepts as its first input argument a Wilkinson ## notation string specified by @var{formula} either as a character vector or a ## string scalar with the following list of valid symbols: ## ## @strong{Right-Hand Side (Model) Operators} ## The RHS specifies the independent variables (predictors) and the structural ## relationships between them, such as interactions and nesting. The parser ## expands these expressions into fundamental model terms following the standard ## statistical rules of marginality. Additionally, explicit nesting notation ## (e.g., @code{B(A)}) is supported to denote that factor B is nested within A. ## ## @multitable @columnfractions 0.15 0.35 0.50 ## @headitem Operator @tab Description @tab Expansion Example ## @item @code{+} @tab Addition (Union) @tab @code{A + B} expands to A, B ## @item @code{*} @tab Crossing @tab @code{A * B} expands to A, B, A:B ## @item @code{-} @tab Deletion @tab @code{A*B - A:B} expands to A, B ## @item @code{/} @tab Nesting @tab @code{A / B} expands to A, A:B ## @item @code{:} @tab Interaction @tab @code{A : B} expands to A:B ## @item @code{^} @tab Power (Limit) @tab @code{(A+B)^2} expands to A, B, A:B ## @item @code{1} @tab Intercept @tab @code{y ~ A - 1} removes intercept ## @end multitable ## ## ## @strong{Left-Hand Side (Response) Operators} ## The LHS, separated by the @code{~} operator, defines the dependent variables. ## It natively supports multi-response syntaxes. ## ## @multitable @columnfractions 0.15 0.35 0.50 ## @headitem Operator @tab Description @tab Usage Example ## @item @code{~} @tab Formula separator @tab @code{y ~ x} ## @item @code{,} @tab List separator @tab @code{y1, y2 ~ x} ## @item @code{-} @tab Range operator @tab @code{T1 - T3 ~ x} ## @end multitable ## ## @strong{Processing Modes} ## @code{parseWilkinsonFormula (@var{formula}, @var{mode})} evaluates the ## formula string based on the selected @var{mode}: ## ## @itemize ## @item @code{'expand'} (default) - Returns a structure containing ## @code{response} and @code{model} fields. Each field contains cell arrays ## of the expanded, fundamental terms. ## ## @item @code{'equation'} - Generates a string representing the mathematical ## equation of the fitted model. Coefficients are represented generically as ## @code{c1, c2, ...}. If multiple responses are specified, it returns a ## string array of equations. ## ## @multitable @columnfractions 0.40 0.60 ## @headitem Formula String @tab Equation Output ## @item @code{y ~ x} @tab @code{"y = c1 + c2*x"} ## @item @code{y ~ A * B} @tab @code{"y = c1 + c2*A + c3*B + c4*A*B"} ## @item @code{y ~ School / Class} @tab ## @code{"y = c1 + c2*School + ## c3*Class*School"} ## @item @code{y ~ x^2} @tab @code{"y = c1 + c2*x + c3*x^2"} ## @item @code{y1 - y2 ~ Trt} @tab @code{["y1 = c1 + c2*Trt", "y2 = ..."]} ## @end multitable ## ## @item @code{'matrix'} - Returns a schema structure containing a binary ## matrix defining term membership, useful for internal algorithmic processing. ## ## @item @code{'model_matrix'} - Constructs the numeric Design Matrix (@var{X}) ## and Response Matrix (@var{y}) directly from a provided data table. ## ## @item @code{'parse'} - Returns the raw Abstract Syntax Tree (AST) structure. ## ## @item @code{'tokenize'} - Returns the array of tokens generated by the lexer. ## ## @item @code{'mixed'} - Decomposes a mixed-effects formula containing ## random-effects terms of the form @code{(@var{expr} | @var{group})} into its ## fixed and random parts. See @strong{Mixed-Effects Formulae} below. ## @end itemize ## ## @strong{Order of the reported variables} ## @var{setOrder} is @qcode{'sorted'} or @qcode{'stable'} and comes last, ## after every other argument, so that it may be given whatever else the mode ## takes. @qcode{'sorted'} is the default and reports the variables in ## alphabetical order; @qcode{'stable'} reports them in the order the formula ## wrote them, as @code{unique} uses the same two words. It applies wherever ## a mode reports variable names, which is @qcode{'matrix'} and ## @qcode{'model_matrix'}, and the order carries into @var{ResponseIdx} and ## into the columns of @var{Terms} and of @var{X} beside it. ## ## @strong{Data Handling ('model_matrix' mode)} ## When using the @code{'model_matrix'} mode, a @var{data} argument must be ## provided as an Octave @code{table}. ## @itemize ## @item @strong{Categorical Variables:} Cell arrays of strings in the table ## are automatically detected as categorical factors and undergo corner-point ## (reference) dummy coding. ## @item @strong{Numeric Variables:} Standard numeric vectors are treated as ## continuous predictors or responses. ## @item @strong{Missing Data:} Rows containing @code{NaN} values in any of the ## active variables are automatically omitted from the final matrices. ## @end itemize ## ## @strong{Mixed-Effects Formulae ('mixed' mode)} ## A random-effects term is written @code{(@var{expr} | @var{group})}, where ## @var{expr} is a Wilkinson design expression for the random intercept and ## slopes and @var{group} is the grouping factor (or an interaction of factors ## such as @code{g1:g2}). As with fixed effects an intercept is implicit; ## suppress it with @code{0} or @code{-1} (for example @code{(x - 1 | g)} or ## @code{(-1 + x | g)} for a random slope with no random intercept). The ## @code{'mixed'} mode returns a structure with the following fields: ## ## @table @code ## @item Response ## The response (LHS) as a character vector. ## @item FixedTerms ## A cell array of the expanded fixed-effects terms, excluding the intercept. ## @item FixedIntercept ## A logical flag, true when the fixed model includes an intercept. ## @item Random ## A struct array with one element per random-effects term, each with fields ## @code{Expr} (the raw expression), @code{Terms} (its expanded predictor ## terms), @code{Intercept} (logical), @code{Group} (the raw grouping spec), ## and @code{GroupVars} (its grouping variables as a cell array). ## @item HasRandom ## A logical flag, true when the formula contains any random-effects term. ## @end table ## ## A formula with no random-effects term is still valid in @code{'mixed'} mode: ## it returns @code{HasRandom} false and an empty @code{Random} array. ## ## @strong{Outputs} ## @table @var ## @item setOrder ## Whether the variables are reported alphabetically, @qcode{'sorted'}, or in ## the order the formula wrote them, @qcode{'stable'}. The default is ## @qcode{'sorted'}. ## @item terms / result ## The processed model structure, string array, or cell array depending on the ## selected @var{mode}. ## @item X ## The generated numeric design matrix (Observations x Parameters). Includes ## a column of ones for the intercept unless @code{- 1} is in the formula. ## @item y ## The numeric response matrix (Observations x K responses). ## @item names ## A cell array of character vectors containing the column names corresponding ## to the generated design matrix @var{X}. ## @end table ## ## @strong{References} ## ## Wilkinson, G. N. and Rogers, C. E. (1973). Symbolic Description of Factorial ## Models for Analysis of Variance. Applied Statistics, 22, 392-399. ## ## ## In @qcode{"model_matrix"} mode a categorical variable expands to indicator ## columns, one per level bar the reference level, which the intercept carries. ## The levels of a character or string column are taken in the order the data ## presents them, so the reference level is the one seen first; a ## @code{categorical} column uses its own category order. When the formula has ## no intercept, the first categorical variable is given an indicator for every ## one of its levels and any further categorical variable stays reference ## coded. MATLAB omits the reference level whether or not an intercept is ## present, and so cannot fit the reference group. ## ## @end deftypefn function varargout = parseWilkinsonFormula (varargin) if (nargin < 1) error ("parseWilkinsonFormula: Input formula string is required."); endif ## The order the variables are reported in, taken from the last argument ## where it names one. It goes last so that it composes with every mode, ## 'model_matrix' already carrying data and categorical names of its own. setOrder = 'sorted'; if (nargin > 1 && ischar (varargin{end}) && any (strcmpi (varargin{end}, {'sorted', 'stable'}))) setOrder = lower (varargin{end}); varargin(end) = []; endif nargs = numel (varargin); if (nargs > 4) error ("parseWilkinsonFormula: Too many input arguments."); endif formula_str = varargin{1}; if (nargs > 1) mode = varargin{2}; else mode = 'expand'; endif mode = lower (mode); ## Mixed-effects decomposition: split off the random-effects '(expr|group)' ## terms at the string level and return a structured decomposition. Kept ## fully separate from the fixed-effects pipeline so the other modes are ## unaffected by formulas without random effects. if (strcmp (mode, 'mixed')) varargout{1} = parse_mixed_formula (formula_str); return; endif ## A '|' only ever appears inside a random-effects group, which the ## fixed-effects modes cannot represent. Fail with a clear pointer to 'mixed' ## rather than the lexer's generic "unexpected character". if (! isempty (strfind (char (formula_str), '|'))) error (strcat ("parseWilkinsonFormula: random-effects formulae (with", ... " '(...|...)' terms) require the 'mixed' mode.")); endif if (strcmp (mode, 'tokenize')) varargout{1} = run_lexer (formula_str); return; endif if (! strcmp (mode, 'model_matrix') && ! strcmp (mode, 'equation')) tokens = run_lexer (formula_str); [tree, curr] = run_parser (tokens); ## Check for Nested Tildes. if (isstruct (tree) && strcmp (tree.type, 'OPERATOR') && strcmp (tree.value, '~')) if ((! isempty (tree.left) && isstruct (tree.left) && strcmp (tree.left.type, 'OPERATOR') && strcmp (tree.left.value, '~')) || (! isempty (tree.right) && isstruct (tree.right) && strcmp (tree.right.type, 'OPERATOR') && strcmp (tree.right.value, '~'))) error ("parseWilkinsonFormula: Unexpected token"); endif endif ## Check for trailing. if (curr <= length (tokens) && ! strcmp (tokens(curr).type, 'EOF')) error ("parseWilkinsonFormula: Unexpected token"); endif else tree = []; endif ## Mode specific Processing switch (mode) case 'parse' varargout{1} = tree; case 'expand' varargout{1} = run_expander (tree, mode); case 'equation' has_data = nargs > 2 && isa (varargin{3}, 'table'); data_table = []; if (has_data), data_table = varargin{3}; endif [lhs_str, rhs_terms] = split_and_expand_rhs (formula_str, mode); ## resolve LHS. if (has_data) lhs_vars = resolve_lhs_vars (lhs_str, data_table); else lhs_vars = resolve_lhs_symbolic (lhs_str); endif ## build the required output. varargout{1} = run_equation_builder (lhs_vars, rhs_terms); case 'matrix' expanded = run_expander (tree, mode); varargout{1} = run_schema_builder (expanded, setOrder); case 'model_matrix' if (nargs < 3) error (strcat ("parseWilkinsonFormula: 'model_matrix'", ... " mode requires a Data Table.")); endif data_table = varargin{3}; if (! isa (data_table, 'table')) error ("parseWilkinsonFormula: Input data must be a 'table' class."); endif [lhs_str, rhs_terms] = split_and_expand_rhs (formula_str, mode); ## build schema. schema = run_schema_builder (rhs_terms, setOrder); ## resolve LHS. if (! isempty (lhs_str)) schema.ResponseVars = resolve_lhs_vars (lhs_str, data_table); else schema.ResponseVars = {}; endif ## build the required matrix. ## Variables the caller declares categorical, over and above those whose ## own type says so. catvars = {}; if (nargs > 3) catvars = varargin{4}; if (! iscellstr (catvars)) error (strcat ("parseWilkinsonFormula: CATVARS must be a cell", ... " array of character vectors.")); endif endif [X, y, names] = run_model_matrix_builder (schema, data_table, catvars); varargout{1} = X; if (nargout > 1), varargout{2} = y; endif if (nargout > 2), varargout{3} = names; endif otherwise error ("parseWilkinsonFormula: Unknown mode: %s", mode); endswitch endfunction ## lexer function tokens = run_lexer (formula_str) if (isempty (formula_str)) tokens = struct ('type', {}, 'value', {}, 'pos', {}); return; endif str = char (formula_str); n = length (str); tokens(n) = struct ('type', '', 'value', '', 'pos', 0); tok_idx = 0; i = 1; while (i <= n) c = str(i); start_pos = i; if (isspace (c)) i = i + 1; continue; endif if (c == ',') tok_idx = tok_idx + 1; tokens(tok_idx) = create_token ('COMMA', ',', start_pos); i++; continue; endif ## Identifiers (Factors) if (isletter (c)) val = c; i = i + 1; while (i <= n) next_c = str(i); if (isletter (next_c) || (next_c >= '0' && next_c <= '9') ... || next_c == '_') val = [val, next_c]; i = i + 1; else break; endif endwhile tok_idx = tok_idx + 1; tokens(tok_idx) = create_token ('IDENTIFIER', val, start_pos); continue; endif ## Numbers if (c >= '0' && c <= '9') val = c; i = i + 1; while (i <= n) next_c = str(i); if (next_c >= '0' && next_c <= '9') val = [val, next_c]; i = i + 1; elseif (next_c == '.') ## Simple float support if (i < n && str(i+1) >= '0' && str(i+1) <= '9') val = [val, next_c]; i = i + 1; else break; endif else break; endif endwhile tok_idx = tok_idx + 1; tokens(tok_idx) = create_token ('NUMBER', val, start_pos); continue; endif ## Operators type = ''; val = c; skip = 0; switch (c) case '-' if (i < n) if (i < n && str(i+1) == '/') type = 'OP_MINUS_MARGIN'; val = '-/'; skip = 1; elseif (i < n && str(i+1) == '*') type = 'OP_MINUS_CLEAN'; val = '-*'; skip = 1; else type = 'OP_MINUS'; endif else type = 'OP_MINUS'; endif case '*' type = 'OP_CROSS'; case '^' type = 'OP_POWER'; case '/' type = 'OP_NEST'; case '+' type = 'OP_PLUS'; case {'.', ':'} type = 'OP_DOT'; case '~' type = 'SEPARATOR'; case '(' type = 'LPAREN'; case ')' type = 'RPAREN'; otherwise error (strcat ("parseWilkinsonFormula: Unexpected", ... " character '%s' at position %d", c, i)); endswitch tok_idx = tok_idx + 1; tokens(tok_idx) = create_token (type, val, start_pos); i = i + 1 + skip; endwhile tokens = tokens(1:tok_idx); tokens(end+1) = create_token ('EOF', 'EOF', i); endfunction function t = create_token (type, val, pos) t.type = type; t.value = val; t.pos = pos; endfunction function [tree, curr] = run_parser (tokens, curr, prec_limit) if (nargin < 2), curr = 1; endif if (nargin < 3), prec_limit = 0; endif n = length (tokens); if (curr > n) error ("parseWilkinsonFormula: Unexpected End Of Formula."); endif t = tokens(curr); curr = curr + 1; ## Basic Units if (strcmp (t.type, 'IDENTIFIER')) ## Check for Function Call: IDENTIFIER followed by LPAREN if (curr <= n && strcmp (tokens(curr).type, 'LPAREN')) func_name = t.value; curr = curr + 1; args = {}; ## Parse arguments if (curr <= n && ! strcmp (tokens(curr).type, 'RPAREN')) while (true) [arg_node, curr] = run_parser (tokens, curr, 0); args{end+1} = arg_node; if (curr <= n && strcmp (tokens(curr).type, 'COMMA')) curr = curr + 1; else break; endif endwhile endif if (curr <= n && strcmp (tokens(curr).type, 'RPAREN')) curr = curr + 1; else error (strcat ("parseWilkinsonFormula: Missing ')'", ... " for function call '%s'."), func_name); endif tree.type = 'FUNCTION'; tree.name = func_name; tree.args = args; tree.left = []; tree.right = []; else ## Standard Variable tree.type = t.type; tree.value = t.value; tree.left = []; tree.right = []; endif elseif (strcmp (t.type, 'NUMBER')) tree.type = t.type; tree.value = t.value; tree.left = []; tree.right = []; elseif (strcmp (t.type, 'SEPARATOR')) ## Handle unary '~' tree.type = 'OPERATOR'; tree.value = t.value; tree.left = []; ## Recursively parse the RHS with precedence 5. [tree.right, curr] = run_parser (tokens, curr, 5); elseif (strcmp (t.type, 'LPAREN')) [tree, curr] = run_parser (tokens, curr, 0); if (curr <= n && strcmp (tokens(curr).type, 'RPAREN')) curr = curr + 1; else error ("parseWilkinsonFormula: Mismatched Parentheses. Missing ')'."); endif elseif (strcmp (t.type, 'EOF')) error ("parseWilkinsonFormula: Unexpected End Of Formula."); else error ("parseWilkinsonFormula: Syntax Error. Unexpected token: '%s'", t.value); endif ## Operator Handling while (curr <= n) op_type = tokens(curr).type; op_prec = 0; ## precedences if (strcmp (op_type, 'OP_POWER')) op_prec = 60; elseif (strcmp (op_type, 'OP_DOT')) op_prec = 50; elseif (strcmp (op_type, 'OP_NEST')) op_prec = 40; elseif (strcmp (op_type, 'OP_CROSS')) op_prec = 30; elseif (strcmp (op_type, 'OP_PLUS')) op_prec = 20; elseif (strncmp (op_type, 'OP_MINUS', 8)) op_prec = 10; elseif (strcmp (op_type, 'SEPARATOR')) op_prec = 5; else break; endif if (op_prec <= prec_limit) break; endif op_val = tokens(curr).value; curr = curr + 1; [right, curr] = run_parser (tokens, curr, op_prec); new_node.type = 'OPERATOR'; new_node.value = op_val; new_node.left = tree; new_node.right = right; tree = new_node; endwhile endfunction ## expander function result = run_expander (node, mode) if (nargin < 2), mode = 'expand'; endif if (isempty (node)) result = {}; return; endif ## Terminals if (strcmp (node.type, 'IDENTIFIER')) result = {{node.value}}; return; elseif (strcmp (node.type, 'FUNCTION')) if (strcmp (mode, 'equation')) ## preserve nesting syntax. args_str_parts = {}; for k = 1:length (node.args) arg_res = run_expander (node.args{k}, mode); if (! isempty (arg_res) && ! isempty (arg_res{1})) args_str_parts{end+1} = arg_res{1}{1}; else args_str_parts{end+1} = ''; endif endfor full_term = sprintf ("%s(%s)", node.name, strjoin (args_str_parts, ',')); result = {{full_term}}; else ## mathematical expansion in matrix mode. result = {{node.name}}; for k = 1:length (node.args) arg_expanded = run_expander (node.args{k}, mode); result = list_product (result, arg_expanded); endfor endif return; elseif (strcmp (node.type, 'NUMBER')) if (strcmp (node.value, '1')) result = {{}}; else result = {{node.value}}; endif return; endif if (strcmp (node.type, 'OPERATOR')) if (strcmp (node.value, '~')) result.response = run_expander (node.left, mode); add_intercept = true; if (! isempty (node.right) && strcmp (node.right.type, 'OPERATOR') ... && (strcmp (node.right.value, '-') ... || strcmp (node.right.value, '-/') ... || strcmp (node.right.value, '-*'))) if (! isempty (node.right.right) ... && strcmp (node.right.right.type, 'NUMBER') ... && strcmp (node.right.right.value, '1')) add_intercept = false; endif endif model_raw = run_expander (node.right, mode); if (add_intercept) result.model = list_union ({{}}, model_raw); else result.model = model_raw; endif return; endif lhs = run_expander (node.left, mode); rhs = run_expander (node.right, mode); switch (node.value) case '+' result = list_union (lhs, rhs); case {'.', ':'} result = list_product (lhs, rhs); case '*' interaction = list_product (lhs, rhs); step1 = list_union (lhs, rhs); result = list_union (step1, interaction); case '^' if (strcmp (node.left.type, 'IDENTIFIER') && strcmp (node.right.type, 'NUMBER')) base_name = node.left.value; power_val = round (str2double (node.right.value)); result = {}; for k = 1:power_val if (k == 1) result{end+1} = {base_name}; else result{end+1} = {sprintf("%s^%d", base_name, k)}; endif endfor return; endif base_terms = run_expander (node.left, mode); if (! strcmp (node.right.type, 'NUMBER')) error ("parseWilkinsonFormula: Exponent must be a number."); endif power_val = str2double (node.right.value); result = base_terms; ## Repeatedly apply Crossing. for k = 2:power_val interaction = list_product (result, base_terms); step1 = list_union (result, base_terms); result = list_union (step1, interaction); endfor case '/' max_L = get_maximal_terms (lhs); interaction = list_product (max_L, rhs); result = list_union (lhs, interaction); case '-' ## Simple Deletion result = list_difference (lhs, rhs, 'exact'); case '-*' ## Delete term + Higher Order Interactions result = list_difference (lhs, rhs, 'clean'); case '-/' ## Delete terms where T is marginal result = list_difference (lhs, rhs, 'margin'); otherwise error ("parseWilkinsonFormula: Unknown operator '%s'", node.value); endswitch return; endif error ("parseWilkinsonFormula: Corrupt Tree."); endfunction ## set operations. function C = list_union (A, B) raw_list = [A, B]; C = simplify_term_list (raw_list); endfunction function C = list_product (A, B) C = {}; idx = 1; for i = 1:length (A) for j = 1:length (B) ## Dot product merges factor sets: new_term = union (A{i}, B{j}); C{idx} = new_term; idx = idx + 1; endfor endfor C = simplify_term_list (C); endfunction function C = list_difference (S, T, mode) if (isempty (S)), C = {}; return; endif if (isempty (T)), C = S; return; endif C = {}; strS = terms_to_strings (S); strT = terms_to_strings (T); keep_mask = true (size (S)); for i = 1:length (S) term_s = S{i}; s_str = strS{i}; for j = 1:length (T) term_t = T{j}; t_str = strT{j}; match = false; switch (mode) case 'exact' if (strcmp (s_str, t_str)) match = true; endif case 'clean' ## Delete T and any S where T is a subset of S if (strcmp (s_str, t_str) || is_subset (term_t, term_s)) match = true; endif case 'margin' ## Delete S where T is subset of S (but not T itself) if (! strcmp (s_str, t_str) && is_subset (term_t, term_s)) match = true; endif endswitch if (match) keep_mask(i) = false; break; endif endfor endfor C = S(keep_mask); endfunction function fac = get_fac (term_list) all_factors = {}; for i = 1:length (term_list) all_factors = union (all_factors, term_list{i}); endfor fac = {all_factors}; endfunction function is_sub = is_subset (small_set, large_set) is_sub = all (ismember (small_set, large_set)); endfunction function clean_list = simplify_term_list (raw_list) if (isempty (raw_list)) clean_list = {}; return; endif str_sigs = terms_to_strings (raw_list); [~, unique_idx] = unique (str_sigs); clean_list = raw_list(sort (unique_idx)); endfunction function strs = terms_to_strings (term_list) strs = cell (size (term_list)); for i = 1:length (term_list) if (isempty (term_list{i})) strs{i} = '1'; else sorted_factors = sort (term_list{i}); strs{i} = strjoin (sorted_factors, ':'); endif endfor endfunction ## schema builder function schema = run_schema_builder (expanded, setOrder) ## Handle struct vs cell if (isstruct (expanded)) if (isfield (expanded, 'model')) rhs_terms = expanded.model; else rhs_terms = expanded.rhs; endif if (isfield (expanded, 'response')) lhs_term = expanded.response; else lhs_term = expanded.lhs; endif else rhs_terms = expanded; lhs_term = {}; endif function out = flatten_recursive (in_val) out = {}; if (ischar (in_val) || isstring (in_val)) out = {char(in_val)}; elseif (iscell (in_val)) for k = 1:numel (in_val) out = [out, flatten_recursive(in_val{k})]; endfor endif endfunction ## extract variables all_vars = {}; if (! isempty (lhs_term)) all_vars = [all_vars, flatten_recursive(lhs_term)]; endif cleaned_rhs = cell (length (rhs_terms), 1); for i = 1:length (rhs_terms) term_vars = flatten_recursive (rhs_terms{i}); final_term_vars = {}; for j = 1:length (term_vars) parts = strsplit (term_vars{j}, ':'); final_term_vars = [final_term_vars, parts]; endfor cleaned_rhs{i} = final_term_vars; all_vars = [all_vars, final_term_vars]; endfor all_vars = unique (all_vars, setOrder); ## Remove intercept marker from var list all_vars(strcmp (all_vars, '1')) = []; schema.VariableNames = all_vars; ## Identify Response schema.ResponseIdx = []; if (! isempty (lhs_term)) flat_lhs = flatten_recursive (lhs_term); if (! isempty (flat_lhs)) [found, idx] = ismember (flat_lhs{1}, all_vars); if (found), schema.ResponseIdx = idx; endif endif endif ## Build terms matrix n_vars = length (all_vars); n_terms = length (cleaned_rhs); terms_mat = zeros (n_terms, n_vars); for i = 1:n_terms vars_in_this_term = cleaned_rhs{i}; ## Check for intercept term. if (isempty (vars_in_this_term) || (length (vars_in_this_term) == 1 && strcmp (vars_in_this_term{1}, '1'))) continue; endif [found, idx] = ismember (vars_in_this_term, all_vars); if (any (! found)) error ("parseWilkinsonFormula: Unknown variable in term definition."); endif terms_mat(i, idx) = 1; endfor ## sorting : order by true polynomial degree, not raw presence count. var_degree = ones (1, n_vars); for v = 1:n_vars tok = regexp (all_vars{v}, '\^(\d+)$', 'tokens'); if (! isempty (tok)) var_degree(v) = str2double (tok{1}{1}); endif endfor term_orders = terms_mat * var_degree'; M = [term_orders, terms_mat]; [~, unique_idx] = unique (M, 'rows'); terms_mat = terms_mat(unique_idx, :); [~, sort_idx] = sortrows ([terms_mat * var_degree', -terms_mat]); schema.Terms = terms_mat(sort_idx, :); endfunction ## model matrix builder. function [X, y, col_names] = run_model_matrix_builder (schema, data, catvars) if (nargin < 3) catvars = {}; endif req_vars = schema.VariableNames; table_vars = data.Properties.VariableNames; ## A model's terms are ordered by the variable order of the data, not by the ## order the formula happens to name them: over a table whose columns are ## (u, g), both 'resp ~ 1 + u + g' and 'resp ~ 1 + g + u' give the same model, ## while the same formula over a (g, u) table puts g first. The schema is ## built without the data in hand, so it arrives in the parser's own order; ## sort it into the table's here, then order the terms over it. [req_vars, schema.Terms, perm] = order_by_table (req_vars, schema.Terms, ... table_vars); if (! isempty (schema.ResponseIdx)) schema.ResponseIdx = find (perm == schema.ResponseIdx, 1); endif schema.Terms = order_terms (schema.Terms, req_vars, table_vars); ## Data validation & masking if (isempty (req_vars)) n_total = height (data); valid_mask = true (n_total, 1); else base0 = regexprep (req_vars{1}, '\^.*$', ''); if (! ismember (base0, table_vars)) error ("parseWilkinsonFormula: Unknown variable '%s' in Data Table.", ... req_vars{1}); endif n_total = length (data.(base0)); valid_mask = true (n_total, 1); for i = 1:length (req_vars) base_i = regexprep (req_vars{i}, '\^.*$', ''); col = data.(base_i); if (isnumeric (col)) valid_mask = valid_mask & ! isnan (col); endif endfor endif if (! isempty (schema.ResponseIdx)) y_name = req_vars{schema.ResponseIdx}; if (! ismember (y_name, table_vars)) error ("parseWilkinsonFormula: Unknown variable '%s' in Data Table.", ... y_name); endif y_col = data.(y_name); if (isnumeric (y_col)) valid_mask = valid_mask & ! isnan (y_col); endif endif if (isfield (schema, 'ResponseVars') && ! isempty (schema.ResponseVars)) for k = 1:length (schema.ResponseVars) y_name = schema.ResponseVars{k}; if (ismember (y_name, table_vars)) col = data.(y_name); if (isnumeric (col)) valid_mask = valid_mask & ! isnan (col); endif endif endfor endif n_rows = sum (valid_mask); ## Process predictors var_info = struct (); for i = 1:length (req_vars) vname = req_vars{i}; hat_pos = strfind (vname, '^'); if (! isempty (hat_pos)) base_name = vname(1:hat_pos(1)-1); exp_val = str2double (vname(hat_pos(1)+1:end)); else base_name = vname; exp_val = 1; endif raw = data.(base_name); if (iscell (raw)), raw = raw(valid_mask); else, raw = raw(valid_mask, :); endif ## A logical column, or a numeric one the caller declared categorical, is ## coded by its distinct values in ascending order, so the omitted ## reference level is the smallest. Left to itself a logical column would ## fall through to the character branch below, where 'cellstr' turns false ## and true into the characters of code 0 and 1. is_declared = any (strcmp (catvars, base_name)); if (islogical (raw) || (is_declared && isnumeric (raw))) if (exp_val != 1) error (strcat ("parseWilkinsonFormula: Power operator '^' is", ... " only valid on numeric variables.")); endif vals = double (raw(:)); vals = sort (unique (vals(! isnan (vals)))); var_info.(vname).type = 'categorical'; var_info.(vname).levels = arrayfun (@(x) strtrim (num2str (x)), ... vals, 'UniformOutput', false); [~, var_info.(vname).indices] = ismember (double (raw), vals); elseif (isnumeric (raw)) var_info.(vname).type = 'numeric'; var_info.(vname).data = raw .^ exp_val; elseif (exp_val != 1) error (strcat ("parseWilkinsonFormula: Power operator '^' is", ... " only valid on numeric variables.")); elseif (isa (raw, 'categorical')) var_info.(vname).type = 'categorical'; var_info.(vname).levels = categories (raw); [~, var_info.(vname).indices] = ismember (raw, var_info.(vname).levels); else if (! iscellstr (raw) && ! isstring (raw)) raw = cellstr (raw); endif ## The levels of a character or string grouping column are taken in the ## order the data presents them, not in sorted order, so the omitted ## reference level is the one seen first. A 'categorical' column carries ## its own category order and is handled above. [u, ~, idx] = unique (raw, 'stable'); var_info.(vname).type = 'categorical'; var_info.(vname).levels = u; var_info.(vname).indices = idx; endif endfor ## Build Design Matrix X X = []; col_names = {}; ## Check for intercept term. intercept_row_idx = find (sum (schema.Terms, 2) == 0); has_intercept = ! isempty (intercept_row_idx); ## Without an intercept the first categorical variable takes its place and is ## given all of its levels; any further categorical stays reference coded, or ## the design would be rank deficient with each full set of indicators ## summing to the intercept column. full_coded_var = ''; if (! has_intercept) for v = 1:numel (req_vars) if (strcmp (var_info.(req_vars{v}).type, 'categorical')) full_coded_var = req_vars{v}; break; endif endfor endif n_terms = size (schema.Terms, 1); for i = 1:n_terms term_row = schema.Terms(i, :); vars_idx = find (term_row); ## Intercept Term if (isempty (vars_idx)) X = [X, ones(n_rows, 1)]; col_names = [col_names; '(Intercept)']; continue; endif current_block = ones (n_rows, 1); current_names = {''}; for v = vars_idx vname = req_vars{v}; info = var_info.(vname); if (strcmp (info.type, 'numeric')) current_block = current_block .* info.data; for k = 1:length (current_names) if (isempty (current_names{k})) current_names{k} = vname; else current_names{k} = [current_names{k}, ':', vname]; endif endfor else ## Categorical n_lev = length (info.levels); ## Drop the reference level only when something can carry it: an ## intercept, or the first categorical standing in for one. This is ## the cell-means parameterisation that dropping the intercept asks ## for. MATLAB drops the reference level either way and so cannot fit ## the reference group at all: 'resp ~ h2 - 1' over a three-level h2 ## gives it two coefficients, fitted values of exactly 0 for that group ## and a negative R^2 (measured against R2024a, 2026-08-05). R and ## patsy both code every level here. Do not "fix" this to match. if (has_intercept || ! strcmp (vname, full_coded_var)) start_lev = 2; n_cols = n_lev - 1; else start_lev = 1; n_cols = n_lev; endif dummies = zeros (n_rows, n_cols); dum_names = {}; for L = start_lev:n_lev col_idx = L - start_lev + 1; dummies(:, col_idx) = (info.indices == L); dum_names = [dum_names; ... sprintf("%s_%s", vname, char (info.levels{L}))]; endfor ## Cartesian product of current block and new dummies next_block = []; next_names = {}; for c1 = 1:size (current_block, 2) for c2 = 1:size (dummies, 2) next_block = [next_block, current_block(:, c1) .* dummies(:, c2)]; n1 = current_names{c1}; n2 = dum_names{c2}; if (isempty (n1)) next_names = [next_names; n2]; else next_names = [next_names; [n1, ':', n2]]; endif endfor endfor current_block = next_block; current_names = next_names; endif endfor X = [X, current_block]; col_names = [col_names; current_names]; endfor ## Extract Response y = []; if (isfield (schema, 'ResponseVars') && ! isempty (schema.ResponseVars)) y_vars = schema.ResponseVars; y = zeros (n_rows, length (y_vars)); for k = 1:length (y_vars) y_name = y_vars{k}; raw_y = data.(y_name); if (iscell (raw_y)) col_data = raw_y(valid_mask); try col_data = cell2mat (col_data); catch error (strcat ("parseWilkinsonFormula: Response", ... " variable '%s' must be numeric."), y_name); end_try_catch else col_data = raw_y(valid_mask, :); endif if (! isnumeric (col_data)) error (strcat ("parseWilkinsonFormula: Response", ... " variable '%s' must be numeric"), y_name); endif if (size (col_data, 1) != n_rows) error (strcat ("parseWilkinsonFormula: Mismatch in number", ... " of rows for response variable '%s'"), y_name); endif y(:, k) = col_data; endfor ## fallback to previous. elseif (! isempty (schema.ResponseIdx)) y_name = req_vars{schema.ResponseIdx}; raw_y = data.(y_name); if (iscell (raw_y)), y = raw_y(valid_mask); else, y = raw_y(valid_mask, :); endif endif endfunction ## Sort a schema's variables into the order the data table lists them and ## permute the columns of its terms matrix to match. A power such as 'u^2' ## sorts with its base variable, immediately after it. Names the table does not ## carry keep their relative order at the end, so that an unresolvable variable ## still reaches the error raised for it further down. function [vars, terms, perm] = order_by_table (vars, terms, table_vars) n = numel (vars); key = zeros (n, 2); for i = 1:n base = regexprep (vars{i}, '\^.*$', ''); j = find (strcmp (table_vars, base), 1); if (isempty (j)) j = numel (table_vars) + i; endif key(i,:) = [j, var_power(vars{i})]; endfor [~, perm] = sortrows (key); vars = vars(perm); terms = terms(:, perm); endfunction ## Order the rows of a terms matrix the way a model's terms are ordered: by the ## total degree of the term, then -- within one degree -- interactions before ## powers, and last by the variables the term involves. With the columns ## already in the data's variable order this yields '1 + u + v + u:v + u^2'. function terms = order_terms (terms, vars, table_vars) n_terms = rows (terms); n_vars = columns (terms); if (n_terms < 2) return; endif base_idx = zeros (1, n_vars); degree = ones (1, n_vars); for j = 1:n_vars base = regexprep (vars{j}, '\^.*$', ''); k = find (strcmp (table_vars, base), 1); if (isempty (k)) k = numel (table_vars) + j; endif base_idx(j) = k; degree(j) = var_power (vars{j}); endfor ## The intercept row is all zeros, so it keys as degree 0 and sorts first. key = zeros (n_terms, n_vars + 2); for t = 1:n_terms bases = unique (base_idx(terms(t, :) != 0)); key(t, 1) = terms(t, :) * degree(:); key(t, 2) = -numel (bases); key(t, 3:2+numel (bases)) = bases; endfor [~, ord] = sortrows (key); terms = terms(ord, :); endfunction ## The exponent carried by a variable name, 1 when it carries none. function p = var_power (name) p = 1; tok = regexp (name, '\^(\d+)$', 'tokens'); if (! isempty (tok)) p = str2double (tok{1}{1}); endif endfunction function max_terms = get_maximal_terms (term_list) n = length (term_list); if (n == 0), max_terms = {}; return; endif is_max = true (1, n); for i = 1:n for j = 1:n if (i == j), continue; endif ## If term 'i' is a subset of 'j', it is NOT maximal if (is_subset (term_list{i}, term_list{j})) is_max(i) = false; break; endif endfor endfor max_terms = term_list(is_max); endfunction function vars = resolve_lhs_vars (lhs_str, data) all_names = data.Properties.VariableNames; vars = {}; if (isempty (lhs_str)), return; endif parts = strsplit (lhs_str, ','); for i = 1:length (parts) p = strtrim (parts{i}); if (isempty (p)), continue; endif ## check for the range. range_parts = strsplit (p, '-'); if (length (range_parts) == 2) start_var = strtrim (range_parts{1}); end_var = strtrim (range_parts{2}); [found_s, idx_s] = ismember (start_var, all_names); [found_e, idx_e] = ismember (end_var, all_names); if (! found_s) error ("parseWilkinsonFormula: Unknown variable '%s' in range", ... start_var); endif if (! found_e) error ("parseWilkinsonFormula: Unknown variable '%s' in range", ... end_var); endif ## Slice names. if (idx_s <= idx_e) range_vars = all_names(idx_s:idx_e); else range_vars = all_names(idx_e:idx_s); endif vars = [vars, range_vars]; elseif (length (range_parts) == 1) ## Single Variable if (! any (strcmp (all_names, p))) error (strcat ("parseWilkinsonFormula: Response", ... " variable '%s' not found in Data."), p); endif vars = [vars, {p}]; else error ("parseWilkinsonFormula: Invalid syntax in response term '%s'", p); endif endfor vars = unique (vars, 'stable'); endfunction function [lhs_str, rhs_terms] = split_and_expand_rhs (formula_str, mode) if (nargin < 2), mode = 'expand'; endif tilde_idx = strfind (formula_str, '~'); if (! isempty (tilde_idx)) lhs_str = strtrim (formula_str(1:tilde_idx(1)-1)); rhs_str = strtrim (formula_str(tilde_idx(1)+1:end)); else lhs_str = ''; rhs_str = formula_str; endif ## process RHS rhs_tokens = run_lexer (rhs_str); [rhs_tree, ~] = run_parser (rhs_tokens); wrapper.type = 'OPERATOR'; wrapper.value = '~'; wrapper.left = []; wrapper.right = rhs_tree; expanded = run_expander (wrapper, mode); ## extract the terms. if (isstruct (expanded) && isfield (expanded, 'model')) rhs_terms = expanded.model; else rhs_terms = expanded; endif endfunction function vars = resolve_lhs_symbolic (lhs_str) vars = {}; if (isempty (lhs_str)), return; endif parts = strsplit (lhs_str, ','); for i = 1:length (parts) p = strtrim (parts{i}); if (isempty (p)), continue; endif range_parts = strsplit (p, '-'); if (length (range_parts) == 2) s_str = strtrim (range_parts{1}); e_str = strtrim (range_parts{2}); [s_tok] = regexp (s_str, '^([a-zA-Z_]\w*)(\d+)$', 'tokens'); [e_tok] = regexp (e_str, '^([a-zA-Z_]\w*)(\d+)$', 'tokens'); if (! isempty (s_tok) && ! isempty (e_tok)) prefix = s_tok{1}{1}; s_num = str2double (s_tok{1}{2}); e_prefix = e_tok{1}{1}; e_num = str2double (e_tok{1}{2}); if (strcmp (prefix, e_prefix) && s_num <= e_num) for n = s_num:e_num vars{end+1} = sprintf ("%s%d", prefix, n); endfor continue; endif endif error ("parseWilkinsonFormula: Invalid symbolic range '%s'.", p); elseif (length (range_parts) == 1) vars{end+1} = p; else error ("parseWilkinsonFormula: Invalid syntax '%s'.", p); endif endfor vars = unique (vars, 'stable'); endfunction function eq_list = run_equation_builder (lhs_vars, rhs_terms) term_strs = {}; for i = 1:length (rhs_terms) t = rhs_terms{i}; if (isempty (t)) term_strs{end+1} = ''; else if (length (t) == 1 && any (strfind (t{1}, '('))) term_strs{end+1} = t{1}; else term_strs{end+1} = strjoin (sort (t), '*'); endif endif endfor lines = {}; c_idx = 1; for k = 1:length (lhs_vars) rhs_parts = {}; for t = 1:length (term_strs) t_str = term_strs{t}; coeff = sprintf ("c%d", c_idx++); if (isempty (t_str)) rhs_parts{end+1} = coeff; elseif (strcmp (t_str, '1')) rhs_parts{end+1} = coeff; else rhs_parts{end+1} = sprintf ("%s*%s", coeff, t_str); endif endfor full_rhs = strjoin (rhs_parts, ' + '); if (isempty (full_rhs)), full_rhs = '0'; endif lines{end+1} = sprintf ("%s = %s", lhs_vars{k}, full_rhs); endfor eq_list = string (lines'); endfunction ## --------------------------------------------------------------------------- ## Mixed-effects formula support: y ~ + ( | ) + ... ## --------------------------------------------------------------------------- ## Decompose a mixed-model formula into its fixed and random parts. Returns a ## struct: Response (char LHS), FixedTerms (cell of predictor term-cells, no ## intercept marker), FixedIntercept (logical), Random (struct array with fields ## Expr/Terms/Intercept/Group/GroupVars), HasRandom (logical). function S = parse_mixed_formula (formula_str) formula_str = char (formula_str); tilde = strfind (formula_str, '~'); if (isempty (tilde)) lhs_str = ''; rhs_str = strtrim (formula_str); else lhs_str = strtrim (formula_str(1:tilde(1)-1)); rhs_str = strtrim (formula_str(tilde(1)+1:end)); endif [fixed_str, specs] = extract_random_effects (rhs_str); ## Fixed part -- full existing Wilkinson grammar, unchanged. if (isempty (strtrim (fixed_str))) fixed_terms = {}; fixed_int = true; ## MATLAB includes a fixed intercept else [~, ft] = split_and_expand_rhs (["__resp__ ~ ", fixed_str], "expand"); fixed_int = any (cellfun (@isempty, ft)); fixed_terms = ft(! cellfun (@isempty, ft)); endif ## Random parts. R = struct ("Expr", {}, "Terms", {}, "Intercept", {}, ... "Group", {}, "GroupVars", {}); for i = 1:numel (specs) [rt, hi] = parse_random_expr (specs(i).Expr); gv = parse_group_spec (specs(i).Group); R(i) = struct ("Expr", specs(i).Expr, "Terms", {rt}, "Intercept", hi, ... "Group", specs(i).Group, "GroupVars", {gv}); endfor S.Response = lhs_str; S.FixedTerms = fixed_terms; S.FixedIntercept = fixed_int; S.Random = R; S.HasRandom = ! isempty (specs); ## Reconstructed fixed-only formula, so callers can build the fixed design ## through the ordinary 'model_matrix' path. if (isempty (strtrim (fixed_str))) fixed_rhs = "1"; else fixed_rhs = fixed_str; endif if (isempty (lhs_str)) S.FixedFormula = ["~ ", fixed_rhs]; else S.FixedFormula = [lhs_str, " ~ ", fixed_rhs]; endif endfunction ## Split a formula RHS into its fixed-effects string and the list of ## random-effects '(expr | group)' specs. Parentheses that do NOT contain a ## top-level '|' (e.g. nesting 'B(A)' or precedence grouping) are left in the ## fixed string for the ordinary parser. function [fixed_str, specs] = extract_random_effects (rhs_str) specs = struct ("Expr", {}, "Group", {}); out = ""; i = 1; n = numel (rhs_str); while (i <= n) c = rhs_str(i); if (c == "(") depth = 1; j = i + 1; pipe = 0; while (j <= n && depth > 0) cj = rhs_str(j); if (cj == "(") depth += 1; elseif (cj == ")") depth -= 1; if (depth == 0) break; endif elseif (cj == "|" && depth == 1 && pipe == 0) pipe = j; endif j += 1; endwhile if (depth != 0) error ("parseWilkinsonFormula: unbalanced parentheses in formula."); endif if (pipe > 0) expr = strtrim (rhs_str(i+1:pipe-1)); grp = strtrim (rhs_str(pipe+1:j-1)); if (isempty (expr) || isempty (grp)) error ("parseWilkinsonFormula: malformed random-effects term '(%s)'.", ... rhs_str(i+1:j-1)); endif specs(end+1) = struct ("Expr", expr, "Group", grp); else out = [out, rhs_str(i:j)]; ## ordinary group -> keep for fixed endif i = j + 1; else out = [out, c]; i += 1; endif endwhile fixed_str = clean_fixed_str (out); endfunction ## Tidy the fixed-effects string after random groups are removed: collapse the ## '+ +' and dangling leading/trailing '+' they leave behind. function s = clean_fixed_str (s) s = strtrim (s); s = regexprep (s, '\+\s*\+', '+'); s = regexprep (s, '^\s*\+', ''); s = regexprep (s, '\+\s*$', ''); s = strtrim (s); endfunction ## Expand a random-effects design expression into predictor terms + an ## intercept flag. Handles the intercept-suppression forms MATLAB accepts but ## the base parser does not: '-1 + x', 'x - 1', '0 + x', 'x + 0'. Random ## expressions use only '+', ':' and '*', so splitting on '+' is safe here. function [terms, has_int] = parse_random_expr (expr) expr = strtrim (regexprep (char (expr), '\s+', ' ')); has_int = true; ## leading '-1 +' / '-0 +' suppresses the intercept e2 = regexprep (expr, '^-\s*[01]\s*\+\s*', ''); if (! strcmp (e2, expr)), has_int = false; endif expr = e2; ## trailing '- 1', '- 0' or '+ 0' suppresses; trailing '+ 1' keeps it e3 = regexprep (expr, '\s*[-+]\s*[01]\s*$', ''); if (! strcmp (e3, expr)) tail = expr(numel (e3)+1:end); if (! isempty (strfind (tail, "-")) || ! isempty (strfind (tail, "0"))) has_int = false; endif endif expr = e3; atoms = strsplit (expr, "+"); preds = {}; for k = 1:numel (atoms) a = strtrim (atoms{k}); if (isempty (a) || strcmp (a, "1")) continue; ## empty or explicit intercept marker elseif (strcmp (a, "0")) has_int = false; else preds{end+1} = a; endif endfor if (isempty (preds)) terms = {}; return; endif [~, ex] = split_and_expand_rhs (["__d__ ~ ", strjoin(preds, " + ")], "expand"); terms = ex(! cellfun (@isempty, ex)); endfunction ## Parse a grouping spec into its variable list ('g' -> {g}; 'g:g2' -> {g,g2}). function gv = parse_group_spec (g) gv = {}; parts = strsplit (strtrim (g), ":"); for i = 1:numel (parts) p = strtrim (parts{i}); if (! isempty (p)) gv{end+1} = p; endif endfor endfunction %!demo %! %! ## Simple Linear Regression : %! ## This example models a continuous response (Height) as a linear function %! ## of a single continuous predictor (Age). The 'equation' mode returns the %! ## symbolic representation, while 'model_matrix' generates the design matrix. %! Age = [10; 12; 14; 16; 18]; %! Height = [140; 148; 155; 162; 170]; %! t = table (Height, Age); %! %! formula = 'Height ~ Age'; %! disp (['Formula: ', formula]); %! equation = parseWilkinsonFormula (formula, 'equation') %! [X, y, names] = parseWilkinsonFormula (formula, 'model_matrix', t) %!demo %! %! ## Multiple Regression : %! ## Here we model House Price based on two independent predictors: Area and %! ## number of Rooms. The '+' operator adds terms to the model without assuming %! ## any interaction between them. %! Price = [300; 350; 400; 450]; %! Area = [1500; 1800; 2200; 2500]; %! Rooms = [3; 3; 4; 5]; %! t = table (Price, Area, Rooms); %! %! formula = 'Price ~ Area + Rooms'; %! disp (['Formula: ', formula]); %! equation = parseWilkinsonFormula (formula, 'equation') %! [X, y, names] = parseWilkinsonFormula (formula, 'model_matrix', t) %!demo %! %! ## Interaction Effects : %! ## We analyze Relief Score based on Drug Type and Dosage Level. %! ## The '*' operator expands to the main effects PLUS the interaction term. %! ## Categorical variables are automatically created. %! Relief = [5; 7; 6; 8]; %! Drug = {'Placebo'; 'Placebo'; 'Active'; 'Active'}; %! Dose = {'Low'; 'High'; 'Low'; 'High'}; %! t = table (Relief, Drug, Dose); %! %! formula = 'Relief ~ Drug * Dose'; %! disp (['Formula: ', formula]); %! equation = parseWilkinsonFormula (formula, 'equation') %! [X, y, names] = parseWilkinsonFormula (formula, 'model_matrix', t) %!demo %! %! ## Polynomial Regression : %! ## Uses the power operator (^) to model non-linear relationships. %! Distance = [20; 45; 80; 125]; %! Speed = [30; 50; 70; 90]; %! Speed_2 = Speed .^ 2; %! t = table (Distance, Speed, Speed_2, 'VariableNames', {'Distance', 'Speed', 'Speed^2'}); %! %! formula = 'Distance ~ Speed^2'; %! disp (['Formula: ', formula]); %! equation = parseWilkinsonFormula (formula, 'equation') %! [X, y, names] = parseWilkinsonFormula (formula, 'model_matrix', t) %!demo %! %! ## Hierarchical Design. %! ## Common in psychometrics. Here, 'Class' is nested within 'School'. %! ## The '/' operator implies School + School:Class. %! Score = [88; 92; 75; 80]; %! School = {'North'; 'North'; 'South'; 'South'}; %! Class = {'Rm101'; 'Rm102'; 'Rm201'; 'Rm202'}; %! t = table (Score, School, Class); %! %! formula = 'Score ~ School / Class'; %! disp (['Formula: ', formula]); %! equation = parseWilkinsonFormula (formula, 'equation') %! terms = parseWilkinsonFormula (formula, 'expand') %!demo %! %! ## Explicit Nesting : %! ## The parser also supports the explicit 'B(A)' syntax, which means %! ## 'B is nested within A'. This is equivalent to the interaction 'A:B' %! ## but often used to denote random effects or specific hierarchy. %! formula = 'y ~ Class(School)'; %! disp (['Formula: ', formula]); %! equation = parseWilkinsonFormula (formula, 'equation') %! terms = parseWilkinsonFormula (formula, 'expand') %!demo %! %! ## Excluding Terms : %! ## Demonstrates building a complex model and then simplifying it. %! ## We define a full 3-way interaction (A*B*C) but explicitly remove the %! ## three-way term (A:B:C) using the minus operator. %! formula = 'y ~ (A + B + C)^3 - A:B:C'; %! disp (['Formula: ', formula]); %! equation = parseWilkinsonFormula (formula, 'equation') %! terms = parseWilkinsonFormula (formula, 'expand') %!demo %! %! ## Repeated Measures : %! ## This allows predicting multiple outcomes simultaneously. %! ## The range operator '-' selects all variables between 'T1' and 'T3' %! ## as the response matrix Y. %! T1 = [10; 11]; %! T2 = [12; 13]; %! T3 = [14; 15]; %! Treatment = {'Control'; 'Treated'}; %! t = table (T1, T2, T3, Treatment); %! %! formula = 'T1 - T3 ~ Treatment'; %! disp (['Formula: ', formula]); %! equations = parseWilkinsonFormula (formula, 'equation') %! [X, Y, names] = parseWilkinsonFormula (formula, 'model_matrix', t) %!test %! ## Test : Identifiers with numbers and underscores %! tokens = parseWilkinsonFormula ('Yield ~ Var_1 + A2_B', 'tokenize'); %! vals = {tokens.value}; %! assert_equal (vals, {'Yield', '~', 'Var_1', '+', 'A2_B', 'EOF'}); %!test %! ## Test : Floating point numbers %! tokens = parseWilkinsonFormula ('y ~ 0.5 * A', 'tokenize'); %! vals = {tokens.value}; %! assert_equal (vals, {'y', '~', '0.5', '*', 'A', 'EOF'}); %!test %! ## Test : Whitespace insensitivity %! t1 = parseWilkinsonFormula ('A*B', 'tokenize'); %! t2 = parseWilkinsonFormula ('A * B', 'tokenize'); %! assert_equal ({t1.value}, {t2.value}); %!test %! ## Test : Precedence %! t = parseWilkinsonFormula ('A + B * C . D', 'expand'); %! terms = cellfun (@(x) strjoin (sort (x), ':'), t, 'UniformOutput', false); %! assert_equal (sort (terms), sort ({'A', 'B', 'C:D', 'B:C:D'})); %!test %! ## Test : Parentheses Override %! t = parseWilkinsonFormula ('(A + B) . C', 'expand'); %! terms = cellfun (@(x) strjoin (sort (x), ':'), t, 'UniformOutput', false); %! assert_equal (sort (terms), sort ({'A:C', 'B:C'})); %!test %! ## Test : Crossing Operator (*) %! t = parseWilkinsonFormula ('A * B', 'expand'); %! assert_equal (length (t), 3); %! t3 = parseWilkinsonFormula ('A * B * C', 'expand'); %! assert_equal (length (t3), 7); %!test %! ## Test : Nesting Operator (/) %! t = parseWilkinsonFormula ('Field / Plot', 'expand'); %! terms = cellfun (@(x) strjoin (sort (x), ':'), t, 'UniformOutput', false); %! assert_equal (sort (terms), sort ({'Field', 'Field:Plot'})); %!test %! ## Test : Multi-level Nesting %! t = parseWilkinsonFormula ('Block / Plot / Subplot', 'expand'); %! terms = cellfun (@(x) strjoin (sort (x), ':'), t, 'UniformOutput', false); %! assert_equal (sort (terms), sort ({'Block', 'Block:Plot', 'Block:Plot:Subplot'})); %!test %! ## Test : Interaction Operator (.) %! t = parseWilkinsonFormula ('A . B', 'expand'); %! assert_equal (length (t), 1); %! assert_equal (t{1}, {'A', 'B'}); %!test %! ## Test : Power operator on cube. %! t = parseWilkinsonFormula ('(A + B + C)^3', 'expand'); %! terms = cellfun (@(x) strjoin (sort (x), ':'), t, 'UniformOutput', false); %! expected = sort ({'A', 'B', 'C', 'A:B', 'A:C', 'B:C', 'A:B:C'}); %! assert_equal (sort (terms), expected); %!test %! ## Test : Power Operator. %! t = parseWilkinsonFormula ('(A + B + C)^2', 'expand'); %! terms = cellfun (@(x) strjoin (sort (x), ':'), t, 'UniformOutput', false); %! assert_equal (! ismember ('A:B:C', terms), true); %! assert_equal (ismember ('A:B', terms), true); %!test %! ## Test : Redundancy Check %! t1 = parseWilkinsonFormula ('A + A', 'expand'); %! assert_equal (length (t1), 1); %! t2 = parseWilkinsonFormula ('A * A', 'expand'); %! assert_equal (length (t2), 1); %!test %! ## Test : Deletion - Exact (-) %! t = parseWilkinsonFormula ('A * B - A', 'expand'); %! terms = cellfun (@(x) strjoin (sort (x), ':'), t, 'UniformOutput', false); %! assert_equal (sort (terms), sort ({'B', 'A:B'})); %!test %! ## Test : Deletion - Clean (-*) %! t = parseWilkinsonFormula ('A * B -* A', 'expand'); %! terms = cellfun (@(x) strjoin (sort (x), ':'), t, 'UniformOutput', false); %! assert_equal (sort (terms), {'B'}); %!test %! ## Test : Deletion - Marginal (-/) %! t = parseWilkinsonFormula ('A * B -/ A', 'expand'); %! terms = cellfun (@(x) strjoin (sort (x), ':'), t, 'UniformOutput', false); %! assert_equal (sort (terms), sort ({'A', 'B'})); %!test %! ## Test : Deletion - Complex Sequence %! t = parseWilkinsonFormula ('A*B*C - A:B:C', 'expand'); %! assert_equal (length (t), 6); %! terms = cellfun (@(x) strjoin (sort (x), ':'), t, 'UniformOutput', false); %! assert_equal (! ismember ('A:B:C', terms), true); %! assert_equal (ismember ('A:B', terms), true); %!test %! ## Test : LHS and RHS Identification %! s = parseWilkinsonFormula ('logY ~ A + B', 'matrix'); %! assert_equal (s.VariableNames{s.ResponseIdx}, 'logY'); %! assert_equal (any (strcmp ('A', s.VariableNames)), true); %!test %! ## Test : No Response Variable %! s = parseWilkinsonFormula ('~ A + B', 'matrix'); %! assert_equal (isempty (s.ResponseIdx), true); %!test %! ## Test : Intercept Handling %! s1 = parseWilkinsonFormula ('~ A', 'matrix'); %! assert_equal (any (all (s1.Terms == 0, 2)), true); %! s2 = parseWilkinsonFormula ('~ A - 1', 'matrix'); %! assert_equal (! any (all (s2.Terms == 0, 2)), true); %!test %! ## Test : Numeric Interaction %! y = [1;2;3;4;5]; %! X1 = [1;2;1;2;1]; %! X2 = [10;10;20;20;10]; %! d = table (y, X1, X2); %! [M, ~, ~] = parseWilkinsonFormula ('y ~ X1:X2', 'model_matrix', d); %! assert_equal (size (M), [5, 2]); %! assert_equal (M(:, 2), d.X1 .* d.X2); %!test %! ## Test : Categorical Expansion %! y = [1;1;1]; %! G = {'A'; 'B'; 'C'}; %! d = table (y, G); %! [M, ~, names] = parseWilkinsonFormula ('~ G', 'model_matrix', d); %! assert_equal (size (M, 2), 3); %! assert_equal (names, {'(Intercept)'; 'G_B'; 'G_C'}); %!test %! ## Test : Categorical * Categorical Rank %! y = [1;2;3;4]; %! F1 = {'a';'b';'a';'b'}; %! F2 = {'x';'x';'y';'y'}; %! d = table (y, F1, F2); %! [M, ~, ~] = parseWilkinsonFormula ('~ F1 * F2', 'model_matrix', d); %! assert_equal (size (M, 2), 4); %! assert_equal (rank (M), 4); %!test %! ## Test : Numeric * Categorical Naming %! ## The terms follow the data's variable order, and the omitted reference %! ## level of a character grouping column is the one the data shows first. %! y = [1;2]; %! N = [10; 20]; %! C = {'lo'; 'hi'}; %! d = table (y, N, C); %! [M, ~, names] = parseWilkinsonFormula ('~ N * C', 'model_matrix', d); %! assert_equal (names(:)', {'(Intercept)', 'N', 'C_hi', 'N:C_hi'}); %!test %! ## Test : the variable order of the data drives the term order %! y = [1;2]; %! N = [10; 20]; %! C = {'lo'; 'hi'}; %! d = table (y, C, N); %! [M, ~, names] = parseWilkinsonFormula ('~ N * C', 'model_matrix', d); %! assert_equal (names(:)', {'(Intercept)', 'C_hi', 'N', 'C_hi:N'}); %!test %! ## Test : Intercept Only Model %! y = [1; 2; 3]; %! d = table (y); %! [X, ~, names] = parseWilkinsonFormula ('y ~ 1', 'model_matrix', d); %! assert_equal (size (X, 2), 1); %! assert_equal (names, {'(Intercept)'}); %! assert_equal (all (X == 1), true); %!test %! ## Test : NaNs and Missing Data %! y = [1; 2; 3; 4]; %! A = [1; 1; NaN; 1]; %! B = [10; 20; 30; NaN]; %! d = table (y, A, B); %! [X, y_out, ~] = parseWilkinsonFormula ('y ~ A', 'model_matrix', d); %! assert_equal (length (y_out), 3); %! assert_equal (y_out(3), 4); %! assert_equal (size (X, 1), 3); %!test %! ## Test : Nesting with Groups %! t = parseWilkinsonFormula ('A / (B + C)', 'expand'); %! terms = cellfun (@(x) strjoin (sort (x), ':'), t, 'UniformOutput', false); %! expected = sort ({'A', 'A:B', 'A:C'}); %! assert_equal (sort (terms), expected); %! ## Test : Variable Name Collision %! Var = [1; 1]; %! Var_1 = [2; 2]; %! d = table (Var, Var_1); %! [~, ~, names] = parseWilkinsonFormula ('~ Var + Var_1', 'model_matrix', d); %! assert_equal (any (strcmp (names, 'Var')), true); %! assert_equal (any (strcmp (names, 'Var_1')), true); %!test %! ## Test : One-argument call %! result = parseWilkinsonFormula ('A * B'); %! expected = sort ({'A', 'B', 'A:B'}); %! actual = cellfun (@(x) strjoin (sort (x), ':'), result, 'UniformOutput', false); %! assert_equal (sort (actual), expected); %!test %! ## Test : Compatibility with Table Data %! Age = [25; 30; 35; 40; 45]; %! Weight = [70; 75; 80; 85; 90]; %! BP = [120; 122; 128; 130; 135]; %! T = table (Age, Weight, BP); %! formula = 'BP ~ Age * Weight'; %! [X, y, names] = parseWilkinsonFormula (formula, 'model_matrix', T); %! assert_equal (size (X), [5, 4]); %! assert_equal (y, BP); %! assert_equal (any (strcmp ('Age', names)), true); %! assert_equal (any (strcmp ('Weight', names)), true); %! assert_equal (names{1}, '(Intercept)'); %!test %! ## Test : Multi-variable List %! y1 = [1; 2; 3]; y2 = [4; 5; 6]; x = [1; 0; 1]; %! d = table (y1, y2, x); %! [X, y, ~] = parseWilkinsonFormula ('y1, y2 ~ x', 'model_matrix', d); %! assert_equal (size (y), [3, 2]); %! assert_equal (y(:,1), d.y1); %! assert_equal (y(:,2), d.y2); %!test %!test %! ## Test : multivariable range. %! A = [10;20]; B = [30;40]; C = [50;60]; x = [1;2]; %! d = table (A, B, C, x); %! [X, y, ~] = parseWilkinsonFormula ('A - C ~ x', 'model_matrix', d); %! assert_equal (size (y), [2, 3]); %! assert_equal (y(:,1), d.A); %! assert_equal (y(:,2), d.B); %! assert_equal (y(:,3), d.C); %!test %! ## Test : multivariable list + range. %! y1 = [1]; y2 = [2]; y3 = [3]; y4 = [4]; y5 = [5]; %! x1 = [10]; x2 = [2]; %! d = table (y1, y2, y3, y4, y5, x1, x2); %! [X, y, names] = parseWilkinsonFormula ('y1, y3 - y5 ~ x1:x2', 'model_matrix', d); %! expected_y = [d.y1, d.y3, d.y4, d.y5]; %! assert_equal (isequal (y, expected_y), true); %! assert_equal (size (X, 2), 2); %! assert_equal (any (strcmp (names, 'x1:x2')), true); %!test %! ## Test : reverse range. %! A = [1]; B = [2]; C = [3]; x = [10]; %! d = table (A, B, C, x); %! [X, y, names] = parseWilkinsonFormula ('C - A ~ x - 1', 'model_matrix', d); %! assert_equal (size (y), [1, 3]); %! assert_equal (y(:,1), d.A); %! assert_equal (y(:,3), d.C); %! assert_equal (size (X, 2), 1); %! assert_equal (! any (strcmp (names, '(Intercept)')), true); %!test %! ## Test : nans in multi-y. %! yA = {1; 2; 3; 4}; %! yB = [10; 20; NaN; 40]; %! x = [1; 1; 1; 1]; %! d = table (yA, yB, x); %! [X, y, ~] = parseWilkinsonFormula ('yA, yB ~ x', 'model_matrix', d); %! assert_equal (size (y), [3, 2]); %! assert_equal (y(3, 1), 4); %! assert_equal (y(3, 2), 40); %! assert_equal (size (X, 1), 3); %!test %! ## Test : basic. %! eq = parseWilkinsonFormula ('y ~ x1 + x2 - 9', 'equation'); %! expected = string ('y = c1 + c2*x1 + c3*x2'); %! assert_equal (isequal (eq, expected), true); %!test %! ## Test : explicit intercept. %! eq = parseWilkinsonFormula ('y ~ x1 + x2', 'equation'); %! expected = string ('y = c1 + c2*x1 + c3*x2'); %! assert_equal (isequal (eq, expected), true); %!test %! ## Test : interaction. %! eq = parseWilkinsonFormula ('y ~ x1:x2:x3:x4', 'equation'); %! expected = string ('y = c1 + c2*x1*x2*x3*x4'); %! assert_equal (isequal (eq, expected), true); %!test %! ## Test : crossing/factorial. %! eq = parseWilkinsonFormula ('y ~ A * B', 'equation'); %! expected = string ('y = c1 + c2*A + c3*B + c4*A*B'); %! assert_equal (isequal (eq, expected), true); %!test %! ## Test : polynomials. %! eq = parseWilkinsonFormula ('y ~ x^4 - x^2', 'equation'); %! expected = string ('y = c1 + c2*x^3 + c3*x^4'); %! assert_equal (isequal (eq, expected), true); %!test %! ## Test : repeated measures %! eq = parseWilkinsonFormula ('y1-y3 ~ x', 'equation'); %! expected = string (['y1 = c1 + c2*x'; ... %! 'y2 = c3 + c4*x'; ... %! 'y3 = c5 + c6*x']); %! assert_equal (isequal (eq, expected), true); %!test %! ## Test : nesting syntax. %! eq = parseWilkinsonFormula ('y ~ x2(x1)', 'equation'); %! expected = string ('y = c1 + c2*x2(x1)'); %! assert_equal (isequal (eq, expected), true); %!test %! ## Test : nesting with interaction. %! eq = parseWilkinsonFormula ('y ~ x3:x2(x1)', 'equation'); %! expected = string ('y = c1 + c2*x2(x1)*x3'); %! assert_equal (isequal (eq, expected), true); %!test %! ## Test : multiple nesting. %! eq = parseWilkinsonFormula ('y ~ Var(A, B)', 'equation'); %! expected = string ('y = c1 + c2*Var(A,B)'); %! assert_equal (isequal (eq, expected), true); %!test %! ## Test : nested factors %! eq = parseWilkinsonFormula ('y ~ x2(x1) + x3(x4)', 'equation'); %! expected = string ('y = c1 + c2*x2(x1) + c3*x3(x4)'); %! assert_equal (isequal (eq, expected), true); %!test %! ## Test : polynomial and nesting. %! eq = parseWilkinsonFormula ('y ~ x^2 + Effect(Group)', 'equation'); %! expected = string ('y = c1 + c2*x + c3*x^2 + c4*Effect(Group)'); %! assert_equal (isequal (eq, expected), true); %!test %! ## Test : symbolic resolution of LHS list %! eq = parseWilkinsonFormula ('A, B ~ x', 'equation'); %! expected = string (['A = c1 + c2*x'; 'B = c3 + c4*x']); %! assert_equal (isequal (eq, expected), true); %!test %! ## Test : intercept only. %! eq = parseWilkinsonFormula ('y ~ 1', 'equation'); %! expected = string ('y = c1'); %! assert_equal (isequal (eq, expected), true); %!test %! ## Test : empty model. %! eq = parseWilkinsonFormula ('y ~ A - A', 'equation'); %! expected = string ('y = c1'); %! assert_equal (isequal (eq, expected), true); %!test %! ## Test : term row sorting. %! eq = parseWilkinsonFormula ('Y ~ x1 * x2 * x3', 'matrix'); %! expected_terms = [0, 0, 0, 0; 0, 1, 0, 0; 0, 0, 1, 0; 0, 0, 0, 1; 0, 1, 1, 0; 0, 1, 0, 1; 0, 0, 1, 1; 0, 1, 1, 1]; %! assert_equal (eq.VariableNames, {'Y', 'x1', 'x2', 'x3'}); %! assert_equal (eq.Terms, expected_terms); %!test %! ## Test : polynomial term Weight^2 resolves from base column not as table variable name %! Weight = [2000; 2500; 3000; 3500; 4000]; %! MPG = [30; 28; 25; 22; 18]; %! d = table (Weight, MPG); %! [X, yout, names] = parseWilkinsonFormula ('MPG ~ Weight^2', 'model_matrix', d); %! assert_equal (size (X), [5, 3]); %! assert_equal (names{1}, '(Intercept)'); %! assert_equal (any (strcmp (names, 'Weight')), true); %! assert_equal (any (strcmp (names, 'Weight^2')), true); %! w2i = find (strcmp (names, 'Weight^2')); %! assert_equal (X(:, w2i), Weight .^ 2, 1e-10); %! assert_equal (yout, MPG); %!test %! ## Test : squared term sorts after a categorical term %! Weight = [2000;2500;3000;3500;4000;4500;2200;2700;3200;3700;4200;4700]; %! MPG = [30;28;25;22;18;16;29;26;23;20;17;15]; %! Year = {'70';'70';'70';'70';'76';'76';'76';'76';'82';'82';'82';'82'}; %! d = table (MPG, Weight, Year); %! [~, ~, n1] = parseWilkinsonFormula ('MPG ~ Year + Weight^2', 'model_matrix', d); %! [~, ~, n2] = parseWilkinsonFormula ('MPG ~ Weight^2 + Year', 'model_matrix', d); %! assert_equal (n1, {'(Intercept)'; 'Weight'; 'Year_76'; 'Year_82'; 'Weight^2'}); %! assert_equal (n2, n1); %!error parseWilkinsonFormula () %!error parseWilkinsonFormula ('y ~ x', 'invalid_mode') %!error parseWilkinsonFormula ('', 'parse') %!error parseWilkinsonFormula ('A +', 'parse') %!error parseWilkinsonFormula ('A *', 'parse') %!error parseWilkinsonFormula ('A .', 'parse') %!error parseWilkinsonFormula ('A /', 'parse') %!error parseWilkinsonFormula ('(A+B)^C', 'expand') %!error parseWilkinsonFormula ('(A + B', 'parse') %!error parseWilkinsonFormula ('A + B)', 'parse') %!error parseWilkinsonFormula ('( )', 'parse') %!error parseWilkinsonFormula ('A + * B', 'parse') %!error parseWilkinsonFormula ('y ~ x ~ z', 'parse') %!error <'model_matrix' mode requires a Data Table> parseWilkinsonFormula ('~ A', 'model_matrix') %!error d=table ([1], 'VariableNames', {'x'}); parseWilkinsonFormula ('~ Z', 'model_matrix', d) %!error d=table ([1], [1], 'VariableNames', {'x', 'y'}); parseWilkinsonFormula ('Z ~ x', 'model_matrix', d) %!error d=table ([1], [1], 'VariableNames', {'x', 'y'}); parseWilkinsonFormula ('A - y ~ x', 'model_matrix', d) %!error d=table ([1], [1], 'VariableNames', {'x', 'y'}); parseWilkinsonFormula ('y - B ~ x', 'model_matrix', d) %!error d=table ([1], 'VariableNames', {'y'}); parseWilkinsonFormula ('y - y - y ~ x', 'model_matrix', d) %!error S={'a';'b'}; x=[1;2]; d=table (S, x); parseWilkinsonFormula ('S ~ x', 'model_matrix', d) %!error parseWilkinsonFormula ('y ~ x', 'model_matrix', [1,2,3]) %!error parseWilkinsonFormula ('y1-yA ~ x', 'equation') %!error parseWilkinsonFormula ('yA-y1 ~ x', 'equation') %!error parseWilkinsonFormula ('A-B ~ x', 'equation') %!error parseWilkinsonFormula ('y1- ~ x', 'equation') %!error parseWilkinsonFormula () ## Mixed-effects 'mixed' mode %!test # random intercept: basic decomposition %! S = parseWilkinsonFormula ('y ~ x + z + (1|g)', 'mixed'); %! assert_equal (S.HasRandom, true); %! assert_equal (S.FixedIntercept, true); %! assert_equal (numel (S.FixedTerms), 2); %! assert_equal (numel (S.Random), 1); %! assert_equal (S.Random(1).Intercept, true); %! assert_equal (isempty (S.Random(1).Terms), true); %! assert_equal (S.Random(1).GroupVars, {'g'}); %! assert_equal (S.Random(1).Group, 'g'); %! assert_equal (S.Response, 'y'); %!test # (x|g) carries an implicit intercept, like the fixed-effects rule %! S = parseWilkinsonFormula ('y ~ x + (x|g)', 'mixed'); %! assert_equal (S.Random(1).Intercept, true); %! assert_equal (numel (S.Random(1).Terms), 1); %! assert_equal (S.Random(1).Terms{1}, {'x'}); %!test # explicit (1 + x|g) matches the implicit form %! S = parseWilkinsonFormula ('y ~ x + (1 + x|g)', 'mixed'); %! assert_equal (S.Random(1).Intercept, true); %! assert_equal (S.Random(1).Terms, {{'x'}}); %!test # all four intercept-suppression spellings agree: slope only, no int %! for f = {'(x-1|g)', '(-1 + x|g)', '(0 + x|g)', '(x + 0|g)'} %! S = parseWilkinsonFormula (['y ~ x + ' f{1}], 'mixed'); %! assert_equal (! S.Random(1).Intercept, true); %! assert_equal (S.Random(1).Terms, {{'x'}}); %! endfor %!test # two random blocks on the same grouping factor %! S = parseWilkinsonFormula ('y ~ x + (1|g) + (x-1|g)', 'mixed'); %! assert_equal (numel (S.Random), 2); %! assert_equal (S.Random(1).Intercept && isempty (S.Random(1).Terms), true); %! assert_equal (! S.Random(2).Intercept, true); %! assert_equal (S.Random(2).Terms, {{'x'}}); %! assert_equal (S.Random(1).GroupVars, {'g'}); %! assert_equal (S.Random(2).GroupVars, {'g'}); %!test # crossed grouping factors %! S = parseWilkinsonFormula ('y ~ x + (1|g) + (1|g2)', 'mixed'); %! assert_equal (numel (S.Random), 2); %! assert_equal (S.Random(1).GroupVars, {'g'}); %! assert_equal (S.Random(2).GroupVars, {'g2'}); %!test # interaction (nested) grouping g:g2 %! S = parseWilkinsonFormula ('y ~ x + (1|g:g2)', 'mixed'); %! assert_equal (S.Random(1).GroupVars, {'g', 'g2'}); %! assert_equal (S.Random(1).Group, 'g:g2'); %!test # triple interaction grouping %! S = parseWilkinsonFormula ('y ~ (1|a:b:c)', 'mixed'); %! assert_equal (S.Random(1).GroupVars, {'a', 'b', 'c'}); %!test # multi-term random slopes (1 + x + z|g) %! S = parseWilkinsonFormula ('y ~ x + (1 + x + z|g)', 'mixed'); %! assert_equal (S.Random(1).Intercept, true); %! assert_equal (numel (S.Random(1).Terms), 2); %!test # interaction in the random design (1 + x:z|g) %! S = parseWilkinsonFormula ('y ~ x + (1 + x:z|g)', 'mixed'); %! assert_equal (numel (S.Random(1).Terms), 1); %! assert_equal (sort (S.Random(1).Terms{1}), {'x', 'z'}); %!test # crossing '*' expands in the random design to x + z + x:z %! S = parseWilkinsonFormula ('y ~ x + (x*z|g)', 'mixed'); %! assert_equal (S.Random(1).Intercept, true); %! assert_equal (numel (S.Random(1).Terms), 3); %!test # intercept-only fixed part: y ~ (1|g) %! S = parseWilkinsonFormula ('y ~ (1|g)', 'mixed'); %! assert_equal (S.FixedIntercept, true); %! assert_equal (isempty (S.FixedTerms), true); %! assert_equal (numel (S.Random), 1); %!test # suppressed fixed intercept coexists with a random term %! S = parseWilkinsonFormula ('y ~ x - 1 + (1|g)', 'mixed'); %! assert_equal (! S.FixedIntercept, true); %! assert_equal (numel (S.FixedTerms), 1); %!test # fixed interaction via '*' expands independently of the random part %! S = parseWilkinsonFormula ('y ~ x*z + (1|g)', 'mixed'); %! assert_equal (numel (S.FixedTerms), 3); %! assert_equal (numel (S.Random), 1); %!test # precedence parentheses in the fixed part are NOT random terms %! S = parseWilkinsonFormula ('y ~ (x + z) + (1|g)', 'mixed'); %! assert_equal (numel (S.FixedTerms), 2); %! assert_equal (numel (S.Random), 1); %!test # nesting in the fixed part coexists with a random term %! S = parseWilkinsonFormula ('y ~ a/b + (1|g)', 'mixed'); %! assert_equal (S.FixedIntercept, true); %! assert_equal (numel (S.Random), 1); %!test # 'mixed' mode degrades gracefully on a fixed-only formula %! S = parseWilkinsonFormula ('y ~ x + z', 'mixed'); %! assert_equal (! S.HasRandom, true); %! assert_equal (isempty (S.Random), true); %! assert_equal (numel (S.FixedTerms), 2); %!test # FixedFormula reconstructs the fixed-only formula string %! S = parseWilkinsonFormula ('y ~ x + x2 + (1 + x|g)', 'mixed'); %! assert_equal (strtrim (S.FixedFormula), 'y ~ x + x2'); %! S2 = parseWilkinsonFormula ('y ~ (1|g)', 'mixed'); %! assert_equal (strtrim (S2.FixedFormula), 'y ~ 1'); %!test # one-sided (no response) formula %! S = parseWilkinsonFormula ('~ x + (1|g)', 'mixed'); %! assert_equal (S.Response, ''); %! assert_equal (numel (S.Random), 1); %!test # random design widths q reproduce the MATLAB-verified reference forms %! chk = { 'y ~ x + x2 + (1|g)', [1]; ... %! 'y ~ x + x2 + (1 + x|g)', [2]; ... %! 'y ~ x + x2 + (1|g) + (x-1|g)', [1 1]; ... %! 'y ~ x + x2 + (x-1|g)', [1]; ... %! 'y ~ x + x2 + (-1 + x|g)', [1]; ... %! 'y ~ x + x2 + (1|g) + (1|g2)', [1 1]; ... %! 'y ~ x + x2 + (1|g:g2)', [1]; ... %! 'y ~ x + x2 + (1 + x + x2|g)', [3] }; %! for k = 1:rows (chk) %! S = parseWilkinsonFormula (chk{k,1}, 'mixed'); %! q = arrayfun (@(r) r.Intercept + numel (r.Terms), S.Random); %! assert_equal (q, chk{k,2}); %! endfor ## Mixed-effects error handling %!error parseWilkinsonFormula ('y ~ x + (1|g)', 'expand') %!error parseWilkinsonFormula ('y ~ (1|g)') %!error parseWilkinsonFormula ('y ~ x + (1|g)', 'model_matrix', table ()) %!error parseWilkinsonFormula ('y ~ x + (1|g', 'mixed') %!error parseWilkinsonFormula ('y ~ (1|)', 'mixed') %!error parseWilkinsonFormula ('y ~ (|g)', 'mixed') %!test # the variables are reported alphabetically unless asked otherwise %! r = parseWilkinsonFormula ('y ~ b + a', 'matrix'); %! assert_equal (r.VariableNames, {'a', 'b', 'y'}); %! assert_equal (r.ResponseIdx, 3); %!test # 'stable' reports them in the order the formula wrote them %! r = parseWilkinsonFormula ('y ~ b + a', 'matrix', 'stable'); %! assert_equal (r.VariableNames, {'y', 'b', 'a'}); %! assert_equal (r.ResponseIdx, 1); ## The order carries into the terms matrix, whose rows name their variable ## through the reported names and so follow them %!test %! a = parseWilkinsonFormula ('y ~ b + a', 'matrix'); %! b = parseWilkinsonFormula ('y ~ b + a', 'matrix', 'stable'); %! an = arrayfun (@(r) a.VariableNames{find(a.Terms(r,:), 1)}, ... %! 2:rows (a.Terms), 'UniformOutput', false); %! bn = arrayfun (@(r) b.VariableNames{find(b.Terms(r,:), 1)}, ... %! 2:rows (b.Terms), 'UniformOutput', false); %! assert_equal (an, {'a', 'b'}); %! assert_equal (bn, {'b', 'a'}); %! assert_equal (sum (a.Terms(:)), sum (b.Terms(:))); %!test # 'sorted' may be named, and is what the default does %! a = parseWilkinsonFormula ('y ~ b + a', 'matrix'); %! b = parseWilkinsonFormula ('y ~ b + a', 'matrix', 'sorted'); %! assert_equal (a.VariableNames, b.VariableNames); %! assert_equal (a.Terms, b.Terms); %!test # the order is taken whatever the mode, the default one included %! r = parseWilkinsonFormula ('y ~ b + a', 'stable'); %! assert_equal (isfield (r, 'response'), true); %! assert_equal (cellstr (r.response), {'y'}); %!test # it comes last, after the data a mode of its own takes %! b = [1; 2; 3; 4]; %! a = [4; 3; 2; 1]; %! y = [1; 0; 1; 0]; %! T = table (b, a, y); %! [X1, y1, n1] = parseWilkinsonFormula ('y ~ a + b', 'model_matrix', T); %! [X2, y2, n2] = parseWilkinsonFormula ('y ~ a + b', 'model_matrix', T, ... %! {}, 'stable'); %! assert_equal (y1, y2); %! assert_equal (size (X1), size (X2)); %! assert_equal (numel (n1), numel (n2)); %!test # a formula whose names are already in order reads the same either way %! a = parseWilkinsonFormula ('y ~ a + b', 'matrix', 'stable'); %! assert_equal (a.VariableNames, {'y', 'a', 'b'}); ## The order argument is taken off the end before the count is checked, so ## the longest call a mode takes still fits %!error ... %! parseWilkinsonFormula ('y ~ a', 'matrix', 1, 2, 3, 'stable') %!error ... %! parseWilkinsonFormula ('y ~ a', 'model_matrix', 'stable') statistics-release-2.1.2/inst/Experimental_Design/sampsizepwr.m000066400000000000000000001265321527311104300247770ustar00rootroot00000000000000## Copyright (C) 2022 Andrew Penn ## Copyright (C) 2022-2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{n} =} sampsizepwr (@var{testtype}, @var{params}, @var{p1}) ## @deftypefnx {statistics} {@var{n} =} sampsizepwr (@var{testtype}, @var{params}, @var{p1}, @var{power}) ## @deftypefnx {statistics} {@var{power} =} sampsizepwr (@var{testtype}, @var{params}, @var{p1}, [], @var{n}) ## @deftypefnx {statistics} {@var{p1} =} sampsizepwr (@var{testtype}, @var{params}, [], @var{power}, @var{n}) ## @deftypefnx {statistics} {[@var{n1}, @var{n2}] =} sampsizepwr (@qcode{'t2'}, @var{params}, @var{p1}, @var{power}) ## @deftypefnx {statistics} {[@dots{}] =} sampsizepwr (@var{testtype}, @var{params}, @var{p1}, @var{power}, @var{n}, @var{name}, @var{value}) ## ## Sample size and power calculation for hypothesis test. ## ## @code{sampsizepwr} computes the sample size, power, or alternative parameter ## value for a hypothesis test, given the other two values. For example, you can ## compute the sample size required to obtain a particular power for a ## hypothesis test, given the parameter value of the alternative hypothesis. ## ## @code{@var{n} = sampsizepwr (@var{testtype}, @var{params}, @var{p1})} returns ## the sample size N required for a two-sided test of the specified type to have ## a power (probability of rejecting the null hypothesis when the alternative is ## true) of 0.90 when the significance level (probability of rejecting the null ## hypothesis when the null hypothesis is true) is 0.05. @var{params} specifies ## the parameter values under the null hypothesis. P1 specifies the value of ## the single parameter being tested under the alternative hypothesis. For the ## two-sample t-test, N is the value of the equal sample size for both samples, ## @var{params} specifies the parameter values of the first sample under the ## null and alternative hypotheses, and P1 specifies the value of the single ## parameter from the other sample under the alternative hypothesis. ## ## The following TESTTYPE values are available: ## ## @multitable @columnfractions 0.1 0.85 ## @item "z" @tab one-sample z-test for normally distributed data with ## known standard deviation. @var{params} is a two-element vector [MU0 SIGMA0] ## of the mean and standard deviation, respectively, under the null hypothesis. ## P1 is the value of the mean under the alternative hypothesis. ## @item "t" @tab one-sample t-test or paired t-test for normally ## distributed data with unknown standard deviation. @var{params} is a ## two-element vector [MU0 SIGMA0] of the mean and standard deviation, ## respectively, under the null hypothesis. P1 is the value of the mean under ## the alternative hypothesis. ## @item "t2" @tab two-sample pooled t-test (test for equal means) for ## normally distributed data with equal unknown standard deviations. ## @var{params} is a two-element vector [MU0 SIGMA0] of the mean and standard ## deviation of the first sample under the null and alternative hypotheses. P1 ## is the the mean of the second sample under the alternative hypothesis. ## @item "var" @tab chi-square test of variance for normally distributed ## data. @var{params} is the variance under the null hypothesis. P1 is the ## variance under the alternative hypothesis. ## @item "p" @tab test of the P parameter (success probability) for a ## binomial distribution. @var{params} is the value of P under the null ## hypothesis. P1 is the value of P under the alternative hypothesis. ## @item "r" @tab test of the correlation coefficient parameter for ## significance. @var{params} is the value of r under the null hypothesis. ## P1 is the value of r under the alternative hypothesis. ## @end multitable ## ## The "p" test for the binomial distribution is a discrete test for which ## increasing the sample size does not always increase the power. For N values ## larger than 200, there may be values smaller than the returned N value that ## also produce the desired power. ## ## @code{@var{n} = sampsizepwr (@var{testtype}, @var{params}, @var{p1}, ## @var{power})} returns the sample size N such that the power is @var{power} ## for the parameter value P1. For the two-sample t-test, N is the equal sample ## size of both samples. ## ## @code{[@var{n1}, @var{n2}] = sampsizepwr ("t2", @var{params}, @var{p1}, ## @var{power})} returns the sample sizes @var{n1} and @var{n2} for the two ## samples. These values are the same unless the "ratio" parameter, ## @code{@var{ratio} = @var{n2} / @var{n2}}, is set to a value other than ## the default (See the name/value pair definition of ratio below). ## ## @code{@var{power} = sampsizepwr (@var{testtype}, @var{params}, @var{p1}, [], ## @var{n})} returns the power achieved for a sample size of @var{n} when the ## true parameter value is @var{p1}. For the two-sample t-test, @var{n} is the ## smaller one of the two sample sizes. ## ## @code{@var{p1} = sampsizepwr (@var{testtype}, @var{params}, [], @var{power}, ## @var{n})} returns the parameter value detectable with the specified sample ## size @var{n} and power @var{power}. For the two-sample t-test, @var{n} is ## the smaller one of the two sample sizes. When computing @var{p1} for the "p" ## test, if no alternative can be rejected for a given @var{params}, @var{n} and ## @var{power} value, the function displays a warning message and returns NaN. ## ## @code{[@dots{}] = sampsizepwr (@dots{}, @var{n}, @var{name}, @var{value})} ## specifies one or more of the following @var{name} / @var{value} pairs: ## ## @multitable @columnfractions 0.15 0.8 ## @headitem @var{Name} @tab @var{Value} ## @item "alpha" @tab significance level of the test (default is 0.05) ## @item "tail" @tab the type of test which can be: ## @end multitable ## ## @multitable @columnfractions 0.20 0.7 ## @item "both" @tab two-sided test for an alternative @var{p1} not equal ## to @var{params} ## ## @item "right" @tab one-sided test for an alternative @var{p1} larger ## than @var{params} ## ## @item "left" @tab one-sided test for an alternative @var{p1} smaller ## than @var{params} ## @end multitable ## ## @multitable @columnfractions 0.15 0.8 ## @item "ratio" @tab desired ratio @var{n2} / @var{n2} of the larger ## sample size @var{n2} to the smaller sample size @var{n1}. Used only for the ## two-sample t-test. The value of @code{@var{ratio}} is greater than or equal ## to 1 (default is 1). ## @end multitable ## ## @code{sampsizepwr} computes the sample size, power, or alternative hypothesis ## value given values for the other two. Specify one of these as [] to compute ## it. The remaining parameters (and ALPHA, RATIO) can be scalars or arrays of ## the same size. ## ## @seealso{vartest, ttest, ttest2, ztest, binocdf} ## @end deftypefn function [out, N2] = sampsizepwr (TestType, params, p1, power, n, varargin) ## Check for valid number of input arguments narginchk (3, Inf); ## Add defaults for 3 or 4 input arguments if (nargin == 3) power = 0.90; n = []; elseif (nargin == 4) n = []; endif ## Force TestType to lower case TestType = lower (TestType); ## Check for valid test type and corresponding size of parameters t_types = {'z', 't', 't2', 'var', 'p', 'r'}; nparams = [2, 2, 2, 1, 1, 1]; if (isempty (TestType) || ! ischar (TestType) || size (TestType, 1) != 1) error ("sampsizepwr: test type must be a non-empty character vector."); endif if (sum (strcmp (TestType, t_types)) != 1) error ("sampsizepwr: invalid test type."); endif if (! isnumeric (params)) error ("sampsizepwr: parameters must be numeric."); endif if (length (params) != nparams(strcmp (TestType, t_types))) error ("sampsizepwr: invalid size of parameters for this test type."); endif ## Check for correct number of output arguments if ((nargout > 1) && ! strcmp (TestType, 't2')) error ("sampsizepwr: wrong number of output arguments for this test type."); endif Lbound = [-Inf, -Inf, -Inf, 0, 0, -1]; Ubound = [ Inf, Inf, Inf, Inf, 1, 1]; Lbound = Lbound(strcmp (TestType, t_types)); Ubound = Ubound(strcmp (TestType, t_types)); ## Check for invalid parameters specific to each test type switch (TestType) case 'z' if (params(2) <= 0) error ("sampsizepwr: negative or zero variance."); endif PowerFunction = @PowerFunction_N; case 't' if (params(2) <= 0) error ("sampsizepwr: negative or zero variance."); endif PowerFunction = @PowerFunction_T; case 't2' if (params(2) <= 0) error ("sampsizepwr: negative or zero variance."); endif PowerFunction = @PowerFunction_T2; case 'var' if (params(1) <= 0) error ("sampsizepwr: negative or zero variance."); endif PowerFunction = @PowerFunction_V; case 'p' if (params(1) <= 0 || params(1) >= 1) error ("sampsizepwr: out of range probability."); endif PowerFunction = @PowerFunction_P; case 'r' if (params(1) <= -1 || params(1) >= 1) error ("sampsizepwr: out of range regression coefficient."); endif PowerFunction = @PowerFunction_R; endswitch ## Parse optional Name-Value paired arguments optNames = {'alpha', 'tail', 'ratio'}; dfValues = {0.05, 'both', 1}; [alpha, tail, ratio, args] = parsePairedArguments (optNames, dfValues, ... varargin(:)); ## Validate optional parameters if (! isempty (args)) error ("sampsizepwr: unrecognized optional Name-Value parameter name."); endif if (numel (varargin) > 0) if (! isnumeric (alpha) || any (alpha(:) <= 0) || any (alpha(:) >= 1)) error ("sampsizepwr: invalid value for 'alpha' parameter."); endif tail = lower (tail); if (! ischar (tail) || (size (tail, 1) != 1)) error (strcat ("sampsizepwr: 'tail' parameter must", ... " be a non-empty character vector.")); endif if (! ismember (tail, {'left', 'both', 'right'})) error ("sampsizepwr: invalid value for 'tail' parameter."); endif if (! isnumeric (ratio) || any (ratio(:) < 1)) error ("sampsizepwr: invalid value for 'ratio' parameter."); endif endif ## Check that only one of either p1, power, or n are missing if (isempty (p1) + isempty (power) + isempty (n) != 1) error ("sampsizepwr: only one of either p1, power, or n must be missing."); endif ## Check for valid P1 if (! isempty (p1)) if (! isnumeric (p1)) error ("sampsizepwr: alternative hypothesis parameter must be numeric."); elseif ((! strcmp (tail, 'right') && any (p1(:) <= Lbound)) || (! strcmp (tail, 'left') && any (p1(:) >= Ubound))) error ("sampsizepwr: alternative hypothesis parameter out of range."); endif endif ## Check for valid POWER if (! isempty (power) && (! isnumeric (power) || any (power(:) <= 0) || any (power(:) >= 1))) error ("sampsizepwr: invalid value for POWER."); endif if (! isempty (power) && any (power(:) <= alpha(:))) error ("sampsizepwr: Cannot compute N or P1 unless POWER > 'alpha'."); endif ## Expand non-empty P1/POWER/N so they are all the same size if (isempty (p1)) [err, power, n, alpha, ratio] = common_size (power, n, alpha, ratio); outclass = getclass (power, n, alpha, ratio); elseif (isempty (power)) [err, p1, n, alpha, ratio] = common_size (p1, n, alpha, ratio); outclass = getclass (p1, n, alpha, ratio); else # n is empty [err, p1, power, alpha, ratio] = common_size (p1, power, alpha, ratio); outclass = getclass (power, p1, alpha, ratio); endif if (err > 0) error ("sampsizepwr: input arguments size mismatch."); endif ## Check for valid options when computing N if (isempty (n)) if (any (p1(:) == params(1))) error ("sampsizepwr: Same value for null and alternative hypothesis."); elseif (strcmp (tail, 'left') && any (p1(:) >= params(1))) error ("sampsizepwr: Invalid P1 for testing left tail."); elseif (strcmp (tail, 'right') && any (p1(:) <= params(1))) error ("sampsizepwr: Invalid P1 for testing right tail."); endif endif ## Allocate output of proper size and class out = zeros (size (alpha), outclass); ## Compute whichever one of P1/POWER/N that is now empty if (isempty (p1)) ## Compute effect size given power and sample size switch (TestType) case 'z' ## z (normal) test out(:) = findP1z (params(1), params(2), power, n, alpha, tail); case 't' ## t-test out(:) = findP1t (params(1), params(2), power, n, alpha, tail); case 't2' ## two-sample t-test out(:) = findP1t2 (params(1), params(2), power, n, alpha, tail, ratio); case 'var' ## chi-square (variance) test out(:) = findP1v (params(1), power, n, alpha, tail); case 'p' ## binomial (p) test out(:) = findP1p (params(1), power, n, alpha, tail); case 'r' ## regression coefficient (r) test out(:) = findP1r (params(1), power, n, alpha, tail); endswitch elseif (isempty (power)) ## Compute power given effect size and sample size switch (TestType) case {'z', 't'} out(:) = PowerFunction(params(1), p1, params(2), alpha, tail, n); case 't2' out(:) = PowerFunction(params(1), p1, params(2), alpha, tail, n, ratio); case {'var', 'p', 'r'} out(:) = PowerFunction(params(1), p1, alpha, tail, n); endswitch else ## Compute sample size given power and effect size switch (TestType) case {'z', 't'} ## Calculate one-sided Z value directly out(:) = z1testN (params(1), p1, params(2), power, alpha, tail); ## Iterate upward from there for the other cases if (strcmp (TestType, 't') || strcmp (tail, 'both')) if (strcmp (TestType, 't')) out = max (out, 2); endif ## Count upward until we get the value we need elem = 1:numel (alpha); while (! isempty (elem)) #MK: THE BUG WAS _T ON THE NEXT LINE! actualpower = PowerFunction(params(1), p1(elem), params(2), ... alpha(elem), tail, out(elem)); elem = elem(actualpower < power(elem)); out(elem) = out(elem) + 1; endwhile endif case 't2' ## Initialize second output argument N2 = zeros (size (alpha), outclass); ## Caculate one-sided two-sample t-test iteratively [out(:), N2(:)] = t1testN (params(1), p1, params(2), power, ... alpha, tail, ratio); case 'var' ## Use a binary search method out(:) = searchbinaryN (PowerFunction, [1, 100], params(1), ... p1, power, alpha, tail); case 'p' ## Use a binary search method out(:) = searchbinaryN (PowerFunction, [0, 100], params(1), ... p1, power, alpha, tail); ## Adjust for discrete distribution t = out <= 200; if (any (t(:))) ## Try values from 1 up to N (out) and pick the smallest value out(t) = adjdiscreteN (out(t), PowerFunction, params(1), ... p1(t), alpha(t), tail, power(t)); endif if (any (! t(:))) warning ("sampsizepwr: approximate N."); endif case 'r' ## Calculate sample size from Fisher's z transformation out(:) = rtestN (params(1), p1, power, alpha, tail); endswitch endif endfunction ## Define class for output function out = getclass (varargin) if (any (cellfun (@(x) isa (x, 'single'), varargin))) out = 'single'; else out = 'double'; endif endfunction ## Sample size calculation for the one-sided Z test function N = z1testN (mu0, mu1, sig, desiredpower, alpha, tail) ## Compute the one-sided normal value directly if (strcmp (tail, 'both')) alpha = alpha ./ 2; endif z1 = -norminv (alpha); z2 = norminv (1 - desiredpower); mudiff = abs (mu0 - mu1) / sig; N = ceil (((z1 - z2) ./ mudiff) .^ 2); endfunction ## Sample size calculation for R test function N = rtestN (r0, r1, desiredpower, alpha, tail) ## Compute only for 2-tailed test if (strcmp (tail, 'both')) alpha = alpha ./ 2; else error ("sampsizepwr: only 2-tailed testing for regression coefficient."); endif ## Get quantiles of the standard normal deviates for alpha and power Za = norminv (alpha); Zb = norminv (1 - desiredpower); ## Fisher's z transformation of each correlation. Transforming their ## difference instead is only right when the null correlation is zero, which ## happened to be the one value this test type refused to accept. C = abs (atanh (r1) - atanh (r0)); ## Compute sample size N = ceil (((Za + Zb) ./ C) .^ 2 + 3); endfunction ## Find alternative hypothesis parameter value P1 for Z test function mu1 = findP1z (mu0, sig, desiredpower, N, alpha, tail) if (strcmp (tail, 'both')) alpha = alpha ./ 2; endif sig = sig ./ sqrt (N); ## Get quantiles of the normal or t distribution if (strcmp (tail, 'left')) z1 = norminv (alpha); z2 = norminv (desiredpower); else # upper or two-tailed test z1 = norminv (1 - alpha); z2 = norminv (1 - desiredpower); endif mu1 = mu0 + sig .* (z1 - z2); ## For 2-sided test, refine by taking the other tail into account if (strcmp (tail, 'both')) elem = 1:numel (alpha); desiredbeta = 1 - desiredpower; betahi = desiredbeta; betalo = zeros (size (desiredbeta)); while (true) ## Compute probability of being below the lower critical value under H1 betalo(elem) = normcdf (-z1(elem) + (mu0 - mu1(elem)) ./ sig(elem)); ## See if the upper and lower probabilities are close enough elem = elem(abs ((betahi(elem) - betalo(elem)) - desiredbeta(elem)) > ... 1e-6 * desiredbeta(elem)); if (isempty (elem)) break endif ## Find a new mu1 by adjusting beta to take lower tail into account betahi(elem) = desiredbeta(elem) + betalo(elem); mu1(elem) = mu0 + sig(elem) .* (z1(elem) - norminv (betahi(elem))); endwhile endif endfunction ## Find alternative hypothesis parameter value P1 for t-test function mu1 = findP1t (mu0, sig, desiredpower, N, alpha, tail) if (strcmp (tail, 'both')) a2 = alpha ./ 2; else a2 = alpha; endif ## Get quantiles of the normal or t distribution if (strcmp (tail, 'left')) z1 = norminv (alpha); z2 = norminv (desiredpower); else # upper or two-tailed test z1 = norminv (1-a2); z2 = norminv (1-desiredpower); endif mu1 = mu0 + sig .* (z1-z2) ./ sqrt (N); ## Refine using fzero for j=1:numel (mu1) if (mu1(j) > mu0) F0 = @(mu1arg) PowerFunction_T (mu0, max (mu0, mu1arg), sig, alpha(j), ... tail, N(j)) - desiredpower(j); else F0 = @(mu1arg) desiredpower(j) - PowerFunction_T (mu0, min (mu0, ... mu1arg), sig, alpha(j), tail, N(j)); endif mu1(j) = fzero (F0, mu1(j)); endfor endfunction ## Sample size calculation for the one-sided two-sample t-test function [N1, N2] = t1testN (mu0, mu1, sig, desiredpower, alpha, tail, ratio) if (strcmp (tail, 'both')) alpha = alpha ./ 2; endif ## Compute the initial value of N, approximated by normal distribution z1 = -norminv (alpha); z2 = norminv (1 - desiredpower); n_0 = ceil ((z1 - z2) .^2 .* (sig ./ abs ((mu0 - mu1))) .^ 2 * 2); ## n need to be > 1, otherwise the degree of freedom of t < 0 n_0(n_0 <= 1) = 2; N = ones (size (n_0)); ## iteratively update the sample size if (strcmp (tail, 'both')) for j = 1:numel (n_0) F = @(n) nctcdf (tinv (alpha(j), n + ratio(j) .* n - 2), ... n + ratio(j) .* n - 2, abs (mu1(j) - mu0) ./ ... (sig .* sqrt (1 ./ n + 1 ./ (ratio(j) .* n)))) + ... (1 - nctcdf (- tinv (alpha(j), n + ratio(j) .* n - 2), ... n + ratio(j) .* n - 2, abs (mu1(j) - mu0) ./ ... (sig .* sqrt (1 ./ n + 1 ./ (ratio(j) .* n)))))- ... desiredpower(j); N(j) = localfzero (F, n_0(j), ratio); endfor else for j = 1:numel (n_0) F = @(n) (1 - nctcdf (- tinv (alpha(j), n + ratio(j) .* n - 2), ... n + ratio(j) .* n - 2, abs (mu1(j) - mu0) ./ (sig .* ... sqrt (1 ./ n + 1 ./ (ratio(j) .* n))))) - desiredpower(j); N(j) = localfzero (F, n_0(j), ratio); endfor endif N1 = ceil (N); N2 = ceil (ratio .* N); endfunction ## Find alternative hypothesis parameter value P1 for two-sample t-test function mu1 = findP1t2 (mu0, sig, desiredpower, N, alpha, tail, ratio) if (strcmp (tail, 'both')) a2 = alpha ./ 2; else a2 = alpha; endif ## Get quantiles of the normal or t distribution if (strcmp (tail, 'left')) t1 = tinv (alpha, N + ratio .* N - 2); t2 = tinv (desiredpower, N + ratio .* N - 2); else # upper or two-tailed test t1 = tinv (1 - a2, N + ratio .* N - 2); # upper tail under H0 t2 = tinv (1 - desiredpower, N + ratio .* N - 2); # lower tail under H1 endif mu1 = mu0 + sig .* (t1 - t2) .* sqrt (1 ./ N + 1 ./ (ratio .* N)); ## Refine using fzero for j = 1:numel (mu1) if (mu1(j) > mu0) F0 = @(mu1arg) PowerFunction_T2 (mu0, max (mu0, mu1arg), sig, ... alpha(j), tail, N(j), ratio(j)) - desiredpower(j); else F0 = @(mu1arg) desiredpower(j) - PowerFunction_T2 (mu0, min (mu0, ... mu1arg), sig, alpha(j), tail, N(j), ratio(j)); endif mu1(j) = fzero (F0, mu1(j)); endfor endfunction ## Find alternative hypothesis parameter value P1 for variance test function p1 = findP1v (p0, desiredpower, N, alpha, tail) ## F and Finv are the cdf and inverse cdf F = @(x,n,p1) chi2cdf (x .* (n - 1) ./ p1, n - 1); # cdf for s^2 Finv = @(p,n,p1) p1 .* chi2inv (p, n - 1) ./ (n - 1); # inverse if (strcmp (tail, 'both')) alpha = alpha ./ 2; endif desiredbeta = 1 - desiredpower; ## Calculate critical values and p1 for one-sided test if (! strcmp (tail, 'left')) critU = Finv(1 - alpha, N, p0); p1 = 1 ./ Finv(desiredbeta, N, 1 ./ critU); endif if (! strcmp (tail, 'right')) critL = Finv(alpha, N, p0); endif if (strcmp (tail, 'left')) p1 = 1 ./ Finv(desiredpower, N, 1 ./ critL); endif if (strcmp (tail, 'both')) ## For 2-sided test, we have the upper tail probability under H1. ## Refine by taking the other tail into account. elem = 1:numel (alpha); betahi = desiredbeta; betalo = zeros (size (desiredbeta)); while (true) ## Compute probability of being in the lower tail under H1 betalo(elem) = F(critL(elem), N(elem), p1(elem)); ## See if the upper and lower probabilities are close enough obsbeta = betahi(elem) - betalo(elem); elem = elem(abs (obsbeta - desiredbeta(elem)) > 1e-6 * desiredbeta(elem)); if (isempty (elem)) break endif ## Find a new mu1 by adjusting beta to take lower tail into account betahi(elem) = desiredbeta(elem) + betalo(elem); p1(elem) = 1 ./ Finv(betahi(elem), N(elem), 1 ./ critU(elem)); endwhile endif endfunction ## Find alternative hypothesis parameter value P1 for p test function p1 = findP1p (p0, desiredpower, N, alpha, tail) ## Get critical values [critL, critU] = getcritP (p0, N, alpha, tail); ## Use a normal approximation to find P1 values sigma = sqrt (p0 .* (1 - p0) ./ N); p1 = findP1z (p0, sigma, desiredpower, N, alpha, tail); ## Problem if we have no critical region left if (strcmp (tail, 'both')) t = (critL == 0 & critU == N); elseif (strcmp (tail, 'right')) t = (critU == N); else t = (critL == 0); endif if (any (t)) warning ("sampsizepwr: No Valid Parameter"); p1(t) = NaN; endif ## Force in bounds t = p1 <= 0; if (any (t(:))) p1(t) = p0 / 2; endif t = p1 >= 1; if (any (t(:))) p1(t) = 1 - p0 / 2; endif ## Refine using fzero for j=1:numel (p1) if (! isnan (p1(j))); if (p1(j) > p0) F0 = @(p1arg) PowerFunction_P (p0, max (p0, min (1, p1arg)), ... alpha(j), tail, N(j), critL(j), critU(j)) - desiredpower(j); else F0 = @(p1arg) desiredpower(j) - PowerFunction_P (p0, max (0, ... min (p0, p1arg)), alpha(j), tail, N(j), critL(j), critU(j)); endif p1(j) = fzero (F0, p1(j)); endif endfor endfunction ## Find alternative hypothesis parameter value P1 for r test function p1 = findP1r (p0, desiredpower, N, alpha, tail) ## Compute only for 2-tailed test if (! strcmp (tail, 'both')) error ("sampsizepwr: only 2-tailed testing for regression coefficient."); endif ## Set initial search boundaries for p1 p1_lo = eps; p1_hi = 1 - eps; ## Compute initial sample size N0 according to P0, POWER and ALPHA N0 = rtestN (p0, 0, desiredpower, alpha, tail); ## Find P0 for N0 == N while (N != N0) if (N0 < N) p1_hi = p0; p1 = (p0 + p1_lo) / 2; p0 = p1; N0 = rtestN (p1, 0, desiredpower, alpha, tail); else p1_lo = p0; p1 = (p0 + p1_hi) / 2; p0 = p1; N0 = rtestN (p1, 0, desiredpower, alpha, tail); endif endwhile endfunction ## Get upper and lower critical values for binomial (p) test. function [critL, critU] = getcritP (p0, N, alpha, tail) ## For two-sided tests, this function tries to compute critical values ## favorable for p0<.5. It does this by allocating alpha/2 to the lower ## tail where the probabilities come in larger chunks, then using any ## left-over alpha, probably more than alpha/2, for the upper tail. ## Get part of alpha available for lower tail if (strcmp (tail, 'both')) Alo = alpha ./ 2; elseif (strcmp (tail, 'left')) Alo = alpha; else Alo = 0; endif ## Calculate critical values critU = N; critL = zeros (size (N)); if (! strcmp (tail, 'right')) critL = binoinv (Alo, N, p0); Alo = binocdf (critL, N, p0); t = (critL < N) & (Alo <= alpha / 2); critL(t) = critL(t) + 1; Alo(! t) = Alo(! t) - binopdf (critL(! t), N(! t), p0); endif if (! strcmp (tail, 'left')) Aup = max (0, alpha - Alo); critU = binoinv (1 - Aup, N, p0); endif endfunction ## Sample size calculation via binary search function N = searchbinaryN (F, lohi, p0, p1, desiredpower, alpha, tail) ## Find uper and lower bounds nlo = repmat (lohi(1),size (alpha)); nhi = repmat (lohi(2),size (alpha)); obspower = F(p0,p1,alpha,tail,nhi); ## Iterate on n until we achieve the desired power elem = 1:numel (alpha); while (! isempty (elem)) elem = elem(obspower(elem) < desiredpower(elem)); nhi(elem) = nhi(elem) * 2; obspower(elem) = F(p0, p1(elem), alpha(elem), tail, nhi(elem)); endwhile ## Binary search between these bounds for required sample size elem = find (nhi > nlo+1); while (! isempty (elem)) n = floor ((nhi(elem) + nlo(elem)) / 2); obspower = F(p0, p1(elem), alpha(elem), tail, n); toohigh = (obspower > desiredpower(elem)); nhi(elem(toohigh)) = n(toohigh); nlo(elem(! toohigh)) = n(! toohigh); elem = elem(nhi(elem) > nlo(elem) + 1); endwhile N = nhi; endfunction ## Adjust sample size to take discreteness into account function N = adjdiscreteN (N, PowerFunction, p0, p1, alpha, tail, power) for j=1:numel (N) allN = 1:N(j); obspower = PowerFunction(p0, p1(j), alpha(j), tail, allN); N(j) = allN(find (obspower >= power(j), 1, 'first')); endfor endfunction ## Normal power calculation function power = PowerFunction_N (mu0, mu1, sig, alpha, tail, n) S = sig ./ sqrt (n); if (strcmp (tail, 'both')) critL = norminv (alpha / 2, mu0, S); critU = mu0 + (mu0 - critL); power = normcdf (critL, mu1, S) + normcdf (-critU, -mu1, S); elseif (strcmp (tail, 'right')) crit = mu0 + (mu0 - norminv (alpha, mu0, S)); power = normcdf (-crit, -mu1, S); else crit = norminv (alpha, mu0, S); power = normcdf (crit, mu1, S); endif endfunction ## T power calculation function power = PowerFunction_T (mu0, mu1, sig, alpha, tail, n) S = sig ./ sqrt (n); ncp = (mu1 - mu0) ./ S; if (strcmp (tail, 'both')) critL = tinv (alpha / 2, n - 1); critU = -critL; power = nctcdf (critL, n - 1, ncp) + nctcdf (-critU, n - 1, -ncp); elseif (strcmp (tail, 'right')) crit = tinv (1 - alpha, n - 1); power = nctcdf (-crit, n - 1, -ncp); else crit = tinv (alpha, n - 1); power = nctcdf (crit, n - 1, ncp); endif endfunction ## Two-sample T power calculation function power = PowerFunction_T2 (mu0, mu1, sig, alpha, tail, n, ratio) ncp = (mu1 - mu0) ./ (sig .* sqrt (1 ./ n + 1 ./ (ratio .* n))); if (strcmp (tail, 'both')) critL = tinv (alpha / 2, n + ratio .* n - 2); critU = -critL; power = nctcdf (critL, n + ratio .* n - 2, ncp) + ... nctcdf (-critU, n + ratio .* n - 2, -ncp); elseif (strcmp (tail, 'right')) crit = tinv (1 - alpha, n + ratio .* n - 2); power = nctcdf (-crit, n + ratio .* n - 2, -ncp); else crit = tinv (alpha, n + ratio .* n - 2); power = nctcdf (crit, n + ratio .* n - 2, ncp); endif endfunction ## Chi-square power calculation #MK: THESE CONFUSED POWER WITH 1-POWER! function power = PowerFunction_V (v0, v1, alpha, tail, n) if (strcmp (tail, 'both')) critU = v0 .* chi2inv (1 - alpha / 2, n - 1); critL = v0 .* chi2inv (alpha / 2, n - 1); power = chi2cdf (critL ./ v1, n - 1) + 1-chi2cdf (critU ./ v1, n - 1); elseif (strcmp (tail, 'right')) crit = v0 .* chi2inv (1 - alpha, n - 1); power = 1-chi2cdf (crit ./ v1, n - 1); else crit = v0 .* chi2inv (alpha, n - 1); power = chi2cdf (crit ./ v1, n - 1); endif endfunction ## Binomial power calculation function [power, critL, critU] = PowerFunction_P (p0, p1, alpha, ... tail, n, critL, critU) if (nargin < 6) [critL, critU] = getcritP (p0, n, alpha, tail); endif if (strcmp (tail, 'both')) power = binocdf (critL - 1, n, p1) + 1 - binocdf (critU, n, p1); elseif (strcmp (tail, 'right')) power = 1 - binocdf (critU , n, p1); else power = binocdf (critL - 1, n, p1); endif endfunction ## Regression power calculation function power = PowerFunction_R (r0, r1, alpha, tail, n) ## Compute only for 2-tailed test if (! strcmp (tail, 'both')) error ("sampsizepwr: only 2-tailed testing for regression coefficient."); endif ## Under Fisher's z transformation the statistic is asymptotically normal ## with unit variance and mean (atanh (r1) - atanh (r0)) * sqrt (n - 3), so ## the power follows in closed form. This was a bisection on the sample ## size that searched for exact equality between two doubles, using a ## variable 'N' that does not exist here -- the parameter is 'n' -- and a ## misspelt 'pd_hi' for 'dp_hi' in the branch it never reached. delta = (atanh (r1) - atanh (r0)) .* sqrt (n - 3); zcrit = - norminv (alpha ./ 2); power = normcdf (- zcrit + delta) + normcdf (- zcrit - delta); endfunction ## Local zero function for "t2" test function N = localfzero (F, N0, ratio) ## Set minN according to ratio if (ratio >= 2) minN = 1; else minN = 2; endif ## Return minN if function gives a value above zero if (F(minN) > 0) N = minN; return; endif ## Make sure that fzero does not try values below minN if (N0 == minN) N0 = N0 + 1; endif ## Find solution if (F(N0) > 0) N = fzero (F, [minN, N0], optimset ('TolX', 1e-6)); # N0 is an upper bound else N = fzero (F, N0, optimset ('TolX', 1e-6)); # N0 is a starting value endif endfunction ## Demos %!demo %! ## Compute the mean closest to 100 that can be determined to be %! ## significantly different from 100 using a t-test with a sample size %! ## of 60 and a power of 0.8. %! mu1 = sampsizepwr ('t', [100, 10], [], 0.8, 60); %! disp (mu1); %!demo %! ## Compute the sample sizes required to distinguish mu0 = 100 from %! ## mu1 = 110 by a two-sample t-test with a ratio of the larger and the %! ## smaller sample sizes of 1.5 and a power of 0.6. %! [N1,N2] = sampsizepwr ('t2', [100, 10], 110, 0.6, [], 'ratio', 1.5) %!demo %! ## Compute the sample size N required to distinguish p=.26 from p=.2 %! ## with a binomial test. The result is approximate, so make a plot to %! ## see if any smaller N values also have the required power of 0.6. %! Napprox = sampsizepwr ('p', 0.2, 0.26, 0.6); %! nn = 1:250; %! pwr = sampsizepwr ('p', 0.2, 0.26, [], nn); %! Nexact = min (nn(pwr >= 0.6)); %! plot (nn,pwr,'b-', [Napprox Nexact],pwr([Napprox Nexact]),'ro'); %! grid on %!demo %! ## The company must test 52 bottles to detect the difference between a mean %! ## volume of 100 mL and 102 mL with a power of 0.80. Generate a power curve %! ## to visualize how the sample size affects the power of the test. %! %! nout = sampsizepwr ('t',[100 5],102,0.80); %! nn = 1:100; %! pwrout = sampsizepwr ('t',[100 5],102,[],nn); %! %! figure; %! plot (nn, pwrout, 'b-', nout, 0.8, 'ro') %! title ('Power versus Sample Size') %! xlabel ('Sample Size') %! ylabel ('Power') ## Input validation %!error ... %! out = sampsizepwr ([], [100, 10], [], 0.8, 60); %!error ... %! out = sampsizepwr (3, [100, 10], [], 0.8, 60); %!error ... %! out = sampsizepwr ({'t', 't2'}, [100, 10], [], 0.8, 60); %!error ... %! out = sampsizepwr ('reg', [100, 10], [], 0.8, 60); %!error ... %! out = sampsizepwr ('t', ['a', 'e'], [], 0.8, 60); %!error ... %! out = sampsizepwr ('z', 100, [], 0.8, 60); %!error ... %! out = sampsizepwr ('t', 100, [], 0.8, 60); %!error ... %! out = sampsizepwr ('t2', 60, [], 0.8, 60); %!error ... %! out = sampsizepwr ('var', [100, 10], [], 0.8, 60); %!error ... %! out = sampsizepwr ('p', [100, 10], [], 0.8, 60); %!error ... %! out = sampsizepwr ('r', [100, 10], [], 0.8, 60); %!error ... %! [out, N1] = sampsizepwr ('z', [100, 10], [], 0.8, 60); %!error ... %! [out, N1] = sampsizepwr ('t', [100, 10], [], 0.8, 60); %!error ... %! [out, N1] = sampsizepwr ('var', 2, [], 0.8, 60); %!error ... %! [out, N1] = sampsizepwr ('p', 0.1, [], 0.8, 60); %!error ... %! [out, N1] = sampsizepwr ('r', 0.5, [], 0.8, 60); %!error ... %! out = sampsizepwr ('z', [100, 0], [], 0.8, 60); %!error ... %! out = sampsizepwr ('z', [100, -5], [], 0.8, 60); %!error ... %! out = sampsizepwr ('t', [100, 0], [], 0.8, 60); %!error ... %! out = sampsizepwr ('t', [100, -5], [], 0.8, 60); %!error ... %! [out, N1] = sampsizepwr ('t2', [100, 0], [], 0.8, 60); %!error ... %! [out, N1] = sampsizepwr ('t2', [100, -5], [], 0.8, 60); %!error ... %! out = sampsizepwr ('var', 0, [], 0.8, 60); %!error ... %! out = sampsizepwr ('var', -5, [], 0.8, 60); %!error ... %! out = sampsizepwr ('p', 0, [], 0.8, 60); %!error ... %! out = sampsizepwr ('p', 1.2, [], 0.8, 60); %!error ... %! out = sampsizepwr ('r', -1.5, [], 0.8, 60); %!error ... %! out = sampsizepwr ('r', -1, [], 0.8, 60); %!error ... %! out = sampsizepwr ('r', 1.2, [], 0.8, 60); %!error ... %! out = sampsizepwr ('r', 0.2, [], 0.8, 60, 'alpha', -0.2); %!error ... %! out = sampsizepwr ('r', 0.2, [], 0.8, 60, 'alpha', 0); %!error ... %! out = sampsizepwr ('r', 0.2, [], 0.8, 60, 'alpha', 1.5); %!error ... %! out = sampsizepwr ('r', 0.2, [], 0.8, 60, 'alpha', 'zero'); %!error ... %! out = sampsizepwr ('r', 0.2, [], 0.8, 60, 'tail', 1.5); %!error ... %! out = sampsizepwr ('r', 0.2, [], 0.8, 60, 'tail', {'both', 'left'}); %!error ... %! out = sampsizepwr ('r', 0.2, [], 0.8, 60, 'tail', 'other'); %!error ... %! out = sampsizepwr ('r', 0.2, [], 0.8, 60, 'ratio', 'some'); %!error ... %! out = sampsizepwr ('r', 0.2, [], 0.8, 60, 'ratio', 0.5); %!error ... %! out = sampsizepwr ('r', 0.2, [], 0.8, 60, 'ratio', [2, 1.3, 0.3]); %!error ... %! out = sampsizepwr ('z', [100, 5], [], [], 60); %!error ... %! out = sampsizepwr ('z', [100, 5], 110, [], []); %!error ... %! out = sampsizepwr ('z', [100, 5], [], 0.8, []); %!error ... %! out = sampsizepwr ('z', [100, 5], 110, 0.8, 60); %!error ... %! out = sampsizepwr ('z', [100, 5], 'mu', [], 60); %!error ... %! out = sampsizepwr ('var', 5, -1, [], 60); %!error ... %! out = sampsizepwr ('p', 0.8, 1.2, [], 60, 'tail', 'right'); %!error ... %! out = sampsizepwr ('r', 0.8, 1.2, [], 60); %!error ... %! out = sampsizepwr ('r', 0.8, -1.2, [], 60); %!error ... %! out = sampsizepwr ('z', [100, 5], 110, 1.2); %!error ... %! out = sampsizepwr ('z', [100, 5], 110, 0); %!error ... %! out = sampsizepwr ('z', [100, 5], 110, 0.05, [], 'alpha', 0.1); %!error ... %! out = sampsizepwr ('z', [100, 5], [], [0.8, 0.7], [60, 80, 100]); %!error ... %! out = sampsizepwr ('t', [100, 5], 100, 0.8, []); %!error ... %! out = sampsizepwr ('t', [100, 5], 110, 0.8, [], 'tail', 'left'); %!error ... %! out = sampsizepwr ('t', [100, 5], 90, 0.8, [], 'tail', 'right'); ## Warning test %!warning ... %! Napprox = sampsizepwr ('p', 0.2, 0.26, 0.6); %!warning ... %! Napprox = sampsizepwr ('p', 0.30, 0.36, 0.8); ## Results validation ## The 'r' test type was unreachable: the sample size branch called a ## misspelt 'r1testN', the power branch read an 'N' that does not exist there, ## and a null correlation of zero, the standard null, was refused outright. %!test %! ## sample size, against the Fisher z formula worked out here %! n = sampsizepwr ('r', 0.1, 0.5); %! C = abs (atanh (0.5) - atanh (0.1)); %! assert_equal (n, ceil (((norminv (0.025) + norminv (0.10)) / C) ^ 2 + 3)); %!test %! ## power at a given sample size, likewise %! pwr = sampsizepwr ('r', 0.1, 0.5, [], 60); %! d = abs (atanh (0.5) - atanh (0.1)) * sqrt (60 - 3); %! zc = - norminv (0.025); %! assert_equal (pwr, normcdf (-zc + d) + normcdf (-zc - d), 1e-12); %!test %! ## a null correlation of zero is the usual null and must be accepted %! assert_equal (sampsizepwr ('r', 0, 0.4, 0.90), 62); %! assert_equal (sampsizepwr ('r', 0, 0.3, 0.80), 85); %!test %! ## asking for a power must yield a sample size that delivers it %! for pw = [0.7, 0.8, 0.9] %! n = sampsizepwr ('r', 0, 0.4, pw); %! assert_equal (sampsizepwr ('r', 0, 0.4, [], n) >= pw, true); %! endfor %!test %! ## power must rise with the sample size and with the effect size %! p20 = sampsizepwr ('r', 0, 0.4, [], 20); %! p40 = sampsizepwr ('r', 0, 0.4, [], 40); %! p80 = sampsizepwr ('r', 0, 0.4, [], 80); %! assert_equal (p20 < p40 && p40 < p80, true); %! assert_equal (sampsizepwr ('r', 0, 0.2, [], 40) < p40, true); %! assert_equal (sampsizepwr ('r', 0, 0.6, [], 40) > p40, true); %!test %! ## a correlation and its negative need the same sample size %! assert_equal (sampsizepwr ('r', 0, -0.4, 0.90), ... %! sampsizepwr ('r', 0, 0.4, 0.90)); %!test %! ## the alternative correlation can be recovered from N and the power, %! ## to the precision of the search findP1r runs %! r1 = sampsizepwr ('r', 0, [], 0.80, 60); %! assert_equal (sampsizepwr ('r', 0, r1, [], 60), 0.80, 5e-3); %!test %! mu1 = sampsizepwr ('t', [100, 10], [], 0.8, 60); %! assert_equal (mu1, 103.67704316, 1e-8); %!test %! [N1,N2] = sampsizepwr ('t2', [100, 10], 110, 0.6, [], 'ratio', 1.5); %! assert_equal (N1, 9); %! assert_equal (N2, 14); %!test %! nn = 1:250; %! pwr = sampsizepwr ('p', 0.2, 0.26, [], nn); %! pwr_out = [0, 0.0676, 0.0176, 0.0566, 0.0181, 0.0431, 0.0802, 0.0322]; %! assert_equal (pwr([1:8]), pwr_out, 1e-4 * ones (1,8)); %! pwr_out = [0.59275, 0.6073, 0.62166, 0.6358, 0.6497, 0.6087, 0.6229, 0.6369]; %! assert_equal (pwr([243:end]), pwr_out, 1e-4 * ones (1,8)); %!test %! nout = sampsizepwr ('t', [100, 5], 102, 0.80); %! assert_equal (nout, 52); %!test %! power = sampsizepwr ('t', [20, 5], 25, [], 5, 'Tail', 'right'); %! assert_equal (power, 0.5797373588621888, 1e-14); %!test %! nout = sampsizepwr ('t', [20, 5], 25, 0.99, [], 'Tail', 'right'); %! assert_equal (nout, 18); %!test %! p1out = sampsizepwr ('t', [20, 5], [], 0.95, 10, 'Tail', 'right'); %! assert_equal (p1out, 25.65317979360237, 5e-14); %!test %! pwr = sampsizepwr ('t2', [1.4, 0.2], 1.7, [], 5, 'Ratio', 2); %! assert_equal (pwr, 0.716504004686586, 1e-14); %!test %! n = sampsizepwr ('t2', [1.4, 0.2], 1.7, 0.9, []); %! assert_equal (n, 11); %!test %! [n1, n2] = sampsizepwr ('t2', [1.4, 0.2], 1.7, 0.9, [], 'Ratio', 2); %! assert_equal ([n1, n2], [8, 16]); statistics-release-2.1.2/inst/Experimental_Design/sigma_pts.m000066400000000000000000000100271527311104300243700ustar00rootroot00000000000000## Copyright (C) 2017 - Juan Pablo Carbajal ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{pts} =} sigma_pts (@var{n}) ## @deftypefnx {statistics} {@var{pts} =} sigma_pts (@var{n}, @var{m}) ## @deftypefnx {statistics} {@var{pts} =} sigma_pts (@var{n}, @var{m}, @var{K}) ## @deftypefnx {statistics} {@var{pts} =} sigma_pts (@var{n}, @var{m}, @var{K}, @var{l}) ## ## Calculates 2*@var{n}+1 sigma points in @var{n} dimensions. ## ## Sigma points are used in the unscented transform to estimate the result of ## applying a given nonlinear transformation to a probability distribution that ## is characterized only in terms of a finite set of statistics. ## ## If only the dimension @var{n} is given the resulting points have zero mean ## and identity covariance matrix. If the mean @var{m} or the covariance matrix ## @var{K} are given, then the resulting points will have those statistics. The ## factor @var{l} scales the points away from the mean. It is useful to tune the ## accuracy of the unscented transform. ## ## There is no unique way of computing sigma points, this function implements ## the algorithm described in section 2.6 "The New Filter" pages 40-41 of ## ## Uhlmann, Jeffrey (1995). "Dynamic Map Building and Localization: New ## Theoretical Foundations". Ph.D. thesis. University of Oxford. ## ## @end deftypefn function pts = sigma_pts (n, m = [], K = [], l = 0) if isempty (K) K = eye (n); endif if isempty (m) m = zeros (1, n); endif if (n != length (m)) error ("Dimension and size of mean vector don't match.") endif if any (n != size (K)) error ("Dimension and size of covariance matrix don't match.") endif if isdefinite (K) <= 0 error ("Covariance matrix should be positive definite.") endif pts = zeros (2 * n + 1, n); pts(1,:) = m; K = sqrtm ((n + l) * K); pts(2:n+1,:) = bsxfun (@plus, m , K); pts(n+2:end,:) = bsxfun (@minus, m , K); endfunction %!demo %! K = [1 0.5; 0.5 1]; # covariance matrix %! # calculate and build associated ellipse %! [R,S,~] = svd (K); %! theta = atan2 (R(2,1), R(1,1)); %! v = sqrt (diag (S)); %! v = v .* [cos(theta) sin(theta); -sin(theta) cos(theta)]; %! t = linspace (0, 2*pi, 100).'; %! xe = v(1,1) * cos (t) + v(2,1) * sin (t); %! ye = v(1,2) * cos (t) + v(2,2) * sin (t); %! %! figure (1); clf; hold on %! # Plot ellipse and axes %! line ([0 0; v(:,1).'],[0 0; v(:,2).']) %! plot (xe,ye,'-r'); %! %! col = 'rgb'; %! l = [-1.8 -1 1.5]; %! for li = 1:3 %! p = sigma_pts (2, [], K, l(li)); %! tmp = plot (p(2:end,1), p(2:end,2), ['x' col(li)], ... %! p(1,1), p(1,2), ['o' col(li)]); %! h(li) = tmp(1); %! endfor %! hold off %! axis image %! legend (h, arrayfun (@(x) sprintf ("l:%.2g", x), l, 'unif', 0)); %!test %! p = sigma_pts (5); %! assert_equal (mean (p), zeros (1,5), sqrt (eps)); %! assert_equal (cov (p), eye (5), sqrt (eps)); %!test %! m = randn (1, 5); %! p = sigma_pts (5, m); %! assert_equal (mean (p), m, sqrt (eps)); %! assert_equal (cov (p), eye (5), sqrt (eps)); %!test %! x = linspace (0,1,5); %! K = exp (- (x.' - x).^2/ 0.5); %! p = sigma_pts (5, [], K); %! assert_equal (mean (p), zeros (1,5), sqrt (eps)); %! assert_equal (cov (p), K, sqrt (eps)); %!error sigma_pts (2,1); %!error sigma_pts (2,[],1); %!error sigma_pts (2,1,1); %!error sigma_pts (2,[0.5 0.5],[-1 0; 0 0]); statistics-release-2.1.2/inst/Experimental_Design/x2fx.m000066400000000000000000000245051527311104300232770ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{d}, @var{model}, @var{termstart}, @var{termend}] =} x2fx (@var{x}) ## @deftypefnx {statistics} {[@var{d}, @var{model}, @var{termstart}, @var{termend}] =} x2fx (@var{x}, @var{model}) ## @deftypefnx {statistics} {[@var{d}, @var{model}, @var{termstart}, @var{termend}] =} x2fx (@var{x}, @var{model}, @var{categ}) ## @deftypefnx {statistics} {[@var{d}, @var{model}, @var{termstart}, @var{termend}] =} x2fx (@var{x}, @var{model}, @var{categ}, @var{catlevels}) ## ## Convert predictors to design matrix. ## ## @code{@var{d} = x2fx (@var{x}, @var{model})} converts a matrix of predictors ## @var{x} to a design matrix @var{d} for regression analysis. Distinct ## predictor variables should appear in different columns of @var{x}. ## ## The optional input @var{model} controls the regression model. By default, ## @code{x2fx} returns the design matrix for a linear additive model with a ## constant term. @var{model} can be any one of the following strings: ## ## @multitable @columnfractions 0.2 0.75 ## @item "linear" @tab Constant and linear terms (the default) ## @item "interaction" @tab Constant, linear, and interaction terms ## @item "quadratic" @tab Constant, linear, interaction, and squared terms ## @item "purequadratic" @tab Constant, linear, and squared terms ## @end multitable ## ## If @var{x} has n columns, the order of the columns of @var{d} for a full ## quadratic model is: ## ## @itemize ## @item ## The constant term. ## @item ## The linear terms (the columns of X, in order 1,2,...,n). ## @item ## The interaction terms (pairwise products of columns of @var{x}, in order ## (1,2), (1,3), ..., (1,n), (2,3), ..., (n-1,n). ## @item ## The squared terms (in the order 1,2,...,n). ## @end itemize ## ## Other models use a subset of these terms, in the same order. ## ## Alternatively, MODEL can be a matrix specifying polynomial terms of arbitrary ## order. In this case, MODEL should have one column for each column in X and ## one r for each term in the model. The entries in any r of MODEL are powers ## for the corresponding columns of @var{x}. For example, if @var{x} has ## columns X1, X2, and X3, then a row [0 1 2] in @var{model} would specify the ## term (X1.^0).*(X2.^1).*(X3.^2). A row of all zeros in @var{model} specifies ## a constant term, which you can omit. ## ## @code{@var{d} = x2fx (@var{x}, @var{model}, @var{categ})} treats columns with ## numbers listed in the vector @var{categ} as categorical variables. Terms ## involving categorical variables produce dummy variable columns in @var{d}. ## Dummy variables are computed under the assumption that possible categorical ## levels are completely enumerated by the unique values that appear in the ## corresponding column of @var{x}. ## ## @code{@var{d} = x2fx (@var{x}, @var{model}, @var{categ}, @var{catlevels})} ## accepts a vector @var{catlevels} the same length as @var{categ}, specifying ## the number of levels in each categorical variable. In this case, values in ## the corresponding column of @var{x} must be integers in the range from 1 to ## the specified number of levels. Not all of the levels need to appear in ## @var{x}. ## ## @end deftypefn function [D, model, termstart, termend] = x2fx (x, model, categ, catlevels) ## Get matrix size [m, n] = size (x); ## Get data class if (isa (x, 'single')) data_class = 'single'; else data_class = 'double'; endif ## Check for input arguments if (nargin < 2 || isempty (model)) model = 'linear'; endif if (nargin < 3) categ = []; else if (! all (ismember (categ, 1:n))) error ("x2fx: category index exceeds number of columns in X."); endif endif if (nargin < 4) catlevels = []; endif ## Convert models parsed as strings to numerical matrix if (ischar (model)) if (strcmpi (model, 'linear') || strcmpi (model, 'additive')) interactions = false; quadratic = false; elseif (strcmpi (model, 'interaction')) interactions = true; quadratic = false; elseif (strcmpi (model, 'quadratic')) interactions = true; quadratic = true; elseif (strcmpi (model, 'purequadratic')) interactions = false; quadratic = true; else try D = feval (model, x); catch error ("x2fx: unrecognized function '%s'.", model); end_try_catch termstart = []; termend = []; return endif I = eye (n); ## Construct interactions part if (interactions && n > 1) [r, c] = find (tril (ones (n) ,-1)); nt = length (r); intpart = zeros (nt,n); intpart(sub2ind (size (intpart),(1:nt)', r)) = 1; intpart(sub2ind (size (intpart),(1:nt)', c)) = 1; else intpart = zeros (0,n); endif ## Construct quadratic part if (quadratic) quadpart = 2 * I; quadpart(categ,:) = []; else quadpart = zeros (0,n); endif model = [zeros(1,n); I]; model = [model; intpart; quadpart]; endif ## Process each categorical variable catmember = ismember (1:n, categ); var_DF = ones (1,n); if (isempty (catlevels)) ## Get values of each categorical variable and replace them with integers for idx=1:length (categ) categ_idx = categ(idx); [Y, I, J] = unique (x(:,categ_idx)); var_DF(categ_idx) = length (Y) - 1; x(:,categ_idx) = J; endfor else ## Ensure all categorical variables take valid values var_DF(categ) = catlevels - 1; for idx = 1:length (categ) categ_idx = categ(idx); if (any (! ismember (x(:,categ_idx), 1:catlevels(idx)))) error ("x2fx: wrong value %f in category %d.", ... catlevels(idx), categ_idx); endif endfor endif ## Get size of model matrix [r, c] = size (model); ## Check for equal number of columns between x and model if (c != n) error ("x2fx: wrong number of columns between X and MODEL."); endif ## Allocate space for the dummy variables for all terms termdf = prod (max (1, (model > 0) .* repmat (var_DF, r, 1)), 2); termend = cumsum (termdf); termstart = termend - termdf + 1; D = zeros (m, termend(end), data_class); allrows = (1:m)'; for idx = 1:r cols = termstart(idx):termend(idx); pwrs = model(idx,:); t = pwrs > 0; C = 1; if (any (t)) if (any (pwrs(! catmember))) pwrs_cat = pwrs .* (! catmember); C = ones (size (x, 1), 1); collist = find (pwrs_cat > 0); for j = 1:length (collist) categ_idx = collist(j); C = C .* x(:,categ_idx) .^ pwrs_cat(categ_idx); endfor endif if (any (pwrs(catmember) > 0)) Z = zeros (m, termdf(idx)); collist = find (pwrs > 0 & catmember); xcol = x(:,collist(1)); keep = (xcol <= var_DF(collist(1))); colnum = xcol; cumdf = 1; for j = 2:length (collist) cumdf = cumdf * var_DF(collist(j-1)); xcol = x(:,collist(j)); keep = keep & (xcol <= var_DF(collist(j))); colnum = colnum + cumdf * (xcol - 1); endfor if (length (C) > 1) C = C(keep); endif Z(sub2ind (size (Z),allrows(keep),colnum(keep))) = C; C = Z; endif endif D(:,cols) = C; endfor endfunction %!test %! X = [1, 10; 2, 20; 3, 10; 4, 20; 5, 15; 6, 15]; %! D = x2fx (X,'quadratic'); %! assert_equal (D(1,:), [1, 1, 10, 10, 1, 100]); %! assert_equal (D(2,:), [1, 2, 20, 40, 4, 400]); %!test %! X = [1, 10; 2, 20; 3, 10; 4, 20; 5, 15; 6, 15]; %! model = [0, 0; 1, 0; 0, 1; 1, 1; 2, 0]; %! D = x2fx (X,model); %! assert_equal (D(1,:), [1, 1, 10, 10, 1]); %! assert_equal (D(2,:), [1, 2, 20, 40, 4]); %! assert_equal (D(4,:), [1, 4, 20, 80, 16]); %!test %! x = [1, 2, 3; 2, 3, 4; 3, 4, 5]; %! D = x2fx (x, 'linear'); %! assert_equal (D, [1, 1, 2, 3; 1, 2, 3, 4;, 1, 3, 4, 5]); %! D = x2fx (x, 'interaction'); %! assert_equal (D(1,:), [1, 1, 2, 3, 2, 3, 6]); %! assert_equal (D(2,:), [1, 2, 3, 4, 6, 8, 12]); %! assert_equal (D(3,:), [1, 3, 4, 5, 12, 15, 20]); %! D = x2fx (x, 'quadratic'); %! assert_equal (D(1,:), [1, 1, 2, 3, 2, 3, 6, 1, 4, 9]); %! assert_equal (D(2,:), [1, 2, 3, 4, 6, 8, 12, 4, 9, 16]); %! assert_equal (D(3,:), [1, 3, 4, 5, 12, 15, 20, 9, 16, 25]); %! D = x2fx (x, 'purequadratic'); %! assert_equal (D(1,:), [1, 1, 2, 3, 1, 4, 9]); %! assert_equal (D(2,:), [1, 2, 3, 4, 4, 9, 16]); %! assert_equal (D(3,:), [1, 3, 4, 5, 9, 16, 25]); %!test %! x = [1, 2, 3; 2, 3, 4; 3, 4, 5]; %! D = x2fx (x, [0, 0, 1; 1, 0, 2]); %! assert_equal (D, [3, 9; 4, 32; 5, 75]); %!test %! x = [1, 2, 3; 2, 3, 4; 3, 4, 5]; %! D = x2fx (x, 'linear', [1, 3]); %! assert_equal (D, [1, 1, 0, 2, 1, 0; 1, 0, 1, 3, 0, 1; 1, 0, 0, 4, 0, 0]); %!test %! x = [1, 2, 3; 2, 3, 4; 3, 4, 5]; %! D = x2fx (x, 'quadratic', [1, 3]); %! assert_equal (D(1,:), [1, 1, 0, 2, 1, 0, 2, 0, 1, 0, 0, 0, 2, 0, 4]); %! assert_equal (D(2,:), [1, 0, 1, 3, 0, 1, 0, 3, 0, 0, 0, 1, 0, 3, 9]); %! assert_equal (D(3,:), [1, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16]); %!test %! x = [1, 2, 3; 2, 3, 4; 3, 4, 5]; %! D = x2fx (x, 'cos'); %! assert_equal (D(1,:), [0.5403, -0.4161, -0.9900], 1e-4); %! assert_equal (D(2,:), [-0.4161, -0.9900, -0.6536], 1e-4); %! assert_equal (D(3,:), [-0.9900, -0.6536, 0.2837], 1e-4); %!error ... %! x2fx ([1, 2, 3; 2, 3, 4], 'quadratic', [1, 4]) %!error ... %! D = x2fx ([1, 2, 3; 2, 3, 4; 3, 4, 5], 'cosine') %!error ... %! x2fx ([1, 10; 2, 20; 3, 10], [0; 1]); %!error ... %! x2fx ([1, 10, 15; 2, 20, 40; 3, 10, 25], [0, 0; 1, 0; 0, 1; 1, 1; 2, 0]); statistics-release-2.1.2/inst/Hypothesis_Testing/000077500000000000000000000000001527311104300221325ustar00rootroot00000000000000statistics-release-2.1.2/inst/Hypothesis_Testing/+stats/000077500000000000000000000000001527311104300233435ustar00rootroot00000000000000statistics-release-2.1.2/inst/Hypothesis_Testing/+stats/+drift/000077500000000000000000000000001527311104300245265ustar00rootroot00000000000000statistics-release-2.1.2/inst/Hypothesis_Testing/+stats/+drift/DriftDiagnostics.m000066400000000000000000000760651527311104300301620ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftp {statistics} stats.drift.DriftDiagnostics ## ## The drift found between two data sets, as @code{detectdrift} returns it. ## ## A @code{stats.drift.DriftDiagnostics} object holds, for each variable ## compared, the metric measuring how far the target data has moved from the ## baseline data, the p-value of the permutation test of that change with its ## confidence interval, and the drift status the interval gives, together with ## the drift status of the data as a whole. Every property is read-only. ## @code{detectdrift} is the documented way to create one. ## ## @code{summary} tabulates the results, @code{ecdf} and @code{histcounts} ## return the distributions compared, and @code{plotDriftStatus}, ## @code{plotEmpiricalCDF}, @code{plotHistogram} and ## @code{plotPermutationResults} draw them. ## ## @seealso{detectdrift} ## @end deftp classdef DriftDiagnostics properties (GetAccess = public, SetAccess = private) ## -*- texinfo -*- ## @deftp {stats.drift.DriftDiagnostics} {property} Baseline ## ## The baseline data, as given to @code{detectdrift}. ## ## @end deftp Baseline = []; ## -*- texinfo -*- ## @deftp {stats.drift.DriftDiagnostics} {property} ConfidenceIntervals ## ## The 95% Clopper-Pearson confidence interval of each p-value, a two-row ## matrix holding the lower bounds over the upper, one column per ## variable, @code{NaN} where p-values were not estimated. ## ## @end deftp ConfidenceIntervals = []; ## -*- texinfo -*- ## @deftp {stats.drift.DriftDiagnostics} {property} DriftStatus ## ## The drift status of each variable, a string array holding ## @qcode{"Drift"}, @qcode{"Warning"} or @qcode{"Stable"}, missing where ## p-values were not estimated. ## ## @end deftp DriftStatus = []; ## -*- texinfo -*- ## @deftp {stats.drift.DriftDiagnostics} {property} Metrics ## ## The metric each variable was measured by, a string array. ## ## @end deftp Metrics = []; ## -*- texinfo -*- ## @deftp {stats.drift.DriftDiagnostics} {property} MetricValues ## ## The metric of each variable between the baseline and the target data, ## a row vector. ## ## @end deftp MetricValues = []; ## -*- texinfo -*- ## @deftp {stats.drift.DriftDiagnostics} {property} MultipleTestCorrection ## ## The correction for testing every variable, @qcode{"Bonferroni"} or ## @qcode{"FalseDiscoveryRate"}. ## ## @end deftp MultipleTestCorrection = []; ## -*- texinfo -*- ## @deftp {stats.drift.DriftDiagnostics} {property} MultipleTestDriftStatus ## ## The drift status of the data as a whole, from the corrected p-values, ## missing where p-values were not estimated. ## ## @end deftp MultipleTestDriftStatus = []; ## -*- texinfo -*- ## @deftp {stats.drift.DriftDiagnostics} {property} NumVariables ## ## The number of variables compared. ## ## @end deftp NumVariables = 0; ## -*- texinfo -*- ## @deftp {stats.drift.DriftDiagnostics} {property} PValues ## ## The p-value of each variable, a row vector, @code{NaN} where p-values ## were not estimated. ## ## @end deftp PValues = []; ## -*- texinfo -*- ## @deftp {stats.drift.DriftDiagnostics} {property} Target ## ## The target data, as given to @code{detectdrift}. ## ## @end deftp Target = []; ## -*- texinfo -*- ## @deftp {stats.drift.DriftDiagnostics} {property} VariableNames ## ## The names of the variables compared, a string array, @qcode{"x1"}, ## @qcode{"x2"}, @dots{} where the data came as arrays. ## ## @end deftp VariableNames = []; ## -*- texinfo -*- ## @deftp {stats.drift.DriftDiagnostics} {property} CategoricalVariables ## ## The indices of the variables holding levels, among those compared. ## ## @end deftp CategoricalVariables = []; ## -*- texinfo -*- ## @deftp {stats.drift.DriftDiagnostics} {property} NumPermutations ## ## The number of permutations drawn for each variable, a row vector, 1 ## where p-values were not estimated. ## ## @end deftp NumPermutations = []; ## -*- texinfo -*- ## @deftp {stats.drift.DriftDiagnostics} {property} PermutationResults ## ## A table with one row per variable, named by it, holding in its one ## variable @code{PermutationResults} a cell with the metric over every ## permutation, the observed arrangement first. ## ## @end deftp PermutationResults = []; ## -*- texinfo -*- ## @deftp {stats.drift.DriftDiagnostics} {property} DriftThreshold ## ## The threshold below which a p-value means drift. ## ## @end deftp DriftThreshold = []; ## -*- texinfo -*- ## @deftp {stats.drift.DriftDiagnostics} {property} WarningThreshold ## ## The threshold below which a p-value means a warning. ## ## @end deftp WarningThreshold = []; endproperties properties (GetAccess = public, SetAccess = private, Hidden) Data_ = []; # the pooled data, baseline rows first, levels coded NumBaseline_ = 0; # how many rows of Data_ are the baseline's Labels_ = {}; # the name of each code of a variable holding levels Estimated_ = false; # whether p-values were estimated endproperties methods (Hidden) ## -*- texinfo -*- ## @deftypefn {stats.drift.DriftDiagnostics} {@var{obj} =} stats.drift.DriftDiagnostics (@var{S}) ## ## Create a @code{stats.drift.DriftDiagnostics} object from the structure ## @var{S} @code{detectdrift} fills, holding every property and the pooled ## data the methods read. The documented way to reach this constructor ## is @code{detectdrift}. ## ## @seealso{detectdrift} ## @end deftypefn function this = DriftDiagnostics (S) if (nargin == 0) return; endif K = numel (S.VariableNames); this.Baseline = S.Baseline; this.Target = S.Target; this.VariableNames = string (S.VariableNames(:)'); this.CategoricalVariables = S.CategoricalVariables(:)'; if (isempty (this.CategoricalVariables)) this.CategoricalVariables = []; endif this.Metrics = string (S.Metrics(:)'); this.MetricValues = S.MetricValues(:)'; this.PValues = S.PValues(:)'; this.ConfidenceIntervals = S.ConfidenceIntervals; this.NumPermutations = S.NumPermutations(:)'; this.NumVariables = K; this.PermutationResults = table (S.PermutationResults(:), ... 'VariableNames', ... {'PermutationResults'}, ... 'RowNames', S.VariableNames(:)); if (S.Estimated) this.DriftStatus = string (S.DriftStatus(:)'); this.MultipleTestDriftStatus = string (S.MultipleTestDriftStatus); else this.DriftStatus = repmat (string (missing), 1, K); this.MultipleTestDriftStatus = string (missing); endif if (strcmp (S.MultipleTestCorrection, 'bonferroni')) this.MultipleTestCorrection = string ('Bonferroni'); else this.MultipleTestCorrection = string ('FalseDiscoveryRate'); endif this.DriftThreshold = S.DriftThreshold; this.WarningThreshold = S.WarningThreshold; this.Data_ = S.Data; this.NumBaseline_ = S.NumBaseline; this.Labels_ = S.Labels; this.Estimated_ = S.Estimated; endfunction function disp (this) printf ('\n DriftDiagnostics\n\n'); printf ('%27s: %s\n', 'VariableNames', ddJoin (this.VariableNames)); printf ('%27s: %s\n', 'CategoricalVariables', ... mat2str (this.CategoricalVariables)); if (this.Estimated_) printf ('%27s: %s\n', 'DriftStatus', ddJoin (this.DriftStatus)); printf ('%27s: %s\n', 'PValues', mat2str (this.PValues, 4)); printf ('%27s: [2x%d double]\n', 'ConfidenceIntervals', ... this.NumVariables); printf ('%27s: "%s"\n', 'MultipleTestDriftStatus', ... char (this.MultipleTestDriftStatus)); printf ('%27s: %g\n', 'DriftThreshold', this.DriftThreshold); printf ('%27s: %g\n', 'WarningThreshold', this.WarningThreshold); else printf ('%27s: %s\n', 'Metrics', ddJoin (this.Metrics)); printf ('%27s: %s\n', 'MetricValues', mat2str (this.MetricValues, 4)); endif printf ('\n'); endfunction function display (this) disp (this); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {stats.drift.DriftDiagnostics} {} summary (@var{obj}) ## @deftypefnx {stats.drift.DriftDiagnostics} {@var{tbl} =} summary (@var{obj}) ## ## Tabulate the drift found. ## ## Where p-values were estimated, @code{summary} prints the drift status ## of the data as a whole and a table with one row per variable holding ## @code{DriftStatus}, @code{PValue} and @code{ConfidenceInterval}. ## @var{tbl} is that table with a last row, @qcode{MultipleTest}, holding ## the status of the data as a whole and @code{NaN} for its p-value and ## interval. Where they were not, the table holds @code{MetricValue} and ## @code{Metric}. ## ## @seealso{detectdrift} ## @end deftypefn function tbl = summary (this) names = cellstr (this.VariableNames(:)); if (! this.Estimated_) T = table (this.MetricValues(:), this.Metrics(:), ... 'VariableNames', {'MetricValue', 'Metric'}, ... 'RowNames', names); else T = table (this.DriftStatus(:), this.PValues(:), ... this.ConfidenceIntervals', 'VariableNames', ... {'DriftStatus', 'PValue', 'ConfidenceInterval'}, ... 'RowNames', names); endif if (nargout == 0) if (this.Estimated_) printf ('\n Multiple Test Correction Drift Status: %s\n\n', ... char (this.MultipleTestDriftStatus)); endif disp (T); return; endif if (this.Estimated_) T = table ([this.DriftStatus(:); this.MultipleTestDriftStatus], ... [this.PValues(:); NaN], ... [this.ConfidenceIntervals'; NaN, NaN], 'VariableNames', ... {'DriftStatus', 'PValue', 'ConfidenceInterval'}, ... 'RowNames', [names; {'MultipleTest'}]); endif tbl = T; endfunction ## -*- texinfo -*- ## @deftypefn {stats.drift.DriftDiagnostics} {@var{tbl} =} ecdf (@var{obj}) ## @deftypefnx {stats.drift.DriftDiagnostics} {@var{tbl} =} ecdf (@var{obj}, @qcode{'Variable'}, @var{var}) ## ## The empirical distribution functions of the baseline and the target. ## ## @var{tbl} holds one row per variable, named by it, and three variables ## of cells: @code{x}, the pooled values sorted with the first repeated, ## and @code{F_Baseline} and @code{F_Target}, the two distribution ## functions there, starting from 0. Where values tie, only the last copy ## takes the full value, so that the points draw the two functions as ## steps. A variable holding levels has @code{NaN} in each cell. ## @var{var} names the variables, by name or index, all of them by ## default. ## ## @seealso{stats.drift.DriftDiagnostics.plotEmpiricalCDF} ## @end deftypefn function tbl = ecdf (this, varargin) idx = ddVariables (this, varargin, 'ecdf'); n = numel (idx); xs = cell (n, 1); fb = cell (n, 1); ft = cell (n, 1); for i = 1:n j = idx(i); if (any (this.CategoricalVariables == j)) [xs{i}, fb{i}, ft{i}] = deal (NaN); else [xs{i}, fb{i}, ft{i}] = ddSteps (this, j); endif endfor tbl = table (xs, fb, ft, 'VariableNames', ... {'x', 'F_Baseline', 'F_Target'}, ... 'RowNames', cellstr (this.VariableNames(idx)(:))); endfunction ## -*- texinfo -*- ## @deftypefn {stats.drift.DriftDiagnostics} {@var{tbl} =} histcounts (@var{obj}) ## @deftypefnx {stats.drift.DriftDiagnostics} {@var{tbl} =} histcounts (@var{obj}, @qcode{'Variable'}, @var{var}) ## ## The histograms of the baseline and the target, in percent. ## ## @var{tbl} holds one row per variable, named by it, and three variables ## of cells: @code{Bins}, and @code{Counts_Baseline} and ## @code{Counts_Target}, each the percentage of its sample in each bin. ## For a continuous variable @code{Bins} holds the bin edges ## @code{histcounts} chooses for the two samples pooled. For a variable ## holding levels it holds the levels as a @code{categorical} array, and ## each count is increased by 0.5 before the percentages are taken, as the ## metrics take them. @var{var} names the variables, by name or index, ## all of them by default. ## ## @seealso{stats.drift.DriftDiagnostics.plotHistogram} ## @end deftypefn function tbl = histcounts (this, varargin) idx = ddVariables (this, varargin, 'histcounts'); n = numel (idx); bins = cell (n, 1); cb = cell (n, 1); ct = cell (n, 1); for i = 1:n [bins{i}, cb{i}, ct{i}] = ddCounts (this, idx(i)); endfor tbl = table (bins, cb, ct, 'VariableNames', ... {'Bins', 'Counts_Baseline', 'Counts_Target'}, ... 'RowNames', cellstr (this.VariableNames(idx)(:))); endfunction ## -*- texinfo -*- ## @deftypefn {stats.drift.DriftDiagnostics} {} plotDriftStatus (@var{obj}) ## @deftypefnx {stats.drift.DriftDiagnostics} {@var{h} =} plotDriftStatus (@var{obj}) ## ## Plot the p-value and confidence interval of every variable. ## ## Each variable is drawn as its p-value with its confidence interval as ## horizontal error bars, one set of bars for each drift status, the ## variables in alphabetical order up the vertical axis, with a line at ## each threshold. @var{h} holds the three sets of bars, for ## @qcode{"Stable"}, @qcode{"Warning"} and @qcode{"Drift"} in turn, each ## spanning every variable with @code{NaN} where a variable has another ## status. MATLAB draws on a categorical axis, which Octave does not have, ## so the variables sit at 1, 2, @dots{} with their names as tick labels. ## ## @seealso{stats.drift.DriftDiagnostics.summary} ## @end deftypefn function h = plotDriftStatus (this) if (! this.Estimated_) error (strcat ("stats.drift.DriftDiagnostics.plotDriftStatus:", ... " p-values were not estimated.")); endif [names, o] = sort (cellstr (this.VariableNames)); K = numel (names); p = this.PValues(o); lo = p - this.ConfidenceIntervals(1,o); hi = this.ConfidenceIntervals(2,o) - p; st = cellstr (this.DriftStatus(o)); groups = {'Stable', 'Warning', 'Drift'}; colors = ddStatusColors (); hax = newplot (); hold (hax, 'on'); hh = zeros (3, 1); for g = 1:3 x = p; x(! strcmp (st, groups{g})) = NaN; hh(g) = errorbar (hax, x, 1:K, lo, hi, '>'); set (hh(g), 'color', colors(g,:), 'linestyle', 'none', ... 'marker', 'o'); endfor xline (hax, this.WarningThreshold, ':', 'color', colors(2,:)); xline (hax, this.DriftThreshold, ':', 'color', colors(3,:)); hold (hax, 'off'); set (hax, 'xlim', [0, 1], 'xtick', 0:0.1:1, 'ylim', [0.5, K + 0.5], ... 'ytick', 1:K, 'yticklabel', names); title (hax, 'Estimated P-Values and Confidence Intervals'); xlabel (hax, 'P-Values'); ylabel (hax, 'Variable Names'); legend (hax, [groups, {'Warning Threshold', 'Drift Threshold'}]); if (nargout > 0) h = hh; endif endfunction ## -*- texinfo -*- ## @deftypefn {stats.drift.DriftDiagnostics} {} plotEmpiricalCDF (@var{obj}) ## @deftypefnx {stats.drift.DriftDiagnostics} {} plotEmpiricalCDF (@var{obj}, @qcode{'Variable'}, @var{var}) ## @deftypefnx {stats.drift.DriftDiagnostics} {@var{h} =} plotEmpiricalCDF (@dots{}) ## ## Plot the empirical distribution functions of one continuous variable. ## ## The baseline and the target are drawn as steps, from the points ## @code{ecdf} returns. @var{var} names the variable, by name or index; ## by default it is the one with the smallest p-value, or the first where ## p-values were not estimated. @var{h} holds the two lines, baseline ## first. ## ## @seealso{stats.drift.DriftDiagnostics.ecdf} ## @end deftypefn function h = plotEmpiricalCDF (this, varargin) j = ddOneVariable (this, varargin, 'plotEmpiricalCDF'); if (any (this.CategoricalVariables == j)) error (strcat ("stats.drift.DriftDiagnostics.plotEmpiricalCDF:", ... " 'Variable' must be a continuous variable.")); endif [x, fb, ft] = ddSteps (this, j); colors = ddSampleColors (); name = char (this.VariableNames(j)); hax = newplot (); hh = stairs (hax, [x, x], [fb, ft]); set (hh(1), 'color', colors(1,:)); set (hh(2), 'color', colors(2,:)); title (hax, ['ECDF for ', name]); xlabel (hax, name); ylabel (hax, 'Cumulative Probability'); legend (hax, {'Baseline', 'Target'}); if (nargout > 0) h = hh(:); endif endfunction ## -*- texinfo -*- ## @deftypefn {stats.drift.DriftDiagnostics} {} plotHistogram (@var{obj}) ## @deftypefnx {stats.drift.DriftDiagnostics} {} plotHistogram (@var{obj}, @qcode{'Variable'}, @var{var}) ## @deftypefnx {stats.drift.DriftDiagnostics} {@var{h} =} plotHistogram (@dots{}) ## ## Plot the histograms of one variable in the baseline and the target. ## ## The percentages @code{histcounts} returns are drawn as bars side by ## side, over the bins of a continuous variable or the levels of one ## holding levels. @var{var} names the variable, by name or index; by ## default it is the one with the smallest p-value, or the first where ## p-values were not estimated. @var{h} holds the two sets of bars, ## baseline first. ## ## @seealso{stats.drift.DriftDiagnostics.histcounts} ## @end deftypefn function h = plotHistogram (this, varargin) j = ddOneVariable (this, varargin, 'plotHistogram'); [bins, cb, ct] = ddCounts (this, j); colors = ddSampleColors (); name = char (this.VariableNames(j)); hax = newplot (); if (iscategorical (bins)) x = 1:numel (bins); else x = (bins(1:end-1) + bins(2:end)) / 2; endif hh = bar (hax, x, [cb(:), ct(:)]); set (hh(1), 'facecolor', colors(1,:)); set (hh(2), 'facecolor', colors(2,:)); if (iscategorical (bins)) set (hax, 'xtick', x, 'xticklabel', cellstr (bins)); endif title (hax, ['Histogram for ', name]); xlabel (hax, [name, ' Bins']); ylabel (hax, 'Distribution (%)'); legend (hax, {'Baseline', 'Target'}); if (nargout > 0) h = hh(:); endif endfunction ## -*- texinfo -*- ## @deftypefn {stats.drift.DriftDiagnostics} {} plotPermutationResults (@var{obj}) ## @deftypefnx {stats.drift.DriftDiagnostics} {} plotPermutationResults (@var{obj}, @qcode{'Variable'}, @var{var}) ## @deftypefnx {stats.drift.DriftDiagnostics} {@var{h} =} plotPermutationResults (@dots{}) ## ## Plot the metric of one variable over its permutations. ## ## The percentage of permutations in each bin is drawn as bars, split at ## the observed metric, which is marked by a line: the bars below it and ## those at or above it, whose share is the p-value. @var{var} names the ## variable, by name or index; by default it is the one with the smallest ## p-value. @var{h} holds the two sets of bars, those below first. ## MATLAB draws them as @code{histogram} objects, which Octave does not ## have. ## ## @seealso{detectdrift} ## @end deftypefn function h = plotPermutationResults (this, varargin) if (! this.Estimated_) error (strcat ("stats.drift.DriftDiagnostics.", ... "plotPermutationResults: p-values were not", ... " estimated.")); endif j = ddOneVariable (this, varargin, 'plotPermutationResults'); v = this.PermutationResults.PermutationResults{j}; obs = this.MetricValues(j); N = numel (v); ## One bin width for both sides, a whole number of bins below the ## observed metric and as many above as reach the largest value [~, e] = histcounts (v); w = e(2) - e(1); lo = min (v); hi = max (v); if (obs > lo) nb = ceil ((obs - lo) / w); w = (obs - lo) / nb; eb = linspace (lo, obs, nb + 1); else eb = []; endif ea = obs:w:hi; if (isempty (ea) || ea(end) < hi) ea(end+1) = hi; endif if (numel (ea) < 2) ea = [obs, obs + w]; endif below = []; xb = []; if (! isempty (eb)) below = 100 * histcounts (v(v < obs), eb) / N; xb = (eb(1:end-1) + eb(2:end)) / 2; endif above = 100 * histcounts (v(v >= obs), ea) / N; xa = (ea(1:end-1) + ea(2:end)) / 2; colors = ddSampleColors (); hax = newplot (); hold (hax, 'on'); hh = zeros (2, 1); if (isempty (xb)) hh(1) = bar (hax, NaN, NaN, 1, 'facecolor', colors(1,:)); else hh(1) = bar (hax, xb, below, 1, 'facecolor', colors(1,:)); endif hh(2) = bar (hax, xa, above, 1, 'facecolor', colors(2,:)); xline (hax, obs, ':', 'color', [0.15, 0.15, 0.15]); hold (hax, 'off'); metric = char (this.Metrics(j)); title (hax, ['Permutation Results for ', ... char(this.VariableNames(j))]); xlabel (hax, [metric, ' Metric Values']); ylabel (hax, 'Distribution (%)'); legend (hax, {sprintf('< %g', obs), sprintf('\\geq %g', obs)}); if (nargout > 0) h = hh; endif endfunction endmethods endclassdef ## The variable names joined for display. function s = ddJoin (names) s = ['[', strjoin(strcat ('"', cellstr (names), '"'), ' '), ']']; endfunction ## The indices of the variables a method is asked about: 'Variable' naming ## them by name or index, all of them where it is absent. function idx = ddVariables (this, args, caller) K = this.NumVariables; if (isempty (args)) idx = 1:K; return; endif if (numel (args) != 2 || ! (ischar (args{1}) || isa (args{1}, 'string')) || ! strcmpi (args{1}, 'Variable')) error (strcat ("stats.drift.DriftDiagnostics.%s: invalid optional", ... " paired argument."), caller); endif v = args{2}; names = cellstr (this.VariableNames); if (isa (v, 'string') || ischar (v) || iscellstr (v)) v = cellstr (v); [ok, idx] = ismember (v, names); if (! all (ok)) error (strcat ("stats.drift.DriftDiagnostics.%s: 'Variable' must", ... " name variables compared."), caller); endif elseif (islogical (v) && numel (v) == K) idx = find (v); elseif (isnumeric (v) && isreal (v) && all (v(:) == fix (v(:))) && all (v(:) >= 1) && all (v(:) <= K)) idx = v; else error (strcat ("stats.drift.DriftDiagnostics.%s: 'Variable' must be", ... " names or indices of variables compared."), caller); endif idx = idx(:)'; endfunction ## The staircase points of one continuous variable. function [x, fb, ft] = ddSteps (this, j) z = this.Data_(:,j); mx = this.NumBaseline_; base = [true(mx, 1); false(numel (z) - mx, 1)]; [s, o] = sort (z); b = base(o); N = numel (s); ## Within a run of ties, every copy but the last takes the count before ## the run, and the last the count at its end last = [s(1:end-1) != s(2:end); true]; first = [true; s(2:end) != s(1:end-1)]; start = cummax (first .* (1:N)'); cb = [0; cumsum(b)]; ct = [0; cumsum(! b)]; fb = cb(start); ft = ct(start); fb(last) = cb(find (last) + 1); ft(last) = ct(find (last) + 1); fb = fb / sum (b); ft = ft / sum (! b); x = [s(1); s]; fb = [0; fb]; ft = [0; ft]; endfunction ## The bins and the percentage of each sample in each, for one variable. function [bins, cb, ct] = ddCounts (this, j) z = this.Data_(:,j); mx = this.NumBaseline_; if (any (this.CategoricalVariables == j)) lab = this.Labels_{j}; L = numel (lab); nb = accumarray (z(1:mx), 1, [L, 1])' + 0.5; nt = accumarray (z(mx+1:end), 1, [L, 1])' + 0.5; bins = categorical (lab(:), lab(:)); cb = 100 * nb / sum (nb); ct = 100 * nt / sum (nt); else [~, bins] = histcounts (z); cb = 100 * histcounts (z(1:mx), bins) / mx; ct = 100 * histcounts (z(mx+1:end), bins) / (numel (z) - mx); endif endfunction ## The variable a plot is of: the one named, or the one with the smallest ## p-value, the first where p-values were not estimated. function j = ddOneVariable (this, args, caller) if (isempty (args)) if (this.Estimated_) [~, j] = min (this.PValues); else j = 1; endif return; endif j = ddVariables (this, args, caller); if (numel (j) != 1) error (strcat ("stats.drift.DriftDiagnostics.%s: 'Variable' must", ... " name one variable."), caller); endif endfunction ## The colours of the three drift statuses, Stable, Warning and Drift. function c = ddStatusColors () c = [0, 0.4470, 0.7410; 0.9290, 0.6940, 0.1250; 0.8500, 0.3250, 0.0980]; endfunction ## The colours of the baseline and the target. function c = ddSampleColors () c = [0, 0.4470, 0.7410; 0.8500, 0.3250, 0.0980]; endfunction ## Expected values from MATLAB R2024a %!shared ddE, ddD %! ddE = detectdrift ([1; 2; 2; 4], [3; 5], 'EstimatePValues', false); %! rand ('seed', 1); %! x = (1:60)' / 10; %! ddD = detectdrift (table (x, x, 'VariableNames', {'Same', 'Far'}), ... %! table (x([2:60, 1]), x + 10, ... %! 'VariableNames', {'Same', 'Far'})); %!test %! e = ecdf (ddE); %! assert_equal (e.x{1}', [1, 1, 2, 2, 3, 4, 5]); %! assert_equal (e.F_Baseline{1}', [0, 0.25, 0.25, 0.75, 0.75, 1, 1]); %! assert_equal (e.F_Target{1}', [0, 0, 0, 0, 0.5, 0.5, 1]); %!test %! h = histcounts (ddE); %! assert_equal (h.Bins{1}, 0.5:5.5); %! assert_equal (h.Counts_Baseline{1}, [25, 50, 0, 25, 0]); %! assert_equal (h.Counts_Target{1}, [0, 0, 50, 0, 50]); %!test %! ## Levels: each count increased by 0.5 %! C = detectdrift (categorical ([1; 1; 2; 3]), categorical ([1; 2; 2; 2]), ... %! 'EstimatePValues', false); %! h = histcounts (C); %! assert_equal (cellstr (h.Bins{1}), {'1'; '2'; '3'}); %! assert_equal (h.Counts_Baseline{1}, [2.5, 1.5, 1.5] * 100 / 5.5, -1e-14); %! assert_equal (C.VariableNames, string ('x1')); %! assert_equal (C.CategoricalVariables, 1); %!test %! T = table ([1; 2; 3], categorical ([1; 2; 1]), 'VariableNames', {'A', 'B'}); %! e = ecdf (detectdrift (T, T, 'EstimatePValues', false), 'Variable', 'B'); %! assert_equal (e.x{1}, NaN); %! assert_equal (e.Properties.RowNames, {'B'}); %!test %! e = ecdf (ddD, 'Variable', 2); %! assert_equal (e.Properties.RowNames, {'Far'}); %!test %! t = summary (ddE); %! assert_equal (t.Properties.VariableNames, {'MetricValue', 'Metric'}); %! assert_equal (t.MetricValue, 1.75, -1e-14); %!test %! t = summary (ddD); %! assert_equal (t.Properties.RowNames, {'Same'; 'Far'; 'MultipleTest'}); %! assert_equal (t.DriftStatus, string ({'Stable'; 'Drift'; 'Drift'})); %! assert_equal (t.PValue, [1; 0.001; NaN]); %! assert_equal (size (t.ConfidenceInterval), [3, 2]); %!assert_equal (ddD.NumVariables, 2) %!assert_equal (ddD.WarningThreshold, 0.1) %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotDriftStatus (ddD); %! assert_equal (numel (h), 3); %! ## Alphabetical, so Far comes before Same %! assert_equal (get (gca (), 'yticklabel'), {'Far'; 'Same'}); %! assert_equal (get (h(3), 'xdata')(:)', [0.001, NaN]); %! assert_equal (get (get (gca (), 'title'), 'string'), ... %! 'Estimated P-Values and Confidence Intervals'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotEmpiricalCDF (ddE); %! assert_equal (numel (h), 2); %! assert_equal (get (get (gca (), 'title'), 'string'), 'ECDF for x1'); %! assert_equal (get (get (gca (), 'ylabel'), 'string'), ... %! 'Cumulative Probability'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotHistogram (ddE); %! assert_equal (get (h(1), 'xdata')(:)', 1:5); %! assert_equal (get (h(1), 'ydata')(:)', [25, 50, 0, 25, 0]); %! assert_equal (get (get (gca (), 'xlabel'), 'string'), 'x1 Bins'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## The variable with the smallest p-value by default %! hf = figure ('visible', 'off'); %! unwind_protect %! plotHistogram (ddD); %! assert_equal (get (get (gca (), 'title'), 'string'), 'Histogram for Far'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotPermutationResults (ddD, 'Variable', 'Same'); %! assert_equal (numel (h), 2); %! assert_equal (sum (get (h(2), 'ydata')(:)), 100, -1e-12); %! assert_equal (get (get (gca (), 'xlabel'), 'string'), ... %! 'Wasserstein Metric Values'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!error ... %! ecdf (ddE, 'Variable', 'nope') %!error ... %! ecdf (ddE, 'Foo', 1) %!error ... %! histcounts (ddE, 'Variable', 3) %!error ... %! plotDriftStatus (ddE) %!error ... %! plotPermutationResults (ddE) %!error ... %! plotEmpiricalCDF (detectdrift (categorical ([1; 2]), categorical ([2; 2]), ... %! 'EstimatePValues', false)) %!error ... %! plotHistogram (ddD, 'Variable', [1, 2]) statistics-release-2.1.2/inst/Hypothesis_Testing/adtest.m000066400000000000000000001011161527311104300235740ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} adtest (@var{x}) ## @deftypefnx {statistics} {@var{h} =} adtest (@var{x}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}] =} adtest (@dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}, @var{adstat}, @var{cv}] =} adtest (@dots{}) ## ## Anderson-Darling goodness-of-fit hypothesis test. ## ## @code{@var{h} = adtest (@var{x})} returns a test decision for the null ## hypothesis that the data in vector @var{x} is from a population with a normal ## distribution, using the Anderson-Darling test. The alternative hypothesis is ## that x is not from a population with a normal distribution. The result ## @var{h} is 1 if the test rejects the null hypothesis at the 5% significance ## level, or 0 otherwise. ## ## @code{@var{h} = adtest (@var{x}, @var{Name}, @var{Value})} returns a test ## decision for the Anderson-Darling test with additional options specified by ## one or more Name-Value pair arguments. For example, you can specify a null ## distribution other than normal, or select an alternative method for ## calculating the p-value, such as a Monte Carlo simulation. ## ## The following parameters can be parsed as Name-Value pair arguments. ## ## @multitable @columnfractions 0.25 0.75 ## @headitem Name @tab Description ## @item "Distribution" @tab The distribution being tested for. It tests ## whether @var{x} could have come from the specified distribution. There are ## two choices available for parsing distribution parameters: ## @end multitable ## ## @itemize ## @item ## One of the following char strings: "norm", "exp", "ev", "logn", "weibull", ## for defining either the 'normal', 'exponential', 'extreme value', lognormal, ## or 'Weibull' distribution family, respectively. In this case, @var{x} is ## tested against a composite hypothesis for the specified distribution family ## and the required distribution parameters are estimated from the data in ## @var{x}. The default is "norm". ## ## @item ## A cell array defining a distribution in which the first cell contains a char ## string with the distribution name, as mentioned above, and the consecutive ## cells containing all specified parameters of the null distribution. In this ## case, @var{x} is tested against a simple hypothesis. ## @end itemize ## ## @multitable @columnfractions 0.25 0.75 ## @headitem @var{Name} @tab @var{Value} ## @item "Alpha" @tab Significance level alpha for the test. Any scalar ## numeric value between 0 and 1. The default is 0.05 corresponding to the 5% ## significance level. ## ## @item "MCTol" @tab Monte-Carlo standard error for the p-value, ## @var{pval}, value. which must be a positive scalar value. In this case, an ## approximation for the p-value is computed directly, using Monte-Carlo ## simulations. ## ## @item "Asymptotic" @tab Method for calculating the p-value of the ## Anderson-Darling test, which can be either true or false logical value. If ## you specify 'true', adtest estimates the p-value using the limiting ## distribution of the Anderson-Darling test statistic. If you specify 'false', ## adtest calculates the p-value based on an analytical formula. For sample ## sizes greater than 120, the limiting distribution estimate is likely to be ## more accurate than the small sample size approximation method. ## @end multitable ## ## @itemize ## @item ## If you specify a distribution family with unknown parameters for the ## distribution Name-Value pair (i.e. composite distribution hypothesis test), ## the "Asymptotic" option must be false. ## @item ## ## If you use MCTol to calculate the p-value using a Monte Carlo simulation, ## the "Asymptotic" option must be false. ## @end itemize ## ## @code{[@var{h}, @var{pval}] = adtest (@dots{})} also returns the p-value, ## @var{pval}, of the Anderson-Darling test, using any of the input arguments ## from the previous syntaxes. ## ## @code{[@var{h}, @var{pval}, @var{adstat}, @var{cv}] = adtest (@dots{})} also ## returns the test statistic, @var{adstat}, and the critical value, @var{cv}, ## for the Anderson-Darling test. ## ## The Anderson-Darling test statistic belongs to the family of Quadratic ## Empirical Distribution Function statistics, which are based on the weighted ## sum of the difference @math{[Fn(x)-F(x)]^2} over the ordered sample values ## @math{X1 < X2 < ... < Xn}, where @math{F} is the hypothesized continuous ## distribution and @math{Fn} is the empirical CDF based on the data sample with ## @math{n} sample points. ## ## @seealso{jbtest, kstest, lillietest, swtest} ## @end deftypefn function [H, pVal, ADStat, CV] = adtest (x, varargin) ## Check for valid input data if (nargin < 1) print_usage; endif ## Ensure the sample data is a real vector. if (! isvector (x) || ! isreal (x)) error ("adtest: X must be a vector of real numbers."); endif ## Add defaults distribution = 'norm'; isCompositeTest = true; alpha = 0.05; MCTol = []; asymptotic = false; ## Parse arguments and check if parameter/value pairs are valid i = 1; while (i <= length (varargin)) switch lower (varargin{i}) case 'distribution' i = i + 1; distribution = varargin{i}; ## Check for char string or cell array ## If distribution is a char string, then X is tested against a ## composite hypothesis for the specified distribution family and ## distribution parameters are estimated from the sample data. valid_dists = {'norm', 'exp', 'ev', 'logn', 'weibull'}; if (ischar (distribution)) if (! any (strcmpi (distribution, valid_dists))) error ("adtest: invalid distribution family in char string."); endif ## If distribution is a cell array, then X is tested against a ## simple hypothesis for the specified distribution and ## distribution parameters given in the cell array. elseif (iscell (distribution)) if (! any (strcmpi (distribution(1), valid_dists))) error ("adtest: invalid distribution family in cell array."); endif ## Check for valid distribution parameter in cell array err_msg = 'adtest: invalid distribution parameters in cell array.'; if (strcmpi (distribution(1), 'norm')) if (numel (distribution) != 3) error (err_msg); endif z = normcdf (x, distribution{2}, distribution{3}); elseif (strcmpi (distribution(1), 'exp')) if (numel (distribution) != 2) error (err_msg); endif z = expcdf (x, distribution{2}); elseif (strcmpi (distribution(1), 'ev')) if (numel (distribution) != 3) error (err_msg); endif z = evcdf (x, distribution{2}, distribution{3}); elseif (strcmpi (distribution(1), 'logn')) if (numel (distribution) != 3) error (err_msg); endif z = logncdf (x, distribution{2}, distribution{3}); elseif (strcmpi (distribution(1), 'weibull')) if (numel (distribution) != 3) error (err_msg); endif z = wblcdf (x, distribution{2}, distribution{3}); endif isCompositeTest = false; else error ("adtest: invalid distribution option."); endif case 'alpha' i = i + 1; alpha = varargin{i}; ## Check for valid alpha if (! isscalar (alpha) || ! isnumeric (alpha) || ... alpha <= 0 || alpha >= 1) error ("adtest: invalid value for alpha."); endif case 'mctol' i = i + 1; MCTol = varargin{i}; ## Check Monte Carlo tolerance is a numeric scalar greater than 0 if (! isempty (MCTol) && (! isscalar (MCTol) || MCTol <= 0)) error ("adtest: invalid Monte Carlo Tolerance."); endif case 'asymptotic' i = i + 1; asymptotic = varargin{i}; ## Check that it is either true or false if (! isbool (asymptotic)) error ("adtest: asymptotic option must be boolean."); endif otherwise error ("adtest: invalid Name argument."); endswitch i = i + 1; endwhile ## Check conflicts with asymptotic option if (asymptotic && isCompositeTest) error (strcat ("adtest: asymptotic option is not valid", ... " for the composite distribution test.")); elseif (asymptotic && ! isempty (MCTol)) error (strcat ("adtest: asymptotic option is not valid", ... " for the Monte Carlo simulation test.")); endif ## Remove missing values. x = x(! isnan (x)); ## Compute sample size n. n = length (x); ## For composite tests if (isCompositeTest) ## Abort for sample size less than 4 if (n < 4) error ("adtest: not enough data for composite testing."); endif ## If data follow a lognormal distribution, log(x) is normally distributed if (strcmpi (distribution, 'logn')) x = log (x); distribution = 'norm'; ## If data follow a Weibull distribution, log(x) has a type I extreme-value ## distribution elseif (strcmpi (distribution, 'weibull')) x = log (x); distribution = 'ev'; endif ## Compute ADStat switch distribution case 'norm' ## Check for complex numbers due to log (x) if (any (! isreal (x))) ## Data is not compatible with logn distribution test warning ("adtest: bad data for lognormal distribution."); ADStat = NaN; else z = normcdf (x, mean (x), std (x)); ADStat = ComputeADStat (z, n); endif case 'exp' z = expcdf (x, mean (x)); ADStat = ComputeADStat (z, n); case 'ev' ## Check for complex numbers due to log (x) if (any (! isreal (x))) ## Data is not compatible with Weibull distribution test warning ("adtest: bad data for Weibull distribution."); ADStat = NaN; else params = evfit (x); z = evcdf (x, params(1), params(2)); ADStat = ComputeADStat (z, n); endif endswitch ## Compute p-value and critical values without Monte Carlo simulation if (isempty (MCTol)) alphas = [0.0005, 0.0010, 0.0015, 0.0020, 0.0050, 0.0100, 0.0250, ... 0.0500, 0.1000, 0.1500, 0.2000, 0.2500, 0.3000, 0.3500, ... 0.4000, 0.4500, 0.5000, 0.5500, 0.6000, 0.6500, 0.7000, ... 0.7500, 0.8000, 0.8500, 0.9000, 0.9500, 0.9900]; switch distribution case 'norm' CVs = computeCriticalValues_norm (n); case 'exp' CVs = computeCriticalValues_exp (n); case 'ev' CVs = computeCriticalValues_ev (n); endswitch ## 1-D interpolation into the tabulated results pp = pchip (log (alphas), CVs); CV = ppval (pp, log (alpha)); ## If alpha is not within the lookup table, throw a warning ## Hypothesis result is computed by comparing the p-value with ## alpha, rather than CV with ADStat if alpha < alphas(1) CV = CVs(1); warning ("adtest: alpha not within the lookup table."); elseif alpha > alphas(end) CV = CVs(end); warning ("adtest: alpha not within the lookup table."); endif if (ADStat > CVs(1)) ## P value is smaller than smallest tabulated value warning (strcat ("adtest: out of range min p-value:", ... sprintf (" %g", alphas(1)))); pVal = alphas(1); elseif (ADStat < CVs(end)) ## P value is larger than largest tabulated value warning (strcat ("adtest: out of range max p-value:", ... sprintf (" %g", alphas(end)))); pVal = alphas(end); elseif (isnan (ADStat)) ## Handle certain cases of negative data (ADStat == NaN) pVal = 0; else ## Find p-value by inverse interpolation i = find (ADStat > CVs, 1, 'first'); logPVal = fzero (@(x)ppval (pp,x) - ADStat, log (alphas([i-1,i]))); pVal = exp (logPVal); endif ## Compute p-value and critical values without Monte Carlo simulation else [CV, pVal] = adtestMC (ADStat, n, alpha, distribution, mctol); endif ## Calculate H if (isnan (ADStat)) H = true; else if (isempty (MCTol)) if (alpha < alphas(1) || alpha > alphas(end)) H = (pVal < alpha); else H = (ADStat > CV); endif else H = (ADStat > CV); endif endif ## For simple tests else ## Compute the Anderson-Darling statistic ADStat = ComputeADStat (z, n); ## Compute p-value and critical values without Monte Carlo simulation if (isempty (MCTol)) alphas = [0.0005, 0.0010, 0.0015, 0.0020, 0.0050, 0.0100, 0.0250, ... 0.0500, 0.1000, 0.1500, 0.2000, 0.2500, 0.3000, 0.3500, ... 0.4000, 0.4500, 0.5000, 0.5500, 0.6000, 0.6500, 0.7000, ... 0.7500, 0.8000, 0.8500, 0.9000, 0.9500, 0.9900]; if (asymptotic) if (n <= 120) warning ("adtest: asymptotic distribution with small sample size."); endif pVal = 1 - ADInf (ADStat); ## For extra output arguments if (nargout > 3) ## Make sure alpha is within the lookup table validateAlpha (alpha, alphas); ## Find critical values critVals = findAsymptoticDistributionCriticalValues; i = find (alphas > alpha, 1, 'first'); startVal = critVals(i-1); CV = fzero (@(ad)1-ADInf (ad)-alpha, startVal); endif else if (n == 1) pVal = 1 - sqrt (1 - 4 * exp (-1 - ADStat)); else if (n < 4) warning ("adtest: small sample size."); endif pVal = 1 - ADn (n, ADStat); endif ## For extra output arguments if (nargout > 3) ## Make sure alpha is within the lookup table validateAlpha (alpha, alphas); ## Find critical values [CVs, sampleSizes] = findCriticalValues; [OneOverSampleSizes, LogAlphas] = meshgrid (1 ./ sampleSizes, ... log (alphas)); CV = interp2 (OneOverSampleSizes, LogAlphas, CVs', 1./n, log (alpha)); endif endif ## Compute p-value and critical values with Monte Carlo simulation else [CV, pVal] = adtestMC (ADStat, n, alpha, 'unif', mctol); endif ## Calculate H H = (pVal < alpha); endif endfunction ## Compute Anderson-Darling Statistic function ADStat = ComputeADStat (z, n) ## Sort the data and compute the statistic z = reshape (z, n, 1); z = sort (z); w = 2 * (1:n) - 1; ADStat = - w * (log (z)+ log (1-z(end:-1:1))) / n - n; endfunction ## Anderson-Darling distribution Pr(An= 2); x(ad >= 2) = exp (-exp (1.0776 - (2.30695 - (0.43424 - (0.082433 - ... (0.008056 - 0.0003146 .* adh) .* adh) .* adh) .* adh) .* adh)); ## Compute error function defined between 0 and 1 if (any (x < 0 | x > 1)) error ("adtest: invalid values for error function."); endif e = zeros (size (x)); c = 0.01265 + 0.1757/n; ## Define function by intervals using 3 fixed functions g1, g2, g3. xc1 = x(x < c) / c; g1 = sqrt (xc1) .* (1 - xc1) .* (49 * xc1 - 102); e(x < c) = (0.0037 / n ^ 3 + 0.00078 / n ^ 2 + 0.00006 / n) * g1; xc2 = (x(x >= c & x < 0.8) - c) ./ (0.8 - c); g2 = -0.00022633 + (6.54034 - (14.6538 - (14.458 - (8.259 -... 1.91864 .* xc2) .* xc2) .* xc2) .* xc2) .* xc2; e(x >= c & x < 0.8) = (0.04213 / n + 0.01365 / n ^ 2) * g2; xc3 = x(x >= 0.8); e(x >= 0.8) = 1 / n * (-130.2137 + (745.2337 - (1705.091 - (1950.646 -... (1116.360 - 255.7844 .* ... xc3) .* xc3) .* xc3) .* xc3) .* xc3) .* xc3; p = x + e; endfunction ## Evaluate the Anderson-Darling limit distribution function ad = ADInf (z) ## Distribution is invalid for negative values if (z < 0) error ("adtest: invalid X for asymptotic distribution."); endif ## Due to floating point precision: ## Return 0 below a certain threshold if (z < 0.02) ad = 0; return; ## Return 1 above the following threshold elseif (z >= 32.4) ad = 1; return; endif n = 1:500; K = 1/z*[1, ((4*n + 1).*cumprod((1/2 - n)./n))]; ADTerms = arrayfun (@(j)ADf (z,j),0:500); ad = ADTerms*K'; endfunction ## Series expansion for f(z,j) called by ADInf function f = ADf (z,j) ## Compute t=tj=(4j+1)^2*pi^2/(8z) t = (4 * j + 1) ^ 2 * 1.233700550136170 / z; ## First 2 terms in recursive series ## c0=pi*exp(-t)/(sqrt(2t)) ## c1=pi*sqrt(pi/2)*erfc(sqrt(t)) c0 = 2.221441469079183 * exp (-t) / sqrt (t); c1 = 3.937402486430604 * erfc (sqrt (t)); r = z / 8; f = c0 + c1 * r; ## Evaluate the recursion for n = 2:500 c = 1 / (n - 1) * ((n - 3 / 2 - t) * c1 + t * c0); r = r * (z / 8) * (1 / n); fn = f + c * r; c0 = c1; c1 = c; if (f == fn) return; endif f = fn; endfor endfunction ## An improved version of the Petitt method for the composite normal case. function CVs = computeCriticalValues_norm (n) CVs = [1.5649, 1.4407, 1.3699, 1.3187, 1.1556, 1.0339, 0.8733, ... 0.7519, 0.6308, 0.5598, 0.5092, 0.4694, 0.4366, 0.4084, ... 0.3835, 0.3611, 0.3405, 0.3212, 0.3029, 0.2852, 0.2679, ... 0.2506, 0.2330, 0.2144, 0.1935, 0.1673, 0.1296] + ... [-0.9362, -0.9029, -0.8906, -0.8865, -0.8375, -0.7835, -0.6746, ... -0.5835, -0.4775, -0.4094, -0.3679, -0.3327, -0.3099, -0.2969, ... -0.2795, -0.2623, -0.2464, -0.2325, -0.2164, -0.1994, -0.1784, ... -0.1569, -0.1377, -0.1201, -0.0989, -0.0800, -0.0598] ./ n + ... [-8.3249, -6.6022, -5.6461, -4.9685, -3.2208, -2.1647, -1.2460, ... -0.7803, -0.4627, -0.3672, -0.2833, -0.2349, -0.1442, -0.0229, ... 0.0377, 0.0817, 0.1150, 0.1583, 0.1801, 0.1887, 0.1695, ... 0.1513, 0.1533, 0.1724, 0.2027, 0.3158, 0.6431] ./ n ^ 2; endfunction ## An improved version of the Petitt method for the composite exponential case. function CVs = computeCriticalValues_exp (n) CVs = [3.2371, 2.9303, 2.7541, 2.6307, 2.2454, 1.9621, 1.5928, ... 1.3223, 1.0621, 0.9153, 0.8134, 0.7355, 0.6725, 0.6194, ... 0.5734, 0.5326, 0.4957, 0.4617, 0.4301, 0.4001, 0.3712, ... 0.3428, 0.3144, 0.2849, 0.2527, 0.2131, 0.1581] + ... [1.6146, 0.8716, 0.4715, 0.2066, -0.4682, -0.7691, -0.7388, ... -0.5758, -0.4036, -0.3142, -0.2564, -0.2152, -0.1845, -0.1607, ... -0.1409, -0.1239, -0.1084, -0.0942, -0.0807, -0.0674, -0.0537, ... -0.0401, -0.0261, -0.0116, 0.0047, 0.0275, 0.0780] ./ n; endfunction ## An improved version of the Petitt method for the composite extreme value case function CVs = computeCriticalValues_ev (n) CVs = [1.6473, 1.5095, 1.4301, 1.3742, 1.1974, 1.0667, 0.8961, ... 0.7683, 0.6416, 0.5680, 0.5156, 0.4744, 0.4405, 0.4115, ... 0.3858, 0.3626, 0.3415, 0.3217, 0.3029, 0.2848, 0.2672, ... 0.2496, 0.2315, 0.2124, 0.1909, 0.1633, 0.1223] + ... [-0.7097, -0.5934, -0.5328, -0.4930, -0.3708, -0.2973, -0.2075, ... -0.1449, -0.0892, -0.0619, -0.0442, -0.0302, -0.0196, -0.0112, ... -0.0039, 0.0024, 0.0074, 0.0122, 0.0167, 0.0207, 0.0245, ... 0.0282, 0.0323, 0.0371, 0.0436, 0.0549, 0.0813] ./ n .^ (1 / 2); endfunction ## Find rows of critical values at relevant significance levels function [CVs, sampleSizes] = findCriticalValues CVs = [7.2943, 6.6014, 6.1962, 5.9088, 4.9940, 4.3033, 3.3946, 2.7142, ... 2.0470, 1.6682, 1.4079, 1.2130, 1.0596, 0.9353, 0.8326, 0.7465, ... 0.6740, 0.6126, 0.5606, 0.5170, 0.4806, 0.4508, 0.4271, 0.4091, ... NaN, NaN, NaN; ... #n=1 7.6624, 6.4955, 5.9916, 5.6682, 4.7338, 4.0740, 3.2247, 2.5920, ... 1.9774, 1.6368, 1.4078, 1.2329, 1.0974, 0.9873, 0.8947, 0.8150, ... 0.7448, 0.6820, 0.6251, 0.5727, 0.5240, 0.4779, 0.4337, 0.3903, ... 0.3462, 0.3030, 0.2558; ... #n=2 7.2278, 6.3094, 5.8569, 5.5557, 4.6578, 4.0111, 3.1763, 2.5581, ... 1.9620, 1.6314, 1.4079, 1.2390, 1.1065, 0.9979, 0.9060, 0.8264, ... 0.7560, 0.6928, 0.6350, 0.5816, 0.5314, 0.4835, 0.4371, 0.3907, ... 0.3424, 0.2885, 0.2255; ... #n=3 7.0518, 6.2208, 5.7904, 5.4993, 4.6187, 3.9788, 3.1518, 2.5414, ... 1.9545, 1.6288, 1.4080, 1.2416, 1.1104, 1.0025, 0.9110, 0.8315, ... 0.7611, 0.6977, 0.6397, 0.5859, 0.5352, 0.4868, 0.4395, 0.3920, ... 0.3421, 0.2845, 0.2146; ... #n=4 6.9550, 6.1688, 5.7507, 5.4653, 4.5949, 3.9591, 3.1370, 2.5314, ... 1.9501, 1.6272, 1.4080, 1.2430, 1.1126, 1.0051, 0.9138, 0.8344, ... 0.7640, 0.7005, 0.6424, 0.5884, 0.5375, 0.4888, 0.4411, 0.3930, ... 0.3424, 0.2833, 0.2097; ... #n=5 6.8935, 6.1345, 5.7242, 5.4426, 4.5789, 3.9459, 3.1271, 2.5248, ... 1.9472, 1.6262, 1.4081, 1.2439, 1.1140, 1.0067, 0.9156, 0.8362, ... 0.7658, 0.7023, 0.6441, 0.5901, 0.5391, 0.4901, 0.4422, 0.3938, ... 0.3427, 0.2828, 0.2071; ... #n=6 6.8509, 6.1102, 5.7053, 5.4264, 4.5674, 3.9364, 3.1201, 2.5201, ... 1.9451, 1.6255, 1.4081, 1.2445, 1.1149, 1.0079, 0.9168, 0.8375, ... 0.7671, 0.7036, 0.6454, 0.5912, 0.5401, 0.4911, 0.4430, 0.3944, ... 0.3430, 0.2826, 0.2056; ... #n=7 6.8196, 6.0920, 5.6912, 5.4142, 4.5588, 3.9293, 3.1148, 2.5166, ... 1.9436, 1.6249, 1.4081, 1.2450, 1.1156, 1.0087, 0.9177, 0.8384, ... 0.7681, 0.7045, 0.6463, 0.5921, 0.5409, 0.4918, 0.4436, 0.3949, ... 0.3433, 0.2825, 0.2046; ... #n=8 6.7486, 6.0500, 5.6582, 5.3856, 4.5384, 3.9124, 3.1024, 2.5084, ... 1.9400, 1.6237, 1.4081, 1.2460, 1.1171, 1.0106, 0.9197, 0.8406, ... 0.7702, 0.7066, 0.6483, 0.5941, 0.5428, 0.4935, 0.4451, 0.3961, ... 0.3441, 0.2826, 0.2029; ... #n=12 6.7140, 6.0292, 5.6417, 5.3713, 4.5281, 3.9040, 3.0962, 2.5044, ... 1.9382, 1.6230, 1.4081, 1.2465, 1.1179, 1.0115, 0.9207, 0.8416, ... 0.7712, 0.7077, 0.6493, 0.5950, 0.5437, 0.4944, 0.4459, 0.3968, ... 0.3445, 0.2827, 0.2023; ... #n=16 6.6801, 6.0084, 5.6252, 5.3569, 4.5178, 3.8955, 3.0900, 2.5003, ... 1.9365, 1.6224, 1.4081, 1.2470, 1.1186, 1.0123, 0.9217, 0.8426, ... 0.7723, 0.7087, 0.6503, 0.5960, 0.5446, 0.4952, 0.4466, 0.3974, ... 0.3450, 0.2829, 0.2019; ... #n=24 6.6468, 5.9877, 5.6087, 5.3425, 4.5075, 3.8869, 3.0837, 2.4963, ... 1.9347, 1.6218, 1.4082, 1.2474, 1.1193, 1.0132, 0.9226, 0.8436, ... 0.7732, 0.7097, 0.6513, 0.5969, 0.5455, 0.4960, 0.4474, 0.3980, ... 0.3455, 0.2832, 0.2016; ... #n=48 6.6634, 5.9980, 5.6169, 5.3497, 4.5127, 3.8912, 3.0868, 2.4983, ... 1.9356, 1.6221, 1.4081, 1.2472, 1.1190, 1.0128, 0.9222, 0.8431, ... 0.7728, 0.7092, 0.6508, 0.5965, 0.5451, 0.4956, 0.4470, 0.3977, ... 0.3453, 0.2830, 0.2017; ... #n=32 6.6385, 5.9825, 5.6046, 5.3389, 4.5049, 3.8848, 3.0822, 2.4953, ... 1.9343, 1.6217, 1.4082, 1.2475, 1.1195, 1.0134, 0.9228, 0.8438, ... 0.7735, 0.7099, 0.6516, 0.5972, 0.5458, 0.4962, 0.4476, 0.3982, ... 0.3456, 0.2833, 0.2016; ... #n=64 6.6318, 5.9783, 5.6012, 5.3360, 4.5028, 3.8830, 3.0809, 2.4944, ... 1.9339, 1.6215, 1.4082, 1.2476, 1.1197, 1.0136, 0.9230, 0.8440, ... 0.7737, 0.7101, 0.6517, 0.5974, 0.5459, 0.4964, 0.4477, 0.3984, ... 0.3457, 0.2833, 0.2015; ... #n=88 6.6297, 5.9770, 5.6001, 5.3350, 4.5021, 3.8825, 3.0805, 2.4942, ... 1.9338, 1.6215, 1.4082, 1.2476, 1.1197, 1.0136, 0.9231, 0.8441, ... 0.7738, 0.7102, 0.6518, 0.5974, 0.5460, 0.4965, 0.4478, 0.3984, ... 0.3458, 0.2834, 0.2015; ... #n=100 6.6262, 5.9748, 5.5984, 5.3335, 4.5010, 3.8816, 3.0798, 2.4937, ... 1.9336, 1.6214, 1.4082, 1.2477, 1.1198, 1.0137, 0.9232, 0.8442, ... 0.7739, 0.7103, 0.6519, 0.5975, 0.5461, 0.4966, 0.4479, 0.3985, ... 0.3458, 0.2834, 0.2015; ... #n=128 6.6201, 5.9709, 5.5953, 5.3308, 4.4990, 3.8800, 3.0787, 2.4930, ... 1.9333, 1.6213, 1.4082, 1.2478, 1.1199, 1.0139, 0.9234, 0.8443, ... 0.7740, 0.7105, 0.6521, 0.5977, 0.5463, 0.4967, 0.4480, 0.3986, ... 0.3459, 0.2834, 0.2015; ... #n=256 6.6127, 5.9694, 5.5955, 5.3314, 4.4982, 3.8781, 3.0775, 2.4924, ... 1.9330, 1.6212, 1.4082, 1.2479, 1.1201, 1.0140, 0.9235, 0.8445, ... 0.7742, 0.7106, 0.6523, 0.5979, 0.5464, 0.4969, 0.4481, 0.3987, ... 0.3460, 0.2835, 0.2015]; #n=Inf sampleSizes = [1 2 3 4 5 6 7 8 12 16 24 32 48 64 88 100 128 256 Inf]; endfunction ## ------------------------------------------ function critVals = findAsymptoticDistributionCriticalValues critVals = [6.6127034546551, 5.9694013422151, 5.5954643397078, ... 5.3313658857909, 4.4981996466091, 3.8781250216054, ... 3.0774641787107, 2.4923671600494, 1.9329578327416, ... 1.6212385363175, 1.4081977005506, 1.2478596347253, ... 1.1200136586965, 1.0140004020016, 0.9235137094902, ... 0.8445069178452, 0.7742142410993, 0.7106405935247, ... 0.6522701010084, 0.5978828157471, 0.5464229310982, ... 0.4968804113119, 0.4481425895777, 0.3987228486242, ... 0.3460480234939, 0.2835161264344, 0.2014922164166]; #n=Inf endfunction ## Make sure alpha is within the lookup table function validateAlpha (alpha, alphas) if (alpha < alphas(1) || alpha > alphas(end)) error (strcat ("adtest: out of range invalid alpha -", ... sprintf (" lower limit: %g", alphas(1)),... sprintf (" upper limit: %g", alphas(end)))); endif endfunction ## Simulated critical values and p-values for Anderson-Darling test function [crit, p] = adtestMC (ADStat, n, alpha, distribution, MCTol) ## Initial values vartol = mctol^2; crit = 0; p = 0; mcRepsTot = 0; mcRepsMin = 1000; ## Monte Carlo loop while true mcRepsOld = mcRepsTot; mcReps = ceil (mcRepsMin - mcRepsOld); ADstatMC = zeros (mcReps,1); ## Switch to selected distribution switch distribution case 'norm' mu0 = 0; sigma0 = 1; for rep = 1:length (ADstatMC) x = normrnd (mu0, sigma0, n, 1); xCDF = sort (x); nullCDF = normcdf (xCDF, mean (x), std (x)); w = 2 * (1:n) - 1 ; ADstatMC(rep) = - w * (log (nullCDF) + ... log (1 - nullCDF(end:-1:1))) / n - n; endfor case 'exp' beta0 = 1; for rep = 1:length (ADstatMC) x = exprnd (beta0, n, 1); xCDF = sort (x); nullCDF = expcdf (xCDF, mean (x)); w = 2 * (1:n) - 1 ; ADstatMC(rep) = - w * (log (nullCDF) + ... log (1 - nullCDF(end:-1:1))) / n - n; endfor case 'ev' mu0 = 0; sigma0 = 1; for rep = 1:length (ADstatMC) x = evrnd (mu0, sigma0, n, 1); pHat = evfit (x); xCDF = sort (x); nullCDF = evcdf (xCDF, pHat(1), pHat(2)); w = 2 * (1:n) - 1 ; ADstatMC(rep) = - w * (log (nullCDF) + ... log (1 - nullCDF(end:-1:1))) / n - n; endfor case 'unif' for rep = 1:length (ADstatMC) z = sort (rand (n, 1)); w = 2 * (1:n) - 1 ; ADstatMC(rep) = - w * (log (z) + ... log (1 - z(end:-1:1))) / n - n; endfor endswitch critMC = prctile (ADstatMC, 100 * (1 - alpha)); pMC = sum (ADstatMC > ADStat) ./ mcReps; mcRepsTot = mcRepsOld + mcReps; crit = (mcRepsOld * crit + mcReps * critMC) / mcRepsTot; p = (mcRepsOld * p + mcReps * pMC) / mcRepsTot; ## Compute a std err for p, with lower bound (1/N)*(1-1/N)/N when p==0. sepsq = max (p * (1 - p) / mcRepsTot, 1 / mcRepsTot ^ 2); if (sepsq < MCTol ^ 2) break endif ## Based on the current estimate, find the number of trials needed to ## make the MC std err less than the specified tolerance. mcRepsMin = 1.2 * (mcRepsTot * sepsq) / (MCTol ^ 2); endwhile endfunction ## Test input validation %!error adtest (); %!error adtest (ones (20,2)); %!error adtest ([1+i,0-3i]); %!error ... %! adtest (ones (20,1), 'Distribution', 'normal'); %!error ... %! adtest (rand (20,1), 'Distribution', {'normal', 5, 3}); %!error ... %! adtest (rand (20,1), 'Distribution', {'norm', 5}); %!error ... %! adtest (rand (20,1), 'Distribution', {'exp', 5, 4}); %!error ... %! adtest (rand (20,1), 'Distribution', {'ev', 5}); %!error ... %! adtest (rand (20,1), 'Distribution', {'logn', 5, 3, 2}); %!error ... %! adtest (rand (20,1), 'Distribution', {'Weibull', 5}); %!error ... %! adtest (rand (20,1), 'Distribution', 35); %!error ... %! adtest (rand (20,1), 'Name', 'norm'); %!error ... %! adtest (rand (20,1), 'Name', {'norm', 75, 10}); %!error ... %! adtest (rand (20,1), 'Distribution', 'norm', 'Asymptotic', true); %!error ... %! adtest (rand (20,1), 'MCTol', 0.001, 'Asymptotic', true); %!error ... %! adtest (rand (20,1), 'Distribution', {'norm', 5, 3}, 'MCTol', 0.001, ... %! 'Asymptotic', true); %!error ... %! [h, pval, ADstat, CV] = adtest (ones (20,1), 'Distribution', {'norm',5,3},... %! 'Alpha', 0.000000001); %!error ... %! [h, pval, ADstat, CV] = adtest (ones (20,1), 'Distribution', {'norm',5,3},... %! 'Alpha', 0.999999999); %!error ... %! adtest (10); ## Test warnings %!warning ... %! randn ('seed', 34); %! adtest (ones (20,1), 'Alpha', 0.000001); %!warning ... %! randn ('seed', 34); %! adtest (normrnd (0,1,100,1), 'Alpha', 0.99999); %!warning ... %! randn ('seed', 34); %! adtest (normrnd (0,1,100,1), 'Alpha', 0.00001); ## Test results %!test %! load examgrades %! x = grades(:,1); %! [h, pval, adstat, cv] = adtest (x); %! assert_equal (h, false); %! assert_equal (pval, 0.1854, 1e-4); %! assert_equal (adstat, 0.5194, 1e-4); %! assert_equal (cv, 0.7470, 1e-4); %!test %! load examgrades %! x = grades(:,1); %! [h, pval, adstat, cv] = adtest (x, 'Distribution', 'ev'); %! assert_equal (h, false); %! assert_equal (pval, 0.071363, 1e-6); %!test %! load examgrades %! x = grades(:,1); %! [h, pval, adstat, cv] = adtest (x, 'Distribution', {'norm', 75, 10}); %! assert_equal (h, false); %! assert_equal (pval, 0.4687, 1e-4); statistics-release-2.1.2/inst/Hypothesis_Testing/anova.m000066400000000000000000005052441527311104300234260ustar00rootroot00000000000000## Copyright (C) 2026 Aman Behera ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef anova ## -*- texinfo -*- ## @deftp {statistics} anova ## ## Object-oriented interface for analysis of variance. ## ## The @code{anova} class provides a MATLAB-compatible object interface for ## analysis of variance. It stores factors, response data, model ## specification, and fitted results in one object. The class chooses the ## narrowest compatible backend, delegates the numeric computation to the ## existing ANOVA functions, and exposes common follow-up operations such as ## @code{stats}, @code{groupmeans}, @code{boxchart}, ## @code{plotComparisons}, @code{varianceComponent}, and ## @code{multcompare}. ## ## Models are fitted lazily. Methods that need fitted results call ## @code{fit} internally when necessary, so users may construct an object and ## immediately call inspection or post-hoc methods. ## ## @seealso{anova1, anova2, anovan, multcompare} ## @end deftp properties (GetAccess = public, SetAccess = private) ## Data ## -*- texinfo -*- ## @deftp {anova} {property} Y ## ## Response data ## ## Numeric response vector (or matrix, for the one-way column form) used to ## fit the ANOVA model. This property is read-only. ## ## @end deftp Y ## -*- texinfo -*- ## @deftp {anova} {property} Factors ## ## Factor data ## ## Table containing one variable for each factor used to fit the ANOVA ## model. This property is read-only. ## ## @end deftp Factors ## -*- texinfo -*- ## @deftp {anova} {property} Formula ## ## Model formula ## ## Read-only structural formula value with response, predictor, term, ## nesting, and linear-predictor fields matching MATLAB's formula object. ## ## @end deftp Formula = struct (); ## -*- texinfo -*- ## @deftp {anova} {property} FactorNames ## ## Factor names ## ## String row vector containing the factor names used by the fitted ANOVA ## model. This property is read-only. ## ## @end deftp FactorNames = []; ## -*- texinfo -*- ## @deftp {anova} {property} ExpandedFactorNames ## ## Coefficient names ## ## Cell array of character vectors naming the model coefficients when the ## selected backend exposes them, otherwise an empty cell array. This ## property is read-only. ## ## @end deftp ExpandedFactorNames = {}; ## -*- texinfo -*- ## @deftp {anova} {property} SumOfSquaresType ## ## Sum-of-squares type ## ## String scalar selecting @qcode{"one"}, @qcode{"two"}, ## @qcode{'three'}, or @qcode{'hierarchical'} sums of squares. This ## property is read-only. ## ## @end deftp SumOfSquaresType = 'three'; ## -*- texinfo -*- ## @deftp {anova} {property} RandomFactors ## ## Random factors ## ## Positive integer indices of random factors. This property is ## read-only. ## ## @end deftp RandomFactors = []; ## -*- texinfo -*- ## @deftp {anova} {property} CategoricalFactors ## ## Categorical factors ## ## Positive integer indices of factors treated as categorical. This ## property is read-only. ## ## @end deftp CategoricalFactors = 'all'; ## -*- texinfo -*- ## @deftp {anova} {property} ResponseName ## ## Response variable name ## ## Character vector used as the response name in formula display. ## This property is read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {anova} {property} NumObservations ## ## Number of observations ## ## Scalar number of response observations used by the model. This ## property is read-only. ## ## @end deftp NumObservations = 0; ## Results (populated after fit; empty until then) ## -*- texinfo -*- ## @deftp {anova} {property} Coefficients ## ## Model coefficient estimates ## ## Numeric vector of fitted coefficient estimates. This property is ## read-only. ## ## @end deftp Coefficients = []; ## -*- texinfo -*- ## @deftp {anova} {property} Residuals ## ## Model residuals ## ## Table with variables @code{Raw} (observed minus fitted values) and ## @code{Pearson} (raw residuals scaled by the root mean squared error) ## when the selected backend exposes residuals, otherwise empty. This ## property is read-only. ## ## @end deftp Residuals = []; ## -*- texinfo -*- ## @deftp {anova} {property} Metrics ## ## Model fit metrics ## ## Table with variables @code{MSE}, @code{RMSE}, @code{SSE}, @code{SSR}, ## @code{SST}, @code{RSquared}, and @code{AdjustedRSquared} summarising the ## fitted model. This property is read-only. ## ## @end deftp Metrics = []; endproperties properties (GetAccess = public, SetAccess = private, Hidden) ## Backend aliases and Octave-specific extensions retained for internal ## use and power users; kept off the documented MATLAB property surface. GROUP ## raw factor data (fit workhorse) Response = []; ModelType = 'linear'; ModelSpecification = 'linear'; SSType = 3; VarNames = {}; Continuous = []; Random = []; NumFactors = 0; AnovaTable = {}; FittedValues = []; DFE = []; MSE = []; DesignMatrix = []; Stats = struct (); endproperties properties (Access = private) Alpha = 0.05; Contrasts = {}; ## Accepted and validated for compatibility with the anova1 / anova2 / ## anovan calling convention, but never forwarded to a backend: results are ## presented through summary, disp, and plotDiagnostics instead. Display = 'off'; Weights = []; endproperties properties (Access = private) fitted_ = false; dirty_ = true; nFactors_ = 0; backend_ = ''; ## 'anova1' | 'anova2' | 'anovan' reps_ = []; ## replicate count for anova2 backend continuousSpecified_ = false; coefficientStats_ = []; rawResiduals_ = []; nesting_ = []; formulaText_ = ''; formulaTerms_ = []; endproperties methods (Hidden) ## Custom display of the model summary: backend, fit state, number of ## factors, sum-of-squares type and significance level. function disp (obj) obj.ensureFit_ (); constrained = 'constrained'; fprintf ("\n %d-way anova, %s (Type %s) sums of squares.\n\n", ... obj.NumFactors, constrained, obj.sstypeLabel_ ()); if (! isempty (obj.formulaText_)) fprintf (" %s\n\n", obj.formulaText_); endif obj.printAtab_ (obj.AnovaTable); fprintf ("\n Properties, Methods\n\n"); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {anova} {@var{obj} =} anova (@var{Y}) ## @deftypefnx {anova} {@var{obj} =} anova (@var{factors}, @var{Y}) ## @deftypefnx {anova} {@var{obj} =} anova (@var{tbl}, @var{Y}) ## @deftypefnx {anova} {@var{obj} =} anova (@var{tbl}, @var{responseVarName}) ## @deftypefnx {anova} {@var{obj} =} anova (@var{tbl}, @var{formula}) ## @deftypefnx {anova} {@var{obj} =} anova (@dots{}, @var{name}, @var{value}) ## ## Create an object-oriented analysis of variance model. ## ## @var{Y} is a non-empty numeric response vector or matrix. ## @var{factors} contains grouping variables for vector responses and may ## be a grouping vector, a matrix of grouping variables, or a cell array of ## grouping vectors. If @var{factors} is omitted and @var{Y} is a matrix, ## columns of @var{Y} are treated as groups following @code{anova1} matrix ## syntax. ## ## @var{tbl} is a table whose variables contain factors. The response may ## be supplied separately, selected by variable name, or specified with a ## Wilkinson formula. @qcode{'FactorNames'} can select a subset of table ## variables when a formula is not supplied. ## ## Supported name-value arguments include @qcode{'ModelSpecification'}, ## @qcode{'SumOfSquaresType'}, @qcode{'FactorNames'}, ## @qcode{'CategoricalFactors'}, @qcode{'RandomFactors'}, ## @qcode{'ResponseName'}, @qcode{'Alpha'}, and @qcode{'Display'}. ## Passing @qcode{'Reps'} selects the balanced two-way @code{anova2} ## backend when @var{Y} is a non-vector matrix. ## ## @end deftypefn function obj = anova (factors, Y, varargin) if (nargin < 1) error ("anova: too few input arguments."); endif if (nargin < 2) Y = []; endif table_factors = []; formula_input = ""; response_locked = false; if (isa (factors, "table")) if (nargin < 2) error (strcat ("anova: table input requires response data or", ... " a response name.")); endif if (mod (numel (varargin), 2) != 0) error ("anova: name-value pairs must come in pairs."); endif response_locked = obj.isName_ (Y); [factors, Y, table_factors, table_names, response_name, ... formula_input, formula_model, formula_nesting] = ... obj.parseTableInput_ (factors, Y, varargin{:}); obj.VarNames = table_names; obj.FactorNames = string (table_names); obj.ResponseName = response_name; if (! isempty (formula_input)) obj.formulaText_ = formula_input; obj.formulaTerms_ = formula_model; obj.ModelSpecification = formula_input; obj.ModelType = formula_model; obj.nesting_ = formula_nesting; endif elseif (nargin < 2) Y = factors; factors = []; elseif (isempty (Y) && isnumeric (factors)) Y = factors; factors = []; elseif (obj.isName_ (Y)) varargin = [{Y}, varargin]; Y = factors; factors = []; endif if (! isnumeric (Y) || isempty (Y)) error ("anova: Y must be a non-empty numeric array."); endif if (mod (numel (varargin), 2) != 0) error ("anova: name-value pairs must come in pairs."); endif obj.Response = Y; obj.Y = Y(:); obj.GROUP = factors; ## Parse name-value pairs (mirrors anovan.m's loop style) for idx = 1:2:numel (varargin) name = varargin{idx}; value = varargin{idx + 1}; if (! ischar (name)) error ("anova: parameter name must be a character vector."); endif switch (lower (name)) case {'model', 'modelspecification'} if (isempty (formula_input)) obj = obj.setModelSpecification_ (value); endif case {'sstype', 'sumofsquarestype'} obj = obj.setSumOfSquaresType_ (value); case {'varnames', 'factornames'} if (isempty (formula_input)) obj = obj.setFactorNames_ (value); endif case 'contrasts' obj.Contrasts = value; case 'alpha' obj.Alpha = value; case {'categoricalfactors'} obj.CategoricalFactors = value; case {'continuous'} obj.Continuous = value; obj.continuousSpecified_ = true; case {'random', 'randomfactors'} obj.RandomFactors = value; obj.Random = value; case 'weights' obj.Weights = value; case {'display', 'displayopt'} obj.Display = value; case 'responsename' if (! response_locked) obj.ResponseName = value; endif case 'reps' obj.reps_ = value; otherwise error ("anova: parameter '%s' is not supported.", name); endswitch endfor [obj.Response, obj.GROUP, obj.Weights, table_factors] = ... obj.removeMissing_ (obj.Response, obj.GROUP, obj.Weights, ... table_factors); obj.GROUP = obj.normalizeGroups_ (obj.GROUP); obj.Y = obj.Response(:); obj.nFactors_ = obj.countFactors_ (); obj.NumFactors = obj.nFactors_; obj.NumObservations = numel (obj.Response); if (isnumeric (obj.ModelType) && isscalar (obj.ModelType)) obj.ModelType = min (obj.ModelType, obj.nFactors_); endif if (isempty (formula_input)) obj = obj.setFormula_ (); endif obj.FactorNames = string (obj.VarNames); obj.SumOfSquaresType = string (obj.SumOfSquaresType); obj = obj.updateFormula_ (); obj = obj.syncFactorSelectors_ (); obj.validateSpec_ (); obj.validateData_ (); if (isempty (table_factors)) obj.Factors = obj.factorTable_ (); else obj.Factors = table_factors; endif obj = obj.selectBackend_ (); obj.dirty_ = true; ## Fit eagerly at construction so result properties are populated on the ## returned value object (value-class semantics; matches MATLAB's anova). obj = obj.ensureFit_ (); endfunction endmethods methods (Hidden) ## -*- texinfo -*- ## @deftypefn {anova} {} fit (@var{obj}) ## ## Fit the ANOVA model if it has not already been fitted. ## ## This method is optional for most workflows because methods such as ## @code{summary}, @code{multcompare}, @code{plotDiagnostics}, ## @code{predict}, and @code{getEffectSizes} call @code{fit} lazily when ## fitted results are needed. ## ## @end deftypefn function fit (obj) obj.ensureFit_ (); endfunction ## -*- texinfo -*- ## @deftypefn {anova} {} summary (@var{obj}) ## ## Display the fitted ANOVA table and basic fit statistics. ## ## The model is fitted first if needed. The printed table uses the ## backend table returned by @code{anova1}, @code{anova2}, or ## @code{anovan}, followed by the mean squared error, error degrees of ## freedom, and significance level. ## ## @end deftypefn function summary (obj) obj.ensureFit_ (); atab = obj.AnovaTable; if (isempty (atab)) fprintf (" anova: no results to display.\n"); return; endif sstype_char = obj.sstypeLabel_ (); fprintf ("\nANOVA TABLE (Type %s sums-of-squares, backend = %s):\n\n", ... sstype_char, obj.backend_); obj.printAtab_ (atab); if (! isempty (obj.MSE)) fprintf ("\nMSE: %g DFE: %g Alpha: %g\n", ... obj.MSE, obj.DFE, obj.Alpha); endif fprintf ("\n"); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {anova} {@var{s} =} stats (@var{obj}) ## @deftypefnx {anova} {@var{s} =} stats (@var{obj}, @var{type}) ## @deftypefnx {anova} {@var{s} =} stats (@var{obj}, @qcode{"Component"}, @var{sstype}) ## @deftypefnx {anova} {[@var{s}, @var{ems}] =} stats (@dots{}) ## ## Return component or summary ANOVA statistics as a table. ## ## With no @var{type}, or with @qcode{"component"}, return statistics for ## each model term, error, and total. With @qcode{"summary"}, group terms ## into linear, nonlinear, and regression rows. Replicated continuous ## designs also report lack-of-fit and pure-error statistics. ## ## The @qcode{"Component"} form computes the component table using ## @var{sstype}, which must be @qcode{"one"}, @qcode{"two"}, ## @qcode{"three"}, or @qcode{"hierarchical"}. This request does not ## change the object's read-only @code{SumOfSquaresType} property. ## The second output @var{ems} contains expected mean-square information ## for each model term and the error term. Its variables are ## @code{Type}, @code{ExpectedMeanSquares}, ## @code{MeanSquaresDenominator}, @code{DFDenominator}, and ## @code{FDenominator}. ## ## @end deftypefn function [s, ems] = stats (obj, varargin) type = "component"; sstype = []; if (numel (varargin) == 1) type = varargin{1}; elseif (numel (varargin) == 2 && obj.isName_ (varargin{1}) ... && strcmpi (varargin{1}, "component")) sstype = obj.parseSSType_ (varargin{2}); elseif (! isempty (varargin)) error ("anova.stats: invalid input arguments."); endif if (! obj.isName_ (type) ... || ! any (strcmpi (type, {"component", "summary"}))) error ("anova.stats: type must be 'component' or 'summary'."); endif if (strcmpi (type, "summary")) atab = obj.summaryStats_ (); elseif (! isempty (sstype)) atab = obj.componentStats_ (sstype); else obj.ensureFit_ (); atab = obj.AnovaTable; endif s = obj.statsTable_ (atab); if (nargout > 1) if (isempty (sstype)) sstype = obj.SSType; endif ems = obj.expectedMeanSquares_ (sstype); endif endfunction ## -*- texinfo -*- ## @deftypefn {anova} {@var{means} =} groupmeans (@var{obj}) ## @deftypefnx {anova} {@var{means} =} groupmeans (@var{obj}, @var{factors}) ## ## Return mean response estimates by factor level. ## ## The returned value is a @code{table} with one row per factor-level ## combination and columns for the level, mean, standard error, and ## confidence bounds. ## ## @end deftypefn function means = groupmeans (obj, factors, varargin) if (nargin < 2 || isempty (factors)) if (obj.NumFactors != 1) error (strcat ("anova.groupmeans: factors must be specified for", ... " a multi-factor ANOVA.")); endif factors = obj.VarNames; endif alpha = obj.parseAlpha_ (varargin{:}); obj.ensureFit_ (); idx = obj.factorIndices_ (factors); names = obj.VarNames(idx); y = obj.Y(:); if (isempty (idx)) error ("anova.groupmeans: factors are required for group means."); endif [gid, levels] = findgroups (obj.Factors(:, names)); valid = isfinite (y) & isfinite (gid) & gid > 0; used = unique (gid(valid), "stable"); [~, compact] = ismember (gid(valid), used); levels = levels(used, :); n = accumarray (compact, 1); mu = accumarray (compact, y(valid), [], @mean); se = sqrt (obj.MSE ./ n); crit = tinv (1 - alpha / 2, max (obj.DFE, 1)); lower = mu - crit * se; upper = mu + crit * se; intervals = table (mu, se, lower, upper, "VariableNames", ... {"Mean", "SE", "MeanLower", "MeanUpper"}); means = [levels, intervals]; endfunction ## -*- texinfo -*- ## @deftypefn {anova} {} boxchart (@var{obj}) ## @deftypefnx {anova} {@var{h} =} boxchart (@var{obj}, @dots{}) ## ## Plot response values grouped by up to two categorical factors. ## ## This method uses @code{boxplot} as the graphics backend in Octave and ## returns the native box graphics handles. A target axes may be supplied ## as the first optional argument. ## ## @end deftypefn function h = boxchart (obj, varargin) obj.ensureFit_ (); [target, varargin] = obj.extractAxes_ (varargin); factors = {}; if (! isempty (varargin)) candidate = varargin{1}; if (isstring (candidate)) candidate = cellstr (candidate(:))'; elseif (ischar (candidate)) candidate = {candidate}; endif if (iscellstr (candidate) ... && all (ismember (candidate, obj.VarNames))) factors = candidate; varargin(1) = []; endif endif if (isempty (factors)) if (obj.NumFactors != 1) error (strcat ("anova.boxchart: factors must be specified for", ... " a multi-factor ANOVA.")); endif factors = obj.VarNames; endif idx = obj.factorIndices_ (factors); if (numel (idx) > 2) error ("anova.boxchart: at most two factors can be plotted."); endif if (! all (ismember (idx, obj.CategoricalFactors))) error ("anova.boxchart: factors must be categorical."); endif grouping = cell (1, numel (idx)); for k = 1:numel (idx) grouping{k} = obj.Factors{:, obj.VarNames{idx(k)}}; endfor if (numel (grouping) == 1) grouping = grouping{1}; endif if (! isempty (target)) axes (target); endif [~, handles] = boxplot (obj.Y(:), grouping, "notch", "on", ... varargin{:}); h = handles.box; endfunction ## -*- texinfo -*- ## @deftypefn {anova} {} plotComparisons (@var{obj}) ## @deftypefnx {anova} {@var{h} =} plotComparisons (@var{obj}, @dots{}) ## ## Plot multiple-comparison intervals for model-adjusted group means. ## ## Clicking a group highlights it and distinguishes groups whose adjusted ## comparison is significant at the requested alpha level. A target axes ## may be supplied as the first optional argument. ## ## @end deftypefn function h = plotComparisons (obj, varargin) obj.ensureFit_ (); [target, varargin] = obj.extractAxes_ (varargin); [stats_, args, sidak] = obj.comparisonArguments_ (varargin{:}); args = obj.replaceOption_ (args, "display", "off"); alpha = obj.optionValue_ (args, "alpha", obj.Alpha); [comparisons, means, ~, names] = multcompare (stats_, args{:}); if (! isempty (sidak) && ! isempty (comparisons)) per_comparison = 1 - (1 - sidak) ^ (1 / rows (comparisons)); critical = tinv (1 - per_comparison / 2, comparisons(1, 8)); half_width = means(:, 2) * critical / sqrt (2); means(:, 3:4) = [means(:, 1) - half_width, ... means(:, 1) + half_width]; comparisons(:, 6) = 1 - (1 - comparisons(:, 6)) ... .^ rows (comparisons); endif if (isempty (target)) target = gca (); endif h = ancestor (target, "figure"); cla (target); hold (target, "on"); count = rows (means); intervals = zeros (count, 1); markers = zeros (count, 1); for group = 1:count intervals(group) = line (target, means(group, 3:4), ... [group, group], "color", [0.35, 0.35, 0.35], ... "linewidth", 1.5); markers(group) = line (target, means(group, 1), group, ... "linestyle", "none", "marker", "o", ... "markerfacecolor", [0.00, 0.45, 0.74], ... "markeredgecolor", [0.00, 0.45, 0.74]); endfor hold (target, "off"); set (target, "ydir", "reverse", "ytick", 1:count, ... "yticklabel", names); ylim (target, [0.5, count + 0.5]); xlabel (target, sprintf ("Group mean with %g%% comparison interval", ... 100 * (1 - alpha))); title (target, "Multiple comparisons"); for group = 1:count set (markers(group), "buttondownfcn", ... @(~, ~) obj.highlightComparison_ (group, markers, intervals, ... comparisons, alpha)); endfor if (count > 0) obj.highlightComparison_ (1, markers, intervals, comparisons, alpha); endif endfunction ## -*- texinfo -*- ## @deftypefn {anova} {@var{v} =} varianceComponent (@var{obj}) ## @deftypefnx {anova} {@var{v} =} varianceComponent (@var{obj}, @dots{}) ## ## Return variance component estimates for random model terms and error. ## ## @end deftypefn function v = varianceComponent (obj, varargin) alpha = obj.parseAlpha_ (varargin{:}); obj.ensureFit_ (); [coefficients, mean_squares, dfs, names] = ... obj.varianceSystem_ (obj.Stats, obj.AnovaTable, obj.SSType); estimates = coefficients \ mean_squares; lower_ms = dfs .* mean_squares ./ chi2inv (1 - alpha / 2, dfs); upper_ms = dfs .* mean_squares ./ chi2inv (alpha / 2, dfs); inverse = pinv (coefficients); lower = sum (max (inverse, 0) .* lower_ms' ... + min (inverse, 0) .* upper_ms', 2); upper = sum (max (inverse, 0) .* upper_ms' ... + min (inverse, 0) .* lower_ms', 2); ## A variance cannot be negative, so the propagated lower bound is ## truncated at zero. A negative estimate has no interval at all. lower = max (lower, 0); undefined = (estimates < 0); lower(undefined) = NaN; upper(undefined) = NaN; v = table (estimates, lower, upper, "VariableNames", ... {"VarianceComponent", "VarianceComponentLower", ... "VarianceComponentUpper"}, "RowNames", names); endfunction ## -*- texinfo -*- ## @deftypefn {anova} {@var{m} =} multcompare (@var{obj}) ## @deftypefnx {anova} {@var{m} =} multcompare (@var{obj}, @var{factors}) ## @deftypefnx {anova} {@var{m} =} multcompare (@dots{}, @var{name}, @var{value}) ## ## Perform post-hoc multiple comparisons for a fitted ANOVA object. ## ## The returned table contains the compared groups, estimated mean ## difference, confidence limits, and p-value. The default critical value ## type is @qcode{"tukey-kramer"}. ## ## @end deftypefn function m = multcompare (obj, varargin) obj.ensureFit_ (); if (isempty (fieldnames (obj.Stats))) error ("anova.multcompare: model has no stats to compare."); endif [stats_, args, sidak, dims] = obj.comparisonArguments_ (varargin{:}); gid = findgroups (obj.Factors(:, obj.VarNames(dims))); if (numel (unique (gid(isfinite (gid) & gid > 0))) < 2) m = obj.comparisonTable_ (zeros (0, 6), cell (0, 1), dims); return; endif [C, ~, ~, group_names] = multcompare (stats_, args{:}); if (! isempty (sidak)) per_comparison = 1 - (1 - sidak) ^ (1 / rows (C)); old_critical = tinv (1 - sidak / 2, C(:, 8)); standard_error = (C(:, 5) - C(:, 3)) ./ (2 * old_critical); critical = tinv (1 - per_comparison / 2, C(:, 8)); C(:, 3) = C(:, 4) - critical .* standard_error; C(:, 5) = C(:, 4) + critical .* standard_error; C(:, 6) = 1 - (1 - C(:, 6)) .^ rows (C); endif if (strcmp (stats_.source, "anovan")) group_names = obj.comparisonGroups_ (stats_, dims); endif m = obj.comparisonTable_ (C, group_names, dims); endfunction endmethods methods (Hidden) ## -*- texinfo -*- ## @deftypefn {anova} {} plotDiagnostics (@var{obj}) ## @deftypefnx {anova} {@var{h} =} plotDiagnostics (@var{obj}) ## @deftypefnx {anova} {@var{h} =} plotDiagnostics (@var{obj}, @var{name}, @var{value}) ## ## Plot residual diagnostics for an ANOVA object. ## ## The method creates a four-panel figure containing a Normal Q-Q plot, a ## Spread-Location plot, a Residual-Leverage plot, and a Cook's distance ## plot. Diagnostic plots require an @code{anovan}-backed fit because the ## fast @code{anova1} and @code{anova2} backends do not expose residuals ## and design-matrix diagnostics. ## ## Supported name-value arguments are @qcode{'FigureName'} and ## @qcode{'Visible'}. ## ## @end deftypefn function h = plotDiagnostics (obj, varargin) obj.ensureFit_ (); if (isempty (obj.rawResiduals_) || isempty (obj.FittedValues) ... || isempty (obj.DesignMatrix)) error (strcat ("anova.plotDiagnostics: diagnostic plots require", ... " an anovan-backed fit.")); endif leverage = obj.leverage_ (); if (isfield (obj.Stats, 'CooksD')) cooksd = obj.Stats.CooksD; else cooksd = obj.cooksDistance_ (leverage); endif h = obj.plotDiagnostics_ (obj.rawResiduals_, obj.FittedValues, ... leverage, cooksd, obj.DFE, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {anova} {@var{ypred} =} predict (@var{obj}) ## @deftypefnx {anova} {@var{ypred} =} predict (@var{obj}, @var{Xnew}) ## ## Predict fitted responses from an ANOVA object. ## ## With no @var{Xnew}, return fitted values for the training data. For an ## @code{anovan}-backed object, @var{Xnew} must be a numeric design matrix ## with one column per coefficient. ## ## @end deftypefn function ypred = predict (obj, Xnew, varargin) obj.ensureFit_ (); if (nargin < 2 || isempty (Xnew)) ypred = obj.FittedValues; return; endif if (isempty (obj.Coefficients)) error ("anova.predict: coefficients are unavailable for this backend."); endif if (! isempty (obj.coefficientStats_)) beta = obj.coefficientStats_(:, 1); else beta = obj.Coefficients; endif if (! isnumeric (Xnew) || columns (Xnew) != numel (beta)) error (strcat ("anova.predict: Xnew must be a numeric design", ... " matrix with %d columns."), numel (beta)); endif ypred = Xnew * beta; endfunction ## -*- texinfo -*- ## @deftypefn {anova} {@var{ES} =} getEffectSizes (@var{obj}) ## ## Return ANOVA effect-size estimates. ## ## The returned structure contains fields @code{Source}, ## @code{EtaSquared}, @code{PartialEtaSquared}, and @code{OmegaSquared}. ## Values are derived from the fitted ANOVA table. ## ## @end deftypefn function es = getEffectSizes (obj) obj.ensureFit_ (); es = obj.effectSizesFromAtab_ (); endfunction endmethods methods (Access = private) function tf = isName_ (obj, value) tf = ischar (value) || (isstring (value) && isscalar (value)); endfunction function [target, args] = extractAxes_ (obj, args) target = []; if (! isempty (args) && isscalar (args{1}) ... && ishghandle (args{1}) ... && strcmp (get (args{1}, "type"), "axes")) target = args{1}; args(1) = []; endif endfunction function highlightComparison_ (obj, selected, markers, intervals, ... comparisons, alpha) neutral = [0.45, 0.45, 0.45]; different = [0.85, 0.20, 0.16]; chosen = [0.00, 0.45, 0.74]; for group = 1:numel (markers) color = neutral; if (group == selected) color = chosen; else row = find ((comparisons(:, 1) == selected ... & comparisons(:, 2) == group) ... | (comparisons(:, 2) == selected ... & comparisons(:, 1) == group), 1); if (! isempty (row) && comparisons(row, 6) < alpha) color = different; endif endif set (markers(group), "markerfacecolor", color, ... "markeredgecolor", color); set (intervals(group), "color", color); endfor endfunction function [groups, response, factor_table, factor_names, response_name, ... formula, model, nesting] = parseTableInput_ (obj, tbl, ... response_arg, ... varargin) variable_names = tbl.Properties.VariableNames; formula = ""; model = []; nesting = []; if (isnumeric (response_arg)) response = response_arg; response_name = "Y"; factor_names = variable_names; elseif (obj.isName_ (response_arg)) value = char (response_arg); if (! isempty (strfind (value, "~"))) formula = value; schema = parseWilkinsonFormula (formula, "matrix"); if (! isscalar (schema.ResponseIdx)) error ("anova: formula must specify one response variable."); endif response_name = schema.VariableNames{schema.ResponseIdx}; [factor_names, model] = obj.schemaModel_ (schema); ordered = variable_names(ismember (variable_names, factor_names)); [~, order] = ismember (ordered, factor_names); factor_names = ordered; model = model(:, order); [model, nesting] = obj.nestedSpecification_ (formula, ... factor_names, model); else response_name = value; factor_names = variable_names(! strcmp (variable_names, value)); endif if (! any (strcmp (variable_names, response_name))) error ("anova: response variable '%s' is not in the table.", ... response_name); endif response = tbl{:, response_name}; else error (strcat ("anova: table response must be numeric, a response", ... " variable name, or a formula.")); endif if (! isempty (formula)) missing = ! ismember (factor_names, variable_names); if (any (missing)) error ("anova: formula variable '%s' is not in the table.", ... factor_names{find (missing, 1)}); endif else selected = obj.optionValue_ (varargin, "factornames", []); if (! isempty (selected)) if (isstring (selected)) selected = cellstr (selected(:))'; elseif (ischar (selected)) selected = {selected}; endif if (! iscellstr (selected) ... || ! all (ismember (selected, factor_names))) error (strcat ("anova: FactorNames must identify factor", ... " variables in the table.")); endif factor_names = selected; endif endif if (! isnumeric (response) || isempty (response) || ! isvector (response)) error (strcat ("anova: table response data must be a non-empty", ... " numeric vector.")); endif if (numel (response) != height (tbl)) error (strcat ("anova: table and response must contain the same", ... " number of rows.")); endif factor_table = tbl(:, factor_names); groups = cell (1, numel (factor_names)); for k = 1:numel (factor_names) groups{k} = tbl{:, factor_names{k}}; if (ischar (groups{k}) && rows (groups{k}) > 1) groups{k} = cellstr (groups{k}); endif endfor if (numel (groups) == 1) groups = groups{1}; endif endfunction function [factor_names, model] = schemaModel_ (obj, schema) predictor_idx = setdiff (1:numel (schema.VariableNames), ... schema.ResponseIdx, "stable"); used = any (schema.Terms(:, predictor_idx) != 0, 1); predictor_idx = predictor_idx(used); factor_names = {}; exponents = zeros (1, numel (predictor_idx)); factor_idx = zeros (1, numel (predictor_idx)); for k = 1:numel (predictor_idx) name = schema.VariableNames{predictor_idx(k)}; token = regexp (name, '^(.*)\^(\d+)$', 'tokens', 'once'); if (isempty (token)) base = name; exponents(k) = 1; else base = token{1}; exponents(k) = str2double (token{2}); endif idx = find (strcmp (factor_names, base), 1); if (isempty (idx)) factor_names{end + 1} = base; idx = numel (factor_names); endif factor_idx(k) = idx; endfor model = zeros (rows (schema.Terms), numel (factor_names)); for k = 1:numel (predictor_idx) rows_ = schema.Terms(:, predictor_idx(k)) != 0; model(rows_, factor_idx(k)) = exponents(k); endfor model(! any (model, 2), :) = []; model = unique (model, "rows", "stable"); endfunction function [model, nesting] = nestedSpecification_ (obj, formula, ... factor_names, model) nesting = false (numel (factor_names)); definitions = regexp (formula, ... '([A-Za-z]\w*)\s*\(([^()]*)\)', ... 'tokens'); for k = 1:numel (definitions) child_name = definitions{k}{1}; parent_names = strtrim (strsplit (definitions{k}{2}, ',')); child = find (strcmp (factor_names, child_name), 1); [found, parents] = ismember (parent_names, factor_names); if (isempty (child) || ! all (found)) error ("anova: nested formula contains an unknown factor."); endif nesting(child, parents) = true; members = [child, parents]; rows_ = all (model(:, members) > 0, 2) ... & sum (model > 0, 2) == numel (members); replacement = zeros (1, columns (model)); replacement(child) = 1; model(rows_, :) = repmat (replacement, sum (rows_), 1); endfor model = unique (model, "rows", "stable"); endfunction function value = optionValue_ (obj, args, option, default) value = default; for k = 1:2:numel (args) if (obj.isName_ (args{k}) && strcmpi (char (args{k}), option)) value = args{k + 1}; endif endfor endfunction function factors = factorTable_ (obj) if (obj.nFactors_ == 0) factors = table (); return; endif if (isempty (obj.GROUP) && ! isvector (obj.Response)) if (isempty (obj.reps_)) [n, groups] = size (obj.Response); group_values = reshape (repmat (1:groups, n, 1), [], 1); columns_ = {group_values}; else [~, columns_] = obj.anova2Data_ (); endif elseif (iscell (obj.GROUP) ... && size (obj.GROUP, 1) == 1 ... && numel (obj.GROUP) == obj.nFactors_) columns_ = cellfun (@(x) x(:), obj.GROUP, "UniformOutput", false); elseif (obj.nFactors_ == 1) columns_ = {obj.GROUP(:)}; else columns_ = cell (1, obj.nFactors_); for k = 1:obj.nFactors_ columns_{k} = obj.GROUP(:, k); endfor endif factors = table (columns_{:}, "VariableNames", obj.VarNames); endfunction function [response, groups, weights, factor_table] = removeMissing_ (... obj, response, groups, weights, ... factor_table) if (! isvector (response)) if (! isempty (obj.reps_)) return; elseif (isempty (groups) && any (! isfinite (response(:)))) [n, levels] = size (response); groups = reshape (repmat (1:levels, n, 1), [], 1); response = response(:); else return; endif endif valid = isfinite (response(:)); columns_ = obj.groupColumns_ (groups); for k = 1:numel (columns_) group_id = grp2idx (columns_{k}); if (numel (group_id) != numel (valid)) return; endif valid &= isfinite (group_id); endfor if (all (valid)) return; endif if (! any (valid)) error (strcat ("anova: no complete observations remain after", ... " removing missing data.")); endif response = response(valid); if (iscell (groups) && size (groups, 1) == 1 ... && numel (groups) == numel (columns_)) for k = 1:numel (groups) value = groups{k}; if (ischar (value) && rows (value) > 1) groups{k} = value(valid, :); else groups{k} = value(valid); endif endfor elseif (! isempty (groups)) groups = groups(valid, :); endif if (! isempty (weights) && numel (weights) != numel (valid)) return; elseif (! isempty (weights)) weights = weights(valid); endif if (isa (factor_table, "table") && width (factor_table) > 0) factor_table = factor_table(valid, :); endif endfunction function columns_ = groupColumns_ (obj, groups) if (isempty (groups)) columns_ = {}; elseif (iscell (groups) && size (groups, 1) == 1 ... && all (cellfun (@(x) isvector (x) || ischar (x), groups))) columns_ = groups; elseif (isvector (groups) || ischar (groups)) columns_ = {groups}; else columns_ = cell (1, columns (groups)); for k = 1:columns (groups) columns_{k} = groups(:, k); endfor endif endfunction function groups = normalizeGroups_ (obj, groups) if (ischar (groups) && rows (groups) > 1) groups = cellstr (groups); elseif (iscell (groups) && size (groups, 1) == 1) for k = 1:numel (groups) if (ischar (groups{k}) && rows (groups{k}) > 1) groups{k} = cellstr (groups{k}); endif endfor endif endfunction function obj = setModelSpecification_ (obj, value) if (isstring (value) && isscalar (value)) value = char (value); endif obj.ModelSpecification = value; if (ischar (value) && strcmpi (value, 'interactions')) obj.ModelType = 'interaction'; else obj.ModelType = value; endif endfunction function obj = setSumOfSquaresType_ (obj, value) if (isnumeric (value) && isscalar (value)) names = {'one', 'two', 'three'}; if (! any (value == [1, 2, 3])) error (strcat ("anova: SumOfSquaresType must be 'one',", ... " 'two', 'three', or 'hierarchical'.")); endif obj.SumOfSquaresType = string (names{value}); obj.SSType = value; return; endif if (isstring (value) && isscalar (value)) value = char (value); endif if (! ischar (value)) error ("anova: SumOfSquaresType must be a character vector."); endif switch (lower (value)) case {"one", "typei", "i"} obj.SumOfSquaresType = string ("one"); obj.SSType = 1; case {"two", "typeii", "ii"} obj.SumOfSquaresType = string ("two"); obj.SSType = 2; case "hierarchical" obj.SumOfSquaresType = string ("hierarchical"); obj.SSType = "h"; case {"three", "typeiii", "iii"} obj.SumOfSquaresType = string ("three"); obj.SSType = 3; otherwise error (strcat ("anova: SumOfSquaresType must be 'one',", ... " 'two', 'three', or 'hierarchical'.")); endswitch endfunction function value = parseSSType_ (obj, value) if (isstring (value) && isscalar (value)) value = char (value); endif if (! ischar (value)) error ("anova.stats: component type must be a character vector."); endif switch (lower (value)) case "one" value = 1; case "two" value = 2; case "three" value = 3; case "hierarchical" value = "h"; otherwise error (strcat ("anova.stats: component type must be 'one',", ... " 'two', 'three', or 'hierarchical'.")); endswitch endfunction function obj = setFactorNames_ (obj, value) if (isstring (value)) value = cellstr (value(:))'; elseif (ischar (value)) value = {value}; endif obj.VarNames = value; obj.FactorNames = string (value); endfunction function obj = syncFactorSelectors_ (obj) if (obj.continuousSpecified_) continuous = obj.normalizeFactorSelector_ (obj.Continuous, ... "CategoricalFactors", ... false); obj.Continuous = continuous; obj.CategoricalFactors = setdiff (1:obj.nFactors_, continuous); else obj.CategoricalFactors = obj.normalizeFactorSelector_ (... obj.CategoricalFactors, ... "CategoricalFactors", true); obj.Continuous = setdiff (1:obj.nFactors_, ... obj.CategoricalFactors); endif obj.RandomFactors = obj.normalizeFactorSelector_ (obj.RandomFactors, ... "RandomFactors", ... true); obj.Random = obj.RandomFactors; endfunction function idx = normalizeFactorSelector_ (obj, value, option, allow_all) if (isempty (value)) idx = []; return; endif if (obj.isName_ (value) && strcmpi (char (value), "all")) if (! allow_all) error ("anova: %s must identify valid factors.", option); endif idx = 1:obj.nFactors_; return; endif if (islogical (value)) if (! isvector (value) || numel (value) != obj.nFactors_) error ("anova: %s logical input must have one value per factor.", ... option); endif idx = find (value(:))'; return; endif if (isstring (value)) value = cellstr (value(:))'; elseif (ischar (value)) value = {value}; endif if (iscellstr (value)) [found, idx] = ismember (value, obj.VarNames); if (! all (found)) error ("anova: %s contains an unknown factor name.", option); endif idx = idx(:)'; return; endif if (! isnumeric (value) || ! isvector (value) ... || any (value != fix (value)) || any (value < 1) ... || any (value > obj.nFactors_)) error ("anova: %s must contain valid factor indices.", option); endif idx = unique (value(:)', "stable"); endfunction function obj = setFormula_ (obj) if (isempty (obj.VarNames)) obj.VarNames = arrayfun (@(k) sprintf ("Factor%d", k), ... 1:obj.nFactors_, "UniformOutput", false); obj.FactorNames = string (obj.VarNames); elseif (numel (obj.VarNames) != obj.nFactors_) error ("anova: FactorNames must contain one name per factor."); endif if (ischar (obj.ModelType) ... && ! isempty (strfind (obj.ModelType, "~"))) schema = parseWilkinsonFormula (obj.ModelType, "matrix"); if (! isscalar (schema.ResponseIdx)) error ("anova: ModelSpecification must contain one response."); endif response = schema.VariableNames{schema.ResponseIdx}; if (! strcmp (response, obj.ResponseName)) error (strcat ("anova: formula response must match", ... " ResponseName.")); endif [schema_names, schema_model] = obj.schemaModel_ (schema); [found, columns_] = ismember (obj.VarNames, schema_names); if (! all (found)) error ("anova: formula must use names in FactorNames."); endif model = schema_model(:, columns_); [model, obj.nesting_] = obj.nestedSpecification_ (... obj.ModelType, obj.VarNames, model); obj.ModelType = model; obj.formulaTerms_ = model; obj.formulaText_ = obj.ModelSpecification; return; endif if (obj.nFactors_ == 0) obj.formulaTerms_ = zeros (0, 0); obj.formulaText_ = sprintf ("%s ~ 1", obj.ResponseName); else if (ischar (obj.ModelType) ... && obj.isPolynomialModel_ (obj.ModelType)) model = obj.polynomialTerms_ (obj.ModelType); obj.ModelType = model; else model = obj.modelTerms_ (obj.ModelType); endif obj.formulaTerms_ = model; terms = obj.formulaTermNames_ (model); rhs = strjoin (terms, " + "); obj.formulaText_ = sprintf ("%s ~ 1 + %s", obj.ResponseName, rhs); endif endfunction function model = modelTerms_ (obj, specification) if (isnumeric (specification) && ! isscalar (specification)) model = specification; return; endif if (isnumeric (specification)) max_order = min (specification, obj.nFactors_); elseif (strcmpi (specification, "linear")) max_order = 1; elseif (strcmpi (specification, "interaction")) max_order = min (2, obj.nFactors_); else max_order = obj.nFactors_; endif model = zeros (0, obj.nFactors_); for order = 1:max_order combinations = nchoosek (1:obj.nFactors_, order); block = zeros (rows (combinations), obj.nFactors_); for row = 1:rows (combinations) block(row, combinations(row, :)) = 1; endfor model = [model; block]; endfor endfunction function names = formulaTermNames_ (obj, model) names = obj.termNames_ (model); if (isempty (obj.nesting_)) return; endif for factor = find (any (obj.nesting_, 2))' parents = find (obj.nesting_(factor, :)); nested = sprintf ("%s(%s)", obj.VarNames{factor}, ... strjoin (obj.VarNames(parents), ",")); for term = find (model(:, factor) > 0)' names{term} = strrep (names{term}, obj.VarNames{factor}, nested); endfor endfor endfunction function obj = updateFormula_ (obj) separator = strfind (obj.formulaText_, "~"); if (isempty (separator)) linear_predictor = obj.formulaText_; else linear_predictor = strtrim (obj.formulaText_(separator(1) + 1:end)); endif term_names = obj.formulaTermNames_ (obj.formulaTerms_); nesting = obj.nesting_; if (isempty (nesting)) nesting = false (obj.nFactors_); endif obj.Formula = struct (... "ResponseName", string (obj.ResponseName), ... "PredictorNames", string (obj.VarNames), ... "Terms", obj.formulaTerms_, ... "TermNames", string (term_names(:)), ... "HasIntercept", true, ... "Nesting", logical (nesting), ... "LinearPredictor", string (linear_predictor), ... "Text", string (obj.formulaText_)); endfunction function tf = isPolynomialModel_ (obj, model) tf = any (strcmpi (model, {"purequadratic", "quadratic"})) ... || ! isempty (regexp (lower (model), '^poly[0-9]+$', 'once')); endfunction function model = polynomialTerms_ (obj, specification) n = obj.nFactors_; if (strcmpi (specification, "purequadratic")) model = [eye(n); 2 * eye(n)]; return; elseif (strcmpi (specification, "quadratic")) interactions = zeros (0, n); if (n > 1) pairs = nchoosek (1:n, 2); interactions = zeros (rows (pairs), n); for i = 1:rows (pairs) interactions(i, pairs(i, :)) = 1; endfor endif model = [eye(n); interactions; 2 * eye(n)]; return; endif degrees = specification(5:end) - '0'; if (numel (degrees) != n) error (strcat ("anova: polyIJK must specify one degree per", ... " factor.")); endif model = []; for factor = 1:n for exponent = 1:degrees(factor) row = zeros (1, n); row(factor) = exponent; model(end + 1, :) = row; endfor endfor grids = cell (1, n); ranges = arrayfun (@(d) 0:d, degrees, "UniformOutput", false); [grids{:}] = ndgrid (ranges{:}); combinations = cell2mat (cellfun (@(g) g(:), grids, ... "UniformOutput", false)); interactions = combinations(sum (combinations > 0, 2) > 1 ... & sum (combinations, 2) <= max (degrees), :); model = [model; interactions]; endfunction function names = termNames_ (obj, model) names = cell (rows (model), 1); for row = 1:rows (model) pieces = {}; for column = find (model(row, :) != 0) exponent = model(row, column); if (exponent == 1) pieces{end + 1} = obj.VarNames{column}; else pieces{end + 1} = sprintf ("%s^%d", ... obj.VarNames{column}, exponent); endif endfor names{row} = strjoin (pieces, ":"); endfor endfunction function alpha = parseAlpha_ (obj, varargin) alpha = obj.Alpha; if (isempty (varargin)) return; endif if (mod (numel (varargin), 2) != 0) error ("anova: name-value pairs must come in pairs."); endif for k = 1:2:numel (varargin) if (! obj.isName_ (varargin{k})) error ("anova: parameter name must be a character vector."); endif switch (lower (char (varargin{k}))) case 'alpha' alpha = varargin{k + 1}; otherwise error ("anova: parameter '%s' is not supported.", varargin{k}); endswitch endfor if (! (isnumeric (alpha) && isscalar (alpha) ... && alpha > 0 && alpha < 1)) error ("anova: Alpha must be a numeric scalar in (0, 1)."); endif endfunction function [stats_, args, sidak_alpha, dims] = comparisonArguments_ (... obj, varargin) factors = {}; option_names = {"alpha", "criticalvaluetype", "approximate", ... "controlgroup", "display", "displayopt", ... "estimate"}; if (! isempty (varargin)) first = varargin{1}; is_option = obj.isName_ (first) ... && any (strcmpi (char (first), option_names)); if ((iscellstr (first) || isstring (first) || ischar (first)) ... && ! is_option) factors = first; varargin(1) = []; endif endif if (isempty (factors)) if (obj.NumFactors != 1) error (strcat ("anova.multcompare: factors must be specified for", ... " a multi-factor ANOVA.")); endif dims = 1; else dims = obj.factorIndices_ (factors); endif if (mod (numel (varargin), 2) != 0) error ("anova.multcompare: name-value pairs must come in pairs."); endif alpha = 0.05; ctype = "tukey-kramer"; has_approximate = false; has_control = false; args = {"display", "off"}; for k = 1:2:numel (varargin) if (! obj.isName_ (varargin{k})) error ("anova.multcompare: parameter name must be text."); endif name = lower (char (varargin{k})); value = varargin{k + 1}; switch (name) case "alpha" alpha = value; case "criticalvaluetype" if (! obj.isName_ (value)) error ("anova.multcompare: CriticalValueType must be text."); endif ctype = lower (char (value)); case "approximate" if (! ((islogical (value) || isnumeric (value)) ... && isscalar (value) && any (value == [0, 1]))) error (strcat ("anova.multcompare: Approximate must be a", ... " logical scalar.")); endif has_approximate = true; case "controlgroup" has_control = true; args = [args, {"controlgroup", value}]; case {"display", "displayopt", "estimate"} args = [args, {name, value}]; otherwise error ("anova.multcompare: parameter '%s' is not supported.", ... varargin{k}); endswitch endfor if (! (isnumeric (alpha) && isscalar (alpha) ... && alpha > 0 && alpha < 1)) error ("anova.multcompare: Alpha must be a numeric scalar in (0, 1)."); endif supported = {"tukey-kramer", "hsd", "dunn-sidak", ... "bonferroni", "scheffe", "dunnett", "lsd"}; if (! any (strcmp (ctype, supported))) error ("anova.multcompare: unsupported CriticalValueType '%s'.", ... ctype); endif if ((has_approximate || has_control) && ! strcmp (ctype, "dunnett")) error (strcat ("anova.multcompare: Approximate and ControlGroup", ... " require CriticalValueType 'dunnett'.")); endif if (strcmp (ctype, "dunnett") && numel (dims) != 1) error ("anova.multcompare: Dunnett's test requires one factor."); endif sidak_alpha = []; if (strcmp (ctype, "dunn-sidak")) sidak_alpha = alpha; ctype = "lsd"; endif args = [args, {"alpha", alpha, "criticalvaluetype", ctype}]; stats_ = obj.Stats; if (strcmp (obj.backend_, "anova2")) if (numel (dims) == 1) estimate = "column"; if (dims == 2) estimate = "row"; endif args = obj.replaceOption_ (args, "estimate", estimate); else [y, groups] = obj.anova2Data_ (); [~, ~, stats_] = anovan (y, groups, obj.buildAnovanArgs_ (){:}); args = [args, {"dim", dims}]; endif elseif (strcmp (obj.backend_, "anovan")) args = [args, {"dim", dims}]; endif endfunction function args = replaceOption_ (obj, args, option, value) names = args(1:2:end); idx = find (cellfun (@(x) obj.isName_ (x) ... && strcmpi (char (x), option), names), 1, "last"); if (isempty (idx)) args = [args, {option, value}]; else args{2 * idx} = value; endif endfunction function idx = factorIndices_ (obj, factors) if (isstring (factors)) factors = cellstr (factors(:))'; elseif (ischar (factors)) factors = {factors}; endif if (! iscellstr (factors)) error ("anova.multcompare: factors must be factor names."); endif [found, idx] = ismember (factors, obj.VarNames); if (! all (found) || isempty (idx)) error ("anova.multcompare: factors contains an unknown factor name."); endif idx = idx(:)'; endfunction function [y, groups] = anova2Data_ (obj) [nr, nc] = size (obj.Response); nlevels = nr / obj.reps_; y = obj.Response(:); column_factor = kron ((1:nc)', ones (nr, 1)); row_factor = repmat (kron ((1:nlevels)', ones (obj.reps_, 1)), ... nc, 1); groups = {column_factor, row_factor}; endfunction function out = comparisonTable_ (obj, C, group_names, dims) if (istable (group_names)) if (numel (dims) == 1) group1 = group_names{C(:, 1), 1}; group2 = group_names{C(:, 2), 1}; if (ischar (group1) && rows (group1) > 1) group1 = cellstr (group1); group2 = cellstr (group2); endif else group1 = group_names(C(:, 1), :); group2 = group_names(C(:, 2), :); endif else group1 = obj.groupIdentifiers_ (group_names(C(:, 1)), dims); group2 = obj.groupIdentifiers_ (group_names(C(:, 2)), dims); endif out = table (group1, group2, C(:, 4), C(:, 3), C(:, 5), C(:, 6), ... "VariableNames", {"Group1", "Group2", ... "MeanDifference", "MeanDifferenceLower", ... "MeanDifferenceUpper", "pValue"}); endfunction function groups = comparisonGroups_ (obj, stats_, dims) df = stats_.df; offsets = 1 + cumsum (df); terms = find (sum (stats_.terms(:,dims) > 0, 2) ... == sum (stats_.terms > 0, 2)); design = zeros (rows (stats_.X), columns (stats_.X)); design(:,1) = 1; for term = terms' columns_ = offsets(term) - df(term) + 1:offsets(term); design(:,columns_) = stats_.X(:,columns_); endfor unique_design = unique (design, "rows", "stable"); rows_ = zeros (rows (unique_design), 1); for i = 1:rows (unique_design) rows_(i) = find (all (design == unique_design(i,:), 2), 1); endfor selected = obj.Factors(rows_, obj.VarNames(dims)); groups = selected; endfunction function groups = groupIdentifiers_ (obj, labels, dims) labels = cellstr (labels); if (isempty (labels)) groups = obj.convertGroupValues_ (cell (0, 1), dims(1)); return; endif parts = cellfun (@(x) strsplit (x, ", "), labels, ... "UniformOutput", false); widths = cellfun (@numel, parts); has_names = ! any (widths != widths(1)) ... && all (cellfun (@(x) ! isempty (strfind (x, "=")), ... [parts{:}])); if (! has_names) parts = cellfun (@(x) {x}, labels, "UniformOutput", false); widths(:) = 1; endif values = cell (numel (labels), widths(1)); for i = 1:numel (labels) for j = 1:widths(1) if (has_names) separator = strfind (parts{i}{j}, "="); value = parts{i}{j}(separator(1) + 1:end); else value = parts{i}{j}; endif values{i, j} = strtrim (value); endfor endfor if (widths(1) == 1) groups = obj.convertGroupValues_ (values(:, 1), dims(1)); else columns_ = cell (1, widths(1)); for j = 1:widths(1) columns_{j} = obj.convertGroupValues_ (values(:, j), dims(j)); endfor groups = table (columns_{:}, ... "VariableNames", obj.VarNames(dims)); endif endfunction function values = convertGroupValues_ (obj, text_values, dim) original = obj.Factors{:, obj.VarNames{dim}}; if (islogical (original)) values = strcmpi (text_values, "true") ... | strcmp (text_values, "1"); elseif (isnumeric (original)) values = cellfun (@str2double, text_values); elseif (iscategorical (original)) values = categorical (text_values, categories (original)); elseif (isstring (original)) values = string (text_values); else values = text_values; endif endfunction function [G, names] = selectedFactorMatrix_ (obj, factors) if (ischar (factors)) idx = find (strcmp (obj.VarNames, factors)); elseif (iscellstr (factors)) idx = cellfun (@(s) find (strcmp (obj.VarNames, s), 1), factors); else idx = factors; endif if (isempty (idx)) G = []; names = {}; return; endif if (any (idx < 1) || any (idx > obj.NumFactors)) error ("anova: factor index exceeds the number of factors."); endif if (isempty (obj.GROUP) && ! isvector (obj.Response)) [n, m] = size (obj.Response); group_arg = reshape (repmat ((1:m), n, 1), [], 1); G = group_arg(:, idx); elseif (iscell (obj.GROUP) ... && all (cellfun (@(c) isvector (c) || ischar (c), ... obj.GROUP(:))) ... && size (obj.GROUP, 1) == 1) G = cell2mat (cellfun (@(c) c(:), obj.GROUP(idx), ... 'UniformOutput', false)); else G = obj.GROUP(:, idx); endif names = obj.VarNames(idx); endfunction function name = factorName_ (obj, idx) if (idx >= 1 && idx <= numel (obj.VarNames)) name = obj.VarNames{idx}; else name = sprintf ("Factor%d", idx); endif endfunction ## Build the public Factors table (one named column per factor) from the ## raw GROUP data, leaving the internal GROUP alias untouched. function tbl = buildFactorsTable_ (obj) if (istable (obj.GROUP)) tbl = obj.GROUP; return; endif if (isempty (obj.GROUP)) if (isvector (obj.Y)) tbl = table (); ## intercept-only: no factors return; endif [nr, nc] = size (obj.Y); if (isempty (obj.reps_)) ## one-way column form: single synthetic factor = column index cols = {reshape(repmat ((1:nc), nr, 1), [], 1)}; else ## balanced two-way (anova2) form: row-block and column factors rowfac = repmat (ceil ((1:nr)' / obj.reps_), nc, 1); colfac = reshape (repmat ((1:nc), nr, 1), [], 1); cols = {rowfac, colfac}; endif elseif (iscell (obj.GROUP) ... && all (cellfun (@(c) isvector (c) || ischar (c), ... obj.GROUP(:))) ... && size (obj.GROUP, 1) == 1) cols = cellfun (@(c) c(:), obj.GROUP, 'UniformOutput', false); elseif (isvector (obj.GROUP)) cols = {obj.GROUP(:)}; else cols = num2cell (obj.GROUP, 1); endif tbl = table (cols{:}, 'VariableNames', obj.FactorNames); endfunction ## Build the public Residuals table (Raw and Pearson) from a raw residual ## vector. Pearson residuals scale the raw residuals by the RMSE. function tbl = residualsTable_ (obj, raw) raw = raw(:); pearson = raw ./ sqrt (max (obj.MSE, eps)); tbl = table (raw, pearson, 'VariableNames', {'Raw', 'Pearson'}); endfunction ## Build the public Metrics table from the fitted ANOVA table and the ## error variance, deriving SSE/SSR/SST and the R-squared measures. function tbl = metricsTable_ (obj) mse = obj.MSE; if (isempty (mse)) mse = NaN; endif dfe = obj.DFE; if (isempty (dfe)) dfe = NaN; endif sse = NaN; sst = NaN; atab = obj.AnovaTable; if (! isempty (atab)) source_col = obj.findAtabColumn_ (atab, {'Source'}); ss_col = obj.findAtabColumn_ (atab, {'SS', 'Sum Sq.', 'Sum Sq'}); for r = 2:rows (atab) name = atab{r, source_col}; if (! ischar (name)) continue; endif if (strcmpi (name, 'Error')) sse = atab{r, ss_col}; elseif (strcmpi (name, 'Total')) sst = atab{r, ss_col}; endif endfor endif ssr = sst - sse; rsq = ssr / max (sst, eps); adj = 1 - (1 - rsq) * (obj.NumObservations - 1) / max (dfe, 1); tbl = table (mse, sqrt (max (mse, 0)), sse, ssr, sst, rsq, adj, ... 'VariableNames', {'MSE', 'RMSE', 'SSE', 'SSR', 'SST', ... 'RSquared', 'AdjustedRSquared'}); endfunction function out = statsTable_ (obj, atab) if (isempty (atab)) out = table ([], [], [], [], [], "VariableNames", ... {"SumOfSquares", "DF", "MeanSquares", "F", "pValue"}); return; endif source_col = obj.findAtabColumn_ (atab, {"Source"}); columns = {{"SumOfSquares", "Sum Sq.", "Sum Sq", "SS"}, ... {"DF", "d.f.", "df"}, ... {"MeanSquares", "Mean Sq.", "Mean Sq", "MS"}, ... {"F"}, {"pValue", "Prob>F"}}; names = {"SumOfSquares", "DF", "MeanSquares", "F", "pValue"}; n = rows (atab) - 1; values = cell (1, numel (columns)); for j = 1:numel (columns) col = obj.findAtabColumn_ (atab, columns{j}); values{j} = NaN (n, 1); for i = 1:n value = atab{i + 1, col}; if (isnumeric (value) && isscalar (value) && ! isempty (value)) values{j}(i) = value; endif endfor endfor row_names = obj.publicSourceNames_ (atab(2:end, source_col)); out = table (values{:}, "VariableNames", names, "RowNames", row_names); endfunction function ems = expectedMeanSquares_ (obj, sstype) obj.ensureFit_ (); [atab, stats_] = obj.componentFit_ (sstype); sources = obj.publicSourceNames_ (atab(2:end-1, 1)); nterms = numel (sources) - 1; term_names = sources(1:nterms); is_random = false (nterms, 1); q_coeff = ones (nterms, 1); variance_coeff = zeros (nterms, nterms); if (strcmp (obj.backend_, "anovan") && isfield (stats_, "terms")) terms = stats_.terms; if (! isempty (obj.RandomFactors)) is_random = obj.randomTermMask_ (terms, stats_); endif [q_coeff, variance_coeff] = obj.emsCoefficients_ (stats_, terms, ... sstype, is_random); elseif (strcmp (obj.backend_, "anova1")) [q_coeff(1), variance_coeff(1, 1)] = obj.oneWayEmsCoefficient_ (); is_random(1) = any (obj.RandomFactors == 1); endif formulas = cell (nterms + 1, 1); types = repmat ({"fixed"}, nterms + 1, 1); for i = 1:nterms pieces = {}; if (is_random(i)) types{i} = "random"; else term = obj.emsTerm_ (q_coeff(i), "Q", term_names{i}); if (! isempty (term)) pieces{end + 1} = term; endif ## A fixed term that contains this one contributes a Q component of ## its own, so the expected mean square names it too. for j = find (! is_random(:))' if (j != i && abs (variance_coeff(i, j)) > 1e-10) pieces{end + 1} = obj.emsTerm_ (variance_coeff(i, j), "Q", ... term_names{j}); endif endfor endif for j = find (is_random(:))' if (abs (variance_coeff(i, j)) > 1e-10) pieces{end + 1} = obj.emsTerm_ (variance_coeff(i, j), ... "V", term_names{j}); endif endfor pieces{end + 1} = "V(Error)"; formulas{i} = strjoin (pieces, "+"); endfor types{end} = "random"; formulas{end} = "V(Error)"; [term_ms, term_df, error_ms, error_df] = obj.meanSquareData_ (atab); error_ms = obj.MSE; error_df = obj.DFE; [denominator_ms, denominator_df, denominator_formula] = ... obj.denominatorData_ (variance_coeff, is_random, term_ms, ... term_df, error_ms, error_df, term_names); denominator_ms(end + 1, 1) = NaN; denominator_df(end + 1, 1) = NaN; denominator_formula{end + 1, 1} = ""; row_names = [term_names; {"Error"}]; ems = table (string (types), string (formulas), denominator_ms, ... denominator_df, string (denominator_formula), ... "VariableNames", {"Type", "ExpectedMeanSquares", ... "MeanSquaresDenominator", "DFDenominator", ... "FDenominator"}, "RowNames", row_names); endfunction function names = publicSourceNames_ (obj, names) if (strcmp (obj.backend_, "anova1") && ! isempty (names)) idx = find (strcmpi (names, "Groups"), 1); if (! isempty (idx)) names{idx} = obj.VarNames{1}; endif elseif (strcmp (obj.backend_, "anova2")) idx = find (strcmpi (names, "Columns"), 1); if (! isempty (idx)) names{idx} = obj.VarNames{1}; endif idx = find (strcmpi (names, "Rows"), 1); if (! isempty (idx)) names{idx} = obj.VarNames{2}; endif idx = find (strcmpi (names, "Interaction"), 1); if (! isempty (idx)) names{idx} = sprintf ("%s:%s", obj.VarNames{:}); endif endif endfunction function [q_coeff, variance_coeff] = emsCoefficients_ (obj, stats_, ... terms, sstype, is_random) X = full (stats_.X); dfs = stats_.df(:); nterms = rows (terms); blocks = cell (nterms, 1); first = 2; for i = 1:nterms blocks{i} = first:first + dfs(i) - 1; first += dfs(i); endfor bases = cell (nterms, 1); for i = 1:nterms factors = find (terms(i, :)); if (isfield (stats_, "vnested") && ! isempty (stats_.vnested)) nested = factors(any (stats_.vnested(factors,:), 2)); if (! isempty (nested)) factors = union (factors, find (any (stats_.vnested(nested,:), 1))); endif endif if (all (! ismember (factors, obj.Continuous))) [~, ~, ids] = unique (stats_.grps(:, factors), "rows"); bases{i} = sparse (1:numel (ids), ids, 1); else bases{i} = X(:, blocks{i}); endif endfor q_coeff = zeros (nterms, 1); variance_coeff = zeros (nterms, nterms); for i = 1:nterms [included, excluded] = obj.emsModels_ (X, blocks, terms, i, sstype); q_coeff(i) = obj.projectionDifference_ (included, excluded, ... bases{i}) / dfs(i); ## Every term that contains this one contributes to its expected mean ## square, whether that term is fixed or random, so the coefficient is ## taken for all of them; which letter names it is decided later. for j = 1:nterms variance_coeff(i, j) = obj.projectionDifference_ (... included, excluded, bases{j}) / dfs(i); endfor endfor endfunction function [term_ms, term_df, error_ms, error_df] = ... meanSquareData_ (obj, atab) source_col = obj.findAtabColumn_ (atab, {"Source"}); ms_col = obj.findAtabColumn_ (atab, ... {"MeanSquares", "Mean Sq.", ... "Mean Sq", "MS"}); df_col = obj.findAtabColumn_ (atab, {"DF", "d.f.", "df"}); sources = atab(2:end, source_col); error_row = find (strcmpi (sources, "Error"), 1); total_row = find (strcmpi (sources, "Total"), 1); term_rows = setdiff (1:numel (sources), [error_row, total_row], ... "stable"); term_ms = cell2mat (atab(term_rows + 1, ms_col)); term_df = cell2mat (atab(term_rows + 1, df_col)); error_ms = atab{error_row + 1, ms_col}; error_df = atab{error_row + 1, df_col}; endfunction function [denominator_ms, denominator_df, formulas] = ... denominatorData_ (obj, variance_coeff, is_random, term_ms, ... term_df, error_ms, error_df, term_names) nterms = numel (term_ms); random_terms = find (is_random(:)); if (isempty (random_terms)) denominator_ms = repmat (error_ms, nterms, 1); denominator_df = repmat (error_df, nterms, 1); formulas = repmat ({"MS(Error)"}, nterms, 1); return; endif random_ms = [term_ms(random_terms); error_ms]; random_df = [term_df(random_terms); error_df]; coefficients = [variance_coeff(random_terms, random_terms), ... ones(numel (random_terms), 1); zeros(1, numel (random_terms)), 1]; names = [term_names(random_terms); {"Error"}]; denominator_ms = zeros (nterms, 1); denominator_df = zeros (nterms, 1); formulas = cell (nterms, 1); for i = 1:nterms target = [variance_coeff(i, random_terms), 1]; own = find (random_terms == i, 1); if (! isempty (own)) target(own) = 0; endif weights = pinv (coefficients') * target'; ## pinv leaves rounding dust on the mean squares a denominator does ## not use. Drop it, so a denominator that is one mean square is ## exactly that mean square, and so the formula and the number agree. weights(abs (weights) <= 1e-10) = 0; denominator_ms(i) = weights' * random_ms; ## Satterthwaite's approximation, over the mean squares the ## denominator actually uses. A mean square carrying no degrees of ## freedom has no sampling distribution to combine, so a denominator ## resting on one has no degrees of freedom either. used = (weights != 0); if (any (used & (random_df(:) <= 0))) denominator_df(i) = 0; else parts = (weights(used) .* random_ms(used)) .^ 2 ./ random_df(used); denominator_df(i) = denominator_ms(i) ^ 2 / sum (parts); endif formulas{i} = obj.meanSquareFormula_ (weights, names); endfor endfunction function formula = meanSquareFormula_ (obj, weights, names) pieces = {}; for i = 1:numel (weights) coefficient = weights(i); if (abs (coefficient) <= 1e-10) continue; elseif (abs (coefficient - 1) <= 1e-10) term = sprintf ("MS(%s)", names{i}); elseif (abs (coefficient + 1) <= 1e-10) term = sprintf ("-MS(%s)", names{i}); else term = sprintf ("%.6g*MS(%s)", coefficient, names{i}); endif if (! isempty (pieces) && coefficient > 0) term = ["+", term]; endif pieces{end + 1} = term; endfor formula = strjoin (pieces, ""); endfunction function [coefficients, mean_squares, dfs, names] = ... varianceSystem_ (obj, stats_, atab, sstype) [term_ms, term_df, error_ms, error_df] = obj.meanSquareData_ (atab); sources = obj.publicSourceNames_ (atab(2:end-1, 1)); term_names = sources(1:numel (term_ms)); is_random = false (numel (term_ms), 1); variance_coeff = zeros (numel (term_ms)); if (! isempty (obj.RandomFactors)) if (strcmp (obj.backend_, "anovan") && isfield (stats_, "terms")) terms = stats_.terms; is_random = obj.randomTermMask_ (terms, stats_); [~, variance_coeff] = obj.emsCoefficients_ (stats_, terms, ... sstype, is_random); elseif (strcmp (obj.backend_, "anova1")) is_random(1) = true; [~, variance_coeff(1, 1)] = obj.oneWayEmsCoefficient_ (); endif endif random_terms = find (is_random); coefficients = [variance_coeff(random_terms, random_terms), ... ones(numel (random_terms), 1); zeros(1, numel (random_terms)), 1]; mean_squares = [term_ms(random_terms); error_ms]; dfs = [term_df(random_terms); error_df]; names = [term_names(random_terms); {"Error"}]; endfunction function atab = applyRandomInference_ (obj, atab, stats_, sstype) if (isempty (obj.RandomFactors) || ! isfield (stats_, "terms")) return; endif terms = stats_.terms; is_random = obj.randomTermMask_ (terms, stats_); [~, variance_coeff] = obj.emsCoefficients_ (stats_, terms, ... sstype, is_random); [term_ms, term_df, error_ms, error_df] = obj.meanSquareData_ (atab); names = obj.publicSourceNames_ (atab(2:rows (terms) + 1, 1)); [denominator_ms, denominator_df] = obj.denominatorData_ (... variance_coeff, is_random, term_ms, term_df, error_ms, ... error_df, names); f_col = obj.findAtabColumn_ (atab, {"F"}); p_col = obj.findAtabColumn_ (atab, {"pValue", "Prob>F"}); f_stat = term_ms ./ denominator_ms; p_value = fcdf (f_stat, term_df, denominator_df, "upper"); atab(2:numel (term_ms) + 1, f_col) = num2cell (f_stat); atab(2:numel (term_ms) + 1, p_col) = num2cell (p_value); endfunction function is_random = randomTermMask_ (obj, terms, stats_) membership = terms > 0; if (isfield (stats_, "vnested") && ! isempty (stats_.vnested)) for factor = 1:columns (membership) parents = find (stats_.vnested(factor,:)); if (! isempty (parents)) membership(:, parents) |= membership(:, factor); endif endfor endif is_random = any (membership(:, obj.RandomFactors), 2); endfunction function [included, excluded] = emsModels_ (obj, X, blocks, terms, ... term, sstype) nterms = numel (blocks); switch (sstype) case 1 included_terms = 1:term; excluded_terms = 1:term-1; case 2 factors = find (terms(term, :)); excluded_terms = find (any (terms(:, factors) ... != terms(term, factors), 2))'; included_terms = [term, excluded_terms]; case "h" factors = find (terms(term, :)); excluded_terms = find (any (terms(:, factors) ... < terms(term, factors), 2))'; included_terms = [term, excluded_terms]; otherwise included_terms = 1:nterms; excluded_terms = setdiff (included_terms, term, "stable"); endswitch included = X(:, [1, blocks{included_terms}]); excluded = X(:, [1, blocks{excluded_terms}]); endfunction function value = projectionDifference_ (obj, included, excluded, basis) value = obj.projectionEnergy_ (included, basis) ... - obj.projectionEnergy_ (excluded, basis); value = max (value, 0); endfunction function value = projectionEnergy_ (obj, design, basis) Q = orth (full (design)); value = sum (sumsq (Q' * basis)); endfunction function [q_coeff, variance_coeff] = oneWayEmsCoefficient_ (obj) if (isvector (obj.Response)) group = obj.GROUP(:); else [n, groups] = size (obj.Response); group = reshape (repmat (1:groups, n, 1), [], 1); endif [~, ~, ids] = unique (group, "stable"); Z = sparse (1:numel (ids), ids, 1); intercept = ones (rows (Z), 1); df = columns (Z) - 1; if (df == 0) q_coeff = 0; else q_coeff = obj.projectionDifference_ (Z, intercept, Z) / df; endif variance_coeff = q_coeff; endfunction function value = emsTerm_ (obj, coefficient, symbol, name) if (! isfinite (coefficient) || abs (coefficient) <= 1e-10) value = ""; elseif (abs (coefficient - 1) <= 1e-10) value = sprintf ("%s(%s)", symbol, name); else value = sprintf ("%.6g*%s(%s)", coefficient, symbol, name); endif endfunction function validateSpec_ (obj) if (! ((isnumeric (obj.SSType) && isscalar (obj.SSType) ... && any (obj.SSType == [1, 2, 3])) ... || (ischar (obj.SSType) && strcmp (obj.SSType, "h")))) error (strcat ("anova: SumOfSquaresType must be 'one',", ... " 'two', 'three', or 'hierarchical'.")); endif if (! (isnumeric (obj.Alpha) && isscalar (obj.Alpha) ... && obj.Alpha > 0 && obj.Alpha < 1)) error ("anova: Alpha must be a numeric scalar in (0, 1)."); endif if (! ischar (obj.Display) ... || ! any (strcmpi (obj.Display, {'on', 'off'}))) error ("anova: Display must be 'on' or 'off'."); endif if (ischar (obj.ModelType)) if (! any (strcmpi (obj.ModelType, ... {'linear', 'interaction', 'full'}))) error (strcat ("anova: ModelSpecification must be 'linear',", ... " 'interactions', 'purequadratic', 'quadratic',", ... " 'polyIJK', 'full', or a terms matrix.")); endif elseif (! isnumeric (obj.ModelType)) error (strcat ("anova: ModelSpecification must be a string", ... " or a numeric terms matrix.")); endif if (isnumeric (obj.ModelType) && isscalar (obj.ModelType) ... && (obj.ModelType != fix (obj.ModelType) || obj.ModelType < 1)) error (strcat ("anova: integer ModelSpecification must be a", ... " positive integer.")); endif if (isnumeric (obj.ModelType) && ! isscalar (obj.ModelType) ... && (any (! isfinite (obj.ModelType(:))) ... || any (obj.ModelType(:) < 0) ... || any (obj.ModelType(:) != fix (obj.ModelType(:))))) error (strcat ("anova: terms matrix entries must be nonnegative", ... " integers.")); endif if (! iscellstr (obj.VarNames)) error ("anova: FactorNames must be a character vector or cellstr."); endif if (! obj.isName_ (obj.ResponseName)) error ("anova: ResponseName must be a character vector."); endif if (! isempty (obj.Continuous) && ! isnumeric (obj.Continuous)) error (strcat ("anova: CategoricalFactors must be 'all' or a", ... " numeric index vector.")); endif if (! isempty (obj.Continuous) ... && (! isvector (obj.Continuous) ... || any (obj.Continuous != fix (obj.Continuous)) ... || any (obj.Continuous < 1))) error ("anova: CategoricalFactors must contain valid factor indices."); endif if (! isempty (obj.Weights) && ! isnumeric (obj.Weights)) error ("anova: Weights must be numeric."); endif if (! isempty (obj.reps_) ... && ! (isnumeric (obj.reps_) && isscalar (obj.reps_) ... && obj.reps_ > 0 && obj.reps_ == fix (obj.reps_))) error ("anova: Reps must be a positive integer scalar."); endif endfunction function validateData_ (obj) nobs = numel (obj.Y); if (! isempty (obj.GROUP)) obj.validateGroupLength_ (obj.GROUP, nobs); endif if (! isempty (obj.Weights) && numel (obj.Weights) != nobs) error ("anova: Weights must have one value per observation."); endif if (! isempty (obj.Continuous) && any (obj.Continuous > obj.nFactors_)) error ("anova: CategoricalFactors must contain valid factor indices."); endif if (! isempty (obj.RandomFactors) ... && any (obj.RandomFactors > obj.nFactors_)) error ("anova: RandomFactors indices exceed the number of factors."); endif if (isnumeric (obj.ModelType) && ! isempty (obj.ModelType) ... && ! isscalar (obj.ModelType) ... && columns (obj.ModelType) != obj.nFactors_) error ("anova: terms matrix must have one column per factor."); endif if (isnumeric (obj.ModelType) && ! isscalar (obj.ModelType)) powered = find (any (obj.ModelType > 1, 1)); if (any (ismember (powered, obj.CategoricalFactors))) error (strcat ("anova: polynomial powers require continuous", ... " factors.")); endif endif endfunction function validateGroupLength_ (obj, group, nobs) if (iscell (group) ... && all (cellfun (@(c) isvector (c) || ischar (c), group(:))) ... && size (group, 1) == 1) for k = 1:numel (group) if (ischar (group{k}) && rows (group{k}) > 1) count = rows (group{k}); else count = numel (group{k}); endif if (count != nobs) error ("anova: GROUP variables must match the number of observations."); endif endfor elseif (isvector (group)) if (numel (group) != nobs) error ("anova: GROUP must match the number of observations."); endif elseif (rows (group) != nobs) error ("anova: GROUP must have one row per observation."); endif endfunction ## Infer the number of factors implied by GROUP / Y. ## - Empty GROUP + matrix Y -> 1 (anova1 matrix form) ## - Empty GROUP + vector Y -> 0 (intercept-only, falls to anovan) ## - GROUP is a cell of vectors -> numel (GROUP) ## - GROUP is a 2-D numeric / cell matrix -> size (GROUP, 2) function nf = countFactors_ (obj) if (isempty (obj.GROUP)) if (ismatrix (obj.Response) && ! isvector (obj.Response)) if (isempty (obj.reps_)) nf = 1; else nf = 2; endif else nf = 0; endif return; endif if (iscell (obj.GROUP) ... && all (cellfun (@(c) isvector (c) || ischar (c), obj.GROUP(:))) ... && size (obj.GROUP, 1) == 1) nf = numel (obj.GROUP); else nf = size (obj.GROUP, 2); endif endfunction ## Backend heuristic. ## anova2 : user passed 'reps' AND Y is a non-vector matrix ## (Y carries the factor structure; reps is required) ## anova1 : 1 factor, no continuous, no weights, SSType == 3 ## anovan : everything else (full generality) function obj = selectBackend_ (obj) anova2_model = ischar (obj.ModelType) ... && any (strcmpi (obj.ModelType, ... {"linear", "interaction", "full"})); ## anova2 requires a balanced design and rejects missing observations, ## so data holding any NaN is fitted through anovan, which omits them. if (! isempty (obj.reps_) && ismatrix (obj.Response) ... && ! isvector (obj.Response) ... && ! any (isnan (obj.Response(:))) ... && isempty (obj.Continuous) && isempty (obj.Weights) ... && isempty (obj.RandomFactors) ... && (isempty (obj.nesting_) || ! any (obj.nesting_(:))) ... && anova2_model) obj.backend_ = 'anova2'; elseif (obj.nFactors_ == 1 && isempty (obj.Continuous) ... && isempty (obj.Weights) && obj.SSType == 3) obj.backend_ = 'anova1'; else obj.backend_ = 'anovan'; endif endfunction ## Lazy refit guard: only fits when never-fit or spec changed. function obj = ensureFit_ (obj) if (! obj.fitted_ || obj.dirty_) obj = obj.selectBackend_ (); obj = obj.fit_ (); endif endfunction ## Dispatch to the selected backend; populate result properties. function obj = fit_ (obj) switch (obj.backend_) case 'anova1' obj = obj.fitAnova1_ (); case 'anova2' obj = obj.fitAnova2_ (); case 'anovan' obj = obj.fitAnovan_ (); endswitch obj = obj.updatePublicResults_ (); obj.fitted_ = true; obj.dirty_ = false; endfunction function obj = fitAnova1_ (obj) ## Run the backend silently; this class owns display and plotting. if (isvector (obj.Response)) [~, atab, stats] = anova1 (obj.Response, obj.GROUP, 'off'); else [~, atab, stats] = anova1 (obj.Response, [], 'off'); endif obj.AnovaTable = atab; obj.Stats = stats; obj.DFE = stats.df; obj.MSE = stats.s ^ 2; ## anova1 reports sqrt(MSE) as s ## Coefficients / Residuals / DesignMatrix / FittedValues are not ## exposed by anova1's stats; they remain at their empty defaults. endfunction function obj = fitAnova2_ (obj) modelarg = 'interaction'; if (ischar (obj.ModelType)) modelarg = obj.ModelType; endif [~, atab, stats] = anova2 (obj.Response, obj.reps_, 'off', ... modelarg); obj.AnovaTable = atab; obj.Stats = stats; obj.DFE = stats.df; obj.MSE = stats.sigmasq; ## Balanced anova2 does not expose coefficients or residuals. endfunction function obj = fitAnovan_ (obj) [y_vec, group_arg] = obj.anovanData_ (); [~, atab, stats] = anovan (y_vec, group_arg, ... obj.buildAnovanArgs_(){:}); obj.AnovaTable = obj.applyRandomInference_ (atab, stats, obj.SSType); obj.Stats = stats; if (isfield (stats, 'coeffs')) obj.coefficientStats_ = stats.coeffs; obj.Coefficients = stats.coeffs(:, 1); endif if (isfield (stats, 'resid')) obj.rawResiduals_ = stats.resid; endif if (isfield (stats, 'X')) obj.DesignMatrix = stats.X; endif if (isfield (stats, 'dfe')) obj.DFE = stats.dfe; endif if (isfield (stats, 'mse')) obj.MSE = stats.mse; endif ## anovan omits any row holding a missing value, so adopt the ## observations it actually fitted whenever it dropped some. if (isfield (stats, 'Y') && numel (stats.Y) != numel (obj.Y)) obj.Y = stats.Y(:); obj.Response = obj.Y; obj.NumObservations = numel (obj.Y); if (isfield (stats, 'grps') ... && columns (stats.grps) == numel (obj.VarNames) ... && rows (stats.grps) == obj.NumObservations) obj.GROUP = num2cell (stats.grps, 1); obj.Factors = table (obj.GROUP{:}, 'VariableNames', obj.VarNames); endif endif if (! isempty (obj.DesignMatrix) && ! isempty (obj.Coefficients)) obj.FittedValues = full (obj.DesignMatrix) * obj.Coefficients; obj.rawResiduals_ = obj.Y - obj.FittedValues; endif if (! isempty (obj.rawResiduals_)) obj.Residuals = obj.residualTable_ (obj.rawResiduals_, obj.MSE); endif endfunction function atab = componentStats_ (obj, sstype) [atab] = obj.componentFit_ (sstype); endfunction function [atab, stats_] = componentFit_ (obj, sstype) if (strcmp (obj.backend_, "anova2")) obj.ensureFit_ (); atab = obj.AnovaTable; stats_ = obj.Stats; return; endif [y_vec, group_arg] = obj.anovanData_ (); args = obj.buildAnovanArgs_ (sstype); [~, atab, stats_] = anovan (y_vec, group_arg, args{:}); atab = obj.applyRandomInference_ (atab, stats_, sstype); endfunction function atab = summaryStats_ (obj) obj.ensureFit_ (); component = obj.componentStats_ (1); sources = component(2:end, 1); error_idx = find (strcmpi (sources, "Error"), 1); total_idx = find (strcmpi (sources, "Total"), 1); term_idx = setdiff (1:numel (sources), [error_idx, total_idx], "stable"); term_ss = cell2mat (component(term_idx + 1, 2)); term_df = cell2mat (component(term_idx + 1, 3)); if (strcmp (obj.backend_, "anova2")) nonlinear = strcmpi (sources(term_idx), "Interaction"); elseif (isfield (obj.Stats, "terms") ... && rows (obj.Stats.terms) == numel (term_idx)) nonlinear = (sum (obj.Stats.terms, 2) > 1); else nonlinear = ! cellfun (@isempty, regexp (sources(term_idx), "[:*]")); endif linear = ! nonlinear; labels = {}; sum_sq = []; df = []; if (any (linear)) labels{end + 1, 1} = "Linear"; sum_sq(end + 1, 1) = sum (term_ss(linear)); df(end + 1, 1) = sum (term_df(linear)); endif if (any (nonlinear)) labels{end + 1, 1} = "NonLinear"; sum_sq(end + 1, 1) = sum (term_ss(nonlinear)); df(end + 1, 1) = sum (term_df(nonlinear)); endif labels{end + 1, 1} = "Regression"; sum_sq(end + 1, 1) = sum (term_ss); df(end + 1, 1) = sum (term_df); tested_rows = numel (labels); error_ss = component{error_idx + 1, 2}; error_df = component{error_idx + 1, 3}; labels{end + 1, 1} = "Error"; sum_sq(end + 1, 1) = error_ss; df(end + 1, 1) = error_df; error_row = numel (labels); lack_row = []; if (strcmp (obj.backend_, "anovan") && isempty (obj.Weights) ... && isfield (obj.Stats, "grps") && isfield (obj.Stats, "Y")) [~, ~, group_id] = unique (obj.Stats.grps, "rows"); group_mean = accumarray (group_id, obj.Stats.Y, [], @mean); pure_ss = sum ((obj.Stats.Y - group_mean(group_id)) .^ 2); pure_df = numel (obj.Stats.Y) - max (group_id); lack_df = error_df - pure_df; if (pure_df > 0 && lack_df > 0) labels(end + 1:end + 2, 1) = {"LackOfFit"; "PureError"}; sum_sq(end + 1:end + 2, 1) = [max(error_ss - pure_ss, 0); pure_ss]; df(end + 1:end + 2, 1) = [lack_df; pure_df]; lack_row = numel (labels) - 1; endif endif labels{end + 1, 1} = "Total"; sum_sq(end + 1, 1) = component{total_idx + 1, 2}; df(end + 1, 1) = component{total_idx + 1, 3}; mean_sq = sum_sq ./ df; mse = mean_sq(error_row); f_stat = mean_sq(1:tested_rows) ./ mse; p_value = fcdf (f_stat, df(1:tested_rows), error_df, "upper"); atab = cell (numel (labels) + 1, 6); atab(1, :) = {"Source", "Sum Sq.", "d.f.", "Mean Sq.", "F", "Prob>F"}; atab(2:end, 1) = labels; atab(2:end, 2:4) = num2cell ([sum_sq, df, mean_sq]); atab(2:tested_rows + 1, 5:6) = num2cell ([f_stat, p_value]); if (! isempty (lack_row)) lack_f = mean_sq(lack_row) / mean_sq(lack_row + 1); lack_p = fcdf (lack_f, df(lack_row), df(lack_row + 1), "upper"); atab(lack_row + 1, 5:6) = {lack_f, lack_p}; endif endfunction function [y_vec, group_arg] = anovanData_ (obj) if (! isempty (obj.reps_) && isempty (obj.GROUP) ... && ! isvector (obj.Response)) [y_vec, group_arg] = obj.anova2Data_ (); elseif (isempty (obj.GROUP) && ! isvector (obj.Response)) [n, m] = size (obj.Response); y_vec = obj.Response(:); group = reshape (repmat (1:m, n, 1), [], 1); group_arg = {group}; else y_vec = obj.Response(:); group_arg = obj.GROUP; if (isempty (group_arg)) group_arg = {}; endif endif endfunction function s = sstypeLabel_ (obj) switch (obj.SSType) case 1; s = "I"; case 2; s = "II"; case 3; s = "III"; otherwise; s = "hierarchical"; endswitch endfunction function printAtab_ (obj, atab) [nrows, ncols] = size (atab); col_w = max (12, ceil (80 / max (ncols, 1))); for j = 1:ncols fprintf ("%-*s", col_w, char (atab{1, j})); endfor fprintf ("\n%s\n", repmat ("-", 1, col_w * ncols)); for i = 2:nrows for j = 1:ncols v = atab{i, j}; if (ischar (v)) fprintf ("%-*s", col_w, v); elseif (isnumeric (v) && ! isempty (v) && isscalar (v)) if (isnan (v)) fprintf ("%-*s", col_w, "NaN"); elseif (v == fix (v) && abs (v) < 1e6) fprintf ("%-*d", col_w, v); else fprintf ("%-*.*g", col_w, 5, v); endif else fprintf ("%-*s", col_w, ""); endif endfor fprintf ("\n"); endfor endfunction function nv = buildAnovanArgs_ (obj, sstype) if (nargin < 2) sstype = obj.SSType; endif ## Run the backend silently; this class owns display and plotting. nv = {'display', 'off', 'sstype', sstype, 'alpha', obj.Alpha}; if (ischar (obj.ModelType) || isnumeric (obj.ModelType)) if (! (isnumeric (obj.ModelType) && isempty (obj.ModelType))) nv = [nv, {'model', obj.ModelType}]; endif endif if (! isempty (obj.VarNames)) nv = [nv, {'varnames', obj.VarNames}]; endif if (! isempty (obj.Continuous)) nv = [nv, {'continuous', obj.Continuous}]; endif if (! isempty (obj.nesting_) && any (obj.nesting_(:))) nv = [nv, {'nested', obj.nesting_}]; endif ## Random terms are retained so their expected mean squares can provide ## the correct F denominators and variance component estimates. if (! isempty (obj.Weights)) nv = [nv, {'weights', obj.Weights}]; endif if (! isempty (obj.Contrasts)) nv = [nv, {'contrasts', obj.Contrasts}]; endif endfunction function h = leverage_ (obj) X = full (obj.DesignMatrix); Q = qr (X, 0); h = sum (Q .^ 2, 2); endfunction function D = cooksDistance_ (obj, leverage) p = max (columns (obj.DesignMatrix), 1); D = (obj.rawResiduals_ .^ 2 ./ max (p * obj.MSE, eps)) ... .* leverage ./ max ((1 - leverage) .^ 2, eps); endfunction function residuals = residualTable_ (obj, raw, mse) pearson = raw ./ sqrt (mse); residuals = table (raw(:), pearson(:), "VariableNames", ... {"Raw", "Pearson"}); endfunction function obj = updatePublicResults_ (obj) if (strcmp (obj.backend_, "anovan") ... && ! isempty (obj.coefficientStats_)) [obj.Coefficients, names] = obj.expandedCoefficients_ (); obj.ExpandedFactorNames = string (names); elseif (strcmp (obj.backend_, "anova1")) obj = obj.populateOneWayResults_ (); elseif (isfield (obj.Stats, "coeffnames") ... && numel (obj.Stats.coeffnames) == numel (obj.Coefficients)) obj.ExpandedFactorNames = string (obj.Stats.coeffnames(:)); elseif (! isempty (obj.Coefficients)) names = arrayfun (@(k) sprintf ("Coefficient%d", k), ... (1:numel (obj.Coefficients))', ... "UniformOutput", false); obj.ExpandedFactorNames = string (names); endif if (isempty (obj.AnovaTable) || isempty (obj.MSE)) return; endif source = obj.findAtabColumn_ (obj.AnovaTable, {"Source"}); ss = obj.findAtabColumn_ (obj.AnovaTable, ... {"SumOfSquares", "Sum Sq.", "Sum Sq", ... "SS"}); df = obj.findAtabColumn_ (obj.AnovaTable, {"DF", "d.f.", "df"}); names = obj.AnovaTable(2:end, source); error_row = find (strcmpi (names, "Error"), 1) + 1; total_row = find (strcmpi (names, "Total"), 1) + 1; if (isempty (error_row) || isempty (total_row)) return; endif sse = obj.AnovaTable{error_row, ss}; sst = obj.AnovaTable{total_row, ss}; total_df = obj.AnovaTable{total_row, df}; if (isnumeric (total_df) && isscalar (total_df) && isfinite (total_df)) obj.NumObservations = total_df + 1; endif ssr = sst - sse; if (sst == 0) rsquared = NaN; adjusted = NaN; else rsquared = ssr / sst; adjusted = 1 - (obj.NumObservations - 1) * sse ... / (obj.DFE * sst); endif obj.Metrics = table (obj.MSE, sqrt (obj.MSE), sse, ssr, sst, ... rsquared, adjusted, "VariableNames", ... {"MSE", "RMSE", "SSE", "SSR", "SST", ... "RSquared", "AdjustedRSquared"}); endfunction function [coefficients, names] = expandedCoefficients_ (obj) coefficients = obj.coefficientStats_(1, 1); names = {"(Intercept)"}; first = 2; for term = 1:rows (obj.Stats.terms) factors = find (obj.Stats.terms(term, :)); mapping = 1; expanded_names = {""}; for factor = factors if (any (factor == obj.Continuous)) contrast = 1; factor_names = {obj.VarNames{factor}}; elseif (isfield (obj.Stats, "vnested") ... && any (obj.Stats.vnested(factor,:))) [contrast, factor_names] = ... obj.nestedCoefficientMap_ (factor); else contrast = obj.Stats.contrasts{factor}; levels = obj.Stats.grpnames{factor}; factor_names = cellfun (@(x) sprintf ("(%s==%s)", ... obj.VarNames{factor}, ... obj.levelText_ (x)), levels, ... "UniformOutput", false); endif mapping = kron (mapping, contrast); combined = cell (numel (expanded_names) * numel (factor_names), 1); cursor = 1; for left = 1:numel (expanded_names) for right = 1:numel (factor_names) if (isempty (expanded_names{left})) combined{cursor} = factor_names{right}; else combined{cursor} = sprintf ("%s:%s", ... expanded_names{left}, ... factor_names{right}); endif cursor += 1; endfor endfor expanded_names = combined; endfor width = obj.Stats.df(term); compact = obj.coefficientStats_(first:first + width - 1, 1); coefficients = [coefficients; mapping * compact]; names = [names; expanded_names]; first += width; endfor endfunction function [mapping, names] = nestedCoefficientMap_ (obj, factor) parents = find (obj.Stats.vnested(factor,:)); [parent_codes, ~, parent_id] = unique (... obj.Stats.grps(:, parents), "rows", "stable"); mapping = zeros (0, 0); names = {}; for parent = 1:rows (parent_codes) rows_ = parent_id == parent; child_codes = unique (obj.Stats.grps(rows_, factor), "stable"); nlevels = numel (child_codes); contrast = [zeros(1, nlevels - 1); eye(nlevels - 1)] ... - 1 / nlevels; mapping = blkdiag (mapping, contrast); for child = child_codes(:)' pieces = cell (1, numel (parents) + 1); pieces{1} = sprintf ("(%s==%s)", obj.VarNames{factor}, ... obj.levelText_ (... obj.Stats.grpnames{factor}{child})); for k = 1:numel (parents) code = parent_codes(parent, k); pieces{k + 1} = sprintf ("(%s==%s)", ... obj.VarNames{parents(k)}, ... obj.levelText_ (... obj.Stats.grpnames{parents(k)}{code})); endfor names{end + 1, 1} = strjoin (pieces, ":"); endfor endfor endfunction function obj = populateOneWayResults_ (obj) means = obj.Stats.means(:); intercept = mean (means); obj.Coefficients = [intercept; means - intercept]; level_names = cellfun (@(x) sprintf ("(%s==%s)", ... obj.VarNames{1}, obj.levelText_ (x)), ... obj.Stats.gnames(:), "UniformOutput", false); obj.ExpandedFactorNames = string ([{"(Intercept)"}; level_names]); if (isvector (obj.Response)) ## grp2idx numbers a categorical group over all its categories, and ## the means are held only for the groups present, in the same order. group_id = grp2idx (obj.GROUP); have = isfinite (group_id); [~, ~, present] = unique (group_id(have)); group_id(have) = present; fitted = NaN (size (group_id)); grouped = isfinite (group_id) & group_id > 0; fitted(grouped) = means(group_id(grouped)); else fitted = repmat (means', rows (obj.Response), 1); fitted = fitted(:); endif valid = isfinite (obj.Y) & isfinite (fitted); obj.FittedValues = NaN (size (obj.Y)); obj.FittedValues(valid) = fitted(valid); obj.rawResiduals_ = obj.Y - obj.FittedValues; obj.Residuals = obj.residualTable_ (obj.rawResiduals_, obj.MSE); endfunction function text = levelText_ (obj, value) if (isnumeric (value) || islogical (value)) text = num2str (value); elseif (isstring (value)) text = char (value); else text = value; endif endfunction function h = plotDiagnostics_ (obj, residuals, fitted, leverage, ... cooksd, dfe, varargin) if (isempty (residuals) || isempty (fitted) || isempty (leverage) ... || isempty (cooksd)) error ("anova.plotDiagnostics: diagnostic inputs must be non-empty."); endif residuals = residuals(:); fitted = fitted(:); leverage = leverage(:); cooksd = cooksd(:); n = numel (residuals); if (numel (fitted) != n || numel (leverage) != n ... || numel (cooksd) != n) error ("anova.plotDiagnostics: diagnostic inputs must match."); endif fig_name = 'Diagnostic Plots: Model Residuals'; visible = 'on'; if (mod (numel (varargin), 2) != 0) error ("anova.plotDiagnostics: name-value pairs must come in pairs."); endif for k = 1:2:numel (varargin) switch (lower (varargin{k})) case 'figurename' fig_name = varargin{k + 1}; case 'visible' visible = varargin{k + 1}; otherwise error ("anova.plotDiagnostics: unknown option '%s'.", varargin{k}); endswitch endfor mse = sum (residuals .^ 2) / max (dfe, 1); t = residuals ./ sqrt (mse * max (1 - leverage, eps)); [~, DI] = sort (cooksd, 'descend'); nk = min (4, n); h = figure ('Name', fig_name, 'Visible', visible); subplot (2, 2, 1); x = ((1:n)' - 0.5) / n; [ts, I] = sort (t); q = norminv (x); plot (q, ts, 'ok', 'markersize', 3); box off; grid on; xlabel ('Theoretical quantiles'); ylabel ('Studentized residuals'); title ('Normal Q-Q Plot'); arrayfun (@(i) text (q(I == DI(i)), t(DI(i)), ... sprintf (" %u", DI(i))), 1:nk); iqr = [0.25; 0.75]; yl = quantile (t, iqr, 1, 6); xl = norminv (iqr); slope = diff (yl) / diff (xl); int = yl(1) - slope * xl(1); ax1_xlim = get (gca, 'XLim'); hold on; plot (ax1_xlim, slope * ax1_xlim + int, 'k-'); hold off; set (gca, 'Xlim', ax1_xlim); subplot (2, 2, 2); plot (fitted, sqrt (abs (t)), 'ko', 'markersize', 3); box off; xlabel ('Fitted values'); ylabel ('sqrt ( | Studentized residuals | )'); title ('Spread-Location Plot'); ax2_xlim = get (gca, 'XLim'); hold on; plot (ax2_xlim, ones (1, 2) * sqrt (2), 'k:'); plot (ax2_xlim, ones (1, 2) * sqrt (3), 'k-.'); plot (ax2_xlim, ones (1, 2) * sqrt (4), 'k--'); hold off; arrayfun (@(i) text (fitted(DI(i)), sqrt (abs (t(DI(i)))), ... sprintf (" %u", DI(i))), 1:nk); xlim (ax2_xlim); subplot (2, 2, 3); plot (leverage, t, 'ko', 'markersize', 3); box off; xlabel ('Leverage'); ylabel ('Studentized residuals'); title ('Residual-Leverage Plot'); ax3_xlim = get (gca, 'XLim'); ax3_ylim = get (gca, 'YLim'); hold on; plot (ax3_xlim, zeros (1, 2), 'k-'); hold off; arrayfun (@(i) text (leverage(DI(i)), t(DI(i)), ... sprintf (" %u", DI(i))), 1:nk); set (gca, 'ygrid', 'on'); xlim (ax3_xlim); ylim (ax3_ylim); subplot (2, 2, 4); stem (cooksd, 'ko', 'markersize', 3); box off; xlabel ('Obs. number'); ylabel ('Cook''s distance'); title ('Cook''s Distance Stem Plot'); xlim ([0, n]); ax4_xlim = get (gca, 'XLim'); ax4_ylim = get (gca, 'YLim'); hold on; plot (ax4_xlim, ones (1, 2) * 4 / max (dfe, eps), 'k:'); plot (ax4_xlim, ones (1, 2) * 0.5, 'k-.'); plot (ax4_xlim, ones (1, 2), 'k--'); hold off; arrayfun (@(i) text (DI(i), cooksd(DI(i)), ... sprintf (" %u", DI(i))), 1:nk); xlim (ax4_xlim); ylim (ax4_ylim); set (findall (gcf, '-property', 'FontSize'), 'FontSize', 7); endfunction function es = effectSizesFromAtab_ (obj) atab = obj.AnovaTable; if (isempty (atab)) error ("anova.getEffectSizes: model has no ANOVA table."); endif source_col = obj.findAtabColumn_ (atab, {'Source'}); ss_col = obj.findAtabColumn_ (atab, {'SS', 'Sum Sq.', 'Sum Sq'}); df_col = obj.findAtabColumn_ (atab, {'df', 'd.f.'}); sources = {}; ss = []; df = []; sse = []; sst = []; for r = 2:rows (atab) name = atab{r, source_col}; if (! ischar (name)) continue; endif val_ss = atab{r, ss_col}; val_df = atab{r, df_col}; if (strcmpi (name, 'Error')) sse = val_ss; elseif (strcmpi (name, 'Total')) sst = val_ss; elseif (isnumeric (val_ss) && isscalar (val_ss)) sources{end + 1} = name; ss(end + 1, 1) = val_ss; df(end + 1, 1) = val_df; endif endfor if (isempty (sst)) sst = sum (ss) + ifelse (isempty (sse), 0, sse); endif if (isempty (sse)) sse = max (sst - sum (ss), 0); endif eta = ss ./ max (sst, eps); partial_eta = ss ./ max (ss + sse, eps); omega = (ss - df .* obj.MSE) ./ max (sst + obj.MSE, eps); es = struct (); es.Source = sources; es.EtaSquared = eta; es.PartialEtaSquared = partial_eta; es.OmegaSquared = omega; endfunction function idx = findAtabColumn_ (obj, atab, names) idx = []; for k = 1:numel (names) hit = find (strcmpi (atab(1, :), names{k}), 1); if (! isempty (hit)) idx = hit; return; endif endfor error ("anova: ANOVA table is missing the '%s' column.", names{1}); endfunction endmethods endclassdef %!demo %! ## Fit an ANOVA object and inspect component and summary statistics %! y = [1; 2; 3; 4; 5; 6; 10; 11; 12]; %! g = [1; 1; 1; 2; 2; 2; 3; 3; 3]; %! aov = anova (g, y, 'FactorNames', {'Treatment'}); %! component = stats (aov) %! summary_table = stats (aov, 'summary') %!demo %! ## Estimate group means and perform post-hoc comparisons %! y = [1; 2; 3; 4; 5; 6; 10; 11; 12]; %! g = [1; 1; 1; 2; 2; 2; 3; 3; 3]; %! aov = anova (g, y, 'SumOfSquaresType', 'two'); %! means = groupmeans (aov) %! comparisons = multcompare (aov, 'display', 'off') %!demo %! ## Plot multiple-comparison intervals %! y = [1; 2; 3; 4; 5; 6; 10; 11; 12]; %! g = [1; 1; 1; 2; 2; 2; 3; 3; 3]; %! aov = anova (g, y, 'SumOfSquaresType', 'two'); %! plotComparisons (aov); ## --- BISTs --------------------------------------------------------------- ## Basic construction: vector Y + single grouping vector %!test %! y = [1; 2; 3; 4; 5; 6]; %! g = [1; 1; 2; 2; 3; 3]; %! a = anova (g, y); %! assert_equal (class (a), 'anova'); %! assert_equal (a.Y, y); %! assert_equal (a.GROUP, g); ## Basic construction: matrix Y, no GROUP %!test %! y = magic (4); %! a = anova (y); %! assert_equal (class (a), "anova"); %! assert_equal (a.Y, y(:)); %! assert_equal (a.GROUP, []); ## Basic construction: cell of two grouping vectors %!test %! y = (1:12)'; %! g1 = repmat ([1;2;3], 4, 1); %! g2 = repmat ([1;1;2;2], 3, 1); %! a = anova ({g1, g2}, y); %! assert_equal (class (a), 'anova'); %! assert_equal (a.NumFactors, 2); ## Property defaults %!test %! a = anova ([1;1;2;2], [1;2;3;4]); %! assert_equal (a.ModelSpecification, 'linear'); %! assert_equal (char (a.SumOfSquaresType), 'three'); %! assert_equal (a.ResponseName, 'Y'); %! assert_equal (cellstr (a.FactorNames), {'Factor1'}); %! assert_equal (a.RandomFactors, []); %! assert_equal (a.CategoricalFactors, 1); %! assert_equal (a.NumFactors, 1); %! assert_equal (a.NumObservations, 4); ## Eager one-way fits populate the derived public result properties. %!test %! a = anova ([1;1;2;2], [1;2;3;4]); %! assert_equal (a.Coefficients, [2.5; -1; 1], 1e-12); %! assert_equal (cellstr (a.ExpandedFactorNames), ... %! {'(Intercept)'; '(Factor1==1)'; '(Factor1==2)'}); %! assert_equal (a.FittedValues, [1.5; 1.5; 3.5; 3.5], 1e-12); %! assert_equal (a.Residuals.Raw, [-0.5; 0.5; -0.5; 0.5], 1e-12); %! assert_equal (a.Residuals.Pearson, ... %! [-1; 1; -1; 1] / sqrt (2), 1e-12); %! assert_equal (a.DesignMatrix, []); ## Name-value parsing: MATLAB-compatible names %!test %! y = (1:12)'; %! g1 = repmat ([1;2;3], 4, 1); %! g2 = repmat ([1;1;2;2], 3, 1); %! a = anova ({g1, g2}, y, 'SumOfSquaresType', 'two', ... %! 'ModelSpecification', 'full', 'FactorNames', {'A', 'B'}); %! assert_equal (char (a.SumOfSquaresType), 'two'); %! assert_equal (a.ModelSpecification, 'full'); %! assert_equal (cellstr (a.FactorNames), {'A', 'B'}); ## Name-value parsing: case-insensitive names, displayopt alias %!test %! a = anova ([1;1;2;2], [1;2;3;4], 'SumOfSquaresType', 'one', 'displayopt', 'on'); %! assert_equal (char (a.SumOfSquaresType), 'one'); ## Display 'on' never reaches a backend: no fit prints a table or opens a ## figure. The three constructions select the anova1, anova2, and anovan ## backends respectively. %!test %! y = [1; 2; 3; 4]; %! g = [1; 1; 2; 2]; %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! figs = get (0, 'children'); %! cmd = strcat ("a1 = anova (g, y, 'Display', 'on');", ... %! "a2 = anova (popcorn, [], 'reps', 3, 'Display', 'on');", ... %! "a3 = anova (g, y, 'SumOfSquaresType', 'one',", ... %! " 'Display', 'on');"); %! str = evalc (cmd); %! assert_equal (isempty (str), true); %! assert_equal (isempty (setdiff (get (0, 'children'), figs)), true); ## Backend selection: one-way default -> anova1 %!test %! a = anova ([1;1;2;2;3;3], [1;2;3;4;5;6]); %! assert_equal (isempty (strfind (evalc ('disp (a)'), '1-way anova')), false); ## Backend selection: one-way matrix-Y form -> anova1 %!test %! a = anova (magic (4)); %! str = evalc ('disp (a)'); %! assert_equal (isempty (strfind (str, '1-way anova')), false); ## Backend selection: matrix Y + explicit 'reps' -> anova2 %!test %! y = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! a = anova (y, [], 'reps', 3); %! assert_equal (isempty (strfind (evalc ('disp (a)'), '2-way anova')), false); ## Backend selection: two-factor cell groups without reps -> anovan %!test %! y = (1:12)'; %! g1 = repmat ([1;2;3], 4, 1); %! g2 = repmat ([1;1;2;2], 3, 1); %! a = anova ({g1, g2}, y); %! str = evalc ('disp (a)'); %! assert_equal (isempty (strfind (str, '2-way anova')), false); ## Backend selection: three factors -> anovan %!test %! y = (1:24)'; %! g1 = repmat ([1;2], 12, 1); %! g2 = repmat ([1;1;2;2], 6, 1); %! g3 = repmat ([1;1;1;1;2;2;2;2], 3, 1); %! a = anova ({g1, g2, g3}, y); %! str = evalc ('disp (a)'); %! assert_equal (isempty (strfind (str, '3-way anova')), false); ## Backend selection: SSType != 3 with 1 factor falls through to anovan %!test %! a = anova ([1;1;2;2;3;3], [1;2;3;4;5;6], 'SumOfSquaresType', 'two'); %! assert_equal (isempty (strfind (evalc ('disp (a)'), 'Type II')), false); ## Backend selection: continuous predictors force anovan %!test %! y = (1:12)'; %! g1 = repmat ([1;2;3], 4, 1); %! g2 = (1:12)'; ## continuous %! a = anova ({g1, g2}, y, 'CategoricalFactors', 1); %! assert_equal (a.CategoricalFactors, 1); ## Backend selection: NaN in a vector response falls through to anovan %!test %! y = [1; 2; 3; NaN; 5; 6; 7; 8]; %! g1 = [1; 2; 1; 2; 1; 2; 1; 2]; %! g2 = [1; 1; 2; 2; 1; 1; 2; 2]; %! a = anova ({g1, g2}, y); %! assert_equal (a.NumFactors, 2); %! assert_equal (a.NumObservations, 7); %! assert_equal (a.Stats.source, 'anovan'); ## Backend selection: weights force anovan %!test %! a = anova ([1;1;2;2;3;3], [1;2;3;4;5;6], 'Weights', ones (6, 1)); %! assert_equal (a.Stats.source, 'anovan'); %! assert_equal (stats (a).Properties.RowNames, ... %! {'Factor1'; 'Error'; 'Total'}); ## --- Week 2: fit delegation smoke tests -------------------------------- ## fit_(): one-way fixture populates the unified result surface %!test %! y = [1; 2; 3; 4; 5; 6]; %! g = [1; 1; 2; 2; 3; 3]; %! a = anova (g, y); %! a.fit (); %! assert_equal (size (a.AnovaTable), [4, 6]); %! assert_equal (a.Stats.source, 'anova1'); %! assert_equal (a.DFE, 3); %! assert_equal (a.MSE, 0.5, 1e-12); ## fit_(): two-way balanced fixture (anova2 backend, popcorn data) %!test %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! a = anova (popcorn, [], 'reps', 3); %! assert_equal (! isempty (strfind (evalc ('disp (a)'), '2-way anova')), true); %! a.fit (); %! assert_equal (size (a.AnovaTable), [5, 6]); %! assert_equal (a.MSE, 0.125, 1e-12); %! assert_equal (a.Stats.sigmasq, 0.125, 1e-12); ## fit_(): N-way fixture (anovan backend, three factors) %!test %! y = (1:24)'; %! g1 = repmat ([1;2], 12, 1); %! g2 = repmat ([1;1;2;2], 6, 1); %! g3 = repmat ([1;1;1;1;2;2;2;2], 3, 1); %! a = anova ({g1, g2, g3}, y); %! assert_equal (a.NumFactors, 3); %! a.fit (); %! assert_equal (a.Stats.source, 'anovan'); %! assert_equal (size (a.AnovaTable), [6, 9]); %! assert_equal (size (a.Coefficients), [7, 1]); %! assert_equal (size (a.Residuals), [24, 2]); %! assert_equal (size (a.DesignMatrix), [24, 4]); ## fit_(): FittedValues = DesignMatrix * Coefficients(:,1) for anovan %!test %! y = [10; 12; 11; 14; 16; 15; 9; 8; 10]; %! g = [1;1;1;2;2;2;3;3;3]; %! a = anova (g, y, 'SumOfSquaresType', 'two'); %! a.fit (); %! assert_equal (numel (a.FittedValues), numel (y)); %! assert_equal (a.FittedValues + a.Residuals.Raw, y, 1e-9); ## ensureFit_(): fit() is idempotent (second call does nothing) %!test %! a = anova ([1;1;1;2;2;2;3;3;3], (1:9)'); %! a.fit (); %! first_table = a.AnovaTable; %! a.fit (); %! assert_equal (a.AnovaTable, first_table); ## buildAnovanArgs_(): SSType and Alpha are forwarded to anovan %!test %! y = (1:12)'; %! g = repmat ([1;2;3], 4, 1); %! a = anova ({g}, y, 'SumOfSquaresType', 'two', 'Alpha', 0.10); %! a.fit (); %! assert_equal (char (a.SumOfSquaresType), 'two'); %! assert_equal (a.Stats.alpha, 0.10, 1e-12); ## --- Numeric references ------------------------------------------------- ## One-way ANOVA: reference values match R's aov(y ~ factor(g)). %!test %! y = [1; 2; 3; 4; 5; 6; 10; 11; 12]; %! g = [1; 1; 1; 2; 2; 2; 3; 3; 3]; %! a = anova (g, y, 'SumOfSquaresType', 'two'); %! a.fit (); %! T = a.AnovaTable; %! assert_equal (T{2, 2}, 126, 1e-12); %! assert_equal (T{3, 2}, 6, 1e-12); %! assert_equal (T{4, 2}, 132, 1e-12); %! assert_equal (T{2, 3}, 2); %! assert_equal (T{3, 3}, 6); %! assert_equal (T{2, 5}, 63, 1e-12); %! assert_equal (T{3, 5}, 1, 1e-12); %! assert_equal (T{2, 6}, 63, 1e-12); %! assert_equal (T{2, 7}, 9.3914e-05, 1e-9); %! assert_equal (a.MSE, 1, 1e-12); %! assert_equal (a.DFE, 6); ## Fitted values and residuals: reference values from one-way cell means. %!test %! y = [1; 2; 3; 4; 5; 6; 10; 11; 12]; %! g = [1; 1; 1; 2; 2; 2; 3; 3; 3]; %! a = anova (g, y, 'SumOfSquaresType', 'two'); %! assert_equal (predict (a), [2; 2; 2; 5; 5; 5; 11; 11; 11], 1e-12); %! assert_equal (a.Residuals.Raw, ... %! [-1; 0; 1; -1; 0; 1; -1; 0; 1], 1e-12); ## Effect sizes: reference eta2 = SS/SST, omega2 = (SS-df*MSE)/(SST+MSE). %!test %! y = [1; 2; 3; 4; 5; 6; 10; 11; 12]; %! g = [1; 1; 1; 2; 2; 2; 3; 3; 3]; %! es = getEffectSizes (anova (g, y, 'SumOfSquaresType', 'two')); %! assert_equal (es.Source, {'Factor1'}); %! assert_equal (es.EtaSquared, 126 / 132, 1e-12); %! assert_equal (es.PartialEtaSquared, 126 / (126 + 6), 1e-12); %! assert_equal (es.OmegaSquared, 124 / 133, 1e-12); ## Two-way balanced ANOVA: popcorn values match the anova2 doc example. %!test %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! a = anova (popcorn, [], 'reps', 3); %! a.fit (); %! T = a.AnovaTable; %! assert_equal (T{2, 2}, 15.75, 1e-12); %! assert_equal (T{3, 2}, 4.5, 1e-12); %! assert_equal (T{4, 2}, 1.75, 1e-12); %! assert_equal (T{5, 2}, 22, 1e-12); %! assert_equal (T{2, 5}, 63, 1e-12); %! assert_equal (T{3, 5}, 36, 1e-12); %! assert_equal (a.MSE, 0.125, 1e-12); ## Post-hoc comparisons: reference values checked against R's TukeyHSD output. %!test %! y = [1; 2; 3; 4; 5; 6; 10; 11; 12]; %! g = [1; 1; 1; 2; 2; 2; 3; 3; 3]; %! a = anova (g, y, 'SumOfSquaresType', 'two'); %! C = multcompare (a, 'CriticalValueType', 'bonferroni'); %! assert_equal (istable (C), true); %! assert_equal (C.Group1, [1; 1; 2]); %! assert_equal (C.Group2, [2; 3; 3]); %! assert_equal (C.MeanDifference, [-3; -9; -6], 1e-12); %! assert_equal (all (C.pValue >= 0 & C.pValue <= 1), true); ## --- Week 3: summary / disp --------------------------------------------- ## summary(): runs ensureFit_ and prints a table %!test %! a = anova ([1;1;1;2;2;2;3;3;3], (1:9)', 'SumOfSquaresType', 'two'); %! str = evalc ('summary (a)'); %! assert_equal (! isempty (strfind (str, 'ANOVA TABLE')), true); %! assert_equal (! isempty (strfind (str, 'backend = anovan')), true); ## summary(): includes MSE / DFE / Alpha line %!test %! a = anova ([1;1;1;2;2;2;3;3;3], (1:9)', 'SumOfSquaresType', 'two', ... %! 'Alpha', 0.10); %! str = evalc ('summary (a)'); %! assert_equal (! isempty (strfind (str, 'Alpha: 0.1')), true); ## disp(): one-line overview of key fields %!test %! a = anova ([1;1;1;2;2;2;3;3;3], (1:9)', 'SumOfSquaresType', 'two'); %! str = evalc ('disp (a)'); %! assert_equal (! isempty (strfind (str, '1-way anova')), true); %! assert_equal (! isempty (strfind (str, 'Type II')), true); %! assert_equal (! isempty (strfind (str, 'Properties, Methods')), true); ## summary(): SSType label appears in the header (Type I / II / III) %!test %! a1 = anova ([1;1;1;2;2;2;3;3;3], (1:9)', 'SumOfSquaresType', 'one'); %! a2 = anova ([1;1;1;2;2;2;3;3;3], (1:9)', 'SumOfSquaresType', 'two'); %! assert_equal (! isempty (strfind (evalc ('summary (a1)'), ... %! 'Type I sums')), true); %! assert_equal (! isempty (strfind (evalc ('summary (a2)'), ... %! 'Type II sums')), true); ## --- Week 4: multcompare pass-through ---------------------------------- ## multcompare(): anovan backend returns MATLAB's comparison table %!test %! y = [1; 2; 3; 4; 5; 6; 10; 11; 12]; %! g = [1; 1; 1; 2; 2; 2; 3; 3; 3]; %! a = anova (g, y, 'SumOfSquaresType', 'two'); %! C = multcompare (a); %! assert_equal (size (C), [3, 6]); %! assert_equal (C.Properties.VariableNames, ... %! {'Group1', 'Group2', 'MeanDifference', ... %! 'MeanDifferenceLower', 'MeanDifferenceUpper', 'pValue'}); ## multcompare(): two-way balanced via anova2 backend %!test %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! a = anova (popcorn, [], 'reps', 3); %! C = multcompare (a, 'Factor1'); %! assert_equal (size (C), [3, 6]); ## multcompare(): runs after a fresh construction (triggers ensureFit_) %!test %! a = anova ([1;1;2;2;3;3], (1:6)', 'SumOfSquaresType', 'two'); %! C = multcompare (a); %! assert_equal (size (C), [3, 6]); ## MATLAB-compatible public methods and properties %!test %! y = [1; 2; 3; 4; 5; 6; 10; 11; 12]; %! g = [1; 1; 1; 2; 2; 2; 3; 3; 3]; %! a = anova (g, y, "FactorNames", {"Brand"}, ... %! "ResponseName", "Yield", "SumOfSquaresType", "two"); %! assert_equal (char (a.Formula.Text), "Yield ~ 1 + Brand"); %! assert_equal (cellstr (a.Formula.PredictorNames), {"Brand"}); %! assert_equal (a.Formula.Terms, 1); %! assert_equal (istable (a.Factors), true); %! assert_equal (a.Factors.Brand, g); %! assert_equal (a.Response, y); %! assert_equal (isstring (a.FactorNames), true); %! assert_equal (cellstr (a.FactorNames), {"Brand"}); %! assert_equal (isstring (a.SumOfSquaresType), true); %! assert_equal (char (a.SumOfSquaresType), "two"); %! assert_equal (isstruct (a.Formula), true); %! assert_equal (! any (strcmp (methods ("anova"), "predict")), true); %! T = stats (a); %! M = groupmeans (a); %! V = varianceComponent (a); %! assert_equal (T.SumOfSquares(1), 126, 1e-12); %! assert_equal (M.Mean, [2; 5; 11], 1e-12); %! assert_equal (V.VarianceComponent, 1, 1e-12); ## Public property surface matches MATLAB's anova object (13 read-only names) %!test %! a = anova ([1;1;1;2;2;2;3;3;3], (1:9)'); %! assert_equal (sort (properties (a)), sort ({'Y'; 'Factors'; 'Formula'; ... %! 'FactorNames'; 'ExpandedFactorNames'; 'SumOfSquaresType'; ... %! 'RandomFactors'; 'CategoricalFactors'; 'ResponseName'; ... %! 'NumObservations'; 'Coefficients'; 'Residuals'; 'Metrics'})); ## Factors is a table with one named column per factor %!test %! y = (1:12)'; %! g1 = repmat ([1;2;3], 4, 1); %! g2 = repmat ([1;1;2;2], 3, 1); %! a = anova ({g1, g2}, y, 'FactorNames', {'A', 'B'}); %! assert_equal (istable (a.Factors), true); %! assert_equal (a.Factors.A, g1); %! assert_equal (a.Factors.B, g2); ## Residuals is a Raw/Pearson table (Pearson = Raw ./ sqrt (MSE)) %!test %! y = [10; 12; 11; 14; 16; 15; 9; 8; 10]; %! g = [1;1;1;2;2;2;3;3;3]; %! a = anova (g, y, 'SumOfSquaresType', 'two'); %! a.fit (); %! assert_equal (istable (a.Residuals), true); %! assert_equal (a.Residuals.Properties.VariableNames, {'Raw', 'Pearson'}); %! assert_equal (a.Residuals.Pearson, a.Residuals.Raw ./ sqrt (a.MSE), 1e-12); ## Metrics table exposes the fit summary with a correct R-squared %!test %! y = [1; 2; 3; 4; 5; 6; 10; 11; 12]; %! g = [1; 1; 1; 2; 2; 2; 3; 3; 3]; %! a = anova (g, y, 'SumOfSquaresType', 'two'); %! a.fit (); %! M = a.Metrics; %! assert_equal (M.Properties.VariableNames, {'MSE', 'RMSE', 'SSE', 'SSR', ... %! 'SST', 'RSquared', 'AdjustedRSquared'}); %! assert_equal (M.SSE, 6, 1e-12); %! assert_equal (M.SST, 132, 1e-12); %! assert_equal (M.RSquared, 126 / 132, 1e-12); ## ExpandedFactorNames is populated for the anovan backend %!test %! y = [1; 2; 3; 4; 5; 6; 10; 11; 12]; %! g = [1; 1; 1; 2; 2; 2; 3; 3; 3]; %! a = anova (g, y, 'SumOfSquaresType', 'two'); %! a.fit (); %! assert_equal (isstring (a.ExpandedFactorNames), true); %! assert_equal (cellstr (a.ExpandedFactorNames), ... %! {'(Intercept)'; '(Factor1==1)'; '(Factor1==2)'; ... %! '(Factor1==3)'}); ## groupmeans(): confidence bounds bracket the group means %!test %! y = [1; 2; 3; 4; 5; 6; 10; 11; 12]; %! g = [1; 1; 1; 2; 2; 2; 3; 3; 3]; %! M = groupmeans (anova (g, y, 'SumOfSquaresType', 'two')); %! assert_equal (all (M.MeanLower <= M.Mean, 'all'), true); %! assert_equal (all (M.Mean <= M.MeanUpper, 'all'), true); ## boxchart(): returns a non-empty graphics result %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! y = [1; 2; 3; 4; 5; 6; 10; 11; 12]; %! g = [1; 1; 1; 2; 2; 2; 3; 3; 3]; %! h = boxchart (anova (g, y)); %! assert_equal (all (ishghandle (h)), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Weighted anovan fit exposes raw residuals (FittedValues + Residuals == Y) %!test %! y = [1; 2; 3; 4; 5; 6; 10; 11; 12]; %! g = [1; 1; 1; 2; 2; 2; 3; 3; 3]; %! w = [1; 1; 1; 2; 2; 2; 3; 3; 3]; %! a = anova (g, y, 'Weights', w); %! a.fit (); %! assert_equal (a.FittedValues + a.Residuals.Raw, y, 1e-9); ## boxchart(): returns graphics handles in the requested axes %!test %! hfig = figure ("visible", "off"); %! unwind_protect %! ax = axes ("parent", hfig); %! a = anova ([1;1;1;2;2;2], [1;2;3;4;5;6], ... %! "SumOfSquaresType", "two"); %! h = boxchart (a, ax); %! assert_equal (all (ishghandle (h)), true); %! assert_equal (all (arrayfun (@(x) ancestor (x, "axes") == ax, h)), true); %! unwind_protect_cleanup %! close (hfig); %! end_unwind_protect ## plotComparisons(): plots adjusted means in the requested figure %!test %! hfig = figure ("visible", "off"); %! unwind_protect %! ax = axes ("parent", hfig); %! a = anova ([1;1;1;2;2;2;3;3;3], (1:9)', ... %! "SumOfSquaresType", "two"); %! h = plotComparisons (a, ax); %! assert_equal (h, hfig); %! assert_equal (numel (findall (ax, "type", "line")) >= 6, true); %! unwind_protect_cleanup %! close (hfig); %! end_unwind_protect ## plotComparisons(): Dunn-Sidak intervals are wider than unadjusted LSD %!test %! hfig = figure ("visible", "off"); %! unwind_protect %! ax1 = subplot (1, 2, 1, "parent", hfig); %! ax2 = subplot (1, 2, 2, "parent", hfig); %! a = anova (kron ((1:4)', ones (3, 1)), (1:12)', ... %! "SumOfSquaresType", "two"); %! plotComparisons (a, ax1, "CriticalValueType", "lsd"); %! plotComparisons (a, ax2, "CriticalValueType", "dunn-sidak"); %! lsd = findobj (ax1, "type", "line", "linestyle", "-"); %! sidak = findobj (ax2, "type", "line", "linestyle", "-"); %! lsd_width = diff (get (lsd(1), "xdata")); %! sidak_width = diff (get (sidak(1), "xdata")); %! assert_equal (sidak_width > lsd_width, true); %! unwind_protect_cleanup %! close (hfig); %! end_unwind_protect ## --- Week 5: diagnostic plots ------------------------------------------ ## plotDiagnostics(): anovan-backed fit creates the four-panel figure %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! y = [10; 12; 11; 14; 16; 15; 9; 8; 10]; %! g = [1;1;1;2;2;2;3;3;3]; %! a = anova (g, y, 'SumOfSquaresType', 'two'); %! h = plotDiagnostics (a, 'Visible', 'off'); %! assert_equal (all (ishghandle (h), 'all'), true); %! assert_equal (numel (findall (h, 'type', 'axes')), 4); %! unwind_protect_cleanup %! close (h); %! close (hf); %! end_unwind_protect ## --- Week 6: predict / effect sizes ------------------------------------- ## predict(): no Xnew returns fitted values %!test %! y = [10; 12; 11; 14; 16; 15; 9; 8; 10]; %! g = [1;1;1;2;2;2;3;3;3]; %! a = anova (g, y, 'SumOfSquaresType', 'two'); %! assert_equal (predict (a), a.FittedValues, 1e-9); ## predict(): accepts an explicit design matrix for anovan-backed fits %!test %! y = [10; 12; 11; 14; 16; 15; 9; 8; 10]; %! g = [1;1;1;2;2;2;3;3;3]; %! a = anova (g, y, 'SumOfSquaresType', 'two'); %! a.fit (); %! assert_equal (predict (a, full (a.DesignMatrix)), a.FittedValues, 1e-9); ## getEffectSizes(): anovan-backed fits report effect-size vectors %!test %! y = [10; 12; 11; 14; 16; 15; 9; 8; 10]; %! g = [1;1;1;2;2;2;3;3;3]; %! a = anova (g, y, 'SumOfSquaresType', 'two'); %! es = getEffectSizes (a); %! assert_equal (iscell (es.Source), true); %! assert_equal (numel (es.EtaSquared), numel (es.Source)); %! assert_equal (all (isfinite (es.PartialEtaSquared), 'all'), true); ## --- Input validation --------------------------------------------------- %!error anova () %!error anova ('abc') %!error anova ([]) %!error ... %! anova ([1;1;2;2], [1;2;3;4], 'SumOfSquaresType') %!error ... %! anova ([1;1;2;2], [1;2;3;4], 'bogus', 1) %!error ... %! anova ([1;1;2;2], [1;2;3;4], 'SumOfSquaresType', 5) %!error ... %! anova ([1;1;2;2], [1;2;3;4], 'Alpha', 2) %!error ... %! anova ([1;1;2;2], [1;2;3;4], 'Display', 'maybe') %!error ... %! anova ([1;1;2;2], [1;2;3;4], 1, 2) %!error ... %! anova (magic (4), [], 'reps', -2) %!error ... %! anova (magic (4), [], 'reps', 1.5) %!error ... %! anova ([1;1;2;2], [1;2;3;4], 'ModelSpecification', 'cubic') %!error ... %! anova ([1;1;2;2], [1;2;3;4], 'Model', [1 0]) %!error ... %! anova ([1;1;2], [1;2;3;4]) %!error ... %! anova ({[1;1;2], [1;2;1;2]}, [1;2;3;4]) %!error ... %! anova ([1;1;2;2], [1;2;3;4], 'Weights', [1;1;1]) %!error ... %! anova ([1;1;2;2], [1;2;3;4], 'CategoricalFactors', 1.5) %!error ... %! anova ([1;1;2;2], [1;2;3;4], 'CategoricalFactors', 2) ## --- Week 9: edge cases ------------------------------------------------ %!test %! a = anova (ones (5, 1), (1:5)'); %! [T, ems] = stats (a); %! assert_equal (T.DF(1), 0); %! assert_equal (T.pValue(1), NaN); %! assert_equal (cellstr (ems.ExpectedMeanSquares), ... %! {'V(Error)'; 'V(Error)'}); %! a = anova (ones (5, 1), (1:5)', 'SumOfSquaresType', 'two'); %! T = stats (a); %! assert_equal (T.DF(1), 0); %! assert_equal (T.pValue(1), NaN); %!test %! a = anova (1, 7); %! T = stats (a); %! assert_equal (a.NumObservations, 1); %! assert_equal (T.pValue(1), NaN); %!test %! a = anova ([1; 1; 2; 2], ones (4, 1)); %! T = stats (a); %! assert_equal (T.SumOfSquares(1), 0); %! assert_equal (T.DF, [1; 2; 3]); %! assert_equal (T.F(1), NaN); %! assert_equal (T.pValue(1), NaN); %!test %! g = categorical ([3; 3; 1; 1], [3, 2, 1]); %! a = anova (g, (1:4)'); %! T = stats (a); %! assert_equal (isfinite (T.pValue(1)), true); %! assert_equal (a.Stats.n, [2, 2]); %! assert_equal (a.Stats.gnames, {'3'; '1'}); %! assert_equal (a.Stats.means, [1.5, 3.5]); %! a = anova (g, (1:4)', 'SumOfSquaresType', 'two'); %! T = stats (a); %! assert_equal (T.F(1), 8, 1e-12); %! assert_equal (a.Stats.grpnames{1}, {'3'; '1'}); %!test # fitted values are the means of the groups a categorical holds %! g = categorical ([3; 3; 1; 1], [3, 2, 1]); %! a = anova (g, (1:4)'); %! assert_equal (a.FittedValues, [1.5; 1.5; 3.5; 3.5]); %!test %! g = kron ((1:120)', ones (2, 1)); %! a = anova (g, (1:240)'); %! T = stats (a); %! assert_equal (T.DF(1), 119); %! assert_equal (T.DF(2), 120); %!test %! n = 128; %! group = cell (1, 6); %! for k = 1:6 %! group{k} = mod (floor ((0:n-1)' / 2^(k-1)), 2) + 1; %! endfor %! a = anova (group, (1:n)', 'ModelSpecification', 'full'); %! stats (a); %! assert_equal (rows (a.Stats.terms), 63); %! assert_equal (columns (a.DesignMatrix), 64); ## --- Week 10: configuration integration ------------------------------- %!test %! y = [1; 2; 3; 4; 5; 6; 10; 11; 12]; %! g = [1; 1; 1; 2; 2; 2; 3; 3; 3]; %! a = anova (g, y, 'FactorNames', {'Treatment'}); %! [component, ems] = stats (a); %! assert_equal (istable (component), true); %! assert_equal (component.Properties.VariableNames, ... %! {'SumOfSquares', 'DF', 'MeanSquares', 'F', 'pValue'}); %! assert_equal (component.Properties.RowNames, ... %! {'Treatment'; 'Error'; 'Total'}); %! assert_equal (component.SumOfSquares, [126; 6; 132], 1e-12); %! assert_equal (istable (ems), true); %! assert_equal (ems.Properties.VariableNames, ... %! {'Type', 'ExpectedMeanSquares', 'MeanSquaresDenominator', ... %! 'DFDenominator', 'FDenominator'}); %! assert_equal (ems.Properties.RowNames, {'Treatment'; 'Error'}); %! assert_equal (cellstr (ems.Type), {'fixed'; 'random'}); %! assert_equal (cellstr (ems.ExpectedMeanSquares), ... %! {'3*Q(Treatment)+V(Error)'; 'V(Error)'}); %! assert_equal (ems.MeanSquaresDenominator, [1; NaN]); %! assert_equal (ems.DFDenominator, [6; NaN]); %! assert_equal (cellstr (ems.FDenominator), {'MS(Error)'; ''}); %!test %! y = [1; 2; 3; 4; 5; 6; 10; 11; 12]; %! g = [1; 1; 1; 2; 2; 2; 3; 3; 3]; %! a = anova (g, y, 'FactorNames', {'Treatment'}, 'RandomFactors', 1); %! [~, ems] = stats (a); %! assert_equal (cellstr (ems.Type), {'random'; 'random'}); %! assert_equal (cellstr (ems.ExpectedMeanSquares), ... %! {'3*V(Treatment)+V(Error)'; 'V(Error)'}); %!test %! y = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! a = anova (y, [], 'Reps', 3, 'ModelSpecification', 'interactions', ... %! 'FactorNames', {'Brand', 'PopperType'}); %! component = stats (a); %! assert_equal (component.Properties.RowNames, ... %! {'Brand'; 'PopperType'; 'Brand:PopperType'; 'Error'; 'Total'}); %!function values = __anova_values__ (tbl) %! if (istable (tbl)) %! values = [tbl.SumOfSquares, tbl.DF, tbl.MeanSquares, tbl.F, tbl.pValue]; %! return; %! endif %! if (columns (tbl) == 6) %! columns_ = [2, 3, 4, 5, 6]; ## anova1 and anova2 layout %! else %! columns_ = [2, 3, 5, 6, 7]; ## anovan layout, Mean Sq. after Singular? %! endif %! values = NaN (rows (tbl) - 1, numel (columns_)); %! for i = 2:rows (tbl) %! for j = 1:numel (columns_) %! value = tbl{i, columns_(j)}; %! if (isnumeric (value) && isscalar (value)) %! values(i - 1, j) = value; %! endif %! endfor %! endfor %!endfunction %!test %! y = [24; 26; 25; 24; 15; 17; 20; 16; 25; 29; 27; 19; 18; 21; 20]; %! gender = [1; 1; 1; 1; 1; 1; 1; 1; 2; 2; 2; 2; 2; 2; 2]; %! degree = [1; 1; 1; 1; 0; 0; 0; 0; 1; 1; 1; 0; 0; 0; 0]; %! a = anova ({gender, degree}, y, 'ModelSpecification', 'full'); %! component = stats (a, 'component', 'one'); %! [~, expected] = anovan (y, {gender, degree}, 'model', 'full', ... %! 'sstype', 1, 'display', 'off'); %! assert_equal (component.Properties.RowNames, ... %! {'Factor1'; 'Factor2'; 'Factor1:Factor2'; 'Error'; 'Total'}); %! assert_equal (__anova_values__ (component), ... %! __anova_values__ (expected), 1e-10); %! assert_equal (char (a.SumOfSquaresType), 'three'); %! assert_equal (istable (stats (a)), true); %!test %! y = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! a = anova (y, [], 'Reps', 3, 'ModelSpecification', 'interactions'); %! component = stats (a, 'Component', 'one'); %! assert_equal (__anova_values__ (component), ... %! __anova_values__ (stats (a)), 1e-10); %! summary_table = stats (a, 'summary'); %! assert_equal (summary_table.Properties.RowNames, ... %! {'Linear'; 'NonLinear'; 'Regression'; 'Error'; 'Total'}); %! assert_equal (summary_table.SumOfSquares(1:3), ... %! [20.25; 1 / 12; 61 / 3], 1e-10); %! assert_equal (summary_table.DF(1:3), [3; 2; 5]); %! assert_equal (summary_table.F(1:3), [48.6; 0.3; 29.28], 1e-10); %!test %! x = kron ((0:3)', ones (2, 1)); %! y = [0; 0.2; 1; 1.2; 4; 4.2; 9; 9.2]; %! summary_table = stats (anova (x, y, 'CategoricalFactors', []), 'summary'); %! assert_equal (summary_table.Properties.RowNames, ... %! {'Linear'; 'Regression'; 'Error'; 'LackOfFit'; ... %! 'PureError'; 'Total'}); %! assert_equal (summary_table.SumOfSquares(3:5), [8.08; 8; 0.08], 1e-12); %! assert_equal (summary_table.DF(3:5), [6; 2; 4]); %! assert_equal (summary_table.F(4), 200, 1e-10); %! assert_equal (summary_table.pValue(4), 9.802960494567e-05, 1e-12); %! g = [ones(10, 1); 2 * ones(10, 1)]; %! y = [(0:9)'; (1000:1009)']; %! summary_table = stats (anova (g, y), 'summary'); %! assert_equal (summary_table.pValue(1) > 0, true); %!test %! [demo_code, demo_idx] = test ('anovan', 'grabdemo'); %! assert_equal (numel (demo_idx) - 1, 13); %! for k = 1:numel (demo_idx) - 1 %! code = demo_code(demo_idx(k):demo_idx(k + 1) - 1); %! code = strrep (code, "'display', 'on'", "'display', 'off'"); %! code = strrep (code, "anovan (y, g, 'weights', v.^-1)", ... %! "anovan (y, g, 'weights', v.^-1, 'display', 'off')"); %! code = regexprep (code, '^ *(figure|plot|xlabel) [^\n]*$', '', 'lineanchors'); %! eval (code); %! switch (k) %! case 1 %! a = anova (gender, score, 'FactorNames', {'gender'}); %! case 2 %! a = anova ({treatment(:), subject(:)}, score(:), ... %! 'ModelSpecification', 'full', 'RandomFactors', 2, ... %! 'SumOfSquaresType', 'two', ... %! 'FactorNames', {'treatment', 'subject'}); %! case 3 %! a = anova (alloy, strength, 'FactorNames', {'alloy'}); %! case 4 %! a = anova ({seconds(:), subject(:)}, words(:), ... %! 'ModelSpecification', 'full', 'RandomFactors', 2, ... %! 'SumOfSquaresType', 'two', ... %! 'FactorNames', {'seconds', 'subject'}); %! case 5 %! a = anova ({brands(:), popper(:)}, popcorn(:), ... %! 'ModelSpecification', 'full', ... %! 'FactorNames', {'brands', 'popper'}); %! case 6 %! a = anova ({gender, degree}, salary, 'ModelSpecification', 'full', ... %! 'FactorNames', {'gender', 'degree'}); %! case 7 %! a = anova ({sugar, milk}, babble, 'ModelSpecification', 'full', ... %! 'FactorNames', {'sugar', 'milk'}); %! case 8 %! a = anova ({drug(:), feedback(:), diet(:)}, BP(:), ... %! 'ModelSpecification', 'full', ... %! 'FactorNames', {'drug', 'feedback', 'diet'}); %! case 9 %! a = anova ({strain, treatment, block}, measurement / 10, ... %! 'ModelSpecification', 'full', 'RandomFactors', 3, ... %! 'SumOfSquaresType', 'two', ... %! 'FactorNames', {'strain', 'treatment', 'block'}); %! case 10 %! a = anova ({species, temp}, pulse, 'CategoricalFactors', 1, ... %! 'SumOfSquaresType', 'hierarchical', ... %! 'FactorNames', {'species', 'temp'}); %! case 11 %! model = [1 0 0; 0 1 0; 0 0 1; 1 1 0]; %! a = anova ({treatment, exercise, age}, score, ... %! 'ModelSpecification', model, 'CategoricalFactors', [1, 2], ... %! 'SumOfSquaresType', 'hierarchical', ... %! 'FactorNames', {'treatment', 'exercise', 'age'}); %! case 12 %! a = anova (g, dv, 'FactorNames', {'score'}); %! case 13 %! a = anova (g, y, 'Weights', v .^ -1); %! endswitch %! switch (k) %! case 2 %! [~, ATAB, STATS] = anovan (score(:), ... %! {treatment(:), subject(:)}, 'model', 'full', 'sstype', 2, ... %! 'varnames', {'treatment', 'subject'}, 'display', 'off'); %! case 4 %! [~, ATAB, STATS] = anovan (words(:), ... %! {seconds(:), subject(:)}, 'model', 'full', 'sstype', 2, ... %! 'varnames', {'seconds', 'subject'}, 'display', 'off'); %! case 9 %! [~, ATAB, STATS] = anovan (measurement / 10, ... %! {strain, treatment, block}, 'model', 'full', 'sstype', 2, ... %! 'varnames', {'strain', 'treatment', 'block'}, ... %! 'display', 'off'); %! endswitch %! actual = stats (a); %! actual_sources = actual.Properties.RowNames; %! expected_sources = ATAB(2:end, 1); %! expected_sources = regexprep (expected_sources, "X([0-9]+)", "Factor$1"); %! if (strcmp (a.Stats.source, 'anova1')) %! actual_sources{1} = expected_sources{1}; %! endif %! assert_equal (actual_sources, expected_sources); %! actual_values = __anova_values__ (actual); %! expected_values = __anova_values__ (ATAB); %! if (any (k == [2, 4, 9])) %! assert_equal (actual_values(:, 1:3), expected_values(:, 1:3), 1e-8); %! else %! assert_equal (actual_values, expected_values, 1e-8); %! endif %! if (! any (k == [1, 3, 12])) %! if (k == 13) %! expected_residuals = STATS.Y - full (STATS.X) * STATS.coeffs(:, 1); %! else %! expected_residuals = STATS.resid; %! endif %! assert_equal (a.Residuals.Raw, expected_residuals, 1e-8); %! assert_equal (size (a.DesignMatrix), size (STATS.X)); %! endif %! endfor ## MATLAB-compatible table constructors and public result schemas. %!test %! dose = [1; 1; 2; 2; 1; 2]; %! site = {'A'; 'B'; 'A'; 'B'; 'A'; 'B'}; %! yield = [1; 2; 4; 5; 2; 6]; %! tbl = table (dose, site, yield, ... %! 'VariableNames', {'Dose', 'Site', 'Yield'}); %! a = anova (tbl, 'Yield'); %! assert_equal (cellstr (a.FactorNames), {'Dose', 'Site'}); %! assert_equal (a.ResponseName, 'Yield'); %! assert_equal (istable (a.Factors), true); %! assert_equal (a.Factors.Properties.VariableNames, {'Dose', 'Site'}); %! assert_equal (a.Y, yield); %! T = stats (a); %! direct = stats (anova ({dose, site}, yield, ... %! 'FactorNames', {'Dose', 'Site'})); %! assert_equal (__anova_values__ (T), __anova_values__ (direct), 1e-12); %!test %! dose = [1; 1; 2; 2; 1; 2]; %! site = {'A'; 'B'; 'A'; 'B'; 'A'; 'B'}; %! yield = [1; 2; 4; 5; 2; 6]; %! tbl = table (dose, site, yield, ... %! 'VariableNames', {'Dose', 'Site', 'Yield'}); %! a = anova (tbl, 'Yield ~ Dose + Site + Dose:Site'); %! T = stats (a); %! direct = stats (anova ({dose, site}, yield, ... %! 'FactorNames', {'Dose', 'Site'}, ... %! 'ModelSpecification', 'full')); %! assert_equal (char (a.Formula.Text), ... %! 'Yield ~ Dose + Site + Dose:Site'); %! assert_equal (a.ModelSpecification, char (a.Formula.Text)); %! assert_equal (__anova_values__ (T), __anova_values__ (direct), 1e-12); %!test %! dose = [1; 1; 2; 2]; %! site = [1; 2; 1; 2]; %! y = [1; 2; 4; 5]; %! tbl = table (dose, site, 'VariableNames', {'Dose', 'Site'}); %! a = anova (tbl, y, 'FactorNames', {'Site'}); %! assert_equal (cellstr (a.FactorNames), {'Site'}); %! assert_equal (a.Factors.Site, site); %! assert_equal (stats (a).DF, [1; 2; 3]); %!test %! g1 = [1; 1; 1; 1; 2; 2; 2; 2]; %! g2 = [1; 1; 2; 2; 1; 1; 2; 2]; %! y = (1:8)'; %! a = anova ({g1, g2}, y, 'FactorNames', {'A', 'B'}, ... %! 'CategoricalFactors', [true, false], 'RandomFactors', 'A'); %! assert_equal (a.CategoricalFactors, 1); %! assert_equal (a.RandomFactors, 1); %! b = anova ({g1, g2}, y, 'FactorNames', {'A', 'B'}, ... %! 'CategoricalFactors', {'A', 'B'}, 'RandomFactors', 'all'); %! assert_equal (b.CategoricalFactors, [1, 2]); %! assert_equal (b.RandomFactors, [1, 2]); %!test %! y = [1; 2; 3; 4; 5; 6]; %! g = [1; 1; 1; 2; 2; 2]; %! a = anova (g, y, 'SumOfSquaresType', 'two'); %! stats (a); %! assert_equal (isvector (a.Coefficients), true); %! assert_equal (isstring (a.ExpandedFactorNames), true); %! assert_equal (istable (a.Residuals), true); %! assert_equal (a.Residuals.Properties.VariableNames, {'Raw', 'Pearson'}); %! assert_equal (a.Residuals.Pearson, ... %! a.Residuals.Raw / sqrt (a.Metrics.MSE), 1e-12); %! assert_equal (a.Metrics.Properties.VariableNames, ... %! {'MSE', 'RMSE', 'SSE', 'SSR', 'SST', ... %! 'RSquared', 'AdjustedRSquared'}); %! assert_equal (a.Metrics.SST, a.Metrics.SSE + a.Metrics.SSR, 1e-12); %! assert_equal (a.Coefficients, [3.5; -1.5; 1.5], 1e-12); %! assert_equal (cellstr (a.ExpandedFactorNames), ... %! {'(Intercept)'; '(Factor1==1)'; '(Factor1==2)'}); %!test %! y = [1; 2; 3; 4; 5; 6; 10; 11; 12]; %! g = [1; 1; 1; 2; 2; 2; 3; 3; 3]; %! a = anova (g, y); %! m = multcompare (a); %! assert_equal (istable (m), true); %! assert_equal (m.Properties.VariableNames, ... %! {'Group1', 'Group2', 'MeanDifference', ... %! 'MeanDifferenceLower', 'MeanDifferenceUpper', 'pValue'}); %! assert_equal (m.MeanDifference, [-3; -9; -6], 1e-12); %! v = varianceComponent (a); %! assert_equal (v.Properties.VariableNames, ... %! {'VarianceComponent', 'VarianceComponentLower', ... %! 'VarianceComponentUpper'}); %! assert_equal (v.Properties.RowNames, {'Error'}); %! one_group = multcompare (anova (ones (4, 1), (1:4)')); %! assert_equal (istable (one_group), true); %! assert_equal (size (one_group), [0, 6]); %!test %! g1 = [1; 1; 1; 1; 2; 2; 2; 2]; %! g2 = {'A'; 'A'; 'B'; 'B'; 'A'; 'A'; 'B'; 'B'}; %! y = [1; 2; 3; 4; 5; 6; 8; 9]; %! a = anova ({g1, g2}, y, 'FactorNames', {'Dose', 'Site'}, ... %! 'ModelSpecification', 'full'); %! m = multcompare (a, {'Dose', 'Site'}, ... %! 'CriticalValueType', 'bonferroni'); %! assert_equal (istable (m.Group1), true); %! assert_equal (istable (m.Group2), true); %! assert_equal (m.Group1.Properties.VariableNames, {'Dose', 'Site'}); %! assert_equal (rows (m), 6); %!test %! g1 = {"x, y"; "x, y"; "x, y"; "x, y"; "z=1"; "z=1"; "z=1"; "z=1"}; %! g2 = {"a=1"; "a=1"; "b,2"; "b,2"; "a=1"; "a=1"; "b,2"; "b,2"}; %! y = (1:8)'; %! a = anova ({g1, g2}, y, "FactorNames", {"Dose", "Site"}, ... %! "ModelSpecification", "full"); %! m = multcompare (a, {"Dose", "Site"}); %! values = [m.Group1.Dose; m.Group2.Dose; m.Group1.Site; m.Group2.Site]; %! assert_equal (any (strcmp (values, "x, y")), true); %! assert_equal (any (strcmp (values, "z=1")), true); %! assert_equal (any (strcmp (values, "a=1")), true); %! assert_equal (any (strcmp (values, "b,2")), true); %!test %! g1 = [1; 1; 1; 1; 2; 2; 2; 2]; %! g2 = [1; 1; 2; 2; 1; 1; 2; 2]; %! y = (1:8)'; %! a = anova ({g1, g2}, y, 'FactorNames', {'A', 'B'}, ... %! 'ResponseName', 'R', ... %! 'ModelSpecification', 'R ~ A + B + A:B'); %! assert_equal (char (a.Formula.Text), 'R ~ A + B + A:B'); %! assert_equal (stats (a).Properties.RowNames, ... %! {'A'; 'B'; 'A:B'; 'Error'; 'Total'}); %! b = anova ({g1, g2}, y, 'FactorNames', {'A', 'B'}, ... %! 'ModelSpecification', 3); %! assert_equal (b.ModelSpecification, 3); %! assert_equal (char (b.Formula.Text), 'Y ~ 1 + A + B + A:B'); %! assert_equal (__anova_values__ (stats (a)), ... %! __anova_values__ (stats (b)), 1e-12); %!test %! tbl = table ([1; 1; 2; 2], [1; 2; 3; 4], ... %! 'VariableNames', {'Group', 'Yield'}); %! a = anova (tbl, 'Yield ~ Group', 'FactorNames', {'Yield'}, ... %! 'ResponseName', 'Ignored', 'ModelSpecification', 'full'); %! assert_equal (cellstr (a.FactorNames), {'Group'}); %! assert_equal (a.ResponseName, 'Yield'); %! assert_equal (a.ModelSpecification, 'Yield ~ Group'); %!test %! a = anova ([1; 1; 2; 2], [1; NaN; 3; 4]); %! stats (a); %! assert_equal (a.NumObservations, 3); %! assert_equal (a.Y, [1; 3; 4]); %! assert_equal (height (a.Factors), 3); %! assert_equal (height (a.Residuals), 3); ## Random-factor variance components match MATLAB's documented carsmall example. %!test %! load carsmall %! a = anova ({Origin, Model_Year}, MPG, "RandomFactors", [1, 2], ... %! "FactorNames", {"Origin", "Year"}); %! v = varianceComponent (a); %! assert_equal (v.Properties.RowNames, {"Origin"; "Year"; "Error"}); %! assert_equal (v.VarianceComponent, [21.337; 44.031; 20.198], 5e-3); %! assert_equal (v.VarianceComponentLower, [6.1257; 11.176; 15.298], 5e-3); %! assert_equal (v.VarianceComponentUpper, [139.94; 1765.7; 27.909], 5e-1); ## Interactions containing a random factor remain in the model and provide ## the denominator for fixed-effect tests. %!test %! A = kron ([1; 2], ones (12, 1)); %! B = repmat (kron ([1; 2; 3], ones (4, 1)), 2, 1); %! y = 10 + 2 * (A == 2) + ... %! [0;1;-1;0; 1;0;-1;0; -1;0;1;0; 0;1;0;-1; 2;1;0;1; -1;0;1;0]; %! a = anova ({A, B}, y, "FactorNames", {"A", "B"}, ... %! "ModelSpecification", "full", "RandomFactors", 2); %! [s, ems] = stats (a); %! assert_equal (s.Properties.RowNames, {"A"; "B"; "A:B"; "Error"; "Total"}); %! assert_equal (s.F(1:3), [49; 1; 1], 1e-12); %! assert_equal (cellstr (ems.Type), {"fixed"; "random"; "random"; "random"}); %! assert_equal (cellstr (ems.FDenominator), ... %! {"MS(A:B)"; "MS(A:B)"; "MS(Error)"; ""}); %! v = varianceComponent (a); %! assert_equal (v.Properties.RowNames, {"B"; "A:B"; "Error"}); ## A negative variance-component estimate is reported without an interval. %!test %! A = kron ([1; 2], ones (12, 1)); %! B = repmat (kron ([1; 2; 3; 4], ones (3, 1)), 2, 1); %! y = [5;6;4; 12;13;11; 20;19;21; 8;7;9; ... %! 7;8;6; 15;14;16; 23;22;24; 10;11;9]; %! a = anova ({A, B}, y, 'FactorNames', {'A', 'B'}, ... %! 'ModelSpecification', 'full', 'RandomFactors', 2); %! v = varianceComponent (a); %! assert_equal (v.Properties.RowNames, {'B'; 'A:B'; 'Error'}); %! assert_equal (v.VarianceComponent, ... %! [45.4166666666666; -0.166666666666666; 1], 1e-9); %! assert_equal (v.VarianceComponentLower, ... %! [13.4429180926305; NaN; 0.554682109897800], 1e-9); %! assert_equal (v.VarianceComponentUpper, ... %! [632.517205323886; NaN; 2.31626772541430], 1e-8); ## A non-negative estimate has its lower bound truncated at zero. %!test %! A = kron ([1; 2], ones (12, 1)); %! B = repmat (kron ([1; 2; 3; 4], ones (3, 1)), 2, 1); %! y = [5;6;4; 12;13;11; 20;19;21; 8;7;9; ... %! 7;8;6; 15;14;16; 23;22;24; 10;11;9]; %! y(1:12) += [0;0;0; 1;1;1; -1;-1;-1; 0.4;0.4;0.4]; %! a = anova ({A, B}, y, 'FactorNames', {'A', 'B'}, ... %! 'ModelSpecification', 'full', 'RandomFactors', 2); %! v = varianceComponent (a); %! assert_equal (v.VarianceComponent(2), 0.253333333333400, 1e-9); %! assert_equal (v.VarianceComponentLower(2), 0); %! assert_equal (v.VarianceComponentUpper(2), 7.97098397237600, 1e-9); ## Named polynomial models preserve MATLAB's term definitions. %!test %! x = (-2:2)'; %! y = 2 + 3 * x + 4 * x .^ 2; %! a = anova (x, y, "CategoricalFactors", [], ... %! "ModelSpecification", "purequadratic", ... %! "FactorNames", {"x"}); %! s = stats (a); %! assert_equal (a.Stats.terms, [1; 2]); %! assert_equal (s.Properties.RowNames, {"x"; "x^2"; "Error"; "Total"}); %! assert_equal (a.FittedValues, y, 1e-12); ## Hierarchical sums preserve polynomial power containment. %!test %! x = [0; 1; 2; 4; 7; 8; 10; 15]; %! y = [1; 2; 3; 6; 12; 15; 21; 40]; %! a = anova (x, y, "CategoricalFactors", [], ... %! "ModelSpecification", [1; 2], ... %! "SumOfSquaresType", "hierarchical"); %! s = stats (a); %! assert_equal (char (a.SumOfSquaresType), "hierarchical"); %! assert_equal (a.SSType, "h"); %! assert_equal (s.SumOfSquares(1:2), ... %! [1149.540669856459; 60.32250042332054], 1e-10); ## Accepted aliases are canonicalized in the public property. %!test %! a = anova ([1; 1; 2; 2], (1:4)', "SumOfSquaresType", "typeii"); %! assert_equal (char (a.SumOfSquaresType), "two"); %!test %! [x1, x2] = ndgrid ((-2:2)', (-1:1)'); %! x1 = x1(:); %! x2 = x2(:); %! y = 1 + 2*x1 + 3*x2 + 4*x1.*x2 + 5*x1.^2 + 6*x2.^2; %! a = anova ({x1, x2}, y, "CategoricalFactors", [], ... %! "ModelSpecification", "quadratic"); %! stats (a); %! assert_equal (a.Stats.terms, [1 0; 0 1; 1 1; 2 0; 0 2]); %! assert_equal (a.FittedValues, y, 1e-10); %!test %! [x1, x2] = ndgrid ([-1; 1], (-2:2)'); %! x1 = x1(:); %! x2 = x2(:); %! y = x1 + x2.^2 + x2.^3 + x1.*x2 + x1.*x2.^2; %! a = anova ({x1, x2}, y, "CategoricalFactors", [], ... %! "ModelSpecification", "poly13"); %! stats (a); %! assert_equal (a.Stats.terms, [1 0; 0 1; 0 2; 0 3; 1 1; 1 2]); %!test %! x = (-2:2)'; %! tbl = table (x, 2 + 3*x + 4*x.^2, "VariableNames", {"x", "y"}); %! a = anova (tbl, "y ~ x^2", "CategoricalFactors", []); %! stats (a); %! assert_equal (a.Stats.terms, [1; 2]); %! assert_equal (a.FittedValues, tbl.y, 1e-12); ## Nested factors use separate within-parent contrasts. %!test %! A = kron ([1; 2], ones (6, 1)); %! B = repmat (kron ([1; 2], ones (3, 1)), 2, 1); %! y = 10*A + 2*B + repmat ([-1; 0; 1], 4, 1); %! tbl = table (A, B, y, "VariableNames", {"A", "B", "Y"}); %! a = anova (tbl, "Y ~ A + B(A)"); %! s = stats (a); %! assert_equal (a.Stats.terms, [1 0; 0 1]); %! assert_equal (a.Stats.vnested, logical ([0 0; 1 0])); %! assert_equal (s.Properties.RowNames, {"A"; "B(A)"; "Error"; "Total"}); %! assert_equal (s.DF, [1; 2; 8; 11]); %!test %! [A, C, B, R] = ndgrid ([1; 2], [1; 2], [1; 2], [1; 2]); %! A = A(:); C = C(:); B = B(:); R = R(:); %! y = A + 2*C + 3*B + 0.1*R; %! tbl = table (A, B, C, y, "VariableNames", {"A", "B", "C", "Y"}); %! a = anova (tbl, "Y ~ A*C + B(A,C)"); %! s = stats (a); %! assert_equal (a.Stats.vnested, logical ([0 0 0; 1 0 1; 0 0 0])); %! assert_equal (s.Properties.RowNames, ... %! {"A"; "C"; "A:C"; "B(A,C)"; "Error"; "Total"}); %! assert_equal (s.DF(4), 4); ## Unbalanced NaN cleanup preserves the anovan fallback results. %!test %! x = [1, 5; 2, 6; 3, 7; NaN, 8]; %! a = anova (x, [], "Reps", 2, "ModelSpecification", "interactions"); %! stats (a); %! assert_equal (a.Stats.source, "anovan"); %! assert_equal (a.NumObservations, 7); %! assert_equal (height (a.Residuals), 7); ## Numeric replicated models bypass anova2 when it cannot represent the terms. %!test %! x = [1, 5; 2, 6; 3, 7; 4, 8]; %! a = anova (x, [], "Reps", 2, ... %! "ModelSpecification", [1 0; 0 1], ... %! "FactorNames", {"A", "B"}); %! s = stats (a); %! assert_equal (s.Properties.RowNames, {"A"; "B"; "Error"; "Total"}); ## Raw residuals are unweighted observation-minus-fit differences. %!test %! g = [1; 1; 2; 2; 3; 3]; %! y = [1; 3; 4; 8; 9; 15]; %! a = anova (g, y, "Weights", [1; 2; 1; 3; 1; 4]); %! stats (a); %! assert_equal (a.Residuals.Raw, a.Y - a.FittedValues, 1e-12); %!error ... %! anova ([1; 1; 2; 2], (1:4)', "ModelSpecification", "purequadratic") %!error ... %! anova ([1;1;2;2], [1;2;3;4], "RandomFactors", 0) %!error ... %! anova ([1;1;2;2], [1;2;3;4], "RandomFactors", 2) %!error %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! plotDiagnostics (anova (popcorn, [], 'reps', 3)); %!error %! y = [10; 12; 11; 14; 16; 15; 9; 8; 10]; %! g = [1;1;1;2;2;2;3;3;3]; %! plotDiagnostics (anova (g, y, 'SumOfSquaresType', 'two'), 'Visible'); %!error %! y = [10; 12; 11; 14; 16; 15; 9; 8; 10]; %! g = [1;1;1;2;2;2;3;3;3]; %! plotDiagnostics (anova (g, y, 'SumOfSquaresType', 'two'), 'BadOption', true); %!error %! y = [10; 12; 11; 14; 16; 15; 9; 8; 10]; %! g = [1;1;1;2;2;2;3;3;3]; %! predict (anova (g, y, 'SumOfSquaresType', 'two'), ones (2, 2)); %!error %! a = anova ([1; 1; 2; 2], (1:4)'); %! stats (a, 'Component', 1); %!error %! a = anova ([1; 1; 2; 2], (1:4)'); %! stats (a, 'Component', 'typei'); %!error %! a = anova ([1; 1; 2; 2], (1:4)'); %! stats (a, 'details'); %!error %! a = anova ([1; 1; 2; 2], (1:4)'); %! stats (a, 'summary', 'one'); ## A saturated mixed model still yields every F ratio and p-value. %!test %! y = [444 614 423 625 408 856 447 719 ... %! 764 831 586 782 609 1002 606 766]' / 10; %! X1 = {'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola', ... %! 'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola'}'; %! X2 = {'C','T','C','T','C','T','C','T','C','T','C','T','C','T','C','T'}'; %! X3 = [1;1;1;1;1;1;1;1;2;2;2;2;2;2;2;2]; %! a = anova ({X1, X2, X3}, y, 'ModelSpecification', 'full', ... %! 'RandomFactors', 3, 'FactorNames', {'X1', 'X2', 'X3'}); %! s = stats (a); %! assert_equal (s.DF', [3, 1, 1, 3, 3, 1, 3, 0, 15]); %! assert_equal (s.SumOfSquares(8), 0); %! assert_equal (s.pValue(1:6), ... %! [0.288811428913179; 0.091455278902114; 0.042134889025806; ... %! 0.010944863181481; 0.061814763198376; 0.066584161056625], ... %! 1e-12); %! assert_equal (s.pValue(7), NaN); ## The denominator of each F ratio comes from the expected mean squares. %!test %! y = [444 614 423 625 408 856 447 719 ... %! 764 831 586 782 609 1002 606 766]' / 10; %! X1 = {'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola', ... %! 'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola'}'; %! X2 = {'C','T','C','T','C','T','C','T','C','T','C','T','C','T','C','T'}'; %! X3 = [1;1;1;1;1;1;1;1;2;2;2;2;2;2;2;2]; %! a = anova ({X1, X2, X3}, y, 'ModelSpecification', 'full', ... %! 'RandomFactors', 3, 'FactorNames', {'X1', 'X2', 'X3'}); %! [~, ems] = stats (a); %! assert_equal (ems.MeanSquaresDenominator(1:7), ... %! [47.1575; 47.61; 88.7925; 5.975; 5.975; 5.975; 0], 1e-9); %! assert_equal (ems.DFDenominator(1:7), ... %! [3; 1; 2.610727841363640; 3; 3; 3; 0], 1e-12); ## A term contained in another contributes to that term's expected mean square, ## whether the containing term is fixed or random. %!test %! y = [444 614 423 625 408 856 447 719 ... %! 764 831 586 782 609 1002 606 766]' / 10; %! X1 = {'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola', ... %! 'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola'}'; %! X2 = {'C','T','C','T','C','T','C','T','C','T','C','T','C','T','C','T'}'; %! X3 = [1;1;1;1;1;1;1;1;2;2;2;2;2;2;2;2]; %! a = anova ({X1, X2, X3}, y, 'ModelSpecification', 'full', ... %! 'RandomFactors', 3, 'FactorNames', {'X1', 'X2', 'X3'}); %! [~, ems] = stats (a); %! assert_equal (cellstr (ems.ExpectedMeanSquares), ... %! {'4*Q(X1)+2*Q(X1:X2)+2*V(X1:X3)+V(X1:X2:X3)+V(Error)'; ... %! '8*Q(X2)+2*Q(X1:X2)+4*V(X2:X3)+V(X1:X2:X3)+V(Error)'; ... %! '8*V(X3)+2*V(X1:X3)+4*V(X2:X3)+V(X1:X2:X3)+V(Error)'; ... %! '2*Q(X1:X2)+V(X1:X2:X3)+V(Error)'; ... %! '2*V(X1:X3)+V(X1:X2:X3)+V(Error)'; ... %! '4*V(X2:X3)+V(X1:X2:X3)+V(Error)'; ... %! 'V(X1:X2:X3)+V(Error)'; 'V(Error)'}); ## A saturated model reports no residual rather than an infinite one. %!test %! y = [3; 4; 7; 8]; %! [~, tbl] = anovan (y, {[1;1;2;2], [1;2;1;2]}, 'model', 'full', ... %! 'display', 'off'); %! assert_equal (tbl{end-1, 2}, 0); %! assert_equal (tbl{end-1, 3}, 0); %! assert_equal (tbl{end-1, 5}, 0); statistics-release-2.1.2/inst/Hypothesis_Testing/anova1.m000066400000000000000000000533261527311104300235060ustar00rootroot00000000000000## Copyright (C) 2021-2022 Andreas Bertsatos ## Copyright (C) 2022 Andrew Penn ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} anova1 (@var{x}) ## @deftypefnx {statistics} {@var{p} =} anova1 (@var{x}, @var{group}) ## @deftypefnx {statistics} {@var{p} =} anova1 (@var{x}, @var{group}, @var{displayopt}) ## @deftypefnx {statistics} {@var{p} =} anova1 (@var{x}, @var{group}, @var{displayopt}, @var{vartype}) ## @deftypefnx {statistics} {[@var{p}, @var{atab}] =} anova1 (@var{x}, @dots{}) ## @deftypefnx {statistics} {[@var{p}, @var{atab}, @var{stats}] =} anova1 (@var{x}, @dots{}) ## ## Perform a one-way analysis of variance (ANOVA) for comparing the means of two ## or more groups of data under the null hypothesis that the groups are drawn ## from distributions with the same mean. For planned contrasts and/or ## diagnostic plots, use @qcode{anovan} instead. ## ## anova1 can take up to three input arguments: ## ## @itemize ## @item ## @var{x} contains the data and it can either be a vector or matrix. ## If @var{x} is a matrix, then each column is treated as a separate group. ## If @var{x} is a vector, it is a single group whichever way it lies, unless ## @var{group} is given; with one group there is nothing to compare, so ## @var{p} is @qcode{NaN}. ## ## @item ## @var{group} contains the names for each group. If @var{x} is a matrix, then ## @var{group} can either be a cell array of strings of a character array, with ## one row per column of @var{x}. If you want to omit this argument, enter an ## empty array ([]). If @var{x} is a vector, then @var{group} must be a vector ## of the same length, or a string array or cell array of strings with one row ## for each element of @var{x}. @var{x} values corresponding to the same value ## of @var{group} are placed in the same group. ## ## @item ## @var{displayopt} is an optional parameter for displaying the groups contained ## in the data in a boxplot. If omitted, it is 'on' by default. If group names ## are defined in @var{group}, these are used to identify the groups in the ## boxplot. Use 'off' to omit displaying this figure. ## ## @item ## @var{vartype} is an optional parameter to used to indicate whether the ## groups can be assumed to come from populations with equal variance. When ## @qcode{vartype} is @qcode{'equal'} the variances are assumed to be equal ## (this is the default). When @qcode{vartype} is @qcode{'unequal'} the ## population variances are not assumed to be equal and Welch's ANOVA test is ## used instead. ## ## @var{vartype} is an Octave extension: MATLAB's @code{anova1} takes no ## fourth argument. It does not error on one either, it accepts it and ## ignores it, returning the same @var{p} and @var{F} for @qcode{'unequal'} ## as for @qcode{'equal'}. Code written against this function and then run ## in MATLAB therefore gets the classic ANOVA silently, with no diagnostic ## of any kind. Note that @code{anova2}'s analogous fourth argument does ## make MATLAB error, so the silence here is particular to @code{anova1}. ## @end itemize ## ## anova1 can return up to three output arguments: ## ## @itemize ## @item ## @var{p} is the p-value of the null hypothesis that all group means are equal. ## ## @item ## @var{atab} is a cell array containing the results in a standard ANOVA table. ## Its first row holds the column headings. For @qcode{'equal'} it is the ## classic partition, @qcode{'Source'}, @qcode{'SS'}, @qcode{'df'}, ## @qcode{'MS'}, @qcode{'F'} and @qcode{'Prob>F'}, with a row for the groups, ## the error and the total. For @qcode{'unequal'} it is @qcode{'Source'}, ## @qcode{'F'}, @qcode{'df'}, @qcode{'dfe'} and @qcode{'Prob>F'}, with one row ## for the groups, since Welch's test partitions no sum of squares and pools ## no error term; @qcode{'df'} and @qcode{'dfe'} are the numerator and the ## denominator degrees of freedom of its F statistic, the latter fractional. ## The two tables therefore differ in width and in what their columns mean. ## ## @item ## @var{stats} is a structure containing statistics useful for performing ## a multiple comparison of means with the MULTCOMPARE function. ## @end itemize ## ## A categorical @var{group} may declare levels that no observation uses. ## Such a level takes no part in the analysis and is dropped from every field ## of @var{stats}, so @code{n}, @code{means} and @code{gnames} always describe ## the same groups, in the same order, and can be indexed together. ## ## This is a deliberate deviation from MATLAB, which drops an unused level ## from @code{gnames} but keeps it in @code{n} and @code{means} as a count of ## zero and a mean of @code{NaN}. Those fields then disagree in length and ## the group indices run past @code{gnames}, so MATLAB's own @code{multcompare} ## reports comparisons against a group holding no observations and labels them ## with indices that its @code{gnames} cannot resolve. ## ## If anova1 is called without any output arguments, then it prints the results ## in a one-way ANOVA table to the standard output. It is also printed when ## @var{displayopt} is 'on'. ## ## ## Examples: ## ## @example ## x = meshgrid (1:6); ## x = x + normrnd (0, 1, 6, 6); ## anova1 (x, [], 'off'); ## [p, atab] = anova1(x); ## @end example ## ## ## @example ## x = ones (50, 4) .* [-2, 0, 1, 5]; ## x = x + normrnd (0, 2, 50, 4); ## groups = @{"A", "B", "C", "D"@}; ## anova1 (x, groups); ## @end example ## ## @seealso{anova2, anovan, multcompare} ## @end deftypefn function [p, anovatab, stats] = anova1 (x, group, displayopt, vartype) ## Check for valid number of input arguments if (nargin < 1 || nargin > 4) error ("anova1: invalid number of input arguments."); endif ## Add defaults if (nargin < 2) group = []; endif if (nargin < 3) displayopt = 'on'; endif if (nargin < 4) vartype = 'equal'; endif plotdata = ! (strcmp (displayopt, 'off')); ## Convert group to cell array from character array, make it a column if (! isempty (group) && ischar (group)) group = cellstr (group); endif if (size (group, 1) == 1) group = group'; endif ## A vector is one group whichever way it lies, and each column of a matrix ## is a group of its own. With GROUP given, a vector is grouped by it ## instead, one element per observation. if (isvector (x)) x = x(:); endif if (isempty (group) || ! isvector (x)) [n, m] = size (x); x = x(:); gi = reshape (repmat ((1:m), n, 1), n*m, 1); if (length (group) == 0) ## no group names are provided group = gi; elseif (size (group, 1) == m) ## group names exist and match columns group = group(gi,:); else error ("anova1: columns in X and GROUP length do not match."); endif endif ## Check that x and group are the same size if (numel (x) != prod (size (group))) error ("anova1: GROUP must be a vector with the same number of rows as x."); endif ## Convert group to indices and separate names first if (isa (group, "categorical")) group_id = double (group(:)); group_names = categories (group); else [group_id, group_names] = grp2idx (group); group_id = group_id(:); endif x = x(:); ## identify NaN values in x or missing/empty categories in the group and remove them. valid_data = ! isnan (x) & ! isnan (group_id); x = x(valid_data); group_id = group_id(valid_data); ## A declared but unused categorical level, or one emptied by NaN removal, ## is dropped from the design and from every field of STATS, so the group ## counts, means and names stay the same length. MATLAB drops it from the ## names alone; see the deviation noted in the help text above. groups = size (group_names, 1); xs = accumarray (group_id, 1, [groups, 1], @sum, 0); observed_groups = (xs > 0); if (any (! observed_groups)) group_map = cumsum (observed_groups); group_id = group_map(group_id); group_names = group_names(observed_groups, :); xs = xs(observed_groups); endif named = 1; ## Center data to improve accuracy and keep uncentered data for plotting xorig = x; mu = mean (x); x = x - mu; xr = x; ## Get group size and mean for each group groups = size (group_names, 1); xsum = accumarray (group_id, xr, [groups, 1], @sum, 0); xm = xsum ./ xs; xdev = xr - xm(group_id); xss = accumarray (group_id, xdev .^ 2, [groups, 1], @sum, 0); xv = xss ./ max (xs - 1, 1); xv(xs == 0) = NaN; xs = xs'; xm = xm'; xv = xv'; ## Calculate statistics lx = length (xr); ## Number of samples in groups gm = mean (xr); ## Grand mean of groups dfm = length (xm) - 1; ## degrees of freedom for model dfe = lx - dfm - 1; ## degrees of freedom for error ## With no observations there is nothing to partition. Counting the groups ## less one, and the samples less that, puts both below zero, which has no ## reading; MATLAB reports the negative numbers. if (lx == 0) dfm = 0; dfe = 0; endif SSM = xs .* (xm - gm) * (xm - gm)'; ## Sum of Squares for Model SST = (xr(:) - gm)' * (xr(:) - gm); ## Sum of Squares Total SSE = SST - SSM; ## Sum of Squares Error if (dfm > 0) MSM = SSM / dfm; ## Mean Square for Model else MSM = NaN; endif if (dfe > 0) MSE = SSE / dfe; ## Mean Square for Error else MSE = NaN; endif ## Calculate F statistic if (dfm <= 0) ## No between-group degrees of freedom. F = NaN; p = NaN; elseif (SSE != 0) ## Regular Matrix case. switch (lower (vartype)) case 'equal' ## Assume equal variances (Fisher's One-way ANOVA) F = (SSM / dfm) / MSE; case 'unequal' ## Accommodate for unequal variances (Welch's One-way ANOVA) ## Calculate the sampling variance for each group (i.e. the square of the SEM) sv = xv ./ xs; ## Calculate weights as the reciprocal of the sampling variance w = 1 ./ sv; ## Calculate the origin ori = sum (w .* xm) ./ sum (w); ## Calculate Welch's F statistic F = (groups - 1)^-1 * sum (w .* (xm - ori).^2) /... (1 + ((2 * (groups - 2)/(groups^2 - 1)) * ... sum ((1 - w / sum (w)).^2 .* (xs - 1).^-1))); ## Welch's test does not use a pooled error term MSE = NaN; ## Correct the error degrees of freedom dfe = (3 /(groups^2 - 1) * sum ((1 - w / sum (w)).^2 .* (xs-1).^-1))^-1; otherwise error ("anova1: invalid fourth (vartype) argument to anova1."); endswitch p = fcdf (F, dfm, dfe, 'upper'); ## Probability of F given equal means. elseif (SSM == 0) ## Constant Matrix case. ## Both sums-of-squares vanish, so F is 0/0. There is no within-group ## scale to test against, and the limit depends on the direction of ## approach, so the statistic is undefined rather than zero. F = NaN; p = NaN; else ## Perfect fit case. F = Inf; p = 0; endif ## Create results table (if requested) if (nargout > 1) switch (lower (vartype)) case 'equal' anovatab = {'Source', 'SS', 'df', 'MS', 'F', 'Prob>F'; ... 'Groups', SSM, dfm, MSM, F, p; ... 'Error', SSE, dfe, MSE, '', ''; ... 'Total', SST, dfm + dfe, '', '', ''}; case 'unequal' anovatab = {'Source', 'F', 'df', 'dfe', 'Prob>F'; ... 'Groups', F, dfm, dfe, p}; endswitch endif ## Create stats structure (if requested) for MULTCOMPARE if (nargout > 2) if (length (group_names) > 0) stats.gnames = group_names; else stats.gnames = strjust (num2str ((1:length (xm))'), 'left'); endif stats.n = xs; stats.source = 'anova1'; stats.vartype = vartype; stats.means = xm + mu; stats.vars = xv; stats.df = dfe; stats.s = sqrt (MSE); endif ## Print results table on screen if no output argument was requested if (nargout == 0 || plotdata) switch (lower (vartype)) case 'equal' printf ("\n ANOVA Table\n\n"); printf ("Source SS df MS F Prob>F\n"); printf ("------------------------------------------------------\n"); printf ("Groups %10.4f %5.0f %10.4f %8.2f %9.4f\n", SSM, dfm, MSM, F, p); printf ("Error %10.4f %5.0f %10.4f\n", SSE, dfe, MSE); printf ("Total %10.4f %5.0f\n\n", SST, dfm + dfe); case 'unequal' printf ("\n Welch's ANOVA Table\n\n"); printf ("Source F df dfe Prob>F\n"); printf ("-----------------------------------------\n"); printf ("Groups %8.2f %5.0f %7.2f %10.4f\n\n", F, dfm, dfe, p); endswitch endif ## Plot data using BOXPLOT (unless opted out) if (plotdata) ## Create a new figure and ensure it becomes active f = figure (); set (0, 'currentfigure', f); ## Create a new axes inside this figure and make it current ax = axes ('parent', f); set (f, 'currentaxes', ax); ## Now boxplot will ALWAYS draw here boxplot (x, group_id, 'Notch', 'on', 'Labels', group_names); endif endfunction %!demo %! rng (42); %! x = meshgrid (1:6); %! x = x + normrnd (0, 1, 6, 6); %! anova1 (x, [], 'off'); %!demo %! rng (42); %! x = meshgrid (1:6); %! x = x + normrnd (0, 1, 6, 6); %! [p, atab] = anova1 (x); %!demo %! rng (42); %! x = ones (50, 4) .* [-2, 0, 1, 5]; %! x = x + normrnd (0, 2, 50, 4); %! groups = {'A', 'B', 'C', 'D'}; %! anova1 (x, groups); %!demo %! y = [54 87 45; 23 98 39; 45 64 51; 54 77 49; 45 89 50; 47 NaN 55]; %! g = [1 2 3 ; 1 2 3 ; 1 2 3 ; 1 2 3 ; 1 2 3 ; 1 2 3 ]; %! anova1 (y(:), g(:), 'on', 'unequal'); ## testing against GEAR.DAT data file and results for one-factor ANOVA from ## https://www.itl.nist.gov/div898/handbook/eda/section3/eda354.htm %!test %! data = [1.006, 0.996, 0.998, 1.000, 0.992, 0.993, 1.002, 0.999, 0.994, 1.000, ... %! 0.998, 1.006, 1.000, 1.002, 0.997, 0.998, 0.996, 1.000, 1.006, 0.988, ... %! 0.991, 0.987, 0.997, 0.999, 0.995, 0.994, 1.000, 0.999, 0.996, 0.996, ... %! 1.005, 1.002, 0.994, 1.000, 0.995, 0.994, 0.998, 0.996, 1.002, 0.996, ... %! 0.998, 0.998, 0.982, 0.990, 1.002, 0.984, 0.996, 0.993, 0.980, 0.996, ... %! 1.009, 1.013, 1.009, 0.997, 0.988, 1.002, 0.995, 0.998, 0.981, 0.996, ... %! 0.990, 1.004, 0.996, 1.001, 0.998, 1.000, 1.018, 1.010, 0.996, 1.002, ... %! 0.998, 1.000, 1.006, 1.000, 1.002, 0.996, 0.998, 0.996, 1.002, 1.006, ... %! 1.002, 0.998, 0.996, 0.995, 0.996, 1.004, 1.004, 0.998, 0.999, 0.991, ... %! 0.991, 0.995, 0.984, 0.994, 0.997, 0.997, 0.991, 0.998, 1.004, 0.997]; %! group = [1:10] .* ones (10,10); %! group = group(:); %! [p, tbl] = anova1 (data, group, 'off'); %! assert_equal (p, 0.022661, 1e-6); %! assert_equal (tbl{2,5}, 2.2969, 1e-4); %! assert_equal (tbl{2,3}, 9, 0); %! assert_equal (tbl{4,2}, 0.003903, 1e-6); %! data = reshape (data, 10, 10); %! [p, tbl, stats] = anova1 (data, [], 'off'); %! assert_equal (p, 0.022661, 1e-6); %! assert_equal (tbl{2,5}, 2.2969, 1e-4); %! assert_equal (tbl{2,3}, 9, 0); %! assert_equal (tbl{4,2}, 0.003903, 1e-6); %! means = [0.998, 0.9991, 0.9954, 0.9982, 0.9919, 0.9988, 1.0015, 1.0004, 0.9983, 0.9948]; %! N = 10 * ones (1, 10); %! assert_equal (stats.means, means, 1e-6); %! assert_equal (length (stats.gnames), 10, 0); %! assert_equal (stats.n, N, 0); ## testing against one-way ANOVA example dataset from GraphPad Prism 8 %!test %! y = [54 87 45; 23 98 39; 45 64 51; 54 77 49; 45 89 50; 47 NaN 55]; %! g = [1 2 3 ; 1 2 3 ; 1 2 3 ; 1 2 3 ; 1 2 3 ; 1 2 3 ]; %! [p, tbl] = anova1 (y(:), g(:), 'off', 'equal'); %! assert_equal (p, 0.00004163, 1e-6); %! assert_equal (tbl(1,:), {'Source', 'SS', 'df', 'MS', 'F', 'Prob>F'}); %! assert_equal (tbl{2,5}, 22.573418, 1e-6); %! assert_equal (tbl{2,3}, 2, 0); %! assert_equal (tbl{3,3}, 14, 0); %! [p, tbl] = anova1 (y(:), g(:), 'off', 'unequal'); %! assert_equal (p, 0.00208877, 1e-8); %! assert_equal (size (tbl), [2, 5]); %! assert_equal (tbl(1,:), {'Source', 'F', 'df', 'dfe', 'Prob>F'}); %! assert_equal (tbl{2,2}, 15.523192, 1e-6); %! assert_equal (tbl{2,3}, 2, 0); %! assert_equal (tbl{2,4}, 7.5786897, 1e-6); ## testing against one-way ANOVA example dataset from GraphPad Prism 8 ## using categorical array as a grouping variable %!test %! y = [54, 87, 45; 23, 98, 39; 45, 64, 51; ... %! 54, 77, 49; 45, 89, 50; 47, NaN, 55]; %! g = categorical ([1, 2, 3; 1, 2, 3; 1, 2, 3; 1, 2, 3; 1, 2, 3; 1, 2, 3]); %! [p, tbl] = anova1 (y(:), g(:), 'off', 'equal'); %! assert_equal (p, 0.00004163, 1e-6); %! assert_equal (tbl(1,:), {'Source', 'SS', 'df', 'MS', 'F', 'Prob>F'}); %! assert_equal (tbl{2,5}, 22.573418, 1e-6); %! assert_equal (tbl{2,3}, 2, 0); %! assert_equal (tbl{3,3}, 14, 0); %! [p, tbl] = anova1 (y(:), g(:), 'off', 'unequal'); %! assert_equal (p, 0.00208877, 1e-8); %! assert_equal (size (tbl), [2, 5]); %! assert_equal (tbl(1,:), {'Source', 'F', 'df', 'dfe', 'Prob>F'}); %! assert_equal (tbl{2,2}, 15.523192, 1e-6); %! assert_equal (tbl{2,3}, 2, 0); %! assert_equal (tbl{2,4}, 7.5786897, 1e-6); ## testing handling of missing values in both data and grouping variables %!test %! y = [10; 20; 9999; NaN; 40; 50]; %! g = [1; 1; NaN; 1; 2; 2]; %! [p, tbl, stats] = anova1 (y, g, 'off'); %! assert_equal (p, 0.051317, 1e-6); %! assert_equal (tbl{2,5}, 18, 1e-6); %! assert_equal (tbl{2,3}, 1, 0); %! assert_equal (tbl{3,3}, 2, 0); %! assert_equal (tbl{4,3}, 3, 0); %! assert_equal (stats.n, [2, 2], 0); ## Grouped variance remains stable when group means have large offsets. %!test %! y = [1e12 + (1:10), -1e12 + (1:10)](:); %! g = [ones(10, 1); 2 * ones(10, 1)]; %! [~, ~, stats] = anova1 (y, g, 'off', 'unequal'); %! assert_equal (stats.vars, [55 / 6, 55 / 6], 1e-10); ## A single group leaves no between-group degrees of freedom. %!test %! [p, tbl] = anova1 ((1:5)', ones (5, 1), 'off'); %! assert_equal (p, NaN); %! assert_equal (tbl{2, 3}, 0); %! assert_equal (tbl{3, 2}, 10); %! [p, tbl] = anova1 (7, 1, 'off'); %! assert_equal (p, NaN); %! assert_equal (tbl{3, 3}, 0); ## One observation per group leaves no residual, so the fit is exact. %!test %! [p, tbl] = anova1 ([1; 2], [1; 2], 'off'); %! assert_equal (p, 0); %! assert_equal (tbl{2, 2}, 0.5); %! assert_equal (tbl{2, 3}, 1); %! assert_equal (tbl{3, 2}, 0); %! assert_equal (tbl{3, 3}, 0); ## Identical observations leave no scale to test against, so F is 0/0. %!test %! [p, tbl] = anova1 (ones (6, 1), [1; 1; 1; 2; 2; 2], 'off'); %! assert_equal (p, NaN); %! assert_equal (tbl{2, 5}, NaN); %! assert_equal (tbl{2, 2}, 0); %! assert_equal (tbl{2, 3}, 1); %! assert_equal (tbl{3, 2}, 0); %! assert_equal (tbl{3, 3}, 4); ## An unused categorical level leaves every field of stats, not just ## gnames as in MATLAB, so the three stay indexable together. %!test %! y = (1:6)'; %! g = categorical ([1; 1; 3; 3; 3; 1], [1, 2, 3]); %! [p, tbl, stats] = anova1 (y, g, 'off'); %! [p_ref, tbl_ref] = anova1 (y, [1; 1; 2; 2; 2; 1], 'off'); %! assert_equal (p, p_ref, 1e-12); %! assert_equal (tbl, tbl_ref); %! assert_equal (stats.n, [3, 3]); %! assert_equal (stats.gnames, {'1'; '3'}); %! assert_equal (stats.means, [3, 4]); %! g = categorical ([3; 3; 1; 1], [3, 2, 1]); %! [~, ~, stats] = anova1 ((1:4)', g, 'off'); %! assert_equal (stats.gnames, {'3'; '1'}); %! assert_equal (stats.means, [1.5, 3.5]); ## Many factor levels retain the expected degrees of freedom. %!test %! g = kron ((1:120)', ones (2, 1)); %! [p, tbl, stats] = anova1 ((1:240)', g, 'off'); %! assert_equal (isfinite (p), true); %! assert_equal (tbl{2, 3}, 119); %! assert_equal (tbl{3, 3}, 120); %! assert_equal (stats.n, 2 * ones (1, 120)); ## testing handling of missing values in both data and grouping variables ## using categorical array as a grouping variable %!test %! y = [10; 20; 9999; NaN; 40; 50]; %! g = categorical ([1; 1; NaN; 1; 2; 2]); %! [p, tbl, stats] = anova1 (y, g, 'off'); %! assert_equal (p, 0.051317, 1e-6); %! assert_equal (tbl{2,5}, 18, 1e-6); %! assert_equal (tbl{2,3}, 1, 0); %! assert_equal (tbl{3,3}, 2, 0); %! assert_equal (tbl{4,3}, 3, 0); %! assert_equal (stats.n, [2, 2], 0); ## A vector without GROUP is one group whichever way it lies, and an empty X ## is answered rather than raised, with no degrees of freedom below zero. %!test %! [p, tbl] = anova1 ((1:5)', [], 'off'); %! assert_equal (p, NaN); %! assert_equal (cell2mat (tbl(2:4,2:3)), [0, 0; 10, 4; 10, 4]); %!test %! [p, tbl] = anova1 (1:5, [], 'off'); %! assert_equal (p, NaN); %! assert_equal (cell2mat (tbl(2:4,2:3)), [0, 0; 10, 4; 10, 4]); %!test %! [p, tbl, stats] = anova1 ((1:5)', [], 'off'); %! assert_equal (stats.n, 5); %! assert_equal (stats.means, 3); %! assert_equal (stats.df, 4); %!test %! [p, tbl] = anova1 ([], [], 'off'); %! assert_equal (p, NaN); %! assert_equal (cell2mat (tbl(2:4,2:3)), zeros (3, 2)); %!test %! [p, tbl] = anova1 (zeros (0, 3), [], 'off'); %! assert_equal (p, NaN); %! assert_equal (cell2mat (tbl(2:4,2:3)), zeros (3, 2)); %!test %! ## Below the resolution of 1 - fcdf, values from MATLAB R2024a %! p = anova1 ([1:10, 101:110, 201:210]', kron ((1:3)', ones (10, 1)), 'off'); %! assert_equal (p, 5.522164708181e-40, -1e-10); statistics-release-2.1.2/inst/Hypothesis_Testing/anova2.m000066400000000000000000000411171527311104300235020ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## Copyright (C) 2022 Andrew Penn ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} anova2 (@var{x}, @var{reps}) ## @deftypefnx {statistics} {@var{p} =} anova2 (@var{x}, @var{reps}, @var{displayopt}) ## @deftypefnx {statistics} {@var{p} =} anova2 (@var{x}, @var{reps}, @var{displayopt}, @var{model}) ## @deftypefnx {statistics} {[@var{p}, @var{atab}] =} anova2 (@dots{}) ## @deftypefnx {statistics} {[@var{p}, @var{atab}, @var{stats}] =} anova2 (@dots{}) ## ## Performs two-way factorial (crossed) or a nested analysis of variance ## (ANOVA) for balanced designs. For unbalanced factorial designs, diagnostic ## plots and/or planned contrasts, use @qcode{anovan} instead. ## ## @qcode{anova2} requires two input arguments with an optional third and ## fourth: ## ## @itemize ## @item ## @var{x} contains the data and it must be a matrix of at least two columns and ## two rows. @code{NaN} values are not accepted, since @code{anova2} requires a ## balanced design; use @code{anovan} for data with missing observations. An ## empty @var{x} is an error, whereas MATLAB returns @code{NaN} p-values. ## ## @item ## @var{reps} is the number of replicates for each combination of factor groups. ## ## @item ## @var{displayopt} is an optional parameter for displaying the ANOVA table, ## when it is 'on' (default) and suppressing the display when it is 'off'. ## ## @item ## @var{model} is an optional parameter to specify the model type as either: ## ## @itemize ## @item ## "interaction" or "full" (default): compute both main effects and their ## interaction ## ## @item ## "linear": compute both main effects without an interaction. When @var{reps} ## > 1 the test is suitable for a balanced randomized block design. When ## @var{reps} == 1, the test becomes a One-way Repeated Measures (RM)-ANOVA ## with Greenhouse-Geisser correction to the column factor degrees of freedom ## to make the test robust to violations of sphericity ## ## @item ## "nested": treat the row factor as nested within columns. Note that the row ## factor is considered a random factor in the calculation of the statistics. ## ## @end itemize ## @end itemize ## ## @qcode{anova2} returns up to three output arguments: ## ## @itemize ## @item ## @var{p} is the p-value of the null hypothesis that all group means are equal. ## ## @item ## @var{atab} is a cell array containing the results in a standard ANOVA table. ## ## @item ## @var{stats} is a structure containing statistics useful for performing ## a multiple comparison of means with the MULTCOMPARE function. ## @end itemize ## ## If anova2 is called without any output arguments, then it prints the results ## in a one-way ANOVA table to the standard output as if @var{displayopt} is ## 'on'. ## ## Examples: ## ## @example ## load popcorn; ## anova2 (popcorn, 3); ## @end example ## ## ## @example ## [p, anovatab, stats] = anova2 (popcorn, 3, "off"); ## disp (p); ## @end example ## ## @seealso{anova1, anovan, multcompare} ## @end deftypefn function [p, anovatab, stats] = anova2 (x, reps, displayopt, model) ## Check for valid number of input arguments if (nargin < 1 || nargin >4) error ("anova2: invalid number of input arguments."); endif ## Check for empty X if (isempty (x)) error ("anova2: X must not be empty."); endif ## Check for NaN values in X if (any (isnan ( x(:)))) error ("anova2: NaN values in input are not allowed. Use anovan instead."); endif ## Add defaults if (nargin == 1) reps = 1; endif if (nargin < 3) displayopt = 'on'; endif if (nargin < 4) model = 'interaction'; endif epsilonhat = []; plotdata = ! (strcmp (displayopt, 'off')); ## Calculate group numbers FFGn = size (x, 1) / reps; ## Number of groups in Row Factor SFGn = size (x, 2); ## Number of groups in Column Factor ## Check for valid repetitions if (! (int16 (FFGn) == FFGn)) error ("anova2: the number of rows in X must be a multiple of REPS."); endif idx_s = 1; idx_e = reps; for i = 1:FFGn RIdx(i,:) = [idx_s:idx_e]; idx_s += reps; idx_e += reps; endfor ## Calculate group sample sizes GTsz = length (x(:)); ## Number of total samples FFGs = prod (size (x(RIdx(1,:),:))); ## Number of group samples of Row Factor SFGs = size (x, 1); ## Number of group samples of Column Factor ## Calculate group means GTmu = sum (x(:)) / GTsz; ## Grand mean of groups for i = 1:FFGn ## Group means of Row Factor FFGm(i) = mean (x(RIdx(i,:),:), 'all'); endfor for i = 1:SFGn ## Group means of Column Factor SFGm(i) = mean (x(:,i)); endfor ## Calculate Sum of Squares for Row and Column Factors SSR = sum (FFGs * ((FFGm - GTmu) .^ 2)); ## Rows Sum of Squares SSC = sum (SFGs * ((SFGm - GTmu) .^ 2)); ## Columns Sum of Squares ## Calculate Total Sum of Squares SST = (x(:) - GTmu)' * (x(:) - GTmu); ## Calculate Sum of Squares Error (Within) if (reps > 1) xcells = reshape (x, reps, FFGn, SFGn); xdev = bsxfun (@minus, xcells, mean (xcells, 1)); cell_sse = squeeze (sumsq (xdev, 1)); SSE = sum (cell_sse(:)); else SSE = SST - SSC - SSR; endif ## Calculate degrees of freedom and Sum of Squares Interaction (if applicable) df_SSR = FFGn - 1; ## Row Factor df_SSC = SFGn - 1; ## Column Factor if (reps > 1) df_SSE = GTsz - (FFGn * SFGn); ## Error with replication df_SSI = df_SSR * df_SSC; ## Interaction: Degrees of Freedom SSI = SST - SSR - SSC - SSE; ## Interaction: Sum of Squares else df_SSE = df_SSR * df_SSC; ## No replication, assuming additive model df_SSI = 0; SSI = 0; endif df_tot = GTsz - 1; ## Total ## Model-specific calculations of sums-of-squares, mean squares and degrees of ## freedom. The calculations are based on equalities for the partitioning of ## variance in fully balanced designs. switch (lower (model)) case {'interaction', 'full'} ## TWO-WAY ANOVA WITH INTERACTION (full factorial model) ## Sums--of-squares are already partitioned into main effects and ## interaction. Just calculate mean-squares and degrees of freedom model = 'interaction'; MSE = SSE / df_SSE; ## Mean Square for Error (Within) MSR = SSR / df_SSR; ## Mean Square for Row Factor MS_DENOM = MSE; df_DENOM = df_SSE; case 'linear' ## TWO-WAY ANOVA WITHOUT INTERACTION (additive, linear model) ## Pool Error and Interaction term model = 'linear'; SSE += SSI; df_SSE += df_SSI; SSI = 0; df_SSI = 0; if (reps == 1) ## Assume one-way repeated measures ANOVA. Perform calculations for a ## correction factor (epsilonhat) to make tests of the Column factor ## robust to violations of sphericity vcov = cov (x); N = SFGn^2 * (mean (diag (vcov)) - mean (mean (vcov)))^2; D = (SFGn - 1) * ... (sum (sumsq (vcov)) - 2 * SFGn * sum ((mean (vcov, 2).^2)) + ... SFGn^2 * mean (mean (vcov))^2); epsilonhat = N / D; dfN_GG = epsilonhat * (SFGn - 1); dfD_GG = epsilonhat * (FFGn - 1) * (SFGn - 1); endif reps = 1; ## Set reps to 1 to avoid printing interaction MSE = SSE / df_SSE; ## Mean Square for Error (Within) MSR = SSR / df_SSR; ## Mean Square for Row Factor MS_DENOM = MSE; df_DENOM = df_SSE; case 'nested' ## NESTED ANOVA ## Row Factor is nested within Column Factor. Treat Row factor as random. ## Pool Row Factor and Interaction term model = 'nested'; SSR += SSI; df_SSR += df_SSI; SSI = 0; df_SSI = 0; reps = 1; ## Set reps to 1 to avoid printing interaction MSE = SSE / df_SSE; ## Mean Square for Error (Within) MSR = SSR / df_SSR; ## Mean Square for Row Factor MS_DENOM = MSR; ## Row factor is random so MSR is denominator df_DENOM = df_SSR; ## Row factor is random so df_SSR is denominator otherwise error ("anova2: model type not recognised"); endswitch ## Calculate F statistics and p values F_MSR = MSR / MSE; ## F statistic for Row Factor p_MSR = fcdf (F_MSR, df_SSR, df_SSE, 'upper'); MSC = SSC / df_SSC; ## Mean Square for Column Factor F_MSC = MSC / MS_DENOM; ## F statistic for Column Factor if (isempty (epsilonhat)) p_MSC = fcdf (F_MSC, df_SSC, df_DENOM, 'upper'); else ## Apply correction for sphericity to the p-value of the column factor p_MSC = fcdf (F_MSC, dfN_GG, dfD_GG, 'upper'); endif ## With replication if (reps > 1) MSI = SSI / df_SSI; ## Mean Square for Interaction F_MSI = MSI / MSE; ## F statistic for Interaction p_MSI = fcdf (F_MSI, df_SSI, df_SSE, 'upper'); else MSI = 0; F_MSI = 0; p_MSI = NaN; endif ## Create p output (if requested) if (nargout > 0) if (reps > 1) p = [p_MSC, p_MSR, p_MSI]; else p = [p_MSC, p_MSR]; endif endif ## Create results table (if requested) if (nargout > 1 && reps > 1) anovatab = {'Source', 'SS', 'df', 'MS', 'F', 'Prob>F'; ... 'Columns', SSC, df_SSC, MSC, F_MSC, p_MSC; ... 'Rows', SSR, df_SSR, MSR, F_MSR, p_MSR; ... 'Interaction', SSI, df_SSI, MSI, F_MSI, p_MSI; ... 'Error', SSE, df_SSE, MSE, '', ''; ... 'Total', SST, df_tot, '', '', ''}; elseif (nargout > 1 && reps == 1) anovatab = {'Source', 'SS', 'df', 'MS', 'F', 'Prob>F'; ... 'Columns', SSC, df_SSC, MSC, F_MSC, p_MSC; ... 'Rows', SSR, df_SSR, MSR, F_MSR, p_MSR; ... 'Error', SSE, df_SSE, MSE, '', ''; ... 'Total', SST, df_tot, '', '', ''}; endif ## Create stats structure (if requested) for MULTCOMPARE if (nargout > 2) stats.source = 'anova2'; stats.sigmasq = MS_DENOM; ## MS used to calculate F relating to stats.pval stats.colmeans = SFGm(:)'; stats.coln = SFGs; stats.rowmeans = FFGm(:)'; stats.rown = FFGs; stats.inter = (reps > 1); if stats.inter stats.pval = p_MSI; ## Interaction p-value if stats.inter is true else stats.pval = p_MSC; ## Column Factor p-value if stats.inter is false endif stats.df = df_DENOM; ## Degrees of freedom used to calculate stats.pval stats.model = model; endif ## Print results table on screen if no output argument was requested if (nargout == 0 || plotdata) printf ("\n ANOVA Table\n\n"); printf ("Source SS df MS F Prob>F\n"); printf ("-----------------------------------------------------------\n"); printf ("Columns %10.4f %5.0f %10.4f %8.2f %9.4f\n", ... SSC, df_SSC, MSC, F_MSC, p_MSC); printf ("Rows %10.4f %5.0f %10.4f %8.2f %9.4f\n", ... SSR, df_SSR, MSR, F_MSR, p_MSR); if (reps > 1) printf ("Interaction %10.4f %5.0f %10.4f %8.2f %9.4f\n", ... SSI, df_SSI, MSI, F_MSI, p_MSI); endif printf ("Error %10.4f %5.0f %10.4f\n", SSE, df_SSE, MSE); printf ("Total %10.4f %5.0f\n\n", SST, df_tot); if (! isempty (epsilonhat)) printf (strcat ("Note: Greenhouse-Geisser's correction was applied to the\n", ... "degrees of freedom for the Column factor: F(%.2f,%.2f)\n\n"), ... dfN_GG, dfD_GG); endif if (strcmpi (model, 'nested')) printf (strcat ("Note: Rows are a random factor nested within the columns.\n", ... "The Column F statistic uses the Row MS instead of the MSE.\n\n")); endif endif endfunction %!demo %! %! # Factorial (Crossed) Two-way ANOVA with Interaction %! %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! %! [p, atab, stats] = anova2 (popcorn, 3, 'on'); %!demo %! %! # One-way Repeated Measures ANOVA (Rows are a crossed random factor) %! %! data = [54, 43, 78, 111; %! 23, 34, 37, 41; %! 45, 65, 99, 78; %! 31, 33, 36, 35; %! 15, 25, 30, 26]; %! %! [p, atab, stats] = anova2 (data, 1, 'on', 'linear'); %!demo %! %! # Balanced Nested One-way ANOVA (Rows are a nested random factor) %! %! data = [4.5924 7.3809 21.322; -0.5488 9.2085 25.0426; ... %! 6.1605 13.1147 22.66; 2.3374 15.2654 24.1283; ... %! 5.1873 12.4188 16.5927; 3.3579 14.3951 10.2129; ... %! 6.3092 8.5986 9.8934; 3.2831 3.4945 10.0203]; %! %! [p, atab, stats] = anova2 (data, 4, 'on', 'nested'); ## testing against popcorn data and results from Matlab %!test %! ## Test for anova2 ("interaction") %! ## comparison with results from Matlab for column effect %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! [p, atab, stats] = anova2 (popcorn, 3, 'off'); %! assert_equal (p(1), 7.678957383294716e-07, 1e-14); %! assert_equal (p(2), 0.0001003738963050171, 1e-14); %! assert_equal (p(3), 0.7462153966366274, 1e-14); %! assert_equal (atab{2,5}, 56.700, 1e-14); %! assert_equal (atab{2,3}, 2, 0); %! assert_equal (atab{4,2}, 0.08333333333333348, 1e-14); %! assert_equal (atab{5,4}, 0.1388888888888889, 1e-14); %! assert_equal (atab{5,2}, 1.666666666666667, 1e-14); %! assert_equal (atab{6,2}, 22); %! assert_equal (stats.source, "anova2"); %! assert_equal (stats.colmeans, [6.25, 4.75, 4]); %! assert_equal (stats.inter, true); %! assert_equal (stats.pval, 0.7462153966366274, 1e-14); %! assert_equal (stats.df, 12); %!test %! ## Test for anova2 ("linear") - comparison with results from GraphPad Prism 8 %! data = [54, 43, 78, 111; %! 23, 34, 37, 41; %! 45, 65, 99, 78; %! 31, 33, 36, 35; %! 15, 25, 30, 26]; %! [p, atab, stats] = anova2 (data, 1, 'off', 'linear'); %! assert_equal (atab{2,2}, 2174.95, 1e-10); %! assert_equal (atab{3,2}, 8371.7, 1e-10); %! assert_equal (atab{4,2}, 2404.3, 1e-10); %! assert_equal (atab{5,2}, 12950.95, 1e-10); %! assert_equal (atab{2,4}, 724.983333333333, 1e-10); %! assert_equal (atab{3,4}, 2092.925, 1e-10); %! assert_equal (atab{4,4}, 200.358333333333, 1e-10); %! assert_equal (atab{2,5}, 3.61843363972882, 1e-10); %! assert_equal (atab{3,5}, 10.445909412303, 1e-10); %! assert_equal (atab{2,6}, 0.087266112738617, 1e-10); %! assert_equal (atab{3,6}, 0.000698397753556, 1e-10); %!test %! ## Test for anova2 ("nested") - comparison with results from GraphPad Prism 8 %! data = [4.5924 7.3809 21.322; -0.5488 9.2085 25.0426; ... %! 6.1605 13.1147 22.66; 2.3374 15.2654 24.1283; ... %! 5.1873 12.4188 16.5927; 3.3579 14.3951 10.2129; ... %! 6.3092 8.5986 9.8934; 3.2831 3.4945 10.0203]; %! [p, atab, stats] = anova2 (data, 4, 'off', 'nested'); %! assert_equal (atab{2,2}, 745.360306290833, 1e-10); %! assert_equal (atab{3,2}, 278.01854140125, 1e-10); %! assert_equal (atab{4,2}, 180.180377467501, 1e-10); %! assert_equal (atab{5,2}, 1203.55922515958, 1e-10); %! assert_equal (atab{2,4}, 372.680153145417, 1e-10); %! assert_equal (atab{3,4}, 92.67284713375, 1e-10); %! assert_equal (atab{4,4}, 10.0100209704167, 1e-10); %! assert_equal (atab{2,5}, 4.02146005730833, 1e-10); %! assert_equal (atab{3,5}, 9.25800729165627, 1e-10); %! assert_equal (atab{2,6}, 0.141597630656771, 1e-10); %! assert_equal (atab{3,6}, 0.000636643812875719, 1e-10); %!test %! ## Below the resolution of 1 - fcdf, values from MATLAB R2024a %! q = (1:6)'; %! p = anova2 ([q, q + 100, q + 200] + 0.1 * sin (reshape (1:18, 6, 3)), 1, ... %! 'off'); %! assert_equal (p, [6.92646657901359e-38, 7.28337250041466e-20], -1e-6); ## Test input validation %!error anova2 ([], 1, 'off') %!error anova2 (zeros (0, 3), 2, 'off') %!error anova2 (zeros (3, 0), 1, 'off') statistics-release-2.1.2/inst/Hypothesis_Testing/anovan.m000066400000000000000000003244301527311104300236000ustar00rootroot00000000000000## Copyright (C) 2003-2005 Andy Adler ## Copyright (C) 2021 Christian Scholz ## Copyright (C) 2022 Andreas Bertsatos ## Copyright (C) 2022 Andrew Penn ## Copyright (C) 2024 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} anovan (@var{Y}, @var{GROUP}) ## @deftypefnx {statistics} {@var{p} =} anovan (@var{Y}, @var{GROUP}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{p}, @var{atab}] =} anovan (@dots{}) ## @deftypefnx {statistics} {[@var{p}, @var{atab}, @var{stats}] =} anovan (@dots{}) ## @deftypefnx {statistics} {[@var{p}, @var{atab}, @var{stats}, @var{terms}] =} anovan (@dots{}) ## ## Perform a multi (N)-way analysis of (co)variance (ANOVA or ANCOVA) to ## evaluate the effect of one or more categorical or continuous predictors (i.e. ## independent variables) on a continuous outcome (i.e. dependent variable). The ## algorithms used make @code{anovan} suitable for balanced or unbalanced ## factorial (crossed) designs. By default, @code{anovan} treats all factors ## as fixed. Examples of function usage can be found by entering the command ## @code{demo anovan}. A bootstrap resampling variant of this function, ## @code{bootlm}, is available in the statistics-resampling package and has ## similar usage. ## ## Data is a single vector @var{Y} with groups specified by a corresponding ## matrix or cell array of group labels @var{GROUP}, where each column of ## @var{GROUP} has the same number of rows as @var{Y}. For example, if ## @code{@var{Y} = [23; 27; 31; 29; 30; 32]; @var{GROUP} = [1, 2; 1, 3; 1, 2; 2, ## 3; 2, 3; 3, ## 2];} ## then observation 23 was measured under conditions 1,2; observation 27 was ## measured under conditions 1,3; and so on. If the @var{GROUP} provided is ## empty, then the linear model is fit with just the intercept (no predictors). ## ## @code{anovan} can take a number of optional parameters as name-value pairs. ## ## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "continuous", ## @var{continuous})} ## ## @itemize ## @item ## @var{continuous} is a vector of indices indicating which of the columns (i.e. ## factors) in @var{GROUP} should be treated as continuous predictors rather ## than as categorical predictors. The relationship between continuous ## predictors and the outcome should be linear. ## @end itemize ## ## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "random", @var{random})} ## ## @itemize ## @item ## @var{random} is a vector of indices indicating which of the columns (i.e. ## factors) in @var{GROUP} should be treated as random effects rather than ## fixed effects. ## ## In the table @code{anovan} prints, the name of a random factor is followed ## by a @qcode{'} so that it can be told apart at a glance. That mark is a ## convention of the printed output only: the names returned in @var{atab}, ## in @var{stats}.varnames, and inside the expected-mean-square and ## denominator expressions carry no mark, so that they read as MATLAB's do. ## Which terms are random is reported in the @qcode{"Type"} column of ## @var{atab} instead. ## ## Every interaction involving a random factor stays in the model, and each ## F ratio is taken against the denominator its expected mean square calls ## for rather than against the mean squared error. The expected mean square ## of a term names the variance each component contributes to it, written ## @code{Q(@dots{})} for a fixed term and @code{V(@dots{})} for a random one; ## the denominator is the combination of mean squares whose expectation ## matches the term's with the term itself removed. Often that is a single ## mean square, in which case the F ratio carries its degrees of freedom. ## When no single mean square will do, one is synthesised from several and ## carries Satterthwaite degrees of freedom, which are generally not whole ## numbers. ## ## The variance component of every random term is estimated from the same ## system, and reported with confidence bounds. A component estimated as ## negative has no interval, as it lies outside the parameter space. ## @end itemize ## ## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "model", @var{modeltype})} ## ## @itemize ## @item ## @var{modeltype} can specified as one of the following: ## ## @itemize ## @item ## "linear" (default) : compute @math{N} main effects with no interactions. ## ## @item ## "interaction" : compute @math{N} effects and @math{N*(N-1)} two-factor ## interactions ## ## @item ## "full" : compute the @math{N} main effects and interactions at all levels ## ## @item ## a scalar integer : representing the maximum interaction order ## ## @item ## a matrix of term definitions : each row is a term and each column is a ## factor. Entries are nonnegative integer exponents. Exponents greater than ## one are valid only for factors selected by @qcode{"continuous"}. ## @end itemize ## ## @example ## -- Example: ## A two-way ANOVA with interaction would be: [1 0; 0 1; 1 1] ## @end example ## ## @end itemize ## ## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "nested", @var{nested})} ## ## @itemize ## @item ## @var{nested} is an N-by-N logical matrix, where N is the number of factors. ## A true entry @code{@var{nested}(i,j)} specifies that factor i is nested in ## factor j. A factor may be nested in more than one parent. Nested factors ## must be categorical and use the default contrasts. ## @end itemize ## ## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "sstype", @var{sstype})} ## ## @itemize ## @item ## @var{sstype} can specified as one of the following: ## ## @itemize ## @item ## 1 : Type I sequential sums-of-squares. ## ## @item ## 2 : Type II partially sequential sums-of-squares. Each term is adjusted ## for every other term that does not contain it. ## ## @item ## "h" : hierarchical sums-of-squares. Each term is adjusted only for the ## terms below it in the hierarchy, so for a model whose terms are all of ## first order it agrees with Type II, while a polynomial term is adjusted ## for its lower powers but not for its higher ones. ## ## @item ## 3 (default) : Type III partial, constrained or marginal sums-of-squares ## ## @end itemize ## @end itemize ## ## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "varnames", @var{varnames})} ## ## @itemize ## @item ## @var{varnames} must be a cell array of strings with each element containing a ## factor name for each column of @var{GROUP}, or a character matrix with one ## name per row. By default (if not parsed as optional argument), ## @var{varnames} are "X1","X2","X3", etc. @var{stats}.varnames holds them ## as a column. ## @end itemize ## ## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "alpha", @var{alpha})} ## ## @itemize ## @item ## @var{alpha} must be a scalar value between 0 and 1 requesting ## @math{100*(1-@var{alpha})%} confidence bounds for the regression coefficients ## returned in @var{stats}.coeffs (default 0.05 for 95% confidence). ## @end itemize ## ## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "display", @var{dispopt})} ## ## @itemize ## @item ## @var{dispopt} can be either "on" (default) or "off" and controls the display ## of the model formula, table of model parameters, the ANOVA table and the ## diagnostic plots. The F-statistic and p-values are formatted in APA-style. ## To avoid p-hacking, the table of model parameters is only displayed if we set ## planned contrasts (see below). ## @end itemize ## ## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "contrasts", ## @var{contrasts})} ## ## @itemize ## @item ## @var{contrasts} can be specified as one of the following: ## ## @itemize ## @item ## A string corresponding to one of the built-in contrasts listed below: ## ## @itemize ## @item ## "simple" or "anova" (default): Simple (ANOVA) contrast coding. (The first ## level appearing in the @var{GROUP} column is the reference level) ## ## @item ## "poly": Polynomial contrast coding for trend analysis. ## ## @item ## "helmert": Helmert contrast coding: the difference between each level with ## the mean of the subsequent levels. ## ## @item ## "effect": Deviation effect coding. (The first level appearing in the ## @var{GROUP} column is omitted). ## ## @item ## "sdif" or "sdiff": Successive differences contrast coding: the difference ## between each level with the previous level. ## ## @item ## "treatment": Treatment contrast (or dummy) coding. (The first level appearing ## in the @var{GROUP} column is the reference level). These contrasts are not ## compatible with @var{sstype} = 3. ## ## @end itemize ## ## @item ## A matrix containing a custom contrast coding scheme (i.e. the generalized ## inverse of contrast weights). Rows in the contrast matrices correspond to ## factor levels in the order that they first appear in the @var{GROUP} column. ## The matrix must contain the same number of columns as there are the number of ## factor levels minus one. ## @end itemize ## ## If the anovan model contains more than one factor and a built-in contrast ## coding scheme was specified, then those contrasts are applied to all factors. ## To specify different contrasts for different factors in the model, ## @var{contrasts} should be a cell array with the same number of cells as there ## are columns in @var{GROUP}. Each cell should define contrasts for the ## respective column in @var{GROUP} by one of the methods described above. If ## cells are left empty, then the default contrasts are applied. Contrasts for ## cells corresponding to continuous factors are ignored. ## @end itemize ## ## @code{[@dots{}] = anovan (@var{Y}, @var{GROUP}, "weights", @var{weights})} ## ## @itemize ## @item ## @var{weights} is an optional vector of weights to be used when fitting the ## linear model. Weighted least squares (WLS) is used with weights (that is, ## minimizing @code{sum (@var{weights} * @var{residuals} .^ 2))}; otherwise ## ordinary least squares (OLS) is used (default is empty for OLS). ## @end itemize ## ## @code{anovan} can return up to four output arguments: ## ## @code{@var{p} = anovan (@dots{})} returns a vector of p-values, one for each ## term. ## ## @code{[@var{p}, @var{atab}] = anovan (@dots{})} returns a cell array ## containing the ANOVA table. Its first row holds the column names, and the ## columns are, in order, the term name, its sum-of-squares, its degrees of ## freedom, a singularity flag, its mean square, the @math{F} statistic, the ## p-value, and then two effect sizes: eta squared and partial eta squared. ## The first seven follow MATLAB's layout, so a caller reading them by ## position gets the same quantity in either language; the two effect sizes ## are an Octave extension and are appended after them. ## ## A model naming any factor as random reports eight further columns after ## the p-value, as MATLAB does, with the two effect sizes still last: the ## term's type, its expected mean square, the mean square and degrees of ## freedom of the denominator its F ratio was taken against, the definition of ## that denominator, and the variance component of a random term with its ## confidence bounds. A denominator that no single mean square provides is ## synthesised from several and carries Satterthwaite degrees of freedom, ## which are generally not whole numbers. ## ## The singularity flag is @math{1} when a term is aliased with the rest of ## the model, which happens when the design is not of full rank, most often ## because a combination of factor levels holds no observations. The degrees ## of freedom reported for such a term are the ones that can be estimated, ## which may be fewer than the term's design block has columns and may be ## zero. Whether a term is aliased depends on the model it is adjusted for, ## and therefore on @qcode{"sstype"}: a term that is estimable in a sequential ## fit may not be estimable in a marginal one. A flagged term's ## sum-of-squares is not uniquely attributable to it, so the corresponding ## @math{F} and p-value should not be read as a test of that term. ## ## @code{[@var{p}, @var{atab}, @var{stats}] = anovan (@dots{})} returns a ## structure containing additional statistics, including degrees of freedom and ## effect sizes for each term in the linear model, the design matrix, the ## variance-covariance matrix, (weighted) model residuals, and the mean squared ## error. The columns of @var{stats}.coeffs (from left-to-right) report the ## model coefficients, standard errors, lower and upper @math{100*(1-alpha)%} ## confidence interval bounds, t-statistics, and p-values relating to the ## contrasts. The number appended to each term name in @var{stats}.coeffnames ## corresponds to the column number in the relevant contrast matrix for that ## factor. The @var{stats} structure can be used as input for ## @code{multcompare}. ## ## @code{[@var{p}, @var{atab}, @var{stats}, @var{terms}] = anovan (@dots{})} ## returns the model term definitions. ## ## @seealso{anova1, anova2, multcompare, fitlm} ## @end deftypefn function [P, T, STATS, TERMS] = anovan (Y, GROUP, varargin) if (nargin < 2) print_usage; endif ## Check supplied parameters if ((numel (varargin) / 2) != fix (numel (varargin) / 2)) error ("anovan: wrong number of arguments.") endif MODELTYPE = 'linear'; DISPLAY = 'on'; SSTYPE = 3; VARNAMES = []; CONTINUOUS = []; RANDOM = []; NESTED = []; CONTRASTS = {}; ALPHA = 0.05; WEIGHTS = []; for idx = 3:2:nargin name = varargin{idx-2}; value = varargin{idx-1}; switch (lower (name)) case 'model' MODELTYPE = value; case 'continuous' CONTINUOUS = value; case 'random' RANDOM = value; case 'nested' NESTED = value; case 'sstype' SSTYPE = value; case 'varnames' VARNAMES = value; case {'display','displayopt'} DISPLAY = value; case 'contrasts' CONTRASTS = value; case 'alpha' ALPHA = value; case 'weights' WEIGHTS = value; otherwise error ("anovan: parameter '%s' is not supported.", name); endswitch endfor ## Evaluate continuous input argument if (isnumeric (CONTINUOUS)) if (any (CONTINUOUS != abs (fix (CONTINUOUS)))) error (strcat ("anovan: the value provided for the CONTINUOUS", ... " parameter must be a positive integer.")); endif else error (strcat ("anovan: the value provided for the CONTINUOUS", ... " parameter must be numeric.")); endif ## Accommodate for different formats for GROUP ## GROUP can be a matrix of numeric identifiers of a cell arrays ## of strings or numeric identifiers N = size (GROUP, 2); # number of anova "ways" n = numel (Y); # total number of observations categorical_levels = cell (1, N); categorical_missing = false (n, 1); if (prod (size (Y)) != n) error ("anovan: for ""anovan (Y, GROUP)"", Y must be a vector."); endif if (numel (unique (CONTINUOUS)) > N) error (strcat ("anovan: the number of factors assigned as continuous", ... " cannot exceed the number of factors in GROUP.")); endif if (any ((CONTINUOUS > N) || any (CONTINUOUS <= 0))) error (strcat ("anovan: one or more indices provided in the value", ... " for the continuous parameter are out of range.")); endif cont_vec = false (1, N); cont_vec(CONTINUOUS) = true; if (isempty (NESTED)) NESTED = false (N); elseif (! (isnumeric (NESTED) || islogical (NESTED)) ... || ! isequal (size (NESTED), [N, N]) ... || any (! ismember (NESTED(:), [0, 1])) ... || any (diag (NESTED))) error (strcat ("anovan: NESTED must be an N-by-N logical matrix", ... " with a zero diagonal.")); else NESTED = logical (NESTED); endif closure = NESTED; for factor = 1:N closure |= closure(:, factor) * closure(factor, :); endfor if (any (diag (closure))) error ("anovan: NESTED factor relationships must be acyclic."); endif nested_factors = any (NESTED, 2)' | any (NESTED, 1); if (any (cont_vec & nested_factors)) error ("anovan: nested factors must be categorical."); endif if (isa (GROUP, "categorical")) for j = 1:N if (ismember (j, CONTINUOUS)) error ("anovan: continuous factors must be a numeric datatype."); endif categorical_levels{j} = categories (GROUP(:,j)); categorical_missing |= isundefined (GROUP(:,j)); endfor GROUP = double (GROUP); elseif (iscell (GROUP)) if (size (GROUP, 1) == 1) tmp = cell (n, N); for j = 1:N if (isa (GROUP{j}, "categorical")) if (ismember (j, CONTINUOUS)) error ("anovan: continuous factors must be a numeric datatype."); endif categorical_levels{j} = categories (GROUP{j}); categorical_missing |= isundefined (GROUP{j}(:)); tmp(:,j) = num2cell (double (GROUP{j}(:))); elseif (isnumeric (GROUP{j})) if (ismember (j, CONTINUOUS)) tmp(:,j) = num2cell (GROUP{j}); else tmp(:,j) = cellstr (num2str (GROUP{j})); endif else if (ismember (j, CONTINUOUS)) error ("anovan: continuous factors must be a numeric datatype."); endif tmp(:,j) = GROUP{j}; endif endfor GROUP = tmp; endif endif if (! isempty (GROUP)) if (size (GROUP,1) != n) error (strcat ("anovan: GROUP must be a matrix", ... " with the same number of rows as Y.")); endif endif if (! isempty (VARNAMES)) if (iscell (VARNAMES)) if (all (cellfun (@ischar, VARNAMES))) nvarnames = numel (VARNAMES); else error (strcat ("anovan: all variable names must", ... " be character or character arrays.")); endif elseif (ischar (VARNAMES) || isstring (VARNAMES)) ## One name per row of a character matrix, as in MATLAB VARNAMES = cellstr (VARNAMES); nvarnames = numel (VARNAMES); else error (strcat ("anovan: varnames is not of a valid type.", ... " Must be a cell array of character arrays,", ... " character array or string.")); endif else nvarnames = N; VARNAMES = arrayfun (@(x) ['X',num2str(x)], 1:N, 'UniformOutput', 0); endif if (nvarnames != N) error (strcat ("anovan: number of variable names is not equal", ... " to the number of grouping variables.")); endif ## A column whatever shape the names came in, as MATLAB returns them VARNAMES = VARNAMES(:); ## Evaluate random argument (if applicable) if (! isempty (RANDOM)) if (isnumeric (RANDOM)) if (any (RANDOM != abs (fix (RANDOM)))) error (strcat ("anovan: the value provided for the RANDOM", ... " parameter must be a positive integer.")); endif else error (strcat ("anovan: the value provided for the RANDOM", ... " parameter must be numeric.")); endif if (numel (RANDOM) > N) error (strcat ("anovan: the number of elements in RANDOM cannot", ... " exceed the number of columns in GROUP.")); endif if (max (RANDOM) > N) error (strcat ("anovan: the indices listed in RANDOM cannot", ... " exceed the number of columns in GROUP.")); endif endif ## Evaluate contrasts (if applicable) if isempty (CONTRASTS) CONTRASTS = cell (1, N); planned = false; else if (ischar (CONTRASTS)) contr_str = CONTRASTS; CONTRASTS = cell (1, N); CONTRASTS(:) = {contr_str}; endif if (! iscell (CONTRASTS)) CONTRASTS = {CONTRASTS}; endif for i = 1:N if (! isempty (CONTRASTS{i})) msg = strcat ("columns in CONTRASTS must sum to", ... " 0 for SSTYPE 3. Switching to SSTYPE 2 instead."); if (isnumeric (CONTRASTS{i})) ## Check whether all the columns sum to 0 if (any (abs (sum (CONTRASTS{i})) > eps ('single'))) warning (strcat ("Note that the CONTRASTS for predictor", ... " %u do not sum to zero."), i); endif ## Check whether contrasts are orthogonal if (any (abs (reshape (corr (CONTRASTS{i}) - ... eye (size (CONTRASTS{i}, 2)), [], 1))... > eps ('single'))) warning (strcat ("Note that the CONTRASTS for predictor", ... " %u are not orthogonal."), i); endif else if (! ismember (lower (CONTRASTS{i}), ... {'simple','anova','poly','helmert','effect',... 'sdif','sdiff','treatment'})) error (strcat ("anovan: valid built-in contrasts are:", ... " 'simple', 'poly', 'helmert',",... "'effect', 'sdif' or 'treatment'.")); endif if (strcmpi (CONTRASTS{i}, 'treatment') && (SSTYPE==3)) warning (msg); SSTYPE = 2; endif endif endif endfor planned = true; endif ## Evaluate alpha input argument if (! isa (ALPHA,'numeric') || numel (ALPHA) != 1) error ("anovan: alpha must be a numeric scalar value."); endif if ((ALPHA <= 0) || (ALPHA >= 1)) error ("anovan: alpha must be a value between 0 and 1."); endif ## Remove NaN or non-finite observations if (isempty (GROUP)) excl = any ([isnan(Y), isinf(Y)], 2); else XC = GROUP(:,CONTINUOUS); if iscell (XC) XC = cell2mat (XC); endif excl = any ([isnan(Y), isinf(Y), any(isnan(XC),2), any(isinf(XC),2)], 2) ... | categorical_missing; GROUP(excl,:) = []; endif Y(excl) = []; if (size (Y, 1) == 1) Y = Y.'; # if Y is a row vector, make it a column vector endif n = numel (Y); # recalculate total number of observations ## Evaluate weights input argument if (! isempty (WEIGHTS)) if (! isnumeric (WEIGHTS)) error ("anovan: WEIGHTS must be a numeric datatype."); endif if (any (size (WEIGHTS) != [n,1])) error (strcat ("anovan: WEIGHTS must be a vector", ... " with the same dimensions as Y.")); endif if (any (! (WEIGHTS > 0)) || any (isinf (WEIGHTS))) error ("anovan: WEIGHTS must be a vector of positive finite values."); endif # Create diagonal matrix of normalized weights W = diag (WEIGHTS / mean (WEIGHTS)); else # Create identity matrix W = eye (n);; endif ## Evaluate model type input argument and create terms matrix if not provided msg = strcat ("anovan: the number of columns in the term definitions", ... " cannot exceed the number of columns of GROUP."); if (ischar (MODELTYPE)) switch (lower (MODELTYPE)) case 'linear' MODELTYPE = 1; case {'interaction','interactions'} MODELTYPE = 2; case 'full' MODELTYPE = N; otherwise error ("anovan: model type not recognised"); endswitch endif if (isscalar (MODELTYPE)) TERMS = cell (MODELTYPE,1); v = false (1, N); switch (lower (MODELTYPE)) case 1 ## Create term definitions for an additive linear model TERMS = eye (N); case 2 ## Create term definitions for a model with two factor interactions if (N > 1) Nx = nchoosek (N, 2); else Nx = 0; endif TERMS = zeros (N + Nx, N); TERMS(1:N,:) = eye (N); cnt = N + 1; for j = 1:N for i = j:N-1 TERMS(cnt,j) = 1; TERMS(cnt,i+1) = 1; cnt++; endfor endfor otherwise if (MODELTYPE > N) error (msg); endif ## Create term definitions for a full model Nx = zeros (1, N-1); Nx = 0; for k = 1:N Nx = Nx + nchoosek (N,k); endfor for j = 1:MODELTYPE v(1:j) = 1; TERMS(j) = flipud (unique (perms (v), 'rows')); endfor TERMS = cell2mat (TERMS); endswitch TERMS = double (TERMS); else ## Assume that the user provided a suitable matrix of term definitions if (size (MODELTYPE, 2) != N) error (msg); endif if (! isnumeric (MODELTYPE) || any (! isfinite (MODELTYPE(:))) ... || any (MODELTYPE(:) < 0) ... || any (MODELTYPE(:) != fix (MODELTYPE(:)))) error (strcat ("anovan: elements of the model terms", ... " matrix must be nonnegative integers.")); endif TERMS = double (MODELTYPE); endif ## Evaluate terms matrix TERMS(! any (TERMS > 0, 2), :) = []; Ng = sum (TERMS > 0, 2); ## Evaluate terms Nt = rows (TERMS); ## Calculate total sum-of-squares ct = sum (Y)^2 / n; # correction term sst = sum (Y.^2) - ct; dft = n - 1; ## Create design matrix mDesignMatrix (); ## Fit linear models, and calculate sums-of-squares for ANOVA sstype_id = lower (SSTYPE); switch (sstype_id) case 1 ## Type I sequential sums-of-squares (SSTYPE = 1) R = sst; ss = zeros (Nt,1); df_est = zeros (Nt,1); [jnk, jnk, jnk, jnk, jnk, rk_prev] = lmfit (cell2mat (X(1)), Y, W); for j = 1:Nt XS = cell2mat (X(1:j+1)); [b, sse, jnk, jnk, jnk, rk_now] = lmfit (XS, Y, W); ss(j) = R - sse; df_est(j) = rk_now - rk_prev; R = sse; rk_prev = rk_now; endfor [b, sse, resid, ucov, hat] = lmfit (XS, Y, W); sstype_char = 'I'; case {2,'h'} ## Type II (partially sequential, or hierarchical) sums-of-squares ss = zeros (Nt,1); df_est = zeros (Nt,1); for j = 1:Nt i = find (TERMS(j,:) > 0); if (isequal (sstype_id, 'h')) excludes_term = any (TERMS(:,i) < TERMS(j,i), 2); else excludes_term = any (TERMS(:,i) != TERMS(j,i), 2); endif k = cat (1, 1, 1 + find (excludes_term)); XS = cell2mat (X(k)); [jnk, R1, jnk, jnk, jnk, rk1] = lmfit (XS, Y, W); k = cat (1, j+1, k); XS = cell2mat (X(k)); [jnk, R2, jnk, jnk, jnk, rk2] = lmfit (XS, Y, W); ss(j) = R1 - R2; df_est(j) = rk2 - rk1; endfor [b, sse, resid, ucov, hat] = lmfit (cell2mat (X), Y, W); sstype_char = 'II'; case 3 ## Type III (partial, constrained or marginal) sums-of-squares ss = zeros (Nt, 1); df_est = zeros (Nt, 1); [b, sse, resid, ucov, hat, rk_full] = lmfit (cell2mat (X), Y, W); for j = 1:Nt XS = cell2mat (X(1:Nt+1 != j+1)); [jnk, R, jnk, jnk, jnk, rk_red] = lmfit (XS, Y, W); ss(j) = R - sse; df_est(j) = rk_full - rk_red; endfor sstype_char = 'III'; otherwise error ("anovan: sstype value not supported."); endswitch ss = max (0, ss); # Truncate negative SS at 0 ## A term that is aliased with the rest of the model contributes fewer ## degrees of freedom than its design block has columns. Report what is ## estimable and flag the shortfall, so a sum-of-squares of zero can be ## told apart from a factor that genuinely has no effect. singular = (df_est < df); df_coef = df; ## nominal width of each term's block, for the coefficients df = df_est; ## estimable degrees of freedom, as reported in the table dfe = n - rank (cell2mat (X)); ms = ss ./ df; if (dfe > 0) mse = sse / dfe; else ## A saturated model leaves no residual degrees of freedom. The ## residual sum-of-squares is zero up to rounding, so report both as ## zero rather than letting 0/0 become Inf and poison every mean square ## that is tested against the error term. sse = 0; mse = 0; endif eta_sq = ss ./ sst; partial_eta_sq = ss ./ (ss + sse); ## With every factor fixed, each mean square is tested against the error. ## A random factor changes that: the expected mean squares decide which ## mean square, or which combination of them, estimates the same variance ## as the term under test, and that becomes the denominator. is_random = false (Nt, 1); ems_coef = []; txtems = {}; denom = []; msdenom = []; dfdenom = []; txtdenom = {}; varest = []; varci = []; if (isempty (RANDOM)) msdenom = repmat (mse, Nt, 1); dfdenom = repmat (dfe, Nt, 1); else [is_random, ems_coef, txtems, denom, msdenom, dfdenom, ... txtdenom, varest, varci] = mRandomEffects (TERMS, RANDOM, ... NESTED, X, gid, CONTINUOUS, ... sstype_id, df, ms, mse, dfe, ... ALPHA, VARNAMES, Nt); endif F = ms ./ msdenom; P = fcdf (F, df, dfdenom, 'upper'); ## The denominator columns describe a random-effects fit, so they stay ## empty when every factor is fixed and the error term is the denominator. if (isempty (RANDOM)) msdenom_out = []; dfdenom_out = []; rtnames = {}; else msdenom_out = msdenom; dfdenom_out = dfdenom; rtnames = [cellfun(@(t) mTermName (t, VARNAMES, NESTED), ... num2cell (TERMS(is_random,:), 2), ... 'UniformOutput', false); {'Error'}]; endif ## Prepare model formula and cell array containing the ANOVA table ## Columns 1 to 7 follow MATLAB's layout. The two effect sizes are an ## Octave extension and are appended, so a caller indexing the shared ## columns by position reads the same quantity in either language. if (isempty (RANDOM)) T = cell (Nt + 3, 9); T(1,:) = {'Source', 'Sum Sq.', 'd.f.', 'Singular?', 'Mean Sq.', 'F', ... 'Prob>F', 'Eta Sq.', 'Part. Eta Sq.'}; T(2:Nt+1,2:9) = num2cell ([ss, df, singular, ms, F, P, eta_sq, ... partial_eta_sq]); else ## A random factor adds the eight columns MATLAB adds, describing what ## each mean square estimates and what it was tested against. The two ## effect sizes stay last, as they are in the fixed-effects layout. T = cell (Nt + 3, 17); T(1,:) = {'Source', 'Sum Sq.', 'd.f.', 'Singular?', 'Mean Sq.', 'F', ... 'Prob>F', 'Type', 'Expected MS', 'MS denom', 'd.f. denom', ... 'Denom. defn.', 'Var. est.', 'Var. lower bnd', ... 'Var. upper bnd', 'Eta Sq.', 'Part. Eta Sq.'}; T(2:Nt+1,2:7) = num2cell ([ss, df, singular, ms, F, P]); types = repmat ({'fixed'}, Nt, 1); types(is_random) = {'random'}; T(2:Nt+1,8) = types; T(2:Nt+1,9) = txtems(1:Nt); T(2:Nt+1,10) = num2cell (msdenom); T(2:Nt+1,11) = num2cell (dfdenom); T(2:Nt+1,12) = txtdenom; rterms = find (is_random); for k = 1:numel (rterms) T(1+rterms(k),13:15) = {varest(k), varci(k,1), varci(k,2)}; endfor T(2:Nt+1,16:17) = num2cell ([eta_sq, partial_eta_sq]); T(end-1,8:9) = {'random', txtems{end}}; T(end-1,13:15) = {varest(end), varci(end,1), varci(end,2)}; endif T(end-1,1:5) = {'Error', sse, dfe, 0, mse}; T(end,1:4) = {'Total', sst, dft, 0}; formula = sprintf ("Y ~ 1"); # Initialize model formula for i = 1:Nt str = mTermName (TERMS(i,:), VARNAMES, NESTED); T(i+1,1) = str; ## Append model term to formula if (any (ismember (find (TERMS(i,:) > 0), RANDOM))) ## Random intercept term formula = sprintf ("%s + (1|%s)", formula, str); ## Remove statistics for random factors from the ANOVA table #T(RANDOM+1,4:7) = cell(1,4); #P(RANDOM) = NaN; else ## Fixed effect term formula = sprintf ("%s + %s", formula, str); endif endfor ## Calculate a standard error, t-statistic and p-value for each ## of the regression coefficients (fixed effects only) t_crit = tinv (1 - ALPHA / 2, dfe); se = sqrt (diag (ucov) * mse); t = b ./ se; p = 2 * tcdf (-abs (t), dfe); coeff_stats = zeros (1 + sum (df_coef), 6); coeff_stats(:,1) = b; # coefficients coeff_stats(:,2) = se; # standard errors coeff_stats(:,3) = b - se * t_crit; # Lower CI bound coeff_stats(:,4) = b + se * t_crit; # Upper CI bound coeff_stats(:,5) = t; # t-statistics coeff_stats(:,6) = p; # p-values ## Assign NaN to p-value to avoid printing statistics relating to ## coefficients for 'random' effects hi = 1 + cumsum (df_coef); random_terms = find (any (TERMS(:,RANDOM) > 0, 2)); for ignore = random_terms' coeff_stats(hi(ignore)-df_coef(ignore)+1:hi(ignore), 6) = NaN; endfor ## Compute leverage values and Cook's distance h = diag (hat); # Leverage values D = resid.^2 / ((1 + sum (df_coef)) * mse) ... .* h ./ (1 - h).^2; # Cook's distance ## Create STATS structure for MULTCOMPARE STATS = struct ('source','anovan', ... 'resid', resid, ... # These are weighted (not raw) residuals 'coeffs', coeff_stats, ... 'Rtr', [], ... # Not used by Octave 'rowbasis', [], ... # Not used by Octave 'dfe', dfe, ... 'mse', mse, ... 'nullproject', [], ... # Not used by Octave 'terms', TERMS, ... 'nlevels', nlevels, ... 'continuous', cont_vec, ... 'vmeans', vmeans, ... 'termcols', termcols, ... 'coeffnames', {cellstr(char(coeffnames{:}))}, ... 'vars', [], ... # Not used by Octave 'varnames', {VARNAMES}, ... 'grpnames', {levels}, ... 'vnested', NESTED, ... ## Empty unless the model holds a random factor, in ## which case they carry its expected mean squares, the ## denominator each F ratio was taken against, and the ## variance component of every random term. 'ems', ems_coef, ... 'denom', denom, ... 'dfdenom', dfdenom_out, ... 'msdenom', msdenom_out, ... 'varest', varest, ... 'varci', varci, ... 'txtdenom', {txtdenom}, ... 'txtems', {txtems}, ... 'rtnames', {rtnames}, ... ## Additional STATS fields used exclusively by Octave 'center_continuous', center_continuous, ... 'random', RANDOM, ... 'formula', formula, ... 'alpha', ALPHA, ... 'df', df, ... 'contrasts', {CONTRASTS}, ... 'X', sparse (cell2mat (X)), ... 'Y', Y, ... 'W', sparse (W), ... 'lmfit', @lmfit, ... 'vcov', sparse (ucov * mse), ... 'CooksD', D, ... 'grps', gid, ... 'eta_squared', eta_sq, ... 'partial_eta_squared', partial_eta_sq); ## Print ANOVA table switch (lower (DISPLAY)) case {'on', true} ## Print model formula fprintf ("\nMODEL FORMULA (based on Wilkinson's notation):\n\n%s\n", formula); ## If applicable, print parameter estimates (a.k.a contrasts) for fixed effects if (planned && ! isempty (GROUP)) ## Parameter estimates correspond to the contrasts we set. To avoid ## p-hacking, don't print contrasts if we don't specify them to start with fprintf ("\nMODEL PARAMETERS (contrasts for the fixed effects)\n\n"); fprintf ("Parameter Estimate SE Lower.CI Upper.CI t Prob>|t|\n"); fprintf ("--------------------------------------------------------------------------------\n"); for j = 1:size (coeff_stats, 1) if (p(j) < 0.001) fprintf ("%-20s %10.3g %9.3g %9.3g %9.3g %8.2f <.001 \n", ... STATS.coeffnames{j}, STATS.coeffs(j,1:end-1)); elseif (p(j) < 0.9995) fprintf ("%-20s %10.3g %9.3g %9.3g %9.3g %8.2f .%03u \n", ... STATS.coeffnames{j}, STATS.coeffs(j,1:end-1), round (p(j) * 1e+03)); elseif (isnan (p(j))) ## Don't display coefficients for 'random' effects since they were ## treated as fixed effects else fprintf ("%-20s %10.3g %9.3g %9.3g %9.3g %8.2f 1.000 \n", ... STATS.coeffnames{j}, STATS.coeffs(j,1:end-1)); endif endfor endif ## Print ANOVA table [nrows, ncols] = size (T); ## The ' marking a random factor is a printing convention of this ## implementation, not part of the names it returns, which carry no ## marker so that they match what MATLAB reports. VARNAMES_DISP = VARNAMES; for v = RANDOM VARNAMES_DISP{v} = strcat (VARNAMES{v}, "'"); endfor fprintf ("\nANOVA TABLE (Type %s sums-of-squares):\n\n", sstype_char); fprintf ("Source Sum Sq. d.f. Mean Sq. Part.Eta F Prob>F\n"); fprintf ("--------------------------------------------------------------------------------\n"); for i = 1:Nt str = mTermName (TERMS(i,:), VARNAMES_DISP, NESTED); l = numel (str); # Needed to truncate source term name at 18 characters ## Format and print the statistics for each model term ## Format F statistics and p-values in APA style ## Mean Sq., F and Prob>F sit at 5, 6 and 7 in both layouts, and ## partial eta squared is the last column of either. row = {T{i+1,2}, T{i+1,3}, T{i+1,5}, T{i+1,end}, T{i+1,6}}; if (P(i) < 0.001) fprintf ("%-20s %10.5g %6d %10.5g %4.3f %11.2f <.001 \n", ... str(1:min (18,l)), row{:}); elseif (P(i) < 0.9995) fprintf ("%-20s %10.5g %6d %10.5g %4.3f %11.2f .%03u \n", ... str(1:min (18,l)), row{:}, round (P(i) * 1e+03)); elseif (isnan (P(i))) fprintf ("%-20s %10.5g %6d \n", str(1:min (18,l)), T{i+1,2:3}); else fprintf ("%-20s %10.5g %6d %10.5g %4.3f %11.2f 1.000 \n", ... str(1:min (18,l)), row{:}); endif endfor fprintf ("Error %10.5g %6d %10.5g\n", ... T{end-1,2}, T{end-1,3}, T{end-1,5}); fprintf ("Total %10.5g %6d \n", T{end,2:3}); if (any (singular)) fprintf (strcat ("Singular terms are aliased with the rest of the", ... " model and are not\nuniquely estimable: %s\n"), ... strjoin (T(1 + find (singular), 1)', ", ")); endif if (! isempty (RANDOM)) fprintf ("\nEach F ratio is taken against the denominator its\n"); fprintf ("expected mean square calls for:\n"); for i = 1:Nt fprintf ("%-20s %s on %.4g d.f.\n", ... T{i+1,1}(1:min (18, numel (T{i+1,1}))), ... txtdenom{i}, dfdenom(i)); endfor endif fprintf ("\n"); ## Make figure of diagnostic plots figure ('Name', 'Diagnostic Plots: Model Residuals'); t = STATS.resid ./ (sqrt (mse * (1 - h))); # Studentized residuals fit = STATS.X * STATS.coeffs(:,1); # Fitted values [jnk, DI] = sort (D, 'descend'); # Indices of sorted D nk = 4; # Top nk residuals with largest D ## Normal quantile-quantile plot subplot (2, 2, 1); x = ((1 : n)' - .5) / n; [ts, I] = sort (t); q = norminv (x); plot (q, ts, 'ok', 'markersize', 3); box off; grid on; xlabel ('Theoretical quantiles'); ylabel ('Studentized Residuals'); title ('Normal Q-Q Plot'); arrayfun (@(i) text (q(I == DI(i)), t(DI(i)), ... sprintf (" %u", DI(i))), [1:min(nk,n)]) iqr = [0.25; 0.75]; yl = quantile (t, iqr, 1, 6); xl = norminv (iqr); slope = diff (yl) / diff (xl); int = yl(1) - slope * xl(1); ax1_xlim = get (gca, 'XLim'); hold on; plot (ax1_xlim, slope * ax1_xlim + int, 'k-'); hold off; set (gca, 'Xlim', ax1_xlim); ## Spread-Location Plot subplot (2, 2, 2); plot (fit, sqrt (abs (t)), 'ko', 'markersize', 3); box off; xlabel ('Fitted values'); ylabel ('sqrt ( | Studentized Residuals | )'); title ('Spread-Location Plot') ax2_xlim = get (gca, 'XLim'); hold on; plot (ax2_xlim, ones (1, 2) * sqrt (2), 'k:'); plot (ax2_xlim, ones (1, 2) * sqrt (3), 'k-.'); plot (ax2_xlim, ones (1, 2) * sqrt (4), 'k--'); hold off; arrayfun (@(i) text (fit(DI(i)), sqrt (abs (t(DI(i)))), ... sprintf (" %u", DI(i))), [1:min(nk,n)]); xlim (ax2_xlim); ## Residual-Leverage plot subplot (2, 2, 3); plot (h, t, 'ko', 'markersize', 3); box off; xlabel ('Leverage') ylabel ('Studentized Residuals'); title ('Residual-Leverage Plot') ax3_xlim = get (gca, 'XLim'); ax3_ylim = get (gca, 'YLim'); hold on; plot (ax3_xlim, zeros (1, 2), 'k-'); hold off; arrayfun (@(i) text (h(DI(i)), t(DI(i)), ... sprintf (" %u", DI(i))), [1:min(nk,n)]); set (gca, 'ygrid', 'on'); xlim (ax3_xlim); ylim (ax3_ylim); ## Cook's distance stem plot subplot (2, 2, 4); stem (D, 'ko', 'markersize', 3); box off; xlabel ('Obs. number') ylabel ('Cook''s distance') title ('Cook''s Distance Stem Plot') xlim ([0, n]); ax4_xlim = get (gca, 'XLim'); ax4_ylim = get (gca, 'YLim'); hold on; plot (ax4_xlim, ones (1, 2) * 4 / dfe, 'k:'); plot (ax4_xlim, ones (1, 2) * 0.5, 'k-.'); plot (ax4_xlim, ones (1, 2), 'k--'); hold off; arrayfun (@(i) text (DI(i), D(DI(i)), ... sprintf (" %u", DI(i))), [1:min(nk,n)]); xlim (ax4_xlim); ylim (ax4_ylim); set (findall ( gcf, '-property', 'FontSize'), 'FontSize', 7) case {'off', false} ## do nothing otherwise error ("anovan: wrong value for 'display' parameter."); endswitch function mDesignMatrix () ## Nested function that returns a cell array of the design matrix for ## each term in the model ## Input variables it uses: ## GROUP, TERMS, CONTINUOUS, CONTRASTS, VARNAMES, n ## Variables it creates or modifies: ## X, grpnames, nlevels, df, termcols, coeffnames, vmeans, gid, CONTRASTS ## EVALUATE FACTOR LEVELS levels = cell (N, 1); gid = zeros (n, N); nlevels = zeros (N, 1); for j = 1:N if (cont_vec(j)) nlevels(j) = 1; if (iscell (GROUP(:,j))) gid(:,j) = cell2mat ([GROUP(:,j)]); else gid(:,j) = GROUP(:,j); endif elseif (isempty (categorical_levels{j})) levels{j} = unique (GROUP(:,j), 'stable'); if (isnumeric (levels{j})) levels{j} = num2cell (levels{j}); endif nlevels(j) = numel (levels{j}); for k = 1:nlevels(j) gid(ismember (GROUP(:,j), levels{j}{k}),j) = k; endfor else if (iscell (GROUP)) codes = cell2mat (GROUP(:,j)); else codes = GROUP(:,j); endif level_values = find (accumarray (codes, 1) > 0); levels{j} = categorical_levels{j}(level_values); nlevels(j) = numel (level_values); for k = 1:nlevels(j) gid(codes == level_values(k),j) = k; endfor endif endfor ## ENCODE EACH FACTOR ONCE base = cell (N, 1); vmeans = zeros (N, 1); center_continuous = cont_vec; for j = 1:N if (cont_vec(j)) ## Keep the predictor uncentred here. A term raises it to its ## exponent first and centres the resulting column, so that x^2 ## means the square of x rather than the square of x - mean (x). base{j} = gid(:,j); if (ischar (CONTRASTS{j}) ... && strcmpi (CONTRASTS{j}, 'treatment')) center_continuous(j) = false; CONTRASTS{j} = []; else vmeans(j) = mean (base{j}); endif elseif (any (NESTED(j,:))) if (! isempty (CONTRASTS{j})) error (strcat ("anovan: custom contrasts are not supported", ... " for nested factors.")); endif parents = find (NESTED(j,:)); [~, ~, parent_id] = unique (gid(:,parents), 'rows'); blocks = cell (max (parent_id), 1); for parent = 1:max (parent_id) rows_ = find (parent_id == parent); child_levels = unique (gid(rows_,j), 'stable'); [~, local_id] = ismember (gid(rows_,j), child_levels); C = contr_simple (numel (child_levels)); block = zeros (n, columns (C)); block(rows_,:) = C(local_id,:); blocks{parent} = block; endfor base{j} = cell2mat (blocks'); CONTRASTS{j} = []; else CONTRASTS{j} = mContrasts (CONTRASTS{j}, nlevels(j)); base{j} = CONTRASTS{j}(gid(:,j), :); endif endfor ## BUILD ONE DESIGN BLOCK FOR EACH REQUESTED TERM X = cell (1, Nt + 1); X{1} = ones (n, 1); coeffnames = cell (1, Nt + 1); coeffnames{1} = '(Intercept)'; df = zeros (Nt, 1); termcols = ones (Nt + 1, 1); for i = 1:Nt factors = find (TERMS(i, :) > 0); tmp = ones (n, 1); for j = factors exponent = TERMS(i, j); if (! cont_vec(j) && exponent != 1) error (strcat ("anovan: categorical factors cannot have", ... " exponent values greater than 1.")); endif block = base{j}; if (cont_vec(j)) if (exponent != 1) block = block .^ exponent; endif if (center_continuous(j)) block -= mean (block); endif endif tmp = reshape (bsxfun (@times, ... reshape (tmp, n, 1, columns (tmp)), ... reshape (block, n, columns (block), 1)), n, []); endfor X{i + 1} = tmp; df(i) = columns (tmp); ## termcols counts the columns a dummy coding of the term would take, ## which is one per level rather than the contrast width in df. termcols(i + 1) = prod (nlevels(factors)); term_name = mTermName (TERMS(i, :), VARNAMES, NESTED); if (df(i) == 1) coeffnames{i + 1} = term_name; else coeffnames{i + 1} = arrayfun (... @(v) sprintf ("%s_%u", term_name, v), (1:df(i))', ... 'UniformOutput', false); endif endfor endfunction endfunction function C = mContrasts (specification, nlevels) if (nlevels == 1) C = zeros (1, 0); elseif (isempty (specification)) C = contr_simple (nlevels); elseif (ischar (specification)) switch (lower (specification)) case {'simple', 'anova'} C = contr_simple (nlevels); case 'poly' C = contr_poly (nlevels); case 'helmert' C = contr_helmert (nlevels); case 'effect' C = contr_sum (nlevels); case {'sdif', 'sdiff'} C = contr_sdif (nlevels); case 'treatment' C = contr_treatment (nlevels); otherwise error ("anovan: unknown contrast specification."); endswitch else C = specification; if (! isequal (size (C), [nlevels, nlevels - 1])) error (strcat ("anovan: each contrast matrix must have one row per", ... " factor level and one fewer column.")); elseif (! all (any (C))) error ("anovan: a contrast must be coded in every column."); endif endif endfunction function [is_random, ems_coef, txtems, denom, msdenom, dfdenom, txtdenom, ... varest, varci] = mRandomEffects (TERMS, RANDOM, NESTED, X, gid, ... CONTINUOUS, sstype_id, df, ms, mse, dfe, ... ALPHA, VARNAMES, Nt) ## Derive the expected mean squares of a model holding one or more random ## factors, and from them the denominator each F ratio is taken against. ## A term is random when any factor in it is random, counting the ## parents of a nested factor as part of the term. membership = TERMS > 0; for factor = 1:columns (membership) parents = find (NESTED(factor,:)); if (! isempty (parents)) membership(:,parents) |= membership(:,factor); endif endfor is_random = any (membership(:,RANDOM), 2); term_names = cell (Nt, 1); for i = 1:Nt term_names{i} = mTermName (TERMS(i,:), VARNAMES, NESTED); endfor ## A term's basis is the set of cells its factors define, so that a ## coefficient counts observations per cell rather than design columns. bases = cell (Nt, 1); for i = 1:Nt factors = find (TERMS(i,:) > 0); nested_here = factors(any (NESTED(factors,:), 2)); if (! isempty (nested_here)) factors = union (factors, find (any (NESTED(nested_here,:), 1))); endif if (all (! ismember (factors, CONTINUOUS))) [jnk, jnk, ids] = unique (gid(:,factors), 'rows'); bases{i} = sparse (1:numel (ids), ids, 1); else bases{i} = X{i+1}; endif endfor ## The coefficient of a component in an expected mean square is the ## energy its basis gains when the term enters the model that the term's ## sum-of-squares is measured against, per degree of freedom. ems_coef = zeros (Nt, Nt); for i = 1:Nt [included, excluded] = mEmsModels (TERMS, X, sstype_id, Nt, i); Qi = orth (full (included)); Qe = orth (full (excluded)); for j = 1:Nt gain = sum (sumsq (Qi' * bases{j})) - sum (sumsq (Qe' * bases{j})); ems_coef(i,j) = max (gain, 0) / max (df(i), 1); endfor endfor ## Name each component: Q for a fixed term, V for a random one. txtems = cell (Nt + 1, 1); for i = 1:Nt pieces = {}; if (! is_random(i)) piece = mEmsComponent (ems_coef(i,i), 'Q', term_names{i}); if (! isempty (piece)) pieces{end+1} = piece; endif for j = find (! is_random(:))' if (j != i && abs (ems_coef(i,j)) > 1e-10) pieces{end+1} = mEmsComponent (ems_coef(i,j), 'Q', ... term_names{j}); endif endfor endif for j = find (is_random(:))' if (abs (ems_coef(i,j)) > 1e-10) pieces{end+1} = mEmsComponent (ems_coef(i,j), 'V', term_names{j}); endif endfor pieces{end+1} = 'V(Error)'; txtems{i} = strjoin (pieces, '+'); endfor txtems{Nt+1} = 'V(Error)'; ## The denominator of a term's F ratio is the combination of mean ## squares whose expectation matches the term's, minus the term itself. rt = find (is_random(:)); random_ms = [ms(rt); mse]; random_df = [df(rt); dfe]; coefficients = [ems_coef(rt,rt), ones(numel (rt), 1); ... zeros(1, numel (rt)), 1]; names = [term_names(rt); {'Error'}]; denom = zeros (Nt, numel (rt) + 1); msdenom = zeros (Nt, 1); dfdenom = zeros (Nt, 1); txtdenom = cell (Nt, 1); for i = 1:Nt target = [ems_coef(i,rt), 1]; own = find (rt == i, 1); if (! isempty (own)) target(own) = 0; endif weights = pinv (coefficients') * target'; ## pinv leaves rounding dust on the mean squares a denominator does ## not use. Drop it, so the formula and the number agree. weights(abs (weights) <= 1e-10) = 0; denom(i,:) = weights'; msdenom(i) = weights' * random_ms; ## Satterthwaite's approximation over the mean squares in use. One ## carrying no degrees of freedom has no sampling distribution to ## combine, so the denominator has none either. used = (weights != 0); if (any (used & (random_df(:) <= 0))) dfdenom(i) = 0; else parts = (weights(used) .* random_ms(used)) .^ 2 ./ random_df(used); dfdenom(i) = msdenom(i) ^ 2 / sum (parts); endif txtdenom{i} = mMeanSquareFormula (weights, names); endfor ## Variance components, and a confidence interval for each from the ## chi-squared bounds of the mean squares it is built from. varest = coefficients \ random_ms; lower_ms = random_df .* random_ms ./ chi2inv (1 - ALPHA / 2, random_df); upper_ms = random_df .* random_ms ./ chi2inv (ALPHA / 2, random_df); inverse = pinv (coefficients); lo = sum (max (inverse, 0) .* lower_ms' ... + min (inverse, 0) .* upper_ms', 2); hi = sum (max (inverse, 0) .* upper_ms' ... + min (inverse, 0) .* lower_ms', 2); lo = max (lo, 0); undefined = (varest < 0) | (! isfinite (lo)) | (! isfinite (hi)); lo(undefined) = NaN; hi(undefined) = NaN; varci = [lo, hi]; ## Report the coefficients with a row and column for the error term, as ## MATLAB does, so the matrix describes every component of the model. ems_coef = [ems_coef, ones(Nt, 1); zeros(1, Nt), 1]; endfunction function [included, excluded] = mEmsModels (TERMS, X, sstype_id, Nt, term) ## Return the models a term's sum-of-squares is measured between, which is ## what its expected mean square describes. switch (sstype_id) case 1 inc = 1:term; exc = 1:term-1; case {2, 'h'} fac = find (TERMS(term,:) > 0); if (isequal (sstype_id, 'h')) exc = find (any (TERMS(:,fac) < TERMS(term,fac), 2))'; else exc = find (any (TERMS(:,fac) != TERMS(term,fac), 2))'; endif inc = [term, exc]; otherwise inc = 1:Nt; exc = setdiff (inc, term, 'stable'); endswitch included = cell2mat (X([1, inc + 1])); excluded = cell2mat (X([1, exc + 1])); endfunction function value = mEmsComponent (coefficient, symbol, name) if (! isfinite (coefficient) || abs (coefficient) <= 1e-10) value = ""; elseif (abs (coefficient - 1) <= 1e-10) value = sprintf ("%s(%s)", symbol, name); else value = sprintf ("%.6g*%s(%s)", coefficient, symbol, name); endif endfunction function formula = mMeanSquareFormula (weights, names) pieces = {}; for i = 1:numel (weights) coefficient = weights(i); if (abs (coefficient) <= 1e-10) continue; elseif (abs (coefficient - 1) <= 1e-10) term = sprintf ("MS(%s)", names{i}); elseif (abs (coefficient + 1) <= 1e-10) term = sprintf ("-MS(%s)", names{i}); else term = sprintf ("%.6g*MS(%s)", coefficient, names{i}); endif if (! isempty (pieces) && coefficient > 0) term = ["+", term]; endif pieces{end+1} = term; endfor formula = strjoin (pieces, ""); endfunction function name = mTermName (term, varnames, nested) pieces = cell (1, nnz (term)); count = 0; for j = find (term > 0) count += 1; factor = varnames{j}; if (nargin > 2 && any (nested(j,:))) parents = varnames(nested(j,:)); factor = sprintf ("%s(%s)", factor, strjoin (parents, ",")); endif if (term(j) == 1) pieces{count} = factor; else pieces{count} = sprintf ("%s^%d", factor, term(j)); endif endfor ## Wilkinson notation: ':' is the interaction term alone, which is what a ## row of the table holds, where '*' would name the crossed set of the ## factors and every interaction among them. name = strjoin (pieces, ":"); endfunction ## BUILT IN CONTRAST CODING FUNCTIONS function C = contr_simple (N) ## Create contrast matrix (of doubles) using simple (ANOVA) contrast coding ## These contrasts are centered (i.e. sum to 0) ## Ideal for unordered factors, with comparison to a reference level ## The first factor level is the reference level C = cat (1, zeros (1,N-1), eye (N-1)) - 1/N; endfunction function C = contr_poly (N) ## Create contrast matrix (of doubles) using polynomial contrast coding ## for trend analysis of ordered categorical factor levels ## These contrasts are orthogonal and centered (i.e. sum to 0) ## Ideal for ordered factors [C, jnk] = qr (bsxfun (@power, [1:N]' - mean ([1:N]'), [0:N-1])); C(:,1) = []; s = ones (1, N-1); s(1:2:N-1) *= -1; f = (sign (C(1,:)) != s); C(:,f) *= -1; endfunction function C = contr_helmert (N) ## Create contrast matrix (of doubles) using Helmert coding contrasts ## These contrasts are orthogonal and centered (i.e. sum to 0) C = cat (1, tril (-ones (N-1), -1) + diag (N-1:-1:1), ... -ones (1, N-1)) ./ (N:-1:2); endfunction function C = contr_sum (N) ## Create contrast matrix (of doubles) using deviation effect coding ## These contrasts are centered (i.e. sum to 0) C = cat (1, - (ones (1,N-1)), eye (N-1)); endfunction function C = contr_sdif (N) ## Create contrast matrix (of doubles) using successive differences coding ## These contrasts are centered (i.e. sum to 0) C = tril (ones (N, N - 1), -1) - ones (N, 1) / N * [N - 1 : -1 : 1]; endfunction function C = contr_treatment (N) ## Create contrast matrix (of doubles) using treatment contrast coding ## Not compatible with SSTYPE 3 since contrasts are not centered ## Ideal for unordered factors, with comparison to a reference level ## The first factor level is the reference level C = cat (1, zeros (1,N-1), eye (N-1)); endfunction ## FUNCTION TO FIT THE LINEAR MODEL function [b, sse, resid, ucov, hat, rk] = lmfit (X, Y, W) ## Get model coefficients by solving the linear equation by QR decomposition ## The number of free parameters (i.e. intercept + coefficients) is equal ## to n - dfe. If optional argument W is provided, it should be a diagonal ## matrix of weights or a positive definite covariance matrix if (nargin < 3) ## If no weights are provided, create an identity matrix n = numel (Y); W = eye (n); endif C = chol (W); XW = C*X; YW = C*Y; [Q, R] = qr (XW, 0); ## A design that is not of full column rank has no unique solution, and the ## triangular solve gives an arbitrarily large one: the residuals follow it, ## so the error sum-of-squares can exceed the total. Detect the deficiency ## and take the minimum-norm least-squares solution instead, which leaves ## the fitted values, the residuals and SSE correct whatever the rank. npar = columns (XW); if (npar == 0) deficient = false; elseif (rows (R) != columns (R)) deficient = true; else deficient = (rcond (R) < eps); endif if (deficient) b = pinv (XW) * YW; rk = rank (XW); else b = R \ (Q' * YW); rk = npar; endif ## Get fitted values fit = XW * b; ## Get residuals from the fit resid = YW - fit; ## Calculate the residual sums-of-squares sse = sum (resid.^2); ## Calculate the unscaled covariance matrix (i.e. inv (X'*X )) if (nargout > 3) if (deficient) ucov = pinv (XW' * XW); else ucov = R \ Q' / XW'; endif endif ## Calculate the Hat matrix if (nargout > 4) w = diag (W); rw = sqrt (w); if (deficient) P = XW * pinv (XW); hat = diag (1 ./ rw) * P * diag (rw); else Q1 = diag (1 ./ rw) * Q; Q2 = diag (rw) * Q; hat = Q1 * Q2'; endif endif endfunction %!demo %! %! # Two-sample unpaired test on independent samples (equivalent to Student's %! # t-test). Note that the absolute value of t-statistic can be obtained by %! # taking the square root of the reported F statistic. In this example, %! # t = sqrt (1.44) = 1.20. %! %! score = [54 23 45 54 45 43 34 65 77 46 65]'; %! gender = {'male' 'male' 'male' 'male' 'male' 'female' 'female' 'female' ... %! 'female' 'female' 'female'}'; %! %! [P, ATAB, STATS] = anovan (score, gender, 'display', 'on', 'varnames', 'gender'); %!demo %! %! # Two-sample paired test on dependent or matched samples equivalent to a %! # paired t-test. As for the first example, the t-statistic can be obtained by %! # taking the square root of the reported F statistic. Naming subject as a %! # random factor (') keeps the treatment x subject interaction in the model %! # and makes its mean square the denominator of the test on treatment. %! %! score = [4.5 5.6; 3.7 6.4; 5.3 6.4; 5.4 6.0; 3.9 5.7]'; %! treatment = {'before' 'after'; 'before' 'after'; 'before' 'after'; %! 'before' 'after'; 'before' 'after'}'; %! subject = {'GS' 'GS'; 'JM' 'JM'; 'HM' 'HM'; 'JW' 'JW'; 'PS' 'PS'}'; %! %! [P, ATAB, STATS] = anovan (score(:), {treatment(:), subject(:)}, ... %! 'model', 'full', 'random', 2, 'sstype', 2, ... %! 'varnames', {'treatment', 'subject'}, ... %! 'display', 'on'); %!demo %! %! # One-way ANOVA on the data from a study on the strength of structural beams, %! # in Hogg and Ledolter (1987) Engineering Statistics. New York: MacMillan %! %! strength = [82 86 79 83 84 85 86 87 74 82 ... %! 78 75 76 77 79 79 77 78 82 79]'; %! alloy = {'st','st','st','st','st','st','st','st', ... %! 'al1','al1','al1','al1','al1','al1', ... %! 'al2','al2','al2','al2','al2','al2'}'; %! %! [P, ATAB, STATS] = anovan (strength, alloy, 'display', 'on', ... %! 'varnames', 'alloy'); %!demo %! %! # One-way repeated measures ANOVA on the data from a study on the number of %! # words recalled by 10 subjects for three time conditions, in Loftus & Masson %! # (1994) Psychon Bull Rev. 1(4):476-490, Table 2. Naming subject as a random %! # factor (') keeps the seconds x subject interaction in the model and makes %! # its mean square the denominator of the test on seconds. %! %! words = [10 13 13; 6 8 8; 11 14 14; 22 23 25; 16 18 20; ... %! 15 17 17; 1 1 4; 12 15 17; 9 12 12; 8 9 12]; %! seconds = [1 2 5; 1 2 5; 1 2 5; 1 2 5; 1 2 5; ... %! 1 2 5; 1 2 5; 1 2 5; 1 2 5; 1 2 5;]; %! subject = [ 1 1 1; 2 2 2; 3 3 3; 4 4 4; 5 5 5; ... %! 6 6 6; 7 7 7; 8 8 8; 9 9 9; 10 10 10]; %! %! [P, ATAB, STATS] = anovan (words(:), {seconds(:), subject(:)}, ... %! 'model', 'full', 'random', 2, 'sstype', 2, ... %! 'display', 'on', 'varnames', {'seconds', 'subject'}); %!demo %! %! # Balanced two-way ANOVA with interaction on the data from a study of popcorn %! # brands and popper types, in Hogg and Ledolter (1987) Engineering Statistics. %! # New York: MacMillan %! %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! brands = {'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'}; %! popper = {'oil', 'oil', 'oil'; 'oil', 'oil', 'oil'; 'oil', 'oil', 'oil'; ... %! 'air', 'air', 'air'; 'air', 'air', 'air'; 'air', 'air', 'air'}; %! %! [P, ATAB, STATS] = anovan (popcorn(:), {brands(:), popper(:)}, ... %! 'display', 'on', 'model', 'full', ... %! 'varnames', {'brands', 'popper'}); %!demo %! %! # Unbalanced two-way ANOVA (2x2) on the data from a study on the effects of %! # gender and having a college degree on salaries of company employees, %! # in Maxwell, Delaney and Kelly (2018): Chapter 7, Table 15 %! %! salary = [24 26 25 24 27 24 27 23 15 17 20 16, ... %! 25 29 27 19 18 21 20 21 22 19]'; %! gender = {'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f'... %! 'm' 'm' 'm' 'm' 'm' 'm' 'm' 'm' 'm' 'm'}'; %! degree = [1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 0 0 0 0 0 0 0]'; %! %! [P, ATAB, STATS] = anovan (salary, {gender, degree}, 'model', 'full', ... %! 'sstype', 3, 'display', 'on', 'varnames', ... %! {'gender', 'degree'}); %!demo %! %! # Unbalanced two-way ANOVA (3x2) on the data from a study of the effect of %! # adding sugar and/or milk on the tendency of coffee to make people babble, %! # in from Navarro (2019): 16.10 %! %! sugar = {'real' 'fake' 'fake' 'real' 'real' 'real' 'none' 'none' 'none' ... %! 'fake' 'fake' 'fake' 'real' 'real' 'real' 'none' 'none' 'fake'}'; %! milk = {'yes' 'no' 'no' 'yes' 'yes' 'no' 'yes' 'yes' 'yes' ... %! 'no' 'no' 'yes' 'no' 'no' 'no' 'no' 'no' 'yes'}'; %! babble = [4.6 4.4 3.9 5.6 5.1 5.5 3.9 3.5 3.7... %! 5.6 4.7 5.9 6.0 5.4 6.6 5.8 5.3 5.7]'; %! %! [P, ATAB, STATS] = anovan (babble, {sugar, milk}, 'model', 'full', ... %! 'sstype', 3, 'display', 'on', ... %! 'varnames', {'sugar', 'milk'}); %!demo %! %! # Unbalanced three-way ANOVA (3x2x2) on the data from a study of the effects %! # of three different drugs, biofeedback and diet on patient blood pressure, %! # adapted* from Maxwell, Delaney and Kelly (2018): Chapter 8, Table 12 %! # * Missing values introduced to make the sample sizes unequal to test the %! # calculation of different types of sums-of-squares %! %! drug = {'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' ... %! 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X'; %! 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' ... %! 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y'; %! 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' ... %! 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z'}; %! feedback = [1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0; %! 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0; %! 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0]; %! diet = [0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1; %! 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1; %! 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1]; %! BP = [170 175 165 180 160 158 161 173 157 152 181 190 ... %! 173 194 197 190 176 198 164 190 169 164 176 175; %! 186 194 201 215 219 209 164 166 159 182 187 174 ... %! 189 194 217 206 199 195 171 173 196 199 180 NaN; %! 180 187 199 170 204 194 162 184 183 156 180 173 ... %! 202 228 190 206 224 204 205 199 170 160 NaN NaN]; %! %! [P, ATAB, STATS] = anovan (BP(:), {drug(:), feedback(:), diet(:)}, ... %! 'model', 'full', 'sstype', 3, ... %! 'display', 'on', ... %! 'varnames', {'drug', 'feedback', 'diet'}); %!demo %! %! # Balanced three-way ANOVA (2x2x2) with one of the factors being a blocking %! # factor. The data is from a randomized block design study on the effects %! # of antioxidant treatment on glutathione-S-transferase (GST) levels in %! # different mouse strains, from Festing (2014), ILAR Journal, 55(3):427-476. %! # Naming block as a random factor (') keeps every interaction with block in %! # the model; each F ratio then takes the denominator its expected mean %! # square calls for, which for block itself is a combination of three mean %! # squares carried on fractional degrees of freedom. %! %! measurement = [444 614 423 625 408 856 447 719 ... %! 764 831 586 782 609 1002 606 766]'; %! strain= {'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola', ... %! 'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola'}'; %! treatment={'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T'}'; %! block = [1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2]'; %! %! [P, ATAB, STATS] = anovan (measurement/10, {strain, treatment, block}, ... %! 'sstype', 2, 'model', 'full', 'random', 3, ... %! 'display', 'on', ... %! 'varnames', {'strain', 'treatment', 'block'}); %!demo %! %! # One-way ANCOVA on data from a study of the additive effects of species %! # and temperature on chirpy pulses of crickets, from Stitch, The Worst Stats %! # Text eveR %! %! pulse = [67.9 65.1 77.3 78.7 79.4 80.4 85.8 86.6 87.5 89.1 ... %! 98.6 100.8 99.3 101.7 44.3 47.2 47.6 49.6 50.3 51.8 ... %! 60 58.5 58.9 60.7 69.8 70.9 76.2 76.1 77 77.7 84.7]'; %! temp = [20.8 20.8 24 24 24 24 26.2 26.2 26.2 26.2 28.4 ... %! 29 30.4 30.4 17.2 18.3 18.3 18.3 18.9 18.9 20.4 ... %! 21 21 22.1 23.5 24.2 25.9 26.5 26.5 26.5 28.6]'; %! species = {'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' ... %! 'ex' 'ex' 'ex' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' ... %! 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv'}; %! %! [P, ATAB, STATS] = anovan (pulse, {species, temp}, 'model', 'linear', ... %! 'continuous', 2, 'sstype', 'h', 'display', 'on', ... %! 'varnames', {'species', 'temp'}); %!demo %! %! # Factorial ANCOVA on data from a study of the effects of treatment and %! # exercise on stress reduction score after adjusting for age. Data from R %! # datarium package). %! %! score = [95.6 82.2 97.2 96.4 81.4 83.6 89.4 83.8 83.3 85.7 ... %! 97.2 78.2 78.9 91.8 86.9 84.1 88.6 89.8 87.3 85.4 ... %! 81.8 65.8 68.1 70.0 69.9 75.1 72.3 70.9 71.5 72.5 ... %! 84.9 96.1 94.6 82.5 90.7 87.0 86.8 93.3 87.6 92.4 ... %! 100. 80.5 92.9 84.0 88.4 91.1 85.7 91.3 92.3 87.9 ... %! 91.7 88.6 75.8 75.7 75.3 82.4 80.1 86.0 81.8 82.5]'; %! treatment = {'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' ... %! 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' ... %! 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' ... %! 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' ... %! 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' ... %! 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no'}'; %! exercise = {'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' ... %! 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' ... %! 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' ... %! 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' ... %! 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' ... %! 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi'}'; %! age = [59 65 70 66 61 65 57 61 58 55 62 61 60 59 55 57 60 63 62 57 ... %! 58 56 57 59 59 60 55 53 55 58 68 62 61 54 59 63 60 67 60 67 ... %! 75 54 57 62 65 60 58 61 65 57 56 58 58 58 52 53 60 62 61 61]'; %! %! [P, ATAB, STATS] = anovan (score, {treatment, exercise, age}, ... %! 'model', [1 0 0; 0 1 0; 0 0 1; 1 1 0], ... %! 'continuous', 3, 'sstype', 'h', 'display', 'on', ... %! 'varnames', {'treatment', 'exercise', 'age'}); %!demo %! %! # Unbalanced one-way ANOVA with custom, orthogonal contrasts. The statistics %! # relating to the contrasts are shown in the table of model parameters, and %! # can be retrieved from the STATS.coeffs output. %! %! dv = [ 8.706 10.362 11.552 6.941 10.983 10.092 6.421 14.943 15.931 ... %! 22.968 18.590 16.567 15.944 21.637 14.492 17.965 18.851 22.891 ... %! 22.028 16.884 17.252 18.325 25.435 19.141 21.238 22.196 18.038 ... %! 22.628 31.163 26.053 24.419 32.145 28.966 30.207 29.142 33.212 ... %! 25.694 ]'; %! g = [1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 3 3 ... %! 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5]'; %! C = [ 0.4001601 0.3333333 0.5 0.0 %! 0.4001601 0.3333333 -0.5 0.0 %! 0.4001601 -0.6666667 0.0 0.0 %! -0.6002401 0.0000000 0.0 0.5 %! -0.6002401 0.0000000 0.0 -0.5]; %! %! [P,ATAB, STATS] = anovan (dv, g, 'contrasts', C, 'varnames', 'score', ... %! 'alpha', 0.05, 'display', 'on'); %!demo %! %! # One-way ANOVA with the linear model fit by weighted least squares to %! # account for heteroskedasticity. In this example, the variance appears %! # proportional to the outcome, so weights have been estimated by initially %! # fitting the model without weights and regressing the absolute residuals on %! # the fitted values. Although this data could have been analysed by Welch's %! # ANOVA test, the approach here can generalize to ANOVA models with more than %! # one factor. %! %! g = [1, 1, 1, 1, 1, 1, 1, 1, ... %! 2, 2, 2, 2, 2, 2, 2, 2, ... %! 3, 3, 3, 3, 3, 3, 3, 3]'; %! y = [13, 16, 16, 7, 11, 5, 1, 9, ... %! 10, 25, 66, 43, 47, 56, 6, 39, ... %! 11, 39, 26, 35, 25, 14, 24, 17]'; %! %! [P,ATAB,STATS] = anovan (y, g, 'display', 'off'); %! fitted = STATS.X * STATS.coeffs(:,1); # fitted values %! b = polyfit (fitted, abs (STATS.resid), 1); %! v = polyval (b, fitted); # Variance as a function of the fitted values %! figure ('Name', 'Regression of the absolute residuals on the fitted values'); %! plot (fitted, abs (STATS.resid),'ob');hold on; plot (fitted,v,'-r'); hold off; %! xlabel ('Fitted values'); ylabel ('Absolute residuals'); %! %! [P,ATAB,STATS] = anovan (y, g, 'weights', v.^-1); ## Test 1 for anovan example 1 ## Test compares anovan to results from MATLAB's anovan and ttest2 functions %!test %! score = [54 23 45 54 45 43 34 65 77 46 65]'; %! gender = {'male' 'male' 'male' 'male' 'male' 'female' 'female' 'female' ... %! 'female' 'female' 'female'}'; %! %! [P, T, STATS] = anovan (score,gender,'display','off'); %! assert_equal (P(1), 0.2612876773271042, 1e-09); # compared to p calculated by MATLAB anovan %! assert_equal (sqrt (T{2,6}), abs (1.198608733288208), 1e-09); # compared to abs(t) calculated from sqrt(F) by MATLAB anovan %! assert_equal (P(1), 0.2612876773271047, 1e-09); # compared to p calculated by MATLAB ttest2 %! assert_equal (sqrt (T{2,6}), abs (-1.198608733288208), 1e-09); # compared to abs(t) calculated by MATLAB ttest2 ## Test 2 for anovan example 2 ## Test compares anovan to results from MATLAB's anovan and ttest functions %!test %! score = [4.5 5.6; 3.7 6.4; 5.3 6.4; 5.4 6.0; 3.9 5.7]'; %! treatment = {'before' 'after'; 'before' 'after'; 'before' 'after'; %! 'before' 'after'; 'before' 'after'}'; %! subject = {'GS' 'GS'; 'JM' 'JM'; 'HM' 'HM'; 'JW' 'JW'; 'PS' 'PS'}'; %! %! [P, ATAB, STATS] = anovan (score(:),{treatment(:),subject(:)},'display','off','sstype',2); %! assert_equal (P(1), 0.016004356735364, 1e-09); # compared to p calculated by MATLAB anovan %! assert_equal (sqrt (ATAB{2,6}), abs (4.00941576558195), 1e-09); # compared to abs(t) calculated from sqrt(F) by MATLAB anovan %! assert_equal (P(1), 0.016004356735364, 1e-09); # compared to p calculated by MATLAB ttest2 %! assert_equal (sqrt (ATAB{2,6}), abs (-4.00941576558195), 1e-09); # compared to abs(t) calculated by MATLAB ttest2 ## Test 3 for anovan example 3 ## Test compares anovan to results from MATLAB's anovan and anova1 functions %!test %! strength = [82 86 79 83 84 85 86 87 74 82 ... %! 78 75 76 77 79 79 77 78 82 79]'; %! alloy = {'st','st','st','st','st','st','st','st', ... %! 'al1','al1','al1','al1','al1','al1', ... %! 'al2','al2','al2','al2','al2','al2'}'; %! %! [P, ATAB, STATS] = anovan (strength,{alloy},'display','off'); %! assert_equal (P(1), 0.000152643638830491, 1e-09); %! assert_equal (ATAB{2,6}, 15.4, 1e-09); ## Test 4 for anovan example 4 ## Test compares anovan to results from MATLAB's anovan function %!test %! words = [10 13 13; 6 8 8; 11 14 14; 22 23 25; 16 18 20; ... %! 15 17 17; 1 1 4; 12 15 17; 9 12 12; 8 9 12]; %! subject = [ 1 1 1; 2 2 2; 3 3 3; 4 4 4; 5 5 5; ... %! 6 6 6; 7 7 7; 8 8 8; 9 9 9; 10 10 10]; %! seconds = [1 2 5; 1 2 5; 1 2 5; 1 2 5; 1 2 5; ... %! 1 2 5; 1 2 5; 1 2 5; 1 2 5; 1 2 5;]; %! %! [P, ATAB, STATS] = anovan (words(:),{seconds(:),subject(:)},'model','full','random',2,'sstype',2,'display','off'); %! assert_equal (P(1), 1.51865926758752e-07, 1e-09); %! assert_equal (ATAB{2,2}, 52.2666666666667, 1e-09); %! assert_equal (ATAB{3,2}, 942.533333333333, 1e-09); %! assert_equal (ATAB{4,2}, 11.0666666666667, 1e-09); ## Test 5 for anovan example 5 ## Test compares anovan to results from MATLAB's anovan function %!test %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! brands = {'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'}; %! popper = {'oil', 'oil', 'oil'; 'oil', 'oil', 'oil'; 'oil', 'oil', 'oil'; ... %! 'air', 'air', 'air'; 'air', 'air', 'air'; 'air', 'air', 'air'}; %! %! [P, ATAB, STATS] = anovan (popcorn(:),{brands(:),popper(:)},'display','off','model','full'); %! assert_equal (P(1), 7.67895738278171e-07, 1e-09); %! assert_equal (P(2), 0.000100373896304998, 1e-09); %! assert_equal (P(3), 0.746215396636649, 1e-09); %! assert_equal (ATAB{2,6}, 56.7, 1e-09); %! assert_equal (ATAB{3,6}, 32.4, 1e-09); %! assert_equal (ATAB{4,6}, 0.29999999999997, 1e-09); ## Test 6 for anovan example 6 ## Test compares anovan to results from MATLAB's anovan function %!test %! salary = [24 26 25 24 27 24 27 23 15 17 20 16, ... %! 25 29 27 19 18 21 20 21 22 19]'; %! gender = {'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f' 'f'... %! 'm' 'm' 'm' 'm' 'm' 'm' 'm' 'm' 'm' 'm'}'; %! degree = [1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 0 0 0 0 0 0 0]'; %! %! [P, ATAB, STATS] = anovan (salary,{gender,degree},'model','full','sstype',1,'display','off'); %! assert_equal (P(1), 0.747462549227232, 1e-09); %! assert_equal (P(2), 1.03809316857694e-08, 1e-09); %! assert_equal (P(3), 0.523689833702691, 1e-09); %! assert_equal (ATAB{2,2}, 0.296969696969699, 1e-09); %! assert_equal (ATAB{3,2}, 272.391841491841, 1e-09); %! assert_equal (ATAB{4,2}, 1.17482517482512, 1e-09); %! assert_equal (ATAB{5,2}, 50.0000000000001, 1e-09); %! [P, ATAB, STATS] = anovan (salary,{degree,gender},'model','full','sstype',1,'display','off'); %! assert_equal (P(1), 2.53445097305047e-08, 1e-09); %! assert_equal (P(2), 0.00388133678528749, 1e-09); %! assert_equal (P(3), 0.523689833702671, 1e-09); %! assert_equal (ATAB{2,2}, 242.227272727273, 1e-09); %! assert_equal (ATAB{3,2}, 30.4615384615384, 1e-09); %! assert_equal (ATAB{4,2}, 1.17482517482523, 1e-09); %! assert_equal (ATAB{5,2}, 50.0000000000001, 1e-09); %! [P, ATAB, STATS] = anovan (salary,{gender,degree},'model','full','sstype',2,'display','off'); %! assert_equal (P(1), 0.00388133678528743, 1e-09); %! assert_equal (P(2), 1.03809316857694e-08, 1e-09); %! assert_equal (P(3), 0.523689833702691, 1e-09); %! assert_equal (ATAB{2,2}, 30.4615384615385, 1e-09); %! assert_equal (ATAB{3,2}, 272.391841491841, 1e-09); %! assert_equal (ATAB{4,2}, 1.17482517482512, 1e-09); %! assert_equal (ATAB{5,2}, 50.0000000000001, 1e-09); %! [P, ATAB, STATS] = anovan (salary,{gender,degree},'model','full','sstype',3,'display','off'); %! assert_equal (P(1), 0.00442898146583742, 1e-09); %! assert_equal (P(2), 1.30634252053587e-08, 1e-09); %! assert_equal (P(3), 0.523689833702691, 1e-09); %! assert_equal (ATAB{2,2}, 29.3706293706294, 1e-09); %! assert_equal (ATAB{3,2}, 264.335664335664, 1e-09); %! assert_equal (ATAB{4,2}, 1.17482517482512, 1e-09); %! assert_equal (ATAB{5,2}, 50.0000000000001, 1e-09); ## Test 7 for anovan example 7 ## Test compares anovan to results from MATLAB's anovan function %!test %! sugar = {'real' 'fake' 'fake' 'real' 'real' 'real' 'none' 'none' 'none' ... %! 'fake' 'fake' 'fake' 'real' 'real' 'real' 'none' 'none' 'fake'}'; %! milk = {'yes' 'no' 'no' 'yes' 'yes' 'no' 'yes' 'yes' 'yes' ... %! 'no' 'no' 'yes' 'no' 'no' 'no' 'no' 'no' 'yes'}'; %! babble = [4.6 4.4 3.9 5.6 5.1 5.5 3.9 3.5 3.7... %! 5.6 4.7 5.9 6.0 5.4 6.6 5.8 5.3 5.7]'; %! %! [P, ATAB, STATS] = anovan (babble,{sugar,milk},'model','full','sstype',1,'display','off'); %! assert_equal (P(1), 0.0108632139833963, 1e-09); %! assert_equal (P(2), 0.0810606976703546, 1e-09); %! assert_equal (P(3), 0.00175433329935627, 1e-09); %! assert_equal (ATAB{2,2}, 3.55752380952381, 1e-09); %! assert_equal (ATAB{3,2}, 0.956108477471702, 1e-09); %! assert_equal (ATAB{4,2}, 5.94386771300448, 1e-09); %! assert_equal (ATAB{5,2}, 3.1625, 1e-09); %! [P, ATAB, STATS] = anovan (babble,{milk,sugar},'model','full','sstype',1,'display','off'); %! assert_equal (P(1), 0.0373333189297505, 1e-09); %! assert_equal (P(2), 0.017075098787169, 1e-09); %! assert_equal (P(3), 0.00175433329935627, 1e-09); %! assert_equal (ATAB{2,2}, 1.444, 1e-09); %! assert_equal (ATAB{3,2}, 3.06963228699552, 1e-09); %! assert_equal (ATAB{4,2}, 5.94386771300448, 1e-09); %! assert_equal (ATAB{5,2}, 3.1625, 1e-09); %! [P, ATAB, STATS] = anovan (babble,{sugar,milk},'model','full','sstype',2,'display','off'); %! assert_equal (P(1), 0.017075098787169, 1e-09); %! assert_equal (P(2), 0.0810606976703546, 1e-09); %! assert_equal (P(3), 0.00175433329935627, 1e-09); %! assert_equal (ATAB{2,2}, 3.06963228699552, 1e-09); %! assert_equal (ATAB{3,2}, 0.956108477471702, 1e-09); %! assert_equal (ATAB{4,2}, 5.94386771300448, 1e-09); %! assert_equal (ATAB{5,2}, 3.1625, 1e-09); %! [P, ATAB, STATS] = anovan (babble,{sugar,milk},'model','full','sstype',3,'display','off'); %! assert_equal (P(1), 0.0454263063473954, 1e-09); %! assert_equal (P(2), 0.0746719907091438, 1e-09); %! assert_equal (P(3), 0.00175433329935627, 1e-09); %! assert_equal (ATAB{2,2}, 2.13184977578476, 1e-09); %! assert_equal (ATAB{3,2}, 1.00413461538462, 1e-09); %! assert_equal (ATAB{4,2}, 5.94386771300448, 1e-09); %! assert_equal (ATAB{5,2}, 3.1625, 1e-09); ## Test 8 for anovan example 8 ## Test compares anovan to results from MATLAB's anovan function %!test %! drug = {'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' ... %! 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X' 'X'; %! 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' ... %! 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y' 'Y'; %! 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' ... %! 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z' 'Z'}; %! feedback = [1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0; %! 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0; %! 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0]; %! diet = [0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1; %! 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1; %! 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 1 1]; %! BP = [170 175 165 180 160 158 161 173 157 152 181 190 ... %! 173 194 197 190 176 198 164 190 169 164 176 175; %! 186 194 201 215 219 209 164 166 159 182 187 174 ... %! 189 194 217 206 199 195 171 173 196 199 180 NaN; %! 180 187 199 170 204 194 162 184 183 156 180 173 ... %! 202 228 190 206 224 204 205 199 170 160 NaN NaN]; %! %! [P, ATAB, STATS] = anovan (BP(:),{drug(:),feedback(:),diet(:)},'model','full','sstype', 1,'display','off'); %! assert_equal (P(1), 7.02561843825325e-05, 1e-09); %! assert_equal (P(2), 0.000425806013389362, 1e-09); %! assert_equal (P(3), 6.16780773446401e-07, 1e-09); %! assert_equal (P(4), 0.261347622678438, 1e-09); %! assert_equal (P(5), 0.0542278432357043, 1e-09); %! assert_equal (P(6), 0.590353225626655, 1e-09); %! assert_equal (P(7), 0.0861628249564267, 1e-09); %! assert_equal (ATAB{2,2}, 3614.70355731226, 1e-09); %! assert_equal (ATAB{3,2}, 2227.46639771024, 1e-09); %! assert_equal (ATAB{4,2}, 5008.25614451819, 1e-09); %! assert_equal (ATAB{5,2}, 437.066007908781, 1e-09); %! assert_equal (ATAB{6,2}, 976.180770397332, 1e-09); %! assert_equal (ATAB{7,2}, 46.616653365254, 1e-09); %! assert_equal (ATAB{8,2}, 814.345251396648, 1e-09); %! assert_equal (ATAB{9,2}, 9065.8, 1e-09); %! [P, ATAB, STATS] = anovan (BP(:),{drug(:),feedback(:),diet(:)},'model','full','sstype',2,'display','off'); %! assert_equal (P(1), 9.4879638470754e-05, 1e-09); %! assert_equal (P(2), 0.00124177666315809, 1e-09); %! assert_equal (P(3), 6.86162012732911e-07, 1e-09); %! assert_equal (P(4), 0.260856132341256, 1e-09); %! assert_equal (P(5), 0.0523758623892078, 1e-09); %! assert_equal (P(6), 0.590353225626655, 1e-09); %! assert_equal (P(7), 0.0861628249564267, 1e-09); %! assert_equal (ATAB{2,2}, 3481.72176560122, 1e-09); %! assert_equal (ATAB{3,2}, 1837.08812970469, 1e-09); %! assert_equal (ATAB{4,2}, 4957.20277938622, 1e-09); %! assert_equal (ATAB{5,2}, 437.693674777847, 1e-09); %! assert_equal (ATAB{6,2}, 988.431929811402, 1e-09); %! assert_equal (ATAB{7,2}, 46.616653365254, 1e-09); %! assert_equal (ATAB{8,2}, 814.345251396648, 1e-09); %! assert_equal (ATAB{9,2}, 9065.8, 1e-09); %! [P, ATAB, STATS] = anovan (BP(:),{drug(:),feedback(:),diet(:)},'model','full','sstype', 3,'display','off'); %! assert_equal (P(1), 0.000106518678028207, 1e-09); %! assert_equal (P(2), 0.00125371366571508, 1e-09); %! assert_equal (P(3), 5.30813260778464e-07, 1e-09); %! assert_equal (P(4), 0.308353667232981, 1e-09); %! assert_equal (P(5), 0.0562901327343161, 1e-09); %! assert_equal (P(6), 0.599091042141092, 1e-09); %! assert_equal (P(7), 0.0861628249564267, 1e-09); %! assert_equal (ATAB{2,2}, 3430.88156424581, 1e-09); %! assert_equal (ATAB{3,2}, 1833.68031496063, 1e-09); %! assert_equal (ATAB{4,2}, 5080.48346456693, 1e-09); %! assert_equal (ATAB{5,2}, 382.07709497207, 1e-09); %! assert_equal (ATAB{6,2}, 963.037988826813, 1e-09); %! assert_equal (ATAB{7,2}, 44.4519685039322, 1e-09); %! assert_equal (ATAB{8,2}, 814.345251396648, 1e-09); %! assert_equal (ATAB{9,2}, 9065.8, 1e-09); ## Test 9 for anovan example 9 ## Test compares anovan to results from MATLAB's anovan function %!test %! measurement = [444 614 423 625 408 856 447 719 ... %! 764 831 586 782 609 1002 606 766]'; %! strain= {'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola', ... %! 'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola'}'; %! treatment={'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T' 'C' 'T'}'; %! block = [1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2]'; %! %! [P, ATAB, STATS] = anovan (measurement/10,{strain,treatment,block},'model','full','random',3,'display','off'); %! assert_equal (P(1:6), [0.288811428913179; 0.091455278902114; ... %! 0.042134889025806; 0.010944863181481; ... %! 0.061814763198376; 0.066584161056625], 1e-12); %! assert_equal (P(7), NaN); %! assert_equal (cell2mat (ATAB(2:8,2)), ... %! [286.132499999999; 2275.28999999999; 1242.5625; ... %! 495.905000000000; 141.472499999998; 47.6099999999987; ... %! 17.9249999999992], 1e-9); %! assert_equal (ATAB{9,2}, 0); %! assert_equal (ATAB{10,2}, 4506.8975, 1e-9); %! assert_equal (STATS.msdenom, ... %! [47.1575; 47.61; 88.7925; 5.975; 5.975; 5.975; 0], 1e-9); %! assert_equal (STATS.dfdenom, ... %! [3; 1; 2.610727841363640; 3; 3; 3; 0], 1e-12); %! assert_equal (STATS.varest, ... %! [144.22125; 20.59125; 10.40875; 5.975; 0], 1e-9); ## Test 10 for anovan example 10 ## Test compares anovan to results from MATLAB's anovan function %!test %! pulse = [67.9 65.1 77.3 78.7 79.4 80.4 85.8 86.6 87.5 89.1 ... %! 98.6 100.8 99.3 101.7 44.3 47.2 47.6 49.6 50.3 51.8 ... %! 60 58.5 58.9 60.7 69.8 70.9 76.2 76.1 77 77.7 84.7]'; %! temp = [20.8 20.8 24 24 24 24 26.2 26.2 26.2 26.2 28.4 ... %! 29 30.4 30.4 17.2 18.3 18.3 18.3 18.9 18.9 20.4 ... %! 21 21 22.1 23.5 24.2 25.9 26.5 26.5 26.5 28.6]'; %! species = {'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' 'ex' ... %! 'ex' 'ex' 'ex' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' ... %! 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv' 'niv'}; %! %! [P, ATAB, STATS] = anovan (pulse,{species,temp},'model','linear','continuous',2,'sstype','h','display','off'); %! assert_equal (P(1), 6.27153318786007e-14, 1e-09); %! assert_equal (P(2), 2.48773241196644e-25, 1e-09); %! assert_equal (ATAB{2,2}, 598.003953318404, 1e-09); %! assert_equal (ATAB{3,2}, 4376.08256843712, 1e-09); %! assert_equal (ATAB{4,2}, 89.3498685376726, 1e-09); %! assert_equal (ATAB{2,6}, 187.399388123951, 1e-09); %! assert_equal (ATAB{3,6}, 1371.35413763454, 1e-09); ## Test 11 for anovan example 11 ## Test compares anovan to results from MATLAB's anovan function %!test %! score = [95.6 82.2 97.2 96.4 81.4 83.6 89.4 83.8 83.3 85.7 ... %! 97.2 78.2 78.9 91.8 86.9 84.1 88.6 89.8 87.3 85.4 ... %! 81.8 65.8 68.1 70.0 69.9 75.1 72.3 70.9 71.5 72.5 ... %! 84.9 96.1 94.6 82.5 90.7 87.0 86.8 93.3 87.6 92.4 ... %! 100. 80.5 92.9 84.0 88.4 91.1 85.7 91.3 92.3 87.9 ... %! 91.7 88.6 75.8 75.7 75.3 82.4 80.1 86.0 81.8 82.5]'; %! treatment = {'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' ... %! 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' ... %! 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' ... %! 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' ... %! 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' ... %! 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no'}'; %! exercise = {'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' ... %! 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' ... %! 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' ... %! 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' ... %! 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' ... %! 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi'}'; %! age = [59 65 70 66 61 65 57 61 58 55 62 61 60 59 55 57 60 63 62 57 ... %! 58 56 57 59 59 60 55 53 55 58 68 62 61 54 59 63 60 67 60 67 ... %! 75 54 57 62 65 60 58 61 65 57 56 58 58 58 52 53 60 62 61 61]'; %! %! [P, ATAB, STATS] = anovan (score,{treatment,exercise,age},'model','full','continuous',3,'sstype','h','display','off'); %! assert_equal (P(5), 0.9245630968248468, 1e-09); %! assert_equal (P(6), 0.791115159521822, 1e-09); %! assert_equal (P(7), 0.9296668751457956, 1e-09); %! [P, ATAB, STATS] = anovan (score,{treatment,exercise,age},'model',[1 0 0; 0 1 0; 0 0 1; 1 1 0],'continuous',3,'sstype','h','display','off'); %! assert_equal (P(1), 0.00158132928938933, 1e-09); %! assert_equal (P(2), 2.12537505039986e-07, 1e-09); %! assert_equal (P(3), 0.00390292555160047, 1e-09); %! assert_equal (P(4), 0.0164086580775543, 1e-09); %! assert_equal (ATAB{2,6}, 11.0956027650549, 1e-09); %! assert_equal (ATAB{3,6}, 20.8195665467178, 1e-09); %! assert_equal (ATAB{4,6}, 9.10966630720186, 1e-09); %! assert_equal (ATAB{5,6}, 4.4457923698584, 1e-09); ## Test 12 for anovan example 12 ## Test compares anovan regression coefficients to R: ## https://www.uvm.edu/~statdhtx/StatPages/Unequal-ns/Unequal_n%27s_contrasts.html %!test %! dv = [ 8.706 10.362 11.552 6.941 10.983 10.092 6.421 14.943 15.931 ... %! 22.968 18.590 16.567 15.944 21.637 14.492 17.965 18.851 22.891 ... %! 22.028 16.884 17.252 18.325 25.435 19.141 21.238 22.196 18.038 ... %! 22.628 31.163 26.053 24.419 32.145 28.966 30.207 29.142 33.212 ... %! 25.694 ]'; %! g = [1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5]'; %! C = [ 0.4001601 0.3333333 0.5 0.0 %! 0.4001601 0.3333333 -0.5 0.0 %! 0.4001601 -0.6666667 0.0 0.0 %! -0.6002401 0.0000000 0.0 0.5 %! -0.6002401 0.0000000 0.0 -0.5]; %! %! [P,ATAB,STATS] = anovan (dv,g,'contrasts',{C},'display','off'); %! assert_equal (STATS.coeffs(1,1), 19.4001, 1e-04); %! assert_equal (STATS.coeffs(2,1), -9.3297, 1e-04); %! assert_equal (STATS.coeffs(3,1), -5.0000, 1e-04); %! assert_equal (STATS.coeffs(4,1), -8.0000, 1e-04); %! assert_equal (STATS.coeffs(5,1), -8.0000, 1e-04); %! assert_equal (STATS.coeffs(1,2), 0.4831, 1e-04); %! assert_equal (STATS.coeffs(2,2), 0.9694, 1e-04); %! assert_equal (STATS.coeffs(3,2), 1.3073, 1e-04); %! assert_equal (STATS.coeffs(4,2), 1.6411, 1e-04); %! assert_equal (STATS.coeffs(5,2), 1.4507, 1e-04); %! assert_equal (STATS.coeffs(1,5), 40.161, 1e-03); %! assert_equal (STATS.coeffs(2,5), -9.624, 1e-03); %! assert_equal (STATS.coeffs(3,5), -3.825, 1e-03); %! assert_equal (STATS.coeffs(4,5), -4.875, 1e-03); %! assert_equal (STATS.coeffs(5,5), -5.515, 1e-03); %! assert_equal (STATS.coeffs(2,6), 5.74e-11, 1e-12); %! assert_equal (STATS.coeffs(3,6), 0.000572, 1e-06); %! assert_equal (STATS.coeffs(4,6), 2.86e-05, 1e-07); %! assert_equal (STATS.coeffs(5,6), 4.44e-06, 1e-08); ## Interaction columns preserve factor-column ordering. %!test %! y = (1:12)'; %! g1 = repmat ([1; 2; 3], 4, 1); %! g2 = kron ([1; 2], ones (6, 1)); %! [~, ~, stats] = anovan (y, {g1, g2}, 'model', 'full', ... %! 'display', 'off'); %! assert_equal (stats.X(:,5), stats.X(:,2) .* stats.X(:,4)); %! assert_equal (stats.X(:,6), stats.X(:,3) .* stats.X(:,4)); ## Full six-factor interactions preserve every model term. %!test %! n = 128; %! group = cell (1, 6); %! for k = 1:6 %! group{k} = mod (floor ((0:n-1)' / 2^(k-1)), 2) + 1; %! endfor %! [p, ~, stats, terms] = anovan ((1:n)', group, 'model', 'full', ... %! 'display', 'off'); %! assert_equal (rows (terms), 63); %! assert_equal (columns (stats.X), 64); %! assert_equal (numel (p), 63); ## A one-level factor has no estimable main effect. %!test %! group = ones (3, 1); %! [p, tbl, stats] = anovan ((1:3)', group, ... %! 'sstype', 2, 'display', 'off'); %! assert_equal (p, NaN); %! assert_equal (tbl{2, 2}, 0); %! assert_equal (tbl{2, 3}, 0); %! assert_equal (tbl{2, 6}, NaN); %! assert_equal (size (stats.X), [3, 1]); ## Categorical factors retain their declared order and omit missing observations. %!test %! group = categorical ([3; 3; NaN; 1; 1], [3, 2, 1]); %! [p, tbl, stats] = anovan ((1:5)', group, 'sstype', 2, 'display', 'off'); %! [p_ref, tbl_ref] = anovan ([1; 2; 4; 5], [1; 1; 2; 2], ... %! 'sstype', 2, 'display', 'off'); %! assert_equal (p, p_ref, 1e-12); %! assert_equal (tbl, tbl_ref); %! assert_equal (stats.grpnames{1}, {'3'; '1'}); %! assert_equal (stats.Y, [1; 2; 4; 5]); ## Terms matrices select factors by column rather than row position. %!test %! A = [1; 1; 2; 2]; %! B = [1; 2; 1; 2]; %! y = 100 * A + B; %! [~, tbl, stats, terms] = anovan (y, {A, B}, 'model', [0, 1], ... %! 'display', 'off'); %! assert_equal (terms, [0, 1]); %! assert_equal (tbl{2, 1}, 'X2'); %! assert_equal (tbl{2, 2}, 1, 1e-12); %! assert_equal (full (stats.X(:, 2)), [-0.5; 0.5; -0.5; 0.5]); ## Reordering terms changes Type I order without relabeling their columns. %!test %! A = [1; 1; 2; 2]; %! B = [1; 2; 1; 2]; %! y = 100 * A + B; %! [~, tbl] = anovan (y, {A, B}, 'model', [0, 1; 1, 0], ... %! 'sstype', 1, 'display', 'off'); %! assert_equal (tbl(2:3, 1), {'X2'; 'X1'}); %! assert_equal (cell2mat (tbl(2:3, 2)), [1; 10000], 1e-10); ## A continuous factor raised to a power is the power of the raw predictor. %!test %! x = (1:5)'; %! y = 2 + 3 * x + 4 * x .^ 2 + [0.3; -0.2; 0.1; 0.4; -0.6]; %! [p, tbl, ~, terms] = anovan (y, x, 'model', [1; 2], 'continuous', 1, ... %! 'sstype', 2, 'display', 'off'); %! assert_equal (terms, [1; 2]); %! assert_equal (tbl(2:3, 1), {'X1'; 'X1^2'}); %! assert_equal (cell2mat (tbl(2:3, 2)), ... %! [4.09767456073500; 216.071428571430], 1e-9); %! assert_equal (cell2mat (tbl(2:3, 3)), [1; 1]); %! assert_equal (cell2mat (tbl(4, 2:3)), [0.444571428570271, 2], 1e-9); %! assert_equal (p, [0.0501972848960940; 0.00102717552592300], 1e-12); ## Hierarchical sums-of-squares adjust a power only for its lower powers. %!test %! x = (1:5)'; %! y = 2 + 3 * x + 4 * x .^ 2 + [0.3; -0.2; 0.1; 0.4; -0.6]; %! [p, tbl] = anovan (y, x, 'model', [1; 2], 'continuous', 1, ... %! 'sstype', 'h', 'display', 'off'); %! assert_equal (cell2mat (tbl(2:3, 2)), ... %! [7225.34400000000; 216.071428571430], 1e-8); %! assert_equal (p, [3.07633044150000e-05; 0.00102717552592300], 1e-12); ## The hierarchical sums-of-squares selector is not case sensitive. %!test %! x = (1:5)'; %! y = 2 + 3 * x + 4 * x .^ 2 + [0.3; -0.2; 0.1; 0.4; -0.6]; %! [p_lower, tbl_lower] = anovan (y, x, 'model', [1; 2], 'continuous', 1, ... %! 'sstype', 'h', 'display', 'off'); %! [p_upper, tbl_upper] = anovan (y, x, 'model', [1; 2], 'continuous', 1, ... %! 'sstype', 'H', 'display', 'off'); %! assert_equal (p_upper, p_lower); %! assert_equal (tbl_upper, tbl_lower); %!test %! ## Below the resolution of 1 - tcdf %! [~, ~, stats] = anovan ([1:10, 101:110, 201:210]', ... %! kron ((1:3)', ones (10, 1)), 'display', 'off'); %! t = stats.coeffs(:,5); %! dfe = stats.dfe; %! assert_equal (stats.coeffs(:,6), ... %! betainc (dfe ./ (dfe + t .^ 2), dfe / 2, 1/2), -1e-12); %!test %! ## Below the resolution of 1 - fcdf, values from MATLAB R2024a %! w = [1:10, 101:110, 201:210]'; %! g = kron ((1:3)', ones (10, 1)); %! p = anovan (w, {g}, 'display', 'off'); %! assert_equal (p, 5.52216470815463e-40, -1e-10); %!error ... %! anovan ((1:4)', {[1; 1; 2; 2], [1; 2; 1; 2]}, ... %! 'model', [2, 0], 'display', 'off') %!error ... %! anovan ((1:4)', {[1; 1; 2; 2], [1; 2; 1; 2]}, ... %! 'nested', [0, 1], 'display', 'off') %!error ... %! anovan ((1:4)', {[1; 1; 2; 2], [1; 2; 1; 2]}, ... %! 'nested', [0, 2; 0, 0], 'display', 'off') %!error ... %! anovan ((1:4)', {[1; 1; 2; 2], [1; 2; 1; 2]}, ... %! 'nested', eye (2), 'display', 'off') %!error ... %! anovan ((1:4)', {[1; 1; 2; 2], [1; 2; 1; 2]}, ... %! 'nested', [0, 1; 1, 0], 'display', 'off') %!error ... %! anovan ((1:4)', {[1; 1; 2; 2], [1; 2; 1; 2]}, ... %! 'nested', [0, 0; 1, 0], 'continuous', 1, 'display', 'off') %!error ... %! anovan ((1:8)', [kron([1; 2], ones(4, 1)), ... %! repmat([1; 1; 2; 2], 2, 1)], 'nested', [0, 0; 1, 0], ... %! 'contrasts', {[]; [-0.5; 0.5]}, 'display', 'off') ## A design that is not of full rank keeps a finite error sum-of-squares. %!test %! A = [1; 1; 1; 1; 2; 2]; %! B = [1; 1; 2; 2; 1; 1]; %! y = [3; 4; 7; 8; 5; 6]; %! [~, tbl] = anovan (y, {A, B}, 'model', 'full', 'sstype', 3, ... %! 'display', 'off'); %! assert_equal (tbl{end-1, 2}, 1.5, 1e-12); %! assert_equal (tbl{end-1, 3}, 3); %! assert_equal (tbl{end-1, 5}, 0.5, 1e-12); %! assert_equal (tbl{end, 2}, 17.5, 1e-12); ## Which terms are aliased depends on the model each is adjusted for. %!test %! A = [1; 1; 1; 1; 2; 2]; %! B = [1; 1; 2; 2; 1; 1]; %! y = [3; 4; 7; 8; 5; 6]; %! [~, t3] = anovan (y, {A, B}, 'model', 'full', 'sstype', 3, 'display', 'off'); %! assert_equal (cell2mat (t3(2:4, 3)), [0; 0; 0]); %! assert_equal (cell2mat (t3(2:4, 4)), [1; 1; 1]); %! [~, t2] = anovan (y, {A, B}, 'model', 'full', 'sstype', 2, 'display', 'off'); %! assert_equal (cell2mat (t2(2:4, 3)), [1; 1; 0]); %! assert_equal (cell2mat (t2(2:4, 4)), [0; 0; 1]); %! assert_equal (cell2mat (t2(2:4, 2)), [4; 16; 0], 1e-12); ## A partially aliased term reports the degrees of freedom it can estimate. %!test %! A = [1; 1; 2; 2; 3; 3]; %! B = [1; 1; 1; 1; 2; 2]; %! y = [3; 4; 7; 8; 5; 6]; %! [~, tbl] = anovan (y, {A, B}, 'model', 'linear', 'sstype', 3, ... %! 'display', 'off'); %! assert_equal (cell2mat (tbl(2:3, 3)), [1; 0]); %! assert_equal (cell2mat (tbl(2:3, 4)), [1; 1]); %! assert_equal (tbl{2, 2}, 16, 1e-12); ## A full rank design flags nothing. %!test %! A = [1; 1; 2; 2; 1; 1; 2; 2]; %! B = [1; 2; 1; 2; 1; 2; 1; 2]; %! y = [3; 4; 7; 8; 5; 6; 9; 11]; %! [~, tbl] = anovan (y, {A, B}, 'model', 'full', 'display', 'off'); %! assert_equal (cell2mat (tbl(2:4, 4)), [0; 0; 0]); %! assert_equal (cell2mat (tbl(2:4, 2)), [36.125; 3.125; 0.125], 1e-12); ## A factor with a single level has no degrees of freedom but is not aliased. %!test %! [~, tbl] = anovan ((1:3)', ones (3, 1), 'sstype', 2, 'display', 'off'); %! assert_equal (tbl{2, 3}, 0); %! assert_equal (tbl{2, 4}, 0); %! assert_equal (tbl{end-1, 2}, 2, 1e-12); ## Both effect sizes are reported, each under its own name. %!test %! A = [1; 1; 2; 2; 1; 1; 2; 2]; %! B = [1; 2; 1; 2; 1; 2; 1; 2]; %! y = [3; 4; 7; 8; 5; 6; 9; 11]; %! [~, tbl] = anovan (y, {A, B}, 'model', 'full', 'display', 'off'); %! assert_equal (tbl(1, :), {'Source', 'Sum Sq.', 'd.f.', 'Singular?', ... %! 'Mean Sq.', 'F', 'Prob>F', 'Eta Sq.', ... %! 'Part. Eta Sq.'}); %! ss = cell2mat (tbl(2:4, 2)); %! sse = tbl{end-1, 2}; %! sst = tbl{end, 2}; %! assert_equal (cell2mat (tbl(2:4, 8)), ss ./ sst, 1e-12); %! assert_equal (cell2mat (tbl(2:4, 9)), ss ./ (ss + sse), 1e-12); ## A model with a random factor reports what each mean square estimates. %!test %! y = [444 614 423 625 408 856 447 719 ... %! 764 831 586 782 609 1002 606 766]' / 10; %! g1 = {'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola', ... %! 'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola'}'; %! g2 = {'C','T','C','T','C','T','C','T','C','T','C','T','C','T','C','T'}'; %! g3 = [1;1;1;1;1;1;1;1;2;2;2;2;2;2;2;2]; %! [~, tbl] = anovan (y, {g1, g2, g3}, 'model', 'full', 'random', 3, ... %! 'display', 'off'); %! assert_equal (size (tbl), [10, 17]); %! assert_equal (tbl(1, 1:15), {'Source', 'Sum Sq.', 'd.f.', 'Singular?', ... %! 'Mean Sq.', 'F', 'Prob>F', 'Type', 'Expected MS', ... %! 'MS denom', 'd.f. denom', 'Denom. defn.', 'Var. est.', ... %! 'Var. lower bnd', 'Var. upper bnd'}); %! assert_equal (tbl(2:9, 8), {'fixed'; 'fixed'; 'random'; 'fixed'; ... %! 'random'; 'random'; 'random'; 'random'}); %! assert_equal (cell2mat (tbl(2:8, 11)), ... %! [3; 1; 2.610727841363640; 3; 3; 3; 0], 1e-12); ## With every factor fixed the table keeps the fixed-effects layout. %!test %! [~, tbl] = anovan ((1:12)', {repmat([1;2;3],4,1), kron([1;2],ones(6,1))}, ... %! 'model', 'full', 'display', 'off'); %! assert_equal (size (tbl), [6, 9]); %! assert_equal (tbl{1, 9}, 'Part. Eta Sq.'); ## A random factor keeps its interactions in the model. %!test %! y = [444 614 423 625 408 856 447 719 ... %! 764 831 586 782 609 1002 606 766]' / 10; %! g1 = {'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola', ... %! 'NIH','NIH','BALB/C','BALB/C','A/J','A/J','129/Ola','129/Ola'}'; %! g2 = {'C','T','C','T','C','T','C','T','C','T','C','T','C','T','C','T'}'; %! g3 = [1;1;1;1;1;1;1;1;2;2;2;2;2;2;2;2]; %! [p, tbl, s, terms] = anovan (y, {g1, g2, g3}, 'model', 'full', ... %! 'random', 3, 'display', 'off'); %! assert_equal (rows (terms), 7); %! assert_equal (numel (p), 7); %! assert_equal (size (s.denom), [7, 5]); %! assert_equal (size (s.ems), [8, 8]); %! assert_equal (numel (s.txtems), 8); ## An interaction term is named with Wilkinson's interaction operator. %!test %! [~, tbl] = anovan ((1:12)', {repmat([1;2;3],4,1), kron([1;2],ones(6,1))}, ... %! 'model', 'full', 'display', 'off'); %! assert_equal (tbl(2:end, 1), ... %! {'X1'; 'X2'; 'X1:X2'; 'Error'; 'Total'}); ## A three-factor interaction names every factor in it. %!test %! g = cell (1, 3); %! for k = 1:3 %! g{k} = mod (floor ((0:15)' / 2^(k-1)), 2) + 1; %! endfor %! [~, tbl] = anovan ((1:16)', g, 'model', 'full', 'display', 'off'); %! assert_equal (tbl{8, 1}, 'X1:X2:X3'); ## The random-factor marker is a printing convention, not part of the names. %!test %! y = [3; 4; 7; 8; 5; 6; 9; 11]; %! A = [1; 1; 2; 2; 1; 1; 2; 2]; %! B = [1; 2; 1; 2; 1; 2; 1; 2]; %! [~, tbl, s] = anovan (y, {A, B}, 'model', 'full', 'random', 2, ... %! 'display', 'off'); %! assert_equal (tbl(2:4, 1), {'X1'; 'X2'; 'X1:X2'}); %! assert_equal (s.varnames, {'X1'; 'X2'}); %! assert_equal (any (cellfun (@(t) any (t == "'"), s.txtems)), false); %! assert_equal (any (cellfun (@(t) any (t == "'"), s.txtdenom)), false); ## The printed table marks a random factor with a trailing quote. %!test %! y = [3; 4; 7; 8; 5; 6; 9; 11]; %! A = [1; 1; 2; 2; 1; 1; 2; 2]; %! B = [1; 2; 1; 2; 1; 2; 1; 2]; %! visible = get (0, 'defaultfigurevisible'); %! unwind_protect %! set (0, 'defaultfigurevisible', 'off'); %! txt = evalc (["anovan (y, {A, B}, 'model', 'full', 'random', 2, ", ... %! "'display', 'on');"]); %! unwind_protect_cleanup %! set (0, 'defaultfigurevisible', visible); %! close all; %! end_unwind_protect %! assert_equal (! isempty (strfind (txt, "X2'")), true); %! assert_equal (! isempty (strfind (txt, "X1:X2'")), true); ## Test the shape of stats.varnames, checked against MATLAB R2024a %!test %! y = [1, 2, 3, 4, 5, 6, 7, 8.5]'; %! g = {[1, 1, 2, 2, 1, 1, 2, 2]', [1, 2, 1, 2, 1, 2, 1, 2]'}; %! [~, ~, s] = anovan (y, g, 'display', 'off'); %! assert_equal (s.varnames, {'X1'; 'X2'}); %!test %! y = [1, 2, 3, 4, 5, 6, 7, 8.5]'; %! g = {[1, 1, 2, 2, 1, 1, 2, 2]', [1, 2, 1, 2, 1, 2, 1, 2]'}; %! [~, ~, s] = anovan (y, g, 'display', 'off', 'varnames', {'A', 'B'}); %! assert_equal (s.varnames, {'A'; 'B'}); %!test %! y = [1, 2, 3, 4, 5, 6, 7, 8.5]'; %! g = {[1, 1, 2, 2, 1, 1, 2, 2]', [1, 2, 1, 2, 1, 2, 1, 2]'}; %! [~, ~, s] = anovan (y, g, 'display', 'off', 'varnames', ['A'; 'B']); %! assert_equal (s.varnames, {'A'; 'B'}); statistics-release-2.1.2/inst/Hypothesis_Testing/ansaribradley.m000066400000000000000000000300651527311104300251340ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} ansaribradley (@var{x}, @var{y}) ## @deftypefnx {statistics} {@var{h} =} ansaribradley (@var{x}, @var{y}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{h}, @var{p}] =} ansaribradley (@dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{p}, @var{stats}] =} ansaribradley (@dots{}) ## ## Ansari-Bradley two-sample test for equal dispersions. ## ## @code{@var{h} = ansaribradley (@var{x}, @var{y})} performs an Ansari-Bradley ## test of the hypothesis that the two independent samples in the vectors ## @var{x} and @var{y} come from distributions with the same dispersion ## parameter, against the alternative that they come from distributions with ## different dispersions. The result is @var{h} = 0 if the null hypothesis of ## equal dispersions cannot be rejected at the 5% significance level, or ## @var{h} = 1 if it can. ## ## The Ansari-Bradley test is a nonparametric alternative to the two-sample ## @math{F} test (@code{vartest2}) that does not assume normality. It assumes ## that the two samples are independent and that they come from distributions ## with the same median and shape, differing (under the alternative) only in ## dispersion. If the medians differ, the data should be recentred (e.g.@: by ## subtracting the sample medians) before applying the test. ## ## @code{ansaribradley} treats NaNs in @var{x} or @var{y} as missing values and ## ignores them. ## ## @code{[@var{h}, @var{p}] = ansaribradley (@dots{})} returns the p-value of ## the test, that is the probability, under the null hypothesis, of observing a ## value of the test statistic as or more extreme than the one observed. ## ## @code{[@var{h}, @var{p}, @var{stats}] = ansaribradley (@dots{})} returns a ## structure with the following fields: ## ## @multitable @columnfractions 0.2 0.75 ## @item @qcode{W} @tab the value of the Ansari-Bradley test statistic, the sum ## of the Ansari-Bradley scores of the sample @var{x} ## @item @qcode{Wstar} @tab the value of the approximate normal (z) statistic ## @end multitable ## ## @code{[@dots{}] = ansaribradley (@dots{}, @var{name}, @var{value})} specifies ## one or more of the following name/value pairs: ## ## @multitable @columnfractions 0.2 0.75 ## @headitem Name @tab Value ## @item @qcode{'alpha'} @tab the significance level. Default is 0.05. ## ## @item @qcode{'tail'} @tab a string specifying the alternative hypothesis ## ## @item @qcode{'method'} @tab a string selecting the p-value computation, ## either @qcode{'exact'} to use the exact permutation distribution of the ## statistic, or @qcode{'approximate'} to use the normal approximation. The ## default is @qcode{'exact'} when the total sample size is 25 or less, and ## @qcode{'approximate'} otherwise. ## @end multitable ## ## The @qcode{'tail'} option can take one of the following values: ## ## @multitable @columnfractions 0.15 0.75 ## @item @qcode{'both'} @tab dispersions are not equal (two-tailed, default) ## @item @qcode{'right'} @tab dispersion of @var{x} is greater than dispersion ## of @var{y} (right-tailed) ## @item @qcode{'left'} @tab dispersion of @var{x} is less than dispersion of ## @var{y} (left-tailed) ## @end multitable ## ## @seealso{vartest2, vartestn, kstest2, ranksum} ## @end deftypefn function [h, p, stats] = ansaribradley (x, y, varargin) ## Validate input arguments if (nargin < 2) error ("ansaribradley: too few input arguments."); endif if (! isvector (x) || ! isvector (y)) error ("ansaribradley: X and Y must be vectors."); endif ## Remove missing data and make column vectors x = x(! isnan (x))(:); y = y(! isnan (y))(:); if (isempty (x)) error ("ansaribradley: not enough data in X."); endif if (isempty (y)) error ("ansaribradley: not enough data in Y."); endif ## Add defaults and parse optional name/value pairs alpha = 0.05; tail = 'both'; method = []; if (mod (numel (varargin), 2) != 0) error ("ansaribradley: optional arguments must be in name/value pairs."); endif for idx = 1:2:numel (varargin) name = varargin{idx}; value = varargin{idx + 1}; switch (lower (name)) case 'alpha' alpha = value; if (! isscalar (alpha) || ! isnumeric (alpha) || ! isreal (alpha) ... || alpha <= 0 || alpha >= 1) error ("ansaribradley: invalid value for alpha."); endif case 'tail' tail = value; if (! (ischar (tail) && isrow (tail)) ... || ! any (strcmpi (tail, {'both', 'left', 'right'}))) error ("ansaribradley: invalid value for tail."); endif case 'method' method = value; if (! (ischar (method) && isrow (method)) ... || ! any (strcmpi (method, {'exact', 'approximate'}))) error ("ansaribradley: invalid value for method."); endif otherwise error ("ansaribradley: invalid name for optional arguments."); endswitch endfor nx = numel (x); ny = numel (y); N = nx + ny; ## Select the default computation method if (isempty (method)) if (N <= 25) method = 'exact'; else method = 'approximate'; endif endif ## Compute the Ansari-Bradley scores of the pooled sample. The raw score of ## the observation ranked I among the N pooled values is min (I, N + 1 - I), ## so that the extremes get the smallest scores and the centre the largest. ## Tied observations receive the average of their raw scores (mid-ranks). z = [x; y]; [zs, ord] = sort (z); pos = (1:N)'; rawscore = min (pos, N + 1 - pos); [~, ~, grp] = unique (zs); meanscore = accumarray (grp, rawscore, [], @mean); sortedscore = meanscore(grp); abscore = zeros (N, 1); abscore(ord) = sortedscore; ## Ansari-Bradley statistic: sum of the scores belonging to sample X W = sum (abscore(1:nx)); ## Normal approximation statistic (tie-corrected), always reported in STATS abar = mean (abscore); EW = nx * abar; VarW = (nx * ny) / (N * (N - 1)) * sum ((abscore - abar) .^ 2); Wstar = (W - EW) / sqrt (VarW); ## Compute the p-value. Larger dispersion in X pushes its observations to the ## extremes, lowering its scores and hence W; thus the 'right' alternative ## (dispersion of X greater) corresponds to the lower tail of W. if (strcmpi (method, 'exact')) ## Exact permutation distribution of W by dynamic programming over the ## scores doubled to integers (mid-ranks are integers or half-integers). s2 = round (2 * abscore); S = sum (s2); ## counts(k+1,t+1) = number of size-k subsets of scores summing to t counts = zeros (nx + 1, S + 1); counts(1, 1) = 1; for i = 1:N si = s2(i); for k = min (i, nx):-1:1 counts(k + 1, (si + 1):(S + 1)) += counts(k, 1:(S + 1 - si)); endfor endfor dist = counts(nx + 1, :); total = sum (dist); w2 = round (2 * W); p_le = sum (dist(1:(w2 + 1))) / total; p_ge = sum (dist((w2 + 1):end)) / total; switch (lower (tail)) case 'both' p = min (1, 2 * min (p_le, p_ge)); case 'right' p = p_le; case 'left' p = p_ge; endswitch else switch (lower (tail)) case 'both' p = 2 * normcdf (- abs (Wstar)); case 'right' p = normcdf (Wstar); case 'left' p = normcdf (- Wstar); endswitch endif ## Determine the test outcome and assemble the STATS structure h = double (p <= alpha); if (nargout > 2) stats = struct ('W', W, 'Wstar', Wstar); endif endfunction %!demo %! ## Test whether two samples have the same dispersion. The second sample is %! ## drawn with twice the standard deviation, so the null hypothesis of equal %! ## dispersions should be rejected. %! x = [42, 44, 38, 52, 48, 46, 40, 50]; %! y = [30, 62, 25, 70, 33, 58, 20, 65]; %! [h, p, stats] = ansaribradley (x, y) ## Test input validation %!error ansaribradley (1); %!error ... %! ansaribradley (ones (3, 2), ones (3, 1)); %!error ... %! ansaribradley (ones (3, 1), ones (2, 2)); %!error ansaribradley ([NaN, NaN], [1, 2]); %!error ansaribradley ([1, 2], [NaN, NaN]); %!error ... %! ansaribradley ([1, 2, 3], [4, 5, 6], 'alpha'); %!error ... %! ansaribradley ([1, 2, 3], [4, 5, 6], 'alpha', 0); %!error ... %! ansaribradley ([1, 2, 3], [4, 5, 6], 'alpha', 1); %!error ... %! ansaribradley ([1, 2, 3], [4, 5, 6], 'alpha', -0.2); %!error ... %! ansaribradley ([1, 2, 3], [4, 5, 6], 'alpha', [0.01, 0.05]); %!error ... %! ansaribradley ([1, 2, 3], [4, 5, 6], 'alpha', 'x'); %!error ... %! ansaribradley ([1, 2, 3], [4, 5, 6], 'tail', 'other'); %!error ... %! ansaribradley ([1, 2, 3], [4, 5, 6], 'tail', 5); %!error ... %! ansaribradley ([1, 2, 3], [4, 5, 6], 'method', 'other'); %!error ... %! ansaribradley ([1, 2, 3], [4, 5, 6], 'method', 5); %!error ... %! ansaribradley ([1, 2, 3], [4, 5, 6], 'name', 'value'); ## Test results %!test %! ## A concentrated sample versus a dispersed one (default exact, N = 16). %! x = [42, 44, 38, 52, 48, 46, 40, 50]; %! y = [30, 62, 25, 70, 33, 58, 20, 65]; %! [h, p, stats] = ansaribradley (x, y); %! assert_equal (h, 1); %! assert_equal (p, 0.000155400155400155, 1e-15); %! assert_equal (stats.W, 52); %! assert_equal (stats.Wstar, 3.38061701891407, 1e-13); %!test %! ## Same data, left-tailed exact test (dispersion of X less than Y). %! x = [42, 44, 38, 52, 48, 46, 40, 50]; %! y = [30, 62, 25, 70, 33, 58, 20, 65]; %! [h, p] = ansaribradley (x, y, 'tail', 'left'); %! assert_equal (h, 1); %! assert_equal (p, 7.77000777000777e-05, 1e-16); %!test %! ## Same data, normal approximation. %! x = [42, 44, 38, 52, 48, 46, 40, 50]; %! y = [30, 62, 25, 70, 33, 58, 20, 65]; %! [h, p, stats] = ansaribradley (x, y, 'method', 'approximate'); %! assert_equal (h, 1); %! assert_equal (p, 0.000723232716430194, 1e-15); %! assert_equal (stats.Wstar, 3.38061701891407, 1e-13); %!test %! ## Total sample size 30 (> 25) defaults to the approximate method. %! u = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; %! v = [3, 3, 4, 5, 6, 7, 8, 8, 9, 9, 10, 11, 12, 12, 13]; %! [h, p, stats] = ansaribradley (u, v); %! assert_equal (h, 0); %! assert_equal (p, 0.184251738662491, 1e-13); %! assert_equal (stats.Wstar, -1.32777714715389, 1e-13); %!test %! ## Tied observations receive mid-rank scores (exact method). %! x = [1, 1, 2, 3, 3]; %! y = [0, 2, 2, 2, 4, 4]; %! [h, p, stats] = ansaribradley (x, y); %! assert_equal (h, 0); %! assert_equal (p, 0.904761904761905, 1e-14); %! assert_equal (stats.W, 17); %! assert_equal (stats.Wstar, 0.245274554572897, 1e-14); %!test %! ## NaNs are ignored as missing values. %! x = [1, 2, 3, NaN, 5]; %! y = [2, NaN, 4, 6]; %! [h, p, stats] = ansaribradley (x, y); %! assert_equal (h, 0); %! assert_equal (p, 0.971428571428571, 1e-14); %! assert_equal (stats.W, 9.5); %! assert_equal (stats.Wstar, 0.253836541283405, 1e-14); statistics-release-2.1.2/inst/Hypothesis_Testing/barttest.m000066400000000000000000000146231527311104300241460ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{ndim} =} barttest (@var{x}) ## @deftypefnx {statistics} {@var{ndim} =} barttest (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@var{ndim}, @var{pval}] =} barttest (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {[@var{ndim}, @var{pval}, @var{chisq}] =} barttest (@var{x}, @var{alpha}) ## ## Bartlett's test of sphericity for correlation. ## ## It compares an observed correlation matrix to the identity matrix in order to ## check if there is a certain redundancy between the variables that we can ## summarize with a few number of factors. A statistically significant test ## shows that the variables (columns) in @var{x} are correlated, thus it makes ## sense to perform some dimensionality reduction of the data in @var{x}. ## ## @code{@var{ndim} = barttest (@var{x}, @var{alpha})} returns the number of ## dimensions necessary to explain the nonrandom variation in the data matrix ## @var{x} at the @var{alpha} significance level. @var{alpha} is an optional ## input argument and, when not provided, it is 0.05 by default. ## ## @code{[@var{ndim}, @var{pval}, @var{chisq}] = barttest (@dots{})} also ## returns the significance values @var{pval} for the hypothesis test for each ## dimension as well as the associated chi^2 values in @var{chisq} ## ## @end deftypefn function [ndim, pval, chisq] = barttest (x, alpha); ## Check for valid number of input arguments if (nargin < 1 || nargin >2) error ("barttest: invalid number of input arguments."); endif ## Check for NaN values in X if (any (isnan ( x(:)))) error ("barttest: NaN values in input are not allowed."); endif ## Add default value for alpha if not supplied if (nargin == 1) alpha = 0.05; endif ## Check for valid value of alpha if (! isscalar (alpha) || ! isnumeric (alpha) || alpha <= 0 || alpha >= 1) error ("barttest: wrong value for alpha."); endif ## Check size of data [row, col] = size (x); if (col <= 1 || row <= 1) error ("barttest: not enough data in X."); endif ## Compute the eigenvalues of X in a more efficient way latent = sort ((svd (x - repmat (mean (x, 1), row, 1)) .^ 2) / (row - 1)); ## The degrees of freedom should be N-1, where N is the sample size row -= 1; k = (0:col - 2)'; pk = col - k; loglatent = flipud (cumsum (log (latent))); ## Compute the chi-square statistic logsum = log (flipud ((latent(1) + cumsum (latent(2:col))) ./ flipud (pk))); chisq = (pk .* logsum - loglatent(1:col - 1)) * row; ## Calculate the degrees of freedom df = (pk - 1) .* (pk + 2) / 2; ## Find the corresponding p-values pval = chi2cdf (chisq, df, 'upper'); ## Get ndim dim = min (find (pval > alpha)); if (isempty (dim)) ndim = col; return; endif if (dim == 1) ndim = NaN; warning ("barttest: heuristics are violated."); else ndim = dim - 1; endif endfunction %!test %! ## Below the resolution of 1 - chi2cdf, values from MATLAB R2024a %! u = (1:30)'; %! [~, p] = barttest ([u, u + sin(u), cos(u)]); %! assert_equal (p, [3.45658879157417e-53; 0.247541092553521], -1e-10); ## Test input validation %!error barttest () %!error barttest ([2,NaN;3,4]) %!error barttest (ones (30, 4), 'alpha') %!error barttest (ones (30, 4), 0) %!error barttest (ones (30, 4), 1.2) %!error barttest (ones (30, 4), [0.2, 0.05]) %!error barttest (ones (30, 1)) %!error barttest (ones (30, 1), 0.05) ## Test results %!test %! x = [2, 3, 4, 5, 6, 7, 8, 9; 1, 2, 3, 4, 5, 6, 7, 8]'; %! [ndim, pval, chisq] = barttest (x); %! assert_equal (ndim, 2); %! assert_equal (pval < 1e-100, true); %! ## assert_equal (chisq, 512.0558, 1e-4); Result differs between octave 6 and 7 ? %!test %! x = [0.53767, 0.62702, -0.10224, -0.25485, 1.4193, 1.5237 ; ... %! 1.8339, 1.6452, -0.24145, -0.23444, 0.29158, 0.1634 ; ... %! -2.2588, -2.1351, 0.31286, 0.39396, 0.19781, 0.20995 ; ... %! 0.86217, 1.0835, 0.31286, 0.46499, 1.5877, 1.495 ; ... %! 0.31877, 0.38454, -0.86488, -0.63839, -0.80447, -0.7536 ; ... %! -1.3077, -1.1487, -0.030051, -0.017629, 0.69662, 0.60497 ; ... %! -0.43359, -0.32672, -0.16488, -0.37364, 0.83509, 0.89586 ; ... %! 0.34262, 0.29639, 0.62771, 0.51672, -0.24372, -0.13698 ; ... %! 3.5784, 3.5841, 1.0933, 0.93258, 0.21567, 0.455 ; ... %! 2.7694, 2.6307, 1.1093, 1.4298, -1.1658, -1.1816 ; ... %! -1.3499, -1.2111, -0.86365, -0.94186, -1.148, -1.4381 ; ... %! 3.0349, 2.8428, 0.077359, 0.18211, 0.10487, -0.014613; ... %! 0.7254, 0.56737, -1.2141, -1.2291, 0.72225, 0.90612 ; ... %! -0.063055,-0.17662, -1.1135, -0.97701, 2.5855, 2.4084 ; ... %! 0.71474, 0.29225, -0.0068493, -0.11468, -0.66689, -0.52466 ; ... %! -0.20497, -7.8874e-06, 1.5326, 1.3195, 0.18733, 0.20296 ; ... %! -0.12414, -0.077029, -0.76967, -0.96262, -0.082494, 0.121 ; ... %! 1.4897, 1.3683, 0.37138, 0.43653, -1.933, -2.1903 ; ... %! 1.409, 1.5882, -0.22558, -0.24835, -0.43897, -0.46247 ; ... %! 1.4172, 1.1616, 1.1174, 1.0785, -1.7947, -1.9471 ]; %! [ndim, pval, chisq] = barttest (x); %! assert_equal (ndim, 3); %! assert_equal (pval, [0; 0; 0; 0.52063; 0.34314], 1e-5); %! chisq_out = [251.6802; 210.2670; 153.1773; 4.2026; 2.1392]; %! assert_equal (chisq, chisq_out, 1e-4); statistics-release-2.1.2/inst/Hypothesis_Testing/binotest.m000066400000000000000000000212501527311104300241370ustar00rootroot00000000000000## Copyright (C) 2016 Andreas Stahel ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{h}, @var{pval}, @var{ci}, @var{stats}] =} binotest (@var{pos}, @var{N}, @var{p0}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}, @var{ci}, @var{stats}] =} binotest (@var{pos}, @var{N}, @var{p0}, @var{Name}, @var{Value}) ## ## Test for probability @var{p} of a binomial sample ## ## Perform a test of the null hypothesis @var{p} == @var{p0} for a sample ## of size @var{N} with @var{pos} positive results. ## ## ## Name-Value pair arguments can be used to set various options. ## @qcode{'alpha'} can be used to specify the significance level ## of the test (the default value is 0.05). The option @qcode{'tail'}, ## can be used to select the desired alternative hypotheses. If the ## value is @qcode{'both'} (default) the null is tested against the two-sided ## alternative @code{@var{p} != @var{p0}}. The value of @var{pval} is ## determined by adding the probabilities of all events less or equally ## likely than the observed number @var{pos} of positive events, equally ## likely to within a relative 1e-7, as R's @code{binom.test} compares them. ## If the value of @qcode{'tail'} is @qcode{'right'} ## the one-sided alternative @code{@var{p} > @var{p0}} is considered. ## Similarly for @qcode{'left'}, the one-sided alternative ## @code{@var{p} < @var{p0}} is considered. ## ## If @var{h} is 0 the null hypothesis is accepted, if it is 1 the null ## hypothesis is rejected. The p-value of the test is returned in @var{pval}. ## A 100(1-alpha)% Clopper-Pearson confidence interval for @var{p} is returned ## in @var{ci}, one-sided for a one-sided test. @var{stats} is a structure ## with the following fields: ## ## @multitable @columnfractions 0.2 0.75 ## @item @qcode{phat} @tab the estimated probability, @code{@var{pos} / @var{N}}. ## @item @qcode{CohensH} @tab Cohen's @math{h}, the difference between the ## arcsine transforms @math{2 asin (sqrt (phat)) - 2 asin (sqrt (p0))}. ## @item @qcode{CohensHCI} @tab its confidence interval, @var{ci} under the same ## transform, which is monotone and so keeps its coverage. ## @end multitable ## ## @end deftypefn function [h, p, ci, stats] = binotest (pos, n, p0, varargin) ## Set default arguments alpha = 0.05; tail = 'both'; i = 1; while (i <= length (varargin)) switch (lower (varargin{i})) case 'alpha' i = i + 1; alpha = varargin{i}; case 'tail' i = i + 1; tail = varargin{i}; otherwise error ("binotest: Invalid Name argument."); endswitch i = i + 1; endwhile if (! (isnumeric (alpha) && isscalar (alpha) && isreal (alpha) && alpha > 0 && alpha < 1)) error ("binotest: invalid value for alpha."); endif if (! isa (tail, 'char')) error ("binotest: tail must be a string."); endif if (n <= 0) error ("binotest: required n > 0."); endif if (p0 < 0) || (p0 > 1) error ("binotest: required 0 <= p0 <= 1."); endif if (pos < 0) || (pos > n) error ("binotest: required 0 <= pos <= n."); endif ## Based on the "tail" argument determine the P-value, the critical values, ## and the confidence interval. switch lower (tail) case 'both' A_low = binoinv (alpha / 2, n, p0) / n; A_high = binoinv (1 - alpha / 2, n, p0) / n; p_pos = binopdf (pos, n, p0); p_all = binopdf ([0:n], n, p0); ## Outcomes as likely as the one observed tie to within rounding ind = find (p_all <= p_pos * (1 + 1e-7)); p = min (1, sum (p_all(ind))); if (pos == 0) p_low = 0; else p_low = fzero (@(pl) 1 - binocdf (pos - 1, n, pl) - alpha / 2, [0, 1]); endif if (pos == n) p_high = 1; else p_high = fzero (@(ph) binocdf (pos, n, ph) - alpha / 2, [0, 1]); endif ci = [p_low, p_high]; case 'left' p = binocdf (pos, n, p0); if (pos == n) p_high = 1; else p_high = fzero (@(ph) binocdf (pos, n, ph) - alpha, [0, 1]); endif ci = [0, p_high]; case 'right' p = binocdf (pos - 1, n, p0, 'upper'); if (pos == 0) p_low = 0; else p_low = fzero (@(pl) 1 - binocdf (pos - 1, n, pl) - alpha, [0, 1]); endif ci = [p_low 1]; otherwise error ("binotest: invalid fifth (tail) argument to binotest."); endswitch ## Determine the test outcome ## MATLAB returns this a double instead of a logical array h = double (p < alpha); ## Estimate, and the effect size with its interval stats.phat = pos / n; stats.CohensH = 2 * asin (sqrt (stats.phat)) - 2 * asin (sqrt (p0)); stats.CohensHCI = 2 * asin (sqrt (ci)) - 2 * asin (sqrt (p0)); endfunction %!demo %! % flip a coin 1000 times, showing 475 heads %! % Hypothesis: coin is fair, i.e. p=1/2 %! [h,p_val,ci] = binotest (475,1000,0.5) %! % Result: h = 0 : null hypothesis not rejected, coin could be fair %! % P value 0.12, i.e. hypothesis not rejected for alpha up to 12% %! % 0.444 <= p <= 0.506 with 95% confidence %!demo %! % flip a coin 100 times, showing 65 heads %! % Alternative: coin shows more heads than tails, i.e. p>1/2 %! [h,p_val,ci] = binotest (65,100,0.5,'tail','right','alpha',0.01) %! % Result: h = 1 : null hypothesis is rejected, i.e. coin shows more heads than tails %! % P value 0.0018, i.e. hypothesis not rejected for alpha up to 0.18% %! % 0.53 <= p <= 1 with 99% confidence %!test #example from https://en.wikipedia.org/wiki/Binomial_test %! [h,p_val,ci] = binotest (51,235,1/6); %! assert_equal (p_val, 0.0437, 0.00005) %! [h,p_val,ci] = binotest (51,235,1/6,'tail','right'); %! assert_equal (p_val, 0.027, 0.0005) %!test %! [~, p] = binotest (51, 235, 1/6, 'tail', 'right'); %! assert_equal (p, sum (binopdf (51:235, 235, 1/6)), -1e-12); %!test %! [~, p] = binotest (51, 235, 1/6, 'tail', 'left'); %! assert_equal (p, sum (binopdf (0:51, 235, 1/6)), -1e-12); %!test %! ## 95 of 100 is no evidence that p < 0.2 %! [h, p] = binotest (95, 100, 0.2, 'tail', 'left'); %! assert_equal ([h, p], [0, 1]); %!test %! ## Below the resolution of 1 - binocdf %! [~, p] = binotest (95, 100, 0.2, 'tail', 'right'); %! assert_equal (p, sum (binopdf (95:100, 100, 0.2)), -1e-12); %!test %! [~, ~, ci] = binotest (51, 235, 1/6, 'tail', 'right'); %! assert_equal (ci(2), 1); %!test %! [~, ~, ci] = binotest (51, 235, 1/6, 'tail', 'left'); %! assert_equal (ci(1), 0); %!test %! ## Outcomes as likely as the one observed are counted, ties to within %! ## rounding included; values from R's binom.test %! [~, p] = binotest (1, 10, 0.5); %! assert_equal (p, 0.021484375, -1e-12); %!test %! [~, p] = binotest (4, 10, 0.5); %! assert_equal (p, 0.75390625, -1e-12); %!test %! [~, p] = binotest (11, 50, 0.5); %! assert_equal (p, 9.021490107130641e-05, -1e-10); %!test %! [~, ~, ~, st] = binotest (51, 235, 1/6); %! assert_equal (st.phat, 51 / 235); %!test %! [~, ~, ~, st] = binotest (51, 235, 1/6); %! assert_equal (st.CohensH, ... %! 2 * asin (sqrt (51 / 235)) - 2 * asin (sqrt (1/6)), -1e-14); %!test %! [~, ~, ci, st] = binotest (51, 235, 1/6); %! assert_equal (st.CohensHCI, 2 * asin (sqrt (ci)) - 2 * asin (sqrt (1/6)), ... %! -1e-14); %!test %! ## A one-sided interval maps to a one-sided interval %! [~, ~, ~, st] = binotest (51, 235, 1/6, 'tail', 'right'); %! assert_equal (st.CohensHCI(2), pi - 2 * asin (sqrt (1/6)), -1e-14); %!error binotest (5, 10, 0.5, 'size', 1) %!error binotest (5, 10, 0.5, 'alpha', 0) %!error binotest (5, 10, 0.5, 'alpha', 1) %!error ... %! binotest (5, 10, 0.5, 'alpha', [0.05, 0.1]) %!error binotest (5, 10, 0.5, 'tail', 1) %!error 0.> binotest (0, 0, 0.5) %!error binotest (5, 10, 1.5) %!error binotest (11, 10, 0.5) %!error ... %! binotest (5, 10, 0.5, 'tail', 'up') statistics-release-2.1.2/inst/Hypothesis_Testing/chi2gof.m000066400000000000000000000420021527311104300236270ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} chi2gof (@var{x}) ## @deftypefnx {statistics} {[@var{h}, @var{p}] =} chi2gof (@var{x}) ## @deftypefnx {statistics} {[@var{p}, @var{h}, @var{stats}] =} chi2gof (@var{x}) ## @deftypefnx {statistics} {[@dots{}] =} chi2gof (@var{x}, @var{Name}, @var{Value}, @dots{}) ## ## Chi-square goodness-of-fit test. ## ## @code{chi2gof} performs a chi-square goodness-of-fit test for discrete or ## continuous distributions. The test is performed by grouping the data into ## bins, calculating the observed and expected counts for those bins, and ## computing the chi-square test statistic ## @tex ## $$ \chi ^ 2 = \sum_{i=1}^N \left (O_i - E_i \right) ^ 2 / E_i $$ ## @end tex ## @ifnottex ## SUM((O-E).^2./E), ## @end ifnottex ## where O is the observed counts and E is the expected counts. This test ## statistic has an approximate chi-square distribution when the counts are ## sufficiently large. ## ## Bins in either tail with an expected count less than 5 are pooled with ## neighboring bins until the count in each extreme bin is at least 5. If ## bins remain in the interior with counts less than 5, @code{chi2gof} displays ## a warning. In that case, you should use fewer bins, or provide bin centers ## or binedges, to increase the expected counts in all bins. ## ## @code{@var{h} = chi2gof (@var{x})} performs a chi-square goodness-of-fit test ## that the data in the vector X are a random sample from a normal distribution ## with mean and variance estimated from @var{x}. The result is @var{h} = 0 if ## the null hypothesis (that @var{x} is a random sample from a normal ## distribution) cannot be rejected at the 5% significance level, or @var{h} = 1 ## if the null hypothesis can be rejected at the 5% level. @code{chi2gof} uses ## by default 10 bins (@qcode{'nbins'}), and compares the test statistic to a ## chi-square distribution with @qcode{@var{nbins} - 3} degrees of freedom, to ## take into account that two parameters were estimated. ## ## @code{[@var{h}, @var{p}] = chi2gof (@var{x})} also returns the p-value ## @var{p}, ## which is the probability of observing the given result, or one more extreme, ## by chance if the null hypothesis is true. If there are not enough degrees of ## freedom to carry out the test, @var{p} is NaN. ## ## @code{[@var{h}, @var{p}, @var{stats}] = chi2gof (@var{x})} also returns a ## @var{stats} structure with the following fields: ## ## @multitable @columnfractions 0.3 0.65 ## @item "chi2stat" @tab Chi-square statistic ## @item "df" @tab Degrees of freedom ## @item "binedges" @tab Vector of bin binedges after pooling ## @item "O" @tab Observed count in each bin ## @item "E" @tab Expected count in each bin ## @end multitable ## ## @code{[@dots{}] = chi2gof (@var{x}, @var{Name}, @var{Value}, @dots{})} ## specifies optional Name/Value pair arguments chosen from the following list. ## ## @multitable @columnfractions 0.2 0.75 ## @headitem Name @tab Value ## @item @qcode{'nbins'} @tab The number of bins to use. Default is 10. ## @item @qcode{'binctrs'} @tab A vector of bin centers. ## @item @qcode{'binedges'} @tab A vector of bin binedges. ## @item @qcode{'cdf'} @tab A fully specified cumulative distribution ## function or a function handle provided in a cell array whose first element is ## a function handle, and all later elements are its parameter values. The ## function must take @var{x} values as its first argument, and other parameters ## as later arguments. ## @item @qcode{'expected'} @tab A vector with one element per bin ## specifying the expected counts for each bin. ## @item @qcode{'nparams'} @tab The number of estimated parameters; used to ## adjust the degrees of freedom to be @qcode{@var{nbins} - 1 - @var{nparams}}, ## where @var{nbins} is the number of bins. ## @item @qcode{'emin'} @tab The minimum allowed expected value for a bin; ## any bin in either tail having an expected value less than this amount is ## pooled with a neighboring bin. Use the value 0 to prevent pooling. Default ## is 5. ## @item @qcode{'frequency'} @tab A vector of the same length as @var{x} ## containing the frequency of the corresponding @var{x} values. ## @item @qcode{'alpha'} @tab An @var{alpha} value such that the hypothesis ## is rejected if @qcode{@var{p} < @var{alpha}}. Default is ## @qcode{@var{alpha} = 0.05}. ## @end multitable ## ## You should specify either @qcode{'cdf'} or @qcode{'expected'} parameters, but ## not both. If your @qcode{'cdf'} input contains extra parameters, these are ## accounted for automatically and there is no need to specify ## @qcode{'nparams'}. If your @qcode{'expected'} input depends on estimated ## parameters, you should use the @qcode{'nparams'} parameter to ensure that the ## degrees of freedom for the test is correct. ## ## @end deftypefn function [h, p, stats] = chi2gof (x, varargin) ## Check input arguments if (nargin < 1) error ("chi2gof: At least one input argument is required."); endif if (! isvector (x) || ! isreal (x)) error ("chi2gof: X must be a vector of real numbers."); endif ## Add initial parameters nbins = []; binctrs = []; binedges = []; cdf_spec = []; expected = []; nparams = []; emin = 5; frequency = []; alpha = 0.05; ## Parse additional arguments numarg = nargin - 1; argpos = 1; while (numarg) argname = varargin{argpos}; switch (lower (argname)) case 'nbins' nbins = varargin{argpos + 1}; case 'ctrs' binctrs = varargin{argpos + 1}; case 'edges' binedges = varargin{argpos + 1}; case 'cdf' cdf_spec = varargin{argpos + 1}; case 'expected' expected = varargin{argpos + 1}; case 'nparams' nparams = varargin{argpos + 1}; case 'emin' emin = varargin{argpos + 1}; case 'frequency' frequency = varargin{argpos + 1}; case 'alpha' alpha = varargin{argpos + 1}; endswitch numarg -= 2; argpos += 2; endwhile ## Check additional arguments for errors if ((! isempty (nbins) + ! isempty (binctrs) + ! isempty (binedges)) > 1) error ("chi2gof: Inconsistent Arguments."); endif if ((! isempty (cdf_spec) + ! isempty (expected)) > 1) error ("chi2gof: Conflicted Arguments."); endif if (! isempty (frequency)) if (! isvector (frequency) || numel (frequency) != numel (x)) error ("chi2gof: X and Frequency vectors mismatch."); endif if (any (frequency < 0)) error ("chi2gof: Frequency vector contains negative numbers."); endif endif if (! isscalar (emin) || emin < 0 || emin != round (emin) || ! isreal (emin)) error ("chi2gof: 'emin' must be a positive integer."); endif if (! isempty (nparams)) if (! isscalar (nparams) || nparams < 0 || nparams != round (nparams) ... || ! isreal (nparams)) error ("chi2gof: Wrong number of parameters."); endif endif if (! isscalar (alpha) || ! isreal (alpha) || alpha <= 0 || alpha >= 1) error ("chi2gof: Wrong value of alpha."); endif ## Make X a column vector x = x(:); ## Parse or create a frequency vector if (isempty (frequency)) frequency = ones (size (x)); else frequency = frequency(:); endif ## Remove NaNs if any remove_NaNs = isnan (frequency) | isnan (x); if (any (remove_NaNs)) x(remove_NaNs) = []; frequency(remove_NaNs) = []; endif ## Check for bin numbers, centers, or edges and calculate bins accordingly if (! isempty (binctrs)) [Observed, binedges] = calculatebins (x, frequency, 'ctrs', binctrs); elseif (! isempty (binedges)) [Observed, binedges] = calculatebins (x, frequency, 'edges', binedges); else if (isempty (nbins)) if (isempty (expected)) nbins = 10; ## default number of bins else nbins = length (expected); ## determined by Expected vector endif endif [Observed, binedges] = calculatebins (x, frequency, 'nbins', nbins); endif Observed = Observed(:); nbins = length (Observed); ## Calculate expected vector cdfargs = {}; if (! isempty (expected)) ## Provided as input argument if (! isvector (expected) || numel (expected) != nbins) error ("chi2gof: Expected counts vector is the wrong size."); endif if (any (expected < 0)) error ("chi2gof: Expected counts vector has negative values."); endif Expected = expected(:); else ## Calculate from the cdf if (isempty (cdf_spec)) ## Use estimated normal as default cdffunc = @normcdf; sumfreq = sum (frequency); mu = sum (x.*frequency)/sumfreq; sigma = sqrt (sum ((x.*frequency - mu) .^ 2) / (sumfreq-1)); cdfargs = {mu, sigma}; if (isempty (nparams)) nparams = 2; endif elseif (isa (cdf_spec, 'function_handle')) ## Split function handle to get function name and optional parameters cstr = ostrsplit (func2str (cdf_spec), ','); ## Simple function handle, no parameters: e.g. @normcdf if (isempty (strfind (cstr, '@')) && numel (cstr) == 1) cdffunc = str2func (char (strcat ("@", cstr))); if (isempty (nparams)) nparams = numel (cdfargs); endif ## Complex function handle, no parameters: e.g. @(x) normcdf(x) elseif (! isempty (strfind (cstr, '@')) && numel (cstr) == 1) ## Remove white spaces cstr = char (cstr); cstr(strfind (cstr, ' ')) = []; ## Remove input argument in parentheses while (length (strfind (cstr,'('))) cstr(index (cstr, '('):index (cstr, ')')) = []; endwhile cdffunc = str2func (cstr); if (isempty (nparams)) nparams = numel (cdfargs); endif elseif (! isempty (strfind (cstr, '@')) && numel (cstr) > 1) ## Evaluate function name in first cell cstr_f = char (cstr(1)); cstr_f(strfind (cstr_f, ' ')) = []; cstr_f(index (cstr_f, '('):index (cstr_f, ')')) = []; cstr_f(index (cstr_f, '('):end) = []; cdffunc = str2func (cstr_f); ## Evaluate optional parameters in remaining cells cstr_idx = 2; while (cstr_idx <= numel (cstr)) cstr_p = char (cstr(cstr_idx)); cstr_p(strfind (cstr_p, ' ')) = []; ## Check for numerical value if (isscalar (str2num (cstr_p))) cdfargs{cstr_idx - 1} = cstr_p; else ## Get function handle: e.g. mean cstr_p(index (cstr_p, '('):end) = []; cdfargs{cstr_idx - 1} = feval (str2func (cstr_p), x .* frequency); cstr_idx += 1; endif endwhile if (isempty (nparams)) nparams = numel (cdfargs); endif endif elseif (iscell (cdf_spec)) ## Get function and args from cell array cdffunc = cdf_spec{1}; cdfargs = cdf_spec(2:end); if (isempty (nparams)) nparams = numel (cdfargs); endif endif if (! is_function_handle (cdffunc)) error ("chi2gof: Poorly specified cumulative distribution function."); else cdfname = func2str (cdffunc); endif ## Calculate only inner bins, since tail probabilities included in the ## calculation of expected counts for the first and last bins interioredges = binedges(2:end-1); ## Compute the cumulative probabilities Fcdf = feval (cdffunc, interioredges, cdfargs{:}); if (! isvector (Fcdf) || numel (Fcdf) != (nbins - 1)) msg = sprintf ("chi2gof: Wrong number of outputs from: %s\n", cdfname); error (msg); endif ## Compute the expected values Expected = sum (Observed) * diff ([0;Fcdf(:);1]); endif ## Avoid too small expected values if (any (Expected < emin)) [Expected, Observed, binedges] = poolbins (Expected, Observed, binedges, emin); nbins = length (Expected); endif ## Compute test statistic cstat = sum (((Observed - Expected) .^ 2) ./ Expected); ## Calculate degrees of freedom if (isempty (nparams)) nparams = 0; endif df = nbins - 1 - nparams; if (df > 0) p = chi2cdf (cstat, df, 'upper'); else df = 0; p = NaN; endif h = cast (p <= alpha, 'double'); ## Create 3rd output argument if necessary if (nargout > 2) stats.chi2stat = cstat; stats.df = df; stats.edges = binedges; stats.O = Observed'; stats.E = Expected'; endif endfunction function [Expected, Observed, binedges] = poolbins (Expected, ... Observed, binedges, emin) i = 1; j = length (Expected); while (i < j - 1 && (Expected(i) < emin || Expected(i + 1) < emin || ... Expected(j) < emin || Expected(j - 1) < emin)) if (Expected(i) < Expected(j)) Expected(i+1) = Expected(i+1) + Expected(i); Observed(i+1) = Observed(i+1) + Observed(i); i = i + 1; else Expected(j-1) = Expected(j-1) + Expected(j); Observed(j-1) = Observed(j-1) + Observed(j); j = j - 1; endif endwhile ## Keep only pooled bins Expected = Expected(i:j); Observed = Observed(i:j); binedges(j+1:end-1) = []; binedges(2:i) = []; endfunction function [Observed, binedges] = calculatebins (x, frequency, binspec, specval) lo = double (min (x(:))); hi = double (max (x(:))); ## Check binspec for bin count, bin centers, or bin edges. switch (binspec) case 'nbins' nbins = specval; if (isempty (x)) lo = 0; hi = 1; endif if (lo == hi) lo = lo - floor (nbins / 2) - 0.5; hi = hi + ceil (nbins / 2) - 0.5; endif binwidth = (hi - lo) ./ nbins; binedges = lo + binwidth * (0:nbins); binedges(length (binedges)) = hi; case 'ctrs' binctrs = specval(:)'; binwidth = diff (binctrs); binwidth = [binwidth binwidth(end)]; binedges = [binctrs(1)-binwidth(1)/2 binctrs+binwidth/2]; case 'edges' binedges = specval(:)'; endswitch ## Update bins nbins = length (binedges) - 1; ## Calculate bin numbers if (isempty (x)) binnum = x; elseif (! isequal (binspec, 'edges')) binedges = binedges + eps (binedges); [ignore, binnum] = histc (x, [-Inf binedges(2:end-1) Inf]); else [ignore, binnum] = histc (x, binedges); binnum(binnum == nbins + 1) = nbins; endif ## Remove empty bins if (any (binnum == 0)) frequency(binnum == 0) = []; binnum(binnum == 0) = []; endif ## Compute Observed vector binnum = binnum(:); Observed = accumarray ([ones(size(binnum)), binnum], frequency, [1, nbins]); endfunction %!demo %! rng (42); %! x = normrnd (50, 5, 100, 1); %! [h, p, stats] = chi2gof (x) %! [h, p, stats] = chi2gof (x, 'cdf', @(x)normcdf (x, mean (x), std (x))) %! [h, p, stats] = chi2gof (x, 'cdf', {@normcdf, mean(x), std(x)}) %!demo %! rng (42); %! x = rand (100,1 ); %! n = length (x); %! binedges = linspace (0, 1, 11); %! expectedCounts = n * diff (binedges); %! [h, p, stats] = chi2gof (x, 'binedges', binedges, 'expected', expectedCounts) %!demo %! bins = 0:5; %! obsCounts = [6 16 10 12 4 2]; %! n = sum (obsCounts); %! lambdaHat = sum (bins.*obsCounts) / n; %! expCounts = n * poisspdf (bins,lambdaHat); %! [h, p, stats] = chi2gof (bins, 'binctrs', bins, 'frequency', obsCounts, ... %! 'expected', expCounts, 'nparams',1) %!test %! ## Below the resolution of 1 - chi2cdf, values from MATLAB R2024a %! [~, p] = chi2gof (1:5, 'Ctrs', 1:5, ... %! 'Frequency', [500, 300, 300, 300, 600], ... %! 'Expected', 400 * ones (1, 5), 'NParams', 0); %! assert_equal (p, 3.75727673578105e-42, -1e-10); ## Test input validation %!error chi2gof () %!error chi2gof ([2,3;3,4]) %!error chi2gof ([1,2,3,4], 'nbins', 3, 'ctrs', [2,3,4]) %!error chi2gof ([1,2,3,4], 'frequency', [2,3,2]) %!error chi2gof ([1,2,3,4], 'frequency', [2,3,2,-2]) %!error chi2gof ([1,2,3,4], 'frequency', [2,3,2,2], 'nparams', i) %!error chi2gof ([1,2,3,4], 'frequency', [2,3,2,2], 'alpha', 1.3) %!error chi2gof ([1,2,3,4], 'expected', [-3,2,2]) %!error chi2gof ([1,2,3,4], 'expected', [3,2,2], 'nbins', 5) %!error chi2gof ([1,2,3,4], 'cdf', @normcdff) %!test %! x = [1 2 1 3 2 4 3 2 4 3 2 2]; %! [h, p, stats] = chi2gof (x); %! assert_equal (h, 0); %! assert_equal (p, NaN); %! assert_equal (stats.chi2stat, 0.1205375022748029, 1e-14); %! assert_equal (stats.df, 0); %! assert_equal (stats.edges, [1, 2.5, 4], 1e-14); %! assert_equal (stats.O, [7, 5], 1e-14); %! assert_equal (stats.E, [6.399995519909668, 5.600004480090332], 1e-14); statistics-release-2.1.2/inst/Hypothesis_Testing/chi2test.m000066400000000000000000000556061527311104300240510ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} chi2test (@var{x}) ## @deftypefnx {statistics} {[@var{h}, @var{p}] =} chi2test (@var{x}) ## @deftypefnx {statistics} {[@var{h}, @var{p}, @var{stats}] =} chi2test (@var{x}) ## @deftypefnx {statistics} {[@dots{}] =} chi2test (@var{x}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@dots{}] =} chi2test (@dots{}, @qcode{'Alpha'}, @var{alpha}) ## ## Perform a chi-squared test (for independence or homogeneity). ## ## For 2-way contingency tables, @code{chi2test} performs a chi-squared test ## for independence or homogeneity, according to the sampling scheme and related ## question. Independence means that the two variables forming the 2-way ## table are not associated, hence you cannot predict from one another. ## Homogeneity refers to the concept of similarity, hence they all come from the ## same distribution. ## ## Both tests are computationally identical and will produce the same result. ## Nevertheless, they answer to different questions. Consider two variables, ## one for gender and another for smoking. To test independence (whether gender ## and smoking is associated), we would randomly sample from the general ## population and break them down into categories in the table. To test ## homogeneity (whether men and women share the same smoking habits), we would ## sample individuals from within each gender, and then measure their smoking ## habits (e.g. smokers vs non-smokers). ## ## When @code{chi2test} is called without any output arguments, it will print ## the result in the terminal including p-value, chi^2 statistic, and degrees of ## freedom. Otherwise it returns @var{h}, 1 if the null hypothesis is rejected ## at the significance level @var{alpha} and 0 otherwise, the p-value @var{p}, ## and a structure @var{stats} with the following fields: ## ## @multitable @columnfractions 0.2 0.75 ## @item @qcode{chi2stat} @tab the chi^2 statistic of the test. ## @item @qcode{df} @tab its degrees of freedom. ## @item @qcode{O} @tab the table tested: @var{x}, or for @qcode{"marginal"} the ## two-way table left after collapsing @var{x}. ## @item @qcode{E} @tab the expected values under the tested model, in the ## layout of @qcode{O}. ## @item @qcode{CohensW} @tab Cohen's @math{w}, the effect size of the test. ## @item @qcode{CohensWCI} @tab its confidence interval. ## @item @qcode{CramersV} @tab Cramer's @math{V}, for a two-way table and for ## the @qcode{"joint"} and @qcode{"marginal"} models, which test one. ## @item @qcode{CramersVCI} @tab its confidence interval. ## @end multitable ## ## Both effect sizes are corrected for bias. With @math{n} observations, ## @math{w = sqrt (max (chi^2 - df, 0) / n)}, and @math{V = w / sqrt (k)} with ## @math{k} one less than the smaller dimension of the two-way table; for ## @qcode{"joint"} that table has the variable it names as rows and every ## combination of the other two as columns. The uncorrected ## @math{sqrt (chi^2 / n)} is biased upwards, by @math{df / n} in its square. ## The confidence intervals invert the noncentral chi^2 distribution: the ## bounds @math{lambda} of the noncentrality consistent with the observed ## chi^2 give @math{sqrt (lambda / n)} for @math{w}, divided by ## @math{sqrt (k)} for @math{V}, whose upper bound is at most 1. ## ## @qcode{'Alpha'} sets the significance level @var{alpha} of @var{h} and the ## level @math{100 (1 - alpha)}% of both confidence intervals. It is 0.05 by ## default. ## ## Unlike MATLAB, in GNU Octave @code{chi2test} also supports 3-way tables, ## which involve three categorical variables (each in a different dimension of ## @var{x}. In its simplest form, @code{[@dots{}] = chi2test (@var{x})} will ## test for mutual independence among the three variables. Alternatively, ## when called in the form @code{[@dots{}] = chi2test (@var{x}, @var{name}, ## @var{value})}, it can perform the following tests: ## ## @multitable @columnfractions 0.2 0.1 0.7 ## @headitem @var{name} @tab @var{value} @tab Description ## @item "mutual" @tab [] @tab Mutual independence. All variables are ## independent from each other, (A, B, C). It takes no value; an empty ## matrix is accepted. ## @item "joint" @tab scalar @tab Joint independence. Two variables are jointly ## independent of the third, (AB, C). The scalar value corresponds to the ## dimension of the independent variable (i.e. 3 for C). ## @item "marginal" @tab scalar @tab Marginal independence. Two variables are ## independent if you ignore the third, (A, C). The scalar value corresponds ## to the dimension of the variable to be ignored (i.e. 2 for B); the table is ## collapsed over it and independence is tested in the two-way table that ## remains. ## @item "conditional" @tab scalar @tab Conditional independence. Two variables ## are independent given the third, (AC, BC). The scalar value corresponds to ## the dimension of the variable that forms the conditional dependence ## (i.e. 3 for C). ## @item "homogeneous" @tab [] @tab Homogeneous associations. Conditional ## (partial) odds-ratios are not related on the value of the third, ## (AB, AC, BC). It takes no value; an empty matrix is accepted. ## @end multitable ## ## When testing for homogeneous associations in 3-way tables, the iterative ## proportional fitting procedure is used. For small samples it is better to ## use the Cochran-Mantel-Haenszel Test. K-way tables for k > 3 are supported ## only for testing mutual independence. Similar to 2-way tables, no optional ## parameters are required for k > 3 multi-way tables. ## ## @code{chi2test} produces a warning if any cell of a 2x2 table has an expected ## frequency less than 5 or if more than 20% of the cells in larger 2-way tables ## have expected frequencies less than 5 or any cell with expected frequency ## less than 1. In such cases, use @code{fishertest}. ## ## @seealso{crosstab, fishertest, mcnemar_test} ## @end deftypefn function [h, p, stats] = chi2test (x, varargin) ## Check input arguments if (nargin < 1) print_usage (); endif if (isvector (x)) error ("chi2test: X must be a matrix."); endif if (! isreal (x)) error ("chi2test: values in X must be real numbers."); endif if (any (isnan (x(:)))) error ("chi2test: X must not have missing values (NaN)."); endif ## Get size and dimensions of contingency table sz = size (x); dim = length (sz); ## Parse the optional arguments: a model for a 3-way table with the ## dimension it names ('mutual' and 'homogeneous' take none, though an ## empty value is accepted), and 'Alpha' alpha = 0.05; model = 'mutual'; c_dim = []; models = {'mutual', 'joint', 'marginal', 'conditional', 'homogeneous'}; i = 1; while (i <= numel (varargin)) name = varargin{i}; if (ischar (name) && strcmpi (name, 'alpha')) if (i == numel (varargin)) error ("chi2test: optional arguments must be in pairs."); endif alpha = varargin{i+1}; if (! (isnumeric (alpha) && isscalar (alpha) && isreal (alpha) && alpha > 0 && alpha < 1)) error ("chi2test: invalid value for alpha."); endif i += 2; elseif (ischar (name) && any (strcmpi (name, models))) if (dim != 3) error ("chi2test: a model applies only to 3-way tables."); endif model = lower (name); novalue = any (strcmp (model, {'mutual', 'homogeneous'})); if (novalue && (i == numel (varargin) || ischar (varargin{i+1}))) i += 1; continue; endif if (i == numel (varargin)) error ("chi2test: optional arguments must be in pairs."); endif value = varargin{i+1}; if (! isnumeric (value)) error (strcat ("chi2test: value must be numeric in optional", ... " argument name/value pair, for 3-way tables.")); endif if (numel (value) > 1) error (strcat ("chi2test: value must be empty or scalar in", ... " optional argument name/value pair, for 3-way", ... " tables.")); endif if (! novalue && ! (isscalar (value) && any (value == 1:3))) error ("chi2test: the dimension must be 1, 2, or 3."); endif c_dim = value; i += 2; elseif (dim == 3) error ("chi2test: invalid model name for testing a 3-way table."); else error ("chi2test: invalid optional argument."); endif endwhile ## The smaller dimension of a two-way table less one, for Cramer's V; ## empty where the test has no two-way table vk = []; ## Calculate total sample size n = sum (x(:)); ## For 2-way contingency table if (length (sz) == 2) ## Calculate degrees of freedom df = prod (sz - 1); vk = min (sz) - 1; ## Calculate expected values E = sum (x')' * sum (x) / n; ## For 3-way contingency table elseif (length (sz) == 3) if (strcmp (model, 'mutual')) ## Calculate degrees of freedom df = prod (sz) - sum (sz) + 2; ## Calculate marginal table sums q1 = sum (sum (x, 2), 3); q2 = sum (sum (x, 1), 3); q3 = sum (sum (x, 1), 2); ns = sum (x(:)) ^ 2; for d1 = 1:size (x, 1) for d2 = 1:size (x, 2) for d3 = 1:size (x, 3) E(d1,d2,d3) = q1(d1,:,:) * q2(:,d2,:) * q3(:,:,d3) / ns; endfor endfor endfor elseif (strcmp (model, 'joint')) ## The test is of the table with the named variable as rows and every ## combination of the other two as columns vk = min (sz(c_dim), prod (sz) / sz(c_dim)) - 1; ## Calculate degrees of freedom c_sz = sz; c_sz(c_dim) = []; df = (sz(c_dim) - 1) * (prod (c_sz) - 1); ## Rearrange dimensions so that independent variable goes in dim 1 dm = [1, 2, 3]; dm(c_dim) = []; x = permute (x, [c_dim, dm]); ## Calculate partial table sums q1 = sum (sum (x, 1), 1); q2 = sum (sum (x, 2), 3); n = sum (x(:)); for d1 = 1:size (x, 1) for d2 = 1:size (x, 2) for d3 = 1:size (x, 3) E(d1,d2,d3) = q1(:,d2,d3) * q2(d1) / n; endfor endfor endfor ## Rearrange OBSERVED and EXPECTED matrices in original dimensions x = ipermute (x, [c_dim, dm]); E = ipermute (E, [c_dim, dm]); elseif (strcmp (model, 'marginal')) ## Collapse over the ignored variable and test independence in the ## two-way table that remains x = reshape (sum (x, c_dim), sz(setdiff (1:3, c_dim))); sz = size (x); vk = min (sz) - 1; dim = 2; df = prod (sz - 1); E = sum (x, 2) * sum (x, 1) / n; elseif (strcmp (model, 'conditional')) ## Calculate degrees of freedom c_sz = sz; c_sz(c_dim) = []; df = prod (c_sz - 1) * sz(c_dim); ## Rearrange dimensions so that conditional variable goes in dim 1 dm = [1, 2, 3]; dm(c_dim) = []; x = permute (x, [c_dim, dm]); ## Calculate partial table sums q1 = sum (sum (x, 3), 3); q2 = sum (sum (x, 2), 2); q3 = sum (sum (x, 2), 3); ## Calculate expected values for d1 = 1:size (x, 1) for d2 = 1:size (x, 2) for d3 = 1:size (x, 3) E(d1,d2,d3) = q1(d1,d2) * q2(d1,:,d3) / q3(d1); endfor endfor endfor ## Rearrange OBSERVED and EXPECTED matrices in original dimensions x = ipermute (x, [c_dim, dm]); E = ipermute (E, [c_dim, dm]); else # homogeneous ## Calculate degrees of freedom df = prod (sz - 1); ## Fit the three two-way margins by iterative proportional fitting, ## until those it last left behind match to within rounding ratio = @(a, b) a ./ (b + (b == 0)); E = ones (sz); converged = false; for iter = 1:1000 E = E .* ratio (sum (x, 3), sum (E, 3)); E = E .* ratio (sum (x, 2), sum (E, 2)); E = E .* ratio (sum (x, 1), sum (E, 1)); gap = max ([abs(sum (E, 3) - sum (x, 3))(:); ... abs(sum (E, 2) - sum (x, 2))(:)]); if (gap <= 1e-12 * n) converged = true; break; endif endfor if (! converged) warning ("chi2test: the homogeneous model did not converge."); endif endif ## For k-way contingency table, where k > 3 else ## Calculate degrees of freedom df = prod (sz) - sum (sz) + 2; ## Calculate squared sample size ns = sum (x(:)) ^ (dim - 1); ## Calculate marginal table sums for each available dimension for i = 1:dim qi(i) = {x}; remdim = [1:dim]; remdim(remdim == i) = []; for j = 1:length (remdim) qi(i) = sum (qi{i}, remdim(j)); endfor qi(i) = squeeze (qi{i}); endfor ## Iterate through all cells cn = numel (x); for i = 1:cn E(i) = 1; cid = i; ## Keep track of indexing for d = dim - 1:-1:1 idx(d+1) = ceil (cid / prod (sz(1:d))); if (idx(d+1) > 1) cid -= (idx(d+1) - 1) * prod (sz(1:d)); endif endfor idx(1) = cid; ## Calculate the expected value for j = 1:dim E(i) = E(i) * qi{j}(idx(j)); endfor E(i) = E(i) / ns; endfor ## Reshape to original dimensions E = reshape (E, sz); endif ## Check expected values and display warnings if ((dim == 2 && isequal (sz, [2, 2]) && any (E(:) < 5)) || ... (dim == 2 && any (sz > 2) && sum (E(:) < 5) > 0.2 * numel (E)) || ... (dim > 2 && sum (E(:) < 5) > 0.2 * numel (E))) warning ("chi2test: Expected values less than 5."); endif if (any (E(:) < 1)) warning ("chi2test: Expected values less than 1."); endif ## Calculate chi-squared and p-value cells = ((x - E) .^2) ./ E; chisq = sum (cells(:)); p = chi2cdf (chisq, df, 'upper'); h = double (p < alpha); ## Statistics, and the effect sizes with their confidence intervals, ## only when asked for, since the intervals are costly if (nargout > 2) stats.chi2stat = chisq; stats.df = df; stats.O = x; stats.E = E; if (isfinite (chisq)) n = sum (x(:)); stats.CohensW = sqrt (max (chisq - df, 0) / n); stats.CohensWCI = sqrt (__ncx2bounds__ (chisq, df, alpha) / n); else stats.CohensW = NaN; stats.CohensWCI = [NaN, NaN]; endif if (! isempty (vk) && vk > 0) stats.CramersV = stats.CohensW / sqrt (vk); stats.CramersVCI = min (stats.CohensWCI / sqrt (vk), 1); endif endif ## Print results if no output requested if (nargout == 0) printf ("p-val = %f with chi^2 statistic = %f and d.f. = %d.\n", ... p, chisq, df); endif endfunction %!function p = chi2p (varargin) %! [~, p] = chi2test (varargin{:}); %!endfunction %!test %! ## Below the resolution of 1 - chi2cdf %! [~, p, st] = chi2test ([300, 100; 100, 300]); %! assert_equal (p, erfc (sqrt (st.chi2stat / 2)), -1e-12); %!test %! x = [11, 3, 8; 2, 9, 14; 12, 13, 28]; %! [~, p] = chi2test (x); %! assert_equal (p, 0.017787, 1e-6); %!test %! x = [11, 3, 8; 2, 9, 14; 12, 13, 28]; %! [~, ~, st] = chi2test (x); %! assert_equal (st.chi2stat, 11.9421, 1e-4); %!test %! x = [11, 3, 8; 2, 9, 14; 12, 13, 28]; %! [~, ~, st] = chi2test (x); %! assert_equal (st.df, 4); %!test %! x = [11, 3, 8; 2, 9, 14; 12, 13, 28]; %! [~, ~, st] = chi2test (x); %! assert_equal (st.O, x); %!assert_equal (chi2test ([11, 3, 8; 2, 9, 14; 12, 13, 28]), 1) %!assert_equal (chi2test ([11, 3, 8; 2, 9, 14; 12, 13, 28], 'Alpha', 0.01), 0) %!test %! ## Effect sizes corrected for bias; the intervals are R's inversion of %! ## pchisq with ncp by uniroot %! x = [11, 3, 8; 2, 9, 14; 12, 13, 28]; %! [~, ~, st] = chi2test (x); %! assert_equal (st.CohensW, sqrt ((st.chi2stat - 4) / 100), -1e-14); %!test %! [~, ~, st] = chi2test ([11, 3, 8; 2, 9, 14; 12, 13, 28]); %! assert_equal (st.CohensWCI, [0.054364192439641336, 0.50550722873705722], ... %! -1e-10); %!test %! [~, ~, st] = chi2test ([11, 3, 8; 2, 9, 14; 12, 13, 28]); %! assert_equal (st.CramersV, 0.19927484317340177, -1e-12); %!test %! [~, ~, st] = chi2test ([11, 3, 8; 2, 9, 14; 12, 13, 28]); %! assert_equal (st.CramersVCI, ... %! [0.054364192439641336, 0.50550722873705722] / sqrt (2), ... %! -1e-10); %!test %! ## A higher confidence level widens the interval on both sides %! x = [11, 3, 8; 2, 9, 14; 12, 13, 28]; %! [~, ~, s95] = chi2test (x); %! [~, ~, s99] = chi2test (x, 'Alpha', 0.01); %! assert_equal ([s99.CohensWCI(1) < s95.CohensWCI(1), ... %! s99.CohensWCI(2) > s95.CohensWCI(2)], [true, true]); %!test %!shared x %! x(:,:,1) = [59, 32; 9,16]; %! x(:,:,2) = [55, 24;12,33]; %! x(:,:,3) = [107,80;17,56]; %!assert_equal (chi2p (x), 2.282063427117009e-11, 1e-14); %!assert_equal (chi2p (x, 'mutual', []), 2.282063427117009e-11, 1e-14); %!assert_equal (chi2p (x, 'joint', 1), 1.164834895206468e-11, 1e-14); %!assert_equal (chi2p (x, 'joint', 2), 7.771350230001417e-11, 1e-14); %!assert_equal (chi2p (x, 'joint', 3), 0.07151361728026107, 1e-14); %!assert_equal (chi2p (x, 'marginal', 1), 0.12455768155123595, -1e-12); %!assert_equal (chi2p (x, 'marginal', 2), 0.039793350279010681, -1e-12); %!assert_equal (chi2p (x, 'marginal', 3), 9.0141038839122684e-13, -1e-12); %!assert_equal (chi2p (x, 'conditional', 1), 0.2303114201312508, 1e-14); %!assert_equal (chi2p (x, 'conditional', 2), 0.0958810684407079, 1e-14); %!assert_equal (chi2p (x, 'conditional', 3), 2.648037344954446e-11, 1e-14); %!assert_equal (chi2p (x, 'homogeneous', []), 0.57357539370887889, -1e-10); %!assert_equal (chi2p (x, 'homogeneous'), chi2p (x, 'homogeneous', [])); %!assert_equal (chi2p (x, 'mutual'), chi2p (x)); %!assert_equal (chi2p (x, 'joint', 3, 'Alpha', 0.01), chi2p (x, 'joint', 3)); %!test %! [~, ~, st] = chi2test (x); %! assert_equal (st.chi2stat, 64.0982, 1e-4); %! assert_equal (st.df, 7); %! assert_equal (st.E(:,:,1), [42.903, 39.921; 17.185, 15.991], 1e-3); %!test %! [~, ~, st] = chi2test (x, 'joint', 2); %! assert_equal (st.chi2stat, 56.0943, 1e-4); %! assert_equal (st.df, 5); %! assert_equal (st.E(:,:,2), [40.922, 38.078; 23.310, 21.690], 1e-3); %!test %! [~, ~, st] = chi2test (x, 'marginal', 3); %! assert_equal (st.chi2stat, 51.0479, 1e-4); %! assert_equal (st.df, 1); %! assert_equal (st.O, sum (x, 3)); %! assert_equal (st.E, [184.926, 172.074; 74.074, 68.926], 1e-3); %!test %! [~, ~, st] = chi2test (x, 'conditional', 3); %! assert_equal (st.chi2stat, 52.2509, 1e-4); %! assert_equal (st.df, 3); %! assert_equal (st.E(:,:,1), [53.345, 37.655; 14.655, 10.345], 1e-3); %!test %! [~, ~, st] = chi2test (x, 'homogeneous', []); %! assert_equal (st.chi2stat, 1.1117, 1e-4); %! assert_equal (st.df, 2); %! assert_equal (st.E(:,:,1), [60.469, 30.531; 7.531, 17.469], 1e-3); %!test %! ## The homogeneous model reproduces every two-way margin %! [~, ~, st] = chi2test (x, 'homogeneous'); %! E = st.E; %! assert_equal ([sum(E, 1)(:); sum(E, 2)(:); sum(E, 3)(:)], ... %! [sum(x, 1)(:); sum(x, 2)(:); sum(x, 3)(:)], -1e-10); %!test %! ## 'joint' measures V on the table flattened to its variable against the %! ## other two; bounds from R as above %! [h, ~, st] = chi2test (x, 'joint', 3, 'Alpha', 0.01); %! assert_equal (st.CohensWCI, [0, 0.24137302257386881], -1e-10); %!test %! [~, ~, st] = chi2test (x, 'marginal', 3); %! assert_equal (st.CramersV, 0.31637908166758733, -1e-12); %!test %! [~, ~, st] = chi2test (x, 'marginal', 3); %! assert_equal (st.CohensWCI, [0.23187195991809992, 0.40717646803341606], ... %! -1e-10); %!test %! ## No two-way table, no Cramer's V %! [~, ~, st] = chi2test (x, 'conditional', 3); %! assert_equal (isfield (st, 'CramersV'), false); %!test %! [~, ~, st] = chi2test (x); %! assert_equal (isfield (st, 'CramersV'), false); %!test %! ## E keeps the layout of a table whose dimensions differ %! y = reshape ([12 7 3 9 15 4 6 8 11 5 14 2 9 10 3 7 6 13], [3, 2, 3]); %! [~, ~, st] = chi2test (y, 'joint', 2); %! assert_equal (sum (st.E, 2), sum (y, 2), -1e-12); %!test %! y = reshape ([12 7 3 9 15 4 6 8 11 5 14 2 9 10 3 7 6 13], [3, 2, 3]); %! [~, ~, st] = chi2test (y, 'conditional', 2); %! assert_equal (size (st.E), [3, 2, 3]); %!test %! ## Chi-squares of R's loglin on a 3-by-2-by-3 table %! y = reshape ([12 7 3 9 15 4 6 8 11 5 14 2 9 10 3 7 6 13], [3, 2, 3]); %! [~, ~, st] = chi2test (y, 'homogeneous'); %! assert_equal (st.chi2stat, 15.05772742, -1e-9); %!test %! ## Marginal independence is tested in the collapsed table, as R's %! ## chisq.test does it %! y = reshape ([12 7 3 9 15 4 6 8 11 5 14 2 9 10 3 7 6 13], [3, 2, 3]); %! [~, ~, st] = chi2test (y, 'marginal', 2); %! assert_equal ([st.chi2stat, st.df], [7.584460, 4], -1e-6); ## Check warnings %!warning h = chi2test (ones (2)); %!warning h = chi2test (ones (3, 2)); %!warning h = chi2test (0.4 * ones (3)); ## Test input validation %!error chi2test (); %!error chi2test ([1, 2, 3, 4, 5]); %!error ... %! chi2test ([1, 2; 2, 1+3i]); %!error ... %! chi2test ([NaN, 6; 34, 12]); %!error ... %! chi2test (ones (3, 3), 'mutual', []); %!error ... %! chi2test (ones (3, 3, 3, 4), 'mutual'); %!error ... %! chi2test (ones (3, 3, 3), 'testtype', 2); %!error ... %! chi2test (ones (3, 3), 'testtype', 2); %!error ... %! chi2test (ones (3, 3, 3), 'joint'); %!error ... %! chi2test (ones (3, 3), 'Alpha'); %!error ... %! chi2test (ones (3, 3, 3), 'joint', 'a'); %!error ... %! chi2test (ones (3, 3, 3), 'joint', [2, 3]); %!error ... %! chi2test (ones (3, 3, 3), 'joint', 4); %!error ... %! chi2test (ones (3, 3, 3), 'marginal', []); %!error chi2test (ones (3, 3), 'Alpha', 0); %!error chi2test (ones (3, 3), 'Alpha', 1.5); %!error ... %! chi2test (ones (3, 3), 'Alpha', [0.1, 0.2]); statistics-release-2.1.2/inst/Hypothesis_Testing/correlation_test.m000066400000000000000000000321021527311104300256660ustar00rootroot00000000000000## Copyright (C) 1995-2017 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} correlation_test (@var{x}, @var{y}) ## @deftypefnx {statistics} {[@var{h}, @var{p}] =} correlation_test (@var{y}, @var{x}) ## @deftypefnx {statistics} {[@var{h}, @var{p}, @var{stats}] =} correlation_test (@var{y}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} correlation_test (@var{y}, @var{x}, @var{Name}, @var{Value}) ## ## Perform a correlation coefficient test to determine whether two samples ## @var{x} and @var{y} come from uncorrelated populations. ## ## @code{@var{h} = correlation_test (@var{y}, @var{x})} tests the null ## hypothesis that the two samples @var{x} and @var{y} come from uncorrelated ## populations. The result is @var{h} = 0 if the null hypothesis cannot be ## rejected at the significance level @var{alpha}, or @var{h} = 1 if it can. ## @var{y} and @var{x} must be vectors of equal length with finite real ## numbers. ## ## The p-value of the test is returned in @var{p}. @var{stats} is a ## structure with the following fields: ## @multitable @columnfractions 0.2 0.70 ## @headitem Field @tab Value ## @item @qcode{method} @tab the type of correlation coefficient used ## for the test ## @item @qcode{tstat} @tab the t statistic, for Pearson's and Spearman's ## coefficients ## @item @qcode{df} @tab its degrees of freedom, @math{n - 2} ## @item @qcode{zval} @tab the z statistic, for Kendall's coefficient ## @item @qcode{dist} @tab the respective distribution for the test ## @item @qcode{alt} @tab the alternative hypothesis for the test ## @item @qcode{CorrCoef} @tab the correlation coefficient, the effect size ## @item @qcode{CorrCoefCI} @tab its @math{100 (1 - alpha)}% confidence ## interval, one-sided for a one-sided test ## @end multitable ## ## Pearson's and Spearman's coefficients are tested with ## @math{t = r sqrt ((n - 2) / (1 - r^2))} on @math{n - 2} degrees of freedom, ## as R's @code{cor.test} does without an exact test; Kendall's with its ## normal approximation, whose variance allows for ties. The confidence ## interval is Fisher's @math{z} transform of the coefficient, with standard ## error @math{1 / sqrt (n - 3)} for Pearson's, ## @math{sqrt ((1 + r^2 / 2) / (n - 3))} for Spearman's and ## @math{sqrt (0.437 / (n - 4))} for Kendall's, after Bonett and Wright (2000). ## ## ## @code{[@dots{}] = correlation_test (@dots{}, @var{name}, @var{value})} ## specifies one or more of the following name/value pairs: ## ## @multitable @columnfractions 0.2 0.75 ## @headitem Name @tab Value ## @item @qcode{'alpha'} @tab the significance level of @var{h} and the level ## of the confidence interval. Default is 0.05. ## ## @item @qcode{'tail'} @tab a string specifying the alternative hypothesis ## @end multitable ## @multitable @columnfractions 0.25 0.65 ## @item @qcode{'both'} @tab @math{corrcoef} is not 0 (two-tailed, default) ## @item @qcode{'left'} @tab @math{corrcoef} is less than 0 (left-tailed) ## @item @qcode{'right'} @tab @math{corrcoef} is greater than 0 ## (right-tailed) ## @end multitable ## ## @multitable @columnfractions 0.2 0.75 ## @item @qcode{'method'} @tab a string specifying the correlation ## coefficient used for the test ## @end multitable ## @multitable @columnfractions 0.25 0.65 ## @item @qcode{'pearson'} @tab Pearson's product moment correlation ## (Default) ## @item @qcode{'kendall'} @tab Kendall's rank correlation tau ## @item @qcode{'spearman'} @tab Spearman's rank correlation rho ## @end multitable ## ## @seealso{regression_ftest, regression_ttest} ## @end deftypefn function [h, p, stats] = correlation_test (x, y, varargin) if (nargin < 2) print_usage (); endif if (! isvector (x) || ! isvector (y) || length (x) != length (y)) error ("correlation_test: X and Y must be vectors of equal length."); endif ## Force to column vectors x = x(:); y = y(:); ## Check for finite real numbers in X and Y if (! all (isfinite (x)) || ! isreal (x)) error ("correlation_test: X must contain finite real numbers."); endif if (! all (isfinite (y(:))) || ! isreal (y)) error ("correlation_test: Y must contain finite real numbers."); endif ## Set default arguments alpha = 0.05; tail = 'both'; method = 'pearson'; ## Check additional options i = 1; while (i <= length (varargin)) switch lower (varargin{i}) case 'alpha' i = i + 1; alpha = varargin{i}; ## Check for valid alpha if (! isscalar (alpha) || ! isnumeric (alpha) || ... alpha <= 0 || alpha >= 1) error ("correlation_test: invalid value for alpha."); endif case 'tail' i = i + 1; tail = varargin{i}; if (! any (strcmpi (tail, {'both', 'left', 'right'}))) error ("correlation_test: invalid value for tail."); endif case 'method' i = i + 1; method = varargin{i}; if (! any (strcmpi (method, {'pearson', 'kendall', 'spearman'}))) error ("correlation_test: invalid value for method."); endif otherwise error ("correlation_test: invalid Name argument."); endswitch i = i + 1; endwhile n = length (x); method = lower (method); tail = lower (tail); if (strcmp (method, 'pearson')) r = corr (x, y); stats.method = 'Pearson''s product moment correlation'; elseif (strcmp (method, 'kendall')) r = kendall (x, y); stats.method = 'Kendall''s rank correlation tau'; else # spearman r = spearman (x, y); stats.method = 'Spearman''s rank correlation rho'; endif if (strcmp (method, 'kendall')) ## The concordance score and its variance, allowing for ties in either ## sample (Kendall, 1970) S = 0; for i = 1:n-1 S += sum (sign (x(i) - x(i+1:n)) .* sign (y(i) - y(i+1:n))); endfor [~, ~, jx] = unique (x); [~, ~, jy] = unique (y); t = accumarray (jx, 1); u = accumarray (jy, 1); v = (n * (n - 1) * (2 * n + 5) - sum (t .* (t - 1) .* (2 * t + 5)) ... - sum (u .* (u - 1) .* (2 * u + 5))) / 18 ... + sum (t .* (t - 1) .* (t - 2)) * sum (u .* (u - 1) .* (u - 2)) ... / (9 * n * (n - 1) * (n - 2)) ... + sum (t .* (t - 1)) * sum (u .* (u - 1)) / (2 * n * (n - 1)); stats.zval = S / sqrt (v); stats.dist = 'standard normal'; lower_tail = normcdf (stats.zval); upper_tail = normcdf (-stats.zval); se = sqrt (0.437 / (n - 4)); else stats.tstat = r * sqrt ((n - 2) / (1 - r ^ 2)); stats.df = n - 2; stats.dist = 'Student''s t'; lower_tail = tcdf (stats.tstat, stats.df); upper_tail = tcdf (stats.tstat, stats.df, 'upper'); if (strcmp (method, 'pearson')) se = 1 / sqrt (n - 3); else se = sqrt ((1 + r ^ 2 / 2) / (n - 3)); endif endif ## Based on the "tail" argument determine the P-value and the quantile of ## the confidence interval switch (tail) case 'both' p = 2 * min (lower_tail, upper_tail); z = norminv (1 - alpha / 2); case 'right' p = upper_tail; z = norminv (1 - alpha); case 'left' p = lower_tail; z = norminv (1 - alpha); endswitch stats.alt = tail; ## The coefficient is the effect size; its interval by Fisher's z, one-sided ## for a one-sided test stats.CorrCoef = r; if (isreal (se) && se > 0) stats.CorrCoefCI = tanh (atanh (r) + [-1, 1] * z * se); else stats.CorrCoefCI = [NaN, NaN]; endif if (strcmp (tail, 'right')) stats.CorrCoefCI(2) = 1; elseif (strcmp (tail, 'left')) stats.CorrCoefCI(1) = -1; endif ## Determine the test outcome h = double (p < alpha); endfunction %!test %! x = [6 7 7 9 10 12 13 14 15 17]; %! y = [19 22 27 25 30 28 30 29 25 32]; %! [h, p, stats] = correlation_test (x, y); %! assert_equal (stats.CorrCoef, corr (x', y'), 1e-14); %! assert_equal (p, 0.0223, 1e-4); %!test %! x = [6 7 7 9 10 12 13 14 15 17]'; %! y = [19 22 27 25 30 28 30 29 25 32]'; %! [h, p, stats] = correlation_test (x, y); %! assert_equal (stats.CorrCoef, corr (x, y), 1e-14); %! assert_equal (p, 0.0223, 1e-4); %!test %! ## Below the resolution of 1 - tcdf, the p-value of corr in MATLAB R2024a %! x = (1:30)'; %! [~, p] = correlation_test (x, x + 0.01 * sin (x)); %! assert_equal (p, 6.68262089195668e-88, -1e-7); %!test %! ## Values from R's cor.test without exact tests, on tied data %! x = [1 2 2 3 4 4 4 5 6 7 7 8]'; %! y = [2 1 3 3 5 4 6 6 8 7 9 9]'; %! [~, ~, stats] = correlation_test (x, y); %! assert_equal (stats.CorrCoefCI, ... %! [0.8116031451285135, 0.98487109489995917], -1e-12); %!test %! x = [1 2 2 3 4 4 4 5 6 7 7 8]'; %! y = [2 1 3 3 5 4 6 6 8 7 9 9]'; %! [~, p, stats] = correlation_test (x, y, 'tail', 'right'); %! assert_equal ([p, stats.CorrCoefCI], ... %! [1.7690247076465537e-06, 0.84452474586233817, 1], -1e-12); %!test %! ## Kendall's variance allows for ties %! x = [1 2 2 3 4 4 4 5 6 7 7 8]'; %! y = [2 1 3 3 5 4 6 6 8 7 9 9]'; %! [~, p, stats] = correlation_test (x, y, 'method', 'kendall'); %! assert_equal ([stats.zval, p], ... %! [3.7786519487436312, 0.00015767962747712322], -1e-12); %!test %! x = [1 2 2 3 4 4 4 5 6 7 7 8]'; %! y = [2 1 3 3 5 4 6 6 8 7 9 9]'; %! [~, p] = correlation_test (x, y, 'method', 'spearman'); %! assert_equal (p, 1.2599089155461711e-06, -1e-12); %!test %! ## Spearman's test is symmetric in the sign of the coefficient %! x = (1:12)'; %! y = [3 1 4 2 6 5 8 9 7 12 10 11]'; %! [~, p] = correlation_test (x, -y, 'method', 'spearman'); %! assert_equal (p, 2.8428045348547355e-05, -1e-12); %!test %! x = (1:12)'; %! y = [3 1 4 2 6 5 8 9 7 12 10 11]'; %! [~, ~, stats] = correlation_test (x, y, 'method', 'spearman'); %! r = stats.CorrCoef; %! assert_equal (stats.CorrCoefCI, ... %! tanh (atanh (r) + [-1, 1] * norminv (0.975) ... %! * sqrt ((1 + r ^ 2 / 2) / 9)), -1e-14); %!test %! x = (1:12)'; %! y = [3 1 4 2 6 5 8 9 7 12 10 11]'; %! [~, ~, stats] = correlation_test (x, y, 'method', 'kendall', 'alpha', 0.01); %! r = stats.CorrCoef; %! assert_equal (stats.CorrCoefCI, ... %! tanh (atanh (r) + [-1, 1] * norminv (0.995) ... %! * sqrt (0.437 / 8)), -1e-14); %!test %! [~, ~, stats] = correlation_test ((1:12)', (12:-1:1)' + sin (1:12)', ... %! 'tail', 'left'); %! assert_equal (stats.CorrCoefCI(1), -1); %!test %! [~, ~, stats] = correlation_test ((1:12)', sin (1:12)', 'method', 'kendall'); %! assert_equal ([isfield(stats, 'zval'), isfield(stats, 'tstat')], ... %! [true, false]); ## Test input validation %!error correlation_test (); %!error correlation_test (1); %!error ... %! correlation_test ([1 2 NaN]', [2 3 4]'); %!error ... %! correlation_test ([1 2 Inf]', [2 3 4]'); %!error ... %! correlation_test ([1 2 3+i]', [2 3 4]'); %!error ... %! correlation_test ([1 2 3]', [2 3 NaN]'); %!error ... %! correlation_test ([1 2 3]', [2 3 Inf]'); %!error ... %! correlation_test ([1 2 3]', [3 4 3+i]'); %!error ... %! correlation_test ([1 2 3]', [3 4 4 5]'); %!error ... %! correlation_test ([1 2 3]', [2 3 4]', 'alpha', 0); %!error ... %! correlation_test ([1 2 3]', [2 3 4]', 'alpha', 1.2); %!error ... %! correlation_test ([1 2 3]', [2 3 4]', 'alpha', [.02 .1]); %!error ... %! correlation_test ([1 2 3]', [2 3 4]', 'alpha', 'a'); %!error ... %! correlation_test ([1 2 3]', [2 3 4]', 'some', 0.05); %!error ... %! correlation_test ([1 2 3]', [2 3 4]', 'tail', 'val'); %!error ... %! correlation_test ([1 2 3]', [2 3 4]', 'alpha', 0.01, 'tail', 'val'); %!error ... %! correlation_test ([1 2 3]', [2 3 4]', 'method', 0.01); %!error ... %! correlation_test ([1 2 3]', [2 3 4]', 'method', 'some'); statistics-release-2.1.2/inst/Hypothesis_Testing/detectdrift.m000066400000000000000000000550421527311104300246170ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{DDiagnostics} =} detectdrift (@var{Baseline}, @var{Target}) ## @deftypefnx {statistics} {@var{DDiagnostics} =} detectdrift (@var{Baseline}, @var{Target}, @var{Name}, @var{Value}) ## ## Detect drift between two data sets, one variable at a time. ## ## @code{@var{DDiagnostics} = detectdrift (@var{Baseline}, @var{Target})} ## measures, for each variable, how far the distribution in @var{Target} has ## moved from the one in @var{Baseline}, tests the change by permutation, and ## returns the results as a @code{stats.drift.DriftDiagnostics} object. ## @var{Baseline} and @var{Target} are both numeric matrices with the same ## number of columns, both @code{categorical} arrays, or both tables, read ## over the variables they share, which must be all the variables of one of ## them. An observation holding a missing value in a variable used is left ## out. ## ## Each variable is measured by a metric: a continuous one by ## @qcode{'ContinuousMetric'}, one holding levels by ## @qcode{'CategoricalMetric'}. With @math{F} and @math{G} the empirical ## distribution functions of the two samples and @math{H} that of the two ## pooled, the metrics are: ## ## @multitable @columnfractions 0.24 0.02 0.74 ## @headitem Metric @tab @tab Definition ## @item @qcode{'wasserstein'} @tab @tab The area between @math{F} and ## @math{G}, the default for a continuous variable. ## @item @qcode{'ks'} @tab @tab The largest difference between @math{F} ## and @math{G}. ## @item @qcode{'ad'} @tab @tab The Anderson-Darling statistic, the sum over ## the distinct pooled values but the largest of ## @math{(F - G)^2 / (H (1 - H))}, divided by the number of pooled ## observations. ## @item @qcode{'energy'} @tab @tab The energy distance, ## @math{sqrt (2 E|x - y| - E|x - x'| - E|y - y'|)}. ## @item @qcode{'hellinger'} @tab @tab @math{sqrt (1 - sum (sqrt (p q)))}, ## the default for a variable holding levels. ## @item @qcode{'bhattacharyya'} @tab @tab @math{-log (sum (sqrt (p q)))}. ## @item @qcode{'tv'} @tab @tab The total variation distance, ## @math{sum (abs (p - q)) / 2}. ## @item @qcode{'psi'} @tab @tab The population stability index, ## @math{sum ((p - q) log (p / q))}. ## @item @qcode{'chi2'} @tab @tab @math{sum ((p - q)^2 / p)}. ## @end multitable ## ## For a variable holding levels, @math{p} and @math{q} are the shares of each ## level in @var{Baseline} and @var{Target}, over the levels either holds, ## each count increased by 0.5 first so that a level one sample lacks leaves ## every metric finite. ## ## The p-value of a variable is the share of permutations whose metric is at ## least the observed one, the observed arrangement counted as the first ## permutation, so it is never below one over their number. Its 95% ## confidence interval is the Clopper-Pearson interval. The drift status is ## @qcode{"Drift"} where the interval lies below @qcode{'DriftThreshold'}, ## @qcode{"Stable"} where it lies above @qcode{'WarningThreshold'}, and ## @qcode{"Warning"} otherwise. Permutations are drawn in stages, 1000 or ## @qcode{'MaxNumPermutations'} if fewer, then four times as many at each ## stage, until the interval lies within a single one of the three regions or ## the next stage would pass @qcode{'MaxNumPermutations'}. They are drawn with ## @code{randperm}, so the state of @code{rand} decides them. ## ## The drift status of the data as a whole comes from the p-values corrected ## for testing every variable: by Bonferroni, the smallest p-value times the ## number of variables, or by the false discovery rate, the smallest ## Benjamini-Hochberg adjusted p-value. It is @qcode{"Drift"} below ## @qcode{'DriftThreshold'}, @qcode{"Warning"} below ## @qcode{'WarningThreshold'}, and @qcode{"Stable"} otherwise. ## ## @code{@var{DDiagnostics} = detectdrift (@dots{}, @var{Name}, @var{Value})} ## takes the following options. ## ## @multitable @columnfractions 0.3 0.02 0.68 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'VariableNames'} @tab @tab The variables to use, among those ## the two tables share. All of them by default. ## @item @qcode{'CategoricalVariables'} @tab @tab The variables holding ## levels: @qcode{'all'}, their indices, a logical vector over the variables, ## or, for tables, their names. A table variable holding logical values, an ## unordered @code{categorical} array, a @code{string} array or a cell array of ## character vectors, and every column of a @code{categorical} array, holds ## levels whether named here or not. ## @item @qcode{'ContinuousMetric'} @tab @tab @qcode{'wasserstein'}, the ## default, @qcode{'ks'}, @qcode{'ad'} or @qcode{'energy'}. ## @item @qcode{'CategoricalMetric'} @tab @tab @qcode{'hellinger'}, the ## default, @qcode{'bhattacharyya'}, @qcode{'tv'}, @qcode{'psi'} or ## @qcode{'chi2'}. ## @item @qcode{'DriftThreshold'} @tab @tab 0.05 by default. ## @item @qcode{'WarningThreshold'} @tab @tab 0.1 by default, and above ## @qcode{'DriftThreshold'}. ## @item @qcode{'MultipleTestCorrection'} @tab @tab @qcode{'bonferroni'}, the ## default, or @qcode{'fdr'}. ## @item @qcode{'MaxNumPermutations'} @tab @tab The most permutations drawn ## for a variable, 1000 by default. ## @item @qcode{'EstimatePValues'} @tab @tab @code{true} by default; with ## @code{false} only the metrics are computed. ## @item @qcode{'Options'} @tab @tab A structure as @code{statset} returns. ## Parallel computing and random streams are not implemented, and are refused ## where asked for. ## @end multitable ## ## MATLAB draws at least 1000 permutations whatever ## @qcode{'MaxNumPermutations'} says, and finishes a stage that passes it, so ## that a maximum of 3000 can end at 4001; here no variable is given more ## permutations than @qcode{'MaxNumPermutations'}. MATLAB's stages are ## otherwise its own, so the number of permutations and the p-values differ ## from MATLAB's, as they would between any two random draws. ## ## @seealso{stats.drift.DriftDiagnostics, knntest, mmdtest} ## @end deftypefn function DDiagnostics = detectdrift (Baseline, Target, varargin) ## Input validation if (nargin < 2) error ("detectdrift: too few input arguments."); endif optNames = {'VariableNames', 'CategoricalVariables', 'ContinuousMetric', ... 'CategoricalMetric', 'DriftThreshold', 'WarningThreshold', ... 'MultipleTestCorrection', 'MaxNumPermutations', ... 'EstimatePValues', 'Options'}; dfValues = {[], [], 'wasserstein', 'hellinger', 0.05, 0.1, 'bonferroni', ... 1000, true, []}; [vnames, catvars, cmetric, lmetric, dthr, wthr, mtc, maxperm, estp, ... opts, args] = parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (args)) error ("detectdrift: invalid optional paired argument."); endif cmetric = ddWord (cmetric, {'wasserstein', 'ks', 'ad', 'energy'}, ... 'ContinuousMetric'); lmetric = ddWord (lmetric, {'hellinger', 'bhattacharyya', 'tv', 'psi', ... 'chi2'}, 'CategoricalMetric'); mtc = ddWord (mtc, {'bonferroni', 'fdr'}, 'MultipleTestCorrection'); if (! (isnumeric (dthr) && isreal (dthr) && isscalar (dthr) && dthr > 0 && dthr < 1)) error ("detectdrift: 'DriftThreshold' must be a scalar between 0 and 1."); endif if (! (isnumeric (wthr) && isreal (wthr) && isscalar (wthr) && wthr > 0 && wthr < 1)) error (strcat ("detectdrift: 'WarningThreshold' must be a scalar", ... " between 0 and 1.")); endif if (! (dthr < wthr)) error (strcat ("detectdrift: 'DriftThreshold' must be smaller than", ... " 'WarningThreshold'.")); endif if (! (isnumeric (maxperm) && isreal (maxperm) && isscalar (maxperm) && isfinite (maxperm) && maxperm >= 1 && maxperm == fix (maxperm))) error ("detectdrift: 'MaxNumPermutations' must be a positive integer."); endif if (! ((islogical (estp) || isnumeric (estp)) && isscalar (estp))) error ("detectdrift: 'EstimatePValues' must be a logical scalar."); endif if (! isempty (opts)) if (! isstruct (opts)) error ("detectdrift: 'Options' must be a structure."); endif if (isfield (opts, 'UseParallel') && ! isempty (opts.UseParallel) && ! strcmpi (opts.UseParallel, 'never') && ! isequal (opts.UseParallel, false)) error ("detectdrift: parallel computing is not implemented."); endif if (isfield (opts, 'Streams') && ! isempty (opts.Streams)) error ("detectdrift: random streams are not implemented."); endif endif ## A categorical array is read as a table of its columns, each of levels X = Baseline; Y = Target; if (isa (X, 'categorical') != isa (Y, 'categorical')) error (strcat ("detectdrift: Baseline and Target must both be", ... " categorical arrays, or neither.")); endif if (isa (X, 'categorical')) X = ddColumns (X); Y = ddColumns (Y); endif ## The pooled sample coded one column per variable, rows holding a missing ## value left out, and which variables hold levels [Z, iscat, mx, my, errmsg, names, labels] = ... __twosample__ (X, Y, vnames, catvars, {'Baseline', 'Target'}); if (! isempty (errmsg)) error ("detectdrift: %s", errmsg); endif K = columns (Z); m = mx + my; if (! istable (Baseline)) names = arrayfun (@(j) sprintf ('x%d', j), 1:K, 'UniformOutput', false); endif ## The metric of each variable, observed and over the permutations metrics = cell (1, K); for j = 1:K if (iscat(j)) metrics{j} = lmetric; else metrics{j} = cmetric; endif endfor obs = zeros (1, K); for j = 1:K obs(j) = ddMetric (Z(:,j), 1:mx, mx+1:m, metrics{j}); endfor P = NaN (1, K); CI = NaN (2, K); NP = ones (1, K); perm = cell (K, 1); status = repmat ({''}, 1, K); overall = ''; for j = 1:K perm{j} = obs(j); endfor if (estp) for j = 1:K [perm{j}, P(j), CI(:,j), status{j}] = ddPermute (Z(:,j), mx, ... metrics{j}, obs(j), maxperm, dthr, wthr); NP(j) = numel (perm{j}); endfor overall = ddOverall (P, mtc, dthr, wthr); endif S = struct (); S.Baseline = Baseline; S.Target = Target; S.VariableNames = names; S.CategoricalVariables = find (iscat); S.Metrics = cellfun (@ddLabel, metrics, 'UniformOutput', false); S.MetricValues = obs; S.PValues = P; S.ConfidenceIntervals = CI; S.NumPermutations = NP; S.PermutationResults = perm; S.DriftStatus = status; S.MultipleTestCorrection = mtc; S.MultipleTestDriftStatus = overall; S.DriftThreshold = dthr; S.WarningThreshold = wthr; S.Data = Z; S.NumBaseline = mx; S.Labels = labels; S.Estimated = logical (estp); DDiagnostics = stats.drift.DriftDiagnostics (S); endfunction ## An option holding one word of a list, matched in any case. function w = ddWord (w, list, name) if (isa (w, 'string') && isscalar (w)) w = char (w); endif if (! (ischar (w) && isrow (w) && any (strcmpi (w, list)))) error ("detectdrift: '%s' must be one of %s.", name, ... strjoin (strcat ("'", list, "'"), ', ')); endif w = lower (w); endfunction ## The name a metric is reported by. function s = ddLabel (name) switch (name) case 'wasserstein', s = 'Wasserstein'; case 'ks', s = 'KolmogorovSmirnov'; case 'ad', s = 'AndersonDarling'; case 'energy', s = 'Energy'; case 'hellinger', s = 'Hellinger'; case 'bhattacharyya', s = 'Bhattacharyya'; case 'tv', s = 'TotalVariation'; case 'psi', s = 'PopulationStabilityIndex'; case 'chi2', s = 'ChiSquare'; endswitch endfunction ## A categorical array as a table of its columns. function T = ddColumns (C) K = columns (C); cols = cell (1, K); for j = 1:K cols{j} = C(:,j); endfor T = table (cols{:}); endfunction ## The metric of one variable with the rows A as the baseline and B as the ## target. function v = ddMetric (z, a, b, name) switch (name) case {'wasserstein', 'ks', 'ad'} x = sort (z(a)); y = sort (z(b)); u = unique ([x; y]); F = ddCount (x, u) / numel (x); G = ddCount (y, u) / numel (y); switch (name) case 'wasserstein' v = sum (abs (F(1:end-1) - G(1:end-1)) .* diff (u)); case 'ks' v = max (abs (F - G)); case 'ad' H = (ddCount (x, u) + ddCount (y, u)) / (numel (x) + numel (y)); k = H < 1; v = sum ((F(k) - G(k)) .^ 2 ./ (H(k) .* (1 - H(k)))) ... / (numel (x) + numel (y)); endswitch case 'energy' x = sort (z(a)); y = sort (z(b)); exy = ddMeanAbs (x, y); exx = ddMeanAbs (x, x); eyy = ddMeanAbs (y, y); v = sqrt (max (0, 2 * exy - exx - eyy)); otherwise ## Levels 1..L, each count increased by 0.5 L = max (z); cx = accumarray (z(a), 1, [L, 1]) + 0.5; cy = accumarray (z(b), 1, [L, 1]) + 0.5; p = cx / sum (cx); q = cy / sum (cy); switch (name) case 'hellinger' v = sqrt (max (0, 1 - sum (sqrt (p .* q)))); case 'bhattacharyya' v = -log (sum (sqrt (p .* q))); case 'tv' v = sum (abs (p - q)) / 2; case 'psi' v = sum ((p - q) .* log (p ./ q)); case 'chi2' v = sum ((p - q) .^ 2 ./ p); endswitch endswitch endfunction ## How many of the sorted values S are at most each value of U. function c = ddCount (s, u) c = lookup (s, u); c = c(:); endfunction ## The mean of |x_i - y_j| over every pair, from the sorted X and Y. function e = ddMeanAbs (x, y) ## Each x_i against the y at most it and the y above it cy = [0; cumsum(y)]; k = lookup (y, x); k = k(:); below = x .* k - cy(k + 1); above = (cy(end) - cy(k + 1)) - x .* (numel (y) - k); e = sum (below + above) / (numel (x) * numel (y)); endfunction ## The permutations of one variable, in stages, and what they say. function [vals, p, ci, status] = ddPermute (z, mx, name, obs, maxperm, ... dthr, wthr) m = numel (z); vals = obs; count = 1; tol = 1e-12 * abs (obs); target = min (1000, maxperm); while (true) add = target - numel (vals); new = zeros (add, 1); for i = 1:add r = randperm (m); new(i) = ddMetric (z, r(1:mx), r(mx+1:end), name); endfor vals = [vals; new]; count += sum (new >= obs - tol); N = numel (vals); p = count / N; ci = ddClopperPearson (count, N); done = ci(2) < dthr || ci(1) > wthr || (ci(1) > dthr && ci(2) < wthr); if (done || N >= maxperm) break; endif target = min (4 * N, maxperm); endwhile if (ci(2) < dthr) status = 'Drift'; elseif (ci(1) > wthr) status = 'Stable'; else status = 'Warning'; endif endfunction ## The 95% Clopper-Pearson interval of X successes in N trials. function ci = ddClopperPearson (x, N) ci = [0; 1]; if (x > 0) ci(1) = betaincinv (0.025, x, N - x + 1); endif if (x < N) ci(2) = betaincinv (0.975, x + 1, N - x); endif endfunction ## The drift status of the data as a whole. function s = ddOverall (P, mtc, dthr, wthr) k = numel (P); if (strcmp (mtc, 'bonferroni')) pc = min (P) * k; else ps = sort (P); pc = min (ps .* k ./ (1:k)); endif if (pc < dthr) s = 'Drift'; elseif (pc < wthr) s = 'Warning'; else s = 'Stable'; endif endfunction ## Expected values from MATLAB R2024a %!shared X, Y, C, T %! X = [0; 1; 2; 3; 7]; %! Y = [1; 2; 5; 6]; %! C = categorical ([1; 1; 2; 3; 3; 3]); %! T = categorical ([1; 2; 2; 2; 3]); %!test %! D = detectdrift (X, Y, 'EstimatePValues', false); %! assert_equal (D.MetricValues, 1.3, -1e-14); %! assert_equal (D.Metrics, string ('Wasserstein')); %! assert_equal (D.VariableNames, string ('x1')); %!assert_equal (detectdrift (X, Y, 'EstimatePValues', false, ... %! 'ContinuousMetric', 'ks').MetricValues, 0.3, -1e-14) %!assert_equal (detectdrift (X, Y, 'EstimatePValues', false, ... %! 'ContinuousMetric', 'AD').MetricValues, ... %! 0.152357142857143, -1e-13) %!assert_equal (detectdrift (X, Y, 'EstimatePValues', false, ... %! 'ContinuousMetric', 'energy').MetricValues, ... %! 0.768114574786861, -1e-13) %!assert_equal (detectdrift (C, T, 'EstimatePValues', false).MetricValues, ... %! 0.257526267734947, -1e-13) %!assert_equal (detectdrift (C, T, 'EstimatePValues', false, ... %! 'CategoricalMetric', 'tv').MetricValues, ... %! 0.338461538461538, -1e-13) %!assert_equal (detectdrift (C, T, 'EstimatePValues', false, ... %! 'CategoricalMetric', 'psi').MetricValues, ... %! 0.539045501736854, -1e-13) %!assert_equal (detectdrift (C, T, 'EstimatePValues', false, ... %! 'CategoricalMetric', 'chi2').MetricValues, ... %! 0.723584108199493, -1e-13) %!assert_equal (detectdrift (C, T, 'EstimatePValues', false, ... %! 'CategoricalMetric', 'bhattacharyya').MetricValues, ... %! 0.068621274723466, -1e-12) %!test %! ## A level the target lacks leaves every metric finite %! T2 = categorical ([1; 2; 2; 2]); %! D = detectdrift (C, T2, 'EstimatePValues', false, 'CategoricalMetric', 'psi'); %! assert_equal (D.MetricValues, 1.131879584405866, -1e-13); %! D = detectdrift (C, T2, 'EstimatePValues', false, 'CategoricalMetric', 'chi2'); %! assert_equal (D.MetricValues, 1.265643447461629, -1e-13); %! D = detectdrift (C, T2, 'EstimatePValues', false); %! assert_equal (D.MetricValues, 0.368461885678999, -1e-13); %!test %! D = detectdrift ([0, 5; 1, 3; 2, 8; 3, 1; 7, 4], [1, 9; 2, 7; 5, 8; 6, 6], ... %! 'EstimatePValues', false); %! assert_equal (D.MetricValues, [1.3, 3.3], -1e-14); %! assert_equal (D.VariableNames, string ({'x1', 'x2'})); %!test %! TB = table ([0; 1; 2; 3; 7; 4], categorical ([1; 1; 2; 3; 3; 3]), ... %! 'VariableNames', {'A', 'B'}); %! TT = table ([1; 2; 5; 6; 3], categorical ([1; 2; 2; 2; 3]), ... %! 'VariableNames', {'A', 'B'}); %! D = detectdrift (TB, TT, 'EstimatePValues', false); %! assert_equal (D.MetricValues, [0.9, 0.257526267734947], -1e-13); %! assert_equal (D.Metrics, string ({'Wasserstein', 'Hellinger'})); %! assert_equal (D.CategoricalVariables, 2); %!test %! D = detectdrift (X, Y, 'EstimatePValues', false); %! assert_equal (ismissing (D.DriftStatus), true); %! assert_equal (ismissing (D.MultipleTestDriftStatus), true); %! assert_equal (D.PValues, NaN); %! assert_equal (D.ConfidenceIntervals, [NaN; NaN]); %! assert_equal (D.NumPermutations, 1); %! assert_equal (D.PermutationResults.PermutationResults, {1.3}); %!test %! ## A shift no permutation reaches: the observed arrangement alone counts %! rand ('seed', 1); %! x = (1:60)' / 10; %! D = detectdrift ([x, x], [x + 10, x([2:60, 1])]); %! assert_equal (D.PValues, [0.001, 1]); %! assert_equal (D.ConfidenceIntervals, [2.53174874912919e-05, ... %! 0.996317916103134; 0.00555892427982512, 1], -1e-10); %! assert_equal (D.DriftStatus, string ({'Drift', 'Stable'})); %! assert_equal (D.NumPermutations, [1000, 1000]); %! assert_equal (D.PermutationResults.PermutationResults{1}(1), 10, -1e-14); %!test %! ## MaxNumPermutations is never passed %! rand ('seed', 1); %! x = (1:60)' / 10; %! D = detectdrift (x, x + 10, 'MaxNumPermutations', 50); %! assert_equal (D.NumPermutations, 50); %! assert_equal (D.PValues, 0.02); %! assert_equal (D.DriftStatus, string ('Warning')); %!test %! ## Bonferroni takes 4 * 0.001 and the false discovery rate 0.001 %! rand ('seed', 1); %! x = (1:60)' / 10; %! Z = repmat (x, 1, 4); %! B = detectdrift (Z, Z + 10, 'DriftThreshold', 0.002, 'WarningThreshold', 0.005); %! assert_equal (B.MultipleTestDriftStatus, string ('Warning')); %! assert_equal (B.MultipleTestCorrection, string ('Bonferroni')); %! F = detectdrift (Z, Z + 10, 'DriftThreshold', 0.002, ... %! 'WarningThreshold', 0.005, 'MultipleTestCorrection', 'fdr'); %! assert_equal (F.MultipleTestDriftStatus, string ('Drift')); %! assert_equal (F.MultipleTestCorrection, string ('FalseDiscoveryRate')); %!error detectdrift (1) %!error detectdrift (X, Y, 'Tail', 1) %!error ... %! detectdrift (X, Y, 'ContinuousMetric', 'kl') %!error ... %! detectdrift (X, Y, 'CategoricalMetric', 'kl') %!error ... %! detectdrift (X, Y, 'MultipleTestCorrection', 'holm') %!error ... %! detectdrift (X, Y, 'DriftThreshold', 1) %!error ... %! detectdrift (X, Y, 'WarningThreshold', 0) %!error ... %! detectdrift (X, Y, 'DriftThreshold', 0.1, 'WarningThreshold', 0.1) %!error ... %! detectdrift (X, Y, 'MaxNumPermutations', 0) %!error ... %! detectdrift (X, Y, 'EstimatePValues', 'no') %!error ... %! detectdrift (X, Y, 'Options', 1) %!error ... %! detectdrift (X, Y, 'Options', struct ('UseParallel', true)) %!error ... %! detectdrift (X, Y, 'Options', struct ('Streams', 1)) %!error ... %! detectdrift (C, X) %!error ... %! detectdrift ([X, X], Y) %!error ... %! detectdrift (NaN, Y) statistics-release-2.1.2/inst/Hypothesis_Testing/doc-cache000066400000000000000000005165651527311104300237050ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 88 # name: # type: sq_string # elements: 1 # length: 6 adtest # name: # type: sq_string # elements: 1 # length: 3685 statistics: h = adtest (x) statistics: h = adtest (x, Name, Value) statistics: [h, pval] = adtest (...) statistics: [h, pval, adstat, cv] = adtest (...) Anderson-Darling goodness-of-fit hypothesis test. h = adtest (x) returns a test decision for the null hypothesis that the data in vector x is from a population with a normal distribution, using the Anderson-Darling test. The alternative hypothesis is that x is not from a population with a normal distribution. The result h is 1 if the test rejects the null hypothesis at the 5% significance level, or 0 otherwise. h = adtest (x, Name, Value) returns a test decision for the Anderson-Darling test with additional options specified by one or more Name-Value pair arguments. For example, you can specify a null distribution other than normal, or select an alternative method for calculating the p-value, such as a Monte Carlo simulation. The following parameters can be parsed as Name-Value pair arguments. Name Description "Distribution" The distribution being tested for. It tests whether x could have come from the specified distribution. There are two choices available for parsing distribution parameters: One of the following char strings: "norm", "exp", "ev", "logn", "weibull", for defining either the 'normal', 'exponential', 'extreme value', lognormal, or 'Weibull' distribution family, respectively. In this case, x is tested against a composite hypothesis for the specified distribution family and the required distribution parameters are estimated from the data in x. The default is "norm". A cell array defining a distribution in which the first cell contains a char string with the distribution name, as mentioned above, and the consecutive cells containing all specified parameters of the null distribution. In this case, x is tested against a simple hypothesis. Name Value "Alpha" Significance level alpha for the test. Any scalar numeric value between 0 and 1. The default is 0.05 corresponding to the 5% significance level. "MCTol" Monte-Carlo standard error for the p-value, pval, value. which must be a positive scalar value. In this case, an approximation for the p-value is computed directly, using Monte-Carlo simulations. "Asymptotic" Method for calculating the p-value of the Anderson-Darling test, which can be either true or false logical value. If you specify 'true', adtest estimates the p-value using the limiting distribution of the Anderson-Darling test statistic. If you specify 'false', adtest calculates the p-value based on an analytical formula. For sample sizes greater than 120, the limiting distribution estimate is likely to be more accurate than the small sample size approximation method. If you specify a distribution family with unknown parameters for the distribution Name-Value pair (i.e. composite distribution hypothesis test), the "Asymptotic" option must be false. If you use MCTol to calculate the p-value using a Monte Carlo simulation, the "Asymptotic" option must be false. [h, pval] = adtest (...) also returns the p-value, pval, of the Anderson-Darling test, using any of the input arguments from the previous syntaxes. [h, pval, adstat, cv] = adtest (...) also returns the test statistic, adstat, and the critical value, cv, for the Anderson-Darling test. The Anderson-Darling test statistic belongs to the family of Quadratic Empirical Distribution Function statistics, which are based on the weighted sum of the difference [Fn(x)-F(x)]^2 over the ordered sample values X1 < X2 < ... < Xn, where F is the hypothesized continuous distribution and Fn is the empirical CDF based on the data sample with n sample points. See also: jbtest, kstest, lillietest, swtest # name: # type: sq_string # elements: 1 # length: 49 Anderson-Darling goodness-of-fit hypothesis test. # name: # type: sq_string # elements: 1 # length: 5 anova # name: # type: sq_string # elements: 1 # length: 725 statistics: anova Object-oriented interface for analysis of variance. The anova class provides a MATLAB-compatible object interface for analysis of variance. It stores factors, response data, model specification, and fitted results in one object. The class chooses the narrowest compatible backend, delegates the numeric computation to the existing ANOVA functions, and exposes common follow-up operations such as stats, groupmeans, boxchart, plotComparisons, varianceComponent, and multcompare. Models are fitted lazily. Methods that need fitted results call fit internally when necessary, so users may construct an object and immediately call inspection or post-hoc methods. See also: anova1, anova2, anovan, multcompare # name: # type: sq_string # elements: 1 # length: 51 Object-oriented interface for analysis of variance. # name: # type: sq_string # elements: 1 # length: 24 anova.CategoricalFactors # name: # type: sq_string # elements: 1 # length: 143 anova: property CategoricalFactors Categorical factors Positive integer indices of factors treated as categorical. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Categorical factors # name: # type: sq_string # elements: 1 # length: 18 anova.Coefficients # name: # type: sq_string # elements: 1 # length: 133 anova: property Coefficients Model coefficient estimates Numeric vector of fitted coefficient estimates. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Model coefficient estimates # name: # type: sq_string # elements: 1 # length: 25 anova.ExpandedFactorNames # name: # type: sq_string # elements: 1 # length: 215 anova: property ExpandedFactorNames Coefficient names Cell array of character vectors naming the model coefficients when the selected backend exposes them, otherwise an empty cell array. This property is read-only. # name: # type: sq_string # elements: 1 # length: 17 Coefficient names # name: # type: sq_string # elements: 1 # length: 17 anova.FactorNames # name: # type: sq_string # elements: 1 # length: 147 anova: property FactorNames Factor names String row vector containing the factor names used by the fitted ANOVA model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 12 Factor names # name: # type: sq_string # elements: 1 # length: 13 anova.Factors # name: # type: sq_string # elements: 1 # length: 139 anova: property Factors Factor data Table containing one variable for each factor used to fit the ANOVA model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 11 Factor data # name: # type: sq_string # elements: 1 # length: 13 anova.Formula # name: # type: sq_string # elements: 1 # length: 176 anova: property Formula Model formula Read-only structural formula value with response, predictor, term, nesting, and linear-predictor fields matching MATLAB's formula object. # name: # type: sq_string # elements: 1 # length: 13 Model formula # name: # type: sq_string # elements: 1 # length: 13 anova.Metrics # name: # type: sq_string # elements: 1 # length: 178 anova: property Metrics Model fit metrics Table with variables MSE, RMSE, SSE, SSR, SST, RSquared, and AdjustedRSquared summarising the fitted model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 17 Model fit metrics # name: # type: sq_string # elements: 1 # length: 21 anova.NumObservations # name: # type: sq_string # elements: 1 # length: 141 anova: property NumObservations Number of observations Scalar number of response observations used by the model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 19 anova.RandomFactors # name: # type: sq_string # elements: 1 # length: 117 anova: property RandomFactors Random factors Positive integer indices of random factors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 14 Random factors # name: # type: sq_string # elements: 1 # length: 15 anova.Residuals # name: # type: sq_string # elements: 1 # length: 254 anova: property Residuals Model residuals Table with variables Raw (observed minus fitted values) and Pearson (raw residuals scaled by the root mean squared error) when the selected backend exposes residuals, otherwise empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Model residuals # name: # type: sq_string # elements: 1 # length: 18 anova.ResponseName # name: # type: sq_string # elements: 1 # length: 143 anova: property ResponseName Response variable name Character vector used as the response name in formula display. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Response variable name # name: # type: sq_string # elements: 1 # length: 22 anova.SumOfSquaresType # name: # type: sq_string # elements: 1 # length: 163 anova: property SumOfSquaresType Sum-of-squares type String scalar selecting "one", "two", 'three', or 'hierarchical' sums of squares. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Sum-of-squares type # name: # type: sq_string # elements: 1 # length: 7 anova.Y # name: # type: sq_string # elements: 1 # length: 154 anova: property Y Response data Numeric response vector (or matrix, for the one-way column form) used to fit the ANOVA model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Response data # name: # type: sq_string # elements: 1 # length: 11 anova.anova # name: # type: sq_string # elements: 1 # length: 1074 anova: obj = anova (Y) anova: obj = anova (factors, Y) anova: obj = anova (tbl, Y) anova: obj = anova (tbl, responseVarName) anova: obj = anova (tbl, formula) anova: obj = anova (..., name, value) Create an object-oriented analysis of variance model. Y is a non-empty numeric response vector or matrix. factors contains grouping variables for vector responses and may be a grouping vector, a matrix of grouping variables, or a cell array of grouping vectors. If factors is omitted and Y is a matrix, columns of Y are treated as groups following anova1 matrix syntax. tbl is a table whose variables contain factors. The response may be supplied separately, selected by variable name, or specified with a Wilkinson formula. 'FactorNames' can select a subset of table variables when a formula is not supplied. Supported name-value arguments include 'ModelSpecification', 'SumOfSquaresType', 'FactorNames', 'CategoricalFactors', 'RandomFactors', 'ResponseName', 'Alpha', and 'Display'. Passing 'Reps' selects the balanced two-way anova2 backend when Y is a non-vector matrix. # name: # type: sq_string # elements: 1 # length: 53 Create an object-oriented analysis of variance model. # name: # type: sq_string # elements: 1 # length: 14 anova.boxchart # name: # type: sq_string # elements: 1 # length: 282 anova: boxchart (obj) anova: h = boxchart (obj, ...) Plot response values grouped by up to two categorical factors. This method uses boxplot as the graphics backend in Octave and returns the native box graphics handles. A target axes may be supplied as the first optional argument. # name: # type: sq_string # elements: 1 # length: 62 Plot response values grouped by up to two categorical factors. # name: # type: sq_string # elements: 1 # length: 16 anova.groupmeans # name: # type: sq_string # elements: 1 # length: 265 anova: means = groupmeans (obj) anova: means = groupmeans (obj, factors) Return mean response estimates by factor level. The returned value is a table with one row per factor-level combination and columns for the level, mean, standard error, and confidence bounds. # name: # type: sq_string # elements: 1 # length: 47 Return mean response estimates by factor level. # name: # type: sq_string # elements: 1 # length: 17 anova.multcompare # name: # type: sq_string # elements: 1 # length: 333 anova: m = multcompare (obj) anova: m = multcompare (obj, factors) anova: m = multcompare (..., name, value) Perform post-hoc multiple comparisons for a fitted ANOVA object. The returned table contains the compared groups, estimated mean difference, confidence limits, and p-value. The default critical value type is "tukey-kramer". # name: # type: sq_string # elements: 1 # length: 64 Perform post-hoc multiple comparisons for a fitted ANOVA object. # name: # type: sq_string # elements: 1 # length: 21 anova.plotComparisons # name: # type: sq_string # elements: 1 # length: 323 anova: plotComparisons (obj) anova: h = plotComparisons (obj, ...) Plot multiple-comparison intervals for model-adjusted group means. Clicking a group highlights it and distinguishes groups whose adjusted comparison is significant at the requested alpha level. A target axes may be supplied as the first optional argument. # name: # type: sq_string # elements: 1 # length: 66 Plot multiple-comparison intervals for model-adjusted group means. # name: # type: sq_string # elements: 1 # length: 11 anova.stats # name: # type: sq_string # elements: 1 # length: 835 anova: s = stats (obj) anova: s = stats (obj, type) anova: s = stats (obj, "Component", sstype) anova: [s, ems] = stats (...) Return component or summary ANOVA statistics as a table. With no type, or with "component", return statistics for each model term, error, and total. With "summary", group terms into linear, nonlinear, and regression rows. Replicated continuous designs also report lack-of-fit and pure-error statistics. The "Component" form computes the component table using sstype, which must be "one", "two", "three", or "hierarchical". This request does not change the object's read-only SumOfSquaresType property. The second output ems contains expected mean-square information for each model term and the error term. Its variables are Type, ExpectedMeanSquares, MeanSquaresDenominator, DFDenominator, and FDenominator. # name: # type: sq_string # elements: 1 # length: 56 Return component or summary ANOVA statistics as a table. # name: # type: sq_string # elements: 1 # length: 23 anova.varianceComponent # name: # type: sq_string # elements: 1 # length: 144 anova: v = varianceComponent (obj) anova: v = varianceComponent (obj, ...) Return variance component estimates for random model terms and error. # name: # type: sq_string # elements: 1 # length: 69 Return variance component estimates for random model terms and error. # name: # type: sq_string # elements: 1 # length: 6 anova1 # name: # type: sq_string # elements: 1 # length: 4238 statistics: p = anova1 (x) statistics: p = anova1 (x, group) statistics: p = anova1 (x, group, displayopt) statistics: p = anova1 (x, group, displayopt, vartype) statistics: [p, atab] = anova1 (x, ...) statistics: [p, atab, stats] = anova1 (x, ...) Perform a one-way analysis of variance (ANOVA) for comparing the means of two or more groups of data under the null hypothesis that the groups are drawn from distributions with the same mean. For planned contrasts and/or diagnostic plots, use anovan instead. anova1 can take up to three input arguments: x contains the data and it can either be a vector or matrix. If x is a matrix, then each column is treated as a separate group. If x is a vector, it is a single group whichever way it lies, unless group is given; with one group there is nothing to compare, so p is NaN. group contains the names for each group. If x is a matrix, then group can either be a cell array of strings of a character array, with one row per column of x. If you want to omit this argument, enter an empty array ([]). If x is a vector, then group must be a vector of the same length, or a string array or cell array of strings with one row for each element of x. x values corresponding to the same value of group are placed in the same group. displayopt is an optional parameter for displaying the groups contained in the data in a boxplot. If omitted, it is 'on' by default. If group names are defined in group, these are used to identify the groups in the boxplot. Use 'off' to omit displaying this figure. vartype is an optional parameter to used to indicate whether the groups can be assumed to come from populations with equal variance. When vartype is 'equal' the variances are assumed to be equal (this is the default). When vartype is 'unequal' the population variances are not assumed to be equal and Welch's ANOVA test is used instead. vartype is an Octave extension: MATLAB's anova1 takes no fourth argument. It does not error on one either, it accepts it and ignores it, returning the same p and F for 'unequal' as for 'equal'. Code written against this function and then run in MATLAB therefore gets the classic ANOVA silently, with no diagnostic of any kind. Note that anova2's analogous fourth argument does make MATLAB error, so the silence here is particular to anova1. anova1 can return up to three output arguments: p is the p-value of the null hypothesis that all group means are equal. atab is a cell array containing the results in a standard ANOVA table. Its first row holds the column headings. For 'equal' it is the classic partition, 'Source', 'SS', 'df', 'MS', 'F' and 'Prob>F', with a row for the groups, the error and the total. For 'unequal' it is 'Source', 'F', 'df', 'dfe' and 'Prob>F', with one row for the groups, since Welch's test partitions no sum of squares and pools no error term; 'df' and 'dfe' are the numerator and the denominator degrees of freedom of its F statistic, the latter fractional. The two tables therefore differ in width and in what their columns mean. stats is a structure containing statistics useful for performing a multiple comparison of means with the MULTCOMPARE function. A categorical group may declare levels that no observation uses. Such a level takes no part in the analysis and is dropped from every field of stats, so n, means and gnames always describe the same groups, in the same order, and can be indexed together. This is a deliberate deviation from MATLAB, which drops an unused level from gnames but keeps it in n and means as a count of zero and a mean of NaN. Those fields then disagree in length and the group indices run past gnames, so MATLAB's own multcompare reports comparisons against a group holding no observations and labels them with indices that its gnames cannot resolve. If anova1 is called without any output arguments, then it prints the results in a one-way ANOVA table to the standard output. It is also printed when displayopt is 'on'. Examples: x = meshgrid (1:6); x = x + normrnd (0, 1, 6, 6); anova1 (x, [], 'off'); [p, atab] = anova1(x); x = ones (50, 4) .* [-2, 0, 1, 5]; x = x + normrnd (0, 2, 50, 4); groups = {"A", "B", "C", "D"}; anova1 (x, groups); See also: anova2, anovan, multcompare # name: # type: sq_string # elements: 1 # length: 191 Perform a one-way analysis of variance (ANOVA) for comparing the means of two or more groups of data under the null hypothesis that the groups are drawn from distributions with the same mean. # name: # type: sq_string # elements: 1 # length: 6 anova2 # name: # type: sq_string # elements: 1 # length: 2212 statistics: p = anova2 (x, reps) statistics: p = anova2 (x, reps, displayopt) statistics: p = anova2 (x, reps, displayopt, model) statistics: [p, atab] = anova2 (...) statistics: [p, atab, stats] = anova2 (...) Performs two-way factorial (crossed) or a nested analysis of variance (ANOVA) for balanced designs. For unbalanced factorial designs, diagnostic plots and/or planned contrasts, use anovan instead. anova2 requires two input arguments with an optional third and fourth: x contains the data and it must be a matrix of at least two columns and two rows. NaN values are not accepted, since anova2 requires a balanced design; use anovan for data with missing observations. An empty x is an error, whereas MATLAB returns NaN p-values. reps is the number of replicates for each combination of factor groups. displayopt is an optional parameter for displaying the ANOVA table, when it is 'on' (default) and suppressing the display when it is 'off'. model is an optional parameter to specify the model type as either: "interaction" or "full" (default): compute both main effects and their interaction "linear": compute both main effects without an interaction. When reps > 1 the test is suitable for a balanced randomized block design. When reps == 1, the test becomes a One-way Repeated Measures (RM)-ANOVA with Greenhouse-Geisser correction to the column factor degrees of freedom to make the test robust to violations of sphericity "nested": treat the row factor as nested within columns. Note that the row factor is considered a random factor in the calculation of the statistics. anova2 returns up to three output arguments: p is the p-value of the null hypothesis that all group means are equal. atab is a cell array containing the results in a standard ANOVA table. stats is a structure containing statistics useful for performing a multiple comparison of means with the MULTCOMPARE function. If anova2 is called without any output arguments, then it prints the results in a one-way ANOVA table to the standard output as if displayopt is 'on'. Examples: load popcorn; anova2 (popcorn, 3); [p, anovatab, stats] = anova2 (popcorn, 3, "off"); disp (p); See also: anova1, anovan, multcompare # name: # type: sq_string # elements: 1 # length: 99 Performs two-way factorial (crossed) or a nested analysis of variance (ANOVA) for balanced designs. # name: # type: sq_string # elements: 1 # length: 6 anovan # name: # type: sq_string # elements: 1 # length: 10358 statistics: p = anovan (Y, GROUP) statistics: p = anovan (Y, GROUP, name, value) statistics: [p, atab] = anovan (...) statistics: [p, atab, stats] = anovan (...) statistics: [p, atab, stats, terms] = anovan (...) Perform a multi (N)-way analysis of (co)variance (ANOVA or ANCOVA) to evaluate the effect of one or more categorical or continuous predictors (i.e. independent variables) on a continuous outcome (i.e. dependent variable). The algorithms used make anovan suitable for balanced or unbalanced factorial (crossed) designs. By default, anovan treats all factors as fixed. Examples of function usage can be found by entering the command demo anovan. A bootstrap resampling variant of this function, bootlm, is available in the statistics-resampling package and has similar usage. Data is a single vector Y with groups specified by a corresponding matrix or cell array of group labels GROUP, where each column of GROUP has the same number of rows as Y. For example, if Y = [23; 27; 31; 29; 30; 32]; GROUP = [1, 2; 1, 3; 1, 2; 2, 3; 2, 3; 3, 2]; then observation 23 was measured under conditions 1,2; observation 27 was measured under conditions 1,3; and so on. If the GROUP provided is empty, then the linear model is fit with just the intercept (no predictors). anovan can take a number of optional parameters as name-value pairs. [...] = anovan (Y, GROUP, "continuous", continuous) continuous is a vector of indices indicating which of the columns (i.e. factors) in GROUP should be treated as continuous predictors rather than as categorical predictors. The relationship between continuous predictors and the outcome should be linear. [...] = anovan (Y, GROUP, "random", random) random is a vector of indices indicating which of the columns (i.e. factors) in GROUP should be treated as random effects rather than fixed effects. In the table anovan prints, the name of a random factor is followed by a ' so that it can be told apart at a glance. That mark is a convention of the printed output only: the names returned in atab, in stats.varnames, and inside the expected-mean-square and denominator expressions carry no mark, so that they read as MATLAB's do. Which terms are random is reported in the "Type" column of atab instead. Every interaction involving a random factor stays in the model, and each F ratio is taken against the denominator its expected mean square calls for rather than against the mean squared error. The expected mean square of a term names the variance each component contributes to it, written Q(...) for a fixed term and V(...) for a random one; the denominator is the combination of mean squares whose expectation matches the term's with the term itself removed. Often that is a single mean square, in which case the F ratio carries its degrees of freedom. When no single mean square will do, one is synthesised from several and carries Satterthwaite degrees of freedom, which are generally not whole numbers. The variance component of every random term is estimated from the same system, and reported with confidence bounds. A component estimated as negative has no interval, as it lies outside the parameter space. [...] = anovan (Y, GROUP, "model", modeltype) modeltype can specified as one of the following: "linear" (default) : compute N main effects with no interactions. "interaction" : compute N effects and Nx(N-1) two-factor interactions "full" : compute the N main effects and interactions at all levels a scalar integer : representing the maximum interaction order a matrix of term definitions : each row is a term and each column is a factor. Entries are nonnegative integer exponents. Exponents greater than one are valid only for factors selected by "continuous". -- Example: A two-way ANOVA with interaction would be: [1 0; 0 1; 1 1] [...] = anovan (Y, GROUP, "nested", nested) nested is an N-by-N logical matrix, where N is the number of factors. A true entry nested(i,j) specifies that factor i is nested in factor j. A factor may be nested in more than one parent. Nested factors must be categorical and use the default contrasts. [...] = anovan (Y, GROUP, "sstype", sstype) sstype can specified as one of the following: 1 : Type I sequential sums-of-squares. 2 : Type II partially sequential sums-of-squares. Each term is adjusted for every other term that does not contain it. "h" : hierarchical sums-of-squares. Each term is adjusted only for the terms below it in the hierarchy, so for a model whose terms are all of first order it agrees with Type II, while a polynomial term is adjusted for its lower powers but not for its higher ones. 3 (default) : Type III partial, constrained or marginal sums-of-squares [...] = anovan (Y, GROUP, "varnames", varnames) varnames must be a cell array of strings with each element containing a factor name for each column of GROUP, or a character matrix with one name per row. By default (if not parsed as optional argument), varnames are "X1","X2","X3", etc. stats.varnames holds them as a column. [...] = anovan (Y, GROUP, "alpha", alpha) alpha must be a scalar value between 0 and 1 requesting 100x(1-alpha)% confidence bounds for the regression coefficients returned in stats.coeffs (default 0.05 for 95% confidence). [...] = anovan (Y, GROUP, "display", dispopt) dispopt can be either "on" (default) or "off" and controls the display of the model formula, table of model parameters, the ANOVA table and the diagnostic plots. The F-statistic and p-values are formatted in APA-style. To avoid p-hacking, the table of model parameters is only displayed if we set planned contrasts (see below). [...] = anovan (Y, GROUP, "contrasts", contrasts) contrasts can be specified as one of the following: A string corresponding to one of the built-in contrasts listed below: "simple" or "anova" (default): Simple (ANOVA) contrast coding. (The first level appearing in the GROUP column is the reference level) "poly": Polynomial contrast coding for trend analysis. "helmert": Helmert contrast coding: the difference between each level with the mean of the subsequent levels. "effect": Deviation effect coding. (The first level appearing in the GROUP column is omitted). "sdif" or "sdiff": Successive differences contrast coding: the difference between each level with the previous level. "treatment": Treatment contrast (or dummy) coding. (The first level appearing in the GROUP column is the reference level). These contrasts are not compatible with sstype = 3. A matrix containing a custom contrast coding scheme (i.e. the generalized inverse of contrast weights). Rows in the contrast matrices correspond to factor levels in the order that they first appear in the GROUP column. The matrix must contain the same number of columns as there are the number of factor levels minus one. If the anovan model contains more than one factor and a built-in contrast coding scheme was specified, then those contrasts are applied to all factors. To specify different contrasts for different factors in the model, contrasts should be a cell array with the same number of cells as there are columns in GROUP. Each cell should define contrasts for the respective column in GROUP by one of the methods described above. If cells are left empty, then the default contrasts are applied. Contrasts for cells corresponding to continuous factors are ignored. [...] = anovan (Y, GROUP, "weights", weights) weights is an optional vector of weights to be used when fitting the linear model. Weighted least squares (WLS) is used with weights (that is, minimizing sum (weights * residuals .^ 2)); otherwise ordinary least squares (OLS) is used (default is empty for OLS). anovan can return up to four output arguments: p = anovan (...) returns a vector of p-values, one for each term. [p, atab] = anovan (...) returns a cell array containing the ANOVA table. Its first row holds the column names, and the columns are, in order, the term name, its sum-of-squares, its degrees of freedom, a singularity flag, its mean square, the F statistic, the p-value, and then two effect sizes: eta squared and partial eta squared. The first seven follow MATLAB's layout, so a caller reading them by position gets the same quantity in either language; the two effect sizes are an Octave extension and are appended after them. A model naming any factor as random reports eight further columns after the p-value, as MATLAB does, with the two effect sizes still last: the term's type, its expected mean square, the mean square and degrees of freedom of the denominator its F ratio was taken against, the definition of that denominator, and the variance component of a random term with its confidence bounds. A denominator that no single mean square provides is synthesised from several and carries Satterthwaite degrees of freedom, which are generally not whole numbers. The singularity flag is 1 when a term is aliased with the rest of the model, which happens when the design is not of full rank, most often because a combination of factor levels holds no observations. The degrees of freedom reported for such a term are the ones that can be estimated, which may be fewer than the term's design block has columns and may be zero. Whether a term is aliased depends on the model it is adjusted for, and therefore on "sstype": a term that is estimable in a sequential fit may not be estimable in a marginal one. A flagged term's sum-of-squares is not uniquely attributable to it, so the corresponding F and p-value should not be read as a test of that term. [p, atab, stats] = anovan (...) returns a structure containing additional statistics, including degrees of freedom and effect sizes for each term in the linear model, the design matrix, the variance-covariance matrix, (weighted) model residuals, and the mean squared error. The columns of stats.coeffs (from left-to-right) report the model coefficients, standard errors, lower and upper 100x(1-alpha)% confidence interval bounds, t-statistics, and p-values relating to the contrasts. The number appended to each term name in stats.coeffnames corresponds to the column number in the relevant contrast matrix for that factor. The stats structure can be used as input for multcompare. [p, atab, stats, terms] = anovan (...) returns the model term definitions. See also: anova1, anova2, multcompare, fitlm # name: # type: sq_string # elements: 1 # length: 147 Perform a multi (N)-way analysis of (co)variance (ANOVA or ANCOVA) to evaluate the effect of one or more categorical or continuous predictors (i.e. # name: # type: sq_string # elements: 1 # length: 13 ansaribradley # name: # type: sq_string # elements: 1 # length: 2356 statistics: h = ansaribradley (x, y) statistics: h = ansaribradley (x, y, name, value) statistics: [h, p] = ansaribradley (...) statistics: [h, p, stats] = ansaribradley (...) Ansari-Bradley two-sample test for equal dispersions. h = ansaribradley (x, y) performs an Ansari-Bradley test of the hypothesis that the two independent samples in the vectors x and y come from distributions with the same dispersion parameter, against the alternative that they come from distributions with different dispersions. The result is h = 0 if the null hypothesis of equal dispersions cannot be rejected at the 5% significance level, or h = 1 if it can. The Ansari-Bradley test is a nonparametric alternative to the two-sample F test (vartest2) that does not assume normality. It assumes that the two samples are independent and that they come from distributions with the same median and shape, differing (under the alternative) only in dispersion. If the medians differ, the data should be recentred (e.g. by subtracting the sample medians) before applying the test. ansaribradley treats NaNs in x or y as missing values and ignores them. [h, p] = ansaribradley (...) returns the p-value of the test, that is the probability, under the null hypothesis, of observing a value of the test statistic as or more extreme than the one observed. [h, p, stats] = ansaribradley (...) returns a structure with the following fields: W the value of the Ansari-Bradley test statistic, the sum of the Ansari-Bradley scores of the sample x Wstar the value of the approximate normal (z) statistic [...] = ansaribradley (..., name, value) specifies one or more of the following name/value pairs: Name Value 'alpha' the significance level. Default is 0.05. 'tail' a string specifying the alternative hypothesis 'method' a string selecting the p-value computation, either 'exact' to use the exact permutation distribution of the statistic, or 'approximate' to use the normal approximation. The default is 'exact' when the total sample size is 25 or less, and 'approximate' otherwise. The 'tail' option can take one of the following values: 'both' dispersions are not equal (two-tailed, default) 'right' dispersion of x is greater than dispersion of y (right-tailed) 'left' dispersion of x is less than dispersion of y (left-tailed) See also: vartest2, vartestn, kstest2, ranksum # name: # type: sq_string # elements: 1 # length: 53 Ansari-Bradley two-sample test for equal dispersions. # name: # type: sq_string # elements: 1 # length: 8 barttest # name: # type: sq_string # elements: 1 # length: 992 statistics: ndim = barttest (x) statistics: ndim = barttest (x, alpha) statistics: [ndim, pval] = barttest (x, alpha) statistics: [ndim, pval, chisq] = barttest (x, alpha) Bartlett's test of sphericity for correlation. It compares an observed correlation matrix to the identity matrix in order to check if there is a certain redundancy between the variables that we can summarize with a few number of factors. A statistically significant test shows that the variables (columns) in x are correlated, thus it makes sense to perform some dimensionality reduction of the data in x. ndim = barttest (x, alpha) returns the number of dimensions necessary to explain the nonrandom variation in the data matrix x at the alpha significance level. alpha is an optional input argument and, when not provided, it is 0.05 by default. [ndim, pval, chisq] = barttest (...) also returns the significance values pval for the hypothesis test for each dimension as well as the associated chi^2 values in chisq # name: # type: sq_string # elements: 1 # length: 46 Bartlett's test of sphericity for correlation. # name: # type: sq_string # elements: 1 # length: 8 binotest # name: # type: sq_string # elements: 1 # length: 1519 statistics: [h, pval, ci, stats] = binotest (pos, N, p0) statistics: [h, pval, ci, stats] = binotest (pos, N, p0, Name, Value) Test for probability p of a binomial sample Perform a test of the null hypothesis p == p0 for a sample of size N with pos positive results. Name-Value pair arguments can be used to set various options. 'alpha' can be used to specify the significance level of the test (the default value is 0.05). The option 'tail', can be used to select the desired alternative hypotheses. If the value is 'both' (default) the null is tested against the two-sided alternative p != p0. The value of pval is determined by adding the probabilities of all events less or equally likely than the observed number pos of positive events, equally likely to within a relative 1e-7, as R's binom.test compares them. If the value of 'tail' is 'right' the one-sided alternative p > p0 is considered. Similarly for 'left', the one-sided alternative p < p0 is considered. If h is 0 the null hypothesis is accepted, if it is 1 the null hypothesis is rejected. The p-value of the test is returned in pval. A 100(1-alpha)% Clopper-Pearson confidence interval for p is returned in ci, one-sided for a one-sided test. stats is a structure with the following fields: phat the estimated probability, pos / N. CohensH Cohen's h, the difference between the arcsine transforms 2 asin (sqrt (phat)) - 2 asin (sqrt (p0)). CohensHCI its confidence interval, ci under the same transform, which is monotone and so keeps its coverage. # name: # type: sq_string # elements: 1 # length: 43 Test for probability p of a binomial sample # name: # type: sq_string # elements: 1 # length: 7 chi2gof # name: # type: sq_string # elements: 1 # length: 3640 statistics: h = chi2gof (x) statistics: [h, p] = chi2gof (x) statistics: [p, h, stats] = chi2gof (x) statistics: [...] = chi2gof (x, Name, Value, ...) Chi-square goodness-of-fit test. chi2gof performs a chi-square goodness-of-fit test for discrete or continuous distributions. The test is performed by grouping the data into bins, calculating the observed and expected counts for those bins, and computing the chi-square test statistic $$ \chi ^ 2 = \sum_{i=1}^N \left (O_i - E_i \right) ^ 2 / E_i $$ where O is the observed counts and E is the expected counts. This test statistic has an approximate chi-square distribution when the counts are sufficiently large. Bins in either tail with an expected count less than 5 are pooled with neighboring bins until the count in each extreme bin is at least 5. If bins remain in the interior with counts less than 5, chi2gof displays a warning. In that case, you should use fewer bins, or provide bin centers or binedges, to increase the expected counts in all bins. h = chi2gof (x) performs a chi-square goodness-of-fit test that the data in the vector X are a random sample from a normal distribution with mean and variance estimated from x. The result is h = 0 if the null hypothesis (that x is a random sample from a normal distribution) cannot be rejected at the 5% significance level, or h = 1 if the null hypothesis can be rejected at the 5% level. chi2gof uses by default 10 bins ('nbins'), and compares the test statistic to a chi-square distribution with nbins - 3 degrees of freedom, to take into account that two parameters were estimated. [h, p] = chi2gof (x) also returns the p-value p, which is the probability of observing the given result, or one more extreme, by chance if the null hypothesis is true. If there are not enough degrees of freedom to carry out the test, p is NaN. [h, p, stats] = chi2gof (x) also returns a stats structure with the following fields: "chi2stat" Chi-square statistic "df" Degrees of freedom "binedges" Vector of bin binedges after pooling "O" Observed count in each bin "E" Expected count in each bin [...] = chi2gof (x, Name, Value, ...) specifies optional Name/Value pair arguments chosen from the following list. Name Value 'nbins' The number of bins to use. Default is 10. 'binctrs' A vector of bin centers. 'binedges' A vector of bin binedges. 'cdf' A fully specified cumulative distribution function or a function handle provided in a cell array whose first element is a function handle, and all later elements are its parameter values. The function must take x values as its first argument, and other parameters as later arguments. 'expected' A vector with one element per bin specifying the expected counts for each bin. 'nparams' The number of estimated parameters; used to adjust the degrees of freedom to be nbins - 1 - nparams, where nbins is the number of bins. 'emin' The minimum allowed expected value for a bin; any bin in either tail having an expected value less than this amount is pooled with a neighboring bin. Use the value 0 to prevent pooling. Default is 5. 'frequency' A vector of the same length as x containing the frequency of the corresponding x values. 'alpha' An alpha value such that the hypothesis is rejected if p < alpha. Default is alpha = 0.05. You should specify either 'cdf' or 'expected' parameters, but not both. If your 'cdf' input contains extra parameters, these are accounted for automatically and there is no need to specify 'nparams'. If your 'expected' input depends on estimated parameters, you should use the 'nparams' parameter to ensure that the degrees of freedom for the test is correct. # name: # type: sq_string # elements: 1 # length: 32 Chi-square goodness-of-fit test. # name: # type: sq_string # elements: 1 # length: 8 chi2test # name: # type: sq_string # elements: 1 # length: 4765 statistics: h = chi2test (x) statistics: [h, p] = chi2test (x) statistics: [h, p, stats] = chi2test (x) statistics: [...] = chi2test (x, name, value) statistics: [...] = chi2test (..., 'Alpha', alpha) Perform a chi-squared test (for independence or homogeneity). For 2-way contingency tables, chi2test performs a chi-squared test for independence or homogeneity, according to the sampling scheme and related question. Independence means that the two variables forming the 2-way table are not associated, hence you cannot predict from one another. Homogeneity refers to the concept of similarity, hence they all come from the same distribution. Both tests are computationally identical and will produce the same result. Nevertheless, they answer to different questions. Consider two variables, one for gender and another for smoking. To test independence (whether gender and smoking is associated), we would randomly sample from the general population and break them down into categories in the table. To test homogeneity (whether men and women share the same smoking habits), we would sample individuals from within each gender, and then measure their smoking habits (e.g. smokers vs non-smokers). When chi2test is called without any output arguments, it will print the result in the terminal including p-value, chi^2 statistic, and degrees of freedom. Otherwise it returns h, 1 if the null hypothesis is rejected at the significance level alpha and 0 otherwise, the p-value p, and a structure stats with the following fields: chi2stat the chi^2 statistic of the test. df its degrees of freedom. O the table tested: x, or for "marginal" the two-way table left after collapsing x. E the expected values under the tested model, in the layout of O. CohensW Cohen's w, the effect size of the test. CohensWCI its confidence interval. CramersV Cramer's V, for a two-way table and for the "joint" and "marginal" models, which test one. CramersVCI its confidence interval. Both effect sizes are corrected for bias. With n observations, w = sqrt (max (chi^2 - df, 0) / n), and V = w / sqrt (k) with k one less than the smaller dimension of the two-way table; for "joint" that table has the variable it names as rows and every combination of the other two as columns. The uncorrected sqrt (chi^2 / n) is biased upwards, by df / n in its square. The confidence intervals invert the noncentral chi^2 distribution: the bounds lambda of the noncentrality consistent with the observed chi^2 give sqrt (lambda / n) for w, divided by sqrt (k) for V, whose upper bound is at most 1. 'Alpha' sets the significance level alpha of h and the level 100 (1 - alpha)% of both confidence intervals. It is 0.05 by default. Unlike MATLAB, in GNU Octave chi2test also supports 3-way tables, which involve three categorical variables (each in a different dimension of x. In its simplest form, [...] = chi2test (x) will test for mutual independence among the three variables. Alternatively, when called in the form [...] = chi2test (x, name, value), it can perform the following tests: name value Description "mutual" [] Mutual independence. All variables are independent from each other, (A, B, C). It takes no value; an empty matrix is accepted. "joint" scalar Joint independence. Two variables are jointly independent of the third, (AB, C). The scalar value corresponds to the dimension of the independent variable (i.e. 3 for C). "marginal" scalar Marginal independence. Two variables are independent if you ignore the third, (A, C). The scalar value corresponds to the dimension of the variable to be ignored (i.e. 2 for B); the table is collapsed over it and independence is tested in the two-way table that remains. "conditional" scalar Conditional independence. Two variables are independent given the third, (AC, BC). The scalar value corresponds to the dimension of the variable that forms the conditional dependence (i.e. 3 for C). "homogeneous" [] Homogeneous associations. Conditional (partial) odds-ratios are not related on the value of the third, (AB, AC, BC). It takes no value; an empty matrix is accepted. When testing for homogeneous associations in 3-way tables, the iterative proportional fitting procedure is used. For small samples it is better to use the Cochran-Mantel-Haenszel Test. K-way tables for k > 3 are supported only for testing mutual independence. Similar to 2-way tables, no optional parameters are required for k > 3 multi-way tables. chi2test produces a warning if any cell of a 2x2 table has an expected frequency less than 5 or if more than 20% of the cells in larger 2-way tables have expected frequencies less than 5 or any cell with expected frequency less than 1. In such cases, use fishertest. See also: crosstab, fishertest, mcnemar_test # name: # type: sq_string # elements: 1 # length: 61 Perform a chi-squared test (for independence or homogeneity). # name: # type: sq_string # elements: 1 # length: 16 correlation_test # name: # type: sq_string # elements: 1 # length: 2293 statistics: h = correlation_test (x, y) statistics: [h, p] = correlation_test (y, x) statistics: [h, p, stats] = correlation_test (y, x) statistics: [...] = correlation_test (y, x, Name, Value) Perform a correlation coefficient test to determine whether two samples x and y come from uncorrelated populations. h = correlation_test (y, x) tests the null hypothesis that the two samples x and y come from uncorrelated populations. The result is h = 0 if the null hypothesis cannot be rejected at the significance level alpha, or h = 1 if it can. y and x must be vectors of equal length with finite real numbers. The p-value of the test is returned in p. stats is a structure with the following fields: Field Value method the type of correlation coefficient used for the test tstat the t statistic, for Pearson's and Spearman's coefficients df its degrees of freedom, n - 2 zval the z statistic, for Kendall's coefficient dist the respective distribution for the test alt the alternative hypothesis for the test CorrCoef the correlation coefficient, the effect size CorrCoefCI its 100 (1 - alpha)% confidence interval, one-sided for a one-sided test Pearson's and Spearman's coefficients are tested with t = r sqrt ((n - 2) / (1 - r^2)) on n - 2 degrees of freedom, as R's cor.test does without an exact test; Kendall's with its normal approximation, whose variance allows for ties. The confidence interval is Fisher's z transform of the coefficient, with standard error 1 / sqrt (n - 3) for Pearson's, sqrt ((1 + r^2 / 2) / (n - 3)) for Spearman's and sqrt (0.437 / (n - 4)) for Kendall's, after Bonett and Wright (2000). [...] = correlation_test (..., name, value) specifies one or more of the following name/value pairs: Name Value 'alpha' the significance level of h and the level of the confidence interval. Default is 0.05. 'tail' a string specifying the alternative hypothesis 'both' corrcoef is not 0 (two-tailed, default) 'left' corrcoef is less than 0 (left-tailed) 'right' corrcoef is greater than 0 (right-tailed) 'method' a string specifying the correlation coefficient used for the test 'pearson' Pearson's product moment correlation (Default) 'kendall' Kendall's rank correlation tau 'spearman' Spearman's rank correlation rho See also: regression_ftest, regression_ttest # name: # type: sq_string # elements: 1 # length: 115 Perform a correlation coefficient test to determine whether two samples x and y come from uncorrelated populations. # name: # type: sq_string # elements: 1 # length: 11 detectdrift # name: # type: sq_string # elements: 1 # length: 4460 statistics: DDiagnostics = detectdrift (Baseline, Target) statistics: DDiagnostics = detectdrift (Baseline, Target, Name, Value) Detect drift between two data sets, one variable at a time. DDiagnostics = detectdrift (Baseline, Target) measures, for each variable, how far the distribution in Target has moved from the one in Baseline, tests the change by permutation, and returns the results as a stats.drift.DriftDiagnostics object. Baseline and Target are both numeric matrices with the same number of columns, both categorical arrays, or both tables, read over the variables they share, which must be all the variables of one of them. An observation holding a missing value in a variable used is left out. Each variable is measured by a metric: a continuous one by 'ContinuousMetric', one holding levels by 'CategoricalMetric'. With F and G the empirical distribution functions of the two samples and H that of the two pooled, the metrics are: Metric Definition 'wasserstein' The area between F and G, the default for a continuous variable. 'ks' The largest difference between F and G. 'ad' The Anderson-Darling statistic, the sum over the distinct pooled values but the largest of (F - G)^2 / (H (1 - H)), divided by the number of pooled observations. 'energy' The energy distance, sqrt (2 E|x - y| - E|x - x'| - E|y - y'|). 'hellinger' sqrt (1 - sum (sqrt (p q))), the default for a variable holding levels. 'bhattacharyya' -log (sum (sqrt (p q))). 'tv' The total variation distance, sum (abs (p - q)) / 2. 'psi' The population stability index, sum ((p - q) log (p / q)). 'chi2' sum ((p - q)^2 / p). For a variable holding levels, p and q are the shares of each level in Baseline and Target, over the levels either holds, each count increased by 0.5 first so that a level one sample lacks leaves every metric finite. The p-value of a variable is the share of permutations whose metric is at least the observed one, the observed arrangement counted as the first permutation, so it is never below one over their number. Its 95% confidence interval is the Clopper-Pearson interval. The drift status is "Drift" where the interval lies below 'DriftThreshold', "Stable" where it lies above 'WarningThreshold', and "Warning" otherwise. Permutations are drawn in stages, 1000 or 'MaxNumPermutations' if fewer, then four times as many at each stage, until the interval lies within a single one of the three regions or the next stage would pass 'MaxNumPermutations'. They are drawn with randperm, so the state of rand decides them. The drift status of the data as a whole comes from the p-values corrected for testing every variable: by Bonferroni, the smallest p-value times the number of variables, or by the false discovery rate, the smallest Benjamini-Hochberg adjusted p-value. It is "Drift" below 'DriftThreshold', "Warning" below 'WarningThreshold', and "Stable" otherwise. DDiagnostics = detectdrift (..., Name, Value) takes the following options. Name Value 'VariableNames' The variables to use, among those the two tables share. All of them by default. 'CategoricalVariables' The variables holding levels: 'all', their indices, a logical vector over the variables, or, for tables, their names. A table variable holding logical values, an unordered categorical array, a string array or a cell array of character vectors, and every column of a categorical array, holds levels whether named here or not. 'ContinuousMetric' 'wasserstein', the default, 'ks', 'ad' or 'energy'. 'CategoricalMetric' 'hellinger', the default, 'bhattacharyya', 'tv', 'psi' or 'chi2'. 'DriftThreshold' 0.05 by default. 'WarningThreshold' 0.1 by default, and above 'DriftThreshold'. 'MultipleTestCorrection' 'bonferroni', the default, or 'fdr'. 'MaxNumPermutations' The most permutations drawn for a variable, 1000 by default. 'EstimatePValues' true by default; with false only the metrics are computed. 'Options' A structure as statset returns. Parallel computing and random streams are not implemented, and are refused where asked for. MATLAB draws at least 1000 permutations whatever 'MaxNumPermutations' says, and finishes a stage that passes it, so that a maximum of 3000 can end at 4001; here no variable is given more permutations than 'MaxNumPermutations'. MATLAB's stages are otherwise its own, so the number of permutations and the p-values differ from MATLAB's, as they would between any two random draws. See also: stats.drift.DriftDiagnostics, knntest, mmdtest # name: # type: sq_string # elements: 1 # length: 59 Detect drift between two data sets, one variable at a time. # name: # type: sq_string # elements: 1 # length: 6 dwtest # name: # type: sq_string # elements: 1 # length: 2115 statistics: p = dwtest (r, x) statistics: p = dwtest (r, x, name, value) statistics: [p, d] = dwtest (...) Durbin-Watson test for autocorrelation in linear regression residuals. p = dwtest (r, x) performs the Durbin-Watson test on the residuals r of a linear regression with design matrix x (which should include a column of ones if the model has a constant term). The null hypothesis is that the residuals are uncorrelated, against the alternative that they are autocorrelated. r is an Nx1 vector and x is an NxP matrix. p is the p-value of the test. The Durbin-Watson statistic is $$ d = \sum_{i=1}^{n-1} (r_{i+1} - r_i)^2 / \sum_{i=1}^{n} r_i^2. $$ Values near 2 indicate no autocorrelation, values towards 0 positive autocorrelation, and values towards 4 negative autocorrelation. p = dwtest (r, x, name, value) specifies additional options using Name-Value pair arguments: Name Value 'Method' 'exact' to compute the exact p-value from the null distribution of the statistic (a ratio of quadratic forms, evaluated with Imhof's method), or 'approximate' to use a normal approximation based on the mean and variance of the statistic. The default is 'exact' for n < 400 and 'approximate' otherwise. 'Tail' The alternative hypothesis: 'both' (default) for a nonzero autocorrelation, 'right' for a positive autocorrelation, or 'left' for a negative autocorrelation. [p, d] = dwtest (...) also returns the Durbin-Watson statistic d. If x has rank equal to the number of observations, no residual degrees of freedom remain and d has no null distribution. In that case p is 0 for 'right' and 'both' and 1 for 'left' with the exact method, and NaN with the approximate method. A p of exactly 0 or 1 signals this rather than strong evidence. If r is empty, p is 0 and d is NaN. MATLAB returns d = 0 for some empty inputs, a 0x3 array among them, and NaN for [] and for zero-length vectors; d is NaN here for every empty r, the statistic being undefined in all of them alike. MATLAB also accepts a character array as r and returns the empty result for it; r must be numeric here. See also: regress, fitlm, runstest # name: # type: sq_string # elements: 1 # length: 70 Durbin-Watson test for autocorrelation in linear regression residuals. # name: # type: sq_string # elements: 1 # length: 10 fishertest # name: # type: sq_string # elements: 1 # length: 1996 statistics: h = fishertest (x) statistics: h = fishertest (x, param1, value1, ...) statistics: [h, pval] = fishertest (...) statistics: [h, pval, stats] = fishertest (...) Fisher's exact test. h = fishertest (x) performs Fisher's exact test on a 2x2 contingency table given in matrix x. This is a test of the hypothesis that there are no non-random associations between the two 2-level categorical variables in x. fishertest returns the result of the tested hypothesis in h. h = 0 indicates that the null hypothesis (of no association) cannot be rejected at the 5% significance level. h = 1 indicates that the null hypothesis can be rejected at the 5% level. x must contain only non-negative integers. Use the crosstab function to generate the contingency table from samples of two categorical variables. Fisher's exact test is not suitable when all integers in x are very large. User can use the Chi-square test in this case. [h, pval] = fishertest (x) returns the p-value in pval. That is the probability of observing the given result, or one more extreme, by chance if the null hypothesis is true. Small values of pval cast doubt on the validity of the null hypothesis. [p, pval, stats] = fishertest (...) returns the structure stats with the following fields: OddsRatio -- the odds ratio ConfidenceInterval -- the asymptotic confidence interval for the odds ratio. If any of the four entries in the contingency table x is zero, the confidence interval will not be computed, and [-Inf Inf] will be displayed. [...] = fishertest (..., name, value, ...) specifies one or more of the following name/value pairs: Name Value 'alpha' the significance level. Default is 0.05. 'tail' a string specifying the alternative hypothesis 'both' odds ratio not equal to 1, indicating association between two variables (two-tailed test, default) 'left' odds ratio greater than 1 (right-tailed test) 'right' odds ratio is less than 1 (left-tailed test) See also: crosstab, chi2test, mcnemar_test, ztest2 # name: # type: sq_string # elements: 1 # length: 20 Fisher's exact test. # name: # type: sq_string # elements: 1 # length: 8 friedman # name: # type: sq_string # elements: 1 # length: 3370 statistics: p = friedman (x) statistics: p = friedman (x, reps) statistics: p = friedman (x, reps, displayopt) statistics: p = friedman (x, reps, displayopt, Name, Value) statistics: [p, tbl] = friedman (...) statistics: [p, tbl, stats] = friedman (...) Performs the nonparametric Friedman's test to compare column effects in a two-way layout. friedman tests the null hypothesis that the column effects are all the same against the alternative that they are not all the same. friedman requires one up to three input arguments: x contains the data and it must be a matrix of at least two columns and two rows. reps is the number of replicates for each combination of factor groups. If not provided, no replicates are assumed. displayopt is an optional parameter for displaying the Friedman's ANOVA table, when it is 'on' (default) and suppressing the display when it is 'off'. MATLAB renders the table in a figure window; this package prints it to the standard output, as anova2 does. friedman returns up to three output arguments: p is the p-value of the null hypothesis that all group means are equal. tbl is a cell array containing the results of the Friedman's test in ANOVA table format. Its first row holds the column labels Source, SS, df, MS, Chi-sq and Prob>Chi-sq, followed by a row per source: Columns, [Interaction], Error and Total. An entry that does not apply to a row, such as the chi-square statistic of the Error row, is empty. stats is a structure containing statistics useful for performing a multiple comparison of medians with the MULTCOMPARE function. stats also holds the effect size of the test, an Octave extension: KendallsW, Kendall's coefficient of concordance, and KendallsWCI, its two-sided confidence interval at the level 100 (1 - alpha) percent. With b blocks of r replicates over c columns, W = Q / Q_max, where Q is the test statistic, corrected for ties, and Q_max = b r^2 (c^2 - 1) / (r c + 1) the value it takes when every block ranks the columns in the same order; without replicates this is the usual W = Q / (b (c - 1)). By default the interval inverts the noncentral chi-square distribution of Q on c - 1 degrees of freedom, taken as Q's distribution under the alternative, which it is for many blocks, and bounds the concordance beyond chance, lambda / Q_max for the noncentrality lambda, which is 0 where the columns do not differ. W itself includes the agreement expected by chance, averaging (c - 1) / Q_max, which is 1/b without replicates, where the columns do not differ; the estimate of the concordance beyond chance that the interval matches is max (W - (c - 1) / Q_max, 0). The bootstrap interval bounds W itself. Three options after displayopt, also an Octave extension, set the interval: Name Value 'Alpha' The significance level of the interval, a scalar between 0 and 1, 0.05 by default. 'ConfidenceIntervalType' 'exact', the default, or 'bootstrap' for the bias-corrected and accelerated bootstrap interval, resampling the blocks. 'NumBootstraps' The number of bootstrap replicates, a positive integer, 1000 by default. If friedman is called without any output arguments, then it prints the results in a Friedman's ANOVA table to the standard output. Examples: load popcorn; friedman (popcorn, 3); [p, anovatab, stats] = friedman (popcorn, 3); disp (p); See also: anova2, kruskalwallis, multcompare # name: # type: sq_string # elements: 1 # length: 89 Performs the nonparametric Friedman's test to compare column effects in a two-way layout. # name: # type: sq_string # elements: 1 # length: 16 hotelling_t2test # name: # type: sq_string # elements: 1 # length: 2255 statistics: [h, pval, stats] = hotelling_t2test (x) statistics: [...] = hotelling_t2test (x, m) statistics: [...] = hotelling_t2test (x, y) statistics: [...] = hotelling_t2test (x, m, Name, Value) statistics: [...] = hotelling_t2test (x, y, Name, Value) Compute Hotelling's T^2 ("T-squared") test for a single sample or two dependent samples (paired-samples). For a sample x from a multivariate normal distribution with unknown mean and covariance matrix, test the null hypothesis that mean (x) == m. For two dependent samples x and y from a multivariate normal distributions with unknown means and covariance matrices, test the null hypothesis that mean (x - y) == 0. hotelling_t2test treats NaNs as missing values, and ignores the corresponding rows. Name-Value pair arguments can be used to set statistical significance. 'alpha' can be used to specify the significance level of the test and the level of the confidence interval (the default value is 0.05). If h is 1 the null hypothesis is rejected, meaning that the tested sample does not come from a multivariate distribution with mean m, or in case of two dependent samples that they do not come from the same multivariate distribution. If h is 0, then the null hypothesis cannot be rejected and it can be assumed that it holds true. The p-value of the test is returned in pval. stats is a structure containing the value of the Hotelling's T^2 test statistic in the field "t2stat", its F transform in "fstat", and the degrees of freedom of the F distribution in the fields "df1" and "df2". Under the null hypothesis, (n-p) T^2 / (p(n-1)) has an F distribution with p and n-p degrees of freedom, where n and p are the numbers of samples and variables, respectively. The effect size is the Mahalanobis distance D between the mean and m, in the field "MahalanobisD", with its 100 (1 - alpha)% confidence interval in "MahalanobisDCI". The noncentrality of the F statistic is n D^2; the distance is estimated from max (F df1 (df2 - 2) / df2 - df1, 0), unbiased for the noncentrality before its truncation at zero, and the interval inverts the noncentral F distribution. Where the interval would need a noncentrality above 10^5, where ncfcdf loses accuracy, it is NaN. See also: hotelling_t2test2 # name: # type: sq_string # elements: 1 # length: 105 Compute Hotelling's T^2 ("T-squared") test for a single sample or two dependent samples (paired-samples). # name: # type: sq_string # elements: 1 # length: 17 hotelling_t2test2 # name: # type: sq_string # elements: 1 # length: 1996 statistics: [h, pval, stats] = hotelling_t2test2 (x, y) statistics: [...] = hotelling_t2test2 (x, y, Name, Value) Compute Hotelling's T^2 ("T-squared") test for two independent samples. For two samples x from multivariate normal distributions with the same number of variables (columns), unknown means and unknown equal covariance matrices, test the null hypothesis mean (x) == mean (y). hotelling_t2test2 treats NaNs as missing values, and ignores the corresponding rows for each sample independently. Name-Value pair arguments can be used to set statistical significance. 'alpha' can be used to specify the significance level of the test and the level of the confidence interval (the default value is 0.05). If h is 1 the null hypothesis is rejected, meaning that the tested samples do not come from the same multivariate distribution. If h is 0, then the null hypothesis cannot be rejected and it can be assumed that both samples come from the same multivariate distribution. The p-value of the test is returned in pval. stats is a structure containing the value of the Hotelling's T^2 test statistic in the field "t2stat", its F transform in "fstat", and the degrees of freedom of the F distribution in the fields "df1" and "df2". Under the null hypothesis, $$ {(n_x+n_y-p-1) T^2 \over p(n_x+n_y-2)} $$ has an F distribution with p and n_x+n_y-p-1 degrees of freedom, where n_x and n_y are the sample sizes and p is the number of variables. The effect size is the Mahalanobis distance D between the two means, in the field "MahalanobisD", with its 100 (1 - alpha)% confidence interval in "MahalanobisDCI". The noncentrality of the F statistic is n_x n_y / (n_x + n_y) D^2; the distance is estimated from max (F df1 (df2 - 2) / df2 - df1, 0), unbiased for the noncentrality before its truncation at zero, and the interval inverts the noncentral F distribution. Where the interval would need a noncentrality above 10^5, where ncfcdf loses accuracy, it is NaN. See also: hotelling_t2test # name: # type: sq_string # elements: 1 # length: 71 Compute Hotelling's T^2 ("T-squared") test for two independent samples. # name: # type: sq_string # elements: 1 # length: 6 jbtest # name: # type: sq_string # elements: 1 # length: 3085 statistics: h = jbtest (x) statistics: h = jbtest (x, alpha) statistics: h = jbtest (x, alpha, mctol) statistics: [h, p] = jbtest (...) statistics: [h, p, jbstat, critval] = jbtest (...) Jarque-Bera hypothesis test of composite normality. h = jbtest (x) performs the Jarque-Bera test of the null hypothesis that the sample in the vector x comes from a normal distribution with unknown mean and variance, against the alternative that it does not come from a normal distribution. The result h is 1 if the test rejects the null hypothesis at the 5% significance level, and 0 otherwise. x must be a vector of real values; NaN values are treated as missing and removed. The Jarque-Bera test statistic is $$ JB = \frac{n}{6} \left( s^2 + \frac{(k-3)^2}{4} \right), $$ where n is the sample size, s is the sample skewness, and k is the sample kurtosis. Under the null hypothesis it is asymptotically chi-square distributed with two degrees of freedom. h = jbtest (x, alpha) performs the test at the significance level alpha, a scalar in the range (0,1). The default is 0.05. h = jbtest (x, alpha, mctol) computes a Monte-Carlo approximation of the p-value instead of interpolating the embedded table. mctol is the maximum Monte-Carlo standard error allowed for the p-value; the number of simulated samples is chosen accordingly. Use this for small samples, where the chi-square approximation is inaccurate, or for significance levels outside [0.001, 0.5]. [h, p] = jbtest (...) also returns the p-value p of the test. p is clamped to the tabulated range [0.001, 0.5], as MATLAB clamps it, and a warning is issued when the value lies outside that range. The warning is an addition here: MATLAB clamps silently, so a p-value reported as 0.001 or 0.5 there may be a bound rather than an estimate, with nothing to say so. [h, p, jbstat, critval] = jbtest (...) also returns the test statistic jbstat and the critical value critval at significance level alpha. The null hypothesis is rejected when jbstat > critval. Note: for n \le 2000 the p-value and critical value are obtained by interpolating an embedded critical-value table (the same approach MATLAB uses); for larger samples the large-sample chi-square approximation with two degrees of freedom is used instead. The embedded table was generated here by Monte-Carlo simulation, so it is itself an estimate of the true null quantiles. MATLAB's table is likewise a Monte-Carlo estimate but from a different simulation, so the two tables agree only to about two decimal places. As a result the reported p-value and critical value, and (in a narrow band of statistic values around the critical value) the test decision h, can differ slightly from MATLAB in edge cases. These differences are an unavoidable consequence of the Monte-Carlo origin of both tables, not a difference in method. Supply mctol for a direct Monte-Carlo p-value. If x has zero variance (or is otherwise degenerate so that skewness and kurtosis are undefined), jbstat is NaN; in this case p is reported as 0 and h as 1. See also: adtest, kstest, lillietest, swtest # name: # type: sq_string # elements: 1 # length: 51 Jarque-Bera hypothesis test of composite normality. # name: # type: sq_string # elements: 1 # length: 7 knntest # name: # type: sq_string # elements: 1 # length: 4331 statistics: knnstat = knntest (X, Y) statistics: knnstat = knntest (X, Y, Name, Value) statistics: [knnstat, p] = knntest (...) statistics: [knnstat, p, h] = knntest (...) Two-sample multivariate test based on nearest neighbours. knnstat = knntest (X, Y) returns the nearest neighbour statistic of the two samples X and Y, whose rows are observations and whose columns are variables. The two samples are pooled, each observation's k nearest neighbours are found among the others, and knnstat is the share of those neighbours that belong to the same sample as the observation. It lies in [0, 1]: near 1 the samples are well separated, and near 0.5 two samples of equal size are alike. X and Y are both numeric matrices with the same number of columns, or both tables. Two tables are read over the variables they share, which must be all the variables of one of them. An observation holding a missing value in a variable used is left out. [knnstat, p, h] = knntest (...) also returns the p-value of the right-tailed test of the null hypothesis that X and Y come from the same distribution, and h, which is 1 where the null hypothesis is rejected at the significance level 'Alpha' and 0 otherwise. The p-value takes (knnstat - mu) / sigma as standard normal, with m_x and m_y the sizes of the samples, m their sum, and q = m_x m_y / m^2: $$\mu = {m_x (m_x - 1) + m_y (m_y - 1) \over m (m - 1)}, \qquad \sigma^2 = {q + 4 q^2 \over m k}.$$ knnstat = knntest (..., Name, Value) takes the following options. Name Value 'Alpha' The significance level, a scalar between 0 and 1, 0.05 by default. 'NumNeighbors' The number of nearest neighbours k, a positive integer, 10 by default. It is taken as one less than the number of observations where it exceeds that. 'Distance' The distance between observations; see below. 'VariableNames' The variables to use, among those the two tables share, as a character vector, a string array or a cell array of character vectors. All the shared variables by default. 'CategoricalVariables' The variables holding levels: 'all', their indices, a logical vector over the variables, or, for tables, their names. A table variable holding logical values, an unordered categorical array, a string array or a cell array of character vectors holds levels whether named here or not. A matrix holds none unless named. 'Distance' is one of the following, 'seuclidean' by default where every variable is continuous, 'hamming' where every variable holds levels, and 'goodall3' where the two are mixed. Distance Description 'euclidean' Euclidean distance. 'seuclidean' Euclidean distance with each variable divided by its standard deviation over [X; Y]. 'cityblock' City block distance. 'cosine' One minus the cosine of the angle between two observations. 'correlation' One minus the correlation between two observations. 'fasteuclidean', 'fastseuclidean' The same distances as 'euclidean' and 'seuclidean', computed exactly. 'hamming' The share of variables that differ. 'goodall3' The Goodall 3 dissimilarity of nomdist, with level frequencies counted over [X; Y] and the observation whose neighbours are sought counted once more, as nomdist2 counts a query with 'CountQuery'. A continuous variable adds its absolute difference divided by its range over [X; Y], and the sum over every variable is divided by their number. The first five treat a variable holding levels as numbers, its levels coded 1, 2, ...; 'hamming' treats every distinct value of a continuous variable as a level. A tie between two neighbours at the same distance is broken by the order of the pooled sample, the rows of X coming before those of Y, and distances equal to within rounding count as tied, so the result does not depend on the platform. MATLAB lets rounding decide such ties, so on data where many distances are equal, such as values on a coarse grid, knnstat can differ slightly from MATLAB's. MATLAB defines its 'goodall3' for a continuous variable in a way it does not document, and which none of the constructions tried here reproduces, so on mixed data knnstat differs from MATLAB's. On levels alone the two agree. Reference: M. Williams (2010). How good are your fits? Unbinned multivariate goodness-of-fit tests in high energy physics. Journal of Instrumentation, 5(09), P09004. See also: nomdist, nomdist2, kstest2 # name: # type: sq_string # elements: 1 # length: 57 Two-sample multivariate test based on nearest neighbours. # name: # type: sq_string # elements: 1 # length: 13 kruskalwallis # name: # type: sq_string # elements: 1 # length: 3333 statistics: p = kruskalwallis (x) statistics: p = kruskalwallis (x, group) statistics: p = kruskalwallis (x, group, displayopt) statistics: p = kruskalwallis (x, group, displayopt, Name, Value) statistics: [p, tbl] = kruskalwallis (x, ...) statistics: [p, tbl, stats] = kruskalwallis (x, ...) Perform a Kruskal-Wallis test, the non-parametric alternative of a one-way analysis of variance (ANOVA), for comparing the means of two or more groups of data under the null hypothesis that the groups are drawn from the same population, i.e. the group means are equal. kruskalwallis can take up to three input arguments: x contains the data and it can either be a vector or matrix. If x is a matrix, then each column is treated as a separate group. If x is a vector, it is a single group whichever way it lies, unless group is given; with one group there is nothing to compare, so p is NaN. group contains the names for each group. If x is a matrix, then group can either be a cell array of strings of a character array, with one row per column of x. If you want to omit this argument, enter an empty array ([]). If x is a vector, then group must be a vector of the same length, or a string array or cell array of strings with one row for each element of x. x values corresponding to the same value of group are placed in the same group. displayopt is an optional parameter for displaying the groups contained in the data in a boxplot. If omitted, it is 'on' by default. If group names are defined in group, these are used to identify the groups in the boxplot. Use 'off' to omit displaying this figure. kruskalwallis can return up to three output arguments: p is the p-value of the null hypothesis that all group means are equal. tbl is a cell array containing the results in a standard ANOVA table. stats is a structure containing statistics useful for performing a multiple comparison of means with the MULTCOMPARE function. stats also holds the effect size of the test, an Octave extension: EtaSquared, the rank eta squared max (0, (H - k + 1) / (n - k)) for the statistic H over k groups and n observations, which is the share of the variance of the ranks the groups account for, corrected for bias, and EtaSquaredCI, its two-sided confidence interval at the level 100 (1 - alpha) percent. By default the interval inverts the noncentral chi-square distribution of H on k - 1 degrees of freedom, taken as H's distribution under the alternative, which it is for large samples. Three options after displayopt, also an Octave extension, set it: Name Value 'Alpha' The significance level of the interval, a scalar between 0 and 1, 0.05 by default. 'ConfidenceIntervalType' 'exact', the default, or 'bootstrap' for the bias-corrected and accelerated bootstrap interval, resampling within each group. 'NumBootstraps' The number of bootstrap replicates, a positive integer, 1000 by default. If kruskalwallis is called without any output arguments, then it prints the results in a one-way ANOVA table to the standard output. It is also printed when displayopt is 'on'. Examples: x = meshgrid (1:6); x = x + normrnd (0, 1, 6, 6); [p, atab] = kruskalwallis(x); x = ones (50, 4) .* [-2, 0, 1, 5]; x = x + normrnd (0, 2, 50, 4); group = {"A", "B", "C", "D"}; kruskalwallis (x, group); See also: anova1, friedman, ranksum, multcompare # name: # type: sq_string # elements: 1 # length: 241 Perform a Kruskal-Wallis test, the non-parametric alternative of a one-way analysis of variance (ANOVA), for comparing the means of two or more groups of data under the null hypothesis that the groups are drawn from the same population, i.e. # name: # type: sq_string # elements: 1 # length: 6 kstest # name: # type: sq_string # elements: 1 # length: 3490 statistics: h = kstest (x) statistics: h = kstest (x, name, value) statistics: [h, p] = kstest (...) statistics: [h, p, ksstat, cv] = kstest (...) Single sample Kolmogorov-Smirnov (K-S) goodness-of-fit hypothesis test. h = kstest (x) performs a Kolmogorov-Smirnov (K-S) test to determine if a random sample x could have come from a standard normal distribution. h indicates the results of the null hypothesis test. h = 0 => Do not reject the null hypothesis at the 5% significance h = 1 => Reject the null hypothesis at the 5% significance x is a vector representing a random sample from some unknown distribution with a cumulative distribution function F(X). Missing values declared as NaNs in x are ignored. h = kstest (x, name, value) returns a test decision for a single-sample K-S test with additional options specified by one or more Name-Value pair arguments as shown below. Name Value 'alpha' A numeric scalar between 0 and 1 specifying th the significance level. Default is 0.05 for 5% significance. 'CDF' The hypothesized CDF under the null hypothesis. It can be specified as a function handle of an existing cdf function, a character vector defining a probability distribution with default parameters, a probability distribution object, or a two-column matrix. If not provided, the default is the standard normal, N(0,1). The one-sample Kolmogorov-Smirnov test is only valid for continuous cumulative distribution functions, and requires the CDF to be predetermined. The result is not accurate if CDF is estimated from the data. 'tail' A string indicating the type of test: 'unequal' "F(X) not equal to CDF(X)" (two-sided) (Default) 'larger' "F(X) > CDF(X)" (one-sided) 'smaller' "F(X) < CDF(X)" (one-sided) Let S(X) be the empirical c.d.f. estimated from the sample vector x, F(X) be the corresponding true (but unknown) population c.d.f., and CDF be the known input c.d.f. specified under the null hypothesis. For tail = "unequal", "larger", and "smaller", the test statistics are max|S(X) - CDF(X)|, max[S(X) - CDF(X)], and max[CDF(X) - S(X)], respectively. [h, p] = kstest (...) also returns the asymptotic p-value p. [h, p, ksstat] = kstest (...) returns the K-S test statistic ksstat defined above for the test type indicated by the "tail" option In the matrix version of CDF, column 1 contains the x-axis data and column 2 the corresponding y-axis c.d.f data. Since the K-S test statistic will occur at one of the observations in x, the calculation is most efficient when CDF is only specified at the observations in x. When column 1 of CDF represents x-axis points independent of x, CDF is linearly interpolated at the observations found in the vector x. In this case, the interval along the x-axis (the column 1 spread of CDF) must span the observations in x for successful interpolation. The decision to reject the null hypothesis is based on comparing the p-value p with the "alpha" value, not by comparing the statistic ksstat with the critical value cv. cv is computed separately using an approximate formula or by interpolation using Miller's approximation table. The formula and table cover the range 0.01 <= "alpha" <= 0.2 for two-sided tests and 0.005 <= "alpha" <= 0.1 for one-sided tests. CV is returned as NaN if "alpha" is outside this range. Since CV is approximate, a comparison of ksstat with cv may occasionally lead to a different conclusion than a comparison of p with "alpha". See also: kstest2, cdfplot, adtest, jbtest, lillietest, swtest # name: # type: sq_string # elements: 1 # length: 71 Single sample Kolmogorov-Smirnov (K-S) goodness-of-fit hypothesis test. # name: # type: sq_string # elements: 1 # length: 7 kstest2 # name: # type: sq_string # elements: 1 # length: 1906 statistics: h = kstest2 (x1, x2) statistics: h = kstest2 (x1, x2, name, value) statistics: [h, p] = kstest2 (...) statistics: [h, p, ks2stat] = kstest2 (...) Two-sample Kolmogorov-Smirnov goodness-of-fit hypothesis test. h = kstest2 (x1, x2) returns a test decision for the null hypothesis that the data in vectors x1 and x2 are from the same continuous distribution, using the two-sample Kolmogorov-Smirnov test. The alternative hypothesis is that x1 and x2 are from different continuous distributions. The result h is 1 if the test rejects the null hypothesis at the 5% significance level, and 0 otherwise. h = kstest2 (x1, x2, name, value) returns a test decision for a two-sample Kolmogorov-Smirnov test with additional options specified by one or more name-value pair arguments as shown below. Name Value "alpha" A value alpha between 0 and 1 specifying the significance level. Default is 0.05 for 5% significance. "tail" A string indicating the type of test: "unequal" "F(X1) not equal to F(X2)" (two-sided) [Default] "larger" "F(X1) > F(X2)" (one-sided) "smaller" "F(X1) < F(X2)" (one-sided) The two-sided test uses the maximum absolute difference between the cdfs of the distributions of the two data vectors. The test statistic is D* = max(|F1(x) - F2(x)|), where F1(x) is the proportion of x1 values less or equal to x and F2(x) is the proportion of x2 values less than or equal to x. The one-sided test uses the actual value of the difference between the cdfs of the distributions of the two data vectors rather than the absolute value. The test statistic is D* = max(F1(x) - F2(x)) or D* = max(F2(x) - F1(x)) for tail = "larger" or "smaller", respectively. [h, p] = kstest2 (...) also returns the asymptotic p-value p. [h, p, ks2stat] = kstest2 (...) also returns the Kolmogorov-Smirnov test statistic ks2stat defined above for the test type indicated by tail. See also: kstest, cdfplot # name: # type: sq_string # elements: 1 # length: 62 Two-sample Kolmogorov-Smirnov goodness-of-fit hypothesis test. # name: # type: sq_string # elements: 1 # length: 10 lillietest # name: # type: sq_string # elements: 1 # length: 2044 statistics: h = lillietest (x) statistics: h = lillietest (x, name, value) statistics: [h, p] = lillietest (...) statistics: [h, p, kstat, critval] = lillietest (...) Lilliefors goodness-of-fit hypothesis test. h = lillietest (x) tests the null hypothesis that the sample in the vector x comes from a normal distribution with unknown mean and variance, against the alternative that it does not, using the Lilliefors test. h is 1 if the test rejects the null at the 5% significance level and 0 otherwise. The Lilliefors statistic is the Kolmogorov-Smirnov statistic --- the maximum absolute difference between the empirical cumulative distribution function of x and the cumulative distribution function of the hypothesized family with parameters estimated from x. Because the parameters are estimated, the null distribution of the statistic differs from that of the ordinary Kolmogorov-Smirnov test. The following Name-Value pairs are supported: Name Value 'Distribution' The hypothesized family: 'normal' (default), 'exponential', or 'extreme value'. The parameters are estimated from x: mean and standard deviation for the normal, mean for the exponential, and location and scale for the extreme value distribution. 'Alpha' The significance level, a scalar. Without 'MCTol' it must lie in [0.001, 0.5] (the tabulated range); with 'MCTol' it may be any value in (0, 1). The default is 0.05. 'MCTol' Maximum Monte-Carlo standard error for the p-value. When supplied, the p-value and critical value are computed by Monte-Carlo simulation instead of by interpolating the embedded table. [h, p, kstat, critval] = lillietest (...) also returns the p-value p, the test statistic kstat, and the critical value critval. Without 'MCTol' the p-value is clamped to the tabulated range [0.001, 0.5] and a warning is issued when it lies outside. The warning is an addition here: MATLAB clamps silently, so a p-value reported as 0.001 or 0.5 there may be a bound rather than an estimate, with nothing to say so. See also: adtest, jbtest, kstest, swtest # name: # type: sq_string # elements: 1 # length: 43 Lilliefors goodness-of-fit hypothesis test. # name: # type: sq_string # elements: 1 # length: 7 manova1 # name: # type: sq_string # elements: 1 # length: 2516 statistics: d = manova1 (x, group) statistics: d = manova1 (x, group, alpha) statistics: [d, p] = manova1 (...) statistics: [d, p, stats] = manova1 (...) One-way multivariate analysis of variance (MANOVA). d = manova1 (x, group, alpha) performs a one-way MANOVA for comparing the mean vectors of two or more groups of multivariate data. x is a matrix with each row representing a multivariate observation, and each column representing a variable. group is a numeric vector, string array, or cell array of strings with the same number of rows as x. x values are in the same group if they correspond to the same value of GROUP. alpha is the scalar significance level and is 0.05 by default. d is an estimate of the dimension of the group means. It is the smallest dimension such that a test of the hypothesis that the means lie on a space of that dimension is not rejected. If d = 0 for example, we cannot reject the hypothesis that the means are the same. If d = 1, we reject the hypothesis that the means are the same but we cannot reject the hypothesis that they lie on a line. [d, p] = manova1 (...) returns P, a vector of p-values for testing the null hypothesis that the mean vectors of the groups lie on various dimensions. P(1) is the p-value for a test of dimension 0, P(2) for dimension 1, etc. [d, p, stats] = manova1 (...) returns a STATS structure with the following fields: "W" within-group sum of squares and products matrix "B" between-group sum of squares and products matrix "T" total sum of squares and products matrix "dfW" degrees of freedom for WSSP matrix "dfB" degrees of freedom for BSSP matrix "dfT" degrees of freedom for TSSP matrix "lambda" value of Wilk's lambda (the test statistic) "chisq" transformation of lambda to a chi-square distribution "chisqdf" degrees of freedom for chisq "eigenval" eigenvalues of (WSSP^-1) * BSSP "eigenvec" eigenvectors of (WSSP^-1) * BSSP; these are the coefficients for canonical variables, and they are scaled so the within-group variance of C is 1 "canon" canonical variables, equal to XC*eigenvec, where XC is X with columns centered by subtracting their means "mdist" Mahalanobis distance from each point to its group mean "gmdist" Mahalanobis distances between each pair of group means "gnames" Group names The canonical variables C have the property that C(:,1) is the linear combination of the x columns that has the maximum separation between groups, C(:,2) has the maximum separation subject to it being orthogonal to C(:,1), and so on. # name: # type: sq_string # elements: 1 # length: 51 One-way multivariate analysis of variance (MANOVA). # name: # type: sq_string # elements: 1 # length: 12 mcnemar_test # name: # type: sq_string # elements: 1 # length: 2280 statistics: h = mcnemar_test (x) statistics: h = mcnemar_test (x, testtype) statistics: h = mcnemar_test (..., 'Alpha', alpha) statistics: [h, p, stats] = mcnemar_test (...) Perform a McNemar's test on paired nominal data. McNemar's test is applied to a 2x2 contingency table x with a dichotomous trait, with matched pairs of subjects, of data cross-classified on the row and column variables to testing the null hypothesis of symmetry of the classification probabilities. More formally, the null hypothesis of marginal homogeneity states that the two marginal probabilities for each outcome are the same. The test rests on the two discordant counts, b = x(1,2) and c = x(2,1). Under the null, with a sufficiently large number of discordants (b + c >= 25), the test statistic follows a chi-squared distribution with 1 degree of freedom. When the number of discordants is less than 25, then the mid-P exact McNemar test is used. testtype will force mcnemar_test to apply a particular method for testing the null hypothesis independently of the number of discordants. Valid options for testtype: 'asymptotic' Original McNemar test statistic 'corrected' Edwards' version with continuity correction 'exact' An exact binomial test 'mid-p' The mid-P McNemar test (mid-p binomial test) The test decision is returned in h, which is 1 when the null hypothesis is rejected at the significance level alpha and 0 otherwise, and the p-value in p. 'Alpha' sets alpha, 0.05 by default, and the level 100 (1 - alpha)% of the confidence intervals. stats is a structure with the following fields: chi2stat the chi-squared statistic, for the 'asymptotic' and 'corrected' tests only. df its degrees of freedom, 1, for those tests only. OddsRatio the odds ratio of the discordant pairs, b / c. OddsRatioCI its confidence interval. CohensG Cohen's g, b / (b + c) - 0.5, the departure of the discordant split from one half. CohensGCI its confidence interval. Both intervals come from the exact Clopper-Pearson interval of the proportion b / (b + c), as does the exact test. With no discordant pairs both effect sizes and their intervals are NaN. Further information about the McNemar's test can be found at https://en.wikipedia.org/wiki/McNemar%27s_test See also: crosstab, chi2test, fishertest # name: # type: sq_string # elements: 1 # length: 48 Perform a McNemar's test on paired nominal data. # name: # type: sq_string # elements: 1 # length: 14 meanEffectSize # name: # type: sq_string # elements: 1 # length: 8590 statistics: Effect = meanEffectSize (X) statistics: Effect = meanEffectSize (X, Y) statistics: Effect = meanEffectSize (..., Name, Value) Effect sizes for the difference between two means, with their confidence intervals. Effect = meanEffectSize (X) returns the difference between the mean of the sample X and a known population mean, zero by default, with its confidence interval. Effect = meanEffectSize (X, Y) returns the difference between the means of the samples X and Y, with its confidence interval. X and Y are vectors of type double or single. A missing value (NaN) is left out of its sample, and out of both where the samples are paired. Where either sample is single, so are the results. Effect is a table with one row for each effect size asked for, in the order asked, named as listed below. Its variable Effect holds the effect size and its variable ConfidenceIntervals the lower and upper bounds of its confidence interval, the latter absent where 'ConfidenceIntervalType' is 'none'. Effect = meanEffectSize (..., Name, Value) takes the following options. Name Value 'Effect' The effect sizes to compute, as a character vector, a string array or a cell array of character vectors, chosen among those below. A name may be shortened to any prefix that names one effect alone, and case does not matter. 'meandiff' by default. 'Mean' The known population mean that one sample is compared with, a real scalar, 0 by default. It is ignored where there are two samples. 'Paired' Whether X and Y are paired observations, true or false (also 'on' or 'off'), false by default. Paired samples must have the same number of elements. 'VarianceType' Whether two unpaired samples are taken to come from populations of 'equal' or 'unequal' variance, 'equal' by default. Paired samples take 'equal' only; one sample ignores it. 'Alpha' The significance level, a scalar between 0 and 1, 0.05 by default, so that each interval covers 100(1 - Alpha) percent. 'ConfidenceIntervalType' 'exact', 'bootstrap' or 'none'. By default each effect takes 'exact' where it has an exact interval and 'bootstrap' where it has not. 'NumBootstraps' The number of bootstrap replicates, a positive integer, 1000 by default. 'BootstrapOptions' A structure, as returned by statset, accepted for compatibility. The replicates are always computed serially and drawn from the generator of rand. 'Resampling' How the bootstrap resamples two unpaired samples, 'pooled' or 'stratified'; see below. 'pooled' by default. This option is an Octave extension. The effect sizes are the following, where J(v) is Hedges' correction for bias, J(v) = Gamma(v/2) / (sqrt(v/2) Gamma((v-1)/2)). Effect Row name Description 'meandiff' MeanDifference The difference between the means, mean(X) - mean(Y), or mean(X) - mu for one sample. Its exact interval is Student's t interval: pooled for equal variances, Welch's for unequal variances, and on the differences for paired samples. 'cohen' CohensD Cohen's d corrected for bias, that is Hedges' g: the difference between the means divided by a standard deviation, times J(v). See below. 'glass' GlasssDelta Glass's delta, J(n_x - 1) (mean(X) - mean(Y)) / s_x, the difference between the means divided by the standard deviation of the control sample X. Two unpaired samples only; 'VarianceType' does not change it. 'cliff' CliffsDelta Cliff's delta, the share of pairs (x_i, y_j) with x_i > y_j less the share with x_i < y_j. For paired samples the pairs are those of different observations, i != j. Two samples only. 'mediandiff' MedianDifference The difference between the medians, median(X) - median(Y), paired samples included. Two samples only. 'robustcohen' RobustCohensD A robust Cohen's d, as MATLAB computes it; see below. 'akpcohen' AKPCohensD The robust Cohen's d of Algina, Keselman and Penfield; see below. This effect is an Octave extension. 'kstest' KolmogorovSmirnovStatistic The two-sample Kolmogorov-Smirnov statistic, the largest distance between the empirical distribution functions of X and Y, paired samples included. Two samples only. 'meandiff', 'cohen', 'glass' and 'cliff' have exact intervals; the others are bootstrap only. Cohen's d. For one sample it is J(n - 1) (mean(X) - mu) / s_x, and for two unpaired samples of equal variance J(n_x + n_y - 2) (mean(X) - mean(Y)) / s_p, with s_p the pooled standard deviation. Its interval inverts the noncentral t distribution of the one-sample or the two-sample t statistic. For unequal variances it is the g^x of Delacre and colleagues: the difference between the means divided by s_a = sqrt((s_x^2 + s_y^2) / 2), times J(v) with v = (n_x - 1)(n_y - 1)(s_x^2 + s_y^2)^2 / ((n_y - 1) s_x^4 + (n_x - 1) s_y^4), and its interval inverts the noncentral t distribution of Welch's statistic on v degrees of freedom. For paired samples it is J(n - 1) mean(X - Y) / s_a, and its interval is the MAG interval of Cousineau and Goulet-Pelletier. The interval of Glass's delta inverts the noncentral t distribution of Welch's statistic on n_x - 1 degrees of freedom. Cliff's delta. Its interval is Cliff's asymmetric interval from the normal distribution, with the variance of delta estimated as ((n_y - 1) var(d_i) + (n_x - 1) var(d_j) + S / ((n_x - 1)(n_y - 1))) / (n_x n_y), where d_i and d_j are the row and column means of the dominance matrix d_ij = sign(x_i - y_j) and S is the sum of (d_ij - delta)^2. For paired samples the interval is delta +/- z s with the variance of the U statistic over the n(n - 1) pairs, which needs four pairs or more. Robust Cohen's d. 'robustcohen' is MATLAB's: 0.642 J(v) (T(X) - T(Y)) / s_w, where T is trimmean (..., 20), which trims 10 percent of each tail, and s_w the standard deviation of the values clipped at their 20th and 80th percentiles, pooled over the two samples as s_p is, or averaged as s_a is for unequal variances and paired samples. v is the degrees of freedom of the matching 'cohen'. For one sample T(Y) is replaced by mu. 'akpcohen' is the estimator of Algina, Keselman and Penfield, as the R package WRS2 computes it: c (T(X) - T(Y)) / s_w, where T trims 20 percent of each tail, floor(0.2 n) values, and s_w is the standard deviation of the sample winsorized at the same order statistics. Two samples of equal variance pool s_w as s_p is pooled; for unequal variances s_w is that of X alone. One sample takes c (T(X) - mu) / s_w, and paired samples the same over the differences X - Y, with mu zero. There is no correction for bias. The constant c = 0.64194, which the authors round to 0.642, is the standard deviation of a standard normal distribution winsorized at 20 percent, so that both estimators estimate Cohen's d where the data are normal; 'akpcohen' trims more of each tail and is the more robust of the two. Bootstrap. The bootstrap interval is the bias-corrected and accelerated (BCa) percentile interval, with the acceleration estimated by the jackknife. One sample is resampled with replacement, and paired samples by pairs. Two unpaired samples are resampled as 'Resampling' says. 'pooled' resamples the observations of both samples together, each keeping its sample, so that the size of each sample varies from one replicate to the next; this is what MATLAB does, and it suits data where the sample an observation falls in is itself random. 'stratified' resamples each sample on its own, keeping their sizes; it suits data where the sizes were fixed by design, as in an experiment. A replicate in which a sample comes out empty is left out. An interval that cannot be computed, because a sample holds too few observations or has zero variance, is NaN, with a warning. MATLAB returns a paired Cliff's delta interval of zero width, [delta, delta], for fewer than four pairs, where the variance cannot be estimated; here that interval is NaN. References: J. Algina, H. J. Keselman and R. D. Penfield (2005). An alternative to Cohen's standardized mean difference effect size: a robust parameter and confidence interval in the two independent groups case. Psychological Methods, 10(3), 317-328. D. Cousineau and J.-C. Goulet-Pelletier (2021). A study of confidence intervals for Cohen's dp in within-subject designs with new proposals. The Quantitative Methods for Psychology, 17(1), 51-75. M. Delacre, D. Lakens, C. Ley, L. Liu and C. Leys (2021). Why Hedges' g*s based on the non-pooled standard deviation should be reported with Welch's t-test. PsyArXiv. N. Cliff (1993). Dominance statistics: ordinal analyses to answer ordinal questions. Psychological Bulletin, 114(3), 494-509. See also: ttest, ttest2, ranksum, kstest2, trimmean # name: # type: sq_string # elements: 1 # length: 83 Effect sizes for the difference between two means, with their confidence intervals. # name: # type: sq_string # elements: 1 # length: 7 mmdtest # name: # type: sq_string # elements: 1 # length: 3073 statistics: mmdval = mmdtest (X, Y) statistics: mmdval = mmdtest (X, Y, Name, Value) statistics: [mmdval, p] = mmdtest (...) statistics: [mmdval, p, h] = mmdtest (...) Two-sample multivariate test on the maximum mean discrepancy. mmdval = mmdtest (X, Y) returns the squared maximum mean discrepancy between the samples X and Y, whose rows are observations and whose columns are variables. It measures how far apart the two distributions lie, 0 where the two samples are alike. With m rows in X, n in Y and k the kernel, it is the biased estimate $$\mathrm{MMD}^2 = {1 \over m^2} \sum_{i,j=1}^{m} k(x_i, x_j) - {2 \over m n} \sum_{i=1}^{m} \sum_{j=1}^{n} k(x_i, y_j) + {1 \over n^2} \sum_{i,j=1}^{n} k(y_i, y_j).$$ X and Y are both numeric matrices with the same number of columns, or both tables, read over the variables they share, which must be all the variables of one of them. An observation holding a missing value in a variable used is left out. The kernel is Gaussian, k(u, v) = exp (-d^2 / s). Each continuous variable is standardised by the mean and standard deviation of X, a standard deviation of 0 being taken as 1, and contributes its squared difference to d^2; a variable holding levels contributes 2 where the two observations differ, as one column per level would. The scale s is the median of d^2 over the pairs of observations in X, taken as 1 where that median is 0. [mmdval, p, h] = mmdtest (...) also returns the p-value of the permutation test of the null hypothesis that X and Y come from the same distribution, and h, which is 1 where the null hypothesis is rejected at the significance level 'Alpha' and 0 otherwise. Each permutation deals the pooled observations out again as X and Y, standardises and scales them afresh, and computes the statistic; p is the share of the permutations whose statistic is at least the observed one. The permutations are drawn with randperm, so the state of rand decides them. They are drawn only where p or h is asked for. mmdval = mmdtest (..., Name, Value) takes the following options. Name Value 'Alpha' The significance level, a scalar between 0 and 1, 0.05 by default. 'NumPermutations' The number of permutations, a positive integer, 1000 by default. 'VariableNames' The variables to use, among those the two tables share, as a character vector, a string array or a cell array of character vectors. All the shared variables by default. 'CategoricalVariables' The variables holding levels: 'all', their indices, a logical vector over the variables, or, for tables, their names. A table variable holding logical values, an unordered categorical array, a string array or a cell array of character vectors holds levels whether named here or not. A matrix holds none unless named. 'Options' A structure as statset returns. Parallel computing and random streams are not implemented, and are refused where asked for. Reference: A. Gretton, K. M. Borgwardt, M. J. Rasch, B. Schoelkopf and A. Smola (2012). A kernel two-sample test. Journal of Machine Learning Research, 13, 723-773. See also: knntest, kstest2 # name: # type: sq_string # elements: 1 # length: 61 Two-sample multivariate test on the maximum mean discrepancy. # name: # type: sq_string # elements: 1 # length: 11 multcompare # name: # type: sq_string # elements: 1 # length: 6927 statistics: C = multcompare (STATS) statistics: C = multcompare (STATS, "name", value) statistics: [C, M] = multcompare (...) statistics: [C, M, H] = multcompare (...) statistics: [C, M, H, GNAMES] = multcompare (...) statistics: padj = multcompare (p) statistics: padj = multcompare (p, "ctype", CTYPE) Perform posthoc multiple comparison tests or p-value adjustments to control the family-wise error rate (FWER) or false discovery rate (FDR). C = multcompare (STATS) performs a multiple comparison using a STATS structure that is obtained as output from any of the following functions: anova1, anova2, anovan, kruskalwallis, and friedman. The return value C is a matrix with one row per comparison and six columns. Columns 1-2 are the indices of the two samples being compared. Columns 3-5 are a lower bound, estimate, and upper bound for their difference, where the bounds are for 95% confidence intervals. Column 6-8 are the multiplicity adjusted p-values for each individual comparison, the test statistic and the degrees of freedom. For STATS from kruskalwallis or friedman, the comparisons use a z statistic, and the degrees of freedom are reported as Inf, which is how the normal reference distribution is signalled; tcdf and tinv at Inf agree with normcdf and norminv. All tests by multcompare are two-tailed. multcompare can take a number of optional parameters as name-value pairs. [...] = multcompare (STATS, "alpha", ALPHA) ALPHA sets the significance level of null hypothesis significance tests to ALPHA, and the central coverage of two-sided confidence intervals to 100*(1-ALPHA)%. (Default ALPHA is 0.05). [...] = multcompare (STATS, "ControlGroup", REF) REF is the index of the control group to limit comparisons to. The index must be a positive integer scalar value. For each dimension (d) listed in DIM, multcompare uses STATS.grpnames{d}(idx) as the control group. (Default is empty, i.e. [], for full pairwise comparisons) [...] = multcompare (STATS, "ctype", CTYPE) CTYPE is the type of comparison test to use. In order of increasing power, the choices are: "bonferroni", "scheffe", "mvt", "holm" (default), "hochberg", "fdr", or "lsd". The first five methods control the family-wise error rate. The "fdr" method controls false discovery rate (by the original Benjamini-Hochberg step-up procedure). The final method, "lsd" (or "none"), makes no attempt to control the Type 1 error rate of multiple comparisons. The coverage of confidence intervals are only corrected for multiple comparisons in the cases where CTYPE is "bonferroni", "scheffe" or "mvt", which control the Type 1 error rate for simultaneous inference. The "mvt" method uses the multivariate t distribution to assess the probability or critical value of the maximum statistic across the tests, thereby accounting for correlations among comparisons in the control of the family-wise error rate with simultaneous inference. In the case of pairwise comparisons, it simulates Tukey's (or the Games-Howell) test, in the case of comparisons with a single control group, it simulates Dunnett's test. CTYPE values "tukey-kramer" and "hsd" are recognised but set the value of CTYPE and REF to "mvt" and empty respectively. A CTYPE value "dunnett" is recognised but sets the value of CTYPE to "mvt", and if REF is empty, sets REF to 1. Since the algorithm uses a Monte Carlo method (of 1e+06 random samples), you can expect the results to fluctuate slightly with each call to multcompare and the calculations may be slow to complete for a large number of comparisons. If the parallel package is installed and loaded, multcompare will automatically accelerate computations by parallel processing. Note that p-values calculated by the "mvt" are truncated at 1e-06. [...] = multcompare (STATS, "df", DF) DF is an optional scalar value to set the number of degrees of freedom in the calculation of p-values for the multiple comparison tests. By default, this value is extracted from the STATS structure of the ANOVA test, but setting DF maybe necessary to approximate Satterthwaite correction if anovan was performed using weights. [...] = multcompare (STATS, "dim", DIM) DIM is a vector specifying the dimension or dimensions over which the estimated marginal means are to be calculated. Used only if STATS comes from anovan. The value [1 3], for example, computes the estimated marginal mean for each combination of the first and third predictor values. The default is to compute over the first dimension (i.e. 1). If the specified dimension is, or includes, a continuous factor then multcompare will return an error. [...] = multcompare (STATS, "estimate", ESTIMATE) ESTIMATE is a string specifying the estimates to be compared when computing multiple comparisons after anova2; this argument is ignored by anovan and anova1. Accepted values for ESTIMATE are either "column" (default) to compare column means, or "row" to compare row means. If the model type in anova2 was "linear" or "nested" then only "column" is accepted for ESTIMATE since the row factor is assumed to be a random effect. [...] = multcompare (STATS, "display", DISPLAY) DISPLAY is either "on" (the default): to display a table and graph of the comparisons (e.g. difference between means), their 100*(1-ALPHA)% intervals and multiplicity adjusted p-values in APA style; or "off": to omit the table and graph. On the graph, markers and error bars colored red have multiplicity adjusted p-values < ALPHA, otherwise the markers and error bars are blue. [...] = multcompare (STATS, "seed", SEED) SEED is a scalar value used to initialize the random number generator so that CTYPE "mvt" produces reproducible results. [C, M, H, GNAMES] = multcompare (...) returns additional outputs. M is a matrix where columns 1-2 are the estimated marginal means and their standard errors, and columns 3-4 are lower and upper bounds of the confidence intervals for the means; the critical value of the test statistic is scaled by a factor of 2^(-0.5) before multiplying by the standard errors of the group means so that the intervals overlap when the difference in means becomes significant at approximately the level ALPHA. When ALPHA is 0.05, this corresponds to confidence intervals with 83.4% central coverage. H is a handle to the figure containing the graph. GNAMES is a cell array with one row for each group, containing the names of the groups. padj = multcompare (p) calculates and returns adjusted p-values (padj) using the Holm-step down Bonferroni procedure to control the family-wise error rate. padj = multcompare (p, "ctype", CTYPE) calculates and returns adjusted p-values (padj) computed using the method CTYPE. In order of increasing power, CTYPE for p-value adjustment can be either "bonferroni", "holm" (default), "hochberg", or "fdr". See above for further information about the CTYPE methods. See also: anova1, anova2, anovan, kruskalwallis, friedman, fitlm # name: # type: sq_string # elements: 1 # length: 140 Perform posthoc multiple comparison tests or p-value adjustments to control the family-wise error rate (FWER) or false discovery rate (FDR). # name: # type: sq_string # elements: 1 # length: 7 ranksum # name: # type: sq_string # elements: 1 # length: 3344 statistics: p = ranksum (x, y) statistics: p = ranksum (x, y, alpha) statistics: p = ranksum (x, y, alpha, Name, Value) statistics: p = ranksum (x, y, Name, Value) statistics: [p, h] = ranksum (x, y, ...) statistics: [p, h, stats] = ranksum (x, y, ...) Wilcoxon rank sum test for equal medians. This test is equivalent to a Mann-Whitney U-test. p = ranksum (x, y) returns the p-value of a two-sided Wilcoxon rank sum test. It tests the null hypothesis that two independent samples, in the vectors X and Y, come from continuous distributions with equal medians, against the alternative hypothesis that they are not. x and y can have different lengths and the test assumes that they are independent. ranksum treats NaN in x, y as missing values. The two-sided p-value is computed by doubling the most significant one-sided value. [p, h] = ranksum (x, y) also returns the result of the hypothesis test with h = 1 indicating a rejection of the null hypothesis at the default alpha = 0.05 significance level, and h = 0 indicating a failure to reject the null hypothesis at the same significance level. [p, h, stats] = ranksum (x, y) also returns the structure stats with information about the test statistic. It contains the field ranksum with the value of the rank sum test statistic, and the field zval with the value of the z-statistic when computed with the "approximate" method, and empty otherwise. The field is always there. Two further fields, an Octave extension, hold the effect size: RankBiserial, the rank-biserial correlation, which is Cliff's delta, the share of pairs (x_i, y_j) with x_i > y_j less the share with x_i < y_j, and RankBiserialCI, its two-sided confidence interval at the level 100 (1 - alpha) percent, whatever the tail. Both are those of meanEffectSize (x, y, "Effect", "cliff"): by default the interval is Cliff's, and the options "ConfidenceIntervalType", "bootstrap" for a BCa bootstrap interval, "NumBootstraps" and "Resampling" are passed on to meanEffectSize. [...] = ranksum (x, y, alpha) or alternatively [...] = ranksum (x, y, "alpha", alpha) returns the result of the hypothesis test performed at the significance level ALPHA. [...] = ranksum (x, y, "method", M) defines the computation method of the p-value specified in M, which can be "exact", "approximate", or "oldexact". M must be a single string. When "method" is unspecified, the default is: "exact" when min (length (x), length (y)) < 10 and length (x) + length (y) < 20, otherwise the "approximate" method is used. "exact" method uses full enumeration for small total sample size (< 10), otherwise the network algorithm is used for larger samples. "approximate" uses normal approximation method for computing the p-value. "oldexact" uses full enumeration for any sample size. Note, that this option can lead to out of memory error for large samples. Use with caution! [...] = ranksum (x, y, "tail", tail) defines the type of test, which can be "both", "right", or "left". tail must be a single string. "both" -- "medians are not equal" (two-tailed test, default) "right" -- "median of X is greater than median of Y" (right-tailed test) "left" -- "median of X is less than median of Y" (left-tailed test) Note: the rank sum statistic is based on the smaller sample of vectors x and y. See also: signrank, kruskalwallis, meanEffectSize # name: # type: sq_string # elements: 1 # length: 41 Wilcoxon rank sum test for equal medians. # name: # type: sq_string # elements: 1 # length: 16 regression_ftest # name: # type: sq_string # elements: 1 # length: 2376 statistics: h = regression_ftest (y, x) statistics: h = regression_ftest (y, x, keep) statistics: h = regression_ftest (..., Name, Value) statistics: [h, p, stats] = regression_ftest (...) F-test for nested linear regression models. Fit the full linear model of the response y on every column of x, and a reduced model on the columns of x indexed by keep, both by least squares, and test the null hypothesis that the columns the reduced model leaves out add nothing to it. keep holds column indices or a logical mask; left out or empty, the reduced model has no predictors and the test is of every column of x together. Both models include a constant term unless 'Intercept' is false. y must be a vector with one element per row of x, and neither may contain missing values. The test decision is returned in h, which is 1 when the null hypothesis is rejected at the significance level alpha and 0 otherwise, and the p-value in p. stats is a structure with the following fields: fstat the F statistic. df1 its numerator degrees of freedom, the number of coefficients the reduced model leaves out. df2 its denominator degrees of freedom, the residual degrees of freedom of the full model. betafull the coefficients of the full model, the constant first where there is one. betareduced those of the reduced model. ssefull the residual sum of squares of the full model. ssereduced that of the reduced model. CohensF2 Cohen's f^2, the effect size of the columns tested. CohensF2CI its 100 (1 - alpha)% confidence interval. The degrees of freedom come from the ranks of the two designs. Cohen's f^2 is taken, as Cohen defines it, from the noncentrality of the F statistic, lambda = f^2 (df1 + df2 + 1): the point estimate from max (F df1 (df2 - 2) / df2 - df1, 0), unbiased for the noncentrality before its truncation at zero, and the interval by inverting the noncentral F distribution. Where the interval would need a noncentrality above 10^5, where ncfcdf loses accuracy, it is NaN. The following Name/Value pairs are accepted: 'Alpha' the significance level of h and the level of the confidence interval. Default is 0.05. 'Intercept' whether both models include a constant term. Default is true. The same tests, and any linear hypothesis on the coefficients, are available from a fitted LinearModel through its coefTest method. See also: regression_ttest, regress, fitlm # name: # type: sq_string # elements: 1 # length: 43 F-test for nested linear regression models. # name: # type: sq_string # elements: 1 # length: 16 regression_ttest # name: # type: sq_string # elements: 1 # length: 1407 statistics: h = regression_ttest (y, x) statistics: [h, pval] = regression_ttest (y, x) statistics: [h, pval, ci] = regression_ttest (y, x) statistics: [h, pval, ci, stats] = regression_ttest (y, x) statistics: [...] = regression_ttest (y, x, Name, Value) Perform a linear regression t-test. h = regression_ttest (y, x) tests the null hypothesis that the slope beta1 of a simple linear regression equals 0. The result is h = 0 if the null hypothesis cannot be rejected at the 5% significance level, or h = 1 if the null hypothesis can be rejected at the 5% level. y and x must be vectors of equal length with finite real numbers. The p-value of the test is returned in pval. A 100(1-alpha)% confidence interval for beta1 is returned in ci. stats is a structure containing the value of the test statistic (tstat), the degrees of freedom (df), the slope coefficient (beta1), and the intercept (beta0). Under the null, the test statistic stats.tstat follows a T-distribution with stats.df degrees of freedom. [...] = regression_ttest (..., name, value) specifies one or more of the following name/value pairs: Name Value 'alpha' the significance level. Default is 0.05. 'tail' a string specifying the alternative hypothesis 'both' beta1 is not 0 (two-tailed, default) 'left' beta1 is less than 0 (left-tailed) 'right' beta1 is greater than 0 (right-tailed) See also: regression_ftest, regress, regress_gp # name: # type: sq_string # elements: 1 # length: 35 Perform a linear regression t-test. # name: # type: sq_string # elements: 1 # length: 6 rjtest # name: # type: sq_string # elements: 1 # length: 3621 statistics: h = rjtest (x) statistics: h = rjtest (x, name, value) statistics: [h, p] = rjtest (...) statistics: [h, p, rjstat, critval] = rjtest (...) Ryan-Joiner hypothesis test of composite normality. h = rjtest (x) performs the Ryan-Joiner test of the null hypothesis that the sample in the vector x comes from a normal distribution with unknown mean and variance, against the alternative that it does not come from a normal distribution. The result h is 1 if the test rejects the null hypothesis at the 5% significance level, and 0 otherwise. x must be a vector of real values with at least four non-missing elements; NaN values are treated as missing and removed. The test statistic is the correlation between the sorted sample and the approximate expected normal order statistics of Blom, $$ R = \frac{\sum_{i=1}^n x_{(i)} b_i} {\sqrt{\sum_{i=1}^n (x_i - \bar{x})^2 \sum_{i=1}^n b_i^2}}, \qquad b_i = \Phi^{-1}\left( \frac{i - 3/8}{n + 1/4} \right). $$ R lies in (0,1], and small values of R are evidence against normality. R^2 is the statistic W' of the Shapiro-Francia test in swtest. To test for a lognormal distribution, test log (x). The following Name-Value pairs are supported: Name Value 'Alpha' The significance level, a double or single scalar in the range (0,1). The default is 0.05. 'MonteCarloTolerance' A positive finite double or single scalar. When given, p and critval are estimated by Monte Carlo simulation of normal samples of the same size, and enough samples are drawn for the standard error of p to fall below this value. The name may be shortened to 'MonteCarloTol'. Without 'MonteCarloTolerance' the critical value at the significance levels 0.01, 0.05 and 0.1 is the approximation of Ryan and Joiner, $$ c_\alpha (n) = \min \left( b_0 + \frac{b_1}{\sqrt{n}} + \frac{b_2}{n} + \frac{b_3}{n^2}, \; 1 \right), $$ with the coefficients Alpha b0 b1 b2 b3 0.01 0.9963 -0.0211 -1.4106 3.1791 0.05 1.0063 -0.1288 -0.6118 1.3505 0.10 1.0071 -0.1371 -0.3682 0.7780 At other significance levels the critical value is interpolated between these three with a monotone piecewise cubic (pchip), extended below 0.01 along the line through the values at 0.01 and 0.05, and above 0.1 along the line to 1 at significance level 1. The p-value is interpolated linearly in R through the points (0, 0), (c01, 0.01), (c05, 0.05), (c10, 0.1) and (1, 1), where c01, c05 and c10 are the three critical values, so away from those levels it is a rough guide only. The approximation is calibrated for samples of up to 100 values, and a larger sample gives a warning; use 'MonteCarloTolerance' for such samples, or for an accurate p-value. [h, p] = rjtest (...) also returns the p-value p of the test, the probability of observing a statistic as small as rjstat under the null hypothesis. [h, p, rjstat, critval] = rjtest (...) also returns the test statistic rjstat and the critical value critval at significance level alpha. The null hypothesis is rejected when rjstat < critval. A sample whose values are all equal has no defined statistic and is refused, as is a sample holding an infinite value, where MATLAB returns NaN for p, rjstat and critval and 1 for h. The option names are not case sensitive but must be given in full, bar 'MonteCarloTol', where MATLAB accepts any shortened name. References: T. A. Ryan and B. L. Joiner. Normal probability plots and tests for normality. Technical report, Statistics Department, The Pennsylvania State University, 1976. G. Blom. Statistical Estimates and Transformed Beta-Variables. Wiley, New York, 1958. See also: swtest, adtest, jbtest, kstest, lillietest # name: # type: sq_string # elements: 1 # length: 51 Ryan-Joiner hypothesis test of composite normality. # name: # type: sq_string # elements: 1 # length: 8 runstest # name: # type: sq_string # elements: 1 # length: 2031 statistics: h = runstest (x) statistics: h = runstest (x, v) statistics: h = runstest (x, 'ud') statistics: h = runstest (..., Name, Value) statistics: [h, pval, stats] = runstest (...) Run test for randomness in the vector x. h = runstest (x) calculates the number of runs of consecutive values above or below the mean of x and tests the null hypothesis that the values in the data vector x come in random order. h is 1 if the test rejects the null hypothesis at the 5% significance level, or 0 otherwise. h = runstest (x, v) tests the null hypothesis based on the number of runs of consecutive values above or below the specified reference value v. Values exactly equal to v are omitted. h = runstest (x, 'ud') calculates the number of runs up or down and tests the null hypothesis that the values in the data vector x follow a trend. Too few runs indicate a trend, while too many runs indicate an oscillation. Values exactly equal to the preceding value are omitted. h = runstest (..., Name, Value) specifies additional options to the above tests by one or more Name-Value pair arguments. Name Value 'alpha' the significance level. Default is 0.05. 'method' a string specifying the method used to compute the p-value of the test. It can be either 'exact' to use an exact algorithm, or 'approximate' to use a normal approximation. The default is 'exact' for runs above/below, and for runs up/down when the length of x is less than or equal to 50. When testing for runs up/down and the length of x is greater than 50, then the default is 'approximate', and the 'exact' method is not available. 'tail' a string specifying the alternative hypothesis 'both' two-tailed (default) 'left' left-tailed 'right' right-tailed If x is empty, h is 0, pval is 1, and the fields of stats are nruns and z both NaN and n1 and n0 both 0, which is also what a sample that is empty after ties are removed returns. MATLAB accepts a character array as x and returns the empty result for it; x must be numeric here. See also: signrank, signtest # name: # type: sq_string # elements: 1 # length: 40 Run test for randomness in the vector x. # name: # type: sq_string # elements: 1 # length: 8 signrank # name: # type: sq_string # elements: 1 # length: 3953 statistics: pval = signrank (x) statistics: pval = signrank (x, my) statistics: pval = signrank (x, my, Name, Value) statistics: [pval, h] = signrank (...) statistics: [pval, h, stats] = signrank (...) Wilcoxon signed rank test for median. pval = signrank (x) returns the p-value of a two-sided Wilcoxon signed rank test. It tests the null hypothesis that data in x come from a distribution with zero median at the 5% significance level under the assumption that the distribution is symmetric about its median. x must be a vector. If the second argument my is a scalar, the null hypothesis is that x has median my, whereas if my is a vector, the null hypothesis is that the distribution of x - my has zero median. pval = signrank (..., Name, Value) performs the Wilcoxon signed rank test with additional options specified by one or more of the following Name, Value pair arguments: Name Value 'alpha' A scalar value for the significance level of the test. Default is 0.05. 'tail' A character vector specifying the alternative hypothesis. It can take one of the following values: Value Description 'both' For one-sample test (my is empty or a scalar), the data in x come from a continuous distribution with median different than zero or my. For two-sample test (my is a vector), the data in x - my come from a continuous distribution with median different than zero. 'left' For one-sample test (my is empty or a scalar), the data in x come from a continuous distribution with median less than zero or my. For two-sample test (my is a vector), the data in x - my come from a continuous distribution with median less than zero. 'right' For one-sample test (my is empty or a scalar), the data in x come from a continuous distribution with median greater than zero or my. For two-sample test (my is a vector), the data in x - my come from a continuous distribution with median greater than zero. Name Value 'method' A character vector specifying the method for computing the p-value. It can take one of the following values: Value Description 'exact' Exact computation of the p-value. It is the default value for 15 of fewer observations when 'method' is not specified. 'approximate' Using normal approximation for computing the p-value. It is the default value for more than 15 observations when 'method' is not specified. [pval, h] = signrank (...) also returns a logical value indicating the test decision. If h is 0, the null hypothesis is accepted, whereas if h is 1, the null hypothesis is rejected. [pval, h, stats] = signrank (...) also returns the structure stats containing the following fields: Field Value signedrank Value of the sign rank test statistic. zval Value of the z-statistic, computed only when the 'method' is 'approximate' and empty otherwise. The field is always there. RankBiserial The matched-pairs rank-biserial correlation, the effect size of the test: the sum of the signed ranks of the positive differences less that of the negative ones, divided by the sum of all the ranks, over the nonzero differences. It lies in [-1, 1]. RankBiserialCI Its two-sided confidence interval at the level 100 (1 - alpha) percent, whatever the 'tail'. The last two fields are an Octave extension. By default the interval is the normal interval of the Fisher transformed correlation, with standard error sqrt(n (n + 1) (2n + 1) / 6) / (n (n + 1) / 2) over the n nonzero differences. The R package effectsize computes the same interval but counts n over the elements of x that differ from the hypothesised median, so where some differences are zero its interval is narrower than this one. Two further options choose otherwise: Name Value 'ConfidenceIntervalType' 'exact', the default, or 'bootstrap' for the bias-corrected and accelerated bootstrap interval, resampling the pairs. 'NumBootstraps' The number of bootstrap replicates, a positive integer, 1000 by default. See also: tiedrank, signtest, runstest, ranksum, meanEffectSize # name: # type: sq_string # elements: 1 # length: 37 Wilcoxon signed rank test for median. # name: # type: sq_string # elements: 1 # length: 8 signtest # name: # type: sq_string # elements: 1 # length: 2769 statistics: pval = signtest (x) statistics: pval = signtest (x, my) statistics: pval = signtest (x, my, Name, Value) statistics: [pval, h] = signtest (...) statistics: [pval, h, stats] = signtest (...) Signed test for median. pval = signtest (x) returns the p-value of a two-sided sign test. It tests the null hypothesis that data in x come from a distribution with zero median at the 5% significance level. x must be a vector. If the second argument my is a scalar, the null hypothesis is that x has median my, whereas if my is a vector, the null hypothesis is that the distribution of x - my has zero median. pval = signtest (..., Name, Value) performs the Wilcoxon signed rank test with additional options specified by one or more of the following Name, Value pair arguments: Name Value 'alpha' A scalar value for the significance level of the test. Default is 0.05. 'tail' A character vector specifying the alternative hypothesis. It can take one of the following values: Value Description 'both' For one-sample test (my is empty or a scalar), the data in x come from a continuous distribution with median different than zero or my. For two-sample test (my is a vector), the data in x - my come from a continuous distribution with median different than zero. 'left' For one-sample test (my is empty or a scalar), the data in x come from a continuous distribution with median less than zero or my. For two-sample test (my is a vector), the data in x - my come from a continuous distribution with median less than zero. 'right' For one-sample test (my is empty or a scalar), the data in x come from a continuous distribution with median greater than zero or my. For two-sample test (my is a vector), the data in x - my come from a continuous distribution with median greater than zero. Name Value 'method' A character vector specifying the method for computing the p-value. It can take one of the following values: Value Description 'exact' Exact computation of the p-value. It is the default value for fewer than 100 observations when 'method' is not specified. 'approximate' Using normal approximation for computing the p-value. It is the default value for 100 or more observations when 'method' is not specified. [pval, h] = signtest (...) also returns a logical value indicating the test decision. If h is 0, the null hypothesis is accepted, whereas if h is 1, the null hypothesis is rejected. [pval, h, stats] = signtest (...) also returns the structure stats containing the following fields: Field Value sign Value of the sign test statistic. zval Value of the z-statistic, computed only when the 'method' is 'approximate' and NaN otherwise, where signrank and ranksum leave it empty. The field is always there. See also: signrank, tiedrank, runstest # name: # type: sq_string # elements: 1 # length: 23 Signed test for median. # name: # type: sq_string # elements: 1 # length: 28 stats.drift.DriftDiagnostics # name: # type: sq_string # elements: 1 # length: 715 statistics: stats.drift.DriftDiagnostics The drift found between two data sets, as detectdrift returns it. A stats.drift.DriftDiagnostics object holds, for each variable compared, the metric measuring how far the target data has moved from the baseline data, the p-value of the permutation test of that change with its confidence interval, and the drift status the interval gives, together with the drift status of the data as a whole. Every property is read-only. detectdrift is the documented way to create one. summary tabulates the results, ecdf and histcounts return the distributions compared, and plotDriftStatus, plotEmpiricalCDF, plotHistogram and plotPermutationResults draw them. See also: detectdrift # name: # type: sq_string # elements: 1 # length: 65 The drift found between two data sets, as detectdrift returns it. # name: # type: sq_string # elements: 1 # length: 37 stats.drift.DriftDiagnostics.Baseline # name: # type: sq_string # elements: 1 # length: 91 stats.drift.DriftDiagnostics: property Baseline The baseline data, as given to detectdrift. # name: # type: sq_string # elements: 1 # length: 43 The baseline data, as given to detectdrift. # name: # type: sq_string # elements: 1 # length: 49 stats.drift.DriftDiagnostics.CategoricalVariables # name: # type: sq_string # elements: 1 # length: 126 stats.drift.DriftDiagnostics: property CategoricalVariables The indices of the variables holding levels, among those compared. # name: # type: sq_string # elements: 1 # length: 66 The indices of the variables holding levels, among those compared. # name: # type: sq_string # elements: 1 # length: 48 stats.drift.DriftDiagnostics.ConfidenceIntervals # name: # type: sq_string # elements: 1 # length: 241 stats.drift.DriftDiagnostics: property ConfidenceIntervals The 95% Clopper-Pearson confidence interval of each p-value, a two-row matrix holding the lower bounds over the upper, one column per variable, NaN where p-values were not estimated. # name: # type: sq_string # elements: 1 # length: 182 The 95% Clopper-Pearson confidence interval of each p-value, a two-row matrix holding the lower bounds over the upper, one column per variable, NaN where p-values were not estimated. # name: # type: sq_string # elements: 1 # length: 45 stats.drift.DriftDiagnostics.DriftDiagnostics # name: # type: sq_string # elements: 1 # length: 298 stats.drift.DriftDiagnostics: obj = stats.drift.DriftDiagnostics (S) Create a stats.drift.DriftDiagnostics object from the structure S detectdrift fills, holding every property and the pooled data the methods read. The documented way to reach this constructor is detectdrift. See also: detectdrift # name: # type: sq_string # elements: 1 # length: 145 Create a stats.drift.DriftDiagnostics object from the structure S detectdrift fills, holding every property and the pooled data the methods read. # name: # type: sq_string # elements: 1 # length: 40 stats.drift.DriftDiagnostics.DriftStatus # name: # type: sq_string # elements: 1 # length: 183 stats.drift.DriftDiagnostics: property DriftStatus The drift status of each variable, a string array holding "Drift", "Warning" or "Stable", missing where p-values were not estimated. # name: # type: sq_string # elements: 1 # length: 132 The drift status of each variable, a string array holding "Drift", "Warning" or "Stable", missing where p-values were not estimated. # name: # type: sq_string # elements: 1 # length: 43 stats.drift.DriftDiagnostics.DriftThreshold # name: # type: sq_string # elements: 1 # length: 102 stats.drift.DriftDiagnostics: property DriftThreshold The threshold below which a p-value means drift. # name: # type: sq_string # elements: 1 # length: 48 The threshold below which a p-value means drift. # name: # type: sq_string # elements: 1 # length: 41 stats.drift.DriftDiagnostics.MetricValues # name: # type: sq_string # elements: 1 # length: 135 stats.drift.DriftDiagnostics: property MetricValues The metric of each variable between the baseline and the target data, a row vector. # name: # type: sq_string # elements: 1 # length: 83 The metric of each variable between the baseline and the target data, a row vector. # name: # type: sq_string # elements: 1 # length: 36 stats.drift.DriftDiagnostics.Metrics # name: # type: sq_string # elements: 1 # length: 104 stats.drift.DriftDiagnostics: property Metrics The metric each variable was measured by, a string array. # name: # type: sq_string # elements: 1 # length: 57 The metric each variable was measured by, a string array. # name: # type: sq_string # elements: 1 # length: 51 stats.drift.DriftDiagnostics.MultipleTestCorrection # name: # type: sq_string # elements: 1 # length: 142 stats.drift.DriftDiagnostics: property MultipleTestCorrection The correction for testing every variable, "Bonferroni" or "FalseDiscoveryRate". # name: # type: sq_string # elements: 1 # length: 80 The correction for testing every variable, "Bonferroni" or "FalseDiscoveryRate". # name: # type: sq_string # elements: 1 # length: 52 stats.drift.DriftDiagnostics.MultipleTestDriftStatus # name: # type: sq_string # elements: 1 # length: 175 stats.drift.DriftDiagnostics: property MultipleTestDriftStatus The drift status of the data as a whole, from the corrected p-values, missing where p-values were not estimated. # name: # type: sq_string # elements: 1 # length: 112 The drift status of the data as a whole, from the corrected p-values, missing where p-values were not estimated. # name: # type: sq_string # elements: 1 # length: 44 stats.drift.DriftDiagnostics.NumPermutations # name: # type: sq_string # elements: 1 # length: 157 stats.drift.DriftDiagnostics: property NumPermutations The number of permutations drawn for each variable, a row vector, 1 where p-values were not estimated. # name: # type: sq_string # elements: 1 # length: 102 The number of permutations drawn for each variable, a row vector, 1 where p-values were not estimated. # name: # type: sq_string # elements: 1 # length: 41 stats.drift.DriftDiagnostics.NumVariables # name: # type: sq_string # elements: 1 # length: 85 stats.drift.DriftDiagnostics: property NumVariables The number of variables compared. # name: # type: sq_string # elements: 1 # length: 33 The number of variables compared. # name: # type: sq_string # elements: 1 # length: 36 stats.drift.DriftDiagnostics.PValues # name: # type: sq_string # elements: 1 # length: 129 stats.drift.DriftDiagnostics: property PValues The p-value of each variable, a row vector, NaN where p-values were not estimated. # name: # type: sq_string # elements: 1 # length: 82 The p-value of each variable, a row vector, NaN where p-values were not estimated. # name: # type: sq_string # elements: 1 # length: 47 stats.drift.DriftDiagnostics.PermutationResults # name: # type: sq_string # elements: 1 # length: 231 stats.drift.DriftDiagnostics: property PermutationResults A table with one row per variable, named by it, holding in its one variable PermutationResults a cell with the metric over every permutation, the observed arrangement first. # name: # type: sq_string # elements: 1 # length: 173 A table with one row per variable, named by it, holding in its one variable PermutationResults a cell with the metric over every permutation, the observed arrangement first. # name: # type: sq_string # elements: 1 # length: 35 stats.drift.DriftDiagnostics.Target # name: # type: sq_string # elements: 1 # length: 87 stats.drift.DriftDiagnostics: property Target The target data, as given to detectdrift. # name: # type: sq_string # elements: 1 # length: 41 The target data, as given to detectdrift. # name: # type: sq_string # elements: 1 # length: 42 stats.drift.DriftDiagnostics.VariableNames # name: # type: sq_string # elements: 1 # length: 152 stats.drift.DriftDiagnostics: property VariableNames The names of the variables compared, a string array, "x1", "x2", ... where the data came as arrays. # name: # type: sq_string # elements: 1 # length: 95 The names of the variables compared, a string array, "x1", "x2", where the data came as arrays. # name: # type: sq_string # elements: 1 # length: 45 stats.drift.DriftDiagnostics.WarningThreshold # name: # type: sq_string # elements: 1 # length: 108 stats.drift.DriftDiagnostics: property WarningThreshold The threshold below which a p-value means a warning. # name: # type: sq_string # elements: 1 # length: 52 The threshold below which a p-value means a warning. # name: # type: sq_string # elements: 1 # length: 33 stats.drift.DriftDiagnostics.ecdf # name: # type: sq_string # elements: 1 # length: 675 stats.drift.DriftDiagnostics: tbl = ecdf (obj) stats.drift.DriftDiagnostics: tbl = ecdf (obj, 'Variable', var) The empirical distribution functions of the baseline and the target. tbl holds one row per variable, named by it, and three variables of cells: x, the pooled values sorted with the first repeated, and F_Baseline and F_Target, the two distribution functions there, starting from 0. Where values tie, only the last copy takes the full value, so that the points draw the two functions as steps. A variable holding levels has NaN in each cell. var names the variables, by name or index, all of them by default. See also: stats.drift.DriftDiagnostics.plotEmpiricalCDF # name: # type: sq_string # elements: 1 # length: 68 The empirical distribution functions of the baseline and the target. # name: # type: sq_string # elements: 1 # length: 39 stats.drift.DriftDiagnostics.histcounts # name: # type: sq_string # elements: 1 # length: 737 stats.drift.DriftDiagnostics: tbl = histcounts (obj) stats.drift.DriftDiagnostics: tbl = histcounts (obj, 'Variable', var) The histograms of the baseline and the target, in percent. tbl holds one row per variable, named by it, and three variables of cells: Bins, and Counts_Baseline and Counts_Target, each the percentage of its sample in each bin. For a continuous variable Bins holds the bin edges histcounts chooses for the two samples pooled. For a variable holding levels it holds the levels as a categorical array, and each count is increased by 0.5 before the percentages are taken, as the metrics take them. var names the variables, by name or index, all of them by default. See also: stats.drift.DriftDiagnostics.plotHistogram # name: # type: sq_string # elements: 1 # length: 58 The histograms of the baseline and the target, in percent. # name: # type: sq_string # elements: 1 # length: 44 stats.drift.DriftDiagnostics.plotDriftStatus # name: # type: sq_string # elements: 1 # length: 720 stats.drift.DriftDiagnostics: plotDriftStatus (obj) stats.drift.DriftDiagnostics: h = plotDriftStatus (obj) Plot the p-value and confidence interval of every variable. Each variable is drawn as its p-value with its confidence interval as horizontal error bars, one set of bars for each drift status, the variables in alphabetical order up the vertical axis, with a line at each threshold. h holds the three sets of bars, for "Stable", "Warning" and "Drift" in turn, each spanning every variable with NaN where a variable has another status. MATLAB draws on a categorical axis, which Octave does not have, so the variables sit at 1, 2, ... with their names as tick labels. See also: stats.drift.DriftDiagnostics.summary # name: # type: sq_string # elements: 1 # length: 59 Plot the p-value and confidence interval of every variable. # name: # type: sq_string # elements: 1 # length: 45 stats.drift.DriftDiagnostics.plotEmpiricalCDF # name: # type: sq_string # elements: 1 # length: 554 stats.drift.DriftDiagnostics: plotEmpiricalCDF (obj) stats.drift.DriftDiagnostics: plotEmpiricalCDF (obj, 'Variable', var) stats.drift.DriftDiagnostics: h = plotEmpiricalCDF (...) Plot the empirical distribution functions of one continuous variable. The baseline and the target are drawn as steps, from the points ecdf returns. var names the variable, by name or index; by default it is the one with the smallest p-value, or the first where p-values were not estimated. h holds the two lines, baseline first. See also: stats.drift.DriftDiagnostics.ecdf # name: # type: sq_string # elements: 1 # length: 69 Plot the empirical distribution functions of one continuous variable. # name: # type: sq_string # elements: 1 # length: 42 stats.drift.DriftDiagnostics.plotHistogram # name: # type: sq_string # elements: 1 # length: 621 stats.drift.DriftDiagnostics: plotHistogram (obj) stats.drift.DriftDiagnostics: plotHistogram (obj, 'Variable', var) stats.drift.DriftDiagnostics: h = plotHistogram (...) Plot the histograms of one variable in the baseline and the target. The percentages histcounts returns are drawn as bars side by side, over the bins of a continuous variable or the levels of one holding levels. var names the variable, by name or index; by default it is the one with the smallest p-value, or the first where p-values were not estimated. h holds the two sets of bars, baseline first. See also: stats.drift.DriftDiagnostics.histcounts # name: # type: sq_string # elements: 1 # length: 67 Plot the histograms of one variable in the baseline and the target. # name: # type: sq_string # elements: 1 # length: 51 stats.drift.DriftDiagnostics.plotPermutationResults # name: # type: sq_string # elements: 1 # length: 677 stats.drift.DriftDiagnostics: plotPermutationResults (obj) stats.drift.DriftDiagnostics: plotPermutationResults (obj, 'Variable', var) stats.drift.DriftDiagnostics: h = plotPermutationResults (...) Plot the metric of one variable over its permutations. The percentage of permutations in each bin is drawn as bars, split at the observed metric, which is marked by a line: the bars below it and those at or above it, whose share is the p-value. var names the variable, by name or index; by default it is the one with the smallest p-value. h holds the two sets of bars, those below first. MATLAB draws them as histogram objects, which Octave does not have. See also: detectdrift # name: # type: sq_string # elements: 1 # length: 54 Plot the metric of one variable over its permutations. # name: # type: sq_string # elements: 1 # length: 36 stats.drift.DriftDiagnostics.summary # name: # type: sq_string # elements: 1 # length: 509 stats.drift.DriftDiagnostics: summary (obj) stats.drift.DriftDiagnostics: tbl = summary (obj) Tabulate the drift found. Where p-values were estimated, summary prints the drift status of the data as a whole and a table with one row per variable holding DriftStatus, PValue and ConfidenceInterval. tbl is that table with a last row, MultipleTest, holding the status of the data as a whole and NaN for its p-value and interval. Where they were not, the table holds MetricValue and Metric. See also: detectdrift # name: # type: sq_string # elements: 1 # length: 25 Tabulate the drift found. # name: # type: sq_string # elements: 1 # length: 6 swtest # name: # type: sq_string # elements: 1 # length: 3452 statistics: h = swtest (x) statistics: h = swtest (x, name, value) statistics: [h, p] = swtest (...) statistics: [h, p, swstat, critval] = swtest (...) Shapiro-Wilk hypothesis test of composite normality. h = swtest (x) performs the Shapiro-Wilk test of the null hypothesis that the sample in the vector x comes from a normal distribution with unknown mean and variance, against the alternative that it does not come from a normal distribution. The result h is 1 if the test rejects the null hypothesis at the 5% significance level, and 0 otherwise. x must be a vector of real values; NaN values are treated as missing and removed. The test statistic is $$ W = \frac{\left( \sum_{i=1}^n a_i x_{(i)} \right)^2} {\sum_{i=1}^n (x_i - \bar{x})^2}, $$ where the weights a, applied to the sorted sample, are derived from the expected values of the order statistics of a standard normal sample of size n. W lies in (0,1], and small values of W are evidence against normality. The following Name-Value pairs are supported: Name Value 'Alpha' The significance level, a scalar in the range (0,1). The default is 0.05. 'Method' The test to perform: 'shapiro-wilk' (default) or 'shapiro-francia'. 'shapiro-wilk' computes the weights and the p-value with Royston's algorithm AS R94, which approximates the weights of Shapiro and Wilk and transforms W to a normal deviate. It is the algorithm used by R's shapiro.test, and it accepts samples of 3 to 5000 values. For n = 3 the p-value is exact. 'shapiro-francia' performs the Shapiro-Francia test instead, whose statistic W' is the squared correlation between the sorted sample and the approximate expected normal order statistics norminv (((1:n) - 3/8) / (n + 1/4)). Its p-value is Royston's normal approximation for W', as in sf.test of R's nortest package, and it accepts samples of 5 to 5000 values. The Shapiro-Francia test is known to be more powerful than the Shapiro-Wilk test against leptokurtic alternatives. The test is never selected automatically; the method used is the one requested. [h, p] = swtest (...) also returns the p-value p of the test, the probability of observing a statistic as small as swstat under the null hypothesis. [h, p, swstat, critval] = swtest (...) also returns the test statistic swstat, W or W', and the critical value critval at significance level alpha, obtained by inverting the same approximation. The null hypothesis is rejected when swstat < critval, which is the same as p < alpha. A sample whose values are all equal has no defined statistic and is refused, as is a sample holding an infinite value. Samples of more than 5000 values are refused, since Royston's approximations are calibrated up to that size; use adtest or jbtest for larger samples. MATLAB has no Shapiro-Wilk test, so swtest is specific to Octave. References: S. S. Shapiro and M. B. Wilk. An analysis of variance test for normality (complete samples). Biometrika, 52(3-4):591--611, 1965. S. S. Shapiro and R. S. Francia. An approximate analysis of variance test for normality. Journal of the American Statistical Association, 67(337):215--216, 1972. P. Royston. A pocket-calculator algorithm for the Shapiro-Francia test for non-normality: an application to medicine. Statistics in Medicine, 12(2):181--184, 1993. P. Royston. Remark AS R94: a remark on algorithm AS 181: the W-test for normality. Applied Statistics, 44(4):547--551, 1995. See also: adtest, jbtest, kstest, lillietest # name: # type: sq_string # elements: 1 # length: 52 Shapiro-Wilk hypothesis test of composite normality. # name: # type: sq_string # elements: 1 # length: 5 ttest # name: # type: sq_string # elements: 1 # length: 2267 statistics: [h, pval, ci, stats] = ttest (x) statistics: [h, pval, ci, stats] = ttest (x, m) statistics: [h, pval, ci, stats] = ttest (x, y) statistics: [h, pval, ci, stats] = ttest (x, m, Name, Value) statistics: [h, pval, ci, stats] = ttest (x, y, Name, Value) Test for mean of a normal sample with unknown variance. Perform a t-test of the null hypothesis mean (x) == m for a sample x from a normal distribution with unknown mean and unknown standard deviation. Under the null, the test statistic t has a Student's t distribution. The default value of m is 0. If the second argument y is a vector, a paired-t test of the hypothesis mean (x) = mean (y) is performed. If x and y are vectors, they must have the same size and dimensions. x (and y) can also be matrices. For matrices, ttest performs separate t-tests along each column, and returns a vector of results. x and y must have the same number of columns. The Type I error rate of the resulting vector of pval can be controlled by entering pval as input to the function multcompare. ttest treats NaNs as missing values, and ignores them. Name-Value pair arguments can be used to set various options. 'alpha' can be used to specify the significance level of the test (the default value is 0.05). 'tail', can be used to select the desired alternative hypotheses. If the value is 'both' (default) the null is tested against the two-sided alternative mean (x) != m. If it is 'right' the one-sided alternative mean (x) > m is considered. Similarly for 'left', the one-sided alternative mean (x) < m is considered. When argument x is a matrix, 'dim' can be used to select the dimension over which to perform the test. (The default is the first non-singleton dimension). If h is 1 the null hypothesis is rejected, meaning that the tested sample does not come from a Student's t distribution. If h is 0, then the null hypothesis cannot be rejected and it can be assumed that x follows a Student's t distribution. The p-value of the test is returned in pval. A 100(1-alpha)% confidence interval is returned in ci. stats is a structure containing the value of the test statistic (tstat), the degrees of freedom (df) and the sample's standard deviation (sd). See also: hotelling_t2test, ttest2, hotelling_t2test2 # name: # type: sq_string # elements: 1 # length: 55 Test for mean of a normal sample with unknown variance. # name: # type: sq_string # elements: 1 # length: 6 ttest2 # name: # type: sq_string # elements: 1 # length: 1824 statistics: [h, pval, ci, stats] = ttest2 (x, y) statistics: [h, pval, ci, stats] = ttest2 (x, y, Name, Value) Perform a t-test to compare the means of two groups of data under the null hypothesis that the groups are drawn from distributions with the same mean. x and y can be vectors or matrices. For matrices, ttest2 performs separate t-tests along each column, and returns a vector of results. x and y must have the same number of columns. The Type I error rate of the resulting vector of pval can be controlled by entering pval as input to the function multcompare. ttest2 treats NaNs as missing values, and ignores them. For a nested t-test, use anova2. The argument 'alpha' can be used to specify the significance level of the test (the default value is 0.05). The string argument 'tail', can be used to select the desired alternative hypotheses. If 'tail' is 'both' (default) the null is tested against the two-sided alternative mean (x) != m. If 'tail' is 'right' the one-sided alternative mean (x) > m is considered. Similarly for 'left', the one-sided alternative mean (x) < m is considered. When 'vartype' is 'equal' the variances are assumed to be equal (this is the default). When 'vartype' is 'unequal' the variances are not assumed equal. When argument x and y are matrices the 'dim' argument can be used to select the dimension over which to perform the test. (The default is the first non-singleton dimension.) If h is 0 the null hypothesis is accepted, if it is 1 the null hypothesis is rejected. The p-value of the test is returned in pval. A 100(1-alpha)% confidence interval is returned in ci. stats is a structure containing the value of the test statistic (tstat), the degrees of freedom (df) and the sample standard deviation (sd). See also: hotelling_t2test, anova1, hotelling_t2test2, ttest # name: # type: sq_string # elements: 1 # length: 150 Perform a t-test to compare the means of two groups of data under the null hypothesis that the groups are drawn from distributions with the same mean. # name: # type: sq_string # elements: 1 # length: 7 vartest # name: # type: sq_string # elements: 1 # length: 1816 statistics: h = vartest (x, v) statistics: h = vartest (x, v, name, value) statistics: [h, pval] = vartest (...) statistics: [h, pval, ci] = vartest (...) statistics: [h, pval, ci, stats] = vartest (...) One-sample test of variance. h = vartest (x, v) performs a chi-square test of the hypothesis that the data in the vector x come from a normal distribution with variance v, against the alternative that x comes from a normal distribution with a different variance. The result is h = 0 if the null hypothesis ("variance is V") cannot be rejected at the 5% significance level, or h = 1 if the null hypothesis can be rejected at the 5% level. x may also be a matrix or an N-D array. For matrices, vartest performs separate tests along each column of x, and returns a vector of results. For N-D arrays, vartest works along the first non-singleton dimension of x. v must be a scalar. vartest treats NaNs as missing values, and ignores them. [h, pval] = vartest (...) returns the p-value. That is the probability of observing the given result, or one more extreme, by chance if the null hypothesis true. [h, pval, ci] = vartest (...) returns a 100 * (1 - alpha)% confidence interval for the true variance. [h, pval, ci, stats] = vartest (...) returns a structure with the following fields: chisqstat the value of the test statistic df the degrees of freedom of the test [...] = vartest (..., name, value), ... specifies one or more of the following name/value pairs: Name Value 'alpha' the significance level. Default is 0.05. 'dim' dimension to work along a matrix or an N-D array. 'tail' a string specifying the alternative hypothesis 'both' variance is not v (two-tailed, default) 'left' variance is less than v (left-tailed) 'right' variance is greater than v (right-tailed) See also: ttest, ztest, kstest # name: # type: sq_string # elements: 1 # length: 28 One-sample test of variance. # name: # type: sq_string # elements: 1 # length: 8 vartest2 # name: # type: sq_string # elements: 1 # length: 1972 statistics: h = vartest2 (x, y) statistics: h = vartest2 (x, y, name, value) statistics: [h, pval] = vartest2 (...) statistics: [h, pval, ci] = vartest2 (...) statistics: [h, pval, ci, stats] = vartest2 (...) Two-sample F test for equal variances. h = vartest2 (x, y) performs an F test of the hypothesis that the independent data in vectors x and y come from normal distributions with equal variance, against the alternative that they come from normal distributions with different variances. The result is h = 0 if the null hypothesis ("variance are equal") cannot be rejected at the 5% significance level, or h = 1 if the null hypothesis can be rejected at the 5% level. x and y may also be matrices or N-D arrays. For matrices, vartest2 performs separate tests along each column and returns a vector of results. For N-D arrays, vartest2 works along the first non-singleton dimension and x and y must have the same size along all the remaining dimensions. vartest2 treats NaNs as missing values, and ignores them. [h, pval] = vartest2 (...) returns the p-value. That is the probability of observing the given result, or one more extreme, by chance if the null hypothesis true. [h, pval, ci] = vartest2 (...) returns a 100 x (1 - alpha)% confidence interval for the true ratio var(X)/var(Y). [h, pval, ci, stats] = vartest2 (...) returns a structure with the following fields: fstat the value of the test statistic df1 the numerator degrees of freedom of the test df2 the denominator degrees of freedom of the test [...] = vartest2 (..., name, value), ... specifies one or more of the following name/value pairs: Name Value 'alpha' the significance level. Default is 0.05. 'dim' dimension to work along a matrix or an N-D array. 'tail' a string specifying the alternative hypothesis 'both' variance is not v (two-tailed, default) 'left' variance is less than v (left-tailed) 'right' variance is greater than v (right-tailed) See also: ttest2, kstest2, vartestn # name: # type: sq_string # elements: 1 # length: 38 Two-sample F test for equal variances. # name: # type: sq_string # elements: 1 # length: 8 vartestn # name: # type: sq_string # elements: 1 # length: 2740 statistics: vartestn (x) statistics: vartestn (x, group) statistics: vartestn (..., name, value) statistics: p = vartestn (...) statistics: [p, stats] = vartestn (...) statistics: [p, stats] = vartestn (..., name, value) Test for equal variances across multiple groups. h = vartestn (x) performs Bartlett's test for equal variances for the columns of the matrix x. This is a test of the null hypothesis that the columns of x come from normal distributions with the same variance, against the alternative that they come from normal distributions with different variances. The result is displayed in a summary table of statistics as well as a box plot of the groups. vartestn (x, group) requires a vector x, and a group argument that is a categorical variable, vector, string array, or cell array of strings with one row for each element of x. Values of x corresponding to the same value of group are placed in the same group. Without group, a vector x is a single group, whichever way it lies, and the test has nothing to compare, so p is NaN. vartestn treats NaNs as missing values, and ignores them. p = vartestn (...) returns the probability of observing the given result, or one more extreme, by chance under the null hypothesis that all groups have equal variances. Small values of p cast doubt on the validity of the null hypothesis. [p, stats] = vartestn (...) returns a structure with the following fields: chisqstat -- the value of Bartlett's test statistic df -- the degrees of freedom of the test [p, stats] = vartestn (..., name, value) specifies one or more of the following name/value pairs: Name Value 'display' 'on' to display a boxplot and table, or 'off' to omit these displays. Default 'on'. 'testtype' One of the following strings to control the type of test to perform 'Bartlett' Bartlett's test (default). 'LeveneQuadratic' Levene's test computed by performing anova on the squared deviations of the data values from their group means. 'LeveneAbsolute' Levene's test computed by performing anova on the absolute deviations of the data values from their group means. 'BrownForsythe' Brown-Forsythe test computed by performing anova on the absolute deviations of the data values from the group medians. 'OBrien' O'Brien's modification of Levene's test with W=0.5. The classical Bartlett's test is sensitive to the assumption that the distribution in each group is normal. The other test types are more robust to non-normal distributions, especially ones prone to outliers. For these tests, the STATS output structure has a field named fstat containing the test statistic in place of chisqstat, and df holds two elements, the numerator and the denominator degrees of freedom. See also: vartest, vartest2, anova1 # name: # type: sq_string # elements: 1 # length: 48 Test for equal variances across multiple groups. # name: # type: sq_string # elements: 1 # length: 5 ztest # name: # type: sq_string # elements: 1 # length: 1702 statistics: h = ztest (x, m, sigma) statistics: h = ztest (x, m, sigma, Name, Value) statistics: [h, pval] = ztest (...) statistics: [h, pval, ci] = ztest (...) statistics: [h, pval, ci, zvalue] = ztest (...) One-sample Z-test. h = ztest (x, v) performs a Z-test of the hypothesis that the data in the vector x come from a normal distribution with mean m, against the alternative that x comes from a normal distribution with a different mean m. The result is h = 0 if the null hypothesis ("mean is M") cannot be rejected at the 5% significance level, or h = 1 if the null hypothesis can be rejected at the 5% level. x may also be a matrix or an N-D array. For matrices, ztest performs separate tests along each column of x, and returns a vector of results. For N-D arrays, ztest works along the first non-singleton dimension of x. m and sigma must be scalars. ztest treats NaNs as missing values, and ignores them. [h, pval] = ztest (...) returns the p-value. That is the probability of observing the given result, or one more extreme, by chance if the null hypothesis true. [h, pval, ci] = ztest (...) returns a 100 * (1 - alpha)% confidence interval for the true mean. [h, pval, ci, zvalue] = ztest (...) returns the value of the test statistic. [...] = ztest (..., Name, Value, ...) specifies one or more of the following Name/Value pairs: Name Value "alpha" the significance level. Default is 0.05. "dim" dimension to work along a matrix or an N-D array. "tail" a string specifying the alternative hypothesis: "both" "mean is not m" (two-tailed, default) "left" "mean is less than m" (left-tailed) "right" "mean is greater than m" (right-tailed) See also: ttest, vartest, signtest, kstest # name: # type: sq_string # elements: 1 # length: 18 One-sample Z-test. # name: # type: sq_string # elements: 1 # length: 6 ztest2 # name: # type: sq_string # elements: 1 # length: 2127 statistics: h = ztest2 (x1, n1, x2, n2) statistics: h = ztest2 (x1, n1, x2, n2, Name, Value) statistics: [h, p] = ztest2 (...) statistics: [h, p, ci] = ztest2 (...) statistics: [h, p, ci, stats] = ztest2 (...) Two proportions Z-test. If x1 and n1 are the counts of successes and trials in one sample, and x2 and n2 those in a second one, test the null hypothesis that the success probabilities p1 and p2 are the same. The result is h = 0 if the null hypothesis cannot be rejected at the significance level alpha, or h = 1 if it can. Under the null, the test statistic approximately follows a standard normal distribution. The size of h and p is the common size of x1, n1, x2, and n2, which must be scalars or of common size. A scalar input functions as a constant matrix of the same size as the other inputs. [h, p] = ztest2 (...) returns the p-value. That is the probability of observing the given result, or one more extreme, by chance if the null hypothesis true. [h, p, ci] = ztest2 (...) returns a 100 (1 - alpha)% confidence interval for p1 - p2, Newcombe's hybrid score interval, which combines the Wilson score intervals of the two proportions. It has one row per test, and is one-sided for a one-sided test: [lower, 1] for 'right', [-1, upper] for 'left'. [h, p, ci, stats] = ztest2 (...) also returns a structure with the following fields: zval the value of the test statistic. CohensH Cohen's h, the difference between the arcsine transforms 2 asin (sqrt (p1)) - 2 asin (sqrt (p2)) of the two estimated proportions. CohensHCI its confidence interval, h -/+ z sqrt (1/n1 + 1/n2) with z the normal quantile at the level of the test, one row per test, within [-pi, pi] and one-sided as ci is. [...] = ztest2 (..., Name, Value, ...) specifies one or more of the following Name/Value pairs: Name Value 'alpha' the significance level of h and the level of both confidence intervals. Default is 0.05. 'tail' a string specifying the alternative hypothesis 'both' p1 is not p2 (two-tailed, default) 'left' p1 is less than p2 (left-tailed) 'right' p1 is greater than p2 (right-tailed) See also: chi2test, fishertest # name: # type: sq_string # elements: 1 # length: 23 Two proportions Z-test. statistics-release-2.1.2/inst/Hypothesis_Testing/dwtest.m000066400000000000000000000265301527311104300236300ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} dwtest (@var{r}, @var{x}) ## @deftypefnx {statistics} {@var{p} =} dwtest (@var{r}, @var{x}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{p}, @var{d}] =} dwtest (@dots{}) ## ## Durbin-Watson test for autocorrelation in linear regression residuals. ## ## @code{@var{p} = dwtest (@var{r}, @var{x})} performs the Durbin-Watson test on ## the residuals @var{r} of a linear regression with design matrix @var{x} ## (which should include a column of ones if the model has a constant term). ## The null hypothesis is that the residuals are uncorrelated, against the ## alternative that they are autocorrelated. @var{r} is an @math{N*1} vector ## and @var{x} is an @math{N*P} matrix. @var{p} is the p-value of the test. ## ## The Durbin-Watson statistic is ## @tex ## $$ d = \sum_{i=1}^{n-1} (r_{i+1} - r_i)^2 / \sum_{i=1}^{n} r_i^2. $$ ## ## @end tex ## @ifnottex ## @code{@var{d} = sum ((diff (@var{r})) .^ 2) / sum (@var{r} .^ 2)}. ## @end ifnottex ## Values near 2 indicate no autocorrelation, values towards 0 positive ## autocorrelation, and values towards 4 negative autocorrelation. ## ## @code{@var{p} = dwtest (@var{r}, @var{x}, @var{name}, @var{value})} specifies ## additional options using @qcode{Name-Value} pair arguments: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Method'} @tab @qcode{'exact'} to compute the exact p-value from ## the null distribution of the statistic (a ratio of quadratic forms, evaluated ## with Imhof's method), or @qcode{'approximate'} to use a normal approximation ## based on the mean and variance of the statistic. The default is ## @qcode{'exact'} for @math{n < 400} and @qcode{'approximate'} otherwise. ## ## @item @qcode{'Tail'} @tab The alternative hypothesis: @qcode{'both'} ## (default) for a nonzero autocorrelation, @qcode{'right'} for a positive ## autocorrelation, or @qcode{'left'} for a negative autocorrelation. ## @end multitable ## ## @code{[@var{p}, @var{d}] = dwtest (@dots{})} also returns the Durbin-Watson ## statistic @var{d}. ## ## If @var{x} has rank equal to the number of observations, no residual degrees ## of freedom remain and @var{d} has no null distribution. In that case ## @var{p} is 0 for @qcode{'right'} and @qcode{'both'} and 1 for ## @qcode{'left'} with the exact method, and @qcode{NaN} with the approximate ## method. A @var{p} of exactly 0 or 1 signals this rather than strong ## evidence. ## ## If @var{r} is empty, @var{p} is 0 and @var{d} is @qcode{NaN}. MATLAB ## returns @var{d} = 0 for some empty inputs, a @math{0*3} array among them, ## and @qcode{NaN} for @code{[]} and for zero-length vectors; @var{d} is ## @qcode{NaN} here for every empty @var{r}, the statistic being undefined in ## all of them alike. MATLAB also accepts a character array as @var{r} and ## returns the empty result for it; @var{r} must be numeric here. ## ## @seealso{regress, fitlm, runstest} ## @end deftypefn function [pval, d] = dwtest (r, x, varargin) if (nargin < 2) print_usage (); endif if (! (isnumeric (r) && isreal (r) && (isvector (r) || isempty (r)))) error ("dwtest: R must be a real vector of residuals."); endif r = r(:); n = numel (r); if (! (isnumeric (x) && isreal (x) && ismatrix (x) && rows (x) == n)) error ("dwtest: X must be a real matrix with one row per residual."); endif ## Parse Name-Value pairs method = ""; tail = "both"; if (mod (numel (varargin), 2) != 0) error ("dwtest: optional arguments must be given as Name-Value pairs."); endif for k = 1:2:numel (varargin) if (! ischar (varargin{k})) error ("dwtest: parameter names must be character vectors."); endif switch (lower (varargin{k})) case "method" method = lower (varargin{k+1}); case "tail" tail = lower (varargin{k+1}); otherwise error ("dwtest: unknown parameter name '%s'.", varargin{k}); endswitch endfor if (isempty (method)) method = ifelse (n < 400, "exact", "approximate"); endif if (! any (strcmp (method, {"exact", "approximate"}))) error ("dwtest: 'Method' must be 'exact' or 'approximate'."); endif if (! any (strcmp (tail, {"both", "right", "left"}))) error ("dwtest: 'Tail' must be 'both', 'right', or 'left'."); endif ## With no observations D is undefined and has no null distribution. MATLAB ## returns d = 0 for some empty inputs, a 0-by-3 array among them, and NaN ## for [] and for zero-length vectors; we return NaN for every empty R, the ## statistic being undefined in all of them alike. if (n == 0) pval = 0; d = NaN; return; endif ## Durbin-Watson statistic d = sum (diff (r) .^ 2) / sum (r .^ 2); ## Orthonormal basis of the column space of X (residual-maker M = I - Q*Q') [Q, R] = qr (x, 0); keep = abs (diag (R)) > max (size (x)) * eps (max (abs (diag (R)))); Q = Q(:, keep); p = columns (Q); ## Durbin-Watson difference operator A (so that r'*A*r = sum (diff (r) .^ 2)) A = 2 * eye (n); A(1,1) = 1; A(n,n) = 1; A -= diag (ones (n-1, 1), 1) + diag (ones (n-1, 1), -1); ## Lower-tail probability P(D <= d) under the null hypothesis. With no ## residual degrees of freedom M is the zero matrix and D has no null ## distribution; the exact method reports 0 there and the approximate one ## NaN, as MATLAB does. if (strcmp (method, "exact")) if (n - p <= 0) plow = 0; else M = eye (n) - Q * Q'; MAM = M * A * M; nu = sort (eig ((MAM + MAM') / 2), "descend"); nu = nu(1:n - p); ## n - p nonzero eigenvalues of M*A*M plow = dwtest_imhof_ (nu, d); endif plow = min (max (plow, 0), 1); ## an undefined D maps to 0 or 1 here else if (n - p <= 0) plow = NaN; else ## Mean and variance of D from traces, avoiding the eigendecomposition AQ = A * Q; QtAQ = Q' * A * Q; ## tr (M*A) and tr ((M*A) ^ 2) sumnu = (2 * n - 2) - trace (QtAQ); sumnu2 = (6 * n - 8) - 2 * sum (AQ(:) .^ 2) + sum (QtAQ(:) .^ 2); m1 = sumnu / (n - p); s2 = 2 * (sumnu2 - sumnu ^ 2 / (n - p)) / ((n - p) * (n - p + 2)); plow = normcdf ((d - m1) / sqrt (s2)); endif if (! isnan (plow)) ## an undefined D stays NaN here plow = min (max (plow, 0), 1); endif endif ## Alternative hypothesis: 'right' -> positive autocorrelation (small D), ## 'left' -> negative autocorrelation (large D) switch (tail) case "right" pval = plow; case "left" pval = 1 - plow; case "both" pval = 2 * min (plow, 1 - plow); endswitch if (! isnan (pval)) pval = min (max (pval, 0), 1); endif endfunction ## Imhof's method for P(sum ((nu_i - d) * chi2_1) <= 0), which equals the ## lower-tail probability P(D <= d) of the Durbin-Watson statistic. function plow = dwtest_imhof_ (nu, d) lam = nu - d; I = quadgk (@(u) dwtest_imhof_integrand_ (u, lam), 0, Inf, ... "AbsTol", 1e-10, "RelTol", 1e-9); plow = 0.5 - I / pi; endfunction function y = dwtest_imhof_integrand_ (u, lam) sz = size (u); uv = u(:).'; ## row vector of quadrature nodes lam = lam(:); ## column vector of coefficients theta = 0.5 * sum (atan (lam * uv), 1); rho = prod ((1 + (lam .^ 2) * (uv .^ 2)) .^ 0.25, 1); y = sin (theta) ./ (uv .* rho); y(uv == 0) = 0.5 * sum (lam); ## limit of the integrand as u -> 0 y = reshape (y, sz); ## match the shape quadgk expects endfunction function out = ifelse (cond, a, b) if (cond) out = a; else out = b; endif endfunction %!demo %! ## Test regression residuals for autocorrelation %! x = [ones(20, 1), (1:20)']; %! y = x * [1; 0.5] + sin ((1:20)' / 2); # add an autocorrelated component %! b = x \ y; %! r = y - x * b; %! [p, d] = dwtest (r, x) ## Test the statistic value %!test %! x = [ones(6, 1), (1:6)']; %! r = [1; -1; 1; -1; 1; -1]; # strong negative autocorrelation %! [p, d] = dwtest (r, x); %! assert_equal (d, sum (diff (r) .^ 2) / sum (r .^ 2), 1e-12); %! assert_equal (d, 20 / 6, 1e-12); %! assert_equal (p >= 0 && p <= 1, true); %!test %! ## Edge cases with empty arrays %! [p, d] = dwtest ([], []); %! assert_equal (p, 0); %! assert_equal (isnan (d), true); %!test %! [p, d] = dwtest (zeros (0, 3), zeros (0, 3)); %! assert_equal (p, 0); %! assert_equal (isnan (d), true); %!test %! [p, d] = dwtest (zeros (0, 1), zeros (0, 2)); %! assert_equal (p, 0); %! assert_equal (isnan (d), true); %!test # exact and approximate methods give similar p-values %! x = [ones(30, 1), (1:30)', ((1:30)') .^ 2]; %! r = sin ((1:30)' / 3); %! pe = dwtest (r, x, "Method", "exact"); %! pa = dwtest (r, x, "Method", "approximate"); %! assert_equal (pe, pa, 0.05); %!test # tail selection is consistent %! x = [ones(15, 1), (1:15)']; %! r = (-1) .^ (1:15)'; # alternating -> D near 4 %! pr = dwtest (r, x, "Tail", "right"); %! pl = dwtest (r, x, "Tail", "left"); %! pb = dwtest (r, x, "Tail", "both"); %! assert_equal (pr + pl, 1, 1e-10); %! assert_equal (pb, 2 * min (pr, pl), 1e-10); %!test # left tail rejects strong negative autocorrelation (D near 4) %! x = [ones(20, 1), (1:20)']; %! r = (-1) .^ (1:20)'; %! assert_equal (dwtest (r, x, "Tail", "left") < 0.05, true); %!test # zero residual degrees of freedom (n == rank(x)) %! x = [1 0 0; 0 1 0; 0 0 1]; %! r = [0.5; -0.3; 0.8]; %! assert_equal (dwtest (r, x, "Tail", "right"), 0); %! assert_equal (dwtest (r, x, "Tail", "left"), 1); %! assert_equal (dwtest (r, x, "Tail", "both"), 0); %!test # zero residual degrees of freedom, approximate method %! x = [1 0 0; 0 1 0; 0 0 1]; %! r = [0.5; -0.3; 0.8]; %! assert_equal (isnan (dwtest (r, x, "Method", "approximate")), true); %!test # the statistic is returned with no residual degrees of freedom %! x = [1 0 0; 0 1 0; 0 0 1]; %! r = [0.5; -0.3; 0.8]; %! [p, d] = dwtest (r, x); %! assert_equal (d, 1.8878, 1e-4); ## Test input validation %!error dwtest (1) %!error dwtest (ones (3, 3), ones (3, 2)) %!error dwtest ('', []) %!error ... %! dwtest ([1;2;3], ones (2, 2)) %!error ... %! dwtest ([1;2;3], ones (3, 1), "Tail") %!error ... %! dwtest ([1;2;3], ones (3, 1), "foo", "bar") %!error ... %! dwtest ([1;2;3], ones (3, 1), "Method", "fast") %!error ... %! dwtest ([1;2;3], ones (3, 1), "Tail", "up") statistics-release-2.1.2/inst/Hypothesis_Testing/fishertest.m000066400000000000000000000215141527311104300244730ustar00rootroot00000000000000## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} fishertest (@var{x}) ## @deftypefnx {statistics} {@var{h} =} fishertest (@var{x}, @var{param1}, @var{value1}, @dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}] =} fishertest (@dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}, @var{stats}] =} fishertest (@dots{}) ## ## Fisher's exact test. ## ## @code{@var{h} = fishertest (@var{x})} performs Fisher's exact test on a ## @math{2*2} contingency table given in matrix @var{x}. This is a test of the ## hypothesis that there are no non-random associations between the two 2-level ## categorical variables in @var{x}. @code{fishertest} returns the result of ## the tested hypothesis in @var{h}. @var{h} = 0 indicates that the null ## hypothesis (of no association) cannot be rejected at the 5% significance ## level. @var{h} = 1 indicates that the null hypothesis can be rejected at the ## 5% level. @var{x} must contain only non-negative integers. Use the ## @code{crosstab} function to generate the contingency table from samples of ## two ## categorical variables. Fisher's exact test is not suitable when all integers ## in @var{x} are very large. User can use the Chi-square test in this case. ## ## @code{[@var{h}, @var{pval}] = fishertest (@var{x})} returns the p-value in ## @var{pval}. That is the probability of observing the given result, or one ## more extreme, by chance if the null hypothesis is true. Small values of ## @var{pval} cast doubt on the validity of the null hypothesis. ## ## @code{[@var{p}, @var{pval}, @var{stats}] = fishertest (@dots{})} returns the ## structure @var{stats} with the following fields: ## ## @multitable @columnfractions 0.3 0.65 ## @item @qcode{OddsRatio} @tab -- the odds ratio ## @item @qcode{ConfidenceInterval} @tab -- the asymptotic confidence ## interval for the odds ratio. If any of the four entries in the contingency ## table @var{x} is zero, the confidence interval will not be computed, and ## @qcode{[-Inf Inf]} will be displayed. ## @end multitable ## ## @code{[@dots{}] = fishertest (@dots{}, @var{name}, @var{value}, @dots{})} ## specifies one or more of the following name/value pairs: ## ## @multitable @columnfractions 0.2 0.75 ## @headitem Name @tab Value ## @item @qcode{'alpha'} @tab the significance level. Default is 0.05. ## ## @item @qcode{'tail'} @tab a string specifying the alternative hypothesis ## @end multitable ## @multitable @columnfractions 0.25 0.65 ## @item @qcode{'both'} @tab odds ratio not equal to 1, indicating ## association between two variables (two-tailed test, default) ## @item @qcode{'left'} @tab odds ratio greater than 1 (right-tailed test) ## @item @qcode{'right'} @tab odds ratio is less than 1 (left-tailed test) ## @end multitable ## ## @seealso{crosstab, chi2test, mcnemar_test, ztest2} ## @end deftypefn function [h, p, stats] = fishertest (x, varargin) if (nargin < 1) error ("fishertest: contingency table is missing."); endif if (nargin > 5) error ("fishertest: too many input parameters."); endif ## Check contingency table if (! ismatrix (x) || ndims (x) != 2) error ("fishertest: X must be a 2-dimensional matrix."); endif if (any (x(:) < 0) || any (isnan (x(:))) || any (isinf (x(:))) || ... iscomplex (x) || any (fix (x(:)) != x(:))) error ("fishertest: X must contain only non-negative real integers."); endif if (all (x(:) >= 1e7)) error ("fishertest: cannot handle large entries (>=1e7)."); endif ## Add defaults and parse optional arguments alpha = 0.05; tail = 'both'; if (nargin > 1) params = numel (varargin); if ((params / 2) != fix (params / 2)) error ("fishertest: optional arguments must be in Name-Value pairs.") endif for idx = 1:2:params name = varargin{idx}; value = varargin{idx+1}; switch (lower (name)) case 'alpha' alpha = value; if (! isscalar (alpha) || ! isnumeric (alpha) || ... alpha <= 0 || alpha >= 1) error ("fishertest: invalid value for alpha."); endif case 'tail' tail = value; if (! any (strcmpi (tail, {'both', 'left', 'right'}))) error ("fishertest: invalid value for tail."); endif otherwise error ("fishertest: invalid name for optional arguments."); endswitch endfor endif ## For 2x2 contingency table apply Fisher's exact test ## For larger tables apply the Fisher-Freeman-Halton variance if (all (size (x) == 2)) ## Get margin sums r1 = sum (x(1,:)); r2 = sum (x(2,:)); c1 = sum (x(:,1)); c2 = sum (x(:,2)); sz = sum (x(:)); ## Use try_catch block to avoid memory overflow for large numbers try if (strcmp (tail, 'left')) p = hygecdf (x(1,1), sz, r1, c1); else if (min (r1, c1) <= min (r2, c2)) x11 = (0 : min (r1, c1))'; else x22 = (0 : min (r2, c2))'; x12 = c2 - x22; x11 = r1 - x12; endif switch tail case 'both' p1 = hygepdf (x11, sz, r1, c1); p2 = hygepdf (x(1,1), sz, r1, c1); p = sum (p1(p1 < p2 + 10 * eps (p2))); case 'right' xr = x11(x11 >= x(1,1)); p = sum (hygepdf (xr,sz,r1,c1)); endswitch endif catch error ("fishertest: cannot handle large entries."); end_try_catch ## Return test decision h = (p <= alpha); ## Calculate extra output arguments (if necessary) if (nargout > 2) OR = x(1,1) * x(2,2) / x(1,2) / x(2,1); if (any (x(:) == 0)) CI = [-Inf, Inf]; else SE = sqrt (1 / x(1,1) + 1 / x(1,2) + 1 / x(2,1) + 1 / x(2,2)); LB = OR * exp (-norminv (1 - alpha / 2) * SE); UB = OR * exp (norminv (1 - alpha / 2) * SE); CI = [LB, UB]; endif stats = struct ('OddsRatio', OR, 'ConfidenceInterval', CI); endif else error ("fishertest: the Fisher-Freeman-Halton test is not implemented yet."); endif endfunction %!demo %! ## A Fisher's exact test example %! %! x = [3, 1; 1, 3] %! [h, p, stats] = fishertest (x) ## Test output against MATLAB R2018 %!assert_equal (fishertest ([3, 4; 5, 7]), false); %!assert_equal (isa (fishertest ([3, 4; 5, 7]), 'logical'), true); %!test %! [h, pval, stats] = fishertest ([3, 4; 5, 7]); %! assert_equal (pval, 1, 1e-14); %! assert_equal (stats.OddsRatio, 1.05); %! CI = [0.159222057151289, 6.92429189601808]; %! assert_equal (stats.ConfidenceInterval, CI, 1e-14) %!test %! [h, pval, stats] = fishertest ([3, 4; 5, 0]); %! assert_equal (pval, 0.08080808080808080, 1e-14); %! assert_equal (stats.OddsRatio, 0); %! assert_equal (stats.ConfidenceInterval, [-Inf, Inf]) ## Test input validation %!error fishertest (); %!error fishertest (1, 2, 3, 4, 5, 6); %!error ... %! fishertest (ones (2, 2, 2)); %!error ... %! fishertest ([1, 2; -3, 4]); %!error ... %! fishertest ([1, 2; 3, 4+i]); %!error ... %! fishertest ([1, 2; 3, 4.2]); %!error ... %! fishertest ([NaN, 2; 3, 4]); %!error ... %! fishertest ([1, Inf; 3, 4]); %!error ... %! fishertest (ones (2) * 1e8); %!error ... %! fishertest ([1, 2; 3, 4], 'alpha', 0); %!error ... %! fishertest ([1, 2; 3, 4], 'alpha', 1.2); %!error ... %! fishertest ([1, 2; 3, 4], 'alpha', 'val'); %!error ... %! fishertest ([1, 2; 3, 4], 'tail', 'val'); %!error ... %! fishertest ([1, 2; 3, 4], 'alpha', 0.01, 'tail', 'val'); %!error ... %! fishertest ([1, 2; 3, 4], 'alpha', 0.01, 'badoption', 3); statistics-release-2.1.2/inst/Hypothesis_Testing/friedman.m000066400000000000000000000446071527311104300241100ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} friedman (@var{x}) ## @deftypefnx {statistics} {@var{p} =} friedman (@var{x}, @var{reps}) ## @deftypefnx {statistics} {@var{p} =} friedman (@var{x}, @var{reps}, @var{displayopt}) ## @deftypefnx {statistics} {@var{p} =} friedman (@var{x}, @var{reps}, @var{displayopt}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{p}, @var{tbl}] =} friedman (@dots{}) ## @deftypefnx {statistics} {[@var{p}, @var{tbl}, @var{stats}] =} friedman (@dots{}) ## ## Performs the nonparametric Friedman's test to compare column effects in a ## two-way layout. @qcode{friedman} tests the null hypothesis that the column ## effects are all the same against the alternative that they are not all the ## same. ## ## @qcode{friedman} requires one up to three input arguments: ## ## @itemize ## @item ## @var{x} contains the data and it must be a matrix of at least two columns and ## two rows. ## @item ## @var{reps} is the number of replicates for each combination of factor groups. ## If not provided, no replicates are assumed. ## @item ## @var{displayopt} is an optional parameter for displaying the Friedman's ANOVA ## table, when it is 'on' (default) and suppressing the display when it is ## 'off'. MATLAB renders the table in a figure window; this package prints it ## to the standard output, as @code{anova2} does. ## @end itemize ## ## @qcode{friedman} returns up to three output arguments: ## ## @itemize ## @item ## @var{p} is the p-value of the null hypothesis that all group means are equal. ## @item ## @var{tbl} is a cell array containing the results of the Friedman's test in ## ANOVA table format. Its first row holds the column labels Source, SS, df, ## MS, Chi-sq and Prob>Chi-sq, followed by a row per source: Columns, ## [Interaction], Error and Total. An entry that does not apply to a row, such ## as the chi-square statistic of the Error row, is empty. ## @item ## @var{stats} is a structure containing statistics useful for performing a ## multiple comparison of medians with the MULTCOMPARE function. ## @end itemize ## ## @var{stats} also holds the effect size of the test, an Octave extension: ## @qcode{KendallsW}, Kendall's coefficient of concordance, and ## @qcode{KendallsWCI}, its two-sided confidence interval at the level ## @math{100 (1 - alpha)} percent. With @math{b} blocks of @math{r} ## replicates over @math{c} columns, @math{W = Q / Q_max}, where @math{Q} is ## the test statistic, corrected for ties, and ## @math{Q_max = b r^2 (c^2 - 1) / (r c + 1)} the value it takes when every ## block ranks the columns in the same order; without replicates this is the ## usual @math{W = Q / (b (c - 1))}. By default the interval inverts the ## noncentral chi-square distribution of @math{Q} on @math{c - 1} degrees of ## freedom, taken as @math{Q}'s distribution under the alternative, which it ## is for many blocks, and bounds the concordance beyond chance, ## @math{lambda / Q_max} for the noncentrality @math{lambda}, which is 0 ## where the columns do not differ. @math{W} itself includes the agreement ## expected by chance, averaging @math{(c - 1) / Q_max}, which is @math{1/b} ## without replicates, where the columns do not differ; the estimate of the ## concordance beyond chance that the interval matches is ## @code{max (W - (c - 1) / Q_max, 0)}. The bootstrap interval bounds ## @math{W} itself. Three options after @var{displayopt}, also an Octave ## extension, set the interval: ## ## @multitable @columnfractions 0.32 0.66 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'Alpha'} @tab The significance level of the interval, a ## scalar between 0 and 1, 0.05 by default. ## @item @qcode{'ConfidenceIntervalType'} @tab @qcode{'exact'}, the default, ## or @qcode{'bootstrap'} for the bias-corrected and accelerated bootstrap ## interval, resampling the blocks. ## @item @qcode{'NumBootstraps'} @tab The number of bootstrap replicates, a ## positive integer, 1000 by default. ## @end multitable ## ## If friedman is called without any output arguments, then it prints the ## results in a Friedman's ANOVA table to the standard output. ## ## Examples: ## ## @example ## load popcorn; ## friedman (popcorn, 3); ## @end example ## ## ## @example ## [p, anovatab, stats] = friedman (popcorn, 3); ## disp (p); ## @end example ## ## @seealso{anova2, kruskalwallis, multcompare} ## @end deftypefn function [p, tbl, stats] = friedman (x, reps, displayopt, varargin) ## Check for valid number of input arguments if (nargin < 1) print_usage (); endif [alpha, citype, nboot, args] = parsePairedArguments ... ({'Alpha', 'ConfidenceIntervalType', 'NumBootstraps'}, ... {0.05, 'exact', 1000}, varargin(:)); if (! isempty (args)) error ("friedman: invalid optional paired argument."); endif if (! (isnumeric (alpha) && isreal (alpha) && isscalar (alpha) && alpha > 0 && alpha < 1)) error ("friedman: 'Alpha' must be a scalar between 0 and 1."); endif if (! (ischar (citype) && any (strcmpi (citype, {'exact', 'bootstrap'})))) error (strcat ("friedman: 'ConfidenceIntervalType' must be 'exact'", ... " or 'bootstrap'.")); endif if (! (isnumeric (nboot) && isreal (nboot) && isscalar (nboot) && isfinite (nboot) && nboot >= 1 && nboot == fix (nboot))) error ("friedman: 'NumBootstraps' must be a positive integer."); endif ## Check for NaN values in X if (any (isnan (x(:)))) error ("friedman: NaN values in input are not allowed."); endif ## Add defaults if (nargin == 1) reps = 1; endif ## Check for correct size of input matrix [r, c] = size (x); if (r <= 1 || c <= 1) error ("friedman: bad size of input matrix."); endif if (reps > 1) r = r / reps; if (floor (r) != r) error ("friedman: repetitions and observations do not match."); endif endif ## Check for displayopt. It is 'on' by default, as it is in MATLAB and in ## ANOVA1 and ANOVA2. disp_table = true; if (nargin >= 3) if (! any (strcmp (displayopt, {'on', 'off'}))) error ("friedman: displayopt must be either 'on' or 'off'."); endif disp_table = strcmp (displayopt, 'on'); endif ## Prepare a matrix of ranks. Replicates are ranked together. m = x; sum_R = 0; for j = 1:r jrows = reps * (j - 1) + (1:reps); v = x(jrows,:); [R, tieadj] = tiedrank (v(:)); m(jrows,:) = reshape (R, reps, c); sum_R = sum_R + 2 * tieadj; endfor ## Perform 2-way anova silently [p0, anova_table] = anova2 (m, reps, 'off'); ## Compute Friedman test statistic and p-value chi_r = anova_table{2,2}; sigmasq = c * reps * (reps * c + 1) / 12; if (sum_R > 0) sigmasq = sigmasq - sum_R / (12 * r * (reps * c - 1)); endif if (chi_r > 0) chi_r = chi_r / sigmasq; endif p = chi2cdf (chi_r, c - 1, 'upper'); ## Create ANOVA table data for output if (reps > 1) ## When there are replicates, include interaction row ## ANOVA2 reports Columns, Rows, Interaction, Error and Total; the ## Friedman table carries the interaction, row 4, and not the block ## effect in row 3. source_list = {'Columns'; 'Interaction'; 'Error'; 'Total'}; ss_list = [anova_table{2,2}; anova_table{4,2}; ... anova_table{end - 1,2}; anova_table{end,2}]; df_list = [anova_table{2,3}; anova_table{4,3}; ... anova_table{end - 1,3}; anova_table{end,3}]; ms_list = {anova_table{2,4}; anova_table{4,4}; ... anova_table{end - 1,4}; []}; chi_sq_list = {chi_r; []; []; []}; prob_list = {p; []; []; []}; else ## When there are no replicates (reps = 1), exclude interaction row source_list = {'Columns'; 'Error'; 'Total'}; ss_list = [anova_table{2,2}; anova_table{end - 1,2}; anova_table{end,2}]; df_list = [anova_table{2,3}; anova_table{end - 1,3}; anova_table{end,3}]; ms_list = {anova_table{2,4}; anova_table{end - 1,4}; []}; chi_sq_list = {chi_r; []; []}; prob_list = {p; []; []}; endif ## Create the output table as a cell array with a header row, as MATLAB ## does. Entries that do not apply to a row are empty, not zero, and the ## column labels carry the characters a table variable name cannot hold. tbl = [{'Source', 'SS', 'df', 'MS', 'Chi-sq', 'Prob>Chi-sq'}; ... [source_list, num2cell(ss_list), num2cell(df_list), ... ms_list, chi_sq_list, prob_list]]; ## Create stats structure (if requested) for MULTCOMPARE if (nargout > 2) stats.source = 'friedman'; stats.n = r; stats.meanranks = mean (m); stats.sigma = sqrt (sigmasq); ## Kendall's W, Q over the value it takes under complete concordance, ## and its interval, which bounds the concordance beyond chance, the ## noncentrality over Qmax qmax = r * reps ^ 2 * (c ^ 2 - 1) / (reps * c + 1); stats.KendallsW = chi_r / qmax; if (strcmpi (citype, 'exact')) stats.KendallsWCI = min (__ncx2bounds__ (chi_r, c - 1, alpha) / qmax, 1); else ## Each block as one row, its replicates of a column side by side B = zeros (r, reps * c); for j = 1:r B(j,:) = reshape (x(reps * (j - 1) + (1:reps),:), 1, []); endfor stats.KendallsWCI = __bootci__ (@(varargin) kendallsW (c, reps, ... varargin{:}), num2cell (B, 1), ... alpha, nboot, 'rows'); endif endif ## Display ANOVA table if opted or no output argument is requested. MATLAB ## renders it in a figure window; this package prints it, as ANOVA2 does. if (nargout == 0 || disp_table) print_friedman_table (tbl); endif endfunction ## Kendall's W of blocks resampled as rows: argument k holds position k of ## every block, a column's replicates side by side, and each of its columns ## is one replicate of the blocks function w = kendallsW (c, reps, varargin) [b, R] = size (varargin{1}); q = c * reps; qmax = b * reps ^ 2 * (c ^ 2 - 1) / (reps * c + 1); col = ceil ((1:q) / reps); w = zeros (1, R); for k = 1:R M = cell2mat (cellfun (@(v) v(:,k), varargin, 'UniformOutput', false)); ## Midranks within each block, and the number tied with each value A = permute (M, [1, 3, 2]); E = squeeze (sum (M == A, 2)); rk = squeeze (sum (M < A, 2)) + (E + 1) / 2; if (b == 1) E = E'; rk = rk'; endif ties = sum (E(:) .^ 2 - 1); sigmasq = q * (q + 1) / 12; if (ties > 0) sigmasq -= ties / (12 * b * (q - 1)); endif m = accumarray (col', mean (rk, 1)') / reps; Q = b * reps * sum ((m - (q + 1) / 2) .^ 2) / sigmasq; w(k) = Q / qmax; endfor endfunction ## Print the ANOVA table the way ANOVA2 prints its own: one header line, then ## a row per source, with an empty field where the statistic does not apply. function print_friedman_table (tbl) printf ("\n"); printf ("%-14s %10s %6s %10s %10s %12s\n", tbl{1,:}); for i = 2:rows (tbl) printf ("%-14s", tbl{i,1}); for j = 2:columns (tbl) v = tbl{i,j}; if (isempty (v)) printf (" %10s", ""); elseif (j == 3) printf (" %6d", v); elseif (j == 6) printf (" %12.4f", v); else printf (" %10.4f", v); endif endfor printf ("\n"); endfor printf ("\n"); endfunction %!demo %! load popcorn; %! friedman (popcorn, 3); %!demo %! load popcorn; %! [p, atab] = friedman (popcorn, 3, 'off'); %! disp (p); ## testing against popcorn data and results from Matlab %!test %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! [p, atab] = friedman (popcorn, 3, 'off'); %! assert_equal (p, 0.001028853354594794, 1e-14); %! assert_equal (atab(1,:), {'Source', 'SS', 'df', 'MS', 'Chi-sq', 'Prob>Chi-sq'}); %! assert_equal (atab{2,1}, 'Columns'); %! assert_equal (atab{2,2}, 99.75, 1e-14); %! assert_equal (atab{2,3}, 2, 0); %! assert_equal (atab{2,4}, 49.875, 1e-14); %! assert_equal (atab{2,5}, 13.75862068965517, 1e-14); %! assert_equal (atab{2,6}, 0.001028853354594794, 1e-14); %!test %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! [p, atab, stats] = friedman (popcorn, 3, 'off'); %! assert_equal (atab{end,1}, 'Total'); %! assert_equal (atab{end,2}, 116, 0); %! assert_equal (atab{end,3}, 17, 0); %! assert_equal (stats.source, 'friedman'); %! assert_equal (stats.n, 2); %! assert_equal (stats.meanranks, [8, 4.75, 2.25], 0); %! assert_equal (stats.sigma, 2.692582403567252, 1e-14); %!test %! ## every row of the table, not only Columns and Total %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! [p, atab] = friedman (popcorn, 3, 'off'); %! assert_equal (atab{3,1}, 'Interaction'); %! assert_equal (atab{3,2}, 0.083333333333258, 1e-12); %! assert_equal (atab{3,3}, 2, 0); %! assert_equal (atab{3,4}, 0.041666666666629, 1e-12); %! assert_equal (atab{4,1}, 'Error'); %! assert_equal (atab{4,2}, 16.166666666666742, 1e-12); %! assert_equal (atab{4,3}, 12, 0); %! assert_equal (atab{4,4}, 1.347222222222229, 1e-12); %!test %! ## the interaction carries (c-1)(r-1) degrees of freedom %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! [p, atab] = friedman (popcorn, 3, 'off'); %! c = 3; r = 2; %! assert_equal (atab{3,3}, (c - 1) * (r - 1), 0); %! assert_equal (atab{2,3} + atab{3,3} + atab{4,3} < atab{5,3}, true); %!test %! ## without replicates the table has no interaction row %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! [p, atab] = friedman (popcorn, 1, 'off'); %! assert_equal (atab(:,1), {'Source'; 'Columns'; 'Error'; 'Total'}); %! assert_equal (atab{2,2}, 12, 1e-12); %! assert_equal (atab{3,2}, 0, 1e-12); %! assert_equal (atab{3,3}, 10, 0); %! assert_equal (atab{4,3}, 17, 0); %!test %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! s = evalc ('[p, atab] = friedman (popcorn, 3, "off");'); %! assert_equal (isempty (strtrim (s)), true); %!test %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! s = evalc ('[p, atab] = friedman (popcorn, 3, "on");'); %! assert_equal (! isempty (strtrim (s)), true); %!test %! ## the table is displayed by default, as in MATLAB and in anova1 and anova2 %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! s = evalc ('[p, atab] = friedman (popcorn, 3);'); %! assert_equal (! isempty (strtrim (s)), true); %!test %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! [p, atab] = friedman (popcorn, 3, 'off'); %! assert_equal (size (atab), [5, 6], 0); %! assert_equal (iscell (atab), true); %! assert_equal (isempty (atab{end,4}), true); %!test %! x = [1, 2, 3; 2, 1, 3; 3, 2, 1]; %! [p, atab] = friedman (x, 1, 'off'); %! assert_equal (size (atab), [4, 6], 0); %! assert_equal (atab{3,1}, 'Error'); %! assert_equal (isempty (atab{2,5}), false); %!test %! ## Below the resolution of 1 - chi2cdf, values from MATLAB R2024a %! q = (1:60)'; %! p = friedman ([q, q + 100, q + 200], 1, 'off'); %! assert_equal (p, 8.75651076269649e-27, -1e-10); ## Effect size against the R package effectsize 1.0.3, kendalls_w %!test %! x = [1, 2, 3, 4; 2, 2, 3, 1; 4, 3, 2, 1; 1, 3, 3, 4; 2, 1, 4, 3; 3, 3, 1, 2]; %! [~, ~, stats] = friedman (x, 1, 'off'); %! assert_equal (stats.KendallsW, 0.02046783625730994, -1e-13); %! ## Q falls below the upper 2.5% point of the central distribution, so the %! ## concordance beyond chance may be 0 %! assert_equal (stats.KendallsWCI(1), 0); %! assert_equal (stats.KendallsWCI(2), 0.0906309740164431, -1e-10); %!test %! x = [1, 2, 3, 4; 1, 3, 2, 4; 2, 1, 3, 4; 1, 2, 4, 3; 1, 2, 3, 4]; %! [~, tbl, stats] = friedman (x, 1, 'off'); %! assert_equal (stats.KendallsW, 0.776, -1e-14); %! lambda = stats.KendallsWCI(1) * 15; %! assert_equal (ncx2cdf (tbl{2,5}, 3, lambda), 0.975, -1e-10); %! assert_equal (stats.KendallsWCI(2), 1); %! ## The estimate of the concordance beyond chance lies within it %! w = stats.KendallsW - 3 / 15; %! assert_equal (w > stats.KendallsWCI(1) && w < stats.KendallsWCI(2), true); %!test %! load popcorn; %! [~, tbl, stats] = friedman (popcorn, 3, 'off'); %! assert_equal (stats.KendallsW, tbl{2,5} / (2 * 9 * 8 / 10), -1e-14); %!test %! rand ('state', 1); %! x = [1, 2, 3, 4; 1, 3, 2, 4; 2, 1, 3, 4; 1, 2, 4, 3; 1, 2, 3, 4; ... %! 2, 1, 3, 4; 1, 2, 3, 4; 1, 3, 2, 4]; %! [~, ~, stats] = friedman (x, 1, 'off', 'ConfidenceIntervalType', ... %! 'bootstrap', 'NumBootstraps', 200); %! ci = stats.KendallsWCI; %! assert_equal ([ci(1) <= ci(2), ci(1) >= 0, ci(2) <= 1], true (1, 3)); %!error ... %! friedman ([5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; 6.5, 5.0, 4.0; ... %! 7.0, 5.5, 5.0; 7.0, 5.0, 4.5], 3, 'invalid_displayopt'); %!error ... %! friedman ([1, 2; NaN, 4]); %!error ... %! friedman ([1,2; 3,4; 5,6], 2); %!error ... %! friedman ([1, 2; 3, 4; 5, 6], 1, 'off', 'Foo', 1) %!error ... %! friedman ([1, 2; 3, 4; 5, 6], 1, 'off', 'Alpha', 0) %!error ... %! friedman ([1, 2; 3, 4; 5, 6], 1, 'off', 'ConfidenceIntervalType', 'none') %!error ... %! friedman ([1, 2; 3, 4; 5, 6], 1, 'off', 'NumBootstraps', -1) statistics-release-2.1.2/inst/Hypothesis_Testing/hotelling_t2test.m000066400000000000000000000221431527311104300256040ustar00rootroot00000000000000## Copyright (C) 1996-2017 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{h}, @var{pval}, @var{stats}] =} hotelling_t2test (@var{x}) ## @deftypefnx {statistics} {[@dots{}] =} hotelling_t2test (@var{x}, @var{m}) ## @deftypefnx {statistics} {[@dots{}] =} hotelling_t2test (@var{x}, @var{y}) ## @deftypefnx {statistics} {[@dots{}] =} hotelling_t2test (@var{x}, @var{m}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@dots{}] =} hotelling_t2test (@var{x}, @var{y}, @var{Name}, @var{Value}) ## ## Compute Hotelling's T^2 ("T-squared") test for a single sample or two ## dependent samples (paired-samples). ## ## For a sample @var{x} from a multivariate normal distribution with unknown ## mean and covariance matrix, test the null hypothesis that ## @code{mean (@var{x}) == @var{m}}. ## ## For two dependent samples @var{x} and @var{y} from a multivariate normal ## distributions with unknown means and covariance matrices, test the null ## hypothesis that @code{mean (@var{x} - @var{y}) == 0}. ## ## @qcode{hotelling_t2test} treats NaNs as missing values, and ignores the ## corresponding rows. ## ## Name-Value pair arguments can be used to set statistical significance. ## @qcode{'alpha'} can be used to specify the significance level of the test ## and the level of the confidence interval (the default value is 0.05). ## ## If @var{h} is 1 the null hypothesis is rejected, meaning that the tested ## sample does not come from a multivariate distribution with mean @var{m}, or ## in case of two dependent samples that they do not come from the same ## multivariate distribution. If @var{h} is 0, then the null hypothesis cannot ## be rejected and it can be assumed that it holds true. ## ## The p-value of the test is returned in @var{pval}. ## ## @var{stats} is a structure containing the value of the Hotelling's @math{T^2} ## test statistic in the field "t2stat", its F transform in "fstat", and the ## degrees of freedom of the F distribution in the fields "df1" and "df2". ## Under the null hypothesis, @math{(n-p) T^2 / (p(n-1))} has an F ## distribution with @math{p} and @math{n-p} degrees of freedom, where ## @math{n} and @math{p} are the numbers of samples and variables, ## respectively. ## ## The effect size is the Mahalanobis distance @math{D} between the mean and ## @var{m}, in the field "MahalanobisD", with its @math{100 (1 - alpha)}% ## confidence interval in "MahalanobisDCI". The noncentrality of the F ## statistic is @math{n D^2}; the distance is estimated from ## @math{max (F df1 (df2 - 2) / df2 - df1, 0)}, unbiased for the ## noncentrality before its truncation at zero, and the interval inverts ## the noncentral F distribution. Where the interval would need a ## noncentrality above @math{10^5}, where @code{ncfcdf} loses accuracy, it ## is @qcode{NaN}. ## ## @seealso{hotelling_t2test2} ## @end deftypefn function [h, pval, stats] = hotelling_t2test (x, my, varargin) ## Check for minimum number of input arguments if (nargin < 1) print_usage (); endif ## Check X being a valid data set if (isscalar (x) || ndims (x) > 2) error ("hotelling_t2test: X must be a vector or a 2D matrix."); endif ## Set default arguments alpha = 0.05; ## Fix MY when X is a single input argument if (nargin == 1) if (isvector (x)) my = 0; elseif (ismatrix (x)) [n, p] = size (x); my = zeros (1, p); endif endif ## When X and MY are of equal size, then assume paired-sample if (isequal (size (x), size (my))) x = x - my; if (isvector (x)) my = 0; elseif (ismatrix (x)) [n, p] = size (x); my = zeros (1, p); endif endif ## Remove rows containing any NaNs x = rmmissing (x); ## Check additional options i = 1; while (i <= length (varargin)) switch lower (varargin{i}) case 'alpha' i = i + 1; alpha = varargin{i}; ## Check for valid alpha if (! isscalar (alpha) || ! isnumeric (alpha) || ... alpha <= 0 || alpha >= 1) error ("hotelling_t2test: invalid value for alpha."); endif otherwise error ("hotelling_t2test: invalid Name argument."); endswitch i = i + 1; endwhile ## Conditional error checking for X being a vector or matrix if (isvector (x)) if (! isscalar (my)) error ("hotelling_t2test: if X is a vector, M must be a scalar."); endif n = length (x); p = 1; elseif (ismatrix (x)) [n, p] = size (x); if (n <= p) error ("hotelling_t2test: X must have more rows than columns."); endif if (isvector (my) && length (my) == p) my = reshape (my, 1, p); else error (strcat ("hotelling_t2test: if X is a matrix, M must be a", ... " vector of length equal to the columns of X.")); endif endif ## Calculate the necessary statistics d = mean (x) - my; stats.t2stat = n * d * (cov (x) \ d'); stats.fstat = (n - p) * stats.t2stat / (p * (n - 1)); stats.df1 = p; stats.df2 = n - p; pval = fcdf (stats.fstat, stats.df1, stats.df2, 'upper'); ## Mahalanobis distance between the mean and M: the noncentrality of F is ## n D^2, estimated without bias and bounded by inverting its distribution [lambda, lambdahat] = __ncfbounds__ (stats.fstat, stats.df1, stats.df2, ... alpha); stats.MahalanobisD = sqrt (lambdahat / n); stats.MahalanobisDCI = sqrt (lambda / n); ## Determine the test outcome ## MATLAB returns this a double instead of a logical array h = double (pval < alpha); endfunction %!test %! ## Below the resolution of 1 - fcdf %! u = (1:30)'; %! [~, p, st] = hotelling_t2test ([u, sin(u)] + 100); %! F = (30 - 2) * st.t2stat / (2 * 29); %! d1 = st.df1; %! d2 = st.df2; %! assert_equal (p, betainc (d2 / (d2 + d1 * F), d2 / 2, d1 / 2), -1e-12); %!test %! u = (1:30)'; %! [~, ~, st] = hotelling_t2test ([u / 30, sin(u)] + [0.3, 0.2]); %! assert_equal (st.fstat, (30 - 2) * st.t2stat / (2 * 29), -1e-14); %!test %! ## The bounds are those of R's pf with ncp, inverted by uniroot %! u = (1:30)'; %! [~, ~, st] = hotelling_t2test ([u / 30, sin(u)] + [0.3, 0.2]); %! assert_equal (st.MahalanobisD, 2.6950446312948171, -1e-12); %!test %! u = (1:30)'; %! [~, ~, st] = hotelling_t2test ([u / 30, sin(u)] + [0.3, 0.2]); %! assert_equal (st.MahalanobisDCI, ... %! [1.9814725140791032, 3.6141479420615656], -1e-8); %!test %! u = (1:30)'; %! [~, ~, st] = hotelling_t2test ([u / 30, sin(u)] + [0.3, 0.2], [0, 0], ... %! 'alpha', 0.01); %! assert_equal (st.MahalanobisDCI, ... %! [1.7453752693724904, 3.8881257650035739], -1e-7); %!test %! ## Beyond a noncentrality of 1e5 the interval is not computed %! u = (1:30)'; %! [~, ~, st] = hotelling_t2test ([u / 30, sin(u)] + 100); %! assert_equal (st.MahalanobisDCI, [NaN, NaN]); ## Test input validation %!error hotelling_t2test (); %!error ... %! hotelling_t2test (1); %!error ... %! hotelling_t2test (ones (2,2,2)); %!error ... %! hotelling_t2test (ones (20,2), [0, 0], 'alpha', 1); %!error ... %! hotelling_t2test (ones (20,2), [0, 0], 'alpha', -0.2); %!error ... %! hotelling_t2test (ones (20,2), [0, 0], 'alpha', 'a'); %!error ... %! hotelling_t2test (ones (20,2), [0, 0], 'alpha', [0.01, 0.05]); %!error ... %! hotelling_t2test (ones (20,2), [0, 0], 'name', 0.01); %!error ... %! hotelling_t2test (ones (20,1), [0, 0]); %!error ... %! hotelling_t2test (ones (4,5), [0, 0, 0, 0, 0]); %!error ... %! hotelling_t2test (ones (20,5), [0, 0, 0, 0]); ## Test results %!test %! randn ('seed', 1); %! x = randn (50000, 5); %! [h, pval, stats] = hotelling_t2test (x); %! assert_equal (h, 0); %! assert_equal (stats.df1, 5); %! assert_equal (stats.df2, 49995); %!test %! randn ('seed', 1); %! x = randn (50000, 5); %! [h, pval, stats] = hotelling_t2test (x, ones (1, 5) * 10); %! assert_equal (h, 1); %! assert_equal (stats.df1, 5); %! assert_equal (stats.df2, 49995); statistics-release-2.1.2/inst/Hypothesis_Testing/hotelling_t2test2.m000066400000000000000000000204771527311104300256760ustar00rootroot00000000000000## Copyright (C) 1996-2017 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{h}, @var{pval}, @var{stats}] =} hotelling_t2test2 (@var{x}, @var{y}) ## @deftypefnx {statistics} {[@dots{}] =} hotelling_t2test2 (@var{x}, @var{y}, @var{Name}, @var{Value}) ## ## Compute Hotelling's T^2 ("T-squared") test for two independent samples. ## ## For two samples @var{x} from multivariate normal distributions with ## the same number of variables (columns), unknown means and unknown ## equal covariance matrices, test the null hypothesis ## @code{mean (@var{x}) == mean (@var{y})}. ## ## @qcode{hotelling_t2test2} treats NaNs as missing values, and ignores the ## corresponding rows for each sample independently. ## ## Name-Value pair arguments can be used to set statistical significance. ## @qcode{'alpha'} can be used to specify the significance level of the test ## and the level of the confidence interval (the default value is 0.05). ## ## If @var{h} is 1 the null hypothesis is rejected, meaning that the tested ## samples do not come from the same multivariate distribution. If @var{h} is ## 0, then the null hypothesis cannot be rejected and it can be assumed that ## both samples come from the same multivariate distribution. ## ## The p-value of the test is returned in @var{pval}. ## ## @var{stats} is a structure containing the value of the Hotelling's @math{T^2} ## test statistic in the field "t2stat", its F transform in "fstat", and the ## degrees of freedom of the F distribution in the fields "df1" and "df2". ## Under the null hypothesis, ## @tex ## $$ ## {(n_x+n_y-p-1) T^2 \over p(n_x+n_y-2)} ## $$ ## @end tex ## @ifnottex ## ## @example ## (n_x+n_y-p-1) T^2 / (p(n_x+n_y-2)) ## @end example ## ## @end ifnottex ## @noindent ## has an F distribution with @math{p} and @math{n_x+n_y-p-1} degrees of ## freedom, where @math{n_x} and @math{n_y} are the sample sizes and ## @math{p} is the number of variables. ## ## The effect size is the Mahalanobis distance @math{D} between the two means, ## in the field "MahalanobisD", with its @math{100 (1 - alpha)}% confidence ## interval in "MahalanobisDCI". The noncentrality of the F statistic is ## @math{n_x n_y / (n_x + n_y) D^2}; the distance is estimated from ## @math{max (F df1 (df2 - 2) / df2 - df1, 0)}, unbiased for the ## noncentrality before its truncation at zero, and the interval inverts ## the noncentral F distribution. Where the interval would need a ## noncentrality above @math{10^5}, where @code{ncfcdf} loses accuracy, it ## is @qcode{NaN}. ## ## @seealso{hotelling_t2test} ## @end deftypefn function [h, pval, stats] = hotelling_t2test2 (x, y, varargin) ## Check for minimum number of input arguments if (nargin < 2) print_usage (); endif ## Check X being a valid data set if (isscalar (x) || ndims (x) > 2) error ("hotelling_t2test2: X must be a vector or a 2D matrix."); endif ## Check Y being a valid data set if (isscalar (y) || ndims (y) > 2) error ("hotelling_t2test2: Y must be a vector or a 2D matrix."); endif ## Set default arguments alpha = 0.05; ## Remove rows containing any NaNs x = rmmissing (x); y = rmmissing (y); ## Check additional options i = 1; while (i <= length (varargin)) switch lower (varargin{i}) case 'alpha' i = i + 1; alpha = varargin{i}; ## Check for valid alpha if (! isscalar (alpha) || ! isnumeric (alpha) || ... alpha <= 0 || alpha >= 1) error ("hotelling_t2test2: invalid value for alpha."); endif otherwise error ("hotelling_t2test2: invalid Name argument."); endswitch i = i + 1; endwhile ## Conditional error checking for X being a vector or matrix if (isvector (x)) n_x = length (x); if (! isvector (y)) error ("hotelling_t2test2: if X is a vector, Y must also be a vector."); else n_y = length (y); p = 1; endif elseif (ismatrix (x)) [n_x, p] = size (x); [n_y, q] = size (y); if (p != q) error (strcat ("hotelling_t2test2: X and Y must have the same", ... " number of columns.")); endif endif ## Calculate the necessary statistics d = mean (x) - mean (y); S = ((n_x - 1) * cov (x) + (n_y - 1) * cov (y)) / (n_x + n_y - 2); stats.t2stat = (n_x * n_y / (n_x + n_y)) * d * (S \ d'); stats.fstat = (n_x + n_y - p - 1) * stats.t2stat / (p * (n_x + n_y - 2)); stats.df1 = p; stats.df2 = n_x + n_y - p - 1; pval = fcdf (stats.fstat, stats.df1, stats.df2, 'upper'); ## Mahalanobis distance between the two means: the noncentrality of F is ## n_x n_y / (n_x + n_y) D^2, estimated without bias and bounded by ## inverting its distribution m = n_x * n_y / (n_x + n_y); [lambda, lambdahat] = __ncfbounds__ (stats.fstat, stats.df1, stats.df2, ... alpha); stats.MahalanobisD = sqrt (lambdahat / m); stats.MahalanobisDCI = sqrt (lambda / m); ## Determine the test outcome ## MATLAB returns this a double instead of a logical array h = double (pval < alpha); endfunction %!test %! ## Below the resolution of 1 - fcdf %! u = (1:30)'; %! [~, p, st] = hotelling_t2test2 ([u, sin(u)], [u + 100, sin(u)]); %! F = (30 + 30 - 2 - 1) * st.t2stat / (2 * (30 + 30 - 2)); %! d1 = st.df1; %! d2 = st.df2; %! assert_equal (p, betainc (d2 / (d2 + d1 * F), d2 / 2, d1 / 2), -1e-12); %!test %! u = (1:30)'; %! [~, ~, st] = hotelling_t2test2 ([u / 30, sin(u)], ... %! [u / 30 + 0.2, sin(u) + 0.3]); %! assert_equal (st.MahalanobisD, 0.73996501021719263, -1e-12); %!test %! ## The bounds are those of R's pf with ncp, inverted by uniroot %! u = (1:30)'; %! [~, ~, st] = hotelling_t2test2 ([u / 30, sin(u)], ... %! [u / 30 + 0.2, sin(u) + 0.3]); %! assert_equal (st.MahalanobisDCI, ... %! [0.22898023040240645, 1.3346946250293135], -1e-8); ## Test input validation %!error hotelling_t2test2 (); %!error ... %! hotelling_t2test2 ([2, 3, 4, 5, 6]); %!error ... %! hotelling_t2test2 (1, [2, 3, 4, 5, 6]); %!error ... %! hotelling_t2test2 (ones (2,2,2), [2, 3, 4, 5, 6]); %!error ... %! hotelling_t2test2 ([2, 3, 4, 5, 6], 2); %!error ... %! hotelling_t2test2 ([2, 3, 4, 5, 6], ones (2,2,2)); %!error ... %! hotelling_t2test2 (ones (20,2), ones (20,2), 'alpha', 1); %!error ... %! hotelling_t2test2 (ones (20,2), ones (20,2), 'alpha', -0.2); %!error ... %! hotelling_t2test2 (ones (20,2), ones (20,2), 'alpha', 'a'); %!error ... %! hotelling_t2test2 (ones (20,2), ones (20,2), 'alpha', [0.01, 0.05]); %!error ... %! hotelling_t2test2 (ones (20,2), ones (20,2), 'name', 0.01); %!error ... %! hotelling_t2test2 (ones (20,1), ones (20,2)); %!error ... %! hotelling_t2test2 (ones (20,2), ones (25,3)); ## Test results %!test %! randn ('seed', 1); %! x1 = randn (60000, 5); %! randn ('seed', 5); %! x2 = randn (30000, 5); %! [h, pval, stats] = hotelling_t2test2 (x1, x2); %! assert_equal (h, 0); %! assert_equal (stats.df1, 5); %! assert_equal (stats.df2, 89994); statistics-release-2.1.2/inst/Hypothesis_Testing/jbtest.m000066400000000000000000000371531527311104300236140ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} jbtest (@var{x}) ## @deftypefnx {statistics} {@var{h} =} jbtest (@var{x}, @var{alpha}) ## @deftypefnx {statistics} {@var{h} =} jbtest (@var{x}, @var{alpha}, @var{mctol}) ## @deftypefnx {statistics} {[@var{h}, @var{p}] =} jbtest (@dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{p}, @var{jbstat}, @var{critval}] =} jbtest (@dots{}) ## ## Jarque-Bera hypothesis test of composite normality. ## ## @code{@var{h} = jbtest (@var{x})} performs the Jarque-Bera test of the null ## hypothesis that the sample in the vector @var{x} comes from a normal ## distribution with unknown mean and variance, against the alternative that it ## does not come from a normal distribution. The result @var{h} is 1 if the ## test rejects the null hypothesis at the 5% significance level, and 0 ## otherwise. @var{x} must be a vector of real values; @qcode{NaN} values are ## treated as missing and removed. ## ## The Jarque-Bera test statistic is ## @tex ## $$ JB = \frac{n}{6} \left( s^2 + \frac{(k-3)^2}{4} \right), $$ ## ## @end tex ## @ifnottex ## @code{JB = (n / 6) * (s^2 + (k - 3)^2 / 4)}, ## @end ifnottex ## where @math{n} is the sample size, @math{s} is the sample skewness, and ## @math{k} is the sample kurtosis. Under the null hypothesis it is ## asymptotically chi-square distributed with two degrees of freedom. ## ## @code{@var{h} = jbtest (@var{x}, @var{alpha})} performs the test at the ## significance level @var{alpha}, a scalar in the range @math{(0,1)}. The ## default is @math{0.05}. ## ## @code{@var{h} = jbtest (@var{x}, @var{alpha}, @var{mctol})} computes a ## Monte-Carlo approximation of the p-value instead of interpolating the ## embedded table. @var{mctol} is the maximum Monte-Carlo standard ## error allowed for the p-value; the number of simulated samples is chosen ## accordingly. Use this for small samples, where the chi-square approximation ## is inaccurate, or for significance levels outside @math{[0.001, 0.5]}. ## ## @code{[@var{h}, @var{p}] = jbtest (@dots{})} also returns the p-value ## @var{p} of the test. @var{p} is clamped to the tabulated range ## @math{[0.001, 0.5]}, as MATLAB clamps it, and a warning is issued when the ## value lies outside that range. The warning is an addition here: MATLAB ## clamps silently, so a p-value reported as @math{0.001} or @math{0.5} there ## may be a bound rather than an estimate, with nothing to say so. ## ## @code{[@var{h}, @var{p}, @var{jbstat}, @var{critval}] = jbtest (@dots{})} ## also returns the test statistic @var{jbstat} and the critical value ## @var{critval} at significance level @var{alpha}. The null hypothesis is ## rejected when @code{@var{jbstat} > @var{critval}}. ## ## Note: for @math{n \le 2000} the p-value and critical value are obtained by ## interpolating an embedded critical-value table (the same approach MATLAB ## uses); for larger samples the large-sample chi-square approximation with two ## degrees of freedom is used instead. The embedded table was generated here by ## Monte-Carlo simulation, so it is itself an estimate of the true null ## quantiles. MATLAB's table is likewise a Monte-Carlo estimate but from a ## different simulation, so the two tables agree only to about two decimal ## places. As a result the reported p-value and critical value, and (in a ## narrow band of statistic values around the critical value) the test decision ## @var{h}, can differ slightly from MATLAB in edge cases. These differences ## are an unavoidable consequence of the Monte-Carlo origin of both tables, not ## a difference in method. Supply @var{mctol} for a direct Monte-Carlo p-value. ## ## If @var{x} has zero variance (or is otherwise degenerate so that skewness ## and kurtosis are undefined), @var{jbstat} is @qcode{NaN}; in this case ## @var{p} is reported as @math{0} and @var{h} as @math{1}. ## ## @seealso{adtest, kstest, lillietest, swtest} ## @end deftypefn function [h, p, jbstat, critval] = jbtest (x, alpha, mctol) ## Check input arguments if (nargin < 1) print_usage (); endif if (! (isnumeric (x) && isreal (x) && isvector (x))) error ("jbtest: X must be a vector of real values."); endif ## Remove missing values and check sample size x = x(! isnan (x)); x = x(:); n = numel (x); if (n < 2) error ("jbtest: X must contain at least two non-missing values."); endif if (nargin < 2 || isempty (alpha)) alpha = 0.05; endif if (! (isnumeric (alpha) && isscalar (alpha) && isreal (alpha) ... && alpha > 0 && alpha < 1)) error ("jbtest: ALPHA must be a scalar in the range (0,1)."); endif domc = (nargin > 2 && ! isempty (mctol)); if (domc && ! (isnumeric (mctol) && isscalar (mctol) && isreal (mctol) ... && mctol > 0)) error ("jbtest: MCTOL must be a positive scalar."); endif ## Jarque-Bera test statistic (biased sample skewness and kurtosis) s = skewness (x); k = kurtosis (x); jbstat = (n / 6) * (s ^ 2 + (k - 3) ^ 2 / 4); if (! domc) [tsizes, talphas, tcv] = jbtest_table_ (); if (n > tsizes(end)) ## Beyond the tabulated sample sizes: large-sample chi-square with two ## degrees of freedom p = chi2cdf (jbstat, 2, 'upper'); critval = chi2inv (1 - alpha, 2); if (isnan (jbstat)) p = 0; elseif (p < 0.001) warning ("jbtest:pTooSmall", ... "jbtest: P is less than the smallest tabulated value; returning 0.001."); p = 0.001; elseif (p > 0.5) warning ("jbtest:pTooBig", ... "jbtest: P is greater than the largest tabulated value; returning 0.5."); p = 0.5; endif else ## Interpolate the embedded Monte-Carlo critical-value table. Critical ## values as a function of the significance level at this sample size: nn = max (n, tsizes(1)); cvn = interp1 (fliplr (1 ./ tsizes), flipud (tcv), 1 / nn, "linear"); ## Critical value at the requested significance level (interpolated over ## the log of the tabulated levels, as in adtest) pp = pchip (log (talphas), cvn); aclamp = min (max (alpha, talphas(1)), talphas(end)); critval = ppval (pp, log (aclamp)); if (alpha < talphas(1) || alpha > talphas(end)) warning ("jbtest:alphaRange", ... "jbtest: ALPHA is outside the tabulated range [0.001, 0.5]."); endif ## p-value by inverse interpolation into the table if (isnan (jbstat)) p = 0; elseif (jbstat > cvn(1)) warning ("jbtest:pTooSmall", ... "jbtest: P is less than the smallest tabulated value; returning 0.001."); p = talphas(1); elseif (jbstat < cvn(end)) warning ("jbtest:pTooBig", ... "jbtest: P is greater than the largest tabulated value; returning 0.5."); p = talphas(end); else i = find (jbstat > cvn, 1, "first"); logp = fzero (@(x) ppval (pp, x) - jbstat, log (talphas([i-1, i]))); p = exp (logp); endif endif else ## Monte-Carlo approximation of the null distribution of the statistic reps = max (1000, ceil (0.25 / mctol ^ 2)); jbsim = jbtest_simulate_ (n, reps); p = sum (jbsim >= jbstat) / reps; critval = quantile (jbsim, 1 - alpha); endif h = double (isnan (jbstat) || jbstat > critval); endfunction ## Simulate REPS values of the Jarque-Bera statistic for standard normal samples ## of size N, evaluated in chunks to bound memory use. function jbsim = jbtest_simulate_ (n, reps) jbsim = zeros (reps, 1); chunk = max (1, floor (1e6 / n)); done = 0; while (done < reps) m = min (chunk, reps - done); z = randn (n, m); s = skewness (z, 1, 1); ## column-wise, biased k = kurtosis (z, 1, 1); jbsim(done+1:done+m) = (n / 6) .* (s(:) .^ 2 + (k(:) - 3) .^ 2 / 4); done += m; endwhile endfunction ## Embedded Jarque-Bera critical-value table, generated by Monte-Carlo ## simulation (1e6 standard-normal samples per sample size). Row i, column j is ## the upper @code{1 - ALPHAS(j)} quantile of the statistic for a sample of size ## @code{SIZES(i)}. Values agree with MATLAB's table to about two decimals; ## both are Monte-Carlo estimates of the same quantiles. function [sizes, alphas, cv] = jbtest_table_ () sizes = [4 5 6 7 8 9 10 11 12 13 14 15 16 18 20 22 25 28 32 37 43 50 ... 60 75 90 110 140 180 250 350 500 750 1000 2000]; alphas = [0.001 0.0025 0.005 0.01 0.025 0.05 0.075 0.1 0.15 0.2 0.25 ... 0.3 0.4 0.5]; cv = [ ... 0.9605 0.9570 0.9509 0.9393 0.9056 0.8522 0.8015 0.7554 0.6729 0.6307 0.5950 0.5630 0.5102 0.4739; ... 1.8291 1.7788 1.7186 1.6283 1.4350 1.2179 1.0626 0.9425 0.7939 0.7298 0.6877 0.6512 0.5896 0.5289; ... 3.1838 2.9786 2.7665 2.4815 2.0002 1.5549 1.2841 1.1007 0.9195 0.8301 0.7709 0.7229 0.6423 0.5735; ... 4.9350 4.4225 3.9482 3.3862 2.5284 1.8493 1.4757 1.2531 1.0229 0.9146 0.8430 0.7859 0.6924 0.6091; ... 6.9241 5.9457 5.1255 4.2348 2.9843 2.0890 1.6446 1.3906 1.1179 0.9911 0.9074 0.8423 0.7352 0.6418; ... 8.8975 7.4734 6.2462 4.9863 3.3891 2.3208 1.8140 1.5169 1.2052 1.0616 0.9675 0.8934 0.7733 0.6699; ... 10.9383 8.9031 7.3105 5.6927 3.7457 2.5276 1.9602 1.6254 1.2823 1.1240 1.0198 0.9389 0.8076 0.6943; ... 12.9458 10.2945 8.2830 6.3396 4.0810 2.7105 2.0797 1.7233 1.3544 1.1818 1.0692 0.9812 0.8393 0.7181; ... 14.7456 11.5915 9.1227 6.8706 4.3397 2.8724 2.2000 1.8123 1.4194 1.2347 1.1137 1.0193 0.8678 0.7392; ... 16.6856 12.6696 9.8759 7.3578 4.6172 3.0267 2.3061 1.8960 1.4816 1.2858 1.1563 1.0558 0.8942 0.7574; ... 18.1583 13.7946 10.6398 7.8586 4.8579 3.1663 2.4152 1.9802 1.5424 1.3333 1.1966 1.0897 0.9196 0.7751; ... 19.3397 14.5252 11.1616 8.1792 5.0433 3.2907 2.5011 2.0505 1.5970 1.3771 1.2321 1.1202 0.9407 0.7903; ... 21.1047 15.4426 11.8050 8.5872 5.2432 3.3999 2.5948 2.1230 1.6496 1.4209 1.2700 1.1521 0.9647 0.8075; ... 22.8485 16.7542 12.7010 9.1686 5.6093 3.6350 2.7427 2.2420 1.7393 1.4936 1.3306 1.2031 1.0012 0.8325; ... 25.0858 18.2343 13.6105 9.7801 5.9031 3.7947 2.8745 2.3471 1.8213 1.5622 1.3877 1.2519 1.0360 0.8567; ... 26.4913 18.9880 14.0979 10.2355 6.1599 3.9611 3.0039 2.4524 1.8990 1.6255 1.4404 1.2959 1.0674 0.8788; ... 28.4147 20.0407 14.9295 10.7391 6.4411 4.1481 3.1440 2.5749 1.9973 1.7047 1.5056 1.3506 1.1061 0.9045; ... 29.8195 21.0006 15.4959 11.0930 6.6884 4.2933 3.2540 2.6754 2.0865 1.7778 1.5680 1.4044 1.1450 0.9309; ... 31.2255 21.8267 16.1070 11.5171 6.9558 4.4934 3.4104 2.8046 2.1884 1.8618 1.6391 1.4628 1.1851 0.9590; ... 32.0166 22.4555 16.5364 11.9241 7.1674 4.6567 3.5449 2.9289 2.2966 1.9503 1.7104 1.5225 1.2280 0.9883; ... 33.2009 23.1281 17.0640 12.1954 7.3963 4.8309 3.6920 3.0593 2.4063 2.0435 1.7879 1.5880 1.2719 1.0165; ... 33.4237 23.2982 17.1731 12.3563 7.5793 4.9742 3.8217 3.1810 2.5093 2.1293 1.8594 1.6467 1.3141 1.0447; ... 33.5896 23.4273 17.3344 12.5402 7.7329 5.1294 3.9730 3.3281 2.6337 2.2310 1.9437 1.7175 1.3627 1.0782; ... 33.1832 23.4014 17.3724 12.6873 7.8909 5.2810 4.1324 3.4803 2.7681 2.3450 2.0372 1.7951 1.4141 1.1114; ... 32.1951 22.8349 17.1964 12.5965 7.9188 5.3719 4.2382 3.5961 2.8783 2.4343 2.1145 1.8597 1.4594 1.1399; ... 30.7519 22.1102 16.7325 12.3753 7.9505 5.4573 4.3608 3.7217 2.9824 2.5237 2.1883 1.9204 1.5003 1.1689; ... 30.1986 21.4958 16.4958 12.2626 7.9444 5.5527 4.4764 3.8553 3.1041 2.6213 2.2712 1.9881 1.5454 1.1974; ... 27.6248 20.4179 15.6934 11.9179 7.9354 5.6402 4.6136 3.9895 3.2225 2.7224 2.3508 2.0582 1.5936 1.2290; ... 26.3970 19.3670 15.1261 11.5818 7.8954 5.7581 4.7527 4.1311 3.3428 2.8256 2.4425 2.1320 1.6437 1.2631; ... 23.9042 17.9448 14.3083 11.1508 7.7814 5.7994 4.8474 4.2331 3.4422 2.9143 2.5134 2.1895 1.6831 1.2874; ... 21.3995 16.6461 13.4998 10.7451 7.6847 5.8609 4.9439 4.3331 3.5378 2.9931 2.5803 2.2471 1.7199 1.3113; ... 19.8917 15.5804 12.8247 10.3682 7.6270 5.9036 5.0108 4.4173 3.6127 3.0576 2.6380 2.2946 1.7544 1.3325; ... 18.8596 15.0900 12.4719 10.1555 7.5849 5.9373 5.0527 4.4619 3.6487 3.0892 2.6623 2.3156 1.7676 1.3407; ... 16.3822 13.5505 11.5058 9.6830 7.4839 5.9564 5.1088 4.5241 3.7189 3.1529 2.7147 2.3586 1.7953 1.3615]; endfunction %!demo %! ## Test whether a sample departs from normality %! x = [1 2 3 4 5 6 7 8 9 100]; # last value is an outlier %! [h, p, jbstat] = jbtest (x) ## Test output against known values (chi-square approximation) %!test %! warning ("off", "jbtest:pTooBig", "local"); %! x = [1 2 3 4 5 6 7 8 9 10]; %! [h, p, jbstat, cv] = jbtest (x); %! assert_equal (h, 0); %! assert_equal (jbstat, 0.624487, 1e-5); # skewness 0, kurtosis 1.7758 %! assert_equal (cv, 2.5276, 1e-4); # tabulated critical value, n=10, a=0.05 %! assert_equal (p, 0.5, 1e-12); # clamped to the tabulated maximum ## A very normal-looking (platykurtic) sample warns that p exceeds 0.5 %!warning ... %! jbtest (1:10); %!test # a strongly non-normal sample is rejected %! warning ("off", "jbtest:pTooSmall", "local"); %! x = [zeros(1, 20), 100]; %! h = jbtest (x); %! assert_equal (h, 1); %!test # NaNs are removed %! warning ("off", "jbtest:pTooBig", "local"); %! assert_equal (jbtest ([1 2 3 4 5 6 7 8 9 10, NaN]), jbtest (1:10)); %!test # alpha controls the critical value %! x = randn (1, 50); %! [~, ~, ~, cv1] = jbtest (x, 0.05); %! [~, ~, ~, cv2] = jbtest (x, 0.01); %! assert_equal (cv2 > cv1, true); %!test # Monte-Carlo p-value runs and lies in (0,1] %! warning ("off", "jbtest:pTooBig", "local"); %! x = [1 2 3 4 5 6 7 8 9 10]; %! [h, p] = jbtest (x, 0.05, 0.05); %! assert_equal (p > 0 && p <= 1, true); %!test # the Monte-Carlo p-value is unsmoothed and reaches exactly zero %! x = [zeros(1, 20), 100]; %! [h, p] = jbtest (x, 0.05, 0.05); %! assert_equal (h, 1); %! assert_equal (p, 0); %!test # a degenerate (zero-variance) sample yields NaN jbstat, p=0, h=1 %! [h, p, jbstat] = jbtest (ones (1, 10)); %! assert_equal (h, 1); %! assert_equal (p, 0); %! assert_equal (isnan (jbstat), true); ## Test input validation %!error jbtest () %!error jbtest (ones (3, 3)) %!error jbtest ({1, 2, 3}) %!error jbtest ([1 2 3i]) %!error jbtest (5) %!error jbtest (1:10, 0) %!error jbtest (1:10, 1) %!error jbtest (1:10, [0.1 0.2]) %!error jbtest (1:10, 0.05, -1) statistics-release-2.1.2/inst/Hypothesis_Testing/knntest.m000066400000000000000000000475351527311104300240140ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{knnstat} =} knntest (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{knnstat} =} knntest (@var{X}, @var{Y}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{knnstat}, @var{p}] =} knntest (@dots{}) ## @deftypefnx {statistics} {[@var{knnstat}, @var{p}, @var{h}] =} knntest (@dots{}) ## ## Two-sample multivariate test based on nearest neighbours. ## ## @code{@var{knnstat} = knntest (@var{X}, @var{Y})} returns the nearest ## neighbour statistic of the two samples @var{X} and @var{Y}, whose rows are ## observations and whose columns are variables. The two samples are pooled, ## each observation's @math{k} nearest neighbours are found among the others, ## and @var{knnstat} is the share of those neighbours that belong to the same ## sample as the observation. It lies in @math{[0, 1]}: near 1 the samples ## are well separated, and near 0.5 two samples of equal size are alike. ## ## @var{X} and @var{Y} are both numeric matrices with the same number of ## columns, or both tables. Two tables are read over the variables they share, ## which must be all the variables of one of them. An observation holding a ## missing value in a variable used is left out. ## ## @code{[@var{knnstat}, @var{p}, @var{h}] = knntest (@dots{})} also returns ## the p-value of the right-tailed test of the null hypothesis that @var{X} and ## @var{Y} come from the same distribution, and @var{h}, which is 1 where the ## null hypothesis is rejected at the significance level @qcode{'Alpha'} and 0 ## otherwise. The p-value takes @math{(knnstat - mu) / sigma} as standard ## normal, with @math{m_x} and @math{m_y} the sizes of the samples, @math{m} ## their sum, and @math{q = m_x m_y / m^2}: ## @tex ## $$\mu = {m_x (m_x - 1) + m_y (m_y - 1) \over m (m - 1)}, \qquad ## \sigma^2 = {q + 4 q^2 \over m k}.$$ ## @end tex ## @ifnottex ## @math{mu = (m_x (m_x - 1) + m_y (m_y - 1)) / (m (m - 1))} and ## @math{sigma^2 = (q + 4 q^2) / (m k)}. ## @end ifnottex ## ## @code{@var{knnstat} = knntest (@dots{}, @var{Name}, @var{Value})} takes the ## following options. ## ## @multitable @columnfractions 0.26 0.02 0.72 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Alpha'} @tab @tab The significance level, a scalar between 0 ## and 1, 0.05 by default. ## @item @qcode{'NumNeighbors'} @tab @tab The number of nearest neighbours ## @math{k}, a positive integer, 10 by default. It is taken as one less than ## the number of observations where it exceeds that. ## @item @qcode{'Distance'} @tab @tab The distance between observations; see ## below. ## @item @qcode{'VariableNames'} @tab @tab The variables to use, among those ## the two tables share, as a character vector, a string array or a cell array ## of character vectors. All the shared variables by default. ## @item @qcode{'CategoricalVariables'} @tab @tab The variables holding ## levels: @qcode{'all'}, their indices, a logical vector over the variables, ## or, for tables, their names. A table variable holding logical values, an ## unordered @code{categorical} array, a @code{string} array or a cell array of ## character vectors holds levels whether named here or not. A matrix holds ## none unless named. ## @end multitable ## ## @qcode{'Distance'} is one of the following, @qcode{'seuclidean'} by default ## where every variable is continuous, @qcode{'hamming'} where every variable ## holds levels, and @qcode{'goodall3'} where the two are mixed. ## ## @multitable @columnfractions 0.22 0.02 0.76 ## @headitem @var{Distance} @tab @tab Description ## @item @qcode{'euclidean'} @tab @tab Euclidean distance. ## @item @qcode{'seuclidean'} @tab @tab Euclidean distance with each variable ## divided by its standard deviation over @code{[@var{X}; @var{Y}]}. ## @item @qcode{'cityblock'} @tab @tab City block distance. ## @item @qcode{'cosine'} @tab @tab One minus the cosine of the angle between ## two observations. ## @item @qcode{'correlation'} @tab @tab One minus the correlation between two ## observations. ## @item @qcode{'fasteuclidean'}, @qcode{'fastseuclidean'} @tab @tab The same ## distances as @qcode{'euclidean'} and @qcode{'seuclidean'}, computed exactly. ## @item @qcode{'hamming'} @tab @tab The share of variables that differ. ## @item @qcode{'goodall3'} @tab @tab The Goodall 3 dissimilarity of ## @code{nomdist}, with level frequencies counted over ## @code{[@var{X}; @var{Y}]} and the observation whose neighbours are sought ## counted once more, as @code{nomdist2} counts a query with ## @qcode{'CountQuery'}. A continuous variable adds its absolute difference ## divided by its range over @code{[@var{X}; @var{Y}]}, and the sum over ## every variable is divided by their number. ## @end multitable ## ## The first five treat a variable holding levels as numbers, its levels coded ## @code{1}, @code{2}, @dots{}; @qcode{'hamming'} treats every distinct value ## of a continuous variable as a level. ## ## A tie between two neighbours at the same distance is broken by the order of ## the pooled sample, the rows of @var{X} coming before those of @var{Y}, and ## distances equal to within rounding count as tied, so the result does not ## depend on the platform. MATLAB lets rounding decide such ties, so on data ## where many distances are equal, such as values on a coarse grid, ## @var{knnstat} can differ slightly from MATLAB's. ## ## MATLAB defines its @qcode{'goodall3'} for a continuous variable in a way it ## does not document, and which none of the constructions tried here ## reproduces, so on mixed data @var{knnstat} differs from MATLAB's. On ## levels alone the two agree. ## ## Reference: M. Williams (2010). How good are your fits? Unbinned ## multivariate goodness-of-fit tests in high energy physics. Journal of ## Instrumentation, 5(09), P09004. ## ## @seealso{nomdist, nomdist2, kstest2} ## @end deftypefn function [knnstat, p, h] = knntest (X, Y, varargin) ## Input validation if (nargin < 2) error ("knntest: too few input arguments."); endif optNames = {'Alpha', 'NumNeighbors', 'Distance', 'VariableNames', ... 'CategoricalVariables'}; ## An empty Distance resolves to the default for the variables' kinds dfValues = {0.05, 10, [], [], []}; [alpha, k, dist, vnames, catvars, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (args)) error ("knntest: invalid optional paired argument."); endif if (! (isnumeric (alpha) && isreal (alpha) && isscalar (alpha) && alpha > 0 && alpha < 1)) error ("knntest: 'Alpha' must be a scalar between 0 and 1."); endif if (! (isnumeric (k) && isreal (k) && isscalar (k) && isfinite (k) && k >= 1 && k == fix (k))) error ("knntest: 'NumNeighbors' must be a positive integer."); endif ## The pooled sample coded one column per variable, rows holding a missing ## value left out, and which variables hold levels [Z, iscat, mx, my, errmsg] = __twosample__ (X, Y, vnames, catvars); if (! isempty (errmsg)) error ("knntest: %s", errmsg); endif m = mx + my; ## The distance, whose default depends on the kinds of variable if (isempty (dist)) if (all (iscat)) dist = 'hamming'; elseif (any (iscat)) dist = 'goodall3'; else dist = 'seuclidean'; endif endif dists = {'cityblock', 'correlation', 'cosine', 'euclidean', ... 'fasteuclidean', 'seuclidean', 'fastseuclidean', 'hamming', ... 'goodall3'}; if (isa (dist, 'string') && isscalar (dist)) dist = char (dist); endif if (! (ischar (dist) && isrow (dist) && any (strcmpi (dist, dists)))) error ("knntest: 'Distance' must be one of %s.", ... strjoin (strcat ("'", dists, "'"), ', ')); endif dist = lower (dist); g = [zeros(mx, 1); ones(my, 1)]; ## The same-sample share among each observation's k nearest neighbours k = min (k, m - 1); I = zeros (k, m); step = max (1, min (m, floor (2e6 / m))); for b1 = 1:step:m b = b1:min (b1 + step - 1, m); D = knnDistances (Z, b, dist, iscat); D(sub2ind (size (D), b, 1:numel (b))) = Inf; ## Distances equal to within rounding tie, and sort keeps the lower row s = max (abs (D(isfinite (D)))); if (! isempty (s) && s > 0) D = round (D / s * 1e12); endif [~, o] = sort (D, 1); I(:,b) = reshape (g(o(1:k,:)), k, numel (b)) == g(b)'; endfor knnstat = mean (I(:)); ## Right tail of the normal approximation mu = (mx * (mx - 1) + my * (my - 1)) / (m * (m - 1)); q = mx * my / m ^ 2; sigma = sqrt ((q + 4 * q ^ 2) / (m * k)); p = 0.5 * erfc ((knnstat - mu) / sigma / sqrt (2)); h = double (p <= alpha); endfunction ## Distances from every observation to the observations B, one column each. function D = knnDistances (Z, b, dist, iscat) switch (dist) case {'euclidean', 'fasteuclidean'} D = pdist2 (Z, Z(b,:), 'euclidean'); case {'seuclidean', 'fastseuclidean'} D = pdist2 (Z, Z(b,:), 'seuclidean', std (Z, [], 1)); case {'cityblock', 'cosine', 'correlation'} D = pdist2 (Z, Z(b,:), dist); case 'hamming' D = pdist2 (Z, Z(b,:), 'hamming'); case 'goodall3' K = columns (Z); D = zeros (rows (Z), numel (b)); if (any (iscat)) C = Z(:,iscat); D = columns (C) * nomdist2 (C, C(b,:), 'goodall3', 'CountQuery', true); endif if (! all (iscat)) R = range (Z(:,! iscat), 1); R(R == 0) = 1; W = Z(:,! iscat) ./ R; D += pdist2 (W, W(b,:), 'cityblock'); endif D /= K; endswitch endfunction %!shared XS, YS, XA, YA %! XS = [8, 4; 13, 1; 14, 2; 3, 3]; %! YS = [3, 19; 1, 7; 1, 17; 4, 10; 2, 17]; %! XA = [2, 2, 3; 3, 1, 1; 3, 1, 3; 3, 3, 1; 3, 3, 2; 1, 1, 1]; %! YA = [3, 2, 2; 1, 2, 2; 3, 2, 2; 2, 3, 3; 2, 1, 1; 3, 1, 1]; ## Expected values from MATLAB R2026a unless stated otherwise %!test %! ## A tie goes to the lower pooled index %! [s, p, h] = knntest ([0; 20], [1; 2], 'NumNeighbors', 1, ... %! 'Distance', 'euclidean'); %! assert_equal (s, 0.25); %! assert_equal (p, 0.593168142116604, -1e-13); %! assert_equal (h, 0); %!test %! [s, p] = knntest ([1; 2], [0; 20], 'NumNeighbors', 1, ... %! 'Distance', 'euclidean'); %! assert_equal (s, 0.5); %! assert_equal (p, 0.318675944116969, -1e-13); %!test %! ## An observation is never its own neighbour, even beside a duplicate %! [s, p] = knntest ([0; 5], [0; 9], 'NumNeighbors', 1, ... %! 'Distance', 'euclidean'); %! assert_equal (s, 0); %! assert_equal (p, 0.827110706924420, -1e-13); %!test %! ## seuclidean, the default, scales by the standard deviation over [X; Y] %! [s, p, h] = knntest (XS, YS, 'NumNeighbors', 1); %! assert_equal (s, 7 / 9, -1e-14); %! assert_equal (p, 0.076726923031103, -1e-13); %! assert_equal (h, 0); %!assert_equal (knntest (XS, YS, 'NumNeighbors', 1, ... %! 'Distance', 'SEuclidean'), ... %! knntest (XS, YS, 'NumNeighbors', 1)) %!assert_equal (knntest (XS, YS, 'NumNeighbors', 1, 'Distance', ... %! 'fastseuclidean'), knntest (XS, YS, 'NumNeighbors', 1)) %!assert_equal (knntest (XS, YS, 'NumNeighbors', 1, ... %! 'Distance', 'fasteuclidean'), ... %! knntest (XS, YS, 'NumNeighbors', 1, 'Distance', 'euclidean')) %!assert_equal (knntest ([XS; NaN, 2], YS, 'NumNeighbors', 1), 7 / 9, -1e-14) %!test %! [~, ~, h] = knntest (XS, YS, 'NumNeighbors', 1, 'Alpha', 0.5); %! assert_equal (h, 1); %!test %! ## Goodall 3 counts the query into the frequencies over [X; Y] %! [s, p] = knntest (XA, YA, 'NumNeighbors', 1, 'CategoricalVariables', ... %! 'all', 'Distance', 'goodall3'); %! assert_equal (s, 7 / 12, -1e-14); %! assert_equal (p, 0.264043416942331, -1e-13); %!test %! ## hamming is the default where every variable holds levels %! [s, p] = knntest (XA, YA, 'NumNeighbors', 1, 'CategoricalVariables', 'all'); %! assert_equal (s, 7 / 12, -1e-14); %! assert_equal (p, 0.264043416942331, -1e-13); %!test %! ## A level of Y that X lacks keeps its row %! [s, p] = knntest (XA, [YA; 4, 1, 1], 'NumNeighbors', 1, ... %! 'CategoricalVariables', 'all', 'Distance', 'goodall3'); %! assert_equal (s, 7 / 13, -1e-14); %! assert_equal (p, 0.346797481676874, -1e-13); %!assert_equal (knntest (XA, [YA; 4, 1, 1], 'NumNeighbors', 1, ... %! 'CategoricalVariables', 'all'), 7 / 13, -1e-14) %!test %! X = [4, 1, 4, 4, 4, 1; 2, 1, 2, 2, 2, 3; 1, 3, 1, 2, 2, 3; 2, 2, 1, 4, 4, 2; %! 2, 3, 3, 1, 3, 4; 2, 4, 1, 2, 2, 3; 3, 1, 2, 3, 4, 1]; %! Y = [1, 4, 4, 2, 4, 1; 1, 3, 1, 1, 4, 2; 1, 3, 3, 1, 3, 2; 1, 3, 2, 4, 2, 3; %! 2, 3, 4, 1, 2, 3; 1, 4, 2, 3, 4, 2; 2, 3, 2, 3, 2, 1]; %! [s, p] = knntest (X, Y, 'NumNeighbors', 1, 'CategoricalVariables', 'all', ... %! 'Distance', 'goodall3'); %! assert_equal (s, 5 / 14, -1e-14); %! assert_equal (p, 0.709666127521303, -1e-13); %!test %! X = [1, 2, 1, 2, 1, 4; 3, 1, 4, 1, 4, 4; 4, 1, 3, 2, 2, 3; 3, 3, 4, 2, 4, 3; %! 4, 3, 2, 4, 3, 4; 2, 3, 4, 3, 1, 1; 3, 4, 1, 2, 3, 2]; %! Y = [4, 2, 4, 2, 1, 2; 4, 1, 1, 3, 4, 1; 4, 3, 1, 3, 4, 4; 4, 4, 2, 4, 2, 1; %! 2, 2, 2, 3, 3, 1; 1, 3, 4, 4, 4, 3; 2, 2, 4, 3, 3, 3]; %! s = knntest (X, Y, 'NumNeighbors', 1, 'CategoricalVariables', 'all', ... %! 'Distance', 'goodall3'); %! assert_equal (s, 5 / 14, -1e-14); %!test %! X = [3, 1, 2, 2; 1, 1, 3, 2; 2, 1, 3, 1; 3, 3, 1, 3; 3, 2, 2, 1; %! 2, 3, 2, 1; 1, 1, 2, 2]; %! Y = [3, 2, 3, 3; 2, 3, 1, 3; 2, 3, 3, 3; 2, 2, 3, 2; 3, 1, 1, 3; %! 3, 2, 1, 3; 3, 1, 1, 2]; %! [s, p] = knntest (X, Y, 'NumNeighbors', 1, 'CategoricalVariables', 'all', ... %! 'Distance', 'goodall3'); %! assert_equal (s, 5 / 7, -1e-14); %! assert_equal (p, 0.090543972339391, -1e-13); %!test %! load fisheriris %! [s, p, h] = knntest (meas(1:25,:), meas(26:50,:)); %! assert_equal (s, 0.438, -1e-14); %! assert_equal (p, 0.949281930319421, -1e-13); %! assert_equal (h, 0); %!test %! ## Ties decided by rounding in MATLAB, which gives 0.909 %! load fisheriris %! [s, p, h] = knntest (meas(51:100,:), meas(101:150,:)); %! assert_equal (s, 0.908, -1e-14); %! assert_equal (p, 1.72915613588074e-76, -1e-10); %! assert_equal (h, 1); %!test %! ## Ties decided by rounding in MATLAB, which gives 0.92 %! load fisheriris %! s = knntest (meas(51:100,:), meas(101:150,:), 'NumNeighbors', 5, ... %! 'Distance', 'cityblock'); %! assert_equal (s, 0.916, -1e-14); %!test %! ## NumNeighbors above the observations but one is taken as that %! [s, p] = knntest ([0; 20], [1; 2]); %! assert_equal (s, 1 / 3, -1e-14); %! assert_equal (p, 0.5, -1e-14); %!test %! ## Mixed data: ours; MATLAB's undocumented rule gives 0.583333 %! X = [3, 51; 3, 53; 2, 44; 2, 56; 1, 46; 3, 45]; %! Y = [1, 69; 3, 8; 2, 23; 2, 1; 1, 5; 1, 86]; %! [s, p] = knntest (X, Y, 'NumNeighbors', 1, 'CategoricalVariables', 1); %! assert_equal (s, 2 / 3, -1e-14); %! assert_equal (p, 0.14936110409448, -1e-12); %!test %! X = [3, 51; 3, 53; 2, 44; 2, 56; 1, 46; 3, 45]; %! Y = [1, 69; 3, 8; 2, 23; 2, 1; 1, 5; 1, 86]; %! s = knntest (X, Y, 'NumNeighbors', 1, 'CategoricalVariables', [true, false]); %! assert_equal (s, knntest (X, Y, 'NumNeighbors', 1, ... %! 'CategoricalVariables', 1)); %!test %! ## A table's categorical variable holds levels without being named %! X = [1, 89; 2, 58; 1, 99; 2, 52; 1, 84; 3, 49]; %! Y = [1, 72; 2, 58; 3, 100; 3, 97; 1, 68; 2, 70]; %! TX = table (categorical (X(:,1)), X(:,2)); %! TY = table (categorical (Y(:,1)), Y(:,2)); %! assert_equal (knntest (TX, TY, 'NumNeighbors', 1), ... %! knntest (X, Y, 'NumNeighbors', 1, 'CategoricalVariables', 1)); %!test %! C = {'a', 'b', 'c'}; %! TX = table (C(XA(:,1))', XA(:,2) == 1, string (C(XA(:,3)))', ... %! 'VariableNames', {'A', 'B', 'C'}); %! TY = table (C(YA(:,1))', YA(:,2) == 1, string (C(YA(:,3)))', ... %! 'VariableNames', {'A', 'B', 'C'}); %! assert_equal (knntest (TX, TY, 'NumNeighbors', 1), ... %! knntest ([XA(:,1), XA(:,2) == 1, XA(:,3)], ... %! [YA(:,1), YA(:,2) == 1, YA(:,3)], ... %! 'NumNeighbors', 1, 'CategoricalVariables', 'all')); %!test %! ## Tables are read over the variables they share %! TX = table (XS(:,1), XS(:,2), (1:4)', 'VariableNames', {'A', 'B', 'Z'}); %! TY = table (YS(:,2), YS(:,1), 'VariableNames', {'B', 'A'}); %! assert_equal (knntest (TX, TY, 'NumNeighbors', 1), 7 / 9, -1e-14); %!test %! TX = table (XS(:,1), XS(:,2), 'VariableNames', {'A', 'B'}); %! TY = table (YS(:,1), YS(:,2), 'VariableNames', {'A', 'B'}); %! assert_equal (knntest (TX, TY, 'NumNeighbors', 1, 'VariableNames', 'A'), ... %! knntest (XS(:,1), YS(:,1), 'NumNeighbors', 1)); %!error knntest (1) %!error knntest (XS, YS, 'Tail', 1) %!error ... %! knntest (XS, YS, 'Alpha', 1) %!error ... %! knntest (XS, YS, 'NumNeighbors', 1.5) %!error ... %! knntest (table (XS), YS) %!error ... %! knntest (XS, YS, 'VariableNames', 'A') %!error ... %! knntest ({1}, YS) %!error ... %! knntest (XS, YS(:,1)) %!error ... %! knntest (table ([1; 2], 'VariableNames', {'A'}), ... %! table ([1; 2], 'VariableNames', {'B'})) %!error ... %! knntest (table ([1; 2], 'VariableNames', {'A'}), ... %! table ([1; 2], 'VariableNames', {'A'}), 'VariableNames', 'B') %!error ... %! knntest (table ([1, 2; 3, 4], 'VariableNames', {'A'}), ... %! table ([1, 2; 3, 4], 'VariableNames', {'A'})) %!error ... %! knntest (table ({1; 2}, 'VariableNames', {'A'}), ... %! table ({1; 2}, 'VariableNames', {'A'})) %!error ... %! knntest (table ([1; 2], 'VariableNames', {'A'}), ... %! table ({'a'; 'b'}, 'VariableNames', {'A'})) %!error ... %! knntest (XS, YS, 'CategoricalVariables', 'A') %!error ... %! knntest (table ([1; 2], 'VariableNames', {'A'}), ... %! table ([1; 2], 'VariableNames', {'A'}), 'CategoricalVariables', 'B') %!error ... %! knntest (XS, YS, 'CategoricalVariables', true) %!error ... %! knntest (XS, YS, 'CategoricalVariables', 3) %!error ... %! knntest ([NaN, 1], YS) %!error ... %! knntest (XS, YS, 'Distance', 'minkowski') statistics-release-2.1.2/inst/Hypothesis_Testing/kruskalwallis.m000066400000000000000000000432501527311104300252040ustar00rootroot00000000000000## Copyright (C) 2021 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} kruskalwallis (@var{x}) ## @deftypefnx {statistics} {@var{p} =} kruskalwallis (@var{x}, @var{group}) ## @deftypefnx {statistics} {@var{p} =} kruskalwallis (@var{x}, @var{group}, @var{displayopt}) ## @deftypefnx {statistics} {@var{p} =} kruskalwallis (@var{x}, @var{group}, @var{displayopt}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{p}, @var{tbl}] =} kruskalwallis (@var{x}, @dots{}) ## @deftypefnx {statistics} {[@var{p}, @var{tbl}, @var{stats}] =} kruskalwallis (@var{x}, @dots{}) ## ## Perform a Kruskal-Wallis test, the non-parametric alternative of a one-way ## analysis of variance (ANOVA), for comparing the means of two or more groups ## of data under the null hypothesis that the groups are drawn from the same ## population, i.e. the group means are equal. ## ## kruskalwallis can take up to three input arguments: ## ## @itemize ## @item ## @var{x} contains the data and it can either be a vector or matrix. ## If @var{x} is a matrix, then each column is treated as a separate group. ## If @var{x} is a vector, it is a single group whichever way it lies, unless ## @var{group} is given; with one group there is nothing to compare, so ## @var{p} is @qcode{NaN}. ## @item ## @var{group} contains the names for each group. If @var{x} is a matrix, then ## @var{group} can either be a cell array of strings of a character array, with ## one row per column of @var{x}. If you want to omit this argument, enter an ## empty array ([]). If @var{x} is a vector, then @var{group} must be a vector ## of the same length, or a string array or cell array of strings with one row ## for each element of @var{x}. @var{x} values corresponding to the same value ## of @var{group} are placed in the same group. ## @item ## @var{displayopt} is an optional parameter for displaying the groups contained ## in the data in a boxplot. If omitted, it is 'on' by default. If group names ## are defined in @var{group}, these are used to identify the groups in the ## boxplot. Use 'off' to omit displaying this figure. ## @end itemize ## ## kruskalwallis can return up to three output arguments: ## ## @itemize ## @item ## @var{p} is the p-value of the null hypothesis that all group means are equal. ## @item ## @var{tbl} is a cell array containing the results in a standard ANOVA table. ## @item ## @var{stats} is a structure containing statistics useful for performing ## a multiple comparison of means with the MULTCOMPARE function. ## @end itemize ## ## @var{stats} also holds the effect size of the test, an Octave extension: ## @qcode{EtaSquared}, the rank eta squared ## @math{max (0, (H - k + 1) / (n - k))} for the statistic @math{H} over ## @math{k} groups and @math{n} observations, which is the share of the ## variance of the ranks the groups account for, corrected for bias, and ## @qcode{EtaSquaredCI}, its two-sided confidence interval at the level ## @math{100 (1 - alpha)} percent. By default the interval inverts the ## noncentral chi-square distribution of @math{H} on @math{k - 1} degrees of ## freedom, taken as @math{H}'s distribution under the alternative, which it ## is for large samples. Three options after @var{displayopt}, also an ## Octave extension, set it: ## ## @multitable @columnfractions 0.32 0.66 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'Alpha'} @tab The significance level of the interval, a ## scalar between 0 and 1, 0.05 by default. ## @item @qcode{'ConfidenceIntervalType'} @tab @qcode{'exact'}, the default, ## or @qcode{'bootstrap'} for the bias-corrected and accelerated bootstrap ## interval, resampling within each group. ## @item @qcode{'NumBootstraps'} @tab The number of bootstrap replicates, a ## positive integer, 1000 by default. ## @end multitable ## ## If kruskalwallis is called without any output arguments, then it prints the ## results in a one-way ANOVA table to the standard output. It is also printed ## when @var{displayopt} is 'on'. ## ## Examples: ## ## @example ## x = meshgrid (1:6); ## x = x + normrnd (0, 1, 6, 6); ## [p, atab] = kruskalwallis(x); ## @end example ## ## ## @example ## x = ones (50, 4) .* [-2, 0, 1, 5]; ## x = x + normrnd (0, 2, 50, 4); ## group = @{"A", "B", "C", "D"@}; ## kruskalwallis (x, group); ## @end example ## ## @seealso{anova1, friedman, ranksum, multcompare} ## @end deftypefn function [p, tbl, stats] = kruskalwallis (x, group, displayopt, varargin) ## check for valid number of input arguments if (nargin < 1) print_usage (); endif ## add defaults if (nargin < 2) group = []; endif if (nargin < 3) displayopt = 'on'; endif [alpha, citype, nboot, args] = parsePairedArguments ... ({'Alpha', 'ConfidenceIntervalType', 'NumBootstraps'}, ... {0.05, 'exact', 1000}, varargin(:)); if (! isempty (args)) error ("kruskalwallis: invalid optional paired argument."); endif if (! (isnumeric (alpha) && isreal (alpha) && isscalar (alpha) && alpha > 0 && alpha < 1)) error ("kruskalwallis: 'Alpha' must be a scalar between 0 and 1."); endif if (! (ischar (citype) && any (strcmpi (citype, {'exact', 'bootstrap'})))) error (strcat ("kruskalwallis: 'ConfidenceIntervalType' must be", ... " 'exact' or 'bootstrap'.")); endif if (! (isnumeric (nboot) && isreal (nboot) && isscalar (nboot) && isfinite (nboot) && nboot >= 1 && nboot == fix (nboot))) error ("kruskalwallis: 'NumBootstraps' must be a positive integer."); endif plotdata = ! (strcmp (displayopt, 'off')); ## Convert group to cell array from character array, make it a column if (! isempty (group) && ischar (group)) group = cellstr (group); endif if (size (group, 1) == 1) group = group'; endif ## A vector is one group whichever way it lies, and each column of a matrix ## is a group of its own. With GROUP given, a vector is grouped by it ## instead, one element per observation. if (isvector (x)) x = x(:); endif if (isempty (group) || ! isvector (x)) [n, m] = size (x); x = x(:); gi = reshape (repmat ((1:m), n, 1), n*m, 1); if (length (group) == 0) ## no group names are provided group = gi; elseif (size (group, 1) == m) ## group names exist and match columns group = group(gi,:); else error ("X columns and GROUP length do not match."); endif endif ## Identify NaN values (if any) and remove them from X along with ## their corresponding values from group vector nonan = ! isnan (x); x = x(nonan); group = group(nonan, :); ## Convert group to indices and separate names [group_id, group_names] = grp2idx (group); group_id = group_id(:); named = 1; ## Rank data for non-parametric analysis [xr, tieadj] = tieranks (x); ## Get group size and mean for each group groups = size (group_names, 1); xs = zeros (1, groups); xm = xs; for j = 1:groups group_size = find (group_id == j); xs(j) = length (group_size); xm(j) = mean (xr(group_size)); endfor ## Calculate statistics lx = length (xr); ## Number of samples in groups gm = mean (xr); ## Grand mean of groups dfm = length (xm) - 1; ## degrees of freedom for model dfe = lx - dfm - 1; ## degrees of freedom for error ## With no observations there is nothing to partition. Counting the groups ## less one, and the samples less that, puts both below zero, which has no ## reading; MATLAB reports the negative numbers. if (lx == 0) dfm = 0; dfe = 0; endif SSM = xs .* (xm - gm) * (xm - gm)'; ## Sum of Squares for Model SST = (xr(:) - gm)' * (xr(:) - gm); ## Sum of Squares Total SSE = SST - SSM; ## Sum of Squares Error if (dfm > 0) MSM = SSM / dfm; ## Mean Square for Model else MSM = NaN; endif if (dfe > 0) MSE = SSE / dfe; ## Mean Squared Error else MSE = NaN; endif ## Calculate Chi-sq statistic ChiSq = (12 * SSM) / (lx * (lx + 1)); if (tieadj > 0) ChiSq = ChiSq / (1 - 2 * tieadj / (lx ^ 3 - lx)); endif if (dfm <= 0) ## Fewer than two groups leaves nothing to compare, so the statistic has ## no reference distribution and the test is undefined rather than ## significant. ChiSq = NaN; p = NaN; else p = chi2cdf (ChiSq, dfm, 'upper'); endif ## Create results table (if requested) if (nargout > 1) tbl = {'Source', 'SS', 'df', 'MS', 'Chi-sq', 'Prob>Chi-sq'; ... 'Groups', SSM, dfm, MSM, ChiSq, p; ... 'Error', SSE, dfe, MSE, '', ''; ... 'Total', SST, dfm + dfe, '', '', ''}; endif ## Create stats structure (if requested) for MULTCOMPARE if (nargout > 2) if (length (group_names) > 0) stats.gnames = group_names; else stats.gnames = strjust (num2str ((1:length (xm))'), 'left'); endif stats.n = xs; stats.source = 'kruskalwallis'; stats.meanranks = xm; stats.sumt = 2 * tieadj; ## The rank eta squared, corrected for bias as H - df estimates the ## noncentrality, and its interval stats.EtaSquared = NaN; stats.EtaSquaredCI = [NaN, NaN]; if (dfm > 0 && dfe > 0) stats.EtaSquared = max (0, (ChiSq - dfm) / dfe); if (strcmpi (citype, 'exact')) stats.EtaSquaredCI = min (__ncx2bounds__ (ChiSq, dfm, alpha) / dfe, 1); else S = arrayfun (@(g) x(group_id == g), 1:groups, 'UniformOutput', false); stats.EtaSquaredCI = __bootci__ (@etaSquared, S, alpha, nboot, ... 'strata'); endif endif endif ## Print results table on screen if no output argument was requested if (nargout == 0 || plotdata) printf (" Kruskal-Wallis ANOVA Table\n"); printf ("Source SS df MS Chi-sq Prob>Chi-sq\n"); printf ("---------------------------------------------------------\n"); printf ("Columns %10.2f %5.0f %10.2f %8.2f %11.5e\n", ... SSM, dfm, MSM, ChiSq, p); printf ("Error %10.2f %5.0f %10.2f\n", SSE, dfe, MSE); printf ("Total %10.2f %5.0f\n", SST, dfm + dfe); endif ## Plot data using BOXPLOT (unless opted out) if (plotdata) boxplot (x, group_id, 'Notch', 'on', 'Labels', group_names); endif endfunction ## The rank eta squared of groups whose columns are resampled copies of each ## sample, one value per column function e = etaSquared (varargin) k = numel (varargin); ns = cellfun (@rows, varargin); n = sum (ns); g = repelem ((1:k)', ns(:)); X = vertcat (varargin{:}); e = NaN (1, columns (X)); if (n <= k) return; endif for j = 1:columns (X) [r, tieadj] = tiedrank (X(:,j)); m = accumarray (g, r) ./ ns(:); H = 12 * sum (ns(:) .* (m - (n + 1) / 2) .^ 2) / (n * (n + 1)); if (tieadj > 0) H /= 1 - 2 * tieadj / (n ^ 3 - n); endif e(j) = max (0, (H - k + 1) / (n - k)); endfor endfunction ## local function for computing tied ranks on column vectors function [r, tieadj] = tieranks (x) ## Sort data [value, x_idx] = sort (x); epsx = zeros (size (x)); epsx = epsx(x_idx); x_l = numel (x); ## Count ranks from start (min value) ranks = [1:x_l]'; ## Initialize tie adjustments tieadj = 0; ## Adjust for ties. ties = value(1:x_l-1) + epsx(1:x_l-1) >= value(2:x_l) - epsx(2:x_l); t_idx = find (ties); t_idx(end+1) = 0; maxTies = numel (t_idx); ## Calculate tie adjustments tiecount = 1; while (tiecount < maxTies) tiestart = t_idx(tiecount); ntied = 2; while (t_idx(tiecount+1) == t_idx(tiecount) + 1) tiecount = tiecount + 1; ntied = ntied + 1; endwhile ## Check for tieflag tieadj = tieadj + ntied * (ntied - 1) * (ntied + 1) / 2; ## Average tied ranks ranks(tiestart:tiestart + ntied - 1) = ... sum (ranks(tiestart:tiestart + ntied - 1)) / ntied; tiecount = tiecount + 1; endwhile ## Remap data to original dimensions r(x_idx) = ranks; endfunction %!demo %! rng (42); %! x = meshgrid (1:6); %! x = x + normrnd (0, 1, 6, 6); %! kruskalwallis (x, [], 'off'); %!demo %! rng (42); %! x = meshgrid (1:6); %! x = x + normrnd (0, 1, 6, 6); %! [p, atab] = kruskalwallis (x); %!demo %! rng (42); %! x = ones (30, 4) .* [-2, 0, 1, 5]; %! x = x + normrnd (0, 2, 30, 4); %! group = {'A', 'B', 'C', 'D'}; %! kruskalwallis (x, group); ## testing results against SPSS and R on the GEAR.DAT data file available from ## https://www.itl.nist.gov/div898/handbook/eda/section3/eda354.htm %!test %! data = [1.006, 0.996, 0.998, 1.000, 0.992, 0.993, 1.002, 0.999, 0.994, 1.000, ... %! 0.998, 1.006, 1.000, 1.002, 0.997, 0.998, 0.996, 1.000, 1.006, 0.988, ... %! 0.991, 0.987, 0.997, 0.999, 0.995, 0.994, 1.000, 0.999, 0.996, 0.996, ... %! 1.005, 1.002, 0.994, 1.000, 0.995, 0.994, 0.998, 0.996, 1.002, 0.996, ... %! 0.998, 0.998, 0.982, 0.990, 1.002, 0.984, 0.996, 0.993, 0.980, 0.996, ... %! 1.009, 1.013, 1.009, 0.997, 0.988, 1.002, 0.995, 0.998, 0.981, 0.996, ... %! 0.990, 1.004, 0.996, 1.001, 0.998, 1.000, 1.018, 1.010, 0.996, 1.002, ... %! 0.998, 1.000, 1.006, 1.000, 1.002, 0.996, 0.998, 0.996, 1.002, 1.006, ... %! 1.002, 0.998, 0.996, 0.995, 0.996, 1.004, 1.004, 0.998, 0.999, 0.991, ... %! 0.991, 0.995, 0.984, 0.994, 0.997, 0.997, 0.991, 0.998, 1.004, 0.997]; %! group = [1:10] .* ones (10,10); %! group = group(:); %! [p, tbl] = kruskalwallis (data, group, 'off'); %! assert_equal (p, 0.048229, 1e-6); %! assert_equal (tbl{2,5}, 17.03124, 1e-5); %! assert_equal (tbl{2,3}, 9, 0); %! assert_equal (tbl{4,2}, 82655.5, 1e-16); %! data = reshape (data, 10, 10); %! [p, tbl, stats] = kruskalwallis (data, [], 'off'); %! assert_equal (p, 0.048229, 1e-6); %! assert_equal (tbl{2,5}, 17.03124, 1e-5); %! assert_equal (tbl{2,3}, 9, 0); %! assert_equal (tbl{4,2}, 82655.5, 1e-16); %! means = [51.85, 60.45, 37.6, 51.1, 29.5, 54.25, 64.55, 66.7, 53.65, 35.35]; %! N = 10 * ones (1, 10); %! assert_equal (stats.meanranks, means, 1e-6); %! assert_equal (length (stats.gnames), 10, 0); %! assert_equal (stats.n, N, 0); ## A vector without GROUP is one group whichever way it lies, and an empty X ## is answered rather than raised, with no degrees of freedom below zero. %!test %! [p, tbl] = kruskalwallis ((1:5)', [], 'off'); %! assert_equal (p, NaN); %! assert_equal (cell2mat (tbl(2:4,2:3)), [0, 0; 10, 4; 10, 4]); %!test %! [p, tbl] = kruskalwallis (1:5, [], 'off'); %! assert_equal (p, NaN); %! assert_equal (cell2mat (tbl(2:4,2:3)), [0, 0; 10, 4; 10, 4]); %!test %! [p, tbl] = kruskalwallis ([], [], 'off'); %! assert_equal (p, NaN); %! assert_equal (cell2mat (tbl(2:4,2:3)), zeros (3, 2)); %!test %! ## Below the resolution of 1 - chi2cdf, values from MATLAB R2024a %! p = kruskalwallis ((1:200)', [ones(100, 1); 2 * ones(100, 1)], 'off'); %! assert_equal (p, 2.5239394239903e-34, -1e-10); %!shared kw, kwg %! kw = [2.1, 3.4, 1.9, 5.6, 4.4, 3.8, 2.7, 6.1, 3.3, 4.9, 1.2, 2.8, 0.9, ... %! 2.2, 3.1, 1.7, 2.5, 0.4, 1.9, 3.6, 2.0, 1.1, 1.8, 2.9, 2.2, 4.1, ... %! 4.9, 2.6, 3.0, 4.8]'; %! kwg = [ones(10, 1); 2 * ones(12, 1); 3 * ones(8, 1)]; ## Effect size against the R package effectsize 1.0.3, rank_eta_squared %!test %! [~, tbl, stats] = kruskalwallis (kw, kwg, 'off'); %! assert_equal (stats.EtaSquared, 0.29653567934214958, -1e-13); %! ## The bounds put H at the upper and lower 2.5% points of the noncentral %! ## chi-square distribution %! lambda = stats.EtaSquaredCI * tbl{3,3}; %! assert_equal (ncx2cdf (tbl{2,5}, 2, lambda), [0.975, 0.025], -1e-10); %!test %! [~, tbl, stats] = kruskalwallis (kw, kwg, 'off', 'Alpha', 0.1); %! lambda = stats.EtaSquaredCI * tbl{3,3}; %! assert_equal (ncx2cdf (tbl{2,5}, 2, lambda), [0.95, 0.05], -1e-10); %!test %! x = [1, 2, 2, 3, 3, 3, 4, 5, 2, 2, 3, 3, 4, 6, 5, 6, 6, 7]'; %! g = [ones(8, 1); 2 * ones(6, 1); 3 * ones(4, 1)]; %! [~, ~, stats] = kruskalwallis (x, g, 'off'); %! assert_equal (stats.EtaSquared, 0.38833809693938293, -1e-13); %! assert_equal (stats.EtaSquaredCI(2), 1); %!test %! [~, ~, stats] = kruskalwallis ([1, 5, 2, 8, 4, 3, 7, 6, 2, 9, 1, 3]', ... %! [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3]', 'off'); %! assert_equal (stats.EtaSquared, 0); %! assert_equal (stats.EtaSquaredCI(1), 0); %!test %! rand ('state', 1); %! [~, ~, stats] = kruskalwallis (kw, kwg, 'off', 'ConfidenceIntervalType', ... %! 'bootstrap', 'NumBootstraps', 200); %! ci = stats.EtaSquaredCI; %! assert_equal ([ci(1) <= ci(2), ci(1) >= 0, ci(2) <= 1], true (1, 3)); %!error ... %! kruskalwallis ([1, 2, 3, 4], [1, 1, 2, 2], 'off', 'Foo', 1) %!error ... %! kruskalwallis ([1, 2, 3, 4], [1, 1, 2, 2], 'off', 'Alpha', 1) %!error ... %! kruskalwallis ([1, 2, 3, 4], [1, 1, 2, 2], 'off', ... %! 'ConfidenceIntervalType', 'none') %!error ... %! kruskalwallis ([1, 2, 3, 4], [1, 1, 2, 2], 'off', 'NumBootstraps', 0) statistics-release-2.1.2/inst/Hypothesis_Testing/kstest.m000066400000000000000000000423121527311104300236270ustar00rootroot00000000000000## Copyright (C) 2022-2025 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} kstest (@var{x}) ## @deftypefnx {statistics} {@var{h} =} kstest (@var{x}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{h}, @var{p}] =} kstest (@dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{p}, @var{ksstat}, @var{cv}] =} kstest (@dots{}) ## ## Single sample Kolmogorov-Smirnov (K-S) goodness-of-fit hypothesis test. ## ## @code{@var{h} = kstest (@var{x})} performs a Kolmogorov-Smirnov (K-S) test to ## determine if a random sample @var{x} could have come from a standard normal ## distribution. @var{h} indicates the results of the null hypothesis test. ## ## @itemize ## @item @var{h} = 0 => Do not reject the null hypothesis at the 5% significance ## @item @var{h} = 1 => Reject the null hypothesis at the 5% significance ## @end itemize ## ## @var{x} is a vector representing a random sample from some unknown ## distribution with a cumulative distribution function F(X). Missing values ## declared as NaNs in @var{x} are ignored. ## ## @code{@var{h} = kstest (@var{x}, @var{name}, @var{value})} returns ## a test decision for a single-sample K-S test with additional options ## specified by one or more @var{Name}-@var{Value} pair arguments as shown ## below. ## ## @multitable @columnfractions 0.15 0.8 ## @headitem Name @tab Value ## @item @qcode{'alpha'} @tab A numeric scalar between 0 and 1 specifying ## th ## the significance level. Default is 0.05 for 5% significance. ## ## @item @qcode{'CDF'} @tab The hypothesized CDF under the null hypothesis. ## It can be specified as a function handle of an existing cdf function, a ## character vector defining a probability distribution with default parameters, ## a probability distribution object, or a two-column matrix. If not provided, ## the default is the standard normal, @math{N(0,1)}. The one-sample ## Kolmogorov-Smirnov test is only valid for continuous cumulative distribution ## functions, and requires the CDF to be predetermined. The result is not ## accurate if CDF is estimated from the data. ## ## @item @qcode{'tail'} @tab A string indicating the type of test: ## @end multitable ## @multitable @columnfractions 0.15 0.5 ## @item @qcode{'unequal'} @tab "F(X) not equal to CDF(X)" (two-sided) ## (Default) ## ## @item @qcode{'larger'} @tab "F(X) > CDF(X)" (one-sided) ## ## @item @qcode{'smaller'} @tab "F(X) < CDF(X)" (one-sided) ## @end multitable ## ## Let S(X) be the empirical c.d.f. estimated from the sample vector @var{x}, ## F(X) be the corresponding true (but unknown) population c.d.f., and CDF be ## the known input c.d.f. specified under the null hypothesis. ## For @code{tail} = "unequal", "larger", and "smaller", the test statistics are ## max|S(X) - CDF(X)|, max[S(X) - CDF(X)], and max[CDF(X) - S(X)], respectively. ## ## @code{[@var{h}, @var{p}] = kstest (@dots{})} also returns the asymptotic ## p-value @var{p}. ## ## @code{[@var{h}, @var{p}, @var{ksstat}] = kstest (@dots{})} returns the K-S ## test statistic @var{ksstat} defined above for the test type indicated by the ## "tail" option ## ## In the matrix version of CDF, column 1 contains the x-axis data and column 2 ## the corresponding y-axis c.d.f data. Since the K-S test statistic will ## occur at one of the observations in @var{x}, the calculation is most ## efficient when CDF is only specified at the observations in @var{x}. When ## column 1 of CDF represents x-axis points independent of @var{x}, CDF is ## linearly interpolated at the observations found in the vector @var{x}. In ## this case, the interval along the x-axis (the column 1 spread of CDF) must ## span the observations in @var{x} for successful interpolation. ## ## The decision to reject the null hypothesis is based on comparing the p-value ## @var{p} with the "alpha" value, not by comparing the statistic @var{ksstat} ## with the critical value @var{cv}. @var{cv} is computed separately using an ## approximate formula or by interpolation using Miller's approximation table. ## The formula and table cover the range 0.01 <= "alpha" <= 0.2 for two-sided ## tests and 0.005 <= "alpha" <= 0.1 for one-sided tests. CV is returned as NaN ## if "alpha" is outside this range. Since CV is approximate, a comparison of ## @var{ksstat} with @var{cv} may occasionally lead to a different conclusion ## than a comparison of @var{p} with "alpha". ## ## @seealso{kstest2, cdfplot, adtest, jbtest, lillietest, swtest} ## @end deftypefn function [H, pValue, ksstat, cV] = kstest (x, varargin) ## Check input parameters if (nargin < 1) error ("kstest: too few inputs."); endif if (! isvector (x) || ! isreal (x)) error ("kstest: X must be a vector of real numbers."); endif ## Add defaults alpha = 0.05; tail = 'unequal'; CDF = []; ## Parse extra parameters if (length (varargin) > 0 && mod (numel (varargin), 2) == 0) [~, prop] = parseparams (varargin); while (! isempty (prop)) switch (lower (prop{1})) case 'alpha' alpha = prop{2}; case 'tail' tail = prop{2}; case 'cdf' CDF = prop{2}; otherwise error ("kstest: unknown option '%s'.", prop{1}); endswitch prop = prop(3:end); endwhile elseif (mod (numel (varargin), 2) != 0) error ("kstest: optional parameters must be in name/value pairs."); endif ## Check for valid alpha and tail parameters if (! isnumeric (alpha) || isnan (alpha) || ! isscalar (alpha) ... || alpha <= 0 || alpha >= 1) error ("kstest: alpha must be a numeric scalar in the range (0,1)."); endif if (! isa (tail, 'char')) error ("kstest: tail argument must be a string."); elseif (sum (strcmpi (tail, {'unequal', 'larger', 'smaller'})) < 1) error ("kstest: tail value must be either 'both', right' or 'left'."); endif ## Remove NaNs, get sample size and compute empirical cdf x(isnan (x)) = []; n = length (x); [sampleCDF, x] = ecdf (x); ## Remove 1st element x = x(2:end); ## Check the hypothesized CDF specified under the null hypothesis. ## No CDF was provided (use default) if (isempty (CDF)) xCDF = x; yCDF = normcdf (x, 0, 1); ## If CDF is a function handle elseif (isa (CDF, 'function_handle')) xCDF = x; yCDF = feval (CDF, x); if (! isequal (size (xCDF), size (yCDF))) error ("kstest: invalid function handle."); endif ## If CDF is character vector elseif (isa (CDF, 'char') && isvector (CDF)) ## Check for supported distributions PDO = makedist (); if (! any (strcmpi (PDO, CDF))) error ("kstest: '%s' is not a supported distribution.", CDF); endif pd = makedist (CDF); xCDF = x; yCDF = pd.cdf (x); ## If CDF is a probability distribution object elseif (isobject (CDF)) PDO = makedist (); PDO = cellfun (@(x) sprintf ("prob.%sDistribution", x), PDO, ... 'UniformOutput', false); if (! any (isa (CDF, PDO))) error ("kstest: 'CDF' must be a probability distribution object."); endif xCDF = x; yCDF = CDF.cdf (x); ## If CDF is numerical elseif (! isempty (CDF) && isnumeric (CDF)) if (size (CDF, 2) != 2) error ("kstest: numerical CDF should have only 2 columns."); endif CDF(isnan (sum (CDF, 2)),:) = []; if (size (CDF, 1) == 0) error ("kstest: numerical CDF should have at least one row."); endif ## Sort numerical CDF [xCDF, i] = sort (CDF(:,1)); yCDF = CDF(i,2); ## Check that numerical CDF is incrementally sorted ydiff = diff (yCDF); if (any (ydiff < 0)) error ("kstest: non-incrementing numerical CDF."); endif ## Remove duplicates. Check for consistency rd = find (diff (xCDF) == 0); if (! isempty (rd)) if (! all (ydiff(rd) == 0)) error ("kstest: wrong duplicates in numerical CDF."); endif xCDF(rd) = []; yCDF(rd) = []; endif ## Invalid value parsed as CDF optional argument else error ("kstest: invalid value parsed as CDF optional argument."); endif ## Check if CDF is specified at the observations in X and assign 2nd column ## of numerical CDF to null CDF if (isequal (x, xCDF)) nCDF = yCDF; ## Otherwise interpolate the numerical CDF to assign values to the null CDF else ## Check that 1st column range bounds the observations in X if (x(1) < xCDF(1) || x(end) > xCDF(end)) error ("kstest: wrong span in CDF."); endif nCDF = interp1 (xCDF, yCDF, x); endif ## Calculate the suitable KS statistic according to tail switch (tail) case 'unequal' # 2-sided test: T = max|S(x) - CDF(x)|. delta1 = sampleCDF(1:end - 1) - nCDF; delta2 = sampleCDF(2:end) - nCDF; deltaCDF = abs ([delta1; delta2]); case 'smaller' # 1-sided test: T = max[CDF(x) - S(x)]. delta1 = nCDF - sampleCDF(1:end - 1); delta2 = nCDF - sampleCDF(2:end); deltaCDF = [delta1; delta2]; case 'larger' # 1-sided test: T = max[S(x) - CDF(x)]. delta1 = sampleCDF(1:end - 1) - nCDF; delta2 = sampleCDF(2:end) - nCDF; deltaCDF = [delta1; delta2]; endswitch ksstat = max (deltaCDF); ## Compute the asymptotic P-value approximation if (strcmpi (tail, 'unequal')) # 2-sided test s = n * ksstat ^ 2; ## For d values that are in the far tail of the distribution (i.e. ## p-values > .999), the following lines will speed up the computation ## significantly, and provide accuracy up to 7 digits. if ((s > 7.24) || ((s > 3.76) && (n > 99))) pValue = 2 * exp (-(2.000071 + 0.331 / sqrt (n) + 1.409 / n) * s); else ## Express d as d = (k-h)/n, where k is a +ve integer and 0 < h < 1. k = ceil (ksstat * n); h = k - ksstat * n; m = 2 * k - 1; ## Create the H matrix, according to Marsaglia et al. if (m > 1) c = 1 ./ gamma ((1:m)' + 1); r = zeros (1,m); r(1) = 1; r(2) = 1; T = toeplitz (c, r); T(:,1) = T(:,1) - (h .^ (1:m)') ./ gamma ((1:m)' + 1); T(m,:) = fliplr (T(:,1)'); T(m,1) = (1 - 2 * h ^ m + max (0, 2 * h - 1) ^m) / gamma (m+1); else T = (1 - 2 * h ^ m + max (0, 2 * h - 1) ^ m) / gamma (m+1); endif ## Scaling before raising the matrix to a power if (! isscalar (T)) lmax = max (eig (T)); T = (T ./ lmax) ^ n; else lmax = 1; endif pValue = 1 - exp (gammaln (n+1) + n * log (lmax) - n * log (n)) * T(k,k); endif else # 1-sided test t = n * ksstat; k = ceil (t):n; pValue = sum (exp (log (t) - n * log (n) + gammaln (n + 1) ... - gammaln (k + 1) - gammaln (n - k + 1) + k .* log (k - t) ... + (n - k - 1) .* log (t + n - k))); endif ## Return hypothesis test H = (pValue < alpha); ## Calculate critical Value (cV) if requested if (nargout > 3) ## The critical value table used below is expressed in reference to a ## 1-sided significance level. Hence alpha is halved for a two-sided test. if (strcmpi (tail, 'unequal')) # 2-sided test alpha1 = alpha / 2; else # 1-sided test alpha1 = alpha; endif if ((alpha1 >= 0.005) && (alpha1 <= 0.10)) ## If the sample size 'n' is greater than 20, use Miller's approximation ## Otherwise interpolate into his 'exact' table. if (n <= 20) # Small sample exact values. ## Exact K-S test critical values based on Miller's approximation. a1 = [0.00500, 0.01000, 0.02500, 0.05000, 0.10000]'; exact = [0.99500, 0.99000, 0.97500, 0.95000, 0.90000; ... 0.92929, 0.90000, 0.84189, 0.77639, 0.68377; ... 0.82900, 0.78456, 0.70760, 0.63604, 0.56481; ... 0.73424, 0.68887, 0.62394, 0.56522, 0.49265; ... 0.66853, 0.62718, 0.56328, 0.50945, 0.44698; ... 0.61661, 0.57741, 0.51926, 0.46799, 0.41037; ... 0.57581, 0.53844, 0.48342, 0.43607, 0.38148; ... 0.54179, 0.50654, 0.45427, 0.40962, 0.35831; ... 0.51332, 0.47960, 0.43001, 0.38746, 0.33910; ... 0.48893, 0.45662, 0.40925, 0.36866, 0.32260; ... 0.46770, 0.43670, 0.39122, 0.35242, 0.30829; ... 0.44905, 0.41918, 0.37543, 0.33815, 0.29577; ... 0.43247, 0.40362, 0.36143, 0.32549, 0.28470; ... 0.41762, 0.38970, 0.34890, 0.31417, 0.27481; ... 0.40420, 0.37713, 0.33760, 0.30397, 0.26588; ... 0.39201, 0.36571, 0.32733, 0.29472, 0.25778; ... 0.38086, 0.35528, 0.31796, 0.28627, 0.25039; ... 0.37062, 0.34569, 0.30936, 0.27851, 0.24360; ... 0.36117, 0.33685, 0.30143, 0.27136, 0.23735; ... 0.35241, 0.32866, 0.29408, 0.26473, 0.23156]; cV = spline (a1 , exact(n,:)' , alpha1); else # Large sample approximate values. A = 0.09037 * (-log10 (alpha1)) .^ 1.5 + 0.01515 * ... log10 (alpha1) .^ 2 - 0.08467 * alpha1 - 0.11143; asymptoticStat = sqrt (-0.5 * log (alpha1) ./ n); cV = asymptoticStat - 0.16693 ./ n - A ./ n .^ 1.5; cV = min (cV, 1 - alpha1); endif else cV = NaN; endif endif endfunction %!demo %! ## Use the stock return data set to test the null hypothesis that the data %! ## come from a standard normal distribution against the alternative %! ## hypothesis that the population CDF of the data is larger that the %! ## standard normal CDF. %! %! load stockreturns; %! x = stocks(:,2); %! [h, p, k, c] = kstest (x, 'Tail', 'larger') %! %! ## Compute the empirical CDF and plot against the standard normal CDF %! [f, x_values] = ecdf (x); %! h1 = plot (x_values, f); %! hold on; %! h2 = plot (x_values, normcdf (x_values), 'r--'); %! set (h1, 'LineWidth', 2); %! set (h2, 'LineWidth', 2); %! legend ([h1, h2], 'Empirical CDF', 'Standard Normal CDF', ... %! 'Location', 'southeast'); %! title ('Empirical CDF of stock return data against standard normal CDF') ## Test input %!error kstest () %!error kstest (ones (2, 4)) %!error kstest ([2, 3, 5, 3+3i]) %!error kstest ([2, 3, 4, 5, 6], 'opt', 0.51) %!error ... %! kstest ([2, 3, 4, 5, 6], 'tail') %!error ... %! kstest ([2,3,4,5,6],'alpha', [0.05, 0.05]) %!error ... %! kstest ([2, 3, 4, 5, 6], 'alpha', NaN) %!error ... %! kstest ([2, 3, 4, 5, 6], 'tail', 0) %!error ... %! kstest ([2,3,4,5,6], 'tail', 'whatever') %!error ... %! kstest ([1, 2, 3, 4, 5], 'CDF', @(x) repmat (x, 2, 3)) %!error ... %! kstest ([1, 2, 3, 4, 5], 'CDF', 'somedist') %!error ... %! kstest ([1, 2, 3, 4, 5], 'CDF', cvpartition (5, 'resubstitution')) %!error ... %! kstest ([2, 3, 4, 5, 6], 'alpha', 0.05, 'CDF', [2, 3, 4; 1, 3, 4; 1, 2, 1]) %!error ... %! kstest ([2, 3, 4, 5, 6], 'alpha', 0.05, 'CDF', nan (5, 2)) %!error ... %! kstest ([2, 3, 4, 5, 6], 'CDF', [2, 3; 1, 4; 3, 2]) %!error ... %! kstest ([2, 3, 4, 5, 6], 'CDF', [2, 3; 2, 4; 3, 5]) %!error ... %! kstest ([2, 3, 4, 5, 6], 'CDF', {1, 2, 3, 4, 5}) ## Test results %!test %! load examgrades %! [h, p] = kstest (grades(:,1)); %! assert_equal (h, true); %! assert_equal (p, 7.58603305206105e-107, 1e-14); %!test %! load examgrades %! [h, p] = kstest (grades(:,1), 'CDF', @(x) normcdf (x, 75, 10)); %! assert_equal (h, false); %! assert_equal (p, 0.5612, 1e-4); %!test %! load examgrades %! x = grades(:,1); %! test_cdf = makedist ('tlocationscale', 'mu', 75, 'sigma', 10, 'nu', 1); %! [h, p] = kstest (x, 'alpha', 0.01, 'CDF', test_cdf); %! assert_equal (h, true); %! assert_equal (p, 0.0021, 1e-4); %!test %! load stockreturns %! x = stocks(:,3); %! [h,p,k,c] = kstest (x, 'Tail', 'larger'); %! assert_equal (h, true); %! assert_equal (p, 5.085438806199252e-05, 1e-14); %! assert_equal (k, 0.2197, 1e-4); %! assert_equal (c, 0.1207, 1e-4); statistics-release-2.1.2/inst/Hypothesis_Testing/kstest2.m000066400000000000000000000170661527311104300237210ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} kstest2 (@var{x1}, @var{x2}) ## @deftypefnx {statistics} {@var{h} =} kstest2 (@var{x1}, @var{x2}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{h}, @var{p}] =} kstest2 (@dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{p}, @var{ks2stat}] =} kstest2 (@dots{}) ## ## Two-sample Kolmogorov-Smirnov goodness-of-fit hypothesis test. ## ## @code{@var{h} = kstest2 (@var{x1}, @var{x2})} returns a test decision for the ## null hypothesis that the data in vectors @var{x1} and @var{x2} are from the ## same continuous distribution, using the two-sample Kolmogorov-Smirnov test. ## The alternative hypothesis is that @var{x1} and @var{x2} are from different ## continuous distributions. The result @var{h} is 1 if the test rejects the ## null hypothesis at the 5% significance level, and 0 otherwise. ## ## @code{@var{h} = kstest2 (@var{x1}, @var{x2}, @var{name}, @var{value})} ## returns a test decision for a two-sample Kolmogorov-Smirnov test with ## additional options specified by one or more name-value pair arguments as ## shown below. ## ## @multitable @columnfractions 0.20 0.8 ## @headitem @var{Name} @tab @var{Value} ## @item "alpha" @tab A value @var{alpha} between 0 and 1 specifying the ## significance level. Default is 0.05 for 5% significance. ## ## @item "tail" @tab A string indicating the type of test: ## @end multitable ## ## @multitable @columnfractions 0.2 0.77 ## @item "unequal" @tab "F(X1) not equal to F(X2)" (two-sided) [Default] ## ## @item "larger" @tab "F(X1) > F(X2)" (one-sided) ## ## @item "smaller" @tab "F(X1) < F(X2)" (one-sided) ## @end multitable ## ## The two-sided test uses the maximum absolute difference between the cdfs of ## the distributions of the two data vectors. The test statistic is ## @code{D* = max(|F1(x) - F2(x)|)}, where F1(x) is the proportion of @var{x1} ## values less or equal to x and F2(x) is the proportion of @var{x2} values less ## than or equal to x. The one-sided test uses the actual value of the ## difference between the cdfs of the distributions of the two data vectors ## rather than the absolute value. The test statistic is ## @code{D* = max(F1(x) - F2(x))} or @code{D* = max(F2(x) - F1(x))} for ## @code{tail} = "larger" or "smaller", respectively. ## ## @code{[@var{h}, @var{p}] = kstest2 (@dots{})} also returns the ## asymptotic p-value @var{p}. ## ## @code{[@var{h}, @var{p}, @var{ks2stat}] = kstest2 (@dots{})} also returns ## the Kolmogorov-Smirnov test statistic @var{ks2stat} defined above for the ## test type indicated by @code{tail}. ## ## @seealso{kstest, cdfplot} ## @end deftypefn function [H, pValue, ks2stat] = kstest2 (x1, x2, varargin) ## Check input parameters if nargin < 2 error ("kstest2: Too few inputs."); endif if ! isvector (x1) || ! isreal (x1) || ! isvector (x2) || ! isreal (x2) error ("kstest2: X1 and X2 must be vectors of real numbers."); endif ## Add defaults alpha = 0.05; tail = 'unequal'; ## Parse extra parameters if nargin > 2 && mod (numel (varargin), 2) == 0 [~, prop] = parseparams (varargin); while (! isempty (prop)) switch (lower (prop{1})) case 'alpha' alpha = prop{2}; case 'tail' tail = prop{2}; otherwise error ("kstest2: Unknown option %s", prop{1}); endswitch prop = prop(3:end); endwhile elseif nargin > 2 error ("kstest2: optional parameters must be in name/value pairs."); endif ## Check for valid alpha and tail parameters if (! isnumeric (alpha) || isnan (alpha) || ! isscalar (alpha) ... || alpha <= 0 || alpha >= 1) error ("kstest2: alpha must be a numeric scalar in the range (0,1)."); endif if ! isa (tail, 'char') error ("kstest2: tail argument must be a string"); elseif sum (strcmpi (tail, {'unequal', 'larger', 'smaller'})) < 1 error (strcat ("kstest2: tail value must be either", ... " 'unequal', 'larger' or 'smaller'.")); endif ## Make x1 and x2 column vectors x1 = x1(:); x2 = x2(:); ## Remove missing values (NaN) x1(isnan (x1)) = []; x2(isnan (x2)) = []; ## Check for remaining data in both vectors if isempty (x1) error ("kstest2: Not enough data in X1"); elseif isempty (x2) error ("kstest2: Not enough data in X2"); endif ## Calculate F1(x) and F2(x) binEdges = [-inf; sort([x1;x2]); inf]; binCounts1 = histc (x1 , binEdges, 1); binCounts2 = histc (x2 , binEdges, 1); sumCounts1 = cumsum (binCounts1) ./ sum (binCounts1); sumCounts2 = cumsum (binCounts2) ./ sum (binCounts2); sampleCDF1 = sumCounts1(1:end - 1); sampleCDF2 = sumCounts2(1:end - 1); ## Calculate the suitable KS statistic according to tail switch tail case 'unequal' # 2-sided test: T = max|F1(x) - F2(x)|. deltaCDF = abs (sampleCDF1 - sampleCDF2); case 'smaller' # 1-sided test: T = max[F2(x) - F1(x)]. deltaCDF = sampleCDF2 - sampleCDF1; case 'larger' # 1-sided test: T = max[F1(x) - F2(x)]. deltaCDF = sampleCDF1 - sampleCDF2; endswitch ks2stat = max (deltaCDF); ## Compute the asymptotic P-value approximation n_x1 = length (x1); n_x2 = length (x2); n = n_x1 * n_x2 /(n_x1 + n_x2); lambda = max ((sqrt (n) + 0.12 + 0.11 / sqrt (n)) * ks2stat, 0); if strcmpi (tail, 'unequal') # 2-sided test v = [1:101]; pValue = 2 * sum ((-1) .^ (v-1) .* exp (-2 * lambda * lambda * v .^ 2)); pValue = min (max (pValue, 0), 1); else # 1-sided test pValue = exp (-2 * lambda * lambda); endif ## Return hypothesis test H = (alpha >= pValue); endfunction ## Test input %!error kstest2 ([1,2,3,4,5,5]) %!error kstest2 (ones (2,4), [1,2,3,4,5,5]) %!error kstest2 ([2,3,5,7,3+3i], [1,2,3,4,5,5]) %!error kstest2 ([2,3,4,5,6],[3;5;7;8;7;6;5],'tail') %!error kstest2 ([2,3,4,5,6],[3;5;7;8;7;6;5],'tail', 'whatever') %!error kstest2 ([2,3,4,5,6],[3;5;7;8;7;6;5],'badoption', 0.51) %!error kstest2 ([2,3,4,5,6],[3;5;7;8;7;6;5],'tail', 0) %!error kstest2 ([2,3,4,5,6],[3;5;7;8;7;6;5],'alpha', 0) %!error kstest2 ([2,3,4,5,6],[3;5;7;8;7;6;5],'alpha', NaN) %!error kstest2 ([NaN,NaN,NaN,NaN,NaN],[3;5;7;8;7;6;5],'tail', 'unequal') ## Test results %!test %! load examgrades %! [h, p] = kstest2 (grades(:,1), grades(:,2)); %! assert_equal (h, false); %! assert_equal (p, 0.1222791870137312, 1e-14); %!test %! load examgrades %! [h, p] = kstest2 (grades(:,1), grades(:,2), 'tail', 'larger'); %! assert_equal (h, false); %! assert_equal (p, 0.1844421391011258, 1e-14); %!test %! load examgrades %! [h, p] = kstest2 (grades(:,1), grades(:,2), 'tail', 'smaller'); %! assert_equal (h, false); %! assert_equal (p, 0.06115357930171663, 1e-14); %!test %! load examgrades %! [h, p] = kstest2 (grades(:,1), grades(:,2), 'tail', 'smaller', 'alpha', 0.1); %! assert_equal (h, true); %! assert_equal (p, 0.06115357930171663, 1e-14); statistics-release-2.1.2/inst/Hypothesis_Testing/lillietest.m000066400000000000000000000721741527311104300244750ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} lillietest (@var{x}) ## @deftypefnx {statistics} {@var{h} =} lillietest (@var{x}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{h}, @var{p}] =} lillietest (@dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{p}, @var{kstat}, @var{critval}] =} lillietest (@dots{}) ## ## Lilliefors goodness-of-fit hypothesis test. ## ## @code{@var{h} = lillietest (@var{x})} tests the null hypothesis that the ## sample in the vector @var{x} comes from a normal distribution with unknown ## mean and variance, against the alternative that it does not, using the ## Lilliefors test. @var{h} is 1 if the test rejects the null at the 5% ## significance level and 0 otherwise. ## ## The Lilliefors statistic is the Kolmogorov-Smirnov statistic --- the maximum ## absolute difference between the empirical cumulative distribution function of ## @var{x} and the cumulative distribution function of the hypothesized family ## with parameters estimated from @var{x}. Because the parameters are ## estimated, the null distribution of the statistic differs from that of the ## ordinary Kolmogorov-Smirnov test. ## ## The following @qcode{Name-Value} pairs are supported: ## ## @multitable @columnfractions 0.2 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Distribution'} @tab The hypothesized family: ## @qcode{'normal'} (default), @qcode{'exponential'}, or ## @qcode{'extreme value'}. The parameters are estimated from @var{x}: mean and ## standard deviation for the normal, mean for the exponential, and location and ## scale for the extreme value distribution. ## ## @item @qcode{'Alpha'} @tab The significance level, a scalar. Without ## @qcode{'MCTol'} it must lie in @math{[0.001, 0.5]} (the tabulated range); ## with @qcode{'MCTol'} it may be any value in @math{(0, 1)}. The default is ## @math{0.05}. ## ## @item @qcode{'MCTol'} @tab Maximum Monte-Carlo standard error for the ## p-value. When supplied, the p-value and critical value are computed by ## Monte-Carlo simulation instead of by interpolating the embedded table. ## @end multitable ## ## @code{[@var{h}, @var{p}, @var{kstat}, @var{critval}] = lillietest (@dots{})} ## also returns the p-value @var{p}, the test statistic @var{kstat}, and the ## critical value @var{critval}. Without @qcode{'MCTol'} the p-value is clamped ## to the tabulated range @math{[0.001, 0.5]} and a warning is issued when it ## lies outside. The warning is an addition here: MATLAB clamps silently, so a ## p-value reported as @math{0.001} or @math{0.5} there may be a bound rather ## than an estimate, with nothing to say so. ## ## @seealso{adtest, jbtest, kstest, swtest} ## @end deftypefn function [h, p, kstat, critval] = lillietest (x, varargin) if (nargin < 1) print_usage (); endif if (! (isnumeric (x) && isreal (x) && isvector (x))) error ("lillietest: X must be a vector of real values."); endif x = x(! isnan (x)); x = x(:); n = numel (x); if (n < 4) error ("lillietest: X must contain at least four non-missing values."); endif ## Defaults and Name-Value parsing distribution = "normal"; alpha = 0.05; mctol = []; if (mod (numel (varargin), 2) != 0) error ("lillietest: optional arguments must be Name-Value pairs."); endif for k = 1:2:numel (varargin) if (! ischar (varargin{k})) error ("lillietest: parameter names must be character vectors."); endif switch (lower (varargin{k})) case "distribution" distribution = lower (varargin{k+1}); case "alpha" alpha = varargin{k+1}; case "mctol" mctol = varargin{k+1}; otherwise error ("lillietest: unknown parameter name '%s'.", varargin{k}); endswitch endfor if (strcmp (distribution, "ev")) distribution = "extreme value"; endif if (! any (strcmp (distribution, {"normal", "exponential", "extreme value"}))) error ("lillietest: unrecognised 'Distribution' value."); endif domc = ! isempty (mctol); if (! (isnumeric (alpha) && isscalar (alpha) && isreal (alpha) ... && alpha > 0 && alpha < 1)) error ("lillietest: 'Alpha' must be a scalar in the range (0,1)."); endif if (! domc && (alpha < 0.001 || alpha > 0.5)) error ("lillietest: 'Alpha' must be in [0.001, 0.5] unless 'MCTol' is set."); endif if (domc && ! (isnumeric (mctol) && isscalar (mctol) && isreal (mctol) ... && mctol > 0)) error ("lillietest: 'MCTol' must be a positive scalar."); endif ## Lilliefors (Kolmogorov-Smirnov) statistic with estimated parameters kstat = lillietest_stat_ (x, distribution); if (domc) ## Monte-Carlo p-value and critical value. REPS is chosen so the worst-case ## standard error of the estimated p-value (at p = 0.5) is below MCTOL. reps = max (1000, ceil (0.25 / mctol ^ 2)); sim = lillietest_simulate_ (n, distribution, reps); p = sum (sim >= kstat) / reps; critval = quantile (sim, 1 - alpha); else ## Interpolate the p-value and critical value from the embedded tables. [p, critval] = lillietest_interp_ (kstat, n, alpha, distribution); endif h = double (kstat > critval); endfunction ## Kolmogorov-Smirnov statistic of X against DISTRIBUTION fitted to X. function ks = lillietest_stat_ (x, distribution) x = sort (x(:)); n = numel (x); switch (distribution) case "normal" cdf = normcdf ((x - mean (x)) / std (x)); case "exponential" cdf = expcdf (x, mean (x)); case "extreme value" [mu, sigma] = lillietest_evfit_ (x); cdf = evcdf (x, mu, sigma); endswitch ks = max (max ((1:n)' / n - cdf), max (cdf - (0:n-1)' / n)); endfunction ## Interpolate the p-value P and critical value CRITVAL for an observed ## Lilliefors statistic KSTAT at sample size N and level ALPHA from the embedded ## tables for DISTRIBUTION. Critical values scale roughly as 1/sqrt(N), so the ## size interpolation is carried out on sqrt(N)*CV, which is nearly flat in N. function [p, critval] = lillietest_interp_ (kstat, n, alpha, distribution) [sizes, alphas, CV] = lillietest_table_ (distribution); ## Critical values at the ALPHAS grid for this N (scaled size interpolation). S = sqrt (sizes(:)) .* CV; nn = min (max (n, sizes(1)), sizes(end)); cvrow = interp1 (sizes, S, nn, "linear") / sqrt (n); ## Critical value at the requested ALPHA. critval = interp1 (alphas, cvrow, alpha, "linear"); ## P-value: invert the (decreasing) CVROW-versus-ALPHA relation, interpolating ## on a logarithmic ALPHA axis. Outside the tabulated range clamp and warn. xa = fliplr (cvrow); ya = fliplr (log (alphas)); if (kstat >= xa(end)) warning ("lillietest:pTooSmall", ... "lillietest: P is less than the smallest tabulated value; returning 0.001."); p = 0.001; elseif (kstat <= xa(1)) warning ("lillietest:pTooBig", ... "lillietest: P is greater than the largest tabulated value; returning 0.5."); p = 0.5; else p = exp (interp1 (xa, ya, kstat, "linear")); endif endfunction ## Embedded Lilliefors critical-value tables. CV(i,j) is the upper ## ALPHAS(j) quantile of the null Kolmogorov-Smirnov statistic for sample ## size SIZES(i), estimated by Monte-Carlo simulation (1e6 replications for ## the normal and exponential families, 5e5 for the extreme value). function [sizes, alphas, CV] = lillietest_table_ (distribution) sizes = [ ... 4 5 6 7 8 9 10 11 12 13 14 15 16 18 20 22 25 28 32 37 43 50 60 75 90 ... 110 140 180 250 350 500 750 1000]; alphas = [ ... 0.001 0.0025 0.005 0.01 0.025 0.05 0.075 0.1 0.15 0.2 0.25 0.3 0.4 0.5]; switch (distribution) case "normal" CV = [ ... 0.432607 0.427314 0.421544 0.412974 0.395412 0.375069 0.358773 ... 0.344895 0.321124 0.302680 0.293294 0.285351 0.271140 0.258179 ; ... 0.438947 0.425875 0.413107 0.396753 0.366969 0.342917 0.329250 ... 0.318907 0.302820 0.289536 0.277772 0.266904 0.247692 0.233374 ; ... 0.424197 0.405483 0.388133 0.370576 0.345884 0.323510 0.308511 ... 0.297386 0.281157 0.268855 0.258699 0.249711 0.233599 0.218785 ; ... 0.401078 0.382781 0.367417 0.350468 0.325084 0.303960 0.290422 ... 0.279960 0.264135 0.252140 0.242311 0.233862 0.219170 0.206117 ; ... 0.382388 0.364242 0.348914 0.332627 0.308427 0.287768 0.274805 ... 0.265040 0.250196 0.238797 0.229315 0.221085 0.207026 0.194764 ; ... 0.364615 0.347571 0.333138 0.317227 0.293796 0.274126 0.261612 ... 0.252164 0.238102 0.227230 0.218245 0.210363 0.196932 0.185128 ; ... 0.350372 0.333142 0.318717 0.303202 0.280639 0.261755 0.249869 ... 0.240907 0.227381 0.217025 0.208460 0.200969 0.188100 0.176802 ; ... 0.337136 0.320659 0.306638 0.291628 0.269841 0.251607 0.240013 ... 0.231260 0.218243 0.208146 0.199874 0.192696 0.180365 0.169564 ; ... 0.326036 0.309246 0.295603 0.280640 0.259588 0.241865 0.230680 ... 0.222431 0.209891 0.200234 0.192271 0.185389 0.173559 0.163133 ; ... 0.314469 0.298673 0.285316 0.271092 0.250789 0.233523 0.222730 ... 0.214592 0.202490 0.193221 0.185511 0.178844 0.167435 0.157421 ; ... 0.305048 0.288845 0.276165 0.262540 0.242548 0.225793 0.215372 ... 0.207618 0.195917 0.186890 0.179512 0.173080 0.161981 0.152232 ; ... 0.295728 0.280416 0.267977 0.254389 0.235139 0.219113 0.208875 ... 0.201248 0.189880 0.181118 0.173979 0.167721 0.156942 0.147554 ; ... 0.287612 0.272464 0.260058 0.246934 0.228260 0.212575 0.202683 ... 0.195333 0.184269 0.175844 0.168839 0.162843 0.152429 0.143337 ; ... 0.273032 0.258786 0.246958 0.234047 0.216321 0.201506 0.192084 ... 0.185039 0.174572 0.166536 0.159949 0.154179 0.144349 0.135730 ; ... 0.260964 0.246747 0.235353 0.223405 0.206317 0.192106 0.183158 ... 0.176435 0.166437 0.158796 0.152459 0.147012 0.137595 0.129406 ; ... 0.249352 0.235477 0.224833 0.213555 0.197044 0.183554 0.175025 ... 0.168686 0.159089 0.151819 0.145781 0.140554 0.131558 0.123713 ; ... 0.235149 0.222673 0.212144 0.201195 0.185787 0.172986 0.164863 ... 0.158889 0.149828 0.142935 0.137273 0.132374 0.123917 0.116555 ; ... 0.222915 0.210979 0.201180 0.190964 0.176099 0.164012 0.156384 ... 0.150657 0.142092 0.135606 0.130265 0.125587 0.117581 0.110586 ; ... 0.210179 0.198110 0.188792 0.179350 0.165482 0.153912 0.146722 ... 0.141333 0.133320 0.127227 0.122176 0.117831 0.110342 0.103795 ; ... 0.196029 0.185358 0.176625 0.167508 0.154654 0.143799 0.136996 ... 0.132002 0.124556 0.118849 0.114129 0.110075 0.103108 0.096989 ; ... 0.181729 0.171874 0.163883 0.155465 0.143695 0.133754 0.127462 ... 0.122841 0.115885 0.110575 0.106155 0.102365 0.095866 0.090223 ; ... 0.169503 0.160647 0.153079 0.145094 0.133664 0.124409 0.118640 ... 0.114311 0.107826 0.102841 0.098792 0.095267 0.089218 0.083942 ; ... 0.155301 0.146647 0.139809 0.132754 0.122473 0.113971 0.108667 ... 0.104718 0.098765 0.094271 0.090541 0.087316 0.081766 0.076932 ; ... 0.139819 0.132195 0.126097 0.119306 0.110058 0.102447 0.097644 ... 0.094080 0.088737 0.084677 0.081312 0.078438 0.073444 0.069100 ; ... 0.127896 0.120937 0.115169 0.109184 0.100722 0.093671 0.089265 ... 0.086023 0.081133 0.077428 0.074397 0.071747 0.067197 0.063233 ; ... 0.116099 0.109584 0.104236 0.098821 0.091194 0.084911 0.080996 ... 0.078010 0.073587 0.070262 0.067502 0.065073 0.060960 0.057379 ; ... 0.102927 0.097209 0.092748 0.087942 0.081104 0.075501 0.071965 ... 0.069305 0.065415 0.062456 0.059984 0.057847 0.054186 0.051006 ; ... 0.091297 0.086224 0.082012 0.077744 0.071708 0.066744 0.063638 ... 0.061317 0.057851 0.055223 0.053058 0.051177 0.047953 0.045135 ; ... 0.077577 0.073285 0.069823 0.066221 0.061083 0.056793 0.054122 ... 0.052157 0.049222 0.046991 0.045141 0.043549 0.040812 0.038427 ; ... 0.065480 0.061877 0.059004 0.055924 0.051649 0.048089 0.045856 ... 0.044200 0.041694 0.039800 0.038244 0.036895 0.034586 0.032573 ; ... 0.055045 0.052013 0.049536 0.046920 0.043271 0.040290 0.038414 ... 0.037032 0.034951 0.033367 0.032059 0.030937 0.029003 0.027311 ; ... 0.045036 0.042528 0.040526 0.038397 0.035409 0.032980 0.031446 ... 0.030319 0.028606 0.027306 0.026244 0.025322 0.023740 0.022364 ; ... 0.039021 0.036838 0.035071 0.033266 0.030704 0.028572 0.027242 ... 0.026260 0.024790 0.023673 0.022750 0.021947 0.020573 0.019381]; case "exponential" CV = [ ... 0.621231 0.596647 0.578555 0.557012 0.521002 0.484365 0.460917 ... 0.444253 0.419905 0.400745 0.383966 0.368475 0.340166 0.316284 ; ... 0.581399 0.556666 0.536434 0.512287 0.474415 0.441820 0.421006 ... 0.404518 0.379200 0.360240 0.344923 0.331829 0.308759 0.287711 ; ... 0.549130 0.522356 0.499491 0.474102 0.439000 0.408508 0.388211 ... 0.373014 0.349883 0.331731 0.316949 0.304399 0.283105 0.264562 ; ... 0.517209 0.491806 0.469835 0.445867 0.410891 0.381419 0.362594 ... 0.348257 0.326600 0.309868 0.295926 0.283728 0.263358 0.245867 ; ... 0.490769 0.464785 0.443930 0.420827 0.387555 0.359052 0.340897 ... 0.327455 0.306965 0.291390 0.278345 0.266966 0.247499 0.230833 ; ... 0.466815 0.442620 0.421629 0.399813 0.367723 0.340727 0.323370 ... 0.310404 0.290787 0.275846 0.263617 0.252940 0.234618 0.218591 ; ... 0.446077 0.422144 0.402122 0.381150 0.350113 0.324100 0.307705 ... 0.295307 0.276634 0.262563 0.250843 0.240710 0.223322 0.208134 ; ... 0.429002 0.404610 0.385513 0.364916 0.335123 0.310642 0.294705 ... 0.282768 0.264905 0.251266 0.240049 0.230302 0.213761 0.199317 ; ... 0.413509 0.390479 0.371529 0.351122 0.322359 0.297998 0.282657 ... 0.271245 0.254188 0.240956 0.230112 0.220761 0.204873 0.191056 ; ... 0.397798 0.374918 0.357659 0.337904 0.310339 0.286979 0.272289 ... 0.261265 0.244793 0.232165 0.221657 0.212660 0.197342 0.184012 ; ... 0.385420 0.362808 0.345258 0.327040 0.300080 0.277503 0.263174 ... 0.252428 0.236426 0.224253 0.214177 0.205439 0.190565 0.177775 ; ... 0.373875 0.351966 0.334784 0.316911 0.290465 0.268630 0.254774 ... 0.244329 0.228761 0.217002 0.207292 0.198913 0.184511 0.172025 ; ... 0.364072 0.342485 0.325739 0.307869 0.282036 0.260677 0.247270 ... 0.237103 0.222032 0.210486 0.201023 0.192872 0.178954 0.166851 ; ... 0.343857 0.323300 0.307894 0.290967 0.266588 0.246316 0.233568 ... 0.224021 0.209856 0.198916 0.189997 0.182308 0.169162 0.157810 ; ... 0.328276 0.309911 0.293642 0.277377 0.254224 0.234822 0.222551 ... 0.213464 0.199829 0.189511 0.180993 0.173601 0.161008 0.150168 ; ... 0.313426 0.295155 0.280713 0.264858 0.242481 0.224028 0.212452 ... 0.203722 0.190762 0.180827 0.172700 0.165711 0.153753 0.143437 ; ... 0.295571 0.277971 0.264150 0.249484 0.228211 0.210880 0.199954 ... 0.191659 0.179378 0.170122 0.162511 0.155881 0.144681 0.134978 ; ... 0.280510 0.263116 0.250524 0.236649 0.216378 0.199786 0.189279 ... 0.181516 0.169932 0.161096 0.153832 0.147583 0.136933 0.127748 ; ... 0.262082 0.246758 0.234743 0.221759 0.202973 0.187268 0.177590 ... 0.170250 0.159334 0.151077 0.144293 0.138403 0.128418 0.119827 ; ... 0.245218 0.231082 0.219361 0.206905 0.189182 0.174581 0.165441 ... 0.158687 0.148603 0.140814 0.134523 0.129092 0.119791 0.111731 ; ... 0.227667 0.214598 0.203750 0.192400 0.175912 0.162384 0.153909 ... 0.147586 0.138135 0.130949 0.125084 0.120027 0.111416 0.103943 ; ... 0.211652 0.199281 0.189172 0.178429 0.163277 0.150705 0.142917 ... 0.137061 0.128254 0.121664 0.116249 0.111518 0.103532 0.096619 ; ... 0.194074 0.182575 0.173232 0.163522 0.149612 0.138049 0.130848 ... 0.125503 0.117451 0.111405 0.106425 0.102130 0.094779 0.088454 ; ... 0.175158 0.164330 0.155762 0.146784 0.134325 0.123906 0.117382 ... 0.112549 0.105375 0.099953 0.095476 0.091623 0.085068 0.079387 ; ... 0.159882 0.149858 0.142202 0.134226 0.122746 0.113283 0.107427 ... 0.103009 0.096380 0.091399 0.087288 0.083769 0.077784 0.072603 ; ... 0.144230 0.135727 0.128659 0.121333 0.111164 0.102668 0.097303 ... 0.093303 0.087350 0.082860 0.079165 0.075970 0.070563 0.065865 ; ... 0.128495 0.120499 0.114330 0.107995 0.098821 0.091225 0.086449 ... 0.082928 0.077630 0.073637 0.070350 0.067503 0.062679 0.058544 ; ... 0.113413 0.106294 0.101052 0.095438 0.087302 0.080475 0.076278 ... 0.073160 0.068548 0.065003 0.062119 0.059610 0.055385 0.051715 ; ... 0.096379 0.090613 0.085925 0.080983 0.074111 0.068417 0.064880 ... 0.062242 0.058272 0.055281 0.052822 0.050704 0.047106 0.044004 ; ... 0.081386 0.076633 0.072761 0.068611 0.062700 0.057906 0.054887 ... 0.052647 0.049306 0.046758 0.044696 0.042923 0.039890 0.037265 ; ... 0.068356 0.064361 0.061005 0.057527 0.052616 0.048557 0.046022 ... 0.044147 0.041356 0.039237 0.037494 0.035999 0.033443 0.031237 ; ... 0.055801 0.052452 0.049770 0.046970 0.042947 0.039645 0.037590 ... 0.036061 0.033791 0.032080 0.030666 0.029444 0.027361 0.025564 ; ... 0.048320 0.045422 0.043178 0.040721 0.037250 0.034406 0.032637 ... 0.031303 0.029314 0.027817 0.026586 0.025531 0.023717 0.022158]; case "extreme value" CV = [ ... 0.467021 0.456860 0.444418 0.431023 0.408276 0.384264 0.365779 ... 0.349871 0.332962 0.323214 0.314608 0.306471 0.291057 0.275911 ; ... 0.448392 0.431006 0.415271 0.395238 0.370420 0.351146 0.337934 ... 0.327333 0.309799 0.295607 0.283706 0.273894 0.258973 0.245929 ; ... 0.421319 0.402657 0.387883 0.371815 0.346615 0.324328 0.311050 ... 0.301127 0.286302 0.274484 0.264125 0.254856 0.238705 0.224770 ; ... 0.398601 0.380627 0.364490 0.348233 0.324999 0.304811 0.291665 ... 0.281572 0.266898 0.255673 0.246363 0.238052 0.223434 0.210266 ; ... 0.377539 0.360570 0.346195 0.330320 0.306864 0.287484 0.275298 ... 0.265885 0.251562 0.240710 0.231741 0.223912 0.210332 0.198229 ; ... 0.361815 0.344239 0.329077 0.313848 0.291618 0.272873 0.261196 ... 0.252438 0.238785 0.228321 0.219688 0.212082 0.199061 0.187662 ; ... 0.346846 0.328814 0.314941 0.300596 0.278715 0.260554 0.249057 ... 0.240517 0.227625 0.217625 0.209319 0.202140 0.189702 0.178789 ; ... 0.331757 0.315718 0.302157 0.288110 0.267152 0.249601 0.238490 ... 0.230278 0.217836 0.208272 0.200438 0.193528 0.181568 0.171059 ; ... 0.320460 0.303888 0.291007 0.276929 0.256887 0.239937 0.229379 ... 0.221394 0.209334 0.200147 0.192548 0.185928 0.174399 0.164215 ; ... 0.309684 0.294064 0.281405 0.267452 0.247970 0.231470 0.221160 ... 0.213316 0.201755 0.192862 0.185483 0.179047 0.167980 0.158141 ; ... 0.299198 0.283407 0.271902 0.258334 0.239607 0.223786 0.213675 ... 0.206326 0.195050 0.186394 0.179276 0.173026 0.162254 0.152888 ; ... 0.290486 0.276319 0.263743 0.250518 0.231948 0.216624 0.206943 ... 0.199721 0.188844 0.180445 0.173514 0.167509 0.157080 0.147923 ; ... 0.281127 0.267349 0.255627 0.243245 0.225333 0.210345 0.200878 ... 0.193781 0.183123 0.174925 0.168261 0.162419 0.152372 0.143462 ; ... 0.267045 0.253562 0.242229 0.230420 0.213477 0.199239 0.190140 ... 0.183476 0.173396 0.165658 0.159319 0.153757 0.144232 0.135816 ; ... 0.254628 0.242018 0.231149 0.219530 0.203026 0.189534 0.181088 ... 0.174655 0.165068 0.157735 0.151650 0.146340 0.137210 0.129251 ; ... 0.243894 0.231515 0.221212 0.210322 0.194282 0.181207 0.173041 ... 0.166941 0.157845 0.150884 0.145026 0.139955 0.131286 0.123601 ; ... 0.230118 0.218163 0.208315 0.197830 0.183121 0.170905 0.163143 ... 0.157357 0.148619 0.142000 0.136513 0.131743 0.123490 0.116309 ; ... 0.218954 0.207136 0.197973 0.187592 0.173576 0.161748 0.154470 ... 0.148986 0.140779 0.134457 0.129275 0.124779 0.117027 0.110225 ; ... 0.206085 0.195001 0.185956 0.176596 0.163038 0.151859 0.145003 ... 0.139799 0.132086 0.126162 0.121259 0.116983 0.109696 0.103309 ; ... 0.191852 0.181144 0.172924 0.164409 0.152019 0.141856 0.135444 ... 0.130619 0.123329 0.117806 0.113257 0.109287 0.102451 0.096493 ; ... 0.178237 0.168926 0.161218 0.153063 0.141587 0.131935 0.125944 ... 0.121462 0.114757 0.109633 0.105382 0.101699 0.095344 0.089798 ; ... 0.165755 0.157078 0.149594 0.142057 0.131446 0.122525 0.117103 ... 0.112954 0.106706 0.101940 0.097987 0.094570 0.088671 0.083521 ; ... 0.151898 0.143496 0.137105 0.130317 0.120396 0.112257 0.107215 ... 0.103351 0.097641 0.093274 0.089688 0.086568 0.081173 0.076472 ; ... 0.137316 0.129609 0.123408 0.117120 0.108162 0.100760 0.096212 ... 0.092774 0.087626 0.083716 0.080489 0.077691 0.072876 0.068630 ; ... 0.124853 0.118346 0.112935 0.107155 0.099075 0.092326 0.088203 ... 0.085039 0.080345 0.076713 0.073747 0.071177 0.066762 0.062898 ; ... 0.113566 0.107243 0.102355 0.097262 0.089748 0.083647 0.079847 ... 0.076958 0.072682 0.069463 0.066772 0.064463 0.060478 0.057010 ; ... 0.100706 0.095348 0.090914 0.086360 0.079839 0.074371 0.070940 ... 0.068405 0.064605 0.061760 0.059375 0.057313 0.053730 0.050627 ; ... 0.089322 0.084347 0.080547 0.076367 0.070562 0.065778 0.062768 ... 0.060509 0.057157 0.054613 0.052499 0.050687 0.047544 0.044801 ; ... 0.076039 0.071818 0.068625 0.065032 0.060008 0.055943 0.053394 ... 0.051499 0.048670 0.046506 0.044702 0.043162 0.040497 0.038171 ; ... 0.064185 0.060831 0.057921 0.054922 0.050793 0.047340 0.045195 ... 0.043590 0.041199 0.039371 0.037852 0.036551 0.034314 0.032327 ; ... 0.053752 0.050888 0.048556 0.046068 0.042607 0.039712 0.037883 ... 0.036532 0.034519 0.032993 0.031719 0.030631 0.028739 0.027076 ; ... 0.043902 0.041496 0.039619 0.037640 0.034817 0.032426 0.030982 ... 0.029891 0.028241 0.026987 0.025957 0.025064 0.023517 0.022179 ; ... 0.038071 0.035964 0.034353 0.032583 0.030154 0.028121 0.026853 ... 0.025915 0.024482 0.023403 0.022504 0.021735 0.020398 0.019227]; endswitch endfunction ## Simulate REPS Lilliefors statistics for samples of size N from the (pivotal) ## standardized DISTRIBUTION, evaluated in chunks to bound memory use. function sim = lillietest_simulate_ (n, distribution, reps) sim = zeros (reps, 1); chunk = max (1, floor (2e5 / n)); done = 0; ii = (1:n)' / n; im = (0:n-1)' / n; while (done < reps) m = min (chunk, reps - done); switch (distribution) case "normal" z = randn (n, m); z = sort (z); c = normcdf ((z - mean (z)) ./ std (z)); case "exponential" z = -log (rand (n, m)); z = sort (z); c = 1 - exp (-z ./ mean (z)); case "extreme value" z = log (-log (rand (n, m))); ## standard EV (minima) draws z = sort (z); [mu, sigma] = lillietest_evfit_ (z); ## column-wise fit c = 1 - exp (-exp ((z - mu) ./ sigma)); endswitch sim(done+1:done+m) = max (max (ii - c), max (c - im)); done += m; endwhile endfunction ## Maximum-likelihood fit of the extreme value (minima Gumbel) distribution, ## vectorised over the columns of X. The scale SIGMA solves the monotone score ## equation g(sigma) = E_w[x] - xbar - sigma = 0, where the weights w_i are ## proportional to exp (x_i / sigma). It is found by bisection: g is positive ## as sigma -> 0 (E_w[x] -> max (x)) and negative for large sigma, so the root ## is unique and bracketed. A fixed-point iteration on the same equation is not ## contractive for all samples and diverges on a small fraction of them, so ## bisection is used here. Returns row vectors MU and SIGMA. function [mu, sigma] = lillietest_evfit_ (x) xbar = mean (x); lo = 1e-8 * ones (1, columns (x)); hi = (max (x) - xbar) + 10; for iter = 1:80 sigma = 0.5 * (lo + hi); mx = max (x); w = exp ((x - mx) ./ sigma); g = sum (x .* w) ./ sum (w) - xbar - sigma; pos = g > 0; lo(pos) = sigma(pos); hi(! pos) = sigma(! pos); endfor sigma = 0.5 * (lo + hi); mx = max (x); w = exp ((x - mx) ./ sigma); mu = mx + sigma .* log (sum (w) / rows (x)); endfunction %!demo %! ## Test whether a sample is normally distributed %! x = [1 2 3 4 5 6 7 8 9 50]; # last value is an outlier %! [h, p, kstat] = lillietest (x) %!test # statistic matches a direct Kolmogorov-Smirnov computation and MATLAB %! warning ("off", "lillietest:pTooBig", "local"); %! warning ("off", "lillietest:pTooSmall", "local"); %! x = [2.1 0.3 1.2 -0.7 0.9 1.5 2.8 0.1 0.4 1.1 3.2 0.6 2.0 0.9 1.7]'; %! [~, ~, ks] = lillietest (x); %! xs = sort (x); n = numel (x); %! cdf = normcdf ((xs - mean (x)) / std (x)); %! d = max (max ((1:n)'/n - cdf), max (cdf - (0:n-1)'/n)); %! assert_equal (ks, d, 1e-12); %! assert_equal (ks, 0.1026, 5e-4); # MATLAB lillietest reference %!test # exponential and extreme value statistics match MATLAB references %! warning ("off", "lillietest:pTooBig", "local"); %! warning ("off", "lillietest:pTooSmall", "local"); %! xe = [0.5 1.2 0.3 2.1 0.8 1.5 0.2 3.0 0.7 1.1 0.4 2.5]'; %! [~, ~, kse] = lillietest (xe, "Distribution", "exponential"); %! assert_equal (kse, 0.1545, 5e-4); %! xn = [2.1 0.3 1.2 -0.7 0.9 1.5 2.8 0.1 0.4 1.1 3.2 0.6 2.0 0.9 1.7]'; %! [~, ~, ksv] = lillietest (xn, "Distribution", "extreme value"); %! assert_equal (ksv, 0.1512, 5e-4); %!test # a clearly non-normal sample is rejected; h, p, critval consistent %! warning ("off", "lillietest:pTooSmall", "local"); %! x = [zeros(1, 15), 100]; %! [h, p, ks, cv] = lillietest (x); %! assert_equal (h, 1); %! assert_equal (h, double (ks > cv)); %! assert_equal (p, 0.001); # clamped to the tabulated minimum %!test # distribution families run and return a decision %! warning ("off", "lillietest:pTooBig", "local"); %! warning ("off", "lillietest:pTooSmall", "local"); %! x = -log (rand (30, 1)); %! assert_equal (ismember (lillietest (x, "Distribution", "exponential"), [0 1]), true); %! assert_equal (ismember (lillietest (x, "Distribution", "extreme value"), [0 1]), true); %!test # p-value stays in the tabulated range; critval grows as alpha falls %! warning ("off", "lillietest:pTooBig", "local"); %! warning ("off", "lillietest:pTooSmall", "local"); %! x = [3 1 4 1 5 9 2 6 5 3 5 8 9 7]'; %! [~, p, ~, cv05] = lillietest (x); %! [~, ~, ~, cv01] = lillietest (x, "Alpha", 0.01); %! assert_equal (p >= 0.001 && p <= 0.5, true); %! assert_equal (cv01 > cv05, true); %!test # Monte-Carlo path runs and returns a valid p-value %! warning ("off", "lillietest:pTooBig", "local"); %! warning ("off", "lillietest:pTooSmall", "local"); %! x = [2.1 0.3 1.2 -0.7 0.9 1.5 2.8 0.1 0.4 1.1 3.2 0.6 2.0 0.9 1.7]'; %! [h, p] = lillietest (x, "MCTol", 0.05); %! assert_equal (ismember (h, [0 1]), true); %! assert_equal (p > 0 && p <= 1, true); %!test # interpolation works for a sample size off the table grid (n = 17) %! warning ("off", "lillietest:pTooBig", "local"); %! warning ("off", "lillietest:pTooSmall", "local"); %! x = [1 3 2 5 4 7 6 9 8 11 10 13 12 15 14 17 16]'; %! [~, ~, ~, cv] = lillietest (x); %! assert_equal (isfinite (cv) && cv > 0, true); %!test # extreme statistic with few reps gives an exact zero p-value %! x = [zeros(1, 15), 100]; %! [h, p, kstat] = lillietest (x, "MCTol", 0.05); %! assert_equal (p, 0); %! assert_equal (kstat, 0.5362, 5e-4); %!warning ... %! lillietest (norminv ((1:20)' / 21)); %!warning ... %! lillietest ([zeros(1, 15), 100]); ## Test input validation %!error lillietest () %!error lillietest (ones (3, 3)) %!error ... %! lillietest ([1 2 3]) %!error ... %! lillietest (1:10, "Distribution", "poisson") %!error ... %! lillietest (1:10, "Alpha", 0) %!error ... %! lillietest (1:10, "Alpha", 0.75) %!error ... %! lillietest (1:10, "MCTol", -1) statistics-release-2.1.2/inst/Hypothesis_Testing/manova1.m000066400000000000000000000222071527311104300236550ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{d} =} manova1 (@var{x}, @var{group}) ## @deftypefnx {statistics} {@var{d} =} manova1 (@var{x}, @var{group}, @var{alpha}) ## @deftypefnx {statistics} {[@var{d}, @var{p}] =} manova1 (@dots{}) ## @deftypefnx {statistics} {[@var{d}, @var{p}, @var{stats}] =} manova1 (@dots{}) ## ## One-way multivariate analysis of variance (MANOVA). ## ## @code{@var{d} = manova1 (@var{x}, @var{group}, @var{alpha})} performs a ## one-way MANOVA for comparing the mean vectors of two or more groups of ## multivariate data. ## ## @var{x} is a matrix with each row representing a multivariate observation, ## and each column representing a variable. ## ## @var{group} is a numeric vector, string array, or cell array of strings with ## the same number of rows as @var{x}. @var{x} values are in the same group if ## they correspond to the same value of GROUP. ## ## @var{alpha} is the scalar significance level and is 0.05 by default. ## ## @var{d} is an estimate of the dimension of the group means. It is the ## smallest dimension such that a test of the hypothesis that the means lie on ## a space of that dimension is not rejected. If @var{d} = 0 for example, we ## cannot reject the hypothesis that the means are the same. If @var{d} = 1, we ## reject the hypothesis that the means are the same but we cannot reject the ## hypothesis that they lie on a line. ## ## @code{[@var{d}, @var{p}] = manova1 (@dots{})} returns P, a vector of p-values ## for testing the null hypothesis that the mean vectors of the groups lie on ## various dimensions. P(1) is the p-value for a test of dimension 0, P(2) for ## dimension 1, etc. ## ## @code{[@var{d}, @var{p}, @var{stats}] = manova1 (@dots{})} returns a STATS ## structure with the following fields: ## ## @multitable @columnfractions 0.2 0.75 ## @item "W" @tab within-group sum of squares and products matrix ## @item "B" @tab between-group sum of squares and products matrix ## @item "T" @tab total sum of squares and products matrix ## @item "dfW" @tab degrees of freedom for WSSP matrix ## @item "dfB" @tab degrees of freedom for BSSP matrix ## @item "dfT" @tab degrees of freedom for TSSP matrix ## @item "lambda" @tab value of Wilk's lambda (the test statistic) ## @item "chisq" @tab transformation of lambda to a chi-square distribution ## @item "chisqdf" @tab degrees of freedom for chisq ## @item "eigenval" @tab eigenvalues of (WSSP^-1) * BSSP ## @item "eigenvec" @tab eigenvectors of (WSSP^-1) * BSSP; these are the ## coefficients for canonical variables, and they are scaled so the within-group ## variance of C is 1 ## @item "canon" @tab canonical variables, equal to XC*eigenvec, where XC ## is X with columns centered by subtracting their means ## @item "mdist" @tab Mahalanobis distance from each point to its group ## mean ## @item "gmdist" @tab Mahalanobis distances between each pair of group ## means ## @item "gnames" @tab Group names ## @end multitable ## ## The canonical variables C have the property that C(:,1) is the linear ## combination of the @var{x} columns that has the maximum separation between ## groups, C(:,2) has the maximum separation subject to it being orthogonal to ## C(:,1), and so on. ## ## @end deftypefn function [d, p, stats] = manova1 (x, group, alpha) ## Check input arguments narginchk (2,3) nargoutchk (1,3) ## Validate alpha value if parsed or add default if (nargin > 2) if (length (alpha) > 1 || ! isreal (alpha)) error ("manova1: Alpha must be a real scalar."); elseif (alpha <= 0 || alpha >= 1) error ("manova1: Alpha must be in the range (0,1)."); endif else alpha = 0.05; endif ## Convert group to cell array from character array if (ischar (group)) group = cellstr (group); endif ## Make group a column if (size (group, 1) == 1) group = group'; endif ## Check for equal size in samples between groups and data if (size (group, 1) != size (x, 1)) error ("manova1: Samples in X and groups mismatch."); endif ## Remove samples (rows) in X and GROUP if there are missing values in X no_nan = (sum (isnan (x), 2) == 0); x = x(no_nan, :); group = group(no_nan, :); is_nan = ! no_nan; ## Get group names and indices [group_idx, group_names] = grp2idx (group); ngroups = length (group_names); ## Remove NaN values from updated GROUP no_nan = ! isnan (group_idx); if (! all (no_nan)) group_idx = group_idx(no_nan); x = x(no_nan,: ); is_nan(! is_nan) = ! no_nan; endif ## Get number of samples and variables [nsample, nvar] = size (x); realgroups = ismember (1:ngroups, group_idx); nrgroups = sum (realgroups); ## Calculate Total Sum of Squares and Products matrix xm = mean (x); x = x - xm; TSSP = x' * x; ## Calculate Within-samples Sum of Squares and Products matrix WSSP = zeros (size (TSSP)); for j = 1:ngroups row = find (group_idx == j); ## Only meaningful for groups with more than one samples if (length (row) > 1) group_x = x(row, :); group_x = group_x - mean (group_x); WSSP = WSSP + group_x' * group_x; endif endfor ## Calculate Between-samples Sum of Squares and Products matrix BSSP = TSSP - WSSP; ## Instead of simply computing `eig (BSSP / WSSP)` we use Matlab's technique ## with chol to insure v' * WSSP * v = I is met [R, p] = chol (WSSP); if (p > 0) error ("manova1: Cannot factorize WSSP."); endif S = R' \ BSSP / R; ## Remove asymmetry caused by roundoff S = (S + S') / 2; [vv, ed] = eig (S); v = R \ vv; ## Sort in descending order [e,ei] = sort (diag (ed)); ## Check for valid eigenvalues if (min (e) <= -1) error ("manova1: wrong value in eigenvector: singular sum of squares."); endif ## Compute Bartlett's statistic for each dimension dims = 0:(min (nrgroups - 1, nvar) - 1); lambda = flipud (1 ./ cumprod (e + 1)); lambda = lambda(1 + dims); chistat = -(nsample - 1 - (nrgroups + nvar) / 2) .* log (lambda); chisqdf = ((nvar - dims) .* (nrgroups - 1 - dims))'; pp = chi2cdf (chistat, chisqdf, 'upper'); ## Get dimension where we can reject the null hypothesis d = dims(pp>alpha); if (length (d) > 0) d = d(1); else d = max (dims) + 1; endif ## Create extra outputs as necessary if (nargout > 1) p = pp; endif if (nargout > 2) stats.W = WSSP; stats.B = BSSP; stats.T = TSSP; stats.dfW = nsample - nrgroups; stats.dfB = nrgroups - 1; stats.dfT = nsample - 1; stats.lambda = lambda; stats.chisq = chistat; stats.chisqdf = chisqdf; ## Reorder to increasing stats.eigenval = flipud (e); ## Flip so that it is in order of increasing eigenvalues v = v(:, flipud (ei)); ## Re-scale eigenvectors so the within-group variance is 1 vs = diag ((v' * WSSP * v))' ./ (nsample - nrgroups); vs(vs<=0) = 1; v = v ./ repmat (sqrt (vs), size (v,1), 1); ## Flip sign so that the average element is positive j = (sum (v) < 0); v(:,j) = -v(:,j); stats.eigenvec = v; canon = x*v; if (any (is_nan)) tmp(! is_nan,:) = canon; tmp(is_nan,:) = NaN; stats.canon = tmp; else stats.canon = canon; endif ## Compute Mahalanobis distances from points to group means gmean = nan (ngroups, size (canon, 2)); gmean(realgroups,:) = grpstats (canon, group_idx); mdist = sum ((canon - gmean(group_idx,:)) .^ 2, 2); if (any (is_nan)) stats.mdist(! is_nan) = mdist; stats.mdist(is_nan) = NaN; else stats.mdist = mdist; endif ## Compute Mahalanobis distances between group means stats.gmdist = squareform (pdist (gmean)) .^ 2; stats.gnames = group_names; endif endfunction %!demo %! load carbig %! [d,p] = manova1 ([MPG, Acceleration, Weight, Displacement], Origin) %!test %! load carbig %! [d,p] = manova1 ([MPG, Acceleration, Weight, Displacement], Origin); %! assert_equal (d, 3); %! assert_equal (p, [0, 3.140583347827075e-07, 0.007510999577743149, ... %! 0.1934100745898493]', [1e-12, 1e-12, 1e-12, 1e-12]'); %!test %! load carbig %! [d,p] = manova1 ([MPG, Acceleration, Weight], Origin); %! assert_equal (d, 2); %! assert_equal (p, [0, 0.00516082975137544, 0.1206528056514453]', ... %! [1e-12, 1e-12, 1e-12]'); %!test %! ## Below the resolution of 1 - chi2cdf, values from MATLAB R2024a %! w = [1:10, 101:110, 201:210]'; %! g = kron ((1:3)', ones (10, 1)); %! [~, p] = manova1 ([w, sin(1:30)'], g); %! assert_equal (p(1), 2.48250847911811e-37, -1e-10); statistics-release-2.1.2/inst/Hypothesis_Testing/mcnemar_test.m000066400000000000000000000240331527311104300247730ustar00rootroot00000000000000## Copyright (C) 1996-2017 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} mcnemar_test (@var{x}) ## @deftypefnx {statistics} {@var{h} =} mcnemar_test (@var{x}, @var{testtype}) ## @deftypefnx {statistics} {@var{h} =} mcnemar_test (@dots{}, @qcode{'Alpha'}, @var{alpha}) ## @deftypefnx {statistics} {[@var{h}, @var{p}, @var{stats}] =} mcnemar_test (@dots{}) ## ## Perform a McNemar's test on paired nominal data. ## ## @nospell{McNemar's} test is applied to a @math{2*2} contingency table @var{x} ## with a dichotomous trait, with matched pairs of subjects, of data ## cross-classified on the row and column variables to testing the null ## hypothesis of symmetry of the classification probabilities. More formally, ## the null hypothesis of marginal homogeneity states that the two marginal ## probabilities for each outcome are the same. The test rests on the two ## discordant counts, @math{b = @var{x}(1,2)} and @math{c = @var{x}(2,1)}. ## ## Under the null, with a sufficiently large number of discordants ## (@math{b + c >= 25}), the test statistic follows a chi-squared distribution ## with 1 degree of freedom. When the number of discordants is less than 25, ## then the mid-P exact McNemar test is used. ## ## @var{testtype} will force @code{mcnemar_test} to apply a particular method ## for testing the null hypothesis independently of the number of discordants. ## Valid options for @var{testtype}: ## @itemize ## @item @qcode{'asymptotic'} Original McNemar test statistic ## @item @qcode{'corrected'} Edwards' version with continuity correction ## @item @qcode{'exact'} An exact binomial test ## @item @qcode{'mid-p'} The mid-P McNemar test (mid-p binomial test) ## @end itemize ## ## The test decision is returned in @var{h}, which is 1 when the null hypothesis ## is rejected at the significance level @var{alpha} and 0 otherwise, and the ## p-value in @var{p}. @qcode{'Alpha'} sets @var{alpha}, 0.05 by default, and ## the level @math{100 (1 - alpha)}% of the confidence intervals. @var{stats} ## is a structure with the following fields: ## ## @multitable @columnfractions 0.2 0.75 ## @item @qcode{chi2stat} @tab the chi-squared statistic, for the ## @qcode{'asymptotic'} and @qcode{'corrected'} tests only. ## @item @qcode{df} @tab its degrees of freedom, 1, for those tests only. ## @item @qcode{OddsRatio} @tab the odds ratio of the discordant pairs, ## @math{b / c}. ## @item @qcode{OddsRatioCI} @tab its confidence interval. ## @item @qcode{CohensG} @tab Cohen's @math{g}, @math{b / (b + c) - 0.5}, the ## departure of the discordant split from one half. ## @item @qcode{CohensGCI} @tab its confidence interval. ## @end multitable ## ## Both intervals come from the exact Clopper-Pearson interval of the ## proportion @math{b / (b + c)}, as does the exact test. With no discordant ## pairs both effect sizes and their intervals are @qcode{NaN}. ## ## Further information about the McNemar's test can be found at ## @url{https://en.wikipedia.org/wiki/McNemar%27s_test} ## ## @seealso{crosstab, chi2test, fishertest} ## @end deftypefn function [h, p, stats] = mcnemar_test (x, varargin) ## Check contingency table if (! isequal (size (x), [2, 2])) error ("mcnemar_test: X must be a 2x2 matrix."); elseif (! (all ((x(:) >= 0)) && all (x(:) == fix (x(:))))) error ("mcnemar_test: all entries of X must be non-negative integers."); endif ## Add defaults alpha = 0.05; b = x(1,2); c = x(2,1); if (b + c < 25) testtype = 'mid-p'; else testtype = 'asymptotic'; endif ## Parse optional arguments: a test type, and 'Alpha' i = 1; while (i <= numel (varargin)) arg = varargin{i}; if (ischar (arg) && strcmpi (arg, 'alpha')) if (i == numel (varargin)) error ("mcnemar_test: optional arguments must be in pairs."); endif alpha = varargin{i+1}; if (! (isnumeric (alpha) && isscalar (alpha) && isreal (alpha) && alpha > 0 && alpha < 1)) error ("mcnemar_test: invalid value for alpha."); endif i += 2; elseif (ischar (arg)) if (! any (strcmpi (arg, {'exact', 'asymptotic', 'mid-p', 'corrected'}))) error ("mcnemar_test: invalid value for TESTTYPE."); endif testtype = lower (arg); i += 1; else error ("mcnemar_test: invalid optional argument."); endif endwhile ## Calculate test. The exact tests are two-sided and symmetric in the ## discordant counts, so they read the tail of the smaller one. n = b + c; k = min (b, c); stats = struct (); switch (testtype) case 'asymptotic' stats.chi2stat = (b - c) .^2 / n; stats.df = 1; p = chi2cdf (stats.chi2stat, 1, 'upper'); case 'corrected' stats.chi2stat = (abs (b - c) - 1) .^2 / n; stats.df = 1; p = chi2cdf (stats.chi2stat, 1, 'upper'); case 'exact' p = min (1, 2 * binocdf (k, n, 0.5)); case 'mid-p' p = min (1, 2 * binocdf (k, n, 0.5) - binopdf (k, n, 0.5)); endswitch h = double (p < alpha); ## Effect sizes, with intervals from the Clopper-Pearson interval of the ## proportion of discordant pairs that fall in b if (n > 0) if (b == 0) lo = 0; else lo = betainv (alpha / 2, b, c + 1); endif if (c == 0) hi = 1; else hi = betainv (1 - alpha / 2, b + 1, c); endif stats.OddsRatio = b / c; stats.OddsRatioCI = [lo / (1 - lo), hi / (1 - hi)]; stats.CohensG = b / n - 0.5; stats.CohensGCI = [lo, hi] - 0.5; else stats.OddsRatio = NaN; stats.OddsRatioCI = [NaN, NaN]; stats.CohensG = NaN; stats.CohensGCI = [NaN, NaN]; endif endfunction %!test %! [h, p, st] = mcnemar_test ([101,121;59,33]); %! assert_equal (h, 1); %! assert_equal (p, 3.8151e-06, 1e-10); %! assert_equal (st.chi2stat, 21.356, 1e-3); %!test %! [h, p, st] = mcnemar_test ([59,6;16,80]); %! assert_equal (h, 1); %! assert_equal (p, 0.034690, 1e-6); %! assert_equal (isfield (st, 'chi2stat'), false); %!test %! [h, p] = mcnemar_test ([59,6;16,80], 'Alpha', 0.01); %! assert_equal (h, 0); %! assert_equal (p, 0.034690, 1e-6); %!test %! [h, p] = mcnemar_test ([59,6;16,80], 'mid-p'); %! assert_equal (h, 1); %! assert_equal (p, 0.034690, 1e-6); %!test %! [h, p, st] = mcnemar_test ([59,6;16,80], 'asymptotic'); %! assert_equal (h, 1); %! assert_equal (p, 0.033006, 1e-6); %! assert_equal (st.chi2stat, 4.5455, 1e-4); %! assert_equal (st.df, 1); %!test %! [h, p, st] = mcnemar_test ([59,6;16,80], 'exact'); %! assert_equal (h, 0); %! assert_equal (p, 0.052479, 1e-6); %! assert_equal (isfield (st, 'chi2stat'), false); %!test %! [h, p, st] = mcnemar_test ([59,6;16,80], 'corrected'); %! assert_equal (h, 0); %! assert_equal (p, 0.055009, 1e-6); %! assert_equal (st.chi2stat, 3.6818, 1e-4); %!test %! [h, p] = mcnemar_test ([59,6;16,80], 'corrected', 'Alpha', 0.1); %! assert_equal (h, 1); %! assert_equal (p, 0.055009, 1e-6); %!test %! ## Below the resolution of 1 - chi2cdf %! [~, p, st] = mcnemar_test ([100, 200; 0, 100]); %! assert_equal (p, erfc (sqrt (st.chi2stat / 2)), -1e-12); %!test %! ## The exact tests are symmetric in the discordant counts, values from R %! [~, p] = mcnemar_test ([59,16;6,80], 'exact'); %! assert_equal (p, 0.052479, 1e-6); %!test %! [~, p] = mcnemar_test ([59,16;6,80], 'mid-p'); %! assert_equal (p, 0.034690, 1e-6); %!test %! [~, p] = mcnemar_test ([5,7;7,5], 'exact'); %! assert_equal (p, 1); %!test %! [~, ~, st] = mcnemar_test ([59,6;16,80]); %! assert_equal (st.OddsRatio, 6 / 16); %!test %! ## Clopper-Pearson interval of 6/22 from R's binom.test %! [~, ~, st] = mcnemar_test ([59,6;16,80]); %! assert_equal (st.OddsRatioCI, [0.12018366326892038, 1.0089244510694295], ... %! -1e-12); %!test %! [~, ~, st] = mcnemar_test ([59,6;16,80]); %! assert_equal (st.CohensG, 6 / 22 - 0.5, -1e-14); %!test %! [~, ~, st] = mcnemar_test ([59,6;16,80]); %! assert_equal (st.CohensGCI, ... %! [0.10728924837039702, 0.50222120126634895] - 0.5, -1e-12); %!test %! [~, ~, st] = mcnemar_test ([101,121;59,33], 'Alpha', 0.01); %! assert_equal (st.OddsRatioCI, [1.3562696986628573, 3.1577641330443376], ... %! -1e-12); %!test %! ## With no discordant pairs there is no effect to measure %! [~, ~, st] = mcnemar_test ([5,0;0,5]); %! assert_equal ([st.OddsRatio, st.CohensG], [NaN, NaN]); %!test %! [~, ~, st] = mcnemar_test ([5,3;0,5]); %! assert_equal ([st.OddsRatio, st.OddsRatioCI(2)], [Inf, Inf]); %!error mcnemar_test (59, 6, 16, 80) %!error mcnemar_test (ones (3, 3)) %!error ... %! mcnemar_test ([59,6;16,-80]) %!error ... %! mcnemar_test ([59,6;16,4.5]) %!error ... %! mcnemar_test ([59,6;16,80], {''}) %!error ... %! mcnemar_test ([59,6;16,80], 0.05) %!error ... %! mcnemar_test ([59,6;16,80], 'Alpha') %!error ... %! mcnemar_test ([59,6;16,80], 'Alpha', -0.2) %!error ... %! mcnemar_test ([59,6;16,80], 'Alpha', [0.05, 0.1]) %!error ... %! mcnemar_test ([59,6;16,80], 'Alpha', 1) %!error ... %! mcnemar_test ([59,6;16,80], '') statistics-release-2.1.2/inst/Hypothesis_Testing/meanEffectSize.m000066400000000000000000001322671527311104300252130ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Effect} =} meanEffectSize (@var{X}) ## @deftypefnx {statistics} {@var{Effect} =} meanEffectSize (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{Effect} =} meanEffectSize (@dots{}, @var{Name}, @var{Value}) ## ## Effect sizes for the difference between two means, with their confidence ## intervals. ## ## @code{@var{Effect} = meanEffectSize (@var{X})} returns the difference ## between the mean of the sample @var{X} and a known population mean, zero by ## default, with its confidence interval. ## ## @code{@var{Effect} = meanEffectSize (@var{X}, @var{Y})} returns the ## difference between the means of the samples @var{X} and @var{Y}, with its ## confidence interval. ## ## @var{X} and @var{Y} are vectors of type double or single. A missing value ## (@qcode{NaN}) is left out of its sample, and out of both where the samples ## are paired. Where either sample is single, so are the results. ## ## @var{Effect} is a table with one row for each effect size asked for, in the ## order asked, named as listed below. Its variable @qcode{Effect} holds the ## effect size and its variable @qcode{ConfidenceIntervals} the lower and upper ## bounds of its confidence interval, the latter absent where ## @qcode{'ConfidenceIntervalType'} is @qcode{'none'}. ## ## @code{@var{Effect} = meanEffectSize (@dots{}, @var{Name}, @var{Value})} ## takes the following options. ## ## @multitable @columnfractions 0.28 0.02 0.70 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Effect'} @tab @tab The effect sizes to compute, as a ## character vector, a string array or a cell array of character vectors, ## chosen among those below. A name may be shortened to any prefix that ## names one effect alone, and case does not matter. ## @qcode{'meandiff'} by default. ## @item @qcode{'Mean'} @tab @tab The known population mean that one sample ## is compared with, a real scalar, 0 by default. It is ignored where there ## are two samples. ## @item @qcode{'Paired'} @tab @tab Whether @var{X} and @var{Y} are paired ## observations, @qcode{true} or @qcode{false} (also @qcode{'on'} or ## @qcode{'off'}), @qcode{false} by default. Paired samples must have the ## same number of elements. ## @item @qcode{'VarianceType'} @tab @tab Whether two unpaired samples are ## taken to come from populations of @qcode{'equal'} or @qcode{'unequal'} ## variance, @qcode{'equal'} by default. Paired samples take ## @qcode{'equal'} only; one sample ignores it. ## @item @qcode{'Alpha'} @tab @tab The significance level, a scalar between 0 ## and 1, 0.05 by default, so that each interval covers ## @math{100(1 - @var{Alpha})} percent. ## @item @qcode{'ConfidenceIntervalType'} @tab @tab @qcode{'exact'}, ## @qcode{'bootstrap'} or @qcode{'none'}. By default each effect takes ## @qcode{'exact'} where it has an exact interval and @qcode{'bootstrap'} ## where it has not. ## @item @qcode{'NumBootstraps'} @tab @tab The number of bootstrap replicates, ## a positive integer, 1000 by default. ## @item @qcode{'BootstrapOptions'} @tab @tab A structure, as returned by ## @code{statset}, accepted for compatibility. The replicates are always ## computed serially and drawn from the generator of @code{rand}. ## @item @qcode{'Resampling'} @tab @tab How the bootstrap resamples two ## unpaired samples, @qcode{'pooled'} or @qcode{'stratified'}; see below. ## @qcode{'pooled'} by default. This option is an Octave extension. ## @end multitable ## ## The effect sizes are the following, where @math{J(v)} is Hedges' ## correction for bias, @math{J(v) = Gamma(v/2) / (sqrt(v/2) Gamma((v-1)/2))}. ## ## @multitable @columnfractions 0.14 0.35 0.51 ## @headitem @var{Effect} @tab Row name @tab Description ## @item @qcode{'meandiff'} @tab @qcode{MeanDifference} @tab The difference ## between the means, @math{mean(X) - mean(Y)}, or @math{mean(X) - mu} for ## one sample. Its exact interval is Student's t interval: pooled for equal ## variances, Welch's for unequal variances, and on the differences for ## paired samples. ## @item @qcode{'cohen'} @tab @qcode{CohensD} @tab Cohen's d corrected for ## bias, that is Hedges' g: the difference between the means divided by a ## standard deviation, times @math{J(v)}. See below. ## @item @qcode{'glass'} @tab @qcode{GlasssDelta} @tab Glass's delta, ## @math{J(n_x - 1) (mean(X) - mean(Y)) / s_x}, the difference between the ## means divided by the standard deviation of the control sample @var{X}. ## Two unpaired samples only; @qcode{'VarianceType'} does not change it. ## @item @qcode{'cliff'} @tab @qcode{CliffsDelta} @tab Cliff's delta, the ## share of pairs @math{(x_i, y_j)} with @math{x_i > y_j} less the share with ## @math{x_i < y_j}. For paired samples the pairs are those of different ## observations, @math{i != j}. Two samples only. ## @item @qcode{'mediandiff'} @tab @qcode{MedianDifference} @tab The ## difference between the medians, @math{median(X) - median(Y)}, paired ## samples included. Two samples only. ## @item @qcode{'robustcohen'} @tab @qcode{RobustCohensD} @tab A robust ## Cohen's d, as MATLAB computes it; see below. ## @item @qcode{'akpcohen'} @tab @qcode{AKPCohensD} @tab The robust Cohen's d ## of Algina, Keselman and Penfield; see below. This effect is an Octave ## extension. ## @item @qcode{'kstest'} @tab @qcode{KolmogorovSmirnovStatistic} @tab The ## two-sample Kolmogorov-Smirnov statistic, the largest distance between the ## empirical distribution functions of @var{X} and @var{Y}, paired samples ## included. Two samples only. ## @end multitable ## ## @qcode{'meandiff'}, @qcode{'cohen'}, @qcode{'glass'} and @qcode{'cliff'} ## have exact intervals; the others are bootstrap only. ## ## @strong{Cohen's d.} For one sample it is ## @math{J(n - 1) (mean(X) - mu) / s_x}, and for two unpaired samples of ## equal variance @math{J(n_x + n_y - 2) (mean(X) - mean(Y)) / s_p}, with ## @math{s_p} the pooled standard deviation. Its interval inverts the ## noncentral t distribution of the one-sample or the two-sample t statistic. ## For unequal variances it is the @math{g^*} of Delacre and colleagues: the ## difference between the means divided by ## @math{s_a = sqrt((s_x^2 + s_y^2) / 2)}, times @math{J(v)} with ## @math{v = (n_x - 1)(n_y - 1)(s_x^2 + s_y^2)^2 / ((n_y - 1) s_x^4 + ## (n_x - 1) s_y^4)}, and its interval inverts the noncentral t distribution ## of Welch's statistic on @math{v} degrees of freedom. For paired samples it ## is @math{J(n - 1) mean(X - Y) / s_a}, and its interval is the MAG interval ## of Cousineau and Goulet-Pelletier. The interval of Glass's delta inverts ## the noncentral t distribution of Welch's statistic on @math{n_x - 1} ## degrees of freedom. ## ## @strong{Cliff's delta.} Its interval is Cliff's asymmetric interval from ## the normal distribution, with the variance of @math{delta} estimated as ## @math{((n_y - 1) var(d_i) + (n_x - 1) var(d_j) + S / ((n_x - 1)(n_y - 1))) ## / (n_x n_y)}, where @math{d_i} and @math{d_j} are the row and column means ## of the dominance matrix @math{d_ij = sign(x_i - y_j)} and @math{S} is the ## sum of @math{(d_ij - delta)^2}. For paired samples the interval is ## @math{delta +/- z s} with the variance of the U statistic over the ## @math{n(n - 1)} pairs, which needs four pairs or more. ## ## @strong{Robust Cohen's d.} @qcode{'robustcohen'} is MATLAB's: ## @math{0.642 J(v) (T(X) - T(Y)) / s_w}, where @math{T} is ## @code{trimmean (@dots{}, 20)}, which trims 10 percent of each tail, and ## @math{s_w} the standard deviation of the values clipped at their 20th and ## 80th percentiles, pooled over the two samples as @math{s_p} is, or averaged ## as @math{s_a} is for unequal variances and paired samples. @math{v} is the ## degrees of freedom of the matching @qcode{'cohen'}. For one sample ## @math{T(Y)} is replaced by @math{mu}. ## ## @qcode{'akpcohen'} is the estimator of Algina, Keselman and Penfield, as ## the R package WRS2 computes it: @math{c (T(X) - T(Y)) / s_w}, where ## @math{T} trims 20 percent of each tail, @math{floor(0.2 n)} values, and ## @math{s_w} is the standard deviation of the sample winsorized at the same ## order statistics. Two samples of equal variance pool @math{s_w} as ## @math{s_p} is pooled; for unequal variances @math{s_w} is that of @var{X} ## alone. One sample takes @math{c (T(X) - mu) / s_w}, and paired samples ## the same over the differences @math{X - Y}, with @math{mu} zero. There is ## no correction for bias. The constant @math{c = 0.64194}, which the ## authors round to 0.642, is the standard deviation of a standard normal ## distribution winsorized at 20 percent, so that both estimators estimate ## Cohen's d where the data are normal; @qcode{'akpcohen'} trims more of each ## tail and is the more robust of the two. ## ## @strong{Bootstrap.} The bootstrap interval is the bias-corrected and ## accelerated (BCa) percentile interval, with the acceleration estimated by ## the jackknife. One sample is resampled with replacement, and paired ## samples by pairs. Two unpaired samples are resampled as ## @qcode{'Resampling'} says. @qcode{'pooled'} resamples the observations of ## both samples together, each keeping its sample, so that the size of each ## sample varies from one replicate to the next; this is what MATLAB does, ## and it suits data where the sample an observation falls in is itself ## random. @qcode{'stratified'} resamples each sample on its own, keeping ## their sizes; it suits data where the sizes were fixed by design, as in an ## experiment. A replicate in which a sample comes out empty is left out. ## ## An interval that cannot be computed, because a sample holds too few ## observations or has zero variance, is @qcode{NaN}, with a warning. ## ## MATLAB returns a paired Cliff's delta interval of zero width, ## @math{[delta, delta]}, for fewer than four pairs, where the variance ## cannot be estimated; here that interval is @qcode{NaN}. ## ## References: J. Algina, H. J. Keselman and R. D. Penfield (2005). An ## alternative to Cohen's standardized mean difference effect size: a robust ## parameter and confidence interval in the two independent groups case. ## Psychological Methods, 10(3), 317-328. D. Cousineau and ## J.-C. Goulet-Pelletier (2021). A study of confidence intervals for Cohen's ## dp in within-subject designs with new proposals. The Quantitative Methods ## for Psychology, 17(1), 51-75. M. Delacre, D. Lakens, C. Ley, L. Liu and ## C. Leys (2021). Why Hedges' g*s based on the non-pooled standard deviation ## should be reported with Welch's t-test. PsyArXiv. N. Cliff (1993). ## Dominance statistics: ordinal analyses to answer ordinal questions. ## Psychological Bulletin, 114(3), 494-509. ## ## @seealso{ttest, ttest2, ranksum, kstest2, trimmean} ## @end deftypefn function Effect = meanEffectSize (X, varargin) ## Input validation if (nargin < 1) error ("meanEffectSize: too few input arguments."); endif if (! isSample (X)) error (strcat ("meanEffectSize: X must be a nonempty vector of type", ... " double or single.")); endif Y = []; if (numel (varargin) > 0 && ! (ischar (varargin{1}) || isa (varargin{1}, 'string'))) Y = varargin{1}; varargin(1) = []; if (! isSample (Y)) error (strcat ("meanEffectSize: Y must be a nonempty vector of type", ... " double or single.")); endif endif optNames = {'Effect', 'Mean', 'Paired', 'VarianceType', 'Alpha', ... 'ConfidenceIntervalType', 'NumBootstraps', ... 'BootstrapOptions', 'Resampling'}; ## An empty ConfidenceIntervalType resolves to each effect's default dfValues = {'meandiff', 0, false, 'equal', 0.05, [], 1000, [], 'pooled'}; [effects, mu, paired, vartype, alpha, citype, nboot, bopts, resampling, ... args] = parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (args)) error ("meanEffectSize: invalid optional paired argument."); endif names = {'meandiff', 'cohen', 'glass', 'cliff', 'mediandiff', ... 'robustcohen', 'akpcohen', 'kstest'}; effects = matchEffects (effects, names); if (isempty (effects)) error ("meanEffectSize: 'Effect' must be one or more of %s.", ... strjoin (strcat ("'", names, "'"), ', ')); endif if (! (isnumeric (mu) && isreal (mu) && isscalar (mu))) error ("meanEffectSize: 'Mean' must be a real scalar."); endif paired = matchSwitch (paired); if (isempty (paired)) error ("meanEffectSize: 'Paired' must be true, false, 'on' or 'off'."); endif vartype = matchOption (vartype, {'equal', 'unequal'}); if (isempty (vartype)) error ("meanEffectSize: 'VarianceType' must be 'equal' or 'unequal'."); endif if (! (isnumeric (alpha) && isreal (alpha) && isscalar (alpha) && alpha > 0 && alpha < 1)) error ("meanEffectSize: 'Alpha' must be a scalar between 0 and 1."); endif if (! isempty (citype)) citype = matchOption (citype, {'exact', 'bootstrap', 'none'}); if (isempty (citype)) error (strcat ("meanEffectSize: 'ConfidenceIntervalType' must be", ... " 'exact', 'bootstrap' or 'none'.")); endif endif if (! (isnumeric (nboot) && isreal (nboot) && isscalar (nboot) && isfinite (nboot) && nboot >= 1 && nboot == fix (nboot))) error ("meanEffectSize: 'NumBootstraps' must be a positive integer."); endif resampling = matchOption (resampling, {'pooled', 'stratified'}); if (isempty (resampling)) error ("meanEffectSize: 'Resampling' must be 'pooled' or 'stratified'."); endif ## BootstrapOptions is checked and otherwise unused if (! (isempty (bopts) || isstruct (bopts))) error ("meanEffectSize: 'BootstrapOptions' must be a structure."); endif ## The design, and which effects it admits if (isempty (Y)) mode = 'one'; if (paired) error ("meanEffectSize: 'Paired' needs two samples."); endif twoOnly = {'glass', 'cliff', 'mediandiff', 'kstest'}; bad = effects(ismember (effects, twoOnly)); if (! isempty (bad)) error ("meanEffectSize: effect '%s' needs two samples.", bad{1}); endif elseif (paired) mode = 'paired'; if (numel (X) != numel (Y)) error (strcat ("meanEffectSize: paired samples X and Y must have", ... " the same number of elements.")); endif if (strcmp (vartype, 'unequal')) error (strcat ("meanEffectSize: paired samples take 'VarianceType'", ... " 'equal' only.")); endif if (any (strcmp (effects, 'glass'))) error (strcat ("meanEffectSize: effect 'glass' does not apply to", ... " paired samples.")); endif else mode = 'two'; endif isExact = {'meandiff', 'cohen', 'glass', 'cliff'}; if (strcmp (citype, 'exact')) bad = effects(! ismember (effects, isExact)); if (! isempty (bad)) error (strcat ("meanEffectSize: effect '%s' has no exact confidence", ... " interval; use 'bootstrap'."), bad{1}); endif endif ## Missing values left out, of both samples where they are paired outclass = 'double'; if (isa (X, 'single') || isa (Y, 'single')) outclass = 'single'; endif x = double (X(:)); y = double (Y(:)); if (strcmp (mode, 'paired')) keep = ! (isnan (x) | isnan (y)); x = x(keep); y = y(keep); else x = x(! isnan (x)); y = y(! isnan (y)); endif ## Each effect and its interval ne = numel (effects); E = zeros (ne, 1); CI = NaN (ne, 2); rowNames = cell (ne, 1); for ii = 1:ne name = effects{ii}; rowNames{ii} = rowName (name); stat = @(a, b) effectValue (name, a, b, mode, vartype, mu); E(ii) = stat (x, y); ci = citype; if (isempty (ci)) if (ismember (name, isExact)) ci = 'exact'; else ci = 'bootstrap'; endif endif switch (ci) case 'exact' [CI(ii,:), why] = exactInterval (name, x, y, mode, vartype, mu, alpha); if (! isempty (why)) warning (strcat ("meanEffectSize: the confidence interval of", ... " effect '%s' is NaN, since %s."), name, why); endif case 'bootstrap' switch (mode) case 'one' CI(ii,:) = __bootci__ (@(a) stat (a, y), {x}, alpha, nboot, 'rows'); case 'paired' CI(ii,:) = __bootci__ (stat, {x, y}, alpha, nboot, 'rows'); otherwise if (strcmp (resampling, 'pooled')) CI(ii,:) = __bootci__ (stat, {x, y}, alpha, nboot, 'pooled'); else CI(ii,:) = __bootci__ (stat, {x, y}, alpha, nboot, 'strata'); endif endswitch endswitch endfor E = cast (E, outclass); if (strcmp (citype, 'none')) Effect = table (E, 'VariableNames', {'Effect'}, 'RowNames', rowNames); else CI = cast (CI, outclass); Effect = table (E, CI, 'VariableNames', {'Effect', ... 'ConfidenceIntervals'}, 'RowNames', rowNames); endif endfunction ## A nonempty vector of type double or single function tf = isSample (v) tf = isfloat (v) && isreal (v) && isvector (v) && ! isempty (v); endfunction ## The effects asked for, in the order asked, each once; empty if any name ## is not recognized function out = matchEffects (in, names) out = {}; if (isa (in, 'string')) in = cellstr (in); elseif (ischar (in) && (isrow (in) || isempty (in))) in = {in}; endif if (! iscellstr (in) || isempty (in)) return; endif for ii = 1:numel (in) name = matchOption (in{ii}, names); if (isempty (name)) out = {}; return; endif if (! any (strcmp (name, out))) out{end+1} = name; endif endfor endfunction ## The option in LIST that VAL names, case free and possibly shortened; ## empty if none or several match function out = matchOption (val, list) out = ''; if (isa (val, 'string') && isscalar (val)) val = char (val); endif if (! (ischar (val) && isrow (val))) return; endif idx = strcmpi (val, list); if (! any (idx)) idx = strncmpi (val, list, numel (val)); endif if (sum (idx) == 1) out = list{idx}; endif endfunction ## A logical switch given as true, false, 0, 1, 'on' or 'off'; empty if not function out = matchSwitch (val) out = []; if ((islogical (val) || isnumeric (val)) && isscalar (val) && (val == 0 || val == 1)) out = logical (val); else val = matchOption (val, {'on', 'off'}); if (! isempty (val)) out = strcmp (val, 'on'); endif endif endfunction ## The row name of each effect function out = rowName (name) switch (name) case 'meandiff' out = 'MeanDifference'; case 'cohen' out = 'CohensD'; case 'glass' out = 'GlasssDelta'; case 'cliff' out = 'CliffsDelta'; case 'mediandiff' out = 'MedianDifference'; case 'robustcohen' out = 'RobustCohensD'; case 'akpcohen' out = 'AKPCohensD'; case 'kstest' out = 'KolmogorovSmirnovStatistic'; endswitch endfunction ## Hedges' correction for bias on V degrees of freedom, 0 at its limit V = 1 ## and undefined below it; elementwise function J = hedgesJ (v) J = NaN (size (v)); k = v >= 1; J(k) = exp (gammaln (v(k) / 2) - log (sqrt (v(k) / 2)) ... - gammaln ((v(k) - 1) / 2)); endfunction ## Delacre's degrees of freedom for two samples of sizes N1, N2 and ## variances V1, V2; elementwise function v = delacreDF (n1, n2, v1, v2) v = (n1 - 1) * (n2 - 1) * (v1 + v2) .^ 2 ./ ((n2 - 1) * v1 .^ 2 ... + (n1 - 1) * v2 .^ 2); endfunction ## The trimmed means and winsorized variances of the columns of V, MATLAB's ## way (trimmean (V, 20), which trims 10 percent of each tail, and the ## values clipped at the 20th and 80th percentiles) or that of Algina, ## Keselman and Penfield (20 percent of each tail by order statistics) function [tm, wv] = robustParts (v, akp) n = rows (v); v = sort (v, 1); if (akp) g = floor (0.2 * n); tm = mean (v(g+1:n-g,:), 1); v(1:g,:) = repmat (v(g+1,:), g, 1); v(n-g+1:n,:) = repmat (v(n-g,:), g, 1); else ## As trimmean (V, 20) rounds, a half downwards k = round (n * 0.1 - eps (n * 0.1)); tm = mean (v(k+1:n-k,:), 1); b = quantile (v, [0.2; 0.8], 1, 5); v = min (max (v, b(1,:)), b(2,:)); endif wv = var (v, 0, 1); endfunction ## Cliff's delta of two unpaired samples, with the row and column means of ## the dominance matrix and the sum of the squares of its deviations from ## delta, all by counting function [d, di, dj, S] = cliffParts (x, y) n1 = numel (x); n2 = numel (y); ## For each x_i the number of y at or below it and at or above it yle = lookup (sort (y), x); yge = lookup (sort (-y), -x); d = sum (yle - yge) / (n1 * n2); if (nargout > 1) di = (yle - yge) / n2; dj = (lookup (sort (-x), -y) - lookup (sort (x), y)) / n1; ## Squares of d_ij are 1 unless tied, so their sum counts untied pairs S = n1 * n2 - sum (yle + yge - n2) - n1 * n2 * d ^ 2; endif endfunction ## Cliff's delta over the pairs of different observations of paired samples, ## with the row and column means of the dominance matrix off its diagonal, ## the sum of the squares of its deviations from delta, S2, and the sum of ## the products of each deviation with its transpose, S3 function [d, di, dj, S2, S3] = cliffPaired (x, y) n = numel (x); m = n * (n - 1); s = sign (x - y); yle = lookup (sort (y), x); yge = lookup (sort (-y), -x); d = (sum (yle - yge) - sum (s)) / m; if (nargout > 1) di = (yle - yge - s) / (n - 1); dj = (lookup (sort (-x), -y) - lookup (sort (x), y) - s) / (n - 1); ## Untied pairs off the diagonal S2 = n * n - sum (yle + yge - n) - sum (s != 0) - m * d ^ 2; ## The sum of d_ij d_ji off the diagonal, over blocks of rows P = 0; step = max (1, floor (1e6 / n)); for i1 = 1:step:n i = i1:min (i1 + step - 1, n); P += sum (sum (sign (x(i) - y') .* sign (x' - y(i)))); endfor P -= sum (s .^ 2); S3 = P - 2 * d * d * m + m * d ^ 2; endif endfunction ## The two-sample Kolmogorov-Smirnov statistic function D = ksStat (x, y) z = [x; y]; D = max (abs (lookup (sort (x), z) / numel (x) ... - lookup (sort (y), z) / numel (y))); endfunction ## The value of one effect over each column of X, and of Y where there are ## two samples; MODE is 'one', 'paired' or 'two' function e = effectValue (name, x, y, mode, vartype, mu) n1 = rows (x); n2 = rows (y); switch (name) case 'meandiff' switch (mode) case 'one' e = mean (x, 1) - mu; case 'paired' e = mean (x - y, 1); otherwise e = mean (x, 1) - mean (y, 1); endswitch case 'cohen' switch (mode) case 'one' e = hedgesJ (n1 - 1) * (mean (x, 1) - mu) ./ std (x, 0, 1); case 'paired' e = hedgesJ (n1 - 1) * mean (x - y, 1) ... ./ sqrt ((var (x, 0, 1) + var (y, 0, 1)) / 2); otherwise v1 = var (x, 0, 1); v2 = var (y, 0, 1); if (strcmp (vartype, 'equal')) v = n1 + n2 - 2; s = sqrt (((n1 - 1) * v1 + (n2 - 1) * v2) / v); else v = delacreDF (n1, n2, v1, v2); s = sqrt ((v1 + v2) / 2); endif e = hedgesJ (v) .* (mean (x, 1) - mean (y, 1)) ./ s; endswitch case 'glass' e = hedgesJ (n1 - 1) * (mean (x, 1) - mean (y, 1)) ./ std (x, 0, 1); case 'cliff' e = zeros (1, columns (x)); for k = 1:columns (x) if (strcmp (mode, 'paired')) e(k) = cliffPaired (x(:,k), y(:,k)); else e(k) = cliffParts (x(:,k), y(:,k)); endif endfor case 'mediandiff' e = median (x, 1) - median (y, 1); case 'robustcohen' [t1, w1] = robustParts (x, false); if (strcmp (mode, 'one')) v = n1 - 1; e = 0.642 * (t1 - mu) ./ sqrt (w1); else [t2, w2] = robustParts (y, false); if (strcmp (mode, 'paired')) v = n1 - 1; s = sqrt ((w1 + w2) / 2); elseif (strcmp (vartype, 'equal')) v = n1 + n2 - 2; s = sqrt (((n1 - 1) * w1 + (n2 - 1) * w2) / v); else v = delacreDF (n1, n2, w1, w2); s = sqrt ((w1 + w2) / 2); endif e = 0.642 * (t1 - t2) ./ s; endif e = e .* hedgesJ (v); case 'akpcohen' ## As akp.effect and D.akp.effect of the R package WRS2 compute it, with ## the constant exact where WRS2 integrates it numerically c = norminv (0.8); c = sqrt (0.6 - 2 * c * normpdf (c) + 0.4 * c ^ 2); switch (mode) case 'one' [t1, w1] = robustParts (x, true); e = c * (t1 - mu) ./ sqrt (w1); case 'paired' [t1, w1] = robustParts (x - y, true); e = c * t1 ./ sqrt (w1); otherwise [t1, w1] = robustParts (x, true); [t2, w2] = robustParts (y, true); if (strcmp (vartype, 'equal')) s = sqrt (((n1 - 1) * w1 + (n2 - 1) * w2) / (n1 + n2 - 2)); else s = sqrt (w1); endif e = c * (t1 - t2) ./ s; endswitch case 'kstest' e = zeros (1, columns (x)); for k = 1:columns (x) e(k) = ksStat (x(:,k), y(:,k)); endfor endswitch endfunction ## The noncentrality at which the noncentral t distribution on DF degrees of ## freedom puts T at its P quantile function lambda = nctBound (t, df, p) f = @(l) nctcdf (t, df, l) - p; lo = t - 10; hi = t + 10; while (f (lo) < 0) lo -= 2 * (hi - lo); endwhile while (f (hi) > 0) hi += 2 * (hi - lo); endwhile lambda = fzero (f, [lo, hi], optimset ('TolX', eps)); endfunction ## Both bounds, on the scale of the noncentrality, of the interval that ## inverts the noncentral t distribution of the statistic T function b = nctInterval (t, df, alpha) if (! isfinite (t)) b = [NaN, NaN]; return; endif b = [nctBound(t, df, 1 - alpha / 2), nctBound(t, df, alpha / 2)]; endfunction ## The exact interval of one effect, or NaN with the reason WHY function [ci, why] = exactInterval (name, x, y, mode, vartype, mu, alpha) ci = [NaN, NaN]; why = ''; n1 = numel (x); n2 = numel (y); q = [alpha / 2, 1 - alpha / 2]; few = 'a sample holds too few observations'; flat = 'a sample has zero variance'; switch (name) case 'meandiff' switch (mode) case {'one', 'paired'} if (strcmp (mode, 'one')) d = x - mu; else d = x - y; endif if (n1 < 2) why = few; return; endif ci = mean (d) + tinv (q, n1 - 1) * std (d) / sqrt (n1); otherwise if (strcmp (vartype, 'equal')) v = n1 + n2 - 2; if (v < 1) why = few; return; endif se = sqrt (((n1 - 1) * var (x) + (n2 - 1) * var (y)) / v ... * (1 / n1 + 1 / n2)); else if (n1 < 2 || n2 < 2) why = few; return; endif a = var (x) / n1; b = var (y) / n2; se = sqrt (a + b); v = (a + b) ^ 2 / (a ^ 2 / (n1 - 1) + b ^ 2 / (n2 - 1)); endif ci = mean (x) - mean (y) + tinv (q, v) * se; endswitch case 'cohen' switch (mode) case 'one' v = n1 - 1; if (v < 1) why = few; return; elseif (std (x) == 0) why = flat; return; endif t = (mean (x) - mu) / (std (x) / sqrt (n1)); ci = nctInterval (t, v, alpha) * hedgesJ (v) / sqrt (n1); case 'paired' ## The MAG interval of Cousineau and Goulet-Pelletier (2021) if (n1 < 2) why = few; return; endif sx = std (x); sy = std (y); sa = sqrt ((sx ^ 2 + sy ^ 2) / 2); rW = corr (x, y) * sx * sy / sa ^ 2; if (! (sx > 0 && sy > 0)) why = flat; return; elseif (rW >= 1) why = 'the paired differences have zero variance'; return; endif sc = sqrt (n1 / (2 * (1 - rW))); lambda = mean (x - y) / sa * hedgesJ (n1 - 1) ^ 2 * sc; ci = nctinv (q, n1 - 1, lambda) / sc; otherwise if (strcmp (vartype, 'equal')) v = n1 + n2 - 2; if (v < 1) why = few; return; endif sp = sqrt (((n1 - 1) * var (x) + (n2 - 1) * var (y)) / v); if (sp == 0) why = flat; return; endif r = sqrt (1 / n1 + 1 / n2); t = (mean (x) - mean (y)) / (sp * r); ci = nctInterval (t, v, alpha) * hedgesJ (v) * r; else if (n1 < 2 || n2 < 2) why = few; return; elseif (var (x) == 0 || var (y) == 0) why = flat; return; endif v = delacreDF (n1, n2, var (x), var (y)); se = sqrt (var (x) / n1 + var (y) / n2); sa = sqrt ((var (x) + var (y)) / 2); t = (mean (x) - mean (y)) / se; ci = nctInterval (t, v, alpha) * hedgesJ (v) * se / sa; endif endswitch case 'glass' if (n1 < 2 || n2 < 2) why = few; return; elseif (var (x) == 0 || var (y) == 0) why = flat; return; endif v = n1 - 1; se = sqrt (var (x) / n1 + var (y) / n2); t = (mean (x) - mean (y)) / se; ci = nctInterval (t, v, alpha) * hedgesJ (v) * se / std (x); case 'cliff' z = norminv (1 - alpha / 2); if (strcmp (mode, 'paired')) [d, di, dj, S2, S3] = cliffPaired (x, y); if (S2 == 0) s2 = 0; elseif (n1 < 4) ## MATLAB returns [d, d] here, a zero width the data cannot support why = 'fewer than four pairs leave its variance undefined'; return; else s2 = ((n1 - 1) ^ 2 * sum ((di + dj - 2 * d) .^ 2) - S2 - S3) ... / (n1 * (n1 - 1) * (n1 - 2) * (n1 - 3)); s2 = max (s2, 0); endif ci = d + [-z, z] * sqrt (s2); else [d, di, dj, S] = cliffParts (x, y); if (S == 0) s2 = 0; elseif (n1 < 2 || n2 < 2) why = few; return; else s2 = ((n2 - 1) * var (di) + (n1 - 1) * var (dj) ... + S / ((n1 - 1) * (n2 - 1))) / (n1 * n2); endif if (s2 == 0) ci = [d, d]; else c = z * sqrt (s2) * sqrt ((1 - d ^ 2) ^ 2 + z ^ 2 * s2); ci = (d - d ^ 3 + [-c, c]) / (1 - d ^ 2 + z ^ 2 * s2); endif endif endswitch endfunction %!shared x, y, yp %! x = [2.1; 3.4; 1.9; 5.6; 4.4; 3.8; 2.7; 6.1; 3.3; 4.9]; %! y = [1.2; 2.8; 0.9; 2.2; 3.1; 1.7; 2.5; 0.4; 1.9; 3.6; 2.0; 1.1]; %! yp = [1.8; 2.9; 2.2; 4.1; 4.9; 2.6; 3.0; 4.8; 2.1; 4.0]; %!test %! T = meanEffectSize (x, y); %! assert_equal (T.Properties.VariableNames, {'Effect', 'ConfidenceIntervals'}); %! assert_equal (T.Properties.RowNames, {'MeanDifference'}); %! assert_equal (T.Effect, 1.87, -1e-14); %! assert_equal (T.ConfidenceIntervals, ... %! [0.8093229690413275, 2.9306770309586709], -1e-13); %!test %! T = meanEffectSize (x, 'Mean', 3); %! assert_equal (T{:,:}, [0.8199999999999994, -0.19771934142469916, ... %! 1.8377193414246979], -1e-13); %!test %! T = meanEffectSize (x, 'Effect', 'cohen'); %! assert_equal (T{:,:}, [2.4538321615715333, 1.1921466698808931, ... %! 3.6909287967065962], -1e-13); %!test %! T = meanEffectSize (x, y, 'VarianceType', 'unequal'); %! assert_equal (T{:,:}, [1.87, 0.74758396809829564, 2.992416031901703], ... %! -1e-13); %!test %! T = meanEffectSize (x, y, 'Effect', 'cohen'); %! assert_equal (T.Properties.RowNames, {'CohensD'}); %! assert_equal (T{:,:}, [1.51473229411906, 0.5682402309164547, ... %! 2.4328200098432089], -1e-13); %!test %! T = meanEffectSize (x, y, 'Effect', 'cohen', 'VarianceType', 'unequal'); %! assert_equal (T{:,:}, [1.4716714731499507, 0.49927861679878149, ... %! 2.411323051341753], -1e-13); %!test %! T = meanEffectSize (x, y, 'Effect', 'glass'); %! assert_equal (T.Properties.RowNames, {'GlasssDelta'}); %! assert_equal (T{:,:}, [1.2012215031776874, 0.32205950867366817, ... %! 2.0417713231164596], -1e-13); %!test %! T = meanEffectSize (x, y, 'Effect', 'glass', 'VarianceType', 'unequal'); %! assert_equal (T{:,:}, [1.2012215031776874, 0.32205950867366817, ... %! 2.0417713231164596], -1e-13); %!test %! T = meanEffectSize (x, y, 'Effect', 'cliff'); %! assert_equal (T.Properties.RowNames, {'CliffsDelta'}); %! assert_equal (T{:,:}, [0.725, 0.27352437233891558, ... %! 0.91469527893564417], -1e-13); %!test %! T = meanEffectSize ([1; 2; 2; 3; 3; 3; 4; 5], [2; 2; 3; 3; 4; 6], ... %! 'Effect', 'cliff'); %! assert_equal (T{:,:}, [-0.14583333333333334, -0.64640108601125412, ... %! 0.44249639444771338], -1e-13); %!test %! T = meanEffectSize ([5; 6; 7], [1; 2; 3], 'Effect', 'cliff'); %! assert_equal (T{:,:}, [1, 1, 1]); %!test %! T = meanEffectSize (1, [3; 4; 5], 'Effect', 'cliff'); %! assert_equal (T{:,:}, [-1, -1, -1]); %!test %! T = meanEffectSize (x, y, 'Effect', 'cliff', 'Alpha', 0.1); %! assert_equal (T.ConfidenceIntervals, ... %! [0.35705295319266317, 0.89817708994266032], -1e-13); %!test %! T = meanEffectSize (x, y, 'Effect', {'cliff', 'meandiff', 'glass', ... %! 'cohen'}); %! assert_equal (T.Properties.RowNames, {'CliffsDelta'; 'MeanDifference'; ... %! 'GlasssDelta'; 'CohensD'}); %! assert_equal (T.Effect, [0.725; 1.87; 1.2012215031776874; ... %! 1.51473229411906], -1e-13); %!test %! T = meanEffectSize (x, y, 'Effect', {'cohen', 'cohen'}); %! assert_equal (T.Properties.RowNames, {'CohensD'}); %!test %! T = meanEffectSize (x, y, 'Effect', 'COH'); %! assert_equal (T.Properties.RowNames, {'CohensD'}); %!test %! T = meanEffectSize (x, y, 'Effect', 'cohen', 'Mean', 3); %! assert_equal (T.Effect, 1.51473229411906, -1e-13); %!test %! T = meanEffectSize (x', y', 'Effect', 'cohen'); %! assert_equal (T.Effect, 1.51473229411906, -1e-13); %!test %! T = meanEffectSize (x, y, 'Effect', 'cohen', ... %! 'ConfidenceIntervalType', 'none'); %! assert_equal (T.Properties.VariableNames, {'Effect'}); %! assert_equal (T.Effect, 1.51473229411906, -1e-13); %!test %! T = meanEffectSize ([x; NaN], [NaN; y], 'Effect', 'cohen'); %! assert_equal (T{:,:}, [1.51473229411906, 0.5682402309164547, ... %! 2.4328200098432089], -1e-13); %!test %! T = meanEffectSize (single (x), y, 'Effect', 'cohen'); %! assert_equal (class (T.Effect), 'single'); %! assert_equal (class (T.ConfidenceIntervals), 'single'); %!test %! T = meanEffectSize (x, yp, 'Paired', true); %! assert_equal (T{:,:}, [0.57999999999999874, 0.044888382079159239, ... %! 1.1151116179208382], -1e-13); %!test %! T = meanEffectSize (x, yp, 'Paired', 'on', 'Effect', 'cohen'); %! assert_equal (T{:,:}, [0.41222519124990314, 0.016525506916817929, ... %! 0.94928510130451305], -1e-12); %!test %! T = meanEffectSize (x, yp, 'Paired', true, 'Effect', 'cohen', ... %! 'Alpha', 0.01); %! assert_equal (T.ConfidenceIntervals, ... %! [-0.10395569010229536, 1.2369598804778004], -1e-12); %!test %! T = meanEffectSize ((1:8)', [8.2; 6.9; 7.4; 5.1; 5.8; 3.6; 4.4; 2.3], ... %! 'Paired', true, 'Effect', 'cohen'); %! assert_equal (T{:,:}, [-0.38188801178758269, -2.124735313427935, ... %! 1.1823775856056995], -1e-12); %!test %! T = meanEffectSize ([1; 2], [2; 4], 'Paired', true, 'Effect', 'cohen'); %! assert_equal (T{:,:}, [0, -5.6823875052232813, 5.6823875052232813], ... %! -1e-12); %!test %! T = meanEffectSize ([x; NaN], [yp; 2], 'Paired', true, 'Effect', 'cohen'); %! assert_equal (T.Effect, 0.41222519124990314, -1e-13); %!test %! T = meanEffectSize (x, yp, 'Paired', true, 'Effect', 'cliff'); %! assert_equal (T{:,:}, [0.22222222222222221, 0.11569417468908846, ... %! 0.32875026975535593], -1e-13); %!test %! T = meanEffectSize ([1; 2; 3; 4], [3; 1; 2; 5], 'Paired', true, ... %! 'Effect', 'cliff'); %! assert_equal (T{:,:}, [-0.083333333333333329, -0.24666366537833756, ... %! 0.079996998711670889], -1e-13); %!test %! T = meanEffectSize (x, y, 'Effect', 'mediandiff', ... %! 'ConfidenceIntervalType', 'none'); %! assert_equal (T.Properties.RowNames, {'MedianDifference'}); %! assert_equal (T.Effect, 1.65, -1e-14); %!test %! T = meanEffectSize (x, yp, 'Paired', true, 'Effect', 'mediandiff', ... %! 'ConfidenceIntervalType', 'none'); %! assert_equal (T.Effect, 0.65, -1e-14); %!test %! T = meanEffectSize (x, y, 'Effect', 'kstest', ... %! 'ConfidenceIntervalType', 'none'); %! assert_equal (T.Properties.RowNames, {'KolmogorovSmirnovStatistic'}); %! assert_equal (T.Effect, 0.6166666666666667, -1e-14); %!test %! T = meanEffectSize (x, yp, 'Paired', true, 'Effect', 'kstest', ... %! 'ConfidenceIntervalType', 'none'); %! assert_equal (T.Effect, 0.3, -1e-14); %!test %! T = meanEffectSize (x, y, 'Effect', 'robustcohen', ... %! 'ConfidenceIntervalType', 'none'); %! assert_equal (T.Properties.RowNames, {'RobustCohensD'}); %! assert_equal (T.Effect, 1.2360631043171186, -1e-13); %!test %! T = meanEffectSize (x, y, 'Effect', 'robustcohen', ... %! 'VarianceType', 'unequal', ... %! 'ConfidenceIntervalType', 'none'); %! assert_equal (T.Effect, 1.1952733752449272, -1e-13); %!test %! T = meanEffectSize (x, yp, 'Paired', true, 'Effect', 'robustcohen', ... %! 'ConfidenceIntervalType', 'none'); %! assert_equal (T.Effect, 0.31641941202232715, -1e-13); %!test %! T = meanEffectSize (x, 'Effect', 'robustcohen', ... %! 'ConfidenceIntervalType', 'none'); %! assert_equal (T.Effect, 1.9756476082746626, -1e-13); ## akpcohen against WRS2 1.1.7, whose constant is integrated numerically ## and differs from the exact one by 1.2e-9 relative %!test %! T = meanEffectSize (x, y, 'Effect', 'akpcohen', ... %! 'ConfidenceIntervalType', 'none'); %! assert_equal (T.Properties.RowNames, {'AKPCohensD'}); %! assert_equal (T.Effect, 1.4340672042318023, -2e-9); %!test %! T = meanEffectSize (x, y, 'Effect', 'akpcohen', 'VarianceType', ... %! 'unequal', 'ConfidenceIntervalType', 'none'); %! assert_equal (T.Effect, 1.2409782758085974, -2e-9); %!test %! T = meanEffectSize ([1; 2; 3; 4; 5; 6; 7; 8; 20; 30], 2 * (1:10)', ... %! 'Effect', 'akpcohen', 'ConfidenceIntervalType', 'none'); %! assert_equal (T.Effect, -1.0275756121741466, -2e-9); %!test %! T = meanEffectSize (x, 'Effect', 'akpcohen', 'Mean', 3, ... %! 'ConfidenceIntervalType', 'none'); %! assert_equal (T.Effect, 0.50999107225010853, -2e-9); %!test %! T = meanEffectSize ([1; 2; 3; 4; 5; 6; 7; 8; 20; 30], ... %! 'Effect', 'akpcohen', 'ConfidenceIntervalType', 'none'); %! assert_equal (T.Effect, 1.6247397012560751, -2e-9); %!test %! T = meanEffectSize (x, yp, 'Paired', true, 'Effect', 'akpcohen', ... %! 'ConfidenceIntervalType', 'none'); %! assert_equal (T.Effect, 0.60651611028436192, -2e-9); %!test %! T = meanEffectSize (x, y, 'Effect', 'cohen', 'NumBootstraps', 10); %! assert_equal (T.ConfidenceIntervals, ... %! [0.5682402309164547, 2.4328200098432089], -1e-13); %!test %! rand ('state', 1); %! T = meanEffectSize ([1; 2; 3], [10; 10; 10; 10], 'NumBootstraps', 5000, ... %! 'ConfidenceIntervalType', 'bootstrap'); %! assert_equal (T.ConfidenceIntervals, [-9, -7]); %!test %! rand ('state', 1); %! T = meanEffectSize ([1; 2; 3], [10; 10; 10; 10], 'NumBootstraps', 5000, ... %! 'ConfidenceIntervalType', 'bootstrap', 'Alpha', 0.4); %! assert_equal (T.ConfidenceIntervals, [-8.5, -7.5]); %!test %! rand ('state', 1); %! T = meanEffectSize ([1; 2; 3], [10; 10; 10; 10], 'NumBootstraps', 5000, ... %! 'ConfidenceIntervalType', 'bootstrap', 'Alpha', 0.4, ... %! 'Resampling', 'stratified'); %! assert_equal (T.ConfidenceIntervals, [-25, -23] / 3, -1e-14); %!test %! rand ('state', 1); %! T = meanEffectSize ([10; 10; 10], [1; 2; 3; 4], 'NumBootstraps', 5000, ... %! 'ConfidenceIntervalType', 'bootstrap', 'Alpha', 0.4, ... %! 'BootstrapOptions', statset ('UseParallel', true)); %! assert_equal (T.ConfidenceIntervals, [7, 8]); %!warning ... %! T = meanEffectSize (ones (5, 1), 2 * ones (6, 1), 'Effect', 'cohen'); %! assert_equal (T{:,:}, [-Inf, NaN, NaN]); %!warning ... %! T = meanEffectSize (5, 'Effect', 'cohen'); %! assert_equal (T{:,:}, [NaN, NaN, NaN]); %!warning ... %! T = meanEffectSize ((1:8)', (2:9)', 'Paired', true, 'Effect', 'cohen'); %! assert_equal (T.ConfidenceIntervals, [NaN, NaN]); %!warning ... %! T = meanEffectSize ([1; 2; 3], [3; 1; 2], 'Paired', true, 'Effect', 'cliff'); %! assert_equal (T{:,:}, [-1 / 6, NaN, NaN], -1e-14); %!error meanEffectSize () %!error ... %! meanEffectSize ([1, 2; 3, 4]) %!error ... %! meanEffectSize ([]) %!error ... %! meanEffectSize (int32 ([1; 2; 3])) %!error ... %! meanEffectSize ([1; 2; 3], true (3, 1)) %!error ... %! meanEffectSize ([1; 2; 3], 'Foo', 1) %!error ... %! meanEffectSize ([1; 2; 3], 'Effect', 'foo') %!error ... %! meanEffectSize ([1; 2; 3], 'Effect', 'c') %!error ... %! meanEffectSize ([1; 2; 3], 'Effect', {}) %!error ... %! meanEffectSize ([1; 2; 3], 'Mean', [1, 2]) %!error ... %! meanEffectSize ([1; 2; 3], [1; 2; 3], 'Paired', 2) %!error ... %! meanEffectSize ([1; 2; 3], [1; 2; 3], 'VarianceType', 'foo') %!error ... %! meanEffectSize ([1; 2; 3], 'Alpha', 1) %!error ... %! meanEffectSize ([1; 2; 3], 'ConfidenceIntervalType', 'foo') %!error ... %! meanEffectSize ([1; 2; 3], 'NumBootstraps', 2.5) %!error ... %! meanEffectSize ([1; 2; 3], 'Resampling', 'foo') %!error ... %! meanEffectSize ([1; 2; 3], 'BootstrapOptions', 1) %!error ... %! meanEffectSize ([1; 2; 3], 'Paired', true) %!error ... %! meanEffectSize ([1; 2; 3], 'Effect', 'glass') %!error ... %! meanEffectSize ([1; 2; 3], [1; 2], 'Paired', true) %!error ... %! meanEffectSize ([1; 2; 3], [3; 2; 1], 'Paired', true, ... %! 'VarianceType', 'unequal') %!error ... %! meanEffectSize ([1; 2; 3], [3; 2; 1], 'Paired', true, 'Effect', 'glass') %!error ... %! meanEffectSize ([1; 2; 3], [3; 2; 1], 'Effect', 'mediandiff', ... %! 'ConfidenceIntervalType', 'exact') statistics-release-2.1.2/inst/Hypothesis_Testing/mmdtest.m000066400000000000000000000275351527311104300240010ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{mmdval} =} mmdtest (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{mmdval} =} mmdtest (@var{X}, @var{Y}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{mmdval}, @var{p}] =} mmdtest (@dots{}) ## @deftypefnx {statistics} {[@var{mmdval}, @var{p}, @var{h}] =} mmdtest (@dots{}) ## ## Two-sample multivariate test on the maximum mean discrepancy. ## ## @code{@var{mmdval} = mmdtest (@var{X}, @var{Y})} returns the squared ## maximum mean discrepancy between the samples @var{X} and @var{Y}, whose rows ## are observations and whose columns are variables. It measures how far ## apart the two distributions lie, 0 where the two samples are alike. With ## @math{m} rows in @var{X}, @math{n} in @var{Y} and @math{k} the kernel, it is ## the biased estimate ## @tex ## $$\mathrm{MMD}^2 = {1 \over m^2} \sum_{i,j=1}^{m} k(x_i, x_j) ## - {2 \over m n} \sum_{i=1}^{m} \sum_{j=1}^{n} k(x_i, y_j) ## + {1 \over n^2} \sum_{i,j=1}^{n} k(y_i, y_j).$$ ## @end tex ## @ifnottex ## @math{MMD^2 = sum (k(x_i, x_j)) / m^2 - 2 sum (k(x_i, y_j)) / (m n) + ## sum (k(y_i, y_j)) / n^2}. ## @end ifnottex ## ## @var{X} and @var{Y} are both numeric matrices with the same number of ## columns, or both tables, read over the variables they share, which must be ## all the variables of one of them. An observation holding a missing value in ## a variable used is left out. ## ## The kernel is Gaussian, @math{k(u, v) = exp (-d^2 / s)}. Each continuous ## variable is standardised by the mean and standard deviation of @var{X}, a ## standard deviation of 0 being taken as 1, and contributes its squared ## difference to @math{d^2}; a variable holding levels contributes 2 where the ## two observations differ, as one column per level would. The scale @math{s} ## is the median of @math{d^2} over the pairs of observations in @var{X}, taken ## as 1 where that median is 0. ## ## @code{[@var{mmdval}, @var{p}, @var{h}] = mmdtest (@dots{})} also returns the ## p-value of the permutation test of the null hypothesis that @var{X} and ## @var{Y} come from the same distribution, and @var{h}, which is 1 where the ## null hypothesis is rejected at the significance level @qcode{'Alpha'} and 0 ## otherwise. Each permutation deals the pooled observations out again as ## @var{X} and @var{Y}, standardises and scales them afresh, and computes the ## statistic; @var{p} is the share of the permutations whose statistic is at ## least the observed one. The permutations are drawn with @code{randperm}, ## so the state of @code{rand} decides them. They are drawn only where ## @var{p} or @var{h} is asked for. ## ## @code{@var{mmdval} = mmdtest (@dots{}, @var{Name}, @var{Value})} takes the ## following options. ## ## @multitable @columnfractions 0.26 0.02 0.72 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Alpha'} @tab @tab The significance level, a scalar between 0 ## and 1, 0.05 by default. ## @item @qcode{'NumPermutations'} @tab @tab The number of permutations, a ## positive integer, 1000 by default. ## @item @qcode{'VariableNames'} @tab @tab The variables to use, among those ## the two tables share, as a character vector, a string array or a cell array ## of character vectors. All the shared variables by default. ## @item @qcode{'CategoricalVariables'} @tab @tab The variables holding ## levels: @qcode{'all'}, their indices, a logical vector over the variables, ## or, for tables, their names. A table variable holding logical values, an ## unordered @code{categorical} array, a @code{string} array or a cell array of ## character vectors holds levels whether named here or not. A matrix holds ## none unless named. ## @item @qcode{'Options'} @tab @tab A structure as @code{statset} returns. ## Parallel computing and random streams are not implemented, and are refused ## where asked for. ## @end multitable ## ## Reference: A. Gretton, K. M. Borgwardt, M. J. Rasch, B. Schoelkopf and ## A. Smola (2012). A kernel two-sample test. Journal of Machine Learning ## Research, 13, 723-773. ## ## @seealso{knntest, kstest2} ## @end deftypefn function [mmdval, p, h] = mmdtest (X, Y, varargin) ## Input validation if (nargin < 2) error ("mmdtest: too few input arguments."); endif optNames = {'Alpha', 'NumPermutations', 'VariableNames', ... 'CategoricalVariables', 'Options'}; dfValues = {0.05, 1000, [], [], []}; [alpha, nperm, vnames, catvars, opts, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (args)) error ("mmdtest: invalid optional paired argument."); endif if (! (isnumeric (alpha) && isreal (alpha) && isscalar (alpha) && alpha > 0 && alpha < 1)) error ("mmdtest: 'Alpha' must be a scalar between 0 and 1."); endif if (! (isnumeric (nperm) && isreal (nperm) && isscalar (nperm) && isfinite (nperm) && nperm >= 1 && nperm == fix (nperm))) error ("mmdtest: 'NumPermutations' must be a positive integer."); endif if (! isempty (opts)) if (! isstruct (opts)) error ("mmdtest: 'Options' must be a structure."); endif if (isfield (opts, 'UseParallel') && ! isempty (opts.UseParallel) && ! strcmpi (opts.UseParallel, 'never') && ! isequal (opts.UseParallel, false)) error ("mmdtest: parallel computing is not implemented."); endif if (isfield (opts, 'Streams') && ! isempty (opts.Streams)) error ("mmdtest: random streams are not implemented."); endif endif ## The pooled sample coded one column per variable, rows holding a missing ## value left out, and which variables hold levels [Z, iscat, mx, my, errmsg] = __twosample__ (X, Y, vnames, catvars); if (! isempty (errmsg)) error ("mmdtest: %s", errmsg); endif m = mx + my; ## A difference in levels does not depend on how the sample is dealt out; ## a continuous difference is rescaled by each dealing's own X Dlev = zeros (m); for j = find (iscat) Dlev += 2 * (Z(:,j) != Z(:,j)'); endfor cont = find (! iscat); G = cell (1, numel (cont)); for j = 1:numel (cont) G{j} = (Z(:,cont(j)) - Z(:,cont(j))') .^ 2; endfor mmdval = mmdStat (Z(:,cont), G, Dlev, 1:mx, mx+1:m); if (nargout < 2) return; endif ## Permutations as extreme as the observed statistic, to within rounding count = 0; tol = 1e-12 * abs (mmdval); for i = 1:nperm r = randperm (m); count += mmdStat (Z(:,cont), G, Dlev, r(1:mx), r(mx+1:end)) >= mmdval - tol; endfor p = count / nperm; h = double (p <= alpha); endfunction ## The squared maximum mean discrepancy with the rows A as X and B as Y. function v = mmdStat (C, G, Dlev, a, b) D2 = Dlev; sd = std (C(a,:), [], 1); sd(sd == 0 | ! isfinite (sd)) = 1; for j = 1:numel (G) D2 += G{j} / sd(j) ^ 2; endfor Dx = D2(a,a); s = median (Dx(triu (true (numel (a)), 1))); if (! (s > 0)) s = 1; endif K = exp (-D2 / s); v = mean (K(a,a)(:)) - 2 * mean (K(a,b)(:)) + mean (K(b,b)(:)); endfunction ## Expected values from MATLAB R2026a %!assert_equal (mmdtest ([0; 1], 0.5), 0.126338154442911, -1e-13) %!assert_equal (mmdtest ([0; 1], 1.5), 0.799739712952452, -1e-13) %!assert_equal (mmdtest ([0; 1], 3), 1.665500671892900, -1e-13) %!assert_equal (mmdtest ([0; 1], 10), 1.683939720585721, -1e-13) %!assert_equal (mmdtest ([0; 1], 1), 0.316060279414279, -1e-13) %!assert_equal (mmdtest ([0; 2], 1), 0.126338154442911, -1e-13) %!assert_equal (mmdtest ([0; 10], 5), 0.126338154442911, -1e-13) %!assert_equal (mmdtest ([0; 1; 4], 2), 0.200029287193120, -1e-13) %!assert_equal (mmdtest ([0; 1; 4], [2; 6]), 0.269711792340645, -1e-13) %!assert_equal (mmdtest ([2; 6], [0; 1; 4]), 0.237458896831246, -1e-13) %!assert_equal (mmdtest ([0; 1; NaN], 1.5), 0.799739712952452, -1e-13) %!assert_equal (mmdtest ([0, 0; 1, 10], [1, 0]), 0.470878401160454, -1e-13) %!assert_equal (mmdtest ([0, 0; 1, 10; 3, 2], [1, 0; 2, 5]), ... %! 0.102827836885193, -1e-13) %!assert_equal (mmdtest ([0, 0; 1, 10; 3, 2], [1, 0; 2, 5; 4, 4; 0, 9]), ... %! 0.029108941526544, -1e-12) %!assert_equal (mmdtest ([1; 2; 1], [2; 2], 'CategoricalVariables', 1), ... %! 0.561884941180940, -1e-13) %!assert_equal (mmdtest ([1; 2; 1; 3], [2; 2; 3], ... %! 'CategoricalVariables', 1), 0.272163018384518, -1e-13) %!assert_equal (mmdtest ([1, 1; 2, 1; 1, 2], [2, 2; 2, 1], ... %! 'CategoricalVariables', 'all'), ... %! 0.419413238496425, -1e-13) %!assert_equal (mmdtest ([1, 0; 2, 1; 1, 4], [2, 2; 1, 6], ... %! 'CategoricalVariables', 1), 0.266631734445491, -1e-13) %!test %! ## Six pairs in X: the median of the squared distances, not its square %! v = mmdtest ([1, 0; 2, 1; 1, 4; 2, 3], [2, 2; 1, 6; 3, 1], ... %! 'CategoricalVariables', 1); %! assert_equal (v, 0.149533632990419, -1e-13); %!test %! ## A table's categorical variable holds levels without being named %! v = mmdtest (table (categorical ([1; 2; 1])), table (categorical ([2; 2]))); %! assert_equal (v, 0.561884941180940, -1e-13); %!test %! ## A standard deviation of 0 is taken as 1, and so is a scale of 0 %! assert_equal (mmdtest ([1, 0; 1, 1; 1, 2], [2, 0; 3, 1]), ... %! 0.887991689349540, -1e-13); %! assert_equal (mmdtest ([1; 1; 1], [2; 3]), 1.297744640525545, -1e-13); %!test %! load fisheriris %! assert_equal (mmdtest (meas(1:25,:), meas(26:50,:)), ... %! 0.028159364150432, -1e-12); %! assert_equal (mmdtest (meas(51:100,:), meas(101:150,:)), ... %! 0.540247447946648, -1e-12); %! assert_equal (mmdtest (meas(101:150,:), meas(51:100,:)), ... %! 0.604761188982786, -1e-12); %!test %! ## Each permutation is standardised afresh; the exact p-value over all 70 %! ## dealings is 0.2286, and MATLAB gave 0.2303 and 0.2273 over 20000 %! rand ('seed', 1); %! [v, p] = mmdtest ([2.5; 4; 3; 2], [3; -8; 2.5; 0], 'NumPermutations', 4000); %! assert_equal (v, 0.249999985933103, -1e-13); %! assert_equal (abs (p - 0.2286) < 0.03, true); %!test %! rand ('seed', 1); %! [~, p, h] = mmdtest ([0; 0.1; 0.2], [10; 10.1; 10.2], ... %! 'NumPermutations', 9, 'Alpha', 0.2); %! assert_equal (p * 9, round (p * 9)); %! assert_equal (h, double (p <= 0.2)); %!assert_equal (mmdtest ([0; 1], 0.5, 'Options', ... %! statset ('UseParallel', false)), ... %! 0.126338154442911, -1e-13) %!error mmdtest (1) %!error mmdtest (1, 2, 'Tail', 1) %!error ... %! mmdtest ([0; 1], 2, 'Alpha', 0) %!error ... %! mmdtest ([0; 1], 2, 'NumPermutations', 0) %!error ... %! mmdtest ([0; 1], 2, 'Options', 1) %!error ... %! mmdtest ([0; 1], 2, 'Options', struct ('UseParallel', true)) %!error ... %! mmdtest ([0; 1], 2, 'Options', struct ('Streams', 1)) %!error ... %! mmdtest ([0, 1; 1, 2], 2) %!error ... %! mmdtest (NaN, 2) statistics-release-2.1.2/inst/Hypothesis_Testing/multcompare.m000066400000000000000000001531171527311104300246500ustar00rootroot00000000000000## Copyright (C) 2022 Andrew Penn ## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{C} =} multcompare (@var{STATS}) ## @deftypefnx {statistics} {@var{C} =} multcompare (@var{STATS}, "name", @var{value}) ## @deftypefnx {statistics} {[@var{C}, @var{M}] =} multcompare (...) ## @deftypefnx {statistics} {[@var{C}, @var{M}, @var{H}] =} multcompare (...) ## @deftypefnx {statistics} {[@var{C}, @var{M}, @var{H}, @var{GNAMES}] =} multcompare (...) ## @deftypefnx {statistics} {@var{padj} =} multcompare (@var{p}) ## @deftypefnx {statistics} {@var{padj} =} multcompare (@var{p}, "ctype", @var{CTYPE}) ## ## Perform posthoc multiple comparison tests or p-value adjustments to control ## the family-wise error rate (FWER) or false discovery rate (FDR). ## ## @code{@var{C} = multcompare (@var{STATS})} performs a multiple comparison ## using a @var{STATS} structure that is obtained as output from any of ## the following functions: anova1, anova2, anovan, kruskalwallis, and friedman. ## The return value @var{C} is a matrix with one row per comparison and six ## columns. Columns 1-2 are the indices of the two samples being compared. ## Columns 3-5 are a lower bound, estimate, and upper bound for their ## difference, where the bounds are for 95% confidence intervals. Column 6-8 are ## the multiplicity adjusted p-values for each individual comparison, the test ## statistic and the degrees of freedom. For @var{STATS} from kruskalwallis or ## friedman, the comparisons use a z statistic, and the degrees of freedom are ## reported as @code{Inf}, which is how the normal reference distribution is ## signalled; @code{tcdf} and @code{tinv} at @code{Inf} agree with ## @code{normcdf} and @code{norminv}. ## All tests by multcompare are two-tailed. ## ## @qcode{multcompare} can take a number of optional parameters as name-value ## pairs. ## ## @code{[@dots{}] = multcompare (@var{STATS}, "alpha", @var{ALPHA})} ## ## @itemize ## @item ## @var{ALPHA} sets the significance level of null hypothesis significance ## tests to ALPHA, and the central coverage of two-sided confidence intervals to ## 100*(1-@var{ALPHA})%. (Default ALPHA is 0.05). ## @end itemize ## ## @code{[@dots{}] = multcompare (@var{STATS}, "ControlGroup", @var{REF})} ## ## @itemize ## @item ## @var{REF} is the index of the control group to limit comparisons to. The ## index must be a positive integer scalar value. For each dimension (d) listed ## in @var{DIM}, multcompare uses STATS.grpnames@{d@}(idx) as the control group. ## (Default is empty, i.e. [], for full pairwise comparisons) ## @end itemize ## ## @code{[@dots{}] = multcompare (@var{STATS}, "ctype", @var{CTYPE})} ## ## @itemize ## @item ## @var{CTYPE} is the type of comparison test to use. In order of increasing ## power, the choices are: "bonferroni", "scheffe", "mvt", "holm" (default), ## "hochberg", "fdr", or "lsd". The first five methods control the family-wise ## error rate. The "fdr" method controls false discovery rate (by the original ## Benjamini-Hochberg step-up procedure). The final method, "lsd" (or "none"), ## makes no attempt to control the Type 1 error rate of multiple comparisons. ## The coverage of confidence intervals are only corrected for multiple ## comparisons in the cases where @var{CTYPE} is "bonferroni", "scheffe" or ## "mvt", which control the Type 1 error rate for simultaneous inference. ## ## The "mvt" method uses the multivariate t distribution to assess the ## probability or critical value of the maximum statistic across the tests, ## thereby accounting for correlations among comparisons in the control of the ## family-wise error rate with simultaneous inference. In the case of pairwise ## comparisons, it simulates Tukey's (or the Games-Howell) test, in the case of ## comparisons with a single control group, it simulates Dunnett's test. ## @var{CTYPE} values "tukey-kramer" and "hsd" are recognised but set the value ## of @var{CTYPE} and @var{REF} to "mvt" and empty respectively. A @var{CTYPE} ## value "dunnett" is recognised but sets the value of @var{CTYPE} to "mvt", and ## if @var{REF} is empty, sets @var{REF} to 1. Since the algorithm uses a Monte ## Carlo method (of 1e+06 random samples), you can expect the results to ## fluctuate slightly with each call to multcompare and the calculations may be ## slow to complete for a large number of comparisons. If the parallel package ## is installed and loaded, @qcode{multcompare} will automatically accelerate ## computations by parallel processing. Note that p-values calculated by the ## "mvt" are truncated at 1e-06. ## @end itemize ## ## @code{[@dots{}] = multcompare (@var{STATS}, "df", @var{DF})} ## ## @itemize ## @item ## @var{DF} is an optional scalar value to set the number of degrees of freedom ## in the calculation of p-values for the multiple comparison tests. By default, ## this value is extracted from the @var{STATS} structure of the ANOVA test, but ## setting @var{DF} maybe necessary to approximate Satterthwaite correction if ## @qcode{anovan} was performed using weights. ## @end itemize ## ## @code{[@dots{}] = multcompare (@var{STATS}, "dim", @var{DIM})} ## ## @itemize ## @item ## @var{DIM} is a vector specifying the dimension or dimensions over which the ## estimated marginal means are to be calculated. Used only if STATS comes from ## anovan. The value [1 3], for example, computes the estimated marginal mean ## for each combination of the first and third predictor values. The default is ## to compute over the first dimension (i.e. 1). If the specified dimension is, ## or includes, a continuous factor then @qcode{multcompare} will return an ## error. ## @end itemize ## ## @code{[@dots{}] = multcompare (@var{STATS}, "estimate", @var{ESTIMATE})} ## ## @itemize ## @item ## @var{ESTIMATE} is a string specifying the estimates to be compared when ## computing multiple comparisons after anova2; this argument is ignored by ## anovan and anova1. Accepted values for @var{ESTIMATE} are either "column" ## (default) to compare column means, or "row" to compare row means. If the ## model type in anova2 was "linear" or "nested" then only "column" is accepted ## for @var{ESTIMATE} since the row factor is assumed to be a random effect. ## @end itemize ## ## @code{[@dots{}] = multcompare (@var{STATS}, "display", @var{DISPLAY})} ## ## @itemize ## @item ## @var{DISPLAY} is either "on" (the default): to display a table and graph of ## the comparisons (e.g. difference between means), their 100*(1-@var{ALPHA})% ## intervals and multiplicity adjusted p-values in APA style; or "off": to omit ## the table and graph. On the graph, markers and error bars colored red have ## multiplicity adjusted p-values < ALPHA, otherwise the markers and error bars ## are blue. ## @end itemize ## ## @code{[@dots{}] = multcompare (@var{STATS}, "seed", @var{SEED})} ## ## @itemize ## @item ## @var{SEED} is a scalar value used to initialize the random number generator ## so that @var{CTYPE} "mvt" produces reproducible results. ## @end itemize ## ## @code{[@var{C}, @var{M}, @var{H}, @var{GNAMES}] = multcompare (@dots{})} ## returns additional outputs. @var{M} is a matrix where columns 1-2 are the ## estimated marginal means and their standard errors, and columns 3-4 are lower ## and upper bounds of the confidence intervals for the means; the critical ## value of the test statistic is scaled by a factor of 2^(-0.5) before ## multiplying by the standard errors of the group means so that the intervals ## overlap when the difference in means becomes significant at approximately ## the level @var{ALPHA}. When @var{ALPHA} is 0.05, this corresponds to ## confidence intervals with 83.4% central coverage. @var{H} is a handle to the ## figure containing the graph. @var{GNAMES} is a cell array with one row for ## each group, containing the names of the groups. ## ## @code{@var{padj} = multcompare (@var{p})} calculates and returns adjusted ## p-values (@var{padj}) using the Holm-step down Bonferroni procedure to ## control the family-wise error rate. ## ## @code{@var{padj} = multcompare (@var{p}, "ctype", @var{CTYPE})} calculates ## and returns adjusted p-values (@var{padj}) computed using the method ## @var{CTYPE}. In order of increasing power, @var{CTYPE} for p-value adjustment ## can be either "bonferroni", "holm" (default), "hochberg", or "fdr". See ## above for further information about the @var{CTYPE} methods. ## ## @seealso{anova1, anova2, anovan, kruskalwallis, friedman, fitlm} ## @end deftypefn function [C, M, H, GNAMES] = multcompare (STATS, varargin) if (nargin < 1) print_usage; endif ## Check supplied parameters if ((numel (varargin) / 2) != fix (numel (varargin) / 2)) error ("multcompare: wrong number of arguments.") endif ALPHA = 0.05; REF = []; CTYPE = 'holm'; DISPLAY = 'on'; DIM = 1; ESTIMATE = 'column'; DFE = []; for idx = 3:2:nargin name = varargin{idx-2}; value = varargin{idx-1}; switch (lower (name)) case 'alpha' ALPHA = value; case {'controlgroup','ref'} REF = value; case {'ctype','criticalvaluetype'} CTYPE = lower (value); case {'display','displayopt'} DISPLAY = lower (value); case {'dim','dimension'} DIM = value; case 'estimate' ESTIMATE = lower (value); case {'df','dfe'} DFE = value; case {'seed'} SEED = value; ## Set random seed for mvtrnd and mvnrnd randn ('seed', SEED); randg ('seed', SEED); otherwise error ("multcompare: parameter %s is not supported.", name); endswitch endfor ## Evaluate ALPHA input argument if (! isa (ALPHA,'numeric') || numel (ALPHA) != 1) error ("multcompare:alpha must be a numeric scalar value."); endif if ((ALPHA <= 0) || (ALPHA >= 1)) error ("multcompare: alpha must be a value between 0 and 1."); endif ## Evaluate CTYPE input argument if (ismember (CTYPE, {'tukey-kramer', 'hsd'})) CTYPE = 'mvt'; REF = []; elseif (strcmp (CTYPE, 'dunnett')) CTYPE = 'mvt'; if (isempty (REF)) REF = 1; endif elseif (strcmp (CTYPE, 'none')) CTYPE = 'lsd'; endif if (! ismember (CTYPE, ... {'bonferroni','scheffe','mvt','holm','hochberg','fdr','lsd'})) error ("multcompare: '%s' is not a supported value for CTYPE.", CTYPE) endif ## Evaluate DFE input argument if (! isempty (DFE)) if (! isscalar (DFE)) error ("multcompare: df must be a scalar value."); endif if (! (DFE > 0) || isinf (DFE)) error ("multcompare: df must be a positive finite value."); endif endif ## If STATS is numeric, assume it is a vector of p-values if (isnumeric (STATS)) if (nargout > 1) error (strcat ("multcompare: invalid number of output", ... " arguments if only used to adjust p-values.")) endif if (! isempty (varargin)) if (! any (strcmpi (varargin{1}, {'ctype','criticalvaluetype'})) ... || (nargin > 3) ) error (strcat ("multcompare: invalid input arguments", ... " if only used to adjust p-values.")); endif endif if (! ismember (CTYPE, {'bonferroni','holm','hochberg','fdr'})) error ("multcompare: '%s' is not a supported p-adjustment method.", ... CTYPE) endif p = STATS; if (all (size (p) > 1)) error ("multcompare: p-values must be a vector.") endif padj = feval (CTYPE, p); if (size (p, 1) > 1) C = padj; else C = padj'; endif return endif ## Perform test specific calculations switch (STATS.source) case 'anova1' ## Make matrix of requested comparisons (pairs) ## Also return the corresponding hypothesis matrix (L) n = STATS.n(:); Ng = numel (n); if (isempty (REF)) ## Pairwise comparisons [pairs, L] = pairwise (Ng); else ## Treatment vs. Control comparisons [pairs, L] = trt_vs_ctrl (Ng, REF); endif Np = size (pairs, 1); switch (STATS.vartype) case 'equal' ## Calculate estimated marginal means and their standard errors gmeans = STATS.means(:); gvar = (STATS.s^2) ./ n; # Sampling variance gcov = diag (gvar); Ng = numel (gmeans); M = zeros (Ng, 4); M(:,1:2) = cat (2, gmeans, sqrt (gvar)); ## Get the error degrees of freedom from anova1 output if (isempty (DFE)) DFE = STATS.df; endif case 'unequal' ## Error checking if (strcmp (CTYPE, 'scheffe')) error (strcat ("multcompare: the CTYPE value 'scheffe'", ... " does not support tests with varying", ... " degrees of freedom ")); endif ## Calculate estimated marginal means and their standard errors gmeans = STATS.means(:); gvar = STATS.vars(:) ./ n; # Sampling variance gcov = diag (gvar); Ng = numel (gmeans); M = zeros (Ng, 4); M(:,1:2) = cat (2, gmeans, sqrt (gvar)); ## Calculate Welch's corrected degrees of freedom if (isempty (DFE)) DFE = sum (gvar(pairs), 2).^2 ./ ... sum ((gvar(pairs).^2 ./ (n(pairs) - 1)), 2); endif endswitch ## Calculate t statistics corresponding to the comparisons defined in L [mean_diff, sed, t] = tValue (gmeans, gcov, L); ## Calculate correlation matrix vcov = L * gcov * L'; R = cov2corr (vcov); ## Create cell array of group names corresponding to each row of m GNAMES = STATS.gnames; case 'anova2' ## Fetch estimate specific information from the STATS structure switch (ESTIMATE) case {'column','columns','col','cols'} gmeans = STATS.colmeans(:); Ng = numel (gmeans); n = STATS.coln; case {'row','rows'} if (ismember (STATS.model, {'linear','nested'})) error (strcat ("multcompare: no support for the row factor",... " (random effect) in a 'nested' or 'linear'",... " anova2 model.")); endif gmeans = STATS.rowmeans(:); Ng = numel (gmeans); n = STATS.rown; endswitch ## Make matrix of requested comparisons (pairs) ## Also return the corresponding hypothesis matrix (L) if (isempty (REF)) ## Pairwise comparisons [pairs, L, R] = pairwise (Ng); else ## Treatment vs. Control comparisons [pairs, L, R] = trt_vs_ctrl (Ng, REF); endif Np = size (pairs, 1); ## Calculate estimated marginal means and their standard errors gvar = ((STATS.sigmasq) / n) * ones (Ng, 1); # Sampling variance gcov = diag (gvar); M = zeros (Ng, 4); M(:,1:2) = cat (2, gmeans, sqrt (gvar)); ## Get the error degrees of freedom from anova2 output if (isempty (DFE)) DFE = STATS.df; endif ## Calculate t statistics corresponding to the comparisons defined in L [mean_diff, sed, t] = tValue (gmeans, gcov, L); ## Create character array of group names corresponding to each row of m GNAMES = cellstr (num2str ([1:Ng]')); case 'anovan' ## Our calculations treat all effects as fixed if (ismember (STATS.random, DIM)) warning (strcat ("multcompare: ignoring random effects", ... " (all effects treated as fixed).")); endif ## Check what type of factor is requested in DIM if (any (STATS.nlevels(DIM) < 2)) error (strcat ("multcompare: DIM must specify only categorical", ... " factors with 2 or more degrees of freedom.")); endif ## Check that all continuous variables were centered msg = strcat ("multcompare: use a STATS structure from a model", ... " refit with a sum-to-zero contrast coding."); if (any (STATS.continuous - STATS.center_continuous)) error (msg) endif ## Check that the columns sum to 0 N = numel (STATS.contrasts); for j = 1:N if (isnumeric (STATS.contrasts{j})) if (any (abs (sum (STATS.contrasts{j})) > eps ('single'))) error (msg); endif endif endfor ## Calculate estimated marginal means and their standard errors Nd = numel (DIM); n = numel (STATS.resid); df = STATS.df; if (isempty (DFE)) DFE = STATS.dfe; endif i = 1 + cumsum (df); k = find (sum (STATS.terms(:,DIM), 2) == sum (STATS.terms, 2)); Nb = 1 + sum (df(k)); Nt = numel (k); L = zeros (n, sum (df) + 1); for j = 1:Nt L(:, i(k(j)) - df(k(j)) + 1 : i(k(j))) = STATS.X(:,i(k(j)) - ... df(k(j)) + 1 : i(k(j))); endfor L(:,1) = 1; U = unique (L, 'rows', 'stable'); Ng = size (U, 1); idx = zeros (Ng, 1); for k = 1:Ng idx(k) = find (all (L == U(k, :), 2),1); endfor gmeans = U * STATS.coeffs(:,1); # Estimated marginal means gcov = U * STATS.vcov * U'; gvar = diag (gcov); # Sampling variance M = zeros (Ng, 4); M(:,1:2) = cat (2, gmeans, sqrt (gvar)); ## Create cell array of group names corresponding to each row of m GNAMES = cell (Ng, 1); for i = 1:Ng str = ''; for j = 1:Nd str = sprintf ("%s%s=%s, ", str, ... num2str (STATS.varnames{DIM(j)}), ... num2str (STATS.grpnames{DIM(j)}{STATS.grps(idx(i),DIM(j))})); endfor GNAMES{i} = str(1:end-2); str = ''; endfor ## Make matrix of requested comparisons (pairs) ## Also return the corresponding hypothesis matrix (L) if (isempty (REF)) ## Pairwise comparisons [pairs, L] = pairwise (Ng); else ## Treatment vs. Control comparisons [pairs, L] = trt_vs_ctrl (Ng, REF); endif Np = size (pairs, 1); ## Calculate t statistics corresponding to the comparisons defined in L [mean_diff, sed, t] = tValue (gmeans, gcov, L); ## Calculate correlation matrix. vcov = L * gcov * L'; R = cov2corr (vcov); case 'friedman' ## Get stats from structure gmeans = STATS.meanranks(:); Ng = length (gmeans); sigma = STATS.sigma; ## Make group names GNAMES = strjust (num2str ((1:Ng)'), 'left'); ## Make matrix of requested comparisons (pairs) ## Also return the corresponding hypothesis matrix (L) if (isempty (REF)) ## Pairwise comparisons [pairs, L, R] = pairwise (Ng); else ## Treatment vs. Control comparisons [pairs, L, R] = trt_vs_ctrl (Ng, REF); endif Np = size (pairs, 1); ## Calculate covariance matrix gcov = ((sigma ^ 2) / STATS.n) * eye (Ng); ## Create matrix with group means and standard errors M = cat (2, gmeans, sqrt (diag (gcov))); ## Calculate t statistics corresponding to the comparisons defined in L [mean_diff, sed, t] = tValue (gmeans, gcov, L); # z-statistic (not t) ## Calculate degrees of freedom from number of groups if (isempty (DFE)) DFE = inf; # this is a z-statistic so infinite degrees of freedom endif case 'kruskalwallis' ## Get stats from structure gmeans = STATS.meanranks(:); sumt = STATS.sumt; Ng = length (gmeans); n = STATS.n(:); N = sum (n); ## Make group names GNAMES = STATS.gnames; ## Make matrix of requested comparisons (pairs) ## Also return the corresponding hypothesis matrix (L) if (isempty (REF)) ## Pairwise comparisons [pairs, L] = pairwise (Ng); else ## Treatment vs. Control comparisons [pairs, L] = trt_vs_ctrl (Ng, REF); endif Np = size (pairs, 1); ## Calculate covariance matrix gcov = diag (((N * (N + 1) / 12) - (sumt / (12 * (N - 1)))) ./ n); ## Create matrix with group means and standard errors M = cat (2, gmeans, sqrt (diag (gcov))); ## Calculate t statistics corresponding to the comparisons defined in L [mean_diff, sed, t] = tValue (gmeans, gcov, L); # z-statistic (not t) ## Calculate correlation matrix vcov = L * gcov * L'; R = cov2corr (vcov); ## Calculate degrees of freedom from number of groups if (isempty (DFE)) DFE = inf; # this is a z-statistic so infinite degrees of freedom endif otherwise error (strcat ("multcompare: the STATS structure from %s", ... " is not currently supported"), STATS.source); endswitch ## The test specific code above needs to create the following variables in ## order to proceed with the remainder of the function tasks ## - Ng: number of groups involved in comparisons ## - M: Ng-by-2 matrix of group means (col 1) and standard errors (col 2) ## - Np: number of comparisons (pairs of groups being compared) ## - pairs: Np-by-2 matrix of numeric group IDs - each row is a comparison ## - R: correlation matrix for the requested comparisons ## - sed: vector containing SE of the difference for each comparisons ## - t: vector containing t for the difference relating to each comparisons ## - DFE: residual/error degrees of freedom ## - GNAMES: a cell array containing the names of the groups being compared ## Create matrix of comparisons and calculate confidence intervals and ## multiplicity adjusted p-values for the comparisons. C = zeros (Np, 8); C(:,1:2) = pairs; C(:,4) = (M(pairs(:, 1),1) - M(pairs(:, 2),1)); C(:,7) = t; # Unlike Matlab, we include the t statistic C(:,8) = DFE; # Unlike Matlab, we include the degrees of freedom if (any (isinf (DFE))) p = 2 * normcdf (-abs (t)); else p = 2 * tcdf (-abs (t), DFE); endif [C(:,6), critval, C(:,8)] = feval (CTYPE, p, t, Ng, DFE, R, ALPHA); C(:,3) = C(:,4) - sed .* critval; C(:,5) = C(:,4) + sed .* critval; ## Calculate confidence intervals of the estimated marginal means with ## central coverage such that the intervals start to overlap where the ## difference reaches a two-tailed p-value of ALPHA. When ALPHA is 0.05, ## central coverage is approximately 83.4% if (! isscalar (DFE)) # Upper bound critval (corresponding to lower bound DFE) critval = max (critval); endif M(:,3) = M(:,1) - M(:,2) .* critval / sqrt (2); M(:,4) = M(:,1) + M(:,2) .* critval / sqrt (2); ## If requested, plot graph of the difference means for each comparison ## with central coverage of confidence intervals at 100*(1-alpha)% switch (lower (DISPLAY)) case {'on',true} H = figure; plot ([0; 0], [0; Np + 1]','k:'); # Plot vertical dashed line at 0 effect set (gca, 'Ydir', 'reverse') # Flip y-axis direction ylim ([0.5, Np + 0.5]); # Set y-axis limits hold on # Plot on the same axis for j = 1:Np if (C(j,6) < ALPHA) ## Plot marker for the difference in means plot (C(j,4), j,'or','MarkerFaceColor', 'r'); ## Plot line for each confidence interval plot ([C(j,3), C(j,5)], j * ones (2,1), 'r-'); else ## Plot marker for the difference in means plot (C(j,4), j,'ob','MarkerFaceColor', 'b'); ## Plot line for each confidence interval plot ([C(j,3), C(j,5)], j * ones (2,1), 'b-'); endif endfor hold off xlabel (sprintf ("%g%% confidence interval for the difference",... 100 * (1 - ALPHA))); ylabel ('Row number in matrix of comparisons (C)'); case {'off',false} H = []; endswitch ## Print multcompare table on screen if no output argument was requested if (nargout == 0 || strcmp (DISPLAY, 'on')) printf ("\n %s Multiple Comparison (Post Hoc) Test for %s\n\n", ... upper (CTYPE), upper (STATS.source)); header = strcat ("Group ID Group ID LBoundDiff EstimatedDiff",... " UBoundDiff p-value\n", ... "-------------------------------------------------",... "---------------------\n"); printf ("%s", header); for j = 1:Np if (C(j,6) < 0.001) printf ("%5i %5i %10.3f %10.3f %10.3f <.001\n",... C(j,1), C(j,2), C(j,3), C(j,4), C(j,5)); elseif (C(j,6) < 0.9995) printf ("%5i %5i %10.3f %10.3f %10.3f .%03u\n",... C(j,1), C(j,2), C(j,3), C(j,4), C(j,5), round (C(j,6) * 1e+03)); else printf ("%5i %5i %10.3f %10.3f %10.3f 1.000\n",... C(j,1), C(j,2), C(j,3), C(j,4), C(j,5)); endif endfor printf ("\n"); endif endfunction ## Posthoc comparisons function [pairs, L, R] = pairwise (Ng) ## Create pairs matrix for pairwise comparisons gid = [1:Ng]'; # Create numeric group ID A = ones (Ng, 1) * gid'; B = tril (gid * ones (1, Ng),-1); pairs = [A(:), B(:)]; ridx = (pairs(:, 2) == 0); pairs(ridx, :) = []; ## Calculate correlation matrix (required for CTYPE "mvt") Np = size (pairs, 1); L = zeros (Np, Ng); for j = 1:Np L(j, pairs(j,:)) = [1,-1]; # Hypothesis matrix endfor R = corr (L'); # Correlation matrix endfunction function [pairs, L, R] = trt_vs_ctrl (Ng, REF) ## Create pairs matrix for comparisons with control (REF) gid = [1:Ng]'; # Create numeric group ID pairs = zeros (Ng - 1, 2); pairs(:, 1) = REF; pairs(:, 2) = gid(gid != REF); ## Calculate correlation matrix (required for CTYPE "mvt") Np = size (pairs, 1); L = zeros (Np, Ng); for j = 1:Np L(j, pairs(j,:)) = [1,-1]; # Hypothesis matrix endfor R = corr (L'); # Correlation matrix endfunction function [mn, se, t] = tValue (gmeans, gcov, L) ## Calculate means, standard errors and t (or z) statistics ## corresponding to the comparisons defined in L. mn = sum (L * diag (gmeans), 2); se = sqrt (diag (L * gcov * L')); t = mn ./ se; endfunction function R = cov2corr (vcov) ## Convert covariance matrix to correlation matrix sed = sqrt (diag (vcov)); R = vcov ./ (sed * sed'); R = (R + R') / 2; # This step ensures that the matrix is positive definite endfunction ## Methods to control family-wise error rate in multiple comparisons function [padj, critval, dfe] = scheffe (p, t, Ng, dfe, R, ALPHA) ## Calculate the p-value if (isinf (dfe)) padj = chi2cdf (t.^2, Ng - 1, 'upper'); else padj = fcdf ((t.^2) / (Ng - 1), Ng - 1, dfe, 'upper'); endif ## Calculate critical value at Scheffe-adjusted ALPHA level if (isinf (dfe)) tmp = chi2inv (1 - ALPHA, Ng - 1) / (Ng - 1); else tmp = finv (1 - ALPHA, Ng - 1, dfe); endif critval = sqrt ((Ng - 1) * tmp); endfunction function [padj, critval, dfe] = bonferroni (p, t, Ng, dfe, R, ALPHA) ## Bonferroni procedure Np = numel (p); padj = min (p * Np, 1.0); ## If requested, calculate critical value at Bonferroni-adjusted ALPHA level if (nargout > 1) critval = tinv (1 - ALPHA / Np * 0.5, dfe); endif endfunction function [padj, critval, dfe] = mvt (p, t, Ng, dfe, R, ALPHA) ## Monte Carlo simulation of the maximum test statistic in random samples ## generated from a multivariate t distribution. This method accounts for ## correlations among comparisons. This method simulates Tukey's test in the ## case of pairwise comparisons or Dunnett's tests in the case of trt_vs_ctrl. ## The "mvt" method is equivalent to methods used in the following R packages: ## - emmeans: the "mvt" adjust method in functions within emmeans ## - glht: the "single-step" adjustment in the multcomp.function ## Lower bound for error degrees of freedom to ensure type 1 error rate isn't ## exceeded for any test if (! isscalar (dfe)) dfe = max (1, round (min (dfe))); fprintf ("Note: df set to %u (lower bound)\n", dfe); endif ## Check if we can use parallel processing to accelerate computations pat = '^parallel'; software = pkg ('list'); names = cellfun (@(S) S.name, software, 'UniformOutput', false); status = cellfun (@(S) S.loaded, software, 'UniformOutput', false); index = find (! cellfun (@isempty, regexpi (names, pat))); if (! isempty (index)) if (logical (status{index})) PARALLEL = true; else PARALLEL = false; endif else PARALLEL = false; endif ## Generate the distribution of (correlated) t statistics under the null, and ## calculate the maximum test statistic for each random sample. Computations ## are performed in chunks to prevent memory issues when the number of ## comparisons is large. chunkSize = 1000; numChunks = 1000; nsim = chunkSize * numChunks; if (isinf (dfe)) # Multivariate z-statistics func = @(jnk) max (abs (mvnrnd (0, R, chunkSize)'), [], 1); else # Multivariate t-statistics func = @(jnk) max (abs (mvtrnd (R, dfe, chunkSize)'), [], 1); endif if (PARALLEL) maxT = cell2mat (parcellfun (nproc, func, ... cell (1, numChunks), 'UniformOutput', false)); else maxT = cell2mat (cellfun (func, cell (1, numChunks), 'UniformOutput', false)); endif ## Calculate multiplicity adjusted p-values (two-tailed) padj = max (sum (bsxfun (@ge, maxT, abs (t)), 2) / nsim, nsim^-1); ## Calculate critical value adjusted by the maxT procedure critval = quantile (maxT, 1 - ALPHA); endfunction function [padj, critval, dfe] = holm (p, t, Ng, dfe, R, ALPHA) ## Holm's step-down Bonferroni procedure ## Order raw p-values [ps, idx] = sort (p, 'ascend'); Np = numel (ps); ## Implement Holm's step-down Bonferroni procedure padj = nan (Np,1); padj(1) = Np * ps(1); for i = 2:Np padj(i) = max (padj(i - 1), (Np - i + 1) * ps(i)); endfor ## Reorder the adjusted p-values to match the order of the original p-values [~, original_order] = sort (idx, 'ascend'); padj = padj(original_order); ## Truncate adjusted p-values to 1.0 padj(padj>1) = 1; ## If requested, calculate critical value at ALPHA ## No adjustment to confidence interval coverage if (nargout > 1) critval = tinv (1 - ALPHA / 2, dfe); endif endfunction function [padj, critval, dfe] = hochberg (p, t, Ng, dfe, R, ALPHA) ## Hochberg's step-up Bonferroni procedure ## Order raw p-values [ps, idx] = sort (p, 'ascend'); Np = numel (ps); ## Implement Hochberg's step-down Bonferroni procedure padj = nan (Np,1); padj(Np) = ps(Np); for j = 1:Np-1 i = Np - j; padj(i) = min (padj(i + 1), (Np -i + 1) * ps(i)); endfor ## Reorder the adjusted p-values to match the order of the original p-values [~, original_order] = sort (idx, 'ascend'); padj = padj(original_order); ## Truncate adjusted p-values to 1.0 padj(padj>1) = 1; ## If requested, calculate critical value at ALPHA ## No adjustment to confidence interval coverage if (nargout > 1) critval = tinv (1 - ALPHA / 2, dfe); endif endfunction function [padj, critval, dfe] = fdr (p, t, Ng, dfe, R, ALPHA) ## Benjamini-Hochberg procedure to control the false discovery rate (FDR) ## This procedure does not control the family-wise error rate ## Order raw p-values [ps, idx] = sort (p, 'ascend'); Np = numel (ps); ## Initialize padj = nan (Np,1); alpha = nan (Np,1); ## Benjamini-Hochberg step-up procedure to control the false discovery rate padj = nan (Np,1); padj(Np) = ps(Np); for j = 1:Np-1 i = Np - j; padj(i) = min (padj(i + 1), Np / i * ps(i)); endfor ## Reorder the adjusted p-values to match the order of the original p-values [~, original_order] = sort (idx, 'ascend'); padj = padj(original_order); ## Truncate adjusted p-values to 1.0 padj(padj>1) = 1; ## If requested, calculate critical value at ALPHA ## No adjustment to confidence interval coverage if (nargout > 1) critval = tinv (1 - ALPHA / 2, dfe); endif endfunction function [padj, critval, dfe] = lsd (p, t, Ng, dfe, R, ALPHA) ## Fisher's Least Significant Difference ## No control of the type I error rate across multiple comparisons padj = p; ## Calculate critical value at ALPHA ## No adjustment to confidence interval coverage critval = tinv (1 - ALPHA / 2, dfe); endfunction %!demo %! %! ## Demonstration using balanced one-way ANOVA from anova1 %! %! rng (42); %! randg ('state', 42); %! x = ones (50, 4) .* [-2, 0, 1, 5]; %! x = x + normrnd (0, 2, 50, 4); %! groups = {'A', 'B', 'C', 'D'}; %! [p, tbl, stats] = anova1 (x, groups, 'off'); %! multcompare (stats); %!demo %! %! ## Demonstration using unbalanced one-way ANOVA example from anovan %! %! dv = [ 8.706 10.362 11.552 6.941 10.983 10.092 6.421 14.943 15.931 ... %! 22.968 18.590 16.567 15.944 21.637 14.492 17.965 18.851 22.891 ... %! 22.028 16.884 17.252 18.325 25.435 19.141 21.238 22.196 18.038 ... %! 22.628 31.163 26.053 24.419 32.145 28.966 30.207 29.142 33.212 ... %! 25.694 ]'; %! g = [1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 3 3 ... %! 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5]'; %! %! [P,ATAB, STATS] = anovan (dv, g, 'varnames', 'score', 'display', 'off'); %! %! [C, M, H, GNAMES] = multcompare (STATS, 'dim', 1, 'ctype', 'holm', ... %! 'ControlGroup', 1, 'display', 'on') %! %!demo %! %! ## Demonstration using factorial ANCOVA example from anovan %! %! score = [95.6 82.2 97.2 96.4 81.4 83.6 89.4 83.8 83.3 85.7 ... %! 97.2 78.2 78.9 91.8 86.9 84.1 88.6 89.8 87.3 85.4 ... %! 81.8 65.8 68.1 70.0 69.9 75.1 72.3 70.9 71.5 72.5 ... %! 84.9 96.1 94.6 82.5 90.7 87.0 86.8 93.3 87.6 92.4 ... %! 100. 80.5 92.9 84.0 88.4 91.1 85.7 91.3 92.3 87.9 ... %! 91.7 88.6 75.8 75.7 75.3 82.4 80.1 86.0 81.8 82.5]'; %! treatment = {'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' ... %! 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' ... %! 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' 'yes' ... %! 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' ... %! 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' ... %! 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no' 'no'}'; %! exercise = {'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' ... %! 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' ... %! 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' ... %! 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' 'lo' ... %! 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' 'mid' ... %! 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi' 'hi'}'; %! age = [59 65 70 66 61 65 57 61 58 55 62 61 60 59 55 57 60 63 62 57 ... %! 58 56 57 59 59 60 55 53 55 58 68 62 61 54 59 63 60 67 60 67 ... %! 75 54 57 62 65 60 58 61 65 57 56 58 58 58 52 53 60 62 61 61]'; %! %! [P, ATAB, STATS] = anovan (score, {treatment, exercise, age}, 'model', ... %! [1 0 0; 0 1 0; 0 0 1; 1 1 0], 'continuous', 3, ... %! 'sstype', 'h', 'display', 'off', 'contrasts', ... %! {'simple','poly',''}); %! %! [C, M, H, GNAMES] = multcompare (STATS, 'dim', [1 2], 'ctype', 'holm', ... %! 'display', 'on') %! %!demo %! %! ## Demonstration using one-way ANOVA from anovan, with fit by weighted least %! ## squares to account for heteroskedasticity. %! %! g = [1, 1, 1, 1, 1, 1, 1, 1, ... %! 2, 2, 2, 2, 2, 2, 2, 2, ... %! 3, 3, 3, 3, 3, 3, 3, 3]'; %! %! y = [13, 16, 16, 7, 11, 5, 1, 9, ... %! 10, 25, 66, 43, 47, 56, 6, 39, ... %! 11, 39, 26, 35, 25, 14, 24, 17]'; %! %! [P,ATAB,STATS] = anovan (y, g, 'display', 'off'); %! fitted = STATS.X * STATS.coeffs(:,1); # fitted values %! b = polyfit (fitted, abs (STATS.resid), 1); %! v = polyval (b, fitted); # Variance as a function of the fitted values %! [P,ATAB,STATS] = anovan (y, g, 'weights', v.^-1, 'display', 'off'); %! [C, M] = multcompare (STATS, 'display', 'on', 'ctype', 'mvt') %!demo %! %! ## Demonstration of p-value adjustments to control the false discovery rate %! ## Data from Westfall (1997) JASA. 92(437):299-306 %! %! p = [.005708; .023544; .024193; .044895; ... %! .048805; .221227; .395867; .693051; .775755]; %! %! padj = multcompare (p,'ctype','fdr') %!test %! %! ## Tests using unbalanced one-way ANOVA example from anovan and anova1 %! %! ## Test for anovan - compare pairwise comparisons with matlab for CTYPE "lsd" %! %! dv = [ 8.706 10.362 11.552 6.941 10.983 10.092 6.421 14.943 15.931 ... %! 22.968 18.590 16.567 15.944 21.637 14.492 17.965 18.851 22.891 ... %! 22.028 16.884 17.252 18.325 25.435 19.141 21.238 22.196 18.038 ... %! 22.628 31.163 26.053 24.419 32.145 28.966 30.207 29.142 33.212 ... %! 25.694 ]'; %! g = [1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 3 3 ... %! 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5]'; %! %! [P, ATAB, STATS] = anovan (dv, g, 'varnames', 'score', 'display', 'off'); %! [C, M, H, GNAMES] = multcompare (STATS, 'dim', 1, 'ctype', 'lsd', ... %! 'display', 'off'); %! assert_equal (C(1,6), 2.85812420217898e-05, 1e-09); %! assert_equal (C(2,6), 5.22936741204085e-07, 1e-09); %! assert_equal (C(3,6), 2.12794763209146e-08, 1e-09); %! assert_equal (C(4,6), 7.82091664406946e-15, 1e-09); %! assert_equal (C(5,6), 0.546591417210693, 1e-09); %! assert_equal (C(6,6), 0.0845897945254446, 1e-09); %! assert_equal (C(7,6), 9.47436557975328e-08, 1e-09); %! assert_equal (C(8,6), 0.188873478781067, 1e-09); %! assert_equal (C(9,6), 4.08974010364197e-08, 1e-09); %! assert_equal (C(10,6), 4.44427348175241e-06, 1e-09); %! assert_equal (M(1,1), 10, 1e-09); %! assert_equal (M(2,1), 18, 1e-09); %! assert_equal (M(3,1), 19, 1e-09); %! assert_equal (M(4,1), 21.0001428571429, 1e-09); %! assert_equal (M(5,1), 29.0001111111111, 1e-09); %! assert_equal (M(1,2), 1.0177537954095, 1e-09); %! assert_equal (M(2,2), 1.28736803631001, 1e-09); %! assert_equal (M(3,2), 1.0177537954095, 1e-09); %! assert_equal (M(4,2), 1.0880245732889, 1e-09); %! assert_equal (M(5,2), 0.959547480416536, 1e-09); %! %! ## Compare "fdr" adjusted p-values to those obtained using p.adjust in R %! %! [C, M, H, GNAMES] = multcompare (STATS, 'dim', 1, 'ctype', 'fdr', ... %! 'display', 'off'); %! assert_equal (C(1,6), 4.08303457454140e-05, 1e-09); %! assert_equal (C(2,6), 1.04587348240817e-06, 1e-09); %! assert_equal (C(3,6), 1.06397381604573e-07, 1e-09); %! assert_equal (C(4,6), 7.82091664406946e-14, 1e-09); %! assert_equal (C(5,6), 5.46591417210693e-01, 1e-09); %! assert_equal (C(6,6), 1.05737243156806e-01, 1e-09); %! assert_equal (C(7,6), 2.36859139493832e-07, 1e-09); %! assert_equal (C(8,6), 2.09859420867852e-01, 1e-09); %! assert_equal (C(9,6), 1.36324670121399e-07, 1e-09); %! assert_equal (C(10,6), 7.40712246958735e-06, 1e-09); %! %! ## Compare "hochberg" adjusted p-values to those obtained using p.adjust in R %! %! [C, M, H, GNAMES] = multcompare (STATS, 'dim', 1, 'ctype', 'hochberg', ... %! 'display', 'off'); %! assert_equal (C(1,6), 1.14324968087159e-04, 1e-09); %! assert_equal (C(2,6), 3.13762044722451e-06, 1e-09); %! assert_equal (C(3,6), 1.91515286888231e-07, 1e-09); %! assert_equal (C(4,6), 7.82091664406946e-14, 1e-09); %! assert_equal (C(5,6), 5.46591417210693e-01, 1e-09); %! assert_equal (C(6,6), 2.53769383576334e-01, 1e-09); %! assert_equal (C(7,6), 6.63205590582730e-07, 1e-09); %! assert_equal (C(8,6), 3.77746957562134e-01, 1e-09); %! assert_equal (C(9,6), 3.27179208291358e-07, 1e-09); %! assert_equal (C(10,6), 2.22213674087620e-05, 1e-09); %! %! ## Compare "holm" adjusted p-values to those obtained using p.adjust in R %! %! [C, M, H, GNAMES] = multcompare (STATS, 'dim', 1, 'ctype', 'holm', ... %! 'display', 'off'); %! assert_equal (C(1,6), 1.14324968087159e-04, 1e-09); %! assert_equal (C(2,6), 3.13762044722451e-06, 1e-09); %! assert_equal (C(3,6), 1.91515286888231e-07, 1e-09); %! assert_equal (C(4,6), 7.82091664406946e-14, 1e-09); %! assert_equal (C(5,6), 5.46591417210693e-01, 1e-09); %! assert_equal (C(6,6), 2.53769383576334e-01, 1e-09); %! assert_equal (C(7,6), 6.63205590582730e-07, 1e-09); %! assert_equal (C(8,6), 3.77746957562134e-01, 1e-09); %! assert_equal (C(9,6), 3.27179208291358e-07, 1e-09); %! assert_equal (C(10,6), 2.22213674087620e-05, 1e-09); %! %! ## Compare "scheffe" adjusted p-values to those obtained using 'scheffe' in Matlab %! %! [C, M, H, GNAMES] = multcompare (STATS, 'dim', 1, 'ctype', 'scheffe', ... %! 'display', 'off'); %! assert_equal (C(1,6), 0.00108105386141085, 1e-09); %! assert_equal (C(2,6), 2.7779386789517e-05, 1e-09); %! assert_equal (C(3,6), 1.3599854038198e-06, 1e-09); %! assert_equal (C(4,6), 7.58830197867751e-13, 1e-09); %! assert_equal (C(5,6), 0.984039948220281, 1e-09); %! assert_equal (C(6,6), 0.539077018557706, 1e-09); %! assert_equal (C(7,6), 5.59475764460574e-06, 1e-09); %! assert_equal (C(8,6), 0.771173490574105, 1e-09); %! assert_equal (C(9,6), 2.52838425729905e-06, 1e-09); %! assert_equal (C(10,6), 0.000200719143889168, 1e-09); %! %! ## Compare "bonferroni" adjusted p-values to those obtained using p.adjust in R %! %! [C, M, H, GNAMES] = multcompare (STATS, 'dim', 1, 'ctype', 'bonferroni', ... %! 'display', 'off'); %! assert_equal (C(1,6), 2.85812420217898e-04, 1e-09); %! assert_equal (C(2,6), 5.22936741204085e-06, 1e-09); %! assert_equal (C(3,6), 2.12794763209146e-07, 1e-09); %! assert_equal (C(4,6), 7.82091664406946e-14, 1e-09); %! assert_equal (C(5,6), 1.00000000000000e+00, 1e-09); %! assert_equal (C(6,6), 8.45897945254446e-01, 1e-09); %! assert_equal (C(7,6), 9.47436557975328e-07, 1e-09); %! assert_equal (C(8,6), 1.00000000000000e+00, 1e-09); %! assert_equal (C(9,6), 4.08974010364197e-07, 1e-09); %! assert_equal (C(10,6), 4.44427348175241e-05, 1e-09); %! %! ## Test for anova1 ("equal")- comparison of results from Matlab %! %! [P, ATAB, STATS] = anova1 (dv, g, 'off', 'equal'); %! [C, M, H, GNAMES] = multcompare (STATS, 'ctype', 'lsd', 'display', 'off'); %! assert_equal (C(1,6), 2.85812420217898e-05, 1e-09); %! assert_equal (C(2,6), 5.22936741204085e-07, 1e-09); %! assert_equal (C(3,6), 2.12794763209146e-08, 1e-09); %! assert_equal (C(4,6), 7.82091664406946e-15, 1e-09); %! assert_equal (C(5,6), 0.546591417210693, 1e-09); %! assert_equal (C(6,6), 0.0845897945254446, 1e-09); %! assert_equal (C(7,6), 9.47436557975328e-08, 1e-09); %! assert_equal (C(8,6), 0.188873478781067, 1e-09); %! assert_equal (C(9,6), 4.08974010364197e-08, 1e-09); %! assert_equal (C(10,6), 4.44427348175241e-06, 1e-09); %! assert_equal (M(1,1), 10, 1e-09); %! assert_equal (M(2,1), 18, 1e-09); %! assert_equal (M(3,1), 19, 1e-09); %! assert_equal (M(4,1), 21.0001428571429, 1e-09); %! assert_equal (M(5,1), 29.0001111111111, 1e-09); %! assert_equal (M(1,2), 1.0177537954095, 1e-09); %! assert_equal (M(2,2), 1.28736803631001, 1e-09); %! assert_equal (M(3,2), 1.0177537954095, 1e-09); %! assert_equal (M(4,2), 1.0880245732889, 1e-09); %! assert_equal (M(5,2), 0.959547480416536, 1e-09); %! %! ## Test for anova1 ("unequal") - comparison with results from GraphPad Prism 8 %! [P, ATAB, STATS] = anova1 (dv, g, 'off', 'unequal'); %! [C, M, H, GNAMES] = multcompare (STATS, 'ctype', 'lsd', 'display', 'off'); %! assert_equal (C(1,6), 0.001247025266382, 1e-09); %! assert_equal (C(2,6), 0.000018037115146, 1e-09); %! assert_equal (C(3,6), 0.000002974595187, 1e-09); %! assert_equal (C(4,6), 0.000000000786046, 1e-09); %! assert_equal (C(5,6), 0.5693192886650109, 1e-09); %! assert_equal (C(6,6), 0.110501699029776, 1e-09); %! assert_equal (C(7,6), 0.000131226488700, 1e-09); %! assert_equal (C(8,6), 0.1912101409715992, 1e-09); %! assert_equal (C(9,6), 0.000005385256394, 1e-09); %! assert_equal (C(10,6), 0.000074089106171, 1e-09); %!test %! %! ## Test for anova2 ("interaction") - comparison with results from Matlab for column effect %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! [P, ATAB, STATS] = anova2 (popcorn, 3, 'off'); %! [C, M, H, GNAMES] = multcompare (STATS, 'estimate', 'column',... %! 'ctype', 'lsd', 'display', 'off'); %! assert_equal (C(1,6), 1.49311100811177e-05, 1e-09); %! assert_equal (C(2,6), 2.20506904243535e-07, 1e-09); %! assert_equal (C(3,6), 0.00449897860490058, 1e-09); %! assert_equal (M(1,1), 6.25, 1e-09); %! assert_equal (M(2,1), 4.75, 1e-09); %! assert_equal (M(3,1), 4, 1e-09); %! assert_equal (M(1,2), 0.152145154862547, 1e-09); %! assert_equal (M(2,2), 0.152145154862547, 1e-09); %! assert_equal (M(3,2), 0.152145154862547, 1e-09); %!test %! %! ## Test for anova2 ("linear") - comparison with results from GraphPad Prism 8 %! words = [10 13 13; 6 8 8; 11 14 14; 22 23 25; 16 18 20; ... %! 15 17 17; 1 1 4; 12 15 17; 9 12 12; 8 9 12]; %! [P, ATAB, STATS] = anova2 (words, 1, 'off', 'linear'); %! [C, M, H, GNAMES] = multcompare (STATS, 'estimate', 'column',... %! 'ctype', 'lsd', 'display', 'off'); %! assert_equal (C(1,6), 0.000020799832702, 1e-09); %! assert_equal (C(2,6), 0.000000035812410, 1e-09); %! assert_equal (C(3,6), 0.003038942449215, 1e-09); %!test %! %! ## Test for anova2 ("nested") - comparison with results from GraphPad Prism 8 %! data = [4.5924 7.3809 21.322; -0.5488 9.2085 25.0426; ... %! 6.1605 13.1147 22.66; 2.3374 15.2654 24.1283; ... %! 5.1873 12.4188 16.5927; 3.3579 14.3951 10.2129; ... %! 6.3092 8.5986 9.8934; 3.2831 3.4945 10.0203]; %! [P, ATAB, STATS] = anova2 (data, 4, 'off', 'nested'); %! [C, M, H, GNAMES] = multcompare (STATS, 'estimate', 'column',... %! 'ctype', 'lsd', 'display', 'off'); %! assert_equal (C(1,6), 0.261031111511073, 1e-09); %! assert_equal (C(2,6), 0.065879755907745, 1e-09); %! assert_equal (C(3,6), 0.241874613529270, 1e-09); %!shared visibility_setting %! visibility_setting = get (0, 'DefaultFigureVisible'); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! %! ## Test for kruskalwallis - comparison with results from MATLAB %! data = [3,2,4; 5,4,4; 4,2,4; 4,2,4; 4,1,5; ... %! 4,2,3; 4,3,5; 4,2,4; 5,2,4; 5,3,3]; %! group = [1:3] .* ones (10,3); %! [P, ATAB, STATS] = kruskalwallis (data(:), group(:), 'off'); %! C = multcompare (STATS, 'ctype', 'lsd', 'display', 'off'); %! assert_equal (C(1,6), 0.000163089828959986, 1e-09); %! assert_equal (C(2,6), 0.630298044801257, 1e-09); %! assert_equal (C(3,6), 0.00100567660695682, 1e-09); %! C = multcompare (STATS, 'ctype', 'bonferroni', 'display', 'off'); %! assert_equal (C(1,6), 0.000489269486879958, 1e-09); %! assert_equal (C(2,6), 1, 1e-09); %! assert_equal (C(3,6), 0.00301702982087047, 1e-09); %! C = multcompare (STATS, 'ctype', 'scheffe', 'display', 'off'); %! assert_equal (C(1,6), 0.000819054880289573, 1e-09); %! assert_equal (C(2,6), 0.890628039849261, 1e-09); %! assert_equal (C(3,6), 0.00447816059021654, 1e-09); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! ## Test for friedman - comparison with results from MATLAB %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! [P, ATAB, STATS] = friedman (popcorn, 3, 'off'); %! C = multcompare (STATS, 'ctype', 'lsd', 'display', 'off'); %! assert_equal (C(1,6), 0.227424558028569, 1e-09); %! assert_equal (C(2,6), 0.0327204848315735, 1e-09); %! assert_equal (C(3,6), 0.353160353315988, 1e-09); %! C = multcompare (STATS, 'ctype', 'bonferroni', 'display', 'off'); %! assert_equal (C(1,6), 0.682273674085708, 1e-09); %! assert_equal (C(2,6), 0.0981614544947206, 1e-09); %! assert_equal (C(3,6), 1, 1e-09); %! C = multcompare (STATS, 'ctype', 'scheffe', 'display', 'off'); %! assert_equal (C(1,6), 0.482657360384373, 1e-09); %! assert_equal (C(2,6), 0.102266573027672, 1e-09); %! assert_equal (C(3,6), 0.649836502233148, 1e-09); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! ## Test for anovan with 'simple' contrasts - same comparisons as for first anovan example %! y = [ 8.706 10.362 11.552 6.941 10.983 10.092 6.421 14.943 15.931 ... %! 22.968 18.590 16.567 15.944 21.637 14.492 17.965 18.851 22.891 ... %! 22.028 16.884 17.252 18.325 25.435 19.141 21.238 22.196 18.038 ... %! 22.628 31.163 26.053 24.419 32.145 28.966 30.207 29.142 33.212 ... %! 25.694 ]'; %! X = [1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5]'; %! [P, ATAB, STATS] = anovan (y, {X}, 'contrasts', 'simple', 'display', 'off'); %! [C, M] = multcompare (STATS, 'ctype', 'lsd', 'display', 'off'); %! assert_equal (C(1,6), 2.85812420217898e-05, 1e-09); %! assert_equal (C(2,6), 5.22936741204085e-07, 1e-09); %! assert_equal (C(3,6), 2.12794763209146e-08, 1e-09); %! assert_equal (C(4,6), 7.82091664406946e-15, 1e-09); %! assert_equal (C(5,6), 0.546591417210693, 1e-09); %! assert_equal (C(6,6), 0.0845897945254446, 1e-09); %! assert_equal (C(7,6), 9.47436557975328e-08, 1e-09); %! assert_equal (C(8,6), 0.188873478781067, 1e-09); %! assert_equal (C(9,6), 4.08974010364197e-08, 1e-09); %! assert_equal (C(10,6), 4.44427348175241e-06, 1e-09); %! assert_equal (M(1,1), 10, 1e-09); %! assert_equal (M(2,1), 18, 1e-09); %! assert_equal (M(3,1), 19, 1e-09); %! assert_equal (M(4,1), 21.0001428571429, 1e-09); %! assert_equal (M(5,1), 29.0001111111111, 1e-09); %! assert_equal (M(1,2), 1.0177537954095, 1e-09); %! assert_equal (M(2,2), 1.28736803631001, 1e-09); %! assert_equal (M(3,2), 1.0177537954095, 1e-09); %! assert_equal (M(4,2), 1.0880245732889, 1e-09); %! assert_equal (M(5,2), 0.959547480416536, 1e-09); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! ## Test p-value adjustments compared to R stats package function p.adjust %! ## Data from Westfall (1997) JASA. 92(437):299-306 %! p = [.005708; .023544; .024193; .044895; ... %! .048805; .221227; .395867; .693051; .775755]; %! padj = multcompare (p); %! assert_equal (padj(1), 0.051372, 1e-06); %! assert_equal (padj(2), 0.188352, 1e-06); %! assert_equal (padj(3), 0.188352, 1e-06); %! assert_equal (padj(4), 0.269370, 1e-06); %! assert_equal (padj(5), 0.269370, 1e-06); %! assert_equal (padj(6), 0.884908, 1e-06); %! assert_equal (padj(7), 1.000000, 1e-06); %! assert_equal (padj(8), 1.000000, 1e-06); %! assert_equal (padj(9), 1.000000, 1e-06); %! padj = multcompare (p,'ctype','holm'); %! assert_equal (padj(1), 0.051372, 1e-06); %! assert_equal (padj(2), 0.188352, 1e-06); %! assert_equal (padj(3), 0.188352, 1e-06); %! assert_equal (padj(4), 0.269370, 1e-06); %! assert_equal (padj(5), 0.269370, 1e-06); %! assert_equal (padj(6), 0.884908, 1e-06); %! assert_equal (padj(7), 1.000000, 1e-06); %! assert_equal (padj(8), 1.000000, 1e-06); %! assert_equal (padj(9), 1.000000, 1e-06); %! padj = multcompare (p,'ctype','hochberg'); %! assert_equal (padj(1), 0.051372, 1e-06); %! assert_equal (padj(2), 0.169351, 1e-06); %! assert_equal (padj(3), 0.169351, 1e-06); %! assert_equal (padj(4), 0.244025, 1e-06); %! assert_equal (padj(5), 0.244025, 1e-06); %! assert_equal (padj(6), 0.775755, 1e-06); %! assert_equal (padj(7), 0.775755, 1e-06); %! assert_equal (padj(8), 0.775755, 1e-06); %! assert_equal (padj(9), 0.775755, 1e-06); %! padj = multcompare (p,'ctype','fdr'); %! assert_equal (padj(1), 0.0513720, 1e-07); %! assert_equal (padj(2), 0.0725790, 1e-07); %! assert_equal (padj(3), 0.0725790, 1e-07); %! assert_equal (padj(4), 0.0878490, 1e-07); %! assert_equal (padj(5), 0.0878490, 1e-07); %! assert_equal (padj(6), 0.3318405, 1e-07); %! assert_equal (padj(7), 0.5089719, 1e-07); %! assert_equal (padj(8), 0.7757550, 1e-07); %! assert_equal (padj(9), 0.7757550, 1e-07); %!test %! ## Below the resolution of 1 - tcdf, values from MATLAB R2024a %! [~, ~, stats] = anova1 ([1:10, 101:110, 201:210]', ... %! kron ((1:3)', ones (10, 1)), 'off'); %! C = multcompare (stats, 'CriticalValueType', 'lsd', 'Display', 'off'); %! assert_equal (C(:,6), [1.07511774090732e-32; 8.40615185462431e-41; ... %! 1.07511774090732e-32], -1e-13); %!test %! ## Scheffe below the resolution of 1 - fcdf, values from MATLAB R2024a %! w = [1:10, 101:110, 201:210]'; %! g = kron ((1:3)', ones (10, 1)); %! [~, ~, st] = anova1 (w, g, 'off'); %! C = multcompare (st, 'CriticalValueType', 'scheffe', 'Display', 'off'); %! assert_equal (C(:,6), [7.05050215176788e-32; 5.522164708181e-40; ... %! 7.05050215176788e-32], -1e-10); statistics-release-2.1.2/inst/Hypothesis_Testing/private/000077500000000000000000000000001527311104300236045ustar00rootroot00000000000000statistics-release-2.1.2/inst/Hypothesis_Testing/private/__bootci__.m000066400000000000000000000140621527311104300260400ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{ci} =} __bootci__ (@var{stat}, @var{S}, @var{alpha}, @var{B}, @var{scheme}) ## ## Bias-corrected and accelerated (BCa) bootstrap confidence interval of a ## statistic of one or more samples. ## ## @var{S} is a cell array of column vectors, the samples, and @var{stat} a ## function handle taking them as separate arguments, @code{@var{stat} ## (@var{S}@{:@})}. Each argument it is given may be a matrix whose columns are ## resampled copies of that sample, and it returns a row vector holding the ## statistic of each column. @var{alpha} sets the level of the two-sided ## interval, @math{100 (1 - @var{alpha})} percent, and @var{B} is the number of ## replicates. @var{scheme} says how the samples are resampled: ## ## @table @asis ## @item @qcode{'rows'} ## The samples have the same length and are resampled by the same indices, as ## the rows of a matrix: one sample, paired samples, or the blocks of a ## design. ## @item @qcode{'strata'} ## Each sample is resampled on its own, keeping its size. ## @item @qcode{'pooled'} ## The observations of all samples are resampled together, each keeping its ## sample, so the sizes vary between replicates; @var{stat} is then given ## column vectors, one replicate at a time. A replicate in which a sample ## comes out empty is left out. ## @end table ## ## The acceleration is estimated by the jackknife: over the rows for ## @qcode{'rows'}, over the pooled observations for @qcode{'pooled'}, and over ## each sample for @qcode{'strata'}, the influence values of each weighted by ## the size of its sample. @var{ci} is @code{[NaN, NaN]} where the statistic ## of the samples or every replicate is @qcode{NaN}. ## ## @end deftypefn function ci = __bootci__ (stat, S, alpha, B, scheme) ns = cellfun (@numel, S(:))'; t0 = stat (S{:}); bs = NaN (B, 1); ## Replicates in blocks of about a million values step = max (1, floor (1e6 / sum (ns))); switch (scheme) case 'rows' n = ns(1); for b1 = 1:step:B b = b1:min (b1 + step - 1, B); I = randi (n, n, numel (b)); R = cellfun (@(v) pick (v, I), S, 'UniformOutput', false); bs(b) = stat (R{:}); endfor u = {jackInfluence(@(i) jackRows (stat, S, i), n)}; case 'strata' for b1 = 1:step:B b = b1:min (b1 + step - 1, B); R = cell (size (S)); for g = 1:numel (S) R{g} = pick (S{g}, randi (ns(g), ns(g), numel (b))); endfor bs(b) = stat (R{:}); endfor ## Jackknife influence values of each sample, weighted by its size u = cell (1, numel (S)); for g = 1:numel (S) u{g} = (ns(g) - 1) * jackInfluence (@(i) jackStratum (stat, S, g, i), ... ns(g)) / ns(g); endfor case 'pooled' v = vertcat (S{:}); g = repelem ((1:numel (S))', ns(:)); n = numel (v); for b1 = 1:step:B b = b1:min (b1 + step - 1, B); I = randi (n, n, numel (b)); for k = 1:numel (b) gb = g(I(:,k)); if (all (accumarray (gb, 1, [numel(S), 1]) > 0)) bs(b(k)) = stat (splitPooled (v(I(:,k)), gb, numel (S)){:}); endif endfor endfor jk = NaN (n, 1); for i = 1:n k = [1:i-1, i+1:n]'; if (all (accumarray (g(k), 1, [numel(S), 1]) > 0)) jk(i) = stat (splitPooled (v(k), g(k), numel (S)){:}); endif endfor jk = jk(! isnan (jk)); u = {mean(jk) - jk}; endswitch bs = bs(! isnan (bs)); if (isempty (bs) || isnan (t0)) ci = [NaN, NaN]; return; endif ## Bias correction and acceleration z0 = norminv (mean (bs < t0) + mean (bs == t0) / 2); u = vertcat (u{:}); u = u(isfinite (u)); den = sum (u .^ 2); if (den > 0) a = sum (u .^ 3) / (6 * den ^ 1.5); else a = 0; endif zq = norminv ([alpha / 2, 1 - alpha / 2]); p = normcdf (z0 + (z0 + zq) ./ (1 - a * (z0 + zq))); ci = quantile (bs, p(:), 1, 5)'; endfunction ## The elements of the vector V at the indices I, in the shape of I function out = pick (v, I) out = reshape (v(I), size (I)); endfunction ## The pooled values V split back into the NS samples their labels G name function out = splitPooled (v, g, ns) out = cell (1, ns); for k = 1:ns out{k} = v(g == k); endfor endfunction ## The statistic with every sample's rows left out as the columns of I say function out = jackRows (stat, S, I) R = cellfun (@(v) pick (v, I), S, 'UniformOutput', false); out = stat (R{:}); endfunction ## The statistic with sample G left out as the columns of I say, and every ## other sample whole function out = jackStratum (stat, S, g, I) R = cellfun (@(v) repmat (v, 1, columns (I)), S, 'UniformOutput', false); R{g} = pick (S{g}, I); out = stat (R{:}); endfunction ## The jackknife deviations mean (jk) - jk of a statistic over N ## observations, FCN taking a matrix of indices whose columns each leave one ## observation out, in blocks function u = jackInfluence (fcn, n) jk = zeros (n, 1); step = max (1, floor (1e6 / n)); for i1 = 1:step:n i = i1:min (i1 + step - 1, n); ## Column k holds 1:n without i(k) K = repmat ((1:n)', 1, numel (i)); K(sub2ind (size (K), i, 1:numel (i))) = 0; K = reshape (K(K > 0), n - 1, numel (i)); jk(i) = fcn (K); endfor u = mean (jk) - jk; endfunction statistics-release-2.1.2/inst/Hypothesis_Testing/private/__ncfbounds__.m000066400000000000000000000046141527311104300265440ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{lambda}, @var{lambdahat}] =} __ncfbounds__ (@var{F}, @var{df1}, @var{df2}, @var{alpha}) ## ## Confidence bounds for the noncentrality of an F statistic, and its unbiased ## estimate. ## ## @var{lambda} holds the noncentralities at which @var{F} falls at the upper ## and at the lower @math{@var{alpha}/2} point of the noncentral F ## distribution with @var{df1} and @var{df2} degrees of freedom; a bound is ## zero where the central distribution already puts @var{F} beyond that point. ## @code{ncfcdf} loses accuracy at large noncentralities, so where the upper ## bound would pass @math{10^5} both bounds are @qcode{NaN}. ## ## @var{lambdahat} is @math{max (F df1 (df2 - 2) / df2 - df1, 0)}, unbiased ## for the noncentrality before the truncation at zero, and @qcode{NaN} where ## @math{df2 <= 2} leaves the mean of @var{F} undefined. ## ## @end deftypefn function [lambda, lambdahat] = __ncfbounds__ (F, df1, df2, alpha) lmax = 1e5; if (df2 > 2) lambdahat = max (F * df1 * (df2 - 2) / df2 - df1, 0); else lambdahat = NaN; endif lambda = [NaN, NaN]; if (! isfinite (F) || lambdahat > lmax) return; endif target = [1 - alpha / 2, alpha / 2]; bounds = [0, 0]; for j = 1:2 if (fcdf (F, df1, df2) > target(j)) hi = max (F * df1, 1); while (ncfcdf (F, df1, df2, hi) > target(j)) hi *= 2; if (hi > 2 * lmax) return; endif endwhile bounds(j) = fzero (@(l) ncfcdf (F, df1, df2, l) - target(j), [0, hi]); if (bounds(j) > lmax) return; endif endif endfor lambda = bounds; endfunction statistics-release-2.1.2/inst/Hypothesis_Testing/private/__ncx2bounds__.m000066400000000000000000000032251527311104300266450ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{lambda} =} __ncx2bounds__ (@var{chisq}, @var{df}, @var{alpha}) ## ## Confidence bounds for the noncentrality of a chi-square statistic. ## ## @var{lambda} holds the noncentralities at which @var{chisq} falls at the ## upper and at the lower @math{@var{alpha}/2} point of the noncentral ## chi-square distribution with @var{df} degrees of freedom; a bound is zero ## where the central distribution already puts @var{chisq} beyond that point. ## ## @end deftypefn function lambda = __ncx2bounds__ (chisq, df, alpha) target = [1 - alpha / 2, alpha / 2]; lambda = [0, 0]; for j = 1:2 if (chi2cdf (chisq, df) > target(j)) hi = max (chisq, 1); while (ncx2cdf (chisq, df, hi) > target(j)) hi *= 2; endwhile lambda(j) = fzero (@(l) ncx2cdf (chisq, df, l) - target(j), [0, hi]); endif endfor endfunction statistics-release-2.1.2/inst/Hypothesis_Testing/private/__twosample__.m000066400000000000000000000206251527311104300265760ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{Z}, @var{iscat}, @var{mx}, @var{my}, @var{errmsg}, @var{names}, @var{labels}] =} __twosample__ (@var{X}, @var{Y}, @var{vnames}, @var{catvars}) ## @deftypefnx {Private Function} {[@dots{}] =} __twosample__ (@dots{}, @var{argnames}) ## ## Read the two samples of @code{knntest} and @code{mmdtest}. ## ## @var{X} and @var{Y} are numeric matrices or tables, @var{vnames} the ## @qcode{'VariableNames'} given and @var{catvars} the ## @qcode{'CategoricalVariables'} given, each empty where not. @var{Z} holds ## the pooled sample, the @var{mx} rows of @var{X} over the @var{my} rows of ## @var{Y}, one column per variable used: a continuous variable as numbers, ## one holding levels as codes @code{1}, @code{2}, @dots{} shared across the ## samples. A row holding a missing value is left out. @var{iscat} says ## which columns hold levels. @var{errmsg} is empty, or the body of the ## message the caller raises under its own name. @var{names} holds the ## variable names of two tables and is empty for matrices, and @var{labels} ## holds, for each variable holding levels, the name of each code in turn. ## @var{argnames}, @code{@{'X', 'Y'@}} by default, names the two samples in ## @var{errmsg}. ## ## @end deftypefn function [Z, iscat, mx, my, errmsg, names, labels] = __twosample__ (X, Y, ... vnames, catvars, argnames) [Z, iscat, mx, my, errmsg, names, labels] = tsRead (X, Y, vnames, catvars); if (! isempty (errmsg) && nargin > 4) errmsg = regexprep (errmsg, '(?= 1) && all (cv(:) <= K)) iscat(cv(:)') = true; else errmsg = sprintf (strcat ("'CategoricalVariables' must be 'all',", ... " indices from 1 to %d, a logical vector", ... " or variable names."), K); endif endfunction ## The columns with every observation holding a missing value removed. function [cols, n] = tsComplete (cols) keep = true (rows (cols{1}), 1); for j = 1:numel (cols) keep &= ! ismissing (cols{j}); endfor for j = 1:numel (cols) cols{j} = cols{j}(keep); endfor n = sum (keep); endfunction statistics-release-2.1.2/inst/Hypothesis_Testing/private/exact2xkCT.m000066400000000000000000000170261527311104300257500ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{p_net}, @var{p_val}] =} exact2xkCT (@var{ct}, @var{weights}, @var{rsstat}) ## ## Compute the exact p-value for a 2-by-K contingency table based on the ## network algorithm. ## ## Reference: Cyrus R. Mehta & Nitin R. Patel (1980) A network algorithm for ## the exact treatment of the 2×k contingency table, Communications in ## Statistics - Simulation and Computation, 9:6, 649-664, ## DOI: 10.1080/03610918008812182 ## ## @end deftypefn function [p_net, p_val] = exact2xkCT (ct, weights, rsstat) ## Calculate nodes and arcs [nodes, arcs] = build_nodes (ct,weights); ## Apply backward induction to nodes nodes = backward_induce (nodes,arcs); ## Forward scan the network to get p-values p_val = forward_scan (nodes, arcs, rsstat); ## Calculate p-values TP = nodes{4,1}; p_val = p_val / TP; p_net = p_val(2) + min (p_val(1), p_val(3)); endfunction ## Calculate structures describing nodes and arcs function [nodes, arcs] = build_nodes (ct, weights) column = size (ct, 2); ## number of columns in contingency table rowsum = sum (ct, 2); ## sum of rows colsum = sum (ct, 1); ## sum of columns oldnodes = zeros (1,2); ## nodes added during last pass oldlo = 0; ## min possible sum so far oldhi = 0; ## max possible sum so far oldnn = 1; ## node numbers (row numbers) from last pass ctsum = rowsum(1); ## sum of entries in first row nodecount = 1; ## current node count ## Initialize cell structures for nodes and arcs nodes = cell (4, column+1); ## to hold nodes nodes{1,1} = zeros (1,2); ## n-by-2 array, n = # of nodes, row = [j,mj] nodes{2,column+1} = 0; ## n-vector of longest path to end from here nodes{3,column+1} = 0; ## n-vector of shortest path to end from here nodes{4,column+1} = 1; ## n-vector of total probability to end from here arcs = cell (3, column); ## to hold arcs ## row 1: n-by-2 array, n = # of connections, row = pair connected ## row 2: n-vector of arc lengths ## row 3: n-vector of arc probabilities for j = 1:column ## Find nodes possible at the next step nj = colsum(j); lo = max (oldlo, ctsum - sum (colsum(j+1:end))); hi = min (ctsum, oldhi + nj); newnodes = zeros (hi - lo + 1,2); newnodes(:,1) = j; newnodes(:,2) = (lo:hi)'; newnn = 1:size (newnodes,1); nodecount = nodecount + size (newnodes, 1); nodes{1,j+1} = newnodes; ## Find arcs possible to the next step [a0, a1] = meshgrid (oldnn, newnn); a0 = a0(:); a1 = a1(:); oldsum = oldnodes(a0,2); newsum = newnodes(a1,2); xj = newsum - oldsum; ok = (xj >= 0) & (xj <= nj); arcs{1,j} = [a0(ok) a1(ok)]; ## arc connections xj = xj(ok); arcs{2,j} = weights(j) * xj; pj = exp (gammaln (nj + 1) - gammaln (xj + 1) - gammaln (nj - xj + 1)); arcs{3,j} = pj; ## arc probabilities ## Update data structures oldlo = lo; oldhi = hi; oldnodes = newnodes; oldnn = newnn; endfor endfunction ## Calculate backward induction by adding information to NODES array function nodes = backward_induce (nodes, arcs) ## initialize for final node column = size (nodes,2) - 1; startSP = zeros (1); startLP = startSP; startTP = ones (1); for j = column:-1:1 ## destination nodes are previous start nodes endSP = startSP; endLP = startLP; endTP = startTP; ## get new start nodes and information about them a = arcs{1,j}; startmax = max (a(:,1)); startSP = zeros (startmax,1); startLP = startSP; startTP = startSP; arclen = arcs{2,j}; arcprob = arcs{3,j}; for nodenum = 1:startmax ## for each start node, compute SP, LP, TP k1 = find (a(:,1) == nodenum); k2 = a(k1,2); startLP(nodenum) = max (arclen(k1) + endLP(k2)); startSP(nodenum) = min (arclen(k1) + endSP(k2)); startTP(nodenum) = sum (arcprob(k1) .* endTP(k2)); endfor ## store information about nodes at this level nodes{2,j} = startLP; nodes{3,j} = startSP; nodes{4,j} = startTP; endfor endfunction ## Get p-values by forward scanning the network function p_val = forward_scan (nodes, arcs, rsstat) NROWS = 50; p_val = zeros (3,1); ## [ProbT] stack = zeros (NROWS, 4); stack(:,1) = Inf; stack(1,1) = 1; ## level of current node stack(1,2) = 1; ## number at this level of current node stack(1,3) = 0; ## length so far to this node stack(1,4) = 1; ## probability so far of reaching this node N = size (stack, 1); i1 = 0; i2 = 0; i3 = 0; while (1) ## Get next lowest level node to process minlevel = min (stack((stack(1:N)>0))); if (isinf (minlevel)) break; endif sp = find (stack(1:N) == minlevel); sp = sp(1); L = stack(sp,1); J = stack(sp,2); pastL = stack(sp,3); pastP = stack(sp,4); stack(sp,1) = Inf; ## Get info for arcs at level L and their target nodes LP = nodes{2,L+1}; SP = nodes{3,L+1}; TP = nodes{4,L+1}; aj = arcs{1,L}; arclen = arcs{2,L}; arcprob = arcs{3,L}; ## Look only at arcs from node J seps = sqrt (eps); arows = find (aj(:,1) == J)'; for k = arows tonode = aj(k,2); thisL = arclen(k); thisP = pastP * arcprob(k); len = pastL + thisL; ## No paths from node J are significant if (len + LP(tonode) < rsstat - seps) p_val(1) = p_val(1) + thisP * TP(tonode); ## All paths from node J are significant elseif (len + SP(tonode) > rsstat + seps) p_val(3) = p_val(3) + thisP * TP(tonode); ## Single match from node J elseif (SP(tonode) == LP(tonode)) p_val(2) = p_val(2) + thisP * TP(tonode); ## Match node J with another already stored node else ## Find a stored node that matches this one r = find (stack(:,1) == L+1); if (any (r)) r = r(stack(r,2) == tonode); if (any (r)) r = r(abs (stack(r,3) - len) < seps); endif endif ## If any one is found, merge node J with it if (any (r)) sp = r(1); stack(sp,4) = stack(sp,4) + thisP; i1 = i1 + 1; ## Otherwise add a new node else z = find (isinf (stack(:,1))); if (isempty (z)) i2 = i2 +1; block = zeros (NROWS, 4); block(:,1) = Inf; stack = [stack; block]; sp = N + 1; N = N + NROWS; else i3 = i3 + 1; sp = z(1); endif stack(sp,1) = L + 1; stack(sp,2) = tonode; stack(sp,3) = len; stack(sp,4) = thisP; endif endif endfor endwhile endfunction statistics-release-2.1.2/inst/Hypothesis_Testing/ranksum.m000066400000000000000000000411121527311104300237670ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{p} =} ranksum (@var{x}, @var{y}) ## @deftypefnx {statistics} {@var{p} =} ranksum (@var{x}, @var{y}, @var{alpha}) ## @deftypefnx {statistics} {@var{p} =} ranksum (@var{x}, @var{y}, @var{alpha}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {@var{p} =} ranksum (@var{x}, @var{y}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{p}, @var{h}] =} ranksum (@var{x}, @var{y}, @dots{}) ## @deftypefnx {statistics} {[@var{p}, @var{h}, @var{stats}] =} ranksum (@var{x}, @var{y}, @dots{}) ## ## Wilcoxon rank sum test for equal medians. This test is equivalent to a ## Mann-Whitney U-test. ## ## @code{@var{p} = ranksum (@var{x}, @var{y})} returns the p-value of a ## two-sided Wilcoxon rank sum test. It tests the null hypothesis that two ## independent samples, in the vectors X and Y, come from continuous ## distributions with equal medians, against the alternative hypothesis that ## they are not. @var{x} and @var{y} can have different lengths and the test ## assumes that they are independent. ## ## @code{ranksum} treats NaN in @var{x}, @var{y} as missing values. ## The two-sided p-value is computed by doubling the most significant one-sided ## value. ## ## @code{[@var{p}, @var{h}] = ranksum (@var{x}, @var{y})} also returns the ## result of the hypothesis test with @code{@var{h} = 1} indicating a rejection ## of the null hypothesis at the default alpha = 0.05 significance level, and ## @code{@var{h} = 0} indicating a failure to reject the null hypothesis at the ## same significance level. ## ## @code{[@var{p}, @var{h}, @var{stats}] = ranksum (@var{x}, @var{y})} also ## returns the structure @var{stats} with information about the test statistic. ## It contains the field @code{ranksum} with the value of the rank sum test ## statistic, and the field @code{zval} with the value of the z-statistic when ## computed with the "approximate" method, and empty otherwise. The field is ## always there. Two further fields, an Octave extension, hold the effect ## size: @code{RankBiserial}, the rank-biserial correlation, which is Cliff's ## delta, the share of pairs @math{(x_i, y_j)} with @math{x_i > y_j} less the ## share with @math{x_i < y_j}, and @code{RankBiserialCI}, its two-sided ## confidence interval at the level @math{100 (1 - alpha)} percent, whatever ## the tail. Both are those of @code{meanEffectSize (@var{x}, @var{y}, ## "Effect", "cliff")}: by default the interval is Cliff's, and the options ## @qcode{"ConfidenceIntervalType"}, @qcode{"bootstrap"} for a BCa bootstrap ## interval, @qcode{"NumBootstraps"} and @qcode{"Resampling"} are passed on to ## @code{meanEffectSize}. ## ## @code{[@dots{}] = ranksum (@var{x}, @var{y}, @var{alpha})} or alternatively ## @code{[@dots{}] = ranksum (@var{x}, @var{y}, "alpha", @var{alpha})} returns ## the result of the hypothesis test performed at the significance level ALPHA. ## ## @code{[@dots{}] = ranksum (@var{x}, @var{y}, "method", @var{M})} defines the ## computation method of the p-value specified in @var{M}, which can be "exact", ## "approximate", or "oldexact". @var{M} must be a single string. When "method" ## is unspecified, the default is: "exact" when ## @code{min (length (@var{x}), length (@var{y})) < 10} and ## @code{length (@var{x}) + length (@var{y}) < 20}, otherwise the "approximate" ## method is used. ## ## @itemize ## @item ## "exact" method uses full enumeration for small total sample size (< 10), ## otherwise the network algorithm is used for larger samples. ## @item ## "approximate" uses normal approximation method for computing the p-value. ## @item ## "oldexact" uses full enumeration for any sample size. Note, that this option ## can lead to out of memory error for large samples. Use with caution! ## @end itemize ## ## @code{[@dots{}] = ranksum (@var{x}, @var{y}, "tail", @var{tail})} defines the ## type of test, which can be "both", "right", or "left". @var{tail} must be a ## single string. ## ## @itemize ## @item ## "both" -- "medians are not equal" (two-tailed test, default) ## @item ## "right" -- "median of X is greater than median of Y" (right-tailed test) ## @item ## "left" -- "median of X is less than median of Y" (left-tailed test) ## @end itemize ## ## Note: the rank sum statistic is based on the smaller sample of vectors ## @var{x} and @var{y}. ## ## @seealso{signrank, kruskalwallis, meanEffectSize} ## @end deftypefn function [p, h, stats] = ranksum(x, y, varargin) ## Check that x and y are vectors if ! isvector (x) || ! isvector (y) error ("ranksum: X and Y must be vectors."); endif ## Remove missing data and make column vectors x = x(! isnan (x))(:); y = y(! isnan (y))(:); if isempty (x) error ("ranksum: X holds no observation that is not missing."); endif if isempty (y) error ("ranksum: Y holds no observation that is not missing."); endif ## Check for extra input arguments alpha = 0.05; method = []; tail = 'both'; citype = 'exact'; nboot = 1000; resampling = 'pooled'; ## Old syntax: ranksum (x, y, alpha) if nargin > 2 && isnumeric (varargin{1}) && isscalar (varargin{1}) alpha = varargin{1}; varargin(1) = []; if isnan (alpha) || alpha <= 0 || alpha >= 1 error ("ranksum: ALPHA must be a scalar between 0 and 1."); endif endif ## Check for Name:Value pairs arg_pairs = length (varargin); if ! (int16 (arg_pairs / 2) == arg_pairs / 2) error ("ranksum: optional arguments must be in Name, Value pairs."); endif num_pair = 1; while (arg_pairs) name = varargin{num_pair}; value = varargin{num_pair + 1}; switch (lower (name)) case 'alpha' alpha = value; if (! isnumeric (alpha) || ! isscalar (alpha) || isnan (alpha) || alpha <= 0 || alpha >= 1) error ("ranksum: 'alpha' must be a scalar between 0 and 1."); endif case 'method' method = value; if ! any (strcmpi (method, {'exact', 'approximate', 'oldexact'})) error (strcat ("ranksum: 'method' must be 'exact',", ... " 'approximate' or 'oldexact'.")); endif case 'tail' tail = value; if ! any (strcmpi (tail, {'both', 'right', 'left'})) error ("ranksum: 'tail' must be 'both', 'right' or 'left'."); endif case 'confidenceintervaltype' citype = value; if (! any (strcmpi (citype, {'exact', 'bootstrap'}))) error (strcat ("ranksum: 'ConfidenceIntervalType' must be", ... " 'exact' or 'bootstrap'.")); endif case 'numbootstraps' nboot = value; if (! (isnumeric (nboot) && isreal (nboot) && isscalar (nboot) && isfinite (nboot) && nboot >= 1 && nboot == fix (nboot))) error ("ranksum: 'NumBootstraps' must be a positive integer."); endif case 'resampling' resampling = value; if (! any (strcmpi (resampling, {'pooled', 'stratified'}))) error ("ranksum: 'Resampling' must be 'pooled' or 'stratified'."); endif otherwise error ("ranksum: invalid optional paired argument."); endswitch arg_pairs -= 2; num_pair += 2; endwhile ## Determine method nx = length (x); ny = length (y); ns = min (nx, ny); if isempty (method) if (ns < 10) && ((nx + ny) < 20) method = 'exact'; else method = 'approximate'; endif endif ## Determine computational technique switch method case 'approximate' technique = 'approximation'; case 'oldexact' technique = 'exact'; case 'exact' if (nx + ny) < 10 technique = 'exact'; else technique = 'network_algorithm'; endif endswitch ## No z-statistic exists for the exact test, so the field is reported empty ## rather than omitted, as R2026a does. zval = []; ## Compute the rank sum statistic based on the smaller sample if nx <= ny [ranks, tieadj] = tiedrank ([x; y]); x_y = true; else [ranks, tieadj] = tiedrank ([y; x]); x_y = false; endif srank = ranks(1:ns); ranksumstat = sum (srank); ## Calculate p-value according to selected technique switch technique case 'exact' allpos = nchoosek (ranks, ns); sumranks = sum (allpos, 2); np = size (sumranks, 1); switch tail case 'both' p_low = sum (sumranks <= ranksumstat) / np; p_high = sum (sumranks >= ranksumstat) / np; p = 2 * min (p_low, p_high); if p > 1 p = 1; endif case 'right' if x_y p = sum (sumranks >= ranksumstat) / np; else p = sum (sumranks <= ranksumstat) / np; endif case 'left' if x_y p = sum (sumranks <= ranksumstat) / np; else p = sum (sumranks >= ranksumstat) / np; endif endswitch case 'network_algorithm' ## Calculate contingency table u = unique ([x; y]); ct = zeros (2, length (u)); if x_y ct(1,:) = histc (x,u)'; ct(2,:) = histc (y,u)'; else ct(1,:) = histc (y,u)'; ct(2,:) = histc (x,u)'; endif ## Calculate weights for wmw test colsum = sum (ct,1); tmp = cumsum (colsum); weights = [0 tmp(1:end - 1)] + .5 * (1 + diff ([0 tmp])); ## Compute p-value using network algorithm for contingency tables [p_net, p_val] = exact2xkCT (ct, weights, ranksumstat); ## Check if p = NaN if any (isnan (p_net)) || any (isnan (p_val)) p = NaN; else switch tail case 'both' p = 2 * p_net; if p > 1 p = 1; endif case 'right' if x_y p = p_val(2) + p_val(3); else p = p_val(2) + p_val(1); endif case 'left' if x_y p = p_val(2) + p_val(1); else p = p_val(2) + p_val(3); endif endswitch endif case 'approximation' wmean = ns * (nx + ny + 1) / 2; tiescores = 2 * tieadj / ((nx + ny) * (nx + ny - 1)); wvar = nx * ny * ((nx + ny + 1) - tiescores) / 12; wc = ranksumstat - wmean; ## compute z-value, including continuity correction switch tail case 'both' z = (wc - 0.5 * sign (wc)) / sqrt (wvar); if ! x_y z = -z; endif p = 2 * normcdf (-abs (z)); case 'right' if x_y z = (wc - 0.5) / sqrt (wvar); else z = -(wc + 0.5) / sqrt (wvar); endif p = normcdf (-z); case 'left' if x_y z = (wc + 0.5) / sqrt (wvar); else z = -(wc - 0.5) / sqrt (wvar); endif p = normcdf (z); endswitch zval = z; endswitch ## For additional output arguments if nargout > 1, h = (p <= alpha); if (nargout > 2) if x_y stats.ranksum = ranksumstat; else stats.ranksum = sum (ranks(ns+1:end)); endif stats.zval = zval; ## The rank-biserial correlation is Cliff's delta T = meanEffectSize (x, y, 'Effect', 'cliff', 'Alpha', alpha, ... 'ConfidenceIntervalType', citype, ... 'NumBootstraps', nboot, 'Resampling', resampling); stats.RankBiserial = T.Effect; stats.RankBiserialCI = T.ConfidenceIntervals; endif endif endfunction ## testing against mileage data and results from Matlab %!test %! mileage = [33.3, 34.5, 37.4; 33.4, 34.8, 36.8; ... %! 32.9, 33.8, 37.6; 32.6, 33.4, 36.6; ... %! 32.5, 33.7, 37.0; 33.0, 33.9, 36.7]; %! [p,h,stats] = ranksum (mileage(:,1),mileage(:,2)); %! assert_equal (p, 0.004329004329004329, 1e-14); %! assert_equal (h, true); %! assert_equal (stats.ranksum, 21.5); %!test %! year1 = [51 52 62 62 52 52 51 53 59 63 59 56 63 74 68 86 82 70 69 75 73 ... %! 49 47 50 60 59 60 62 61 71]'; %! year2 = [54 53 64 66 57 53 54 54 62 66 59 59 67 76 75 86 82 67 74 80 75 ... %! 54 50 53 62 62 62 72 60 67]'; %! [p,h,stats] = ranksum (year1, year2, 'alpha', 0.01, 'tail', 'left'); %! assert_equal (p, 0.1270832752950605, 1e-14); %! assert_equal (h, false); %! assert_equal (stats.ranksum, 837.5); %! assert_equal (stats.zval, -1.140287483634606, 1e-14); %! [p,h,stats] = ranksum (year1, year2, 'alpha', 0.01, 'tail', 'left', ... %! 'method', 'exact'); %! assert_equal (p, 0.127343916432862, 1e-14); %! assert_equal (h, false); %! assert_equal (stats.ranksum, 837.5); %!test # zval field always present, empty for the exact method %! x = 1:8; %! y = 9:16; %! [p, h, stats] = ranksum (x, y); %! assert_equal (fieldnames (stats), {'ranksum'; 'zval'; 'RankBiserial'; ... %! 'RankBiserialCI'}); %! assert_equal (isempty (stats.zval), true); %!test # zval is second for the approximate method too %! x = 1:8; %! y = 9:16; %! [p, h, stats] = ranksum (x, y, 'method', 'approximate'); %! assert_equal (fieldnames (stats), {'ranksum'; 'zval'; 'RankBiserial'; ... %! 'RankBiserialCI'}); %! assert_equal (stats.zval, -3.3082, 1e-4); %!test %! ## Effect size against the R package effectsize 1.0.3, rank_biserial %! m1 = [33.3, 33.4, 32.9, 32.6, 32.5, 33.0]; %! m2 = [34.5, 34.8, 33.8, 33.4, 33.7, 33.9]; %! [~, ~, stats] = ranksum (m1, m2); %! assert_equal (stats.RankBiserial, -0.97222222222222232, -1e-14); %! T = meanEffectSize (m1, m2, 'Effect', 'cliff'); %! assert_equal (stats.RankBiserialCI, T.ConfidenceIntervals); %!test %! year1 = [51 52 62 62 52 52 51 53 59 63 59 56 63 74 68 86 82 70 69 75 73 ... %! 49 47 50 60 59 60 62 61 71]'; %! year2 = [54 53 64 66 57 53 54 54 62 66 59 59 67 76 75 86 82 67 74 80 75 ... %! 54 50 53 62 62 62 72 60 67]'; %! [~, ~, stats] = ranksum (year1, year2, 'alpha', 0.1); %! assert_equal (stats.RankBiserial, -0.17222222222222228, -1e-14); %! T = meanEffectSize (year1, year2, 'Effect', 'cliff', 'Alpha', 0.1); %! assert_equal (stats.RankBiserialCI, T.ConfidenceIntervals); %!test %! rand ('state', 1); %! [~, ~, stats] = ranksum ([1, 3, 4, 7, 8], [2, 5, 6, 9, 10, 11], ... %! 'ConfidenceIntervalType', 'bootstrap', ... %! 'NumBootstraps', 200, 'Resampling', 'stratified'); %! ci = stats.RankBiserialCI; %! assert_equal ([ci(1) <= ci(2), ci(1) >= -1, ci(2) <= 1], true (1, 3)); %!error ... %! ranksum ([1, 2, 3], [4, 5, 6], 'ConfidenceIntervalType', 'none') %!error ... %! ranksum ([1, 2, 3], [4, 5, 6], 'NumBootstraps', 0) %!error ... %! ranksum ([1, 2, 3], [4, 5, 6], 'Resampling', 'foo') %!error ranksum ([1, 2; 3, 4], [1, 2, 3]) %!error ... %! ranksum ([NaN, NaN], [1, 2, 3]) %!error ... %! ranksum ([1, 2, 3], NaN) %!error ... %! ranksum ([1, 2, 3], [4, 5, 6], 1.5) %!error ... %! ranksum ([1, 2, 3], [4, 5, 6], 'tail') %!error ... %! ranksum ([1, 2, 3], [4, 5, 6], 'Tial', 'left') %!error ... %! ranksum ([1, 2, 3], [4, 5, 6], 'alpha', 0.1, 5, 'left') %!error ... %! ranksum ([1, 2, 3], [4, 5, 6], 'alpha', 0) %!error ... %! ranksum ([1, 2, 3], [4, 5, 6], 'alpha', [0.1, 0.2]) %!error ... %! ranksum ([1, 2, 3], [4, 5, 6], 'method', 'foo') %!error ... %! ranksum ([1, 2, 3], [4, 5, 6], 'tail', 'up') statistics-release-2.1.2/inst/Hypothesis_Testing/regression_ftest.m000066400000000000000000000317431527311104300257050ustar00rootroot00000000000000## Copyright (C) 1995-2017 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} regression_ftest (@var{y}, @var{x}) ## @deftypefnx {statistics} {@var{h} =} regression_ftest (@var{y}, @var{x}, @var{keep}) ## @deftypefnx {statistics} {@var{h} =} regression_ftest (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{h}, @var{p}, @var{stats}] =} regression_ftest (@dots{}) ## ## F-test for nested linear regression models. ## ## Fit the full linear model of the response @var{y} on every column of ## @var{x}, and a reduced model on the columns of @var{x} indexed by ## @var{keep}, both by least squares, and test the null hypothesis that the ## columns the reduced model leaves out add nothing to it. @var{keep} holds ## column indices or a logical mask; left out or empty, the reduced model has ## no predictors and the test is of every column of @var{x} together. Both ## models include a constant term unless @qcode{'Intercept'} is false. ## @var{y} must be a vector with one element per row of @var{x}, and neither ## may contain missing values. ## ## The test decision is returned in @var{h}, which is 1 when the null ## hypothesis is rejected at the significance level @var{alpha} and 0 ## otherwise, and the p-value in @var{p}. @var{stats} is a structure with the ## following fields: ## ## @multitable @columnfractions 0.2 0.75 ## @item @qcode{fstat} @tab the F statistic. ## @item @qcode{df1} @tab its numerator degrees of freedom, the number of ## coefficients the reduced model leaves out. ## @item @qcode{df2} @tab its denominator degrees of freedom, the residual ## degrees of freedom of the full model. ## @item @qcode{betafull} @tab the coefficients of the full model, the constant ## first where there is one. ## @item @qcode{betareduced} @tab those of the reduced model. ## @item @qcode{ssefull} @tab the residual sum of squares of the full model. ## @item @qcode{ssereduced} @tab that of the reduced model. ## @item @qcode{CohensF2} @tab Cohen's @math{f^2}, the effect size of the ## columns tested. ## @item @qcode{CohensF2CI} @tab its @math{100 (1 - alpha)}% confidence ## interval. ## @end multitable ## ## The degrees of freedom come from the ranks of the two designs. Cohen's ## @math{f^2} is taken, as Cohen defines it, from the noncentrality of the F ## statistic, @math{lambda = f^2 (df1 + df2 + 1)}: the point estimate from ## @math{max (F df1 (df2 - 2) / df2 - df1, 0)}, unbiased for the noncentrality ## before its truncation at zero, and the interval by inverting the noncentral ## F distribution. Where the interval would need a noncentrality above ## @math{10^5}, where @code{ncfcdf} loses accuracy, it is @qcode{NaN}. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @multitable @columnfractions 0.2 0.75 ## @item @qcode{'Alpha'} @tab the significance level of @var{h} and the level ## of the confidence interval. Default is 0.05. ## @item @qcode{'Intercept'} @tab whether both models include a constant term. ## Default is true. ## @end multitable ## ## The same tests, and any linear hypothesis on the coefficients, are available ## from a fitted @code{LinearModel} through its @code{coefTest} method. ## ## @seealso{regression_ttest, regress, fitlm} ## @end deftypefn function [h, p, stats] = regression_ftest (y, x, varargin) if (nargin < 2) print_usage (); endif ## Check for finite real numbers in Y, X if (! all (isfinite (y(:))) || ! isreal (y)) error ("regression_ftest: Y must contain finite real numbers."); endif if (! all (isfinite (x(:))) || ! isreal (x)) error ("regression_ftest: X must contain finite real numbers."); endif [n, v] = size (x); if (! (isvector (y) && numel (y) == n)) error ("regression_ftest: Y must be a vector of length 'rows (X)'."); endif y = y(:); ## The columns of the reduced model, then the options keep = []; if (! isempty (varargin) && ! ischar (varargin{1})) keep = varargin{1}; varargin(1) = []; endif alpha = 0.05; intercept = true; if (mod (numel (varargin), 2) != 0) error ("regression_ftest: optional arguments must be in pairs."); endif for i = 1:2:numel (varargin) name = varargin{i}; value = varargin{i+1}; if (! ischar (name)) error ("regression_ftest: invalid Name argument."); endif switch (lower (name)) case 'alpha' if (! (isnumeric (value) && isscalar (value) && isreal (value) && value > 0 && value < 1)) error ("regression_ftest: invalid value for alpha."); endif alpha = value; case 'intercept' if (! (isscalar (value) && (islogical (value) || isnumeric (value)))) error ("regression_ftest: invalid value for Intercept."); endif intercept = logical (value); otherwise error ("regression_ftest: invalid Name argument."); endswitch endfor ## Resolve KEEP to column indices of a proper subset of X if (islogical (keep)) if (numel (keep) != v) error (strcat ("regression_ftest: a logical KEEP must have one", ... " element per column of X.")); endif keep = find (keep); elseif (! (isempty (keep) || (isnumeric (keep) && isvector (keep) && all (keep == fix (keep)) && all (keep >= 1 & keep <= v) && numel (unique (keep)) == numel (keep)))) error (strcat ("regression_ftest: KEEP must hold distinct column", ... " indices of X.")); endif if (numel (keep) >= v) error ("regression_ftest: KEEP must leave out at least one column of X."); endif ## Fit both models by least squares if (intercept) Xf = [ones(n, 1), x]; Xr = [ones(n, 1), x(:,keep)]; else Xf = x; Xr = x(:,keep); endif bf = Xf \ y; sse_f = sumsq (y - Xf * bf); if (columns (Xr) > 0) br = Xr \ y; sse_r = sumsq (y - Xr * br); else br = zeros (0, 1); sse_r = sumsq (y); endif df1 = rank (Xf) - rank (Xr); df2 = n - rank (Xf); if (df2 < 1) error ("regression_ftest: too few observations for the full model."); endif if (df1 < 1) error (strcat ("regression_ftest: the columns left out add nothing", ... " to the reduced model's design.")); endif stats.fstat = ((sse_r - sse_f) / df1) / (sse_f / df2); stats.df1 = df1; stats.df2 = df2; stats.betafull = bf; stats.betareduced = br; stats.ssefull = sse_f; stats.ssereduced = sse_r; p = fcdf (stats.fstat, df1, df2, 'upper'); h = double (p < alpha); ## Cohen's f^2 from the noncentrality, lambda = f^2 (df1 + df2 + 1) [lambda, lambdahat] = __ncfbounds__ (stats.fstat, df1, df2, alpha); stats.CohensF2 = lambdahat / (df1 + df2 + 1); stats.CohensF2CI = lambda / (df1 + df2 + 1); endfunction %!shared X, y %! X = [1 1; 2 1; 3 2; 4 2; 5 3; 6 3; 7 4; 8 4; 9 5; 10 5; 11 6; 12 6]; %! y = 2 + 3 * X(:,1) - 1.5 * X(:,2) + ... %! [0.2 -0.3 0.1 0.4 -0.2 0.3 -0.1 0.2 -0.4 0.1 0.3 -0.2]'; %!test %! ## Every column together, as R's anova (lm (y ~ 1), lm (y ~ x1 + x2)) %! [h, p, st] = regression_ftest (y, X); %! assert_equal ([h, st.df1, st.df2], [1, 2, 9]); %! assert_equal ([st.fstat, p], ... %! [4553.7218634686296, 2.9845354551931477e-14], -1e-10); %!test %! ## Dropping one column, as R's anova (lm (y ~ x1), lm (y ~ x1 + x2)) %! [~, p, st] = regression_ftest (y, X, 1); %! assert_equal ([st.fstat, p], [27.05295073929755, 0.00056311880681268267], ... %! -1e-10); %!test %! ## Without a constant, as R's anova (lm (y ~ 0 + x1), lm (y ~ 0 + x1 + x2)) %! [~, p, st] = regression_ftest (y, X, 1, 'Intercept', false); %! assert_equal ([st.fstat, p], ... %! [0.0076424950016706168, 0.93206240862541445], -1e-10); %!test %! [~, ~, st] = regression_ftest (y, X, 1, 'Intercept', false); %! assert_equal ([st.df1, st.df2], [1, 10]); %!test %! [~, pa] = regression_ftest (y, X, [true, false]); %! [~, pb] = regression_ftest (y, X, 1); %! assert_equal (pa, pb); %!test %! [~, ~, st] = regression_ftest (y, X, 1); %! assert_equal (st.betafull, [ones(12, 1), X] \ y, -1e-12); %!test %! [~, ~, st] = regression_ftest (y, X, 1); %! assert_equal (st.betareduced, [ones(12, 1), X(:,1)] \ y, -1e-12); %!test %! [~, ~, st] = regression_ftest (y, X, 1); %! assert_equal (st.ssefull, sumsq (y - [ones(12, 1), X] * st.betafull), ... %! -1e-12); %!test %! ## Cohen's f^2 with lambda = f^2 (df1 + df2 + 1); the bounds are R's pf %! ## with ncp inverted by uniroot %! [~, ~, st] = regression_ftest (y, X, 1); %! assert_equal (st.CohensF2, 1.8219258098493329, -1e-12); %!test %! [~, ~, st] = regression_ftest (y, X, 1); %! assert_equal (st.CohensF2CI, [0.39131195887888376, 6.1366401225022624], ... %! -1e-8); %!test %! ## A column that adds nothing has an interval from zero %! [h, ~, st] = regression_ftest (X(:,1) + sin (5 * (1:12)'), X, 1); %! assert_equal ([h, st.CohensF2CI(1)], [0, 0]); %!test %! ## 'Alpha' moves the decision but never the p-value %! [h1, p1] = regression_ftest (y, X, 1, 'Alpha', 1e-4); %! [h2, p2] = regression_ftest (y, X, 1, 'Alpha', 0.01); %! assert_equal ([h1, h2, p1], [0, 1, p2]); %!test %! [~, pr] = regression_ftest (y', X, 1); %! [~, pc] = regression_ftest (y, X, 1); %! assert_equal (pr, pc); %!test %! ## Degrees of freedom follow the ranks of the designs %! [~, ~, st] = regression_ftest (y, [X, X(:,1)]); %! assert_equal (st.df1, 2); ## Test input validation %!error regression_ftest (); %!error ... %! regression_ftest ([1 2 3]'); %!error ... %! regression_ftest ([1 2 NaN]', [2 3 4; 3 4 5]'); %!error ... %! regression_ftest ([1 2 Inf]', [2 3 4; 3 4 5]'); %!error ... %! regression_ftest ([1 2 3+i]', [2 3 4; 3 4 5]'); %!error ... %! regression_ftest ([1 2 3]', [2 3 NaN; 3 4 5]'); %!error ... %! regression_ftest ([1 2 3]', [2 3 Inf; 3 4 5]'); %!error ... %! regression_ftest ([1 2 3]', [2 3 4; 3 4 3+i]'); %!error ... %! regression_ftest ([1 2 3]', [2 3; 3 4]'); %!error ... %! regression_ftest ([1 2; 3 4]', [2 3; 3 4]'); %!error ... %! regression_ftest ((1:5)', [1:5; 2 1 4 3 5]', 1, 'Alpha'); %!error ... %! regression_ftest ((1:5)', [1:5; 2 1 4 3 5]', 1, 'Alpha', 0); %!error ... %! regression_ftest ((1:5)', [1:5; 2 1 4 3 5]', 1, 'Alpha', 1.2); %!error ... %! regression_ftest ((1:5)', [1:5; 2 1 4 3 5]', 1, 'Alpha', [0.02, 0.1]); %!error ... %! regression_ftest ((1:5)', [1:5; 2 1 4 3 5]', 1, 'Alpha', 'a'); %!error ... %! regression_ftest ((1:5)', [1:5; 2 1 4 3 5]', 1, 'Intercept', 'yes'); %!error ... %! regression_ftest ((1:5)', [1:5; 2 1 4 3 5]', 1, 'some', 0.05); %!error ... %! regression_ftest ((1:5)', [1:5; 2 1 4 3 5]', 1, 3, 0.05); %!error ... %! regression_ftest ((1:5)', [1:5; 2 1 4 3 5]', 3); %!error ... %! regression_ftest ((1:5)', [1:5; 2 1 4 3 5]', 1.5); %!error ... %! regression_ftest ((1:6)', [1:6; 2 1 4 3 6 5; 1 1 2 2 3 4]', [1, 1]); %!error ... %! regression_ftest ((1:5)', [1:5; 2 1 4 3 5]', true); %!error ... %! regression_ftest ((1:5)', [1:5; 2 1 4 3 5]', [1, 2]); %!error ... %! regression_ftest ([1 2 3]', [1 2 3; 2 1 3]'); %!error ... %! regression_ftest ((1:6)', [1:6; 2 1 4 3 6 5; 1:6]', [1, 2]); statistics-release-2.1.2/inst/Hypothesis_Testing/regression_ttest.m000066400000000000000000000254261527311104300257240ustar00rootroot00000000000000## Copyright (C) 1995-2017 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} regression_ttest (@var{y}, @var{x}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}] =} regression_ttest (@var{y}, @var{x}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}, @var{ci}] =} regression_ttest (@var{y}, @var{x}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}, @var{ci}, @var{stats}] =} regression_ttest (@var{y}, @var{x}) ## @deftypefnx {statistics} {[@dots{}] =} regression_ttest (@var{y}, @var{x}, @var{Name}, @var{Value}) ## ## Perform a linear regression t-test. ## ## @code{@var{h} = regression_ttest (@var{y}, @var{x})} tests the null ## hypothesis that the slope @math{beta1} of a simple linear regression equals ## 0. The result is @var{h} = 0 if the null hypothesis cannot be rejected at ## the 5% significance level, or @var{h} = 1 if the null hypothesis can be ## rejected at the 5% level. @var{y} and @var{x} must be vectors of equal ## length with finite real numbers. ## ## The p-value of the test is returned in @var{pval}. A @math{100(1-alpha)%} ## confidence interval for @math{beta1} is returned in @var{ci}. @var{stats} is ## a structure containing the value of the test statistic (@qcode{tstat}), ## the degrees of freedom (@qcode{df}), the slope coefficient (@qcode{beta1}), ## and the intercept (@qcode{beta0}). Under the null, the test statistic ## @var{stats}.@qcode{tstat} follows a @math{T}-distribution with ## @var{stats}.@qcode{df} degrees of freedom. ## ## @code{[@dots{}] = regression_ttest (@dots{}, @var{name}, @var{value})} ## specifies one or more of the following name/value pairs: ## ## @multitable @columnfractions 0.2 0.75 ## @headitem Name @tab Value ## @item @qcode{'alpha'} @tab the significance level. Default is 0.05. ## ## @item @qcode{'tail'} @tab a string specifying the alternative hypothesis ## @end multitable ## @multitable @columnfractions 0.25 0.65 ## @item @qcode{'both'} @tab @math{beta1} is not 0 (two-tailed, default) ## @item @qcode{'left'} @tab @math{beta1} is less than 0 (left-tailed) ## @item @qcode{'right'} @tab @math{beta1} is greater than 0 (right-tailed) ## @end multitable ## ## @seealso{regression_ftest, regress, regress_gp} ## @end deftypefn function [h, pval, ci, stats] = regression_ttest (y, x, varargin) ## Check for valid input if (nargin < 2) print_usage (); endif ## Check for finite real numbers in Y, X if (! all (isfinite (y)) || ! isreal (y)) error ("regression_ttest: Y must contain finite real numbers."); endif if (! all (isfinite (x(:))) || ! isreal (x)) error ("regression_ttest: X must contain finite real numbers."); endif # Get number of observations n = length (y); ## Check Y and X have the same number of observations if (! isvector (y) || ! isvector (x) || length (x) != n) error ("regression_ttest: Y and X must be vectors of equal length."); endif ## Set default arguments alpha = 0.05; tail = 'both'; ## Check additional options i = 1; while (i <= length (varargin)) switch lower (varargin{i}) case 'alpha' i = i + 1; alpha = varargin{i}; ## Check for valid alpha if (! isscalar (alpha) || ! isnumeric (alpha) || ... alpha <= 0 || alpha >= 1) error ("regression_ttest: invalid value for alpha."); endif case 'tail' i = i + 1; tail = varargin{i}; if (! any (strcmpi (tail, {'both', 'left', 'right'}))) error ("regression_ttest: invalid value for tail."); endif otherwise error ("regression_ttest: invalid Name argument."); endswitch i = i + 1; endwhile y_bar = mean (y); x_bar = mean (x); ## Ordinary least squares slope, taken from the centred data. Going through ## cov (x, y) tied this to whether cov returns the scalar covariance or the ## 2*2 matrix; it returns the matrix, which left every call form of this ## function dying on a nonconformant operand. x_ctr = x - x_bar; Sxx = sum (x_ctr .^ 2); stats.beta1 = sum (x_ctr .* (y - y_bar)) / Sxx; stats.beta0 = y_bar - stats.beta1 * x_bar; ## Fitted values, and from them the residual sum of squares. Evaluating the ## fit at x_bar rather than at x returned y_bar for every observation, since ## the least squares line passes through the means, so SSE was really the ## total sum of squares and the test statistic came out far too small. y_hat = stats.beta0 + stats.beta1 * x; SSE = sum ((y - y_hat) .^ 2); stats.df = n - 2; SE = sqrt (SSE / stats.df); term = SE / sqrt (Sxx); stats.tstat = stats.beta1 / term; ## Based on the "tail" argument determine the P-value, the critical values, ## and the confidence interval. switch lower (tail) case 'both' pval = 2 * tcdf (-abs (stats.tstat), stats.df); tcrit = - tinv (alpha / 2, stats.df); ci = [stats.beta1 - tcrit * term; stats.beta1 + tcrit * term]; case 'left' pval = tcdf (stats.tstat, stats.df); tcrit = - tinv (alpha, stats.df); ci = [-inf; stats.beta1 + tcrit * term]; case 'right' pval = tcdf (stats.tstat, stats.df, 'upper'); tcrit = - tinv (alpha, stats.df); ci = [stats.beta1 - tcrit * term; inf]; endswitch ## Determine the test outcome h = double (pval < alpha); h(isnan (pval)) = NaN; endfunction ## The function had no test that called it successfully, which is how every ## documented call form came to die on a nonconformant operand. Check the ## returned quantities against an ordinary least squares fit done here. %!test %! x = [1 2 3 4 5 6 7 8 9 10]'; %! y = 2 + 3 * x + [0.3 -0.2 0.5 -0.4 0.1 0.2 -0.3 0.4 -0.1 0.2]'; %! [h, pval, ci, stats] = regression_ttest (y, x); %! n = numel (x); %! b = [ones(n,1), x] \ y; %! resid = y - [ones(n,1), x] * b; %! se = sqrt (sum (resid .^ 2) / (n - 2) / sum ((x - mean (x)) .^ 2)); %! assert_equal (stats.beta1, b(2), 1e-12); %! assert_equal (stats.beta0, b(1), 1e-12); %! assert_equal (stats.df, n - 2); %! assert_equal (stats.tstat, b(2) / se, 1e-9); %! assert_equal (pval, 2 * (1 - tcdf (abs (b(2) / se), n - 2)), 1e-12); %! assert_equal (ci(:)', [b(2) - tinv(0.975, n-2) * se, ... %! b(2) + tinv(0.975, n-2) * se], 1e-9); %! assert_equal (h, 1); ## The fitted values must come from x, not from its mean. Evaluating the fit ## at x_bar returns y_bar for every observation, because the least squares ## line passes through the means, which made SSE the total sum of squares. %!test %! x = [1 2 3 4 5 6 7 8 9 10]'; %! y = 2 + 3 * x + [0.3 -0.2 0.5 -0.4 0.1 0.2 -0.3 0.4 -0.1 0.2]'; %! [~, ~, ~, stats] = regression_ttest (y, x); %! SSE = sum ((y - (stats.beta0 + stats.beta1 * x)) .^ 2); %! SST = sum ((y - mean (y)) .^ 2); %! assert_equal (stats.tstat, ... %! stats.beta1 / sqrt (SSE / stats.df / sum ((x - mean (x)) .^ 2)), ... %! 1e-9); %! assert_equal (SSE < SST / 100, true); ## A slope of zero must not be rejected. %!test %! x = (1:40)'; %! y = [0.5 -1.2 0.3 0.8 -0.4 1.1 -0.7 0.2 -0.9 0.6 ... %! 1.3 -0.1 0.4 -1.1 0.7 0.9 -0.5 0.1 -0.8 1.0 ... %! -0.3 0.2 1.2 -0.6 0.5 -1.0 0.8 0.3 -0.2 0.9 ... %! -1.3 0.6 0.1 -0.7 1.1 -0.4 0.7 0.2 -0.9 0.4]'; %! [h, pval] = regression_ttest (y, x); %! assert_equal (h, 0); %! assert_equal (pval > 0.05, true); ## The one-sided tails must bracket the two-sided p-value. %!test %! x = [1 2 3 4 5 6 7 8 9 10]'; %! y = 2 + 3 * x + [0.3 -0.2 0.5 -0.4 0.1 0.2 -0.3 0.4 -0.1 0.2]'; %! [~, pb] = regression_ttest (y, x, 'tail', 'both'); %! [~, pr] = regression_ttest (y, x, 'tail', 'right'); %! [~, pl] = regression_ttest (y, x, 'tail', 'left'); %! assert_equal (pr, pb / 2, 1e-12); %! assert_equal (pl, 1 - pr, 1e-12); ## A row-vector input must give the same answer as a column. %!test %! x = [1 2 3 4 5 6 7 8 9 10]'; %! y = 2 + 3 * x + [0.3 -0.2 0.5 -0.4 0.1 0.2 -0.3 0.4 -0.1 0.2]'; %! [~, ~, ~, sc] = regression_ttest (y, x); %! [~, ~, ~, sr] = regression_ttest (y', x'); %! assert_equal (sr.beta1, sc.beta1, 1e-12); %! assert_equal (sr.tstat, sc.tstat, 1e-9); %!test %! ## Below the resolution of 1 - tcdf, the slope p-value of fitlm in %! ## MATLAB R2024a %! x = (1:30)'; %! [~, pval] = regression_ttest (2 * x + 0.01 * sin (x), x); %! assert_equal (pval, 2.48569558345347e-96, -1e-6); %!test %! x = (1:30)'; %! [~, pval] = regression_ttest (2 * x + 0.01 * sin (x), x, 'tail', 'right'); %! assert_equal (pval, 2.48569558345347e-96 / 2, -1e-6); ## Test input validation %!error regression_ttest (); %!error regression_ttest (1); %!error ... %! regression_ttest ([1 2 NaN]', [2 3 4]'); %!error ... %! regression_ttest ([1 2 Inf]', [2 3 4]'); %!error ... %! regression_ttest ([1 2 3+i]', [2 3 4]'); %!error ... %! regression_ttest ([1 2 3]', [2 3 NaN]'); %!error ... %! regression_ttest ([1 2 3]', [2 3 Inf]'); %!error ... %! regression_ttest ([1 2 3]', [3 4 3+i]'); %!error ... %! regression_ttest ([1 2 3]', [3 4 4 5]'); %!error ... %! regression_ttest ([1 2 3]', [2 3 4]', 'alpha', 0); %!error ... %! regression_ttest ([1 2 3]', [2 3 4]', 'alpha', 1.2); %!error ... %! regression_ttest ([1 2 3]', [2 3 4]', 'alpha', [.02 .1]); %!error ... %! regression_ttest ([1 2 3]', [2 3 4]', 'alpha', 'a'); %!error ... %! regression_ttest ([1 2 3]', [2 3 4]', 'some', 0.05); %!error ... %! regression_ttest ([1 2 3]', [2 3 4]', 'tail', 'val'); %!error ... %! regression_ttest ([1 2 3]', [2 3 4]', 'alpha', 0.01, 'tail', 'val'); statistics-release-2.1.2/inst/Hypothesis_Testing/rjtest.m000066400000000000000000000540161527311104300236310ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} rjtest (@var{x}) ## @deftypefnx {statistics} {@var{h} =} rjtest (@var{x}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{h}, @var{p}] =} rjtest (@dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{p}, @var{rjstat}, @var{critval}] =} rjtest (@dots{}) ## ## Ryan-Joiner hypothesis test of composite normality. ## ## @code{@var{h} = rjtest (@var{x})} performs the Ryan-Joiner test of the null ## hypothesis that the sample in the vector @var{x} comes from a normal ## distribution with unknown mean and variance, against the alternative that it ## does not come from a normal distribution. The result @var{h} is 1 if the ## test rejects the null hypothesis at the 5% significance level, and 0 ## otherwise. @var{x} must be a vector of real values with at least four ## non-missing elements; @qcode{NaN} values are treated as missing and removed. ## ## The test statistic is the correlation between the sorted sample and the ## approximate expected normal order statistics of Blom, ## @tex ## $$ R = \frac{\sum_{i=1}^n x_{(i)} b_i} ## {\sqrt{\sum_{i=1}^n (x_i - \bar{x})^2 \sum_{i=1}^n b_i^2}}, ## \qquad b_i = \Phi^{-1}\left( \frac{i - 3/8}{n + 1/4} \right). $$ ## ## @end tex ## @ifnottex ## @code{R = sum (sort (x) .* b) / sqrt (sum ((x - mean (x)).^2) * sum (b.^2))}, ## where @code{b = norminv (((1:n) - 3/8) / (n + 1/4))}. ## @end ifnottex ## @math{R} lies in @math{(0,1]}, and small values of @math{R} are evidence ## against normality. @math{R^2} is the statistic @math{W'} of the ## Shapiro-Francia test in @code{swtest}. To test for a lognormal ## distribution, test @code{log (@var{x})}. ## ## The following @qcode{Name-Value} pairs are supported: ## ## @multitable @columnfractions 0.32 0.68 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab The significance level, a double or single scalar ## in the range @math{(0,1)}. The default is @math{0.05}. ## ## @item @qcode{'MonteCarloTolerance'} @tab A positive finite double or single ## scalar. When given, @var{p} and @var{critval} are estimated by Monte Carlo ## simulation of normal samples of the same size, and enough samples are drawn ## for the standard error of @var{p} to fall below this value. The name may be ## shortened to @qcode{'MonteCarloTol'}. ## @end multitable ## ## Without @qcode{'MonteCarloTolerance'} the critical value at the ## significance levels @math{0.01}, @math{0.05} and @math{0.1} is the ## approximation of Ryan and Joiner, ## @tex ## $$ c_\alpha (n) = \min \left( b_0 + \frac{b_1}{\sqrt{n}} + \frac{b_2}{n} ## + \frac{b_3}{n^2}, \; 1 \right), $$ ## ## @end tex ## @ifnottex ## @code{min (b0 + b1 / sqrt (n) + b2 / n + b3 / n^2, 1)}, ## @end ifnottex ## with the coefficients ## ## @multitable @columnfractions 0.16 0.16 0.16 0.16 0.16 ## @headitem Alpha @tab b0 @tab b1 @tab b2 @tab b3 ## @item 0.01 @tab 0.9963 @tab -0.0211 @tab -1.4106 @tab 3.1791 ## @item 0.05 @tab 1.0063 @tab -0.1288 @tab -0.6118 @tab 1.3505 ## @item 0.10 @tab 1.0071 @tab -0.1371 @tab -0.3682 @tab 0.7780 ## @end multitable ## ## At other significance levels the critical value is interpolated between ## these three with a monotone piecewise cubic (@code{pchip}), extended below ## @math{0.01} along the line through the values at @math{0.01} and ## @math{0.05}, and above @math{0.1} along the line to 1 at significance level ## 1. The p-value is interpolated linearly in @math{R} through the points ## (0, 0), (c01, 0.01), (c05, 0.05), (c10, 0.1) and (1, 1), where c01, c05 and ## c10 are the three critical values, so away from those levels it is a rough ## guide only. The approximation is calibrated for samples of up to 100 ## values, and a larger sample gives a warning; use ## @qcode{'MonteCarloTolerance'} for such samples, or for an accurate p-value. ## ## @code{[@var{h}, @var{p}] = rjtest (@dots{})} also returns the p-value ## @var{p} of the test, the probability of observing a statistic as small as ## @var{rjstat} under the null hypothesis. ## ## @code{[@var{h}, @var{p}, @var{rjstat}, @var{critval}] = rjtest (@dots{})} ## also returns the test statistic @var{rjstat} and the critical value ## @var{critval} at significance level @var{alpha}. The null hypothesis is ## rejected when @code{@var{rjstat} < @var{critval}}. ## ## A sample whose values are all equal has no defined statistic and is refused, ## as is a sample holding an infinite value, where MATLAB returns @qcode{NaN} ## for @var{p}, @var{rjstat} and @var{critval} and 1 for @var{h}. The option ## names are not case sensitive but must be given in full, bar ## @qcode{'MonteCarloTol'}, where MATLAB accepts any shortened name. ## ## References: ## @enumerate ## @item ## T. A. Ryan and B. L. Joiner. Normal probability plots and tests for ## normality. Technical report, Statistics Department, The Pennsylvania State ## University, 1976. ## @item ## G. Blom. @emph{Statistical Estimates and Transformed Beta-Variables}. ## Wiley, New York, 1958. ## @end enumerate ## ## @seealso{swtest, adtest, jbtest, kstest, lillietest} ## @end deftypefn function [h, p, rjstat, critval] = rjtest (x, varargin) ## Input validation if (nargin < 1) print_usage (); endif if (! ((isnumeric (x) || islogical (x)) && isreal (x) && isvector (x))) error ("rjtest: X must be a vector of real values."); endif ## Parse optional Name-Value paired arguments, 'MonteCarloTol' being the ## one short form taken k = 2 * find (strcmpi (varargin(1:2:end), 'MonteCarloTol')) - 1; varargin(k) = {'MonteCarloTolerance'}; optNames = {'Alpha', 'MonteCarloTolerance'}; dfValues = {0.05, []}; [alpha, mctol, args] = parsePairedArguments (optNames, dfValues, ... varargin(:)); if (! isempty (args)) if (mod (numel (args), 2) == 0 && ischar (args{1})) error ("rjtest: unknown parameter name '%s'.", args{1}); endif error ("rjtest: optional arguments must be Name-Value pairs."); endif if (! (isfloat (alpha) && isreal (alpha) && isscalar (alpha) && alpha > 0 && alpha < 1)) error ("rjtest: 'Alpha' must be a scalar between 0 and 1."); endif domc = any (strcmpi (varargin(1:2:end), 'MonteCarloTolerance')); if (domc && ! (isfloat (mctol) && isreal (mctol) && isscalar (mctol) && mctol > 0 && isfinite (mctol))) error ("rjtest: 'MonteCarloTolerance' must be a positive finite scalar."); endif mctol = double (mctol); ## Remove missing values and check the sample x = sort (double (x(! isnan (x))(:))); n = numel (x); if (any (isinf (x))) error ("rjtest: X must not contain infinite values."); endif if (n < 4) error ("rjtest: X must contain at least four non-missing values."); endif if (x(n) == x(1)) error ("rjtest: X must not be constant."); endif ## Correlation with Blom's approximate expected normal order statistics b = norminv (((1:n)' - 3/8) / (n + 1/4)); rjstat = sum (x .* b) / sqrt (sum ((x - mean (x)) .^ 2) * sum (b .^ 2)); if (domc) ## Draw normal samples until the standard error of P is below MCTOL sim = zeros (0, 1); reps = 1000; while (true) sim = [sim; rjtest_simulate_(b, reps - numel (sim))]; p = mean (sim <= rjstat); pq = p * (1 - p); if (pq / reps < mctol ^ 2) break; endif reps = max (ceil (pq / mctol ^ 2), reps + 1); endwhile critval = quantile (sim, double (alpha)); else if (n > 100) warning (strcat ("rjtest: the approximation is calibrated for at", ... " most 100 values; use 'MonteCarloTolerance' for a", ... " more reliable p-value.")); endif ## Critical values of Ryan and Joiner at ALPHA = 0.01, 0.05 and 0.1 C = [0.9963, -0.0211, -1.4106, 3.1791; 1.0063, -0.1288, -0.6118, 1.3505; 1.0071, -0.1371, -0.3682, 0.7780]; cv = min (C * [1; 1 / sqrt(n); 1 / n; 1 / n ^ 2], 1); ## A single ALPHA is compared with the three levels in its own precision, ## so single (0.05) takes the tabulated value as MATLAB does istab = (alpha == [0.01, 0.05, 0.1]); alpha = double (alpha); if (any (istab)) critval = cv(istab); elseif (alpha < 0.01) critval = cv(1) + (alpha - 0.01) * (cv(2) - cv(1)) / 0.04; elseif (alpha <= 0.1) critval = interp1 ([0.01, 0.05, 0.1], cv, alpha, "pchip"); else critval = cv(3) + (alpha - 0.1) * (1 - cv(3)) / 0.9; endif ## P-value linear in RJSTAT through the three critical values. A ## statistic of 1 has P 1 even where the cap at 1 has met C05 or C10. r = min (rjstat, 1); if (r == 1) p = 1; elseif (r <= cv(1)) p = 0.01 * r / cv(1); elseif (r <= cv(2)) p = 0.01 + 0.04 * (r - cv(1)) / (cv(2) - cv(1)); elseif (r <= cv(3)) p = 0.05 + 0.05 * (r - cv(2)) / (cv(3) - cv(2)); else p = 0.1 + 0.9 * (r - cv(3)) / (1 - cv(3)); endif endif h = double (rjstat < critval); endfunction ## Ryan-Joiner statistics of REPS standard normal samples of size numel (B), ## drawn in blocks of at most 1e6 values to bound the memory used. function r = rjtest_simulate_ (b, reps) n = numel (b); r = zeros (reps, 1); block = max (floor (1e6 / n), 1); sb = sqrt (sum (b .^ 2)); for k = 1:block:reps m = min (block, reps - k + 1); X = sort (randn (n, m)); r(k:k+m-1) = (b' * X) ./ (sb * sqrt (sum ((X - mean (X)) .^ 2))); endfor endfunction %!demo %! ## Test whether the sodium content of breakfast cereals is normal %! load cereal %! [h, p, rjstat, critval] = rjtest (Sodium) %!demo %! ## A Monte Carlo p-value for a sample of more than 100 values %! load carbig %! [h, p, rjstat, critval] = rjtest (MPG, 'MonteCarloTolerance', 0.001) ## Test output ## Values checked against MATLAB R2026b Update 1 %!test %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [h, p, rjstat, critval] = rjtest (exp (0.6 * b)); %! assert_equal (h, 1); %! assert_equal (p, 0.046583958270526, 1e-14); %! assert_equal (rjstat, 0.948467843754262, 1e-14); %! assert_equal (critval, 0.950285694449803, 1e-14); %!test %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [h, p, rjstat, critval] = rjtest (exp (0.6 * b), 'Alpha', 0.01); %! assert_equal (h, 0); %! assert_equal (p, 0.046583958270526, 1e-14); %! assert_equal (critval, 0.928999646567475, 1e-14); %!test %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [h, ~, ~, critval] = rjtest (exp (0.6 * b), 'Alpha', 0.2); %! assert_equal (h, 1); %! assert_equal (critval, 0.964425340469758, 1e-14); %!test %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [h, p, rjstat] = rjtest (b); %! assert_equal (h, 0); %! assert_equal (p, 1); %! assert_equal (rjstat, 1); %!test %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [h, p, rjstat] = rjtest (exp (0.1 * b)); %! assert_equal (h, 0); %! assert_equal (p, 0.964707970026192, 1e-13); %! assert_equal (rjstat, 0.998430622561938, 1e-14); %!test %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [h, p, rjstat] = rjtest (exp (0.55 * b)); %! assert_equal (h, 0); %! assert_equal (p, 0.079811798723863, 1e-14); %! assert_equal (rjstat, 0.956064898599311, 1e-14); %!test %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [h, p, rjstat] = rjtest (exp (0.7 * b)); %! assert_equal (h, 1); %! assert_equal (p, 0.015775669183456, 1e-14); %! assert_equal (rjstat, 0.932073175837263, 1e-14); %!test %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [h, p, rjstat] = rjtest (exp (3 * b)); %! assert_equal (h, 1); %! assert_equal (p, 0.006366180860427, 1e-14); %! assert_equal (rjstat, 0.591417976932142, 1e-14); %!test %! b = norminv (((1:100) - 3/8) / (100 + 1/4)); %! [h, p, rjstat, critval] = rjtest (exp (0.2 * b)); %! assert_equal (h, 0); %! assert_equal (p, 0.251186660905392, 1e-12); %! assert_equal (rjstat, 0.991501634213133, 1e-14); %! assert_equal (critval, 0.98743705, 1e-14); %!test %! b = norminv (((1:5) - 3/8) / (5 + 1/4)); %! [h, p, rjstat, critval] = rjtest (exp (2 * b)); %! assert_equal (h, 1); %! assert_equal (p, 0.026740104695873, 1e-14); %! assert_equal (rjstat, 0.852184703528202, 1e-14); %! assert_equal (critval, 0.880358888899605, 1e-14); %!test %! [h, p, rjstat, critval] = rjtest ([1 2 3 5]); %! assert_equal (h, 0); %! assert_equal (p, 0.871931434395105, 1e-14); %! assert_equal (rjstat, 0.985076454646874, 1e-14); %! assert_equal (critval, 0.87335625, 1e-14); %!test %! [h, p, rjstat, critval] = rjtest (int8 ([1 2 3 5 8 13])); %! assert_equal (h, 0); %! assert_equal (p, 0.520710875770748, 1e-14); %! assert_equal (rjstat, 0.952802591324644, 1e-14); %! assert_equal (critval, 0.889264842410477, 1e-14); %!test %! [h, p, rjstat] = rjtest (logical ([1 0 1 1 0])); %! assert_equal (h, 1); %! assert_equal (p, 0.021235058557769, 1e-14); %! assert_equal (rjstat, 0.845516565587550, 1e-14); %!test %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [~, p, rjstat] = rjtest (single (exp (0.6 * b))); %! assert_equal (class (rjstat), 'double'); %! assert_equal (p, 0.046583962851523, 1e-14); %! assert_equal (rjstat, 0.948467846192045, 1e-14); %!test %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [h, p, rjstat] = rjtest ([NaN, exp(0.6 * b), NaN]'); %! assert_equal (h, 1); %! assert_equal (p, 0.046583958270526, 1e-14); %! assert_equal (rjstat, 0.948467843754262, 1e-14); %!test %! load cereal %! [h, p, rjstat, critval] = rjtest (Sodium); %! assert_equal (h, 1); %! assert_equal (p, 0.009908718822304, 1e-14); %! assert_equal (rjstat, 0.967202079663432, 1e-14); %! assert_equal (critval, 0.983904201921336, 1e-14); %!test %! load cereal %! [~, ~, ~, critval] = rjtest (Sodium, 'Alpha', 0.01); %! assert_equal (critval, 0.976112146291078, 1e-14); %!warning ... %! load carbig %! [h, p, rjstat, critval] = rjtest (MPG); %! assert_equal (h, 1); %! assert_equal (p, 0.009928312778520, 1e-14); %! assert_equal (rjstat, 0.984608848799090, 1e-14); %! assert_equal (critval, 0.998315179111227, 1e-14); %!warning ... %! b = norminv (((1:500) - 3/8) / (500 + 1/4)); %! [h, p, rjstat, critval] = rjtest (b); %! assert_equal (h, 0); %! assert_equal (p, 1); %! assert_equal (rjstat, 1); %! assert_equal (critval, 0.999321690889961, 1e-14); %!warning ... %! b = norminv (((1:500) - 3/8) / (500 + 1/4)); %! [h, p, rjstat] = rjtest (exp (0.02 * b)); %! assert_equal (h, 0); %! assert_equal (p, 0.092980498877550, 1e-11); %! assert_equal (rjstat, 0.999904772168814, 1e-13); %!warning ... %! b = norminv (((1:500) - 3/8) / (500 + 1/4)); %! [h, p, rjstat] = rjtest (exp (0.08 * b)); %! assert_equal (h, 1); %! assert_equal (p, 0.045015541119476, 1e-12); %! assert_equal (rjstat, 0.998477598301906, 1e-13); %!warning ... %! b = norminv (((1:1000) - 3/8) / (1000 + 1/4)); %! [h, p, rjstat, critval] = rjtest (exp (0.02 * b)); %! assert_equal (h, 1); %! assert_equal (p, 0.049327273854406, 1e-12); %! assert_equal (rjstat, 0.999902880855905, 1e-13); %! assert_equal (critval, 1); %!warning ... %! b = norminv (((1:1000) - 3/8) / (1000 + 1/4)); %! [h, p] = rjtest (b); %! assert_equal (h, 0); %! assert_equal (p, 1); ## Test the critical values ## Values checked against MATLAB R2026b Update 1 %!test %! [~, ~, ~, critval] = rjtest ([1 2 3 5], 'Alpha', 0.01); %! assert_equal (critval, 0.83179375, 1e-15); %!test %! [~, ~, ~, critval] = rjtest ([1 2 3 5], 'Alpha', 0.1); %! assert_equal (critval, 0.895125, 1e-15); %!test %! b = norminv (((1:7) - 3/8) / (7 + 1/4)); %! [~, ~, ~, critval] = rjtest (b, 'Alpha', 0.05); %! assert_equal (critval, 0.897779400366207, 1e-14); %!test %! b = norminv (((1:50) - 3/8) / (50 + 1/4)); %! [~, ~, ~, critval] = rjtest (b, 'Alpha', 0.1); %! assert_equal (critval, 0.980658332059865, 1e-14); %!test %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [~, ~, ~, critval] = rjtest (b, 'Alpha', 0.001); %! assert_equal (critval, 0.924210285793952, 1e-14); %!test %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [~, ~, ~, critval] = rjtest (b, 'Alpha', 0.03); %! assert_equal (critval, 0.941609306183096, 1e-14); %!test %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [~, ~, ~, critval] = rjtest (b, 'Alpha', 0.075); %! assert_equal (critval, 0.956902493214627, 1e-14); %!test %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [~, ~, ~, critval] = rjtest (b, 'Alpha', 0.9); %! assert_equal (critval, 0.995553167558720, 1e-14); %!test %! b = norminv (((1:100) - 3/8) / (100 + 1/4)); %! [~, ~, ~, critval] = rjtest (b, 'Alpha', 0.2); %! assert_equal (critval, 0.990920711111111, 1e-14); %!test %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [~, ~, ~, critval] = rjtest (b, 'Alpha', single (0.05)); %! assert_equal (critval, 0.950285694449803, 1e-15); %!warning ... %! b = norminv (((1:500) - 3/8) / (500 + 1/4)); %! [~, ~, ~, critval] = rjtest (b, 'Alpha', 0.075); %! assert_equal (critval, 0.999822959048033, 1e-14); %!warning ... %! b = norminv (((1:1000) - 3/8) / (1000 + 1/4)); %! [~, ~, ~, critval] = rjtest (b, 'Alpha', 0.03); %! assert_equal (critval, 0.998155316469655, 1e-14); %!warning ... %! b = norminv (((1:1000) - 3/8) / (1000 + 1/4)); %! [~, ~, ~, critval] = rjtest (b, 'Alpha', 0.005); %! assert_equal (critval, 0.993503505827917, 1e-14); %!warning ... %! b = norminv (((1:1000) - 3/8) / (1000 + 1/4)); %! [~, ~, ~, critval] = rjtest (b, 'Alpha', 0.2); %! assert_equal (critval, 1); ## Test the Monte Carlo p-value %!test %! randn ('state', 1); %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [~, p] = rjtest (exp (3 * b), 'MonteCarloTol', 1e-4); %! assert_equal (p, 0); %!test %! randn ('state', 1); %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [h, p, rjstat, critval] = rjtest (exp (0.6 * b), 'MonteCarloTolerance', ... %! 0.001); %! assert_equal (h, 1); %! assert_equal (p, 0.042123485285632, 0.005); %! assert_equal (rjstat, 0.948467843754262, 1e-14); %! assert_equal (critval, 0.950586583658806, 0.002); %!test %! randn ('state', 1); %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [h, p] = rjtest (exp (0.3 * b), 'MonteCarloTolerance', 0.001); %! assert_equal (h, 0); %! assert_equal (p, 0.718153708746535, 0.005); %!test %! randn ('state', 1); %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [h, p] = rjtest (exp (3 * b), 'MonteCarloTolerance', 1e-4); %! assert_equal (h, 1); %! assert_equal (p, 0); %!test %! randn ('state', 1); %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [~, p] = rjtest (b, 'MonteCarloTolerance', 1); %! assert_equal (p, 1); %!test %! randn ('state', 1); %! b = norminv (((1:20) - 3/8) / (20 + 1/4)); %! [~, ~, ~, critval] = rjtest (exp (0.6 * b), 'MonteCarloTolerance', 0.01, ... %! 'alpha', 0.3); %! assert_equal (critval, 0.974724689853778, 0.003); %!test %! randn ('state', 1); %! b = norminv (((1:1000) - 3/8) / (1000 + 1/4)); %! [h, p, ~, critval] = rjtest (exp (0.3 * b), 'MonteCarloTolerance', 0.01); %! assert_equal (h, 1); %! assert_equal (p, 0); %! assert_equal (critval, 0.998460671929262, 2e-4); ## Test input validation %!error rjtest () %!error rjtest ([]) %!error rjtest (magic (4)) %!error rjtest ([1 2 3 4 5] + 1i) %!error rjtest ('abcde') %!error rjtest ({1, 2, 3, 4}) %!error rjtest (1:10, 'Foo', 0.05) %!error rjtest (1:10, 'Alp', 0.05) %!error ... %! rjtest (1:10, 'MonteCarlo', 0.01) %!error ... %! rjtest (1:10, 'montecarlotol', 0) %!error ... %! rjtest (1:10, 'Alpha') %!error ... %! rjtest (1:10, 'Alpha', 0) %!error ... %! rjtest (1:10, 'Alpha', 1) %!error ... %! rjtest (1:10, 'Alpha', NaN) %!error ... %! rjtest (1:10, 'Alpha', [0.05 0.1]) %!error ... %! rjtest (1:10, 'Alpha', int8 (1)) %!error ... %! rjtest (1:10, 'MonteCarloTolerance', 0) %!error ... %! rjtest (1:10, 'MonteCarloTolerance', -1) %!error ... %! rjtest (1:10, 'MonteCarloTolerance', NaN) %!error ... %! rjtest (1:10, 'MonteCarloTolerance', Inf) %!error ... %! rjtest (1:10, 'MonteCarloTolerance', []) %!error rjtest ([1 2 3 Inf 5 6]) %!error ... %! rjtest ([1 2 3 NaN]) %!error rjtest (ones (1, 10)) statistics-release-2.1.2/inst/Hypothesis_Testing/runstest.m000066400000000000000000000320131527311104300241760ustar00rootroot00000000000000## Copyright (C) 2013 Nir Krakauer ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} runstest (@var{x}) ## @deftypefnx {statistics} {@var{h} =} runstest (@var{x}, @var{v}) ## @deftypefnx {statistics} {@var{h} =} runstest (@var{x}, @qcode{'ud'}) ## @deftypefnx {statistics} {@var{h} =} runstest (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}, @var{stats}] =} runstest (@dots{}) ## ## Run test for randomness in the vector @var{x}. ## ## @code{@var{h} = runstest (@var{x})} calculates the number of runs of ## consecutive values above or below the mean of @var{x} and tests the null ## hypothesis that the values in the data vector @var{x} come in random order. ## @var{h} is 1 if the test rejects the null hypothesis at the 5% significance ## level, or 0 otherwise. ## ## @code{@var{h} = runstest (@var{x}, @var{v})} tests the null hypothesis based ## on the number of runs of consecutive values above or below the specified ## reference value @var{v}. Values exactly equal to @var{v} are omitted. ## ## @code{@var{h} = runstest (@var{x}, @qcode{'ud'})} calculates the number of ## runs up or down and tests the null hypothesis that the values in the data ## vector @var{x} follow a trend. Too few runs indicate a trend, while too ## many runs indicate an oscillation. Values exactly equal to the preceding ## value are omitted. ## ## @code{@var{h} = runstest (@dots{}, @var{Name}, @var{Value})} specifies ## additional options to the above tests by one or more @var{Name}-@var{Value} ## pair arguments. ## ## @multitable @columnfractions 0.15 0.8 ## @headitem Name @tab Value ## @item @qcode{'alpha'} @tab the significance level. Default is 0.05. ## ## @item @qcode{'method'} @tab a string specifying the method used to ## compute the p-value of the test. It can be either @qcode{'exact'} to use an ## exact algorithm, or @qcode{'approximate'} to use a normal approximation. The ## default is @qcode{'exact'} for runs above/below, and for runs up/down when ## the length of x is less than or equal to 50. When testing for runs up/down ## and the length of @var{x} is greater than 50, then the default is ## @qcode{'approximate'}, and the @qcode{'exact'} method is not available. ## ## @item @qcode{'tail'} @tab a string specifying the alternative hypothesis ## @end multitable ## @multitable @columnfractions 0.15 0.5 ## @item @qcode{'both'} @tab two-tailed (default) ## @item @qcode{'left'} @tab left-tailed ## @item @qcode{'right'} @tab right-tailed ## @end multitable ## ## If @var{x} is empty, @var{h} is 0, @var{pval} is 1, and the fields of ## @var{stats} are @code{nruns} and @code{z} both @qcode{NaN} and @code{n1} and ## @code{n0} both 0, which is also what a sample that is empty after ties are ## removed returns. MATLAB accepts a character array as @var{x} and returns ## the empty result for it; @var{x} must be numeric here. ## ## @seealso{signrank, signtest} ## @end deftypefn function [h, pval, stats] = runstest (x, v, varargin) ## Check arguments if (nargin < 1) print_usage; endif ## Check X being a vector of scalar values if (! ((isvector (x) || isempty (x)) && isnumeric (x))) error ("runstest: X must be a vector a scalar values."); else ## Remove missing values (NaNs) x = x(:); x(isnan (x)) = []; endif ## Check second argument being either a scalar reference number or "ud" string if (nargin > 1) if (isempty (v)) v = mean (x); endif if (isnumeric (v) && isscalar (v)) x = sign (x - v); rm = x == 0; if (sum (rm) > 0) warning ("runstest: %d elements equal to V were omitted.", sum (rm)); endif x(rm) = []; N = numel (x); UD = false; elseif (strcmpi (v, 'ud')) x = diff (x); rm = x == 0; if (sum (rm) > 0) warning ("runstest: %d repeated elements were omitted.", sum (rm)); endif x(rm) = []; N = numel (x) + 1; UD = true; else error ("runstest: V must be either a scalar number or 'ud' char string."); endif v = v; else v = mean (x); x = sign (x - v); rm = x == 0; if (sum (rm) > 0) warning ("runstest: %d elements equal to 'mean(X)' were omitted.", ... sum (rm)); endif x(rm) = []; N = numel (x); UD = false; endif ## Get number of runs n_up = sum (x==1); n_dn = numel (x) - n_up; ## Add defaults alpha = 0.05; if (N < 50 || ! UD) method = 'exact'; else method = 'approximate'; endif tail = 'both'; ## Parse optional arguments and validate parameters while (numel (varargin) > 1) switch (lower (varargin{1})) case 'alpha' alpha = varargin{2}; if (! isscalar (alpha) || ! isnumeric (alpha) || alpha <= 0 || alpha >= 1) error ("runstest: invalid value for alpha."); endif case 'method' method = varargin{2}; if (! any (strcmpi (method, {'exact', 'approximate'}))) error ("runstest: invalid value for method."); endif if (strcmpi (method, 'exact') && N > 50) warning ("runstest: exact method is not available for N > 50."); method = 'approximate'; endif case 'tail' tail = varargin{2}; if (! any (strcmpi (tail, {'both', 'left', 'right'}))) error ("runstest: invalid value for tail."); endif otherwise error ("runstest: invalid optional argument."); endswitch varargin([1:2]) = []; endwhile ## Do the calculations here if (N > 0) R_num = sum (x([1:end-1]) != x([2:end])) + 1; ##R_num = sum ((x(1:(end-1)) .* x(2:end)) < 0) + 1; #number of runs ## Special case if (N == 1) z = NaN; ## Compute with z statistic else ## Handle up/down or above/below if (UD) R_bar = (2 * N - 1) / 3; R_std = sqrt ((16 * N - 29) / 90); else R_bar = 1 + 2 * n_up * n_dn / N; R_std = sqrt (2 * n_up * n_dn * (2 * n_up * n_dn - N) / ... (N ^ 2 * (N - 1))); endif ## Handle tail if (strcmpi (tail, 'both')) tc = -0.5 * sign (R_num - R_bar); elseif (strcmpi (tail, 'left')) tc = 0.5; else tc = -0.5; endif ## Compute z value if (R_std > 0) z = (R_num + tc - R_bar) / R_std; else z = Inf * sign (R_num + tc - R_bar); endif endif ## Exact method if (strcmpi (method, 'exact')) if (UD) R_max = N - 1; ## Get precalculated results from rundist.mat file temp = load ('rundist.mat'); runD = temp.rundist; M = runD{N}; p = M / sum (M); p = p([1:R_max]); else R_max = 2 * min ([n_up, n_dn]) + 1; if (n_up == 0 || n_dn == 0) p = 1; else R_vec = [1:R_max]; p = zeros (size (R_vec)); t = mod (R_vec, 2) == 0; ## Compute even if (any (t)) k = R_vec(t) / 2; p(t) = 2 * exp (logBinoCoeff (n_up - 1, k - 1) + ... logBinoCoeff (n_dn - 1, k - 1) - ... logBinoCoeff (N, n_dn)); endif ## Compute odd if (any (! t)) k = floor (R_vec(! t) / 2); logdenom = logBinoCoeff (N, n_dn); p(! t) = exp (logBinoCoeff (n_up - 1, k - 1) + ... logBinoCoeff (n_dn - 1, k) - logdenom) + ... exp (logBinoCoeff (n_up - 1, k) + ... logBinoCoeff (n_dn - 1, k - 1) - logdenom); endif endif endif if (isempty (p)) p_ex = 1; else p_ex = p(R_num); endif p_lo = sum (p([1:R_num-1])); p_hi = sum (p([R_num+1:end])); else ## Compute with z statistic p_ex = 0; p_lo = normcdf (z); p_hi = normcdf (-z); endif ## Assume a constant vector in data else R_num = NaN; p_ex = 1; p_lo = 0; p_hi = 0; z = NaN; endif ## Compute tail probability if (strcmpi (tail, 'both')) pval = min ([1, 2*(p_ex + min ([p_lo, p_hi]))]); elseif (strcmpi (tail, 'left')) pval = p_ex + p_lo; else pval = p_ex + p_hi; endif ## Return decision of test h = double (pval <= alpha); if (nargout > 2) stats.nruns = R_num; stats.n1 = n_up; stats.n0 = n_dn; stats.z = z; endif endfunction ## Compute the log of the binomial coefficient function logBC = logBinoCoeff (N,n) logBC = gammaln (N + 1) - gammaln (n + 1) - gammaln (N - n + 1); endfunction %!test %! ## NIST beam deflection data %! ## http://www.itl.nist.gov/div898/handbook/eda/section4/eda425.htm %! data = [-213, -564, -35, -15, 141, 115, -420, -360, 203, -338, -431, ... %! 194, -220, -513, 154, -125, -559, 92, -21, -579, -52, 99, -543, ... %! -175, 162, -457, -346, 204, -300, -474, 164, -107, -572, -8, 83, ... %! -541, -224, 180, -420, -374, 201, -236, -531, 83, 27, -564, -112, ... %! 131, -507, -254, 199, -311, -495, 143, -46, -579, -90, 136, ... %! -472, -338, 202, -287, -477, 169, -124, -568, 17, 48, -568, -135, ... %! 162, -430, -422, 172, -74, -577, -13, 92, -534, -243, 194, -355, ... %! -465, 156, -81, -578, -64, 139, -449, -384, 193, -198, -538, 110, ... %! -44, -577, -6, 66, -552, -164, 161, -460, -344, 205, -281, -504, ... %! 134, -28, -576, -118, 156, -437, -381, 200, -220, -540, 83, 11, ... %! -568, -160, 172, -414, -408, 188, -125, -572, -32, 139, -492, ... %! -321, 205, -262, -504, 142, -83, -574, 0, 48, -571, -106, 137, ... %! -501, -266, 190, -391, -406, 194, -186, -553, 83, -13, -577, -49, ... %! 103, -515, -280, 201, 300, -506, 131, -45, -578, -80, 138, -462, ... %! -361, 201, -211, -554, 32, 74, -533, -235, 187, -372, -442, 182, ... %! -147, -566, 25, 68, -535, -244, 194, -351, -463, 174, -125, -570, ... %! 15, 72, -550, -190, 172, -424, -385, 198, -218, -536, 96]; %! [h, p, stats] = runstest (data, median (data)); %! expected_h = 1; %! expected_p = 0.008562; %! expected_z = 2.6229; %! assert_equal (h, expected_h); %! assert_equal (p, expected_p, 1E-6); %! assert_equal (stats.z, expected_z, 1E-4); %!shared x %! x = [45, -60, 1.225, 55.4, -9 27]; %!test %! [h, p, stats] = runstest (x); %! assert_equal (h, 0); %! assert_equal (p, 0.6, 1e-14); %! assert_equal (stats.nruns, 5); %! assert_equal (stats.n1, 3); %! assert_equal (stats.n0, 3); %! assert_equal (stats.z, 0.456435464587638, 1e-14); %!test %! ## Edge cases with empty arrays %! [h, p, s] = runstest ([]); %! assert_equal (h, 0); %! assert_equal (p, 1); %! assert_equal (isnan (s.nruns), true); %! assert_equal (s.n1, 0); %! assert_equal (s.n0, 0); %! assert_equal (isnan (s.z), true); %!test %! [h, p, s] = runstest (zeros (0, 3)); %! assert_equal (h, 0); %! assert_equal (p, 1); %! assert_equal (isnan (s.nruns), true); %! assert_equal (s.n1, 0); %! assert_equal (s.n0, 0); %! assert_equal (isnan (s.z), true); %!test %! [h, p, stats] = runstest (x, [], 'method', 'approximate'); %! assert_equal (h, 0); %! assert_equal (p, 0.6481, 1e-4); %! assert_equal (stats.z, 0.456435464587638, 1e-14); %!test %! [h, p, stats] = runstest (x, [], 'tail', 'left'); %! assert_equal (h, 0); %! assert_equal (p, 0.9, 1e-14); %! assert_equal (stats.z, 1.369306393762915, 1e-14); %!error runstest (ones (2,20)) %!error runstest (['asdasda']) %!error runstest ('') %!error ... %! runstest ([2 3 4 3 2 3 4], 'updown') %!error ... %! runstest ([2 3 4 3 2 3 4], [], 'alpha', 0) %!error ... %! runstest ([2 3 4 3 2 3 4], [], 'alpha', [0.02 0.2]) %!error ... %! runstest ([2 3 4 3 2 3 4], [], 'alpha', 1.2) %!error ... %! runstest ([2 3 4 3 2 3 4], [], 'alpha', -0.05) %!error ... %! runstest ([2 3 4 3 2 3 4], [], 'method', 'some') %!error ... %! runstest ([2 3 4 3 2 3 4], [], 'tail', 'some') %!error ... %! runstest ([2 3 4 3 2 3 4], [], 'option', 'some') statistics-release-2.1.2/inst/Hypothesis_Testing/signrank.m000066400000000000000000000535071527311104300241360ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{pval} =} signrank (@var{x}) ## @deftypefnx {statistics} {@var{pval} =} signrank (@var{x}, @var{my}) ## @deftypefnx {statistics} {@var{pval} =} signrank (@var{x}, @var{my}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{pval}, @var{h}] =} signrank (@dots{}) ## @deftypefnx {statistics} {[@var{pval}, @var{h}, @var{stats}] =} signrank (@dots{}) ## ## Wilcoxon signed rank test for median. ## ## @code{@var{pval} = signrank (@var{x})} returns the @math{p}-value of a ## two-sided Wilcoxon signed rank test. It tests the null hypothesis that data ## in @var{x} come from a distribution with zero median at the 5% significance ## level under the assumption that the distribution is symmetric about its ## median. @var{x} must be a vector. ## ## If the second argument @var{my} is a scalar, the null hypothesis is that ## @var{x} has median @var{my}, whereas if @var{my} is a vector, the null ## hypothesis is that the distribution of @code{@var{x} - @var{my}} has zero ## median. ## ## @code{@var{pval} = signrank (@dots{}, @var{Name}, @var{Value})} performs the ## Wilcoxon signed rank test with additional options specified by one or more of ## the following @var{Name}, @var{Value} pair arguments: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'alpha'} @tab A scalar value for the significance level of ## the test. Default is 0.05. ## ## @item @qcode{'tail'} @tab A character vector specifying the alternative ## hypothesis. It can take one of the following values: ## @end multitable ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Value} @tab @var{Description} ## ## @item @qcode{'both'} @tab For one-sample test (@var{my} is empty or a ## scalar), the data in @var{x} come from a continuous distribution with median ## different than zero or @var{my}. For two-sample test (@var{my} is a vector), ## the data in @qcode{@var{x} - @var{my}} come from a continuous distribution ## with median different than zero. ## ## @item @qcode{'left'} @tab For one-sample test (@var{my} is empty or a ## scalar), the data in @var{x} come from a continuous distribution with median ## less than zero or @var{my}. For two-sample test (@var{my} is a vector), the ## data in @qcode{@var{x} - @var{my}} come from a continuous distribution with ## median less than zero. ## ## @item @qcode{'right'} @tab For one-sample test (@var{my} is empty or a ## scalar), the data in @var{x} come from a continuous distribution with median ## greater than zero or @var{my}. For two-sample test (@var{my} is a vector), ## the data in @qcode{@var{x} - @var{my}} come from a continuous distribution ## with median greater than zero. ## @end multitable ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'method'} @tab A character vector specifying the method for ## computing the @math{p}-value. It can take one of the following values: ## @end multitable ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Value} @tab @var{Description} ## ## @item @qcode{'exact'} @tab Exact computation of the @math{p}-value. It ## is the default value for 15 of fewer observations when @qcode{'method'} is ## not specified. ## ## @item @qcode{'approximate'} @tab Using normal approximation for ## computing the @math{p}-value. It is the default value for more than 15 ## observations when @qcode{'method'} is not specified. ## @end multitable ## ## @code{[@var{pval}, @var{h}] = signrank (@dots{})} also returns a logical ## value indicating the test decision. If @var{h} is 0, the null hypothesis is ## accepted, whereas if @var{h} is 1, the null hypothesis is rejected. ## ## @code{[@var{pval}, @var{h}, @var{stats}] = signrank (@dots{})} also returns ## the structure @var{stats} containing the following fields: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Field} @tab @var{Value} ## @item @qcode{signedrank} @tab Value of the sign rank test statistic. ## ## @item @qcode{zval} @tab Value of the @math{z}-statistic, computed only ## when the @qcode{'method'} is @qcode{'approximate'} and empty otherwise. ## The field is always there. ## ## @item @qcode{RankBiserial} @tab The matched-pairs rank-biserial ## correlation, the effect size of the test: the sum of the signed ranks of the ## positive differences less that of the negative ones, divided by the sum of ## all the ranks, over the nonzero differences. It lies in @math{[-1, 1]}. ## ## @item @qcode{RankBiserialCI} @tab Its two-sided confidence interval at the ## level @math{100 (1 - alpha)} percent, whatever the @qcode{'tail'}. ## @end multitable ## ## The last two fields are an Octave extension. By default the interval is ## the normal interval of the Fisher transformed correlation, with standard ## error @math{sqrt(n (n + 1) (2n + 1) / 6) / (n (n + 1) / 2)} over the ## @math{n} nonzero differences. The R package effectsize computes the same ## interval but counts @math{n} over the elements of @var{x} that differ from ## the hypothesised median, so where some differences are zero its interval ## is narrower than this one. Two further options choose otherwise: ## ## @multitable @columnfractions 0.32 0.66 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'ConfidenceIntervalType'} @tab @qcode{'exact'}, the default, ## or @qcode{'bootstrap'} for the bias-corrected and accelerated bootstrap ## interval, resampling the pairs. ## @item @qcode{'NumBootstraps'} @tab The number of bootstrap replicates, a ## positive integer, 1000 by default. ## @end multitable ## ## @seealso{tiedrank, signtest, runstest, ranksum, meanEffectSize} ## @end deftypefn function [p, h, stats] = signrank (x, my, varargin) ## Check X being a vector if (! isvector (x)) error ("signrank: X must be a vector."); endif ## Add defaults alpha = 0.05; tail = 'both'; if (numel (x) <= 15) method = 'exact'; else method = 'approximate'; endif method_present = false; citype = 'exact'; nboot = 1000; ## When called with a single input argument of second argument is empty if (nargin == 1 || isempty (my)) my = zeros (size (x)); endif ## If second argument is a scalar convert to vector or check for Y being a ## vector and that X and Y have equal lengths if (isscalar (my)) my = repmat (my, size (x)); elseif (! isvector (my)) error ("signrank: Y must be either a scalar of a vector."); elseif (numel (x) != numel (my)) error ("signrank: X and Y vectors have different lengths."); endif ## Get optional input arguments if (mod (numel (varargin), 2) != 0) error ("signrank: optional arguments must be in pairs."); endif while (numel (varargin) > 0) switch (lower (varargin{1})) case 'alpha' alpha = varargin{2}; case 'tail' tail = varargin{2}; case 'method' method = varargin{2}; method_present = true; case 'confidenceintervaltype' citype = varargin{2}; case 'numbootstraps' nboot = varargin{2}; otherwise error ("signrank: invalid Name argument."); endswitch varargin([1:2]) = []; endwhile ## Check values for optional input arguments if (! isnumeric (alpha) || isnan (alpha) || ! isscalar (alpha) ... || alpha <= 0 || alpha >= 1) error ("signrank: 'alpha' must be a numeric scalar in the range 0 to 1."); endif if (! ischar (tail)) error ("signrank: 'tail' argument must be a character vector."); elseif (sum (strcmpi (tail, {'both', 'right', 'left'})) != 1) error ("signrank: 'tail' value must be either 'both', right' or 'left'."); endif if (! ischar (method)) error ("signrank: 'method' argument must be a character vector."); elseif (sum (strcmpi (method, {'exact', 'approximate'})) != 1) error ("signrank: 'method' value must be either 'exact' or 'approximate'."); endif if (! (ischar (citype) && any (strcmpi (citype, {'exact', 'bootstrap'})))) error (strcat ("signrank: 'ConfidenceIntervalType' must be 'exact'", ... " or 'bootstrap'.")); endif if (! (isnumeric (nboot) && isreal (nboot) && isscalar (nboot) && isfinite (nboot) && nboot >= 1 && nboot == fix (nboot))) error ("signrank: 'NumBootstraps' must be a positive integer."); endif ## Calculate differences between X and Y vectors: remove equal values of NaNs. ## A difference smaller than the combined resolution of the two values it came ## from is not a real difference, so it counts as equal, and the same ## tolerance decides which differences rank as tied. MATLAB defines it as ## EPS (X) + EPS (Y) per pair. XY_diff = x(:) - my(:); if (isfloat (x) && isfloat (my)) epsdiff = eps (x(:)) + eps (my(:)); else epsdiff = zeros (size (XY_diff)); endif drop = abs (XY_diff) < epsdiff | XY_diff == 0 | isnan (XY_diff); ## Every pair with a difference, zeros included, for the bootstrap all_diff = XY_diff(! isnan (XY_diff)); all_diff(abs (all_diff) < epsdiff(! isnan (XY_diff))) = 0; XY_diff(drop) = []; epsdiff(drop) = []; ## Recalculate remaining length of X vector (after equal or NaNs removal) n = length (XY_diff); ## Check for identical X and Y input arguments if (n == 0) p = 1; h = 0; stats.signedrank = 0; stats.zval = []; stats.RankBiserial = NaN; stats.RankBiserialCI = [NaN, NaN]; return; endif ## Re-evaluate method selection if (! method_present) if (n <= 15) method = 'exact'; else method = 'approximate'; endif endif ## Compute signed rank statistic [tie_rank, tieadj] = tiedrank (abs (XY_diff), 0, 0, epsdiff); w = sum (tie_rank(XY_diff > 0)); stats.signedrank = w; ## The rank sums of both signs, for the effect size, before the exact ## method below reorders the ranks tp = w; tm = sum (tie_rank(XY_diff < 0)); ## Calculate stats according to selected method and tail switch (lower (method)) case 'exact' w_max = n * (n + 1) / 2; ## Always compute lower tail switch_tail = false; if (w > w_max / 2) w = w_max - w; switch_tail = true; endif ## Avoid integers in tied ranks double_ties = any (tie_rank != fix (tie_rank)); if (double_ties) tie_rank = round (2 * tie_rank); w = round (2 * w); endif ## Loop through all combinations of ranks C = zeros (w + 1,1); C(1) = 1; curr = 1; tie_rank = sort (tie_rank); w_tr = tie_rank(tie_rank <= w); for tr = 1:numel (w_tr) next = min (curr + w_tr(tr), w + 1); C_hi = min (w_tr(tr), w + 1) + 1:next; C_lo = 1:length (C_hi); C(C_hi) = C(C_hi) + C(C_lo); curr = next; endfor ## Fix rank statistic if (double_ties) w = w / 2; endif ## Compute tail probability C = C / (2 ^ n); p = sum (C); switch (lower (tail)) case 'both' p = min (1, 2 * p); # two-sided case 'right' if (! switch_tail) # right tail is larger p = 1 - p + C(end); endif case 'left' if (switch_tail) # left tail is larger p = 1 - p + C(end); endif endswitch ## No Z-statistic exists for the exact test, so the field is created ## empty rather than holding a value. R2024a omits it altogether, but ## R2026a and the documentation both give an empty one. stats.zval = []; case 'approximate' ## Compute z-value z_nom = w - n * (n + 1) / 4; z_den = sqrt ((n * (n + 1) * (2 * n + 1) - tieadj) / 24); switch (lower (tail)) case 'both' z = z_nom / z_den; p = 2 * normcdf (-abs (z)); case 'right' z = (z_nom - 0.5) / z_den; p = normcdf (-z); case 'left' z = (z_nom + 0.5) / z_den; p = normcdf (z); endswitch stats.zval = z; endswitch h = p <= alpha; ## The matched-pairs rank-biserial correlation and its interval if (nargout > 2) r = (tp - tm) / (tp + tm); stats.RankBiserial = r; if (strcmpi (citype, 'exact')) ## Fisher's z, with the standard error the R package effectsize uses, ## over the nonzero differences, which are the ones ranked; effectsize ## 1.0.3 counts the elements of X that differ from the hypothesised ## median instead, so its interval differs where a difference is zero se = sqrt ((2 * n ^ 3 + 3 * n ^ 2 + n) / 6) / ((n ^ 2 + n) / 2); stats.RankBiserialCI = tanh (atanh (r) ... + norminv ([alpha / 2, 1 - alpha / 2]) * se); else stats.RankBiserialCI = __bootci__ (@rankBiserial, {all_diff}, alpha, ... nboot, 'rows'); endif endif endfunction ## The matched-pairs rank-biserial correlation of each column of differences ## D, over its nonzero differences function r = rankBiserial (D) r = NaN (1, columns (D)); for k = 1:columns (D) d = D(D(:,k) != 0, k); if (! isempty (d)) rk = tiedrank (abs (d)); r(k) = (sum (rk(d > 0)) - sum (rk(d < 0))) / sum (rk); endif endfor endfunction ## Test output ## Field layouts below are R2024a's, measured 2026-08-17, followed by the ## two effect size fields, which are ours. %!test %! ## the exact test has no z-statistic, so no ZVAL field is created at all %! x = [1.83 0.50 1.62 2.48 1.68 1.88 1.55 3.06 1.30]; %! y = [0.878 0.647 0.598 2.05 1.06 1.29 1.06 3.14 1.29]; %! [p, h, stats] = signrank (x, y, 'method', 'exact'); %! assert_equal (fieldnames (stats), {'signedrank'; 'zval'; 'RankBiserial'; ... %! 'RankBiserialCI'}); %! assert_equal (stats.signedrank, 40); %! assert_equal (isempty (stats.zval), true); %! assert_equal (p, 0.039062500000000, 1e-14); %!test %! ## the default method for a small sample is the exact one %! x = [1.83 0.50 1.62 2.48 1.68 1.88 1.55 3.06 1.30]; %! [~, ~, stats] = signrank (x, 1); %! assert_equal (fieldnames (stats), {'signedrank'; 'zval'; 'RankBiserial'; ... %! 'RankBiserialCI'}); %! assert_equal (stats.signedrank, 43); %! assert_equal (isempty (stats.zval), true); %!test %! ## identical inputs give a zero statistic and no z-value %! [p, h, stats] = signrank ([1 2 3], [1 2 3]); %! assert_equal (p, 1); %! assert_equal (fieldnames (stats), {'signedrank'; 'zval'; 'RankBiserial'; ... %! 'RankBiserialCI'}); %! assert_equal (stats.signedrank, 0); %! assert_equal (isempty (stats.zval), true); %!test %! load gradespaired.mat %! [p, h, stats] = signrank (gradespaired(:,1), ... %! gradespaired(:,2), 'tail', 'left'); %! assert_equal (p, 0.0047, 1e-4); %! assert_equal (h, true); %! assert_equal (stats.zval, -2.5982, 1e-4); %! assert_equal (stats.signedrank, 2017.5); %!test %! load ('gradespaired.mat'); %! [p, h, stats] = signrank (gradespaired(:,1), gradespaired(:,2), ... %! 'tail', 'left', 'method', 'exact'); %! assert_equal (p, 0.0045, 1e-4); %! assert_equal (h, true); %! assert_equal (isempty (stats.zval), true); %! assert_equal (stats.signedrank, 2017.5); %!test %! load mileage %! [p, h, stats] = signrank (mileage(:,2), 33); %! assert_equal (p, 0.0312, 1e-4); %! assert_equal (h, true); %! assert_equal (isempty (stats.zval), true); %! assert_equal (stats.signedrank, 21); %!test %! load mileage %! [p, h, stats] = signrank (mileage(:,2), 33, 'tail', 'right'); %! assert_equal (p, 0.0156, 1e-4); %! assert_equal (h, true); %! assert_equal (isempty (stats.zval), true); %! assert_equal (stats.signedrank, 21); %!test %! load mileage %! [p, h, stats] = signrank (mileage(:,2), 33, 'tail', 'right', ... %! 'alpha', 0.01, 'method', 'approximate'); %! assert_equal (p, 0.0180, 1e-4); %! assert_equal (h, false); %! assert_equal (stats.zval, 2.0966, 1e-4); %! assert_equal (stats.signedrank, 21); %!test %! x = [1, 2, 3, NaN, 4, 5]; %! p_clean = signrank ([1, 2, 3, 4, 5]); %! p_nan = signrank (x); %! assert_equal (p_nan, p_clean); %!test %! ## Differences equal to within the precision of the values they came from %! ## rank as tied, as MATLAB ranks them. Two of these sit 2 ulps apart. %! big = [2.1 3.4 1.2 5.6 4.3 2.2 6.7 3.3 4.4 5.5 1.1 2.9 3.8 4.9 5.1 6.2 ... %! 7.3 2.4]; %! [p, h, stats] = signrank (big, 3, 'method', 'approximate'); %! assert_equal (stats.signedrank, 132); %! assert_equal (stats.zval, 2.025571814690999, 1e-12); %!test %! ## A difference below that precision counts as no difference at all, and is %! ## dropped exactly as an exact zero is. Both forms measured against R2024a. %! x = [5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20]; %! y = x + 1; %! y(16) = x(16) + eps (x(16)); %! [p, h, stats] = signrank (x, y, 'method', 'approximate'); %! assert_equal (stats.signedrank, 0); %! assert_equal (stats.zval, -3.872983346207417, 1e-12); %! y(16) = x(16); %! [p2, h2, stats2] = signrank (x, y, 'method', 'approximate'); %! assert_equal (p2, p, 1e-15); %! assert_equal (stats2.zval, stats.zval, 1e-15); ## Effect size against the R package effectsize 1.0.3, rank_biserial %!test %! x = [1.83 0.50 1.62 2.48 1.68 1.88 1.55 3.06 1.30]; %! y = [0.878 0.647 0.598 2.05 1.06 1.29 1.06 3.14 1.29]; %! [~, ~, stats] = signrank (x, y); %! assert_equal (stats.RankBiserial, 0.77777777777777768, -1e-14); %! assert_equal (stats.RankBiserialCI, ... %! [0.29536312933624187, 0.94415590192008148], -1e-13); %!test %! x = [1.83 0.50 1.62 2.48 1.68 1.88 1.55 3.06 1.30]; %! y = [0.878 0.647 0.598 2.05 1.06 1.29 1.06 3.14 1.29]; %! [~, ~, stats] = signrank (x, y, 'alpha', 0.1, 'method', 'approximate'); %! assert_equal (stats.RankBiserial, 0.77777777777777768, -1e-14); %! assert_equal (stats.RankBiserialCI, ... %! [0.39915793850732861, 0.92978414736490256], -1e-13); %!test %! x = [1.83 0.50 1.62 2.48 1.68 1.88 1.55 3.06 1.30]; %! [~, ~, stats] = signrank (x, 1); %! assert_equal (stats.RankBiserial, 0.9111111111111112, -1e-14); %! assert_equal (stats.RankBiserialCI, ... %! [0.66333041160415562, 0.9788499892207112], -1e-12); %!test %! ## Three differences are zero, so the interval runs over six, where %! ## effectsize counts nine and gives [-0.4148, 0.7736] %! x = [1, 2, 2, 3, 5, 5, 4, 7, 6]; %! y = [1, 1, 3, 2, 4, 5, 6, 3, 6]; %! [~, ~, stats] = signrank (x, y); %! assert_equal (stats.RankBiserial, 0.28571428571428575, -1e-14); %! se = sqrt ((2 * 6 ^ 3 + 3 * 6 ^ 2 + 6) / 6) / 21; %! assert_equal (stats.RankBiserialCI, ... %! tanh (atanh (2 / 7) + [-1, 1] * norminv (0.975) * se), -1e-14); %!test %! [~, ~, stats] = signrank ([1, 2, 3], [1, 2, 3]); %! assert_equal (stats.RankBiserial, NaN); %! assert_equal (stats.RankBiserialCI, [NaN, NaN]); %!test %! rand ('state', 1); %! [~, ~, stats] = signrank ([1.2, 3.4, 2.2, 5.1, 0.3, 4.4, 2.8, 3.9], 2, ... %! 'ConfidenceIntervalType', 'bootstrap', ... %! 'NumBootstraps', 200); %! ci = stats.RankBiserialCI; %! assert_equal ([ci(1) <= ci(2), ci(1) >= -1, ci(2) <= 1], true (1, 3)); ## Test input validation %!error signrank (ones (2)) %!error ... %! signrank ([1, 2, 3, 4], ones (2)) %!error ... %! signrank ([1, 2, 3, 4], [1, 2, 3]) %!error ... %! signrank ([1, 2, 3, 4], [], 'tail') %!error ... %! signrank ([1, 2, 3, 4], [], 'alpha', 1.2) %!error ... %! signrank ([1, 2, 3, 4], [], 'alpha', 0) %!error ... %! signrank ([1, 2, 3, 4], [], 'alpha', -0.05) %!error ... %! signrank ([1, 2, 3, 4], [], 'alpha', 'a') %!error ... %! signrank ([1, 2, 3, 4], [], 'alpha', [0.01, 0.05]) %!error ... %! signrank ([1, 2, 3, 4], [], 'tail', 0.01) %!error ... %! signrank ([1, 2, 3, 4], [], 'tail', {'both'}) %!error ... %! signrank ([1, 2, 3, 4], [], 'tail', 'some') %!error ... %! signrank ([1, 2, 3, 4], [], 'method', 'exact', 'tail', 'some') %!error ... %! signrank ([1, 2, 3, 4], [], 'method', 0.01) %!error ... %! signrank ([1, 2, 3, 4], [], 'method', {'exact'}) %!error ... %! signrank ([1, 2, 3, 4], [], 'method', 'some') %!error ... %! signrank ([1, 2, 3, 4], [], 'tail', 'both', 'method', 'some') %!error ... %! signrank ([1, 2, 3], 0, 'ConfidenceIntervalType', 'none') %!error ... %! signrank ([1, 2, 3], 0, 'NumBootstraps', 2.5) statistics-release-2.1.2/inst/Hypothesis_Testing/signtest.m000066400000000000000000000301071527311104300241510ustar00rootroot00000000000000## Copyright (C) 2014 Tony Richardson ## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{pval} =} signtest (@var{x}) ## @deftypefnx {statistics} {@var{pval} =} signtest (@var{x}, @var{my}) ## @deftypefnx {statistics} {@var{pval} =} signtest (@var{x}, @var{my}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{pval}, @var{h}] =} signtest (@dots{}) ## @deftypefnx {statistics} {[@var{pval}, @var{h}, @var{stats}] =} signtest (@dots{}) ## ## Signed test for median. ## ## @code{@var{pval} = signtest (@var{x})} returns the @math{p}-value of a ## two-sided sign test. It tests the null hypothesis that data in @var{x} come ## from a distribution with zero median at the 5% significance level. @var{x} ## must be a vector. ## ## If the second argument @var{my} is a scalar, the null hypothesis is that ## @var{x} has median @var{my}, whereas if @var{my} is a vector, the null ## hypothesis is that the distribution of @code{@var{x} - @var{my}} has zero ## median. ## ## @code{@var{pval} = signtest (@dots{}, @var{Name}, @var{Value})} performs the ## Wilcoxon signed rank test with additional options specified by one or more of ## the following @var{Name}, @var{Value} pair arguments: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'alpha'} @tab A scalar value for the significance level of ## the test. Default is 0.05. ## ## @item @qcode{'tail'} @tab A character vector specifying the alternative ## hypothesis. It can take one of the following values: ## @end multitable ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Value} @tab @var{Description} ## ## @item @qcode{'both'} @tab For one-sample test (@var{my} is empty or a ## scalar), the data in @var{x} come from a continuous distribution with median ## different than zero or @var{my}. For two-sample test (@var{my} is a vector), ## the data in @qcode{@var{x} - @var{my}} come from a continuous distribution ## with median different than zero. ## ## @item @qcode{'left'} @tab For one-sample test (@var{my} is empty or a ## scalar), the data in @var{x} come from a continuous distribution with median ## less than zero or @var{my}. For two-sample test (@var{my} is a vector), the ## data in @qcode{@var{x} - @var{my}} come from a continuous distribution with ## median less than zero. ## ## @item @qcode{'right'} @tab For one-sample test (@var{my} is empty or a ## scalar), the data in @var{x} come from a continuous distribution with median ## greater than zero or @var{my}. For two-sample test (@var{my} is a vector), ## the data in @qcode{@var{x} - @var{my}} come from a continuous distribution ## with median greater than zero. ## @end multitable ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'method'} @tab A character vector specifying the method for ## computing the @math{p}-value. It can take one of the following values: ## @end multitable ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Value} @tab @var{Description} ## ## @item @qcode{'exact'} @tab Exact computation of the @math{p}-value. It ## is the default value for fewer than 100 observations when @qcode{'method'} is ## not specified. ## ## @item @qcode{'approximate'} @tab Using normal approximation for ## computing the @math{p}-value. It is the default value for 100 or more ## observations when @qcode{'method'} is not specified. ## @end multitable ## ## @code{[@var{pval}, @var{h}] = signtest (@dots{})} also returns a logical ## value indicating the test decision. If @var{h} is 0, the null hypothesis is ## accepted, whereas if @var{h} is 1, the null hypothesis is rejected. ## ## @code{[@var{pval}, @var{h}, @var{stats}] = signtest (@dots{})} also returns ## the structure @var{stats} containing the following fields: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Field} @tab @var{Value} ## @item @qcode{sign} @tab Value of the sign test statistic. ## ## @item @qcode{zval} @tab Value of the @math{z}-statistic, computed only ## when the @qcode{'method'} is @qcode{'approximate'} and @code{NaN} ## otherwise, where @code{signrank} and @code{ranksum} leave it empty. The ## field is always there. ## @end multitable ## ## @seealso{signrank, tiedrank, runstest} ## @end deftypefn function [p, h, stats] = signtest (x, my, varargin) ## Check X being a vector if (! isvector (x)) error ("signtest: X must be a vector."); endif ## Add defaults alpha = 0.05; tail = 'both'; if (numel (x) < 100) method = 'exact'; else method = 'approximate'; endif method_present = false; ## When called with a single input argument of second argument is empty if (nargin == 1 || isempty (my)) my = zeros (size (x)); endif ## If second argument is a scalar convert to vector or check for Y being a ## vector and that X and Y have equal lengths if (isscalar (my)) my = repmat (my, size (x)); elseif (! isvector (my)) error ("signtest: Y must be either a scalar of a vector."); elseif (numel (x) != numel (my)) error ("signtest: X and Y vectors have different lengths."); endif ## Get optional input arguments if (mod (numel (varargin), 2) != 0) error ("signtest: optional arguments must be in pairs."); endif while (numel (varargin) > 0) switch (lower (varargin{1})) case 'alpha' alpha = varargin{2}; case 'tail' tail = varargin{2}; case 'method' method = varargin{2}; method_present = true; otherwise error ("signtest: invalid Name argument."); endswitch varargin([1:2]) = []; endwhile ## Check values for optional input arguments if (! isnumeric (alpha) || isnan (alpha) || ! isscalar (alpha) ... || alpha <= 0 || alpha >= 1) error ("signtest: 'alpha' must be a numeric scalar in the range 0 to 1."); endif if (! ischar (tail)) error ("signtest: 'tail' argument must be a character vector."); elseif (sum (strcmpi (tail, {'both', 'right', 'left'})) != 1) error ("signtest: 'tail' value must be either 'both', right' or 'left'."); endif if (! ischar (method)) error ("signtest: 'method' argument must be a character vector."); elseif (sum (strcmpi (method, {'exact', 'approximate'})) != 1) error ("signtest: 'method' value must be either 'exact' or 'approximate'."); endif ## Calculate differences between X and Y vectors: remove equal values of NaNs XY_diff = x(:) - my(:); XY_diff(XY_diff == 0 | isnan (XY_diff)) = []; ## Recalculate remaining length of X vector (after equal or NaNs removal) n = length (XY_diff); ## Check for identical X and Y input arguments if (n == 0) p = 1; h = 0; stats.sign = 0; stats.zval = NaN; return; endif ## Re-evaluate method selection if (! method_present) if (n < 100) method = 'exact'; else method = 'approximate'; endif endif ## Get the number of positive and negative elements from X-Y differences pos_n = sum (XY_diff > 0); neg_n = n - pos_n; ## Set before the Z-statistic so the fields come in MATLAB's order stats.sign = pos_n; ## Calculate stats according to selected method and tail switch (lower (method)) case 'exact' switch (lower (tail)) case 'both' p = 2 * binocdf (min (neg_n, pos_n), n, 0.5); p = min (1, p); case 'left' p = binocdf (pos_n, n, 0.5); case 'right' p = binocdf (neg_n, n, 0.5); endswitch stats.zval = NaN; case 'approximate' switch (lower (tail)) case 'both' z_value = (pos_n - neg_n - sign (pos_n - neg_n)) / sqrt (n); p = 2 * normcdf (- abs (z_value)); case 'left' z_value = (pos_n - neg_n + 1) / sqrt (n); p = normcdf (z_value); case 'right' z_value = (pos_n - neg_n - 1) / sqrt (n); p = normcdf (- z_value); endswitch stats.zval = z_value; endswitch h = p <= alpha; endfunction ## Test output %!test %! [pval, h, stats] = signtest ([-ones(1, 1000) 1], 0, 'tail', 'left'); %! assert_equal (pval, 1.091701889420221e-218, 1e-14); %! assert_equal (h, true); %! assert_equal (stats.zval, -31.5437631079266, 1e-14); %!test %! [pval, h, stats] = signtest ([-2 -1 0 2 1 3 1], 0); %! assert_equal (pval, 0.6875000000000006, 1e-14); %! assert_equal (h, false); %! assert_equal (stats.zval, NaN); %! assert_equal (stats.sign, 4); %!test %! [pval, h, stats] = signtest ([-2 -1 0 2 1 3 1], 0, 'method', 'approximate'); %! assert_equal (pval, 0.6830913983096086, 1e-14); %! assert_equal (h, false); %! assert_equal (stats.zval, 0.4082482904638631, 1e-14); %! assert_equal (stats.sign, 4); %!test %! x = [1, 2, 3, 4, NaN, NaN, NaN]; %! [pval, h] = signtest (x); %! assert_equal (pval, 0.1250, 1e-4); %! assert_equal (h, false); %!test %! x = [1, 2, 3, 4, 5]; %! y = [1, 1, NaN, 5, 4]; %! [pval, h] = signtest (x, y); %! assert_equal (pval, 1.0, 1e-4); %! assert_equal (h, false); %!test %! x = [1, 2, 3, 4, 5, -1]; %! [p_val, ~] = signtest (x); %! [p, h, stats] = signtest (x, 0, 'alpha', p_val); %! assert_equal (h, true); %!test %! ## the decision is logical, as it is in MATLAB and in signrank and ranksum %! [~, h] = signtest ([1.2 2.3 0.5 3.1 2.2 1.8 0.9 2.7 1.1 3.3], 2); %! assert_equal (class (h), 'logical'); %! assert_equal (h, false); %!test %! [~, h] = signtest ([1.2 2.3 0.5 3.1 2.2 1.8 0.9 2.7 1.1 3.3], 100); %! assert_equal (class (h), 'logical'); %! assert_equal (h, true); ## Test input validation %!error signtest (ones (2)) %!error ... %! signtest ([1, 2, 3, 4], ones (2)) %!error ... %! signtest ([1, 2, 3, 4], [1, 2, 3]) %!error ... %! signtest ([1, 2, 3, 4], [], 'tail') %!error ... %! signtest ([1, 2, 3, 4], [], 'alpha', 1.2) %!error ... %! signtest ([1, 2, 3, 4], [], 'alpha', 0) %!error ... %! signtest ([1, 2, 3, 4], [], 'alpha', -0.05) %!error ... %! signtest ([1, 2, 3, 4], [], 'alpha', 'a') %!error ... %! signtest ([1, 2, 3, 4], [], 'alpha', [0.01, 0.05]) %!error ... %! signtest ([1, 2, 3, 4], [], 'tail', 0.01) %!error ... %! signtest ([1, 2, 3, 4], [], 'tail', {'both'}) %!error ... %! signtest ([1, 2, 3, 4], [], 'tail', 'some') %!error ... %! signtest ([1, 2, 3, 4], [], 'method', 'exact', 'tail', 'some') %!error ... %! signtest ([1, 2, 3, 4], [], 'method', 0.01) %!error ... %! signtest ([1, 2, 3, 4], [], 'method', {'exact'}) %!error ... %! signtest ([1, 2, 3, 4], [], 'method', 'some') %!error ... %! signtest ([1, 2, 3, 4], [], 'tail', 'both', 'method', 'some') statistics-release-2.1.2/inst/Hypothesis_Testing/swtest.m000066400000000000000000000415701527311104300236500ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} swtest (@var{x}) ## @deftypefnx {statistics} {@var{h} =} swtest (@var{x}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{h}, @var{p}] =} swtest (@dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{p}, @var{swstat}, @var{critval}] =} swtest (@dots{}) ## ## Shapiro-Wilk hypothesis test of composite normality. ## ## @code{@var{h} = swtest (@var{x})} performs the Shapiro-Wilk test of the null ## hypothesis that the sample in the vector @var{x} comes from a normal ## distribution with unknown mean and variance, against the alternative that it ## does not come from a normal distribution. The result @var{h} is 1 if the ## test rejects the null hypothesis at the 5% significance level, and 0 ## otherwise. @var{x} must be a vector of real values; @qcode{NaN} values are ## treated as missing and removed. ## ## The test statistic is ## @tex ## $$ W = \frac{\left( \sum_{i=1}^n a_i x_{(i)} \right)^2} ## {\sum_{i=1}^n (x_i - \bar{x})^2}, $$ ## ## @end tex ## @ifnottex ## @code{W = (sum (a .* sort (x)))^2 / sum ((x - mean (x)).^2)}, ## @end ifnottex ## where the weights @math{a}, applied to the sorted sample, are derived from ## the expected values of the order statistics of a standard normal sample of ## size @math{n}. @math{W} lies in ## @math{(0,1]}, and small values of @math{W} are evidence against normality. ## ## The following @qcode{Name-Value} pairs are supported: ## ## @multitable @columnfractions 0.2 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab The significance level, a scalar in the range ## @math{(0,1)}. The default is @math{0.05}. ## ## @item @qcode{'Method'} @tab The test to perform: ## @qcode{'shapiro-wilk'} (default) or @qcode{'shapiro-francia'}. ## @end multitable ## ## @qcode{'shapiro-wilk'} computes the weights and the p-value with Royston's ## algorithm AS R94, which approximates the weights of Shapiro and Wilk and ## transforms @math{W} to a normal deviate. It is the algorithm used by R's ## @code{shapiro.test}, and it accepts samples of 3 to 5000 values. For ## @math{n = 3} the p-value is exact. ## ## @qcode{'shapiro-francia'} performs the Shapiro-Francia test instead, whose ## statistic @math{W'} is the squared correlation between the sorted sample and ## the approximate expected normal order statistics ## @code{norminv (((1:n) - 3/8) / (n + 1/4))}. Its p-value is Royston's ## normal approximation for @math{W'}, as in @code{sf.test} of R's ## @code{nortest} package, and it accepts samples of 5 to 5000 values. The ## Shapiro-Francia test is known to be more powerful than the Shapiro-Wilk test ## against leptokurtic alternatives. The test is never selected automatically; ## the method used is the one requested. ## ## @code{[@var{h}, @var{p}] = swtest (@dots{})} also returns the p-value ## @var{p} of the test, the probability of observing a statistic as small as ## @var{swstat} under the null hypothesis. ## ## @code{[@var{h}, @var{p}, @var{swstat}, @var{critval}] = swtest (@dots{})} ## also returns the test statistic @var{swstat}, @math{W} or @math{W'}, and the ## critical value @var{critval} at significance level @var{alpha}, obtained by ## inverting the same approximation. The null hypothesis is rejected when ## @code{@var{swstat} < @var{critval}}, which is the same as ## @code{@var{p} < @var{alpha}}. ## ## A sample whose values are all equal has no defined statistic and is refused, ## as is a sample holding an infinite value. Samples of more than 5000 values ## are refused, since Royston's approximations are calibrated up to that size; ## use @code{adtest} or @code{jbtest} for larger samples. ## ## MATLAB has no Shapiro-Wilk test, so @code{swtest} is specific to Octave. ## ## References: ## @enumerate ## @item ## S. S. Shapiro and M. B. Wilk. An analysis of variance test for normality ## (complete samples). @emph{Biometrika}, 52(3-4):591--611, 1965. ## @item ## S. S. Shapiro and R. S. Francia. An approximate analysis of variance test ## for normality. @emph{Journal of the American Statistical Association}, ## 67(337):215--216, 1972. ## @item ## P. Royston. A pocket-calculator algorithm for the Shapiro-Francia test for ## non-normality: an application to medicine. @emph{Statistics in Medicine}, ## 12(2):181--184, 1993. ## @item ## P. Royston. Remark AS R94: a remark on algorithm AS 181: the W-test for ## normality. @emph{Applied Statistics}, 44(4):547--551, 1995. ## @end enumerate ## ## @seealso{adtest, jbtest, kstest, lillietest} ## @end deftypefn function [h, p, swstat, critval] = swtest (x, varargin) ## Input validation if (nargin < 1) print_usage (); endif if (! (isnumeric (x) && isreal (x) && isvector (x))) error ("swtest: X must be a vector of real values."); endif ## Parse optional Name-Value paired arguments optNames = {'Alpha', 'Method'}; dfValues = {0.05, 'shapiro-wilk'}; [alpha, method, args] = parsePairedArguments (optNames, dfValues, ... varargin(:)); if (! isempty (args)) error (strcat ("swtest: optional arguments must be 'Alpha' or", ... " 'Method' Name-Value pairs.")); endif if (! (isnumeric (alpha) && isscalar (alpha) && isreal (alpha) ... && alpha > 0 && alpha < 1)) error ("swtest: 'Alpha' must be a scalar in the range (0,1)."); endif if (isstring (method) && isscalar (method)) method = char (method); endif if (! (ischar (method) && isrow (method) ... && any (strcmpi (method, {'shapiro-wilk', 'shapiro-francia'})))) error (strcat ("swtest: 'Method' must be 'shapiro-wilk' or", ... " 'shapiro-francia'.")); endif francia = strcmpi (method, 'shapiro-francia'); ## Remove missing values and check the sample x = sort (double (x(! isnan (x))(:))); n = numel (x); if (any (isinf (x))) error ("swtest: X must not contain infinite values."); endif if (francia && n < 5) error (strcat ("swtest: X must contain at least five non-missing", ... " values for the Shapiro-Francia test.")); elseif (n < 3) error ("swtest: X must contain at least three non-missing values."); endif if (n > 5000) error ("swtest: X must contain at most 5000 non-missing values."); endif if (x(n) == x(1)) error ("swtest: X must not be constant."); endif ## Scale by the range and centre the sample x = x / (x(n) - x(1)); x = x - mean (x); if (francia) ## Approximate expected normal order statistics (Blom's scores) a = norminv (((1:n)' - 0.375) / (n + 0.25)); else a = swtest_weights_ (n); endif ## 1 - W as the squared sine of the angle between A and X, which keeps its ## precision when W is close to 1. It is clamped at 0 because rounding can ## take it just below. ssa = sumsq (a); ssx = sumsq (x); sax = a' * x; ssassx = sqrt (ssa * ssx); w1 = max ((ssassx - sax) * (ssassx + sax) / (ssa * ssx), 0); swstat = 1 - w1; if (francia) ## Royston (1993): log (1 - W') is approximately normal u = log (n); v = log (u); mu = -1.2725 + 1.0521 * (v - u); sigma = 1.0308 - 0.26758 * (v + 2 / u); p = normcdf ((log (w1) - mu) / sigma, 'upper'); critval = -expm1 (mu - sigma * norminv (alpha)); elseif (n == 3) ## Exact null distribution for three values p = min (max (6 / pi * (asin (sqrt (swstat)) - pi / 3), 0), 1); critval = sin (pi / 3 + alpha * pi / 6) ^ 2; elseif (n <= 11) ## Royston (1995): -log (gamma - log (1 - W)) is approximately normal. ## GMA exceeds log (1 - W) for every attainable W. gma = polyval ([0.459, -2.273], n); mu = polyval ([-6.714e-4, 0.025054, -0.39978, 0.544], n); sigma = exp (polyval ([-0.0020322, 0.062767, -0.77857, 1.3822], n)); p = normcdf ((-log (gma - log (w1)) - mu) / sigma, 'upper'); critval = -expm1 (gma - exp (-(mu - sigma * norminv (alpha)))); else ## Royston (1995): log (1 - W) is approximately normal u = log (n); mu = polyval ([0.0038915, -0.083751, -0.31082, -1.5861], u); sigma = exp (polyval ([0.0030302, -0.082676, -0.4803], u)); p = normcdf ((log (w1) - mu) / sigma, 'upper'); critval = -expm1 (mu - sigma * norminv (alpha)); endif h = double (swstat < critval); endfunction ## Shapiro-Wilk weights for a sample of size N by Royston's approximation ## (algorithm AS R94), returned antisymmetric and ordered for sorted data. function a = swtest_weights_ (n) nh = floor (n / 2); if (n == 3) ah = sqrt (0.5); else m = norminv (((1:nh)' - 0.375) / (n + 0.25)); summ2 = 2 * sumsq (m); ssumm2 = sqrt (summ2); rsn = 1 / sqrt (n); a1 = polyval ([-2.706056, 4.434685, -2.07119, -0.147981, 0.221157, 0], ... rsn) - m(1) / ssumm2; if (n > 5) a2 = polyval ([-3.582633, 5.682633, -1.752461, -0.293762, 0.042981, ... 0], rsn) - m(2) / ssumm2; fac = sqrt ((summ2 - 2 * m(1) ^ 2 - 2 * m(2) ^ 2) ... / (1 - 2 * a1 ^ 2 - 2 * a2 ^ 2)); ah = [a1; a2; -m(3:nh) / fac]; else fac = sqrt ((summ2 - 2 * m(1) ^ 2) / (1 - 2 * a1 ^ 2)); ah = [a1; -m(2:nh) / fac]; endif endif a = zeros (n, 1); a(1:nh) = -ah; a(n:-1:n-nh+1) = ah; endfunction %!demo %! ## Test whether a sample departs from normality %! x = [148 154 158 160 161 162 166 170 182 195 236]; %! [h, p, W] = swtest (x) %!demo %! ## The Shapiro-Francia test on the same sample %! x = [148 154 158 160 161 162 166 170 182 195 236]; %! [h, p, W] = swtest (x, 'Method', 'shapiro-francia') ## Test output ## W and P checked against shapiro.test in R 4.3.3 (2024-02-29) %!test # Shapiro and Wilk's example, n <= 11 %! x = [148 154 158 160 161 162 166 170 182 195 236]; %! [h, p, W, c] = swtest (x); %! assert_equal (h, 1); %! assert_equal (p, 0.00670381405650293, 1e-12); %! assert_equal (W, 0.788814694835387, 1e-12); %! assert_equal (c, 0.855278601529214, 1e-12); %!test # n > 11 %! [h, p, W, c] = swtest (log (1:25)); %! assert_equal (h, 1); %! assert_equal (p, 0.00741545379169184, 1e-12); %! assert_equal (W, 0.881481586050452, 1e-12); %! assert_equal (c, 0.91953513814811, 1e-12); %!test # n = 5, two approximated weights %! [h, p, W, c] = swtest ([2 3 5 8 13]); %! assert_equal (h, 0); %! assert_equal (p, 0.534654754257205, 1e-12); %! assert_equal (W, 0.920729192441235, 1e-12); %! assert_equal (c, 0.775099786396077, 1e-12); %!test # n = 4 %! [h, p, W, c] = swtest ([1 2 4 8]); %! assert_equal (h, 0); %! assert_equal (p, 0.53808377727497, 1e-12); %! assert_equal (W, 0.92020267879194, 1e-12); %! assert_equal (c, 0.762289320516799, 1e-12); %!test # n = 3, exact null distribution %! [h, p, W, c] = swtest ([1 2 4]); %! assert_equal (h, 0); %! assert_equal (W, 27 / 28, 1e-14); %! assert_equal (p, 6 / pi * (asin (sqrt (27 / 28)) - pi / 3), 1e-14); %! assert_equal (c, sin (pi / 3 + pi / 120) ^ 2, 1e-14); ## R 4.3.3 gives P = 0.99999999999999334 here, its 6/pi and pi/3 being ## written to 15 digits %!test # three equally spaced values fit a normal sample exactly %! [h, p, W] = swtest ([1 2 3]); %! assert_equal (h, 0); %! assert_equal (p, 1); %! assert_equal (W, 1); ## W' and P checked in R 4.3.3 (2024-02-29) against the formula of sf.test ## from the nortest package, which was not installed %!test # Shapiro-Francia, n <= 11 %! x = [148 154 158 160 161 162 166 170 182 195 236]; %! [h, p, W, c] = swtest (x, 'Method', 'shapiro-francia'); %! assert_equal (h, 1); %! assert_equal (p, 0.00734764001456067, 1e-12); %! assert_equal (W, 0.771381939646386, 1e-12); %! assert_equal (c, 0.855095971555222, 1e-12); %!test # Shapiro-Francia, n > 11 %! [h, p, W, c] = swtest (log (1:25), 'Method', 'shapiro-francia'); %! assert_equal (h, 1); %! assert_equal (p, 0.00991400561293846, 1e-12); %! assert_equal (W, 0.882794840134263, 1e-12); %! assert_equal (c, 0.919670595475726, 1e-12); %!test # Shapiro-Francia, n = 5 %! [h, p, W, c] = swtest ([2 3 5 8 13], 'Method', 'shapiro-francia'); %! assert_equal (h, 0); %! assert_equal (p, 0.592971063346381, 1e-12); %! assert_equal (W, 0.925681475433048, 1e-12); %! assert_equal (c, 0.782592834233546, 1e-12); %!test # Shapiro-Francia, n = 200, measured with R's nortest sf.test %! [h, p, W] = swtest (sqrt (1:200), 'Method', 'shapiro-francia'); %! assert_equal (h, 1); %! assert_equal (p, 8.0650594412092e-06, -1e-10); %! assert_equal (W, 0.950552730992928, -1e-10); %!test # Shapiro-Francia on a strong skew, measured with R's nortest sf.test %! [h, p, W] = swtest (exp ((1:20) / 5), 'Method', 'shapiro-francia'); %! assert_equal (h, 1); %! assert_equal (p, 0.00305794323924647, -1e-10); %! assert_equal (W, 0.824238460116288, -1e-10); %!test # Shapiro-Francia on bounded data, measured with R's nortest sf.test %! [h, p, W] = swtest (sin (1:30), 'Method', 'shapiro-francia'); %! assert_equal (h, 1); %! assert_equal (p, 0.018296975230196, -1e-10); %! assert_equal (W, 0.911248253555032, -1e-10); %!test # Alpha sets the critical value and leaves P unchanged %! [h, p, W, c] = swtest (log (1:25), 'Alpha', 0.01); %! assert_equal (h, 1); %! assert_equal (p, 0.00741545379169184, 1e-12); %! assert_equal (c, 0.887697842838192, 1e-12); %!test # Alpha decides H %! assert_equal (swtest ([2 3 5 8 13], 'Alpha', 0.6), 1); %!test # Method is case insensitive %! [~, p] = swtest (log (1:25), 'Method', 'Shapiro-Francia'); %! assert_equal (p, 0.00991400561293846, 1e-12); %!test # Method as a string scalar %! [~, p] = swtest (log (1:25), 'Method', string ('shapiro-francia')); %! assert_equal (p, 0.00991400561293846, 1e-12); %!test # NaNs are removed %! [h, p, W, c] = swtest ([NaN, log(1:25), NaN]); %! assert_equal ([h, p, W, c], ... %! [1, 0.00741545379169184, 0.881481586050452, ... %! 0.91953513814811], 1e-12); %!test # a column vector gives the same result %! [h, p, W, c] = swtest (log (1:25)'); %! assert_equal ([h, p, W, c], ... %! [1, 0.00741545379169184, 0.881481586050452, ... %! 0.91953513814811], 1e-12); %!test # unsorted data gives the same result %! [h, p, W, c] = swtest (fliplr (log (1:25))); %! assert_equal ([h, p, W, c], ... %! [1, 0.00741545379169184, 0.881481586050452, ... %! 0.91953513814811], 1e-12); %!test # integer data is tested as double %! [~, p, W] = swtest (int16 ([148 154 158 160 161 162 166 170 182 195 236])); %! assert_equal (p, 0.00670381405650293, 1e-12); %! assert_equal (W, 0.788814694835387, 1e-12); %!test # single data is tested as double %! [~, p, W] = swtest (single ([148 154 158 160 161 162 166 170 182 195 236])); %! assert_equal (p, 0.00670381405650293, 1e-12); %! assert_equal (W, 0.788814694835387, 1e-12); %!test # a sample of 5000 values is accepted %! assert_equal (swtest (1:5000), 1); ## Test input validation %!error swtest () %!error swtest (ones (3, 3)) %!error swtest ({1, 2, 3}) %!error swtest ([1 2 3i]) %!error swtest ('abcde') %!error ... %! swtest (1:10, 0.01) %!error ... %! swtest (1:10, 'Tail', 'left') %!error ... %! swtest (1:10, 'Alpha', 0) %!error ... %! swtest (1:10, 'Alpha', 1) %!error ... %! swtest (1:10, 'Alpha', [0.01 0.05]) %!error ... %! swtest (1:10, 'Alpha', '0.05') %!error ... %! swtest (1:10, 'Method', 'sf') %!error ... %! swtest (1:10, 'Method', 5) %!error swtest ([1 2 3 Inf]) %!error ... %! swtest (1:4, 'Method', 'shapiro-francia') %!error ... %! swtest ([1 2 NaN]) %!error ... %! swtest (1:5001) %!error swtest (ones (1, 10)) statistics-release-2.1.2/inst/Hypothesis_Testing/ttest.m000066400000000000000000000242151527311104300234570ustar00rootroot00000000000000## Copyright (C) 2014 Tony Richardson ## Copyright (C) 2022 Andrew Penn ## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{h}, @var{pval}, @var{ci}, @var{stats}] =} ttest (@var{x}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}, @var{ci}, @var{stats}] =} ttest (@var{x}, @var{m}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}, @var{ci}, @var{stats}] =} ttest (@var{x}, @var{y}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}, @var{ci}, @var{stats}] =} ttest (@var{x}, @var{m}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}, @var{ci}, @var{stats}] =} ttest (@var{x}, @var{y}, @var{Name}, @var{Value}) ## ## Test for mean of a normal sample with unknown variance. ## ## Perform a t-test of the null hypothesis @code{mean (@var{x}) == ## @var{m}} for a sample @var{x} from a normal distribution with unknown ## mean and unknown standard deviation. Under the null, the test statistic ## @var{t} has a Student's t distribution. The default value of ## @var{m} is 0. ## ## If the second argument @var{y} is a vector, a paired-t test of the ## hypothesis @code{mean (@var{x}) = mean (@var{y})} is performed. If @var{x} ## and @var{y} are vectors, they must have the same size and dimensions. ## ## @var{x} (and @var{y}) can also be matrices. For matrices, @qcode{ttest} ## performs separate t-tests along each column, and returns a vector of results. ## @var{x} and @var{y} must have the same number of columns. The Type I error ## rate of the resulting vector of @var{pval} can be controlled by entering ## @var{pval} as input to the function @qcode{multcompare}. ## ## @qcode{ttest} treats NaNs as missing values, and ignores them. ## ## Name-Value pair arguments can be used to set various options. ## @qcode{'alpha'} can be used to specify the significance level ## of the test (the default value is 0.05). @qcode{'tail'}, can be used ## to select the desired alternative hypotheses. If the value is ## @qcode{'both'} (default) the null is tested against the two-sided ## alternative @code{mean (@var{x}) != @var{m}}. ## If it is @qcode{'right'} the one-sided alternative @code{mean (@var{x}) ## > @var{m}} is considered. Similarly for @qcode{'left'}, the one-sided ## alternative @code{mean (@var{x}) < @var{m}} is considered. ## When argument @var{x} is a matrix, @qcode{'dim'} can be used to select ## the dimension over which to perform the test. (The default is the ## first non-singleton dimension). ## ## If @var{h} is 1 the null hypothesis is rejected, meaning that the tested ## sample does not come from a Student's t distribution. If @var{h} is 0, then ## the null hypothesis cannot be rejected and it can be assumed that @var{x} ## follows a Student's t distribution. The p-value of the test is returned in ## @var{pval}. A 100(1-alpha)% confidence interval is returned in @var{ci}. ## ## @var{stats} is a structure containing the value of the test statistic ## (@var{tstat}), the degrees of freedom (@var{df}) and the sample's standard ## deviation (@var{sd}). ## ## @seealso{hotelling_t2test, ttest2, hotelling_t2test2} ## @end deftypefn function [h, p, ci, stats] = ttest (x, my, varargin) ## Set default arguments my_default = 0; alpha = 0.05; tail = 'both'; ## Find the first non-singleton dimension of x dim = min (find (size (x) != 1)); if (isempty (dim)) dim = 1; endif if (nargin == 1) my = my_default; endif i = 1; while (i <= length (varargin)) switch lower (varargin{i}) case 'alpha' i = i + 1; alpha = varargin{i}; if (! (isscalar (alpha) && isnumeric (alpha) && isreal (alpha) && alpha > 0 && alpha < 1)) error ("ttest: ALPHA must be a scalar between 0 and 1."); endif case 'tail' i = i + 1; tail = varargin{i}; case 'dim' i = i + 1; dim = varargin{i}; otherwise error ("ttest: Invalid Name argument."); endswitch i = i + 1; endwhile if (! isa (tail, 'char')) error ("ttest: tail argument must be a string."); endif if (any (and (! isscalar (my), size (x) != size (my)))) error ("ttest: Arrays in paired test must be the same size."); endif ## Set default values if arguments are present but empty if (isempty (my)) my = my_default; endif ## This adjustment allows everything else to remain the ## same for both the one-sample t test and paired tests. x = x - my; if (! isscalar (my)) my = 0; endif ## Calculate the test statistic value (tval) n = sum (! isnan (x), dim); x_bar = mean (x, dim, 'omitnan'); stats.tstat = []; stats.df = n - 1; stats.sd = std (x, 0, dim, 'omitnan'); x_bar_std = stats.sd ./ sqrt (n); tval = (x_bar) ./ x_bar_std; stats.tstat = tval; ## Based on the "tail" argument determine the P-value, the critical values, ## and the confidence interval. switch lower (tail) case 'both' p = 2 * tcdf (-abs (tval), n - 1); tcrit = - tinv (alpha / 2, n - 1); ci = [x_bar-tcrit.*x_bar_std; x_bar+tcrit.*x_bar_std] + my; case 'left' p = tcdf (tval, n - 1); tcrit = - tinv (alpha, n - 1); ci = [-inf*ones(size(x_bar)); my+x_bar+tcrit.*x_bar_std]; case 'right' p = tcdf (tval, n - 1, 'upper'); tcrit = - tinv (alpha, n - 1); ci = [my+x_bar-tcrit.*x_bar_std; inf*ones(size(x_bar))]; otherwise error ("ttest: Invalid value for tail argument."); endswitch ## Reshape the ci array to match MATLAB shaping if (isscalar (x_bar) && dim == 2) ci = ci(:)'; elseif (size (x_bar, 2) < size (x_bar, 1)) ci = reshape (ci(:), length (x_bar), 2); endif ## Determine the test outcome ## MATLAB returns this a double instead of a logical array h = double (p < alpha); endfunction %!test %! x = 8:0.1:12; %! [h, pval, ci] = ttest (x, 10); %! assert_equal (h, 0) %! assert_equal (pval, 1, 10*eps) %! assert_equal (ci, [9.6219 10.3781], 1E-5) %! [h, pval, ci0] = ttest (x, 0); %! assert_equal (h, 1) %! assert_equal (pval, 7.99598458174846e-39, -1e-13) # MATLAB R2024a %! assert_equal (ci0, ci, 2e-15) %! [h, pval, ci] = ttest (x, 10, 'tail', 'right', 'dim', 2, 'alpha', 0.05); %! assert_equal (h, 0) %! assert_equal (pval, 0.5, 10*eps) %! assert_equal (ci, [9.68498 Inf], 1E-5) %!test %! ## Below the resolution of 1 - tcdf, values from MATLAB R2024a %! [~, pval] = ttest (10 + sin (1:30)'); %! assert_equal (pval, 9.53562250074996e-35, -1e-13); %!test %! [~, pval] = ttest (10 + sin (1:30)', 0, 'tail', 'right'); %! assert_equal (pval, 4.76781125037498e-35, -1e-13); %!error ttest ([8:0.1:12], 10, 'tail', 'invalid'); %!error ttest ([8:0.1:12], 10, 'tail', 25); ## Reference values from MATLAB R2024a (probe run 2026-08-02). Only the ## two-tailed default and 'tail','right' at the default alpha were covered ## before, so the left tail, a non-default alpha, the stats output and the ## 'dim' option went unchecked. %!shared x, xm %! x = [10.2 9.7 11.1 10.5 9.9 10.8 10.1 9.6 10.4 10.7]; %! xm = [10.2 11.4; 9.7 10.9; 11.1 12.6; 10.5 11.1; 9.9 13.2]; %!test %! [h, p, ci] = ttest (x, 10, 'Tail', 'left'); %! assert_equal (h, 0); %! assert_equal (p, 0.957607393606414, 1e-14); %! assert_equal (ci, [-Inf, 10.583984634395803], 1e-13); %!test %! [h, p, ci] = ttest (x, 10, 'Tail', 'right'); %! assert_equal (h, 1); %! assert_equal (p, 0.042392606393586, 1e-14); %! assert_equal (ci, [10.016015365604199, Inf], 1e-13); %!test %! ## a non-default alpha must actually widen the interval %! [~, p1, ci1] = ttest (x, 10); %! [~, p2, ci2] = ttest (x, 10, 'Alpha', 0.01); %! assert_equal (ci1, [9.949548119279150, 10.650451880720851], 1e-13); %! assert_equal (ci2, [9.796537642780031, 10.803462357219971], 1e-13); %! assert_equal (diff (ci2) > diff (ci1), true); %! assert_equal (p1, p2, 0); %!test %! ## the fourth output was never exercised %! [~, ~, ~, stats] = ttest (x, 10); %! assert_equal (stats.tstat, 1.936491673103713, 1e-13); %! assert_equal (stats.df, 9); %! assert_equal (stats.sd, 0.489897948556636, 1e-14); %!test %! [h, p] = ttest (xm, 10, 'Dim', 1); %! assert_equal (h, [0, 1]); %! assert_equal (p, [0.318172286240873, 0.015001169519765], 1e-13); %!test %! [h, p] = ttest (xm, 10, 'Dim', 2); %! assert_equal (h(:)', [0, 0, 0, 0, 0]); %! assert_equal (p(:)', [0.409665529398267, 0.704832764699133, ... %! 0.245198884026780, 0.228400502439816, ... %! 0.519887895647178], 1e-13); ## ALPHA was not validated at all, so a negative one silently produced a ## [NaN NaN] confidence interval and "do not reject", and a vector one ## quietly vectorised into an undocumented signature. MATLAB refuses each of ## these with "ALPHA must be a scalar between 0 and 1." %!error ... %! ttest ([8:0.1:12], 10, 'Alpha', -0.05); %!error ... %! ttest ([8:0.1:12], 10, 'Alpha', 0); %!error ... %! ttest ([8:0.1:12], 10, 'Alpha', 1); %!error ... %! ttest ([8:0.1:12], 10, 'Alpha', 1.5); %!error ... %! ttest ([8:0.1:12], 10, 'Alpha', [0.01, 0.05]); %!error ... %! ttest ([8:0.1:12], 10, 'Alpha', 'a'); %!error ... %! ttest ([8:0.1:12], 10, 'Alpha', NaN); %!error ... %! ttest ([8:0.1:12], 10, 'Alpha', 2 + 1i); statistics-release-2.1.2/inst/Hypothesis_Testing/ttest2.m000066400000000000000000000255011527311104300235400ustar00rootroot00000000000000## Copyright (C) 2014 Tony Richardson ## Copyright (C) 2022 Andrew Penn ## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{h}, @var{pval}, @var{ci}, @var{stats}] =} ttest2 (@var{x}, @var{y}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}, @var{ci}, @var{stats}] =} ttest2 (@var{x}, @var{y}, @var{Name}, @var{Value}) ## ## Perform a t-test to compare the means of two groups of data under the null ## hypothesis that the groups are drawn from distributions with the same mean. ## ## @var{x} and @var{y} can be vectors or matrices. For matrices, @qcode{ttest2} ## performs separate t-tests along each column, and returns a vector of results. ## @var{x} and @var{y} must have the same number of columns. The Type I error ## rate of the resulting vector of @var{pval} can be controlled by entering ## @var{pval} as input to the function @qcode{multcompare}. ## ## @qcode{ttest2} treats NaNs as missing values, and ignores them. ## ## For a nested t-test, use @qcode{anova2}. ## ## The argument @qcode{'alpha'} can be used to specify the significance level ## of the test (the default value is 0.05). The string argument @qcode{'tail'}, ## can be used to select the desired alternative hypotheses. If @qcode{'tail'} ## is @qcode{'both'} (default) the null is tested against the two-sided ## alternative @code{mean (@var{x}) != @var{m}}. If @qcode{'tail'} is ## @qcode{'right'} the one-sided alternative @code{mean (@var{x}) > @var{m}} is ## considered. Similarly for @qcode{'left'}, the one-sided alternative ## @code{mean (@var{x}) < @var{m}} is considered. ## ## When @qcode{'vartype'} is @qcode{'equal'} the variances are assumed to be ## equal (this is the default). When @qcode{'vartype'} is @qcode{'unequal'} the ## variances are not assumed equal. ## ## When argument @var{x} and @var{y} are matrices the @qcode{'dim'} argument can ## be used to select the dimension over which to perform the test. ## (The default is the first non-singleton dimension.) ## ## If @var{h} is 0 the null hypothesis is accepted, if it is 1 the null ## hypothesis is rejected. The p-value of the test is returned in @var{pval}. ## A 100(1-alpha)% confidence interval is returned in @var{ci}. @var{stats} ## is a structure containing the value of the test statistic (@var{tstat}), ## the degrees of freedom (@var{df}) and the sample standard deviation ## (@var{sd}). ## ## @seealso{hotelling_t2test, anova1, hotelling_t2test2, ttest} ## @end deftypefn function [h, p, ci, stats] = ttest2 (x, y, varargin) ## Set defaults alpha = 0.05; tail = 'both'; vartype = 'equal'; ## Find the first non-singleton dimension of x dim = min (find (size (x) != 1)); if (isempty (dim)) dim = 1; endif ## Evaluate optional input arguments i = 1; while ( i <= length (varargin) ) switch lower (varargin{i}) case 'alpha' i = i + 1; alpha = varargin{i}; if (! (isscalar (alpha) && isnumeric (alpha) && isreal (alpha) && alpha > 0 && alpha < 1)) error ("ttest2: ALPHA must be a scalar between 0 and 1."); endif case 'tail' i = i + 1; tail = varargin{i}; case 'vartype' i = i + 1; vartype = varargin{i}; case 'dim' i = i + 1; dim = varargin{i}; otherwise error ("ttest2: Invalid Name argument."); endswitch i = i + 1; endwhile ## Error checking if (! isa (tail, 'char')) error ("ttest2: tail argument must be a string."); endif if (size (x, abs (dim - 3)) != size (y, abs (dim - 3))) error ("ttest2: The data in a 2-sample t-test must be commensurate") endif ## Calculate mean, variance and size of each sample m = sum (! isnan (x), dim); n = sum (! isnan (y), dim); x_bar = mean (x, dim, 'omitnan') - mean (y, dim, 'omitnan'); s1_var = var (x, 0, dim, 'omitnan'); s2_var = var (y, 0, dim, 'omitnan'); ## Perform test-specific calculations switch lower (vartype) case 'equal' stats.tstat = []; stats.df = (m + n - 2); sp_var = ((m - 1) .* s1_var + (n - 1) .* s2_var) ./ stats.df; stats.sd = sqrt (sp_var); x_bar_std = sqrt (sp_var .* (1 ./ m + 1 ./ n)); n_sd = 1; case 'unequal' stats.tstat = []; se1 = sqrt (s1_var ./ m); se2 = sqrt (s2_var ./ n); sp_var = s1_var ./ m + s2_var ./ n; stats.df = ((se1 .^ 2 + se2 .^ 2) .^ 2 ./ ... (se1 .^ 4 ./ (m - 1) + se2 .^ 4 ./ (n - 1))); stats.sd = [sqrt(s1_var); sqrt(s2_var)]; x_bar_std = sqrt (sp_var); n_sd = 2; otherwise error ("ttest2: Invalid value for vartype argument."); endswitch stats.tstat = x_bar ./ x_bar_std; ## Based on the "tail" argument determine the P-value, the critical values, ## and the confidence interval. switch lower (tail) case 'both' p = 2 * tcdf (-abs (stats.tstat), stats.df); tcrit = - tinv (alpha / 2, stats.df); ci = [x_bar-tcrit.*x_bar_std; x_bar+tcrit.*x_bar_std]; case 'left' p = tcdf (stats.tstat, stats.df); tcrit = - tinv (alpha, stats.df); ci = [-inf*ones(size(x_bar)); x_bar+tcrit.*x_bar_std]; case 'right' p = tcdf (stats.tstat, stats.df, 'upper'); tcrit = - tinv (alpha, stats.df); ci = [x_bar-tcrit.*x_bar_std; inf*ones(size(x_bar))]; otherwise error ("ttest2: Invalid value for tail argument."); endswitch ## Reshape the ci array to match MATLAB shaping if (isscalar (x_bar) && dim == 2) ci = ci(:)'; stats.sd = stats.sd(:)'; elseif (size (x_bar, 2) < size (x_bar, 1)) ci = reshape (ci(:), length (x_bar), 2); stats.sd = reshape (stats.sd(:), length (x_bar), n_sd); endif ## Determine the test outcome ## MATLAB returns this a double instead of a logical array h = double (p < alpha); endfunction %!test %! a = 1:5; %! b = 6:10; %! b(5) = NaN; %! [h,p,ci,stats] = ttest2 (a,b); %! assert_equal (h, 1); %! assert_equal (p, 0.002535996080258229, 1e-14); %! assert_equal (ci, [-6.822014919225481, -2.17798508077452], 1e-14); %! assert_equal (stats.tstat, -4.582575694955839, 1e-14); %! assert_equal (stats.df, 7); %! assert_equal (stats.sd, 1.4638501094228, 1e-13); %!test %! ## Below the resolution of 1 - tcdf, values from MATLAB R2024a %! x = 10 + sin (1:30)'; %! [~, pval] = ttest2 (x, x - 20); %! assert_equal (pval, 4.62837535481253e-68, -1e-13); %!test %! x = 10 + sin (1:30)'; %! [~, pval] = ttest2 (x, x - 20, 'tail', 'right'); %! assert_equal (pval, 2.31418767740626e-68, -1e-13); %!error ttest2 ([8:0.1:12], [8:0.1:12], 'tail', 'invalid'); %!error ttest2 ([8:0.1:12], [8:0.1:12], 'tail', 25); ## Reference values from MATLAB R2024a (probe run 2026-08-02). The single ## existing block covered only the two-sample default, leaving both 'Vartype' ## values, both one-sided tails, 'alpha' and 'dim' unchecked. 'Vartype' ## matters most: 'unequal' is Welch's test, with its own denominator and a ## non-integer degrees of freedom. %!shared x, y, xm %! x = [10.2 9.7 11.1 10.5 9.9 10.8 10.1 9.6 10.4 10.7]; %! y = [11.4 10.9 12.6 11.1 13.2 10.4 12.8 11.7]; %! xm = [10.2 11.4; 9.7 10.9; 11.1 12.6; 10.5 11.1; 9.9 13.2]; %!test %! [h, p, ci, stats] = ttest2 (x, y, 'Vartype', 'equal'); %! assert_equal (h, 1); %! assert_equal (p, 8.895270853487265e-04, 1e-15); %! assert_equal (ci, [-2.224122032184765, -0.700877967815235], 1e-13); %! assert_equal (stats.tstat, -4.070735048482627, 1e-13); %! assert_equal (stats.df, 16); %! assert_equal (stats.sd, 0.757411298436985, 1e-14); %!test %! ## Welch: a pooled denominator would give the 'equal' answer instead %! [h, p, ci, stats] = ttest2 (x, y, 'Vartype', 'unequal'); %! assert_equal (h, 1); %! assert_equal (p, 0.003801117252046, 1e-14); %! assert_equal (ci, [-2.327640087870322, -0.597359912129679], 1e-13); %! assert_equal (stats.tstat, -3.784559445642580, 1e-13); %! assert_equal (stats.df, 9.661941319801253, 1e-13); %! assert_equal (stats.sd(:)', [0.489897948556636, 1.001338390070295], 1e-14); %!test %! ## the default is the equal-variance test %! [~, pd] = ttest2 (x, y); %! [~, pe] = ttest2 (x, y, 'Vartype', 'equal'); %! [~, pu] = ttest2 (x, y, 'Vartype', 'unequal'); %! assert_equal (pd, pe, 0); %! assert_equal (isequal (pd, pu), false); %!test %! [h, p, ci] = ttest2 (x, y, 'Tail', 'left'); %! assert_equal (h, 1); %! assert_equal (p, 4.447635426743633e-04, 1e-15); %! assert_equal (ci, [-Inf, -0.835253361212791], 1e-13); %!test %! [h, p, ci] = ttest2 (x, y, 'Tail', 'right'); %! assert_equal (h, 0); %! assert_equal (p, 0.999555236457326, 1e-14); %! assert_equal (ci, [-2.089746638787210, Inf], 1e-13); %!test %! ## a non-default alpha must widen the interval and leave the p-value alone %! [~, p1, ci1] = ttest2 (x, y); %! [~, p2, ci2] = ttest2 (x, y, 'Alpha', 0.01); %! assert_equal (ci2, [-2.511854249766015, -0.413145750233985], 1e-13); %! assert_equal (diff (ci2) > diff (ci1), true); %! assert_equal (p1, p2, 0); %!test %! [h, p] = ttest2 (xm, xm + 1, 'Dim', 1); %! assert_equal (h, [1, 0]); %! assert_equal (p, [0.020600636177229, 0.154833732538475], 1e-13); ## ALPHA was not validated at all, so a negative one silently produced a ## [NaN NaN] confidence interval and "do not reject", and a vector one ## quietly vectorised into an undocumented signature. MATLAB refuses each of ## these with "ALPHA must be a scalar between 0 and 1." %!error ... %! ttest2 ([8:0.1:12], [9:0.1:13], 'Alpha', -0.05); %!error ... %! ttest2 ([8:0.1:12], [9:0.1:13], 'Alpha', 0); %!error ... %! ttest2 ([8:0.1:12], [9:0.1:13], 'Alpha', 1); %!error ... %! ttest2 ([8:0.1:12], [9:0.1:13], 'Alpha', 1.5); %!error ... %! ttest2 ([8:0.1:12], [9:0.1:13], 'Alpha', [0.01, 0.05]); %!error ... %! ttest2 ([8:0.1:12], [9:0.1:13], 'Alpha', 'a'); %!error ... %! ttest2 ([8:0.1:12], [9:0.1:13], 'Alpha', NaN); %!error ... %! ttest2 ([8:0.1:12], [9:0.1:13], 'Alpha', 2 + 1i); statistics-release-2.1.2/inst/Hypothesis_Testing/vartest.m000066400000000000000000000207021527311104300240010ustar00rootroot00000000000000## Copyright (C) 2014 Tony Richardson ## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} vartest (@var{x}, @var{v}) ## @deftypefnx {statistics} {@var{h} =} vartest (@var{x}, @var{v}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}] =} vartest (@dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}, @var{ci}] =} vartest (@dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}, @var{ci}, @var{stats}] =} vartest (@dots{}) ## ## One-sample test of variance. ## ## @code{@var{h} = vartest (@var{x}, @var{v})} performs a chi-square test of the ## hypothesis that the data in the vector @var{x} come from a normal ## distribution with variance @var{v}, against the alternative that @var{x} ## comes from a normal distribution with a different variance. The result is ## @var{h} = 0 if the null hypothesis ("variance is V") cannot be rejected at ## the 5% significance level, or @var{h} = 1 if the null hypothesis can be ## rejected at the 5% level. ## ## @var{x} may also be a matrix or an N-D array. For matrices, @code{vartest} ## performs separate tests along each column of @var{x}, and returns a vector of ## results. For N-D arrays, @code{vartest} works along the first non-singleton ## dimension of @var{x}. @var{v} must be a scalar. ## ## @code{vartest} treats NaNs as missing values, and ignores them. ## ## @code{[@var{h}, @var{pval}] = vartest (@dots{})} returns the p-value. That ## is the probability of observing the given result, or one more extreme, by ## chance if the null hypothesis true. ## ## @code{[@var{h}, @var{pval}, @var{ci}] = vartest (@dots{})} returns a ## 100 * (1 - @var{alpha})% confidence interval for the true variance. ## ## @code{[@var{h}, @var{pval}, @var{ci}, @var{stats}] = vartest (@dots{})} ## returns a structure with the following fields: ## ## @multitable @columnfractions 0.2 0.75 ## @item @qcode{chisqstat} @tab the value of the test statistic ## @item @qcode{df} @tab the degrees of freedom of the test ## @end multitable ## ## @code{[@dots{}] = vartest (@dots{}, @var{name}, @var{value}), @dots{}} ## specifies one or more of the following name/value pairs: ## ## @multitable @columnfractions 0.2 0.75 ## @headitem Name @tab Value ## @item @qcode{'alpha'} @tab the significance level. Default is 0.05. ## ## @item @qcode{'dim'} @tab dimension to work along a matrix or an N-D ## array. ## ## @item @qcode{'tail'} @tab a string specifying the alternative hypothesis ## @end multitable ## @multitable @columnfractions 0.15 0.75 ## @item @qcode{'both'} @tab variance is not @var{v} (two-tailed, default) ## @item @qcode{'left'} @tab variance is less than @var{v} (left-tailed) ## @item @qcode{'right'} @tab variance is greater than @var{v} ## (right-tailed) ## @end multitable ## ## @seealso{ttest, ztest, kstest} ## @end deftypefn function [h, pval, ci, stats] = vartest (x, v, varargin) ## Validate input arguments if (nargin < 2) error ("vartest: too few input arguments."); endif if (! isscalar (v) || ! isnumeric (v) || ! isreal (v) || v < 0) error ("vartest: invalid value for variance."); endif ## Add defaults alpha = 0.05; tail = 'both'; dim = []; if (nargin > 2 && mod (numel (varargin(:)), 2) == 0) for idx = 3:2:nargin name = varargin{idx-2}; value = varargin{idx-1}; switch (lower (name)) case 'alpha' alpha = value; if (! isscalar (alpha) || ! isnumeric (alpha) || ... alpha <= 0 || alpha >= 1) error ("vartest: invalid value for alpha."); endif case 'tail' tail = value; if (! any (strcmpi (tail, {'both', 'left', 'right'}))) error ("vartest: invalid value for tail."); endif case 'dim' dim = value; if (! isscalar (dim) || ! ismember (dim, 1:ndims (x))) error ("vartest: invalid value for operating dimension."); endif otherwise error ("vartest: invalid name for optional arguments."); endswitch endfor elseif (nargin > 2 && mod (numel (varargin(:)), 2) != 0) error ("vartest: optional arguments must be in name/value pairs."); endif ## Figure out which dimension mean will work along if (isempty (dim)) dim = find (size (x) != 1, 1); endif ## Replace all NaNs with zeros is_nan = isnan (x); x_dims = ndims (x); x(is_nan) = 0; ## Find sample size for each group (if more than one) if (any (is_nan(:))) sz = sum (! is_nan, dim); else sz = size (x, dim); endif ## Find degrees of freedom for each group (if more than one) df = max (sz - 1, 0); ## Calculate mean for each group (if more than one) x_mean = sum (x, dim) ./ max (1, sz); ## Center data if (isscalar (x_mean)) x_centered = x - x_mean; else rep = ones (1, x_dims); rep(dim) = size (x, dim); x_centered = x - repmat (x_mean, rep); endif ## Replace all NaNs with zeros x_centered(is_nan) = 0; ## Calculate chi-square statistic sumsq = sum (abs (x_centered) .^ 2, dim); if (v > 0) chisqstat = sumsq ./ v; else chisqstat = Inf (size (sumsq)); chisqstat(sumsq == 0) = NaN; endif ## Calculate p-value for the test and confidence intervals (if requested) if (strcmpi (tail, 'both')) pval = chi2cdf (chisqstat, df); pval = 2 * min (pval, 1 - pval); if (nargout > 2) ci = cat (dim, sumsq ./ chi2inv (1 - alpha / 2, df), ... sumsq ./ chi2inv (alpha / 2, df)); endif elseif (strcmpi (tail, 'right')) pval = gammainc (chisqstat / 2, df / 2, 'upper'); if (nargout > 2) ci = cat (dim, sumsq ./ chi2inv (1 - alpha, df), Inf (size (pval))); endif elseif (strcmpi (tail, 'left')) pval = chi2cdf (chisqstat, df); if (nargout > 2) ci = cat (dim, zeros (size (pval)), sumsq ./ chi2inv (alpha, df)); endif endif ## Determine the test outcome h = double (pval < alpha); h(isnan (pval)) = NaN; ## Create stats output structure (if requested) if (nargout > 3) stats = struct ('chisqstat', chisqstat, 'df', df); endif endfunction ## Test input validation %!error vartest (); %!error vartest ([1, 2, 3, 4], -0.5); %!error ... %! vartest ([1, 2, 3, 4], 1, 'alpha', 0); %!error ... %! vartest ([1, 2, 3, 4], 1, 'alpha', 1.2); %!error ... %! vartest ([1, 2, 3, 4], 1, 'alpha', 'val'); %!error ... %! vartest ([1, 2, 3, 4], 1, 'tail', 'val'); %!error ... %! vartest ([1, 2, 3, 4], 1, 'alpha', 0.01, 'tail', 'val'); %!error ... %! vartest ([1, 2, 3, 4], 1, 'dim', 3); %!error ... %! vartest ([1, 2, 3, 4], 1, 'alpha', 0.01, 'tail', 'both', 'dim', 3); %!error ... %! vartest ([1, 2, 3, 4], 1, 'alpha', 0.01, 'tail', 'both', 'badoption', 3); %!error ... %! vartest ([1, 2, 3, 4], 1, 'alpha', 0.01, 'tail'); ## Test results %!test %! load carsmall %! [h, pval, ci] = vartest (MPG, 7^2); %! assert_equal (h, 1); %! assert_equal (pval, 0.04335086742174443, 1e-14); %! assert_equal (ci, [49.397; 88.039], 1e-3); %!test %! load carsmall %! [h, pval, ci] = vartest (MPG, 7^2, 'tail', 'left'); %! assert_equal (h, 0); %! assert_equal (pval, 0.978324566289128, 1e-14); %! assert_equal (ci, [0; 83.685], 1e-3); %!test %! load carsmall %! [h, pval, ci] = vartest (MPG, 7^2, 'tail', 'right'); %! assert_equal (h, 1); %! assert_equal (pval, 0.021675433710872, 1e-14); %! assert_equal (ci, [51.543; Inf], 1e-3); statistics-release-2.1.2/inst/Hypothesis_Testing/vartest2.m000066400000000000000000000242001527311104300240600ustar00rootroot00000000000000## Copyright (C) 2014 Tony Richardson ## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} vartest2 (@var{x}, @var{y}) ## @deftypefnx {statistics} {@var{h} =} vartest2 (@var{x}, @var{y}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}] =} vartest2 (@dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}, @var{ci}] =} vartest2 (@dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}, @var{ci}, @var{stats}] =} vartest2 (@dots{}) ## ## Two-sample F test for equal variances. ## ## @code{@var{h} = vartest2 (@var{x}, @var{y})} performs an F test of the ## hypothesis that the independent data in vectors @var{x} and @var{y} come from ## normal distributions with equal variance, against the alternative that they ## come from normal distributions with different variances. The result is ## @var{h} = 0 if the null hypothesis ("variance are equal") cannot be rejected ## at the 5% significance level, or @var{h} = 1 if the null hypothesis can be ## rejected at the 5% level. ## ## @var{x} and @var{y} may also be matrices or N-D arrays. For matrices, ## @code{vartest2} performs separate tests along each column and returns a ## vector of results. For N-D arrays, @code{vartest2} works along the first ## non-singleton dimension and @var{x} and @var{y} must have the same size along ## all the remaining dimensions. ## ## @code{vartest2} treats NaNs as missing values, and ignores them. ## ## @code{[@var{h}, @var{pval}] = vartest2 (@dots{})} returns the p-value. That ## is the probability of observing the given result, or one more extreme, by ## chance if the null hypothesis true. ## ## @code{[@var{h}, @var{pval}, @var{ci}] = vartest2 (@dots{})} returns a ## @math{100 * (1 - @var{alpha})%} confidence interval for the true ratio ## var(X)/var(Y). ## ## @code{[@var{h}, @var{pval}, @var{ci}, @var{stats}] = vartest2 (@dots{})} ## returns a structure with the following fields: ## ## @multitable @columnfractions 0.2 0.75 ## @item @qcode{fstat} @tab the value of the test statistic ## @item @qcode{df1} @tab the numerator degrees of freedom of the test ## @item @qcode{df2} @tab the denominator degrees of freedom of the test ## @end multitable ## ## @code{[@dots{}] = vartest2 (@dots{}, @var{name}, @var{value}), @dots{}} ## specifies one or more of the following name/value pairs: ## ## @multitable @columnfractions 0.2 0.75 ## @headitem Name @tab Value ## @item @qcode{'alpha'} @tab the significance level. Default is 0.05. ## ## @item @qcode{'dim'} @tab dimension to work along a matrix or an N-D ## array. ## ## @item @qcode{'tail'} @tab a string specifying the alternative hypothesis ## @end multitable ## @multitable @columnfractions 0.15 0.75 ## @item @qcode{'both'} @tab variance is not @var{v} (two-tailed, default) ## @item @qcode{'left'} @tab variance is less than @var{v} (left-tailed) ## @item @qcode{'right'} @tab variance is greater than @var{v} ## (right-tailed) ## @end multitable ## ## @seealso{ttest2, kstest2, vartestn} ## @end deftypefn function [h, pval, ci, stats] = vartest2 (x, y, varargin) ## Validate input arguments if (nargin < 2) error ("vartest2: too few input arguments."); endif if (isscalar (x) || isscalar (y)) error ("vartest2: X and Y must be vectors or matrices or N-D arrays."); endif ## If X and Y are vectors make them the same orientation if (isvector (x) && isvector (y)) if (size (x, 1) == 1) y = y(:)'; else y = y(:); endif endif ## Add defaults alpha = 0.05; tail = 'both'; dim = []; if (nargin > 2 && mod (numel (varargin(:)), 2) == 0) for idx = 3:2:nargin name = varargin{idx-2}; value = varargin{idx-1}; switch (lower (name)) case 'alpha' alpha = value; if (! isscalar (alpha) || ! isnumeric (alpha) || ... alpha <= 0 || alpha >= 1) error ("vartest2: invalid value for alpha."); endif case 'tail' tail = value; if (! any (strcmpi (tail, {'both', 'left', 'right'}))) error ("vartest2: invalid value for tail."); endif case 'dim' dim = value; if (! isscalar (dim) || ! ismember (dim, 1:ndims (x))) error ("vartest2: invalid value for operating dimension."); endif otherwise error ("vartest2: invalid name for optional arguments."); endswitch endfor elseif (nargin > 2 && mod (numel (varargin(:)), 2) != 0) error ("vartest2: optional arguments must be in name/value pairs."); endif ## Figure out which dimension mean will work along if (isempty (dim)) dim = find (size (x) != 1, 1); endif ## Check that all non-working dimensions of X and Y are of equal size x_size = size (x); y_size = size (y); x_size(dim) = 1; y_size(dim) = 1; if (! isequal (x_size, y_size)) error ("vartest2: input size mismatch."); endif ## Compute statistics for each sample [df1, x_var] = getstats (x,dim); [df2, y_var] = getstats (y,dim); ## Compute F statistic F = NaN (size (x_var)); t1 = (y_var > 0); F(t1) = x_var(t1) ./ y_var(t1); t2 = (x_var > 0) & ! t1; F(t2) = Inf; ## Calculate p-value for the test and confidence intervals (if requested) if (strcmpi (tail, 'both')) pval = 2 * min (fcdf (F, df1, df2), fcdf (F, df1, df2, 'upper')); if (nargout > 2) ci = cat (dim, F .* finv (alpha / 2, df2, df1), ... F ./ finv (alpha / 2, df1, df2)); endif elseif (strcmpi (tail, 'right')) Ftmp = F; Ftmp(Ftmp < 0) = 0; pval = fcdf (1 ./ Ftmp, df2, df1); if (nargout > 2) ci = cat (dim, F .* finv (alpha, df2, df1), Inf (size (F))); endif elseif (strcmpi (tail, 'left')) pval = fcdf (F, df1, df2); if (nargout > 2) ci = cat (dim, zeros (size (F)), F ./ finv (alpha, df1, df2)); endif endif ## Determine the test outcome h = double (pval < alpha); h(isnan (pval)) = NaN; ## Create stats output structure (if requested) if (nargout > 3) stats = struct ('fstat', F, 'df1', df1, 'df2', df2); endif endfunction ## Compute statistics for one sample function [df, data_var] = getstats (data, dim) ## Calculate sample size and df by ignoring NaNs is_nan = isnan (data); n_data = sum (! is_nan, dim); df = max (n_data - 1, 0); ## Calculate mean data(is_nan) = 0; m_data = sum (data, dim) ./ max (1, n_data); ## Calculate variance if (isscalar (m_data)) c_data = data - m_data; else rep = ones (1, ndims (data)); rep(dim) = size (data, dim); c_data = data - repmat (m_data, rep); endif c_data(is_nan) = 0; data_var = sum (abs (c_data) .^ 2,dim); t = (df > 0); data_var(t) = data_var(t) ./ df(t); data_var(! t) = NaN; ## Make df a scalar if possible if (numel (df) > 1 && all (df(:) == df(1))) df = df(1); endif endfunction %!test %! ## Below the resolution of 1 - fcdf, values from MATLAB R2024a %! s = sin (1:50)'; %! [~, p] = vartest2 (100 * s, 0.01 * s); %! assert_equal (p, 6.38404544818042e-183, -1e-10); ## Test input validation %!error vartest2 (); %!error vartest2 (ones (20,1)); %!error ... %! vartest2 (rand (20,1), 5); %!error ... %! vartest2 (rand (20,1), rand (25,1)*2, 'alpha', 0); %!error ... %! vartest2 (rand (20,1), rand (25,1)*2, 'alpha', 1.2); %!error ... %! vartest2 (rand (20,1), rand (25,1)*2, 'alpha', 'some'); %!error ... %! vartest2 (rand (20,1), rand (25,1)*2, 'alpha', [0.05, 0.001]); %!error ... %! vartest2 (rand (20,1), rand (25,1)*2, 'tail', [0.05, 0.001]); %!error ... %! vartest2 (rand (20,1), rand (25,1)*2, 'tail', 'some'); %!error ... %! vartest2 (rand (20,1), rand (25,1)*2, 'dim', 3); %!error ... %! vartest2 (rand (20,1), rand (25,1)*2, 'alpha', 0.001, 'dim', 3); %!error ... %! vartest2 (rand (20,1), rand (25,1)*2, 'some', 3); %!error ... %! vartest2 (rand (20,1), rand (25,1)*2, 'some'); ## Test results %!test %! load carsmall %! [h, pval, ci, stat] = vartest2 (MPG(Model_Year==82), MPG(Model_Year==76)); %! assert_equal (h, 0); %! assert_equal (pval, 0.6288022362718455, 1e-13); %! assert_equal (ci, [0.4139; 1.7193], 1e-4); %! assert_equal (stat.fstat, 0.8384, 1e-4); %! assert_equal (stat.df1, 30); %! assert_equal (stat.df2, 33); %!test %! load carsmall %! [h, pval, ci, stat] = vartest2 (MPG(Model_Year==82), MPG(Model_Year==76), ... %! 'tail', 'left'); %! assert_equal (h, 0); %! assert_equal (pval, 0.314401118135922, 1e-13); %! assert_equal (ci, [0; 1.5287], 1e-4); %! assert_equal (stat.fstat, 0.8384, 1e-4); %! assert_equal (stat.df1, 30); %! assert_equal (stat.df2, 33); %!test %! load carsmall %! [h, pval, ci, stat] = vartest2 (MPG(Model_Year==82), MPG(Model_Year==76), ... %! 'tail', 'right'); %! assert_equal (h, 0); %! assert_equal (pval, 0.685598881864077, 1e-13); %! assert_equal (ci, [0.4643; Inf], 1e-4); %! assert_equal (stat.fstat, 0.8384, 1e-4); %! assert_equal (stat.df1, 30); %! assert_equal (stat.df2, 33); statistics-release-2.1.2/inst/Hypothesis_Testing/vartestn.m000066400000000000000000000447021527311104300241650ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} vartestn (@var{x}) ## @deftypefnx {statistics} {} vartestn (@var{x}, @var{group}) ## @deftypefnx {statistics} {} vartestn (@dots{}, @var{name}, @var{value}) ## @deftypefnx {statistics} {@var{p} =} vartestn (@dots{}) ## @deftypefnx {statistics} {[@var{p}, @var{stats}] =} vartestn (@dots{}) ## @deftypefnx {statistics} {[@var{p}, @var{stats}] =} vartestn (@dots{}, @var{name}, @var{value}) ## ## Test for equal variances across multiple groups. ## ## @code{@var{h} = vartestn (@var{x})} performs Bartlett's test for equal ## variances for the columns of the matrix @var{x}. This is a test of the null ## hypothesis that the columns of @var{x} come from normal distributions with ## the same variance, against the alternative that they come from normal ## distributions with different variances. The result is displayed in a summary ## table of statistics as well as a box plot of the groups. ## ## @code{vartestn (@var{x}, @var{group})} requires a vector @var{x}, and a ## @var{group} argument that is a categorical variable, vector, string array, or ## cell array of strings with one row for each element of @var{x}. Values of ## @var{x} corresponding to the same value of @var{group} are placed in the same ## group. Without @var{group}, a vector @var{x} is a single group, whichever ## way it lies, and the test has nothing to compare, so @var{p} is @qcode{NaN}. ## ## @code{vartestn} treats NaNs as missing values, and ignores them. ## ## @code{@var{p} = vartestn (@dots{})} returns the probability of observing the ## given result, or one more extreme, by chance under the null hypothesis that ## all groups have equal variances. Small values of @var{p} cast doubt on the ## validity of the null hypothesis. ## ## @code{[@var{p}, @var{stats}] = vartestn (@dots{})} returns a structure with ## the following fields: ## ## @multitable @columnfractions 0.2 0.75 ## @item @qcode{chisqstat} @tab -- the value of Bartlett's test statistic ## @item @qcode{df} @tab -- the degrees of freedom of the test ## @end multitable ## ## ## @code{[@var{p}, @var{stats}] = vartestn (@dots{}, @var{name}, @var{value})} ## specifies one or more of the following @var{name}/@var{value} pairs: ## ## @multitable @columnfractions 0.20 0.8 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'display'} @tab @qcode{'on'} to display a boxplot and table, or ## @qcode{'off'} to omit these displays. Default @qcode{'on'}. ## ## @item @qcode{'testtype'} @tab One of the following strings to control the ## type of test to perform ## @end multitable ## ## @multitable @columnfractions 0.25 0.72 ## @item @qcode{'Bartlett'} @tab Bartlett's test (default). ## ## @item @qcode{'LeveneQuadratic'} @tab Levene's test computed by ## performing anova on the squared deviations of the data values from their ## group means. ## ## @item @qcode{'LeveneAbsolute'} @tab Levene's test computed by performing ## anova on the absolute deviations of the data values from their group means. ## ## @item @qcode{'BrownForsythe'} @tab Brown-Forsythe test computed by ## performing anova on the absolute deviations of the data values from the group ## medians. ## ## @item @qcode{'OBrien'} @tab O'Brien's modification of Levene's test with ## @math{W=0.5}. ## @end multitable ## ## The classical Bartlett's test is sensitive to the assumption that the ## distribution in each group is normal. The other test types are more robust ## to non-normal distributions, especially ones prone to outliers. For these ## tests, the STATS output structure has a field named @qcode{fstat} containing ## the test statistic in place of @qcode{chisqstat}, and @qcode{df} holds two ## elements, the numerator and the denominator degrees of freedom. ## ## @seealso{vartest, vartest2, anova1} ## @end deftypefn function [p, stats] = vartestn (x, group, varargin) ## Validate input arguments if (nargin < 1) error ("vartestn: too few input arguments."); endif if (isscalar (x)) error ("vartestn: X must be a vector or a matrix."); endif if (nargin < 2) group = []; endif if (nargin > 1 && ischar (group) && rows (group) == 1 && any (strcmpi (group, {'display', 'testtype'}))) varargin = [{group} varargin]; group = []; endif ## A test of variance needs something to measure it on. MATLAB refuses an ## empty X here too, where ANOVA1 and KRUSKALWALLIS answer. if (isempty (x)) error ("vartestn: X is empty."); endif ## Add defaults plotdata = true; testtype = 'Bartlett'; if (numel (varargin(:)) > 0 && mod (numel (varargin(:)), 2) == 0) for idx = 1:2:numel (varargin(:)) name = varargin{idx}; value = varargin{idx+1}; switch (lower (name)) case 'display' plotdata = value; if (! any (strcmpi (plotdata, {'on', 'off'}))) error ("vartestn: invalid value for display."); endif if (strcmpi (plotdata, 'on')) plotdata = true; else plotdata = false; endif case 'testtype' testtype = value; if (! any (strcmpi (testtype, {'Bartlett', 'LeveneAbsolute', ... 'LeveneQuadratic', 'BrownForsythe', 'OBrien'}))) error ("vartestn: invalid value for testtype."); endif otherwise error ("vartestn: invalid name for optional arguments."); endswitch endfor elseif (numel (varargin(:)) > 0 && mod (numel (varargin(:)), 2) != 0) error ("vartestn: optional arguments must be in name/value pairs."); endif ## Convert group to cell array from character array, make it a column if (! isempty (group) && ischar (group)) group = cellstr (group); endif if (size (group, 1) == 1) group = group'; endif ## A vector is one group whichever way it lies, and each column of a matrix ## is a group of its own. With GROUP given, a vector is grouped by it ## instead, one element per observation. if (isvector (x)) x = x(:); endif if (isempty (group) || ! isvector (x)) [n, m] = size (x); x = x(:); gi = reshape (repmat ((1:m), n, 1), n*m, 1); if (length (group) == 0) ## no group names are provided group = gi; elseif (size (group, 1) == m) ## group names exist and match columns group = group(gi,:); else error ("vartestn: columns in X and GROUP length do not match."); endif endif ## Check that x and group are the same size if (! all (numel (x) == numel (group))) error ("vartestn: GROUP must be a vector with the same number of rows as x."); endif ## Identify NaN values (if any) and remove them from X along with ## their corresponding values from group vector nonan = ! isnan (x); x = x(nonan); group = group(nonan, :); ## Convert group to indices and separate names [group_id, group_names] = grp2idx (group); group_id = group_id(:); ## Compute group summary statistics [group_mean, group_ster, group_size] = grpstats (x, group_id, ... {'mean', 'sem', 'numel'}); ## Compute group degrees of freedom and variances group_DF = group_size - 1; groupVAR = group_size .* group_ster .^ 2; sum_DF = sum (group_DF); ## Calculate pooled variance if (sum_DF > 0) pooledVAR = sum (group_DF .* groupVAR) / sum_DF; else pooledVAR = NaN; endif ## Get number of groups k = length (group_DF); ## Test for equal variance according to specified testtype switch (lower (testtype)) case 'bartlett' ## Calculate degrees of freedom Bdf = max (0, sum (group_DF > 0) - 1); ## Get valid groups msgroups = group_DF > 0; ## For valid groups if (Bdf > 0 && sum_DF > 0) B = log (pooledVAR) * sum (group_DF) - ... sum (group_DF(msgroups) .* log (groupVAR(msgroups))); C = 1 + (sum (1 ./ group_DF(msgroups)) - 1 / sum (group_DF)) / (3 * Bdf); F = B / C; else F = NaN; endif ## Compute p-value. Fewer than two groups leaves nothing to compare, ## so the test is undefined rather than significant. if (Bdf > 0) p = chi2cdf (F, Bdf, 'upper'); else p = NaN; endif testname = 'Bartlett''s statistic '; if (nargout > 1) stats = struct ('chisqstat', F, 'df', Bdf); endif case {'leveneabsolute', 'levenequadratic'} ## Remove single-sample groups ssgroups = find (group_size < 2); msgroups = ! ismember (group_id, ssgroups); ## Center each group with mean x_center = x(msgroups) - group_mean(group_id(msgroups)); ## Get number of valid groups (group size > 1) n_groups = length (group_size) - length (ssgroups); ## Perform one-way anova and extract results from the anova table if (n_groups > 1) if (strcmpi (testtype, 'LeveneAbsolute')) [p, atab] = anova1 (abs (x_center), group_id(msgroups), 'off'); testname = 'Levene''s statistic (absolute) '; else [p, atab] = anova1 (x_center .^ 2, group_id(msgroups), 'off'); testname = 'Levene''s statistic (quadratic) '; endif ## Get F statistic and both degrees of freedom F = atab{2,5}; Bdf = [atab{2,3}, atab{3,3}]; else p = NaN; F = NaN; Bdf = [0, (length (x_center) - n_groups)]; endif if (nargout > 1) stats = struct ('fstat', F, 'df', Bdf); endif case 'brownforsythe' ## Remove single-sample groups ssgroups = find (group_size < 2); msgroups = ! ismember (group_id, ssgroups); ## Calculate group medians group_md = grpstats (x, group_id, 'median'); ## Center each group with median xcbf = x(msgroups) - group_md(group_id(msgroups)); ## Get number of valid groups (group size > 1) n_groups = length (group_size) - length (ssgroups); ## Perform one-way anova and extract results from the anova table if (n_groups > 1) [p, atab] = anova1 (abs (xcbf), group_id(msgroups), 'off'); ## Get F statistic and both degrees of freedom F = atab{2,5}; Bdf = [atab{2,3}, atab{3,3}]; else p = NaN; F = NaN; Bdf = [0, (length (xcbf) - n_groups)]; endif testname = 'Brown-Forsythe statistic '; if (nargout > 1) stats = struct ('fstat', F, 'df', Bdf); endif case 'obrien' ## Remove single-sample groups ssgroups = find (group_size < 2); msgroups = ! ismember (group_id, ssgroups); ## Center each group with mean x_center = x(msgroups) - group_mean(group_id(msgroups)); ## Calculate OBrien Z_ij xcs = x_center.^2; W = 0.5; xcw = ((W + group_size(group_id(msgroups)) - 2) .* ... group_size(group_id(msgroups)) .* xcs - W .* ... (group_size(group_id(msgroups)) - 1) .* ... groupVAR(group_id(msgroups))) ./ ... ((group_size(group_id(msgroups)) - 1) .* ... (group_size(group_id(msgroups)) - 2)); ## Get number of valid groups (group size > 1) n_groups = length (group_size) - length (ssgroups); ## Perform one-way anova and extract results from the anova table if (n_groups > 1) [p, atab] = anova1 (xcw, group_id(msgroups), 'off'); ## Get F statistic and both degrees of freedom F = atab{2,5}; Bdf = [atab{2,3}, atab{3,3}]; else p = NaN; F = NaN; Bdf = [0, length(xcw)-n_groups]; endif testname = 'OBrien statistic '; if (nargout > 1) stats = struct ('fstat', F, 'df', Bdf); endif endswitch ## Print Group Summary Table (unless opted out) if (nargout == 0 || plotdata) groupSTD = sqrt (groupVAR); printf ("\n Group Summary Table\n\n"); printf ("Group Count Mean Std Dev\n"); printf ("------------------------------------------------------------\n"); for i = 1:k printf ("%-20s %10i %9.4f %1.6f\n", ... group_names{i}, group_size(i), group_mean(i), groupSTD(i)); endfor printf ("Pooled Groups %10i %9.4f %1.6f\n", ... sum (group_size), mean (group_mean), mean (groupSTD)); printf ("Pooled valid Groups %10i %9.4f %1.6f\n\n", ... sum (group_size(group_id(msgroups))), ... mean (group_mean(group_id(msgroups))), ... mean (groupSTD(group_id(msgroups)))); printf ("%s %7.5f\n", testname, F); if (numel (Bdf) == 1) printf ("Degrees of Freedom %10i\n", Bdf); else printf ("Degrees of Freedom %10i, %3i\n", Bdf(1), Bdf(2)); endif printf ("p-value %1.6f\n\n", p); endif ## Plot data using BOXPLOT (unless opted out) if (plotdata) boxplot (x, group_id, 'Notch', 'on', 'Labels', group_names); endif endfunction %!demo %! ## Test the null hypothesis that the variances are equal across the five %! ## columns of data in the students’ exam grades matrix, grades. %! %! load examgrades %! vartestn (grades) %!demo %! ## Test the null hypothesis that the variances in miles per gallon (MPG) are %! ## equal across different model years. %! %! load carsmall %! vartestn (MPG, Model_Year) %!demo %! ## Use Levene’s test to test the null hypothesis that the variances in miles %! ## per gallon (MPG) are equal across different model years. %! %! load carsmall %! p = vartestn (MPG, Model_Year, 'TestType', 'LeveneAbsolute') %!demo %! ## Test the null hypothesis that the variances are equal across the five %! ## columns of data in the students’ exam grades matrix, grades, using the %! ## Brown-Forsythe test. Suppress the display of the summary table of %! ## statistics and the box plot. %! %! load examgrades %! [p, stats] = vartestn (grades, 'TestType', 'BrownForsythe', 'Display', 'off') %!test %! ## Below the resolution of 1 - chi2cdf, values from MATLAB R2024a %! s = sin (1:50)'; %! p = vartestn ([0.01 * s; 100 * s], [ones(50, 1); 2 * ones(50, 1)], ... %! 'Display', 'off'); %! assert_equal (p, 1.05713927539863e-181, -1e-10); %!test %! ## Group labels as text labels in a cell array %! v = [sin(1:12)'; 3 * cos(1:17)'; 0.5 * sin(2 * (1:9))']; %! g = [ones(12, 1); 2 * ones(17, 1); 3 * ones(9, 1)]; %! gc = cellstr (char ('a' + g - 1)); %! [~, s0] = vartestn (v, g, 'Display', 'off'); %! [~, s] = vartestn (v, gc, 'Display', 'off'); %! assert_equal (s.chisqstat, s0.chisqstat); %!test %! ## Group labels as a character matrix %! v = [sin(1:12)'; 3 * cos(1:17)'; 0.5 * sin(2 * (1:9))']; %! g = [ones(12, 1); 2 * ones(17, 1); 3 * ones(9, 1)]; %! gc = cellstr (char ('a' + g - 1)); %! [~, s0] = vartestn (v, g, 'Display', 'off'); %! [~, s] = vartestn (v, char (gc), 'Display', 'off'); %! assert_equal (s.chisqstat, s0.chisqstat); %!test %! ## Group labels as categorical labels %! v = [sin(1:12)'; 3 * cos(1:17)'; 0.5 * sin(2 * (1:9))']; %! g = [ones(12, 1); 2 * ones(17, 1); 3 * ones(9, 1)]; %! gc = cellstr (char ('a' + g - 1)); %! [~, s0] = vartestn (v, g, 'Display', 'off'); %! [~, s] = vartestn (v, categorical (gc), 'Display', 'off'); %! assert_equal (s.chisqstat, s0.chisqstat); %!test %! ## Group labels as string labels %! v = [sin(1:12)'; 3 * cos(1:17)'; 0.5 * sin(2 * (1:9))']; %! g = [ones(12, 1); 2 * ones(17, 1); 3 * ones(9, 1)]; %! gc = cellstr (char ('a' + g - 1)); %! [~, s0] = vartestn (v, g, 'Display', 'off'); %! [~, s] = vartestn (v, string (gc), 'Display', 'off'); %! assert_equal (s.chisqstat, s0.chisqstat); ## Test input validation %!error vartestn (); %!error vartestn (1); %!error vartestn ([]); %!error vartestn (zeros (0, 3)); %!error ... %! vartestn ([1, 2, 3, 4, 5, 6, 7], [1, 1, 1, 2, 2, 2, 2], 'Display', 'some'); %!error ... %! vartestn (ones (50,3), 'Display', 'some'); %!error ... %! vartestn (ones (50,3), 'Display', 'off', 'testtype', 'some'); %!error ... %! vartestn (ones (50,3), [], 'som'); %!error ... %! vartestn (ones (50,3), [], 'some', 'some'); %!error ... %! vartestn (ones (50,3), [1, 2], 'Display', 'off'); ## Test results %!test %! load examgrades %! [p, stat] = vartestn (grades, 'Display', 'off'); %! assert_equal (p, 7.908647337018238e-08, 1e-14); %! assert_equal (stat.chisqstat, 38.7332, 1e-4); %! assert_equal (stat.df, 4); %!test %! load examgrades %! [p, stat] = vartestn (grades, 'Display', 'off', 'TestType', 'LeveneAbsolute'); %! assert_equal (p, 9.523239714592791e-07, 1e-14); %! assert_equal (stat.fstat, 8.5953, 1e-4); %! assert_equal (stat.df, [4, 595]); %!test %! load examgrades %! [p, stat] = vartestn (grades, 'Display', 'off', 'TestType', 'LeveneQuadratic'); %! assert_equal (p, 7.219514351897161e-07, 1e-14); %! assert_equal (stat.fstat, 8.7503, 1e-4); %! assert_equal (stat.df, [4, 595]); %!test %! load examgrades %! [p, stat] = vartestn (grades, 'Display', 'off', 'TestType', 'BrownForsythe'); %! assert_equal (p, 1.312093241723211e-06, 1e-14); %! assert_equal (stat.fstat, 8.4160, 1e-4); %! assert_equal (stat.df, [4, 595]); %!test %! load examgrades %! [p, stat] = vartestn (grades, 'Display', 'off', 'TestType', 'OBrien'); %! assert_equal (p, 8.235660885480556e-07, 1e-14); %! assert_equal (stat.fstat, 8.6766, 1e-4); %! assert_equal (stat.df, [4, 595]); ## A vector without GROUP is one group whichever way it lies, so there is ## nothing to compare. %!test %! [p, stats] = vartestn ((1:5)', [], 'Display', 'off'); %! assert_equal (p, NaN); %! assert_equal (stats.df, 0); %!test %! p = vartestn (1:5, [], 'Display', 'off'); %! assert_equal (p, NaN); statistics-release-2.1.2/inst/Hypothesis_Testing/ztest.m000066400000000000000000000236161527311104300234710ustar00rootroot00000000000000## Copyright (C) 2014 Tony Richardson ## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} ztest (@var{x}, @var{m}, @var{sigma}) ## @deftypefnx {statistics} {@var{h} =} ztest (@var{x}, @var{m}, @var{sigma}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}] =} ztest (@dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}, @var{ci}] =} ztest (@dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{pval}, @var{ci}, @var{zvalue}] =} ztest (@dots{}) ## ## One-sample Z-test. ## ## @code{@var{h} = ztest (@var{x}, @var{v})} performs a Z-test of the hypothesis ## that the data in the vector @var{x} come from a normal distribution with mean ## @var{m}, against the alternative that @var{x} comes from a normal ## distribution with a different mean @var{m}. The result is @var{h} = 0 if the ## null hypothesis ("mean is M") cannot be rejected at the 5% significance ## level, or @var{h} = 1 if the null hypothesis can be rejected at the 5% level. ## ## @var{x} may also be a matrix or an N-D array. For matrices, @code{ztest} ## performs separate tests along each column of @var{x}, and returns a vector of ## results. For N-D arrays, @code{ztest} works along the first non-singleton ## dimension of @var{x}. @var{m} and @var{sigma} must be scalars. ## ## @code{ztest} treats NaNs as missing values, and ignores them. ## ## @code{[@var{h}, @var{pval}] = ztest (@dots{})} returns the p-value. That ## is the probability of observing the given result, or one more extreme, by ## chance if the null hypothesis true. ## ## @code{[@var{h}, @var{pval}, @var{ci}] = ztest (@dots{})} returns a ## 100 * (1 - @var{alpha})% confidence interval for the true mean. ## ## @code{[@var{h}, @var{pval}, @var{ci}, @var{zvalue}] = ztest (@dots{})} ## returns the value of the test statistic. ## ## @code{[@dots{}] = ztest (@dots{}, @var{Name}, @var{Value}, @dots{})} ## specifies one or more of the following @var{Name}/@var{Value} pairs: ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Name} @tab @var{Value} ## @item "alpha" @tab the significance level. Default is 0.05. ## ## @item "dim" @tab dimension to work along a matrix or an N-D array. ## ## @item "tail" @tab a string specifying the alternative hypothesis: ## @end multitable ## @multitable @columnfractions 0.15 0.75 ## @item "both" @tab "mean is not @var{m}" (two-tailed, default) ## @item "left" @tab "mean is less than @var{m}" (left-tailed) ## @item "right" @tab "mean is greater than @var{m}" (right-tailed) ## @end multitable ## ## @seealso{ttest, vartest, signtest, kstest} ## @end deftypefn function [h, pval, ci, zvalue] = ztest (x, m, sigma, varargin) ## Validate input arguments if (nargin < 3) error ("ztest: too few input arguments."); endif if (! isscalar (m) || ! isnumeric (m) || ! isreal (m)) error ("ztest: invalid value for mean."); endif if (! isscalar (sigma) || ! isnumeric (sigma) || ! isreal (sigma) || sigma < 0) error ("ztest: invalid value for standard deviation."); endif ## Add defaults alpha = 0.05; tail = 'both'; dim = []; if (nargin > 3) for idx = 4:2:nargin name = varargin{idx-3}; value = varargin{idx-2}; switch (lower (name)) case 'alpha' alpha = value; if (! isscalar (alpha) || ! isnumeric (alpha) || ... alpha <= 0 || alpha >= 1) error ("ztest: invalid VALUE for alpha."); endif case 'tail' tail = value; if (! any (strcmpi (tail, {'both', 'left', 'right'}))) error ("ztest: invalid VALUE for tail."); endif case 'dim' dim = value; if (! isscalar (dim) || ! ismember (dim, 1:ndims (x))) error ("ztest: invalid VALUE for operating dimension."); endif otherwise error ("ztest: invalid NAME for optional arguments."); endswitch endfor endif ## Figure out which dimension mean will work along if (isempty (dim)) dim = find (size (x) != 1, 1); if (isempty (dim)) dim = 1; endif endif ## Find sample size for each group, ignoring NaNs sz = sum (! isnan (x), dim); ## Calculate mean, standard error and z-value for each group x_mean = mean (x, dim, 'omitnan'); stderr = sigma ./ sqrt (sz); zvalue = (x_mean - m) ./ stderr; ## Calculate p-value for the test and confidence intervals (if requested) if (strcmpi (tail, 'both')) pval = 2 * normcdf (- abs (zvalue), 0, 1); if (nargout > 2) crit = norminv (1 - alpha / 2, 0, 1) .* stderr; ci = cat (dim, x_mean - crit, x_mean + crit); endif elseif (strcmpi (tail, 'right')) pval = normcdf (- zvalue, 0, 1); if (nargout > 2) crit = norminv (1 - alpha, 0, 1) .* stderr; ci = cat (dim, x_mean - crit, Inf (size (pval))); endif elseif (strcmpi (tail, 'left')) pval = normcdf (zvalue, 0, 1); if (nargout > 2) crit = norminv (1 - alpha, 0, 1) .* stderr; ci = cat (dim, - Inf (size (pval)), x_mean + crit); endif endif ## Determine the test outcome h = double (pval < alpha); h(isnan (pval)) = NaN; endfunction ## Test input validation %!error ztest (); %!error ... %! ztest ([1, 2, 3, 4], 2, -0.5); %!error ... %! ztest ([1, 2, 3, 4], 1, 2, 'alpha', 0); %!error ... %! ztest ([1, 2, 3, 4], 1, 2, 'alpha', 1.2); %!error ... %! ztest ([1, 2, 3, 4], 1, 2, 'alpha', 'val'); %!error ... %! ztest ([1, 2, 3, 4], 1, 2, 'tail', 'val'); %!error ... %! ztest ([1, 2, 3, 4], 1, 2, 'alpha', 0.01, 'tail', 'val'); %!error ... %! ztest ([1, 2, 3, 4], 1, 2, 'dim', 3); %!error ... %! ztest ([1, 2, 3, 4], 1, 2, 'alpha', 0.01, 'tail', 'both', 'dim', 3); %!error ... %! ztest ([1, 2, 3, 4], 1, 2, 'alpha', 0.01, 'tail', 'both', 'badoption', 3); ## Test results %!test %! load carsmall %! [h, pval, ci] = ztest (MPG, mean (MPG, 'omitnan'), std (MPG, 'omitnan')); %! assert_equal (h, 0); %! assert_equal (pval, 1, 1e-14); %! assert_equal (ci, [22.094; 25.343], 1e-3); %!test %! load carsmall %! [h, pval, ci] = ztest (MPG, 26, 8); %! assert_equal (h, 1); %! assert_equal (pval, 0.00568359158544743, 1e-14); %! assert_equal (ci, [22.101; 25.335], 1e-3); %!test %! load carsmall %! [h, pval, ci] = ztest (MPG, 26, 4); %! assert_equal (h, 1); %! assert_equal (pval, 3.184168011941316e-08, 1e-14); %! assert_equal (ci, [22.909; 24.527], 1e-3); %!test %! x = normrnd (10, 2, 100, 1); %! [h, pval, ci] = ztest (x, 10, 2, 'tail', 'right'); %! assert_equal (isnan (pval), false); %! assert_equal (pval >= 0 && pval <= 1, true); %!test %! x = normrnd (10, 2, 100, 1); %! [h, pval, ci] = ztest (x, 10, 2, 'tail', 'left'); %! assert_equal (isnan (pval), false); %! assert_equal (pval >= 0 && pval <= 1, true); %!test %! load fisheriris; %! x = meas(:,1); %! m = 5.8; %! sigma = 0.8; %! [h, pval, ci] = ztest (x, m, sigma, 'tail', 'right'); %! assert_equal (h, 0) %! assert_equal (pval, 0.2535, 1e-4) %! assert_equal (ci, [5.7359; Inf], 1e-5) %!test %! load fisheriris; %! x = meas(:,1); %! m = 5.8; %! sigma = 0.8; %! [h, pval, ci] = ztest (x, m, sigma, 'tail', 'left'); %! assert_equal (h, 0) %! assert_equal (pval, 0.7465, 1e-4) %! assert_equal (ci, [-Inf; 5.9508], 1e-4) %!test %! [h, pval, ci, zvalue] = ztest ([1, 2; 3, 4; 5, 6], 3, 1); %! assert_equal (h, [0, 0]); %! assert_equal (pval, [1, 0.0833], 1e-4); %! assert_equal (ci, [1.8684, 2.8684; 4.1316, 5.1316], 1e-4); %! assert_equal (zvalue, [0, 1.7321], 1e-4); %!test %! [h, pval, ci, zvalue] = ztest ([1, 2; NaN, 4; 5, 6], 3, 1); %! assert_equal (h, [0, 0]); %! assert_equal (pval, [1, 0.0833], 1e-4); %! assert_equal (ci, [1.6141, 2.8684; 4.3859, 5.1316], 1e-4); %! assert_equal (zvalue, [0, 1.7321], 1e-4); %!test %! h = ztest ([1, 2; NaN, 4; 5, 6], 3, 1, 'dim', 2); %! assert_equal (h, [1; 0; 1]); %!test %! h = ztest (reshape (1:12, 2, 3, 2), 3, 1); %! assert_equal (h, reshape ([1, 0, 1, 1, 1, 1], 1, 3, 2)); %!test %! [h, pval, ci, zvalue] = ztest (5, 0, 1); %! assert_equal (h, 1); %! assert_equal (pval, 5.7330e-07, 1e-11); %! assert_equal (ci, [3.0400; 6.9600], 1e-4); %! assert_equal (zvalue, 5); %!test %! assert_equal (ztest (NaN, 0, 1), NaN); %!test %! [h, pval, ci] = ztest ([], 0, 1); %! assert_equal (h, zeros (1, 0)); %! assert_equal (pval, zeros (1, 0)); %! assert_equal (ci, zeros (2, 0)); %!test %! [h, pval, ci] = ztest (zeros (0, 3), 0, 1); %! assert_equal (h, NaN (1, 3)); %! assert_equal (pval, NaN (1, 3)); %! assert_equal (ci, NaN (2, 3)); %!test %! [h, pval, ci] = ztest (zeros (3, 0), 0, 1); %! assert_equal (h, zeros (1, 0)); %! assert_equal (pval, zeros (1, 0)); %! assert_equal (ci, zeros (2, 0)); %!test %! assert_equal (ztest (zeros (0, 0, 3), 0, 1), zeros (1, 0, 3)); %!test %! [h, pval, ci] = ztest (zeros (1, 0), 0, 1); %! assert_equal (h, NaN); %! assert_equal (pval, NaN); %! assert_equal (ci, [NaN, NaN]); %!test %! [h, pval, ci] = ztest ([NaN; NaN], 0, 1); %! assert_equal (h, NaN); %! assert_equal (pval, NaN); %! assert_equal (ci, [NaN; NaN]); statistics-release-2.1.2/inst/Hypothesis_Testing/ztest2.m000066400000000000000000000232371527311104300235520ustar00rootroot00000000000000## Copyright (C) 1996-2017 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{h} =} ztest2 (@var{x1}, @var{n1}, @var{x2}, @var{n2}) ## @deftypefnx {statistics} {@var{h} =} ztest2 (@var{x1}, @var{n1}, @var{x2}, @var{n2}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{h}, @var{p}] =} ztest2 (@dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{p}, @var{ci}] =} ztest2 (@dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{p}, @var{ci}, @var{stats}] =} ztest2 (@dots{}) ## ## Two proportions Z-test. ## ## If @var{x1} and @var{n1} are the counts of successes and trials in one ## sample, and @var{x2} and @var{n2} those in a second one, test the null ## hypothesis that the success probabilities @math{p1} and @math{p2} are the ## same. The result is @var{h} = 0 if the null hypothesis cannot be rejected at ## the significance level @var{alpha}, or @var{h} = 1 if it can. ## ## Under the null, the test statistic approximately follows a standard normal ## distribution. ## ## The size of @var{h} and @var{p} is the common size of @var{x1}, @var{n1}, ## @var{x2}, and @var{n2}, which must be scalars or of common size. A scalar ## input functions as a constant matrix of the same size as the other inputs. ## ## @code{[@var{h}, @var{p}] = ztest2 (@dots{})} returns the p-value. That ## is the probability of observing the given result, or one more extreme, by ## chance if the null hypothesis true. ## ## @code{[@var{h}, @var{p}, @var{ci}] = ztest2 (@dots{})} returns a ## @math{100 (1 - alpha)}% confidence interval for @math{p1 - p2}, Newcombe's ## hybrid score interval, which combines the Wilson score intervals of the two ## proportions. It has one row per test, and is one-sided for a one-sided ## test: @code{[lower, 1]} for @qcode{'right'}, @code{[-1, upper]} for ## @qcode{'left'}. ## ## @code{[@var{h}, @var{p}, @var{ci}, @var{stats}] = ztest2 (@dots{})} also ## returns a structure with the following fields: ## ## @multitable @columnfractions 0.2 0.75 ## @item @qcode{zval} @tab the value of the test statistic. ## @item @qcode{CohensH} @tab Cohen's @math{h}, the difference between the ## arcsine transforms @math{2 asin (sqrt (p1)) - 2 asin (sqrt (p2))} of the ## two estimated proportions. ## @item @qcode{CohensHCI} @tab its confidence interval, ## @math{h -/+ z sqrt (1/n1 + 1/n2)} with @math{z} the normal quantile at the ## level of the test, one row per test, within @math{[-pi, pi]} and one-sided ## as @var{ci} is. ## @end multitable ## ## @code{[@dots{}] = ztest2 (@dots{}, @var{Name}, @var{Value}, @dots{})} ## specifies one or more of the following @var{Name}/@var{Value} pairs: ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'alpha'} @tab the significance level of @var{h} and the level ## of both confidence intervals. Default is 0.05. ## ## @item @qcode{'tail'} @tab a string specifying the alternative hypothesis ## @end multitable ## @multitable @columnfractions 0.25 0.65 ## @item @qcode{'both'} @tab @math{p1} is not @math{p2} ## (two-tailed, default) ## @item @qcode{'left'} @tab @math{p1} is less than @math{p2} ## (left-tailed) ## @item @qcode{'right'} @tab @math{p1} is greater than @math{p2} ## (right-tailed) ## @end multitable ## ## @seealso{chi2test, fishertest} ## @end deftypefn function [h, p, ci, stats] = ztest2 (x1, n1, x2, n2, varargin) if (nargin < 4) print_usage (); endif if (! isscalar (x1) || ! isscalar (n1) || ! isscalar (x2) || ! isscalar (n2)) [retval, x1, n1, x2, n2] = common_size (x1, n1, x2, n2); if (retval > 0) error ("ztest2: X1, N1, X2, and N2 must be of common size or scalars."); endif endif if (iscomplex (x1) || iscomplex (n1) || iscomplex (x2) || iscomplex (n2)) error ("ztest2: X1, N1, X2, and N2 must not be complex."); endif if (any (x1(:) > n1(:)) || any (x2(:) > n2(:))) error ("ztest2: X1 must be <= N1 and X2 must be <= N2."); endif ## Add defaults and parse optional arguments alpha = 0.05; tail = 'both'; if (nargin > 4) params = numel (varargin); if ((params / 2) != fix (params / 2)) error ("ztest2: optional arguments must be in NAME-VALUE pairs.") endif for idx = 1:2:params name = varargin{idx}; value = varargin{idx+1}; switch (lower (name)) case 'alpha' alpha = value; if (! isscalar (alpha) || ! isnumeric (alpha) || ... alpha <= 0 || alpha >= 1) error ("ztest2: invalid VALUE for alpha."); endif case 'tail' tail = lower (value); if (! any (strcmpi (tail, {'both', 'left', 'right'}))) error ("ztest2: invalid VALUE for tail."); endif otherwise error ("ztest2: invalid NAME for optional arguments."); endswitch endfor endif p1 = x1 ./ n1; p2 = x2 ./ n2; pc = (x1 + x2) ./ (n1 + n2); zvalue = (p1 - p2) ./ sqrt (pc .* (1 - pc) .* (1 ./ n1 + 1 ./ n2)); switch (tail) case 'both' p = 2 * normcdf (-abs (zvalue)); z = norminv (1 - alpha / 2); case 'right' p = normcdf (-zvalue); z = norminv (1 - alpha); case 'left' p = normcdf (zvalue); z = norminv (1 - alpha); endswitch ## Determine the test outcome h = double (p < alpha); h(isnan (p)) = NaN; ## Newcombe's hybrid score interval for p1 - p2, from the Wilson score ## interval of each proportion [l1, u1] = wilson (x1(:), n1(:), z); [l2, u2] = wilson (x2(:), n2(:), z); d = p1(:) - p2(:); lo = d - sqrt ((p1(:) - l1) .^ 2 + (u2 - p2(:)) .^ 2); hi = d + sqrt ((u1 - p1(:)) .^ 2 + (p2(:) - l2) .^ 2); ci = [lo, hi]; ## Cohen's h, with its interval on the arcsine scale stats.zval = zvalue; stats.CohensH = 2 * asin (sqrt (p1)) - 2 * asin (sqrt (p2)); se = sqrt (1 ./ n1(:) + 1 ./ n2(:)); hCI = stats.CohensH(:) + [-1, 1] .* z .* se; hCI = min (max (hCI, -pi), pi); ## A one-sided test has a one-sided interval if (strcmp (tail, 'right')) ci(:,2) = 1; hCI(:,2) = pi; elseif (strcmp (tail, 'left')) ci(:,1) = -1; hCI(:,1) = -pi; endif stats.CohensHCI = hCI; endfunction ## The Wilson score interval of the proportion X / N at the normal quantile Z function [lo, hi] = wilson (x, n, z) phat = x ./ n; centre = (x + z ^ 2 / 2) ./ (n + z ^ 2); half = z * sqrt (n) ./ (n + z ^ 2) ... .* sqrt (phat .* (1 - phat) + z ^ 2 ./ (4 * n)); lo = centre - half; hi = centre + half; endfunction %!test %! ## Values from R's prop.test without continuity correction %! [h, p] = ztest2 (30, 100, 18, 100); %! assert_equal ([h, p], [1, 0.046944726978481718], -1e-12); %!test %! [~, ~, ~, st] = ztest2 (30, 100, 18, 100); %! assert_equal (st.zval ^ 2, 3.9473684210526314, -1e-12); %!test %! ## Newcombe's interval, from the Wilson intervals of R's prop.test %! [~, ~, ci] = ztest2 (30, 100, 18, 100); %! assert_equal (ci, [0.0013340883914530755, 0.23469812426823411], -1e-12); %!test %! [h, p] = ztest2 (45, 60, 20, 70, 'tail', 'right'); %! assert_equal ([h, p], [1, 6.5305496467962847e-08], -1e-12); %!test %! [~, ~, ci] = ztest2 (45, 60, 20, 70, 'tail', 'right'); %! assert_equal (ci, [0.32502269598878675, 1], -1e-12); %!test %! [~, ~, ci] = ztest2 (45, 60, 20, 70, 'tail', 'left'); %! assert_equal (ci(1), -1); %!test %! [h, p] = ztest2 (30, 100, 18, 100, 'alpha', 0.01); %! assert_equal (h, 0); %!test %! ## Below the resolution of 1 - normcdf %! [~, p] = ztest2 (900, 1000, 300, 1000); %! assert_equal (p, 4.01237554141706e-165, -1e-10); %!test %! [~, ~, ~, st] = ztest2 (30, 100, 18, 100); %! assert_equal (st.CohensH, 2 * asin (sqrt (0.3)) - 2 * asin (sqrt (0.18)), ... %! -1e-14); %!test %! [~, ~, ~, st] = ztest2 (30, 100, 18, 100); %! assert_equal (st.CohensHCI, ... %! st.CohensH + [-1, 1] * norminv (0.975) * sqrt (2 / 100), ... %! -1e-14); %!test %! [~, ~, ~, st] = ztest2 (45, 60, 20, 70, 'tail', 'right'); %! assert_equal (st.CohensHCI(2), pi); %!test %! ## One row of the interval per test %! [~, ~, ci, st] = ztest2 ([30; 45], [100; 60], [18; 20], [100; 70]); %! assert_equal (size (ci), [2, 2]); %!test %! [~, ~, ci] = ztest2 ([30; 45], [100; 60], [18; 20], [100; 70]); %! [~, ~, c1] = ztest2 (45, 60, 20, 70); %! assert_equal (ci(2,:), c1); ## Test input validation %!error ztest2 (); %!error ztest2 (1); %!error ztest2 (1, 2); %!error ztest2 (1, 2, 3); %!error ztest2 (1, 2, 3, 2); %!error ... %! ztest2 (1, 2, 3, 4, 'alpha') %!error ... %! ztest2 (1, 2, 3, 4, 'alpha', 0); %!error ... %! ztest2 (1, 2, 3, 4, 'alpha', 1.2); %!error ... %! ztest2 (1, 2, 3, 4, 'alpha', 'val'); %!error ... %! ztest2 (1, 2, 3, 4, 'tail', 'val'); %!error ... %! ztest2 (1, 2, 3, 4, 'alpha', 0.01, 'tail', 'val'); %!error ... %! ztest2 (1, 2, 3, 4, 'alpha', 0.01, 'tail', 'both', 'badoption', 3); statistics-release-2.1.2/inst/Machine_Learning/000077500000000000000000000000001527311104300214615ustar00rootroot00000000000000statistics-release-2.1.2/inst/Machine_Learning/ClassificationBaggedEnsemble.m000066400000000000000000001130641527311104300273440ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ClassificationBaggedEnsemble < ClassificationEnsemble ## -*- texinfo -*- ## @deftp {statistics} ClassificationBaggedEnsemble ## ## Bagged ensemble of decision trees for classification ## ## A @code{ClassificationBaggedEnsemble} object holds decision trees each ## grown on a sample drawn from the training data in proportion to the ## observation weights, each split chosen from @code{ceil (sqrt (P))} ## predictors drawn afresh at every node. It predicts by averaging its ## trees' class probabilities. ## ## Create one with @code{fitcensemble} and @qcode{'Method'} set to ## @qcode{'Bag'}. It carries everything a @code{ClassificationEnsemble} ## does, and which rows each tree drew. A boosting method that resamples, ## asked for with @qcode{'Resample'}, @qcode{'FResample'} or ## @qcode{'Replace'}, returns this class too: its trees are boosted and ## summed by their weights, each grown on the rows it drew. ## ## @seealso{fitcensemble, ClassificationEnsemble, ## CompactClassificationEnsemble, TreeBagger} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationBaggedEnsemble} {property} FResample ## ## Share of the observations each tree draws ## ## A number greater than 0 and no greater than 1, each tree drawing ## @code{ceil (FResample * N)} observations. This property is read-only. ## ## @end deftp FResample = 1; ## -*- texinfo -*- ## @deftp {ClassificationBaggedEnsemble} {property} Replace ## ## Whether the trees draw with replacement ## ## A logical scalar, true by default. This property is read-only. ## ## @end deftp Replace = true; ## -*- texinfo -*- ## @deftp {ClassificationBaggedEnsemble} {property} UseObsForLearner ## ## Which observations each tree drew ## ## An @math{NxNumTrained} logical matrix, true where a tree's sample holds ## an observation. This property is read-only. ## ## @end deftp UseObsForLearner = []; endproperties methods (Access = public) ## -*- texinfo -*- ## @deftypefn {ClassificationBaggedEnsemble} {@var{obj} =} ClassificationBaggedEnsemble (@var{X}, @var{Y}) ## @deftypefnx {ClassificationBaggedEnsemble} {@var{obj} =} ClassificationBaggedEnsemble (@dots{}, @var{name}, @var{value}) ## ## Fit a bagged ensemble of decision trees. ## ## @code{fitcensemble} with @qcode{'Method'} set to @qcode{'Bag'} is the ## documented way in, and its help lists the options both take. ## ## @seealso{fitcensemble, ClassificationEnsemble} ## @end deftypefn function this = ClassificationBaggedEnsemble (X, Y, varargin) if (nargin < 2) error ("ClassificationBaggedEnsemble: too few input arguments."); endif ## A table names its own predictors and says which hold levels ## An ordinal column is a numeric predictor here, as in R2024a [this, X, Y, varargin] = resolveTable (this, 'ClassificationBaggedEnsemble', ... X, Y, varargin, true); if (! any (cellfun (@(a) ischar (a) && strcmpi (a, 'Method'), ... varargin(1:2:end)))) varargin(end+1:end+2) = {'Method', 'Bag'}; endif this = this@ClassificationEnsemble (X, Y, varargin{:}); this = bagProperties (this); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationBaggedEnsemble} {@var{CVMdl} =} crossval (@var{obj}) ## @deftypefnx {ClassificationBaggedEnsemble} {@var{CVMdl} =} crossval (@dots{}, @var{name}, @var{value}) ## ## Cross-validate a bagged ensemble. ## ## Behaves as @code{ClassificationEnsemble.crossval}, returning a ## @code{ClassificationPartitionedEnsemble}. ## ## @seealso{ClassificationBaggedEnsemble, ClassificationPartitionedEnsemble} ## @end deftypefn function CVMdl = crossval (this, varargin) CVMdl = crossval@ClassificationEnsemble (this, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationBaggedEnsemble} {@var{CMdl} =} compact (@var{obj}) ## ## Drop the training data from a bagged ensemble. ## ## Returns a @code{CompactClassificationEnsemble}, as ## @code{ClassificationEnsemble.compact} does. ## ## @seealso{ClassificationBaggedEnsemble, CompactClassificationEnsemble} ## @end deftypefn function CMdl = compact (this) CMdl = compact@ClassificationEnsemble (this); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationBaggedEnsemble} {@var{B} =} resume (@var{obj}, @var{NumLearningCycles}) ## @deftypefnx {ClassificationBaggedEnsemble} {@var{B} =} resume (@dots{}, 'NPrint', @var{n}) ## ## Grow more trees in a bagged ensemble. ## ## Behaves as @code{ClassificationEnsemble.resume}, the new trees' samples ## added to @code{UseObsForLearner}. ## ## @seealso{ClassificationBaggedEnsemble, ClassificationEnsemble.resume} ## @end deftypefn function this = resume (this, varargin) this = resume@ClassificationEnsemble (this, varargin{:}); this = bagProperties (this); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationBaggedEnsemble} {@var{label} =} predict (@var{obj}, @var{X}) ## @deftypefnx {ClassificationBaggedEnsemble} {[@var{label}, @var{scores}] =} predict (@dots{}) ## @deftypefnx {ClassificationBaggedEnsemble} {[@dots{}] =} predict (@dots{}, @var{name}, @var{value}) ## ## Classify new data with a bagged ensemble. ## ## Behaves as @code{CompactClassificationEnsemble.predict}. ## ## @seealso{ClassificationBaggedEnsemble, ## CompactClassificationEnsemble.predict} ## @end deftypefn function [label, scores] = predict (this, varargin) [label, scores] = predict@ClassificationEnsemble (this, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationBaggedEnsemble} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationBaggedEnsemble} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationBaggedEnsemble} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {ClassificationBaggedEnsemble} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Classification loss of a bagged ensemble. ## ## Behaves as @code{CompactClassificationEnsemble.loss}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{ClassificationBaggedEnsemble, ## CompactClassificationEnsemble.loss} ## @end deftypefn function L = loss (this, varargin) L = loss@ClassificationEnsemble (this, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationBaggedEnsemble} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationBaggedEnsemble} {@var{e} =} edge (@dots{}, @var{name}, @var{value}) ## @deftypefnx {ClassificationBaggedEnsemble} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationBaggedEnsemble} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Classification edge of a bagged ensemble. ## ## Behaves as @code{CompactClassificationEnsemble.edge}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{ClassificationBaggedEnsemble, ## CompactClassificationEnsemble.edge} ## @end deftypefn function e = edge (this, varargin) e = edge@ClassificationEnsemble (this, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationBaggedEnsemble} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationBaggedEnsemble} {@var{m} =} margin (@dots{}, @var{name}, @var{value}) ## @deftypefnx {ClassificationBaggedEnsemble} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationBaggedEnsemble} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margins of a bagged ensemble. ## ## Behaves as @code{CompactClassificationEnsemble.margin}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{ClassificationBaggedEnsemble, ## CompactClassificationEnsemble.margin} ## @end deftypefn function m = margin (this, varargin) m = margin@ClassificationEnsemble (this, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationBaggedEnsemble} {@var{label} =} resubPredict (@var{obj}) ## @deftypefnx {ClassificationBaggedEnsemble} {[@var{label}, @var{scores}] =} resubPredict (@dots{}) ## ## Classify the training data with a bagged ensemble. ## ## Behaves as @code{ClassificationEnsemble.resubPredict}. ## ## @seealso{ClassificationBaggedEnsemble, ## ClassificationEnsemble.resubPredict} ## @end deftypefn function [label, scores] = resubPredict (this, varargin) [label, scores] = resubPredict@ClassificationEnsemble (this, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationBaggedEnsemble} {@var{L} =} resubLoss (@var{obj}) ## @deftypefnx {ClassificationBaggedEnsemble} {@var{L} =} resubLoss (@dots{}, @var{name}, @var{value}) ## ## Classification loss of a bagged ensemble on the training data. ## ## Behaves as @code{ClassificationEnsemble.resubLoss}. ## ## @seealso{ClassificationBaggedEnsemble, ClassificationEnsemble.resubLoss} ## @end deftypefn function L = resubLoss (this, varargin) L = resubLoss@ClassificationEnsemble (this, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationBaggedEnsemble} {@var{e} =} resubEdge (@var{obj}) ## @deftypefnx {ClassificationBaggedEnsemble} {@var{e} =} resubEdge (@dots{}, @var{name}, @var{value}) ## ## Classification edge of a bagged ensemble on the training data. ## ## Behaves as @code{ClassificationEnsemble.resubEdge}. ## ## @seealso{ClassificationBaggedEnsemble, ClassificationEnsemble.resubEdge} ## @end deftypefn function e = resubEdge (this, varargin) e = resubEdge@ClassificationEnsemble (this, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationBaggedEnsemble} {@var{m} =} resubMargin (@var{obj}) ## @deftypefnx {ClassificationBaggedEnsemble} {@var{m} =} resubMargin (@dots{}, @var{name}, @var{value}) ## ## Classification margins of the training data of a bagged ensemble. ## ## Behaves as @code{ClassificationEnsemble.resubMargin}. ## ## @seealso{ClassificationBaggedEnsemble, ## ClassificationEnsemble.resubMargin} ## @end deftypefn function m = resubMargin (this, varargin) m = resubMargin@ClassificationEnsemble (this, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationBaggedEnsemble} {@var{imp} =} predictorImportance (@var{obj}) ## @deftypefnx {ClassificationBaggedEnsemble} {[@var{imp}, @var{ma}] =} predictorImportance (@var{obj}) ## ## Estimate the importance of each predictor. ## ## The mean over the trees of each tree's @code{predictorImportance}, as ## @code{CompactClassificationEnsemble.predictorImportance} computes it. ## ## @seealso{ClassificationBaggedEnsemble, ## ClassificationBaggedEnsemble.oobPermutedPredictorImportance} ## @end deftypefn function [imp, ma] = predictorImportance (this) [imp, ma] = predictorImportance@ClassificationEnsemble (this); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationBaggedEnsemble} {@var{label} =} oobPredict (@var{obj}) ## @deftypefnx {ClassificationBaggedEnsemble} {[@var{label}, @var{scores}] =} oobPredict (@dots{}) ## @deftypefnx {ClassificationBaggedEnsemble} {[@dots{}] =} oobPredict (@dots{}, 'Learners', @var{idx}) ## ## Out-of-bag predictions for the training data. ## ## Each training observation is classified by the trees whose samples ## left it out, as @code{predict} classifies it with ## @qcode{'UseObsForLearner'} set to @code{! UseObsForLearner}. An ## observation in the sample of every tree used has @code{NaN} scores and ## is given the class of greatest prior probability. @qcode{'Learners'} ## restricts the trees. ## ## @seealso{ClassificationBaggedEnsemble, ## ClassificationBaggedEnsemble.oobLoss} ## @end deftypefn function [label, scores] = oobPredict (this, varargin) caller = 'ClassificationBaggedEnsemble.oobPredict'; args = oobArgs (varargin, {'Learners'}, caller); [label, scores] = ensemblePredict (compact (this), this.X, ... [{'UseObsForLearner', ... ! this.UseObsForLearner}, args], ... caller); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationBaggedEnsemble} {@var{L} =} oobLoss (@var{obj}) ## @deftypefnx {ClassificationBaggedEnsemble} {@var{L} =} oobLoss (@dots{}, @var{name}, @var{value}) ## ## Out-of-bag classification loss. ## ## The loss of the out-of-bag scores against @code{Y}, weighted by ## @code{W}, an observation in the sample of every tree used being left ## out. @qcode{'LossFun'} and @qcode{'Mode'} are taken as by ## @code{CompactClassificationEnsemble.loss}, and @qcode{'Learners'} ## restricts the trees. ## ## @seealso{ClassificationBaggedEnsemble, ## ClassificationBaggedEnsemble.oobPredict} ## @end deftypefn function L = oobLoss (this, varargin) caller = 'ClassificationBaggedEnsemble.oobLoss'; args = oobArgs (varargin, {'Learners', 'LossFun', 'Mode'}, caller); L = ensembleLoss (compact (this), this.X, this.Y, ... [{'UseObsForLearner', ! this.UseObsForLearner, ... 'Weights', this.W}, args], caller); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationBaggedEnsemble} {@var{e} =} oobEdge (@var{obj}) ## @deftypefnx {ClassificationBaggedEnsemble} {@var{e} =} oobEdge (@dots{}, @var{name}, @var{value}) ## ## Out-of-bag classification edge. ## ## The weighted mean of the out-of-bag margins, weighted by @code{W}. ## @qcode{'Mode'} and @qcode{'Learners'} are taken as by @code{oobLoss}. ## ## @seealso{ClassificationBaggedEnsemble, ## ClassificationBaggedEnsemble.oobMargin} ## @end deftypefn function e = oobEdge (this, varargin) caller = 'ClassificationBaggedEnsemble.oobEdge'; args = oobArgs (varargin, {'Learners', 'Mode'}, caller); e = ensembleEdge (compact (this), this.X, this.Y, ... [{'UseObsForLearner', ! this.UseObsForLearner, ... 'Weights', this.W}, args], caller); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationBaggedEnsemble} {@var{m} =} oobMargin (@var{obj}) ## @deftypefnx {ClassificationBaggedEnsemble} {@var{m} =} oobMargin (@dots{}, 'Learners', @var{idx}) ## ## Out-of-bag classification margins. ## ## The margin of each training observation under its out-of-bag scores, ## @code{NaN} for one in the sample of every tree used. ## ## @seealso{ClassificationBaggedEnsemble, ## ClassificationBaggedEnsemble.oobEdge} ## @end deftypefn function m = oobMargin (this, varargin) caller = 'ClassificationBaggedEnsemble.oobMargin'; args = oobArgs (varargin, {'Learners'}, caller); m = ensembleMargin (compact (this), this.X, this.Y, ... [{'UseObsForLearner', ... ! this.UseObsForLearner}, args], caller); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationBaggedEnsemble} {@var{imp} =} oobPermutedPredictorImportance (@var{obj}) ## @deftypefnx {ClassificationBaggedEnsemble} {@var{imp} =} oobPermutedPredictorImportance (@dots{}, 'Learners', @var{idx}) ## ## Out-of-bag predictor importance by permutation. ## ## For each tree, the values of each predictor are permuted among the ## observations out of its bag, and the tree's misclassification rate on ## them, weighted by @code{W}, is taken before and after. @var{imp} holds, ## for each predictor, the mean of the rise over the trees divided by its ## standard deviation over the trees, zero where the mean is zero. ## @qcode{'Learners'} restricts the trees. ## ## @seealso{ClassificationBaggedEnsemble, ## ClassificationBaggedEnsemble.predictorImportance} ## @end deftypefn function imp = oobPermutedPredictorImportance (this, varargin) caller = 'ClassificationBaggedEnsemble.oobPermutedPredictorImportance'; if (mod (numel (varargin), 2) != 0) error ("%s: name-value arguments must be in pairs.", caller); endif ## Parse optional paired arguments; empty 'Learners' stands for every ## trained learner [Learners, Options, args] = ... parsePairedArguments ({'Learners', 'Options'}, {[], []}, ... varargin(:)); ## Validate optional paired arguments if (isempty (Learners)) learners = 1:this.NumTrained; else learners = oobLearners (Learners, this.NumTrained, caller); endif if (! isempty (Options)) error ("%s: 'Options' is not implemented.", caller); endif if (! isempty (args)) error ("%s: invalid optional paired argument.", caller); endif p = columns (this.X); gY = labelIndices (this.ClassNames, this.Y); D = zeros (numel (learners), p); for j = 1:numel (learners) t = learners(j); r = find (! this.UseObsForLearner(:,t)); w = double (this.W(r)); if (isempty (r) || ! (sum (w) > 0)) continue; endif w /= sum (w); tree = this.Trained{t}; Xo = this.X(r,:); miss = @(Z) labelIndices (this.ClassNames, predict (tree, Z)) != gY(r); e0 = sum (w .* miss (Xo)); for v = 1:p Xp = Xo; Xp(:,v) = Xo(randperm (numel (r)), v); D(j,v) = sum (w .* miss (Xp)) - e0; endfor endfor imp = mean (D, 1) ./ std (D, 0, 1); imp(mean (D, 1) == 0) = 0; endfunction endmethods methods (Access = private) ## The public bag properties, from what the fit keeps in the parent. function this = bagProperties (this) this.FResample = this.BagFResample; this.Replace = this.BagReplace; this.UseObsForLearner = this.BagInBag; endfunction endmethods endclassdef ## The Name-Value pairs of an out-of-bag method, refused unless in ALLOWED. function args = oobArgs (args, allowed, caller) if (mod (numel (args), 2) != 0) error ("%s: name-value arguments must be in pairs.", caller); endif for i = 1:2:numel (args) if (! (ischar (args{i}) && any (strcmpi (args{i}, allowed)))) error ("%s: invalid optional paired argument.", caller); endif endfor endfunction ## The indices of the learners 'Learners' names. function learners = oobLearners (val, T, caller) if (! (isnumeric (val) && isvector (val) && isreal (val) && all (val >= 1) && all (val <= T) && all (val == fix (val)))) error (strcat ("%s: 'Learners' must be a vector of indices of", ... " trained learners."), caller); endif learners = double (val(:)'); endfunction ## Test output %!test # MATLAB parity: the properties of a bagged ensemble %! load fisheriris %! Mdl = ClassificationBaggedEnsemble (meas, species, 'NumLearningCycles', 3); %! assert_equal (numel (properties (Mdl)), 29); %! assert_equal (Mdl.Method, 'Bag'); %! assert_equal (Mdl.TrainedWeights, [1; 1; 1]); %! assert_equal (Mdl.FitInfo, []); %! assert_equal (Mdl.FitInfoDescription, 'None'); %! assert_equal (Mdl.FResample, 1); %! assert_equal (Mdl.Replace, true); %! assert_equal (size (Mdl.UseObsForLearner), [150, 3]); %! assert_equal (isfield (Mdl.ModelParameters, 'LearnRate'), false); %!test # MATLAB parity: the scores are the mean of the trees' probabilities %! load fisheriris %! rng (5); %! Mdl = ClassificationBaggedEnsemble (meas, species, 'NumLearningCycles', 5); %! [~, s] = predict (Mdl, meas); %! P = zeros (150, 3); %! for t = 1:5 %! [~, st] = predict (Mdl.Trained{t}, meas); %! [~, k] = ismember (Mdl.Trained{t}.ClassNames, Mdl.ClassNames); %! P(:,k) += st; %! endfor %! assert_equal (s, P / 5, 1e-15); %!test # MATLAB parity: a subset of trees is averaged over the subset %! load fisheriris %! rng (5); %! Mdl = ClassificationBaggedEnsemble (meas, species, 'NumLearningCycles', 3); %! [~, s] = predict (Mdl, meas(1:2,:), 'Learners', [1, 3]); %! [~, a] = predict (Mdl.Trained{1}, meas(1:2,:)); %! [~, b] = predict (Mdl.Trained{3}, meas(1:2,:)); %! assert_equal (s, (a + b) / 2, 1e-15); %!test # MATLAB parity: each tree draws FResample of the rows %! load fisheriris %! rng (8); %! Mdl = ClassificationBaggedEnsemble (meas, species, ... %! 'NumLearningCycles', 3, ... %! 'Replace', 'off', 'FResample', 0.5); %! assert_equal (sum (Mdl.UseObsForLearner), [75, 75, 75]); %! assert_equal (Mdl.Trained{1}.NodeSize(1), 75); %! R = ClassificationBaggedEnsemble (meas, species, 'NumLearningCycles', 1, ... %! 'FResample', 0.5); %! assert_equal (R.Trained{1}.NodeSize(1), 75); %!test # MATLAB parity: the bootstrap draws in proportion to the weights %! load fisheriris %! rng (9); %! w = [10 * ones(50, 1); ones(100, 1)]; %! Mdl = ClassificationBaggedEnsemble (meas, species, ... %! 'NumLearningCycles', 20, 'Weights', w); %! assert_equal (Mdl.Prior, [5, 0.5, 0.5] / 6, 1e-15); %! share = mean (sum (Mdl.UseObsForLearner(1:50,:)) / 50); %! assert_equal (share > 0.85, true); %!test # resuming adds the new trees' samples %! load fisheriris %! Mdl = ClassificationBaggedEnsemble (meas, species, 'NumLearningCycles', 2); %! Mdl = resume (Mdl, 2); %! assert_equal (class (Mdl), 'ClassificationBaggedEnsemble'); %! assert_equal (Mdl.NumTrained, 4); %! assert_equal (size (Mdl.UseObsForLearner), [150, 4]); %! assert_equal (Mdl.ModelParameters.NLearn, 4); %!test # MATLAB parity: the cumulative loss of the bagged trees %! load fisheriris %! Mdl = ClassificationBaggedEnsemble (meas, species, 'NumLearningCycles', 3); %! L = resubLoss (Mdl, 'Mode', 'cumulative'); %! assert_equal (size (L), [3, 1]); %! assert_equal (L(end), resubLoss (Mdl), 1e-15); %! assert_equal (class (compact (Mdl)), 'CompactClassificationEnsemble'); ## Test input validation %!error ... %! ClassificationBaggedEnsemble (1) %!error ... %! load fisheriris %! ClassificationBaggedEnsemble (meas, species, 'Method', 'AdaBoostM2') %!error ... %! load fisheriris %! ClassificationBaggedEnsemble (meas, species, 'LearnRate', 0.5) %!error ... %! load fisheriris %! predict (ClassificationBaggedEnsemble (meas, species, ... %! 'NumLearningCycles', 1)) %!test # MATLAB parity: the importance of a bagged ensemble is the mean %! load fisheriris %! rng (1); %! Mdl = ClassificationBaggedEnsemble (meas, species, 'NumLearningCycles', 5); %! I = cell2mat (cellfun (@(t) predictorImportance (t), Mdl.Trained, ... %! 'UniformOutput', false)); %! assert_equal (predictorImportance (Mdl), mean (I), 1e-15); %!test # MATLAB parity: out-of-bag predictions invert UseObsForLearner %! load fisheriris %! rng (2); %! Mdl = ClassificationBaggedEnsemble (meas, species, 'NumLearningCycles', 6); %! U = ! Mdl.UseObsForLearner; %! [l1, s1] = oobPredict (Mdl); %! [l2, s2] = predict (Mdl, meas, 'UseObsForLearner', U); %! assert_equal (l1, l2); %! assert_equal (isequaln (s1, s2), true); %! r = find (all (Mdl.UseObsForLearner, 2)); %! assert_equal (all (isnan (s1(r,:))(:)), true); %!test # MATLAB parity: the out-of-bag loss leaves out rows in every bag %! load fisheriris %! rng (2); %! Mdl = ClassificationBaggedEnsemble (meas, species, 'NumLearningCycles', 6); %! [~, s] = oobPredict (Mdl); %! have = ! any (isnan (s), 2); %! g = grp2idx (species); %! st = s(sub2ind (size (s), (1:150)', g)); %! so = s; %! so(sub2ind (size (s), (1:150)', g)) = -Inf; %! miss = st <= max (so, [], 2); %! assert_equal (oobLoss (Mdl), mean (miss(have)), 1e-15); %! U = ! Mdl.UseObsForLearner; %! assert_equal (oobLoss (Mdl, 'Mode', 'cumulative'), ... %! loss (Mdl, meas, species, 'UseObsForLearner', U, ... %! 'Mode', 'cumulative'), 1e-15); %! assert_equal (oobLoss (Mdl, 'Learners', [2, 4]), ... %! loss (Mdl, meas, species, 'UseObsForLearner', U, ... %! 'Learners', [2, 4]), 1e-15); %!test # MATLAB parity: the out-of-bag edge and margins %! load fisheriris %! rng (2); %! Mdl = ClassificationBaggedEnsemble (meas, species, 'NumLearningCycles', 6); %! U = ! Mdl.UseObsForLearner; %! assert_equal (oobEdge (Mdl), edge (Mdl, meas, species, ... %! 'UseObsForLearner', U), 1e-15); %! m = oobMargin (Mdl); %! assert_equal (isequaln (m, margin (Mdl, meas, species, ... %! 'UseObsForLearner', U)), true); %! assert_equal (oobEdge (Mdl), mean (m(! isnan (m))), 1e-14); %!test # MATLAB parity: permuted importance of one tree is zero or infinite %! load fisheriris %! rng (4); %! Mdl = ClassificationBaggedEnsemble (meas, species, 'NumLearningCycles', 1); %! imp = oobPermutedPredictorImportance (Mdl); %! assert_equal (size (imp), [1, 4]); %! assert_equal (all (imp == 0 | isinf (imp)), true); %!test # MATLAB parity: a constant predictor has zero permuted importance %! load fisheriris %! rng (5); %! Mdl = ClassificationBaggedEnsemble ([meas, ones(150, 1)], species, ... %! 'NumLearningCycles', 20); %! imp = oobPermutedPredictorImportance (Mdl); %! assert_equal (imp(5), 0); %! assert_equal (size (oobPermutedPredictorImportance (Mdl, ... %! 'Learners', 1:5)), ... %! [1, 5]); %!test # MATLAB parity: permuting a petal measurement matters most %! load fisheriris %! rng (3); %! Mdl = ClassificationBaggedEnsemble (meas, species, 'NumLearningCycles', 60); %! imp = oobPermutedPredictorImportance (Mdl); %! assert_equal (min (imp(3:4)) > max (imp(1:2)), true); %!error ... %! load fisheriris %! oobPredict (ClassificationBaggedEnsemble (meas, species, ... %! 'NumLearningCycles', 1), ... %! 'UseObsForLearner', true (150, 1)) %!error ... %! load fisheriris %! oobLoss (ClassificationBaggedEnsemble (meas, species, ... %! 'NumLearningCycles', 1), 'Mode') %!error ... %! load fisheriris %! oobLoss (ClassificationBaggedEnsemble (meas, species, ... %! 'NumLearningCycles', 1), ... %! 'Weights', ones (150, 1)) %!error ... %! load fisheriris %! oobEdge (ClassificationBaggedEnsemble (meas, species, ... %! 'NumLearningCycles', 1), ... %! 'LossFun', 'hinge') %!error ... %! load fisheriris %! oobMargin (ClassificationBaggedEnsemble (meas, species, ... %! 'NumLearningCycles', 1), ... %! 'Mode', 'cumulative') %!error ... %! load fisheriris %! oobPermutedPredictorImportance (ClassificationBaggedEnsemble (meas, ... %! species, 'NumLearningCycles', 1), ... %! 'Learners', 2) %!error ... %! load fisheriris %! oobPermutedPredictorImportance (ClassificationBaggedEnsemble (meas, ... %! species, 'NumLearningCycles', 1), ... %! 'Options', struct ()) %!error ... %! load fisheriris %! oobPermutedPredictorImportance (ClassificationBaggedEnsemble (meas, ... %! species, 'NumLearningCycles', 1), ... %! 'Bogus', struct ()) %!shared X2, Y2, S %! load fisheriris %! X2 = meas(51:150,:); %! Y2 = species(51:150); %! S = templateTree ('MaxNumSplits', 1); %!test # MATLAB parity: drawing every row without replacement is plain boosting %! M = ClassificationBaggedEnsemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 5, 'Learners', S, ... %! 'FResample', 1, 'Replace', 'off'); %! assert_equal (M.Method, 'AdaBoostM1'); %! assert_equal (M.CombineWeights, 'WeightedSum'); %! assert_equal (M.UseObsForLearner, true (100, 5)); %! assert_equal (M.TrainedWeights', [1.37576765652097, 0.99353411077441, ... %! 0.883434373403997, 0.554364192108759, ... %! 0.268194447432049], 1e-12); %!test # MATLAB parity: a resampling boosting method draws with replacement %! M = ClassificationBaggedEnsemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 5, 'Learners', S, ... %! 'Resample', 'on'); %! assert_equal ([M.FResample, M.Replace], [1, true]); %! assert_equal (size (M.UseObsForLearner), [100, M.NumTrained]); %! assert_equal (M.FitInfo * 100, round (M.FitInfo * 100), 1e-9); %!test # MATLAB parity: AdaBoostM1 reweights only the rows it drew %! M = ClassificationBaggedEnsemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 4, 'Learners', S, ... %! 'FResample', 0.5, 'Replace', 'off'); %! y = 2 * strcmp (Y2, M.ClassNames{1}) - 1; %! d = M.W / sum (M.W); %! for t = 1:M.NumTrained %! u = M.UseObsForLearner(:,t); %! h = 2 * strcmp (predict (M.Trained{t}, X2), M.ClassNames{1}) - 1; %! e = sum (d(u) .* (h(u) != y(u))) / sum (d(u)); %! assert_equal (M.FitInfo(t), e, 1e-12); %! s0 = sum (d(u)); %! d(u) = d(u) .* exp (-M.TrainedWeights(t) * y(u) .* h(u)); %! d(u) = d(u) / sum (d(u)) * s0; %! endfor %!test # MATLAB parity: GentleBoost reweights only the rows it drew %! M = ClassificationBaggedEnsemble (X2, Y2, 'Method', 'GentleBoost', ... %! 'NumLearningCycles', 4, 'Learners', S, ... %! 'FResample', 0.5, 'Replace', 'off'); %! y = 2 * strcmp (Y2, M.ClassNames{1}) - 1; %! d = M.W / sum (M.W); %! for t = 1:M.NumTrained %! u = M.UseObsForLearner(:,t); %! h = predict (M.Trained{t}, X2); %! assert_equal (M.FitInfo(t), ... %! sum (d(u) .* (y(u) - h(u)) .^ 2) / sum (d(u)), 1e-12); %! s0 = sum (d(u)); %! d(u) = d(u) .* exp (-y(u) .* h(u)); %! d(u) = d(u) / sum (d(u)) * s0; %! endfor %!test # MATLAB parity: LogitBoost moves the score of the rows it drew %! M = ClassificationBaggedEnsemble (X2, Y2, 'Method', 'LogitBoost', ... %! 'NumLearningCycles', 4, 'Learners', S, ... %! 'FResample', 0.5, 'Replace', 'off'); %! y01 = double (strcmp (Y2, M.ClassNames{1})); %! w0 = M.W / sum (M.W); %! w = w0; %! F = zeros (100, 1); %! for t = 1:M.NumTrained %! u = M.UseObsForLearner(:,t); %! p = 1 ./ (1 + exp (-F)); %! z = (y01 - p) ./ (p .* (1 - p)); %! h = predict (M.Trained{t}, X2); %! assert_equal (M.FitInfo(t), ... %! sum (w(u) .* (z(u) - h(u)) .^ 2) / sum (w(u)), 1e-10); %! F(u) += h(u) / 2; %! s0 = sum (w(u)); %! pu = 1 ./ (1 + exp (-F(u))); %! w(u) = w0(u) .* pu .* (1 - pu); %! w(u) = w(u) / sum (w(u)) * s0; %! endfor %!test # MATLAB parity: AdaBoostM2 keeps one weight per observation %! load fisheriris %! M = ClassificationBaggedEnsemble (meas, species, 'Method', 'AdaBoostM2', ... %! 'NumLearningCycles', 3, 'Learners', S, ... %! 'FResample', 0.5, 'Replace', 'off'); %! g = grp2idx (species); %! tru = sub2ind ([150, 3], (1:150)', g); %! w = M.W / sum (M.W); %! for t = 1:M.NumTrained %! u = M.UseObsForLearner(:,t); %! [~, P] = predict (M.Trained{t}, meas); %! hy = P(tru); %! L = 1 - hy + P; %! L(tru) = 0; %! e = sum (w(u) .* sum (L(u,:), 2)) / sum (w(u)) / 4; %! assert_equal (M.FitInfo(t), e, 1e-12); %! E = exp (-M.TrainedWeights(t) * (1 + hy - P)); %! E(tru) = 0; %! s0 = sum (w(u)); %! w(u) = w(u) .* sum (E(u,:), 2); %! w(u) = w(u) / sum (w(u)) * s0; %! endfor %!test # MATLAB parity: out-of-bag scores sum the learners that did not draw %! M = ClassificationBaggedEnsemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 8, 'Learners', S, ... %! 'Resample', 'on'); %! [~, s] = oobPredict (M); %! out = ! M.UseObsForLearner; %! man = zeros (100, 1); %! for t = 1:M.NumTrained %! h = 2 * strcmp (predict (M.Trained{t}, X2), M.ClassNames{1}) - 1; %! man += M.TrainedWeights(t) * h .* out(:,t); %! endfor %! k = any (out, 2); %! assert_equal (s(k,1), man(k), 1e-12); %!test # resume grows the record of the rows each learner drew %! M = ClassificationBaggedEnsemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 2, 'Learners', S, ... %! 'FResample', 0.7, 'Replace', 'off'); %! R = resume (M, 2); %! assert_equal (size (R.UseObsForLearner), [100, R.NumTrained]); %!test # a resampled boosting ensemble cross-validates with its learning rate %! M = ClassificationBaggedEnsemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 2, 'Learners', S, ... %! 'FResample', 0.7, 'LearnRate', 0.5); %! CV = crossval (M, 'KFold', 3); %! assert_equal (CV.Trainable{1}.LearnRate, 0.5); %! assert_equal (class (CV.Trainable{1}), 'ClassificationBaggedEnsemble'); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcensemble (T, 'Species', 'Method', 'Bag'); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'Species'), a); %! assert_equal (loss (Mdl, T), a); ## A table at edge %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcensemble (T, 'Species', 'Method', 'Bag'); %! a = edge (Mdl, X, y); %! assert_equal (edge (Mdl, T(:,1:2), y), a); %! assert_equal (edge (Mdl, T, 'Species'), a); %! assert_equal (edge (Mdl, T), a); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcensemble (T, 'Species', 'Method', 'Bag'); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); statistics-release-2.1.2/inst/Machine_Learning/ClassificationDiscriminant.m000066400000000000000000005540601527311104300271510ustar00rootroot00000000000000## Copyright (C) 2024 Ruchika Sonagote ## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ClassificationDiscriminant < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} ClassificationDiscriminant ## ## Discriminant analysis classification ## ## The @code{ClassificationDiscriminant} class implements a ## discriminant analysis classifier object, which can predict responses for ## new data using the @code{predict} method. ## ## Discriminant analysis classification is a statistical method used to ## classify observations into predefined groups based on their ## characteristics. It estimates the parameters of different distributions ## for each class and predicts the class of new observations by finding the ## one with the smallest misclassification cost. ## ## Create a @code{ClassificationDiscriminant} object by using the ## @code{fitcdiscr} function or the class constructor. ## ## ## Six discriminant types are available, in two families. The linear family, ## @qcode{'linear'}, @qcode{'diagLinear'} and @qcode{'pseudoLinear'}, pools ## one covariance across the classes and separates them with a hyperplane. ## The quadratic family, @qcode{'quadratic'}, @qcode{'diagQuadratic'} and ## @qcode{'pseudoQuadratic'}, estimates a covariance per class and separates ## them with a quadric. A @qcode{'diag'} type keeps only the variances, ## which is the same model as a @qcode{Gamma} of 1, and a @qcode{'pseudo'} ## type inverts a singular covariance rather than refusing it. ## ## @qcode{DiscrimType} may be assigned after fitting, but @emph{only within ## its own family}: the family is fixed when the model is fitted, because it ## decides which covariances the fit has to estimate. Assigning it, or ## @qcode{Gamma}, re-derives @qcode{Sigma}, @qcode{LogDetSigma} and ## @qcode{Coeffs} without refitting. ## ## @seealso{fitcdiscr} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} W ## ## Observation weights ## ## A numeric column vector with one entry per observation used for fitting: ## the @qcode{'Weights'} given, or equal weights, scaled to sum to one. The ## prior does not enter them. It has the class of the @qcode{'Weights'} ## given, single or double. This property is read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} X ## ## Predictor data ## ## A numeric matrix containing the unstandardized predictor data. Each ## column of @var{X} represents one predictor (variable), and each row ## represents one observation. This property is read-only. ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} Y ## ## Class labels ## ## Specified as a logical or numeric column vector, or as a character array ## or a cell array of character vectors with the same number of rows as the ## predictor data. Each row in @var{Y} is the observed class label for ## the corresponding row in @var{X}. This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} NumObservations ## ## Number of observations ## ## A positive integer value specifying the number of observations in the ## training dataset used for training the ClassificationDiscriminant model. ## This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} RowsUsed ## ## Rows used for fitting ## ## A logical column vector with the same length as the observations in the ## original predictor data @var{X}, true for each row that was used for ## fitting the ClassificationDiscriminant model. It is empty, @qcode{[]}, ## when every observation was used, so a non-empty value means that rows ## holding missing values were dropped. This property is read-only. ## ## @end deftp RowsUsed = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} NumPredictors ## ## Number of predictors ## ## A positive integer value specifying the number of predictors in the ## training dataset used for training the ClassificationDiscriminant model. ## This property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} PredictorNames ## ## Names of predictor variables ## ## A cell array of character vectors specifying the names of the predictor ## variables. The names are in the order in which they appear in the ## training dataset. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} BetweenSigma ## ## Between-class covariance matrix ## ## A @math{P}-by-@math{P} matrix holding the covariance of the class means ## about the overall mean, weighted by how many observations each class ## contributes. With @math{n_k} observations in class @math{k}, ## @math{p_k = n_k / n} and @math{\bar{\mu} = \sum_k p_k \mu_k}, it is ## ## @example ## @group ## BetweenSigma = sum_k n_k (Mu(k,:) - mubar)' * (Mu(k,:) - mubar) ## / (n * (1 - sum_k p_k^2)) ## @end group ## @end example ## ## The denominator is the unbiased one for a weighted covariance, so a ## balanced fit divides by @math{n (K-1) / K}. It reads the @strong{class ## sizes}, not @qcode{Prior}: assigning a prior leaves it where it was. It ## is estimated for every discriminant type, the quadratic family included, ## since it describes the classes rather than the fit. This property is ## read-only. ## ## @end deftp BetweenSigma = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A numeric vector of column indices into @code{X} naming the predictors ## treated as categorical, and empty when none is. This property is ## read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} ExpandedPredictorNames ## ## Names of the predictors as the model expanded them ## ## A cell array of character vectors. It matches @code{PredictorNames} ## unless a categorical predictor was expanded into indicator variables. ## This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} ResponseName ## ## Response variable name ## ## A character vector specifying the name of the response variable @var{Y}. ## This property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} ClassNames ## ## Names of classes in the response variable ## ## An array of unique values of the response variable @var{Y}, which has the ## same data types as the data in @var{Y}, sorted or in the order given by ## the @qcode{'ClassNames'} option. This property is read-only. ## @qcode{ClassNames} can have any of the following datatypes: ## ## @itemize ## @item Cell array of character vectors ## @item Character array ## @item Logical vector ## @item Numeric vector ## @end itemize ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} Sigma ## ## Within-class covariance ## ## A numeric array whose shape follows @qcode{DiscrimType}, with @math{P} ## predictors and @math{K} classes: ## ## @multitable @columnfractions 0.4 0.25 0.35 ## @headitem @var{DiscrimType} @tab @var{Sigma} @tab @var{LogDetSigma} ## @item @qcode{'linear'}, @qcode{'pseudoLinear'} @tab @math{PxP} ## @tab scalar ## @item @qcode{'quadratic'}, @qcode{'pseudoQuadratic'} @tab @math{PxPxK} ## @tab @math{Kx1} ## @item @qcode{'diagLinear'} @tab @math{1xP} @tab scalar ## @item @qcode{'diagQuadratic'} @tab @math{1xPxK} @tab @math{Kx1} ## @end multitable ## ## The linear family pools one covariance across the classes and the ## quadratic family estimates one per class. This property is read-only, ## but it is re-derived whenever @qcode{DiscrimType} or @qcode{Gamma} is ## assigned. ## ## @end deftp Sigma = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} Mu ## ## Class means ## ## A @math{K*P} numeric matrix specifying the mean of the multivariate ## normal distribution of each corresponding class, where @math{K} is the ## number of classes and @math{P} is the number of predictors in @var{X}. ## This property is read-only. ## ## @end deftp Mu = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} Coeffs ## ## Coefficient matrices ## ## A @math{K*K} structure containing the coefficient matrices, where ## @math{K} is the number of classes. If the @qcode{'FillCoeffs'} parameter ## was set to @qcode{'off'} in either the @code{fitcdiscr} function or the ## @code{ClassificationDiscriminant} constructor, then @qcode{Coeffs} is ## empty @qcode{([])}. This property is read-only. ## ## @qcode{Coeffs(i,j)} contains the coefficients of the boundary between ## the classes @code{i} and @code{j} in the following fields: ## ## @itemize ## @item @qcode{DiscrimType} - A character vector ## @item @qcode{Class1} - @qcode{@var{ClassNames}(i)} ## @item @qcode{Class2} - @qcode{@var{ClassNames}(j)} ## @item @qcode{Const} - A scalar ## @item @qcode{Linear} - A vector with length as the number of predictors. ## @item @qcode{Quadratic} - The quadratic family only. A @math{PxP} ## matrix, or a @math{1xP} vector for @qcode{'diagQuadratic'}, following ## the shape of @qcode{Sigma}. ## @end itemize ## ## The diagonal entries carry the two class names and nothing else. The ## structure is rebuilt whenever @qcode{DiscrimType}, @qcode{Gamma} or ## @qcode{Prior} is assigned. ## ## @end deftp Coeffs = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} DeltaPredictor ## ## Minimum Delta at which each predictor drops out ## ## A row vector with one entry per predictor, the value of @qcode{Delta} at ## which that predictor's coefficient is zero for every class and the ## predictor leaves the model altogether. It is all zeros for the ## quadratic family, which has no linear coefficients to eliminate. ## ## This property is read-only, and it describes the fit rather than the ## threshold: assigning @qcode{Delta} does not move it. ## ## @end deftp DeltaPredictor = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} MinGamma ## ## Minimum value for the Gamma regularization parameter ## ## A scalar from 0 to 1, the least regularization that leaves the ## correlation matrix invertible. It is 0 when the matrix is already ## invertible, and positive when the predictors are collinear, in which ## case a plain @qcode{'linear'} or @qcode{'quadratic'} fit is raised to it ## rather than failing. Assigning a @qcode{Gamma} below it is refused. ## ## This property is read-only. ## ## @end deftp MinGamma = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} LogDetSigma ## ## Logarithm of the determinant of the within-class covariance matrix ## ## A scalar for the linear family and a @math{Kx1} vector for the quadratic ## one, one entry per class. It is computed in correlation space, as the ## sum of the logarithms of the predictor variances plus the log ## determinant of the correlation matrix, which is far better conditioned ## than the covariance when the data are nearly collinear. A predictor ## with no variance contributes nothing rather than an infinity, and the ## @qcode{'pseudo'} types sum only over the directions that carry variance. ## ## This property is read-only. ## ## @end deftp LogDetSigma = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} XCentered ## ## Predictor data with class means subtracted ## ## A matrix of the same size as @var{X} and the values in @var{X} with the ## corresponding class means subtracted. This property is read-only. ## ## @end deftp XCentered = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} BinEdges ## ## Bin edges of the predictors ## ## A cell array with one entry per predictor, holding that predictor's bin ## edges where the learner discretized it before fitting. It is empty here ## and stays empty: this learner fits the predictors as they are, and ## MATLAB's reports an empty cell for it as well. ## ## This property is read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} ModelParameters ## ## Fitting options, as they were given ## ## A structure holding the parameters of the fit: @qcode{DiscrimType}, ## @qcode{Gamma}, @qcode{Delta}, @qcode{FillCoeffs}, and the ## @qcode{Version}, @qcode{Method} and @qcode{Type} tags. ## ## MATLAB reports a @qcode{SaveMemory} field beside these. This class ## has no such option and always stores the full covariance, so there is ## no setting to report and the field is absent rather than answering for ## a knob that does not exist. This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} HyperparameterOptimizationResults ## ## Results of the hyperparameter optimization ## ## @strong{Always empty.} It is declared for MATLAB compatibility, where ## it holds what an automatic search over the hyperparameters found. This ## class fits the parameters it is given and runs no such search, so there ## is nothing to report. This property is read-only. ## ## @end deftp HyperparameterOptimizationResults = []; endproperties ## Properties a user may set after the model is fitted. Each one is ## validated by its set method below. properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} DiscrimType ## ## Discriminant type ## ## A character vector naming the discriminant model, one of ## @qcode{'linear'}, @qcode{'quadratic'}, @qcode{'diagLinear'}, ## @qcode{'diagQuadratic'}, @qcode{'pseudoLinear'} or ## @qcode{'pseudoQuadratic'}. A linear type pools one covariance across ## the classes; a quadratic type estimates one per class. A ## @qcode{'diag'} type keeps only the variances, and a @qcode{'pseudo'} ## type inverts a singular covariance instead of refusing it. ## ## This property may be assigned, but @emph{only within its own family}: ## the three linear types interchange freely and so do the three quadratic ## ones, while no assignment moves a model between the two. The family is ## fixed when the model is fitted, because it decides which covariances the ## fit has to estimate. Assigning re-derives @qcode{Sigma}, ## @qcode{LogDetSigma}, @qcode{Gamma} and @qcode{Coeffs}. ## ## @end deftp DiscrimType = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} Gamma ## ## Gamma regularization parameter ## ## A scalar from 0 to 1 shrinking the covariance towards its diagonal. ## @qcode{Gamma} and @qcode{DiscrimType} are one state: a value of 1 is the ## diagonal type, so assigning it renames @qcode{DiscrimType} to ## @qcode{'diagLinear'} or @qcode{'diagQuadratic'}, and assigning a ## diagonal type sets @qcode{Gamma} to 1. ## ## The quadratic family admits 0 and 1 only. A value below ## @qcode{MinGamma} is refused, since it would leave the covariance ## singular. Assigning re-derives @qcode{Sigma}, @qcode{LogDetSigma} and ## @qcode{Coeffs}. ## ## @end deftp Gamma = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} Delta ## ## Delta threshold for the linear coefficients ## ## A nonnegative scalar that eliminates predictors. A per-class linear ## coefficient is set to zero when it falls below @qcode{Delta}, and the ## comparison is made on the @strong{standardized} coefficient, the ## coefficient times the within-class standard deviation of its predictor. ## Scaling matters here: a threshold on the raw coefficients would depend ## on the units each predictor is measured in, so the same model in ## centimetres and in metres would drop different predictors. ## ## @qcode{DeltaPredictor} reports, per predictor, the value at which it ## drops out of every class at once. ## ## It applies to the linear family only, a quadratic discriminant having no ## linear coefficients to eliminate. Assigning it rebuilds @qcode{Coeffs} ## and changes what @code{predict} answers. ## ## @end deftp Delta = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} Cost ## ## Cost of Misclassification ## ## A square matrix specifying the cost of misclassification of a point. ## @qcode{Cost(i,j)} is the cost of classifying a point into class @qcode{j} ## if its true class is @qcode{i} (that is, the rows correspond to the true ## class and the columns correspond to the predicted class). The order of ## the rows and columns in @qcode{Cost} corresponds to the order of the ## classes in @qcode{ClassNames}. The number of rows and columns in ## @qcode{Cost} is the number of unique classes in the response. By ## default, @qcode{Cost(i,j) = 1} if @qcode{i != j}, and ## @qcode{Cost(i,j) = 0} if @qcode{i = j}. In other words, the cost is 0 ## for correct classification and 1 for incorrect classification. ## ## Add or change the @qcode{Cost} property using dot notation as in: ## @itemize ## @item @qcode{@var{obj}.Cost = @var{costMatrix}} ## @end itemize ## ## ## A cost may also be given as a struct with the fields ## @qcode{ClassNames} and @qcode{ClassificationCosts}, which names the ## order its own matrix is written in. That matrix is permuted into the ## order of @qcode{ClassNames} above, so a caller need not know which ## order the classes were sorted into. It must name every class. ## ## A cost must be floating point, not sparse, not complex, non-negative ## and zero down its diagonal, and must hold no @qcode{NaN} or ## @qcode{Inf}. A @code{single} is widened to @code{double}. ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} Prior ## ## Prior probability for each class ## ## A numeric vector specifying the prior probabilities for each class. The ## order of the elements in @qcode{Prior} corresponds to the order of the ## classes in @qcode{ClassNames}. ## ## Add or change the @qcode{Prior} property using dot notation as in: ## @itemize ## @item @qcode{@var{obj}.Prior = @var{priorVector}} ## @end itemize ## ## Specified as a row vector with one entry per class, in the order of ## @qcode{ClassNames}, and rescaled to sum to one. It may be given as ## @qcode{'empirical'}, @qcode{'uniform'}, a numeric vector, or a ## structure with @qcode{ClassNames} and @qcode{ClassProbs} fields, which ## assigns each probability by class name rather than by position. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {ClassificationDiscriminant} {property} ScoreTransform ## ## Transformation function for classification scores ## ## Specified as a function handle for transforming the classification ## scores. Add or change the @qcode{ScoreTransform} property using dot ## notation as in: ## ## @itemize ## @item @qcode{@var{obj}.ScoreTransform = 'function_name'} ## @item @qcode{@var{obj}.ScoreTransform = @@function_handle} ## @end itemize ## ## When specified as a character vector, it can be any of the following ## built-in functions. Nevertheless, the @qcode{ScoreTransform} property ## always stores their function handle equivalent. ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Value} @tab @var{Description} ## @item @qcode{'doublelogit'} @tab @math{1 ./ (1 + exp (-2 * x))} ## @item @qcode{'invlogit'} @tab @math{log (x ./ (1 - x))} ## @item @qcode{'ismax'} @tab Sets the score for the class with the ## largest score to 1, and for all other classes to 0 ## @item @qcode{'logit'} @tab @math{1 ./ (1 + exp (-x))} ## @item @qcode{'none'} @tab @math{x} (no transformation) ## @item @qcode{'identity'} @tab @math{x} (no transformation) ## @item @qcode{'sign'} @tab ## @math{-1 for x < 0, 0 for x = 0, 1 for x > ## 0} ## @item @qcode{'symmetric'} @tab @math{2 * x - 1} ## @item @qcode{'symmetricismax'} @tab Sets the score for the class ## with the largest score to 1, and for all other classes to -1 ## @item @qcode{'symmetriclogit'} @tab @math{2 ./ (1 + exp (-x)) - 1} ## @end multitable ## ## @end deftp ScoreTransform = 'none'; endproperties ## Readable by the compact counterpart, which copies it, and kept out of ## the documented surface. MATLAB hides its own equivalent the same way. properties (GetAccess = public, SetAccess = protected, Hidden) STfun = @(x) x; ## The unregularized within-class covariance the fit estimated, PxP for the ## linear family and PxPxK for the quadratic one. Sigma, LogDetSigma and ## Coeffs are all derived from it, so assigning DiscrimType within the ## family re-derives rather than refits. BaseSigma = []; ## The observation weights as they were given, before the prior scaled ## them, so that reassigning Prior can re-derive W. RawWeights = []; endproperties ## Set methods for the properties a user may assign after fitting. methods (Hidden) function this = set.Cost (this, Cost) gnY = this.ClassNames; if (isempty (Cost)) this.Cost = cast (! eye (classCount (gnY)), 'double'); else ## Everything a cost must be, and the struct form, which ## is permuted into this model's class order. [Cost, errmsg] = costMatrix (Cost, gnY); if (! isempty (errmsg)) error ("ClassificationDiscriminant: %s", errmsg); endif this.Cost = Cost; endif endfunction function this = set.Prior (this, Prior) gY = labelIndices (this.ClassNames, this.Y); K = classCount (this.ClassNames); if (isstruct (Prior)) Prior = priorFromStruct (Prior, this.ClassNames, ... 'ClassificationDiscriminant'); endif ## Set prior if (strcmpi ('uniform', Prior)) this.Prior = ones (1, K) ./ K; elseif (isempty (Prior) || strcmpi ('empirical', Prior)) ## Empirical over the weights, which are counts when none were given. pr = zeros (K, 1); for i = 1:K if (isempty (this.RawWeights)) pr(i) = sum (gY == i); else pr(i) = sum (double (this.RawWeights(gY == i))); endif endfor this.Prior = pr(:)' ./ sum (pr); elseif (isnumeric (Prior)) if (K != numel (Prior)) error (strcat ("ClassificationDiscriminant: the elements", ... " in 'Prior' do not correspond to the", ... " selected classes in Y.")); endif this.Prior = Prior(:)' ./ sum (Prior); endif ## Rebuild the coefficients, whose constant term reads the priors. ## Delegating rather than restating the linear formula is what keeps the ## six discriminant types in step: the formula here was linear only, and ## a quadratic model's Sigma is not even the same shape. if (! isempty (this.Coeffs) && ! isempty (this.BaseSigma)) ## Derived here rather than read off Sigma, so the covariance and the ## type always agree even when the object is part way through a load. [Sg, Ld] = discrimderive (this.BaseSigma, this.DiscrimType, ... this.Gamma, this.MinGamma); this.Coeffs = discrimcoeffs (this.Mu, Sg, Ld, this.Prior, ... this.DiscrimType, this.Delta, ... this.ClassNames); endif endfunction ## DiscrimType, Gamma and Delta are one state, and these three methods are ## the only place it is changed. Assigning the type or the regularization ## re-derives Sigma, LogDetSigma and Coeffs from BaseSigma, the covariance ## the fit estimated, so no assignment ever refits. Each writes its own ## property directly, which does not re-enter its own set method, and ## reaches the others through theirs, which is what keeps the three ## consistent whichever one the user assigns. function this = set.DiscrimType (this, val) [t, fam] = discrimcanon (val); ## The family a fit belongs to is written in BaseSigma's shape: one ## covariance for the linear family, one per class for the quadratic. ## Reading it there rather than from the stored type is what lets a ## loaded model take its own type back, the stub it is loaded into ## carrying the default type until the file has been read. if (! isempty (this.BaseSigma) && size (this.BaseSigma, 3) > 1) cur = 'quadratic'; elseif (! isempty (this.BaseSigma)) cur = 'linear'; else [~, cur] = discrimcanon (this.DiscrimType); endif if (isempty (t) || (! isempty (cur) && ! strcmp (fam, cur))) ## The family was fixed by the fit, so only three types are on offer, ## and an unrecognized name is refused by the same message. if (strcmp (cur, 'quadratic')) ok = "quadratic, diagQuadratic, or pseudoQuadratic"; else ok = "linear, diagLinear, or pseudoLinear"; endif error (strcat ("ClassificationDiscriminant: 'DiscrimType' can only", ... " be set to one of: %s."), ok); endif this.DiscrimType = t; if (isempty (this.BaseSigma)) return; # not fitted yet, nothing to derive endif ## A diagonal type is Gamma of 1 and every other type starts from 0, ## which is what the oracle reports for each transition. The derivation ## raises it to MinGamma where the covariance needs it. g = double (strncmp (t, 'diag', 4)); [~, ~, g] = discrimderive (this.BaseSigma, t, g, this.MinGamma); this.Gamma = g; endfunction function this = set.Gamma (this, val) if (! (isnumeric (val) && isscalar (val) && val >= 0 && val <= 1)) error (strcat ("ClassificationDiscriminant: 'Gamma' must be a", ... " scalar between 0 and 1.")); endif if (isempty (this.BaseSigma)) this.Gamma = val; return; endif [~, fam] = discrimcanon (this.DiscrimType); if (strcmp (fam, 'quadratic') && val > 0 && val < 1) error (strcat ("ClassificationDiscriminant: cannot set 'Gamma' to", ... " any value but 0 or 1 for a quadratic discriminant.")); endif ## Gamma of 1 IS the diagonal type. Naming the type does the rest, and ## comes back here with a value that no longer needs the rename. if (val == 1 && ! strncmp (this.DiscrimType, 'diag', 4)) if (strcmp (fam, 'quadratic')) this.DiscrimType = 'diagQuadratic'; else this.DiscrimType = 'diagLinear'; endif return; elseif (val < 1 && strncmp (this.DiscrimType, 'diag', 4)) if (strcmp (fam, 'quadratic')) this.DiscrimType = 'quadratic'; else this.DiscrimType = 'linear'; endif endif ## The MinGamma floor belongs to the one type that regularizes its way ## out of a singular covariance. A pseudo type is Gamma of 0 by ## definition, inverting the rank it has instead; a diagonal type is ## Gamma of 1 and never near the floor; and the quadratic family refuses ## a singular class covariance outright rather than ridging it. Mg = this.MinGamma; [Sg, Ld, Ge] = discrimderive (this.BaseSigma, this.DiscrimType, val, Mg); if (val < Mg && strcmp (this.DiscrimType, 'linear')) error (strcat ("ClassificationDiscriminant: 'Gamma' must be", ... " between %g and 1."), Mg); endif this.Gamma = Ge; this.Sigma = Sg; this.LogDetSigma = Ld; ## The delta at which a predictor drops out is read off the linear ## coefficients, and regularizing the covariance moves them, so this ## follows Gamma. It does NOT follow Delta, which is a threshold ## applied to those coefficients rather than a change to them. if (strcmp (fam, 'linear')) [~, ~, this.DeltaPredictor] = discrimlinear (this.Mu, Sg, ... this.Prior, this.DiscrimType, 0); endif if (! isempty (this.Coeffs)) this.Coeffs = discrimcoeffs (this.Mu, Sg, Ld, this.Prior, ... this.DiscrimType, this.Delta, ... this.ClassNames); endif endfunction function this = set.Delta (this, val) if (! (isnumeric (val) && isscalar (val) && val >= 0)) error (strcat ("ClassificationDiscriminant: 'Delta' must be a", ... " nonnegative scalar.")); endif [~, fam] = discrimcanon (this.DiscrimType); if (val > 0 && strcmp (fam, 'quadratic')) error (strcat ("ClassificationDiscriminant: cannot eliminate", ... " linear predictors in a quadratic discriminant.")); endif this.Delta = val; if (! isempty (this.BaseSigma) && ! isempty (this.Coeffs)) this.Coeffs = discrimcoeffs (this.Mu, this.Sigma, this.LogDetSigma, ... this.Prior, this.DiscrimType, val, ... this.ClassNames); endif endfunction function this = set.ScoreTransform (this, val) [f, nm] = parseScoreTransform (val, 'ClassificationDiscriminant'); this.ScoreTransform = nm; this.STfun = f; endfunction ## Custom display function disp (this) fprintf ("\n ClassificationDiscriminant\n\n"); ## Print selected properties fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); if (iscellstr (this.ClassNames)) str = repmat ({'''%s'''}, 1, numel (this.ClassNames)); str = strcat ('{', strjoin (str, ' '), '}'); str = sprintf (str, this.ClassNames{:}); elseif (ischar (this.ClassNames)) str = repmat ({'''%s'''}, 1, rows (this.ClassNames)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, cellstr (this.ClassNames){:}); else # single, double, logical str = repmat ({'%d'}, 1, numel (this.ClassNames)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, this.ClassNames); endif fprintf ("%+25s: %s\n", 'ClassNames', str); fprintf ("%+25s: '%s'\n", 'ScoreTransform', this.ScoreTransform); fprintf ("%+25s: %d\n", 'NumObservations', this.NumObservations); fprintf ("%+25s: %d\n", 'NumPredictors', this.NumPredictors); fprintf ("%+25s: '%s'\n", 'DiscrimType', this.DiscrimType); fprintf ("%+25s: [%dx%d double]\n", 'Mu', size (this.Mu)); ## Coeffs is KxK, which is Sigma's shape only for a linear discriminant. fprintf ("%+25s: [%dx%d struct]\n\n", 'Coeffs', ... rows (this.ClassNames), rows (this.ClassNames)); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {ClassificationDiscriminant} {@var{obj} =} ClassificationDiscriminant (@var{X}, @var{Y}) ## @deftypefnx {ClassificationDiscriminant} {@var{obj} =} ClassificationDiscriminant (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationDiscriminant} {@var{obj} =} ClassificationDiscriminant (@var{Tbl}, @var{formula}) ## @deftypefnx {ClassificationDiscriminant} {@var{obj} =} ClassificationDiscriminant (@var{Tbl}, @var{Y}) ## @deftypefnx {ClassificationDiscriminant} {@var{obj} =} ClassificationDiscriminant (@dots{}, @var{name}, @var{value}) ## ## Create a @qcode{ClassificationDiscriminant} class object containing a ## discriminant analysis model. ## ## @code{@var{obj} = ClassificationDiscriminant (@var{X}, @var{Y})} returns ## a ClassificationDiscriminant object, with @var{X} as the predictor data ## and @var{Y} containing the class labels of observations in @var{X}. ## ## @itemize ## @item ## @code{X} must be a @math{N*P} numeric matrix of input data where rows ## correspond to observations and columns correspond to features or ## variables. @var{X} will be used to train the discriminant model. ## @item ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels ## of corresponding predictor data in @var{X}. @var{Y} can contain any type ## of categorical data. @var{Y} must have the same number of rows as ## @var{X}. ## @end itemize ## ## @code{@var{obj} = ClassificationDiscriminant (@dots{}, @var{name}, ## @var{value})} returns a ClassificationDiscriminant object with parameters ## specified by the following @qcode{@var{name}, @var{value}} paired input ## arguments: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PredictorNames'} @tab A cell array of character ## vectors specifying the names of the predictors. The length of this array ## must match the number of columns in @var{X}. ## ## @item @qcode{'ResponseName'} @tab A character vector specifying the ## name of the response variable. ## ## @item @qcode{'ClassNames'} @tab Names of the classes in the class ## labels, @var{Y}, used for fitting the Discriminant model. ## @qcode{ClassNames} are of the same type as the class labels in @var{Y}. ## ## @item @qcode{'Cost'} @tab An @math{N*R} numeric matrix containing ## misclassification cost for the corresponding instances in @var{X}, where ## @math{R} is the number of unique categories in @var{Y}. If an instance ## is correctly classified into its category the cost is calculated to be 1, ## otherwise 0. The cost matrix can be altered by using ## @code{@var{Mdl}.cost = somecost}. By default, its value is ## @qcode{@var{cost} = ones (rows (X), numel (unique (Y)))}. ## ## @item @qcode{'Prior'} @tab A numeric vector specifying the prior ## probabilities for each class. The order of the elements in @qcode{Prior} ## corresponds to the order of the classes in @qcode{ClassNames}. ## Alternatively, you can specify @qcode{'empirical'} to use the empirical ## class probabilities or @qcode{'uniform'} to assume equal class ## probabilities. ## ## @item @qcode{'Weights'} @tab A single or double vector of nonnegative ## observation weights, one per row of @var{X}. They weigh the class means ## and covariances, the covariances being unbiased for them, and an ## empirical prior sums them per class. Only their proportions matter, and ## a row of zero weight is left out of the fit. The model's @code{W} keeps ## the class of the weights, while every computation runs in double, so ## @code{Prior} and the predictions are double where MATLAB returns single. ## ## @item @qcode{'ScoreTransform'} @tab A user-defined function handle ## or a character vector specifying one of the following builtin functions ## specifying the transformation applied to predicted classification scores. ## Supported values include @qcode{'doublelogit'}, @qcode{'invlogit'}, ## @qcode{'ismax'}, @qcode{'logit'}, @qcode{'none'}, @qcode{'identity'}, ## @qcode{'sign'}, @qcode{'symmetric'}, @qcode{'symmetricismax'}, and ## @qcode{'symmetriclogit'}. ## ## @item @qcode{'DiscrimType'} @tab A character vector or string scalar ## specifying the type of discriminant analysis to perform. The only ## supported value is @qcode{'linear'}. ## ## @item @qcode{'FillCoeffs'} @tab A character vector or string scalar ## with values @qcode{'on'} or @qcode{'off'} specifying whether to fill the ## coefficients after fitting. If set to @qcode{'on'}, the coefficients are ## computed during model fitting, which can be useful for prediction. ## ## @item @qcode{'Gamma'} @tab A numeric scalar specifying the ## regularization parameter for the covariance matrix. It adjusts the linear ## discriminant analysis to make the model more stable in the presence of ## multicollinearity or small sample sizes. A value of 0 corresponds to no ## regularization, while a value of 1 corresponds to ## a completely regularized model. ## @end multitable ## ## @seealso{fitcdiscr} ## @end deftypefn function this = ClassificationDiscriminant (X, Y, varargin) ## Check for appropriate number of input arguments if (nargin < 2) error ("ClassificationDiscriminant: too few input arguments."); endif ## A table names its own predictors and says which hold levels [this, X, Y, varargin] = resolveTable (this, 'ClassificationDiscriminant', ... X, Y, varargin); if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationDiscriminant: Name-Value", ... " arguments must be in pairs.")); endif ## Validate X if (! isnumeric (X)) error ("ClassificationDiscriminant: X must be a numeric matrix."); endif ## Check X and Y have the same number of observations if (rows (X) != rows (Y)) error (strcat ("ClassificationDiscriminant: number", ... " of rows in X and Y must be equal.")); endif ## Assign original X and Y data this.X = X; this.Y = Y; ## Get groups in Y [gY, gnY, glY] = grp2idx (Y); ## Parse optional paired arguments optNames = {'PredictorNames', 'ResponseName', 'ClassNames', 'Prior', ... 'Weights', 'Cost', 'ScoreTransform', 'DiscrimType', ... 'FillCoeffs', 'Gamma', 'Delta', 'CategoricalPredictors'}; ## An empty default stands for one resolved once the data are known: ## 'PredictorNames' are x1, x2, ...; the classes and 'Cost' come from ## the response; 'Weights' are uniform; and no 'ScoreTransform' keeps ## the one the class sets. dfValues = {{}, 'Y', [], 'empirical', [], [], [], 'linear', 'on', 0, ... 0, []}; [PredictorNames, ResponseName, ClassNames, Prior, Weights, Cost, STin, ... DiscrimType, FillCoeffs, Gamma, Delta, CatPreds, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! isempty (PredictorNames) && ! iscellstr (PredictorNames)) error (strcat ("ClassificationDiscriminant: 'PredictorNames' must", ... " be supplied as a cellstring array.")); elseif (! isempty (PredictorNames) && numel (PredictorNames) != columns (X)) error (strcat ("ClassificationDiscriminant: 'PredictorNames' must", ... " equal the number of columns in X.")); endif if (! ischar (ResponseName)) error (strcat ("ClassificationDiscriminant: 'ResponseName' must be", ... " a character vector.")); endif if (! isempty (ClassNames) && ! (iscellstr (ClassNames) || isnumeric (ClassNames) || islogical (ClassNames) || ischar (ClassNames) || isa (ClassNames, 'categorical') || isa (ClassNames, 'string'))) error (strcat ("ClassificationDiscriminant: 'ClassNames' must be a", ... " categorical array, a character array, a string", ... " array, a logical vector, a numeric vector, or a", ... " cell array of character vectors.")); endif if (! isempty (ClassNames)) [~, errmsg] = namedClasses (glY, ClassNames); if (! isempty (errmsg)) error ("ClassificationDiscriminant: %s", errmsg); endif endif if (! (isstruct (Prior) || (isnumeric (Prior) && isvector (Prior)) || (ischar (Prior) && (strcmpi (Prior, 'empirical') || strcmpi (Prior, 'uniform'))))) error (strcat ("ClassificationDiscriminant: 'Prior' must be either", ... " a numeric or a character vector.")); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("ClassificationDiscriminant: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isvector (Weights) && isreal (Weights))) error (strcat ("ClassificationDiscriminant: 'Weights' must be a", ... " real numeric vector.")); endif if (! isempty (Weights) && numel (Weights) != rows (X)) error (strcat ("ClassificationDiscriminant: 'Weights' must have", ... " one element per row in X.")); endif if (! isempty (Weights) && (any (Weights < 0) || ! (sum (Weights) > 0))) error (strcat ("ClassificationDiscriminant: 'Weights' must be", ... " nonnegative and must not be all zero.")); endif ## A struct carrying its own class order is a cost too, ## and is resolved by the property's own set method. if (! isempty (Cost) && ! (isstruct (Cost) || (isnumeric (Cost) && issquare (Cost)))) error (strcat ("ClassificationDiscriminant: 'Cost' must be a", ... " numeric square matrix.")); endif DiscrimType = discrimcanon (DiscrimType); if (isempty (DiscrimType)) error (strcat ("ClassificationDiscriminant: 'DiscrimType' must be", ... " one of the following: linear, quadratic,", ... " diagLinear, diagQuadratic, pseudoLinear, or", ... " pseudoQuadratic.")); endif FillCoeffs = tolower (FillCoeffs); if (! any (strcmpi (FillCoeffs, {'on', 'off'}))) error (strcat ("ClassificationDiscriminant: 'FillCoeffs' must be", ... " 'on' or 'off'.")); endif if (! (isnumeric (Gamma) && isscalar (Gamma) && Gamma >= 0 && Gamma <= 1)) error (strcat ("ClassificationDiscriminant: 'Gamma' must be a", ... " scalar between 0 and 1.")); endif if (! (isnumeric (Delta) && isscalar (Delta) && Delta >= 0)) error (strcat ("ClassificationDiscriminant: 'Delta' must be a", ... " nonnegative scalar.")); endif ## Measured on R2024a, which refuses any categorical predictor. if (! isempty (CatPreds)) error (strcat ("ClassificationDiscriminant: categorical predictors", ... " cannot be used for discriminant analysis.")); endif if (! isempty (STin)) this.ScoreTransform = STin; endif if (! isempty (args)) error ("ClassificationDiscriminant: invalid optional paired argument."); endif ## Generate default predictors and response variable names (if necessary) NumPredictors = columns (X); if (isempty (PredictorNames)) for i = 1:NumPredictors PredictorNames {i} = strcat ("x", num2str (i)); endfor endif if (isempty (ResponseName)) ResponseName = 'Y'; endif ## Assign predictors and response variable names this.NumPredictors = NumPredictors; this.PredictorNames = PredictorNames; this.CategoricalPredictors = []; this.ExpandedPredictorNames = PredictorNames; this.ResponseName = ResponseName; ## Handle class names if (! isempty (ClassNames)) ## The observations of a class ClassNames leaves out are dropped gY(! ismember (gY, namedClasses (glY, ClassNames))) = NaN; endif ## An observation is dropped when its response or every predictor is ## missing, or its weight is zero, as R2024a drops it. A row missing only ## some predictors is kept, and each estimate below uses whatever part of ## it is present. allmissing = all (isnan (X), 2); if (! isempty (setdiff (gY(allmissing & ! isnan (gY)), ... gY(! allmissing)))) error (strcat ("ClassificationDiscriminant: a class has no", ... " observation left after removing missing predictors.")); endif RowsUsed = ! isnan (gY) & ! allmissing; if (! isempty (Weights)) RowsUsed = RowsUsed & Weights(:) > 0; endif Y = Y(RowsUsed, :); X = X(RowsUsed, :); if (isempty (Weights)) RawWeights = ones (rows (X), 1); else RawWeights = Weights(RowsUsed); RawWeights = RawWeights(:); endif ## Store the retained observations; the weights keep their class in the ## model, and every computation runs on them as double this.X = X; this.Y = Y; this.RawWeights = RawWeights; RawWeights = double (RawWeights); ## Renew groups in Y: the classes keep the type of Y, sorted or in the ## order ClassNames gives them [this.ClassNames, gY] = classOrder (Y, ClassNames); ## Check X contains valid data if (! (isnumeric (X) && ! any (isinf (X(:))))) error ("ClassificationDiscriminant: invalid values in X."); endif ## Assign the number of observations and their corresponding indices ## on the original data, which will be used for training the model, ## to the ClassificationDiscriminant object this.NumObservations = rows (X); ## RowsUsed is left empty when every observation was used, as in MATLAB if (all (RowsUsed)) this.RowsUsed = []; else this.RowsUsed = RowsUsed; endif ## Handle Cost and Prior this.Cost = Cost; this.Prior = Prior; ## The weights scaled to sum to one. The prior enters prediction rather ## than the weights: measured on R2024a, W is the same under an ## empirical, a uniform, a given or a reassigned prior. this.W = cast (RawWeights / sum (RawWeights), class (this.RawWeights)); ## Assign DiscrimType ## Reconcile the type with the regularization before anything is ## estimated. Gamma of 1 IS the diagonal type, in both directions, and ## the quadratic family admits neither an intermediate Gamma nor a Delta. [~, fam] = discrimcanon (DiscrimType); if (Gamma == 1 && ! strncmp (DiscrimType, 'diag', 4)) if (strcmp (fam, 'linear')) DiscrimType = 'diagLinear'; else DiscrimType = 'diagQuadratic'; endif elseif (Gamma > 0 && Gamma < 1 && strcmp (fam, 'quadratic')) error (strcat ("ClassificationDiscriminant: cannot set 'Gamma' to", ... " any value but 0 or 1 for a quadratic discriminant.")); endif if (Delta > 0 && strcmp (fam, 'quadratic')) error (strcat ("ClassificationDiscriminant: cannot set linear", ... " coefficients to zero for a quadratic discriminant.")); endif this.DiscrimType = DiscrimType; this.Delta = Delta; this.Gamma = Gamma; num_classes = rows (this.ClassNames); num_features = columns (X); ## Each class mean uses every observation where that predictor is ## present, so a row missing one predictor still counts towards the rest ## The means are weighted, measured on R2024a to 3e-15. this.Mu = zeros (num_classes, num_features); for i = 1:num_classes Xi = X(gY == i, :); wi = RawWeights(gY == i); for j = 1:num_features xj = Xi(:, j); ok = ! isnan (xj); this.Mu(i, j) = sum (wi(ok) .* xj(ok)) / sum (wi(ok)); endfor endfor ## The between-class covariance of the means about the overall mean, ## weighted by the total weight of each class and divided by the unbiased ## weighted-covariance denominator. It reads the weights rather than ## Prior, which is why assigning a prior does not move it. nk = accumarray (gY(:), RawWeights, [num_classes, 1]); pk = nk ./ sum (nk); D = this.Mu - pk' * this.Mu; this.BetweenSigma = (D' * (D .* nk)) ./ (sum (nk) * (1 - sum (pk .^ 2))); ## Center the predictors (XCentered), keeping the missing entries this.XCentered = X - this.Mu(gY, :); ## Estimate the covariance the family calls for. A linear family pools ## one covariance across the classes and a quadratic family estimates one ## per class; every type in a family is derived from what is estimated ## here, which is what lets DiscrimType be assigned after the fit without ## the model having to keep both. [~, fam] = discrimcanon (this.DiscrimType); cobs = ! any (isnan (X), 2); ## The pooled covariance is estimated whatever the family, because ## MinGamma is taken from it. Only complete observations enter it, ## reweighted so that each class keeps the total weight it carried ## before any was dropped. ## The weights are reliability weights: the covariance is unbiased for ## them, which R2024a's fit reproduces to 4e-16, and not a count of ## repeated rows. cw = zeros (rows (X), 1); Wk = zeros (num_classes, 1); for i = 1:num_classes Wk(i) = sum (RawWeights(gY == i)) / sum (RawWeights); ci = (gY == i) & cobs; cw(ci) = Wk(i) * RawWeights(ci) / sum (RawWeights(ci)); endfor den = 1; for i = 1:num_classes ci = (gY == i) & cobs; den -= sum (cw(ci) .^ 2) / Wk(i); endfor Zc = this.XCentered(cobs, :); pooled = (Zc .* cw(cobs))' * Zc / den; this.MinGamma = discrimmingamma (pooled); if (strcmp (fam, 'linear')) this.BaseSigma = pooled; ## A predictor with no within-class variance cannot be inverted, so the ## plain type refuses it and names the two types that can take it. zwcv = find (diag (this.BaseSigma) == 0); if (! isempty (zwcv) && strcmp (this.DiscrimType, 'linear')) error (strcat ("ClassificationDiscriminant: predictor '%s'", ... " has zero within-class variance. Either exclude", ... " this predictor or set 'DiscrimType' to", ... " 'pseudoLinear' or 'diagLinear'."), ... PredictorNames{zwcv(1)}); endif else this.BaseSigma = zeros (num_features, num_features, num_classes); for i = 1:num_classes ci = (gY == i) & cobs; Zi = this.XCentered(ci, :); wi = RawWeights(ci) / sum (RawWeights(ci)); this.BaseSigma(:,:,i) = (Zi .* wi)' * Zi / (1 - sum (wi .^ 2)); zwcv = find (diag (this.BaseSigma(:,:,i)) == 0); if (! isempty (zwcv) && strcmp (this.DiscrimType, 'quadratic')) ## ClassNames keeps the type of Y, so the name is rendered rather ## than indexed as though it were always a cell. if (iscellstr (this.ClassNames)) cname = this.ClassNames{i}; elseif (ischar (this.ClassNames)) cname = this.ClassNames(i,:); else cname = num2str (this.ClassNames(i)); endif error (strcat ("ClassificationDiscriminant: predictor '%s' has", ... " zero variance for class '%s'. Either exclude", ... " this predictor or set 'DiscrimType' to", ... " 'pseudoQuadratic' or 'diagQuadratic'."), ... PredictorNames{zwcv(1)}, cname); endif endfor endif ## Derive Sigma, its log determinant and the regularization from the ## estimate above. The type and Gamma are one state, so both come back. [this.Sigma, this.LogDetSigma, this.Gamma] = ... discrimderive (this.BaseSigma, this.DiscrimType, this.Gamma, ... this.MinGamma); ## A singular class covariance is what the plain quadratic type cannot ## take; the pseudo and diagonal ones answer it. if (strcmp (this.DiscrimType, 'quadratic')) for i = 1:num_classes if (rcond (this.Sigma(:,:,i)) < num_features * eps) error (strcat ("ClassificationDiscriminant: cannot use", ... " 'quadratic' type because one or more classes", ... " have singular covariance matrices.")); endif endfor endif ## Delta is a threshold on the linear coefficients, so the quadratic ## family reports it as zeros and has nothing to eliminate. if (strcmp (fam, 'linear')) [~, ~, this.DeltaPredictor] = discrimlinear (this.Mu, this.Sigma, ... this.Prior, this.DiscrimType, 0); else this.DeltaPredictor = zeros (1, num_features); endif if (strcmpi (FillCoeffs, 'on')) this.Coeffs = discrimcoeffs (this.Mu, this.Sigma, this.LogDetSigma, ... this.Prior, this.DiscrimType, ... this.Delta, this.ClassNames); endif ## The fit as it was asked for: Gamma and Delta are the values given, ## which the properties of the same name do not always keep, this class ## forcing Gamma to 0 for a pseudo type and to 1 for a diagonal one. ## MATLAB reports the given value here. FillCoeffs is taken on and off ## and reported as the flag it is. this.ModelParameters = struct ( ... 'DiscrimType', this.DiscrimType, ... 'Gamma', Gamma, ... 'Delta', Delta, ... 'FillCoeffs', strcmpi (FillCoeffs, 'on'), ... 'Version', 1, ... 'Method', 'Discriminant', ... 'Type', 'classification'); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationDiscriminant} {@var{n} =} nLinearCoeffs (@var{obj}) ## @deftypefnx {ClassificationDiscriminant} {@var{n} =} nLinearCoeffs (@var{obj}, @var{delta}) ## ## Number of nonzero linear coefficients at a regularization threshold. ## ## @code{@var{n} = nLinearCoeffs (@var{obj})} returns the number of ## predictors the discriminant keeps at its own @code{Delta}. ## ## @code{@var{n} = nLinearCoeffs (@var{obj}, @var{delta})} returns the ## number it would keep at each threshold in @var{delta}, as a column ## vector however @var{delta} is shaped. ## ## A predictor survives a threshold when its @code{DeltaPredictor} reaches ## it, the comparison including equality, so @var{delta} at exactly a ## predictor's own value still counts it. A threshold above every ## @code{DeltaPredictor} therefore leaves nothing and returns zero. ## ## The count is taken whatever the @code{DiscrimType}, as MATLAB takes it, ## even though @code{Delta} regularizes the linear types alone. ## ## @seealso{fitcdiscr, ClassificationDiscriminant, ## CompactClassificationDiscriminant} ## @end deftypefn function n = nLinearCoeffs (this, delta) if (nargin < 1 || nargin > 2) print_usage (); endif if (nargin < 2) delta = this.Delta; endif if (! (isnumeric (delta) && isreal (delta))) error (strcat ("ClassificationDiscriminant.nLinearCoeffs: DELTA", ... " must be a real numeric value.")); endif ## One column per threshold, one row per predictor, summed down. n = sum (this.DeltaPredictor(:) >= delta(:)', 1)'; endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationDiscriminant} {@var{label} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {ClassificationDiscriminant} {[@var{label}, @var{score}, @var{cost}] =} predict (@var{obj}, @var{XC}) ## ## Classify new data points into categories using the discriminant ## analysis model from a ClassificationDiscriminant object. ## ## @code{@var{label} = predict (@var{obj}, @var{XC})} returns the vector of ## labels predicted for the corresponding instances in @var{XC}, using the ## predictor data in @code{obj.X} and corresponding labels, @code{obj.Y}, ## stored in the ClassificationDiscriminant model, @var{obj}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{ClassificationDiscriminant} class object. ## @item ## @var{XC} must be an @math{M*P} numeric matrix with the same number of ## features @math{P} as the corresponding predictors of the discriminant ## model in @var{obj}. ## @end itemize ## ## @code{[@var{label}, @var{score}, @var{cost}] = predict (@var{obj}, ## @var{XC})} also returns @var{score}, which contains the predicted class ## scores or posterior probabilities for each instance of the corresponding ## unique classes, and @var{cost}, which is a matrix containing the expected ## cost of the classifications. A query with a missing value has ## @qcode{NaN} scores and costs and takes the class of largest prior. ## ## The @var{score} matrix contains the posterior probabilities for each ## class, calculated using the multivariate normal probability density ## function and the prior probabilities of each class. These scores are ## normalized to ensure they sum to 1 for each observation. ## ## The @var{cost} matrix contains the expected classification cost for each ## class, computed based on the posterior probabilities and the specified ## misclassification costs. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @seealso{ClassificationDiscriminant, fitcdiscr} ## @end deftypefn function [label, score, cost] = predict (this, XC) ## Check for sufficient input arguments if (nargin < 2) error ("ClassificationDiscriminant.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'ClassificationDiscriminant.predict', XC); ## Check for valid XC if (isempty (XC)) error ("ClassificationDiscriminant.predict: XC is empty."); elseif (columns (this.X) != columns (XC)) error (strcat ("ClassificationDiscriminant.predict: XC must have ", ... " the same number of predictors as the trained model.")); endif ## Get training data and labels X = this.X; Y = this.Y; numObservations = rows (XC); numClasses = rows (this.ClassNames); score = zeros (numObservations, numClasses); cost = zeros (numObservations, numClasses); ## Score from the inverse covariance and its log determinant rather than ## through mvnpdf. A pseudo type's covariance is deliberately singular ## and mvnpdf refuses it, and LogDetSigma has to be the value the ## property reports, so the score and the property cannot drift apart. [~, fam] = discrimcanon (this.DiscrimType); logscore = zeros (numObservations, numClasses); if (strcmp (fam, 'linear')) ## The linear family scores from its per-class coefficients, which is ## the same function with the terms common to every class dropped, and ## is the only form in which Delta can eliminate a predictor. [Z, b] = discrimlinear (this.Mu, this.Sigma, this.Prior, ... this.DiscrimType, this.Delta); logscore = XC * Z' + b; else SigmaInv = discriminv (this.Sigma, this.DiscrimType); nInv = size (SigmaInv, 3); logdet = this.LogDetSigma; if (isscalar (logdet)) logdet = repmat (logdet, numClasses, 1); endif for i = 1:numClasses Si = SigmaInv(:,:,min (i, nInv)); Zc = XC - this.Mu(i, :); logscore(:, i) = -0.5 * sum ((Zc * Si) .* Zc, 2) ... - 0.5 * logdet(i) + log (this.Prior(i)); endfor endif ## The shared 2*pi factor cancels in the normalization, and subtracting ## the row maximum first keeps a well separated observation a posterior ## rather than a ratio of two underflowed zeros. logscore = logscore - max (logscore, [], 2); score = exp (logscore); score = score ./ sum (score, 2); ## Calculate expected classification cost for i = 1:numClasses cost(:, i) = sum (bsxfun (@times, score, this.Cost(:, i)'), 2); endfor ## Predict the class labels based on the minimum cost [~, minIdx] = min (cost, [], 2); ## A query with no posterior takes the class of largest prior [~, minIdx(any (isnan (score), 2))] = max (this.Prior); label = this.ClassNames(minIdx,:); ## Apply ScoreTransform once to the whole matrix, after the label and ## the cost have been taken from the untransformed posteriors: a ## transform reshapes what predict reports and must not move what it ## decides. score = this.STfun (score); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationDiscriminant} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationDiscriminant} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationDiscriminant} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {ClassificationDiscriminant} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Compute loss for a trained ClassificationDiscriminant object. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} computes the loss, ## @var{L}, using the default loss function @qcode{'mincost'}. ## ## @itemize ## @item ## @code{obj} is a @var{ClassificationDiscriminant} object trained on ## @code{X} and @code{Y}. ## @item ## @code{X} must be a @math{N*P} numeric matrix of input data where rows ## correspond to observations and columns correspond to features or ## variables. ## @item ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels ## of corresponding predictor data in @var{X}. @var{Y} must have same ## numbers of Rows as @var{X}. ## @end itemize ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{L} = loss (@dots{}, @var{name}, @var{value})} allows ## additional options specified by @var{name}-@var{value} pairs: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'LossFun'} @tab Specifies the loss function to use. ## Can be a function handle with four input arguments (C, S, W, Cost) ## which returns a scalar value or one of: ## 'binodeviance', 'classifcost', 'classiferror', 'exponential', ## 'hinge', 'logit','mincost', 'quadratic'. ## @itemize ## @item ## @code{C} is a logical matrix of size @math{N*K}, where @math{N} is the ## number of observations and @math{K} is the number of classes. ## The element @code{C(i,j)} is true if the class label of the i-th ## observation is equal to the j-th class. ## @item ## @code{S} is a numeric matrix of size @math{N*K}, where each element ## represents the classification score for the corresponding class. ## @item ## @code{W} is a numeric vector of length @math{N}, representing ## the observation weights. ## @item ## @code{Cost} is a @math{K*K} matrix representing the misclassification ## costs. ## @end itemize ## ## @item @qcode{'Weights'} @tab Specifies observation weights, must be ## a numeric vector of length equal to the number of rows in X. ## Default is @code{ones (size (X, 1))}. loss normalizes the weights so that ## observation weights in each class sum to the prior probability of that ## class. When you supply Weights, loss computes the weighted ## classification loss. ## ## @end multitable ## ## @seealso{ClassificationDiscriminant} ## @end deftypefn function L = loss (this, X, Y, varargin) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationDiscriminant.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationDiscriminant.loss: name-value", ... " arguments must be in pairs.")); endif ## Check for valid X if (isempty (X)) error ("ClassificationDiscriminant.loss: X is empty."); elseif (columns (this.X) != columns (X)) error (strcat ("ClassificationDiscriminant.loss: X must have the", ... " same number of predictors as the trained model.")); endif ## Validate Y valid_types = {'char', 'string', 'logical', 'single', 'double', ... 'cell', 'categorical'}; if (! (any (strcmp (class (Y), valid_types)))) error ("ClassificationDiscriminant.loss: Y must be of a valid type."); endif ## Validate size of Y if (size (Y, 1) != size (X, 1)) error (strcat ("ClassificationDiscriminant.loss: Y must", ... " have the same number of rows as X.")); endif ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights'}; dfValues = {'mincost', []}; [LossFun, Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments lf_opt = {'binodeviance', 'classifcost', 'classiferror', ... 'exponential', 'hinge','logit', 'mincost', 'quadratic'}; if (isa (LossFun, 'function_handle')) ## Check if the loss function is valid if (nargin (LossFun) != 4) error (strcat ("ClassificationDiscriminant.loss: custom loss", ... " function must accept exactly four input", ... " arguments.")); endif try n = 1; K = 2; C_test = false (n, K); S_test = zeros (n, K); W_test = ones (n, 1); Cost_test = ones (K) - eye (K); test_output = LossFun(C_test, S_test, W_test, Cost_test); if (! isscalar (test_output)) error (strcat ("ClassificationDiscriminant.loss: custom loss", ... " function must return a scalar value.")); endif catch error (strcat ("ClassificationDiscriminant.loss: custom loss", ... " function is not valid or does not produce", ... " correct output.")); end_try_catch elseif (! (ischar (LossFun) && any (strcmpi (LossFun, lf_opt)))) error ("ClassificationDiscriminant.loss: invalid loss function."); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("ClassificationDiscriminant.loss: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isvector (Weights))) error ("ClassificationDiscriminant.loss: invalid 'Weights'."); endif if (! isempty (Weights) && numel (Weights) != rows (X)) error (strcat ("ClassificationDiscriminant.loss: number of", ... " 'Weights' must be equal to the number of rows in", ... " X.")); endif if (! isempty (args)) error (strcat ("ClassificationDiscriminant.loss: invalid optional", ... " paired argument.")); endif ## Check for missing values in X if (! isa (LossFun, 'function_handle')) lossfun = tolower (LossFun); if (! strcmp (lossfun, 'mincost') && ! strcmp (lossfun, 'classiferror') && ! strcmp (lossfun, 'classifcost') && any (isnan (X(:)))) L = NaN; return; endif endif ## If Y is a char array convert it to a cell array of character vectors classes = this.ClassNames; if (ischar (Y) && ischar (classes)) Y = cellstr (Y); classes = cellstr (classes); endif ## Check if Y contains correct classes if (! labelsKnown (Y, classes)) error (strcat ("ClassificationDiscriminant.loss: Y must contain", ... " only the classes in model's ClassNames.")); endif ## Set default weights if not specified if (isempty (Weights)) Weights = ones (size (X, 1), 1); endif Weights = double (Weights(:)); ## Normalize Weights K = classCount (classes); class_prior_probs = this.Prior; norm_weights = zeros (size (Weights)); ## Labels are matched to the classes by name, whatever their type. gYw = labelIndices (classes, Y); for i = 1:K class_idx = (gYw == i); if (sum (Weights(class_idx)) > 0) norm_weights(class_idx) = ... Weights(class_idx) * class_prior_probs(i) / sum (Weights(class_idx)); endif endfor Weights = norm_weights / sum (norm_weights); ## Number of observations n = size (X, 1); ## Predict classification scores [label, scores] = predict (this, X); gLab = labelIndices (classes, label); ## C is vector of K-1 zeros, with 1 in the ## position corresponding to the true class C = false (n, K); ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:n class_idx = gYidx(i); C(i, class_idx) = true; endfor Y_new = C'; ## Compute the loss using custom loss function if (isa (LossFun, 'function_handle')) L = LossFun(C, scores, Weights, this.Cost); return; endif ## Compute the scalar classification score for each observation m_j = zeros (n, 1); for i = 1:n m_j(i) = scores(i,:) * Y_new(:,i); endfor ## Compute the loss switch (tolower (LossFun)) case 'binodeviance' b = log (1 + exp (-2 * m_j)); L = (Weights') * b; case 'hinge' h = max (0, 1 - m_j); L = (Weights') * h; case 'exponential' e = exp (-m_j); L = (Weights') * e; case 'logit' l = log (1 + exp (-m_j)); L = (Weights') * l; case 'quadratic' q = (1 - m_j) .^ 2; L = (Weights') * q; case 'classiferror' L = 0; for i = 1:n L = L + Weights(i) * (gYw(i) != gLab(i)); endfor case 'mincost' Cost = this.Cost; L = 0; ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:n f_Xj = scores(i, :); gamma_jk = f_Xj * Cost; [~, min_cost_class] = min (gamma_jk); if (any (isnan (gamma_jk))) min_cost_class = gLab(i); endif cj = Cost(gYidx(i), min_cost_class); L = L + Weights(i) * cj; endfor case 'classifcost' Cost = this.Cost; L = 0; ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:n y_idx = gYidx(i); y_hat_idx = find (ismember (classes, label(i))); L = L + Weights(i) * Cost(y_idx, y_hat_idx); endfor otherwise error ("ClassificationDiscriminant.loss: invalid loss function."); endswitch endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationDiscriminant} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationDiscriminant} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationDiscriminant} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margins for discriminant analysis classifier. ## ## @code{@var{m} = margin (@var{obj}, @var{X}, @var{Y})} returns ## the classification margins for @var{obj} with data @var{X} and ## classification @var{Y}. @var{m} is a numeric vector of length size (X,1). ## ## @itemize ## @item ## @code{obj} is a @var{ClassificationDiscriminant} object trained on ## @code{X} ## and @code{Y}. ## @item ## @code{X} must be a @math{N*P} numeric matrix of input data where rows ## correspond to observations and columns correspond to features or ## variables. ## @item ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels ## of corresponding predictor data in @var{X}. @var{Y} must have same ## numbers of Rows as @var{X}. ## @end itemize ## ## The classification margin for each observation is the difference between ## the classification score for the true class and the maximal ## classification score for the false classes. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{fitcdiscr, ClassificationDiscriminant} ## @end deftypefn function m = margin (this, X, Y) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationDiscriminant.margin: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y] = tableResponse (this, 'margin', X, Y, {}, nargin > 2); ## Check for valid X if (isempty (X)) error ("ClassificationDiscriminant.margin: X is empty."); elseif (columns (this.X) != columns (X)) error (strcat ("ClassificationDiscriminant.margin: X must have the", ... " same number of predictors as the trained model.")); endif ## Validate Y valid_types = {'char', 'string', 'logical', 'single', 'double', ... 'cell', 'categorical'}; if (! (any (strcmp (class (Y), valid_types)))) error ("ClassificationDiscriminant.margin: Y must be of a valid type."); endif ## Validate X valid_types = {'single', 'double'}; if (! (any (strcmp (class (X), valid_types)))) error ("ClassificationDiscriminant.margin: X must be of a valid type."); endif ## Validate size of Y if (size (Y, 1) != size (X, 1)) error (strcat ("ClassificationDiscriminant.margin: Y must", ... " have the same number of rows as X.")); endif ## If Y is a char array convert it to a cell array of character vectors classes = this.ClassNames; if (ischar (Y) && ischar (classes)) Y = cellstr (Y); classes = cellstr (classes); endif ## Check if Y contains correct classes if (! labelsKnown (Y, classes)) error (strcat ("ClassificationDiscriminant.margin: Y must", ... " contain only the classes in model's ClassNames.")); endif ## Number of Observations n = size (X, 1); ## Initialize the margin vector m = zeros (n, 1); ## Calculate the classification scores [~, scores] = predict (this, X); ## Loop over each observation to compute the margin ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:n ## True class index true_class_idx = gYidx(i); ## Score for the true class true_class_score = scores(i, true_class_idx); ## Get the maximal score for the false classes scores(i, true_class_idx) = -Inf; # Temporarily max_false_class_score = max (scores(i, :)); if (max_false_class_score == -Inf) m(i) = NaN; continue; endif scores(i, true_class_idx) = true_class_score; # Restore ## Calculate the margin m(i) = true_class_score - max_false_class_score; endfor endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationDiscriminant} {@var{CVMdl} =} crossval (@var{obj}) ## @deftypefnx {ClassificationDiscriminant} {@var{CVMdl} =} crossval (@dots{}, @var{Name}, @var{Value}) ## ## Cross Validate a Discriminant classification object. ## ## @code{@var{CVMdl} = crossval (@var{obj})} returns a cross-validated model ## object, @var{CVMdl}, from a trained model, @var{obj}, using 10-fold ## cross-validation by default. ## ## @code{@var{CVMdl} = crossval (@var{obj}, @var{name}, @var{value})} ## specifies additional name-value pair arguments to customize the ## cross-validation process. ## ## @multitable @columnfractions 0.28 0.7 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'KFold'} @tab Specify the number of folds to use in ## k-fold cross-validation. @code{"KFold", @var{k}}, where @var{k} is an ## integer greater than 1. ## ## @item @qcode{'Holdout'} @tab Specify the fraction of the data to ## hold out for testing. @code{"Holdout", @var{p}}, where @var{p} is a ## scalar in the range @math{(0,1)}. ## ## @item @qcode{'Leaveout'} @tab Specify whether to perform ## leave-one-out cross-validation. @code{"Leaveout", @var{Value}}, where ## @var{Value} is 'on' or 'off'. ## ## @item @qcode{'CVPartition'} @tab Specify a @qcode{cvpartition} ## object used for cross-validation. @code{"CVPartition", @var{cv}}, where ## @code{isa (@var{cv}, "cvpartition")} = 1. ## ## @end multitable ## ## @seealso{fitcdiscr, ClassificationDiscriminant, cvpartition, ## ClassificationPartitionedModel} ## @end deftypefn function CVMdl = crossval (this, varargin) ## Check input if (nargin < 1) error ("ClassificationDiscriminant.crossval: too few input arguments."); endif if (numel (varargin) == 1) error (strcat ("ClassificationDiscriminant.crossval: Name-Value", ... " arguments must be in pairs.")); elseif (numel (varargin) > 2) error (strcat ("ClassificationDiscriminant.crossval: specify only", ... " one of the optional Name-Value paired arguments.")); endif ## Add default values if (this.NumObservations < 10) numFolds = this.NumObservations; else numFolds = 10; endif Holdout = []; Leaveout = 'off'; CVPartition = []; ## Parse extra parameters while (numel (varargin) > 0) switch (tolower (varargin {1})) case 'kfold' numFolds = varargin{2}; if (! (isnumeric (numFolds) && isscalar (numFolds) && (numFolds == fix (numFolds)) && numFolds > 1)) error (strcat ("ClassificationDiscriminant.crossval: 'KFold'", ... " must be an integer value greater than 1.")); endif case 'holdout' Holdout = varargin{2}; if (! (isnumeric (Holdout) && isscalar (Holdout) && Holdout > 0 && Holdout < 1)) error (strcat ("ClassificationDiscriminant.crossval: 'Holdout'", ... " must be a numeric value between 0 and 1.")); endif case 'leaveout' Leaveout = varargin{2}; if (! (ischar (Leaveout) && (strcmpi (Leaveout, 'on') || strcmpi (Leaveout, 'off')))) error (strcat ("ClassificationDiscriminant.crossval:", ... " 'Leaveout' must be either 'on' or 'off'.")); endif case 'cvpartition' CVPartition = varargin{2}; if (! (isa (CVPartition, 'cvpartition'))) error (strcat ("ClassificationDiscriminant.crossval:",... " 'CVPartition' must be a 'cvpartition' object.")); endif otherwise error (strcat ("ClassificationDiscriminant.crossval: invalid",... " parameter name in optional paired arguments.")); endswitch varargin(1:2) = []; endwhile ## Determine the cross-validation method to use. The partition covers ## the observations actually trained on: a row dropped for a missing ## value is not one the folds can use, and including it would leave the ## partition, the stored data and NumObservations disagreeing. The ## response is passed rather than a count so the folds stay stratified. Yused = this.Y; if (! isempty (CVPartition)) partition = CVPartition; elseif (! isempty (Holdout)) partition = cvpartition (Yused, 'Holdout', Holdout); elseif (strcmpi (Leaveout, 'on')) partition = cvpartition (numel (this.Y), 'LeaveOut'); else partition = cvpartition (Yused, 'KFold', numFolds); endif ## Create a cross-validated model object CVMdl = ClassificationPartitionedModel (this, partition); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationDiscriminant} {@var{CVMdl} =} compact (@var{obj}) ## ## Create a CompactClassificationDiscriminant object. ## ## @code{@var{CVMdl} = compact (@var{obj})} creates a compact version of the ## ClassificationDiscriminant object, @var{obj}. ## ## @seealso{fitcdiscr, ClassificationDiscriminant, ## CompactClassificationDiscriminant} ## @end deftypefn function CVMdl = compact (this) ## Create a compact model CVMdl = CompactClassificationDiscriminant (this); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationDiscriminant} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationDiscriminant} {@var{e} =} edge (@dots{}, @qcode{"Weights"}, @var{w}) ## @deftypefnx {ClassificationDiscriminant} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationDiscriminant} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Classification edge, the mean of the classification margins. ## ## @code{@var{e} = edge (@var{obj}, @var{X}, @var{Y})} reduces the vector ## that @code{margin} returns to a single number, the mean margin over the ## rows of @var{X}. It says how far the model puts the true class ahead of ## its nearest rival on average, so a larger edge is a better model, and ## unlike a loss it is not bounded above and rewards confidence rather than ## bare correctness. ## ## @code{@var{e} = edge (@dots{}, @qcode{"Weights"}, @var{w})} takes the ## weighted mean instead, with one weight per row of @var{X}. ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @end deftypefn function e = edge (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationDiscriminant.edge: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'edge', X, Y, varargin, ... nargin > 2); ## The weights are parsed before anything is computed, so a bad ## Name-Value pair is reported as such rather than after a margin. W = edgeWeights (varargin, Y, this.ClassNames, this.Prior, ... "ClassificationDiscriminant", "edge"); m = margin (this, X, Y); e = sum (W .* m(:)) / sum (W); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationDiscriminant} {@var{M} =} mahal (@var{obj}, @var{X}) ## @deftypefnx {ClassificationDiscriminant} {@var{M} =} mahal (@dots{}, @qcode{'ClassLabels'}, @var{labels}) ## ## Squared Mahalanobis distance to the class means. ## ## @code{@var{M} = mahal (@var{obj}, @var{X})} returns an @math{NxK} ## matrix whose element @math{(i,j)} is the squared Mahalanobis distance ## from observation @math{i} to the mean of class @math{j}, measured ## against the covariance that class carries: the one shared covariance ## for a linear discriminant and the class's own for a quadratic one. ## ## @itemize ## @item ## @var{obj} must be a @qcode{ClassificationDiscriminant} object. ## @item ## @var{X} must be an @math{NxP} numeric matrix with one column per ## predictor of the trained model, or a table holding them. ## @end itemize ## ## @code{@var{M} = mahal (@dots{}, @qcode{'ClassLabels'}, @var{labels})} ## returns an @math{Nx1} vector instead, holding for each observation the ## distance to the mean of the class @var{labels} names for it. ## @var{labels} must have one entry per row of @var{X}, each of them one ## of @code{ClassNames}. ## ## The distance is measured against the covariance the model reports, so ## a regularized model is measured against its regularized covariance. ## The prior does not enter it. ## ## A table's variables are matched to the predictors the model was ## fitted on by name and not by position: one the model was not fitted on ## is passed over, one it needs and cannot find is named, and a value ## holding a level is coded as that level was coded at fitting. ## ## @end deftypefn function M = mahal (this, X, varargin) ## Check for sufficient input arguments if (nargin < 2) error (strcat ("ClassificationDiscriminant.mahal:", ... " too few input arguments.")); endif ## A table is read by the names the model was fitted on X = tableColumns (this, 'ClassificationDiscriminant.mahal', X); ## Check for valid X if (isempty (X)) error (strcat ("ClassificationDiscriminant.mahal:", ... " X is empty.")); elseif (! (isnumeric (X) && isreal (X))) error (strcat ("ClassificationDiscriminant.mahal:", ... " X must be a real numeric matrix.")); elseif (columns (this.X) != columns (X)) error (strcat ("ClassificationDiscriminant.mahal:", ... " X must have the same number of predictors as the", ... " trained model.")); endif if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationDiscriminant.mahal:", ... " Name-Value arguments must be in pairs.")); endif ## Parse optional paired arguments; without 'ClassLabels' every class ## mean is measured [labels, args] = parsePairedArguments ({'ClassLabels'}, {[]}, ... varargin(:)); if (! isempty (args)) error (strcat ("ClassificationDiscriminant.mahal: invalid optional", ... " paired argument.")); endif M = discrimmahal (X, this.Mu, this.Sigma, this.DiscrimType); if (isempty (labels)) return; endif ## One distance per observation, to the mean of the class named for it, ## the labels matched to the classes as every other method matches them if (! (isnumeric (labels) || islogical (labels) || ischar (labels) || iscellstr (labels) || isa (labels, 'categorical') || isa (labels, 'string'))) error (strcat ("ClassificationDiscriminant.mahal:", ... " 'ClassLabels' must be of a valid type.")); endif if (ischar (labels)) nL = rows (labels); else nL = numel (labels); endif if (nL != rows (X)) error (strcat ("ClassificationDiscriminant.mahal:", ... " 'ClassLabels' must have one entry per row of X.")); endif [idx, errmsg] = labelIndices (this.ClassNames, labels); if (! isempty (errmsg)) error (strcat ("ClassificationDiscriminant.mahal:", ... " every 'ClassLabels' entry must be one of", ... " ClassNames.")); endif M = M(sub2ind (size (M), (1:rows (X))', idx)); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationDiscriminant} {@var{lp} =} logp (@var{obj}, @var{X}) ## ## Log unconditional probability density of the observations. ## ## @code{@var{lp} = logp (@var{obj}, @var{X})} returns an @math{Nx1} ## vector holding, for each row of @var{X}, the natural logarithm of ## @math{P(x) = sum_k P(k) P(x|k)}, the density of the observation summed ## over the classes with each class weighted by its prior @math{P(k)}. ## Each @math{P(x|k)} is the multivariate normal density of class ## @math{k}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{ClassificationDiscriminant} object. ## @item ## @var{X} must be an @math{NxP} numeric matrix with one column per ## predictor of the trained model, or a table holding them. ## @end itemize ## ## An unusually low value marks an observation the model finds unlikely ## under every class, which is what makes this an outlier test rather ## than a classification. ## ## A table's variables are matched to the predictors the model was ## fitted on by name and not by position: one the model was not fitted on ## is passed over, one it needs and cannot find is named, and a value ## holding a level is coded as that level was coded at fitting. ## ## @end deftypefn function lp = logp (this, X) ## Check for sufficient input arguments if (nargin < 2) error (strcat ("ClassificationDiscriminant.logp:", ... " too few input arguments.")); endif ## A table is read by the names the model was fitted on X = tableColumns (this, 'ClassificationDiscriminant.logp', X); ## Check for valid X if (isempty (X)) error (strcat ("ClassificationDiscriminant.logp:", ... " X is empty.")); elseif (! (isnumeric (X) && isreal (X))) error (strcat ("ClassificationDiscriminant.logp:", ... " X must be a real numeric matrix.")); elseif (columns (this.X) != columns (X)) error (strcat ("ClassificationDiscriminant.logp:", ... " X must have the same number of predictors as the", ... " trained model.")); endif lp = discrimlogp (X, this.Mu, this.Sigma, this.LogDetSigma, ... this.Prior, this.DiscrimType); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationDiscriminant} {@var{label} =} resubPredict (@var{obj}) ## @deftypefnx {ClassificationDiscriminant} {[@var{label}, @var{score}, @var{cost}] =} resubPredict (@var{obj}) ## ## Classify the training data with the model fitted to it. ## ## @code{@var{label} = resubPredict (@var{obj})} is @code{predict} applied ## to the observations the model was fitted on, which it holds in ## @qcode{X}. Handing them over yourself is not the same thing: a row ## dropped for a missing response is not in @qcode{X}, so the original ## matrix and the model's own are different data. ## ## The result measures fit and not generalization, and is optimistic by ## construction. @code{crossval} is what estimates performance on data the ## model has not seen. ## ## @end deftypefn function [label, score, cost] = resubPredict (this) [label, score, cost] = predict (this, this.X); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationDiscriminant} {@var{m} =} resubMargin (@var{obj}) ## ## Classification margins of the model on its own training data. ## ## @code{@var{m} = resubMargin (@var{obj})} is @code{margin} applied to the ## observations the model was fitted on, one number per observation. Being ## a resubstitution quantity it is optimistic by construction. ## ## @end deftypefn function m = resubMargin (this) m = margin (this, this.X, this.Y); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationDiscriminant} {@var{e} =} resubEdge (@var{obj}) ## ## Classification edge of the model on its own training data. ## ## @code{@var{e} = resubEdge (@var{obj})} is @code{edge} applied to the ## observations the model was fitted on, the mean of @code{resubMargin} ## weighted by the training weights. ## ## @end deftypefn function e = resubEdge (this) ## The training rows keep their weights, as R2024a keeps them. e = edge (this, this.X, this.Y, 'Weights', this.RawWeights); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationDiscriminant} {@var{L} =} resubLoss (@var{obj}) ## @deftypefnx {ClassificationDiscriminant} {@var{L} =} resubLoss (@dots{}, @var{name}, @var{value}) ## ## Classification loss of the model on its own training data. ## ## @code{@var{L} = resubLoss (@var{obj})} is @code{loss} applied to the ## observations the model was fitted on, defaulting to ## @qcode{'mincost'} and to the training weights, and it accepts the same ## @qcode{Name-Value} pairs. ## ## Being a resubstitution quantity it is a lower bound on the error rather ## than an estimate of it. It is worth least on a lazy learner: a ## one-neighbour @code{ClassificationKNN} has a resubstitution loss of ## exactly zero, every training point being its own nearest neighbour. ## ## @end deftypefn function L = resubLoss (this, varargin) ## The training rows keep their weights unless others are given. L = loss (this, this.X, this.Y, 'Weights', this.RawWeights, ... varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationDiscriminant} {} savemodel (@var{obj}, @var{filename}) ## ## Save a ClassificationDiscriminant object. ## ## @code{savemodel (@var{obj}, @var{filename})} saves each property of a ## ClassificationDiscriminant object into an Octave binary file, the name of ## which is specified in @var{filename}, along with an extra variable, which ## defines the type classification object these variables constitute. Use ## @code{loadmodel} in order to load a classification object into Octave's ## workspace. ## ## @seealso{loadmodel, fitcdiscr, ClassificationDiscriminant} ## @end deftypefn function savemodel (this, fname) if (nargin < 2) error ("ClassificationDiscriminant.savemodel: too few input arguments."); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error ("ClassificationDiscriminant.savemodel: FNAME must be a character vector."); endif ## Generate variable for class name classdef_name = 'ClassificationDiscriminant'; ## Create variables from model properties X = this.X; Y = encodeLabels (this.Y); NumObservations = this.NumObservations; W = this.W; RawWeights = this.RawWeights; RowsUsed = this.RowsUsed; NumPredictors = this.NumPredictors; PredictorNames = this.PredictorNames; ResponseName = this.ResponseName; ClassNames = encodeLabels (this.ClassNames); Prior = this.Prior; Cost = this.Cost; ScoreTransform = this.ScoreTransform; Sigma = this.Sigma; BinEdges = this.BinEdges; BaseSigma = this.BaseSigma; Mu = this.Mu; Coeffs = encodeLabels (this.Coeffs); Delta = this.Delta; DiscrimType = this.DiscrimType; Gamma = this.Gamma; MinGamma = this.MinGamma; LogDetSigma = this.LogDetSigma; XCentered = this.XCentered; BetweenSigma = this.BetweenSigma; ModelParameters = this.ModelParameters; CategoricalPredictors = this.CategoricalPredictors; ExpandedPredictorNames = this.ExpandedPredictorNames; STfun = this.STfun; ## Save classdef name and all model properties as individual variables HyperparameterOptimizationResults = this.HyperparameterOptimizationResults; save ('-binary', fname, 'classdef_name', 'X', 'Y', 'NumObservations', ... 'W', 'RawWeights', 'RowsUsed', 'NumPredictors', ... 'PredictorNames', 'BinEdges', ... 'ResponseName', ... 'ClassNames', 'ScoreTransform', 'Prior', 'Cost', 'Sigma', ... 'BaseSigma', 'Mu', ... 'Coeffs', 'Delta', 'DiscrimType', 'Gamma', 'MinGamma', ... 'LogDetSigma', 'XCentered', 'BetweenSigma', 'ModelParameters', ... 'CategoricalPredictors', 'ExpandedPredictorNames', 'STfun', ... 'HyperparameterOptimizationResults'); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationDiscriminant} {@var{err} =} cvshrink (@var{obj}) ## @deftypefnx {ClassificationDiscriminant} {[@var{err}, @var{gamma}] =} cvshrink (@var{obj}) ## @deftypefnx {ClassificationDiscriminant} {[@var{err}, @var{gamma}, @var{delta}] =} cvshrink (@var{obj}) ## @deftypefnx {ClassificationDiscriminant} {[@var{err}, @var{gamma}, @var{delta}, @var{numpred}] =} cvshrink (@var{obj}) ## @deftypefnx {ClassificationDiscriminant} {[@dots{}] =} cvshrink (@dots{}, @var{Name}, @var{Value}) ## ## Cross validate the regularization of a discriminant. ## ## @code{@var{err} = cvshrink (@var{obj})} cross validates @var{obj} over a ## grid of @qcode{Gamma} values and returns the misclassification rate at ## each of them, so that a regularization can be chosen by what it costs ## on held-out data rather than on the data it was fitted to. ## ## @code{[@var{err}, @var{gamma}, @var{delta}, @var{numpred}] = cvshrink ## (@var{obj})} also returns the grid itself and the number of predictors ## surviving at each point of it. @var{gamma} is a column with one entry ## per @qcode{Gamma}; @var{err}, @var{delta} and @var{numpred} carry one ## row per @qcode{Gamma} and one column per @qcode{Delta}. ## ## @multitable @columnfractions 0.28 0.72 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'NumGamma'} @tab The number of @qcode{Gamma} intervals, a ## positive integer, 10 by default, giving @qcode{NumGamma + 1} values ## evenly spaced from 0 to 1. ## ## @item @qcode{'NumDelta'} @tab The number of @qcode{Delta} intervals, a ## non-negative integer, 0 by default. For each @qcode{Gamma} the ## @qcode{Delta} values run from 0 to the point at which every predictor ## has been eliminated, so the grid is not the same in every row. ## ## @item @qcode{'Gamma'} @tab The @qcode{Gamma} values to try, given ## explicitly as a vector, in place of @qcode{'NumGamma'}. ## ## @item @qcode{'Delta'} @tab The @qcode{Delta} values to try, given ## explicitly, in place of @qcode{'NumDelta'}: a vector used for every ## @qcode{Gamma}, or a matrix with one row per @qcode{Gamma}. ## @end multitable ## ## Every point of the grid is cross validated against the same partition, ## so the errors differ by the regularization and not by the split. The ## partition is drawn at random, so @var{err} is not reproducible across ## runs and does not match MATLAB's; @var{gamma}, @var{delta} and ## @var{numpred} are deterministic and do. ## ## @seealso{ClassificationDiscriminant, fitcdiscr, ## ClassificationDiscriminant.nLinearCoeffs} ## @end deftypefn function [err, gamma, delta, numpred] = cvshrink (this, varargin) if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationDiscriminant.cvshrink: Name/Value", ... " arguments must be given in pairs.")); endif ## Parse optional paired arguments; empty 'Gamma' and 'Delta' stand ## for the grids 'NumGamma' and 'NumDelta' describe optNames = {'NumGamma', 'NumDelta', 'Gamma', 'Delta'}; dfValues = {10, 0, [], []}; [NumGamma, NumDelta, Gamma, Delta, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (isnumeric (NumGamma) && isscalar (NumGamma) && NumGamma >= 1 && fix (NumGamma) == NumGamma)) error (strcat ("ClassificationDiscriminant.cvshrink: 'NumGamma'", ... " must be a positive integer.")); endif if (! (isnumeric (NumDelta) && isscalar (NumDelta) && NumDelta >= 0 && fix (NumDelta) == NumDelta)) error (strcat ("ClassificationDiscriminant.cvshrink: 'NumDelta'", ... " must be a non-negative integer.")); endif if (! isempty (Gamma) && ! (isnumeric (Gamma) && isvector (Gamma) && all (Gamma >= 0) && all (Gamma <= 1))) error (strcat ("ClassificationDiscriminant.cvshrink: 'Gamma' must", ... " be a vector of values between 0 and 1.")); endif if (! isempty (Delta) && ! (isnumeric (Delta) && all (Delta(:) >= 0))) error (strcat ("ClassificationDiscriminant.cvshrink: 'Delta' must", ... " be non-negative.")); endif if (! isempty (args)) error (strcat ("ClassificationDiscriminant.cvshrink: invalid", ... " optional paired argument.")); endif if (isempty (Gamma)) gamma = linspace (0, 1, NumGamma + 1)'; else gamma = Gamma(:); endif ng = numel (gamma); if (isempty (Delta)) nd = NumDelta + 1; elseif (isvector (Delta)) nd = numel (Delta); else if (rows (Delta) != ng) error (strcat ("ClassificationDiscriminant.cvshrink: a matrix", ... " 'Delta' must have one row per Gamma value, %d", ... " here, not %d."), ng, rows (Delta)); endif nd = columns (Delta); endif err = zeros (ng, nd); delta = zeros (ng, nd); numpred = zeros (ng, nd); ## One partition for the whole grid: comparing points cross validated ## against different splits would measure the split as much as the ## regularization. part = cvpartition (this.Y, "KFold", 10); for i = 1:ng m = this; m.Gamma = gamma(i); if (isempty (Delta)) ## Up to the point where the last predictor has gone, which moves ## with Gamma because regularizing the covariance moves the ## coefficients the threshold is applied to. dmax = max (m.DeltaPredictor); if (nd == 1) delta(i,:) = 0; else delta(i,:) = linspace (0, dmax, nd); endif elseif (isvector (Delta)) delta(i,:) = Delta(:)'; else delta(i,:) = Delta(i,:); endif for j = 1:nd mj = m; mj.Delta = delta(i,j); err(i,j) = kfoldLoss (crossval (mj, "CVPartition", part)); numpred(i,j) = nLinearCoeffs (mj, delta(i,j)); endfor endfor endfunction endmethods methods(Static, Hidden) function mdl = load_model (filename, data) data = decodeLabels (data); ## Create a ClassificationDiscriminant object ## Built without coefficients: every set method rebuilds Coeffs when it ## finds one, and a stub's would be rebuilt against half a loaded model. mdl = ClassificationDiscriminant (1, 1, 'FillCoeffs', 'off'); ## Copy the saved data into the object. Iterate over what was ## saved rather than over fieldnames (mdl): a private property such ## as STfun is written out by savemodel but is not reported by ## fieldnames, so comparing the two sets could never match and every ## load failed. Assignment is legal here because this is a method of ## the class itself. names = fieldnames (data); ## The set methods for these read other properties, and some of them ## rebuild Sigma or Coeffs, so they are assigned once everything else is ## in place rather than in the order the file happens to list them. ## Order matters within the late group as well: Prior and Cost before ## DiscrimType, because assigning the type rebuilds Coeffs and Coeffs ## reads the priors; and DiscrimType before Gamma, because assigning the ## type implies a Gamma and would overwrite the saved one if it came ## second. Coeffs is installed last of all: the set methods rebuild it ## whenever it is already there, and rebuilding it from a half-loaded ## model is how a diagonal Sigma met a linear formula. order = {'Cost', 'Prior', 'ScoreTransform', 'ResponseTransform', ... 'DiscrimType', 'Gamma', 'Delta', 'Coeffs'}; late = ismember (names, order); tail = {}; for k = 1:numel (order) if (any (strcmp (names, order{k}))) tail{end+1} = order{k}; endif endfor names = [names(! late); tail(:)]; for i = 1:numel (names) try mdl.(names{i}) = data.(names{i}); catch msg = 'ClassificationDiscriminant.load_model: invalid model in ''%s''.'; error (msg, filename); end_try_catch endfor endfunction endmethods methods(Access = private) endmethods endclassdef %!demo %! ## Create discriminant classifier %! ## Evaluate some model predictions on new data. %! %! load fisheriris %! x = meas; %! y = species; %! xc = [min(x); mean(x); max(x)]; %! obj = fitcdiscr (x, y); %! [label, score, cost] = predict (obj, xc); %!demo %! load fisheriris %! model = fitcdiscr (meas, species); %! X = mean (meas); %! Y = {'versicolor'}; %! ## Compute loss for discriminant model %! L = loss (model, X, Y) %!demo %! load fisheriris %! mdl = fitcdiscr (meas, species); %! X = mean (meas); %! Y = {'versicolor'}; %! ## Margin for discriminant model %! m = margin (mdl, X, Y) %!demo %! load fisheriris %! x = meas; %! y = species; %! obj = fitcdiscr (x, y, 'gamma', 0.4); %! ## Cross-validation for discriminant model %! CVMdl = crossval (obj) ## Test constructor %!test %! load fisheriris %! x = meas; %! y = species; %! PredictorNames = {'Sepal Length', 'Sepal Width', 'Petal Length', 'Petal Width'}; %! Mdl = ClassificationDiscriminant (x, y, 'PredictorNames', PredictorNames); %! sigma = [0.265008, 0.092721, 0.167514, 0.038401; ... %! 0.092721, 0.115388, 0.055244, 0.032710; ... %! 0.167514, 0.055244, 0.185188, 0.042665; ... %! 0.038401, 0.032710, 0.042665, 0.041882]; %! mu = [5.0060, 3.4280, 1.4620, 0.2460; ... %! 5.9360, 2.7700, 4.2600, 1.3260; ... %! 6.5880, 2.9740, 5.5520, 2.0260]; %! xCentered = [ 9.4000e-02, 7.2000e-02, -6.2000e-02, -4.6000e-02; ... %! -1.0600e-01, -4.2800e-01, -6.2000e-02, -4.6000e-02; ... %! -3.0600e-01, -2.2800e-01, -1.6200e-01, -4.6000e-02]; %! assert_equal (class (Mdl), "ClassificationDiscriminant"); %! assert_equal ({Mdl.X, Mdl.Y, Mdl.NumObservations}, {x, y, 150}) %! assert_equal ({Mdl.DiscrimType, Mdl.ResponseName}, {'linear', 'Y'}) %! assert_equal ({Mdl.Gamma, Mdl.MinGamma}, {0, 0}, 1e-15) %! assert_equal (Mdl.ClassNames, unique (species)) %! assert_equal (Mdl.Sigma, sigma, 1e-6) %! assert_equal (Mdl.Mu, mu, 1e-14) %! assert_equal (Mdl.XCentered([1:3],:), xCentered, 1e-14) %! assert_equal (Mdl.LogDetSigma, -9.9585, 1e-4) %! assert_equal (Mdl.PredictorNames, PredictorNames) %!test %! load fisheriris %! x = meas; %! y = species; %! Mdl = ClassificationDiscriminant (x, y, 'Gamma', 0.5); %! sigma = [0.265008, 0.046361, 0.083757, 0.019201; ... %! 0.046361, 0.115388, 0.027622, 0.016355; ... %! 0.083757, 0.027622, 0.185188, 0.021333; ... %! 0.019201, 0.016355, 0.021333, 0.041882]; %! mu = [5.0060, 3.4280, 1.4620, 0.2460; ... %! 5.9360, 2.7700, 4.2600, 1.3260; ... %! 6.5880, 2.9740, 5.5520, 2.0260]; %! xCentered = [ 9.4000e-02, 7.2000e-02, -6.2000e-02, -4.6000e-02; ... %! -1.0600e-01, -4.2800e-01, -6.2000e-02, -4.6000e-02; ... %! -3.0600e-01, -2.2800e-01, -1.6200e-01, -4.6000e-02]; %! assert_equal (class (Mdl), "ClassificationDiscriminant"); %! assert_equal ({Mdl.X, Mdl.Y, Mdl.NumObservations}, {x, y, 150}) %! assert_equal ({Mdl.DiscrimType, Mdl.ResponseName}, {'linear', 'Y'}) %! assert_equal ({Mdl.Gamma, Mdl.MinGamma}, {0.5, 0}) %! assert_equal (Mdl.ClassNames, unique (species)) %! assert_equal (Mdl.Sigma, sigma, 1e-6) %! assert_equal (Mdl.Mu, mu, 1e-14) %! assert_equal (Mdl.XCentered([1:3],:), xCentered, 1e-14) %! assert_equal (Mdl.LogDetSigma, -8.6884, 1e-4) %!test # A categorical 'ClassNames' keeps only the classes it names %! load fisheriris %! Mdl = ClassificationDiscriminant (meas, categorical (species), ... %! 'ClassNames', categorical ({'versicolor'; 'virginica'})); %! assert_equal (Mdl.NumObservations, 100); %! assert_equal (class (Mdl.ClassNames), 'categorical'); ## Test input validation for constructor %!shared X, Y, MODEL %! X = rand (10,2); %! Y = [ones(5,1);2*ones(5,1)]; %! MODEL = ClassificationDiscriminant (X, Y); ## nLinearCoeffs counts the predictors a threshold keeps. R2024a on ## fisheriris reports 4 at the model's own Delta of zero. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! assert_equal (nLinearCoeffs (Mdl), 4); ## The comparison includes equality, which only a threshold sitting exactly ## on a DeltaPredictor can show: R2024a returns 4, 3, 2, 1 at the four ## sorted values, where a strict comparison would return 3, 2, 1, 0. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! dps = sort (Mdl.DeltaPredictor); %! assert_equal (nLinearCoeffs (Mdl, dps), [4; 3; 2; 1]); ## A threshold above every DeltaPredictor keeps nothing, and the result is a ## column however the thresholds were shaped. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! assert_equal (nLinearCoeffs (Mdl, [0, 1e6]), [4; 0]); %! assert_equal (size (nLinearCoeffs (Mdl, [0, 1, 2])), [3, 1]); ## The count is taken whatever the DiscrimType, as MATLAB takes it, even ## though Delta regularizes the linear types alone. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, "DiscrimType", "quadratic"); %! assert_equal (nLinearCoeffs (Mdl), 4); ## A response naming its classes in the rows of a character matrix is one ## of the documented types and MATLAB accepts it on every classifier. The ## whole surface below was broken and untested, which is why it stayed so. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcdiscr (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcdiscr (Xch, Ycell); %! assert_equal (size (Mc.ClassNames), [2, 10]); %! assert_equal (cellstr (Mc.ClassNames), Ms.ClassNames); ## predict returns whole names, not their first letters. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcdiscr (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcdiscr (Xch, Ycell); %! pch = predict (Mc, Xch); %! assert_equal (columns (pch), 10); %! assert_equal (cellstr (pch), predict (Ms, Xch)); ## loss, margin and edge read a character response as the same response. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcdiscr (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcdiscr (Xch, Ycell); %! assert_equal (loss (Mc, Xch, Ych), loss (Ms, Xch, Ycell), 1e-12); %! assert_equal (margin (Mc, Xch, Ych), margin (Ms, Xch, Ycell), 1e-12); %! assert_equal (edge (Mc, Xch, Ych), edge (Ms, Xch, Ycell), 1e-12); ## A character matrix pads its rows out to the longest name, and the padding ## is part of the name: R2024a reports ClassNames of ['ab '; 'abcd']. %!test %! Xpad = [1 2; 3 4; 1.1 2.1; 3.1 4.1; 1.2 2.2; 3.2 4.2]; %! Ypad = char ({"ab", "abcd", "ab", "abcd", "ab", "abcd"}); %! rand ("state", 1); randn ("state", 1); %! Mp = fitcdiscr (Xpad, Ypad); %! assert_equal (size (Mp.ClassNames), [2, 4]); %! assert_equal (Mp.ClassNames(1,:), "ab "); ## A row dropped for a missing predictor is the only case that exercises ## indexing the response by row rather than by element. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! Xmiss = Xch; Xmiss(3,2) = NaN; %! rand ("state", 1); randn ("state", 1); Md = fitcdiscr (Xmiss, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcdiscr (Xmiss, Ycell); %! assert_equal (size (Md.ClassNames), [2, 10]); %! assert_equal (cellstr (Md.ClassNames), Ms.ClassNames); ## ClassNames may itself be given as a character matrix, which selects the ## classes by whole name: ismember between two character matrices compares ## them character by character and would select by letter. %!test %! load fisheriris %! rand ("state", 1); randn ("state", 1); %! Mf = fitcdiscr (meas, char (species), ... %! "ClassNames", char ({"versicolor", "virginica"})); %! assert_equal (rows (Mf.ClassNames), 2); %! assert_equal (cellstr (Mf.ClassNames), {"versicolor"; "virginica"}); ## A model fitted from a character response comes back off disk unchanged. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcdiscr (Xch, Ych); %! fname = tempname (); %! savemodel (Mc, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.ClassNames, Mc.ClassNames); %! assert_equal (predict (M2, Xch), predict (Mc, Xch)); ## crossval carries a character response through cvpartition and back. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcdiscr (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcdiscr (Xch, Ycell); %! rand ("state", 2); cvc = crossval (Mc, "KFold", 3); %! rand ("state", 2); cvs = crossval (Ms, "KFold", 3); %! assert_equal (cellstr (kfoldPredict (cvc)), kfoldPredict (cvs)); ## MATLAB parity: loss and margin match labels to the classes by name, ## whatever their type. Values from R2024a. %!test %! load fisheriris %! Xd = meas(51:150, 1:2); %! ys = species(51:150); %! yc = categorical (ys); %! Mdl = fitcdiscr (Xd, yc); %! assert_equal (loss (Mdl, Xd, ys), 0.25, 1e-15); %! assert_equal (loss (Mdl, Xd, yc), 0.25, 1e-15); %! assert_equal (numel (margin (Mdl, Xd, ys)), 100); %!error ... %! load fisheriris %! nLinearCoeffs (fitcdiscr (meas, species), "a") %!error ... %! fitcdiscr ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', 'a') %!error ... %! fitcdiscr ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', ones (2, 2)) %!error ... %! fitcdiscr ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', [1, 2]) %!error ... %! fitcdiscr ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', -ones (4, 1)) %!error ClassificationDiscriminant () %!error ... %! ClassificationDiscriminant (ones (4, 1)) %!error ... %! ClassificationDiscriminant (X, Y, 'prior') %!error ... %! ClassificationDiscriminant (ones (4,2), ones (1,4)) %!error ... %! ClassificationDiscriminant (X, Y, 'PredictorNames', ['A']) %!error ... %! ClassificationDiscriminant (X, Y, 'PredictorNames', 'A') %!error ... %! ClassificationDiscriminant (X, Y, 'Bogus', 1) %!error ... %! ClassificationDiscriminant (X, Y, 'PredictorNames', {'A', 'B', 'C'}) %!error ... %! ClassificationDiscriminant (X, Y, 'ResponseName', {'Y'}) %!error ... %! ClassificationDiscriminant (X, Y, 'ResponseName', 1) %!error ... %! ClassificationDiscriminant (X, Y, 'ClassNames', @(x)x) %!error ... %! ClassificationDiscriminant (X, Y, 'ClassNames', {1}) %!error ... %! ClassificationDiscriminant (X, ones (10,1), 'ClassNames', [1, 2]) %!error ... %! ClassificationDiscriminant ([1;2;3;4;5], ['a';'b';'a';'a';'b'], 'ClassNames', ['a';'c']) %!error ... %! ClassificationDiscriminant ([1;2;3;4;5], {'a';'b';'a';'a';'b'}, 'ClassNames', {'a','c'}) %!error ... %! ClassificationDiscriminant (X, logical (ones (10,1)), 'ClassNames', [true, false]) %!error ... %! ClassificationDiscriminant (X, Y, 'Prior', {'1', '2'}) %!error ... %! ClassificationDiscriminant (X, ones (10,1), 'Prior', [1 2]) ## What a misclassification cost may be, measured against MATLAB R2024a. ## The guard is shared by every class with a settable Cost; the battery is ## here, the other classes each check that they are behind it. %!test %! ## A single cost is widened to double rather than refused %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Cost = single ([0, 1, 2; 3, 0, 4; 5, 6, 0]); %! assert_equal (class (Mdl.Cost), 'double'); %! assert_equal (Mdl.Cost, [0, 1, 2; 3, 0, 4; 5, 6, 0]); %!test %! ## A cost need not be symmetric, and is stored as it was given %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Cost = [0, 1, 2; 3, 0, 4; 5, 6, 0]; %! assert_equal (Mdl.Cost, [0, 1, 2; 3, 0, 4; 5, 6, 0]); %!test %! ## Complex means a nonzero imaginary part: a zero one is accepted and %! ## stored as a double, so the guard is not ! isreal %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Cost = complex ([0, 1, 2; 3, 0, 4; 5, 6, 0], 0); %! assert_equal (class (Mdl.Cost), 'double'); %! assert_equal (Mdl.Cost, [0, 1, 2; 3, 0, 4; 5, 6, 0]); %!test %! ## A struct names the order its matrix is written in, and the matrix is %! ## permuted into the model's order entry by entry %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! S = struct ('ClassNames', {{'virginica'; 'setosa'; 'versicolor'}}, ... %! 'ClassificationCosts', [0, 1, 2; 3, 0, 4; 5, 6, 0]); %! Mdl.Cost = S; %! assert_equal (Mdl.Cost, [0, 4, 3; 6, 0, 5; 1, 2, 0]); %!test %! ## A struct already in the model's order permutes to itself %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! S = struct ('ClassNames', {{'setosa'; 'versicolor'; 'virginica'}}, ... %! 'ClassificationCosts', [0, 1, 2; 3, 0, 4; 5, 6, 0]); %! Mdl.Cost = S; %! assert_equal (Mdl.Cost, [0, 1, 2; 3, 0, 4; 5, 6, 0]); %!test %! ## The constructor takes the struct form too %! load fisheriris %! S = struct ('ClassNames', {{'virginica'; 'setosa'; 'versicolor'}}, ... %! 'ClassificationCosts', [0, 1, 2; 3, 0, 4; 5, 6, 0]); %! Mdl = fitcdiscr (meas, species, 'Cost', S); %! assert_equal (Mdl.Cost, [0, 4, 3; 6, 0, 5; 1, 2, 0]); %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Cost = int32 ([0, 1, 2; 3, 0, 4; 5, 6, 0]); %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Cost = ! logical (eye (3)); %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Cost = sparse ([0, 1, 2; 3, 0, 4; 5, 6, 0]); %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Cost = [0, 1, 2; 3, 0, 4i; 5, 6, 0]; %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Cost = [0, -1, 2; 3, 0, 4; 5, 6, 0]; %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Cost = ones (3); %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Cost = [0, 1, 2; 3, 0, NaN; 5, 6, 0]; %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Cost = [0, 1, 2; 3, 0, Inf; 5, 6, 0]; %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Cost = struct ('ClassificationCosts', [0, 1, 2; 3, 0, 4; 5, 6, 0]); %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Cost = struct ('ClassNames', {{'setosa'; 'versicolor'}}, ... %! 'ClassificationCosts', [0, 1; 2, 0]); %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Cost = struct ('ClassNames', {{'setosa'; 'versicolor'; 'virginica'}}, ... %! 'ClassificationCosts', [0, 1; 2, 0]); %!error ... %! ClassificationDiscriminant (X, Y, 'Cost', [1, 2]) %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Cost = 1:9; %!error ... %! ClassificationDiscriminant (X, Y, 'Cost', 'string') %!error ... %! ClassificationDiscriminant (X, Y, 'Cost', {eye(2)}) %!error ... %! ClassificationDiscriminant (X, Y, 'Cost', ones (3)) %!error ... %! ClassificationDiscriminant (ones (5,2), [1; 1; 2; 2; 2]) %!error ... %! ClassificationDiscriminant (ones (5,2), [1; 1; 2; 2; 2], 'PredictorNames', {'A', 'B'}) %!error ... %! ClassificationDiscriminant ([1,2;2,2;3,2;4,2;5,2], ones (5, 1)) %!error ... %! ClassificationDiscriminant ([1,2;2,2;3,2;4,2;5,2], ones (5, 1), 'PredictorNames', {'A', 'B'}) ## Test predict method %!test %! load fisheriris %! x = meas; %! y = species; %! Mdl = fitcdiscr (meas, species, 'Gamma', 0.5); %! [label, score, cost] = predict (Mdl, [2, 2, 2, 2]); %! assert_equal (label, {'versicolor'}) %! assert_equal (score, [0, 0.9999, 0.0001], 1e-4) %! assert_equal (cost, [1, 0.0001, 0.9999], 1e-4) %! [label, score, cost] = predict (Mdl, [2.5, 2.5, 2.5, 2.5]); %! assert_equal (label, {'versicolor'}) %! assert_equal (score, [0, 0.6368, 0.3632], 1e-4) %! assert_equal (cost, [1, 0.3632, 0.6368], 1e-4) %!test %! load fisheriris %! x = meas; %! y = species; %! xc = [min(x); mean(x); max(x)]; %! Mdl = fitcdiscr (x, y); %! [label, score, cost] = predict (Mdl, xc); %! l = {'setosa'; 'versicolor'; 'virginica'}; %! s = [1, 0, 0; 0, 1, 0; 0, 0, 1]; %! c = [0, 1, 1; 1, 0, 1; 1, 1, 0]; %! assert_equal (label, l) %! assert_equal (score, s, 1e-4) %! assert_equal (cost, c, 1e-4) ## Test input validation for predict method %!error ... %! predict (MODEL) %!error ... %! predict (MODEL, []) %!error ... %! predict (MODEL, 1) ## Test loss method %!test %! load fisheriris %! model = fitcdiscr (meas, species); %! x = mean (meas); %! y = {'versicolor'}; %! L = loss (model, x, y); %! assert_equal (L, 0) %!test %! x = [1, 2; 3, 4; 5, 6]; %! y = {'A'; 'B'; 'A'}; %! model = fitcdiscr (x, y, 'Gamma', 0.4); %! x_test = [1, 6; 3, 3]; %! y_test = {'A'; 'B'}; %! L = loss (model, x_test, y_test); %! assert_equal (L, 0.3333, 1e-4) %!test %! x = [1, 2; 3, 4; 5, 6; 7, 8]; %! y = ['1'; '2'; '3'; '1']; %! model = fitcdiscr (x, y, 'gamma' , 0.5); %! x_test = [3, 3]; %! y_test = ['1']; %! L = loss (model, x_test, y_test, 'LossFun', 'quadratic'); %! assert_equal (L, 0.2423, 1e-4) %!test %! x = [1, 2; 3, 4; 5, 6; 7, 8]; %! y = ['1'; '2'; '3'; '1']; %! model = fitcdiscr (x, y, 'gamma' , 0.5); %! x_test = [3, 3; 5, 7]; %! y_test = ['1'; '2']; %! L = loss (model, x_test, y_test, 'LossFun', 'classifcost'); %! assert_equal (L, 0.3333, 1e-4) %!test %! x = [1, 2; 3, 4; 5, 6; 7, 8]; %! y = ['1'; '2'; '3'; '1']; %! model = fitcdiscr (x, y, 'gamma' , 0.5); %! x_test = [3, 3; 5, 7]; %! y_test = ['1'; '2']; %! L = loss (model, x_test, y_test, 'LossFun', 'hinge'); %! assert_equal (L, 0.5886, 1e-4) %!test %! x = [1, 2; 3, 4; 5, 6; 7, 8]; %! y = ['1'; '2'; '3'; '1']; %! model = fitcdiscr (x, y, 'gamma' , 0.5); %! x_test = [3, 3; 5, 7]; %! y_test = ['1'; '2']; %! W = [1; 2]; %! L = loss (model, x_test, y_test, 'LossFun', 'logit', 'Weights', W); %! assert_equal (L, 0.5107, 1e-4) %!test %! x = [1, 2; 3, 4; 5, 6]; %! y = {'A'; 'B'; 'A'}; %! model = fitcdiscr (x, y, 'gamma' , 0.5); %! x_with_nan = [1, 2; NaN, 4]; %! y_test = {'A'; 'B'}; %! L = loss (model, x_with_nan, y_test); %! assert_equal (L, 0.3333, 1e-4) %!test %! x = [1, 2; 3, 4; 5, 6]; %! y = {'A'; 'B'; 'A'}; %! model = fitcdiscr (x, y); %! x_with_nan = [1, 2; NaN, 4]; %! y_test = {'A'; 'B'}; %! L = loss (model, x_with_nan, y_test, 'LossFun', 'logit'); %! assert_equal (isnan (L), true) %!test %! x = [1, 2; 3, 4; 5, 6]; %! y = {'A'; 'B'; 'A'}; %! model = fitcdiscr (x, y); %! customLossFun = @(C, S, W, Cost) sum (W .* sum (abs (C - S), 2)); %! L = loss (model, x, y, 'LossFun', customLossFun); %! assert_equal (L, 0.8889, 1e-4) %!test %! x = [1, 2; 3, 4; 5, 6]; %! y = [1; 2; 1]; %! model = fitcdiscr (x, y); %! L = loss (model, x, y, 'LossFun', 'classiferror'); %! assert_equal (L, 0.3333, 1e-4) ## Test input validation for loss method %!error ... %! loss (MODEL) %!error ... %! loss (MODEL, ones (4,2)) %!error ... %! loss (MODEL, [], zeros (2)) %!error ... %! loss (MODEL, 1, zeros (2)) %!error ... %! loss (MODEL, ones (4,2), ones (4,1), 'LossFun') %!error ... %! loss (MODEL, ones (4,2), ones (3,1)) %!error ... %! loss (MODEL, ones (4,2), ones (4,1), 'LossFun', 'a') %!error ... %! loss (MODEL, ones (4,2), ones (4,1), 'Bogus', 1) %!error ... %! loss (MODEL, ones (4,2), ones (4,1), 'Weights', 'w') %!error ... %! loss (MODEL, ones (4,2), ones (4,1), 'Weights', ones (2, 2)) ## Test margin method %! load fisheriris %! mdl = fitcdiscr (meas, species); %! X = mean (meas); %! Y = {'versicolor'}; %! m = margin (mdl, X, Y); %! assert_equal (m, 1, 1e-6) %!test %! X = [1, 2; 3, 4; 5, 6]; %! Y = [1; 2; 1]; %! mdl = fitcdiscr (X, Y, 'gamma', 0.5); %! m = margin (mdl, X, Y); %! assert_equal (m, [0.3333; -0.3333; 0.3333], 1e-4) ## Test input validation for margin method %!error ... %! margin (MODEL) %!error ... %! margin (MODEL, ones (4,2)) %!error ... %! margin (MODEL, [], zeros (2)) %!error ... %! margin (MODEL, 1, zeros (2)) %!error ... %! margin (MODEL, ones (4,2), ones (3,1)) ## Test crossval method %!shared x, y, obj %! load fisheriris %! x = meas; %! y = species; %! obj = fitcdiscr (x, y, 'gamma', 0.4); %!test %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! CVMdl = crossval (obj); %! warning (status); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y}) %! assert_equal (CVMdl.KFold == 10, true) %! assert_equal (class (CVMdl.Trained{1}), "CompactClassificationDiscriminant") %! assert_equal (CVMdl.CrossValidatedModel, "Discriminant") %!test %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! CVMdl = crossval (obj, 'KFold', 3); %! warning (status); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y}) %! assert_equal (CVMdl.KFold == 3, true) %! assert_equal (class (CVMdl.Trained{1}), "CompactClassificationDiscriminant") %! assert_equal (CVMdl.CrossValidatedModel, "Discriminant") %!test %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! CVMdl = crossval (obj, 'HoldOut', 0.2); %! warning (status); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y}) %! assert_equal (class (CVMdl.Trained{1}), "CompactClassificationDiscriminant") %! assert_equal (CVMdl.CrossValidatedModel, "Discriminant") %!test %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! CVMdl = crossval (obj, 'LeaveOut', 'on'); %! warning (status); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y}) %! assert_equal (class (CVMdl.Trained{1}), "CompactClassificationDiscriminant") %! assert_equal (CVMdl.CrossValidatedModel, "Discriminant") %!test %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! partition = cvpartition (y, 'KFold', 3); %! warning (status); %! CVMdl = crossval (obj, 'cvPartition', partition); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal (CVMdl.KFold == 3, true) %! assert_equal (class (CVMdl.Trained{1}), "CompactClassificationDiscriminant") %! assert_equal (CVMdl.CrossValidatedModel, "Discriminant") ## Test input validation for crossval method %!error ... %! crossval (obj, 'kfold') %!error... %! crossval (obj, 'kfold', 12, 'holdout', 0.2) %!error ... %! crossval (obj, 'kfold', 'a') %!error ... %! crossval (obj, 'holdout', 2) %!error ... %! crossval (obj, 'leaveout', 1) %!error ... %! crossval (obj, 'cvpartition', 1) %!error ... %! savemodel (ClassificationDiscriminant ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2])) %!error ... %! savemodel (ClassificationDiscriminant ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), 1) %!error ... %! savemodel (ClassificationDiscriminant ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), ['ab'; 'cd']) ## A ScoreTransform can be assigned, and is stored as a function handle. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.ScoreTransform = 'symmetric'; %! assert_equal (class (Mdl.ScoreTransform), 'char'); %! assert_equal (Mdl.ScoreTransform, 'symmetric'); ## RowsUsed is empty when every observation was used. %!test %! load fisheriris %! X = meas; %! Y = grp2idx (species); %! Mdl = fitcdiscr (X, Y); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (class (Mdl.RowsUsed), 'double'); %! assert_equal (Mdl.NumObservations, 150); %! assert_equal (rows (Mdl.X), 150); ## A missing response drops its observation and RowsUsed marks it. %!test %! load fisheriris %! X = meas; %! Y = grp2idx (species); %! Y(5) = NaN; %! Mdl = fitcdiscr (X, Y); %! assert_equal (class (Mdl.RowsUsed), 'logical'); %! assert_equal (size (Mdl.RowsUsed), [150, 1]); %! assert_equal (sum (Mdl.RowsUsed), 149); %! assert_equal (Mdl.RowsUsed(5), false); %! assert_equal (Mdl.NumObservations, 149); %! assert_equal (rows (Mdl.X), 149); ## A missing predictor keeps its observation, so RowsUsed stays empty. %!test %! load fisheriris %! X = meas; %! X(3,2) = NaN; %! Y = grp2idx (species); %! Mdl = fitcdiscr (X, Y); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (Mdl.NumObservations, 150); %! assert_equal (rows (Mdl.X), 150); %! assert_equal (sum (isnan (Mdl.X(:))), 1); ## MATLAB parity: observation weights enter the means and covariances as ## reliability weights, the empirical prior sums them per class, and W is ## the weights over their sum whatever the prior. Values from R2024a. %!test %! load fisheriris %! X = meas(1:130,[1, 3]); %! Y = species(1:130); %! k = (1:130)'; %! w = (1 + mod (k, 4)) .* (1 + (k > 50) + 2 * (k > 100)); %! Mdl = fitcdiscr (X, Y, 'Weights', w); %! assert_equal (Mdl.Prior, [0.185185185185185, 0.37037037037037, ... %! 0.444444444444444], 1e-14); %! assert_equal (Mdl.W([1, 51, 101])', [2, 8, 8] / 675, 1e-15); %! assert_equal (Mdl.Mu, [4.9832, 1.4472; 5.9456, 4.2536; ... %! 6.61333333333333, 5.64133333333333], 1e-13); %! assert_equal (Mdl.Sigma, [0.409670341318635, 0.303326062849595; ... %! 0.303326062849595, 0.307940102085745], 1e-14); %! assert_equal (Mdl.BetweenSigma, [0.563597620869565, 1.42708708173913; ... %! 1.42708708173913, 3.65290384695652], ... %! 1e-13); %! [~, s] = predict (Mdl, [6, 4.5; 6.5, 5.2]); %! assert_equal (s, [2.8720869604692e-15, 0.945382202857469, ... %! 0.0546177971425277; 4.32813750825658e-20, ... %! 0.182682233471567, 0.817317766528433], 1e-12); %!test %! load fisheriris %! X = meas(1:130,[1, 3]); %! Y = species(1:130); %! k = (1:130)'; %! w = (1 + mod (k, 4)) .* (1 + (k > 50) + 2 * (k > 100)); %! Mdl = fitcdiscr (X, Y, 'Weights', w, 'Prior', 'uniform'); %! assert_equal (Mdl.W([1, 51, 101])', [2, 8, 8] / 675, 1e-15); %! assert_equal (Mdl.Sigma, [0.409670341318635, 0.303326062849595; ... %! 0.303326062849595, 0.307940102085745], 1e-14); %!test %! load fisheriris %! X = meas(1:130,[1, 3]); %! Y = species(1:130); %! k = (1:130)'; %! w = (1 + mod (k, 4)) .* (1 + (k > 50) + 2 * (k > 100)); %! Mdl = fitcdiscr (X, Y, 'Weights', w, 'DiscrimType', 'quadratic'); %! assert_equal (Mdl.Sigma(:,:,3), [0.619863013698631, 0.507571269900037; ... %! 0.507571269900037, 0.487067752684191], ... %! 1e-14); %!test %! load fisheriris %! X = meas(1:130,[1, 3]); %! Y = species(1:130); %! k = (1:130)'; %! w = (1 + mod (k, 4)) .* (1 + (k > 50) + 2 * (k > 100)); %! w([2, 60]) = 0; %! Mdl = fitcdiscr (X, Y, 'Weights', w); %! assert_equal (Mdl.NumObservations, 128); %! assert_equal (Mdl.RowsUsed([1, 2, 60]), [true; false; false]); %!test %! load fisheriris %! X = meas(1:130,[1, 3]); %! Y = species(1:130); %! k = (1:130)'; %! w = (1 + mod (k, 4)) .* (1 + (k > 50) + 2 * (k > 100)); %! M1 = fitcdiscr (X, Y, 'Weights', w); %! M5 = fitcdiscr (X, Y, 'Weights', 5 * w); %! assert_equal (M5.Sigma, M1.Sigma, 1e-14); %! assert_equal (M5.W, M1.W, 1e-15); ## MATLAB parity: resubstitution weighs the training rows by their weights. %!test %! load fisheriris %! X = meas(1:130,[1, 3]); %! Y = species(1:130); %! k = (1:130)'; %! w = (1 + mod (k, 4)) .* (1 + (k > 50) + 2 * (k > 100)); %! Mdl = fitcdiscr (X, Y, 'Weights', w); %! assert_equal (resubLoss (Mdl), 0.0503703703703704, 1e-14); %! assert_equal (resubEdge (Mdl), 0.860269668211995, 1e-13); %! assert_equal (resubLoss (Mdl, 'LossFun', 'binodeviance'), ... %! 0.153822057071369, 1e-13); %! assert_equal (loss (Mdl, X, Y), 0.0518518518518519, 1e-14); ## Prior is a row of class frequencies and W stays uniform: a discriminant ## applies the prior when it predicts, not to the fit. Values from R2024a. %!test %! load fisheriris %! i3 = [1:50, 51:80, 101:120]; %! Mdl = fitcdiscr (meas(i3,:), species(i3)); %! assert_equal (size (Mdl.Prior), [1, 3]); %! assert_equal (Mdl.Prior, [0.5, 0.3, 0.2], 1e-14); %! assert_equal (Mdl.W, ones (100, 1) / 100, 1e-14); ## A uniform prior leaves a discriminant's weights alone. %!test %! load fisheriris %! i3 = [1:50, 51:80, 101:120]; %! Mdl = fitcdiscr (meas(i3,:), species(i3), 'Prior', 'uniform'); %! assert_equal (Mdl.Prior, [1, 1, 1] / 3, 1e-14); %! assert_equal (Mdl.W, ones (100, 1) / 100, 1e-14); ## A structure Prior assigns by class name, whatever order it names them in. %!test %! load fisheriris %! i3 = [1:50, 51:80, 101:120]; %! p = struct ('ClassNames', {{'setosa'; 'virginica'; 'versicolor'}}, ... %! 'ClassProbs', [0.2, 0.3, 0.5]); %! Mdl = fitcdiscr (meas(i3,:), species(i3), 'Prior', p); %! assert_equal (Mdl.Prior, [0.2, 0.5, 0.3], 1e-14); ## A structure Prior finds its classes in a character matrix response. %!test %! load fisheriris %! i3 = [1:50, 51:80, 101:120]; %! p = struct ('ClassNames', {{'setosa'; 'virginica'; 'versicolor'}}, ... %! 'ClassProbs', [0.2, 0.3, 0.5]); %! Mdl = fitcdiscr (meas(i3,:), char (species(i3)), 'Prior', p); %! assert_equal (Mdl.Prior, [0.2, 0.5, 0.3], 1e-14); ## A structure Prior finds its classes in a categorical response. %!test %! load fisheriris %! i3 = [1:50, 51:80, 101:120]; %! p = struct ('ClassNames', {{'setosa'; 'virginica'; 'versicolor'}}, ... %! 'ClassProbs', [0.2, 0.3, 0.5]); %! Mdl = fitcdiscr (meas(i3,:), categorical (species(i3)), 'Prior', p); %! assert_equal (Mdl.Prior, [0.2, 0.5, 0.3], 1e-14); ## An unnormalized Prior is rescaled on assignment. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Prior = [2, 3, 5]; %! assert_equal (Mdl.Prior, [0.2, 0.3, 0.5], 1e-14); %! Mdl.Cost = [0, 2, 3; 1, 0, 1; 1, 1, 0]; %! assert_equal (Mdl.Cost, [0, 2, 3; 1, 0, 1; 1, 1, 0]); ## A fitted model survives savemodel and loadmodel: the properties come ## back as they were and it predicts the same. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2), 'ClassificationDiscriminant'); %! assert_equal (M2.NumObservations, Mdl.NumObservations); %! assert_equal (M2.PredictorNames, Mdl.PredictorNames); %! assert_equal (class (M2.ScoreTransform), class (Mdl.ScoreTransform)); %! assert_equal (predict (M2, meas(1:5,:)), predict (Mdl, meas(1:5,:))); ## The six discriminant types. Every value below was measured on MATLAB ## R2024a; see DISCRIMINANT_LEDGER.md for the probes that produced them. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'linear'); %! assert_equal (Mdl.DiscrimType, 'linear'); %! assert_equal (size (Mdl.Sigma), [4, 4]); %! assert_equal (Mdl.LogDetSigma, -9.9585, 1e-4); %! assert_equal (Mdl.Gamma, 0); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'quadratic'); %! assert_equal (Mdl.DiscrimType, 'quadratic'); %! assert_equal (size (Mdl.Sigma), [4, 4, 3]); %! assert_equal (Mdl.LogDetSigma', [-13.0674, -10.8743, -8.9271], 1e-4); %! assert_equal (Mdl.Gamma, 0); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'diagLinear'); %! assert_equal (Mdl.DiscrimType, 'diagLinear'); %! assert_equal (size (Mdl.Sigma), [1, 4]); %! assert_equal (Mdl.LogDetSigma, -8.3467, 1e-4); %! assert_equal (Mdl.Gamma, 1); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'diagQuadratic'); %! assert_equal (Mdl.DiscrimType, 'diagQuadratic'); %! assert_equal (size (Mdl.Sigma), [1, 4, 3]); %! assert_equal (Mdl.LogDetSigma', [-12.0271, -8.3925, -6.9421], 1e-4); %! assert_equal (Mdl.Gamma, 1); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'pseudoLinear'); %! assert_equal (Mdl.DiscrimType, 'pseudoLinear'); %! assert_equal (size (Mdl.Sigma), [4, 4]); %! assert_equal (Mdl.LogDetSigma, -9.9585, 1e-4); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'pseudoQuadratic'); %! assert_equal (Mdl.DiscrimType, 'pseudoQuadratic'); %! assert_equal (size (Mdl.Sigma), [4, 4, 3]); %! assert_equal (Mdl.LogDetSigma', [-13.0674, -10.8743, -8.9271], 1e-4); ## The posterior of the one iris the model is unsure about separates the six ## types from one another, which a correctly classified row does not. %!test %! load fisheriris %! [~, s] = predict (fitcdiscr (meas, species), meas(71,:)); %! assert_equal (s, [0, 0.2532, 0.7468], 1e-4); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'quadratic'); %! [~, s] = predict (Mdl, meas(71,:)); %! assert_equal (s, [0, 0.3359, 0.6641], 1e-4); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'diagLinear'); %! [~, s] = predict (Mdl, meas(71,:)); %! assert_equal (s, [0, 0.2646, 0.7354], 1e-4); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'diagQuadratic'); %! [~, s] = predict (Mdl, meas(71,:)); %! assert_equal (s, [0, 0.1609, 0.8391], 1e-4); ## Exact coefficients on a fixture small enough that every figure is closed ## form: the pooled covariance is 13.75/6 and each class covariance n_k-1. %!test %! x = [1, 2; 2, 1; 3, 4; 4, 3; 5, 7; 7, 5; 8, 9; 9, 8]; %! y = [1; 1; 1; 1; 2; 2; 2; 2]; %! Mdl = fitcdiscr (x, y, 'DiscrimType', 'linear'); %! assert_equal (Mdl.Sigma, [2.2917, 1.125; 1.125, 2.2917], 1e-4); %! assert_equal (Mdl.LogDetSigma, 1.3828, 1e-4); %! assert_equal (Mdl.Coeffs(1,2).Const, 13.5549, 1e-4); %! assert_equal (Mdl.Coeffs(1,2).Linear', [-1.3902, -1.3902], 1e-4); %!test %! x = [1, 2; 2, 1; 3, 4; 4, 3; 5, 7; 7, 5; 8, 9; 9, 8]; %! y = [1; 1; 1; 1; 2; 2; 2; 2]; %! Mdl = fitcdiscr (x, y, 'DiscrimType', 'quadratic'); %! assert_equal (Mdl.Sigma(:,:,1), [1.6667, 1; 1, 1.6667], 1e-4); %! assert_equal (Mdl.Coeffs(1,2).Const, 10.9525, 1e-4); %! assert_equal (Mdl.Coeffs(1,2).Linear', [-0.8025, -0.8025], 1e-4); %! assert_equal (Mdl.Coeffs(1,2).Quadratic, ... %! [-0.2587, 0.1912; 0.1912, -0.2587], 1e-4); %!test %! x = [1, 2; 2, 1; 3, 4; 4, 3; 5, 7; 7, 5; 8, 9; 9, 8]; %! y = [1; 1; 1; 1; 2; 2; 2; 2]; %! Mdl = fitcdiscr (x, y, 'DiscrimType', 'diagQuadratic'); %! assert_equal (Mdl.Sigma(:,:,1), [1.6667, 1.6667], 1e-4); %! assert_equal (Mdl.Coeffs(1,2).Const, 14.8310, 1e-4); %! assert_equal (Mdl.Coeffs(1,2).Quadratic, [-0.1286, -0.1286], 1e-4); ## The quadratic family reports a Quadratic field and the linear family does ## not, which is how a user tells the two apart from the structure alone. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! assert_equal (fieldnames (Mdl.Coeffs)', ... %! {'DiscrimType', 'Const', 'Linear', 'Class1', 'Class2'}); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'quadratic'); %! assert_equal (fieldnames (Mdl.Coeffs)', {'DiscrimType', 'Const', ... %! 'Linear', 'Quadratic', 'Class1', 'Class2'}); ## DiscrimType is assignable within its family and re-derives the covariance. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.DiscrimType = 'diagLinear'; %! assert_equal (size (Mdl.Sigma), [1, 4]); %! assert_equal (Mdl.Gamma, 1); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'diagLinear'); %! Mdl.DiscrimType = 'linear'; %! assert_equal (size (Mdl.Sigma), [4, 4]); %! assert_equal (Mdl.Gamma, 0); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'quadratic'); %! Mdl.DiscrimType = 'diagQuadratic'; %! assert_equal (size (Mdl.Sigma), [1, 4, 3]); ## Assigning the type rebuilds the coefficients rather than leaving the ones ## the previous type produced. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! before = Mdl.Coeffs(1,2).Const; %! Mdl.DiscrimType = 'diagLinear'; %! assert (abs (Mdl.Coeffs(1,2).Const - before) > 1); ## Gamma and the diagonal type are one state, in both directions. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Gamma = 1; %! assert_equal (Mdl.DiscrimType, 'diagLinear'); %! assert_equal (size (Mdl.Sigma), [1, 4]); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'quadratic'); %! Mdl.Gamma = 1; %! assert_equal (Mdl.DiscrimType, 'diagQuadratic'); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'Gamma', 1); %! assert_equal (Mdl.DiscrimType, 'diagLinear'); ## An intermediate Gamma shrinks the covariance towards its diagonal, which ## the resubstitution error follows. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Gamma = 0.25; %! assert_equal (loss (Mdl, meas, species), 0.0267, 1e-4); ## A collinear fit is raised to MinGamma rather than failing, and cannot be ## brought back below it. MinGamma lands a few eps above zero, so ## LogDetSigma is the logarithm of a rounding-level eigenvalue: it moves by ## 0.02 between platforms where doubling MinGamma moves it by log (2). %!test %! load fisheriris %! Mdl = fitcdiscr ([meas(:,1:3), meas(:,3)], species); %! assert (Mdl.MinGamma > 0); %! assert_equal (Mdl.Gamma, Mdl.MinGamma); %! assert_equal (Mdl.LogDetSigma, -41.3112, 0.1); ## The pseudo types answer a singular covariance where the plain one is ## regularized and the diagonal one drops the correlations. %!test %! load fisheriris %! Mdl = fitcdiscr ([meas(:,1:3), meas(:,3)], species, ... %! 'DiscrimType', 'pseudoLinear'); %! assert_equal (Mdl.LogDetSigma, -7.3470, 1e-4); %! assert_equal (Mdl.Gamma, 0); %!test %! load fisheriris %! Mdl = fitcdiscr ([meas(:,1:3), meas(:,3)], species, ... %! 'DiscrimType', 'pseudoQuadratic'); %! assert_equal (Mdl.LogDetSigma', [-11.2116, -7.2229, -6.4619], 1e-4); ## A predictor with no variance is dropped rather than inverted, so its ## coefficient is zero. %!test %! load fisheriris %! Mdl = fitcdiscr ([meas(:,1:3), ones(150,1)], species, ... %! 'DiscrimType', 'pseudoLinear'); %! assert_equal (Mdl.Coeffs(1,2).Linear(4), 0); %! assert_equal (Mdl.LogDetSigma, -6.3538, 1e-4); ## The compact model answers identically for every type. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'quadratic'); %! CMdl = compact (Mdl); %! assert_equal (predict (CMdl, meas), predict (Mdl, meas)); %! assert_equal (CMdl.Sigma, Mdl.Sigma); ## A saved quadratic model comes back able to change its type, which it can ## only do if the covariance the fit estimated was saved with it. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'quadratic'); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (predict (M2, meas), predict (Mdl, meas)); %! M2.DiscrimType = 'diagQuadratic'; %! assert_equal (size (M2.Sigma), [1, 4, 3]); ## Five observations per class over two predictors, enough for a quadratic ## discriminant to have a covariance it can invert. %!function x = dfix () %! x = [1, 2; 2, 1; 3, 4; 4, 3; 2, 3; 5, 7; 7, 5; 8, 9; 9, 8; 7, 8]; %!endfunction ## Delta eliminates predictors, and the threshold is on the standardized ## coefficient: the raw one would depend on the units each predictor is ## measured in. Values measured on MATLAB R2024a. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! assert_equal (Mdl.DeltaPredictor, ... %! [3.2508, 4.1236, 7.2926, 4.2506], 1e-4); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'diagLinear'); %! assert_equal (Mdl.DeltaPredictor, ... %! [1.6266, 1.0912, 5.3354, 4.6584], 1e-4); ## The quadratic family has no linear coefficients to eliminate. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'quadratic'); %! assert_equal (Mdl.DeltaPredictor, [0, 0, 0, 0]); ## A coefficient drops out one class at a time, and the boundary follows. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Delta = 2.25; %! assert_equal (Mdl.Coeffs(1,2).Linear', ... %! [6.3148, 12.1393, -16.9464, -20.7701], 1e-4); %! assert_equal (Mdl.Coeffs(1,2).Const, -14.3792, 1e-4); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Delta = 5; %! assert_equal (Mdl.Coeffs(1,2).Linear', [0, 0, -16.9464, 0], 1e-4); %! assert_equal (loss (Mdl, meas, species), 0.08, 1e-10); ## Past the largest DeltaPredictor every predictor is gone and the model ## answers from the priors alone. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.Delta = 8; %! assert_equal (Mdl.Coeffs(1,2).Linear', [0, 0, 0, 0]); %! assert_equal (loss (Mdl, meas, species), 2/3, 1e-10); ## Delta changes what predict answers, which is the whole point of it. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! before = loss (Mdl, meas, species); %! Mdl.Delta = 5; %! assert_equal (before, 0.02, 1e-10); %! assert (loss (Mdl, meas, species) > before); ## DeltaPredictor describes the fit, not the threshold, so assigning Delta ## leaves it where it was. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! before = Mdl.DeltaPredictor; %! Mdl.Delta = 5; %! assert_equal (Mdl.DeltaPredictor, before); %!error ... %! fitcdiscr (ones (10, 2), [1;1;1;1;1;2;2;2;2;2], 'DiscrimType', 'bogus') %!error ... %! Mdl = fitcdiscr (dfix (), [1;1;1;1;1;2;2;2;2;2]); ... %! Mdl.DiscrimType = 'quadratic'; %!error ... %! Mdl = fitcdiscr (dfix (), [1;1;1;1;1;2;2;2;2;2], ... %! 'DiscrimType', 'quadratic'); ... %! Mdl.DiscrimType = 'linear'; %!error ... %! Mdl = fitcdiscr (dfix (), [1;1;1;1;1;2;2;2;2;2]); Mdl.DiscrimType = 'bogus'; %!error ... %! Mdl = fitcdiscr (dfix (), [1;1;1;1;1;2;2;2;2;2], ... %! 'DiscrimType', 'quadratic'); ... %! Mdl.Gamma = 0.5; %!error ... %! fitcdiscr (dfix (), [1;1;1;1;1;2;2;2;2;2], ... %! 'DiscrimType', 'quadratic', 'Gamma', 0.5) %!error ... %! Mdl = fitcdiscr (dfix (), [1;1;1;1;1;2;2;2;2;2]); Mdl.Gamma = 1.5; %!error ... %! Mdl = fitcdiscr (dfix (), [1;1;1;1;1;2;2;2;2;2], ... %! 'DiscrimType', 'quadratic'); ... %! Mdl.Delta = 0.5; %!error ... %! fitcdiscr (dfix (), [1;1;1;1;1;2;2;2;2;2], ... %! 'DiscrimType', 'quadratic', 'Delta', 0.5) %!error ... %! fitcdiscr (dfix (), [1;1;1;1;1;2;2;2;2;2], 'Delta', -1) %!error ... %! Mdl = fitcdiscr (dfix (), [1;1;1;1;1;2;2;2;2;2]); Mdl.Delta = [1, 2]; %!error ... %! load fisheriris; ... %! fitcdiscr ([meas(:,1:3), meas(:,3)], species, 'DiscrimType', 'quadratic') %!error ... %! load fisheriris; ... %! fitcdiscr ([meas(:,1:3), ones(150,1)], species, 'DiscrimType', 'quadratic') ## MinGamma describes the data rather than the type, so a pseudo fit reports ## the same value as a plain one on the same predictors while regularizing by ## nothing at all. %!test %! load fisheriris %! Mdl = fitcdiscr ([meas(:,1:3), meas(:,3)], species, ... %! 'DiscrimType', 'pseudoLinear'); %! assert (Mdl.MinGamma > 0); %! assert_equal (Mdl.Gamma, 0); %!test %! load fisheriris %! Mdl = fitcdiscr ([meas(:,1:3), meas(:,3)], species, ... %! 'DiscrimType', 'diagLinear'); %! assert (Mdl.MinGamma > 0); %! assert_equal (Mdl.Gamma, 1); ## edge, resubPredict, resubMargin, resubEdge and resubLoss. Every value ## below was measured on MATLAB R2024a; the discriminant's scores agree with ## it exactly, so these are the oracle's own numbers and not ours. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! assert_equal (edge (Mdl, meas, species), 0.9454289377, 1e-9); ## The edge is the mean of the margins, by definition. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! m = margin (Mdl, meas, species); %! assert_equal (edge (Mdl, meas, species), mean (m), 1e-12); ## Weights are normalized within each class to that class's prior, which is ## not the same as dividing by their total: that would give 0.9269539697. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! assert_equal (edge (Mdl, meas, species, 'Weights', (1:150)'), ... %! 0.9438468986, 1e-9); ## A weight constant within a class therefore changes nothing. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! w = [ones(50,1); 7 * ones(50,1); 0.5 * ones(50,1)]; %! assert_equal (edge (Mdl, meas, species, 'Weights', w), ... %! edge (Mdl, meas, species), 1e-12); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! assert_equal (resubEdge (Mdl), 0.9454289377, 1e-9); %! assert_equal (resubLoss (Mdl), 0.02, 1e-12); %! assert_equal (sum (resubMargin (Mdl)), 141.8143406564, 1e-8); ## resubPredict is predict on the data the model kept. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! [label, score, cost] = resubPredict (Mdl); %! [l2, s2, c2] = predict (Mdl, meas); %! assert_equal (label, l2); %! assert_equal (score, s2); %! assert_equal (cost, c2); %! assert_equal (numel (label), 150); ## The compact model answers the same edge as the full one. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! assert_equal (edge (compact (Mdl), meas, species), 0.9454289377, 1e-9); %!error ... %! load fisheriris; edge (fitcdiscr (meas, species), meas) %!error ... %! load fisheriris; ... %! edge (fitcdiscr (meas, species), meas, species, 'Weights') %!error ... %! load fisheriris; ... %! edge (fitcdiscr (meas, species), meas, species, 'Weights', ones (3, 1)) %!error ... %! load fisheriris; ... %! edge (fitcdiscr (meas, species), meas, species, 'Nope', 1) ## BinEdges is an empty cell, which is what MATLAB reports for this ## learner as well: it fits the predictors as they are. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! assert_equal (class (Mdl.BinEdges), 'cell'); %! assert_equal (Mdl.BinEdges, {}); ## CategoricalPredictors and ExpandedPredictorNames, shapes measured on ## R2024a: an empty double and one name per predictor. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! assert_equal (Mdl.CategoricalPredictors, []); %! assert_equal (size (Mdl.CategoricalPredictors), [0, 0]); %! assert_equal (Mdl.ExpandedPredictorNames, Mdl.PredictorNames); %! assert_equal (size (Mdl.ExpandedPredictorNames), [1, 4]); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.CategoricalPredictors, Mdl.CategoricalPredictors); %! assert_equal (M2.ExpandedPredictorNames, Mdl.ExpandedPredictorNames); ## BetweenSigma, measured on MATLAB R2024a. It weights the class means by ## class size, not by Prior, and the denominator is the unbiased one for a ## weighted covariance. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! assert_equal (Mdl.BetweenSigma, ... %! [0.632121333333333, -0.199526666666667, ... %! 1.652483999999999, 0.712793333333333; %! -0.199526666666667, 0.113449333333333, ... %! -0.572395999999999, -0.229326666666666; %! 1.652483999999999, -0.572395999999999, ... %! 4.371027999999996, 1.867739999999998; %! 0.712793333333333, -0.229326666666666, ... %! 1.867739999999998, 0.804133333333333], 1e-12); ## Unequal class sizes: 50, 30 and 10 of the three species. %!test %! load fisheriris %! k = [1:50, 51:80, 101:110]; %! Mdl = fitcdiscr (meas(k,:), species(k)); %! assert_equal (Mdl.BetweenSigma, ... %! [0.651346086956520, -0.299426956521739, ... %! 1.775435652173911, 0.711567826086955; %! -0.299426956521739, 0.160084347826087, ... %! -0.811819130434784, -0.318816086956522; %! 1.775435652173911, -0.811819130434784, ... %! 4.840319130434781, 1.941198695652173; %! 0.711567826086955, -0.318816086956522, ... %! 1.941198695652173, 0.780424347826086], 1e-12); ## A row missing a predictor still counts towards the others, as it does for ## Mu; only a missing response drops one. %!test %! load fisheriris %! X = meas; X(3,2) = NaN; X(77,4) = NaN; %! Mdl = fitcdiscr (X, species); %! assert_equal (Mdl.BetweenSigma(1,2), -0.201474748299320, 1e-12); %! assert_equal (Mdl.BetweenSigma(4,4), 0.803942801055115, 1e-12); ## It describes the classes, not the fit, so the quadratic family reports it ## and assigning a prior does not move it. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'quadratic'); %! assert_equal (Mdl.BetweenSigma(1,1), 0.632121333333333, 1e-12); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! before = Mdl.BetweenSigma; %! Mdl.Prior = [0.6, 0.2, 0.2]; %! assert_equal (Mdl.BetweenSigma, before); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.BetweenSigma, Mdl.BetweenSigma); ## mahal is the squared distance to every class mean. Measured on R2024a. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! M = mahal (Mdl, meas(1:5,:)); %! assert_equal (size (M), [5, 3]); %! assert_equal (M(1,:), [0.2910898404344, 98.8847494279393, ... %! 191.788642179719], 1e-10); %! assert_equal (M(5,:), [0.5956300391717, 100.923170228959, ... %! 193.854037009331], 1e-10); ## 'ClassLabels' picks one class mean per observation. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! M = mahal (Mdl, meas(1:5,:), 'ClassLabels', species(1:5)); %! assert_equal (size (M), [5, 1]); %! assert_equal (M, [0.291089840434356; 2.031345104042097; ... %! 0.553281423559203; 2.086697905677364; ... %! 0.595630039171744], 1e-12); ## 'ClassLabels' of any label type, whatever type the classes are. %!test %! load fisheriris %! idx = [1; 51; 101]; %! D = diag (mahal (fitcdiscr (meas, species), meas(idx,:))); %! Mdl = fitcdiscr (meas, categorical (species)); %! assert_equal (mahal (Mdl, meas(idx,:), 'ClassLabels', species(idx)), D); %! assert_equal (mahal (Mdl, meas(idx,:), 'ClassLabels', ... %! categorical (species(idx))), D); %! assert_equal (mahal (Mdl, meas(idx,:), 'ClassLabels', ... %! string (species(idx))), D); %! assert_equal (mahal (Mdl, meas(idx,:), 'ClassLabels', ... %! char (species(idx))), D); %!test %! load fisheriris %! idx = [1; 51; 101]; %! D = diag (mahal (fitcdiscr (meas, species), meas(idx,:))); %! Mdl = fitcdiscr (meas, species); %! assert_equal (mahal (Mdl, meas(idx,:), 'ClassLabels', ... %! categorical (species(idx))), D); %! assert_equal (mahal (Mdl, meas(idx,:), 'ClassLabels', ... %! string (species(idx))), D); %!test %! load fisheriris %! idx = [1; 51; 101]; %! Mdl = fitcdiscr (meas, grp2idx (species)); %! D = diag (mahal (Mdl, meas(idx,:))); %! assert_equal (mahal (Mdl, meas(idx,:), 'ClassLabels', [1; 2; 3]), D); ## A quadratic model measures against each class's own covariance. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'quadratic'); %! M = mahal (Mdl, meas(1:5,:)); %! assert_equal (M(1,:), [0.4491137892273, 114.804489260461, ... %! 182.935908699285], 1e-10); ## The distance follows the regularized covariance the model reports. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'Gamma', 0.5); %! M = mahal (Mdl, meas(1:5,:)); %! assert_equal (M(1,:), [0.1926136709121, 79.4757202663154, ... %! 163.294484902038], 1e-10); ## The prior does not enter the distance. %!test %! load fisheriris %! M1 = mahal (fitcdiscr (meas, species), meas(1:5,:)); %! Mdl = fitcdiscr (meas, species, 'Prior', [0.6, 0.2, 0.2]); %! assert_equal (mahal (Mdl, meas(1:5,:)), M1, 1e-12); ## logp is the log density summed over the classes at their priors. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! lp = logp (Mdl, meas(1:5,:)); %! assert_equal (size (lp), [5, 1]); %! assert_equal (lp, [0.059358043320009; -0.810769588483861; ... %! -0.071737748242414; -0.838445989301495; ... %! -0.092912056048684], 1e-12); ## logp on a quadratic model. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'quadratic'); %! lp = logp (Mdl, meas(1:5,:)); %! assert_equal (lp, [1.534756847193468; 0.718766664165731; ... %! 1.117146185488189; 0.906210252665867; ... %! 1.378471050607217], 1e-12); ## The prior reweights the density, unlike the distance. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'Prior', [0.6, 0.2, 0.2]); %! lp = logp (Mdl, meas(1:5,:)); %! assert_equal (lp, [0.647144708222129; -0.222982923581741; ... %! 0.516048916659706; -0.250659324399375; ... %! 0.494874608853435], 1e-12); %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! mahal (Mdl, []) %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! mahal (Mdl, ones (3, 2)) %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! mahal (Mdl, {1, 2, 3, 4}) %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! mahal (Mdl, meas(1:5,:), 'ClassLabels') %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! mahal (Mdl, meas(1:5,:), 5, 1) %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! mahal (Mdl, meas(1:5,:), 'bogus', 1) %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! mahal (Mdl, meas(1:2,:), 'ClassLabels', {1; 2}) %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! mahal (Mdl, meas(1:5,:), 'ClassLabels', species(1:3)) %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! mahal (Mdl, meas(1:2,:), 'ClassLabels', {'setosa'; 'nosuchspecies'}) %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! logp (Mdl, []) %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! logp (Mdl, ones (3, 2)) %!error ... %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! logp (Mdl, {1, 2, 3, 4}) %!test %! ## DeltaPredictor follows Gamma: regularizing the covariance moves the %! ## coefficients, and the delta at which a predictor drops out with them. %! ## Measured against R2024a at four points of the grid. %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! got = zeros (1, 4); %! g = [0, 1/3, 2/3, 1]; %! for k = 1:4 %! m = Mdl; %! m.Gamma = g(k); %! got(k) = max (m.DeltaPredictor); %! endfor %! assert_equal (got, [7.2926, 5.2537, 4.8816, 5.3354], 1e-4); %!test %! ## cvshrink returns a column per output over the default grid of eleven %! ## Gamma values. %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! [err, gamma, delta, numpred] = cvshrink (Mdl); %! assert_equal (size (err), [11, 1]); %! assert_equal (size (gamma), [11, 1]); %! assert_equal (size (delta), [11, 1]); %! assert_equal (size (numpred), [11, 1]); %! assert_equal (gamma, (0:0.1:1)', 1e-12); %! assert_equal (delta, zeros (11, 1)); %! assert_equal (numpred, repmat (4, 11, 1)); %! assert_equal (all (err >= 0 & err <= 1), true); %!test %! ## NumGamma sets the number of intervals, so one more value than that. %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! [err, gamma] = cvshrink (Mdl, "NumGamma", 4); %! assert_equal (size (err), [5, 1]); %! assert_equal (gamma, [0; 0.25; 0.5; 0.75; 1], 1e-12); %!test %! ## The Delta grid runs to the point where the last predictor is gone, and %! ## that point moves with Gamma. Measured against R2024a. %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! [err, gamma, delta, numpred] = cvshrink (Mdl, "NumGamma", 3, "NumDelta", 2); %! assert_equal (size (err), [4, 3]); %! assert_equal (size (delta), [4, 3]); %! assert_equal (delta, [0, 3.6463, 7.2926; 0, 2.6269, 5.2537; ... %! 0, 2.4408, 4.8816; 0, 2.6677, 5.3354], 1e-4); %! ## every predictor is in at Delta of zero, and they leave as it grows %! assert_equal (numpred(:,1), repmat (4, 4, 1)); %! assert_equal (all (numpred(:,3) < numpred(:,1)), true); %!test %! ## An explicit Gamma is used as given. %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! [err, gamma] = cvshrink (Mdl, "Gamma", [0, 0.5, 1]); %! assert_equal (size (err), [3, 1]); %! assert_equal (gamma, [0; 0.5; 1], 1e-12); %!test %! ## An explicit Delta vector is used for every Gamma. %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! [err, gamma, delta] = cvshrink (Mdl, "Gamma", [0, 1], "Delta", [0, 2]); %! assert_equal (size (delta), [2, 2]); %! assert_equal (delta, [0, 2; 0, 2]); %!error ... %! cvshrink (fitcdiscr (ones (6, 2) + [1;2;3;4;5;6], [1;1;1;2;2;2]), "NumGamma") %!error ... %! cvshrink (fitcdiscr (ones (6, 2) + [1;2;3;4;5;6], [1;1;1;2;2;2]), "NumGamma", 0) %!error ... %! cvshrink (fitcdiscr (ones (6, 2) + [1;2;3;4;5;6], [1;1;1;2;2;2]), "NumDelta", -1) %!error ... %! cvshrink (fitcdiscr (ones (6, 2) + [1;2;3;4;5;6], [1;1;1;2;2;2]), "Gamma", 2) %!error ... %! cvshrink (fitcdiscr (ones (6, 2) + [1;2;3;4;5;6], [1;1;1;2;2;2]), "bogus", 1) ## HyperparameterOptimizationResults is declared for MATLAB compatibility and ## stays empty, this class running no search over its hyperparameters. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! assert_equal (isempty (Mdl.HyperparameterOptimizationResults), true); ## ModelParameters records the fit as it was asked for. The field list and ## its order are MATLAB's, measured on R2024a, less SaveMemory, which this ## class does not offer. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! assert_equal (fieldnames (Mdl.ModelParameters)', {'DiscrimType', ... %! 'Gamma', 'Delta', 'FillCoeffs', 'Version', 'Method', 'Type'}); %!test %! load fisheriris %! MP = fitcdiscr (meas, species).ModelParameters; %! assert_equal (MP.DiscrimType, 'linear'); %! assert_equal (MP.Gamma, 0); %! assert_equal (MP.Delta, 0); %! assert_equal (MP.FillCoeffs, true); %!test %! load fisheriris %! MP = fitcdiscr (meas, species).ModelParameters; %! assert_equal (MP.Version, 1); %! assert_equal (MP.Method, 'Discriminant'); %! assert_equal (MP.Type, 'classification'); ## FillCoeffs is reported as the flag it is, not as the 'on'/'off' it is ## given as. %!test %! load fisheriris %! MP = fitcdiscr (meas, species, 'FillCoeffs', 'off').ModelParameters; %! assert_equal (MP.FillCoeffs, false); %! assert_equal (class (MP.FillCoeffs), 'logical'); %!test %! load fisheriris %! MP = fitcdiscr (meas, species, 'DiscrimType', 'pseudoLinear', ... %! 'Gamma', 0.3, 'Delta', 0.1).ModelParameters; %! assert_equal (MP.DiscrimType, 'pseudoLinear'); %! assert_equal (MP.Gamma, 0.3); %! assert_equal (MP.Delta, 0.1); ## The Gamma property is forced by the type, 0 for a pseudo type and 1 for a ## diagonal one, but the fit was asked for 0.3 and that is what is recorded. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'diagLinear', 'Gamma', 0.3); %! assert_equal (Mdl.ModelParameters.Gamma, 0.3); %! assert_equal (Mdl.Gamma, 1); ## A compact model keeps what predict needs and drops the record of the fit, ## as MATLAB's does. %!test %! load fisheriris %! CMdl = compact (fitcdiscr (meas, species)); %! assert_equal (isprop (CMdl, 'ModelParameters'), false); ## Every documented score transform reaches the scores that are reported, and ## none of them moves the label: a transform reshapes what is reported, not ## what is decided. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! T = {'identity', @(x) x; 'doublelogit', @(x) 1 ./ (1 + exp (-2 * x)); ... %! 'invlogit', @(x) log (x ./ (1 - x)); ... %! 'logit', @(x) 1 ./ (1 + exp (-x)); ... %! 'sign', @(x) sign (x); 'symmetric', @(x) 2 * x - 1; ... %! 'symmetriclogit', @(x) 2 ./ (1 + exp (-x)) - 1}; %! for i = 1:rows (T) %! Mdl.ScoreTransform = T{i,1}; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, T{i,2}(raw), 1e-12); %! assert_equal (l, label); %! endfor %! ## ismax marks the largest score of each observation, ties to the first. %! [~, k] = max (raw, [], 2); %! e = zeros (size (raw)); %! e(sub2ind (size (raw), (1:rows (raw))', k)) = 1; %! Mdl.ScoreTransform = 'ismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, e); %! Mdl.ScoreTransform = 'symmetricismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, 2 * e - 1); ## A function handle is taken as given and applied to the scores. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! Mdl.ScoreTransform = @(x) x .^ 2; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, raw .^ 2, 1e-12); %! assert_equal (l, label); %!error ... %! ClassificationDiscriminant ([1, 2; 2, 1; 3, 2; 4, 1], [1; 1; 2; 2], ... %! 'CategoricalPredictors', 1) ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcdiscr (T, 'Species'); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'Species'), a); %! assert_equal (loss (Mdl, T), a); ## A table at edge %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcdiscr (T, 'Species'); %! a = edge (Mdl, X, y); %! assert_equal (edge (Mdl, T(:,1:2), y), a); %! assert_equal (edge (Mdl, T, 'Species'), a); %! assert_equal (edge (Mdl, T), a); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcdiscr (T, 'Species'); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## A table at mahal and logp %!test # a table is matched to the predictors by name %! load fisheriris %! T = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species); %! Mdl = fitcdiscr (T, 'Species'); %! a = mahal (Mdl, meas); %! assert_equal (mahal (Mdl, T(:,1:4)), a); %! assert_equal (mahal (Mdl, T(:,[5, 4, 2, 3, 1])), a); %!test # a table is matched to the predictors by name %! load fisheriris %! T = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species); %! Mdl = fitcdiscr (T, 'Species'); %! a = logp (Mdl, meas); %! assert_equal (logp (Mdl, T(:,1:4)), a); %! assert_equal (logp (Mdl, T(:,[5, 4, 2, 3, 1])), a); ## resubLoss takes a loss function and weights together, the weights given ## replacing the ones the model was fitted with %!test %! load fisheriris %! w = (1:150)'; %! Mdl = fitcdiscr (meas, species); %! assert_equal (resubLoss (Mdl, 'LossFun', 'classiferror', 'Weights', w), ... %! loss (Mdl, meas, species, 'LossFun', 'classiferror', ... %! 'Weights', w)); ## Observation weights of class single or double %!error ... %! fitcdiscr ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', ... %! int8 ([1; 1; 1; 1])) %!error ... %! fitcdiscr ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', true (4, 1)) %!test %! ## Single weights are stored single, summing to one %! load fisheriris %! w = 1 + (1:150)' / 7; %! Mdl = fitcdiscr (meas, species, 'Weights', single (w)); %! assert_equal (class (Mdl.W), 'single'); %! assert_equal (sum (double (Mdl.W)), 1, 1e-6); %!test %! ## Single weights compute as double %! load fisheriris %! w = 1 + (1:150)' / 7; %! A = fitcdiscr (meas, species, 'Weights', single (w)); %! B = fitcdiscr (meas, species, 'Weights', double (single (w))); %! assert_equal (nthargout (2, @predict, A, meas), nthargout (2, @predict, ... %! B, meas)); ## A query with a missing value has no posterior %!test %! X = [1 2; 2 1; 2 3; 3 2; 6 7; 7 6; 7 8; 8 7; 1 7; 2 8; 2 6; 3 7]; %! Y = [1 1 1 1 2 2 2 2 3 3 3 3]'; %! Mdl = ClassificationDiscriminant (X, Y, 'Prior', [0.2 0.5 0.3], ... %! 'Cost', [0 9 1; 1 0 1; 1 9 0]); %! [label, score, cost] = predict (Mdl, [NaN 2; 2 NaN; Inf 2; 2 2]); %! assert_equal (label, [2; 2; 2; 1]); %! assert_equal (isnan (score), [true(3); false(1, 3)]); %! assert_equal (isnan (cost), [true(3); false(1, 3)]); %!test %! X = [1 2; 2 1; 2 3; 3 2; 6 7; 7 6; 7 8; 8 7; 1 7; 2 8; 2 6; 3 7]; %! Y = [1 1 1 1 2 2 2 2 3 3 3 3]'; %! Mdl = ClassificationDiscriminant (X, Y, 'Prior', [0.2 0.5 0.3]); %! q = [NaN 2; 2 NaN; NaN NaN; 2 2]; %! assert_equal (loss (Mdl, q, [1; 2; 3; 1]), 0.4, 1e-14); %! assert_equal (margin (Mdl, q, [1; 2; 3; 1]), ... %! [NaN; NaN; NaN; 0.999999978417601], 1e-12); %! assert_equal (edge (Mdl, q, [1; 2; 3; 1]), NaN); ## A row missing every predictor is dropped, one missing some is kept %!test %! X = [1 2; 2 1; NaN NaN; 3 2; 6 7; 7 6; 7 NaN; 8 7; 1 7; 2 8; 2 6; 3 7]; %! Y = [1 1 1 1 2 2 2 2 3 3 3 3]'; %! Mdl = ClassificationDiscriminant (X, Y); %! assert_equal (Mdl.NumObservations, 11); %! assert_equal (Mdl.RowsUsed, [true; true; false; true(9, 1)]); %! assert_equal (Mdl.Prior, [3, 4, 4] / 11, 1e-14); %! assert_equal (Mdl.Sigma, diag ([0.869565217391305, 0.463768115942029]), ... %! 1e-14); %! assert_equal (size (Mdl.X), [11, 2]); %!error ... %! ClassificationDiscriminant ([NaN NaN; NaN NaN; 1 2; 2 1], [1; 1; 2; 2]) statistics-release-2.1.2/inst/Machine_Learning/ClassificationECOC.m000066400000000000000000001577771527311104300252540ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ClassificationECOC < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} ClassificationECOC ## ## A multiclass model built from binary learners. ## ## An error correcting output codes model turns a problem of @math{K} classes ## into a set of two class problems. A coding matrix gives one column per ## binary learner saying which classes that learner calls +1, which it calls ## -1, and which sit it out; a new observation is sent to every learner and ## given the class whose column of the matrix its scores match most closely. ## ## The fit is carried out by the learners themselves, whichever ## @code{fitcecoc} was asked for, and the decoding by ## @code{CompactClassificationECOC}, which this class holds the data of a fit ## on top of. ## ## @seealso{fitcecoc, CompactClassificationECOC, designecoc} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} X ## ## The predictor data the model was fitted on, one row per observation. ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## This property is read-only. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} Y ## ## The class labels the model was fitted on. This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} W ## ## The observation weights, scaled so that each class carries its prior and ## the whole sums to one. It has the class of the @qcode{'Weights'} given, ## single or double. This property is read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} RowsUsed ## ## A logical column marking the rows of the original data that were ## used. A row is dropped when its class is missing, when every predictor ## is missing, or when its weight is zero; a row missing only some ## predictors is used, each binary learner taking the missing values by ## its own rule. This property is read-only. ## ## @end deftp RowsUsed = []; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} NumObservations ## ## The number of observations used. This property is read-only. ## ## @end deftp NumObservations = 0; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} BinaryY ## ## What each observation was to each binary learner ## ## An @math{NxL} matrix of -1, 0 and +1, row @math{n} being the row of ## @code{CodingMatrix} belonging to that observation's class. This ## property is read-only. ## ## @end deftp BinaryY = []; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} BinaryLearners ## ## The trained binary learners, one per column of @code{CodingMatrix}. ## This property is read-only. ## ## @end deftp BinaryLearners = {}; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} CodingMatrix ## ## The coding design, a @math{KxL} matrix of -1, 0 and +1. This ## property is read-only. ## ## @end deftp CodingMatrix = []; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} CodingName ## ## The name of the coding design, or @qcode{'custom'} when the matrix was ## given outright. This property is read-only. ## ## @end deftp CodingName = 'onevsone'; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} LearnerWeights ## ## The total observation weight each binary learner was trained on. This ## property is read-only. ## ## @end deftp LearnerWeights = []; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} ClassNames ## ## The distinct class labels, sorted or in the order given by the ## @qcode{'ClassNames'} option; the rows of @code{CodingMatrix}, ## @code{Prior} and @code{Cost} take them in that order. This property is ## read-only. ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} PredictorNames ## ## The names of the predictors. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} ExpandedPredictorNames ## ## The names of the predictors as the learners saw them. This property ## is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} CategoricalPredictors ## ## The columns holding categorical predictors, empty when none is. Every ## binary learner was given them. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} ResponseName ## ## The name of the response variable. This property is read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} BinEdges ## ## The bin edges of the predictors, empty unless the learners binned ## them, which none here does. This property is read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} HyperparameterOptimizationResults ## ## The result of optimizing the hyperparameters, always empty here, no ## such optimization being implemented. This property is read-only. ## ## @end deftp HyperparameterOptimizationResults = []; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} ModelParameters ## ## A structure of the options the model was fitted with. This property ## is read-only. ## ## @end deftp ModelParameters = []; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} BinaryLoss ## ## The loss that turns a binary learner's score into a cost. See ## @code{CompactClassificationECOC.BinaryLoss}. ## ## @end deftp BinaryLoss = 'hinge'; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} Prior ## ## The prior probability of each class. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} Cost ## ## The cost of misclassification, a @math{KxK} matrix. ## ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {ClassificationECOC} {property} ScoreTransform ## ## The transform applied to the predicted scores. ## ## @end deftp ScoreTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ScoreRange = [-Inf, Inf]; STfun = @(x) x; endproperties methods (Hidden) function this = set.ScoreTransform (this, val) name = 'ClassificationECOC'; try [this.STfun, this.ScoreTransform] = parseScoreTransform (val, name); catch error (strcat ("ClassificationECOC.subsasgn: 'ScoreTransform' must", ... " be a character vector or a 'function_handle'", ... " object.")); end_try_catch endfunction function this = set.BinaryLoss (this, val) if (! (ischar (val) && isrow (val))) error (strcat ("ClassificationECOC.subsasgn: 'BinaryLoss' must be", ... " a character vector.")); endif [~, errmsg] = ecocDecode (zeros (1, columns (this.CodingMatrix)), ... this.CodingMatrix, tolower (val), ... 'lossweighted', this.ScoreRange); if (! isempty (errmsg)) error ("ClassificationECOC.subsasgn: %s", errmsg); endif this.BinaryLoss = tolower (val); endfunction function disp (this) fprintf ("\n ClassificationECOC\n\n"); fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); fprintf ("%+25s: %s\n", 'CategoricalPredictors', ... mat2str (this.CategoricalPredictors)); fprintf ("%+25s: %s\n", 'ClassNames', classNameListing (this.ClassNames)); fprintf ("%+25s: '%s'\n", 'ScoreTransform', this.ScoreTransform); fprintf ("%+25s: {%dx%d cell}\n", 'BinaryLearners', ... numel (this.BinaryLearners), 1); fprintf ("%+25s: '%s'\n", 'CodingName', this.CodingName); fprintf ("\n"); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {ClassificationECOC} {@var{obj} =} ClassificationECOC (@var{X}, @var{Y}) ## @deftypefnx {ClassificationECOC} {@var{obj} =} ClassificationECOC (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationECOC} {@var{obj} =} ClassificationECOC (@var{Tbl}, @var{formula}) ## @deftypefnx {ClassificationECOC} {@var{obj} =} ClassificationECOC (@var{Tbl}, @var{Y}) ## @deftypefnx {ClassificationECOC} {@var{obj} =} ClassificationECOC (@dots{}, @var{name}, @var{value}) ## ## Fit a multiclass model from binary learners. ## ## @code{@var{obj} = ClassificationECOC (@var{X}, @var{Y})} fits one ## binary learner per column of a one against one coding design and ## returns them as a @code{ClassificationECOC} object. @code{fitcecoc} is ## the documented way in, and its help lists the options both take. ## ## @seealso{fitcecoc, CompactClassificationECOC, designecoc} ## @end deftypefn function this = ClassificationECOC (X, Y, varargin) if (nargin < 2) error ("ClassificationECOC: too few input arguments."); endif ## A table names its own predictors and says which hold levels [this, X, Y, varargin] = resolveTable (this, 'ClassificationECOC', ... X, Y, varargin); if (mod (numel (varargin), 2) != 0) error ("ClassificationECOC: name-value arguments must be in pairs."); endif if (rows (X) != rows (Y)) error ("ClassificationECOC: number of rows in X and Y must be equal."); endif ## Parse optional paired arguments; the classes, prior, cost and ## weights come from the response when left empty, and the binary loss ## from the learners. optNames = {'ClassNames', 'Cost', 'Prior', 'Weights', ... 'PredictorNames', 'ResponseName', 'ScoreTransform', ... 'Coding', 'Learners', 'BinaryLoss', ... 'CategoricalPredictors', 'FitPosterior'}; dfValues = {[], [], [], [], {}, 'Y', 'none', 'onevsone', 'svm', [], ... [], []}; [ClassNames, Cost, Prior, Weights, PredictorNames, ResponseName, ... ScoreTransform, Coding, Learners, BinaryLoss, CatPreds, ... FitPosterior, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments. Asking for no posterior is ## asking for nothing; asking for one is refused. if (! (isempty (FitPosterior) || (isscalar (FitPosterior) && ! FitPosterior))) error (strcat ("ClassificationECOC: 'FitPosterior' is not", ... " implemented, the binary learners having no", ... " fitted score transform to install.")); endif if (! isempty (args)) error ("ClassificationECOC: invalid optional paired argument."); endif ## The learner, as a template or as a name. [tmpl, errmsg] = ClassificationECOC.ecocLearnerTemplate (Learners); if (! isempty (errmsg)) error ("ClassificationECOC: %s", errmsg); endif ## A row missing some predictors is kept, each binary learner taking ## the missing values by its own rule, as in R2024a; a row missing every ## predictor, or of weight zero, is dropped F = classFrame (X, Y, ClassNames, Prior, Cost, Weights, ... 'ClassificationECOC', false, 'keep', 'partial'); K = classCount (F.ClassNames); ## The coding design, named or given outright. if (ischar (Coding) && isrow (Coding)) M = designecoc (K, Coding); this.CodingName = tolower (Coding); elseif (isnumeric (Coding) && ismatrix (Coding)) [M, errmsg] = ClassificationECOC.checkCodingMatrix (Coding, K); if (! isempty (errmsg)) error ("ClassificationECOC: %s", errmsg); endif this.CodingName = 'custom'; else error (strcat ("ClassificationECOC: 'Coding' must be a character", ... " vector or a coding matrix.")); endif this.X = F.X; this.Y = F.Y; this.W = cast (F.W, F.WeightsClass); this.RowsUsed = F.RowsUsed; this.NumObservations = F.n; this.ClassNames = F.ClassNames; this.Prior = F.Prior; this.Cost = F.Cost; this.ResponseName = ResponseName; this.CodingMatrix = M; this.BinaryY = M(F.gY, :); this.ScoreRange = ... ClassificationECOC.ecocScoreRange (tmpl.Method); if (isempty (PredictorNames)) PredictorNames = arrayfun (@(k) sprintf ('x%d', k), ... 1:columns (F.X), 'UniformOutput', false); endif this.PredictorNames = PredictorNames; this.ExpandedPredictorNames = PredictorNames; ## Categorical predictors are checked here and handed to every binary ## learner as given, which codes them its own way: the model keeps the ## predictor names, the learners report the expanded ones, as in R2024a. [Cod, errmsg] = dummyCoding (F.X, CatPreds, PredictorNames); if (! isempty (errmsg)) error ("ClassificationECOC: %s", errmsg); endif this.CategoricalPredictors = []; if (! isempty (Cod.Index)) this.CategoricalPredictors = Cod.Index; endif ## One learner per column: the classes that column marks +1 against ## those it marks -1, the rest of the rows left out of the fit. L = columns (M); this.BinaryLearners = cell (L, 1); this.LearnerWeights = zeros (1, L); for j = 1:L take = M(F.gY, j) != 0; by = M(F.gY(take), j); ## A side left without observations, its classes removed for a prior ## of 0, has prior 0 in its learner too, which then fits the other ## side alone, as MATLAB's does pr = []; if (! any (by == -1)) pr = [0, 1]; elseif (! any (by == 1)) pr = [1, 0]; endif this.BinaryLearners{j} = ... ClassificationECOC.ecocFitBinary (tmpl, F.X(take,:), by, ... F.W(take), PredictorNames, ... CatPreds, pr); this.LearnerWeights(j) = sum (F.W(take)); endfor if (isempty (BinaryLoss)) BinaryLoss = ClassificationECOC.ecocDefaultLoss (tmpl.Method, ... this.ScoreRange); endif this.BinaryLoss = BinaryLoss; this.ScoreTransform = ScoreTransform; this.ModelParameters = struct ('BinaryLearners', tmpl, ... 'Coding', this.CodingName, ... 'FitPosterior', false, ... 'Method', 'ECOC', ... 'Type', 'classification'); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationECOC} {@var{CVMdl} =} crossval (@var{obj}) ## @deftypefnx {ClassificationECOC} {@var{CVMdl} =} crossval (@dots{}, @var{name}, @var{value}) ## ## Cross validate a @code{ClassificationECOC}. ## ## @code{@var{CVMdl} = crossval (@var{obj})} partitions the training data ## into ten folds, fits the same model to each fold's training part, and ## returns them as a @code{ClassificationPartitionedECOC}. ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## ## @item @qcode{'KFold'} @tab @tab The number of folds, an integer greater ## than one. The default is ten, or the number of observations when there ## are fewer than ten. ## ## @item @qcode{'Holdout'} @tab @tab The share of the data held out, a ## number strictly between 0 and 1. ## ## @item @qcode{'Leaveout'} @tab @tab @qcode{'on'} for one fold per ## observation, @qcode{'off'} otherwise. ## ## @item @qcode{'CVPartition'} @tab @tab A @code{cvpartition} object, ## which names the folds outright. ## @end multitable ## ## Only one of the four may be given. ## ## @seealso{ClassificationECOC, ClassificationPartitionedECOC, cvpartition} ## @end deftypefn function CVMdl = crossval (this, varargin) if (numel (varargin) == 1) error (strcat ("ClassificationECOC.crossval: name-value arguments", ... " must be in pairs.")); elseif (numel (varargin) > 2) error (strcat ("ClassificationECOC.crossval: specify only one of", ... " the optional name-value paired arguments.")); endif if (this.NumObservations < 10) numFolds = this.NumObservations; else numFolds = 10; endif Holdout = []; Leaveout = 'off'; CVPartition = []; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'kfold' numFolds = varargin{2}; if (! (isnumeric (numFolds) && isscalar (numFolds) && numFolds == fix (numFolds) && numFolds > 1)) error (strcat ("ClassificationECOC.crossval: 'KFold' must", ... " be an integer value greater than 1.")); endif case 'holdout' Holdout = varargin{2}; if (! (isnumeric (Holdout) && isscalar (Holdout) && Holdout > 0 && Holdout < 1)) error (strcat ("ClassificationECOC.crossval: 'Holdout' must", ... " be a numeric value between 0 and 1.")); endif case 'leaveout' Leaveout = varargin{2}; if (! (ischar (Leaveout) && any (strcmpi (Leaveout, {'on', 'off'})))) error (strcat ("ClassificationECOC.crossval: 'Leaveout' must", ... " be either 'on' or 'off'.")); endif case 'cvpartition' CVPartition = varargin{2}; if (! isa (CVPartition, 'cvpartition')) error (strcat ("ClassificationECOC.crossval: 'CVPartition'", ... " must be a 'cvpartition' object.")); endif otherwise error (strcat ("ClassificationECOC.crossval: invalid parameter", ... " name in optional paired arguments.")); endswitch varargin(1:2) = []; endwhile ## The response is passed rather than a count so that the folds stay ## stratified, as every other learner here does. if (! isempty (CVPartition)) partition = CVPartition; elseif (! isempty (Holdout)) partition = cvpartition (this.Y, 'Holdout', Holdout); elseif (strcmpi (Leaveout, 'on')) partition = cvpartition (this.NumObservations, 'LeaveOut'); else partition = cvpartition (this.Y, 'KFold', numFolds); endif CVMdl = ClassificationPartitionedECOC (this, partition); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationECOC} {@var{CMdl} =} compact (@var{obj}) ## ## Drop the training data from a @code{ClassificationECOC}. ## ## @code{@var{CMdl} = compact (@var{obj})} returns a ## @code{CompactClassificationECOC} carrying the binary learners and the ## coding matrix but not @code{X}, @code{Y} or @code{W}, so it predicts ## and scores new data but cannot be refitted or cross validated. ## ## @seealso{ClassificationECOC, CompactClassificationECOC} ## @end deftypefn function CMdl = compact (this) CMdl = CompactClassificationECOC (this); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationECOC} {@var{label} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {ClassificationECOC} {[@var{label}, @var{NegLoss}, @var{PBScore}] =} predict (@dots{}) ## @deftypefnx {ClassificationECOC} {[@dots{}] =} predict (@dots{}, @var{name}, @var{value}) ## ## Classify new data with a trained @code{ClassificationECOC}. ## ## It takes and returns exactly what ## @code{CompactClassificationECOC.predict} does, the training data ## playing no part in a prediction. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @seealso{ClassificationECOC, CompactClassificationECOC.predict} ## @end deftypefn function [label, NegLoss, PBScore] = predict (this, XC, varargin) if (nargin < 2) error ("ClassificationECOC.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'ClassificationECOC.predict', XC); [label, NegLoss, PBScore] = predict (compact (this), XC, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationECOC} {@var{obj} =} discardSupportVectors (@var{obj}) ## ## Give up the support vectors of the binary learners. ## ## @code{@var{obj} = discardSupportVectors (@var{obj})} empties ## @code{Alpha}, @code{SupportVectors} and @code{SupportVectorLabels} on ## every binary learner that is a support vector machine on a linear ## kernel, whose linear model stands in for them exactly, so nothing the ## model answers changes. Any other learner is left as it is, a code ## being free to mix them, and a model with no linear support vector ## machine among its learners warns and is returned unchanged. ## ## @seealso{ClassificationECOC, ClassificationSVM.discardSupportVectors} ## @end deftypefn function this = discardSupportVectors (this) [learners, n] = ecocDiscardSVs (this.BinaryLearners); if (n == 0) warning (strcat ("ClassificationECOC.discardSupportVectors: no", ... " binary", ... " learner is a support vector machine on a", ... " linear kernel; nothing was discarded.")); return; endif this.BinaryLearners = learners; endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationECOC} {@var{sub} =} selectModels (@var{obj}, @var{idx}) ## ## Keep a subset of the fitted regularization strengths. ## ## @code{@var{sub} = selectModels (@var{obj}, @var{idx})} narrows every ## binary learner to the strengths @var{idx} names, which may be indices ## into the learner's @qcode{Lambda} or a logical vector over it. Only a ## linear learner is fitted over several strengths, so any other raises. ## ## @seealso{ClassificationECOC, ClassificationLinear.selectModels} ## @end deftypefn function sub = selectModels (this, idx) if (nargin < 2) error ("ClassificationECOC.selectModels: too few input arguments."); endif [learners, errmsg] = ecocSelectModels (this.BinaryLearners, idx); if (! isempty (errmsg)) error ("ClassificationECOC.selectModels: %s", errmsg); endif sub = this; sub.BinaryLearners = learners; endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationECOC} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationECOC} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationECOC} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margin of a @code{ClassificationECOC}. See ## @code{CompactClassificationECOC.margin}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{CompactClassificationECOC.margin} ## @end deftypefn function m = margin (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationECOC.margin: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'margin', X, Y, ... varargin, nargin > 2); m = margin (compact (this), X, Y, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationECOC} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationECOC} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationECOC} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Classification edge of a @code{ClassificationECOC}. See ## @code{CompactClassificationECOC.edge}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{CompactClassificationECOC.edge} ## @end deftypefn function e = edge (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationECOC.edge: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'edge', X, Y, varargin, ... nargin > 2); e = edge (compact (this), X, Y, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationECOC} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationECOC} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationECOC} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## ## Classification loss of a @code{ClassificationECOC}. See ## @code{CompactClassificationECOC.loss}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{CompactClassificationECOC.loss} ## @end deftypefn function L = loss (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationECOC.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); L = loss (compact (this), X, Y, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationECOC} {@var{label} =} resubPredict (@var{obj}) ## ## Classify the training data with a trained @code{ClassificationECOC}. ## ## @seealso{ClassificationECOC.predict} ## @end deftypefn function [label, NegLoss, PBScore] = resubPredict (this, varargin) [label, NegLoss, PBScore] = predict (this, this.X, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationECOC} {@var{m} =} resubMargin (@var{obj}) ## ## Classification margin on the training data. ## ## @seealso{ClassificationECOC.margin} ## @end deftypefn function m = resubMargin (this, varargin) m = margin (this, this.X, this.Y, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationECOC} {@var{e} =} resubEdge (@var{obj}) ## ## Classification edge on the training data. ## ## @seealso{ClassificationECOC.edge} ## @end deftypefn function e = resubEdge (this, varargin) e = edge (this, this.X, this.Y, 'Weights', this.W, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationECOC} {@var{L} =} resubLoss (@var{obj}) ## ## Classification loss on the training data. ## ## @seealso{ClassificationECOC.loss} ## @end deftypefn function L = resubLoss (this, varargin) L = loss (this, this.X, this.Y, 'Weights', this.W, varargin{:}); endfunction endmethods ## Helpers of the fit. Each has one consumer, so it sits here rather than ## in private/, and none of them needs the object. methods (Static, Access = private) ## The learner, given as a template or by name. function [tmpl, errmsg] = ecocLearnerTemplate (Learners) tmpl = []; errmsg = ''; known = {'svm', 'tree', 'knn', 'naivebayes', 'discriminant', ... 'linear', 'kernel'}; makers = {@templateSVM, @templateTree, @templateKNN, ... @templateNaiveBayes, @templateDiscriminant, ... @templateLinear, @templateKernel}; if (ischar (Learners) && isrow (Learners)) k = find (strcmp (tolower (Learners), known)); if (strcmpi (Learners, 'ensemble')) ## MATLAB's default ensemble for a binary learner. tmpl = templateEnsemble ('LogitBoost', 100, 'tree'); elseif (isempty (k)) errmsg = strcat ("'", Learners, "' is not a binary learner."); return; else tmpl = makers{k} (); endif elseif (isstruct (Learners) && isscalar (Learners) && isfield (Learners, 'Method')) tmpl = Learners; if (any (strcmp (tmpl.Method, ClassificationECOC.ensembleMethods ()))) if (! strcmp (tmpl.Type, 'classification')) errmsg = "templates of regression type are not supported."; return; endif elseif (! any (strcmp (tolower (tmpl.Method), known))) errmsg = strcat ("'", tmpl.Method, "' is not a binary learner."); return; endif else errmsg = strcat ("'Learners' must be a learner name or a template."); endif endfunction ## The interval a learner scores on, which is what decides the losses its ## scores can be read with. Measured on R2024a across all seven learners ## and the ensembles: a boosted ensemble scores on the whole line, and a ## bagged or random subspace one with class probabilities. A RUSBoost ## one scores in [0,1] once CompactClassificationECOC scales it. function range = ecocScoreRange (method) if (any (strcmpi (method, {'SVM', 'Linear', 'Kernel', 'AdaBoostM1', ... 'AdaBoostM2', 'GentleBoost', ... 'LogitBoost', 'LPBoost', 'TotalBoost', ... 'RobustBoost'}))) range = [-Inf, Inf]; else range = [0, 1]; endif endfunction ## The loss a learner's scores are read with when none is asked for, as ## R2024a chooses it. function loss = ecocDefaultLoss (method, range) if (any (strcmp (method, {'AdaBoostM1', 'GentleBoost'}))) loss = 'exponential'; elseif (strcmp (method, 'LogitBoost')) loss = 'binodeviance'; elseif (isequal (range, [0, 1])) loss = 'quadratic'; else loss = 'hinge'; endif endfunction ## The methods an ensemble template may name. function names = ensembleMethods () names = {'AdaBoostM1', 'AdaBoostM2', 'GentleBoost', 'LogitBoost', ... 'LPBoost', 'TotalBoost', 'RobustBoost', 'RUSBoost', ... 'Subspace', 'Bag', 'LSBoost'}; endfunction ## Fit one binary learner. Its two classes are given outright as -1 and ## +1 so that the second is always the one the column calls +1, which is ## the score the decoding reads. function Mdl = ecocFitBinary (tmpl, X, y, w, pnames, cats = [], pr = []) ## An ensemble template carries the method, cycles and learners under ## names of its own, which the ensemble takes under fitcensemble's. ensemble = any (strcmp (tmpl.Method, ... ClassificationECOC.ensembleMethods ())); skip = {'Method', 'Type'}; args = {}; if (ensemble) skip(end+1:end+2) = {'NLearn', 'LearnerTemplates'}; args = {'Method', tmpl.Method, 'NumLearningCycles', tmpl.NLearn, ... 'Learners', tmpl.LearnerTemplates}; endif for [val, name] = tmpl if (any (strcmp (name, skip))) continue; endif args(end+1:end+2) = {name, val}; endfor ## Weights are passed only when they carry information: uniform ones ## say nothing a learner does not assume. args(end+1:end+4) = {'PredictorNames', pnames, 'ClassNames', [-1; 1]}; if (! isempty (cats)) args(end+1:end+2) = {'CategoricalPredictors', cats}; endif ## Weights spread a prior over its classes one class at a time, so even ## equal ones differ in the last bits; only a real spread counts. if (max (w) - min (w) > 1e-12 * max (w)) args(end+1:end+2) = {'Weights', w}; endif if (! isempty (pr)) args(end+1:end+2) = {'Prior', pr}; endif if (ensemble && strcmp (tmpl.Method, 'Bag')) Mdl = ClassificationBaggedEnsemble (X, y, args{:}); return; elseif (ensemble) Mdl = ClassificationEnsemble (X, y, args{:}); return; endif switch (tolower (tmpl.Method)) case 'svm' Mdl = ClassificationSVM (X, y, args{:}); case 'tree' Mdl = ClassificationTree (X, y, args{:}); case 'knn' Mdl = ClassificationKNN (X, y, args{:}); case 'naivebayes' Mdl = ClassificationNaiveBayes (X, y, args{:}); case 'discriminant' Mdl = ClassificationDiscriminant (X, y, args{:}); case 'linear' Mdl = ClassificationLinear (X, y, args{:}); case 'kernel' Mdl = ClassificationKernel (X, y, args{:}); endswitch endfunction ## A coding matrix given outright. A column that marks nothing +1 or ## nothing -1 trains no learner, and one that repeats another up to sign ## trains the same learner twice; both are refused, as R2024a refuses ## them. function [M, errmsg] = checkCodingMatrix (M, K) errmsg = ''; if (! (isnumeric (M) && ismatrix (M) && ! isempty (M) && rows (M) == K)) errmsg = strcat ("'Coding' must be a matrix with one row per", ... " class."); return; endif if (! all (ismember (M(:), [-1, 0, 1]))) errmsg = "'Coding' must hold only -1, 0 and 1."; return; endif if (! all (any (M > 0, 1) & any (M < 0, 1))) errmsg = strcat ("every column of 'Coding' must mark at least one", ... " class 1 and at least one -1."); return; endif if (any (all (M == 0, 2))) errmsg = "every class must take part in at least one column."; return; endif L = columns (M); for a = 1:L-1 for b = a+1:L if (isequal (M(:,a), M(:,b)) || isequal (M(:,a), -M(:,b))) errmsg = sprintf (strcat ("columns %d and %d in the coding", ... " matrix are identical or differ", ... " only by sign."), a, b); return; endif endfor endfor endfunction endmethods endclassdef ## Tests %!test # MATLAB parity: the property surface a fit reports %! load fisheriris %! Mdl = ClassificationECOC (meas, species); %! assert_equal (class (Mdl), 'ClassificationECOC'); %! assert_equal (numel (properties (Mdl)), 22); %! assert_equal (Mdl.ResponseName, 'Y'); %! assert_equal (Mdl.ClassNames, unique (species)); %! assert_equal (Mdl.Prior, [1/3, 1/3, 1/3], 1e-14); %! assert_equal (Mdl.Cost, ones (3) - eye (3)); %! assert_equal (Mdl.ScoreTransform, 'none'); %! assert_equal (Mdl.CategoricalPredictors, []); %!test # the binary learners are one per column of the coding matrix %! load fisheriris %! Mdl = ClassificationECOC (meas, species, 'Coding', 'ternarycomplete'); %! assert_equal (columns (Mdl.CodingMatrix), 6); %! assert_equal (numel (Mdl.BinaryLearners), 6); %! assert_equal (numel (Mdl.LearnerWeights), 6); %!test # MATLAB parity: resubstitution answers the training data %! load fisheriris %! Mdl = ClassificationECOC (meas, species, 'Learners', 'tree'); %! assert_equal (resubLoss (Mdl), 0.02, 1e-12); %! label = resubPredict (Mdl); %! assert_equal (sum (! strcmp (label, species)), 3); %!test # a margin is positive exactly where the label was right %! load fisheriris %! Mdl = ClassificationECOC (meas, species, 'Learners', 'tree'); %! m = resubMargin (Mdl); %! right = strcmp (resubPredict (Mdl), species); %! assert_equal (m > 0, right); %! assert_equal (sum (! right), 3); %!test # compact keeps the learners and drops the data %! load fisheriris %! Mdl = ClassificationECOC (meas, species); %! CMdl = compact (Mdl); %! assert_equal (class (CMdl), 'CompactClassificationECOC'); %! assert_equal (CMdl.CodingMatrix, Mdl.CodingMatrix); %! assert_equal (predict (CMdl, meas(1:5,:)), predict (Mdl, meas(1:5,:))); %! assert_equal (isprop (CMdl, 'X'), false); %!test # a class the coding matrix leaves out of every column is refused %! load fisheriris %! M = [1, 1; -1, 0; 0, -1]; %! assert_equal (columns (ClassificationECOC (meas, species, ... %! 'Coding', M).CodingMatrix), 2); %!test # crossval returns the ECOC partitioned class, not the general one %! load fisheriris %! CV = crossval (ClassificationECOC (meas, species, 'Learners', 'tree')); %! assert_equal (class (CV), 'ClassificationPartitionedECOC'); %! assert_equal (CV.KFold, 10); %! assert_equal (CV.CodingMatrix, [1, 1, 0; -1, 0, 1; 0, -1, -1]); %!test # discarding support vectors changes nothing the model answers %! ## The linear model stands in for the vectors exactly, so the labels and %! ## the loss are what they were and only the memory is gone. %! load fisheriris %! Mdl = fitcecoc (meas, species); %! assert_equal (size (Mdl.BinaryLearners{1}.SupportVectors), [3, 4]); %! D = discardSupportVectors (Mdl); %! assert_equal (class (D), 'ClassificationECOC'); %! assert_equal (size (D.BinaryLearners{1}.SupportVectors), [0, 0]); %! assert_equal (isempty (D.BinaryLearners{1}.Alpha), true); %! assert_equal (predict (D, meas), predict (Mdl, meas)); %! assert_equal (resubLoss (D), 0.0066666666666667, 1e-12); %!warning ... %! load fisheriris; ... %! discardSupportVectors (fitcecoc (meas, species, 'Learners', 'tree')); %!test # a code of trees is returned unchanged by discardSupportVectors %! load fisheriris %! Mdl = fitcecoc (meas, species, 'Learners', 'tree'); %! ## The state is saved and put back rather than switched on: 'on' would %! ## enable warning classes Octave disables by default and leak them into %! ## every test that runs after this one. %! w = warning ('off', 'all'); %! D = discardSupportVectors (Mdl); %! warning (w); %! assert_equal (predict (D, meas), predict (Mdl, meas)); %!test # selectModels narrows every binary learner to the same strengths %! load fisheriris %! ## The constructor is used and not fitcecoc: a linear learner keeps no %! ## training data, so the fit route gives back the compact model. %! LC = ClassificationECOC (meas, species, ... %! 'Learners', templateLinear ('Lambda', [1e-4, 1e-3, 1e-2])); %! assert_equal (LC.BinaryLearners{1}.Lambda, [1e-4, 1e-3, 1e-2], 1e-12); %! S = selectModels (LC, 2); %! assert_equal (class (S), 'ClassificationECOC'); %! for j = 1:numel (S.BinaryLearners) %! assert_equal (S.BinaryLearners{j}.Lambda, 1e-3, 1e-12); %! endfor %!error ... %! load fisheriris; ... %! selectModels (fitcecoc (meas, species)) %!error ... %! load fisheriris; ... %! selectModels (fitcecoc (meas, species, 'Learners', 'tree'), 1) %!test # An unused category of a categorical response is not a class %! load fisheriris %! y = categorical (species); %! Mdl = ClassificationECOC (meas(51:150,:), y(51:150)); %! assert_equal (cellstr (Mdl.ClassNames), {'versicolor'; 'virginica'}); %! assert_equal (size (Mdl.CodingMatrix), [2, 1]); ## Test input validation %!error ... %! crossval (ClassificationECOC (ones (4, 2), [1; 2; 1; 2]), 'KFold', 1) %!error ... %! Mdl = ClassificationECOC (ones (4, 2), [1; 2; 1; 2]); ... %! crossval (Mdl, 'KFold', 2, 'Holdout', 0.3) %!error ... %! ClassificationECOC (ones (4, 2)) %!error ... %! ClassificationECOC (ones (4, 2), [1; 2; 1; 2], 'Coding') %!error ... %! ClassificationECOC (ones (4, 2), [1; 2; 1]) %!error ... %! ClassificationECOC (ones (4, 2), [1; 2; 1; 2], 'Coding', [2; -2]) %!error ... %! ClassificationECOC (ones (4, 2), [1; 2; 1; 2], 'Coding', [1; 1]) %!error ... %! ClassificationECOC (ones (4, 2), [1; 2; 1; 2], 'Coding', [1, -1; -1, 1]) %!error ... %! ClassificationECOC (ones (4, 2), [1; 2; 1; 2], 'Coding', {1}) %!shared Xc, y3 %! k = (0:119)'; %! c1 = mod (k, 3) + 1; %! c3 = 10 * (mod (floor (k / 2), 2) + 1); %! Xc = [c1, sin(k), c3]; %! y3 = mod (c1 + floor (k / 7), 3) + 1; %!test # MATLAB parity: every binary learner gets the categorical predictors %! Mdl = ClassificationECOC (Xc, y3, 'CategoricalPredictors', [1, 3]); %! assert_equal (Mdl.CategoricalPredictors, [1, 3]); %! assert_equal (Mdl.ExpandedPredictorNames, {'x1', 'x2', 'x3'}); %! L = Mdl.BinaryLearners{1}; %! assert_equal (L.CategoricalPredictors, [1, 3]); %! assert_equal (L.ExpandedPredictorNames, {'x1 == 1', 'x1 == 2', ... %! 'x1 == 3', 'x2', 'x3 == 10', 'x3 == 20'}); %! assert_equal (compact (Mdl).CategoricalPredictors, [1, 3]); %!test # tree learners take them too, and so do the folds %! Mdl = ClassificationECOC (Xc, y3, 'CategoricalPredictors', [1, 3], ... %! 'Learners', 'tree'); %! assert_equal (Mdl.BinaryLearners{1}.CategoricalPredictors, [1, 3]); %! CV = crossval (Mdl, 'KFold', 3); %! assert_equal (CV.Trained{1}.CategoricalPredictors, [1, 3]); %!test # MATLAB parity: nearest neighbour learners compare levels %! Mdl = ClassificationECOC (Xc(:,[1, 3]), y3, 'CategoricalPredictors', ... %! 'all', 'Learners', 'knn'); %! assert_equal (Mdl.BinaryLearners{1}.Distance, 'hamming'); %!error ... %! ClassificationECOC (Xc, y3, 'CategoricalPredictors', 4) %!error ... %! ClassificationECOC (Xc, y3, 'CategoricalPredictors', 1, ... %! 'Learners', 'discriminant') %!test # equal weights that differ only by rounding reach any learner %! load fisheriris %! y = [ones(60, 1); 2 * ones(50, 1); 3 * ones(40, 1)]; %! Mdl = ClassificationECOC (meas, y, 'Learners', 'knn'); %! assert_equal (numel (Mdl.BinaryLearners), 3); %! Mdl = ClassificationECOC (meas, y); %! assert_equal (class (Mdl.BinaryLearners{1}), 'ClassificationSVM'); %!test # MATLAB parity: a learner without weights reports its side's prior %! load fisheriris %! y = [ones(60, 1); 2 * ones(50, 1); 3 * ones(40, 1)]; %! Mdl = ClassificationECOC (meas, y, 'Learners', 'knn', 'Prior', 'uniform'); %! assert_equal (Mdl.BinaryLearners{1}.Prior, [0.5, 0.5], 1e-12); %! Mdl = ClassificationECOC (meas(11:150,:), y(11:150)); %! assert_equal (Mdl.BinaryLearners{1}.Prior, [50, 50] / 100, 1e-12); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcecoc (T, 'Species'); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'Species'), a); %! assert_equal (loss (Mdl, T), a); ## A table at edge %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcecoc (T, 'Species'); %! a = edge (Mdl, X, y); %! assert_equal (edge (Mdl, T(:,1:2), y), a); %! assert_equal (edge (Mdl, T, 'Species'), a); %! assert_equal (edge (Mdl, T), a); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcecoc (T, 'Species'); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## Asking for no posterior is accepted; asking for one is refused %!test %! load fisheriris %! Mdl = ClassificationECOC (meas, species, 'FitPosterior', false); %! assert_equal (class (Mdl), 'ClassificationECOC'); ## Observation weights of class single or double %!error ... %! fitcecoc ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', ... %! int8 ([1; 1; 1; 1])) %!error ... %! fitcecoc ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', true (4, 1)) %!test %! ## Single weights are stored single, summing to one %! load fisheriris %! w = 1 + (1:150)' / 7; %! Mdl = fitcecoc (meas, species, 'Weights', single (w), 'Learners', 'tree'); %! assert_equal (class (Mdl.W), 'single'); %! assert_equal (sum (double (Mdl.W)), 1, 1e-6); %!test %! ## Weights varying within a class reach discriminant learners, as R2024a %! load fisheriris %! Mdl = ClassificationECOC (meas, species, 'Learners', 'discriminant', ... %! 'Weights', 1 + (1:150)' / 7); %! [~, NegLoss] = predict (Mdl, meas(134,:)); %! assert_equal (NegLoss, [-2, -0.2957570199841673, -0.2080860771266001], ... %! 1e-12); %!test %! ## Weights varying within a class reach nearest neighbour learners %! load fisheriris %! Mdl = ClassificationECOC (meas, species, ... %! 'Learners', templateKNN ('NumNeighbors', 5), ... %! 'Weights', 1 + (1:150)' / 7); %! [~, NegLoss] = predict (Mdl, meas(58,:)); %! assert_equal (NegLoss, [-1.211303329864724, 0, -1.291948491155047], 1e-12); %!test %! ## Weights varying within a class reach support vector machine learners %! load fisheriris %! Mdl = ClassificationECOC (meas, species, 'Learners', 'svm', ... %! 'Weights', 1 + (1:150)' / 7); %! [~, NegLoss] = predict (Mdl, meas(23,:)); %! assert_equal (NegLoss, [0, -0.749030561477596, -3.406162840589479], 2e-3); %!test %! ## Weights varying within a class reach naive Bayes learners, as R2024a %! load fisheriris %! Mdl = ClassificationECOC (meas, species, 'Learners', 'naivebayes', ... %! 'Weights', 1 + (1:150)' / 7); %! [~, NegLoss] = predict (Mdl, meas(51,:)); %! assert_equal (NegLoss, [-2, -0.1264612831076871, -0.4152333784029093], ... %! 1e-12); ## Test a class with prior 0, whose observations are removed before fitting, ## as MATLAB R2024a and R2026b remove them %!warning ... %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! Mdl = ClassificationECOC (X, Y, 'Prior', [1, 0]); %! assert_equal (Mdl.NumObservations, 50); %! assert_equal (Mdl.RowsUsed, [true(50, 1); false(50, 1)]); %! assert_equal (Mdl.Prior, [1, 0]); %! assert_equal (predict (Mdl, meas([51, 110], 3:4)), ... %! {'versicolor'; 'versicolor'}); %!warning ... %! load fisheriris %! Mdl = ClassificationECOC (meas, species, 'Prior', [0.5, 0.5, 0]); %! assert_equal (predict (Mdl, meas([1, 51, 101], :)), ... %! {'setosa'; 'versicolor'; 'versicolor'}); %!test %! ## A named class with prior 0 may be absent, having nothing to remove %! load fisheriris %! X = meas(51:100, 3:4); %! Y = species(51:100); %! C = {'versicolor', 'virginica'}; %! Mdl = ClassificationECOC (X, Y, 'ClassNames', C, 'Prior', [1, 0]); %! assert_equal (Mdl.ClassNames, {'versicolor'; 'virginica'}); %! assert_equal (predict (Mdl, meas(51, 3:4)), {'versicolor'}); %!warning ... %! ## Each learner left with one side fits it alone; scores as R2024a's %! load fisheriris %! Mdl = ClassificationECOC (meas, species, 'Prior', [0.5, 0.5, 0]); %! [~, score] = predict (Mdl, meas([1, 51, 101], :)); %! assert_equal (score, [0, -0.636121215772315, -1; ... %! -0.891685729975082, 0, -1; ... %! -1.324242131075491, 0, -1], 1e-4); %!test %! load fisheriris %! Mdl = ClassificationECOC (meas(51:150, 3:4), species(51:150), ... %! 'Cost', [0, 1; 0, 0]); %! assert_equal (Mdl.NumObservations, 100); %! assert_equal (Mdl.Prior, [0.5, 0.5]); %!warning ... %! load fisheriris %! Mdl = ClassificationECOC (meas, species, 'Learners', 'tree', ... %! 'Prior', [0.5, 0.5, 0]); %! assert_equal (predict (Mdl, meas([1, 51, 101], :)), ... %! {'setosa'; 'versicolor'; 'versicolor'}); %!warning ... %! load fisheriris %! Mdl = ClassificationECOC (meas, species, 'Learners', 'knn', ... %! 'Prior', [0.5, 0.5, 0]); %! assert_equal (predict (Mdl, meas([1, 51, 101], :)), ... %! {'setosa'; 'versicolor'; 'versicolor'}); ## Test a 'Cost' given as a structure, permuted by name to the model's order, ## as MATLAB R2024a permutes it %!test %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! S = struct ('ClassNames', {{'virginica', 'versicolor'}}, ... %! 'ClassificationCosts', [0, 2; 1, 0]); %! Mdl = ClassificationECOC (X, Y, 'Cost', S); %! assert_equal (Mdl.Cost, [0, 1; 2, 0]); %! assert_equal (Mdl.Prior, [0.5, 0.5]); %!test %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! S = struct ('ClassNames', {{'virginica', 'versicolor'}}, ... %! 'ClassificationCosts', [0, 2; 1, 0]); %! M1 = ClassificationECOC (X, Y, 'Cost', S); %! M2 = ClassificationECOC (X, Y, 'Cost', [0, 1; 2, 0]); %! assert_equal (predict (M1, X), predict (M2, X)); %!error ... %! load fisheriris %! ClassificationECOC (meas(51:150, 3:4), species(51:150), ... %! 'Cost', struct ('a', 1)); ## MATLAB parity: a row missing some predictors is kept for the binary ## learners to take by their own rules, and a row missing every predictor, of ## weight zero, or of missing class is dropped, as measured on R2024a %!shared ecX, ecL, ecQ %! x1 = mod ((1:30)', 7) + (1:30)' / 10; %! x2 = mod ((1:30)', 5) * 2 - (1:30)' / 20; %! ecX = [x1, x2]; %! ecX(2,1) = NaN; %! ecX(9,2) = NaN; %! ecX(20,:) = NaN; %! ecL = repmat ({'p'; 'q'; 'r'; 'p'; 'q'; 'r'}, 5, 1); %! ecQ = [3, 1; 5, 4; 7, 2; NaN, 3]; %!test # only the row missing every predictor is dropped %! Mdl = fitcecoc (ecX, ecL); %! assert_equal (Mdl.NumObservations, 29); %! assert_equal (find (! Mdl.RowsUsed)', 20); %! assert_equal (rows (Mdl.X), 29); %!test # tree learners take the rows missing some predictors %! [label, score] = predict (fitcecoc (ecX, ecL, 'Learners', 'tree'), ecQ); %! assert_equal (label, {'p'; 'r'; 'r'; 'r'}); %! assert_equal (score, [-0.183673469387755, -2, -0.326530612244898; ... %! -1.00694444444444, -0.555555555555556, ... %! -0.173611111111111; ... %! -1.00694444444444, -0.555555555555556, ... %! -0.173611111111111; ... %! -0.530276816608997, -0.498461597446539, ... %! -0.474376731301939], 1e-13); %!test # and so do naive Bayes learners %! [label, score] = predict (fitcecoc (ecX, ecL, 'Learners', ... %! 'naivebayes'), ecQ); %! assert_equal (label, {'p'; 'q'; 'r'; 'r'}); %! assert_equal (score, [-0.448479707551261, -0.580029631746784, ... %! -0.477722073040902; ... %! -0.588600916657713, -0.433272656977071, ... %! -0.486316971236823; ... %! -0.625414076165671, -0.661745551079288, ... %! -0.277969200170033; ... %! -0.509550065393847, -0.513266763880489, ... %! -0.477696597473307], 1e-13); %!test # a row of weight zero is dropped %! Mdl = fitcecoc (ecX, ecL, 'Weights', [0; ones(29, 1)]); %! assert_equal (Mdl.NumObservations, 28); %! assert_equal (Mdl.Prior, [9, 9, 10] / 28, 1e-15); statistics-release-2.1.2/inst/Machine_Learning/ClassificationEnsemble.m000066400000000000000000002606251527311104300262600ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ClassificationEnsemble < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} ClassificationEnsemble ## ## Boosted ensemble of decision trees for classification ## ## A @code{ClassificationEnsemble} object holds the weak learners a ## boosting method grew one after another, each on the observations ## reweighted by the errors of those before it, together with the data it ## was fitted on. AdaBoostM1, GentleBoost and LogitBoost fit two classes, ## AdaBoostM2 more than two. ## ## Create one with @code{fitcensemble}. A bagged ensemble is a ## @code{ClassificationBaggedEnsemble}, and @code{compact} returns a ## @code{CompactClassificationEnsemble} without the data. ## ## @seealso{fitcensemble, CompactClassificationEnsemble, ## ClassificationBaggedEnsemble} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} X ## ## Predictor data ## ## The predictors the ensemble was fitted on, one row per observation, a ## row missing a value or a class having been left out. This property is ## read-only. ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} Y ## ## Class labels ## ## The labels the ensemble was fitted on, in the type they were given in. ## This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} RowsUsed ## ## Rows of the data that were used ## ## A logical column over the rows as supplied. This property is ## read-only. ## ## @end deftp RowsUsed = []; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} W ## ## Observation weights ## ## The weights given, scaled so that the observations of each class sum to ## its prior probability. A cost matrix is not folded into them. It has ## the class of the @qcode{'Weights'} given, single or double. This ## property is read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} ModelParameters ## ## Parameters of the fit ## ## A structure with the fields @qcode{Type}, @qcode{Method}, ## @qcode{LearnerTemplates}, the tree template the learners were grown ## from, @qcode{NLearn}, the number of learning cycles asked for in all, ## and @qcode{LearnRate}. This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} NumObservations ## ## Number of observations used ## ## This property is read-only. ## ## @end deftp NumObservations = 0; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} BinEdges ## ## Bin edges of the predictors ## ## Always empty, binning not being implemented. This property is ## read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} HyperparameterOptimizationResults ## ## Results of optimizing the hyperparameters ## ## Always empty, such optimization not being implemented. This property ## is read-only. ## ## @end deftp HyperparameterOptimizationResults = []; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} CategoricalPredictors ## ## Indices of categorical predictors ## ## The predictors every tree treats as categorical, empty when none ## is. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} ResponseName ## ## Name of the response variable ## ## This property is read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} ExpandedPredictorNames ## ## Names of the predictors as the learners saw them ## ## The same as @code{PredictorNames}. This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} ClassNames ## ## Names of the classes ## ## The classes, in the type of the response, sorted or in the order given ## by the @qcode{'ClassNames'} option; the columns of the scores take them ## in that order. This property is read-only. ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} Prior ## ## Prior probabilities of the classes ## ## A row vector with one probability per class. This property is ## read-only. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} Cost ## ## Misclassification costs ## ## A square matrix, @code{Cost(i,j)} being the cost of classifying an ## observation of class @math{i} as class @math{j}. A boosting method ## starts from observation weights multiplied by the total cost of ## misclassifying each observation's class. This property is read-only. ## ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} Method ## ## Ensemble method ## ## @qcode{'AdaBoostM1'}, @qcode{'AdaBoostM2'}, @qcode{'GentleBoost'}, ## @qcode{'LogitBoost'}, @qcode{'RUSBoost'}, @qcode{'LPBoost'}, ## @qcode{'TotalBoost'}, @qcode{'Bag'} or @qcode{'Subspace'}. This ## property is read-only. ## ## @end deftp Method = ''; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} LearnerNames ## ## Names of the weak learners ## ## @code{@{'Tree'@}}, or for Subspace @code{@{'KNN'@}} or ## @code{@{'Discriminant'@}}. This property is read-only. ## ## @end deftp LearnerNames = {'Tree'}; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} ReasonForTermination ## ## Why the fit stopped adding learners ## ## A character vector. This property is read-only. ## ## @end deftp ReasonForTermination = ''; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} FitInfo ## ## Fit information ## ## A column with one element per learner: the weighted classification error ## for AdaBoostM1, the weighted pseudo-loss for AdaBoostM2 and RUSBoost, and ## the weighted mean squared error of the regression tree for GentleBoost ## and LogitBoost. For LPBoost and TotalBoost a matrix with a row per ## learner: its margin on each observation, and its edge last. Empty for ## Bag. This property is read-only. ## ## @end deftp FitInfo = []; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} FitInfoDescription ## ## Description of @code{FitInfo} ## ## This property is read-only. ## ## @end deftp FitInfoDescription = {}; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} UsePredForLearner ## ## Which predictors each learner uses ## ## For the @qcode{'Subspace'} method, a logical matrix with one row per ## predictor and one column per learner. Empty for tree learners, as ## MATLAB returns it. This property is read-only. ## ## @end deftp UsePredForLearner = []; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} NumTrained ## ## Number of trained weak learners ## ## This property is read-only. ## ## @end deftp NumTrained = 0; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} Trained ## ## Trained weak learners ## ## A column cell array of compact trees, as described under ## @code{CompactClassificationEnsemble.Trained}. This property is ## read-only. ## ## @end deftp Trained = {}; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} TrainedWeights ## ## Weights of the trained weak learners ## ## A column with one weight per learner. This property is read-only. ## ## @end deftp TrainedWeights = []; ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} CombineWeights ## ## How the weak learners are combined ## ## @qcode{'WeightedSum'} for boosting, @qcode{'WeightedAverage'} for ## Bag. This property is read-only. ## ## @end deftp CombineWeights = 'WeightedSum'; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {ClassificationEnsemble} {property} ScoreTransform ## ## Transform applied to the predicted scores ## ## See @code{CompactClassificationEnsemble.ScoreTransform}. ## ## @end deftp ScoreTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) STfun = @(x) x; # the score transform as a function DefaultIndex = 1; # class given to a row no learner may score gY = []; # class index of each observation LearnRate = 1; # shrinkage of the boosting methods TreeArgs = {}; # the options given in the tree template State = []; # what boosting carries from one learner to the next Stopped = false; # whether a learner ended the fit BagFResample = 1; # share of the observations each bag draws BagReplace = true; # whether the bags draw with replacement BagInBag = []; # NxNumTrained logical, the rows each bag drew NPredToSample = []; # predictors each Subspace learner is fitted on AllCombinations = false; # whether Subspace takes every combination RatioToSmallest = []; # RUSBoost's class sample sizes over the smallest Resampling = false; # whether a boosting method resamples its rows MarginPrecision = 0.01; # LPBoost's and TotalBoost's edge tolerance endproperties methods (Hidden) function this = set.ScoreTransform (this, val) try [this.STfun, this.ScoreTransform] = parseScoreTransform (val, ... 'ClassificationEnsemble'); catch error (strcat ("ClassificationEnsemble.subsasgn: 'ScoreTransform'", ... " must be a character vector or a", ... " 'function_handle' object.")); end_try_catch endfunction function disp (this) fprintf ("\n %s\n\n", class (this)); fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); fprintf ("%+25s: %s\n", 'CategoricalPredictors', ... mat2str (this.CategoricalPredictors)); fprintf ("%+25s: %s\n", 'ClassNames', classNameListing (this.ClassNames)); fprintf ("%+25s: '%s'\n", 'ScoreTransform', this.ScoreTransform); fprintf ("%+25s: %d\n", 'NumObservations', this.NumObservations); fprintf ("%+25s: %d\n", 'NumTrained', this.NumTrained); fprintf ("%+25s: '%s'\n", 'Method', this.Method); fprintf ("%+25s: '%s'\n", 'ReasonForTermination', ... this.ReasonForTermination); fprintf ("%+25s: [%dx%d double]\n", 'FitInfo', rows (this.FitInfo), ... columns (this.FitInfo)); if (isa (this, 'ClassificationBaggedEnsemble')) fprintf ("%+25s: %g\n", 'FResample', this.FResample); fprintf ("%+25s: %d\n", 'Replace', this.Replace); fprintf ("%+25s: [%dx%d logical]\n", 'UseObsForLearner', ... rows (this.UseObsForLearner), ... columns (this.UseObsForLearner)); endif fprintf ("\n"); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {ClassificationEnsemble} {@var{obj} =} ClassificationEnsemble (@var{X}, @var{Y}) ## @deftypefnx {ClassificationEnsemble} {@var{obj} =} ClassificationEnsemble (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationEnsemble} {@var{obj} =} ClassificationEnsemble (@var{Tbl}, @var{formula}) ## @deftypefnx {ClassificationEnsemble} {@var{obj} =} ClassificationEnsemble (@var{Tbl}, @var{Y}) ## @deftypefnx {ClassificationEnsemble} {@var{obj} =} ClassificationEnsemble (@dots{}, @var{name}, @var{value}) ## ## Fit a boosted ensemble of decision trees. ## ## @code{fitcensemble} is the documented way in, and its help lists the ## options both take. A bagged ensemble is fitted by ## @code{ClassificationBaggedEnsemble}. ## ## @seealso{fitcensemble, ClassificationBaggedEnsemble} ## @end deftypefn function this = ClassificationEnsemble (X, Y, varargin) if (nargin < 2) error ("ClassificationEnsemble: too few input arguments."); endif ## A table names its own predictors and says which hold levels ## An ordinal column is a numeric predictor here, as in R2024a [this, X, Y, varargin] = resolveTable (this, 'ClassificationEnsemble', ... X, Y, varargin, true); if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationEnsemble: name-value arguments must", ... " be in pairs.")); endif bagged = isa (this, 'ClassificationBaggedEnsemble'); if (bagged) caller = 'ClassificationBaggedEnsemble'; else caller = 'ClassificationEnsemble'; endif ## Parse optional paired arguments. An empty default stands for one ## resolved once the data are known: 'Method' is 'LogitBoost' for two ## classes and 'AdaBoostM2' for more, 'NumLearningCycles' is 100, the ## learners are trees (nearest neighbours for 'Subspace'), the classes, ## prior, cost and weights come from the response, and 'Resample' is ## off. 'NPredToSample', 'LearnRate', 'FResample', 'Replace', ## 'RatioToSmallest' and 'MarginPrecision' stay empty when not given, ## which is how the checks below tell them apart from a value. optNames = {'Method', 'NumLearningCycles', 'NPredToSample', ... 'Learners', 'LearnRate', 'NPrint', 'ClassNames', 'Cost', ... 'Prior', 'Weights', 'PredictorNames', 'ResponseName', ... 'ScoreTransform', 'FResample', 'Replace', 'Resample', ... 'CategoricalPredictors', 'RatioToSmallest', ... 'MarginPrecision'}; dfValues = {'', [], [], [], [], 'off', [], [], [], [], {}, 'Y', ... 'none', [], [], [], [], [], []}; [Method, NumCycles, NPred, Learners, LearnRate, NPrint, ClassNames, ... Cost, Prior, Weights, PredictorNames, ResponseName, ScoreTransform, ... FResample, Replace, Resample, CatPreds, Ratio, MarginPrecision, ... args] = parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments NLearn = 100; AllCombinations = false; if (ischar (NumCycles) && strcmpi (NumCycles, 'AllPredictorCombinations')) AllCombinations = true; elseif (isnumeric (NumCycles) && isscalar (NumCycles) && isreal (NumCycles) && NumCycles >= 1 && NumCycles == fix (NumCycles)) NLearn = double (NumCycles); elseif (! isempty (NumCycles)) error (strcat ("%s: 'NumLearningCycles' must be a positive", ... " integer or 'AllPredictorCombinations'."), caller); endif if (! isempty (NPred) && ! (isnumeric (NPred) && isscalar (NPred) && isreal (NPred) && NPred >= 1 && NPred == fix (NPred))) error ("%s: 'NPredToSample' must be a positive integer.", caller); endif NPred = double (NPred); if (! isempty (LearnRate) && ! (isnumeric (LearnRate) && isscalar (LearnRate) && isreal (LearnRate) && LearnRate > 0 && LearnRate <= 1)) error (strcat ("%s: 'LearnRate' must be a number greater than 0", ... " and no greater than 1."), caller); endif LearnRate = double (LearnRate); if (ischar (NPrint) && strcmpi (NPrint, 'off')) NPrint = 0; elseif (isnumeric (NPrint) && isscalar (NPrint) && isreal (NPrint) && NPrint >= 1 && NPrint == fix (NPrint)) NPrint = double (NPrint); else error ("%s: 'NPrint' must be a positive integer or 'off'.", caller); endif if (! (ischar (ResponseName) && isrow (ResponseName))) error ("%s: 'ResponseName' must be a character vector.", caller); endif if (! isempty (FResample) && ! (isnumeric (FResample) && isscalar (FResample) && isreal (FResample) && FResample > 0 && FResample <= 1)) error (strcat ("%s: 'FResample' must be a number greater than 0", ... " and no greater than 1."), caller); endif FResample = double (FResample); if (! isempty (Replace)) [Replace, ok] = onOff (Replace); if (! ok) error ("%s: 'Replace' must be 'on' or 'off'.", caller); endif endif if (isempty (Resample)) Resample = false; else [Resample, ok] = onOff (Resample); if (! ok) error ("%s: 'Resample' must be 'on' or 'off'.", caller); endif endif if (! isempty (Ratio) && ! (isnumeric (Ratio) && isvector (Ratio) && isreal (Ratio) && all (isfinite (Ratio)) && all (Ratio >= 0) && any (Ratio > 0))) error (strcat ("%s: 'RatioToSmallest' must be a vector of", ... " nonnegative numbers with a positive element."), ... caller); endif Ratio = double (Ratio(:)'); if (! isempty (MarginPrecision) && ! (isnumeric (MarginPrecision) && isscalar (MarginPrecision) && isreal (MarginPrecision) && MarginPrecision >= 0 && MarginPrecision <= 1)) error ("%s: 'MarginPrecision' must be a number from 0 to 1.", caller); endif MarginPrecision = double (MarginPrecision); ## Options MATLAB takes that this class does not implement are named ## one by one, so that asking for one is refused rather than quietly ## doing nothing; anything else left over is unknown. notImpl = {'RobustErrorGoal', 'RobustMaxMargin', 'RobustMarginSigma', ... 'NumBins', 'OptimizeHyperparameters', ... 'HyperparameterOptimizationOptions', 'Options'}; for i = 1:2:numel (args) if (ischar (args{i}) && any (strcmpi (args{i}, notImpl))) error ("%s: '%s' is not implemented.", caller, args{i}); endif endfor if (! isempty (args)) error ("%s: invalid optional paired argument.", caller); endif ## A row missing some predictors is kept, its learners taking the ## missing values, as in R2024a F = classFrame (X, Y, ClassNames, Prior, Cost, Weights, caller, false, ... 'refuse', 'partial'); K = classCount (F.ClassNames); ## The method, by default the one MATLAB chooses for the class count. methods2 = {'AdaBoostM1', 'GentleBoost', 'LogitBoost'}; known = [methods2, {'AdaBoostM2', 'RUSBoost', 'LPBoost', ... 'TotalBoost', 'Bag', 'Subspace'}]; later = {'RobustBoost'}; if (isempty (Method)) if (K == 2) Method = 'LogitBoost'; else Method = 'AdaBoostM2'; endif elseif (! (ischar (Method) && isrow (Method))) error ("%s: 'Method' must be a character vector.", caller); elseif (any (strcmpi (Method, known))) Method = known{strcmpi (Method, known)}; elseif (any (strcmpi (Method, later))) error ("%s: the '%s' method is not implemented.", caller, ... later{strcmpi (Method, later)}); else error ("%s: '%s' is not a valid ensemble method.", caller, Method); endif isbag = strcmp (Method, 'Bag'); issub = strcmp (Method, 'Subspace'); resampled = Resample || ! isempty (FResample) || ! isempty (Replace); ismargin = any (strcmp (Method, {'LPBoost', 'TotalBoost'})); if (resampled && strcmp (Method, 'RUSBoost')) error (strcat ("%s: the 'RUSBoost' method cannot resample the", ... " observations."), caller); elseif (resampled && ismargin) error (strcat ("%s: resampling with the '%s' method is not", ... " implemented."), caller, Method); elseif (resampled && issub) error (strcat ("%s: the 'Subspace' method cannot resample the", ... " observations."), caller); endif if (isbag && ! bagged) error (strcat ("ClassificationEnsemble: a bagged ensemble is", ... " fitted by ClassificationBaggedEnsemble.")); elseif (! isbag && bagged && ! resampled) error (strcat ("ClassificationBaggedEnsemble: 'Method' must be", ... " 'Bag' unless the ensemble resamples.")); elseif (! isbag && ! bagged && resampled) error (strcat ("ClassificationEnsemble: a resampled ensemble is", ... " fitted by ClassificationBaggedEnsemble.")); endif if (any (strcmp (Method, methods2)) && K != 2) error ("%s: the '%s' method fits exactly two classes.", caller, Method); elseif (strcmp (Method, 'AdaBoostM2') && K < 3) error (strcat ("%s: the 'AdaBoostM2' method fits more than two", ... " classes."), caller); endif ## The learners, by name or as a template: trees for every method but ## Subspace, which takes nearest neighbours or discriminants instead. if (isempty (Learners)) Learners = ifelse_learner (issub); endif if (ischar (Learners) && isrow (Learners)) names = {'tree', 'knn', 'discriminant'}; makers = {@templateTree, @templateKNN, @templateDiscriminant}; k = find (strcmpi (Learners, names)); if (isempty (k)) tmpl = []; else tmpl = makers{k} (); endif elseif (isstruct (Learners) && isscalar (Learners) && isfield (Learners, 'Method')) tmpl = Learners; else tmpl = []; endif if (issub) if (isempty (tmpl) || ! any (strcmpi (tmpl.Method, ... {'KNN', 'Discriminant', 'Tree'}))) error (strcat ("%s: 'Learners' must be 'knn', 'discriminant' or", ... " a template of either for the 'Subspace'", ... " method."), caller); elseif (strcmpi (tmpl.Method, 'Tree')) error (strcat ("%s: trees cannot be the learners of the", ... " 'Subspace' method."), caller); endif elseif (isempty (tmpl) || ! strcmpi (tmpl.Method, 'Tree')) error ("%s: 'Learners' must be 'tree' or a tree template.", caller); endif TreeArgs = {}; for [val, key] = tmpl if (! any (strcmp (key, {'Method', 'Type'}))) TreeArgs(end+1:end+2) = {key, val}; endif endfor if (! issub && ! isempty (NPred)) error ("%s: 'NPredToSample' applies only to the 'Subspace' method.", ... caller); endif if (! issub && AllCombinations) error (strcat ("%s: 'AllPredictorCombinations' applies only to the", ... " 'Subspace' method."), caller); endif if (strcmp (Method, 'RUSBoost')) if (isempty (Ratio)) Ratio = ones (1, K); elseif (isscalar (Ratio)) Ratio = repmat (Ratio, 1, K); elseif (numel (Ratio) != K) error ("%s: 'RatioToSmallest' must have one element per class.", ... caller); endif elseif (! isempty (Ratio)) error (strcat ("%s: 'RatioToSmallest' applies only to the", ... " 'RUSBoost' method."), caller); endif if (ismargin) if (! isempty (LearnRate)) error ("%s: 'LearnRate' cannot be used with the '%s' method.", ... caller, Method); endif if (exist ('__glpk__') == 0) error (strcat ("%s: the '%s' method needs GLPK, which this", ... " Octave was built without."), caller, Method); endif if (isempty (MarginPrecision)) MarginPrecision = 0.01; endif elseif (! isempty (MarginPrecision)) error (strcat ("%s: 'MarginPrecision' applies only to the", ... " 'LPBoost' and 'TotalBoost' methods."), caller); endif if (issub) if (! isempty (LearnRate)) error (strcat ("%s: 'LearnRate' cannot be used with the", ... " 'Subspace' method."), caller); endif elseif (isbag) if (! isempty (LearnRate)) error ("%s: 'LearnRate' cannot be used with the 'Bag' method.", ... caller); endif if (isempty (FResample)) FResample = 1; endif if (isempty (Replace)) Replace = true; endif else if (resampled && isempty (FResample)) FResample = 1; endif if (resampled && isempty (Replace)) Replace = true; endif if (isempty (LearnRate)) LearnRate = 1; endif endif if (isempty (PredictorNames)) PredictorNames = arrayfun (@(k) sprintf ('x%d', k), 1:F.p, ... 'UniformOutput', false); elseif (! (iscellstr (PredictorNames) && numel (PredictorNames) == F.p)) error (strcat ("%s: 'PredictorNames' must be a cell array of", ... " character vectors with one element per column", ... " of X."), caller); endif ## Categorical predictors reach every tree, and no other learner [Cod, errmsg] = dummyCoding (F.X, CatPreds, PredictorNames); if (! isempty (errmsg)) error ("%s: %s", caller, errmsg); endif CatIdx = []; if (! isempty (Cod.Index)) if (issub) error (strcat ("%s: 'CategoricalPredictors' cannot be used with", ... " the 'Subspace' method."), caller); endif CatIdx = Cod.Index; TreeArgs = [{'CategoricalPredictors', CatIdx}, TreeArgs]; endif this.X = F.X; this.Y = F.Y; this.RowsUsed = F.RowsUsed; this.NumObservations = F.n; this.gY = F.gY(:); this.ClassNames = F.ClassNames; this.Prior = F.Prior; [this.Cost, errmsg] = costMatrix (F.Cost, F.ClassNames); if (! isempty (errmsg)) error ("%s: %s", caller, errmsg); endif ## The weights keep their class in the model; every computation runs on ## them as double. W = priorNormalize (F.Weights, this.gY, this.Prior); this.W = cast (W / sum (W), F.WeightsClass); [~, this.DefaultIndex] = max (this.Prior); if (issub) ## The nearest neighbour and discriminant learners take no weights, ## so weights that are not uniform cannot reach them. if (max (F.Weights) - min (F.Weights) > 1e-12 * max (F.Weights)) error (strcat ("%s: 'Weights' that are not uniform cannot be", ... " used with the 'Subspace' method."), caller); endif if (isempty (NPred)) NPred = 1; endif if (NPred >= F.p) error (strcat ("%s: 'NPredToSample' must be less than the number", ... " of predictors."), caller); endif if (AllCombinations) NLearn = nchoosek (F.p, NPred); endif endif this.PredictorNames = PredictorNames(:)'; this.ExpandedPredictorNames = this.PredictorNames; this.ResponseName = ResponseName; this.ScoreTransform = ScoreTransform; this.Method = Method; this.TreeArgs = TreeArgs; this.CategoricalPredictors = CatIdx; this.Trained = cell (0, 1); this.TrainedWeights = zeros (0, 1); this.ModelParameters = struct ('Type', 'classification', ... 'Method', Method, ... 'LearnerTemplates', tmpl, ... 'NLearn', 0); if (issub) this.CombineWeights = 'WeightedAverage'; this.FitInfo = []; this.FitInfoDescription = 'None'; this.LearnerNames = {tmpl.Method}; this.NPredToSample = NPred; this.AllCombinations = AllCombinations; this.UsePredForLearner = false (F.p, 0); elseif (isbag) this.CombineWeights = 'WeightedAverage'; this.FitInfo = []; this.FitInfoDescription = 'None'; this.BagFResample = FResample; this.BagReplace = Replace; this.BagInBag = false (F.n, 0); else this.LearnRate = LearnRate; this.RatioToSmallest = Ratio; this.Resampling = resampled; if (resampled) this.BagFResample = FResample; this.BagReplace = Replace; this.BagInBag = false (F.n, 0); endif if (ismargin) this.MarginPrecision = MarginPrecision; this.ModelParameters.MarginPrecision = MarginPrecision; this.FitInfo = zeros (0, F.n + 1); else this.ModelParameters.LearnRate = LearnRate; this.FitInfo = zeros (0, 1); endif this.FitInfoDescription = ClassificationEnsemble.fitInfoText (Method); endif this = growLearners (this, NLearn, NPrint, caller); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationEnsemble} {@var{CVMdl} =} crossval (@var{obj}) ## @deftypefnx {ClassificationEnsemble} {@var{CVMdl} =} crossval (@dots{}, @var{name}, @var{value}) ## ## Cross-validate an ensemble. ## ## @code{@var{CVMdl} = crossval (@var{obj})} refits the ensemble on the ## training part of each of ten folds, stratified by class, and returns a ## @code{ClassificationPartitionedEnsemble}. One of @qcode{'KFold'}, an ## integer greater than 1, @qcode{'Holdout'}, a number between 0 and 1, ## @qcode{'Leaveout'}, @qcode{'on'} for one fold per observation, or ## @qcode{'CVPartition'}, a @code{cvpartition} object, may choose the ## partition instead. ## ## @seealso{ClassificationEnsemble, ClassificationPartitionedEnsemble, ## cvpartition} ## @end deftypefn function CVMdl = crossval (this, varargin) [P, errmsg] = ensemblePartition (varargin, this.Y, ... this.NumObservations, ... true); if (! isempty (errmsg)) error ("%s.crossval: %s", class (this), errmsg); endif if (isempty (P)) error (strcat ("%s.crossval: no partition was asked for; set", ... " 'CrossVal' to 'on' or give 'KFold'."), class (this)); endif CVMdl = ClassificationPartitionedEnsemble (this, P); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationEnsemble} {@var{CMdl} =} compact (@var{obj}) ## ## Drop the training data from an ensemble. ## ## @code{@var{CMdl} = compact (@var{obj})} returns a ## @code{CompactClassificationEnsemble} holding the learners and what ## prediction needs. It predicts new data identically. ## ## @seealso{ClassificationEnsemble, CompactClassificationEnsemble} ## @end deftypefn function CMdl = compact (this) CMdl = CompactClassificationEnsemble (this); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationEnsemble} {@var{B} =} resume (@var{obj}, @var{NumLearningCycles}) ## @deftypefnx {ClassificationEnsemble} {@var{B} =} resume (@dots{}, 'NPrint', @var{n}) ## ## Grow more weak learners. ## ## @var{B} is the ensemble with up to @var{NumLearningCycles} further ## learners grown as though the fit had asked for them from the start: the ## boosting weights carry on from where it stopped. An ensemble whose fit ## ended on a learner that classified the data perfectly grows no more. ## @qcode{'NPrint'} is taken as by @code{fitcensemble}. ## ## MATLAB R2024a restarts the mislabel weights of AdaBoostM2 on a resume, ## so its learner weights then differ from those of one longer fit; here ## they are the same. ## ## @seealso{ClassificationEnsemble, fitcensemble} ## @end deftypefn function this = resume (this, NumLearningCycles, varargin) caller = sprintf ('%s.resume', class (this)); if (nargin < 2) error ("%s: too few input arguments.", caller); endif if (! (isnumeric (NumLearningCycles) && isscalar (NumLearningCycles) && isreal (NumLearningCycles) && NumLearningCycles >= 1 && NumLearningCycles == fix (NumLearningCycles))) error ("%s: NUMLEARNINGCYCLES must be a positive integer.", caller); endif if (mod (numel (varargin), 2) != 0) error ("%s: name-value arguments must be in pairs.", caller); endif if (this.AllCombinations) error (strcat ("%s: an ensemble of all predictor combinations", ... " cannot grow further."), caller); endif ## Parse optional paired arguments [NPrint, args] = parsePairedArguments ({'NPrint'}, {'off'}, ... varargin(:)); ## Validate optional paired arguments if (ischar (NPrint) && strcmpi (NPrint, 'off')) NPrint = 0; elseif (isnumeric (NPrint) && isscalar (NPrint) && isreal (NPrint) && NPrint >= 1 && NPrint == fix (NPrint)) NPrint = double (NPrint); else error ("%s: 'NPrint' must be a positive integer or 'off'.", caller); endif if (! isempty (args)) error ("%s: invalid optional paired argument.", caller); endif this = growLearners (this, double (NumLearningCycles), NPrint, caller); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationEnsemble} {@var{label} =} predict (@var{obj}, @var{X}) ## @deftypefnx {ClassificationEnsemble} {[@var{label}, @var{scores}] =} predict (@dots{}) ## @deftypefnx {ClassificationEnsemble} {[@dots{}] =} predict (@dots{}, @var{name}, @var{value}) ## ## Classify new data with an ensemble. ## ## Behaves as @code{CompactClassificationEnsemble.predict} and takes the ## same Name-Value arguments. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @seealso{ClassificationEnsemble, CompactClassificationEnsemble.predict} ## @end deftypefn function [label, scores] = predict (this, X, varargin) if (nargin < 2) error ("%s.predict: too few input arguments.", class (this)); endif ## A table is read by the names the model was fitted on X = tableColumns (this, 'ClassificationEnsemble.predict', X); [label, scores] = ensemblePredict (compact (this), X, varargin, ... [class(this), '.predict']); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationEnsemble} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationEnsemble} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationEnsemble} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {ClassificationEnsemble} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Classification loss of an ensemble. ## ## Behaves as @code{CompactClassificationEnsemble.loss} and takes the ## same Name-Value arguments. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{ClassificationEnsemble, CompactClassificationEnsemble.loss} ## @end deftypefn function L = loss (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("%s.loss: too few input arguments.", class (this)); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); L = ensembleLoss (compact (this), X, Y, varargin, ... [class(this), '.loss']); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationEnsemble} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationEnsemble} {@var{e} =} edge (@dots{}, @var{name}, @var{value}) ## @deftypefnx {ClassificationEnsemble} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationEnsemble} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Classification edge of an ensemble. ## ## Behaves as @code{CompactClassificationEnsemble.edge} and takes the ## same Name-Value arguments. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{ClassificationEnsemble, CompactClassificationEnsemble.edge} ## @end deftypefn function e = edge (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("%s.edge: too few input arguments.", class (this)); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'edge', X, Y, varargin, ... nargin > 2); e = ensembleEdge (compact (this), X, Y, varargin, ... [class(this), '.edge']); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationEnsemble} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationEnsemble} {@var{m} =} margin (@dots{}, @var{name}, @var{value}) ## @deftypefnx {ClassificationEnsemble} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationEnsemble} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margins of an ensemble. ## ## Behaves as @code{CompactClassificationEnsemble.margin} and takes the ## same Name-Value arguments. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{ClassificationEnsemble, CompactClassificationEnsemble.margin} ## @end deftypefn function m = margin (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("%s.margin: too few input arguments.", class (this)); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'margin', X, Y, ... varargin, nargin > 2); m = ensembleMargin (compact (this), X, Y, varargin, ... [class(this), '.margin']); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationEnsemble} {@var{imp} =} predictorImportance (@var{obj}) ## @deftypefnx {ClassificationEnsemble} {[@var{imp}, @var{ma}] =} predictorImportance (@var{obj}) ## ## Estimate the importance of each predictor. ## ## Behaves as @code{CompactClassificationEnsemble.predictorImportance}. ## ## @seealso{ClassificationEnsemble, ## CompactClassificationEnsemble.predictorImportance} ## @end deftypefn function [imp, ma] = predictorImportance (this) [imp, ma] = ensembleImportance (compact (this), ... [class(this), '.predictorImportance']); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationEnsemble} {@var{label} =} resubPredict (@var{obj}) ## @deftypefnx {ClassificationEnsemble} {[@var{label}, @var{scores}] =} resubPredict (@dots{}) ## @deftypefnx {ClassificationEnsemble} {[@dots{}] =} resubPredict (@dots{}, @var{name}, @var{value}) ## ## Classify the training data. ## ## @code{predict} on @code{X}, taking the same Name-Value arguments. ## ## @seealso{ClassificationEnsemble, ClassificationEnsemble.predict} ## @end deftypefn function [label, scores] = resubPredict (this, varargin) [label, scores] = ensemblePredict (compact (this), this.X, varargin, ... [class(this), '.resubPredict']); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationEnsemble} {@var{L} =} resubLoss (@var{obj}) ## @deftypefnx {ClassificationEnsemble} {@var{L} =} resubLoss (@dots{}, @var{name}, @var{value}) ## ## Classification loss on the training data. ## ## @code{loss} on @code{X} and @code{Y}, weighted by @code{W} unless ## @qcode{'Weights'} are given. ## ## @seealso{ClassificationEnsemble, ClassificationEnsemble.loss} ## @end deftypefn function L = resubLoss (this, varargin) L = ensembleLoss (compact (this), this.X, this.Y, ... [{'Weights', this.W}, varargin], ... [class(this), '.resubLoss']); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationEnsemble} {@var{e} =} resubEdge (@var{obj}) ## @deftypefnx {ClassificationEnsemble} {@var{e} =} resubEdge (@dots{}, @var{name}, @var{value}) ## ## Classification edge on the training data. ## ## @code{edge} on @code{X} and @code{Y}, weighted by @code{W} unless ## @qcode{'Weights'} are given. ## ## @seealso{ClassificationEnsemble, ClassificationEnsemble.edge} ## @end deftypefn function e = resubEdge (this, varargin) e = ensembleEdge (compact (this), this.X, this.Y, ... [{'Weights', this.W}, varargin], ... [class(this), '.resubEdge']); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationEnsemble} {@var{m} =} resubMargin (@var{obj}) ## @deftypefnx {ClassificationEnsemble} {@var{m} =} resubMargin (@dots{}, @var{name}, @var{value}) ## ## Classification margins of the training data. ## ## @code{margin} on @code{X} and @code{Y}, taking the same Name-Value ## arguments. ## ## @seealso{ClassificationEnsemble, ClassificationEnsemble.margin} ## @end deftypefn function m = resubMargin (this, varargin) m = ensembleMargin (compact (this), this.X, this.Y, varargin, ... [class(this), '.resubMargin']); endfunction endmethods methods (Access = protected) ## Grow up to N learners, carrying the boosting weights in State from one ## call to the next, so that a resume continues the fit exactly. function this = growLearners (this, N, NPrint, caller) n = this.NumObservations; K = classCount (this.ClassNames); g = this.gY; LR = this.LearnRate; this.ModelParameters.NLearn += N; if (NPrint > 0) printf ("Training %s...\n", this.Method); endif if (isempty (this.State) && ! any (strcmp (this.Method, ... {'Bag', 'Subspace'}))) csum = sum (this.Cost, 2); d0 = double (this.W) .* csum(g); d0 /= sum (d0); switch (this.Method) case 'AdaBoostM2' if (this.Resampling) this.State = struct ('d', d0); else D = repmat (d0 / (K - 1), 1, K); D(sub2ind ([n, K], (1:n)', g)) = 0; this.State = struct ('D', D); endif case 'LogitBoost' this.State = struct ('d0', d0, 'F', zeros (n, 1), 'w', d0); case 'LPBoost' this.State = struct ('d', d0, 'ghat', Inf, 'U', zeros (0, n)); case 'TotalBoost' this.State = struct ('d0', d0, 'd', d0, 'ghat', Inf, ... 'U', zeros (0, n)); otherwise this.State = struct ('d', d0); endswitch endif ctree = {'PredictorNames', this.PredictorNames, 'MaxNumSplits', 10, ... 'MinParentSize', 2, 'MinLeafSize', 1, 'Prune', 'off', ... 'MergeLeaves', 'off'}; rtree = {'PredictorNames', this.PredictorNames, 'MaxNumSplits', 10, ... 'MinParentSize', 10, 'MinLeafSize', 1, 'Prune', 'off', ... 'MergeLeaves', 'off'}; y = double (g == 1) - double (g == 2); rs = this.Resampling; m = ceil (this.BagFResample * n); ## The classes the trees are shown: a class removed for its prior of 0 ## has no observation left, and the trees' scores are mapped back to ## the ensemble's classes by name fitted = labelsFromIndex (this.ClassNames, unique (g)); for c = 1:N if (this.Stopped) break; endif switch (this.Method) case 'AdaBoostM1' d = this.State.d; if (rs) [idx, sw, cnt] = boostSample (d, m, this.BagReplace); present = labelsFromIndex (this.ClassNames, unique (g(idx))); T = compact (ClassificationTree (this.X(idx,:), ... this.Y(idx,:), ... 'Weights', sw, ... 'ClassNames', present, ... ctree{:}, this.TreeArgs{:})); else T = compact (ClassificationTree (this.X, this.Y, ... 'Weights', d, ... 'ClassNames', fitted, ... ctree{:}, this.TreeArgs{:})); endif gh = labelIndices (this.ClassNames, predict (T, this.X)); h = double (gh == 1) - double (gh == 2); if (rs) e = sum (sw .* (h(idx) != y(idx))); else e = sum (d .* (h != y)); endif if (e > 0.5) this.Stopped = true; this.ReasonForTermination = sprintf (strcat ("Classification", ... " error from the last weak learner is too high: err=%g"), e); break; endif a = LR * log ((1 - e) / max (e, eps)) / 2; this = addLearner (this, T, a, e); if (rs) this.BagInBag(:,end+1) = cnt > 0; endif if (e <= 0) this = stopPerfect (this, strcat ("Classification error from", ... " the last weak learner is", ... " zero.")); break; endif if (rs) this.State.d = boostRescale (d, d .* exp (-a * y .* h), cnt); else d = d .* exp (-a * y .* h); this.State.d = d / sum (d); endif case 'AdaBoostM2' tru = sub2ind ([n, K], (1:n)', g); if (rs) ## A resampled AdaBoostM2 keeps one weight per observation, ## as RUSBoost does, and as MATLAB R2024a does. d = this.State.d; [idx, sw, cnt] = boostSample (d, m, this.BagReplace); present = labelsFromIndex (this.ClassNames, unique (g(idx))); T = compact (ClassificationTree (this.X(idx,:), ... this.Y(idx,:), ... 'Weights', sw, ... 'ClassNames', present, ... ctree{:}, this.TreeArgs{:})); else D = this.State.D; T = compact (ClassificationTree (this.X, this.Y, ... 'Weights', sum (D, 2), ... 'ClassNames', fitted, ... ctree{:}, this.TreeArgs{:})); endif [~, s] = predict (T, this.X); P = zeros (n, K); P(:, labelIndices (this.ClassNames, T.ClassNames)) = s; hy = P(tru); if (rs) L = 1 - hy + P; L(tru) = 0; e = sum (sw .* sum (L(idx,:), 2)) / (2 * (K - 1)); else e = sum (sum (D .* (1 - hy + P))) / 2; endif if (e > 0.5) this.Stopped = true; this.ReasonForTermination = sprintf (strcat ("Pseudo-loss", ... " from the last weak learner is too high: err=%g"), e); break; endif a = LR * log ((1 - e) / max (e, eps)) / 2; this = addLearner (this, T, a, e); if (rs) this.BagInBag(:,end+1) = cnt > 0; endif if (e <= 0) this = stopPerfect (this, strcat ("Pseudo-loss from the last", ... " weak learner is zero.")); break; endif if (rs) E = exp (-a * (1 + hy - P)); E(tru) = 0; this.State.d = boostRescale (d, d .* sum (E, 2) / (K - 1), cnt); else D = D .* exp (-a * (1 + hy - P)); D(tru) = 0; this.State.D = D / sum (D(:)); endif case 'RUSBoost' d = this.State.d; idx = rusSample (g, d, this.RatioToSmallest); present = labelsFromIndex (this.ClassNames, unique (g(idx))); T = compact (ClassificationTree (this.X(idx,:), this.Y(idx,:), ... 'ClassNames', present, ... ctree{:}, this.TreeArgs{:})); [~, s] = predict (T, this.X); P = zeros (n, K); P(:, labelIndices (this.ClassNames, T.ClassNames)) = s; tru = sub2ind ([n, K], (1:n)', g); hy = P(tru); L = 1 - hy + P; L(tru) = 0; e = sum (d .* sum (L, 2)) / (2 * (K - 1)); if (e > 0.5) this.Stopped = true; this.ReasonForTermination = sprintf (strcat ("Pseudo-loss", ... " from the last weak learner is too high: err=%g"), e); break; endif a = LR * log ((1 - e) / max (e, eps)) / 2; this = addLearner (this, T, a, e); if (e <= 0) this = stopPerfect (this, strcat ("Pseudo-loss from the last", ... " weak learner is zero.")); break; endif E = exp (-a * (1 + hy - P)); E(tru) = 0; d = d .* sum (E, 2); this.State.d = d / sum (d); case 'GentleBoost' d = this.State.d; if (rs) [idx, sw, cnt] = boostSample (d, m, this.BagReplace); R = compact (RegressionTree (this.X(idx,:), y(idx), ... 'Weights', sw, rtree{:}, ... this.TreeArgs{:})); h = predict (R, this.X); this = addLearner (this, R, LR, ... sum (sw .* (y(idx) - h(idx)) .^ 2)); this.BagInBag(:,end+1) = cnt > 0; this.State.d = boostRescale (d, d .* exp (-LR * y .* h), cnt); else R = compact (RegressionTree (this.X, y, 'Weights', d, ... rtree{:}, this.TreeArgs{:})); h = predict (R, this.X); this = addLearner (this, R, LR, sum (d .* (y - h) .^ 2)); d = d .* exp (-LR * y .* h); this.State.d = d / sum (d); endif case 'LogitBoost' p = 1 ./ (1 + exp (-this.State.F)); p = min (max (p, eps), 1 - eps); z = (double (y > 0) - p) ./ (p .* (1 - p)); if (rs) ## Each row keeps its own score and weight; only the rows drawn ## move, as in MATLAB R2024a. w = this.State.w; [idx, sw, cnt] = boostSample (w, m, this.BagReplace); R = compact (RegressionTree (this.X(idx,:), z(idx), ... 'Weights', sw, rtree{:}, ... this.TreeArgs{:})); h = predict (R, this.X); this = addLearner (this, R, LR / 2, ... sum (sw .* (z(idx) - h(idx)) .^ 2)); this.BagInBag(:,end+1) = cnt > 0; u = cnt > 0; this.State.F(u) += LR / 2 * h(u); pu = 1 ./ (1 + exp (-this.State.F(u))); pu = min (max (pu, eps), 1 - eps); w1 = w; w1(u) = this.State.d0(u) .* pu .* (1 - pu); this.State.w = boostRescale (w, w1, cnt); else d = this.State.d0 .* p .* (1 - p); d /= sum (d); R = compact (RegressionTree (this.X, z, 'Weights', d, ... rtree{:}, this.TreeArgs{:})); h = predict (R, this.X); this = addLearner (this, R, LR / 2, sum (d .* (z - h) .^ 2)); this.State.F += LR / 2 * h; endif case 'LPBoost' ## As MATLAB R2024a grows LPBoost: the linear program over the ## trees with the new one gives the least largest edge; when the ## smallest edge is no more than MarginPrecision above it, the new ## tree is not kept and the fit stops. Otherwise the program gives ## the learner weights, and as its dual the next observation ## weights. d = this.State.d; T = compact (ClassificationTree (this.X, this.Y, 'Weights', d, ... 'ClassNames', this.ClassNames, ... ctree{:}, this.TreeArgs{:})); mg = treeMargins (T, this.X, this.ClassNames, g); e = mg * d; ghat = min (this.State.ghat, e); U = [this.State.U; mg]; [gamma, a, dn] = minMaxEdge (U); if (ghat - gamma <= this.MarginPrecision) this = stopPerfect (this, strcat ("No improvement in the", ... " last iteration.")); break; endif this.Trained{end+1,1} = T; this.TrainedWeights = a; this.FitInfo(end+1,:) = [mg, e]; this.NumTrained = numel (this.Trained); this.State.d = dn; this.State.ghat = ghat; this.State.U = U; case 'TotalBoost' ## As MATLAB R2024a grows TotalBoost: the tree's margins give its ## edge; the fit stops when the least largest edge over the trees ## kept rises above the smallest edge less MarginPrecision; the ## weights on the observations take one quadratic step towards ## the least relative entropy to the starting weights, and the ## learner weights maximise the smallest margin. d = this.State.d; T = compact (ClassificationTree (this.X, this.Y, 'Weights', d, ... 'ClassNames', this.ClassNames, ... ctree{:}, this.TreeArgs{:})); mg = treeMargins (T, this.X, this.ClassNames, g); e = mg * d; ghat = min (this.State.ghat, e); U = [this.State.U; mg]; [gamma, a] = minMaxEdge (U); if (gamma > ghat - this.MarginPrecision) this = stopPerfect (this, strcat ("No improvement in the", ... " last iteration.")); break; endif this.Trained{end+1,1} = T; this.TrainedWeights = a; this.FitInfo(end+1,:) = [mg, e]; this.NumTrained = numel (this.Trained); p = max (d, 1e-12); H = diag (1 ./ p); q = log (p ./ max (this.State.d0, 1e-12)) + 1 - H * p; dn = qp (d, H, q, ones (1, n), 1, zeros (n, 1), ones (n, 1), ... [], U, (ghat - this.MarginPrecision) * ones (rows (U), 1)); ## The solver may leave rounding below zero, which no tree takes. dn = max (dn, 0); this.State.d = dn / sum (dn); this.State.ghat = ghat; this.State.U = U; case 'Subspace' p = columns (this.X); if (this.AllCombinations) ## nchoosek's order; MATLAB R2024a reverses it for some sizes. combos = nchoosek (1:p, this.NPredToSample); if (this.NumTrained >= rows (combos)) break; endif cols = combos(this.NumTrained + 1,:); else perm = randperm (p); cols = sort (perm(1:this.NPredToSample)); endif largs = [{'PredictorNames', this.PredictorNames(cols), ... 'ClassNames', this.ClassNames, 'Weights', this.W, ... 'Cost', this.Cost}, this.TreeArgs]; if (strcmp (this.LearnerNames{1}, 'KNN')) mdl = ClassificationKNN (this.X(:,cols), this.Y, largs{:}); else mdl = compact (ClassificationDiscriminant (this.X(:,cols), ... this.Y, largs{:})); endif use = false (p, 1); use(cols) = true; this.UsePredForLearner(:,end+1) = use; this = addLearner (this, mdl, 1, []); case 'Bag' m = ceil (this.BagFResample * n); if (this.BagReplace) cw = [0; cumsum(double (this.W))]; cw /= cw(end); idx = lookup (cw, rand (m, 1)); else [~, order] = sort (rand (n, 1) .^ (1 ./ double (this.W)), ... 'descend'); idx = order(1:m); endif present = labelsFromIndex (this.ClassNames, unique (g(idx))); p = columns (this.X); T = compact (ClassificationTree (this.X(idx,:), this.Y(idx,:), ... 'ClassNames', present, ... 'PredictorNames', ... this.PredictorNames, ... 'MinParentSize', 2, ... 'MinLeafSize', 1, ... 'Prune', 'off', ... 'MergeLeaves', 'off', ... 'NumVariablesToSample', ... ceil (sqrt (p)), ... this.TreeArgs{:})); inbag = false (n, 1); inbag(idx) = true; this.BagInBag(:,end+1) = inbag; this = addLearner (this, T, 1, []); endswitch if (NPrint > 0 && mod (this.NumTrained, NPrint) == 0) printf ("Grown weak learners: %d\n", this.NumTrained); endif endfor if (! this.Stopped) this.ReasonForTermination = strcat ("Terminated normally after", ... " completing the requested", ... " number of training cycles."); endif endfunction function this = addLearner (this, mdl, a, info) this.Trained{end+1,1} = mdl; this.TrainedWeights(end+1,1) = a; if (! isempty (info)) this.FitInfo(end+1,1) = info; endif this.NumTrained = numel (this.Trained); endfunction function this = stopPerfect (this, reason) this.Stopped = true; this.ReasonForTermination = reason; endfunction endmethods methods (Static, Access = private) function txt = fitInfoText (Method) head = strcat ("Vector of length NumTrained, where NumTrained is the", ... " number of learned weak hypotheses."); switch (Method) case 'AdaBoostM1' tail = strcat ("Element t of this vector is the weighted", ... " classification error from hypothesis t."); case 'AdaBoostM2' tail = strcat ("Element t of this vector is the weighted", ... " pseudoloss from hypothesis t."); case 'RUSBoost' tail = strcat ("Element t of this vector is the weighted loss", ... " from hypothesis t."); case {'LPBoost', 'TotalBoost'} l1 = strcat ("Matrix of size NumTrained-by-(N+1), where N is the", ... " number of training observations"); l2 = strcat ("and NumTrained is the number of learned weak", ... " hypotheses."); l3 = strcat ("Row t of this matrix stores N classification", ... " margins from hypothesis t"); l4 = "and the weighted edge of hypothesis t in the last element."; txt = {l1; l2; l3; l4}; return; otherwise tail = strcat ("Element t of this vector is the weighted mean", ... " squared error from regression hypothesis t."); endswitch txt = {head; tail}; endfunction endmethods endclassdef ## The default learner: nearest neighbours for Subspace, trees otherwise. function name = ifelse_learner (issub) if (issub) name = 'knn'; else name = 'tree'; endif endfunction ## The rows of one RUSBoost tree: from class k, round (RATIO(k) times the size ## of the smallest class) rows drawn in proportion to the weights D, without ## replacement unless the class holds fewer rows than that. function idx = rusSample (g, d, ratio) K = numel (ratio); cnt = accumarray (g, 1, [K, 1]); nmin = min (cnt(cnt > 0)); idx = zeros (0, 1); for k = 1:K rk = find (g == k); m = round (ratio(k) * nmin); if (m == 0 || isempty (rk)) continue; endif w = d(rk); if (! any (w > 0)) w = ones (size (w)); endif if (m <= numel (rk)) [~, order] = sort (rand (numel (rk), 1) .^ (1 ./ w), 'descend'); idx = [idx; rk(order(1:m))]; else cw = [0; cumsum(w)]; cw /= cw(end); idx = [idx; rk(lookup (cw, rand (m, 1)))]; endif endfor idx = sort (idx); endfunction ## The margins of tree T on X, a row: the probability it gives each ## observation's class, of index G, less the largest it gives another. function mg = treeMargins (T, X, ClassNames, g) n = rows (X); K = classCount (ClassNames); [~, s] = predict (T, X); P = zeros (n, K); P(:, labelIndices (ClassNames, T.ClassNames)) = s; tru = sub2ind ([n, K], (1:n)', g); other = P; other(tru) = -Inf; mg = (P(tru) - max (other, [], 2))'; endfunction ## The least, over distributions D on the observations, of the largest edge ## U * D of the learners, one row of margins each, as GAMMA, with such a D, ## and from its dual the learner weights A that maximise the smallest margin, ## summing to one. function [gamma, a, d] = minMaxEdge (U) [T, n] = size (U); c = [zeros(n, 1); 1]; A = [U, -ones(T, 1); ones(1, n), 0]; b = [zeros(T, 1); 1]; [x, gamma, ~, extra] = glpk (c, A, b, [zeros(n, 1); -Inf], [], ... [repmat('U', 1, T), 'S'], ... repmat ('C', 1, n + 1), 1); a = max (-extra.lambda(1:T), 0) + 0; if (sum (a) > 0) a /= sum (a); else a = ones (T, 1) / T; endif d = max (x(1:n), 0); d /= sum (d); endfunction ## The weights W after a resampled learner: the rows drawn take their new ## weights W1, rescaled so that over their draws they carry what they carried ## before, and the whole is normalised. function w = boostRescale (w, w1, cnt) u = cnt > 0; s1 = sum (cnt(u) .* w1(u)); if (s1 > 0) w(u) = w1(u) * (sum (cnt(u) .* w(u)) / s1); endif w /= sum (w); endfunction ## 'on' or 'off' as a logical scalar, OK false for anything else. function [tf, ok] = onOff (val) ok = ischar (val) && any (strcmpi (val, {'on', 'off'})); tf = ok && strcmpi (val, 'on'); endfunction ## Test output %!shared X2, Y2, S %! load fisheriris %! X2 = meas(51:150,:); %! Y2 = species(51:150); %! S = templateTree ('MaxNumSplits', 1); %!test # MATLAB parity: the properties of a boosted ensemble %! Mdl = ClassificationEnsemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 2, 'Learners', S); %! assert_equal (numel (properties (Mdl)), 26); %! assert_equal (Mdl.NumObservations, 100); %! assert_equal (Mdl.LearnerNames, {'Tree'}); %! assert_equal (Mdl.UsePredForLearner, []); %! assert_equal (Mdl.ReasonForTermination, ["Terminated normally after ", ... %! "completing the requested number of training cycles."]); %! assert_equal (size (Mdl.FitInfoDescription), [2, 1]); %!test # MATLAB parity: the parameters of a boosted fit %! Mdl = ClassificationEnsemble (X2, Y2, 'Method', 'LogitBoost', ... %! 'NumLearningCycles', 2, 'Learners', S, ... %! 'LearnRate', 0.5); %! mp = Mdl.ModelParameters; %! assert_equal (mp.Type, 'classification'); %! assert_equal (mp.Method, 'LogitBoost'); %! assert_equal (mp.NLearn, 2); %! assert_equal (mp.LearnRate, 0.5); %! assert_equal (mp.LearnerTemplates.MaxNumSplits, 1); %!test # MATLAB parity: resuming continues the fit exactly %! for m = {'AdaBoostM1', 'GentleBoost', 'LogitBoost'} %! M5 = ClassificationEnsemble (X2, Y2, 'Method', m{1}, ... %! 'NumLearningCycles', 5, 'Learners', S, ... %! 'LearnRate', 0.5); %! M3 = ClassificationEnsemble (X2, Y2, 'Method', m{1}, ... %! 'NumLearningCycles', 3, 'Learners', S, ... %! 'LearnRate', 0.5); %! R = resume (M3, 2); %! assert_equal (R.NumTrained, 5); %! assert_equal (R.ModelParameters.NLearn, 5); %! assert_equal (R.TrainedWeights, M5.TrainedWeights, 1e-14); %! assert_equal (R.FitInfo, M5.FitInfo, 1e-14); %! endfor %!test # resuming AdaBoostM2 continues exactly, where MATLAB restarts %! load fisheriris %! M5 = ClassificationEnsemble (meas, species, 'Method', 'AdaBoostM2', ... %! 'NumLearningCycles', 5, 'Learners', S); %! R = resume (ClassificationEnsemble (meas, species, ... %! 'Method', 'AdaBoostM2', ... %! 'NumLearningCycles', 2, ... %! 'Learners', S), 3); %! assert_equal (R.TrainedWeights, M5.TrainedWeights, 1e-14); %!test # an ensemble ended by a perfect learner grows no more %! load fisheriris %! Mdl = ClassificationEnsemble (meas, strcmp (species, 'setosa'), ... %! 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 2, 'Learners', S); %! assert_equal (resume (Mdl, 3).NumTrained, 1); %!test # MATLAB parity: resubstitution with the training weights %! Mdl = ClassificationEnsemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 5, 'Learners', S); %! assert_equal (resubLoss (Mdl), 0.04, 1e-15); %! assert_equal (resubLoss (Mdl, 'LossFun', 'exponential'), ... %! 0.182079166822146, 1e-13); %! assert_equal (resubEdge (Mdl), 5.115272254726563, 1e-12); %! m = resubMargin (Mdl); %! assert_equal (m([1, 21]), [3.544074277136197; -1.958996348947703], 1e-12); %! assert_equal (resubPredict (Mdl), predict (Mdl, X2)); %!test # compact keeps the learners and drops the data %! Mdl = ClassificationEnsemble (X2, Y2, 'Method', 'GentleBoost', ... %! 'NumLearningCycles', 3, 'Learners', S); %! C = compact (Mdl); %! assert_equal (class (C), 'CompactClassificationEnsemble'); %! [l1, s1] = predict (Mdl, X2); %! [l2, s2] = predict (C, X2); %! assert_equal (l2, l1); %! assert_equal (s2, s1); %!test # the score transform can be set on a fitted ensemble %! Mdl = ClassificationEnsemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 5, 'Learners', S); %! Mdl.ScoreTransform = 'doublelogit'; %! [~, s] = predict (Mdl, X2(1,:)); %! assert_equal (s, [0.971916134164721, 0.028083865835279], 1e-13); ## Test input validation %!error ... %! ClassificationEnsemble (X2) %!error ... %! ClassificationEnsemble (X2, Y2, 'Method') %!error ... %! ClassificationEnsemble (X2, Y2, 'Foo', 1) %!error ... %! ClassificationEnsemble (X2, Y2, 1, 1) %!error ... %! ClassificationEnsemble (X2, Y2, 'NumLearningCycles', 0) %!error ... %! ClassificationEnsemble (X2, Y2, 'LearnRate', 2) %!error ... %! ClassificationEnsemble (X2, Y2, 'NPrint', 0) %!error ... %! ClassificationEnsemble (X2, Y2, 'ResponseName', 1) %!error ... %! ClassificationEnsemble (X2, Y2, 'FResample', 0) %!error ... %! ClassificationEnsemble (X2, Y2, 'Replace', 1) %!error ... %! ClassificationEnsemble (X2, Y2, 'Resample', 1) %!error ... %! ClassificationEnsemble (X2, Y2, 'Method', 'RUSBoost', ... %! 'RatioToSmallest', [0, 0]) %!error ... %! ClassificationEnsemble (X2, Y2, 'Method', 'RUSBoost', ... %! 'RatioToSmallest', [1, NaN]) %!error ... %! ClassificationEnsemble (X2, Y2, 'Method', 'RUSBoost', ... %! 'RatioToSmallest', [1, 2, 3]) %!error ... %! ClassificationEnsemble (X2, Y2, 'RatioToSmallest', 1) %!error ... %! ClassificationEnsemble (X2, Y2, 'Method', 'RUSBoost', 'Resample', 'on') %!error ... %! ClassificationEnsemble (X2, Y2, 'Learners', 'knn') %!error ... %! ClassificationEnsemble (X2, Y2, 'Method', 1) %!error ... %! ClassificationEnsemble (X2, Y2, 'Method', 'Boost') %!error ... %! ClassificationEnsemble (X2, Y2, 'Method', 'robustboost') %!error ... %! ClassificationEnsemble (X2, Y2, 'Method', 'Bag') %!error ... %! load fisheriris %! ClassificationEnsemble (meas, species, 'Method', 'AdaBoostM1') %!error ... %! ClassificationEnsemble (X2, Y2, 'Method', 'AdaBoostM2') %!error ... %! ClassificationEnsemble (X2, Y2, 'FResample', 0.5) %!error ... %! ClassificationEnsemble (X2, Y2, 'PredictorNames', {'a'}) %!error ... %! resume (ClassificationEnsemble (X2, Y2, 'NumLearningCycles', 1)) %!error ... %! resume (ClassificationEnsemble (X2, Y2, 'NumLearningCycles', 1), 0) %!error ... %! resume (ClassificationEnsemble (X2, Y2, 'NumLearningCycles', 1), 1, 'NPrint') %!error ... %! resume (ClassificationEnsemble (X2, Y2, 'NumLearningCycles', 1), 1, 'Foo', 1) %!error ... %! resume (ClassificationEnsemble (X2, Y2, 'NumLearningCycles', 1), 1, ... %! 'NPrint', 0) %!error ... %! predict (ClassificationEnsemble (X2, Y2, 'NumLearningCycles', 1)) %!error ... %! loss (ClassificationEnsemble (X2, Y2, 'NumLearningCycles', 1), X2) %!error ... %! edge (ClassificationEnsemble (X2, Y2, 'NumLearningCycles', 1), X2) %!error ... %! margin (ClassificationEnsemble (X2, Y2, 'NumLearningCycles', 1), X2) %!error ... %! resubLoss (ClassificationEnsemble (X2, Y2, 'NumLearningCycles', 1), 'Foo', 1) %!error ... %! Mdl = ClassificationEnsemble (X2, Y2, 'NumLearningCycles', 1); %! Mdl.ScoreTransform = 1; %!test # MATLAB parity: GentleBoost importance over its regression trees %! Mdl = ClassificationEnsemble (X2, Y2, 'Method', 'GentleBoost', ... %! 'NumLearningCycles', 3, 'Learners', S); %! assert_equal (predictorImportance (Mdl), ... %! [0, 0, 0.222096984079193, 0.388833770583033], 1e-13); %!test # MATLAB parity: crossval equals cross-validating at fit time %! load fisheriris %! c = cvpartition (Y2, 'KFold', 5); %! M = ClassificationEnsemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 4, 'Learners', S); %! CV = crossval (M, 'CVPartition', c); %! F = fitcensemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 4, 'Learners', S, 'CVPartition', c); %! assert_equal (kfoldLoss (CV), kfoldLoss (F), 1e-15); %! assert_equal (crossval (M).KFold, 10); %! assert_equal (crossval (M, 'KFold', 3).KFold, 3); %!error ... %! crossval (ClassificationEnsemble (X2, Y2, 'NumLearningCycles', 1), ... %! 'CrossVal', 'off') %!error ... %! crossval (ClassificationEnsemble (X2, Y2, 'NumLearningCycles', 1), 'Foo', 1) %!error ... %! load fisheriris %! ClassificationEnsemble (meas, species, 'Method', 'Subspace', ... %! 'Learners', 'tree') %!error ... %! load fisheriris %! ClassificationEnsemble (meas, species, 'Method', 'Subspace', ... %! 'Learners', 'svm') %!error ... %! ClassificationEnsemble (X2, Y2, 'NPredToSample', 0) %!error ... %! load fisheriris %! ClassificationEnsemble (meas, species, 'Method', 'Subspace', ... %! 'NPredToSample', 4) %!error ... %! ClassificationEnsemble (X2, Y2, 'NPredToSample', 2) %!error ... %! ClassificationEnsemble (X2, Y2, 'NumLearningCycles', ... %! 'AllPredictorCombinations') %!error ... %! load fisheriris %! ClassificationEnsemble (meas, species, 'Method', 'Subspace', ... %! 'LearnRate', 0.5) %!error ... %! load fisheriris %! ClassificationEnsemble (meas, species, 'Method', 'Subspace', ... %! 'FResample', 0.5) %!error ... %! load fisheriris %! ClassificationEnsemble (meas, species, 'Method', 'Subspace', ... %! 'Weights', [5 * ones(50, 1); ones(100, 1)]) %!error ... %! load fisheriris %! predictorImportance (ClassificationEnsemble (meas, species, ... %! 'Method', 'Subspace', 'NumLearningCycles', 2)) %!error ... %! load fisheriris %! resume (ClassificationEnsemble (meas, species, 'Method', 'Subspace', ... %! 'NumLearningCycles', 'AllPredictorCombinations'), 1) %!shared X, yb %! k = (0:119)'; %! c = mod (k, 4) + 1; %! j = floor (k / 4); %! x2 = mod (k * 7, 10); %! X = [c, x2]; %! yb = (c == 1) | (c == 2 & mod (j, 4) != 0) | (c == 3 & mod (j, 4) == 0) ... %! | (x2 > 7); %! yr = [3; 1; 4; 1.5]; %! yr = yr(c) + 0.1 * sin (k) + 0.2 * x2; %! Xq = [1, 0; 3, 5; 5, 0; NaN, 2; 2.5, 9]; %!test # the categorical predictors travel to compact models and folds %! Mdl = ClassificationEnsemble (X, yb, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 3, ... %! 'CategoricalPredictors', logical ([1, 0])); %! assert_equal (compact (Mdl).CategoricalPredictors, 1); %! CV = crossval (Mdl, 'KFold', 3); %! assert_equal (CV.CategoricalPredictors, 1); %! assert_equal (CV.Trained{1}.Trained{1}.CategoricalPredictors, 1); %!test # a tree template's categorical options reach every tree %! t = templateTree ('MaxNumCategories', 3, 'MaxNumSplits', 3); %! Mdl = ClassificationEnsemble (X, yb, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 2, 'Learners', t, ... %! 'CategoricalPredictors', 1); %! assert_equal (Mdl.Trained{1}.CategoricalPredictors, 1); %!error ... %! ClassificationEnsemble (X, yb, 'CategoricalPredictors', 3) ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcensemble (T, 'Species'); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'Species'), a); %! assert_equal (loss (Mdl, T), a); ## A table at edge %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcensemble (T, 'Species'); %! a = edge (Mdl, X, y); %! assert_equal (edge (Mdl, T(:,1:2), y), a); %! assert_equal (edge (Mdl, T, 'Species'), a); %! assert_equal (edge (Mdl, T), a); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcensemble (T, 'Species'); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## Observation weights of class single or double %!error ... %! fitcensemble (ones (4, 2), [1; 1; 2; 2], 'Weights', int8 ([1; 1; 1; 1])) %!error ... %! fitcensemble (ones (4, 2), [1; 1; 2; 2], 'Weights', true (4, 1)) %!test %! ## Single weights are stored single, summing to one %! load fisheriris %! w = 1 + (1:150)' / 7; %! Mdl = fitcensemble (meas, species, 'Weights', single (w), ... %! 'NumLearningCycles', 10); %! assert_equal (class (Mdl.W), 'single'); %! assert_equal (sum (double (Mdl.W)), 1, 1e-6); %!test %! ## Single weights compute as double %! load fisheriris %! w = 1 + (1:150)' / 7; %! A = fitcensemble (meas, species, 'Weights', single (w), ... %! 'NumLearningCycles', 10); %! B = fitcensemble (meas, species, 'Weights', double (single (w)), ... %! 'NumLearningCycles', 10); %! assert_equal (nthargout (2, @predict, A, meas), nthargout (2, @predict, ... %! B, meas)); ## Test a class with prior 0, whose observations are removed before fitting, ## as MATLAB R2024a and R2026b remove them %!warning ... %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! Mdl = ClassificationEnsemble (X, Y, 'Prior', [1, 0]); %! assert_equal (Mdl.NumObservations, 50); %! assert_equal (Mdl.RowsUsed, [true(50, 1); false(50, 1)]); %! assert_equal (Mdl.Prior, [1, 0]); %! assert_equal (predict (Mdl, meas([51, 110], 3:4)), ... %! {'versicolor'; 'versicolor'}); %!warning ... %! load fisheriris %! Mdl = ClassificationEnsemble (meas, species, 'Prior', [0.5, 0.5, 0]); %! assert_equal (predict (Mdl, meas([1, 51, 101], :)), ... %! {'setosa'; 'versicolor'; 'versicolor'}); %!test %! ## A named class with prior 0 may be absent, having nothing to remove %! load fisheriris %! X = meas(51:100, 3:4); %! Y = species(51:100); %! C = {'versicolor', 'virginica'}; %! Mdl = ClassificationEnsemble (X, Y, 'ClassNames', C, 'Prior', [1, 0]); %! assert_equal (Mdl.ClassNames, {'versicolor'; 'virginica'}); %! assert_equal (predict (Mdl, meas(51, 3:4)), {'versicolor'}); %!error ... %! load fisheriris %! ClassificationEnsemble (meas(51:150, 3:4), species(51:150), ... %! 'Cost', [0, 1; 0, 0]); ## Test a 'Cost' given as a structure, permuted by name to the model's order, ## as MATLAB R2024a permutes it %!test %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! S = struct ('ClassNames', {{'virginica', 'versicolor'}}, ... %! 'ClassificationCosts', [0, 2; 1, 0]); %! Mdl = ClassificationEnsemble (X, Y, 'Cost', S); %! assert_equal (Mdl.Cost, [0, 1; 2, 0]); %! assert_equal (Mdl.Prior, [0.5, 0.5]); %!test %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! S = struct ('ClassNames', {{'virginica', 'versicolor'}}, ... %! 'ClassificationCosts', [0, 2; 1, 0]); %! M1 = ClassificationEnsemble (X, Y, 'Cost', S); %! M2 = ClassificationEnsemble (X, Y, 'Cost', [0, 1; 2, 0]); %! assert_equal (predict (M1, X), predict (M2, X)); %!error ... %! load fisheriris %! ClassificationEnsemble (meas(51:150, 3:4), species(51:150), ... %! 'Cost', struct ('a', 1)); ## MATLAB parity: a row missing some predictors is kept, and dropped only ## when every predictor is missing or its weight is zero, as measured on ## R2024a for every method %!shared emX, emL, emQ %! x1 = mod ((1:30)', 7) + (1:30)' / 10; %! x2 = mod ((1:30)', 5) * 2 - (1:30)' / 20; %! emX = [x1, x2]; %! emX([2, 9, 17], 1) = NaN; %! emX(23, 2) = NaN; %! emX(27, :) = NaN; %! emQ = [3, 1; 5, 4; 7, 2; NaN, 3]; %! emL = repmat ({'p'; 'q'; 'q'; 'p'; 'p'; 'q'}, 5, 1); %!test # boosting keeps the rows a predictor is missing from %! Mdl = fitcensemble (emX, emL, 'Method', 'AdaBoostM1'); %! assert_equal (Mdl.NumObservations, 29); %! assert_equal (Mdl.RowsUsed, [true(26, 1); false; true(3, 1)]); %! assert_equal (rows (Mdl.X), 29); %!test # and so does bagging %! Mdl = fitcensemble (emX, emL, 'Method', 'Bag'); %! assert_equal (Mdl.NumObservations, 29); %!test # and the random subspace, whose learners drop such rows themselves %! Mdl = fitcensemble (emX, emL, 'Method', 'Subspace', 'Learners', 'knn'); %! assert_equal (Mdl.NumObservations, 29); %!test # a row of weight zero is dropped %! Mdl = fitcensemble (emX, emL, 'Method', 'AdaBoostM1', ... %! 'Weights', [0; ones(29, 1)]); %! assert_equal (Mdl.NumObservations, 28); %!test # the first learner sees the rows a predictor is missing from %! Mdl = fitcensemble (emX, emL, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 1); %! [label, score] = predict (Mdl, emQ); %! assert_equal (label, {'p'; 'q'; 'p'; 'p'}); %! assert_equal (score(:,1), 1.30134484272219 * [1; -1; 1; 1], 1e-13); ## A Subspace learner fits the rows it keeps under the ensemble's weights %!shared Xs, Ys %! Xs = [1 2 3; 2 1 1; NaN NaN 4; 3 2 1; 6 7 5; 7 6 9; 7 NaN NaN; 8 7 6; ... %! 1 7 5; 2 8 3; 2 6 5; 3 7 8]; %! Ys = [1 1 1 1 2 2 2 2 3 3 3 3]'; %!test %! Mdl = fitcensemble (Xs, Ys, 'Method', 'Subspace', 'Learners', ... %! 'discriminant', 'NPredToSample', 2, ... %! 'NumLearningCycles', 'AllPredictorCombinations'); %! assert_equal (Mdl.Trained{1}.Prior, [3, 4, 4] / 11, 1e-14); %! assert_equal (resubLoss (Mdl), 1 / 12, 1e-14); %! assert_equal (resubEdge (Mdl), NaN); %!test %! Mdl = fitcensemble (Xs, Ys, 'Method', 'Subspace', 'Learners', ... %! 'discriminant', 'NPredToSample', 2, ... %! 'NumLearningCycles', 'AllPredictorCombinations', ... %! 'Prior', [0.2, 0.5, 0.3]); %! assert_equal (Mdl.Trained{1}.Prior, [0.15, 0.5, 0.3] / 0.95, 1e-14); statistics-release-2.1.2/inst/Machine_Learning/ClassificationGAM.m000066400000000000000000005570001527311104300251260ustar00rootroot00000000000000## Copyright (C) 2024 Ruchika Sonagote ## Copyright (C) 2025 Swayam Shah ## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ClassificationGAM < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} ClassificationGAM ## ## Generalized additive model classification ## ## The @code{ClassificationGAM} class implements a gradient boosting ## algorithm for classification. This approach allows the model to capture ## non-linear relationships between predictors and the binary response ## variable. ## ## Generalized additive model classification is a statistical method that ## extends linear models by allowing non-linear relationships between each ## predictor and the response variable through smooth functions. It combines ## the interpretability of linear models with the flexibility of ## non-parametric methods. ## ## Create a @code{ClassificationGAM} object by using the @code{fitcgam} ## function or the class constructor. ## ## Two weak learners are available, selected by @code{FitMethod}. ## ## @qcode{'boostedtrees'}, the default, boosts one shallow decision tree per ## predictor in each round, which is the scheme MATLAB's generalized ## additive model uses. A second phase then boosts trees over pairs of ## predictors, where interactions are asked for. ## ## @qcode{'splines'} boosts a smoothing spline per predictor over ## @code{NumIterations} passes. It has no MATLAB counterpart and is an ## Octave extension, kept because a smooth additive fit is a genuinely ## different and often better answer than a staircase of stumps. ## ## The two take different arguments, and an argument meant for one is ## refused by the other rather than ignored. ## ## The choice is visible in the properties. @code{Knots}, @code{Order}, ## @code{DoF}, @code{Formula}, @code{LearningRate}, @code{NumIterations}, ## @code{BaseModel}, @code{ModelwInt} and @code{IntMatrix} describe a spline ## fit and are empty under the boosted-tree engine, while ## @code{ModelParameters}, @code{ReasonForTermination}, @code{BinEdges}, ## @code{PairDetectionBinEdges} and @code{TreeModel} describe a tree fit and ## are empty under the spline engine. ## ## Fitted values are not expected to equal MATLAB's even under ## @qcode{'boostedtrees'}. The stopping rule and the step-reduction limit ## are not recoverable from anything MATLAB reports, so this engine ## documents its own; what the two share is the estimator and the reported ## surface, not the arithmetic. ## ## @seealso{fitcgam} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} X ## ## Predictor data ## ## A numeric matrix containing the unstandardized predictor data. Each ## column of @var{X} represents one predictor (variable), and each row ## represents one observation. This property is read-only. ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} Y ## ## Class labels ## ## Specified as a logical or numeric column vector, or as a character array ## or a cell array of character vectors with the same number of rows as the ## predictor data. Each row in @var{Y} is the observed class label for ## the corresponding row in @var{X}. This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} NumObservations ## ## Number of observations ## ## A positive integer value specifying the number of observations in the ## training dataset used for training the ClassificationGAM model. ## This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} RowsUsed ## ## Rows used for fitting ## ## A logical column vector with the same length as the observations in the ## original predictor data @var{X}, true for each row that was used for ## fitting the ClassificationGAM model. It is empty, @qcode{[]}, ## when every observation was used, so a non-empty value means that rows ## holding missing values were dropped. This property is read-only. ## ## @end deftp RowsUsed = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} NumPredictors ## ## Number of predictors ## ## A positive integer value specifying the number of predictors in the ## training dataset used for training the ClassificationGAM model. ## This property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} PredictorNames ## ## Names of predictor variables ## ## A cell array of character vectors specifying the names of the predictor ## variables. The names are in the order in which they appear in the ## training dataset. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} ResponseName ## ## Response variable name ## ## A character vector specifying the name of the response variable @var{Y}. ## This property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} ClassNames ## ## Names of classes in the response variable ## ## An array of unique values of the response variable @var{Y}, which has the ## same data types as the data in @var{Y}, sorted or in the order given by ## the @qcode{'ClassNames'} option. This property is read-only. ## @qcode{ClassNames} can have any of the following datatypes: ## ## @itemize ## @item Cell array of character vectors ## @item Character array ## @item Logical vector ## @item Numeric vector ## @end itemize ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} Prior ## ## Prior probability for each class ## ## A 2-element numeric vector specifying the prior probabilities for each ## class. The order of the elements in @qcode{Prior} corresponds to the ## order of the classes in @qcode{ClassNames}. This property is read-only. ## ## Specified as a row vector with one entry per class, in the order of ## @qcode{ClassNames}, and rescaled to sum to one. It may be given as ## @qcode{'empirical'}, @qcode{'uniform'}, a numeric vector, or a ## structure with @qcode{ClassNames} and @qcode{ClassProbs} fields, which ## assigns each probability by class name rather than by position. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} Formula ## ## Model specification formula ## ## A character vector specifying the model formula in the form ## @qcode{'Y ~ terms'} where @qcode{Y} represents the response variable and ## @qcode{terms} specifies the predictor variables and interaction terms. ## This property is read-only. ## ## @end deftp Formula = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} Interactions ## ## Two-way interaction terms of the fitted model ## ## A @math{Kx2} matrix of predictor index pairs, one row per two-way term ## the model carries, and @code{zeros (0, 2)} when it carries none. It ## reports what was fitted rather than what was asked for, so a count of ## terms, @qcode{'all'}, a logical matrix and a formula all leave the same ## kind of value behind. This property is read-only. ## ## A main effect names one predictor and a higher-order term names three ## or more, and neither has a two-column form, so neither appears here. ## @code{IntMatrix} remains the complete record of every term fitted. ## ## @end deftp Interactions = zeros (0, 2); ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} Knots ## ## Knots for spline fitting ## ## A scalar or row vector specifying the number of knots for each predictor ## variable in the spline fitting. This property is read-only. ## ## @end deftp Knots = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} Order ## ## Order of spline fitting ## ## A scalar or row vector specifying the order of the spline for each ## predictor variable. This property is read-only. ## ## @end deftp Order = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} DoF ## ## Degrees of freedom for spline fitting ## ## A scalar or row vector specifying the degrees of freedom for each ## predictor variable in the spline fitting. This property is read-only. ## ## @end deftp DoF = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} LearningRate ## ## Learning rate for gradient boosting ## ## A scalar value between 0 and 1 specifying the learning rate used in the ## gradient boosting algorithm. This property is read-only. ## ## @end deftp LearningRate = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} NumIterations ## ## Maximum number of iterations ## ## A positive integer specifying the maximum number of iterations for the ## gradient boosting algorithm. This property is read-only. ## ## @end deftp NumIterations = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} Intercept ## ## Intercept of the fitted model ## ## A numeric scalar, the log-odds of the response mean, which every ## additive term is measured against. This property is read-only. ## ## @end deftp Intercept = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} W ## ## Observation weights ## ## A numeric column vector with one entry per observation used for training, ## normalised to sum to one. It has the class of the @qcode{'Weights'} ## given, single or double. This property is read-only. ## ## Each class carries its prior, spread over its own observations in ## proportion to their @qcode{'Weights'}, and evenly when none were given. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A numeric vector holding the column of each predictor treated as ## categorical, and empty when none is. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} ExpandedPredictorNames ## ## Names of the expanded predictor variables ## ## A cell array of character vectors naming the predictors as the model ## sees them. It matches @code{PredictorNames} unless a categorical ## predictor was expanded into dummy variables. This property is ## read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} BaseModel ## ## Base model parameters ## ## A structure containing the parameters of the base model without any ## interaction terms. The base model represents the generalized additive ## model with only the main effects (predictor terms) included. ## This property is read-only. ## ## @end deftp BaseModel = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} ModelwInt ## ## Model parameters with interactions ## ## A structure containing the parameters of the model that includes ## interaction terms. This model extends the base model by adding ## interaction terms between predictors. This property is read-only. ## ## @end deftp ModelwInt = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} IntMatrix ## ## Every term the model fits ## ## A logical matrix with one row per term and one column per predictor, ## true wherever the term multiplies that predictor. A row naming one ## predictor is a main effect, two an interaction, and three or more a ## higher-order term. This property is read-only. ## ## It is the complete record, where @code{Interactions} reports only the ## two-way terms, in the form MATLAB reports them. It is also the form ## the @qcode{'Interactions'} option takes back, so passing it to the ## constructor rebuilds a model over the same terms. ## ## @end deftp IntMatrix = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} BinEdges ## ## Bin edges of the predictors ## ## A cell array with one entry per predictor, holding that predictor's bin ## edges where the model discretized it before fitting. It is empty here ## and stays empty: this generalized additive model is built from splines, ## which take the predictors as they are, where MATLAB's is built from ## boosted trees and bins them. That difference is described in the class ## documentation. ## ## This property is read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} PairDetectionBinEdges ## ## Bin edges used to detect interactions ## ## A cell array with one row vector per predictor, holding the coarse cut ## points the residuals of the predictor phase were laid on while pairs ## were being tested. The grid is eight equal-frequency bins whatever the ## sample size, as MATLAB's is. It is empty when the model carries no ## interaction terms, and empty throughout under the spline engine, which ## does not bin. ## ## This property is read-only. ## ## @end deftp PairDetectionBinEdges = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} ModelParameters ## ## Parameters the model was fitted with ## ## A structure holding the fitting parameters. Under the boosted-tree ## engine it carries MATLAB's own fields: @qcode{NumPrint}, ## @qcode{MaxPValue}, @qcode{InitialLearnRateForPredictors}, ## @qcode{InitialLearnRateForInteractions}, ## @qcode{NumTreesPerPredictor}, @qcode{NumTreesPerInteraction}, ## @qcode{MaxNumSplitsPerPredictor}, @qcode{MaxNumSplitsPerInteraction}, ## @qcode{VerbosityLevel}, @qcode{Interactions}, @qcode{Version}, ## @qcode{Method} and @qcode{Type}. @qcode{Interactions} here is the ## request as it was made, a count or @qcode{'all'}, where the ## @qcode{Interactions} property of the model is the pairs actually ## selected. ## ## Under the spline engine it describes that scheme instead, carrying ## @qcode{Knots}, @qcode{Order}, @qcode{DoF}, @qcode{Formula}, ## @qcode{Interactions}, @qcode{LearningRate} and @qcode{NumIterations}, ## since none of the tree vocabulary applies to it. ## ## This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} ReasonForTermination ## ## Why each fitting phase stopped ## ## A structure with the fields @qcode{PredictorTrees} and ## @qcode{InteractionTrees}, each a character vector saying why that phase ## of the fit ended: that it trained the trees it was asked for, or that ## it could no longer improve the model. A phase that never ran reports ## an empty character vector, which is what a model with no interaction ## terms shows for the second field. ## ## It is empty under the spline engine, which has no tree budget to ## exhaust. ## ## This property is read-only. ## ## @end deftp ReasonForTermination = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} FitMethod ## ## Which engine fitted the model ## ## A character vector, either @qcode{'boostedtrees'} or ## @qcode{'splines'}. The default is @qcode{'boostedtrees'}, which is the ## scheme MATLAB's generalized additive model uses and the one the ## tree-shaped properties above describe. ## ## @qcode{'splines'} selects the penalised-spline engine instead, which is ## an Octave extension with no MATLAB counterpart. It is the scheme this ## class fitted before version 1.9.0, and it is kept because a smooth ## additive fit is a genuinely different and often better answer than a ## staircase of stumps. The two engines take different arguments and an ## argument meant for one is refused by the other rather than ignored. ## ## This property is read-only. ## ## @end deftp FitMethod = 'boostedtrees'; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} TreeModel ## ## The fitted shape functions and interaction surfaces ## ## A structure holding what the boosted-tree engine fitted, with fields ## @qcode{ShapeValues}, one column vector per predictor giving that ## predictor's contribution in each of its bins, @qcode{PairValues}, one ## matrix per selected pair, and @qcode{Pairs}, the predictor indices those ## matrices belong to. A shape function is a step function, so these are ## the whole of the fit however many trees produced them. ## ## MATLAB exposes no equivalent: it reports the bin edges but never the ## values on them, so its shape functions can only be reached through ## @code{predict}. This property is an Octave extension, and it is empty ## under the spline engine, whose fit lives in @code{BaseModel} and ## @code{ModelwInt}. ## ## This property is read-only. ## ## @end deftp TreeModel = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} HyperparameterOptimizationResults ## ## Results of the hyperparameter optimization ## ## @strong{Always empty.} It is declared for MATLAB compatibility, where ## it holds what an automatic search over the hyperparameters found. This ## class fits the parameters it is given and runs no such search, so there ## is nothing to report. This property is read-only. ## ## @end deftp HyperparameterOptimizationResults = []; endproperties ## Properties a user may set after the model is built. Each one is ## validated by its set method below. properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} Cost ## ## Cost of Misclassification ## ## A square matrix specifying the cost of misclassification of a point. ## @qcode{Cost(i,j)} is the cost of classifying a point into class @qcode{j} ## if its true class is @qcode{i} (that is, the rows correspond to the true ## class and the columns correspond to the predicted class). The order of ## the rows and columns in @qcode{Cost} corresponds to the order of the ## classes in @qcode{ClassNames}. The number of rows and columns in ## @qcode{Cost} is the number of unique classes in the response. By ## default, @qcode{Cost(i,j) = 1} if @qcode{i != j}, and ## @qcode{Cost(i,j) = 0} if @qcode{i = j}. In other words, the cost is 0 ## for correct classification and 1 for incorrect classification. ## ## Add or change the @qcode{Cost} property using dot notation as in: ## @itemize ## @item @qcode{@var{obj}.Cost = @var{costMatrix}} ## @end itemize ## ## ## A cost may also be given as a struct with the fields ## @qcode{ClassNames} and @qcode{ClassificationCosts}, which names the ## order its own matrix is written in. That matrix is permuted into the ## order of @qcode{ClassNames} above, so a caller need not know which ## order the classes were sorted into. It must name every class. ## ## A cost must be floating point, not sparse, not complex, non-negative ## and zero down its diagonal, and must hold no @qcode{NaN} or ## @qcode{Inf}. A @code{single} is widened to @code{double}. ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {ClassificationGAM} {property} ScoreTransform ## ## Transformation function for classification scores ## ## Specified as a function handle for transforming the classification ## scores. Add or change the @qcode{ScoreTransform} property using dot ## notation as in: ## ## @itemize ## @item @qcode{@var{obj}.ScoreTransform = 'function_name'} ## @item @qcode{@var{obj}.ScoreTransform = @@function_handle} ## @end itemize ## ## When specified as a character vector, it can be any of the following ## built-in functions. Nevertheless, the @qcode{ScoreTransform} property ## always stores their function handle equivalent. ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Value} @tab @var{Description} ## @item @qcode{'doublelogit'} @tab @math{1 ./ (1 + exp (-2 * x))} ## @item @qcode{'invlogit'} @tab @math{log (x ./ (1 - x))} ## @item @qcode{'ismax'} @tab Sets the score for the class with the ## largest score to 1, and for all other classes to 0 ## @item @qcode{'logit'} @tab @math{1 ./ (1 + exp (-x))} ## @item @qcode{'none'} @tab @math{x} (no transformation) ## @item @qcode{'identity'} @tab @math{x} (no transformation) ## @item @qcode{'sign'} @tab ## @math{-1 for x < 0, 0 for x = 0, 1 for x > ## 0} ## @item @qcode{'symmetric'} @tab @math{2 * x - 1} ## @item @qcode{'symmetricismax'} @tab Sets the score for the class ## with the largest score to 1, and for all other classes to -1 ## @item @qcode{'symmetriclogit'} @tab @math{2 ./ (1 + exp (-x)) - 1} ## @end multitable ## ## The default is @qcode{'logit'}, as in MATLAB. This model's raw ## score is a log-odds, reported as the pair @math{[-f, f]} whose two ## columns sum to zero, and the transform is what turns it into the ## posterior probabilities that sum to one. Every transform therefore ## composes on the log-odds and not on the probabilities, so ## @qcode{'none'} returns the log-odds themselves. ## ## @end deftp ScoreTransform = 'logit'; endproperties ## Readable by the counterpart class, which copies it, and kept out of ## the documented surface. properties (GetAccess = public, SetAccess = protected, Hidden) STfun = @(x) 1 ./ (1 + exp (-x)); ## How many trees each boosting phase actually fitted, which the ## budget in ModelParameters does not say: a phase may stop early. ## Hidden because MATLAB reports it on the partitioned classes and ## not on the model, and that is where this is read from. NumTrainedTrees = []; endproperties ## Set methods for the properties a user may assign. methods (Hidden) function this = set.Cost (this, val) gnY = this.ClassNames; if (isempty (val)) this.Cost = cast (! eye (classCount (gnY)), 'double'); else ## Everything a cost must be, and the struct form, which ## is permuted into this model's class order. [val, errmsg] = costMatrix (val, gnY); if (! isempty (errmsg)) error ("ClassificationGAM: %s", errmsg); endif this.Cost = val; endif endfunction function this = set.ScoreTransform (this, val) [f, nm] = parseScoreTransform (val, 'ClassificationGAM'); this.ScoreTransform = nm; this.STfun = f; endfunction ## Custom display function disp (this) fprintf ("\n ClassificationGAM\n\n"); ## Print selected properties fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); if (iscellstr (this.ClassNames)) str = repmat ({'''%s'''}, 1, numel (this.ClassNames)); str = strcat ('{', strjoin (str, ' '), '}'); str = sprintf (str, this.ClassNames{:}); elseif (ischar (this.ClassNames)) str = repmat ({'''%s'''}, 1, rows (this.ClassNames)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, cellstr (this.ClassNames){:}); else # single, double, logical str = repmat ({'%d'}, 1, numel (this.ClassNames)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, this.ClassNames); endif fprintf ("%+25s: %s\n", 'ClassNames', str); fprintf ("%+25s: '%s'\n", 'ScoreTransform', this.ScoreTransform); fprintf ("%+25s: %d\n", 'NumObservations', this.NumObservations); fprintf ("%+25s: %d\n", 'NumPredictors', this.NumPredictors); if (! isempty (this.Formula)) fprintf ("%+25s: '%s'\n", 'Formula', this.Formula); endif if (! isempty (this.Interactions)) fprintf ("%+25s: [%dx%d %s]\n", 'Interactions', ... size (this.Interactions, 1), size (this.Interactions, 2), ... class (this.Interactions)); endif endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {ClassificationGAM} {@var{obj} =} ClassificationGAM (@var{X}, @var{Y}) ## @deftypefnx {ClassificationGAM} {@var{obj} =} ClassificationGAM (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationGAM} {@var{obj} =} ClassificationGAM (@var{Tbl}, @var{formula}) ## @deftypefnx {ClassificationGAM} {@var{obj} =} ClassificationGAM (@var{Tbl}, @var{Y}) ## @deftypefnx {ClassificationGAM} {@var{obj} =} ClassificationGAM (@dots{}, @var{name}, @var{value}) ## ## Create a @qcode{ClassificationGAM} class object containing a generalized ## additive classification model. ## ## @code{@var{obj} = ClassificationGAM (@var{X}, @var{Y})} returns ## a ClassificationGAM object, with @var{X} as the predictor data ## and @var{Y} containing the class labels of observations in @var{X}. ## ## @itemize ## @item ## @code{X} must be a @math{N*P} numeric matrix of input data where rows ## correspond to observations and columns correspond to features or ## variables. @var{X} will be used to train the GAM model. ## @item ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels ## of corresponding predictor data in @var{X}. @var{Y} can contain any type ## of categorical data. @var{Y} must have the same number of rows as ## @var{X}. ## @end itemize ## ## @code{@var{obj} = ClassificationGAM (@dots{}, @var{name}, ## @var{value})} returns a ClassificationGAM object with parameters ## specified by the following @qcode{@var{name}, @var{value}} paired input ## arguments: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PredictorNames'} @tab A cell array of character ## vectors specifying the names of the predictors. The length of this array ## must match the number of columns in @var{X}. ## ## @item @qcode{'ResponseName'} @tab A character vector specifying the ## name of the response variable. ## ## @item @qcode{'ClassNames'} @tab Names of the classes in the class ## labels, @var{Y}, used for fitting the GAM model. ## @qcode{ClassNames} are of the same type as the class labels in @var{Y}. ## The model keeps the classes in this order; by default they are sorted. ## ## @item @qcode{'Cost'} @tab An @math{N*R} numeric matrix containing ## misclassification cost for the corresponding instances in @var{X}, where ## @math{R} is the number of unique categories in @var{Y}. If an instance ## is correctly classified into its category the cost is calculated to be 1, ## otherwise 0. The cost matrix can be altered by using ## @code{@var{Mdl}.cost = somecost}. By default, its value is ## @qcode{@var{cost} = ones (rows (X), numel (unique (Y)))}. ## ## @item @qcode{'Prior'} @tab A numeric vector specifying the prior ## probabilities for each class. The order of the elements in @qcode{Prior} ## corresponds to the order of the classes in @qcode{ClassNames}. ## Alternatively, you can specify @qcode{'empirical'} to use the empirical ## class probabilities or @qcode{'uniform'} to assume equal class ## probabilities. The prior weighs the boosted-tree fit: each class ## carries its prior, spread over its observations in proportion to ## @qcode{'Weights'}, and an empirical prior is each class's share of the ## weight. ## A class whose prior is 0 is removed from the data before fitting, with a ## warning, as in MATLAB. ## ## @item @qcode{'ScoreTransform'} @tab A user-defined function handle ## or a character vector specifying one of the following builtin functions ## specifying the transformation applied to predicted classification scores. ## Supported values include @qcode{'doublelogit'}, @qcode{'invlogit'}, ## @qcode{'ismax'}, @qcode{'logit'}, @qcode{'none'}, @qcode{'identity'}, ## @qcode{'sign'}, @qcode{'symmetric'}, @qcode{'symmetricismax'}, and ## @qcode{'symmetriclogit'}. ## ## @item @qcode{'Formula'} @tab (spline option) A character vector ## specifying the model ## formula in the form @qcode{'Y ~ terms'} where @qcode{Y} represents the ## response variable and @qcode{terms} specifies the predictor variables and ## interaction terms. ## ## @item @qcode{'Interactions'} @tab A logical matrix, a positive ## integer scalar, or the string @qcode{'all'} for defining the interactions ## between predictor variables. ## ## @item @qcode{'Knots'} @tab (spline option) A scalar or row vector ## specifying the ## number of knots for each predictor variable in the spline fitting. ## ## @item @qcode{'Order'} @tab (spline option) A scalar or row vector ## specifying the ## order of the spline for each predictor variable. ## ## @item @qcode{'DoF'} @tab (spline option) A scalar or row vector ## specifying the ## degrees of freedom for each predictor variable in the spline fitting. ## ## @item @qcode{'LearningRate'} @tab (spline option) A scalar value between ## 0 and 1 ## specifying the learning rate used in the gradient boosting algorithm. ## ## @item @qcode{'NumIterations'} @tab (spline option) A positive integer ## specifying ## the maximum number of iterations for the gradient boosting algorithm. ## @end multitable ## ## A row marked @qcode{(spline option)} belongs to the spline ## engine and requires @qcode{'FitMethod', 'splines'}; passing one ## under the default boosted-tree engine is an error rather than ## being ignored. The boosted-tree engine's own options are ## documented under @code{fitcgam}. ## ## @seealso{fitcgam} ## @end deftypefn function this = ClassificationGAM (X, Y, varargin) ## Check for sufficient number of input arguments if (nargin < 2) error ("ClassificationGAM: too few input arguments."); endif ## A table names its own predictors and says which hold levels ## An ordinal column is a numeric predictor here, as in R2024a [this, X, Y, varargin] = resolveTable (this, 'ClassificationGAM', ... X, Y, varargin, true); ## Check X and Y have the same number of observations if (rows (X) != rows (Y)) error ("ClassificationGAM: number of rows in X and Y must be equal."); endif nsample = rows (X); ndims_X = columns (X); ## Assign original X and Y data this.X = X; this.Y = Y; ## Get groups in Y [gY, gnY, glY] = grp2idx (Y); ## Parse optional paired arguments optNames = {'PredictorNames', 'ResponseName', 'ClassNames', 'Prior', ... 'CategoricalPredictors', 'Weights', 'Cost', ... 'ScoreTransform', 'Formula', 'Interactions', 'Knots', ... 'Order', 'DoF', 'LearningRate', 'NumIterations', ... 'FitMethod', 'NumTreesPerPredictor', ... 'NumTreesPerInteraction', 'MaxNumSplitsPerPredictor', ... 'MaxNumSplitsPerInteraction', ... 'InitialLearnRateForPredictors', ... 'InitialLearnRateForInteractions', 'Verbose', 'NumPrint', ... 'MaxPValue'}; ## An empty default stands for one resolved once the options are ## known, so that giving an option can be told apart from leaving it ## out: 'Knots', 'Order' and 'DoF' are 5, 3 and 8 per predictor, any ## two determining the third; 'LearningRate' is 0.1 and ## 'NumIterations' 100. The boosted-tree options take MATLAB's own ## defaults, which ModelParameters reports and so are part of the ## surface being matched: 300 and 100 trees, 1 and 4 splits, learning ## rates of 1, a 'MaxPValue' of 1, 'Verbose' 0 and 'NumPrint' 10. ## Every other boosted additive model shrinks far harder than a step of ## 1, scikit-learn, gbm and mboost defaulting to 0.1 and the ## Explainable Boosting Machine to 0.015; the docstring says so rather ## than the default being quietly changed. dfValues = {{}, [], [], 'empirical', [], [], [], [], [], [], [], [], ... [], [], [], 'boostedtrees', [], [], [], [], [], [], [], ... [], []}; [PredictorNames, ResponseName, ClassNames, Prior, ... CategoricalPredictors, Weights, Cost, STin, Formula, Interactions, ... Knots, Order, DoF, LearningRate, NumIterations, FitMethod, ... NumTreesPerPredictor, NumTreesPerInteraction, ... MaxNumSplitsPerPredictor, MaxNumSplitsPerInteraction, ... InitialLearnRateForPredictors, InitialLearnRateForInteractions, ... Verbose, NumPrint, MaxPValue, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! isempty (PredictorNames) && ! iscellstr (PredictorNames)) error (strcat ("ClassificationGAM: 'PredictorNames'", ... " must be supplied as a cellstring array.")); elseif (! isempty (PredictorNames) && (numel (PredictorNames) != columns (X))) error (strcat ("ClassificationGAM: 'PredictorNames'", ... " must equal the number of columns in X.")); endif if (! isempty (ResponseName) && ! ischar (ResponseName)) error (strcat ("ClassificationGAM: 'ResponseName'", ... " must be a character vector.")); endif if (! isempty (ClassNames) && ! (iscellstr (ClassNames) || isnumeric (ClassNames) || islogical (ClassNames) || ischar (ClassNames) || isa (ClassNames, 'categorical') || isa (ClassNames, 'string'))) error (strcat ("ClassificationGAM: 'ClassNames' must be a", ... " categorical array, a character array, a", ... " string array, a logical vector, a numeric", ... " vector, or a cell array of character", ... " vectors.")); endif ## A named class with prior 0 is removed below, so it may be absent exempt = []; if (! isempty (ClassNames)) exempt = zeroPriorNames (ClassNames, Prior, []); [~, errmsg] = namedClasses (glY, ClassNames, exempt); if (! isempty (errmsg)) error ("ClassificationGAM: %s", errmsg); endif endif if (! (isstruct (Prior) || isnumeric (Prior) || ischar (Prior))) error (strcat ("ClassificationGAM: 'Prior' must be", ... " a numeric vector or a string.")); endif if (ischar (Prior) && ! any (strcmpi (Prior, {'empirical', 'uniform'}))) error (strcat ("ClassificationGAM: 'Prior' must be", ... " 'empirical', 'uniform', or a numeric vector.")); endif if (isnumeric (Prior) && numel (Prior) != 2 && ! isstruct (Prior)) error ("ClassificationGAM: 'Prior' must be a 2-element vector."); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("ClassificationGAM: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isreal (Weights) && isvector (Weights))) error ("ClassificationGAM: 'Weights' must be a numeric vector."); endif if (! isempty (Weights) && numel (Weights) != rows (X)) error (strcat ("ClassificationGAM: 'Weights' must have one", ... " element per row of X.")); endif if (! isempty (Weights) && (any (Weights(:) < 0) || ! all (isfinite (Weights(:))))) error (strcat ("ClassificationGAM: 'Weights' must hold", ... " finite non-negative values.")); endif ## A struct carrying its own class order is a cost too, ## and is resolved by the property's own set method. if (! isempty (Cost) && ! (isstruct (Cost) || (isnumeric (Cost) && issquare (Cost)))) error (strcat ("ClassificationGAM: 'Cost' must be", ... " a numeric square matrix.")); endif if (! isempty (Formula) && ! ischar (Formula) && ! islogical (Formula)) error ("ClassificationGAM: 'Formula' must be a string."); endif if (! isempty (Interactions) && ! ((isnumeric (Interactions) && isscalar (Interactions) && Interactions == fix (Interactions) && Interactions >= 0) || islogical (Interactions) || (ischar (Interactions) && strcmpi (Interactions, 'all')))) error ("ClassificationGAM: invalid 'Interactions' parameter."); endif if (! isempty (Knots) && (! isnumeric (Knots) || ! (isscalar (Knots) || isequal (size (Knots), [1, ndims_X])))) error ("ClassificationGAM: invalid value for 'Knots'."); endif if (! isempty (Order) && (! isnumeric (Order) || ! (isscalar (Order) || isequal (size (Order), [1, ndims_X])))) error ("ClassificationGAM: invalid value for 'Order'."); endif if (! isempty (DoF) && (! isnumeric (DoF) || ! (isscalar (DoF) || isequal (size (DoF), [1, ndims_X])))) error ("ClassificationGAM: invalid value for 'DoF'."); endif if (! isempty (LearningRate) && (LearningRate > 1 || LearningRate <= 0)) error (strcat ("ClassificationGAM: 'LearningRate'", ... " must be between 0 and 1.")); endif if (! isempty (NumIterations) && (! isnumeric (NumIterations) || NumIterations <= 0)) error (strcat ("ClassificationGAM: 'NumIterations'", ... " must be a positive integer value.")); endif if (! (ischar (FitMethod) && isrow (FitMethod)) || ! any (strcmpi (FitMethod, {'boostedtrees', 'splines'}))) error (strcat ("ClassificationGAM: 'FitMethod' must be", ... " 'boostedtrees' or 'splines'.")); endif FitMethod = tolower (FitMethod); if (! isempty (NumTreesPerPredictor) && (! isnumeric (NumTreesPerPredictor) || ! isscalar (NumTreesPerPredictor) || NumTreesPerPredictor < 1 || fix (NumTreesPerPredictor) != NumTreesPerPredictor)) error (strcat ("ClassificationGAM:", ... " 'NumTreesPerPredictor' must be a positive", ... " integer value.")); endif if (! isempty (NumTreesPerInteraction) && (! isnumeric (NumTreesPerInteraction) || ! isscalar (NumTreesPerInteraction) || NumTreesPerInteraction < 1 || fix (NumTreesPerInteraction) != NumTreesPerInteraction)) error (strcat ("ClassificationGAM:", ... " 'NumTreesPerInteraction' must be a", ... " positive integer value.")); endif if (! isempty (MaxNumSplitsPerPredictor) && (! isnumeric (MaxNumSplitsPerPredictor) || ! isscalar (MaxNumSplitsPerPredictor) || MaxNumSplitsPerPredictor < 1 || fix (MaxNumSplitsPerPredictor) != MaxNumSplitsPerPredictor)) error (strcat ("ClassificationGAM:", ... " 'MaxNumSplitsPerPredictor' must be a", ... " positive integer value.")); endif if (! isempty (MaxNumSplitsPerInteraction) && (! isnumeric (MaxNumSplitsPerInteraction) || ! isscalar (MaxNumSplitsPerInteraction) || MaxNumSplitsPerInteraction < 1 || fix (MaxNumSplitsPerInteraction) != MaxNumSplitsPerInteraction)) error (strcat ("ClassificationGAM:", ... " 'MaxNumSplitsPerInteraction' must be a", ... " positive integer value.")); endif if (! isempty (InitialLearnRateForPredictors) && (! isnumeric (InitialLearnRateForPredictors) || ! isscalar (InitialLearnRateForPredictors) || InitialLearnRateForPredictors <= 0 || InitialLearnRateForPredictors > 1)) error (strcat ("ClassificationGAM:", ... " 'InitialLearnRateForPredictors' must be", ... " greater than 0 and at most 1.")); endif if (! isempty (InitialLearnRateForInteractions) && (! isnumeric (InitialLearnRateForInteractions) || ! isscalar (InitialLearnRateForInteractions) || InitialLearnRateForInteractions <= 0 || InitialLearnRateForInteractions > 1)) error (strcat ("ClassificationGAM:", ... " 'InitialLearnRateForInteractions' must be", ... " greater than 0 and at most 1.")); endif if (! isempty (Verbose) && (! isnumeric (Verbose) || ! isscalar (Verbose) || Verbose < 0 || fix (Verbose) != Verbose)) error (strcat ("ClassificationGAM: 'Verbose' must be a", ... " non-negative integer value.")); endif if (! isempty (NumPrint) && (! isnumeric (NumPrint) || ! isscalar (NumPrint) || NumPrint < 1 || fix (NumPrint) != NumPrint)) error (strcat ("ClassificationGAM: 'NumPrint' must be a", ... " positive integer value.")); endif if (! isempty (MaxPValue) && (! isnumeric (MaxPValue) || ! isscalar (MaxPValue) || MaxPValue < 0 || MaxPValue > 1)) error (strcat ("ClassificationGAM: 'MaxPValue' must be", ... " between 0 and 1.")); endif if (! isempty (STin)) this.ScoreTransform = STin; endif if (! isempty (args)) error ("ClassificationGAM: invalid optional paired argument."); endif ## An argument belongs to one engine or the other, and asking for one ## the chosen engine cannot honour is refused rather than ignored. The ## alternative is the trap MATLAB sets with a name it accepts and never ## reads: the caller gets a fit that quietly disregarded what it asked ## for. ## An option was given when it is not empty, its default being filled ## in below. splineOnly = {'knots', Knots; 'order', Order; 'dof', DoF; ... 'formula', Formula; 'learningrate', LearningRate; ... 'numiterations', NumIterations}; treeOnly = {'numtreesperpredictor', NumTreesPerPredictor; ... 'numtreesperinteraction', NumTreesPerInteraction; ... 'maxnumsplitsperpredictor', MaxNumSplitsPerPredictor; ... 'maxnumsplitsperinteraction', MaxNumSplitsPerInteraction; ... 'initiallearnrateforpredictors', ... InitialLearnRateForPredictors; ... 'initiallearnrateforinteractions', ... InitialLearnRateForInteractions; ... 'maxpvalue', MaxPValue; 'verbose', Verbose; ... 'numprint', NumPrint; 'weights', Weights; ... 'categoricalpredictors', CategoricalPredictors}; if (strcmp (FitMethod, 'boostedtrees')) clash = splineOnly(! cellfun (@isempty, splineOnly(:,2)), 1); if (! isempty (clash)) error (strcat ("ClassificationGAM: '", clash{1}, "' is a", ... " parameter of the spline engine and cannot be", ... " used with 'FitMethod' 'boostedtrees'.")); endif else clash = treeOnly(! cellfun (@isempty, treeOnly(:,2)), 1); if (! isempty (clash)) error (strcat ("ClassificationGAM: '", clash{1}, "' is a", ... " parameter of the boosted-tree engine and cannot", ... " be used with 'FitMethod' 'splines'.")); endif endif ## A model is described either by a formula or by its interactions if (! isempty (Formula) && ! isempty (Interactions)) error (strcat ("ClassificationGAM: 'Formula' and 'Interactions'", ... " cannot be given together.")); endif ## Any two of 'Knots', 'Order' and 'DoF' determine the third, DoF being ## Knots plus Order; one given alone keeps the default of another. if (! isempty (Knots) && ! isempty (Order) && ! isempty (DoF)) error (strcat ("ClassificationGAM: at most two of 'Knots',", ... " 'Order' and 'DoF' may be given.")); endif if (isempty (DoF)) if (isempty (Knots)) Knots = ones (1, ndims_X) * 5; endif if (isempty (Order)) Order = ones (1, ndims_X) * 3; endif DoF = Knots + Order; elseif (isempty (Knots)) if (isempty (Order)) Order = ones (1, ndims_X) * 3; endif Knots = DoF - Order; else Order = DoF - Knots; endif ## The defaults of the remaining engine options if (isempty (LearningRate)) LearningRate = 0.1; endif if (isempty (NumIterations)) NumIterations = 100; endif if (isempty (NumTreesPerPredictor)) NumTreesPerPredictor = 300; endif if (isempty (NumTreesPerInteraction)) NumTreesPerInteraction = 100; endif if (isempty (MaxNumSplitsPerPredictor)) MaxNumSplitsPerPredictor = 1; endif if (isempty (MaxNumSplitsPerInteraction)) MaxNumSplitsPerInteraction = 4; endif if (isempty (InitialLearnRateForPredictors)) InitialLearnRateForPredictors = 1; endif if (isempty (InitialLearnRateForInteractions)) InitialLearnRateForInteractions = 1; endif if (isempty (MaxPValue)) MaxPValue = 1; endif if (isempty (Verbose)) Verbose = 0; endif if (isempty (NumPrint)) NumPrint = 10; endif ## Generate default predictors and response variable names (if necessary) if (isempty (PredictorNames)) for i = 1:columns (X) PredictorNames {i} = strcat ("x", num2str (i)); endfor endif if (isempty (ResponseName)) ResponseName = 'Y'; endif ## Assign predictors and response variable names this.PredictorNames = PredictorNames; this.ResponseName = ResponseName; ## Handle class names if (! isempty (ClassNames)) ## The observations of a class ClassNames leaves out are dropped gY(! ismember (gY, namedClasses (glY, ClassNames))) = NaN; endif ## An observation is dropped only when its response is missing. A row ## whose predictors hold missing values is kept and reported as used, ## and the boosted-tree fit uses it, as R2024a does, the row sitting in ## the root of each tree it cannot be split by; the spline fit below ## draws on the complete observations alone. RowsUsed = ! isnan (gY); ## Index the rows and not the elements: a response naming its ## classes in the rows of a character matrix has one column per ## character, and a linear index flattens the names into single ## letters. Every other accepted type is a column, for which ## the two forms agree. Yret = Y(RowsUsed, :); Xret = X(RowsUsed, :); if (isempty (Weights)) Wret = ones (rows (Xret), 1); else Wret = Weights(RowsUsed); Wret = Wret(:); endif ## The weights keep their class in the model; every computation runs on ## them as double. Wclass = class (Wret); Wret = double (Wret); this.X = Xret; this.Y = Yret; if (strcmp (FitMethod, 'boostedtrees')) cobs = true (rows (Xret), 1); else cobs = ! any (isnan (Xret), 2); endif Y = Yret(cobs, :); X = Xret(cobs, :); Wfit = Wret(cobs); ## Renew groups in Y: the classes keep the type of Y, sorted or in the ## order ClassNames gives them. [this.ClassNames, gY] = classOrder (Y, ClassNames, exempt); ## Check that we are dealing only with binary classification if (classCount (this.ClassNames) > 2) error ("ClassificationGAM: can only be used for binary classification."); endif ## Force Y into the 0/1 coding the fitter needs. This used to be done ## only for a response that was not already numeric, so a numeric one ## reached the fitter as it was given: a response of 1 and 2 seeded the ## model with log (mean (Y) / (1 - mean (Y))) at a mean of 1.5, whose ## argument is negative, and the intercept came back NaN with every ## score along with it. gY indexes the sorted class names, so gY - 1 is ## the coding for any binary response and reproduces a 0/1 one exactly. Y = gY - 1; this.NumObservations = rows (this.X); ## RowsUsed is left empty when every observation was used, as in MATLAB if (all (RowsUsed)) this.RowsUsed = []; else this.RowsUsed = RowsUsed; endif ## Assign the number of original predictors to the ClassificationGAM object this.NumPredictors = ndims_X; ## Assign Cost and compute Prior this.Cost = Cost; if (isstruct (Prior)) Prior = priorFromStruct (Prior, this.ClassNames, ... 'ClassificationGAM'); endif if (ischar (Prior)) if (strcmpi (Prior, 'uniform')) this.Prior = [0.5, 0.5]; elseif (strcmpi (Prior, 'empirical')) ## Each class's share of the weight, which is its share of the ## observations when no weights were given. counts = accumarray (gY(:), Wfit, [2, 1]); this.Prior = counts(:)' / sum (counts); endif else ## Numeric prior - normalize to sum to 1 this.Prior = Prior(:)' / sum (Prior); endif ## A class with prior 0 is never predicted, so its observations are ## removed; one that costs nothing to misclassify is kept, as MATLAB's ## learner keeps it gret = labelIndices (this.ClassNames, this.Y); [drop, P, msg] = removedClasses (this.Prior, [], ... this.ClassNames, gret); if (all (drop)) error ("ClassificationGAM: every class has prior probability 0."); elseif (any (drop)) if (! isempty (msg)) warning ("ClassificationGAM: %s", msg); endif keep = ! drop(gret); used = find (RowsUsed); RowsUsed(used(! keep)) = false; this.X = this.X(keep,:); this.Y = this.Y(keep,:); Wret = Wret(keep); kc = keep(cobs); X = X(kc,:); Y = Y(kc); gY = gY(kc); Wfit = Wfit(kc); cobs = cobs(keep); this.NumObservations = rows (this.X); this.RowsUsed = RowsUsed; this.Prior = P; endif ## A scalar 'Knots', 'Order' or 'DoF' applies to every predictor, which ## the validation above accepts but nothing expanded, so the fit indexed ## past the end of a scalar for the second predictor onwards. if (isscalar (Knots)) Knots = repmat (Knots, 1, ndims_X); endif if (isscalar (Order)) Order = repmat (Order, 1, ndims_X); endif DoF = Knots + Order; ## Assign remaining optional parameters this.Formula = Formula; this.Interactions = Interactions; this.Knots = Knots; this.Order = Order; this.DoF = DoF; this.LearningRate = LearningRate; this.NumIterations = NumIterations; this.FitMethod = FitMethod; ## Bookkeeping MATLAB reports alongside the fit, whichever engine ran ## Each class carries its prior, spread over its observations in ## proportion to their weights: the W MATLAB reports and the weight the ## boosted fit sees. Every stored row gets one, those missing a ## predictor included, so that W lines up with X. gYall = labelIndices (this.ClassNames, this.Y); this.W = zeros (this.NumObservations, 1); for k = 1:2 idx = (gYall(:) == k); if (sum (Wret(idx)) > 0) this.W(idx) = this.Prior(k) * Wret(idx) / sum (Wret(idx)); endif endfor this.W = cast (this.W, Wclass); ## A categorical predictor is fitted on the indices of its levels, ## which the engine is handed in place of the values. this.CategoricalPredictors = []; if (! isempty (CategoricalPredictors)) [CP, errmsg] = dummyCoding (X, CategoricalPredictors, ... this.PredictorNames); if (! isempty (errmsg)) error ("ClassificationGAM: %s", errmsg); endif this.CategoricalPredictors = CP.Index; Levels = cell (1, columns (X)); Levels(CP.Index) = CP.Levels(CP.Index); this.TreeModel = struct ('CategoricalLevels', {Levels}); endif this.ExpandedPredictorNames = this.PredictorNames; if (strcmp (FitMethod, 'boostedtrees')) ## The spline parameters describe a scheme that did not run, so they ## are left empty rather than reporting numbers nothing used. this.Knots = []; this.Order = []; this.DoF = []; this.LearningRate = []; this.NumIterations = []; ## The engine wants the response as zeros and ones. Y itself is ## only converted when it was not numeric to begin with, so a numeric ## label of 1 and 2 would arrive unconverted; the group index is the ## encoding that is always right. this = this.fitBoosted (X, gY(:) - 1, Interactions, ... NumTreesPerPredictor, ... NumTreesPerInteraction, ... MaxNumSplitsPerPredictor, ... MaxNumSplitsPerInteraction, ... InitialLearnRateForPredictors, ... InitialLearnRateForInteractions, MaxPValue, ... Verbose, NumPrint); else ## Fit the basic model Inter = mean (Y); [iter, param, res, RSS, intercept] = this.fitGAM (X, Y, Inter, ... Knots, Order, ... LearningRate, ... NumIterations); this.BaseModel.Intercept = intercept; this.BaseModel.Parameters = param; this.BaseModel.Iterations = iter; this.BaseModel.Residuals = res; this.BaseModel.RSS = RSS; this.Intercept = intercept; ## Handle interaction terms (if given) if (! isempty (Formula) || ! isempty (Interactions)) this = this.fitModelwInt (X, Y, Inter, Knots, Order, DoF, ... LearningRate, NumIterations); endif ## The property MATLAB reports is the two-way terms the fitted model ## carries, as predictor index pairs, whatever form they were asked ## for in. The term matrix stays the complete record: it also holds ## the main effects a formula names and any term above two ## predictors, neither of which has a two-column form. this.Interactions = interactionPairs (this.IntMatrix); ## The spline scheme has no tree vocabulary to report, so its ## parameter struct describes itself instead. this.ModelParameters = struct ('Knots', this.Knots, ... 'Order', this.Order, ... 'DoF', this.DoF, ... 'Formula', this.Formula, ... 'Interactions', this.Interactions, ... 'LearningRate', this.LearningRate, ... 'NumIterations', this.NumIterations); endif endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationGAM} {@var{obj} =} addInteractions (@var{obj}, @var{interactions}) ## ## Add interaction terms to a fitted model. ## ## @code{@var{obj} = addInteractions (@var{obj}, @var{interactions})} fits ## the interaction terms named by @var{interactions} on top of the terms ## the model already carries and returns the updated model. The univariate ## fit is left alone, so @code{predict} with ## @qcode{'IncludeInteractions'} set @qcode{false} answers exactly as it ## answered before. ## ## @var{interactions} takes the forms the constructor's ## @qcode{'Interactions'} option takes: a nonnegative integer count of ## terms, a logical matrix with a column per predictor, or @qcode{'all'}. ## ## A model already carrying interaction terms is not extended, which is ## what MATLAB refuses too. A model fitted from a @qcode{'Formula'} names ## every term it has, interactions among them, and is refused for the same ## reason. ## ## Which terms a count selects is this implementation's own: they are ## taken in the order @code{nchoosek} lists the pairs, where MATLAB ranks ## them by how much each contributes. The constructor's option chooses ## the same way, so the two agree with each other. ## ## @seealso{fitcgam, ClassificationGAM} ## @end deftypefn function this = addInteractions (this, interactions) if (nargin != 2) print_usage (); endif ## Which store already holds interaction terms depends on the engine. hasInt = ! isempty (this.IntMatrix); if (strcmp (this.FitMethod, 'boostedtrees') && ! isempty (this.TreeModel)) hasInt = ! isempty (this.TreeModel.Pairs); endif if (hasInt) error (strcat ("ClassificationGAM.addInteractions: adding", ... " interaction terms to a model that already", ... " includes them is not supported.")); endif if (! ((isnumeric (interactions) && isscalar (interactions) && interactions == fix (interactions) && interactions >= 0) || islogical (interactions) || (ischar (interactions) && strcmpi (interactions, 'all')))) error (strcat ("ClassificationGAM.addInteractions: invalid", ... " 'Interactions' parameter.")); endif ## Under the boosted-tree engine the interaction phase simply runs now, ## starting from the predictor phase the model already carries, which is ## the same order the constructor would have run them in. The predictor ## phase is not refitted, so the main effects are untouched and a model ## with interactions added is the model the constructor would have built ## had it been asked for them. if (strcmp (this.FitMethod, 'boostedtrees')) cobs = true (rows (this.X), 1); Xfit = this.X(cobs, :); gY = labelIndices (this.ClassNames, this.Y(cobs, :)); Yfit = gY(:) - 1; MP = this.ModelParameters; lrInter = MP.InitialLearnRateForInteractions; this = this.fitBoostedInteractions (Xfit, Yfit, interactions, ... MP.NumTreesPerInteraction, ... MP.MaxNumSplitsPerInteraction, ... lrInter, MP.MaxPValue); return; endif ## parseInteractions reads the specification from the property, which ## afterwards holds the pairs the fit settled on, exactly as the ## constructor leaves it. this.Interactions = interactions; this.IntMatrix = this.parseInteractions (); ## The fit sees the complete observations and the response in the ## coding the boosting works in, prepared as the constructor prepares ## them. Knots, Order and DoF are held unexpanded on the object and ## are widened to the interaction columns by fitModelwInt. cobs = ! any (isnan (this.X), 2); Xfit = this.X(cobs, :); gY = labelIndices (this.ClassNames, this.Y(cobs, :)); Yfit = gY(:) - 1; this = this.fitModelwInt (Xfit, Yfit, mean (Yfit), this.Knots, ... this.Order, this.DoF, this.LearningRate, ... this.NumIterations); this.Interactions = interactionPairs (this.IntMatrix); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationGAM} {@var{label} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {ClassificationGAM} {[@var{label}, @var{score}] =} predict (@var{obj}, @var{XC}) ## @deftypefnx {ClassificationGAM} {[@var{label}, @var{score}] =} predict (@dots{}, @qcode{'IncludeInteractions'}, @var{includeInteractions}) ## ## Predict labels for new data using the Generalized Additive Model (GAM) ## stored in a ClassificationGAM object. ## ## @code{@var{label} = predict (@var{obj}, @var{XC})} returns the predicted ## labels for the data in @var{XC} based on the model stored in the ## ClassificationGAM object, @var{obj}. ## ## @code{[@var{label}, @var{score}] = predict (@var{obj}, @var{XC})} also ## returns @var{score}, which contains the predicted class scores or ## posterior probabilities for each observation. Every row is predicted. ## Under boosted trees a missing value adds nothing from a main effect, ## and an interaction term takes the value its trees give a row missing ## that predictor; under splines a row holding a missing value is scored ## @code{NaN} and takes the class of largest prior. ## ## @code{[@var{label}, @var{score}] = predict (@var{obj}, @var{XC}, ## 'IncludeInteractions', @var{includeInteractions})} allows you to specify ## whether interaction terms should be included when making predictions. ## ## @itemize ## @item ## @var{obj} must be a @qcode{ClassificationGAM} class object. ## @item ## @var{XC} must be an @math{M*P} numeric matrix where each row is an ## observation and each column corresponds to a predictor variable. ## @item ## @var{includeInteractions} is a logical scalar indicating whether to ## include interaction terms in the predictions. ## @end itemize ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: one ## the model was not fitted on is passed over, one it needs and cannot ## find is named, and a value holding a level is coded as that level ## was coded at fitting. ## @seealso{ClassificationGAM, fitcgam} ## @end deftypefn function [labels, scores] = predict (this, XC, varargin) ## Check for sufficient input arguments if (nargin < 2) error ("ClassificationGAM.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'ClassificationGAM.predict', XC); ## Check for valid XC if (isempty (XC)) error ("ClassificationGAM.predict: XC is empty."); elseif (this.NumPredictors != columns (XC)) error (strcat ("ClassificationGAM.predict:", ... " XC must have the same number of", ... " predictors as the trained model.")); endif ## Default values for Name-Value Pairs ## Which store holds the interaction terms depends on the engine: the ## spline scheme keeps them as extra columns described by IntMatrix, ## the boosted-tree scheme as surfaces over predictor pairs. hasInt = ! isempty (this.IntMatrix); if (strcmp (this.FitMethod, 'boostedtrees') && ! isempty (this.TreeModel)) hasInt = ! isempty (this.TreeModel.Pairs); endif incInt = hasInt; Cost = this.Cost; ## Parse optional paired arguments; interactions are included when the ## model has them if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationGAM.predict: optional arguments must", ... " be given in Name-Value pairs.")); endif optNames = {'IncludeInteractions'}; dfValues = {incInt}; [incInt, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! islogical (incInt) || (incInt != 0 && incInt != 1)) error (strcat ("ClassificationGAM.predict: includeinteractions", ... " must be a logical value.")); endif if (incInt && ! hasInt) error (strcat ("ClassificationGAM.predict: trained model does not", ... " include any interactions.")); endif if (! isempty (args)) error ("ClassificationGAM.predict: invalid optional paired argument."); endif ## The boosted-tree engine keeps its fit as step functions over bins, ## so a term is a lookup rather than a spline evaluation and the whole ## prediction is one call. It shares everything after it: the cost ## matrix, the label, and the transform. ## An empty TreeModel means no tree fit is present, whatever FitMethod ## says: a default-constructed object has one, and so would a model ## saved before the engine existed. Such an object falls through to ## the spline path rather than indexing into nothing. if (strcmp (this.FitMethod, 'boostedtrees') && ! isempty (this.TreeModel)) ## Excluding the interactions means excluding the constant they ## handed the intercept as well. [Xb, E] = gamCatCode (this.TreeModel, XC, this.BinEdges); interc = this.Intercept; if (! incInt && isfield (this.TreeModel, 'PairIntercept')) interc = interc - this.TreeModel.PairIntercept; endif if (! incInt || isempty (this.TreeModel.Pairs)) scores = gamboostpredict (E, ... this.TreeModel.ShapeValues, Xb, ... interc); else [PE, PM] = gamPairEdges (this.TreeModel, this.PairDetectionBinEdges); scores = gamboostpredict (E, ... this.TreeModel.ShapeValues, Xb, ... interc, 0, PE, ... this.TreeModel.PairValues, ... this.TreeModel.Pairs, PM); endif scores = [-scores, scores]; post = 1 ./ (1 + exp (-scores)); numObservations = size (XC, 1); CE = zeros (numObservations, 2); for k = 1:2 for i = 1:2 CE(:, k) = CE(:, k) + post(:, i) * Cost(k, i); endfor endfor [~, minIdx] = min (CE, [], 2); labels = labelsFromIndex (this.ClassNames, minIdx); scores = this.STfun (scores); return; endif ## Choose whether interactions must be included if (incInt) ## Which construction path the model took: an interaction ## list appends its terms to the predictors, a formula ## names every term the model has and replaces them. if (isempty (this.Formula)) ## Append interaction terms to the predictor matrix for i = 1:rows (this.IntMatrix) tindex = logical (this.IntMatrix(i,:)); Xterms = XC(:,tindex); Xinter = ones (rows (XC), 1); for c = 1:sum (tindex) Xinter = Xinter .* Xterms(:,c); endfor ## Append interaction terms XC = [XC, Xinter]; endfor else ## Add selected predictors and interaction terms XN = []; for i = 1:rows (this.IntMatrix) tindex = logical (this.IntMatrix(i,:)); Xterms = XC(:,tindex); Xinter = ones (rows (XC), 1); for c = 1:sum (tindex) Xinter = Xinter .* Xterms(:,c); endfor ## Append selected predictors and interaction terms XN = [XN, Xinter]; endfor XC = XN; endif ## Get parameters and intercept vectors from model with interactions params = this.ModelwInt.Parameters; Interc = this.ModelwInt.Intercept; else ## Get parameters and intercept vectors from base model params = this.BaseModel.Parameters; Interc = this.BaseModel.Intercept; endif ## Predict the raw score from testing data scores = predict_val (params, XC, Interc); ## Expected misclassification cost is defined on the posteriors, which ## the score becomes only under the logistic link, so the label is ## taken from those and not from the score the caller is handed. post = 1 ./ (1 + exp (-scores)); ## Compute the expected misclassification cost matrix numObservations = size (XC, 1); CE = zeros (numObservations, 2); for k = 1:2 for i = 1:2 CE(:, k) = CE(:, k) + post(:, i) * Cost(k, i); endfor endfor ## Select the class with the minimum expected misclassification cost. ## A spline term has no value at a missing predictor, so such a row has ## no score, and takes the class of largest prior as other learners do. [~, minIdx] = min (CE, [], 2); [~, top] = max (this.Prior); minIdx(isnan (scores(:,1))) = top; labels = labelsFromIndex (this.ClassNames, minIdx); ## Apply ScoreTransform scores = this.STfun (scores); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationGAM} {@var{CVMdl} =} crossval (@var{obj}) ## @deftypefnx {ClassificationGAM} {@var{CVMdl} =} crossval (@dots{}, @var{name}, @var{value}) ## ## Cross Validate a Generalized Additive Model classification object. ## ## @code{@var{CVMdl} = crossval (@var{obj})} returns a cross-validated model ## object, @var{CVMdl}, from a trained model, @var{obj}, using 10-fold ## cross-validation by default. ## ## @code{@var{CVMdl} = crossval (@var{obj}, @var{name}, @var{value})} ## specifies additional name-value pair arguments to customize the ## cross-validation process. ## ## @multitable @columnfractions 0.28 0.7 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'KFold'} @tab Specify the number of folds to use in ## k-fold cross-validation. @code{"KFold", @var{k}}, where @var{k} is an ## integer greater than 1. ## ## @item @qcode{'Holdout'} @tab Specify the fraction of the data to ## hold out for testing. @code{"Holdout", @var{p}}, where @var{p} is a ## scalar in the range @math{(0,1)}. ## ## @item @qcode{'Leaveout'} @tab Specify whether to perform ## leave-one-out cross-validation. @code{"Leaveout", @var{Value}}, where ## @var{Value} is 'on' or 'off'. ## ## @item @qcode{'CVPartition'} @tab Specify a @qcode{cvpartition} ## object used for cross-validation. @code{"CVPartition", @var{cv}}, where ## @code{isa (@var{cv}, "cvpartition")} = 1. ## ## @end multitable ## ## @seealso{fitcgam, ClassificationGAM, cvpartition, ## ClassificationPartitionedModel} ## @end deftypefn function CVMdl = crossval (this, varargin) ## Check input if (nargin < 1) error ("ClassificationGAM.crossval: too few input arguments."); endif if (numel (varargin) == 1) error (strcat ("ClassificationGAM.crossval: Name-Value", ... " arguments must be in pairs.")); elseif (numel (varargin) > 2) error (strcat ("ClassificationGAM.crossval: specify only", ... " one of the optional Name-Value paired arguments.")); endif ## Add default values if (this.NumObservations < 10) numFolds = this.NumObservations; else numFolds = 10; endif Holdout = []; Leaveout = 'off'; CVPartition = []; ## Parse extra parameters while (numel (varargin) > 0) switch (tolower (varargin {1})) case 'kfold' numFolds = varargin{2}; if (! (isnumeric (numFolds) && isscalar (numFolds) && (numFolds == fix (numFolds)) && numFolds > 1)) error (strcat ("ClassificationGAM.crossval: 'KFold'", ... " must be an integer value greater than 1.")); endif case 'holdout' Holdout = varargin{2}; if (! (isnumeric (Holdout) && isscalar (Holdout) && Holdout > 0 && Holdout < 1)) error (strcat ("ClassificationGAM.crossval: 'Holdout'", ... " must be a numeric value between 0 and 1.")); endif case 'leaveout' Leaveout = varargin{2}; if (! (ischar (Leaveout) && (strcmpi (Leaveout, 'on') || strcmpi (Leaveout, 'off')))) error (strcat ("ClassificationGAM.crossval: 'Leaveout'", ... " must be either 'on' or 'off'.")); endif case 'cvpartition' CVPartition = varargin{2}; if (! (isa (CVPartition, 'cvpartition'))) error (strcat ("ClassificationGAM.crossval: 'CVPartition'",... " must be a 'cvpartition' object.")); endif otherwise error (strcat ("ClassificationGAM.crossval: invalid",... " parameter name in optional paired arguments.")); endswitch varargin(1:2) = []; endwhile ## Determine the cross-validation method to use. The partition covers ## the observations actually trained on: a row dropped for a missing ## value is not one the folds can use, and including it would leave the ## partition, the stored data and NumObservations disagreeing. The ## response is passed rather than a count so the folds stay stratified. Yused = this.Y; if (! isempty (CVPartition)) partition = CVPartition; elseif (! isempty (Holdout)) partition = cvpartition (Yused, 'Holdout', Holdout); elseif (strcmpi (Leaveout, 'on')) partition = cvpartition (this.NumObservations, 'LeaveOut'); else partition = cvpartition (Yused, 'KFold', numFolds); endif ## Create a cross-validated model object CVMdl = ClassificationPartitionedModel (this, partition); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationGAM} {@var{CVMdl} =} compact (@var{obj}) ## ## Create a CompactClassificationGAM object. ## ## @code{@var{CVMdl} = compact (@var{obj})} creates a compact version of the ## ClassificationGAM object, @var{obj}. ## ## @seealso{fitcgam, ClassificationGAM, CompactClassificationGAM} ## @end deftypefn function CVMdl = compact (this) ## Create a compact model CVMdl = CompactClassificationGAM (this); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationGAM} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationGAM} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationGAM} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margin of a generalized additive model. ## ## @code{@var{m} = margin (@var{obj}, @var{X}, @var{Y})} returns a column ## vector holding, for each row of @var{X}, the score the model gives its ## true class in @var{Y} less the score it gives the other class. A ## positive margin means the observation is classified correctly, and the ## larger it is the more confidently so. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{ClassificationGAM, ClassificationGAM.edge, ## ClassificationGAM.loss, ClassificationGAM.predict} ## @end deftypefn function m = margin (this, X, Y) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationGAM.margin: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y] = tableResponse (this, 'margin', X, Y, {}, nargin > 2); [X, Y] = checkXY_ (this, X, Y, "margin"); [~, scores] = predict (this, X); classes = this.ClassNames; m = zeros (rows (X), 1); ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:rows (X) idx = gYidx(i); if (isempty (idx)) m(i) = NaN; continue; endif true_score = scores(i, idx); scores(i, idx) = -Inf; m(i) = true_score - max (scores(i,:)); scores(i, idx) = true_score; endfor endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationGAM} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationGAM} {@var{e} =} edge (@dots{}, @qcode{"Weights"}, @var{w}) ## @deftypefnx {ClassificationGAM} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationGAM} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Classification edge of a generalized additive model. ## ## @code{@var{e} = edge (@var{obj}, @var{X}, @var{Y})} returns the mean of ## the classification margins over the rows of @var{X}. ## ## @code{@var{e} = edge (@dots{}, @qcode{"Weights"}, @var{w})} takes the ## weighted mean instead, with one weight per row of @var{X}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{ClassificationGAM, ClassificationGAM.margin, ## ClassificationGAM.loss, ClassificationGAM.predict} ## @end deftypefn function e = edge (this, X, Y, varargin) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationGAM.edge: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'edge', X, Y, varargin, ... nargin > 2); [X, Y] = checkXY_ (this, X, Y, "edge"); ## The weights are normalized within each class to that class's prior, ## which is what the oracle does and is not the same as dividing by ## their total. This used to divide by the total. ## The weights are parsed before anything is computed, so a bad ## Name-Value pair is reported as such rather than after a margin. W = edgeWeights (varargin, Y, this.ClassNames, this.Prior, ... "ClassificationGAM", "edge"); m = margin (this, X, Y); e = sum (W .* m(:)) / sum (W); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationGAM} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationGAM} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationGAM} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {ClassificationGAM} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Classification loss of a generalized additive model. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} returns the loss of ## the model on the rows of @var{X} against the true labels @var{Y}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{L} = loss (@dots{}, @var{name}, @var{value})} accepts the ## following name-value pairs: ## ## @itemize ## @item ## @qcode{"LossFun"} selects the loss. Supported values are ## @qcode{"mincost"}, the default, @qcode{"binodeviance"}, ## @qcode{"classifcost"}, @qcode{"classiferror"}, @qcode{"exponential"}, ## @qcode{"hinge"}, @qcode{"logit"} and @qcode{"quadratic"}. ## @qcode{"mincost"} assigns each observation to the class of least ## expected cost and charges what that assignment costs, so it reads the ## scores as a posterior, which is what this model returns; ## @qcode{"classifcost"} charges what the model's own prediction costs. ## ## @item ## @qcode{"Weights"} holds one weight per row of @var{X}, normalised to ## sum to one before it is applied. ## @end itemize ## ## @seealso{ClassificationGAM, ClassificationGAM.margin, ## ClassificationGAM.edge, ClassificationGAM.predict} ## @end deftypefn function L = loss (this, X, Y, varargin) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationGAM.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationGAM.loss: Name-Value arguments", ... " must be in pairs.")); endif [X, Y] = checkXY_ (this, X, Y, "loss"); ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights'}; dfValues = {'mincost', []}; [LossFun, W, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments lossnames = {'binodeviance', 'classifcost', 'classiferror', ... 'exponential', 'hinge', 'logit', 'mincost', 'quadratic'}; if (! (ischar (LossFun) && isrow (LossFun))) error ("ClassificationGAM.loss: 'LossFun' must be a character vector."); endif LossFun = tolower (LossFun); if (! any (strcmpi (LossFun, lossnames))) error ("ClassificationGAM.loss: unsupported Loss function."); endif errmsg = weightsClass (W); if (! isempty (errmsg)) error ("ClassificationGAM.loss: %s", errmsg); endif if (! isempty (W) && ! (isnumeric (W) && isvector (W))) error ("ClassificationGAM.loss: 'Weights' must be a numeric vector."); endif if (! isempty (W) && numel (W) != rows (X)) error (strcat ("ClassificationGAM.loss: size of 'Weights' must", ... " equal the number of rows in X.")); endif if (! isempty (args)) error ("ClassificationGAM.loss: invalid optional paired argument."); endif if (isempty (W)) W = ones (rows (X), 1); endif W = double (W(:)); W = W / sum (W); [label, scores] = predict (this, X); classes = this.ClassNames; ## Membership of the true class, as an indicator per class Yind = zeros (rows (X), classCount (classes)); ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:rows (X) idx = gYidx(i); if (isempty (idx)) L = NaN; return; endif Yind(i, idx) = 1; endfor ## The scalar score of the true class of each observation mj = sum (scores .* Yind, 2); switch (LossFun) case 'classiferror' wrong = zeros (rows (X), 1); for i = 1:rows (X) wrong(i) = ! isequal (label(i), Y(i)); endfor L = sum (W .* wrong); case 'binodeviance' L = sum (W .* log (1 + exp (-2 * mj))); case 'hinge' L = sum (W .* max (0, 1 - mj)); case 'exponential' L = sum (W .* exp (-mj)); case 'logit' L = sum (W .* log (1 + exp (-mj))); case 'quadratic' L = sum (W .* (1 - mj) .^ 2); case 'mincost' ## Each observation is assigned to the class of least expected ## cost, and charged what that assignment actually costs given its ## true class. L = 0; ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:rows (X) [~, k] = min (scores(i,:) * this.Cost); true_idx = gYidx(i); L = L + W(i) * this.Cost(true_idx, k); endfor case 'classifcost' ## What the model's own prediction costs, given the true class L = 0; ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:rows (X) true_idx = gYidx(i); pred_idx = find (ismember (classes, label(i))); L = L + W(i) * this.Cost(true_idx, pred_idx); endfor endswitch endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationGAM} {@var{label} =} resubPredict (@var{obj}) ## @deftypefnx {ClassificationGAM} {[@var{label}, @var{score}] =} resubPredict (@var{obj}) ## ## Classify the training data with the generalized additive model it was ## fitted on. ## ## @code{@var{label} = resubPredict (@var{obj})} is @code{predict} applied ## to the observations the model was fitted on. ## ## @seealso{ClassificationGAM, ClassificationGAM.predict} ## @end deftypefn function [labels, scores] = resubPredict (this) used = true (rows (this.X), 1); [labels, scores] = predict (this, this.X(used, :)); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationGAM} {@var{m} =} resubMargin (@var{obj}) ## ## Classification margin of a generalized additive model on its training ## data. ## ## @seealso{ClassificationGAM, ClassificationGAM.margin} ## @end deftypefn function m = resubMargin (this) used = true (rows (this.X), 1); m = margin (this, this.X(used, :), this.Y(used)); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationGAM} {@var{e} =} resubEdge (@var{obj}) ## ## Classification edge of a generalized additive model on its training ## data. ## ## @seealso{ClassificationGAM, ClassificationGAM.edge} ## @end deftypefn function e = resubEdge (this) used = true (rows (this.X), 1); e = edge (this, this.X(used, :), this.Y(used)); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationGAM} {@var{L} =} resubLoss (@var{obj}) ## @deftypefnx {ClassificationGAM} {@var{L} =} resubLoss (@dots{}, @var{name}, @var{value}) ## ## Classification loss of a generalized additive model on its training ## data. ## ## @seealso{ClassificationGAM, ClassificationGAM.loss} ## @end deftypefn function L = resubLoss (this, varargin) used = true (rows (this.X), 1); L = loss (this, this.X(used, :), this.Y(used), varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationGAM} {} savemodel (@var{obj}, @var{filename}) ## ## Save a ClassificationGAM object. ## ## @code{savemodel (@var{obj}, @var{filename})} saves each property of a ## ClassificationGAM object into an Octave binary file, the name of which is ## specified in @var{filename}, along with an extra variable, which defines ## the type classification object these variables constitute. Use ## @code{loadmodel} in order to load a classification object into Octave's ## workspace. ## ## @seealso{loadmodel, fitcgam, ClassificationGAM} ## @end deftypefn function savemodel (this, fname) if (nargin < 2) error ("ClassificationGAM.savemodel: too few input arguments."); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error ("ClassificationGAM.savemodel: FNAME must be a character vector."); endif ## Generate variable for class name classdef_name = 'ClassificationGAM'; ## Create variables from model properties X = this.X; Y = encodeLabels (this.Y); NumObservations = this.NumObservations; RowsUsed = this.RowsUsed; NumPredictors = this.NumPredictors; PredictorNames = this.PredictorNames; BinEdges = this.BinEdges; ResponseName = this.ResponseName; ClassNames = encodeLabels (this.ClassNames); Prior = this.Prior; Cost = this.Cost; ScoreTransform = this.ScoreTransform; Formula = this.Formula; Interactions = this.Interactions; Knots = this.Knots; Order = this.Order; DoF = this.DoF; BaseModel = this.BaseModel; ModelwInt = this.ModelwInt; IntMatrix = this.IntMatrix; Intercept = this.Intercept; W = this.W; CategoricalPredictors = this.CategoricalPredictors; ExpandedPredictorNames = this.ExpandedPredictorNames; STfun = this.STfun; FitMethod = this.FitMethod; TreeModel = this.TreeModel; ModelParameters = this.ModelParameters; ReasonForTermination = this.ReasonForTermination; PairDetectionBinEdges = this.PairDetectionBinEdges; ## Save classdef name and all model properties as individual variables LearningRate = this.LearningRate; NumIterations = this.NumIterations; HyperparameterOptimizationResults = this.HyperparameterOptimizationResults; save ('-binary', fname, 'classdef_name', 'X', 'Y', 'NumObservations', ... 'RowsUsed', 'NumPredictors', 'PredictorNames', 'BinEdges', ... 'ResponseName', ... 'ClassNames', 'Prior', 'Cost', 'ScoreTransform', 'Formula', ... 'Interactions', 'Knots', 'Order', 'DoF', 'BaseModel', ... 'ModelwInt', 'IntMatrix', 'LearningRate', 'NumIterations', ... 'Intercept', 'W', 'CategoricalPredictors', ... 'ExpandedPredictorNames', 'STfun', 'FitMethod', ... 'TreeModel', 'ModelParameters', 'ReasonForTermination', ... 'PairDetectionBinEdges', ... 'HyperparameterOptimizationResults'); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationGAM} {@var{Mdl} =} resume (@var{obj}, @var{numTrees}) ## ## Resume training a generalized additive model. ## ## @code{@var{Mdl} = resume (@var{obj}, @var{numTrees})} adds ## @var{numTrees} more trees to @var{obj} and returns the result. The ## original model is not modified. ## ## Training continues in the phase that ran last, which is what MATLAB ## does: a model carrying interaction terms gains interaction trees and ## its predictor shape functions are left alone, while a model without ## them gains predictor trees. A round starts at its initial learning ## rate whatever its number, so the model this returns is the model a ## single fit of the combined budget would have produced. ## ## @var{numTrees} must be a positive integer scalar. Resuming raises ## where there is nothing left to gain, rather than returning the model ## unchanged, and it is not available under ## @qcode{'FitMethod', 'splines'}: a backfit that has converged to its ## tolerance has no budget to extend. ## ## @seealso{ClassificationGAM, fitcgam, ClassificationGAM.addInteractions} ## @end deftypefn function Mdl = resume (this, numTrees) if (nargin < 2) error ("ClassificationGAM.resume: Not enough input arguments."); endif if (! strcmp (this.FitMethod, 'boostedtrees')) error (strcat ("ClassificationGAM.resume: resuming is available", ... " only under 'FitMethod', 'boostedtrees'; a spline", ... " backfit stops at its tolerance and has no budget", ... " to extend.")); endif if (! (isnumeric (numTrees) && isscalar (numTrees) && isreal (numTrees) && numTrees > 0 && fix (numTrees) == numTrees)) error (strcat ("ClassificationGAM.resume: NUMTREES must be a", ... " positive integer scalar.")); endif ## The rows the fit saw, all of them, coded as the engine takes them. cobs = true (rows (this.X), 1); [X, E, cat] = gamCatCode (this.TreeModel, this.X(cobs, :), ... this.BinEdges); gY = labelIndices (this.ClassNames, this.Y(cobs, :)); Y = gY(:) - 1; Mdl = this; MP = this.ModelParameters; reason = this.ReasonForTermination; ntrees = this.NumTrainedTrees; if (isempty (this.TreeModel.Pairs)) ## No interaction phase ever ran, so the predictor phase is the one ## still open. The engine is handed the prediction reached so far and ## returns the increment to add to it. f = gamboostpredict (E, this.TreeModel.ShapeValues, X, ... this.Intercept); M = gamboosttrain (X, Y, 1, numTrees, ... MP.InitialLearnRateForPredictors, ... MP.MaxNumSplitsPerPredictor, 0, MP.NumPrint, ... f(:), double (this.W(cobs)), cat); if (M.NumTrees == 0) error (strcat ("ClassificationGAM.resume: unable to resume", ... " training because the software was unable to", ... " improve the model fit.")); endif sv = this.TreeModel.ShapeValues; for j = 1:numel (sv) sv{j} = sv{j} + M.ShapeValues{j}; endfor Mdl.TreeModel.ShapeValues = sv; Mdl.Intercept = this.Intercept + M.Intercept; MP.NumTreesPerPredictor = MP.NumTreesPerPredictor + M.NumTrees; reason.PredictorTrees = M.ReasonForTermination; ntrees.PredictorTrees = ntrees.PredictorTrees + M.NumTrees; else ## The interaction phase ran last, so it is the one extended. The ## running prediction includes the surfaces already fitted, and the ## new ones are added to them. [PE, PM] = gamPairEdges (this.TreeModel, this.PairDetectionBinEdges); f = gamboostpredict (E, this.TreeModel.ShapeValues, X, ... this.Intercept, 0, PE, ... this.TreeModel.PairValues, ... this.TreeModel.Pairs, PM); I = gamboostinter (X, Y, f(:), 1, this.TreeModel.Pairs, numTrees, ... MP.InitialLearnRateForInteractions, ... MP.MaxNumSplitsPerInteraction, ... double (this.W(cobs)), cat); if (I.NumTrees == 0) error (strcat ("ClassificationGAM.resume: unable to resume", ... " training because the software was unable to", ... " improve the model fit.")); endif ## The new trees cut where they cut, so the surfaces are added on the ## union of both grids. [pe, pv, pm] = gamPairMerge (PE, this.TreeModel.PairValues, PM, ... I.PairEdges, I.PairValues, ... I.PairMissing); Mdl.TreeModel.PairEdges = pe; Mdl.TreeModel.PairValues = pv; Mdl.TreeModel.PairMissing = pm; Mdl.TreeModel.PairIntercept = this.TreeModel.PairIntercept ... + I.Intercept; Mdl.Intercept = this.Intercept + I.Intercept; MP.NumTreesPerInteraction = MP.NumTreesPerInteraction + I.NumTrees; reason.InteractionTrees = I.ReasonForTermination; ntrees.InteractionTrees = ntrees.InteractionTrees + I.NumTrees; endif Mdl.ModelParameters = MP; Mdl.ReasonForTermination = reason; Mdl.NumTrainedTrees = ntrees; endfunction endmethods methods(Static, Hidden) function mdl = load_model (filename, data) data = decodeLabels (data); ## Create a ClassificationGAM object mdl = ClassificationGAM (1, 1); ## Copy the saved data into the object. Iterate over what was ## saved rather than over fieldnames (mdl): a private property such ## as STfun is written out by savemodel but is not reported by ## fieldnames, so comparing the two sets could never match and every ## load failed. Assignment is legal here because this is a method of ## the class itself. names = fieldnames (data); ## The set methods for these read other properties, and one of them ## rebuilds Coeffs, so they are assigned once everything else is in ## place rather than in the order the file happens to list them. late = ismember (names, {'Cost', 'Prior', 'ScoreTransform', ... 'ResponseTransform'}); names = [names(! late); names(late)]; for i = 1:numel (names) try mdl.(names{i}) = data.(names{i}); catch error ("ClassificationGAM.load_model: invalid model in '%s'.", filename) end_try_catch endfor ## A model written before RowsUsed became a mask stored it as a ## double, which is a valid subscript for nothing. An empty RowsUsed ## means every observation was used and stays an empty double. if (! isempty (mdl.RowsUsed)) mdl.RowsUsed = logical (mdl.RowsUsed); endif endfunction endmethods ## Helper functions methods(Access = private) ## Shared validation for the assessment methods, so each reports under ## its own name. function [X, Y] = checkXY_ (this, X, Y, caller) if (isempty (X)) error ("ClassificationGAM.%s: X is empty.", caller); elseif (this.NumPredictors != columns (X)) error (strcat ("ClassificationGAM.%s: X must have the same number", ... " of predictors as the trained model."), caller); endif if (isempty (Y)) error ("ClassificationGAM.%s: Y is empty.", caller); elseif (rows (X) != rows (Y)) error (strcat ("ClassificationGAM.%s: Y must have the same number", ... " of rows as X."), caller); endif endfunction ## Determine interactions from Interactions optional parameter ## Fit the model that carries the interaction terms. The constructor and ## Run the interaction phase alone, over a model whose predictor phase is ## already fitted. Shared by addInteractions and by the constructor, so ## that asking for interactions after the fact gives the same model as ## asking for them up front. function this = fitBoostedInteractions (this, X, Y, Interactions, NTI, ... MSI, LRI, MaxPValue) [X, E, cat] = gamCatCode (this.TreeModel, X, this.BinEdges); f = gamboostpredict (E, this.TreeModel.ShapeValues, X, ... this.Intercept); Wfit = double (this.W); ## Residuals of the predictor phase, which is what pairs are tested on. res = Y - 1 ./ (1 + exp (-f)); wanted = -1; pairs = zeros (0, 2); if (ischar (Interactions)) wanted = Inf; elseif (isscalar (Interactions) && ! isempty (Interactions)) wanted = Interactions; elseif (! isempty (Interactions)) pairs = interactionPairs (logical (Interactions)); endif if (wanted > 0 && columns (X) > 1) S = gamboostpairs (X, res, cat); pval = fcdf (S.F, S.DF1, S.DF2, 'upper'); pval(S.DF1 <= 0) = 1; [pval, ord] = sort (pval); ranked = S.Pairs(ord, :); ranked = ranked(pval <= MaxPValue, :); if (isfinite (wanted) && rows (ranked) > wanted) ranked = ranked(1:wanted, :); endif pairs = ranked; ## A pair tree allowed a single split can only cut along one ## predictor, which is a main effect and no interaction, so no pair is ## kept. R2024a selects none and warns as below. if (MSI < 2) pairs = zeros (0, 2); endif this.PairDetectionBinEdges = S.BinEdges(:); this.PairDetectionBinEdges(cat) = {[]}; if (isempty (pairs)) warning (strcat ("ClassificationGAM: model does not include", ... " interaction terms because all interaction", ... " terms have p-values greater than the", ... " 'MaxPValue' value, or the software was unable", ... " to improve the model fit.")); endif endif reason = this.ReasonForTermination; ntrees = this.NumTrainedTrees; if (! isempty (pairs)) I = gamboostinter (X, Y, f, 1, pairs, NTI, LRI, MSI, Wfit, cat); ## A pair tree splitting on one predictor alone is a main effect and ## adds nothing, so a fit made only of such trees keeps no pair, as ## R2024a keeps none. if (I.NumTrees == 0) pairs = zeros (0, 2); warning (strcat ("ClassificationGAM: model does not include", ... " interaction terms because all interaction", ... " terms have p-values greater than the", ... " 'MaxPValue' value, or the software was unable", ... " to improve the model fit.")); endif endif if (! isempty (pairs)) this.Intercept = this.Intercept + I.Intercept; if (wanted <= 0) this.PairDetectionBinEdges = I.PairBinEdges(:); this.PairDetectionBinEdges(cat) = {[]}; endif this.TreeModel.PairValues = I.PairValues; this.TreeModel.PairEdges = I.PairEdges; this.TreeModel.PairMissing = I.PairMissing; this.TreeModel.PairIntercept = I.Intercept; reason.InteractionTrees = I.ReasonForTermination; ntrees.InteractionTrees = I.NumTrees; endif this.TreeModel.Pairs = pairs; if (isempty (pairs)) this.TreeModel.PairIntercept = 0; endif this.Interactions = pairs; this.ReasonForTermination = reason; this.NumTrainedTrees = ntrees; MP = this.ModelParameters; if (ischar (Interactions)) MP.Interactions = Interactions; elseif (isempty (Interactions)) MP.Interactions = 0; else MP.Interactions = Interactions; endif this.ModelParameters = MP; endfunction ## Drive the boosted-tree engine: the predictor phase, then a search for ## interactions worth adding, then the interaction phase over whichever ## pairs survived. The two phases share a running fit, so the second ## continues from the prediction the first left rather than starting over, ## which is how MATLAB's own trace behaves. function this = fitBoosted (this, X, Y, Interactions, NTP, NTI, MSP, ... MSI, LRP, LRI, MaxPValue, Verb, NPrint) ## The predictor phase, weighted by W over the rows the fit sees. Wfit = double (this.W); [X, ~, cat, Levels] = gamCatCode (this.TreeModel, X); M = gamboosttrain (X, Y, 1, NTP, LRP, MSP, Verb, NPrint, [], Wfit, ... cat); f = gamboostpredict (M.BinEdges, M.ShapeValues, X, M.Intercept); this.BinEdges = M.BinEdges(:); ## a column cell, as MATLAB reports it this.BinEdges(cat) = {[]}; this.Intercept = M.Intercept; reason = struct ('PredictorTrees', M.ReasonForTermination, ... 'InteractionTrees', ''); ntrees = struct ('PredictorTrees', M.NumTrees, ... 'InteractionTrees', 0); pairs = zeros (0, 2); pairValues = {}; pairEdges = {}; pairMissing = {}; pairShift = 0; ## How many pairs were asked for. A count or 'all' means search; an ## explicit Nx2 matrix names the pairs outright and skips the test, as ## MATLAB takes a matrix at its word. wanted = -1; if (ischar (Interactions)) wanted = Inf; elseif (isscalar (Interactions) && ! isempty (Interactions)) wanted = Interactions; elseif (! isempty (Interactions)) ## A matrix names the terms outright. This class has always taken ## that as a term matrix, one row per term and one column per ## predictor, so it is converted to the pairs the engine works in ## rather than being mistaken for a two-column list of indices. pairs = interactionPairs (logical (Interactions)); endif if (wanted > 0 && columns (X) > 1) S = gamboostpairs (X, M.Residuals, cat); ## The F ratio becomes a probability through the package's own fcdf, ## which is verified against MATLAB; the engine deliberately does not ## carry a second incomplete beta of its own. pval = fcdf (S.F, S.DF1, S.DF2, 'upper'); pval(S.DF1 <= 0) = 1; [pval, ord] = sort (pval); ranked = S.Pairs(ord, :); keep = pval <= MaxPValue; ranked = ranked(keep, :); if (isfinite (wanted) && rows (ranked) > wanted) ranked = ranked(1:wanted, :); endif pairs = ranked; ## A pair tree allowed a single split can only cut along one ## predictor, which is a main effect and no interaction, so no pair is ## kept. R2024a selects none and warns as below. if (MSI < 2) pairs = zeros (0, 2); endif this.PairDetectionBinEdges = S.BinEdges(:); this.PairDetectionBinEdges(cat) = {[]}; if (isempty (pairs)) warning (strcat ("ClassificationGAM: model does not include", ... " interaction terms because all interaction", ... " terms have p-values greater than the", ... " 'MaxPValue' value, or the software was unable", ... " to improve the model fit.")); endif endif if (! isempty (pairs)) I = gamboostinter (X, Y, f, 1, pairs, NTI, LRI, MSI, Wfit, cat); ## A pair tree splitting on one predictor alone is a main effect and ## adds nothing, so a fit made only of such trees keeps no pair, as ## R2024a keeps none. if (I.NumTrees == 0) pairs = zeros (0, 2); warning (strcat ("ClassificationGAM: model does not include", ... " interaction terms because all interaction", ... " terms have p-values greater than the", ... " 'MaxPValue' value, or the software was unable", ... " to improve the model fit.")); endif endif if (! isempty (pairs)) this.Intercept = this.Intercept + I.Intercept; pairShift = I.Intercept; ## A requested pair list skips detection, so the grid it would have ## used comes from the interaction phase instead. if (wanted <= 0) this.PairDetectionBinEdges = I.PairBinEdges(:); this.PairDetectionBinEdges(cat) = {[]}; endif pairValues = I.PairValues; pairEdges = I.PairEdges; pairMissing = I.PairMissing; reason.InteractionTrees = I.ReasonForTermination; ntrees.InteractionTrees = I.NumTrees; endif this.Interactions = pairs; this.ReasonForTermination = reason; this.NumTrainedTrees = ntrees; ## The constant the interaction surfaces gave up when they were ## recentred is kept apart from the predictor phase's intercept. The ## Intercept property still reports their sum, as MATLAB's does, but ## predicting without the interactions has to take this part back out ## or it would answer with a constant the main effects never earned. this.TreeModel = struct ('ShapeValues', {M.ShapeValues}, ... 'PairValues', {pairValues}, ... 'PairEdges', {pairEdges}, ... 'PairMissing', {pairMissing}, ... 'Pairs', pairs, ... 'PairIntercept', pairShift, ... 'CategoricalLevels', {Levels}); ## MATLAB reports the request rather than the selection here, and the ## selection through the Interactions property. NumPrint and ## VerbosityLevel are reported at their defaults: this engine keeps no ## printed trace, so they are stated rather than accepted and ignored. if (ischar (Interactions)) request = Interactions; elseif (isempty (Interactions)) request = 0; else request = Interactions; endif this.ModelParameters = struct ( ... 'NumPrint', NPrint, ... 'MaxPValue', MaxPValue, ... 'InitialLearnRateForPredictors', LRP, ... 'InitialLearnRateForInteractions', LRI, ... 'NumTreesPerPredictor', NTP, ... 'NumTreesPerInteraction', NTI, ... 'MaxNumSplitsPerPredictor', MSP, ... 'MaxNumSplitsPerInteraction', MSI, ... 'VerbosityLevel', Verb, ... 'Interactions', request, ... 'Version', 1, ... 'Method', 'GAM', ... 'Type', 'classification'); endfunction ## addInteractions both arrive here with IntMatrix already decided and the ## predictors and response prepared as the fit wants them, so the two ## cannot drift: a model given its interactions after the fact is the ## model it would have been had they been asked for at the outset. function this = fitModelwInt (this, X, Y, Inter, Knots, Order, DoF, ... LearningRate, NumIterations) if (isempty (this.Formula)) ## Analyze Interactions optional parameter this.IntMatrix = this.parseInteractions (); ## Append interaction terms to the predictor matrix for i = 1:rows (this.IntMatrix) tindex = logical (this.IntMatrix(i,:)); Xterms = X(:,tindex); Xinter = ones (this.NumObservations, 1); for c = 1:sum (tindex) Xinter = Xinter .* Xterms(:,c); endfor ## Append interaction terms X = [X, Xinter]; endfor else ## Analyze Formula optional parameter this.IntMatrix = this.parseFormula (); ## Add selected predictors and interaction terms XN = []; for i = 1:rows (this.IntMatrix) tindex = logical (this.IntMatrix(i,:)); Xterms = X(:,tindex); Xinter = ones (this.NumObservations, 1); for c = 1:sum (tindex) Xinter = Xinter .* Xterms(:,c); endfor ## Append selected predictors and interaction terms XN = [XN, Xinter]; endfor X = XN; endif ## Update length of Knots, Order, and DoF vectors to match ## the columns of X with the interaction terms Knots = ones (1, columns (X)) * Knots(1); # Knots Order = ones (1, columns (X)) * Order(1); # Order of spline DoF = ones (1, columns (X)) * DoF(1); # Degrees of freedom ## Fit the model with interactions [iter, param, res, RSS, intercept] = this.fitGAM (X, Y, Inter, Knots, ... Order, LearningRate, ... NumIterations); this.ModelwInt.Intercept = intercept; this.ModelwInt.Parameters = param; this.ModelwInt.Iterations = iter; this.ModelwInt.Residuals = res; this.ModelwInt.RSS = RSS; endfunction function intMat = parseInteractions (this) if (islogical (this.Interactions)) ## Check that interaction matrix corresponds to predictors if (numel (this.PredictorNames) != columns (this.Interactions)) error (strcat ("ClassificationGAM: columns in 'Interactions'", ... " matrix must equal to the number of predictors.")); endif intMat = this.Interactions; elseif (isnumeric (this.Interactions)) ## Need to measure the effect of all interactions to keep the best ## performing. Just check that the given number is not higher than ## p*(p-1)/2, where p is the number of predictors. p = this.NumPredictors; if (this.Interactions > p * (p - 1) / 2) error (strcat ("ClassificationGAM: number of interaction terms", ... " requested is larger than all possible", ... " combinations of predictors in X.")); endif ## The pairs are not ranked by how much each contributes, so the ## first ones asked for are taken in the order nchoosek lists them. intMat = pairTerms (p)(1:this.Interactions, :); elseif (strcmpi (this.Interactions, 'all')) ## Calculate all p*(p-1)/2 interaction terms intMat = pairTerms (this.NumPredictors); endif endfunction ## Determine interactions from formula function intMat = parseFormula (this) intMat = []; ## Check formula for syntax if (isempty (strfind (this.Formula, '~'))) error ("ClassificationGAM: invalid syntax in 'Formula'."); endif ## Split formula and keep predictor terms formulaParts = strsplit (this.Formula, '~'); ## Check there is some string after '~' if (numel (formulaParts) < 2) error ("ClassificationGAM: no predictor terms in 'Formula'."); endif predictorString = strtrim (formulaParts{2}); if (isempty (predictorString)) error ("ClassificationGAM: no predictor terms in 'Formula'."); endif ## Split additive terms (between + sign) aterms = strtrim (strsplit (predictorString, '+')); ## Process all terms for i = 1:numel (aterms) ## Find individual terms (string missing ':') if (isempty (strfind (aterms(i), ':'){:})) ## Search PredictorNames to associate with column in X sterms = strcmp (this.PredictorNames, aterms(i)); ## Append to interactions matrix intMat = [intMat; sterms]; else ## Split interaction terms (string contains ':') mterms = strsplit (aterms{i}, ':'); ## Add each individual predictor to interaction term vector iterms = logical (zeros (1, this.NumPredictors)); for t = 1:numel (mterms) iterms = iterms | strcmp (this.PredictorNames, mterms(t)); endfor ## Check that all predictors have been identified if (sum (iterms) != t) error (strcat ("ClassificationGAM: some predictors", ... " have not been identified.")); endif ## Append to interactions matrix intMat = [intMat; iterms]; endif endfor ## Check that all terms have been identified if (! all (sum (intMat, 2) > 0)) error ("ClassificationGAM: some terms have not been identified."); endif endfunction ## Fit the model function [iter, param, res, RSS, intercept] = fitGAM (this, X, Y, Inter, ... Knots, Order, learning_rate, num_iterations) ## The fit is performed by the shared spline engine, which builds and ## factorises each predictor's design once and reduces a boosting round ## to two products against the factors. Mdl = gamtrain (X, Y, Knots, Order, 1, Inter, learning_rate, ... num_iterations); iter = Mdl.Iterations; param = Mdl.Parameters; res = Mdl.Residuals; RSS = Mdl.RSS; intercept = Mdl.Intercept; endfunction ## Set cost endmethods endclassdef ## Helper function function scores = predict_val (params, XC, intercept) ## The shared prediction engine evaluates every additive term and sums ## them. That sum is the log-odds of the second class, so the raw score of ## the first is its negative and the pair sums to zero, as MATLAB's does. f = gampredict (params, XC, intercept, 0); scores = [-f, f]; endfunction %!demo %! ## Train a GAM classifier for binary classification %! ## using specific data and plot the decision boundaries. %! %! ## Define specific data %! X = [1, 2; 2, 3; 3, 3; 4, 5; 5, 5; ... %! 6, 7; 7, 8; 8, 8; 9, 9; 10, 10]; %! Y = [0; 0; 0; 0; 0; ... %! 1; 1; 1; 1; 1]; %! %! ## Train the GAM model %! obj = fitcgam (X, Y, 'Interactions', 'all') %! %! ## Create a grid of values for prediction %! x1 = [min(X(:,1)):0.1:max(X(:,1))]; %! x2 = [min(X(:,2)):0.1:max(X(:,2))]; %! [x1G, x2G] = meshgrid (x1, x2); %! XGrid = [x1G(:), x2G(:)]; %! [labels, score] = predict (obj, XGrid); ## Test constructor %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = [0; 0; 1; 1]; %! PredictorNames = {'Feature1', 'Feature2', 'Feature3'}; %! a = ClassificationGAM (x, y, 'FitMethod', 'splines', ... %! 'PredictorNames', PredictorNames); %! assert_equal (class (a), "ClassificationGAM"); %! assert_equal ({a.X, a.Y, a.NumObservations}, {x, y, 4}) %! assert_equal ({a.NumPredictors, a.ResponseName}, {3, 'Y'}) %! assert_equal (a.ClassNames, [0; 1]) %! assert_equal (a.PredictorNames, PredictorNames) %! assert_equal (a.BaseModel.Intercept, 0) %!test %! load fisheriris %! inds = strcmp (species,'versicolor') | strcmp (species,'virginica'); %! X = meas(inds, :); %! Y = species(inds, :)'; %! Y = strcmp (Y, 'virginica')'; %! a = ClassificationGAM (X, Y, 'FitMethod', 'splines', ... %! 'Formula', 'Y ~ x1 + x2 + x3 + x4 + x1:x2 + x2:x3'); %! assert_equal (class (a), "ClassificationGAM"); %! assert_equal ({a.X, a.Y, a.NumObservations}, {X, Y, 100}) %! assert_equal ({a.NumPredictors, a.ResponseName}, {4, 'Y'}) %! assert_equal (a.ClassNames, logical ([0; 1])) %! assert_equal (a.Formula, 'Y ~ x1 + x2 + x3 + x4 + x1:x2 + x2:x3') %! assert_equal (a.PredictorNames, {'x1', 'x2', 'x3', 'x4'}) %! assert_equal (a.ModelwInt.Intercept, 0) %!test %! X = [2, 3, 5; 4, 6, 8; 1, 2, 3; 7, 8, 9; 5, 4, 3]; %! Y = [0; 1; 0; 1; 1]; %! a = ClassificationGAM (X, Y, 'FitMethod', 'splines', ... %! 'Knots', [4, 4, 4], 'Order', [3, 3, 3]); %! assert_equal (class (a), "ClassificationGAM"); %! assert_equal ({a.X, a.Y, a.NumObservations}, {X, Y, 5}) %! assert_equal ({a.NumPredictors, a.ResponseName}, {3, 'Y'}) %! assert_equal (a.ClassNames, [0; 1]) %! assert_equal (a.PredictorNames, {'x1', 'x2', 'x3'}) %! assert_equal (a.Knots, [4, 4, 4]) %! assert_equal (a.Order, [3, 3, 3]) %! assert_equal (a.DoF, [7, 7, 7]) %! assert_equal (a.BaseModel.Intercept, 0.4055, 1e-1) ## Test Prior calculation %!test %! ## Test uniform prior %! x = [1, 2; 3, 4; 5, 6; 7, 8]; %! y = [0; 0; 1; 1]; %! a = ClassificationGAM (x, y, 'Prior', 'uniform'); %! assert_equal (a.Prior, [0.5, 0.5], 1e-6); %!test %! ## Test empirical prior %! x = [1, 2; 3, 4; 5, 6; 7, 8; 9, 10]; %! y = [0; 0; 0; 1; 1]; %! a = ClassificationGAM (x, y, 'Prior', 'empirical'); %! assert_equal (a.Prior, [0.6, 0.4], 1e-6); %!test %! ## Test numeric prior %! x = [1, 2; 3, 4; 5, 6; 7, 8]; %! y = [0; 0; 1; 1]; %! a = ClassificationGAM (x, y, 'Prior', [0.7, 0.3]); %! assert_equal (a.Prior, [0.7, 0.3], 1e-6); %!test %! ## Test default prior (empirical) %! x = [1, 2; 3, 4; 5, 6; 7, 8; 9, 10; 11, 12]; %! y = [0; 0; 0; 1; 1; 1]; %! a = ClassificationGAM (x, y); %! assert_equal (a.Prior, [0.5, 0.5], 1e-6); %!test %! ## Test prior normalization %! x = [1, 2; 3, 4; 5, 6; 7, 8]; %! y = [0; 0; 1; 1]; %! a = ClassificationGAM (x, y, 'Prior', [2, 1]); %! assert_equal (a.Prior, [2/3, 1/3], 1e-6); %!test # A categorical 'ClassNames' is accepted %! load fisheriris %! y = categorical (species(51:150)); %! Mdl = ClassificationGAM (meas(51:150,:), y, 'ClassNames', ... %! categorical ({'versicolor'; 'virginica'})); %! assert_equal (class (Mdl.ClassNames), 'categorical'); %! assert_equal (numel (Mdl.ClassNames), 2); %!test # An unused category of a categorical response is not a class %! load fisheriris %! y = categorical (species); %! Mdl = ClassificationGAM (meas(51:150,:), y(51:150)); %! assert_equal (cellstr (Mdl.ClassNames), {'versicolor'; 'virginica'}); %! assert_equal (predict (Mdl, meas(51,:)), y(51)); ## Test input validation for Prior ## Interactions reports the two-way terms the fitted model carries, as ## predictor index pairs. R2024a's GAM with 'Interactions', 'all' over three ## predictors returns [1 2; 1 3; 2 3], which is what this matches. %!test %! k = (1:60)'; %! X = [mod(k*7,11)-5, mod(k*3,11)-5, mod(k*5,11)-5]; %! y = double (X(:,1).*X(:,2) > 0) + 1; %! Mdl = fitcgam (X, y, "Interactions", "all"); %! assert_equal (Mdl.Interactions, [1, 2; 1, 3; 2, 3]); ## No interactions is an empty list of pairs, keeping its two columns, and ## not an empty matrix of no width. %!test %! k = (1:60)'; %! X = [mod(k*7,11)-5, mod(k*3,11)-5, mod(k*5,11)-5]; %! y = double (X(:,1).*X(:,2) > 0) + 1; %! Mdl = fitcgam (X, y); %! assert_equal (size (Mdl.Interactions), [0, 2]); %! assert_equal (class (Mdl.Interactions), "double"); ## The same kind of value follows whatever form the request took. %!test %! k = (1:60)'; %! X = [mod(k*7,11)-5, mod(k*3,11)-5, mod(k*5,11)-5]; %! y = double (X(:,1).*X(:,2) > 0) + 1; %! Mc = fitcgam (X, y, "Interactions", 2); %! Ml = fitcgam (X, y, "Interactions", logical ([1, 1, 0; 0, 1, 1])); %! assert_equal (Mc.Interactions, [1, 2; 1, 3]); %! assert_equal (Ml.Interactions, [1, 2; 2, 3]); ## A formula names its main effects as terms of the model, and a main effect ## is not an interaction: the term matrix holds all three, Interactions the ## one two-way term among them. %!test %! k = (1:60)'; %! X = [mod(k*7,11)-5, mod(k*3,11)-5, mod(k*5,11)-5]; %! y = double (X(:,1).*X(:,2) > 0) + 1; %! Mdl = fitcgam (X, y, "FitMethod", "splines", ... %! "Formula", "Y ~ x1 + x2 + x1:x2"); %! assert_equal (Mdl.Interactions, [1, 2]); %! assert_equal (rows (Mdl.IntMatrix), 3); ## The compact model carries the same pairs. %!test %! k = (1:60)'; %! X = [mod(k*7,11)-5, mod(k*3,11)-5, mod(k*5,11)-5]; %! y = double (X(:,1).*X(:,2) > 0) + 1; %! Mdl = fitcgam (X, y, "Interactions", "all"); %! assert_equal (compact (Mdl).Interactions, Mdl.Interactions); ## A response naming its classes in the rows of a character matrix is one ## of the documented types and MATLAB accepts it on every classifier. The ## whole surface below was broken and untested, which is why it stayed so. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcgam (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcgam (Xch, Ycell); %! assert_equal (size (Mc.ClassNames), [2, 10]); %! assert_equal (cellstr (Mc.ClassNames), Ms.ClassNames); ## predict returns whole names, not their first letters. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcgam (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcgam (Xch, Ycell); %! pch = predict (Mc, Xch); %! assert_equal (columns (pch), 10); %! assert_equal (cellstr (pch), predict (Ms, Xch)); ## loss, margin and edge read a character response as the same response. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcgam (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcgam (Xch, Ycell); %! assert_equal (loss (Mc, Xch, Ych), loss (Ms, Xch, Ycell), 1e-12); %! assert_equal (margin (Mc, Xch, Ych), margin (Ms, Xch, Ycell), 1e-12); %! assert_equal (edge (Mc, Xch, Ych), edge (Ms, Xch, Ycell), 1e-12); ## A character matrix pads its rows out to the longest name, and the padding ## is part of the name: R2024a reports ClassNames of ['ab '; 'abcd']. %!test %! Xpad = [1 2; 3 4; 1.1 2.1; 3.1 4.1; 1.2 2.2; 3.2 4.2]; %! Ypad = char ({"ab", "abcd", "ab", "abcd", "ab", "abcd"}); %! rand ("state", 1); randn ("state", 1); %! Mp = fitcgam (Xpad, Ypad); %! assert_equal (size (Mp.ClassNames), [2, 4]); %! assert_equal (Mp.ClassNames(1,:), "ab "); ## A row dropped for a missing predictor is the only case that exercises ## indexing the response by row rather than by element. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! Xmiss = Xch; Xmiss(3,2) = NaN; %! rand ("state", 1); randn ("state", 1); Md = fitcgam (Xmiss, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcgam (Xmiss, Ycell); %! assert_equal (size (Md.ClassNames), [2, 10]); %! assert_equal (cellstr (Md.ClassNames), Ms.ClassNames); ## ClassNames may itself be given as a character matrix, which selects the ## classes by whole name: ismember between two character matrices compares ## them character by character and would select by letter. %!test %! load fisheriris %! rand ("state", 1); randn ("state", 1); %! Mf = fitcgam (meas, char (species), ... %! "ClassNames", char ({"versicolor", "virginica"})); %! assert_equal (rows (Mf.ClassNames), 2); %! assert_equal (cellstr (Mf.ClassNames), {"versicolor"; "virginica"}); ## A model fitted from a character response comes back off disk unchanged. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcgam (Xch, Ych); %! fname = tempname (); %! savemodel (Mc, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.ClassNames, Mc.ClassNames); %! assert_equal (predict (M2, Xch), predict (Mc, Xch)); ## crossval carries a character response through cvpartition and back. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcgam (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcgam (Xch, Ycell); %! rand ("state", 2); cvc = crossval (Mc, "KFold", 3); %! rand ("state", 2); cvs = crossval (Ms, "KFold", 3); %! assert_equal (cellstr (kfoldPredict (cvc)), kfoldPredict (cvs)); ## addInteractions fits the interaction terms onto a model that already has ## its univariate ones. The result is the model that would have been fitted ## had the terms been asked for at the outset: both go through one private ## method, so the two cannot drift apart. %!test %! load fisheriris %! bai = ! strcmp (species, "setosa"); %! Xai = meas(bai,2:4); Yai = species(bai); %! Aai = addInteractions (fitcgam (Xai, Yai), "all"); %! Bai = fitcgam (Xai, Yai, "Interactions", "all"); %! assert_equal (Aai.Interactions, Bai.Interactions); %! assert_equal (Aai.ModelwInt, Bai.ModelwInt); %! assert_equal (predict (Aai, Xai), predict (Bai, Xai)); ## The univariate fit is left alone, which is what MATLAB leaves alone too. %!test %! load fisheriris %! bai = ! strcmp (species, "setosa"); %! Xai = meas(bai,2:4); Yai = species(bai); %! Cai = fitcgam (Xai, Yai); %! Aai = addInteractions (Cai, "all"); %! assert_equal (predict (Aai, Xai, "IncludeInteractions", false), ... %! predict (Cai, Xai)); ## A count and a logical matrix name terms as the constructor's option does. %!test %! load fisheriris %! bai = ! strcmp (species, "setosa"); %! Xai = meas(bai,2:4); Yai = species(bai); %! Aai = addInteractions (fitcgam (Xai, Yai, 'FitMethod', 'splines'), 2); %! assert_equal (Aai.Interactions, [1, 2; 1, 3]); %! Lai = addInteractions (fitcgam (Xai, Yai, 'FitMethod', 'splines'), ... %! logical ([1 1 0; 0 1 1])); %! assert_equal (Lai.Interactions, [1, 2; 2, 3]); ## The boosted-tree engine names the same terms from the same specifications, ## but a count takes the pairs the interaction search ranked highest rather ## than the first in index order, so the two engines select differently. ## ## Only the top of the ranking is pinned, and deliberately. On this fixture ## one pair carries an interaction and the other two do not: (2,3) scores ## F 3.25 at p 5.5e-06, while (1,2) and (1,3) score F 0.5599 and F 0.4331, ## both below one, meaning their cells explain less than the noise within ## them. Their p values sit at 0.974 and 0.998, so which of the two the ## search ranks second is decided by 2% of the scale at the very top of it ## and turns over on any change that moves the predictor-phase residuals at ## all. Pinning that order pins nothing about the search, and it broke when ## the tree learner gained its minimum leaf size. What is worth holding is ## that the pair with signal comes first, that a count does not simply take ## the first pair in index order, and that 'all' names every pair. %!test %! load fisheriris %! bai = ! strcmp (species, "setosa"); %! Xai = meas(bai,2:4); Yai = species(bai); %! Aai = addInteractions (fitcgam (Xai, Yai), 2); %! assert_equal (rows (Aai.Interactions), 2); %! assert_equal (Aai.Interactions(1,:), [2, 3]); %! Lai = addInteractions (fitcgam (Xai, Yai), logical ([1 1 0; 0 1 1])); %! assert_equal (Lai.Interactions, [1, 2; 2, 3]); %! All = addInteractions (fitcgam (Xai, Yai), "all"); %! assert_equal (All.Interactions(1,:), [2, 3]); %! assert_equal (sortrows (All.Interactions), [1, 2; 1, 3; 2, 3]); ## A model that already carries interaction terms is not extended, and a ## model fitted from a formula names every term it has, interactions among ## them, so it is refused for the same reason. R2024a refuses both. ## resume continues the phase that ran last, and a model with no interactions ## has only the predictor phase open. Resuming reproduces the model a single ## fit of the combined budget would have produced, which is the oracle every ## test here uses. %!test %! load fisheriris %! X = meas(51:150,:); %! Y = species(51:150); %! A = fitcgam (X, Y, 'NumTreesPerPredictor', 5); %! B = resume (A, 10); %! C = fitcgam (X, Y, 'NumTreesPerPredictor', 15); %! assert_equal (B.ModelParameters.NumTreesPerPredictor, 15); %! [~, sB] = predict (B, X); %! [~, sC] = predict (C, X); %! assert_equal (sB, sC, 1e-12); %!test %! ## A model carrying interactions gains interaction trees, and its predictor %! ## shape functions are left where they were. %! load fisheriris %! X = meas(51:150,:); %! Y = species(51:150); %! A = fitcgam (X, Y, 'NumTreesPerPredictor', 5, 'Interactions', 3, ... %! 'NumTreesPerInteraction', 4); %! B = resume (A, 10); %! assert_equal (B.ModelParameters.NumTreesPerPredictor, 5); %! assert_equal (B.ModelParameters.NumTreesPerInteraction, 14); %! assert_equal (B.TreeModel.ShapeValues, A.TreeModel.ShapeValues); %! C = fitcgam (X, Y, 'NumTreesPerPredictor', 5, 'Interactions', 3, ... %! 'NumTreesPerInteraction', 14); %! [~, sB] = predict (B, X); %! [~, sC] = predict (C, X); %! assert_equal (sB, sC, 1e-12); %!test %! ## The selected pairs survive, and resuming twice accumulates. %! load fisheriris %! X = meas(51:150,:); %! Y = species(51:150); %! A = fitcgam (X, Y, 'NumTreesPerPredictor', 5, 'Interactions', 3, ... %! 'NumTreesPerInteraction', 4); %! B = resume (resume (A, 10), 6); %! assert_equal (B.Interactions, A.Interactions); %! assert_equal (B.ModelParameters.NumTreesPerInteraction, 20); %! assert_equal (B.ModelParameters.NumTreesPerPredictor, 5); %!test %! ## The model handed in is not modified. %! load fisheriris %! X = meas(51:150,:); %! Y = species(51:150); %! A = fitcgam (X, Y, 'NumTreesPerPredictor', 5); %! B = resume (A, 10); %! assert_equal (A.ModelParameters.NumTreesPerPredictor, 5); %! assert_equal (B.ModelParameters.NumTreesPerPredictor, 15); %!test # a row missing a predictor is kept, and without a spline score it %! ## takes the class of largest prior %! x = linspace (0, 1, 30)'; %! X = [x, cos(4 * x)]; %! y = [ones(18, 1); 2 * ones(12, 1)]; %! Mdl = ClassificationGAM (X, y, 'FitMethod', 'splines'); %! [label, score] = predict (Mdl, [0.5, 0.2; NaN, 0.2]); %! assert_equal (size (label), [2, 1]); %! assert_equal (label(2), 1); %! assert_equal (score(2,:), [NaN, NaN]); %!test # MATLAB parity: a missing value adds nothing from its term %! k = (1:200)'; %! X = [sin(k), cos(2 * k), mod(k, 5)]; %! y = 2 * sin (k) + X(:,2) .^ 2 + 0.3 * X(:,3); %! Q = [0.5, 0.2, 1; NaN, 0.2, 1; 0.5, NaN, 1; NaN, NaN, NaN; 0.1, 0.2, 1; ... %! NaN, 0.7, 3; NaN, NaN, 1]; %! Mdl = ClassificationGAM (X, y > median (y)); %! [label, score] = predict (Mdl, Q); %! assert_equal (label', [true, false, true, true, true, true, false]); %! assert_equal (score(4,2), 1 / (1 + exp (-Mdl.Intercept)), 1e-14); %!test # MATLAB parity: 'Prior' weighs the fit as the matching 'Weights' do %! k = (1:200)'; %! X = [sin(k), cos(2 * k), mod(k, 5)]; %! y = 2 * sin (k) + X(:,2) .^ 2 + 0.3 * X(:,3); %! yc = y > median (y); %! w = 0.2 * ! yc / sum (! yc) + 0.8 * yc / sum (yc); %! M0 = ClassificationGAM (X, yc, 'NumTreesPerPredictor', 50); %! M1 = ClassificationGAM (X, yc, 'Prior', [0.2, 0.8], ... %! 'NumTreesPerPredictor', 50); %! M2 = ClassificationGAM (X, yc, 'Weights', w, 'NumTreesPerPredictor', 50); %! M3 = ClassificationGAM (X, yc, 'Weights', 3 * w, ... %! 'NumTreesPerPredictor', 50); %! assert_equal (M2.Prior, [0.2, 0.8], 1e-12); %! assert_equal ([sum(M1.W(! yc)), sum(M1.W(yc))], [0.2, 0.8], 1e-12); %! assert_equal (M2.Intercept, M1.Intercept, 1e-10); %! assert_equal (M3.Intercept, M1.Intercept, 1e-10); %! assert_equal (M1.Intercept != M0.Intercept, true); %!test # MATLAB parity: an empirical prior is each class's share of the weight %! k = (1:200)'; %! X = [sin(k), cos(2 * k), mod(k, 5)]; %! y = 2 * sin (k) + X(:,2) .^ 2 + 0.3 * X(:,3); %! Mdl = ClassificationGAM (X, y > median (y), 'Weights', 1 + mod (k, 3), ... %! 'NumTreesPerPredictor', 10); %! assert_equal (Mdl.Prior, [0.498753117207, 0.501246882793], 1e-12); %!test # MATLAB parity: categorical predictors are split by sets of levels %! k = (0:119)'; %! c1 = mod (k, 3) + 1; %! x2 = sin (k); %! c3 = 10 * (mod (floor (k / 2), 2) + 1); %! X = [c1, x2, c3]; %! y = 5 * (c1 == 2) + 0.5 * x2 - 3 * (c3 == 20) + 0.1 * cos (k); %! Q = [1, 0, 10; 2, 0, 10; 3, 0, 10; 1, 0, 20; 1, 0.5, 10; 4, 0, 10; ... %! NaN, 0, 10; 2.5, 0, 20]; %! Mdl = ClassificationGAM (X, y > 1, 'CategoricalPredictors', [1, 3], ... %! 'NumTreesPerPredictor', 1); %! assert_equal (Mdl.CategoricalPredictors, [1, 3]); %! assert_equal (Mdl.BinEdges{1}, []); %! [~, s] = predict (Mdl, Q); %! assert_equal (s(:,2), [0.11920292; 0.88079708; 0.11920292; 0.11920292; ... %! 0.11920292; 0.33924363; 0.33924363; ... %! 0.33924363], 1e-8); %!test # MATLAB parity: categorical predictors with the default budget %! k = (0:119)'; %! c1 = mod (k, 3) + 1; %! x2 = sin (k); %! c3 = 10 * (mod (floor (k / 2), 2) + 1); %! X = [c1, x2, c3]; %! y = 5 * (c1 == 2) + 0.5 * x2 - 3 * (c3 == 20) + 0.1 * cos (k); %! Q = [1, 0, 10; 2, 0, 10; 3, 0, 10; 1, 0, 20; 1, 0.5, 10; 4, 0, 10; ... %! NaN, 0, 10; 2.5, 0, 20]; %! Mdl = ClassificationGAM (X, y > 1, 'CategoricalPredictors', [1, 3]); %! assert_equal (predict (Mdl, Q), logical ([0; 1; 0; 0; 0; 0; 0; 0])); %!test # MATLAB parity: a row missing a predictor is fitted, not dropped %! k = (1:200)'; %! X = [sin(k), cos(2 * k), mod(k, 5)]; %! y = 2 * sin (k) + X(:,2) .^ 2 + 0.3 * X(:,3); %! yc = y > median (y); %! X(1:5,1) = NaN; %! Mdl = ClassificationGAM (X, yc, 'NumTreesPerPredictor', 5); %! assert_equal (Mdl.Intercept, 0.0570640590529, 1e-10); %! [~, s] = resubPredict (Mdl); %! assert_equal (s(1:8,2)', [0.560351428119, 0.717900932422, ... %! 0.93278347118, 0.717900932422, ... %! 0.026714476491, 0.0252640986738, ... %! 0.998403457947, 0.998403457947], 1e-10); %!test # MATLAB parity: a pair tree of one split selects no interaction %! k = (1:200)'; %! X = [sin(k), cos(2 * k), mod(k, 5)]; %! y = 2 * sin (k) + X(:,2) .^ 2 + 0.3 * X(:,3); %! S = warning ('off', 'all'); %! unwind_protect %! Mdl = ClassificationGAM (X, y > median (y), 'Interactions', 1, ... %! 'MaxNumSplitsPerInteraction', 1, ... %! 'NumTreesPerPredictor', 5); %! unwind_protect_cleanup %! warning (S); %! end_unwind_protect %! assert_equal (Mdl.Interactions, zeros (0, 2)); %!test # MATLAB parity: pair trees are fitted to the rows, on their own grid %! k = (1:200)'; %! X = [sin(k), cos(2 * k), mod(k, 5)]; %! y = 2 * sin (k) + X(:,2) .^ 2 + 0.3 * X(:,3); %! P = [0, -0.99, 0; 0, -0.42, 2; 0, 0.33, 1; 0, 0.78, 4]; %! Mdl = ClassificationGAM (X, y > median (y), 'NumTreesPerPredictor', 5, ... %! 'Interactions', 1, ... %! 'NumTreesPerInteraction', 2, ... %! 'MaxNumSplitsPerInteraction', 4); %! assert_equal (Mdl.Interactions, [1, 3]); %! assert_equal (Mdl.Intercept, 0.108918646053, 1e-10); %! [~, s1] = predict (Mdl, P); %! [~, s0] = predict (Mdl, P, 'IncludeInteractions', false); %! pc = log (s1(:,2) ./ s1(:,1)) - log (s0(:,2) ./ s0(:,1)); %! assert_equal (pc, [-1.052063091; 1.368982697; 1.368982697; ... %! 2.759514023], 1e-8); %!test # resuming the interaction phase adds trees a longer run would add %! k = (1:200)'; %! X = [sin(k), cos(2 * k), mod(k, 5)]; %! y = 2 * sin (k) + X(:,2) .^ 2 + 0.3 * X(:,3); %! P = [0, -0.99, 0; 0, -0.42, 2; 0, 0.33, 1; 0, 0.78, 4]; %! M1 = ClassificationGAM (X, y > median (y), 'NumTreesPerPredictor', 5, ... %! 'Interactions', 1, 'NumTreesPerInteraction', 1, ... %! 'MaxNumSplitsPerInteraction', 4); %! M2 = ClassificationGAM (X, y > median (y), 'NumTreesPerPredictor', 5, ... %! 'Interactions', 1, 'NumTreesPerInteraction', 2, ... %! 'MaxNumSplitsPerInteraction', 4); %! [~, s1] = predict (resume (M1, 1), P); %! [~, s2] = predict (M2, P); %! assert_equal (s1, s2, 1e-12); %!test # MATLAB parity: a missing value stops at the node that splits on it %! k = (1:200)'; %! X = [sin(k), cos(2 * k), mod(k, 5)]; %! y = 2 * sin (k) + X(:,2) .^ 2 + 0.3 * X(:,3); %! yc = y > median (y); %! X(1:5,1) = NaN; %! X(6:10,3) = NaN; %! P = [-0.99, 0, 0; -0.33, 0, 2; 0.33, 0, 4; 0.99, 0, 1; ... %! NaN, 0, 0; -0.9, 0, NaN; 0.9, 0, NaN; NaN, 0, NaN]; %! Mdl = ClassificationGAM (X, yc, 'NumTreesPerPredictor', 5, ... %! 'Interactions', logical ([1, 0, 1]), ... %! 'NumTreesPerInteraction', 2, ... %! 'MaxNumSplitsPerInteraction', 4); %! assert_equal (Mdl.Interactions, [1, 3]); %! assert_equal (Mdl.Intercept, 0.193222952248, 1e-10); %! [~, s1] = predict (Mdl, P); %! [~, s0] = predict (Mdl, P, 'IncludeInteractions', false); %! pc = log (s1(:,2) ./ s1(:,1)) - log (s0(:,2) ./ s0(:,1)); %! assert_equal (pc, [-2.768730478; -1.120470644; 3.467791398; ... %! 2.253371556; 0.09617229376; -1.145305518; ... %! 1.63875225; 0.09617229376], 1e-8); %!error ... %! load fisheriris; ... %! resume (fitcgam (meas(51:150,:), species(51:150), 'NumTreesPerPredictor', 5)) %!error ... %! load fisheriris; ... %! resume (fitcgam (meas(51:150,:), species(51:150), 'FitMethod', 'splines'), 5) %!error ... %! load fisheriris; ... %! resume (fitcgam (meas(51:150,:), species(51:150), 'NumTreesPerPredictor', 5), 0) %!error ... %! load fisheriris; ... %! resume (fitcgam (meas(51:150,:), species(51:150), 'NumTreesPerPredictor', 5), 2.5) %!error ... %! load fisheriris; ... %! resume (fitcgam (meas(51:150,:), species(51:150), 'NumTreesPerPredictor', 5), [1, 2]) %!error ... %! load fisheriris %! bai = ! strcmp (species, "setosa"); %! Mai = fitcgam (meas(bai,2:4), species(bai), "Interactions", 2); %! addInteractions (Mai, "all") %!error ... %! load fisheriris %! bai = ! strcmp (species, "setosa"); %! addInteractions (fitcgam (meas(bai,2:4), species(bai), ... %! "FitMethod", "splines", ... %! "Formula", "Y ~ x1 + x2 + x1:x2"), "all") %!error ... %! load fisheriris %! bai = ! strcmp (species, "setosa"); %! addInteractions (fitcgam (meas(bai,2:4), species(bai)), {1}) %!error ... %! ClassificationGAM (ones (10, 2), [0; 1; 0; 1; 0; 1; 0; 1; 0; 1], ... %! 'CategoricalPredictors', 3) %!error ... %! ClassificationGAM (ones (10, 2), [0; 1; 0; 1; 0; 1; 0; 1; 0; 1], ... %! 'FitMethod', 'splines', 'CategoricalPredictors', 1) %!error ... %! ClassificationGAM (ones (10, 2), [ones(5,1); zeros(5,1)], 'Weights', 'a') %!error ... %! ClassificationGAM (ones (10, 2), [ones(5,1); zeros(5,1)], ... %! 'Weights', ones (2, 2)) %!error ... %! ClassificationGAM (ones (10, 2), [ones(5,1); zeros(5,1)], 'Weights', [1, 2]) %!error ... %! ClassificationGAM (ones (10, 2), [ones(5,1); zeros(5,1)], 'Weights', ... %! -ones (10, 1)) %!error ... %! ClassificationGAM ([(1:10)', mod((1:10)', 3)], [ones(5,1); zeros(5,1)], ... %! 'Weights', ones (10, 1), 'FitMethod', 'splines') %!error ... %! ClassificationGAM (ones (4,2), ones (4,1), 'Prior', [1]) %!error ... %! ClassificationGAM (ones (4,2), ones (4,1), 'Prior', [1, 2, 3]) %!error ... %! ClassificationGAM (ones (4,2), ones (4,1), 'Prior', {1, 2}) %!error ... %! ClassificationGAM (ones (4,2), ones (4,1), 'Prior', 'invalid') ## Test input validation for constructor %!error ClassificationGAM () %!error ... %! ClassificationGAM (ones (4, 1)) %!error ... %! ClassificationGAM (ones (4,2), ones (1,4)) %!error ... %! ClassificationGAM (ones (5,2), ones (5,1), 'PredictorNames', ['A']) %!error ... %! ClassificationGAM (ones (5,2), ones (5,1), 'PredictorNames', 'A') %!error ... %! ClassificationGAM (ones (5,2), ones (5,1), 'PredictorNames', {'A', 'B', 'C'}) %!error ... %! ClassificationGAM (ones (5,2), ones (5,1), 'ResponseName', {'Y'}) %!error ... %! ClassificationGAM (ones (5,2), ones (5,1), 'ResponseName', 1) %!error ... %! ClassificationGAM (ones (10,2), ones (10,1), 'ClassNames', @(x)x) %!error ... %! ClassificationGAM (ones (10,2), ones (10,1), 'ClassNames', {1}) %!error ... %! ClassificationGAM (ones (10,2), ones (10,1), 'ClassNames', [1, 2]) %!error ... %! ClassificationGAM (ones (5,2), ['a';'b';'a';'a';'b'], 'ClassNames', ['a';'c']) %!error ... %! ClassificationGAM (ones (5,2), {'a';'b';'a';'a';'b'}, 'ClassNames', {'a','c'}) %!error ... %! ClassificationGAM (ones (10,2), logical (ones (10,1)), 'ClassNames', [true, false]) %!error ... %! ClassificationGAM (ones (5,2), ones (5,1), 'Cost', [1, 2]) %!error ... %! Mdl = fitcgam ([1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1], [0; 0; 1; 1]); %! Mdl.Cost = 1:4; %!error ... %! ClassificationGAM (ones (5,2), ones (5,1), 'Cost', 'string') %!error ... %! ClassificationGAM (ones (5,2), ones (5,1), 'Cost', {eye(2)}) ## Test predict method %!test %! x = [1, 2; 3, 4; 5, 6; 7, 8; 9, 10]; %! y = [1; 0; 1; 0; 1]; %! a = ClassificationGAM (x, y, 'FitMethod', 'splines', ... %! 'interactions', 'all'); %! l = [1; 0; 1; 0; 1]; %! s = [0.0334, 0.9666; 0.9648, 0.0352; 0.0334, 0.9666; ... %! 0.9648, 0.0352; 0.0334, 0.9666]; %! [labels, scores] = predict (a, x); %! assert_equal (class (a), "ClassificationGAM"); %! assert_equal ({a.X, a.Y, a.NumObservations}, {x, y, 5}) %! assert_equal ({a.NumPredictors, a.ResponseName}, {2, 'Y'}) %! assert_equal (a.ClassNames, [0; 1]) %! assert_equal (a.PredictorNames, {'x1', 'x2'}) %! assert_equal (a.ModelwInt.Intercept, 0.4055, 1e-1) %! assert_equal (labels, l) %! assert_equal (scores, s, 1e-1) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = [0; 0; 1; 1]; %! interactions = [false, true, false; true, false, true; false, true, false]; %! a = fitcgam (x, y, 'FitMethod', 'splines', ... %! 'learningrate', 0.2, 'interactions', interactions); %! [label, score] = predict (a, x, 'includeinteractions', true); %! l = [0; 0; 1; 1]; %! s = [0.9725, 0.0275; 0.9895, 0.0105; 0.0070, 0.9930; 0.0238, 0.9762]; %! assert_equal (class (a), "ClassificationGAM"); %! assert_equal ({a.X, a.Y, a.NumObservations}, {x, y, 4}) %! assert_equal ({a.NumPredictors, a.ResponseName}, {3, 'Y'}) %! assert_equal (a.ClassNames, [0; 1]) %! assert_equal (a.PredictorNames, {'x1', 'x2', 'x3'}) %! assert_equal (a.ModelwInt.Intercept, 0) %! assert_equal (label, l) %! assert_equal (score, s, 1e-1) ## Test input validation for predict method %!error ... %! predict (ClassificationGAM (ones (4,2), ones (4,1))) %!error ... %! predict (ClassificationGAM (ones (4,2), ones (4,1)), []) %!error ... %! predict (ClassificationGAM (ones (4,2), ones (4,1)), 1) %!error ... %! predict (ClassificationGAM (ones (4,2), ones (4,1)), ones (4,2), 'Bogus') %!error ... %! predict (ClassificationGAM (ones (4,2), ones (4,1)), ones (4,2), 'Bogus', 1) ## Test crossval method ## A numeric response is coded 0/1 for the fitter whatever its own labels ## are. A response of 1 and 2 used to reach the fitter as it was given: the ## seed is log (mean (Y) / (1 - mean (Y))), whose argument is negative at a ## mean of 1.5, so the intercept came back NaN and took every score with it, ## and predict answered class 1 for every row. %!test %! Xn = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! Mn = fitcgam (Xn, [1; 1; 2; 2], 'FitMethod', 'splines'); %! assert_equal (Mn.BaseModel.Intercept, 0, 1e-12); %! [label, score] = predict (Mn, Xn); %! assert_equal (label, [1; 1; 2; 2]); %! assert_equal (all (isfinite (score(:))), true); ## Labels nowhere near 0 and 1 fit the same way, the coding being the class ## index rather than the label. %!test %! Xn = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! Mn = fitcgam (Xn, [5; 5; 9; 9], 'FitMethod', 'splines'); %! assert_equal (Mn.BaseModel.Intercept, 0, 1e-12); %! assert_equal (predict (Mn, Xn), [5; 5; 9; 9]); ## A response already coded 0 and 1 is unchanged by that coding. %!test %! Xn = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! M0 = fitcgam (Xn, [0; 0; 1; 1], 'FitMethod', 'splines'); %! assert_equal (M0.BaseModel.Intercept, 0, 1e-12); %! assert_equal (predict (M0, Xn), [0; 0; 1; 1]); ## The boosted engine fits its predictors by local scoring: the working ## response and the weights are formed once per cycle and held fixed across ## it, so the first predictor sees the residual it would see fitted alone and ## each one after sees what the earlier ones took out. Measured against ## R2024a over two predictors at one tree each, on the two overlapping species ## with every third label flipped. %!test %! load fisheriris %! X = meas(51:150,1:2); %! Y = species(51:150); %! f = 1:3:100; %! t = Y(f); %! t(strcmp (t, 'versicolor')) = {'ZZ'}; %! t(strcmp (t, 'virginica')) = {'versicolor'}; %! t(strcmp (t, 'ZZ')) = {'virginica'}; %! Y(f) = t; %! o = {'NumTreesPerPredictor', 1, 'MaxNumSplitsPerPredictor', 1, ... %! 'Interactions', 0}; %! g1 = (4.8:0.1:8.0)'; %! g2 = (1.9:0.1:3.9)'; %! M = fitcgam (X, Y, o{:}); %! M.ScoreTransform = 'none'; %! [~, s0] = predict (M, [4.8, 1.9]); %! [~, sA] = predict (M, [g1, repmat(1.9, numel (g1), 1)]); %! [~, sB] = predict (M, [repmat(4.8, numel (g2), 1), g2]); %! ## the first predictor is what it is fitted alone, the second is not %! assert_equal (max (sA(:,2)) - s0(1,2), 1.19047619047619, 1e-12); %! assert_equal (max (sB(:,2)) - s0(1,2), 1.04232804232804, 1e-12); %! A1 = fitcgam (X(:,1), Y, o{:}); %! A1.ScoreTransform = 'none'; %! [~, a0] = predict (A1, 4.8); %! [~, aA] = predict (A1, g1); %! assert_equal (max (aA(:,2)) - a0(1,2), 1.19047619047619, 1e-12); %! A2 = fitcgam (X(:,2), Y, o{:}); %! A2.ScoreTransform = 'none'; %! [~, b0] = predict (A2, 1.9); %! [~, bB] = predict (A2, g2); %! assert_equal (max (bB(:,2)) - b0(1,2), 1.33333333333333, 1e-12); ## Swapping the predictors swaps which of them is fitted first, so both shape ## functions change. R2024a again. %!test %! load fisheriris %! X = meas(51:150,1:2); %! Y = species(51:150); %! f = 1:3:100; %! t = Y(f); %! t(strcmp (t, 'versicolor')) = {'ZZ'}; %! t(strcmp (t, 'virginica')) = {'versicolor'}; %! t(strcmp (t, 'ZZ')) = {'virginica'}; %! Y(f) = t; %! o = {'NumTreesPerPredictor', 1, 'MaxNumSplitsPerPredictor', 1, ... %! 'Interactions', 0}; %! S = fitcgam (X(:,[2, 1]), Y, o{:}); %! S.ScoreTransform = 'none'; %! g1 = (4.8:0.1:8.0)'; %! g2 = (1.9:0.1:3.9)'; %! [~, t0] = predict (S, [1.9, 4.8]); %! [~, tA] = predict (S, [repmat(1.9, numel (g1), 1), g1]); %! [~, tB] = predict (S, [g2, repmat(4.8, numel (g2), 1)]); %! assert_equal (max (tB(:,2)) - t0(1,2), 1.33333333333333, 1e-12); %! assert_equal (max (tA(:,2)) - t0(1,2), 1.04497354497354, 1e-12); ## The intercept is the working response's own weighted mean and not the base ## log-odds of the response. A balanced fixture cannot tell them apart, both ## being zero, so this one is unbalanced: 42 against 28, whose log-odds is ## -0.40546510810816 and whose first-cycle intercept R2024a reports as exactly ## -0.4. Two and three cycles are pinned too, the weights being refreshed at ## the start of each. %!test %! load fisheriris %! ii = [51:100, 101:120]'; %! X = meas(ii,1:2); %! Y = species(ii); %! f = 1:4:numel (ii); %! t = Y(f); %! t(strcmp (t, 'versicolor')) = {'ZZ'}; %! t(strcmp (t, 'virginica')) = {'versicolor'}; %! t(strcmp (t, 'ZZ')) = {'virginica'}; %! Y(f) = t; %! assert_equal (sum (strcmp (Y, 'virginica')), 28); %! M1 = fitcgam (X, Y, 'NumTreesPerPredictor', 1, ... %! 'MaxNumSplitsPerPredictor', 1, 'Interactions', 0); %! assert_equal (M1.Intercept, -0.4, 1e-12); %! M2 = fitcgam (X, Y, 'NumTreesPerPredictor', 2, ... %! 'MaxNumSplitsPerPredictor', 1, 'Interactions', 0); %! assert_equal (M2.Intercept, -0.43634347574616, 1e-11); %! M3 = fitcgam (X, Y, 'NumTreesPerPredictor', 3, ... %! 'MaxNumSplitsPerPredictor', 1, 'Interactions', 0); %! assert_equal (M3.Intercept, -0.4460203997322, 1e-11); ## The coding of a numeric response is a property of the class, not of either ## engine: the boosted-tree engine reads labels of 1 and 2, or 5 and 9, the ## same way and returns them in their own values. ## ## Four observations cannot be split at all under the tree learner's minimum ## leaf size, so the boosted fit here is a constant and every row comes back ## as the first class. R2024a answers exactly the same, [1 1 1 1], [5 5 5 5] ## and [0 0 0 0], and takes its first split at ten observations as this does. ## What the test pins is therefore the coding and not the separation: a ## response of 5 and 9 comes back as 5 rather than as a 0/1 index. The ## spline engine above, which does not fit trees, still separates these four. %!test %! Xn = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! assert_equal (predict (fitcgam (Xn, [1; 1; 2; 2]), Xn), [1; 1; 1; 1]); %! assert_equal (predict (fitcgam (Xn, [5; 5; 9; 9]), Xn), [5; 5; 5; 5]); %! assert_equal (predict (fitcgam (Xn, [0; 0; 1; 1]), Xn), [0; 0; 0; 0]); ## Ten observations is where a boosted fit first splits, two leaves of the ## minimum five. Measured against R2024a, which does the same. %!test %! Y9 = repmat ({'a'}, 9, 1); Y9(6:9) = {'b'}; %! M9 = fitcgam ((1:9)', Y9, 'NumTreesPerPredictor', 1, ... %! 'MaxNumSplitsPerPredictor', 1, 'Interactions', 0); %! M9.ScoreTransform = 'none'; %! [~, s9] = predict (M9, (1:9)'); %! assert_equal (max (s9(:,2)) - min (s9(:,2)), 0, 1e-12); %! Y10 = repmat ({'a'}, 10, 1); Y10(6:10) = {'b'}; %! M10 = fitcgam ((1:10)', Y10, 'NumTreesPerPredictor', 1, ... %! 'MaxNumSplitsPerPredictor', 1, 'Interactions', 0); %! M10.ScoreTransform = 'none'; %! [~, s10] = predict (M10, (1:10)'); %! assert_equal (max (s10(:,2)) - min (s10(:,2)) > 1, true); %!shared x, y, obj %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1; 4, 5, 6]; %! y = [0; 0; 1; 1; 0]; %! obj = fitcgam (x, y); %!test %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! CVMdl = crossval (obj); %! warning (status); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y}) %! assert_equal (CVMdl.KFold == 5, true) %! assert_equal (class (CVMdl.Trained{1}), "CompactClassificationGAM") %! assert_equal (CVMdl.CrossValidatedModel, "GAM") %!test %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! CVMdl = crossval (obj, 'KFold', 2); %! warning (status); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y}) %! assert_equal (CVMdl.KFold == 2, true) %! assert_equal (class (CVMdl.Trained{1}), "CompactClassificationGAM") %! assert_equal (CVMdl.CrossValidatedModel, "GAM") %!test %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! CVMdl = crossval (obj, 'HoldOut', 0.2); %! warning (status); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y}) %! assert_equal (class (CVMdl.Trained{1}), "CompactClassificationGAM") %! assert_equal (CVMdl.CrossValidatedModel, "GAM") %!test %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! partition = cvpartition (y, 'KFold', 3); %! warning (status); %! CVMdl = crossval (obj, 'cvPartition', partition); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal (CVMdl.KFold == 3, true) %! assert_equal (class (CVMdl.Trained{1}), "CompactClassificationGAM") %! assert_equal (CVMdl.CrossValidatedModel, "GAM") ## Test input validation for crossval method %!error ... %! crossval (obj, 'kfold') %!error... %! crossval (obj, 'kfold', 12, 'holdout', 0.2) %!error ... %! crossval (obj, 'kfold', 'a') %!error ... %! crossval (obj, 'holdout', 2) %!error ... %! crossval (obj, 'leaveout', 1) %!error ... %! crossval (obj, 'cvpartition', 1) %!error ... %! savemodel (ClassificationGAM ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2])) %!error ... %! savemodel (ClassificationGAM ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), 1) %!error ... %! savemodel (ClassificationGAM ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), ['ab'; 'cd']) ## A ScoreTransform is stored under its own name. That it reaches the ## scores is covered above, through predict. %!test %! Mdl = fitcgam ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]); %! Mdl.ScoreTransform = 'symmetric'; %! assert_equal (class (Mdl.ScoreTransform), 'char'); %! assert_equal (Mdl.ScoreTransform, 'symmetric'); ## The new properties MATLAB reports are carried and saved. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = fitcgam (meas(inds,:), species(inds), 'FitMethod', 'splines'); %! assert_equal (Mdl.Intercept, Mdl.BaseModel.Intercept); %! assert_equal (size (Mdl.W), [Mdl.NumObservations, 1]); %! assert_equal (sum (Mdl.W), 1, 1e-12); %! assert_equal (Mdl.CategoricalPredictors, []); %! assert_equal (Mdl.ExpandedPredictorNames, Mdl.PredictorNames); ## ClassNames keeps the type of the response, as MATLAB has it. %!test %! assert_equal (fitcgam ([1;2;3;4], [7;3;7;3]).ClassNames, [3; 7]); %! assert_equal (fitcgam ([1;2;3;4], logical ([1;0;1;0])).ClassNames, ... %! logical ([0; 1])); %! assert_equal (fitcgam ([1;2;3;4], {'b';'a';'b';'a'}).ClassNames, ... %! {'a'; 'b'}); %! assert_equal (fitcgam ([1;2;3;4], ['b';'a';'b';'a']).ClassNames, ['a';'b']); ## An assigned ScoreTransform reaches the scores predict returns, and it ## composes on the raw log-odds rather than on the posteriors, as measured ## on MATLAB R2024a. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = fitcgam (meas(inds,:), species(inds)); %! Mdl.ScoreTransform = 'none'; %! [~, raw] = predict (Mdl, meas(inds,:)); %! Mdl.ScoreTransform = 'symmetric'; %! [~, s1] = predict (Mdl, meas(inds,:)); %! assert_equal (s1, 2 * raw - 1, 1e-12); ## The edge is the mean of the margins, and weights reweight that mean. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! X = meas(inds,:); %! Y = species(inds); %! Mdl = fitcgam (X, Y); %! m = margin (Mdl, X, Y); %! assert_equal (edge (Mdl, X, Y), mean (m), 1e-12); %! ## The weights are normalized within each class to that class's prior, %! ## not divided by their total, so a class keeps the influence its prior %! ## gives it however the weights inside it are spread. %! w = [ones(50, 1); 3 * ones(50, 1)]; %! wn = w; %! wn(1:50) = w(1:50) / sum (w(1:50)) * Mdl.Prior(1); %! wn(51:100) = w(51:100) / sum (w(51:100)) * Mdl.Prior(2); %! assert_equal (edge (Mdl, X, Y, 'Weights', w), ... %! sum (wn .* m) / sum (wn), 1e-12); %! ## Scaling a whole class leaves the edge where it was. %! assert_equal (edge (Mdl, X, Y, 'Weights', w), ... %! edge (Mdl, X, Y, 'Weights', [ones(50,1); ones(50,1)]), 1e-12); ## The resubstitution methods are the assessment methods on the training data. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! X = meas(inds,:); %! Y = species(inds); %! Mdl = fitcgam (X, Y); %! assert_equal (resubPredict (Mdl), predict (Mdl, X)); %! assert_equal (resubMargin (Mdl), margin (Mdl, X, Y)); %! assert_equal (resubEdge (Mdl), edge (Mdl, X, Y)); %! assert_equal (resubLoss (Mdl), loss (Mdl, X, Y)); ## Every loss function returns a finite scalar. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! X = meas(inds,:); %! Y = species(inds); %! Mdl = fitcgam (X, Y); %! names = {'binodeviance', 'classifcost', 'classiferror', 'exponential', ... %! 'hinge', 'logit', 'mincost', 'quadratic'}; %! for k = 1:numel (names) %! L = loss (Mdl, X, Y, 'LossFun', names{k}); %! assert_equal (isscalar (L) && isfinite (L), true); %! endfor ## A 'Cost' matching the number of classes is accepted. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = fitcgam (meas(inds,:), species(inds)); %! Mdl.Cost = [0, 2; 5, 0]; %! assert_equal (Mdl.Cost, [0, 2; 5, 0]); ## A saved and reloaded model carries every property, and predicts alike. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! X = meas(inds,:); %! Mdl = fitcgam (X, species(inds), 'FitMethod', 'splines', ... %! 'Interactions', 'all', 'NumIterations', 20); %! Mdl.ScoreTransform = 'symmetric'; %! fname = tempname (); %! savemodel (Mdl, fname); %! Mdl2 = loadmodel (fname); %! delete (fname); %! assert_equal (Mdl2.Intercept, Mdl.Intercept); %! assert_equal (Mdl2.W, Mdl.W); %! assert_equal (Mdl2.ExpandedPredictorNames, Mdl.ExpandedPredictorNames); %! assert_equal (Mdl2.ModelwInt.Parameters(1).coefs, ... %! Mdl.ModelwInt.Parameters(1).coefs); %! assert_equal (Mdl2.ScoreTransform, 'symmetric'); %! [label, score] = predict (Mdl, X); %! [label2, score2] = predict (Mdl2, X); %! assert_equal (label2, label); %! assert_equal (score2, score); %!shared x, y, Mdl %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! x = meas(inds,:); %! y = species(inds); %! Mdl = fitcgam (x, y); ## Test input validation for margin method %!error ... %! margin (Mdl, x) %!error ... %! margin (Mdl, [], y) %!error ... %! margin (Mdl, 1, y) %!error ... %! margin (Mdl, x, []) %!error ... %! margin (Mdl, x, y(1:10)) ## Test input validation for edge method %!error ... %! edge (Mdl, x) %!error ... %! edge (Mdl, x, y, 'Weights') %!error ... %! edge (Mdl, x, y, 'LossFun', 'hinge') %!error ... %! edge (Mdl, x, y, 'Weights', 'a') %!error ... %! edge (Mdl, x, y, 'Weights', ones (2, 2)) %!error ... %! edge (Mdl, x, y, 'Weights', [1, 2, 3]) ## Test input validation for loss method %!error ... %! loss (Mdl, x) %!error ... %! loss (Mdl, x, y, 'LossFun') %!error ... %! loss (Mdl, x, y, 'LossFun', 1) %!error ... %! loss (Mdl, x, y, 'LossFun', 'nonsense') %!error ... %! loss (Mdl, x, y, 'Bogus', 1) ## RowsUsed is empty when every observation was used. %!test %! load fisheriris %! X = meas(1:100,:); %! Y = grp2idx (species(1:100)); %! Mdl = fitcgam (X, Y); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (class (Mdl.RowsUsed), 'double'); %! assert_equal (Mdl.NumObservations, 100); %! assert_equal (rows (Mdl.X), 100); %! assert_equal (rows (Mdl.W), 100); ## A missing response drops its observation and RowsUsed marks it. %!test %! load fisheriris %! X = meas(1:100,:); %! Y = grp2idx (species(1:100)); %! Y(5) = NaN; %! Mdl = fitcgam (X, Y); %! assert_equal (class (Mdl.RowsUsed), 'logical'); %! assert_equal (size (Mdl.RowsUsed), [100, 1]); %! assert_equal (sum (Mdl.RowsUsed), 99); %! assert_equal (Mdl.RowsUsed(5), false); %! assert_equal (Mdl.NumObservations, 99); %! assert_equal (rows (Mdl.X), 99); %! assert_equal (rows (Mdl.W), 99); ## A missing predictor keeps its observation, so RowsUsed stays empty. %!test %! load fisheriris %! X = meas(1:100,:); %! X(3,2) = NaN; %! Y = grp2idx (species(1:100)); %! Mdl = fitcgam (X, Y); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (Mdl.NumObservations, 100); %! assert_equal (rows (Mdl.X), 100); %! assert_equal (sum (isnan (Mdl.X(:))), 1); ## The prior reweights the observations of each class. Values from R2024a. %!test %! load fisheriris %! i2 = [1:50, 51:80]; %! Mdl = fitcgam (meas(i2,:), species(i2)); %! assert_equal (Mdl.Prior, [0.625, 0.375], 1e-14); %! assert_equal (Mdl.W(1), 0.0125, 1e-14); %! Mdl = fitcgam (meas(i2,:), species(i2), 'Prior', 'uniform'); %! assert_equal (Mdl.W(1), 0.01, 1e-14); %! assert_equal (Mdl.W(51), 1/60, 1e-14); ## A fitted model survives savemodel and loadmodel: the properties come ## back as they were and it predicts the same. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = fitcgam (meas(inds,:), species(inds)); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2), 'ClassificationGAM'); %! assert_equal (M2.NumObservations, Mdl.NumObservations); %! assert_equal (M2.PredictorNames, Mdl.PredictorNames); %! assert_equal (class (M2.ScoreTransform), class (Mdl.ScoreTransform)); %! assert_equal (predict (M2, meas(1:5,:)), predict (Mdl, meas(1:5,:))); ## The default transform is 'logit', as MATLAB's is, so the scores predict ## reports are posterior probabilities that sum to one. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = fitcgam (meas(inds,:), species(inds)); %! assert_equal (Mdl.ScoreTransform, 'logit'); %! [~, scores] = predict (Mdl, meas(1:6,:)); %! assert_equal (sum (scores, 2), ones (6, 1), 1e-12); %! assert_equal (all (scores(:) >= 0 & scores(:) <= 1), true); ## The untransformed score is the log-odds pair whose columns sum to zero, ## and the default transform is what maps it to the posteriors. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = fitcgam (meas(inds,:), species(inds)); %! [~, post] = predict (Mdl, meas(1:6,:)); %! Mdl.ScoreTransform = 'none'; %! [~, raw] = predict (Mdl, meas(1:6,:)); %! assert_equal (sum (raw, 2), zeros (6, 1), 1e-12); %! assert_equal (1 ./ (1 + exp (-raw)), post, 1e-12); ## BinEdges reports the cut points the boosted-tree engine binned each ## predictor at, as MATLAB's generalized additive model does. Under the ## spline engine, which does no binning, it stays the empty cell MATLAB ## reports for every learner that does none. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = fitcgam (meas(inds,:), species(inds)); %! assert_equal (class (Mdl.BinEdges), 'cell'); %! assert_equal (numel (Mdl.BinEdges), 4); %! assert_equal (numel (Mdl.BinEdges{1}), 27); %! Msp = fitcgam (meas(inds,:), species(inds), 'FitMethod', 'splines'); %! assert_equal (Msp.BinEdges, {}); ## The shared cost guard is in force here too, and the struct form is ## permuted into this model's class order. The battery is on ## ClassificationDiscriminant. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = fitcgam (meas(inds,:), species(inds)); %! S = struct ('ClassNames', {{'versicolor'; 'setosa'}}, ... %! 'ClassificationCosts', [0, 1; 2, 0]); %! Mdl.Cost = S; %! assert_equal (Mdl.Cost, [0, 2; 1, 0]); %!error ... %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = fitcgam (meas(inds,:), species(inds)); %! Mdl.Cost = ones (2); ## The boosted-tree engine is reachable by name while the default is still ## the spline engine, and it reports the surface MATLAB reports. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = fitcgam (meas(inds,:), species(inds), 'FitMethod', 'boostedtrees'); %! assert_equal (Mdl.FitMethod, 'boostedtrees'); %! assert_equal (numel (Mdl.BinEdges), 4); %! assert_equal (numel (Mdl.BinEdges{1}), 27); %! assert_equal (numel (fieldnames (Mdl.ModelParameters)), 13); %! assert_equal (Mdl.ModelParameters.Type, 'classification'); %! assert_equal (Mdl.ModelParameters.Method, 'GAM'); ## Its defaults are MATLAB's own, reported through ModelParameters. %!test %! Mdl = fitcgam ([1, 2; 2, 3; 3, 4; 4, 5; 5, 6; 6, 7], [0;0;0;1;1;1], ... %! 'FitMethod', 'boostedtrees'); %! MP = Mdl.ModelParameters; %! assert_equal (MP.NumTreesPerPredictor, 300); %! assert_equal (MP.NumTreesPerInteraction, 100); %! assert_equal (MP.MaxNumSplitsPerPredictor, 1); %! assert_equal (MP.MaxNumSplitsPerInteraction, 4); %! assert_equal (MP.InitialLearnRateForPredictors, 1); %! assert_equal (MP.InitialLearnRateForInteractions, 1); %! assert_equal (MP.MaxPValue, 1); %! assert_equal (MP.NumPrint, 10); %! assert_equal (MP.VerbosityLevel, 0); ## Each phase reports why it stopped; a phase that never ran reports nothing. %!test %! Mdl = fitcgam ([1, 2; 2, 3; 3, 4; 4, 5; 5, 6; 6, 7], [0;0;0;1;1;1], ... %! 'FitMethod', 'boostedtrees'); %! assert_equal (isfield (Mdl.ReasonForTermination, 'PredictorTrees'), true); %! assert_equal (isfield (Mdl.ReasonForTermination, 'InteractionTrees'), true); %! assert_equal (Mdl.ReasonForTermination.InteractionTrees, ''); ## Interactions are detected on their own coarse grid, and the second ## phase reports its own termination. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = fitcgam (meas(inds,:), species(inds), 'FitMethod', 'boostedtrees', ... %! 'Interactions', 'all'); %! assert_equal (rows (Mdl.Interactions), 6); %! assert_equal (columns (Mdl.Interactions), 2); %! assert_equal (numel (Mdl.PairDetectionBinEdges{1}), 7); %! assert_equal (! isempty (Mdl.ReasonForTermination.InteractionTrees), true); ## A tree-fitted model predicts, and its scores are posteriors summing to one ## under the default transform. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! X = meas(inds,:); %! Mdl = fitcgam (X, species(inds), 'FitMethod', 'boostedtrees'); %! [label, score] = predict (Mdl, X); %! assert_equal (numel (label), rows (X)); %! assert_equal (sum (score, 2), ones (rows (X), 1), 1e-12); ## compact carries the engine and every property it needs to predict alike. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! X = meas(inds,:); %! Mdl = fitcgam (X, species(inds), 'FitMethod', 'boostedtrees'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.FitMethod, 'boostedtrees'); %! assert_equal (CMdl.BinEdges, Mdl.BinEdges); %! assert_equal (predict (CMdl, X), predict (Mdl, X)); ## savemodel and loadmodel carry the tree fit, so a reloaded model predicts ## identically rather than coming back with an empty engine. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! X = meas(inds,:); %! Mdl = fitcgam (X, species(inds), 'FitMethod', 'boostedtrees'); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.FitMethod, 'boostedtrees'); %! assert_equal (M2.TreeModel.ShapeValues, Mdl.TreeModel.ShapeValues); %! assert_equal (predict (M2, X), predict (Mdl, X)); ## The spline engine is unchanged and still reachable by name. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = fitcgam (meas(inds,:), species(inds), 'FitMethod', 'splines'); %! assert_equal (Mdl.FitMethod, 'splines'); %! assert_equal (Mdl.BinEdges, {}); %! assert_equal (isempty (Mdl.TreeModel), true); %! assert_equal (Mdl.Knots, [5, 5, 5, 5]); ## An argument belonging to the other engine is refused, not ignored. %!error ... %! fitcgam ([1;2;3;4], [0;0;1;1], 'FitMethod', 'nonsense') %!error ... %! fitcgam ([1;2;3;4], [0;0;1;1], 'FitMethod', 5) %!error ... %! fitcgam ([1;2;3;4], [0;0;1;1], 'FitMethod', 'boostedtrees', 'Knots', 4) %!error ... %! fitcgam ([1;2;3;4], [0;0;1;1], 'FitMethod', 'splines', 'MaxPValue', 0.5) %!error ... %! fitcgam ([1;2;3;4], [0;0;1;1], 'FitMethod', 'boostedtrees', ... %! 'NumTreesPerPredictor', 0) %!error ... %! fitcgam ([1;2;3;4], [0;0;1;1], 'FitMethod', 'boostedtrees', ... %! 'NumTreesPerInteraction', 1.5) %!error ... %! fitcgam ([1;2;3;4], [0;0;1;1], 'FitMethod', 'boostedtrees', ... %! 'MaxNumSplitsPerPredictor', -1) %!error ... %! fitcgam ([1;2;3;4], [0;0;1;1], 'FitMethod', 'boostedtrees', ... %! 'MaxNumSplitsPerInteraction', 'a') %!error ... %! fitcgam ([1;2;3;4], [0;0;1;1], 'FitMethod', 'boostedtrees', ... %! 'InitialLearnRateForPredictors', 0) %!error ... %! fitcgam ([1;2;3;4], [0;0;1;1], 'FitMethod', 'boostedtrees', ... %! 'InitialLearnRateForInteractions', 2) %!error ... %! fitcgam ([1;2;3;4], [0;0;1;1], 'FitMethod', 'boostedtrees', 'Verbose', -1) %!error ... %! fitcgam ([1;2;3;4], [0;0;1;1], 'FitMethod', 'boostedtrees', 'NumPrint', 0) %!error ... %! fitcgam ([1;2;3;4], [0;0;1;1], 'FitMethod', 'boostedtrees', 'MaxPValue', 2) ## HyperparameterOptimizationResults is declared for MATLAB compatibility and ## stays empty, this class running no search over its hyperparameters. %!test %! load fisheriris %! b = ! strcmp (species, 'virginica'); %! Mdl = fitcgam (meas(b,:), species(b)); %! assert_equal (isempty (Mdl.HyperparameterOptimizationResults), true); ## Every documented score transform reaches the scores that are reported, and ## none of them moves the label: a transform reshapes what is reported, not ## what is decided. %!test %! load fisheriris %! Mdl = fitcgam (meas, strcmp (species, 'setosa')); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! T = {'identity', @(x) x; 'doublelogit', @(x) 1 ./ (1 + exp (-2 * x)); ... %! 'invlogit', @(x) log (x ./ (1 - x)); ... %! 'logit', @(x) 1 ./ (1 + exp (-x)); ... %! 'sign', @(x) sign (x); 'symmetric', @(x) 2 * x - 1; ... %! 'symmetriclogit', @(x) 2 ./ (1 + exp (-x)) - 1}; %! for i = 1:rows (T) %! Mdl.ScoreTransform = T{i,1}; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, T{i,2}(raw), 1e-12); %! assert_equal (l, label); %! endfor %! ## ismax marks the largest score of each observation, ties to the first. %! [~, k] = max (raw, [], 2); %! e = zeros (size (raw)); %! e(sub2ind (size (raw), (1:rows (raw))', k)) = 1; %! Mdl.ScoreTransform = 'ismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, e); %! Mdl.ScoreTransform = 'symmetricismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, 2 * e - 1); ## A function handle is taken as given and applied to the scores. %!test %! load fisheriris %! Mdl = fitcgam (meas, strcmp (species, 'setosa')); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! Mdl.ScoreTransform = @(x) x .^ 2; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, raw .^ 2, 1e-12); %! assert_equal (l, label); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(51:150,1:2); %! y = categorical (species(51:150)); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcgam (T, 'Species'); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'Species'), a); %! assert_equal (loss (Mdl, T), a); ## A table at edge %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(51:150,1:2); %! y = categorical (species(51:150)); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcgam (T, 'Species'); %! a = edge (Mdl, X, y); %! assert_equal (edge (Mdl, T(:,1:2), y), a); %! assert_equal (edge (Mdl, T, 'Species'), a); %! assert_equal (edge (Mdl, T), a); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(51:150,1:2); %! y = categorical (species(51:150)); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcgam (T, 'Species'); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## Any two of 'Knots', 'Order' and 'DoF' determine the third, whichever ## order they are given in %!test %! load fisheriris %! X = meas(51:150,1); %! Y = species(51:150); %! M = fitcgam (X, Y, 'FitMethod', 'splines', 'Knots', 5, 'DoF', 9); %! assert_equal ([M.Knots, M.Order, M.DoF], [5, 4, 9]); %! M = fitcgam (X, Y, 'FitMethod', 'splines', 'DoF', 9, 'Knots', 5); %! assert_equal ([M.Knots, M.Order, M.DoF], [5, 4, 9]); %! M = fitcgam (X, Y, 'FitMethod', 'splines', 'Order', 2, 'DoF', 9); %! assert_equal ([M.Knots, M.Order, M.DoF], [7, 2, 9]); %!error ... %! ClassificationGAM (ones (10,2), [ones(5,1); zeros(5,1)], ... %! 'FitMethod', 'splines', 'Knots', 5, 'Order', 3, 'DoF', 8) %!error ... %! ClassificationGAM (ones (10,2), [ones(5,1); zeros(5,1)], ... %! 'FitMethod', 'splines', 'Interactions', 1, ... %! 'Formula', 'Y ~ x1 + x2') %!error ... %! ClassificationGAM (ones (10,2), [ones(5,1); zeros(5,1)], 'Bogus', 1) ## Observation weights of class single or double %!error ... %! ClassificationGAM ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', ... %! int8 ([1; 1; 1; 1])) %!error ... %! ClassificationGAM ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', ... %! true (4, 1)) %!error ... %! loss (ClassificationGAM ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2]), ... %! [1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', int8 ([1; 1; 1; 1])) %!test %! ## Single weights are stored single, summing to one %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! w = 1 + (1:100)' / 7; %! Mdl = ClassificationGAM (X, Y, 'Weights', single (w)); %! assert_equal (class (Mdl.W), 'single'); %! assert_equal (sum (double (Mdl.W)), 1, 1e-6); %!test %! ## Single weights compute as double, to the precision of the stored W %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! w = 1 + (1:100)' / 7; %! A = ClassificationGAM (X, Y, 'Weights', single (w)); %! B = ClassificationGAM (X, Y, 'Weights', double (single (w))); %! assert_equal (nthargout (2, @predict, A, X), ... %! nthargout (2, @predict, B, X), 1e-8); ## Test a class with prior 0, whose observations are removed before fitting, ## as MATLAB R2024a and R2026b remove them %!warning ... %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! Mdl = ClassificationGAM (X, Y, 'Prior', [1, 0]); %! assert_equal (Mdl.NumObservations, 50); %! assert_equal (Mdl.RowsUsed, [true(50, 1); false(50, 1)]); %! assert_equal (Mdl.Prior, [1, 0]); %! assert_equal (predict (Mdl, meas([51, 110], 3:4)), ... %! {'versicolor'; 'versicolor'}); %!test %! ## A named class with prior 0 may be absent, having nothing to remove %! load fisheriris %! X = meas(51:100, 3:4); %! Y = species(51:100); %! C = {'versicolor', 'virginica'}; %! Mdl = ClassificationGAM (X, Y, 'ClassNames', C, 'Prior', [1, 0]); %! assert_equal (Mdl.ClassNames, {'versicolor'; 'virginica'}); %! assert_equal (predict (Mdl, meas(51, 3:4)), {'versicolor'}); %!test %! load fisheriris %! Mdl = ClassificationGAM (meas(51:150, 3:4), species(51:150), ... %! 'Cost', [0, 1; 0, 0]); %! assert_equal (Mdl.NumObservations, 100); %! assert_equal (Mdl.Prior, [0.5, 0.5]); statistics-release-2.1.2/inst/Machine_Learning/ClassificationKNN.m000066400000000000000000006067071527311104300251610ustar00rootroot00000000000000## Copyright (C) 2023 Mohammed Azmat Khan ## Copyright (C) 2024 Ruchika Sonagote ## Copyright (C) 2023-2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ClassificationKNN < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} ClassificationKNN ## ## K-nearest neighbors classification ## ## The @code{ClassificationKNN} class implements a K-nearest neighbor ## classifier object, which can predict responses for new data using the ## @code{predict} method. The implemented algorithm allows you choose a range ## of different distance metrics, the number of nearest neighbors, as well as ## the searching algorithm. ## ## The K-nearest neighbors (k-NN) classifier is a simple, non-parametric ## machine learning algorithm used for classification tasks. It classifies a ## data point based on the majority class of its k closest neighbors in the ## feature space. ## ## Create a @code{ClassificationKNN} object by using the @code{fitcknn} ## function or the class constructor. ## ## @seealso{fitcknn} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} W ## ## Observation weights ## ## A numeric column vector with one entry per observation used for fitting. ## Each class carries its prior, spread over its own observations in ## proportion to the @qcode{'Weights'} given, or evenly when none were. ## Reassigning @qcode{Prior} re-derives it. It has the class of the ## @qcode{'Weights'} given, single or double. This property is read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} X ## ## Predictor data ## ## A numeric matrix containing the unstandardized predictor data. Each ## column of @var{X} represents one predictor (variable), and each row ## represents one observation. This property is read-only. ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} Y ## ## Class labels ## ## Specified as a logical or numeric column vector, or as a character array ## or a cell array of character vectors with the same number of rows as the ## predictor data. Each row in @var{Y} is the observed class label for ## the corresponding row in @var{X}. This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} NumObservations ## ## Number of observations ## ## A positive integer value specifying the number of observations in the ## training dataset used for training the ClassificationKNN model. ## This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} RowsUsed ## ## Rows used for fitting ## ## A logical column vector with the same length as the observations in the ## original predictor data @var{X}, true for each row that was used for ## fitting the ClassificationKNN model. It is empty, @qcode{[]}, ## when every observation was used. A row is dropped when its class is ## missing, when every predictor is missing, or when its weight is zero; ## a row missing only some predictors is used. This property is ## read-only. ## ## @end deftp RowsUsed = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} NumPredictors ## ## Number of predictors ## ## A positive integer value specifying the number of predictors in the ## training dataset used for training the ClassificationKNN model. ## This property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} PredictorNames ## ## Names of predictor variables ## ## A cell array of character vectors specifying the names of the predictor ## variables. The names are in the order in which they appear in the ## training dataset. This property is read-only. ## ## @end deftp PredictorNames = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A numeric vector of column indices into @code{X} naming the predictors ## treated as categorical, and empty when none is. This property is ## read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} ExpandedPredictorNames ## ## Names of the predictors as the model expanded them ## ## A cell array of character vectors. It matches @code{PredictorNames} ## unless a categorical predictor was expanded into indicator variables. ## This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} ResponseName ## ## Response variable name ## ## A character vector specifying the name of the response variable @var{Y}. ## This property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} ClassNames ## ## Names of classes in the response variable ## ## An array of unique values of the response variable @var{Y}, which has the ## same data types as the data in @var{Y}, sorted or in the order given by ## the @qcode{'ClassNames'} option. This property is read-only. ## @qcode{ClassNames} can have any of the following datatypes: ## ## @itemize ## @item Cell array of character vectors ## @item Character array ## @item Logical vector ## @item Numeric vector ## @end itemize ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} Sigma ## ## Predictor standard deviations ## ## A numeric vector of the same length as the columns in @var{X} with the ## standard deviations corresponding to each predictor. If the predictor ## variables have not been standardized, then @qcode{'obj.Sigma'} is empty. ## This property is read-only. ## ## Each predictor is summarized from every observation where that ## predictor is present, so a row holding a missing value in another ## predictor still contributes to this one. ## ## @end deftp Sigma = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} Mu ## ## Predictor means ## ## A numeric vector of the same length as the columns in @var{X} with the ## mean values corresponding to each predictor. If the predictor variables ## have not been standardized, then @qcode{'obj.Mu'} is empty. This ## property is read-only. ## ## Each predictor is summarized from every observation where that ## predictor is present, so a row holding a missing value in another ## predictor still contributes to this one. ## ## @end deftp Mu = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} NSMethod ## ## Nearest neighbor search method ## ## A character vector specified as either @qcode{'kdtree'}, which creates ## and uses a Kd-tree to find nearest neighbors, or @qcode{'exhaustive'}, ## which uses the exhaustive search algorithm by computing the distance ## values from all points in @var{X} to find nearest neighbors. ## ## Change the @qcode{NSMethod} property using dot notation as in: ## @itemize ## @item @qcode{@var{obj}.NSMethod = @var{newNSMethod}} ## @end itemize ## ## @end deftp NSMethod = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} BucketSize ## ## Maximum data points in each node ## ## A positive integer scalar specifying the maximum number of data points in ## the leaf node of the Kd-tree. @qcode{BucketSize} only applies when the ## @qcode{NSMethod} property is @qcode{'kdtree'}. ## ## Change the @qcode{BucketSize} property using dot notation as in: ## @itemize ## @item @qcode{@var{obj}.BucketSize = @var{maxnum}} ## @end itemize ## ## @end deftp BucketSize = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} BinEdges ## ## Bin edges of the predictors ## ## A cell array with one entry per predictor, holding that predictor's bin ## edges where the learner discretized it before fitting. It is empty here ## and stays empty: this learner fits the predictors as they are, and ## MATLAB's reports an empty cell for it as well. ## ## This property is read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} ModelParameters ## ## Fitting options, as they were given ## ## A structure holding the parameters of the fit: @qcode{NumNeighbors}, ## @qcode{NSMethod}, @qcode{Distance}, @qcode{BucketSize}, ## @qcode{IncludeTies}, @qcode{DistanceWeight}, @qcode{BreakTies}, ## @qcode{Exponent}, @qcode{Cov}, @qcode{Scale}, @qcode{StandardizeData}, ## and the @qcode{Version}, @qcode{Method} and @qcode{Type} tags. ## ## Each of the three distance parameters belongs to one metric and is ## empty under the others: @qcode{Exponent} to @qcode{'minkowski'}, ## @qcode{Cov} to @qcode{'mahalanobis'} and @qcode{Scale} to ## @qcode{'seuclidean'}. @qcode{Cov} and @qcode{Scale} hold what was ## passed and stay empty otherwise, while @qcode{Exponent} carries its ## default of 2 for a @qcode{'minkowski'} fit that did not name one. ## What the fit used in every case is the @qcode{DistParameter} property. ## @qcode{BucketSize} is likewise empty unless the search is ## @qcode{'kdtree'}, the only method that reads it. This property is ## read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} HyperparameterOptimizationResults ## ## Results of the hyperparameter optimization ## ## @strong{Always empty.} It is declared for MATLAB compatibility, where ## it holds what an automatic search over the hyperparameters found. This ## class fits the parameters it is given and runs no such search, so there ## is nothing to report. This property is read-only. ## ## @end deftp HyperparameterOptimizationResults = []; endproperties ## Properties a user may set after the model is built. Each one is ## validated by its set method below. properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} NumNeighbors ## ## Number of nearest neighbors ## ## A positive integer value specifyingNumber of nearest neighbors in @var{X} ## used to classify each point during prediction. Change the ## @qcode{NumNeighbors} property using dot notation as in: ## @itemize ## @item @qcode{@var{obj}.NumNeighbors = @var{newNumNeighbors}} ## @end itemize ## ## ## This property may be assigned after fitting. A value larger than ## @qcode{NumObservations} is reduced to it rather than refused. ## @end deftp NumNeighbors = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} Distance ## ## Distance metric ## ## A character vector specifying the distance metric used by the ## neighbor-searcher method, or a function handle to a custom distance ## function. See the available distance metrics in @code{knnsearch} for ## more info. A custom distance function must have the form ## @qcode{@var{D2} = @var{distfun} (@var{ZI}, @var{ZJ})}, where ## @var{ZI} is a @math{1*N} vector containing one row of the predictor ## data, @var{ZJ} is an @math{M2*N} matrix containing multiple rows of the ## predictor data, and @var{D2} is an @math{M2*1} vector of distances ## whose @math{k}-th element is the distance between the observations ## @var{ZI} and @qcode{@var{ZJ}(@var{k},:)}. A custom distance function ## carries no @qcode{DistParameter}. Change the @qcode{Distance} ## property using dot notation as in: ## @itemize ## @item @qcode{@var{obj}.Distance = @var{newDistance}} ## @end itemize ## ## ## This property may be assigned after fitting. @qcode{NSMethod} is ## read-only and constrains it: a @qcode{'kdtree'} model takes ## @qcode{'euclidean'}, @qcode{'cityblock'}, @qcode{'chebychev'} and ## @qcode{'minkowski'} only, and never a function handle. Assigning a ## different metric recomputes @qcode{DistParameter}, since a parameter ## belonging to one metric means nothing under another. ## @end deftp Distance = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} DistanceWeight ## ## Distance weighting function ## ## A character vector or a function handle specifying the distance weighting ## function, which can be any of the following values: ## ## @itemize ## @item @qcode{'equal'}, which corresponds to @code{@@(d) d}. ## @item @qcode{'inverse'}, which corresponds to @code{@@(d) 1/d}. ## @item @qcode{'squaredinverse'}, which corresponds to @code{@@(d) 1/d.^2}. ## @item @qcode{@@fcn}, which is a function handle that accepts a matrix of ## nonnegative distances, and returns a matrix the same size containing ## nonnegative distance weights. ## @end itemize ## ## Change the @qcode{DistanceWeight} property ## using dot notation as in: ## @itemize ## @item @qcode{@var{obj}.DistanceWeight = @var{newDistanceWeight}} ## @end itemize ## ## ## A character vector naming the weight, or the @code{func2str} form of a ## supplied handle. This property may be assigned after fitting. ## @end deftp DistanceWeight = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} BreakTies ## ## Tie-breaking algorithm ## ## A character vector specifying the tie-breaking algorithm used by the ## @code{predict} method, when multiple classes have the same smallest cost. ## It can be one of the following: ## ## @itemize ## @item @qcode{'smallest'} (default), which favors the class with the ## smallest index among the tied groups, i.e. the one that appears first in ## the training labelled data. ## @item @qcode{'nearest'}, which favors the class with the nearest neighbor ## among the tied groups, i.e. the class with the closest member point ## according to the distance metric used. ## @item @qcode{'random'}, which randomly picks one class among the tied ## groups. ## @end itemize ## ## The tie-breaking algorithm is only used when @qcode{IncludeTies} is ## @qcode{false}. Change the @qcode{BreakTies} property using dot notation ## as in: ## @itemize ## @item @qcode{@var{obj}.BreakTies = @var{algorithm}} ## @end itemize ## ## ## This property may be assigned after fitting. It decides the label ## when two classes hold the same weight among the neighbours, and it ## applies whether or not @qcode{IncludeTies} is set. ## @end deftp BreakTies = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} IncludeTies ## ## Flag for handling ties ## ## A logical scalar specifying whether prediction includes all the neighbors ## whose distance values are equal to the @math{k^th} smallest distance. If ## @qcode{IncludeTies} is @qcode{true}, prediction includes all of these ## neighbors. Otherwise, prediction uses exactly @math{k} neighbors. ## ## Change the @qcode{IncludeTies} property using dot notation as in: ## @itemize ## @item @qcode{@var{obj}.IncludeTies = @var{flag}} ## @end itemize ## ## ## This property may be assigned after fitting. ## @end deftp IncludeTies = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} DistParameter ## ## Parameter for distance metric ## ## A positive definite covariance matrix, a positive scalar, or a vector of ## positive scale values specifying the parameter for the corresponding ## distance metric as shown below: ## ## @itemize ## @item @qcode{'mahalanobis'} accepts a positive definite covariance ## matrix. ## @item @qcode{'minkowski'} accepts a positive scalar as the Minkowski ## distance exponent; @code{Inf} gives the Chebychev distance. ## @item @qcode{'seuclidean'} accepts a vector of positive scale values of ## equal length as the number of predictors in @var{X}. ## @end itemize ## ## For any other distance metric, @qcode{DistParameter} is empty ## @qcode{([])}. Change the @qcode{DistParameter} property using dot ## notation as in: ## @itemize ## @item @qcode{@var{obj}.DistParameter = @var{distParam}} ## @end itemize ## ## ## This property may be assigned after fitting, but only under the three ## metrics that carry one: @qcode{'minkowski'}, @qcode{'seuclidean'} and ## @qcode{'mahalanobis'}. Under any other metric there is nothing for it ## to mean and the assignment is refused. ## ## @strong{Deviation from MATLAB.} A @qcode{'seuclidean'} scale of zeros ## is refused here. MATLAB accepts it, then warns from inside its distance ## routine at predict time and answers anyway, which contradicts its own ## message that the scale must hold positive values. A zero scale divides ## that predictor by nothing, so it is rejected where it is given rather ## than surfacing later as a warning attached to an answer. ## @end deftp DistParameter = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} Cost ## ## Cost of Misclassification ## ## A square matrix specifying the cost of misclassification of a point. ## @qcode{Cost(i,j)} is the cost of classifying a point into class @qcode{j} ## if its true class is @qcode{i} (that is, the rows correspond to the true ## class and the columns correspond to the predicted class). The order of ## the rows and columns in @qcode{Cost} corresponds to the order of the ## classes in @qcode{ClassNames}. The number of rows and columns in ## @qcode{Cost} is the number of unique classes in the response. By ## default, @qcode{Cost(i,j) = 1} if @qcode{i != j}, and ## @qcode{Cost(i,j) = 0} if @qcode{i = j}. In other words, the cost is 0 ## for correct classification and 1 for incorrect classification. ## ## Add or change the @qcode{Cost} property using dot notation as in: ## @itemize ## @item @qcode{@var{obj}.Cost = @var{costMatrix}} ## @end itemize ## ## ## A cost may also be given as a struct with the fields ## @qcode{ClassNames} and @qcode{ClassificationCosts}, which names the ## order its own matrix is written in. That matrix is permuted into the ## order of @qcode{ClassNames} above, so a caller need not know which ## order the classes are in. It must name every class. ## ## A cost must be floating point, not sparse, not complex, non-negative ## and zero down its diagonal, and must hold no @qcode{NaN} or ## @qcode{Inf}. A @code{single} is widened to @code{double}. ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} Prior ## ## Prior probability for each class ## ## A numeric vector specifying the prior probabilities for each class. The ## order of the elements in @qcode{Prior} corresponds to the order of the ## classes in @qcode{ClassNames}. ## ## Add or change the @qcode{Prior} property using dot notation as in: ## @itemize ## @item @qcode{@var{obj}.Prior = @var{priorVector}} ## @end itemize ## ## Specified as a row vector with one entry per class, in the order of ## @qcode{ClassNames}, and rescaled to sum to one. It may be given as ## @qcode{'empirical'}, @qcode{'uniform'}, a numeric vector, or a ## structure with @qcode{ClassNames} and @qcode{ClassProbs} fields, which ## assigns each probability by class name rather than by position. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} ScoreTransform ## ## Transformation function for classification scores ## ## Specified as a function handle for transforming the classification ## scores. Add or change the @qcode{ScoreTransform} property using dot ## notation as in: ## ## @itemize ## @item @qcode{@var{obj}.ScoreTransform = 'function_name'} ## @item @qcode{@var{obj}.ScoreTransform = @@function_handle} ## @end itemize ## ## When specified as a character vector, it can be any of the following ## built-in functions. Nevertheless, the @qcode{ScoreTransform} property ## always stores their function handle equivalent. ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Value} @tab @var{Description} ## @item @qcode{'doublelogit'} @tab @math{1 ./ (1 + exp (-2 * x))} ## @item @qcode{'invlogit'} @tab @math{log (x ./ (1 - x))} ## @item @qcode{'ismax'} @tab Sets the score for the class with the ## largest score to 1, and for all other classes to 0 ## @item @qcode{'logit'} @tab @math{1 ./ (1 + exp (-x))} ## @item @qcode{'none'} @tab @math{x} (no transformation) ## @item @qcode{'identity'} @tab @math{x} (no transformation) ## @item @qcode{'sign'} @tab ## @math{-1 for x < 0, 0 for x = 0, 1 for x > ## 0} ## @item @qcode{'symmetric'} @tab @math{2 * x - 1} ## @item @qcode{'symmetricismax'} @tab Sets the score for the class ## with the largest score to 1, and for all other classes to -1 ## @item @qcode{'symmetriclogit'} @tab @math{2 ./ (1 + exp (-x)) - 1} ## @end multitable ## ## @end deftp ScoreTransform = 'none'; ## -*- texinfo -*- ## @deftp {ClassificationKNN} {property} CacheSize ## ## Size of the Gram matrix cache ## ## A positive scalar giving the cache size in megabytes, 1000 by default. ## Change the @qcode{CacheSize} property using dot notation as in: ## @itemize ## @item @qcode{@var{obj}.CacheSize = @var{newCacheSize}} ## @end itemize ## ## This property is stored and reported for compatibility and ## @strong{does not affect the fit or any prediction}. A nearest-neighbour ## model keeps no Gram matrix to cache: it holds the training data and ## computes each distance when asked. Assigning it changes nothing but ## the value read back. ## ## MATLAB carries the same property and hides it from @code{properties}, ## where this package reports it, so that a value a user may set is a ## value a user can find. ## @end deftp CacheSize = 1000; endproperties ## Readable by the counterpart class, which copies it, and kept out of ## the documented surface. properties (GetAccess = public, SetAccess = protected, Hidden) ## The callable that DistanceWeight names. The property itself holds the ## text, as MATLAB reports it, and predict reaches for this. DWfun = @(d) ones (size (d)); ## Raised once the constructor is done. The set methods validate a user's ## assignment; the values the fit computes for itself are already checked ## where they are built, and one of them, the Mahalanobis covariance, is ## legitimately singular on data whose predictors are collinear. Fitted = false; STfun = @(x) x; ## The observation weights as they were given, before the prior scaled ## them, so that reassigning Prior can re-derive W. RawWeights = []; endproperties ## Set methods for the properties a user may assign. methods (Hidden) function this = set.Cost (this, val) gnY = this.ClassNames; if (isempty (val)) this.Cost = cast (! eye (classCount (gnY)), 'double'); else ## Everything a cost must be, and the struct form, which ## is permuted into this model's class order. [val, errmsg] = costMatrix (val, gnY); if (! isempty (errmsg)) error ("ClassificationKNN: %s", errmsg); endif this.Cost = val; endif endfunction function this = set.Prior (this, val) gY = labelIndices (this.ClassNames, this.Y); K = classCount (this.ClassNames); if (isstruct (val)) val = priorFromStruct (val, this.ClassNames, 'ClassificationKNN'); endif if (strcmpi ('uniform', val)) this.Prior = ones (1, K) ./ K; elseif (isempty (val) || strcmpi ('empirical', val)) ## Empirical over the weights, which are counts when none were given. pr = zeros (K, 1); for i = 1:K if (isempty (this.RawWeights)) pr(i) = sum (gY == i); else pr(i) = sum (double (this.RawWeights(gY == i))); endif endfor this.Prior = pr(:)' ./ sum (pr); elseif (isnumeric (val)) if (K != numel (val)) error (strcat ("ClassificationKNN: the elements in 'Prior' do", ... " not correspond to the selected classes in Y.")); endif this.Prior = val(:)' ./ sum (val); endif ## The weights follow the prior, so they are rebuilt with it if (isempty (this.RawWeights)) this.W = priorWeights (this.Prior, gY, numel (gY)); else this.W = cast (priorNormalize (double (this.RawWeights), gY, ... this.Prior), class (this.RawWeights)); endif endfunction ## The six properties a fitted model may be reassigned. All of them are ## live: a model whose Distance or NumNeighbors is changed predicts exactly ## as one refitted with the new value would, because a lazy learner stores ## its training data and does the work at predict time. function this = set.NumNeighbors (this, val) if (! (isnumeric (val) && isscalar (val) && val > 0 && fix (val) == val)) error (strcat ("ClassificationKNN: 'NumNeighbors' must be a", ... " positive integer.")); endif ## More neighbours than there are observations is reduced rather than ## refused, which is what the fit does with the same value. if (! isempty (this.X)) val = min (val, rows (this.X)); endif this.NumNeighbors = val; endfunction function this = set.Distance (this, val) ## The same five the constructor builds a kd-tree for. 'manhattan' is ## a synonym of 'cityblock' and was missing here, so a kd-tree model ## fitted with it could be neither reassigned nor reloaded. kdt = {'euclidean', 'cityblock', 'manhattan', 'chebychev', ... 'minkowski'}; all = [kdt, {'seuclidean', 'mahalanobis', 'manhattan', 'cosine', ... 'correlation', 'spearman', 'hamming', 'jaccard'}]; ## A kd-tree can only be searched with the metrics it was built for, and ## NSMethod is read-only, so the metric is what has to give way. if (strcmpi (this.NSMethod, 'kdtree')) if (! (ischar (val) && any (strcmpi (kdt, val)))) error (strcat ("ClassificationKNN: 'Distance' for a kd-tree", ... " model can only be 'euclidean', 'cityblock',", ... " 'manhattan', 'chebychev', or 'minkowski'.")); endif elseif (! (is_function_handle (val) || (ischar (val) && any (strcmpi (all, val))))) error ("ClassificationKNN: unsupported distance metric."); endif ## A custom metric is probed here as the constructor probes it: called ## on one observation against the training data, it must return one ## distance per row. Taken unchecked, it would defer the failure to ## predict, which names neither the property nor the user's function. if (is_function_handle (val) && ! isempty (this.X)) try D2 = val (this.X(1,:), this.X); catch error (strcat ("ClassificationKNN: invalid function", ... " handle for distance metric.")); end_try_catch if (! isequal (size (D2), [rows(this.X), 1])) error (strcat ("ClassificationKNN: custom distance", ... " function produces wrong output size.")); endif endif if (ischar (val) && ischar (this.Distance) && strcmpi (val, this.Distance)) return; # unchanged, so the parameter it carries is kept endif this.Distance = val; ## The parameter belongs to the metric, so it is recomputed and whatever ## the previous metric held is discarded. if (! isempty (this.X)) this.DistParameter = knndistparam (val, this.X, ! isempty (this.Mu)); endif endfunction function this = set.DistanceWeight (this, val) [f, dw] = parseDistanceWeight (val, 'ClassificationKNN'); this.DistanceWeight = dw; this.DWfun = f; endfunction function this = set.BreakTies (this, val) if (! (ischar (val) && any (strcmpi ({'smallest', 'random', ... 'nearest'}, val)))) error (strcat ("ClassificationKNN: 'BreakTies' must be", ... " 'smallest', 'random' or 'nearest'.")); endif this.BreakTies = lower (val); endfunction function this = set.IncludeTies (this, val) if (! (islogical (val) && isscalar (val))) error (strcat ("ClassificationKNN: 'IncludeTies' must be a", ... " logical scalar.")); endif this.IncludeTies = val; endfunction function this = set.DistParameter (this, val) ## An empty value is the state a metric without a parameter is in, so it ## is always allowed; anything else has to belong to the current metric. if (! isempty (val) && this.Fitted) if (! (ischar (this.Distance) && any (strcmpi ({'minkowski', 'seuclidean', ... 'mahalanobis'}, this.Distance)))) error (strcat ("ClassificationKNN: 'DistParameter' can only be", ... " provided when 'Distance' is 'minkowski',", ... " 'mahalanobis', or 'seuclidean'.")); endif switch (lower (this.Distance)) case 'minkowski' if (! (isnumeric (val) && isscalar (val) && val > 0)) error (strcat ("ClassificationKNN: the exponent for the", ... " Minkowski distance must be a positive", ... " scalar.")); endif case 'seuclidean' if (! (isnumeric (val) && isvector (val) && all (val > 0) && numel (val) == columns (this.X))) error (strcat ("ClassificationKNN: the scale for the", ... " standardized Euclidean distance must be a", ... " vector of positive values, with length", ... " equal to the number of columns in X.")); endif case 'mahalanobis' if (! (isnumeric (val) && issquare (val) && columns (val) == columns (this.X))) error (strcat ("ClassificationKNN: the covariance for the", ... " Mahalanobis distance must be a square", ... " matrix with the same number of columns", ... " as X.")); endif if (! (isequal (val, val') && all (eig (val) > 0))) error (strcat ("ClassificationKNN: the covariance for the", ... " Mahalanobis distance must be symmetric and", ... " positive definite.")); endif endswitch endif this.DistParameter = val; endfunction function this = set.CacheSize (this, val) if (! (isnumeric (val) && isscalar (val) && isreal (val) && isfinite (val) && val > 0)) error (strcat ("ClassificationKNN: 'CacheSize' must be a", ... " positive finite scalar.")); endif this.CacheSize = val; endfunction function this = set.ScoreTransform (this, val) [f, nm] = parseScoreTransform (val, 'ClassificationKNN'); this.ScoreTransform = nm; this.STfun = f; endfunction ## Custom display function disp (this) fprintf ("\n ClassificationKNN\n\n"); ## Print selected properties fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); if (iscellstr (this.ClassNames)) str = repmat ({'''%s'''}, 1, numel (this.ClassNames)); str = strcat ('{', strjoin (str, ' '), '}'); str = sprintf (str, this.ClassNames{:}); elseif (ischar (this.ClassNames)) str = repmat ({'''%s'''}, 1, rows (this.ClassNames)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, cellstr (this.ClassNames){:}); else # single, double, logical str = repmat ({'%d'}, 1, numel (this.ClassNames)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, this.ClassNames); endif fprintf ("%+25s: %s\n", 'ClassNames', str); fprintf ("%+25s: '%s'\n", 'ScoreTransform', this.ScoreTransform); fprintf ("%+25s: %d\n", 'NumObservations', this.NumObservations); fprintf ("%+25s: %d\n", 'NumPredictors', this.NumPredictors); if (is_function_handle (this.Distance)) fprintf ("%+25s: %s\n", 'Distance', func2str (this.Distance)); else fprintf ("%+25s: '%s'\n", 'Distance', this.Distance); endif fprintf ("%+25s: '%s'\n", 'NSMethod', this.NSMethod); fprintf ("%+25s: %d\n", 'NumNeighbors', this.NumNeighbors); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {ClassificationKNN} {@var{obj} =} ClassificationKNN (@var{X}, @var{Y}) ## @deftypefnx {ClassificationKNN} {@var{obj} =} ClassificationKNN (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationKNN} {@var{obj} =} ClassificationKNN (@var{Tbl}, @var{formula}) ## @deftypefnx {ClassificationKNN} {@var{obj} =} ClassificationKNN (@var{Tbl}, @var{Y}) ## @deftypefnx {ClassificationKNN} {@var{obj} =} ClassificationKNN (@dots{}, @var{name}, @var{value}) ## ## Create a @qcode{ClassificationKNN} class object containing a k-Nearest ## Neighbor classification model. ## ## @code{@var{obj} = ClassificationKNN (@var{X}, @var{Y})} returns a ## ClassificationKNN object, with @var{X} as the predictor data and @var{Y} ## containing the class labels of observations in @var{X}. ## ## @itemize ## @item ## @code{X} must be a @math{N*P} numeric matrix of input data where rows ## correspond to observations and columns correspond to features or ## variables. @var{X} will be used to train the kNN model. ## @item ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels ## of corresponding predictor data in @var{X}. @var{Y} can contain any type ## of categorical data. @var{Y} must have same numbers of Rows as @var{X}. ## @end itemize ## ## @code{@var{obj} = ClassificationKNN (@dots{}, @var{name}, @var{value})} ## returns a ClassificationKNN object with parameters specified by the ## following @qcode{@var{name}, @var{value}} paired input arguments: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PredictorNames'} @tab A cell array of character ## vectors specifying the names of the predictors. The length of this array ## must match the number of columns in @var{X}. ## ## @item @qcode{'ResponseName'} @tab A character vector specifying the ## name of the response variable. ## ## @item @qcode{'ClassNames'} @tab Names of the classes in the class ## labels, @var{Y}, used for fitting the GAM model. ## @qcode{ClassNames} are of the same type as the class labels in @var{Y}. ## ## @item @qcode{'Cost'} @tab An @math{N*R} numeric matrix containing ## misclassification cost for the corresponding instances in @var{X}, where ## @math{R} is the number of unique categories in @var{Y}. If an instance ## is correctly classified into its category the cost is calculated to be 1, ## otherwise 0. The cost matrix can be altered by using ## @code{@var{Mdl}.cost = somecost}. By default, its value is ## @qcode{@var{cost} = ones (rows (X), numel (unique (Y)))}. ## ## @item @qcode{'Prior'} @tab A numeric vector specifying the prior ## probabilities for each class. The order of the elements in @qcode{Prior} ## corresponds to the order of the classes in @qcode{ClassNames}. ## Alternatively, you can specify @qcode{'empirical'} to use the empirical ## class probabilities or @qcode{'uniform'} to assume equal class ## probabilities. ## A class whose prior is 0 is removed from the data before fitting, with a ## warning, as in MATLAB. ## ## @item @qcode{'Weights'} @tab A single or double vector of nonnegative ## observation weights, one per row of @var{X}. Each class carries its ## prior, spread over its observations in proportion to their weights, and a ## neighbour votes with that weight. An empirical prior sums the weights ## per class, standardization uses weighted means and standard deviations, ## and a row of zero weight is left out. The model's @code{W} keeps the ## class of the weights, while every computation runs in double, so ## @code{Prior} is double where MATLAB returns single. ## ## @item @qcode{'ScoreTransform'} @tab A user-defined function handle ## or a character vector specifying one of the following builtin functions ## specifying the transformation applied to predicted classification scores. ## Supported values include @qcode{'doublelogit'}, @qcode{'invlogit'}, ## @qcode{'ismax'}, @qcode{'logit'}, @qcode{'none'}, @qcode{'identity'}, ## @qcode{'sign'}, @qcode{'symmetric'}, @qcode{'symmetricismax'}, and ## @qcode{'symmetriclogit'}. ## ## @item @qcode{'BreakTies'} @tab A character vector specifying the ## tie-breaking algorithm used by @code{predict} method, when multiple ## classes have the same smallest cost. Available options are ## @qcode{'smallest'} (default), which uses the smallest index among tied ## groups, @qcode{'nearest'}, which uses the class with the nearest neighbor ## among tied groups, and @qcode{'random'}, which randomly selects one of ## the tied groups. ## ## @item @qcode{'NumNeighbors'} @tab A positive integer value that ## specifies the number of nearest neighbors to be found in the kNN search ## algorithm for classifying each point during prediction. By default, ## it is 1. ## ## @item @qcode{'Distance'} @tab Any valid distance metric supported by ## the @code{pdist2} function. Note that the allowable distance metrics ## depend on the selected nearest neighbor search method. ## ## @item @qcode{'DistanceWeight'} @tab Either a distance weighting ## function, specified either as a function handle, which accepts a matrix ## of nonnegative distances and returns a matrix the same size containing ## nonnegative distance weights, or a character vector with one of the ## following values: @qcode{'equal'}, which corresponds to no weighting; ## @qcode{'inverse'}, which corresponds to a weight equal to ## @math{1/distance}; @qcode{'squaredinverse'}, which corresponds to a ## weight equal to @math{1/distance^2}. ## ## @item @qcode{'Cov'} @tab A square matrix with the same number of ## columns @var{X} specifying the covariance matrix for computing the ## mahalanobis distance. This must be a positive definite matrix matching. ## This argument is only valid when the selected distance metric is ## @qcode{'mahalanobis'}. ## ## @item @qcode{'Exponent'} @tab A positive scalar (usually an integer) ## specifying the Minkowski distance exponent. This argument is only valid ## when the selected distance metric is @qcode{'minkowski'}. By default, ## it is 2. An exponent of @code{Inf} gives the Chebychev distance. ## ## @item @qcode{'Scale'} @tab A nonnegative numeric vector specifying ## the scale parameters for the standardized Euclidean distance. The vector ## length must be equal to the number of columns in @var{X}. This argument ## is only valid when the selected distance metric is @qcode{'seuclidean'}, ## in which case each coordinate of @var{X} is scaled by the corresponding ## element of @qcode{'scale'}, as is each query point in @var{Y}. By ## default, the scale parameter is the standard deviation of each coordinate ## in @var{X}. If a variable in @var{X} is constant, i.e. zero variance, ## this value is forced to 1 to avoid division by zero. This is the ## equivalent of this variable not being standardized. ## ## @item @qcode{'NSMethod'} @tab A character vector specifying the ## nearest neighbor search method used by @code{knnsearch}, which can be ## @qcode{'kdtree'} or @qcode{'exhaustive'}. See @code{knnsearch} for more ## information about default values and allowable distance metrics for each ## search method. ## ## @item @qcode{'BucketSize'} @tab A positive integer value specifying ## the maximum number of data points in the leaf node of the Kd-tree. This ## argument is meaningful only when the selected nearest neighbor search ## method is @qcode{'kdtree'}. By default, it is 50. ## @end multitable ## ## @seealso{fitcknn, knnsearch, rangesearch, pdist2} ## @end deftypefn function this = ClassificationKNN (X, Y, varargin) ## Check for sufficient number of input arguments if (nargin < 2) error ("ClassificationKNN: too few input arguments."); endif ## A table names its own predictors and says which hold levels [this, X, Y, varargin] = resolveTable (this, 'ClassificationKNN', ... X, Y, varargin); ## Check X and Y have the same number of observations if (rows (X) != rows (Y)) error ("ClassificationKNN: number of rows in X and Y must be equal."); endif ## Assign original X and Y data to the ClassificationKNN object this.X = X; this.Y = Y; ## Get groups in Y [gY, gnY, glY] = grp2idx (Y); ## Set default values before parsing optional parameters Standardize = false; PredictorNames = []; ResponseName = []; ClassNames = []; Prior = []; Cost = []; Scale = []; # Distance scale for 'seuclidean' Cov = []; # Covariance matrix for 'mahalanobis' Exponent = []; # Exponent for 'minkowski' BreakTies = []; NumNeighbors = []; Distance = []; DistanceWeight = []; DistParameter = []; NSMethod = []; IncludeTies = false; BucketSize = 50; CacheSize = 1000; CatPreds = []; Weights = []; ## Number of parameters for Standardize, Scale, Cov (maximum 1 allowed) SSC = 0; ## Parse extra parameters while (numel (varargin) > 0) switch (tolower (varargin {1})) case 'weights' Weights = varargin{2}; errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("ClassificationKNN: %s", errmsg); endif if (! (isnumeric (Weights) && isvector (Weights) && isreal (Weights))) error (strcat ("ClassificationKNN: 'Weights' must be a real", ... " numeric vector.")); endif if (numel (Weights) != rows (X)) error (strcat ("ClassificationKNN: 'Weights' must have one", ... " element per row in X.")); endif if (any (Weights < 0) || ! (sum (Weights) > 0)) error (strcat ("ClassificationKNN: 'Weights' must be", ... " nonnegative and must not be all zero.")); endif case 'standardize' if (SSC < 1) Standardize = varargin{2}; if (! (Standardize == true || Standardize == false)) error (strcat ("ClassificationKNN: 'Standardize' must", ... " be either true or false.")); endif SSC += 1; else error (strcat ("ClassificationKNN: 'Standardize' cannot", ... " simultaneously be specified with either", ... " Scale or Cov.")); endif case 'predictornames' PredictorNames = varargin{2}; if (! iscellstr (PredictorNames)) error (strcat ("ClassificationKNN: 'PredictorNames' must", ... " be supplied as a cellstring array.")); elseif (columns (PredictorNames) != columns (X)) error (strcat ("ClassificationKNN: 'PredictorNames' must", ... " have the same number of columns as X.")); endif case 'responsename' ResponseName = varargin{2}; if (! ischar (ResponseName)) error (strcat ("ClassificationKNN: 'ResponseName'", ... " must be a character vector.")); endif case 'classnames' ClassNames = varargin{2}; if (! (iscellstr (ClassNames) || isnumeric (ClassNames) || islogical (ClassNames) || ischar (ClassNames) || isa (ClassNames, 'categorical') || isa (ClassNames, 'string'))) error (strcat ("ClassificationKNN: 'ClassNames' must be a", ... " categorical array, a character array, a", ... " string array, a logical vector, a numeric", ... " vector, or a cell array of character", ... " vectors.")); endif case 'prior' Prior = varargin{2}; if (! (isstruct (Prior) || (isnumeric (Prior) && isvector (Prior)) || (ischar (Prior) && (strcmpi (Prior, 'empirical') || strcmpi (Prior, 'uniform'))))) error (strcat ("ClassificationKNN: 'Prior' must be either", ... " a numeric vector or a character vector.")); endif case 'cost' Cost = varargin{2}; ## A struct carrying its own class order is a cost too, ## and is resolved by the property's own set method. if (! (isstruct (Cost) || (isnumeric (Cost) && issquare (Cost)))) error (strcat ("ClassificationKNN: 'Cost' must be", ... " a numeric square matrix.")); endif case 'scoretransform' name = 'ClassificationKNN'; this.ScoreTransform = varargin{2}; case 'breakties' BreakTies = varargin{2}; if (! ischar (BreakTies)) error (strcat ("ClassificationKNN: 'BreakTies'", ... " must be a character vector.")); endif ## Check that all class names are available in gnY BTs = {'smallest', 'nearest', 'random'}; if (! any (strcmpi (BTs, BreakTies))) error ("ClassificationKNN: invalid value for 'BreakTies'."); endif case 'numneighbors' NumNeighbors = varargin{2}; if (! (isnumeric (NumNeighbors) && isscalar (NumNeighbors) && NumNeighbors > 0 && fix (NumNeighbors) == NumNeighbors)) error (strcat ("ClassificationKNN: 'NumNeighbors'", ... " must be a positive integer.")); endif case 'distance' Distance = varargin{2}; DMs = {'euclidean', 'seuclidean', 'mahalanobis', 'minkowski', ... 'cityblock', 'manhattan', 'chebychev', 'cosine', ... 'correlation', 'spearman', 'hamming', 'jaccard'}; if (ischar (Distance)) if (! any (strcmpi (DMs, Distance))) error ("ClassificationKNN: unsupported distance metric."); endif elseif (is_function_handle (Distance)) ## Check the input output sizes of the user function D2 = []; try D2 = Distance(X(1,:), X); catch ME error (strcat ("ClassificationKNN: invalid function", ... " handle for distance metric.")); end_try_catch Xrows = rows (X); if (! isequal (size (D2), [Xrows, 1])) error (strcat ("ClassificationKNN: custom distance", ... " function produces wrong output size.")); endif else error ("ClassificationKNN: invalid distance metric."); endif case 'distanceweight' [this.DWfun, this.DistanceWeight] = ... parseDistanceWeight (varargin{2}, 'ClassificationKNN'); DistanceWeight = this.DistanceWeight; case 'scale' if (SSC < 1) Scale = varargin{2}; if (! (isnumeric (Scale) && isvector (Scale))) error ("ClassificationKNN: 'Scale' must be a numeric vector."); endif SSC += 1; else error (strcat ("ClassificationKNN: 'Scale' cannot", ... " simultaneously be specified with either", ... " 'Standardize' or 'Cov'.")); endif case 'cov' if (SSC < 1) Cov = varargin{2}; [~, p] = chol (Cov); if (p != 0) error (strcat ("ClassificationKNN: 'Cov' must be a", ... " symmetric positive definite matrix.")); endif SSC += 1; else error (strcat ("ClassificationKNN: 'Cov' cannot", ... " simultaneously be specified with either", ... " 'Standardize' or 'Scale'.")); endif case 'exponent' Exponent = varargin{2}; if (! (isnumeric (Exponent) && isscalar (Exponent) && Exponent > 0)) error (strcat ("ClassificationKNN: 'Exponent'", ... " must be a positive scalar.")); endif case 'nsmethod' NSMethod = varargin{2}; NSM = {'kdtree', 'exhaustive'}; if (! ischar (NSMethod)) error (strcat ("ClassificationKNN: 'NSMethod' must", ... " be a character vector.")); endif if (! any (strcmpi (NSM, NSMethod))) error (strcat ("ClassificationKNN: 'NSMethod' must", ... " be either 'kdtree' or 'exhaustive'.")); endif case 'cachesize' CacheSize = varargin{2}; if (! (isnumeric (CacheSize) && isscalar (CacheSize) && isreal (CacheSize) && isfinite (CacheSize) && CacheSize > 0)) error (strcat ("ClassificationKNN: 'CacheSize' must be a", ... " positive finite scalar.")); endif case 'includeties' IncludeTies = varargin{2}; if (! (IncludeTies == true || IncludeTies == false)) error (strcat ("ClassificationKNN: 'IncludeTies'", ... " must be either true or false.")); endif case 'bucketsize' BucketSize = varargin{2}; if (! (isnumeric (BucketSize) && isscalar (BucketSize) && BucketSize > 0 && fix (BucketSize) == BucketSize)) error (strcat ("ClassificationKNN: 'BucketSize'", ... " must be a positive integer.")); endif case 'categoricalpredictors' ## Measured on R2024a, which takes either every predictor as ## categorical or none. CatPreds = varargin{2}; ## A table names the categorical predictors by index, and a ## table whose every predictor holds levels names them all, ## which is what 'all' says in fewer characters if (isnumeric (CatPreds) && ! isempty (CatPreds) && isequal (sort (CatPreds(:)'), 1:columns (X))) CatPreds = 'all'; varargin{2} = 'all'; endif if (! (isempty (CatPreds) || (ischar (CatPreds) && strcmpi (CatPreds, 'all')))) error (strcat ("ClassificationKNN: 'CategoricalPredictors'", ... " must be empty or 'all'.")); endif otherwise error (strcat ("ClassificationKNN: invalid parameter",... " name in optional pair arguments.")); endswitch varargin(1:2) = []; endwhile ## Generate default predictors and response variable names (if necessary) NumPredictors = columns (X); if (isempty (PredictorNames)) for i = 1:NumPredictors PredictorNames {i} = strcat ("x", num2str (i)); endfor endif if (isempty (ResponseName)) ResponseName = 'Y'; endif ## Assign predictors and response variable names this.NumPredictors = NumPredictors; this.PredictorNames = PredictorNames; this.CategoricalPredictors = []; if (! isempty (CatPreds)) this.CategoricalPredictors = 1:NumPredictors; endif this.ExpandedPredictorNames = PredictorNames; this.ResponseName = ResponseName; ## Handle class names, checked once every option is read: a named ## class with prior 0 is removed below, so it may be absent exempt = []; if (! isempty (ClassNames)) exempt = zeroPriorNames (ClassNames, Prior, []); [~, errmsg] = namedClasses (glY, ClassNames, exempt); if (! isempty (errmsg)) error ("ClassificationKNN: %s", errmsg); endif ## The observations of a class ClassNames leaves out are dropped gY(! ismember (gY, namedClasses (glY, ClassNames))) = NaN; endif ## An observation is dropped when its response is missing, when every ## predictor is missing, or when its weight is zero, as R2024a drops it. ## A row missing only some predictors is kept and reported as used, ## while the fit below draws on the complete observations alone. RowsUsed = ! isnan (gY) & ! all (isnan (X), 2); if (! isempty (Weights)) RowsUsed = RowsUsed & Weights(:) > 0; endif Yret = Y(RowsUsed, :); Xret = X(RowsUsed, :); this.X = Xret; this.Y = Yret; if (isempty (Weights)) this.RawWeights = ones (rows (Xret), 1); else this.RawWeights = Weights(RowsUsed); this.RawWeights = this.RawWeights(:); endif cobs = ! any (isnan (Xret), 2); Y = Yret(cobs, :); X = Xret(cobs, :); ## Renew groups in Y over the retained observations, so a class held ## only by a row with missing predictors is still a class of the model; ## the classes are sorted or in the order ClassNames gives them [this.ClassNames, gret] = classOrder (Yret, ClassNames, exempt); gY = gret(cobs); ## Check X contains valid data if (! (isnumeric (X) && isfinite (X))) error ("ClassificationKNN: invalid values in X."); endif ## Assign the number of observations and their corresponding indices ## on the original data, which will be used for training the model, ## to the ClassificationKNN object this.NumObservations = rows (this.X); ## RowsUsed is left empty when every observation was used, as in MATLAB if (all (RowsUsed)) this.RowsUsed = []; else this.RowsUsed = RowsUsed; endif ## Handle BreakTies if (isempty (BreakTies)) this.BreakTies = 'smallest'; else this.BreakTies = BreakTies; endif ## Handle Cost and Prior this.Cost = Cost; this.Prior = Prior; ## A class with prior 0 is never predicted, so its observations are ## removed; one that costs nothing to misclassify is kept, as MATLAB's ## learner keeps it [drop, P, msg] = removedClasses (this.Prior, [], ... this.ClassNames, gret); if (all (drop)) error ("ClassificationKNN: every class has prior probability 0."); elseif (any (drop)) if (! isempty (msg)) warning ("ClassificationKNN: %s", msg); endif keep = ! drop(gret); used = find (RowsUsed); RowsUsed(used(! keep)) = false; this.X = this.X(keep,:); this.Y = this.Y(keep,:); this.RawWeights = this.RawWeights(keep); kc = keep(cobs); X = X(kc,:); Y = Y(kc,:); gY = gY(kc); cobs = cobs(keep); gret = gret(keep); this.NumObservations = rows (this.X); this.RowsUsed = RowsUsed; this.Prior = P; endif ## Each class carries its prior, spread over its own observations in ## proportion to their weights. Every retained row gets one, those ## missing a predictor included, so that W lines up with X. ## The weights keep their class in the model; every computation runs on ## them as double. W = priorNormalize (double (this.RawWeights), gret, this.Prior); this.W = cast (W, class (this.RawWeights)); ## Handle the Standardize option if (Standardize) ## A lazy learner standardizes at predict time, so each predictor is ## summarized from every observation where it is present, whatever is ## missing elsewhere in the row. The mean and standard deviation are ## weighted by W, the latter unbiased for reliability weights, which ## R2024a reproduces to 4e-15. this.Mu = zeros (1, columns (this.X)); this.Sigma = zeros (1, columns (this.X)); for j = 1:columns (this.X) xj = this.X(:,j); ok = ! isnan (xj); wj = W(ok) / sum (W(ok)); xj = xj(ok); this.Mu(j) = sum (wj .* xj); this.Sigma(j) = sqrt (sum (wj .* (xj - this.Mu(j)) .^ 2) ... / (1 - sum (wj .^ 2))); ## A constant predictor is left unscaled; its weighted mean can ## miss the constant by one rounding, so its deviation need not be ## zero. if (! isempty (xj) && all (xj == xj(1))) this.Sigma(j) = 1; endif endfor this.Sigma(this.Sigma == 0) = 1; # predictor is constant else this.Sigma = []; this.Mu = []; endif ## Get number of neighbors if (isempty (NumNeighbors)) this.NumNeighbors = 1; else ## There are only as many neighbors available as there are training ## samples, so a larger request is capped at that. Asking for more ## raised an internal nonconformant operator error at prediction. this.NumNeighbors = min (NumNeighbors, rows (X)); endif ## Get distance metric if (isempty (Distance)) ## Levels are compared for equality, as R2024a compares them. if (isempty (CatPreds)) Distance = 'euclidean'; else Distance = 'hamming'; endif endif this.Distance = Distance; ## Get distance weight if (isempty (DistanceWeight)) [this.DWfun, this.DistanceWeight] = ... parseDistanceWeight ('equal', 'ClassificationKNN'); endif ## Handle distance metric parameters (Scale, Cov, Exponent) if (! isempty (Scale)) if (! strcmpi (Distance, 'seuclidean')) error (strcat ("ClassificationKNN: 'Scale' is only valid", ... " when distance metric is seuclidean.")); endif if (numel (Scale) != NumPredictors) error (strcat ("ClassificationKNN: 'Scale' vector must have", ... " equal length to the number of columns in X.")); endif if (any (Scale < 0)) error (strcat ("ClassificationKNN: 'Scale' vector must", ... " contain nonnegative scalar values.")); endif this.DistParameter = Scale; else if (strcmpi (Distance, 'seuclidean')) if (Standardize) this.DistParameter = ones (1, NumPredictors); else this.DistParameter = std (X, [], 1); endif endif endif if (! isempty (Cov)) if (! strcmpi (Distance, 'mahalanobis')) error (strcat ("ClassificationKNN: 'Cov' is only valid", ... " when distance metric is 'mahalanobis'.")); endif if (columns (Cov) != NumPredictors) error (strcat ("ClassificationKNN: 'Cov' matrix", ... " must have equal columns as X.")); endif this.DistParameter = Cov; else if (strcmpi (Distance, 'mahalanobis')) this.DistParameter = cov (X); endif endif if (! isempty (Exponent)) if (! strcmpi (Distance, 'minkowski')) error (strcat ("ClassificationKNN: 'Exponent' is only", ... " valid when distance metric is 'minkowski'.")); endif this.DistParameter = Exponent; else if (strcmpi (Distance, 'minkowski')) this.DistParameter = 2; endif endif ## Get Nearest neighbor search method kdm = {'euclidean', 'cityblock', 'manhattan', 'minkowski', 'chebychev'}; if (! isempty (NSMethod)) if (strcmpi ('kdtree', NSMethod) && (! any (strcmpi (kdm, Distance)))) error (strcat ("ClassificationKNN: 'kdtree' method is only va", ... "lid for 'euclidean', 'cityblock', 'manhattan',", ... " 'minkowski', and 'chebychev' distance metrics.")); endif this.NSMethod = NSMethod; else if (any (strcmpi (kdm, Distance)) && NumPredictors <= 10) this.NSMethod = 'kdtree'; else this.NSMethod = 'exhaustive'; endif endif ## Assign IncludeTies, BucketSize and CacheSize properties this.IncludeTies = IncludeTies; this.BucketSize = BucketSize; this.CacheSize = CacheSize; ## The fit as it was asked for, not as it was resolved: Exponent, Cov ## and Scale hold what was passed and nothing else, DistParameter ## holding what was used, and BucketSize applies to the kd-tree alone. if (strcmpi (this.NSMethod, 'kdtree')) BucketSizeIn = this.BucketSize; else BucketSizeIn = []; endif ## Exponent is the exception among the three: MATLAB fills its default ## of 2 for a 'minkowski' fit, where Cov and Scale stay empty until ## they are passed. Measured on R2024a. if (strcmpi (this.Distance, 'minkowski')) ExponentIn = this.DistParameter; else ExponentIn = []; endif this.ModelParameters = struct ( ... 'NumNeighbors', this.NumNeighbors, ... 'NSMethod', this.NSMethod, ... 'Distance', this.Distance, ... 'BucketSize', BucketSizeIn, ... 'IncludeTies', this.IncludeTies, ... 'DistanceWeight', this.DistanceWeight, ... 'BreakTies', this.BreakTies, ... 'Exponent', ExponentIn, ... 'Cov', Cov, ... 'Scale', Scale, ... 'StandardizeData', logical (Standardize), ... 'Version', 1, ... 'Method', 'KNN', ... 'Type', 'classification'); this.Fitted = true; endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationKNN} {@var{labels} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {ClassificationKNN} {[@var{labels}, @var{scores}, @var{cost}] =} predict (@var{obj}, @var{XC}) ## ## Classify new data points into categories using the kNN algorithm from a ## k-Nearest Neighbor classification model. ## ## @code{@var{labels} = predict (@var{obj}, @var{XC})} returns the matrix of ## labels predicted for the corresponding instances in @var{XC}, using the ## predictor data in @code{obj.X} and corresponding labels, @code{obj.Y}, ## stored in the k-Nearest Neighbor classification model, @var{obj}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{ClassificationKNN} class object. ## @item ## @var{XC} must be an @math{M*P} numeric matrix with the same number of ## features @math{P} as the corresponding predictors of the SVM model in ## @var{obj}. ## @end itemize ## ## @code{[@var{labels}, @var{scores}, @var{cost}] = predict (@var{obj}, ## @var{XC})} also returns @var{scores}, which contains the predicted class ## scores or posterior probabilities for each instance of the corresponding ## unique classes, and @var{cost}, which is a matrix containing the expected ## cost of the classifications. By default, @var{scores} returns the ## posterior probabilities for KNN models, unless a specific ScoreTransform ## function has been specified. See @code{fitcknn} for more info. ## ## Note! @code{predict} is explicitly using @qcode{'exhaustive'} as the ## nearest search method due to the very slow implementation of ## @qcode{'kdtree'} in the @code{knnsearch} function. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @seealso{fitcknn, ClassificationKNN, knnsearch} ## @end deftypefn function [labels, scores, cost] = predict (this, XC) ## Check for sufficient input arguments if (nargin < 2) error ("ClassificationKNN.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'ClassificationKNN.predict', XC); ## Check for valid XC if (isempty (XC)) error ("ClassificationKNN.predict: XC is empty."); elseif (this.NumPredictors != columns (XC)) error (strcat ("ClassificationKNN.predict:", ... " XC must have the same number of", ... " predictors as the trained model.")); endif ## Get training data and labels used = true (rows (this.X), 1); X = this.X(used,:); Y = this.Y(used,:); ## Standardize (if necessary) if (! isempty (this.Mu)) X = (X - this.Mu) ./ this.Sigma; XC = (XC - this.Mu) ./ this.Sigma; endif ## Train kNN if (strcmpi (this.Distance, 'seuclidean')) [idx, dist] = knnsearch (X, XC, 'k', this.NumNeighbors, ... 'NSMethod', this.NSMethod, 'Distance', 'seuclidean', ... 'Scale', this.DistParameter, 'sortindices', true, ... 'includeties', this.IncludeTies, ... 'bucketsize', this.BucketSize); elseif (strcmpi (this.Distance, 'mahalanobis')) [idx, dist] = knnsearch (X, XC, 'k', this.NumNeighbors, ... 'NSMethod', this.NSMethod, 'Distance', 'mahalanobis', ... 'cov', this.DistParameter, 'sortindices', true, ... 'includeties', this.IncludeTies, ... 'bucketsize', this.BucketSize); elseif (strcmpi (this.Distance, 'minkowski')) [idx, dist] = knnsearch (X, XC, 'k', this.NumNeighbors, ... 'NSMethod', this.NSMethod, 'Distance', 'minkowski', ... 'P', this.DistParameter, 'sortindices', true, ... 'includeties',this.IncludeTies, ... 'bucketsize', this.BucketSize); else [idx, dist] = knnsearch (X, XC, 'k', this.NumNeighbors, ... 'NSMethod', this.NSMethod, 'Distance', this.Distance, ... 'sortindices', true, 'includeties', this.IncludeTies, ... 'bucketsize', this.BucketSize); endif ## Make prediction. The class index of each row is kept and the names ## are taken once at the end: indexing keeps the type of ClassNames, ## categorical and string included, which concatenating onto [] did not. cnum = zeros (rows (idx), 1); scores = []; cost = []; ## Get IDs of labels for each point in training data gY = labelIndices (this.ClassNames, Y); ## Evaluate the K nearest neighbours for each new point for i = 1:rows (idx) ## Get K nearest neighbours if (this.IncludeTies) NN_idx = idx{i}; NNdist = dist{i}; else NN_idx = idx(i,:); NNdist = dist(i,:); endif k = numel (NN_idx); kNNgY = gY(NN_idx); ## Weight each neighbour by its distance and normalize, which is what ## DistanceWeight selects. Equal weighting reduces to the vote count ## this used to be, and is the default. kNNgY = kNNgY(:)'; w = this.DWfun (NNdist(:)'); if (any (isinf (w))) ## A neighbour sitting on the query takes the whole vote rather than ## turning every share into a ratio of infinities. w = double (isinf (w)); endif ## Each neighbour also votes with its observation weight, W, which ## carries the prior: measured on R2024a, this reproduces its scores ## with and without weights, under any prior, to 4e-16. w = w .* double (this.W(NN_idx(:)))'; for c = 1:rows (this.ClassNames) freq(c) = sum (w(kNNgY == c)); endfor freq = freq ./ sum (freq); ## Get labels according to BreakTies if (strcmpi (this.BreakTies, 'smallest')) [~, idl] = max (freq); else idl = find (freq == max (freq)); tgn = numel (idl); if (tgn > 1) if (strcmpi (this.BreakTies, 'nearest')) for t = 1:tgn tgs(t) = find (gY(NN_idx) == idl(t)); endfor [~, idm] = min (tgs); idl = idl(idm); else # "random" idl = idl(randperm (numel (idl))(1)); endif endif endif cnum(i) = idl; ## The expected cost of assigning to each class is the posterior ## weighted by the cost matrix, sum_j P(j) * Cost(j,k). It was ## 1 - freq, which is that product only for the default matrix and ## ignored any other. scores = [scores; freq]; cost = [cost; freq * this.Cost]; endfor ## A row missing a predictor has no neighbours to speak of: the search ## finds some regardless, so its score and cost are set aside and it ## takes the class of largest prior, as MATLAB R2024a labels it. miss = any (isnan (XC), 2); if (any (miss)) [~, top] = max (this.Prior); cnum(miss) = top; scores(miss,:) = NaN; cost(miss,:) = NaN; endif ## Apply ScoreTransform once to the whole matrix. Inside the loop it ## was applied to everything accumulated so far, so observation i of n ## came back transformed n-i+1 times. scores = this.STfun (scores); labels = this.ClassNames(cnum,:); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationKNN} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationKNN} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationKNN} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {ClassificationKNN} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Compute loss for a trained ClassificationKNN object. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} computes the loss, ## @var{L}, using the default loss function @qcode{'mincost'}. ## ## @itemize ## @item ## @code{obj} is a @var{ClassificationKNN} object trained on @code{X} and ## @code{Y}. ## @item ## @code{X} must be a @math{N*P} numeric matrix of input data where rows ## correspond to observations and columns correspond to features or ## variables. ## @item ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels ## of corresponding predictor data in @var{X}. @var{Y} must have same ## numbers of Rows as @var{X}. ## @end itemize ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{L} = loss (@dots{}, @var{name}, @var{value})} allows ## additional options specified by @var{name}-@var{value} pairs: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'LossFun'} @tab Specifies the loss function to use. ## Can be a function handle with four input arguments (C, S, W, Cost) ## which returns a scalar value or one of: ## 'binodeviance', 'classifcost', 'classiferror', 'exponential', ## 'hinge', 'logit','mincost', 'quadratic'. ## @itemize ## @item ## @code{C} is a logical matrix of size @math{N*K}, where @math{N} is the ## number of observations and @math{K} is the number of classes. ## The element @code{C(i,j)} is true if the class label of the i-th ## observation is equal to the j-th class. ## @item ## @code{S} is a numeric matrix of size @math{N*K}, where each element ## represents the classification score for the corresponding class. ## @item ## @code{W} is a numeric vector of length @math{N}, representing ## the observation weights. ## @item ## @code{Cost} is a @math{K*K} matrix representing the misclassification ## costs. ## @end itemize ## ## @item @qcode{'Weights'} @tab Specifies observation weights, must be ## a numeric vector of length equal to the number of rows in X. ## Default is @code{ones (size (X, 1))}. loss normalizes the weights so that ## observation weights in each class sum to the prior probability of that ## class. When you supply Weights, loss computes the weighted ## classification loss. ## ## @end multitable ## ## @seealso{fitcknn, ClassificationKNN} ## @end deftypefn function L = loss (this, X, Y, varargin) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationKNN.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationKNN.loss: name-value", ... " arguments must be in pairs.")); endif ## Check for valid X if (isempty (X)) error ("ClassificationKNN.loss: X is empty."); elseif (columns (this.X) != columns (X)) error (strcat ("ClassificationKNN.loss: X must have the same", ... " number of predictors as the trained model.")); endif ## Validate Y valid_types = {'char', 'string', 'logical', 'single', 'double', ... 'cell', 'categorical'}; if (! (any (strcmp (class (Y), valid_types)))) error ("ClassificationKNN.loss: Y must be of a valid type."); endif ## Validate size of Y if (size (Y, 1) != size (X, 1)) error (strcat ("ClassificationKNN.loss: Y must have", ... " the same number of rows as X.")); endif ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights'}; dfValues = {'mincost', []}; [LossFun, Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (isa (LossFun, 'function_handle')) ## Check if the loss function is valid if (nargin (LossFun) != 4) error (strcat ("ClassificationKNN.loss: custom loss function", ... " must accept exactly four input arguments.")); endif try n = 1; K = 2; C_test = false (n, K); S_test = zeros (n, K); W_test = ones (n, 1); Cost_test = ones (K) - eye (K); test_output = LossFun(C_test, S_test, W_test, Cost_test); if (! isscalar (test_output)) error (strcat ("ClassificationKNN.loss: custom loss function", ... " must return a scalar value.")); endif catch error (strcat ("ClassificationKNN.loss: custom loss function is", ... " not valid or does not produce correct output.")); end_try_catch elseif (! (ischar (LossFun) && any (strcmpi (LossFun, {'binodeviance', 'classifcost', ... 'classiferror', 'exponential', ... 'hinge', 'logit', 'mincost', ... 'quadratic'})))) error ("ClassificationKNN.loss: invalid loss function."); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("ClassificationKNN.loss: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isvector (Weights))) error ("ClassificationKNN.loss: invalid Weights."); endif if (! isempty (Weights) && numel (Weights) != rows (X)) error (strcat ("ClassificationKNN.loss: size of Weights must be", ... " equal to the number of rows in X.")); endif if (! isempty (args)) error ("ClassificationKNN.loss: invalid optional paired argument."); endif ## Check for missing values in X if (! isa (LossFun, 'function_handle')) lossfun = tolower (LossFun); if (! strcmp (lossfun, 'mincost') && ! strcmp (lossfun, ... 'classiferror') && ! strcmp (lossfun, 'classifcost') ... && any (isnan (X(:)))) L = NaN; return; endif endif ## If Y is a char array convert it to a cell array of character vectors classes = this.ClassNames; if (ischar (Y) && ischar (classes)) Y = cellstr (Y); classes = cellstr (classes); endif ## Check if Y contains correct classes if (! labelsKnown (Y, this.ClassNames)) error (strcat ("ClassificationKNN.loss: Y must contain only", ... " the classes in model's ClassNames.")); endif ## Set default weights if not specified if (isempty (Weights)) Weights = ones (size (X, 1), 1); endif Weights = double (Weights(:)); ## Normalize Weights K = classCount (classes); class_prior_probs = this.Prior; norm_weights = zeros (size (Weights)); ## Labels are matched to the classes by name, whatever their type. gYw = labelIndices (classes, Y); for i = 1:K class_idx = (gYw == i); if (sum (Weights(class_idx)) > 0) norm_weights(class_idx) = ... Weights(class_idx) * class_prior_probs(i) / sum (Weights(class_idx)); endif endfor Weights = norm_weights / sum (norm_weights); ## Number of observations n = size (X, 1); ## Predict classification scores [label, scores] = predict (this, X); if (ischar (label)) label = cellstr (label); endif gLab = labelIndices (classes, label); ## C is vector of K-1 zeros, with 1 in the ## position corresponding to the true class C = false (n, K); ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:n class_idx = gYidx(i); C(i, class_idx) = true; endfor Y_new = C'; ## Compute the loss using custom loss function if (isa (LossFun, 'function_handle')) L = LossFun(C, scores, Weights, this.Cost); return; endif ## Compute the scalar classification score for each observation m_j = zeros (n, 1); for i = 1:n m_j(i) = scores(i,:) * Y_new(:,i); endfor ## Compute the loss switch (tolower (LossFun)) case 'binodeviance' b = log (1 + exp (-2 * m_j)); L = (Weights') * b; case 'hinge' h = max (0, 1 - m_j); L = (Weights') * h; case 'exponential' e = exp (-m_j); L = (Weights') * e; case 'logit' l = log (1 + exp (-m_j)); L = (Weights') * l; case 'quadratic' q = (1 - m_j) .^ 2; L = (Weights') * q; case 'classiferror' L = 0; for i = 1:n L = L + Weights(i) * (gYw(i) != gLab(i)); endfor case 'mincost' Cost = this.Cost; L = 0; ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:n f_Xj = scores(i, :); gamma_jk = f_Xj * Cost; [~, min_cost_class] = min (gamma_jk); cj = Cost(gYidx(i), min_cost_class); L = L + Weights(i) * cj; endfor case 'classifcost' Cost = this.Cost; L = 0; ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:n y_idx = gYidx(i); y_hat_idx = find (ismember (classes, label(i))); L = L + Weights(i) * Cost(y_idx, y_hat_idx); endfor otherwise error ("ClassificationKNN.loss: invalid loss function."); endswitch endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationKNN} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationKNN} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationKNN} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## @code{@var{m} = margin (@var{obj}, @var{X}, @var{Y})} returns ## the classification margins for @var{obj} with data @var{X} and ## classification @var{Y}. @var{m} is a numeric vector of length size (X,1). ## ## @itemize ## @item ## @code{obj} is a @var{ClassificationKNN} object trained on @code{X} ## and @code{Y}. ## @item ## @code{X} must be a @math{N*P} numeric matrix of input data where rows ## correspond to observations and columns correspond to features or ## variables. ## @item ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels ## of corresponding predictor data in @var{X}. @var{Y} must have same ## numbers of Rows as @var{X}. ## @end itemize ## ## The classification margin for each observation is the difference between ## the classification score for the true class and the maximal ## classification score for the false classes. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{fitcknn, ClassificationKNN} ## @end deftypefn function m = margin (this, X, Y) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationKNN.margin: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y] = tableResponse (this, 'margin', X, Y, {}, nargin > 2); ## Check for valid X if (isempty (X)) error ("ClassificationKNN.margin: X is empty."); elseif (columns (this.X) != columns (X)) error (strcat ("ClassificationKNN.margin: X must have the same", ... " number of predictors as the trained model.")); endif ## Validate Y valid_types = {'char', 'string', 'logical', 'single', 'double', ... 'cell', 'categorical'}; if (! (any (strcmp (class (Y), valid_types)))) error ("ClassificationKNN.margin: Y must be of a valid type."); endif ## Validate X valid_types = {'single', 'double'}; if (! (any (strcmp (class (X), valid_types)))) error ("ClassificationKNN.margin: X must be of a valid type."); endif ## Validate size of Y if (size (Y, 1) != size (X, 1)) error (strcat ("ClassificationKNN.margin: Y must have", ... " the same number of rows as X.")); endif ## If Y is a char array convert it to a cell array of character vectors classes = this.ClassNames; if (ischar (Y) && ischar (classes)) Y = cellstr (Y); classes = cellstr (classes); endif ## Check if Y contains correct classes if (! labelsKnown (Y, classes)) error (strcat ("ClassificationKNN.margin: Y must contain", ... " only the classes in model's ClassNames.")); endif ## Number of Observations n = size (X, 1); ## Initialize the margin vector m = zeros (n, 1); ## Calculate the classification scores [~, scores] = predict (this, X); ## Loop over each observation to compute the margin ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:n ## True class index true_class_idx = gYidx(i); ## Score for the true class true_class_score = scores(i, true_class_idx); ## Get the maximal score for the false classes scores(i, true_class_idx) = -Inf; ## Temporarily max_false_class_score = max (scores(i, :)); if (max_false_class_score == -Inf) m = NaN; return; endif scores(i, true_class_idx) = true_class_score; ## Restore ## Calculate the margin m(i) = true_class_score - max_false_class_score; endfor endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationKNN} {@var{CVMdl} =} crossval (@var{obj}) ## @deftypefnx {ClassificationKNN} {@var{CVMdl} =} crossval (@dots{}, @var{Name}, @var{Value}) ## ## Cross Validate a ClassificationKNN object. ## ## @code{@var{CVMdl} = crossval (@var{obj})} returns a cross-validated model ## object, @var{CVMdl}, from a trained model, @var{obj}, using 10-fold ## cross-validation by default. ## ## @code{@var{CVMdl} = crossval (@var{obj}, @var{name}, @var{value})} ## specifies additional name-value pair arguments to customize the ## cross-validation process. ## ## @multitable @columnfractions 0.28 0.7 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'KFold'} @tab Specify the number of folds to use in ## k-fold cross-validation. @code{"KFold", @var{k}}, where @var{k} is an ## integer greater than 1. ## ## @item @qcode{'Holdout'} @tab Specify the fraction of the data to ## hold out for testing. @code{"Holdout", @var{p}}, where @var{p} is a ## scalar in the range @math{(0,1)}. ## ## @item @qcode{'Leaveout'} @tab Specify whether to perform ## leave-one-out cross-validation. @code{"Leaveout", @var{Value}}, where ## @var{Value} is 'on' or 'off'. ## ## @item @qcode{'CVPartition'} @tab Specify a @qcode{cvpartition} ## object used for cross-validation. @code{"CVPartition", @var{cv}}, where ## @code{isa (@var{cv}, "cvpartition")} = 1. ## ## @end multitable ## ## @seealso{fitcknn, ClassificationKNN, cvpartition, ## ClassificationPartitionedModel} ## @end deftypefn function CVMdl = crossval (this, varargin) ## Check input if (nargin < 1) error ("ClassificationKNN.crossval: too few input arguments."); endif if (numel (varargin) == 1) error (strcat ("ClassificationKNN.crossval: Name-Value", ... " arguments must be in pairs.")); elseif (numel (varargin) > 2) error (strcat ("ClassificationKNN.crossval: specify only one", ... " of the optional Name-Value paired arguments.")); endif ## Add default values if (this.NumObservations < 10) numFolds = this.NumObservations; else numFolds = 10; endif Holdout = []; Leaveout = 'off'; CVPartition = []; ## Parse extra parameters while (numel (varargin) > 0) switch (tolower (varargin {1})) case 'kfold' numFolds = varargin{2}; if (! (isnumeric (numFolds) && isscalar (numFolds) && (numFolds == fix (numFolds)) && numFolds > 1)) error (strcat ("ClassificationKNN.crossval: 'KFold' must", ... " be an integer value greater than 1.")); endif case 'holdout' Holdout = varargin{2}; if (! (isnumeric (Holdout) && isscalar (Holdout) && Holdout > 0 && Holdout < 1)) error (strcat ("ClassificationKNN.crossval: 'Holdout' must", ... " be a numeric value between 0 and 1.")); endif case 'leaveout' Leaveout = varargin{2}; if (! (ischar (Leaveout) && (strcmpi (Leaveout, 'on') || strcmpi (Leaveout, 'off')))) error (strcat ("ClassificationKNN.crossval: 'Leaveout'", ... " must be either 'on' or 'off'.")); endif case 'cvpartition' CVPartition = varargin{2}; if (! (isa (CVPartition, 'cvpartition'))) error (strcat ("ClassificationKNN.crossval: 'CVPartition'",... " must be a 'cvpartition' object.")); endif otherwise error (strcat ("ClassificationKNN.crossval: invalid",... " parameter name in optional paired arguments.")); endswitch varargin(1:2) = []; endwhile ## Determine the cross-validation method to use. The partition covers ## the observations actually trained on: a row dropped for a missing ## value is not one the folds can use, and including it would leave the ## partition, the stored data and NumObservations disagreeing. The ## response is passed rather than a count so the folds stay stratified. Yused = this.Y; if (! isempty (CVPartition)) partition = CVPartition; elseif (! isempty (Holdout)) partition = cvpartition (Yused, 'Holdout', Holdout); elseif (strcmpi (Leaveout, 'on')) partition = cvpartition (this.NumObservations, 'LeaveOut'); else partition = cvpartition (Yused, 'KFold', numFolds); endif ## Create a cross-validated model object CVMdl = ClassificationPartitionedModel (this, partition); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationKNN} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationKNN} {@var{e} =} edge (@dots{}, @qcode{"Weights"}, @var{w}) ## @deftypefnx {ClassificationKNN} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationKNN} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Classification edge, the mean of the classification margins. ## ## @code{@var{e} = edge (@var{obj}, @var{X}, @var{Y})} reduces the vector ## that @code{margin} returns to a single number, the mean margin over the ## rows of @var{X}. It says how far the model puts the true class ahead of ## its nearest rival on average, so a larger edge is a better model, and ## unlike a loss it is not bounded above and rewards confidence rather than ## bare correctness. ## ## @code{@var{e} = edge (@dots{}, @qcode{"Weights"}, @var{w})} takes the ## weighted mean instead, with one weight per row of @var{X}. ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @end deftypefn function e = edge (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationKNN.edge: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'edge', X, Y, varargin, ... nargin > 2); ## The weights are parsed before anything is computed, so a bad ## Name-Value pair is reported as such rather than after a margin. W = edgeWeights (varargin, Y, this.ClassNames, this.Prior, ... "ClassificationKNN", "edge"); m = margin (this, X, Y); e = sum (W .* m(:)) / sum (W); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationKNN} {@var{label} =} resubPredict (@var{obj}) ## @deftypefnx {ClassificationKNN} {[@var{label}, @var{score}, @var{cost}] =} resubPredict (@var{obj}) ## ## Classify the training data with the model fitted to it. ## ## @code{@var{label} = resubPredict (@var{obj})} is @code{predict} applied ## to the observations the model was fitted on, which it holds in ## @qcode{X}. Handing them over yourself is not the same thing: a row ## dropped for a missing response is not in @qcode{X}, so the original ## matrix and the model's own are different data. ## ## The result measures fit and not generalization, and is optimistic by ## construction. @code{crossval} is what estimates performance on data the ## model has not seen. ## ## @end deftypefn function [label, score, cost] = resubPredict (this) [label, score, cost] = predict (this, this.X); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationKNN} {@var{m} =} resubMargin (@var{obj}) ## ## Classification margins of the model on its own training data. ## ## @code{@var{m} = resubMargin (@var{obj})} is @code{margin} applied to the ## observations the model was fitted on, one number per observation. Being ## a resubstitution quantity it is optimistic by construction. ## ## @end deftypefn function m = resubMargin (this) m = margin (this, this.X, this.Y); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationKNN} {@var{e} =} resubEdge (@var{obj}) ## ## Classification edge of the model on its own training data. ## ## @code{@var{e} = resubEdge (@var{obj})} is @code{edge} applied to the ## observations the model was fitted on, the mean of @code{resubMargin}. ## ## @end deftypefn function e = resubEdge (this) ## The training rows keep their weights, as R2024a keeps them. e = edge (this, this.X, this.Y, 'Weights', this.RawWeights); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationKNN} {@var{L} =} resubLoss (@var{obj}) ## @deftypefnx {ClassificationKNN} {@var{L} =} resubLoss (@dots{}, @var{name}, @var{value}) ## ## Classification loss of the model on its own training data. ## ## @code{@var{L} = resubLoss (@var{obj})} is @code{loss} applied to the ## observations the model was fitted on, defaulting to ## @qcode{'mincost'}, and it accepts the same @qcode{Name-Value} pairs. ## ## Being a resubstitution quantity it is a lower bound on the error rather ## than an estimate of it. It is worth least on a lazy learner: a ## one-neighbour @code{ClassificationKNN} has a resubstitution loss of ## exactly zero, every training point being its own nearest neighbour. ## ## @end deftypefn function L = resubLoss (this, varargin) ## The training rows keep their weights unless others are given. L = loss (this, this.X, this.Y, 'Weights', this.RawWeights, ... varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationKNN} {} savemodel (@var{obj}, @var{filename}) ## ## Save a ClassificationKNN object. ## ## @code{savemodel (@var{obj}, @var{filename})} saves each property of a ## ClassificationKNN object into an Octave binary file, the name of which is ## specified in @var{filename}, along with an extra variable, which defines ## the type classification object these variables constitute. Use ## @code{loadmodel} in order to load a classification object into Octave's ## workspace. ## ## @seealso{loadmodel, fitcknn, ClassificationKNN} ## @end deftypefn function savemodel (this, fname) if (nargin < 2) error ("ClassificationKNN.savemodel: too few input arguments."); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error ("ClassificationKNN.savemodel: FNAME must be a character vector."); endif ## Generate variable for class name classdef_name = 'ClassificationKNN'; ## Create variables from model properties X = this.X; Y = encodeLabels (this.Y); NumObservations = this.NumObservations; W = this.W; RawWeights = this.RawWeights; RowsUsed = this.RowsUsed; Sigma = this.Sigma; BinEdges = this.BinEdges; Mu = this.Mu; NumPredictors = this.NumPredictors; PredictorNames = this.PredictorNames; ResponseName = this.ResponseName; ClassNames = encodeLabels (this.ClassNames); Prior = this.Prior; Cost = this.Cost; ScoreTransform = this.ScoreTransform; BreakTies = this.BreakTies; NumNeighbors = this.NumNeighbors; Distance = this.Distance; DistanceWeight = this.DistanceWeight; DWfun = this.DWfun; DistParameter = this.DistParameter; NSMethod = this.NSMethod; IncludeTies = this.IncludeTies; BucketSize = this.BucketSize; CacheSize = this.CacheSize; ModelParameters = this.ModelParameters; STfun = this.STfun; CategoricalPredictors = this.CategoricalPredictors; ExpandedPredictorNames = this.ExpandedPredictorNames; ## Save classdef name and all model properties as individual variables HyperparameterOptimizationResults = this.HyperparameterOptimizationResults; save ('-binary', fname, 'classdef_name', 'X', 'Y', 'NumObservations', ... 'W', 'RawWeights', 'RowsUsed', 'Sigma', 'Mu', 'NumPredictors', ... 'PredictorNames', 'BinEdges', 'ResponseName', 'ClassNames', ... 'Prior', 'Cost', ... 'ScoreTransform', 'BreakTies', 'NumNeighbors', 'Distance', ... 'DistanceWeight', 'DWfun', 'DistParameter', 'NSMethod', ... 'IncludeTies', ... 'BucketSize', 'CacheSize', 'ModelParameters', ... 'CategoricalPredictors', ... 'ExpandedPredictorNames', 'STfun', ... 'HyperparameterOptimizationResults'); endfunction endmethods methods(Static, Hidden) function mdl = load_model (filename, data) data = decodeLabels (data); ## Create a ClassificationKNN object mdl = ClassificationKNN (1, 1); ## Copy the saved data into the object. Iterate over what was ## saved rather than over fieldnames (mdl): a private property such ## as STfun is written out by savemodel but is not reported by ## fieldnames, so comparing the two sets could never match and every ## load failed. Assignment is legal here because this is a method of ## the class itself. names = fieldnames (data); ## set.Distance validates the metric against NSMethod, which the model ## built above still holds at its default of 'kdtree', so a saved model ## searched exhaustively with a metric no kd-tree can take was refused ## by its own loader until NSMethod is in place first. early = ismember (names, {'NSMethod'}); ## The set methods for these read other properties, and one of them ## rebuilds Coeffs, so they are assigned once everything else is in ## place rather than in the order the file happens to list them. late = ismember (names, {'Cost', 'Prior', 'ScoreTransform', ... 'ResponseTransform'}); names = [names(early); names(! early & ! late); names(late)]; for i = 1:numel (names) try mdl.(names{i}) = data.(names{i}); catch error ("ClassificationKNN.load_model: invalid model in '%s'.", filename) end_try_catch endfor endfunction endmethods methods(Access = private) endmethods endclassdef ## Helper functions for ScoreTransform function out = ismax (score) out = score; out(score == max (score)) = 1; out(score != max (score)) = 0; endfunction function out = symmetricismax (score) out = score; out(score == max (score)) = 1; out(score != max (score)) = -1; endfunction %!demo %! ## Create a k-nearest neighbor classifier for Fisher's iris data with k = 5. %! ## Evaluate some model predictions on new data. %! %! load fisheriris %! x = meas; %! y = species; %! xc = [min(x); mean(x); max(x)]; %! obj = fitcknn (x, y, 'NumNeighbors', 5, 'Standardize', 1); %! [label, score, cost] = predict (obj, xc) %!demo %! load fisheriris %! x = meas; %! y = species; %! obj = fitcknn (x, y, 'NumNeighbors', 5, 'Standardize', 1); %! %! ## Create a cross-validated model %! CVMdl = crossval (obj) %!demo %! load fisheriris %! x = meas; %! y = species; %! covMatrix = cov (x); %! %! ## Fit the k-NN model using the 'mahalanobis' distance %! ## and the custom covariance matrix %! obj = fitcknn (x, y, 'NumNeighbors', 5, 'Distance','mahalanobis', ... %! 'Cov', covMatrix); %! %! ## Create a partition model using cvpartition %! Partition = cvpartition (size (x, 1), 'kfold', 12); %! %! ## Create cross-validated model using 'cvPartition' name-value argument %! CVMdl = crossval (obj, 'cvPartition', Partition) %! %! ## Access the trained model from first fold of cross-validation %! CVMdl.Trained{1} %!demo %! X = [1, 2; 3, 4; 5, 6]; %! Y = {'A'; 'B'; 'A'}; %! model = fitcknn (X, Y); %! customLossFun = @(C, S, W, Cost) sum (W .* sum (abs (C - S), 2)); %! ## Calculate loss using custom loss function %! L = loss (model, X, Y, 'LossFun', customLossFun) %!demo %! X = [1, 2; 3, 4; 5, 6]; %! Y = {'A'; 'B'; 'A'}; %! model = fitcknn (X, Y); %! ## Calculate loss using 'mincost' loss function %! L = loss (model, X, Y, 'LossFun', 'mincost') %!demo %! X = [1, 2; 3, 4; 5, 6]; %! Y = ['1'; '2'; '3']; %! model = fitcknn (X, Y); %! X_test = [3, 3; 5, 7]; %! Y_test = ['1'; '2']; %! ## Specify custom Weights %! W = [1; 2]; %! L = loss (model, X_test, Y_test, 'LossFun', 'logit', 'Weights', W); %!demo %! load fisheriris %! mdl = fitcknn (meas, species); %! X = mean (meas); %! Y = {'versicolor'}; %! m = margin (mdl, X, Y) %!demo %! X = [1, 2; 4, 5; 7, 8; 3, 2]; %! Y = [2; 1; 3; 2]; %! ## Train the model %! mdl = fitcknn (X, Y); %! ## Specify Vars and Labels %! Vars = 1; %! Labels = 2; %! ## Calculate partialDependence %! [pd, x, y] = partialDependence (mdl, Vars, Labels); %!demo %! X = [1, 2; 4, 5; 7, 8; 3, 2]; %! Y = [2; 1; 3; 2]; %! ## Train the model %! mdl = fitcknn (X, Y); %! ## Specify Vars and Labels %! Vars = 1; %! Labels = 1; %! queryPoints = linspace (0, 1, 3)'; %! ## Calculate partialDependence using queryPoints %! [pd, x, y] = partialDependence (mdl, Vars, Labels, 'QueryPoints', ... %! queryPoints) ## Test constructor with custom Distance as function_handle (issue #459) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! f = @(d1,d2) sqrt (sum ((d1 - d2) .^ 2, 2)); %! a = ClassificationKNN (x, y, 'Distance', f); %!test # A row missing a predictor takes the class of largest prior %! X = [(1:10)', mod((1:10)', 3)]; %! y = [1; 1; 1; 1; 1; 1; 2; 2; 2; 2]; %! Mdl = ClassificationKNN (X, y); %! [label, score, cost] = predict (Mdl, [NaN, 1]); %! assert_equal (label, 1); %! assert_equal (score, [NaN, NaN]); %! assert_equal (cost, [NaN, NaN]); %! Mdl = ClassificationKNN (X, y, 'Prior', [0.3, 0.7]); %! assert_equal (predict (Mdl, [NaN, 1]), 2); ## MATLAB parity: a neighbour votes with its weight, W, which carries the ## prior. Values from R2024a. %!test %! load fisheriris %! X = meas(1:130,[1, 3]); %! Y = species(1:130); %! k = (1:130)'; %! w = (1 + mod (k, 4)) .* (1 + (k > 50) + 2 * (k > 100)); %! Q = [5, 1.5; 6, 4.5; 6.5, 5.2; 5.8, 4.9; 6.3, 4.9; 5.7, 4.5]; %! Mdl = fitcknn (X, Y, 'Weights', w, 'NumNeighbors', 5); %! assert_equal (Mdl.Prior, [0.185185185185185, 0.37037037037037, ... %! 0.444444444444444], 1e-14); %! assert_equal (Mdl.W([1, 51, 101])', [2, 8, 8] / 675, 1e-15); %! [~, s] = predict (Mdl, Q); %! assert_equal (s, [1, 0, 0; %! 0, 1, 0; %! 0, 0.157894736842105, 0.842105263157895; %! 0, 0.133333333333333, 0.866666666666667; %! 0, 0.25, 0.75; %! 0, 1, 0], 1e-14); %!test %! load fisheriris %! X = meas(1:130,[1, 3]); %! Y = species(1:130); %! k = (1:130)'; %! w = (1 + mod (k, 4)) .* (1 + (k > 50) + 2 * (k > 100)); %! Q = [5, 1.5; 6, 4.5; 6.5, 5.2; 5.8, 4.9; 6.3, 4.9; 5.7, 4.5]; %! Mdl = fitcknn (X, Y, 'Weights', w, 'NumNeighbors', 5, 'Prior', 'uniform'); %! [~, s] = predict (Mdl, Q); %! assert_equal (s, [1, 0, 0; %! 0, 1, 0; %! 0, 0.183673469387755, 0.816326530612245; %! 0, 0.155844155844156, 0.844155844155844; %! 0, 0.285714285714286, 0.714285714285714; %! 0, 1, 0], 1e-14); %!test %! load fisheriris %! X = meas(1:130,[1, 3]); %! Y = species(1:130); %! k = (1:130)'; %! w = (1 + mod (k, 4)) .* (1 + (k > 50) + 2 * (k > 100)); %! Q = [5, 1.5; 6, 4.5; 6.5, 5.2; 5.8, 4.9; 6.3, 4.9; 5.7, 4.5]; %! Mdl = fitcknn (X, Y, 'NumNeighbors', 5, 'Prior', 'uniform'); %! [~, s] = predict (Mdl, Q); %! assert_equal (s, [1, 0, 0; %! 0, 1, 0; %! 0, 0.130434782608696, 0.869565217391304; %! 0, 0.130434782608696, 0.869565217391304; %! 0, 0.285714285714286, 0.714285714285714; %! 0, 1, 0], 1e-14); %!test %! load fisheriris %! X = meas(1:130,[1, 3]); %! Y = species(1:130); %! k = (1:130)'; %! w = (1 + mod (k, 4)) .* (1 + (k > 50) + 2 * (k > 100)); %! Q = [5, 1.5; 6, 4.5; 6.5, 5.2; 5.8, 4.9; 6.3, 4.9; 5.7, 4.5]; %! Mdl = fitcknn (X, Y, 'Weights', w, 'NumNeighbors', 5, ... %! 'DistanceWeight', 'inverse'); %! [~, s] = predict (Mdl, Q); %! assert_equal (s, [1, 0, 0; %! 0, 1, 0; %! 0, 0.080216792482354, 0.919783207517646; %! 0, 0.165685424949237, 0.834314575050763; %! 0, 0.5, 0.5; %! 0, 1, 0], 1e-14); %!test %! load fisheriris %! X = meas(1:130,[1, 3]); %! Y = species(1:130); %! k = (1:130)'; %! w = (1 + mod (k, 4)) .* (1 + (k > 50) + 2 * (k > 100)); %! Mdl = fitcknn (X, Y, 'Weights', w, 'NumNeighbors', 5, 'Standardize', true); %! assert_equal (Mdl.Mu, [6.06414814814815, 4.35066666666667], 1e-13); %! assert_equal (Mdl.Sigma, [0.872766097166902, 1.62323758619346], 1e-13); %!test %! load fisheriris %! X = meas(1:130,[1, 3]); %! Y = species(1:130); %! k = (1:130)'; %! w = (1 + mod (k, 4)) .* (1 + (k > 50) + 2 * (k > 100)); %! Q = [5, 1.5; 6, 4.5; 6.5, 5.2; 5.8, 4.9; 6.3, 4.9; 5.7, 4.5]; %! Mdl = fitcknn (X, Y, 'Weights', w, 'NumNeighbors', 5); %! Mdl.Prior = [0.2, 0.3, 0.5]; %! assert_equal (Mdl.W([1, 51, 101])', [0.0032, 0.0096, ... %! 0.0133333333333333], 1e-15); %! [~, s] = predict (Mdl, Q); %! assert_equal (s, [1, 0, 0; %! 0, 1, 0; %! 0, 0.118942731277533, 0.881057268722467; %! 0, 0.0997229916897507, 0.900277008310249; %! 0, 0.193548387096774, 0.806451612903226; %! 0, 1, 0], 1e-14); %!test %! load fisheriris %! X = meas(1:130,[1, 3]); %! Y = species(1:130); %! k = (1:130)'; %! w = (1 + mod (k, 4)) .* (1 + (k > 50) + 2 * (k > 100)); %! Mdl = fitcknn (X, Y, 'Weights', w, 'NumNeighbors', 5); %! assert_equal (resubLoss (Mdl), 0.0681481481481481, 1e-14); %! assert_equal (loss (Mdl, X, Y), 0.0592592592592593, 1e-14); %! w([2, 60]) = 0; %! Mdl = fitcknn (X, Y, 'Weights', w, 'NumNeighbors', 5); %! assert_equal (Mdl.NumObservations, 128); ## MATLAB parity: of two neighbours at equal distance the earlier row counts. %!test %! load fisheriris %! X = meas(1:130,[1, 3]); %! Y = species(1:130); %! Mdl = fitcknn (X, Y, 'NumNeighbors', 4); %! [~, s] = predict (Mdl, [6.3, 4.9]); %! assert_equal (s, [0, 0.5, 0.5], 1e-15); %! k = (1:130)'; %! w = (1 + mod (k, 4)) .* (1 + (k > 50) + 2 * (k > 100)); %! Mdl = fitcknn (X, Y, 'Weights', w, 'NumNeighbors', 5); %! assert_equal (resubEdge (Mdl), 0.86407659007327, 1e-13); ## MATLAB parity: loss, margin and edge match labels to the classes by name, ## whatever their type. Values from R2024a. %!test %! load fisheriris %! X = meas(51:150, 1:2); %! ys = species(51:150); %! yc = categorical (ys); %! Mdl = fitcknn (X, yc); %! assert_equal (loss (Mdl, X, ys), 0.12, 1e-15); %! assert_equal (loss (Mdl, X, string (ys)), 0.12, 1e-15); %! assert_equal (loss (Mdl, X, yc), 0.12, 1e-15); %! assert_equal (numel (margin (Mdl, X, ys)), 100); %! assert_equal (edge (Mdl, X, ys), edge (Mdl, X, yc), 1e-15); %! assert_equal (loss (fitcknn (X, ys), X, yc), 0.12, 1e-15); %!error ... %! fitcknn ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', 'a') %!error ... %! fitcknn ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', ones (2, 2)) %!error ... %! fitcknn ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', [1, 2]) %!error ... %! fitcknn ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', -ones (4, 1)) %!error ... %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! f = @(d1,d2) sqrt (dot (d1, d2)); %! a = ClassificationKNN (x, y, 'Distance', f); %!error ... %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! f = @(d1,d2) sqrt (sum ((d1 - d2) .^ 2, 1)); %! a = ClassificationKNN (x, y, 'Distance', f); %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! f = @(d1,d2) sqrt (sum ((d1 - d2) .^ 2, 2)); %! a = ClassificationKNN (x, y, 'NSMethod', 'exhaustive', 'Distance', ... %! 'cityblock'); %! a.Distance = f; %! assert_equal (a.Distance, f); %! assert_equal (isempty (a.DistParameter), true); %!error ... %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = ClassificationKNN (x, y, 'NSMethod', 'exhaustive'); %! a.Distance = @(d1,d2) sqrt (dot (d1, d2)); %!error ... %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = ClassificationKNN (x, y, 'NSMethod', 'exhaustive'); %! a.Distance = @(d1,d2) sqrt (sum ((d1 - d2) .^ 2, 1)); %!error ... %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = ClassificationKNN (x, y); %! a.Distance = @(d1,d2) sqrt (sum ((d1 - d2) .^ 2, 2)); ## Test constructor with NSMethod and NumNeighbors parameters %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = ClassificationKNN (x, y); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 1}) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = ClassificationKNN (x, y, 'NSMethod', 'exhaustive'); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 1}) %! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! k = 10; %! a = ClassificationKNN (x, y, 'NumNeighbors' ,k); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 4}) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = ones (4, 11); %! y = ['a'; 'a'; 'b'; 'b']; %! k = 10; %! a = ClassificationKNN (x, y, 'NumNeighbors' ,k); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 4}) %! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! k = 10; %! a = ClassificationKNN (x, y, 'NumNeighbors' ,k, 'NSMethod', 'exhaustive'); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 4}) %! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! k = 10; %! a = ClassificationKNN (x, y, 'NumNeighbors' ,k, 'Distance', 'hamming'); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 4}) %! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'hamming'}) %! assert_equal ({a.BucketSize}, {50}) ## Test constructor with Standardize and DistParameter parameters %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! weights = ones (4,1); %! a = ClassificationKNN (x, y, 'Standardize', 1); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 1}) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %! assert_equal ({a.Sigma}, {std(x, [], 1)}) %! assert_equal ({a.Mu}, {[3.75, 4.25, 4.75]}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! weights = ones (4,1); %! a = ClassificationKNN (x, y, 'Standardize', false); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 1}) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %! assert_equal ({a.Sigma}, {[]}) %! assert_equal ({a.Mu}, {[]}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! s = ones (1, 3); %! a = ClassificationKNN (x, y, 'Scale' , s, 'Distance', 'seuclidean'); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.DistParameter}, {s}) %! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'seuclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = ClassificationKNN (x, y, 'Exponent' , 5, 'Distance', 'minkowski'); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal (a.DistParameter, 5) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'minkowski'}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = ClassificationKNN (x, y, 'Exponent' , 5, 'Distance', 'minkowski', ... %! 'NSMethod', 'exhaustive'); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal (a.DistParameter, 5) %! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'minkowski'}) ## Test constructor with BucketSize and IncludeTies parameters %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = ClassificationKNN (x, y, 'BucketSize' , 20, 'distance', 'mahalanobis'); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'mahalanobis'}) %! assert_equal ({a.BucketSize}, {20}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = ClassificationKNN (x, y, 'IncludeTies', true); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal (a.IncludeTies, true); %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = ClassificationKNN (x, y); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal (a.IncludeTies, false); %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) ## Test constructor with Prior and Cost parameters %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = ClassificationKNN (x, y); %! assert_equal (class (a), "ClassificationKNN") %! assert_equal (a.Prior, [0.5, 0.5]) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! prior = [0.5, 0.5]; %! a = ClassificationKNN (x, y, 'Prior', 'empirical'); %! assert_equal (class (a), "ClassificationKNN") %! assert_equal (a.Prior, prior) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'a'; 'b']; %! prior = [0.75, 0.25]; %! a = ClassificationKNN (x, y, 'Prior', 'empirical'); %! assert_equal (class (a), "ClassificationKNN") %! assert_equal (a.Prior, prior) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'a'; 'b']; %! prior = [0.5, 0.5]; %! a = ClassificationKNN (x, y, 'Prior', 'uniform'); %! assert_equal (class (a), "ClassificationKNN") %! assert_equal (a.Prior, prior) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! cost = [0, 1; 1, 0]; %! a = ClassificationKNN (x, y, 'Cost', cost); %! assert_equal (class (a), "ClassificationKNN") %! assert_equal (a.Cost, [0, 1; 1, 0]) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! cost = [0, 1; 1, 0]; %! a = ClassificationKNN (x, y, 'Cost', cost, 'Distance', 'hamming' ); %! assert_equal (class (a), "ClassificationKNN") %! assert_equal (a.Cost, [0, 1; 1, 0]) %! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'hamming'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2; 3, 4; 5,6; 5, 8]; %! y = {'9'; '9'; '6'; '7'}; %! a = ClassificationKNN (x, y); %! assert_equal (a.Prior, [0.25, 0.25, 0.5]) ## Test constructor with ClassNames parameter %!test %! load fisheriris %! x = meas; %! y = species; %! ClassNames = {'setosa', 'versicolor', 'virginica'}; %! a = ClassificationKNN (x, y, 'ClassNames', ClassNames); %! assert_equal (a.ClassNames, ClassNames') %!test # A categorical 'ClassNames' keeps only the classes it names %! load fisheriris %! Mdl = ClassificationKNN (meas, categorical (species), 'ClassNames', ... %! categorical ({'versicolor'; 'virginica'})); %! assert_equal (Mdl.NumObservations, 100); %! assert_equal (class (Mdl.ClassNames), 'categorical'); ## Test input validation for constructor ## A response naming its classes in the rows of a character matrix is one ## of the documented types and MATLAB accepts it on every classifier. The ## whole surface below was broken and untested, which is why it stayed so. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcknn (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcknn (Xch, Ycell); %! assert_equal (size (Mc.ClassNames), [2, 10]); %! assert_equal (cellstr (Mc.ClassNames), Ms.ClassNames); ## predict returns whole names, not their first letters. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcknn (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcknn (Xch, Ycell); %! pch = predict (Mc, Xch); %! assert_equal (columns (pch), 10); %! assert_equal (cellstr (pch), predict (Ms, Xch)); ## loss, margin and edge read a character response as the same response. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcknn (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcknn (Xch, Ycell); %! assert_equal (loss (Mc, Xch, Ych), loss (Ms, Xch, Ycell), 1e-12); %! assert_equal (margin (Mc, Xch, Ych), margin (Ms, Xch, Ycell), 1e-12); %! assert_equal (edge (Mc, Xch, Ych), edge (Ms, Xch, Ycell), 1e-12); ## A character matrix pads its rows out to the longest name, and the padding ## is part of the name: R2024a reports ClassNames of ['ab '; 'abcd']. %!test %! Xpad = [1 2; 3 4; 1.1 2.1; 3.1 4.1; 1.2 2.2; 3.2 4.2]; %! Ypad = char ({"ab", "abcd", "ab", "abcd", "ab", "abcd"}); %! rand ("state", 1); randn ("state", 1); %! Mp = fitcknn (Xpad, Ypad); %! assert_equal (size (Mp.ClassNames), [2, 4]); %! assert_equal (Mp.ClassNames(1,:), "ab "); ## A row dropped for a missing predictor is the only case that exercises ## indexing the response by row rather than by element. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! Xmiss = Xch; Xmiss(3,2) = NaN; %! rand ("state", 1); randn ("state", 1); Md = fitcknn (Xmiss, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcknn (Xmiss, Ycell); %! assert_equal (size (Md.ClassNames), [2, 10]); %! assert_equal (cellstr (Md.ClassNames), Ms.ClassNames); ## ClassNames may itself be given as a character matrix, which selects the ## classes by whole name: ismember between two character matrices compares ## them character by character and would select by letter. %!test %! load fisheriris %! rand ("state", 1); randn ("state", 1); %! Mf = fitcknn (meas, char (species), ... %! "ClassNames", char ({"versicolor", "virginica"})); %! assert_equal (rows (Mf.ClassNames), 2); %! assert_equal (cellstr (Mf.ClassNames), {"versicolor"; "virginica"}); ## A model fitted from a character response comes back off disk unchanged. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcknn (Xch, Ych); %! fname = tempname (); %! savemodel (Mc, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.ClassNames, Mc.ClassNames); %! assert_equal (predict (M2, Xch), predict (Mc, Xch)); ## crossval carries a character response through cvpartition and back. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcknn (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcknn (Xch, Ycell); %! rand ("state", 2); cvc = crossval (Mc, "KFold", 3); %! rand ("state", 2); cvs = crossval (Ms, "KFold", 3); %! assert_equal (cellstr (kfoldPredict (cvc)), kfoldPredict (cvs)); %!test %! ## CacheSize defaults to 1000 and is reported, where MATLAB hides it. %! X = [1, 2; 3, 4; 5, 6; 7, 8; 2, 2]; %! Mdl = fitcknn (X, [1; 1; 2; 2; 1]); %! assert_equal (Mdl.CacheSize, 1000); %! assert_equal (any (strcmp (properties (Mdl), 'CacheSize')), true); %!test %! ## It is settable after fitting and through fitcknn alike. %! X = [1, 2; 3, 4; 5, 6; 7, 8; 2, 2]; %! Mdl = fitcknn (X, [1; 1; 2; 2; 1]); %! Mdl.CacheSize = 5000; %! assert_equal (Mdl.CacheSize, 5000); %! Mdl2 = fitcknn (X, [1; 1; 2; 2; 1], "CacheSize", 250); %! assert_equal (Mdl2.CacheSize, 250); %!test %! ## It changes nothing: the model keeps no cache to size. %! X = [1, 2; 3, 4; 5, 6; 7, 8; 2, 2]; %! y = [1; 1; 2; 2; 1]; %! Mdl = fitcknn (X, y, "CacheSize", 1); %! Mdl2 = fitcknn (X, y, "CacheSize", 1e6); %! [l1, s1] = predict (Mdl, X); %! [l2, s2] = predict (Mdl2, X); %! assert_equal (l1, l2); %! assert_equal (s1, s2); %!error ... %! fitcknn (ones (5, 2), [1; 1; 1; 2; 2], "CacheSize", 0) %!error ... %! fitcknn (ones (5, 2), [1; 1; 1; 2; 2], "CacheSize", [1, 2]) %!error ... %! fitcknn (ones (5, 2), [1; 1; 1; 2; 2], "CacheSize", Inf) %!test %! Mdl = fitcknn (ones (5, 2), [1; 1; 1; 2; 2]); %! fail ("Mdl.CacheSize = -1", ... %! "ClassificationKNN: 'CacheSize' must be a positive finite scalar."); %!error ClassificationKNN () %!error ... %! ClassificationKNN (ones (4, 1)) %!error ... %! ClassificationKNN (ones (4,2), ones (1,4)) %!error ... %! ClassificationKNN (ones (5,3), ones (5,1), 'standardize', 'a') %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'scale', [1 1], 'standardize', true) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'PredictorNames', ['A']) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'PredictorNames', 'A') %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'PredictorNames', {'A', 'B', 'C'}) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'ResponseName', {'Y'}) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'ResponseName', 1) %!error ... %! ClassificationKNN (ones (10,2), ones (10,1), 'ClassNames', @(x)x) %!error ... %! ClassificationKNN (ones (10,2), ones (10,1), 'ClassNames', {1}) %!error ... %! ClassificationKNN (ones (10,2), ones (10,1), 'ClassNames', [1, 2]) %!error ... %! ClassificationKNN (ones (5,2), ['a';'b';'a';'a';'b'], 'ClassNames', ['a';'c']) %!error ... %! ClassificationKNN (ones (5,2), {'a';'b';'a';'a';'b'}, 'ClassNames', {'a','c'}) %!error ... %! ClassificationKNN (ones (10,2), logical (ones (10,1)), 'ClassNames', [true, false]) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'BreakTies', 1) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'BreakTies', {'1'}) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'BreakTies', 'some') %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Prior', {'1', '2'}) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Cost', [1, 2]) %!error ... %! Mdl = fitcknn ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]); %! Mdl.Cost = 1:4; %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Cost', 'string') %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Cost', {eye(2)}) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'NumNeighbors', 0) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'NumNeighbors', 15.2) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'NumNeighbors', 'asd') %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Distance', 'somemetric') %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Distance', ... %! @(v,m)sqrt (repmat (v,rows (m),1)-m,2)) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Distance', ... %! @(v,m)sqrt (sum (sumsq (repmat (v,rows (m),1)-m,2)))) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Distance', [1 2 3]) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Distance', {'mahalanobis'}) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Distance', logical (5)) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'DistanceWeight', @(x)sum (x)) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'DistanceWeight', 'text') %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'DistanceWeight', [1 2 3]) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Scale', 'scale') %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Scale', {[1 2 3]}) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'standardize', true, 'scale', [1 1]) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Cov', ones (2), 'Distance', 'mahalanobis') %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'scale', [1 1], 'Cov', ones (2)) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Exponent', 0) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Exponent', -3) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Exponent', [2, 3]) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Exponent', 'three') %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Exponent', {3}) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'NSMethod', {'kdtree'}) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'NSMethod', 3) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'NSMethod', 'some') %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'IncludeTies', 'some') %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'BucketSize', 42.5) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'BucketSize', -50) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'BucketSize', 'some') %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'BucketSize', {50}) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'some', 'some') %!error ... %! ClassificationKNN ([1;2;3;'a';4], ones (5,1)) %!error ... %! ClassificationKNN ([1;2;3;Inf;4], ones (5,1)) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Prior', [1 2]) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Cost', [1 2; 1 3]) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Scale', [1 1]) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Scale', [1 1 1], 'Distance', 'seuclidean') %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Scale', [1 -1], 'Distance', 'seuclidean') %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Cov', eye (2)) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Cov', eye (3), 'Distance', 'mahalanobis') %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Exponent', 3) %!error ... %! ClassificationKNN (ones (5,2), ones (5,1), 'Distance', 'hamming', 'NSMethod', 'kdtree') ## Test output for predict method %!shared x, y %! load fisheriris %! x = meas; %! y = species; %!test %! xc = [min(x); mean(x); max(x)]; %! obj = fitcknn (x, y, 'NumNeighbors', 5); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'setosa'; 'versicolor'; 'virginica'}) %! assert_equal (s, [1, 0, 0; 0, 1, 0; 0, 0, 1]) %! assert_equal (c, [0, 1, 1; 1, 0, 1; 1, 1, 0]) %!test %! xc = [min(x); mean(x); max(x)]; %! obj = fitcknn (x, categorical (y), 'NumNeighbors', 5); %! l = predict (obj, xc); %! assert_equal (l, categorical ({'setosa'; 'versicolor'; 'virginica'})) %!test %! xc = [min(x); mean(x); max(x)]; %! obj = fitcknn (x, y, 'NumNeighbors', 5, 'Standardize', 1); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'versicolor'; 'versicolor'; 'virginica'}) %! assert_equal (s, [0.4, 0.6, 0; 0, 1, 0; 0, 0, 1], 1e-15) %! assert_equal (c, [0.6, 0.4, 1; 1, 0, 1; 1, 1, 0], 1e-15) %!test %! xc = [min(x); mean(x); max(x)]; %! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'mahalanobis'); %! [l, s, c] = predict (obj, xc); %! assert_equal (s, [0.3, 0.7, 0; 0, 0.9, 0.1; 0.2, 0.2, 0.6], 1e-4) %! assert_equal (c, [0.7, 0.3, 1; 1, 0.1, 0.9; 0.8, 0.8, 0.4], 1e-4) %!test %! xc = [min(x); mean(x); max(x)]; %! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'cosine'); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'setosa'; 'versicolor'; 'virginica'}) %! assert_equal (s, [1, 0, 0; 0, 1, 0; 0, 0.3, 0.7], 1e-4) %! assert_equal (c, [0, 1, 1; 1, 0, 1; 1, 0.7, 0.3], 1e-4) %!test %! xc = [5.2, 4.1, 1.5, 0.1; 5.1, 3.8, 1.9, 0.4; ... %! 5.1, 3.8, 1.5, 0.3; 4.9, 3.6, 1.4, 0.1]; %! obj = fitcknn (x, y, 'NumNeighbors', 5); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'setosa'; 'setosa'; 'setosa'; 'setosa'}) %! assert_equal (s, [1, 0, 0; 1, 0, 0; 1, 0, 0; 1, 0, 0]) %! assert_equal (c, [0, 1, 1; 0, 1, 1; 0, 1, 1; 0, 1, 1]) %!test %! xc = [5, 3, 5, 1.45]; %! obj = fitcknn (x, y, 'NumNeighbors', 5); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'versicolor'}) %! assert_equal (s, [0, 0.6, 0.4], 1e-4) %! assert_equal (c, [1, 0.4, 0.6], 1e-4) %!test %! xc = [5, 3, 5, 1.45]; %! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'minkowski', 'Exponent', 5); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'versicolor'}) %! assert_equal (s, [0, 0.5, 0.5], 1e-4) %! assert_equal (c, [1, 0.5, 0.5], 1e-4) %!test %! ## An exponent need not be an integer, as in MATLAB R2024a %! xc = [5, 3, 5, 1.45]; %! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'minkowski', ... %! 'Exponent', 2.5); %! [l, s, c] = predict (obj, xc); %! assert_equal (obj.DistParameter, 2.5) %! assert_equal (l, {'virginica'}) %! assert_equal (s, [0, 0.4, 0.6], 1e-15) %! assert_equal (c, [1, 0.6, 0.4], 1e-15) %!test %! xc = [5, 3, 5, 1.45]; %! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'minkowski', ... %! 'Exponent', 0.5); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'versicolor'}) %! assert_equal (s, [0, 0.6, 0.4], 1e-15) %! assert_equal (c, [1, 0.4, 0.6], 1e-15) %!test %! ## An exponent of Inf is the Chebychev distance, as in MATLAB R2024a %! xc = [5, 3, 5, 1.45]; %! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'minkowski', ... %! 'Exponent', Inf); %! [l, s, c] = predict (obj, xc); %! assert_equal (obj.DistParameter, Inf) %! assert_equal (obj.NSMethod, 'kdtree') %! assert_equal (l, {'versicolor'}) %! assert_equal (s, [0, 0.7, 0.3], 1e-15) %! assert_equal (c, [1, 0.3, 0.7], 1e-15) %!test %! xc = [5, 3, 5, 1.45]; %! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'minkowski', ... %! 'Exponent', Inf, 'NSMethod', 'exhaustive'); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'versicolor'}) %! assert_equal (s, [0, 0.7, 0.3], 1e-15) %! assert_equal (c, [1, 0.3, 0.7], 1e-15) %!test %! xc = [5, 3, 5, 1.45]; %! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'minkowski'); %! obj.DistParameter = Inf; %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'versicolor'}) %! assert_equal (s, [0, 0.7, 0.3], 1e-15) %! assert_equal (c, [1, 0.3, 0.7], 1e-15) %!test %! xc = [5, 3, 5, 1.45]; %! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'jaccard'); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'setosa'}) %! assert_equal (s, [0.9, 0.1, 0], 1e-4) %! assert_equal (c, [0.1, 0.9, 1], 1e-4) %!test %! xc = [5, 3, 5, 1.45]; %! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'mahalanobis'); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'versicolor'}) %! assert_equal (s, [0.1000, 0.5000, 0.4000], 1e-4) %! assert_equal (c, [0.9000, 0.5000, 0.6000], 1e-4) %!test %! xc = [5, 3, 5, 1.45]; %! obj = fitcknn (x, y, 'NumNeighbors', 5, 'distance', 'jaccard'); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'setosa'}) %! assert_equal (s, [0.8, 0.2, 0], 1e-4) %! assert_equal (c, [0.2, 0.8, 1], 1e-4) %!test %! xc = [5, 3, 5, 1.45]; %! obj = fitcknn (x, y, 'NumNeighbors', 5, 'distance', 'seuclidean'); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'versicolor'}) %! assert_equal (s, [0, 1, 0], 1e-4) %! assert_equal (c, [1, 0, 1], 1e-4) %!test %! xc = [5, 3, 5, 1.45]; %! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'chebychev'); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'versicolor'}) %! assert_equal (s, [0, 0.7, 0.3], 1e-4) %! assert_equal (c, [1, 0.3, 0.7], 1e-4) %!test %! xc = [5, 3, 5, 1.45]; %! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'cityblock'); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'versicolor'}) %! assert_equal (s, [0, 0.6, 0.4], 1e-4) %! assert_equal (c, [1, 0.4, 0.6], 1e-4) %!test %! xc = [5, 3, 5, 1.45]; %! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'cosine'); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'virginica'}) %! assert_equal (s, [0, 0.1, 0.9], 1e-4) %! assert_equal (c, [1, 0.9, 0.1], 1e-4) %!test %! xc = [5, 3, 5, 1.45]; %! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'correlation'); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'virginica'}) %! assert_equal (s, [0, 0.1, 0.9], 1e-4) %! assert_equal (c, [1, 0.9, 0.1], 1e-4) %!test %! xc = [5, 3, 5, 1.45]; %! obj = fitcknn (x, y, 'NumNeighbors', 30, 'distance', 'spearman'); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'versicolor'}) %! assert_equal (s, [0, 1, 0], 1e-4) %! assert_equal (c, [1, 0, 1], 1e-4) %!test %! xc = [5, 3, 5, 1.45]; %! obj = fitcknn (x, y, 'NumNeighbors', 30, 'distance', 'hamming'); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'setosa'}) %! assert_equal (s, [0.4333, 0.3333, 0.2333], 1e-4) %! assert_equal (c, [0.5667, 0.6667, 0.7667], 1e-4) %!test %! xc = [5, 3, 5, 1.45]; %! obj = fitcknn (x, y, 'NumNeighbors', 5, 'distance', 'hamming'); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'setosa'}) %! assert_equal (s, [0.8, 0.2, 0], 1e-4) %! assert_equal (c, [0.2, 0.8, 1], 1e-4) %!test %! xc = [min(x); mean(x); max(x)]; %! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'correlation'); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'setosa'; 'versicolor'; 'virginica'}) %! assert_equal (s, [1, 0, 0; 0, 1, 0; 0, 0.4, 0.6], 1e-4) %! assert_equal (c, [0, 1, 1; 1, 0, 1; 1, 0.6, 0.4], 1e-4) %!test %! xc = [min(x); mean(x); max(x)]; %! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'hamming'); %! [l, s, c] = predict (obj, xc); %! assert_equal (l, {'setosa';'setosa';'setosa'}) %! assert_equal (s, [0.9, 0.1, 0; 1, 0, 0; 0.5, 0, 0.5], 1e-4) %! assert_equal (c, [0.1, 0.9, 1; 0, 1, 1; 0.5, 1, 0.5], 1e-4) ## Test input validation for predict method %!error ... %! predict (ClassificationKNN (ones (4,2), ones (4,1))) %!error ... %! predict (ClassificationKNN (ones (4,2), ones (4,1)), []) %!error ... %! predict (ClassificationKNN (ones (4,2), ones (4,1)), 1) ## Test output for loss method %!test %! load fisheriris %! model = fitcknn (meas, species, 'NumNeighbors', 5); %! X = mean (meas); %! Y = {'versicolor'}; %! L = loss (model, X, Y); %! assert_equal (L, 0) %!test %! load fisheriris %! model = fitcknn (meas, species, 'NumNeighbors', 5); %! L = loss (model, meas, species, 'LossFun', 'binodeviance'); %! assert_equal (L, 0.1413, 1e-4) %!test %! load fisheriris %! model = fitcknn (meas, species); %! L = loss (model, meas, species, 'LossFun', 'binodeviance'); %! assert_equal (L, 0.1269, 1e-4) %!test %! X = [1, 2; 3, 4; 5, 6]; %! Y = {'A'; 'B'; 'A'}; %! model = fitcknn (X, Y); %! X_test = [1, 6; 3, 3]; %! Y_test = {'A'; 'B'}; %! L = loss (model, X_test, Y_test); %! assert_equal (abs (L - 0.6667) > 1e-5, true) %!test %! X = [1, 2; 3, 4; 5, 6]; %! Y = {'A'; 'B'; 'A'}; %! model = fitcknn (X, Y); %! X_with_nan = [1, 2; NaN, 4]; %! Y_test = {'A'; 'B'}; %! L = loss (model, X_with_nan, Y_test); %! assert_equal (abs (L - 0.3333) < 1e-4, true) %!test %! X = [1, 2; 3, 4; 5, 6]; %! Y = {'A'; 'B'; 'A'}; %! model = fitcknn (X, Y); %! X_with_nan = [1, 2; NaN, 4]; %! Y_test = {'A'; 'B'}; %! L = loss (model, X_with_nan, Y_test, 'LossFun', 'logit'); %! assert_equal (isnan (L), true) %!test %! X = [1, 2; 3, 4; 5, 6]; %! Y = {'A'; 'B'; 'A'}; %! model = fitcknn (X, Y); %! customLossFun = @(C, S, W, Cost) sum (W .* sum (abs (C - S), 2)); %! L = loss (model, X, Y, 'LossFun', customLossFun); %! assert_equal (L, 0) %!test %! X = [1, 2; 3, 4; 5, 6]; %! Y = [1; 2; 1]; %! model = fitcknn (X, Y); %! L = loss (model, X, Y, 'LossFun', 'classiferror'); %! assert_equal (L, 0) %!test %! X = [1, 2; 3, 4; 5, 6]; %! Y = [true; false; true]; %! model = fitcknn (X, Y); %! L = loss (model, X, Y, 'LossFun', 'binodeviance'); %! assert_equal (abs (L - 0.1269) < 1e-4, true) %!test %! X = [1, 2; 3, 4; 5, 6]; %! Y = ['1'; '2'; '1']; %! model = fitcknn (X, Y); %! L = loss (model, X, Y, 'LossFun', 'classiferror'); %! assert_equal (L, 0) %!test %! X = [1, 2; 3, 4; 5, 6]; %! Y = ['1'; '2'; '3']; %! model = fitcknn (X, Y); %! X_test = [3, 3]; %! Y_test = ['1']; %! L = loss (model, X_test, Y_test, 'LossFun', 'quadratic'); %! assert_equal (L, 1) %!test %! X = [1, 2; 3, 4; 5, 6]; %! Y = ['1'; '2'; '3']; %! model = fitcknn (X, Y); %! X_test = [3, 3; 5, 7]; %! Y_test = ['1'; '2']; %! L = loss (model, X_test, Y_test, 'LossFun', 'classifcost'); %! assert_equal (L, 1) %!test %! X = [1, 2; 3, 4; 5, 6]; %! Y = ['1'; '2'; '3']; %! model = fitcknn (X, Y); %! X_test = [3, 3; 5, 7]; %! Y_test = ['1'; '2']; %! L = loss (model, X_test, Y_test, 'LossFun', 'hinge'); %! assert_equal (L, 1) %!test %! X = [1, 2; 3, 4; 5, 6]; %! Y = ['1'; '2'; '3']; %! model = fitcknn (X, Y); %! X_test = [3, 3; 5, 7]; %! Y_test = ['1'; '2']; %! W = [1; 2]; %! L = loss (model, X_test, Y_test, 'LossFun', 'logit', 'Weights', W); %! assert_equal (abs (L - 0.6931) < 1e-4, true) ## Test input validation for loss method %!error ... %! loss (ClassificationKNN (ones (4,2), ones (4,1))) %!error ... %! loss (ClassificationKNN (ones (4,2), ones (4,1)), ones (4,2)) %!error ... %! loss (ClassificationKNN (ones (40,2), randi ([1, 2], 40, 1)), [], zeros (2)) %!error ... %! loss (ClassificationKNN (ones (40,2), randi ([1, 2], 40, 1)), 1, zeros (2)) %!error ... %! loss (ClassificationKNN (ones (4,2), ones (4,1)), ones (4,2), ... %! ones (4,1), 'LossFun') %!error ... %! loss (ClassificationKNN (ones (4,2), ones (4,1)), ones (4,2), ones (3,1)) %!error ... %! loss (ClassificationKNN (ones (4,2), ones (4,1)), ones (4,2), ... %! ones (4,1), 'LossFun', 'a') %!error ... %! loss (ClassificationKNN (ones (4,2), ones (4,1)), ones (4,2), ... %! ones (4,1), 'Bogus', 1) %!error ... %! loss (ClassificationKNN (ones (4,2), ones (4,1)), ones (4,2), ... %! ones (4,1), 'Weights', 'w') %!error ... %! loss (ClassificationKNN (ones (4,2), ones (4,1)), ones (4,2), ... %! ones (4,1), 'Weights', ones (2, 2)) %!error ... %! loss (ClassificationKNN (ones (4,2), ones (4,1)), ones (4,2), ... %! ones (4,1), 'Weights', ones (3,1)) ## Test output for margin method %!test %! load fisheriris %! mdl = fitcknn (meas, species, 'NumNeighbors', 5); %! X = mean (meas); %! Y = {'versicolor'}; %! m = margin (mdl, X, Y); %! assert_equal (m, 1) %!test %! X = [1, 2; 3, 4; 5, 6]; %! Y = [1; 2; 3]; %! mdl = fitcknn (X, Y); %! m = margin (mdl, X, Y); %! assert_equal (m, [1; 1; 1]) %!test %! X = [7, 8; 9, 10]; %! Y = ['1'; '2']; %! mdl = fitcknn (X, Y); %! m = margin (mdl, X, Y); %! assert_equal (m, [1; 1]) %!test %! X = [11, 12]; %! Y = {'1'}; %! mdl = fitcknn (X, Y); %! m = margin (mdl, X, Y); %! assert_equal (isnan (m), true) %!test %! X = [1, 2; 3, 4; 5, 6]; %! Y = [1; 2; 3]; %! mdl = fitcknn (X, Y); %! X1 = [15, 16]; %! Y1 = [1]; %! m = margin (mdl, X1, Y1); %! assert_equal (m, -1) ## Test input validation for margin method %!error ... %! margin (ClassificationKNN (ones (4,2), ones (4,1))) %!error ... %! margin (ClassificationKNN (ones (4,2), ones (4,1)), ones (4,2)) %!error ... %! margin (ClassificationKNN (ones (40,2), randi ([1, 2], 40, 1)), [], zeros (2)) %!error ... %! margin (ClassificationKNN (ones (40,2), randi ([1, 2], 40, 1)), 1, zeros (2)) %!error ... %! margin (ClassificationKNN (ones (4,2), ones (4,1)), ones (4,2), ones (3,1)) ## Test output for partialDependence %!shared X, Y, mdl %! X = [1, 2; 4, 5; 7, 8; 3, 2]; %! Y = [2; 1; 3; 2]; %! mdl = fitcknn (X, Y); %!test %! Vars = 1; %! Labels = 2; %! [pd, x, y] = partialDependence (mdl, Vars, Labels); %! pdm = [0.7500, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000]; %! assert_equal (pd, pdm) %!test %! Vars = 1; %! Labels = 2; %! [pd, x, y] = partialDependence (mdl, Vars, Labels, ... %! 'NumObservationsToSample', 5); %! pdm = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... %! 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... %! 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... %! 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... %! 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; %! assert_equal (all ((abs (pdm - pd) < 1)(:)), true) %!test %! Vars = [1, 2]; %! Labels = 1; %! queryPoints = {linspace(0, 1, 3)', linspace(0, 1, 3)'}; %! [pd, x, y] = partialDependence (mdl, Vars, Labels, 'QueryPoints', ... %! queryPoints); %! pdm = [0, 0, 0; 0, 0, 0; 0, 0, 0]; %! assert_equal (pd, pdm) %!test %! Vars = 1; %! Labels = [1; 2]; %! [pd, x, y] = partialDependence (mdl, Vars, Labels); %! pdm = [0.2500, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.2500, 0.2500, 0.2500, ... %! 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, ... %! 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, ... %! 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, ... %! 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, ... %! 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, ... %! 0.2500, 0.2500; 0.7500, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000]; %! assert_equal (pd, pdm) %!test %! Vars = [1, 2]; %! Labels = [1; 2]; %! queryPoints = {linspace(0, 1, 3)', linspace(0, 1, 3)'}; %! [pd, x, y] = partialDependence (mdl, Vars, Labels, 'QueryPoints', queryPoints); %! pdm(:,:,1) = [0, 0, 0; 1, 1, 1]; %! pdm(:,:,2) = [0, 0, 0; 1, 1, 1]; %! pdm(:,:,3) = [0, 0, 0; 1, 1, 1]; %! assert_equal (pd, pdm) %!test %! X1 = [1; 2; 4; 5; 7; 8; 3; 2]; %! X2 = ['2'; '3'; '1'; '3'; '1'; '3'; '2'; '2']; %! X = [X1, double(X2)]; %! Y = [1; 2; 3; 3; 2; 1; 2; 1]; %! mdl = fitcknn (X, Y, 'ClassNames', {'1', '2', '3'}); %! Vars = 1; %! Labels = 1; %! [pd, x, y] = partialDependence (mdl, Vars, Labels); %! pdm = [1.0000, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, ... %! 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, ... %! 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... %! 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... %! 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.3750, ... %! 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, ... %! 0.3750, 0.3750, 0.3750, 0.3750, 0.7500, 0.7500, 0.7500, 0.7500, 0.7500, ... %! 0.7500, 0.7500, 0.7500]; %! assert_equal (pd, pdm) %!test %! X1 = [1; 2; 4; 5; 7; 8; 3; 2]; %! X2 = ['2'; '3'; '1'; '3'; '1'; '3'; '2'; '2']; %! X = [X1, double(X2)]; %! Y = [1; 2; 3; 3; 2; 1; 2; 1]; %! predictorNames = {'Feature1', 'Feature2'}; %! mdl = fitcknn (X, Y, 'PredictorNames', predictorNames); %! Vars = 'Feature1'; %! Labels = 1; %! [pd, x, y] = partialDependence (mdl, Vars, Labels); %! pdm = [1.0000, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, ... %! 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, ... %! 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... %! 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... %! 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.3750, ... %! 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, ... %! 0.3750, 0.3750, 0.3750, 0.3750, 0.7500, 0.7500, 0.7500, 0.7500, 0.7500, ... %! 0.7500, 0.7500, 0.7500]; %! assert_equal (pd, pdm) %!test %! X1 = [1; 2; 4; 5; 7; 8; 3; 2]; %! X2 = ['2'; '3'; '1'; '3'; '1'; '3'; '2'; '2']; %! X = [X1, double(X2)]; %! Y = [1; 2; 3; 3; 2; 1; 2; 1]; %! predictorNames = {'Feature1', 'Feature2'}; %! mdl = fitcknn (X, Y, 'PredictorNames', predictorNames); %! new_X1 = [10; 5; 6; 8; 9; 20; 35; 6]; %! new_X2 = ['2'; '2'; '1'; '2'; '1'; '3'; '3'; '2']; %! new_X = [new_X1, double(new_X2)]; %! Vars = 'Feature1'; %! Labels = 1; %! [pd, x, y] = partialDependence (mdl, Vars, Labels, new_X); %! pdm = [0, 0, 0, 0, 0, 0.2500, 0.2500, 0.2500, 0.2500, 0.7500, 0.7500, ... %! 0.7500, 0.7500, 0.7500, 0.7500, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, ... %! 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, ... %! 1.0000, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ... %! 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ... %! 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]; %! assert_equal (pd, pdm) ## partialDependence, as measured on R2024a %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'NumNeighbors', 5); %! [pd, x] = partialDependence (Mdl, 4, 'versicolor', ... %! 'QueryPoints', [0.5; 1; 1.5; 2]); %! assert_equal (pd, [0.5013333333, 0.452, 0.384, 0.236], 1e-10); %! [pd, x] = partialDependence (Mdl, 3, {'setosa'; 'virginica'}); %! assert_equal (size (pd), [2, 100]); %! assert_equal (pd(:,[1, 100]), [1, 0; 0, 0.9466666667], 1e-10); %! [pd, x] = partialDependence (Mdl, 2, 'virginica', meas(1:60,:)); %! assert_equal (pd([1, 50, 100]), [0.01666666667, 0, 0.003333333333], 1e-10); %! assert_equal (x([1, 100]), [2.3; 4.4], 1e-12); %!test # points of the default grid, rows following y and columns x %! load fisheriris %! Mdl = fitcknn (meas, species, 'NumNeighbors', 5); %! gx = linspace (min (meas(:,1)), max (meas(:,1)), 100)'; %! gy = linspace (min (meas(:,3)), max (meas(:,3)), 100)'; %! pd = partialDependence (Mdl, [1, 3], 'versicolor', ... %! 'QueryPoints', {gx([1, 2, 60, 100]), ... %! gy([1, 2, 50, 100])}); %! assert_equal (size (pd), [4, 4]); %! assert_equal ([pd(1,1), pd(1,2), pd(2,1), pd(4,1), pd(1,4), pd(3,3)], ... %! [0, 0, 0, 0.09733333333, 0, 0.9933333333], 1e-10); ## Test output for crossval method %!shared x, y, obj %! load fisheriris %! x = meas; %! y = species; %! covMatrix = cov (x); %! obj = fitcknn (x, y, 'NumNeighbors', 5, 'Distance', ... %! 'mahalanobis', 'Cov', covMatrix); %!test %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! CVMdl = crossval (obj); %! warning (status); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y}) %! assert_equal (CVMdl.KFold == 10, true) %! assert_equal (CVMdl.ModelParameters.NumNeighbors == 5, true) %! assert_equal (strcmp (CVMdl.ModelParameters.Distance, 'mahalanobis'), true) %! assert_equal (class (CVMdl.Trained{1}), "ClassificationKNN") %! assert_equal (isempty (CVMdl.Trained{1}.Mu), true) %!test %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! CVMdl = crossval (obj, 'KFold', 5); %! warning (status); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y}) %! assert_equal (CVMdl.KFold == 5, true) %! assert_equal (CVMdl.ModelParameters.NumNeighbors == 5, true) %! assert_equal (strcmp (CVMdl.ModelParameters.Distance, 'mahalanobis'), true) %! assert_equal (class (CVMdl.Trained{1}), "ClassificationKNN") %! assert_equal (isempty (CVMdl.Trained{1}.Mu), isempty (obj.Mu)) %!test %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! CVMdl = crossval (obj, 'HoldOut', 0.2); %! warning (status); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y}) %! assert_equal (CVMdl.ModelParameters.NumNeighbors == 5, true) %! assert_equal (strcmp (CVMdl.ModelParameters.Distance, 'mahalanobis'), true) %! assert_equal (class (CVMdl.Trained{1}), "ClassificationKNN") %! assert_equal (isempty (CVMdl.Trained{1}.Mu), isempty (obj.Mu)) %!test %! obj = fitcknn (x, y, 'NumNeighbors', 10, 'Distance', 'cityblock'); %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! CVMdl = crossval (obj, 'HoldOut', 0.2); %! warning (status); %! CVMdl = crossval (obj, 'LeaveOut', 'on'); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y}) %! assert_equal (CVMdl.ModelParameters.NumNeighbors == 10, true) %! assert_equal (strcmp (CVMdl.ModelParameters.Distance, 'cityblock'), true) %! assert_equal (class (CVMdl.Trained{1}), "ClassificationKNN") %! assert_equal (isempty (CVMdl.Trained{1}.Mu), isempty (obj.Mu)) %!test %! obj = fitcknn (x, y, 'NumNeighbors', 10, 'Distance', 'cityblock'); %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! partition = cvpartition (y, 'KFold', 3); %! warning (status); %! CVMdl = crossval (obj, 'cvPartition', partition); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal (CVMdl.KFold == 3, true) %! assert_equal (CVMdl.ModelParameters.NumNeighbors == 10, true) %! assert_equal (strcmp (CVMdl.ModelParameters.Distance, 'cityblock'), true) %! assert_equal (class (CVMdl.Trained{1}), "ClassificationKNN") %! assert_equal (isempty (CVMdl.Trained{1}.Mu), isempty (obj.Mu)) ## Test input validation for crossval method %!error ... %! crossval (ClassificationKNN (ones (4,2), ones (4,1)), 'kfold') %!error... %! crossval (ClassificationKNN (ones (4,2), ones (4,1)), 'kfold', 12, 'holdout', 0.2) %!error ... %! crossval (ClassificationKNN (ones (4,2), ones (4,1)), 'kfold', 'a') %!error ... %! crossval (ClassificationKNN (ones (4,2), ones (4,1)), 'holdout', 2) %!error ... %! crossval (ClassificationKNN (ones (4,2), ones (4,1)), 'leaveout', 1) %!error ... %! crossval (ClassificationKNN (ones (4,2), ones (4,1)), 'cvpartition', 1) %!error ... %! savemodel (ClassificationKNN ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2])) %!error ... %! savemodel (ClassificationKNN ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), 1) %!error ... %! savemodel (ClassificationKNN ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), ['ab'; 'cd']) ## There are only as many neighbors as training samples, so a larger ## NumNeighbors is capped at that. It used to be stored as requested and then ## fail at prediction with a nonconformant operator error. %!test %! x = [1, 2; 3, 4; 5, 6; 7, 8; 2, 3]; %! y = [1; 2; 1; 2; 1]; %! for k = [5, 6, 20] %! a = fitcknn (x, y, 'NumNeighbors', k); %! assert_equal (a.NumNeighbors, 5); %! assert_equal (predict (a, [2, 2; 6, 6]), [1; 1]); %! endfor ## An assigned ScoreTransform reaches the scores predict returns. %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'NumNeighbors', 5); %! [~, s0] = predict (Mdl, meas(60,:)); %! Mdl.ScoreTransform = 'logit'; %! [~, s1] = predict (Mdl, meas(60,:)); %! assert_equal (s1, 1 ./ (1 + exp (-s0)), 1e-12); ## The transform is applied once per prediction, not once per observation. ## It used to sit inside the loop over the query rows and be applied to ## everything accumulated so far, so row i of n came back transformed n-i+1 ## times and only the last row was right. %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'NumNeighbors', 5); %! [~, s0] = predict (Mdl, meas(1:6,:)); %! Mdl.ScoreTransform = 'logit'; %! [~, s1] = predict (Mdl, meas(1:6,:)); %! assert_equal (s1, 1 ./ (1 + exp (-s0)), 1e-12); ## A row predicted on its own and the same row predicted among others get the ## same scores, which is what applying the transform once per row means. %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'ScoreTransform', 'doublelogit'); %! [~, s1] = predict (Mdl, meas(1,:)); %! [~, s6] = predict (Mdl, meas(1:6,:)); %! assert_equal (s6(1,:), s1, 1e-12); ## The values are R2024a's: a default KNN scores the first four observations ## [1 0 0] and doublelogit takes that to [0.8808 0.5 0.5], every row alike. %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'ScoreTransform', 'doublelogit'); %! [~, s] = predict (Mdl, meas(1:4,:)); %! assert_equal (s, repmat ([0.880797077977882, 0.5, 0.5], 4, 1), 1e-12); ## 'logit' is the logistic function and 'invlogit' its inverse, as MATLAB has ## them, and not the other way about. %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'NumNeighbors', 4); %! [~, s0] = predict (Mdl, meas(60,:)); %! Mdl.ScoreTransform = 'invlogit'; %! [~, s] = predict (Mdl, meas(60,:)); %! assert_equal (s, log (s0 ./ (1 - s0)), 1e-12); ## Setting ScoreTransform to 'none' leaves predict working, the transform being ## a function handle like every other. %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'NumNeighbors', 5); %! [~, s0] = predict (Mdl, meas(1:3,:)); %! Mdl.ScoreTransform = 'none'; %! assert_equal (class (Mdl.ScoreTransform), 'char'); %! [~, s1] = predict (Mdl, meas(1:3,:)); %! assert_equal (s1, s0); ## RowsUsed is empty when every observation was used. %!test %! load fisheriris %! X = meas; %! Y = grp2idx (species); %! Mdl = fitcknn (X, Y); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (class (Mdl.RowsUsed), 'double'); %! assert_equal (Mdl.NumObservations, 150); %! assert_equal (rows (Mdl.X), 150); ## A missing response drops its observation and RowsUsed marks it. %!test %! load fisheriris %! X = meas; %! Y = grp2idx (species); %! Y(5) = NaN; %! Mdl = fitcknn (X, Y); %! assert_equal (class (Mdl.RowsUsed), 'logical'); %! assert_equal (size (Mdl.RowsUsed), [150, 1]); %! assert_equal (sum (Mdl.RowsUsed), 149); %! assert_equal (Mdl.RowsUsed(5), false); %! assert_equal (Mdl.NumObservations, 149); %! assert_equal (rows (Mdl.X), 149); ## A missing predictor keeps its observation, so RowsUsed stays empty. %!test %! load fisheriris %! X = meas; %! X(3,2) = NaN; %! Y = grp2idx (species); %! Mdl = fitcknn (X, Y); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (Mdl.NumObservations, 150); %! assert_equal (rows (Mdl.X), 150); %! assert_equal (sum (isnan (Mdl.X(:))), 1); ## Mu and Sigma are empty unless the predictors are standardized. %!test %! load fisheriris %! Mdl = fitcknn (meas, species); %! assert_equal (Mdl.Mu, []); %! assert_equal (Mdl.Sigma, []); ## Each predictor is summarized from every observation that has it. Column 3 ## is complete, so its mean spans all 150 rows. Values from MATLAB R2024a. %!test %! load fisheriris %! X = meas; %! X(3,2) = NaN; X(17,4) = NaN; X(140,1) = NaN; %! Mdl = fitcknn (X, species, 'Standardize', true); %! assert_equal (Mdl.Mu, [5.8362416107382593, 3.0563758389261766, ... %! 3.7580000000000031, 1.2046979865771823], 1e-13); %! assert_equal (Mdl.Sigma, [0.82627581654893234, 0.43717801242449683, ... %! 1.7652982332594667, 0.76196186774754338], 1e-13); %! assert_equal (Mdl.Mu(3), mean (meas(:,3)), 1e-13); ## Each class carries its prior spread over its own observations. Values ## from R2024a. %!test %! load fisheriris %! i3 = [1:50, 51:80, 101:120]; %! Mdl = fitcknn (meas(i3,:), species(i3), 'Prior', 'uniform'); %! assert_equal (Mdl.Prior, [1, 1, 1] / 3, 1e-14); %! assert_equal (Mdl.W(1), 1/150, 1e-14); %! assert_equal (Mdl.W(51), 1/90, 1e-14); %! assert_equal (Mdl.W(81), 1/60, 1e-14); ## Reassigning the prior rebuilds the weights with it. %!test %! load fisheriris %! i3 = [1:50, 51:80, 101:120]; %! Mdl = fitcknn (meas(i3,:), species(i3)); %! assert_equal (Mdl.W(1), 0.01, 1e-14); %! Mdl.Prior = [0.98, 0.01, 0.01]; %! assert_equal (Mdl.W(1), 0.0196, 1e-14); ## A fitted model survives savemodel and loadmodel: the properties come ## back as they were and it predicts the same. %!test %! load fisheriris %! Mdl = fitcknn (meas, species); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2), 'ClassificationKNN'); %! assert_equal (M2.NumObservations, Mdl.NumObservations); %! assert_equal (M2.PredictorNames, Mdl.PredictorNames); %! assert_equal (class (M2.ScoreTransform), class (Mdl.ScoreTransform)); %! assert_equal (predict (M2, meas(1:5,:)), predict (Mdl, meas(1:5,:))); ## The six settable properties. Every value below was measured on MATLAB ## R2024a; see KNN_SETTABLE_LEDGER.md for the probes that produced them. ## A lazy learner does its work at predict time, so a reassigned model ## predicts exactly as one refitted with the same options. %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'NumNeighbors', 1); %! Mdl.NumNeighbors = 50; %! Mdl.DistanceWeight = 'inverse'; %! Mdl.Distance = 'cityblock'; %! M2 = fitcknn (meas, species, 'NumNeighbors', 50, ... %! 'DistanceWeight', 'inverse', 'Distance', 'cityblock'); %! assert_equal (predict (Mdl, meas), predict (M2, meas)); ## More neighbours than observations is reduced, not refused. %!test %! load fisheriris %! Mdl = fitcknn (meas, species); %! Mdl.NumNeighbors = 1000; %! assert_equal (Mdl.NumNeighbors, 150); ## NSMethod is read-only and constrains the metric: a kd-tree can only be ## searched with the four metrics it was built for. %!test %! load fisheriris %! Mdl = fitcknn (meas, species); %! assert_equal (Mdl.NSMethod, 'kdtree'); %! Mdl.Distance = 'minkowski'; %! assert_equal (Mdl.NSMethod, 'kdtree'); %! assert_equal (Mdl.DistParameter, 2); ## The parameter belongs to the metric, so changing the metric recomputes it ## and whatever the previous one held is discarded. %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'Distance', 'minkowski', ... %! 'NSMethod', 'exhaustive'); %! Mdl.DistParameter = 3; %! Mdl.Distance = 'euclidean'; %! assert_equal (Mdl.DistParameter, []); %! Mdl.Distance = 'minkowski'; %! assert_equal (Mdl.DistParameter, 2); ## Reassigning the same metric is not a change, so the parameter survives. %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'Distance', 'minkowski', ... %! 'NSMethod', 'exhaustive'); %! Mdl.DistParameter = 3; %! Mdl.Distance = 'minkowski'; %! assert_equal (Mdl.DistParameter, 3); %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'NSMethod', 'exhaustive'); %! Mdl.Distance = 'seuclidean'; %! assert_equal (Mdl.DistParameter, std (meas), 1e-12); %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'NSMethod', 'exhaustive'); %! Mdl.Distance = 'mahalanobis'; %! assert_equal (Mdl.DistParameter, cov (meas), 1e-12); ## DistanceWeight is the text, as MATLAB reports it, and it weights the vote. %!test %! Mdl = fitcknn ([1, 0; 2, 0; 50, 0; 51, 0], {'b';'a';'a';'b'}, ... %! 'NumNeighbors', 2, 'NSMethod', 'exhaustive'); %! assert_equal (Mdl.DistanceWeight, 'equal'); %! [label, score] = predict (Mdl, [0, 0]); %! assert_equal (label, {'a'}); %! assert_equal (score, [0.5, 0.5], 1e-6); %!test %! Mdl = fitcknn ([1, 0; 2, 0; 50, 0; 51, 0], {'b';'a';'a';'b'}, ... %! 'NumNeighbors', 2, 'DistanceWeight', 'inverse', ... %! 'NSMethod', 'exhaustive'); %! [label, score] = predict (Mdl, [0, 0]); %! assert_equal (label, {'b'}); %! assert_equal (score, [1/3, 2/3], 1e-6); %!test %! Mdl = fitcknn ([1, 0; 2, 0; 50, 0; 51, 0], {'b';'a';'a';'b'}, ... %! 'NumNeighbors', 2, 'DistanceWeight', 'squaredinverse', ... %! 'NSMethod', 'exhaustive'); %! [label, score] = predict (Mdl, [0, 0]); %! assert_equal (label, {'b'}); %! assert_equal (score, [0.2, 0.8], 1e-6); ## BreakTies decides the label when two classes hold the same weight: the ## first class name, or the class of the nearest of the tied neighbours. %!test %! Mdl = fitcknn ([1, 0; 2, 0; 50, 0; 51, 0], {'b';'a';'a';'b'}, ... %! 'NumNeighbors', 2, 'NSMethod', 'exhaustive'); %! Mdl.BreakTies = 'smallest'; %! assert_equal (predict (Mdl, [0, 0]), {'a'}); %! Mdl.BreakTies = 'nearest'; %! assert_equal (predict (Mdl, [0, 0]), {'b'}); ## IncludeTies takes every neighbour sitting at the kth distance, which turns ## a single winner into a split. %!test %! x = [0, 0; 0, 0; 1, 1; 1, 1; 2, 2; 2, 2]; %! y = {'a'; 'b'; 'a'; 'b'; 'a'; 'b'}; %! Mdl = fitcknn (x, y, 'NumNeighbors', 1, 'NSMethod', 'exhaustive'); %! [~, score] = predict (Mdl, [0.5, 0.5]); %! assert_equal (score, [1, 0]); %! Mdl.IncludeTies = true; %! [~, score] = predict (Mdl, [0.5, 0.5]); %! assert_equal (score, [0.5, 0.5]); ## A saved model comes back with the weight it was given. %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'DistanceWeight', 'squaredinverse'); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.DistanceWeight, 'squaredinverse'); %! assert_equal (predict (M2, meas), predict (Mdl, meas)); %!error ... %! Mdl = fitcknn (ones (5, 2), [1;1;1;2;2]); Mdl.NumNeighbors = 0; %!error ... %! Mdl = fitcknn (ones (5, 2), [1;1;1;2;2]); Mdl.NumNeighbors = 2.5; %!error ... %! Mdl = fitcknn (ones (5, 2), [1;1;1;2;2]); Mdl.BreakTies = 'bogus'; %!error ... %! Mdl = fitcknn (ones (5, 2), [1;1;1;2;2]); Mdl.IncludeTies = 2; %!error ... %! Mdl = fitcknn (ones (5, 2), [1;1;1;2;2]); Mdl.DistanceWeight = 'bogus'; %!error ... %! load fisheriris; Mdl = fitcknn (meas, species); ... %! Mdl.Distance = 'mahalanobis'; %!error ... %! load fisheriris; ... %! Mdl = fitcknn (meas, species, 'NSMethod', 'exhaustive'); ... %! Mdl.DistParameter = 2; %!error ... %! load fisheriris; ... %! Mdl = fitcknn (meas, species, 'Distance', 'minkowski', ... %! 'NSMethod', 'exhaustive'); ... %! Mdl.DistParameter = 0; %!error ... %! load fisheriris; ... %! Mdl = fitcknn (meas, species, 'Distance', 'seuclidean', ... %! 'NSMethod', 'exhaustive'); ... %! Mdl.DistParameter = ones (1, 3); %!error ... %! load fisheriris; ... %! Mdl = fitcknn (meas, species, 'Distance', 'mahalanobis', ... %! 'NSMethod', 'exhaustive'); ... %! Mdl.DistParameter = -eye (4); ## A zero scale is refused, where MATLAB accepts it and warns at predict time. ## The message MATLAB raises for a negative scale already promises positive ## values, so this keeps the property and its own contract in agreement. %!error ... %! load fisheriris; ... %! Mdl = fitcknn (meas, species, 'Distance', 'seuclidean', ... %! 'NSMethod', 'exhaustive'); ... %! Mdl.DistParameter = zeros (1, 4); ## edge, resubPredict, resubMargin, resubEdge and resubLoss. Measured on ## MATLAB R2024a, whose posteriors this class reproduces exactly. %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'NumNeighbors', 5); %! assert_equal (edge (Mdl, meas, species), 0.9253333333, 1e-9); %! assert_equal (edge (Mdl, meas, species), ... %! mean (margin (Mdl, meas, species)), 1e-12); ## Weights normalized within class to the prior, not divided by their total, ## which would give 0.9006799117. %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'NumNeighbors', 5); %! assert_equal (edge (Mdl, meas, species, 'Weights', (1:150)'), ... %! 0.9265719286, 1e-9); %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'NumNeighbors', 5); %! assert_equal (resubEdge (Mdl), 0.9253333333, 1e-9); %! assert_equal (resubLoss (Mdl), 1/30, 1e-12); %! assert_equal (sum (resubMargin (Mdl)), 138.8, 1e-9); ## A one-neighbour model is its own nearest neighbour everywhere, so the ## resubstitution numbers are perfect and say nothing about the model. %!test %! load fisheriris %! Mdl = fitcknn (meas, species); %! assert_equal (resubLoss (Mdl), 0); %! assert_equal (resubEdge (Mdl), 1); %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'NumNeighbors', 5); %! [label, score, cost] = resubPredict (Mdl); %! [l2, s2, c2] = predict (Mdl, meas); %! assert_equal (label, l2); %! assert_equal (score, s2); %! assert_equal (cost, c2); %!error ... %! load fisheriris; edge (fitcknn (meas, species), meas) %!error ... %! load fisheriris; ... %! edge (fitcknn (meas, species), meas, species, 'Weights', ones (3, 1)) %!error ... %! load fisheriris; edge (fitcknn (meas, species), meas, species, 'Nope', 1) ## BinEdges is an empty cell, which is what MATLAB reports for this ## learner as well: it fits the predictors as they are. %!test %! load fisheriris %! Mdl = fitcknn (meas, species); %! assert_equal (class (Mdl.BinEdges), 'cell'); %! assert_equal (Mdl.BinEdges, {}); ## CategoricalPredictors and ExpandedPredictorNames, shapes measured on ## R2024a: an empty double and one name per predictor. %!test %! load fisheriris %! Mdl = fitcknn (meas, species); %! assert_equal (Mdl.CategoricalPredictors, []); %! assert_equal (size (Mdl.CategoricalPredictors), [0, 0]); %! assert_equal (Mdl.ExpandedPredictorNames, Mdl.PredictorNames); %! assert_equal (size (Mdl.ExpandedPredictorNames), [1, 4]); %!test %! load fisheriris %! Mdl = fitcknn (meas, species); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.CategoricalPredictors, Mdl.CategoricalPredictors); %! assert_equal (M2.ExpandedPredictorNames, Mdl.ExpandedPredictorNames); ## The shared cost guard is in force here too, and the struct form is ## permuted into this model's class order. The battery is on ## ClassificationDiscriminant. %!test %! load fisheriris %! Mdl = fitcknn (meas, species); %! S = struct ('ClassNames', {{'virginica'; 'setosa'; 'versicolor'}}, ... %! 'ClassificationCosts', [0, 1, 2; 3, 0, 4; 5, 6, 0]); %! Mdl.Cost = S; %! assert_equal (Mdl.Cost, [0, 4, 3; 6, 0, 5; 1, 2, 0]); %!error ... %! load fisheriris %! Mdl = fitcknn (meas, species); %! Mdl.Cost = ones (3); ## HyperparameterOptimizationResults is declared for MATLAB compatibility and ## stays empty, this class running no search over its hyperparameters. %!test %! load fisheriris %! Mdl = fitcknn (meas, species); %! assert_equal (isempty (Mdl.HyperparameterOptimizationResults), true); ## ModelParameters records the fit as it was asked for. The field list and ## its order are MATLAB's, measured on R2024a. %!test %! load fisheriris %! Mdl = fitcknn (meas, species); %! assert_equal (fieldnames (Mdl.ModelParameters)', {'NumNeighbors', ... %! 'NSMethod', 'Distance', 'BucketSize', 'IncludeTies', 'DistanceWeight', ... %! 'BreakTies', 'Exponent', 'Cov', 'Scale', 'StandardizeData', 'Version', ... %! 'Method', 'Type'}); %!test %! load fisheriris %! MP = fitcknn (meas, species).ModelParameters; %! assert_equal (MP.NumNeighbors, 1); %! assert_equal (MP.NSMethod, 'kdtree'); %! assert_equal (MP.Distance, 'euclidean'); %! assert_equal (MP.BucketSize, 50); %! assert_equal (MP.IncludeTies, false); %! assert_equal (MP.DistanceWeight, 'equal'); %! assert_equal (MP.BreakTies, 'smallest'); %!test %! load fisheriris %! MP = fitcknn (meas, species).ModelParameters; %! assert_equal (MP.Version, 1); %! assert_equal (MP.Method, 'KNN'); %! assert_equal (MP.Type, 'classification'); ## BucketSize belongs to the kd-tree alone, so an exhaustive search reports ## none, as MATLAB does. %!test %! load fisheriris %! MP = fitcknn (meas, species, 'NSMethod', 'exhaustive').ModelParameters; %! assert_equal (isempty (MP.BucketSize), true); ## The three distance parameters hold what was passed, never what was ## computed: a mahalanobis fit given no Cov reports none, while ## DistParameter carries the covariance the fit used. %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'Distance', 'mahalanobis'); %! assert_equal (isempty (Mdl.ModelParameters.Cov), true); %! assert_equal (size (Mdl.DistParameter), [4, 4]); %!test %! load fisheriris %! C = cov (meas); %! MP = fitcknn (meas, species, 'Distance', 'mahalanobis', ... %! 'Cov', C).ModelParameters; %! assert_equal (MP.Cov, C); %!test %! load fisheriris %! MP = fitcknn (meas, species, 'Distance', 'minkowski', ... %! 'Exponent', 3).ModelParameters; %! assert_equal (MP.Exponent, 3); %! assert_equal (isempty (MP.Cov), true); ## Exponent is the one of the three that carries a default: a 'minkowski' ## fit that names none reports 2, where Cov and Scale would stay empty. %!test %! load fisheriris %! MP = fitcknn (meas, species, 'Distance', 'minkowski').ModelParameters; %! assert_equal (MP.Exponent, 2); %!test %! load fisheriris %! MP = fitcknn (meas, species, 'Distance', 'seuclidean').ModelParameters; %! assert_equal (isempty (MP.Scale), true); ## An explicit BucketSize is still not reported when the search cannot use ## it, as MATLAB does. %!test %! load fisheriris %! MP = fitcknn (meas, species, 'NSMethod', 'exhaustive', ... %! 'BucketSize', 30).ModelParameters; %! assert_equal (isempty (MP.BucketSize), true); %!test %! load fisheriris %! MP = fitcknn (meas, species, 'NSMethod', 'kdtree', ... %! 'BucketSize', 30).ModelParameters; %! assert_equal (MP.BucketSize, 30); %!test %! load fisheriris %! S = std (meas); %! MP = fitcknn (meas, species, 'Distance', 'seuclidean', ... %! 'Scale', S).ModelParameters; %! assert_equal (MP.Scale, S); %!test %! load fisheriris %! MP = fitcknn (meas, species, 'Standardize', true).ModelParameters; %! assert_equal (MP.StandardizeData, true); %! assert_equal (class (MP.StandardizeData), 'logical'); %!test %! load fisheriris %! MP = fitcknn (meas, species, 'NumNeighbors', 10, ... %! 'DistanceWeight', 'inverse', ... %! 'BreakTies', 'nearest', 'IncludeTies', true).ModelParameters; %! assert_equal (MP.NumNeighbors, 10); %! assert_equal (MP.DistanceWeight, 'inverse'); %! assert_equal (MP.BreakTies, 'nearest'); %! assert_equal (MP.IncludeTies, true); ## A model searched exhaustively with a metric no kd-tree can take survives ## the round trip. Its loader used to refuse it: the metric was assigned ## while NSMethod still held the default the empty model is built with. %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'Distance', 'seuclidean', ... %! 'NSMethod', 'exhaustive'); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.NSMethod, 'exhaustive'); %! assert_equal (M2.Distance, 'seuclidean'); %! assert_equal (M2.DistParameter, Mdl.DistParameter); %! assert_equal (predict (M2, meas(1:5,:)), predict (Mdl, meas(1:5,:))); %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'Distance', 'mahalanobis'); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.Distance, 'mahalanobis'); %! assert_equal (M2.DistParameter, Mdl.DistParameter); %! assert_equal (predict (M2, meas(1:5,:)), predict (Mdl, meas(1:5,:))); ## 'manhattan' names the same metric as 'cityblock' and a kd-tree is built ## for it, so it may be assigned to one and a model fitted with it reloads. %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'Distance', 'manhattan'); %! assert_equal (Mdl.NSMethod, 'kdtree'); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.Distance, 'manhattan'); %! assert_equal (predict (M2, meas(1:5,:)), predict (Mdl, meas(1:5,:))); ## The third output is the expected cost of each assignment, the posterior ## weighted by the cost matrix. A default matrix cannot tell that apart from ## 1 - posterior, so the fixture below sets an asymmetric one. %!test %! load fisheriris %! b = strcmp (species, 'setosa'); %! Mdl = fitcknn (meas, b, 'Cost', [0, 2; 5, 0]); %! [label, ~, cost] = predict (Mdl, meas([1, 51],:)); %! assert_equal (label, [true; false]); %! assert_equal (cost, [5, 0; 0, 2]); %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'NumNeighbors', 5); %! [~, score, cost] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (cost, 1 - score, 1e-15); ## Every documented score transform reaches the scores that are reported, and ## none of them moves the label: a transform reshapes what is reported, not ## what is decided. %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'NumNeighbors', 5); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! T = {'identity', @(x) x; 'doublelogit', @(x) 1 ./ (1 + exp (-2 * x)); ... %! 'invlogit', @(x) log (x ./ (1 - x)); ... %! 'logit', @(x) 1 ./ (1 + exp (-x)); ... %! 'sign', @(x) sign (x); 'symmetric', @(x) 2 * x - 1; ... %! 'symmetriclogit', @(x) 2 ./ (1 + exp (-x)) - 1}; %! for i = 1:rows (T) %! Mdl.ScoreTransform = T{i,1}; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, T{i,2}(raw), 1e-12); %! assert_equal (l, label); %! endfor %! ## ismax marks the largest score of each observation, ties to the first. %! [~, k] = max (raw, [], 2); %! e = zeros (size (raw)); %! e(sub2ind (size (raw), (1:rows (raw))', k)) = 1; %! Mdl.ScoreTransform = 'ismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, e); %! Mdl.ScoreTransform = 'symmetricismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, 2 * e - 1); ## A function handle is taken as given and applied to the scores. %!test %! load fisheriris %! Mdl = fitcknn (meas, species, 'NumNeighbors', 5); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! Mdl.ScoreTransform = @(x) x .^ 2; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, raw .^ 2, 1e-12); %! assert_equal (l, label); %!shared XC, yb, y3 %! k = (0:119)'; %! c1 = mod (k, 3) + 1; %! c3 = 10 * (mod (floor (k / 2), 2) + 1); %! XC = [c1, c3]; %! yb = 5 * (c1 == 2) + 0.5 * sin (k) - 3 * (c3 == 20) + 0.1 * cos (k) > 1; %! y3 = mod (c1 + floor (k / 7), 3) + 1; %!test # MATLAB parity: every predictor categorical compares levels %! Mdl = ClassificationKNN (XC, yb, 'CategoricalPredictors', 'all'); %! assert_equal (Mdl.Distance, 'hamming'); %! assert_equal (Mdl.CategoricalPredictors, [1, 2]); %! assert_equal (Mdl.ExpandedPredictorNames, {'x1', 'x2'}); %! [label, s] = predict (Mdl, [1, 10; 2, 20; 4, 10]); %! assert_equal (label, [false; true; false]); %! assert_equal (s, [1, 0; 0, 1; 1, 0]); %!test # MATLAB parity: five neighbours by Hamming distance, three classes %! Mdl = ClassificationKNN (XC, y3, 'CategoricalPredictors', 'all', ... %! 'NumNeighbors', 5); %! [label, s] = predict (Mdl, [1, 10; 2, 20; 4, 10]); %! assert_equal (label, [2; 1; 2]); %! assert_equal (s, [0, 0.6, 0.4; 0.6, 0.2, 0.2; 0.2, 0.4, 0.4], 1e-12); %!test # a given distance is kept, and folds take 'all' %! Mdl = ClassificationKNN (XC, yb, 'CategoricalPredictors', 'all', ... %! 'Distance', 'cityblock'); %! assert_equal (Mdl.Distance, 'cityblock'); %! CV = crossval (ClassificationKNN (XC, yb, 'CategoricalPredictors', ... %! 'all'), 'KFold', 3); %! assert_equal (CV.Trained{1}.Distance, 'hamming'); %! assert_equal (CV.Trained{1}.CategoricalPredictors, [1, 2]); %!error ... %! ClassificationKNN (XC, yb, 'CategoricalPredictors', 1) ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcknn (T, 'Species'); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'Species'), a); %! assert_equal (loss (Mdl, T), a); ## A table at edge %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcknn (T, 'Species'); %! a = edge (Mdl, X, y); %! assert_equal (edge (Mdl, T(:,1:2), y), a); %! assert_equal (edge (Mdl, T, 'Species'), a); %! assert_equal (edge (Mdl, T), a); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcknn (T, 'Species'); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## resubLoss takes a loss function and weights together, the weights given ## replacing the ones the model was fitted with %!test %! load fisheriris %! w = (1:150)'; %! Mdl = fitcknn (meas, species); %! assert_equal (resubLoss (Mdl, 'LossFun', 'classiferror', 'Weights', w), ... %! loss (Mdl, meas, species, 'LossFun', 'classiferror', ... %! 'Weights', w)); ## Observation weights of class single or double %!error ... %! fitcknn (ones (4, 2), [1; 1; 2; 2], 'Weights', int8 ([1; 1; 1; 1])) %!error ... %! fitcknn (ones (4, 2), [1; 1; 2; 2], 'Weights', true (4, 1)) %!test %! ## Single weights are stored single, summing to one %! load fisheriris %! w = 1 + (1:150)' / 7; %! Mdl = fitcknn (meas, species, 'Weights', single (w)); %! assert_equal (class (Mdl.W), 'single'); %! assert_equal (sum (double (Mdl.W)), 1, 1e-6); %!test %! ## Single weights compute as double %! load fisheriris %! w = 1 + (1:150)' / 7; %! A = fitcknn (meas, species, 'Weights', single (w)); %! B = fitcknn (meas, species, 'Weights', double (single (w))); %! assert_equal (nthargout (2, @predict, A, meas), nthargout (2, @predict, ... %! B, meas)); %!test %! ## A constant predictor is left unscaled by standardization %! X = [linspace(0, 1, 20)', ones(20, 1)]; %! Mdl = ClassificationKNN (X, [ones(10, 1); 2 * ones(10, 1)], 'Standardize', true); %! assert_equal (Mdl.Sigma(2), 1); ## Test a class with prior 0, whose observations are removed before fitting, ## as MATLAB R2024a and R2026b remove them %!warning ... %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! Mdl = ClassificationKNN (X, Y, 'Prior', [1, 0]); %! assert_equal (Mdl.NumObservations, 50); %! assert_equal (Mdl.RowsUsed, [true(50, 1); false(50, 1)]); %! assert_equal (Mdl.Prior, [1, 0]); %! assert_equal (predict (Mdl, meas([51, 110], 3:4)), ... %! {'versicolor'; 'versicolor'}); %!warning ... %! load fisheriris %! Mdl = ClassificationKNN (meas, species, 'Prior', [0.5, 0.5, 0]); %! assert_equal (predict (Mdl, meas([1, 51, 101], :)), ... %! {'setosa'; 'versicolor'; 'versicolor'}); %!test %! ## A named class with prior 0 may be absent, having nothing to remove %! load fisheriris %! X = meas(51:100, 3:4); %! Y = species(51:100); %! C = {'versicolor', 'virginica'}; %! Mdl = ClassificationKNN (X, Y, 'ClassNames', C, 'Prior', [1, 0]); %! assert_equal (Mdl.ClassNames, {'versicolor'; 'virginica'}); %! assert_equal (predict (Mdl, meas(51, 3:4)), {'versicolor'}); %!warning ... %! load fisheriris %! X = meas(51:150, 3:4); %! X([3, 70], 1) = NaN; %! Mdl = ClassificationKNN (X, species(51:150), 'Prior', [1, 0]); %! assert_equal ([Mdl.NumObservations, rows(Mdl.X), nnz(Mdl.RowsUsed)], ... %! [50, 50, 50]); %!test %! ## A class that costs nothing to misclassify is kept, as in R2024a %! load fisheriris %! Mdl = ClassificationKNN (meas(51:150, 3:4), species(51:150), ... %! 'Cost', [0, 1; 0, 0]); %! assert_equal (Mdl.NumObservations, 100); %! assert_equal (Mdl.Prior, [0.5, 0.5]); %!test %! load fisheriris %! Y = char (species(51:100)); %! C = char ({'versicolor', 'virginica'}); %! Mdl = ClassificationKNN (meas(51:100, 3:4), Y, 'ClassNames', C, ... %! 'Prior', [1, 0]); %! assert_equal (cellstr (predict (Mdl, meas(51, 3:4))), {'versicolor'}); ## MATLAB parity: a row missing every predictor is dropped, and one missing ## only some is used, as measured on R2024a %!shared knX, knL %! knX = [mod((1:24)', 7) + (1:24)' / 10, mod((1:24)', 5) - (1:24)' / 20]; %! knX(2,1) = NaN; %! knL = repmat ({'p'; 'q'; 'q'; 'p'; 'p'; 'q'; 'p'; 'q'}, 3, 1); %!test # a row missing one predictor is used %! Mdl = fitcknn (knX, knL); %! assert_equal (Mdl.NumObservations, 24); %! assert_equal (Mdl.RowsUsed, []); %!test # a row missing every predictor is not %! X = knX; %! X([7, 15],:) = NaN; %! Mdl = fitcknn (X, knL); %! assert_equal (Mdl.NumObservations, 22); %! assert_equal (find (! Mdl.RowsUsed)', [7, 15]); %! assert_equal ([rows(Mdl.X), numel(Mdl.Y), numel(Mdl.W)], [22, 22, 22]); %! assert_equal (rows (resubPredict (Mdl)), 22); %!test # the empirical prior counts the rows used %! X = knX; %! X([7, 15],:) = NaN; %! assert_equal (fitcknn (X, knL).Prior, [10, 12] / 22, 1e-15); %!test # a class held only by dropped rows is no class of the model; R2024a %! # lists it in ClassNames with prior 0 %! X = knX; %! X([7, 15],:) = NaN; %! L = knL; %! L([7, 15]) = {'r'}; %! Mdl = fitcknn (X, L); %! assert_equal (Mdl.ClassNames(:)', {'p', 'q'}); %! assert_equal (Mdl.Prior, [10, 12] / 22, 1e-15); statistics-release-2.1.2/inst/Machine_Learning/ClassificationKernel.m000066400000000000000000002300571527311104300257420ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ClassificationKernel < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} ClassificationKernel ## ## Gaussian kernel binary classifier for large data. ## ## A @qcode{ClassificationKernel} object maps the predictors into a ## randomized feature space whose inner product approximates a Gaussian ## kernel, and then fits a linear model there. A kernel classifier is ## therefore as nonlinear as a support vector machine with a Gaussian kernel, ## while costing what a linear fit costs: nothing of size @math{NxN} is ever ## formed. ## ## The expansion is the random Fourier basis of Rahimi and Recht, drawn once ## when the model is fitted and kept with it, so @code{predict} maps new data ## through the same basis. MATLAB approximates the same kernel by the ## Fastfood construction, which reaches the same distribution more cheaply; ## the two are interchangeable in distribution but not draw by draw, and the ## draws come from different generators in any case, so the scores of a model ## fitted here and one fitted in MATLAB differ even from the same seed. ## What does not differ is what they estimate. ## ## Like @qcode{ClassificationLinear} the object holds no copy of the training ## data. It does hold the basis and the coefficients, so it is bounded by ## the number of expansion dimensions rather than by the number of ## observations. ## ## Create a @qcode{ClassificationKernel} object with @code{fitckernel}. ## ## @seealso{fitckernel, ClassificationLinear, ClassificationSVM} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationKernel} {property} BoxConstraint ## ## Box constraint of the support vector machine ## ## A positive scalar. It is the reciprocal of the product of ## @qcode{Lambda} and the number of observations, so setting either of ## the two in the constructor fixes the other, and giving both is an ## error. This property is read-only. ## ## @end deftp BoxConstraint = 1; ## -*- texinfo -*- ## @deftp {ClassificationKernel} {property} ClassNames ## ## Names of the two classes ## ## A column of the same type as the response supplied to the constructor, ## sorted or in the order given by the @qcode{'ClassNames'} option. The ## second of the two is the positive class, the one a positive score ## belongs to. This property is read-only. ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {ClassificationKernel} {property} Prior ## ## Prior probability of each class ## ## A numeric row vector with one element per class, in the order of ## @qcode{ClassNames} and summing to one. It defaults to the class ## frequencies of the training data. This property is read-only. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {ClassificationKernel} {property} Cost ## ## Cost of misclassifying an observation ## ## A square numeric matrix with one row and one column per class, whose ## @math{(i,j)} element is the cost of classifying an observation of ## class @math{i} into class @math{j}. It defaults to one everywhere ## except the diagonal, which is zero. This property is read-only, as it ## is in MATLAB; a cost matrix is given to the constructor instead. ## ## The costs are folded into the prior before the observations are ## weighted, so a class that is costlier to misclassify weighs more in ## the fit. They are read again by the @qcode{'mincost'} and ## @qcode{'classifcost'} losses. ## ## @end deftp Cost = []; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {ClassificationKernel} {property} ScoreTransform ## ## Transformation applied to the predicted scores ## ## A character vector naming a transformation, or the text of the ## function handle that was supplied. Assigning to it accepts either. ## It defaults to @qcode{'logit'} for a logistic learner and to ## @qcode{'none'} for a support vector machine. ## ## @end deftp ScoreTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationKernel} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors with one name per column of the ## training data, defaulting to @qcode{'x1'}, @qcode{'x2'} and so on. ## This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationKernel} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A row vector of column indices, empty when every predictor is ## numeric. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationKernel} {property} ResponseName ## ## Name of the response ## ## A character vector, defaulting to @qcode{'Y'}. This property is ## read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {ClassificationKernel} {property} ExpandedPredictorNames ## ## Names of the predictors as the fit saw them ## ## A cell array of character vectors. It equals @qcode{PredictorNames} ## unless categorical predictors were dummy coded, each then named once ## per level, as in @qcode{'x1 == 2'}. The expansion dimensions have no ## names. This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationKernel} {property} NumExpansionDimensions ## ## Number of dimensions of the expanded space ## ## A positive integer scalar. It defaults to ## @code{2 .^ ceil (min (log2 (@var{p}) + 5, 15))} for @var{p} ## predictors, so four predictors give 128 dimensions. More dimensions ## approximate the kernel more closely and cost proportionally more. ## This property is read-only. ## ## @end deftp NumExpansionDimensions = []; ## -*- texinfo -*- ## @deftp {ClassificationKernel} {property} FittedLoss ## ## Loss function the fit minimized ## ## @qcode{'hinge'} for a support vector machine and @qcode{'logit'} for a ## logistic regression. This property is read-only. ## ## @end deftp FittedLoss = 'hinge'; ## -*- texinfo -*- ## @deftp {ClassificationKernel} {property} Lambda ## ## Regularization strength ## ## A nonnegative scalar, the reciprocal of the product of ## @qcode{BoxConstraint} and the number of observations. This property ## is read-only. ## ## @end deftp Lambda = []; ## -*- texinfo -*- ## @deftp {ClassificationKernel} {property} ModelParameters ## ## Fitting options, as they were given ## ## A structure holding every parameter of the fit, with the ## @qcode{'auto'} values as they were given rather than as they were ## resolved. This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {ClassificationKernel} {property} Regularization ## ## Penalty on the coefficients ## ## Always @qcode{'ridge (L2)'}: a kernel model fits in the expanded ## space, where a lasso penalty has nothing to select. This property is ## read-only. ## ## @end deftp Regularization = 'ridge (L2)'; ## -*- texinfo -*- ## @deftp {ClassificationKernel} {property} KernelScale ## ## Scale of the Gaussian kernel ## ## A positive scalar dividing every predictor before the expansion, so a ## larger scale makes the kernel wider and the classifier smoother. This ## property is read-only. ## ## @end deftp KernelScale = 1; ## -*- texinfo -*- ## @deftp {ClassificationKernel} {property} Learner ## ## Linear model fitted in the expanded space ## ## Either @qcode{'svm'} or @qcode{'logistic'}. This property is ## read-only. ## ## @end deftp Learner = 'svm'; ## -*- texinfo -*- ## @deftp {ClassificationKernel} {property} Mu ## ## Predictor means used to standardize ## ## A row vector with one element per predictor, or empty when the model ## was fitted without standardizing. This property is read-only. ## ## @end deftp Mu = []; ## -*- texinfo -*- ## @deftp {ClassificationKernel} {property} Sigma ## ## Predictor standard deviations used to standardize ## ## A row vector with one element per predictor, or empty when the model ## was fitted without standardizing. This property is read-only. ## ## @end deftp Sigma = []; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ## The callable behind ScoreTransform. STfun = @(s) s; ## The random basis and the coefficients fitted in the space it spans. ## MATLAB keeps all three out of sight, so they are hidden here too, but ## they are the model: without the basis the coefficients index nothing. Basis_ = []; Beta_ = []; Bias_ = []; ## Number of predictors and of observations the fit saw. Neither is a ## property of MATLAB's class, but predict needs the first to validate ## its input and resume needs the second to keep Lambda and BoxConstraint ## reciprocal. NumPredictors_ = []; Coding_ = []; NumObservations_ = []; ## What the fit reported, so that fitckernel can hand it back as its ## second output. FitInfo_ = []; endproperties methods (Access = public) ## -*- texinfo -*- ## @deftypefn {ClassificationKernel} {@var{obj} =} ClassificationKernel (@var{X}, @var{Y}) ## @deftypefnx {ClassificationKernel} {@var{obj} =} ClassificationKernel (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationKernel} {@var{obj} =} ClassificationKernel (@var{Tbl}, @var{formula}) ## @deftypefnx {ClassificationKernel} {@var{obj} =} ClassificationKernel (@var{Tbl}, @var{Y}) ## @deftypefnx {ClassificationKernel} {@var{obj} =} ClassificationKernel (@dots{}, @var{name}, @var{value}) ## ## Fit a Gaussian kernel binary classifier. ## ## @code{@var{obj} = ClassificationKernel (@var{X}, @var{Y})} fits a ## support vector machine in a randomized Gaussian kernel space to the ## @math{NxP} predictor matrix @var{X} and the @math{Nx1} response ## @var{Y}, which must name exactly two classes. ## ## @code{@var{obj} = ClassificationKernel (@dots{}, @var{name}, ## @var{value})} takes the following @qcode{Name-Value} pairs. ## ## @multitable @columnfractions 0.28 0.72 ## @headitem Name @tab Value ## ## @item @qcode{'Learner'} @tab @qcode{'svm'}, the default, or ## @qcode{'logistic'}. ## ## @item @qcode{'NumExpansionDimensions'} @tab @qcode{'auto'}, the ## default, or a positive integer. ## ## @item @qcode{'KernelScale'} @tab @qcode{1} by default, a positive ## scalar, or @qcode{'auto'}, which takes the median distance between the ## observations. ## ## @item @qcode{'Lambda'} @tab @qcode{'auto'}, the default, which is the ## reciprocal of the number of observations, or a nonnegative scalar. It ## cannot be given beside @qcode{'BoxConstraint'}. ## ## @item @qcode{'BoxConstraint'} @tab A positive scalar, @qcode{1} by ## default. It applies to a support vector machine alone. ## ## @item @qcode{'Standardize'} @tab Whether to centre and scale the ## predictors, false by default. ## ## @item @qcode{'BetaTolerance'} @tab Relative tolerance on the ## coefficients, @qcode{1e-4} by default. ## ## @item @qcode{'GradientTolerance'} @tab Absolute tolerance on the ## gradient's infinity norm, @qcode{1e-6} by default. ## ## @item @qcode{'IterationLimit'} @tab Largest number of iterations, ## @qcode{1000} by default. ## ## @item @qcode{'HessianHistorySize'} @tab Number of curvature pairs the ## solver keeps, @qcode{15} by default. ## ## @item @qcode{'BlockSize'} @tab Memory the expansion may occupy, in ## megabytes, @qcode{4e3} by default. ## ## @item @qcode{'ClassNames'} @tab The classes to keep, given in the type ## of @var{Y}, in the order the model keeps them; by default they are ## sorted. ## ## @item @qcode{'Cost'} @tab A square misclassification cost matrix. ## Or a structure with fields @qcode{ClassNames} and ## @qcode{ClassificationCosts}, which names its own order of the classes and ## is permuted to the model's. ## ## @item @qcode{'Prior'} @tab @qcode{'empirical'}, the default, ## @qcode{'uniform'}, a vector of probabilities, or a structure with ## @qcode{ClassNames} and @qcode{ClassProbs} fields. ## A class whose prior is 0, or whose misclassification costs are all 0, is ## removed from the data before fitting, with a warning, as in MATLAB. ## ## @item @qcode{'ScoreTransform'} @tab A transformation applied to the ## scores, named or given as a function handle. ## ## @item @qcode{'Weights'} @tab One nonnegative weight per observation, as a ## single or double vector. Every computation runs in double. ## ## @item @qcode{'PredictorNames'} @tab One name per predictor. ## ## @item @qcode{'ResponseName'} @tab A name for the response. ## ## @item @qcode{'CategoricalPredictors'} @tab The categorical predictors, ## as indices, as a logical vector with one element per predictor, or as ## @qcode{'all'}. Each is dummy coded in its place, one column of zeros ## and ones per distinct value it takes in the training data, named as in ## @qcode{'x1 == 2'}, and the coded columns are not standardized. A row ## holding a value the training data did not is scored @code{NaN} and ## labelled with the class of largest prior, as a row missing a predictor ## is. ## A predictor may be named rather than indexed, as a character matrix of ## one padded name per row, a string array or a cellstr; a name must match ## an entry of @qcode{'PredictorNames'} exactly, its case included. ## @end multitable ## ## The fit is always by limited-memory BFGS, the only solver MATLAB ## offers a kernel model, and always under a ridge penalty. ## ## @seealso{fitckernel, ClassificationLinear} ## @end deftypefn function this = ClassificationKernel (X, Y, varargin) if (nargin < 2) error ("ClassificationKernel: too few input arguments."); endif ## A table names its own predictors and says which hold levels [this, X, Y, varargin] = resolveTable (this, 'ClassificationKernel', ... X, Y, varargin); if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationKernel: optional arguments must", ... " be given in Name-Value pairs.")); endif ## Parse optional paired arguments optNames = {'Learner', 'NumExpansionDimensions', 'KernelScale', ... 'Lambda', 'BoxConstraint', 'Standardize', ... 'BetaTolerance', 'GradientTolerance', 'IterationLimit', ... 'HessianHistorySize', 'BlockSize', 'Verbose', ... 'ClassNames', 'Cost', 'Prior', 'ScoreTransform', ... 'Weights', 'PredictorNames', 'ResponseName', ... 'CategoricalPredictors'}; ## An empty default stands for one resolved once the data are known: ## 'Lambda' is 'auto', 1/n, and 'BoxConstraint' is 1, each empty so ## that giving it is told apart from leaving it out; 'ClassNames', ## 'Cost', 'Prior' and 'Weights' come from the response as every ## class, a zero-one cost, the empirical prior and uniform weights; ## 'ScoreTransform' is 'logit' for a logistic learner and 'none' ## otherwise; 'PredictorNames' are x1, x2, ... dfValues = {'svm', 'auto', 1, [], [], false, 1e-4, 1e-6, 1000, 15, ... 4e3, 0, [], [], [], [], [], {}, 'Y', []}; [Learner, NumDimsIn, KernelScaleIn, LambdaIn, BoxConstraint, ... Standardize, BetaTolerance, GradientTolerance, IterationLimit, ... HessianHistorySize, BlockSize, Verbose, ClassNames, CostIn, Prior, ... ScoreTransform, Weights, PredictorNames, ResponseName, ... CategoricalPredictors, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (ischar (Learner) && any (strcmpi (Learner, {'svm', 'logistic'})))) error (strcat ("ClassificationKernel: 'Learner' must be either", ... " 'svm' or 'logistic'.")); endif Learner = lower (Learner); if (! ((ischar (NumDimsIn) && strcmpi (NumDimsIn, 'auto')) || (isnumeric (NumDimsIn) && isscalar (NumDimsIn) && isreal (NumDimsIn) && NumDimsIn > 0 && fix (NumDimsIn) == NumDimsIn))) error (strcat ("ClassificationKernel: 'NumExpansionDimensions'", ... " must be 'auto' or a positive integer scalar.")); endif if (! ((ischar (KernelScaleIn) && strcmpi (KernelScaleIn, 'auto')) || (isnumeric (KernelScaleIn) && isscalar (KernelScaleIn) && isreal (KernelScaleIn) && KernelScaleIn > 0))) error (strcat ("ClassificationKernel: 'KernelScale' must be 'auto'", ... " or a positive scalar.")); endif if (! isempty (LambdaIn) && ! ((ischar (LambdaIn) && strcmpi (LambdaIn, 'auto')) || (isnumeric (LambdaIn) && isscalar (LambdaIn) && isreal (LambdaIn) && LambdaIn >= 0 && isfinite (LambdaIn)))) error (strcat ("ClassificationKernel: 'Lambda' must be 'auto' or a", ... " nonnegative finite scalar.")); endif if (! isempty (BoxConstraint) && ! (isnumeric (BoxConstraint) && isscalar (BoxConstraint) && isreal (BoxConstraint) && BoxConstraint > 0 && isfinite (BoxConstraint))) error (strcat ("ClassificationKernel: 'BoxConstraint' must be a", ... " positive finite scalar.")); endif if (! (islogical (Standardize) || (isnumeric (Standardize) && isscalar (Standardize) && any (Standardize == [0, 1])))) error (strcat ("ClassificationKernel: 'Standardize' must be either", ... " true or false.")); endif Standardize = logical (Standardize); if (! (isnumeric (BetaTolerance) && isscalar (BetaTolerance) && isreal (BetaTolerance) && BetaTolerance >= 0)) error (strcat ("ClassificationKernel: 'BetaTolerance' must be a", ... " nonnegative scalar.")); endif if (! (isnumeric (GradientTolerance) && isscalar (GradientTolerance) && isreal (GradientTolerance) && GradientTolerance >= 0)) error (strcat ("ClassificationKernel: 'GradientTolerance' must be", ... " a nonnegative scalar.")); endif if (! (isnumeric (IterationLimit) && isscalar (IterationLimit) && isreal (IterationLimit) && IterationLimit > 0 && fix (IterationLimit) == IterationLimit)) error (strcat ("ClassificationKernel: 'IterationLimit' must be a", ... " positive integer scalar.")); endif if (! (isnumeric (HessianHistorySize) && isscalar (HessianHistorySize) && isreal (HessianHistorySize) && HessianHistorySize > 0 && fix (HessianHistorySize) == HessianHistorySize)) error (strcat ("ClassificationKernel: 'HessianHistorySize' must be", ... " a positive integer scalar.")); endif if (! (isnumeric (BlockSize) && isscalar (BlockSize) && isreal (BlockSize) && BlockSize > 0)) error ("ClassificationKernel: 'BlockSize' must be a positive scalar."); endif if (! (isnumeric (Verbose) && isscalar (Verbose) && isreal (Verbose) && any (Verbose == [0, 1]))) error ("ClassificationKernel: 'Verbose' must be 0 or 1."); endif if (! (iscellstr (ClassNames) || isnumeric (ClassNames) || islogical (ClassNames) || ischar (ClassNames) || isa (ClassNames, 'categorical') || isa (ClassNames, 'string'))) error (strcat ("ClassificationKernel: 'ClassNames' must be a", ... " categorical array, a character array, a string", ... " array, a logical vector, a numeric vector, or a", ... " cell array of character vectors.")); endif ## A structure names its own order, and is permuted to the model's if (! (isstruct (CostIn) || (isnumeric (CostIn) && isreal (CostIn) && ismatrix (CostIn) && ndims (CostIn) == 2 && rows (CostIn) == columns (CostIn)))) error (strcat ("ClassificationKernel: 'Cost' must be a square", ... " numeric matrix or a structure.")); endif if (! isempty (Prior) && ! ((ischar (Prior) && any (strcmpi (Prior, {'empirical', ... 'uniform'}))) || (isnumeric (Prior) && isreal (Prior) && isvector (Prior) && all (Prior >= 0)) || (isstruct (Prior) && isscalar (Prior)))) error (strcat ("ClassificationKernel: 'Prior' must be 'empirical',", ... " 'uniform', a vector of nonnegative values, or a", ... " structure.")); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("ClassificationKernel: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isreal (Weights) && isvector (Weights) && all (Weights >= 0))) error (strcat ("ClassificationKernel: 'Weights' must be a vector", ... " of nonnegative values.")); endif if (! isempty (PredictorNames) && ! (iscellstr (PredictorNames) && isvector (PredictorNames))) error (strcat ("ClassificationKernel: 'PredictorNames' must be a", ... " cell array of character vectors.")); endif if (! (ischar (ResponseName) && isrow (ResponseName))) error (strcat ("ClassificationKernel: 'ResponseName' must be a", ... " character vector.")); endif if (! isempty (args)) error ("ClassificationKernel: invalid optional paired argument."); endif ## Lambda and the box constraint are reciprocal, so naming both ## overdetermines the fit rather than describing it twice. if (! isempty (LambdaIn) && ! isempty (BoxConstraint)) error (strcat ("ClassificationKernel: 'Lambda' and", ... " 'BoxConstraint' cannot be given together, one", ... " being the reciprocal of the other times the", ... " number of observations.")); endif if (! isempty (BoxConstraint) && strcmp (Learner, 'logistic')) error (strcat ("ClassificationKernel: 'BoxConstraint' applies to", ... " a support vector machine only.")); endif ## Validate the data, resolve the classes, the prior, the cost and ## the observation weights. The four linear and kernel classifiers ## share that opening, so it lives in one place. F = classFrame (X, Y, ClassNames, Prior, CostIn, Weights, ... 'ClassificationKernel'); X = F.X; y = F.y; W = F.W; n = F.n; p = F.p; this.ClassNames = F.ClassNames; this.Prior = F.Prior; this.Cost = F.Cost; ## Dummy code the categorical predictors on the rows kept; the fit ## sees the coded columns, and new data is coded the same way. if (isempty (PredictorNames)) PredictorNames = arrayfun (@(k) sprintf ("x%d", k), 1:p, ... 'UniformOutput', false); elseif (numel (PredictorNames) != p) error (strcat ("ClassificationKernel: 'PredictorNames' must have", ... " one name per predictor.")); endif [Coding, errmsg] = dummyCoding (X, CategoricalPredictors, ... PredictorNames); if (! isempty (errmsg)) error ("ClassificationKernel: %s", errmsg); endif X = dummyCoding (X, Coding); p = columns (X); ## Standardize before anything is measured off the predictors, so the ## kernel scale and the expansion both see the same data predict will. if (Standardize) this.Mu = mean (X, 1); this.Sigma = std (X, 0, 1); this.Sigma(this.Sigma == 0) = 1; ## A level's column is left as it is, as in MATLAB R2024a. this.Mu(Coding.Dummy) = 0; this.Sigma(Coding.Dummy) = 1; X = (X - this.Mu) ./ this.Sigma; endif ## Resolve the expansion if (ischar (NumDimsIn)) m = 2 .^ ceil (min (log2 (p) + 5, 15)); else m = NumDimsIn; endif if (ischar (KernelScaleIn)) sigma = autoKernelScale (X); else sigma = KernelScaleIn; endif ## Resolve Lambda and the box constraint from whichever was given if (! isempty (BoxConstraint)) Lambda = 1 / (n * BoxConstraint); elseif (! isempty (LambdaIn) && ! ischar (LambdaIn)) Lambda = LambdaIn; BoxConstraint = 1 / (n * Lambda); else Lambda = 1 / n; BoxConstraint = 1 / (n * Lambda); endif if (isempty (LambdaIn)) LambdaIn = 'auto'; endif ## Draw the basis, map the data through it, and fit a linear model ## there. The basis is what makes the fit nonlinear; everything after ## it is the linear machinery. basis = kernelBasis (p, m, sigma); T = kernelExpand (X, basis); P = struct (); P.Learner = Learner; P.LossFunction = 'hinge'; if (strcmp (Learner, 'logistic')) P.LossFunction = 'logit'; endif P.Epsilon = []; P.Regularization = 'ridge'; P.Lambda = Lambda; P.Solver = 'lbfgs'; P.FitBias = true; P.PostFitBias = false; P.BetaTolerance = BetaTolerance; P.GradientTolerance = GradientTolerance; P.DeltaGradientTolerance = []; P.IterationLimit = IterationLimit; P.PassLimit = 1; P.BatchSize = 10; P.BatchLimit = []; P.LearnRate = 1; P.OptimizeLearnRate = true; P.TruncationPeriod = 10; P.NumCheckConvergence = 5; P.HessianHistorySize = HessianHistorySize; P.InitialBeta = zeros (m, 1); P.InitialBias = 0; [Beta, Bias, S] = linearSolve (T, y, W, P); ## Fill in the model this.BoxConstraint = BoxConstraint; this.PredictorNames = PredictorNames; this.ExpandedPredictorNames = PredictorNames; if (! isempty (Coding.Index)) this.ExpandedPredictorNames = Coding.ExpandedNames; this.CategoricalPredictors = Coding.Index; this.Coding_ = Coding; endif this.ResponseName = ResponseName; this.NumExpansionDimensions = m; this.FittedLoss = P.LossFunction; this.Lambda = Lambda; this.KernelScale = sigma; this.Learner = Learner; this.Basis_ = basis; this.Beta_ = Beta; this.Bias_ = Bias; this.NumPredictors_ = Coding.NumPredictors; this.NumObservations_ = n; if (isempty (ScoreTransform)) if (strcmp (Learner, 'logistic')) ScoreTransform = 'logit'; else ScoreTransform = 'none'; endif endif this.ScoreTransform = ScoreTransform; this.ModelParameters = struct ('BetaTolerance', BetaTolerance, ... 'BlockSize', BlockSize, 'BoxConstraint', BoxConstraint, ... 'Epsilon', 'auto', 'NumExpansionDimensions', NumDimsIn, ... 'GradientTolerance', GradientTolerance, ... 'HessianHistorySize', HessianHistorySize, ... 'IterationLimit', IterationLimit, 'KernelScale', KernelScaleIn, ... 'Lambda', LambdaIn, 'Learner', Learner, ... 'LossFunction', P.LossFunction, 'Stream', [], ... 'VerbosityLevel', Verbose, 'StandardizeData', Standardize, ... 'Version', 1, 'Method', 'Kernel', 'Type', 'classification'); this.FitInfo_ = kernelFitInfo (S, P.LossFunction, Lambda, ... BetaTolerance, GradientTolerance); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {ClassificationKernel} {@var{labels} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {ClassificationKernel} {[@var{labels}, @var{scores}] =} predict (@var{obj}, @var{XC}) ## ## Classify new observations. ## ## @code{@var{labels} = predict (@var{obj}, @var{XC})} maps each row of ## @var{XC} through the model's own random basis and returns the class of ## largest score. ## ## @code{[@var{labels}, @var{scores}] = predict (@var{obj}, @var{XC})} ## also returns the @math{Nx2} scores, whose columns follow ## @qcode{ClassNames}, after @qcode{ScoreTransform} has been applied. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @end deftypefn function [labels, scores] = predict (this, XC) if (nargin < 2) error ("ClassificationKernel.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'ClassificationKernel.predict', XC); if (isempty (XC)) error ("ClassificationKernel.predict: XC is empty."); endif if (! (isnumeric (XC) && isreal (XC) && ismatrix (XC))) error ("ClassificationKernel.predict: invalid values in XC."); endif if (columns (XC) != this.NumPredictors_) error (strcat ("ClassificationKernel.predict: XC must have the", ... " same number of predictors as the trained model.")); endif f = rawScore (this, XC); ## A row missing a predictor has no score, and takes the class of ## largest prior, as MATLAB R2024a labels it whatever the cost. idx = 1 + (f > 0); [~, top] = max (this.Prior); idx(isnan (f)) = top; labels = labelsFromIndex (this.ClassNames, idx); if (nargout > 1) scores = this.STfun ([-f, f]); endif endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationKernel} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationKernel} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationKernel} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margin of each observation. ## ## @code{@var{m} = margin (@var{obj}, @var{X}, @var{Y})} returns the ## score of the true class less the score of the other one. A positive ## margin is a correct classification. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @end deftypefn function m = margin (this, X, Y) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationKernel.margin: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y] = tableResponse (this, 'margin', X, Y, {}, nargin > 2); [gY, errmsg] = labelIndices (this.ClassNames, Y); if (! isempty (errmsg)) error ("ClassificationKernel.margin: %s", errmsg); endif if (rows (X) != numel (gY)) error (strcat ("ClassificationKernel.margin: number of rows in X", ... " and Y must be equal.")); endif [~, s] = predict (this, X); n = rows (s); strue = s(sub2ind (size (s), (1:n)', gY)); sother = s(sub2ind (size (s), (1:n)', 3 - gY)); m = strue - sother; endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationKernel} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationKernel} {@var{e} =} edge (@dots{}, @qcode{'Weights'}, @var{W}) ## @deftypefnx {ClassificationKernel} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationKernel} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Weighted mean of the classification margins. ## ## The weights are normalized within each class to that class's prior ## before they are applied. ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @end deftypefn function e = edge (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationKernel.edge: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'edge', X, Y, varargin, ... nargin > 2); W = edgeWeights (varargin, Y, this.ClassNames, this.Prior, ... 'ClassificationKernel', 'edge'); m = margin (this, X, Y); e = sum (W .* m(:)) / sum (W); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationKernel} {@var{l} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationKernel} {@var{l} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationKernel} {@var{l} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {ClassificationKernel} {@var{l} =} loss (@dots{}, @var{name}, @var{value}) ## ## Classification loss on new data. ## ## @code{@var{l} = loss (@var{obj}, @var{X}, @var{Y})} returns the ## misclassification rate. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{l} = loss (@dots{}, @var{name}, @var{value})} takes ## @qcode{'LossFun'}, one of @qcode{'binodeviance'}, ## @qcode{'classifcost'}, @qcode{'classiferror'}, @qcode{'exponential'}, ## @qcode{'hinge'}, @qcode{'logit'}, @qcode{'mincost'} and ## @qcode{'quadratic'}, and @qcode{'Weights'}. ## ## @end deftypefn function l = loss (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationKernel.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationKernel.loss: optional arguments", ... " must be given in Name-Value pairs.")); endif ## Parse optional paired arguments optNames = {'LossFun', 'Weights'}; ## An empty 'Weights' stands for uniform weights dfValues = {'classiferror', []}; [LossFun, Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments valid = {'binodeviance', 'classifcost', 'classiferror', ... 'exponential', 'hinge', 'logit', 'mincost', ... 'quadratic'}; if (! (ischar (LossFun) && any (strcmpi (LossFun, valid)))) error (strcat ("ClassificationKernel.loss: 'LossFun' must", ... " be 'binodeviance', 'classifcost',", ... " 'classiferror', 'exponential', 'hinge',", ... " 'logit', 'mincost', or 'quadratic'.")); endif LossFun = lower (LossFun); errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("ClassificationKernel.loss: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isreal (Weights) && isvector (Weights) && all (Weights >= 0))) error (strcat ("ClassificationKernel.loss: 'Weights' must", ... " be a vector of nonnegative values.")); endif if (! isempty (args)) error ("ClassificationKernel.loss: invalid optional paired argument."); endif [gY, errmsg] = labelIndices (this.ClassNames, Y); if (! isempty (errmsg)) error ("ClassificationKernel.loss: %s", errmsg); endif if (rows (X) != numel (gY)) error (strcat ("ClassificationKernel.loss: number of rows in X", ... " and Y must be equal.")); endif if (isempty (Weights)) w = ones (numel (gY), 1); else w = double (Weights(:)); if (numel (w) != numel (gY)) error (strcat ("ClassificationKernel.loss: 'Weights' must have", ... " one element per observation.")); endif endif w = w / sum (w); [~, s] = predict (this, X); l = classificationLoss (LossFun, s, gY, w, this.Cost); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationKernel} {@var{obj} =} resume (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationKernel} {@var{obj} =} resume (@dots{}, @var{name}, @var{value}) ## @deftypefnx {ClassificationKernel} {@var{obj} =} resume (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## ## Continue fitting a kernel classifier. ## ## @code{@var{obj} = resume (@var{obj}, @var{X}, @var{Y})} restarts the ## optimization from the coefficients the model already carries, through ## the basis it already holds, and returns the model it reaches. It ## takes @qcode{'BetaTolerance'}, @qcode{'GradientTolerance'} and ## @qcode{'IterationLimit'}, each defaulting to what the model was ## fitted with, and @qcode{'Weights'}. ## ## @var{X} and @var{Y} must be the data the model was fitted to; the ## object keeps no copy of them, which is what makes it small. Neither ## does it keep the observation weights, so a model fitted with ## @qcode{'Weights'} must be given them again here or it will resume ## against uniform ones. MATLAB behaves the same way: measured on ## R2024a, resuming a weighted fit without passing the weights back ## reaches the objective of the @emph{unweighted} fit. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## The response is then named by @var{ResponseVarName}, a variable of the ## table, or given beside the table as @var{Y}; unlike ## @code{ClassificationKernel.loss}, it is never taken from the table ## unasked. ## ## @end deftypefn function this = resume (this, X, Y, varargin) if (nargin < 3) error ("ClassificationKernel.resume: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationKernel.resume: optional arguments", ... " must be given in Name-Value pairs.")); endif ## Parse optional paired arguments optNames = {'Weights', 'BetaTolerance', 'GradientTolerance', ... 'IterationLimit'}; dfValues = {[], this.ModelParameters.BetaTolerance, ... this.ModelParameters.GradientTolerance, ... this.ModelParameters.IterationLimit}; [Weights, BetaTolerance, GradientTolerance, IterationLimit, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("ClassificationKernel.resume: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isreal (Weights) && isvector (Weights) && all (Weights >= 0))) error (strcat ("ClassificationKernel.resume: 'Weights'", ... " must be a vector of nonnegative values.")); endif if (! (isnumeric (BetaTolerance) && isscalar (BetaTolerance) && isreal (BetaTolerance) && BetaTolerance >= 0)) error (strcat ("ClassificationKernel.resume: 'BetaTolerance'", ... " must be a nonnegative scalar.")); endif if (! (isnumeric (GradientTolerance) && isscalar (GradientTolerance) && isreal (GradientTolerance) && GradientTolerance >= 0)) error (strcat ("ClassificationKernel.resume: 'GradientTolerance'", ... " must be a nonnegative scalar.")); endif if (! (isnumeric (IterationLimit) && isscalar (IterationLimit) && isreal (IterationLimit) && IterationLimit > 0 && fix (IterationLimit) == IterationLimit)) error (strcat ("ClassificationKernel.resume: 'IterationLimit'", ... " must be a positive integer scalar.")); endif ## Handle table input if (istable (X)) [X, Y] = tableResponse (this, 'resume', X, Y, {}, true); endif if (! isempty (args)) error (strcat ("ClassificationKernel.resume: invalid optional", ... " paired argument.")); endif [T, y, W] = resumeData (this, X, Y, Weights, 'resume'); P = resumeOptions (this, BetaTolerance, GradientTolerance, ... IterationLimit); P.InitialBeta = this.Beta_; P.InitialBias = this.Bias_; [Beta, Bias, S] = linearSolve (T, y, W, P); this.Beta_ = Beta; this.Bias_ = Bias; this.ModelParameters.BetaTolerance = BetaTolerance; this.ModelParameters.GradientTolerance = GradientTolerance; this.ModelParameters.IterationLimit = IterationLimit; this.FitInfo_ = kernelFitInfo (S, this.FittedLoss, this.Lambda, ... BetaTolerance, GradientTolerance); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationKernel} {} savemodel (@var{obj}, @var{filename}) ## ## Save a kernel classifier to a file. ## ## @code{savemodel (@var{obj}, @var{filename})} saves the model ## @var{obj} into @var{filename} in a form @code{loadmodel} can read ## back, the random basis included. ## ## @end deftypefn function savemodel (obj, fname) classdef_name = 'ClassificationKernel'; BoxConstraint = obj.BoxConstraint; ClassNames = encodeLabels (obj.ClassNames); Prior = obj.Prior; Cost = obj.Cost; ScoreTransform = obj.ScoreTransform; PredictorNames = obj.PredictorNames; CategoricalPredictors = obj.CategoricalPredictors; ResponseName = obj.ResponseName; ExpandedPredictorNames = obj.ExpandedPredictorNames; NumExpansionDimensions = obj.NumExpansionDimensions; FittedLoss = obj.FittedLoss; Lambda = obj.Lambda; ModelParameters = obj.ModelParameters; Regularization = obj.Regularization; KernelScale = obj.KernelScale; Learner = obj.Learner; Mu = obj.Mu; Sigma = obj.Sigma; Basis_ = obj.Basis_; Beta_ = obj.Beta_; Bias_ = obj.Bias_; NumPredictors_ = obj.NumPredictors_; NumObservations_ = obj.NumObservations_; Coding_ = obj.Coding_; save ('-binary', fname, 'classdef_name', 'BoxConstraint', ... 'ClassNames', 'Prior', 'Cost', 'ScoreTransform', ... 'PredictorNames', 'CategoricalPredictors', 'ResponseName', ... 'ExpandedPredictorNames', 'NumExpansionDimensions', ... 'FittedLoss', 'Lambda', 'ModelParameters', 'Regularization', ... 'KernelScale', 'Learner', 'Mu', 'Sigma', 'Basis_', 'Beta_', ... 'Bias_', 'NumPredictors_', 'NumObservations_', 'Coding_'); endfunction endmethods methods (Access = public, Hidden) function disp (this) printf ("\n ClassificationKernel\n\n"); printf ("%+26s: '%s'\n", 'ResponseName', this.ResponseName); printf ("%+26s: %s\n", 'ClassNames', ... classNameListing (this.ClassNames)); printf ("%+26s: '%s'\n", 'Learner', this.Learner); printf ("%+26s: %d\n", 'NumExpansionDimensions', ... this.NumExpansionDimensions); printf ("%+26s: %g\n", 'KernelScale', this.KernelScale); printf ("%+26s: %g\n", 'Lambda', this.Lambda); printf ("%+26s: %g\n", 'BoxConstraint', this.BoxConstraint); printf ("\n"); endfunction ## What the fit reported, which fitckernel returns as its second output. function S = fitInfo_ (this) S = this.FitInfo_; endfunction ## Custom setter, so that assigning a name or a handle updates both the ## text the property reports and the callable predict uses. function this = set.ScoreTransform (this, val) [this.STfun, this.ScoreTransform] = ... parseScoreTransform (val, 'ClassificationKernel'); endfunction endmethods methods (Access = private) ## The raw model value of each row of XC, standardized and expanded ## through the model's own basis first. function f = rawScore (this, XC) if (! isempty (this.Coding_)) XC = dummyCoding (XC, this.Coding_); endif if (! isempty (this.Mu)) XC = (XC - this.Mu) ./ this.Sigma; endif f = kernelExpand (XC, this.Basis_) * this.Beta_ + this.Bias_; endfunction ## The expanded predictors, the signed response and the weights of a ## resume call, validated the way the constructor validated them. function [T, y, W] = resumeData (this, X, Y, Weights, caller) if (! (isnumeric (X) && isreal (X) && ismatrix (X))) error ("ClassificationKernel.%s: invalid values in X.", caller); endif if (columns (X) != this.NumPredictors_) error (strcat ("ClassificationKernel.%s: X must have the same", ... " number of predictors as the trained model."), ... caller); endif [gY, errmsg] = labelIndices (this.ClassNames, Y); if (! isempty (errmsg)) error ("ClassificationKernel.%s: %s", caller, errmsg); endif if (rows (X) != numel (gY)) error (strcat ("ClassificationKernel.%s: number of rows in X and", ... " Y must be equal."), caller); endif y = -ones (numel (gY), 1); y(gY == 2) = 1; if (isempty (Weights)) Weights = ones (numel (gY), 1); else Weights = double (Weights(:)); if (numel (Weights) != numel (gY)) error (strcat ("ClassificationKernel.%s: 'Weights' must have", ... " one element per observation."), caller); endif endif adjPrior = this.Prior .* sum (this.Cost, 2)'; adjPrior = adjPrior / sum (adjPrior); W = zeros (numel (gY), 1); for k = 1:numel (this.Prior) idx = (gY == k); tot = sum (Weights(idx)); if (tot > 0) W(idx) = Weights(idx) / tot * adjPrior(k); endif endfor W = W / sum (W); if (! isempty (this.Coding_)) X = dummyCoding (X, this.Coding_); endif if (! isempty (this.Mu)) X = (X - this.Mu) ./ this.Sigma; endif T = kernelExpand (X, this.Basis_); endfunction ## The option structure linearSolve takes, filled from the model. function P = resumeOptions (this, BetaTol, GradTol, IterLimit) P = struct (); P.Learner = this.Learner; P.LossFunction = this.FittedLoss; P.Epsilon = []; P.Regularization = 'ridge'; P.Lambda = this.Lambda; P.Solver = 'lbfgs'; P.FitBias = true; P.PostFitBias = false; P.BetaTolerance = BetaTol; P.GradientTolerance = GradTol; P.DeltaGradientTolerance = []; P.IterationLimit = IterLimit; P.PassLimit = 1; P.BatchSize = 10; P.BatchLimit = []; P.LearnRate = 1; P.OptimizeLearnRate = true; P.TruncationPeriod = 10; P.NumCheckConvergence = 5; P.HessianHistorySize = this.ModelParameters.HessianHistorySize; endfunction endmethods methods (Static, Hidden) function mdl = load_model (filename, data) data = decodeLabels (data); mdl = ClassificationKernel (zeros (2, 1), [0; 1]); fields = fieldnames (data); for k = 1:numel (fields) mdl.(fields{k}) = data.(fields{k}); endfor endfunction endmethods endclassdef %!demo %! ## Separate the two overlapping iris species through a randomized %! ## Gaussian kernel, and read the model the fit produced. %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mdl = ClassificationKernel (X, Y) %! predict (Mdl, X([1, 51],:)) %!demo %! ## The box constraint and the regularization strength are two names for %! ## the same quantity: setting either fixes the other. %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mdl = ClassificationKernel (X, Y, 'BoxConstraint', 4); %! Mdl.BoxConstraint %! Mdl.Lambda %!test %! ## The model reports the surface MATLAB reports %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mdl = ClassificationKernel (X, Y); %! assert_equal (class (Mdl), 'ClassificationKernel'); %! assert_equal (Mdl.ClassNames, {'versicolor'; 'virginica'}); %! assert_equal (Mdl.Learner, 'svm'); %! assert_equal (Mdl.FittedLoss, 'hinge'); %! assert_equal (Mdl.Regularization, 'ridge (L2)'); %! assert_equal (Mdl.ScoreTransform, 'none'); %! assert_equal (Mdl.KernelScale, 1); %! assert_equal (Mdl.BoxConstraint, 1); %! assert_equal (Mdl.Lambda, 0.01); %! assert_equal (Mdl.NumExpansionDimensions, 128); %! assert_equal (Mdl.Mu, []); %! assert_equal (Mdl.Sigma, []); %! assert_equal (Mdl.PredictorNames, {'x1', 'x2', 'x3', 'x4'}); %!test %! ## The properties are the ones MATLAB lists, in its order %! load fisheriris %! Mdl = ClassificationKernel (meas(51:end,:), species(51:end)); %! assert_equal (sort (properties (Mdl)), ... %! sort ({'BoxConstraint'; 'ClassNames'; 'Prior'; 'Cost'; ... %! 'ScoreTransform'; 'PredictorNames'; ... %! 'CategoricalPredictors'; 'ResponseName'; ... %! 'ExpandedPredictorNames'; 'NumExpansionDimensions'; ... %! 'FittedLoss'; 'Lambda'; 'ModelParameters'; ... %! 'Regularization'; 'KernelScale'; 'Learner'; 'Mu'; ... %! 'Sigma'})); %!test %! ## The default expansion is MATLAB's, two to the power of five more than %! ## the base two logarithm of the predictors, capped at fifteen %! X = randn (40, 2); %! Y = [ones(20, 1); 2 * ones(20, 1)]; %! assert_equal (ClassificationKernel (X, Y).NumExpansionDimensions, 64); %! assert_equal (ClassificationKernel (randn (40, 32), Y) ... %! .NumExpansionDimensions, 1024); %!test %! ## Lambda and the box constraint are reciprocal through the number of %! ## observations, and either one may be the one that is given %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mb = ClassificationKernel (X, Y, 'BoxConstraint', 3); %! assert_equal (Mb.Lambda, 1 / 300, 1e-15); %! assert_equal (Mb.BoxConstraint, 3); %! Ml = ClassificationKernel (X, Y, 'Lambda', 0.05); %! assert_equal (Ml.Lambda, 0.05); %! assert_equal (Ml.BoxConstraint, 0.2, 1e-15); %!test %! ## Standardizing records the means and deviations MATLAB records %! load fisheriris %! Mdl = ClassificationKernel (meas(51:end,:), species(51:end), ... %! 'Standardize', true); %! assert_equal (Mdl.Mu, [6.262, 2.872, 4.906, 1.676], 1e-12); %! assert_equal (Mdl.Sigma, [0.662834440074967, 0.332751006494695, ... %! 0.82557846264289, 0.424768504986284], 1e-12); %!test %! ## A logistic learner fits the deviance and reports posteriors %! load fisheriris %! X = meas(51:end,:); %! Mdl = ClassificationKernel (X, species(51:end), 'Learner', 'logistic'); %! assert_equal (Mdl.FittedLoss, 'logit'); %! assert_equal (Mdl.ScoreTransform, 'logit'); %! [~, score] = predict (Mdl, X(1:5,:)); %! assert_equal (sum (score, 2), ones (5, 1), 1e-12); %!test %! ## A support vector machine leaves the scores untransformed, so they are %! ## a value and its negative %! load fisheriris %! X = meas(51:end,:); %! Mdl = ClassificationKernel (X, species(51:end)); %! [~, score] = predict (Mdl, X(1:5,:)); %! assert_equal (score(:,1), -score(:,2), 1e-12); %!test %! ## The fit separates the two species it was given, whatever basis it drew %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mdl = ClassificationKernel (X, Y); %! assert_equal (loss (Mdl, X, Y) < 0.15, true); %! assert_equal (edge (Mdl, X, Y) > 0, true); %!test %! ## On a set holding one class the edge is still the mean margin %! load fisheriris %! X = meas(51:100,:); %! Y = species(51:100); %! Mdl = ClassificationKernel (meas(51:end,:), species(51:end)); %! assert_equal (edge (Mdl, X, Y), mean (margin (Mdl, X, Y)), 1e-14); %!test %! ## margin is the true class score less the other, and the labels follow %! ## the sign of the raw score %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mdl = ClassificationKernel (X, Y); %! m = margin (Mdl, X, Y); %! assert_equal (size (m), [100, 1]); %! assert_equal (mean (m > 0), 1 - loss (Mdl, X, Y), 1e-12); %!test %! ## Predicting through the model's own basis is what makes it a model at %! ## all: the same rows give the same scores every time it is asked %! load fisheriris %! X = meas(51:end,:); %! Mdl = ClassificationKernel (X, species(51:end)); %! [~, s1] = predict (Mdl, X(1:10,:)); %! [~, s2] = predict (Mdl, X(1:10,:)); %! assert_equal (s1, s2); %!test %! ## A wider kernel gives a smoother rule, so it fits the training data %! ## less closely than a narrow one does %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mnarrow = ClassificationKernel (X, Y, 'KernelScale', 0.5, ... %! 'Lambda', 1e-4); %! Mwide = ClassificationKernel (X, Y, 'KernelScale', 20, 'Lambda', 1e-4); %! assert_equal (loss (Mnarrow, X, Y) <= loss (Mwide, X, Y), true); %!test %! ## resume continues from the coefficients the model already holds, so it %! ## cannot leave the objective higher than it found it %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mdl = ClassificationKernel (X, Y, 'IterationLimit', 3); %! before = Mdl.FitInfo_.ObjectiveValue; %! Mdl = resume (Mdl, X, Y, 'IterationLimit', 500); %! assert_equal (class (Mdl), 'ClassificationKernel'); %! assert_equal (Mdl.FitInfo_.ObjectiveValue <= before, true); %! assert_equal (Mdl.ModelParameters.IterationLimit, 500); %!test %! ## The fit information is MATLAB's kernel structure, not its linear one %! load fisheriris %! Mdl = ClassificationKernel (meas(51:end,:), species(51:end)); %! F = Mdl.FitInfo_; %! assert_equal (fieldnames (F), {'Solver'; 'LossFunction'; 'Lambda'; ... %! 'BetaTolerance'; 'GradientTolerance'; ... %! 'ObjectiveValue'; 'GradientMagnitude'; ... %! 'RelativeChangeInBeta'; 'FitTime'; ... %! 'History'}); %! assert_equal (F.Solver, 'LBFGS-fast'); %! assert_equal (F.LossFunction, 'hinge'); %! assert_equal (F.Lambda, 0.01); %!test %! ## A cost matrix reaches the fit through the prior, as it does for the %! ## linear classifier %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mdl = ClassificationKernel (X, Y, 'Cost', [0, 4; 1, 0]); %! assert_equal (Mdl.Cost, [0, 4; 1, 0]); %! assert_equal (Mdl.Prior, [0.5, 0.5]); %!test %! ## A saved model reads back as the same model, the random basis included %! load fisheriris %! X = meas(51:end,:); %! Mdl = ClassificationKernel (X, species(51:end)); %! fname = tempname (); %! savemodel (Mdl, fname); %! Mnew = loadmodel (fname); %! delete (fname); %! assert_equal (class (Mnew), 'ClassificationKernel'); %! [~, s1] = predict (Mdl, X(1:5,:)); %! [~, s2] = predict (Mnew, X(1:5,:)); %! assert_equal (s1, s2); %!test %! ## The labels come back in the type the response was given in, a %! ## character matrix included %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mchar = ClassificationKernel (X, char (Y)); %! assert_equal (size (Mchar.ClassNames), [2, 10]); %! assert_equal (size (predict (Mchar, X(1:3,:))), [3, 10]); %! Mnum = ClassificationKernel (X, double (strcmp (Y, 'virginica'))); %! assert_equal (Mnum.ClassNames, [0; 1]); %! assert_equal (class (predict (Mnum, X(1:3,:))), 'double'); %!test %! ## A character matrix response names one class per row, and every method %! ## answers the same through it as through the equivalent cell array %! load fisheriris %! X = meas(51:end,:); %! Yc = species(51:end); %! Ym = char (Yc); %! rand ('seed', 11); randn ('seed', 11); %! Mc = ClassificationKernel (X, Yc); %! rand ('seed', 11); randn ('seed', 11); %! Mm = ClassificationKernel (X, Ym); %! assert_equal (cellstr (Mm.ClassNames), Mc.ClassNames); %! assert_equal (cellstr (predict (Mm, X)), predict (Mc, X)); %! assert_equal (margin (Mm, X, Ym), margin (Mc, X, Yc)); %! assert_equal (edge (Mm, X, Ym), edge (Mc, X, Yc)); %! assert_equal (loss (Mm, X, Ym), loss (Mc, X, Yc)); %!test %! ## 'ClassNames' selects a subset when it is given as a character matrix %! load fisheriris %! Mdl = ClassificationKernel (meas, char (species), 'ClassNames', ... %! char ({'versicolor', 'virginica'})); %! assert_equal (size (Mdl.ClassNames), [2, 10]); %! assert_equal (cellstr (Mdl.ClassNames), {'versicolor'; 'virginica'}); %!test %! ## resume keeps no observation weights, because the model keeps no data: %! ## a weighted fit resumed without them continues against uniform ones, %! ## and passing them back restores the weighted fit. MATLAB behaves the %! ## same way, measured on R2024a. %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! w = (1:100)'; %! rand ('seed', 4); randn ('seed', 4); %! Mdl = ClassificationKernel (X, Y, 'IterationLimit', 5, 'Weights', w); %! kept = resume (Mdl, X, Y, 'IterationLimit', 400, 'Weights', w); %! lost = resume (Mdl, X, Y, 'IterationLimit', 400); %! assert_equal (kept.FitInfo_.ObjectiveValue ... %! != lost.FitInfo_.ObjectiveValue, true); %! rand ('seed', 4); randn ('seed', 4); %! direct = ClassificationKernel (X, Y, 'IterationLimit', 405, 'Weights', w); %! assert_equal (kept.FitInfo_.ObjectiveValue, ... %! direct.FitInfo_.ObjectiveValue, 1e-3); %!test %! ## A character matrix response survives a round trip through savemodel %! ## and loadmodel, the random basis with it %! load fisheriris %! X = meas(51:end,:); %! Ym = char (species(51:end)); %! Mdl = ClassificationKernel (X, Ym); %! fname = tempname (); %! savemodel (Mdl, fname); %! Mnew = loadmodel (fname); %! delete (fname); %! assert_equal (Mnew.ClassNames, Mdl.ClassNames); %! assert_equal (predict (Mnew, X(1:5,:)), predict (Mdl, X(1:5,:))); %!test # A categorical 'ClassNames' is accepted %! load fisheriris %! y = categorical (species(51:150)); %! Mdl = ClassificationKernel (meas(51:150,:), y, 'ClassNames', ... %! categorical ({'versicolor'; 'virginica'})); %! assert_equal (class (Mdl.ClassNames), 'categorical'); %! assert_equal (numel (Mdl.ClassNames), 2); %!test # An unused category of a categorical response is not a class %! load fisheriris %! y = categorical (species); %! Mdl = ClassificationKernel (meas(51:150,:), y(51:150)); %! assert_equal (cellstr (Mdl.ClassNames), {'versicolor'; 'virginica'}); %!test # A row missing a predictor takes the class of largest prior %! X = [(1:10)', mod((1:10)', 3)]; %! y = [1; 1; 1; 1; 1; 1; 2; 2; 2; 2]; %! Mdl = ClassificationKernel (X, y); %! [label, score] = predict (Mdl, [NaN, 1]); %! assert_equal (label, 1); %! assert_equal (score, [NaN, NaN]); %! Mdl = ClassificationKernel (X, y, 'Prior', [0.3, 0.7]); %! assert_equal (predict (Mdl, [NaN, 1]), 2); ## Test input validation %!error ... %! ClassificationKernel (ones (5, 2)) %!error ... %! ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Learner') %!error ... %! ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Learner', ... %! 'tree') %!error ... %! ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'NumExpansionDimensions', 0) %!error ... %! ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'KernelScale', -1) %!error ... %! ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Lambda', -1) %!error ... %! ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'BoxConstraint', 0) %!error ... %! ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Lambda', ... %! 0.1, 'BoxConstraint', 2) %!error ... %! ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Learner', ... %! 'logistic', 'BoxConstraint', 2) %!error ... %! ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'Standardize', 'yes') %!error ... %! ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'IterationLimit', -5) %!error ... %! ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Verbose', 2) %!error ... %! ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Nonsense', 1) %!error ... %! ClassificationKernel ({1, 2; 3, 4}, [1; 2]) %!error ClassificationKernel ([], []) %!error ... %! ClassificationKernel (ones (10, 2), [1; 2]) %!error ... %! ClassificationKernel (ones (9, 2), [1; 1; 1; 2; 2; 2; 3; 3; 3]) %!error ... %! ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Cost', ... %! ones (3)) %!error ... %! predict (ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)]), ... %! ones (3, 5)) %!error ... %! predict (ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)]), []) %!error ... %! margin (ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)]), ... %! ones (3, 2)) %!error ... %! loss (ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)]), ... %! ones (10, 2), [ones(5,1); 2*ones(5,1)], 'LossFun', 'mse') %!error ... %! loss (ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)]), ... %! ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Bogus', 1) %!error ... %! resume (ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)]), ... %! ones (10, 2)) %!error ... %! resume (ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)]), ... %! ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'IterationLimit', 0) %!error ... %! resume (ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)]), ... %! ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Nonsense', 1) %!error ... %! resume (ClassificationKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)]), ... %! ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Weights', -1) ## Every documented score transform reaches the scores that are reported, and ## none of them moves the label: a transform reshapes what is reported, not ## what is decided. %!test %! load fisheriris %! Mdl = fitckernel (meas, strcmp (species, 'setosa')); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! T = {'identity', @(x) x; 'doublelogit', @(x) 1 ./ (1 + exp (-2 * x)); ... %! 'invlogit', @(x) log (x ./ (1 - x)); ... %! 'logit', @(x) 1 ./ (1 + exp (-x)); ... %! 'sign', @(x) sign (x); 'symmetric', @(x) 2 * x - 1; ... %! 'symmetriclogit', @(x) 2 ./ (1 + exp (-x)) - 1}; %! for i = 1:rows (T) %! Mdl.ScoreTransform = T{i,1}; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, T{i,2}(raw), 1e-12); %! assert_equal (l, label); %! endfor %! ## ismax marks the largest score of each observation, ties to the first. %! [~, k] = max (raw, [], 2); %! e = zeros (size (raw)); %! e(sub2ind (size (raw), (1:rows (raw))', k)) = 1; %! Mdl.ScoreTransform = 'ismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, e); %! Mdl.ScoreTransform = 'symmetricismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, 2 * e - 1); ## A function handle is taken as given and applied to the scores. %!test %! load fisheriris %! Mdl = fitckernel (meas, strcmp (species, 'setosa')); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! Mdl.ScoreTransform = @(x) x .^ 2; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, raw .^ 2, 1e-12); %! assert_equal (l, label); %!shared Xc, Dc, yc %! c1 = repmat ([1; 2; 3], 20, 1); %! x2 = sin ((1:60)'); %! c3 = repmat ([10; 10; 20; 20], 15, 1); %! Xc = [c1, x2, c3]; %! Dc = [c1 == 1, c1 == 2, c1 == 3, x2, c3 == 10, c3 == 20]; %! yc = 5 * (c1 == 2) + 0.5 * x2 - 3 * (c3 == 20) + 0.1 * cos ((1:60)') > 1; %!test # MATLAB parity: the kernel expands the dummy coded predictors %! randn ('seed', 9); %! rand ('seed', 9); %! Mdl = ClassificationKernel (Xc, yc, 'CategoricalPredictors', [1, 3]); %! randn ('seed', 9); %! rand ('seed', 9); %! H = ClassificationKernel (Dc, yc); %! assert_equal (Mdl.ExpandedPredictorNames, {'x1 == 1', 'x1 == 2', ... %! 'x1 == 3', 'x2', 'x3 == 10', 'x3 == 20'}); %! assert_equal (Mdl.NumExpansionDimensions, 256); %! [~, s] = predict (Mdl, Xc(1:5,:)); %! [~, sh] = predict (H, Dc(1:5,:)); %! assert_equal (s, sh, 1e-12); %!test # MATLAB parity: the coded columns are not standardized %! Mdl = ClassificationKernel (Xc, yc, 'CategoricalPredictors', [1, 3], ... %! 'Standardize', true); %! assert_equal (Mdl.Mu([1:3, 5:6]), zeros (1, 5)); %! assert_equal (Mdl.Sigma([1:3, 5:6]), ones (1, 5)); %! assert_equal (Mdl.Mu(4), mean (Xc(:,2)), 1e-12); %!test # a level the training data did not hold is scored NaN %! Mdl = ClassificationKernel (Xc, yc, 'CategoricalPredictors', [1, 3]); %! [~, s] = predict (Mdl, [4, 0, 10; 2, 0, 20]); %! assert_equal (isnan (s(:,1))', [true, false]); %!error ... %! ClassificationKernel (Xc, yc, 'CategoricalPredictors', 4) %!error ... %! ClassificationKernel (Xc, yc, 'CategoricalPredictors', logical ([1, 0])) ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(51:150,1:2); %! y = categorical (species(51:150)); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitckernel (T, 'Species'); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'Species'), a); %! assert_equal (loss (Mdl, T), a); ## A table at edge %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(51:150,1:2); %! y = categorical (species(51:150)); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitckernel (T, 'Species'); %! a = edge (Mdl, X, y); %! assert_equal (edge (Mdl, T(:,1:2), y), a); %! assert_equal (edge (Mdl, T, 'Species'), a); %! assert_equal (edge (Mdl, T), a); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(51:150,1:2); %! y = categorical (species(51:150)); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitckernel (T, 'Species'); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## A table at resume %!test # the response is named or given beside the table %! load fisheriris %! X = meas(51:150,1:2); %! y = categorical (species(51:150)); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitckernel (T, 'Species', 'IterationLimit', 5); %! [~, a] = predict (resume (Mdl, X, y, 'IterationLimit', 20), X); %! [~, b] = predict (resume (Mdl, T(:,1:2), y, 'IterationLimit', 20), X); %! assert_equal (b, a); %! [~, b] = predict (resume (Mdl, T, 'Species', 'IterationLimit', 20), X); %! assert_equal (b, a); %! [~, b] = predict (resume (Mdl, T(:,[3, 2, 1]), 'Species'), X); %! [~, c] = predict (resume (Mdl, X, y), X); %! assert_equal (b, c); %!error ... %! load fisheriris %! T = table (meas(51:150,1), meas(51:150,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = categorical (species(51:150)); %! resume (fitckernel (T, 'Species'), T) %!error ... %! load fisheriris %! T = table (meas(51:150,1), meas(51:150,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = categorical (species(51:150)); %! resume (fitckernel (T, 'Species'), T, 'IterationLimit', 5) ## Observation weights of class single or double %!error ... %! fitckernel ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', ... %! int8 ([1; 1; 1; 1])) %!error ... %! fitckernel ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', true (4, 1)) %!error %! X = [1, 2; 3, 4; 5, 6; 7, 8]; %! Y = [1; 1; 2; 2]; %! loss (fitckernel (X, Y), X, Y, 'Weights', int8 ([1; 1; 1; 1])) %!error %! X = [1, 2; 3, 4; 5, 6; 7, 8]; %! Y = [1; 1; 2; 2]; %! resume (fitckernel (X, Y), X, Y, 'Weights', int8 ([1; 1; 1; 1])) %!test %! ## Single weights compute as double %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! w = 1 + (1:100)' / 7; %! rand ('seed', 1); %! randn ('seed', 1); %! A = fitckernel (X, Y, 'Weights', single (w)); %! rand ('seed', 1); %! randn ('seed', 1); %! B = fitckernel (X, Y, 'Weights', double (single (w))); %! assert_equal (nthargout (2, @predict, A, X), nthargout (2, @predict, B, X)); ## Test a class with prior 0, whose observations are removed before fitting, ## as MATLAB R2024a and R2026b remove them %!warning ... %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! Mdl = ClassificationKernel (X, Y, 'Prior', [1, 0]); %! assert_equal (Mdl.Prior, [1, 0]); %! assert_equal (predict (Mdl, meas([51, 110], 3:4)), ... %! {'versicolor'; 'versicolor'}); %!test %! ## A named class with prior 0 may be absent, having nothing to remove %! load fisheriris %! X = meas(51:100, 3:4); %! Y = species(51:100); %! C = {'versicolor', 'virginica'}; %! Mdl = ClassificationKernel (X, Y, 'ClassNames', C, 'Prior', [1, 0]); %! assert_equal (Mdl.ClassNames, {'versicolor'; 'virginica'}); %! assert_equal (predict (Mdl, meas(51, 3:4)), {'versicolor'}); %!warning ... %! load fisheriris %! Mdl = ClassificationKernel (meas(51:150, 3:4), species(51:150), ... %! 'Cost', [0, 1; 0, 0]); %! assert_equal (Mdl.Prior, [1, 0]); ## Test a 'Cost' given as a structure, permuted by name to the model's order, ## as MATLAB R2024a permutes it %!test %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! S = struct ('ClassNames', {{'virginica', 'versicolor'}}, ... %! 'ClassificationCosts', [0, 2; 1, 0]); %! Mdl = ClassificationKernel (X, Y, 'Cost', S); %! assert_equal (Mdl.Cost, [0, 1; 2, 0]); %! assert_equal (Mdl.Prior, [0.5, 0.5]); %!error ... %! load fisheriris %! ClassificationKernel (meas(51:150, 3:4), species(51:150), ... %! 'Cost', struct ('a', 1)); statistics-release-2.1.2/inst/Machine_Learning/ClassificationLinear.m000066400000000000000000002734441527311104300257430ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ClassificationLinear < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} ClassificationLinear ## ## Linear binary classifier for high dimensional data. ## ## A @qcode{ClassificationLinear} object fits a linear model, ## @code{@var{X} * Beta + Bias}, to a two class problem by minimizing a ## regularized average loss. The loss is the hinge loss for a support vector ## machine and the deviance for a logistic regression, and the penalty is ## either a ridge or a lasso one. ## ## Unlike the other classifiers of this package the object holds no copy of ## the training data: the coefficients, the intercept and the fitting options ## are the whole model. That is what makes it suited to data with more ## predictors than an in memory kernel matrix could carry, and it is why the ## class has no @code{compact} method and no resubstitution methods. ## ## A vector of regularization strengths fits one model per value in a single ## object. @qcode{Beta} is then a @math{PxL} matrix and @qcode{Bias} a ## @math{1xL} row, every method returns one column per strength, and ## @code{selectModels} narrows the object down to the strengths worth ## keeping. ## ## Create a @qcode{ClassificationLinear} object with @code{fitclinear}. ## ## @seealso{fitclinear, ClassificationKernel, ClassificationSVM} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationLinear} {property} ClassNames ## ## Names of the two classes ## ## A column of the same type as the response supplied to the constructor: ## a cell array of character vectors, a numeric vector, a logical vector ## or a character matrix, sorted or in the order given by the ## @qcode{'ClassNames'} option. The second of the two is the positive ## class, the one a positive score belongs to. This property is read-only. ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {ClassificationLinear} {property} Prior ## ## Prior probability of each class ## ## A numeric row vector with one element per class, in the order of ## @qcode{ClassNames} and summing to one. It defaults to the class ## frequencies of the training data. This property is read-only. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {ClassificationLinear} {property} Cost ## ## Cost of misclassifying an observation ## ## A square numeric matrix with one row and one column per class, whose ## @math{(i,j)} element is the cost of classifying an observation of ## class @math{i} into class @math{j}. It defaults to one everywhere ## except the diagonal, which is zero. This property is read-only: ## MATLAB refuses an assignment into it on this class, as it does on the ## support vector machine, so a cost matrix is given to the constructor ## instead. ## ## The cost matrix takes no part in the fit and none in @code{predict}, ## which returns the class of largest score. It is read by the ## @qcode{'mincost'} and @qcode{'classifcost'} losses alone. ## ## @end deftp Cost = []; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {ClassificationLinear} {property} ScoreTransform ## ## Transformation applied to the predicted scores ## ## A character vector naming a transformation, or the text of the ## function handle that was supplied. Assigning to it accepts either. ## It defaults to @qcode{'logit'} for a logistic learner, which turns the ## scores into posterior probabilities, and to @qcode{'none'} for a ## support vector machine. ## ## @end deftp ScoreTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationLinear} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors with one name per column of the ## training data, defaulting to @qcode{'x1'}, @qcode{'x2'} and so on. ## This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationLinear} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A row vector of column indices, empty when every predictor is ## numeric. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationLinear} {property} ResponseName ## ## Name of the response ## ## A character vector, defaulting to @qcode{'Y'}. This property is ## read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {ClassificationLinear} {property} ExpandedPredictorNames ## ## Names of the predictors as the fit saw them ## ## A cell array of character vectors. It equals @qcode{PredictorNames} ## unless categorical predictors were expanded into indicator variables. ## This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationLinear} {property} Learner ## ## Linear classification model that was fitted ## ## Either @qcode{'svm'} or @qcode{'logistic'}. This property is ## read-only. ## ## @end deftp Learner = 'svm'; ## -*- texinfo -*- ## @deftp {ClassificationLinear} {property} Beta ## ## Fitted linear coefficients ## ## A @math{Px1} column, or a @math{PxL} matrix with one column per ## regularization strength when @qcode{Lambda} holds more than one. This ## property is read-only. ## ## @end deftp Beta = []; ## -*- texinfo -*- ## @deftp {ClassificationLinear} {property} Bias ## ## Fitted intercept ## ## A scalar, or a @math{1xL} row with one element per regularization ## strength. It is zero throughout when the model was fitted with ## @qcode{'FitBias'} set to false. This property is read-only. ## ## @end deftp Bias = []; ## -*- texinfo -*- ## @deftp {ClassificationLinear} {property} FittedLoss ## ## Loss function the fit minimized ## ## @qcode{'hinge'} for a support vector machine and @qcode{'logit'} for a ## logistic regression. This is the loss of the objective, which is not ## the loss @code{loss} reports unless it is asked for. This property is ## read-only. ## ## @end deftp FittedLoss = 'hinge'; ## -*- texinfo -*- ## @deftp {ClassificationLinear} {property} Lambda ## ## Regularization strength ## ## A nonnegative scalar, or a @math{1xL} row of them in ascending order. ## It defaults to the reciprocal of the number of observations used to ## train the model. This property is read-only. ## ## @end deftp Lambda = []; ## -*- texinfo -*- ## @deftp {ClassificationLinear} {property} ModelParameters ## ## Fitting options, as they were given ## ## A structure holding every parameter of the fit, including the ones ## that a different solver would have used and the @qcode{'auto'} values ## before they were resolved. This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {ClassificationLinear} {property} Regularization ## ## Penalty on the coefficients ## ## @qcode{'ridge (L2)'} or @qcode{'lasso (L1)'}. This property is ## read-only. ## ## @end deftp Regularization = 'ridge (L2)'; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ## The callable behind ScoreTransform. The public property is the text ## MATLAB stores; this is what predict actually applies. STfun = @(s) s; ## Number of predictors the fit saw, which the class does not report ## because MATLAB does not, but predict needs to validate its input. NumPredictors_ = []; Coding_ = []; ## What the fit reported, so that fitclinear can hand it back as its ## second output. MATLAB returns it from the fitting function rather ## than storing it on the model, and a constructor cannot have a second ## output argument. FitInfo_ = []; endproperties methods (Access = public) ## -*- texinfo -*- ## @deftypefn {ClassificationLinear} {@var{obj} =} ClassificationLinear (@var{X}, @var{Y}) ## @deftypefnx {ClassificationLinear} {@var{obj} =} ClassificationLinear (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationLinear} {@var{obj} =} ClassificationLinear (@var{Tbl}, @var{formula}) ## @deftypefnx {ClassificationLinear} {@var{obj} =} ClassificationLinear (@var{Tbl}, @var{Y}) ## @deftypefnx {ClassificationLinear} {@var{obj} =} ClassificationLinear (@dots{}, @var{name}, @var{value}) ## ## Fit a linear binary classifier. ## ## @code{@var{obj} = ClassificationLinear (@var{X}, @var{Y})} fits a ## linear support vector machine to the @math{NxP} predictor matrix ## @var{X} and the @math{Nx1} response @var{Y}, which must name exactly ## two classes. ## ## @code{@var{obj} = ClassificationLinear (@dots{}, @var{name}, ## @var{value})} takes the following @qcode{Name-Value} pairs. ## ## @multitable @columnfractions 0.28 0.72 ## @headitem Name @tab Value ## ## @item @qcode{'Learner'} @tab @qcode{'svm'}, the default, or ## @qcode{'logistic'}. The first minimizes the hinge loss and the second ## the deviance. ## ## @item @qcode{'Regularization'} @tab @qcode{'ridge'} or ## @qcode{'lasso'}. It defaults to @qcode{'lasso'} when the solver is ## @qcode{'sparsa'} and to @qcode{'ridge'} otherwise. ## ## @item @qcode{'Lambda'} @tab @qcode{'auto'}, the default, which is the ## reciprocal of the number of observations, or a nonnegative scalar, or ## a vector of them. A vector fits one model per value. ## ## @item @qcode{'Solver'} @tab One of @qcode{'sgd'}, @qcode{'asgd'}, ## @qcode{'dual'}, @qcode{'bfgs'}, @qcode{'lbfgs'} and @qcode{'sparsa'}, ## or a cell array of them applied in turn, each warm starting the next. ## The default depends on the data and the penalty, as described below. ## ## @item @qcode{'Beta'} @tab Initial coefficients, a @math{Px1} column or ## a @math{PxL} matrix. It defaults to zeros. ## ## @item @qcode{'Bias'} @tab Initial intercept, a scalar or a @math{1xL} ## row. It defaults to the weighted average of the class labels for a ## logistic learner and to zero for a support vector machine. ## ## @item @qcode{'FitBias'} @tab Whether to fit an intercept at all, true ## by default. ## ## @item @qcode{'PostFitBias'} @tab Whether to refit the intercept once ## the coefficients are settled, false by default. ## ## @item @qcode{'ObservationsIn'} @tab @qcode{'rows'}, the default, or ## @qcode{'columns'}, which transposes @var{X} before fitting. ## ## @item @qcode{'BetaTolerance'} @tab Relative tolerance on the ## coefficients, @qcode{1e-4} by default. ## ## @item @qcode{'GradientTolerance'} @tab Absolute tolerance on the ## gradient's infinity norm, @qcode{1e-6} by default. ## ## @item @qcode{'DeltaGradientTolerance'} @tab Tolerance on the ## complementarity gap of the @qcode{'dual'} solver, @qcode{1} by ## default for a hinge loss. MathWorks documents @qcode{0.1}, which is ## the default of the @emph{regression} counterpart; R2024a and R2026a ## both report @qcode{1} here. ## ## @item @qcode{'IterationLimit'} @tab Largest number of iterations, ## @qcode{1000} by default. ## ## @item @qcode{'PassLimit'} @tab Largest number of passes over the data ## for the stochastic solvers, @qcode{1} by default, and @qcode{10} for ## @qcode{'dual'}. ## ## @item @qcode{'BatchSize'} @tab Mini-batch size of the stochastic ## solvers, @qcode{10} by default. ## ## @item @qcode{'BatchLimit'} @tab Largest number of mini-batches. ## ## @item @qcode{'LearnRate'} @tab Step size of the stochastic solvers. ## ## @item @qcode{'OptimizeLearnRate'} @tab Whether to halve the step size ## when the objective rises, true by default. ## ## @item @qcode{'TruncationPeriod'} @tab Number of mini-batches between ## soft thresholdings under a lasso penalty, @qcode{10} by default. ## ## @item @qcode{'NumCheckConvergence'} @tab Number of passes between ## convergence checks of the @qcode{'dual'} solver, @qcode{2} by ## default. MathWorks documents @qcode{5}; R2024a and R2026a both ## report @qcode{2}, so the documentation is stale rather than the ## releases being inconsistent. ## ## @item @qcode{'HessianHistorySize'} @tab Number of curvature pairs the ## quasi-Newton solvers keep, @qcode{15} by default. ## ## @item @qcode{'ClassNames'} @tab The classes to keep, given in the type ## of @var{Y}. Observations of any other class are dropped. The model ## keeps the classes in this order; by default they are sorted. ## ## @item @qcode{'Cost'} @tab A square misclassification cost matrix. ## Or a structure with fields @qcode{ClassNames} and ## @qcode{ClassificationCosts}, which names its own order of the classes and ## is permuted to the model's. ## ## @item @qcode{'Prior'} @tab @qcode{'empirical'}, the default, ## @qcode{'uniform'}, a vector of probabilities, or a structure with ## @qcode{ClassNames} and @qcode{ClassProbs} fields. ## A class whose prior is 0, or whose misclassification costs are all 0, is ## removed from the data before fitting, with a warning, as in MATLAB. ## ## @item @qcode{'ScoreTransform'} @tab A transformation applied to the ## scores, named or given as a function handle. ## ## @item @qcode{'Weights'} @tab One nonnegative weight per observation, as a ## single or double vector. Every computation runs in double. ## ## @item @qcode{'PredictorNames'} @tab One name per predictor. ## ## @item @qcode{'ResponseName'} @tab A name for the response. ## ## @item @qcode{'CategoricalPredictors'} @tab The categorical predictors, ## as indices, as a logical vector with one element per predictor, or as ## @qcode{'all'}. Each is dummy coded in its place, one column of zeros ## and ones per distinct value it takes in the training data, named as in ## @qcode{'x1 == 2'}. A row holding a value the training data did not ## is scored @code{NaN} and labelled with the class of largest prior, as ## a row missing a predictor is. ## A predictor may be named rather than indexed, as a character matrix of ## one padded name per row, a string array or a cellstr; a name must match ## an entry of @qcode{'PredictorNames'} exactly, its case included. ## @end multitable ## ## The default solver is @qcode{'sparsa'} under a lasso penalty. Under a ## ridge penalty it is @qcode{'bfgs'} when there are no more than 100 ## predictors, and beyond that @qcode{'dual'} for a support vector ## machine and @qcode{'sgd'} for a logistic regression. ## ## @seealso{fitclinear, ClassificationKernel} ## @end deftypefn function this = ClassificationLinear (X, Y, varargin) ## Check for sufficient number of input arguments if (nargin < 2) error ("ClassificationLinear: too few input arguments."); endif ## A table names its own predictors and says which hold levels [this, X, Y, varargin] = resolveTable (this, 'ClassificationLinear', ... X, Y, varargin); if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationLinear: optional arguments must", ... " be given in Name-Value pairs.")); endif ## Parse optional paired arguments optNames = {'Learner', 'Regularization', 'Lambda', 'Solver', 'Beta', ... 'Bias', 'FitBias', 'PostFitBias', 'ObservationsIn', ... 'BetaTolerance', 'GradientTolerance', ... 'DeltaGradientTolerance', 'IterationLimit', 'PassLimit', ... 'BatchSize', 'BatchLimit', 'LearnRate', ... 'OptimizeLearnRate', 'TruncationPeriod', ... 'NumCheckConvergence', 'HessianHistorySize', 'Verbose', ... 'ClassNames', 'Cost', 'Prior', 'ScoreTransform', ... 'Weights', 'PredictorNames', 'ResponseName', ... 'CategoricalPredictors'}; ## An empty default stands for one resolved once the data are known: ## 'Regularization' is 'lasso' for the 'sparsa' solver and 'ridge' ## otherwise; 'Solver' is 'sparsa' for a lasso penalty, 'bfgs' up to ## 100 predictors, then 'dual' for an SVM and 'sgd' for a logistic ## learner; 'Beta' is zero and 'Bias' zero, or the weighted mean ## response for a logistic learner; 'PassLimit' is 10 with the 'dual' ## solver and 1 otherwise; 'BatchLimit' sets no limit; 'LearnRate' is ## one over the root of one plus the largest squared row length; ## 'ClassNames', 'Cost', 'Prior' and 'Weights' come from the response ## as every class, a zero-one cost, the empirical prior and uniform ## weights; 'ScoreTransform' is 'logit' for a logistic learner and ## 'none' otherwise; 'PredictorNames' are x1, x2, ... dfValues = {'svm', [], 'auto', [], [], [], true, false, 'rows', 1e-4, ... 1e-6, 1, 1000, [], 10, [], [], true, 10, 2, 15, 0, [], [], ... [], [], [], {}, 'Y', []}; [Learner, Regularization, Lambda, Solver, BetaIn, BiasIn, FitBias, ... PostFitBias, ObservationsIn, BetaTolerance, GradientTolerance, ... DeltaGradientTolerance, IterationLimit, PassLimit, BatchSize, ... BatchLimit, LearnRate, OptimizeLearnRate, TruncationPeriod, ... NumCheckConvergence, HessianHistorySize, Verbose, ClassNames, CostIn, ... Prior, ScoreTransform, Weights, PredictorNames, ResponseName, ... CategoricalPredictors, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (ischar (Learner) && any (strcmpi (Learner, {'svm', 'logistic'})))) error (strcat ("ClassificationLinear: 'Learner' must be", ... " either 'svm' or 'logistic'.")); endif Learner = lower (Learner); if (! isempty (Regularization) && ! (ischar (Regularization) && any (strcmpi (Regularization, {'ridge', 'lasso'})))) error (strcat ("ClassificationLinear: 'Regularization'", ... " must be either 'ridge' or 'lasso'.")); endif Regularization = lower (Regularization); if (! ((ischar (Lambda) && strcmpi (Lambda, 'auto')) || (isnumeric (Lambda) && isreal (Lambda) && isvector (Lambda) && ! isempty (Lambda) && all (Lambda >= 0) && all (isfinite (Lambda))))) error (strcat ("ClassificationLinear: 'Lambda' must be", ... " 'auto' or a vector of nonnegative", ... " finite values.")); endif if (ischar (Solver)) Solver = {Solver}; endif valid = {'sgd', 'asgd', 'dual', 'bfgs', 'lbfgs', 'sparsa'}; if (! isempty (Solver) && ! (iscellstr (Solver) && ! isempty (Solver) && all (cellfun (@(s) any (strcmpi (s, valid)), Solver)))) error (strcat ("ClassificationLinear: 'Solver' must be one", ... " of 'sgd', 'asgd', 'dual', 'bfgs',", ... " 'lbfgs' and 'sparsa', or a cell array of", ... " them.")); endif Solver = lower (Solver); if (! isempty (BetaIn) && ! (isnumeric (BetaIn) && isreal (BetaIn) && ismatrix (BetaIn) && ! isempty (BetaIn))) error (strcat ("ClassificationLinear: 'Beta' must be a", ... " real numeric matrix.")); endif if (! isempty (BiasIn) && ! (isnumeric (BiasIn) && isreal (BiasIn) && isvector (BiasIn) && ! isempty (BiasIn))) error (strcat ("ClassificationLinear: 'Bias' must be a", ... " real numeric vector.")); endif if (! (islogical (FitBias) || (isnumeric (FitBias) && isscalar (FitBias) && any (FitBias == [0, 1])))) error (strcat ("ClassificationLinear: 'FitBias' must be", ... " either true or false.")); endif FitBias = logical (FitBias); if (! (islogical (PostFitBias) || (isnumeric (PostFitBias) && isscalar (PostFitBias) && any (PostFitBias == [0, 1])))) error (strcat ("ClassificationLinear: 'PostFitBias' must", ... " be either true or false.")); endif PostFitBias = logical (PostFitBias); if (! (ischar (ObservationsIn) && any (strcmpi (ObservationsIn, {'rows', 'columns'})))) error (strcat ("ClassificationLinear: 'ObservationsIn'", ... " must be either 'rows' or 'columns'.")); endif ObservationsIn = lower (ObservationsIn); if (! (isnumeric (BetaTolerance) && isscalar (BetaTolerance) && isreal (BetaTolerance) && BetaTolerance >= 0)) error (strcat ("ClassificationLinear: 'BetaTolerance'", ... " must be a nonnegative scalar.")); endif if (! (isnumeric (GradientTolerance) && isscalar (GradientTolerance) && isreal (GradientTolerance) && GradientTolerance >= 0)) error (strcat ("ClassificationLinear:", ... " 'GradientTolerance' must be a", ... " nonnegative scalar.")); endif if (! (isnumeric (DeltaGradientTolerance) && isscalar (DeltaGradientTolerance) && isreal (DeltaGradientTolerance) && DeltaGradientTolerance >= 0)) error (strcat ("ClassificationLinear:", ... " 'DeltaGradientTolerance' must be a", ... " nonnegative scalar.")); endif if (! (isnumeric (IterationLimit) && isscalar (IterationLimit) && isreal (IterationLimit) && IterationLimit > 0 && fix (IterationLimit) == IterationLimit)) error (strcat ("ClassificationLinear: 'IterationLimit'", ... " must be a positive integer scalar.")); endif if (! isempty (PassLimit) && ! (isnumeric (PassLimit) && isscalar (PassLimit) && isreal (PassLimit) && PassLimit > 0 && fix (PassLimit) == PassLimit)) error (strcat ("ClassificationLinear: 'PassLimit' must be", ... " a positive integer scalar.")); endif if (! (isnumeric (BatchSize) && isscalar (BatchSize) && isreal (BatchSize) && BatchSize > 0 && fix (BatchSize) == BatchSize)) error (strcat ("ClassificationLinear: 'BatchSize' must be", ... " a positive integer scalar.")); endif if (! isempty (BatchLimit) && ! (isnumeric (BatchLimit) && isscalar (BatchLimit) && isreal (BatchLimit) && BatchLimit > 0 && fix (BatchLimit) == BatchLimit)) error (strcat ("ClassificationLinear: 'BatchLimit' must", ... " be a positive integer scalar.")); endif if (! isempty (LearnRate) && ! (isnumeric (LearnRate) && isscalar (LearnRate) && isreal (LearnRate) && LearnRate > 0)) error (strcat ("ClassificationLinear: 'LearnRate' must be", ... " a positive scalar.")); endif if (! (islogical (OptimizeLearnRate) || (isnumeric (OptimizeLearnRate) && isscalar (OptimizeLearnRate) && any (OptimizeLearnRate == [0, 1])))) error (strcat ("ClassificationLinear:", ... " 'OptimizeLearnRate' must be either true", ... " or false.")); endif OptimizeLearnRate = logical (OptimizeLearnRate); if (! (isnumeric (TruncationPeriod) && isscalar (TruncationPeriod) && isreal (TruncationPeriod) && TruncationPeriod > 0 && fix (TruncationPeriod) == TruncationPeriod)) error (strcat ("ClassificationLinear:", ... " 'TruncationPeriod' must be a positive", ... " integer scalar.")); endif if (! (isnumeric (NumCheckConvergence) && isscalar (NumCheckConvergence) && isreal (NumCheckConvergence) && NumCheckConvergence > 0 && fix (NumCheckConvergence) == NumCheckConvergence)) error (strcat ("ClassificationLinear:", ... " 'NumCheckConvergence' must be a positive", ... " integer scalar.")); endif if (! (isnumeric (HessianHistorySize) && isscalar (HessianHistorySize) && isreal (HessianHistorySize) && HessianHistorySize > 0 && fix (HessianHistorySize) == HessianHistorySize)) error (strcat ("ClassificationLinear:", ... " 'HessianHistorySize' must be a positive", ... " integer scalar.")); endif if (! (isnumeric (Verbose) && isscalar (Verbose) && isreal (Verbose) && any (Verbose == [0, 1, 2]))) error (strcat ("ClassificationLinear: 'Verbose' must be", ... " 0, 1, or 2.")); endif if (! isempty (ClassNames) && ! (iscellstr (ClassNames) || isnumeric (ClassNames) || islogical (ClassNames) || ischar (ClassNames) || isa (ClassNames, 'categorical') || isa (ClassNames, 'string'))) error (strcat ("ClassificationLinear: 'ClassNames' must be", ... " a categorical array, a character array, a", ... " string array, a logical vector, a numeric", ... " vector, or a cell array of character", ... " vectors.")); endif ## A structure names its own order, and is permuted to the model's if (! isempty (CostIn) && ! (isstruct (CostIn) || (isnumeric (CostIn) && isreal (CostIn) && ismatrix (CostIn) && ndims (CostIn) == 2 && rows (CostIn) == columns (CostIn)))) error (strcat ("ClassificationLinear: 'Cost' must be a", ... " square numeric matrix or a structure.")); endif if (! isempty (Prior) && ! ((ischar (Prior) && any (strcmpi (Prior, {'empirical', ... 'uniform'}))) || (isnumeric (Prior) && isreal (Prior) && isvector (Prior) && all (Prior >= 0)) || (isstruct (Prior) && isscalar (Prior)))) error (strcat ("ClassificationLinear: 'Prior' must be", ... " 'empirical', 'uniform', a vector of", ... " nonnegative values, or a structure.")); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("ClassificationLinear: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isreal (Weights) && isvector (Weights) && all (Weights >= 0))) error (strcat ("ClassificationLinear: 'Weights' must be a", ... " vector of nonnegative values.")); endif if (! isempty (PredictorNames) && ! (iscellstr (PredictorNames) && isvector (PredictorNames))) error (strcat ("ClassificationLinear: 'PredictorNames'", ... " must be a cell array of character", ... " vectors.")); endif if (! (ischar (ResponseName) && isrow (ResponseName))) error (strcat ("ClassificationLinear: 'ResponseName' must", ... " be a character vector.")); endif if (! isempty (args)) error ("ClassificationLinear: invalid optional paired argument."); endif ## Observations may be given down the columns, which only means the ## predictor matrix arrives transposed. if (strcmp (ObservationsIn, 'columns')) X = X'; endif ## Validate the data, resolve the classes, the prior, the cost and ## the observation weights. The four linear and kernel classifiers ## share that opening, so it lives in one place. F = classFrame (X, Y, ClassNames, Prior, CostIn, Weights, ... 'ClassificationLinear'); X = F.X; y = F.y; W = F.W; n = F.n; p = F.p; this.ClassNames = F.ClassNames; this.Prior = F.Prior; this.Cost = F.Cost; ## Dummy code the categorical predictors on the rows kept; the fit ## sees the coded columns, and new data is coded the same way. if (isempty (PredictorNames)) PredictorNames = arrayfun (@(k) sprintf ("x%d", k), 1:p, ... 'UniformOutput', false); elseif (numel (PredictorNames) != p) error (strcat ("ClassificationLinear: 'PredictorNames' must", ... " have one name per predictor.")); endif [Coding, errmsg] = dummyCoding (X, CategoricalPredictors, ... PredictorNames); if (! isempty (errmsg)) error ("ClassificationLinear: %s", errmsg); endif X = dummyCoding (X, Coding); p = columns (X); ## Resolve the penalty and the solver against one another, since each ## has a default that depends on the other. if (isempty (Regularization)) if (! isempty (Solver) && numel (Solver) == 1 && strcmp (Solver{1}, 'sparsa')) Regularization = 'lasso'; else Regularization = 'ridge'; endif endif if (isempty (Solver)) if (strcmp (Regularization, 'lasso')) Solver = {'sparsa'}; elseif (p <= 100) Solver = {'bfgs'}; elseif (strcmp (Learner, 'svm')) Solver = {'dual'}; else Solver = {'sgd'}; endif endif for k = 1:numel (Solver) if (strcmp (Regularization, 'lasso') && ! any (strcmp (Solver{k}, {'sgd', 'asgd', 'sparsa'}))) error (strcat ("ClassificationLinear: the '%s' solver fits a", ... " ridge penalty only."), Solver{k}); endif if (strcmp (Regularization, 'ridge') && strcmp (Solver{k}, 'sparsa')) error (strcat ("ClassificationLinear: the 'sparsa' solver", ... " fits a lasso penalty only.")); endif if (strcmp (Solver{k}, 'dual') && strcmp (Learner, 'logistic')) error (strcat ("ClassificationLinear: the 'dual' solver fits", ... " a hinge loss only, so it needs 'Learner' set", ... " to 'svm'.")); endif endfor ## Resolve Lambda, which the class reports as a number even when it ## was given as 'auto', and which ModelParameters keeps as given. LambdaIn = Lambda; if (ischar (Lambda)) Lambda = 1 / n; else Lambda = sort (Lambda(:)'); endif L = numel (Lambda); ## Resolve the starting point if (isempty (BetaIn)) Beta0 = zeros (p, L); else if (rows (BetaIn) != p) error (strcat ("ClassificationLinear: 'Beta' must have one", ... " row per predictor.")); endif if (columns (BetaIn) == 1) Beta0 = repmat (BetaIn, 1, L); elseif (columns (BetaIn) == L) Beta0 = BetaIn; else error (strcat ("ClassificationLinear: 'Beta' must have one", ... " column, or one per value of 'Lambda'.")); endif endif if (isempty (BiasIn)) if (strcmp (Learner, 'logistic')) Bias0 = repmat (sum (W .* y), 1, L); else Bias0 = zeros (1, L); endif else BiasIn = BiasIn(:)'; if (numel (BiasIn) == 1) Bias0 = repmat (BiasIn, 1, L); elseif (numel (BiasIn) == L) Bias0 = BiasIn; else error (strcat ("ClassificationLinear: 'Bias' must be a", ... " scalar, or hold one value per value of", ... " 'Lambda'.")); endif endif if (isempty (PassLimit)) if (any (strcmp (Solver, 'dual'))) PassLimit = 10; else PassLimit = 1; endif endif if (isempty (LearnRate)) ## MATLAB's default: the reciprocal root of one plus the largest ## squared length of an observation, so a step never overshoots the ## widest row of the data. LearnRate = 1 / sqrt (1 + max (sum (X .^ 2, 2))); endif ## Fit one model per regularization strength, each warm starting the ## next, which is what makes an ascending Lambda cheaper than the same ## values fitted apart. Beta = zeros (p, L); Bias = zeros (1, L); info = struct ([]); P = struct (); P.Learner = Learner; P.LossFunction = 'hinge'; if (strcmp (Learner, 'logistic')) P.LossFunction = 'logit'; endif P.Epsilon = []; P.Regularization = Regularization; P.FitBias = FitBias; P.PostFitBias = PostFitBias; P.BetaTolerance = BetaTolerance; P.GradientTolerance = GradientTolerance; P.DeltaGradientTolerance = DeltaGradientTolerance; P.IterationLimit = IterationLimit; P.PassLimit = PassLimit; P.BatchSize = BatchSize; P.BatchLimit = BatchLimit; P.LearnRate = LearnRate; P.OptimizeLearnRate = OptimizeLearnRate; P.TruncationPeriod = TruncationPeriod; P.NumCheckConvergence = NumCheckConvergence; P.HessianHistorySize = HessianHistorySize; for l = 1:L P.Lambda = Lambda(l); b = Beta0(:,l); b0 = Bias0(l); if (l > 1 && isempty (BetaIn)) b = Beta(:,l-1); b0 = Bias(l-1); endif for k = 1:numel (Solver) P.Solver = Solver{k}; P.InitialBeta = b; P.InitialBias = b0; [b, b0, S] = linearSolve (X, y, W, P); endfor Beta(:,l) = b; Bias(l) = b0; S.Lambda = Lambda(l); if (l == 1) info = S; else info(l) = S; endif endfor ## Fill in the model this.PredictorNames = PredictorNames; this.ExpandedPredictorNames = PredictorNames; if (! isempty (Coding.Index)) this.ExpandedPredictorNames = Coding.ExpandedNames; this.CategoricalPredictors = Coding.Index; this.Coding_ = Coding; endif this.ResponseName = ResponseName; this.Learner = Learner; this.Beta = Beta; this.Bias = Bias; this.FittedLoss = P.LossFunction; this.Lambda = Lambda; this.NumPredictors_ = Coding.NumPredictors; if (strcmp (Regularization, 'ridge')) this.Regularization = 'ridge (L2)'; else this.Regularization = 'lasso (L1)'; endif if (isempty (ScoreTransform)) if (strcmp (Learner, 'logistic')) ScoreTransform = 'logit'; else ScoreTransform = 'none'; endif endif this.ScoreTransform = ScoreTransform; this.ModelParameters = linearModelParams (P, Solver, LambdaIn, ... [], Beta0, Bias0, ... Verbose, 'classification'); this.FitInfo_ = linearFitInfo (info, BetaTolerance, ... GradientTolerance, ... DeltaGradientTolerance, ... IterationLimit, Solver, PassLimit, ... BatchLimit); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {ClassificationLinear} {@var{labels} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {ClassificationLinear} {[@var{labels}, @var{scores}] =} predict (@var{obj}, @var{XC}) ## ## Classify new observations. ## ## @code{@var{labels} = predict (@var{obj}, @var{XC})} returns the class ## of largest score for each row of @var{XC}, in the type of the ## response the model was fitted to. With @math{L} regularization ## strengths @var{labels} has one column per strength. ## ## @code{[@var{labels}, @var{scores}] = predict (@var{obj}, @var{XC})} ## also returns the scores, an @math{Nx2} matrix whose columns follow ## @qcode{ClassNames}, or an @math{Nx2xL} array with more than one ## strength. The scores are @math{-f} and @math{+f} for the raw model ## value @math{f}, after @qcode{ScoreTransform} has been applied. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @end deftypefn function [labels, scores] = predict (this, XC) if (nargin < 2) error ("ClassificationLinear.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'ClassificationLinear.predict', XC); if (isempty (XC)) error ("ClassificationLinear.predict: XC is empty."); endif if (! (isnumeric (XC) && isreal (XC) && ismatrix (XC))) error ("ClassificationLinear.predict: invalid values in XC."); endif if (columns (XC) != this.NumPredictors_) error (strcat ("ClassificationLinear.predict: XC must have the", ... " same number of predictors as the trained model.")); endif if (! isempty (this.Coding_)) XC = dummyCoding (XC, this.Coding_); endif f = XC * this.Beta + this.Bias; ## A character matrix holds one name per row and cannot hold a grid of ## them, so a response given that way has nowhere to put the labels of ## more than one regularization strength. Refused rather than ## returned in a shape nothing can read. if (ischar (this.ClassNames) && numel (this.Lambda) > 1) error (strcat ("ClassificationLinear.predict: a character matrix", ... " response cannot carry labels for more than one", ... " value of 'Lambda'; give the response as a cell", ... " array of character vectors.")); endif ## A row missing a predictor has no score, and takes the class of ## largest prior, as MATLAB R2024a labels it whatever the cost. idx = 1 + (f > 0); [~, top] = max (this.Prior); idx(isnan (f)) = top; labels = labelsFromIndex (this.ClassNames, idx); if (nargout > 1) L = numel (this.Lambda); scores = zeros (rows (XC), 2, L); for l = 1:L scores(:,:,l) = this.STfun ([-f(:,l), f(:,l)]); endfor if (L == 1) scores = scores(:,:,1); endif endif endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationLinear} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationLinear} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationLinear} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margin of each observation. ## ## @code{@var{m} = margin (@var{obj}, @var{X}, @var{Y})} returns the ## score of the true class less the score of the other one, one row per ## observation and one column per regularization strength. A positive ## margin is a correct classification. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @end deftypefn function m = margin (this, X, Y) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationLinear.margin: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y] = tableResponse (this, 'margin', X, Y, {}, nargin > 2); [gY, errmsg] = labelIndices (this.ClassNames, Y); if (! isempty (errmsg)) error ("ClassificationLinear.margin: %s", errmsg); endif if (rows (X) != numel (gY)) error (strcat ("ClassificationLinear.margin: number of rows in", ... " X and Y must be equal.")); endif [~, scores] = predict (this, X); L = numel (this.Lambda); m = zeros (rows (X), L); for l = 1:L s = scores(:,:,l); strue = s(sub2ind (size (s), (1:rows (s))', gY)); sother = s(sub2ind (size (s), (1:rows (s))', 3 - gY)); m(:,l) = strue - sother; endfor endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationLinear} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationLinear} {@var{e} =} edge (@dots{}, @qcode{'Weights'}, @var{W}) ## @deftypefnx {ClassificationLinear} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationLinear} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Weighted mean of the classification margins. ## ## @code{@var{e} = edge (@var{obj}, @var{X}, @var{Y})} returns one value ## per regularization strength. The weights are normalized within each ## class to that class's prior before they are applied. ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @end deftypefn function e = edge (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationLinear.edge: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'edge', X, Y, varargin, ... nargin > 2); W = edgeWeights (varargin, Y, this.ClassNames, this.Prior, ... 'ClassificationLinear', 'edge'); m = margin (this, X, Y); e = sum (W .* m, 1) / sum (W); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationLinear} {@var{l} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationLinear} {@var{l} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationLinear} {@var{l} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {ClassificationLinear} {@var{l} =} loss (@dots{}, @var{name}, @var{value}) ## ## Classification loss on new data. ## ## @code{@var{l} = loss (@var{obj}, @var{X}, @var{Y})} returns the ## misclassification rate, one value per regularization strength. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{l} = loss (@dots{}, @var{name}, @var{value})} takes ## @qcode{'LossFun'}, one of @qcode{'binodeviance'}, ## @qcode{'classifcost'}, @qcode{'classiferror'}, @qcode{'exponential'}, ## @qcode{'hinge'}, @qcode{'logit'}, @qcode{'mincost'} and ## @qcode{'quadratic'}, and @qcode{'Weights'}. ## ## @end deftypefn function l = loss (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationLinear.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationLinear.loss: optional arguments", ... " must be given in Name-Value pairs.")); endif ## Parse optional paired arguments optNames = {'LossFun', 'Weights'}; ## An empty 'Weights' stands for uniform weights dfValues = {'classiferror', []}; [LossFun, Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments valid = {'binodeviance', 'classifcost', 'classiferror', ... 'exponential', 'hinge', 'logit', 'mincost', ... 'quadratic'}; if (! (ischar (LossFun) && any (strcmpi (LossFun, valid)))) error (strcat ("ClassificationLinear.loss: 'LossFun' must", ... " be 'binodeviance', 'classifcost',", ... " 'classiferror', 'exponential', 'hinge',", ... " 'logit', 'mincost', or 'quadratic'.")); endif LossFun = lower (LossFun); errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("ClassificationLinear.loss: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isreal (Weights) && isvector (Weights) && all (Weights >= 0))) error (strcat ("ClassificationLinear.loss: 'Weights' must", ... " be a vector of nonnegative values.")); endif if (! isempty (args)) error ("ClassificationLinear.loss: invalid optional paired argument."); endif [gY, errmsg] = labelIndices (this.ClassNames, Y); if (! isempty (errmsg)) error ("ClassificationLinear.loss: %s", errmsg); endif if (rows (X) != numel (gY)) error (strcat ("ClassificationLinear.loss: number of rows in X", ... " and Y must be equal.")); endif if (isempty (Weights)) w = ones (numel (gY), 1); else w = double (Weights(:)); if (numel (w) != numel (gY)) error (strcat ("ClassificationLinear.loss: 'Weights' must", ... " have one element per observation.")); endif endif w = w / sum (w); [~, scores] = predict (this, X); L = numel (this.Lambda); l = zeros (1, L); for k = 1:L s = scores(:,:,k); l(k) = classificationLoss (LossFun, s, gY, w, this.Cost); endfor endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationLinear} {@var{sub} =} selectModels (@var{obj}, @var{idx}) ## ## Keep a subset of the fitted regularization strengths. ## ## @code{@var{sub} = selectModels (@var{obj}, @var{idx})} returns a model ## holding only the strengths @var{idx} names, which may be indices into ## @qcode{Lambda} or a logical vector over it. ## ## @end deftypefn function sub = selectModels (this, idx) if (nargin < 2) error (strcat ("ClassificationLinear.selectModels: too few", ... " input arguments.")); endif L = numel (this.Lambda); if (islogical (idx)) if (numel (idx) != L) error (strcat ("ClassificationLinear.selectModels: a logical", ... " IDX must have one element per value of", ... " 'Lambda'.")); endif idx = find (idx); endif if (! (isnumeric (idx) && isreal (idx) && isvector (idx) && ! isempty (idx) && all (fix (idx) == idx) && all (idx >= 1) && all (idx <= L))) error (strcat ("ClassificationLinear.selectModels: IDX must", ... " hold integers between 1 and %d."), L); endif sub = this; sub.Lambda = this.Lambda(idx); sub.Beta = this.Beta(:,idx); sub.Bias = this.Bias(idx); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationLinear} {} savemodel (@var{obj}, @var{filename}) ## ## Save a linear classifier to a file. ## ## @code{savemodel (@var{obj}, @var{filename})} saves the model ## @var{obj} into @var{filename} in a form @code{loadmodel} can read ## back. ## ## @end deftypefn function savemodel (obj, fname) classdef_name = 'ClassificationLinear'; ClassNames = encodeLabels (obj.ClassNames); Prior = obj.Prior; Cost = obj.Cost; ScoreTransform = obj.ScoreTransform; PredictorNames = obj.PredictorNames; CategoricalPredictors = obj.CategoricalPredictors; ResponseName = obj.ResponseName; ExpandedPredictorNames = obj.ExpandedPredictorNames; Learner = obj.Learner; Beta = obj.Beta; Bias = obj.Bias; FittedLoss = obj.FittedLoss; Lambda = obj.Lambda; ModelParameters = obj.ModelParameters; Regularization = obj.Regularization; NumPredictors_ = obj.NumPredictors_; Coding_ = obj.Coding_; save ('-binary', fname, 'classdef_name', 'ClassNames', 'Prior', ... 'Cost', 'ScoreTransform', 'PredictorNames', ... 'CategoricalPredictors', 'ResponseName', ... 'ExpandedPredictorNames', 'Learner', 'Beta', 'Bias', ... 'FittedLoss', 'Lambda', 'ModelParameters', 'Regularization', ... 'NumPredictors_', 'Coding_'); endfunction endmethods methods (Access = public, Hidden) function disp (this) printf ("\n ClassificationLinear\n\n"); printf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); printf ("%+25s: %s\n", 'ClassNames', classNameListing (this.ClassNames)); printf ("%+25s: '%s'\n", 'ScoreTransform', this.ScoreTransform); printf ("%+25s: [%dx%d double]\n", 'Beta', rows (this.Beta), ... columns (this.Beta)); if (numel (this.Bias) == 1) printf ("%+25s: %g\n", 'Bias', this.Bias); printf ("%+25s: %g\n", 'Lambda', this.Lambda); else printf ("%+25s: [1x%d double]\n", 'Bias', numel (this.Bias)); printf ("%+25s: [1x%d double]\n", 'Lambda', numel (this.Lambda)); endif printf ("%+25s: '%s'\n", 'Learner', this.Learner); printf ("\n"); endfunction ## What the fit reported, which fitclinear returns as its second output. function S = fitInfo_ (this) S = this.FitInfo_; endfunction ## Custom setter, so that assigning a name or a handle updates both the ## text the property reports and the callable predict uses. function this = set.ScoreTransform (this, val) [this.STfun, this.ScoreTransform] = ... parseScoreTransform (val, 'ClassificationLinear'); endfunction endmethods methods (Static, Hidden) function mdl = load_model (filename, data) data = decodeLabels (data); mdl = ClassificationLinear (zeros (2, 1), [0; 1]); fields = fieldnames (data); for k = 1:numel (fields) mdl.(fields{k}) = data.(fields{k}); endfor endfunction endmethods endclassdef %!demo %! ## Separate the two overlapping iris species with a linear classifier and %! ## read the posterior probability it gives each observation. %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mdl = ClassificationLinear (X, Y, 'Learner', 'logistic') %! [label, score] = predict (Mdl, X([1, 51],:)) %!demo %! ## One object can hold a whole regularization path. A stronger penalty %! ## shrinks the coefficients, and every method reports one column per %! ## strength. %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mdl = ClassificationLinear (X, Y, 'Lambda', [0.001, 0.01, 0.1]); %! Mdl.Beta %! loss (Mdl, X, Y) %!test %! ## The model reports the surface MATLAB reports %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mdl = ClassificationLinear (X, Y); %! assert_equal (class (Mdl), 'ClassificationLinear'); %! assert_equal (Mdl.ClassNames, {'versicolor'; 'virginica'}); %! assert_equal (Mdl.Learner, 'svm'); %! assert_equal (Mdl.FittedLoss, 'hinge'); %! assert_equal (Mdl.Regularization, 'ridge (L2)'); %! assert_equal (Mdl.ScoreTransform, 'none'); %! assert_equal (Mdl.ResponseName, 'Y'); %! assert_equal (Mdl.PredictorNames, {'x1', 'x2', 'x3', 'x4'}); %! assert_equal (Mdl.ExpandedPredictorNames, {'x1', 'x2', 'x3', 'x4'}); %! assert_equal (Mdl.CategoricalPredictors, []); %! assert_equal (Mdl.Prior, [0.5, 0.5]); %! assert_equal (Mdl.Cost, [0, 1; 1, 0]); %! assert_equal (Mdl.Lambda, 0.01); %! assert_equal (size (Mdl.Beta), [4, 1]); %!test %! ## The properties are the ones MATLAB lists, in its order %! load fisheriris %! Mdl = ClassificationLinear (meas(51:end,:), species(51:end)); %! assert_equal (sort (properties (Mdl)), ... %! sort ({'ClassNames'; 'Prior'; 'Cost'; 'ScoreTransform'; ... %! 'PredictorNames'; 'CategoricalPredictors'; ... %! 'ResponseName'; 'ExpandedPredictorNames'; ... %! 'Learner'; 'Beta'; 'Bias'; 'FittedLoss'; 'Lambda'; ... %! 'ModelParameters'; 'Regularization'})); %!test %! ## A logistic ridge fit reproduces R2024a's coefficients %! load fisheriris %! Mdl = ClassificationLinear (meas(51:end,:), species(51:end), ... %! 'Learner', 'logistic', 'Solver', 'lbfgs', ... %! 'BetaTolerance', 0, 'GradientTolerance', ... %! 1e-12, ... %! 'IterationLimit', 20000); %! assert_equal (Mdl.Beta, [-0.394433478724131; -0.513277404049627; ... %! 2.9307513837558; 2.41703218835114], 1e-7); %! assert_equal (Mdl.Bias, -14.4307581801051, 1e-7); %! assert_equal (Mdl.FittedLoss, 'logit'); %! assert_equal (Mdl.ScoreTransform, 'logit'); %!test %! ## The default score transform follows the learner, and only the logistic %! ## one turns the scores into posterior probabilities %! load fisheriris %! X = meas(51:end,:); %! Ml = ClassificationLinear (X, species(51:end), 'Learner', 'logistic'); %! Ms = ClassificationLinear (X, species(51:end), 'Learner', 'svm'); %! assert_equal (Ml.ScoreTransform, 'logit'); %! assert_equal (Ms.ScoreTransform, 'none'); %! [~, sl] = predict (Ml, X(1:5,:)); %! [~, ss] = predict (Ms, X(1:5,:)); %! assert_equal (sum (sl, 2), ones (5, 1), 1e-12); %! assert_equal (ss(:,1), -ss(:,2), 1e-12); %!test %! ## The scores of a logistic fit are R2024a's posteriors %! load fisheriris %! X = meas(51:end,:); %! Mdl = ClassificationLinear (X, species(51:end), 'Learner', 'logistic', ... %! 'Solver', 'lbfgs', 'BetaTolerance', 0, ... %! 'GradientTolerance', 1e-12, ... %! 'IterationLimit', 20000); %! [label, score] = predict (Mdl, X(1:2,:)); %! assert_equal (label, {'versicolor'; 'versicolor'}); %! assert_equal (score, [0.842361345526662, 0.157638654473338; ... %! 0.856151985655854, 0.143848014344146], 1e-7); %!test %! ## margin is the true class score less the other, and edge their weighted %! ## mean. Both halves are asserted: the values against R2024a, and the %! ## relationship against the scores this model actually reports. The %! ## values alone would not catch a margin computed consistently wrongly, %! ## since a wrong-but-stable margin agrees with its own past output %! ## forever; the relationship alone would not catch our agreeing with %! ## ourselves about the wrong quantity. %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mdl = ClassificationLinear (X, Y, 'Learner', 'logistic', ... %! 'Solver', 'lbfgs', 'BetaTolerance', 0, ... %! 'GradientTolerance', 1e-12, ... %! 'IterationLimit', 20000); %! assert_equal (margin (Mdl, X(1,:), Y(1)), 0.684722691053324, 1e-7); %! assert_equal (edge (Mdl, X, Y), 0.726067240395032, 1e-7); %! ## the invariant, independent of any solver output %! [~, score] = predict (Mdl, X); %! virg = strcmp (Y, 'virginica'); %! strue = score(:,1); %! strue(virg) = score(virg,2); %! sother = score(:,2); %! sother(virg) = score(virg,1); %! assert_equal (margin (Mdl, X, Y), strue - sother, 1e-15); %! assert_equal (edge (Mdl, X, Y), mean (margin (Mdl, X, Y)), 1e-15); %!test # MATLAB parity: edge on a set holding one class %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mdl = ClassificationLinear (X, Y); %! assert_equal (edge (Mdl, X(1:50,:), Y(1:50)), 3.3451311639, 1e-10); %!test %! ## Every loss reproduces R2024a, and each is a function of the score the %! ## model gives the true class rather than of the margin %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mdl = ClassificationLinear (X, Y, 'Learner', 'logistic', ... %! 'Solver', 'lbfgs', 'BetaTolerance', 0, ... %! 'GradientTolerance', 1e-12, ... %! 'IterationLimit', 20000); %! assert_equal (loss (Mdl, X, Y), 0.04, 1e-12); %! assert_equal (loss (Mdl, X, Y, 'LossFun', 'hinge'), ... %! 0.136966379802484, 1e-7); %! assert_equal (loss (Mdl, X, Y, 'LossFun', 'logit'), ... %! 0.354333320227268, 1e-7); %! assert_equal (loss (Mdl, X, Y, 'LossFun', 'binodeviance'), ... %! 0.170050306584128, 1e-7); %! assert_equal (loss (Mdl, X, Y, 'LossFun', 'exponential'), ... %! 0.426899191073676, 1e-7); %! assert_equal (loss (Mdl, X, Y, 'LossFun', 'quadratic'), ... %! 0.040701761220091, 1e-7); %!test %! ## Under the default cost both cost based losses reduce to the error %! ## rate: the class of least expected cost is then the class of largest %! ## score, which is what predict returns %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mdl = ClassificationLinear (X, Y, 'Learner', 'logistic'); %! assert_equal (loss (Mdl, X, Y, 'LossFun', 'classifcost'), ... %! loss (Mdl, X, Y), 1e-12); %! assert_equal (loss (Mdl, X, Y, 'LossFun', 'mincost'), ... %! loss (Mdl, X, Y), 1e-12); %!test %! ## A cost that is not symmetric moves the least cost assignment away from %! ## the largest score, so the two cost based losses part company %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mdl = ClassificationLinear (X, Y, 'Learner', 'logistic', ... %! 'Cost', [0, 2; 3, 0]); %! assert_equal (loss (Mdl, X, Y, 'LossFun', 'classifcost') > 0, true); %! assert_equal (loss (Mdl, X, Y, 'LossFun', 'mincost') > 0, true); %!test %! ## A cost matrix reaches the fit through the prior, which is what MATLAB %! ## does: a fit costing four times as much to miss the first class equals %! ## one given the prior that scaling implies %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mc = ClassificationLinear (X, Y, 'Cost', [0, 4; 1, 0]); %! Mp = ClassificationLinear (X, Y, 'Prior', [0.8, 0.2]); %! assert_equal (Mc.Beta, Mp.Beta, 1e-12); %! assert_equal (Mc.Bias, Mp.Bias, 1e-12); %! assert_equal (Mc.Prior, [0.5, 0.5]); %!test %! ## Lambda defaults to the reciprocal of the observations that were used %! load fisheriris %! Mdl = ClassificationLinear (meas(51:end,:), species(51:end)); %! assert_equal (Mdl.Lambda, 1 / 100, 1e-15); %!test %! ## A vector of strengths fits one model per value, sorted ascending, and %! ## every method reports one column per value %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mdl = ClassificationLinear (X, Y, 'Lambda', [0.1, 0.001, 0.01]); %! assert_equal (Mdl.Lambda, [0.001, 0.01, 0.1]); %! assert_equal (size (Mdl.Beta), [4, 3]); %! assert_equal (size (Mdl.Bias), [1, 3]); %! [label, score] = predict (Mdl, X(1:4,:)); %! assert_equal (size (label), [4, 3]); %! assert_equal (size (score), [4, 2, 3]); %! assert_equal (size (margin (Mdl, X, Y)), [100, 3]); %! assert_equal (size (edge (Mdl, X, Y)), [1, 3]); %! assert_equal (size (loss (Mdl, X, Y)), [1, 3]); %!test %! ## A stronger penalty shrinks the coefficients %! load fisheriris %! Mdl = ClassificationLinear (meas(51:end,:), species(51:end), ... %! 'Learner', 'logistic', 'Lambda', [0.001, 1]); %! assert_equal (norm (Mdl.Beta(:,1)) > norm (Mdl.Beta(:,2)), true); %!test %! ## selectModels keeps the strengths it is given and drops the rest %! load fisheriris %! Mdl = ClassificationLinear (meas(51:end,:), species(51:end), ... %! 'Lambda', [0.001, 0.01, 0.1]); %! sub = selectModels (Mdl, [1, 3]); %! assert_equal (sub.Lambda, [0.001, 0.1]); %! assert_equal (size (sub.Beta), [4, 2]); %! assert_equal (sub.Beta, Mdl.Beta(:,[1, 3])); %! sub = selectModels (Mdl, logical ([0, 1, 0])); %! assert_equal (sub.Lambda, 0.01); %!test %! ## A lasso penalty drives coefficients to exactly zero, which a ridge %! ## penalty never does %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Ml = ClassificationLinear (X, Y, 'Learner', 'logistic', ... %! 'Regularization', 'lasso', 'Lambda', 0.05); %! Mr = ClassificationLinear (X, Y, 'Learner', 'logistic', ... %! 'Regularization', 'ridge', 'Lambda', 0.05); %! assert_equal (Ml.Regularization, 'lasso (L1)'); %! assert_equal (sum (Ml.Beta == 0) > 0, true); %! assert_equal (any (Mr.Beta == 0), false); %!test %! ## The default solver follows the penalty and the width of the data %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! [~, F] = fitclinear (X, Y); %! assert_equal (F.Solver, {'bfgs'}); %! [~, F] = fitclinear (X, Y, 'Regularization', 'lasso'); %! assert_equal (F.Solver, {'sparsa'}); %!test %! ## FitBias false leaves the intercept at zero %! load fisheriris %! Mdl = ClassificationLinear (meas(51:end,:), species(51:end), ... %! 'FitBias', false); %! assert_equal (Mdl.Bias, 0); %!test %! ## Observations may be given down the columns instead %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mr = ClassificationLinear (X, Y, 'Learner', 'logistic'); %! Mc = ClassificationLinear (X', Y, 'Learner', 'logistic', ... %! 'ObservationsIn', 'columns'); %! assert_equal (Mr.Beta, Mc.Beta, 1e-12); %!test %! ## A row with a missing predictor or a missing response is dropped, and %! ## Lambda follows the count that survived %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! X(3,2) = NaN; %! Mdl = ClassificationLinear (X, Y); %! assert_equal (Mdl.Lambda, 1 / 99, 1e-15); %!test %! ## ScoreTransform is settable and reaches predict %! load fisheriris %! X = meas(51:end,:); %! Mdl = ClassificationLinear (X, species(51:end)); %! Mdl.ScoreTransform = 'logit'; %! assert_equal (Mdl.ScoreTransform, 'logit'); %! [~, score] = predict (Mdl, X(1:3,:)); %! assert_equal (sum (score, 2), ones (3, 1), 1e-12); %!test %! ## A saved model reads back as the same model %! load fisheriris %! X = meas(51:end,:); %! Mdl = ClassificationLinear (X, species(51:end), 'Learner', 'logistic'); %! fname = tempname (); %! savemodel (Mdl, fname); %! Mnew = loadmodel (fname); %! delete (fname); %! assert_equal (class (Mnew), 'ClassificationLinear'); %! assert_equal (Mnew.Beta, Mdl.Beta); %! assert_equal (Mnew.Bias, Mdl.Bias); %! assert_equal (predict (Mnew, X(1:5,:)), predict (Mdl, X(1:5,:))); %!test %! ## The labels come back in the type the response was given in, a %! ## character matrix included: its rows name the classes, and indexing it %! ## by element rather than by row would flatten it into single characters %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mcell = ClassificationLinear (X, Y); %! assert_equal (class (predict (Mcell, X(1:3,:))), 'cell'); %! Mchar = ClassificationLinear (X, char (Y)); %! assert_equal (size (Mchar.ClassNames), [2, 10]); %! assert_equal (size (predict (Mchar, X(1:3,:))), [3, 10]); %! assert_equal (loss (Mchar, X, char (Y)), loss (Mcell, X, Y), 1e-12); %! Ynum = double (strcmp (Y, 'virginica')); %! Mnum = ClassificationLinear (X, Ynum); %! assert_equal (class (predict (Mnum, X(1:3,:))), 'double'); %! assert_equal (Mnum.ClassNames, [0; 1]); %! Mlog = ClassificationLinear (X, logical (Ynum)); %! assert_equal (class (predict (Mlog, X(1:3,:))), 'logical'); %!test %! ## Observation weights reach the fit, and the prior follows them. Both %! ## numbers are R2024a's for the weights 1 to 100. %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! Mdl = ClassificationLinear (X, Y, 'Learner', 'logistic', ... %! 'Solver', 'lbfgs', 'Weights', (1:100)', ... %! 'BetaTolerance', 0, ... %! 'GradientTolerance', 1e-12, ... %! 'IterationLimit', 20000); %! assert_equal (Mdl.Prior, [0.252475247524752, 0.747524752475248], 1e-12); %! assert_equal (Mdl.Beta, [-0.0907861095243143; -0.344147850956068; ... %! 2.73449884653132; 2.20876095012392], 1e-7); %! assert_equal (Mdl.Bias, -14.6291356484861, 1e-6); %! assert_equal (Mdl.FitInfo_.Objective, 0.208158687811647, 1e-10); %!test %! ## A character matrix response names one class per row, and a model %! ## fitted from one is the model fitted from the equivalent cell array. %! ## The check is worth its length: a linear index into a character matrix %! ## flattens the names into single characters, and so does an ismember %! ## between two character matrices, and neither shows up at the property %! ## that is usually looked at. %! load fisheriris %! X = meas(51:end,:); %! Yc = species(51:end); %! Ym = char (Yc); %! Mc = ClassificationLinear (X, Yc, 'Learner', 'logistic'); %! Mm = ClassificationLinear (X, Ym, 'Learner', 'logistic'); %! assert_equal (cellstr (Mm.ClassNames), Mc.ClassNames); %! assert_equal (Mm.Prior, Mc.Prior); %! assert_equal (Mm.Beta, Mc.Beta); %! assert_equal (Mm.Bias, Mc.Bias); %!test %! ## Every method answers the same through a character matrix response as %! ## through the cell array it came from %! load fisheriris %! X = meas(51:end,:); %! Yc = species(51:end); %! Ym = char (Yc); %! Mc = ClassificationLinear (X, Yc, 'Learner', 'logistic'); %! Mm = ClassificationLinear (X, Ym, 'Learner', 'logistic'); %! assert_equal (cellstr (predict (Mm, X)), predict (Mc, X)); %! assert_equal (margin (Mm, X, Ym), margin (Mc, X, Yc)); %! assert_equal (edge (Mm, X, Ym), edge (Mc, X, Yc)); %! assert_equal (edge (Mm, X, Ym, 'Weights', (1:100)'), ... %! edge (Mc, X, Yc, 'Weights', (1:100)')); %! assert_equal (loss (Mm, X, Ym), loss (Mc, X, Yc)); %! assert_equal (loss (Mm, X, Ym, 'LossFun', 'hinge'), ... %! loss (Mc, X, Yc, 'LossFun', 'hinge')); %! assert_equal (loss (Mm, X, Ym, 'LossFun', 'mincost'), ... %! loss (Mc, X, Yc, 'LossFun', 'mincost')); %!test %! ## 'ClassNames' selects a subset when it is given as a character matrix %! ## too, which needs both sides of the comparison turned into names %! load fisheriris %! Mcell = ClassificationLinear (meas, species, ... %! 'ClassNames', {'versicolor', 'virginica'}); %! Mchar = ClassificationLinear (meas, char (species), 'ClassNames', ... %! char ({'versicolor', 'virginica'})); %! assert_equal (cellstr (Mchar.ClassNames), Mcell.ClassNames); %! assert_equal (Mchar.Beta, Mcell.Beta); %! assert_equal (Mchar.Lambda, Mcell.Lambda); %!test %! ## Names of unequal length are padded by the character matrix and the %! ## padding is not part of the name %! load fisheriris %! X = meas(51:end,:); %! Y = [repmat({'ab'}, 50, 1); repmat({'abcd'}, 50, 1)]; %! Mcell = ClassificationLinear (X, Y); %! Mchar = ClassificationLinear (X, char (Y)); %! assert_equal (cellstr (Mchar.ClassNames), Mcell.ClassNames); %! assert_equal (cellstr (predict (Mchar, X)), predict (Mcell, X)); %!test # MATLAB parity: 'bfgs' keeps the full inverse Hessian %! load fisheriris %! [Mdl, FI] = fitclinear (meas(51:end,:), species(51:end), ... %! 'Learner', 'logistic', 'Lambda', 1e-10, ... %! 'Solver', 'bfgs'); %! assert_equal (FI.NumIterations, 71); %! assert_equal (FI.TerminationCode, 1); %! assert_equal (Mdl.Beta', [-2.465354655400692, -6.680676306572678, ... %! 9.429341643431608, 18.2859364726242], 1e-9); %! assert_equal (Mdl.Bias, -42.6370704726009, 1e-9); %!test # MATLAB parity: 'lbfgs' keeps fifteen curvature pairs %! load fisheriris %! [Mdl, FI] = fitclinear (meas(51:end,:), species(51:end), ... %! 'Learner', 'logistic', 'Lambda', 1e-10, ... %! 'Solver', 'lbfgs'); %! assert_equal (FI.NumIterations, 40); %! assert_equal (FI.TerminationCode, 1); %! assert_equal (Mdl.Beta', [-2.465511460111347, -6.67942802197831, ... %! 9.429867603796366, 18.28403561808411], 1e-9); %! assert_equal (Mdl.Bias, -42.6390545166892, 1e-9); %!test # MATLAB parity: the first step of 'bfgs' starts from a unit one %! load fisheriris %! [Mdl, FI] = fitclinear (100 * meas(51:end,:), species(51:end), ... %! 'Learner', 'logistic', 'Lambda', 1e-10, ... %! 'Solver', 'bfgs'); %! assert_equal (FI.NumIterations, 46); %! assert_equal (Mdl.Beta', [-0.06327372376455137, -0.06618088890242214, ... %! 0.0843323334344509, 0.1028323453382017], 1e-12); %!test # MATLAB parity: 'sparsa' stops on the gradient with BetaTolerance 0 %! load fisheriris %! [~, FI] = fitclinear (meas(51:end,:), species(51:end), ... %! 'Learner', 'logistic', 'Lambda', 1e-10, ... %! 'Regularization', 'lasso', 'Solver', 'sparsa', ... %! 'BetaTolerance', 0); %! assert_equal (FI.TerminationCode, 2); %! assert_equal (FI.TerminationStatus, {'Tolerance on gradient satisfied.'}); %! assert_equal (FI.RelativeChangeInBeta, NaN); %!test %! ## MATLAB parity: with both tolerances at zero neither tolerance test can %! ## fire, so the fit ends either by the line search failing to improve or by %! ## the iteration limit, whichever the platform's arithmetic reaches first. %! ## R2024a reports the first; that is not reproducible across toolchains, so %! ## only the tolerance codes are ruled out here. %! load fisheriris %! [~, FI] = fitclinear (meas(51:end,:), species(51:end), ... %! 'Learner', 'logistic', 'Lambda', 1e-10, ... %! 'Regularization', 'lasso', 'Solver', 'sparsa', ... %! 'BetaTolerance', 0, 'GradientTolerance', 0); %! assert_equal (any (FI.TerminationCode == [-11, 0]), true); %!test %! ## The default fit stops on the coefficients, as MATLAB's does, which is %! ## only true once the engine offers BetaTolerance. Before it did, this %! ## fit ran on to the gradient tolerance and reported NaN here. %! load fisheriris %! Mdl = ClassificationLinear (meas(51:end,:), species(51:end), ... %! 'Learner', 'logistic'); %! assert_equal (Mdl.FitInfo_.BetaTolerance, 1e-4); %! assert_equal (Mdl.FitInfo_.TerminationCode, 1); %! assert_equal (Mdl.FitInfo_.TerminationStatus, ... %! {'Tolerance on coefficients satisfied.'}); %! assert_equal (isfinite (Mdl.FitInfo_.RelativeChangeInBeta), true); %!test %! ## A line search that cannot improve the objective says so, rather than %! ## reporting the iteration limit it never reached. Code and wording %! ## measured on R2024a. Where the search gives up varies by platform, so %! ## the count is only asserted to be short of the limit. %! load fisheriris %! Mdl = ClassificationLinear (meas(51:end,:), species(51:end), ... %! 'Learner', 'logistic', 'Lambda', 1e-10, ... %! 'BetaTolerance', 0, 'GradientTolerance', 0); %! assert_equal (Mdl.FitInfo_.TerminationCode, -11); %! assert_equal (Mdl.FitInfo_.TerminationStatus, ... %! {'Unable to find a step decreasing the objective.'}); %! S = Mdl.fitInfo_ (); %! assert_equal (S.NumIterations < S.IterationLimit, true); %!test %! ## A tolerance of exactly zero switches its test off and the quantity it %! ## governs comes back NaN, which is what MATLAB reports. Not "never %! ## satisfied": not computed. %! load fisheriris %! Mdl = ClassificationLinear (meas(51:end,:), species(51:end), ... %! 'BetaTolerance', 0, 'GradientTolerance', 1e-8); %! assert_equal (isnan (Mdl.FitInfo_.RelativeChangeInBeta), true); %! Mdl = ClassificationLinear (meas(51:end,:), species(51:end), ... %! 'GradientTolerance', 0); %! assert_equal (isnan (Mdl.FitInfo_.GradientNorm), true); %! assert_equal (Mdl.FitInfo_.GradientTolerance, 0); %!test %! ## A character matrix response survives a round trip through savemodel %! ## and loadmodel, names and all %! load fisheriris %! X = meas(51:end,:); %! Ym = char (species(51:end)); %! Mdl = ClassificationLinear (X, Ym, 'Learner', 'logistic'); %! fname = tempname (); %! savemodel (Mdl, fname); %! Mnew = loadmodel (fname); %! delete (fname); %! assert_equal (Mnew.ClassNames, Mdl.ClassNames); %! assert_equal (size (Mnew.ClassNames), [2, 10]); %! assert_equal (predict (Mnew, X(1:5,:)), predict (Mdl, X(1:5,:))); %! assert_equal (loss (Mnew, X, Ym), loss (Mdl, X, Ym), 1e-12); %!test %! ## ModelParameters reports every option but gives a value only to the %! ## ones the chosen solver can act on, which is MATLAB's behaviour and was %! ## measured one fit per solver on R2024a. Two of these are easy to get %! ## wrong: HessianHistorySize is empty for 'bfgs' and 15 for 'lbfgs', a %! ## full quasi-Newton method having no limited memory to size, and the two %! ## solvers that run no gradient test report a tolerance of zero. %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! b = ClassificationLinear (X, Y, 'Solver', 'bfgs').ModelParameters; %! assert_equal (numel (fieldnames (b)), 28); %! assert_equal (b.HessianHistorySize, []); %! assert_equal (b.BatchSize, []); %! assert_equal (b.PassLimit, []); %! assert_equal (b.IterationLimit, 1000); %! assert_equal (b.LineSearch, 'weakwolfe'); %! l = ClassificationLinear (X, Y, 'Solver', 'lbfgs').ModelParameters; %! assert_equal (l.HessianHistorySize, 15); %! g = ClassificationLinear (X, Y, 'Solver', 'sgd').ModelParameters; %! assert_equal (g.BatchSize, 10); %! assert_equal (g.IterationLimit, []); %! assert_equal (g.GradientTolerance, 0); %! assert_equal (g.LineSearch, []); %! d = ClassificationLinear (X, Y, 'Solver', 'dual').ModelParameters; %! assert_equal (isempty (d.DeltaGradientTolerance), false); %! assert_equal (d.PassLimit, 10); %! assert_equal (d.HessianHistorySize, []); %! assert_equal (d.GradientTolerance, 0); %!test %! ## The default learning rate is MATLAB's own formula, the reciprocal root %! ## of one plus the largest squared observation norm. R2024a reports %! ## 0.0896365435911655 on this fixture. %! load fisheriris %! X = meas(51:end,:); %! Mdl = ClassificationLinear (X, species(51:end), 'Solver', 'sgd'); %! assert_equal (Mdl.ModelParameters.LearnRate, ... %! 1 / sqrt (1 + max (sum (X .^ 2, 2))), 1e-15); %! assert_equal (Mdl.ModelParameters.LearnRate, 0.0896365435911655, 1e-12); %!test %! ## The dual solver's two defaults, measured on R2024a and confirmed on %! ## R2026a through MATLAB Online. Both differ from what MathWorks %! ## documents, and the first differs between the two learners: the %! ## complementarity tolerance is 1 for a hinge loss and 0.1 for an %! ## epsilon-insensitive one, which is the 0.1 the documentation quotes. %! load fisheriris %! Mdl = ClassificationLinear (meas(51:end,:), species(51:end), ... %! 'Solver', 'dual'); %! assert_equal (Mdl.ModelParameters.DeltaGradientTolerance, 1); %! assert_equal (Mdl.ModelParameters.NumCheckConvergence, 2); %! assert_equal (Mdl.ModelParameters.PassLimit, 10); %!test # An unused category of a categorical response is not a class %! load fisheriris %! y = categorical (species); %! Mdl = ClassificationLinear (meas(51:150,:), y(51:150)); %! assert_equal (cellstr (Mdl.ClassNames), {'versicolor'; 'virginica'}); %!test # A row missing a predictor takes the class of largest prior %! X = [(1:10)', mod((1:10)', 3)]; %! y = [1; 1; 1; 1; 1; 1; 2; 2; 2; 2]; %! Mdl = ClassificationLinear (X, y); %! [label, score] = predict (Mdl, [NaN, 1]); %! assert_equal (label, 1); %! assert_equal (score, [NaN, NaN]); %! Mdl = ClassificationLinear (X, y, 'Prior', [0.3, 0.7]); %! assert_equal (predict (Mdl, [NaN, 1]), 2); ## Test input validation %!error ... %! ClassificationLinear (ones (5, 2)) %!error ... %! ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Learner') %!error ... %! ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Learner', ... %! 'tree') %!error ... %! ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'Regularization', 'elastic') %!error ... %! ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Lambda', -1) %!error ... %! ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Solver', ... %! 'newton') %!error ... %! ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'FitBias', ... %! 'yes') %!error ... %! ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'ObservationsIn', 'pages') %!error ... %! ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'BetaTolerance', -1) %!error ... %! ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'IterationLimit', 2.5) %!error ... %! ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Verbose', 3) %!error ... %! ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Nonsense', 1) %!error ... %! ClassificationLinear ({1, 2; 3, 4}, [1; 2]) %!error ClassificationLinear ([], []) %!error ... %! ClassificationLinear (ones (10, 2), [1; 2]) %!error ... %! ClassificationLinear (ones (9, 2), [1; 1; 1; 2; 2; 2; 3; 3; 3]) %!error ... %! ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Prior', ... %! [0.2, 0.3, 0.5]) %!error ... %! ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Cost', ... %! ones (3)) %!error ... %! ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'Regularization', 'ridge', 'Solver', 'sparsa') %!error ... %! ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'Regularization', 'lasso', 'Solver', 'lbfgs') %!error ... %! ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Learner', ... %! 'logistic', 'Solver', 'dual') %!error ... %! ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'PredictorNames', {'a', 'b', 'c'}) %!error ... %! ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Beta', ... %! ones (3, 1)) %!error ... %! ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Bias', [1, 2]) %!error ... %! predict (ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)])) %!error ... %! predict (ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)]), []) %!error ... %! predict (ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)]), ... %! ones (3, 5)) %!error ... %! margin (ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)]), ... %! ones (3, 2)) %!error ... %! loss (ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)]), ... %! ones (10, 2), [ones(5,1); 2*ones(5,1)], 'LossFun', 'mse') %!error ... %! loss (ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)]), ... %! ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Nonsense', 1) %!error ... %! loss (ClassificationLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)]), ... %! ones (10, 2), 3*ones (10, 1)) %!error ... %! selectModels (ClassificationLinear (ones (10, 2), ... %! [ones(5,1); 2*ones(5,1)], 'Lambda', [0.1, 0.2, 0.3]), 5) %!error ... %! selectModels (ClassificationLinear (ones (10, 2), ... %! [ones(5,1); 2*ones(5,1)], 'Lambda', [0.1, 0.2, ... %! 0.3]), logical ([1, 0])) %!error ... %! predict (ClassificationLinear (ones (10, 2), ... %! ['a';'a';'a';'a';'a';'b';'b';'b';'b';'b'], 'Lambda', ... %! [0.1, 0.2]), ones (3, 2)) ## Every documented score transform reaches the scores that are reported, and ## none of them moves the label: a transform reshapes what is reported, not ## what is decided. %!test %! load fisheriris %! Mdl = fitclinear (meas, strcmp (species, 'setosa')); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! T = {'identity', @(x) x; 'doublelogit', @(x) 1 ./ (1 + exp (-2 * x)); ... %! 'invlogit', @(x) log (x ./ (1 - x)); ... %! 'logit', @(x) 1 ./ (1 + exp (-x)); ... %! 'sign', @(x) sign (x); 'symmetric', @(x) 2 * x - 1; ... %! 'symmetriclogit', @(x) 2 ./ (1 + exp (-x)) - 1}; %! for i = 1:rows (T) %! Mdl.ScoreTransform = T{i,1}; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, T{i,2}(raw), 1e-12); %! assert_equal (l, label); %! endfor %! ## ismax marks the largest score of each observation, ties to the first. %! [~, k] = max (raw, [], 2); %! e = zeros (size (raw)); %! e(sub2ind (size (raw), (1:rows (raw))', k)) = 1; %! Mdl.ScoreTransform = 'ismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, e); %! Mdl.ScoreTransform = 'symmetricismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, 2 * e - 1); ## A function handle is taken as given and applied to the scores. %!test %! load fisheriris %! Mdl = fitclinear (meas, strcmp (species, 'setosa')); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! Mdl.ScoreTransform = @(x) x .^ 2; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, raw .^ 2, 1e-12); %! assert_equal (l, label); %!shared Xc, Dc, yc %! c1 = repmat ([1; 2; 3], 20, 1); %! x2 = sin ((1:60)'); %! c3 = repmat ([10; 10; 20; 20], 15, 1); %! Xc = [c1, x2, c3]; %! Dc = [c1 == 1, c1 == 2, c1 == 3, x2, c3 == 10, c3 == 20]; %! yc = 5 * (c1 == 2) + 0.5 * x2 - 3 * (c3 == 20) + 0.1 * cos ((1:60)') > 1; %!test # MATLAB parity: a categorical predictor is dummy coded in its place %! Mdl = ClassificationLinear (Xc, yc, 'CategoricalPredictors', [1, 3]); %! assert_equal (Mdl.CategoricalPredictors, [1, 3]); %! assert_equal (Mdl.ExpandedPredictorNames, {'x1 == 1', 'x1 == 2', ... %! 'x1 == 3', 'x2', 'x3 == 10', 'x3 == 20'}); %! assert_equal (size (Mdl.Beta), [6, 1]); %! H = ClassificationLinear (Dc, yc); %! assert_equal (Mdl.Beta, H.Beta, 1e-12); %! [~, s] = predict (Mdl, Xc(1:5,:)); %! [~, sh] = predict (H, Dc(1:5,:)); %! assert_equal (s, sh, 1e-12); %!test # a logical vector names the categorical predictors too %! Mdl = ClassificationLinear (Xc, yc, 'CategoricalPredictors', ... %! logical ([1, 0, 1])); %! assert_equal (Mdl.CategoricalPredictors, [1, 3]); %!test # a level the training data did not hold is scored NaN %! Mdl = ClassificationLinear (Xc, yc, 'CategoricalPredictors', [1, 3]); %! [~, s] = predict (Mdl, [4, 0, 10; 2.5, 0, 20; NaN, 0, 10; 2, 0, 20]); %! assert_equal (isnan (s(:,1))', [true, true, true, false]); %!error ... %! ClassificationLinear (Xc, yc, 'CategoricalPredictors', 4) %!error ... %! ClassificationLinear (Xc, yc, 'CategoricalPredictors', logical ([1, 0])) %!error ... %! ClassificationLinear (Xc, yc, 'CategoricalPredictors', 0) ## A categorical or string ClassNames %!test # the classes may be named as a categorical, as the other learners take %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! Mdl = ClassificationLinear (meas(inds,:), categorical (species(inds)), ... %! 'ClassNames', ... %! categorical ({'versicolor', 'virginica'})); %! assert_equal (class (Mdl.ClassNames), 'categorical'); %!test # or as a string array %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! Mdl = ClassificationLinear (meas(inds,:), string (species(inds)), ... %! 'ClassNames', ... %! string ({'versicolor', 'virginica'})); %! assert_equal (numel (Mdl.ClassNames), 2); ## A response of levels survives cross-validation, which passes the classes ## it read down to the model of each fold %!test %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! CVMdl = fitclinear (meas(inds,:), categorical (species(inds)), ... %! 'KFold', 3); %! assert_equal (class (CVMdl), 'ClassificationPartitionedLinear'); %! assert_equal (CVMdl.KFold, 3); %!error ... %! ClassificationLinear ([1, 2; 3, 4], [1; 2], 'ClassNames', {1, 2}) ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(51:150,1:2); %! y = categorical (species(51:150)); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitclinear (T, 'Species'); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'Species'), a); %! assert_equal (loss (Mdl, T), a); ## A table at edge %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(51:150,1:2); %! y = categorical (species(51:150)); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitclinear (T, 'Species'); %! a = edge (Mdl, X, y); %! assert_equal (edge (Mdl, T(:,1:2), y), a); %! assert_equal (edge (Mdl, T, 'Species'), a); %! assert_equal (edge (Mdl, T), a); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(51:150,1:2); %! y = categorical (species(51:150)); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitclinear (T, 'Species'); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## Observation weights of class single or double %!error ... %! fitclinear ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', ... %! int8 ([1; 1; 1; 1])) %!error ... %! fitclinear ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', true (4, 1)) %!error %! X = [1, 2; 3, 4; 5, 6; 7, 8]; %! Y = [1; 1; 2; 2]; %! loss (fitclinear (X, Y), X, Y, 'Weights', int8 ([1; 1; 1; 1])) %!test %! ## Single weights compute as double %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! w = 1 + (1:100)' / 7; %! A = fitclinear (X, Y, 'Weights', single (w)); %! B = fitclinear (X, Y, 'Weights', double (single (w))); %! assert_equal (nthargout (2, @predict, A, X), nthargout (2, @predict, B, X)); ## Test a class with prior 0, whose observations are removed before fitting, ## as MATLAB R2024a and R2026b remove them %!warning ... %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! Mdl = ClassificationLinear (X, Y, 'Prior', [1, 0]); %! assert_equal (Mdl.Prior, [1, 0]); %! assert_equal (predict (Mdl, meas([51, 110], 3:4)), ... %! {'versicolor'; 'versicolor'}); %!test %! ## A named class with prior 0 may be absent, having nothing to remove %! load fisheriris %! X = meas(51:100, 3:4); %! Y = species(51:100); %! C = {'versicolor', 'virginica'}; %! Mdl = ClassificationLinear (X, Y, 'ClassNames', C, 'Prior', [1, 0]); %! assert_equal (Mdl.ClassNames, {'versicolor'; 'virginica'}); %! assert_equal (predict (Mdl, meas(51, 3:4)), {'versicolor'}); %!warning ... %! load fisheriris %! Mdl = ClassificationLinear (meas(51:150, 3:4), species(51:150), ... %! 'Cost', [0, 1; 0, 0]); %! assert_equal (Mdl.Prior, [1, 0]); ## Test a 'Cost' given as a structure, permuted by name to the model's order, ## as MATLAB R2024a permutes it %!test %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! S = struct ('ClassNames', {{'virginica', 'versicolor'}}, ... %! 'ClassificationCosts', [0, 2; 1, 0]); %! Mdl = ClassificationLinear (X, Y, 'Cost', S); %! assert_equal (Mdl.Cost, [0, 1; 2, 0]); %! assert_equal (Mdl.Prior, [0.5, 0.5]); %!test %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! S = struct ('ClassNames', {{'virginica', 'versicolor'}}, ... %! 'ClassificationCosts', [0, 2; 1, 0]); %! M1 = ClassificationLinear (X, Y, 'Cost', S); %! M2 = ClassificationLinear (X, Y, 'Cost', [0, 1; 2, 0]); %! assert_equal (predict (M1, X), predict (M2, X)); %!error ... %! load fisheriris %! ClassificationLinear (meas(51:150, 3:4), species(51:150), ... %! 'Cost', struct ('a', 1)); statistics-release-2.1.2/inst/Machine_Learning/ClassificationNaiveBayes.m000066400000000000000000003365501527311104300265550ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ClassificationNaiveBayes < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} ClassificationNaiveBayes ## ## Naive Bayes classification ## ## The @code{ClassificationNaiveBayes} class implements a naive Bayes ## classifier object, which can predict responses for new data using the ## @code{predict} method. ## ## A naive Bayes classifier estimates one univariate density per class and ## per predictor, and treats the predictors as conditionally independent ## given the class. The joint likelihood of an observation is therefore the ## product of its per-predictor densities, and the posterior follows from ## the class prior by Bayes' rule. The independence assumption is rarely ## true, but it costs only one density per predictor rather than one joint ## density over all of them, which is what makes the model usable when the ## predictors are many and the observations few. ## ## Create a @code{ClassificationNaiveBayes} object by using the ## @code{fitcnb} function or the class constructor. ## ## Each predictor carries its own distribution, named in ## @qcode{DistributionNames}, and the fitted parameters of class @math{k} and ## predictor @math{j} are held in @code{DistributionParameters@{k,j@}}. A ## @qcode{'normal'} predictor stores a two element column vector, the class ## conditional mean and standard deviation; a @qcode{'kernel'} predictor ## stores a @code{prob.KernelDistribution} object. ## ## @seealso{fitcnb} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} Y ## ## Class labels ## ## Specified as a logical or numeric column vector, or as a character array ## or a cell array of character vectors with the same number of rows as ## @var{X}. Each row in @var{Y} is the observed class label for the ## corresponding row in @var{X}. This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} X ## ## Predictor data ## ## A numeric matrix containing the predictor data the model was fitted on. ## Each column of @var{X} represents one predictor (variable), and each ## row represents one observation; a row dropped from the fit, as ## @code{RowsUsed} describes, is not kept. This property is read-only. ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} RowsUsed ## ## Rows used for fitting ## ## A logical column vector with the same length as the observations in the ## original predictor data @var{X}, true for each row that was used for ## fitting the model. It is empty, @qcode{[]}, when every observation was ## used. A row is dropped when its class is missing, when every predictor ## is missing, or when its weight is zero or missing. A row missing only ## some predictors is used; with the @qcode{'mn'} distribution it counts ## in the prior but not in the multinomial, which describes the whole ## row. This property is read-only. ## ## @end deftp RowsUsed = []; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} W ## ## Observation weights ## ## A numeric column vector with one entry per observation used for fitting, ## summing to one. Each class contributes its prior, spread over the ## observations belonging to it in proportion to the @qcode{'Weights'} ## given, or evenly when none were. It has the class of the ## @qcode{'Weights'} given, single or double. This property is read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} ModelParameters ## ## What was fitted, and how ## ## A structure carrying @qcode{DistributionNames}, @qcode{Kernel}, ## @qcode{Support}, @qcode{Width}, @qcode{StandardizeData}, ## @qcode{Version}, @qcode{Method} and @qcode{Type}. ## ## It records the arguments as they were @emph{given}, where the ## properties of the same name record what they were resolved to: a model ## fitted with no @qcode{'DistributionNames'} argument reports the single ## name @qcode{'normal'} here and one name per predictor there. The ## kernel settings are filled in with their defaults when a kernel density ## was asked for, and left empty when none was. This property is ## read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} NumObservations ## ## Number of observations ## ## A positive integer specifying the number of observations used to train ## the model, the rows dropped as @code{RowsUsed} describes left out. Each ## predictor is fitted on the rows it is known for, and an empirical prior ## counts every row used. This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} BinEdges ## ## Bin edges ## ## A cell array with one entry per predictor, holding that predictor's bin ## edges where the learner discretized it before fitting. A naive Bayes ## model fits a density to each predictor as it stands and bins nothing, so ## this is always an empty cell. It is kept because the cross-validated ## model carries it across, and because code that reaches into it with ## @code{cellfun} must find a cell rather than an empty matrix. This ## property is read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} PredictorNames ## ## Predictor variable names ## ## A cell array of character vectors naming the predictors, in the order in ## which they appear in @var{X}. The default names are @qcode{'x1'}, ## @qcode{'x2'}, and so on. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} CategoricalPredictors ## ## Categorical predictor indices ## ## A numeric row vector of the column indices of @var{X} treated as ## categorical, or empty when none is. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} ResponseName ## ## Response variable name ## ## A character vector naming the response variable, @qcode{'Y'} by default. ## This property is read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} ExpandedPredictorNames ## ## Expanded predictor variable names ## ## A cell array of character vectors naming the predictors as the model ## sees them. It equals @qcode{PredictorNames} unless a categorical ## predictor has been expanded into indicator variables. This property is ## read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} ClassNames ## ## Class labels of the fitted model ## ## A cell array of character vectors, a logical or numeric column vector, ## or a character array, holding the distinct classes the model was fitted ## on, in the order the other per-class properties use. This property is ## read-only. ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} HyperparameterOptimizationResults ## ## Results of the hyperparameter optimization ## ## @strong{Always empty.} It is declared for MATLAB compatibility, where ## it holds what an automatic search over the hyperparameters found. This ## class fits the parameters it is given and runs no such search, so there ## is nothing to report. This property is read-only. ## ## @end deftp HyperparameterOptimizationResults = []; endproperties ## Properties a user may set after the model is fitted. Each one is ## validated by its set method below. They sit between the two read-only ## blocks so that 'properties' reports MATLAB's own order. properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} Prior ## ## Class prior probabilities ## ## A numeric row vector with one entry per class, in the order of ## @qcode{ClassNames}, summing to one. It may be assigned after fitting, ## as a numeric vector, as a structure carrying @qcode{ClassNames} and ## @qcode{ClassProbs}, or as @qcode{'empirical'} or @qcode{'uniform'}. ## Assigning it re-derives @qcode{W}. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} Cost ## ## Misclassification cost ## ## A square numeric matrix with one row and column per class, in the order ## of @qcode{ClassNames}. @code{Cost(i,j)} is the cost of classifying an ## observation of class @math{i} into class @math{j}, and the default is ## one off the diagonal and zero on it. It may be assigned after fitting, ## as a matrix or as a structure carrying @qcode{ClassNames} and ## @qcode{ClassificationCosts}. ## ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} ScoreTransform ## ## Score transformation ## ## A character vector naming the function applied to the posterior returned ## by @code{predict}, or a function handle taking and returning a matrix of ## the same size. The default, @qcode{'none'}, leaves the posterior ## untouched. ## ## @end deftp ScoreTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} DistributionNames ## ## Predictor distributions ## ## A cell array of character vectors with one entry per predictor, naming ## the distribution fitted to it: @qcode{'normal'} or @qcode{'kernel'}. ## This property is read-only. ## ## @end deftp DistributionNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} Mu ## ## Predictor means ## ## The means used to center the predictors, when the model standardizes ## them, and empty otherwise. These are @emph{not} the class conditional ## means, which are held in @qcode{DistributionParameters}. This property ## is read-only. ## ## @end deftp Mu = []; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} Sigma ## ## Predictor standard deviations ## ## The standard deviations used to scale the predictors, when the model ## standardizes them, and empty otherwise. These are @emph{not} the class ## conditional standard deviations, which are held in ## @qcode{DistributionParameters}. This property is read-only. ## ## @end deftp Sigma = []; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} DistributionParameters ## ## Fitted distribution parameters ## ## A cell array with one row per class and one column per predictor. ## @code{DistributionParameters@{k,j@}} holds the parameters fitted to ## predictor @math{j} within class @math{k}: a two element column vector, ## the mean and the standard deviation, for a @qcode{'normal'} predictor, ## and a @code{prob.KernelDistribution} object for a @qcode{'kernel'} one. ## This property is read-only. ## ## @end deftp DistributionParameters = {}; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} CategoricalLevels ## ## Levels of the categorical predictors ## ## A cell array with one entry per predictor, holding the distinct levels ## of each categorical predictor and empty for every other. This property ## is read-only. ## ## @end deftp CategoricalLevels = {}; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} Kernel ## ## Kernel smoothing functions ## ## A cell array with one entry per predictor naming the smoothing kernel ## used by a @qcode{'kernel'} predictor, and empty for every other. This ## property is read-only. ## ## @end deftp Kernel = {}; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} Support ## ## Kernel smoothing supports ## ## A cell array with one entry per predictor giving the support of a ## @qcode{'kernel'} predictor's density, and empty for every other. This ## property is read-only. ## ## @end deftp Support = {}; ## -*- texinfo -*- ## @deftp {ClassificationNaiveBayes} {property} Width ## ## Kernel smoothing bandwidths ## ## A numeric matrix with one row per class and one column per predictor, ## giving the bandwidth of each @qcode{'kernel'} predictor's density, and ## empty when no predictor uses one. This property is read-only. ## ## @end deftp Width = []; endproperties ## Readable by the compact counterpart, which copies it, and kept out of the ## documented surface. properties (GetAccess = public, SetAccess = protected, Hidden) ## The parsed ScoreTransform, applied to the posterior by predict. STfun = []; ## The observation weights given, one per observation used and in their ## own class, from which W follows the prior; empty when none were given. RawWeights = []; endproperties ## Set methods for the properties a user may assign after fitting. methods (Hidden) function this = set.Cost (this, Cost) [C, errmsg] = costMatrix (Cost, this.ClassNames); if (! isempty (errmsg)) error ('ClassificationNaiveBayes.Cost: %s', errmsg); endif this.Cost = C; endfunction function this = set.Prior (this, Prior) P = Prior; if (isstruct (P)) P = priorFromStruct (P, this.ClassNames, ... 'ClassificationNaiveBayes.Prior'); elseif (ischar (P)) if (strcmpi (P, 'uniform')) P = ones (1, rows (this.Cost)) / rows (this.Cost); elseif (! strcmpi (P, 'empirical')) error (strcat ("ClassificationNaiveBayes.Prior: a character", ... " vector must be 'empirical' or 'uniform'.")); else return; # 'empirical' after fitting is what is already stored endif endif if (! (isnumeric (P) && isvector (P) && isreal (P))) error (strcat ("ClassificationNaiveBayes.Prior: must be a real", ... " numeric vector, a structure, 'empirical', or", ... " 'uniform'.")); endif if (numel (P) != rows (this.Cost)) error (strcat ("ClassificationNaiveBayes.Prior: must have one", ... " element per class.")); endif if (any (P < 0) || ! (sum (P) > 0)) error (strcat ("ClassificationNaiveBayes.Prior: must be", ... " nonnegative and must not be all zero.")); endif this.Prior = P(:)' / sum (P); ## The weights follow the prior, so reassigning one re-derives the other. ## Weights that were given keep their class in W, computed as double. ## Y holds the rows the model was fitted on, one weight each. if (! isempty (this.RawWeights)) gY = labelIndices (this.ClassNames, this.Y); this.W = cast (priorNormalize (double (this.RawWeights), gY, ... this.Prior), class (this.RawWeights)); elseif (! isempty (this.Y)) gY = labelIndices (this.ClassNames, this.Y); keep = gY > 0; this.W = priorWeights (this.Prior, gY(keep), sum (keep)); endif endfunction function this = set.ScoreTransform (this, val) [f, st] = parseScoreTransform (val, 'ClassificationNaiveBayes'); this.ScoreTransform = st; this.STfun = f; endfunction function disp (this) fprintf ('\n ClassificationNaiveBayes\n\n'); fprintf ('%22s: %s\n', 'ResponseName', this.ResponseName); fprintf ('%22s: %s\n', 'CategoricalPredictors', ... mat2str (this.CategoricalPredictors)); fprintf ('%22s: %s\n', 'ClassNames', classNameListing (this.ClassNames)); fprintf ('%22s: %s\n', 'ScoreTransform', this.ScoreTransform); fprintf ('%22s: %d\n', 'NumObservations', this.NumObservations); fprintf ('%22s: %s\n', 'DistributionNames', ... classNameListing (this.DistributionNames)); fprintf ('%22s: {%dx%d cell}\n', 'DistributionParameters', ... size (this.DistributionParameters)); fprintf ('\n'); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {ClassificationNaiveBayes} {@var{obj} =} ClassificationNaiveBayes (@var{X}, @var{Y}) ## @deftypefnx {ClassificationNaiveBayes} {@var{obj} =} ClassificationNaiveBayes (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationNaiveBayes} {@var{obj} =} ClassificationNaiveBayes (@var{Tbl}, @var{formula}) ## @deftypefnx {ClassificationNaiveBayes} {@var{obj} =} ClassificationNaiveBayes (@var{Tbl}, @var{Y}) ## @deftypefnx {ClassificationNaiveBayes} {@var{obj} =} ClassificationNaiveBayes (@dots{}, @var{name}, @var{value}) ## ## Create a @code{ClassificationNaiveBayes} object. ## ## @code{@var{obj} = ClassificationNaiveBayes (@var{X}, @var{Y})} fits a ## naive Bayes classifier to the predictor data @var{X} and the class ## labels @var{Y}. The supported @qcode{Name}/@qcode{Value} pairs are ## those of @code{fitcnb}, which is the documented way to reach this ## constructor. ## ## @seealso{fitcnb} ## @end deftypefn function this = ClassificationNaiveBayes (X, Y, varargin) ## Check for appropriate number of input arguments if (nargin < 2) error ("ClassificationNaiveBayes: too few input arguments."); endif ## A table names its own predictors and says which hold levels [this, X, Y, varargin] = resolveTable (this, 'ClassificationNaiveBayes', ... X, Y, varargin); if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationNaiveBayes: Name-Value", ... " arguments must be in pairs.")); endif ## Validate X if (! (isnumeric (X) && isreal (X) && ismatrix (X) && ! isempty (X))) error (strcat ("ClassificationNaiveBayes: X must be a", ... " non-empty real numeric matrix.")); endif ## Check X and Y have the same number of observations if (rows (X) != rows (Y)) error (strcat ("ClassificationNaiveBayes: number of rows in", ... " X and Y must be equal.")); endif nPred = columns (X); ## Parse optional paired arguments optNames = {'PredictorNames', 'ResponseName', 'ClassNames', 'Prior', ... 'Cost', 'ScoreTransform', 'CategoricalPredictors', ... 'DistributionNames', 'Kernel', 'Support', 'Width', ... 'Weights'}; ## An empty default stands for one resolved once the data are known: ## the distributions, kernels, supports and widths follow from the ## predictors, the classes and 'Cost' from the response, and ## 'PredictorNames' are x1, x2, ... dfValues = {{}, 'Y', [], 'empirical', [], 'none', [], [], [], [], [], ... []}; [PredictorNames, ResponseName, ClassNames, Prior, Cost, ... ScoreTransform, CatPreds, DistNames, Kernel, Support, Width, ... Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! isempty (PredictorNames) && ! iscellstr (PredictorNames)) error (strcat ("ClassificationNaiveBayes: 'PredictorNames' must be", ... " supplied as a cellstring array.")); elseif (! isempty (PredictorNames) && numel (PredictorNames) != nPred) error (strcat ("ClassificationNaiveBayes: 'PredictorNames' must", ... " equal the number of columns in X.")); endif if (! (ischar (ResponseName) && isrow (ResponseName))) error (strcat ("ClassificationNaiveBayes: 'ResponseName' must be a", ... " character vector.")); endif if (! isempty (ClassNames) && ! (iscellstr (ClassNames) || isnumeric (ClassNames) || islogical (ClassNames) || ischar (ClassNames) || isa (ClassNames, 'categorical') || isa (ClassNames, 'string'))) error (strcat ("ClassificationNaiveBayes: 'ClassNames' must be a", ... " categorical array, a character array, a string", ... " array, a logical vector, a numeric vector, or a", ... " cell array of character vectors.")); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("ClassificationNaiveBayes: %s", errmsg); endif if (! isempty (Weights) && ! (isvector (Weights) && numel (Weights) == rows (X))) error (strcat ("ClassificationNaiveBayes: 'Weights' must be a", ... " vector with one element per row of X.")); endif if (! isempty (Weights) && (any (Weights < 0) || ! (sum (Weights(! isnan (Weights))) > 0))) error (strcat ("ClassificationNaiveBayes: 'Weights' must be", ... " nonnegative and must not be all zero.")); endif if (! isempty (args)) error ("ClassificationNaiveBayes: invalid optional paired argument."); endif ## Resolve the categorical predictors and the distribution of every ## predictor. The two are tied: naming a predictor categorical makes ## it multivariate multinomial unless a distribution is named for it, ## and naming that distribution makes the predictor categorical, which ## MATLAB warns about rather than silently accepting. catidx = nbCategorical (CatPreds, nPred, 'ClassificationNaiveBayes'); if (isempty (DistNames) && ! isempty (catidx)) DistNames = repmat ({'normal'}, 1, nPred); DistNames(catidx) = {'mvmn'}; endif D = nbDistNames (DistNames, nPred, 'ClassificationNaiveBayes'); if (iscell (D)) mvidx = find (strcmp (D, 'mvmn')); if (! all (ismember (mvidx, catidx))) warning (strcat ("ClassificationNaiveBayes: the 'mvmn'", ... " distribution was named for a predictor that is", ... " not in 'CategoricalPredictors', which is", ... " updated to include every 'mvmn' predictor.")); catidx = sort (unique ([catidx, mvidx])); endif endif this.DistributionNames = D; this.CategoricalPredictors = catidx; ## Drop the rows R2024a and R2026b drop: a row whose class is missing, ## one missing every predictor, and one of zero or missing weight. A ## row missing only some predictors is kept, each distribution being ## fitted on the rows it is known for, and the empirical prior counting ## every row kept. The multinomial describes the whole row at once, so ## it is fitted on complete rows alone, while a row missing some counts ## still counts in the prior. X and Y hold the rows kept, as in ## R2024a, so that they line up with W and with resubPredict. ok = ! isnan (grp2idx (Y)) & ! all (isnan (X), 2); if (! isempty (Weights)) ok &= ! isnan (Weights(:)) & Weights(:) > 0; endif if (! all (ok)) this.RowsUsed = ok; endif Xf = X(ok, :); Yf = Y(ok, :); this.X = Xf; this.Y = Yf; if (isempty (Xf)) error (strcat ("ClassificationNaiveBayes: no observation has a", ... " known class, a known predictor and a positive", ... " weight.")); endif ## Resolve the classes. Naming them keeps only those observations, so ## the rows of the others are dropped exactly as a missing value is. if (isempty (ClassNames)) C = uniqueLabels (Yf); else ## The named classes, in the type of the response U = uniqueLabels (Yf); ## A named class with prior 0 is removed below, so it may be absent; ## it is added to the labels the classes are drawn from exempt = zeroPriorNames (ClassNames, Prior, []); [pos, errmsg] = namedClasses (U, ClassNames, exempt); if (! isempty (errmsg)) error ("ClassificationNaiveBayes: %s", errmsg); endif absent = (pos(:) == 0); if (any (absent)) if (ischar (ClassNames)) U = uniqueLabels (namesAsLabels (Yf, ClassNames(absent,:))); else U = uniqueLabels (namesAsLabels (Yf, ClassNames(absent))); endif pos = namedClasses (U, ClassNames); endif C = U(pos,:); endif ## A label outside the classes indexes as zero. That is a fault only ## when the classes came from the response itself; naming a subset of ## them is a request to keep those observations and drop the rest. [gY, errmsg] = labelIndices (C, Yf); if (! isempty (errmsg) && isempty (ClassNames)) error ('ClassificationNaiveBayes: %s', errmsg); endif inC = gY > 0; if (! all (inC)) Xf = Xf(inC, :); Yf = Yf(inC, :); gY = gY(inC); ok(ok) = inC; this.RowsUsed = ok; this.X = Xf; this.Y = Yf; if (isempty (Xf)) error (strcat ("ClassificationNaiveBayes: no observation", ... " belongs to the named classes.")); endif endif ## A categorical response reports only the categories it holds, as ## R2024a does. if (isa (C, 'categorical')) C = removecats (C); endif this.ClassNames = C; nObs = rows (Xf); nCls = rows (C); this.NumObservations = nObs; this.ResponseName = ResponseName; if (isempty (PredictorNames)) PredictorNames = arrayfun (@(k) sprintf ('x%d', k), 1:nPred, ... 'UniformOutput', false); endif this.PredictorNames = PredictorNames; this.ExpandedPredictorNames = PredictorNames; ## Cost first: the Prior set method reads its size to count the classes if (isempty (Cost)) this.Cost = ones (nCls) - eye (nCls); else this.Cost = Cost; endif ## Prior, and the observation weights that follow from it. The weights ## given keep their class in the model; every computation runs on them ## as double. wf = ones (nObs, 1); if (! isempty (Weights)) this.RawWeights = Weights(:)(ok); wf = double (this.RawWeights); endif if (ischar (Prior) && strcmpi (Prior, 'empirical')) this.Prior = accumarray (gY, wf, [nCls, 1])' / sum (wf); elseif (ischar (Prior) && strcmpi (Prior, 'uniform')) this.Prior = ones (1, nCls) / nCls; else this.Prior = Prior; endif ## A class with prior 0 is never predicted, so its observations are ## removed; one that costs nothing to misclassify is kept, as MATLAB's ## learner keeps it [drop, P, msg] = removedClasses (this.Prior, [], ... this.ClassNames, gY); if (all (drop)) error (strcat ("ClassificationNaiveBayes: every class has prior", ... " probability 0.")); elseif (any (drop)) if (! isempty (msg)) warning ("ClassificationNaiveBayes: %s", msg); endif keep = ! drop(gY); Xf = Xf(keep,:); Yf = Yf(keep,:); gY = gY(keep); ok(ok) = keep; this.RowsUsed = ok; this.X = Xf; this.Y = Yf; this.NumObservations = rows (Xf); if (! isempty (this.RawWeights)) this.RawWeights = this.RawWeights(keep); endif this.Prior = P; endif this.ScoreTransform = ScoreTransform; ## Fit one density per class and per predictor [this.DistributionParameters, this.Kernel, this.Support, ... this.Width, this.CategoricalLevels] = ... nbFit (Xf, gY, nCls, this.DistributionNames, double (this.W), ... Kernel, Support, Width, 'ClassificationNaiveBayes', ... this.ClassNames, this.PredictorNames); ## The arguments as they were given. The fields are assigned one at a ## time: a cell handed to struct() would make a structure array of its ## elements rather than one structure holding the cell. mp = struct (); if (isempty (DistNames)) mp.DistributionNames = 'normal'; else mp.DistributionNames = DistNames; endif ## The kernel settings carry their defaults only where a kernel density ## was actually asked for, and stay empty otherwise. if (iscell (this.DistributionNames) && any (strcmp (this.DistributionNames, 'kernel'))) mp.Kernel = nbGivenOr (Kernel, 'normal'); mp.Support = nbGivenOr (Support, 'unbounded'); mp.Width = nbGivenOr (Width, NaN); mp.StandardizeData = 0; else mp.Kernel = []; mp.Support = []; mp.Width = []; mp.StandardizeData = []; endif mp.Version = 1; mp.Method = 'NaiveBayes'; mp.Type = 'classification'; this.ModelParameters = mp; endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNaiveBayes} {@var{label} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {ClassificationNaiveBayes} {[@var{label}, @var{score}, @var{cost}] =} predict (@var{obj}, @var{XC}) ## ## Classify new data with a trained @code{ClassificationNaiveBayes} object. ## ## @code{@var{label} = predict (@var{obj}, @var{XC})} returns the predicted ## class label for each row of @var{XC}, which must have as many columns as ## the predictor data the model was fitted on. ## ## @code{[@var{label}, @var{score}, @var{cost}] = predict (@var{obj}, ## @var{XC})} also returns @var{score}, the posterior probability of each ## class, and @var{cost}, the expected misclassification cost of assigning ## each observation to each class. The label of an observation is the ## class of least expected cost. ## ## A missing predictor is skipped and the observation classified on the ## rest. Under the @qcode{'mn'} distribution, which describes the whole ## row at once, an observation missing any count has posterior and cost ## @qcode{NaN} and takes the class of largest prior, whatever the cost. ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @end deftypefn function [label, score, cost] = predict (this, XC) if (nargin < 2) error ("ClassificationNaiveBayes.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'ClassificationNaiveBayes.predict', XC); if (isempty (XC)) error ("ClassificationNaiveBayes.predict: XC is empty."); endif if (! (isnumeric (XC) && isreal (XC) && ismatrix (XC))) error (strcat ("ClassificationNaiveBayes.predict: XC must be a", ... " real numeric matrix.")); endif if (columns (this.X) != columns (XC)) error (strcat ("ClassificationNaiveBayes.predict: XC must have", ... " the same number of predictors as the trained", ... " model.")); endif nCls = numel (this.Prior); ## Work in logs: the product over predictors underflows for even a ## moderate number of them, and the posterior only needs the differences. logscore = nbLogLik (XC, this.DistributionNames, ... this.DistributionParameters, nCls, ... this.CategoricalLevels); logscore = logscore + log (this.Prior); ## Subtracting the row maximum keeps a well separated observation a ## posterior rather than a ratio of two underflowed zeros. ## An observation no class can account for, a categorical level the ## model never saw among them, leaves the likelihood at -Inf for every ## class alike. Its posterior is the prior: what remains when the data ## says nothing. Measured against R2024a, which returns the prior and ## not a uniform distribution, and does so for the whole observation ## even when its other predictors are perfectly informative. noinfo = all (logscore == -Inf, 2); logscore = logscore - max (logscore, [], 2); score = exp (logscore); score = score ./ sum (score, 2); score(isnan (score)) = 0; if (any (noinfo)) score(noinfo,:) = repmat (this.Prior, sum (noinfo), 1); endif ## Expected misclassification cost, and the label of least cost cost = score * this.Cost; [~, minIdx] = min (cost, [], 2); ## The multinomial describes the whole row at once, so a row missing a ## count has no likelihood: its posterior and cost are NaN and it takes ## the class of largest prior, the cost aside, as R2024a and R2026b do if (ischar (this.DistributionNames)) miss = any (isnan (XC), 2); if (any (miss)) score(miss,:) = NaN; cost(miss,:) = NaN; [~, minIdx(miss)] = max (this.Prior); endif endif label = labelsFromIndex (this.ClassNames, minIdx); ## The transform is applied once, to the assembled posterior score = this.STfun (score); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNaiveBayes} {@var{CVMdl} =} crossval (@var{obj}) ## @deftypefnx {ClassificationNaiveBayes} {@var{CVMdl} =} crossval (@dots{}, @var{name}, @var{value}) ## ## Cross-validate a trained naive Bayes model. ## ## @code{@var{CVMdl} = crossval (@var{obj})} partitions the training data ## into ten folds, or into as many folds as there are observations when ## there are fewer than ten, refits the model on each fold's training part ## and returns a @code{ClassificationPartitionedModel}. ## ## @code{@var{CVMdl} = crossval (@dots{}, @var{name}, @var{value})} takes ## exactly one of @qcode{'KFold'}, @qcode{'Holdout'}, @qcode{'Leaveout'} ## or @qcode{'CVPartition'}. ## ## @seealso{ClassificationPartitionedModel, cvpartition} ## @end deftypefn function CVMdl = crossval (this, varargin) if (nargin < 1) error (strcat ("ClassificationNaiveBayes.crossval: too few", ... " input arguments.")); endif if (numel (varargin) == 1) error (strcat ("ClassificationNaiveBayes.crossval: Name-Value", ... " arguments must be in pairs.")); elseif (numel (varargin) > 2) error (strcat ("ClassificationNaiveBayes.crossval: specify only", ... " one of the optional Name-Value paired arguments.")); endif if (this.NumObservations < 10) numFolds = this.NumObservations; else numFolds = 10; endif Holdout = []; Leaveout = 'off'; CVPartition = []; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'kfold' numFolds = varargin{2}; if (! (isnumeric (numFolds) && isscalar (numFolds) && (numFolds == fix (numFolds)) && numFolds > 1)) error (strcat ("ClassificationNaiveBayes.crossval: 'KFold'", ... " must be an integer value greater than 1.")); endif case 'holdout' Holdout = varargin{2}; if (! (isnumeric (Holdout) && isscalar (Holdout) && Holdout > 0 && Holdout < 1)) error (strcat ("ClassificationNaiveBayes.crossval:", ... " 'Holdout' must be a numeric value between", ... " 0 and 1.")); endif case 'leaveout' Leaveout = varargin{2}; if (! (ischar (Leaveout) && any (strcmpi (Leaveout, {'on', 'off'})))) error (strcat ("ClassificationNaiveBayes.crossval:", ... " 'Leaveout' must be either 'on' or 'off'.")); endif case 'cvpartition' CVPartition = varargin{2}; if (! (isa (CVPartition, 'cvpartition'))) error (strcat ("ClassificationNaiveBayes.crossval:", ... " 'CVPartition' must be a 'cvpartition'", ... " object.")); endif otherwise error (strcat ("ClassificationNaiveBayes.crossval: invalid", ... " parameter name in optional paired arguments.")); endswitch varargin(1:2) = []; endwhile ## The partition covers the observations actually trained on: a row ## dropped from the fit is not one the folds can use. The ## response is passed rather than a count so the folds stay stratified. Yused = this.Y; if (! isempty (CVPartition)) partition = CVPartition; elseif (! isempty (Holdout)) partition = cvpartition (Yused, 'Holdout', Holdout); elseif (strcmpi (Leaveout, 'on')) partition = cvpartition (numel (Yused), 'LeaveOut'); else partition = cvpartition (Yused, 'KFold', numFolds); endif CVMdl = ClassificationPartitionedModel (this, partition); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNaiveBayes} {@var{CMdl} =} compact (@var{obj}) ## ## Drop the training data from a trained model. ## ## @code{@var{CMdl} = compact (@var{obj})} returns a ## @code{CompactClassificationNaiveBayes} object carrying the fitted ## densities and everything @code{predict} needs, but not the observations ## the model was fitted on. It classifies new data identically and is far ## smaller to keep or to ship. ## ## @seealso{CompactClassificationNaiveBayes} ## @end deftypefn function CMdl = compact (this) CMdl = CompactClassificationNaiveBayes (this); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNaiveBayes} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationNaiveBayes} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationNaiveBayes} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margin on new data. ## ## @code{@var{m} = margin (@var{obj}, @var{X}, @var{Y})} returns one margin ## per observation: the posterior the model gives the observation's true ## class, less the largest posterior it gives any other class. A positive ## margin means the observation is classified correctly, and a larger one ## means it is classified more confidently. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @end deftypefn function m = margin (this, X, Y) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationNaiveBayes.margin: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y] = tableResponse (this, 'margin', X, Y, {}, nargin > 2); [gY, errmsg] = labelIndices (this.ClassNames, Y); if (! isempty (errmsg)) error ("ClassificationNaiveBayes.margin: %s", errmsg); endif if (rows (X) != numel (gY)) error (strcat ("ClassificationNaiveBayes.margin: number of rows", ... " in X and Y must be equal.")); endif [~, s] = predict (this, X); m = marginsOf (s, gY, 1); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNaiveBayes} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationNaiveBayes} {@var{e} =} edge (@dots{}, @qcode{'Weights'}, @var{w}) ## @deftypefnx {ClassificationNaiveBayes} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationNaiveBayes} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Classification edge on new data. ## ## @code{@var{e} = edge (@var{obj}, @var{X}, @var{Y})} returns the weighted ## mean of the margins, a single number summarising how confidently the ## model classifies the data. ## ## The weights are normalized within each class to that class's prior ## before they are applied. ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @end deftypefn function e = edge (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationNaiveBayes.edge: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'edge', X, Y, varargin, ... nargin > 2); W = edgeWeights (varargin, Y, this.ClassNames, this.Prior, ... 'ClassificationNaiveBayes', 'edge'); m = margin (this, X, Y); e = sum (W .* m(:)) / sum (W); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNaiveBayes} {@var{l} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationNaiveBayes} {@var{l} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationNaiveBayes} {@var{l} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {ClassificationNaiveBayes} {@var{l} =} loss (@dots{}, @var{name}, @var{value}) ## ## Classification loss on new data. ## ## @code{@var{l} = loss (@var{obj}, @var{X}, @var{Y})} returns the minimum ## expected misclassification cost. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{l} = loss (@dots{}, @var{name}, @var{value})} takes the ## following options. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'LossFun'} @tab One of @qcode{'binodeviance'}, ## @qcode{'classifcost'}, @qcode{'classiferror'}, @qcode{'exponential'}, ## @qcode{'hinge'}, @qcode{'logit'}, @qcode{'mincost'} (default) or ## @qcode{'quadratic'}. ## ## @item @qcode{'Weights'} @tab A numeric vector of observation weights, ## one per row of @var{X}. ## ## @end multitable ## ## @end deftypefn function l = loss (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationNaiveBayes.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationNaiveBayes.loss: name-value", ... " arguments must be in pairs.")); endif ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights'}; dfValues = {'mincost', []}; [LossFun, Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments lf_opt = {'binodeviance', 'classifcost', 'classiferror', ... 'exponential', 'hinge', 'logit', 'mincost', 'quadratic'}; if (! (ischar (LossFun) && any (strcmpi (LossFun, lf_opt)))) error ("ClassificationNaiveBayes.loss: invalid loss function."); endif LossFun = tolower (LossFun); errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("ClassificationNaiveBayes.loss: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isvector (Weights))) error ("ClassificationNaiveBayes.loss: invalid 'Weights'."); endif if (! isempty (args)) error (strcat ("ClassificationNaiveBayes.loss: invalid optional", ... " paired argument.")); endif [gY, errmsg] = labelIndices (this.ClassNames, Y); if (! isempty (errmsg)) error ("ClassificationNaiveBayes.loss: %s", errmsg); endif if (rows (X) != numel (gY)) error (strcat ("ClassificationNaiveBayes.loss: number of rows in", ... " X and Y must be equal.")); endif if (isempty (Weights)) w = ones (numel (gY), 1); else w = double (Weights(:)); if (numel (w) != numel (gY)) error (strcat ("ClassificationNaiveBayes.loss: 'Weights' must", ... " have one element per observation.")); endif endif ## The weights are normalized within each class to that class's prior, ## so that a loss and an edge weight the classes the same way. w = priorNormalize (w, gY, this.Prior); [~, s] = predict (this, X); l = classificationLoss (LossFun, s, gY, w, this.Cost); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNaiveBayes} {@var{lp} =} logp (@var{obj}, @var{X}) ## ## Log unconditional probability density of new data. ## ## @code{@var{lp} = logp (@var{obj}, @var{X})} returns one value per ## observation, the logarithm of its density under the fitted model taken ## over all the classes, each weighted by its prior. A markedly low value ## marks an observation the model finds unlike anything it was trained on, ## whatever class it would be assigned to. ## ## @var{X} may also be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: one ## the model was not fitted on is passed over, one it needs and cannot ## find is named, and a value holding a level is coded as that level was ## coded at fitting. ## ## @end deftypefn function lp = logp (this, X) if (nargin < 2) error ("ClassificationNaiveBayes.logp: too few input arguments."); endif ## A table is read by the names the model was fitted on X = tableColumns (this, 'ClassificationNaiveBayes.logp', X); if (isempty (X)) error ("ClassificationNaiveBayes.logp: X is empty."); endif if (columns (this.X) != columns (X)) error (strcat ("ClassificationNaiveBayes.logp: X must have the", ... " same number of predictors as the trained model.")); endif nCls = numel (this.Prior); L = nbLogLik (X, this.DistributionNames, ... this.DistributionParameters, nCls, ... this.CategoricalLevels); L = L + log (this.Prior); ## Sum the classes in logs: the largest term is factored out so that a ## density small enough to underflow still contributes its logarithm. ## Where every class is impossible the largest term is -Inf and the ## factoring is 0/0, so the answer is written directly: the density ## really is zero there. Lmax = max (L, [], 2); lp = Lmax + log (sum (exp (L - Lmax), 2)); lp(Lmax == -Inf) = -Inf; ## predict skips a missing predictor and classifies on the rest, but a ## density is not defined for an observation that is not fully observed, ## so this reports NaN where predict reports a class. R2024a does the ## same. lp(any (isnan (X), 2)) = NaN; endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNaiveBayes} {@var{label} =} resubPredict (@var{obj}) ## @deftypefnx {ClassificationNaiveBayes} {[@var{label}, @var{score}, @var{cost}] =} resubPredict (@var{obj}) ## ## Classify the training data with the trained model. ## ## The same as calling @code{predict} on the data the model was fitted on, ## with the rows that were dropped, as @code{RowsUsed} describes, left out. ## ## @end deftypefn function [label, score, cost] = resubPredict (this) if (nargin < 1) error (strcat ("ClassificationNaiveBayes.resubPredict:", ... " too few input arguments.")); endif [label, score, cost] = predict (this, this.X); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNaiveBayes} {@var{m} =} resubMargin (@var{obj}) ## ## Classification margin on the training data. ## ## @end deftypefn function m = resubMargin (this) if (nargin < 1) error (strcat ("ClassificationNaiveBayes.resubMargin:", ... " too few input arguments.")); endif X = this.X; Y = this.Y; m = margin (this, X, Y); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNaiveBayes} {@var{e} =} resubEdge (@var{obj}) ## ## Classification edge on the training data. ## ## @end deftypefn function e = resubEdge (this) if (nargin < 1) error (strcat ("ClassificationNaiveBayes.resubEdge:", ... " too few input arguments.")); endif ## The training data weighs by the model's own weights, as in R2024a X = this.X; Y = this.Y; e = edge (this, X, Y, 'Weights', this.W); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNaiveBayes} {@var{l} =} resubLoss (@var{obj}) ## @deftypefnx {ClassificationNaiveBayes} {@var{l} =} resubLoss (@dots{}, @var{name}, @var{value}) ## ## Classification loss on the training data. ## ## Takes the same options as @code{loss}. ## ## @end deftypefn function l = resubLoss (this, varargin) if (nargin < 1) error (strcat ("ClassificationNaiveBayes.resubLoss:", ... " too few input arguments.")); endif ## The model's own weights stand unless others are given, as in R2024a X = this.X; Y = this.Y; if (! any (strcmpi (varargin(1:2:end), 'Weights'))) varargin = [varargin, {'Weights', this.W}]; endif l = loss (this, X, Y, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNaiveBayes} {} savemodel (@var{obj}, @var{filename}) ## ## Save a ClassificationNaiveBayes object. ## ## @code{savemodel (@var{obj}, @var{filename})} saves each property of a ## ClassificationNaiveBayes object into an Octave binary file, the name of ## which is specified in @var{filename}, along with an extra variable, which ## defines the type classification object these variables constitute. Use ## @code{loadmodel} in order to load a classification object into Octave's ## workspace. ## ## @seealso{loadmodel, fitcnb, ClassificationNaiveBayes} ## @end deftypefn function savemodel (this, fname) if (nargin < 2) error (strcat ("ClassificationNaiveBayes.savemodel:", ... " too few input arguments.")); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error (strcat ("ClassificationNaiveBayes.savemodel:", ... " FNAME must be a character vector.")); endif ## Generate variable for class name classdef_name = 'ClassificationNaiveBayes'; ## Create variables from model properties X = this.X; Y = encodeLabels (this.Y); RowsUsed = this.RowsUsed; W = this.W; ModelParameters = this.ModelParameters; NumObservations = this.NumObservations; BinEdges = this.BinEdges; PredictorNames = this.PredictorNames; CategoricalPredictors = this.CategoricalPredictors; ResponseName = this.ResponseName; ExpandedPredictorNames = this.ExpandedPredictorNames; ClassNames = encodeLabels (this.ClassNames); Prior = this.Prior; Cost = this.Cost; ScoreTransform = this.ScoreTransform; DistributionNames = this.DistributionNames; Mu = this.Mu; Sigma = this.Sigma; CategoricalLevels = this.CategoricalLevels; ## A kernel predictor's density is a classdef object, which Octave's ## save cannot serialize, so it goes out as the sample it was fitted to ## and load_model rebuilds it from that and the recorded bandwidth. DistributionParameters = nbKernelPack (this.DistributionParameters, ... this.DistributionNames); Kernel = this.Kernel; Support = this.Support; Width = this.Width; STfun = this.STfun; RawWeights = this.RawWeights; ## Save classdef name and all model properties as individual variables HyperparameterOptimizationResults = this.HyperparameterOptimizationResults; save ('-binary', fname, 'classdef_name', 'X', 'Y', 'RowsUsed', 'W', ... 'ModelParameters', 'NumObservations', 'BinEdges', ... 'PredictorNames', 'CategoricalPredictors', 'ResponseName', ... 'ExpandedPredictorNames', 'ClassNames', 'Prior', 'Cost', ... 'ScoreTransform', 'DistributionNames', 'Mu', 'Sigma', ... 'DistributionParameters', 'CategoricalLevels', 'Kernel', ... 'Support', 'Width', 'STfun', 'RawWeights', ... 'HyperparameterOptimizationResults'); endfunction endmethods methods (Static, Hidden) function mdl = load_model (filename, data) data = decodeLabels (data); ## A model saved before X and Y held only the rows used carries every ## row given; keep the rows used, so that Y lines up with W again ## before the prior, which reads Y, is assigned. if (isfield (data, 'RowsUsed') && ! isempty (data.RowsUsed) && rows (data.X) == numel (data.RowsUsed) && rows (data.X) > data.NumObservations) data.X = data.X(data.RowsUsed, :); data.Y = data.Y(data.RowsUsed, :); endif ## The smallest fit the class accepts, filled property by property ## below. Two observations per class give every distribution something ## to estimate from, and nothing of the stub survives the copy. mdl = ClassificationNaiveBayes ([0; 1; 2; 3], [1; 1; 2; 2]); ## Copy the saved data into the object. Iterate over what was saved ## rather than over fieldnames (mdl): a private property such as STfun ## is written out by savemodel but is not reported by fieldnames, so ## comparing the two sets could never match and every load failed. ## Assignment is legal here because this is a method of the class ## itself. names = fieldnames (data); ## These three are assigned once everything else is in place, and in ## this order rather than the file's. Cost comes before Prior because ## set.Prior counts the classes by the rows of Cost and not by ## ClassNames, so a prior arriving first is measured against the stub's. order = {'Cost', 'Prior', 'ScoreTransform'}; late = ismember (names, order); tail = order(ismember (order, names)); names = [names(! late); tail(:)]; for i = 1:numel (names) try mdl.(names{i}) = data.(names{i}); catch msg = 'ClassificationNaiveBayes.load_model: invalid model in ''%s''.'; error (msg, filename); end_try_catch endfor mdl.DistributionParameters = nbKernelUnpack ( ... mdl.DistributionParameters, mdl.DistributionNames, ... mdl.Kernel, mdl.Support, mdl.Width); endfunction endmethods endclassdef ## Tests %!test # MATLAB parity: the surface a default fit reports %! load fisheriris %! Mdl = fitcnb (meas, species); %! assert_equal (class (Mdl), 'ClassificationNaiveBayes'); %! assert_equal (Mdl.NumObservations, 150); %! assert_equal (Mdl.ClassNames, unique (species)); %! assert_equal (Mdl.Prior, [1/3, 1/3, 1/3], 1e-15); %! assert_equal (Mdl.Cost, [0, 1, 1; 1, 0, 1; 1, 1, 0]); %! assert_equal (Mdl.ResponseName, 'Y'); %! assert_equal (Mdl.PredictorNames, {'x1', 'x2', 'x3', 'x4'}); %! assert_equal (Mdl.ExpandedPredictorNames, {'x1', 'x2', 'x3', 'x4'}); %! assert_equal (Mdl.DistributionNames, {'normal', 'normal', 'normal', 'normal'}); %! assert_equal (Mdl.ScoreTransform, 'none'); %! assert_equal (Mdl.CategoricalPredictors, []); %! assert_equal (Mdl.RowsUsed, []); ## Mu and Sigma are the standardization parameters and stay empty, where the ## class conditional ones live in DistributionParameters. BinEdges is a cell ## rather than an empty matrix, as code reaching into it with cellfun needs. %!test # MATLAB parity: the properties a normal-distribution fit leaves unset %! load fisheriris %! Mdl = fitcnb (meas, species); %! assert_equal (Mdl.Mu, []); %! assert_equal (Mdl.Sigma, []); %! assert_equal (class (Mdl.BinEdges), 'cell'); %! assert_equal (Mdl.BinEdges, {}); %! assert_equal (Mdl.Width, NaN (3, 4)); %! assert_equal (Mdl.CategoricalLevels, cell (1, 4)); %!test # MATLAB parity: the fitted normal parameters and the weights %! load fisheriris %! Mdl = fitcnb (meas, species); %! assert_equal (size (Mdl.DistributionParameters), [3, 4]); %! assert_equal (Mdl.DistributionParameters{1,1}, ... %! [5.005999999999998; 0.352489687213451], 1e-13); %! assert_equal (Mdl.DistributionParameters{2,3}, ... %! [4.260000000000001; 0.469910977239958], 1e-13); %! assert_equal (Mdl.W, repmat (1/150, 150, 1), 1e-15); %! assert_equal (sum (Mdl.W), 1, 1e-14); %!test # MATLAB parity: predict returns the label, posterior and cost %! load fisheriris %! Mdl = fitcnb (meas, species); %! [label, score, cost] = predict (Mdl, meas(1:3,:)); %! assert_equal (label, {'setosa'; 'setosa'; 'setosa'}); %! assert_equal (score, repmat ([1, 0, 0], 3, 1), 1e-12); %! assert_equal (cost, repmat ([0, 1, 1], 3, 1), 1e-12); %!test # MATLAB parity: resubstitution loss, edge and margin %! load fisheriris %! Mdl = fitcnb (meas, species); %! assert_equal (resubLoss (Mdl), 0.04, 1e-14); %! assert_equal (resubEdge (Mdl), 0.894430597464877, 1e-12); %! assert_equal (sum (resubMargin (Mdl)), 134.164589619731402, 1e-10); %! assert_equal (resubMargin (Mdl)(1:5), ones (5, 1), 1e-12); %!test # MATLAB parity: resubPredict agrees with predict on the training data %! load fisheriris %! Mdl = fitcnb (meas, species); %! [rl, rs, rc] = resubPredict (Mdl); %! [pl, ps, pc] = predict (Mdl, meas); %! assert_equal (rl, pl); %! assert_equal (rs, ps); %! assert_equal (rc, pc); %!test # MATLAB parity: every one of the eight loss functions %! load fisheriris %! Mdl = fitcnb (meas, species); %! assert_equal (loss (Mdl, meas, species), 0.04, 1e-14); %! assert_equal (loss (Mdl, meas, species, 'LossFun', 'mincost'), 0.04, 1e-14); %! assert_equal (loss (Mdl, meas, species, 'LossFun', 'classiferror'), ... %! 0.04, 1e-14); %! assert_equal (loss (Mdl, meas, species, 'LossFun', 'classifcost'), ... %! 0.04, 1e-14); %! assert_equal (loss (Mdl, meas, species, 'LossFun', 'binodeviance'), ... %! 0.149545751140664, 1e-12); %! assert_equal (loss (Mdl, meas, species, 'LossFun', 'exponential'), ... %! 0.395395632839880, 1e-12); %! assert_equal (loss (Mdl, meas, species, 'LossFun', 'hinge'), ... %! 0.052784701267562, 1e-12); %! assert_equal (loss (Mdl, meas, species, 'LossFun', 'logit'), ... %! 0.331045591275855, 1e-12); %! assert_equal (loss (Mdl, meas, species, 'LossFun', 'quadratic'), ... %! 0.033005648597277, 1e-12); %!test # MATLAB parity: weighted loss and edge %! load fisheriris %! Mdl = fitcnb (meas, species); %! w = (1:150)' / sum (1:150); %! assert_equal (loss (Mdl, meas, species, 'Weights', w), ... %! 0.037013271417641, 1e-12); %! assert_equal (edge (Mdl, meas, species, 'Weights', w), ... %! 0.898902916457462, 1e-12); %! assert_equal (edge (Mdl, meas, species), 0.894430597464877, 1e-12); %!test # MATLAB parity: edge on a set missing a class %! load fisheriris %! Mdl = fitcnb (meas, species); %! r = 51:150; %! assert_equal (edge (Mdl, meas(r,:), species(r)), 0.8416458962, 1e-10); %!test # MATLAB parity: logp over all the classes %! load fisheriris %! Mdl = fitcnb (meas, species); %! lp = logp (Mdl, meas); %! assert_equal (numel (lp), 150); %! assert_equal (lp(1), 1.026591235856343, 1e-12); %! assert_equal (lp(5), 0.977098877116533, 1e-12); %! assert_equal (sum (lp), -309.559846128495394, 1e-10); %!test # MATLAB parity: a given prior reweights the observations and the loss %! load fisheriris %! Mdl = fitcnb (meas, species, 'Prior', [0.2, 0.3, 0.5]); %! assert_equal (Mdl.Prior, [0.2, 0.3, 0.5], 1e-15); %! assert_equal (Mdl.W(1), 0.004, 1e-15); %! assert_equal (Mdl.W(51), 0.006, 1e-15); %! assert_equal (Mdl.W(101), 0.010, 1e-15); %! assert_equal (resubLoss (Mdl), 0.054, 1e-14); %! assert_equal (edge (Mdl, meas, species), 0.873545897578786, 1e-12); %!test # MATLAB parity: a uniform prior, and a given cost %! load fisheriris %! Mdl = fitcnb (meas, species, 'Prior', 'uniform'); %! assert_equal (Mdl.Prior, [1/3, 1/3, 1/3], 1e-15); %! Mdl = fitcnb (meas, species, 'Cost', [0, 1, 2; 1, 0, 1; 2, 1, 0]); %! assert_equal (Mdl.Cost, [0, 1, 2; 1, 0, 1; 2, 1, 0]); %!test # MATLAB parity: ScoreTransform is applied to the posterior %! load fisheriris %! Mdl = fitcnb (meas, species, 'ScoreTransform', 'logit'); %! assert_equal (Mdl.ScoreTransform, 'logit'); %! [~, score] = predict (Mdl, meas(1:2,:)); %! assert_equal (score, repmat ([0.731058578630005, 0.5, 0.5], 2, 1), 1e-12); %!test # MATLAB parity: the kernel densities and their default bandwidths %! load fisheriris %! Mdl = fitcnb (meas, species, 'DistributionNames', 'kernel'); %! assert_equal (Mdl.DistributionNames, ... %! {'kernel', 'kernel', 'kernel', 'kernel'}); %! assert_equal (Mdl.Kernel, {'normal', 'normal', 'normal', 'normal'}); %! assert_equal (Mdl.Support, ... %! {'unbounded', 'unbounded', 'unbounded', 'unbounded'}); %! assert_equal (class (Mdl.DistributionParameters{1,1}), ... %! 'prob.KernelDistribution'); %! width = [0.143628884694882, 0.179536105868602, 0.071814442347441, ... %! 0.242194206816745; ... %! 0.251350548216043, 0.143628884694882, 0.251350548216043, ... %! 0.107721663521161; ... %! 0.287257769389764, 0.143628884694882, 0.323164990563484, ... %! 0.143628884694882]; %! assert_equal (Mdl.Width, width, 1e-12); ## The bandwidth belongs to the space the density is smoothed in, so a bounded ## support takes it in the transformed one. %!test # MATLAB parity: an explicit bandwidth, and a positive support %! load fisheriris %! Mdl = fitcnb (meas, species, 'DistributionNames', 'kernel', ... %! 'Kernel', 'triangle', 'Width', 0.5); %! assert_equal (Mdl.Kernel, {'triangle', 'triangle', 'triangle', 'triangle'}); %! assert_equal (Mdl.Width, repmat (0.5, 3, 4), 1e-15); %! x = [0.13; 0.41; 0.22; 1.87; 0.55; 0.09; 2.94; 0.31; 0.68; 0.17; ... %! 0.44; 3.61; 0.26; 0.72; 0.05; 1.13; 0.38; 0.91; 0.19; 4.52]; %! y = [1.02; 1.44; 0.87; 1.19; 2.63; 1.07; 0.95; 1.31; 1.76; 1.12; ... %! 0.99; 1.28; 3.41; 1.05; 1.21; 0.93; 1.38; 1.14; 1.09; 2.02]; %! G = [repmat({'a'}, 20, 1); repmat({'b'}, 20, 1)]; %! Mdl = fitcnb ([x; y], G, 'DistributionNames', 'kernel'); %! assert_equal (Mdl.Width, ... %! [0.237209723894721; 0.138012930266019], 1e-12); %! Mdl = fitcnb ([x; y], G, 'DistributionNames', 'kernel', ... %! 'Support', 'positive'); %! assert_equal (Mdl.Width, ... %! [0.675582146901479; 0.127329555528534], 1e-12); %!test # MATLAB parity: a kernel model classifies as MATLAB does %! x = [0.13; 0.41; 0.22; 1.87; 0.55; 0.09; 2.94; 0.31; 0.68; 0.17; ... %! 0.44; 3.61; 0.26; 0.72; 0.05; 1.13; 0.38; 0.91; 0.19; 4.52]; %! y = [1.02; 1.44; 0.87; 1.19; 2.63; 1.07; 0.95; 1.31; 1.76; 1.12; ... %! 0.99; 1.28; 3.41; 1.05; 1.21; 0.93; 1.38; 1.14; 1.09; 2.02]; %! G = [repmat({'a'}, 20, 1); repmat({'b'}, 20, 1)]; %! Mdl = fitcnb ([x; y], G, 'DistributionNames', 'kernel'); %! [label, score] = predict (Mdl, [0.5; 1.5; 3.0]); %! assert_equal (label, {'a'; 'b'; 'a'}); %! assert_equal (score, [0.991485542298633, 0.008514457701367; ... %! 0.121739189175926, 0.878260810824074; ... %! 0.936549935287367, 0.063450064712633], 1e-12); %! assert_equal (resubLoss (Mdl), 0.075, 1e-14); %!test # MATLAB parity: one distribution per predictor %! load fisheriris %! Mdl = fitcnb (meas, species, 'DistributionNames', ... %! {'normal', 'kernel', 'normal', 'kernel'}); %! assert_equal (Mdl.DistributionNames, ... %! {'normal', 'kernel', 'normal', 'kernel'}); %! assert_equal (class (Mdl.DistributionParameters{1,1}), 'double'); %! assert_equal (class (Mdl.DistributionParameters{1,2}), ... %! 'prob.KernelDistribution'); %! assert_equal (Mdl.DistributionParameters{1,2}.Bandwidth, ... %! 0.179536105868602, 1e-12); ## ModelParameters records the arguments as they were given, where the ## properties of the same name record what they were resolved to. %!test # MATLAB parity: ModelParameters %! load fisheriris %! Mdl = fitcnb (meas, species); %! assert_equal (fieldnames (Mdl.ModelParameters), ... %! {'DistributionNames'; 'Kernel'; 'Support'; 'Width'; ... %! 'StandardizeData'; 'Version'; 'Method'; 'Type'}); %! assert_equal (Mdl.ModelParameters.DistributionNames, 'normal'); %! assert_equal (Mdl.ModelParameters.Kernel, []); %! assert_equal (Mdl.ModelParameters.Method, 'NaiveBayes'); %! assert_equal (Mdl.ModelParameters.Type, 'classification'); %! assert_equal (Mdl.ModelParameters.Version, 1); %! Mdl = fitcnb (meas, species, 'DistributionNames', 'kernel'); %! assert_equal (Mdl.ModelParameters.DistributionNames, 'kernel'); %! assert_equal (Mdl.ModelParameters.Kernel, 'normal'); %! assert_equal (Mdl.ModelParameters.Support, 'unbounded'); %! assert_equal (isnan (Mdl.ModelParameters.Width), true); %! assert_equal (Mdl.ModelParameters.StandardizeData, 0); %!test # naming a subset of the classes keeps only those observations %! load fisheriris %! Mdl = fitcnb (meas, species, 'ClassNames', {'setosa', 'virginica'}); %! assert_equal (Mdl.ClassNames, {'setosa'; 'virginica'}); %! assert_equal (Mdl.NumObservations, 100); %! assert_equal (sum (Mdl.RowsUsed), 100); %! assert_equal (size (Mdl.DistributionParameters), [2, 4]); %! assert_equal (Mdl.Prior, [0.5, 0.5], 1e-15); %!test # a row missing one predictor is kept, as fitcnb's documentation says %! load fisheriris %! X = meas; %! X(3,2) = NaN; %! X(77,4) = NaN; %! Mdl = fitcnb (X, species); %! assert_equal (Mdl.NumObservations, 150); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (size (Mdl.X), [150, 4]); %!test # PredictorNames and ResponseName are carried through %! load fisheriris %! Mdl = fitcnb (meas, species, 'PredictorNames', {'a', 'b', 'c', 'd'}, ... %! 'ResponseName', 'flower'); %! assert_equal (Mdl.PredictorNames, {'a', 'b', 'c', 'd'}); %! assert_equal (Mdl.ExpandedPredictorNames, {'a', 'b', 'c', 'd'}); %! assert_equal (Mdl.ResponseName, 'flower'); %!test # Cost and Prior may be assigned after fitting, and W follows the prior %! load fisheriris %! Mdl = fitcnb (meas, species); %! Mdl.Cost = [0, 2, 2; 2, 0, 2; 2, 2, 0]; %! assert_equal (Mdl.Cost, [0, 2, 2; 2, 0, 2; 2, 2, 0]); %! Mdl.Prior = [0.2, 0.3, 0.5]; %! assert_equal (Mdl.Prior, [0.2, 0.3, 0.5], 1e-15); %! assert_equal (Mdl.W(1), 0.004, 1e-15); %! assert_equal (Mdl.W(101), 0.010, 1e-15); %!test # a numeric response is classified as a numeric response %! X = [1, 2; 1.1, 2.1; 5, 6; 5.2, 6.1; 0.9, 1.8; 5.1, 6.2]; %! Y = [1; 1; 2; 2; 1; 2]; %! Mdl = fitcnb (X, Y); %! assert_equal (Mdl.ClassNames, [1; 2]); %! assert_equal (predict (Mdl, [1, 2; 5, 6]), [1; 2]); %!test # MATLAB parity: compact classifies identically to the model it came from %! load fisheriris %! Mdl = fitcnb (meas, species); %! CMdl = compact (Mdl); %! assert_equal (class (CMdl), 'CompactClassificationNaiveBayes'); %! [ml, ms] = predict (Mdl, meas); %! [cl, cs] = predict (CMdl, meas); %! assert_equal (cl, ml); %! assert_equal (cs, ms); %! assert_equal (loss (CMdl, meas, species), 0.04, 1e-14); %! assert_equal (edge (CMdl, meas, species), 0.894430597464877, 1e-12); %!test # MATLAB parity: leave-one-out cross-validation %! load fisheriris %! Mdl = fitcnb (meas, species); %! CVMdl = crossval (Mdl, 'Leaveout', 'on'); %! assert_equal (class (CVMdl), 'ClassificationPartitionedModel'); %! assert_equal (CVMdl.KFold, 150); %! assert_equal (class (CVMdl.Trained{1}), 'CompactClassificationNaiveBayes'); %! assert_equal (kfoldLoss (CVMdl), 0.046666666666667, 1e-12); %! assert_equal (sum (strcmp (kfoldPredict (CVMdl), species)), 143); %!test # a k-fold partition holds k folds, each a model of its own %! load fisheriris %! CVMdl = crossval (fitcnb (meas, species), 'KFold', 3); %! assert_equal (CVMdl.KFold, 3); %! assert_equal (numel (CVMdl.Trained), 3); %! assert_equal (class (CVMdl.Trained{3}), 'CompactClassificationNaiveBayes'); %! assert_equal (CVMdl.CrossValidatedModel, 'NaiveBayes'); ## The categorical distributions. Every level's count is raised by one before ## it is normalized, so a level a class never took stays possible. %!test # MATLAB parity: mvmn fits a smoothed distribution over the levels %! X = [1, 2; 1, 3; 2, 2; 2, 3; 1, 2; 3, 1; 3, 3; 2, 1; 1, 1; 3, 2; ... %! 2, 2; 1, 3; 3, 1; 2, 3; 1, 1; 3, 2; 2, 1; 1, 2; 3, 3; 2, 2]; %! Y = [repmat({'a'}, 10, 1); repmat({'b'}, 10, 1)]; %! Mdl = fitcnb (X, Y, 'DistributionNames', 'mvmn', ... %! 'CategoricalPredictors', [1, 2]); %! assert_equal (Mdl.DistributionNames, {'mvmn', 'mvmn'}); %! assert_equal (Mdl.CategoricalPredictors, [1, 2]); %! assert_equal (Mdl.CategoricalLevels{1}, [1; 2; 3]); %! assert_equal (Mdl.CategoricalLevels{2}, [1; 2; 3]); %! assert_equal (Mdl.DistributionParameters{1,1}, ... %! [0.384615384615385; 0.307692307692308; 0.307692307692308], ... %! 1e-12); %! assert_equal (Mdl.DistributionParameters{2,2}(2), ... %! 0.384615384615385, 1e-12); %! assert_equal (all (isnan (Mdl.Width(:))), true); %! assert_equal (size (Mdl.Width), [2, 2]); %!test # MATLAB parity: an mvmn model classifies as MATLAB does %! X = [1, 2; 1, 3; 2, 2; 2, 3; 1, 2; 3, 1; 3, 3; 2, 1; 1, 1; 3, 2; ... %! 2, 2; 1, 3; 3, 1; 2, 3; 1, 1; 3, 2; 2, 1; 1, 2; 3, 3; 2, 2]; %! Y = [repmat({'a'}, 10, 1); repmat({'b'}, 10, 1)]; %! Mdl = fitcnb (X, Y, 'DistributionNames', 'mvmn', ... %! 'CategoricalPredictors', [1, 2]); %! [label, score] = predict (Mdl, [1, 1; 3, 3; 2, 2]); %! assert_equal (label, {'a'; 'a'; 'b'}); %! assert_equal (score(1,1), 0.555555555555556, 1e-12); %! assert_equal (score(3,2), 0.555555555555556, 1e-12); %! assert_equal (resubLoss (Mdl), 0.45, 1e-14); %! assert_equal (logp (Mdl, [1, 1; 3, 3]), ... %! [-2.239526957026909; -2.357309992683292], 1e-12); %!test # MATLAB parity: a level a class never took keeps a probability %! Z = [1, 2; 1, 3; 1, 2; 1, 3; 1, 2; 2, 1; 2, 3; 2, 1; 2, 3; 2, 2]; %! G = [repmat({'p'}, 5, 1); repmat({'q'}, 5, 1)]; %! Mdl = fitcnb (Z, G, 'DistributionNames', 'mvmn', ... %! 'CategoricalPredictors', [1, 2]); %! assert_equal (Mdl.CategoricalLevels{1}, [1; 2]); %! assert_equal (Mdl.DistributionParameters{1,1}, ... %! [0.857142857142857; 0.142857142857143], 1e-12); %! assert_equal (Mdl.DistributionParameters{2,1}, ... %! [0.142857142857143; 0.857142857142857], 1e-12); %! [label, score] = predict (Mdl, [2, 2; 1, 1]); %! assert_equal (label, {'q'; 'p'}); %! assert_equal (score(1,:), [0.25, 0.75], 1e-12); %!test # MATLAB parity: naming a predictor categorical makes it mvmn %! X = [1, 2; 1, 3; 2, 2; 2, 3; 1, 2; 3, 1; 3, 3; 2, 1; 1, 1; 3, 2; ... %! 2, 2; 1, 3; 3, 1; 2, 3; 1, 1; 3, 2; 2, 1; 1, 2; 3, 3; 2, 2]; %! Y = [repmat({'a'}, 10, 1); repmat({'b'}, 10, 1)]; %! Mdl = fitcnb (X, Y, 'CategoricalPredictors', [1, 2]); %! assert_equal (Mdl.DistributionNames, {'mvmn', 'mvmn'}); %! assert_equal (Mdl.CategoricalPredictors, [1, 2]); %! assert_equal (Mdl.DistributionParameters{1,1}(1), ... %! 0.384615384615385, 1e-12); %!test # MATLAB parity: a normal and a categorical predictor side by side %! X = [1, 2; 1, 3; 2, 2; 2, 3; 1, 2; 3, 1; 3, 3; 2, 1; 1, 1; 3, 2; ... %! 2, 2; 1, 3; 3, 1; 2, 3; 1, 1; 3, 2; 2, 1; 1, 2; 3, 3; 2, 2]; %! Y = [repmat({'a'}, 10, 1); repmat({'b'}, 10, 1)]; %! Mdl = fitcnb (X, Y, 'DistributionNames', {'normal', 'mvmn'}, ... %! 'CategoricalPredictors', 2); %! assert_equal (Mdl.DistributionNames, {'normal', 'mvmn'}); %! assert_equal (isempty (Mdl.CategoricalLevels{1}), true); %! assert_equal (Mdl.CategoricalLevels{2}, [1; 2; 3]); %! assert_equal (Mdl.DistributionParameters{1,1}, ... %! [1.900000000000001; 0.875595035770913], 1e-12); %! assert_equal (Mdl.DistributionParameters{1,2}(2), ... %! 0.384615384615385, 1e-12); ## The multinomial reads a row as token counts and is one distribution over ## the whole predictor vector, so its DistributionNames stays a character ## vector where every other distribution reports one name per predictor. %!test # MATLAB parity: mn over token counts %! C = [2, 0, 1; 1, 3, 0; 0, 1, 4; 3, 1, 0; ... %! 0, 2, 2; 1, 0, 3; 4, 1, 1; 0, 3, 2]; %! L = [repmat({'x'}, 4, 1); repmat({'y'}, 4, 1)]; %! Mdl = fitcnb (C, L, 'DistributionNames', 'mn'); %! assert_equal (Mdl.DistributionNames, 'mn'); %! assert_equal (class (Mdl.DistributionNames), 'char'); %! assert_equal (size (Mdl.DistributionParameters), [2, 3]); %! assert_equal (Mdl.DistributionParameters{1,1}, 0.368421052631579, 1e-12); %! assert_equal (Mdl.DistributionParameters{1,2}, 0.315789473684211, 1e-12); %! assert_equal (Mdl.DistributionParameters{2,3}, 0.409090909090909, 1e-12); %! assert_equal (Mdl.CategoricalPredictors, []); %!test # MATLAB parity: an mn model classifies as MATLAB does %! C = [2, 0, 1; 1, 3, 0; 0, 1, 4; 3, 1, 0; ... %! 0, 2, 2; 1, 0, 3; 4, 1, 1; 0, 3, 2]; %! L = [repmat({'x'}, 4, 1); repmat({'y'}, 4, 1)]; %! Mdl = fitcnb (C, L, 'DistributionNames', 'mn'); %! [label, score] = predict (Mdl, [1, 1, 1; 4, 0, 0]); %! assert_equal (label, {'x'; 'x'}); %! assert_equal (score(1,1), 0.508585484679865, 1e-12); %! assert_equal (score(2,1), 0.769060987976952, 1e-12); %! assert_equal (resubLoss (Mdl), 0.25, 1e-14); ## An observation carrying a level the model never saw tells it nothing, so ## its posterior is the prior. One such predictor decides the whole ## observation, whatever the others say. %!test # MATLAB parity: an unseen level falls back to the prior %! Z = [1, 1; 1, 1; 1, 2; 2, 2; 2, 2; 2, 1; 1, 1; 2, 2]; %! G = [repmat({'p'}, 4, 1); repmat({'q'}, 4, 1)]; %! Mdl = fitcnb (Z, G, 'DistributionNames', 'mvmn', ... %! 'CategoricalPredictors', [1, 2]); %! [~, score] = predict (Mdl, [1, 1]); %! assert_equal (score, [0.666666666666667, 0.333333333333333], 1e-12); %! [~, score, cost] = predict (Mdl, [3, 1]); %! assert_equal (score, [0.5, 0.5], 1e-12); %! assert_equal (cost, [0.5, 0.5], 1e-12); %! [~, score] = predict (Mdl, [3, 3]); %! assert_equal (score, [0.5, 0.5], 1e-12); %! assert_equal (logp (Mdl, [1, 1]), -1.386294361119891, 1e-12); %! assert_equal (logp (Mdl, [3, 1]), -Inf); %!test # MATLAB parity: the fallback is the prior, not a uniform distribution %! Z = [1, 1; 1, 1; 1, 2; 1, 2; 1, 1; 1, 2; 2, 2; 2, 1]; %! G = [repmat({'p'}, 6, 1); repmat({'q'}, 2, 1)]; %! Mdl = fitcnb (Z, G, 'DistributionNames', 'mvmn', ... %! 'CategoricalPredictors', [1, 2]); %! assert_equal (Mdl.Prior, [0.75, 0.25], 1e-15); %! [~, score] = predict (Mdl, [3, 1]); %! assert_equal (score, [0.75, 0.25], 1e-12); %! Mdl = fitcnb (Z, G, 'DistributionNames', 'mvmn', ... %! 'CategoricalPredictors', [1, 2], 'Prior', [0.2, 0.8]); %! [label, score] = predict (Mdl, [3, 1]); %! assert_equal (score, [0.2, 0.8], 1e-12); %! assert_equal (label, {'q'}); %!test # MATLAB parity: an informative predictor cannot rescue the row %! Z = [1, 1; 1, 1; 1, 2; 2, 2; 2, 2; 2, 1; 1, 1; 2, 2]; %! G = [repmat({'p'}, 4, 1); repmat({'q'}, 4, 1)]; %! Mdl = fitcnb (Z, G, 'DistributionNames', {'normal', 'mvmn'}, ... %! 'CategoricalPredictors', 2); %! [~, score] = predict (Mdl, [1, 3]); %! assert_equal (score, [0.5, 0.5], 1e-12); %! assert_equal (logp (Mdl, [1, 3]), -Inf); ## Naming 'mvmn' for a predictor makes it categorical, which MATLAB reports ## rather than doing silently. %!warning ... %! fitcnb ([1, 2; 2, 1; 1, 1; 2, 2], [1; 1; 2; 2], ... %! 'DistributionNames', 'mvmn'); %!test # the warning is not raised when the predictor is already categorical %! Mdl = fitcnb ([1, 2; 2, 1; 1, 1; 2, 2], [1; 1; 2; 2], ... %! 'DistributionNames', 'mvmn', 'CategoricalPredictors', 'all'); %! assert_equal (Mdl.CategoricalPredictors, [1, 2]); %! assert_equal (Mdl.DistributionNames, {'mvmn', 'mvmn'}); ## A fitted model survives savemodel and loadmodel: every property comes back ## as it was and it predicts the same. %!test %! load fisheriris %! Mdl = fitcnb (meas, species); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2), 'ClassificationNaiveBayes'); %! p = properties (Mdl); %! for i = 1:numel (p) %! assert_equal (M2.(p{i}), Mdl.(p{i})); %! endfor %! assert_equal (predict (M2, meas(1:10,:)), predict (Mdl, meas(1:10,:))); ## A kernel predictor stores a classdef object, which Octave's save cannot ## serialize. It is written as the sample and refitted at the recorded ## bandwidth, so the density comes back identical and not merely close. %!test %! load fisheriris %! Mdl = fitcnb (meas, species, 'DistributionNames', 'kernel'); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2.DistributionParameters{1,1}), ... %! 'prob.KernelDistribution'); %! assert_equal (M2.Width, Mdl.Width); %! assert_equal (predict (M2, meas(1:10,:)), predict (Mdl, meas(1:10,:))); %! assert_equal (logp (M2, meas(1:10,:)), logp (Mdl, meas(1:10,:)), 1e-12); ## Mixed distributions, a categorical predictor among them, and a model ## carrying a transform and a non-default cost. %!test %! load fisheriris %! X = [meas, round(meas(:,1))]; %! Mdl = fitcnb (X, species, 'DistributionNames', ... %! {'normal', 'kernel', 'normal', 'kernel', 'mvmn'}, ... %! 'CategoricalPredictors', 5, ... %! 'Cost', [0, 2, 1; 1, 0, 1; 1, 1, 0]); %! Mdl.ScoreTransform = 'logit'; %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.Cost, Mdl.Cost); %! assert_equal (M2.ScoreTransform, 'logit'); %! assert_equal (M2.CategoricalLevels, Mdl.CategoricalLevels); %! [l1, s1] = predict (Mdl, X(1:10,:)); %! [l2, s2] = predict (M2, X(1:10,:)); %! assert_equal (l2, l1); %! assert_equal (s2, s1); ## MATLAB parity: a categorical response keeps only the categories it holds. %!test %! load fisheriris %! Mdl = fitcnb (meas(51:150,:), categorical (species(51:150))); %! assert_equal (categories (Mdl.ClassNames), {'versicolor'; 'virginica'}); %!error ... %! savemodel (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2])) %!error ... %! savemodel (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), 1) %!error ... %! savemodel (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), ['ab'; 'cd']) %!error ... %! fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], 'DistributionNames', {'mn', 'normal'}) %!error ... %! fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], 'CategoricalPredictors', 'some') %!error ... %! fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], 'CategoricalPredictors', [true, true, true]) %!error ... %! fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], 'CategoricalPredictors', 5) ## A missing predictor is skipped, not propagated: the predictors are ## conditionally independent given the class, so the others still describe the ## observation. An observation missing every predictor falls back to the ## prior, by the same arithmetic that gives an unseen level the prior. %!test # MATLAB parity: predict skips a NaN predictor %! load fisheriris %! Mdl = fitcnb (meas, species); %! [label, score, cost] = predict (Mdl, [NaN, 3, 1, 0.2]); %! assert_equal (label, {'setosa'}); %! assert_equal (score, [1, 0, 0], 1e-12); %! assert_equal (cost, [0, 1, 1], 1e-12); %! [~, s1] = predict (Mdl, [5.1, 3.5, 1.4, 0.2]); %! [~, s2] = predict (Mdl, [5.1, NaN, 1.4, 0.2]); %! assert_equal (s2, s1, 1e-12); %! [~, s3] = predict (Mdl, [NaN, NaN, NaN, NaN]); %! assert_equal (s3, Mdl.Prior, 1e-12); ## logp reports NaN where predict reports a class: predict can classify on the ## predictors it has, but a density is not defined for a partial observation. %!test # MATLAB parity: logp of an incomplete observation is NaN %! load fisheriris %! Mdl = fitcnb (meas, species); %! assert_equal (isnan (logp (Mdl, [NaN, 3, 1, 0.2])), true); %! assert_equal (isnan (logp (Mdl, [5.1, NaN, 1.4, 0.2])), true); %! assert_equal (logp (Mdl, [5.1, 3.5, 1.4, 0.2]), 1.026591235856343, 1e-12); %!test # MATLAB parity: a row with a missing predictor still counts in a loss %! load fisheriris %! Mdl = fitcnb (meas, species); %! X = [meas; NaN, 3, 1, 0.2]; %! Y = [species; {'setosa'}]; %! assert_equal (loss (Mdl, X, Y), 0.04, 1e-14); %! assert_equal (loss (Mdl, X, Y, 'LossFun', 'classiferror'), 0.04, 1e-14); ## A predictor that does not vary within a class has no normal density to fit. ## MATLAB refuses the combination rather than answering from a distribution it ## never fitted, and so do we. The refusal is per class and predictor, not per ## model: a kernel on the same column fits, which is the caller's way out. %!test # MATLAB parity: a kernel fits data a normal cannot %! X = [1, 2; 2, 3; 3, 4; 10, 20; 10, 25]; %! Y = [1; 1; 1; 2; 2]; %! Mdl = fitcnb (X, Y, 'DistributionNames', 'kernel'); %! assert_equal (class (Mdl), 'ClassificationNaiveBayes'); %! assert_equal (Mdl.Width(2,1), 1, 1e-12); %! Mdl = fitcnb (X, Y, 'DistributionNames', 'mvmn', ... %! 'CategoricalPredictors', [1, 2]); %! assert_equal (Mdl.CategoricalLevels{1}, [1; 2; 3; 10]); ## The row that proves the guard is per combination rather than per model: ## give the degenerate column a kernel and leave the other normal, and the fit ## goes through. Widening the guard to the whole model would fail this. %!test # MATLAB parity: only the degenerate combination is refused %! X = [1, 2; 2, 3; 3, 4; 10, 20; 10, 25]; %! Y = [1; 1; 1; 2; 2]; %! Mdl = fitcnb (X, Y, 'DistributionNames', {'kernel', 'normal'}); %! assert_equal (Mdl.DistributionNames, {'kernel', 'normal'}); %! assert_equal (Mdl.DistributionParameters{2,2}, [22.5; 3.535533905932738], ... %! 1e-12); %!test # A categorical 'ClassNames' keeps only the classes it names %! load fisheriris %! Mdl = ClassificationNaiveBayes (meas, categorical (species), ... %! 'ClassNames', categorical ({'versicolor'; 'virginica'})); %! assert_equal (Mdl.NumObservations, 100); %! assert_equal (class (Mdl.ClassNames), 'categorical'); %!test # cellstr 'ClassNames' over a categorical response keep its type %! load fisheriris %! Mdl = ClassificationNaiveBayes (meas, categorical (species), ... %! 'ClassNames', {'virginica'; 'versicolor'}); %! assert_equal (class (Mdl.ClassNames), 'categorical'); %! assert_equal (cellstr (Mdl.ClassNames), {'virginica'; 'versicolor'}); %! assert_equal (Mdl.NumObservations, 100); ## Test input validation %!error ... %! ClassificationNaiveBayes ([1, 2; 2, 3; 3, 4; 4, 5]) %!error ... %! ClassificationNaiveBayes ([1, 2; 2, 3; 3, 4; 4, 5], ones (4, 1), 'Prior') %!error ... %! ClassificationNaiveBayes ('a', ones (4, 1)) %!error ... %! ClassificationNaiveBayes ([1, 2; 2, 3; 3, 4; 4, 5], ones (3, 1)) %!error ... %! ClassificationNaiveBayes ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], 'PredictorNames', 5) %!error ... %! ClassificationNaiveBayes ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], 'PredictorNames', {'a'}) %!error ... %! ClassificationNaiveBayes ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], 'ResponseName', 5) %!error ... %! ClassificationNaiveBayes ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], 'ClassNames', [1, 3]) %!error ... %! ClassificationNaiveBayes ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], 'nope', 1) %!error ... %! ClassificationNaiveBayes ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], ... %! 'DistributionNames', 'poisson') %!error ... %! ClassificationNaiveBayes ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], ... %! 'DistributionNames', {'normal'}) %!error ... %! ClassificationNaiveBayes ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], ... %! 'DistributionNames', 'kernel', 'Kernel', 'cosine') %!error ... %! ClassificationNaiveBayes ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], ... %! 'DistributionNames', 'kernel', 'Width', -1) %!error ... %! predict (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2])) %!error ... %! predict (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), []) %!error ... %! predict (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), ones (2, 3)) %!error ... %! loss (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), [1, 2; 2, 3; 3, 4; 4, 5]) %!error ... %! loss (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), [1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], ... %! 'LossFun', 'nope') %!error ... %! loss (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), ... %! [1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], 'Bogus', 1) %!error ... %! loss (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), [1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], ... %! 'Weights', [1, 2]) %!error ... %! margin (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), [1, 2; 2, 3; 3, 4; 4, 5]) %!error ... %! edge (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), [1, 2; 2, 3; 3, 4; 4, 5]) %!error ... %! logp (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), []) %!error ... %! crossval (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), 'KFold', 1) %!error ... %! crossval (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), 'Leaveout', 5) %!error ... %! crossval (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), 'KFold', 2, 'Leaveout', 'on') %!error ... %! fitcnb ([1, 2; 2, 3; 3, 4; 10, 20], [1; 1; 1; 2]) %!error ... %! fitcnb ([1, 2; 2, 3; 3, 4; 10, 20; 10, 25], [1; 1; 1; 2; 2]) %!error ... %! fitcnb ([1, 2; 2, 3; 3, 4; 10, 20; 10, 25], ... %! [{'alpha'}; {'alpha'}; {'alpha'}; {'beta'}; {'beta'}]) %!error ... %! fitcnb ([1, 2; 2, 3; 3, 4; 10, 20; 10, 25], [1; 1; 1; 2; 2], ... %! 'PredictorNames', {'height', 'weight'}) %!error ... %! fitcnb ([5, 2; 5, 3; 5, 4; 10, 20; 11, 25], [1; 1; 1; 2; 2]) ## HyperparameterOptimizationResults is declared for MATLAB compatibility and ## stays empty, this class running no search over its hyperparameters. %!test %! load fisheriris %! Mdl = fitcnb (meas, species); %! assert_equal (isempty (Mdl.HyperparameterOptimizationResults), true); ## Every documented score transform reaches the scores that are reported, and ## none of them moves the label: a transform reshapes what is reported, not ## what is decided. %!test %! load fisheriris %! Mdl = fitcnb (meas, species); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! T = {'identity', @(x) x; 'doublelogit', @(x) 1 ./ (1 + exp (-2 * x)); ... %! 'invlogit', @(x) log (x ./ (1 - x)); ... %! 'logit', @(x) 1 ./ (1 + exp (-x)); ... %! 'sign', @(x) sign (x); 'symmetric', @(x) 2 * x - 1; ... %! 'symmetriclogit', @(x) 2 ./ (1 + exp (-x)) - 1}; %! for i = 1:rows (T) %! Mdl.ScoreTransform = T{i,1}; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, T{i,2}(raw), 1e-12); %! assert_equal (l, label); %! endfor %! ## ismax marks the largest score of each observation, ties to the first. %! [~, k] = max (raw, [], 2); %! e = zeros (size (raw)); %! e(sub2ind (size (raw), (1:rows (raw))', k)) = 1; %! Mdl.ScoreTransform = 'ismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, e); %! Mdl.ScoreTransform = 'symmetricismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, 2 * e - 1); ## A function handle is taken as given and applied to the scores. %!test %! load fisheriris %! Mdl = fitcnb (meas, species); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! Mdl.ScoreTransform = @(x) x .^ 2; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, raw .^ 2, 1e-12); %! assert_equal (l, label); ## A table at prediction %!test # the class may be built from a table as the fitter builds it %! load fisheriris %! T = table (meas(:,1), meas(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = categorical (species); %! Mdl = ClassificationNaiveBayes (T, 'Species'); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW'}); %! assert_equal (Mdl.ResponseName, 'Species'); ## MATLAB parity: empty text, a blank character row and a missing string are ## missing, not a level, as measured on R2024a %!shared nbT, nbL %! nbT = {'a'; ''; 'b'; 'a'; 'b'; 'a'; ''; 'b'; 'a'; 'b'; 'a'; 'b'}; %! nbL = repmat ({'p'; 'q'}, 6, 1); %!test # empty text %! Mdl = fitcnb (table (nbT, nbL), 'nbL'); %! assert_equal (Mdl.CategoricalLevels{1}, [1; 2]); %! assert_equal (Mdl.DistributionParameters{1,1}, [4; 3] / 7, 1e-15); %!test # an empty and a missing string %! s = string (nbT); %! s(2) = missing; %! Mdl = fitcnb (table (s, nbL), 'nbL'); %! assert_equal (Mdl.CategoricalLevels{1}, [1; 2]); %!test # a blank character row %! Mdl = fitcnb (table (char (nbT), nbL), 'nbL'); %! assert_equal (Mdl.CategoricalLevels{1}, [1; 2]); %!test # an ordinal column stays categorical here, as measured on R2024a %! o = categorical (repmat ({'lo'; 'hi'; 'lo'; 'mid'; 'hi'; 'lo'; 'mid'; ... %! 'hi'; 'lo'; 'mid'; 'hi'; 'lo'}, 2, 1), ... %! {'lo', 'mid', 'hi'}, 'Ordinal', true); %! x = (1:24)'; %! lab = repmat ({'p'; 'q'; 'p'; 'q'; 'p'; 'q'; 'p'; 'q'; 'p'; 'q'; 'p'; ... %! 'q'}, 2, 1); %! Mdl = fitcnb (table (o, x, lab), 'lab'); %! assert_equal (Mdl.CategoricalPredictors, 1); %!test # 'Weights' may name a table variable, no predictor then, as R2024a %! x1 = (1:12)'; %! w = [1; 1; 2; 2; 1; 1; 3; 3; 1; 1; 2; 2]; %! lab = repmat ({'p'; 'q'}, 6, 1); %! Mdl = fitcnb (table (x1, w, lab), 'lab', 'Weights', 'w'); %! assert_equal (Mdl.PredictorNames, {'x1'}); %! assert_equal (Mdl.W, w / 20, 1e-15); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcnb (T, 'Species'); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'Species'), a); %! assert_equal (loss (Mdl, T), a); ## A table at edge %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcnb (T, 'Species'); %! a = edge (Mdl, X, y); %! assert_equal (edge (Mdl, T(:,1:2), y), a); %! assert_equal (edge (Mdl, T, 'Species'), a); %! assert_equal (edge (Mdl, T), a); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcnb (T, 'Species'); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## A table at logp %!test # a table is matched to the predictors by name %! load fisheriris %! T = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species); %! Mdl = fitcnb (T, 'Species'); %! a = logp (Mdl, meas); %! assert_equal (logp (Mdl, T(:,1:4)), a); %! assert_equal (logp (Mdl, T(:,[5, 4, 2, 3, 1])), a); ## Observation weights of class single or double %!error ... %! loss (fitcnb ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2]), ... %! [1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', int8 ([1; 1; 1; 1])) ## Observation weights %!error ... %! ClassificationNaiveBayes ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], ... %! 'Weights', int8 ([1; 1; 1; 1])) %!error ... %! ClassificationNaiveBayes ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], ... %! 'Weights', true (4, 1)) %!error ... %! ClassificationNaiveBayes ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], ... %! 'Weights', [1; 1]) %!error ... %! ClassificationNaiveBayes ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], ... %! 'Weights', [1; -1; 1; 1]) %!test %! ## An empirical prior and a normal density weigh the observations %! load fisheriris %! Mdl = ClassificationNaiveBayes (meas, species, 'Weights', 1 + (1:150)' / 7); %! assert_equal (Mdl.Prior, [0.1313131313131313, 0.3333333333333333, ... %! 0.5353535353535354], 1e-15); %! assert_equal (Mdl.DistributionParameters{1,1}, ... %! [5.0008; 0.3400154735180538], 1e-13); %!test %! ## A kernel density weighs the observations, its bandwidth does not %! load fisheriris %! Mdl = ClassificationNaiveBayes (meas, species, ... %! 'Weights', 1 + (1:150)' / 7, ... %! 'DistributionNames', 'kernel'); %! Mdl0 = ClassificationNaiveBayes (meas, species, ... %! 'DistributionNames', 'kernel'); %! assert_equal (Mdl.Width, Mdl0.Width); %! [~, Posterior] = predict (Mdl, meas(51,:)); %! assert_equal (Posterior, [0, 0.7141545707763224, 0.2858454292236776], 1e-12); %!test %! ## A multivariate multinomial counts the observations by their weights %! load fisheriris %! Mdl = ClassificationNaiveBayes (round (meas), species, ... %! 'Weights', 1 + (1:150)' / 7, ... %! 'DistributionNames', 'mvmn'); %! assert_equal (Mdl.DistributionParameters{1,1}, ... %! [0.09258741258741258; 0.7655944055944056; ... %! 0.1054545454545455; 0.01818181818181818; ... %! 0.01818181818181818], 1e-15); %!test %! ## A multinomial counts the tokens by their weights %! load fisheriris %! Mdl = ClassificationNaiveBayes (round (meas * 2), species, ... %! 'Weights', 1 + (1:150)' / 7, ... %! 'DistributionNames', 'mn'); %! assert_equal ([Mdl.DistributionParameters{1,:}], ... %! [0.4954405908994194, 0.3390224626888355, ... %! 0.1481959937992036, 0.01734095261254141], 1e-15); %!test %! ## Rows of zero weight are left out, as R2024a leaves them %! load fisheriris %! w = 1 + (1:150)' / 7; %! w(5) = 0; %! Mdl = ClassificationNaiveBayes (meas, species, 'Weights', w); %! assert_equal (Mdl.NumObservations, 149); %! assert_equal (Mdl.RowsUsed(5), false); %!test %! ## Single weights are stored single, summing to one %! load fisheriris %! Mdl = ClassificationNaiveBayes (meas, species, ... %! 'Weights', single (1 + (1:150)' / 7)); %! assert_equal (class (Mdl.W), 'single'); %! assert_equal (sum (double (Mdl.W)), 1, 1e-6); %!test %! ## resubLoss weighs the observations by W, as R2024a does %! load fisheriris %! Mdl = ClassificationNaiveBayes (meas, species, 'Weights', 1 + (1:150)' / 7); %! assert_equal (resubLoss (Mdl), 0.06141414141414142, 1e-15); %!test %! ## Reassigning the prior keeps W in proportion to the weights %! load fisheriris %! w = 1 + (1:150)' / 7; %! Mdl = ClassificationNaiveBayes (meas, species, 'Weights', w); %! Mdl.Prior = [1, 1, 1] / 3; %! assert_equal (Mdl.W(1:50), w(1:50) / sum (w(1:50)) / 3, 1e-15); %!test %! ## A weighted kernel model saves and loads with its weights %! load fisheriris %! Mdl = ClassificationNaiveBayes (meas, species, ... %! 'Weights', 1 + (1:150)' / 7, ... %! 'DistributionNames', 'kernel'); %! fname = [tempname(), '.mat']; %! savemodel (Mdl, fname); %! Mdl2 = loadmodel (fname); %! delete (fname); %! assert_equal (nthargout (2, @predict, Mdl2, meas(51,:)), ... %! nthargout (2, @predict, Mdl, meas(51,:)), 1e-15); ## Test a class with prior 0, whose observations are removed before fitting, ## as MATLAB R2024a and R2026b remove them %!warning ... %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! Mdl = ClassificationNaiveBayes (X, Y, 'Prior', [1, 0]); %! assert_equal (Mdl.NumObservations, 50); %! assert_equal (Mdl.RowsUsed, [true(50, 1); false(50, 1)]); %! assert_equal (Mdl.Prior, [1, 0]); %! assert_equal (predict (Mdl, meas([51, 110], 3:4)), ... %! {'versicolor'; 'versicolor'}); %!warning ... %! load fisheriris %! Mdl = ClassificationNaiveBayes (meas, species, 'Prior', [0.5, 0.5, 0]); %! assert_equal (predict (Mdl, meas([1, 51, 101], :)), ... %! {'setosa'; 'versicolor'; 'versicolor'}); %!test %! ## A named class with prior 0 may be absent, having nothing to remove %! load fisheriris %! X = meas(51:100, 3:4); %! Y = species(51:100); %! C = {'versicolor', 'virginica'}; %! Mdl = ClassificationNaiveBayes (X, Y, 'ClassNames', C, 'Prior', [1, 0]); %! assert_equal (Mdl.ClassNames, {'versicolor'; 'virginica'}); %! assert_equal (predict (Mdl, meas(51, 3:4)), {'versicolor'}); %!warning ... %! load fisheriris %! Mdl = ClassificationNaiveBayes (meas(51:150, 3:4), species(51:150), ... %! 'Prior', [1, 0]); %! assert_equal (Mdl.DistributionParameters(2,:), {[], []}); %!test %! load fisheriris %! Mdl = ClassificationNaiveBayes (meas(51:150, 3:4), species(51:150), ... %! 'Cost', [0, 1; 0, 0]); %! assert_equal (Mdl.NumObservations, 100); %! assert_equal (Mdl.Prior, [0.5, 0.5]); %!test %! load fisheriris %! Y = string (species(51:100)); %! C = {'versicolor', 'virginica'}; %! Mdl = ClassificationNaiveBayes (meas(51:100, 3:4), Y, 'ClassNames', C, ... %! 'Prior', [1, 0]); %! assert_equal (char (predict (Mdl, meas(51, 3:4))), 'versicolor'); %!warning ... %! load fisheriris %! Mdl = ClassificationNaiveBayes (meas, species, 'Prior', [0.5, 0, 0.5]); %! fname = [tempname(), '.mat']; %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (predict (M2, meas([1, 51, 101], :)), ... %! predict (Mdl, meas([1, 51, 101], :))); ## MATLAB parity: a row missing only some predictors is kept, each predictor ## is fitted on the rows it is known for, and the empirical prior counts ## every row kept; a row with a missing class, one missing every predictor ## and one of zero or missing weight are dropped, as measured on R2024a %!shared nbX, nbL %! k = repmat ([1; 2; 3; 1; 2; 3; 1; 2], 3, 1); %! x = mod ((1:24)', 7) + (1:24)' / 10; %! nbL = repmat ({'p'; 'q'; 'q'; 'p'; 'p'; 'q'; 'p'; 'q'}, 3, 1); %! x([2, 7, 15]) = NaN; %! k([4, 11]) = NaN; %! nbX = [k, x]; %!test # every row is kept, and the prior is over all of them %! Mdl = fitcnb (nbX, nbL, 'CategoricalPredictors', 1); %! assert_equal (Mdl.NumObservations, 24); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (Mdl.Prior, [0.5, 0.5]); %!test # a categorical predictor is fitted on the rows it is known for %! Mdl = fitcnb (nbX, nbL, 'CategoricalPredictors', 1); %! assert_equal (Mdl.DistributionParameters{1,1}, [9; 4; 1] / 14, 1e-15); %!test # and so is a normal one %! Mdl = fitcnb (nbX, nbL, 'CategoricalPredictors', 1); %! assert_equal (Mdl.DistributionParameters{1,2}, ... %! [4.65; 2.20416272841488], 1e-13); %! assert_equal (Mdl.DistributionParameters{2,2}, ... %! [4.28181818181818; 1.83293108534837], 1e-13); %!test # predict skips the predictor a query is missing %! Mdl = fitcnb (nbX, nbL, 'CategoricalPredictors', 1); %! [~, score] = predict (Mdl, [1, 3; 2, 5; NaN, 7; 3, NaN]); %! assert_equal (score(:,1), [0.878375277703031; 0.336285341421947; ... %! 0.585845090044561; 0.142857142857143], 1e-13); %!test # weights shape the fit on the rows a predictor is known for %! Mdl = fitcnb (nbX, nbL, 'CategoricalPredictors', 1, ... %! 'Weights', 1 + mod ((1:24)', 3)); %! assert_equal (Mdl.DistributionParameters{1,2}, ... %! [4.96666666666667; 2.14498819553264], 1e-13); %!test # a row missing every predictor is dropped %! X = nbX; %! X(20,:) = NaN; %! Mdl = fitcnb (X, nbL, 'CategoricalPredictors', 1); %! assert_equal (Mdl.NumObservations, 23); %! assert_equal (find (! Mdl.RowsUsed)', 20); %! assert_equal (Mdl.Prior, [11, 12] / 23, 1e-15); %!test # so is a row of weight zero %! Mdl = fitcnb (nbX, nbL, 'CategoricalPredictors', 1, ... %! 'Weights', [0; ones(23, 1)]); %! assert_equal (Mdl.NumObservations, 23); %!test # and of missing weight %! Mdl = fitcnb (nbX, nbL, 'CategoricalPredictors', 1, ... %! 'Weights', [NaN; ones(23, 1)]); %! assert_equal (Mdl.NumObservations, 23); %!test # and a row whose class is missing %! L = nbL; %! L{6} = ''; %! Mdl = fitcnb (nbX, L, 'CategoricalPredictors', 1); %! assert_equal (Mdl.NumObservations, 23); %!test # a kernel density is fitted on the rows it is known for %! Mdl = fitcnb (nbX(:,2), nbL, 'DistributionNames', 'kernel'); %! assert_equal (Mdl.NumObservations, 21); %! pd = Mdl.DistributionParameters{1,1}; %! assert_equal (pd.Bandwidth, 1.634894517763, 1e-12); %! assert_equal (pdf (pd, [2, 4, 6]), ... %! [0.0934400788914836, 0.137172679762077, ... %! 0.12969107168089], 1e-13); %!error ... %! X = nbX; %! X(strcmp (nbL, 'p'), 2) = NaN; %! fitcnb (X, nbL, 'CategoricalPredictors', 1); %!error ... %! fitcnb ([NaN, NaN; NaN, NaN], {'p'; 'q'}) ## MATLAB parity: with the multinomial a row missing some counts counts in the ## prior but is left out of the fit, as measured on R2026b %!shared mnC, mnL %! mnL = repmat ({'p'; 'q'; 'q'; 'p'; 'p'; 'q'; 'p'; 'q'}, 3, 1); %! mnC = [mod((1:24)', 4), mod((1:24)', 3), 1 + mod((1:24)', 5)]; %! mnC(5,2) = NaN; %!test # the row is used %! Mdl = fitcnb (mnC, mnL, 'DistributionNames', 'mn'); %! assert_equal (Mdl.NumObservations, 24); %! assert_equal (Mdl.RowsUsed, []); %!test # but the multinomial is fitted on the complete rows %! Mdl = fitcnb (mnC, mnL, 'DistributionNames', 'mn'); %! assert_equal (cell2mat (Mdl.DistributionParameters), ... %! [0.25, 0.183333333333333, 0.566666666666667; ... %! 0.289473684210526, 0.171052631578947, ... %! 0.539473684210526], 1e-14); %!test # and the prior counts it %! C = mnC; %! C(9,:) = NaN; %! Mdl = fitcnb (C, mnL, 'DistributionNames', 'mn'); %! assert_equal (Mdl.NumObservations, 23); %! [~, score] = predict (Mdl, [1, 2, 3]); %! assert_equal (score, [0.534585278127478, 0.465414721872522], 1e-14); %!error ... %! C = mnC; %! C(strcmp (mnL, 'p'), 1) = NaN; %! fitcnb (C, mnL, 'DistributionNames', 'mn'); ## X and Y hold the rows the model used, lined up with W and resubPredict, as ## in R2024a, so that cross-validation partitions those same rows %!shared alX, alL %! alX = [mod((1:24)', 7) + (1:24)' / 10, mod((1:24)', 5) - (1:24)' / 20]; %! alX(2,1) = NaN; %! alX(20,:) = NaN; %! alL = repmat ({'p'; 'q'; 'q'; 'p'; 'p'; 'q'; 'p'; 'q'}, 3, 1); %!test # the dropped row is in neither X nor Y %! Mdl = fitcnb (alX, alL); %! assert_equal ([rows(Mdl.X), rows(Mdl.Y), numel(Mdl.W)], [23, 23, 23]); %! assert_equal (numel (resubPredict (Mdl)), 23); %! assert_equal (find (! Mdl.RowsUsed)', 20); %!test # cross-validation partitions the rows used %! Mdl = fitcnb (alX, alL); %! CV = crossval (Mdl, 'KFold', 3); %! assert_equal (rows (CV.X), 23); %! assert_equal (numel (kfoldPredict (CV)), 23); %! assert_equal (isscalar (kfoldLoss (CV)), true); %!test # reassigning the prior keeps one weight per row used %! Mdl = fitcnb (alX, alL, 'Weights', 1 + mod ((1:24)', 3)); %! Mdl.Prior = [0.3, 0.7]; %! assert_equal (numel (Mdl.W), 23); %! assert_equal (sum (Mdl.W), 1, 1e-15); ## MATLAB parity: under the multinomial a query missing a count has posterior ## and cost NaN and the class of largest prior, the cost aside, as measured ## on R2024a and R2026b %!test # the class of largest prior %! L = repmat ({'p'; 'q'; 'q'; 'p'; 'p'; 'q'; 'p'; 'q'}, 3, 1); %! C = [mod((1:24)', 4), mod((1:24)', 3), 1 + mod((1:24)', 5)]; %! Mdl = fitcnb (C, L, 'DistributionNames', 'mn', 'Prior', [0.3, 0.7]); %! [label, score, cost] = predict (Mdl, [1, 2, 3; 2, NaN, 1; NaN, NaN, NaN]); %! assert_equal (label, {'q'; 'q'; 'q'}); %! assert_equal (score(:,1), [0.352213859428937; NaN; NaN], 1e-14); %! assert_equal (cost(:,1), [0.647786140571063; NaN; NaN], 1e-14); %!test # the cost does not move it %! L = repmat ({'p'; 'q'; 'q'; 'p'; 'p'; 'q'; 'p'; 'q'}, 3, 1); %! C = [mod((1:24)', 4), mod((1:24)', 3), 1 + mod((1:24)', 5)]; %! Mdl = fitcnb (C, L, 'DistributionNames', 'mn', 'Prior', [0.7, 0.3], ... %! 'Cost', [0, 1; 5, 0]); %! [label, ~, cost] = predict (Mdl, [1, 2, 3; 2, NaN, 1; NaN, NaN, NaN]); %! assert_equal (label, {'q'; 'p'; 'p'}); %! assert_equal (cost(:,1), [1.26254662156491; NaN; NaN], 1e-14); statistics-release-2.1.2/inst/Machine_Learning/ClassificationNeuralNetwork.m000066400000000000000000004267621527311104300273340ustar00rootroot00000000000000## Copyright (C) 2024 Pallav Purbia ## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ClassificationNeuralNetwork < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} ClassificationNeuralNetwork ## ## Neural network classification ## ## The @code{ClassificationNeuralNetwork} class implements a neural network ## classifier object, which can predict responses for new data using the ## @code{predict} method. ## ## Neural network classification is a machine learning method that uses ## interconnected nodes in multiple layers to learn complex patterns in data. ## It processes inputs through hidden layers with activation functions to ## produce classification outputs. ## ## Create a @code{ClassificationNeuralNetwork} object by using the ## @code{fitcnet} function or the class constructor. ## ## @seealso{fitcnet} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} X ## ## Predictor data ## ## A numeric matrix containing the unstandardized predictor data. Each ## column of @var{X} represents one predictor (variable), and each row ## represents one observation. This property is read-only. ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} Y ## ## Class labels ## ## Specified as a logical or numeric column vector, or as a character array ## or a cell array of character vectors with the same number of rows as the ## predictor data. Each row in @var{Y} is the observed class label for ## the corresponding row in @var{X}. This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} NumObservations ## ## Number of observations ## ## A positive integer value specifying the number of observations in the ## training dataset used for training the ClassificationNeuralNetwork model. ## This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} RowsUsed ## ## Rows used for fitting ## ## A logical column vector with the same length as the observations in the ## original predictor data @var{X}, true for each row that was used for ## fitting the ClassificationNeuralNetwork model. It is empty, @qcode{[]}, ## when every observation was used, so a non-empty value means that rows ## holding missing values were dropped. This property is read-only. ## ## @end deftp RowsUsed = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} NumPredictors ## ## Number of predictors ## ## A positive integer value specifying the number of predictors in the ## training dataset used for training the ClassificationNeuralNetwork model. ## This property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} PredictorNames ## ## Names of predictor variables ## ## A cell array of character vectors specifying the names of the predictor ## variables. The names are in the order in which they appear in the ## training dataset. This property is read-only. ## ## @end deftp PredictorNames = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} ResponseName ## ## Response variable name ## ## A character vector specifying the name of the response variable @var{Y}. ## This property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} ClassNames ## ## Names of classes in the response variable ## ## An array of unique values of the response variable @var{Y}, which has the ## same data types as the data in @var{Y}, sorted or in the order given by ## the @qcode{'ClassNames'} option. This property is read-only. ## @qcode{ClassNames} can have any of the following datatypes: ## ## @itemize ## @item Cell array of character vectors ## @item Character array ## @item Logical vector ## @item Numeric vector ## @end itemize ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} Sigma ## ## Predictor standard deviations ## ## A numeric vector containing the standard deviations of the predictors ## used for standardization. Empty when the predictor data were not ## standardized. ## This property is read-only. ## ## Only observations with no missing predictor enter the estimate, and ## they are weighted so that each class keeps the share of the ## observation weight it carried before any row was set aside. ## ## @end deftp Sigma = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} Mu ## ## Predictor means ## ## A numeric vector containing the means of the predictors used for ## standardization. Empty when the predictor data were not standardized. ## This property is read-only. ## ## Only observations with no missing predictor enter the estimate, and ## they are weighted so that each class keeps the share of the ## observation weight it carried before any row was set aside. ## ## @end deftp Mu = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} LayerSizes ## ## Sizes of fully connected layers ## ## A positive integer vector specifying the sizes of the fully connected ## layers in the neural network model. The i-th element of ## @qcode{LayerSizes} is the number of outputs in the i-th fully connected ## layer of the neural network model. @qcode{LayerSizes} does not include ## the size of the final fully connected layer. This layer always has K ## outputs, where K is the number of classes in Y. This property is ## read-only. ## ## @end deftp LayerSizes = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} Activations ## ## Activation functions for hidden layers ## ## A character vector or cell array of character vectors specifying the ## activation functions used in the hidden layers of the neural network. ## Supported activation functions include: @qcode{'linear'}, ## @qcode{'sigmoid'}, @qcode{'relu'}, @qcode{'tanh'}, @qcode{'softmax'}, ## @qcode{'lrelu'}, @qcode{'prelu'}, @qcode{'elu'}, and @qcode{'gelu'}. ## This property is read-only. ## ## @end deftp Activations = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} OutputLayerActivation ## ## Activation function for output layer ## ## A character vector specifying the activation function of the output layer ## of the neural network. Supported activation functions are the same as ## for the @qcode{Activations} property. The default, @qcode{softmax}, ## reports a probability over the classes; the network is then trained ## against cross entropy rather than the mean squared error. This ## property is read-only. ## ## @end deftp OutputLayerActivation = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} LearningRate ## ## Learning rate for gradient descent ## ## A positive scalar value defining the learning rate used by the gradient ## descent algorithm during training. This property is read-only. ## ## @end deftp LearningRate = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} IterationLimit ## ## Maximum number of training iterations ## ## A positive integer value defining the maximum number of epochs for ## training the model. This property is read-only. ## ## @end deftp IterationLimit = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} ModelParameters ## ## Neural network model parameters ## ## A structure holding the fit as it was asked for: @qcode{LayerSizes}, ## @qcode{Activations}, @qcode{OutputLayerActivation}, ## @qcode{LayerWeightsInitializers}, @qcode{Solver}, ## @qcode{LearningRate}, @qcode{IterationLimit}, ## @qcode{GradientTolerance}, @qcode{LossTolerance}, ## @qcode{StepTolerance}, @qcode{DisplayInfo}, @qcode{StandardizeData}, ## and the @qcode{Version}, @qcode{Method} and @qcode{Type} tags. ## ## What came out of the fit is elsewhere: the @qcode{LayerWeights} and ## @qcode{LayerBiases} properties hold the network, @qcode{TrainingHistory} ## the series and @qcode{ConvergenceInfo} where it stopped. ## ## @qcode{LayerWeightsInitializers} names the scheme each layer's weights ## were drawn with, the output layer last: @qcode{'he'} for a rectifying ## activation and @qcode{'glorot'} for a symmetric one. It is a report, ## not a setting, the engine choosing per layer from the activation and ## offering no way to override it. ## ## @qcode{OutputLayerActivation}, @qcode{Solver} and @qcode{LearningRate} ## are this package's own; MATLAB has no counterpart for them. The fields ## it reports that this class does not accept as arguments ## (@qcode{Lambda}, the validation set and its patience and frequency, ## @qcode{InitialStepSize} and the two initializer settings) are absent. ## This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} ConvergenceInfo ## ## Training convergence information ## ## A structure containing convergence information of the neural network ## classifier model with the following fields: ## ## @itemize ## @item @qcode{Accuracy} - The prediction accuracy at each iteration ## during training ## @item @qcode{TrainingLoss} - The loss value recorded at each iteration ## during training ## @item @qcode{Time} - The cumulative time taken for all iterations in ## seconds ## @end itemize ## ## This property is read-only. ## ## ## Under @qcode{'lbfgs'} the structure carries @code{Gradient} and ## @code{Step}, the two quantities the solver measured to decide it had ## converged, and @code{ConvergenceCriterion}, naming the test that ## stopped it. It carries no @code{Accuracy}: MATLAB reports none, and ## measuring it would cost a pass over the whole training set at every ## iteration. ## @end deftp ConvergenceInfo = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} DisplayInfo ## ## Display training information flag ## ## A boolean flag indicating whether to print information during training. ## This property is read-only. ## ## @end deftp DisplayInfo = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} Solver ## ## Solver used for training ## ## A character vector specifying the solver algorithm used to train the ## neural network model, either @qcode{'Gradient Descent'} for the ## stochastic solver or @qcode{'LBFGS'} for the full-batch one. This ## property is read-only. ## ## @end deftp Solver = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} LayerWeights ## ## Learned weights of each fully connected layer ## ## A cell array holding one weight matrix per layer, the output layer ## included. @code{LayerWeights@{i@}} has one row per neuron of layer ## @math{i} and one column per input it receives. This property is ## read-only. ## ## @end deftp LayerWeights = {}; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} LayerBiases ## ## Learned bias of each fully connected layer ## ## A cell array holding one column vector per layer, the output layer ## included, with one entry per neuron of that layer. This property is ## read-only. ## ## @end deftp LayerBiases = {}; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} TrainingHistory ## ## Iteration by iteration record of training ## ## A table with one row per iteration, holding the iteration number, the ## training loss and the training accuracy recorded at it. This property ## is read-only. ## ## ## The columns follow the solver. Under @qcode{'sgd'} they are ## @code{Iteration} and @code{TrainingLoss}, with @code{TrainingAccuracy} ## for a classifier. Under @qcode{'lbfgs'} they are @code{Iteration}, ## @code{TrainingLoss}, @code{Gradient} and @code{Step}, as MATLAB's are. ## @end deftp TrainingHistory = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} Prior ## ## Prior probability of each class ## ## A numeric vector with one entry per class, in the order of ## @code{ClassNames}, summing to one. It defaults to the relative ## frequency of each class in the training data. This property is ## read-only, as MATLAB documents it; pass @qcode{'Prior'} to ## @code{fitcnet} to set it. ## ## Specified as a row vector with one entry per class, in the order of ## @qcode{ClassNames}, and rescaled to sum to one. It may be given as ## @qcode{'empirical'}, @qcode{'uniform'}, a numeric vector, or a ## structure with @qcode{ClassNames} and @qcode{ClassProbs} fields, which ## assigns each probability by class name rather than by position. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} W ## ## Observation weights ## ## A numeric column vector with one entry per training observation, ## summing to one, by which the training loss weighs each observation. It ## has the class of the @qcode{'Weights'} given, single or double. This ## property is read-only. ## ## Each class carries its prior spread over its own observations in ## proportion to the @qcode{'Weights'} given, or evenly when none were, so ## that an observation of a class weighs @qcode{Prior} for that class ## divided by the number of observations it holds. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A numeric vector of column indices into @code{X} naming the predictors ## treated as categorical, and empty when none is. This property is ## read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} ExpandedPredictorNames ## ## Names of the predictors as the model expanded them ## ## A cell array of character vectors. It matches @code{PredictorNames} ## unless a categorical predictor was expanded into indicator variables. ## This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} BinEdges ## ## Bin edges of the predictors ## ## A cell array with one entry per predictor, holding that predictor's bin ## edges where the learner discretized it before fitting. It is empty here ## and stays empty: this learner fits the predictors as they are, and ## MATLAB's reports an empty cell for it as well. ## ## This property is read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} HyperparameterOptimizationResults ## ## Results of the hyperparameter optimization ## ## @strong{Always empty.} It is declared for MATLAB compatibility, where ## it holds what an automatic search over the hyperparameters found. This ## class fits the parameters it is given and runs no such search, so there ## is nothing to report. This property is read-only. ## ## @end deftp HyperparameterOptimizationResults = []; endproperties ## Properties a user may set after the model is built. Each one is ## validated by its set method below. properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} Cost ## ## Cost of misclassification ## ## A numeric matrix with one row and one column per class, where ## @code{Cost(i,j)} is the cost of classifying an observation of class ## @math{i} as class @math{j}. The default has zeros on the diagonal and ## ones elsewhere. Change it on a trained model with dot notation, as ## in @qcode{@var{obj}.Cost = @var{cost}}. ## ## ## A cost may also be given as a struct with the fields ## @qcode{ClassNames} and @qcode{ClassificationCosts}, which names the ## order its own matrix is written in. That matrix is permuted into the ## order of @qcode{ClassNames} above, so a caller need not know which ## order the classes are in. It must name every class. ## ## A cost must be floating point, not sparse, not complex, non-negative ## and zero down its diagonal, and must hold no @qcode{NaN} or ## @qcode{Inf}. A @code{single} is widened to @code{double}. ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {ClassificationNeuralNetwork} {property} ScoreTransform ## ## Transformation function for classification scores ## ## Specified as a function handle for transforming the classification ## scores. Add or change the @qcode{ScoreTransform} property using dot ## notation as in: ## ## @itemize ## @item @qcode{@var{obj}.ScoreTransform = 'function_name'} ## @item @qcode{@var{obj}.ScoreTransform = @@function_handle} ## @end itemize ## ## When specified as a character vector, it can be any of the following ## built-in functions. Nevertheless, the @qcode{ScoreTransform} property ## always stores their function handle equivalent. ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Value} @tab @var{Description} ## @item @qcode{'doublelogit'} @tab @math{1 ./ (1 + exp (-2 * x))} ## @item @qcode{'invlogit'} @tab @math{log (x ./ (1 - x))} ## @item @qcode{'ismax'} @tab Sets the score for the class with the ## largest score to 1, and for all other classes to 0 ## @item @qcode{'logit'} @tab @math{1 ./ (1 + exp (-x))} ## @item @qcode{'none'} @tab @math{x} (no transformation) ## @item @qcode{'identity'} @tab @math{x} (no transformation) ## @item @qcode{'sign'} @tab ## @math{-1 for x < 0, 0 for x = 0, 1 for x > ## 0} ## @item @qcode{'symmetric'} @tab @math{2 * x - 1} ## @item @qcode{'symmetricismax'} @tab Sets the score for the class ## with the largest score to 1, and for all other classes to -1 ## @item @qcode{'symmetriclogit'} @tab @math{2 ./ (1 + exp (-x)) - 1} ## @end multitable ## ## @end deftp ScoreTransform = 'none'; endproperties ## Readable by the counterpart class, which copies it, and kept out of ## the documented surface. properties (GetAccess = public, SetAccess = protected, Hidden) ## The dummy coding of the categorical predictors, empty when none. Coding_ = []; STfun = @(x) x; endproperties ## Set methods for the properties a user may assign. methods (Hidden) function this = set.ScoreTransform (this, val) name = 'ClassificationNeuralNetwork'; [this.STfun, this.ScoreTransform] = parseScoreTransform (val, ... name); endfunction function this = set.Cost (this, val) gnY = this.ClassNames; if (isempty (val)) this.Cost = cast (! eye (classCount (gnY)), 'double'); else ## Everything a cost must be, and the struct form, which ## is permuted into this model's class order. [val, errmsg] = costMatrix (val, gnY); if (! isempty (errmsg)) error ("ClassificationNeuralNetwork: %s", errmsg); endif this.Cost = val; endif endfunction ## Custom display function disp (this) fprintf ("\n ClassificationNeuralNetwork\n\n"); ## Print selected properties fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); if (iscellstr (this.ClassNames)) str = repmat ({'''%s'''}, 1, numel (this.ClassNames)); str = strcat ('{', strjoin (str, ' '), '}'); str = sprintf (str, this.ClassNames{:}); elseif (ischar (this.ClassNames)) str = repmat ({'''%s'''}, 1, rows (this.ClassNames)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, cellstr (this.ClassNames){:}); else # single, double, logical str = repmat ({'%d'}, 1, numel (this.ClassNames)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, this.ClassNames); endif fprintf ("%+25s: %s\n", 'ClassNames', str); fprintf ("%+25s: '%s'\n", 'ScoreTransform', this.ScoreTransform); fprintf ("%+25s: %d\n", 'NumObservations', this.NumObservations); fprintf ("%+25s: %d\n", 'NumPredictors', this.NumPredictors); str = repmat ({'%d'}, 1, numel (this.LayerSizes)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, this.LayerSizes); fprintf ("%+25s: %s\n", 'LayerSizes', str); if (iscellstr (this.Activations)) str = repmat ({'''%s'''}, 1, numel (this.Activations)); str = strcat ('{', strjoin (str, ' '), '}'); str = sprintf (str, this.Activations{:}); fprintf ("%+25s: %s\n", 'Activations', str); else # character vector fprintf ("%+25s: '%s'\n", 'Activations', this.Activations); endif fprintf ("%+25s: '%s'\n", 'OutputLayerActivation', ... this.OutputLayerActivation); fprintf ("%+25s: '%s'\n", 'Solver', this.Solver); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {ClassificationNeuralNetwork} {@var{obj} =} ClassificationNeuralNetwork (@var{X}, @var{Y}) ## @deftypefnx {ClassificationNeuralNetwork} {@var{obj} =} ClassificationNeuralNetwork (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationNeuralNetwork} {@var{obj} =} ClassificationNeuralNetwork (@var{Tbl}, @var{formula}) ## @deftypefnx {ClassificationNeuralNetwork} {@var{obj} =} ClassificationNeuralNetwork (@var{Tbl}, @var{Y}) ## @deftypefnx {ClassificationNeuralNetwork} {@var{obj} =} ClassificationNeuralNetwork (@dots{}, @var{name}, @var{value}) ## ## Create a @qcode{ClassificationNeuralNetwork} class object containing a ## neural network classification model. ## ## @code{@var{obj} = ClassificationNeuralNetwork (@var{X}, @var{Y})} returns ## a ClassificationNeuralNetwork object, with @var{X} as the predictor data ## and @var{Y} containing the class labels of observations in @var{X}. ## ## @itemize ## @item ## @code{X} must be a @math{N*P} numeric matrix of input data where rows ## correspond to observations and columns correspond to features or ## variables. @var{X} will be used to train the neural network model. ## @item ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels ## of corresponding predictor data in @var{X}. @var{Y} can contain any type ## of categorical data. @var{Y} must have the same number of rows as ## @var{X}. ## @end itemize ## ## @code{@var{obj} = ClassificationNeuralNetwork (@dots{}, @var{name}, ## @var{value})} returns a ClassificationNeuralNetwork object with ## parameters specified by the following @qcode{@var{name}, @var{value}} ## paired input arguments: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'CategoricalPredictors'} @tab The predictors whose values ## are levels, as indices, as a logical vector with one element per ## predictor, or as @qcode{'all'}. Each is dummy coded in its place, one ## column of zeros and ones per level seen in training, named as in ## @qcode{'x1 == 2'} in @code{ExpandedPredictorNames}, and the coded columns ## are not standardized. An observation holding a level the training data ## did not has no score and takes the class of largest prior, as a row ## missing a predictor does. ## A predictor may be named rather than indexed, as a character matrix of ## one padded name per row, a string array or a cellstr; a name must match ## an entry of @qcode{'PredictorNames'} exactly, its case included. ## ## @item @qcode{'PredictorNames'} @tab A cell array of character ## vectors specifying the names of the predictors. The length of this array ## must match the number of columns in @var{X}. ## ## @item @qcode{'ResponseName'} @tab A character vector specifying the ## name of the response variable. ## ## @item @qcode{'ClassNames'} @tab Names of the classes in the class ## labels, @var{Y}, used for fitting the neural network model. ## @qcode{ClassNames} are of the same type as the class labels in @var{Y}. ## ## @item @qcode{'ScoreTransform'} @tab A user-defined function handle ## or a character vector specifying one of the following builtin functions ## specifying the transformation applied to predicted classification scores. ## Supported values include @qcode{'doublelogit'}, @qcode{'invlogit'}, ## @qcode{'ismax'}, @qcode{'logit'}, @qcode{'none'}, @qcode{'identity'}, ## @qcode{'sign'}, @qcode{'symmetric'}, @qcode{'symmetricismax'}, and ## @qcode{'symmetriclogit'}. ## ## @item @qcode{'Standardize'} @tab A logical scalar specifying whether ## to standardize the predictor data. When @qcode{true}, the predictors are ## centered and scaled to have zero mean and unit variance. ## ## @item @qcode{'LayerSizes'} @tab A positive integer vector specifying ## the sizes of the fully connected layers in the neural network. The ## default is 10. ## ## @item @qcode{'Activations'} @tab A character vector or cell array of ## character vectors specifying the activation functions for the hidden ## layers. Supported values include @qcode{'linear'}, @qcode{'sigmoid'}, ## @qcode{'relu'}, @qcode{'tanh'}, @qcode{'softmax'}, @qcode{'lrelu'}, ## @qcode{'prelu'}, @qcode{'elu'}, and @qcode{'gelu'}. The default is ## @qcode{'relu'}, whose gradient is one wherever a unit is active and so ## does not shrink as it passes back through the layers, where a sigmoid ## multiplies it by at most a quarter at every one. ## ## @item @qcode{'OutputLayerActivation'} @tab A character vector ## specifying the activation function for the output layer. Supported ## values are the same as for @qcode{'Activations'}. The default is ## @qcode{'softmax'}, which makes the scores a probability over the ## classes and trains the network against cross entropy; any other value ## trains it against the mean squared error. ## ## @item @qcode{'LearningRate'} @tab A positive scalar specifying the ## learning rate for gradient descent. The default is 0.003. A larger ## rate can drive every unit of a hidden layer negative, after which a ## rectifier passes no gradient and the network stops training. ## Applies only when @qcode{'Solver'} is @qcode{'sgd'}. ## ## @item @qcode{'Solver'} @tab A character vector naming the solver that ## trains the network, either @qcode{'lbfgs'} or @qcode{'sgd'}. The ## default is @qcode{'lbfgs'}, which minimizes the loss over the whole ## training set at once by limited-memory BFGS, as MATLAB does. It takes ## no learning rate, stops on the three tolerances below, and reaches a ## lower training loss in fewer passes over the data, though each of its ## iterations costs several passes where an epoch costs one. ## @qcode{'sgd'} visits the samples one at a time and steps down the ## gradient of each, running for @qcode{'IterationLimit'} epochs; it was ## the default before version 1.9.0. ## ## @item @qcode{'GradientTolerance'} @tab A nonnegative scalar. Training ## stops once the gradient's infinity norm falls to or below it, which is ## the quantity MATLAB tests too. The default is @qcode{1e-6}. Applies ## only when @qcode{'Solver'} is @qcode{'lbfgs'}. ## ## @item @qcode{'StepTolerance'} @tab A nonnegative scalar. Training ## stops once the step's infinity norm falls to or below it, which is the ## quantity MATLAB tests too. The default is @qcode{1e-6}. Applies only ## when @qcode{'Solver'} is @qcode{'lbfgs'}. ## ## @item @qcode{'LossTolerance'} @tab A real scalar. Training stops once ## the training loss falls to or below it. The test is on the loss ## itself and not on its change, matching MATLAB; pass @code{-Inf} to ## switch it off. The default is @qcode{1e-6}. Applies only when ## @qcode{'Solver'} is @qcode{'lbfgs'}. ## ## @item @qcode{'IterationLimit'} @tab A positive integer specifying ## the maximum number of training iterations. The default is 1000. ## Under @qcode{'sgd'} this counts epochs, under ## @qcode{'lbfgs'} solver iterations. ## ## @item @qcode{'DisplayInfo'} @tab A logical scalar specifying whether ## to display training information. The default is @qcode{false}. ## @end multitable ## ## @seealso{fitcnet} ## @end deftypefn function this = ClassificationNeuralNetwork (X, Y, varargin) ## Check for sufficient number of input arguments if (nargin < 2) error ("ClassificationNeuralNetwork: too few input arguments."); endif ## A table names its own predictors and says which hold levels [this, X, Y, varargin] = resolveTable (this, 'ClassificationNeuralNetwork', ... X, Y, varargin); ## Check X and Y have the same number of observations if (rows (X) != rows (Y)) error (strcat ("ClassificationNeuralNetwork: number of", ... " rows in X and Y must be equal.")); endif ## Assign original X and Y data to the ClassificationNeuralNetwork object this.X = X; this.Y = Y; ## Get groups in Y [gY, gnY, glY] = grp2idx (Y); ## Supported activation functions acList = {'linear', 'none', 'sigmoid', 'relu', 'tanh', 'softmax', ... 'lrelu', 'prelu', 'elu', 'gelu'}; ## Parse optional paired arguments optNames = {'Standardize', 'PredictorNames', 'ResponseName', ... 'ClassNames', 'ScoreTransform', 'Prior', 'Cost', ... 'LayerSizes', 'LearningRate', 'Activations', ... 'OutputLayerActivation', 'IterationLimit', 'Solver', ... 'GradientTolerance', 'LossTolerance', 'StepTolerance', ... 'DisplayInfo', 'CategoricalPredictors', 'Weights'}; ## An empty default stands for one resolved once the data are known: ## 'PredictorNames' are x1, x2, ... and 'ResponseName' is 'Y'; the ## classes, 'Prior' and 'Cost' come from the response; no ## 'ScoreTransform' leaves the scores as they are; 'LearningRate' is ## 0.003 and each tolerance 1e-6, empty so that giving one can be ## refused by name when the solver cannot use it. dfValues = {false, [], [], [], [], [], [], 10, [], 'relu', 'softmax', ... 1000, 'lbfgs', [], [], [], false, [], []}; [Standardize, PredictorNames, ResponseName, ClassNames, STin, Prior, ... Cost, LayerSizes, LearningRate, Activations, OutputLayerActivation, ... IterationLimit, Solver, GradientTolerance, LossTolerance, ... StepTolerance, DisplayInfo, CatPreds, Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (Standardize == true || Standardize == false)) error (strcat ("ClassificationNeuralNetwork: 'Standardize' must be", ... " either true or false.")); endif if (! isempty (PredictorNames) && ! iscellstr (PredictorNames)) error (strcat ("ClassificationNeuralNetwork: 'PredictorNames' must", ... " be supplied as a cellstring array.")); elseif (! isempty (PredictorNames) && columns (PredictorNames) != columns (X)) error (strcat ("ClassificationNeuralNetwork: 'PredictorNames' must", ... " have the same number of columns as X.")); endif if (! isempty (ResponseName) && ! ischar (ResponseName)) error (strcat ("ClassificationNeuralNetwork: 'ResponseName' must", ... " be a character vector.")); endif if (! isempty (ClassNames) && ! (iscellstr (ClassNames) || isnumeric (ClassNames) || islogical (ClassNames) || ischar (ClassNames) || isa (ClassNames, 'categorical') || isa (ClassNames, 'string'))) error (strcat ("ClassificationNeuralNetwork: 'ClassNames' must be", ... " a categorical array, a character array, a string", ... " array, a logical vector, a numeric vector, or a", ... " cell array of character vectors.")); endif ## A named class with prior 0 is removed below, so it may be absent exempt = []; if (! isempty (ClassNames)) exempt = zeroPriorNames (ClassNames, Prior, []); [~, errmsg] = namedClasses (glY, ClassNames, exempt); if (! isempty (errmsg)) error ("ClassificationNeuralNetwork: %s", errmsg); endif endif if (! (isnumeric (LayerSizes) && isvector (LayerSizes) && all (LayerSizes > 0) && all (mod (LayerSizes, 1) == 0))) error (strcat ("ClassificationNeuralNetwork: 'LayerSizes' must be", ... " a positive integer vector.")); endif if (! isempty (LearningRate) && ! (isnumeric (LearningRate) && isscalar (LearningRate) && LearningRate > 0)) error (strcat ("ClassificationNeuralNetwork: 'LearningRate' must", ... " be a positive scalar.")); endif if (! (ischar (Activations) || iscellstr (Activations))) error (strcat ("ClassificationNeuralNetwork: 'Activations' must be", ... " a character vector or a cellstring vector.")); endif if (ischar (Activations)) if (! any (strcmpi (Activations, acList))) error (strcat ("ClassificationNeuralNetwork: unsupported", ... " 'Activation' function.")); endif else if (! all (cell2mat (cellfun (@(x) any (strcmpi (x, acList)), Activations, 'UniformOutput', false)))) error (strcat ("ClassificationNeuralNetwork: unsupported", ... " 'Activation' functions.")); endif endif Activations = tolower (Activations); if (! (ischar (OutputLayerActivation))) error (strcat ("ClassificationNeuralNetwork:", ... " 'OutputLayerActivation' must be a character", ... " vector.")); endif if (! any (strcmpi (OutputLayerActivation, acList))) error (strcat ("ClassificationNeuralNetwork: unsupported", ... " 'OutputLayerActivation' function.")); endif OutputLayerActivation = tolower (OutputLayerActivation); if (! (isnumeric (IterationLimit) && isscalar (IterationLimit) && (IterationLimit > 0) && mod (IterationLimit, 1) == 0)) error (strcat ("ClassificationNeuralNetwork: 'IterationLimit' must", ... " be a positive integer.")); endif if (! (ischar (Solver) && any (strcmpi (Solver, {'sgd', ... 'lbfgs'})))) error (strcat ("ClassificationNeuralNetwork: 'Solver' must be", ... " either 'sgd' or 'lbfgs'.")); endif Solver = tolower (Solver); if (! isempty (GradientTolerance) && ! (isnumeric (GradientTolerance) && isscalar (GradientTolerance) && GradientTolerance >= 0)) error (strcat ("ClassificationNeuralNetwork: 'GradientTolerance'", ... " must be a nonnegative scalar.")); endif if (! isempty (LossTolerance) && ! (isnumeric (LossTolerance) && isscalar (LossTolerance) && ! isnan (LossTolerance))) error (strcat ("ClassificationNeuralNetwork: 'LossTolerance' must", ... " be a real scalar.")); endif if (! isempty (StepTolerance) && ! (isnumeric (StepTolerance) && isscalar (StepTolerance) && StepTolerance >= 0)) error (strcat ("ClassificationNeuralNetwork: 'StepTolerance' must", ... " be a nonnegative scalar.")); endif if (! (DisplayInfo == true || DisplayInfo == false)) error (strcat ("ClassificationNeuralNetwork: 'DisplayInfo' must be", ... " either true or false.")); endif if (! isempty (STin)) [this.STfun, this.ScoreTransform] = ... parseScoreTransform (STin, 'ClassificationNeuralNetwork'); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("ClassificationNeuralNetwork: %s", errmsg); endif if (! isempty (Weights) && ! (isvector (Weights) && numel (Weights) == rows (X))) error (strcat ("ClassificationNeuralNetwork: 'Weights' must be a", ... " vector with one element per row of X.")); endif if (! isempty (Weights) && (any (Weights < 0) || ! (sum (Weights(! isnan (Weights))) > 0))) error (strcat ("ClassificationNeuralNetwork: 'Weights' must be", ... " nonnegative and must not be all zero.")); endif if (! isempty (args)) error (strcat ("ClassificationNeuralNetwork: invalid optional", ... " paired argument.")); endif ## The solver-specific options the caller named, so that one meant for ## the other solver can be refused by name, then their defaults tolNames = {'GradientTolerance', 'LossTolerance', 'StepTolerance'}; GivenTols = tolNames(! cellfun (@isempty, {GradientTolerance, ... LossTolerance, ... StepTolerance})); LearningRateGiven = ! isempty (LearningRate); if (isempty (LearningRate)) LearningRate = 0.003; endif if (isempty (GradientTolerance)) GradientTolerance = 1e-6; endif if (isempty (LossTolerance)) LossTolerance = 1e-6; endif if (isempty (StepTolerance)) StepTolerance = 1e-6; endif ## Generate default predictors and response variable names (if necessary) NumPredictors = columns (X); if (isempty (PredictorNames)) for i = 1:NumPredictors PredictorNames {i} = strcat ("x", num2str (i)); endfor endif if (isempty (ResponseName)) ResponseName = 'Y'; endif ## Assign predictors and response variable names this.NumPredictors = NumPredictors; this.PredictorNames = PredictorNames; this.ExpandedPredictorNames = PredictorNames; this.ResponseName = ResponseName; ## Handle class names if (! isempty (ClassNames)) ## The observations of a class ClassNames leaves out are dropped gY(! ismember (gY, namedClasses (glY, ClassNames))) = NaN; endif ## An observation is dropped when its response is missing, or when its ## weight is zero or missing, as R2024a drops it; the weights keep their ## class in the model and every computation runs on them as double. A row ## whose predictors hold missing values is kept and reported as used, ## while the fit below draws on the complete observations alone. Wclass = "double"; Wall = ones (rows (X), 1); if (! isempty (Weights)) Wclass = class (Weights); Wall = double (Weights(:)); endif RowsUsed = ! isnan (gY) & ! isnan (Wall) & Wall > 0; wret = Wall(RowsUsed); ## Index the rows and not the elements: a response naming its ## classes in the rows of a character matrix has one column per ## character, and a linear index flattens the names into single ## letters. Every other accepted type is a column, for which ## the two forms agree. Yret = Y(RowsUsed, :); Xret = X(RowsUsed, :); this.X = Xret; this.Y = Yret; cobs = ! any (isnan (Xret), 2); Y = Yret(cobs, :); X = Xret(cobs, :); wfit = wret(cobs); ## Renew groups in Y over the retained observations, so a class held ## only by a row with missing predictors is still a class of the model; ## the classes are sorted or in the order ClassNames gives them [this.ClassNames, gret] = classOrder (Yret, ClassNames, exempt); gY = gret(cobs); ## Dummy code the categorical predictors on the rows the fit draws on. ## X keeps the predictors as given; the fit and every prediction see ## the coded columns. cnames = PredictorNames; if (isempty (cnames)) cnames = arrayfun (@(k) sprintf ('x%d', k), 1:columns (X), ... 'UniformOutput', false); endif [Coding, errmsg] = dummyCoding (X, CatPreds, cnames); if (! isempty (errmsg)) error ("ClassificationNeuralNetwork: %s", errmsg); endif X = dummyCoding (X, Coding); if (! isempty (Coding.Index)) this.CategoricalPredictors = Coding.Index; this.ExpandedPredictorNames = Coding.ExpandedNames; this.Coding_ = Coding; endif ## Check X contains valid data if (! (isnumeric (X) && isfinite (X))) error ("ClassificationNeuralNetwork: invalid values in X."); endif ## Assign the number of observations and their corresponding indices ## on the original data, which will be used for training the model, ## to the ClassificationNeuralNetwork object this.NumObservations = rows (this.X); ## RowsUsed is left empty when every observation was used, as in MATLAB if (all (RowsUsed)) this.RowsUsed = []; else this.RowsUsed = RowsUsed; endif ## Cost, Prior and the observation weights. Cost defaults to zero on ## the diagonal and one elsewhere, Prior to the class frequencies of the ## training data, and each class spreads its prior over its own ## observations. Prior cannot be changed once the model is built, so ## the option here is the only way to weigh the classes differently. nclasses = classCount (this.ClassNames); if (isempty (Cost)) this.Cost = ones (nclasses) - eye (nclasses); else ## A struct carrying its own class order is a cost too, and is ## resolved by the property's own set method. if (! (isstruct (Cost) || (isnumeric (Cost) && issquare (Cost) && rows (Cost) == nclasses))) error (strcat ("ClassificationNeuralNetwork: 'Cost' must be a", ... " numeric square matrix with one row and column", ... " per class.")); endif this.Cost = Cost; endif if (isstruct (Prior)) Prior = priorFromStruct (Prior, this.ClassNames, ... 'ClassificationNeuralNetwork'); endif empirical = isempty (Prior) || (ischar (Prior) && strcmpi (Prior, 'empirical')); if (empirical) this.Prior = accumarray (gY(:), wfit, [nclasses, 1])' / sum (wfit); elseif (ischar (Prior) && strcmpi (Prior, 'uniform')) this.Prior = ones (1, nclasses) / nclasses; elseif (isnumeric (Prior) && isreal (Prior) && isvector (Prior) && numel (Prior) == nclasses && all (Prior >= 0) && sum (Prior) > 0) this.Prior = Prior(:)' / sum (Prior); else error (strcat ("ClassificationNeuralNetwork: 'Prior' must be", ... " 'empirical', 'uniform', or a non-negative numeric", ... " vector with one entry per class.")); endif ## A class with prior 0 is never predicted, so its observations are ## removed; one that costs nothing to misclassify is kept, as MATLAB's ## learner keeps it [drop, P, msg] = removedClasses (this.Prior, [], ... this.ClassNames, gret); if (all (drop)) error (strcat ("ClassificationNeuralNetwork: every class has prior", ... " probability 0.")); elseif (any (drop)) if (! isempty (msg)) warning ("ClassificationNeuralNetwork: %s", msg); endif keep = ! drop(gret); used = find (RowsUsed); RowsUsed(used(! keep)) = false; this.X = this.X(keep,:); this.Y = this.Y(keep,:); wret = wret(keep); kc = keep(cobs); X = X(kc,:); Y = Y(kc,:); gY = gY(kc); wfit = wfit(kc); cobs = cobs(keep); gret = gret(keep); this.NumObservations = rows (this.X); this.RowsUsed = RowsUsed; this.Prior = P; endif ## W has one entry per stored observation, those missing a predictor ## included, so it lines up with X; the fit sees the complete ones. if (isempty (Weights)) this.W = priorWeights (this.Prior, gret, this.NumObservations); else this.W = cast (priorNormalize (wret, gret, this.Prior), Wclass); endif ## Handle the Standardize option if (Standardize) ## Mu and Sigma weight the complete observations so that each class ## keeps the share of the observation weight it carried before any row ## was set aside, or its prior where one was given, which is what ## MATLAB reports. sw = zeros (rows (X), 1); for k = 1:classCount (this.ClassNames) ck = (gY == k); if (any (ck)) if (empirical) share = sum (wret(gret == k)) / sum (wret); else share = this.Prior(k); endif sw(ck) = share * wfit(ck) / sum (wfit(ck)); endif endfor sw = sw / sum (sw); this.Mu = sum (sw .* X, 1); Zs = X - this.Mu; this.Sigma = sqrt (sum (sw .* Zs .^ 2, 1) / (1 - sum (sw .^ 2))); ## A constant predictor is left unscaled; its weighted mean can miss ## the constant by one rounding, so its deviation need not be zero. this.Sigma(this.Sigma == 0 | all (X == X(1,:), 1)) = 1; ## A level's column is left as it is, as in MATLAB R2024a. this.Mu(Coding.Dummy) = 0; this.Sigma(Coding.Dummy) = 1; ## Train on the scale the model predicts on: predict, resubPredict ## and loss all standardize their input from Mu and Sigma, so the ## training data must be standardized here as well. X = (X - this.Mu) ./ this.Sigma; else this.Sigma = []; this.Mu = []; endif ## An option that cannot act is refused rather than ignored. The three ## tolerances are how the lbfgs solver decides it has converged and mean ## nothing to the epoch loop, which runs to 'IterationLimit' whatever ## they say; a learning rate is what the epoch loop scales its step by ## and means nothing to a line search, which finds its own. if (strcmp (Solver, 'sgd') && ! isempty (GivenTols)) error (strcat ("ClassificationNeuralNetwork: '", GivenTols{1}, ... "' applies only when 'Solver' is 'lbfgs'.")); endif if (strcmp (Solver, 'lbfgs') && LearningRateGiven) error (strcat ("ClassificationNeuralNetwork: 'LearningRate'", ... " applies only when 'Solver' is 'sgd'.")); endif if (strcmp (Solver, 'lbfgs')) this.Solver = 'LBFGS'; else this.Solver = 'Gradient Descent'; endif ## Store training parameters this.LayerSizes = LayerSizes; this.Activations = Activations; this.OutputLayerActivation = OutputLayerActivation; this.LearningRate = LearningRate; this.IterationLimit = IterationLimit; this.DisplayInfo = DisplayInfo; ## Start the training process NumThreads = nproc (); cnn_timer_ = tic; ## A softmax output reports a probability over the classes, and the ## loss that belongs with it is cross entropy: paired that way the two ## gradients compose to y - t. Any other output layer keeps the mean ## squared error. LossFunction = double (strcmp (OutputLayerActivation, 'softmax')); SolverOptions = struct ('Solver', Solver, ... 'GradientTolerance', GradientTolerance, ... 'LossTolerance', LossTolerance, ... 'StepTolerance', StepTolerance); ## The loss weighs each observation by W, which carries the prior as ## well as the weights given, as R2024a weighs it. Weights that are all ## equal are left out, so that such a fit keeps the plain mean. Wfit = double (this.W(cobs)); if (max (Wfit) - min (Wfit) > 1e-12 * max (Wfit)) SolverOptions.Weights = Wfit; endif ## The engine names the layers itself; this check stays here so the ## count is reported under the class rather than under fcnntrain. if (! ischar (Activations) && numel (LayerSizes) != numel (Activations)) error (strcat ("ClassificationNeuralNetwork: 'Activations'", ... " vector does not match the number of layers.")); endif ## The engine makes one output per class it is shown, so a class ## removed for its prior of 0 is left out of the numbering gYfit = gY; fitted = this.Prior > 0; if (! all (fitted)) renum = cumsum (fitted); gYfit = renum(gY); gYfit = gYfit(:); endif Mdl = fcnntrain (X, gYfit, LayerSizes, Activations, ... OutputLayerActivation, NumThreads, ... LearningRate, IterationLimit, DisplayInfo, ... LossFunction, SolverOptions); ## The solver records a value per iteration. The series belongs to the ## history and ConvergenceInfo reports where the fit ended up, as ## MATLAB divides them; SERIES carries the columns to both. series = struct ('TrainingLoss', Mdl.Loss(:)); Mdl = rmfield (Mdl, 'Loss'); ## What the fit recorded depends on which solver ran. The epoch loop ## scores the network once an epoch and reports the accuracy; lbfgs ## reports the gradient and step it measured to decide it had stopped, ## and which test stopped it, as MATLAB does. if (strcmp (Solver, 'lbfgs')) series.Gradient = Mdl.Gradient(:); series.Step = Mdl.Step(:); criterion = Mdl.Criterion; Mdl = rmfield (Mdl, {'Gradient', 'Step', 'Criterion'}); else series.Accuracy = Mdl.Accuracy(:); criterion = ''; Mdl = rmfield (Mdl, 'Accuracy'); endif ConvergenceInfo = convergenceStruct (series, toc (cnn_timer_), ... criterion); ## The fit as it was asked for. What came out of it is the ## LayerWeights and LayerBiases properties, TrainingHistory and ## ConvergenceInfo; this structure holds what went in. The weight ## initializer of each layer is decided by that layer's activation ## inside the engine and cannot be chosen, so it is recorded rather ## than taken from an argument. initz = fcnnInitializers (this.Activations, numel (this.LayerSizes), ... this.OutputLayerActivation); this.ModelParameters = struct ( ... 'LayerSizes', this.LayerSizes, ... 'Activations', {this.Activations}, ... 'OutputLayerActivation', this.OutputLayerActivation, ... 'LayerWeightsInitializers', {initz}, ... 'Solver', this.Solver, ... 'LearningRate', this.LearningRate, ... 'IterationLimit', this.IterationLimit, ... 'GradientTolerance', GradientTolerance, ... 'LossTolerance', LossTolerance, ... 'StepTolerance', StepTolerance, ... 'DisplayInfo', logical (this.DisplayInfo), ... 'StandardizeData', logical (Standardize), ... 'Version', 1, 'Method', 'NeuralNetwork', ... 'Type', 'classification'); this.ConvergenceInfo = ConvergenceInfo; ## fcnntrain packs each neuron as [weights, bias] in one row, so the ## last column of every layer's matrix is its bias. nlay = numel (Mdl.LayerWeights); this.LayerWeights = cell (1, nlay); this.LayerBiases = cell (1, nlay); for i = 1:nlay Wb = Mdl.LayerWeights{i}; this.LayerWeights{i} = Wb(:, 1:end-1); this.LayerBiases{i} = Wb(:, end); endfor ## Iteration by iteration record of the fit this.TrainingHistory = trainingTable (series); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNeuralNetwork} {@var{label} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {ClassificationNeuralNetwork} {[@var{label}, @var{score}] =} predict (@var{obj}, @var{XC}) ## ## Classify new data points into categories using the neural network ## classification model from a ClassificationNeuralNetwork object. ## ## @code{@var{label} = predict (@var{obj}, @var{XC})} returns the vector of ## labels predicted for the corresponding instances in @var{XC}, using the ## predictor data in @code{obj.X} and corresponding labels, @code{obj.Y}, ## stored in the ClassificationNeuralNetwork model, @var{obj}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{ClassificationNeuralNetwork} class object. ## @item ## @var{XC} must be an @math{M*P} numeric matrix with the same number of ## features @math{P} as the corresponding predictors of the neural network ## model in @var{obj}. ## @end itemize ## ## @code{[@var{label}, @var{score}] = predict (@var{obj}, @var{XC})} also ## returns @var{score}, which contains the predicted class scores or ## posterior probabilities for each instance of the corresponding unique ## classes. ## ## The @var{score} matrix contains the classification scores for each class. ## For each observation in @var{XC}, the predicted class label is the one ## with the highest score among all classes. If the @qcode{ScoreTransform} ## property is set to a transformation function, the scores are transformed ## accordingly before being returned. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @seealso{ClassificationNeuralNetwork, fitcnet} ## @end deftypefn function [labels, scores] = predict (this, XC) ## Check for sufficient input arguments if (nargin < 2) error ("ClassificationNeuralNetwork.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'ClassificationNeuralNetwork.predict', XC); ## Check for valid XC if (isempty (XC)) error ("ClassificationNeuralNetwork.predict: XC is empty."); elseif (this.NumPredictors != columns (XC)) error (strcat ("ClassificationNeuralNetwork.predict: XC must have", ... " the same number of predictors as the trained model.")); endif ## Code the categorical predictors and standardize (if necessary) if (! isempty (this.Coding_)) XC = dummyCoding (XC, this.Coding_); endif if (! isempty (this.Mu)) XC = (XC - this.Mu) ./ this.Sigma; endif ## Predict labels from new data NumThreads = nproc (); [labels, scores] = fcnnpredict (this.LayerWeights, this.LayerBiases, ... this.Activations, ... this.OutputLayerActivation, ... XC, NumThreads); ## A class removed for its prior of 0 had no output of its own: the ## scores of the classes fitted go back to their places, 0 for it fitted = find (this.Prior > 0); if (numel (fitted) < numel (this.Prior)) full = zeros (rows (scores), numel (this.Prior), class (scores)); full(:,fitted) = scores; scores = full; labels = reshape (fitted(labels), size (labels)); endif ## A row missing a predictor, or holding a level the fit did not see, ## has no score: a rectified missing value would make one up. It ## takes the class of largest prior, as MATLAB R2024a labels it. miss = any (isnan (XC), 2); scores(miss,:) = NaN; [~, top] = max (this.Prior); labels(miss) = top; # Get class labels labels = labelsFromIndex (this.ClassNames, labels); ## Apply ScoreTransform scores = this.STfun (scores); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNeuralNetwork} {@var{label} =} resubPredict (@var{obj}) ## @deftypefnx {ClassificationNeuralNetwork} {[@var{label}, @var{score}] =} resubPredict (@var{obj}) ## ## Classify the training data using the trained neural network ## classification object. ## ## @code{@var{label} = resubPredict (@var{obj})} returns the vector of ## labels predicted for the corresponding instances in the training data, ## using the predictor data in @code{obj.X} and corresponding labels, ## @code{obj.Y}, stored in the neural network classification model, ## @var{obj}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{ClassificationNeuralNetwork} class object. ## @end itemize ## ## @code{[@var{label}, @var{score}] = resubPredict (@var{obj})} also ## returns @var{score}, which contains the predicted class scores or ## posterior probabilities for each instance of the corresponding unique ## classes. ## ## @seealso{ClassificationNeuralNetwork, fitcnet} ## @end deftypefn function [labels, scores] = resubPredict (this) ## Get used rows X = this.X; ## Code the categorical predictors and standardize (if necessary) if (! isempty (this.Coding_)) X = dummyCoding (X, this.Coding_); endif if (! isempty (this.Mu)) X = (X - this.Mu) ./ this.Sigma; endif ## Predict labels from existing data NumThreads = nproc (); [labels, scores] = fcnnpredict (this.LayerWeights, this.LayerBiases, ... this.Activations, ... this.OutputLayerActivation, ... X, NumThreads); ## A class removed for its prior of 0 had no output of its own: the ## scores of the classes fitted go back to their places, 0 for it fitted = find (this.Prior > 0); if (numel (fitted) < numel (this.Prior)) full = zeros (rows (scores), numel (this.Prior), class (scores)); full(:,fitted) = scores; scores = full; labels = reshape (fitted(labels), size (labels)); endif ## A row missing a predictor, or holding a level the fit did not see, ## has no score: a rectified missing value would make one up. It ## takes the class of largest prior, as MATLAB R2024a labels it. miss = any (isnan (X), 2); scores(miss,:) = NaN; [~, top] = max (this.Prior); labels(miss) = top; # Get class labels labels = labelsFromIndex (this.ClassNames, labels); ## Apply ScoreTransform scores = this.STfun (scores); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNeuralNetwork} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationNeuralNetwork} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationNeuralNetwork} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margin of a neural network classifier. ## ## @code{@var{m} = margin (@var{obj}, @var{X}, @var{Y})} returns a column ## vector holding, for each row of @var{X}, the score the model gives its ## true class in @var{Y} less the largest score it gives any other class. ## A positive margin means the observation is classified correctly, and ## the larger it is the more confidently so. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{ClassificationNeuralNetwork, ClassificationNeuralNetwork.edge, ## ClassificationNeuralNetwork.loss, ClassificationNeuralNetwork.predict} ## @end deftypefn function m = margin (this, X, Y) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationNeuralNetwork.margin: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y] = tableResponse (this, 'margin', X, Y, {}, nargin > 2); [X, Y] = checkXY_ (this, X, Y, "margin"); [~, scores] = predict (this, X); classes = this.ClassNames; m = zeros (rows (X), 1); ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:rows (X) idx = gYidx(i); if (isempty (idx)) m(i) = NaN; continue; endif true_score = scores(i, idx); scores(i, idx) = -Inf; m(i) = true_score - max (scores(i,:)); scores(i, idx) = true_score; endfor endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNeuralNetwork} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationNeuralNetwork} {@var{e} =} edge (@dots{}, @qcode{"Weights"}, @var{w}) ## @deftypefnx {ClassificationNeuralNetwork} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationNeuralNetwork} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Classification edge of a neural network classifier. ## ## @code{@var{e} = edge (@var{obj}, @var{X}, @var{Y})} returns the mean of ## the classification margins over the rows of @var{X}. ## ## @code{@var{e} = edge (@dots{}, @qcode{"Weights"}, @var{w})} takes the ## weighted mean instead, @var{w} holding one weight per row of @var{X}. ## The weights are normalised to sum to one before they are applied. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{ClassificationNeuralNetwork, ClassificationNeuralNetwork.margin, ## ClassificationNeuralNetwork.loss, ClassificationNeuralNetwork.predict} ## @end deftypefn function e = edge (this, X, Y, varargin) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationNeuralNetwork.edge: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'edge', X, Y, varargin, ... nargin > 2); [X, Y] = checkXY_ (this, X, Y, "edge"); ## The weights are normalized within each class to that class's prior, ## which is what the oracle does and is not the same as dividing by ## their total. This used to divide by the total. ## The weights are parsed before anything is computed, so a bad ## Name-Value pair is reported as such rather than after a margin. W = edgeWeights (varargin, Y, this.ClassNames, this.Prior, ... "ClassificationNeuralNetwork", "edge"); m = margin (this, X, Y); e = sum (W .* m(:)) / sum (W); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNeuralNetwork} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationNeuralNetwork} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationNeuralNetwork} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {ClassificationNeuralNetwork} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Classification loss of a neural network classifier. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} returns the ## proportion of the rows of @var{X} the model misclassifies against the ## true labels @var{Y}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{L} = loss (@dots{}, @var{name}, @var{value})} accepts the ## following name-value pairs: ## ## @itemize ## @item ## @qcode{"LossFun"} selects the loss. Supported values are ## @qcode{"mincost"}, the default, @qcode{"binodeviance"}, ## @qcode{"classifcost"}, @qcode{"classiferror"}, @qcode{"crossentropy"}, ## @qcode{"exponential"}, @qcode{"hinge"}, @qcode{"logit"} and ## @qcode{"quadratic"}. @qcode{"mincost"} assigns each observation to ## the class of least expected cost and charges what that assignment ## costs, so it reads the scores as a posterior; @qcode{"classifcost"} ## charges what the model's own prediction costs. @qcode{"crossentropy"} ## is defined for a network only. Note that the default differs from the ## other classifiers in this package, which default to ## @qcode{"classiferror"}, and follows MATLAB's for this class. ## ## @item ## @qcode{"Weights"} holds one weight per row of @var{X}, normalised to ## sum to one before it is applied. ## @end itemize ## ## @seealso{ClassificationNeuralNetwork, ClassificationNeuralNetwork.margin, ## ClassificationNeuralNetwork.edge, ClassificationNeuralNetwork.predict} ## @end deftypefn function L = loss (this, X, Y, varargin) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationNeuralNetwork.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationNeuralNetwork.loss: Name-Value", ... " arguments must be in pairs.")); endif [X, Y] = checkXY_ (this, X, Y, "loss"); ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights'}; dfValues = {'mincost', []}; [LossFun, W, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments lossnames = {'binodeviance', 'classifcost', 'classiferror', ... 'crossentropy', 'exponential', 'hinge', 'logit', ... 'mincost', 'quadratic'}; if (! (ischar (LossFun) && isrow (LossFun))) error (strcat ("ClassificationNeuralNetwork.loss: 'LossFun' must", ... " be a character vector.")); endif LossFun = tolower (LossFun); if (! any (strcmpi (LossFun, lossnames))) error ("ClassificationNeuralNetwork.loss: unsupported Loss function."); endif errmsg = weightsClass (W); if (! isempty (errmsg)) error ("ClassificationNeuralNetwork.loss: %s", errmsg); endif if (! isempty (W) && ! (isnumeric (W) && isvector (W))) error (strcat ("ClassificationNeuralNetwork.loss: 'Weights' must", ... " be a numeric vector.")); endif if (! isempty (W) && numel (W) != rows (X)) error (strcat ("ClassificationNeuralNetwork.loss: size of", ... " 'Weights' must equal the number of rows in X.")); endif if (! isempty (args)) error (strcat ("ClassificationNeuralNetwork.loss: invalid optional", ... " paired argument.")); endif if (isempty (W)) W = ones (rows (X), 1); endif W = double (W(:)); W = W / sum (W); [label, scores] = predict (this, X); classes = this.ClassNames; K = classCount (classes); ## Membership of the true class, as a +1/-1 indicator per class Yind = zeros (rows (X), K); ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:rows (X) idx = gYidx(i); if (isempty (idx)) L = NaN; return; endif Yind(i, idx) = 1; endfor ## The scalar score of the true class of each observation mj = sum (scores .* Yind, 2); switch (LossFun) case 'classiferror' wrong = zeros (rows (X), 1); for i = 1:rows (X) wrong(i) = ! isequal (label(i), Y(i)); endfor L = sum (W .* wrong); case 'binodeviance' L = sum (W .* log (1 + exp (-2 * mj))); case 'hinge' L = sum (W .* max (0, 1 - mj)); case 'exponential' L = sum (W .* exp (-mj)); case 'logit' L = sum (W .* log (1 + exp (-mj))); case 'quadratic' L = sum (W .* (1 - mj) .^ 2); case 'mincost' ## Each observation is assigned to the class of least expected ## cost, and charged what that assignment actually costs given its ## true class. L = 0; ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:rows (X) [~, k] = min (scores(i,:) * this.Cost); true_idx = gYidx(i); L = L + W(i) * this.Cost(true_idx, k); endfor case 'classifcost' ## What the model's own prediction costs, given the true class L = 0; ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:rows (X) true_idx = gYidx(i); pred_idx = find (ismember (classes, label(i))); L = L + W(i) * this.Cost(true_idx, pred_idx); endfor case 'crossentropy' ## Defined for a network only, whose scores are a posterior. The ## weights are rescaled to sum to the number of observations, as ## MATLAB documents, and the sum is taken over classes as well. Wn = W * rows (X); L = -sum (Wn .* log (max (mj, realmin))) / (K * rows (X)); endswitch endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNeuralNetwork} {@var{m} =} resubMargin (@var{obj}) ## ## Classification margin of a neural network classifier on its training ## data. ## ## @code{@var{m} = resubMargin (@var{obj})} is @code{margin} applied to ## the observations the model was fitted on. ## ## @seealso{ClassificationNeuralNetwork, ClassificationNeuralNetwork.margin} ## @end deftypefn function m = resubMargin (this) if (nargin != 1) error (strcat ("ClassificationNeuralNetwork.resubMargin:", ... " too many input arguments.")); endif m = margin (this, this.X, this.Y); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNeuralNetwork} {@var{e} =} resubEdge (@var{obj}) ## ## Classification edge of a neural network classifier on its training ## data. ## ## @code{@var{e} = resubEdge (@var{obj})} is @code{edge} applied to the ## observations the model was fitted on, weighted by @code{obj.W}. ## ## @seealso{ClassificationNeuralNetwork, ClassificationNeuralNetwork.edge} ## @end deftypefn function e = resubEdge (this) if (nargin != 1) error (strcat ("ClassificationNeuralNetwork.resubEdge:", ... " too many input arguments.")); endif e = edge (this, this.X, this.Y, 'Weights', this.W); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNeuralNetwork} {@var{L} =} resubLoss (@var{obj}) ## @deftypefnx {ClassificationNeuralNetwork} {@var{L} =} resubLoss (@dots{}, @var{name}, @var{value}) ## ## Classification loss of a neural network classifier on its training ## data. ## ## @code{@var{L} = resubLoss (@var{obj})} is @code{loss} applied to the ## observations the model was fitted on, weighted by @code{obj.W}. It ## takes the same @qcode{"LossFun"} name-value pair. ## ## @seealso{ClassificationNeuralNetwork, ClassificationNeuralNetwork.loss} ## @end deftypefn function L = resubLoss (this, varargin) if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationNeuralNetwork.resubLoss: Name-Value", ... " arguments must be in pairs.")); endif L = loss (this, this.X, this.Y, varargin{:}, 'Weights', this.W); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNeuralNetwork} {@var{CVMdl} =} crossval (@var{obj}) ## @deftypefnx {ClassificationNeuralNetwork} {@var{CVMdl} =} crossval (@dots{}, @var{Name}, @var{Value}) ## ## Cross Validate a Neural Network classification object. ## ## @code{@var{CVMdl} = crossval (@var{obj})} returns a cross-validated model ## object, @var{CVMdl}, from a trained model, @var{obj}, using 10-fold ## cross-validation by default. ## ## @code{@var{CVMdl} = crossval (@var{obj}, @var{name}, @var{value})} ## specifies additional name-value pair arguments to customize the ## cross-validation process. ## ## @multitable @columnfractions 0.28 0.7 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'KFold'} @tab Specify the number of folds to use in ## k-fold cross-validation. @code{"KFold", @var{k}}, where @var{k} is an ## integer greater than 1. ## ## @item @qcode{'Holdout'} @tab Specify the fraction of the data to ## hold out for testing. @code{"Holdout", @var{p}}, where @var{p} is a ## scalar in the range @math{(0,1)}. ## ## @item @qcode{'Leaveout'} @tab Specify whether to perform ## leave-one-out cross-validation. @code{"Leaveout", @var{Value}}, where ## @var{Value} is 'on' or 'off'. ## ## @item @qcode{'CVPartition'} @tab Specify a @qcode{cvpartition} ## object used for cross-validation. @code{"CVPartition", @var{cv}}, where ## @code{isa (@var{cv}, "cvpartition")} = 1. ## ## @end multitable ## ## @seealso{fitcnet, ClassificationNeuralNetwork, cvpartition, ## ClassificationPartitionedModel} ## @end deftypefn function CVMdl = crossval (this, varargin) ## Check for sufficient input arguments if (nargin < 1) error ("ClassificationNeuralNetwork.crossval: too few input arguments."); endif if (numel (varargin) == 1) error (strcat ("ClassificationNeuralNetwork.crossval: Name-Value", ... " arguments must be in pairs.")); elseif (numel (varargin) > 2) error (strcat ("ClassificationNeuralNetwork.crossval:", ... " specify only one of the optional", ... " Name-Value paired arguments.")); endif ## Add default values if (this.NumObservations < 10) numFolds = this.NumObservations; else numFolds = 10; endif Holdout = []; Leaveout = 'off'; CVPartition = []; ## Parse extra parameters while (numel (varargin) > 0) switch (tolower (varargin {1})) case 'kfold' numFolds = varargin{2}; if (! (isnumeric (numFolds) && isscalar (numFolds) && (numFolds == fix (numFolds)) && numFolds > 1)) error (strcat ("ClassificationNeuralNetwork.crossval:", ... " 'KFold' must be an integer value", ... " greater than 1.")); endif case 'holdout' Holdout = varargin{2}; if (! (isnumeric (Holdout) && isscalar (Holdout) && Holdout > 0 && Holdout < 1)) error (strcat ("ClassificationNeuralNetwork.crossval:", ... " 'Holdout' must be a numeric value", ... " between 0 and 1.")); endif case 'leaveout' Leaveout = varargin{2}; if (! (ischar (Leaveout) && (strcmpi (Leaveout, 'on') || strcmpi (Leaveout, 'off')))) error (strcat ("ClassificationNeuralNetwork.crossval:", ... " 'Leaveout' must be either 'on' or 'off'.")); endif case 'cvpartition' CVPartition = varargin{2}; if (! (isa (CVPartition, 'cvpartition'))) error (strcat ("ClassificationNeuralNetwork.crossval:", ... " 'CVPartition' must be a 'cvpartition' object.")); endif otherwise error (strcat ("ClassificationNeuralNetwork.crossval: invalid",... " parameter name in optional paired arguments.")); endswitch varargin(1:2) = []; endwhile ## Determine the cross-validation method to use. The partition covers ## the observations actually trained on: a row dropped for a missing ## value is not one the folds can use, and including it would leave the ## partition, the stored data and NumObservations disagreeing. The ## response is passed rather than a count so the folds stay stratified. Yused = this.Y; if (! isempty (CVPartition)) partition = CVPartition; elseif (! isempty (Holdout)) partition = cvpartition (Yused, 'Holdout', Holdout); elseif (strcmpi (Leaveout, 'on')) partition = cvpartition (this.NumObservations, 'LeaveOut'); else partition = cvpartition (Yused, 'KFold', numFolds); endif ## Create a cross-validated model object CVMdl = ClassificationPartitionedModel (this, partition); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNeuralNetwork} {@var{CVMdl} =} compact (@var{obj}) ## ## Create a CompactClassificationNeuralNetwork object. ## ## @code{@var{CVMdl} = compact (@var{obj})} creates a compact version of the ## ClassificationNeuralNetwork object, @var{obj}. ## ## @seealso{fitcnet, ClassificationNeuralNetwork, ## CompactClassificationNeuralNetwork} ## @end deftypefn function CVMdl = compact (this) ## Create a compact model CVMdl = CompactClassificationNeuralNetwork (this); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationNeuralNetwork} {} savemodel (@var{obj}, @var{filename}) ## ## Save a ClassificationNeuralNetwork object. ## ## @code{savemodel (@var{obj}, @var{filename})} saves each property of a ## ClassificationNeuralNetwork object into an Octave binary file, the name ## of which is specified in @var{filename}, along with an extra variable, ## which defines the type classification object these variables constitute. ## Use ## @code{loadmodel} in order to load a classification object into Octave's ## workspace. ## ## @seealso{loadmodel, fitcnet, ClassificationNeuralNetwork} ## @end deftypefn function savemodel (this, fname) if (nargin < 2) error ("ClassificationNeuralNetwork.savemodel: too few input arguments."); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error ("ClassificationNeuralNetwork.savemodel: FNAME must be a character vector."); endif ## Generate variable for class name classdef_name = 'ClassificationNeuralNetwork'; ## Create variables from model properties X = this.X; Y = encodeLabels (this.Y); NumObservations = this.NumObservations; RowsUsed = this.RowsUsed; NumPredictors = this.NumPredictors; PredictorNames = this.PredictorNames; ResponseName = this.ResponseName; ClassNames = encodeLabels (this.ClassNames); ScoreTransform = this.ScoreTransform; Sigma = this.Sigma; BinEdges = this.BinEdges; Mu = this.Mu; LayerSizes = this.LayerSizes; Activations = this.Activations; OutputLayerActivation = this.OutputLayerActivation; LearningRate = this.LearningRate; IterationLimit = this.IterationLimit; ModelParameters = this.ModelParameters; ConvergenceInfo = this.ConvergenceInfo; DisplayInfo = this.DisplayInfo; Solver = this.Solver; LayerWeights = this.LayerWeights; LayerBiases = this.LayerBiases; Cost = this.Cost; Prior = this.Prior; W = this.W; CategoricalPredictors = this.CategoricalPredictors; Coding_ = this.Coding_; ExpandedPredictorNames = this.ExpandedPredictorNames; STfun = this.STfun; TrainingHistory = this.TrainingHistory; ## Save classdef name and all model properties as individual variables ## The history is a table and ConvergenceInfo holds another; both lose ## their class on the way to the file and load_model rebuilds them, so ## the warning that says so is expected and is not shown. ws_ = warning ('off', 'Octave:save:classdef:unsupported'); unwind_protect HyperparameterOptimizationResults = this.HyperparameterOptimizationResults; save ('-binary', fname, 'classdef_name', 'X', 'Y', 'NumObservations', ... 'RowsUsed', 'NumPredictors', 'PredictorNames', 'BinEdges', ... 'ResponseName', ... 'ClassNames', 'ScoreTransform', 'Sigma', 'Mu', ... 'LayerSizes', 'Activations', 'OutputLayerActivation', ... 'LearningRate', 'IterationLimit', 'Solver', 'ModelParameters', ... 'ConvergenceInfo', 'TrainingHistory', 'DisplayInfo', ... 'LayerWeights', 'LayerBiases', ... 'Cost', 'Prior', 'W', 'CategoricalPredictors', 'Coding_', ... 'ExpandedPredictorNames', 'STfun', ... 'HyperparameterOptimizationResults'); unwind_protect_cleanup warning (ws_); end_unwind_protect endfunction endmethods methods (Access = private) ## Shared validation for the assessment methods, so each reports under ## its own name. function [X, Y] = checkXY_ (this, X, Y, caller) if (isempty (X)) error ("ClassificationNeuralNetwork.%s: X is empty.", caller); elseif (columns (this.X) != columns (X)) error (strcat ("ClassificationNeuralNetwork.%s: X must have the", ... " same number of predictors as the trained model."), ... caller); endif if (isempty (Y)) error ("ClassificationNeuralNetwork.%s: Y is empty.", caller); elseif (rows (X) != rows (Y)) error (strcat ("ClassificationNeuralNetwork.%s: Y must have the", ... " same number of rows as X."), caller); endif endfunction endmethods methods(Static, Hidden) function mdl = load_model (filename, data) data = decodeLabels (data); ## Create a ClassificationNeuralNetwork object mdl = ClassificationNeuralNetwork (1, 1); ## Get fieldnames from DATA (including private properties) names = fieldnames (data); ## The set methods for these read other properties, and one of them ## rebuilds Coeffs, so they are assigned once everything else is in ## place rather than in the order the file happens to list them. late = ismember (names, {'Cost', 'Prior', 'ScoreTransform', ... 'ResponseTransform'}); names = [names(! late); names(late)]; ## Copy data into object. A table loses its class on the way to a ## binary file and comes back a structure, so it is rebuilt before the ## property it belongs to is assigned. for i = 1:numel (names) ## Check fieldnames in DATA match properties in ClassificationNeuralNetwork try mdl.(names{i}) = restore_tables (data.(names{i})); catch error (strcat ("ClassificationNeuralNetwork.load_model:", ... " invalid model in '%s'."), filename) end_try_catch endfor ## A model saved before the history was written out carries the series ## as vectors in ConvergenceInfo instead; rebuild from those, so that an ## older file still loads and loads as the current shape. if (isempty (mdl.TrainingHistory) && ! isempty (mdl.ConvergenceInfo)) mdl = restoreOlderModel (mdl); endif endfunction endmethods endclassdef ## The recorded history, whose columns follow the solver that produced it. ## Building it in one place keeps the fit and the model reloaded from disk ## from drifting apart. function T = trainingTable (series) iter = (1:numel (series.TrainingLoss))'; ## Time is NaN because neither solver records a per-iteration figure; the ## total the fit took is ConvergenceInfo.Time. The validation pair is NaN ## because no validation set can be given, which is what MATLAB reports ## when none is. Both are present so the columns are MATLAB's. pad = NaN (numel (iter), 1); if (isfield (series, 'Gradient')) T = table (iter, series.TrainingLoss(:), series.Gradient(:), ... series.Step(:), pad, pad, pad, 'VariableNames', ... {'Iteration', 'TrainingLoss', 'Gradient', 'Step', 'Time', ... 'ValidationLoss', 'ValidationChecks'}); else T = table (iter, series.TrainingLoss(:), series.Accuracy(:), ... pad, pad, pad, 'VariableNames', ... {'Iteration', 'TrainingLoss', 'TrainingAccuracy', 'Time', ... 'ValidationLoss', 'ValidationChecks'}); endif endfunction ## ConvergenceInfo reports where the fit ended: the last value of each series ## as a scalar, beside the whole series as History. MATLAB divides the two ## the same way, and a vector here would repeat what History already holds. function ci = convergenceStruct (series, elapsed, criterion) ## The fields are assigned in MATLAB's own order, which fieldnames reports. ci.Iterations = numel (series.TrainingLoss); ci.TrainingLoss = lastValue (series.TrainingLoss); if (isfield (series, 'Gradient')) ci.Gradient = lastValue (series.Gradient); ci.Step = lastValue (series.Step); else ci.Accuracy = lastValue (series.Accuracy); endif ci.Time = elapsed; ci.ValidationLoss = NaN; ci.ValidationChecks = NaN; if (isfield (series, 'Gradient')) ci.ConvergenceCriterion = criterion; endif ci.History = trainingTable (series); endfunction ## Where the fit ended. A fit that took no iteration recorded nothing, so ## the value it ended at is empty rather than an error. function v = lastValue (x) if (isempty (x)) v = []; else v = x(end); endif endfunction ## The series as it was persisted, or, from a model saved before the series ## became a property of its own, rebuilt from the vectors ConvergenceInfo ## used to carry. ## Read a model written before ConvergenceInfo reported where the fit ended ## rather than the whole series: the vectors it carries are the history. function mdl = restoreOlderModel (mdl) ci = mdl.ConvergenceInfo; series = struct ('TrainingLoss', ci.TrainingLoss(:)); if (isfield (ci, 'Gradient')) series.Gradient = ci.Gradient(:); series.Step = ci.Step(:); criterion = ci.ConvergenceCriterion; else series.Accuracy = ci.Accuracy(:); criterion = ''; endif mdl.TrainingHistory = trainingTable (series); mdl.ConvergenceInfo = convergenceStruct (series, ci.Time, criterion); endfunction %!test # A categorical 'ClassNames' keeps only the classes it names %! load fisheriris %! Mdl = ClassificationNeuralNetwork (meas, categorical (species), ... %! 'ClassNames', categorical ({'versicolor'; 'virginica'})); %! assert_equal (Mdl.NumObservations, 100); %! assert_equal (class (Mdl.ClassNames), 'categorical'); ## Test input validation for constructor ## The full-batch solver is selected by name and says so. %!test %! load fisheriris %! Mdl = fitcnet (meas, species, "IterationLimit", 50, "Solver", "lbfgs"); %! assert_equal (Mdl.Solver, "LBFGS"); %! assert_equal (columns (Mdl.TrainingHistory), 7); %! assert_equal (Mdl.TrainingHistory.Properties.VariableNames, ... %! {"Iteration", "TrainingLoss", "Gradient", "Step", ... %! "Time", "ValidationLoss", "ValidationChecks"}); ## It records what it measured to decide it had stopped, and no accuracy. %!test %! load fisheriris %! Mdl = fitcnet (meas, species, "IterationLimit", 50, "Solver", "lbfgs"); %! ci = Mdl.ConvergenceInfo; %! assert_equal (isfield (ci, "Gradient"), true); %! assert_equal (isfield (ci, "Step"), true); %! assert_equal (isfield (ci, "ConvergenceCriterion"), true); %! assert_equal (isfield (ci, "Accuracy"), false); ## The stochastic solver still reports accuracy, and is still reached by name. %!test %! load fisheriris %! Mdl = fitcnet (meas, species, "Solver", "sgd", "IterationLimit", 20); %! assert_equal (Mdl.Solver, "Gradient Descent"); %! assert_equal (isfield (Mdl.ConvergenceInfo, "Accuracy"), true); %! assert_equal (columns (Mdl.TrainingHistory), 6); ## The default solver is lbfgs, which reports the quantities it converges on ## rather than an accuracy. %!test %! load fisheriris %! Mdl = fitcnet (meas, species, "IterationLimit", 20); %! assert_equal (Mdl.Solver, "LBFGS"); %! assert_equal (isfield (Mdl.ConvergenceInfo, "Accuracy"), false); %! assert_equal (fieldnames (Mdl.ConvergenceInfo), ... %! {"Iterations"; "TrainingLoss"; "Gradient"; "Step"; ... %! "Time"; "ValidationLoss"; "ValidationChecks"; ... %! "ConvergenceCriterion"; "History"}); %! assert_equal (Mdl.TrainingHistory.Properties.VariableNames, ... %! {"Iteration", "TrainingLoss", "Gradient", "Step", ... %! "Time", "ValidationLoss", "ValidationChecks"}); %! assert_equal (Mdl.ConvergenceInfo.Iterations, rows (Mdl.TrainingHistory)); ## From the same starting weights it reaches a lower training loss in fewer ## passes over the data, which is the reason for offering it. %!test %! load fisheriris %! rand ("state", 3); randn ("state", 3); %! Ms = fitcnet (meas, species, "IterationLimit", 200, "Solver", "sgd"); %! rand ("state", 3); randn ("state", 3); %! Ml = fitcnet (meas, species, "IterationLimit", 200, "Solver", "lbfgs"); %! ls = Ms.ConvergenceInfo.TrainingLoss(end); %! ll = Ml.ConvergenceInfo.TrainingLoss(end); %! assert_equal (ll < ls, true); %! assert_equal (height (Ml.TrainingHistory) < height (Ms.TrainingHistory), ... %! true); ## A model trained by lbfgs comes back off disk with its own four columns. %!test %! load fisheriris %! Mdl = fitcnet (meas, species, "IterationLimit", 30, "Solver", "lbfgs"); %! fname = tempname (); %! savemodel (Mdl, fname); %! Mdl2 = loadmodel (fname); %! delete (fname); %! assert_equal (table2cell (Mdl2.TrainingHistory), ... %! table2cell (Mdl.TrainingHistory)); ## An option that cannot act is refused rather than ignored. ## A response naming its classes in the rows of a character matrix is one ## of the documented types and MATLAB accepts it on every classifier. The ## whole surface below was broken and untested, which is why it stayed so. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcnet (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcnet (Xch, Ycell); %! assert_equal (size (Mc.ClassNames), [2, 10]); %! assert_equal (cellstr (Mc.ClassNames), Ms.ClassNames); ## predict returns whole names, not their first letters. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcnet (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcnet (Xch, Ycell); %! pch = predict (Mc, Xch); %! assert_equal (columns (pch), 10); %! assert_equal (cellstr (pch), predict (Ms, Xch)); ## loss, margin and edge read a character response as the same response. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcnet (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcnet (Xch, Ycell); %! assert_equal (loss (Mc, Xch, Ych), loss (Ms, Xch, Ycell), 1e-12); %! assert_equal (margin (Mc, Xch, Ych), margin (Ms, Xch, Ycell), 1e-12); %! assert_equal (edge (Mc, Xch, Ych), edge (Ms, Xch, Ycell), 1e-12); ## A character matrix pads its rows out to the longest name, and the padding ## is part of the name: R2024a reports ClassNames of ['ab '; 'abcd']. %!test %! Xpad = [1 2; 3 4; 1.1 2.1; 3.1 4.1; 1.2 2.2; 3.2 4.2]; %! Ypad = char ({"ab", "abcd", "ab", "abcd", "ab", "abcd"}); %! rand ("state", 1); randn ("state", 1); %! Mp = fitcnet (Xpad, Ypad); %! assert_equal (size (Mp.ClassNames), [2, 4]); %! assert_equal (Mp.ClassNames(1,:), "ab "); ## A row dropped for a missing predictor is the only case that exercises ## indexing the response by row rather than by element. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! Xmiss = Xch; Xmiss(3,2) = NaN; %! rand ("state", 1); randn ("state", 1); Md = fitcnet (Xmiss, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcnet (Xmiss, Ycell); %! assert_equal (size (Md.ClassNames), [2, 10]); %! assert_equal (cellstr (Md.ClassNames), Ms.ClassNames); ## ClassNames may itself be given as a character matrix, which selects the ## classes by whole name: ismember between two character matrices compares ## them character by character and would select by letter. %!test %! load fisheriris %! rand ("state", 1); randn ("state", 1); %! Mf = fitcnet (meas, char (species), ... %! "ClassNames", char ({"versicolor", "virginica"})); %! assert_equal (rows (Mf.ClassNames), 2); %! assert_equal (cellstr (Mf.ClassNames), {"versicolor"; "virginica"}); ## A model fitted from a character response comes back off disk unchanged. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcnet (Xch, Ych); %! fname = tempname (); %! savemodel (Mc, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.ClassNames, Mc.ClassNames); %! assert_equal (predict (M2, Xch), predict (Mc, Xch)); ## crossval carries a character response through cvpartition and back. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcnet (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcnet (Xch, Ycell); %! rand ("state", 2); cvc = crossval (Mc, "KFold", 3); %! rand ("state", 2); cvs = crossval (Ms, "KFold", 3); %! assert_equal (cellstr (kfoldPredict (cvc)), kfoldPredict (cvs)); %!test # A row missing a predictor takes the class of largest prior %! X = [(1:10)', mod((1:10)', 3)]; %! y = [1; 1; 1; 1; 1; 1; 2; 2; 2; 2]; %! Mdl = ClassificationNeuralNetwork (X, y); %! [label, score] = predict (Mdl, [NaN, 1]); %! assert_equal (label, 1); %! assert_equal (score, [NaN, NaN]); %! Mdl = ClassificationNeuralNetwork (X, y, 'Prior', [0.3, 0.7]); %! assert_equal (predict (Mdl, [NaN, 1]), 2); %!error ... %! fitcnet (ones (5, 2), [1; 1; 2; 2; 2], "Solver", "sgd", ... %! "GradientTolerance", 1e-8) %!error ... %! fitcnet (ones (5, 2), [1; 1; 2; 2; 2], "Solver", "sgd", "LossTolerance", 1) %!error ... %! fitcnet (ones (5, 2), [1; 1; 2; 2; 2], "Solver", "sgd", ... %! "StepTolerance", 1e-8) %!error ... %! fitcnet (ones (5, 2), [1; 1; 2; 2; 2], "Solver", "lbfgs", "LearningRate", 0.1) %!error ... %! fitcnet (ones (5, 2), [1; 1; 2; 2; 2], "Solver", "bogus") %!error ... %! fitcnet (ones (5, 2), [1; 1; 2; 2; 2], "Solver", "lbfgs", "GradientTolerance", -1) %!error ... %! fitcnet (ones (5, 2), [1; 1; 2; 2; 2], "Solver", "lbfgs", "StepTolerance", -1) %!error ... %! fitcnet (ones (5, 2), [1; 1; 2; 2; 2], "Solver", "lbfgs", "LossTolerance", NaN) %!error ... %! ClassificationNeuralNetwork () %!error ... %! ClassificationNeuralNetwork (ones (10,2)) %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (5,1)) %!error ... %! ClassificationNeuralNetwork (ones (5,3), ones (5,1), 'standardize', 'a') %!error ... %! ClassificationNeuralNetwork (ones (5,2), ones (5,1), 'PredictorNames', ['A']) %!error ... %! ClassificationNeuralNetwork (ones (5,2), ones (5,1), 'PredictorNames', 'A') %!error ... %! ClassificationNeuralNetwork (ones (5,2), ones (5,1), 'PredictorNames', {'A', 'B', 'C'}) %!error ... %! ClassificationNeuralNetwork (ones (5,2), ones (5,1), 'ResponseName', {'Y'}) %!error ... %! ClassificationNeuralNetwork (ones (5,2), ones (5,1), 'ResponseName', 1) %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'ClassNames', @(x)x) %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'ClassNames', {1}) %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'ClassNames', [1, 2]) %!error ... %! ClassificationNeuralNetwork (ones (5,2), ['a';'b';'a';'a';'b'], 'ClassNames', ['a';'c']) %!error ... %! ClassificationNeuralNetwork (ones (5,2), {'a';'b';'a';'a';'b'}, 'ClassNames', {'a','c'}) %!error ... %! ClassificationNeuralNetwork (ones (10,2), logical (ones (10,1)), 'ClassNames', [true, false]) %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'LayerSizes', -1) %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'LayerSizes', 0.5) %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'LayerSizes', [1,-2]) %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'LayerSizes', [10,20,30.5]) %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'LearningRate', -0.1) %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'LearningRate', [0.1, 0.01]) %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'LearningRate', 'a') %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'Activations', 123) %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'Activations', 'unsupported_type') %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'LayerSizes', [10, 5], ... %! 'Activations', {'sigmoid', 'unsupported_type'}) %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'Activations', {'sigmoid', 'relu', 'softmax'}) %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'OutputLayerActivation', 123) %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'OutputLayerActivation', 'unsupported_type') %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'IterationLimit', -1) %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'IterationLimit', 0.5) %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'IterationLimit', [1,2]) %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'ScoreTransform', [1,2]) %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'ScoreTransform', 'unsupported_type') %!error ... %! ClassificationNeuralNetwork (ones (10,2), ones (10,1), 'some', 'some') %!error ... %! ClassificationNeuralNetwork ([1;2;3;'a';4], ones (5,1)) %!error ... %! ClassificationNeuralNetwork ([1;2;3;Inf;4], ones (5,1)) ## Test input validation for subsasgn method %!shared x, y, objST, Mdl %! load fisheriris %! x = meas; %! y = grp2idx (species); %! Mdl = fitcnet (x, y, 'IterationLimit', 100); %!error ... %! Mdl.ScoreTransform = 'a'; ## Test input validation for predict method %!error ... %! predict (Mdl) %!error ... %! predict (Mdl, []) %!error ... %! predict (Mdl, 1) ## Test output for crossval method %!test %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! CVMdl = crossval (Mdl, 'KFold', 5); %! warning (status); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y}) %! assert_equal (CVMdl.KFold == 5, true) %! assert_equal (class (CVMdl.Trained{1}), "CompactClassificationNeuralNetwork") %! assert_equal (CVMdl.CrossValidatedModel, "NeuralNetwork") %!test %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! CVMdl = crossval (Mdl, 'HoldOut', 0.2); %! warning (status); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y}) %! assert_equal (class (CVMdl.Trained{1}), "CompactClassificationNeuralNetwork") %! assert_equal (CVMdl.CrossValidatedModel, "NeuralNetwork") ## Test input validation for crossval method %!error ... %! crossval (Mdl, 'KFold') %!error ... %! crossval (Mdl, 'KFold', 5, 'leaveout', 'on') %!error ... %! crossval (Mdl, 'KFold', 'a') %!error ... %! crossval (Mdl, 'KFold', 1) %!error ... %! crossval (Mdl, 'KFold', -1) %!error ... %! crossval (Mdl, 'KFold', 11.5) %!error ... %! crossval (Mdl, 'KFold', [1,2]) %!error ... %! crossval (Mdl, 'Holdout', 'a') %!error ... %! crossval (Mdl, 'Holdout', 11.5) %!error ... %! crossval (Mdl, 'Holdout', -1) %!error ... %! crossval (Mdl, 'Holdout', 0) %!error ... %! crossval (Mdl, 'Holdout', 1) %!error ... %! crossval (Mdl, 'Leaveout', 1) %!error ... %! crossval (Mdl, 'CVPartition', 1) %!error ... %! crossval (Mdl, 'CVPartition', 'a') %!error ... %! crossval (Mdl, 'some', 'some') ## A saved and reloaded model carries the trained parameters, not those of ## the placeholder object load_model starts from. %!test %! Mdl = fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], ... %! 'LayerSizes', [3, 2], 'IterationLimit', 20); %! fname = tempname (); %! savemodel (Mdl, fname); %! Mdl2 = loadmodel (fname); %! delete (fname); %! assert_equal (Mdl2.LayerWeights, Mdl.LayerWeights); %! assert_equal (Mdl2.LayerBiases, Mdl.LayerBiases); ## Cost, Prior, W and the predictor bookkeeping survive a save and load. %!test %! Mdl = fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], ... %! 'IterationLimit', 20, 'Prior', [0.25, 0.75]); %! Mdl.Cost = [0, 3; 5, 0]; %! fname = tempname (); %! savemodel (Mdl, fname); %! Mdl2 = loadmodel (fname); %! delete (fname); %! assert_equal (Mdl2.Cost, [0, 3; 5, 0]); %! assert_equal (Mdl2.Prior, [0.25, 0.75]); %! assert_equal (Mdl2.W, Mdl.W); %! assert_equal (Mdl2.ExpandedPredictorNames, Mdl.ExpandedPredictorNames); %! assert_equal (Mdl2.CategoricalPredictors, Mdl.CategoricalPredictors); ## TrainingHistory comes back a table, rebuilt from ConvergenceInfo because ## Octave cannot write a classdef object to a binary file. %!test %! Mdl = fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], ... %! 'Solver', 'sgd', 'IterationLimit', 25); %! fname = tempname (); %! savemodel (Mdl, fname); %! Mdl2 = loadmodel (fname); %! delete (fname); %! assert_equal (istable (Mdl2.TrainingHistory), true); %! assert_equal (height (Mdl2.TrainingHistory), 25); %! assert_equal (table2cell (Mdl2.TrainingHistory), ... %! table2cell (Mdl.TrainingHistory)); ## A reloaded model predicts exactly what the original did. %!test %! load fisheriris %! Mdl = fitcnet (meas, species, 'IterationLimit', 20); %! fname = tempname (); %! savemodel (Mdl, fname); %! Mdl2 = loadmodel (fname); %! delete (fname); %! [label, score] = predict (Mdl, meas); %! [label2, score2] = predict (Mdl2, meas); %! assert_equal (label2, label); %! assert_equal (score2, score); ## A non-default ScoreTransform survives a save and load. %!test %! Mdl = fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], 'IterationLimit', 20); %! Mdl.ScoreTransform = 'symmetric'; %! fname = tempname (); %! savemodel (Mdl, fname); %! Mdl2 = loadmodel (fname); %! delete (fname); %! assert_equal (Mdl2.ScoreTransform, 'symmetric'); %! [~, s1] = predict (Mdl2, [1, 2; 4, 5]); %! [~, s2] = predict (Mdl, [1, 2; 4, 5]); %! assert_equal (s1, s2); %!error ... %! savemodel (ClassificationNeuralNetwork ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2])) %!error ... %! savemodel (ClassificationNeuralNetwork ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), 1) %!error ... %! savemodel (ClassificationNeuralNetwork ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), ['ab'; 'cd']) ## A trained network returns a posterior, not a fraction of one. A backward ## pass that settles at half the target leaves every label right and every ## score halved, so the scores and their row sums are what catch it. %!test %! rand ('seed', 42); %! randn ('seed', 42); %! X = [randn(40, 2) * 0.3 + 3; randn(40, 2) * 0.3 - 3]; %! Y = [ones(40, 1); 2 * ones(40, 1)]; %! Mdl = fitcnet (X, Y, 'LayerSizes', [8, 8], 'IterationLimit', 400); %! [label, score] = predict (Mdl, [3, 3; -3, -3]); %! assert_equal (label, [1; 2]); %! assert_equal (all (abs (sum (score, 2) - 1) < 0.1), true); %! assert_equal (max (score(1,:)) > 0.8, true); %! assert_equal (max (score(2,:)) > 0.8, true); ## The defaults follow MATLAB: rectified hidden layers and a softmax output. %!test %! Mdl = fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]); %! assert_equal (Mdl.Activations, 'relu'); %! assert_equal (Mdl.OutputLayerActivation, 'softmax'); ## Training reports a history that was recorded, one entry per iteration. %!test %! rand ('seed', 42); %! randn ('seed', 42); %! X = [randn(30, 2) * 0.4 + 2; randn(30, 2) * 0.4 - 2]; %! Y = [ones(30, 1); 2 * ones(30, 1)]; %! Mdl = fitcnet (X, Y, 'Solver', 'sgd', 'IterationLimit', 100); %! loss = Mdl.ConvergenceInfo.History.TrainingLoss; %! acc = Mdl.ConvergenceInfo.History.TrainingAccuracy; %! assert_equal (numel (loss), 100); %! assert_equal (numel (acc), 100); %! assert_equal (any (loss != 0), true); %! assert_equal (loss(end) < loss(1), true); %! ## ConvergenceInfo itself reports where the fit ended. %! assert_equal (Mdl.ConvergenceInfo.TrainingLoss, loss(end)); %! assert_equal (Mdl.ConvergenceInfo.Accuracy, acc(end)); %! assert_equal (mean (predict (Mdl, X) == Y) > 0.95, true); ## Every activation trains to a usable posterior on separable data. %!test %! rand ('seed', 7); %! randn ('seed', 7); %! X = [randn(30, 2) * 0.3 + 3; randn(30, 2) * 0.3 - 3]; %! Y = [ones(30, 1); 2 * ones(30, 1)]; %! names = {'linear', 'sigmoid', 'relu', 'tanh', 'lrelu', 'elu', 'gelu'}; %! for k = 1:numel (names) %! Mdl = fitcnet (X, Y, 'LayerSizes', 8, 'Activations', names{k}, ... %! 'IterationLimit', 300); %! [label, score] = predict (Mdl, [3, 3; -3, -3]); %! assert_equal (label, [1; 2]); %! assert_equal (all (isfinite (score(:))), true); %! endfor ## The trained parameters are reachable, one matrix and one bias per layer. %!test %! Mdl = fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], ... %! 'LayerSizes', [3, 2], 'IterationLimit', 20); %! assert_equal (numel (Mdl.LayerWeights), 3); %! assert_equal (numel (Mdl.LayerBiases), 3); %! assert_equal (size (Mdl.LayerWeights{1}), [3, 2]); %! assert_equal (size (Mdl.LayerWeights{2}), [2, 3]); %! assert_equal (size (Mdl.LayerWeights{3}), [2, 2]); %! assert_equal (size (Mdl.LayerBiases{1}), [3, 1]); %! assert_equal (size (Mdl.LayerBiases{3}), [2, 1]); ## Cost, Prior, W and the predictor bookkeeping carry their defaults. %!test %! Mdl = fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], 'IterationLimit', 20); %! assert_equal (Mdl.Cost, [0, 1; 1, 0]); %! assert_equal (Mdl.Prior, [0.5, 0.5]); %! assert_equal (size (Mdl.W), [4, 1]); %! assert_equal (sum (Mdl.W), 1, 1e-12); %! assert_equal (Mdl.CategoricalPredictors, []); %! assert_equal (Mdl.ExpandedPredictorNames, Mdl.PredictorNames); ## TrainingHistory holds one row per iteration. %!test %! Mdl = fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], ... %! 'Solver', 'sgd', 'IterationLimit', 25); %! assert_equal (istable (Mdl.TrainingHistory), true); %! assert_equal (height (Mdl.TrainingHistory), 25); %! assert_equal (Mdl.TrainingHistory.Properties.VariableNames, ... %! {'Iteration', 'TrainingLoss', 'TrainingAccuracy', ... %! 'Time', 'ValidationLoss', 'ValidationChecks'}); %! assert_equal (Mdl.TrainingHistory.Iteration', 1:25); ## A margin is positive wherever the model is right, and edge averages it. %!test %! rand ('seed', 42); %! randn ('seed', 42); %! X = [randn(30, 2) * 0.4 + 2; randn(30, 2) * 0.4 - 2]; %! Y = [ones(30, 1); 2 * ones(30, 1)]; %! Mdl = fitcnet (X, Y, 'IterationLimit', 200); %! m = margin (Mdl, X, Y); %! assert_equal (size (m), [60, 1]); %! assert_equal (all (m > 0), true); %! assert_equal (edge (Mdl, X, Y), mean (m), 1e-12); %! assert_equal (resubMargin (Mdl), m, 1e-12); %! assert_equal (resubEdge (Mdl), edge (Mdl, X, Y, 'Weights', Mdl.W), 1e-12); ## Every documented loss function is accepted and finite. %!test %! rand ('seed', 42); %! randn ('seed', 42); %! X = [randn(30, 2) * 0.4 + 2; randn(30, 2) * 0.4 - 2]; %! Y = [ones(30, 1); 2 * ones(30, 1)]; %! Mdl = fitcnet (X, Y, 'IterationLimit', 200); %! names = {'binodeviance', 'classifcost', 'classiferror', 'crossentropy', ... %! 'exponential', 'hinge', 'logit', 'mincost', 'quadratic'}; %! for k = 1:numel (names) %! L = loss (Mdl, X, Y, 'LossFun', names{k}); %! assert_equal (isscalar (L) && isfinite (L) && L >= 0, true); %! endfor %! assert_equal (loss (Mdl, X, Y), loss (Mdl, X, Y, 'LossFun', 'mincost')); %! assert_equal (resubLoss (Mdl), loss (Mdl, X, Y, 'Weights', Mdl.W), 1e-12); ## Cost is assigned with dot notation, and an empty restores the default. %!test %! Mdl = fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 1; 2], 'IterationLimit', 20); %! assert_equal (Mdl.Prior, [0.75, 0.25], 1e-12); %! Mdl.Cost = [0, 2; 1, 0]; %! assert_equal (Mdl.Cost, [0, 2; 1, 0]); %! Mdl.Cost = []; %! assert_equal (Mdl.Cost, [0, 1; 1, 0]); ## Prior is taken at the fit and follows into the observation weights. %!test %! Mdl = fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 1; 2], ... %! 'IterationLimit', 20, 'Prior', [0.25, 0.75]); %! assert_equal (Mdl.Prior, [0.25, 0.75]); %! assert_equal (Mdl.W, [0.25/3; 0.25/3; 0.25/3; 0.75], 1e-12); %!error ... %! margin (fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), [1, 2]) %!error ... %! loss (fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), [], [1]) %!error ... %! loss (fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), [1, 2, 3], [1]) %!error ... %! loss (fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), [1, 2], [1; 2]) %!error ... %! loss (fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), [1, 2], 1, 'LossFun', 'bogus') %!error ... %! loss (fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), [1, 2], 1, 'Bogus', 1) %!error ... %! edge (fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), [1, 2], 1, 'bogus', 1) %!error ... %! Mdl = fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]); %! Mdl.Cost = [0, 1, 2]; %!error ... %! Mdl = fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]); %! Mdl.Cost = 1:4; ## RowsUsed is empty when every observation was used. %!test %! load fisheriris %! X = meas; %! Y = grp2idx (species); %! Mdl = fitcnet (X, Y, 'IterationLimit', 20); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (class (Mdl.RowsUsed), 'double'); %! assert_equal (Mdl.NumObservations, 150); %! assert_equal (rows (Mdl.X), 150); %! assert_equal (rows (Mdl.W), 150); ## A missing response drops its observation and RowsUsed marks it. %!test %! load fisheriris %! X = meas; %! Y = grp2idx (species); %! Y(5) = NaN; %! Mdl = fitcnet (X, Y, 'IterationLimit', 20); %! assert_equal (class (Mdl.RowsUsed), 'logical'); %! assert_equal (size (Mdl.RowsUsed), [150, 1]); %! assert_equal (sum (Mdl.RowsUsed), 149); %! assert_equal (Mdl.RowsUsed(5), false); %! assert_equal (Mdl.NumObservations, 149); %! assert_equal (rows (Mdl.X), 149); %! assert_equal (rows (Mdl.W), 149); ## A missing predictor keeps its observation, so RowsUsed stays empty. %!test %! load fisheriris %! X = meas; %! X(3,2) = NaN; %! Y = grp2idx (species); %! Mdl = fitcnet (X, Y, 'IterationLimit', 20); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (Mdl.NumObservations, 150); %! assert_equal (rows (Mdl.X), 150); %! assert_equal (sum (isnan (Mdl.X(:))), 1); ## Mu and Sigma are empty unless the predictors are standardized. %!test %! load fisheriris %! Mdl = fitcnet (meas, species, 'IterationLimit', 20); %! assert_equal (Mdl.Mu, []); %! assert_equal (Mdl.Sigma, []); ## Standardizing weights the complete observations by each class's original ## share of the observation weight. Values from MATLAB R2024a. %!test %! load fisheriris %! X = meas; %! X(3,2) = NaN; X(17,4) = NaN; X(140,1) = NaN; %! Mdl = fitcnet (X, species, 'Standardize', true, 'IterationLimit', 20); %! assert_equal (Mdl.Mu, [5.8405997732426291, 3.0547817460317455, ... %! 3.7612840136054406, 1.1980799319727886], 1e-13); %! assert_equal (Mdl.Sigma, [0.82803317153591371, 0.43533400398915184, ... %! 1.7640762592568813, 0.76297286301694878], 1e-13); ## fitcnet takes Prior and Cost, and the prior reweights the observations. ## Values from R2024a. %!test %! load fisheriris %! i3 = [1:50, 51:80, 101:120]; %! Mdl = fitcnet (meas(i3,:), species(i3), 'IterationLimit', 20, ... %! 'Prior', [0.2, 0.3, 0.5]); %! assert_equal (Mdl.Prior, [0.2, 0.3, 0.5], 1e-14); %! assert_equal (Mdl.W(1), 0.004, 1e-14); %! assert_equal (Mdl.W(51), 0.01, 1e-14); %! assert_equal (Mdl.W(81), 0.025, 1e-14); ## Cost may be assigned after the fit. %!test %! load fisheriris %! i3 = [1:50, 51:80, 101:120]; %! Mdl = fitcnet (meas(i3,:), species(i3), 'IterationLimit', 20); %! Mdl.Cost = [0, 2, 3; 1, 0, 1; 1, 1, 0]; %! assert_equal (Mdl.Cost, [0, 2, 3; 1, 0, 1; 1, 1, 0]); ## Prior is fixed at construction and cannot be assigned afterwards. The ## refusal comes from the property attributes, so the message is core ## Octave's and is not pinned here. %!error ... %! load fisheriris; ... %! Mdl = fitcnet (meas, species, 'IterationLimit', 20); ... %! Mdl.Prior = [0.2, 0.3, 0.5]; ## A fitted model survives savemodel and loadmodel: the properties come ## back as they were and it predicts the same. %!test %! load fisheriris %! Mdl = fitcnet (meas, species, 'IterationLimit', 20); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2), 'ClassificationNeuralNetwork'); %! assert_equal (M2.NumObservations, Mdl.NumObservations); %! assert_equal (M2.PredictorNames, Mdl.PredictorNames); %! assert_equal (class (M2.ScoreTransform), class (Mdl.ScoreTransform)); %! assert_equal (predict (M2, meas(1:5,:)), predict (Mdl, meas(1:5,:))); ## BinEdges is an empty cell, which is what MATLAB reports for this ## learner as well: it fits the predictors as they are. %!test %! load fisheriris %! Mdl = fitcnet (meas, species, 'IterationLimit', 10); %! assert_equal (class (Mdl.BinEdges), 'cell'); %! assert_equal (Mdl.BinEdges, {}); ## The shared cost guard is in force here too, and the struct form is ## permuted into this model's class order. The battery is on ## ClassificationDiscriminant. %!test %! load fisheriris %! Mdl = fitcnet (meas, species); %! S = struct ('ClassNames', {{'virginica'; 'setosa'; 'versicolor'}}, ... %! 'ClassificationCosts', [0, 1, 2; 3, 0, 4; 5, 6, 0]); %! Mdl.Cost = S; %! assert_equal (Mdl.Cost, [0, 4, 3; 6, 0, 5; 1, 2, 0]); %!error ... %! load fisheriris %! Mdl = fitcnet (meas, species); %! Mdl.Cost = ones (3); ## HyperparameterOptimizationResults is declared for MATLAB compatibility and ## stays empty, this class running no search over its hyperparameters. %!test %! load fisheriris %! Mdl = fitcnet (meas, species, 'IterationLimit', 20); %! assert_equal (isempty (Mdl.HyperparameterOptimizationResults), true); ## ModelParameters records the fit as it was asked for, not the network that ## came out of it. The field list is MATLAB's where the quantity is the same, ## with OutputLayerActivation, Solver and LearningRate added, which MATLAB has ## no counterpart for. %!test %! load fisheriris %! Mdl = fitcnet (meas, species); %! assert_equal (fieldnames (Mdl.ModelParameters)', {'LayerSizes', ... %! 'Activations', 'OutputLayerActivation', 'LayerWeightsInitializers', ... %! 'Solver', 'LearningRate', 'IterationLimit', 'GradientTolerance', ... %! 'LossTolerance', 'StepTolerance', 'DisplayInfo', 'StandardizeData', ... %! 'Version', 'Method', 'Type'}); %!test %! load fisheriris %! MP = fitcnet (meas, species).ModelParameters; %! assert_equal (MP.LayerSizes, 10); %! assert_equal (MP.Activations, 'relu'); %! assert_equal (MP.OutputLayerActivation, 'softmax'); %! assert_equal (MP.Solver, 'LBFGS'); %! assert_equal (MP.IterationLimit, 1000); %! assert_equal (MP.GradientTolerance, 1e-6); %! assert_equal (MP.LossTolerance, 1e-6); %! assert_equal (MP.StepTolerance, 1e-6); %! assert_equal (MP.StandardizeData, false); %!test %! load fisheriris %! MP = fitcnet (meas, species).ModelParameters; %! assert_equal (MP.Version, 1); %! assert_equal (MP.Method, 'NeuralNetwork'); %! assert_equal (MP.Type, 'classification'); ## Every value tracks the argument that set it, and a per-layer Activations ## stays a cellstr rather than splitting the structure into an array. %!test %! load fisheriris %! MP = fitcnet (meas, species, 'LayerSizes', [5, 3], 'Activations', ... %! {'relu', 'tanh'}, 'Standardize', true, ... %! 'IterationLimit', 50).ModelParameters; %! assert_equal (size (MP), [1, 1]); %! assert_equal (MP.LayerSizes, [5, 3]); %! assert_equal (MP.Activations, {'relu', 'tanh'}); %! assert_equal (MP.StandardizeData, true); %! assert_equal (MP.IterationLimit, 50); ## ModelParameters reports the weight initializer each layer was built with, ## the output layer last. The engine picks it from the activation and it ## cannot be chosen, so the report is the only way to see it. %!test %! load fisheriris %! MP = fitcnet (meas, species).ModelParameters; %! assert_equal (MP.LayerWeightsInitializers, {'he', 'glorot'}); ## A rectifier takes He and a symmetric activation takes Glorot, so a network ## whose layers differ is built with both. %!test %! load fisheriris %! Mdl = fitcnet (meas, species, 'LayerSizes', [5, 3], ... %! 'Activations', {'relu', 'tanh'}); %! assert_equal (Mdl.ModelParameters.LayerWeightsInitializers, ... %! {'he', 'glorot', 'glorot'}); %!test %! load fisheriris %! MP = fitcnet (meas, species, 'Activations', 'sigmoid').ModelParameters; %! assert_equal (MP.LayerWeightsInitializers, {'glorot', 'glorot'}); ## Every documented score transform reaches the scores that are reported, and ## none of them moves the label: a transform reshapes what is reported, not ## what is decided. %!test %! load fisheriris %! Mdl = fitcnet (meas, species); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! T = {'identity', @(x) x; 'doublelogit', @(x) 1 ./ (1 + exp (-2 * x)); ... %! 'invlogit', @(x) log (x ./ (1 - x)); ... %! 'logit', @(x) 1 ./ (1 + exp (-x)); ... %! 'sign', @(x) sign (x); 'symmetric', @(x) 2 * x - 1; ... %! 'symmetriclogit', @(x) 2 ./ (1 + exp (-x)) - 1}; %! for i = 1:rows (T) %! Mdl.ScoreTransform = T{i,1}; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, T{i,2}(raw), 1e-12); %! assert_equal (l, label); %! endfor %! ## ismax marks the largest score of each observation, ties to the first. %! [~, k] = max (raw, [], 2); %! e = zeros (size (raw)); %! e(sub2ind (size (raw), (1:rows (raw))', k)) = 1; %! Mdl.ScoreTransform = 'ismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, e); %! Mdl.ScoreTransform = 'symmetricismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, 2 * e - 1); ## A function handle is taken as given and applied to the scores. %!test %! load fisheriris %! Mdl = fitcnet (meas, species); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! Mdl.ScoreTransform = @(x) x .^ 2; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, raw .^ 2, 1e-12); %! assert_equal (l, label); %!shared Xc, Dc, yc, Xq, Dq %! k = (0:119)'; %! c1 = mod (k, 3) + 1; %! x2 = sin (k); %! c3 = 10 * (mod (floor (k / 2), 2) + 1); %! Xc = [c1, x2, c3]; %! Dc = [c1 == 1, c1 == 2, c1 == 3, x2, c3 == 10, c3 == 20]; %! yr = 5 * (c1 == 2) + 0.5 * x2 - 3 * (c3 == 20) + 0.1 * cos (k); %! yc = yr > 1; %! Xq = [1, 0, 10; 2, 0.5, 20]; %! Dq = [1, 0, 0, 0, 1, 0; 0, 1, 0, 0.5, 0, 1]; %!test # MATLAB parity: a categorical predictor is dummy coded in its place %! rand ('seed', 1); %! randn ('seed', 1); %! Mdl = ClassificationNeuralNetwork (Xc, yc, 'CategoricalPredictors', ... %! [1, 3], 'LayerSizes', 4); %! rand ('seed', 1); %! randn ('seed', 1); %! H = ClassificationNeuralNetwork (Dc, yc, 'LayerSizes', 4); %! assert_equal (Mdl.ExpandedPredictorNames, {'x1 == 1', 'x1 == 2', ... %! 'x1 == 3', 'x2', 'x3 == 10', 'x3 == 20'}); %! assert_equal (Mdl.LayerWeights{1}, H.LayerWeights{1}, 1e-12); %! [~, s] = predict (Mdl, Xq); %! [~, sh] = predict (H, Dq); %! assert_equal (s, sh, 1e-12); %!test # MATLAB parity: a level the fit did not see has no score %! Mdl = ClassificationNeuralNetwork (Xc, yc, 'CategoricalPredictors', ... %! [1, 3], 'LayerSizes', 4); %! [label, s] = predict (Mdl, [4, 0, 10; 2.5, 0, 20]); %! assert_equal (all (isnan (s(:))), true); %! assert_equal (label, [false; false]); %!test # a row missing a predictor has no score, whatever the activation %! load fisheriris %! Mdl = ClassificationNeuralNetwork (meas(51:150,:), species(51:150), ... %! 'LayerSizes', 4); %! [~, s] = predict (Mdl, [NaN, 3, 5, 2; 6, 3, 5, 2]); %! assert_equal (isnan (s(:,1))', [true, false]); %!test # MATLAB parity: the coded columns are not standardized %! Mdl = ClassificationNeuralNetwork (Xc, yc, 'CategoricalPredictors', ... %! [1, 3], 'LayerSizes', 4, ... %! 'Standardize', true); %! assert_equal (Mdl.Mu([1:3, 5:6]), zeros (1, 5)); %! assert_equal (Mdl.Sigma([1:3, 5:6]), ones (1, 5)); %!test # the coding travels with saved models and cross-validation folds %! Mdl = ClassificationNeuralNetwork (Xc, yc, 'CategoricalPredictors', ... %! [1, 3], 'LayerSizes', 4); %! fname = [tempname(), '.mdl']; %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! [~, s2] = predict (M2, Xq); %! [~, s] = predict (Mdl, Xq); %! assert_equal (s2, s); %! CV = crossval (Mdl, 'KFold', 3); %! assert_equal (CV.Trained{1}.CategoricalPredictors, [1, 3]); %!error ... %! ClassificationNeuralNetwork (Xc, yc, 'CategoricalPredictors', 4) ## A table at prediction %!test # the class may be built from a table as the fitter builds it %! load fisheriris %! T = table (meas(:,1), meas(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = categorical (species); %! Mdl = ClassificationNeuralNetwork (T, 'Species'); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW'}); %! assert_equal (Mdl.ResponseName, 'Species'); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcnet (T, 'Species'); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'Species'), a); %! assert_equal (loss (Mdl, T), a); ## A table at edge %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcnet (T, 'Species'); %! a = edge (Mdl, X, y); %! assert_equal (edge (Mdl, T(:,1:2), y), a); %! assert_equal (edge (Mdl, T, 'Species'), a); %! assert_equal (edge (Mdl, T), a); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcnet (T, 'Species'); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## Observation weights of class single or double %!error ... %! loss (fitcnet ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2]), ... %! [1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', int8 ([1; 1; 1; 1])) ## Observation weights %!error ... %! ClassificationNeuralNetwork ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], ... %! 'Weights', int8 ([1; 1; 1; 1])) %!error ... %! ClassificationNeuralNetwork ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], ... %! 'Weights', true (4, 1)) %!error ... %! ClassificationNeuralNetwork ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], ... %! 'Weights', [1; 1]) %!error ... %! ClassificationNeuralNetwork ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], ... %! 'Weights', [1; -1; 1; 1]) %!test %! ## A weight of two fits as the observation given twice %! load fisheriris %! X = meas(51:130,:); %! Y = species(51:130); %! w = ones (80, 1); %! w(1:10) = 2; %! rand ('seed', 1); %! A = ClassificationNeuralNetwork (X, Y, 'LayerSizes', 1, 'Weights', w); %! rand ('seed', 1); %! B = ClassificationNeuralNetwork ([X; X(1:10,:)], [Y; Y(1:10)], ... %! 'LayerSizes', 1); %! assert_equal (nthargout (2, @predict, A, X), ... %! nthargout (2, @predict, B, X), 1e-10); %!test %! ## A prior weighs the fit as weights would, as R2024a weighs it %! load fisheriris %! X = meas(51:130,:); %! Y = species(51:130); %! w = [ones(50, 1) / 50; ones(30, 1) / 30]; %! rand ('seed', 1); %! A = ClassificationNeuralNetwork (X, Y, 'LayerSizes', 1, 'Prior', 'uniform'); %! rand ('seed', 1); %! B = ClassificationNeuralNetwork (X, Y, 'LayerSizes', 1, 'Weights', w); %! assert_equal (nthargout (2, @predict, A, X), ... %! nthargout (2, @predict, B, X), 1e-8); %!test %! ## W sums to one and keeps the class of single weights %! load fisheriris %! w = ones (150, 1); %! w([3, 60, 120]) = 2; %! Mdl = ClassificationNeuralNetwork (meas, species, 'LayerSizes', 3, ... %! 'Weights', single (w)); %! assert_equal (class (Mdl.W), 'single'); %! assert_equal (double (Mdl.W(1)), 1 / 153, 1e-8); %!test %! ## Rows of zero weight are left out, as R2024a leaves them %! load fisheriris %! w = ones (150, 1); %! w(5) = 0; %! Mdl = ClassificationNeuralNetwork (meas, species, 'LayerSizes', 3, ... %! 'Weights', w); %! assert_equal (Mdl.NumObservations, 149); %!test %! ## Standardization weighs the observations by W, as R2024a does %! load fisheriris %! w = 1 + (1:150)' / 7; %! Mdl = ClassificationNeuralNetwork (meas, species, 'LayerSizes', 3, ... %! 'Weights', w, 'Standardize', true); %! assert_equal (Mdl.Mu, [6.153769696969698, 2.965608080808081, ... %! 4.573050505050506, 1.55819797979798], 1e-14); %! Mdl = ClassificationNeuralNetwork (meas, species, 'LayerSizes', 3, ... %! 'Weights', w, 'Standardize', true, ... %! 'Prior', 'uniform'); %! assert_equal (Mdl.Mu, [5.833297045931007, 3.054926460541555, ... %! 3.749907727492633, 1.199335039802964], 1e-14); %!test %! ## A constant predictor is left unscaled by standardization %! X = [linspace(0, 1, 20)', ones(20, 1)]; %! Mdl = ClassificationNeuralNetwork (X, [ones(10, 1); 2 * ones(10, 1)], 'Standardize', true); %! assert_equal (Mdl.Sigma(2), 1); ## Test a class with prior 0, whose observations are removed before fitting, ## as MATLAB R2024a and R2026b remove them %!warning ... %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! Mdl = ClassificationNeuralNetwork (X, Y, 'Prior', [1, 0]); %! assert_equal (Mdl.NumObservations, 50); %! assert_equal (Mdl.RowsUsed, [true(50, 1); false(50, 1)]); %! assert_equal (Mdl.Prior, [1, 0]); %! assert_equal (predict (Mdl, meas([51, 110], 3:4)), ... %! {'versicolor'; 'versicolor'}); %!warning ... %! load fisheriris %! Mdl = ClassificationNeuralNetwork (meas, species, 'Prior', [0.5, 0.5, 0]); %! assert_equal (predict (Mdl, meas([1, 51, 101], :)), ... %! {'setosa'; 'versicolor'; 'versicolor'}); %!test %! ## A named class with prior 0 may be absent, having nothing to remove %! load fisheriris %! X = meas(51:100, 3:4); %! Y = species(51:100); %! C = {'versicolor', 'virginica'}; %! Mdl = ClassificationNeuralNetwork (X, Y, 'ClassNames', C, 'Prior', [1, 0]); %! assert_equal (Mdl.ClassNames, {'versicolor'; 'virginica'}); %! assert_equal (predict (Mdl, meas(51, 3:4)), {'versicolor'}); %!warning ... %! ## A class removed from the middle keeps a score of 0 %! load fisheriris %! Mdl = ClassificationNeuralNetwork (meas, species, 'Prior', [0.5, 0, 0.5]); %! [label, score] = predict (Mdl, meas([1, 51, 101], :)); %! assert_equal (any (strcmp (label, 'versicolor')), false); %! assert_equal (score(:,2), [0; 0; 0]); %!test %! load fisheriris %! Mdl = ClassificationNeuralNetwork (meas(51:150, 3:4), species(51:150), ... %! 'Cost', [0, 1; 0, 0]); %! assert_equal (Mdl.NumObservations, 100); %! assert_equal (Mdl.Prior, [0.5, 0.5]); %!warning ... %! load fisheriris %! Mdl = ClassificationNeuralNetwork (meas, species, 'Prior', [0.5, 0, 0.5]); %! fname = [tempname(), '.mat']; %! savemodel (compact (Mdl), fname); %! M2 = loadmodel (fname); %! delete (fname); %! [~, score] = predict (M2, meas([1, 51, 101], :)); %! assert_equal (score(:,2), [0; 0; 0]); statistics-release-2.1.2/inst/Machine_Learning/ClassificationPartitionedECOC.m000066400000000000000000000570241527311104300274370ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ClassificationPartitionedECOC ## -*- texinfo -*- ## @deftp {statistics} ClassificationPartitionedECOC ## ## A cross-validated multiclass model built from binary learners. ## ## Each fold holds out part of the data, fits an error correcting output codes ## model on the rest, and answers the part it held out, so every observation ## is classified by a model that never saw it. ## ## It comes from @code{crossval} on a @code{ClassificationECOC}, and from ## @code{fitcecoc} given any of @qcode{'KFold'}, @qcode{'Holdout'}, ## @qcode{'Leaveout'} or @qcode{'CVPartition'}. ## ## This is the one cross-validated class in the package that is not the ## general @code{ClassificationPartitionedModel}. It carries ## @code{CodingMatrix}, @code{BinaryLoss} and @code{BinaryY}, three things ## the general class has nowhere to put and without which a fold's scores ## cannot be decoded at all. ## ## @seealso{fitcecoc, ClassificationECOC, CompactClassificationECOC} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} BinaryY ## ## What each observation was to each binary learner, an @math{NxL} matrix ## of -1, 0 and +1. This property is read-only. ## ## @end deftp BinaryY = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} CodingMatrix ## ## The coding design every fold was fitted on, a @math{KxL} matrix. This ## property is read-only. ## ## @end deftp CodingMatrix = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} BinaryLoss ## ## The loss the binary scores of a fold are read with. This property is ## read-only. ## ## @end deftp BinaryLoss = 'hinge'; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} ClassNames ## ## The distinct class labels. This property is read-only. ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} Cost ## ## The cost of misclassification. This property is read-only. ## ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} Prior ## ## The prior probability of each class. This property is read-only. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} CrossValidatedModel ## ## The kind of model that was cross-validated, @qcode{'ECOC'} here. This ## property is read-only. ## ## @end deftp CrossValidatedModel = 'ECOC'; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} PredictorNames ## ## The names of the predictors. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} CategoricalPredictors ## ## The columns holding categorical predictors, empty when none is. This ## property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} ResponseName ## ## The name of the response variable. This property is read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} NumObservations ## ## The number of observations used. This property is read-only. ## ## @end deftp NumObservations = 0; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} X ## ## The predictor data the folds were fitted on. This property is ## read-only. ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} Y ## ## The class labels the folds were fitted on. This property is ## read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} W ## ## The observation weights. This property is read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} ModelParameters ## ## A structure of the options the folds were fitted with. This property ## is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} Trained ## ## The models the folds were fitted to ## ## A cell column with one @code{CompactClassificationECOC} per fold, the ## training data having no place in a model that only ever answers the ## rows it did not see. This property is read-only. ## ## @end deftp Trained = {}; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} KFold ## ## The number of folds. This property is read-only. ## ## @end deftp KFold = 0; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} Partition ## ## The @code{cvpartition} object that says which rows each fold held out. ## This property is read-only. ## ## @end deftp Partition = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} BinEdges ## ## The bin edges of the predictors, always empty here. This property is ## read-only. ## ## @end deftp BinEdges = {}; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {ClassificationPartitionedECOC} {property} ScoreTransform ## ## The transform applied to the assembled out-of-fold scores. The folds ## never carry it: it is applied once to what they return, which is what ## R2024a does. ## ## @end deftp ScoreTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) STfun = @(x) x; endproperties methods (Hidden) function this = set.ScoreTransform (this, val) name = 'ClassificationPartitionedECOC'; try [this.STfun, this.ScoreTransform] = parseScoreTransform (val, name); catch error (strcat ("ClassificationPartitionedECOC.subsasgn:", ... " 'ScoreTransform' must be a character vector or a", ... " 'function_handle' object.")); end_try_catch endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedECOC} {@var{obj} =} ClassificationPartitionedECOC (@var{Mdl}, @var{Partition}) ## ## Create a @code{ClassificationPartitionedECOC} object. ## ## @var{Mdl} is the @code{ClassificationECOC} to cross validate and ## @var{Partition} the @code{cvpartition} saying which rows each fold ## holds out. The documented way to reach this constructor is ## @code{crossval}. ## ## @end deftypefn function this = ClassificationPartitionedECOC (Mdl, Partition) if (nargin < 2) error (strcat ("ClassificationPartitionedECOC: too few input", ... " arguments.")); endif if (! strcmp (class (Mdl), 'ClassificationECOC')) error (strcat ("ClassificationPartitionedECOC: MDL must be a", ... " 'ClassificationECOC' object.")); endif if (! isa (Partition, 'cvpartition')) error (strcat ("ClassificationPartitionedECOC: PARTITION must be a", ... " 'cvpartition' object.")); endif this.ClassNames = Mdl.ClassNames; this.Cost = Mdl.Cost; this.Prior = Mdl.Prior; this.PredictorNames = Mdl.PredictorNames; this.CategoricalPredictors = Mdl.CategoricalPredictors; this.ResponseName = Mdl.ResponseName; this.NumObservations = Mdl.NumObservations; this.X = Mdl.X; this.Y = Mdl.Y; this.W = Mdl.W; this.BinaryY = Mdl.BinaryY; this.CodingMatrix = Mdl.CodingMatrix; this.BinaryLoss = Mdl.BinaryLoss; this.Partition = Partition; this.KFold = Partition.NumTestSets; this.ModelParameters = Mdl.ModelParameters; ## Each fold is fitted on the coding matrix the parent settled, not on ## one of its own: a random design would differ fold to fold and the ## out-of-fold scores could not be put side by side. fargs = {'Coding', Mdl.CodingMatrix, ... 'Learners', Mdl.ModelParameters.BinaryLearners, ... 'ClassNames', Mdl.ClassNames, 'Prior', Mdl.Prior, ... 'Cost', Mdl.Cost, 'BinaryLoss', Mdl.BinaryLoss, ... 'ResponseName', Mdl.ResponseName, ... 'PredictorNames', Mdl.PredictorNames}; ## A nearest neighbour learner takes 'all' and never a list of indices, ## so a model whose every predictor is categorical passes it that way. if (! isempty (Mdl.CategoricalPredictors)) if (numel (Mdl.CategoricalPredictors) == columns (Mdl.X)) fargs(end+1:end+2) = {'CategoricalPredictors', 'all'}; else fargs(end+1:end+2) = {'CategoricalPredictors', ... Mdl.CategoricalPredictors}; endif endif G = struct (); G.X = Mdl.X; G.Y = Mdl.Y; G.Weights = Mdl.W; Trained = foldModels ('ClassificationECOC', G, Partition, fargs); ## A fold answers rows it never saw, so it keeps no training data. ## R2024a stores a CompactClassificationECOC here too. for k = 1:numel (Trained) Trained{k} = compact (Trained{k}); endfor this.Trained = Trained; endfunction function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ('%s =\n', in_name); endif disp (this); endfunction function disp (this) fprintf ("\n ClassificationPartitionedECOC\n\n"); fprintf ("%+25s: '%s'\n", 'CrossValidatedModel', ... this.CrossValidatedModel); fprintf ("%+25s: %s\n", 'PredictorNames', ... classNameListing (this.PredictorNames)); fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); fprintf ("%+25s: %d\n", 'NumObservations', this.NumObservations); fprintf ("%+25s: %d\n", 'KFold', this.KFold); fprintf ("%+25s: %s\n", 'ClassNames', classNameListing (this.ClassNames)); fprintf ("%+25s: '%s'\n", 'ScoreTransform', this.ScoreTransform); fprintf ("\n"); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedECOC} {@var{label} =} kfoldPredict (@var{obj}) ## @deftypefnx {ClassificationPartitionedECOC} {[@var{label}, @var{NegLoss}] =} kfoldPredict (@var{obj}) ## ## Out-of-fold class of every observation. ## ## Each observation is classified by the fold that held it out, so the ## labels are out-of-sample. An observation no fold held out, which ## under a holdout partition is most of them, comes back missing rather ## than classified, and its @var{NegLoss} row comes back @qcode{NaN}. ## ## @seealso{ClassificationPartitionedECOC, ## CompactClassificationECOC.predict} ## @end deftypefn function [label, NegLoss] = kfoldPredict (this) n = this.NumObservations; K = classCount (this.ClassNames); label = missingLabels (this.Y, n); NegLoss = nan (n, K); for k = 1:this.KFold idx = test (this.Partition, k); if (! any (idx)) continue; endif [lab, NL] = predict (this.Trained{k}, this.X(idx,:)); label(idx,:) = lab; NegLoss(idx,:) = NL; endfor ## The folds never carry the transform, so it is applied once here. NegLoss = this.STfun (NegLoss); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedECOC} {@var{m} =} kfoldMargin (@var{obj}) ## ## Out-of-fold classification margin of every observation. ## ## @seealso{ClassificationPartitionedECOC.kfoldPredict, ## ClassificationPartitionedECOC.kfoldEdge} ## @end deftypefn function m = kfoldMargin (this) [~, NegLoss] = kfoldPredict (this); [gY, errmsg] = labelIndices (this.ClassNames, this.Y); if (! isempty (errmsg)) error ("ClassificationPartitionedECOC.kfoldMargin: %s", errmsg); endif m = marginsOf (NegLoss, gY, 1); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedECOC} {@var{e} =} kfoldEdge (@var{obj}) ## ## Out-of-fold classification edge, the weighted mean of the margins. ## ## @seealso{ClassificationPartitionedECOC.kfoldMargin} ## @end deftypefn function e = kfoldEdge (this) m = kfoldMargin (this); keep = ! isnan (m); w = double (this.W(:))(keep); e = sum (w .* m(keep)) / sum (w); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedECOC} {@var{L} =} kfoldLoss (@var{obj}) ## @deftypefnx {ClassificationPartitionedECOC} {@var{L} =} kfoldLoss (@dots{}, @var{name}, @var{value}) ## ## Out-of-fold classification loss. ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## ## @item @qcode{'LossFun'} @tab @tab @qcode{'classiferror'} (default), ## @qcode{'classifcost'}, @qcode{'mincost'}, @qcode{'binodeviance'}, ## @qcode{'exponential'}, @qcode{'hinge'}, @qcode{'logit'} or ## @qcode{'quadratic'}. ## @end multitable ## ## @seealso{ClassificationPartitionedECOC.kfoldPredict} ## @end deftypefn function L = kfoldLoss (this, varargin) if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationPartitionedECOC.kfoldLoss:", ... " name-value arguments must be in pairs.")); endif ## Parse optional paired arguments [LossFun, args] = parsePairedArguments ({'LossFun'}, {'classiferror'}, ... varargin(:)); ## Validate optional paired arguments if (! (ischar (LossFun) && isrow (LossFun))) error (strcat ("ClassificationPartitionedECOC.kfoldLoss:", ... " 'LossFun' must be a character vector.")); endif LossFun = tolower (LossFun); if (! isempty (args)) error (strcat ("ClassificationPartitionedECOC.kfoldLoss:", ... " invalid optional paired argument.")); endif [~, NegLoss] = kfoldPredict (this); [gY, errmsg] = labelIndices (this.ClassNames, this.Y); if (! isempty (errmsg)) error ("ClassificationPartitionedECOC.kfoldLoss: %s", errmsg); endif ## An observation no fold held out has no out-of-sample answer and is ## left out of the loss rather than counted as an error. keep = ! any (isnan (NegLoss), 2); L = classificationLoss (LossFun, NegLoss(keep,:), gY(keep), ... double (this.W(keep)), this.Cost); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedECOC} {@var{vals} =} kfoldfun (@var{obj}, @var{fun}) ## ## Apply a function to every fold. ## ## @var{fun} is called once per fold as ## @code{@var{fun} (@var{CMdl}, @var{Xtrain}, @var{Ytrain}, @var{Wtrain}, ## @var{Xtest}, @var{Ytest}, @var{Wtest})} and must return a numeric row ## of the same length every time. @var{vals} has one row per fold. ## ## @seealso{ClassificationPartitionedECOC.kfoldPredict} ## @end deftypefn function vals = kfoldfun (this, fun) if (nargin < 2) error (strcat ("ClassificationPartitionedECOC.kfoldfun: too few", ... " input arguments.")); endif if (! isa (fun, 'function_handle')) error (strcat ("ClassificationPartitionedECOC.kfoldfun: FUN must", ... " be a function handle.")); endif vals = []; for k = 1:this.KFold tr = training (this.Partition, k); te = test (this.Partition, k); v = fun (this.Trained{k}, this.X(tr,:), this.Y(tr,:), this.W(tr), ... this.X(te,:), this.Y(te,:), this.W(te)); if (! (isnumeric (v) && isrow (v))) error (strcat ("ClassificationPartitionedECOC.kfoldfun: FUN must", ... " return a numeric row vector.")); endif if (! isempty (vals) && numel (v) != columns (vals)) error (strcat ("ClassificationPartitionedECOC.kfoldfun: FUN must", ... " return the same number of values for every", ... " fold.")); endif vals = [vals; v]; endfor endfunction endmethods endclassdef ## Tests %!test # MATLAB parity: the property surface of a cross-validated model %! load fisheriris %! c = cvpartition ('CustomPartition', repmat ((1:5)', 30, 1)); %! CV = fitcecoc (meas, species, 'Learners', 'tree', 'CVPartition', c); %! assert_equal (class (CV), 'ClassificationPartitionedECOC'); %! assert_equal (numel (properties (CV)), 20); %! assert_equal (CV.CrossValidatedModel, 'ECOC'); %! assert_equal (CV.KFold, 5); %! assert_equal (numel (CV.Trained), 5); %! assert_equal (class (CV.Trained{1}), 'CompactClassificationECOC'); %!test # MATLAB parity: the out-of-fold answers of a discriminant code %! ## Measured on R2024a over a partition both engines are given outright. %! ## The learner is a discriminant and not a tree on purpose: a tree fitted %! ## on four fifths of this fixture has ties among its best splits, which %! ## the two engines break differently and which no test can pin. %! load fisheriris %! c = cvpartition ('CustomPartition', repmat ((1:5)', 30, 1)); %! CV = fitcecoc (meas, species, 'Learners', 'discriminant', 'CVPartition', c); %! assert_equal (kfoldLoss (CV), 0.0333333333333334, 1e-12); %! assert_equal (kfoldEdge (CV), 0.923697865527129, 1e-12); %! m = kfoldMargin (CV); %! assert_equal (m([1, 20, 51, 70, 101, 130])', ... %! [1, 1, 0.999820708981217, 0.999920781404916, ... %! 0.999999631930005, -0.0807626827878425], 1e-12); %!test # MATLAB parity: the labels a tree code gives out of fold %! load fisheriris %! c = cvpartition ('CustomPartition', repmat ((1:5)', 30, 1)); %! CV = fitcecoc (meas, species, 'Learners', 'tree', 'CVPartition', c); %! [label, NegLoss] = kfoldPredict (CV); %! assert_equal (size (NegLoss), [150, 3]); %! assert_equal (sum (! strcmp (label, species)), 11); %! assert_equal (kfoldLoss (CV), 11/150, 1e-12); %!test # every observation is answered by the fold that held it out %! load fisheriris %! c = cvpartition ('CustomPartition', repmat ((1:5)', 30, 1)); %! CV = fitcecoc (meas, species, 'Learners', 'discriminant', 'CVPartition', c); %! [label, NegLoss] = kfoldPredict (CV); %! assert_equal (any (cellfun (@isempty, label)), false); %! assert_equal (any (any (isnan (NegLoss))), false); %!test # a character matrix response gives one out-of-fold label per row %! load fisheriris %! c = cvpartition ('CustomPartition', repmat ((1:5)', 30, 1)); %! CV = fitcecoc (meas, char (species), 'Learners', 'tree', 'CVPartition', c); %! [label, NegLoss] = kfoldPredict (CV); %! assert_equal (size (NegLoss), [150, 3]); %! assert_equal (sum (! strcmp (cellstr (label), species)), 11); %!test # a holdout partition leaves the rows no fold tested unanswered %! load fisheriris %! CV = crossval (ClassificationECOC (meas, species, 'Learners', ... %! 'discriminant'), 'Holdout', 0.2); %! [label, NegLoss] = kfoldPredict (CV); %! assert_equal (sum (cellfun (@isempty, label)), 120); %! assert_equal (sum (all (isnan (NegLoss), 2)), 120); %!test # the margin is positive exactly where the out-of-fold label was right %! load fisheriris %! c = cvpartition ('CustomPartition', repmat ((1:5)', 30, 1)); %! CV = fitcecoc (meas, species, 'Learners', 'discriminant', 'CVPartition', c); %! assert_equal (kfoldMargin (CV) > 0, strcmp (kfoldPredict (CV), species)); %!test # kfoldfun is called once per fold and its rows are stacked %! load fisheriris %! c = cvpartition ('CustomPartition', repmat ((1:5)', 30, 1)); %! CV = fitcecoc (meas, species, 'Learners', 'discriminant', 'CVPartition', c); %! v = kfoldfun (CV, @(m, xt, yt, wt, xs, ys, ws) [rows(xt), rows(xs)]); %! assert_equal (v, [120, 30; 120, 30; 120, 30; 120, 30; 120, 30]); %!test # crossval and the fit route give the same partitioned class %! load fisheriris %! c = cvpartition ('CustomPartition', repmat ((1:5)', 30, 1)); %! a = fitcecoc (meas, species, 'Learners', 'discriminant', 'CVPartition', c); %! b = crossval (ClassificationECOC (meas, species, 'Learners', ... %! 'discriminant'), 'CVPartition', c); %! assert_equal (kfoldLoss (a), kfoldLoss (b), 1e-12); %!test # a categorical response gives categorical out-of-fold labels %! load fisheriris %! c = cvpartition ('CustomPartition', repmat ((1:5)', 30, 1)); %! a = fitcecoc (meas, species, 'Learners', 'tree', 'CVPartition', c); %! b = fitcecoc (meas, categorical (species), 'Learners', 'tree', ... %! 'CVPartition', c); %! label = kfoldPredict (b); %! assert_equal (class (label), 'categorical'); %! assert_equal (cellstr (label), kfoldPredict (a)); ## Test input validation %!error ... %! ClassificationPartitionedECOC (1) %!error ... %! ClassificationPartitionedECOC (1, cvpartition (10, 'KFold', 2)) %!error ... %! Mdl = ClassificationECOC (ones (4, 2), [1; 2; 1; 2]); ... %! ClassificationPartitionedECOC (Mdl, 1) %!error ... %! y = [1; 2; 1; 2; 1; 2; 1; 2]; ... %! CV = crossval (ClassificationECOC (ones (8, 2), y), 'KFold', 2); ... %! kfoldLoss (CV, 'LossFun') %!error ... %! y = [1; 2; 1; 2; 1; 2; 1; 2]; ... %! CV = crossval (ClassificationECOC (ones (8, 2), y), 'KFold', 2); ... %! kfoldLoss (CV, 'Bogus', 1) %!error ... %! y = [1; 2; 1; 2; 1; 2; 1; 2]; ... %! CV = crossval (ClassificationECOC (ones (8, 2), y), 'KFold', 2); ... %! kfoldfun (CV, 1) statistics-release-2.1.2/inst/Machine_Learning/ClassificationPartitionedEnsemble.m000066400000000000000000000754701527311104300304650ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ClassificationPartitionedEnsemble ## -*- texinfo -*- ## @deftp {statistics} ClassificationPartitionedEnsemble ## ## Cross-validated ensemble of decision trees for classification ## ## A @code{ClassificationPartitionedEnsemble} object holds one ensemble per ## fold of a partition, each fitted on the observations the fold keeps for ## training, and answers for every observation with the ensemble of the fold ## that held it out. ## ## Create one with @code{fitcensemble} given a cross-validation option, or ## with the @code{crossval} method of a @code{ClassificationEnsemble} or ## @code{ClassificationBaggedEnsemble}. ## ## @seealso{fitcensemble, ClassificationEnsemble.crossval, ## CompactClassificationEnsemble, cvpartition} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationPartitionedEnsemble} {property} Trainable ## ## The full ensemble of each fold ## ## A column cell array with one @code{ClassificationEnsemble} or ## @code{ClassificationBaggedEnsemble} per fold, which @code{resume} ## grows. This property is read-only. ## ## @end deftp Trainable = {}; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedEnsemble} {property} NumTrainedPerFold ## ## Number of learners in each fold ## ## A row vector. This property is read-only. ## ## @end deftp NumTrainedPerFold = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedEnsemble} {property} CrossValidatedModel ## ## Method of the cross-validated ensemble ## ## The @code{Method} of the ensemble, such as @qcode{'AdaBoostM1'} or ## @qcode{'Bag'}. This property is read-only. ## ## @end deftp CrossValidatedModel = ''; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedEnsemble} {property} PredictorNames ## ## Names of the predictors ## ## This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedEnsemble} {property} CategoricalPredictors ## ## Indices of categorical predictors ## ## Always empty. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedEnsemble} {property} ResponseName ## ## Name of the response variable ## ## This property is read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedEnsemble} {property} NumObservations ## ## Number of observations ## ## This property is read-only. ## ## @end deftp NumObservations = 0; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedEnsemble} {property} X ## ## Predictor data ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## This property is read-only. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedEnsemble} {property} Y ## ## Class labels ## ## This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedEnsemble} {property} W ## ## Observation weights ## ## The weights of the ensemble that was cross-validated. The losses and ## edges weigh the held-out observations by them. This property is ## read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedEnsemble} {property} ModelParameters ## ## Parameters of the cross-validation ## ## A structure with the fields @qcode{Type}, @qcode{Method}, ## @qcode{'PartitionedEnsemble'}, @qcode{LearnerTemplates}, the ## @code{ModelParameters} of the ensemble the folds were fitted as, and ## @qcode{NLearn}, the number of folds. This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedEnsemble} {property} Trained ## ## The compact ensemble of each fold ## ## A column cell array with one @code{CompactClassificationEnsemble} per ## fold. This property is read-only. ## ## @end deftp Trained = {}; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedEnsemble} {property} KFold ## ## Number of folds ## ## This property is read-only. ## ## @end deftp KFold = 0; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedEnsemble} {property} Partition ## ## The partition of the observations ## ## A @code{cvpartition} object. This property is read-only. ## ## @end deftp Partition = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedEnsemble} {property} BinEdges ## ## Bin edges of the predictors ## ## Always empty. This property is read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedEnsemble} {property} ClassNames ## ## Names of the classes ## ## This property is read-only. ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedEnsemble} {property} Cost ## ## Misclassification costs ## ## This property is read-only. ## ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedEnsemble} {property} Prior ## ## Prior probabilities of the classes ## ## This property is read-only. ## ## @end deftp Prior = []; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {ClassificationPartitionedEnsemble} {property} ScoreTransform ## ## Transform applied to the out-of-fold scores ## ## The folds carry none; this one is applied once to the scores they ## return. ## ## @end deftp ScoreTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) STfun = @(x) x; # the score transform as a function gY = []; # class index of each observation DefaultIndex = 1; # class given to an observation no fold held out endproperties methods (Hidden) function this = set.ScoreTransform (this, val) try [this.STfun, this.ScoreTransform] = parseScoreTransform (val, ... 'ClassificationPartitionedEnsemble'); catch error (strcat ("ClassificationPartitionedEnsemble.subsasgn:", ... " 'ScoreTransform' must be a character vector or a", ... " 'function_handle' object.")); end_try_catch endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedEnsemble} {@var{CVMdl} =} ClassificationPartitionedEnsemble (@var{Mdl}, @var{Partition}) ## ## Cross-validate a classification ensemble. ## ## @var{Mdl} is the ensemble to cross-validate and @var{Partition} a ## @code{cvpartition} of its observations. The documented ways in are ## @code{fitcensemble} and @code{crossval}. ## ## @end deftypefn function this = ClassificationPartitionedEnsemble (Mdl, Partition) if (nargin < 2) error (strcat ("ClassificationPartitionedEnsemble: too few input", ... " arguments.")); endif if (! isa (Mdl, 'ClassificationEnsemble')) error (strcat ("ClassificationPartitionedEnsemble: MDL must be a", ... " 'ClassificationEnsemble' object.")); endif if (! isa (Partition, 'cvpartition')) error (strcat ("ClassificationPartitionedEnsemble: PARTITION must", ... " be a 'cvpartition' object.")); endif if (numel (test (Partition, 1)) != Mdl.NumObservations) error (strcat ("ClassificationPartitionedEnsemble: PARTITION must", ... " partition the observations MDL was fitted on.")); endif this.CrossValidatedModel = Mdl.Method; this.PredictorNames = Mdl.PredictorNames; this.CategoricalPredictors = Mdl.CategoricalPredictors; this.ResponseName = Mdl.ResponseName; this.NumObservations = Mdl.NumObservations; this.X = Mdl.X; this.Y = Mdl.Y; this.W = Mdl.W; this.ClassNames = Mdl.ClassNames; this.Cost = Mdl.Cost; this.Prior = Mdl.Prior; this.Partition = Partition; this.KFold = Partition.NumTestSets; this.gY = Mdl.gY; this.DefaultIndex = Mdl.DefaultIndex; this.ModelParameters = struct ('Type', 'classification', ... 'Method', 'PartitionedEnsemble', ... 'LearnerTemplates', ... Mdl.ModelParameters, ... 'NLearn', this.KFold); if (any (strcmp (Mdl.ScoreTransform, {'doublelogit', 'invlogit', ... 'ismax', 'logit', 'none', 'identity', 'sign', ... 'symmetric', 'symmetricismax', 'symmetriclogit'}))) this.ScoreTransform = Mdl.ScoreTransform; else this.ScoreTransform = Mdl.STfun; endif ## Each fold is fitted with the options the parent settled, its classes, ## prior and cost included, and carries no score transform of its own: ## this object applies one, once, to what the folds return. mp = Mdl.ModelParameters; fargs = {'Method', Mdl.Method, 'NumLearningCycles', mp.NLearn, ... 'Learners', mp.LearnerTemplates, ... 'ClassNames', Mdl.ClassNames, 'Prior', Mdl.Prior, ... 'Cost', Mdl.Cost, 'PredictorNames', Mdl.PredictorNames, ... 'ResponseName', Mdl.ResponseName}; if (isa (Mdl, 'ClassificationBaggedEnsemble')) onoff = {'off', 'on'}; fargs(end+1:end+4) = {'FResample', Mdl.FResample, ... 'Replace', onoff{Mdl.Replace + 1}}; endif if (strcmp (Mdl.Method, 'Subspace')) fargs(end+1:end+2) = {'NPredToSample', Mdl.NPredToSample}; if (Mdl.AllCombinations) fargs{4} = 'AllPredictorCombinations'; endif elseif (any (strcmp (Mdl.Method, {'LPBoost', 'TotalBoost'}))) fargs(end+1:end+2) = {'MarginPrecision', Mdl.MarginPrecision}; elseif (! strcmp (Mdl.Method, 'Bag')) fargs(end+1:end+2) = {'LearnRate', mp.LearnRate}; if (strcmp (Mdl.Method, 'RUSBoost')) fargs(end+1:end+2) = {'RatioToSmallest', Mdl.RatioToSmallest}; endif endif ## Assigned field by field: struct () given a cell array of labels ## would build one structure per label. F = struct (); F.X = Mdl.X; F.Y = Mdl.Y; F.Weights = Mdl.W; if (! isempty (Mdl.CategoricalPredictors)) fargs(end+1:end+2) = {'CategoricalPredictors', ... Mdl.CategoricalPredictors}; endif this.Trainable = foldModels (class (Mdl), F, Partition, fargs); this = compactFolds (this); endfunction function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ('%s =\n', in_name); endif disp (this); endfunction function disp (this) fprintf ("\n ClassificationPartitionedEnsemble\n\n"); fprintf ("%+25s: '%s'\n", 'CrossValidatedModel', ... this.CrossValidatedModel); fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); fprintf ("%+25s: %d\n", 'NumObservations', this.NumObservations); fprintf ("%+25s: %d\n", 'KFold', this.KFold); fprintf ("%+25s: %s\n", 'NumTrainedPerFold', ... mat2str (this.NumTrainedPerFold)); fprintf ("%+25s: %s\n", 'ClassNames', classNameListing (this.ClassNames)); fprintf ("%+25s: '%s'\n", 'ScoreTransform', this.ScoreTransform); fprintf ("\n"); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedEnsemble} {@var{label} =} kfoldPredict (@var{obj}) ## @deftypefnx {ClassificationPartitionedEnsemble} {[@var{label}, @var{scores}] =} kfoldPredict (@var{obj}) ## ## Classify each observation with the fold that held it out. ## ## @var{scores} holds each observation's scores from the ensemble of the ## fold that held it out, after @code{ScoreTransform}, and @var{label} ## the class of highest score. An observation no fold held out, as under ## a holdout partition, has @code{NaN} scores and the class of greatest ## prior probability. ## ## @seealso{ClassificationPartitionedEnsemble, ## ClassificationPartitionedEnsemble.kfoldLoss} ## @end deftypefn function [label, scores] = kfoldPredict (this) scores = this.STfun (foldScores (this, 1:this.KFold)); [~, k] = max (scores, [], 2); k(any (isnan (scores), 2)) = this.DefaultIndex; label = labelsFromIndex (this.ClassNames, k); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedEnsemble} {@var{L} =} kfoldLoss (@var{obj}) ## @deftypefnx {ClassificationPartitionedEnsemble} {@var{L} =} kfoldLoss (@dots{}, @var{name}, @var{value}) ## ## Cross-validated classification loss. ## ## @var{L} is the loss of the out-of-fold scores, the held-out observations ## weighted by @code{W}. An observation no fold held out is left out. One ## held out but scored by no learner, for its missing values, stays, as in ## @code{CompactClassificationEnsemble.loss}. ## ## Name-Value arguments: ## ## @multitable @columnfractions 0.2 0.02 0.78 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Folds'} @tab @tab The folds to use, pooled. The default ## is all of them. ## @item @qcode{'LossFun'} @tab @tab A loss ## @code{CompactClassificationEnsemble.loss} accepts. The default is ## @qcode{'classiferror'}. ## @item @qcode{'Mode'} @tab @tab @qcode{'average'} (default) for one loss ## over the observations of every fold used, @qcode{'individual'} for a ## column with the loss of each fold, or @qcode{'cumulative'} for a column ## whose element @math{t} uses the first @math{t} learners of every fold. ## @end multitable ## ## @seealso{ClassificationPartitionedEnsemble, ## ClassificationPartitionedEnsemble.kfoldEdge} ## @end deftypefn function L = kfoldLoss (this, varargin) o = kfoldOpts (varargin, CLASSIFICATION_LOSSES, ... 'ClassificationPartitionedEnsemble', 'kfoldLoss', ... this.KFold, ENSEMBLE_MODES, true); if (isempty (o.LossFun)) o.LossFun = 'classiferror'; endif [sets, S] = foldSets_ (this, o); L = zeros (numel (sets), 1); for i = 1:numel (sets) L(i) = setLoss (this, S(:,:,min (i, size (S, 3))), sets{i}, o.LossFun); endfor endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedEnsemble} {@var{e} =} kfoldEdge (@var{obj}) ## @deftypefnx {ClassificationPartitionedEnsemble} {@var{e} =} kfoldEdge (@dots{}, @var{name}, @var{value}) ## ## Cross-validated classification edge. ## ## The weighted mean of the out-of-fold margins, weighted by @code{W}, ## @code{NaN} if a held-out observation has no score. @qcode{'Folds'} and ## @qcode{'Mode'} are taken as by @code{kfoldLoss}. ## ## @seealso{ClassificationPartitionedEnsemble, ## ClassificationPartitionedEnsemble.kfoldMargin} ## @end deftypefn function e = kfoldEdge (this, varargin) o = kfoldOpts (varargin, {}, 'ClassificationPartitionedEnsemble', ... 'kfoldEdge', this.KFold, ENSEMBLE_MODES); [sets, S] = foldSets_ (this, o); e = zeros (numel (sets), 1); for i = 1:numel (sets) rows = sets{i}; m = marginsOf (this.STfun (S(rows,:,min (i, size (S, 3)))), ... this.gY(rows), 1); w = double (this.W(rows)); e(i) = sum (w .* m) / sum (w); endfor endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedEnsemble} {@var{m} =} kfoldMargin (@var{obj}) ## ## Cross-validated classification margins. ## ## @var{m} holds each observation's margin under its out-of-fold scores, ## @code{NaN} for one no fold held out. ## ## @seealso{ClassificationPartitionedEnsemble, ## ClassificationPartitionedEnsemble.kfoldEdge} ## @end deftypefn function m = kfoldMargin (this) [~, scores] = kfoldPredict (this); m = marginsOf (scores, this.gY, 1); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedEnsemble} {@var{vals} =} kfoldfun (@var{obj}, @var{fun}) ## ## Apply a function to each fold. ## ## @var{fun} is called once per fold as ## @code{@var{fun} (CMP, Xtrain, Ytrain, Wtrain, Xtest, Ytest, Wtest)}, ## @var{CMP} being the fold's compact ensemble and the weights those of ## @code{W}, and must return a row. @var{vals} stacks the rows. ## ## @seealso{ClassificationPartitionedEnsemble} ## @end deftypefn function vals = kfoldfun (this, fun) if (nargin < 2) error (strcat ("ClassificationPartitionedEnsemble.kfoldfun: too", ... " few input arguments.")); endif if (! is_function_handle (fun)) error (strcat ("ClassificationPartitionedEnsemble.kfoldfun: FUN", ... " must be a function handle.")); endif vals = []; for k = 1:this.KFold tr = training (this.Partition, k); te = test (this.Partition, k); v = fun (this.Trained{k}, this.X(tr,:), this.Y(tr,:), this.W(tr), ... this.X(te,:), this.Y(te,:), this.W(te)); vals = [vals; v]; endfor endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedEnsemble} {@var{CVMdl} =} resume (@var{obj}, @var{NumLearningCycles}) ## @deftypefnx {ClassificationPartitionedEnsemble} {@var{CVMdl} =} resume (@dots{}, 'NPrint', @var{n}) ## ## Grow more learners in every fold. ## ## Each fold's ensemble is resumed, as @code{ClassificationEnsemble.resume} ## does, by @var{NumLearningCycles} learners. ## ## @seealso{ClassificationPartitionedEnsemble, ## ClassificationEnsemble.resume} ## @end deftypefn function this = resume (this, varargin) if (nargin < 2) error (strcat ("ClassificationPartitionedEnsemble.resume: too few", ... " input arguments.")); endif for k = 1:this.KFold this.Trainable{k} = resume (this.Trainable{k}, varargin{:}); endfor this = compactFolds (this); endfunction endmethods methods (Access = private) function this = compactFolds (this) this.Trained = cellfun (@(M) compact (M), this.Trainable, ... 'UniformOutput', false); this.NumTrainedPerFold = cellfun (@(M) M.NumTrained, this.Trainable)'; endfunction ## The untransformed scores of the rows the FOLDS held out, from the ## ensemble of the fold that held each out, NaN elsewhere. function S = foldScores (this, folds) K = classCount (this.ClassNames); S = NaN (this.NumObservations, K); for k = folds te = test (this.Partition, k); if (any (te) && this.Trained{k}.NumTrained > 0) [~, S(te,:)] = predict (this.Trained{k}, this.X(te,:)); endif endfor endfunction ## The row sets a kfold method reports over and the untransformed scores ## to read them with: one pooled set for 'average', one per fold for ## 'individual', and for 'cumulative' one pooled set per learner count, ## the scores then NxKxT. function [sets, S] = foldSets_ (this, o) n = this.NumObservations; if (! strcmp (o.Mode, 'cumulative')) sets = trainedRows (this, foldSets (this.Partition, o.Folds, ... o.Mode, n)); S = foldScores (this, o.Folds); return; endif pooled = trainedRows (this, foldSets (this.Partition, o.Folds, ... 'average', n)); K = classCount (this.ClassNames); T = max ([0, this.NumTrainedPerFold(o.Folds)]); S = NaN (n, K, T); for k = o.Folds te = test (this.Partition, k); Tk = this.NumTrainedPerFold(k); if (any (te) && Tk > 0) Sk = ensembleSteps (this.Trained{k}, this.X(te,:)); S(te,:,:) = cat (3, Sk, repmat (Sk(:,:,end), 1, 1, T - Tk)); endif endfor sets = repmat (pooled, T, 1); endfunction ## The sets without the rows of a fold that trained no learner. function sets = trainedRows (this, sets) none = false (this.NumObservations, 1); for k = find (this.NumTrainedPerFold(:)' == 0) none = none | test (this.Partition, k); endfor sets = cellfun (@(r) r & ! none, sets, 'UniformOutput', false); endfunction ## The weighted loss of the untransformed scores S over the rows IDX. function L = setLoss (this, S, idx, LossFun) S = this.STfun (S(idx,:)); g = this.gY(idx); w = double (this.W(idx)); if (! (sum (w) > 0)) L = NaN; return; endif w /= sum (w); if (is_function_handle (LossFun)) C = false (size (S)); C(sub2ind (size (S), (1:rows (S))', g)) = true; L = LossFun (C, S, w, this.Cost); else L = unscoredLoss (LossFun, S, g, w, this.Cost, this.DefaultIndex); endif endfunction endmethods endclassdef ## The modes of an ensemble's kfold methods, the first the default. function m = ENSEMBLE_MODES () m = {'average', 'individual', 'cumulative'}; endfunction ## The classification losses a kfold method takes by name. function l = CLASSIFICATION_LOSSES () l = {'binodeviance', 'classifcost', 'classiferror', 'exponential', ... 'hinge', 'logit', 'mincost', 'quadratic'}; endfunction ## Test output %!shared X2, Y2, S, c, CV %! load fisheriris %! X2 = meas(51:150,:); %! Y2 = species(51:150); %! S = templateTree ('MaxNumSplits', 1); %! c = cvpartition (Y2, 'KFold', 5); %! CV = fitcensemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 4, 'Learners', S, ... %! 'CVPartition', c); %!test # MATLAB parity: the properties of a cross-validated ensemble %! assert_equal (class (CV), 'ClassificationPartitionedEnsemble'); %! assert_equal (numel (properties (CV)), 19); %! assert_equal (CV.KFold, 5); %! assert_equal (CV.NumTrainedPerFold, [4, 4, 4, 4, 4]); %! assert_equal (CV.CrossValidatedModel, 'AdaBoostM1'); %! assert_equal (size (CV.Trained), [5, 1]); %! assert_equal (class (CV.Trained{1}), 'CompactClassificationEnsemble'); %! assert_equal (class (CV.Trainable{1}), 'ClassificationEnsemble'); %! assert_equal (CV.ModelParameters.Method, 'PartitionedEnsemble'); %! assert_equal (CV.ModelParameters.NLearn, 5); %!test # MATLAB parity: each row is predicted by the fold that held it out %! S0 = zeros (100, 2); %! for k = 1:5 %! te = test (c, k); %! [~, S0(te,:)] = predict (CV.Trained{k}, X2(te,:)); %! endfor %! [label, s] = kfoldPredict (CV); %! assert_equal (s, S0); %! [~, j] = max (S0, [], 2); %! assert_equal (label, CV.ClassNames(j)); %!test # MATLAB parity: the loss pooled over the folds and fold by fold %! [~, s] = kfoldPredict (CV); %! g = 1 + strcmp (Y2, 'virginica'); %! miss = s(sub2ind (size (s), (1:100)', g)) <= s(sub2ind (size (s), ... %! (1:100)', 3 - g)); %! assert_equal (kfoldLoss (CV), mean (miss), 1e-15); %! Li = kfoldLoss (CV, 'Mode', 'individual'); %! assert_equal (size (Li), [5, 1]); %! assert_equal (Li(2), mean (miss(test (c, 2))), 1e-15); %! assert_equal (kfoldLoss (CV, 'Folds', [1, 3]), ... %! mean (miss(test (c, 1) | test (c, 3))), 1e-15); %!test # MATLAB parity: the cumulative loss grows the learners of every fold %! g = 1 + strcmp (Y2, 'virginica'); %! Lc = kfoldLoss (CV, 'Mode', 'cumulative'); %! assert_equal (size (Lc), [4, 1]); %! s = zeros (100, 2); %! for k = 1:5 %! te = test (c, k); %! [~, s(te,:)] = predict (CV.Trained{k}, X2(te,:), 'Learners', 1:2); %! endfor %! miss = s(sub2ind (size (s), (1:100)', g)) <= s(sub2ind (size (s), ... %! (1:100)', 3 - g)); %! assert_equal (Lc(2), mean (miss), 1e-15); %! assert_equal (Lc(4), kfoldLoss (CV), 1e-15); %!test # MATLAB parity: the losses weigh the held-out rows by W %! w = [5 * ones(50, 1); ones(50, 1)]; %! M = fitcensemble (X2, Y2, 'Method', 'AdaBoostM1', 'NumLearningCycles', 4, ... %! 'Learners', S, 'CVPartition', c, 'Weights', w); %! [~, s] = kfoldPredict (M); %! g = 1 + strcmp (Y2, 'virginica'); %! miss = s(sub2ind (size (s), (1:100)', g)) <= s(sub2ind (size (s), ... %! (1:100)', 3 - g)); %! assert_equal (kfoldLoss (M), sum (M.W .* miss) / sum (M.W), 1e-15); %!test # MATLAB parity: the edge and margins of the out-of-fold scores %! [~, s] = kfoldPredict (CV); %! g = 1 + strcmp (Y2, 'virginica'); %! m = s(sub2ind (size (s), (1:100)', g)) - s(sub2ind (size (s), ... %! (1:100)', 3 - g)); %! assert_equal (kfoldMargin (CV), m, 1e-14); %! assert_equal (kfoldEdge (CV), mean (m), 1e-13); %! assert_equal (size (kfoldEdge (CV, 'Mode', 'cumulative')), [4, 1]); %! assert_equal (size (kfoldEdge (CV, 'Mode', 'individual')), [5, 1]); %!test # MATLAB parity: the score transform is applied once, by the parent %! M = fitcensemble (X2, Y2, 'Method', 'AdaBoostM1', 'NumLearningCycles', 4, ... %! 'Learners', S, 'CVPartition', c, ... %! 'ScoreTransform', 'doublelogit'); %! assert_equal (M.Trained{1}.ScoreTransform, 'none'); %! [~, s0] = kfoldPredict (CV); %! [~, s] = kfoldPredict (M); %! assert_equal (s, 1 ./ (1 + exp (-2 * s0)), 1e-15); %!test # MATLAB parity: kfoldfun gets each fold's data and weights %! f = kfoldfun (CV, @(C, Xtr, Ytr, Wtr, Xte, Yte, Wte) ... %! [rows(Xtr), sum(Wtr), rows(Xte), sum(Wte), ... %! isa(C, 'CompactClassificationEnsemble')]); %! assert_equal (f(1,:), [80, 0.8, 20, 0.2, 1], 1e-15); %! assert_equal (rows (f), 5); %!test # MATLAB parity: resuming grows every fold %! R = resume (CV, 2); %! assert_equal (R.NumTrainedPerFold, [6, 6, 6, 6, 6]); %! assert_equal (class (R.Trained{3}), 'CompactClassificationEnsemble'); %!test # MATLAB parity: a holdout leaves the training rows unscored %! load fisheriris %! rng (3); %! M = fitcensemble (meas, species, 'Method', 'Bag', 'NumLearningCycles', 3, ... %! 'Holdout', 0.3); %! assert_equal (class (M), 'ClassificationPartitionedEnsemble'); %! assert_equal (M.KFold, 1); %! [~, s] = kfoldPredict (M); %! assert_equal (sum (any (isnan (s), 2)), sum (training (M.Partition, 1))); ## Test input validation %!error ... %! ClassificationPartitionedEnsemble (1) %!error ... %! ClassificationPartitionedEnsemble (1, c) %!error ... %! ClassificationPartitionedEnsemble (CV.Trainable{1}, 1) %!error ... %! ClassificationPartitionedEnsemble (CV.Trainable{1}, c) %!error ... %! kfoldLoss (CV, 'Mode') %!error ... %! kfoldLoss (CV, 'Learners', 1) %!error ... %! kfoldLoss (CV, 'Mode', 'ensemble') %!error ... %! kfoldLoss (CV, 'Folds', 6) %!error ... %! kfoldLoss (CV, 'LossFun', 'mse') %!error ... %! kfoldEdge (CV, 'LossFun', 'hinge') %!error ... %! kfoldfun (CV) %!error ... %! kfoldfun (CV, 1) %!error ... %! resume (CV) %!error ... %! M = CV; %! M.ScoreTransform = 1; ## A held-out row no learner can score stays; one no fold held out is left out %!shared CVn, CVh %! X = [1 2 3; 2 1 1; NaN NaN 4; 3 2 1; 6 7 5; 7 6 9; 7 NaN NaN; 8 7 6; ... %! 1 7 5; 2 8 3; 2 6 5; 3 7 8]; %! Y = [1 1 1 1 2 2 2 2 3 3 3 3]'; %! M = fitcensemble (X, Y, 'Method', 'Subspace', 'Learners', 'discriminant', ... %! 'NPredToSample', 2, 'NumLearningCycles', ... %! 'AllPredictorCombinations'); %! CVn = crossval (M, 'CVPartition', cvpartition ('CustomPartition', ... %! [1 2 3 1 2 3 1 2 3 1 2 3]')); %! CVh = crossval (M, 'CVPartition', cvpartition ('CustomPartition', ... %! logical ([0 1 1 0 0 1 1 0 0 0 1 0]'))); %!assert_equal (kfoldLoss (CVn), 1 / 12, 1e-14) %!assert_equal (kfoldEdge (CVn), NaN) %!assert_equal (kfoldLoss (CVn, 'LossFun', 'hinge'), NaN) %!assert_equal (kfoldLoss (CVh), 0.2, 1e-14) %!assert_equal (kfoldEdge (CVh), NaN) %!assert_equal (kfoldLoss (CVh, 'LossFun', @(C, S, W, Cost) rows (S)), 5) statistics-release-2.1.2/inst/Machine_Learning/ClassificationPartitionedKernel.m000066400000000000000000001003341527311104300301370ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ClassificationPartitionedKernel ## -*- texinfo -*- ## @deftp {statistics} ClassificationPartitionedKernel ## ## Cross-validated Gaussian kernel binary classifier. ## ## A @qcode{ClassificationPartitionedKernel} object holds one ## @qcode{ClassificationKernel} per fold of a partition, each fitted to the ## observations the fold trains on. Every @code{kfold} method predicts each ## observation with the fold that held it @emph{out}, so the estimate it ## returns is an out-of-sample one. ## ## A @qcode{ClassificationKernel} stores no copy of its training data and so ## has no resubstitution methods and no @code{compact} form. This class is ## what takes their place: cross-validation is the way a linear model is ## asked how it would do on data it has not seen. ## ## When the fold models carry a whole regularization path, every method ## returns one column per strength, in the order of the @qcode{'Lambda'} ## that was asked for. ## ## Create one with @code{fitclinear} and a cross-validation option, or ## directly. ## ## @seealso{fitclinear, ClassificationKernel, ClassificationPartitionedKernel} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationPartitionedKernel} {property} ClassNames ## ## Names of the two classes ## ## A column of the same type as the response, shared by every fold. ## This property is read-only. ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedKernel} {property} Cost ## ## Cost of misclassifying an observation ## ## A square numeric matrix with one row and one column per class. It is ## handed to every fold rather than re-derived by each. This property is ## read-only. ## ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedKernel} {property} Prior ## ## Prior probability of each class ## ## A numeric row vector summing to one, in the order of ## @qcode{ClassNames}. Like the cost it is the parent's and is handed to ## every fold, so a fold of an unbalanced problem does not quietly adopt ## a prior of its own. This property is read-only. ## ## @end deftp Prior = []; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {ClassificationPartitionedKernel} {property} ScoreTransform ## ## Transformation applied to the predicted scores ## ## A character vector naming a transformation, or the text of the ## function handle that was supplied, which may be assigned after the ## model is built. It is applied once to the assembled scores and is ## not handed to the folds. A transform the learner implies, as ## @qcode{'logistic'} implies @qcode{'logit'}, does stay with the folds, ## and this one is then applied on top of it. ## ## @end deftp ScoreTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationPartitionedKernel} {property} CrossValidatedModel ## ## Name of the model that was cross-validated ## ## Always @qcode{'Linear'}, the short name MATLAB uses. This property is ## read-only. ## ## @end deftp CrossValidatedModel = 'Kernel'; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedKernel} {property} NumObservations ## ## Number of observations the partition covers ## ## A positive integer scalar, counting the rows that survived the removal ## of missing values. This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedKernel} {property} Y ## ## Response of the retained observations ## ## In the type it was supplied in. This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedKernel} {property} W ## ## Observation weights ## ## An @math{Nx1} numeric vector summing to one, normalized within each ## class to that class's cost-adjusted prior. This property is ## read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedKernel} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedKernel} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A row vector of column indices, empty when every predictor is ## numeric. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedKernel} {property} ResponseName ## ## Name of the response ## ## A character vector, defaulting to @qcode{'Y'}. This property is ## read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedKernel} {property} Trained ## ## The models fitted to the folds ## ## A cell column with one @qcode{ClassificationKernel} per fold, each ## fitted to the observations its fold trains on. This property is ## read-only. ## ## @end deftp Trained = {}; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedKernel} {property} KFold ## ## Number of folds ## ## A positive integer scalar. A holdout partition has one fold and a ## leave-one-out partition has as many as there are observations. This ## property is read-only. ## ## @end deftp KFold = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedKernel} {property} Partition ## ## The partition itself ## ## A @code{cvpartition} object over the retained observations. This ## property is read-only. ## ## @end deftp Partition = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedKernel} {property} ModelParameters ## ## What was cross-validated, and how ## ## A structure holding the parameters the folds were fitted with, carried ## through from the learner that was cross validated, beside ## @qcode{NLearn}, the number of folds, and the @qcode{Version}, ## @qcode{Method} and @qcode{Type} tags of this class, with ## @qcode{LearnerTemplates} naming the backing. The ## learner's own tags are replaced rather than kept, so a cross-validated ## SVM reports @qcode{Method} as @qcode{'PartitionedKernel'} and not ## @qcode{'SVM'}. ## ## @strong{Deviation from MATLAB.} MATLAB reports the parameter record of ## the cross-validation @emph{ensemble} here rather than of the learner, ## so it says nothing at all about how the folds were fitted: of its ## eighteen fields only the fold count, its partitioner and a fit template ## carry anything, and the rest are boosting settings left inert. Nor can ## the parameters be reached through the folds, a compact model carrying ## none in MATLAB. This class reports the fit instead, which is strictly ## more than MATLAB offers, and everything MATLAB's record does carry is ## published here as the @qcode{KFold}, @qcode{Partition}, @qcode{X}, ## @qcode{Y}, @qcode{W} and @qcode{CrossValidatedModel} properties. ## ## This property is read-only. ## ## @end deftp ModelParameters = []; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ## The callable behind ScoreTransform. STfun = @(s) s; ## The predictors of the retained observations. MATLAB does not report ## them and neither do we, but the kfold methods have to predict from ## something and the fold models hold no data of their own. X_ = []; ## Number of regularization strengths the fold models carry, so that ## every method knows how many columns to return without asking one. NumLambda_ = 1; endproperties methods (Access = public) ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedKernel} {@var{obj} =} ClassificationPartitionedKernel (@var{X}, @var{Y}) ## @deftypefnx {ClassificationPartitionedKernel} {@var{obj} =} ClassificationPartitionedKernel (@dots{}, @var{name}, @var{value}) ## ## Cross-validate a linear binary classifier. ## ## @code{@var{obj} = ClassificationPartitionedKernel (@var{X}, @var{Y})} ## partitions the data into ten stratified folds and fits a ## @qcode{ClassificationKernel} to each. ## ## @code{@var{obj} = ClassificationPartitionedKernel (@dots{}, ## @var{name}, @var{value})} takes one of @qcode{'KFold'}, ## @qcode{'Holdout'}, @qcode{'Leaveout'} and @qcode{'CVPartition'} to say ## how to partition, and any option @code{ClassificationKernel} takes to ## say how to fit. @qcode{'CrossVal'} is accepted and has no effect ## here, this class being cross-validated by construction. ## ## The classes, the prior and the cost are resolved once over the whole ## data and handed to every fold. Anything left as @qcode{'auto'} is ## not: each fold resolves @qcode{'Lambda'} and @qcode{'KernelScale'} ## against its own training rows, so ten folds of a hundred ## observations each get a @qcode{Lambda} of one ninetieth rather than ## one hundredth. Both are MATLAB's behaviour, measured. ## ## @seealso{fitclinear, ClassificationKernel} ## @end deftypefn function this = ClassificationPartitionedKernel (X, Y, varargin) if (nargin < 2) error (strcat ("ClassificationPartitionedKernel: too few input", ... " arguments.")); endif if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationPartitionedKernel: optional", ... " arguments must be given in Name-Value pairs.")); endif ## A table names its own predictors and says which hold levels. This ## class answers over its folds rather than over new data, so the ## levels are not kept: there is no later prediction to code. if (istable (X)) [X, Y, varargin, ~, errmsg] = tableFrame (X, Y, varargin); if (! isempty (errmsg)) error ("ClassificationPartitionedKernel: %s", errmsg); endif endif ## Split the argument list three ways: what says how to partition, ## what the parent owns, and what each fold is fitted with. [P, args] = partitionedArgs (varargin, ... 'ClassificationPartitionedKernel'); F = classFrame (X, Y, P.ClassNames, P.Prior, P.Cost, P.Weights, ... 'ClassificationPartitionedKernel'); [part, args] = cvPartitionOf (args, F.Y, F.n, ... 'ClassificationPartitionedKernel'); ## A transform asked for by name belongs to the parent, which applies ## it once to the assembled scores, and the folds are not given it. ## Measured on R2024a, where a cross-validated model fitted with a ## ScoreTransform reports it and leaves every Trained@{k@} at 'none'. ## A transform the learner implies is a different thing: a logistic ## fold reports posteriors and carries 'logit' of its own, which ## R2024a also does, and the parent's transform composes on top of it. fargs = [args, {'ClassNames', F.ClassNames, 'Prior', F.Prior, ... 'Cost', F.Cost}]; ## Built field by field rather than with one struct call: a cell ## array response passed to struct () makes a struct array of that ## size instead of a scalar struct holding the cell. G = struct (); G.X = F.X; G.Y = F.Y; G.Weights = F.Weights; this.Trained = foldModels ('ClassificationKernel', G, part, fargs); this.ClassNames = F.ClassNames; this.Cost = F.Cost; this.Prior = F.Prior; this.NumObservations = F.n; this.Y = F.Y; this.W = F.W; this.X_ = F.X; this.KFold = part.NumTestSets; this.Partition = part; this.NumLambda_ = numel (this.Trained{1}.Lambda); this.PredictorNames = this.Trained{1}.PredictorNames; this.CategoricalPredictors = this.Trained{1}.CategoricalPredictors; this.ResponseName = this.Trained{1}.ResponseName; if (isempty (P.ScoreTransform)) P.ScoreTransform = 'none'; endif [this.STfun, this.ScoreTransform] = ... parseScoreTransform (P.ScoreTransform, ... 'ClassificationPartitionedKernel'); ## The learner's parameters, under this class's own tags. MATLAB ## reports an EnsembleParams here instead and so says nothing about ## the fit; see the ModelParameters property for the deviation. this.ModelParameters = partitionedModelParams (this.Trained{1}, ... this.KFold, 'PartitionedKernel', ... 'classification', 'Kernel'); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedKernel} {@var{labels} =} kfoldPredict (@var{obj}) ## @deftypefnx {ClassificationPartitionedKernel} {[@var{labels}, @var{scores}] =} kfoldPredict (@var{obj}) ## ## Out-of-fold class of every observation. ## ## Each observation is classified by the fold that held it out, so the ## labels are out-of-sample. An observation that no fold held out, which ## under a holdout partition is most of them, comes back missing rather ## than classified, and its scores come back @qcode{NaN}. ## ## @end deftypefn function [labels, scores] = kfoldPredict (this) [labels, raw] = kfoldScores (this.Trained, this.Partition, this.X_, ... this.Y, this.ClassNames, ... this.NumLambda_); if (nargout > 1) scores = transformScores (this, raw); endif endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedKernel} {@var{m} =} kfoldMargin (@var{obj}) ## ## Out-of-fold classification margin of every observation. ## ## The score the out-of-fold model gives the true class, less the score ## it gives the other one. An observation no fold held out comes back ## @qcode{NaN}. ## ## @end deftypefn function m = kfoldMargin (this) [~, raw] = kfoldScores (this.Trained, this.Partition, this.X_, ... this.Y, this.ClassNames, this.NumLambda_); s = transformScores (this, raw); [gY, errmsg] = labelIndices (this.ClassNames, this.Y); if (! isempty (errmsg)) error ("ClassificationPartitionedKernel.kfoldMargin: %s", errmsg); endif m = marginsOf (s, gY, this.NumLambda_); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedKernel} {@var{e} =} kfoldEdge (@var{obj}) ## @deftypefnx {ClassificationPartitionedKernel} {@var{e} =} kfoldEdge (@dots{}, @var{name}, @var{value}) ## ## Weighted mean of the out-of-fold classification margins. ## ## @code{@var{e} = kfoldEdge (@dots{}, @var{name}, @var{value})} takes ## @qcode{'Folds'}, a subset of the folds to average over, and ## @qcode{'Mode'}, either @qcode{'average'}, the default, or ## @qcode{'individual'}, which returns one row per fold instead. ## ## @end deftypefn function e = kfoldEdge (this, varargin) O = kfoldOpts (varargin, {}, 'ClassificationPartitionedKernel', ... 'kfoldEdge', this.KFold); [~, raw] = kfoldScores (this.Trained, this.Partition, this.X_, ... this.Y, this.ClassNames, this.NumLambda_); s = transformScores (this, raw); [gY, errmsg] = labelIndices (this.ClassNames, this.Y); if (! isempty (errmsg)) error ("ClassificationPartitionedKernel.kfoldEdge: %s", errmsg); endif m = marginsOf (s, gY, this.NumLambda_); sets = foldSets (this.Partition, O.Folds, O.Mode, ... this.NumObservations); e = zeros (numel (sets), this.NumLambda_); for i = 1:numel (sets) idx = sets{i}; w = this.W(idx); w = w / sum (w); e(i,:) = sum (w .* m(idx,:), 1); endfor endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedKernel} {@var{l} =} kfoldLoss (@var{obj}) ## @deftypefnx {ClassificationPartitionedKernel} {@var{l} =} kfoldLoss (@dots{}, @var{name}, @var{value}) ## ## Out-of-fold classification loss. ## ## @code{@var{l} = kfoldLoss (@var{obj})} returns the out-of-fold ## misclassification rate. ## ## @code{@var{l} = kfoldLoss (@dots{}, @var{name}, @var{value})} takes ## @qcode{'LossFun'}, one of @qcode{'binodeviance'}, ## @qcode{'classifcost'}, @qcode{'classiferror'}, @qcode{'exponential'}, ## @qcode{'hinge'}, @qcode{'logit'}, @qcode{'mincost'} and ## @qcode{'quadratic'}; @qcode{'Folds'}; and @qcode{'Mode'}. ## ## @end deftypefn function l = kfoldLoss (this, varargin) valid = {'binodeviance', 'classifcost', 'classiferror', ... 'exponential', 'hinge', 'logit', 'mincost', 'quadratic'}; O = kfoldOpts (varargin, valid, 'ClassificationPartitionedKernel', ... 'kfoldLoss', this.KFold); if (isempty (O.LossFun)) O.LossFun = 'classiferror'; endif [~, raw] = kfoldScores (this.Trained, this.Partition, this.X_, ... this.Y, this.ClassNames, this.NumLambda_); s = transformScores (this, raw); [gY, errmsg] = labelIndices (this.ClassNames, this.Y); if (! isempty (errmsg)) error ("ClassificationPartitionedKernel.kfoldLoss: %s", errmsg); endif sets = foldSets (this.Partition, O.Folds, O.Mode, ... this.NumObservations); l = zeros (numel (sets), this.NumLambda_); for i = 1:numel (sets) idx = sets{i}; w = this.W(idx); w = w / sum (w); for k = 1:this.NumLambda_ if (this.NumLambda_ == 1) sk = s(idx,:); else sk = s(idx,:,k); endif l(i,k) = classificationLoss (O.LossFun, sk, gY(idx), w, this.Cost); endfor endfor endfunction endmethods methods (Access = public, Hidden) function this = set.ScoreTransform (this, val) [f, nm] = parseScoreTransform (val, 'ClassificationPartitionedKernel'); this.ScoreTransform = nm; this.STfun = f; endfunction function display (this) in_name = inputname (1); if (! isempty (in_name)) printf ('%s =\n', in_name); endif disp (this); endfunction function disp (this) printf ("\n ClassificationPartitionedKernel\n\n"); printf ("%+25s: '%s'\n", 'CrossValidatedModel', ... this.CrossValidatedModel); printf ("%+25s: %s\n", 'ClassNames', ... classNameListing (this.ClassNames)); printf ("%+25s: '%s'\n", 'ScoreTransform', this.ScoreTransform); printf ("%+25s: %d\n", 'NumObservations', this.NumObservations); printf ("%+25s: %d\n", 'KFold', this.KFold); printf ("\n"); endfunction endmethods methods (Access = private) ## Apply the parent's score transform to the assembled scores, whatever ## shape they came back in. function s = transformScores (this, raw) if (this.NumLambda_ == 1) s = this.STfun (raw); else s = raw; for k = 1:this.NumLambda_ s(:,:,k) = this.STfun (raw(:,:,k)); endfor endif endfunction endmethods endclassdef %!demo %! ## Cross-validate a Gaussian kernel classifier on the two overlapping %! ## iris species and read the out-of-sample error rate. %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! CVMdl = ClassificationPartitionedKernel (X, Y, 'KFold', 5) %! outOfSample = kfoldLoss (CVMdl) %!test %! ## The model reports the surface MATLAB reports %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! CVMdl = ClassificationPartitionedKernel (X, Y, 'KFold', 5); %! assert_equal (class (CVMdl), 'ClassificationPartitionedKernel'); %! assert_equal (CVMdl.CrossValidatedModel, 'Kernel'); %! assert_equal (CVMdl.KFold, 5); %! assert_equal (CVMdl.NumObservations, 100); %! assert_equal (CVMdl.ClassNames, {'versicolor'; 'virginica'}); %! assert_equal (CVMdl.Prior, [0.5, 0.5]); %! assert_equal (class (CVMdl.Trained{1}), 'ClassificationKernel'); %! assert_equal (CVMdl.ModelParameters.Method, 'PartitionedKernel'); %! assert_equal (CVMdl.ModelParameters.LearnerTemplates, 'Kernel'); %! assert_equal (CVMdl.ModelParameters.NLearn, 5); %!test %! ## The properties are the ones MATLAB lists, in its order %! load fisheriris %! CVMdl = ClassificationPartitionedKernel (meas(51:end,:), species(51:end)); %! assert_equal (sort (properties (CVMdl)), ... %! sort ({'ClassNames'; 'Cost'; 'Prior'; 'ScoreTransform'; ... %! 'CrossValidatedModel'; 'NumObservations'; 'Y'; 'W'; ... %! 'PredictorNames'; 'CategoricalPredictors'; ... %! 'ResponseName'; 'Trained'; 'KFold'; 'Partition'; ... %! 'ModelParameters'})); %!test %! ## Each fold resolves 'Lambda' against its own training rows, which is %! ## one eightieth of five folds over a hundred observations. R2024a's %! ## number. %! load fisheriris %! CVMdl = ClassificationPartitionedKernel (meas(51:end,:), ... %! species(51:end), 'KFold', 5); %! assert_equal (CVMdl.Trained{1}.Lambda, 1 / 80, 1e-15); %! assert_equal (CVMdl.Trained{1}.NumExpansionDimensions, 128); %! assert_equal (CVMdl.Trained{1}.KernelScale, 1); %!test %! ## Every fold draws its own basis, so two folds hold different %! ## expansions of the same kernel %! load fisheriris %! CVMdl = ClassificationPartitionedKernel (meas(51:end,:), ... %! species(51:end), 'KFold', 5); %! X = meas(51:53,:); %! [~, s1] = predict (CVMdl.Trained{1}, X); %! [~, s2] = predict (CVMdl.Trained{2}, X); %! assert_equal (isequal (s1, s2), false); %!test %! ## kfoldPredict classifies each observation with the fold that held it %! ## out, and doing the same partition by hand agrees %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! part = cvpartition (Y, 'KFold', 4); %! CVMdl = ClassificationPartitionedKernel (X, Y, 'CVPartition', part); %! label = kfoldPredict (CVMdl); %! byhand = cell (100, 1); %! for k = 1:4 %! te = test (part, k); %! byhand(te) = predict (CVMdl.Trained{k}, X(te,:)); %! endfor %! assert_equal (label, byhand); %!test %! ## The fit separates the two species out of sample %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! CVMdl = ClassificationPartitionedKernel (X, Y, 'KFold', 5); %! assert_equal (kfoldLoss (CVMdl) < 0.2, true); %! assert_equal (kfoldEdge (CVMdl) > 0, true); %! assert_equal (numel (kfoldMargin (CVMdl)), 100); %!test %! ## 'Mode', 'individual' gives one row per fold, and averaging pools the %! ## observations rather than the fold values %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! CVMdl = ClassificationPartitionedKernel (X, Y, 'KFold', 5); %! assert_equal (size (kfoldLoss (CVMdl, 'Mode', 'individual')), [5, 1]); %! label = kfoldPredict (CVMdl); %! assert_equal (kfoldLoss (CVMdl), mean (! strcmp (label, Y)), 1e-12); %!test %! ## A logistic fit reports posteriors, and the transform stays with the %! ## fold models as it does in MATLAB %! load fisheriris %! CVMdl = ClassificationPartitionedKernel (meas(51:end,:), ... %! species(51:end), 'KFold', 5, ... %! 'Learner', 'logistic'); %! assert_equal (CVMdl.ScoreTransform, 'none'); %! assert_equal (CVMdl.Trained{1}.ScoreTransform, 'logit'); %! [~, score] = kfoldPredict (CVMdl); %! assert_equal (sum (score, 2), ones (100, 1), 1e-12); %!test %! ## An observation that no fold held out is not classified %! load fisheriris %! CVMdl = ClassificationPartitionedKernel (meas(51:end,:), ... %! species(51:end), 'Holdout', 0.3); %! assert_equal (CVMdl.KFold, 1); %! [label, score] = kfoldPredict (CVMdl); %! assert_equal (sum (cellfun (@isempty, label)), 70); %! assert_equal (sum (isnan (score(:,1))), 70); %!test %! ## A character matrix response carries through cross-validation %! load fisheriris %! X = meas(51:end,:); %! Yc = species(51:end); %! part = cvpartition (Yc, 'KFold', 4); %! CVm = ClassificationPartitionedKernel (X, char (Yc), 'CVPartition', part); %! assert_equal (size (CVm.ClassNames), [2, 10]); %! assert_equal (cellstr (CVm.ClassNames), {'versicolor'; 'virginica'}); %! assert_equal (size (kfoldPredict (CVm)), [100, 10]); %! assert_equal (isfinite (kfoldLoss (CVm)), true); %! assert_equal (isfinite (kfoldEdge (CVm)), true); %!test %! ## A transform asked for by name goes to the parent and not to the folds, %! ## and is applied once to the assembled scores. R2024a's arrangement. %! ## The baseline is read from the same object with the transform switched %! ## off, a second fit being no baseline at all here: the random feature %! ## expansion differs between two fits of the same data. %! load fisheriris %! CVMdl = ClassificationPartitionedKernel (meas(51:end,:), species(51:end), ... %! 'KFold', 5, ... %! 'ScoreTransform', 'doublelogit'); %! assert_equal (CVMdl.ScoreTransform, 'doublelogit'); %! assert_equal (CVMdl.Trained{1}.ScoreTransform, 'none'); %! [~, s1] = kfoldPredict (CVMdl); %! CVMdl.ScoreTransform = 'none'; %! [~, s0] = kfoldPredict (CVMdl); %! assert_equal (s1, 1 ./ (1 + exp (-2 * s0)), 1e-12); %!test %! ## It can be assigned after the model is built, and reaches kfoldPredict %! ## without being carried into the folds %! load fisheriris %! CVMdl = ClassificationPartitionedKernel (meas(51:end,:), species(51:end), ... %! 'KFold', 5); %! [~, s0] = kfoldPredict (CVMdl); %! CVMdl.ScoreTransform = 'doublelogit'; %! [~, s1] = kfoldPredict (CVMdl); %! assert_equal (CVMdl.Trained{1}.ScoreTransform, 'none'); %! assert_equal (s1, 1 ./ (1 + exp (-2 * s0)), 1e-12); %!test %! ## A transform the learner implies stays with the folds, and an assigned %! ## one is applied on top of it rather than replacing it. Measured on %! ## R2024a, where the folds keep 'logit' and the parent's transform %! ## composes. %! load fisheriris %! CVMdl = ClassificationPartitionedKernel (meas(51:end,:), species(51:end), ... %! 'KFold', 5, ... %! 'Learner', 'logistic'); %! [~, s0] = kfoldPredict (CVMdl); %! CVMdl.ScoreTransform = 'doublelogit'; %! [~, s1] = kfoldPredict (CVMdl); %! assert_equal (CVMdl.Trained{1}.ScoreTransform, 'logit'); %! assert_equal (s1, 1 ./ (1 + exp (-2 * s0)), 1e-12); %!test %! ## 'none' is the identity, so assigning it transforms nothing %! load fisheriris %! CVMdl = ClassificationPartitionedKernel (meas(51:end,:), species(51:end), ... %! 'KFold', 5); %! [~, s0] = kfoldPredict (CVMdl); %! CVMdl.ScoreTransform = 'none'; %! [~, s1] = kfoldPredict (CVMdl); %! assert_equal (s1, s0); %!error ... %! load fisheriris %! CVMdl = ClassificationPartitionedKernel (meas(51:end,:), species(51:end), ... %! 'KFold', 5); %! CVMdl.ScoreTransform = 'nosuchtransform'; ## Test input validation %!error ... %! ClassificationPartitionedKernel (ones (10, 2)) %!error ... %! ClassificationPartitionedKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'KFold') %!error ... %! ClassificationPartitionedKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'KFold', 0) %!error ... %! ClassificationPartitionedKernel (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'KFold', 2, 'Leaveout', 'on') %!error ... %! kfoldLoss (ClassificationPartitionedKernel (ones (10, 2), ... %! [ones(5,1); 2*ones(5,1)], 'KFold', 2), 'LossFun', 'mse') %!error ... %! kfoldEdge (ClassificationPartitionedKernel (ones (10, 2), ... %! [ones(5,1); 2*ones(5,1)], 'KFold', 2), 'Folds', 0) ## ModelParameters carries the learner's parameters beside the tags this ## class reports for itself. %!test %! load fisheriris %! CVMdl = fitckernel (meas, strcmp (species, 'setosa'), 'KFold', 3); %! MP = CVMdl.ModelParameters; %! assert_equal (MP.Method, 'PartitionedKernel'); %! assert_equal (MP.LearnerTemplates, 'Kernel'); %! assert_equal (MP.NLearn, 3); %! assert_equal (MP.Learner, 'svm'); %! assert_equal (MP.BlockSize, 4000); ## Every documented score transform reaches the scores that are reported, and ## none of them moves the label: a transform reshapes what is reported, not ## what is decided. %!test %! load fisheriris %! Mdl = fitckernel (meas, strcmp (species, 'setosa'), 'KFold', 3); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = kfoldPredict (Mdl); %! T = {'identity', @(x) x; 'doublelogit', @(x) 1 ./ (1 + exp (-2 * x)); ... %! 'invlogit', @(x) log (x ./ (1 - x)); ... %! 'logit', @(x) 1 ./ (1 + exp (-x)); ... %! 'sign', @(x) sign (x); 'symmetric', @(x) 2 * x - 1; ... %! 'symmetriclogit', @(x) 2 ./ (1 + exp (-x)) - 1}; %! for i = 1:rows (T) %! Mdl.ScoreTransform = T{i,1}; %! [l, s] = kfoldPredict (Mdl); %! assert_equal (s, T{i,2}(raw), 1e-12); %! assert_equal (l, label); %! endfor %! ## ismax marks the largest score of each observation, ties to the first. %! [~, k] = max (raw, [], 2); %! e = zeros (size (raw)); %! e(sub2ind (size (raw), (1:rows (raw))', k)) = 1; %! Mdl.ScoreTransform = 'ismax'; %! [~, s] = kfoldPredict (Mdl); %! assert_equal (s, e); %! Mdl.ScoreTransform = 'symmetricismax'; %! [~, s] = kfoldPredict (Mdl); %! assert_equal (s, 2 * e - 1); ## A function handle is taken as given and applied to the scores. %!test %! load fisheriris %! Mdl = fitckernel (meas, strcmp (species, 'setosa'), 'KFold', 3); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = kfoldPredict (Mdl); %! Mdl.ScoreTransform = @(x) x .^ 2; %! [l, s] = kfoldPredict (Mdl); %! assert_equal (s, raw .^ 2, 1e-12); %! assert_equal (l, label); ## Table input %!test # the predictors and the response may come from a table %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! T = table (meas(inds,1), meas(inds,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = categorical (species(inds)); %! CVMdl = ClassificationPartitionedKernel (T, 'Species', 'KFold', 3); %! assert_equal (class (CVMdl), 'ClassificationPartitionedKernel'); %! assert_equal (CVMdl.KFold, 3); statistics-release-2.1.2/inst/Machine_Learning/ClassificationPartitionedLinear.m000066400000000000000000001170531527311104300301370ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ClassificationPartitionedLinear ## -*- texinfo -*- ## @deftp {statistics} ClassificationPartitionedLinear ## ## Cross-validated linear binary classifier. ## ## A @qcode{ClassificationPartitionedLinear} object holds one ## @qcode{ClassificationLinear} per fold of a partition, each fitted to the ## observations the fold trains on. Every @code{kfold} method predicts each ## observation with the fold that held it @emph{out}, so the estimate it ## returns is an out-of-sample one. ## ## A @qcode{ClassificationLinear} stores no copy of its training data and so ## has no resubstitution methods and no @code{compact} form. This class is ## what takes their place: cross-validation is the way a linear model is ## asked how it would do on data it has not seen. ## ## When the fold models carry a whole regularization path, every method ## returns one column per strength, in the order of the @qcode{'Lambda'} ## that was asked for. ## ## Create one with @code{fitclinear} and a cross-validation option, or ## directly. ## ## @seealso{fitclinear, ClassificationLinear, ClassificationPartitionedKernel} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationPartitionedLinear} {property} ClassNames ## ## Names of the two classes ## ## A column of the same type as the response, shared by every fold. ## This property is read-only. ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedLinear} {property} Cost ## ## Cost of misclassifying an observation ## ## A square numeric matrix with one row and one column per class. It is ## handed to every fold rather than re-derived by each. This property is ## read-only. ## ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedLinear} {property} Prior ## ## Prior probability of each class ## ## A numeric row vector summing to one, in the order of ## @qcode{ClassNames}. Like the cost it is the parent's and is handed to ## every fold, so a fold of an unbalanced problem does not quietly adopt ## a prior of its own. This property is read-only. ## ## @end deftp Prior = []; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {ClassificationPartitionedLinear} {property} ScoreTransform ## ## Transformation applied to the predicted scores ## ## A character vector naming a transformation, or the text of the ## function handle that was supplied, which may be assigned after the ## model is built. It is applied once to the assembled scores and is ## not handed to the folds. A transform the learner implies, as ## @qcode{'logistic'} implies @qcode{'logit'}, does stay with the folds, ## and this one is then applied on top of it. ## ## @end deftp ScoreTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationPartitionedLinear} {property} CrossValidatedModel ## ## Name of the model that was cross-validated ## ## Always @qcode{'Linear'}, the short name MATLAB uses. This property is ## read-only. ## ## @end deftp CrossValidatedModel = 'Linear'; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedLinear} {property} NumObservations ## ## Number of observations the partition covers ## ## A positive integer scalar, counting the rows that survived the removal ## of missing values. This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedLinear} {property} Y ## ## Response of the retained observations ## ## In the type it was supplied in. This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedLinear} {property} W ## ## Observation weights ## ## An @math{Nx1} numeric vector summing to one, normalized within each ## class to that class's cost-adjusted prior. This property is ## read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedLinear} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedLinear} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A row vector of column indices, empty when every predictor is ## numeric. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedLinear} {property} ResponseName ## ## Name of the response ## ## A character vector, defaulting to @qcode{'Y'}. This property is ## read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedLinear} {property} Trained ## ## The models fitted to the folds ## ## A cell column with one @qcode{ClassificationLinear} per fold, each ## fitted to the observations its fold trains on. This property is ## read-only. ## ## @end deftp Trained = {}; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedLinear} {property} KFold ## ## Number of folds ## ## A positive integer scalar. A holdout partition has one fold and a ## leave-one-out partition has as many as there are observations. This ## property is read-only. ## ## @end deftp KFold = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedLinear} {property} Partition ## ## The partition itself ## ## A @code{cvpartition} object over the retained observations. This ## property is read-only. ## ## @end deftp Partition = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedLinear} {property} ModelParameters ## ## What was cross-validated, and how ## ## A structure holding the parameters the folds were fitted with, carried ## through from the learner that was cross validated, beside ## @qcode{NLearn}, the number of folds, and the @qcode{Version}, ## @qcode{Method} and @qcode{Type} tags of this class, with ## @qcode{LearnerTemplates} naming the backing. The ## learner's own tags are replaced rather than kept, so a cross-validated ## SVM reports @qcode{Method} as @qcode{'PartitionedLinear'} and not ## @qcode{'SVM'}. ## ## @strong{Deviation from MATLAB.} MATLAB reports the parameter record of ## the cross-validation @emph{ensemble} here rather than of the learner, ## so it says nothing at all about how the folds were fitted: of its ## eighteen fields only the fold count, its partitioner and a fit template ## carry anything, and the rest are boosting settings left inert. Nor can ## the parameters be reached through the folds, a compact model carrying ## none in MATLAB. This class reports the fit instead, which is strictly ## more than MATLAB offers, and everything MATLAB's record does carry is ## published here as the @qcode{KFold}, @qcode{Partition}, @qcode{X}, ## @qcode{Y}, @qcode{W} and @qcode{CrossValidatedModel} properties. ## ## This property is read-only. ## ## @end deftp ModelParameters = []; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ## The callable behind ScoreTransform. STfun = @(s) s; ## The predictors of the retained observations. MATLAB does not report ## them and neither do we, but the kfold methods have to predict from ## something and the fold models hold no data of their own. X_ = []; ## Number of regularization strengths the fold models carry, so that ## every method knows how many columns to return without asking one. NumLambda_ = 1; endproperties methods (Access = public) ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedLinear} {@var{obj} =} ClassificationPartitionedLinear (@var{X}, @var{Y}) ## @deftypefnx {ClassificationPartitionedLinear} {@var{obj} =} ClassificationPartitionedLinear (@dots{}, @var{name}, @var{value}) ## ## Cross-validate a linear binary classifier. ## ## @code{@var{obj} = ClassificationPartitionedLinear (@var{X}, @var{Y})} ## partitions the data into ten stratified folds and fits a ## @qcode{ClassificationLinear} to each. ## ## @code{@var{obj} = ClassificationPartitionedLinear (@dots{}, ## @var{name}, @var{value})} takes one of @qcode{'KFold'}, ## @qcode{'Holdout'}, @qcode{'Leaveout'} and @qcode{'CVPartition'} to say ## how to partition, and any option @code{ClassificationLinear} takes to ## say how to fit. @qcode{'CrossVal'} is accepted and has no effect ## here, this class being cross-validated by construction. ## ## The classes, the prior and the cost are resolved once over the whole ## data and handed to every fold. Anything left as @qcode{'auto'} is ## not: each fold resolves @qcode{'Lambda'} against its own training ## rows, so ten folds of a hundred observations each get one ninetieth ## rather than one hundredth. Both are MATLAB's behaviour, measured. ## ## @seealso{fitclinear, ClassificationLinear} ## @end deftypefn function this = ClassificationPartitionedLinear (X, Y, varargin) if (nargin < 2) error (strcat ("ClassificationPartitionedLinear: too few input", ... " arguments.")); endif if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationPartitionedLinear: optional", ... " arguments must be given in Name-Value pairs.")); endif ## A table names its own predictors and says which hold levels. This ## class answers over its folds rather than over new data, so the ## levels are not kept: there is no later prediction to code. if (istable (X)) [X, Y, varargin, ~, errmsg] = tableFrame (X, Y, varargin); if (! isempty (errmsg)) error ("ClassificationPartitionedLinear: %s", errmsg); endif endif ## Split the argument list three ways: what says how to partition, ## what the parent owns, and what each fold is fitted with. [P, args] = partitionedArgs (varargin, ... 'ClassificationPartitionedLinear'); F = classFrame (X, Y, P.ClassNames, P.Prior, P.Cost, P.Weights, ... 'ClassificationPartitionedLinear'); [part, args] = cvPartitionOf (args, F.Y, F.n, ... 'ClassificationPartitionedLinear'); ## A transform asked for by name belongs to the parent, which applies ## it once to the assembled scores, and the folds are not given it. ## Measured on R2024a, where a cross-validated model fitted with a ## ScoreTransform reports it and leaves every Trained@{k@} at 'none'. ## A transform the learner implies is a different thing: a logistic ## fold reports posteriors and carries 'logit' of its own, which ## R2024a also does, and the parent's transform composes on top of it. fargs = [args, {'ClassNames', F.ClassNames, 'Prior', F.Prior, ... 'Cost', F.Cost}]; ## Built field by field rather than with one struct call: a cell ## array response passed to struct () makes a struct array of that ## size instead of a scalar struct holding the cell. G = struct (); G.X = F.X; G.Y = F.Y; G.Weights = F.Weights; this.Trained = foldModels ('ClassificationLinear', G, part, fargs); this.ClassNames = F.ClassNames; this.Cost = F.Cost; this.Prior = F.Prior; this.NumObservations = F.n; this.Y = F.Y; this.W = F.W; this.X_ = F.X; this.KFold = part.NumTestSets; this.Partition = part; this.NumLambda_ = numel (this.Trained{1}.Lambda); this.PredictorNames = this.Trained{1}.PredictorNames; this.CategoricalPredictors = this.Trained{1}.CategoricalPredictors; this.ResponseName = this.Trained{1}.ResponseName; if (isempty (P.ScoreTransform)) P.ScoreTransform = 'none'; endif [this.STfun, this.ScoreTransform] = ... parseScoreTransform (P.ScoreTransform, ... 'ClassificationPartitionedLinear'); ## The learner's parameters, under this class's own tags. MATLAB ## reports an EnsembleParams here instead and so says nothing about ## the fit; see the ModelParameters property for the deviation. this.ModelParameters = partitionedModelParams (this.Trained{1}, ... this.KFold, 'PartitionedLinear', ... 'classification', 'Linear'); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedLinear} {@var{labels} =} kfoldPredict (@var{obj}) ## @deftypefnx {ClassificationPartitionedLinear} {[@var{labels}, @var{scores}] =} kfoldPredict (@var{obj}) ## ## Out-of-fold class of every observation. ## ## Each observation is classified by the fold that held it out, so the ## labels are out-of-sample. An observation that no fold held out, which ## under a holdout partition is most of them, comes back missing rather ## than classified, and its scores come back @qcode{NaN}. ## ## With @math{L} regularization strengths @var{labels} has one column per ## strength and @var{scores} is @math{Nx2xL}. ## ## @end deftypefn function [labels, scores] = kfoldPredict (this) [labels, raw] = kfoldScores (this.Trained, this.Partition, this.X_, ... this.Y, this.ClassNames, ... this.NumLambda_); if (nargout > 1) scores = transformScores (this, raw); endif endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedLinear} {@var{m} =} kfoldMargin (@var{obj}) ## ## Out-of-fold classification margin of every observation. ## ## The score the out-of-fold model gives the true class, less the score ## it gives the other one. An observation no fold held out comes back ## @qcode{NaN}. With @math{L} regularization strengths @var{m} has one ## column per strength. ## ## @end deftypefn function m = kfoldMargin (this) [~, raw] = kfoldScores (this.Trained, this.Partition, this.X_, ... this.Y, this.ClassNames, this.NumLambda_); s = transformScores (this, raw); [gY, errmsg] = labelIndices (this.ClassNames, this.Y); if (! isempty (errmsg)) error ("ClassificationPartitionedLinear.kfoldMargin: %s", errmsg); endif m = marginsOf (s, gY, this.NumLambda_); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedLinear} {@var{e} =} kfoldEdge (@var{obj}) ## @deftypefnx {ClassificationPartitionedLinear} {@var{e} =} kfoldEdge (@dots{}, @var{name}, @var{value}) ## ## Weighted mean of the out-of-fold classification margins. ## ## @code{@var{e} = kfoldEdge (@dots{}, @var{name}, @var{value})} takes ## @qcode{'Folds'}, a subset of the folds to average over, and ## @qcode{'Mode'}, either @qcode{'average'}, the default, or ## @qcode{'individual'}, which returns one row per fold instead. ## ## @end deftypefn function e = kfoldEdge (this, varargin) O = kfoldOpts (varargin, {}, 'ClassificationPartitionedLinear', ... 'kfoldEdge', this.KFold); [~, raw] = kfoldScores (this.Trained, this.Partition, this.X_, ... this.Y, this.ClassNames, this.NumLambda_); s = transformScores (this, raw); [gY, errmsg] = labelIndices (this.ClassNames, this.Y); if (! isempty (errmsg)) error ("ClassificationPartitionedLinear.kfoldEdge: %s", errmsg); endif m = marginsOf (s, gY, this.NumLambda_); sets = foldSets (this.Partition, O.Folds, O.Mode, ... this.NumObservations); e = zeros (numel (sets), this.NumLambda_); for i = 1:numel (sets) idx = sets{i}; w = this.W(idx); w = w / sum (w); e(i,:) = sum (w .* m(idx,:), 1); endfor endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedLinear} {@var{l} =} kfoldLoss (@var{obj}) ## @deftypefnx {ClassificationPartitionedLinear} {@var{l} =} kfoldLoss (@dots{}, @var{name}, @var{value}) ## ## Out-of-fold classification loss. ## ## @code{@var{l} = kfoldLoss (@var{obj})} returns the out-of-fold ## misclassification rate. ## ## @code{@var{l} = kfoldLoss (@dots{}, @var{name}, @var{value})} takes ## @qcode{'LossFun'}, one of @qcode{'binodeviance'}, ## @qcode{'classifcost'}, @qcode{'classiferror'}, @qcode{'exponential'}, ## @qcode{'hinge'}, @qcode{'logit'}, @qcode{'mincost'} and ## @qcode{'quadratic'}; @qcode{'Folds'}; and @qcode{'Mode'}. ## ## @end deftypefn function l = kfoldLoss (this, varargin) valid = {'binodeviance', 'classifcost', 'classiferror', ... 'exponential', 'hinge', 'logit', 'mincost', 'quadratic'}; O = kfoldOpts (varargin, valid, 'ClassificationPartitionedLinear', ... 'kfoldLoss', this.KFold); if (isempty (O.LossFun)) O.LossFun = 'classiferror'; endif [~, raw] = kfoldScores (this.Trained, this.Partition, this.X_, ... this.Y, this.ClassNames, this.NumLambda_); s = transformScores (this, raw); [gY, errmsg] = labelIndices (this.ClassNames, this.Y); if (! isempty (errmsg)) error ("ClassificationPartitionedLinear.kfoldLoss: %s", errmsg); endif sets = foldSets (this.Partition, O.Folds, O.Mode, ... this.NumObservations); l = zeros (numel (sets), this.NumLambda_); for i = 1:numel (sets) idx = sets{i}; w = this.W(idx); w = w / sum (w); for k = 1:this.NumLambda_ if (this.NumLambda_ == 1) sk = s(idx,:); else sk = s(idx,:,k); endif l(i,k) = classificationLoss (O.LossFun, sk, gY(idx), w, this.Cost); endfor endfor endfunction endmethods methods (Access = public, Hidden) function this = set.ScoreTransform (this, val) [f, nm] = parseScoreTransform (val, 'ClassificationPartitionedLinear'); this.ScoreTransform = nm; this.STfun = f; endfunction function display (this) in_name = inputname (1); if (! isempty (in_name)) printf ('%s =\n', in_name); endif disp (this); endfunction function disp (this) printf ("\n ClassificationPartitionedLinear\n\n"); printf ("%+25s: '%s'\n", 'CrossValidatedModel', ... this.CrossValidatedModel); printf ("%+25s: %s\n", 'ClassNames', ... classNameListing (this.ClassNames)); printf ("%+25s: '%s'\n", 'ScoreTransform', this.ScoreTransform); printf ("%+25s: %d\n", 'NumObservations', this.NumObservations); printf ("%+25s: %d\n", 'KFold', this.KFold); printf ("\n"); endfunction endmethods methods (Access = private) ## Apply the parent's score transform to the assembled scores, whatever ## shape they came back in. function s = transformScores (this, raw) if (this.NumLambda_ == 1) s = this.STfun (raw); else s = raw; for k = 1:this.NumLambda_ s(:,:,k) = this.STfun (raw(:,:,k)); endfor endif endfunction endmethods endclassdef %!demo %! ## Cross-validate a linear classifier on the two overlapping iris %! ## species and read the out-of-sample error rate. %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! CVMdl = ClassificationPartitionedLinear (X, Y, 'KFold', 5) %! outOfSample = kfoldLoss (CVMdl) %!test %! ## The model reports the surface MATLAB reports %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! CVMdl = ClassificationPartitionedLinear (X, Y, 'KFold', 5); %! assert_equal (class (CVMdl), 'ClassificationPartitionedLinear'); %! assert_equal (CVMdl.CrossValidatedModel, 'Linear'); %! assert_equal (CVMdl.KFold, 5); %! assert_equal (CVMdl.NumObservations, 100); %! assert_equal (CVMdl.ClassNames, {'versicolor'; 'virginica'}); %! assert_equal (CVMdl.Prior, [0.5, 0.5]); %! assert_equal (CVMdl.Cost, [0, 1; 1, 0]); %! assert_equal (CVMdl.ScoreTransform, 'none'); %! assert_equal (CVMdl.ResponseName, 'Y'); %! assert_equal (CVMdl.PredictorNames, {'x1', 'x2', 'x3', 'x4'}); %! assert_equal (size (CVMdl.Trained), [5, 1]); %! assert_equal (class (CVMdl.Trained{1}), 'ClassificationLinear'); %! assert_equal (sum (CVMdl.W), 1, 1e-12); %!test %! ## The properties are the ones MATLAB lists, in its order %! load fisheriris %! CVMdl = ClassificationPartitionedLinear (meas(51:end,:), species(51:end)); %! assert_equal (sort (properties (CVMdl)), ... %! sort ({'ClassNames'; 'Cost'; 'Prior'; 'ScoreTransform'; ... %! 'CrossValidatedModel'; 'NumObservations'; 'Y'; 'W'; ... %! 'PredictorNames'; 'CategoricalPredictors'; ... %! 'ResponseName'; 'Trained'; 'KFold'; 'Partition'; ... %! 'ModelParameters'})); %!test %! ## The default is ten stratified folds %! load fisheriris %! CVMdl = ClassificationPartitionedLinear (meas(51:end,:), species(51:end)); %! assert_equal (CVMdl.KFold, 10); %! assert_equal (numel (CVMdl.Trained), 10); %!test %! ## Each fold resolves 'Lambda' against its own training rows, not the %! ## parent's count: eighty of a hundred observations train each fold of %! ## five, so the strength is one eightieth. This is R2024a's number. %! load fisheriris %! CVMdl = ClassificationPartitionedLinear (meas(51:end,:), ... %! species(51:end), 'KFold', 5); %! assert_equal (CVMdl.Trained{1}.Lambda, 1 / 80, 1e-15); %! assert_equal (CVMdl.Trained{3}.Lambda, 1 / 80, 1e-15); %!test %! ## The class names, the prior and the cost are the parent's and are %! ## handed to every fold, so an unbalanced problem does not give each fold %! ## a prior of its own. Also R2024a's behaviour. %! load fisheriris %! X = meas([51:100, 101:120],:); %! Y = species([51:100, 101:120]); %! CVMdl = ClassificationPartitionedLinear (X, Y, 'KFold', 5); %! assert_equal (CVMdl.Prior, [50/70, 20/70], 1e-12); %! assert_equal (CVMdl.Trained{1}.Prior, CVMdl.Prior); %! assert_equal (CVMdl.Trained{4}.Prior, CVMdl.Prior); %! assert_equal (CVMdl.Trained{1}.ClassNames, CVMdl.ClassNames); %!test %! ## kfoldPredict predicts each observation with the fold that held it out. %! ## Doing the same partition by hand must give the same answers, which is %! ## what pins the assembly rather than the fit. %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! part = cvpartition (Y, 'KFold', 4); %! CVMdl = ClassificationPartitionedLinear (X, Y, 'CVPartition', part); %! [label, score] = kfoldPredict (CVMdl); %! byhand = cell (100, 1); %! for k = 1:4 %! tr = training (part, k); %! te = test (part, k); %! m = ClassificationLinear (X(tr,:), Y(tr), 'ClassNames', ... %! CVMdl.ClassNames, 'Prior', CVMdl.Prior, ... %! 'Cost', CVMdl.Cost); %! byhand(te) = predict (m, X(te,:)); %! endfor %! assert_equal (label, byhand); %! assert_equal (size (score), [100, 2]); %!test %! ## The margin is the out-of-fold score of the true class less the other, %! ## and the edge its weighted mean %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! CVMdl = ClassificationPartitionedLinear (X, Y, 'KFold', 5); %! [~, score] = kfoldPredict (CVMdl); %! m = kfoldMargin (CVMdl); %! virg = strcmp (Y, 'virginica'); %! expect = score(:,1) - score(:,2); %! expect(virg) = score(virg,2) - score(virg,1); %! assert_equal (m, expect, 1e-12); %! assert_equal (mean (m > 0), 1 - kfoldLoss (CVMdl), 1e-12); %!test %! ## 'Mode', 'individual' gives one row per fold %! load fisheriris %! CVMdl = ClassificationPartitionedLinear (meas(51:end,:), ... %! species(51:end), 'KFold', 5); %! assert_equal (size (kfoldLoss (CVMdl, 'Mode', 'individual')), [5, 1]); %! assert_equal (size (kfoldEdge (CVMdl, 'Mode', 'individual')), [5, 1]); %!test %! ## Averaging pools the observations rather than averaging the per-fold %! ## values, which is not the same thing once the folds differ in size. %! ## The pooled reading is the one R2024a returns. %! load fisheriris %! X = meas([51:100, 101:143],:); %! Y = species([51:100, 101:143]); %! CVMdl = ClassificationPartitionedLinear (X, Y, 'KFold', 4); %! [~, score] = kfoldPredict (CVMdl); %! gY = 1 + strcmp (Y, 'virginica'); %! wrong = mean (score(sub2ind (size (score), (1:93)', gY)) ... %! <= score(sub2ind (size (score), (1:93)', 3 - gY))); %! assert_equal (kfoldLoss (CVMdl), wrong, 1e-12); %!test %! ## 'Folds' reports over the observations of the folds it names %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! CVMdl = ClassificationPartitionedLinear (X, Y, 'KFold', 5); %! two = kfoldLoss (CVMdl, 'Folds', [1, 2]); %! idx = test (CVMdl.Partition, 1) | test (CVMdl.Partition, 2); %! [label, ~] = kfoldPredict (CVMdl); %! assert_equal (two, mean (! strcmp (label(idx), Y(idx))), 1e-12); %!test %! ## Every classification loss is offered, and each reads the out-of-fold %! ## score of the true class %! load fisheriris %! CVMdl = ClassificationPartitionedLinear (meas(51:end,:), ... %! species(51:end), 'KFold', 5); %! for f = {'binodeviance', 'classifcost', 'classiferror', 'exponential', ... %! 'hinge', 'logit', 'mincost', 'quadratic'} %! assert_equal (isfinite (kfoldLoss (CVMdl, 'LossFun', f{1})), true); %! endfor %!test %! ## A logistic fit reports posteriors, and the transform that produces %! ## them stays with the fold models: the cross-validated model reports %! ## none of its own, which is R2024a's arrangement and not the reverse %! load fisheriris %! CVMdl = ClassificationPartitionedLinear (meas(51:end,:), ... %! species(51:end), 'KFold', 5, ... %! 'Learner', 'logistic'); %! assert_equal (CVMdl.ScoreTransform, 'none'); %! assert_equal (CVMdl.Trained{1}.ScoreTransform, 'logit'); %! [~, score] = kfoldPredict (CVMdl); %! assert_equal (sum (score, 2), ones (100, 1), 1e-12); %!test %! ## A whole regularization path gives one column per strength everywhere %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! CVMdl = ClassificationPartitionedLinear (X, Y, 'KFold', 5, ... %! 'Lambda', [0.001, 0.01, 0.1]); %! [label, score] = kfoldPredict (CVMdl); %! assert_equal (size (label), [100, 3]); %! assert_equal (size (score), [100, 2, 3]); %! assert_equal (size (kfoldMargin (CVMdl)), [100, 3]); %! assert_equal (size (kfoldLoss (CVMdl)), [1, 3]); %! assert_equal (size (kfoldEdge (CVMdl)), [1, 3]); %! assert_equal (size (kfoldLoss (CVMdl, 'Mode', 'individual')), [5, 3]); %!test %! ## An observation that no fold held out is not classified: under a %! ## holdout partition that is the training rows, and they come back %! ## missing rather than carrying a class %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! CVMdl = ClassificationPartitionedLinear (X, Y, 'Holdout', 0.3); %! assert_equal (CVMdl.KFold, 1); %! [label, score] = kfoldPredict (CVMdl); %! assert_equal (sum (cellfun (@isempty, label)), 70); %! assert_equal (sum (isnan (score(:,1))), 70); %! assert_equal (isfinite (kfoldLoss (CVMdl)), true); %!test %! ## Leave-one-out gives as many folds as there are observations %! load fisheriris %! X = meas([51:70, 101:120],:); %! Y = species([51:70, 101:120]); %! CVMdl = ClassificationPartitionedLinear (X, Y, 'Leaveout', 'on'); %! assert_equal (CVMdl.KFold, 40); %! assert_equal (numel (kfoldPredict (CVMdl)), 40); %!test %! ## A row with a missing predictor is dropped before the partition, so %! ## every index below refers to the same set of observations %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! X(3,2) = NaN; %! CVMdl = ClassificationPartitionedLinear (X, Y, 'KFold', 5); %! assert_equal (CVMdl.NumObservations, 99); %! assert_equal (numel (CVMdl.Y), 99); %! assert_equal (CVMdl.Partition.NumObservations, 99); %!test %! ## Observation weights reach both the folds and the reported prior %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! CVMdl = ClassificationPartitionedLinear (X, Y, 'KFold', 5, ... %! 'Weights', (1:100)'); %! assert_equal (CVMdl.Prior, [0.252475247524752, 0.747524752475248], 1e-12); %! assert_equal (CVMdl.Trained{1}.Prior, CVMdl.Prior); %! assert_equal (sum (CVMdl.W), 1, 1e-12); %!test %! ## A character matrix response carries through cross-validation: the %! ## class names, the fold models, the assembled labels and every kfold %! ## method answer as they do for the equivalent cell array %! load fisheriris %! X = meas(51:end,:); %! Yc = species(51:end); %! Ym = char (Yc); %! part = cvpartition (Yc, 'KFold', 4); %! CVc = ClassificationPartitionedLinear (X, Yc, 'CVPartition', part); %! CVm = ClassificationPartitionedLinear (X, Ym, 'CVPartition', part); %! assert_equal (cellstr (CVm.ClassNames), CVc.ClassNames); %! assert_equal (size (CVm.ClassNames), [2, 10]); %! assert_equal (cellstr (CVm.Trained{1}.ClassNames), CVc.ClassNames); %! assert_equal (cellstr (kfoldPredict (CVm)), kfoldPredict (CVc)); %! assert_equal (kfoldMargin (CVm), kfoldMargin (CVc)); %! assert_equal (kfoldEdge (CVm), kfoldEdge (CVc)); %! assert_equal (kfoldLoss (CVm), kfoldLoss (CVc)); %! assert_equal (kfoldLoss (CVm, 'LossFun', 'hinge'), ... %! kfoldLoss (CVc, 'LossFun', 'hinge')); %!test %! ## Names of unequal length are padded by the character matrix and the %! ## padding is not part of the name, through the cross-validated path too %! load fisheriris %! X = meas(51:end,:); %! Y = [repmat({'ab'}, 50, 1); repmat({'abcd'}, 50, 1)]; %! part = cvpartition (Y, 'KFold', 4); %! CVc = ClassificationPartitionedLinear (X, Y, 'CVPartition', part); %! CVm = ClassificationPartitionedLinear (X, char (Y), 'CVPartition', part); %! assert_equal (cellstr (CVm.ClassNames), CVc.ClassNames); %! assert_equal (cellstr (kfoldPredict (CVm)), kfoldPredict (CVc)); %!test %! ## A row dropped for a missing predictor is dropped before the partition, %! ## through the character path, which is the case that exercises the row %! ## indexing rather than the label comparison %! load fisheriris %! X = meas(51:end,:); %! X(7,3) = NaN; %! CVMdl = ClassificationPartitionedLinear (X, char (species(51:end)), ... %! 'KFold', 4); %! assert_equal (CVMdl.NumObservations, 99); %! assert_equal (size (CVMdl.Y), [99, 10]); %! assert_equal (numel (kfoldPredict (CVMdl)) / 10, 99); %!test %! ## A transform asked for by name goes to the parent and not to the folds, %! ## and is applied once to the assembled scores. R2024a's arrangement. %! load fisheriris %! part = cvpartition (species(51:end), 'KFold', 5); %! plain = ClassificationPartitionedLinear (meas(51:end,:), species(51:end), ... %! 'CVPartition', part); %! CVMdl = ClassificationPartitionedLinear (meas(51:end,:), species(51:end), ... %! 'CVPartition', part, ... %! 'ScoreTransform', 'doublelogit'); %! assert_equal (CVMdl.ScoreTransform, 'doublelogit'); %! assert_equal (CVMdl.Trained{1}.ScoreTransform, 'none'); %! [~, s0] = kfoldPredict (plain); %! [~, s1] = kfoldPredict (CVMdl); %! assert_equal (s1, 1 ./ (1 + exp (-2 * s0)), 1e-12); %!test %! ## It can be assigned after the model is built, and reaches kfoldPredict %! ## without being carried into the folds %! load fisheriris %! CVMdl = ClassificationPartitionedLinear (meas(51:end,:), species(51:end), ... %! 'KFold', 5); %! [~, s0] = kfoldPredict (CVMdl); %! CVMdl.ScoreTransform = 'doublelogit'; %! [~, s1] = kfoldPredict (CVMdl); %! assert_equal (CVMdl.Trained{1}.ScoreTransform, 'none'); %! assert_equal (s1, 1 ./ (1 + exp (-2 * s0)), 1e-12); %!test %! ## A transform the learner implies stays with the folds, and an assigned %! ## one is applied on top of it rather than replacing it. Measured on %! ## R2024a, where the folds keep 'logit' and the parent's transform %! ## composes. %! load fisheriris %! CVMdl = ClassificationPartitionedLinear (meas(51:end,:), species(51:end), ... %! 'KFold', 5, ... %! 'Learner', 'logistic'); %! [~, s0] = kfoldPredict (CVMdl); %! CVMdl.ScoreTransform = 'doublelogit'; %! [~, s1] = kfoldPredict (CVMdl); %! assert_equal (CVMdl.Trained{1}.ScoreTransform, 'logit'); %! assert_equal (s1, 1 ./ (1 + exp (-2 * s0)), 1e-12); %!test %! ## 'none' is the identity, so assigning it transforms nothing %! load fisheriris %! CVMdl = ClassificationPartitionedLinear (meas(51:end,:), species(51:end), ... %! 'KFold', 5); %! [~, s0] = kfoldPredict (CVMdl); %! CVMdl.ScoreTransform = 'none'; %! [~, s1] = kfoldPredict (CVMdl); %! assert_equal (s1, s0); %!error ... %! load fisheriris %! CVMdl = ClassificationPartitionedLinear (meas(51:end,:), species(51:end), ... %! 'KFold', 5); %! CVMdl.ScoreTransform = 'nosuchtransform'; ## Test input validation %!error ... %! ClassificationPartitionedLinear (ones (10, 2)) %!error ... %! ClassificationPartitionedLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'KFold') %!error ... %! ClassificationPartitionedLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'KFold', 1) %!error ... %! ClassificationPartitionedLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'Holdout', 1.5) %!error ... %! ClassificationPartitionedLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'Leaveout', 1) %!error ... %! ClassificationPartitionedLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'CVPartition', 3) %!error ... %! ClassificationPartitionedLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'KFold', 2, 'Holdout', 0.2) %!error ... %! ClassificationPartitionedLinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'CVPartition', cvpartition (20, 'KFold', 2)) %!error ... %! ClassificationPartitionedLinear (ones (9, 2), ... %! [1; 1; 1; 2; 2; 2; 3; 3; 3], 'KFold', 3) %!error ... %! kfoldLoss (ClassificationPartitionedLinear (ones (10, 2), ... %! [ones(5,1); 2*ones(5,1)], 'KFold', 2), 'Mode', 'each') %!error ... %! kfoldLoss (ClassificationPartitionedLinear (ones (10, 2), ... %! [ones(5,1); 2*ones(5,1)], 'KFold', 2), 'Folds', 7) %!error ... %! kfoldLoss (ClassificationPartitionedLinear (ones (10, 2), ... %! [ones(5,1); 2*ones(5,1)], 'KFold', 2), 'LossFun', 'mse') %!error ... %! kfoldEdge (ClassificationPartitionedLinear (ones (10, 2), ... %! [ones(5,1); 2*ones(5,1)], 'KFold', 2), 'LossFun', ... %! 'hinge') ## Every documented score transform reaches the scores that are reported, and ## none of them moves the label: a transform reshapes what is reported, not ## what is decided. %!test %! load fisheriris %! Mdl = fitclinear (meas, strcmp (species, 'setosa'), 'KFold', 3); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = kfoldPredict (Mdl); %! T = {'identity', @(x) x; 'doublelogit', @(x) 1 ./ (1 + exp (-2 * x)); ... %! 'invlogit', @(x) log (x ./ (1 - x)); ... %! 'logit', @(x) 1 ./ (1 + exp (-x)); ... %! 'sign', @(x) sign (x); 'symmetric', @(x) 2 * x - 1; ... %! 'symmetriclogit', @(x) 2 ./ (1 + exp (-x)) - 1}; %! for i = 1:rows (T) %! Mdl.ScoreTransform = T{i,1}; %! [l, s] = kfoldPredict (Mdl); %! assert_equal (s, T{i,2}(raw), 1e-12); %! assert_equal (l, label); %! endfor %! ## ismax marks the largest score of each observation, ties to the first. %! [~, k] = max (raw, [], 2); %! e = zeros (size (raw)); %! e(sub2ind (size (raw), (1:rows (raw))', k)) = 1; %! Mdl.ScoreTransform = 'ismax'; %! [~, s] = kfoldPredict (Mdl); %! assert_equal (s, e); %! Mdl.ScoreTransform = 'symmetricismax'; %! [~, s] = kfoldPredict (Mdl); %! assert_equal (s, 2 * e - 1); ## A function handle is taken as given and applied to the scores. %!test %! load fisheriris %! Mdl = fitclinear (meas, strcmp (species, 'setosa'), 'KFold', 3); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = kfoldPredict (Mdl); %! Mdl.ScoreTransform = @(x) x .^ 2; %! [l, s] = kfoldPredict (Mdl); %! assert_equal (s, raw .^ 2, 1e-12); %! assert_equal (l, label); ## Table input %!test # the predictors and the response may come from a table %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! T = table (meas(inds,1), meas(inds,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = species(inds); %! CVMdl = ClassificationPartitionedLinear (T, 'Species', 'KFold', 3); %! assert_equal (class (CVMdl), 'ClassificationPartitionedLinear'); %! assert_equal (CVMdl.KFold, 3); statistics-release-2.1.2/inst/Machine_Learning/ClassificationPartitionedModel.m000066400000000000000000003263771527311104300300000ustar00rootroot00000000000000## Copyright (C) 2024 Ruchika Sonagote ## Copyright (C) 2024 Pallav Purbia ## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ClassificationPartitionedModel ## -*- texinfo -*- ## @deftp {statistics} ClassificationPartitionedModel ## ## Cross-validated classification model ## ## The @code{ClassificationPartitionedModel} class stores cross-validated ## classification models trained on different partitions of the data. ## It can predict responses for observations not used for training using ## the @code{kfoldPredict} method. ## ## Create a @code{ClassificationPartitionedModel} object by using the ## @code{crossval} function. ## ## @seealso{crossval} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationPartitionedModel} {property} ClassNames ## ## Names of classes in the response variable ## ## An array of unique values of the response variable @var{Y}, which has the ## same data types as the data in @var{Y}, sorted or in the order given by ## the @qcode{'ClassNames'} option. This property is read-only. ## @qcode{ClassNames} can have any of the following datatypes: ## ## @itemize ## @item Cell array of character vectors ## @item Character array ## @item Logical vector ## @item Numeric vector ## @end itemize ## ## @end deftp ClassNames = []; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {ClassificationPartitionedModel} {property} Cost ## ## Cost of Misclassification ## ## A square matrix specifying the cost of misclassification of a point. ## @qcode{Cost(i,j)} is the cost of classifying a point into class @qcode{j} ## if its true class is @qcode{i} (that is, the rows correspond to the true ## class and the columns correspond to the predicted class). The order of ## the rows and columns in @qcode{Cost} corresponds to the order of the ## classes in @qcode{ClassNames}. The number of rows and columns in ## @qcode{Cost} is the number of unique classes in the response. By ## default, @qcode{Cost(i,j) = 1} if @qcode{i != j}, and ## @qcode{Cost(i,j) = 0} if @qcode{i = j}. In other words, the cost is 0 ## for correct classification and 1 for incorrect classification. ## ## Assigning @qcode{Cost} rebuilds it on every fold in @qcode{Trained}, so ## @code{kfoldPredict} and @code{kfoldLoss} answer under the new costs. It ## is refused on a cross-validated @code{ClassificationSVM}, whose costs ## enter the box constraint while it is being fitted: a model already fitted ## under one cost matrix cannot be made to describe another. ## ## ## A cost may also be given as a struct with the fields ## @qcode{ClassNames} and @qcode{ClassificationCosts}, which names the ## order its own matrix is written in. That matrix is permuted into the ## order of @qcode{ClassNames} above, so a caller need not know which ## order the classes were sorted into. It must name every class. ## ## A cost must be floating point, not sparse, not complex, non-negative ## and zero down its diagonal, and must hold no @qcode{NaN} or ## @qcode{Inf}. A @code{single} is widened to @code{double}. ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedModel} {property} Prior ## ## Prior probability for each class ## ## A numeric vector specifying the prior probabilities for each class. The ## order of the elements in @qcode{Prior} corresponds to the order of the ## classes in @qcode{ClassNames}. ## ## It may be assigned only on a cross-validated ## @code{ClassificationDiscriminant} or @code{ClassificationNaiveBayes}, ## the two learners that score from the priors they are given rather than ## consuming them while they fit: the discriminant re-derives its ## coefficients from them and the naive Bayes weights its class densities ## by them. Every other learner cannot revisit them afterwards. ## Assigning it rebuilds the priors on every fold in @qcode{Trained}. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedModel} {property} ScoreTransform ## ## Transformation function for classification scores ## ## Specified as a function handle for transforming the classification ## scores. @code{fitSVMPosterior} stores here the transformation from ## scores to posterior probabilities it fits for an SVM, as ## @code{ClassificationSVM.ScoreTransform} describes. ## ## @end deftp ScoreTransform = []; endproperties properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationPartitionedModel} {property} CrossValidatedModel ## ## Cross-validated model class ## ## A character vector holding the short name of the learner that was ## cross validated, as MATLAB reports it: @qcode{'Discriminant'}, ## @qcode{'GAM'}, @qcode{'KNN'}, @qcode{'NeuralNetwork'} or ## @qcode{'SVM'}. It is not the class name of that learner, and the ## regression side uses the same names. This property is read-only. ## ## @end deftp CrossValidatedModel = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedModel} {property} PredictorNames ## ## Names of predictor variables ## ## A cell array of character vectors specifying the names of the predictor ## variables. The names are in the order in which they appear in the ## training dataset. This property is read-only. ## ## @end deftp PredictorNames = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedModel} {property} CategoricalPredictors ## ## Indices of categorical predictors ## ## A vector of positive integers specifying the indices of categorical ## predictors. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedModel} {property} ResponseName ## ## Response variable name ## ## A character vector specifying the name of the response variable @var{Y}. ## This property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedModel} {property} NumObservations ## ## Number of observations ## ## A positive integer value specifying the number of observations in the ## training dataset used for training the cross-validated model. ## This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedModel} {property} X ## ## Predictor data ## ## A numeric matrix containing the unstandardized predictor data. Each ## column of @var{X} represents one predictor (variable), and each row ## represents one observation. This property is read-only. ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedModel} {property} Y ## ## Class labels ## ## Specified as a logical or numeric column vector, or as a character array ## or a cell array of character vectors with the same number of rows as the ## predictor data. Each row in @var{Y} is the observed class label for ## the corresponding row in @var{X}. This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedModel} {property} W ## ## Observation weights ## ## A numeric column vector with one entry per observation, carried over ## from the model that was cross validated. This property is read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedModel} {property} ModelParameters ## ## Model parameters ## ## A structure holding the parameters the folds were fitted with, carried ## through from the learner that was cross validated, beside ## @qcode{NLearn}, the number of folds, and the @qcode{Version}, ## @qcode{Method} and @qcode{Type} tags of this class. The ## learner's own tags are replaced rather than kept, so a cross-validated ## SVM reports @qcode{Method} as @qcode{'PartitionedModel'} and not ## @qcode{'SVM'}. ## ## @strong{Deviation from MATLAB.} MATLAB reports the parameter record of ## the cross-validation @emph{ensemble} here rather than of the learner, ## so it says nothing at all about how the folds were fitted: of its ## eighteen fields only the fold count, its partitioner and a fit template ## carry anything, and the rest are boosting settings left inert. Nor can ## the parameters be reached through the folds, a compact model carrying ## none in MATLAB. This class reports the fit instead, which is strictly ## more than MATLAB offers, and everything MATLAB's record does carry is ## published here as the @qcode{KFold}, @qcode{Partition}, @qcode{X}, ## @qcode{Y}, @qcode{W} and @qcode{CrossValidatedModel} properties. ## ## This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedModel} {property} Trained ## ## Models trained on each fold ## ## A cell array of models trained on each fold. Each cell contains a model ## trained on the minus-one fold of the data (all but one fold used for ## training and the remaining fold used for validation). This property is ## read-only. ## ## @end deftp Trained = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedModel} {property} KFold ## ## Number of cross-validated folds ## ## A positive integer value specifying the number of cross-validated folds. ## This property is read-only. ## ## @end deftp KFold = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedModel} {property} Partition ## ## Partition configuration ## ## A @code{cvpartition} object specifying the partition configuration used ## for cross-validation. This field stores the cvpartition instance that ## describes how the data was split into training and validation sets. ## This property is read-only. ## ## @end deftp Partition = []; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedModel} {property} BinEdges ## ## Bin edges ## ## A cell array with one entry per predictor, holding that predictor's ## bin edges where the learner discretized it before fitting. It is ## carried over from the model that was cross validated, and is empty ## whenever that model did no binning, which is every learner this package ## implements: MATLAB fills it only for its GAM, which bins because it is ## built from boosted trees where ours is built from splines. ## ## This property is read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {ClassificationPartitionedModel} {property} NumTrainedPerFold ## ## How many trees each fold fitted ## ## A scalar structure with fields @qcode{PredictorTrees} and ## @qcode{InteractionTrees}, each a row with one entry per fold, for a ## generalized additive model backing, and empty for every other. ## ## It reports what each fold actually fitted, which the budget in ## @qcode{ModelParameters} does not: a phase stops early when it can no ## longer improve the fit, and the folds need not stop at the same place. ## ## MATLAB carries this on its per-learner partitioned GAM classes, which ## this package deliberately does not have (see @code{crossval}), so like ## @qcode{IsStandardDeviationFit} it is declared here for every backing ## and left empty where it does not apply. ## ## This property is read-only. ## ## @end deftp NumTrainedPerFold = []; endproperties ## Copied from the parent model and kept out of the documented surface. properties (GetAccess = public, SetAccess = protected, Hidden) STfun = @(x) x; ## Raised once the constructor is done. Cost and Prior are refused for ## the learners MATLAB refuses them for, but the constructor has to carry ## whatever the cross validated model held, so the guards only apply to a ## user's assignment. Fitted = false; endproperties ## Set methods for the properties a user may assign. methods (Hidden) function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ('%s =\n', in_name); endif disp (this); endfunction ## Shaped after RegressionPartitionedModel's, with the classifier's own ## fields: the classes it separates and the transform its scores carry. function disp (this) fprintf ("\n ClassificationPartitionedModel\n\n"); fprintf ("%+25s: '%s'\n", 'CrossValidatedModel', ... this.CrossValidatedModel); fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); if (iscellstr (this.ClassNames)) str = repmat ({'''%s'''}, 1, numel (this.ClassNames)); str = strcat ('{', strjoin (str, ' '), '}'); str = sprintf (str, this.ClassNames{:}); elseif (ischar (this.ClassNames)) str = repmat ({'''%s'''}, 1, rows (this.ClassNames)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, cellstr (this.ClassNames){:}); else # single, double, logical str = repmat ({'%d'}, 1, numel (this.ClassNames)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, this.ClassNames); endif fprintf ("%+25s: %s\n", 'ClassNames', str); fprintf ("%+25s: %d\n", 'NumObservations', this.NumObservations); fprintf ("%+25s: %d\n", 'KFold', this.KFold); fprintf ("%+25s: '%s'\n\n", 'ScoreTransform', this.ScoreTransform); endfunction ## MATLAB refuses this one on the two backings whose costs are consumed ## while they are fitted, and so do we: the SVM's enter its box ## constraint and the tree's enter the split criterion as a prior ## adjustment, so a model fitted under one cost matrix cannot be made to ## report another. Both measured on R2024a. function this = set.Cost (this, val) consumes_cost = {'SVM', 'Tree'}; if (this.Fitted && any (strcmp (this.CrossValidatedModel, ... consumes_cost))) error (strcat ("ClassificationPartitionedModel: cannot assign", ... " 'Cost' on a cross-validated Classification%s,", ... " whose costs are consumed while it is fitted."), ... this.CrossValidatedModel); endif gnY = this.ClassNames; if (isempty (val)) this.Cost = cast (! eye (classCount (gnY)), 'double'); else ## Everything a cost must be, and the struct form, which ## is permuted into this model's class order. [val, errmsg] = costMatrix (val, gnY); if (! isempty (errmsg)) error ("ClassificationPartitionedModel: %s", errmsg); endif this.Cost = val; endif this = pushToFolds (this, 'Cost', this.Cost); endfunction ## A generative learner scores from the priors it holds, so its folds can ## honour a prior assigned after the fit: the discriminant re-derives its ## coefficients from them and the naive Bayes weights its class densities ## by them. The discriminative learners fitted them in and cannot. ## MATLAB draws the same line, refusing the KNN, the network, the GAM and ## the SVM. function this = set.Prior (this, val) takes_prior = {'Discriminant', 'NaiveBayes'}; if (this.Fitted && ! any (strcmp (this.CrossValidatedModel, takes_prior))) error (strcat ("ClassificationPartitionedModel: 'Prior' can only", ... " be assigned on a cross-validated", ... " ClassificationDiscriminant or", ... " ClassificationNaiveBayes.")); endif if (! this.Fitted) this.Prior = val; return; endif if (isstruct (val)) val = priorFromStruct (val, this.ClassNames, ... 'ClassificationPartitionedModel'); endif if (ischar (val) && strcmpi ('uniform', val)) n = classCount (this.ClassNames); this.Prior = ones (1, n) ./ n; elseif (isempty (val) || (ischar (val) && strcmpi ('empirical', val))) gY = labelIndices (this.ClassNames, this.Y); pr = accumarray (gY(:), 1, [classCount(this.ClassNames), 1]); this.Prior = pr(:)' ./ sum (pr); elseif (isnumeric (val)) if (classCount (this.ClassNames) != numel (val)) error (strcat ("ClassificationPartitionedModel: the elements", ... " in 'Prior' do not correspond to the selected", ... " classes in Y.")); endif this.Prior = val(:)' ./ sum (val); else error (strcat ("ClassificationPartitionedModel: invalid value", ... " for 'Prior'.")); endif this = pushToFolds (this, 'Prior', this.Prior); endfunction function this = set.ScoreTransform (this, val) [f, nm] = parseScoreTransform (val, 'ClassificationPartitionedModel'); this.ScoreTransform = nm; this.STfun = f; endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedModel} {@var{this} =} ClassificationPartitionedModel (@var{Mdl}, @var{Partition}) ## ## Create a @code{ClassificationPartitionedModel} class object for ## cross-validation of classification models. ## ## @code{@var{this} = ClassificationPartitionedModel (@var{Mdl}, ## @var{Partition})} returns a ClassificationPartitionedModel object, with ## @var{Mdl} as the trained classification model object and ## @var{Partition} as the partitioning object obtained using ## @code{cvpartition} ## function. ## ## @seealso{cvpartition} ## @end deftypefn function this = ClassificationPartitionedModel (Mdl, Partition) ## Check input arguments if (nargin < 2) error ("ClassificationPartitionedModel: too few input arguments."); endif ## Check for valid Classification object validTypes = {'ClassificationDiscriminant', 'ClassificationGAM', ... 'ClassificationKNN', 'ClassificationNaiveBayes', ... 'ClassificationNeuralNetwork', 'ClassificationSVM', ... 'ClassificationTree'}; if (! any (strcmp (class (Mdl), validTypes))) error ("ClassificationPartitionedModel: unsupported model type."); endif ## Check for valid cvpartition object if (! strcmp (class (Partition), 'cvpartition')) error ("ClassificationPartitionedModel: invalid 'cvpartition' object."); endif ## Set properties. The rows dropped for missing values are outside the ## partition, so they are dropped here too and every index below, the ## partition's included, refers to the same set of observations. this.X = Mdl.X; this.Y = Mdl.Y; this.KFold = Partition.NumTestSets; this.Trained = cell (this.KFold, 1); this.ClassNames = Mdl.ClassNames; this.ResponseName = Mdl.ResponseName; this.NumObservations = rows (this.X); this.PredictorNames = Mdl.PredictorNames; this.Partition = Partition; ## MATLAB stores a short name here, shared with the regression ## side: 'Discriminant', 'GAM', 'KNN', 'NeuralNetwork', 'SVM'. this.CrossValidatedModel = strrep (class (Mdl), 'Classification', ''); this.ScoreTransform = Mdl.ScoreTransform; this.STfun = Mdl.STfun; ## Every classifier reports a prior and a cost now, so they are carried ## whatever was cross validated; this used to name three of the five. this.Prior = Mdl.Prior; this.Cost = Mdl.Cost; this.W = Mdl.W; this.BinEdges = Mdl.BinEdges; ## Switch Classification object types switch (this.CrossValidatedModel) case 'Discriminant' ## Arguments to pass in fitcdiscr args = {}; ## List of acceptable parameters for fitcdiscr DiscrParams = {'PredictorNames', 'ResponseName', 'ClassNames', ... 'Cost', 'Prior', 'DiscrimType', 'Gamma'}; ## Set parameters for i = 1:numel (DiscrParams) paramName = DiscrParams{i}; paramValue = Mdl.(paramName); if (! isempty (paramValue)) args = [args, {paramName, paramValue}]; endif endfor ## Add 'FillCoeffs' parameter if (isempty (Mdl.Coeffs)) args = [args, {'FillCoeffs', 'off'}]; endif ## Train model according to partition object for k = 1:this.KFold idx = training (this.Partition, k); tmp = fitcdiscr (this.X(idx, :), this.Y(idx,:), args{:}, ... 'Weights', Mdl.RawWeights(idx)); this.Trained{k} = compact (tmp); endfor case 'GAM' ## Arguments to pass in fitcgam args = {}; ## List of acceptable parameters for fitcdiscr ## Which parameters a fold takes depends on which engine fitted ## the parent: the two have disjoint argument surfaces and each ## refuses the other's, so the fold is refitted with its own. if (strcmp (Mdl.FitMethod, 'boostedtrees')) GAMparams = {'PredictorNames', 'ResponseName', 'ClassNames', ... 'Cost', 'CategoricalPredictors'}; else GAMparams = {'PredictorNames', 'ResponseName', 'ClassNames', ... 'Cost', 'Formula', 'Knots', 'Order', ... 'LearningRate', 'NumIterations'}; endif args = [args, {'FitMethod', Mdl.FitMethod}]; ## Set parameters for i = 1:numel (GAMparams) paramName = GAMparams{i}; paramValue = Mdl.(paramName); if (! isempty (paramValue)) args = [args, {paramName, paramValue}]; endif endfor ## Interactions now holds the fitted pairs, which the constructor ## does not take as a specification. The term matrix does, and it ## reproduces the parent's terms exactly rather than re-selecting ## them. A formula names its own terms and is passed instead, so ## this must not be passed alongside one. if (strcmp (Mdl.FitMethod, 'boostedtrees')) ## The tree engine holds its interactions as predictor pairs and ## takes them back as a term matrix, so the pairs are widened ## into one before they are handed to the fold. if (! isempty (Mdl.Interactions)) IM = false (rows (Mdl.Interactions), Mdl.NumPredictors); for q = 1:rows (Mdl.Interactions) IM(q, Mdl.Interactions(q,:)) = true; endfor args = [args, {'Interactions', IM}]; endif elseif (isempty (Mdl.Formula) && ! isempty (Mdl.IntMatrix)) args = [args, {'Interactions', Mdl.IntMatrix}]; endif ## Train model according to partition object. The GAM is the one ## backing whose fold prior is not the parent's: MATLAB carries the ## prior as observation weight and refits it from the rows the fold ## kept, so a fold holding a class in a different proportion from ## the whole reports a different prior. The other five take the ## parent's value unchanged. ## One prior per entry of the model's own, which a response holding ## a single class still carries two of. gY = labelIndices (this.ClassNames, this.Y); nclass = numel (this.Prior); for k = 1:this.KFold idx = training (this.Partition, k); pf = gamFoldPrior (double (this.W), gY, nclass, idx); fargs = [args, {'Prior', pf}]; ## A boosted-tree fold also keeps the weights of the rows it holds. if (strcmp (Mdl.FitMethod, 'boostedtrees')) fargs = [fargs, {'Weights', this.W(idx)}]; endif tmp = fitcgam (this.X(idx, :), this.Y(idx,:), fargs{:}); this.Trained{k} = compact (tmp); endfor ## The model's own, not a list restated here. The names this ## branch used to gather were the spline engine's, and the fit has ## been boosted trees by default since 2026-08-24, so the struct it ## built described a scheme the folds were no longer fitted under. case 'KNN' ## Arguments to pass in fitcknn args = {}; ## List of acceptable parameters for fitcknn. ScoreTransform is ## deliberately absent: the parent applies it to the assembled ## scores, so a fold carrying it too would apply it twice. KNNparams = {'PredictorNames', 'ResponseName', 'ClassNames', ... 'Prior', 'Cost', 'BreakTies', ... 'NSMethod', 'BucketSize', 'CacheSize', ... 'NumNeighbors', 'Exponent', ... 'Scale', 'Cov', 'Distance', 'DistanceWeight', ... 'IncludeTies'}; ## Set parameters for i = 1:numel (KNNparams) paramName = KNNparams{i}; if (isprop (Mdl, paramName)) paramValue = Mdl.(paramName); if (! isempty (paramValue)) args = [args, {paramName, paramValue}]; endif else switch (paramName) case 'Cov' if (strcmpi (Mdl.Distance, 'mahalanobis') && ... (! isempty (Mdl.DistParameter))) args = [args, {'Cov', Mdl.DistParameter}]; endif case 'Exponent' if (strcmpi (Mdl.Distance,'minkowski') && ... (! isempty (Mdl.DistParameter))) args = [args, {'Exponent', Mdl.DistParameter}]; endif case 'Scale' if (strcmpi (Mdl.Distance,'seuclidean') && ... (! isempty (Mdl.DistParameter))) args = [args, {'Scale', Mdl.DistParameter}]; endif endswitch endif endfor ## Standardization is told by Mu, and is passed on: leaving it out ## refitted every fold on the raw scale while the parent reported ## itself standardized. It is passed only when it is on, the ## constructor taking at most one of Standardize, Scale and Cov, so ## a model carrying either of those was never standardized. if (! isempty (Mdl.Mu)) args = [args, {'Standardize', true}]; endif ## A categorical model takes every predictor as categorical if (! isempty (Mdl.CategoricalPredictors)) args = [args, {'CategoricalPredictors', 'all'}]; endif ## Train model according to partition object for k = 1:this.KFold idx = training (this.Partition, k); this.Trained{k} = fitcknn (this.X(idx, :), this.Y(idx,:), ... args{:}, 'Weights', Mdl.RawWeights(idx)); endfor case 'NaiveBayes' ## Arguments to pass in fitcnb. ScoreTransform is deliberately ## absent, as it is for the KNN above: the parent applies it to the ## assembled scores, so a fold carrying it too would apply it twice. args = {}; NBparams = {'PredictorNames', 'ResponseName', 'ClassNames', ... 'Prior', 'Cost', 'DistributionNames'}; for i = 1:numel (NBparams) paramName = NBparams{i}; paramValue = Mdl.(paramName); if (! isempty (paramValue)) args = [args, {paramName, paramValue}]; endif endfor ## The kernel settings are taken from the request the model records ## rather than from the resolved properties: a fold is fitted the ## way the model was asked for, and resolving them again per fold is ## what lets each fold choose its own bandwidth, as it must. MP = Mdl.ModelParameters; if (! isempty (MP.Kernel)) args = [args, {'Kernel', MP.Kernel}]; endif if (! isempty (MP.Support)) args = [args, {'Support', MP.Support}]; endif if (! (isempty (MP.Width) || all (isnan (MP.Width(:))))) args = [args, {'Width', MP.Width}]; endif ## Train model according to partition object. The fold is stored ## compact, as MATLAB stores it: a naive Bayes fold is a set of ## fitted densities and needs none of the observations it was fitted ## on. Measured on R2024a, where Trained{k} is a ## CompactClassificationNaiveBayes. ## A model fitted with weights passes each fold the weights of its ## rows, X holding only the rows the model used. Within a class W ## is in proportion to the weights given, which is all a fold reads. wAll = []; if (! isempty (Mdl.RawWeights)) wAll = double (Mdl.W(:)); endif for k = 1:this.KFold idx = training (this.Partition, k); fargs = args; if (! isempty (wAll)) fargs = [fargs, {'Weights', wAll(idx)}]; endif tmp = fitcnb (this.X(idx, :), this.Y(idx,:), fargs{:}); this.Trained{k} = compact (tmp); endfor ## Store ModelParameters to ClassificationPartitionedModel object case 'NeuralNetwork' ## Arguments to pass in fitcnet args = {}; ## List of acceptable parameters for fitcnet. ScoreTransform is ## deliberately absent, as it is for the KNN above. NNparams = {'PredictorNames', 'ResponseName', 'ClassNames', ... 'Cost', 'Prior', 'CategoricalPredictors', ... 'LayerSizes', ... 'Activations', 'OutputLayerActivation', ... 'IterationLimit', 'DisplayInfo'}; ## Set parameters for i = 1:numel (NNparams) paramName = NNparams{i}; paramValue = Mdl.(paramName); if (! isempty (paramValue)) args = [args, {paramName, paramValue}]; endif endfor ## The folds must be trained by the solver the parent was trained ## by, and the solver decides which of the remaining options are ## legal: a learning rate belongs to the epoch loop alone. Solver ## reports a display name, so it is mapped back to the option. if (strcmp (Mdl.Solver, 'LBFGS')) args = [args, {'Solver', 'lbfgs'}]; else args = [args, {'Solver', 'sgd', 'LearningRate', Mdl.LearningRate}]; endif stdz = ! isempty (Mdl.Mu); args = [args, {'Standardize', stdz}]; ## A model fitted with weights passes each fold the weights of its ## rows. Weights given vary within a class, which a prior alone ## never makes them do; a model without them leaves each fold to its ## own empirical prior, as before. wAll = []; gW = labelIndices (Mdl.ClassNames, this.Y); for c = 1:max (gW) wc = double (this.W(gW == c)); if (numel (wc) > 1 && max (wc) - min (wc) > 1e-12 * max (wc)) wAll = this.W; endif endfor ## Train model according to partition object for k = 1:this.KFold idx = training (this.Partition, k); fargs = args; if (! isempty (wAll)) fargs = [fargs, {'Weights', wAll(idx)}]; endif tmp = fitcnet (this.X(idx, :), this.Y(idx,:), fargs{:}); this.Trained{k} = compact (tmp); endfor ## The model's own, rather than a list of names restated here. case 'SVM' ## Get ModelParameters structure from ClassificationSVM object params = Mdl.ModelParameters; ## The polynomial order is recorded for the polynomial kernel ## alone, so it is passed only when the parent carries one. korder = {}; if (! isempty (params.KernelPolynomialOrder)) korder = {'PolynomialOrder', params.KernelPolynomialOrder}; endif ## Train model according to partition object for k = 1:this.KFold idx = training (this.Partition, k); ## Pass all arguments directly to fitcsvm tmp = fitcsvm (this.X(idx, :), this.Y(idx,:), ... 'Standardize', ! isempty (Mdl.Mu), ... 'CategoricalPredictors', ... Mdl.CategoricalPredictors, ... 'PredictorNames', Mdl.PredictorNames, ... 'ResponseName', Mdl.ResponseName, ... 'ClassNames', Mdl.ClassNames, ... 'Cost', Mdl.Cost, 'Prior', Mdl.Prior, ... 'SVMtype', params.SVMtype, ... 'KernelFunction', params.KernelFunction, ... korder{:}, ... 'KernelScale', params.KernelScale, ... 'KernelOffset', params.KernelOffset, ... 'BoxConstraint', params.BoxConstraint, ... 'Nu', params.Nu, ... 'CacheSize', params.CacheSize, ... 'Tolerance', params.Tolerance, ... 'Shrinking', params.Shrinking, ... 'Weights', Mdl.RawWeights(idx)); this.Trained{k} = compact (tmp); endfor ## The model's own, rather than the argument struct this branch ## assembled to refit the folds with. case 'Tree' ## The arguments a fold is grown with, which cvloss shares. args = treeFoldArgs (Mdl); ## Train model according to partition object. The fold is stored ## compact, as MATLAB stores it: measured on R2024a, where ## Trained{k} is a CompactClassificationTree. The weights are ## sliced per fold, this being the one backing whose constructor ## takes them. W = Mdl.RawWeights; for k = 1:this.KFold idx = training (this.Partition, k); tmp = fitctree (this.X(idx, :), this.Y(idx,:), args{:}, ... 'Weights', W(idx)); this.Trained{k} = compact (tmp); endfor ## The model's own, rather than a list of names restated here. endswitch ## The learner's parameters, under this class's own tags. MATLAB ## reports an EnsembleParams here instead and so says nothing about the ## fit; see the ModelParameters property for the deviation. this.ModelParameters = partitionedModelParams (Mdl, this.KFold, ... 'PartitionedModel', 'classification'); ## No fold carries the transform: the parent applies it once to the ## assembled score. Cleared here rather than at each fit call so that ## a backing added later cannot reintroduce the double application by ## inheriting a class default of its own, which is how a cross ## validated GAM came to transform twice. The KNN case stores a full ## ClassificationKNN where the other four store compacts, and both ## accept the assignment. ## ## Safe because the transform here is a caller's preference and not ## fitted content. A fitted transform would not be: fitPosterior ## installs a sigmoid it has estimated, so if that is ever implemented ## through crossval it must not be cleared here. this = pushToFolds (this, 'ScoreTransform', 'none'); ## Gathered across the folds, which is the shape MATLAB reports: one ## structure carrying a row per phase rather than a structure per fold. if (strcmp (this.CrossValidatedModel, 'GAM')) pt = zeros (1, this.KFold); it = zeros (1, this.KFold); boosted = true; for k = 1:this.KFold nt = this.Trained{k}.NumTrainedTrees; ## A spline fit counts no trees, so there is nothing to report and ## the property stays empty rather than claiming zero of them. if (isempty (nt)) boosted = false; break; endif pt(k) = nt.PredictorTrees; it(k) = nt.InteractionTrees; endfor if (boosted) this.NumTrainedPerFold = struct ('PredictorTrees', pt, ... 'InteractionTrees', it); endif endif this.Fitted = true; endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedModel} {@var{label} =} kfoldPredict (@var{this}) ## @deftypefnx {ClassificationPartitionedModel} {[@var{label}, @var{score}, @var{cost}] =} kfoldPredict (@var{this}) ## ## Predict responses for observations not used for training in a ## cross-validated classification model. ## ## @code{@var{[label, Score, Cost]} = kfoldPredict (@var{this})} ## returns the predicted class labels, classification scores, and ## classification costs for the data used ## to train the cross-validated model @var{this}. ## ## @var{this} is a @code{ClassificationPartitionedModel} object. ## The function predicts the response for each observation that was ## held out during training in the cross-validation process. ## ## An observation that no fold held out is not predicted at all: its ## scores and costs are @qcode{NaN} and its label is missing, an empty ## character vector for a cell array of strings and @qcode{NaN} for a ## numeric response. Under a @qcode{'Holdout'} partition that is every ## observation outside the holdout set. @strong{This differs from ## MATLAB}, which reports @qcode{NaN} scores for those rows as we do but ## labels every one of them with the @emph{first} class, whatever their ## response: that label is the least-cost class of a row of @qcode{NaN} ## costs rather than a prediction any model made, and naming a class for ## an observation nothing scored would be wrong. A logical response has ## no missing value to give, so those rows stay @qcode{false}. ## ## @multitable @columnfractions 0.28 0.7 ## @headitem @var{Output} @tab @var{Description} ## ## @item @qcode{label} @tab Predicted class labels, returned as a ## vector or cell array. The type of @var{label} matches the type of ## @var{Y} in the original training data. Each element of @var{label} ## corresponds to the predicted class ## label for the corresponding row in @var{X}. ## ## @item @qcode{Score} @tab Classification scores, returned as a ## numeric matrix. Each row of @var{Score} corresponds to an observation, ## and each column corresponds to a class. The value in row @var{i} and ## column @var{j} is the ## classification score for class @var{j} for observation @var{i}. ## ## @item @qcode{Cost} @tab Classification costs, returned as a ## numeric matrix. Each row of @var{Cost} corresponds to an observation, ## and each column corresponds to a class. The value in row @var{i} ## and column @var{j} is the classification cost for class @var{j} for ## observation @var{i}. This output is optional and only returned if ## requested. ## @end multitable ## ## @seealso{ClassificationKNN, ClassificationSVM, ## ClassificationPartitionedModel} ## @end deftypefn function [label, Score, Cost] = kfoldPredict (this) ## Input validation ## A cross-validated GAM reports no cost in MATLAB either: predict ## refuses a third output on the full and the compact class alike, and ## so does kfoldPredict. Measured on R2024a. refuse_cost = {'GAM'}; if (any (strcmp (this.CrossValidatedModel, refuse_cost)) && nargout > 2) error (strcat ("ClassificationPartitionedModel.kfoldPredict:", ... " 'Cost' output is not supported for %s cross", ... " validated models."), this.CrossValidatedModel); endif ## The label vector starts missing rather than empty, so an observation ## no fold tests is reported as missing instead of carrying a class. ## Under a holdout partition that is most of them. A logical response ## has no missing value to give, so those rows stay false. label = missingLabels (this.Y, this.NumObservations); ## Initialize the score and cost matrices Score = nan (this.NumObservations, classCount (this.ClassNames)); Cost = nan (this.NumObservations, classCount (this.ClassNames)); ## Predict label, score, and cost (if applicable) for each KFold partition for k = 1:this.KFold ## Get data and trained model for this fold testIdx = test (this.Partition, k); model = this.Trained{k}; ## Train. A fold whose predict returns two outputs still carries a ## posterior in its score, so the expected cost is formed here rather ## than asked of the fold. MATLAB reports one for a network backing ## although its own network predict refuses the output. ## A tree's third output is the node an observation landed in, not ## a cost, so its expected cost is formed here from the score as the ## two-output backings' is. MATLAB reports one for all three. no_cost_models = {'GAM', 'NeuralNetwork', 'Tree'}; if (any (strcmp (this.CrossValidatedModel, no_cost_models))) [predictedLabel, score] = predict (model, this.X(testIdx, :)); if (nargout > 2) cost = score * this.Cost; endif else [predictedLabel, score, cost] = predict (model, this.X(testIdx, :)); endif ## Convert cell array of labels to appropriate type (if applicable) if (iscell (predictedLabel)) if (isnumeric (this.Y)) predictedLabel = cellfun (@str2num, predictedLabel); elseif (islogical (this.Y)) predictedLabel = cellfun (@logical, predictedLabel); elseif (iscellstr (this.Y)) predictedLabel = predictedLabel; endif endif ## Get labels, score, and cost (if applicable). The rows are ## assigned and not the elements: a character matrix of labels was ## allocated one name per row above, and a linear index would write ## down its first column. label(testIdx, :) = predictedLabel; Score(testIdx, :) = score; if (nargout > 2) Cost(testIdx, :) = cost; endif endfor ## The folds never carry the transform: assigning ScoreTransform on a ## cross-validated model leaves every Trained{k} at 'none', in MATLAB as ## here, and it is applied once to the assembled scores instead. It used ## to be accepted and never read, so the property was inert on every ## cross-validated classifier. kfoldLoss reads these scores, so the ## margin-based losses follow the transform as they should. Score = this.STfun (Score); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedModel} {@var{L} =} kfoldLoss (@var{obj}) ## @deftypefnx {ClassificationPartitionedModel} {@var{L} =} kfoldLoss (@dots{}, @var{name}, @var{value}) ## ## Compute the cross-validated classification loss. ## ## @code{@var{L} = kfoldLoss (@var{obj})} returns the fraction of ## observations the folds misclassify, each answered for by the fold's ## model that did not see it, which is what @code{kfoldPredict} returns. ## ## @itemize ## @item ## @var{obj} must be a @qcode{ClassificationPartitionedModel} object. ## @end itemize ## ## @code{@var{L} = kfoldLoss (@dots{}, @var{name}, @var{value})} accepts ## the following @qcode{Name-Value} pairs. ## ## @multitable @columnfractions 0.24 0.76 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'LossFun'} @tab @qcode{'classiferror'}, the default, ## @qcode{'classifcost'}, @qcode{'mincost'}, or a function handle called ## as ## @code{@var{lossfun} (@var{C}, @var{S}, @var{W}, @var{Cost})}, where ## @var{C} is a logical matrix with one true per row marking the true ## class, @var{S} the scores, @var{W} the weights and @var{Cost} the ## misclassification cost. ## ## @item @qcode{'Mode'} @tab @qcode{'average'}, the default, which returns ## one number over the observations of every fold asked for, or ## @qcode{'individual'}, which returns one number per fold. ## ## @item @qcode{'Folds'} @tab A vector of fold indices to restrict the ## loss to. It defaults to every fold. ## @end multitable ## ## @seealso{ClassificationPartitionedModel, ## ClassificationPartitionedModel.kfoldPredict} ## @end deftypefn function L = kfoldLoss (this, varargin) if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationPartitionedModel.kfoldLoss:", ... " Name-Value arguments must be in pairs.")); endif ## Only the losses whose value has been measured against R2024a are ## offered. The margin-based ones are not: ours came out 2x MATLAB's ## for hinge and 4x for quadratic, the square of the same factor, which ## says its margin is not the score difference this would use. Shipping ## a number that close to right and not right is worse than not ## shipping it. names = {'classifcost', 'classiferror', 'mincost'}; ## Parse optional paired arguments optNames = {'LossFun', 'Mode', 'Folds'}; dfValues = {'classiferror', 'average', 1:this.KFold}; [LossFun, Mode, Folds, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (is_function_handle (LossFun) || (ischar (LossFun) && isrow (LossFun)))) error (strcat ("ClassificationPartitionedModel.kfoldLoss:", ... " 'LossFun' must be a character vector or a", ... " function handle.")); endif if (ischar (LossFun)) LossFun = tolower (LossFun); if (! any (strcmp (LossFun, names))) error (strcat ("ClassificationPartitionedModel.kfoldLoss:", ... " unsupported 'LossFun' value.")); endif endif if (! (ischar (Mode) && isrow (Mode) && any (strcmpi (Mode, {'average', 'individual'})))) error (strcat ("ClassificationPartitionedModel.kfoldLoss: 'Mode'", ... " must be either 'average' or 'individual'.")); endif if (! (isnumeric (Folds) && isvector (Folds) && all (Folds == fix (Folds)) && all (Folds >= 1) && all (Folds <= this.KFold))) error (strcat ("ClassificationPartitionedModel.kfoldLoss: 'Folds'", ... " must be a vector of fold indices between 1 and", ... " KFold.")); endif if (! isempty (args)) error (strcat ("ClassificationPartitionedModel.kfoldLoss: invalid", ... " optional paired argument.")); endif ## Every observation answered for by the fold that held it out [label, Score] = kfoldPredict (this); classes = this.ClassNames; K = classCount (classes); n = rows (this.X); ## Index of the true and the predicted class, so the cost matrix and ## the score matrix can be addressed by the same column. true_idx = zeros (n, 1); pred_idx = zeros (n, 1); for i = 1:n t = find (ismember (classes, this.Y(i))); p = find (ismember (classes, label(i))); if (! isempty (t)) true_idx(i) = t(1); endif if (! isempty (p)) pred_idx(i) = p(1); endif endfor if (strcmpi (Mode, 'individual')) L = nan (numel (Folds), 1); for i = 1:numel (Folds) idx = test (this.Partition, Folds(i)); L(i) = foldLoss_ (this, idx, Score, true_idx, pred_idx, LossFun, K); endfor else idx = false (n, 1); for i = 1:numel (Folds) idx = idx | test (this.Partition, Folds(i)); endfor L = foldLoss_ (this, idx, Score, true_idx, pred_idx, LossFun, K); endif endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedModel} {@var{m} =} kfoldMargin (@var{obj}) ## ## Classification margins of the cross-validated observations. ## ## @code{@var{m} = kfoldMargin (@var{obj})} returns an @math{Nx1} vector ## holding, for every observation, the score its own fold's model gave the ## true class less the largest score that model gave any other class. A ## larger margin is a more confident correct answer and a negative one is ## a misclassification. Every observation is scored by the fold that held ## it out, so no model answers for a row it was trained on. ## ## @itemize ## @item ## @var{obj} must be a @qcode{ClassificationPartitionedModel} object. ## @end itemize ## ## Where the fold that held an observation out produced no score for it, ## the margin is @qcode{NaN}. This method takes no optional arguments, ## as MATLAB's does not. ## ## @end deftypefn function m = kfoldMargin (this) m = foldMargin_ (this); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedModel} {@var{e} =} kfoldEdge (@var{obj}) ## @deftypefnx {ClassificationPartitionedModel} {@var{e} =} kfoldEdge (@dots{}, @var{name}, @var{value}) ## ## Classification edge of the cross-validated observations. ## ## @code{@var{e} = kfoldEdge (@var{obj})} returns the mean of the ## classification margins over every cross-validated observation, which is ## the mean of @code{kfoldMargin (@var{obj})}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{ClassificationPartitionedModel} object. ## @end itemize ## ## @code{@var{e} = kfoldEdge (@dots{}, @var{name}, @var{value})} accepts ## the following @qcode{Name-Value} pairs. ## ## @multitable @columnfractions 0.24 0.76 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Mode'} @tab @qcode{'average'}, the default, which returns ## one number over the observations of every fold asked for, or ## @qcode{'individual'}, which returns one number per fold. ## ## @item @qcode{'Folds'} @tab A vector of fold indices to restrict the ## edge to. It defaults to every fold. ## @end multitable ## ## The observations of a selection are weighted uniformly and normalized ## over that selection, so a subset of folds is an average rather than a ## sum, exactly as @code{kfoldLoss} does. ## ## @end deftypefn function e = kfoldEdge (this, varargin) if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationPartitionedModel.kfoldEdge:", ... " Name-Value arguments must be in pairs.")); endif ## Parse optional paired arguments optNames = {'Mode', 'Folds'}; dfValues = {'average', 1:this.KFold}; [Mode, Folds, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (ischar (Mode) && isrow (Mode) && any (strcmpi (Mode, {'average', 'individual'})))) error (strcat ("ClassificationPartitionedModel.kfoldEdge: 'Mode'", ... " must be either 'average' or 'individual'.")); endif if (! (isnumeric (Folds) && isvector (Folds) && all (Folds == fix (Folds)) && all (Folds >= 1) && all (Folds <= this.KFold))) error (strcat ("ClassificationPartitionedModel.kfoldEdge: 'Folds'", ... " must be a vector of fold indices between 1 and", ... " KFold.")); endif if (! isempty (args)) error (strcat ("ClassificationPartitionedModel.kfoldEdge: invalid", ... " optional paired argument.")); endif m = foldMargin_ (this); n = rows (this.X); if (strcmpi (Mode, 'individual')) e = nan (numel (Folds), 1); for i = 1:numel (Folds) idx = test (this.Partition, Folds(i)); e(i) = foldEdge_ (this, m, idx); endfor else idx = false (n, 1); for i = 1:numel (Folds) idx = idx | test (this.Partition, Folds(i)); endfor e = foldEdge_ (this, m, idx); endif endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationPartitionedModel} {@var{vals} =} kfoldfun (@var{obj}, @var{fun}) ## ## Apply a function to each fold of a cross-validated model. ## ## @code{@var{vals} = kfoldfun (@var{obj}, @var{fun})} calls @var{fun} once ## per fold and returns a @math{K*M} numeric matrix whose row @math{k} is ## what @var{fun} returned for fold @math{k}. ## ## @var{fun} is a function handle taking seven inputs and returning a ## numeric vector of the same length every time it is called: ## ## @example ## @var{testvals} = @var{fun} (@var{M}, @var{Xtrain}, @var{Ytrain}, @dots{} ## @var{Wtrain}, @var{Xtest}, @var{Ytest}, @var{Wtest}) ## @end example ## ## @var{M} is the model the fold was fitted with, taken from ## @code{@var{obj}.Trained@{k@}}; @var{Xtrain}, @var{Ytrain} and ## @var{Wtrain} are the predictors, response and weights of the ## observations that fold was trained on, and @var{Xtest}, @var{Ytest} and ## @var{Wtest} those of the observations it held out. ## ## @seealso{ClassificationPartitionedModel, ## ClassificationPartitionedModel.kfoldPredict, ## ClassificationPartitionedModel.kfoldLoss, ## ClassificationPartitionedModel.kfoldMargin, ## ClassificationPartitionedModel.kfoldEdge} ## @end deftypefn function vals = kfoldfun (this, fun) if (nargin < 2) error ("ClassificationPartitionedModel.kfoldfun: too few input arguments."); endif if (! is_function_handle (fun)) error ("ClassificationPartitionedModel.kfoldfun: FUN must be a function handle."); endif vals = []; for k = 1:this.KFold trIdx = training (this.Partition, k); teIdx = test (this.Partition, k); tv = fun (this.Trained{k}, this.X(trIdx,:), this.Y(trIdx,:), ... this.W(trIdx), this.X(teIdx,:), this.Y(teIdx,:), ... this.W(teIdx)); ## The returned values become one row, so they have to be numeric and ## of one length: a fold answering with a different width could not be ## stacked with the others, and finding that out at the concatenation ## would name neither the fold nor the reason. if (! ((isnumeric (tv) || islogical (tv)) && isvector (tv))) error (strcat ("ClassificationPartitionedModel.kfoldfun: FUN must", ... " return a numeric vector; fold %d returned a %s."), ... k, class (tv)); endif tv = tv(:)'; if (! isempty (vals) && numel (tv) != columns (vals)) error (strcat ("ClassificationPartitionedModel.kfoldfun: FUN must", ... " return the same number of values for every fold;", ... " fold %d returned %d where the first returned", ... " %d."), k, numel (tv), columns (vals)); endif vals = [vals; tv]; endfor endfunction endmethods methods (Access = private) ## Margin of every observation from the fold that held it out: the score ## of the true class less the largest score given any other class. NaN ## where the response is not one of the trained classes, since a margin ## needs a true class to measure from. function m = foldMargin_ (this) [~, Score] = kfoldPredict (this); classes = this.ClassNames; K = classCount (classes); n = rows (this.X); m = nan (n, 1); for i = 1:n t = find (ismember (classes, this.Y(i))); if (isempty (t)) continue; endif t = t(1); if (K < 2) ## Degenerate single-class model: nothing competes, so the score ## itself is the whole margin. m(i) = Score(i, t); else other = Score(i, [1:t-1, t+1:K]); m(i) = Score(i, t) - max (other); endif endfor endfunction ## Edge over the observations selected by IDX: the margins weighted ## uniformly and normalized over that selection, so a subset of folds is ## an average rather than a sum, as foldLoss_ does for the losses. function e = foldEdge_ (this, m, idx) if (! any (idx)) e = NaN; return; endif mi = m(idx); e = sum (mi) / numel (mi); endfunction ## Loss over the observations selected by IDX, weighted uniformly and ## normalized over that selection, so a subset of folds is an average ## rather than a sum. function L = foldLoss_ (this, idx, Score, true_idx, pred_idx, LossFun, K) if (! any (idx) || ! any (true_idx(idx))) L = NaN; return; endif S = Score(idx, :); ti = true_idx(idx); pi_ = pred_idx(idx); m = sum (idx); W = ones (m, 1) / m; if (is_function_handle (LossFun)) C = false (m, K); C(sub2ind ([m, K], (1:m)', ti)) = true; L = LossFun (C, S, W, this.Cost); if (! (isnumeric (L) && isscalar (L))) error (strcat ("ClassificationPartitionedModel.kfoldLoss:", ... " 'LossFun' must return a numeric scalar.")); endif return; endif switch (LossFun) case 'classiferror' L = sum (W .* (pi_ != ti)); case 'classifcost' L = 0; for i = 1:m L = L + W(i) * this.Cost(ti(i), pi_(i)); endfor case 'mincost' L = 0; for i = 1:m [~, k] = min (S(i,:) * this.Cost); L = L + W(i) * this.Cost(ti(i), k); endfor endswitch endfunction ## Carry an assigned property down into every fold. MATLAB's Cost and ## Prior reach the fold models, so kfoldPredict and kfoldLoss answer under ## what was assigned rather than under what was fitted. The folds are ## absent while the constructor is still running, and the two properties ## are only ever pushed for the learners that accept the assignment. function this = pushToFolds (this, name, val) for k = 1:numel (this.Trained) if (! isempty (this.Trained{k})) this.Trained{k}.(name) = val; endif endfor endfunction endmethods endclassdef ## The prior a GAM fold is fitted with. The parent's prior is carried as the ## observation weight W, prior(k) spread over class k in proportion to the ## weights given, and the fold's prior is the weight it retained, renormalised. ## Without weights W is prior(k) over the class count, measured on R2024a over ## ten folds of three and seven with five distinct compositions, exact to eight ## digits; an empirical parent prior makes the weight the same for every class, ## so the fold's prior collapses to its own proportions, which is what a ## default fit reports. function pf = gamFoldPrior (W, gY, nclass, idx) gf = gY(idx); pf = accumarray (gf(:), W(idx), [nclass, 1])'; pf = pf ./ sum (pf); endfunction %!demo %! %! load fisheriris %! x = meas; %! y = species; %! %! ## Create a KNN classifier model %! obj = fitcknn (x, y, 'NumNeighbors', 5, 'Standardize', 1); %! %! ## Create a partition for 5-fold cross-validation %! partition = cvpartition (y, 'KFold', 5); %! %! ## Create the ClassificationPartitionedModel object %! cvModel = crossval (obj, 'cvPartition', partition) %!demo %! %! load fisheriris %! x = meas; %! y = species; %! %! ## Create a KNN classifier model %! obj = fitcknn (x, y, 'NumNeighbors', 5, 'Standardize', 1); %! %! ## Create the ClassificationPartitionedModel object %! cvModel = crossval (obj); %! %! ## Predict the class labels for the observations not used for training %! [label, score, cost] = kfoldPredict (cvModel); %! fprintf ("Cross-validated accuracy = %1.2f%% (%d/%d)\n", ... %! sum (strcmp (label, y)) / numel (y) *100, ... %! sum (strcmp (label, y)), numel (y)) ## Tests %!test %! load fisheriris %! a = fitcdiscr (meas, species, 'gamma', 0.3); %! cvModel = crossval (a, 'KFold', 5); %! assert_equal (class (cvModel), "ClassificationPartitionedModel"); %! assert_equal (cvModel.NumObservations, 150); %! assert_equal (numel (cvModel.Trained), 5); %! assert_equal (class (cvModel.Trained{1}), "CompactClassificationDiscriminant"); %! assert_equal (cvModel.CrossValidatedModel, "Discriminant"); %! assert_equal (cvModel.KFold, 5); %!test %! load fisheriris %! a = fitcdiscr (meas, species, 'gamma', 0.5, 'fillcoeffs', 'off'); %! cvModel = crossval (a, 'HoldOut', 0.3); %! assert_equal (class (cvModel), "ClassificationPartitionedModel"); %! assert_equal ({cvModel.X, cvModel.Y}, {meas, species}); %! assert_equal (cvModel.NumObservations, 150); %! assert_equal (numel (cvModel.Trained), 1); %! assert_equal (class (cvModel.Trained{1}), "CompactClassificationDiscriminant"); %! assert_equal (cvModel.CrossValidatedModel, "Discriminant"); %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = fitcgam (x, y, 'Interactions', 'all'); %! cvModel = crossval (a, 'KFold', 2); %! assert_equal (class (cvModel), "ClassificationPartitionedModel"); %! assert_equal (cvModel.NumObservations, 4); %! assert_equal (numel (cvModel.Trained), 2); %! assert_equal (class (cvModel.Trained{1}), "CompactClassificationGAM"); %! assert_equal (cvModel.CrossValidatedModel, "GAM"); %! assert_equal (cvModel.KFold, 2); %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = fitcgam (x, y); %! cvModel = crossval (a, 'LeaveOut', 'on'); %! assert_equal (class (cvModel), "ClassificationPartitionedModel"); %! assert_equal ({cvModel.X, cvModel.Y}, {x, y}); %! assert_equal (cvModel.NumObservations, 4); %! assert_equal (numel (cvModel.Trained), 4); %! assert_equal (class (cvModel.Trained{1}), "CompactClassificationGAM"); %! assert_equal (cvModel.CrossValidatedModel, "GAM"); %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = fitcknn (x, y); %! partition = cvpartition (y, 'KFold', 2); %! cvModel = ClassificationPartitionedModel (a, partition); %! assert_equal (class (cvModel), "ClassificationPartitionedModel"); %! assert_equal (class (cvModel.Trained{1}), "ClassificationKNN"); %! assert_equal (cvModel.NumObservations, 4); %! assert_equal (cvModel.ModelParameters.NumNeighbors, 1); %! assert_equal (cvModel.ModelParameters.NSMethod, "kdtree"); %! assert_equal (cvModel.ModelParameters.Distance, "euclidean"); %! assert_equal (isempty (cvModel.Trained{1}.Mu), true); %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = fitcknn (x, y, 'NSMethod', 'exhaustive'); %! partition = cvpartition (y, 'HoldOut', 0.2); %! cvModel = ClassificationPartitionedModel (a, partition); %! assert_equal (class (cvModel), "ClassificationPartitionedModel"); %! assert_equal (class (cvModel.Trained{1}), "ClassificationKNN"); %! assert_equal ({cvModel.X, cvModel.Y}, {x, y}); %! assert_equal (cvModel.NumObservations, 4); %! assert_equal (cvModel.ModelParameters.NumNeighbors, 1); %! assert_equal (cvModel.ModelParameters.NSMethod, "exhaustive"); %! assert_equal (cvModel.ModelParameters.Distance, "euclidean"); %! assert_equal (isempty (cvModel.Trained{1}.Mu), true); %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! k = 2; %! a = fitcknn (x, y, 'NumNeighbors' ,k); %! partition = cvpartition (numel (y), 'LeaveOut'); %! cvModel = ClassificationPartitionedModel (a, partition); %! assert_equal (class (cvModel), "ClassificationPartitionedModel"); %! assert_equal (class (cvModel.Trained{1}), "ClassificationKNN"); %! assert_equal ({cvModel.X, cvModel.Y}, {x, y}); %! assert_equal (cvModel.NumObservations, 4); %! assert_equal (cvModel.ModelParameters.NumNeighbors, k); %! assert_equal (cvModel.ModelParameters.NSMethod, "kdtree"); %! assert_equal (cvModel.ModelParameters.Distance, "euclidean"); %! assert_equal (isempty (cvModel.Trained{1}.Mu), true); %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = {'a'; 'a'; 'b'; 'b'}; %! a = fitcnet (x, y, 'IterationLimit', 50); %! cvModel = crossval (a, 'KFold', 2); %! assert_equal (class (cvModel), "ClassificationPartitionedModel"); %! assert_equal (cvModel.NumObservations, 4); %! assert_equal (numel (cvModel.Trained), 2); %! assert_equal (class (cvModel.Trained{1}), "CompactClassificationNeuralNetwork"); %! assert_equal (cvModel.CrossValidatedModel, "NeuralNetwork"); %! assert_equal (cvModel.KFold, 2); %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = {'a'; 'a'; 'b'; 'b'}; %! a = fitcnet (x, y, 'LayerSizes', [5, 3]); %! cvModel = crossval (a, 'LeaveOut', 'on'); %! assert_equal (class (cvModel), "ClassificationPartitionedModel"); %! assert_equal ({cvModel.X, cvModel.Y}, {x, y}); %! assert_equal (cvModel.NumObservations, 4); %! assert_equal (numel (cvModel.Trained), 4); %! assert_equal (class (cvModel.Trained{1}), "CompactClassificationNeuralNetwork"); %! assert_equal (cvModel.CrossValidatedModel, "NeuralNetwork"); %!test %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! x = meas(inds, 3:4); %! y = grp2idx (species(inds)); %! SVMModel = fitcsvm (x,y); %! CVMdl = crossval (SVMModel, 'KFold', 5); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y}) %! assert_equal (CVMdl.KFold == 5, true) %! assert_equal (class (CVMdl.Trained{1}), "CompactClassificationSVM") %! assert_equal (CVMdl.CrossValidatedModel, "SVM"); %!test %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! x = meas(inds, 3:4); %! y = grp2idx (species(inds)); %! obj = fitcsvm (x, y); %! CVMdl = crossval (obj, 'HoldOut', 0.2); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y}) %! assert_equal (class (CVMdl.Trained{1}), "CompactClassificationSVM") %! assert_equal (CVMdl.CrossValidatedModel, "SVM"); %!test %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! x = meas(inds, 3:4); %! y = grp2idx (species(inds)); %! obj = fitcsvm (x, y); %! CVMdl = crossval (obj, 'LeaveOut', 'on'); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y}) %! assert_equal (class (CVMdl.Trained{1}), "CompactClassificationSVM") %! assert_equal (CVMdl.CrossValidatedModel, "SVM"); ## The KNN short name, the one of the five that no test pinned. %!test %! load fisheriris %! CVMdl = crossval (fitcknn (meas, species), 'KFold', 5); %! assert_equal (CVMdl.CrossValidatedModel, "KNN"); %!test # MATLAB parity: a cross-validated tree, over compact folds %! load fisheriris %! a = fitctree (meas, species); %! cvModel = crossval (a, 'KFold', 5); %! assert_equal (class (cvModel), "ClassificationPartitionedModel"); %! assert_equal (cvModel.CrossValidatedModel, "Tree"); %! assert_equal (class (cvModel.Trained{1}), "CompactClassificationTree"); %! assert_equal (cvModel.KFold, 5); %! assert_equal (cvModel.NumObservations, 150); %! assert_equal (cvModel.Prior, [1/3, 1/3, 1/3], 1e-15); %! assert_equal (cvModel.Cost, [0, 1, 1; 1, 0, 1; 1, 1, 0]); %!test # A tree fold is grown with the parameters the parent was grown with %! load fisheriris %! a = fitctree (meas, species, 'SplitCriterion', 'deviance', ... %! 'MinLeafSize', 5, 'MaxNumSplits', 4); %! cvModel = crossval (a, 'KFold', 3); %! assert_equal (cvModel.ModelParameters.SplitCriterion, 'deviance'); %! assert_equal (cvModel.ModelParameters.MinLeaf, 5); %! assert_equal (cvModel.ModelParameters.MaxSplits, 4); %! assert_equal (sum (cvModel.Trained{1}.IsBranchNode) <= 4, true); %!test # A tree fold carries the parent's classes, prior and cost %! load fisheriris %! a = fitctree (meas, species, 'Weights', (1:150)', ... %! 'Cost', [0, 1, 10; 1, 0, 1; 10, 1, 0]); %! cvModel = crossval (a, 'KFold', 3); %! assert_equal (cvModel.Trained{1}.Prior, a.Prior, 1e-15); %! assert_equal (cvModel.Trained{1}.Cost, a.Cost); %! assert_equal (cvModel.Trained{1}.ClassNames, a.ClassNames); %!test # MATLAB parity: kfoldPredict costs a tree from its score %! load fisheriris %! cvModel = crossval (fitctree (meas, species), 'KFold', 5); %! [label, score, cost] = kfoldPredict (cvModel); %! assert_equal (size (label), [150, 1]); %! assert_equal (cost, score * cvModel.Cost, 1e-14); %! assert_equal (sum (score, 2), ones (150, 1), 1e-14); %! assert_equal (kfoldLoss (cvModel) < 0.2, true); %! assert_equal (size (kfoldMargin (cvModel)), [150, 1]); %!test # kfoldPredict gives categorical labels for a categorical response %! load fisheriris %! c = cvpartition ('CustomPartition', repmat ((1:5)', 30, 1)); %! a = crossval (fitctree (meas, species), 'CVPartition', c); %! b = crossval (fitctree (meas, categorical (species)), 'CVPartition', c); %! label = kfoldPredict (b); %! assert_equal (class (label), 'categorical'); %! assert_equal (cellstr (label), kfoldPredict (a)); %!test # kfoldPredict gives string labels for a string response %! load fisheriris %! c = cvpartition ('CustomPartition', repmat ((1:5)', 30, 1)); %! a = crossval (fitctree (meas, species), 'CVPartition', c); %! b = crossval (fitctree (meas, string (species)), 'CVPartition', c); %! label = kfoldPredict (b); %! assert_equal (class (label), 'string'); %! assert_equal (cellstr (label), kfoldPredict (a)); %!test # a holdout leaves the rows no fold tested %! load fisheriris %! b = crossval (fitctree (meas, categorical (species)), 'Holdout', 0.2); %! assert_equal (sum (isundefined (kfoldPredict (b))), 120); ## Test input validation for ClassificationPartitionedModel ## Cross-validating a GAM rebuilds each fold from the term matrix, the ## interaction pairs no longer being a form the constructor accepts. Every ## fold carries the parent's terms. %!test %! k = (1:60)'; %! X = [mod(k*7,11)-5, mod(k*3,11)-5, mod(k*5,11)-5]; %! y = double (X(:,1).*X(:,2) > 0) + 1; %! Mdl = fitcgam (X, y, "Interactions", "all"); %! rand ("state", 1); %! cv = crossval (Mdl, "KFold", 3); %! assert_equal (numel (cv.Trained), 3); %! assert_equal (cv.Trained{1}.Interactions, [1, 2; 1, 3; 2, 3]); %! assert_equal (cv.Trained{3}.Interactions, Mdl.Interactions); ## kfoldPredict writes whole names into its result: the labels are laid out ## one name per row, and assigning by element would fill the first column. %!test %! load fisheriris %! b = ! strcmp (species, "setosa"); %! X = meas(b,:); Ys = species(b); Yc = char (Ys); %! rand ("state", 1); Mc = fitcdiscr (X, Yc); %! rand ("state", 1); Ms = fitcdiscr (X, Ys); %! rand ("state", 2); cvc = crossval (Mc, "KFold", 3); %! rand ("state", 2); cvs = crossval (Ms, "KFold", 3); %! p = kfoldPredict (cvc); %! assert_equal (columns (p), 10); %! assert_equal (cellstr (p), kfoldPredict (cvs)); %!test # a GAM fold is fitted with the weights of the rows it holds %! k = (1:60)'; %! X = [sin(k), cos(2 * k)]; %! y = 2 * sin (k) + X(:,2) .^ 2 > 1; %! w = 1 + mod (k, 3); %! CVMdl = crossval (ClassificationGAM (X, y, 'Weights', w), 'KFold', 3); %! idx = training (CVMdl.Partition, 1); %! F = fitcgam (X(idx,:), y(idx), 'Weights', w(idx)); %! assert_equal (CVMdl.Trained{1}.Prior, F.Prior, 1e-12); %! assert_equal (CVMdl.Trained{1}.Intercept, F.Intercept, 1e-10); %!error ... %! ClassificationPartitionedModel () %!error ... %! ClassificationPartitionedModel (ClassificationKNN (ones (4,2), ones (4,1))) %!error ... %! ClassificationPartitionedModel (RegressionGAM (ones (40,2), ... %! randi ([1, 2], 40, 1)), cvpartition (randi ([1, 2], 40, 1), 'Holdout', 0.3)) %!error ... %! ClassificationPartitionedModel (ClassificationKNN (ones (4,2), ... %! ones (4,1)), 'Holdout') ## Test for kfoldPredict %!test %! load fisheriris %! a = fitcdiscr (meas, species, 'gamma', 0.5, 'fillcoeffs', 'off'); %! cvModel = crossval (a, 'Kfold', 4); %! [label, score, cost] = kfoldPredict (cvModel); %! assert_equal (class (cvModel), "ClassificationPartitionedModel"); %! assert_equal ({cvModel.X, cvModel.Y}, {meas, species}); %! assert_equal (cvModel.NumObservations, 150); %!# assert_equal (label, {"b"; "b"; "a"; "a"}); %!# assert_equal (score, [4.5380e-01, 5.4620e-01; 2.4404e-01, 7.5596e-01; ... %!# 9.9392e-01, 6.0844e-03; 9.9820e-01, 1.8000e-03], 1e-4); %!# assert_equal (cost, [5.4620e-01, 4.5380e-01; 7.5596e-01, 2.4404e-01; ... %!# 6.0844e-03, 9.9392e-01; 1.8000e-03, 9.9820e-01], 1e-4); %!test %! x = ones (4, 11); %! y = {'a'; 'a'; 'b'; 'b'}; %! k = 3; %! a = fitcknn (x, y, 'NumNeighbors', k); %! partition = cvpartition (numel (y), 'LeaveOut'); %! cvModel = ClassificationPartitionedModel (a, partition); %! [label, score, cost] = kfoldPredict (cvModel); %! assert_equal (class (cvModel), "ClassificationPartitionedModel"); %! assert_equal ({cvModel.X, cvModel.Y}, {x, y}); %! assert_equal (cvModel.NumObservations, 4); %! assert_equal (cvModel.ModelParameters.NumNeighbors, k); %! assert_equal (cvModel.ModelParameters.NSMethod, "exhaustive"); %! assert_equal (cvModel.ModelParameters.Distance, "euclidean"); %! assert_equal (isempty (cvModel.Trained{1}.Mu), true); %! ## Each fold keeps the parent's prior, so a fold holding one 'a' and two %! ## 'b' rows gives the classes equal votes; values from R2024a. %! assert_equal (label, {'a'; 'a'; 'a'; 'a'}); %! assert_equal (score, 0.5 * ones (4, 2), 1e-15); %! assert_equal (cost, 0.5 * ones (4, 2), 1e-15); ## Test input validation for kfoldPredict ## The partition, the stored data and NumObservations all describe the same ## set of observations. A row dropped for a missing value is outside all ## three: before this the partition covered it, NumObservations did not, and ## the folds trained on rows their own fit then discarded. %!test %! randn ('seed', 42); %! lab = double (randn (40, 1) > 0) + 1; %! X = [randn(40, 2); NaN, 1; 2, NaN]; %! Y = [lab; 1; 2]; %! Mdl = fitcsvm (X, Y); %! assert_equal (Mdl.NumObservations, 42); %! assert_equal (Mdl.RowsUsed, []); %! CVMdl = crossval (Mdl, 'KFold', 4); %! assert_equal (CVMdl.NumObservations, 42); %! assert_equal (rows (CVMdl.X), 42); %! assert_equal (rows (CVMdl.Y), 42); %! assert_equal (CVMdl.Partition.NumObservations, 42); %! assert_equal (numel (kfoldPredict (CVMdl)), 42); ## The folds stay stratified, the response being passed to cvpartition ## rather than a bare count. %!test %! randn ('seed', 7); %! X = randn (60, 2); %! Y = [ones(20, 1); 2 * ones(40, 1)]; %! CVMdl = crossval (fitcsvm (X, Y), 'KFold', 4); %! for k = 1:4 %! idx = test (CVMdl.Partition, k); %! assert_equal (sum (CVMdl.Y(idx) == 1) >= 4, true); %! assert_equal (sum (CVMdl.Y(idx) == 2) >= 8, true); %! endfor ## kfoldLoss defaults to the misclassification rate of the out-of-fold ## answers, which is the identity R2024a shows, and holds for every model ## type the class accepts. %!test %! load fisheriris %! X = meas(51:150,:); %! Y = species(51:150); %! rand ('seed', 42); %! cvp = cvpartition (Y, 'KFold', 5); %! for f = {'fitcsvm', 'fitcnet', 'fitcknn', 'fitcdiscr', 'fitcgam'} %! CVMdl = crossval (feval (f{1}, X, Y), 'CVPartition', cvp); %! assert_equal (kfoldLoss (CVMdl), ... %! mean (! strcmp (kfoldPredict (CVMdl), CVMdl.Y)), 1e-12); %! endfor ## Leave-one-out on a two-class fixture agrees with R2024a to the digit for ## every loss offered: it reports 0.02 for all three. %!test %! load fisheriris %! idx = [51:75, 101:125]; %! CVMdl = crossval (fitcdiscr (meas(idx,:), species(idx)), 'Leaveout', 'on'); %! assert_equal (CVMdl.KFold, 50); %! assert_equal (kfoldLoss (CVMdl), 0.02, 1e-12); %! assert_equal (kfoldLoss (CVMdl, 'LossFun', 'classifcost'), 0.02, 1e-12); %! assert_equal (kfoldLoss (CVMdl, 'LossFun', 'mincost'), 0.02, 1e-12); ## 'individual' is one number per fold, 'Folds' restricts to those named, and ## a handle is called with the class indicator, the scores, the weights and ## the cost, its weights summing to one as MATLAB's do. %!test %! load fisheriris %! X = meas(51:150,:); %! Y = species(51:150); %! rand ('seed', 42); %! cvp = cvpartition (Y, 'KFold', 5); %! CVMdl = crossval (fitcsvm (X, Y), 'CVPartition', cvp); %! L = kfoldLoss (CVMdl, 'Mode', 'individual'); %! assert_equal (size (L), [5, 1]); %! lab = kfoldPredict (CVMdl); %! k2 = test (cvp, 2); %! assert_equal (L(2), mean (! strcmp (lab(k2), CVMdl.Y(k2))), 1e-12); %! sel = test (cvp, 1) | test (cvp, 3); %! assert_equal (kfoldLoss (CVMdl, 'Folds', [1, 3]), ... %! mean (! strcmp (lab(sel), CVMdl.Y(sel))), 1e-12); %! assert_equal (numel (kfoldLoss (CVMdl, 'Folds', [2, 4], ... %! 'Mode', 'individual')), 2); %! assert_equal (kfoldLoss (CVMdl, 'LossFun', @(C, S, W, Cost) sum (W)), ... %! 1, 1e-12); ## Cost reaches the cost-aware losses. %!test %! load fisheriris %! X = meas(51:150,:); %! Y = species(51:150); %! rand ('seed', 42); %! CVMdl = crossval (fitcsvm (X, Y, 'Cost', [0, 4; 1, 0]), 'KFold', 5); %! assert_equal (CVMdl.Cost, [0, 4; 1, 0]); %! assert_equal (kfoldLoss (CVMdl, 'LossFun', 'classifcost') >= ... %! kfoldLoss (CVMdl, 'LossFun', 'classiferror'), true); ## Test input validation for kfoldLoss %!shared CVK %! load fisheriris %! rand ('seed', 42); %! CVK = crossval (fitcsvm (meas(51:150,:), species(51:150)), 'KFold', 4); %!error ... %! kfoldLoss (CVK, 'Mode') %!error ... %! kfoldLoss (CVK, 5, 1) %!error ... %! kfoldLoss (CVK, 'LossFun', 5) %!error ... %! kfoldLoss (CVK, 'LossFun', 'hinge') %!error ... %! kfoldLoss (CVK, 'LossFun', @(C, S, W, Cost) [1, 2]) %!error ... %! kfoldLoss (CVK, 'Mode', 'nope') %!error ... %! kfoldLoss (CVK, 'Folds', 0) %!error ... %! kfoldLoss (CVK, 'Nope', 1) ## Standardization reaches the folds. It did not for the KNN, whose refit ## arguments omitted it, so every fold trained on the raw scale while the ## parent reported itself standardized and nothing said so. %!test %! load fisheriris %! X = meas(:,1:3); %! X(:,1) = X(:,1) * 1000; %! CVMdl = crossval (fitcknn (X, species, 'Standardize', true), 'KFold', 5); %! assert_equal (isempty (CVMdl.Trained{1}.Mu), false); %!test %! load fisheriris %! X = meas(:,1:3); %! X(:,1) = X(:,1) * 1000; %! cvp = cvpartition (species, 'KFold', 5); %! CVMdl = crossval (fitcknn (X, species, 'Standardize', true), ... %! 'CVPartition', cvp); %! hit = 0; %! for k = 1:5 %! tr = training (cvp, k); %! te = test (cvp, k); %! Mdl = fitcknn (X(tr,:), species(tr), 'Standardize', true); %! hit += sum (strcmp (predict (Mdl, X(te,:)), species(te))); %! endfor %! assert_equal (sum (strcmp (kfoldPredict (CVMdl), species)), hit); ## The weights of the model that was cross validated are carried over, as ## the regression counterpart already did. %!test %! load fisheriris %! Mdl = fitcknn (meas, species); %! CVMdl = crossval (Mdl, 'KFold', 3); %! assert_equal (CVMdl.W, Mdl.W); %! assert_equal (size (CVMdl.W), [150, 1]); ## BinEdges is an empty cell, and a cell rather than an empty matrix: code ## that reaches into it with cellfun works against MATLAB and used to fail ## here on the type alone. %!test %! load fisheriris %! CVMdl = crossval (fitcknn (meas, species), 'KFold', 3); %! assert_equal (class (CVMdl.BinEdges), 'cell'); %! assert_equal (CVMdl.BinEdges, {}); ## An assigned Cost reaches every fold, so the folds predict under what was ## assigned rather than under what was fitted. Values measured on R2024a. %!test %! load fisheriris %! CVMdl = crossval (fitcdiscr (meas, species), 'KFold', 3); %! C = double (! eye (3)); C(1,2) = 4; %! CVMdl.Cost = C; %! assert_equal (CVMdl.Trained{1}.Cost, C); %! assert_equal (CVMdl.Trained{3}.Cost, C); %!test %! load fisheriris %! CVMdl = crossval (fitcknn (meas, species), 'KFold', 3); %! C = double (! eye (3)); C(1,2) = 4; %! CVMdl.Cost = C; %! assert_equal (CVMdl.Trained{2}.Cost, C); ## The two generative learners take a prior after the fit, and it reaches the ## folds too. %!test %! load fisheriris %! CVMdl = crossval (fitcdiscr (meas, species), 'KFold', 3); %! CVMdl.Prior = [0.6, 0.2, 0.2]; %! assert_equal (CVMdl.Prior, [0.6, 0.2, 0.2], 1e-15); %! assert_equal (CVMdl.Trained{1}.Prior, [0.6, 0.2, 0.2], 1e-15); %!test %! load fisheriris %! CVMdl = crossval (fitcnb (meas, species), 'KFold', 3); %! CVMdl.Prior = [0.6, 0.2, 0.2]; %! assert_equal (CVMdl.Prior, [0.6, 0.2, 0.2], 1e-15); %! assert_equal (CVMdl.Trained{1}.Prior, [0.6, 0.2, 0.2], 1e-15); ## The folds do not merely store the prior, they score by it. Iris is too ## well separated to show it, its posteriors sitting at 0 and 1, so the two ## overlapping species on their two weakest predictors are used instead. %!test %! load fisheriris %! X = meas(51:150,1:2); %! Y = species(51:150); %! CVMdl = crossval (fitcnb (X, Y), 'KFold', 5); %! before = kfoldPredict (CVMdl); %! CVMdl.Prior = [0.9, 0.1]; %! after = kfoldPredict (CVMdl); %! assert_equal (sum (! strcmp (before, after)) > 0, true); ## Priors are normalized, as they are on the learner itself. %!test %! load fisheriris %! CVMdl = crossval (fitcdiscr (meas, species), 'KFold', 3); %! CVMdl.Prior = [3, 1, 1]; %! assert_equal (CVMdl.Prior, [0.6, 0.2, 0.2], 1e-15); %!test %! load fisheriris %! CVMdl = crossval (fitcdiscr (meas, species), 'KFold', 3); %! CVMdl.Prior = 'uniform'; %! assert_equal (CVMdl.Prior, [1, 1, 1] / 3, 1e-15); ## An SVM's costs enter its box constraint while it is fitted, so MATLAB ## refuses them afterwards on the cross-validated model and so do we. %!error ... %! load fisheriris; ... %! b = ismember (species, {'setosa', 'versicolor'}); ... %! CVMdl = crossval (fitcsvm (meas(b,:), species(b)), 'KFold', 3); ... %! CVMdl.Cost = [0, 4; 1, 0]; %!error ... %! load fisheriris; ... %! CVMdl = crossval (fitcknn (meas, species), 'KFold', 3); ... %! CVMdl.Prior = [0.6, 0.2, 0.2]; %!error ... %! load fisheriris; ... %! CVMdl = crossval (fitcnet (meas, species), 'KFold', 3); ... %! CVMdl.Prior = [0.6, 0.2, 0.2]; %!error ... %! load fisheriris; ... %! CVMdl = crossval (fitcdiscr (meas, species), 'KFold', 3); ... %! CVMdl.Prior = [0.5, 0.5]; ## A GAM backing transforms once, like every other. Its class default is ## 'logit', so the fold has to be pinned at 'none' or the parent's transform ## lands on scores the fold has already transformed and the rows sum to about ## 1.23 instead of one. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! CVMdl = crossval (fitcgam (meas(inds,:), species(inds)), 'KFold', 3); %! assert_equal (CVMdl.ScoreTransform, 'logit'); %! assert_equal (CVMdl.Trained{1}.ScoreTransform, 'none'); %! [~, s] = kfoldPredict (CVMdl); %! assert_equal (sum (s, 2), ones (rows (s), 1), 1e-12); ## ScoreTransform is applied to the assembled scores, not carried into the ## folds. MATLAB leaves every Trained{k} at 'none' and transforms at the ## wrapper; the property used to be stored here and never read. %!test %! load fisheriris %! CVMdl = crossval (fitcdiscr (meas, species), 'KFold', 3); %! [~, s0] = kfoldPredict (CVMdl); %! CVMdl.ScoreTransform = 'doublelogit'; %! [~, s1] = kfoldPredict (CVMdl); %! assert_equal (CVMdl.Trained{1}.ScoreTransform, 'none'); %! assert_equal (s1, 1 ./ (1 + exp (-2 * s0)), 1e-12); ## The transform reaches kfoldLoss, which reads those scores. A loss that ## sums them shows it; mincost and classiferror do not move on this fixture, ## since a monotone transform leaves the chosen labels where they were. %!test %! load fisheriris %! CVMdl = crossval (fitcdiscr (meas, species), 'KFold', 3); %! f = @(C, S, W, Cost) sum (S(:)); %! [~, s0] = kfoldPredict (CVMdl); %! assert_equal (kfoldLoss (CVMdl, 'LossFun', f), 150, 1e-12); %! CVMdl.ScoreTransform = 'doublelogit'; %! assert_equal (kfoldLoss (CVMdl, 'LossFun', f), ... %! sum (sum (1 ./ (1 + exp (-2 * s0)))), 1e-12); ## 'none' is the identity, so the default transforms nothing. %!test %! load fisheriris %! CVMdl = crossval (fitcknn (meas, species), 'KFold', 3); %! [~, s0] = kfoldPredict (CVMdl); %! CVMdl.ScoreTransform = 'none'; %! [~, s1] = kfoldPredict (CVMdl); %! assert_equal (s1, s0); ## A transform carried by the model being cross-validated moves to the parent ## and is not handed to the folds. R2024a leaves every Trained@{k@} at 'none' ## for all five backings; the KNN and the network used to be given it as well ## and so applied it a second time. %!test %! load fisheriris %! CVMdl = crossval (fitcknn (meas, species, ... %! 'ScoreTransform', 'doublelogit'), 'KFold', 3); %! assert_equal (CVMdl.ScoreTransform, 'doublelogit'); %! assert_equal (CVMdl.Trained{1}.ScoreTransform, 'none'); %!test %! load fisheriris %! CVMdl = crossval (fitcnet (meas, species, ... %! 'ScoreTransform', 'doublelogit'), 'KFold', 3); %! assert_equal (CVMdl.ScoreTransform, 'doublelogit'); %! assert_equal (CVMdl.Trained{1}.ScoreTransform, 'none'); %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! CVMdl = crossval (fitcsvm (meas(inds,:), species(inds), ... %! 'ScoreTransform', 'doublelogit'), 'KFold', 3); %! assert_equal (CVMdl.ScoreTransform, 'doublelogit'); %! assert_equal (CVMdl.Trained{1}.ScoreTransform, 'none'); ## And it is applied once, not twice. A KNN is deterministic given the ## partition, so the two fits share their folds and the untransformed one is ## an exact baseline: doublelogit applied twice would not match it. %!test %! load fisheriris %! c = cvpartition (species, 'KFold', 3); %! CV0 = crossval (fitcknn (meas, species), 'CVPartition', c); %! CV1 = crossval (fitcknn (meas, species, ... %! 'ScoreTransform', 'doublelogit'), 'CVPartition', c); %! [~, s0] = kfoldPredict (CV0); %! [~, s1] = kfoldPredict (CV1); %! assert_equal (s1, 1 ./ (1 + exp (-2 * s0)), 1e-12); %!test %! load fisheriris %! CVMdl = crossval (fitcnet (meas, species, ... %! 'ScoreTransform', 'doublelogit'), 'KFold', 3); %! [~, s1] = kfoldPredict (CVMdl); %! CVMdl.ScoreTransform = 'none'; %! [~, s0] = kfoldPredict (CVMdl); %! assert_equal (s1, 1 ./ (1 + exp (-2 * s0)), 1e-12); %!error ... %! load fisheriris %! CVMdl = crossval (fitcdiscr (meas, species), 'KFold', 3); %! CVMdl.Cost = 1:9; ## foldLoss_ is a private helper and stays out of the method list. %!test %! assert_equal (any (strcmp (methods ("ClassificationPartitionedModel"), ... %! "foldLoss_")), false); ## kfoldMargin answers for every observation, from the fold that held it out. %!test %! load fisheriris %! CVMdl = crossval (fitcknn (meas, species), 'Leaveout', 'on'); %! m = kfoldMargin (CVMdl); %! assert_equal (size (m), [150, 1]); %! assert_equal (all (m >= -1 & m <= 1), true); ## kfoldEdge is the mean of the margins. R2024a returns 0.92 on this ## leave-one-out KNN and ours reproduces it exactly. %!test %! load fisheriris %! CVMdl = crossval (fitcknn (meas, species), 'Leaveout', 'on'); %! assert_equal (kfoldEdge (CVMdl), 0.92, 1e-14); %! assert_equal (kfoldEdge (CVMdl), mean (kfoldMargin (CVMdl)), 1e-14); ## 'Mode' selects one number per fold or one over them all. %!test %! load fisheriris %! CVMdl = crossval (fitcdiscr (meas, species), 'KFold', 5); %! assert_equal (size (kfoldEdge (CVMdl, 'Mode', 'individual')), [5, 1]); %! assert_equal (isscalar (kfoldEdge (CVMdl, 'Mode', 'average')), true); ## 'Folds' restricts the selection, and one fold's edge is that fold's mean. %!test %! load fisheriris %! CVMdl = crossval (fitcdiscr (meas, species), 'KFold', 5); %! m = kfoldMargin (CVMdl); %! idx = test (CVMdl.Partition, 2); %! assert_equal (kfoldEdge (CVMdl, 'Folds', 2), mean (m(idx)), 1e-12); %! assert_equal (kfoldEdge (CVMdl, 'Folds', 1:5), kfoldEdge (CVMdl), 1e-12); %!error ... %! load fisheriris %! kfoldEdge (crossval (fitcdiscr (meas, species), 'KFold', 3), 'Mode') %!error ... %! load fisheriris %! kfoldEdge (crossval (fitcdiscr (meas, species), 'KFold', 3), 5, 'average') %!error ... %! load fisheriris %! CVMdl = crossval (fitcdiscr (meas, species), 'KFold', 3); %! kfoldEdge (CVMdl, 'Mode', 'cumulative') %!error ... %! load fisheriris %! kfoldEdge (crossval (fitcdiscr (meas, species), 'KFold', 3), 'Folds', 7) %!error ... %! load fisheriris %! kfoldEdge (crossval (fitcdiscr (meas, species), 'KFold', 3), 'bogus', 1) ## A holdout partition predicts the holdout set and leaves the rest missing. %!test %! load fisheriris %! CVMdl = crossval (fitcdiscr (meas, species), 'Holdout', 0.2); %! idx = test (CVMdl.Partition, 1); %! [label, Score] = kfoldPredict (CVMdl); %! assert_equal (iscellstr (label), true); %! assert_equal (all (strcmp (label(! idx), '')), true); %! assert_equal (any (strcmp (label(idx), '')), false); %! assert_equal (all (all (isnan (Score(! idx, :)))), true); %! assert_equal (any (any (isnan (Score(idx, :)))), false); ## The holdout predictions are the fold's own, not a stand-in class. %!test %! load fisheriris %! CVMdl = crossval (fitcdiscr (meas, species), 'Holdout', 0.2); %! idx = test (CVMdl.Partition, 1); %! label = kfoldPredict (CVMdl); %! assert_equal (label(idx), predict (CVMdl.Trained{1}, meas(idx,:))); %! assert_equal (mean (strcmp (label(idx), species(idx))) > 0.9, true); ## A numeric response reports a missing label as NaN. %!test %! load fisheriris %! y = grp2idx (species); %! CVMdl = crossval (fitcdiscr (meas, y), 'Holdout', 0.2); %! idx = test (CVMdl.Partition, 1); %! label = kfoldPredict (CVMdl); %! assert_equal (all (isnan (label(! idx))), true); %! assert_equal (any (isnan (label(idx))), false); ## kfoldLoss and kfoldEdge answer over the holdout set alone. %!test %! load fisheriris %! CVMdl = crossval (fitcdiscr (meas, species), 'Holdout', 0.2); %! idx = test (CVMdl.Partition, 1); %! label = kfoldPredict (CVMdl); %! assert_equal (kfoldLoss (CVMdl), ... %! mean (! strcmp (label(idx), species(idx))), 1e-12); %! m = kfoldMargin (CVMdl); %! assert_equal (all (isnan (m(! idx))), true); %! assert_equal (kfoldEdge (CVMdl), mean (m(idx)), 1e-12); ## The shared cost guard is in force here too, and the struct form is ## permuted into this model's class order. The battery is on ## ClassificationDiscriminant. %!test %! load fisheriris %! Mdl = crossval (fitcdiscr (meas, species), 'KFold', 3); %! S = struct ('ClassNames', {{'virginica'; 'setosa'; 'versicolor'}}, ... %! 'ClassificationCosts', [0, 1, 2; 3, 0, 4; 5, 6, 0]); %! Mdl.Cost = S; %! assert_equal (Mdl.Cost, [0, 4, 3; 6, 0, 5; 1, 2, 0]); %!error ... %! load fisheriris %! Mdl = crossval (fitcdiscr (meas, species), 'KFold', 3); %! Mdl.Cost = ones (3); ## A fold is refitted with the argument set of the engine that fitted the ## parent: the two surfaces are disjoint and each refuses the other's, so a ## tree-fitted parent must not hand its folds Knots and Order. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = fitcgam (meas(inds,:), species(inds), 'FitMethod', 'boostedtrees'); %! CVMdl = crossval (Mdl, 'KFold', 3); %! assert_equal (class (CVMdl), 'ClassificationPartitionedModel'); %! assert_equal (numel (CVMdl.Trained), 3); %! assert_equal (CVMdl.Trained{1}.FitMethod, 'boostedtrees'); %! assert_equal (isempty (CVMdl.Trained{1}.TreeModel), false); ## The fold still carries no transform of its own, whatever engine fitted it: ## the parent applies it once to the assembled score. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = fitcgam (meas(inds,:), species(inds), 'FitMethod', 'boostedtrees'); %! CVMdl = crossval (Mdl, 'KFold', 3); %! assert_equal (CVMdl.ScoreTransform, 'logit'); %! assert_equal (CVMdl.Trained{1}.ScoreTransform, 'none'); %! [~, s] = kfoldPredict (CVMdl); %! assert_equal (sum (s, 2), ones (rows (s), 1), 1e-12); ## A tree-fitted parent holds its interactions as predictor pairs where the ## constructor takes a term matrix, so the pairs are widened before the fold ## is refitted and every fold carries the same terms as the parent. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = fitcgam (meas(inds,:), species(inds), 'FitMethod', 'boostedtrees', ... %! 'Interactions', 2); %! CVMdl = crossval (Mdl, 'KFold', 3); %! assert_equal (rows (Mdl.Interactions), 2); %! assert_equal (rows (CVMdl.Trained{1}.Interactions), 2); ## A spline-fitted parent is unaffected: its folds take the spline parameters ## and are fitted by the same engine. They do not report the parameters ## themselves, a compact model keeping no record of its fitting. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = fitcgam (meas(inds,:), species(inds), 'FitMethod', 'splines', ... %! 'Knots', 4); %! CVMdl = crossval (Mdl, 'KFold', 3); %! assert_equal (CVMdl.Trained{1}.FitMethod, 'splines'); %! assert_equal (isfield (CVMdl.Trained{1}.BaseModel, 'Intercept'), true); %!test %! ## kfoldfun hands the fold's model, its training data and its held-out %! ## data to the function, seven arguments in that order. %! load fisheriris %! CV = crossval (fitcknn (meas, species), "KFold", 3); %! seen = kfoldfun (CV, @(M, Xtr, Ytr, Wtr, Xte, Yte, Wte) ... %! [rows(Xtr), rows(Ytr), rows(Wtr), ... %! rows(Xte), rows(Yte), rows(Wte)]); %! assert_equal (size (seen), [3, 6]); %! ## the three training counts agree with each other, as do the three test %! ## counts, and each row accounts for every observation %! assert_equal (seen(:,1), seen(:,2)); %! assert_equal (seen(:,1), seen(:,3)); %! assert_equal (seen(:,4), seen(:,5)); %! assert_equal (seen(:,4), seen(:,6)); %! assert_equal (seen(:,1) + seen(:,4), repmat (150, 3, 1)); %!test %! ## The result is one row per fold, whatever the width. %! load fisheriris %! CV = crossval (fitcknn (meas, species), "KFold", 4); %! assert_equal (size (kfoldfun (CV, @(varargin) 1)), [4, 1]); %! assert_equal (size (kfoldfun (CV, @(varargin) [1, 2, 3])), [4, 3]); %!test %! ## The use it exists for: a count of errors on each held-out fold. %! load fisheriris %! CV = crossval (fitcknn (meas, species), "KFold", 3); %! f = @(M, Xtr, Ytr, Wtr, Xte, Yte, Wte) sum (! strcmp (predict (M, Xte), Yte)); %! n = kfoldfun (CV, f); %! assert_equal (size (n), [3, 1]); %! assert_equal (all (n >= 0 & n <= 150), true); %!test %! ## The model handed over is the one the fold was fitted with. %! load fisheriris %! CV = crossval (fitcknn (meas, species), "KFold", 3); %! same = kfoldfun (CV, @(M, varargin) isequal (M, CV.Trained{1})); %! assert_equal (same(1), 1); %!error ... %! kfoldfun (crossval (fitcknn (ones (6, 2), [1;1;1;2;2;2]), "KFold", 2)) %!error ... %! kfoldfun (crossval (fitcknn (ones (6, 2), [1;1;1;2;2;2]), "KFold", 2), 42) %!error ... %! kfoldfun (crossval (fitcknn (ones (6, 2), [1;1;1;2;2;2]), "KFold", 2), ... %! @(varargin) "x") %!test %! load fisheriris %! CV = crossval (fitcknn (meas, species), "KFold", 3); %! g = @(M, Xtr, varargin) ones (1, rows (Xtr)); %! fail ("kfoldfun (CV, g)", ... %! "must return the same number of values for every fold"); %!test %! ## The property order is MATLAB's, measured on R2024a. It is one list per %! ## class there and does not vary with the backing, so one fixture pins it. %! load fisheriris %! CVMdl = crossval (fitcknn (meas, species), "KFold", 3); %! assert_equal (sort (properties (CVMdl)), ... %! sort ({'ClassNames'; 'Cost'; 'Prior'; 'ScoreTransform'; ... %! 'CrossValidatedModel'; 'PredictorNames'; ... %! 'CategoricalPredictors'; 'ResponseName'; ... %! 'NumObservations'; 'X'; 'Y'; 'W'; ... %! 'ModelParameters'; 'Trained'; 'KFold'; 'Partition'; ... %! 'BinEdges'; 'NumTrainedPerFold'})); %!test %! ## NumTrainedPerFold reports what each fold actually fitted, which the %! ## budget in ModelParameters does not: boosting stops early and the folds %! ## need not stop together. Structure and semantics follow R2024a; the %! ## counts do not and are not expected to, the engine being this package's. %! load fisheriris %! b = strcmp (species, "setosa"); %! CVMdl = crossval (fitcgam (meas(:,2:4), b), "KFold", 3); %! n = CVMdl.NumTrainedPerFold; %! assert_equal (isstruct (n), true); %! assert_equal (sort (fieldnames (n)), {"InteractionTrees"; "PredictorTrees"}); %! assert_equal (size (n.PredictorTrees), [1, 3]); %! assert_equal (size (n.InteractionTrees), [1, 3]); %! ## early stopping, so under the budget rather than at it %! assert_equal (all (n.PredictorTrees < CVMdl.ModelParameters.NumTreesPerPredictor), true); %! ## no interactions were asked for, so none were fitted %! assert_equal (n.InteractionTrees, [0, 0, 0]); %!test %! ## A backing that fits no trees reports nothing rather than zero of them. %! load fisheriris %! CVMdl = crossval (fitcknn (meas, species), "KFold", 3); %! assert_equal (isempty (CVMdl.NumTrainedPerFold), true); %!test %! ## Nor does a GAM fitted by splines, which counts no trees either. %! load fisheriris %! b = strcmp (species, "setosa"); %! Mdl = fitcgam (meas(1:30,2:4), b(1:30), "FitMethod", "splines"); %! CVMdl = crossval (Mdl, "KFold", 3); %! assert_equal (isempty (CVMdl.NumTrainedPerFold), true); ## A polynomial-kernel SVM refits its folds through the recorded order, which ## the parent carries only under that kernel. %!test %! load fisheriris %! Mdl = fitcsvm (meas, strcmp (species, 'setosa'), ... %! 'KernelFunction', 'polynomial', 'PolynomialOrder', 2); %! CVMdl = crossval (Mdl, 'KFold', 3); %! assert_equal (CVMdl.ModelParameters.KernelPolynomialOrder, 2); %! assert_equal (numel (CVMdl.Trained), 3); ## ModelParameters carries the learner's parameters under this class's own ## tags, so a cross-validated SVM says what it was fitted with and still ## reports itself as a partitioned model. %!test %! load fisheriris %! CVMdl = crossval (fitcsvm (meas, strcmp (species, 'setosa')), 'KFold', 3); %! MP = CVMdl.ModelParameters; %! assert_equal (MP.Method, 'PartitionedModel'); %! assert_equal (MP.Type, 'classification'); %! assert_equal (MP.Version, 1); %! assert_equal (MP.NLearn, 3); %! assert_equal (MP.SVMtype, 'c_svc'); ## The discriminant's parameters come through whole. They were assembled ## from a name list before, which dropped FillCoeffs because this class has ## no property of that name to read it from. %!test %! load fisheriris %! CVMdl = crossval (fitcdiscr (meas, species, 'FillCoeffs', 'off'), ... %! 'KFold', 3); %! MP = CVMdl.ModelParameters; %! assert_equal (MP.FillCoeffs, false); %! assert_equal (MP.DiscrimType, 'linear'); %! assert_equal (MP.Method, 'PartitionedModel'); %!test %! load fisheriris %! MP = crossval (fitcknn (meas, species, 'Distance', 'minkowski'), ... %! 'KFold', 3).ModelParameters; %! assert_equal (MP.Exponent, 2); %! assert_equal (MP.Method, 'PartitionedModel'); ## A network fold's score is a posterior, so the expected cost is formed from ## it here. MATLAB reports the same for a network backing, although its own ## network predict refuses a third output as ours does. %!test %! load fisheriris %! CVMdl = crossval (fitcnet (meas, strcmp (species, 'setosa'), ... %! 'Cost', [0, 2; 5, 0]), 'KFold', 3); %! [label, score, cost] = kfoldPredict (CVMdl); %! assert_equal (size (cost), [150, 2]); %! assert_equal (cost, score * [0, 2; 5, 0], 1e-12); %!test %! load fisheriris %! CVMdl = crossval (fitcnet (meas, species), 'KFold', 3); %! [~, score, cost] = kfoldPredict (CVMdl); %! assert_equal (cost, 1 - score, 1e-12); ## A cross-validated GAM reports no cost, MATLAB's refusing it as well. %!error ... %! load fisheriris; ... %! [l, s, c] = kfoldPredict (crossval (fitcgam (meas, strcmp (species, 'setosa')), 'KFold', 3)) ## An SVM backing reports its expected cost, the fold models carrying one. %!test %! load fisheriris %! CVMdl = crossval (fitcsvm (meas, strcmp (species, 'setosa'), ... %! 'Cost', [0, 2; 5, 0]), 'KFold', 3); %! [~, ~, cost] = kfoldPredict (CVMdl); %! assert_equal (size (cost), [150, 2]); %! assert_equal (unique (cost, 'rows'), [0, 2; 5, 0]); ## Every documented score transform reaches the scores that are reported, and ## none of them moves the label: a transform reshapes what is reported, not ## what is decided. %!test %! load fisheriris %! Mdl = crossval (fitcknn (meas, species), 'KFold', 3); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = kfoldPredict (Mdl); %! T = {'identity', @(x) x; 'doublelogit', @(x) 1 ./ (1 + exp (-2 * x)); ... %! 'invlogit', @(x) log (x ./ (1 - x)); ... %! 'logit', @(x) 1 ./ (1 + exp (-x)); ... %! 'sign', @(x) sign (x); 'symmetric', @(x) 2 * x - 1; ... %! 'symmetriclogit', @(x) 2 ./ (1 + exp (-x)) - 1}; %! for i = 1:rows (T) %! Mdl.ScoreTransform = T{i,1}; %! [l, s] = kfoldPredict (Mdl); %! assert_equal (s, T{i,2}(raw), 1e-12); %! assert_equal (l, label); %! endfor %! ## ismax marks the largest score of each observation, ties to the first. %! [~, k] = max (raw, [], 2); %! e = zeros (size (raw)); %! e(sub2ind (size (raw), (1:rows (raw))', k)) = 1; %! Mdl.ScoreTransform = 'ismax'; %! [~, s] = kfoldPredict (Mdl); %! assert_equal (s, e); %! Mdl.ScoreTransform = 'symmetricismax'; %! [~, s] = kfoldPredict (Mdl); %! assert_equal (s, 2 * e - 1); ## A function handle is taken as given and applied to the scores. %!test %! load fisheriris %! Mdl = crossval (fitcknn (meas, species), 'KFold', 3); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = kfoldPredict (Mdl); %! Mdl.ScoreTransform = @(x) x .^ 2; %! [l, s] = kfoldPredict (Mdl); %! assert_equal (s, raw .^ 2, 1e-12); %! assert_equal (l, label); ## The GAM is the one backing whose fold prior is not the parent's: the prior ## is carried as observation weight and refitted from the rows the fold kept, ## which MATLAB does and the other five backings do not. The values below are ## that rule applied to each fold's own composition, and each was measured on ## R2024a for the composition it belongs to. %!test %! load fisheriris %! b = strcmp (species, 'setosa'); %! CVMdl = crossval (fitcgam (meas, b, 'Prior', [0.3, 0.7]), 'KFold', 3); %! for k = 1:3 %! tr = training (CVMdl.Partition, k); %! n = [sum(b(tr) == 0), sum(b(tr) == 1)]; %! w = [0.3, 0.7] .* (n ./ [100, 50]); %! assert_equal (CVMdl.Trained{k}.Prior, w ./ sum (w), 1e-12); %! endfor ## An empirical parent prior gives every class the same weight, so the fold's ## prior collapses to its own proportions. %!test %! load fisheriris %! b = strcmp (species, 'setosa'); %! CVMdl = crossval (fitcgam (meas, b), 'KFold', 3); %! for k = 1:3 %! tr = training (CVMdl.Partition, k); %! n = [sum(b(tr) == 0), sum(b(tr) == 1)]; %! assert_equal (CVMdl.Trained{k}.Prior, n ./ sum (n), 1e-12); %! endfor %!error %! load fisheriris %! CVMdl = crossval (fitctree (meas, species), 'KFold', 3); %! CVMdl.Cost = [0, 2, 8; 3, 0, 1; 5, 4, 0]; %!test %! ## A naive Bayes fold is fitted with its rows' weights %! load fisheriris %! w = 1 + (1:150)' / 7; %! CVMdl = crossval (fitcnb (meas, species, 'Weights', w), 'KFold', 3); %! idx = training (CVMdl.Partition, 1); %! Mdl = fitcnb (meas(idx,:), species(idx), 'Weights', w(idx)); %! assert_equal (CVMdl.Trained{1}.DistributionParameters, ... %! Mdl.DistributionParameters, 1e-12); %!test %! ## A neural network fold is fitted with its rows' weights %! load fisheriris %! w = 1 + (1:150)' / 7; %! Mdl = fitcnet (meas, species, 'LayerSizes', 3, 'Weights', w); %! rand ('seed', 7); %! CVMdl = crossval (Mdl, 'KFold', 3); %! idx = training (CVMdl.Partition, 1); %! rand ('seed', 7); %! cvpartition (150, 'KFold', 3); %! F = fitcnet (meas(idx,:), species(idx), 'LayerSizes', 3, ... %! 'Weights', Mdl.W(idx), 'Prior', Mdl.Prior); %! assert_equal (nthargout (2, @predict, CVMdl.Trained{1}, meas), ... %! nthargout (2, @predict, F, meas), 1e-10); statistics-release-2.1.2/inst/Machine_Learning/ClassificationSVM.m000066400000000000000000005660061527311104300251750ustar00rootroot00000000000000## Copyright (C) 2024 Pallav Purbia ## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ClassificationSVM < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} ClassificationSVM ## ## Support Vector Machine classification ## ## The @code{ClassificationSVM} class implements a Support Vector Machine ## classifier object for one-class or two-class problems, which can predict ## responses for new data using the @code{predict} method. ## ## Support Vector Machine classification is a supervised learning method used ## for classification tasks. It works by finding the optimal hyperplane that ## separates classes in the feature space with the maximum margin. For ## non-linearly separable data, it uses kernel functions to map data to a ## higher-dimensional space where separation is possible. ## ## Create a @code{ClassificationSVM} object by using the @code{fitcsvm} ## function or the class constructor. ## ## @seealso{fitcsvm} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} X ## ## Predictor data ## ## A numeric matrix containing the unstandardized predictor data. Each ## column of @var{X} represents one predictor (variable), and each row ## represents one observation. This property is read-only. ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} Y ## ## Class labels ## ## Specified as a logical or numeric column vector, or as a character array ## or a cell array of character vectors with the same number of rows as the ## predictor data. Each row in @var{Y} is the observed class label for ## the corresponding row in @var{X}. This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} NumObservations ## ## Number of observations ## ## A positive integer value specifying the number of observations in the ## training dataset used for training the ClassificationSVM model. ## This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} RowsUsed ## ## Rows used for fitting ## ## A logical column vector with the same length as the observations in the ## original predictor data @var{X}, true for each row that was used for ## fitting the ClassificationSVM model. It is empty, @qcode{[]}, ## when every observation was used. A row is dropped when its class is ## missing, when every predictor is missing, or when its weight is zero. ## A row missing only some predictors is used: it counts in the prior and ## in @code{W}, while the support vector machine is fitted on the complete ## rows, as MATLAB does. This property is read-only. ## ## @end deftp RowsUsed = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} NumPredictors ## ## Number of predictors ## ## A positive integer value specifying the number of predictors in the ## training dataset used for training the ClassificationSVM model. ## This property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} PredictorNames ## ## Names of predictor variables ## ## A cell array of character vectors specifying the names of the predictor ## variables. The names are in the order in which they appear in the ## training dataset. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} ResponseName ## ## Response variable name ## ## A character vector specifying the name of the response variable @var{Y}. ## This property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} ClassNames ## ## Names of classes in the response variable ## ## An array of unique values of the response variable @var{Y}, which has the ## same data types as the data in @var{Y}, sorted or in the order given by ## the @qcode{'ClassNames'} option. This property is read-only. ## @qcode{ClassNames} can have any of the following datatypes: ## ## @itemize ## @item Cell array of character vectors ## @item Character array ## @item Logical vector ## @item Numeric vector ## @end itemize ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} Sigma ## ## Predictor standard deviations ## ## A numeric vector of the same length as the columns in @var{X} containing ## the standard deviations of predictor variables. If the predictor ## variables have not been standardized, then @qcode{Sigma} is empty. ## This property is read-only. ## ## Only observations with no missing predictor enter the estimate, and ## they are weighted so that each class keeps the share of the ## observation weight it carried before any row was set aside. ## ## @end deftp Sigma = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} Mu ## ## Predictor means ## ## A numeric vector of the same length as the columns in @var{X} containing ## the means of predictor variables. If the predictor variables have not ## been standardized, then @qcode{Mu} is empty. This property is read-only. ## ## Only observations with no missing predictor enter the estimate, and ## they are weighted so that each class keeps the share of the ## observation weight it carried before any row was set aside. ## ## @end deftp Mu = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} ModelParameters ## ## SVM training parameters ## ## A structure holding the parameters the fit was given. The engine is ## LIBSVM and the record is LIBSVM's, so @qcode{SVMtype} names its ## formulation and @qcode{Tolerance} and @qcode{Shrinking} are its own ## controls; the parameters MathWorks reports for its SMO and ISDA ## solvers are absent, this class running neither. ## ## @qcode{KernelPolynomialOrder} belongs to the polynomial kernel alone ## and is empty under every other, as it is in MATLAB. ## ## A structure containing the parameters used to train the SVM model with ## the following fields: @code{SVMtype}, @code{BoxConstraint}, ## @code{CacheSize}, @code{KernelScale}, @code{KernelOffset}, ## @code{KernelFunction}, @code{PolynomialOrder}, @code{Nu}, ## @code{Tolerance}, and @code{Shrinking}. This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} Alpha ## ## Trained classifier coefficients ## ## The coefficients of the trained SVM classifier specified as an @math{s*1} ## numeric vector, where @math{s} is the number of support vectors equal to ## @qcode{sum (obj.IsSupportVector)}. They are the magnitudes of the dual ## coefficients and are never negative; the class each belongs to is given ## by the corresponding entry of @qcode{SupportVectorLabels}. ## @qcode{Alpha} is populated for every kernel function. This property is ## read-only. ## ## @end deftp Alpha = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} Beta ## ## Linear predictor coefficients ## ## The linear predictor coefficients specified as a @math{p*1} numeric ## vector, where @math{p} is the number of predictors. @qcode{Beta} is ## the primal representation of the fitted hyperplane and exists only when ## the SVM classifier was trained with a @qcode{'linear'} kernel function; ## for any other kernel there is no such representation and @qcode{Beta} is ## empty. It equals @qcode{(obj.SupportVectors / s)' * (obj.Alpha .* ## obj.SupportVectorLabels)}, where @math{s} is the kernel scale, and a ## score is @qcode{(@var{x} / s) * Beta + Bias}, as in MATLAB. ## This property is read-only. ## ## @end deftp Beta = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} Bias ## ## Bias term ## ## The bias term specified as a scalar. This property is read-only. ## ## @end deftp Bias = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} IsSupportVector ## ## Support vector indicator ## ## An @math{N*1} logical vector that flags whether a corresponding ## observation in the predictor data matrix is a Support Vector. @math{N} ## is the number of observations in the training data. This property is ## read-only. ## ## @end deftp IsSupportVector = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} SupportVectorLabels ## ## Support vector class labels ## ## The support vector class labels specified as an @math{s*1} numeric ## vector, where @math{s} is the number of support vectors equal to ## @qcode{sum (obj.IsSupportVector)}. A value of +1 in ## @code{SupportVectorLabels} indicates that the corresponding support ## vector ## belongs to the positive class @qcode{(ClassNames@{2@})}. A value of -1 ## indicates that the corresponding support vector belongs to the negative ## class @qcode{(ClassNames@{1@})}. A one-class model labels every support ## vector +1. This property is read-only. ## ## @end deftp SupportVectorLabels = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} SupportVectors ## ## Support vectors ## ## The support vectors of the trained SVM classifier specified an @math{s*p} ## numeric matrix, where @math{s} is the number of support vectors equal to ## @qcode{sum (obj.IsSupportVector)}, and @math{p} is the number of ## predictor ## variables in the predictor data. This property is read-only. ## ## @end deftp SupportVectors = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} Prior ## ## Prior probabilities of the classes ## ## A numeric row vector with one entry per class, in the order of ## @code{ClassNames}, summing to one. It defaults to the class ## frequencies of the training data, a row missing some predictors ## included. This property is read-only. ## ## Specified as a row vector with one entry per class, in the order of ## @qcode{ClassNames}, and rescaled to sum to one. It may be given as ## @qcode{'empirical'}, @qcode{'uniform'}, a numeric vector, or a ## structure with @qcode{ClassNames} and @qcode{ClassProbs} fields, which ## assigns each probability by class name rather than by position. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} Cost ## ## Cost of misclassification ## ## A numeric square matrix, where @code{Cost(i,j)} is the cost of ## classifying an observation of class @math{i} as class @math{j}. It ## defaults to zero on the diagonal and one elsewhere. This property is ## read-only. ## ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} W ## ## Observation weights ## ## A numeric column vector with one entry per training observation, ## normalized to sum to one, as MATLAB reports it. It has the class of the ## @qcode{'Weights'} given, single or double. This property is read-only. ## ## Each class carries its prior, spread over its own observations in ## proportion to the @qcode{'Weights'} given, or evenly when none were. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} KernelParameters ## ## Parameters of the kernel function ## ## A structure with fields @qcode{Function} and @qcode{Scale}, and ## @qcode{Order} for a polynomial kernel. @qcode{Function} names the ## kernel as MATLAB names it, so a radial basis kernel reports ## @qcode{'gaussian'} whichever spelling was given; the kernel the fit was ## handed is unchanged in @qcode{ModelParameters}. This property is ## read-only. ## ## @end deftp KernelParameters = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} BoxConstraints ## ## Box constraints ## ## A numeric column vector with one entry per observation, holding the ## box constraint the fit applied to it. It is @qcode{BoxConstraint} for ## every observation unless @qcode{Prior} or @qcode{Cost} reweighted the ## classes, in which case each class is scaled by the weight it carried ## into the fit, normalized so the weights average to one. This property ## is read-only. ## ## @end deftp BoxConstraints = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} OutlierFraction ## ## Expected fraction of outliers in the training data ## ## A scalar in @code{[0, 1)}, zero unless one was asked for. ## ## @strong{Deviation from MATLAB.} The value is reported as it was given, ## but it reaches the fit by a different route: MATLAB removes outliers ## iteratively and reports @qcode{Solver} as @qcode{'ISDA'}, where a ## nonzero fraction here selects LIBSVM's @math{\nu}-SVC, in which ## @math{\nu} bounds the fraction of margin errors. The two agree on what ## the number means and not on how the fit reaches it. This property is ## read-only. ## ## @end deftp OutlierFraction = 0; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} Nu ## ## Nu parameter for one-class learning ## ## A positive scalar, and empty unless the model is a one-class learner, ## which is what MATLAB reports. This property is read-only. ## ## @end deftp Nu = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A numeric vector of column indices into @code{X} naming the predictors ## treated as categorical, and empty when none is. This property is ## read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} ExpandedPredictorNames ## ## Names of the predictors as the model expanded them ## ## A cell array of character vectors. It matches @code{PredictorNames} ## unless a categorical predictor was expanded into indicator variables. ## This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} BinEdges ## ## Bin edges of the predictors ## ## A cell array with one entry per predictor, holding that predictor's bin ## edges where the learner discretized it before fitting. It is empty here ## and stays empty: this learner fits the predictors as they are, and ## MATLAB's reports an empty cell for it as well. ## ## This property is read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} HyperparameterOptimizationResults ## ## Results of the hyperparameter optimization ## ## @strong{Always empty.} It is declared for MATLAB compatibility, where ## it holds what an automatic search over the hyperparameters found. This ## class fits the parameters it is given and runs no such search, so there ## is nothing to report. This property is read-only. ## ## @end deftp HyperparameterOptimizationResults = []; endproperties ## The LIBSVM structure the engine works in. It is ours alone, with no ## MATLAB counterpart, so it is kept out of the property listing while ## staying readable for anyone who needs the raw model. properties (Hidden, GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} Model ## ## Trained SVM model ## ## A structure containing the trained model in @qcode{'libsvm'} format. ## This property is read-only. ## ## It is the engine's own structure and has no MATLAB counterpart, ## so it is kept out of @code{properties} and out of the online ## documentation. Reading it works exactly as it always did. ## ## @end deftp Model = []; endproperties ## Properties a user may set after the model is built. Each one is ## validated by its set method below. properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {ClassificationSVM} {property} ScoreTransform ## ## Transformation function for classification scores ## ## Specified as a function handle for transforming the classification ## scores. Add or change the @qcode{ScoreTransform} property using dot ## notation as in: ## ## @itemize ## @item @qcode{@var{obj}.ScoreTransform = 'function_name'} ## @item @qcode{@var{obj}.ScoreTransform = @@function_handle} ## @end itemize ## ## When specified as a character vector, it can be any of the following ## built-in functions. Nevertheless, the @qcode{ScoreTransform} property ## always stores their function handle equivalent. ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Value} @tab @var{Description} ## @item @qcode{'doublelogit'} @tab @math{1 ./ (1 + exp (-2 * x))} ## @item @qcode{'invlogit'} @tab @math{log (x ./ (1 - x))} ## @item @qcode{'ismax'} @tab Sets the score for the class with the ## largest score to 1, and for all other classes to 0 ## @item @qcode{'logit'} @tab @math{1 ./ (1 + exp (-x))} ## @item @qcode{'none'} @tab @math{x} (no transformation) ## @item @qcode{'identity'} @tab @math{x} (no transformation) ## @item @qcode{'sign'} @tab ## @math{-1 for x < 0, 0 for x = 0, 1 for x > ## 0} ## @item @qcode{'symmetric'} @tab @math{2 * x - 1} ## @item @qcode{'symmetricismax'} @tab Sets the score for the class ## with the largest score to 1, and for all other classes to -1 ## @item @qcode{'symmetriclogit'} @tab @math{2 ./ (1 + exp (-x)) - 1} ## @end multitable ## ## @code{fitPosterior} stores the transformation from scores to ## posterior probabilities it fits as @qcode{'@@(S)sigmoid(S,A,B)'}, ## @qcode{'@@(S)step(S,LowerBound,UpperBound,PositiveClassProbability)'} ## or @qcode{'@@(S)constant(S,K)'}. Such a text may also be assigned ## directly, which MATLAB refuses. ## ## @end deftp ScoreTransform = 'none'; endproperties ## Readable by the counterpart class, which copies it, and kept out of ## the documented surface. properties (GetAccess = public, SetAccess = protected, Hidden) ## The dummy coding of the categorical predictors, empty when none. Coding_ = []; STfun = @(x) x; ## The observation weights as they were given, before any prior scaled ## them. RawWeights = []; endproperties ## Set methods for the properties a user may assign. methods (Hidden) function this = set.ScoreTransform (this, val) name = 'ClassificationSVM'; try [this.STfun, this.ScoreTransform] = parseScoreTransform ... (val, name); catch error (strcat ("ClassificationSVM.subsasgn: 'ScoreTransform'", ... " must be a 'function_handle' object.")); end_try_catch endfunction ## Custom display function disp (this) fprintf ("\n ClassificationSVM\n\n"); ## Print selected properties fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); if (iscellstr (this.ClassNames)) str = repmat ({'''%s'''}, 1, numel (this.ClassNames)); str = strcat ('{', strjoin (str, ' '), '}'); str = sprintf (str, this.ClassNames{:}); elseif (ischar (this.ClassNames)) str = repmat ({'''%s'''}, 1, rows (this.ClassNames)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, cellstr (this.ClassNames){:}); else # single, double, logical str = repmat ({'%d'}, 1, numel (this.ClassNames)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, this.ClassNames); endif fprintf ("%+25s: %s\n", 'ClassNames', str); fprintf ("%+25s: '%s'\n", 'ScoreTransform', this.ScoreTransform); fprintf ("%+25s: %d\n", 'NumObservations', this.NumObservations); fprintf ("%+25s: %d\n", 'NumPredictors', this.NumPredictors); fprintf ("%+25s: [%dx1 double]\n", 'Alpha', numel (this.Alpha)); if (! isempty (this.Beta)) fprintf ("%+25s: [%dx1 double]\n", 'Beta', numel (this.Beta)); endif fprintf ("%+25s: %f\n", 'Bias', this.Bias); fprintf ("%+25s: [1x1 struct]\n", 'KernelParameters'); if (! isempty (this.Mu)) if (numel (this.Mu) < 6) str = repmat ({'''%0.4f'''}, 1, numel (this.Mu)); str = strcat ('[', strjoin (str, ' '), ']'); out = sprintf (str, this.Mu); fprintf ("%+25s: %s\n", 'Mu', out); out = sprintf (str, this.Sigma); fprintf ("%+25s: %s\n", 'Sigma', out); else fprintf ("%+25s: [1x%d double]\n", 'Mu', numel (this.Mu)); fprintf ("%+25s: [1x%d double]\n", 'Sigma', numel (this.Sigma)); endif endif endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {ClassificationSVM} {@var{obj} =} ClassificationSVM (@var{X}, @var{Y}) ## @deftypefnx {ClassificationSVM} {@var{obj} =} ClassificationSVM (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationSVM} {@var{obj} =} ClassificationSVM (@var{Tbl}, @var{formula}) ## @deftypefnx {ClassificationSVM} {@var{obj} =} ClassificationSVM (@var{Tbl}, @var{Y}) ## @deftypefnx {ClassificationSVM} {@var{obj} =} ClassificationSVM (@dots{}, @var{name}, @var{value}) ## ## Create a @qcode{ClassificationSVM} class object containing a Support ## Vector Machine classification model for one-class or two-class problems. ## ## @code{@var{obj} = ClassificationSVM (@var{X}, @var{Y})} returns a ## ClassificationSVM object, with @var{X} as the predictor data and @var{Y} ## containing the class labels of observations in @var{X}. ## ## @itemize ## @item ## @code{X} must be a @math{N*P} numeric matrix of input data where rows ## correspond to observations and columns correspond to features or ## variables. @var{X} will be used to train the SVM model. ## @item ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels ## of corresponding predictor data in @var{X}. @var{Y} can be either ## numeric, logical, or cell array of character vectors. It must have same ## numbers of rows as @var{X}. ## @end itemize ## ## @code{@var{obj} = ClassificationSVM (@dots{}, @var{name}, @var{value})} ## returns a ClassificationSVM object with parameters specified by the ## following @qcode{@var{name}, @var{value}} paired input arguments: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'CategoricalPredictors'} @tab The predictors whose values ## are levels, as indices, as a logical vector with one element per ## predictor, or as @qcode{'all'}. Each is dummy coded in its place, one ## column of zeros and ones per level seen in training, named as in ## @qcode{'x1 == 2'} in @code{ExpandedPredictorNames}, and the coded columns ## are not standardized. An observation holding a level the training data ## did not has no score and takes the class of largest prior, as a row ## missing a predictor does. ## A predictor may be named rather than indexed, as a character matrix of ## one padded name per row, a string array or a cellstr; a name must match ## an entry of @qcode{'PredictorNames'} exactly, its case included. ## ## @item @qcode{'PredictorNames'} @tab A cell array of character ## vectors specifying the names of the predictors. The length of this array ## must match the number of columns in @var{X}. ## ## @item @qcode{'ResponseName'} @tab A character vector specifying the ## name of the response variable. ## ## @item @qcode{'ClassNames'} @tab Names of the classes in the class ## labels, @var{Y}, used for fitting the SVM model. @qcode{ClassNames} are ## of the same type as the class labels in @var{Y}. ## The model keeps the classes in this order; by default they are sorted. ## ## @item @qcode{'ScoreTransform'} @tab A user-defined function handle ## or a character vector specifying one of the following builtin functions ## specifying the transformation applied to predicted classification scores. ## Supported values include @qcode{'doublelogit'}, @qcode{'invlogit'}, ## @qcode{'ismax'}, @qcode{'logit'}, @qcode{'none'}, @qcode{'identity'}, ## @qcode{'sign'}, @qcode{'symmetric'}, @qcode{'symmetricismax'}, and ## @qcode{'symmetriclogit'}. ## ## @item @qcode{'Standardize'} @tab A logical scalar specifying whether ## to standardize the predictor variables. Default is @qcode{false}. ## ## @item @qcode{'SVMtype'} @tab A character vector specifying the type ## of SVM to use. Supported values are @qcode{'c_svc'} (C-support vector ## classification), @qcode{'nu_svc'} (nu-support vector classification), and ## @qcode{'one_class_svm'} (one-class SVM). ## ## @item @qcode{'KernelFunction'} @tab A character vector specifying ## the kernel function to use. Supported values are @qcode{'linear'}, ## @qcode{'rbf'} or @qcode{'gaussian'}, @qcode{'polynomial'}, and ## @qcode{'sigmoid'}. ## ## @item @qcode{'PolynomialOrder'} @tab A positive integer specifying ## the order of the polynomial kernel function. Default is 3. ## ## @item @qcode{'KernelScale'} @tab A positive scalar dividing every ## predictor before any kernel is applied, as MATLAB does, so that with ## @math{u} and @math{v} the divided predictors the kernels are @math{u'v}, ## @math{exp (-||u - v||^2)}, @math{(1 + u'v)^q} and @math{tanh (u'v + c)}, ## @math{c} being @qcode{'KernelOffset'}. The default is 1. ## ## @item @qcode{'KernelOffset'} @tab A non-negative scalar, the constant ## @math{c} of the sigmoid kernel, which MATLAB does not have. MATLAB adds ## it to every element of the Gram matrix, which leaves the fitted model ## unchanged, so it changes no other kernel here. The default is 0. ## ## @item @qcode{'Weights'} @tab A single or double vector of nonnegative ## observation weights, one per row of @var{X}. Each observation's box ## constraint is @math{n} times @qcode{BoxConstraint} times its weight, the ## weights scaled so that each class carries its prior times the cost of ## misclassifying it. An empirical prior sums the weights per class, ## standardization uses weighted means and standard deviations, and a row of ## zero weight is left out. The model's @code{W} keeps the class of the ## weights, while every computation runs in double, so @code{Prior} is ## double where MATLAB returns single. ## ## @item @qcode{'Prior'} @tab @qcode{'empirical'} (default), ## @qcode{'uniform'}, a vector with one element per class, or a structure ## with fields @qcode{ClassNames} and @qcode{ClassProbs}. ## A class whose prior is 0, or whose misclassification costs are all 0, ## is removed from the data before fitting, with a warning, as in MATLAB. ## ## @item @qcode{'Cost'} @tab A square matrix of misclassification costs, ## @code{Cost(i,j)} being the cost of classifying into class @math{j} an ## observation of class @math{i}, @code{1 - eye (2)} by default; or a ## structure with fields @qcode{ClassNames} and @qcode{ClassificationCosts}, ## which names its own order of the classes and is permuted to the model's. ## ## @item @qcode{'BoxConstraint'} @tab A positive scalar specifying the ## box constraint parameter. Default is 1. ## ## @item @qcode{'Nu'} @tab A positive scalar in the range (0,1] ## specifying the nu parameter for nu-SVM and one-class SVM. Default is 0.5. ## ## @item @qcode{'CacheSize'} @tab A positive scalar specifying the ## cache size in MB. Default is 1000. ## ## @item @qcode{'Tolerance'} @tab A positive scalar specifying the ## tolerance of termination criterion. Default is 1e-6. ## ## @item @qcode{'Shrinking'} @tab Either 0 or 1 specifying whether to ## use the shrinking heuristics. Default is 1. ## ## @item @qcode{'OutlierFraction'} @tab A positive scalar in the range ## [0,1) specifying the fraction of outliers for one-class SVM. ## @end multitable ## ## @seealso{fitcsvm} ## @end deftypefn function this = ClassificationSVM (X, Y, varargin) ## Check for sufficient number of input arguments if (nargin < 2) error ("ClassificationSVM: too few input arguments."); endif ## A table names its own predictors and says which hold levels [this, X, Y, varargin] = resolveTable (this, 'ClassificationSVM', ... X, Y, varargin); ## Check X and Y have the same number of observations if (rows (X) != rows (Y)) error ("ClassificationSVM: number of rows in X and Y must be equal."); endif ## Assign original X and Y data to the ClassificationSVM object this.X = X; this.Y = Y; ## Get groups in Y [gY, gnY, glY] = grp2idx (Y); ## Parse optional paired arguments optNames = {'Weights', 'Standardize', 'PredictorNames', ... 'ResponseName', 'ClassNames', 'Prior', 'Cost', ... 'ScoreTransform', 'SVMtype', 'OutlierFraction', ... 'KernelFunction', 'PolynomialOrder', 'KernelScale', ... 'KernelOffset', 'BoxConstraint', 'Nu', 'CacheSize', ... 'Tolerance', 'Shrinking', 'CategoricalPredictors'}; ## An empty default stands for one resolved once the classes are ## known: 'SVMtype' is a one-class fit for one class and 'c_svc' for ## two, 'Nu' is 0.5, 'OutlierFraction' 0, 'KernelFunction' 'rbf' for ## one class and 'linear' for two, and the classes, prior, cost and ## weights come from the response. dfValues = {[], false, [], [], [], [], [], [], [], [], [], 3, 1, 0, 1, ... [], 1000, 1e-6, 1, []}; [Weights, Standardize, PredictorNames, ResponseName, ClassNames, ... Prior, Cost, STin, SVMtype, OutlierFraction, KernelFunction, ... PolynomialOrder, KernelScale, KernelOffset, BoxConstraint, Nu, ... CacheSize, Tolerance, Shrinking, CatPreds, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("ClassificationSVM: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isvector (Weights) && isreal (Weights))) error (strcat ("ClassificationSVM: 'Weights' must be a real", ... " numeric vector.")); endif if (! isempty (Weights) && numel (Weights) != rows (X)) error (strcat ("ClassificationSVM: 'Weights' must have one", ... " element per row in X.")); endif if (! isempty (Weights) && (any (Weights < 0) || ! (sum (Weights) > 0))) error (strcat ("ClassificationSVM: 'Weights' must be", ... " nonnegative and must not be all zero.")); endif if (! (Standardize == true || Standardize == false)) error (strcat ("ClassificationSVM: 'Standardize' must", ... " be either true or false.")); endif if (! isempty (PredictorNames) && ! iscellstr (PredictorNames)) error (strcat ("ClassificationSVM: 'PredictorNames' must", ... " be supplied as a cellstring array.")); elseif (! isempty (PredictorNames) && (columns (PredictorNames) != columns (X))) error (strcat ("ClassificationSVM: 'PredictorNames' must", ... " have the same number of columns as X.")); endif if (! isempty (ResponseName) && ! ischar (ResponseName)) error (strcat ("ClassificationSVM: 'ResponseName' must", ... " be a character vector.")); endif if (! isempty (ClassNames) && ! (iscellstr (ClassNames) || isnumeric (ClassNames) || islogical (ClassNames) || ischar (ClassNames) || isa (ClassNames, 'categorical') || isa (ClassNames, 'string'))) error (strcat ("ClassificationSVM: 'ClassNames' must be a", ... " categorical array, a character array, a", ... " string array, a logical vector, a numeric", ... " vector, or a cell array of character", ... " vectors.")); endif ## A named class with prior 0 is removed below, so it may be absent exempt = []; if (! isempty (ClassNames)) exempt = zeroPriorNames (ClassNames, Prior, Cost); [~, errmsg] = namedClasses (glY, ClassNames, exempt); if (! isempty (errmsg)) error ("ClassificationSVM: %s", errmsg); endif endif if (! isempty (Prior) && ! (isstruct (Prior) || (isnumeric (Prior) && isvector (Prior) && all (Prior >= 0) && any (Prior > 0)) || (ischar (Prior) && any (strcmpi (Prior, {'empirical', 'uniform'}))))) error (strcat ("ClassificationSVM: 'Prior' must be a", ... " non-negative numeric vector, 'empirical'", ... " or 'uniform'.")); endif ## A structure names its own order, and is permuted to the model's ## once the classes are known if (! isempty (Cost) && ! (isstruct (Cost) || (isnumeric (Cost) && issquare (Cost) && all (Cost(:) >= 0)))) error (strcat ("ClassificationSVM: 'Cost' must be a", ... " non-negative square matrix or a structure.")); endif if (! isempty (SVMtype) && (! any (strcmp (SVMtype, {'c_svc', 'nu_svc', 'one_class_svm'})))) error (strcat ("ClassificationSVM: 'SVMtype' must be", ... " 'c_svc', 'nu_svc', or 'one_class_svm'.")); endif if (! isempty (OutlierFraction) && ! (isscalar (OutlierFraction) && OutlierFraction >= 0 && OutlierFraction < 1)) error (strcat ("ClassificationSVM: 'OutlierFraction' must", ... " be a positive scalar in the range 0 =<", ... " OutlierFraction < 1.")); endif if (! isempty (KernelFunction) && ! ischar (KernelFunction)) error (strcat ("ClassificationSVM: 'KernelFunction' must", ... " be a character vector.")); endif KernelFunction = tolower (KernelFunction); if (! isempty (KernelFunction) && (! any (strcmpi (KernelFunction, ... {'linear', 'rbf', 'gaussian', 'polynomial', 'sigmoid'})))) error ("ClassificationSVM: unsupported Kernel function."); endif if (! (isnumeric (PolynomialOrder) && isscalar (PolynomialOrder) && PolynomialOrder > 0 && mod (PolynomialOrder, 1) == 0)) error (strcat ("ClassificationSVM: 'PolynomialOrder' must", ... " be a positive integer.")); endif if (! (isscalar (KernelScale) && KernelScale > 0)) error (strcat ("ClassificationSVM: 'KernelScale'", ... " must be a positive scalar.")); endif if (! (isnumeric (KernelOffset) && isscalar (KernelOffset) && KernelOffset >= 0)) error (strcat ("ClassificationSVM: 'KernelOffset' must", ... " be a non-negative scalar.")); endif if (! (isscalar (BoxConstraint) && BoxConstraint > 0)) error (strcat ("ClassificationSVM: 'BoxConstraint' must", ... " be a positive scalar.")); endif if (! isempty (Nu) && ! (isscalar (Nu) && Nu > 0 && Nu <= 1)) error (strcat ("ClassificationSVM: 'Nu' must be a positive", ... " scalar in the range 0 < Nu <= 1.")); endif if (! (isscalar (CacheSize) && CacheSize > 0)) error (strcat ("ClassificationSVM: 'CacheSize' must", ... " be a positive scalar.")); endif if (! (isscalar (Tolerance) && Tolerance >= 0)) error (strcat ("ClassificationSVM: 'Tolerance' must", ... " be a positive scalar.")); endif if (! (ismember (Shrinking, [0, 1]) && isscalar (Shrinking))) error ("ClassificationSVM: 'Shrinking' must be either 0 or 1."); endif if (! isempty (STin)) [this.STfun, this.ScoreTransform] = ... parseScoreTransform (STin, 'ClassificationSVM'); endif if (! isempty (args)) error ("ClassificationSVM: invalid optional paired argument."); endif ## Get number of variables in training data ndims_X = columns (X); ## Assign the number of predictors to the ClassificationSVM object this.NumPredictors = ndims_X; ## Handle class names if (! isempty (ClassNames)) ## The observations of a class ClassNames leaves out are dropped gY(! ismember (gY, namedClasses (glY, ClassNames))) = NaN; endif ## An observation is dropped when its response is missing, when every ## predictor is missing, or when its weight is zero, as R2024a drops ## it. A row missing only some predictors is kept and reported as ## used, while the fit below draws on the complete observations alone. RowsUsed = ! isnan (gY) & ! all (isnan (X), 2); if (! isempty (Weights)) RowsUsed = RowsUsed & Weights(:) > 0; endif ## Index the rows and not the elements: a response naming its ## classes in the rows of a character matrix has one column per ## character, and a linear index flattens the names into single ## letters. Every other accepted type is a column, for which ## the two forms agree. Yret = Y(RowsUsed, :); Xret = X(RowsUsed, :); this.X = Xret; this.Y = Yret; cobs = ! any (isnan (Xret), 2); Y = Yret(cobs, :); X = Xret(cobs, :); if (isempty (Weights)) RawWeights = ones (rows (Xret), 1); else RawWeights = Weights(RowsUsed); RawWeights = RawWeights(:); endif ## The weights keep their class in the model; every computation runs on ## them as double. this.RawWeights = RawWeights; RawWeights = double (RawWeights); wc = RawWeights(cobs); ## Renew groups in Y over the retained observations, so a class held ## only by a row with missing predictors is still a class of the model ## The classes keep the type of Y, sorted or in the order ClassNames ## gives them. [this.ClassNames, gret] = classOrder (Yret, ClassNames, exempt); gY = gret(cobs); nclasses = classCount (this.ClassNames); ## Resolve Prior and Cost against the classes that survived. Prior ## defaults to the frequencies of the training data and Cost to zero on ## the diagonal and one elsewhere, which is what MATLAB reports. if (isstruct (Prior)) Prior = priorFromStruct (Prior, this.ClassNames, ... 'ClassificationSVM'); endif ## The empirical prior counts every row kept, a row missing some ## predictors included, as R2024a counts it; the fit then sees the ## complete rows alone, whose weights need not keep that balance. freq = accumarray (gret(:), RawWeights, [nclasses, 1])' ... / sum (RawWeights); if (isempty (Prior) || (ischar (Prior) && strcmpi (Prior, 'empirical'))) Prior = freq; elseif (ischar (Prior) && strcmpi (Prior, 'uniform')) Prior = ones (1, nclasses) / nclasses; else if (numel (Prior) != nclasses) error (strcat ("ClassificationSVM: 'Prior' must have one entry", ... " per class.")); endif Prior = Prior(:)' / sum (Prior); endif if (isstruct (Cost)) [Cost, errmsg] = costMatrix (Cost, this.ClassNames); if (! isempty (errmsg)) error ("ClassificationSVM: %s", errmsg); endif endif if (isempty (Cost)) Cost = ones (nclasses) - eye (nclasses); elseif (rows (Cost) != nclasses) error (strcat ("ClassificationSVM: the number of rows and columns", ... " in 'Cost' must correspond to the classes in Y.")); endif ## A class with prior 0, or that costs nothing to misclassify, is never ## predicted and weighs nothing, so its observations are removed [drop, Prior, msg] = removedClasses (Prior, Cost, this.ClassNames, gret); if (all (drop)) error ("ClassificationSVM: every class has prior probability 0."); elseif (any (drop)) if (! isempty (msg)) warning ("ClassificationSVM: %s", msg); endif keep = ! drop(gret); used = find (RowsUsed); RowsUsed(used(! keep)) = false; this.X = this.X(keep,:); this.Y = this.Y(keep,:); this.RawWeights = this.RawWeights(keep); RawWeights = RawWeights(keep); kc = keep(cobs); X = X(kc,:); Y = Y(kc,:); gY = gY(kc); wc = wc(kc); cobs = cobs(keep); gret = gret(keep); endif this.Prior = Prior; this.Cost = Cost; ## One class makes a one-class fit, and 'SVMtype' may only agree. Two ## classes make a C-SVM unless 'SVMtype' says otherwise. 'Nu' never ## chooses the type: a C-SVM records it and does not use it, as R2024a ## does. 'OutlierFraction' moves the bias of a one-class fit and ## leaves its Nu alone; two-class robust learning is not implemented. if (isempty (Nu)) Nu = 0.5; endif if (isempty (OutlierFraction)) OutlierFraction = 0; endif if (nclasses == 1) if (! isempty (SVMtype) && ! strcmp (SVMtype, 'one_class_svm')) error (strcat ("ClassificationSVM: cannot train a binary", ... " problem with only one class available.")); endif SVMtype = 'one_class_svm'; elseif (isempty (SVMtype)) SVMtype = 'c_svc'; endif if (OutlierFraction > 0 && ! strcmp (SVMtype, 'one_class_svm')) error (strcat ("ClassificationSVM: 'OutlierFraction' is not", ... " implemented for two-class learning.")); endif if (nclasses == 1) if (isempty (KernelFunction)) KernelFunction = 'rbf'; endif else if (isempty (KernelFunction)) KernelFunction = 'linear'; endif endif ## Check that we are dealing only with one-class or binary classification if (nclasses > 2) error (strcat ("ClassificationSVM: can only be used for", ... " one-class or two-class learning.")); endif ## The solver is given the class indices, never the labels themselves. ## A numeric label is not an index, so recoding one below would leave ## it as it stands, and labels of -1 and +1 are the single pair LIBSVM ## reorders: svm_group_classes swaps them so that its first class is ## the +1 one, after which every label read back is the other class. ## Taking the indices first makes the response the same whatever the ## labels are named. Y = gY; ## Force Y labels to -1 and +1 to avoid numeric issues with different ## compiling options; see https://github.com/cjlin1/libsvm/issues/220 if (nclasses == 2) Y(Y == 2) = -1; endif ## Dummy code the categorical predictors on the rows the fit draws on. ## X keeps the predictors as given; the fit and every prediction see ## the coded columns. cnames = PredictorNames; if (isempty (cnames)) cnames = arrayfun (@(k) sprintf ('x%d', k), 1:columns (X), ... 'UniformOutput', false); endif [Coding, errmsg] = dummyCoding (X, CatPreds, cnames); if (! isempty (errmsg)) error ("ClassificationSVM: %s", errmsg); endif X = dummyCoding (X, Coding); if (! isempty (Coding.Index)) this.CategoricalPredictors = Coding.Index; this.ExpandedPredictorNames = Coding.ExpandedNames; this.Coding_ = Coding; endif ## Check X contains valid data if (! (isnumeric (X) && isfinite (X))) error ("ClassificationSVM: invalid values in X."); endif ## Assign the number of observations and their corresponding indices ## on the original data, which will be used for training the model, ## to the ClassificationSVM object this.NumObservations = rows (this.X); ## RowsUsed is left empty when every observation was used, as in MATLAB if (all (RowsUsed)) this.RowsUsed = []; else this.RowsUsed = RowsUsed; endif ## Handle the Standardize option. The model must be fitted on the ## scale it predicts on: predict and resubPredict standardize their ## input from Mu and Sigma, so the training data is standardized here ## as well. ## The support vectors are therefore stored standardized too, which is ## the scale svmpredict receives. if (Standardize) ## Mu and Sigma weight the complete observations by their weights so ## that each class keeps the share of the weight it carried before any ## row was set aside, which is what MATLAB reports; the deviation is ## unbiased for those weights, measured on R2024a to 4e-16. sw = zeros (rows (X), 1); for k = 1:nclasses ck = (gY == k); if (any (ck)) sw(ck) = (sum (RawWeights(gret == k)) / sum (RawWeights)) ... * wc(ck) / sum (wc(ck)); endif endfor sw = sw / sum (sw); this.Mu = sum (sw .* X, 1); Zs = X - this.Mu; this.Sigma = sqrt (sum (sw .* Zs .^ 2, 1) / (1 - sum (sw .^ 2))); ## A constant predictor is left unscaled; its weighted mean can miss ## the constant by one rounding, so its deviation need not be zero. this.Sigma(this.Sigma == 0 | all (X == X(1,:), 1)) = 1; ## A level's column is left as it is, as in MATLAB R2024a. this.Mu(Coding.Dummy) = 0; this.Sigma(Coding.Dummy) = 1; X = (X - this.Mu) ./ this.Sigma; else this.Sigma = []; this.Mu = []; endif ## Generate default predictors and response variable names (if necessary) if (isempty (PredictorNames)) for i = 1:ndims_X PredictorNames {i} = strcat ("x", num2str (i)); endfor endif if (isempty (ResponseName)) ResponseName = 'Y'; endif ## Assign predictors and response variable names this.PredictorNames = PredictorNames; this.ResponseName = ResponseName; ## Without categorical predictors the expanded names are the predictor ## names themselves. Each class carries its prior, spread over its ## observations in proportion to their weights, as MATLAB reports it. if (isempty (this.Coding_)) this.ExpandedPredictorNames = PredictorNames; endif this.W = cast (priorNormalize (RawWeights, gret, this.Prior), ... class (this.RawWeights)); ## Set svmtrain parameters for SVMtype and KernelFunction switch (SVMtype) case 'c_svc' s = 0; case 'nu_svc' s = 1; case 'one_class_svm' s = 2; endswitch switch (KernelFunction) case 'linear' t = 0; case 'polynomial' t = 1; case {'rbf', 'gaussian'} t = 2; case 'sigmoid' t = 3; endswitch ## MATLAB divides the predictors by KernelScale for every kernel, so the ## fit sees X / KernelScale with gamma 1. Its polynomial kernel is ## (1 + x'z) ^ q, and it adds KernelOffset to the Gram matrix, which ## leaves the fitted model unchanged, so the offset reaches only the ## sigmoid kernel, which is ours alone. Xu = X; X = X / KernelScale; g = 1; r = 0; if (t == 1) r = 1; elseif (t == 3) r = KernelOffset; endif ## svmpredict: ## '-s': SVMtype ## '-t': KernelFunction ## '-g': Gamma ## '-d': PolynomialOrder ## '-r': coef0 ## '-c': BoxConstraint ## '-n': Nu ## '-m': CacheSize ## '-e': Tolerance ## '-h': Shrinking ## Build options string for svmtrain function str_options = strcat ("-s %d -t %d -g %.16g -d %d -r %.16g", ... " -c %.16g -n %.16g -m %.16g -e %.16g", ... " -h %d -q"); svm_options = sprintf (str_options, s, t, g, PolynomialOrder, ... r, BoxConstraint, Nu, ... CacheSize, Tolerance, Shrinking); ## Prior, Cost and the observation weights enter the fit through one ## box constraint per observation, n * C * w, as R2024a sets them: w is ## the weight scaled so that each class carries its prior times the cost ## of misclassifying it, renormalized. The default prior is the weighted ## frequency and the default cost one, so without weights every ## constraint is C and the fit is the same as with none. ## A one-class fit ignores the weights: measured on R2024a, its bias, ## support vectors, scores and box constraints are the same with them as ## without, although W still reports them. if (strcmp (SVMtype, 'one_class_svm')) instW = ones (rows (X), 1); else adjPrior = Prior; if (nclasses == 2) adjPrior = Prior .* sum (Cost, 2)'; adjPrior = adjPrior / sum (adjPrior); endif instW = rows (X) * priorNormalize (wc, gY, adjPrior); endif ## Train the SVM model using svmtrain from libsvm [Model, converged] = svmtrain (Y, X, svm_options, instW); if (! converged) warning (strcat ("ClassificationSVM: the solver stopped at its", ... " iteration limit without converging;", ... " standardizing the predictors may help.")); endif ## A one-class model's bias puts its least supported support vector on ## the boundary: R2024a's bias is minus the smallest kernel sum over the ## support vectors, which reproduces it to 1e-13 for linear and gaussian ## kernels at every Nu measured. LIBSVM's rho averages the free support ## vectors instead, so it is replaced, and the scores follow. ## With OutlierFraction the bias instead leaves that fraction of the ## training rows below the boundary: minus the quantile of their kernel ## sums, which R2024a reproduces to 1e-14 with Octave's default method. if (strcmp (SVMtype, 'one_class_svm')) if (OutlierFraction > 0) [~, ~, dall] = svmpredict (ones (rows (X), 1), X, Model, '-q'); Model.rho = quantile (dall(:) + Model.rho, OutlierFraction); else [~, ~, dsv] = svmpredict (ones (rows (Model.SVs), 1), ... full (Model.SVs), Model, '-q'); Model.rho = min (dsv + Model.rho); endif endif ## Populate ClassificationSVM object properties. LIBSVM returns the ## dual coefficients already multiplied by the class sign, whereas ## MATLAB keeps their magnitudes in ALPHA and the sign in ## SUPPORTVECTORLABELS, so the two are separated here. this.Alpha = abs (Model.sv_coef); this.Bias = Model.rho; if (strcmp (SVMtype, 'one_class_svm')) this.Bias = -Model.rho; endif ## One label per support vector, in the order of SupportVectors, taking ## the sign from the coefficients themselves. LIBSVM's sign is opposite ## to the labelling MATLAB reports. ## A one-class model labels every support vector +1, as MATLAB does. if (strcmp (SVMtype, 'one_class_svm')) this.SupportVectorLabels = ones (size (Model.sv_coef)); else this.SupportVectorLabels = -sign (Model.sv_coef); endif this.IsSupportVector = false (this.NumObservations, 1); this.IsSupportVector(Model.sv_indices) = true; this.SupportVectors = Xu(Model.sv_indices,:); ## BETA holds the primal coefficients, one per predictor, and exists ## only for a linear kernel; for any other kernel there is no primal ## representation and MATLAB leaves it empty. It weighs the divided ## predictors, as MATLAB's does. if (t == 0) this.Beta = (this.SupportVectors / KernelScale)' ... * (this.Alpha .* this.SupportVectorLabels); else this.Beta = []; endif ## Re-express the engine's model on the undivided predictors, which is ## what every prediction hands it: the scale moves into gamma, or into ## the coefficients of a linear kernel, which has no gamma. Model.SVs = sparse (this.SupportVectors); if (t == 0) Model.sv_coef = Model.sv_coef / KernelScale ^ 2; else Model.Parameters(4) = 1 / KernelScale ^ 2; endif this.Model = Model; ## The kernel, the per-observation box constraints and the two ## one-class parameters, in the shapes MATLAB reports them. The box ## constraints are the scalar scaled by the very weights the fit was ## given above, so they describe what was solved rather than what was ## asked for. this.KernelParameters = svmKernelParams (KernelFunction, KernelScale, ... PolynomialOrder); this.BoxConstraints = BoxConstraint * instW(:); this.OutlierFraction = OutlierFraction; if (strcmp (SVMtype, 'one_class_svm')) this.Nu = Nu; endif ## Populate ModelParameters structure. The polynomial order belongs to ## the polynomial kernel alone and is reported under no other, as ## MATLAB reports it. if (strcmpi (KernelFunction, 'polynomial')) KPOrder = PolynomialOrder; else KPOrder = []; endif params = struct ('SVMtype', SVMtype, 'BoxConstraint', BoxConstraint, ... 'CacheSize', CacheSize, 'KernelScale', KernelScale, ... 'KernelOffset', KernelOffset, 'KernelFunction', ... KernelFunction, 'KernelPolynomialOrder', KPOrder, ... 'Nu', Nu, 'Tolerance', Tolerance, ... 'Shrinking', Shrinking, ... 'OutlierFraction', OutlierFraction, ... 'StandardizeData', logical (Standardize), ... 'Version', 1, 'Method', 'SVM', ... 'Type', 'classification'); this.ModelParameters = params; endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationSVM} {@var{obj} =} discardSupportVectors (@var{obj}) ## ## Discard the support vectors of a linear SVM model. ## ## @code{@var{obj} = discardSupportVectors (@var{obj})} empties ## @code{Alpha}, @code{SupportVectors} and ## @code{SupportVectorLabels}, leaving @code{Beta} and @code{Bias} to ## decide every prediction. A linear kernel needs nothing else, so the ## returned model predicts what it predicted before while carrying one ## vector in place of many. ## ## The kernel must be linear. Under any other the support vectors are ## part of the decision function and cannot be dropped. Discarding twice ## is not an error and changes nothing. ## ## @seealso{fitcsvm, ClassificationSVM, CompactClassificationSVM} ## @end deftypefn function this = discardSupportVectors (this) if (nargin != 1) print_usage (); endif if (! strcmpi (this.ModelParameters.KernelFunction, 'linear')) error (strcat ("ClassificationSVM.discardSupportVectors: you", ... " cannot discard support vectors for a non-linear", ... " kernel.")); endif ## The engine keeps its own copy of the support vectors, so emptying ## the properties alone would free nothing. Collapsing the model onto ## the single vector that decides it leaves every scoring path as it ## was, svmpredict going on being the engine over one vector. this.Model = discardSVs (this.Model); this.Alpha = []; this.SupportVectors = []; this.SupportVectorLabels = []; endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationSVM} {@var{label} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {ClassificationSVM} {[@var{label}, @var{score}] =} predict (@var{obj}, @var{XC}) ## @deftypefnx {ClassificationSVM} {[@var{label}, @var{score}, @var{cost}] =} predict (@var{obj}, @var{XC}) ## ## Classify new data points into categories using the Support Vector Machine ## classification model from a ClassificationSVM object. ## ## @code{@var{label} = predict (@var{obj}, @var{XC})} returns the vector of ## labels predicted for the corresponding instances in @var{XC}, using the ## predictor data in @code{obj.X} and corresponding labels, @code{obj.Y}, ## stored in the ClassificationSVM model, @var{obj}. For one-class SVM ## model, +1 or -1 is returned. ## ## @itemize ## @item ## @var{obj} must be a @qcode{ClassificationSVM} class object. ## @item ## @var{XC} must be an @math{M*P} numeric matrix with the same number of ## features @math{P} as the corresponding predictors of the SVM model in ## @var{obj}. ## @end itemize ## ## @code{[@var{label}, @var{score}] = predict (@var{obj}, @var{XC})} also ## returns @var{score}, which contains the decision values for each ## prediction. A @qcode{ScoreTransform} assigned to @var{obj} is applied ## to them, so @var{score} holds whatever that transform returns. Posterior ## probabilities need a transform fitted to the model, which this package ## does not compute yet. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @seealso{ClassificationSVM, fitcsvm} ## ## @strong{Deviation from MATLAB.} @var{cost} is the expected cost of ## each assignment, @math{sum_j P(j) Cost(j,k)}. An SVM score is a ## signed distance to the boundary and not a posterior, so the only ## distribution available is the one concentrated on the predicted class ## and @var{cost} is the row of @qcode{Cost} belonging to it. MATLAB ## returns the @strong{column} instead, which is the same matrix read the ## wrong way and contradicts its own @code{ClassificationKNN}, ## @code{ClassificationDiscriminant} and @code{ClassificationNaiveBayes} ## on any asymmetric cost matrix; the two agree wherever @qcode{Cost} is ## symmetric, the default included. Measured on R2024a. ## ## @end deftypefn function [labels, scores, cost] = predict (this, XC) ## Check for sufficient input arguments if (nargin < 2) error ("ClassificationSVM.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'ClassificationSVM.predict', XC); ## Check for valid XC if (isempty (XC)) error ("ClassificationSVM.predict: XC is empty."); elseif (this.NumPredictors != columns (XC)) error (strcat ("ClassificationSVM.predict:", ... " XC must have the same number of", ... " predictors as the trained model.")); endif ## Code the categorical predictors and standardize (if necessary) if (! isempty (this.Coding_)) XC = dummyCoding (XC, this.Coding_); endif if (! isempty (this.Mu)) XC = (XC - this.Mu) ./ this.Sigma; endif ## Predict labels and scores from new data [out, ~, scores] = svmpredict (ones (rows (XC), 1), XC, this.Model, '-q'); ## A model fitted on the second class alone, the first removed for its ## prior of 0, holds LIBSVM's single label -1, whose decision value of ## 1 points to that class if (isscalar (this.Model.Label) && this.Model.Label == -1) scores = -scores; endif ## Expand scores for two classes if (classCount (this.ClassNames) == 2) scores = [scores, -scores]; endif ## Translate labels to classnames. Indexing the class names by a ## per-observation class number keeps every response type on one path: ## assigning into a preallocated result instead has to know that a ## character matrix holds a name per row and not per element. ## A one-class model labels every row with its one class, outliers ## included, as R2024a does; the score tells them apart. if (classCount (this.ClassNames) == 1) idx = ones (rows (XC), 1); else idx = 2 - (out == 1); endif ## A row missing a predictor has no score, and takes the class of ## largest prior, as MATLAB R2024a labels it whatever the cost. miss = isnan (scores(:,1)); [~, top] = max (this.Prior); idx(miss) = top; labels = labelsFromIndex (this.ClassNames, idx); ## The expected cost of each assignment, sum_j P(j) * Cost(j,k). An ## SVM score is a signed distance and not a posterior, so the only ## honest distribution is the one concentrated on the predicted class, ## which leaves the row of Cost belonging to it. ## ## MATLAB returns the *column* here, Cost(:,k)', which is the same ## matrix read the wrong way and disagrees with its own KNN, ## discriminant and naive Bayes on any asymmetric cost matrix. Both ## agree when Cost is symmetric, the default included. cost = this.Cost(idx, :); cost(miss,:) = NaN; ## Apply ScoreTransform scores = this.STfun (scores); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationSVM} {@var{label} =} resubPredict (@var{obj}) ## @deftypefnx {ClassificationSVM} {[@var{label}, @var{score}] =} resubPredict (@var{obj}) ## @deftypefnx {ClassificationSVM} {[@var{label}, @var{score}, @var{cost}] =} resubPredict (@var{obj}) ## ## Classify the training data using the trained Support Vector Machine ## classification object. ## ## @code{@var{label} = resubPredict (@var{obj})} returns the vector of ## labels predicted for the corresponding instances in the training data, ## using the predictor data in @code{obj.X} and corresponding labels, ## @code{obj.Y}, stored in the Support Vector Machine classification model, ## @var{obj}. For one-class model, +1 or -1 is returned. ## ## @itemize ## @item ## @var{obj} must be a @qcode{ClassificationSVM} class object. ## @end itemize ## ## @code{[@var{label}, @var{scores}] = resubPredict (@var{obj}} also returns ## @var{scores}, which contains the decision values for each prediction. A ## @qcode{ScoreTransform} assigned to @var{obj} is applied to them, so ## @var{scores} holds whatever that transform returns. Posterior ## probabilities need a transform fitted to the model, which this package ## does not compute yet. ## ## @seealso{fitcsvm} ## ## @strong{Deviation from MATLAB.} @var{cost} is the expected cost of ## each assignment, @math{sum_j P(j) Cost(j,k)}. An SVM score is a ## signed distance to the boundary and not a posterior, so the only ## distribution available is the one concentrated on the predicted class ## and @var{cost} is the row of @qcode{Cost} belonging to it. MATLAB ## returns the @strong{column} instead, which is the same matrix read the ## wrong way and contradicts its own @code{ClassificationKNN}, ## @code{ClassificationDiscriminant} and @code{ClassificationNaiveBayes} ## on any asymmetric cost matrix; the two agree wherever @qcode{Cost} is ## symmetric, the default included. Measured on R2024a. ## ## @end deftypefn function [labels, scores, cost] = resubPredict (this) ## X and Y hold exactly the observations the model retained X = this.X; Y = this.Y; ## Code the categorical predictors and standardize (if necessary) if (! isempty (this.Coding_)) X = dummyCoding (X, this.Coding_); endif if (! isempty (this.Mu)) X = (X - this.Mu) ./ this.Sigma; endif ## Predict labels and scores from new data [out, ~, scores] = svmpredict (ones (rows (X), 1), X, this.Model, '-q'); ## A model fitted on the second class alone, the first removed for its ## prior of 0, holds LIBSVM's single label -1, whose decision value of ## 1 points to that class if (isscalar (this.Model.Label) && this.Model.Label == -1) scores = -scores; endif ## Expand scores for two classes if (classCount (this.ClassNames) == 2) scores = [scores, -scores]; endif ## Translate labels to classnames, as predict does idx = 2 - (out == 1); ## A row missing a predictor has no score, and takes the class of ## largest prior, as MATLAB R2024a labels it whatever the cost. miss = isnan (scores(:,1)); [~, top] = max (this.Prior); idx(miss) = top; labels = labelsFromIndex (this.ClassNames, idx); ## The expected cost of each assignment, as in predict above: an SVM ## score is a signed distance and not a posterior, so the row of Cost ## belonging to the predicted class is the whole of it. MATLAB returns ## the column instead, which its own KNN, discriminant and naive Bayes ## contradict on any asymmetric cost matrix. cost = this.Cost(idx, :); cost(miss,:) = NaN; ## Apply ScoreTransform scores = this.STfun (scores); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationSVM} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationSVM} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationSVM} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margins for Support Vector Machine classifier. ## ## @code{@var{m} = margin (@var{obj}, @var{X}, @var{Y})} returns ## the classification margins for @var{obj} with data @var{X} and ## classification @var{Y}. @var{m} is a numeric vector of length size (X,1). ## ## @itemize ## @item ## @var{obj} is a @var{ClassificationSVM} object trained on @code{X} ## and @code{Y}. ## @item ## @var{X} must be a @math{N*P} numeric matrix of input data where rows ## correspond to observations and columns correspond to features or ## variables. ## @item ## @var{Y} is @math{N*1} matrix or cell matrix containing the class labels ## of corresponding predictor data in @var{X}. @var{Y} must have same ## numbers of Rows as @var{X}. ## @end itemize ## ## The classification margin for each observation is the difference between ## the classification score for the true class and the maximal ## classification score for the false classes. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{fitcsvm, ClassificationSVM} ## @end deftypefn function m = margin (this, X, Y) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationSVM.margin: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y] = tableResponse (this, 'margin', X, Y, {}, nargin > 2); ## Check for valid X if (isempty (X)) error ("ClassificationSVM.margin: X is empty."); elseif (columns (this.X) != columns (X)) error (strcat ("ClassificationSVM.margin: X must have the same", ... " number of predictors as the trained model.")); endif ## Check for valid Y if (isempty (Y)) error ("ClassificationSVM.margin: Y is empty."); elseif (rows (X) != rows (Y)) error (strcat ("ClassificationSVM.margin: Y must have", ... " the same number of rows as X.")); endif ## Y may be the class labels, which is what this method documents and ## what MATLAB accepts, or already the +1/-1 coding the solver works in. ## It used to be the latter only, so passing the labels the docstring ## promises reached LIBSVM as a cell array and raised its own message. Ypm = svmPlusMinus (Y, this.ClassNames); ## The model scores the coded, standardized predictors, which is the ## scale predict gives it; X taken as it stands was scored unscaled. if (! isempty (this.Coding_)) X = dummyCoding (X, this.Coding_); endif if (! isempty (this.Mu)) X = (X - this.Mu) ./ this.Sigma; endif [~, ~, dec_values_L] = svmpredict (Ypm, X, this.Model, '-q'); ## A model fitted on the second class alone, the first removed for its ## prior of 0, holds LIBSVM's single label -1, whose decision value of ## 1 points to that class if (isscalar (this.Model.Label) && this.Model.Label == -1) dec_values_L = -dec_values_L; endif m = 2 * Ypm .* dec_values_L; ## A one-class model has no second class to measure a margin against, ## and R2024a returns NaN for every row, so edge is NaN too. if (classCount (this.ClassNames) == 1) m = NaN (rows (X), 1); endif endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationSVM} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationSVM} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationSVM} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {ClassificationSVM} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Compute loss for a trained ClassificationSVM object. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} computes the loss, ## @var{L}, using the default loss function @qcode{'classiferror'}. ## ## @itemize ## @item ## @code{obj} is a @var{ClassificationSVM} object trained on ## @code{X} and @code{Y}. ## @item ## @code{X} must be a @math{N*P} numeric matrix of input data where rows ## correspond to observations and columns correspond to features or ## variables. ## @item ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels ## of corresponding predictor data in @var{X}. @var{Y} must have same ## numbers of Rows as @var{X}. ## @end itemize ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{L} = loss (@dots{}, @var{name}, @var{value})} allows ## additional options specified by @var{name}-@var{value} pairs: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'LossFun'} @tab Specifies the loss function to use. ## Can be a function handle with four input arguments (C, S, W, Cost) ## which returns a scalar value or one of: ## 'binodeviance', 'classifcost', 'classiferror', 'exponential', ## 'hinge', 'logit','mincost', 'quadratic'. ## @itemize ## @item ## @code{C} is a logical matrix of size @math{N*K}, where @math{N} is the ## number of observations and @math{K} is the number of classes. ## The element @code{C(i,j)} is true if the class label of the i-th ## observation is equal to the j-th class. ## @item ## @code{S} is a numeric matrix of size @math{N*K}, where each element ## represents the classification score for the corresponding class. ## @item ## @code{W} is a numeric vector of length @math{N}, representing ## the observation weights. ## @item ## @code{Cost} is a @math{K*K} matrix representing the misclassification ## costs. ## @end itemize ## ## @item @qcode{'Weights'} @tab Specifies observation weights, must be ## a numeric vector of length equal to the number of rows in X. ## Default is @code{ones (size (X, 1))}. loss normalizes the weights so that ## observation weights in each class sum to the prior probability of that ## class. When you supply Weights, loss computes the weighted ## classification loss. ## ## @end multitable ## ## @seealso{ClassificationSVM} ## @end deftypefn function L = loss (this, X, Y, varargin) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationSVM.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error ("ClassificationSVM.loss: Name-Value arguments must be in pairs."); endif ## Check for valid X if (isempty (X)) error ("ClassificationSVM.loss: X is empty."); elseif (columns (this.X) != columns (X)) error (strcat ("ClassificationSVM.loss: X must have the same", ... " number of predictors as the trained model.")); endif ## Check for valid Y if (isempty (Y)) error ("ClassificationSVM.loss: Y is empty."); elseif (rows (X)!= rows (Y)) error (strcat ("ClassificationSVM.loss: Y must have the same", ... " number of rows as X.")); endif ## Parse optional paired arguments; 'Weights' are uniform unless given W0 = ones (rows (X), 1); optNames = {'LossFun', 'Weights'}; dfValues = {'classiferror', W0}; [LossFun, Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (ischar (LossFun))) error ("ClassificationSVM.loss: 'LossFun' must be a character vector."); endif LossFun = tolower (LossFun); if (! any (strcmpi (LossFun, {'binodeviance', 'classiferror', ... 'classifcost', 'exponential', ... 'hinge', 'logit', 'mincost', ... 'quadratic'}))) error ("ClassificationSVM.loss: unsupported Loss function."); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("ClassificationSVM.loss: %s", errmsg); endif if (! (isnumeric (Weights) && isvector (Weights))) error ("ClassificationSVM.loss: 'Weights' must be a numeric vector."); endif if (numel (Weights) != size (X, 1)) error (strcat ("ClassificationSVM.loss: size of 'Weights' must be", ... " equal to the number of rows in X.")); endif if (! isempty (args)) error ("ClassificationSVM.loss: invalid optional paired argument."); endif ## Y may be the class labels, which is what this method documents and ## what MATLAB accepts, or already the +1/-1 coding the solver works in. ## margin was given this treatment and loss was missed, so the labels ## the docstring promises reached LIBSVM unmapped. Ypm = svmPlusMinus (Y, this.ClassNames); ## The weights are scaled so that each class carries its prior and the ## loss is their weighted sum, as in MATLAB. The prior of a model fitted ## with weights is the weighted class frequency, so counting every row ## alike, as this did, disagreed with it. Weights = priorNormalize (double (Weights(:)), 1 + (Ypm(:) == -1), ... this.Prior); ## Compute the classification score ## The model scores the coded, standardized predictors, which is the ## scale predict gives it; X taken as it stands was scored unscaled. if (! isempty (this.Coding_)) X = dummyCoding (X, this.Coding_); endif if (! isempty (this.Mu)) X = (X - this.Mu) ./ this.Sigma; endif [~, ~, dec_values_L] = svmpredict (Ypm, X, this.Model, '-q'); ## A model fitted on the second class alone, the first removed for its ## prior of 0, holds LIBSVM's single label -1, whose decision value of ## 1 points to that class if (isscalar (this.Model.Label) && this.Model.Label == -1) dec_values_L = -dec_values_L; endif ## Compute the margin margin = Ypm .* dec_values_L; ## Compute the loss based on the specified loss function switch (LossFun) case 'classiferror' ## A one-class model labels every row with its one class, so it ## never misclassifies one; R2024a reports 0. if (classCount (this.ClassNames) == 1) L = 0; else L = sum ((margin <= 0) .* Weights); endif case 'hinge' L = sum (max (0, 1 - margin) .* Weights); case 'logit' L = sum (log (1 + exp (-margin)) .* Weights); case 'exponential' L = sum (exp (-margin) .* Weights); case 'quadratic' L = sum (((1 - margin) .^2) .* Weights); case 'binodeviance' L = sum (log (1 + exp (-2 * margin)) .* Weights); case 'mincost' ## Each observation is assigned to the class of least expected ## cost, and charged what that assignment actually costs given ## its true class. Y is the +1/-1 coding the margin above uses, ## in which +1 is the first of ClassNames and -1 the second. [~, scores] = predict (this, X); true_idx = ones (rows (X), 1); true_idx(Y == -1) = 2; L = 0; for i = 1:rows (X) [~, k] = min (scores(i,:) * this.Cost); L = L + Weights(i) * this.Cost(true_idx(i), k); endfor case 'classifcost' ## What the model's own prediction costs, given the true class. ## A one-class model predicts its one class for every row, so an ## outlier costs nothing, as R2024a reports. pred_idx = ones (rows (X), 1); if (classCount (this.ClassNames) == 2) pred_idx(dec_values_L <= 0) = 2; endif true_idx = ones (rows (X), 1); true_idx(Y == -1) = 2; L = 0; for i = 1:rows (X) L = L + Weights(i) * this.Cost(true_idx(i), pred_idx(i)); endfor otherwise error ("ClassificationSVM.loss: unsupported Loss function."); endswitch endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationSVM} {@var{L} =} resubLoss (@var{obj}) ## @deftypefnx {ClassificationSVM} {@var{L} =} resubLoss (@dots{}, @var{name}, @var{value}) ## ## Compute resubstitution loss for a trained ClassificationSVM object. ## ## @code{@var{L} = resubLoss (@var{obj})} computes the resubstitution loss, ## @var{L}, using the default loss function @qcode{'classiferror'}. ## ## @itemize ## @item ## @code{obj} is a @var{ClassificationSVM} object trained on ## @code{X} and @code{Y}. ## @end itemize ## ## @code{@var{L} = resubLoss (@dots{}, @var{name}, @var{value})} allows ## additional options specified by @var{name}-@var{value} pairs: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'LossFun'} @tab Specifies the loss function to use. ## Can be a function handle with four input arguments (C, S, W, Cost) ## which returns a scalar value or one of: ## 'binodeviance', 'classifcost', 'classiferror', 'exponential', ## 'hinge', 'logit','mincost', 'quadratic'. ## @itemize ## @item ## @code{C} is a logical matrix of size @math{N*K}, where @math{N} is the ## number of observations and @math{K} is the number of classes. ## The element @code{C(i,j)} is true if the class label of the i-th ## observation is equal to the j-th class. ## @item ## @code{S} is a numeric matrix of size @math{N*K}, where each element ## represents the classification score for the corresponding class. ## @item ## @code{W} is a numeric vector of length @math{N}, representing ## the observation weights. ## @item ## @code{Cost} is a @math{K*K} matrix representing the misclassification ## costs. ## @end itemize ## ## @item @qcode{'Weights'} @tab Specifies observation weights, must be ## a numeric vector of length equal to the number of rows in X. ## Default is @code{ones (size (X, 1))}. loss normalizes the weights so that ## observation weights in each class sum to the prior probability of that ## class. When you supply Weights, loss computes the weighted ## classification loss. ## ## @end multitable ## ## @seealso{ClassificationSVM} ## @end deftypefn function L = resubLoss (this, varargin) if (mod (nargin, 2) != 1) error (strcat ("ClassificationSVM.resubLoss: Name-Value", ... " arguments must be in pairs.")); endif ## Parse optional paired arguments; the training rows keep the weights ## they were fitted with unless others are given W0 = this.RawWeights; if (isempty (W0)) W0 = ones (rows (this.X), 1); endif optNames = {'LossFun', 'Weights'}; dfValues = {'classiferror', W0}; [LossFun, Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (ischar (LossFun))) error (strcat ("ClassificationSVM.resubLoss: 'LossFun' must be a", ... " character vector.")); endif LossFun = tolower (LossFun); if (! any (strcmpi (LossFun, {'binodeviance', 'classiferror', ... 'classifcost', 'exponential', ... 'hinge', 'logit', 'mincost', ... 'quadratic'}))) error ("ClassificationSVM.resubLoss: unsupported Loss function."); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("ClassificationSVM.resubLoss: %s", errmsg); endif if (! (isnumeric (Weights) && isvector (Weights))) error (strcat ("ClassificationSVM.resubLoss: 'Weights' must be a", ... " numeric vector.")); endif if (numel (Weights) != size (this.X, 1)) error (strcat ("ClassificationSVM.resubLoss: size of 'Weights'", ... " must be equal to the number of rows in X.")); endif if (! isempty (args)) error (strcat ("ClassificationSVM.resubLoss: invalid optional", ... " paired argument.")); endif ## The loss of the model on its own training data. This used to ## recompute every loss here from this.Y, which holds the labels as ## they were given: the margin wants the +1/-1 coding, so a 1/2 coded ## response scored 0.49 where the answer was 0.01, and a logical one ## did not reach LIBSVM at all. The rows dropped for missing values ## were included as well. used = true (rows (this.X), 1); Xu = this.X(used, :); gY = labelIndices (this.ClassNames, this.Y(used, :)); Ypm = ones (rows (Xu), 1); Ypm(gY == 2) = -1; L = loss (this, Xu, Ypm, 'LossFun', LossFun, 'Weights', Weights); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationSVM} {@var{CVMdl} =} crossval (@var{obj}) ## @deftypefnx {ClassificationSVM} {@var{CVMdl} =} crossval (@dots{}, @var{name}, @var{value}) ## ## Cross Validate a Support Vector Machine classification object. ## ## @code{@var{CVMdl} = crossval (@var{obj})} returns a cross-validated model ## object, @var{CVMdl}, from a trained model, @var{obj}, using 10-fold ## cross-validation by default. ## ## @code{@var{CVMdl} = crossval (@var{obj}, @var{name}, @var{value})} ## specifies additional name-value pair arguments to customize the ## cross-validation process. ## ## @multitable @columnfractions 0.28 0.7 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'KFold'} @tab Specify the number of folds to use in ## k-fold cross-validation. @code{"KFold", @var{k}}, where @var{k} is an ## integer greater than 1. ## ## @item @qcode{'Holdout'} @tab Specify the fraction of the data to ## hold out for testing. @code{"Holdout", @var{p}}, where @var{p} is a ## scalar in the range @math{(0,1)}. ## ## @item @qcode{'Leaveout'} @tab Specify whether to perform ## leave-one-out cross-validation. @code{"Leaveout", @var{Value}}, where ## @var{Value} is 'on' or 'off'. ## ## @item @qcode{'CVPartition'} @tab Specify a @qcode{cvpartition} ## object used for cross-validation. @code{"CVPartition", @var{cv}}, where ## @code{isa (@var{cv}, "cvpartition")} = 1. ## ## @end multitable ## ## @seealso{fitcsvm, ClassificationSVM, cvpartition, ## ClassificationPartitionedModel} ## @end deftypefn function CVMdl = crossval (this, varargin) ## Check for sufficient input arguments if (nargin < 1) error ("ClassificationSVM.crossval: too few input arguments."); endif if (numel (varargin) == 1) error (strcat ("ClassificationSVM.crossval: Name-Value arguments", ... " must be in pairs.")); elseif (numel (varargin) > 2) error (strcat ("ClassificationSVM.crossval: specify only one of", ... " the optional Name-Value paired arguments.")); endif ## Add default values if (this.NumObservations < 10) numFolds = this.NumObservations; else numFolds = 10; endif Holdout = []; Leaveout = 'off'; CVPartition = []; ## Parse extra parameters while (numel (varargin) > 0) switch (tolower (varargin {1})) case 'kfold' numFolds = varargin{2}; if (! (isnumeric (numFolds) && isscalar (numFolds) && (numFolds == fix (numFolds)) && numFolds > 1)) error (strcat ("ClassificationSVM.crossval: 'KFold' must", ... " be an integer value greater than 1.")); endif case 'holdout' Holdout = varargin{2}; if (! (isnumeric (Holdout) && isscalar (Holdout) && Holdout > 0 && Holdout < 1)) error (strcat ("ClassificationSVM.crossval: 'Holdout' must", ... " be a numeric value between 0 and 1.")); endif case 'leaveout' Leaveout = varargin{2}; if (! (ischar (Leaveout) && (strcmpi (Leaveout, 'on') || strcmpi (Leaveout, 'off')))) error (strcat ("ClassificationSVM.crossval: 'Leaveout'", ... " must be either 'on' or 'off'.")); endif case 'cvpartition' CVPartition = varargin{2}; if (! (isa (CVPartition, 'cvpartition'))) error (strcat ("ClassificationSVM.crossval: 'CVPartition'",... " must be a 'cvpartition' object.")); endif otherwise error (strcat ("ClassificationSVM.crossval: invalid",... " parameter name in optional paired arguments.")); endswitch varargin(1:2) = []; endwhile ## Determine the cross-validation method to use. The partition covers ## the observations actually trained on: a row dropped for a missing ## value is not one the folds can use, and including it would leave the ## partition, the stored data and NumObservations disagreeing. The ## response is passed rather than a count so the folds stay stratified. Yused = this.Y; if (! isempty (CVPartition)) partition = CVPartition; elseif (! isempty (Holdout)) partition = cvpartition (Yused, 'Holdout', Holdout); elseif (strcmpi (Leaveout, 'on')) partition = cvpartition (this.NumObservations, 'LeaveOut'); else partition = cvpartition (Yused, 'KFold', numFolds); endif ## Create a cross-validated model object CVMdl = ClassificationPartitionedModel (this, partition); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationSVM} {@var{CVMdl} =} compact (@var{obj}) ## ## Create a CompactClassificationSVM object. ## ## @code{@var{CVMdl} = compact (@var{obj})} creates a compact version of the ## ClassificationSVM object, @var{obj}. ## ## @seealso{fitcsvm, ClassificationSVM, CompactClassificationSVM} ## @end deftypefn function CVMdl = compact (this) ## Create a compact model CVMdl = CompactClassificationSVM (this); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationSVM} {@var{ScoreSVMModel} =} fitPosterior (@var{obj}) ## @deftypefnx {ClassificationSVM} {@var{ScoreSVMModel} =} fitPosterior (@var{obj}, @var{name}, @var{value}) ## @deftypefnx {ClassificationSVM} {[@var{ScoreSVMModel}, @var{ScoreTransform}] =} fitPosterior (@dots{}) ## ## Fit the transformation from scores to posterior probabilities. ## ## @code{@var{ScoreSVMModel} = fitPosterior (@var{obj})} returns the ## two-class model @var{obj} with the transformation from the score of the ## second class, @code{@var{obj}.ClassNames(2)}, to its posterior ## probability stored in its @qcode{ScoreTransform} property, so that ## @code{predict} and @code{resubPredict} return posterior probabilities, ## the two columns summing to 1. The transformation is fitted to scores ## the model gives observations it was not trained on, by 10-fold ## cross-validation unless one of the following options says otherwise. ## ## @multitable @columnfractions 0.28 0.7 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'KFold'} @tab The number of folds, an integer greater ## than 1. The default is 10. ## ## @item @qcode{'Holdout'} @tab The fraction of the data held out, a ## scalar in the range @math{(0,1)}; the transformation is fitted to the ## scores of the observations held out. ## ## @item @qcode{'Leaveout'} @tab @qcode{'on'} for leave-one-out ## cross-validation, or @qcode{'off'} (default). ## ## @item @qcode{'CVPartition'} @tab A @qcode{cvpartition} object over the ## observations of @var{obj}. ## @end multitable ## ## Only one of these options may be given. The transformation is one of ## three: ## ## @itemize ## @item A sigmoid @code{1 / (1 + exp (A * s + B))} of the score @math{s} ## when the scores of the two classes overlap, with @math{A} and @math{B} ## fitted by maximum likelihood, as a logistic regression of the class on ## the score. Neither the observation weights nor the prior enter the ## fit, as in MATLAB. ## @item A step function when the scores separate the classes: 0 below the ## largest score of the first class, 1 above the smallest score of the ## second, and @code{@var{obj}.Prior(2)} between, with a warning. ## @item A constant when one class has prior probability 0, posterior 1 ## for the other class. ## @end itemize ## ## A @qcode{ScoreTransform} already set on @var{obj} is reset to ## @qcode{'none'} first, with a warning. The property holds the ## transformation as @qcode{'@@(S)sigmoid(S,A,B)'}, ## @qcode{'@@(S)step(S,LowerBound,UpperBound,PositiveClassProbability)'} ## or @qcode{'@@(S)constant(S,K)'}, @math{K} being the index of the ## predicted class. Each parameter is written exactly, in the fewest ## significant digits that read back to it, where MATLAB shows seven, so ## a model saved and reloaded predicts as before. Such a text may also be ## assigned to the property directly, which MATLAB refuses. ## ## @code{[@var{ScoreSVMModel}, @var{ScoreTransform}] = fitPosterior ## (@dots{})} also returns the parameters in a structure whose field ## @qcode{Type} is @qcode{'sigmoid'}, with fields @qcode{Slope} and ## @qcode{Intercept}, @qcode{'step'}, with fields @qcode{LowerBound}, ## @qcode{UpperBound} and @qcode{PositiveClassProbability}, or ## @qcode{'constant'}, with the field @qcode{PredictedClass}. ## ## @seealso{fitSVMPosterior, CompactClassificationSVM.fitPosterior, ## ClassificationSVM.predict, fitcsvm, cvpartition} ## @end deftypefn function [this, ScoreTransform] = fitPosterior (this, varargin) if (classCount (this.ClassNames) != 2) error (strcat ("ClassificationSVM.fitPosterior: the model must", ... " have exactly two classes.")); endif ## Only the cross-validation options, which cvPartitionOf builds the ## partition from; it takes 'CrossVal' too, which is not one of them if (any (strcmpi (varargin(1:2:end), 'CrossVal'))) error (strcat ("ClassificationSVM.fitPosterior: unknown", ... " parameter name 'CrossVal'.")); endif [part, rest] = cvPartitionOf (varargin, this.Y, this.NumObservations, ... "ClassificationSVM.fitPosterior"); if (! isempty (rest)) if (ischar (rest{1})) error (strcat ("ClassificationSVM.fitPosterior: unknown", ... " parameter name '%s'."), rest{1}); endif error (strcat ("ClassificationSVM.fitPosterior: optional", ... " arguments must be given in Name-Value pairs.")); endif if (! strcmp (this.ScoreTransform, 'none')) warning (strcat ("ClassificationSVM.fitPosterior: resetting", ... " 'ScoreTransform' to 'none' before fitting the", ... " transformation.")); this.ScoreTransform = 'none'; endif ## Scores of observations left out of the fit, which a prior of zero ## makes unnecessary s = []; k = []; if (all (this.Prior > 0)) CVMdl = crossval (this, 'CVPartition', part); [~, scores] = kfoldPredict (CVMdl); s = scores(:,2); k = labelIndices (this.ClassNames, this.Y); endif [st, ScoreTransform, separated] = svmPosterior (s, k, this.Prior, ... this.ClassNames); if (separated) warning (strcat ("ClassificationSVM.fitPosterior: the classes are", ... " perfectly separated, so the transformation is a", ... " step function.")); endif this.ScoreTransform = st; endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationSVM} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationSVM} {@var{e} =} edge (@dots{}, @qcode{"Weights"}, @var{w}) ## @deftypefnx {ClassificationSVM} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationSVM} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Classification edge, the mean of the classification margins. ## ## @code{@var{e} = edge (@var{obj}, @var{X}, @var{Y})} reduces the vector ## that @code{margin} returns to a single number, the mean margin over the ## rows of @var{X}. It says how far the model puts the true class ahead of ## its nearest rival on average, so a larger edge is a better model, and ## unlike a loss it is not bounded above and rewards confidence rather than ## bare correctness. ## ## @code{@var{e} = edge (@dots{}, @qcode{"Weights"}, @var{w})} takes the ## weighted mean instead, with one weight per row of @var{X}. ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @end deftypefn function e = edge (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationSVM.edge: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'edge', X, Y, varargin, ... nargin > 2); ## The weights are parsed before anything is computed, so a bad ## Name-Value pair is reported as such rather than after a margin. W = edgeWeights (varargin, Y, this.ClassNames, this.Prior, ... "ClassificationSVM", "edge"); m = margin (this, X, Y); e = sum (W .* m(:)) / sum (W); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationSVM} {@var{m} =} resubMargin (@var{obj}) ## ## Classification margins of the model on its own training data. ## ## @code{@var{m} = resubMargin (@var{obj})} is @code{margin} applied to the ## observations the model was fitted on, one number per observation. Being ## a resubstitution quantity it is optimistic by construction. ## ## @end deftypefn function m = resubMargin (this) m = margin (this, this.X, this.Y); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationSVM} {@var{e} =} resubEdge (@var{obj}) ## ## Classification edge of the model on its own training data. ## ## @code{@var{e} = resubEdge (@var{obj})} is @code{edge} applied to the ## observations the model was fitted on, the mean of @code{resubMargin}. ## ## @end deftypefn function e = resubEdge (this) ## The training rows keep their weights, as R2024a keeps them. e = edge (this, this.X, this.Y, 'Weights', this.RawWeights); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationSVM} {} savemodel (@var{obj}, @var{filename}) ## ## Save a ClassificationSVM object. ## ## @code{savemodel (@var{obj}, @var{filename})} saves each property of a ## ClassificationSVM object into an Octave binary file, the name of which is ## specified in @var{filename}, along with an extra variable, which defines ## the type classification object these variables constitute. Use ## @code{loadmodel} in order to load a classification object into Octave's ## workspace. ## ## @seealso{loadmodel, fitcsvm, ClassificationSVM} ## @end deftypefn function savemodel (this, fname) if (nargin < 2) error ("ClassificationSVM.savemodel: too few input arguments."); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error ("ClassificationSVM.savemodel: FNAME must be a character vector."); endif ## Generate variable for class name classdef_name = 'ClassificationSVM'; ## Create variables from model properties X = this.X; Y = encodeLabels (this.Y); NumObservations = this.NumObservations; RowsUsed = this.RowsUsed; NumPredictors = this.NumPredictors; PredictorNames = this.PredictorNames; ResponseName = this.ResponseName; ClassNames = encodeLabels (this.ClassNames); ScoreTransform = this.ScoreTransform; Sigma = this.Sigma; BinEdges = this.BinEdges; Mu = this.Mu; ModelParameters = this.ModelParameters; KernelParameters = this.KernelParameters; BoxConstraints = this.BoxConstraints; OutlierFraction = this.OutlierFraction; Nu = this.Nu; Model = this.Model; Alpha = this.Alpha; Beta = this.Beta; Bias = this.Bias; IsSupportVector = this.IsSupportVector; SupportVectorLabels = this.SupportVectorLabels; SupportVectors = this.SupportVectors; W = this.W; RawWeights = this.RawWeights; Prior = this.Prior; Cost = this.Cost; CategoricalPredictors = this.CategoricalPredictors; Coding_ = this.Coding_; ExpandedPredictorNames = this.ExpandedPredictorNames; ## Save classdef name and all model properties as individual variables STfun = this.STfun; HyperparameterOptimizationResults = this.HyperparameterOptimizationResults; save ('-binary', fname, 'classdef_name', 'X', 'Y', 'NumObservations', ... 'RowsUsed', 'NumPredictors', 'PredictorNames', 'BinEdges', ... 'ResponseName', ... 'ClassNames', 'ScoreTransform', 'Sigma', 'Mu', ... 'ModelParameters', 'Model', 'Alpha', 'Beta', 'Bias', ... 'IsSupportVector', 'SupportVectorLabels', 'SupportVectors', ... 'W', 'RawWeights', 'Prior', 'Cost', 'CategoricalPredictors', ... 'Coding_', ... 'ExpandedPredictorNames', 'KernelParameters', ... 'BoxConstraints', 'OutlierFraction', 'Nu', 'STfun', ... 'HyperparameterOptimizationResults'); endfunction endmethods methods(Static, Hidden) function mdl = load_model (filename, data) data = decodeLabels (data); ## Create a ClassificationSVM object mdl = ClassificationSVM (1, 1); ## Copy the saved data into the object. Iterate over what was ## saved rather than over fieldnames (mdl): a private property such ## as STfun is written out by savemodel but is not reported by ## fieldnames, so comparing the two sets could never match and every ## load failed. Assignment is legal here because this is a method of ## the class itself. names = fieldnames (data); ## The set methods for these read other properties, and one of them ## rebuilds Coeffs, so they are assigned once everything else is in ## place rather than in the order the file happens to list them. late = ismember (names, {'Cost', 'Prior', 'ScoreTransform', ... 'ResponseTransform'}); names = [names(! late); names(late)]; for i = 1:numel (names) try mdl.(names{i}) = data.(names{i}); catch error ("ClassificationSVM.load_model: invalid model in '%s'.", filename) end_try_catch endfor ## A model written before RowsUsed became a mask stored it as a ## double, which is a valid subscript for nothing. An empty RowsUsed ## means every observation was used and stays an empty double. if (! isempty (mdl.RowsUsed)) mdl.RowsUsed = logical (mdl.RowsUsed); endif endfunction endmethods endclassdef %!demo %! ## Create a Support Vector Machine classifier and determine margin for test %! ## data. %! load fisheriris %! %! ## Select indices of the non-setosa species %! inds = ! strcmp (species, 'setosa'); %! %! ## Select features and labels for non-setosa species %! X = meas(inds, 3:4); %! Y = grp2idx (species(inds)); %! %! ## Convert labels to +1 and -1 %! unique_classes = unique (Y); %! Y(Y == unique_classes(1)) = -1; %! Y(Y == unique_classes(2)) = 1; %! %! ## Partition data for training and testing %! cv = cvpartition (Y, 'HoldOut', 0.15); %! X_train = X(training(cv), :); %! Y_train = Y(training(cv)); %! X_test = X(test (cv), :); %! Y_test = Y(test (cv)); %! %! ## Train the SVM model %! CVSVMModel = fitcsvm (X_train, Y_train); %! %! ## Calculate margins %! m = margin (CVSVMModel, X_test, Y_test); %! disp (m); %!demo %! ## Create a Support Vector Machine classifier and determine loss for test %! ## data. %! load fisheriris %! %! ## Select indices of the non-setosa species %! inds = ! strcmp (species, 'setosa'); %! %! ## Select features and labels for non-setosa species %! X = meas(inds, 3:4); %! Y = grp2idx (species(inds)); %! %! ## Convert labels to +1 and -1 %! unique_classes = unique (Y); %! Y(Y == unique_classes(1)) = -1; %! Y(Y == unique_classes(2)) = 1; %! %! ## Randomly partition the data into training and testing sets %! cv = cvpartition (Y, 'HoldOut', 0.3); # 30% data for testing, 60% for training %! %! X_train = X(training(cv), :); %! Y_train = Y(training(cv)); %! %! X_test = X(test (cv), :); %! Y_test = Y(test (cv)); %! %! ## Train the SVM model %! SVMModel = fitcsvm (X_train, Y_train); %! %! ## Calculate loss %! %! L = loss (SVMModel,X_test,Y_test,'LossFun','binodeviance') %! L = loss (SVMModel,X_test,Y_test,'LossFun','classiferror') %! L = loss (SVMModel,X_test,Y_test,'LossFun','exponential') %! L = loss (SVMModel,X_test,Y_test,'LossFun','hinge') %! L = loss (SVMModel,X_test,Y_test,'LossFun','logit') %! L = loss (SVMModel,X_test,Y_test,'LossFun','quadratic') ## Test output of constructor %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1; 4, 5, 6; 7, 8, 9; ... %! 3, 2, 1; 4, 5, 6; 7, 8, 9; 3, 2, 1; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = [1; 2; 3; 4; 2; 3; 4; 2; 3; 4; 2; 3; 4]; %! a = ClassificationSVM (x, y, 'ClassNames', [1, 2]); %! assert_equal (class (a), "ClassificationSVM"); %! m = logical ([1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0]'); %! assert_equal (a.RowsUsed, m); %! assert_equal ({a.X, a.Y}, {x(m,:), y(m)}) %! assert_equal (a.NumObservations, 5) %! assert_equal ({a.ResponseName, a.PredictorNames}, {'Y', {'x1', 'x2', 'x3'}}) %! assert_equal ({a.ClassNames, a.ModelParameters.SVMtype}, {[1; 2], 'c_svc'}) %!test %! x = [1, 2; 2, 3; 3, 4; 4, 5; 2, 3; 3, 4; 2, 3; 3, 4; 2, 3; 3, 4]; %! y = [1; 1; -1; -1; 1; -1; -1; -1; -1; -1]; %! a = ClassificationSVM (x, y); %! assert_equal (class (a), "ClassificationSVM"); %! assert_equal ({a.X, a.Y, a.ModelParameters.KernelFunction}, {x, y, 'linear'}) %! assert_equal (a.ModelParameters.BoxConstraint, 1) %! assert_equal (a.ClassNames, [-1; 1]) %! assert_equal (a.ModelParameters.KernelOffset, 0) %!test %! x = [1, 2; 2, 3; 3, 4; 4, 5; 2, 3; 3, 4; 2, 3; 3, 4; 2, 3; 3, 4]; %! y = [1; 1; -1; -1; 1; -1; -1; -1; -1; -1]; %! a = ClassificationSVM (x, y, 'KernelFunction', 'rbf', 'BoxConstraint', 2, ... %! 'KernelOffset', 2); %! assert_equal (class (a), "ClassificationSVM"); %! assert_equal ({a.X, a.Y, a.ModelParameters.KernelFunction}, {x, y, 'rbf'}) %! assert_equal (a.ModelParameters.BoxConstraint, 2) %! assert_equal (a.ModelParameters.KernelOffset, 2) %!test %! x = [1, 2; 2, 3; 3, 4; 4, 5; 2, 3; 3, 4; 2, 3; 3, 4; 2, 3; 3, 4]; %! y = [1; 1; -1; -1; 1; -1; -1; -1; -1; -1]; %! a = ClassificationSVM (x, y, 'KernelFunction', 'polynomial', ... %! 'PolynomialOrder', 3); %! assert_equal (class (a), "ClassificationSVM"); %! assert_equal ({a.X, a.Y, a.ModelParameters.KernelFunction}, {x, y, 'polynomial'}) %! assert_equal (a.ModelParameters.KernelPolynomialOrder, 3) ## RowsUsed is the logical mask its documentation describes, so the documented ## use of it works. It was a double, for which a 0 is not a subscript at all. %!test %! randn ('seed', 42); %! lab = double (randn (40, 1) > 0) + 1; %! X = [randn(40, 3); NaN, 1, 1]; %! Y = [lab; 1]; %! Mdl = fitcsvm (X, Y); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (rows (Mdl.X), Mdl.NumObservations); ## resubPredict answers about the rows it was trained on, all their columns. ## Selecting them with a bare mask took the first column only, and the model ## answered about data it was never given, without complaint. %!test %! randn ('seed', 42); %! lab = double (randn (40, 1) > 0) + 1; %! X = [randn(40, 3); NaN, 1, 1]; %! Y = [lab; 1]; %! Mdl = fitcsvm (X, Y); %! assert_equal (Mdl.NumObservations, 41); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (resubPredict (Mdl), predict (Mdl, Mdl.X)); ## A model saved before RowsUsed became a mask still reads back as one. %!test %! randn ('seed', 42); %! X = randn (31, 2); %! Y = [double(randn (30, 1) > 0) + 1; NaN]; %! Mdl = fitcsvm (X, Y); %! fname = tempname (); %! savemodel (Mdl, fname); %! d = load (fname); %! d.RowsUsed = double (d.RowsUsed); %! save ('-binary', fname, '-struct', 'd'); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2.RowsUsed), 'logical'); %! assert_equal (rows (M2.X(M2.RowsUsed, :)), M2.NumObservations); ## Prior defaults to the class frequencies and Cost to zero on the diagonal ## and one elsewhere, both measured against R2024a. %!test %! load fisheriris %! Yb = strcmp (species, 'setosa'); %! Mdl = fitcsvm (meas, Yb); %! assert_equal (Mdl.Prior, [2/3, 1/3], 1e-12); %! assert_equal (Mdl.Cost, [0, 1; 1, 0]); %! Yu = [repmat({'a'}, 120, 1); repmat({'b'}, 30, 1)]; %! M2 = fitcsvm (meas, Yu); %! assert_equal (M2.Prior, [0.8, 0.2], 1e-12); %! assert_equal (fitcsvm (meas, Yu, 'Prior', 'uniform').Prior, [0.5, 0.5]); %! assert_equal (fitcsvm (meas, Yu, 'Prior', [3, 1]).Prior, [0.75, 0.25]); ## Prior and Cost reach the fit, as they do in MATLAB, through the per-class ## box constraint. Asking for the defaults explicitly changes nothing. %!test %! load fisheriris %! Yu = [repmat({'a'}, 120, 1); repmat({'b'}, 30, 1)]; %! base = fitcsvm (meas, Yu); %! assert_equal (isequal (fitcsvm (meas, Yu, 'Prior', [0.5, 0.5]).Alpha, ... %! base.Alpha), false); %! assert_equal (isequal (fitcsvm (meas, Yu, 'Cost', [0, 5; 1, 0]).Alpha, ... %! base.Alpha), false); %! same = fitcsvm (meas, Yu, 'Prior', 'empirical', 'Cost', [0, 1; 1, 0]); %! assert_equal (same.Alpha, base.Alpha); %! assert_equal (same.Bias, base.Bias); ## The cost-aware losses agree with R2024a, which returns 0.01 for all three ## on this fixture and 0.04 once an error is charged four times over. %!test %! load fisheriris %! X = meas(51:150,:); %! Yb = strcmp (species(51:150), 'versicolor'); %! Ypm = ones (100, 1); %! Ypm(Yb) = -1; %! Mdl = fitcsvm (X, Yb, 'KernelFunction', 'linear'); %! assert_equal (loss (Mdl, X, Ypm, 'LossFun', 'classiferror'), 0.01, 1e-12); %! assert_equal (loss (Mdl, X, Ypm, 'LossFun', 'classifcost'), 0.01, 1e-12); %! assert_equal (loss (Mdl, X, Ypm, 'LossFun', 'mincost'), 0.01, 1e-12); %! Mc = fitcsvm (X, Yb, 'KernelFunction', 'linear', 'Cost', [0, 4; 1, 0]); %! assert_equal (loss (Mc, X, Ypm, 'LossFun', 'classifcost'), 0.04, 1e-12); %! assert_equal (loss (Mc, X, Ypm, 'LossFun', 'mincost'), 0.04, 1e-12); ## resubLoss is the loss of the model on its own training rows, whatever type ## the labels are. It formed the margin from the labels as given, so a 1/2 ## coded response scored 0.49 where the answer is 0.01, and a logical one did ## not reach LIBSVM at all. %!test %! load fisheriris %! X = meas(51:150,:); %! Yb = strcmp (species(51:150), 'versicolor'); %! assert_equal (resubLoss (fitcsvm (X, Yb, 'KernelFunction', 'linear')), ... %! 0.01, 1e-12); %! assert_equal (resubLoss (fitcsvm (X, double (Yb) + 1, ... %! 'KernelFunction', 'linear')), 0.01, 1e-12); %! assert_equal (resubLoss (fitcsvm (X, species(51:150), ... %! 'KernelFunction', 'linear')), 0.01, 1e-12); ## The compact model carries both, and its cost-aware losses agree with the ## model it was compacted from. %!test %! load fisheriris %! X = meas(51:150,:); %! Yb = strcmp (species(51:150), 'versicolor'); %! Ypm = ones (100, 1); %! Ypm(Yb) = -1; %! Mdl = fitcsvm (X, Yb, 'KernelFunction', 'linear', 'Cost', [0, 4; 1, 0]); %! CMdl = compact (Mdl); %! assert_equal (CMdl.Prior, Mdl.Prior); %! assert_equal (CMdl.Cost, Mdl.Cost); %! for f = {'classiferror', 'classifcost', 'mincost'} %! assert_equal (loss (CMdl, X, Ypm, 'LossFun', f{1}), ... %! loss (Mdl, X, Ypm, 'LossFun', f{1}), 1e-12); %! endfor ## discardSupportVectors empties what R2024a empties and keeps what it ## keeps: Alpha and the support vectors go, Beta, Bias and IsSupportVector ## stay, and the class is unchanged. %!test %! load fisheriris %! keep = ! strcmp (species, "setosa"); %! X = meas(keep,:); y = species(keep); %! Mdl = fitcsvm (X, y, "KernelFunction", "linear"); %! D = discardSupportVectors (Mdl); %! assert_equal (class (D), "ClassificationSVM"); %! assert_equal (isempty (D.Alpha), true); %! assert_equal (isempty (D.SupportVectors), true); %! assert_equal (isempty (D.SupportVectorLabels), true); %! assert_equal (D.Beta, Mdl.Beta); %! assert_equal (D.Bias, Mdl.Bias); %! assert_equal (D.IsSupportVector, Mdl.IsSupportVector); ## A linear decision needs only Beta and Bias, so the model predicts what it ## predicted before. %!test %! load fisheriris %! keep = ! strcmp (species, "setosa"); %! X = meas(keep,:); y = species(keep); %! Mdl = fitcsvm (X, y, "KernelFunction", "linear"); %! D = discardSupportVectors (Mdl); %! assert_equal (predict (D, X), predict (Mdl, X), 1e-10); ## The saving is real rather than cosmetic: the engine keeps its own copy of ## the support vectors, and it collapses to the one vector that decides a ## linear model. Emptying the properties alone would free nothing. %!test %! load fisheriris %! keep = ! strcmp (species, "setosa"); %! X = meas(keep,:); y = species(keep); %! Mdl = fitcsvm (X, y, "KernelFunction", "linear"); %! D = discardSupportVectors (Mdl); %! assert_equal (rows (Mdl.Model.SVs) > 1, true); %! assert_equal (rows (D.Model.SVs), 1); %! assert_equal (predict (discardSupportVectors (D), X), predict (D, X)); ## A response naming its classes in the rows of a character matrix is one ## of the documented types and MATLAB accepts it on every classifier. The ## whole surface below was broken and untested, which is why it stayed so. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcsvm (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcsvm (Xch, Ycell); %! assert_equal (size (Mc.ClassNames), [2, 10]); %! assert_equal (cellstr (Mc.ClassNames), Ms.ClassNames); ## predict returns whole names, not their first letters. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcsvm (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcsvm (Xch, Ycell); %! pch = predict (Mc, Xch); %! assert_equal (columns (pch), 10); %! assert_equal (cellstr (pch), predict (Ms, Xch)); ## loss, margin and edge read a character response as the same response. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcsvm (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcsvm (Xch, Ycell); %! assert_equal (loss (Mc, Xch, Ych), loss (Ms, Xch, Ycell), 1e-12); %! assert_equal (margin (Mc, Xch, Ych), margin (Ms, Xch, Ycell), 1e-12); %! assert_equal (edge (Mc, Xch, Ych), edge (Ms, Xch, Ycell), 1e-12); ## A character matrix pads its rows out to the longest name, and the padding ## is part of the name: R2024a reports ClassNames of ['ab '; 'abcd']. %!test %! Xpad = [1 2; 3 4; 1.1 2.1; 3.1 4.1; 1.2 2.2; 3.2 4.2]; %! Ypad = char ({"ab", "abcd", "ab", "abcd", "ab", "abcd"}); %! rand ("state", 1); randn ("state", 1); %! Mp = fitcsvm (Xpad, Ypad); %! assert_equal (size (Mp.ClassNames), [2, 4]); %! assert_equal (Mp.ClassNames(1,:), "ab "); ## A row dropped for a missing predictor is the only case that exercises ## indexing the response by row rather than by element. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! Xmiss = Xch; Xmiss(3,2) = NaN; %! rand ("state", 1); randn ("state", 1); Md = fitcsvm (Xmiss, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcsvm (Xmiss, Ycell); %! assert_equal (size (Md.ClassNames), [2, 10]); %! assert_equal (cellstr (Md.ClassNames), Ms.ClassNames); ## ClassNames may itself be given as a character matrix, which selects the ## classes by whole name: ismember between two character matrices compares ## them character by character and would select by letter. %!test %! load fisheriris %! rand ("state", 1); randn ("state", 1); %! Mf = fitcsvm (meas, char (species), ... %! "ClassNames", char ({"versicolor", "virginica"})); %! assert_equal (rows (Mf.ClassNames), 2); %! assert_equal (cellstr (Mf.ClassNames), {"versicolor"; "virginica"}); ## A model fitted from a character response comes back off disk unchanged. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcsvm (Xch, Ych); %! fname = tempname (); %! savemodel (Mc, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.ClassNames, Mc.ClassNames); %! assert_equal (predict (M2, Xch), predict (Mc, Xch)); ## crossval carries a character response through cvpartition and back. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Mc = fitcsvm (Xch, Ych); %! rand ("state", 1); randn ("state", 1); Ms = fitcsvm (Xch, Ycell); %! rand ("state", 2); cvc = crossval (Mc, "KFold", 3); %! rand ("state", 2); cvs = crossval (Ms, "KFold", 3); %! assert_equal (cellstr (kfoldPredict (cvc)), kfoldPredict (cvs)); %!test # A row missing a predictor takes the class of largest prior %! X = [(1:10)', mod((1:10)', 3)]; %! y = [1; 1; 1; 1; 1; 1; 2; 2; 2; 2]; %! Mdl = ClassificationSVM (X, y); %! [label, score, cost] = predict (Mdl, [NaN, 1]); %! assert_equal (label, 1); %! assert_equal (score, [NaN, NaN]); %! assert_equal (cost, [NaN, NaN]); %! Mdl = ClassificationSVM (X, y, 'Prior', [0.3, 0.7]); %! assert_equal (predict (Mdl, [NaN, 1]), 2); %!test # resubPredict on a row missing a predictor %! X = [NaN, 1; (2:10)', mod((2:10)', 3)]; %! y = [1; 1; 1; 1; 1; 1; 2; 2; 2; 2]; %! [label, score, cost] = resubPredict (ClassificationSVM (X, y)); %! assert_equal (label(1), 1); %! assert_equal (score(1,:), [NaN, NaN]); %! assert_equal (cost(1,:), [NaN, NaN]); ## MATLAB parity: observation weights scale each observation's box ## constraint, n * C * w with w normalized to the cost-adjusted prior; the ## empirical prior sums them and W carries the prior. Values from R2024a. %!test %! load fisheriris %! X = meas(51:150,[1, 3]); %! Y = species(51:150); %! k = (1:100)'; %! w = (1 + mod (k, 4)) .* (1 + 2 * (k > 50)); %! Mdl = fitcsvm (X, Y, 'Weights', w); %! assert_equal (Mdl.Prior, [0.25, 0.75], 1e-15); %! assert_equal (Mdl.W([1, 51])', [0.004, 0.024], 1e-15); %! assert_equal (Mdl.BoxConstraints([1, 2, 51])', [0.4, 0.6, 2.4], 1e-14); %! assert_equal (sum (Mdl.IsSupportVector), 33); %! assert_equal (resubLoss (Mdl), 0.036, 1e-15); %! assert_equal (loss (Mdl, X, Y), 0.035, 1e-15); %!test %! load fisheriris %! X = meas(51:150,[1, 3]); %! Y = species(51:150); %! k = (1:100)'; %! w = (1 + mod (k, 4)) .* (1 + 2 * (k > 50)); %! Mdl = fitcsvm (X, Y, 'Weights', w, 'Prior', 'uniform'); %! assert_equal (Mdl.W([1, 51])', [0.008, 0.016], 1e-15); %! assert_equal (Mdl.BoxConstraints([1, 2, 51])', [0.8, 1.2, 1.6], 1e-14); %!test %! load fisheriris %! X = meas(51:150,[1, 3]); %! Y = species(51:150); %! k = (1:100)'; %! w = (1 + mod (k, 4)) .* (1 + 2 * (k > 50)); %! Mdl = fitcsvm (X, Y, 'Weights', w, 'Cost', [0, 1; 2, 0]); %! assert_equal (Mdl.W([1, 51])', [0.004, 0.024], 1e-15); %! assert_equal (Mdl.BoxConstraints([1, 51])', ... %! [0.228571428571429, 2.74285714285714], 1e-14); %!test %! load fisheriris %! X = meas(51:150,[1, 3]); %! Y = species(51:150); %! k = (1:100)'; %! w = (1 + mod (k, 4)) .* (1 + 2 * (k > 50)); %! Mdl = fitcsvm (X, Y, 'Weights', w, 'Standardize', true); %! assert_equal (Mdl.Mu, [6.4276, 5.2492], 1e-13); %! assert_equal (Mdl.Sigma, [0.614100287471456, 0.737655900905504], 1e-14); %!test %! load fisheriris %! X = meas(51:150,[1, 3]); %! Y = species(51:150); %! k = (1:100)'; %! w = (1 + mod (k, 4)) .* (1 + 2 * (k > 50)); %! w([2, 60]) = 0; %! Mdl = fitcsvm (X, Y, 'Weights', w); %! assert_equal (Mdl.NumObservations, 98); ## MATLAB parity: a one-class model labels every row with its one class and ## ignores observation weights in the fit. Measured on R2024a. %!test %! load fisheriris %! Xo = meas(51:100,[1, 3]); %! M = fitcsvm (Xo, ones (50, 1)); %! [lab, s] = predict (M, [6, 4.3; 5, 3.5; 7, 4.7; 5.5, 5]); %! assert_equal (lab, ones (4, 1)); %! assert_equal (size (s), [4, 1]); %! assert_equal (resubLoss (M), 0); %! Mw = fitcsvm (Xo, ones (50, 1), 'Weights', 1 + mod ((1:50)', 4)); %! assert_equal (Mw.Bias, M.Bias); %! assert_equal (Mw.BoxConstraints, ones (50, 1)); %! assert_equal (Mw.W(1:2)', [0.016, 0.024], 1e-15); %! lab = predict (fitcsvm (Xo, repmat ({'a'}, 50, 1)), [5, 3.5; 7, 4.7]); %! assert_equal (lab, {'a'; 'a'}); ## MATLAB parity: a one-class bias puts the least supported support vector ## on the boundary, or, with OutlierFraction, leaves that fraction of the ## training rows below it. Values from R2024a (linear kernel, exact). %!test %! load fisheriris %! Xo = meas(51:100,[1, 3]); %! M = fitcsvm (Xo, ones (50, 1), 'KernelFunction', 'linear', 'Nu', 0.5); %! assert_equal (M.Bias, -998.52, 1e-10); %! assert_equal (sum (M.IsSupportVector), 25); %! M = fitcsvm (Xo, ones (50, 1), 'KernelFunction', 'linear', 'Nu', 0.1); %! assert_equal (M.Bias, -179.52, 1e-10); %! [~, s] = predict (M, Xo); %! assert_equal (min (s(M.IsSupportVector)), 0, 1e-10); %!test %! load fisheriris %! Xo = meas(51:100,[1, 3]); %! M = fitcsvm (Xo, ones (50, 1), 'OutlierFraction', 0.1); %! assert_equal (M.Nu, 0.5); %! [~, s] = predict (M, Xo); %! assert_equal (mean (s < 0), 0.1); ## MATLAB parity: the one-class losses, with margin and edge NaN. Values ## from R2024a (linear kernel). %!test %! load fisheriris %! M = fitcsvm (meas(51:100,[1, 3]), ones (50, 1), 'KernelFunction', ... %! 'linear', 'Nu', 0.5); %! assert_equal (resubLoss (M, 'LossFun', 'classifcost'), 0); %! assert_equal (resubLoss (M, 'LossFun', 'binodeviance'), ... %! 0.0144818627743958, 1e-12); %! assert_equal (resubLoss (M, 'LossFun', 'exponential'), ... %! 0.0235456917291944, 1e-12); %! assert_equal (resubLoss (M, 'LossFun', 'hinge'), 0.02, 1e-12); %! assert_equal (resubLoss (M, 'LossFun', 'logit'), 0.0171271559174886, 1e-12); %! assert_equal (resubLoss (M, 'LossFun', 'quadratic'), 68502.381296, 1e-6); %! Q = [6, 4.3; 5, 3.5; 7, 4.7; 5.5, 5; 6.2, 4.5]; %! assert_equal (margin (M, Q, ones (5, 1)), NaN (5, 1)); %! assert_equal (edge (M, Q, ones (5, 1)), NaN); %! assert_equal (resubEdge (M), NaN); %! assert_equal (margin (compact (M), Q, ones (5, 1)), NaN (5, 1)); ## A fit stopped at the solver's iteration limit says so. %!warning ... %! ClassificationSVM ((1:20)' * 1000, repmat ([1; 2], 10, 1), ... %! 'KernelFunction', 'polynomial'); %!error ... %! fitcsvm ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', 'a') %!error ... %! fitcsvm ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', ones (2, 2)) %!error ... %! fitcsvm ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', [1, 2]) %!error ... %! fitcsvm ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', -ones (4, 1)) %!error ... %! load fisheriris %! keep = ! strcmp (species, "setosa"); %! X = meas(keep,:); y = species(keep); %! discardSupportVectors (fitcsvm (X, y, "KernelFunction", "rbf")) %!error ... %! fitcsvm (ones (10,2), [1;1;1;1;1;2;2;2;2;2], 'Prior', 'nope') %!error ... %! fitcsvm (ones (10,2), [1;1;1;1;1;2;2;2;2;2], 'Cost', [0, 1]) %!error ... %! fitcsvm (ones (10,2), [1;1;1;1;1;2;2;2;2;2], 'Prior', [1, 1, 1]) %!error ... %! fitcsvm (ones (10,2), [1;1;1;1;1;2;2;2;2;2], 'Cost', eye (3)) ## The model reports the observation weights and the expanded predictor ## names MATLAB reports, W normalized to sum to one. %!test %! load fisheriris %! Yb = strcmp (species, 'setosa'); %! Mdl = fitcsvm (meas, Yb); %! assert_equal (size (Mdl.W), [150, 1]); %! assert_equal (sum (Mdl.W), 1, 1e-12); %! assert_equal (Mdl.W, ones (150, 1) / 150, 1e-12); %! assert_equal (Mdl.CategoricalPredictors, []); %! assert_equal (Mdl.ExpandedPredictorNames, Mdl.PredictorNames); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.W, Mdl.W); %! assert_equal (M2.ExpandedPredictorNames, Mdl.ExpandedPredictorNames); ## Standardize fits on the scale it predicts on. A model fitted on raw data ## and asked about standardized data is not merely worse, it is wrong: on a ## problem whose second predictor is a thousand times the first it answered at ## chance, where the same data standardized by hand reaches 0.9. %!test %! rand ('seed', 42); %! randn ('seed', 42); %! n = 80; %! A = [randn(n,1) + 1, (randn(n,1) + 1) * 1000]; %! B = [randn(n,1) - 1, (randn(n,1) - 1) * 1000]; %! X = [A; B]; %! Y = [ones(n,1); 2 * ones(n,1)]; %! Mdl = fitcsvm (X, Y, 'Standardize', true, 'KernelFunction', 'rbf'); %! assert_equal (mean (resubPredict (Mdl) == Y) > 0.85, true); %! assert_equal (predict (Mdl, X), resubPredict (Mdl)); %! Mu = mean (X, 1); %! Sg = std (X, [], 1); %! byhand = fitcsvm ((X - Mu) ./ Sg, Y, 'Standardize', false, ... %! 'KernelFunction', 'rbf'); %! assert_equal (mean (resubPredict (Mdl) == Y), ... %! mean (resubPredict (byhand) == Y)); %!test # the fit does not depend on what the two classes are called %! ## LIBSVM reorders a two-class problem labelled -1 and +1 so that its %! ## first class is the +1 one, and the class indices are what it is given %! ## so that this cannot reach the labels read back. Before, a numeric %! ## response went through as it stood and every label came back the other %! ## one: on this fixture resubLoss was 1 rather than 0. %! rand ('seed', 42); %! randn ('seed', 42); %! X = [randn(20, 2); randn(20, 2) + 6]; %! truth = [ones(20, 1); 2 * ones(20, 1)]; %! names = {[-1; 1], [1; 2], [0; 1], [5; 7], [2; 1]}; %! for i = 1:numel (names) %! lab = names{i}; %! Mdl = ClassificationSVM (X, lab(truth)); %! assert_equal (resubLoss (Mdl), 0); %! assert_equal (predict (Mdl, X), lab(truth)); %! endfor %!test # a numeric response and a textual one give the same fit %! rand ('seed', 42); %! randn ('seed', 42); %! X = [randn(20, 2); randn(20, 2) + 6]; %! truth = [ones(20, 1); 2 * ones(20, 1)]; %! num = ClassificationSVM (X, [-1; 1](truth)); %! txt = ClassificationSVM (X, {'neg', 'pos'}'(truth)); %! [~, snum] = predict (num, X); %! [~, stxt] = predict (txt, X); %! assert_equal (snum, stxt, 1e-12); %! assert_equal (num.Beta, txt.Beta, 1e-12); %!test # A categorical 'ClassNames' is accepted %! load fisheriris %! y = categorical (species(51:150)); %! Mdl = ClassificationSVM (meas(51:150,:), y, 'ClassNames', ... %! categorical ({'versicolor'; 'virginica'})); %! assert_equal (class (Mdl.ClassNames), 'categorical'); %! assert_equal (numel (Mdl.ClassNames), 2); %!test # An unused category of a categorical response is not a class %! load fisheriris %! y = categorical (species); %! Mdl = ClassificationSVM (meas(51:150,:), y(51:150)); %! assert_equal (cellstr (Mdl.ClassNames), {'versicolor'; 'virginica'}); %! assert_equal (predict (Mdl, meas(51,:)), y(51)); ## Test input validation for constructor %!error ClassificationSVM () %!error ... %! ClassificationSVM (ones (10,2)) %!error ... %! ClassificationSVM (ones (10,2), ones (5,1)) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'Standardize', 'a') %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'PredictorNames', ['x1';'x2']) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'PredictorNames', {'x1','x2','x3'}) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'ResponseName', {'Y'}) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'ResponseName', 21) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'ClassNames', @(x)x) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'ClassNames', {1}) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'ClassNames', [1, 2]) %!error ... %! ClassificationSVM (ones (5,2), ['a';'b';'a';'a';'b'], 'ClassNames', ['a';'c']) %!error ... %! ClassificationSVM (ones (5,2), {'a';'b';'a';'a';'b'}, 'ClassNames', {'a','c'}) %!error ... %! ClassificationSVM (ones (10,2), logical (ones (10,1)), 'ClassNames', [true, false]) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'svmtype', 123) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'svmtype', 'some_type') %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'OutlierFraction', -1) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'KernelFunction', 123) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'KernelFunction', 'fcn') %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'PolynomialOrder', -1) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'PolynomialOrder', 0.5) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'PolynomialOrder', [1,2]) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'KernelScale', -1) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'KernelScale', 0) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'KernelScale', [1, 2]) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'KernelScale', 'invalid') %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'KernelOffset', -1) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'KernelOffset', [1,2]) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'BoxConstraint', -1) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'BoxConstraint', 0) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'BoxConstraint', [1, 2]) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'BoxConstraint', 'invalid') %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'nu', -0.5) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'nu', 0) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'nu', 1.5) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'CacheSize', -1) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'CacheSize', [1,2]) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'Tolerance', -0.1) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'Tolerance', [0.1,0.2]) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'shrinking', 2) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'shrinking', -1) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'shrinking', [1 0]) %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'invalid_name', 'c_svc') %!error ... %! ClassificationSVM (ones (10,2), ones (10,1), 'SVMtype', 'c_svc') %!error ... %! ClassificationSVM (ones (10,2), [1;1;1;1;2;2;2;2;3;3]) %!error ... %! ClassificationSVM ([ones(9,2);2,Inf], ones (10,1)) ## Test output for predict method %!shared x, y, x_train, x_test, y_train, y_test, objST %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! x = meas(inds, 3:4); %! y = grp2idx (species(inds)); %!test %! xc = [min(x); mean(x); max(x)]; %! obj = fitcsvm (x, y, 'KernelFunction', 'rbf', 'Tolerance', 1e-7); %! assert_equal (isempty (obj.Beta), true) %! assert_equal (sum (obj.IsSupportVector), numel (obj.Alpha)) %! [label, score] = predict (obj, xc); %! assert_equal (label, [1; 2; 2]); %! ## R2024a's scores. %! assert_equal (score(:,1), [0.9813697204; -0.1752955874; ... %! -0.9410361822], 5e-4); %! assert_equal (score(:,1), -score(:,2), eps) %!test %! obj = fitcsvm (x, y); %! assert_equal (obj.Beta, [2.182926829268275; 2.253658536585344], 1e-5) %! assert_equal (sum (obj.IsSupportVector), numel (obj.Alpha)) %! assert_equal (numel (obj.Alpha), 24) %! assert_equal (obj.Bias, -14.415, 1e-3) %! xc = [min(x); mean(x); max(x)]; %! label = predict (obj, xc); %! assert_equal (label, [1; 2; 2]); ## Values below are R2024a's, measured 2026-08-17. %!test %! ## a linear kernel has a primal representation, one coefficient per predictor %! obj = fitcsvm (x, y); %! assert_equal (size (obj.Beta), [2, 1]); %! assert_equal (obj.Beta, [2.182926829268275; 2.253658536585344], 1e-5); %! assert_equal (obj.Beta, ... %! obj.SupportVectors' * (obj.Alpha .* obj.SupportVectorLabels)); %!test %! ## a nonlinear kernel has none, but keeps the dual coefficients %! obj = fitcsvm (x, y, 'KernelFunction', 'rbf'); %! assert_equal (isempty (obj.Beta), true); %! assert_equal (numel (obj.Alpha), sum (obj.IsSupportVector)); ## KernelScale divides every predictor, as in MATLAB; expected values are ## R2024a's. %!test %! obj = fitcsvm (x, y, 'KernelScale', 2); %! assert_equal (obj.Beta, [3.19512195122; 2.55609756098], 1e-6); %!test %! obj = fitcsvm (x, y, 'KernelScale', 2); %! assert_equal (obj.Bias, -10.0870731707, 1e-6); %!test %! obj = fitcsvm (x, y, 'KernelScale', 2); %! Q = x([1, 30, 60, 90],:); %! [~, score] = predict (obj, Q); %! assert_equal (score(:,2), (Q / 2) * obj.Beta + obj.Bias, 1e-12); %!test %! obj = fitcsvm (x, y, 'KernelFunction', 'gaussian', 'KernelScale', 2); %! [~, score] = predict (obj, x([1, 30, 60, 90],:)); %! assert_equal (score(:,2), [-0.827757529685; -2.17748767098; ... %! 2.25809330158; 1.59440000202], 1e-6); %!test %! obj = fitcsvm (x, y, 'KernelFunction', 'polynomial', 'KernelScale', 2, ... %! 'PolynomialOrder', 2); %! [~, score] = predict (obj, x([1, 30, 60, 90],:)); %! assert_equal (score(:,2), [-1.23481189405; -4.72633601992; ... %! 6.48549554678; 2.83864647222], 2e-3); %!test %! obj = fitcsvm (x, y, 'KernelFunction', 'gaussian', 'KernelScale', 2); %! assert_equal (obj.SupportVectors, x(obj.IsSupportVector,:)); %!test %! obj = fitcsvm (x, ones (100, 1), 'KernelFunction', 'gaussian', ... %! 'KernelScale', 2, 'Nu', 0.3); %! assert_equal (obj.Bias, -13.4656686131, 1e-6); %!test %! obj = fitcsvm (x, ones (100, 1), 'KernelFunction', 'linear', 'Nu', 0.3); %! assert_equal (unique (obj.SupportVectorLabels), 1); %!test %! obj = fitcsvm (x, ones (100, 1), 'KernelFunction', 'linear', 'Nu', 0.3); %! assert_equal (obj.Beta, [119.2; 36.4], 1e-9); %!test %! ## Parameters reach the engine at full precision. %! obj = fitcsvm (x, y, 'BoxConstraint', 1e-7); %! assert_equal (max (obj.Alpha), 1e-7, 1e-20); %!test %! obj = fitcsvm (x, y, 'BoxConstraint', 0.1234567891); %! assert_equal (max (obj.Alpha), 0.1234567891, 1e-15); %!test %! obj = fitcsvm (x, ones (100, 1), 'Nu', 1e-7); %! assert_equal (sum (obj.Alpha), 1e-5, 1e-18); %!test %! ## MATLAB adds KernelOffset to the Gram matrix, which changes no fit. %! A = fitcsvm (x, y, 'KernelFunction', 'polynomial'); %! B = fitcsvm (x, y, 'KernelFunction', 'polynomial', 'KernelOffset', 0.5); %! [~, sA] = predict (A, x); %! [~, sB] = predict (B, x); %! assert_equal (sB, sA); %!test %! obj = fitcsvm (x, y, 'KernelScale', 2); %! [~, s1] = predict (obj, x); %! [~, s2] = predict (discardSupportVectors (obj), x); %! assert_equal (s2, s1, 1e-12); %!test %! ## the dual coefficients are magnitudes; their class is in the labels %! obj = fitcsvm (x, y); %! assert_equal (any (obj.Alpha < 0), false); %! assert_equal (max (obj.Alpha) <= 1, true); %! assert_equal (size (obj.SupportVectorLabels), [24, 1]); %! assert_equal (unique (obj.SupportVectorLabels)', [-1, 1]); %!test %! ## the support vector indicator is logical, one entry per observation %! obj = fitcsvm (x, y); %! assert_equal (class (obj.IsSupportVector), 'logical'); %! assert_equal (size (obj.IsSupportVector), [100, 1]); %! assert_equal (sum (obj.IsSupportVector), 24); ## A single-observation query used to corrupt the heap and abort the ## interpreter; each row on its own must agree with the batch answer. %!test %! obj = fitcsvm (x, y); %! xc = [min(x); mean(x); max(x)]; %! batch = predict (obj, xc); %! for i = 1:rows (xc) %! assert_equal (predict (obj, xc(i,:)), batch(i)); %! endfor %!test %! obj = fitcsvm (x, y, 'KernelFunction', 'rbf', 'Tolerance', 1e-7); %! xc = [min(x); mean(x); max(x)]; %! [bl, bs] = predict (obj, xc); %! [l1, s1] = predict (obj, xc(1,:)); %! assert_equal (l1, bl(1)); %! assert_equal (size (l1), [1, 1]); %! assert_equal (size (s1), [1, 2]); %! assert_equal (s1, bs(1,:), 2e-5); %!test %! obj = compact (fitcsvm (x, y)); %! xc = [min(x); mean(x); max(x)]; %! batch = predict (obj, xc); %! assert_equal (predict (obj, xc(1,:)), batch(1)); ## Test input validation for predict method %!error ... %! predict (ClassificationSVM (ones (40,2), ones (40,1))) %!error ... %! predict (ClassificationSVM (ones (40,2), ones (40,1)), []) %!error ... %! predict (ClassificationSVM (ones (40,2), ones (40,1)), 1) %!test %! objST = fitcsvm (x, y); %!error ... %! objST.ScoreTransform = 'a'; %! [labels, scores] = predict (objST, x); ## Test input validation for resubPredict method %! [labels, scores] = resubPredict (objST); ## Test output for margin method %!test %! rand ('seed', 1); %! CVSVMModel = fitcsvm (x, y, 'KernelFunction', 'rbf', 'HoldOut', 0.15, ... %! 'Tolerance', 1e-7); %! obj = CVSVMModel.Trained{1}; %! testInds = test (CVSVMModel.Partition); %! ## R2024a's margins, fitted on the same training rows with the prior the %! ## fold inherits, [0.5 0.5]. Every one of the fifteen is classified %! ## correctly, so every margin is positive. %! expected_margin = [2.185059262; 0.993999246; 1.999333315; 3.078654435; ... %! 2.977495193; 2.191955906; 3.269512295; 2.323010586; ... %! 3.185859883; 3.140175181; 1.701291423; 3.210493533; ... %! 1.034142171; 3.033060133; 2.799817759]; %! computed_margin = margin (obj, x(testInds,:), y(testInds,:)); %! assert_equal (computed_margin, expected_margin, 2e-3); %! assert (all (computed_margin > 0)); ## Test input validation for margin method %!error ... %! margin (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1))) %!error ... %! margin (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), zeros (2)) %!error ... %! margin (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), [], zeros (2)) %!error ... %! margin (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), 1, zeros (2)) %!error ... %! margin (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), zeros (2), []) %!error ... %! margin (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), zeros (2), 1) ## Test output for loss method %!test %! rand ('seed', 1); %! CVSVMModel = fitcsvm (x, y, 'KernelFunction', 'rbf', 'HoldOut', 0.15); %! obj = CVSVMModel.Trained{1}; %! testInds = test (CVSVMModel.Partition); %! L1 = loss (obj, x(testInds,:), y(testInds,:), 'LossFun', 'binodeviance'); %! L2 = loss (obj, x(testInds,:), y(testInds,:), 'LossFun', 'classiferror'); %! L3 = loss (obj, x(testInds,:), y(testInds,:), 'LossFun', 'exponential'); %! L4 = loss (obj, x(testInds,:), y(testInds,:), 'LossFun', 'hinge'); %! L5 = loss (obj, x(testInds,:), y(testInds,:), 'LossFun', 'logit'); %! L6 = loss (obj, x(testInds,:), y(testInds,:), 'LossFun', 'quadratic'); %! ## R2024a's losses, fitted on the same training rows with the prior the %! ## fold inherits, [0.5 0.5]. %! assert_equal (L1, 0.1057769174, 5e-4); %! assert_equal (L2, 0, 5e-4); %! assert_equal (L3, 0.3138559104, 5e-4); %! assert_equal (L4, 0.07547010872, 5e-4); %! assert_equal (L5, 0.2681965038, 5e-4); %! assert_equal (L6, 0.1954123863, 5e-4); ## Test input validation for loss method %!error ... %! loss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1))) %!error ... %! loss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), zeros (2)) %!error ... %! loss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), zeros (2), ... %! ones (2,1), 'LossFun') %!error ... %! loss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), [], zeros (2)) %!error ... %! loss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), 1, zeros (2)) %!error ... %! loss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), zeros (2), []) %!error ... %! loss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), zeros (2), 1) %!error ... %! loss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), zeros (2), ... %! ones (2,1), 'LossFun', 1) %!error ... %! loss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), zeros (2), ... %! ones (2,1), 'LossFun', 'some') %!error ... %! loss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), zeros (2), ... %! ones (2,1), 'Weights', ['a','b']) %!error ... %! loss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), zeros (2), ... %! ones (2,1), 'Weights', ones (2, 2)) %!error ... %! loss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), zeros (2), ... %! ones (2,1), 'Weights', 'a') %!error ... %! loss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), zeros (2), ... %! ones (2,1), 'Weights', [1,2,3]) %!error ... %! loss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), zeros (2), ... %! ones (2,1), 'Weights', 3) %!error ... %! loss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), zeros (2), ... %! ones (2,1), 'some', 'some') ## Test input validation for resubLoss method %!error ... %! resubLoss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), 'LossFun') %!error ... %! resubLoss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), 'LossFun', 1) %!error ... %! resubLoss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), 'LossFun', 'some') %!error ... %! resubLoss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), 'Weights', ['a','b']) %!error ... %! resubLoss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), ... %! 'Weights', ones (2, 2)) %!error ... %! resubLoss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), 'Weights', 'a') %!error ... %! resubLoss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), 'Weights', [1,2,3]) %!error ... %! resubLoss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), 'Weights', 3) %!error ... %! resubLoss (ClassificationSVM (ones (40,2), randi ([1, 2], 40, 1)), 'some', 'some') ## Test output for crossval method %!test %! SVMModel = fitcsvm (x, y); %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! CVMdl = crossval (SVMModel, 'KFold', 5); %! warning (status); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y}) %! assert_equal (CVMdl.KFold == 5, true) %! assert_equal (class (CVMdl.Trained{1}), "CompactClassificationSVM") %! assert_equal (CVMdl.CrossValidatedModel, "SVM") %!test %! obj = fitcsvm (x, y); %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! CVMdl = crossval (obj, 'HoldOut', 0.2); %! warning (status); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y}) %! assert_equal (class (CVMdl.Trained{1}), "CompactClassificationSVM") %! assert_equal (CVMdl.CrossValidatedModel, "SVM") %!test %! obj = fitcsvm (x, y); %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! CVMdl = crossval (obj, 'LeaveOut', 'on'); %! warning (status); %! assert_equal (class (CVMdl), "ClassificationPartitionedModel") %! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y}) %! assert_equal (class (CVMdl.Trained{1}), "CompactClassificationSVM") %! assert_equal (CVMdl.CrossValidatedModel, "SVM") ## Test input validation for crossval method %!error ... %! crossval (ClassificationSVM (ones (40,2),randi ([1, 2], 40, 1)), 'KFold') %!error ... %! crossval (ClassificationSVM (ones (40,2),randi ([1, 2], 40, 1)), ... %! 'KFold', 5, 'leaveout', 'on') %!error ... %! crossval (ClassificationSVM (ones (40,2),randi ([1, 2], 40, 1)), 'KFold', 'a') %!error ... %! crossval (ClassificationSVM (ones (40,2),randi ([1, 2], 40, 1)), 'KFold', 1) %!error ... %! crossval (ClassificationSVM (ones (40,2),randi ([1, 2], 40, 1)), 'KFold', -1) %!error ... %! crossval (ClassificationSVM (ones (40,2),randi ([1, 2], 40, 1)), 'KFold', 11.5) %!error ... %! crossval (ClassificationSVM (ones (40,2),randi ([1, 2], 40, 1)), 'KFold', [1,2]) %!error ... %! crossval (ClassificationSVM (ones (40,2),randi ([1, 2], 40, 1)), 'Holdout', 'a') %!error ... %! crossval (ClassificationSVM (ones (40,2),randi ([1, 2], 40, 1)), 'Holdout', 11.5) %!error ... %! crossval (ClassificationSVM (ones (40,2),randi ([1, 2], 40, 1)), 'Holdout', -1) %!error ... %! crossval (ClassificationSVM (ones (40,2),randi ([1, 2], 40, 1)), 'Holdout', 0) %!error ... %! crossval (ClassificationSVM (ones (40,2),randi ([1, 2], 40, 1)), 'Holdout', 1) %!error ... %! crossval (ClassificationSVM (ones (40,2),randi ([1, 2], 40, 1)), 'Leaveout', 1) %!error ... %! crossval (ClassificationSVM (ones (40,2),randi ([1, 2], 40, 1)), 'CVPartition', 1) %!error ... %! crossval (ClassificationSVM (ones (40,2),randi ([1, 2], 40, 1)), 'CVPartition', 'a') %!error ... %! crossval (ClassificationSVM (ones (40,2),randi ([1, 2], 40, 1)), 'some', 'some') ## Test output for fitPosterior method ## Values checked against MATLAB R2024a, whose SMO solver gives scores up to ## 1.3e-3 away from LIBSVM's out of fold %!test %! load fisheriris %! Mdl = fitcsvm (meas(51:150, 3:4), species(51:150), ... %! 'KernelFunction', 'linear'); %! [M, ST] = fitPosterior (Mdl, 'Leaveout', 'on'); %! assert_equal (ST.Type, 'sigmoid'); %! assert_equal (ST.Slope, -3.4011027773863747, 1e-3); %! assert_equal (ST.Intercept, -0.092575940016464561, 1e-4); %! tok = regexp (M.ScoreTransform, '^@\(S\)sigmoid\(S,([^,]+),([^)]+)\)$', ... %! 'tokens', 'once'); %! assert_equal (str2double (tok)', [ST.Slope, ST.Intercept]); %!test %! load fisheriris %! X = meas(51:150, 3:4); %! Mdl = fitcsvm (X, species(51:150), 'KernelFunction', 'linear'); %! [M, ST] = fitPosterior (Mdl, 'Leaveout', 'on'); %! [~, s] = predict (Mdl, X([1 2 60 61], :)); %! [~, p] = predict (M, X([1 2 60 61], :)); %! P = 1 ./ (1 + exp (ST.Slope * s(:,2) + ST.Intercept)); %! assert_equal (p, [1 - P, P], 1e-15); %! assert_equal (p, [0.96472018335949683, 0.035279816640503191; ... %! 0.98248291693946965, 0.017517083060530327; ... %! 1.8241290766773943e-07, 0.99999981758709233; ... %! 0.013923286598323315, 0.98607671340167669], 1e-4); %!test %! load fisheriris %! X = meas(51:150, 3:4); %! Mdl = fitcsvm (X, species(51:150), 'KernelFunction', 'linear'); %! M = fitPosterior (Mdl, 'Leaveout', 'on'); %! [~, p] = resubPredict (M); %! [~, q] = predict (M, X); %! assert_equal (p, q); %!test %! load fisheriris %! Mdl = fitcsvm (meas(51:150, 3:4), species(51:150), ... %! 'KernelFunction', 'linear'); %! [~, ST1] = fitPosterior (Mdl, 'Leaveout', 'on'); %! [~, ST2] = fitPosterior (Mdl, 'CVPartition', cvpartition (100, 'LeaveOut')); %! assert_equal (ST2, ST1); %!warning ... %! load fisheriris %! Mdl = fitcsvm (meas(51:150, 3:4), species(51:150), ... %! 'KernelFunction', 'linear'); %! [M, ST1] = fitPosterior (Mdl, 'Leaveout', 'on'); %! [~, ST2] = fitPosterior (M, 'Leaveout', 'on'); %! assert_equal (ST2, ST1); %!warning ... %! load fisheriris %! Mdl = fitcsvm (meas(1:100, 3:4), species(1:100), 'KernelFunction', 'linear'); %! [M, ST] = fitPosterior (Mdl, 'Leaveout', 'on'); %! assert_equal (ST.Type, 'step'); %! assert_equal (ST.LowerBound, -0.8446461538461536, 1e-6); %! assert_equal (ST.UpperBound, 0.68965517241379271, 1e-6); %! assert_equal (ST.PositiveClassProbability, 0.5); %! tok = regexp (M.ScoreTransform, '^@\(S\)step\(S,([^,]+),([^,]+),0.5\)$', ... %! 'tokens', 'once'); %! assert_equal (str2double (tok)', [ST.LowerBound, ST.UpperBound]); %!warning ... %! load fisheriris %! Mdl = fitcsvm (meas(1:100, 3:4), species(1:100), 'KernelFunction', 'linear'); %! M = fitPosterior (Mdl, 'Leaveout', 'on'); %! [~, p] = predict (M, [2.45, 0.8; 1.4, 0.2; 4.5, 1.5]); %! assert_equal (p, [0.5, 0.5; 1, 0; 0, 1]); %!warning ... %! load fisheriris %! Mdl = fitcsvm (meas(1:70, 3:4), species(1:70), 'KernelFunction', 'linear'); %! [~, ST] = fitPosterior (Mdl, 'Leaveout', 'on'); %! assert_equal (ST.PositiveClassProbability, 0.2857142857142857, 1e-15); %!warning ... %! load fisheriris %! Mdl = fitcsvm (meas(1:100, 3:4), species(1:100), 'KernelFunction', ... %! 'linear', 'Prior', [0.2, 0.8]); %! [~, ST] = fitPosterior (Mdl, 'Leaveout', 'on'); %! assert_equal (ST.PositiveClassProbability, 0.8, 1e-15); %!warning ... %! load fisheriris %! Mdl = fitcsvm (meas(1:70, 3:4), species(1:70), 'KernelFunction', ... %! 'linear', 'Weights', 1 + mod ((1:70)', 3)); %! [~, ST] = fitPosterior (Mdl, 'Leaveout', 'on'); %! assert_equal (ST.PositiveClassProbability, 0.27857142857142858, 1e-15); %!test %! load fisheriris %! X = meas(51:150, 3:4); %! Mdl = fitcsvm (X, species(51:150), 'KernelFunction', 'linear', ... %! 'Prior', [1, 0]); %! [M, ST] = fitPosterior (Mdl); %! assert_equal (ST.Type, 'constant'); %! assert_equal (ST.PredictedClass, {'versicolor'}); %! assert_equal (M.ScoreTransform, '@(S)constant(S,1)'); %! [~, p] = predict (M, X([1, 60], :)); %! assert_equal (p, [1, 0; 1, 0]); %!test %! load fisheriris %! X = meas(51:150, 3:4); %! Mdl = fitcsvm (X, species(51:150), 'KernelFunction', 'linear'); %! M = fitPosterior (Mdl, 'Leaveout', 'on'); %! fname = [tempname(), '.mat']; %! savemodel (M, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.ScoreTransform, M.ScoreTransform); %! [~, p] = predict (M, X(1:5, :)); %! [~, p2] = predict (M2, X(1:5, :)); %! assert_equal (p2, p); %!test %! load fisheriris %! X = meas(51:150, 3:4); %! Mdl = fitcsvm (X, species(51:150), 'KernelFunction', 'linear'); %! [~, s] = predict (Mdl, X(1:3, :)); %! Mdl.ScoreTransform = '@(S)sigmoid(S,-2,0.5)'; %! [~, p] = predict (Mdl, X(1:3, :)); %! P = 1 ./ (1 + exp (-2 * s(:,2) + 0.5)); %! assert_equal (p, [1 - P, P], 1e-15); ## Test input validation for fitPosterior method %!error ... %! load fisheriris %! fitPosterior (fitcsvm (meas(51:150, 3:4), ones (100, 1))) %!error ... %! load fisheriris %! Mdl = fitcsvm (meas(51:150, 3:4), species(51:150)); %! fitPosterior (Mdl, 'Foo', 5) %!error ... %! load fisheriris %! Mdl = fitcsvm (meas(51:150, 3:4), species(51:150)); %! fitPosterior (Mdl, 'CrossVal', 'on') %!error ... %! load fisheriris %! Mdl = fitcsvm (meas(51:150, 3:4), species(51:150)); %! fitPosterior (Mdl, 5, 5) %!error ... %! load fisheriris %! Mdl = fitcsvm (meas(51:150, 3:4), species(51:150)); %! fitPosterior (Mdl, 'KFold') %!error ... %! load fisheriris %! Mdl = fitcsvm (meas(51:150, 3:4), species(51:150)); %! fitPosterior (Mdl, 'KFold', 5, 'Holdout', 0.2) %!error ... %! load fisheriris %! Mdl = fitcsvm (meas(51:150, 3:4), species(51:150)); %! fitPosterior (Mdl, 'KFold', 1) %!error ... %! load fisheriris %! Mdl = fitcsvm (meas(51:150, 3:4), species(51:150)); %! fitPosterior (Mdl, 'KFold', 2.5) %!error ... %! load fisheriris %! Mdl = fitcsvm (meas(51:150, 3:4), species(51:150)); %! fitPosterior (Mdl, 'Holdout', 0) %!error ... %! load fisheriris %! Mdl = fitcsvm (meas(51:150, 3:4), species(51:150)); %! fitPosterior (Mdl, 'Leaveout', 'yes') %!error ... %! load fisheriris %! Mdl = fitcsvm (meas(51:150, 3:4), species(51:150)); %! fitPosterior (Mdl, 'CVPartition', 5) %!error ... %! load fisheriris %! Mdl = fitcsvm (meas(51:150, 3:4), species(51:150)); %! fitPosterior (Mdl, 'CVPartition', cvpartition (50, 'KFold', 5)) %!error ... %! savemodel (ClassificationSVM ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2])) %!error ... %! savemodel (ClassificationSVM ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), 1) %!error ... %! savemodel (ClassificationSVM ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), ['ab'; 'cd']) ## RowsUsed is empty when every observation was used. %!test %! load fisheriris %! X = meas(1:100,:); %! Y = grp2idx (species(1:100)); %! Mdl = fitcsvm (X, Y); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (class (Mdl.RowsUsed), 'double'); %! assert_equal (Mdl.NumObservations, 100); %! assert_equal (rows (Mdl.X), 100); %! assert_equal (rows (Mdl.W), 100); ## A missing response drops its observation and RowsUsed marks it. %!test %! load fisheriris %! X = meas(1:100,:); %! Y = grp2idx (species(1:100)); %! Y(5) = NaN; %! Mdl = fitcsvm (X, Y); %! assert_equal (class (Mdl.RowsUsed), 'logical'); %! assert_equal (size (Mdl.RowsUsed), [100, 1]); %! assert_equal (sum (Mdl.RowsUsed), 99); %! assert_equal (Mdl.RowsUsed(5), false); %! assert_equal (Mdl.NumObservations, 99); %! assert_equal (rows (Mdl.X), 99); %! assert_equal (rows (Mdl.W), 99); ## A missing predictor keeps its observation, so RowsUsed stays empty. %!test %! load fisheriris %! X = meas(1:100,:); %! X(3,2) = NaN; %! Y = grp2idx (species(1:100)); %! Mdl = fitcsvm (X, Y); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (Mdl.NumObservations, 100); %! assert_equal (rows (Mdl.X), 100); %! assert_equal (sum (isnan (Mdl.X(:))), 1); ## Mu and Sigma are empty unless the predictors are standardized. %!test %! load fisheriris %! Mdl = fitcsvm (meas(1:100,:), species(1:100)); %! assert_equal (Mdl.Mu, []); %! assert_equal (Mdl.Sigma, []); ## Standardizing weights the complete observations by each class's original ## share of the observation weight. Values from MATLAB R2024a. %!test %! load fisheriris %! X = meas(1:100,:); %! X(3,2) = NaN; X(17,4) = NaN; %! Mdl = fitcsvm (X, species(1:100), 'Standardize', true); %! assert_equal (Mdl.Mu, [5.4700833333333314, 3.0964583333333331, ... %! 2.864374999999999, 0.78487499999999988], 1e-13); %! assert_equal (Mdl.Sigma, [0.64239212471819018, 0.47709034264660688, ... %! 1.4464268842305728, 0.56625850081877471], 1e-13); ## The prior reweights the observations of each class. Values from R2024a. %!test %! load fisheriris %! i2 = [1:50, 51:80]; %! Mdl = fitcsvm (meas(i2,:), species(i2)); %! assert_equal (Mdl.Prior, [0.625, 0.375], 1e-14); %! assert_equal (Mdl.W(1), 0.0125, 1e-14); %! Mdl = fitcsvm (meas(i2,:), species(i2), 'Prior', 'uniform'); %! assert_equal (Mdl.W(1), 0.01, 1e-14); %! assert_equal (Mdl.W(51), 1/60, 1e-14); ## Neither Prior nor Cost may be assigned after the fit. The refusal comes ## from the property attributes, so the message is core Octave's and is not ## pinned here. %!error ... %! load fisheriris; ... %! Mdl = fitcsvm (meas(1:80,:), species(1:80)); ... %! Mdl.Prior = [0.5, 0.5]; %!error ... %! load fisheriris; ... %! Mdl = fitcsvm (meas(1:80,:), species(1:80)); ... %! Mdl.Cost = [0, 2; 1, 0]; ## A fitted model survives savemodel and loadmodel: the properties come ## back as they were and it predicts the same. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = fitcsvm (meas(inds,:), species(inds)); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2), 'ClassificationSVM'); %! assert_equal (M2.NumObservations, Mdl.NumObservations); %! assert_equal (M2.PredictorNames, Mdl.PredictorNames); %! assert_equal (class (M2.ScoreTransform), class (Mdl.ScoreTransform)); %! assert_equal (predict (M2, meas(1:5,:)), predict (Mdl, meas(1:5,:))); ## edge, resubMargin and resubEdge. The absolute value is not pinned to the ## oracle here: this class fits through LIBSVM where MATLAB uses SMO, and the ## two disagree on the support vector set, so the edge reads 3.6942705869 ## against MATLAB's 3.6935623843 on the iris pair. What is pinned is what the ## methods mean. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! X = meas(inds,:); %! Y = species(inds); %! Mdl = fitcsvm (X, Y); %! assert_equal (edge (Mdl, X, Y), mean (margin (Mdl, X, Y)), 1e-12); %! assert_equal (resubEdge (Mdl), edge (Mdl, X, Y), 1e-12); %! assert_equal (resubMargin (Mdl), margin (Mdl, X, Y), 1e-12); ## margin takes the class labels, as it documents, and the +1/-1 coding the ## solver works in gives the same answer. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! X = meas(inds,:); %! Y = species(inds); %! Mdl = fitcsvm (X, Y); %! Ypm = ones (100, 1); %! Ypm(strcmp (Y, Mdl.ClassNames{2})) = -1; %! assert_equal (margin (Mdl, X, Y), margin (Mdl, X, Ypm), 1e-12); ## The compact model answers the same edge as the full one. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! X = meas(inds,:); %! Y = species(inds); %! Mdl = fitcsvm (X, Y); %! assert_equal (edge (compact (Mdl), X, Y), edge (Mdl, X, Y), 1e-12); %!error ... %! load fisheriris; ... %! inds = ! strcmp (species, 'virginica'); ... %! edge (fitcsvm (meas(inds,:), species(inds)), meas(inds,:)) ## BinEdges is an empty cell, which is what MATLAB reports for this ## learner as well: it fits the predictors as they are. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = fitcsvm (meas(inds,:), species(inds)); %! assert_equal (class (Mdl.BinEdges), 'cell'); %! assert_equal (Mdl.BinEdges, {}); ## KernelParameters, BoxConstraints, OutlierFraction and Nu, all measured on ## MATLAB R2024a. A radial basis kernel reports MATLAB's name for it. %!test %! load fisheriris %! b = ismember (species, {'setosa', 'versicolor'}); %! Mdl = fitcsvm (meas(b,:), species(b)); %! assert_equal (Mdl.KernelParameters, struct ('Function', 'linear', ... %! 'Scale', 1)); %! assert_equal (Mdl.BoxConstraints, ones (100, 1), 1e-14); %! assert_equal (Mdl.OutlierFraction, 0); %! assert_equal (Mdl.Nu, []); %!test %! load fisheriris %! b = ismember (species, {'setosa', 'versicolor'}); %! Mdl = fitcsvm (meas(b,:), species(b), 'KernelFunction', 'rbf', ... %! 'KernelScale', 2.5, 'BoxConstraint', 3); %! assert_equal (Mdl.KernelParameters.Function, 'gaussian'); %! assert_equal (Mdl.KernelParameters.Scale, 2.5); %! n = rows (Mdl.BoxConstraints); %! assert_equal (Mdl.BoxConstraints, 3 * ones (n, 1), 1e-14); %!test %! load fisheriris %! b = ismember (species, {'setosa', 'versicolor'}); %! Mdl = fitcsvm (meas(b,:), species(b), 'KernelFunction', 'polynomial', ... %! 'PolynomialOrder', 3); %! assert_equal (fieldnames (Mdl.KernelParameters), ... %! {'Function'; 'Scale'; 'Order'}); %! assert_equal (Mdl.KernelParameters.Order, 3); ## A cost matrix reweights the classes, and the box constraints report what ## the fit actually solved: 1.6 and 0.4 against MATLAB's 1.6 and 0.4. %!test %! load fisheriris %! b = ismember (species, {'setosa', 'versicolor'}); %! Mdl = fitcsvm (meas(b,:), species(b), 'Cost', [0, 4; 1, 0]); %! assert_equal (Mdl.BoxConstraints(1), 1.6, 1e-12); %! assert_equal (Mdl.BoxConstraints(51), 0.4, 1e-12); %! assert_equal (mean (Mdl.BoxConstraints), 1, 1e-12); %!error ... %! load fisheriris %! b = ismember (species, {'setosa', 'versicolor'}); %! fitcsvm (meas(b,:), species(b), 'OutlierFraction', 0.05); %!test %! load fisheriris %! b = ismember (species, {'setosa', 'versicolor'}); %! Mdl = fitcsvm (meas(b,:), species(b)); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.KernelParameters, Mdl.KernelParameters); %! assert_equal (M2.BoxConstraints, Mdl.BoxConstraints); ## HyperparameterOptimizationResults is declared for MATLAB compatibility and ## stays empty, this class running no search over its hyperparameters. %!test %! load fisheriris %! b = ! strcmp (species, 'virginica'); %! Mdl = fitcsvm (meas(b,:), species(b)); %! assert_equal (isempty (Mdl.HyperparameterOptimizationResults), true); ## ModelParameters records what LIBSVM was given. The field list and its ## order are ours, MATLAB's SMO and ISDA parameters having no counterpart ## here. %!test %! load fisheriris %! Mdl = fitcsvm (meas, strcmp (species, 'setosa')); %! assert_equal (fieldnames (Mdl.ModelParameters)', {'SVMtype', ... %! 'BoxConstraint', 'CacheSize', 'KernelScale', 'KernelOffset', ... %! 'KernelFunction', 'KernelPolynomialOrder', 'Nu', 'Tolerance', ... %! 'Shrinking', 'OutlierFraction', 'StandardizeData', 'Version', ... %! 'Method', 'Type'}); %!test %! load fisheriris %! MP = fitcsvm (meas, strcmp (species, 'setosa')).ModelParameters; %! assert_equal (MP.OutlierFraction, 0); %! assert_equal (MP.StandardizeData, false); %! assert_equal (MP.Version, 1); %! assert_equal (MP.Method, 'SVM'); %! assert_equal (MP.Type, 'classification'); ## The polynomial order belongs to the polynomial kernel alone, as it does in ## MATLAB, and carries its default there rather than staying empty. %!test %! load fisheriris %! b = strcmp (species, 'setosa'); %! assert_equal (isempty (fitcsvm (meas, b).ModelParameters. ... %! KernelPolynomialOrder), true); %! assert_equal (fitcsvm (meas, b, 'KernelFunction', ... %! 'polynomial').ModelParameters.KernelPolynomialOrder, 3); %!test %! load fisheriris %! MP = fitcsvm (meas(1:50,:), ones (50, 1), 'Standardize', true, ... %! 'OutlierFraction', 0.05).ModelParameters; %! assert_equal (MP.StandardizeData, true); %! assert_equal (MP.OutlierFraction, 0.05); ## The third output is the expected cost of each assignment. An SVM has no ## posterior, so it is the row of Cost belonging to the predicted class; ## MATLAB returns the column, which only an asymmetric matrix reveals. %!test %! load fisheriris %! b = strcmp (species, 'setosa'); %! Mdl = fitcsvm (meas, b, 'Cost', [0, 2; 5, 0]); %! [label, ~, cost] = predict (Mdl, meas([1, 51],:)); %! assert_equal (label, [true; false]); %! assert_equal (cost, [5, 0; 0, 2]); %!test %! load fisheriris %! b = strcmp (species, 'setosa'); %! Mdl = fitcsvm (meas, b); %! [~, ~, cost] = predict (Mdl, meas([1, 51],:)); %! assert_equal (cost, [1, 0; 0, 1]); %!test %! load fisheriris %! b = strcmp (species, 'setosa'); %! Mdl = fitcsvm (meas, b, 'Cost', [0, 2; 5, 0]); %! [~, ~, cost] = resubPredict (Mdl); %! assert_equal (size (cost), [150, 2]); %! assert_equal (cost([1, 51],:), [5, 0; 0, 2]); ## Every documented score transform reaches the scores that are reported, and ## none of them moves the label: a transform reshapes what is reported, not ## what is decided. %!test %! load fisheriris %! Mdl = fitcsvm (meas, strcmp (species, 'setosa')); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! T = {'identity', @(x) x; 'doublelogit', @(x) 1 ./ (1 + exp (-2 * x)); ... %! 'invlogit', @(x) log (x ./ (1 - x)); ... %! 'logit', @(x) 1 ./ (1 + exp (-x)); ... %! 'sign', @(x) sign (x); 'symmetric', @(x) 2 * x - 1; ... %! 'symmetriclogit', @(x) 2 ./ (1 + exp (-x)) - 1}; %! for i = 1:rows (T) %! Mdl.ScoreTransform = T{i,1}; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, T{i,2}(raw), 1e-12); %! assert_equal (l, label); %! endfor %! ## ismax marks the largest score of each observation, ties to the first. %! [~, k] = max (raw, [], 2); %! e = zeros (size (raw)); %! e(sub2ind (size (raw), (1:rows (raw))', k)) = 1; %! Mdl.ScoreTransform = 'ismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, e); %! Mdl.ScoreTransform = 'symmetricismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, 2 * e - 1); ## A function handle is taken as given and applied to the scores. %!test %! load fisheriris %! Mdl = fitcsvm (meas, strcmp (species, 'setosa')); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! Mdl.ScoreTransform = @(x) x .^ 2; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, raw .^ 2, 1e-12); %! assert_equal (l, label); %!shared Xc, Dc, yc, Xq, Dq %! k = (0:119)'; %! c1 = mod (k, 3) + 1; %! x2 = sin (k); %! c3 = 10 * (mod (floor (k / 2), 2) + 1); %! Xc = [c1, x2, c3]; %! Dc = [c1 == 1, c1 == 2, c1 == 3, x2, c3 == 10, c3 == 20]; %! yr = 5 * (c1 == 2) + 0.5 * x2 - 3 * (c3 == 20) + 0.1 * cos (k); %! yc = yr > 1; %! Xq = [1, 0, 10; 2, 0.5, 20]; %! Dq = [1, 0, 0, 0, 1, 0; 0, 1, 0, 0.5, 0, 1]; %!test # MATLAB parity: a categorical predictor is dummy coded in its place %! Mdl = ClassificationSVM (Xc, yc, 'CategoricalPredictors', [1, 3]); %! H = ClassificationSVM (Dc, yc); %! assert_equal (Mdl.CategoricalPredictors, [1, 3]); %! assert_equal (Mdl.ExpandedPredictorNames, {'x1 == 1', 'x1 == 2', ... %! 'x1 == 3', 'x2', 'x3 == 10', 'x3 == 20'}); %! assert_equal (size (Mdl.X), [120, 3]); %! assert_equal (columns (Mdl.SupportVectors), 6); %! assert_equal (Mdl.Beta, H.Beta, 1e-12); %! [~, s] = predict (Mdl, Xq); %! [~, sh] = predict (H, Dq); %! assert_equal (s, sh, 1e-12); %!test # MATLAB parity: a level the fit did not see has no score %! Mdl = ClassificationSVM (Xc, yc, 'CategoricalPredictors', [1, 3]); %! [~, s] = predict (Mdl, [4, 0, 10; 2.5, 0, 20]); %! assert_equal (all (isnan (s(:))), true); %!test # MATLAB parity: the coded columns are not standardized %! Mdl = ClassificationSVM (Xc, yc, 'CategoricalPredictors', [1, 3], ... %! 'Standardize', true); %! assert_equal (Mdl.Mu([1:3, 5:6]), zeros (1, 5)); %! assert_equal (Mdl.Sigma([1:3, 5:6]), ones (1, 5)); %!test # the coding travels with saved models and cross-validation folds %! Mdl = ClassificationSVM (Xc, yc, 'CategoricalPredictors', [1, 3]); %! fname = [tempname(), '.mdl']; %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (predict (M2, Xq), predict (Mdl, Xq)); %! CV = crossval (Mdl, 'KFold', 3); %! assert_equal (CV.Trained{1}.CategoricalPredictors, [1, 3]); %!test # margin and loss score the data on the scale the model was fitted on %! load fisheriris %! X = meas(51:150,:) * 10; %! Y = species(51:150); %! Mdl = ClassificationSVM (X, Y, 'Standardize', true); %! [~, s] = predict (Mdl, X); %! g = 1 + strcmp (Y, 'virginica'); %! want = s(sub2ind (size (s), (1:100)', g)) ... %! - s(sub2ind (size (s), (1:100)', 3 - g)); %! assert_equal (margin (Mdl, X, Y), want, 1e-10); %! assert_equal (loss (Mdl, X, Y), resubLoss (Mdl)); %! assert_equal (loss (Mdl, X, Y) < 0.1, true); %!error ... %! ClassificationSVM (Xc, yc, 'CategoricalPredictors', 4) ## A table at prediction %!test # the class may be built from a table as the fitter builds it %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! T = table (meas(inds,1), meas(inds,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = categorical (species(inds)); %! Mdl = ClassificationSVM (T, 'Species'); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW'}); %! assert_equal (Mdl.ResponseName, 'Species'); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(51:150,1:2); %! y = species(51:150); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcsvm (T, 'Species'); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'Species'), a); %! assert_equal (loss (Mdl, T), a); ## A categorical or string response %!test # loss reads one as it reads a cellstr response %! load fisheriris %! X = meas(51:150,1:2); %! y = species(51:150); %! a = loss (fitcsvm (X, y), X, y); %! yc = categorical (y); %! assert_equal (loss (fitcsvm (X, yc), X, yc), a); %! ys = string (y); %! assert_equal (loss (fitcsvm (X, ys), X, ys), a); %!test # margin reads one as it reads a cellstr response %! load fisheriris %! X = meas(51:150,1:2); %! y = species(51:150); %! a = margin (fitcsvm (X, y), X, y); %! yc = categorical (y); %! assert_equal (margin (fitcsvm (X, yc), X, yc), a); %! ys = string (y); %! assert_equal (margin (fitcsvm (X, ys), X, ys), a); ## A table at edge %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(51:150,1:2); %! y = categorical (species(51:150)); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcsvm (T, 'Species'); %! a = edge (Mdl, X, y); %! assert_equal (edge (Mdl, T(:,1:2), y), a); %! assert_equal (edge (Mdl, T, 'Species'), a); %! assert_equal (edge (Mdl, T), a); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(51:150,1:2); %! y = categorical (species(51:150)); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = fitcsvm (T, 'Species'); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## 'Nu' does not choose the model: a two-class C-SVM records it and fits as ## if it were not given, as R2024a does %!test %! load fisheriris %! X = meas(51:150,1:2); %! Y = species(51:150); %! A = fitcsvm (X, Y); %! B = fitcsvm (X, Y, 'Nu', 0.3); %! assert_equal (B.ModelParameters.SVMtype, 'c_svc'); %! assert_equal (B.ModelParameters.Nu, 0.3); %! assert_equal ([B.Bias; B.Alpha], [A.Bias; A.Alpha]); ## A one-class fit keeps its 'Nu' whatever order 'OutlierFraction' comes in %!test %! load fisheriris %! X = meas(1:50,1:2); %! Y = ones (50, 1); %! A = fitcsvm (X, Y, 'Nu', 0.3, 'OutlierFraction', 0.1); %! B = fitcsvm (X, Y, 'OutlierFraction', 0.1, 'Nu', 0.3); %! assert_equal (A.ModelParameters.Nu, 0.3); %! assert_equal (sum (A.Alpha), 15, 1e-10); %! assert_equal ([A.Bias; A.Alpha], [B.Bias; B.Alpha]); ## Observation weights of class single or double %!error ... %! fitcsvm ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', ... %! int8 ([1; 1; 1; 1])) %!error ... %! fitcsvm ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', true (4, 1)) %!test %! ## Single weights are stored single, summing to one %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! w = 1 + (1:100)' / 7; %! Mdl = fitcsvm (X, Y, 'Weights', single (w)); %! assert_equal (class (Mdl.W), 'single'); %! assert_equal (sum (double (Mdl.W)), 1, 1e-6); %!test %! ## Single weights compute as double %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! w = 1 + (1:100)' / 7; %! A = fitcsvm (X, Y, 'Weights', single (w)); %! B = fitcsvm (X, Y, 'Weights', double (single (w))); %! assert_equal (nthargout (2, @predict, A, X), nthargout (2, @predict, B, X)); %!test %! ## A constant predictor is left unscaled by standardization %! X = [linspace(0, 1, 20)', ones(20, 1)]; %! Mdl = ClassificationSVM (X, [ones(10, 1); 2 * ones(10, 1)], 'Standardize', true); %! assert_equal (Mdl.Sigma(2), 1); ## Test a class with prior 0, whose observations are removed before fitting, ## as MATLAB R2024a and R2026b remove them %!warning ... %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! Mdl = ClassificationSVM (X, Y, 'Prior', [1, 0]); %! assert_equal (Mdl.NumObservations, 50); %! assert_equal (Mdl.RowsUsed, [true(50, 1); false(50, 1)]); %! assert_equal (Mdl.Prior, [1, 0]); %! assert_equal (predict (Mdl, meas([51, 110], 3:4)), ... %! {'versicolor'; 'versicolor'}); %!test %! ## A named class with prior 0 may be absent, having nothing to remove %! load fisheriris %! X = meas(51:100, 3:4); %! Y = species(51:100); %! C = {'versicolor', 'virginica'}; %! Mdl = ClassificationSVM (X, Y, 'ClassNames', C, 'Prior', [1, 0]); %! assert_equal (Mdl.ClassNames, {'versicolor'; 'virginica'}); %! assert_equal (predict (Mdl, meas(51, 3:4)), {'versicolor'}); %!warning ... %! load fisheriris %! Mdl = ClassificationSVM (meas(51:150, 3:4), species(51:150), ... %! 'Cost', [0, 1; 0, 0]); %! assert_equal (Mdl.NumObservations, 50); %! assert_equal (Mdl.Prior, [1, 0]); %!warning ... %! ## The class left is the second, and the scores point to it %! load fisheriris %! Mdl = ClassificationSVM (meas(51:150, 3:4), species(51:150), ... %! 'Prior', [0, 1]); %! [label, score] = predict (Mdl, meas([51, 110], 3:4)); %! assert_equal (label, {'virginica'; 'virginica'}); %! assert_equal (score, [-1, 1; -1, 1]); %!warning ... %! load fisheriris %! Mdl = ClassificationSVM (meas(51:150, 3:4), species(51:150), ... %! 'Prior', [1, 0]); %! CVMdl = crossval (Mdl, 'KFold', 5); %! assert_equal (CVMdl.NumObservations, 50); %! assert_equal (numel (kfoldPredict (CVMdl)), 50); %!warning ... %! ## A missing predictor in a kept row leaves it used, as in R2024a %! load fisheriris %! X = meas(51:150, 3:4); %! X([3, 70], 1) = NaN; %! Mdl = ClassificationSVM (X, species(51:150), 'Prior', [1, 0]); %! assert_equal ([Mdl.NumObservations, rows(Mdl.X), nnz(Mdl.RowsUsed)], ... %! [50, 50, 50]); %!warning ... %! ## The weights left are renormalized over the class kept, as in R2024a %! load fisheriris %! W = 1 + mod ((1:100)', 3); %! Mdl = ClassificationSVM (meas(51:150, 3:4), species(51:150), ... %! 'Prior', [1, 0], 'Weights', W); %! assert_equal (Mdl.W(1:3)', [2, 3, 1] / 101, 1e-15); %!test %! load fisheriris %! g = [ones(50, 1); 2 * ones(50, 1)]; %! Mdl = ClassificationSVM (meas(51:100, 3:4), g(1:50), 'ClassNames', ... %! [1, 2], 'Prior', [1, 0]); %! assert_equal (Mdl.ClassNames, [1; 2]); %! assert_equal (predict (Mdl, meas(51, 3:4)), 1); %!warning ... %! load fisheriris %! X = meas(51:150, 3:4); %! Mdl = ClassificationSVM (X, species(51:150), 'Prior', [0, 1]); %! fname = [tempname(), '.mat']; %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! [label, score] = predict (M2, X([1, 60], :)); %! assert_equal (label, {'virginica'; 'virginica'}); %! assert_equal (score, [-1, 1; -1, 1]); ## Test a 'Cost' given as a structure, permuted by name to the model's order, ## as MATLAB R2024a permutes it %!test %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! S = struct ('ClassNames', {{'virginica', 'versicolor'}}, ... %! 'ClassificationCosts', [0, 2; 1, 0]); %! Mdl = ClassificationSVM (X, Y, 'Cost', S); %! assert_equal (Mdl.Cost, [0, 1; 2, 0]); %! assert_equal (Mdl.Prior, [0.5, 0.5]); %!test %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! S = struct ('ClassNames', {{'virginica', 'versicolor'}}, ... %! 'ClassificationCosts', [0, 2; 1, 0]); %! M1 = ClassificationSVM (X, Y, 'Cost', S); %! M2 = ClassificationSVM (X, Y, 'Cost', [0, 1; 2, 0]); %! assert_equal (predict (M1, X), predict (M2, X)); %!error ... %! load fisheriris %! ClassificationSVM (meas(51:150, 3:4), species(51:150), ... %! 'Cost', struct ('a', 1)); ## MATLAB parity: a row missing every predictor is dropped, and one missing ## only some is used, counting in the prior and in W while the fit sees the ## complete rows, as measured on R2024a %!shared svX, svY, svW %! x1 = mod ((1:20)', 7) + (1:20)' / 10; %! x2 = mod ((1:20)', 5) * 2 - (1:20)' / 20; %! svX = [x1, x2]; %! svX(2,1) = NaN; %! svX(20,:) = NaN; %! svY = repmat ({'p'; 'q'}, 10, 1); %! svW = 1 + mod ((1:20)', 3); %!test # only the row missing every predictor is dropped %! Mdl = fitcsvm (svX, svY); %! assert_equal (Mdl.NumObservations, 19); %! assert_equal (find (! Mdl.RowsUsed)', 20); %! assert_equal ([rows(Mdl.X), numel(Mdl.W)], [19, 19]); %!test # the prior counts the row missing one predictor %! Mdl = fitcsvm (svX, svY); %! assert_equal (Mdl.Prior, [10, 9] / 19, 1e-15); %! assert_equal (Mdl.W, ones (19, 1) / 19, 1e-15); %!test # and so do weights %! Mdl = fitcsvm (svX, svY, 'Weights', svW); %! assert_equal (Mdl.Prior, [20, 18] / 38, 1e-15); %! assert_equal (Mdl.W(1:4)', [2, 3, 1, 2] / 38, 1e-15); %!test # the fit is the complete rows' under that prior %! Mdl = fitcsvm (svX, svY); %! assert_equal (Mdl.Bias, -1.66405405405405, 1e-5); %! assert_equal (Mdl.Beta', [0.314551920341389, 0.163100995732572], 1e-5); %! [label, score] = predict (Mdl, [3, 1; 5, 4; 7, 2]); %! assert_equal (label, {'p'; 'q'; 'q'}); %! assert_equal (score(:,1), [0.557297297297318; -0.561109530583184; ... %! -0.864011379800817], 1e-5); %!test # standardizing weighs every row used %! Mdl = fitcsvm (svX, svY, 'Standardize', true); %! assert_equal (Mdl.Mu, [4.10657894736842, 3.71315789473684], 1e-14); %! assert_equal (Mdl.Bias, -0.75860341301819, 1e-5); statistics-release-2.1.2/inst/Machine_Learning/ClassificationTree.m000066400000000000000000004625461527311104300254330ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ClassificationTree < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} ClassificationTree ## ## Binary decision tree for classification ## ## The @code{ClassificationTree} class implements a CART binary decision ## tree. Growth splits each node on the single predictor and cut point ## that lower the impurity of the response the most, and stops when a node ## is pure, too small to be a parent, or has no split leaving enough ## observations on both sides. The grown tree is then optionally reduced, ## first by merging the leaves that buy no accuracy and then by cost ## complexity pruning, which orders the branch nodes by how little risk ## their subtrees remove and records that order so a subtree of any size ## can be recovered afterwards with @code{prune}. ## ## Create a @code{ClassificationTree} object by using the @code{fitctree} ## function or the class constructor. ## ## The fit is carried out by the compiled engine @code{treetrain} and ## predictions by @code{treepredict}, which the regression tree shares. ## ## An observation missing the predictor a node cuts on descends to neither ## child. It is counted in that node and in every node above it, and ## @code{predict} stops it there and gives it that node's answer, so a row ## is never sent down a branch on evidence it does not carry. ## Choosing a split, a predictor known for only part of a node is scored on ## the rows it is known for, by how much the split lowers their impurity, ## and that is scaled by their share of the node, as C4.5 does, so a ## predictor known for few rows is not preferred for that alone. ## ## A categorical predictor is split into two sets of levels, and an ## observation whose level a node did not see stops there, as one missing ## the predictor does. ## ## @strong{What this class does not do yet.} Surrogate splits and the ## @qcode{'twoing'} split criterion are not implemented, and an option ## asking for one of them is refused rather than quietly ignored. The six ## @code{Surrogate} properties are therefore always empty, as they are in ## MATLAB without surrogate splits. ## ## @seealso{fitctree, treetrain, treepredict} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} X ## ## Predictor data ## ## A numeric matrix holding the predictor data the model was fitted on. ## Each column is one predictor and each row one observation. This ## property is read-only. ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} Y ## ## Class labels ## ## A logical or numeric column vector, a character array, or a cell array ## of character vectors with one row per row of @var{X}, holding the ## observed class label of each observation. This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} NumObservations ## ## Number of observations ## ## A positive integer, the number of observations the model was fitted ## on. It counts the rows kept, so it is smaller than the number of rows ## given whenever a response was missing. This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} RowsUsed ## ## Rows used for fitting ## ## A logical column vector with one element per row of the predictor data ## as it was given, true for each row used for fitting. It is empty, ## @qcode{[]}, when every row was used, so a non-empty value means that ## rows were dropped. A row is dropped when its response is missing, when ## every predictor is missing, or when its weight is zero; a row missing ## only some of its predictors is kept. This property is read-only. ## ## @end deftp RowsUsed = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} W ## ## Observation weights ## ## A numeric column vector of the weights the fit used, one per retained ## observation. They are the weights given, scaled so that the observations ## of each class sum to that class's prior, and so that all of them together ## sum to one. It has the class of the @qcode{'Weights'} given, single or ## double. This property is read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} PredictorNames ## ## Names of the predictor variables ## ## A cell array of character vectors with one name per column of ## @var{X}. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} ResponseName ## ## Name of the response variable ## ## A character vector naming the response. This property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} ClassNames ## ## Names of the classes ## ## The distinct class labels, in the type the response was given in, ## sorted or in the order given by the @qcode{'ClassNames'} option. This ## property is read-only. ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A row vector of column indices into @var{X}, naming the predictors ## treated as categorical, empty when none is. This property is ## read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} ExpandedPredictorNames ## ## Expanded predictor names ## ## A cell array of character vectors. It differs from ## @code{PredictorNames} only when a categorical predictor has been ## expanded into one column per level, which this class does not do, so ## the two are always equal. This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} BinEdges ## ## Bin edges of the predictors ## ## A cell array with one column vector of bin edges per predictor, empty ## unless the predictors were binned before fitting. Binning is not ## implemented, so this is always empty. This property is read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} ModelParameters ## ## Parameters the fit was run with ## ## A structure recording the options the tree was grown under: ## @qcode{SplitCriterion}, @qcode{MinParent}, @qcode{MinLeaf}, ## @qcode{MaxSplits}, @qcode{NVarToSample}, @qcode{MergeLeaves}, ## @qcode{Prune}, @qcode{PruneCriterion}, @qcode{QEToler}, ## @qcode{NSurrogate}, @qcode{MaxCat}, @qcode{AlgCat}, ## @qcode{PredictorSelection}, @qcode{Method} and @qcode{Type}. ## ## @qcode{MinParent} is the value the fit used, which is ## @code{max (MinParentSize, 2 * MinLeafSize)} and so may exceed the ## @qcode{'MinParentSize'} asked for. This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} HyperparameterOptimizationResults ## ## Results of a hyperparameter optimization ## ## Hyperparameter optimization is not implemented, so this is always ## empty. This property is read-only. ## ## @end deftp HyperparameterOptimizationResults = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} NumNodes ## ## Number of nodes in the tree ## ## A positive integer, the number of nodes the tree holds, branch nodes ## and leaves together. Nodes are numbered as they are created, so a ## parent always carries a lower number than either of its children. ## This property is read-only. ## ## @end deftp NumNodes = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} Children ## ## Child nodes of each node ## ## A @math{NumNodesx2} matrix naming the left and the right child of each ## node. A leaf carries a zero in both columns. This property is ## read-only. ## ## @end deftp Children = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} Parent ## ## Parent of each node ## ## A column vector naming the parent of each node. The root carries a ## zero. This property is read-only. ## ## @end deftp Parent = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} IsBranchNode ## ## Which nodes are branch nodes ## ## A logical column vector, true for each node that carries a split and ## false for each leaf. This property is read-only. ## ## @end deftp IsBranchNode = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} CutPredictor ## ## Name of the predictor each node cuts on ## ## A cell array of character vectors with one entry per node, holding the ## name of the predictor the node splits on and an empty character vector ## at a leaf. This property is read-only. ## ## @end deftp CutPredictor = {}; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} CutPredictorIndex ## ## Index of the predictor each node cuts on ## ## A column vector holding, for each node, the column of @var{X} the node ## splits on, and zero at a leaf. This property is read-only. ## ## @end deftp CutPredictorIndex = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} CutPoint ## ## Cut point of each node ## ## A column vector holding, for each node, the value the split compares ## the predictor against: an observation goes left when its value is less ## than the cut point and right otherwise. A leaf and a categorical cut ## carry @qcode{NaN}. ## This property is read-only. ## ## @end deftp CutPoint = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} CutType ## ## Type of cut at each node ## ## A cell array of character vectors holding @qcode{'continuous'} at a ## branch node that cuts a numeric predictor at a point, ## @qcode{'categorical'} at one that splits a set of levels, and an empty ## character vector at a leaf. This property is read-only. ## ## @end deftp CutType = {}; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} CutCategories ## ## Categories used at each branch ## ## A @math{NumNodesx2} cell array holding, for a node that cuts a ## categorical predictor, the levels sent left and the levels sent right. ## Every other entry is empty. This property is read-only. ## ## @end deftp CutCategories = {}; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} CategoricalSplit ## ## Categorical splits of the tree ## ## A @math{Nx2} cell array with one row per categorical split, in node ## order, holding the levels sent left and the levels sent right. It is ## empty when no split is categorical. This property is read-only. ## ## @end deftp CategoricalSplit = {}; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} NodeSize ## ## Number of observations at each node ## ## A column vector holding how many training observations reached each ## node. A row missing the predictor its node cuts on is counted at that ## node and at none below it, so a parent's size is not in general the ## sum of its children's. This property is read-only. ## ## @end deftp NodeSize = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} NodeClass ## ## Class assigned to each node ## ## A cell array of character vectors naming, for each node, the class of ## least expected misclassification cost given the node's class ## probabilities. Under the default cost that is simply the most ## probable class, with the first of the class names kept on a tie. This ## property is read-only. ## ## @end deftp NodeClass = {}; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} NodeError ## ## Misclassification cost of each node ## ## A column vector holding, for each node, the expected misclassification ## cost of the class the node is assigned. Under the default cost that ## is the probability that the node's class is wrong, one less the ## largest class probability. This property is read-only. ## ## @end deftp NodeError = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} NodeProbability ## ## Probability of reaching each node ## ## A column vector holding, for each node, the total weight of the ## observations that reached it, the weights being those in @code{W}. ## The root carries one. This property is read-only. ## ## @end deftp NodeProbability = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} NodeRisk ## ## Risk of each node ## ## A column vector holding, for each node, the impurity of the node ## weighted by the probability of reaching it, measured by whichever of ## @qcode{'gdi'} and @qcode{'deviance'} the tree was grown under. ## ## A non-default @code{Cost} enters here rather than through the class ## probabilities: the weights are scaled class by class by the total cost ## of misclassifying that class, and the impurity is measured on the ## scaled distribution. This property is read-only. ## ## @end deftp NodeRisk = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} ClassCount ## ## Class counts at each node ## ## A @math{NumNodesxK} matrix holding how many training observations of ## each class reached each node. These are counts and take no notice of ## the observation weights. This property is read-only. ## ## @end deftp ClassCount = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} ClassProbability ## ## Class probabilities at each node ## ## A @math{NumNodesxK} matrix holding, for each node, the weight of each ## class among the observations that reached it, as a proportion of the ## node's total weight. The root row is the prior. This property is ## read-only. ## ## @end deftp ClassProbability = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} PruneList ## ## Pruning level of each node ## ## A column vector holding, for each branch node, the level of the cost ## complexity sequence at which it stops being a branch node, and zero at ## a leaf. Pruning the tree to level @var{L} turns every node whose ## level is between one and @var{L} into a leaf. It is empty when ## neither @qcode{'Prune'} nor @qcode{'MergeLeaves'} was asked for, since ## no sequence was then estimated. This property is read-only. ## ## @end deftp PruneList = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} PruneAlpha ## ## Cost complexity parameter of each pruning level ## ## A column vector with one element per level of the pruning sequence, ## the first of which is zero and stands for the unpruned tree. Level ## @var{L} is the smallest subtree that is optimal for every complexity ## parameter from @code{PruneAlpha(@var{L}+1)} up to the next one. This ## property is read-only. ## ## @end deftp PruneAlpha = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} SurrogateCutCategories ## ## Categories of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutCategories = {}; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} SurrogateCutFlip ## ## Cut assignments of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutFlip = {}; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} SurrogateCutPoint ## ## Cut points of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutPoint = {}; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} SurrogateCutType ## ## Types of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutType = {}; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} SurrogateCutPredictor ## ## Predictors of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutPredictor = {}; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} SurrogatePredictorAssociation ## ## Predictive measures of association of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogatePredictorAssociation = {}; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} Prior ## ## Prior probability of each class ## ## A numeric row vector with one element per class, summing to one. It ## defaults to the weight each class carries in the training data, and ## may be reassigned after fitting. ## ## Reassigning it re-derives @code{W} and every node statistic that ## depends on the class weights, so @code{ClassProbability}, ## @code{NodeProbability}, @code{NodeClass}, @code{NodeError} and ## @code{NodeRisk} all follow. The shape of the tree does not, having ## been decided by the prior in force when it was grown. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} Cost ## ## Misclassification cost ## ## A square numeric matrix with one row and column per class, where ## @code{Cost(i,j)} is the cost of classifying an observation of class ## @var{i} into class @var{j}. It defaults to @code{1 - eye (K)} and may ## be reassigned after fitting. ## ## Reassigning it re-derives @code{NodeClass}, @code{NodeError} and ## @code{NodeRisk}, and changes what @code{predict} answers. The shape ## of the tree does not follow, having been decided by the cost in force ## when it was grown. ## ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {ClassificationTree} {property} ScoreTransform ## ## Transform applied to the scores ## ## A character vector naming the function @code{predict} applies to the ## class probabilities before returning them, or a function handle. The ## default is @qcode{'none'}. ## ## @end deftp ScoreTransform = []; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ## The parsed ScoreTransform, applied to the scores by predict. STfun = []; ## The share of each class's total weight that reached each node, one ## row per node and one column per class. It is what the node ## statistics are re-derived from, and it is free of both the prior and ## the cost: multiplying a class's column by a constant leaves it ## unchanged, so the same table serves whatever Prior and Cost are ## assigned afterwards. ClassShare = []; ## The risk each branch node carries on account of the observations that ## stop there, missing the predictor it cuts on. Those observations are ## in neither child, so a subtree's risk is its children's plus this. It ## is zero at a leaf and zero throughout a tree fitted on data with no ## missing values. HeldRisk = []; ## The same, on the scale NodeRisk lives on, which is the impurity of the ## cost-adjusted distribution. The two part company under a non-default ## cost, and the pruning sequence wants the first while ## predictorImportance wants this one. HeldImpurity = []; ## The probability of reaching each node on the cost-adjusted weights. NodeProbAdj = []; ## The observation weights as they were given, before the prior scaled ## them. Reassigning Prior re-derives W from these, which reassigning it ## from W could not do: W has already had a prior divided into it. RawWeights = []; endproperties ## Set methods for the properties a user may assign after fitting. methods (Hidden) function this = set.Cost (this, Cost) [C, errmsg] = costMatrix (Cost, this.ClassNames); if (! isempty (errmsg)) error ("ClassificationTree.Cost: %s", errmsg); endif this.Cost = C; this = deriveNodes (this); endfunction function this = set.Prior (this, Prior) P = Prior; K = classCount (this.ClassNames); if (isstruct (P)) P = priorFromStruct (P, this.ClassNames, 'ClassificationTree.Prior'); elseif (ischar (P)) if (strcmpi (P, 'uniform')) P = ones (1, K) / K; elseif (! strcmpi (P, 'empirical')) error (strcat ("ClassificationTree.Prior: a character vector", ... " must be 'empirical' or 'uniform'.")); else return; # 'empirical' after fitting is what is already stored endif endif if (! (isnumeric (P) && isvector (P) && isreal (P))) error (strcat ("ClassificationTree.Prior: must be a real numeric", ... " vector, a structure, 'empirical', or 'uniform'.")); endif if (numel (P) != K) error (strcat ("ClassificationTree.Prior: must have one element", ... " per class.")); endif if (any (P < 0) || ! (sum (P) > 0)) error (strcat ("ClassificationTree.Prior: must be nonnegative and", ... " must not be all zero.")); endif this.Prior = P(:)' / sum (P); ## The weights follow the prior, so reassigning one re-derives the ## other, and the node statistics follow both. if (! isempty (this.RawWeights)) gY = labelIndices (this.ClassNames, this.Y); this.W = cast (priorNormalize (double (this.RawWeights), gY, ... this.Prior), class (this.RawWeights)); endif this = deriveNodes (this); endfunction function this = set.ScoreTransform (this, val) [f, st] = parseScoreTransform (val, 'ClassificationTree'); this.ScoreTransform = st; this.STfun = f; endfunction function disp (this) fprintf ('\n ClassificationTree\n\n'); fprintf ('%22s: %s\n', 'ResponseName', this.ResponseName); fprintf ('%22s: %s\n', 'CategoricalPredictors', ... mat2str (this.CategoricalPredictors)); fprintf ('%22s: %s\n', 'ClassNames', classNameListing (this.ClassNames)); fprintf ('%22s: %s\n', 'ScoreTransform', this.ScoreTransform); fprintf ('%22s: %d\n', 'NumObservations', this.NumObservations); fprintf ('%22s: %d\n', 'NumNodes', this.NumNodes); fprintf ('\n'); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {ClassificationTree} {@var{obj} =} ClassificationTree (@var{X}, @var{Y}) ## @deftypefnx {ClassificationTree} {@var{obj} =} ClassificationTree (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationTree} {@var{obj} =} ClassificationTree (@var{Tbl}, @var{formula}) ## @deftypefnx {ClassificationTree} {@var{obj} =} ClassificationTree (@var{Tbl}, @var{Y}) ## @deftypefnx {ClassificationTree} {@var{obj} =} ClassificationTree (@dots{}, @var{name}, @var{value}) ## ## Grow a binary decision tree for classification. ## ## @code{@var{obj} = ClassificationTree (@var{X}, @var{Y})} grows a tree ## on the @math{NxP} numeric matrix @var{X} of predictor data and the ## @math{Nx1} response @var{Y}, and returns it as a ## @code{ClassificationTree} object. @var{Y} may be a numeric or logical ## vector, a character array, or a cell array of character vectors, and ## the class names come back in the type it was given in. ## ## @code{@var{obj} = ClassificationTree (@dots{}, @var{name}, ## @var{value})} takes the options below. ## ## @multitable @columnfractions 0.20 0.78 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'AlgorithmForCategorical'} @tab How a node with three or ## more classes splits a categorical predictor: @qcode{'exact'}, ## @qcode{'pullleft'}, @qcode{'pca'} or @qcode{'ovabyclass'}. By default ## the exact search is taken when the node holds at most ## @qcode{'MaxNumCategories'} levels, and otherwise the best split of ## @qcode{'ovabyclass'}, @qcode{'pca'} and @qcode{'pullleft'}, leaving ## @qcode{'ovabyclass'} out above four classes. ## ## @item @qcode{'CategoricalPredictors'} @tab The predictors whose values ## are levels, as indices, as a logical vector with one element per ## predictor, or as @qcode{'all'}. ## A predictor may be named rather than indexed, as a character matrix of ## one padded name per row, a string array or a cellstr; a name must match ## an entry of @qcode{'PredictorNames'} exactly, its case included. ## ## @item @qcode{'ClassNames'} @tab The classes to fit, of the same type ## as @var{Y}. Observations of any other class are dropped. The model ## keeps the classes in this order; by default they are sorted. ## ## @item @qcode{'Cost'} @tab A square matrix with one row and column per ## class, where element @math{(i,j)} is the cost of classifying an ## observation of class @math{i} into class @math{j}, or a structure with ## fields @qcode{ClassNames} and @qcode{ClassificationCosts}. The ## default is @code{1 - eye (K)}. ## ## @item @qcode{'MaxNumCategories'} @tab A nonnegative integer, the most ## levels a node with three or more classes searches exactly by default. ## The default is 10. ## ## @item @qcode{'MaxNumSplits'} @tab A nonnegative integer, the largest ## number of branch nodes the tree may take. The default is one less ## than the number of observations, which is as many as a tree can have. ## ## @item @qcode{'MergeLeaves'} @tab @qcode{'on'} (default) or ## @qcode{'off'}. When on, a pair of leaves whose parent is no worse ## than the two of them together is merged back into that parent. ## ## @item @qcode{'MinLeafSize'} @tab A positive integer, the fewest ## observations a leaf may hold. The default is 1. A split leaving ## fewer than this on either side is not taken. ## ## @item @qcode{'MinParentSize'} @tab A positive integer, the fewest ## observations a node must hold to be split at all. The default is 10. ## The value the fit uses is @code{max (MinParentSize, 2 * MinLeafSize)}, ## since a smaller node cannot give both children a legal leaf. ## ## @item @qcode{'PredictorNames'} @tab A cell array of character vectors ## naming the columns of @var{X}. ## ## @item @qcode{'Prior'} @tab @qcode{'empirical'} (default), ## @qcode{'uniform'}, a numeric vector with one element per class, or a ## structure with fields @qcode{ClassNames} and ## @qcode{ClassProbs}. ## A class whose prior is 0, or whose misclassification costs are all 0, is ## removed from the data before fitting, with a warning, as in MATLAB. ## ## @item @qcode{'Prune'} @tab @qcode{'on'} (default) or @qcode{'off'}. ## When on, the cost complexity pruning sequence is estimated and ## reported in @code{PruneList} and @code{PruneAlpha}. The tree returned ## is the unpruned one either way; @code{prune} takes a subtree out of ## the sequence. ## ## @item @qcode{'PruneCriterion'} @tab @qcode{'error'}, the only ## criterion implemented. ## ## @item @qcode{'ResponseName'} @tab A character vector naming the ## response. The default is @qcode{'Y'}. ## ## @item @qcode{'ScoreTransform'} @tab A character vector naming a ## transform to apply to the scores, or a function handle. The default ## is @qcode{'none'}. ## ## @item @qcode{'SplitCriterion'} @tab @qcode{'gdi'} (default), the Gini ## diversity index, or @qcode{'deviance'}, the cross entropy. ## ## @item @qcode{'Weights'} @tab A nonnegative single or double vector with ## one element per observation. The default is uniform. The model's ## @code{W} keeps the class of the weights, while every computation runs in ## double, so @code{Prior} and the predictions are double where MATLAB ## returns single. ## ## @end multitable ## ## On a node with three or more classes and more than ## @qcode{'MaxNumCategories'} levels, the heuristic splits, the choice ## between equally good partitions and which side each set of levels ## takes may differ from MATLAB's. ## ## @seealso{fitctree, treetrain, treepredict} ## @end deftypefn function this = ClassificationTree (X, Y, varargin) ## Input validation if (nargin < 2) error ("ClassificationTree: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationTree: name-value arguments must be", ... " in pairs.")); endif ## A table names its own predictors and says which hold levels ## An ordinal column is a numeric predictor here, as in R2024a [this, X, Y, varargin] = resolveTable (this, 'ClassificationTree', ... X, Y, varargin, true); if (! (isnumeric (X) && isreal (X) && ismatrix (X) && ! isempty (X))) error (strcat ("ClassificationTree: X must be a non-empty real", ... " numeric matrix.")); endif if (rows (X) != rows (Y)) error (strcat ("ClassificationTree: number of rows in X and Y", ... " must be equal.")); endif ## Groups in Y, which is how a missing response is found: grp2idx ## leaves one NaN, whatever type the response is given in. [gY, gnY, glY] = grp2idx (Y); ## Parse optional paired arguments optNames = {'PredictorNames', 'ResponseName', 'ClassNames', 'Prior', ... 'Cost', 'Weights', 'ScoreTransform', 'MaxNumSplits', ... 'MinLeafSize', 'MinParentSize', 'NumVariablesToSample', ... 'MergeLeaves', 'Prune', 'PruneCriterion', ... 'SplitCriterion', 'CategoricalPredictors', ... 'MaxNumCategories', 'AlgorithmForCategorical'}; ## An empty default stands for one resolved once the data are known: ## 'PredictorNames' are x1, x2, ... and 'ResponseName' is 'Y'; ## 'ClassNames' are every class in the response, 'Cost' is zero-one ## and 'Weights' are uniform; 'MaxNumSplits' is one less than the ## number of rows kept; no predictor is categorical; and ## 'AlgorithmForCategorical' is 'auto', which MATLAB reports but does ## not accept as a value. dfValues = {{}, [], [], 'empirical', [], [], 'none', [], 1, 10, 'all', ... 'on', 'on', 'error', 'gdi', [], 10, []}; [PredictorNames, ResponseName, ClassNames, Prior, Cost, Weights, ... ScoreTransform, MaxNumSplits, MinLeafSize, MinParentSize, ... NumVarSample, MergeLeaves, Prune, PruneCriterion, SplitCriterion, ... CatPreds, MaxNumCat, AlgCat, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! isempty (PredictorNames) && ! iscellstr (PredictorNames)) error (strcat ("ClassificationTree: 'PredictorNames' must", ... " be supplied as a cellstring array.")); elseif (! isempty (PredictorNames) && numel (PredictorNames) != columns (X)) error (strcat ("ClassificationTree: 'PredictorNames' must", ... " equal the number of columns in X.")); endif if (! isempty (ResponseName) && ! (ischar (ResponseName) && isrow (ResponseName))) error (strcat ("ClassificationTree: 'ResponseName' must be", ... " a character vector.")); endif if (! isempty (ClassNames) && ! (iscellstr (ClassNames) || isnumeric (ClassNames) || islogical (ClassNames) || ischar (ClassNames) || isa (ClassNames, 'categorical') || isa (ClassNames, 'string'))) error (strcat ("ClassificationTree: 'ClassNames' must be a", ... " categorical array, a character array, a", ... " string array, a logical vector, a numeric", ... " vector, or a cell array of character", ... " vectors.")); endif ## A named class with prior 0 is removed below, so it may be absent exempt = []; if (! isempty (ClassNames)) exempt = zeroPriorNames (ClassNames, Prior, Cost); [~, errmsg] = namedClasses (glY, ClassNames, exempt); if (! isempty (errmsg)) error ("ClassificationTree: %s", errmsg); endif endif if (! (isstruct (Prior) || isnumeric (Prior) || ischar (Prior))) error (strcat ("ClassificationTree: 'Prior' must be a", ... " numeric vector, a structure, or a", ... " character vector.")); endif if (ischar (Prior) && ! any (strcmpi (Prior, {'empirical', 'uniform'}))) error (strcat ("ClassificationTree: 'Prior' must be", ... " 'empirical', 'uniform', a numeric vector,", ... " or a structure.")); endif if (! isempty (Cost) && ! (isstruct (Cost) || (isnumeric (Cost) && issquare (Cost)))) error (strcat ("ClassificationTree: 'Cost' must be a", ... " numeric square matrix or a structure.")); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("ClassificationTree: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isvector (Weights) && isreal (Weights))) error (strcat ("ClassificationTree: 'Weights' must be a", ... " real numeric vector.")); endif if (! isempty (Weights) && numel (Weights) != rows (X)) error (strcat ("ClassificationTree: 'Weights' must have one", ... " element per row in X.")); endif if (! isempty (Weights) && (any (Weights < 0) || ! (sum (Weights) > 0))) error (strcat ("ClassificationTree: 'Weights' must be", ... " nonnegative and must not be all zero.")); endif this.ScoreTransform = ScoreTransform; if (! isempty (MaxNumSplits) && ! (isnumeric (MaxNumSplits) && isscalar (MaxNumSplits) && isreal (MaxNumSplits) && MaxNumSplits >= 0 && MaxNumSplits == fix (MaxNumSplits))) error (strcat ("ClassificationTree: 'MaxNumSplits' must be", ... " a nonnegative integer.")); endif if (! (isnumeric (MinLeafSize) && isscalar (MinLeafSize) && isreal (MinLeafSize) && MinLeafSize >= 1 && MinLeafSize == fix (MinLeafSize))) error (strcat ("ClassificationTree: 'MinLeafSize' must be a", ... " positive integer.")); endif if (! (isnumeric (MinParentSize) && isscalar (MinParentSize) && isreal (MinParentSize) && MinParentSize >= 1 && MinParentSize == fix (MinParentSize))) error (strcat ("ClassificationTree: 'MinParentSize' must be", ... " a positive integer.")); endif if (! ((ischar (NumVarSample) && strcmpi (NumVarSample, 'all')) || (isnumeric (NumVarSample) && isscalar (NumVarSample) && isreal (NumVarSample) && NumVarSample >= 1 && NumVarSample == fix (NumVarSample)))) error (strcat ("ClassificationTree: 'NumVariablesToSample'", ... " must be a positive integer or 'all'.")); endif if (! (ischar (MergeLeaves) && any (strcmpi (MergeLeaves, {'on', 'off'})))) error (strcat ("ClassificationTree: 'MergeLeaves' must be", ... " either 'on' or 'off'.")); endif if (! (ischar (Prune) && any (strcmpi (Prune, {'on', 'off'})))) error (strcat ("ClassificationTree: 'Prune' must be either", ... " 'on' or 'off'.")); endif if (! (ischar (PruneCriterion) && any (strcmpi (PruneCriterion, {'error', 'impurity'})))) error (strcat ("ClassificationTree: 'PruneCriterion' must", ... " be either 'error' or 'impurity'.")); endif if (strcmpi (PruneCriterion, 'impurity')) error (strcat ("ClassificationTree: 'PruneCriterion'", ... " 'impurity' is not implemented.")); endif if (! (ischar (SplitCriterion) && any (strcmpi (SplitCriterion, {'gdi', 'deviance', ... 'twoing'})))) error (strcat ("ClassificationTree: 'SplitCriterion' must", ... " be 'gdi', 'deviance', or 'twoing'.")); endif if (strcmpi (SplitCriterion, 'twoing')) error (strcat ("ClassificationTree: 'SplitCriterion'", ... " 'twoing' is not implemented.")); endif if (! (isnumeric (MaxNumCat) && isscalar (MaxNumCat) && isreal (MaxNumCat) && MaxNumCat >= 0 && (MaxNumCat == fix (MaxNumCat) || isinf (MaxNumCat)))) error (strcat ("ClassificationTree: 'MaxNumCategories' must", ... " be a nonnegative integer.")); endif if (! isempty (AlgCat) && ! (ischar (AlgCat) && any (strcmpi (AlgCat, {'exact', ... 'pullleft', 'pca', 'ovabyclass'})))) error (strcat ("ClassificationTree:", ... " 'AlgorithmForCategorical' must be 'exact',", ... " 'pullleft', 'pca', or 'ovabyclass'.")); endif if (isempty (AlgCat)) AlgCat = 'auto'; endif ## Options MATLAB takes that this class does not implement are named ## one by one, so that asking for one is refused rather than quietly ## doing nothing; anything else left over is unknown. notImpl = {'Surrogate', 'PredictorSelection', 'NumBins', ... 'OptimizeHyperparameters', ... 'HyperparameterOptimizationOptions'}; cvNames = {'CrossVal', 'CVPartition', 'Holdout', 'KFold', 'Leaveout'}; for i = 1:2:numel (args) if (ischar (args{i}) && any (strcmpi (args{i}, notImpl))) error ("ClassificationTree: '%s' is not implemented.", args{i}); elseif (ischar (args{i}) && any (strcmpi (args{i}, cvNames))) error (strcat ("ClassificationTree: '%s' is not implemented;", ... " fit the model and cross-validate it", ... " afterwards."), args{i}); endif endfor if (! isempty (args)) error ("ClassificationTree: invalid optional paired argument."); endif ## Default predictor and response names if (isempty (PredictorNames)) PredictorNames = cell (1, columns (X)); for ii = 1:columns (X) PredictorNames{ii} = sprintf ("x%d", ii); endfor endif if (isempty (ResponseName)) ResponseName = 'Y'; endif this.PredictorNames = PredictorNames; this.ResponseName = ResponseName; ## The categorical predictors, checked as every learner checks them if (ischar (CatPreds) && strcmpi (CatPreds, 'none')) CatPreds = []; endif [C, errmsg] = dummyCoding (X, CatPreds, PredictorNames); if (! isempty (errmsg)) error ("ClassificationTree: %s", errmsg); endif this.CategoricalPredictors = []; if (! isempty (C.Index)) this.CategoricalPredictors = C.Index; endif this.ExpandedPredictorNames = PredictorNames; ## A class the caller did not ask for takes its observations with it. if (! isempty (ClassNames)) gY(! ismember (gY, namedClasses (glY, ClassNames))) = NaN; endif ## An observation is dropped when its class is missing, when every ## predictor is missing, or when its weight is zero, as R2024a drops ## it. A row missing only some predictors is kept and reported as ## used; the fit sends it as far down the tree as the predictors it ## does carry allow. RowsUsed = ! isnan (gY); if (! any (RowsUsed)) error ("ClassificationTree: no observations with a known class."); endif RowsUsed &= ! all (isnan (X), 2); if (! isempty (Weights)) RowsUsed &= Weights(:) != 0; endif if (! any (RowsUsed)) error (strcat ("ClassificationTree: no observation has a known", ... " class, a known predictor and a nonzero weight.")); endif ## Index the rows and not the elements: a response naming its classes ## in the rows of a character matrix has one column per character, and ## a linear index would flatten the names into single letters. Y = Y(RowsUsed, :); X = X(RowsUsed, :); if (! isempty (Weights)) Weights = Weights(RowsUsed); endif this.X = X; this.Y = Y; this.NumObservations = rows (X); ## RowsUsed is left empty when every observation was used, as in MATLAB if (all (RowsUsed)) this.RowsUsed = []; else this.RowsUsed = RowsUsed; endif ## The classes, in the type the response was given in, sorted or in the ## order ClassNames gives them [this.ClassNames, gY] = classOrder (Y, ClassNames, exempt); K = classCount (this.ClassNames); ## The raw weights, kept so that reassigning Prior can re-derive W if (isempty (Weights)) RawWeights = ones (this.NumObservations, 1); else RawWeights = Weights(:); endif ## The weights keep their class in the model; every computation runs on ## them as double. this.RawWeights = RawWeights; RawWeights = double (RawWeights); ## Cost first: set.Prior counts the classes and set.Cost does not ## depend on the prior, and both are wanted before the weights. if (isempty (Cost)) Cost = 1 - eye (K); endif this.Cost = Cost; ## The prior, empirical over the weights unless it was given if (ischar (Prior) && strcmpi (Prior, 'empirical')) P = zeros (1, K); for k = 1:K P(k) = sum (RawWeights(gY == k)); endfor if (! (sum (P) > 0)) error (strcat ("ClassificationTree: 'Weights' must not be zero", ... " for every observation.")); endif this.Prior = P / sum (P); else this.Prior = Prior; endif ## A class with prior 0, or that costs nothing to misclassify, is never ## predicted and weighs nothing, so its observations are removed [drop, P, msg] = removedClasses (this.Prior, this.Cost, ... this.ClassNames, gY); if (all (drop)) error ("ClassificationTree: every class has prior probability 0."); elseif (any (drop)) if (! isempty (msg)) warning ("ClassificationTree: %s", msg); endif keep = ! drop(gY); used = find (RowsUsed); RowsUsed(used(! keep)) = false; X = X(keep,:); Y = Y(keep,:); gY = gY(keep); RawWeights = RawWeights(keep); this.X = X; this.Y = Y; this.NumObservations = rows (X); this.RowsUsed = RowsUsed; this.RawWeights = this.RawWeights(keep); this.Prior = P; endif ## Weights scaled so that each class carries its prior and the whole ## sums to one, which is the W MATLAB reports. W = priorNormalize (RawWeights, gY, this.Prior); this.W = cast (W, class (this.RawWeights)); ## The engine grows the tree on the cost-adjusted weights. Scaling a ## class by the total cost of misclassifying it is the classical way a ## cost matrix enters CART, and it is what MATLAB does: the split ## criterion then needs no notion of cost, while everything reported ## below is measured on the unadjusted weights. cAdj = sum (this.Cost, 2)'; wAdj = W .* cAdj(gY)'; if (sum (wAdj) > 0) wAdj = wAdj / sum (wAdj); else ## Nothing costs anything, which is what a single class means: its ## cost matrix is the one by one zero. There is no misclassification ## to weigh, so the fit runs on the weights as they stand. wAdj = W; endif if (isempty (MaxNumSplits)) MaxNumSplits = max (this.NumObservations - 1, 0); endif ## A node smaller than two leaves cannot be split whatever the parent ## size asked for, so the fit uses the larger of the two. MinParent = max (MinParentSize, 2 * MinLeafSize); mergeOn = strcmpi (MergeLeaves, 'on'); pruneOn = strcmpi (Prune, 'on'); ## The engine measures risk as misclassified weight, which is the ## expected misclassification cost only under the default cost. Under ## any other, growth still belongs to the engine but the merge and the ## pruning sequence are taken over below, on the risk the cost defines. plainCost = isequal (this.Cost, 1 - eye (K)); ## A number covering every predictor samples them all, and is reported ## as 'all', as MATLAB reports it. The engine's seed is drawn only when ## predictors are sampled, so an unsampled fit leaves the generator alone. if (isnumeric (NumVarSample) && NumVarSample >= columns (X)) NumVarSample = 'all'; endif opts = struct ('NumClasses', K, ... 'MinParent', MinParent, ... 'MinLeaf', MinLeafSize, ... 'MaxSplits', MaxNumSplits, ... 'SplitCriterion', tolower (SplitCriterion), ... 'MergeLeaves', plainCost && mergeOn); if (isnumeric (NumVarSample)) opts.NumVariablesToSample = NumVarSample; opts.Seed = randi ([0, 4294967295]); endif if (! isempty (this.CategoricalPredictors)) opts.CategoricalPredictors = this.CategoricalPredictors; opts.MaxNumCategories = MaxNumCat; opts.AlgorithmForCategorical = tolower (AlgCat); endif T = treetrain (X, gY, wAdj, opts); ## The node table the engine returns this.NumNodes = T.NumNodes; this.Children = T.Children; this.Parent = T.Parent; this.CutPredictorIndex = T.CutPredictorIndex; this.CutPoint = T.CutPoint; this.CutCategories = T.CutCategories; this.IsBranchNode = logical (T.IsBranchNode); this.NodeSize = T.NodeSize; this.ClassCount = T.ClassCount; ## The share of each class's weight that reached each node. Dividing ## by the root's row cancels both the prior and the cost adjustment, ## since each scales a whole column, so what is left is the table the ## node statistics are re-derived from whenever either is reassigned. this.ClassShare = classShareOf (T.ClassWeight); this = fillCuts (this); this.ModelParameters = struct ('SplitCriterion', ... tolower (SplitCriterion), ... 'MinParent', MinParent, ... 'MinLeaf', MinLeafSize, ... 'MaxSplits', MaxNumSplits, ... 'NVarToSample', NumVarSample, ... 'MergeLeaves', tolower (MergeLeaves), ... 'Prune', tolower (Prune), ... 'PruneCriterion', ... tolower (PruneCriterion), ... 'QEToler', [], ... 'NSurrogate', 0, ... 'MaxCat', MaxNumCat, ... 'AlgCat', AlgCat, ... 'PredictorSelection', 'allsplits', ... 'Method', 'Tree', ... 'Type', 'classification'); ## deriveNodes reads ModelParameters.SplitCriterion, so it follows the ## structure above rather than the node table it works on. this = deriveNodes (this); this = deriveHeld (this); if (! plainCost && mergeOn) this = mergeLeaves (this); endif ## Merging leaves is the first step of the cost complexity sequence, so ## a merged tree carries one whether or not pruning was asked for. ## Measured on R2024a: only turning both off leaves the two pruning ## properties empty. if (pruneOn || mergeOn) this = pruneSequence (this); endif endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationTree} {@var{label} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {ClassificationTree} {[@var{label}, @var{score}] =} predict (@dots{}) ## @deftypefnx {ClassificationTree} {[@var{label}, @var{score}, @var{node}] =} predict (@dots{}) ## @deftypefnx {ClassificationTree} {[@var{label}, @var{score}, @var{node}, @var{cnum}] =} predict (@dots{}) ## ## Classify new data with a trained @code{ClassificationTree} object. ## ## @code{@var{label} = predict (@var{obj}, @var{XC})} sends each row of ## @var{XC} down the tree and returns the class of the node it comes to ## rest at. @var{XC} must have as many columns as the predictor data the ## model was fitted on. ## ## @var{XC} may also be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: one ## the model was not fitted on is passed over, one it needs and cannot ## find is named, and a value holding a level is coded as that level was ## coded at fitting. ## ## @code{[@var{label}, @var{score}] = predict (@dots{})} also returns ## @var{score}, an @math{NxK} matrix holding the class probabilities of ## the node each row landed in, after @code{ScoreTransform}. ## ## @code{[@var{label}, @var{score}, @var{node}] = predict (@dots{})} also ## returns the number of the node each row landed in, and ## @code{[@var{label}, @var{score}, @var{node}, @var{cnum}] = predict ## (@dots{})} the index of the predicted class into @code{ClassNames}. ## ## The label is the class of least expected misclassification cost, which ## under the default @code{Cost} is the most probable class of the node. ## ## A row missing the predictor a node cuts on is stopped at that node and ## takes its answer, rather than being sent down a branch on evidence the ## row does not carry. ## ## @seealso{ClassificationTree, fitctree} ## @end deftypefn function [label, score, node, cnum] = predict (this, XC) ## Input validation if (nargin < 2) error ("ClassificationTree.predict: too few input arguments."); endif if (isempty (XC)) error ("ClassificationTree.predict: XC is empty."); endif ## A table is read by the names the model was fitted on, not by the ## order its own columns happen to come in XC = tableColumns (this, 'ClassificationTree.predict', XC); if (! (isnumeric (XC) && isreal (XC) && ismatrix (XC))) error (strcat ("ClassificationTree.predict: XC must be a real", ... " numeric matrix.")); endif if (columns (this.X) != columns (XC)) error (strcat ("ClassificationTree.predict: XC must have the same", ... " number of predictors as the trained model.")); endif [score, node] = treepredict (XC, this.Children, ... this.CutPredictorIndex, this.CutPoint, ... this.ClassProbability, ... this.CutCategories); ## The class of least expected cost, ties kept by the first class [~, cnum] = min (score * this.Cost, [], 2); label = labelsFromIndex (this.ClassNames, cnum); ## The transform is applied once, to the assembled probabilities score = this.STfun (score); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationTree} {@var{obj2} =} prune (@var{obj}) ## @deftypefnx {ClassificationTree} {@var{obj2} =} prune (@var{obj}, @qcode{'Level'}, @var{L}) ## @deftypefnx {ClassificationTree} {@var{obj2} =} prune (@var{obj}, @qcode{'Alpha'}, @var{A}) ## @deftypefnx {ClassificationTree} {@var{obj2} =} prune (@var{obj}, @qcode{'Nodes'}, @var{N}) ## ## Take a subtree out of the pruning sequence. ## ## @code{@var{obj2} = prune (@var{obj})} returns the tree unchanged. ## ## @code{@var{obj2} = prune (@var{obj}, @qcode{'Level'}, @var{L})} turns ## every branch node whose @code{PruneList} level is between one and ## @var{L} into a leaf and discards everything below it. Level zero is ## the tree itself and the largest level is the root alone. A level ## above the largest prunes to the root and warns. ## ## @code{@var{obj2} = prune (@var{obj}, @qcode{'Alpha'}, @var{A})} prunes ## to the smallest subtree that is optimal for the cost complexity ## parameter @var{A}, which is the largest level whose @code{PruneAlpha} ## does not exceed it. ## ## @code{@var{obj2} = prune (@var{obj}, @qcode{'Nodes'}, @var{N})} turns ## the branch nodes named in @var{N} into leaves, along with everything ## below them, and leaves the rest of the tree alone. ## ## Pruning renumbers the nodes, so the properties of the returned tree ## are those of a tree of that shape and not a subset of the original's. ## ## @seealso{ClassificationTree, fitctree, ClassificationTree.PruneList, ## ClassificationTree.PruneAlpha} ## @end deftypefn function this = prune (this, varargin) ## Input validation if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationTree.prune: name-value arguments", ... " must be in pairs.")); endif if (numel (varargin) > 2) error (strcat ("ClassificationTree.prune: specify only one of the", ... " optional name-value paired arguments.")); endif nodes = []; if (numel (varargin) == 2) Value = varargin{2}; switch (tolower (varargin{1})) case 'level' if (! (isnumeric (Value) && isscalar (Value) && isreal (Value) && Value >= 0 && Value == fix (Value))) error (strcat ("ClassificationTree.prune: 'Level' must be a", ... " nonnegative integer.")); endif nodes = this.nodesAtLevel (Value, 'prune'); case 'alpha' if (! (isnumeric (Value) && isscalar (Value) && isreal (Value) && Value >= 0)) error (strcat ("ClassificationTree.prune: 'Alpha' must be a", ... " nonnegative scalar.")); endif if (isempty (this.PruneAlpha)) error (strcat ("ClassificationTree.prune: the tree carries", ... " no pruning sequence.")); endif ## The largest level whose parameter the value reaches L = sum (this.PruneAlpha <= Value) - 1; nodes = this.nodesAtLevel (max (L, 0), 'prune'); case 'nodes' if (! (isnumeric (Value) && isreal (Value) && isvector (Value) && all (Value >= 1) && all (Value <= this.NumNodes) && all (Value == fix (Value)))) error (strcat ("ClassificationTree.prune: 'Nodes' must hold", ... " indices of nodes of the tree.")); endif nodes = Value(:); otherwise error (strcat ("ClassificationTree.prune: invalid parameter", ... " name in optional pair arguments.")); endswitch endif if (isempty (nodes)) return; endif this = collapseNodes (this, nodes); ## The sequence of the tree that is left, not a slice of the one the ## tree came with: pruning renumbers the nodes and a subtree of an ## optimal sequence is the optimal sequence of that subtree. this = pruneSequence (this); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationTree} {@var{E} =} cvloss (@var{obj}) ## @deftypefnx {ClassificationTree} {[@var{E}, @var{SE}, @var{Nleaf}, @var{BestLevel}] =} cvloss (@var{obj}) ## @deftypefnx {ClassificationTree} {[@dots{}] =} cvloss (@dots{}, @var{name}, @var{value}) ## ## Cross-validated loss of a tree and of its subtrees. ## ## @code{@var{E} = cvloss (@var{obj})} partitions the training data into ## ten stratified folds, grows a tree on the training part of each, and ## returns the loss of the held-out part. ## ## @code{[@var{E}, @var{SE}, @var{Nleaf}, @var{BestLevel}] = cvloss ## (@dots{})} also returns @var{SE}, the standard error of @var{E} over ## the folds, @var{Nleaf}, the number of leaves each subtree holds, and ## @var{BestLevel}, the pruning level chosen by @qcode{'TreeSize'}. Each ## has one element per subtree asked for. ## ## @code{[@dots{}] = cvloss (@dots{}, @var{name}, @var{value})} takes the ## options below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'SubTrees'} @tab A vector of pruning levels in ascending ## order, or @qcode{'all'} for every level of the sequence. The default ## is 0, the unpruned tree. ## ## @item @qcode{'TreeSize'} @tab @qcode{'se'} (default), the smallest ## subtree whose loss is within one standard error of the smallest loss, ## or @qcode{'min'}, the smallest subtree of least loss. ## ## @item @qcode{'KFold'} @tab An integer greater than 1, the number of ## folds. The default is 10. A value above the number of observations ## is reduced to it. ## ## @end multitable ## ## A fold's tree is pruned to the level its own sequence gives for the ## geometric mean of the parent's two neighbouring complexity parameters, ## which is the classical way a fold is matched to a subtree of the whole ## tree. The last level takes every fold's tree back to its root. ## ## Every fold is grown with the parent's class names, prior, cost and ## observation weights, and the loss is weighed by the model's own ## weights. ## ## @strong{The standard error is not MATLAB's.} This is the standard ## error of the loss over the folds, which is what the name means and ## which is zero when every fold answers alike, as MATLAB's is. Its ## value is not MATLAB's: on the iris tree MATLAB reports 0.019956 where ## the folds give 0.024343, and no formula over the fold losses, the ## observation losses or the loss itself reproduces MATLAB's number. ## @var{E}, @var{Nleaf} and @var{BestLevel} are measured and match. ## ## @seealso{ClassificationTree, ClassificationTree.prune, ## ClassificationTree.crossval, ClassificationTree.loss} ## @end deftypefn function [E, SE, Nleaf, BestLevel] = cvloss (this, varargin) ## Input validation if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationTree.cvloss: name-value arguments", ... " must be in pairs.")); endif if (isempty (this.PruneAlpha)) error (strcat ("ClassificationTree.cvloss: the tree carries no", ... " pruning sequence; fit it with 'Prune' or", ... " 'MergeLeaves' on.")); endif maxLevel = numel (this.PruneAlpha) - 1; ## Parse optional paired arguments; 'SubTrees' defaults to the tree ## as fitted, level 0 optNames = {'SubTrees', 'TreeSize', 'KFold'}; dfValues = {0, 'se', 10}; [SubTrees, TreeSize, KFold, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (ischar (SubTrees) && strcmpi (SubTrees, 'all')) SubTrees = 0:maxLevel; elseif (isnumeric (SubTrees) && isreal (SubTrees) && isvector (SubTrees) && ! isempty (SubTrees) && all (SubTrees >= 0) && all (SubTrees == fix (SubTrees)) && all (diff (SubTrees(:)') > 0)) SubTrees = SubTrees(:)'; else error (strcat ("ClassificationTree.cvloss: 'SubTrees' must be", ... " 'all' or a vector of nonnegative integers in", ... " ascending order.")); endif if (! (ischar (TreeSize) && any (strcmpi (TreeSize, {'se', 'min'})))) error (strcat ("ClassificationTree.cvloss: 'TreeSize' must be", ... " either 'se' or 'min'.")); endif TreeSize = tolower (TreeSize); if (! (isnumeric (KFold) && isscalar (KFold) && isreal (KFold) && KFold == fix (KFold) && KFold > 1)) error (strcat ("ClassificationTree.cvloss: 'KFold' must be an", ... " integer value greater than 1.")); endif if (! isempty (args)) error ("ClassificationTree.cvloss: invalid optional paired argument."); endif if (any (SubTrees > maxLevel)) error (strcat ("ClassificationTree.cvloss: 'SubTrees' must not", ... " exceed the largest pruning level, %d."), maxLevel); endif if (KFold > this.NumObservations) warning (strcat ("ClassificationTree.cvloss: 'KFold' is greater", ... " than the number of observations and is reduced", ... " to %d."), this.NumObservations); KFold = this.NumObservations; endif ## The complexity parameter each subtree is matched to in a fold: the ## geometric mean of the two the parent's own sequence brackets it ## with, and infinity for the last, which takes a fold back to its ## root. Measured on R2024a: matching a fold by level index or by the ## parent's parameter itself both give different answers, and this one ## reproduces every loss the oracle reports. alpha = this.PruneAlpha(:)'; ## Formed outside the brackets: inside them the space before the paren ## would split the call off into an element of its own. geo = sqrt (alpha(1:end-1) .* alpha(2:end)); geo(end+1) = Inf; nsub = numel (SubTrees); n = this.NumObservations; gY = labelIndices (this.ClassNames, this.Y); partition = cvpartition (this.Y, 'KFold', KFold); args = treeFoldArgs (this); ## The loss of every observation, from the fold that did not train on it L = zeros (n, nsub); fold = zeros (n, 1); for k = 1:KFold tr = training (partition, k); te = test (partition, k); fold(te) = k; ft = ClassificationTree (this.X(tr,:), this.Y(tr,:), args{:}, ... 'Weights', this.RawWeights(tr)); fa = ft.PruneAlpha(:)'; for j = 1:nsub lv = sum (fa <= geo(SubTrees(j) + 1)) - 1; ## The nodes are collapsed without re-estimating the fold's own ## sequence, which prune would do and which nothing here reads. sub = collapseNodes (ft, nodesAtLevel (ft, max (lv, 0), 'cvloss')); [~, ~, ~, cnum] = predict (sub, this.X(te,:)); L(te,j) = this.Cost(sub2ind (size (this.Cost), gY(te), cnum)); endfor endfor ## The loss is weighed by the model's own weights, which sum to one. ## Measured on R2024a, where a tree weighted 1 to 150 reports ## 0.0537748344370861 and the unweighted mean of the same losses is ## 0.0533333333333333. W = double (this.W(:)); E = W' * L; ## The standard error over the folds, each fold's loss being its ## observations' share of the whole. foldloss = zeros (KFold, nsub); for k = 1:KFold idx = fold == k; sw = sum (W(idx)); if (sw > 0) foldloss(k,:) = (W(idx)' * L(idx,:)) / sw; endif endfor SE = std (foldloss, 0, 1) / sqrt (KFold); ## The leaves each subtree of the whole tree holds Nleaf = zeros (1, nsub); for j = 1:nsub Nleaf(j) = sum (! prune (this, 'Level', SubTrees(j)).IsBranchNode); endfor ## The smallest subtree the rule allows, which is the largest level if (strcmp (TreeSize, 'min')) best = find (E == min (E), 1, 'last'); else [~, i] = min (E); best = find (E <= E(i) + SE(i), 1, 'last'); endif BestLevel = SubTrees(best); E = E(:); SE = SE(:); Nleaf = Nleaf(:); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationTree} {@var{imp} =} predictorImportance (@var{obj}) ## ## Estimate the importance of each predictor. ## ## @code{@var{imp} = predictorImportance (@var{obj})} returns a row ## vector with one element per predictor, holding the total drop in risk ## the splits on that predictor bring about, divided by the number of ## branch nodes. A predictor the tree never splits on scores zero. ## ## The drop at a branch node is its @code{NodeRisk} less the risk of its ## two children and less what it holds back, so a predictor that is ## chosen often, high up, and on nodes it separates well, scores ## highest. The numbers are comparable ## between predictors of one tree and not between trees. ## ## @seealso{ClassificationTree, fitctree, ClassificationTree.NodeRisk} ## @end deftypefn function imp = predictorImportance (this) imp = zeros (1, columns (this.X)); branch = find (this.Children(:,1) > 0); if (isempty (branch)) return; endif for ii = 1:numel (branch) b = branch(ii); kids = this.Children(b,:); ## The rows that stop at the node are in neither child and are ## answered the same whether it is split or not, so they are no part ## of what the split buys. drop = this.NodeRisk(b) - this.NodeRisk(kids(1)) ... - this.NodeRisk(kids(2)) - this.HeldImpurity(b); v = this.CutPredictorIndex(b); imp(v) = imp(v) + drop; endfor imp = imp / numel (branch); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationTree} {@var{r} =} nodeVariableRange (@var{obj}, @var{node}) ## ## Range of each predictor at a node. ## ## @code{@var{r} = nodeVariableRange (@var{obj}, @var{node})} returns a ## structure with one field per predictor the path from the root to ## @var{node} cuts on, holding the two-element range of values that reach ## the node, or for a categorical predictor the levels that reach it. A ## predictor the path never cuts on is unconstrained and is left out, so ## the root gives a structure with no fields. ## ## @seealso{ClassificationTree, fitctree} ## @end deftypefn function r = nodeVariableRange (this, node) ## Input validation if (nargin < 2) error (strcat ("ClassificationTree.nodeVariableRange: too few", ... " input arguments.")); endif if (! (isnumeric (node) && isscalar (node) && isreal (node) && node >= 1 && node <= this.NumNodes && node == fix (node))) error (strcat ("ClassificationTree.nodeVariableRange: NODE must", ... " be a positive integer no greater than the number", ... " of nodes in the tree.")); endif p = columns (this.X); lo = -Inf (1, p); hi = Inf (1, p); touched = false (1, p); ## Walk up to the root, narrowing the range of whichever predictor ## each step cut on. Going up rather than down finds the path ## without a search, a node having exactly one parent. ## A categorical cut narrows its predictor to a set of levels instead, ## and the first such cut met on the way up is the narrowest. levels = cell (1, p); child = node; up = this.Parent(child); while (up > 0) v = this.CutPredictorIndex(up); side = 1 + (this.Children(up,1) != child); cats = this.CutCategories{up,side}; if (! isempty (cats)) if (! touched(v)) levels{v} = cats; endif elseif (side == 1) hi(v) = min (hi(v), this.CutPoint(up)); else lo(v) = max (lo(v), this.CutPoint(up)); endif touched(v) = true; child = up; up = this.Parent(child); endwhile r = struct (); for v = find (touched) if (isempty (levels{v})) r.(this.PredictorNames{v}) = [lo(v), hi(v)]; else r.(this.PredictorNames{v}) = levels{v}; endif endfor endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationTree} {} view (@var{obj}) ## ## Print the tree as text. ## ## @code{view (@var{obj})} prints one line per node: a branch node names ## the predictor it cuts on, the cut point, and the node each side leads ## to, and a leaf names the class it assigns. A branch node's line ends ## with the class it would assign itself, which is the answer an ## observation missing that predictor gets. ## ## @seealso{ClassificationTree, fitctree} ## @end deftypefn function view (this) ## The node numbers are right aligned on the widest of them, which is ## how MATLAB lays the listing out. w = numel (sprintf ("%d", this.NumNodes)); fprintf ("Decision tree for classification\n"); for ii = 1:this.NumNodes if (this.Children(ii,1) == 0) fprintf ("%*d class = %s\n", w, ii, this.NodeClass{ii}); else v = this.PredictorNames{this.CutPredictorIndex(ii)}; cc = this.CutCategories(ii,:); if (! isempty (cc{1})) fprintf ("%*d if %s then node %d elseif %s then node", w, ii, ... treeLevelText (v, cc{1}), this.Children(ii,1), ... treeLevelText (v, cc{2})); else c = sprintf ("%g", this.CutPoint(ii)); fprintf ("%*d if %s<%s then node %d elseif %s>=%s then node", ... w, ii, v, c, this.Children(ii,1), v, c); endif fprintf (" %d else %s\n", this.Children(ii,2), this.NodeClass{ii}); endif endfor endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationTree} {@var{CMdl} =} compact (@var{obj}) ## ## Drop the training data from a trained model. ## ## @code{@var{CMdl} = compact (@var{obj})} returns a ## @code{CompactClassificationTree} object carrying the tree and ## everything @code{predict} needs, but not the observations the model ## was fitted on. It classifies new data identically and is far smaller ## to keep or to ship. ## ## @seealso{CompactClassificationTree, ClassificationTree} ## @end deftypefn function CMdl = compact (this) CMdl = CompactClassificationTree (this); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationTree} {@var{CVMdl} =} crossval (@var{obj}) ## @deftypefnx {ClassificationTree} {@var{CVMdl} =} crossval (@dots{}, @var{name}, @var{value}) ## ## Cross-validate a trained decision tree. ## ## @code{@var{CVMdl} = crossval (@var{obj})} partitions the training data ## into ten folds, or into as many folds as there are observations when ## there are fewer than ten, grows a tree on the training part of each ## and returns them as a @code{ClassificationPartitionedModel}. ## ## @code{@var{CVMdl} = crossval (@dots{}, @var{name}, @var{value})} takes ## one of the following, and one only. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'KFold'} @tab An integer greater than 1, the number of ## folds. ## ## @item @qcode{'Holdout'} @tab A value between 0 and 1, the fraction of ## the data held out for testing, which gives a single fold. ## ## @item @qcode{'Leaveout'} @tab @qcode{'on'} or @qcode{'off'}, one fold ## per observation. ## ## @item @qcode{'CVPartition'} @tab A @code{cvpartition} object. ## ## @end multitable ## ## Every fold is grown with the parent's class names, prior, cost and ## observation weights rather than being left to re-derive them from its ## own rows, so a fold reports the prior of the whole data and not its own ## frequencies. ## ## @seealso{ClassificationPartitionedModel, ClassificationTree, cvpartition} ## @end deftypefn function CVMdl = crossval (this, varargin) ## Input validation if (numel (varargin) == 1) error (strcat ("ClassificationTree.crossval: Name-Value", ... " arguments must be in pairs.")); elseif (numel (varargin) > 2) error (strcat ("ClassificationTree.crossval: specify only one of", ... " the optional Name-Value paired arguments.")); endif if (this.NumObservations < 10) numFolds = this.NumObservations; else numFolds = 10; endif Holdout = []; Leaveout = 'off'; CVPartition = []; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'kfold' numFolds = varargin{2}; if (! (isnumeric (numFolds) && isscalar (numFolds) && (numFolds == fix (numFolds)) && numFolds > 1)) error (strcat ("ClassificationTree.crossval: 'KFold' must", ... " be an integer value greater than 1.")); endif case 'holdout' Holdout = varargin{2}; if (! (isnumeric (Holdout) && isscalar (Holdout) && Holdout > 0 && Holdout < 1)) error (strcat ("ClassificationTree.crossval: 'Holdout' must", ... " be a numeric value between 0 and 1.")); endif case 'leaveout' Leaveout = varargin{2}; if (! (ischar (Leaveout) && any (strcmpi (Leaveout, {'on', 'off'})))) error (strcat ("ClassificationTree.crossval: 'Leaveout'", ... " must be either 'on' or 'off'.")); endif case 'cvpartition' CVPartition = varargin{2}; if (! (isa (CVPartition, 'cvpartition'))) error (strcat ("ClassificationTree.crossval: 'CVPartition'", ... " must be a 'cvpartition' object.")); endif otherwise error (strcat ("ClassificationTree.crossval: invalid", ... " parameter name in optional paired arguments.")); endswitch varargin(1:2) = []; endwhile ## The partition covers the observations actually trained on: a row ## dropped for a missing response is not one the folds can use. The ## response is passed rather than a count so the folds stay stratified. if (! isempty (CVPartition)) partition = CVPartition; elseif (! isempty (Holdout)) partition = cvpartition (this.Y, 'Holdout', Holdout); elseif (strcmpi (Leaveout, 'on')) partition = cvpartition (this.NumObservations, 'LeaveOut'); else partition = cvpartition (this.Y, 'KFold', numFolds); endif CVMdl = ClassificationPartitionedModel (this, partition); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationTree} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationTree} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationTree} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margin on new data. ## ## @code{@var{m} = margin (@var{obj}, @var{X}, @var{Y})} returns one ## margin per observation: the score the model gives the observation's ## true class, less the largest score it gives any other class. A ## positive margin means the observation is classified correctly, and a ## larger one means it is classified more confidently. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{ClassificationTree, ClassificationTree.edge, ## ClassificationTree.loss, ClassificationTree.predict} ## @end deftypefn function m = margin (this, X, Y) ## Input validation if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationTree.margin: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y] = tableResponse (this, 'margin', X, Y, {}, nargin > 2); [gY, errmsg] = labelIndices (this.ClassNames, Y); if (! isempty (errmsg)) error ("ClassificationTree.margin: %s", errmsg); endif if (rows (X) != numel (gY)) error (strcat ("ClassificationTree.margin: number of rows in X", ... " and Y must be equal.")); endif [~, s] = predict (this, X); m = marginsOf (s, gY, 1); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationTree} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationTree} {@var{e} =} edge (@dots{}, @qcode{'Weights'}, @var{w}) ## @deftypefnx {ClassificationTree} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationTree} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Classification edge on new data. ## ## @code{@var{e} = edge (@var{obj}, @var{X}, @var{Y})} returns the ## weighted mean of the margins, a single number summarising how ## confidently the model classifies the data. ## ## The weights are normalized within each class to that class's prior ## before they are applied. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{ClassificationTree, ClassificationTree.margin, ## ClassificationTree.loss, ClassificationTree.predict} ## @end deftypefn function e = edge (this, X, Y, varargin) ## Input validation if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationTree.edge: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'edge', X, Y, varargin, ... nargin > 2); W = edgeWeights (varargin, Y, this.ClassNames, this.Prior, ... 'ClassificationTree', 'edge'); m = margin (this, X, Y); e = sum (W .* m(:)) / sum (W); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationTree} {@var{l} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {ClassificationTree} {@var{l} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {ClassificationTree} {@var{l} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {ClassificationTree} {@var{l} =} loss (@dots{}, @var{name}, @var{value}) ## ## Classification loss on new data. ## ## @code{@var{l} = loss (@var{obj}, @var{X}, @var{Y})} returns the ## minimum expected misclassification cost. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{l} = loss (@dots{}, @var{name}, @var{value})} takes the ## following options. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'LossFun'} @tab One of @qcode{'binodeviance'}, ## @qcode{'classifcost'}, @qcode{'classiferror'}, @qcode{'exponential'}, ## @qcode{'hinge'}, @qcode{'logit'}, @qcode{'mincost'} (default) or ## @qcode{'quadratic'}. ## ## @item @qcode{'Weights'} @tab A numeric vector of observation weights, ## one per row of @var{X}. ## ## @end multitable ## ## @seealso{ClassificationTree, ClassificationTree.margin, ## ClassificationTree.edge, ClassificationTree.predict} ## @end deftypefn function l = loss (this, X, Y, varargin) ## Input validation if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("ClassificationTree.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("ClassificationTree.loss: name-value arguments", ... " must be in pairs.")); endif ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights'}; dfValues = {'mincost', []}; [LossFun, Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments lf_opt = {'binodeviance', 'classifcost', 'classiferror', ... 'exponential', 'hinge', 'logit', 'mincost', 'quadratic'}; if (! (ischar (LossFun) && any (strcmpi (LossFun, lf_opt)))) error ("ClassificationTree.loss: invalid loss function."); endif LossFun = tolower (LossFun); errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("ClassificationTree.loss: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isvector (Weights))) error ("ClassificationTree.loss: invalid 'Weights'."); endif if (! isempty (args)) error ("ClassificationTree.loss: invalid optional paired argument."); endif [gY, errmsg] = labelIndices (this.ClassNames, Y); if (! isempty (errmsg)) error ("ClassificationTree.loss: %s", errmsg); endif if (rows (X) != numel (gY)) error (strcat ("ClassificationTree.loss: number of rows in X and", ... " Y must be equal.")); endif if (isempty (Weights)) w = ones (numel (gY), 1); else w = double (Weights(:)); if (numel (w) != numel (gY)) error (strcat ("ClassificationTree.loss: 'Weights' must have", ... " one element per observation.")); endif endif ## The weights are normalized within each class to that class's prior, ## so that a loss and an edge weight the classes the same way. w = priorNormalize (w, gY, this.Prior); [~, s] = predict (this, X); l = classificationLoss (LossFun, s, gY, w, this.Cost); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationTree} {@var{label} =} resubPredict (@var{obj}) ## @deftypefnx {ClassificationTree} {[@var{label}, @var{score}, @var{node}, @var{cnum}] =} resubPredict (@var{obj}) ## ## Classify the training data with the model fitted to it. ## ## @code{@var{label} = resubPredict (@var{obj})} is ## @code{predict (@var{obj}, @var{obj}.X)}, and takes the same outputs. ## ## @seealso{ClassificationTree, ClassificationTree.predict} ## @end deftypefn function [label, score, node, cnum] = resubPredict (this) [label, score, node, cnum] = predict (this, this.X); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationTree} {@var{m} =} resubMargin (@var{obj}) ## ## Classification margin on the training data. ## ## @code{@var{m} = resubMargin (@var{obj})} is ## @code{margin (@var{obj}, @var{obj}.X, @var{obj}.Y)}. ## ## @seealso{ClassificationTree, ClassificationTree.margin} ## @end deftypefn function m = resubMargin (this) m = margin (this, this.X, this.Y); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationTree} {@var{e} =} resubEdge (@var{obj}) ## ## Classification edge on the training data. ## ## @code{@var{e} = resubEdge (@var{obj})} is @code{edge} over the ## training data, weighed as the fit weighed it. ## ## @seealso{ClassificationTree, ClassificationTree.edge} ## @end deftypefn function e = resubEdge (this) ## As with resubLoss, the training data is weighed as the fit weighed ## it. Measured on R2024a: on a weighted fit this is edge over the ## model's own weights, 0.856171294940654 on an iris tree weighted ## 1 to 150, and not the 0.853582991377224 uniform weights give. e = edge (this, this.X, this.Y, 'Weights', this.RawWeights); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationTree} {@var{l} =} resubLoss (@var{obj}) ## @deftypefnx {ClassificationTree} {@var{l} =} resubLoss (@dots{}, @var{name}, @var{value}) ## ## Classification loss on the training data. ## ## @code{@var{l} = resubLoss (@var{obj})} is @code{loss} over the ## training data, weighed as the fit weighed it, and takes the same ## @qcode{'LossFun'} option. Giving @qcode{'Weights'} weighs the ## training data some other way instead, which MATLAB refuses rather ## than honours. ## ## @seealso{ClassificationTree, ClassificationTree.loss} ## @end deftypefn function l = resubLoss (this, varargin) ## The training data is weighed as the fit weighed it. Measured on ## R2024a: a weighted fit reports a resubstitution loss over its own ## weights, where an unweighted one and a loss call over the same data ## agree with it because their weights are uniform. if (! any (strcmpi (varargin(1:2:end), 'weights'))) varargin = [varargin, {'Weights', this.RawWeights}]; endif l = loss (this, this.X, this.Y, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {ClassificationTree} {} savemodel (@var{obj}, @var{filename}) ## ## Save a ClassificationTree model to a file. ## ## @code{savemodel (@var{obj}, @var{filename})} saves each property of a ## ClassificationTree object into an Octave binary file, the name of ## which is specified in @var{filename}, along with an extra variable, ## which defines the type of classification object these variables ## constitute. Use @code{loadmodel} in order to load a classification ## object into Octave's workspace. ## ## @seealso{loadmodel, fitctree, ClassificationTree} ## @end deftypefn function savemodel (this, fname) ## Input validation if (nargin < 2) error ("ClassificationTree.savemodel: too few input arguments."); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error (strcat ("ClassificationTree.savemodel: FNAME must be a", ... " character vector.")); endif ## Generate variable for class name classdef_name = 'ClassificationTree'; ## Create variables from model properties X = this.X; Y = encodeLabels (this.Y); RowsUsed = this.RowsUsed; W = this.W; RawWeights = this.RawWeights; NumObservations = this.NumObservations; PredictorNames = this.PredictorNames; ResponseName = this.ResponseName; ClassNames = encodeLabels (this.ClassNames); CategoricalPredictors = this.CategoricalPredictors; ExpandedPredictorNames = this.ExpandedPredictorNames; BinEdges = this.BinEdges; ModelParameters = this.ModelParameters; NumNodes = this.NumNodes; Children = this.Children; Parent = this.Parent; IsBranchNode = this.IsBranchNode; CutPredictorIndex = this.CutPredictorIndex; CutPoint = this.CutPoint; CutCategories = this.CutCategories; NodeSize = this.NodeSize; ClassCount = this.ClassCount; ClassShare = this.ClassShare; PruneList = this.PruneList; PruneAlpha = this.PruneAlpha; Prior = this.Prior; Cost = this.Cost; ScoreTransform = this.ScoreTransform; STfun = this.STfun; HyperparameterOptimizationResults = ... this.HyperparameterOptimizationResults; ## The cut and node descriptions are not saved: every one of them is ## re-derived from the node table above, so writing them out would only ## make a stale copy possible. save ('-binary', fname, 'classdef_name', 'X', 'Y', 'RowsUsed', 'W', ... 'RawWeights', 'NumObservations', 'PredictorNames', ... 'ResponseName', 'ClassNames', 'CategoricalPredictors', ... 'ExpandedPredictorNames', 'BinEdges', 'ModelParameters', ... 'NumNodes', 'Children', 'Parent', 'IsBranchNode', ... 'CutPredictorIndex', 'CutPoint', 'CutCategories', ... 'NodeSize', 'ClassCount', ... 'ClassShare', 'PruneList', 'PruneAlpha', 'Prior', 'Cost', ... 'ScoreTransform', 'STfun', ... 'HyperparameterOptimizationResults'); endfunction endmethods methods (Static, Hidden) function mdl = load_model (filename, data) data = decodeLabels (data); ## The smallest fit the class accepts, filled property by property ## below. Nothing of the stub survives the copy. mdl = ClassificationTree ([1; 2], [1; 2]); ## Copy the saved data into the object. Iterate over what was saved ## rather than over fieldnames (mdl): a hidden property such as STfun ## is written out by savemodel but is not reported by fieldnames, so ## comparing the two sets could never match. Assignment is legal here ## because this is a method of the class itself. names = fieldnames (data); ## These three are assigned once everything else is in place, and in ## this order rather than the file's. Cost comes before Prior because ## the two set methods re-derive the node statistics, which want the ## node table already loaded. order = {'Cost', 'Prior', 'ScoreTransform'}; late = ismember (names, order); tail = order(ismember (order, names)); names = [names(! late); tail(:)]; for ii = 1:numel (names) try mdl.(names{ii}) = data.(names{ii}); catch msg = 'ClassificationTree.load_model: invalid model in ''%s''.'; error (msg, filename); end_try_catch endfor ## The cut and node descriptions were not saved, being derived mdl = fillCuts (mdl); mdl = deriveNodes (mdl); mdl = deriveHeld (mdl); endfunction endmethods methods (Access = private) ## Re-derive every node statistic that depends on the prior or the cost. ## ClassShare is free of both, so this is all that a reassignment of ## either has to redo, and the shape of the tree is untouched. function this = deriveNodes (this) ## An object part way through being assembled, as load_model assembles ## one property at a time, has a node table and a prior that do not yet ## describe the same model. There is nothing to derive until they do, ## and load_model derives it once they do. if (isempty (this.ClassShare) || isempty (this.Prior) || columns (this.ClassShare) != numel (this.Prior) || rows (this.Cost) != numel (this.Prior)) return; endif S = treeNodeStats (this.ClassShare, this.Prior, this.Cost, ... this.ModelParameters.SplitCriterion, ... this.ClassNames); this.NodeProbability = S.NodeProbability; this.ClassProbability = S.ClassProbability; this.NodeError = S.NodeError; this.NodeClass = S.NodeClass; this.NodeRisk = S.NodeRisk; this.NodeProbAdj = S.NodeProbAdj; endfunction ## The descriptions of the cuts, all of them derived from the node table ## so that they cannot fall out of step with it. function this = fillCuts (this) S = treeCutInfo (this.CutPredictorIndex, this.PredictorNames, ... this.CutCategories); for [val, name] = S this.(name) = val; endfor endfunction ## The branch nodes a pruning level turns into leaves. function nodes = nodesAtLevel (this, L, caller) if (isempty (this.PruneList)) error (strcat ("ClassificationTree.%s: the tree carries no", ... " pruning sequence."), caller); endif maxL = max (this.PruneList); if (L > maxL) warning (strcat ("ClassificationTree.%s: pruning level %d is", ... " greater than the largest level %d; the tree", ... " will be pruned to its root."), caller, L, maxL); L = maxL; endif nodes = find (this.PruneList >= 1 & this.PruneList <= L); endfunction ## Turn the named branch nodes into leaves, discard everything below ## them, and renumber what is left. The surviving nodes keep their ## order, so a parent still precedes its children. function this = collapseNodes (this, nodes) if (isempty (nodes(this.Children(nodes,1) > 0))) return; endif S = treeCollapse (this.Children, this.Parent, ... this.CutPredictorIndex, this.CutPoint, nodes, ... this.CutCategories); this.NumNodes = numel (S.keep); this.Children = S.Children; this.Parent = S.Parent; this.CutPredictorIndex = S.CutPredictorIndex; this.CutPoint = S.CutPoint; this.CutCategories = S.CutCategories; this.NodeSize = this.NodeSize(S.keep); this.ClassCount = this.ClassCount(S.keep,:); this.ClassShare = this.ClassShare(S.keep,:); this.PruneList = zeros (this.NumNodes, 1); this.PruneAlpha = []; this = fillCuts (this); this = deriveNodes (this); ## A node that became a leaf now holds back nothing, so this is ## re-derived rather than subset. this = deriveHeld (this); endfunction ## The risk each branch node carries on account of the observations that ## stop there, missing the predictor it cuts on. Those observations are ## in neither child, and MATLAB charges them the node's own error rather ## than their own. It is charged twice over, once on each of the two ## scales a classification tree measures a node by: the pruning sequence ## runs on the expected misclassification cost and predictorImportance ## on the impurity, and under a non-default cost the weight held back is ## itself a different fraction of the node on the two. Both measured on ## R2024a against an iris fit with a fifth of one predictor missing. function this = deriveHeld (this) this.HeldRisk = zeros (this.NumNodes, 1); this.HeldImpurity = zeros (this.NumNodes, 1); br = find (this.Children(:,1) > 0); if (isempty (br)) return; endif kids = this.Children(br,:); wheld = this.NodeProbability(br) - this.NodeProbability(kids(:,1)) ... - this.NodeProbability(kids(:,2)); this.HeldRisk(br) = wheld .* this.NodeError(br); aheld = this.NodeProbAdj(br) - this.NodeProbAdj(kids(:,1)) ... - this.NodeProbAdj(kids(:,2)); nz = this.NodeProbAdj(br) > 0; this.HeldImpurity(br(nz)) = aheld(nz) .* this.NodeRisk(br(nz)) ... ./ this.NodeProbAdj(br(nz)); endfunction ## Merge back the pairs of leaves that buy nothing, which is what ## MergeLeaves asks for: a pair whose risks together are no less than ## their parent's tells nothing the parent did not. Repeated until ## nothing more merges, so a pair freed by a merge below is caught. function this = mergeLeaves (this) do kid = this.Children; leaf = kid(:,1) == 0; pair = find (! leaf); pair = pair(leaf(kid(pair,1)) & leaf(kid(pair,2))); R = costRisk (this); tol = 1e-12 * R(1); useless = pair(R(kid(pair,1)) + R(kid(pair,2)) ... + this.HeldRisk(pair) >= R(pair) - tol); if (! isempty (useless)) this = collapseNodes (this, useless); endif until (isempty (useless)) endfunction ## The risk a node carries as a leaf: the expected cost of ## misclassifying the observations that reached it, taken over the ## weight they carry rather than their share of the node. It is ## NodeProbability times NodeError, formed in one step because the two ## are a division and a multiplication by the same quantity and the ## merge test below turns on their last digits. function R = costRisk (this) R = min ((this.ClassShare .* this.Prior(:)') * this.Cost, [], 2); endfunction ## The cost complexity pruning sequence, on the risk the cost defines. ## The compiled engine measures risk as misclassified weight, which is ## the expected misclassification cost only under the default cost, and ## it opens a level for a subtree that costs nothing to give up where ## MATLAB opens none, so the sequence is estimated here throughout. function this = pruneSequence (this) [this.PruneList, this.PruneAlpha] = ... __treeprune__ (this.Children, this.Parent, costRisk (this), ... this.HeldRisk); endfunction endmethods endclassdef ## The share of each class's total weight that reached each node. Dividing ## by the root's row cancels whatever scaled the columns, the prior and the ## cost adjustment alike; a class with no weight at all divides by zero and ## is left at zero, having reached nothing. function S = classShareOf (CW) S = zeros (size (CW)); root = CW(1,:); nz = root > 0; S(:,nz) = CW(:,nz) ./ root(nz); endfunction ## Tests %!test # MATLAB parity: the surface a default fit reports %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! assert_equal (class (Mdl), 'ClassificationTree'); %! assert_equal (Mdl.NumObservations, 150); %! assert_equal (Mdl.ClassNames, unique (species)); %! assert_equal (Mdl.Prior, [1/3, 1/3, 1/3], 1e-15); %! assert_equal (Mdl.Cost, [0, 1, 1; 1, 0, 1; 1, 1, 0]); %! assert_equal (Mdl.ResponseName, 'Y'); %! assert_equal (Mdl.PredictorNames, {'x1', 'x2', 'x3', 'x4'}); %! assert_equal (Mdl.ExpandedPredictorNames, {'x1', 'x2', 'x3', 'x4'}); %! assert_equal (Mdl.ScoreTransform, 'none'); %! assert_equal (Mdl.CategoricalPredictors, []); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (Mdl.W, ones (150, 1) / 150, 1e-15); %!test # MATLAB parity: what a tree with no categories and no surrogates holds %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! assert_equal (size (Mdl.CutCategories), [9, 2]); %! assert_equal (all (cellfun (@isempty, Mdl.CutCategories(:))), true); %! assert_equal (size (Mdl.CategoricalSplit), [0, 0]); %! assert_equal (size (Mdl.SurrogateCutPredictor), [0, 1]); %! assert_equal (size (Mdl.SurrogateCutPoint), [0, 0]); %! assert_equal (size (Mdl.SurrogatePredictorAssociation), [0, 0]); %! assert_equal (Mdl.BinEdges, {}); %! assert_equal (Mdl.HyperparameterOptimizationResults, []); %!test # MATLAB parity: the parameters a default fit reports %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! MP = Mdl.ModelParameters; %! assert_equal (MP.SplitCriterion, 'gdi'); %! assert_equal ({MP.MinParent, MP.MinLeaf, MP.MaxSplits}, {10, 1, 149}); %! assert_equal ({MP.MergeLeaves, MP.Prune}, {'on', 'on'}); %! assert_equal ({MP.PruneCriterion, MP.NVarToSample}, {'error', 'all'}); %! assert_equal ({MP.NSurrogate, MP.MaxCat, MP.AlgCat}, {0, 10, 'auto'}); %! assert_equal (MP.PredictorSelection, 'allsplits'); %! assert_equal ({MP.Method, MP.Type}, {'Tree', 'classification'}); %!test # MATLAB parity: the node statistics of the iris tree %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! assert_equal (Mdl.NodeProbability', [150, 50, 100, 54, 46, 48, 6, 47, 1] ... %! / 150, 1e-14); %! assert_equal (Mdl.NodeError', [2/3, 0, 1/2, 5/54, 1/46, 1/48, 1/3, 0, 0], ... %! 1e-14); %! assert_equal (Mdl.NodeRisk(1), 2/3, 1e-14); %! assert_equal (Mdl.NodeRisk(3), 1/3, 1e-14); %! assert_equal (Mdl.ClassProbability(1,:), [1/3, 1/3, 1/3], 1e-14); %! assert_equal (Mdl.ClassProbability(7,:), [0, 1/3, 2/3], 1e-14); %!test # NodeRisk is the impurity weighted by the node probability %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! gdi = 1 - sum (Mdl.ClassProbability .^ 2, 2); %! assert_equal (Mdl.NodeRisk, Mdl.NodeProbability .* gdi, 1e-14); %!test # MATLAB parity: prune takes a subtree out of the sequence %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! sub = prune (Mdl, 'Level', 1); %! assert_equal (sub.NumNodes, 7); %! assert_equal (sub.NodeSize', [150, 50, 100, 54, 46, 48, 6]); %! assert_equal (sub.PruneList', [3, 0, 2, 1, 0, 0, 0]); %! assert_equal (sub.Children, [2, 3; 0, 0; 4, 5; 6, 7; 0, 0; 0, 0; 0, 0]); %! assert_equal (sub.Parent', [0, 1, 1, 3, 3, 4, 4]); %!test # MATLAB parity: a level of two, and level zero changing nothing %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! assert_equal (prune (Mdl, 'Level', 2).NodeSize', [150, 50, 100, 54, 46]); %! assert_equal (prune (Mdl, 'Level', 0).NumNodes, 9); %! assert_equal (prune (Mdl).NumNodes, 9); %!test # MATLAB parity: a cost complexity parameter picks its own level %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! assert_equal (prune (Mdl, 'Alpha', 0.1).NumNodes, 5); %! assert_equal (prune (Mdl, 'Alpha', 0).NumNodes, 9); %! assert_equal (prune (Mdl, 'Alpha', 1).NumNodes, 1); %!test # Pruning named nodes turns just those into leaves %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! sub = prune (Mdl, 'Nodes', 3); %! assert_equal (sub.NumNodes, 3); %! assert_equal (sub.NodeSize', [150, 50, 100]); %! assert_equal (sub.IsBranchNode', [true, false, false]); %!test # The pruned tree re-derives its own sequence, and it is the shifted one %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! sub = prune (Mdl, 'Level', 1); %! assert_equal (sub.PruneAlpha', [0, 2/150, 44/150, 50/150], 1e-14); %!warning %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! sub = prune (Mdl, 'Level', 9); %!test # MATLAB parity: the four outputs of predict, and a missing predictor %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! x = meas([1, 60, 120], :); %! x(1, 3) = NaN; %! [label, score, node, cnum] = predict (Mdl, x); %! assert_equal (node', [1, 8, 7]); %! assert_equal (label, {'setosa'; 'versicolor'; 'virginica'}); %! assert_equal (cnum', [1, 2, 3]); %! assert_equal (score(1,:), [1/3, 1/3, 1/3], 1e-14); %!test # MATLAB parity: a row missing every predictor stops at the root %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! [label, score, node] = predict (Mdl, NaN (1, 4)); %! assert_equal (node, 1); %! assert_equal (label, {'setosa'}); %! assert_equal (score, [1/3, 1/3, 1/3], 1e-14); %!test # MATLAB parity: the score transform is applied to the probabilities %! load fisheriris %! Mdl = ClassificationTree (meas, species, 'ScoreTransform', 'logit'); %! [~, score] = predict (Mdl, meas([1, 60, 120], :)); %! assert_equal (Mdl.ScoreTransform, 'logit'); %! assert_equal (score(1,:), [0.731058578630005, 0.5, 0.5], 1e-14); %!test # MATLAB parity: predictorImportance divides by the branch node count %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! imp = predictorImportance (Mdl); %! assert_equal (size (imp), [1, 4]); %! assert_equal (imp(1:2), [0, 0]); %! assert_equal (imp(3:4), [0.0907, 0.0682], 1e-4); %!test # MATLAB parity: the range of each predictor on the path to a node %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! assert_equal (fieldnames (nodeVariableRange (Mdl, 1)), cell (0, 1)); %! r = nodeVariableRange (Mdl, 8); %! assert_equal (r.x3, [2.45, 4.95], 1e-14); %! assert_equal (r.x4, [-Inf, 1.65], 1e-14); %!test # MATLAB parity: the text form of the tree %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! txt = evalc ('view (Mdl)'); %! lines = strsplit (strtrim (txt), "\n"); %! assert_equal (numel (lines), 10); %! assert_equal (lines{1}, 'Decision tree for classification'); %! assert_equal (lines{2}, ... %! '1 if x3<2.45 then node 2 elseif x3>=2.45 then node 3 else setosa'); %! assert_equal (lines{3}, '2 class = setosa'); %!test # MATLAB parity: the seven classification losses on the training data %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! assert_equal (resubLoss (Mdl, 'LossFun', 'binodeviance'), 0.1388, 1e-4); %! assert_equal (resubLoss (Mdl, 'LossFun', 'classiferror'), 0.02, 1e-14); %! assert_equal (resubLoss (Mdl, 'LossFun', 'exponential'), 0.3829, 1e-4); %! assert_equal (resubLoss (Mdl, 'LossFun', 'hinge'), 0.0308, 1e-4); %! assert_equal (resubLoss (Mdl, 'LossFun', 'logit'), 0.3232, 1e-4); %! assert_equal (resubLoss (Mdl, 'LossFun', 'mincost'), 0.02, 1e-14); %! assert_equal (resubLoss (Mdl, 'LossFun', 'quadratic'), 0.0154, 1e-4); %!test # MATLAB parity: margin and edge %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! m = margin (Mdl, meas([1, 60, 120], :), species([1, 60, 120])); %! assert_equal (m', [1, 1, 1/3], 1e-14); %! assert_equal (size (resubMargin (Mdl)), [150, 1]); %! assert_equal (resubEdge (Mdl), 0.9384, 1e-4); %! assert_equal (edge (Mdl, meas, species), resubEdge (Mdl), 1e-14); %!test # resubPredict answers exactly as predict on the training data %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! [l1, s1, n1, c1] = resubPredict (Mdl); %! [l2, s2, n2, c2] = predict (Mdl, meas); %! assert_equal ({l1, s1, n1, c1}, {l2, s2, n2, c2}); %!test # MATLAB parity: a cost matrix reaches the label and the risk %! load fisheriris %! Mdl = ClassificationTree (meas, species, ... %! 'Cost', [0, 2, 8; 3, 0, 1; 5, 4, 0]); %! assert_equal (Mdl.NodeSize', [150, 50, 100, 45, 55, 44, 1]); %! assert_equal (Mdl.NodeClass{1}, 'versicolor'); %! assert_equal (Mdl.NodeError(1), 2, 1e-14); %! assert_equal (Mdl.NodeRisk(1), 0.6276, 1e-4); %! assert_equal (Mdl.PruneAlpha', [0, 0.0267, 0.2667, 1.6667], 1e-4); %!test # Reassigning Cost re-derives the node statistics, not the tree %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! shape = {Mdl.Children, Mdl.NodeSize, Mdl.ClassCount}; %! Mdl.Cost = [0, 2, 8; 3, 0, 1; 5, 4, 0]; %! assert_equal ({Mdl.Children, Mdl.NodeSize, Mdl.ClassCount}, shape); %! assert_equal (Mdl.NodeClass{1}, 'versicolor'); %! assert_equal (Mdl.NodeError(1), 2, 1e-14); %!test # Reassigning Prior re-derives the weights and the node statistics %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! Mdl.Prior = [0.5, 0.25, 0.25]; %! assert_equal (Mdl.Prior, [0.5, 0.25, 0.25], 1e-14); %! assert_equal (Mdl.W([1, 51, 101])', [0.01, 0.005, 0.005], 1e-14); %! assert_equal (Mdl.ClassProbability(1,:), [0.5, 0.25, 0.25], 1e-14); %! assert_equal (Mdl.NodeProbability(2), 0.5, 1e-14); %! assert_equal (Mdl.NodeRisk(1), 0.625, 1e-14); %!test # A prior given as a structure names its own class order %! load fisheriris %! p.ClassNames = {'virginica'; 'versicolor'; 'setosa'}; %! p.ClassProbs = [0.25, 0.25, 0.5]; %! Mdl = ClassificationTree (meas, species, 'Prior', p); %! assert_equal (Mdl.Prior, [0.5, 0.25, 0.25], 1e-14); %!test # A structure prior may name its classes in a character matrix %! load fisheriris %! p.ClassNames = char ({'virginica'; 'versicolor'; 'setosa'}); %! p.ClassProbs = [0.25, 0.25, 0.5]; %! Mdl = ClassificationTree (meas, char (species), 'Prior', p); %! assert_equal (Mdl.Prior, [0.5, 0.25, 0.25], 1e-14); %!test # A cost given as a structure names its own class order %! load fisheriris %! c.ClassNames = {'virginica'; 'versicolor'; 'setosa'}; %! c.ClassificationCosts = [0, 1, 10; 1, 0, 1; 10, 1, 0]; %! Mdl = ClassificationTree (meas, species, 'Cost', c); %! assert_equal (Mdl.Cost, [0, 1, 10; 1, 0, 1; 10, 1, 0]); %! assert_equal (Mdl.NodeSize', [150, 50, 100, 45, 55, 44, 1, 9, 46]); %!test # A response of one class alone gives a tree of one node %! load fisheriris %! Mdl = ClassificationTree (meas(1:5, :), species(1:5)); %! assert_equal (Mdl.NumNodes, 1); %! assert_equal (Mdl.NodeClass, {'setosa'}); %! assert_equal (Mdl.ClassProbability, 1); %! assert_equal (predict (Mdl, meas(1, :)), {'setosa'}); %!test # A model saved and loaded answers exactly as it did %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! fname = tempname (); %! unwind_protect %! savemodel (Mdl, fname); %! New = loadmodel (fname); %! assert_equal (class (New), 'ClassificationTree'); %! assert_equal (New.NumNodes, Mdl.NumNodes); %! assert_equal (New.NodeRisk, Mdl.NodeRisk, 1e-15); %! assert_equal (New.CutPredictor, Mdl.CutPredictor); %! assert_equal (predict (New, meas), predict (Mdl, meas)); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect %!test # MATLAB parity: an unmerged tree records no level for a free subtree %! ## With MergeLeaves off, the pair of leaves that merging would have %! ## removed survives, and giving it up costs nothing. MATLAB records %! ## neither a level nor an alpha for such a subtree, so the sequence is %! ## the merged tree's, over eleven nodes instead of nine. %! load fisheriris %! Mdl = ClassificationTree (meas, species, 'MergeLeaves', 'off'); %! assert_equal (Mdl.NumNodes, 11); %! assert_equal (Mdl.PruneList', [4, 0, 3, 2, 0, 1, 0, 0, 0, 0, 0]); %! assert_equal (Mdl.PruneAlpha', [0, 1/150, 2/150, 44/150, 50/150], 1e-14); %!test # MATLAB parity: a deeper tree, every branch pruned at its own level %! load fisheriris %! Mdl = ClassificationTree (meas, species, 'MinParentSize', 2); %! assert_equal (Mdl.NumNodes, 17); %! assert_equal (Mdl.PruneList', [5, 0, 4, 3, 1, 2, 2, 1, 0, 0, 0, 0, 2, ... %! 0, 0, 0, 0]); %! assert_equal (Mdl.PruneAlpha', [0, 0.5, 1, 2, 44, 50] / 150, 1e-14); %! assert_equal (predictorImportance (Mdl), ... %! [0.00222222222222222, 0, 0.0458935520665593, ... %! 0.0352175590445518], 1e-14); %!test # MATLAB parity: the resubstitution methods weigh as the fit weighed %! load fisheriris %! Mdl = ClassificationTree (meas, species, 'Weights', (1:150)'); %! assert_equal (resubLoss (Mdl), 0.0384988962472406, 1e-14); %! assert_equal (resubEdge (Mdl), 0.856171294940654, 1e-14); %! ## loss and edge over the same data weigh it uniformly unless told not to %! assert_equal (loss (Mdl, meas, species), 0.04, 1e-14); %! assert_equal (edge (Mdl, meas, species), 0.853582991377224, 1e-14); %! assert_equal (loss (Mdl, meas, species, 'Weights', (1:150)'), ... %! 0.0384988962472406, 1e-14); %!test # MATLAB parity: edge on a set missing a class %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! r = 51:150; %! assert_equal (edge (Mdl, meas(r,:), species(r)), 0.9075362319, 1e-10); %! assert_equal (edge (Mdl, meas(r,:), species(r), 'Weights', (1:100)'), ... %! 0.9037275415, 1e-10); %!test # MATLAB parity: the tree a weighted fit grows %! load fisheriris %! Mdl = ClassificationTree (meas, species, 'Weights', (1:150)'); %! assert_equal (Mdl.NodeSize', [150, 95, 55, 50, 45, 44, 1]); %! assert_equal (Mdl.NodeProbability', [1, 0.416865342163355, ... %! 0.583134657836645, ... %! 0.112582781456954, ... %! 0.304282560706402, ... %! 0.294834437086093, ... %! 0.00944812362030905], 1e-14); %! assert_equal (Mdl.NodeRisk(1), 0.569205054359214, 1e-14); %! assert_equal (Mdl.PruneAlpha', [0, 0.00944812362030906, ... %! 0.112582781456954, 0.285386313465784], ... %! 1e-14); %!test # MATLAB parity: the resubstitution edge under a cost matrix %! load fisheriris %! Mdl = ClassificationTree (meas, species, ... %! 'Cost', [0, 1, 10; 1, 0, 1; 10, 1, 0]); %! assert_equal (resubLoss (Mdl), 1/30, 1e-14); %! assert_equal (resubEdge (Mdl), 0.914653784219002, 1e-14); %!test # MATLAB parity: compact drops the data and answers identically %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! CMdl = compact (Mdl); %! assert_equal (class (CMdl), 'CompactClassificationTree'); %! assert_equal (numel (properties (CMdl)), 33); %! assert_equal (predict (CMdl, meas), predict (Mdl, meas)); %! assert_equal (CMdl.NodeRisk, Mdl.NodeRisk, 1e-15); %!test # MATLAB parity: crossval returns a partitioned model over compacts %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! CVMdl = crossval (Mdl); %! assert_equal (class (CVMdl), 'ClassificationPartitionedModel'); %! assert_equal (CVMdl.CrossValidatedModel, 'Tree'); %! assert_equal (class (CVMdl.Trained{1}), 'CompactClassificationTree'); %! assert_equal (CVMdl.KFold, 10); %! assert_equal (CVMdl.NumObservations, 150); %! assert_equal (CVMdl.Prior, Mdl.Prior, 1e-15); %! assert_equal (CVMdl.Cost, Mdl.Cost); %!test # Each way of asking for a partition gives the folds it names %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! assert_equal (crossval (Mdl, 'KFold', 5).KFold, 5); %! assert_equal (crossval (Mdl, 'Holdout', 0.3).KFold, 1); %! assert_equal (crossval (Mdl, 'Leaveout', 'on').KFold, 150); %! assert_equal (crossval (Mdl, 'CVPartition', ... %! cvpartition (species, 'KFold', 4)).KFold, 4); %!test # A fold is grown with the parent's prior, cost and weights %! load fisheriris %! Mdl = ClassificationTree (meas, species, 'Weights', (1:150)', ... %! 'Cost', [0, 1, 10; 1, 0, 1; 10, 1, 0]); %! CVMdl = crossval (Mdl, 'KFold', 3); %! assert_equal (CVMdl.Trained{1}.Prior, Mdl.Prior, 1e-15); %! assert_equal (CVMdl.Trained{1}.Cost, Mdl.Cost); %! assert_equal (CVMdl.Trained{1}.ClassNames, Mdl.ClassNames); %!test # A fold is grown with the parameters the parent was grown with %! load fisheriris %! Mdl = ClassificationTree (meas, species, 'SplitCriterion', 'deviance', ... %! 'MinLeafSize', 7, 'MergeLeaves', 'off'); %! CVMdl = crossval (Mdl, 'KFold', 3); %! assert_equal (CVMdl.ModelParameters.SplitCriterion, 'deviance'); %! assert_equal (CVMdl.ModelParameters.MinLeaf, 7); %! assert_equal (CVMdl.ModelParameters.MergeLeaves, 'off'); %!test # kfoldPredict scores every observation and costs them from the score %! load fisheriris %! CVMdl = crossval (ClassificationTree (meas, species), 'KFold', 5); %! [label, score, cost] = kfoldPredict (CVMdl); %! assert_equal (size (label), [150, 1]); %! assert_equal (size (score), [150, 3]); %! assert_equal (cost, score * CVMdl.Cost, 1e-14); %! assert_equal (sum (score, 2), ones (150, 1), 1e-14); %! assert_equal (kfoldLoss (CVMdl) < 0.2, true); %! assert_equal (size (kfoldMargin (CVMdl)), [150, 1]); %!test # cvloss over a fixture whose folds all answer alike %! ## The two classes are separated by a gap no fold can straddle, so every %! ## fold grows the same tree and every quantity below is the same whatever %! ## partition is drawn. %! x = [(1:20)'; (101:120)']; %! y = [repmat({'a'}, 20, 1); repmat({'b'}, 20, 1)]; %! Mdl = ClassificationTree (x, y); %! assert_equal (Mdl.NumNodes, 3); %! [E, SE, Nleaf, BestLevel] = cvloss (Mdl, 'SubTrees', 'all'); %! assert_equal (E', [0, 0.5], 1e-14); %! assert_equal (SE', [0, 0], 1e-14); %! assert_equal (Nleaf', [2, 1]); %! assert_equal (BestLevel, 0); %!test # MATLAB parity: the leaf counts and the stable loss of the iris tree %! ## Only the last level's loss is asserted. Its parameter is infinite, so %! ## every fold's tree goes back to its root whatever the partition, and a %! ## root under a uniform prior answers the first class, which two thirds %! ## of the data are not. The levels below it are not fixed: a fold's tree %! ## is matched to a subtree of the whole by a complexity parameter, and %! ## which of its own levels that picks out depends on the rows it saw. %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! [E, SE, Nleaf, BestLevel] = cvloss (Mdl, 'SubTrees', 'all'); %! assert_equal (size (E), [5, 1]); %! assert_equal (Nleaf', [5, 4, 3, 2, 1]); %! assert_equal (E(5), 2/3, 1e-14); %! assert_equal (SE(5), 0, 1e-12); %! assert_equal (BestLevel >= 0 && BestLevel <= 4, true); %!test # cvloss defaults to the unpruned tree alone %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! [E, SE, Nleaf, BestLevel] = cvloss (Mdl); %! assert_equal (size (E), [1, 1]); %! assert_equal (Nleaf, 5); %! assert_equal (BestLevel, 0); %! assert_equal (E > 0 && E < 0.2, true); %!test # A subset of the levels is answered in the order it was asked for %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! [E, SE, Nleaf, BestLevel] = cvloss (Mdl, 'SubTrees', [0, 2, 4]); %! assert_equal (Nleaf', [5, 3, 1]); %! assert_equal (E(3), 2/3, 1e-14); %! assert_equal (any (BestLevel == [0, 2, 4]), true); %!test # TreeSize picks the smallest tree the rule allows %! ## 'min' takes the smallest tree of least loss and 'se' the smallest whose %! ## loss is within one standard error of it, so 'se' never names a larger %! ## tree than 'min' does. %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! [E, SE] = cvloss (Mdl, 'SubTrees', 'all'); %! [~, ~, ~, bmin] = cvloss (Mdl, 'SubTrees', 'all', 'TreeSize', 'min'); %! [~, ~, ~, bse] = cvloss (Mdl, 'SubTrees', 'all', 'TreeSize', 'se'); %! assert_equal (bmin >= 0 && bmin <= 4, true); %! assert_equal (bse >= 0 && bse <= 4, true); %!test # The number of folds is the number asked for %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! [E, SE, Nleaf] = cvloss (Mdl, 'KFold', 5); %! assert_equal (size (E), [1, 1]); %! assert_equal (Nleaf, 5); %! assert_equal (E > 0 && E < 0.2, true); %!test # cvloss weighs the loss as the fit weighed it %! ## A weighted fit whose folds all answer alike. The three to one weights %! ## make the prior 0.25 and 0.75, so every fold's root answers the heavier %! ## class and the twenty rows of the lighter one are wrong. Their weight %! ## is 0.25, where their share of the rows is 0.5. %! x = [(1:20)'; (101:120)']; %! y = [repmat({'a'}, 20, 1); repmat({'b'}, 20, 1)]; %! Mdl = ClassificationTree (x, y, 'Weights', [ones(20, 1); 3 * ones(20, 1)]); %! assert_equal (Mdl.Prior, [0.25, 0.75], 1e-14); %! E = cvloss (Mdl, 'SubTrees', 'all'); %! assert_equal (E(1), 0, 1e-14); %! assert_equal (E(2), 0.25, 1e-14); %!test # More folds than observations are reduced to one fold each %! ## The reduction warns, and so does cvpartition about a fold that cannot %! ## hold every class, so the message is not what is asserted here; the %! ## answer is, and it is the one leaving each observation out in turn. %! x = [(1:20)'; (101:120)']; %! y = [repmat({'a'}, 20, 1); repmat({'b'}, 20, 1)]; %! Mdl = ClassificationTree (x, y); %! ws = warning ('off', 'all'); %! unwind_protect %! E = cvloss (Mdl, 'KFold', 500, 'SubTrees', 'all'); %! unwind_protect_cleanup %! warning (ws); %! end_unwind_protect %! assert_equal (E', [0, 0.5], 1e-14); %!test # MATLAB parity: a node that holds rows back pays for them %! ## Twenty rows have no fourth predictor, and the node that cuts on it %! ## sends them to neither child. They are in no child's risk, so a %! ## subtree's risk is its children's plus what the node holds back, and %! ## leaving that out would overstate every link above it. %! load fisheriris %! x = meas; %! x(51:70, 4) = NaN; %! Mdl = ClassificationTree (x, species); %! assert_equal (Mdl.NumNodes, 9); %! assert_equal (Mdl.NodeSize', [150, 50, 100, 45, 55, 25, 1, 8, 46]); %! assert_equal (Mdl.NodeSize(4) - sum (Mdl.NodeSize(Mdl.Children(4,:))), 19); %! assert_equal (Mdl.PruneList', [4, 0, 3, 1, 2, 0, 0, 0, 0]); %! assert_equal (Mdl.PruneAlpha', [0, 0.00385185185185185, ... %! 0.00593939393939394, 0.286666666666666, ... %! 0.333333333333333], 1e-14); %! assert_equal (predictorImportance (Mdl), ... %! [0, 0, 0.145589225589225, 0.00944980621616545], 1e-14); %! assert_equal (resubLoss (Mdl), 0.04, 1e-14); %!test # MATLAB parity: held-back rows under a cost matrix %! ## The weight a node holds back is a different fraction of it on the two %! ## scales a classification tree is measured by, so the pruning sequence %! ## and the predictor importances discount different amounts. %! load fisheriris %! x = meas; %! x(51:80, 3) = NaN; %! Mdl = ClassificationTree (x, species, ... %! 'Cost', [0, 1, 10; 1, 0, 1; 10, 1, 0]); %! assert_equal (Mdl.NumNodes, 7); %! assert_equal (Mdl.NodeSize', [150, 50, 70, 15, 55, 5, 50]); %! assert_equal (Mdl.NodeSize(1) - sum (Mdl.NodeSize(Mdl.Children(1,:))), 30); %! assert_equal (Mdl.PruneList', [3, 0, 2, 0, 1, 0, 0]); %! assert_equal (Mdl.PruneAlpha', [0, 0.02, 0.1, 0.4], 1e-14); %! assert_equal (predictorImportance (Mdl), ... %! [0, 0, 0.17774350820443, 0], 1e-14); %!test # MATLAB parity: held-back rows with the leaves left unmerged %! load fisheriris %! x = meas; %! x(51:70, 4) = NaN; %! Mdl = ClassificationTree (x, species, 'MergeLeaves', 'off'); %! assert_equal (Mdl.NumNodes, 11); %! assert_equal (Mdl.NodeSize', [150, 50, 100, 45, 55, 25, 1, 8, 46, 3, 43]); %! assert_equal (Mdl.PruneList', [4, 0, 3, 1, 2, 0, 0, 0, 0, 0, 0]); %! assert_equal (Mdl.PruneAlpha', [0, 0.00385185185185185, ... %! 0.00593939393939394, 0.286666666666666, ... %! 0.333333333333333], 1e-14); %!test # A categorical 'ClassNames' keeps only the classes it names %! load fisheriris %! Mdl = ClassificationTree (meas, categorical (species), 'ClassNames', ... %! categorical ({'versicolor'; 'virginica'})); %! assert_equal (Mdl.NumObservations, 100); %! assert_equal (class (Mdl.ClassNames), 'categorical'); ## Test input validation ## MATLAB parity: a categorical response keeps only the categories it holds. %!test %! load fisheriris %! Mdl = fitctree (meas(51:150,:), categorical (species(51:150))); %! assert_equal (categories (Mdl.ClassNames), {'versicolor'; 'virginica'}); %! assert_equal (categories (predict (Mdl, meas(51,:))), ... %! {'versicolor'; 'virginica'}); %!error ClassificationTree () %!error %! ClassificationTree (ones (4, 2)) %!error %! ClassificationTree (ones (4, 2), ones (4, 1), 'K') %!error %! ClassificationTree ('a', ones (4, 1)) %!error %! ClassificationTree (ones (4, 2), ones (3, 1)) %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'PredictorNames', 'a') %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'PredictorNames', {'a'}) %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'ResponseName', 5) %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'ClassNames', {1}) %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'ClassNames', 5) %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'Prior', {1}) %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'Prior', 'bogus') %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'Cost', ones (2, 3)) %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'Weights', 'a') %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'Weights', ones (2, 2)) %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'Weights', [1, 2, 3]) %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'Weights', -ones (4, 1)) %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'MaxNumSplits', -1) %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'MinLeafSize', 0) %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'MinParentSize', 0) %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'MergeLeaves', 'x') %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'Prune', 'x') %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'PruneCriterion', 'x') %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'PruneCriterion', 'impurity') %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'SplitCriterion', 'x') %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'SplitCriterion', 'twoing') %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'Surrogate', 'on') %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'KFold', 5) %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'Bogus', 1) %!error %! predict (ClassificationTree (ones (4, 2), [1; 1; 2; 2])) %!error %! predict (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), []) %!error %! predict (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), 'a') %!error %! predict (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), ones (2, 3)) %!error %! margin (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), ones (4, 2)) %!error %! margin (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), ones (4, 2), ... %! [1; 1; 2; 9]) %!error %! margin (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), ones (3, 2), ... %! [1; 1; 2; 2]) %!error %! edge (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), ones (4, 2)) %!error ... %! edge (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), ones (4, 2), ... %! [1; 1; 2; 2], 'Weights') %!error ... %! edge (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), ones (4, 2), ... %! [1; 1; 2; 2], 'Bogus', 1) %!error %! loss (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), ones (4, 2)) %!error %! loss (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), ones (4, 2), ... %! [1; 1; 2; 2], 'LossFun') %!error %! loss (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), ones (4, 2), ... %! [1; 1; 2; 2], 'LossFun', 'x') %!error %! loss (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), ones (4, 2), ... %! [1; 1; 2; 2], 'Weights', 'a') %!error %! loss (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), ones (4, 2), ... %! [1; 1; 2; 2], 'Weights', ones (2, 2)) %!error %! loss (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), ones (4, 2), ... %! [1; 1; 2; 2], 'Bogus', 1) %!error %! prune (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), 'Level') %!error %! prune (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), 'Level', 1, ... %! 'Alpha', 1) %!error %! prune (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), 'Level', -1) %!error %! prune (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), 'Alpha', -1) %!error %! prune (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), 'Nodes', 99) %!error %! prune (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), 'Bogus', 1) %!error %! nodeVariableRange (ClassificationTree (ones (4, 2), [1; 1; 2; 2])) %!error %! nodeVariableRange (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), 99) %!error %! savemodel (ClassificationTree (ones (4, 2), [1; 1; 2; 2])) %!error %! savemodel (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), 5) %!error %! cvloss (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), 'SubTrees') %!error %! load fisheriris %! cvloss (ClassificationTree (meas, species), 'SubTrees', -1) %!error %! load fisheriris %! cvloss (ClassificationTree (meas, species), 'SubTrees', [2, 1]) %!error %! load fisheriris %! cvloss (ClassificationTree (meas, species), 'SubTrees', 'most') %!error %! load fisheriris %! cvloss (ClassificationTree (meas, species), 'SubTrees', 99) %!error %! load fisheriris %! cvloss (ClassificationTree (meas, species), 'TreeSize', 'x') %!error %! load fisheriris %! cvloss (ClassificationTree (meas, species), 'KFold', 1) %!error %! load fisheriris %! cvloss (ClassificationTree (meas, species), 'Bogus', 1) %!error %! load fisheriris %! cvloss (ClassificationTree (meas, species, 'Prune', 'off', ... %! 'MergeLeaves', 'off')) %!error %! crossval (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), 'KFold') %!error %! crossval (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), 'KFold', 2, ... %! 'Holdout', 0.3) %!error %! crossval (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), 'KFold', 1) %!error %! crossval (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), 'Holdout', 2) %!error %! crossval (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), 'Leaveout', 'x') %!error %! crossval (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), 'CVPartition', 5) %!error %! crossval (ClassificationTree (ones (4, 2), [1; 1; 2; 2]), 'Bogus', 1) %!error %! Mdl = ClassificationTree (ones (4, 2), [1; 1; 2; 2]); %! Mdl.Prior = [0.2, 0.3, 0.5]; %!error %! Mdl = ClassificationTree (ones (4, 2), [1; 1; 2; 2]); %! Mdl.Prior = 'bogus'; %!error %! Mdl = ClassificationTree (ones (4, 2), [1; 1; 2; 2]); %! Mdl.Prior = [0, 0]; %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'NumVariablesToSample', 0) %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'NumVariablesToSample', 1.5) %!error %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], ... %! 'NumVariablesToSample', 'some') %!shared Xb, yb, X3, y3 %! k = (0:79)'; %! c = mod (k, 4) + 1; %! j = floor (k / 4); %! Xb = [c, mod(k * 7, 10)]; %! yb = (c == 1) | (c == 2 & mod (j, 4) != 0) | (c == 3 & mod (j, 4) == 0); %! k = (0:119)'; %! c = mod (k, 6) + 1; %! base = [1, 2, 3, 1, 2, 3]; %! alt = [1, 2, 3, 2, 3, 1]; %! y3 = base(c)'; %! odd = mod (floor (k / 6), 2) == 1 & c >= 4; %! y3(odd) = alt(c(odd)); %! X3 = [c, mod(k * 5, 7)]; %!test # MATLAB parity: a categorical predictor splits into sets of levels %! Mdl = ClassificationTree (Xb, yb, 'CategoricalPredictors', 1); %! assert_equal (Mdl.NumNodes, 3); %! assert_equal (Mdl.CategoricalPredictors, 1); %! assert_equal (Mdl.CutType, {'categorical'; ''; ''}); %! assert_equal (Mdl.CutCategories(1,:), {[1, 2], [3, 4]}); %! assert_equal (Mdl.CategoricalSplit, {[1, 2], [3, 4]}); %! assert_equal (isnan (Mdl.CutPoint(1)), true); %! assert_equal (predictorImportance (Mdl), [0.28125, 0], 1e-12); %!test # a categorical predictor may be named in any of the three containers %! pn = {'grp', 'val'}; %! M1 = ClassificationTree (Xb, yb, 'PredictorNames', pn, ... %! 'CategoricalPredictors', {'grp'}); %! M2 = ClassificationTree (Xb, yb, 'PredictorNames', pn, ... %! 'CategoricalPredictors', string ({'grp'})); %! M3 = ClassificationTree (Xb, yb, 'PredictorNames', pn, ... %! 'CategoricalPredictors', 'grp'); %! assert_equal (M1.CategoricalPredictors, 1); %! assert_equal (M2.CategoricalPredictors, 1); %! assert_equal (M3.CategoricalPredictors, 1); %!test # a character matrix names one predictor per row, its padding stripped %! M = ClassificationTree (Xb, yb, 'PredictorNames', {'g', 'val'}, ... %! 'CategoricalPredictors', ['g '; 'val']); %! assert_equal (M.CategoricalPredictors, [1, 2]); %!test # 'all' is every predictor even where a predictor is named 'all' %! M = ClassificationTree (Xb, yb, 'PredictorNames', {'all', 'val'}, ... %! 'CategoricalPredictors', 'all'); %! assert_equal (M.CategoricalPredictors, [1, 2]); %!test # a repeated name gives one index, where MATLAB repeats it %! M = ClassificationTree (Xb, yb, 'PredictorNames', {'grp', 'val'}, ... %! 'CategoricalPredictors', {'grp', 'grp'}); %! assert_equal (M.CategoricalPredictors, 1); %!error ... %! ClassificationTree (Xb, yb, 'PredictorNames', {'grp', 'val'}, ... %! 'CategoricalPredictors', {'GRP'}) %!error ... %! ClassificationTree (Xb, yb, 'PredictorNames', {'grp', 'val'}, ... %! 'CategoricalPredictors', {'grp', 2}) %!test # MATLAB parity: a level a node did not see stops the row there %! Mdl = ClassificationTree (Xb, yb, 'CategoricalPredictors', 1); %! [~, s, nd] = predict (Mdl, [1, 0; 3, 0; 5, 0; NaN, 0; 2.5, 0]); %! assert_equal (nd', [2, 3, 1, 1, 1]); %! assert_equal (s(3,:), [0.5, 0.5], 1e-12); %! assert_equal (s(1,:), [0.125, 0.875], 1e-12); %!test # MATLAB parity: three classes search every partition of the levels %! Mdl = ClassificationTree (X3, y3, 'CategoricalPredictors', 1); %! assert_equal (Mdl.NumNodes, 25); %! assert_equal (Mdl.CutCategories(1,:), {[1, 4, 6], [2, 3, 5]}); %! assert_equal (size (Mdl.CategoricalSplit), [5, 2]); %! assert_equal (nodeVariableRange (Mdl, 2), struct ('x1', [1, 4, 6])); %! assert_equal (Mdl.ModelParameters.MaxCat, 10); %! assert_equal (Mdl.ModelParameters.AlgCat, 'auto'); %!test # MATLAB parity: view prints a categorical cut as sets of levels %! Mdl = ClassificationTree (X3, y3, 'CategoricalPredictors', 1); %! txt = evalc ('view (Mdl)'); %! assert_equal (! isempty (strfind (txt, [' 1 if x1 in {1 4 6} then node', ... %! ' 2 elseif x1 in {2 3 5} then node 3 else 1'])), true); %!test # MATLAB parity: the categorical options are recorded as given %! Mdl = ClassificationTree (X3, y3, 'CategoricalPredictors', 1, ... %! 'MaxNumCategories', 4, ... %! 'AlgorithmForCategorical', 'PCA'); %! assert_equal (Mdl.ModelParameters.MaxCat, 4); %! assert_equal (Mdl.ModelParameters.AlgCat, 'PCA'); %!test # MATLAB parity: pruning keeps the categorical splits that remain %! Mdl = ClassificationTree (X3, y3, 'CategoricalPredictors', 1); %! P = prune (Mdl, 'Level', 1); %! assert_equal (P.NumNodes, 13); %! assert_equal (rows (P.CategoricalSplit), 3); %!test # the level sets travel with a saved and a compact tree %! Mdl = ClassificationTree (X3, y3, 'CategoricalPredictors', 1); %! fname = [tempname(), '.mdl']; %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.CutCategories, Mdl.CutCategories); %! assert_equal (predict (compact (Mdl), X3), predict (Mdl, X3)); %!test # cross-validation grows every fold with the categorical predictors %! Mdl = ClassificationTree (X3, y3, 'CategoricalPredictors', 1); %! CV = crossval (Mdl, 'KFold', 3); %! assert_equal (CV.Trained{1}.CategoricalPredictors, 1); %!error ... %! ClassificationTree (Xb, yb, 'CategoricalPredictors', 3) %!error ... %! ClassificationTree (Xb, yb, 'MaxNumCategories', -1) %!error ... %! ClassificationTree (Xb, yb, 'AlgorithmForCategorical', 'bogus') ## A table at prediction %!shared ctT, ctM %! load fisheriris %! ctT = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! ctT.Species = categorical (species); %! ctT.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! ctM = fitctree (ctT, 'Species'); %!test # the class may be built from a table as the fitter builds it %! Mdl = ClassificationTree (ctT, 'Species'); %! assert_equal (Mdl.PredictorNames, ctM.PredictorNames); %! assert_equal (Mdl.CategoricalPredictors, ctM.CategoricalPredictors); %!test # predict takes a table and answers in the type the labels came in %! label = predict (ctM, ctT); %! assert_equal (class (label), 'categorical'); %! assert_equal (numel (label), 150); ## MATLAB parity: a table is read by name, so the order of its columns does ## not matter and a column the model was not fitted on is passed over %!test %! a = predict (ctM, ctT); %! b = predict (ctM, ctT(:, [6, 5, 4, 3, 2, 1])); %! assert_equal (a, b); %! T = ctT; %! T.Extra = (1:150)'; %! assert_equal (predict (ctM, T), a); %!test # a level carries at prediction the code it carried at fitting %! T = ctT(1:60,:); %! T.Wide = categorical (repmat ({'wide'}, 60, 1), {'narrow', 'wide'}); %! assert_equal (numel (predict (ctM, T)), 60); %!test # a matrix is still taken, as the model was fitted from one before %! load fisheriris %! Mdl = fitctree (meas, species); %! assert_equal (numel (predict (Mdl, meas)), 150); %!test # the levels travel with the model when it is made compact %! CMdl = compact (ctM); %! assert_equal (CMdl.PredictorLevels, ctM.PredictorLevels); %! assert_equal (predict (CMdl, ctT), predict (ctM, ctT)); %!error ... %! predict (ctM, ctT(:, [1, 3, 4, 5, 6])) %!test # an ordinal column is numeric, as measured on R2024a %! o = categorical (repmat ({'lo'; 'hi'; 'lo'; 'mid'; 'hi'; 'lo'; 'mid'; ... %! 'hi'; 'lo'; 'mid'; 'hi'; 'lo'}, 2, 1), ... %! {'lo', 'mid', 'hi'}, 'Ordinal', true); %! lab = repmat ({'p'; 'q'; 'q'; 'p'; 'p'; 'q'; 'q'; 'p'; 'p'; 'q'; 'q'; ... %! 'p'}, 2, 1); %! lab(double (o) == 3) = {'q'}; %! Mdl = fitctree (table (o, lab), 'lab'); %! assert_equal (Mdl.CategoricalPredictors, []); %! assert_equal (Mdl.CutPoint', [2.5, 1.5, NaN, NaN, NaN]); %! N = table (categorical ({'lo'; 'mid'; 'hi'; ''}, {'lo', 'mid', 'hi'}, ... %! 'Ordinal', true), 'VariableNames', {'o'}); %! assert_equal (predict (Mdl, N), {'p'; 'q'; 'q'; 'q'}); %!test # a missing value and an unseen level predict as missing, as R2024a %! k = categorical (repmat ({'a'; 'b'}, 6, 1)); %! lab = repmat ({'p'; 'q'}, 6, 1); %! Mdl = fitctree (table (k, lab), 'lab'); %! N = table (categorical ({'a'; ''; 'z'}), 'VariableNames', {'k'}); %! assert_equal (predict (Mdl, N), {'p'; 'p'; 'p'}); %!error ... %! predict (ctM, setfield (ctT, 'SL', categorical (ctT.SL > 5.8))) ## A table at loss %!shared lctT, lctM %! load fisheriris %! lctT = table (meas(:,1), meas(:,2), 'VariableNames', {'SL', 'SW'}); %! lctT.Species = categorical (species); %! lctM = fitctree (lctT, 'Species'); %!test # the response is named, left out, or given beside the table %! a = loss (lctM, [lctT.SL, lctT.SW], lctT.Species); %! assert_equal (loss (lctM, lctT(:,1:2), lctT.Species), a); %! assert_equal (loss (lctM, lctT, 'Species'), a); %! assert_equal (loss (lctM, lctT), a); %!test # an even number of arguments after the table is all name-value %! a = loss (lctM, lctT, 'LossFun', 'classiferror'); %! assert_equal (loss (lctM, lctT, 'Species', 'LossFun', 'classiferror'), a); %!test # a table is read by name, so the order of its columns does not matter %! assert_equal (loss (lctM, lctT(:, [3, 2, 1])), loss (lctM, lctT)); %!error ... %! loss (lctM, lctT, 'NoSuch') %!error ... %! loss (lctM, lctT(:,1:2)) ## A table at edge, over the fixture the loss section sets up %!test # the response is named, left out, or given beside the table %! a = edge (lctM, [lctT.SL, lctT.SW], lctT.Species); %! assert_equal (edge (lctM, lctT(:,1:2), lctT.Species), a); %! assert_equal (edge (lctM, lctT, 'Species'), a); %! assert_equal (edge (lctM, lctT), a); %!test # an even number of arguments after the table is all name-value %! w = ones (150, 1); %! a = edge (lctM, lctT, 'Weights', w); %! assert_equal (edge (lctM, lctT, 'Species', 'Weights', w), a); %!test # a table is read by name, so the order of its columns does not matter %! assert_equal (edge (lctM, lctT(:, [3, 2, 1])), edge (lctM, lctT)); %!error ... %! edge (lctM, lctT, 'NoSuch') %!error ... %! edge (lctM, lctT(:,1:2)) ## A table at margin, over the fixture the loss section sets up %!test # the response is named, left out, or given beside the table %! a = margin (lctM, [lctT.SL, lctT.SW], lctT.Species); %! assert_equal (margin (lctM, lctT(:,1:2), lctT.Species), a); %! assert_equal (margin (lctM, lctT, 'Species'), a); %! assert_equal (margin (lctM, lctT), a); %!test # a table is read by name, so the order of its columns does not matter %! assert_equal (margin (lctM, lctT(:, [3, 2, 1])), margin (lctM, lctT)); %!error ... %! margin (lctM, lctT, 'NoSuch') %!error ... %! margin (lctM, lctT(:,1:2)) ## Observation weights of class single or double %!error ... %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'Weights', ... %! int8 ([1; 1; 1; 1])) %!error ... %! ClassificationTree (ones (4, 2), [1; 1; 2; 2], 'Weights', true (4, 1)) %!test %! ## Single weights are stored single, summing to one %! load fisheriris %! w = 1 + (1:150)' / 7; %! Mdl = ClassificationTree (meas, species, 'Weights', single (w)); %! assert_equal (class (Mdl.W), 'single'); %! assert_equal (sum (double (Mdl.W)), 1, 1e-6); %!test %! ## Single weights compute as double %! load fisheriris %! w = 1 + (1:150)' / 7; %! A = ClassificationTree (meas, species, 'Weights', single (w)); %! B = ClassificationTree (meas, species, 'Weights', double (single (w))); %! assert_equal (nthargout (2, @predict, A, meas), nthargout (2, @predict, ... %! B, meas)); ## Test a class with prior 0, whose observations are removed before fitting, ## as MATLAB R2024a and R2026b remove them %!warning ... %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! Mdl = ClassificationTree (X, Y, 'Prior', [1, 0]); %! assert_equal (Mdl.NumObservations, 50); %! assert_equal (Mdl.RowsUsed, [true(50, 1); false(50, 1)]); %! assert_equal (Mdl.Prior, [1, 0]); %! assert_equal (predict (Mdl, meas([51, 110], 3:4)), ... %! {'versicolor'; 'versicolor'}); %!warning ... %! load fisheriris %! Mdl = ClassificationTree (meas, species, 'Prior', [0.5, 0.5, 0]); %! assert_equal (predict (Mdl, meas([1, 51, 101], :)), ... %! {'setosa'; 'versicolor'; 'versicolor'}); %!test %! ## A named class with prior 0 may be absent, having nothing to remove %! load fisheriris %! X = meas(51:100, 3:4); %! Y = species(51:100); %! C = {'versicolor', 'virginica'}; %! Mdl = ClassificationTree (X, Y, 'ClassNames', C, 'Prior', [1, 0]); %! assert_equal (Mdl.ClassNames, {'versicolor'; 'virginica'}); %! assert_equal (predict (Mdl, meas(51, 3:4)), {'versicolor'}); %!warning ... %! load fisheriris %! Mdl = ClassificationTree (meas(51:150, 3:4), species(51:150), ... %! 'Cost', [0, 1; 0, 0]); %! assert_equal (Mdl.NumObservations, 50); %! assert_equal (Mdl.Prior, [1, 0]); %!warning ... %! load fisheriris %! X = meas(51:150, 3:4); %! X([3, 70], 1) = NaN; %! Mdl = ClassificationTree (X, species(51:150), 'Prior', [1, 0]); %! assert_equal ([Mdl.NumObservations, rows(Mdl.X), nnz(Mdl.RowsUsed)], ... %! [50, 50, 50]); %!warning ... %! ## A cost given as a structure removes the class it makes free %! load fisheriris %! S = struct ('ClassNames', {{'virginica', 'versicolor'}}, ... %! 'ClassificationCosts', [0, 0; 1, 0]); %! Mdl = ClassificationTree (meas(51:150, 3:4), species(51:150), 'Cost', S); %! assert_equal (Mdl.NumObservations, 50); %! assert_equal (Mdl.Prior, [1, 0]); %!test %! load fisheriris %! Y = categorical (species(51:100)); %! C = {'versicolor', 'virginica'}; %! Mdl = ClassificationTree (meas(51:100, 3:4), Y, 'ClassNames', C, ... %! 'Prior', [1, 0]); %! assert_equal (cellstr (Mdl.ClassNames), C'); %!warning ... %! load fisheriris %! Mdl = ClassificationTree (meas, species, 'Prior', [0.5, 0, 0.5]); %! fname = [tempname(), '.mat']; %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (predict (M2, meas([1, 51, 101], :)), ... %! predict (Mdl, meas([1, 51, 101], :))); ## MATLAB parity: a row missing every predictor, or of weight zero, is ## dropped, and the empirical prior counts only the rows kept, as measured ## on R2024a %!shared trX, trL %! x1 = mod ((1:30)', 7) + (1:30)' / 10; %! x2 = mod ((1:30)', 5) * 2 - (1:30)' / 20; %! trX = [x1, x2]; %! trX([2, 9, 17], 1) = NaN; %! trX(23, 2) = NaN; %! trX(27, :) = NaN; %! trL = repmat ({'p'; 'q'; 'q'; 'p'; 'p'; 'q'}, 5, 1); %!test # the row missing every predictor is not used %! Mdl = fitctree (trX, trL); %! assert_equal (Mdl.NumObservations, 29); %! assert_equal (find (! Mdl.RowsUsed)', 27); %! assert_equal ([rows(Mdl.X), numel(Mdl.W)], [29, 29]); %!test # the empirical prior is taken over the rows kept %! Mdl = fitctree (trX, trL); %! assert_equal (Mdl.Prior, [15, 14] / 29, 1e-15); %!test # and so are the class probabilities %! [~, score] = predict (fitctree (trX, trL), [3, 1; NaN, 3]); %! assert_equal (score, [7, 1; 15 * 8 / 29, 14 * 8 / 29] / 8, 1e-15); %!test # a row of weight zero is not used %! Mdl = fitctree (trX, trL, 'Weights', [0; ones(29, 1)]); %! assert_equal (Mdl.NumObservations, 28); %! assert_equal (Mdl.Prior, [0.5, 0.5], 1e-15); %!error ... %! fitctree ([NaN, NaN; NaN, NaN; 1, 2], {'a'; 'b'; 'c'}, 'Weights', [1; 1; 0]) statistics-release-2.1.2/inst/Machine_Learning/CompactClassificationDiscriminant.m000066400000000000000000002777061527311104300304710ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef CompactClassificationDiscriminant < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} CompactClassificationDiscriminant ## ## Compact discriminant analysis classification ## ## The @code{CompactClassificationDiscriminant} class implements a compact ## version of a linear discriminant analysis classifier object, which can ## predict responses for new data using the @code{predict} method but does not ## store the training data. ## ## A @code{CompactClassificationDiscriminant} object is a compact version of a ## discriminant analysis model, @code{ClassificationDiscriminant}. It does ## not include the training data resulting in a smaller classifier size, which ## can be used for making predictions from new data, but not for tasks such as ## cross validation. It can only be created from a ## @code{ClassificationDiscriminant} model by using the @code{compact} object ## method. ## ## Create a @code{CompactClassificationDiscriminant} object by using the ## @code{compact} method of a @code{ClassificationDiscriminant} object. ## ## ## Six discriminant types are available, in two families. The linear family, ## @qcode{'linear'}, @qcode{'diagLinear'} and @qcode{'pseudoLinear'}, pools ## one covariance across the classes and separates them with a hyperplane. ## The quadratic family, @qcode{'quadratic'}, @qcode{'diagQuadratic'} and ## @qcode{'pseudoQuadratic'}, estimates a covariance per class and separates ## them with a quadric. A @qcode{'diag'} type keeps only the variances, ## which is the same model as a @qcode{Gamma} of 1, and a @qcode{'pseudo'} ## type inverts a singular covariance rather than refusing it. ## ## @qcode{DiscrimType} may be assigned after fitting, but @emph{only within ## its own family}: the family is fixed when the model is fitted, because it ## decides which covariances the fit has to estimate. Assigning it, or ## @qcode{Gamma}, re-derives @qcode{Sigma}, @qcode{LogDetSigma} and ## @qcode{Coeffs} without refitting. ## ## @seealso{fitcdiscr, ClassificationDiscriminant} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CompactClassificationDiscriminant} {property} NumPredictors ## ## Number of predictors ## ## A positive integer value specifying the number of predictors in the ## training dataset used for training the CompactClassificationDiscriminant ## model. This property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {CompactClassificationDiscriminant} {property} PredictorNames ## ## Names of predictor variables ## ## A cell array of character vectors specifying the names of the predictor ## variables. The names are in the order in which they appear in the ## training dataset. This property is read-only. ## ## @end deftp PredictorNames = []; ## -*- texinfo -*- ## @deftp {CompactClassificationDiscriminant} {property} BetweenSigma ## ## Between-class covariance matrix ## ## A @math{P}-by-@math{P} matrix holding the covariance of the class means ## about the overall mean, weighted by how many observations each class ## contributes. With @math{n_k} observations in class @math{k}, ## @math{p_k = n_k / n} and @math{\bar{\mu} = \sum_k p_k \mu_k}, it is ## ## @example ## @group ## BetweenSigma = sum_k n_k (Mu(k,:) - mubar)' * (Mu(k,:) - mubar) ## / (n * (1 - sum_k p_k^2)) ## @end group ## @end example ## ## The denominator is the unbiased one for a weighted covariance, so a ## balanced fit divides by @math{n (K-1) / K}. It reads the @strong{class ## sizes}, not @qcode{Prior}: assigning a prior leaves it where it was. It ## is estimated for every discriminant type, the quadratic family included, ## since it describes the classes rather than the fit. This property is ## read-only. ## ## @end deftp BetweenSigma = []; ## -*- texinfo -*- ## @deftp {CompactClassificationDiscriminant} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A numeric vector of column indices into @code{X} naming the predictors ## treated as categorical, and empty when none is. This property is ## read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {CompactClassificationDiscriminant} {property} ExpandedPredictorNames ## ## Names of the predictors as the model expanded them ## ## A cell array of character vectors. It matches @code{PredictorNames} ## unless a categorical predictor was expanded into indicator variables. ## This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationDiscriminant} {property} ResponseName ## ## Response variable name ## ## A character vector specifying the name of the response variable @var{Y}. ## This property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {CompactClassificationDiscriminant} {property} ClassNames ## ## Names of classes in the response variable ## ## An array of unique values of the response variable @var{Y}, which has the ## same data types as the data in @var{Y}. This property is read-only. ## @qcode{ClassNames} can have any of the following datatypes: ## ## @itemize ## @item Cell array of character vectors ## @item Character array ## @item Logical vector ## @item Numeric vector ## @end itemize ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {CompactClassificationDiscriminant} {property} Sigma ## ## Within-class covariance ## ## A numeric array whose shape follows @qcode{DiscrimType}, with @math{P} ## predictors and @math{K} classes: ## ## @multitable @columnfractions 0.4 0.25 0.35 ## @headitem @var{DiscrimType} @tab @var{Sigma} @tab @var{LogDetSigma} ## @item @qcode{'linear'}, @qcode{'pseudoLinear'} @tab @math{PxP} ## @tab scalar ## @item @qcode{'quadratic'}, @qcode{'pseudoQuadratic'} @tab @math{PxPxK} ## @tab @math{Kx1} ## @item @qcode{'diagLinear'} @tab @math{1xP} @tab scalar ## @item @qcode{'diagQuadratic'} @tab @math{1xPxK} @tab @math{Kx1} ## @end multitable ## ## The linear family pools one covariance across the classes and the ## quadratic family estimates one per class. This property is read-only, ## but it is re-derived whenever @qcode{DiscrimType} or @qcode{Gamma} is ## assigned. ## ## @end deftp Sigma = []; ## -*- texinfo -*- ## @deftp {CompactClassificationDiscriminant} {property} Mu ## ## Class means ## ## A @math{K*P} numeric matrix specifying the mean of the multivariate ## normal distribution of each corresponding class, where @math{K} is the ## number of classes and @math{P} is the number of predictors. This property ## is read-only. ## ## @end deftp Mu = []; ## -*- texinfo -*- ## @deftp {CompactClassificationDiscriminant} {property} Coeffs ## ## Coefficient matrices ## ## A @math{K*K} structure containing the coefficient matrices, where ## @math{K} is the number of classes. If the @qcode{'FillCoeffs'} parameter ## was set to @qcode{'off'} in the original ## @code{ClassificationDiscriminant} model, then @qcode{Coeffs} is empty ## @qcode{([])}. This property is read-only. ## ## @qcode{Coeffs(i,j)} contains the coefficients of the boundary between ## the classes @code{i} and @code{j} in the following fields: ## ## @itemize ## @item @qcode{DiscrimType} - A character vector ## @item @qcode{Class1} - @qcode{@var{ClassNames}(i)} ## @item @qcode{Class2} - @qcode{@var{ClassNames}(j)} ## @item @qcode{Const} - A scalar ## @item @qcode{Linear} - A vector with length as the number of predictors. ## @item @qcode{Quadratic} - The quadratic family only. A @math{PxP} ## matrix, or a @math{1xP} vector for @qcode{'diagQuadratic'}, following ## the shape of @qcode{Sigma}. ## @end itemize ## ## The diagonal entries carry the two class names and nothing else. The ## structure is rebuilt whenever @qcode{DiscrimType}, @qcode{Gamma} or ## @qcode{Prior} is assigned. ## ## @end deftp Coeffs = []; ## -*- texinfo -*- ## @deftp {CompactClassificationDiscriminant} {property} DeltaPredictor ## ## Minimum Delta at which each predictor drops out ## ## A row vector with one entry per predictor, the value of @qcode{Delta} at ## which that predictor's coefficient is zero for every class and the ## predictor leaves the model altogether. It is all zeros for the ## quadratic family, which has no linear coefficients to eliminate. ## ## This property is read-only, and it describes the fit rather than the ## threshold: assigning @qcode{Delta} does not move it. ## ## @end deftp DeltaPredictor = []; ## -*- texinfo -*- ## @deftp {CompactClassificationDiscriminant} {property} MinGamma ## ## Minimum value for the Gamma regularization parameter ## ## A scalar from 0 to 1, the least regularization that leaves the ## correlation matrix invertible. It is 0 when the matrix is already ## invertible, and positive when the predictors are collinear, in which ## case a plain @qcode{'linear'} or @qcode{'quadratic'} fit is raised to it ## rather than failing. Assigning a @qcode{Gamma} below it is refused. ## ## This property is read-only. ## ## @end deftp MinGamma = []; ## -*- texinfo -*- ## @deftp {CompactClassificationDiscriminant} {property} LogDetSigma ## ## Logarithm of the determinant of the within-class covariance matrix ## ## A scalar for the linear family and a @math{Kx1} vector for the quadratic ## one, one entry per class. It is computed in correlation space, as the ## sum of the logarithms of the predictor variances plus the log ## determinant of the correlation matrix, which is far better conditioned ## than the covariance when the data are nearly collinear. A predictor ## with no variance contributes nothing rather than an infinity, and the ## @qcode{'pseudo'} types sum only over the directions that carry variance. ## ## This property is read-only. ## ## @end deftp LogDetSigma = []; endproperties ## Properties a user may set after the model is built. Each one is ## validated by its set method below. properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {CompactClassificationDiscriminant} {property} DiscrimType ## ## Discriminant type ## ## A character vector naming the discriminant model, one of ## @qcode{'linear'}, @qcode{'quadratic'}, @qcode{'diagLinear'}, ## @qcode{'diagQuadratic'}, @qcode{'pseudoLinear'} or ## @qcode{'pseudoQuadratic'}. A linear type pools one covariance across ## the classes; a quadratic type estimates one per class. A ## @qcode{'diag'} type keeps only the variances, and a @qcode{'pseudo'} ## type inverts a singular covariance instead of refusing it. ## ## This property may be assigned, but @emph{only within its own family}: ## the three linear types interchange freely and so do the three quadratic ## ones, while no assignment moves a model between the two. The family is ## fixed when the model is fitted, because it decides which covariances the ## fit has to estimate. Assigning re-derives @qcode{Sigma}, ## @qcode{LogDetSigma}, @qcode{Gamma} and @qcode{Coeffs}. ## ## @end deftp DiscrimType = []; ## -*- texinfo -*- ## @deftp {CompactClassificationDiscriminant} {property} Gamma ## ## Gamma regularization parameter ## ## A scalar from 0 to 1 shrinking the covariance towards its diagonal. ## @qcode{Gamma} and @qcode{DiscrimType} are one state: a value of 1 is the ## diagonal type, so assigning it renames @qcode{DiscrimType} to ## @qcode{'diagLinear'} or @qcode{'diagQuadratic'}, and assigning a ## diagonal type sets @qcode{Gamma} to 1. ## ## The quadratic family admits 0 and 1 only. A value below ## @qcode{MinGamma} is refused, since it would leave the covariance ## singular. Assigning re-derives @qcode{Sigma}, @qcode{LogDetSigma} and ## @qcode{Coeffs}. ## ## @end deftp Gamma = []; ## -*- texinfo -*- ## @deftp {CompactClassificationDiscriminant} {property} Delta ## ## Delta threshold for the linear coefficients ## ## A nonnegative scalar that eliminates predictors. A per-class linear ## coefficient is set to zero when it falls below @qcode{Delta}, and the ## comparison is made on the @strong{standardized} coefficient, the ## coefficient times the within-class standard deviation of its predictor. ## Scaling matters here: a threshold on the raw coefficients would depend ## on the units each predictor is measured in, so the same model in ## centimetres and in metres would drop different predictors. ## ## @qcode{DeltaPredictor} reports, per predictor, the value at which it ## drops out of every class at once. ## ## It applies to the linear family only, a quadratic discriminant having no ## linear coefficients to eliminate. Assigning it rebuilds @qcode{Coeffs} ## and changes what @code{predict} answers. ## ## @end deftp Delta = []; ## -*- texinfo -*- ## @deftp {CompactClassificationDiscriminant} {property} Cost ## ## Cost of Misclassification ## ## A square matrix specifying the cost of misclassification of a point. ## @qcode{Cost(i,j)} is the cost of classifying a point into class @qcode{j} ## if its true class is @qcode{i} (that is, the rows correspond to the true ## class and the columns correspond to the predicted class). The order of ## the rows and columns in @qcode{Cost} corresponds to the order of the ## classes in @qcode{ClassNames}. The number of rows and columns in ## @qcode{Cost} is the number of unique classes in the response. By ## default, @qcode{Cost(i,j) = 1} if @qcode{i != j}, and ## @qcode{Cost(i,j) = 0} if @qcode{i = j}. In other words, the cost is 0 ## for correct classification and 1 for incorrect classification. ## ## This property is read-only. ## ## ## A cost may also be given as a struct with the fields ## @qcode{ClassNames} and @qcode{ClassificationCosts}, which names the ## order its own matrix is written in. That matrix is permuted into the ## order of @qcode{ClassNames} above, so a caller need not know which ## order the classes were sorted into. It must name every class. ## ## A cost must be floating point, not sparse, not complex, non-negative ## and zero down its diagonal, and must hold no @qcode{NaN} or ## @qcode{Inf}. A @code{single} is widened to @code{double}. ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {CompactClassificationDiscriminant} {property} Prior ## ## Prior probability for each class ## ## A numeric vector specifying the prior probabilities for each class. The ## order of the elements in @qcode{Prior} corresponds to the order of the ## classes in @qcode{ClassNames}. ## ## This property is read-only. ## ## Specified as a row vector with one entry per class, in the order of ## @qcode{ClassNames}, and rescaled to sum to one. It may be given as ## @qcode{'empirical'}, @qcode{'uniform'}, a numeric vector, or a ## structure with @qcode{ClassNames} and @qcode{ClassProbs} fields, which ## assigns each probability by class name rather than by position. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {CompactClassificationDiscriminant} {property} ScoreTransform ## ## Transformation function for classification scores ## ## Specified as a function handle for transforming the classification ## scores. This property is read-only. ## ## When specified as a character vector, it can be any of the following ## built-in functions. Nevertheless, the @qcode{ScoreTransform} property ## always stores their function handle equivalent. ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Value} @tab @var{Description} ## @item @qcode{'doublelogit'} @tab @math{1 ./ (1 + exp (-2 * x))} ## @item @qcode{'invlogit'} @tab @math{log (x ./ (1 - x))} ## @item @qcode{'ismax'} @tab Sets the score for the class with the ## largest score to 1, and for all other classes to 0 ## @item @qcode{'logit'} @tab @math{1 ./ (1 + exp (-x))} ## @item @qcode{'none'} @tab @math{x} (no transformation) ## @item @qcode{'identity'} @tab @math{x} (no transformation) ## @item @qcode{'sign'} @tab ## @math{-1 for x < 0, 0 for x = 0, 1 for x > ## 0} ## @item @qcode{'symmetric'} @tab @math{2 * x - 1} ## @item @qcode{'symmetricismax'} @tab Sets the score for the class ## with the largest score to 1, and for all other classes to -1 ## @item @qcode{'symmetriclogit'} @tab @math{2 ./ (1 + exp (-x)) - 1} ## @end multitable ## ## @end deftp ScoreTransform = 'none'; endproperties ## Readable by the counterpart class, which copies it, and kept out of ## the documented surface. properties (GetAccess = public, SetAccess = protected, Hidden) ## The unregularized within-class covariance the fit estimated, carried ## over from the full model so that assigning DiscrimType re-derives here ## exactly as it does there. BaseSigma = []; STfun = @(x) x; endproperties ## Set methods for the properties a user may assign. methods (Hidden) ## DiscrimType, Gamma and Delta are one state, and these three methods are ## the only place it is changed. Assigning the type or the regularization ## re-derives Sigma, LogDetSigma and Coeffs from BaseSigma, the covariance ## the fit estimated, so no assignment ever refits. Each writes its own ## property directly, which does not re-enter its own set method, and ## reaches the others through theirs, which is what keeps the three ## consistent whichever one the user assigns. function this = set.DiscrimType (this, val) [t, fam] = discrimcanon (val); ## The family a fit belongs to is written in BaseSigma's shape: one ## covariance for the linear family, one per class for the quadratic. ## Reading it there rather than from the stored type is what lets a ## loaded model take its own type back, the stub it is loaded into ## carrying the default type until the file has been read. if (! isempty (this.BaseSigma) && size (this.BaseSigma, 3) > 1) cur = 'quadratic'; elseif (! isempty (this.BaseSigma)) cur = 'linear'; else [~, cur] = discrimcanon (this.DiscrimType); endif if (isempty (t) || (! isempty (cur) && ! strcmp (fam, cur))) ## The family was fixed by the fit, so only three types are on offer, ## and an unrecognized name is refused by the same message. if (strcmp (cur, 'quadratic')) ok = "quadratic, diagQuadratic, or pseudoQuadratic"; else ok = "linear, diagLinear, or pseudoLinear"; endif error (strcat ("CompactClassificationDiscriminant:", ... " 'DiscrimType' can only be set to one of: %s."), ok); endif this.DiscrimType = t; if (isempty (this.BaseSigma)) return; # not fitted yet, nothing to derive endif ## A diagonal type is Gamma of 1 and every other type starts from 0, ## which is what the oracle reports for each transition. The derivation ## raises it to MinGamma where the covariance needs it. g = double (strncmp (t, 'diag', 4)); [~, ~, g] = discrimderive (this.BaseSigma, t, g, this.MinGamma); this.Gamma = g; endfunction function this = set.Gamma (this, val) if (! (isnumeric (val) && isscalar (val) && val >= 0 && val <= 1)) error (strcat ("CompactClassificationDiscriminant: 'Gamma' must", ... " be a scalar between 0 and 1.")); endif if (isempty (this.BaseSigma)) this.Gamma = val; return; endif [~, fam] = discrimcanon (this.DiscrimType); if (strcmp (fam, 'quadratic') && val > 0 && val < 1) error (strcat ("CompactClassificationDiscriminant: cannot set", ... " 'Gamma' to any value but 0 or 1 for a quadratic", ... " discriminant.")); endif ## Gamma of 1 IS the diagonal type. Naming the type does the rest, and ## comes back here with a value that no longer needs the rename. if (val == 1 && ! strncmp (this.DiscrimType, 'diag', 4)) if (strcmp (fam, 'quadratic')) this.DiscrimType = 'diagQuadratic'; else this.DiscrimType = 'diagLinear'; endif return; elseif (val < 1 && strncmp (this.DiscrimType, 'diag', 4)) if (strcmp (fam, 'quadratic')) this.DiscrimType = 'quadratic'; else this.DiscrimType = 'linear'; endif endif ## The MinGamma floor belongs to the one type that regularizes its way ## out of a singular covariance. A pseudo type is Gamma of 0 by ## definition, inverting the rank it has instead; a diagonal type is ## Gamma of 1 and never near the floor; and the quadratic family refuses ## a singular class covariance outright rather than ridging it. Mg = this.MinGamma; [Sg, Ld, Ge] = discrimderive (this.BaseSigma, this.DiscrimType, val, Mg); if (val < Mg && strcmp (this.DiscrimType, 'linear')) error (strcat ("CompactClassificationDiscriminant: 'Gamma' must be", ... " between %g and 1."), Mg); endif this.Gamma = Ge; this.Sigma = Sg; this.LogDetSigma = Ld; if (! isempty (this.Coeffs)) this.Coeffs = discrimcoeffs (this.Mu, Sg, Ld, this.Prior, ... this.DiscrimType, this.Delta, ... this.ClassNames); endif endfunction function this = set.Delta (this, val) if (! (isnumeric (val) && isscalar (val) && val >= 0)) error (strcat ("CompactClassificationDiscriminant: 'Delta' must", ... " be a nonnegative scalar.")); endif [~, fam] = discrimcanon (this.DiscrimType); if (val > 0 && strcmp (fam, 'quadratic')) error (strcat ("CompactClassificationDiscriminant: cannot", ... " eliminate linear predictors in a quadratic", ... " discriminant.")); endif this.Delta = val; if (! isempty (this.BaseSigma) && ! isempty (this.Coeffs)) this.Coeffs = discrimcoeffs (this.Mu, this.Sigma, this.LogDetSigma, ... this.Prior, this.DiscrimType, val, ... this.ClassNames); endif endfunction function this = set.ScoreTransform (this, val) name = 'CompactClassificationDiscriminant'; [this.STfun, this.ScoreTransform] = parseScoreTransform (val, name); endfunction function this = set.Cost (this, val) gnY = this.ClassNames; if (isempty (val)) this.Cost = cast (! eye (classCount (gnY)), 'double'); else ## Everything a cost must be, and the struct form, which ## is permuted into this model's class order. [val, errmsg] = costMatrix (val, gnY); if (! isempty (errmsg)) error ("CompactClassificationDiscriminant: %s", errmsg); endif this.Cost = val; endif endfunction function this = set.Prior (this, val) K = classCount (this.ClassNames); if (isstruct (val)) val = priorFromStruct (val, this.ClassNames, ... 'CompactClassificationDiscriminant'); endif if (ischar (val) && strcmpi (val, 'uniform')) this.Prior = ones (1, K) / K; elseif (isnumeric (val) && isreal (val) && isvector (val) && numel (val) == K && all (val >= 0) && sum (val) > 0) this.Prior = val(:)' / sum (val); else error (strcat ("CompactClassificationDiscriminant: 'Prior' must be", ... " 'uniform' or a non-negative numeric vector with", ... " one entry per class.")); endif endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationDiscriminant} {@var{obj} =} CompactClassificationDiscriminant (@var{Mdl}) ## @deftypefnx {CompactClassificationDiscriminant} {@var{obj} =} CompactClassificationDiscriminant () ## ## Create a @code{CompactClassificationDiscriminant} object. ## ## @var{Mdl} is the @code{ClassificationDiscriminant} object to ## compact. The documented way to reach this constructor is the ## @code{compact} method. ## ## Called with no arguments it returns an object with its properties ## empty, which is how a saved model is rebuilt before its values are ## filled in. ## ## @end deftypefn function this = CompactClassificationDiscriminant (Mdl = []) ## Check for appropriate class if (isempty (Mdl)) return; elseif (! strcmpi (class (Mdl), 'ClassificationDiscriminant')) error (strcat ("CompactClassificationDiscriminant: invalid", ... " classification object.")); endif ## The levels a predictor read from a table was coded through travel ## with the model, so a compact one still reads a table this.PredictorLevels = Mdl.PredictorLevels; ## Save properties to compact model this.NumPredictors = Mdl.NumPredictors; this.PredictorNames = Mdl.PredictorNames; this.CategoricalPredictors = Mdl.CategoricalPredictors; this.ExpandedPredictorNames = Mdl.ExpandedPredictorNames; this.ResponseName = Mdl.ResponseName; this.ClassNames = Mdl.ClassNames; this.Cost = Mdl.Cost; this.Prior = Mdl.Prior; this.ScoreTransform = Mdl.ScoreTransform; this.STfun = Mdl.STfun; this.Sigma = Mdl.Sigma; this.Mu = Mdl.Mu; this.BetweenSigma = Mdl.BetweenSigma; this.Coeffs = Mdl.Coeffs; this.Delta = Mdl.Delta; this.DiscrimType = Mdl.DiscrimType; this.Gamma = Mdl.Gamma; this.MinGamma = Mdl.MinGamma; this.LogDetSigma = Mdl.LogDetSigma; this.DeltaPredictor = Mdl.DeltaPredictor; this.BaseSigma = Mdl.BaseSigma; endfunction ## Custom display function disp (this) fprintf ("\n CompactClassificationDiscriminant\n\n"); ## Print selected properties fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); if (iscellstr (this.ClassNames)) str = repmat ({'''%s'''}, 1, numel (this.ClassNames)); str = strcat ('{', strjoin (str, ' '), '}'); str = sprintf (str, this.ClassNames{:}); else # numeric str = repmat ({'%d'}, 1, numel (this.ClassNames)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, this.ClassNames); endif fprintf ("%+25s: %s\n", 'ClassNames', str); fprintf ("%+25s: '%s'\n", 'ScoreTransform', this.ScoreTransform); fprintf ("%+25s: '%d'\n", 'NumPredictors', this.NumPredictors); fprintf ("%+25s: '%s'\n", 'DiscrimType', this.DiscrimType); fprintf ("%+25s: [%dx%d double]\n", 'Mu', size (this.Mu)); ## Coeffs is KxK, which is Sigma's shape only for a linear discriminant. fprintf ("%+25s: [%dx%d struct]\n\n", 'Coeffs', ... rows (this.ClassNames), rows (this.ClassNames)); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {CompactClassificationDiscriminant} {@var{n} =} nLinearCoeffs (@var{obj}) ## @deftypefnx {CompactClassificationDiscriminant} {@var{n} =} nLinearCoeffs (@var{obj}, @var{delta}) ## ## Number of nonzero linear coefficients at a regularization threshold. ## ## @code{@var{n} = nLinearCoeffs (@var{obj})} returns the number of ## predictors the discriminant keeps at its own @code{Delta}. ## ## @code{@var{n} = nLinearCoeffs (@var{obj}, @var{delta})} returns the ## number it would keep at each threshold in @var{delta}, as a column ## vector however @var{delta} is shaped. ## ## A predictor survives a threshold when its @code{DeltaPredictor} reaches ## it, the comparison including equality, so @var{delta} at exactly a ## predictor's own value still counts it. A threshold above every ## @code{DeltaPredictor} therefore leaves nothing and returns zero. ## ## The count is taken whatever the @code{DiscrimType}, as MATLAB takes it, ## even though @code{Delta} regularizes the linear types alone. ## ## @seealso{fitcdiscr, ClassificationDiscriminant, ## CompactClassificationDiscriminant} ## @end deftypefn function n = nLinearCoeffs (this, delta) if (nargin < 1 || nargin > 2) print_usage (); endif if (nargin < 2) delta = this.Delta; endif if (! (isnumeric (delta) && isreal (delta))) error (strcat ("CompactClassificationDiscriminant.nLinearCoeffs:", ... " DELTA must be a real numeric value.")); endif ## One column per threshold, one row per predictor, summed down. n = sum (this.DeltaPredictor(:) >= delta(:)', 1)'; endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationDiscriminant} {@var{label} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {CompactClassificationDiscriminant} {[@var{label}, @var{score}, @var{cost}] =} predict (@var{obj}, @var{XC}) ## ## Classify new data points into categories using the discriminant ## analysis model from a CompactClassificationDiscriminant object. ## ## @code{@var{label} = predict (@var{obj}, @var{XC})} returns the vector of ## labels predicted for the corresponding instances in @var{XC}, using the ## corresponding labels from the trained @qcode{ClassificationDiscriminant}, ## model, @var{obj}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{CompactClassificationDiscriminant} class ## object. ## @item ## @var{XC} must be an @math{M*P} numeric matrix with the same number of ## features @math{P} as the corresponding predictors of the discriminant ## model in @var{obj}. ## @end itemize ## ## @code{[@var{label}, @var{score}, @var{cost}] = predict (@var{obj}, ## @var{XC})} also returns @var{score}, which contains the predicted class ## scores or posterior probabilities for each instance of the corresponding ## unique classes, and @var{cost}, which is a matrix containing the expected ## cost of the classifications. A query with a missing value has ## @qcode{NaN} scores and costs and takes the class of largest prior. ## ## The @var{score} matrix contains the posterior probabilities for each ## class, calculated using the multivariate normal probability density ## function and the prior probabilities of each class. These scores are ## normalized to ensure they sum to 1 for each observation. ## ## The @var{cost} matrix contains the expected classification cost for each ## class, computed based on the posterior probabilities and the specified ## misclassification costs. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @seealso{CompactClassificationDiscriminant, fitcdiscr} ## @end deftypefn function [label, score, cost] = predict (this, XC) ## Check for sufficient input arguments if (nargin < 2) error (strcat ("CompactClassificationDiscriminant.predict:", ... " too few input arguments.")); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'CompactClassificationDiscriminant.predict', XC); ## Check for valid XC if (isempty (XC)) error ("CompactClassificationDiscriminant.predict: XC is empty."); elseif (this.NumPredictors != columns (XC)) error (strcat ("CompactClassificationDiscriminant.predict: XC", ... " must have the same number of features as the", ... " trained model.")); endif ## Initialize matrices numObservations = rows (XC); numClasses = classCount (this.ClassNames); score = zeros (numObservations, numClasses); cost = zeros (numObservations, numClasses); ## Score from the inverse covariance and its log determinant rather than ## through mvnpdf. A pseudo type's covariance is deliberately singular ## and mvnpdf refuses it, and LogDetSigma has to be the value the ## property reports, so the score and the property cannot drift apart. [~, fam] = discrimcanon (this.DiscrimType); logscore = zeros (numObservations, numClasses); if (strcmp (fam, 'linear')) ## The linear family scores from its per-class coefficients, which is ## the same function with the terms common to every class dropped, and ## is the only form in which Delta can eliminate a predictor. [Z, b] = discrimlinear (this.Mu, this.Sigma, this.Prior, ... this.DiscrimType, this.Delta); logscore = XC * Z' + b; else SigmaInv = discriminv (this.Sigma, this.DiscrimType); nInv = size (SigmaInv, 3); logdet = this.LogDetSigma; if (isscalar (logdet)) logdet = repmat (logdet, numClasses, 1); endif for i = 1:numClasses Si = SigmaInv(:,:,min (i, nInv)); Zc = XC - this.Mu(i, :); logscore(:, i) = -0.5 * sum ((Zc * Si) .* Zc, 2) ... - 0.5 * logdet(i) + log (this.Prior(i)); endfor endif ## The shared 2*pi factor cancels in the normalization, and subtracting ## the row maximum first keeps a well separated observation a posterior ## rather than a ratio of two underflowed zeros. logscore = logscore - max (logscore, [], 2); score = exp (logscore); score = score ./ sum (score, 2); ## Calculate expected classification cost for i = 1:numClasses cost(:, i) = sum (bsxfun (@times, score, this.Cost(:, i)'), 2); endfor ## Predict the class labels based on the minimum cost [~, minIdx] = min (cost, [], 2); ## A query with no posterior takes the class of largest prior [~, minIdx(any (isnan (score), 2))] = max (this.Prior); label = labelsFromIndex (this.ClassNames, minIdx); ## Apply ScoreTransform once to the whole matrix, after the label and ## the cost have been taken from the untransformed posteriors: a ## transform reshapes what predict reports and must not move what it ## decides. score = this.STfun (score); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationDiscriminant} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationDiscriminant} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationDiscriminant} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {CompactClassificationDiscriminant} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Compute loss for a trained CompactClassificationDiscriminant object. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} computes the loss, ## @var{L}, using the default loss function @qcode{'mincost'}. ## ## @itemize ## @item ## @code{obj} is a @var{CompactClassificationDiscriminant} object. ## @item ## @code{X} must be a @math{N*P} numeric matrix of input data where rows ## correspond to observations and columns correspond to features or ## variables. ## @item ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels ## of corresponding predictor data in @var{X}. @var{Y} must have same ## numbers of rows as @var{X}. ## @end itemize ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{L} = loss (@dots{}, @var{name}, @var{value})} allows ## additional options specified by @var{name}-@var{value} pairs: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'LossFun'} @tab Specifies the loss function to use. ## Can be a function handle with four input arguments (C, S, W, Cost) ## which returns a scalar value or one of: ## 'binodeviance', 'classifcost', 'classiferror', 'exponential', ## 'hinge', 'logit','mincost', 'quadratic'. ## @itemize ## @item ## @code{C} is a logical matrix of size @math{N*K}, where @math{N} is the ## number of observations and @math{K} is the number of classes. ## The element @code{C(i,j)} is true if the class label of the i-th ## observation is equal to the j-th class. ## @item ## @code{S} is a numeric matrix of size @math{N*K}, where each element ## represents the classification score for the corresponding class. ## @item ## @code{W} is a numeric vector of length @math{N}, representing ## the observation weights. ## @item ## @code{Cost} is a @math{K*K} matrix representing the misclassification ## costs. ## @end itemize ## ## @item @qcode{'Weights'} @tab Specifies observation weights, must be ## a numeric vector of length equal to the number of rows in X. ## Default is @code{ones (size (X, 1))}. loss normalizes the weights so that ## observation weights in each class sum to the prior probability of that ## class. When you supply Weights, loss computes the weighted ## classification loss. ## ## @end multitable ## ## @seealso{CompactClassificationDiscriminant} ## @end deftypefn function L = loss (this, X, Y, varargin) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error (strcat ("CompactClassificationDiscriminant.loss:", ... " too few input arguments.")); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("CompactClassificationDiscriminant.loss:", ... " name-value arguments must be in pairs.")); endif ## Validate Y valid_types = {'char', 'string', 'logical', 'single', 'double', ... 'cell', 'categorical'}; if (! (any (strcmp (class (Y), valid_types)))) error (strcat ("CompactClassificationDiscriminant.loss:", ... " Y must be of a valid type.")); endif ## Validate size of Y if (size (Y, 1) != size (X, 1)) error (strcat ("CompactClassificationDiscriminant.loss: Y must", ... " have the same number of rows as X.")); endif ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights'}; dfValues = {'mincost', []}; [LossFun, Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments lf_opt = {'binodeviance', 'classifcost', 'classiferror', ... 'exponential', 'hinge','logit', 'mincost', 'quadratic'}; if (isa (LossFun, 'function_handle')) ## Check if the loss function is valid if (nargin (LossFun) != 4) error (strcat ("CompactClassificationDiscriminant.loss: custom", ... " loss function must accept exactly four input", ... " arguments.")); endif try n = 1; K = 2; C_test = false (n, K); S_test = zeros (n, K); W_test = ones (n, 1); Cost_test = ones (K) - eye (K); test_output = LossFun(C_test, S_test, W_test, Cost_test); if (! isscalar (test_output)) error (strcat ("CompactClassificationDiscriminant.loss: custom", ... " loss function must return a scalar value.")); endif catch error (strcat ("CompactClassificationDiscriminant.loss: custom", ... " loss function is not valid or does not produce", ... " correct output.")); end_try_catch elseif (! (ischar (LossFun) && any (strcmpi (LossFun, lf_opt)))) error (strcat ("CompactClassificationDiscriminant.loss: invalid", ... " loss function.")); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("CompactClassificationDiscriminant.loss: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isvector (Weights))) error ("CompactClassificationDiscriminant.loss: invalid 'Weights'."); endif if (! isempty (Weights) && numel (Weights) != rows (X)) error (strcat ("CompactClassificationDiscriminant.loss: number of", ... " 'Weights' must be equal to the number of rows in", ... " X.")); endif if (! isempty (args)) error (strcat ("CompactClassificationDiscriminant.loss: invalid", ... " optional paired argument.")); endif ## Check for missing values in X if (! isa (LossFun, 'function_handle')) lossfun = tolower (LossFun); if (! strcmp (lossfun, 'mincost') && ! strcmp (lossfun, 'classiferror') && ! strcmp (lossfun, 'classifcost') && any (isnan (X(:)))) L = NaN; return; endif endif ## Bring Y to the type of the classes if (ischar (Y)) Y = cellstr (Y); elseif (isa (Y, 'string')) Y = cellstr (Y); elseif (isnumeric (Y) || islogical (Y)) if (iscellstr (this.ClassNames)) Y = cellstr (num2str (double (Y))); endif elseif (iscell (Y)) Y = cellfun (@num2str, Y, 'UniformOutput', false); elseif (! isa (Y, 'categorical')) ## A categorical is matched to the classes by name as it stands. error (strcat ("CompactClassificationDiscriminant.loss: Y must be", ... " a numeric, logical, char, string, categorical or", ... " cell array.")); endif ## Check if Y contains correct classes if (! labelsKnown (Y, this.ClassNames)) error (strcat ("CompactClassificationDiscriminant.loss: Y must", ... " contain only the classes in ClassNames.")); endif ## Set default weights if not specified if (isempty (Weights)) Weights = ones (size (X, 1), 1); endif Weights = double (Weights(:)); ## Normalize Weights unique_classes = this.ClassNames; class_prior_probs = this.Prior; norm_weights = zeros (size (Weights)); ## Labels are matched to the classes by name, whatever their type. gYw = labelIndices (unique_classes, Y); for i = 1:classCount (unique_classes) class_idx = (gYw == i); if (sum (Weights(class_idx)) > 0) norm_weights(class_idx) = ... Weights(class_idx) * class_prior_probs(i) / sum (Weights(class_idx)); endif endfor Weights = norm_weights / sum (norm_weights); ## Number of observations n = size (X, 1); ## Predict classification scores [label, scores] = predict (this, X); gLab = labelIndices (this.ClassNames, label); ## C is vector of K-1 zeros, with 1 in the ## position corresponding to the true class K = classCount (this.ClassNames); C = false (n, K); ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (this.ClassNames, Y); for i = 1:n class_idx = gYidx(i); C(i, class_idx) = true; endfor Y_new = C'; ## Compute the loss using custom loss function if (isa (LossFun, 'function_handle')) L = LossFun(C, scores, Weights, this.Cost); return; endif ## Compute the scalar classification score for each observation m_j = zeros (n, 1); for i = 1:n m_j(i) = scores(i,:) * Y_new(:,i); endfor ## Compute the loss switch (tolower (LossFun)) case 'binodeviance' b = log (1 + exp (-2 * m_j)); L = (Weights') * b; case 'hinge' h = max (0, 1 - m_j); L = (Weights') * h; case 'exponential' e = exp (-m_j); L = (Weights') * e; case 'logit' l = log (1 + exp (-m_j)); L = (Weights') * l; case 'quadratic' q = (1 - m_j) .^ 2; L = (Weights') * q; case 'classiferror' L = 0; for i = 1:n L = L + Weights(i) * (gYidx(i) != gLab(i)); endfor case 'mincost' Cost = this.Cost; L = 0; ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (this.ClassNames, Y); for i = 1:n f_Xj = scores(i, :); gamma_jk = f_Xj * Cost; [~, min_cost_class] = min (gamma_jk); if (any (isnan (gamma_jk))) min_cost_class = gLab(i); endif cj = Cost(gYidx(i), min_cost_class); L = L + Weights(i) * cj; endfor case 'classifcost' Cost = this.Cost; L = 0; ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (this.ClassNames, Y); for i = 1:n y_idx = gYidx(i); y_hat_idx = find (ismember (this.ClassNames, label(i))); L = L + Weights(i) * Cost(y_idx, y_hat_idx); endfor otherwise error (strcat ("CompactClassificationDiscriminant.loss:", ... " invalid loss function.")); endswitch endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationDiscriminant} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationDiscriminant} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationDiscriminant} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margins for discriminant analysis classifier. ## ## @code{@var{m} = margin (@var{obj}, @var{X}, @var{Y})} returns ## the classification margins for @var{obj} with data @var{X} and ## classification @var{Y}. @var{m} is a numeric vector of length size (X,1). ## ## @itemize ## @item ## @code{obj} is a @var{CompactClassificationDiscriminant} object. ## @item ## @code{X} must be a @math{N*P} numeric matrix of input data where rows ## correspond to observations and columns correspond to features or ## variables. ## @item ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels ## of corresponding predictor data in @var{X}. @var{Y} must have same ## numbers of rows as @var{X}. ## @end itemize ## ## The classification margin for each observation is the difference between ## the classification score for the true class and the maximal ## classification score for the false classes. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{fitcdiscr, CompactClassificationDiscriminant} ## @end deftypefn function m = margin (this, X, Y) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error (strcat ("CompactClassificationDiscriminant.margin:", ... " too few input arguments.")); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y] = tableResponse (this, 'margin', X, Y, {}, nargin > 2); ## Validate Y valid_types = {'char', 'string', 'logical', 'single', 'double', ... 'cell', 'categorical'}; if (! (any (strcmp (class (Y), valid_types)))) error (strcat ("CompactClassificationDiscriminant.margin:", ... " Y must be of a valid type.")); endif ## Validate X valid_types = {'single', 'double'}; if (! (any (strcmp (class (X), valid_types)))) error (strcat ("CompactClassificationDiscriminant.margin:", ... " X must be of a valid type.")); endif ## Validate size of Y if (size (Y, 1) != size (X, 1)) error (strcat ("CompactClassificationDiscriminant.margin: Y must", ... " have the same number of rows as X.")); endif ## Bring Y to the type of the classes if (ischar (Y)) Y = cellstr (Y); elseif (isa (Y, 'string')) Y = cellstr (Y); elseif (isnumeric (Y) || islogical (Y)) if (iscellstr (this.ClassNames)) Y = cellstr (num2str (double (Y))); endif elseif (iscell (Y)) Y = cellfun (@num2str, Y, 'UniformOutput', false); elseif (! isa (Y, 'categorical')) ## A categorical is matched to the classes by name as it stands. error (strcat ("CompactClassificationDiscriminant.margin: Y must", ... " be a numeric, logical, char, string, categorical", ... " or cell array.")); endif ## Check if Y contains correct classes if (! labelsKnown (Y, this.ClassNames)) error (strcat ("CompactClassificationDiscriminant.margin: Y must", ... " contain only the classes in ClassNames.")); endif ## Number of Observations n = size (X, 1); ## Initialize the margin vector m = zeros (n, 1); ## Calculate the classification scores [~, scores] = predict (this, X); ## Loop over each observation to compute the margin ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (this.ClassNames, Y); for i = 1:n ## True class index true_class_idx = gYidx(i); ## Score for the true class true_class_score = scores(i, true_class_idx); ## Get the maximal score for the false classes scores(i, true_class_idx) = -Inf; # Temporarily max_false_class_score = max (scores(i, :)); if (max_false_class_score == -Inf) m(i) = NaN; continue; endif scores(i, true_class_idx) = true_class_score; # Restore ## Calculate the margin m(i) = true_class_score - max_false_class_score; endfor endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationDiscriminant} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationDiscriminant} {@var{e} =} edge (@dots{}, @qcode{"Weights"}, @var{w}) ## @deftypefnx {CompactClassificationDiscriminant} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationDiscriminant} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Classification edge, the mean of the classification margins. ## ## @code{@var{e} = edge (@var{obj}, @var{X}, @var{Y})} reduces the vector ## that @code{margin} returns to a single number, the mean margin over the ## rows of @var{X}. It says how far the model puts the true class ahead of ## its nearest rival on average, so a larger edge is a better model, and ## unlike a loss it is not bounded above and rewards confidence rather than ## bare correctness. ## ## @code{@var{e} = edge (@dots{}, @qcode{"Weights"}, @var{w})} takes the ## weighted mean instead, with one weight per row of @var{X}. ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @end deftypefn function e = edge (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error (strcat ("CompactClassificationDiscriminant.edge: too few", ... " input arguments.")); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'edge', X, Y, varargin, ... nargin > 2); ## The weights are parsed before anything is computed, so a bad ## Name-Value pair is reported as such rather than after a margin. W = edgeWeights (varargin, Y, this.ClassNames, this.Prior, ... "CompactClassificationDiscriminant", "edge"); m = margin (this, X, Y); e = sum (W .* m(:)) / sum (W); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationDiscriminant} {@var{M} =} mahal (@var{obj}, @var{X}) ## @deftypefnx {CompactClassificationDiscriminant} {@var{M} =} mahal (@dots{}, @qcode{'ClassLabels'}, @var{labels}) ## ## Squared Mahalanobis distance to the class means. ## ## @code{@var{M} = mahal (@var{obj}, @var{X})} returns an @math{NxK} ## matrix whose element @math{(i,j)} is the squared Mahalanobis distance ## from observation @math{i} to the mean of class @math{j}, measured ## against the covariance that class carries: the one shared covariance ## for a linear discriminant and the class's own for a quadratic one. ## ## @itemize ## @item ## @var{obj} must be a @qcode{CompactClassificationDiscriminant} object. ## @item ## @var{X} must be an @math{NxP} numeric matrix with one column per ## predictor of the trained model, or a table holding them. ## @end itemize ## ## @code{@var{M} = mahal (@dots{}, @qcode{'ClassLabels'}, @var{labels})} ## returns an @math{Nx1} vector instead, holding for each observation the ## distance to the mean of the class @var{labels} names for it. ## @var{labels} must have one entry per row of @var{X}, each of them one ## of @code{ClassNames}. ## ## The distance is measured against the covariance the model reports, so ## a regularized model is measured against its regularized covariance. ## The prior does not enter it. ## ## A table's variables are matched to the predictors the model was ## fitted on by name and not by position: one the model was not fitted on ## is passed over, one it needs and cannot find is named, and a value ## holding a level is coded as that level was coded at fitting. ## ## @end deftypefn function M = mahal (this, X, varargin) ## Check for sufficient input arguments if (nargin < 2) error (strcat ("CompactClassificationDiscriminant.mahal:", ... " too few input arguments.")); endif ## A table is read by the names the model was fitted on X = tableColumns (this, 'CompactClassificationDiscriminant.mahal', X); ## Check for valid X if (isempty (X)) error (strcat ("CompactClassificationDiscriminant.mahal:", ... " X is empty.")); elseif (! (isnumeric (X) && isreal (X))) error (strcat ("CompactClassificationDiscriminant.mahal:", ... " X must be a real numeric matrix.")); elseif (this.NumPredictors != columns (X)) error (strcat ("CompactClassificationDiscriminant.mahal:", ... " X must have the same number of predictors as the", ... " trained model.")); endif if (mod (numel (varargin), 2) != 0) error (strcat ("CompactClassificationDiscriminant.mahal:", ... " Name-Value arguments must be in pairs.")); endif ## Parse optional paired arguments; without 'ClassLabels' every class ## mean is measured [labels, args] = parsePairedArguments ({'ClassLabels'}, {[]}, ... varargin(:)); if (! isempty (args)) error (strcat ("CompactClassificationDiscriminant.mahal: invalid optional", ... " paired argument.")); endif M = discrimmahal (X, this.Mu, this.Sigma, this.DiscrimType); if (isempty (labels)) return; endif ## One distance per observation, to the mean of the class named for it, ## the labels matched to the classes as every other method matches them if (! (isnumeric (labels) || islogical (labels) || ischar (labels) || iscellstr (labels) || isa (labels, 'categorical') || isa (labels, 'string'))) error (strcat ("CompactClassificationDiscriminant.mahal:", ... " 'ClassLabels' must be of a valid type.")); endif if (ischar (labels)) nL = rows (labels); else nL = numel (labels); endif if (nL != rows (X)) error (strcat ("CompactClassificationDiscriminant.mahal:", ... " 'ClassLabels' must have one entry per row of X.")); endif [idx, errmsg] = labelIndices (this.ClassNames, labels); if (! isempty (errmsg)) error (strcat ("CompactClassificationDiscriminant.mahal:", ... " every 'ClassLabels' entry must be one of", ... " ClassNames.")); endif M = M(sub2ind (size (M), (1:rows (X))', idx)); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationDiscriminant} {@var{lp} =} logp (@var{obj}, @var{X}) ## ## Log unconditional probability density of the observations. ## ## @code{@var{lp} = logp (@var{obj}, @var{X})} returns an @math{Nx1} ## vector holding, for each row of @var{X}, the natural logarithm of ## @math{P(x) = sum_k P(k) P(x|k)}, the density of the observation summed ## over the classes with each class weighted by its prior @math{P(k)}. ## Each @math{P(x|k)} is the multivariate normal density of class ## @math{k}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{CompactClassificationDiscriminant} object. ## @item ## @var{X} must be an @math{NxP} numeric matrix with one column per ## predictor of the trained model, or a table holding them. ## @end itemize ## ## An unusually low value marks an observation the model finds unlikely ## under every class, which is what makes this an outlier test rather ## than a classification. ## ## A table's variables are matched to the predictors the model was ## fitted on by name and not by position: one the model was not fitted on ## is passed over, one it needs and cannot find is named, and a value ## holding a level is coded as that level was coded at fitting. ## ## @end deftypefn function lp = logp (this, X) ## Check for sufficient input arguments if (nargin < 2) error (strcat ("CompactClassificationDiscriminant.logp:", ... " too few input arguments.")); endif ## A table is read by the names the model was fitted on X = tableColumns (this, 'CompactClassificationDiscriminant.logp', X); ## Check for valid X if (isempty (X)) error (strcat ("CompactClassificationDiscriminant.logp:", ... " X is empty.")); elseif (! (isnumeric (X) && isreal (X))) error (strcat ("CompactClassificationDiscriminant.logp:", ... " X must be a real numeric matrix.")); elseif (this.NumPredictors != columns (X)) error (strcat ("CompactClassificationDiscriminant.logp:", ... " X must have the same number of predictors as the", ... " trained model.")); endif lp = discrimlogp (X, this.Mu, this.Sigma, this.LogDetSigma, ... this.Prior, this.DiscrimType); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationDiscriminant} {} savemodel (@var{obj}, @var{filename}) ## ## Save a CompactClassificationDiscriminant object. ## ## @code{savemodel (@var{obj}, @var{filename})} saves each property of a ## CompactClassificationDiscriminant object into an Octave binary file, the ## name of which is specified in @var{filename}, along with an extra ## variable, which defines the type classification object these variables ## constitute. Use @code{loadmodel} in order to load a classification object ## into Octave's workspace. ## ## @seealso{loadmodel, fitcdiscr, ClassificationDiscriminant} ## @end deftypefn function savemodel (this, fname) if (nargin < 2) error ("CompactClassificationDiscriminant.savemodel: too few input arguments."); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error ("CompactClassificationDiscriminant.savemodel: FNAME must be a character vector."); endif ## Generate variable for class name classdef_name = 'CompactClassificationDiscriminant'; ## Create variables from model properties NumPredictors = this.NumPredictors; PredictorNames = this.PredictorNames; ResponseName = this.ResponseName; ClassNames = encodeLabels (this.ClassNames); Prior = this.Prior; Cost = this.Cost; ScoreTransform = this.ScoreTransform; STfun = this.STfun; BetweenSigma = this.BetweenSigma; CategoricalPredictors = this.CategoricalPredictors; ExpandedPredictorNames = this.ExpandedPredictorNames; Sigma = this.Sigma; BaseSigma = this.BaseSigma; Mu = this.Mu; Coeffs = encodeLabels (this.Coeffs); Delta = this.Delta; DiscrimType = this.DiscrimType; Gamma = this.Gamma; MinGamma = this.MinGamma; LogDetSigma = this.LogDetSigma; ## Save classdef name and all model properties as individual variables save ('-binary', fname, 'classdef_name', 'NumPredictors', ... 'PredictorNames', 'ResponseName', 'ClassNames', 'Prior', ... 'Cost', 'ScoreTransform', 'STfun', 'Sigma', 'BaseSigma', ... 'Mu', 'Coeffs', ... 'Delta', 'DiscrimType', 'Gamma', 'MinGamma', 'LogDetSigma', ... 'BetweenSigma', 'CategoricalPredictors', ... 'ExpandedPredictorNames'); endfunction endmethods methods(Static, Hidden) function mdl = load_model (filename, data) data = decodeLabels (data); ## Create a CompactClassificationDiscriminant object mdl = CompactClassificationDiscriminant (); ## Copy the saved data into the object. Iterate over what was ## saved rather than over fieldnames (mdl): a private property such ## as STfun is written out by savemodel but is not reported by ## fieldnames, so comparing the two sets could never match and every ## load failed. Assignment is legal here because this is a method of ## the class itself. names = fieldnames (data); ## The set methods for these read other properties, and some of them ## rebuild Sigma or Coeffs, so they are assigned once everything else is ## in place rather than in the order the file happens to list them. ## Order matters within the late group as well: Prior and Cost before ## DiscrimType, because assigning the type rebuilds Coeffs and Coeffs ## reads the priors; and DiscrimType before Gamma, because assigning the ## type implies a Gamma and would overwrite the saved one if it came ## second. Coeffs is installed last of all: the set methods rebuild it ## whenever it is already there, and rebuilding it from a half-loaded ## model is how a diagonal Sigma met a linear formula. order = {'Cost', 'Prior', 'ScoreTransform', 'ResponseTransform', ... 'DiscrimType', 'Gamma', 'Delta', 'Coeffs'}; late = ismember (names, order); tail = {}; for k = 1:numel (order) if (any (strcmp (names, order{k}))) tail{end+1} = order{k}; endif endfor names = [names(! late); tail(:)]; for i = 1:numel (names) try mdl.(names{i}) = data.(names{i}); catch msg = strcat ("CompactClassificationDiscriminant.load_model:", ... " invalid model in '%s'."); error (msg, filename); end_try_catch endfor endfunction endmethods endclassdef %!demo %! ## Create a discriminant analysis classifier and its compact version %! # and compare their size %! %! load fisheriris %! X = meas; %! Y = species; %! %! Mdl = fitcdiscr (X, Y, 'ClassNames', unique (species)) %! CMdl = crossval (Mdl) ## Test constructor %!test %! load fisheriris %! x = meas; %! y = species; %! PredictorNames = {'Sepal Length', 'Sepal Width', 'Petal Length', 'Petal Width'}; %! Mdl = fitcdiscr (x, y, 'PredictorNames', PredictorNames); %! CMdl = compact (Mdl); %! sigma = [0.265008, 0.092721, 0.167514, 0.038401; ... %! 0.092721, 0.115388, 0.055244, 0.032710; ... %! 0.167514, 0.055244, 0.185188, 0.042665; ... %! 0.038401, 0.032710, 0.042665, 0.041882]; %! mu = [5.0060, 3.4280, 1.4620, 0.2460; ... %! 5.9360, 2.7700, 4.2600, 1.3260; ... %! 6.5880, 2.9740, 5.5520, 2.0260]; %! xCentered = [ 9.4000e-02, 7.2000e-02, -6.2000e-02, -4.6000e-02; ... %! -1.0600e-01, -4.2800e-01, -6.2000e-02, -4.6000e-02; ... %! -3.0600e-01, -2.2800e-01, -1.6200e-01, -4.6000e-02]; %! assert_equal (class (CMdl), "CompactClassificationDiscriminant"); %! assert_equal ({CMdl.DiscrimType, CMdl.ResponseName}, {'linear', 'Y'}) %! assert_equal ({CMdl.Gamma, CMdl.MinGamma}, {0, 0}, 1e-15) %! assert_equal (CMdl.ClassNames, unique (species)) %! assert_equal (CMdl.Sigma, sigma, 1e-6) %! assert_equal (CMdl.Mu, mu, 1e-14) %! assert_equal (CMdl.LogDetSigma, -9.9585, 1e-4) %! assert_equal (CMdl.PredictorNames, PredictorNames) %!test %! load fisheriris %! x = meas; %! y = species; %! Mdl = fitcdiscr (x, y, 'Gamma', 0.5); %! CMdl = compact (Mdl); %! sigma = [0.265008, 0.046361, 0.083757, 0.019201; ... %! 0.046361, 0.115388, 0.027622, 0.016355; ... %! 0.083757, 0.027622, 0.185188, 0.021333; ... %! 0.019201, 0.016355, 0.021333, 0.041882]; %! mu = [5.0060, 3.4280, 1.4620, 0.2460; ... %! 5.9360, 2.7700, 4.2600, 1.3260; ... %! 6.5880, 2.9740, 5.5520, 2.0260]; %! xCentered = [ 9.4000e-02, 7.2000e-02, -6.2000e-02, -4.6000e-02; ... %! -1.0600e-01, -4.2800e-01, -6.2000e-02, -4.6000e-02; ... %! -3.0600e-01, -2.2800e-01, -1.6200e-01, -4.6000e-02]; %! assert_equal (class (CMdl), "CompactClassificationDiscriminant"); %! assert_equal ({CMdl.DiscrimType, CMdl.ResponseName}, {'linear', 'Y'}) %! assert_equal ({CMdl.Gamma, CMdl.MinGamma}, {0.5, 0}) %! assert_equal (CMdl.ClassNames, unique (species)) %! assert_equal (CMdl.Sigma, sigma, 1e-6) %! assert_equal (CMdl.Mu, mu, 1e-14) %! assert_equal (CMdl.LogDetSigma, -8.6884, 1e-4) ## Test input validation for constructor ## nLinearCoeffs counts the predictors a threshold keeps. R2024a on ## fisheriris reports 4 at the model's own Delta of zero. %!test %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! assert_equal (nLinearCoeffs (Mdl), 4); ## The comparison includes equality, which only a threshold sitting exactly ## on a DeltaPredictor can show: R2024a returns 4, 3, 2, 1 at the four ## sorted values, where a strict comparison would return 3, 2, 1, 0. %!test %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! dps = sort (Mdl.DeltaPredictor); %! assert_equal (nLinearCoeffs (Mdl, dps), [4; 3; 2; 1]); ## A threshold above every DeltaPredictor keeps nothing, and the result is a ## column however the thresholds were shaped. %!test %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! assert_equal (nLinearCoeffs (Mdl, [0, 1e6]), [4; 0]); %! assert_equal (size (nLinearCoeffs (Mdl, [0, 1, 2])), [3, 1]); ## The count is taken whatever the DiscrimType, as MATLAB takes it, even ## though Delta regularizes the linear types alone. %!test %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species, "DiscrimType", "quadratic")); %! assert_equal (nLinearCoeffs (Mdl), 4); ## A compact model keeps the character class names and predicts whole names. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Cc = compact (fitcdiscr (Xch, Ych)); %! rand ("state", 1); randn ("state", 1); Cs = compact (fitcdiscr (Xch, Ycell)); %! assert_equal (cellstr (Cc.ClassNames), Cs.ClassNames); %! assert_equal (cellstr (predict (Cc, Xch)), predict (Cs, Xch)); ## and reads one back in its assessment methods. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Cc = compact (fitcdiscr (Xch, Ych)); %! rand ("state", 1); randn ("state", 1); Cs = compact (fitcdiscr (Xch, Ycell)); %! assert_equal (loss (Cc, Xch, Ych), loss (Cs, Xch, Ycell), 1e-12); ## loss and margin match labels to the classes by name, whatever their type. %!test %! load fisheriris %! X = meas(51:150, 1:2); %! ys = species(51:150); %! yc = categorical (ys); %! C = compact (fitcdiscr (X, yc)); %! assert_equal (loss (C, X, ys), 0.25, 1e-15); %! assert_equal (loss (C, X, yc), 0.25, 1e-15); %! L = loss (C, X, string (ys), 'LossFun', 'classiferror'); %! assert_equal (L, 0.25, 1e-15); %! assert_equal (margin (C, X, yc), margin (C, X, ys), 1e-15); %!error ... %! load fisheriris %! nLinearCoeffs (compact (fitcdiscr (meas, species)), "a") %!error ... %! CompactClassificationDiscriminant (1) ## Test predict method %!test %! load fisheriris %! x = meas; %! y = species; %! Mdl = fitcdiscr (meas, species, 'Gamma', 0.5); %! CMdl = compact (Mdl); %! [label, score, cost] = predict (CMdl, [2, 2, 2, 2]); %! assert_equal (label, {'versicolor'}) %! assert_equal (score, [0, 0.9999, 0.0001], 1e-4) %! assert_equal (cost, [1, 0.0001, 0.9999], 1e-4) %! [label, score, cost] = predict (CMdl, [2.5, 2.5, 2.5, 2.5]); %! assert_equal (label, {'versicolor'}) %! assert_equal (score, [0, 0.6368, 0.3632], 1e-4) %! assert_equal (cost, [1, 0.3632, 0.6368], 1e-4) %!test %! load fisheriris %! x = meas; %! y = species; %! xc = [min(x); mean(x); max(x)]; %! Mdl = fitcdiscr (x, y); %! CMdl = compact (Mdl); %! [label, score, cost] = predict (CMdl, xc); %! l = {'setosa'; 'versicolor'; 'virginica'}; %! s = [1, 0, 0; 0, 1, 0; 0, 0, 1]; %! c = [0, 1, 1; 1, 0, 1; 1, 1, 0]; %! assert_equal (label, l) %! assert_equal (score, s, 1e-4) %! assert_equal (cost, c, 1e-4) %!shared MODEL %! X = rand (10,2); %! Y = [ones(5,1);2*ones(5,1)]; %! MODEL = compact (ClassificationDiscriminant (X, Y)); ## Test input validation for predict method %!error ... %! predict (MODEL) %!error ... %! predict (MODEL, []) %!error ... %! predict (MODEL, 1) ## Test loss method %!test %! load fisheriris %! model = fitcdiscr (meas, species); %! x = mean (meas); %! y = {'versicolor'}; %! L = loss (model, x, y); %! assert_equal (L, 0) %!test %! x = [1, 2; 3, 4; 5, 6]; %! y = {'A'; 'B'; 'A'}; %! model = fitcdiscr (x, y, 'Gamma', 0.4); %! x_test = [1, 6; 3, 3]; %! y_test = {'A'; 'B'}; %! L = loss (model, x_test, y_test); %! assert_equal (L, 0.3333, 1e-4) %!test %! x = [1, 2; 3, 4; 5, 6; 7, 8]; %! y = ['1'; '2'; '3'; '1']; %! model = fitcdiscr (x, y, 'gamma' , 0.5); %! x_test = [3, 3]; %! y_test = ['1']; %! L = loss (model, x_test, y_test, 'LossFun', 'quadratic'); %! assert_equal (L, 0.2423, 1e-4) %!test %! x = [1, 2; 3, 4; 5, 6; 7, 8]; %! y = ['1'; '2'; '3'; '1']; %! model = fitcdiscr (x, y, 'gamma' , 0.5); %! x_test = [3, 3; 5, 7]; %! y_test = ['1'; '2']; %! L = loss (model, x_test, y_test, 'LossFun', 'classifcost'); %! assert_equal (L, 0.3333, 1e-4) %!test %! x = [1, 2; 3, 4; 5, 6; 7, 8]; %! y = ['1'; '2'; '3'; '1']; %! model = fitcdiscr (x, y, 'gamma' , 0.5); %! x_test = [3, 3; 5, 7]; %! y_test = ['1'; '2']; %! L = loss (model, x_test, y_test, 'LossFun', 'hinge'); %! assert_equal (L, 0.5886, 1e-4) %!test %! x = [1, 2; 3, 4; 5, 6; 7, 8]; %! y = ['1'; '2'; '3'; '1']; %! model = fitcdiscr (x, y, 'gamma' , 0.5); %! x_test = [3, 3; 5, 7]; %! y_test = ['1'; '2']; %! W = [1; 2]; %! L = loss (model, x_test, y_test, 'LossFun', 'logit', 'Weights', W); %! assert_equal (L, 0.5107, 1e-4) %!test %! x = [1, 2; 3, 4; 5, 6]; %! y = {'A'; 'B'; 'A'}; %! model = fitcdiscr (x, y, 'gamma' , 0.5); %! x_with_nan = [1, 2; NaN, 4]; %! y_test = {'A'; 'B'}; %! L = loss (model, x_with_nan, y_test); %! assert_equal (L, 0.3333, 1e-4) %!test %! x = [1, 2; 3, 4; 5, 6]; %! y = {'A'; 'B'; 'A'}; %! model = fitcdiscr (x, y); %! x_with_nan = [1, 2; NaN, 4]; %! y_test = {'A'; 'B'}; %! L = loss (model, x_with_nan, y_test, 'LossFun', 'logit'); %! assert_equal (isnan (L), true) %!test %! x = [1, 2; 3, 4; 5, 6]; %! y = {'A'; 'B'; 'A'}; %! model = fitcdiscr (x, y); %! customLossFun = @(C, S, W, Cost) sum (W .* sum (abs (C - S), 2)); %! L = loss (model, x, y, 'LossFun', customLossFun); %! assert_equal (L, 0.8889, 1e-4) %!test %! x = [1, 2; 3, 4; 5, 6]; %! y = [1; 2; 1]; %! model = fitcdiscr (x, y); %! L = loss (model, x, y, 'LossFun', 'classiferror'); %! assert_equal (L, 0.3333, 1e-4) ## Test input validation for loss method %!error ... %! loss (MODEL) %!error ... %! loss (MODEL, ones (4,2)) %!error ... %! loss (MODEL, ones (4,2), ones (4,1), 'LossFun') %!error ... %! loss (MODEL, ones (4,2), ones (4,1), 'Bogus', 1) %!error ... %! loss (MODEL, ones (4,2), ones (3,1)) %!error ... %! loss (MODEL, ones (4,2), ones (4,1), 'LossFun', 'a') %!error ... %! loss (MODEL, ones (4,2), ones (4,1), 'Weights', 'w') %!error ... %! loss (MODEL, ones (4,2), ones (4,1), 'Weights', ones (2, 2)) ## Test margin method %! load fisheriris %! mdl = fitcdiscr (meas, species); %! X = mean (meas); %! Y = {'versicolor'}; %! m = margin (mdl, X, Y); %! assert_equal (m, 1, 1e-6) %!test %! X = [1, 2; 3, 4; 5, 6]; %! Y = [1; 2; 1]; %! mdl = fitcdiscr (X, Y, 'gamma', 0.5); %! m = margin (mdl, X, Y); %! assert_equal (m, [0.3333; -0.3333; 0.3333], 1e-4) ## Test input validation for margin method %!error ... %! margin (MODEL) %!error ... %! margin (MODEL, ones (4,2)) %!error ... %! margin (MODEL, ones (4,2), ones (3,1)) %!error ... %! savemodel (CompactClassificationDiscriminant ()) %!error ... %! savemodel (CompactClassificationDiscriminant (), 1) %!error ... %! savemodel (CompactClassificationDiscriminant (), ['ab'; 'cd']) ## A ScoreTransform can be assigned, and is stored as a function handle. %!test %! load fisheriris %! CMdl = compact (fitcdiscr (meas, species)); %! CMdl.ScoreTransform = 'symmetric'; %! assert_equal (class (CMdl.ScoreTransform), 'char'); %! assert_equal (CMdl.ScoreTransform, 'symmetric'); ## Prior and Cost are settable on the compact model, as they are on the full ## one, and an unnormalized prior is rescaled. %!test %! load fisheriris %! CMdl = compact (fitcdiscr (meas, species)); %! CMdl.Prior = [2, 3, 5]; %! assert_equal (CMdl.Prior, [0.2, 0.3, 0.5], 1e-14); %! CMdl.Cost = [0, 2, 3; 1, 0, 1; 1, 1, 0]; %! assert_equal (CMdl.Cost, [0, 2, 3; 1, 0, 1; 1, 1, 0]); ## A fitted model survives savemodel and loadmodel: the properties come ## back as they were and it predicts the same. %!test %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2), 'CompactClassificationDiscriminant'); %! assert_equal (M2.PredictorNames, Mdl.PredictorNames); %! assert_equal (class (M2.ScoreTransform), class (Mdl.ScoreTransform)); %! assert_equal (predict (M2, meas(1:5,:)), predict (Mdl, meas(1:5,:))); ## The compact model carries the discriminant type and answers identically to ## the full one, for every type. Values measured on MATLAB R2024a; see ## DISCRIMINANT_LEDGER.md. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'quadratic'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.DiscrimType, 'quadratic'); %! assert_equal (size (CMdl.Sigma), [4, 4, 3]); %! assert_equal (predict (CMdl, meas), predict (Mdl, meas)); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'diagQuadratic'); %! CMdl = compact (Mdl); %! assert_equal (size (CMdl.Sigma), [1, 4, 3]); %! assert_equal (predict (CMdl, meas), predict (Mdl, meas)); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'pseudoLinear'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.LogDetSigma, -9.9585, 1e-4); %! assert_equal (predict (CMdl, meas), predict (Mdl, meas)); ## The type is assignable on a compact model too, under the same family rule, ## which it can only do because the fit's covariance came across with it. %!test %! load fisheriris %! CMdl = compact (fitcdiscr (meas, species, 'DiscrimType', 'quadratic')); %! CMdl.DiscrimType = 'diagQuadratic'; %! assert_equal (size (CMdl.Sigma), [1, 4, 3]); %! assert_equal (CMdl.Gamma, 1); %!test %! load fisheriris %! CMdl = compact (fitcdiscr (meas, species)); %! CMdl.Gamma = 1; %! assert_equal (CMdl.DiscrimType, 'diagLinear'); %! assert_equal (size (CMdl.Sigma), [1, 4]); %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'DiscrimType', 'diagQuadratic'); %! CMdl = compact (Mdl); %! fname = tempname (); %! savemodel (CMdl, fname); %! C2 = loadmodel (fname); %! delete (fname); %! assert_equal (predict (C2, meas), predict (CMdl, meas)); %! C2.DiscrimType = 'quadratic'; %! assert_equal (size (C2.Sigma), [4, 4, 3]); %!error ... %! load fisheriris; ... %! CMdl = compact (fitcdiscr (meas, species, 'DiscrimType', 'quadratic')); ... %! CMdl.DiscrimType = 'linear'; %!error ... %! load fisheriris; ... %! CMdl = compact (fitcdiscr (meas, species, 'DiscrimType', 'quadratic')); ... %! CMdl.Gamma = 0.5; %!error ... %! load fisheriris; ... %! CMdl = compact (fitcdiscr (meas, species, 'DiscrimType', 'quadratic')); ... %! CMdl.Delta = 0.5; %!error ... %! load fisheriris; ... %! CMdl = compact (fitcdiscr (meas, species)); CMdl.Delta = -1; ## edge, measured on MATLAB R2024a, whose discriminant scores this class ## reproduces exactly. %!test %! load fisheriris %! CMdl = compact (fitcdiscr (meas, species)); %! assert_equal (edge (CMdl, meas, species), 0.9454289377, 1e-9); %! assert_equal (edge (CMdl, meas, species), ... %! mean (margin (CMdl, meas, species)), 1e-12); ## Weights are normalized within each class to that class's prior. %!test %! load fisheriris %! CMdl = compact (fitcdiscr (meas, species)); %! assert_equal (edge (CMdl, meas, species, 'Weights', (1:150)'), ... %! 0.9438468986, 1e-9); %!error ... %! load fisheriris; edge (compact (fitcdiscr (meas, species)), meas) ## Both carry across to the compact form, and survive a round trip. %!test %! load fisheriris %! CMdl = compact (fitcdiscr (meas, species)); %! assert_equal (CMdl.CategoricalPredictors, []); %! assert_equal (CMdl.ExpandedPredictorNames, CMdl.PredictorNames); %! assert_equal (size (CMdl.ExpandedPredictorNames), [1, 4]); %!test %! load fisheriris %! CMdl = compact (fitcdiscr (meas, species)); %! fname = tempname (); %! savemodel (CMdl, fname); %! C2 = loadmodel (fname); %! delete (fname); %! assert_equal (C2.CategoricalPredictors, CMdl.CategoricalPredictors); %! assert_equal (C2.ExpandedPredictorNames, CMdl.ExpandedPredictorNames); ## BetweenSigma comes across with the compact form and survives a round trip. %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species); %! CMdl = compact (Mdl); %! assert_equal (CMdl.BetweenSigma, Mdl.BetweenSigma); %! assert_equal (CMdl.BetweenSigma(1,1), 0.632121333333333, 1e-12); %!test %! load fisheriris %! CMdl = compact (fitcdiscr (meas, species)); %! fname = tempname (); %! savemodel (CMdl, fname); %! C2 = loadmodel (fname); %! delete (fname); %! assert_equal (C2.BetweenSigma, CMdl.BetweenSigma); %!error ... %! load fisheriris %! CMdl = compact (fitcdiscr (meas, species)); %! CMdl.Cost = 1:9; ## mahal is the squared distance to every class mean. Measured on R2024a. %!test %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! M = mahal (Mdl, meas(1:5,:)); %! assert_equal (size (M), [5, 3]); %! assert_equal (M(1,:), [0.2910898404344, 98.8847494279393, ... %! 191.788642179719], 1e-10); %! assert_equal (M(5,:), [0.5956300391717, 100.923170228959, ... %! 193.854037009331], 1e-10); ## 'ClassLabels' picks one class mean per observation. %!test %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! M = mahal (Mdl, meas(1:5,:), 'ClassLabels', species(1:5)); %! assert_equal (size (M), [5, 1]); %! assert_equal (M, [0.291089840434356; 2.031345104042097; ... %! 0.553281423559203; 2.086697905677364; ... %! 0.595630039171744], 1e-12); ## 'ClassLabels' of any label type, whatever type the classes are. %!test %! load fisheriris %! idx = [1; 51; 101]; %! D = diag (mahal (compact (fitcdiscr (meas, species)), meas(idx,:))); %! Mdl = compact (fitcdiscr (meas, categorical (species))); %! assert_equal (mahal (Mdl, meas(idx,:), 'ClassLabels', species(idx)), D); %! assert_equal (mahal (Mdl, meas(idx,:), 'ClassLabels', ... %! categorical (species(idx))), D); %! assert_equal (mahal (Mdl, meas(idx,:), 'ClassLabels', ... %! string (species(idx))), D); %! assert_equal (mahal (Mdl, meas(idx,:), 'ClassLabels', ... %! char (species(idx))), D); %!test %! load fisheriris %! idx = [1; 51; 101]; %! D = diag (mahal (compact (fitcdiscr (meas, species)), meas(idx,:))); %! Mdl = compact (fitcdiscr (meas, species)); %! assert_equal (mahal (Mdl, meas(idx,:), 'ClassLabels', ... %! categorical (species(idx))), D); %! assert_equal (mahal (Mdl, meas(idx,:), 'ClassLabels', ... %! string (species(idx))), D); %!test %! load fisheriris %! idx = [1; 51; 101]; %! Mdl = compact (fitcdiscr (meas, grp2idx (species))); %! D = diag (mahal (Mdl, meas(idx,:))); %! assert_equal (mahal (Mdl, meas(idx,:), 'ClassLabels', [1; 2; 3]), D); ## A quadratic model measures against each class's own covariance. %!test %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species, 'DiscrimType', 'quadratic')); %! M = mahal (Mdl, meas(1:5,:)); %! assert_equal (M(1,:), [0.4491137892273, 114.804489260461, ... %! 182.935908699285], 1e-10); ## The distance follows the regularized covariance the model reports. %!test %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species, 'Gamma', 0.5)); %! M = mahal (Mdl, meas(1:5,:)); %! assert_equal (M(1,:), [0.1926136709121, 79.4757202663154, ... %! 163.294484902038], 1e-10); ## The prior does not enter the distance. %!test %! load fisheriris %! M1 = mahal (compact (fitcdiscr (meas, species)), meas(1:5,:)); %! Mdl = compact (fitcdiscr (meas, species, 'Prior', [0.6, 0.2, 0.2])); %! assert_equal (mahal (Mdl, meas(1:5,:)), M1, 1e-12); ## logp is the log density summed over the classes at their priors. %!test %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! lp = logp (Mdl, meas(1:5,:)); %! assert_equal (size (lp), [5, 1]); %! assert_equal (lp, [0.059358043320009; -0.810769588483861; ... %! -0.071737748242414; -0.838445989301495; ... %! -0.092912056048684], 1e-12); ## logp on a quadratic model. %!test %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species, 'DiscrimType', 'quadratic')); %! lp = logp (Mdl, meas(1:5,:)); %! assert_equal (lp, [1.534756847193468; 0.718766664165731; ... %! 1.117146185488189; 0.906210252665867; ... %! 1.378471050607217], 1e-12); ## The prior reweights the density, unlike the distance. %!test %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species, 'Prior', [0.6, 0.2, 0.2])); %! lp = logp (Mdl, meas(1:5,:)); %! assert_equal (lp, [0.647144708222129; -0.222982923581741; ... %! 0.516048916659706; -0.250659324399375; ... %! 0.494874608853435], 1e-12); %!error ... %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! mahal (Mdl, []) %!error ... %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! mahal (Mdl, ones (3, 2)) %!error ... %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! mahal (Mdl, {1, 2, 3, 4}) %!error ... %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! mahal (Mdl, meas(1:5,:), 'ClassLabels') %!error ... %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! mahal (Mdl, meas(1:5,:), 5, 1) %!error ... %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! mahal (Mdl, meas(1:5,:), 'bogus', 1) %!error ... %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! mahal (Mdl, meas(1:2,:), 'ClassLabels', {1; 2}) %!error ... %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! mahal (Mdl, meas(1:5,:), 'ClassLabels', species(1:3)) %!error ... %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! mahal (Mdl, meas(1:2,:), 'ClassLabels', {'setosa'; 'nosuchspecies'}) %!error ... %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! logp (Mdl, []) %!error ... %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! logp (Mdl, ones (3, 2)) %!error ... %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! logp (Mdl, {1, 2, 3, 4}) ## The shared cost guard is in force here too, and the struct form is ## permuted into this model's class order. The battery is on ## ClassificationDiscriminant. %!test %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! S = struct ('ClassNames', {{'virginica'; 'setosa'; 'versicolor'}}, ... %! 'ClassificationCosts', [0, 1, 2; 3, 0, 4; 5, 6, 0]); %! Mdl.Cost = S; %! assert_equal (Mdl.Cost, [0, 4, 3; 6, 0, 5; 1, 2, 0]); %!error ... %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! Mdl.Cost = ones (3); ## Every documented score transform reaches the scores that are reported, and ## none of them moves the label: a transform reshapes what is reported, not ## what is decided. %!test %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! T = {'identity', @(x) x; 'doublelogit', @(x) 1 ./ (1 + exp (-2 * x)); ... %! 'invlogit', @(x) log (x ./ (1 - x)); ... %! 'logit', @(x) 1 ./ (1 + exp (-x)); ... %! 'sign', @(x) sign (x); 'symmetric', @(x) 2 * x - 1; ... %! 'symmetriclogit', @(x) 2 ./ (1 + exp (-x)) - 1}; %! for i = 1:rows (T) %! Mdl.ScoreTransform = T{i,1}; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, T{i,2}(raw), 1e-12); %! assert_equal (l, label); %! endfor %! ## ismax marks the largest score of each observation, ties to the first. %! [~, k] = max (raw, [], 2); %! e = zeros (size (raw)); %! e(sub2ind (size (raw), (1:rows (raw))', k)) = 1; %! Mdl.ScoreTransform = 'ismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, e); %! Mdl.ScoreTransform = 'symmetricismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, 2 * e - 1); ## A function handle is taken as given and applied to the scores. %!test %! load fisheriris %! Mdl = compact (fitcdiscr (meas, species)); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! Mdl.ScoreTransform = @(x) x .^ 2; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, raw .^ 2, 1e-12); %! assert_equal (l, label); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcdiscr (T, 'Species')); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'Species'), a); %! assert_equal (loss (Mdl, T), a); ## A table at edge %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcdiscr (T, 'Species')); %! a = edge (Mdl, X, y); %! assert_equal (edge (Mdl, T(:,1:2), y), a); %! assert_equal (edge (Mdl, T, 'Species'), a); %! assert_equal (edge (Mdl, T), a); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcdiscr (T, 'Species')); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## A table at mahal and logp %!test # a table is matched to the predictors by name %! load fisheriris %! T = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species); %! Mdl = compact (fitcdiscr (T, 'Species')); %! a = mahal (Mdl, meas); %! assert_equal (mahal (Mdl, T(:,1:4)), a); %! assert_equal (mahal (Mdl, T(:,[5, 4, 2, 3, 1])), a); %!test # a table is matched to the predictors by name %! load fisheriris %! T = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species); %! Mdl = compact (fitcdiscr (T, 'Species')); %! a = logp (Mdl, meas); %! assert_equal (logp (Mdl, T(:,1:4)), a); %! assert_equal (logp (Mdl, T(:,[5, 4, 2, 3, 1])), a); ## A query with a missing value has no posterior %!test %! X = [1 2; 2 1; 2 3; 3 2; 6 7; 7 6; 7 8; 8 7; 1 7; 2 8; 2 6; 3 7]; %! Y = [1 1 1 1 2 2 2 2 3 3 3 3]'; %! Mdl = compact (fitcdiscr (X, Y, 'Prior', [0.2 0.5 0.3], ... %! 'Cost', [0 9 1; 1 0 1; 1 9 0])); %! [label, score, cost] = predict (Mdl, [NaN 2; 2 NaN; Inf 2; 2 2]); %! assert_equal (label, [2; 2; 2; 1]); %! assert_equal (isnan (score), [true(3); false(1, 3)]); %! assert_equal (isnan (cost), [true(3); false(1, 3)]); %!test %! X = [1 2; 2 1; 2 3; 3 2; 6 7; 7 6; 7 8; 8 7; 1 7; 2 8; 2 6; 3 7]; %! Y = [1 1 1 1 2 2 2 2 3 3 3 3]'; %! Mdl = compact (fitcdiscr (X, Y, 'Prior', [0.2 0.5 0.3])); %! q = [NaN 2; 2 NaN; NaN NaN; 2 2]; %! assert_equal (loss (Mdl, q, [1; 2; 3; 1]), 0.4, 1e-14); %! assert_equal (margin (Mdl, q, [1; 2; 3; 1]), ... %! [NaN; NaN; NaN; 0.999999978417601], 1e-12); %! assert_equal (edge (Mdl, q, [1; 2; 3; 1]), NaN); statistics-release-2.1.2/inst/Machine_Learning/CompactClassificationECOC.m000066400000000000000000001101521527311104300265330ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef CompactClassificationECOC < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} CompactClassificationECOC ## ## A multiclass model built from binary learners, without its training data. ## ## A @code{CompactClassificationECOC} carries the binary learners of an error ## correcting output codes model and the coding matrix that says what each of ## them was trained to tell apart, and nothing else: the predictor data, the ## labels and the weights are gone, so it predicts and scores new data but ## cannot be refitted or cross validated. ## ## It comes from @code{compact} on a @code{ClassificationECOC}, and from ## @code{fitcecoc} itself when the binary learners are linear or kernel ## classifiers, which carry no training data of their own. ## ## @seealso{fitcecoc, ClassificationECOC, designecoc} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CompactClassificationECOC} {property} BinaryLearners ## ## The trained binary learners ## ## A cell column with one trained model per column of ## @code{CodingMatrix}, each telling the classes that column marks +1 ## from those it marks -1. This property is read-only. ## ## @end deftp BinaryLearners = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationECOC} {property} CodingMatrix ## ## The coding design ## ## A @math{KxL} matrix of -1, 0 and +1 with one row per class and one ## column per binary learner. A class marked 0 took no part in that ## learner. This property is read-only. ## ## @end deftp CodingMatrix = []; ## -*- texinfo -*- ## @deftp {CompactClassificationECOC} {property} LearnerWeights ## ## The weight each binary learner was trained on ## ## A row with one element per binary learner, the total observation ## weight of the classes that learner took part in. This property is ## read-only. ## ## @end deftp LearnerWeights = []; ## -*- texinfo -*- ## @deftp {CompactClassificationECOC} {property} ClassNames ## ## The class labels ## ## The distinct labels seen in the training data, in the order the rows ## of @code{CodingMatrix}, @code{Prior} and @code{Cost} take them. This ## property is read-only. ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {CompactClassificationECOC} {property} PredictorNames ## ## The names of the predictors, one per column of the training data. ## This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationECOC} {property} ExpandedPredictorNames ## ## The names of the predictors as the learners saw them. It differs from ## @code{PredictorNames} only where a categorical predictor was expanded, ## which this package does not do. This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationECOC} {property} CategoricalPredictors ## ## The columns of the training data that held categorical predictors, ## empty when none did. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {CompactClassificationECOC} {property} ResponseName ## ## The name of the response variable. This property is read-only. ## ## @end deftp ResponseName = 'Y'; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {CompactClassificationECOC} {property} BinaryLoss ## ## The loss that turns a binary learner's score into a cost ## ## One of @qcode{'binodeviance'}, @qcode{'exponential'}, ## @qcode{'hamming'}, @qcode{'hinge'}, @qcode{'linear'}, ## @qcode{'logit'} or @qcode{'quadratic'}. ## ## A learner scoring on @math{(-Inf,+Inf)} takes every one of them but ## @qcode{'quadratic'}, and one scoring on @math{[0,1]} takes only ## @qcode{'hamming'} and @qcode{'quadratic'}: a loss reads a score ## against the interval it was written for, and the other way round it ## would read a posterior as a signed score. Assigning one the learners ## cannot take raises. ## ## @end deftp BinaryLoss = 'hinge'; ## -*- texinfo -*- ## @deftp {CompactClassificationECOC} {property} Prior ## ## The prior probability of each class, in the order of ## @code{ClassNames} and summing to one. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {CompactClassificationECOC} {property} Cost ## ## The cost of misclassification ## ## A @math{KxK} matrix whose @math{(i,j)} element is the cost of calling ## a member of class @math{i} a member of class @math{j}. ## ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {CompactClassificationECOC} {property} ScoreTransform ## ## The transform applied to the predicted scores ## ## A character vector naming one of the transforms ## @code{parseScoreTransform} accepts, or a function handle. ## ## @end deftp ScoreTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ## The interval the binary learners score on, which decides what losses ## they can be read with. MATLAB carries it inside each learner; here it ## is one property, every learner of a model being of one kind. ScoreRange = [-Inf, Inf]; STfun = @(x) x; endproperties ## Set methods for the properties a user may assign. methods (Hidden) function this = set.ScoreTransform (this, val) name = 'CompactClassificationECOC'; try [this.STfun, this.ScoreTransform] = parseScoreTransform (val, name); catch error (strcat ("CompactClassificationECOC.subsasgn:", ... " 'ScoreTransform' must be a character vector or a", ... " 'function_handle' object.")); end_try_catch endfunction function this = set.BinaryLoss (this, val) if (! (ischar (val) && isrow (val))) error (strcat ("CompactClassificationECOC.subsasgn: 'BinaryLoss'", ... " must be a character vector.")); endif [~, errmsg] = ecocDecode (zeros (1, columns (this.CodingMatrix)), ... this.CodingMatrix, tolower (val), ... 'lossweighted', this.ScoreRange); if (! isempty (errmsg)) error ("CompactClassificationECOC.subsasgn: %s", errmsg); endif this.BinaryLoss = tolower (val); endfunction endmethods methods (Hidden) ## -*- texinfo -*- ## @deftypefn {CompactClassificationECOC} {@var{obj} =} CompactClassificationECOC (@var{Mdl}) ## @deftypefnx {CompactClassificationECOC} {@var{obj} =} CompactClassificationECOC () ## ## Create a @code{CompactClassificationECOC} object. ## ## @var{Mdl} is the @code{ClassificationECOC} object to compact. The ## documented way to reach this constructor is the @code{compact} method. ## ## Called with no arguments it returns an object with its properties at ## their defaults, which is what @code{loadmodel} fills in. ## ## @end deftypefn function this = CompactClassificationECOC (Mdl = []) if (isempty (Mdl)) return; endif if (! strcmp (class (Mdl), 'ClassificationECOC')) error (strcat ("CompactClassificationECOC: MDL must be a", ... " 'ClassificationECOC' object.")); endif ## The levels a predictor read from a table was coded through travel ## with the model, so a compact one still reads a table this.PredictorLevels = Mdl.PredictorLevels; this.BinaryLearners = Mdl.BinaryLearners; this.CodingMatrix = Mdl.CodingMatrix; this.LearnerWeights = Mdl.LearnerWeights; this.ClassNames = Mdl.ClassNames; this.PredictorNames = Mdl.PredictorNames; this.ExpandedPredictorNames = Mdl.ExpandedPredictorNames; this.CategoricalPredictors = Mdl.CategoricalPredictors; this.ResponseName = Mdl.ResponseName; this.Prior = Mdl.Prior; this.Cost = Mdl.Cost; this.ScoreRange = Mdl.ScoreRange; this.BinaryLoss = Mdl.BinaryLoss; this.ScoreTransform = Mdl.ScoreTransform; endfunction ## Custom display function disp (this) fprintf ("\n CompactClassificationECOC\n\n"); fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); fprintf ("%+25s: %s\n", 'CategoricalPredictors', ... mat2str (this.CategoricalPredictors)); fprintf ("%+25s: %s\n", 'ClassNames', classNameListing (this.ClassNames)); fprintf ("%+25s: '%s'\n", 'ScoreTransform', this.ScoreTransform); fprintf ("%+25s: {%dx%d cell}\n", 'BinaryLearners', ... numel (this.BinaryLearners), 1); fprintf ("%+25s: [%dx%d double]\n", 'CodingMatrix', ... rows (this.CodingMatrix), columns (this.CodingMatrix)); fprintf ("\n"); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {CompactClassificationECOC} {@var{label} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {CompactClassificationECOC} {[@var{label}, @var{NegLoss}] =} predict (@dots{}) ## @deftypefnx {CompactClassificationECOC} {[@var{label}, @var{NegLoss}, @var{PBScore}] =} predict (@dots{}) ## @deftypefnx {CompactClassificationECOC} {[@dots{}] =} predict (@dots{}, @var{name}, @var{value}) ## ## Classify new data with a trained @code{CompactClassificationECOC}. ## ## @code{@var{label} = predict (@var{obj}, @var{XC})} sends each row of ## @var{XC} to every binary learner, turns the scores they return into a ## cost per class, and returns the class of least cost. @var{XC} must ## have as many columns as the data the model was fitted on. ## ## @code{[@var{label}, @var{NegLoss}] = predict (@dots{})} also returns ## the @math{NxK} negated average loss, the largest entry of a row naming ## the class that row was given. ## ## @code{[@var{label}, @var{NegLoss}, @var{PBScore}] = predict (@dots{})} ## also returns the @math{NxL} scores the binary learners gave the class ## each was trained to call +1. ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## ## @item @qcode{'BinaryLoss'} @tab @tab The loss to read the binary ## scores with, overriding the @code{BinaryLoss} property for this call. ## ## @item @qcode{'Decoding'} @tab @tab @qcode{'lossweighted'} (default) or ## @qcode{'lossbased'}. The first averages the loss over the learners a ## class took part in, the second over every learner, a class that sat a ## column out costing the same there as a class at the decision boundary. ## @end multitable ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @seealso{CompactClassificationECOC, ClassificationECOC, fitcecoc} ## @end deftypefn function [label, NegLoss, PBScore] = predict (this, XC, varargin) if (nargin < 2) error ("CompactClassificationECOC.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'CompactClassificationECOC.predict', XC); if (mod (numel (varargin), 2) != 0) error (strcat ("CompactClassificationECOC.predict:", ... " name-value arguments must be in pairs.")); endif if (isempty (XC)) error ("CompactClassificationECOC.predict: XC is empty."); endif if (columns (XC) != numel (this.PredictorNames)) error (strcat ("CompactClassificationECOC.predict: XC must have", ... " the same number of predictors as the trained", ... " model.")); endif ## Parse optional paired arguments optNames = {'BinaryLoss', 'Decoding'}; dfValues = {this.BinaryLoss, 'lossweighted'}; [lossname, decoding, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (ischar (lossname) && isrow (lossname))) error (strcat ("CompactClassificationECOC.predict:", ... " 'BinaryLoss' must be a character vector.")); endif lossname = tolower (lossname); if (! (ischar (decoding) && isrow (decoding) && any (strcmpi (decoding, {'lossweighted', 'lossbased'})))) error (strcat ("CompactClassificationECOC.predict:", ... " 'Decoding' must be 'lossweighted' or 'lossbased'.")); endif decoding = tolower (decoding); if (! isempty (args)) error (strcat ("CompactClassificationECOC.predict:", ... " invalid optional paired argument.")); endif PBScore = binaryScores (this, XC); [NegLoss, errmsg] = ecocDecode (PBScore, this.CodingMatrix, lossname, ... decoding, this.ScoreRange); if (! isempty (errmsg)) error ("CompactClassificationECOC.predict: %s", errmsg); endif ## A row a binary learner cannot score has no loss either, and takes the ## class of largest prior, as MATLAB R2024a labels it. miss = any (isnan (PBScore), 2); NegLoss(miss,:) = NaN; NegLoss = this.STfun (NegLoss); [~, idx] = max (NegLoss, [], 2); [~, top] = max (this.Prior); idx(miss) = top; label = labelsFromIndex (this.ClassNames, idx); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationECOC} {@var{obj} =} discardSupportVectors (@var{obj}) ## ## Give up the support vectors of the binary learners. ## ## @code{@var{obj} = discardSupportVectors (@var{obj})} empties ## @code{Alpha}, @code{SupportVectors} and @code{SupportVectorLabels} on ## every binary learner that is a support vector machine on a linear ## kernel, whose linear model stands in for them exactly, so nothing the ## model answers changes. Any other learner is left as it is, a code ## being free to mix them, and a model with no linear support vector ## machine among its learners warns and is returned unchanged. ## ## @seealso{CompactClassificationECOC, ## ClassificationSVM.discardSupportVectors} ## @end deftypefn function this = discardSupportVectors (this) [learners, n] = ecocDiscardSVs (this.BinaryLearners); if (n == 0) warning (strcat ("CompactClassificationECOC.discardSupportVectors:", ... " no binary", ... " learner is a support vector machine on a", ... " linear kernel; nothing was discarded.")); return; endif this.BinaryLearners = learners; endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationECOC} {@var{sub} =} selectModels (@var{obj}, @var{idx}) ## ## Keep a subset of the fitted regularization strengths. ## ## @code{@var{sub} = selectModels (@var{obj}, @var{idx})} narrows every ## binary learner to the strengths @var{idx} names, which may be indices ## into the learner's @qcode{Lambda} or a logical vector over it. Only a ## linear learner is fitted over several strengths, so any other raises. ## ## @seealso{CompactClassificationECOC, ClassificationLinear.selectModels} ## @end deftypefn function sub = selectModels (this, idx) if (nargin < 2) error (strcat ("CompactClassificationECOC.selectModels: too few", ... " input arguments.")); endif [learners, errmsg] = ecocSelectModels (this.BinaryLearners, idx); if (! isempty (errmsg)) error ("CompactClassificationECOC.selectModels: %s", errmsg); endif sub = this; sub.BinaryLearners = learners; endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationECOC} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationECOC} {@var{m} =} margin (@dots{}, @var{name}, @var{value}) ## @deftypefnx {CompactClassificationECOC} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationECOC} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margin of a @code{CompactClassificationECOC}. ## ## @code{@var{m} = margin (@var{obj}, @var{X}, @var{Y})} returns one ## margin per row of @var{X}: the negated loss of the true class less the ## largest negated loss among the others. A positive margin means the ## row was classified correctly, and the larger it is the further the ## decision was from going the other way. ## ## It takes the same @qcode{'BinaryLoss'} and @qcode{'Decoding'} ## arguments @code{predict} does. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{CompactClassificationECOC.predict, ## CompactClassificationECOC.edge} ## @end deftypefn function m = margin (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactClassificationECOC.margin: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'margin', X, Y, ... varargin, nargin > 2); [gY, errmsg] = labelIndices (this.ClassNames, Y); if (! isempty (errmsg)) error ("CompactClassificationECOC.margin: %s", errmsg); endif if (rows (X) != numel (gY)) error (strcat ("CompactClassificationECOC.margin: X and Y must", ... " have the same number of rows.")); endif [~, NegLoss] = predict (this, X, varargin{:}); ## marginsOf takes the number of score matrices, which is one here. m = marginsOf (NegLoss, gY, 1); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationECOC} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationECOC} {@var{e} =} edge (@dots{}, @var{name}, @var{value}) ## @deftypefnx {CompactClassificationECOC} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationECOC} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Classification edge of a @code{CompactClassificationECOC}. ## ## @code{@var{e} = edge (@var{obj}, @var{X}, @var{Y})} returns the ## weighted mean of the margins, one number for the whole of @var{X}. ## ## It takes @qcode{'Weights'} beside the arguments @code{predict} takes. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{CompactClassificationECOC.margin, ## CompactClassificationECOC.loss} ## @end deftypefn function e = edge (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactClassificationECOC.edge: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'edge', X, Y, varargin, ... nargin > 2); [W, rest] = splitWeights (this, varargin, Y, 'edge'); m = margin (this, X, Y, rest{:}); e = sum (W(:) .* m(:)) / sum (W); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationECOC} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationECOC} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationECOC} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {CompactClassificationECOC} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Classification loss of a @code{CompactClassificationECOC}. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} returns the ## weighted classification loss of @var{X} against the true labels ## @var{Y}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## ## @item @qcode{'LossFun'} @tab @tab @qcode{'classiferror'} (default), ## @qcode{'classifcost'}, @qcode{'mincost'}, @qcode{'binodeviance'}, ## @qcode{'exponential'}, @qcode{'hinge'}, @qcode{'logit'} or ## @qcode{'quadratic'}. ## ## @item @qcode{'Weights'} @tab @tab One nonnegative weight per row of ## @var{X}. The default is uniform. ## @end multitable ## ## It also takes the @qcode{'BinaryLoss'} and @qcode{'Decoding'} ## arguments @code{predict} takes. ## ## @seealso{CompactClassificationECOC.predict, ## CompactClassificationECOC.edge} ## @end deftypefn function L = loss (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactClassificationECOC.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); [gY, errmsg] = labelIndices (this.ClassNames, Y); if (! isempty (errmsg)) error ("CompactClassificationECOC.loss: %s", errmsg); endif if (rows (X) != numel (gY)) error (strcat ("CompactClassificationECOC.loss: X and Y must have", ... " the same number of rows.")); endif LossFun = 'classiferror'; keep = {}; W = []; for i = 1:2:numel (varargin) switch (tolower (varargin{i})) case 'lossfun' LossFun = varargin{i+1}; if (! (ischar (LossFun) && isrow (LossFun))) error (strcat ("CompactClassificationECOC.loss: 'LossFun'", ... " must be a character vector.")); endif LossFun = tolower (LossFun); case 'weights' W = varargin{i+1}; otherwise keep(end+1:end+2) = varargin(i:i+1); endswitch endfor W = observationWeights (this, W, gY, 'loss'); [~, NegLoss] = predict (this, X, keep{:}); L = classificationLoss (LossFun, NegLoss, gY, W, this.Cost); endfunction endmethods methods (Access = private) ## The score each binary learner gives the class it calls +1, which is ## always the second of its two classes: the learners are fitted on -1 ## and +1 and a learner lists its classes in order. ## ## A RUSBoost ensemble scores with the weighted sum of its trees' class ## probabilities, which no binary loss can read. Divided by the total ## weight of the trees it is their weighted mean, a probability like a ## bagged ensemble's, as R's ebmc and adabag and scikit-learn scale a ## boosted ensemble's weighted votes. MATLAB cannot predict with it. function S = binaryScores (this, XC) L = numel (this.BinaryLearners); S = zeros (rows (XC), L); for j = 1:L mdl = this.BinaryLearners{j}; [~, sc] = predict (mdl, XC); S(:,j) = sc(:,2); if (any (strcmp (class (mdl), {'ClassificationEnsemble', ... 'CompactClassificationEnsemble'})) && strcmp (mdl.Method, 'RUSBoost')) total = sum (mdl.TrainedWeights); if (total > 0) S(:,j) /= total; endif endif endfor endfunction ## Pull 'Weights' out of a name-value list, leaving the rest for predict. function [W, rest] = splitWeights (this, args, Y, caller) rest = {}; W = []; for i = 1:2:numel (args) if (strcmpi (args{i}, 'weights')) W = args{i+1}; else rest(end+1:end+2) = args(i:i+1); endif endfor [gY, errmsg] = labelIndices (this.ClassNames, Y); if (! isempty (errmsg)) error ("CompactClassificationECOC.%s: %s", caller, errmsg); endif W = observationWeights (this, W, gY, caller); endfunction ## The weights an observation carries, defaulting to the class prior ## spread evenly over the rows of that class, as every learner here does. function W = observationWeights (this, W, gY, caller) n = numel (gY); if (isempty (W)) W = ones (n, 1); endif errmsg = weightsClass (W); if (! isempty (errmsg)) error ("CompactClassificationECOC.%s: %s", caller, errmsg); endif if (! (isnumeric (W) && isvector (W) && numel (W) == n && all (W >= 0) && any (W > 0))) error (strcat ("CompactClassificationECOC.%s: 'Weights' must be a", ... " nonnegative numeric vector with one element per", ... " observation."), caller); endif W = priorNormalize (double (W(:)), gY, this.Prior); endfunction endmethods endclassdef ## Tests %!test # MATLAB parity: the property surface of a compact model %! load fisheriris %! CMdl = compact (fitcecoc (meas, species)); %! assert_equal (class (CMdl), 'CompactClassificationECOC'); %! assert_equal (numel (properties (CMdl)), 12); %! assert_equal (CMdl.ClassNames, unique (species)); %! assert_equal (CMdl.CodingMatrix, [1, 1, 0; -1, 0, 1; 0, -1, -1]); %!test # MATLAB parity: the three outputs of predict %! load fisheriris %! CMdl = compact (fitcecoc (meas, species, 'Learners', 'tree')); %! [label, NegLoss, PBScore] = predict (CMdl, meas([1, 51, 101], :)); %! assert_equal (label, {'setosa'; 'versicolor'; 'virginica'}); %! assert_equal (size (NegLoss), [3, 3]); %! assert_equal (size (PBScore), [3, 3]); %!test # MATLAB parity: the decoding of a tree code, both schemes %! ## Measured on R2024a, whose trees this package reproduces on this %! ## fixture, so the whole path is compared and not only the labels. %! load fisheriris %! CMdl = compact (fitcecoc (meas, species, 'Learners', 'tree')); %! r = [1, 20, 51, 70, 101, 130]; %! [~, W] = predict (CMdl, meas(r,:), 'Decoding', 'lossweighted'); %! assert_equal (W, ... %! [0, -1, -2; ... %! 0, -1, -2; ... %! -2, 0, -1; ... %! -2, 0, -1; ... %! -2, -0.956994328922495, -0.000472589792060491; ... %! -2, -0.444444444444445, -0.111111111111111], 1e-12); %! [~, B] = predict (CMdl, meas(r,:), 'Decoding', 'lossbased'); %! assert_equal (B(1,:), [-0.166666666666667, -0.833333333333333, ... %! -1.5], 1e-12); %!test # lossweighted is the default decoding %! load fisheriris %! CMdl = compact (fitcecoc (meas, species)); %! [~, a] = predict (CMdl, meas(1:5,:)); %! [~, b] = predict (CMdl, meas(1:5,:), 'Decoding', 'lossweighted'); %! assert_equal (a, b); %!test # MATLAB parity: the edge is the weighted mean of the margins %! load fisheriris %! CMdl = compact (fitcecoc (meas, species, 'Learners', 'tree')); %! m = margin (CMdl, meas, species); %! assert_equal (edge (CMdl, meas, species), mean (m), 1e-12); %!test # loss counts the labels it got wrong %! load fisheriris %! CMdl = compact (fitcecoc (meas, species, 'Learners', 'tree')); %! assert_equal (loss (CMdl, meas, species), 0.02, 1e-12); %! assert_equal (loss (CMdl, meas, species, 'LossFun', 'classiferror'), ... %! 0.02, 1e-12); %!test # a binary loss the learners cannot be read with is refused %! load fisheriris %! CMdl = compact (fitcecoc (meas, species)); %! assert_equal (CMdl.BinaryLoss, 'hinge'); %! CMdl.BinaryLoss = 'logit'; %! assert_equal (CMdl.BinaryLoss, 'logit'); %!test # discarding support vectors changes nothing the model answers %! ## The linear model stands in for the vectors exactly, so the labels and %! ## the loss are what they were and only the memory is gone. %! load fisheriris %! Mdl = compact (fitcecoc (meas, species)); %! assert_equal (size (Mdl.BinaryLearners{1}.SupportVectors), [3, 4]); %! D = discardSupportVectors (Mdl); %! assert_equal (class (D), 'CompactClassificationECOC'); %! assert_equal (size (D.BinaryLearners{1}.SupportVectors), [0, 0]); %! assert_equal (isempty (D.BinaryLearners{1}.Alpha), true); %! assert_equal (predict (D, meas), predict (Mdl, meas)); %! assert_equal (loss (D, meas, species), 0.0066666666666667, 1e-12); %!warning ... %! load fisheriris; ... %! M = fitcecoc (meas, species, 'Learners', 'tree'); ... %! discardSupportVectors (compact (M)); %!test # a code of trees is returned unchanged by discardSupportVectors %! load fisheriris %! Mdl = compact (fitcecoc (meas, species, 'Learners', 'tree')); %! ## The state is saved and put back rather than switched on: 'on' would %! ## enable warning classes Octave disables by default and leak them into %! ## every test that runs after this one. %! w = warning ('off', 'all'); %! D = discardSupportVectors (Mdl); %! warning (w); %! assert_equal (predict (D, meas), predict (Mdl, meas)); %!test # selectModels narrows every binary learner to the same strengths %! load fisheriris %! L = fitcecoc (meas, species, ... %! 'Learners', templateLinear ('Lambda', [1e-4, 1e-3, 1e-2])); %! LC = L; %! assert_equal (LC.BinaryLearners{1}.Lambda, [1e-4, 1e-3, 1e-2], 1e-12); %! S = selectModels (LC, 2); %! assert_equal (class (S), 'CompactClassificationECOC'); %! for j = 1:numel (S.BinaryLearners) %! assert_equal (S.BinaryLearners{j}.Lambda, 1e-3, 1e-12); %! endfor %!test # A row missing a predictor takes the class of largest prior %! X = [(1:12)', mod((1:12)', 3)]; %! y = [1; 1; 1; 1; 1; 2; 2; 2; 2; 3; 3; 3]; %! [label, NegLoss] = predict (compact (fitcecoc (X, y)), [NaN, 1]); %! assert_equal (label, 1); %! assert_equal (NegLoss, [NaN, NaN, NaN]); %! Mdl = compact (fitcecoc (X, y, 'Prior', [0.2, 0.2, 0.6])); %! assert_equal (predict (Mdl, [NaN, 1]), 3); %!error ... %! load fisheriris; ... %! selectModels (compact (fitcecoc (meas, species))) %!error ... %! load fisheriris; ... %! selectModels (compact (fitcecoc (meas, species, 'Learners', 'tree')), 1) ## Test input validation %!shared CMdl %! CMdl = compact (fitcecoc (ones (4, 2), [1; 2; 1; 2])); %!error ... %! predict (CMdl) %!error ... %! predict (CMdl, ones (1, 2), 'Decoding') %!error ... %! predict (CMdl, ones (1, 2), 'Decoding', 'nosuch') %!error ... %! predict (CMdl, ones (1, 2), 'Bogus', 1) %!error ... %! predict (CMdl, ones (1, 2), 'BinaryLoss', 'quadratic') %!error ... %! CMdl.BinaryLoss = 'quadratic'; %!error ... %! margin (CMdl, ones (1, 2)) %!error ... %! loss (CMdl, ones (1, 2)) ## A table at prediction %!test # the levels travel with the model, and predict reads a table by name %! load fisheriris %! T = table (meas(:,1), meas(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! T.Species = categorical (species); %! Mdl = fitcecoc (T, 'Species'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.PredictorLevels, Mdl.PredictorLevels); %! assert_equal (predict (CMdl, T(:, [4, 3, 2, 1])), predict (CMdl, T)); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcecoc (T, 'Species')); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'Species'), a); %! assert_equal (loss (Mdl, T), a); ## A table at edge %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcecoc (T, 'Species')); %! a = edge (Mdl, X, y); %! assert_equal (edge (Mdl, T(:,1:2), y), a); %! assert_equal (edge (Mdl, T, 'Species'), a); %! assert_equal (edge (Mdl, T), a); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcecoc (T, 'Species')); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## Observation weights of class single or double %!error ... %! loss (compact (fitcecoc ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2])), ... %! [1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', int8 ([1; 1; 1; 1])) statistics-release-2.1.2/inst/Machine_Learning/CompactClassificationEnsemble.m000066400000000000000000001320711527311104300275600ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef CompactClassificationEnsemble < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} CompactClassificationEnsemble ## ## Compact ensemble of weak learners for classification ## ## A @code{CompactClassificationEnsemble} object carries the trained weak ## learners of a boosted or bagged ensemble and what prediction needs, but ## not the observations it was fitted on. It predicts new data identically ## to the ensemble it came from, and weak learners can be removed from it. ## ## Create one with the @code{compact} method of a ## @code{ClassificationEnsemble} or @code{ClassificationBaggedEnsemble} ## object. ## ## @seealso{fitcensemble, ClassificationEnsemble, ## ClassificationBaggedEnsemble} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CompactClassificationEnsemble} {property} CategoricalPredictors ## ## Indices of categorical predictors ## ## The predictors every tree treats as categorical, empty when none ## is. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {CompactClassificationEnsemble} {property} ClassNames ## ## Names of the classes ## ## The classes, in the type of the response and in the order the columns ## of the scores take them. This property is read-only. ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {CompactClassificationEnsemble} {property} CombineWeights ## ## How the weak learners are combined ## ## @qcode{'WeightedSum'} for a boosted ensemble, whose scores are the sum ## of each learner's output times its weight, or @qcode{'WeightedAverage'} ## for a bagged one, whose scores are the weighted average of its trees' ## class probabilities. This property is read-only. ## ## @end deftp CombineWeights = 'WeightedSum'; ## -*- texinfo -*- ## @deftp {CompactClassificationEnsemble} {property} Cost ## ## Misclassification costs ## ## A square matrix, @code{Cost(i,j)} being the cost of classifying an ## observation of class @math{i} as class @math{j}. This property is ## read-only. ## ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {CompactClassificationEnsemble} {property} ExpandedPredictorNames ## ## Names of the predictors as the learners saw them ## ## The same as @code{PredictorNames}, no predictor being expanded. This ## property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationEnsemble} {property} NumTrained ## ## Number of trained weak learners ## ## A nonnegative integer. This property is read-only. ## ## @end deftp NumTrained = 0; ## -*- texinfo -*- ## @deftp {CompactClassificationEnsemble} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationEnsemble} {property} Prior ## ## Prior probabilities of the classes ## ## A row vector with one probability per class, in the order of ## @code{ClassNames}. This property is read-only. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {CompactClassificationEnsemble} {property} ResponseName ## ## Name of the response variable ## ## A character vector. This property is read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {CompactClassificationEnsemble} {property} Trained ## ## Trained weak learners ## ## A column cell array with one compact model per learner: a ## @code{CompactClassificationTree} for AdaBoostM1, AdaBoostM2, RUSBoost and ## Bag, and a @code{CompactRegressionTree} for GentleBoost and LogitBoost, ## which fit regression trees. MATLAB wraps those regression trees in a ## classifier object of its own; here they are held as they are. This ## property is read-only. ## ## @end deftp Trained = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationEnsemble} {property} TrainedWeights ## ## Weights of the trained weak learners ## ## A column with one weight per learner. This property is read-only. ## ## @end deftp TrainedWeights = []; ## -*- texinfo -*- ## @deftp {CompactClassificationEnsemble} {property} UsePredForLearner ## ## Which predictors each learner uses ## ## For the @qcode{'Subspace'} method, a logical matrix with one row per ## predictor and one column per learner. Empty for tree learners, as ## MATLAB returns it. This property is read-only. ## ## @end deftp UsePredForLearner = []; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {CompactClassificationEnsemble} {property} ScoreTransform ## ## Transform applied to the predicted scores ## ## A character vector naming a built-in transform, such as ## @qcode{'none'} (default) or @qcode{'doublelogit'}, or a function ## handle. The labels, losses, edges and margins are computed from the ## transformed scores. ## ## @end deftp ScoreTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) Method = ''; # the ensemble method, deciding a learner's output STfun = @(x) x; # the score transform as a function DefaultIndex = 1; # class given to a row no learner may score endproperties methods (Hidden) function this = set.ScoreTransform (this, val) try [this.STfun, this.ScoreTransform] = parseScoreTransform (val, ... 'CompactClassificationEnsemble'); catch error (strcat ("CompactClassificationEnsemble.subsasgn:", ... " 'ScoreTransform' must be a character vector or a", ... " 'function_handle' object.")); end_try_catch endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationEnsemble} {@var{obj} =} CompactClassificationEnsemble (@var{Mdl}) ## ## Create a @code{CompactClassificationEnsemble} object. ## ## @var{Mdl} is the @code{ClassificationEnsemble} or ## @code{ClassificationBaggedEnsemble} object to compact. The documented ## way to reach this constructor is the @code{compact} method. ## ## @end deftypefn function this = CompactClassificationEnsemble (Mdl = []) if (isempty (Mdl)) return; endif if (! isa (Mdl, 'ClassificationEnsemble')) error (strcat ("CompactClassificationEnsemble: MDL must be a", ... " 'ClassificationEnsemble' object.")); endif ## The levels a predictor read from a table was coded through travel ## with the model, so a compact one still reads a table this.PredictorLevels = Mdl.PredictorLevels; this.CategoricalPredictors = Mdl.CategoricalPredictors; this.ClassNames = Mdl.ClassNames; this.CombineWeights = Mdl.CombineWeights; this.Cost = Mdl.Cost; this.ExpandedPredictorNames = Mdl.ExpandedPredictorNames; this.NumTrained = Mdl.NumTrained; this.PredictorNames = Mdl.PredictorNames; this.Prior = Mdl.Prior; this.ResponseName = Mdl.ResponseName; this.Trained = Mdl.Trained; this.TrainedWeights = Mdl.TrainedWeights; this.UsePredForLearner = Mdl.UsePredForLearner; this.Method = Mdl.Method; this.DefaultIndex = Mdl.DefaultIndex; ## A transform given as a function handle is held as its text, which ## names no built-in transform, so the function itself is copied. named = {'doublelogit', 'invlogit', 'ismax', 'logit', 'none', ... 'identity', 'sign', 'symmetric', 'symmetricismax', ... 'symmetriclogit'}; if (any (strcmp (Mdl.ScoreTransform, named))) this.ScoreTransform = Mdl.ScoreTransform; else this.ScoreTransform = Mdl.STfun; endif endfunction function disp (this) fprintf ("\n CompactClassificationEnsemble\n\n"); fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); fprintf ("%+25s: %s\n", 'CategoricalPredictors', ... mat2str (this.CategoricalPredictors)); fprintf ("%+25s: %s\n", 'ClassNames', classNameListing (this.ClassNames)); fprintf ("%+25s: '%s'\n", 'ScoreTransform', this.ScoreTransform); fprintf ("%+25s: %d\n", 'NumTrained', this.NumTrained); fprintf ("\n"); endfunction ## The shared bodies of the scoring methods. The full and bagged ## classes call them with their own names, so an error names the method ## that was called. function [label, S] = ensemblePredict (this, X, args, caller) o = ensembleArgs (this, X, args, {'Learners', 'UseObsForLearner'}, ... caller); S = this.STfun (ensembleScores (this, X, o, 'ensemble')); label = scoreLabels (this, S); endfunction function L = ensembleLoss (this, X, Y, args, caller) o = ensembleArgs (this, X, args, {'LossFun', 'Learners', 'Mode', ... 'UseObsForLearner', 'Weights'}, ... caller); [gY, w] = ensembleResponse (this, X, Y, o, caller); S = this.STfun (ensembleScores (this, X, o, o.Mode)); L = zeros (size (S, 3), 1); allowed = allowedRows (this, o); for k = 1:size (S, 3) ## A row hidden from every learner is left out with its weight, the ## rest renormalized, as MATLAB does. have = allowed(:,k); wk = w(have); if (! (sum (wk) > 0)) L(k) = NaN; continue; endif wk /= sum (wk); Sk = S(have,:,k); gk = gY(have); if (is_function_handle (o.LossFun)) C = false (size (Sk)); C(sub2ind (size (Sk), (1:rows (Sk))', gk)) = true; L(k) = o.LossFun (C, Sk, wk, this.Cost); else L(k) = unscoredLoss (o.LossFun, Sk, gk, wk, this.Cost, ... this.DefaultIndex); endif endfor endfunction function e = ensembleEdge (this, X, Y, args, caller) o = ensembleArgs (this, X, args, {'Learners', 'Mode', ... 'UseObsForLearner', 'Weights'}, ... caller); [gY, w] = ensembleResponse (this, X, Y, o, caller); S = this.STfun (ensembleScores (this, X, o, o.Mode)); m = marginsOf (S, gY, size (S, 3)); ## Rows hidden from every learner are left out, the weights renormalized. have = allowedRows (this, o); m(! have) = 0; W2 = w .* have; e = (sum (W2 .* m, 1) ./ sum (W2, 1))(:); endfunction function m = ensembleMargin (this, X, Y, args, caller) o = ensembleArgs (this, X, args, {'Learners', 'UseObsForLearner'}, ... caller); gY = ensembleResponse (this, X, Y, o, caller); S = this.STfun (ensembleScores (this, X, o, 'ensemble')); m = marginsOf (S, gY, 1); endfunction ## The untransformed scores of the rows of X over the first t learners, ## for every t, as an NxKxNumTrained array. function S = ensembleSteps (this, X) T = this.NumTrained; o = struct ('Learners', 1:T, 'U', true (rows (X), T)); S = ensembleScores (this, X, o, 'cumulative'); endfunction function [imp, ma] = ensembleImportance (this, caller) if (strcmp (this.Method, 'Subspace')) error (strcat ("%s: predictor importance is defined only for", ... " ensembles of trees."), caller); endif imp = zeros (1, numel (this.PredictorNames)); for t = 1:this.NumTrained imp += this.TrainedWeights(t) * predictorImportance (this.Trained{t}); endfor if (sum (this.TrainedWeights) > 0) imp /= sum (this.TrainedWeights); endif ma = []; endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {CompactClassificationEnsemble} {@var{label} =} predict (@var{obj}, @var{X}) ## @deftypefnx {CompactClassificationEnsemble} {[@var{label}, @var{scores}] =} predict (@dots{}) ## @deftypefnx {CompactClassificationEnsemble} {[@dots{}] =} predict (@dots{}, @var{name}, @var{value}) ## ## Classify new data with a compact ensemble. ## ## @var{label} holds the class of highest score for each row of @var{X}, ## in the type of @code{ClassNames}, and @var{scores} the @math{NxK} ## scores after @code{ScoreTransform}. ## ## The scores of a boosted ensemble are the sum over the learners of each ## learner's weight times its output. For AdaBoostM1 the output is +1 for ## the class the learner predicts and -1 for the other; for GentleBoost and ## LogitBoost it is the regression tree's prediction for the first class and ## its negative for the second; for AdaBoostM2 and RUSBoost it is the ## learner's class probabilities. The scores of a bagged ensemble are the ## average of its trees' class probabilities. A row that no learner may ## score has @code{NaN} scores and is given the class of greatest prior ## probability. ## ## Name-Value arguments: ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Learners'} @tab @tab A vector of indices of the learners ## to use. The default is all of them. ## @item @qcode{'UseObsForLearner'} @tab @tab An @math{NxNumTrained} ## logical matrix saying which learner may score which row. The default ## lets every learner score every row. ## @end multitable ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @seealso{CompactClassificationEnsemble, fitcensemble} ## @end deftypefn function [label, scores] = predict (this, X, varargin) if (nargin < 2) error (strcat ("CompactClassificationEnsemble.predict: too few", ... " input arguments.")); endif ## A table is read by the names the model was fitted on X = tableColumns (this, 'CompactClassificationEnsemble.predict', X); [label, scores] = ensemblePredict (this, X, varargin, ... 'CompactClassificationEnsemble.predict'); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationEnsemble} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationEnsemble} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationEnsemble} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {CompactClassificationEnsemble} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Classification loss of a compact ensemble. ## ## @var{L} is the weighted loss of the scores @code{predict} gives the rows ## of @var{X} against the labels @var{Y}. The weights are normalized so ## that each class carries its prior probability. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## Name-Value arguments: ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'LossFun'} @tab @tab @qcode{'classiferror'} (default), ## @qcode{'binodeviance'}, @qcode{'classifcost'}, @qcode{'exponential'}, ## @qcode{'hinge'}, @qcode{'logit'}, @qcode{'mincost'}, ## @qcode{'quadratic'}, or a function handle called as ## @code{lossfun (C, S, W, Cost)}, @var{C} being an @math{NxK} logical ## matrix marking each row's class, @var{S} the scores and @var{W} the ## normalized weights. ## @item @qcode{'Mode'} @tab @tab @qcode{'ensemble'} (default) for one ## loss over the learners used, @qcode{'cumulative'} for a column whose ## element @math{j} uses the first @math{j} of them, or ## @qcode{'individual'} for a column with the loss of each on its own. ## @item @qcode{'Weights'} @tab @tab A nonnegative vector with one weight ## per row. The default is uniform. ## @end multitable ## ## @qcode{'Learners'} and @qcode{'UseObsForLearner'} are taken as by ## @code{predict}. A row @qcode{'UseObsForLearner'} hides from every ## learner used is left out and the weights are renormalized over the rest. ## A row no learner can score for its missing values stays: it counts as ## the class of greatest prior under @qcode{'classiferror'}, ## @qcode{'classifcost'} and @qcode{'mincost'}, and any other loss is ## @code{NaN}. @code{edge} treats the two alike, its result being ## @code{NaN} for the second. ## ## @seealso{CompactClassificationEnsemble, ## CompactClassificationEnsemble.edge} ## @end deftypefn function L = loss (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactClassificationEnsemble.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); L = ensembleLoss (this, X, Y, varargin, ... 'CompactClassificationEnsemble.loss'); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationEnsemble} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationEnsemble} {@var{e} =} edge (@dots{}, @var{name}, @var{value}) ## @deftypefnx {CompactClassificationEnsemble} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationEnsemble} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Classification edge of a compact ensemble. ## ## @var{e} is the weighted mean of the margins of the rows of @var{X}, the ## weights normalized so that each class carries its prior probability. ## @qcode{'Mode'} and @qcode{'Weights'} are taken as by @code{loss}, and ## @qcode{'Learners'} and @qcode{'UseObsForLearner'} as by @code{predict}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{CompactClassificationEnsemble, ## CompactClassificationEnsemble.margin} ## @end deftypefn function e = edge (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactClassificationEnsemble.edge: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'edge', X, Y, varargin, ... nargin > 2); e = ensembleEdge (this, X, Y, varargin, ... 'CompactClassificationEnsemble.edge'); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationEnsemble} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationEnsemble} {@var{m} =} margin (@dots{}, @var{name}, @var{value}) ## @deftypefnx {CompactClassificationEnsemble} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationEnsemble} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margins of a compact ensemble. ## ## @var{m} holds, for each row of @var{X}, the score of its true class ## less the highest score among the other classes. @qcode{'Learners'} ## and @qcode{'UseObsForLearner'} are taken as by @code{predict}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{CompactClassificationEnsemble, ## CompactClassificationEnsemble.edge} ## @end deftypefn function m = margin (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error (strcat ("CompactClassificationEnsemble.margin: too few", ... " input arguments.")); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'margin', X, Y, ... varargin, nargin > 2); m = ensembleMargin (this, X, Y, varargin, ... 'CompactClassificationEnsemble.margin'); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationEnsemble} {@var{imp} =} predictorImportance (@var{obj}) ## @deftypefnx {CompactClassificationEnsemble} {[@var{imp}, @var{ma}] =} predictorImportance (@var{obj}) ## ## Estimate the importance of each predictor. ## ## @var{imp} is a row vector with one element per predictor, the average ## over the trees of each tree's @code{predictorImportance}, weighted by ## @code{TrainedWeights}. GentleBoost and LogitBoost ensembles take the ## importance of their regression trees. @var{ma}, the predictive ## measure of association between the predictors, is empty, the trees ## growing no surrogate splits. ## ## @seealso{CompactClassificationEnsemble} ## @end deftypefn function [imp, ma] = predictorImportance (this) [imp, ma] = ensembleImportance (this, ... 'CompactClassificationEnsemble.predictorImportance'); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationEnsemble} {@var{C} =} removeLearners (@var{obj}, @var{idx}) ## ## Remove weak learners from a compact ensemble. ## ## @var{C} is the ensemble without the learners whose indices @var{idx} ## holds, their weights and their columns of @code{UsePredForLearner} ## removed with them. ## ## @seealso{CompactClassificationEnsemble} ## @end deftypefn function this = removeLearners (this, idx) if (nargin < 2) error (strcat ("CompactClassificationEnsemble.removeLearners: too", ... " few input arguments.")); endif if (! (isnumeric (idx) && isvector (idx) && isreal (idx) && all (idx >= 1) && all (idx <= this.NumTrained) && all (idx == fix (idx)))) error (strcat ("CompactClassificationEnsemble.removeLearners: IDX", ... " must be a vector of indices of trained learners.")); endif keep = true (1, this.NumTrained); keep(idx) = false; this.Trained = this.Trained(keep); this.TrainedWeights = this.TrainedWeights(keep); if (! isempty (this.UsePredForLearner)) this.UsePredForLearner = this.UsePredForLearner(:,keep); endif this.NumTrained = sum (keep); endfunction endmethods methods (Access = private) ## The Name-Value arguments of the scoring methods, those not in ALLOWED ## refused. function o = ensembleArgs (this, X, args, allowed, caller) if (! (isnumeric (X) && isreal (X) && ismatrix (X))) error ("%s: X must be a real numeric matrix.", caller); endif if (columns (X) != numel (this.PredictorNames)) error ("%s: X must have one column per predictor.", caller); endif if (mod (numel (args), 2) != 0) error ("%s: name-value arguments must be in pairs.", caller); endif ## Parse optional paired arguments. Each caller accepts the subset of ## them ALLOWED lists; an option left empty takes its default below: ## every trained learner, every observation for every learner, the ## whole ensemble, uniform weights and the classiferror loss. optNames = {'Learners', 'UseObsForLearner', 'Mode', 'Weights', ... 'LossFun'}; [Learners, U, Mode, Weights, LossFun, args] = ... parsePairedArguments (optNames, {[], [], [], [], []}, args(:)); given = optNames(! cellfun (@isempty, {Learners, U, Mode, Weights, ... LossFun})); if (! isempty (args) || ! all (ismember (lower (given), lower (allowed)))) error ("%s: invalid optional paired argument.", caller); endif ## Validate optional paired arguments T = this.NumTrained; o = struct ('Learners', 1:T, 'U', true (rows (X), T), ... 'Mode', 'ensemble', 'Weights', [], ... 'LossFun', 'classiferror'); if (! isempty (Learners)) if (! (isnumeric (Learners) && isvector (Learners) && isreal (Learners) && all (Learners >= 1) && all (Learners <= T) && all (Learners == fix (Learners)))) error (strcat ("%s: 'Learners' must be a vector of indices", ... " of trained learners."), caller); endif o.Learners = double (Learners(:)'); endif if (! isempty (U)) if (! (islogical (U) && isequal (size (U), [rows(X), T]))) error (strcat ("%s: 'UseObsForLearner' must be a logical", ... " matrix with one row per observation and", ... " one column per trained learner."), caller); endif o.U = U; endif if (! isempty (Mode)) if (! (ischar (Mode) && any (strcmpi (Mode, {'ensemble', ... 'cumulative', ... 'individual'})))) error (strcat ("%s: 'Mode' must be 'ensemble', 'cumulative'", ... " or 'individual'."), caller); endif o.Mode = tolower (Mode); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("%s: %s", caller, errmsg); endif if (! isempty (Weights)) if (! (isnumeric (Weights) && isvector (Weights) && isreal (Weights) && numel (Weights) == rows (X) && all (Weights >= 0) && any (Weights > 0))) error (strcat ("%s: 'Weights' must be a nonnegative numeric", ... " vector with one element per observation,", ... " not all zero."), caller); endif o.Weights = double (Weights(:)); endif if (! isempty (LossFun)) losses = {'binodeviance', 'classifcost', 'classiferror', ... 'exponential', 'hinge', 'logit', 'mincost', ... 'quadratic'}; if (ischar (LossFun) && any (strcmpi (LossFun, losses))) o.LossFun = tolower (LossFun); elseif (is_function_handle (LossFun)) o.LossFun = LossFun; else error (strcat ("%s: 'LossFun' must be the name of a", ... " classification loss or a function handle."), ... caller); endif endif endfunction ## The class index of each row of Y and the weights, normalized so that ## each class carries its prior and the whole sums to one. function [gY, w] = ensembleResponse (this, X, Y, o, caller) if (rows (Y) != rows (X)) error ("%s: X and Y must have the same number of rows.", caller); endif [gY, errmsg] = labelIndices (this.ClassNames, Y); if (! isempty (errmsg)) error ("%s: %s", caller, errmsg); endif w = o.Weights; if (isempty (w)) w = ones (rows (X), 1); endif w = priorNormalize (w, gY, this.Prior); w /= sum (w); endfunction ## What learner T says about each row of X, laid out over the classes. function G = learnerOutput (this, t, X) K = classCount (this.ClassNames); mdl = this.Trained{t}; switch (this.Method) case 'AdaBoostM1' g = labelIndices (this.ClassNames, predict (mdl, X)); h = double (g == 1) - double (g == 2); G = [h, -h]; case {'GentleBoost', 'LogitBoost'} h = predict (mdl, X); G = [h, -h]; case {'LPBoost', 'TotalBoost'} ## An LPBoost or TotalBoost learner scores a class with twice its ## probability less one, as in MATLAB R2024a. [~, s] = predict (mdl, X); G = -ones (rows (X), K); G(:, labelIndices (this.ClassNames, mdl.ClassNames)) = 2 * s - 1; case 'Subspace' [~, s] = predict (mdl, X(:,this.UsePredForLearner(:,t))); G = zeros (rows (X), K); G(:, labelIndices (this.ClassNames, mdl.ClassNames)) = s; otherwise [~, s] = predict (mdl, X); G = zeros (rows (X), K); G(:, labelIndices (this.ClassNames, mdl.ClassNames)) = s; endswitch endfunction ## The untransformed scores over the learners O names: NxK for MODE ## 'ensemble', NxKxT otherwise, T being the number of learners. A row no ## learner may score has NaN scores. function S = ensembleScores (this, X, o, mode) n = rows (X); K = classCount (this.ClassNames); T = numel (o.Learners); A = zeros (n, K, T); V = zeros (n, 1, T); U = false (n, 1, T); for j = 1:T t = o.Learners(j); G = learnerOutput (this, t, X); ## A learner with no score for a row does not vote on it u = o.U(:,t) & ! any (isnan (G), 2); G(isnan (G)) = 0; A(:,:,j) = this.TrainedWeights(t) * G .* u; V(:,1,j) = this.TrainedWeights(t) * u; U(:,1,j) = u; endfor average = strcmp (this.CombineWeights, 'WeightedAverage'); switch (mode) case 'ensemble' num = sum (A, 3); den = sum (V, 3); used = any (U, 3); case 'cumulative' num = cumsum (A, 3); den = cumsum (V, 3); used = cumsum (U, 3) > 0; otherwise num = A; den = V; used = U; endswitch if (average) S = num ./ den; else S = num; endif S(repmat (! used, 1, K)) = NaN; endfunction ## Which rows of X each result may see through UseObsForLearner: an Nx1 ## mask for O.Mode 'ensemble', NxT otherwise, T being the learners of O. function A = allowedRows (this, o) U = o.U(:,o.Learners); switch (o.Mode) case 'ensemble' A = any (U, 2); case 'cumulative' A = cumsum (U, 2) > 0; otherwise A = U; endswitch endfunction ## The class of highest score, and for a row with NaN scores the class of ## greatest prior probability. function label = scoreLabels (this, S) [~, k] = max (S, [], 2); k(any (isnan (S), 2)) = this.DefaultIndex; label = labelsFromIndex (this.ClassNames, k); endfunction endmethods endclassdef ## Test output %!shared X2, Y2, C %! load fisheriris %! X2 = meas(51:150,:); %! Y2 = species(51:150); %! C = compact (fitcensemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 5, ... %! 'Learners', templateTree ('MaxNumSplits', 1))); %!test # MATLAB parity: the properties of a compact ensemble %! assert_equal (numel (properties (C)), 13); %! assert_equal (C.NumTrained, 5); %! assert_equal (C.CombineWeights, 'WeightedSum'); %! assert_equal (isprop (C, 'X'), false); %!test # MATLAB parity: scores over a subset of the learners %! [~, s] = predict (C, X2([1, 51],:), 'Learners', [2, 4]); %! assert_equal (s(:,1), [1.547898302883168; -0.439169918665653], 1e-13); %!test # MATLAB parity: a row no learner may score has NaN scores %! U = true (2, 5); %! U(1,:) = false; %! U(2,[1, 3]) = false; %! [label, s] = predict (C, X2(1:2,:), 'UseObsForLearner', U); %! assert_equal (isnan (s(1,:)), [true, true]); %! assert_equal (label{1}, 'versicolor'); %! assert_equal (s(2,1), 1.279703855451121, 1e-13); %!test # MATLAB parity: such a row takes the class of greatest prior %! load fisheriris %! Y = species(41:150); %! Y(1:10) = {'virginica'}; %! M = compact (fitcensemble (meas(41:150,:), Y, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 3, ... %! 'Learners', templateTree ('MaxNumSplits', 1))); %! label = predict (M, meas(1,:), 'UseObsForLearner', false (1, 3)); %! assert_equal (label, {'virginica'}); %!test # MATLAB parity: the loss in its three modes %! assert_equal (loss (C, X2, Y2), 0.04, 1e-15); %! assert_equal (loss (C, X2, Y2, 'Mode', 'cumulative'), ... %! [0.06; 0.06; 0.04; 0.06; 0.04], 1e-15); %! assert_equal (loss (C, X2, Y2, 'Mode', 'individual'), ... %! [0.06; 0.08; 0.21; 0.5; 0.5], 1e-14); %! assert_equal (loss (C, X2, Y2, 'Learners', [1, 3]), 0.06, 1e-15); %!test # MATLAB parity: every built-in loss function %! f = {'binodeviance', 'exponential', 'hinge', 'logit', 'quadratic', ... %! 'mincost', 'classifcost'}; %! L = cellfun (@(n) loss (C, X2, Y2, 'LossFun', n), f); %! assert_equal (L, [0.088288301419851, 0.182079166822146, ... %! 0.093152538904937, 0.136695159561291, ... %! 3.600631106449406, 0.04, 0.04], 1e-12); %!test # a loss function given as a handle %! f = @(Cl, Sc, W, Cost) sum (W .* (Sc(Cl) < 0)); %! assert_equal (loss (C, X2, Y2, 'LossFun', f), 0.04, 1e-15); %!test # MATLAB parity: the edge and its cumulative mode %! assert_equal (edge (C, X2, Y2), 5.115272254726563, 1e-12); %! assert_equal (edge (C, X2, Y2, 'Mode', 'cumulative'), ... %! [2.421351075476915; 4.090488381577923; ... %! 5.115272254726563; 5.115272254726564; ... %! 5.115272254726563], 1e-12); %!test # MATLAB parity: margins are twice the score of the true class %! [~, s] = predict (C, X2([1, 21],:)); %! m = margin (C, X2([1, 21],:), Y2([1, 21])); %! assert_equal (m, [3.544074277136197; -1.958996348947703], 1e-12); %! assert_equal (m, 2 * s(:,1), 1e-14); %!test # MATLAB parity: losses read the transformed scores %! D = C; %! D.ScoreTransform = 'doublelogit'; %! assert_equal (loss (D, X2, Y2, 'LossFun', 'quadratic'), ... %! 0.028390700687515, 1e-13); %!test # a score transform given as a function handle survives compacting %! load fisheriris %! Mdl = fitcensemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 2, ... %! 'Learners', templateTree ('MaxNumSplits', 1), ... %! 'ScoreTransform', @(s) 2 * s); %! [~, s] = predict (Mdl, X2(1,:)); %! [~, s0] = predict (C, X2(1,:), 'Learners', [1, 2]); %! assert_equal (s, 2 * s0, 1e-14); %!test # MATLAB parity: removing learners %! D = removeLearners (C, [2, 4]); %! assert_equal (D.NumTrained, 3); %! assert_equal (D.TrainedWeights, [1.375767656520975; ... %! 0.883434373403998; 0.268194447432047], 1e-13); ## Test input validation %!error ... %! CompactClassificationEnsemble (1) %!error ... %! predict (C) %!error ... %! predict (C, {1}) %!error ... %! predict (C, ones (2, 3)) %!error ... %! predict (C, X2, 'Learners') %!error ... %! predict (C, X2, 'Mode', 'ensemble') %!error ... %! predict (C, X2, 'Learners', 6) %!error ... %! predict (C, X2, 'UseObsForLearner', true (2, 5)) %!error ... %! loss (C, X2) %!error ... %! loss (C, X2, Y2, 'Mode', 'all') %!error ... %! loss (C, X2, Y2, 'Weights', ones (3, 1)) %!error ... %! loss (C, X2, Y2, 'LossFun', 'mse') %!error ... %! loss (C, X2, Y2(1:3)) %!error ... %! loss (C, X2(1:2,:), {'rose'; 'versicolor'}) %!error ... %! edge (C, X2) %!error ... %! edge (C, X2, Y2, 'LossFun', 'hinge') %!error ... %! margin (C, X2) %!error ... %! margin (C, X2, Y2, 'Mode', 'cumulative') %!error ... %! removeLearners (C) %!error ... %! removeLearners (C, 0) %!error ... %! D = C; %! D.ScoreTransform = 1; %!test # MATLAB parity: importance is the weighted average over the learners %! [imp, ma] = predictorImportance (C); %! assert_equal (imp, [0.020544313498471, 0, 0.094343052487178, ... %! 0.131555749375574], 1e-13); %! assert_equal (ma, []); %!test # MATLAB parity: the importance of AdaBoostM2 stumps %! load fisheriris %! M = fitcensemble (meas, species, 'Method', 'AdaBoostM2', ... %! 'NumLearningCycles', 4, ... %! 'Learners', templateTree ('MaxNumSplits', 1)); %! assert_equal (predictorImportance (compact (M)), ... %! [0, 0, 0.230674233368758, 0.072325623666526], 1e-13); %!test # MATLAB parity: boosted regression trees give their importance %! M = fitcensemble (X2, Y2, 'Method', 'LogitBoost', 'NumLearningCycles', 3, ... %! 'Learners', templateTree ('MaxNumSplits', 1), ... %! 'LearnRate', 0.5); %! assert_equal (predictorImportance (M), ... %! [0, 0, 0.698823105603178, 1.563129005828868], 1e-13); %!test # MATLAB parity: a row no learner may score is left out of the loss %! U = true (100, 5); %! U(1,:) = false; %! miss = ! strcmp (predict (C, X2(2:end,:)), Y2(2:end)); %! assert_equal (loss (C, X2, Y2, 'UseObsForLearner', U), mean (miss), 1e-15); %! m = margin (C, X2(2:end,:), Y2(2:end)); %! assert_equal (edge (C, X2, Y2, 'UseObsForLearner', U), mean (m), 1e-13); ## A table at prediction %!test # the levels travel with the model, and predict reads a table by name %! load fisheriris %! T = table (meas(:,1), meas(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! T.Species = categorical (species); %! Mdl = fitcensemble (T, 'Species'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.PredictorLevels, Mdl.PredictorLevels); %! a = predict (CMdl, T); %! assert_equal (predict (CMdl, T(:, [4, 3, 2, 1])), a); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcensemble (T, 'Species')); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'Species'), a); %! assert_equal (loss (Mdl, T), a); ## A table at edge %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcensemble (T, 'Species')); %! a = edge (Mdl, X, y); %! assert_equal (edge (Mdl, T(:,1:2), y), a); %! assert_equal (edge (Mdl, T, 'Species'), a); %! assert_equal (edge (Mdl, T), a); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcensemble (T, 'Species')); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## Observation weights of class single or double %!error ... %! loss (compact (fitcensemble ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], ... %! 'NumLearningCycles', 3)), ... %! [1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', int8 ([1; 1; 1; 1])) ## A learner with no score for a row does not vote on it %!test %! X = [1 2; 2 1; 2 3; 3 2; 6 7; 7 6; 7 8; 8 7; 1 7; 2 8; 2 6; 3 7]; %! Y = [1 1 1 1 2 2 2 2 3 3 3 3]'; %! Mdl = fitcensemble (X, Y, 'Method', 'Subspace', 'Learners', ... %! 'discriminant', 'NPredToSample', 1, ... %! 'NumLearningCycles', 'AllPredictorCombinations'); %! [~, s] = predict (compact (Mdl), [NaN 2; 2 NaN; NaN NaN]); %! assert_equal (s(:,1), [0.999999985611734; 0.499999998201467; NaN], ... %! 1e-12); %! assert_equal (isnan (s(3,:)), true (1, 3)); ## A row no learner can score stays; one hidden from every learner is left out %!shared Mf, q, y %! X = [1 2; 2 1; 2 3; 3 2; 6 7; 7 6; 7 8; 8 7; 1 7; 2 8; 2 6; 3 7]; %! Y = [1 1 1 1 2 2 2 2 3 3 3 3]'; %! Mf = compact (fitcensemble (X, Y, 'Method', 'Subspace', 'Learners', ... %! 'discriminant', 'NPredToSample', 1, ... %! 'NumLearningCycles', ... %! 'AllPredictorCombinations')); %! q = [NaN 2; 2 NaN; NaN NaN; 2 2]; %! y = [1; 2; 3; 1]; %!assert_equal (loss (Mf, q, y), 2 / 3, 1e-14) %!assert_equal (loss (Mf, q, y, 'LossFun', 'mincost'), 2 / 3, 1e-14) %!assert_equal (loss (Mf, q, y, 'LossFun', 'classifcost'), 2 / 3, 1e-14) %!assert_equal (loss (Mf, q, y, 'LossFun', 'hinge'), NaN) %!assert_equal (loss (Mf, q, [1; 2; 1; 1]), 0.5, 1e-14) %!assert_equal (edge (Mf, q, y), NaN) %!assert_equal (edge (Mf, q, y, 'Mode', 'individual'), [NaN; NaN]) %!test %! U = true (4, 2); %! U(3,:) = false; %! assert_equal (loss (Mf, q, y, 'UseObsForLearner', U), 0.5, 1e-14); %! assert_equal (loss (Mf, [2 2; 7 7; 2 7; 1 1], y, ... %! 'UseObsForLearner', U), 0); %! assert_equal (edge (Mf, [2 2; 7 7; 2 7; 1 1], y, ... %! 'UseObsForLearner', U), 0.499999991905108, 1e-12); %!test %! f = @(C, S, W, Cost) rows (S); %! assert_equal (loss (Mf, q, y, 'LossFun', f), 4); statistics-release-2.1.2/inst/Machine_Learning/CompactClassificationGAM.m000066400000000000000000001703341527311104300264360ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef CompactClassificationGAM < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} CompactClassificationGAM ## ## Compact generalized additive model classification ## ## The @code{CompactClassificationGAM} class is a compact version of a ## Generalized Additive Model classifier, @code{ClassificationGAM}. It does ## not include the training data, resulting in a smaller classifier size that ## can be used for making predictions from new data, but not for tasks such as ## cross validation. ## ## A @code{CompactClassificationGAM} object can only be created from a ## @code{ClassificationGAM} model by using the @code{compact} method. ## ## The engine that fitted the model is carried over in @code{FitMethod}, ## and the compact model predicts by the same scheme the full one did. ## Under @qcode{'boostedtrees'}, the default, the fit is described by ## @code{TreeModel}, @code{BinEdges} and @code{PairDetectionBinEdges}. ## Under @qcode{'splines'} it is described by @code{Formula}, ## @code{BaseModel}, @code{ModelwInt} and @code{IntMatrix}, which MATLAB's ## compact model does not carry. Whichever fitted the model, the other ## set is empty. ## ## @seealso{ClassificationGAM, fitcgam} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CompactClassificationGAM} {property} NumPredictors ## ## Number of predictors ## ## A positive integer value specifying the number of predictors in the ## training dataset used for training the ClassificationGAM model. ## This property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {CompactClassificationGAM} {property} PredictorNames ## ## Names of predictor variables ## ## A cell array of character vectors specifying the names of the predictor ## variables. The names are in the order in which they appear in the ## training dataset. This property is read-only. ## ## @end deftp PredictorNames = []; ## -*- texinfo -*- ## @deftp {CompactClassificationGAM} {property} ResponseName ## ## Response variable name ## ## A character vector specifying the name of the response variable @var{Y}. ## This property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {CompactClassificationGAM} {property} ClassNames ## ## Names of classes in the response variable ## ## An array of unique values of the response variable @var{Y}, which has the ## same data types as the data in @var{Y}. This property is read-only. ## @qcode{ClassNames} can have any of the following datatypes: ## ## @itemize ## @item Cell array of character vectors ## @item Character array ## @item Logical vector ## @item Numeric vector ## @end itemize ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {CompactClassificationGAM} {property} Prior ## ## Prior probability for each class ## ## A 2-element numeric vector specifying the prior probabilities for each ## class. The order of the elements in @qcode{Prior} corresponds to the ## order of the classes in @qcode{ClassNames}. This property is read-only. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {CompactClassificationGAM} {property} Formula ## ## Model specification formula ## ## A character vector specifying the model formula in the form ## @qcode{'Y ~ terms'} where @qcode{Y} represents the response variable and ## @qcode{terms} specifies the predictor variables and interaction terms. ## This property is read-only. ## ## @end deftp Formula = []; ## -*- texinfo -*- ## @deftp {CompactClassificationGAM} {property} Interactions ## ## Two-way interaction terms of the fitted model ## ## A @math{Kx2} matrix of predictor index pairs, one row per two-way term ## the model carries, and @code{zeros (0, 2)} when it carries none. It ## reports what was fitted rather than what was asked for, so a count of ## terms, @qcode{'all'}, a logical matrix and a formula all leave the same ## kind of value behind. This property is read-only. ## ## A main effect names one predictor and a higher-order term names three ## or more, and neither has a two-column form, so neither appears here. ## @code{IntMatrix} remains the complete record of every term fitted. ## ## @end deftp Interactions = zeros (0, 2); ## -*- texinfo -*- ## @deftp {CompactClassificationGAM} {property} BaseModel ## ## Base model parameters ## ## A structure containing the parameters of the base model without any ## interaction terms. The base model represents the generalized additive ## model with only the main effects (predictor terms) included. ## This property is read-only. ## ## @end deftp BaseModel = []; ## -*- texinfo -*- ## @deftp {CompactClassificationGAM} {property} ModelwInt ## ## Model parameters with interactions ## ## A structure containing the parameters of the model that includes ## interaction terms. This model extends the base model by adding ## interaction terms between predictors. This property is read-only. ## ## @end deftp ModelwInt = []; ## -*- texinfo -*- ## @deftp {CompactClassificationGAM} {property} IntMatrix ## ## Every term the model fits ## ## A logical matrix with one row per term and one column per predictor, ## true wherever the term multiplies that predictor. A row naming one ## predictor is a main effect, two an interaction, and three or more a ## higher-order term. This property is read-only. ## ## It is the complete record, where @code{Interactions} reports only the ## two-way terms, in the form MATLAB reports them. It is also the form ## the @qcode{'Interactions'} option takes back, so passing it to the ## constructor rebuilds a model over the same terms. ## ## @end deftp IntMatrix = []; ## -*- texinfo -*- ## @deftp {CompactClassificationGAM} {property} Intercept ## ## Intercept of the fitted model ## ## A numeric scalar, the log-odds of the response mean, which every ## additive term is measured against. This property is read-only. ## ## @end deftp Intercept = []; ## -*- texinfo -*- ## @deftp {CompactClassificationGAM} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A numeric vector holding the column of each predictor treated as ## categorical, and empty when none is. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {CompactClassificationGAM} {property} ExpandedPredictorNames ## ## Names of the expanded predictor variables ## ## A cell array of character vectors naming the predictors as the model ## sees them. It matches @code{PredictorNames} unless a categorical ## predictor was expanded into dummy variables. This property is ## read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationGAM} {property} BinEdges ## ## Bin edges of the fitted shape functions ## ## A cell array with one row vector per predictor, holding the cut points ## the boosted-tree engine binned it at. It is the empty cell under the ## spline engine, which does no binning. This property is read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationGAM} {property} PairDetectionBinEdges ## ## Bin edges used to detect interactions ## ## A cell array with one row vector per predictor, the eight ## equal-frequency bins pairs were tested on, empty when the model carries ## no interaction terms. This property is read-only. ## ## @end deftp PairDetectionBinEdges = []; ## -*- texinfo -*- ## @deftp {CompactClassificationGAM} {property} FitMethod ## ## Which engine fitted the model ## ## Either @qcode{'boostedtrees'} or @qcode{'splines'}, as the model it was ## compacted from was fitted. This property is read-only. ## ## @end deftp FitMethod = 'boostedtrees'; ## -*- texinfo -*- ## @deftp {CompactClassificationGAM} {property} TreeModel ## ## The fitted shape functions and interaction surfaces ## ## The structure the full model reports, carried over unchanged, and ## empty under the spline engine. This property is read-only. ## ## @end deftp TreeModel = []; endproperties ## Properties a user may set after the model is built. Each one is ## validated by its set method below. properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {CompactClassificationGAM} {property} Cost ## ## Cost of Misclassification ## ## A square matrix specifying the cost of misclassification of a point. ## @qcode{Cost(i,j)} is the cost of classifying a point into class @qcode{j} ## if its true class is @qcode{i} (that is, the rows correspond to the true ## class and the columns correspond to the predicted class). The order of ## the rows and columns in @qcode{Cost} corresponds to the order of the ## classes in @qcode{ClassNames}. The number of rows and columns in ## @qcode{Cost} is the number of unique classes in the response. By ## default, @qcode{Cost(i,j) = 1} if @qcode{i != j}, and ## @qcode{Cost(i,j) = 0} if @qcode{i = j}. In other words, the cost is 0 ## for correct classification and 1 for incorrect classification. ## ## Add or change the @qcode{Cost} property using dot notation as in: ## @itemize ## @item @qcode{@var{obj}.Cost = @var{costMatrix}} ## @end itemize ## ## ## A cost may also be given as a struct with the fields ## @qcode{ClassNames} and @qcode{ClassificationCosts}, which names the ## order its own matrix is written in. That matrix is permuted into the ## order of @qcode{ClassNames} above, so a caller need not know which ## order the classes were sorted into. It must name every class. ## ## A cost must be floating point, not sparse, not complex, non-negative ## and zero down its diagonal, and must hold no @qcode{NaN} or ## @qcode{Inf}. A @code{single} is widened to @code{double}. ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {CompactClassificationGAM} {property} ScoreTransform ## ## Transformation function for classification scores ## ## Specified as a function handle for transforming the classification ## scores. Add or change the @qcode{ScoreTransform} property using dot ## notation as in: ## ## @itemize ## @item @qcode{@var{obj}.ScoreTransform = 'function_name'} ## @item @qcode{@var{obj}.ScoreTransform = @@function_handle} ## @end itemize ## ## When specified as a character vector, it can be any of the following ## built-in functions. Nevertheless, the @qcode{ScoreTransform} property ## always stores their function handle equivalent. ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Value} @tab @var{Description} ## @item @qcode{'doublelogit'} @tab @math{1 ./ (1 + exp (-2 * x))} ## @item @qcode{'invlogit'} @tab @math{log (x ./ (1 - x))} ## @item @qcode{'ismax'} @tab Sets the score for the class with the ## largest score to 1, and for all other classes to 0 ## @item @qcode{'logit'} @tab @math{1 ./ (1 + exp (-x))} ## @item @qcode{'none'} @tab @math{x} (no transformation) ## @item @qcode{'identity'} @tab @math{x} (no transformation) ## @item @qcode{'sign'} @tab ## @math{-1 for x < 0, 0 for x = 0, 1 for x > ## 0} ## @item @qcode{'symmetric'} @tab @math{2 * x - 1} ## @item @qcode{'symmetricismax'} @tab Sets the score for the class ## with the largest score to 1, and for all other classes to -1 ## @item @qcode{'symmetriclogit'} @tab @math{2 ./ (1 + exp (-x)) - 1} ## @end multitable ## ## The default is @qcode{'logit'}, as in MATLAB. This model's raw ## score is a log-odds, reported as the pair @math{[-f, f]} whose two ## columns sum to zero, and the transform is what turns it into the ## posterior probabilities that sum to one. Every transform therefore ## composes on the log-odds and not on the probabilities, so ## @qcode{'none'} returns the log-odds themselves. ## ## @end deftp ScoreTransform = 'logit'; endproperties ## Readable by the counterpart class, which copies it, and kept out of ## the documented surface. properties (GetAccess = public, SetAccess = protected, Hidden) ## Carried from the fitted model so a fold, which is stored ## compact, can still say how many trees it fitted. NumTrainedTrees = []; STfun = @(x) 1 ./ (1 + exp (-x)); endproperties ## Set methods for the properties a user may assign. methods (Hidden) function this = set.Cost (this, val) gnY = this.ClassNames; if (isempty (val)) this.Cost = cast (! eye (classCount (gnY)), 'double'); else ## Everything a cost must be, and the struct form, which ## is permuted into this model's class order. [val, errmsg] = costMatrix (val, gnY); if (! isempty (errmsg)) error ("CompactClassificationGAM: %s", errmsg); endif this.Cost = val; endif endfunction function this = set.ScoreTransform (this, val) [f, nm] = parseScoreTransform (val, 'CompactClassificationGAM'); this.ScoreTransform = nm; this.STfun = f; endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationGAM} {@var{obj} =} CompactClassificationGAM (@var{Mdl}) ## @deftypefnx {CompactClassificationGAM} {@var{obj} =} CompactClassificationGAM () ## ## Create a @code{CompactClassificationGAM} object. ## ## @var{Mdl} is the @code{ClassificationGAM} object to ## compact. The documented way to reach this constructor is the ## @code{compact} method. ## ## Called with no arguments it returns an object with its properties ## empty, which is how a saved model is rebuilt before its values are ## filled in. ## ## @end deftypefn function this = CompactClassificationGAM (Mdl = []) ## Check for appropriate class if (isempty (Mdl)) return; elseif (! strcmpi (class (Mdl), 'ClassificationGAM')) error ("CompactClassificationGAM: invalid classification object."); endif ## The levels a predictor read from a table was coded through ## travel with the model, so a compact one still reads a table this.PredictorLevels = Mdl.PredictorLevels; ## Save properties to compact model this.NumPredictors = Mdl.NumPredictors; this.PredictorNames = Mdl.PredictorNames; this.ResponseName = Mdl.ResponseName; this.ClassNames = Mdl.ClassNames; this.Cost = Mdl.Cost; this.Prior = Mdl.Prior; this.ScoreTransform = Mdl.ScoreTransform; this.STfun = Mdl.STfun; this.Formula = Mdl.Formula; this.Interactions = Mdl.Interactions; this.BaseModel = Mdl.BaseModel; this.ModelwInt = Mdl.ModelwInt; this.IntMatrix = Mdl.IntMatrix; this.Intercept = Mdl.Intercept; this.FitMethod = Mdl.FitMethod; this.TreeModel = Mdl.TreeModel; this.BinEdges = Mdl.BinEdges; this.PairDetectionBinEdges = Mdl.PairDetectionBinEdges; this.NumTrainedTrees = Mdl.NumTrainedTrees; this.CategoricalPredictors = Mdl.CategoricalPredictors; this.ExpandedPredictorNames = Mdl.ExpandedPredictorNames; endfunction ## Custom display function disp (this) fprintf ("\n CompactClassificationGAM\n\n"); ## Print selected properties fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); if (iscellstr (this.ClassNames)) str = repmat ({'''%s'''}, 1, numel (this.ClassNames)); str = strcat ('{', strjoin (str, ' '), '}'); str = sprintf (str, this.ClassNames{:}); elseif (ischar (this.ClassNames)) str = repmat ({'''%s'''}, 1, rows (this.ClassNames)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, cellstr (this.ClassNames){:}); else # single, double, logical str = repmat ({'%d'}, 1, numel (this.ClassNames)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, this.ClassNames); endif fprintf ("%+25s: %s\n", 'ClassNames', str); fprintf ("%+25s: '%s'\n", 'ScoreTransform', this.ScoreTransform); fprintf ("%+25s: %d\n", 'NumObservations', this.NumObservations); fprintf ("%+25s: %d\n", 'NumPredictors', this.NumPredictors); if (! isempty (this.Formula)) fprintf ("%+25s: '%s'\n", 'Formula', this.Formula); endif if (! isempty (this.Interactions)) fprintf ("%+25s: [%dx%d %s]\n", 'Interactions', ... size (this.Interactions, 1), size (this.Interactions, 2), ... class (this.Interactions)); endif endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {CompactClassificationGAM} {@var{label} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {CompactClassificationGAM} {[@var{label}, @var{score}] =} predict (@var{obj}, @var{XC}) ## @deftypefnx {CompactClassificationGAM} {[@var{label}, @var{score}] =} predict (@dots{}, @qcode{'IncludeInteractions'}, @var{includeInteractions}) ## ## Predict labels for new data using the Generalized Additive Model (GAM) ## stored in a CompactClassificationGAM object. ## ## @code{@var{label} = predict (@var{obj}, @var{XC})} returns the predicted ## labels for the data in @var{XC} based on the model stored in the ## CompactClassificationGAM object, @var{obj}. ## ## @code{[@var{label}, @var{score}] = predict (@var{obj}, @var{XC})} also ## returns @var{score}, which contains the predicted class scores or ## posterior probabilities for each observation. Every row is predicted. ## Under boosted trees a missing value adds nothing from a main effect, ## and an interaction term takes the value its trees give a row missing ## that predictor; under splines a row holding a missing value is scored ## @code{NaN} and takes the class of largest prior. ## ## @code{[@var{label}, @var{score}] = predict (@var{obj}, @var{XC}, ## 'IncludeInteractions', @var{includeInteractions})} allows you to specify ## whether interaction terms should be included when making predictions. ## ## @itemize ## @item ## @var{obj} must be a @qcode{CompactClassificationGAM} class object. ## @item ## @var{XC} must be an @math{M*P} numeric matrix where each row is an ## observation and each column corresponds to a predictor variable. ## @item ## @var{includeInteractions} is a logical scalar indicating whether to ## include interaction terms in the predictions. ## @end itemize ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: one ## the model was not fitted on is passed over, one it needs and cannot ## find is named, and a value holding a level is coded as that level ## was coded at fitting. ## @seealso{CompactClassificationGAM, ClassificationGAM, fitcgam} ## @end deftypefn function [labels, scores] = predict (this, XC, varargin) ## Check for sufficient input arguments if (nargin < 2) error ("CompactClassificationGAM.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'CompactClassificationGAM.predict', XC); ## Check for valid XC if (isempty (XC)) error ("CompactClassificationGAM.predict: XC is empty."); elseif (this.NumPredictors != columns (XC)) error (strcat ("CompactClassificationGAM.predict: XC must have", ... " the same number of features as the trained model.")); endif ## Default values for Name-Value Pairs ## Which store holds the interaction terms depends on the engine: the ## spline scheme keeps them as extra columns described by IntMatrix, ## the boosted-tree scheme as surfaces over predictor pairs. hasInt = ! isempty (this.IntMatrix); if (strcmp (this.FitMethod, 'boostedtrees') && ! isempty (this.TreeModel)) hasInt = ! isempty (this.TreeModel.Pairs); endif incInt = hasInt; Cost = this.Cost; ## Parse optional paired arguments; interactions are included when the ## model has them if (mod (numel (varargin), 2) != 0) error (strcat ("CompactClassificationGAM.predict: optional", ... " arguments must be given in Name-Value pairs.")); endif optNames = {'IncludeInteractions'}; dfValues = {incInt}; [incInt, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! islogical (incInt) || (incInt != 0 && incInt != 1)) error (strcat ("CompactClassificationGAM.predict:", ... " includeinteractions must be a logical value.")); endif if (incInt && ! hasInt) error (strcat ("CompactClassificationGAM.predict: trained model", ... " does not include any interactions.")); endif if (! isempty (args)) error (strcat ("CompactClassificationGAM.predict: invalid optional", ... " paired argument.")); endif ## The boosted-tree engine keeps its fit as step functions over bins, so ## a term is a lookup rather than a spline evaluation and the whole ## prediction is one call. Everything after it is shared: the cost ## matrix, the label, and the transform. ## An empty TreeModel means no tree fit is present, whatever FitMethod ## says: a default-constructed object has one, and so would a model ## saved before the engine existed. Such an object falls through to ## the spline path rather than indexing into nothing. if (strcmp (this.FitMethod, 'boostedtrees') && ! isempty (this.TreeModel)) ## Excluding the interactions means excluding the constant they ## handed the intercept as well. [Xb, E] = gamCatCode (this.TreeModel, XC, this.BinEdges); interc = this.Intercept; if (! incInt && isfield (this.TreeModel, 'PairIntercept')) interc = interc - this.TreeModel.PairIntercept; endif if (! incInt || isempty (this.TreeModel.Pairs)) scores = gamboostpredict (E, ... this.TreeModel.ShapeValues, Xb, ... interc); else [PE, PM] = gamPairEdges (this.TreeModel, this.PairDetectionBinEdges); scores = gamboostpredict (E, ... this.TreeModel.ShapeValues, Xb, ... interc, 0, PE, ... this.TreeModel.PairValues, ... this.TreeModel.Pairs, PM); endif scores = [-scores, scores]; post = 1 ./ (1 + exp (-scores)); numObservations = size (XC, 1); CE = zeros (numObservations, 2); for k = 1:2 for i = 1:2 CE(:, k) = CE(:, k) + post(:, i) * Cost(k, i); endfor endfor [~, minIdx] = min (CE, [], 2); labels = labelsFromIndex (this.ClassNames, minIdx); scores = this.STfun (scores); return; endif ## Choose whether interactions must be included if (incInt) ## Which construction path the model took: an interaction ## list appends its terms to the predictors, a formula ## names every term the model has and replaces them. if (isempty (this.Formula)) ## Append interaction terms to the predictor matrix for i = 1:rows (this.IntMatrix) tindex = logical (this.IntMatrix(i,:)); Xterms = XC(:,tindex); Xinter = ones (rows (XC), 1); for c = 1:sum (tindex) Xinter = Xinter .* Xterms(:,c); endfor ## Append interaction terms XC = [XC, Xinter]; endfor else ## Add selected predictors and interaction terms XN = []; for i = 1:rows (this.IntMatrix) tindex = logical (this.IntMatrix(i,:)); Xterms = XC(:,tindex); Xinter = ones (rows (XC), 1); for c = 1:sum (tindex) Xinter = Xinter .* Xterms(:,c); endfor ## Append selected predictors and interaction terms XN = [XN, Xinter]; endfor XC = XN; endif ## Get parameters and intercept vectors from model with interactions params = this.ModelwInt.Parameters; Interc = this.ModelwInt.Intercept; else ## Get parameters and intercept vectors from base model params = this.BaseModel.Parameters; Interc = this.BaseModel.Intercept; endif ## Predict the raw score from testing data scores = predict_val (params, XC, Interc); ## Expected misclassification cost is defined on the posteriors, which ## the score becomes only under the logistic link, so the label is ## taken from those and not from the score the caller is handed. post = 1 ./ (1 + exp (-scores)); ## Compute the expected misclassification cost matrix numObservations = size (XC, 1); CE = zeros (numObservations, 2); for k = 1:2 for i = 1:2 CE(:, k) = CE(:, k) + post(:, i) * Cost(k, i); endfor endfor ## Select the class with the minimum expected misclassification cost. ## A spline term has no value at a missing predictor, so such a row has ## no score, and takes the class of largest prior as other learners do. [~, minIdx] = min (CE, [], 2); [~, top] = max (this.Prior); minIdx(isnan (scores(:,1))) = top; labels = labelsFromIndex (this.ClassNames, minIdx); ## Apply ScoreTransform scores = this.STfun (scores); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationGAM} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationGAM} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationGAM} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margin of a compact generalized additive model. ## ## @code{@var{m} = margin (@var{obj}, @var{X}, @var{Y})} returns a column ## vector holding, for each row of @var{X}, the score the model gives its ## true class in @var{Y} less the score it gives the other class. A ## positive margin means the observation is classified correctly, and the ## larger it is the more confidently so. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{CompactClassificationGAM, ClassificationGAM, ## CompactClassificationGAM.edge, CompactClassificationGAM.loss, ## CompactClassificationGAM.predict} ## @end deftypefn function m = margin (this, X, Y) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactClassificationGAM.margin: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y] = tableResponse (this, 'margin', X, Y, {}, nargin > 2); [X, Y] = checkXY_ (this, X, Y, "margin"); [~, scores] = predict (this, X); classes = this.ClassNames; m = zeros (rows (X), 1); ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:rows (X) idx = gYidx(i); if (isempty (idx)) m(i) = NaN; continue; endif true_score = scores(i, idx); scores(i, idx) = -Inf; m(i) = true_score - max (scores(i,:)); scores(i, idx) = true_score; endfor endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationGAM} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationGAM} {@var{e} =} edge (@dots{}, @qcode{"Weights"}, @var{w}) ## @deftypefnx {CompactClassificationGAM} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationGAM} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Classification edge of a compact generalized additive model. ## ## @code{@var{e} = edge (@var{obj}, @var{X}, @var{Y})} returns the mean of ## the classification margins over the rows of @var{X}. ## ## @code{@var{e} = edge (@dots{}, @qcode{"Weights"}, @var{w})} takes the ## weighted mean instead, with one weight per row of @var{X}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{CompactClassificationGAM, ClassificationGAM, ## CompactClassificationGAM.margin, CompactClassificationGAM.loss, ## CompactClassificationGAM.predict} ## @end deftypefn function e = edge (this, X, Y, varargin) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactClassificationGAM.edge: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'edge', X, Y, varargin, ... nargin > 2); [X, Y] = checkXY_ (this, X, Y, "edge"); ## The weights are normalized within each class to that class's prior, ## which is what the oracle does and is not the same as dividing by ## their total. This used to divide by the total. ## The weights are parsed before anything is computed, so a bad ## Name-Value pair is reported as such rather than after a margin. W = edgeWeights (varargin, Y, this.ClassNames, this.Prior, ... "CompactClassificationGAM", "edge"); m = margin (this, X, Y); e = sum (W .* m(:)) / sum (W); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationGAM} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationGAM} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationGAM} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {CompactClassificationGAM} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Classification loss of a compact generalized additive model. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} returns the loss of ## the model on the rows of @var{X} against the true labels @var{Y}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{L} = loss (@dots{}, @var{name}, @var{value})} accepts the ## following name-value pairs: ## ## @itemize ## @item ## @qcode{"LossFun"} selects the loss. Supported values are ## @qcode{"mincost"}, the default, @qcode{"binodeviance"}, ## @qcode{"classifcost"}, @qcode{"classiferror"}, @qcode{"exponential"}, ## @qcode{"hinge"}, @qcode{"logit"} and @qcode{"quadratic"}. ## @qcode{"mincost"} assigns each observation to the class of least ## expected cost and charges what that assignment costs, so it reads the ## scores as a posterior, which is what this model returns; ## @qcode{"classifcost"} charges what the model's own prediction costs. ## ## @item ## @qcode{"Weights"} holds one weight per row of @var{X}, normalised to ## sum to one before it is applied. ## @end itemize ## ## @seealso{CompactClassificationGAM, ClassificationGAM, ## CompactClassificationGAM.margin, CompactClassificationGAM.edge, ## CompactClassificationGAM.predict} ## @end deftypefn function L = loss (this, X, Y, varargin) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactClassificationGAM.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("CompactClassificationGAM.loss: Name-Value", ... " arguments must be in pairs.")); endif [X, Y] = checkXY_ (this, X, Y, "loss"); ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights'}; dfValues = {'mincost', []}; [LossFun, W, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments lossnames = {'binodeviance', 'classifcost', 'classiferror', ... 'exponential', 'hinge', 'logit', 'mincost', 'quadratic'}; if (! (ischar (LossFun) && isrow (LossFun))) error (strcat ("CompactClassificationGAM.loss: 'LossFun' must be a", ... " character vector.")); endif LossFun = tolower (LossFun); if (! any (strcmpi (LossFun, lossnames))) error ("CompactClassificationGAM.loss: unsupported Loss function."); endif errmsg = weightsClass (W); if (! isempty (errmsg)) error ("CompactClassificationGAM.loss: %s", errmsg); endif if (! isempty (W) && ! (isnumeric (W) && isvector (W))) error (strcat ("CompactClassificationGAM.loss: 'Weights' must be a", ... " numeric vector.")); endif if (! isempty (W) && numel (W) != rows (X)) error (strcat ("CompactClassificationGAM.loss: size of 'Weights'", ... " must equal the number of rows in X.")); endif if (! isempty (args)) error (strcat ("CompactClassificationGAM.loss: invalid optional", ... " paired argument.")); endif if (isempty (W)) W = ones (rows (X), 1); endif W = double (W(:)); W = W / sum (W); [label, scores] = predict (this, X); classes = this.ClassNames; ## Membership of the true class, as an indicator per class Yind = zeros (rows (X), classCount (classes)); ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:rows (X) idx = gYidx(i); if (isempty (idx)) L = NaN; return; endif Yind(i, idx) = 1; endfor ## The scalar score of the true class of each observation mj = sum (scores .* Yind, 2); switch (LossFun) case 'classiferror' wrong = zeros (rows (X), 1); for i = 1:rows (X) wrong(i) = ! isequal (label(i), Y(i)); endfor L = sum (W .* wrong); case 'binodeviance' L = sum (W .* log (1 + exp (-2 * mj))); case 'hinge' L = sum (W .* max (0, 1 - mj)); case 'exponential' L = sum (W .* exp (-mj)); case 'logit' L = sum (W .* log (1 + exp (-mj))); case 'quadratic' L = sum (W .* (1 - mj) .^ 2); case 'mincost' ## Each observation is assigned to the class of least expected ## cost, and charged what that assignment actually costs given its ## true class. L = 0; ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:rows (X) [~, k] = min (scores(i,:) * this.Cost); true_idx = gYidx(i); L = L + W(i) * this.Cost(true_idx, k); endfor case 'classifcost' ## What the model's own prediction costs, given the true class L = 0; ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:rows (X) true_idx = gYidx(i); pred_idx = find (ismember (classes, label(i))); L = L + W(i) * this.Cost(true_idx, pred_idx); endfor endswitch endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationGAM} {} savemodel (@var{obj}, @var{filename}) ## ## Save a CompactClassificationGAM object. ## ## @code{savemodel (@var{obj}, @var{filename})} saves each property of a ## CompactClassificationGAM object into an Octave binary file, the name of ## which is specified in @var{filename}, along with an extra variable, ## which defines the type classification object these variables constitute. ## Use @code{loadmodel} in order to load a classification object into ## Octave's workspace. ## ## @seealso{loadmodel, fitcgam, ClassificationGAM, CompactClassificationGAM} ## @end deftypefn function savemodel (this, fname) if (nargin < 2) error ("CompactClassificationGAM.savemodel: too few input arguments."); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error ("CompactClassificationGAM.savemodel: FNAME must be a character vector."); endif ## Generate variable for class name classdef_name = 'CompactClassificationGAM'; ## Create variables from model properties NumPredictors = this.NumPredictors; PredictorNames = this.PredictorNames; ResponseName = this.ResponseName; ClassNames = encodeLabels (this.ClassNames); Prior = this.Prior; Cost = this.Cost; ScoreTransform = this.ScoreTransform; Intercept = this.Intercept; CategoricalPredictors = this.CategoricalPredictors; ExpandedPredictorNames = this.ExpandedPredictorNames; STfun = this.STfun; Formula = this.Formula; Interactions = this.Interactions; BaseModel = this.BaseModel; ModelwInt = this.ModelwInt; IntMatrix = this.IntMatrix; FitMethod = this.FitMethod; TreeModel = this.TreeModel; BinEdges = this.BinEdges; PairDetectionBinEdges = this.PairDetectionBinEdges; ## Save classdef name and all model properties as individual variables save ('-binary', fname, 'classdef_name', 'NumPredictors', ... 'PredictorNames', 'ResponseName', 'ClassNames', 'Prior', 'Cost', ... 'ScoreTransform', 'STfun', 'Intercept', ... 'CategoricalPredictors', 'ExpandedPredictorNames', ... 'Formula', 'Interactions', 'BaseModel', 'ModelwInt', ... 'IntMatrix', 'FitMethod', 'TreeModel', 'BinEdges', ... 'PairDetectionBinEdges'); endfunction endmethods methods (Access = private) ## Shared validation for the assessment methods, so each reports under ## its own name. function [X, Y] = checkXY_ (this, X, Y, caller) if (isempty (X)) error ("CompactClassificationGAM.%s: X is empty.", caller); elseif (this.NumPredictors != columns (X)) error (strcat ("CompactClassificationGAM.%s: X must have the", ... " same number of predictors as the trained", ... " model."), caller); endif if (isempty (Y)) error ("CompactClassificationGAM.%s: Y is empty.", caller); elseif (rows (X) != rows (Y)) error (strcat ("CompactClassificationGAM.%s: Y must have the", ... " same number of rows as X."), caller); endif endfunction endmethods methods(Static, Hidden) function mdl = load_model (filename, data) data = decodeLabels (data); ## Create a CompactClassificationGAM object mdl = CompactClassificationGAM (); ## Copy the saved data into the object. Iterate over what was ## saved rather than over fieldnames (mdl): a private property such ## as STfun is written out by savemodel but is not reported by ## fieldnames, so comparing the two sets could never match and every ## load failed. Assignment is legal here because this is a method of ## the class itself. names = fieldnames (data); ## The set methods for these read other properties, and one of them ## rebuilds Coeffs, so they are assigned once everything else is in ## place rather than in the order the file happens to list them. late = ismember (names, {'Cost', 'Prior', 'ScoreTransform', ... 'ResponseTransform'}); names = [names(! late); names(late)]; for i = 1:numel (names) try mdl.(names{i}) = data.(names{i}); catch error ("CompactClassificationGAM.load_model: invalid model in '%s'.", ... filename) end_try_catch endfor endfunction endmethods methods(Access = private) ## Set cost endmethods endclassdef ## Helper function function scores = predict_val (params, XC, intercept) ## The shared prediction engine evaluates every additive term and sums ## them. That sum is the log-odds of the second class, so the raw score of ## the first is its negative and the pair sums to zero, as MATLAB's does. f = gampredict (params, XC, intercept, 0); scores = [-f, f]; endfunction %!demo %! ## Create a generalized additive model classifier and its compact version %! # and compare their size %! %! load fisheriris %! X = meas; %! Y = species; %! %! Mdl = fitcdiscr (X, Y, 'ClassNames', unique (species)) %! CMdl = crossval (Mdl) ## Test constructor %!test %! Mdl = CompactClassificationGAM (); %! assert_equal (class (Mdl), "CompactClassificationGAM") %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = [0; 0; 1; 1]; %! PredictorNames = {'Feature1', 'Feature2', 'Feature3'}; %! Mdl = fitcgam (x, y, 'FitMethod', 'splines', ... %! 'PredictorNames', PredictorNames); %! CMdl = compact (Mdl); %! assert_equal (class (CMdl), "CompactClassificationGAM"); %! assert_equal ({CMdl.NumPredictors, CMdl.ResponseName}, {3, 'Y'}) %! assert_equal (CMdl.ClassNames, [0; 1]) %! assert_equal (CMdl.PredictorNames, PredictorNames) %! assert_equal (CMdl.BaseModel.Intercept, 0) %!test %! load fisheriris %! inds = strcmp (species,'versicolor') | strcmp (species,'virginica'); %! X = meas(inds, :); %! Y = species(inds, :)'; %! Y = strcmp (Y, 'virginica')'; %! Mdl = fitcgam (X, Y, 'FitMethod', 'splines', ... %! 'Formula', 'Y ~ x1 + x2 + x3 + x4 + x1:x2 + x2:x3'); %! CMdl = compact (Mdl); %! assert_equal (class (CMdl), "CompactClassificationGAM"); %! assert_equal ({CMdl.NumPredictors, CMdl.ResponseName}, {4, 'Y'}) %! assert_equal (CMdl.ClassNames, logical ([0; 1])) %! assert_equal (CMdl.Formula, 'Y ~ x1 + x2 + x3 + x4 + x1:x2 + x2:x3') %! assert_equal (CMdl.PredictorNames, {'x1', 'x2', 'x3', 'x4'}) %! assert_equal (CMdl.ModelwInt.Intercept, 0) %!test %! X = [2, 3, 5; 4, 6, 8; 1, 2, 3; 7, 8, 9; 5, 4, 3]; %! Y = [0; 1; 0; 1; 1]; %! Mdl = fitcgam (X, Y, 'FitMethod', 'splines', ... %! 'Knots', [4, 4, 4], 'Order', [3, 3, 3]); %! CMdl = compact (Mdl); %! assert_equal (class (CMdl), "CompactClassificationGAM"); %! assert_equal ({CMdl.NumPredictors, CMdl.ResponseName}, {3, 'Y'}) %! assert_equal (CMdl.ClassNames, [0; 1]) %! assert_equal (CMdl.PredictorNames, {'x1', 'x2', 'x3'}) %! assert_equal (CMdl.BaseModel.Intercept, 0.4055, 1e-1) ## Test input validation for constructor ## A compact model keeps the character class names and predicts whole names. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Cc = compact (fitcgam (Xch, Ych)); %! rand ("state", 1); randn ("state", 1); Cs = compact (fitcgam (Xch, Ycell)); %! assert_equal (cellstr (Cc.ClassNames), Cs.ClassNames); %! assert_equal (cellstr (predict (Cc, Xch)), predict (Cs, Xch)); ## and reads one back in its assessment methods. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Cc = compact (fitcgam (Xch, Ych)); %! rand ("state", 1); randn ("state", 1); Cs = compact (fitcgam (Xch, Ycell)); %! assert_equal (loss (Cc, Xch, Ych), loss (Cs, Xch, Ycell), 1e-12); %!test # a row missing a predictor is kept, and without a spline score it %! ## takes the class of largest prior %! x = linspace (0, 1, 30)'; %! X = [x, cos(4 * x)]; %! y = [ones(18, 1); 2 * ones(12, 1)]; %! Mdl = compact (ClassificationGAM (X, y, 'FitMethod', 'splines')); %! [label, score] = predict (Mdl, [0.5, 0.2; NaN, 0.2]); %! assert_equal (size (label), [2, 1]); %! assert_equal (label(2), 1); %! assert_equal (score(2,:), [NaN, NaN]); %!test # a row missing every predictor is scored from the intercept %! k = (1:200)'; %! X = [sin(k), cos(2 * k), mod(k, 5)]; %! y = 2 * sin (k) + X(:,2) .^ 2 + 0.3 * X(:,3); %! Q = [0.5, 0.2, 1; NaN, 0.2, 1; 0.5, NaN, 1; NaN, NaN, NaN; 0.1, 0.2, 1; ... %! NaN, 0.7, 3; NaN, NaN, 1]; %! Mdl = compact (ClassificationGAM (X, y > median (y))); %! [label, score] = predict (Mdl, Q); %! assert_equal (size (label), [7, 1]); %! assert_equal (score(4,2), 1 / (1 + exp (-Mdl.Intercept)), 1e-14); %!error ... %! CompactClassificationGAM (1) ## Test predict method %!test %! x = [1, 2; 3, 4; 5, 6; 7, 8; 9, 10]; %! y = [1; 0; 1; 0; 1]; %! Mdl = fitcgam (x, y, 'FitMethod', 'splines', 'interactions', 'all'); %! CMdl = compact (Mdl); %! l = [1; 0; 1; 0; 1]; %! s = [0.0334, 0.9666; 0.9648, 0.0352; 0.0334, 0.9666; ... %! 0.9648, 0.0352; 0.0334, 0.9666]; %! [labels, scores] = predict (CMdl, x); %! assert_equal (class (CMdl), "CompactClassificationGAM"); %! assert_equal ({CMdl.NumPredictors, CMdl.ResponseName}, {2, 'Y'}) %! assert_equal (CMdl.ClassNames, [0; 1]) %! assert_equal (CMdl.PredictorNames, {'x1', 'x2'}) %! assert_equal (CMdl.ModelwInt.Intercept, 0.4055, 1e-1) %! assert_equal (labels, l) %! assert_equal (scores, s, 1e-1) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = [0; 0; 1; 1]; %! interactions = [false, true, false; true, false, true; false, true, false]; %! Mdl = fitcgam (x, y, 'FitMethod', 'splines', ... %! 'learningrate', 0.2, 'interactions', interactions); %! CMdl = compact (Mdl); %! [label, score] = predict (CMdl, x, 'includeinteractions', true); %! l = [0; 0; 1; 1]; %! s = [0.9725, 0.0275; 0.9895, 0.0105; 0.0070, 0.9930; 0.0238, 0.9762]; %! assert_equal (class (CMdl), "CompactClassificationGAM"); %! assert_equal ({CMdl.NumPredictors, CMdl.ResponseName}, {3, 'Y'}) %! assert_equal (CMdl.ClassNames, [0; 1]) %! assert_equal (CMdl.PredictorNames, {'x1', 'x2', 'x3'}) %! assert_equal (CMdl.ModelwInt.Intercept, 0) %! assert_equal (label, l) %! assert_equal (score, s, 1e-1) ## Test input validation for predict method %!shared CMdl %! Mdl = fitcgam (ones (4,2), ones (4,1)); %! CMdl = compact (Mdl); %!error ... %! predict (CMdl) %!error ... %! predict (CMdl, []) %!error ... %! predict (CMdl, 1) %!error ... %! predict (CMdl, ones (4,2), 'Bogus', 1) %!error ... %! savemodel (CompactClassificationGAM ()) %!error ... %! savemodel (CompactClassificationGAM (), 1) %!error ... %! savemodel (CompactClassificationGAM (), ['ab'; 'cd']) ## The compact model carries the full model's transform, which defaults to ## 'logit' as MATLAB's does, so its scores are posteriors summing to one. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! CMdl = compact (fitcgam (meas(inds,:), species(inds))); %! assert_equal (CMdl.ScoreTransform, 'logit'); %! [~, scores] = predict (CMdl, meas(1:6,:)); %! assert_equal (sum (scores, 2), ones (6, 1), 1e-12); ## A ScoreTransform is stored under its own name. That it reaches the ## scores is covered above, through predict. %!test %! CMdl = compact (fitcgam ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2])); %! CMdl.ScoreTransform = 'symmetric'; %! assert_equal (class (CMdl.ScoreTransform), 'char'); %! assert_equal (CMdl.ScoreTransform, 'symmetric'); ## The compact model carries what MATLAB's compact model reports. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = fitcgam (meas(inds,:), species(inds)); %! CMdl = compact (Mdl); %! assert_equal (CMdl.Intercept, Mdl.Intercept); %! assert_equal (CMdl.CategoricalPredictors, Mdl.CategoricalPredictors); %! assert_equal (CMdl.ExpandedPredictorNames, Mdl.ExpandedPredictorNames); ## margin, edge and loss agree with the model it was compacted from. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! X = meas(inds,:); %! Y = species(inds); %! Mdl = fitcgam (X, Y); %! CMdl = compact (Mdl); %! assert_equal (margin (CMdl, X, Y), margin (Mdl, X, Y)); %! assert_equal (edge (CMdl, X, Y), edge (Mdl, X, Y)); %! assert_equal (loss (CMdl, X, Y), loss (Mdl, X, Y)); ## A saved and reloaded compact model carries every property it holds. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! CMdl = compact (fitcgam (meas(inds,:), species(inds), ... %! 'FitMethod', 'splines')); %! fname = tempname (); %! savemodel (CMdl, fname); %! CMdl2 = loadmodel (fname); %! delete (fname); %! assert_equal (CMdl2.Intercept, CMdl.Intercept); %! assert_equal (CMdl2.ClassNames, CMdl.ClassNames); %! assert_equal (CMdl2.BaseModel.Parameters(1).coefs, ... %! CMdl.BaseModel.Parameters(1).coefs); %! assert_equal (predict (CMdl2, meas(inds,:)), predict (CMdl, meas(inds,:))); ## The same round trip under the boosted-tree engine, whose fit lives in ## TreeModel rather than BaseModel. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! CMdl = compact (fitcgam (meas(inds,:), species(inds), ... %! 'FitMethod', 'boostedtrees')); %! fname = tempname (); %! savemodel (CMdl, fname); %! CMdl2 = loadmodel (fname); %! delete (fname); %! assert_equal (CMdl2.Intercept, CMdl.Intercept); %! assert_equal (CMdl2.TreeModel.ShapeValues, CMdl.TreeModel.ShapeValues); %! assert_equal (CMdl2.BinEdges, CMdl.BinEdges); %! assert_equal (predict (CMdl2, meas(inds,:)), predict (CMdl, meas(inds,:))); %!shared x2, y2, CM %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! x2 = meas(inds,:); %! y2 = species(inds); %! CM = compact (fitcgam (x2, y2)); ## Test input validation for the assessment methods %!error ... %! margin (CM, x2) %!error ... %! margin (CM, [], y2) %!error ... %! edge (CM, x2, y2, 'Weights') %!error ... %! loss (CM, x2, y2, 'LossFun', 'nonsense') %!error ... %! loss (CM, x2, y2, 'Bogus', 1) ## A fitted model survives savemodel and loadmodel: the properties come ## back as they were and it predicts the same. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = compact (fitcgam (meas(inds,:), species(inds))); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2), 'CompactClassificationGAM'); %! assert_equal (M2.PredictorNames, Mdl.PredictorNames); %! assert_equal (class (M2.ScoreTransform), class (Mdl.ScoreTransform)); %! assert_equal (predict (M2, meas(1:5,:)), predict (Mdl, meas(1:5,:))); %!error ... %! Mdl = fitcgam ([1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1], [0; 0; 1; 1]); %! CMdl = compact (Mdl); %! CMdl.Cost = 1:4; ## The shared cost guard is in force here too, and the struct form is ## permuted into this model's class order. The battery is on ## ClassificationDiscriminant. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = compact (fitcgam (meas(inds,:), species(inds))); %! S = struct ('ClassNames', {{'versicolor'; 'setosa'}}, ... %! 'ClassificationCosts', [0, 1; 2, 0]); %! Mdl.Cost = S; %! assert_equal (Mdl.Cost, [0, 2; 1, 0]); %!error ... %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = compact (fitcgam (meas(inds,:), species(inds))); %! Mdl.Cost = ones (2); ## A compacted tree-fitted model carries the engine and predicts alike. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! X = meas(inds,:); %! Mdl = fitcgam (X, species(inds), 'FitMethod', 'boostedtrees'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.FitMethod, 'boostedtrees'); %! assert_equal (CMdl.TreeModel.ShapeValues, Mdl.TreeModel.ShapeValues); %! assert_equal (predict (CMdl, X), predict (Mdl, X)); %! fname = tempname (); %! savemodel (CMdl, fname); %! C2 = loadmodel (fname); %! delete (fname); %! assert_equal (predict (C2, X), predict (CMdl, X)); ## Every documented score transform reaches the scores that are reported, and ## none of them moves the label: a transform reshapes what is reported, not ## what is decided. %!test %! load fisheriris %! Mdl = compact (fitcgam (meas, strcmp (species, 'setosa'))); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! T = {'identity', @(x) x; 'doublelogit', @(x) 1 ./ (1 + exp (-2 * x)); ... %! 'invlogit', @(x) log (x ./ (1 - x)); ... %! 'logit', @(x) 1 ./ (1 + exp (-x)); ... %! 'sign', @(x) sign (x); 'symmetric', @(x) 2 * x - 1; ... %! 'symmetriclogit', @(x) 2 ./ (1 + exp (-x)) - 1}; %! for i = 1:rows (T) %! Mdl.ScoreTransform = T{i,1}; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, T{i,2}(raw), 1e-12); %! assert_equal (l, label); %! endfor %! ## ismax marks the largest score of each observation, ties to the first. %! [~, k] = max (raw, [], 2); %! e = zeros (size (raw)); %! e(sub2ind (size (raw), (1:rows (raw))', k)) = 1; %! Mdl.ScoreTransform = 'ismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, e); %! Mdl.ScoreTransform = 'symmetricismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, 2 * e - 1); ## A function handle is taken as given and applied to the scores. %!test %! load fisheriris %! Mdl = compact (fitcgam (meas, strcmp (species, 'setosa'))); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! Mdl.ScoreTransform = @(x) x .^ 2; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, raw .^ 2, 1e-12); %! assert_equal (l, label); ## A table at prediction %!test # the levels a predictor was coded through travel with the model %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! X = meas(inds,:); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Wide = categorical (X(:,2) > 2.9, [false true], {'narrow', 'wide'}); %! T.Species = categorical (species(inds)); %! CMdl = compact (fitcgam (T, 'Species')); %! assert_equal (numel (CMdl.PredictorLevels), 3); %! assert_equal (CMdl.PredictorLevels{3}, {'narrow', 'wide'}); %!test # predict takes a table, read by name and not by position %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! X = meas(inds,:); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = categorical (species(inds)); %! CMdl = compact (fitcgam (T, 'Species')); %! a = predict (CMdl, T); %! assert_equal (class (a), 'categorical'); %! assert_equal (predict (CMdl, T(:, [3, 2, 1])), a); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(51:150,1:2); %! y = categorical (species(51:150)); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcgam (T, 'Species')); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'Species'), a); %! assert_equal (loss (Mdl, T), a); ## A table at edge %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(51:150,1:2); %! y = categorical (species(51:150)); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcgam (T, 'Species')); %! a = edge (Mdl, X, y); %! assert_equal (edge (Mdl, T(:,1:2), y), a); %! assert_equal (edge (Mdl, T, 'Species'), a); %! assert_equal (edge (Mdl, T), a); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(51:150,1:2); %! y = categorical (species(51:150)); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcgam (T, 'Species')); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## Observation weights of class single or double %!error ... %! loss (compact (fitcgam ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2])), ... %! [1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', int8 ([1; 1; 1; 1])) statistics-release-2.1.2/inst/Machine_Learning/CompactClassificationNaiveBayes.m000066400000000000000000001235001527311104300300510ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef CompactClassificationNaiveBayes < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} CompactClassificationNaiveBayes ## ## Compact naive Bayes classification ## ## A @code{CompactClassificationNaiveBayes} object carries the fitted ## densities of a @code{ClassificationNaiveBayes} model and everything ## @code{predict} needs, but not the observations the model was fitted on. ## It classifies new data identically to the model it came from, and is far ## smaller to keep or to ship. ## ## Create one with the @code{compact} method of a ## @code{ClassificationNaiveBayes} object. Because it holds no training ## data, it has no @code{resub} methods and cannot be cross-validated. ## ## @seealso{ClassificationNaiveBayes, fitcnb} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CompactClassificationNaiveBayes} {property} DistributionNames ## ## Predictor distributions ## ## A cell array of character vectors with one entry per predictor, naming ## the distribution fitted to it. This property is read-only. ## ## @end deftp DistributionNames = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationNaiveBayes} {property} Mu ## ## Predictor means ## ## The means used to center the predictors, when the model standardizes ## them, and empty otherwise. This property is read-only. ## ## @end deftp Mu = []; ## -*- texinfo -*- ## @deftp {CompactClassificationNaiveBayes} {property} Sigma ## ## Predictor standard deviations ## ## The standard deviations used to scale the predictors, when the model ## standardizes them, and empty otherwise. This property is read-only. ## ## @end deftp Sigma = []; ## -*- texinfo -*- ## @deftp {CompactClassificationNaiveBayes} {property} DistributionParameters ## ## Fitted distribution parameters ## ## A cell array with one row per class and one column per predictor, ## holding the parameters fitted to each. This property is read-only. ## ## @end deftp DistributionParameters = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationNaiveBayes} {property} CategoricalLevels ## ## Levels of the categorical predictors ## ## A cell array with one entry per predictor, holding the distinct levels ## of each categorical predictor and empty for every other. This property ## is read-only. ## ## @end deftp CategoricalLevels = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationNaiveBayes} {property} Kernel ## ## Kernel smoothing functions ## ## A cell array naming the smoothing kernel of each kernel predictor, and ## empty for every other. This property is read-only. ## ## @end deftp Kernel = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationNaiveBayes} {property} Support ## ## Kernel smoothing supports ## ## A cell array giving the support of each kernel predictor's density, and ## empty for every other. This property is read-only. ## ## @end deftp Support = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationNaiveBayes} {property} Width ## ## Kernel smoothing bandwidths ## ## A numeric matrix with one row per class and one column per predictor, ## and empty when no predictor uses a kernel density. This property is ## read-only. ## ## @end deftp Width = []; ## -*- texinfo -*- ## @deftp {CompactClassificationNaiveBayes} {property} ClassNames ## ## Class labels of the fitted model ## ## The distinct classes the model was fitted on, in the order the other ## per-class properties use. This property is read-only. ## ## @end deftp ClassNames = []; endproperties ## Properties a user may set after the model is fitted. They sit between ## the read-only blocks so that 'properties' reports MATLAB's own order. properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {CompactClassificationNaiveBayes} {property} Prior ## ## Class prior probabilities ## ## A numeric row vector with one entry per class, in the order of ## @qcode{ClassNames}, summing to one. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {CompactClassificationNaiveBayes} {property} Cost ## ## Misclassification cost ## ## A square numeric matrix where @code{Cost(i,j)} is the cost of ## classifying an observation of class @math{i} into class @math{j}. ## ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {CompactClassificationNaiveBayes} {property} ScoreTransform ## ## Score transformation ## ## A character vector naming the function applied to the posterior ## returned by @code{predict}, or a function handle. ## ## @end deftp ScoreTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CompactClassificationNaiveBayes} {property} PredictorNames ## ## Predictor variable names ## ## A cell array of character vectors naming the predictors. This property ## is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationNaiveBayes} {property} CategoricalPredictors ## ## Categorical predictor indices ## ## The column indices treated as categorical, or empty when none is. This ## property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {CompactClassificationNaiveBayes} {property} ResponseName ## ## Response variable name ## ## A character vector naming the response variable. This property is ## read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {CompactClassificationNaiveBayes} {property} ExpandedPredictorNames ## ## Expanded predictor variable names ## ## A cell array of character vectors naming the predictors as the model ## sees them. This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ## The parsed ScoreTransform, applied to the posterior by predict. STfun = []; endproperties methods (Hidden) function this = set.Cost (this, Cost) [C, errmsg] = costMatrix (Cost, this.ClassNames); if (! isempty (errmsg)) error ('CompactClassificationNaiveBayes.Cost: %s', errmsg); endif this.Cost = C; endfunction function this = set.Prior (this, Prior) P = Prior; if (isstruct (P)) P = priorFromStruct (P, this.ClassNames, ... 'CompactClassificationNaiveBayes.Prior'); elseif (ischar (P)) if (strcmpi (P, 'uniform')) P = ones (1, rows (this.Cost)) / rows (this.Cost); elseif (strcmpi (P, 'empirical')) return; else error (strcat ("CompactClassificationNaiveBayes.Prior: a", ... " character vector must be 'empirical' or", ... " 'uniform'.")); endif endif if (! (isnumeric (P) && isvector (P) && isreal (P))) error (strcat ("CompactClassificationNaiveBayes.Prior: must be a", ... " real numeric vector, a structure, 'empirical', or", ... " 'uniform'.")); endif if (any (P < 0) || ! (sum (P) > 0)) error (strcat ("CompactClassificationNaiveBayes.Prior: must be", ... " nonnegative and must not be all zero.")); endif this.Prior = P(:)' / sum (P); endfunction function this = set.ScoreTransform (this, val) [f, st] = parseScoreTransform (val, 'CompactClassificationNaiveBayes'); this.ScoreTransform = st; this.STfun = f; endfunction function disp (this) fprintf ('\n CompactClassificationNaiveBayes\n\n'); fprintf ('%22s: %s\n', 'ResponseName', this.ResponseName); fprintf ('%22s: %s\n', 'CategoricalPredictors', ... mat2str (this.CategoricalPredictors)); fprintf ('%22s: %s\n', 'ClassNames', classNameListing (this.ClassNames)); fprintf ('%22s: %s\n', 'ScoreTransform', this.ScoreTransform); fprintf ('%22s: %s\n', 'DistributionNames', ... classNameListing (this.DistributionNames)); fprintf ('%22s: {%dx%d cell}\n', 'DistributionParameters', ... size (this.DistributionParameters)); fprintf ('\n'); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationNaiveBayes} {@var{obj} =} CompactClassificationNaiveBayes (@var{Mdl}) ## ## Create a @code{CompactClassificationNaiveBayes} object. ## ## @var{Mdl} is the @code{ClassificationNaiveBayes} object to compact. The ## documented way to reach this constructor is the @code{compact} method. ## ## @seealso{ClassificationNaiveBayes} ## @end deftypefn function this = CompactClassificationNaiveBayes (Mdl) if (nargin < 1) error (strcat ("CompactClassificationNaiveBayes: too few", ... " input arguments.")); endif if (! isa (Mdl, 'ClassificationNaiveBayes')) error (strcat ("CompactClassificationNaiveBayes: MDL must be a", ... " ClassificationNaiveBayes object.")); endif ## The levels a predictor read from a table was coded through ## travel with the model, so a compact one still reads a table this.PredictorLevels = Mdl.PredictorLevels; this.DistributionNames = Mdl.DistributionNames; this.Mu = Mdl.Mu; this.Sigma = Mdl.Sigma; this.DistributionParameters = Mdl.DistributionParameters; this.CategoricalLevels = Mdl.CategoricalLevels; this.Kernel = Mdl.Kernel; this.Support = Mdl.Support; this.Width = Mdl.Width; this.ClassNames = Mdl.ClassNames; this.Cost = Mdl.Cost; this.Prior = Mdl.Prior; this.ScoreTransform = Mdl.ScoreTransform; this.PredictorNames = Mdl.PredictorNames; this.CategoricalPredictors = Mdl.CategoricalPredictors; this.ResponseName = Mdl.ResponseName; this.ExpandedPredictorNames = Mdl.ExpandedPredictorNames; endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {CompactClassificationNaiveBayes} {@var{label} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {CompactClassificationNaiveBayes} {[@var{label}, @var{score}, @var{cost}] =} predict (@var{obj}, @var{XC}) ## ## Classify new data with a compact naive Bayes model. ## ## The same classification the model it came from would give: the label of ## least expected cost, the posterior of each class, and the expected ## misclassification cost of each. ## ## A missing predictor is skipped and the observation classified on the ## rest. Under the @qcode{'mn'} distribution, which describes the whole ## row at once, an observation missing any count has posterior and cost ## @qcode{NaN} and takes the class of largest prior, whatever the cost. ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @end deftypefn function [label, score, cost] = predict (this, XC) if (nargin < 2) error (strcat ("CompactClassificationNaiveBayes.predict: too few", ... " input arguments.")); endif if (isempty (XC)) error ("CompactClassificationNaiveBayes.predict: XC is empty."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'CompactClassificationNaiveBayes.predict', XC); if (! (isnumeric (XC) && isreal (XC) && ismatrix (XC))) error (strcat ("CompactClassificationNaiveBayes.predict: XC must", ... " be a real numeric matrix.")); endif if (numel (this.PredictorNames) != columns (XC)) error (strcat ("CompactClassificationNaiveBayes.predict: XC must", ... " have the same number of predictors as the", ... " trained model.")); endif nCls = numel (this.Prior); logscore = nbLogLik (XC, this.DistributionNames, ... this.DistributionParameters, nCls, ... this.CategoricalLevels); logscore = logscore + log (this.Prior); ## An observation no class can account for, a categorical level the ## model never saw among them, leaves the likelihood at -Inf for every ## class alike. Its posterior is the prior: what remains when the data ## says nothing. Measured against R2024a, which returns the prior and ## not a uniform distribution, and does so for the whole observation ## even when its other predictors are perfectly informative. noinfo = all (logscore == -Inf, 2); logscore = logscore - max (logscore, [], 2); score = exp (logscore); score = score ./ sum (score, 2); score(isnan (score)) = 0; if (any (noinfo)) score(noinfo,:) = repmat (this.Prior, sum (noinfo), 1); endif cost = score * this.Cost; [~, minIdx] = min (cost, [], 2); ## The multinomial describes the whole row at once, so a row missing a ## count has no likelihood: its posterior and cost are NaN and it takes ## the class of largest prior, the cost aside, as R2024a and R2026b do if (ischar (this.DistributionNames)) miss = any (isnan (XC), 2); if (any (miss)) score(miss,:) = NaN; cost(miss,:) = NaN; [~, minIdx(miss)] = max (this.Prior); endif endif label = labelsFromIndex (this.ClassNames, minIdx); score = this.STfun (score); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationNaiveBayes} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationNaiveBayes} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationNaiveBayes} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margin on new data. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @end deftypefn function m = margin (this, X, Y) if (nargin < 3 && ! (nargin > 1 && istable (X))) error (strcat ("CompactClassificationNaiveBayes.margin: too few", ... " input arguments.")); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y] = tableResponse (this, 'margin', X, Y, {}, nargin > 2); [gY, errmsg] = labelIndices (this.ClassNames, Y); if (! isempty (errmsg)) error ("CompactClassificationNaiveBayes.margin: %s", errmsg); endif if (rows (X) != numel (gY)) error (strcat ("CompactClassificationNaiveBayes.margin: number of", ... " rows in X and Y must be equal.")); endif [~, s] = predict (this, X); m = marginsOf (s, gY, 1); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationNaiveBayes} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationNaiveBayes} {@var{e} =} edge (@dots{}, @qcode{'Weights'}, @var{w}) ## @deftypefnx {CompactClassificationNaiveBayes} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationNaiveBayes} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Classification edge on new data. ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @end deftypefn function e = edge (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error (strcat ("CompactClassificationNaiveBayes.edge: too few", ... " input arguments.")); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'edge', X, Y, varargin, ... nargin > 2); W = edgeWeights (varargin, Y, this.ClassNames, this.Prior, ... 'CompactClassificationNaiveBayes', 'edge'); m = margin (this, X, Y); e = sum (W .* m(:)) / sum (W); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationNaiveBayes} {@var{l} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationNaiveBayes} {@var{l} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationNaiveBayes} {@var{l} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {CompactClassificationNaiveBayes} {@var{l} =} loss (@dots{}, @var{name}, @var{value}) ## ## Classification loss on new data. ## ## Takes the @qcode{'LossFun'} and @qcode{'Weights'} options that ## @code{ClassificationNaiveBayes.loss} takes. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @end deftypefn function l = loss (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error (strcat ("CompactClassificationNaiveBayes.loss: too few", ... " input arguments.")); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("CompactClassificationNaiveBayes.loss: name-value", ... " arguments must be in pairs.")); endif ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights'}; dfValues = {'mincost', []}; [LossFun, Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments lf_opt = {'binodeviance', 'classifcost', 'classiferror', ... 'exponential', 'hinge', 'logit', 'mincost', 'quadratic'}; if (! (ischar (LossFun) && any (strcmpi (LossFun, lf_opt)))) error ("CompactClassificationNaiveBayes.loss: invalid loss function."); endif LossFun = tolower (LossFun); errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("CompactClassificationNaiveBayes.loss: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isvector (Weights))) error ("CompactClassificationNaiveBayes.loss: invalid 'Weights'."); endif if (! isempty (args)) error (strcat ("CompactClassificationNaiveBayes.loss: invalid", ... " optional paired argument.")); endif [gY, errmsg] = labelIndices (this.ClassNames, Y); if (! isempty (errmsg)) error ("CompactClassificationNaiveBayes.loss: %s", errmsg); endif if (rows (X) != numel (gY)) error (strcat ("CompactClassificationNaiveBayes.loss: number of", ... " rows in X and Y must be equal.")); endif if (isempty (Weights)) w = ones (numel (gY), 1); else w = double (Weights(:)); if (numel (w) != numel (gY)) error (strcat ("CompactClassificationNaiveBayes.loss:", ... " 'Weights' must have one element per", ... " observation.")); endif endif w = priorNormalize (w, gY, this.Prior); [~, s] = predict (this, X); l = classificationLoss (LossFun, s, gY, w, this.Cost); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationNaiveBayes} {@var{lp} =} logp (@var{obj}, @var{X}) ## ## Log unconditional probability density of new data. ## ## @code{@var{lp} = logp (@var{obj}, @var{X})} returns one value per ## observation, the logarithm of its density under the fitted model taken ## over all the classes, each weighted by its prior. A markedly low value ## marks an observation the model finds unlike anything it was trained on, ## whatever class it would be assigned to. ## ## @var{X} may also be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: one ## the model was not fitted on is passed over, one it needs and cannot ## find is named, and a value holding a level is coded as that level was ## coded at fitting. ## ## @end deftypefn function lp = logp (this, X) if (nargin < 2) error (strcat ("CompactClassificationNaiveBayes.logp: too few", ... " input arguments.")); endif ## A table is read by the names the model was fitted on X = tableColumns (this, 'CompactClassificationNaiveBayes.logp', X); if (isempty (X)) error ("CompactClassificationNaiveBayes.logp: X is empty."); endif if (numel (this.PredictorNames) != columns (X)) error (strcat ("CompactClassificationNaiveBayes.logp: X must have", ... " the same number of predictors as the trained", ... " model.")); endif nCls = numel (this.Prior); L = nbLogLik (X, this.DistributionNames, ... this.DistributionParameters, nCls, ... this.CategoricalLevels); L = L + log (this.Prior); ## Factoring out the largest term keeps a density small enough to ## underflow contributing its logarithm. Where every class is ## impossible the largest term is -Inf and the factoring is 0/0, so the ## answer is written directly: the density really is zero there. Lmax = max (L, [], 2); lp = Lmax + log (sum (exp (L - Lmax), 2)); lp(Lmax == -Inf) = -Inf; ## predict skips a missing predictor and classifies on the rest, but a ## density is not defined for an observation that is not fully observed, ## so this reports NaN where predict reports a class. R2024a does the ## same. lp(any (isnan (X), 2)) = NaN; endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationNaiveBayes} {} savemodel (@var{obj}, @var{filename}) ## ## Save a CompactClassificationNaiveBayes object. ## ## @code{savemodel (@var{obj}, @var{filename})} saves each property of a ## CompactClassificationNaiveBayes object into an Octave binary file, the ## name of which is specified in @var{filename}, along with an extra ## variable, which defines the type classification object these variables ## constitute. Use @code{loadmodel} in order to load a classification ## object into Octave's workspace. ## ## @seealso{loadmodel, fitcnb, CompactClassificationNaiveBayes} ## @end deftypefn function savemodel (this, fname) if (nargin < 2) error (strcat ("CompactClassificationNaiveBayes.savemodel:", ... " too few input arguments.")); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error (strcat ("CompactClassificationNaiveBayes.savemodel:", ... " FNAME must be a character vector.")); endif ## Generate variable for class name classdef_name = 'CompactClassificationNaiveBayes'; ## Create variables from model properties DistributionNames = this.DistributionNames; Mu = this.Mu; Sigma = this.Sigma; CategoricalLevels = this.CategoricalLevels; ## A kernel predictor's density is a classdef object, which Octave's ## save cannot serialize, so it goes out as the sample it was fitted to ## and load_model rebuilds it from that and the recorded bandwidth. DistributionParameters = nbKernelPack (this.DistributionParameters, ... this.DistributionNames); Kernel = this.Kernel; Support = this.Support; Width = this.Width; ClassNames = encodeLabels (this.ClassNames); Prior = this.Prior; Cost = this.Cost; ScoreTransform = this.ScoreTransform; PredictorNames = this.PredictorNames; CategoricalPredictors = this.CategoricalPredictors; ResponseName = this.ResponseName; ExpandedPredictorNames = this.ExpandedPredictorNames; STfun = this.STfun; ## Save classdef name and all model properties as individual variables save ('-binary', fname, 'classdef_name', 'DistributionNames', 'Mu', ... 'Sigma', 'DistributionParameters', 'CategoricalLevels', ... 'Kernel', 'Support', 'Width', 'ClassNames', 'Prior', 'Cost', ... 'ScoreTransform', 'PredictorNames', 'CategoricalPredictors', ... 'ResponseName', 'ExpandedPredictorNames', 'STfun'); endfunction endmethods methods (Static, Hidden) function mdl = load_model (filename, data) data = decodeLabels (data); ## The compact model is built from a full one and has no training data ## of its own, so the smallest fit the full class accepts is compacted ## and then filled property by property. mdl = CompactClassificationNaiveBayes ( ... ClassificationNaiveBayes ([0; 1; 2; 3], [1; 1; 2; 2])); ## Copy the saved data into the object. Iterate over what was saved ## rather than over fieldnames (mdl): a private property such as STfun ## is written out by savemodel but is not reported by fieldnames, so ## comparing the two sets could never match and every load failed. ## Assignment is legal here because this is a method of the class ## itself. names = fieldnames (data); ## These three are assigned once everything else is in place, and in ## this order rather than the file's. Cost comes before Prior because ## set.Prior counts the classes by the rows of Cost and not by ## ClassNames, so a prior arriving first is measured against the stub's. order = {'Cost', 'Prior', 'ScoreTransform'}; late = ismember (names, order); tail = order(ismember (order, names)); names = [names(! late); tail(:)]; for i = 1:numel (names) try mdl.(names{i}) = data.(names{i}); catch msg = strcat ("CompactClassificationNaiveBayes.load_model:", ... " invalid model in '%s'."); error (msg, filename); end_try_catch endfor mdl.DistributionParameters = nbKernelUnpack ( ... mdl.DistributionParameters, mdl.DistributionNames, ... mdl.Kernel, mdl.Support, mdl.Width); endfunction endmethods endclassdef ## Tests %!test # MATLAB parity: the surface a compact model reports %! load fisheriris %! CMdl = compact (fitcnb (meas, species)); %! assert_equal (class (CMdl), 'CompactClassificationNaiveBayes'); %! assert_equal (CMdl.ClassNames, unique (species)); %! assert_equal (CMdl.Prior, [1/3, 1/3, 1/3], 1e-15); %! assert_equal (CMdl.Cost, [0, 1, 1; 1, 0, 1; 1, 1, 0]); %! assert_equal (CMdl.ResponseName, 'Y'); %! assert_equal (CMdl.PredictorNames, {'x1', 'x2', 'x3', 'x4'}); %! assert_equal (CMdl.DistributionNames, ... %! {'normal', 'normal', 'normal', 'normal'}); %! assert_equal (CMdl.ScoreTransform, 'none'); ## A compact model keeps no observations, so it has neither the training data ## nor anything derived from it. %!test # the properties a compact model does not carry %! load fisheriris %! CMdl = compact (fitcnb (meas, species)); %! assert_equal (isprop (CMdl, 'X'), false); %! assert_equal (isprop (CMdl, 'Y'), false); %! assert_equal (isprop (CMdl, 'W'), false); %! assert_equal (isprop (CMdl, 'NumObservations'), false); %! assert_equal (isprop (CMdl, 'RowsUsed'), false); %! assert_equal (isprop (CMdl, 'ModelParameters'), false); %!test # MATLAB parity: it classifies exactly as the model it came from %! load fisheriris %! Mdl = fitcnb (meas, species); %! CMdl = compact (Mdl); %! [ml, ms, mc] = predict (Mdl, meas); %! [cl, cs, cc] = predict (CMdl, meas); %! assert_equal (cl, ml); %! assert_equal (cs, ms); %! assert_equal (cc, mc); %!test # MATLAB parity: loss, edge, margin and logp %! load fisheriris %! CMdl = compact (fitcnb (meas, species)); %! assert_equal (loss (CMdl, meas, species), 0.04, 1e-14); %! assert_equal (loss (CMdl, meas, species, 'LossFun', 'hinge'), ... %! 0.052784701267562, 1e-12); %! assert_equal (edge (CMdl, meas, species), 0.894430597464877, 1e-12); %! assert_equal (sum (margin (CMdl, meas, species)), ... %! 134.164589619731402, 1e-10); %! assert_equal (logp (CMdl, meas)(1), 1.026591235856343, 1e-12); %!test # MATLAB parity: edge on a set missing a class %! load fisheriris %! CMdl = compact (fitcnb (meas, species)); %! r = 51:150; %! assert_equal (edge (CMdl, meas(r,:), species(r)), 0.8416458962, 1e-10); %!test # a kernel model compacts, densities and all %! load fisheriris %! Mdl = fitcnb (meas, species, 'DistributionNames', 'kernel'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.Kernel, {'normal', 'normal', 'normal', 'normal'}); %! assert_equal (CMdl.Width, Mdl.Width); %! assert_equal (class (CMdl.DistributionParameters{1,1}), ... %! 'prob.KernelDistribution'); %! assert_equal (predict (CMdl, meas), predict (Mdl, meas)); %!test # Cost and Prior may be assigned on a compact model too %! load fisheriris %! CMdl = compact (fitcnb (meas, species)); %! CMdl.Cost = [0, 2, 2; 2, 0, 2; 2, 2, 0]; %! assert_equal (CMdl.Cost, [0, 2, 2; 2, 0, 2; 2, 2, 0]); %! CMdl.Prior = [0.2, 0.3, 0.5]; %! assert_equal (CMdl.Prior, [0.2, 0.3, 0.5], 1e-15); %! CMdl.ScoreTransform = 'logit'; %! assert_equal (CMdl.ScoreTransform, 'logit'); %!test # a categorical model compacts, levels and all %! X = [1, 2; 1, 3; 2, 2; 2, 3; 1, 2; 3, 1; 3, 3; 2, 1; 1, 1; 3, 2; ... %! 2, 2; 1, 3; 3, 1; 2, 3; 1, 1; 3, 2; 2, 1; 1, 2; 3, 3; 2, 2]; %! Y = [repmat({'a'}, 10, 1); repmat({'b'}, 10, 1)]; %! Mdl = fitcnb (X, Y, 'DistributionNames', 'mvmn', ... %! 'CategoricalPredictors', [1, 2]); %! CMdl = compact (Mdl); %! assert_equal (CMdl.CategoricalLevels{1}, [1; 2; 3]); %! assert_equal (CMdl.CategoricalPredictors, [1, 2]); %! assert_equal (predict (CMdl, X), predict (Mdl, X)); %! assert_equal (loss (CMdl, X, Y), 0.45, 1e-14); %!test # a multinomial model compacts, and keeps its character DistributionNames %! C = [2, 0, 1; 1, 3, 0; 0, 1, 4; 3, 1, 0; ... %! 0, 2, 2; 1, 0, 3; 4, 1, 1; 0, 3, 2]; %! L = [repmat({'x'}, 4, 1); repmat({'y'}, 4, 1)]; %! CMdl = compact (fitcnb (C, L, 'DistributionNames', 'mn')); %! assert_equal (CMdl.DistributionNames, 'mn'); %! [label, score] = predict (CMdl, [1, 1, 1; 4, 0, 0]); %! assert_equal (label, {'x'; 'x'}); %! assert_equal (score(2,1), 0.769060987976952, 1e-12); %!test # an unseen level falls back to the prior on a compact model too %! Z = [1, 1; 1, 1; 1, 2; 1, 2; 1, 1; 1, 2; 2, 2; 2, 1]; %! G = [repmat({'p'}, 6, 1); repmat({'q'}, 2, 1)]; %! CMdl = compact (fitcnb (Z, G, 'DistributionNames', 'mvmn', ... %! 'CategoricalPredictors', [1, 2])); %! [~, score] = predict (CMdl, [3, 1]); %! assert_equal (score, [0.75, 0.25], 1e-12); %! assert_equal (logp (CMdl, [3, 1]), -Inf); ## A compact model survives savemodel and loadmodel, kernel densities and all, ## although it has no training data of its own to be rebuilt from. %!test %! load fisheriris %! CMdl = compact (fitcnb (meas, species)); %! fname = tempname (); %! savemodel (CMdl, fname); %! C2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (C2), 'CompactClassificationNaiveBayes'); %! p = properties (CMdl); %! for i = 1:numel (p) %! assert_equal (C2.(p{i}), CMdl.(p{i})); %! endfor %! assert_equal (predict (C2, meas(1:10,:)), predict (CMdl, meas(1:10,:))); %!test %! load fisheriris %! CMdl = compact (fitcnb (meas, species, 'DistributionNames', 'kernel')); %! fname = tempname (); %! savemodel (CMdl, fname); %! C2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (C2.DistributionParameters{1,1}), ... %! 'prob.KernelDistribution'); %! assert_equal (C2.Width, CMdl.Width); %! assert_equal (predict (C2, meas(1:10,:)), predict (CMdl, meas(1:10,:))); %!error ... %! savemodel (compact (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]))) %!error ... %! savemodel (compact (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2])), 1) %!error ... %! savemodel (compact (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2])), ... %! ['ab'; 'cd']) ## Test input validation %!error ... %! CompactClassificationNaiveBayes () %!error ... %! CompactClassificationNaiveBayes (5) %!error ... %! predict (compact (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2])), []) %!error ... %! predict (compact (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2])), ones (2, 3)) %!error ... %! loss (compact (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2])), [1, 2; 2, 3; 3, 4; 4, 5]) %!error ... %! loss (compact (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2])), [1, 2; 2, 3; 3, 4; 4, 5], ... %! [1; 1; 2; 2], 'LossFun', 'nope') %!error ... %! loss (compact (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2])), ... %! [1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], 'Bogus', 1) %!error ... %! margin (compact (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2])), [1, 2; 2, 3; 3, 4; 4, 5]) %!error ... %! edge (compact (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2])), [1, 2; 2, 3; 3, 4; 4, 5]) %!error ... %! logp (compact (fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2])), []) ## Every documented score transform reaches the scores that are reported, and ## none of them moves the label: a transform reshapes what is reported, not ## what is decided. %!test %! load fisheriris %! Mdl = compact (fitcnb (meas, species)); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! T = {'identity', @(x) x; 'doublelogit', @(x) 1 ./ (1 + exp (-2 * x)); ... %! 'invlogit', @(x) log (x ./ (1 - x)); ... %! 'logit', @(x) 1 ./ (1 + exp (-x)); ... %! 'sign', @(x) sign (x); 'symmetric', @(x) 2 * x - 1; ... %! 'symmetriclogit', @(x) 2 ./ (1 + exp (-x)) - 1}; %! for i = 1:rows (T) %! Mdl.ScoreTransform = T{i,1}; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, T{i,2}(raw), 1e-12); %! assert_equal (l, label); %! endfor %! ## ismax marks the largest score of each observation, ties to the first. %! [~, k] = max (raw, [], 2); %! e = zeros (size (raw)); %! e(sub2ind (size (raw), (1:rows (raw))', k)) = 1; %! Mdl.ScoreTransform = 'ismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, e); %! Mdl.ScoreTransform = 'symmetricismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, 2 * e - 1); ## A function handle is taken as given and applied to the scores. %!test %! load fisheriris %! Mdl = compact (fitcnb (meas, species)); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! Mdl.ScoreTransform = @(x) x .^ 2; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, raw .^ 2, 1e-12); %! assert_equal (l, label); ## A table at prediction %!test # the levels a predictor was coded through travel with the model %! load fisheriris %! T = table (meas(:,1), meas(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! T.Species = categorical (species); %! CMdl = compact (fitcnb (T, 'Species')); %! assert_equal (numel (CMdl.PredictorLevels), 3); %! assert_equal (CMdl.PredictorLevels{3}, {'narrow', 'wide'}); %!test # predict takes a table, read by name and not by position %! load fisheriris %! T = table (meas(:,1), meas(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = categorical (species); %! CMdl = compact (fitcnb (T, 'Species')); %! a = predict (CMdl, T); %! assert_equal (class (a), 'categorical'); %! assert_equal (predict (CMdl, T(:, [3, 2, 1])), a); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcnb (T, 'Species')); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'Species'), a); %! assert_equal (loss (Mdl, T), a); ## A table at edge %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcnb (T, 'Species')); %! a = edge (Mdl, X, y); %! assert_equal (edge (Mdl, T(:,1:2), y), a); %! assert_equal (edge (Mdl, T, 'Species'), a); %! assert_equal (edge (Mdl, T), a); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcnb (T, 'Species')); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## A table at logp %!test # a table is matched to the predictors by name %! load fisheriris %! T = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species); %! Mdl = compact (fitcnb (T, 'Species')); %! a = logp (Mdl, meas); %! assert_equal (logp (Mdl, T(:,1:4)), a); %! assert_equal (logp (Mdl, T(:,[5, 4, 2, 3, 1])), a); ## Observation weights of class single or double %!error ... %! loss (compact (fitcnb ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2])), ... %! [1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', int8 ([1; 1; 1; 1])) ## MATLAB parity: under the multinomial a query missing a count has posterior ## NaN and the class of largest prior, as measured on R2024a %!test %! L = repmat ({'p'; 'q'; 'q'; 'p'; 'p'; 'q'; 'p'; 'q'}, 3, 1); %! C = [mod((1:24)', 4), mod((1:24)', 3), 1 + mod((1:24)', 5)]; %! Mdl = compact (fitcnb (C, L, 'DistributionNames', 'mn', ... %! 'Prior', [0.7, 0.3])); %! [label, score] = predict (Mdl, [1, 2, 3; 2, NaN, 1; NaN, NaN, NaN]); %! assert_equal (label, {'p'; 'p'; 'p'}); %! assert_equal (score(:,1), [0.747490675687019; NaN; NaN], 1e-14); statistics-release-2.1.2/inst/Machine_Learning/CompactClassificationNeuralNetwork.m000066400000000000000000001507601527311104300306330ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef CompactClassificationNeuralNetwork < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} CompactClassificationNeuralNetwork ## ## Compact neural network classification ## ## The @code{CompactClassificationNeuralNetwork} class implements a compact ## version of the neural network classifier object, which can predict ## responses for new data using the @code{predict} method, but does not store ## the training data. ## ## A compact neural network classification model is a smaller version of the ## full @code{ClassificationNeuralNetwork} model that does not include the ## training data. It consumes less memory than the full model, but cannot ## perform tasks that require the training data, such as cross-validation. ## ## Create a @code{CompactClassificationNeuralNetwork} object by using the ## @code{compact} method on a @code{ClassificationNeuralNetwork} object. ## ## @seealso{ClassificationNeuralNetwork, fitcnet} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CompactClassificationNeuralNetwork} {property} NumPredictors ## ## Number of predictors ## ## A positive integer value specifying the number of predictors in the ## training dataset used for training the neural network model. ## This property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {CompactClassificationNeuralNetwork} {property} PredictorNames ## ## Names of predictor variables ## ## A cell array of character vectors specifying the names of the predictor ## variables. The names are in the order in which they appear in the ## training dataset. This property is read-only. ## ## @end deftp PredictorNames = []; ## -*- texinfo -*- ## @deftp {CompactClassificationNeuralNetwork} {property} ResponseName ## ## Response variable name ## ## A character vector specifying the name of the response variable @var{Y}. ## This property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {CompactClassificationNeuralNetwork} {property} ClassNames ## ## Names of classes in the response variable ## ## An array of unique values of the response variable @var{Y}, which has the ## same data types as the data in @var{Y}. This property is read-only. ## @qcode{ClassNames} can have any of the following datatypes: ## ## @itemize ## @item Cell array of character vectors ## @item Character array ## @item Logical vector ## @item Numeric vector ## @end itemize ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {CompactClassificationNeuralNetwork} {property} Sigma ## ## Predictor standard deviations ## ## A numeric vector containing the standard deviations of the predictors ## used for standardization. Empty when the predictor data were not ## standardized. ## This property is read-only. ## ## @end deftp Sigma = []; ## -*- texinfo -*- ## @deftp {CompactClassificationNeuralNetwork} {property} Mu ## ## Predictor means ## ## A numeric vector containing the means of the predictors used for ## standardization. Empty when the predictor data were not standardized. ## This property is read-only. ## ## @end deftp Mu = []; ## -*- texinfo -*- ## @deftp {CompactClassificationNeuralNetwork} {property} LayerSizes ## ## Sizes of fully connected layers ## ## A positive integer vector specifying the sizes of the fully connected ## layers in the neural network model. The i-th element of ## @qcode{LayerSizes} is the number of outputs in the i-th fully connected ## layer of the neural network model. @qcode{LayerSizes} does not include ## the size of the final fully connected layer. This layer always has K ## outputs, where K is the number of classes in Y. This property is ## read-only. ## ## @end deftp LayerSizes = []; ## -*- texinfo -*- ## @deftp {CompactClassificationNeuralNetwork} {property} Activations ## ## Activation functions for hidden layers ## ## A character vector or cell array of character vectors specifying the ## activation functions used in the hidden layers of the neural network. ## Supported activation functions include: @qcode{'linear'}, ## @qcode{'sigmoid'}, @qcode{'relu'}, @qcode{'tanh'}, @qcode{'softmax'}, ## @qcode{'lrelu'}, @qcode{'prelu'}, @qcode{'elu'}, and @qcode{'gelu'}. ## This property is read-only. ## ## @end deftp Activations = []; ## -*- texinfo -*- ## @deftp {CompactClassificationNeuralNetwork} {property} OutputLayerActivation ## ## Activation function for output layer ## ## A character vector specifying the activation function of the output layer ## of the neural network. Supported activation functions are the same as ## for the @qcode{Activations} property. This property is read-only. ## ## @end deftp OutputLayerActivation = []; ## -*- texinfo -*- ## @deftp {CompactClassificationNeuralNetwork} {property} LayerWeights ## ## Learned weights of each fully connected layer ## ## A cell array holding one matrix per layer, the output layer included, ## with one row per neuron of that layer and one column per input it ## takes. This property is read-only. ## ## @end deftp LayerWeights = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationNeuralNetwork} {property} LayerBiases ## ## Learned bias of each fully connected layer ## ## A cell array holding one column vector per layer, the output layer ## included, with one entry per neuron of that layer. This property is ## read-only. ## ## @end deftp LayerBiases = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationNeuralNetwork} {property} Prior ## ## Prior probability of each class ## ## A numeric vector with one entry per class, in the order of ## @code{ClassNames}, summing to one. It is taken from the model this ## object was compacted from. This property is read-only. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {CompactClassificationNeuralNetwork} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A numeric vector holding the column of each predictor treated as ## categorical, and empty when none is. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {CompactClassificationNeuralNetwork} {property} ExpandedPredictorNames ## ## Names of the expanded predictor variables ## ## A cell array of character vectors naming the predictors as the model ## sees them. It matches @code{PredictorNames} unless a categorical ## predictor was expanded into dummy variables. This property is ## read-only. ## ## @end deftp ExpandedPredictorNames = {}; endproperties ## Properties a user may set after the model is built. Each one is ## validated by its set method below. properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {CompactClassificationNeuralNetwork} {property} Cost ## ## Cost of misclassification ## ## A numeric matrix with one row and one column per class, where ## @code{Cost(i,j)} is the cost of classifying an observation of class ## @math{i} as class @math{j}. It is taken from the model this object was ## compacted from. This property is read-only. ## ## ## A cost may also be given as a struct with the fields ## @qcode{ClassNames} and @qcode{ClassificationCosts}, which names the ## order its own matrix is written in. That matrix is permuted into the ## order of @qcode{ClassNames} above, so a caller need not know which ## order the classes were sorted into. It must name every class. ## ## A cost must be floating point, not sparse, not complex, non-negative ## and zero down its diagonal, and must hold no @qcode{NaN} or ## @qcode{Inf}. A @code{single} is widened to @code{double}. ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {CompactClassificationNeuralNetwork} {property} ScoreTransform ## ## Transformation function for classification scores ## ## Specified as a function handle for transforming the classification ## scores. Add or change the @qcode{ScoreTransform} property using dot ## notation as in: ## ## @itemize ## @item @qcode{@var{obj}.ScoreTransform = 'function_name'} ## @item @qcode{@var{obj}.ScoreTransform = @@function_handle} ## @end itemize ## ## When specified as a character vector, it can be any of the following ## built-in functions. Nevertheless, the @qcode{ScoreTransform} property ## always stores their function handle equivalent. ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Value} @tab @var{Description} ## @item @qcode{'doublelogit'} @tab @math{1 ./ (1 + exp (-2 * x))} ## @item @qcode{'invlogit'} @tab @math{log (x ./ (1 - x))} ## @item @qcode{'ismax'} @tab Sets the score for the class with the ## largest score to 1, and for all other classes to 0 ## @item @qcode{'logit'} @tab @math{1 ./ (1 + exp (-x))} ## @item @qcode{'none'} @tab @math{x} (no transformation) ## @item @qcode{'identity'} @tab @math{x} (no transformation) ## @item @qcode{'sign'} @tab ## @math{-1 for x < 0, 0 for x = 0, 1 for x > ## 0} ## @item @qcode{'symmetric'} @tab @math{2 * x - 1} ## @item @qcode{'symmetricismax'} @tab Sets the score for the class ## with the largest score to 1, and for all other classes to -1 ## @item @qcode{'symmetriclogit'} @tab @math{2 ./ (1 + exp (-x)) - 1} ## @end multitable ## ## @end deftp ScoreTransform = 'none'; endproperties ## Readable by the counterpart class, which copies it, and kept out of ## the documented surface. properties (GetAccess = public, SetAccess = protected, Hidden) ## The dummy coding of the categorical predictors, empty when none. Coding_ = []; STfun = @(x) x; endproperties ## Set methods for the properties a user may assign. methods (Hidden) function this = set.ScoreTransform (this, val) name = 'CompactClassificationNeuralNetwork'; [this.STfun, this.ScoreTransform] = parseScoreTransform (val, name); endfunction function this = set.Cost (this, val) gnY = this.ClassNames; if (isempty (val)) this.Cost = cast (! eye (classCount (gnY)), 'double'); else ## Everything a cost must be, and the struct form, which ## is permuted into this model's class order. [val, errmsg] = costMatrix (val, gnY); if (! isempty (errmsg)) error ("CompactClassificationNeuralNetwork: %s", errmsg); endif this.Cost = val; endif endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationNeuralNetwork} {@var{obj} =} CompactClassificationNeuralNetwork (@var{Mdl}) ## @deftypefnx {CompactClassificationNeuralNetwork} {@var{obj} =} CompactClassificationNeuralNetwork () ## ## Create a @code{CompactClassificationNeuralNetwork} object. ## ## @var{Mdl} is the @code{ClassificationNeuralNetwork} object to ## compact. The documented way to reach this constructor is the ## @code{compact} method. ## ## Called with no arguments it returns an object with its properties ## empty, which is how a saved model is rebuilt before its values are ## filled in. ## ## @end deftypefn function this = CompactClassificationNeuralNetwork (Mdl = []) ## Check for appropriate class if (isempty (Mdl)) return; elseif (! strcmpi (class (Mdl), 'ClassificationNeuralNetwork')) error (strcat ("CompactClassificationNeuralNetwork: invalid", ... " classification object.")); endif ## The levels a predictor read from a table was coded through ## travel with the model, so a compact one still reads a table this.PredictorLevels = Mdl.PredictorLevels; ## Save properties to compact model this.NumPredictors = Mdl.NumPredictors; this.PredictorNames = Mdl.PredictorNames; this.ResponseName = Mdl.ResponseName; this.ClassNames = Mdl.ClassNames; this.ScoreTransform = Mdl.ScoreTransform; this.STfun = Mdl.STfun; this.Sigma = Mdl.Sigma; this.Mu = Mdl.Mu; this.LayerSizes = Mdl.LayerSizes; this.Activations = Mdl.Activations; this.OutputLayerActivation = Mdl.OutputLayerActivation; this.LayerWeights = Mdl.LayerWeights; this.LayerBiases = Mdl.LayerBiases; this.Cost = Mdl.Cost; this.Prior = Mdl.Prior; this.CategoricalPredictors = Mdl.CategoricalPredictors; this.Coding_ = Mdl.Coding_; this.ExpandedPredictorNames = Mdl.ExpandedPredictorNames; endfunction ## Custom display function disp (this) fprintf ("\n CompactClassificationNeuralNetwork\n\n"); ## Print selected properties fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); if (iscellstr (this.ClassNames)) str = repmat ({'''%s'''}, 1, numel (this.ClassNames)); str = strcat ('{', strjoin (str, ' '), '}'); str = sprintf (str, this.ClassNames{:}); elseif (ischar (this.ClassNames)) str = repmat ({'''%s'''}, 1, rows (this.ClassNames)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, cellstr (this.ClassNames){:}); else # single, double, logical str = repmat ({'%d'}, 1, numel (this.ClassNames)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, this.ClassNames); endif fprintf ("%+25s: %s\n", 'ClassNames', str); fprintf ("%+25s: '%s'\n", 'ScoreTransform', this.ScoreTransform); fprintf ("%+25s: %d\n", 'NumPredictors', this.NumPredictors); str = repmat ({'%d'}, 1, numel (this.LayerSizes)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, this.LayerSizes); fprintf ("%+25s: %s\n", 'LayerSizes', str); if (iscellstr (this.Activations)) str = repmat ({'''%s'''}, 1, numel (this.Activations)); str = strcat ('{', strjoin (str, ' '), '}'); str = sprintf (str, this.Activations{:}); fprintf ("%+25s: %s\n", 'Activations', str); else # character vector fprintf ("%+25s: '%s'\n", 'Activations', this.Activations); endif fprintf ("%+25s: '%s'\n", 'OutputLayerActivation', ... this.OutputLayerActivation); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {CompactClassificationNeuralNetwork} {@var{label} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {CompactClassificationNeuralNetwork} {[@var{label}, @var{score}] =} predict (@var{obj}, @var{XC}) ## ## Classify new data points into categories using the neural network ## classification model from a CompactClassificationNeuralNetwork object. ## ## @code{@var{label} = predict (@var{obj}, @var{XC})} returns the vector of ## labels predicted for the corresponding instances in @var{XC}, using the ## neural network model stored in the CompactClassificationNeuralNetwork ## model, @var{obj}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{CompactClassificationNeuralNetwork} class ## object. ## @item ## @var{XC} must be an @math{M*P} numeric matrix with the same number of ## features @math{P} as the corresponding predictors of the neural network ## model in @var{obj}. ## @end itemize ## ## @code{[@var{label}, @var{score}] = predict (@var{obj}, @var{XC})} also ## returns @var{score}, which contains the predicted class scores or ## posterior probabilities for each instance of the corresponding unique ## classes. ## ## The @var{score} matrix contains the classification scores for each class. ## For each observation in @var{XC}, the predicted class label is the one ## with the highest score among all classes. If the @qcode{ScoreTransform} ## property is set to a transformation function, the scores are transformed ## accordingly before being returned. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @seealso{CompactClassificationNeuralNetwork, ## ClassificationNeuralNetwork, fitcnet} ## @end deftypefn function [labels, scores] = predict (this, XC) ## Check for sufficient input arguments if (nargin < 2) error (strcat ("CompactClassificationNeuralNetwork.predict:", ... " too few input arguments.")); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'CompactClassificationNeuralNetwork.predict', XC); ## Check for valid XC if (isempty (XC)) error ("CompactClassificationNeuralNetwork.predict: XC is empty."); elseif (this.NumPredictors != columns (XC)) error (strcat ("CompactClassificationNeuralNetwork.predict:", ... " XC must have the same number of predictors", ... " as the trained neural network model.")); endif ## Code the categorical predictors and standardize (if necessary) if (! isempty (this.Coding_)) XC = dummyCoding (XC, this.Coding_); endif if (! isempty (this.Mu)) XC = (XC - this.Mu) ./ this.Sigma; endif ## Predict labels from new data NumThreads = nproc (); [labels, scores] = fcnnpredict (this.LayerWeights, this.LayerBiases, ... this.Activations, ... this.OutputLayerActivation, ... XC, NumThreads); ## A class removed for its prior of 0 had no output of its own: the ## scores of the classes fitted go back to their places, 0 for it fitted = find (this.Prior > 0); if (numel (fitted) < numel (this.Prior)) full = zeros (rows (scores), numel (this.Prior), class (scores)); full(:,fitted) = scores; scores = full; labels = reshape (fitted(labels), size (labels)); endif ## A row missing a predictor, or holding a level the fit did not see, ## has no score: a rectified missing value would make one up. It ## takes the class of largest prior, as MATLAB R2024a labels it. miss = any (isnan (XC), 2); scores(miss,:) = NaN; [~, top] = max (this.Prior); labels(miss) = top; # Get class labels labels = labelsFromIndex (this.ClassNames, labels); ## Apply ScoreTransform scores = this.STfun (scores); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationNeuralNetwork} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationNeuralNetwork} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationNeuralNetwork} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margin of a compact neural network classifier. ## ## @code{@var{m} = margin (@var{obj}, @var{X}, @var{Y})} returns a column ## vector holding, for each row of @var{X}, the score the model gives its ## true class in @var{Y} less the largest score it gives any other class. ## A positive margin means the observation is classified correctly, and ## the larger it is the more confidently so. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{CompactClassificationNeuralNetwork, ## ClassificationNeuralNetwork, CompactClassificationNeuralNetwork.edge, ## CompactClassificationNeuralNetwork.loss, ## CompactClassificationNeuralNetwork.predict} ## @end deftypefn function m = margin (this, X, Y) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error (strcat ("CompactClassificationNeuralNetwork.margin:", ... " too few input arguments.")); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y] = tableResponse (this, 'margin', X, Y, {}, nargin > 2); [X, Y] = checkXY_ (this, X, Y, "margin"); [~, scores] = predict (this, X); classes = this.ClassNames; m = zeros (rows (X), 1); ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:rows (X) idx = gYidx(i); if (isempty (idx)) m(i) = NaN; continue; endif true_score = scores(i, idx); scores(i, idx) = -Inf; m(i) = true_score - max (scores(i,:)); scores(i, idx) = true_score; endfor endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationNeuralNetwork} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationNeuralNetwork} {@var{e} =} edge (@dots{}, @qcode{"Weights"}, @var{w}) ## @deftypefnx {CompactClassificationNeuralNetwork} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationNeuralNetwork} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Classification edge of a compact neural network classifier. ## ## @code{@var{e} = edge (@var{obj}, @var{X}, @var{Y})} returns the mean of ## the classification margins over the rows of @var{X}. ## ## @code{@var{e} = edge (@dots{}, @qcode{"Weights"}, @var{w})} takes the ## weighted mean instead, with one weight per row of @var{X}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{CompactClassificationNeuralNetwork, ## ClassificationNeuralNetwork, CompactClassificationNeuralNetwork.margin, ## CompactClassificationNeuralNetwork.loss, ## CompactClassificationNeuralNetwork.predict} ## @end deftypefn function e = edge (this, X, Y, varargin) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error (strcat ("CompactClassificationNeuralNetwork.edge:", ... " too few input arguments.")); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'edge', X, Y, varargin, ... nargin > 2); [X, Y] = checkXY_ (this, X, Y, "edge"); ## The weights are normalized within each class to that class's prior, ## which is what the oracle does and is not the same as dividing by ## their total. This used to divide by the total. ## The weights are parsed before anything is computed, so a bad ## Name-Value pair is reported as such rather than after a margin. W = edgeWeights (varargin, Y, this.ClassNames, this.Prior, ... "CompactClassificationNeuralNetwork", "edge"); m = margin (this, X, Y); e = sum (W .* m(:)) / sum (W); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationNeuralNetwork} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationNeuralNetwork} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationNeuralNetwork} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {CompactClassificationNeuralNetwork} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Classification loss of a compact neural network classifier. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} returns the loss of ## the model on the rows of @var{X} against the true labels @var{Y}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{L} = loss (@dots{}, @var{name}, @var{value})} accepts the ## following name-value pairs: ## ## @itemize ## @item ## @qcode{"LossFun"} selects the loss. Supported values are ## @qcode{"mincost"}, the default, @qcode{"binodeviance"}, ## @qcode{"classifcost"}, @qcode{"classiferror"}, @qcode{"crossentropy"}, ## @qcode{"exponential"}, @qcode{"hinge"}, @qcode{"logit"} and ## @qcode{"quadratic"}. @qcode{"mincost"} assigns each observation to ## the class of least expected cost and charges what that assignment ## costs, so it reads the scores as a posterior; @qcode{"classifcost"} ## charges what the model's own prediction costs. @qcode{"crossentropy"} ## is defined for a network only. Note that the default differs from the ## other classifiers in this package, which default to ## @qcode{"classiferror"}, and follows MATLAB's for this class. ## ## @item ## @qcode{"Weights"} holds one weight per row of @var{X}, normalised to ## sum to one before it is applied. ## @end itemize ## ## @seealso{CompactClassificationNeuralNetwork, ## ClassificationNeuralNetwork, CompactClassificationNeuralNetwork.margin, ## CompactClassificationNeuralNetwork.edge, ## CompactClassificationNeuralNetwork.predict} ## @end deftypefn function L = loss (this, X, Y, varargin) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error (strcat ("CompactClassificationNeuralNetwork.loss:", ... " too few input arguments.")); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("CompactClassificationNeuralNetwork.loss:", ... " Name-Value arguments must be in pairs.")); endif [X, Y] = checkXY_ (this, X, Y, "loss"); ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights'}; dfValues = {'mincost', []}; [LossFun, W, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments lossnames = {'binodeviance', 'classifcost', 'classiferror', ... 'crossentropy', 'exponential', 'hinge', 'logit', ... 'mincost', 'quadratic'}; if (! (ischar (LossFun) && isrow (LossFun))) error (strcat ("CompactClassificationNeuralNetwork.loss: 'LossFun'", ... " must be a character vector.")); endif LossFun = tolower (LossFun); if (! any (strcmpi (LossFun, lossnames))) error (strcat ("CompactClassificationNeuralNetwork.loss:", ... " unsupported Loss function.")); endif errmsg = weightsClass (W); if (! isempty (errmsg)) error ("CompactClassificationNeuralNetwork.loss: %s", errmsg); endif if (! isempty (W) && ! (isnumeric (W) && isvector (W))) error (strcat ("CompactClassificationNeuralNetwork.loss: 'Weights'", ... " must be a numeric vector.")); endif if (! isempty (W) && numel (W) != rows (X)) error (strcat ("CompactClassificationNeuralNetwork.loss: size of", ... " 'Weights' must equal the number of rows in X.")); endif if (! isempty (args)) error (strcat ("CompactClassificationNeuralNetwork.loss: invalid", ... " optional paired argument.")); endif if (isempty (W)) W = ones (rows (X), 1); endif W = double (W(:)); W = W / sum (W); [label, scores] = predict (this, X); classes = this.ClassNames; K = classCount (classes); ## Membership of the true class, as a +1/-1 indicator per class Yind = zeros (rows (X), K); ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:rows (X) idx = gYidx(i); if (isempty (idx)) L = NaN; return; endif Yind(i, idx) = 1; endfor ## The scalar score of the true class of each observation mj = sum (scores .* Yind, 2); switch (LossFun) case 'classiferror' wrong = zeros (rows (X), 1); for i = 1:rows (X) wrong(i) = ! isequal (label(i), Y(i)); endfor L = sum (W .* wrong); case 'binodeviance' L = sum (W .* log (1 + exp (-2 * mj))); case 'hinge' L = sum (W .* max (0, 1 - mj)); case 'exponential' L = sum (W .* exp (-mj)); case 'logit' L = sum (W .* log (1 + exp (-mj))); case 'quadratic' L = sum (W .* (1 - mj) .^ 2); case 'mincost' ## Each observation is assigned to the class of least expected ## cost, and charged what that assignment actually costs given its ## true class. L = 0; ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:rows (X) [~, k] = min (scores(i,:) * this.Cost); true_idx = gYidx(i); L = L + W(i) * this.Cost(true_idx, k); endfor case 'classifcost' ## What the model's own prediction costs, given the true class L = 0; ## Resolve every observation's class once, rather than once per ## iteration: the lookup does not depend on i. [gYidx, ~] = labelIndices (classes, Y); for i = 1:rows (X) true_idx = gYidx(i); pred_idx = find (ismember (classes, label(i))); L = L + W(i) * this.Cost(true_idx, pred_idx); endfor case 'crossentropy' ## Defined for a network only, whose scores are a posterior. The ## weights are rescaled to sum to the number of observations, as ## MATLAB documents, and the sum is taken over classes as well. Wn = W * rows (X); L = -sum (Wn .* log (max (mj, realmin))) / (K * rows (X)); endswitch endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationNeuralNetwork} {} savemodel (@var{obj}, @var{filename}) ## ## Save a CompactClassificationNeuralNetwork object. ## ## @code{savemodel (@var{obj}, @var{filename})} saves each property of a ## CompactClassificationNeuralNetwork object into an Octave binary file, the ## name of which is specified in @var{filename}, along with an extra ## variable, which defines the type classification object these variables ## constitute. Use @code{loadmodel} in order to load a classification ## object into Octave's workspace. ## ## @seealso{loadmodel, fitcnet, ClassificationNeuralNetwork} ## @end deftypefn function savemodel (this, fname) if (nargin < 2) error ("CompactClassificationNeuralNetwork.savemodel: too few input arguments."); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error ("CompactClassificationNeuralNetwork.savemodel: FNAME must be a character vector."); endif ## Generate variable for class name classdef_name = 'CompactClassificationNeuralNetwork'; ## Create variables from model properties NumPredictors = this.NumPredictors; PredictorNames = this.PredictorNames; ResponseName = this.ResponseName; ClassNames = encodeLabels (this.ClassNames); ScoreTransform = this.ScoreTransform; Sigma = this.Sigma; Mu = this.Mu; LayerSizes = this.LayerSizes; Activations = this.Activations; OutputLayerActivation = this.OutputLayerActivation; LayerWeights = this.LayerWeights; LayerBiases = this.LayerBiases; Cost = this.Cost; Prior = this.Prior; CategoricalPredictors = this.CategoricalPredictors; Coding_ = this.Coding_; ExpandedPredictorNames = this.ExpandedPredictorNames; STfun = this.STfun; ## Save classdef name and all model properties as individual variables save ('-binary', fname, 'classdef_name', 'NumPredictors', ... 'PredictorNames', 'ResponseName', 'ClassNames', ... 'ScoreTransform', 'Sigma', 'Mu', 'LayerSizes', ... 'Activations', 'OutputLayerActivation', ... ... 'LayerWeights', 'LayerBiases', ... 'Cost', 'Prior', 'CategoricalPredictors', 'Coding_', ... 'ExpandedPredictorNames', 'STfun'); endfunction endmethods methods (Access = private) ## Shared validation for the assessment methods, so each reports under ## its own name. function [X, Y] = checkXY_ (this, X, Y, caller) if (isempty (X)) error ("CompactClassificationNeuralNetwork.%s: X is empty.", caller); elseif (this.NumPredictors != columns (X)) error (strcat ("CompactClassificationNeuralNetwork.%s: X must have", ... " the same number of predictors as the trained", ... " neural network model."), caller); endif if (isempty (Y)) error ("CompactClassificationNeuralNetwork.%s: Y is empty.", caller); elseif (rows (X) != rows (Y)) error (strcat ("CompactClassificationNeuralNetwork.%s: Y must have", ... " the same number of rows as X."), caller); endif endfunction endmethods methods(Static, Hidden) function mdl = load_model (filename, data) data = decodeLabels (data); ## Create a ClassificationNeuralNetwork object mdl = CompactClassificationNeuralNetwork (); ## Get fieldnames from DATA (including private properties) names = fieldnames (data); ## The set methods for these read other properties, and one of them ## rebuilds Coeffs, so they are assigned once everything else is in ## place rather than in the order the file happens to list them. late = ismember (names, {'Cost', 'Prior', 'ScoreTransform', ... 'ResponseTransform'}); names = [names(! late); names(late)]; ## Copy data into object for i = 1:numel (names) ## Check fieldnames in DATA match properties in ## CompactClassificationNeuralNetwork try mdl.(names{i}) = data.(names{i}); catch error (strcat ("CompactClassificationNeuralNetwork.load_model:", ... " invalid model in '%s'."), filename) end_try_catch endfor endfunction endmethods endclassdef %!demo %! ## Train a neural network classifier and take its compact version, which %! ## drops the training data but predicts identically. %! %! load fisheriris %! X = meas; %! Y = species; %! %! Mdl = fitcnet (X, Y, 'IterationLimit', 100) %! CMdl = compact (Mdl) %! %! ## The compact model keeps no training data %! isprop (Mdl, 'X') %! isprop (CMdl, 'X') %! %! ## and predicts the same labels %! isequal (predict (Mdl, X), predict (CMdl, X)) ## Test input validation for constructor ## A compact model keeps the character class names and predicts whole names. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Cc = compact (fitcnet (Xch, Ych)); %! rand ("state", 1); randn ("state", 1); Cs = compact (fitcnet (Xch, Ycell)); %! assert_equal (cellstr (Cc.ClassNames), Cs.ClassNames); %! assert_equal (cellstr (predict (Cc, Xch)), predict (Cs, Xch)); ## and reads one back in its assessment methods. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Cc = compact (fitcnet (Xch, Ych)); %! rand ("state", 1); randn ("state", 1); Cs = compact (fitcnet (Xch, Ycell)); %! assert_equal (loss (Cc, Xch, Ych), loss (Cs, Xch, Ycell), 1e-12); %!test # A row missing a predictor takes the class of largest prior %! X = [(1:10)', mod((1:10)', 3)]; %! y = [1; 1; 1; 1; 1; 1; 2; 2; 2; 2]; %! Mdl = compact (ClassificationNeuralNetwork (X, y)); %! [label, score] = predict (Mdl, [NaN, 1]); %! assert_equal (label, 1); %! assert_equal (score, [NaN, NaN]); %! Mdl = compact (ClassificationNeuralNetwork (X, y, 'Prior', [0.3, 0.7])); %! assert_equal (predict (Mdl, [NaN, 1]), 2); %!error ... %! CompactClassificationNeuralNetwork (1) ## Test output for predict method %!shared x, y, CMdl %! load fisheriris %! x = meas; %! y = grp2idx (species); %! Mdl = fitcnet (x, y, 'IterationLimit', 100); %! CMdl = compact (Mdl); ## Test input validation for predict method %!error ... %! predict (CMdl) %!error ... %! predict (CMdl, []) %!error ... %! predict (CMdl, 1) ## Test input validation for assigning a new ScoreTransform %!error ... %! CMdl.ScoreTransform = 'a'; ## The compact model carries the trained parameters and the class bookkeeping. %!test %! Mdl = fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], ... %! 'LayerSizes', [3, 2], 'IterationLimit', 20); %! CMdl = compact (Mdl); %! assert_equal (CMdl.LayerWeights, Mdl.LayerWeights); %! assert_equal (CMdl.LayerBiases, Mdl.LayerBiases); %! assert_equal (CMdl.Cost, Mdl.Cost); %! assert_equal (CMdl.Prior, Mdl.Prior); %! assert_equal (CMdl.CategoricalPredictors, Mdl.CategoricalPredictors); %! assert_equal (CMdl.ExpandedPredictorNames, Mdl.ExpandedPredictorNames); ## margin, edge and loss agree with the model the object was compacted from. %!test %! load fisheriris %! Mdl = fitcnet (meas, species, 'IterationLimit', 20); %! CMdl = compact (Mdl); %! assert_equal (margin (CMdl, meas, species), margin (Mdl, meas, species)); %! assert_equal (edge (CMdl, meas, species), edge (Mdl, meas, species)); %! assert_equal (loss (CMdl, meas, species), loss (Mdl, meas, species)); ## Every loss function agrees with the full model's. %!test %! load fisheriris %! Mdl = fitcnet (meas, species, 'IterationLimit', 20); %! CMdl = compact (Mdl); %! names = {'binodeviance', 'classifcost', 'classiferror', 'crossentropy', ... %! 'exponential', 'hinge', 'logit', 'mincost', 'quadratic'}; %! for k = 1:numel (names) %! assert_equal (loss (CMdl, meas, species, 'LossFun', names{k}), ... %! loss (Mdl, meas, species, 'LossFun', names{k})); %! endfor ## A weighted edge weights the margins it averages. %!test %! load fisheriris %! CMdl = compact (fitcnet (meas, species, 'IterationLimit', 20)); %! ## The weights are normalized within each class to that class's prior, %! ## not divided by their total. A weight constant within a class therefore %! ## leaves the edge exactly where the unweighted one is. %! w = [ones(50, 1); 2 * ones(50, 1); 3 * ones(50, 1)]; %! m = margin (CMdl, meas, species); %! assert_equal (edge (CMdl, meas, species, 'Weights', w), ... %! edge (CMdl, meas, species), 1e-12); ## Test input validation for margin method %!error ... %! margin (CMdl) %!error ... %! margin (CMdl, x) %!error ... %! margin (CMdl, [], y) %!error ... %! margin (CMdl, 1, y) %!error ... %! margin (CMdl, x, []) %!error ... %! margin (CMdl, x, y(1:10)) ## Test input validation for edge method %!error ... %! edge (CMdl, x) %!error ... %! edge (CMdl, x, y, 'Weights') %!error ... %! edge (CMdl, x, y, 'LossFun', 'hinge') %!error ... %! edge (CMdl, x, y, 'Weights', 'a') %!error ... %! edge (CMdl, x, y, 'Weights', ones (2, 2)) %!error ... %! edge (CMdl, x, y, 'Weights', [1, 2, 3]) ## Test input validation for loss method %!error ... %! loss (CMdl, x) %!error ... %! loss (CMdl, x, y, 'LossFun') %!error ... %! loss (CMdl, x, y, 'LossFun', 1) %!error ... %! loss (CMdl, x, y, 'LossFun', 'nonsense') %!error ... %! loss (CMdl, x, y, 'Bogus', 1) ## A saved and reloaded compact model carries every property it holds. %!test %! Mdl = fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], ... %! 'LayerSizes', [3, 2], 'IterationLimit', 20); %! CMdl = compact (Mdl); %! fname = tempname (); %! savemodel (CMdl, fname); %! CMdl2 = loadmodel (fname); %! delete (fname); %! assert_equal (CMdl2.LayerWeights, CMdl.LayerWeights); %! assert_equal (CMdl2.LayerBiases, CMdl.LayerBiases); %! assert_equal (CMdl2.LayerSizes, CMdl.LayerSizes); %! assert_equal (CMdl2.ClassNames, CMdl.ClassNames); ## A reloaded compact model predicts exactly what the full model did. %!test %! load fisheriris %! Mdl = fitcnet (meas, species, 'IterationLimit', 20); %! fname = tempname (); %! savemodel (compact (Mdl), fname); %! CMdl2 = loadmodel (fname); %! delete (fname); %! [label, score] = predict (Mdl, meas); %! [label2, score2] = predict (CMdl2, meas); %! assert_equal (label2, label); %! assert_equal (score2, score); ## A non-default ScoreTransform survives compacting and a save and load. %!test %! Mdl = fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2], 'IterationLimit', 20); %! Mdl.ScoreTransform = 'symmetric'; %! fname = tempname (); %! savemodel (compact (Mdl), fname); %! CMdl2 = loadmodel (fname); %! delete (fname); %! assert_equal (CMdl2.ScoreTransform, 'symmetric'); %! [~, s1] = predict (CMdl2, [1, 2; 4, 5]); %! [~, s2] = predict (compact (Mdl), [1, 2; 4, 5]); %! assert_equal (s1, s2); %!error ... %! savemodel (CompactClassificationNeuralNetwork ()) %!error ... %! savemodel (CompactClassificationNeuralNetwork (), 1) %!error ... %! savemodel (CompactClassificationNeuralNetwork (), ['ab'; 'cd']) ## A fitted model survives savemodel and loadmodel: the properties come ## back as they were and it predicts the same. %!test %! load fisheriris %! Mdl = compact (fitcnet (meas, species, 'IterationLimit', 20)); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2), 'CompactClassificationNeuralNetwork'); %! assert_equal (M2.PredictorNames, Mdl.PredictorNames); %! assert_equal (class (M2.ScoreTransform), class (Mdl.ScoreTransform)); %! assert_equal (predict (M2, meas(1:5,:)), predict (Mdl, meas(1:5,:))); %!error ... %! CMdl = compact (fitcnet ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2])); %! CMdl.Cost = 1:4; ## The shared cost guard is in force here too, and the struct form is ## permuted into this model's class order. The battery is on ## ClassificationDiscriminant. %!test %! load fisheriris %! Mdl = compact (fitcnet (meas, species)); %! S = struct ('ClassNames', {{'virginica'; 'setosa'; 'versicolor'}}, ... %! 'ClassificationCosts', [0, 1, 2; 3, 0, 4; 5, 6, 0]); %! Mdl.Cost = S; %! assert_equal (Mdl.Cost, [0, 4, 3; 6, 0, 5; 1, 2, 0]); %!error ... %! load fisheriris %! Mdl = compact (fitcnet (meas, species)); %! Mdl.Cost = ones (3); ## Every documented score transform reaches the scores that are reported, and ## none of them moves the label: a transform reshapes what is reported, not ## what is decided. %!test %! load fisheriris %! Mdl = compact (fitcnet (meas, species)); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! T = {'identity', @(x) x; 'doublelogit', @(x) 1 ./ (1 + exp (-2 * x)); ... %! 'invlogit', @(x) log (x ./ (1 - x)); ... %! 'logit', @(x) 1 ./ (1 + exp (-x)); ... %! 'sign', @(x) sign (x); 'symmetric', @(x) 2 * x - 1; ... %! 'symmetriclogit', @(x) 2 ./ (1 + exp (-x)) - 1}; %! for i = 1:rows (T) %! Mdl.ScoreTransform = T{i,1}; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, T{i,2}(raw), 1e-12); %! assert_equal (l, label); %! endfor %! ## ismax marks the largest score of each observation, ties to the first. %! [~, k] = max (raw, [], 2); %! e = zeros (size (raw)); %! e(sub2ind (size (raw), (1:rows (raw))', k)) = 1; %! Mdl.ScoreTransform = 'ismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, e); %! Mdl.ScoreTransform = 'symmetricismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, 2 * e - 1); ## A function handle is taken as given and applied to the scores. %!test %! load fisheriris %! Mdl = compact (fitcnet (meas, species)); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! Mdl.ScoreTransform = @(x) x .^ 2; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, raw .^ 2, 1e-12); %! assert_equal (l, label); %!shared Xc, Dc, yr, yc, Xq, Dq %! k = (0:119)'; %! c1 = mod (k, 3) + 1; %! x2 = sin (k); %! c3 = 10 * (mod (floor (k / 2), 2) + 1); %! Xc = [c1, x2, c3]; %! Dc = [c1 == 1, c1 == 2, c1 == 3, x2, c3 == 10, c3 == 20]; %! yr = 5 * (c1 == 2) + 0.5 * x2 - 3 * (c3 == 20) + 0.1 * cos (k); %! yc = yr > 1; %! Xq = [1, 0, 10; 2, 0.5, 20]; %! Dq = [1, 0, 0, 0, 1, 0; 0, 1, 0, 0.5, 0, 1]; %!test # a compact model keeps the coding %! Full = fitcnet (Xc, yc, 'CategoricalPredictors', [1, 3], 'LayerSizes', 4); %! Mdl = compact (Full); %! [~, s] = predict (Mdl, [Xq; 4, 0, 10]); %! [~, sf] = predict (Full, [Xq; 4, 0, 10]); %! assert_equal (s, sf); %! assert_equal (isnan (s(3,1)), true); %! fname = [tempname(), '.mdl']; %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! [~, s2] = predict (M2, Xq); %! assert_equal (s2, s(1:2,:)); ## A table at prediction %!test # the levels a predictor was coded through travel with the model %! load fisheriris %! T = table (meas(:,1), meas(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! T.Species = categorical (species); %! CMdl = compact (fitcnet (T, 'Species')); %! assert_equal (numel (CMdl.PredictorLevels), 3); %! assert_equal (CMdl.PredictorLevels{3}, {'narrow', 'wide'}); %!test # predict takes a table, read by name and not by position %! load fisheriris %! T = table (meas(:,1), meas(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = categorical (species); %! CMdl = compact (fitcnet (T, 'Species')); %! a = predict (CMdl, T); %! assert_equal (class (a), 'categorical'); %! assert_equal (predict (CMdl, T(:, [3, 2, 1])), a); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcnet (T, 'Species')); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'Species'), a); %! assert_equal (loss (Mdl, T), a); ## A table at edge %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcnet (T, 'Species')); %! a = edge (Mdl, X, y); %! assert_equal (edge (Mdl, T(:,1:2), y), a); %! assert_equal (edge (Mdl, T, 'Species'), a); %! assert_equal (edge (Mdl, T), a); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcnet (T, 'Species')); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## Observation weights of class single or double %!error ... %! loss (compact (fitcnet ([1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2])), ... %! [1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', int8 ([1; 1; 1; 1])) statistics-release-2.1.2/inst/Machine_Learning/CompactClassificationSVM.m000066400000000000000000002200501527311104300264660ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef CompactClassificationSVM < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} CompactClassificationSVM ## ## Compact Support Vector Machine classification ## ## The @code{CompactClassificationSVM} class implements a compact version of a ## Support Vector Machine classifier object for one-class or two-class ## problems, which can predict responses for new data using the @code{predict} ## method. ## ## A @code{CompactClassificationSVM} object is a compact version of a support ## vector machine model, @code{ClassificationSVM}. It does not include the ## training data resulting in a smaller classifier size, which can be used for ## making predictions from new data, but not for tasks such as cross ## validation. It can only be created from a @code{ClassificationSVM} model ## by using the @code{compact} object method. ## ## @seealso{ClassificationSVM} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CompactClassificationSVM} {property} NumPredictors ## ## Number of predictors ## ## A positive integer value specifying the number of predictors in the ## training dataset used for training the SVM model. This property is ## read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {CompactClassificationSVM} {property} PredictorNames ## ## Names of predictor variables ## ## A cell array of character vectors specifying the names of the predictor ## variables. The names are in the order in which they appear in the ## training dataset. This property is read-only. ## ## @end deftp PredictorNames = []; ## -*- texinfo -*- ## @deftp {CompactClassificationSVM} {property} KernelParameters ## ## Parameters of the kernel function ## ## A structure with fields @qcode{Function} and @qcode{Scale}, and ## @qcode{Order} for a polynomial kernel. @qcode{Function} names the ## kernel as MATLAB names it, so a radial basis kernel reports ## @qcode{'gaussian'} whichever spelling was given. This property is ## read-only. ## ## @end deftp KernelParameters = []; ## -*- texinfo -*- ## @deftp {CompactClassificationSVM} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A numeric vector of column indices into @code{X} naming the predictors ## treated as categorical, and empty when none is. This property is ## read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {CompactClassificationSVM} {property} ExpandedPredictorNames ## ## Names of the predictors as the model expanded them ## ## A cell array of character vectors. It matches @code{PredictorNames} ## unless a categorical predictor was expanded into indicator variables. ## This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationSVM} {property} ResponseName ## ## Response variable name ## ## A character vector specifying the name of the response variable @var{Y}. ## This property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {CompactClassificationSVM} {property} ClassNames ## ## Names of classes in the response variable ## ## An array of unique values of the response variable @var{Y}, which has the ## same data types as the data in @var{Y}. This property is read-only. ## @qcode{ClassNames} can have any of the following datatypes: ## ## @itemize ## @item Cell array of character vectors ## @item Character array ## @item Logical vector ## @item Numeric vector ## @end itemize ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {CompactClassificationSVM} {property} Prior ## ## Prior probabilities of the classes ## ## A numeric row vector with one entry per class, in the order of ## @code{ClassNames}, summing to one. This property is read-only. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {CompactClassificationSVM} {property} Cost ## ## Cost of misclassification ## ## A numeric square matrix, where @code{Cost(i,j)} is the cost of ## classifying an observation of class @math{i} as class @math{j}. This ## property is read-only. ## ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {CompactClassificationSVM} {property} Sigma ## ## Predictor standard deviations ## ## A numeric vector of the same length as the columns in @var{X} containing ## the standard deviations of predictor variables. If the predictor ## variables have not been standardized, then @qcode{Sigma} is empty. ## This property is read-only. ## ## @end deftp Sigma = []; ## -*- texinfo -*- ## @deftp {CompactClassificationSVM} {property} Mu ## ## Predictor means ## ## A numeric vector of the same length as the columns in @var{X} containing ## the means of predictor variables. If the predictor variables have not ## been standardized, then @qcode{Mu} is empty. This property is read-only. ## ## @end deftp Mu = []; ## -*- texinfo -*- ## @deftp {CompactClassificationSVM} {property} Alpha ## ## Trained classifier coefficients ## ## The coefficients of the trained SVM classifier specified as an @math{s*1} ## numeric vector, where @math{s} is the number of support vectors, ## @qcode{rows (obj.SupportVectors)}. If the SVM classifier was trained ## with a kernel function other than @qcode{'linear'}, then @qcode{Alpha} is ## empty. This property is read-only. ## ## @end deftp Alpha = []; ## -*- texinfo -*- ## @deftp {CompactClassificationSVM} {property} Beta ## ## Linear predictor coefficients ## ## The linear predictor coefficients specified as a @math{p*1} numeric ## vector, where @math{p} is the number of predictors. @qcode{Beta} is ## the primal representation of the fitted hyperplane and exists only when ## the SVM classifier was trained with a @qcode{'linear'} kernel function; ## for any other kernel there is no such representation and @qcode{Beta} is ## empty. It equals @qcode{(obj.SupportVectors / s)' * (obj.Alpha .* ## obj.SupportVectorLabels)}, where @math{s} is the kernel scale, and a ## score is @qcode{(@var{x} / s) * Beta + Bias}, as in MATLAB. This ## property is read-only. ## ## @end deftp Beta = []; ## -*- texinfo -*- ## @deftp {CompactClassificationSVM} {property} Bias ## ## Bias term ## ## The bias term specified as a scalar. This property is read-only. ## ## @end deftp Bias = []; ## -*- texinfo -*- ## @deftp {CompactClassificationSVM} {property} SupportVectorLabels ## ## Support vector class labels ## ## The support vector class labels specified as an @math{s*1} numeric ## vector, where @math{s} is the number of support vectors, ## @qcode{rows (obj.SupportVectors)}. A value of +1 in ## @code{SupportVectorLabels} indicates that the corresponding support ## vector belongs to the positive class @qcode{(ClassNames@{2@})}. A value ## of -1 indicates that the corresponding support vector belongs to the ## negative class @qcode{(ClassNames@{1@})}. A one-class model labels ## every support vector +1. This property is read-only. ## ## @end deftp SupportVectorLabels = []; ## -*- texinfo -*- ## @deftp {CompactClassificationSVM} {property} SupportVectors ## ## Support vectors ## ## The support vectors of the trained SVM classifier specified an @math{s*p} ## numeric matrix, where @math{s} is the number of support vectors, ## @qcode{rows (obj.SupportVectors)}, and @math{p} is the number of ## predictor variables in the predictor data. This property is read-only. ## ## @end deftp SupportVectors = []; endproperties ## The LIBSVM structure the engine works in. It is ours alone, with no ## MATLAB counterpart, so it is kept out of the property listing while ## staying readable for anyone who needs the raw model. properties (Hidden, GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CompactClassificationSVM} {property} Model ## ## Trained SVM model ## ## A structure containing the trained model in @qcode{'libsvm'} format. ## This property is read-only. ## ## It is the engine's own structure and has no MATLAB counterpart, ## so it is kept out of @code{properties} and out of the online ## documentation. Reading it works exactly as it always did. ## ## @end deftp Model = []; endproperties ## Properties a user may set after the model is built. Each one is ## validated by its set method below. properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {CompactClassificationSVM} {property} ScoreTransform ## ## Transformation function for classification scores ## ## Specified as a function handle for transforming the classification ## scores. Add or change the @qcode{ScoreTransform} property using dot ## notation as in: ## ## @itemize ## @item @qcode{@var{obj}.ScoreTransform = 'function_name'} ## @item @qcode{@var{obj}.ScoreTransform = @@function_handle} ## @end itemize ## ## When specified as a character vector, it can be any of the following ## built-in functions. Nevertheless, the @qcode{ScoreTransform} property ## always stores their function handle equivalent. ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Value} @tab @var{Description} ## @item @qcode{'doublelogit'} @tab @math{1 ./ (1 + exp (-2 * x))} ## @item @qcode{'invlogit'} @tab @math{log (x ./ (1 - x))} ## @item @qcode{'ismax'} @tab Sets the score for the class with the ## largest score to 1, and for all other classes to 0 ## @item @qcode{'logit'} @tab @math{1 ./ (1 + exp (-x))} ## @item @qcode{'none'} @tab @math{x} (no transformation) ## @item @qcode{'identity'} @tab @math{x} (no transformation) ## @item @qcode{'sign'} @tab ## @math{-1 for x < 0, 0 for x = 0, 1 for x > ## 0} ## @item @qcode{'symmetric'} @tab @math{2 * x - 1} ## @item @qcode{'symmetricismax'} @tab Sets the score for the class ## with the largest score to 1, and for all other classes to -1 ## @item @qcode{'symmetriclogit'} @tab @math{2 ./ (1 + exp (-x)) - 1} ## @end multitable ## ## @code{fitPosterior} stores the transformation from scores to ## posterior probabilities it fits as @qcode{'@@(S)sigmoid(S,A,B)'}, ## @qcode{'@@(S)step(S,LowerBound,UpperBound,PositiveClassProbability)'} ## or @qcode{'@@(S)constant(S,K)'}. Such a text may also be assigned ## directly, which MATLAB refuses. ## ## @end deftp ScoreTransform = 'none'; endproperties ## Readable by the counterpart class, which copies it, and kept out of ## the documented surface. properties (GetAccess = public, SetAccess = protected, Hidden) ## The dummy coding of the categorical predictors, empty when none. Coding_ = []; STfun = @(x) x; endproperties ## Set methods for the properties a user may assign. methods (Hidden) function this = set.ScoreTransform (this, val) name = 'CompactClassificationSVM'; try [this.STfun, this.ScoreTransform] = parseScoreTransform (val, name); catch error (strcat ("CompactClassificationSVM.subsasgn:", ... " 'ScoreTransform' must be a", ... " 'function_handle' object.")); end_try_catch endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationSVM} {@var{obj} =} CompactClassificationSVM (@var{Mdl}) ## @deftypefnx {CompactClassificationSVM} {@var{obj} =} CompactClassificationSVM () ## ## Create a @code{CompactClassificationSVM} object. ## ## @var{Mdl} is the @code{ClassificationSVM} object to ## compact. The documented way to reach this constructor is the ## @code{compact} method. ## ## Called with no arguments it returns an object with its properties ## empty, which is how a saved model is rebuilt before its values are ## filled in. ## ## @end deftypefn function this = CompactClassificationSVM (Mdl = []) ## Check for appropriate class if (isempty (Mdl)) return; elseif (! strcmpi (class (Mdl), 'ClassificationSVM')) error (strcat ("CompactClassificationSVM: invalid", ... " classification object.")); endif ## The levels a predictor read from a table was coded through ## travel with the model, so a compact one still reads a table this.PredictorLevels = Mdl.PredictorLevels; ## Save properties to compact model this.NumPredictors = Mdl.NumPredictors; this.PredictorNames = Mdl.PredictorNames; this.CategoricalPredictors = Mdl.CategoricalPredictors; this.Coding_ = Mdl.Coding_; this.ExpandedPredictorNames = Mdl.ExpandedPredictorNames; this.ResponseName = Mdl.ResponseName; this.ClassNames = Mdl.ClassNames; this.Prior = Mdl.Prior; this.Cost = Mdl.Cost; this.ScoreTransform = Mdl.ScoreTransform; this.STfun = Mdl.STfun; this.Sigma = Mdl.Sigma; this.Mu = Mdl.Mu; this.KernelParameters = Mdl.KernelParameters; this.Model = Mdl.Model; this.Alpha = Mdl.Alpha; this.Beta = Mdl.Beta; this.Bias = Mdl.Bias; this.SupportVectorLabels = Mdl.SupportVectorLabels; this.SupportVectors = Mdl.SupportVectors; endfunction ## Custom display function disp (this) fprintf ("\n CompactClassificationSVM\n\n"); ## Print selected properties fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); if (iscellstr (this.ClassNames)) str = repmat ({'''%s'''}, 1, numel (this.ClassNames)); str = strcat ('{', strjoin (str, ' '), '}'); str = sprintf (str, this.ClassNames{:}); elseif (ischar (this.ClassNames)) str = repmat ({'''%s'''}, 1, rows (this.ClassNames)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, cellstr (this.ClassNames){:}); else # single, double, logical str = repmat ({'%d'}, 1, numel (this.ClassNames)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, this.ClassNames); endif fprintf ("%+25s: %s\n", 'ClassNames', str); fprintf ("%+25s: '%s'\n", 'ScoreTransform', this.ScoreTransform); fprintf ("%+25s: %d\n", 'NumPredictors', this.NumPredictors); fprintf ("%+25s: [%dx1 double]\n", 'Alpha', numel (this.Alpha)); if (! isempty (this.Beta)) fprintf ("%+25s: [%dx1 double]\n", 'Beta', numel (this.Beta)); endif fprintf ("%+25s: %f\n", 'Bias', this.Bias); fprintf ("%+25s: '%s'\n", 'KernelFunction', ... this.KernelParameters.Function); fprintf ("%+25s: [%dx%d double]\n", 'SupportVectors', ... rows (this.SupportVectors), columns (this.SupportVectors)); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {CompactClassificationSVM} {@var{obj} =} discardSupportVectors (@var{obj}) ## ## Discard the support vectors of a linear SVM model. ## ## @code{@var{obj} = discardSupportVectors (@var{obj})} empties ## @code{Alpha}, @code{SupportVectors} and ## @code{SupportVectorLabels}, leaving @code{Beta} and @code{Bias} to ## decide every prediction. A linear kernel needs nothing else, so the ## returned model predicts what it predicted before while carrying one ## vector in place of many. ## ## The kernel must be linear. Under any other the support vectors are ## part of the decision function and cannot be dropped. Discarding twice ## is not an error and changes nothing. ## ## @seealso{fitcsvm, ClassificationSVM, CompactClassificationSVM} ## @end deftypefn function this = discardSupportVectors (this) if (nargin != 1) print_usage (); endif if (! strcmpi (this.KernelParameters.Function, 'linear')) error (strcat ("CompactClassificationSVM.discardSupportVectors:", ... " you cannot discard support vectors for a", ... " non-linear kernel.")); endif ## The engine keeps its own copy of the support vectors, so emptying ## the properties alone would free nothing. Collapsing the model onto ## the single vector that decides it leaves every scoring path as it ## was, svmpredict going on being the engine over one vector. this.Model = discardSVs (this.Model); this.Alpha = []; this.SupportVectors = []; this.SupportVectorLabels = []; endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationSVM} {@var{label} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {CompactClassificationSVM} {[@var{label}, @var{score}] =} predict (@var{obj}, @var{XC}) ## @deftypefnx {CompactClassificationSVM} {[@var{label}, @var{score}, @var{cost}] =} predict (@var{obj}, @var{XC}) ## ## Classify new data points into categories using the Support Vector Machine ## classification model from a CompactClassificationSVM object. ## ## @code{@var{label} = predict (@var{obj}, @var{XC})} returns the vector of ## labels predicted for the corresponding instances in @var{XC}, using the ## predictor data in the CompactClassificationSVM model, @var{obj}. For ## one-class SVM model, +1 or -1 is returned. ## ## @itemize ## @item ## @var{obj} must be a @qcode{CompactClassificationSVM} class object. ## @item ## @var{XC} must be an @math{M*P} numeric matrix with the same number of ## features @math{P} as the corresponding predictors of the SVM model in ## @var{obj}. ## @end itemize ## ## @code{[@var{label}, @var{score}] = predict (@var{obj}, @var{XC})} also ## returns @var{score}, which contains the decision values for each ## prediction. A @qcode{ScoreTransform} assigned to @var{obj} is applied ## to them, so @var{score} holds whatever that transform returns. Posterior ## probabilities need a transform fitted to the model, which this package ## does not compute yet. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @seealso{CompactClassificationSVM, ClassificationSVM} ## ## @strong{Deviation from MATLAB.} @var{cost} is the expected cost of ## each assignment, @math{sum_j P(j) Cost(j,k)}. An SVM score is a ## signed distance to the boundary and not a posterior, so the only ## distribution available is the one concentrated on the predicted class ## and @var{cost} is the row of @qcode{Cost} belonging to it. MATLAB ## returns the @strong{column} instead, which is the same matrix read the ## wrong way and contradicts its own @code{ClassificationKNN}, ## @code{ClassificationDiscriminant} and @code{ClassificationNaiveBayes} ## on any asymmetric cost matrix; the two agree wherever @qcode{Cost} is ## symmetric, the default included. Measured on R2024a. ## ## @end deftypefn function [labels, scores, cost] = predict (this, XC) ## Check for sufficient input arguments if (nargin < 2) error ("CompactClassificationSVM.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'CompactClassificationSVM.predict', XC); ## Check for valid XC if (isempty (XC)) error ("CompactClassificationSVM.predict: XC is empty."); elseif (this.NumPredictors != columns (XC)) error (strcat ("CompactClassificationSVM.predict: XC must have", ... " the same number of predictors as the trained", ... " SVM model.")); endif ## Code the categorical predictors and standardize (if necessary) if (! isempty (this.Coding_)) XC = dummyCoding (XC, this.Coding_); endif if (! isempty (this.Mu)) XC = (XC - this.Mu) ./ this.Sigma; endif ## Predict labels and scores from new data [out, ~, scores] = svmpredict (ones (rows (XC), 1), XC, this.Model, '-q'); ## A model fitted on the second class alone, the first removed for its ## prior of 0, holds LIBSVM's single label -1, whose decision value of ## 1 points to that class if (isscalar (this.Model.Label) && this.Model.Label == -1) scores = -scores; endif ## Expand scores for two classes if (classCount (this.ClassNames) == 2) scores = [scores, -scores]; endif ## Translate labels to classnames. Indexing the class names by a ## per-observation class number keeps every response type on one path: ## assigning into a preallocated result instead has to know that a ## character matrix holds a name per row and not per element. ## A one-class model labels every row with its one class, outliers ## included, as R2024a does; the score tells them apart. if (classCount (this.ClassNames) == 1) idx = ones (rows (XC), 1); else idx = 2 - (out == 1); endif ## A row missing a predictor has no score, and takes the class of ## largest prior, as MATLAB R2024a labels it whatever the cost. miss = isnan (scores(:,1)); [~, top] = max (this.Prior); idx(miss) = top; labels = labelsFromIndex (this.ClassNames, idx); ## The expected cost of each assignment, sum_j P(j) * Cost(j,k). An ## SVM score is a signed distance and not a posterior, so the only ## honest distribution is the one concentrated on the predicted class, ## which leaves the row of Cost belonging to it. ## ## MATLAB returns the *column* here, Cost(:,k)', which is the same ## matrix read the wrong way and disagrees with its own KNN, ## discriminant and naive Bayes on any asymmetric cost matrix. Both ## agree when Cost is symmetric, the default included. cost = this.Cost(idx, :); cost(miss,:) = NaN; if (nargout > 1) ## Apply ScoreTransform scores = this.STfun (scores); endif endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationSVM} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationSVM} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationSVM} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margins for Support Vector Machine classifier. ## ## @code{@var{m} = margin (@var{obj}, @var{X}, @var{Y})} returns ## the classification margins for @var{obj} with data @var{X} and ## classification @var{Y}. @var{m} is a numeric vector of length size (X,1). ## ## @itemize ## @item ## @var{obj} is a @var{CompactClassificationSVM} object. ## @item ## @var{X} must be a @math{N*P} numeric matrix of input data where rows ## correspond to observations and columns correspond to features or ## variables. ## @item ## @var{Y} is @math{N*1} matrix or cell matrix containing the class labels ## of corresponding predictor data in @var{X}. @var{Y} must have same ## numbers of Rows as @var{X}. ## @end itemize ## ## The classification margin for each observation is the difference between ## the classification score for the true class and the maximal ## classification score for the false classes. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{CompactClassificationSVM} ## @end deftypefn function m = margin (this, X, Y) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactClassificationSVM.margin: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y] = tableResponse (this, 'margin', X, Y, {}, nargin > 2); ## Check for valid X if (isempty (X)) error ("CompactClassificationSVM.margin: X is empty."); elseif (this.NumPredictors != columns (X)) error (strcat ("CompactClassificationSVM.margin: X must", ... " have the same number of predictors as", ... " the trained SVM model.")); endif ## Check for valid Y if (isempty (Y)) error ("CompactClassificationSVM.margin: Y is empty."); elseif (rows (X) != rows (Y)) error (strcat ("CompactClassificationSVM.margin: Y must have", ... " the same number of rows as X.")); endif ## Y may be the class labels, which is what this method documents and ## what MATLAB accepts, or already the +1/-1 coding the solver works in. ## It used to be the latter only, so passing the labels the docstring ## promises reached LIBSVM as a cell array and raised its own message. Ypm = svmPlusMinus (Y, this.ClassNames); ## The model scores the coded, standardized predictors, which is the ## scale predict gives it; X taken as it stands was scored unscaled. if (! isempty (this.Coding_)) X = dummyCoding (X, this.Coding_); endif if (! isempty (this.Mu)) X = (X - this.Mu) ./ this.Sigma; endif [~, ~, dec_values_L] = svmpredict (Ypm, X, this.Model, '-q'); ## A model fitted on the second class alone, the first removed for its ## prior of 0, holds LIBSVM's single label -1, whose decision value of ## 1 points to that class if (isscalar (this.Model.Label) && this.Model.Label == -1) dec_values_L = -dec_values_L; endif m = 2 * Ypm .* dec_values_L; ## A one-class model has no second class to measure a margin against, ## and R2024a returns NaN for every row, so edge is NaN too. if (classCount (this.ClassNames) == 1) m = NaN (rows (X), 1); endif endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationSVM} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationSVM} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationSVM} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {CompactClassificationSVM} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Compute loss for a trained CompactClassificationSVM object. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} computes the loss, ## @var{L}, using the default loss function @qcode{'classiferror'}. ## ## @itemize ## @item ## @code{obj} is a @var{CompactClassificationSVM} object. ## @item ## @code{X} must be a @math{N*P} numeric matrix of input data where rows ## correspond to observations and columns correspond to features or ## variables. ## @item ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels ## of corresponding predictor data in @var{X}. @var{Y} must have same ## numbers of Rows as @var{X}. ## @end itemize ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{L} = loss (@dots{}, @var{name}, @var{value})} allows ## additional options specified by @var{name}-@var{value} pairs: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'LossFun'} @tab Specifies the loss function to use. ## Can be a function handle with four input arguments (C, S, W, Cost) ## which returns a scalar value or one of: ## 'binodeviance', 'classifcost', 'classiferror', 'exponential', ## 'hinge', 'logit','mincost', 'quadratic'. ## @itemize ## @item ## @code{C} is a logical matrix of size @math{N*K}, where @math{N} is the ## number of observations and @math{K} is the number of classes. ## The element @code{C(i,j)} is true if the class label of the i-th ## observation is equal to the j-th class. ## @item ## @code{S} is a numeric matrix of size @math{N*K}, where each element ## represents the classification score for the corresponding class. ## @item ## @code{W} is a numeric vector of length @math{N}, representing ## the observation weights. ## @item ## @code{Cost} is a @math{K*K} matrix representing the misclassification ## costs. ## @end itemize ## ## @item @qcode{'Weights'} @tab Specifies observation weights, must be ## a numeric vector of length equal to the number of rows in X. ## Default is @code{ones (size (X, 1))}. loss normalizes the weights so that ## observation weights in each class sum to the prior probability of that ## class. When you supply Weights, loss computes the weighted ## classification loss. ## ## @end multitable ## ## @seealso{CompactClassificationSVM} ## @end deftypefn function L = loss (this, X, Y, varargin) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactClassificationSVM.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("CompactClassificationSVM.loss: Name-Value", ... " arguments must be in pairs.")); endif ## Check for valid X if (isempty (X)) error ("CompactClassificationSVM.loss: X is empty."); elseif (this.NumPredictors != columns (X)) error (strcat ("CompactClassificationSVM.loss: X must", ... " have the same number of predictors as", ... " the trained SVM model.")); endif ## Check for valid Y if (isempty (Y)) error ("CompactClassificationSVM.loss: Y is empty."); elseif (rows (X)!= rows (Y)) error (strcat ("CompactClassificationSVM.loss: Y must have", ... " the same number of rows as X.")); endif ## Parse optional paired arguments; 'Weights' are uniform unless given W0 = ones (rows (X), 1); optNames = {'LossFun', 'Weights'}; dfValues = {'classiferror', W0}; [LossFun, Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (ischar (LossFun))) error (strcat ("CompactClassificationSVM.loss: 'LossFun' must be a", ... " character vector.")); endif LossFun = tolower (LossFun); if (! any (strcmpi (LossFun, {'binodeviance', 'classiferror', ... 'classifcost', 'exponential', ... 'hinge', 'logit', 'mincost', ... 'quadratic'}))) error ("CompactClassificationSVM.loss: unsupported Loss function."); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("CompactClassificationSVM.loss: %s", errmsg); endif if (! (isnumeric (Weights) && isvector (Weights))) error (strcat ("CompactClassificationSVM.loss: 'Weights' must be a", ... " numeric vector.")); endif if (numel (Weights) != size (X, 1)) error (strcat ("CompactClassificationSVM.loss: size of 'Weights'", ... " must be equal to the number of rows in X.")); endif if (! isempty (args)) error (strcat ("CompactClassificationSVM.loss: invalid optional", ... " paired argument.")); endif ## Compute the classification score ## Y may be the class labels, as this documents and MATLAB ## accepts, or already the solver's own +1/-1 coding. Ypm = svmPlusMinus (Y, this.ClassNames); ## The weights are scaled so that each class carries its prior and the ## loss is their weighted sum, as in MATLAB. Weights = priorNormalize (double (Weights(:)), 1 + (Ypm(:) == -1), ... this.Prior); ## The model scores the coded, standardized predictors, which is the ## scale predict gives it; X taken as it stands was scored unscaled. if (! isempty (this.Coding_)) X = dummyCoding (X, this.Coding_); endif if (! isempty (this.Mu)) X = (X - this.Mu) ./ this.Sigma; endif [~, ~, dec_values_L] = svmpredict (Ypm, X, this.Model, '-q'); ## A model fitted on the second class alone, the first removed for its ## prior of 0, holds LIBSVM's single label -1, whose decision value of ## 1 points to that class if (isscalar (this.Model.Label) && this.Model.Label == -1) dec_values_L = -dec_values_L; endif ## Compute the margin margin = Ypm .* dec_values_L; ## Compute the loss based on the specified loss function switch (LossFun) case 'classiferror' ## A one-class model labels every row with its one class, so it ## never misclassifies one; R2024a reports 0. if (classCount (this.ClassNames) == 1) L = 0; else L = sum ((margin <= 0) .* Weights); endif case 'hinge' L = sum (max (0, 1 - margin) .* Weights); case 'logit' L = sum (log (1 + exp (-margin)) .* Weights); case 'exponential' L = sum (exp (-margin) .* Weights); case 'quadratic' L = sum (((1 - margin) .^2) .* Weights); case 'binodeviance' L = sum (log (1 + exp (-2 * margin)) .* Weights); case 'mincost' ## Each observation is assigned to the class of least expected ## cost, and charged what that assignment actually costs given ## its true class. Y is the +1/-1 coding the margin above uses, ## in which +1 is the first of ClassNames and -1 the second. [~, scores] = predict (this, X); true_idx = ones (rows (X), 1); true_idx(Y == -1) = 2; L = 0; for i = 1:rows (X) [~, k] = min (scores(i,:) * this.Cost); L = L + Weights(i) * this.Cost(true_idx(i), k); endfor case 'classifcost' ## What the model's own prediction costs, given the true class. ## A one-class model predicts its one class for every row, so an ## outlier costs nothing, as R2024a reports. pred_idx = ones (rows (X), 1); if (classCount (this.ClassNames) == 2) pred_idx(dec_values_L <= 0) = 2; endif true_idx = ones (rows (X), 1); true_idx(Y == -1) = 2; L = 0; for i = 1:rows (X) L = L + Weights(i) * this.Cost(true_idx(i), pred_idx(i)); endfor otherwise error ("CompactClassificationSVM.loss: unsupported Loss function."); endswitch endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationSVM} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationSVM} {@var{e} =} edge (@dots{}, @qcode{"Weights"}, @var{w}) ## @deftypefnx {CompactClassificationSVM} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationSVM} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Classification edge, the mean of the classification margins. ## ## @code{@var{e} = edge (@var{obj}, @var{X}, @var{Y})} reduces the vector ## that @code{margin} returns to a single number, the mean margin over the ## rows of @var{X}. It says how far the model puts the true class ahead of ## its nearest rival on average, so a larger edge is a better model, and ## unlike a loss it is not bounded above and rewards confidence rather than ## bare correctness. ## ## @code{@var{e} = edge (@dots{}, @qcode{"Weights"}, @var{w})} takes the ## weighted mean instead, with one weight per row of @var{X}. ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @end deftypefn function e = edge (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactClassificationSVM.edge: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'edge', X, Y, varargin, ... nargin > 2); ## The weights are parsed before anything is computed, so a bad ## Name-Value pair is reported as such rather than after a margin. W = edgeWeights (varargin, Y, this.ClassNames, this.Prior, ... "CompactClassificationSVM", "edge"); m = margin (this, X, Y); e = sum (W .* m(:)) / sum (W); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationSVM} {@var{ScoreSVMModel} =} fitPosterior (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationSVM} {@var{ScoreSVMModel} =} fitPosterior (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationSVM} {@var{ScoreSVMModel} =} fitPosterior (@var{obj}, @var{Tbl}, @var{Y}) ## @deftypefnx {CompactClassificationSVM} {@var{ScoreSVMModel} =} fitPosterior (@var{obj}, @var{Tbl}) ## @deftypefnx {CompactClassificationSVM} {[@var{ScoreSVMModel}, @var{ScoreTransform}] =} fitPosterior (@dots{}) ## ## Fit the transformation from scores to posterior probabilities. ## ## @code{@var{ScoreSVMModel} = fitPosterior (@var{obj}, @var{X}, @var{Y})} ## returns the two-class model @var{obj} with the transformation from the ## score of the second class, @code{@var{obj}.ClassNames(2)}, to its ## posterior probability stored in its @qcode{ScoreTransform} property, so ## that @code{predict} returns posterior probabilities, the two columns ## summing to 1. A compact model holds no training data, so the ## transformation is fitted to the scores it gives the predictors @var{X}, ## whose classes are @var{Y}, without cross-validation; data the model was ## not trained on give the better estimate. Rows whose score is ## @qcode{NaN} are left out. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## The classes are then the variable @var{ResponseVarName} names, @var{Y} ## given beside the table, or the variable the model was fitted on. ## ## The transformation is a sigmoid @code{1 / (1 + exp (A * s + B))} of the ## score @math{s} when the scores of the two classes overlap, fitted by ## maximum likelihood without the prior or any weights; a step function ## when the scores separate the classes, 0 below the largest score of the ## first class, 1 above the smallest score of the second and ## @code{@var{obj}.Prior(2)} between, with a warning; and a constant, ## posterior 1 for the other class, when one class has prior probability ## 0. A @qcode{ScoreTransform} already set on @var{obj} is reset to ## @qcode{'none'} first, with a warning. The property holds the ## transformation as @code{ClassificationSVM.fitPosterior} describes. ## ## @code{[@var{ScoreSVMModel}, @var{ScoreTransform}] = fitPosterior ## (@dots{})} also returns the parameters in a structure whose field ## @qcode{Type} is @qcode{'sigmoid'}, with fields @qcode{Slope} and ## @qcode{Intercept}, @qcode{'step'}, with fields @qcode{LowerBound}, ## @qcode{UpperBound} and @qcode{PositiveClassProbability}, or ## @qcode{'constant'}, with the field @qcode{PredictedClass}. ## ## @seealso{fitSVMPosterior, ClassificationSVM.fitPosterior, ## CompactClassificationSVM.predict} ## @end deftypefn function [this, ScoreTransform] = fitPosterior (this, X, Y) if (nargin < 3 && ! (nargin > 1 && istable (X))) error (strcat ("CompactClassificationSVM.fitPosterior: too few", ... " input arguments.")); endif if (classCount (this.ClassNames) != 2) error (strcat ("CompactClassificationSVM.fitPosterior: the model", ... " must have exactly two classes.")); endif ## A table carries the classes: named in the call, given beside the ## table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y] = tableResponse (this, 'fitPosterior', X, Y, {}, nargin > 2); if (rows (X) != rows (Y)) error (strcat ("CompactClassificationSVM.fitPosterior: Y must", ... " have the same number of rows as X.")); endif if (! strcmp (this.ScoreTransform, 'none')) warning (strcat ("CompactClassificationSVM.fitPosterior: resetting", ... " 'ScoreTransform' to 'none' before fitting the", ... " transformation.")); this.ScoreTransform = 'none'; endif s = []; k = []; if (all (this.Prior > 0)) [~, scores] = predict (this, X); s = scores(:,2); [k, errmsg] = labelIndices (this.ClassNames, Y); if (! isempty (errmsg)) error ("CompactClassificationSVM.fitPosterior: %s", errmsg); endif endif [st, ScoreTransform, separated] = svmPosterior (s, k, this.Prior, ... this.ClassNames); if (separated) warning (strcat ("CompactClassificationSVM.fitPosterior: the", ... " classes are perfectly separated, so the", ... " transformation is a step function.")); endif this.ScoreTransform = st; endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationSVM} {} savemodel (@var{obj}, @var{filename}) ## ## Save a CompactClassificationSVM object. ## ## @code{savemodel (@var{obj}, @var{filename})} saves each property of a ## CompactClassificationSVM object into an Octave binary file, the name of ## which is specified in @var{filename}, along with an extra variable, ## which defines the type classification object these variables constitute. ## Use @code{loadmodel} in order to load a classification object into ## Octave's workspace. ## ## @seealso{loadmodel, ClassificationSVM, CompactClassificationSVM} ## @end deftypefn function savemodel (this, fname) if (nargin < 2) error ("CompactClassificationSVM.savemodel: too few input arguments."); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error ("CompactClassificationSVM.savemodel: FNAME must be a character vector."); endif ## Generate variable for class name classdef_name = 'CompactClassificationSVM'; ## Create variables from model properties NumPredictors = this.NumPredictors; PredictorNames = this.PredictorNames; ResponseName = this.ResponseName; ClassNames = encodeLabels (this.ClassNames); Prior = this.Prior; Cost = this.Cost; ScoreTransform = this.ScoreTransform; Sigma = this.Sigma; Mu = this.Mu; KernelParameters = this.KernelParameters; Model = this.Model; Alpha = this.Alpha; Beta = this.Beta; Bias = this.Bias; SupportVectorLabels = this.SupportVectorLabels; SupportVectors = this.SupportVectors; STfun = this.STfun; CategoricalPredictors = this.CategoricalPredictors; Coding_ = this.Coding_; ExpandedPredictorNames = this.ExpandedPredictorNames; ## Save classdef name and all model properties as individual variables save ('-binary', fname, 'classdef_name', 'NumPredictors', ... 'PredictorNames', 'ResponseName', 'ClassNames', ... 'Prior', 'Cost', ... 'ScoreTransform', 'Sigma', 'Mu', ... 'Model', 'Alpha', 'Beta', 'Bias', ... 'SupportVectorLabels', 'SupportVectors', ... 'CategoricalPredictors', 'Coding_', 'ExpandedPredictorNames', ... 'KernelParameters', 'STfun'); endfunction endmethods methods(Static, Hidden) function mdl = load_model (filename, data) data = decodeLabels (data); ## Create a ClassificationSVM object mdl = CompactClassificationSVM (); ## Copy the saved data into the object. Iterate over what was ## saved rather than over fieldnames (mdl): a private property such ## as STfun is written out by savemodel but is not reported by ## fieldnames, so comparing the two sets could never match and every ## load failed. Assignment is legal here because this is a method of ## the class itself. names = fieldnames (data); ## The set methods for these read other properties, and one of them ## rebuilds Coeffs, so they are assigned once everything else is in ## place rather than in the order the file happens to list them. late = ismember (names, {'Cost', 'Prior', 'ScoreTransform', ... 'ResponseTransform'}); names = [names(! late); names(late)]; for i = 1:numel (names) try mdl.(names{i}) = data.(names{i}); catch msg = 'CompactClassificationSVM.load_model: invalid model in ''%s''.'; error (msg, filename); end_try_catch endfor endfunction endmethods endclassdef %!demo %! ## Create a support vectors machine classifier and its compact version %! rng (42); %! # and compare their size %! %! load fisheriris %! X = meas; %! Y = species; %! %! selected_classes = unique (Y)(randperm (3, 2)); %! selected_indices = ismember (Y, selected_classes); %! X_selected = X(selected_indices, :); %! Y_selected = Y(selected_indices); %! Mdl = fitcsvm (X_selected, Y_selected, 'ClassNames', selected_classes); %! CMdl = crossval (Mdl) ## Test input validation for constructor ## discardSupportVectors empties what R2024a empties and keeps what it ## keeps: Alpha and the support vectors go, Beta, Bias and IsSupportVector ## stay, and the class is unchanged. %!test %! load fisheriris %! keep = ! strcmp (species, "setosa"); %! X = meas(keep,:); y = species(keep); %! Mdl = compact (fitcsvm (X, y, "KernelFunction", "linear")); %! D = discardSupportVectors (Mdl); %! assert_equal (class (D), "CompactClassificationSVM"); %! assert_equal (isempty (D.Alpha), true); %! assert_equal (isempty (D.SupportVectors), true); %! assert_equal (isempty (D.SupportVectorLabels), true); %! assert_equal (D.Beta, Mdl.Beta); %! assert_equal (D.Bias, Mdl.Bias); ## A linear decision needs only Beta and Bias, so the model predicts what it ## predicted before. %!test %! load fisheriris %! keep = ! strcmp (species, "setosa"); %! X = meas(keep,:); y = species(keep); %! Mdl = compact (fitcsvm (X, y, "KernelFunction", "linear")); %! D = discardSupportVectors (Mdl); %! assert_equal (predict (D, X), predict (Mdl, X), 1e-10); ## The saving is real rather than cosmetic: the engine keeps its own copy of ## the support vectors, and it collapses to the one vector that decides a ## linear model. Emptying the properties alone would free nothing. %!test %! load fisheriris %! keep = ! strcmp (species, "setosa"); %! X = meas(keep,:); y = species(keep); %! Mdl = compact (fitcsvm (X, y, "KernelFunction", "linear")); %! D = discardSupportVectors (Mdl); %! assert_equal (rows (Mdl.Model.SVs) > 1, true); %! assert_equal (rows (D.Model.SVs), 1); %! assert_equal (predict (discardSupportVectors (D), X), predict (D, X)); ## A compact model keeps the character class names and predicts whole names. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Cc = compact (fitcsvm (Xch, Ych)); %! rand ("state", 1); randn ("state", 1); Cs = compact (fitcsvm (Xch, Ycell)); %! assert_equal (cellstr (Cc.ClassNames), Cs.ClassNames); %! assert_equal (cellstr (predict (Cc, Xch)), predict (Cs, Xch)); ## and reads one back in its assessment methods. %!test %! load fisheriris %! bch = ! strcmp (species, "setosa"); %! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell); %! rand ("state", 1); randn ("state", 1); Cc = compact (fitcsvm (Xch, Ych)); %! rand ("state", 1); randn ("state", 1); Cs = compact (fitcsvm (Xch, Ycell)); %! assert_equal (loss (Cc, Xch, Ych), loss (Cs, Xch, Ycell), 1e-12); %!test # A row missing a predictor takes the class of largest prior %! X = [(1:10)', mod((1:10)', 3)]; %! y = [1; 1; 1; 1; 1; 1; 2; 2; 2; 2]; %! Mdl = compact (ClassificationSVM (X, y)); %! [label, score, cost] = predict (Mdl, [NaN, 1]); %! assert_equal (label, 1); %! assert_equal (score, [NaN, NaN]); %! assert_equal (cost, [NaN, NaN]); %! Mdl = compact (ClassificationSVM (X, y, 'Prior', [0.3, 0.7])); %! assert_equal (predict (Mdl, [NaN, 1]), 2); ## MATLAB parity: a compact one-class model labels every row with its class. %!test %! load fisheriris %! C = compact (fitcsvm (meas(51:100,[1, 3]), ones (50, 1))); %! [lab, s] = predict (C, [6, 4.3; 5, 3.5; 7, 4.7; 5.5, 5]); %! assert_equal (lab, ones (4, 1)); %! assert_equal (size (s), [4, 1]); %! assert_equal (loss (C, meas(51:100,[1, 3]), ones (50, 1)), 0); %!error ... %! load fisheriris %! keep = ! strcmp (species, "setosa"); %! X = meas(keep,:); y = species(keep); %! discardSupportVectors (compact (fitcsvm (X, y, "KernelFunction", "rbf"))) %!error ... %! CompactClassificationSVM (1) ## Test output for predict method %!shared x, y, CMdl %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! x = meas(inds, 3:4); %! y = grp2idx (species(inds)); %!test %! xc = [min(x); mean(x); max(x)]; %! Mdl = fitcsvm (x, y, 'KernelFunction', 'rbf', 'Tolerance', 1e-7); %! CMdl = compact (Mdl); %! assert_equal (isempty (CMdl.Beta), true) %! assert_equal (rows (CMdl.SupportVectors), numel (CMdl.Alpha)) %! [label, score] = predict (CMdl, xc); %! assert_equal (label, [1; 2; 2]); %! ## R2024a's scores. %! assert_equal (score(:,1), [0.9813697204; -0.1752955874; ... %! -0.9410361822], 5e-4); %! assert_equal (score(:,1), -score(:,2), eps) %!test %! Mdl = fitcsvm (x, y); %! CMdl = compact (Mdl); %! assert_equal (CMdl.Beta, [2.182926829268275; 2.253658536585344], 1e-5) %! assert_equal (rows (CMdl.SupportVectors), numel (CMdl.Alpha)) %! assert_equal (numel (CMdl.Alpha), 24) %! assert_equal (CMdl.Bias, -14.415, 1e-3) %! xc = [min(x); mean(x); max(x)]; %! label = predict (CMdl, xc); %! assert_equal (label, [1; 2; 2]); ## Test input validation for predict method %!error ... %! predict (CMdl) %!error ... %! predict (CMdl, []) %!error ... %! predict (CMdl, 1) %!error ... %! CMdl.ScoreTransform = 'a'; ## Every property the documentation calls read-only is read-only. Without a ## subsasgn of its own the class took any assignment: Bias could be replaced ## outright, and a character vector could be stored where predict expects a ## function handle. ## A ScoreTransform set by name and the same one set by handle agree, the ## name survives a save, and chained reference still reaches through. %!test %! load fisheriris %! Yb = strcmp (species, 'setosa'); %! CMdl2 = compact (fitcsvm (meas, Yb)); %! assert_equal (CMdl2.KernelParameters.Function, 'linear'); %! CMdl2.ScoreTransform = 'logit'; %! [~, s1] = predict (CMdl2, meas(1:3,:)); %! CMdl2.ScoreTransform = @(x) 1 ./ (1 + exp (-x)); %! [~, s2] = predict (CMdl2, meas(1:3,:)); %! assert_equal (s1, s2); %! CMdl2.ScoreTransform = 'logit'; %! fname = tempname (); %! savemodel (CMdl2, fname); %! M2 = loadmodel (fname); %! delete (fname); %! [~, s3] = predict (M2, meas(1:3,:)); %! assert_equal (s3, s1); ## Test output for margin method %!test %! rand ('seed', 1); %! C = cvpartition (y, 'HoldOut', 0.15); %! Mdl = fitcsvm (x(training (C),:), y(training (C)), ... %! 'KernelFunction', 'rbf', 'Tolerance', 1e-7); %! CMdl = compact (Mdl); %! testInds = test (C); %! ## R2024a's margins, fitted on the same training rows. Every one of the %! ## fifteen is classified correctly, so every margin is positive. %! expected_margin = [2.183612293; 0.998032770; 1.999658371; 3.068544065; ... %! 2.967820747; 2.189865834; 3.256968349; 2.318380120; ... %! 3.188442076; 3.154586323; 1.701435679; 3.218617427; ... %! 1.028076370; 3.029995574; 2.811141675]; %! computed_margin = margin (CMdl, x(testInds,:), y(testInds,:)); %! assert_equal (computed_margin, expected_margin, 2e-3); %! assert (all (computed_margin > 0)); ## Test input validation for margin method %!error ... %! margin (CMdl) %!error ... %! margin (CMdl, zeros (2)) %!error ... %! margin (CMdl, [], 1) %!error ... %! margin (CMdl, 1, 1) %!error ... %! margin (CMdl, [1, 2], []) %!error ... %! margin (CMdl, [1, 2], [1; 2]) ## Test output for loss method %!test %! rand ('seed', 1); %! C = cvpartition (y, 'HoldOut', 0.15); %! Mdl = fitcsvm (x(training (C),:), y(training (C)), ... %! 'KernelFunction', 'rbf', 'Tolerance', 1e-7); %! CMdl = compact (Mdl); %! testInds = test (C); %! L1 = loss (CMdl, x(testInds,:), y(testInds,:), 'LossFun', 'binodeviance'); %! L2 = loss (CMdl, x(testInds,:), y(testInds,:), 'LossFun', 'classiferror'); %! L3 = loss (CMdl, x(testInds,:), y(testInds,:), 'LossFun', 'exponential'); %! L4 = loss (CMdl, x(testInds,:), y(testInds,:), 'LossFun', 'hinge'); %! L5 = loss (CMdl, x(testInds,:), y(testInds,:), 'LossFun', 'logit'); %! L6 = loss (CMdl, x(testInds,:), y(testInds,:), 'LossFun', 'quadratic'); %! ## R2024a's losses, fitted on the same training rows. %! assert_equal (L1, 0.1059272872, 5e-4); %! assert_equal (L2, 0, 5e-4); %! assert_equal (L3, 0.3141243253, 5e-4); %! assert_equal (L4, 0.07545354045, 5e-4); %! assert_equal (L5, 0.2684014524, 5e-4); %! assert_equal (L6, 0.194889185, 5e-4); ## Test input validation for loss method %!error ... %! loss (CMdl) %!error ... %! loss (CMdl, zeros (2)) %!error ... %! loss (CMdl, [1, 2], 1, 'LossFun') %!error ... %! loss (CMdl, [], zeros (2)) %!error ... %! loss (CMdl, 1, zeros (2)) %!error ... %! loss (CMdl, [1, 2], []) %!error ... %! loss (CMdl, [1, 2], [1; 2]) %!error ... %! loss (CMdl, [1, 2], 1, 'LossFun', 1) %!error ... %! loss (CMdl, [1, 2], 1, 'LossFun', 'some') %!error ... %! loss (CMdl, [1, 2], 1, 'Weights', ['a', 'b']) %!error ... %! loss (CMdl, [1, 2], 1, 'Weights', ones (2, 2)) %!error ... %! loss (CMdl, [1, 2], 1, 'Weights', 'a') %!error ... %! loss (CMdl, [1, 2], 1, 'Weights', [1, 2]) %!error ... %! loss (CMdl, [1, 2], 1, 'some', 'some') %!error ... %! savemodel (CompactClassificationSVM ()) %!error ... %! savemodel (CompactClassificationSVM (), 1) %!error ... %! savemodel (CompactClassificationSVM (), ['ab'; 'cd']) ## A fitted model survives savemodel and loadmodel: the properties come ## back as they were and it predicts the same. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! Mdl = compact (fitcsvm (meas(inds,:), species(inds))); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2), 'CompactClassificationSVM'); %! assert_equal (M2.PredictorNames, Mdl.PredictorNames); %! assert_equal (class (M2.ScoreTransform), class (Mdl.ScoreTransform)); %! assert_equal (predict (M2, meas(1:5,:)), predict (Mdl, meas(1:5,:))); ## edge. The absolute value is not pinned to the oracle: this class fits ## through LIBSVM where MATLAB uses SMO and the two disagree on the support ## vector set, so what is pinned is that the edge is the mean of the margins ## and that the compact model answers as the full one does. %!test %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! X = meas(inds,:); %! Y = species(inds); %! Mdl = fitcsvm (X, Y); %! CMdl = compact (Mdl); %! assert_equal (edge (CMdl, X, Y), mean (margin (CMdl, X, Y)), 1e-12); %! assert_equal (edge (CMdl, X, Y), edge (Mdl, X, Y), 1e-12); %!error ... %! load fisheriris; ... %! inds = ! strcmp (species, 'virginica'); ... %! edge (compact (fitcsvm (meas(inds,:), species(inds))), meas(inds,:)) ## Both carry across to the compact form, and survive a round trip. %!test %! load fisheriris %! b = ismember (species, {'setosa', 'versicolor'}); %! CMdl = compact (fitcsvm (meas(b,:), species(b))); %! assert_equal (CMdl.CategoricalPredictors, []); %! assert_equal (CMdl.ExpandedPredictorNames, CMdl.PredictorNames); %! assert_equal (size (CMdl.ExpandedPredictorNames), [1, 4]); %!test %! load fisheriris %! b = ismember (species, {'setosa', 'versicolor'}); %! CMdl = compact (fitcsvm (meas(b,:), species(b))); %! fname = tempname (); %! savemodel (CMdl, fname); %! C2 = loadmodel (fname); %! delete (fname); %! assert_equal (C2.CategoricalPredictors, CMdl.CategoricalPredictors); %! assert_equal (C2.ExpandedPredictorNames, CMdl.ExpandedPredictorNames); ## KernelParameters comes across with the compact form. %!test %! load fisheriris %! b = ismember (species, {'setosa', 'versicolor'}); %! Mdl = fitcsvm (meas(b,:), species(b), 'KernelFunction', 'rbf'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.KernelParameters, Mdl.KernelParameters); %! assert_equal (CMdl.KernelParameters.Function, 'gaussian'); ## A compact model answers exactly as the model it was compacted from. %!test %! load fisheriris %! b = strcmp (species, 'setosa'); %! Mdl = fitcsvm (meas, b, 'Cost', [0, 2; 5, 0]); %! [~, ~, cost] = predict (compact (Mdl), meas([1, 51],:)); %! assert_equal (cost, [5, 0; 0, 2]); ## Every documented score transform reaches the scores that are reported, and ## none of them moves the label: a transform reshapes what is reported, not ## what is decided. %!test %! load fisheriris %! Mdl = compact (fitcsvm (meas, strcmp (species, 'setosa'))); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! T = {'identity', @(x) x; 'doublelogit', @(x) 1 ./ (1 + exp (-2 * x)); ... %! 'invlogit', @(x) log (x ./ (1 - x)); ... %! 'logit', @(x) 1 ./ (1 + exp (-x)); ... %! 'sign', @(x) sign (x); 'symmetric', @(x) 2 * x - 1; ... %! 'symmetriclogit', @(x) 2 ./ (1 + exp (-x)) - 1}; %! for i = 1:rows (T) %! Mdl.ScoreTransform = T{i,1}; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, T{i,2}(raw), 1e-12); %! assert_equal (l, label); %! endfor %! ## ismax marks the largest score of each observation, ties to the first. %! [~, k] = max (raw, [], 2); %! e = zeros (size (raw)); %! e(sub2ind (size (raw), (1:rows (raw))', k)) = 1; %! Mdl.ScoreTransform = 'ismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, e); %! Mdl.ScoreTransform = 'symmetricismax'; %! [~, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, 2 * e - 1); ## A function handle is taken as given and applied to the scores. %!test %! load fisheriris %! Mdl = compact (fitcsvm (meas, strcmp (species, 'setosa'))); %! Mdl.ScoreTransform = 'none'; %! [label, raw] = predict (Mdl, meas([1, 60, 120],:)); %! Mdl.ScoreTransform = @(x) x .^ 2; %! [l, s] = predict (Mdl, meas([1, 60, 120],:)); %! assert_equal (s, raw .^ 2, 1e-12); %! assert_equal (l, label); %!shared Xc, Dc, yr, yc, Xq, Dq %! k = (0:119)'; %! c1 = mod (k, 3) + 1; %! x2 = sin (k); %! c3 = 10 * (mod (floor (k / 2), 2) + 1); %! Xc = [c1, x2, c3]; %! Dc = [c1 == 1, c1 == 2, c1 == 3, x2, c3 == 10, c3 == 20]; %! yr = 5 * (c1 == 2) + 0.5 * x2 - 3 * (c3 == 20) + 0.1 * cos (k); %! yc = yr > 1; %! Xq = [1, 0, 10; 2, 0.5, 20]; %! Dq = [1, 0, 0, 0, 1, 0; 0, 1, 0, 0.5, 0, 1]; %!test # a compact model keeps the coding, margin and loss included %! Full = fitcsvm (Xc, yc, 'CategoricalPredictors', [1, 3], ... %! 'Standardize', true); %! Mdl = compact (Full); %! assert_equal (Mdl.CategoricalPredictors, [1, 3]); %! assert_equal (predict (Mdl, Xq), predict (Full, Xq)); %! assert_equal (margin (Mdl, Xc, yc), margin (Full, Xc, yc), 1e-12); %! assert_equal (loss (Mdl, Xc, yc), resubLoss (Full)); %! fname = [tempname(), '.mdl']; %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (predict (M2, Xq), predict (Mdl, Xq)); ## A table at prediction %!test # the levels a predictor was coded through travel with the model %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! T = table (meas(inds,1), meas(inds,2), 'VariableNames', {'SL', 'SW'}); %! T.Wide = categorical (meas(inds,2) > 2.9, [false true], ... %! {'narrow', 'wide'}); %! T.Species = categorical (species(inds)); %! CMdl = compact (fitcsvm (T, 'Species')); %! assert_equal (numel (CMdl.PredictorLevels), 3); %! assert_equal (CMdl.PredictorLevels{3}, {'narrow', 'wide'}); %!test # predict takes a table, read by name and not by position %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! T = table (meas(inds,1), meas(inds,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = categorical (species(inds)); %! CMdl = compact (fitcsvm (T, 'Species')); %! a = predict (CMdl, T); %! assert_equal (class (a), 'categorical'); %! assert_equal (predict (CMdl, T(:, [3, 2, 1])), a); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(51:150,1:2); %! y = species(51:150); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcsvm (T, 'Species')); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'Species'), a); %! assert_equal (loss (Mdl, T), a); ## A categorical or string response %!test # loss reads one as it reads a cellstr response %! load fisheriris %! X = meas(51:150,1:2); %! y = species(51:150); %! a = loss (compact (fitcsvm (X, y)), X, y); %! yc = categorical (y); %! assert_equal (loss (compact (fitcsvm (X, yc)), X, yc), a); %! ys = string (y); %! assert_equal (loss (compact (fitcsvm (X, ys)), X, ys), a); %!test # margin reads one as it reads a cellstr response %! load fisheriris %! X = meas(51:150,1:2); %! y = species(51:150); %! a = margin (compact (fitcsvm (X, y)), X, y); %! yc = categorical (y); %! assert_equal (margin (compact (fitcsvm (X, yc)), X, yc), a); %! ys = string (y); %! assert_equal (margin (compact (fitcsvm (X, ys)), X, ys), a); ## A table at edge %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(51:150,1:2); %! y = categorical (species(51:150)); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcsvm (T, 'Species')); %! a = edge (Mdl, X, y); %! assert_equal (edge (Mdl, T(:,1:2), y), a); %! assert_equal (edge (Mdl, T, 'Species'), a); %! assert_equal (edge (Mdl, T), a); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(51:150,1:2); %! y = categorical (species(51:150)); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitcsvm (T, 'Species')); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## Test output for fitPosterior method ## Values checked against MATLAB R2024a, whose SMO solver gives scores up to ## 1e-5 away from LIBSVM's %!test %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! C = compact (fitcsvm (X, Y, 'KernelFunction', 'linear')); %! [M, ST] = fitPosterior (C, X, Y); %! assert_equal (class (M), 'CompactClassificationSVM'); %! assert_equal (ST.Type, 'sigmoid'); %! assert_equal (ST.Slope, -3.3591642562674378, 1e-4); %! assert_equal (ST.Intercept, -0.1511873140962528, 1e-4); %! tok = regexp (M.ScoreTransform, '^@\(S\)sigmoid\(S,([^,]+),([^)]+)\)$', ... %! 'tokens', 'once'); %! assert_equal (str2double (tok)', [ST.Slope, ST.Intercept]); %!test %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! C = compact (fitcsvm (X, Y, 'KernelFunction', 'linear')); %! [~, ST] = fitPosterior (C, X(1:2:end, :), Y(1:2:end)); %! assert_equal (ST.Slope, -3.4804281119663738, 1e-4); %! assert_equal (ST.Intercept, -0.25030354136184213, 1e-4); %!test %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! C = compact (fitcsvm (X, Y, 'KernelFunction', 'linear')); %! [~, ST1] = fitPosterior (C, X(2:end, :), Y(2:end)); %! [~, ST2] = fitPosterior (C, [NaN, NaN; X(2:end, :)], Y); %! assert_equal (ST2, ST1); %!test %! load fisheriris %! T = table (meas(51:150, 3), meas(51:150, 4), species(51:150), ... %! 'VariableNames', {'PL', 'PW', 'Species'}); %! C = compact (fitcsvm (T, 'Species', 'KernelFunction', 'linear')); %! [~, ST] = fitPosterior (C, meas(51:150, 3:4), species(51:150)); %! [~, ST1] = fitPosterior (C, T, 'Species'); %! [~, ST2] = fitPosterior (C, T); %! [~, ST3] = fitPosterior (C, T(:,1:2), species(51:150)); %! assert_equal (ST1, ST); %! assert_equal (ST2, ST); %! assert_equal (ST3, ST); %!warning ... %! load fisheriris %! X = meas(1:100, 3:4); %! Y = species(1:100); %! C = compact (fitcsvm (X, Y, 'KernelFunction', 'linear')); %! [~, ST] = fitPosterior (C, X, Y); %! assert_equal (ST.Type, 'step'); %! assert_equal (ST.LowerBound, -0.909999999999998, 1e-4); %! assert_equal (ST.UpperBound, 0.790000000000004, 1e-4); %! assert_equal (ST.PositiveClassProbability, 0.5); %!warning ... %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! C = compact (fitcsvm (X, Y, 'KernelFunction', 'linear')); %! [M, ST1] = fitPosterior (C, X, Y); %! [~, ST2] = fitPosterior (M, X, Y); %! assert_equal (ST2, ST1); %!test %! load fisheriris %! X = meas(51:150, 3:4); %! C = compact (fitcsvm (X, species(51:150), 'KernelFunction', 'linear', ... %! 'Prior', [1, 0])); %! [M, ST] = fitPosterior (C, X, species(51:150)); %! assert_equal (ST.Type, 'constant'); %! assert_equal (ST.PredictedClass, {'versicolor'}); %! assert_equal (M.ScoreTransform, '@(S)constant(S,1)'); ## Test input validation for fitPosterior method %!error ... %! load fisheriris %! C = compact (fitcsvm (meas(51:150, 3:4), species(51:150))); %! fitPosterior (C) %!error ... %! load fisheriris %! C = compact (fitcsvm (meas(51:150, 3:4), species(51:150))); %! fitPosterior (C, meas(51:150, 3:4)) %!error ... %! load fisheriris %! C = compact (fitcsvm (meas(51:150, 3:4), ones (100, 1))); %! fitPosterior (C, meas(51:150, 3:4), ones (100, 1)) %!error ... %! load fisheriris %! C = compact (fitcsvm (meas(51:150, 3:4), species(51:150))); %! fitPosterior (C, meas(51:150, 3:4), species(51:100)) %!error ... %! load fisheriris %! C = compact (fitcsvm (meas(51:150, 3:4), species(51:150))); %! fitPosterior (C, meas(1:100, 3:4), species(1:100)) statistics-release-2.1.2/inst/Machine_Learning/CompactClassificationTree.m000066400000000000000000001574151527311104300267360ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef CompactClassificationTree < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} CompactClassificationTree ## ## Compact binary decision tree for classification ## ## A @code{CompactClassificationTree} object carries the tree a ## @code{ClassificationTree} model grew and everything @code{predict} needs, ## but not the observations it was fitted on. It classifies new data ## identically to the model it came from, and is far smaller to keep or to ## ship. ## ## Create one with the @code{compact} method of a @code{ClassificationTree} ## object. Because it holds no training data, it has no @code{resub} ## methods and cannot be cross-validated, and it cannot be pruned: the ## pruning sequence is reported but taking a subtree out of it rewrites the ## node table, which is work for the model that still has its data. ## ## @seealso{ClassificationTree, fitctree} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} NumNodes ## ## Number of nodes in the tree ## ## A positive integer, the number of nodes the tree holds, branch nodes ## and leaves together. Nodes are numbered as they are created, so a ## parent always carries a lower number than either of its children. ## This property is read-only. ## ## @end deftp NumNodes = []; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} Children ## ## Child nodes of each node ## ## A @math{NumNodesx2} matrix naming the left and the right child of each ## node. A leaf carries a zero in both columns. This property is ## read-only. ## ## @end deftp Children = []; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} Parent ## ## Parent of each node ## ## A column vector naming the parent of each node. The root carries a ## zero. This property is read-only. ## ## @end deftp Parent = []; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} IsBranchNode ## ## Which nodes are branch nodes ## ## A logical column vector, true for each node that carries a split and ## false for each leaf. This property is read-only. ## ## @end deftp IsBranchNode = []; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} CutPredictor ## ## Name of the predictor each node cuts on ## ## A cell array of character vectors with one entry per node, holding the ## name of the predictor the node splits on and an empty character vector ## at a leaf. This property is read-only. ## ## @end deftp CutPredictor = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} CutPredictorIndex ## ## Index of the predictor each node cuts on ## ## A column vector holding, for each node, the column of @var{X} the node ## splits on, and zero at a leaf. This property is read-only. ## ## @end deftp CutPredictorIndex = []; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} CutPoint ## ## Cut point of each node ## ## A column vector holding, for each node, the value the split compares ## the predictor against: an observation goes left when its value is less ## than the cut point and right otherwise. A leaf and a categorical cut ## carry @qcode{NaN}. ## This property is read-only. ## ## @end deftp CutPoint = []; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} CutType ## ## Type of cut at each node ## ## A cell array of character vectors holding @qcode{'continuous'} at a ## branch node that cuts a numeric predictor at a point, ## @qcode{'categorical'} at one that splits a set of levels, and an empty ## character vector at a leaf. This property is read-only. ## ## @end deftp CutType = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} CutCategories ## ## Categories used at each branch ## ## A @math{NumNodesx2} cell array holding, for a node that cuts a ## categorical predictor, the levels sent left and the levels sent right. ## Every other entry is empty. This property is read-only. ## ## @end deftp CutCategories = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} CategoricalSplit ## ## Categorical splits of the tree ## ## A @math{Nx2} cell array with one row per categorical split, in node ## order, holding the levels sent left and the levels sent right. It is ## empty when no split is categorical. This property is read-only. ## ## @end deftp CategoricalSplit = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} NodeSize ## ## Number of observations at each node ## ## A column vector holding how many training observations reached each ## node. A row missing the predictor its node cuts on is counted at that ## node and at none below it, so a parent's size is not in general the ## sum of its children's. This property is read-only. ## ## @end deftp NodeSize = []; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} NodeClass ## ## Class assigned to each node ## ## A cell array of character vectors naming, for each node, the class of ## least expected misclassification cost given the node's class ## probabilities. Under the default cost that is simply the most ## probable class, with the first of the class names kept on a tie. This ## property is read-only. ## ## @end deftp NodeClass = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} NodeError ## ## Misclassification cost of each node ## ## A column vector holding, for each node, the expected misclassification ## cost of the class the node is assigned. Under the default cost that ## is the probability that the node's class is wrong, one less the ## largest class probability. This property is read-only. ## ## @end deftp NodeError = []; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} NodeProbability ## ## Probability of reaching each node ## ## A column vector holding, for each node, the total weight of the ## observations that reached it, the weights being those the model it ## came from was fitted with. The root carries one. This property is ## read-only. ## ## @end deftp NodeProbability = []; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} NodeRisk ## ## Risk of each node ## ## A column vector holding, for each node, the impurity of the node ## weighted by the probability of reaching it, measured by whichever of ## @qcode{'gdi'} and @qcode{'deviance'} the tree was grown under. ## ## A non-default @code{Cost} enters here rather than through the class ## probabilities: the weights are scaled class by class by the total cost ## of misclassifying that class, and the impurity is measured on the ## scaled distribution. This property is read-only. ## ## @end deftp NodeRisk = []; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} ClassCount ## ## Class counts at each node ## ## A @math{NumNodesxK} matrix holding how many training observations of ## each class reached each node. These are counts and take no notice of ## the observation weights. This property is read-only. ## ## @end deftp ClassCount = []; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} ClassProbability ## ## Class probabilities at each node ## ## A @math{NumNodesxK} matrix holding, for each node, the weight of each ## class among the observations that reached it, as a proportion of the ## node's total weight. The root row is the prior. This property is ## read-only. ## ## @end deftp ClassProbability = []; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} PruneList ## ## Pruning level of each node ## ## A column vector holding, for each branch node, the level of the cost ## complexity sequence at which it stops being a branch node, and zero at ## a leaf. Pruning the tree to level @var{L} turns every node whose ## level is between one and @var{L} into a leaf. It is empty when ## neither @qcode{'Prune'} nor @qcode{'MergeLeaves'} was asked for, since ## no sequence was then estimated. This property is read-only. ## ## @end deftp PruneList = []; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} PruneAlpha ## ## Cost complexity parameter of each pruning level ## ## A column vector with one element per level of the pruning sequence, ## the first of which is zero and stands for the unpruned tree. Level ## @var{L} is the smallest subtree that is optimal for every complexity ## parameter from @code{PruneAlpha(@var{L}+1)} up to the next one. This ## property is read-only. ## ## @end deftp PruneAlpha = []; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} SurrogateCutCategories ## ## Categories of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutCategories = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} SurrogateCutFlip ## ## Cut assignments of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutFlip = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} SurrogateCutPoint ## ## Cut points of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutPoint = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} SurrogateCutType ## ## Types of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutType = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} SurrogateCutPredictor ## ## Predictors of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutPredictor = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} SurrogatePredictorAssociation ## ## Predictive measures of association of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogatePredictorAssociation = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} PredictorNames ## ## Names of the predictor variables ## ## A cell array of character vectors with one name per column of ## @var{X}. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} ResponseName ## ## Name of the response variable ## ## A character vector naming the response. This property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} ClassNames ## ## Names of the classes ## ## The distinct class labels, in the type the response was given in and ## sorted. This property is read-only. ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A row vector of column indices into @var{X}, naming the predictors ## treated as categorical, empty when none is. This property is ## read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} ExpandedPredictorNames ## ## Expanded predictor names ## ## A cell array of character vectors. It differs from ## @code{PredictorNames} only when a categorical predictor has been ## expanded into one column per level, which this class does not do, so ## the two are always equal. This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} Prior ## ## Prior probability of each class ## ## A numeric row vector with one element per class, summing to one. It ## defaults to the weight each class carries in the training data, and ## may be reassigned after fitting. ## ## Reassigning it re-derives every node statistic that depends on the ## class weights, so @code{ClassProbability}, @code{NodeProbability}, ## @code{NodeClass}, @code{NodeError} and @code{NodeRisk} all follow. ## The shape of the tree does not, having been decided by the prior in ## force when it was grown. MATLAB reports this property read-only on a ## compact tree and refuses the assignment; it is settable here, as it is ## on the package's other compact classifiers. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} Cost ## ## Misclassification cost ## ## A square numeric matrix with one row and column per class, where ## @code{Cost(i,j)} is the cost of classifying an observation of class ## @var{i} into class @var{j}. It defaults to @code{1 - eye (K)} and may ## be reassigned after fitting. ## ## Reassigning it re-derives @code{NodeClass}, @code{NodeError} and ## @code{NodeRisk}, and changes what @code{predict} answers. The shape ## of the tree does not follow, having been decided by the cost in force ## when it was grown. MATLAB reports this property read-only on a ## compact tree and refuses the assignment; it is settable here, as it is ## on the package's other compact classifiers. ## ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {CompactClassificationTree} {property} ScoreTransform ## ## Transform applied to the scores ## ## A character vector naming the function @code{predict} applies to the ## class probabilities before returning them, or a function handle. The ## default is @qcode{'none'}. ## ## @end deftp ScoreTransform = []; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ## The parsed ScoreTransform, applied to the scores by predict. STfun = []; ## The split criterion the tree was grown under, which the risk is ## measured by. The full class reads it from ModelParameters, which a ## compact model does not carry. SplitCriterion = []; ## The share of each class's total weight that reached each node, one row ## per node and one column per class. It is what the node statistics are ## re-derived from, and it is free of both the prior and the cost. ClassShare = []; endproperties ## Set methods for the properties a user may assign after compacting. methods (Hidden) function this = set.Cost (this, Cost) [C, errmsg] = costMatrix (Cost, this.ClassNames); if (! isempty (errmsg)) error ("CompactClassificationTree.Cost: %s", errmsg); endif this.Cost = C; this = deriveNodes (this); endfunction function this = set.Prior (this, Prior) P = Prior; K = classCount (this.ClassNames); if (isstruct (P)) P = priorFromStruct (P, this.ClassNames, ... 'CompactClassificationTree.Prior'); elseif (ischar (P)) if (strcmpi (P, 'uniform')) P = ones (1, K) / K; elseif (! strcmpi (P, 'empirical')) error (strcat ("CompactClassificationTree.Prior: a character", ... " vector must be 'empirical' or 'uniform'.")); else return; # 'empirical' after fitting is what is already stored endif endif if (! (isnumeric (P) && isvector (P) && isreal (P))) error (strcat ("CompactClassificationTree.Prior: must be a real", ... " numeric vector, a structure, 'empirical', or", ... " 'uniform'.")); endif if (numel (P) != K) error (strcat ("CompactClassificationTree.Prior: must have one", ... " element per class.")); endif if (any (P < 0) || ! (sum (P) > 0)) error (strcat ("CompactClassificationTree.Prior: must be", ... " nonnegative and must not be all zero.")); endif this.Prior = P(:)' / sum (P); this = deriveNodes (this); endfunction function this = set.ScoreTransform (this, val) [f, st] = parseScoreTransform (val, 'CompactClassificationTree'); this.ScoreTransform = st; this.STfun = f; endfunction function disp (this) fprintf ('\n CompactClassificationTree\n\n'); fprintf ('%22s: %s\n', 'ResponseName', this.ResponseName); fprintf ('%22s: %s\n', 'CategoricalPredictors', ... mat2str (this.CategoricalPredictors)); fprintf ('%22s: %s\n', 'ClassNames', classNameListing (this.ClassNames)); fprintf ('%22s: %s\n', 'ScoreTransform', this.ScoreTransform); fprintf ('%22s: %d\n', 'NumNodes', this.NumNodes); fprintf ('\n'); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationTree} {@var{obj} =} CompactClassificationTree (@var{Mdl}) ## ## Create a @code{CompactClassificationTree} object. ## ## @var{Mdl} is the @code{ClassificationTree} object to compact. The ## documented way to reach this constructor is the @code{compact} method. ## ## @seealso{ClassificationTree, fitctree} ## @end deftypefn function this = CompactClassificationTree (Mdl) ## Input validation if (nargin < 1) error (strcat ("CompactClassificationTree: too few input", ... " arguments.")); endif if (! isa (Mdl, 'ClassificationTree')) error (strcat ("CompactClassificationTree: MDL must be a", ... " ClassificationTree object.")); endif ## The levels a predictor read from a table was coded through ## travel with the model, so a compact one still reads a table this.PredictorLevels = Mdl.PredictorLevels; ## The node table and everything derived from it, then the three ## properties whose set methods re-derive the rest. The criterion the ## risk is measured by travels with the tree, there being no ## ModelParameters here to look it up in. this.NumNodes = Mdl.NumNodes; this.Children = Mdl.Children; this.Parent = Mdl.Parent; this.CutPredictorIndex = Mdl.CutPredictorIndex; this.CutPoint = Mdl.CutPoint; this.CutCategories = Mdl.CutCategories; this.NodeSize = Mdl.NodeSize; this.ClassCount = Mdl.ClassCount; this.PruneList = Mdl.PruneList; this.PruneAlpha = Mdl.PruneAlpha; this.ClassShare = Mdl.ClassShare; this.SplitCriterion = Mdl.ModelParameters.SplitCriterion; this.PredictorNames = Mdl.PredictorNames; this.ResponseName = Mdl.ResponseName; this.ClassNames = Mdl.ClassNames; this.CategoricalPredictors = Mdl.CategoricalPredictors; this.ExpandedPredictorNames = Mdl.ExpandedPredictorNames; this = fillCuts (this); this.Cost = Mdl.Cost; this.Prior = Mdl.Prior; this.ScoreTransform = Mdl.ScoreTransform; endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {CompactClassificationTree} {@var{label} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {CompactClassificationTree} {[@var{label}, @var{score}] =} predict (@dots{}) ## @deftypefnx {CompactClassificationTree} {[@var{label}, @var{score}, @var{node}] =} predict (@dots{}) ## @deftypefnx {CompactClassificationTree} {[@var{label}, @var{score}, @var{node}, @var{cnum}] =} predict (@dots{}) ## ## Classify new data with a trained @code{CompactClassificationTree} object. ## ## @code{@var{label} = predict (@var{obj}, @var{XC})} sends each row of ## @var{XC} down the tree and returns the class of the node it comes to ## rest at. @var{XC} must have as many columns as the predictor data the ## model was fitted on. ## ## @var{XC} may also be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: one ## the model was not fitted on is passed over, one it needs and cannot ## find is named, and a value holding a level is coded as that level was ## coded at fitting. ## ## @code{[@var{label}, @var{score}] = predict (@dots{})} also returns ## @var{score}, an @math{NxK} matrix holding the class probabilities of ## the node each row landed in, after @code{ScoreTransform}. ## ## @code{[@var{label}, @var{score}, @var{node}] = predict (@dots{})} also ## returns the number of the node each row landed in, and ## @code{[@var{label}, @var{score}, @var{node}, @var{cnum}] = predict ## (@dots{})} the index of the predicted class into @code{ClassNames}. ## ## The label is the class of least expected misclassification cost, which ## under the default @code{Cost} is the most probable class of the node. ## ## A row missing the predictor a node cuts on is stopped at that node and ## takes its answer, rather than being sent down a branch on evidence the ## row does not carry. ## ## @seealso{CompactClassificationTree, fitctree} ## @end deftypefn function [label, score, node, cnum] = predict (this, XC) ## Input validation if (nargin < 2) error ("CompactClassificationTree.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'CompactClassificationTree.predict', XC); if (isempty (XC)) error ("CompactClassificationTree.predict: XC is empty."); endif if (! (isnumeric (XC) && isreal (XC) && ismatrix (XC))) error (strcat ("CompactClassificationTree.predict: XC must be", ... " a real numeric matrix.")); endif if (numel (this.PredictorNames) != columns (XC)) error (strcat ("CompactClassificationTree.predict: XC must have", ... " the same number of predictors as the trained", ... " model.")); endif [score, node] = treepredict (XC, this.Children, ... this.CutPredictorIndex, this.CutPoint, ... this.ClassProbability, ... this.CutCategories); ## The class of least expected cost, ties kept by the first class [~, cnum] = min (score * this.Cost, [], 2); label = labelsFromIndex (this.ClassNames, cnum); ## The transform is applied once, to the assembled probabilities score = this.STfun (score); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationTree} {@var{imp} =} predictorImportance (@var{obj}) ## ## Estimate the importance of each predictor. ## ## @code{@var{imp} = predictorImportance (@var{obj})} returns a row ## vector with one element per predictor, holding the total drop in risk ## the splits on that predictor bring about, divided by the number of ## branch nodes. A predictor the tree never splits on scores zero. ## ## The drop at a branch node is its @code{NodeRisk} less the risk of its ## two children, so a predictor that is chosen often, high up, and on ## nodes it separates well, scores highest. The numbers are comparable ## between predictors of one tree and not between trees. ## ## @seealso{CompactClassificationTree, fitctree, ## CompactClassificationTree.NodeRisk} ## @end deftypefn function imp = predictorImportance (this) imp = zeros (1, numel (this.PredictorNames)); branch = find (this.Children(:,1) > 0); if (isempty (branch)) return; endif for ii = 1:numel (branch) b = branch(ii); kids = this.Children(b,:); drop = this.NodeRisk(b) - this.NodeRisk(kids(1)) ... - this.NodeRisk(kids(2)); v = this.CutPredictorIndex(b); imp(v) = imp(v) + drop; endfor imp = imp / numel (branch); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationTree} {@var{r} =} nodeVariableRange (@var{obj}, @var{node}) ## ## Range of each predictor at a node. ## ## @code{@var{r} = nodeVariableRange (@var{obj}, @var{node})} returns a ## structure with one field per predictor the path from the root to ## @var{node} cuts on, holding the two-element range of values that reach ## the node, or for a categorical predictor the levels that reach it. A ## predictor the path never cuts on is unconstrained and is left out, so ## the root gives a structure with no fields. ## ## @seealso{CompactClassificationTree, fitctree} ## @end deftypefn function r = nodeVariableRange (this, node) ## Input validation if (nargin < 2) error (strcat ("CompactClassificationTree.nodeVariableRange:", ... " too few input arguments.")); endif if (! (isnumeric (node) && isscalar (node) && isreal (node) && node >= 1 && node <= this.NumNodes && node == fix (node))) error (strcat ("CompactClassificationTree.nodeVariableRange:", ... " NODE must be a positive integer no greater than", ... " the number of nodes in the tree.")); endif p = numel (this.PredictorNames); lo = -Inf (1, p); hi = Inf (1, p); touched = false (1, p); ## Walk up to the root, narrowing the range of whichever predictor ## each step cut on. Going up rather than down finds the path ## without a search, a node having exactly one parent. ## A categorical cut narrows its predictor to a set of levels instead, ## and the first such cut met on the way up is the narrowest. levels = cell (1, p); child = node; up = this.Parent(child); while (up > 0) v = this.CutPredictorIndex(up); side = 1 + (this.Children(up,1) != child); cats = this.CutCategories{up,side}; if (! isempty (cats)) if (! touched(v)) levels{v} = cats; endif elseif (side == 1) hi(v) = min (hi(v), this.CutPoint(up)); else lo(v) = max (lo(v), this.CutPoint(up)); endif touched(v) = true; child = up; up = this.Parent(child); endwhile r = struct (); for v = find (touched) if (isempty (levels{v})) r.(this.PredictorNames{v}) = [lo(v), hi(v)]; else r.(this.PredictorNames{v}) = levels{v}; endif endfor endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationTree} {} view (@var{obj}) ## ## Print the tree as text. ## ## @code{view (@var{obj})} prints one line per node: a branch node names ## the predictor it cuts on, the cut point, and the node each side leads ## to, and a leaf names the class it assigns. A branch node's line ends ## with the class it would assign itself, which is the answer an ## observation missing that predictor gets. ## ## @seealso{CompactClassificationTree, fitctree} ## @end deftypefn function view (this) fprintf ("Decision tree for classification\n"); for ii = 1:this.NumNodes if (this.Children(ii,1) == 0) fprintf ("%d class = %s\n", ii, this.NodeClass{ii}); else v = this.PredictorNames{this.CutPredictorIndex(ii)}; cc = this.CutCategories(ii,:); if (! isempty (cc{1})) fprintf ("%d if %s then node %d elseif %s then node", ii, ... treeLevelText (v, cc{1}), this.Children(ii,1), ... treeLevelText (v, cc{2})); else c = num2str (this.CutPoint(ii)); fprintf ("%d if %s<%s then node %d elseif %s>=%s then node", ... ii, v, c, this.Children(ii,1), v, c); endif fprintf (" %d else %s\n", this.Children(ii,2), this.NodeClass{ii}); endif endfor endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationTree} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationTree} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationTree} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margin on new data. ## ## @code{@var{m} = margin (@var{obj}, @var{X}, @var{Y})} returns one ## margin per observation: the score the model gives the observation's ## true class, less the largest score it gives any other class. A ## positive margin means the observation is classified correctly, and a ## larger one means it is classified more confidently. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{CompactClassificationTree, CompactClassificationTree.edge, ## CompactClassificationTree.loss, CompactClassificationTree.predict} ## @end deftypefn function m = margin (this, X, Y) ## Input validation if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactClassificationTree.margin: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y] = tableResponse (this, 'margin', X, Y, {}, nargin > 2); [gY, errmsg] = labelIndices (this.ClassNames, Y); if (! isempty (errmsg)) error ("CompactClassificationTree.margin: %s", errmsg); endif if (rows (X) != numel (gY)) error (strcat ("CompactClassificationTree.margin: number of rows", ... " in X and Y must be equal.")); endif [~, s] = predict (this, X); m = marginsOf (s, gY, 1); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationTree} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationTree} {@var{e} =} edge (@dots{}, @qcode{'Weights'}, @var{w}) ## @deftypefnx {CompactClassificationTree} {@var{e} =} edge (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationTree} {@var{e} =} edge (@var{obj}, @var{Tbl}) ## ## Classification edge on new data. ## ## @code{@var{e} = edge (@var{obj}, @var{X}, @var{Y})} returns the ## weighted mean of the margins, a single number summarising how ## confidently the model classifies the data. ## ## The weights are normalized within each class to that class's prior ## before they are applied. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{edge (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{edge (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{CompactClassificationTree, CompactClassificationTree.margin, ## CompactClassificationTree.loss, CompactClassificationTree.predict} ## @end deftypefn function e = edge (this, X, Y, varargin) ## Input validation if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactClassificationTree.edge: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'edge', X, Y, varargin, ... nargin > 2); W = edgeWeights (varargin, Y, this.ClassNames, this.Prior, ... 'CompactClassificationTree', 'edge'); m = margin (this, X, Y); e = sum (W .* m(:)) / sum (W); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationTree} {@var{l} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactClassificationTree} {@var{l} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactClassificationTree} {@var{l} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {CompactClassificationTree} {@var{l} =} loss (@dots{}, @var{name}, @var{value}) ## ## Classification loss on new data. ## ## @code{@var{l} = loss (@var{obj}, @var{X}, @var{Y})} returns the ## minimum expected misclassification cost. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{l} = loss (@dots{}, @var{name}, @var{value})} takes the ## following options. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'LossFun'} @tab One of @qcode{'binodeviance'}, ## @qcode{'classifcost'}, @qcode{'classiferror'}, @qcode{'exponential'}, ## @qcode{'hinge'}, @qcode{'logit'}, @qcode{'mincost'} (default) or ## @qcode{'quadratic'}. ## ## @item @qcode{'Weights'} @tab A numeric vector of observation weights, ## one per row of @var{X}. ## ## @end multitable ## ## @seealso{CompactClassificationTree, CompactClassificationTree.margin, ## CompactClassificationTree.edge, CompactClassificationTree.predict} ## @end deftypefn function l = loss (this, X, Y, varargin) ## Input validation if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactClassificationTree.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("CompactClassificationTree.loss: name-value", ... " arguments must be in pairs.")); endif ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights'}; dfValues = {'mincost', []}; [LossFun, Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments lf_opt = {'binodeviance', 'classifcost', 'classiferror', ... 'exponential', 'hinge', 'logit', 'mincost', 'quadratic'}; if (! (ischar (LossFun) && any (strcmpi (LossFun, lf_opt)))) error ("CompactClassificationTree.loss: invalid loss function."); endif LossFun = tolower (LossFun); errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("CompactClassificationTree.loss: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isvector (Weights))) error ("CompactClassificationTree.loss: invalid 'Weights'."); endif if (! isempty (args)) error (strcat ("CompactClassificationTree.loss: invalid optional", ... " paired argument.")); endif [gY, errmsg] = labelIndices (this.ClassNames, Y); if (! isempty (errmsg)) error ("CompactClassificationTree.loss: %s", errmsg); endif if (rows (X) != numel (gY)) error (strcat ("CompactClassificationTree.loss: number of rows in", ... " X and Y must be equal.")); endif if (isempty (Weights)) w = ones (numel (gY), 1); else w = double (Weights(:)); if (numel (w) != numel (gY)) error (strcat ("CompactClassificationTree.loss: 'Weights' must", ... " have one element per observation.")); endif endif ## The weights are normalized within each class to that class's prior, ## so that a loss and an edge weight the classes the same way. w = priorNormalize (w, gY, this.Prior); [~, s] = predict (this, X); l = classificationLoss (LossFun, s, gY, w, this.Cost); endfunction ## -*- texinfo -*- ## @deftypefn {CompactClassificationTree} {} savemodel (@var{obj}, @var{filename}) ## ## Save a CompactClassificationTree model to a file. ## ## @code{savemodel (@var{obj}, @var{filename})} saves each property of a ## CompactClassificationTree object into an Octave binary file, the name ## of which is specified in @var{filename}, along with an extra variable, ## which defines the type of classification object these variables ## constitute. Use @code{loadmodel} in order to load a classification ## object into Octave's workspace. ## ## @seealso{loadmodel, fitctree, ClassificationTree} ## @end deftypefn function savemodel (this, fname) ## Input validation if (nargin < 2) error (strcat ("CompactClassificationTree.savemodel: too few", ... " input arguments.")); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error (strcat ("CompactClassificationTree.savemodel: FNAME must", ... " be a character vector.")); endif ## Generate variable for class name classdef_name = 'CompactClassificationTree'; ## Create variables from model properties PredictorNames = this.PredictorNames; ResponseName = this.ResponseName; ClassNames = encodeLabels (this.ClassNames); CategoricalPredictors = this.CategoricalPredictors; ExpandedPredictorNames = this.ExpandedPredictorNames; NumNodes = this.NumNodes; Children = this.Children; Parent = this.Parent; CutPredictorIndex = this.CutPredictorIndex; CutPoint = this.CutPoint; CutCategories = this.CutCategories; NodeSize = this.NodeSize; ClassCount = this.ClassCount; ClassShare = this.ClassShare; SplitCriterion = this.SplitCriterion; PruneList = this.PruneList; PruneAlpha = this.PruneAlpha; Prior = this.Prior; Cost = this.Cost; ScoreTransform = this.ScoreTransform; STfun = this.STfun; ## The cut and node descriptions are not saved: every one of them is ## re-derived from the node table above, so writing them out would only ## make a stale copy possible. save ('-binary', fname, 'classdef_name', 'PredictorNames', ... 'ResponseName', 'ClassNames', 'CategoricalPredictors', ... 'ExpandedPredictorNames', 'NumNodes', 'Children', 'Parent', ... 'CutPredictorIndex', 'CutPoint', 'CutCategories', ... 'NodeSize', 'ClassCount', ... 'ClassShare', 'SplitCriterion', 'PruneList', 'PruneAlpha', ... 'Prior', 'Cost', 'ScoreTransform', 'STfun'); endfunction endmethods methods (Static, Hidden) function mdl = load_model (filename, data) data = decodeLabels (data); ## The compact model is built from a full one and has no training data ## of its own, so the smallest fit the full class accepts is compacted ## and then filled property by property. mdl = CompactClassificationTree (ClassificationTree ([1; 2], [1; 2])); ## Copy the saved data into the object. Iterate over what was saved ## rather than over fieldnames (mdl): a hidden property such as STfun ## is written out by savemodel but is not reported by fieldnames, so ## comparing the two sets could never match. Assignment is legal here ## because this is a method of the class itself. names = fieldnames (data); ## These three are assigned once everything else is in place, and in ## this order rather than the file's. Cost comes before Prior because ## the two set methods re-derive the node statistics, which want the ## node table already loaded. order = {'Cost', 'Prior', 'ScoreTransform'}; late = ismember (names, order); tail = order(ismember (order, names)); names = [names(! late); tail(:)]; for ii = 1:numel (names) try mdl.(names{ii}) = data.(names{ii}); catch msg = strcat ("CompactClassificationTree.load_model: invalid", ... " model in '%s'."); error (msg, filename); end_try_catch endfor ## The cut and node descriptions were not saved, being derived mdl = fillCuts (mdl); mdl = deriveNodes (mdl); endfunction endmethods methods (Access = private) ## Re-derive every node statistic that depends on the prior or the cost. function this = deriveNodes (this) ## An object part way through being assembled, as load_model assembles ## one property at a time, has a node table and a prior that do not yet ## describe the same model. There is nothing to derive until they do, ## and load_model derives it once they do. if (isempty (this.ClassShare) || isempty (this.Prior) || columns (this.ClassShare) != numel (this.Prior) || rows (this.Cost) != numel (this.Prior)) return; endif S = treeNodeStats (this.ClassShare, this.Prior, this.Cost, ... this.SplitCriterion, this.ClassNames); this.NodeProbability = S.NodeProbability; this.ClassProbability = S.ClassProbability; this.NodeError = S.NodeError; this.NodeClass = S.NodeClass; this.NodeRisk = S.NodeRisk; endfunction ## The descriptions of the cuts, derived from the node table. function this = fillCuts (this) S = treeCutInfo (this.CutPredictorIndex, this.PredictorNames, ... this.CutCategories); for [val, name] = S this.(name) = val; endfor endfunction endmethods endclassdef ## Tests %!test # MATLAB parity: the surface a compact tree reports %! load fisheriris %! CMdl = compact (ClassificationTree (meas, species)); %! assert_equal (class (CMdl), 'CompactClassificationTree'); %! assert_equal (numel (properties (CMdl)), 33); %! assert_equal (CMdl.NumNodes, 9); %! assert_equal (CMdl.ClassNames, unique (species)); %! assert_equal (CMdl.Prior, [1/3, 1/3, 1/3], 1e-15); %! assert_equal (CMdl.Cost, [0, 1, 1; 1, 0, 1; 1, 1, 0]); %! assert_equal (CMdl.ResponseName, 'Y'); %! assert_equal (CMdl.PredictorNames, {'x1', 'x2', 'x3', 'x4'}); %! assert_equal (CMdl.ScoreTransform, 'none'); %!test # The compact tree carries the node table the model it came from has %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! CMdl = compact (Mdl); %! assert_equal (CMdl.Children, Mdl.Children); %! assert_equal (CMdl.Parent, Mdl.Parent); %! assert_equal (CMdl.NodeSize, Mdl.NodeSize); %! assert_equal (CMdl.ClassCount, Mdl.ClassCount); %! assert_equal (CMdl.NodeRisk, Mdl.NodeRisk, 1e-15); %! assert_equal (CMdl.NodeError, Mdl.NodeError, 1e-15); %! assert_equal (CMdl.NodeProbability, Mdl.NodeProbability, 1e-15); %! assert_equal (CMdl.ClassProbability, Mdl.ClassProbability, 1e-15); %! assert_equal (CMdl.NodeClass, Mdl.NodeClass); %! assert_equal (CMdl.PruneList, Mdl.PruneList); %! assert_equal (CMdl.PruneAlpha, Mdl.PruneAlpha, 1e-15); %! assert_equal (CMdl.CutPredictor, Mdl.CutPredictor); %! assert_equal (CMdl.CutType, Mdl.CutType); %!test # MATLAB parity: what a tree with no categories and no surrogates holds %! load fisheriris %! CMdl = compact (ClassificationTree (meas, species)); %! assert_equal (size (CMdl.CutCategories), [9, 2]); %! assert_equal (size (CMdl.CategoricalSplit), [0, 0]); %! assert_equal (size (CMdl.SurrogateCutPredictor), [0, 1]); %! assert_equal (size (CMdl.SurrogateCutPoint), [0, 0]); %! assert_equal (size (CMdl.SurrogatePredictorAssociation), [0, 0]); %!test # MATLAB parity: predict answers exactly as the full model does %! load fisheriris %! Mdl = ClassificationTree (meas, species); %! CMdl = compact (Mdl); %! [l1, s1, n1, c1] = predict (Mdl, meas); %! [l2, s2, n2, c2] = predict (CMdl, meas); %! assert_equal ({l1, s1, n1, c1}, {l2, s2, n2, c2}); %! assert_equal (predict (CMdl, meas([1, 60, 120], :)), ... %! {'setosa'; 'versicolor'; 'virginica'}); %!test # MATLAB parity: loss, margin and edge on a compact tree %! load fisheriris %! CMdl = compact (ClassificationTree (meas, species)); %! assert_equal (loss (CMdl, meas, species), 0.02, 1e-14); %! assert_equal (edge (CMdl, meas, species), 0.938357487922706, 1e-14); %! m = margin (CMdl, meas([1, 60, 120], :), species([1, 60, 120])); %! assert_equal (m', [1, 1, 1/3], 1e-14); %! assert_equal (loss (CMdl, meas, species, 'LossFun', 'hinge'), ... %! 0.0308212560386473, 1e-14); %!test # MATLAB parity: edge on a set missing a class %! load fisheriris %! CMdl = compact (ClassificationTree (meas, species)); %! r = 51:150; %! assert_equal (edge (CMdl, meas(r,:), species(r)), 0.9075362319, 1e-10); %!test # MATLAB parity: predictorImportance and nodeVariableRange %! load fisheriris %! CMdl = compact (ClassificationTree (meas, species)); %! assert_equal (predictorImportance (CMdl), ... %! [0, 0, 0.0907484567901233, 0.0682128958668813], 1e-14); %! r = nodeVariableRange (CMdl, 8); %! assert_equal (r.x3, [2.45, 4.95], 1e-14); %! assert_equal (r.x4, [-Inf, 1.65], 1e-14); %! assert_equal (fieldnames (nodeVariableRange (CMdl, 1)), cell (0, 1)); %!test # MATLAB parity: the text form of a compact tree %! load fisheriris %! CMdl = compact (ClassificationTree (meas, species)); %! lines = strsplit (strtrim (evalc ('view (CMdl)')), "\n"); %! assert_equal (numel (lines), 10); %! assert_equal (lines{2}, ... %! '1 if x3<2.45 then node 2 elseif x3>=2.45 then node 3 else setosa'); %! assert_equal (lines{10}, '9 class = virginica'); %!test # The risk follows the criterion the tree it came from was grown under %! load fisheriris %! Mdl = ClassificationTree (meas, species, 'SplitCriterion', 'deviance'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.NodeRisk, Mdl.NodeRisk, 1e-15); %! assert_equal (CMdl.NodeRisk(1), 0.792481250360577, 1e-14); %!test # Reassigning Cost re-derives the node statistics, not the tree %! load fisheriris %! CMdl = compact (ClassificationTree (meas, species)); %! shape = {CMdl.Children, CMdl.NodeSize, CMdl.ClassCount}; %! CMdl.Cost = [0, 2, 8; 3, 0, 1; 5, 4, 0]; %! assert_equal ({CMdl.Children, CMdl.NodeSize, CMdl.ClassCount}, shape); %! assert_equal (CMdl.NodeClass{1}, 'versicolor'); %! assert_equal (CMdl.NodeError(1), 2, 1e-14); %!test # Reassigning Prior re-derives the node statistics %! load fisheriris %! CMdl = compact (ClassificationTree (meas, species)); %! CMdl.Prior = [0.5, 0.25, 0.25]; %! assert_equal (CMdl.Prior, [0.5, 0.25, 0.25], 1e-14); %! assert_equal (CMdl.ClassProbability(1,:), [0.5, 0.25, 0.25], 1e-14); %! assert_equal (CMdl.NodeProbability(2), 0.5, 1e-14); %! assert_equal (CMdl.NodeRisk(1), 0.625, 1e-14); %!test # The score transform travels with the compact model %! load fisheriris %! CMdl = compact (ClassificationTree (meas, species, ... %! 'ScoreTransform', 'logit')); %! assert_equal (CMdl.ScoreTransform, 'logit'); %! [~, score] = predict (CMdl, meas(1, :)); %! assert_equal (score, [0.731058578630005, 0.5, 0.5], 1e-14); %!test # A compact model saved and loaded answers exactly as it did %! load fisheriris %! CMdl = compact (ClassificationTree (meas, species)); %! fname = tempname (); %! unwind_protect %! savemodel (CMdl, fname); %! New = loadmodel (fname); %! assert_equal (class (New), 'CompactClassificationTree'); %! assert_equal (New.NumNodes, CMdl.NumNodes); %! assert_equal (New.NodeRisk, CMdl.NodeRisk, 1e-15); %! assert_equal (New.CutPredictor, CMdl.CutPredictor); %! assert_equal (predict (New, meas), predict (CMdl, meas)); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test input validation %!error %! CompactClassificationTree () %!error %! CompactClassificationTree (5) %!error %! predict (compact (ClassificationTree (ones (4, 2), [1; 1; 2; 2]))) %!error %! predict (compact (ClassificationTree (ones (4, 2), [1; 1; 2; 2])), []) %!error %! predict (compact (ClassificationTree (ones (4, 2), [1; 1; 2; 2])), 'a') %!error %! predict (compact (ClassificationTree (ones (4, 2), [1; 1; 2; 2])), ... %! ones (2, 3)) %!error %! margin (compact (ClassificationTree (ones (4, 2), [1; 1; 2; 2])), ... %! ones (4, 2)) %!error %! edge (compact (ClassificationTree (ones (4, 2), [1; 1; 2; 2])), ones (4, 2)) %!error %! loss (compact (ClassificationTree (ones (4, 2), [1; 1; 2; 2])), ones (4, 2)) %!error %! loss (compact (ClassificationTree (ones (4, 2), [1; 1; 2; 2])), ... %! ones (4, 2), [1; 1; 2; 2], 'LossFun', 'x') %!error ... %! loss (compact (ClassificationTree (ones (4, 2), [1; 1; 2; 2])), ... %! ones (4, 2), [1; 1; 2; 2], 'Bogus', 1) %!error %! nodeVariableRange (compact (ClassificationTree (ones (4, 2), ... %! [1; 1; 2; 2])), 99) %!error %! savemodel (compact (ClassificationTree (ones (4, 2), [1; 1; 2; 2]))) %!error %! savemodel (compact (ClassificationTree (ones (4, 2), [1; 1; 2; 2])), 5) %!error %! CMdl = compact (ClassificationTree (ones (4, 2), [1; 1; 2; 2])); %! CMdl.Prior = [0.2, 0.3, 0.5]; %!test # a compact tree keeps and uses the level sets of its cuts %! k = (0:79)'; %! c = mod (k, 4) + 1; %! j = floor (k / 4); %! X = [c, mod(k * 7, 10)]; %! y = (c == 1) | (c == 2 & mod (j, 4) != 0) | (c == 3 & mod (j, 4) == 0); %! Mdl = compact (fitctree (X, y, 'CategoricalPredictors', 1)); %! assert_equal (Mdl.CategoricalPredictors, 1); %! assert_equal (Mdl.CategoricalSplit, {[1, 2], [3, 4]}); %! [~, ~, nd] = predict (Mdl, [1, 0; 3, 0; 5, 0]); %! assert_equal (nd', [2, 3, 1]); %! fname = [tempname(), '.mdl']; %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.CutCategories, Mdl.CutCategories); ## A table at prediction %!shared cctT, cctM %! load fisheriris %! cctT = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! cctT.Species = categorical (species); %! cctT.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! cctM = compact (fitctree (cctT, 'Species')); %!test # the levels a predictor was coded through travel with the model %! assert_equal (numel (cctM.PredictorLevels), 5); %! assert_equal (cctM.PredictorLevels{5}, {'narrow', 'wide'}); %!test # predict takes a table, read by name and not by position %! a = predict (cctM, cctT); %! assert_equal (class (a), 'categorical'); %! assert_equal (predict (cctM, cctT(:, [6, 5, 4, 3, 2, 1])), a); %!test # a matrix is still taken %! load fisheriris %! CMdl = compact (fitctree (meas, species)); %! assert_equal (numel (predict (CMdl, meas)), 150); %!error ... %! predict (cctM, cctT(:, [1, 3, 4, 5, 6])) ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitctree (T, 'Species')); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'Species'), a); %! assert_equal (loss (Mdl, T), a); ## A table at edge %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitctree (T, 'Species')); %! a = edge (Mdl, X, y); %! assert_equal (edge (Mdl, T(:,1:2), y), a); %! assert_equal (edge (Mdl, T, 'Species'), a); %! assert_equal (edge (Mdl, T), a); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (fitctree (T, 'Species')); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## Observation weights of class single or double %!error ... %! loss (compact (fitctree (ones (4, 2), [1; 1; 2; 2])), ones (4, 2), ... %! [1; 1; 2; 2], ... %! 'Weights', int8 ([1; 1; 1; 1])) statistics-release-2.1.2/inst/Machine_Learning/CompactRegressionEnsemble.m000066400000000000000000000703411527311104300267460ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef CompactRegressionEnsemble < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} CompactRegressionEnsemble ## ## Compact ensemble of regression trees ## ## A @code{CompactRegressionEnsemble} object carries the trained trees of a ## boosted or bagged regression ensemble and what prediction needs, but not ## the observations it was fitted on. It predicts new data identically to ## the ensemble it came from, and trees can be removed from it. ## ## Create one with the @code{compact} method of a ## @code{RegressionEnsemble} or @code{RegressionBaggedEnsemble} object. ## ## @seealso{fitrensemble, RegressionEnsemble, RegressionBaggedEnsemble} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CompactRegressionEnsemble} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionEnsemble} {property} CategoricalPredictors ## ## Indices of categorical predictors ## ## The predictors every tree treats as categorical, empty when none ## is. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {CompactRegressionEnsemble} {property} ResponseName ## ## Name of the response variable ## ## This property is read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {CompactRegressionEnsemble} {property} ExpandedPredictorNames ## ## Names of the predictors as the learners saw them ## ## The same as @code{PredictorNames}. This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionEnsemble} {property} UsePredForLearner ## ## Which predictors each learner uses ## ## Always empty, as MATLAB returns it for tree learners. This property ## is read-only. ## ## @end deftp UsePredForLearner = []; ## -*- texinfo -*- ## @deftp {CompactRegressionEnsemble} {property} NumTrained ## ## Number of trained trees ## ## This property is read-only. ## ## @end deftp NumTrained = 0; ## -*- texinfo -*- ## @deftp {CompactRegressionEnsemble} {property} Trained ## ## Trained trees ## ## A column cell array of @code{CompactRegressionTree} objects. This ## property is read-only. ## ## @end deftp Trained = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionEnsemble} {property} TrainedWeights ## ## Weights of the trained trees ## ## A column with one weight per tree. This property is read-only. ## ## @end deftp TrainedWeights = []; ## -*- texinfo -*- ## @deftp {CompactRegressionEnsemble} {property} CombineWeights ## ## How the trees are combined ## ## @qcode{'WeightedSum'} for LSBoost, whose prediction is the sum of each ## tree's prediction times its weight, or @qcode{'WeightedAverage'} for ## Bag, whose prediction is the weighted average of its trees'. This ## property is read-only. ## ## @end deftp CombineWeights = 'WeightedSum'; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {CompactRegressionEnsemble} {property} ResponseTransform ## ## Transform applied to the predicted response ## ## @qcode{'none'} (default), @qcode{'exp'}, @qcode{'log'} or a function ## handle. Predictions and losses use the transformed response. ## ## @end deftp ResponseTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) RTfun = @(y) y; # the response transform as a function endproperties methods (Hidden) function this = set.ResponseTransform (this, val) [this.RTfun, this.ResponseTransform] = ... parseResponseTransform (val, 'CompactRegressionEnsemble'); endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionEnsemble} {@var{obj} =} CompactRegressionEnsemble (@var{Mdl}) ## ## Create a @code{CompactRegressionEnsemble} object. ## ## @var{Mdl} is the @code{RegressionEnsemble} or ## @code{RegressionBaggedEnsemble} object to compact. The documented way ## to reach this constructor is the @code{compact} method. ## ## @end deftypefn function this = CompactRegressionEnsemble (Mdl = []) if (isempty (Mdl)) return; endif if (! isa (Mdl, 'RegressionEnsemble')) error (strcat ("CompactRegressionEnsemble: MDL must be a", ... " 'RegressionEnsemble' object.")); endif ## The levels a predictor read from a table was coded through travel ## with the model, so a compact one still reads a table this.PredictorLevels = Mdl.PredictorLevels; this.PredictorNames = Mdl.PredictorNames; this.CategoricalPredictors = Mdl.CategoricalPredictors; this.ResponseName = Mdl.ResponseName; this.ExpandedPredictorNames = Mdl.ExpandedPredictorNames; this.UsePredForLearner = Mdl.UsePredForLearner; this.NumTrained = Mdl.NumTrained; this.Trained = Mdl.Trained; this.TrainedWeights = Mdl.TrainedWeights; this.CombineWeights = Mdl.CombineWeights; ## A transform given as a function handle is held as its text, which ## names no built-in transform, so the function itself is copied. if (any (strcmp (Mdl.ResponseTransform, {'none', 'exp', 'log'}))) this.ResponseTransform = Mdl.ResponseTransform; else this.ResponseTransform = Mdl.RTfun; endif endfunction ## Keep the learners IDX, in that order, with the weights WEIGHTS and the ## combination rule COMBINE; shrink builds its ensemble with this. function this = keepLearners (this, idx, weights, combine) this.Trained = this.Trained(idx); this.TrainedWeights = weights(:); this.NumTrained = numel (idx); this.CombineWeights = combine; endfunction function disp (this) fprintf ("\n CompactRegressionEnsemble\n\n"); fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); fprintf ("%+25s: %s\n", 'CategoricalPredictors', ... mat2str (this.CategoricalPredictors)); fprintf ("%+25s: '%s'\n", 'ResponseTransform', this.ResponseTransform); fprintf ("%+25s: %d\n", 'NumTrained', this.NumTrained); fprintf ("\n"); endfunction ## The shared bodies of predict and loss. The full and bagged classes ## call them with their own names, so an error names the method called. function yfit = ensemblePredict (this, X, args, caller) o = ensembleArgs (this, X, args, {'Learners', 'UseObsForLearner'}, ... caller); yfit = this.RTfun (ensembleResponse (this, X, o, 'ensemble')); endfunction function L = ensembleLoss (this, X, Y, args, caller) o = ensembleArgs (this, X, args, {'LossFun', 'Learners', 'Mode', ... 'UseObsForLearner', 'Weights'}, ... caller); if (! (isnumeric (Y) && isreal (Y) && isvector (Y))) error ("%s: Y must be a real numeric vector.", caller); endif Y = double (Y(:)); if (numel (Y) != rows (X)) error ("%s: X and Y must have the same number of rows.", caller); endif w = o.Weights; if (isempty (w)) w = ones (rows (X), 1); endif ## A row with no response cannot be judged, so it is left out and the ## weights are normalized over the rest, as MATLAB does. keep = ! isnan (Y); if (! (sum (w(keep)) > 0)) error (strcat ("%s: 'Weights' must not be zero for every", ... " observation with a response."), caller); endif o.U = o.U(keep,:); Yf = this.RTfun (ensembleResponse (this, X(keep,:), o, o.Mode)); Y = Y(keep); w = w(keep) / sum (w(keep)); L = zeros (columns (Yf), 1); for k = 1:columns (Yf) ## A row no tree may predict is left out with its weight, the rest ## renormalized, as MATLAB does. have = ! isnan (Yf(:,k)); wk = w(have); if (! (sum (wk) > 0)) L(k) = NaN; continue; endif wk /= sum (wk); if (is_function_handle (o.LossFun)) L(k) = o.LossFun (Y(have), Yf(have,k), wk); else L(k) = sum (wk .* (Yf(have,k) - Y(have)) .^ 2); endif endfor endfunction ## The untransformed predictions for the rows of X over the first t ## trees, for every t, as an NxNumTrained matrix. function Y = ensembleSteps (this, X) T = this.NumTrained; o = struct ('Learners', 1:T, 'U', true (rows (X), T)); Y = ensembleResponse (this, X, o, 'cumulative'); endfunction function [imp, ma] = ensembleImportance (this) imp = zeros (1, numel (this.PredictorNames)); for t = 1:this.NumTrained imp += this.TrainedWeights(t) * predictorImportance (this.Trained{t}); endfor if (sum (this.TrainedWeights) > 0) imp /= sum (this.TrainedWeights); endif ma = []; endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {CompactRegressionEnsemble} {@var{yfit} =} predict (@var{obj}, @var{X}) ## @deftypefnx {CompactRegressionEnsemble} {@var{yfit} =} predict (@dots{}, @var{name}, @var{value}) ## ## Predict the response with a compact regression ensemble. ## ## @var{yfit} holds, for each row of @var{X}, the sum over the trees of ## each tree's prediction times its weight for LSBoost, or the weighted ## average of the trees' predictions for Bag, after ## @code{ResponseTransform}. A row that no tree may predict is ## @code{NaN}. ## ## Name-Value arguments: ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Learners'} @tab @tab A vector of indices of the trees to ## use. The default is all of them. ## @item @qcode{'UseObsForLearner'} @tab @tab An @math{NxNumTrained} ## logical matrix saying which tree may predict which row. The default ## lets every tree predict every row. ## @end multitable ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @seealso{CompactRegressionEnsemble, fitrensemble} ## @end deftypefn function yfit = predict (this, X, varargin) if (nargin < 2) error ("CompactRegressionEnsemble.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on X = tableColumns (this, 'CompactRegressionEnsemble.predict', X); yfit = ensemblePredict (this, X, varargin, ... 'CompactRegressionEnsemble.predict'); endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionEnsemble} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactRegressionEnsemble} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactRegressionEnsemble} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {CompactRegressionEnsemble} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Regression loss of a compact ensemble. ## ## @var{L} is the weighted mean squared error of the predictions for the ## rows of @var{X} against @var{Y}, the weights normalized to sum to one ## over the rows that have a response; a row whose response is missing is ## left out. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## Name-Value arguments: ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'LossFun'} @tab @tab @qcode{'mse'} (default) or a ## function handle called as @code{lossfun (Y, Yfit, W)}, with column ## vectors of the responses, the predictions and the normalized weights, ## returning a scalar. ## @item @qcode{'Mode'} @tab @tab @qcode{'ensemble'} (default) for one ## loss over the trees used, @qcode{'cumulative'} for a column whose ## element @math{j} uses the first @math{j} of them, or ## @qcode{'individual'} for a column with the loss of each on its own. ## @item @qcode{'Weights'} @tab @tab A nonnegative vector with one weight ## per row. The default is uniform. ## @end multitable ## ## @qcode{'Learners'} and @qcode{'UseObsForLearner'} are taken as by ## @code{predict}. A row that no tree may predict is left out and the ## weights are renormalized over the rest. ## ## @seealso{CompactRegressionEnsemble, CompactRegressionEnsemble.predict} ## @end deftypefn function L = loss (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactRegressionEnsemble.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); L = ensembleLoss (this, X, Y, varargin, ... 'CompactRegressionEnsemble.loss'); endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionEnsemble} {@var{imp} =} predictorImportance (@var{obj}) ## @deftypefnx {CompactRegressionEnsemble} {[@var{imp}, @var{ma}] =} predictorImportance (@var{obj}) ## ## Estimate the importance of each predictor. ## ## @var{imp} is a row vector with one element per predictor, the average ## over the trees of each tree's @code{predictorImportance}, weighted by ## @code{TrainedWeights}. @var{ma}, the predictive measure of ## association between the predictors, is empty, the trees growing no ## surrogate splits. ## ## @seealso{CompactRegressionEnsemble} ## @end deftypefn function [imp, ma] = predictorImportance (this) [imp, ma] = ensembleImportance (this); endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionEnsemble} {@var{C} =} removeLearners (@var{obj}, @var{idx}) ## ## Remove trees from a compact regression ensemble. ## ## @var{C} is the ensemble without the trees whose indices @var{idx} ## holds, their weights removed with them. ## ## @seealso{CompactRegressionEnsemble} ## @end deftypefn function this = removeLearners (this, idx) if (nargin < 2) error (strcat ("CompactRegressionEnsemble.removeLearners: too few", ... " input arguments.")); endif if (! (isnumeric (idx) && isvector (idx) && isreal (idx) && all (idx >= 1) && all (idx <= this.NumTrained) && all (idx == fix (idx)))) error (strcat ("CompactRegressionEnsemble.removeLearners: IDX must", ... " be a vector of indices of trained learners.")); endif keep = true (1, this.NumTrained); keep(idx) = false; this.Trained = this.Trained(keep); this.TrainedWeights = this.TrainedWeights(keep); this.NumTrained = sum (keep); endfunction endmethods methods (Access = private) ## The Name-Value arguments of predict and loss, those not in ALLOWED ## refused. function o = ensembleArgs (this, X, args, allowed, caller) if (! (isnumeric (X) && isreal (X) && ismatrix (X))) error ("%s: X must be a real numeric matrix.", caller); endif if (columns (X) != numel (this.PredictorNames)) error ("%s: X must have one column per predictor.", caller); endif if (mod (numel (args), 2) != 0) error ("%s: name-value arguments must be in pairs.", caller); endif ## Parse optional paired arguments. Each caller accepts the subset of ## them ALLOWED lists; an option left empty takes its default below: ## every trained learner, every observation for every learner, the ## whole ensemble, uniform weights and the mse loss. optNames = {'Learners', 'UseObsForLearner', 'Mode', 'Weights', ... 'LossFun'}; [Learners, U, Mode, Weights, LossFun, args] = ... parsePairedArguments (optNames, {[], [], [], [], []}, args(:)); given = optNames(! cellfun (@isempty, {Learners, U, Mode, Weights, ... LossFun})); if (! isempty (args) || ! all (ismember (lower (given), lower (allowed)))) error ("%s: invalid optional paired argument.", caller); endif ## Validate optional paired arguments T = this.NumTrained; o = struct ('Learners', 1:T, 'U', true (rows (X), T), ... 'Mode', 'ensemble', 'Weights', [], 'LossFun', 'mse'); if (! isempty (Learners)) if (! (isnumeric (Learners) && isvector (Learners) && isreal (Learners) && all (Learners >= 1) && all (Learners <= T) && all (Learners == fix (Learners)))) error (strcat ("%s: 'Learners' must be a vector of indices", ... " of trained learners."), caller); endif o.Learners = double (Learners(:)'); endif if (! isempty (U)) if (! (islogical (U) && isequal (size (U), [rows(X), T]))) error (strcat ("%s: 'UseObsForLearner' must be a logical", ... " matrix with one row per observation and", ... " one column per trained learner."), caller); endif o.U = U; endif if (! isempty (Mode)) if (! (ischar (Mode) && any (strcmpi (Mode, {'ensemble', ... 'cumulative', ... 'individual'})))) error (strcat ("%s: 'Mode' must be 'ensemble', 'cumulative'", ... " or 'individual'."), caller); endif o.Mode = tolower (Mode); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("%s: %s", caller, errmsg); endif if (! isempty (Weights)) if (! (isnumeric (Weights) && isvector (Weights) && isreal (Weights) && numel (Weights) == rows (X) && all (Weights >= 0))) error (strcat ("%s: 'Weights' must be a nonnegative numeric", ... " vector with one element per observation."), ... caller); endif o.Weights = double (Weights(:)); endif if (! isempty (LossFun)) if (ischar (LossFun) && strcmpi (LossFun, 'mse')) o.LossFun = 'mse'; elseif (is_function_handle (LossFun)) o.LossFun = LossFun; else error ("%s: 'LossFun' must be 'mse' or a function handle.", ... caller); endif endif endfunction ## The untransformed predictions over the trees O names: a column for ## MODE 'ensemble', one column per tree otherwise. A row no tree may ## predict is NaN. function Y = ensembleResponse (this, X, o, mode) n = rows (X); T = numel (o.Learners); A = zeros (n, T); V = zeros (n, T); for j = 1:T t = o.Learners(j); u = o.U(:,t); A(:,j) = this.TrainedWeights(t) * predict (this.Trained{t}, X) .* u; V(:,j) = this.TrainedWeights(t) * u; endfor U = double (o.U(:,o.Learners)); switch (mode) case 'ensemble' num = sum (A, 2); den = sum (V, 2); used = sum (U, 2) > 0; case 'cumulative' num = cumsum (A, 2); den = cumsum (V, 2); used = cumsum (U, 2) > 0; otherwise num = A; den = V; used = U > 0; endswitch if (strcmp (this.CombineWeights, 'WeightedAverage')) Y = num ./ den; else Y = num; endif Y(! used) = NaN; endfunction endmethods endclassdef ## Test output %!shared X, y, C %! load fisheriris %! X = meas(:,2:4); %! y = meas(:,1); %! C = compact (fitrensemble (X, y, 'NumLearningCycles', 4, ... %! 'Learners', templateTree ('MaxNumSplits', 1))); %!test # MATLAB parity: the properties of a compact regression ensemble %! assert_equal (numel (properties (C)), 10); %! assert_equal (C.NumTrained, 4); %! assert_equal (isprop (C, 'X'), false); %!test # MATLAB parity: predictions over a subset of the trees %! assert_equal (predict (C, X(1:2,:), 'Learners', [2, 4]), ... %! [0.061599937710460; 0.061599937710460], 1e-13); %!test # MATLAB parity: a row no tree may predict is NaN %! U = true (2, 4); %! U(1,:) = false; %! U(2,[1, 3]) = false; %! yf = predict (C, X(1:2,:), 'UseObsForLearner', U); %! assert_equal (isnan (yf(1)), true); %! assert_equal (yf(2), 0.061599937710460, 1e-13); %!test # MATLAB parity: the loss in its three modes %! assert_equal (loss (C, X, y), 0.159714716160712, 1e-13); %! assert_equal (loss (C, X, y, 'Mode', 'cumulative'), ... %! [0.263279369032794; 0.185334478476685; ... %! 0.176267398277276; 0.159714716160713], 1e-13); %! assert_equal (loss (C, X, y, 'Mode', 'individual'), ... %! [0.263279369032794; 34.658932998586877; ... %! 34.730819835683143; 35.008549381231070], 1e-11); %! assert_equal (loss (C, X, y, 'Learners', [1, 3]), 0.245973792973264, 1e-13); %!test # MATLAB parity: weights are normalized to sum to one %! w = [2 * ones(50, 1); ones(100, 1)]; %! assert_equal (loss (C, X, y, 'Weights', w), 0.150822300473683, 1e-13); %!test # MATLAB parity: a custom loss gets normalized weights %! f = @(Y, Yf, W) sum (W .* abs (Y - Yf)) / sum (W); %! assert_equal (loss (C, X, y, 'LossFun', f), 0.321254856340782, 1e-13); %! w = [3 * ones(50, 1); ones(100, 1)]; %! assert_equal (loss (C, X, y, 'LossFun', @(Y, Yf, W) sum (W), ... %! 'Weights', w), 1, 1e-14); %! assert_equal (loss (C, X, y, 'LossFun', @(Y, Yf, W) W(1), ... %! 'Weights', w), 0.012, 1e-15); %!test # MATLAB parity: a missing response is left out of the loss %! C3 = removeLearners (C, 4); %! yn = y; %! yn(3) = NaN; %! assert_equal (loss (C3, X, yn), 0.177018571063146, 1e-13); %! assert_equal (loss (C3, X, y, 'Mode', 'cumulative', 'Weights', ... %! [3 * ones(50, 1); ones(100, 1)]), ... %! [0.218707467544654; 0.164143043739940; ... %! 0.149902130360542], 1e-13); %!test # MATLAB parity: removing trees %! D = removeLearners (C, [1, 3]); %! assert_equal (D.NumTrained, 2); %! assert_equal (predict (D, X(1:2,:)), [0.061599937710460; ... %! 0.061599937710460], 1e-13); ## Test input validation %!error ... %! CompactRegressionEnsemble (1) %!error ... %! predict (C) %!error ... %! predict (C, {1}) %!error ... %! predict (C, ones (2, 4)) %!error ... %! predict (C, X, 'Learners') %!error ... %! predict (C, X, 'Mode', 'ensemble') %!error ... %! predict (C, X, 'Learners', 5) %!error ... %! predict (C, X, 'UseObsForLearner', true (2, 4)) %!error ... %! loss (C, X) %!error ... %! loss (C, X, y, 'Mode', 'all') %!error ... %! loss (C, X, y, 'Weights', ones (3, 1)) %!error ... %! loss (C, X, y, 'LossFun', 'mae') %!error ... %! loss (C, X, {1}) %!error ... %! loss (C, X, y(1:3)) %!error ... %! loss (C, X, y, 'Weights', zeros (150, 1)) %!error ... %! removeLearners (C) %!error ... %! removeLearners (C, 5) %!error ... %! D = C; %! D.ResponseTransform = 1; %!test # MATLAB parity: importance is the weighted average over the trees %! M = fitrensemble (X, y, 'NumLearningCycles', 3, 'LearnRate', 0.5, ... %! 'Learners', templateTree ('MaxNumSplits', 1)); %! [imp, ma] = predictorImportance (compact (M)); %! assert_equal (imp, [0, 0.223154680811778, 0], 1e-13); %! assert_equal (ma, []); %!test # MATLAB parity: a row no tree may predict is left out of the loss %! U = true (150, 4); %! U(1,:) = false; %! e = mean ((predict (C, X(2:end,:)) - y(2:end)) .^ 2); %! assert_equal (loss (C, X, y, 'UseObsForLearner', U), e, 1e-14); ## A table at prediction %!test # the levels travel with the model, and predict reads a table by name %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'SL'}); %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! Mdl = fitrensemble (T, 'SL'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.PredictorLevels, Mdl.PredictorLevels); %! a = predict (CMdl, T); %! assert_equal (predict (CMdl, T(:, [4, 3, 2, 1])), a); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,2:3); %! y = meas(:,1); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SW', 'PL'}); %! T.SL = y; %! Mdl = compact (fitrensemble (T, 'SL')); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'SL'), a); %! assert_equal (loss (Mdl, T), a); ## Observation weights of class single or double %!error ... %! loss (compact (fitrensemble ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', ... %! 'NumLearningCycles', 3)), ... %! [1, 2; 3, 4; 5, 6; 7, 8], (1:4)', 'Weights', int8 ([1; 1; 1; 1])) statistics-release-2.1.2/inst/Machine_Learning/CompactRegressionGAM.m000066400000000000000000001114271527311104300256210ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef CompactRegressionGAM < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} CompactRegressionGAM ## ## Compact generalized additive model regression ## ## The @code{CompactRegressionGAM} class implements a compact version of the ## generalized additive model regression object, which predicts responses for ## new data with the @code{predict} method but does not store the training ## data. ## ## A compact model consumes less memory than the full @code{RegressionGAM} ## model, but cannot perform tasks that need the training data, such as ## computing a resubstitution loss or the standard deviation of a prediction. ## ## Create a @code{CompactRegressionGAM} object by using the @code{compact} ## method on a @code{RegressionGAM} object. ## ## The engine that fitted the model is carried over in @code{FitMethod}, ## and the compact model predicts by the same scheme the full one did. ## Under @qcode{'boostedtrees'}, the default, the fit is described by ## @code{TreeModel}, @code{BinEdges} and @code{PairDetectionBinEdges}. ## Under @qcode{'splines'} it is described by @code{Formula}, ## @code{BaseModel}, @code{ModelwInt} and @code{IntMatrix}, which MATLAB's ## compact model does not carry. ## Whichever fitted the model, the other set is empty. A standard ## deviation is available from the spline engine alone. ## ## @seealso{RegressionGAM, fitrgam} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CompactRegressionGAM} {property} NumPredictors ## ## Number of predictors ## ## A positive integer, the number of predictors of the training data. ## This property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {CompactRegressionGAM} {property} PredictorNames ## ## Names of the predictor variables ## ## A cell array of character vectors naming the predictors, in the order ## they appear in the training data. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionGAM} {property} ResponseName ## ## Response variable name ## ## A character vector naming the response variable @var{Y}. This ## property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {CompactRegressionGAM} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A numeric vector holding the column of each predictor treated as ## categorical, and empty when none is. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {CompactRegressionGAM} {property} ExpandedPredictorNames ## ## Names of the expanded predictor variables ## ## A cell array of character vectors naming the predictors as the model ## sees them. It matches @code{PredictorNames} unless a categorical ## predictor was expanded into dummy variables. This property is ## read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionGAM} {property} Intercept ## ## Intercept of the fitted model ## ## A numeric scalar, the mean of the response, which every additive term ## is measured against. This property is read-only. ## ## @end deftp Intercept = []; ## -*- texinfo -*- ## @deftp {CompactRegressionGAM} {property} Formula ## ## Formula of the model ## ## A character vector naming the response and the terms of the model, as ## in @qcode{'Y ~ x1 + x2 + x1:x2'}, or empty when the model was not ## given one. This property is read-only. ## ## @end deftp Formula = []; ## -*- texinfo -*- ## @deftp {CompactRegressionGAM} {property} Interactions ## ## Two-way interaction terms of the fitted model ## ## A @math{Kx2} matrix of predictor index pairs, one row per two-way term ## the model carries, and @code{zeros (0, 2)} when it carries none. It ## reports what was fitted rather than what was asked for, so a count of ## terms, @qcode{'all'}, a logical matrix and a formula all leave the same ## kind of value behind. This property is read-only. ## ## A main effect names one predictor and a higher-order term names three ## or more, and neither has a two-column form, so neither appears here. ## @code{IntMatrix} remains the complete record of every term fitted. ## ## @end deftp Interactions = zeros (0, 2); ## -*- texinfo -*- ## @deftp {CompactRegressionGAM} {property} IsStandardDeviationFit ## ## Flag for a fitted standard deviation model ## ## A boolean flag, always @qcode{false}, as this class estimates the ## standard deviation of a prediction from the residuals of the fit ## rather than fitting a model for it. This property is read-only. ## ## @end deftp IsStandardDeviationFit = false; ## -*- texinfo -*- ## @deftp {CompactRegressionGAM} {property} BaseModel ## ## Model without interaction terms ## ## A structure holding the intercept, the piecewise polynomial of each ## predictor, the number of backfitting cycles, the residuals and the ## residual sum of squares of the model fitted without interaction ## terms. This property is read-only. ## ## @end deftp BaseModel = []; ## -*- texinfo -*- ## @deftp {CompactRegressionGAM} {property} ModelwInt ## ## Model with interaction terms ## ## A structure of the same fields as @code{BaseModel}, for the model ## fitted with the interaction terms, and empty when none was asked for. ## This property is read-only. ## ## @end deftp ModelwInt = []; ## -*- texinfo -*- ## @deftp {CompactRegressionGAM} {property} IntMatrix ## ## Every term the model fits ## ## A logical matrix with one row per term and one column per predictor, ## true wherever the term multiplies that predictor. A row naming one ## predictor is a main effect, two an interaction, and three or more a ## higher-order term. This property is read-only. ## ## It is the complete record, where @code{Interactions} reports only the ## two-way terms, in the form MATLAB reports them. It is also the form ## the @qcode{'Interactions'} option takes back, so passing it to the ## constructor rebuilds a model over the same terms. ## ## @end deftp IntMatrix = []; ## -*- texinfo -*- ## @deftp {CompactRegressionGAM} {property} BinEdges ## ## Bin edges of the fitted shape functions, empty under the spline ## engine. This property is read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionGAM} {property} PairDetectionBinEdges ## ## Bin edges used to detect interactions, eight equal-frequency bins per ## predictor, empty when the model carries none. This property is ## read-only. ## ## @end deftp PairDetectionBinEdges = []; ## -*- texinfo -*- ## @deftp {CompactRegressionGAM} {property} FitMethod ## ## Which engine fitted the model, @qcode{'boostedtrees'} or ## @qcode{'splines'}. This property is read-only. ## ## @end deftp FitMethod = 'boostedtrees'; ## -*- texinfo -*- ## @deftp {CompactRegressionGAM} {property} TreeModel ## ## The fitted shape functions and interaction surfaces, empty under the ## spline engine. This property is read-only. ## ## @end deftp TreeModel = []; endproperties ## Properties a user may set after the model is built. Each one is ## validated by its set method below. properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {CompactRegressionGAM} {property} ResponseTransform ## ## Transformation applied to the predicted response ## ## A function handle applied to the response the model predicts. Add or ## change it using dot notation, as in ## @qcode{@var{obj}.ResponseTransform = 'log'} or ## @qcode{@var{obj}.ResponseTransform = @@function_handle}. It defaults ## to @qcode{'none'}, the identity. ## ## @end deftp ResponseTransform = @(x) x; endproperties ## Readable by the counterpart class, which copies it, and kept out of ## the documented surface. properties (GetAccess = public, SetAccess = protected, Hidden) ## Carried from the fitted model so a fold, which is stored ## compact, can still say how many trees it fitted. NumTrainedTrees = []; RTfun = @(y) y; endproperties ## Set methods for the properties a user may assign. methods (Hidden) function this = set.ResponseTransform (this, val) [this.RTfun, this.ResponseTransform] = parseResponseTransform ... (val, 'CompactRegressionGAM'); endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionGAM} {@var{obj} =} CompactRegressionGAM (@var{Mdl}) ## @deftypefnx {CompactRegressionGAM} {@var{obj} =} CompactRegressionGAM () ## ## Create a @code{CompactRegressionGAM} object. ## ## @var{Mdl} is the @code{RegressionGAM} object to ## compact. The documented way to reach this constructor is the ## @code{compact} method. ## ## Called with no arguments it returns an object with its properties ## empty, which is how a saved model is rebuilt before its values are ## filled in. ## ## @end deftypefn function this = CompactRegressionGAM (Mdl = []) ## Check for appropriate class if (isempty (Mdl)) return; elseif (! strcmpi (class (Mdl), 'RegressionGAM')) error ("CompactRegressionGAM: invalid regression object."); endif ## The levels a predictor read from a table was coded through ## travel with the model, so a compact one still reads a table this.PredictorLevels = Mdl.PredictorLevels; ## Save properties to compact model this.NumPredictors = Mdl.NumPredictors; this.PredictorNames = Mdl.PredictorNames; this.ResponseName = Mdl.ResponseName; this.CategoricalPredictors = Mdl.CategoricalPredictors; this.ExpandedPredictorNames = Mdl.ExpandedPredictorNames; this.ResponseTransform = Mdl.ResponseTransform; this.Intercept = Mdl.Intercept; this.Formula = Mdl.Formula; this.Interactions = Mdl.Interactions; this.IsStandardDeviationFit = Mdl.IsStandardDeviationFit; this.BaseModel = Mdl.BaseModel; this.ModelwInt = Mdl.ModelwInt; this.IntMatrix = Mdl.IntMatrix; this.RTfun = Mdl.RTfun; this.FitMethod = Mdl.FitMethod; this.TreeModel = Mdl.TreeModel; this.BinEdges = Mdl.BinEdges; this.PairDetectionBinEdges = Mdl.PairDetectionBinEdges; this.NumTrainedTrees = Mdl.NumTrainedTrees; endfunction ## Custom display function disp (this) fprintf ("\n CompactRegressionGAM\n\n"); ## Print selected properties fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); fprintf ("%+25s: %d\n", 'NumPredictors', this.NumPredictors); fprintf ("%+25s: '%s'\n", 'ResponseTransform', this.ResponseTransform); fprintf ("%+25s: %g\n", 'Intercept', this.Intercept); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {CompactRegressionGAM} {@var{yFit} =} predict (@var{obj}, @var{Xfit}) ## @deftypefnx {CompactRegressionGAM} {@var{yFit} =} predict (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {CompactRegressionGAM} {[@var{yFit}, @var{ySD}, @var{yInt}] =} predict (@dots{}) ## ## Predict new data points using generalized additive model regression ## object. ## ## @code{@var{yFit} = predict (@var{obj}, @var{Xfit}} returns a vector of ## predicted responses, @var{yFit}, for the predictor data in matrix ## @var{Xfit} based on the Generalized Additive Model in @var{obj}. ## @var{Xfit} must have the same number of features/variables as the ## training data in @var{obj}. Every row is predicted. Under boosted ## trees a missing value adds nothing from a main effect, and an ## interaction term takes the value its trees give a row missing that ## predictor, so a row missing every predictor predicts the intercept; ## under splines a row holding a missing value is predicted as ## @code{NaN}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{CompactRegressionGAM} class object. ## @end itemize ## ## @code{[@var{yFit}, @var{ySD}, @var{yInt}] = predict (@var{obj}, ## @var{Xfit}} ## also returns the standard deviations, @var{ySD}, and prediction ## intervals, ## @var{yInt}, of the response variable @var{yFit}, evaluated at each ## observation in the predictor data @var{Xfit}. ## ## @code{@var{yFit} = predict (@dots{}, @var{Name}, @var{Value})} returns ## the ## aforementioned results with additional properties specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.28 0.7 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'alpha'} @tab significance level of the prediction ## intervals @var{yInt}, specified as scalar in range @qcode{[0,1]}. The ## default value is 0.05, which corresponds to 95% prediction intervals. ## ## @item @qcode{'includeinteractions'} @tab a boolean flag to include ## interactions to predict new values based on @var{Xfit}. By default, ## @qcode{'includeinteractions'} is @qcode{true} when the GAM model in ## @var{obj} ## contains a @qcode{obj.Formula} or @qcode{obj.Interactions} fields. ## Otherwise, is set to @qcode{false}. If set to @qcode{true} when no ## interactions are present in the trained model, it will result to an ## error. If set to ## @qcode{false} when using a model that includes interactions, the ## predictions ## will be made on the basic model without any interaction terms. This way ## you can make predictions from the same GAM model without having to ## retrain it. ## @end multitable ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: one ## the model was not fitted on is passed over, one it needs and cannot ## find is named, and a value holding a level is coded as that level ## was coded at fitting. ## @seealso{fitrgam, RegressionGAM} ## @end deftypefn function yFit = predict (this, Xfit, varargin) ## Check for sufficient input arguments if (nargin < 2) error ("CompactRegressionGAM.predict: too few arguments."); endif ## A table is read by the names the model was fitted on Xfit = tableColumns (this, 'CompactRegressionGAM.predict', Xfit); ## Check for valid XC if (isempty (Xfit)) error ("CompactRegressionGAM.predict: Xfit is empty."); elseif (this.NumPredictors != columns (Xfit)) error (strcat ("CompactRegressionGAM.predict: Xfit must have the", ... " same number of features (columns) as in the GAM model.")); endif ## Default values for Name-Value Pairs alpha = 0.05; hasInt = ! isempty (this.IntMatrix); if (strcmp (this.FitMethod, 'boostedtrees') && ! isempty (this.TreeModel)) hasInt = ! isempty (this.TreeModel.Pairs); endif if (! hasInt) incInt = false; else incInt = true; endif ## Parse optional paired arguments; interactions are included when the ## model has them if (mod (numel (varargin), 2) != 0) error (strcat ("CompactRegressionGAM.predict: optional arguments", ... " must be given in Name-Value pairs.")); endif optNames = {'IncludeInteractions', 'Alpha'}; dfValues = {incInt, alpha}; [incInt, alpha, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! islogical (incInt) || (incInt != 0 && incInt != 1)) error (strcat ("CompactRegressionGAM.predict: includeinteractions", ... " must be a logical value.")); endif if (incInt && ! hasInt) error (strcat ("CompactRegressionGAM.predict: trained model does", ... " not include any interactions.")); endif if (! (isnumeric (alpha) && isscalar (alpha) && alpha > 0 && alpha < 1)) error (strcat ("CompactRegressionGAM.predict: alpha must be a", ... " scalar value between 0 and 1.")); endif if (! isempty (args)) error (strcat ("CompactRegressionGAM.predict: invalid optional", ... " paired argument.")); endif ## Choose whether interactions must be included ## The boosted-tree engine keeps its fit as step functions over bins, so ## a term is a lookup rather than a spline evaluation. if (strcmp (this.FitMethod, 'boostedtrees') && ! isempty (this.TreeModel)) ## Excluding the interactions means excluding the constant they ## handed the intercept as well. [Xb, E] = gamCatCode (this.TreeModel, Xfit, this.BinEdges); interc = this.Intercept; if (! incInt && isfield (this.TreeModel, 'PairIntercept')) interc = interc - this.TreeModel.PairIntercept; endif if (! incInt || isempty (this.TreeModel.Pairs)) yFit = gamboostpredict (E, ... this.TreeModel.ShapeValues, Xb, ... interc); else [PE, PM] = gamPairEdges (this.TreeModel, this.PairDetectionBinEdges); yFit = gamboostpredict (E, ... this.TreeModel.ShapeValues, Xb, ... interc, 0, PE, ... this.TreeModel.PairValues, ... this.TreeModel.Pairs, PM); endif yFit = this.RTfun (yFit); return; endif if (incInt) ## Which construction path the model took: an interaction ## list appends its terms to the predictors, a formula ## names every term the model has and replaces them. if (isempty (this.Formula)) ## Append interaction terms to the predictor matrix for i = 1:rows (this.IntMatrix) tindex = logical (this.IntMatrix(i,:)); Xterms = Xfit(:,tindex); Xinter = ones (rows (Xfit), 1); for c = 1:sum (tindex) Xinter = Xinter .* Xterms(:,c); endfor ## Append interaction terms Xfit = [Xfit, Xinter]; endfor else ## Add selected predictors and interaction terms XN = []; for i = 1:rows (this.IntMatrix) tindex = logical (this.IntMatrix(i,:)); Xterms = Xfit(:,tindex); Xinter = ones (rows (Xfit), 1); for c = 1:sum (tindex) Xinter = Xinter .* Xterms(:,c); endfor ## Append selected predictors and interaction terms XN = [XN, Xinter]; endfor Xfit = XN; endif ## Get parameters and intercept vectors from model with interactions params = this.ModelwInt.Parameters; Interc = this.ModelwInt.Intercept; else ## Get parameters and intercept vectors from base model params = this.BaseModel.Parameters; Interc = this.BaseModel.Intercept; endif ## Predict values from testing data yFit = predict_val (params, Xfit, Interc); yFit = this.RTfun (yFit); endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionGAM} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactRegressionGAM} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactRegressionGAM} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {CompactRegressionGAM} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Regression loss of a generalized additive model. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} returns the weighted ## mean squared error of the model on the rows of @var{X} against the true ## response @var{Y}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{L} = loss (@dots{}, @var{name}, @var{value})} accepts the ## following name-value pairs: ## ## @itemize ## @item ## @qcode{"LossFun"} selects the loss, either @qcode{"mse"}, the default, ## or a function handle taking the true response, the predicted response ## and the weights, and returning a numeric scalar. ## ## @item ## @qcode{"Weights"} holds one weight per row of @var{X}, normalised to ## sum to one before it is applied. ## @end itemize ## ## @seealso{CompactRegressionGAM, RegressionGAM, fitrgam, ## CompactRegressionGAM.predict} ## @end deftypefn function L = loss (this, X, Y, varargin) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactRegressionGAM.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("CompactRegressionGAM.loss: Name-Value arguments", ... " must be in pairs.")); endif [X, Y] = checkXY_ (this, X, Y, 'loss'); ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights'}; dfValues = {'mse', []}; [LossFun, W, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (is_function_handle (LossFun) || (ischar (LossFun) && isrow (LossFun)))) error (strcat ("CompactRegressionGAM.loss: 'LossFun' must be a", ... " character vector or a function handle.")); endif if (ischar (LossFun) && ! strcmpi (LossFun, 'mse')) error ("CompactRegressionGAM.loss: unsupported 'LossFun' value."); endif errmsg = weightsClass (W); if (! isempty (errmsg)) error ("CompactRegressionGAM.loss: %s", errmsg); endif if (! isempty (W) && ! (isnumeric (W) && isvector (W))) error (strcat ("CompactRegressionGAM.loss: 'Weights' must be a", ... " numeric vector.")); endif if (! isempty (W) && numel (W) != rows (X)) error (strcat ("CompactRegressionGAM.loss: size of 'Weights' must", ... " equal the number of rows in X.")); endif if (! isempty (args)) error ("CompactRegressionGAM.loss: invalid optional paired argument."); endif if (isempty (W)) W = ones (rows (X), 1); endif ## Weights are normalized to sum to one, as MATLAB does, so a loss is ## a weighted average rather than a weighted sum. W = double (W(:)); W = W / sum (W); yFit = predict (this, X); Y = Y(:); if (is_function_handle (LossFun)) L = LossFun (Y, yFit, W); if (! (isnumeric (L) && isscalar (L))) error (strcat ("CompactRegressionGAM.loss: 'LossFun' must", ... " return a numeric scalar.")); endif else L = sum (W .* (Y - yFit) .^ 2); endif endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionGAM} {} savemodel (@var{obj}, @var{filename}) ## ## Save a CompactRegressionGAM object. ## ## @code{savemodel (@var{obj}, @var{filename})} saves each property of a ## CompactRegressionGAM object into an Octave binary file, the name of ## which is specified in @var{filename}, along with an extra variable which ## defines the type of object these variables constitute. Use ## @code{loadmodel} in order to load the object back into Octave. ## ## @seealso{loadmodel, fitrgam, RegressionGAM} ## @end deftypefn function savemodel (this, fname) if (nargin < 2) error ("CompactRegressionGAM.savemodel: too few input arguments."); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error (strcat ("CompactRegressionGAM.savemodel: FNAME must be a", ... " character vector.")); endif ## Generate variable for class name classdef_name = 'CompactRegressionGAM'; ## Create variables from model properties NumPredictors = this.NumPredictors; PredictorNames = this.PredictorNames; ResponseName = this.ResponseName; CategoricalPredictors = this.CategoricalPredictors; ExpandedPredictorNames = this.ExpandedPredictorNames; ResponseTransform = this.ResponseTransform; Intercept = this.Intercept; Formula = this.Formula; Interactions = this.Interactions; IsStandardDeviationFit = this.IsStandardDeviationFit; BaseModel = this.BaseModel; ModelwInt = this.ModelwInt; IntMatrix = this.IntMatrix; RTfun = this.RTfun; FitMethod = this.FitMethod; TreeModel = this.TreeModel; BinEdges = this.BinEdges; PairDetectionBinEdges = this.PairDetectionBinEdges; ## Save classdef name and all model properties as individual variables save ('-binary', fname, 'classdef_name', 'NumPredictors', ... 'PredictorNames', 'ResponseName', 'CategoricalPredictors', ... 'ExpandedPredictorNames', 'ResponseTransform', 'Intercept', ... 'Formula', 'Interactions', 'IsStandardDeviationFit', ... 'BaseModel', 'ModelwInt', 'IntMatrix', 'RTfun', 'FitMethod', ... 'TreeModel', 'BinEdges', 'PairDetectionBinEdges'); endfunction endmethods methods(Access = private) ## Shared validation for the assessment methods, so each reports under ## its own name. function [X, Y] = checkXY_ (this, X, Y, caller) if (isempty (X)) error ("CompactRegressionGAM.%s: X is empty.", caller); elseif (this.NumPredictors != columns (X)) error (strcat ("CompactRegressionGAM.%s: X must have the same", ... " number of predictors as the trained model."), caller); endif if (isempty (Y)) error ("CompactRegressionGAM.%s: Y is empty.", caller); elseif (rows (X) != rows (Y)) error (strcat ("CompactRegressionGAM.%s: Y must have the same", ... " number of rows as X."), caller); endif endfunction endmethods methods(Static, Hidden) function mdl = load_model (filename, data) ## Create a CompactRegressionGAM object mdl = CompactRegressionGAM (); ## Get fieldnames from DATA (including private properties) names = fieldnames (data); ## The set methods for these read other properties, and one of them ## rebuilds Coeffs, so they are assigned once everything else is in ## place rather than in the order the file happens to list them. late = ismember (names, {'Cost', 'Prior', 'ScoreTransform', ... 'ResponseTransform'}); names = [names(! late); names(late)]; ## Copy data into object for i = 1:numel (names) try mdl.(names{i}) = data.(names{i}); catch error ("CompactRegressionGAM.load_model: invalid model in '%s'.", ... filename); end_try_catch endfor endfunction endmethods endclassdef ## Helper function function ypred = predict_val (params, X, intercept) ## The shared prediction engine evaluates every additive term and adds the ## intercept. ypred = gampredict (params, X, intercept); endfunction %!demo %! ## Take the compact version of a fitted model and predict with it %! %! load fisheriris %! X = meas(:,1:3); %! Y = meas(:,4); %! %! mdl = fitrgam (X, Y) %! cmdl = compact (mdl) %!test # a row missing a predictor is kept, and its spline term has no value %! x = linspace (0, 1, 30)'; %! X = [x, cos(4 * x)]; %! y = sin (3 * x) + X(:,2); %! Mdl = compact (RegressionGAM (X, y, 'FitMethod', 'splines')); %! yFit = predict (Mdl, [0.5, 0.2; NaN, 0.2; 0.5, 0.2]); %! assert_equal (size (yFit), [3, 1]); %! assert_equal (isnan (yFit)', [false, true, false]); %!test # a row missing every predictor predicts the intercept %! k = (1:200)'; %! X = [sin(k), cos(2 * k), mod(k, 5)]; %! y = 2 * sin (k) + X(:,2) .^ 2 + 0.3 * X(:,3); %! Q = [0.5, 0.2, 1; NaN, 0.2, 1; 0.5, NaN, 1; NaN, NaN, NaN; 0.1, 0.2, 1; ... %! NaN, 0.7, 3; NaN, NaN, 1]; %! Mdl = compact (RegressionGAM (X, y)); %! yFit = predict (Mdl, Q); %! assert_equal (size (yFit), [7, 1]); %! assert_equal (yFit(4), Mdl.Intercept); ## Test input validation for constructor %!error ... %! CompactRegressionGAM (1) ## The compact model carries what MATLAB's compact model reports. %!test %! load fisheriris %! Mdl = fitrgam (meas(:,1:3), meas(:,4)); %! CMdl = compact (Mdl); %! assert_equal (class (CMdl), 'CompactRegressionGAM'); %! assert_equal (CMdl.Intercept, Mdl.Intercept); %! assert_equal (CMdl.CategoricalPredictors, Mdl.CategoricalPredictors); %! assert_equal (CMdl.ExpandedPredictorNames, Mdl.ExpandedPredictorNames); %! assert_equal (CMdl.IsStandardDeviationFit, false); %! assert_equal (isprop (CMdl, 'X'), false); ## predict and loss agree with the model it was compacted from. %!test %! load fisheriris %! X = meas(:,1:3); %! Y = meas(:,4); %! Mdl = fitrgam (X, Y, 'Interactions', 'all'); %! CMdl = compact (Mdl); %! assert_equal (predict (CMdl, X), predict (Mdl, X)); %! assert_equal (loss (CMdl, X, Y), loss (Mdl, X, Y)); ## A saved and reloaded compact model carries every property it holds. %!test %! load fisheriris %! X = meas(:,1:3); %! CMdl = compact (fitrgam (X, meas(:,4))); %! fname = tempname (); %! savemodel (CMdl, fname); %! CMdl2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (CMdl2), 'CompactRegressionGAM'); %! assert_equal (CMdl2.Intercept, CMdl.Intercept); %! assert_equal (predict (CMdl2, X), predict (CMdl, X)); ## An assigned ResponseTransform reaches the predicted response. %!test %! load fisheriris %! X = meas(:,1:3); %! CMdl = compact (fitrgam (X, meas(:,4))); %! y0 = predict (CMdl, X); %! CMdl.ResponseTransform = 'log'; %! assert_equal (predict (CMdl, X), log (y0), 1e-12); ## Test input validation %!shared xc, yc, CMr %! load fisheriris %! xc = meas(:,1:3); %! yc = meas(:,4); %! CMr = compact (fitrgam (xc, yc)); %!error ... %! predict (CMr) %!error ... %! predict (CMr, []) %!error ... %! predict (CMr, xc, 'Bogus', 1) %!error ... %! loss (CMr, xc) %!error ... %! loss (CMr, xc, yc, 'LossFun', 'mad') %!error ... %! loss (CMr, xc, yc, 'Bogus', 1) %!error ... %! savemodel (CompactRegressionGAM ()) %!error ... %! savemodel (CompactRegressionGAM (), 1) ## A fitted model survives savemodel and loadmodel: the properties come ## back as they were and it predicts the same. %!test %! load fisheriris %! X = meas(:,2:4); %! Y = meas(:,1); %! Mdl = compact (fitrgam (X, Y, 'FitMethod', 'splines')); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2), 'CompactRegressionGAM'); %! assert_equal (M2.PredictorNames, Mdl.PredictorNames); %! assert_equal (class (M2.ResponseTransform), class (Mdl.ResponseTransform)); %! assert_equal (M2.BaseModel.Parameters(1).coefs, ... %! Mdl.BaseModel.Parameters(1).coefs); %! assert_equal (predict (M2, X(1:5,:)), predict (Mdl, X(1:5,:)), 1e-12); ## The same round trip under the boosted-tree engine. %!test %! load fisheriris %! X = meas(:,2:4); %! Y = meas(:,1); %! Mdl = compact (fitrgam (X, Y, 'FitMethod', 'boostedtrees')); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.FitMethod, 'boostedtrees'); %! assert_equal (M2.TreeModel.ShapeValues, Mdl.TreeModel.ShapeValues); %! assert_equal (predict (M2, X(1:5,:)), predict (Mdl, X(1:5,:)), 1e-12); ## A compacted tree-fitted model predicts as the full model does. %!test %! load fisheriris %! X = meas(:,2:4); %! CMdl = compact (fitrgam (X, meas(:,1), 'FitMethod', 'boostedtrees')); %! assert_equal (CMdl.FitMethod, 'boostedtrees'); %! assert_equal (numel (CMdl.BinEdges), 3); %! assert_equal (numel (predict (CMdl, X)), rows (X)); ## Every documented response transform reaches the response that is reported. %!test %! load fisheriris %! Mdl = compact (fitrgam (meas(:,2:4), meas(:,1))); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! T = {'identity', @(x) x; 'exp', @(x) exp (x); 'log', @(x) log (x)}; %! for i = 1:rows (T) %! Mdl.ResponseTransform = T{i,1}; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, T{i,2}(raw), 1e-12); %! endfor ## A function handle is taken as given and applied to the response. %!test %! load fisheriris %! Mdl = compact (fitrgam (meas(:,2:4), meas(:,1))); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! Mdl.ResponseTransform = @(x) x .^ 2; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, raw .^ 2, 1e-12); ## A table at prediction %!test # the levels a predictor was coded through travel with the model %! load fisheriris %! T = table (meas(:,2), meas(:,3), 'VariableNames', {'SW', 'PL'}); %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! T.SL = meas(:,1); %! CMdl = compact (fitrgam (T, 'SL')); %! assert_equal (numel (CMdl.PredictorLevels), 3); %! assert_equal (CMdl.PredictorLevels{3}, {'narrow', 'wide'}); %!test # predict takes a table, read by name and not by position %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'SL'}); %! CMdl = compact (fitrgam (T, 'SL')); %! a = predict (CMdl, T); %! assert_equal (numel (a), 150); %! assert_equal (predict (CMdl, T(:, [3, 2, 1])), a); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,2:3); %! y = meas(:,1); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SW', 'PL'}); %! T.SL = y; %! Mdl = compact (fitrgam (T, 'SL')); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'SL'), a); %! assert_equal (loss (Mdl, T), a); ## Observation weights of class single or double %!error %! X = [1, 2; 3, 4; 5, 6; 7, 8]; %! y = (1:4)'; %! loss (compact (fitrgam (X, y)), X, y, 'Weights', int8 ([1; 1; 1; 1])) statistics-release-2.1.2/inst/Machine_Learning/CompactRegressionGP.m000066400000000000000000000714201527311104300255210ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef CompactRegressionGP < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} CompactRegressionGP ## ## Compact Gaussian process regression ## ## A @code{CompactRegressionGP} object holds a Gaussian process regression ## model without its training data, keeping what is needed to predict and ## dropping the rest. ## ## Create a @code{CompactRegressionGP} object by using the @code{compact} ## method of a @code{RegressionGP} object. ## ## A compact model keeps the active set it predicts from, the prediction ## weights, the covariance function and its parameters, the explicit basis ## and its coefficients, the noise standard deviation and the standardizing ## location and scale. It drops the response, the observation weights, the ## rows used, the count of observations and the maximized log likelihood, so ## it can predict but cannot be cross validated, refitted, or asked for its ## resubstitution loss or its post-fit statistics. ## ## The standard deviation and the prediction intervals remain available, ## because the active set of an exactly fitted model is the whole of the ## training predictors and the factorization can be rebuilt from it. ## ## @seealso{RegressionGP, fitrgp} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CompactRegressionGP} {property} PredictorNames ## ## Predictor variable names ## ## A cell array of character vectors. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionGP} {property} ExpandedPredictorNames ## ## Expanded predictor variable names ## ## A cell array of character vectors. This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionGP} {property} ResponseName ## ## Response variable name ## ## A character vector. This property is read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {CompactRegressionGP} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A vector of positive integers, or empty. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {CompactRegressionGP} {property} FitMethod ## ## Method used to estimate the parameters ## ## @qcode{'Exact'} or @qcode{'None'}. This property is read-only. ## ## @end deftp FitMethod = 'Exact'; ## -*- texinfo -*- ## @deftp {CompactRegressionGP} {property} BasisFunction ## ## Explicit basis of the model ## ## A character vector or a function handle. This property is read-only. ## ## @end deftp BasisFunction = 'Constant'; ## -*- texinfo -*- ## @deftp {CompactRegressionGP} {property} Beta ## ## Estimated coefficients of the explicit basis ## ## A numeric vector, empty when the basis is @qcode{'None'}. This property ## is read-only. ## ## @end deftp Beta = []; ## -*- texinfo -*- ## @deftp {CompactRegressionGP} {property} Sigma ## ## Estimated noise standard deviation ## ## A positive scalar. This property is read-only. ## ## @end deftp Sigma = []; ## -*- texinfo -*- ## @deftp {CompactRegressionGP} {property} KernelFunction ## ## Form of the covariance function ## ## A character vector or a function handle. This property is read-only. ## ## @end deftp KernelFunction = 'SquaredExponential'; ## -*- texinfo -*- ## @deftp {CompactRegressionGP} {property} KernelInformation ## ## Covariance function and its parameters ## ## A structure with fields @qcode{Name}, @qcode{KernelParameters} and ## @qcode{KernelParameterNames}. This property is read-only. ## ## @end deftp KernelInformation = []; ## -*- texinfo -*- ## @deftp {CompactRegressionGP} {property} PredictMethod ## ## Method used to make predictions ## ## @qcode{'Exact'}. This property is read-only. ## ## @end deftp PredictMethod = 'Exact'; ## -*- texinfo -*- ## @deftp {CompactRegressionGP} {property} Alpha ## ## Weights the predictions are made from ## ## A numeric vector with one weight per active set vector. This property ## is read-only. ## ## @end deftp Alpha = []; ## -*- texinfo -*- ## @deftp {CompactRegressionGP} {property} ActiveSetVectors ## ## Subset of the training data used for predictions ## ## An @math{MxP} numeric matrix, standardized where the model standardized ## its predictors. This property is read-only. ## ## @end deftp ActiveSetVectors = []; ## -*- texinfo -*- ## @deftp {CompactRegressionGP} {property} ActiveSetMethod ## ## Method used to select the active set ## ## @qcode{'Random'}. This property is read-only. ## ## @end deftp ActiveSetMethod = 'Random'; ## -*- texinfo -*- ## @deftp {CompactRegressionGP} {property} ActiveSetSize ## ## Size of the active set ## ## A positive integer scalar. This property is read-only. ## ## @end deftp ActiveSetSize = []; ## -*- texinfo -*- ## @deftp {CompactRegressionGP} {property} PredictorLocation ## ## Means the predictors were centred by ## ## A @math{1xP} numeric vector, or empty. This property is read-only. ## ## @end deftp PredictorLocation = []; ## -*- texinfo -*- ## @deftp {CompactRegressionGP} {property} PredictorScale ## ## Standard deviations the predictors were scaled by ## ## A @math{1xP} numeric vector, or empty. This property is read-only. ## ## @end deftp PredictorScale = []; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {CompactRegressionGP} {property} ResponseTransform ## ## Transformation applied to the predicted response ## ## A character vector, or the text of the function handle that was ## supplied. Assigning to it accepts either. ## ## @end deftp ResponseTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ## The dummy coding of the categorical predictors, empty when none. Coding_ = []; ## The prediction for a row missing a predictor. MissingResponse_ = NaN; ## The callable behind ResponseTransform. RTfun = @(y) y; endproperties methods (Access = public) ## -*- texinfo -*- ## @deftypefn {CompactRegressionGP} {@var{yFit} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {CompactRegressionGP} {[@var{yFit}, @var{ySD}, @var{yInt}] =} predict (@var{obj}, @var{XC}) ## @deftypefnx {CompactRegressionGP} {[@dots{}] =} predict (@dots{}, @qcode{'Alpha'}, @var{alpha}) ## ## Predict the response for new data with a compact Gaussian process model. ## ## @code{@var{yFit} = predict (@var{obj}, @var{XC})} returns the predicted ## response of the @qcode{CompactRegressionGP} model @var{obj} at the ## points in @var{XC}, and the further outputs are the standard deviation ## of each predicted response and the prediction intervals, exactly as the ## full model returns them. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @end deftypefn function [yFit, ySD, yInt] = predict (this, XC, varargin) if (nargin < 2) error ("CompactRegressionGP.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'CompactRegressionGP.predict', XC); if (isempty (XC)) error ("CompactRegressionGP.predict: XC is empty."); endif p = columns (this.ActiveSetVectors); if (! isempty (this.Coding_)) p = this.Coding_.NumPredictors; endif if (columns (XC) != p) error (strcat ("CompactRegressionGP.predict: XC must have the same", ... " number of predictors as the trained model.")); endif ## Parse optional paired arguments optNames = {'Alpha'}; dfValues = {0.05}; [CIAlpha, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (isnumeric (CIAlpha) && isscalar (CIAlpha) && ... CIAlpha >= 0 && CIAlpha <= 1)) error (strcat ("CompactRegressionGP.predict: 'Alpha' must be a", ... " scalar between 0 and 1.")); endif if (! isempty (args)) error (strcat ("CompactRegressionGP.predict: invalid optional", ... " paired argument.")); endif if (! isempty (this.Coding_)) XC = dummyCoding (XC, this.Coding_); endif M = struct ('X', this.ActiveSetVectors, 'Alpha', this.Alpha, ... 'KernelFunction', this.KernelFunction, ... 'Theta', this.KernelInformation.KernelParameters, ... 'BasisFunction', this.BasisFunction, 'Beta', this.Beta, ... 'Sigma', this.Sigma, 'Location', this.PredictorLocation, ... 'Scale', this.PredictorScale, 'CIAlpha', CIAlpha); ## A row missing a predictor is predicted as MATLAB R2024a predicts it, ## the weighted lower median of the training response. miss = any (isnan (XC), 2); if (nargout < 2) yFit = gpPredict (XC, M); elseif (nargout < 3) [yFit, ySD] = gpPredict (XC, M); else [yFit, ySD, yInt] = gpPredict (XC, M); yInt = this.RTfun (yInt); endif yFit(miss) = this.MissingResponse_; yFit = this.RTfun (yFit); endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionGP} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactRegressionGP} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactRegressionGP} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {CompactRegressionGP} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Compute the regression loss of a compact Gaussian process model. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} returns the mean ## squared error of the model @var{obj} on the data @var{X} and @var{Y}, ## and accepts the same @qcode{'LossFun'} and @qcode{'Weights'} pairs the ## full model accepts. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @end deftypefn function L = loss (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactRegressionGP.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (! (isnumeric (X) && isreal (X) && ismatrix (X))) error ("CompactRegressionGP.loss: invalid values in X."); endif if (! (isnumeric (Y) && isreal (Y) && isvector (Y))) error ("CompactRegressionGP.loss: invalid values in Y."); endif Y = Y(:); if (rows (X) != rows (Y)) error (strcat ("CompactRegressionGP.loss: number of rows in X and", ... " Y must be equal.")); endif ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights', 'Epsilon'}; dfValues = {'mse', [], 0}; [LossFun, Weights, Epsilon, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (ischar (LossFun) || is_function_handle (LossFun))) error (strcat ("CompactRegressionGP.loss: 'LossFun' must be a", ... " character vector or a function handle.")); endif if (ischar (LossFun) && ... ! any (strcmpi (LossFun, {'mse', 'mae', ... 'epsiloninsensitive'}))) error ("CompactRegressionGP.loss: unsupported 'LossFun' value."); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("CompactRegressionGP.loss: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isvector (Weights) && ... numel (Weights) == rows (X) && all (Weights >= 0))) error (strcat ("CompactRegressionGP.loss: 'Weights' must be a", ... " vector of non-negative values with one element", ... " per observation.")); endif Weights = double (Weights(:)); if (! isempty (args)) error ("CompactRegressionGP.loss: invalid optional paired argument."); endif if (isempty (Weights)) Weights = ones (rows (X), 1); endif yFit = this.predict (X); if (is_function_handle (LossFun)) L = LossFun (Y, yFit); return; endif switch (lower (LossFun)) case 'mse' L = sum (Weights .* (Y - yFit) .^ 2) / sum (Weights); case 'mae' L = sum (Weights .* abs (Y - yFit)) / sum (Weights); case 'epsiloninsensitive' e = max (0, abs (Y - yFit) - Epsilon); L = sum (Weights .* e) / sum (Weights); endswitch endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionGP} {} savemodel (@var{obj}, @var{filename}) ## ## Save a compact Gaussian process model to a file. ## ## @code{savemodel (@var{obj}, @var{filename})} saves the model @var{obj} ## into @var{filename} in a form @code{loadmodel} can read back. ## ## @end deftypefn function savemodel (obj, fname) classdef_name = 'CompactRegressionGP'; PredictorNames = obj.PredictorNames; ExpandedPredictorNames = obj.ExpandedPredictorNames; ResponseName = obj.ResponseName; CategoricalPredictors = obj.CategoricalPredictors; FitMethod = obj.FitMethod; BasisFunction = obj.BasisFunction; Beta = obj.Beta; Sigma = obj.Sigma; KernelFunction = obj.KernelFunction; KernelInformation = obj.KernelInformation; PredictMethod = obj.PredictMethod; Alpha = obj.Alpha; ActiveSetVectors = obj.ActiveSetVectors; ActiveSetMethod = obj.ActiveSetMethod; ActiveSetSize = obj.ActiveSetSize; PredictorLocation = obj.PredictorLocation; PredictorScale = obj.PredictorScale; ResponseTransform = obj.ResponseTransform; Coding_ = obj.Coding_; MissingResponse_ = obj.MissingResponse_; save ('-binary', fname, 'classdef_name', 'PredictorNames', ... 'ExpandedPredictorNames', 'ResponseName', ... 'CategoricalPredictors', 'FitMethod', 'BasisFunction', 'Beta', ... 'Sigma', 'KernelFunction', ... 'KernelInformation', 'PredictMethod', 'Alpha', ... 'ActiveSetVectors', 'ActiveSetMethod', 'ActiveSetSize', ... 'PredictorLocation', 'PredictorScale', 'ResponseTransform', ... 'Coding_', 'MissingResponse_'); endfunction endmethods methods (Access = public, Hidden) function disp (this) printf ("\n CompactRegressionGP\n\n"); printf ("%25s: '%s'\n", 'ResponseName', this.ResponseName); printf ("%25s: %d\n", 'NumPredictors', ... columns (this.ActiveSetVectors)); if (is_function_handle (this.KernelFunction)) printf ("%25s: '%s'\n", 'KernelFunction', ... func2str (this.KernelFunction)); else printf ("%25s: '%s'\n", 'KernelFunction', this.KernelFunction); endif printf ("%25s: '%s'\n", 'PredictMethod', this.PredictMethod); printf ("%25s: %g\n", 'Sigma', this.Sigma); printf ("\n"); endfunction function this = set.ResponseTransform (this, val) [this.RTfun, this.ResponseTransform] = ... parseResponseTransform (val, 'CompactRegressionGP'); endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionGP} {@var{obj} =} CompactRegressionGP (@var{Mdl}) ## ## Create a @code{CompactRegressionGP} object. ## ## @var{Mdl} is the @code{RegressionGP} object to ## compact, and is required: the compact model has no training data to ## build itself from. The documented way to reach this constructor is ## the @code{compact} method. ## ## @end deftypefn function this = CompactRegressionGP (Mdl) if (nargin < 1) error ("CompactRegressionGP: too few input arguments."); endif if (! isa (Mdl, 'RegressionGP')) error (strcat ("CompactRegressionGP: MDL must be a RegressionGP", ... " object.")); endif ## The levels a predictor read from a table was coded through ## travel with the model, so a compact one still reads a table this.PredictorLevels = Mdl.PredictorLevels; this.PredictorNames = Mdl.PredictorNames; this.ExpandedPredictorNames = Mdl.ExpandedPredictorNames; this.ResponseName = Mdl.ResponseName; this.CategoricalPredictors = Mdl.CategoricalPredictors; this.FitMethod = Mdl.FitMethod; this.BasisFunction = Mdl.BasisFunction; this.Beta = Mdl.Beta; this.Sigma = Mdl.Sigma; this.KernelFunction = Mdl.KernelFunction; this.KernelInformation = Mdl.KernelInformation; this.PredictMethod = Mdl.PredictMethod; this.Alpha = Mdl.Alpha; this.ActiveSetVectors = Mdl.ActiveSetVectors; this.ActiveSetMethod = Mdl.ActiveSetMethod; this.ActiveSetSize = Mdl.ActiveSetSize; this.PredictorLocation = Mdl.PredictorLocation; this.PredictorScale = Mdl.PredictorScale; this.ResponseTransform = Mdl.ResponseTransform; this.Coding_ = Mdl.Coding_; this.MissingResponse_ = Mdl.MissingResponse_; endfunction endmethods methods (Static, Hidden) function mdl = load_model (filename, data) ## The compact model is rebuilt field by field: it has no training data ## to construct itself from, so an empty shell is filled directly. mdl = CompactRegressionGP.empty_model (); fields = fieldnames (data); for k = 1:numel (fields) mdl.(fields{k}) = data.(fields{k}); endfor endfunction ## An unfitted shell, which only load_model needs. function mdl = empty_model () Mdl = RegressionGP ([0; 1], [0; 1], 'FitMethod', 'none'); mdl = CompactRegressionGP (Mdl); endfunction endmethods endclassdef %!demo %! ## A compact model predicts what the full model predicts, and carries none %! ## of the training data. %! x = linspace (0, 1, 20)'; %! y = sin (2*pi*x) + 0.05 * cos (9*x); %! Mdl = fitrgp (x, y); %! CMdl = compact (Mdl) %! xq = [0.15; 0.55; 0.85]; %! [yq, ysd] = predict (CMdl, xq) %!test %! ## The compact model carries the fitted surface and nothing that describes %! ## the training data %! x = linspace (0, 1, 15)'; %! y = cos (3*x) + 0.1 * sin (11*x); %! Mdl = RegressionGP (x, y); %! CMdl = compact (Mdl); %! assert_equal (class (CMdl), 'CompactRegressionGP'); %! assert_equal (CMdl.Beta, Mdl.Beta); %! assert_equal (CMdl.Sigma, Mdl.Sigma); %! assert_equal (CMdl.KernelFunction, Mdl.KernelFunction); %! assert_equal (CMdl.KernelInformation, Mdl.KernelInformation); %! assert_equal (CMdl.Alpha, Mdl.Alpha); %! assert_equal (CMdl.ActiveSetVectors, Mdl.ActiveSetVectors); %! assert_equal (CMdl.ResponseName, Mdl.ResponseName); %! assert_equal (CMdl.PredictorNames, Mdl.PredictorNames); %!test %! ## It predicts exactly what the full model predicts, standard deviation %! ## and interval included, because it predicts through the same code %! x = linspace (0, 1, 20)'; %! y = sin (2*pi*x) + 0.1 * cos (7*x); %! Mdl = RegressionGP (x, y); %! CMdl = compact (Mdl); %! xq = [0.05; 0.33; 0.5; 0.77; 0.95]; %! [y1, s1, i1] = predict (Mdl, xq); %! [y2, s2, i2] = predict (CMdl, xq); %! assert_equal (y2, y1, 1e-14); %! assert_equal (s2, s1, 1e-14); %! assert_equal (i2, i1, 1e-14); %!test %! ## The R2024a values, reached through the compact model %! x = linspace (0, 1, 20)'; %! y = sin (2*pi*x) + 0.1 * cos (7*x); %! CMdl = compact (RegressionGP (x, y)); %! xq = [0.05; 0.33; 0.5; 0.77; 0.95]; %! assert_equal (predict (CMdl, xq), ... %! [0.404401855406521; 0.809355242891053; ... %! -0.093708552144171; -0.928420803239529; ... %! -0.217104024154071], 1e-7); %!test %! ## The interval level is settable here as it is on the full model %! x = linspace (0, 1, 15)'; %! y = cos (3*x) + 0.1 * sin (11*x); %! CMdl = compact (RegressionGP (x, y)); %! xq = [0.2; 0.6]; %! [yp, ysd, yint] = predict (CMdl, xq); %! assert_equal (yint(:,2) - yp, norminv (0.975) * ysd, 1e-12); %! [~, ~, yint90] = predict (CMdl, xq, 'Alpha', 0.10); %! assert (all (yint90(:,2) - yint90(:,1) < yint(:,2) - yint(:,1))); %!test %! ## Standardization is carried over, so the compact model transforms new %! ## data the way the full model did %! X = [linspace(0, 10, 20)', linspace(-5, 5, 20)']; %! y = 0.3 * X(:,1) - 0.2 * X(:,2); %! Mdl = RegressionGP (X, y, 'Standardize', true); %! CMdl = compact (Mdl); %! assert_equal (CMdl.PredictorLocation, Mdl.PredictorLocation); %! assert_equal (CMdl.PredictorScale, Mdl.PredictorScale); %! assert_equal (predict (CMdl, X), predict (Mdl, X), 1e-14); %!test %! ## loss on the compact model agrees with loss on the full one %! x = linspace (0, 1, 15)'; %! y = cos (3*x) + 0.1 * sin (11*x); %! Mdl = RegressionGP (x, y); %! CMdl = compact (Mdl); %! assert_equal (loss (CMdl, x, y), loss (Mdl, x, y), 1e-14); %! assert_equal (loss (CMdl, x, y, 'LossFun', 'mae'), ... %! loss (Mdl, x, y, 'LossFun', 'mae'), 1e-14); %! w = linspace (1, 2, 15)'; %! assert_equal (loss (CMdl, x, y, 'Weights', w), ... %! loss (Mdl, x, y, 'Weights', w), 1e-14); %!test %! ## A compact model saved and loaded predicts what it predicted before %! x = linspace (0, 1, 15)'; %! y = cos (3*x) + 0.1 * sin (11*x); %! CMdl = compact (RegressionGP (x, y)); %! fname = tempname (); %! savemodel (CMdl, fname); %! CMdl2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (CMdl2), 'CompactRegressionGP'); %! assert_equal (CMdl2.Beta, CMdl.Beta); %! assert_equal (CMdl2.Sigma, CMdl.Sigma); %! assert_equal (CMdl2.Alpha, CMdl.Alpha); %! assert_equal (CMdl2.ActiveSetVectors, CMdl.ActiveSetVectors); %! assert_equal (predict (CMdl2, x), predict (CMdl, x), 1e-14); %!test %! ## A response transform survives compacting %! x = linspace (0, 1, 12)'; %! y = cos (3*x); %! Mdl = RegressionGP (x, y, 'ResponseTransform', 'exp'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.ResponseTransform, 'exp'); %! assert_equal (predict (CMdl, x), predict (Mdl, x), 1e-14); %!test # A row missing a predictor predicts the lower median of the response %! X = [(1:10)', mod((1:10)', 3)]; %! Mdl = compact (RegressionGP (X, (1:10)')); %! assert_equal (predict (Mdl, [NaN, 1]), 5); ## Test input validation for the constructor %!error CompactRegressionGP () %!error ... %! CompactRegressionGP (5) ## Test input validation for the predict method %!error ... %! predict (compact (RegressionGP (ones (5, 2), ones (5, 1)))) %!error ... %! predict (compact (RegressionGP (ones (5, 2), ones (5, 1))), []) %!error ... %! predict (compact (RegressionGP (ones (5, 2), ones (5, 1))), ones (3, 3)) %!error ... %! predict (compact (RegressionGP (ones (5, 2), ones (5, 1))), ... %! ones (3, 2), 'Alpha', 2) %!error ... %! predict (compact (RegressionGP (ones (5, 2), ones (5, 1))), ... %! ones (3, 2), 'bogus', 1) ## Test input validation for the loss method %!error ... %! loss (compact (RegressionGP (ones (5, 2), ones (5, 1))), ones (3, 2)) %!error ... %! loss (compact (RegressionGP (ones (5, 2), ones (5, 1))), 'a', ones (3, 1)) %!error ... %! loss (compact (RegressionGP (ones (5, 2), ones (5, 1))), ... %! ones (3, 2), ones (2, 1)) %!error ... %! loss (compact (RegressionGP (ones (5, 2), ones (5, 1))), ... %! ones (3, 2), ones (3, 1), 'Bogus', 1) %!error ... %! loss (compact (RegressionGP (ones (5, 2), ones (5, 1))), ones (3, 2), ... %! ones (3, 1), 'LossFun', 'bogus') ## Every documented response transform reaches the response that is reported. %!test %! load fisheriris %! Mdl = compact (fitrgp (meas(:,2:4), meas(:,1))); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! T = {'identity', @(x) x; 'exp', @(x) exp (x); 'log', @(x) log (x)}; %! for i = 1:rows (T) %! Mdl.ResponseTransform = T{i,1}; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, T{i,2}(raw), 1e-12); %! endfor ## A function handle is taken as given and applied to the response. %!test %! load fisheriris %! Mdl = compact (fitrgp (meas(:,2:4), meas(:,1))); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! Mdl.ResponseTransform = @(x) x .^ 2; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, raw .^ 2, 1e-12); %!test # a compact model codes new data as the full model does %! c1 = repmat ([1; 2; 3], 20, 1); %! x2 = sin ((1:60)'); %! X = [c1, x2]; %! y = 5 * (c1 == 2) + 0.5 * x2 + 0.1 * cos ((1:60)'); %! Mdl = fitrgp (X, y, 'CategoricalPredictors', 1); %! CMdl = compact (Mdl); %! Xq = [1, 0; 3, 0.5; 4, 0]; %! assert_equal (predict (CMdl, Xq), predict (Mdl, Xq)); %! assert_equal (CMdl.ExpandedPredictorNames, Mdl.ExpandedPredictorNames); ## A table at prediction %!test # the levels travel with the model, and predict reads a table by name %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'SL'}); %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! Mdl = fitrgp (T, 'SL'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.PredictorLevels, Mdl.PredictorLevels); %! assert_equal (predict (CMdl, T(:, [4, 3, 2, 1])), predict (CMdl, T)); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,2:3); %! y = meas(:,1); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SW', 'PL'}); %! T.SL = y; %! Mdl = compact (fitrgp (T, 'SL')); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'SL'), a); %! assert_equal (loss (Mdl, T), a); ## Observation weights of class single or double %!error %! X = [1, 2; 3, 4; 5, 6; 7, 8]; %! y = (1:4)'; %! loss (compact (fitrgp (X, y)), X, y, 'Weights', int8 ([1; 1; 1; 1])) statistics-release-2.1.2/inst/Machine_Learning/CompactRegressionNeuralNetwork.m000066400000000000000000001015421527311104300300120ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef CompactRegressionNeuralNetwork < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} CompactRegressionNeuralNetwork ## ## Compact neural network regression ## ## A @code{CompactRegressionNeuralNetwork} object holds a neural network ## regression model that has dropped its training data. ## ## Create a @code{CompactRegressionNeuralNetwork} object by using the ## @code{compact} method of a @code{RegressionNeuralNetwork} object. ## ## The compact model keeps what is needed to answer about new data, the layer ## weights and biases, the activations, the standardization and the response ## transform, and drops what only describes the fit: the predictor and ## response data, the observation weights, the rows used, the number of ## observations and the iteration by iteration training history. ## @code{predict} and @code{loss} therefore agree with the full model to the ## last digit, while @code{resubPredict} and @code{resubLoss} do not exist ## here, there being no training data left to resubstitute. ## ## @seealso{RegressionNeuralNetwork, fitrnet} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CompactRegressionNeuralNetwork} {property} NumPredictors ## ## Number of predictors ## ## A positive integer scalar. This property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {CompactRegressionNeuralNetwork} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors. This property is read-only. ## ## @end deftp PredictorNames = []; ## -*- texinfo -*- ## @deftp {CompactRegressionNeuralNetwork} {property} ResponseName ## ## Name of the response variable ## ## A character vector. This property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {CompactRegressionNeuralNetwork} {property} Sigma ## ## Standard deviation of the predictors ## ## A row vector with one entry per predictor, used for standardization. ## Empty when the predictor data were not standardized. This property is ## read-only. ## ## @end deftp Sigma = []; ## -*- texinfo -*- ## @deftp {CompactRegressionNeuralNetwork} {property} Mu ## ## Mean of the predictors ## ## A row vector with one entry per predictor, used for standardization. ## Empty when the predictor data were not standardized. This property is ## read-only. ## ## @end deftp Mu = []; ## -*- texinfo -*- ## @deftp {CompactRegressionNeuralNetwork} {property} LayerSizes ## ## Sizes of the fully connected hidden layers ## ## A row vector of positive integers, one per hidden layer. This property ## is read-only. ## ## @end deftp LayerSizes = []; ## -*- texinfo -*- ## @deftp {CompactRegressionNeuralNetwork} {property} Activations ## ## Activation functions of the hidden layers ## ## A character vector, or a cell array of character vectors with one entry ## per hidden layer. This property is read-only. ## ## @end deftp Activations = []; ## -*- texinfo -*- ## @deftp {CompactRegressionNeuralNetwork} {property} OutputLayerActivation ## ## Activation function of the output layer ## ## A character vector. @qcode{'none'} applies the identity, so a ## prediction is an unrestricted real number. This property is read-only. ## ## @end deftp OutputLayerActivation = []; ## -*- texinfo -*- ## @deftp {CompactRegressionNeuralNetwork} {property} LayerWeights ## ## Weights the network learned ## ## A cell array with one entry per layer, the output layer included. This ## property is read-only. ## ## @end deftp LayerWeights = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionNeuralNetwork} {property} LayerBiases ## ## Biases the network learned ## ## A cell array with one entry per layer, the output layer included. This ## property is read-only. ## ## @end deftp LayerBiases = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionNeuralNetwork} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A numeric vector of column indices, and empty when none is. This ## property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {CompactRegressionNeuralNetwork} {property} ExpandedPredictorNames ## ## Names of the predictors as the model expanded them ## ## A cell array of character vectors. This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; endproperties ## Properties a user may set after the model is built. Each one is ## validated by its set method below. properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {CompactRegressionNeuralNetwork} {property} ResponseTransform ## ## Transformation applied to the predicted response ## ## A function handle, applied by @code{predict} to the network's output. ## It may be set after construction, either to a handle or to the name of ## a supported transformation. ## ## @end deftp ResponseTransform = 'none'; endproperties ## Readable by the counterpart class, which copies it, and kept out of ## the documented surface. properties (GetAccess = public, SetAccess = protected, Hidden) ## The dummy coding of the categorical predictors, empty when none. Coding_ = []; ## The prediction for a row missing a predictor. MissingResponse_ = NaN; RTfun = @(y) y; endproperties ## Set methods for the properties a user may assign. methods (Hidden) function this = set.ResponseTransform (this, val) name = 'CompactRegressionNeuralNetwork'; [this.RTfun, this.ResponseTransform] = parseResponseTransform (val, name); endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionNeuralNetwork} {@var{obj} =} CompactRegressionNeuralNetwork (@var{Mdl}) ## @deftypefnx {CompactRegressionNeuralNetwork} {@var{obj} =} CompactRegressionNeuralNetwork () ## ## Create a @code{CompactRegressionNeuralNetwork} object. ## ## @var{Mdl} is the @code{RegressionNeuralNetwork} object to ## compact. The documented way to reach this constructor is the ## @code{compact} method. ## ## Called with no arguments it returns an object with its properties ## empty, which is how a saved model is rebuilt before its values are ## filled in. ## ## @end deftypefn function this = CompactRegressionNeuralNetwork (Mdl = []) ## Check for appropriate class if (isempty (Mdl)) return; elseif (! strcmpi (class (Mdl), 'RegressionNeuralNetwork')) error (strcat ("CompactRegressionNeuralNetwork: invalid", ... " regression object.")); endif ## The levels a predictor read from a table was coded through ## travel with the model, so a compact one still reads a table this.PredictorLevels = Mdl.PredictorLevels; ## Save properties to compact model. The training data, the observation ## weights, the rows used, the observation count and the training ## history are deliberately left behind: they describe the fit, not the ## model, and keeping them is what "compact" exists to avoid. this.NumPredictors = Mdl.NumPredictors; this.PredictorNames = Mdl.PredictorNames; this.ResponseName = Mdl.ResponseName; this.ResponseTransform = Mdl.ResponseTransform; this.RTfun = Mdl.RTfun; this.Sigma = Mdl.Sigma; this.Mu = Mdl.Mu; this.LayerSizes = Mdl.LayerSizes; this.Activations = Mdl.Activations; this.OutputLayerActivation = Mdl.OutputLayerActivation; this.LayerWeights = Mdl.LayerWeights; this.LayerBiases = Mdl.LayerBiases; this.CategoricalPredictors = Mdl.CategoricalPredictors; this.Coding_ = Mdl.Coding_; this.MissingResponse_ = Mdl.MissingResponse_; this.ExpandedPredictorNames = Mdl.ExpandedPredictorNames; endfunction ## Custom display function disp (this) fprintf ("\n CompactRegressionNeuralNetwork\n\n"); ## Print selected properties fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); fprintf ("%+25s: %d\n", 'NumPredictors', this.NumPredictors); str = repmat ({'%d'}, 1, numel (this.LayerSizes)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, this.LayerSizes); fprintf ("%+25s: %s\n", 'LayerSizes', str); if (iscellstr (this.Activations)) str = repmat ({'''%s'''}, 1, numel (this.Activations)); str = strcat ('{', strjoin (str, ' '), '}'); str = sprintf (str, this.Activations{:}); fprintf ("%+25s: %s\n", 'Activations', str); else # character vector fprintf ("%+25s: '%s'\n", 'Activations', this.Activations); endif fprintf ("%+25s: '%s'\n", 'OutputLayerActivation', ... this.OutputLayerActivation); fprintf ("%+25s: '%s'\n", 'ResponseTransform', this.ResponseTransform); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {CompactRegressionNeuralNetwork} {@var{yFit} =} predict (@var{obj}, @var{XC}) ## ## Predict the response for new data with a compact neural network ## regression model. ## ## @code{@var{yFit} = predict (@var{obj}, @var{XC})} returns a column ## vector holding the predicted response for each row of @var{XC}. It ## agrees with the full model this object was compacted from. ## ## @itemize ## @item ## @var{obj} must be a @qcode{CompactRegressionNeuralNetwork} class object. ## @item ## @var{XC} must be a numeric matrix with the same number of predictors as ## the data the model was trained on. ## @end itemize ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @seealso{CompactRegressionNeuralNetwork, RegressionNeuralNetwork} ## @end deftypefn function yFit = predict (this, XC) ## Check for sufficient input arguments if (nargin < 2) error (strcat ("CompactRegressionNeuralNetwork.predict:", ... " too few input arguments.")); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'CompactRegressionNeuralNetwork.predict', XC); ## Check for valid XC if (isempty (XC)) error ("CompactRegressionNeuralNetwork.predict: XC is empty."); elseif (this.NumPredictors != columns (XC)) error (strcat ("CompactRegressionNeuralNetwork.predict:", ... " XC must have the same number of predictors", ... " as the trained neural network model.")); endif ## Code the categorical predictors and standardize (if necessary) if (! isempty (this.Coding_)) XC = dummyCoding (XC, this.Coding_); endif if (! isempty (this.Mu)) XC = (XC - this.Mu) ./ this.Sigma; endif ## The network's output is the second return value; the first is an ## index of the largest output, constant for a single regression unit. NumThreads = nproc (); [~, yFit] = fcnnpredict (this.LayerWeights, this.LayerBiases, ... this.Activations, this.OutputLayerActivation, ... XC, NumThreads); ## A row missing a predictor, or holding a level the fit did not see, ## is predicted as MATLAB R2024a predicts it, not from a rectified ## missing value. yFit(any (isnan (XC), 2)) = this.MissingResponse_; ## Apply ResponseTransform yFit = this.RTfun (yFit); endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionNeuralNetwork} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactRegressionNeuralNetwork} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactRegressionNeuralNetwork} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {CompactRegressionNeuralNetwork} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Compute the regression loss of a compact neural network model. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} returns the ## weighted mean squared error between the response @var{Y} and the ## response the model predicts for @var{X}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{CompactRegressionNeuralNetwork} class object. ## @item ## @var{X} must be a numeric matrix with the same number of predictors as ## the data the model was trained on. ## @item ## @var{Y} must be a numeric vector with as many rows as @var{X}. ## @end itemize ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{L} = loss (@dots{}, @var{name}, @var{value})} accepts the ## following @qcode{Name-Value} pairs. ## ## @multitable @columnfractions 0.28 0.72 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'LossFun'} @tab @qcode{'mse'}, the default, or a function ## handle called as @code{@var{lossfun} (@var{Y}, @var{yFit}, @var{W})} ## and returning a scalar. ## ## @item @qcode{'Weights'} @tab A numeric vector of observation weights ## with one entry per row of @var{X}. It defaults to a uniform weight. ## The weights are normalized to sum to one before the loss is formed. ## @end multitable ## ## @seealso{CompactRegressionNeuralNetwork, RegressionNeuralNetwork} ## @end deftypefn function L = loss (this, X, Y, varargin) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error (strcat ("CompactRegressionNeuralNetwork.loss:", ... " too few input arguments.")); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("CompactRegressionNeuralNetwork.loss: Name-Value", ... " arguments must be in pairs.")); endif [X, Y] = checkXY_ (this, X, Y, 'loss'); ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights'}; dfValues = {'mse', []}; [LossFun, W, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (is_function_handle (LossFun) || (ischar (LossFun) && isrow (LossFun)))) error (strcat ("CompactRegressionNeuralNetwork.loss: 'LossFun'", ... " must be a character vector or a function handle.")); endif if (ischar (LossFun) && ! strcmpi (LossFun, 'mse')) error (strcat ("CompactRegressionNeuralNetwork.loss: unsupported", ... " 'LossFun' value.")); endif errmsg = weightsClass (W); if (! isempty (errmsg)) error ("CompactRegressionNeuralNetwork.loss: %s", errmsg); endif if (! isempty (W) && ! (isnumeric (W) && isvector (W))) error (strcat ("CompactRegressionNeuralNetwork.loss: 'Weights'", ... " must be a numeric vector.")); endif if (! isempty (W) && numel (W) != rows (X)) error (strcat ("CompactRegressionNeuralNetwork.loss: size of", ... " 'Weights' must equal the number of rows in X.")); endif if (! isempty (args)) error (strcat ("CompactRegressionNeuralNetwork.loss: invalid", ... " optional paired argument.")); endif if (isempty (W)) W = ones (rows (X), 1); endif ## Weights are normalized to sum to one, as MATLAB does, so a loss is ## a weighted average rather than a weighted sum. W = double (W(:)); W = W / sum (W); yFit = predict (this, X); Y = Y(:); if (is_function_handle (LossFun)) L = LossFun (Y, yFit, W); if (! (isnumeric (L) && isscalar (L))) error (strcat ("CompactRegressionNeuralNetwork.loss: 'LossFun'", ... " must return a numeric scalar.")); endif else L = sum (W .* (Y - yFit) .^ 2); endif endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionNeuralNetwork} {} savemodel (@var{obj}, @var{filename}) ## ## Save a compact neural network regression model to a file. ## ## @code{savemodel (@var{obj}, @var{filename})} saves every property of ## the @qcode{CompactRegressionNeuralNetwork} object @var{obj} into ## @var{filename} in binary format, so that it can be read back with ## @code{loadmodel}. ## ## @seealso{loadmodel, CompactRegressionNeuralNetwork} ## @end deftypefn function savemodel (this, fname) if (nargin < 2) error (strcat ("CompactRegressionNeuralNetwork.savemodel:", ... " too few input arguments.")); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error (strcat ("CompactRegressionNeuralNetwork.savemodel: FNAME", ... " must be a character vector.")); endif ## Generate variable for class name classdef_name = 'CompactRegressionNeuralNetwork'; ## Create variables from model properties NumPredictors = this.NumPredictors; PredictorNames = this.PredictorNames; ResponseName = this.ResponseName; ResponseTransform = this.ResponseTransform; Sigma = this.Sigma; Mu = this.Mu; LayerSizes = this.LayerSizes; Activations = this.Activations; OutputLayerActivation = this.OutputLayerActivation; LayerWeights = this.LayerWeights; LayerBiases = this.LayerBiases; CategoricalPredictors = this.CategoricalPredictors; Coding_ = this.Coding_; MissingResponse_ = this.MissingResponse_; ExpandedPredictorNames = this.ExpandedPredictorNames; RTfun = this.RTfun; ## Save classdef name and all model properties as individual variables save ('-binary', fname, 'classdef_name', 'NumPredictors', ... 'PredictorNames', 'ResponseName', 'ResponseTransform', ... 'Sigma', 'Mu', 'LayerSizes', ... 'Activations', 'OutputLayerActivation', ... ... 'LayerWeights', 'LayerBiases', ... 'CategoricalPredictors', 'Coding_', ... 'MissingResponse_', 'ExpandedPredictorNames', ... 'RTfun'); endfunction endmethods methods (Access = private) ## Shared validation for the assessment methods, so each reports under ## its own name. function [X, Y] = checkXY_ (this, X, Y, caller) if (isempty (X)) error ("CompactRegressionNeuralNetwork.%s: X is empty.", caller); elseif (this.NumPredictors != columns (X)) error (strcat ("CompactRegressionNeuralNetwork.%s: X must have the", ... " same number of predictors as the trained model."), ... caller); endif if (isempty (Y)) error ("CompactRegressionNeuralNetwork.%s: Y is empty.", caller); elseif (! (isnumeric (Y) && isreal (Y))) error (strcat ("CompactRegressionNeuralNetwork.%s: Y must be a", ... " real numeric vector."), caller); elseif (rows (X) != numel (Y)) error (strcat ("CompactRegressionNeuralNetwork.%s: Y must have the", ... " same number of rows as X."), caller); endif endfunction endmethods methods(Static, Hidden) function mdl = load_model (filename, data) ## Create a CompactRegressionNeuralNetwork object mdl = CompactRegressionNeuralNetwork (); ## Get fieldnames from DATA (including private properties) names = fieldnames (data); ## The set methods for these read other properties, and one of them ## rebuilds Coeffs, so they are assigned once everything else is in ## place rather than in the order the file happens to list them. late = ismember (names, {'Cost', 'Prior', 'ScoreTransform', ... 'ResponseTransform'}); names = [names(! late); names(late)]; ## Copy data into object for i = 1:numel (names) ## Check fieldnames in DATA match the class properties try mdl.(names{i}) = data.(names{i}); catch error (strcat ("CompactRegressionNeuralNetwork.load_model:", ... " invalid model in '%s'."), filename) end_try_catch endfor endfunction endmethods endclassdef ## The compact model keeps what answers about new data and drops the fit. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = [randn(40, 2); randn(40, 2) + 3]; %! Y = X(:,1) - 2 * X(:,2); %! Mdl = fitrnet (X, Y, 'IterationLimit', 100); %! CMdl = compact (Mdl); %! assert_equal (class (CMdl), 'CompactRegressionNeuralNetwork'); %! kept = {'NumPredictors', 'PredictorNames', 'ResponseName', ... %! 'ResponseTransform', 'Sigma', 'Mu', 'LayerSizes', ... %! 'Activations', 'OutputLayerActivation', 'LayerWeights', ... %! 'LayerBiases', 'CategoricalPredictors', 'ExpandedPredictorNames'}; %! assert_equal (all (ismember (kept, properties (CMdl))), true); %! dropped = {'X', 'Y', 'W', 'NumObservations', 'RowsUsed', 'TrainingHistory'}; %! assert_equal (any (ismember (dropped, properties (CMdl))), false); ## A compact model predicts exactly what the model it came from predicts. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = [randn(50, 2); randn(50, 2) + 2]; %! Y = 3 * X(:,1) + X(:,2); %! Mdl = fitrnet (X, Y, 'LayerSizes', [8, 6], 'IterationLimit', 200); %! CMdl = compact (Mdl); %! assert_equal (predict (CMdl, X), predict (Mdl, X)); %! assert_equal (loss (CMdl, X, Y), loss (Mdl, X, Y)); %! assert_equal (loss (CMdl, X, Y), resubLoss (Mdl), 1e-12); %! assert_equal (CMdl.LayerWeights, Mdl.LayerWeights); %! assert_equal (CMdl.LayerBiases, Mdl.LayerBiases); ## Standardization travels with the compact model, so it predicts on the ## scale the network was trained on. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = [randn(60, 1), randn(60, 1) * 1000]; %! Y = X(:,1) + X(:,2) / 1000; %! Mdl = fitrnet (X, Y, 'Standardize', true, 'IterationLimit', 200); %! CMdl = compact (Mdl); %! assert_equal (CMdl.Mu, Mdl.Mu); %! assert_equal (CMdl.Sigma, Mdl.Sigma); %! assert_equal (predict (CMdl, X), predict (Mdl, X)); ## loss takes the same options as the full model's. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = linspace (0, 1, 30)'; %! Y = 3 * X + 1; %! CMdl = compact (fitrnet (X, Y, 'IterationLimit', 100)); %! yFit = predict (CMdl, X); %! assert_equal (loss (CMdl, X, Y), mean ((Y - yFit) .^ 2), 1e-12); %! assert_equal (loss (CMdl, X, Y, 'LossFun', 'mse'), loss (CMdl, X, Y), 1e-12); %! w = rand (30, 1) + 0.1; %! assert_equal (loss (CMdl, X, Y, 'Weights', w), ... %! loss (CMdl, X, Y, 'Weights', 5 * w), 1e-12); %! f = @(y, yf, ww) sum (ww .* abs (y - yf)); %! assert_equal (loss (CMdl, X, Y, 'LossFun', f), mean (abs (Y - yFit)), 1e-12); ## ResponseTransform travels, and can be replaced on the compact model. %!test %! rand ('seed', 42); %! X = linspace (0, 1, 20)'; %! Mdl = fitrnet (X, 2 * X + 1, 'ResponseTransform', 'exp', ... %! 'IterationLimit', 50); %! CMdl = compact (Mdl); %! assert_equal (predict (CMdl, X), predict (Mdl, X)); %! CMdl.ResponseTransform = 'none'; %! assert_equal (predict (CMdl, X), log (predict (Mdl, X)), 1e-12); ## A saved compact model comes back carrying its own numbers. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = linspace (0, 1, 30)'; %! Y = 4 * X - 1; %! CMdl = compact (fitrnet (X, Y, 'LayerSizes', [6, 4], ... %! 'IterationLimit', 60)); %! fname = tempname (); %! savemodel (CMdl, fname); %! C2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (C2), 'CompactRegressionNeuralNetwork'); %! assert_equal (C2.LayerWeights, CMdl.LayerWeights); %! assert_equal (C2.LayerBiases, CMdl.LayerBiases); %! assert_equal (C2.NumPredictors, CMdl.NumPredictors); %! assert_equal (C2.LayerSizes, CMdl.LayerSizes); %! assert_equal (predict (C2, X), predict (CMdl, X)); %!test # A row missing a predictor predicts the lower median of the response %! X = [(1:10)', mod((1:10)', 3)]; %! Mdl = compact (RegressionNeuralNetwork (X, (1:10)')); %! assert_equal (predict (Mdl, [NaN, 1]), 5); ## Test input validation for the constructor %!error ... %! CompactRegressionNeuralNetwork (1) %!error ... %! CompactRegressionNeuralNetwork (fitcnet (ones (4, 2), [1; 1; 2; 2])) ## Test input validation for predict and loss %!shared CRNN %! rand ('seed', 42); %! CRNN = compact (fitrnet ([1; 2; 3; 4], [2; 4; 6; 8], 'IterationLimit', 10)); %!error ... %! predict (CRNN) %!error ... %! predict (CRNN, []) %!error ... %! predict (CRNN, ones (2, 3)) %!error ... %! loss (CRNN) %!error ... %! loss (CRNN, [1; 2], [2; 4], 'Weights') %!error ... %! loss (CRNN, [], [2; 4]) %!error ... %! loss (CRNN, ones (2, 3), [2; 4]) %!error ... %! loss (CRNN, [1; 2], []) %!error ... %! loss (CRNN, [1; 2], {'a'; 'b'}) %!error ... %! loss (CRNN, [1; 2], [2; 4; 6]) %!error ... %! loss (CRNN, [1; 2], [2; 4], 'LossFun', 5) %!error ... %! loss (CRNN, [1; 2], [2; 4], 'LossFun', 'mae') %!error ... %! loss (CRNN, [1; 2], [2; 4], 'LossFun', @(y, yf, w) [1, 2]) %!error ... %! loss (CRNN, [1; 2], [2; 4], 'Weights', {'a'}) %!error ... %! loss (CRNN, [1; 2], [2; 4], 'Weights', ones (2, 2)) %!error ... %! loss (CRNN, [1; 2], [2; 4], 'Weights', [1; 2; 3]) %!error ... %! loss (CRNN, [1; 2], [2; 4], 'Nope', 1) ## Test input validation for savemodel %!error ... %! savemodel (CRNN) %!error ... %! savemodel (CRNN, 5) %!error ... %! CRNN.ResponseTransform = 'nope'; %!error ... %! CRNN.ResponseTransform = @(y) [y; y]; ## A fitted model survives savemodel and loadmodel: the properties come ## back as they were and it predicts the same. %!test %! load fisheriris %! X = meas(:,2:4); %! Y = meas(:,1); %! Mdl = compact (fitrnet (X, Y, 'IterationLimit', 20)); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2), 'CompactRegressionNeuralNetwork'); %! assert_equal (M2.PredictorNames, Mdl.PredictorNames); %! assert_equal (class (M2.ResponseTransform), class (Mdl.ResponseTransform)); %! assert_equal (predict (M2, X(1:5,:)), predict (Mdl, X(1:5,:)), 1e-12); ## Every documented response transform reaches the response that is reported. %!test %! load fisheriris %! Mdl = compact (fitrnet (meas(:,2:4), meas(:,1))); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! T = {'identity', @(x) x; 'exp', @(x) exp (x); 'log', @(x) log (x)}; %! for i = 1:rows (T) %! Mdl.ResponseTransform = T{i,1}; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, T{i,2}(raw), 1e-12); %! endfor ## A function handle is taken as given and applied to the response. %!test %! load fisheriris %! Mdl = compact (fitrnet (meas(:,2:4), meas(:,1))); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! Mdl.ResponseTransform = @(x) x .^ 2; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, raw .^ 2, 1e-12); %!shared Xc, Dc, yr, yc, Xq, Dq %! k = (0:119)'; %! c1 = mod (k, 3) + 1; %! x2 = sin (k); %! c3 = 10 * (mod (floor (k / 2), 2) + 1); %! Xc = [c1, x2, c3]; %! Dc = [c1 == 1, c1 == 2, c1 == 3, x2, c3 == 10, c3 == 20]; %! yr = 5 * (c1 == 2) + 0.5 * x2 - 3 * (c3 == 20) + 0.1 * cos (k); %! yc = yr > 1; %! Xq = [1, 0, 10; 2, 0.5, 20]; %! Dq = [1, 0, 0, 0, 1, 0; 0, 1, 0, 0.5, 0, 1]; %!test # a compact model keeps the coding %! Full = fitrnet (Xc, yr, 'CategoricalPredictors', [1, 3], 'LayerSizes', 4); %! Mdl = compact (Full); %! assert_equal (predict (Mdl, [Xq; 4, 0, 10]), predict (Full, [Xq; 4, 0, 10])); %! fname = [tempname(), '.mdl']; %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (predict (M2, Xq), predict (Mdl, Xq)); ## A table at prediction %!test # the levels a predictor was coded through travel with the model %! load fisheriris %! T = table (meas(:,2), meas(:,3), 'VariableNames', {'SW', 'PL'}); %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! T.SL = meas(:,1); %! CMdl = compact (fitrnet (T, 'SL')); %! assert_equal (numel (CMdl.PredictorLevels), 3); %! assert_equal (CMdl.PredictorLevels{3}, {'narrow', 'wide'}); %!test # predict takes a table, read by name and not by position %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'SL'}); %! CMdl = compact (fitrnet (T, 'SL')); %! a = predict (CMdl, T); %! assert_equal (numel (a), 150); %! assert_equal (predict (CMdl, T(:, [3, 2, 1])), a); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,2:3); %! y = meas(:,1); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SW', 'PL'}); %! T.SL = y; %! Mdl = compact (fitrnet (T, 'SL')); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'SL'), a); %! assert_equal (loss (Mdl, T), a); statistics-release-2.1.2/inst/Machine_Learning/CompactRegressionSVM.m000066400000000000000000001117571527311104300256700ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef CompactRegressionSVM < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} CompactRegressionSVM ## ## Compact Support Vector Machine regression ## ## A @code{CompactRegressionSVM} object holds a support vector regression ## model that has dropped its training data. ## ## Create a @code{CompactRegressionSVM} object by using the @code{compact} ## method of a @code{RegressionSVM} object. ## ## The compact model keeps what is needed to answer about new data, the ## support vectors and their coefficients, the intercept, the kernel, the ## standardization and the response transform, and drops what only describes ## the fit: the predictor and response data, the observation weights, the ## rows used, the observation count, and which training rows became support ## vectors. @code{predict} and @code{loss} therefore agree with the full ## model to the last digit, while @code{resubPredict} and @code{resubLoss} ## do not exist here, there being no training data left to resubstitute. ## ## @seealso{RegressionSVM, fitrsvm} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CompactRegressionSVM} {property} NumPredictors ## ## Number of predictors ## ## A positive integer scalar. This property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {CompactRegressionSVM} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors. This property is read-only. ## ## @end deftp PredictorNames = []; ## -*- texinfo -*- ## @deftp {CompactRegressionSVM} {property} ResponseName ## ## Name of the response variable ## ## A character vector. This property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {CompactRegressionSVM} {property} Epsilon ## ## Half-width of the insensitive tube ## ## A non-negative scalar, carried over from the model this one was ## compacted from. It is what the @qcode{'epsiloninsensitive'} loss ## charges against. This property is read-only. ## ## @end deftp Epsilon = []; ## -*- texinfo -*- ## @deftp {CompactRegressionSVM} {property} Sigma ## ## Standard deviation of the predictors ## ## A row vector with one entry per predictor, used for standardization. ## Empty when the predictor data were not standardized. This property is ## read-only. ## ## @end deftp Sigma = []; ## -*- texinfo -*- ## @deftp {CompactRegressionSVM} {property} Mu ## ## Mean of the predictors ## ## A row vector with one entry per predictor, used for standardization. ## Empty when the predictor data were not standardized. This property is ## read-only. ## ## @end deftp Mu = []; ## -*- texinfo -*- ## @deftp {CompactRegressionSVM} {property} Alpha ## ## Dual coefficients of the support vectors ## ## A numeric column vector with one entry per support vector, signed, as ## in the model this one was compacted from. This property is read-only. ## ## @end deftp Alpha = []; ## -*- texinfo -*- ## @deftp {CompactRegressionSVM} {property} Beta ## ## Primal coefficients, one per predictor ## ## A numeric column vector, equal to ## @code{(obj.SupportVectors / s)' * obj.Alpha}, where @math{s} is the ## kernel scale, so that a prediction is @code{(@var{x} / s) * Beta + Bias}, ## as in MATLAB. It is empty for any kernel other than linear. This ## property is read-only. ## ## @end deftp Beta = []; ## -*- texinfo -*- ## @deftp {CompactRegressionSVM} {property} Bias ## ## Intercept of the fitted function ## ## A numeric scalar. With a linear kernel the prediction is ## @code{X * obj.Beta + obj.Bias}. This property is read-only. ## ## @end deftp Bias = []; ## -*- texinfo -*- ## @deftp {CompactRegressionSVM} {property} SupportVectors ## ## The support vectors themselves ## ## A numeric matrix with one row per support vector, on the scale the ## model was trained on. This property is read-only. ## ## @end deftp SupportVectors = []; ## -*- texinfo -*- ## @deftp {CompactRegressionSVM} {property} KernelParameters ## ## Parameters of the kernel function ## ## A structure with fields @qcode{Function} and @qcode{Scale}, and ## @qcode{Order} for a polynomial kernel. @qcode{Function} names the ## kernel as MATLAB names it, so a radial basis kernel reports ## @qcode{'gaussian'} whichever spelling was given. This property is ## read-only. ## ## @end deftp KernelParameters = []; ## -*- texinfo -*- ## @deftp {CompactRegressionSVM} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A numeric vector of column indices, and empty when none is. This ## property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {CompactRegressionSVM} {property} ExpandedPredictorNames ## ## Names of the predictors as the model expanded them ## ## A cell array of character vectors. This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; endproperties ## The LIBSVM structure the engine works in. It is ours alone, with no ## MATLAB counterpart, so it is kept out of the property listing while ## staying readable for anyone who needs the raw model. properties (Hidden, GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CompactRegressionSVM} {property} Model ## ## The trained LIBSVM model ## ## A structure as returned by @code{svmtrain} and consumed by ## @code{svmpredict}. This property is read-only. ## ## It is the engine's own structure and has no MATLAB counterpart, ## so it is kept out of @code{properties} and out of the online ## documentation. Reading it works exactly as it always did. ## ## @end deftp Model = []; endproperties ## Properties a user may set after the model is built. Each one is ## validated by its set method below. properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {CompactRegressionSVM} {property} ResponseTransform ## ## Transformation applied to the predicted response ## ## A function handle, applied by @code{predict} to the model's output. It ## may be set after construction, either to a handle or to the name of a ## supported transformation. ## ## @end deftp ResponseTransform = 'none'; endproperties ## Readable by the counterpart class, which copies it, and kept out of ## the documented surface. properties (GetAccess = public, SetAccess = protected, Hidden) ## The dummy coding of the categorical predictors, empty when none. Coding_ = []; ## The prediction for a row missing a predictor. MissingResponse_ = NaN; RTfun = @(y) y; endproperties ## Set methods for the properties a user may assign. methods (Hidden) function this = set.ResponseTransform (this, val) name = 'CompactRegressionSVM'; [this.RTfun, this.ResponseTransform] = parseResponseTransform (val, name); endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionSVM} {@var{obj} =} CompactRegressionSVM (@var{Mdl}) ## @deftypefnx {CompactRegressionSVM} {@var{obj} =} CompactRegressionSVM () ## ## Create a @code{CompactRegressionSVM} object. ## ## @var{Mdl} is the @code{RegressionSVM} object to ## compact. The documented way to reach this constructor is the ## @code{compact} method. ## ## Called with no arguments it returns an object with its properties ## empty, which is how a saved model is rebuilt before its values are ## filled in. ## ## @end deftypefn function this = CompactRegressionSVM (Mdl = []) ## Check for appropriate class if (isempty (Mdl)) return; elseif (! strcmpi (class (Mdl), 'RegressionSVM')) error ("CompactRegressionSVM: invalid regression object."); endif ## The levels a predictor read from a table was coded through ## travel with the model, so a compact one still reads a table this.PredictorLevels = Mdl.PredictorLevels; ## Save properties to compact model. The training data, the observation ## weights, the rows used, the observation count and IsSupportVector are ## deliberately left behind: each is sized to the training set, which is ## what "compact" exists not to carry. this.NumPredictors = Mdl.NumPredictors; this.PredictorNames = Mdl.PredictorNames; this.ResponseName = Mdl.ResponseName; this.ResponseTransform = Mdl.ResponseTransform; this.RTfun = Mdl.RTfun; this.Epsilon = Mdl.Epsilon; this.Sigma = Mdl.Sigma; this.Mu = Mdl.Mu; this.KernelParameters = Mdl.KernelParameters; this.Model = Mdl.Model; this.Alpha = Mdl.Alpha; this.Beta = Mdl.Beta; this.Bias = Mdl.Bias; this.SupportVectors = Mdl.SupportVectors; this.CategoricalPredictors = Mdl.CategoricalPredictors; this.Coding_ = Mdl.Coding_; this.MissingResponse_ = Mdl.MissingResponse_; this.ExpandedPredictorNames = Mdl.ExpandedPredictorNames; endfunction ## Custom display function disp (this) fprintf ("\n CompactRegressionSVM\n\n"); ## Print selected properties fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); fprintf ("%+25s: %d\n", 'NumPredictors', this.NumPredictors); fprintf ("%+25s: '%s'\n", 'ResponseTransform', this.ResponseTransform); fprintf ("%+25s: %g\n", 'Epsilon', this.Epsilon); fprintf ("%+25s: [%dx1 double]\n", 'Alpha', numel (this.Alpha)); if (! isempty (this.Beta)) fprintf ("%+25s: [%dx1 double]\n", 'Beta', numel (this.Beta)); endif fprintf ("%+25s: %f\n", 'Bias', this.Bias); fprintf ("%+25s: '%s'\n", 'KernelFunction', ... this.KernelParameters.Function); fprintf ("%+25s: [%dx%d double]\n", 'SupportVectors', ... rows (this.SupportVectors), columns (this.SupportVectors)); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {CompactRegressionSVM} {@var{obj} =} discardSupportVectors (@var{obj}) ## ## Discard the support vectors of a linear SVM model. ## ## @code{@var{obj} = discardSupportVectors (@var{obj})} empties ## @code{Alpha} and @code{SupportVectors}, leaving @code{Beta} and ## @code{Bias} to decide every prediction. A linear kernel needs ## nothing else, so the returned model predicts what it predicted ## before while carrying one vector in place of many. ## ## The kernel must be linear. Under any other the support vectors are ## part of the decision function and cannot be dropped. Discarding twice ## is not an error and changes nothing. ## ## @seealso{fitrsvm, RegressionSVM, CompactRegressionSVM} ## @end deftypefn function this = discardSupportVectors (this) if (nargin != 1) print_usage (); endif if (! strcmpi (this.KernelParameters.Function, 'linear')) error (strcat ("CompactRegressionSVM.discardSupportVectors: you", ... " cannot discard support vectors for a non-linear", ... " kernel.")); endif ## The engine keeps its own copy of the support vectors, so emptying ## the properties alone would free nothing. Collapsing the model onto ## the single vector that decides it leaves every scoring path as it ## was, svmpredict going on being the engine over one vector. this.Model = discardSVs (this.Model); this.Alpha = []; this.SupportVectors = []; endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionSVM} {@var{yFit} =} predict (@var{obj}, @var{XC}) ## ## Predict the response for new data with a compact support vector ## regression model. ## ## @code{@var{yFit} = predict (@var{obj}, @var{XC})} returns a column ## vector holding the predicted response for each row of @var{XC}. It ## agrees with the full model this object was compacted from. ## ## @itemize ## @item ## @var{obj} must be a @qcode{CompactRegressionSVM} class object. ## @item ## @var{XC} must be a numeric matrix with the same number of predictors as ## the data the model was trained on. ## @end itemize ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @seealso{CompactRegressionSVM, RegressionSVM} ## @end deftypefn function yFit = predict (this, XC) ## Check for sufficient input arguments if (nargin < 2) error ("CompactRegressionSVM.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'CompactRegressionSVM.predict', XC); ## Check for valid XC if (isempty (XC)) error ("CompactRegressionSVM.predict: XC is empty."); elseif (this.NumPredictors != columns (XC)) error (strcat ("CompactRegressionSVM.predict: XC must have the", ... " same number of predictors as the trained model.")); endif ## Code the categorical predictors and standardize (if necessary) if (! isempty (this.Coding_)) XC = dummyCoding (XC, this.Coding_); endif if (! isempty (this.Mu)) XC = (XC - this.Mu) ./ this.Sigma; endif ## LIBSVM returns the fitted response as its first output for a ## regression model, there being no label to decide. yFit = svmpredict (zeros (rows (XC), 1), XC, this.Model, '-q'); yFit(any (isnan (XC), 2)) = this.MissingResponse_; ## Apply ResponseTransform yFit = this.RTfun (yFit); endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionSVM} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactRegressionSVM} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactRegressionSVM} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {CompactRegressionSVM} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Compute the regression loss of a compact support vector machine model. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} returns the ## weighted mean squared error between the response @var{Y} and the ## response the model predicts for @var{X}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{CompactRegressionSVM} class object. ## @item ## @var{X} must be a numeric matrix with the same number of predictors as ## the data the model was trained on. ## @item ## @var{Y} must be a numeric vector with as many rows as @var{X}. ## @end itemize ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{L} = loss (@dots{}, @var{name}, @var{value})} accepts the ## following @qcode{Name-Value} pairs. ## ## @multitable @columnfractions 0.28 0.72 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'LossFun'} @tab @qcode{'mse'}, the default, ## @qcode{'epsiloninsensitive'}, or a function handle called as ## @code{@var{lossfun} (@var{Y}, @var{yFit}, @var{W})} returning a scalar. ## ## @item @qcode{'Weights'} @tab A numeric vector of observation weights ## with one entry per row of @var{X}. It defaults to a uniform weight. ## The weights are normalized to sum to one before the loss is formed. ## @end multitable ## ## @seealso{CompactRegressionSVM, RegressionSVM} ## @end deftypefn function L = loss (this, X, Y, varargin) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactRegressionSVM.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("CompactRegressionSVM.loss: Name-Value arguments", ... " must be in pairs.")); endif [X, Y] = checkXY_ (this, X, Y, 'loss'); ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights'}; dfValues = {'mse', []}; [LossFun, W, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (is_function_handle (LossFun) || (ischar (LossFun) && isrow (LossFun)))) error (strcat ("CompactRegressionSVM.loss: 'LossFun' must be a", ... " character vector or a function handle.")); endif if (ischar (LossFun) && ! any (strcmpi (LossFun, ... {'mse', 'epsiloninsensitive'}))) error ("CompactRegressionSVM.loss: unsupported 'LossFun' value."); endif errmsg = weightsClass (W); if (! isempty (errmsg)) error ("CompactRegressionSVM.loss: %s", errmsg); endif if (! isempty (W) && ! (isnumeric (W) && isvector (W))) error (strcat ("CompactRegressionSVM.loss: 'Weights' must be a", ... " numeric vector.")); endif if (! isempty (W) && numel (W) != rows (X)) error (strcat ("CompactRegressionSVM.loss: size of 'Weights' must", ... " equal the number of rows in X.")); endif if (! isempty (args)) error ("CompactRegressionSVM.loss: invalid optional paired argument."); endif if (isempty (W)) W = ones (rows (X), 1); endif ## Weights are normalized to sum to one, as MATLAB does, so a loss is ## a weighted average rather than a weighted sum. W = double (W(:)); W = W / sum (W); yFit = predict (this, X); Y = Y(:); if (is_function_handle (LossFun)) L = LossFun (Y, yFit, W); if (! (isnumeric (L) && isscalar (L))) error (strcat ("CompactRegressionSVM.loss: 'LossFun' must", ... " return a numeric scalar.")); endif elseif (strcmpi (LossFun, 'epsiloninsensitive')) L = sum (W .* max (0, abs (Y - yFit) - this.Epsilon)); else L = sum (W .* (Y - yFit) .^ 2); endif endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionSVM} {} savemodel (@var{obj}, @var{filename}) ## ## Save a compact support vector regression model to a file. ## ## @code{savemodel (@var{obj}, @var{filename})} saves every property of ## the @qcode{CompactRegressionSVM} object @var{obj} into @var{filename} ## in binary format, so that it can be read back with @code{loadmodel}. ## ## @seealso{loadmodel, CompactRegressionSVM} ## @end deftypefn function savemodel (this, fname) if (nargin < 2) error ("CompactRegressionSVM.savemodel: too few input arguments."); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error (strcat ("CompactRegressionSVM.savemodel: FNAME must be", ... " a character vector.")); endif ## Generate variable for class name classdef_name = 'CompactRegressionSVM'; ## Create variables from model properties NumPredictors = this.NumPredictors; PredictorNames = this.PredictorNames; ResponseName = this.ResponseName; ResponseTransform = this.ResponseTransform; Epsilon = this.Epsilon; Sigma = this.Sigma; Mu = this.Mu; KernelParameters = this.KernelParameters; Model = this.Model; Alpha = this.Alpha; Beta = this.Beta; Bias = this.Bias; SupportVectors = this.SupportVectors; CategoricalPredictors = this.CategoricalPredictors; Coding_ = this.Coding_; MissingResponse_ = this.MissingResponse_; ExpandedPredictorNames = this.ExpandedPredictorNames; RTfun = this.RTfun; ## Save classdef name and all model properties as individual variables save ('-binary', fname, 'classdef_name', 'NumPredictors', ... 'PredictorNames', 'ResponseName', 'ResponseTransform', ... 'Epsilon', 'Sigma', 'Mu', ... 'Model', 'Alpha', 'Beta', 'Bias', 'SupportVectors', ... 'CategoricalPredictors', 'Coding_', ... 'MissingResponse_', 'ExpandedPredictorNames', ... 'KernelParameters', 'RTfun'); endfunction endmethods methods (Access = private) ## Shared validation for the assessment methods, so each reports under ## its own name. function [X, Y] = checkXY_ (this, X, Y, caller) if (isempty (X)) error ("CompactRegressionSVM.%s: X is empty.", caller); elseif (this.NumPredictors != columns (X)) error (strcat ("CompactRegressionSVM.%s: X must have the same", ... " number of predictors as the trained model."), caller); endif if (isempty (Y)) error ("CompactRegressionSVM.%s: Y is empty.", caller); elseif (! (isnumeric (Y) && isreal (Y))) error (strcat ("CompactRegressionSVM.%s: Y must be a real", ... " numeric vector."), caller); elseif (rows (X) != numel (Y)) error (strcat ("CompactRegressionSVM.%s: Y must have the same", ... " number of rows as X."), caller); endif endfunction endmethods methods(Static, Hidden) function mdl = load_model (filename, data) ## Create a CompactRegressionSVM object mdl = CompactRegressionSVM (); ## Get fieldnames from DATA (including private properties) names = fieldnames (data); ## The set methods for these read other properties, and one of them ## rebuilds Coeffs, so they are assigned once everything else is in ## place rather than in the order the file happens to list them. late = ismember (names, {'Cost', 'Prior', 'ScoreTransform', ... 'ResponseTransform'}); names = [names(! late); names(late)]; ## Copy data into object for i = 1:numel (names) ## Check fieldnames in DATA match the class properties try mdl.(names{i}) = data.(names{i}); catch error (strcat ("CompactRegressionSVM.load_model: invalid", ... " model in '%s'."), filename) end_try_catch endfor endfunction endmethods endclassdef ## The compact model keeps what answers about new data and drops the fit. %!test %! randn ('seed', 42); %! X = randn (50, 2); %! Y = X(:,1) - 2 * X(:,2); %! CMdl = compact (RegressionSVM (X, Y)); %! assert_equal (class (CMdl), 'CompactRegressionSVM'); %! kept = {'NumPredictors', 'PredictorNames', 'ResponseName', ... %! 'ResponseTransform', 'Epsilon', 'Sigma', 'Mu', ... %! 'Alpha', 'Beta', 'Bias', 'SupportVectors', ... %! 'CategoricalPredictors', 'ExpandedPredictorNames'}; %! assert_equal (all (ismember (kept, properties (CMdl))), true); %! dropped = {'X', 'Y', 'W', 'NumObservations', 'RowsUsed', ... %! 'IsSupportVector'}; %! assert_equal (any (ismember (dropped, properties (CMdl))), false); ## A compact model predicts exactly what the model it came from predicts. %!test %! randn ('seed', 42); %! X = randn (60, 3); %! Y = X * [1; -2; 0.5] + 3; %! Mdl = RegressionSVM (X, Y); %! CMdl = compact (Mdl); %! assert_equal (predict (CMdl, X), predict (Mdl, X)); %! assert_equal (loss (CMdl, X, Y), loss (Mdl, X, Y)); %! assert_equal (loss (CMdl, X, Y), resubLoss (Mdl), 1e-12); %! assert_equal (CMdl.Alpha, Mdl.Alpha); %! assert_equal (CMdl.Beta, Mdl.Beta); %! assert_equal (CMdl.Bias, Mdl.Bias); %! assert_equal (CMdl.SupportVectors, Mdl.SupportVectors); %! assert_equal (CMdl.Epsilon, Mdl.Epsilon); ## With a linear kernel the compact model is a plain linear function. %!test %! randn ('seed', 42); %! X = randn (40, 2); %! Y = X * [3; -1] + 2; %! CMdl = compact (RegressionSVM (X, Y)); %! assert_equal (X * CMdl.Beta + CMdl.Bias, predict (CMdl, X), 1e-8); %! assert_equal (CMdl.Beta, CMdl.SupportVectors' * CMdl.Alpha, 1e-12); ## Standardization travels with the compact model. %!test %! randn ('seed', 42); %! X = [randn(60, 1), randn(60, 1) * 1000]; %! Y = X(:,1) + X(:,2) / 1000; %! Mdl = RegressionSVM (X, Y, 'Standardize', true, 'Epsilon', 0.01); %! CMdl = compact (Mdl); %! assert_equal (CMdl.Mu, Mdl.Mu); %! assert_equal (CMdl.Sigma, Mdl.Sigma); %! assert_equal (predict (CMdl, X), predict (Mdl, X)); ## A non-linear kernel compacts too, and keeps Beta empty. %!test %! randn ('seed', 42); %! X = randn (40, 2); %! Y = sum (X .^ 2, 2); %! Mdl = RegressionSVM (X, Y, 'KernelFunction', 'rbf'); %! CMdl = compact (Mdl); %! assert_equal (isempty (CMdl.Beta), true); %! assert_equal (predict (CMdl, X), predict (Mdl, X)); ## loss takes the same options as the full model's. %!test %! randn ('seed', 42); %! X = randn (30, 2); %! Y = X(:,1) * 3 + 1; %! Mdl = RegressionSVM (X, Y, 'Epsilon', 0.5); %! CMdl = compact (Mdl); %! yFit = predict (CMdl, X); %! assert_equal (loss (CMdl, X, Y), mean ((Y - yFit) .^ 2), 1e-12); %! assert_equal (loss (CMdl, X, Y, 'LossFun', 'epsiloninsensitive'), ... %! mean (max (0, abs (Y - yFit) - 0.5)), 1e-12); %! w = rand (30, 1) + 0.1; %! assert_equal (loss (CMdl, X, Y, 'Weights', w), ... %! loss (CMdl, X, Y, 'Weights', 4 * w), 1e-12); %! f = @(y, yf, ww) sum (ww .* abs (y - yf)); %! assert_equal (loss (CMdl, X, Y, 'LossFun', f), ... %! mean (abs (Y - yFit)), 1e-12); ## ResponseTransform travels, and can be replaced on the compact model. %!test %! X = [linspace(0, 1, 20)', linspace(1, 2, 20)']; %! Mdl = RegressionSVM (X, 2 * X(:,1) + 1, 'ResponseTransform', 'exp'); %! CMdl = compact (Mdl); %! assert_equal (predict (CMdl, X), predict (Mdl, X)); %! CMdl.ResponseTransform = 'none'; %! assert_equal (predict (CMdl, X), log (predict (Mdl, X)), 1e-12); ## A saved compact model comes back carrying its own numbers. %!test %! randn ('seed', 42); %! X = randn (30, 2); %! Y = X(:,1) * 4 - 1; %! CMdl = compact (RegressionSVM (X, Y, 'Standardize', true)); %! fname = tempname (); %! savemodel (CMdl, fname); %! C2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (C2), 'CompactRegressionSVM'); %! assert_equal (C2.Alpha, CMdl.Alpha); %! assert_equal (C2.Bias, CMdl.Bias); %! assert_equal (C2.Epsilon, CMdl.Epsilon); %! assert_equal (C2.SupportVectors, CMdl.SupportVectors); %! assert_equal (predict (C2, X), predict (CMdl, X)); ## Test input validation for the constructor ## discardSupportVectors empties what R2024a empties and keeps what it ## keeps: Alpha and the support vectors go, Beta, Bias and IsSupportVector ## stay, and the class is unchanged. %!test %! load fisheriris %! keep = ! strcmp (species, "setosa"); %! X = meas(keep,2:4); y = meas(keep,1); %! Mdl = compact (fitrsvm (X, y, "KernelFunction", "linear")); %! D = discardSupportVectors (Mdl); %! assert_equal (class (D), "CompactRegressionSVM"); %! assert_equal (isempty (D.Alpha), true); %! assert_equal (isempty (D.SupportVectors), true); %! assert_equal (D.Beta, Mdl.Beta); %! assert_equal (D.Bias, Mdl.Bias); ## A linear decision needs only Beta and Bias, so the model predicts what it ## predicted before. %!test %! load fisheriris %! keep = ! strcmp (species, "setosa"); %! X = meas(keep,2:4); y = meas(keep,1); %! Mdl = compact (fitrsvm (X, y, "KernelFunction", "linear")); %! D = discardSupportVectors (Mdl); %! assert_equal (predict (D, X), predict (Mdl, X), 1e-10); ## The saving is real rather than cosmetic: the engine keeps its own copy of ## the support vectors, and it collapses to the one vector that decides a ## linear model. Emptying the properties alone would free nothing. %!test %! load fisheriris %! keep = ! strcmp (species, "setosa"); %! X = meas(keep,2:4); y = meas(keep,1); %! Mdl = compact (fitrsvm (X, y, "KernelFunction", "linear")); %! D = discardSupportVectors (Mdl); %! assert_equal (rows (Mdl.Model.SVs) > 1, true); %! assert_equal (rows (D.Model.SVs), 1); %! assert_equal (predict (discardSupportVectors (D), X), predict (D, X)); %!test # A row missing a predictor predicts the lower median of the response %! X = [(1:10)', mod((1:10)', 3)]; %! Mdl = compact (RegressionSVM (X, (1:10)')); %! assert_equal (predict (Mdl, [NaN, 1]), 5); %!error ... %! load fisheriris %! keep = ! strcmp (species, "setosa"); %! X = meas(keep,2:4); y = meas(keep,1); %! discardSupportVectors (compact (fitrsvm (X, y, "KernelFunction", "rbf"))) %!error ... %! CompactRegressionSVM (1) %!error ... %! CompactRegressionSVM (fitcsvm (ones (4, 2), [1; 1; 2; 2])) ## Test input validation for predict and loss %!shared CRSVM %! CRSVM = compact (RegressionSVM ([1, 1; 2, 1; 3, 2; 4, 2], [2; 4; 6; 8])); %!error ... %! predict (CRSVM) %!error ... %! predict (CRSVM, []) %!error ... %! predict (CRSVM, ones (2, 3)) %!error ... %! loss (CRSVM) %!error ... %! loss (CRSVM, [1, 1; 2, 1], [2; 4], 'Weights') %!error ... %! loss (CRSVM, [], [2; 4]) %!error ... %! loss (CRSVM, ones (2, 3), [2; 4]) %!error ... %! loss (CRSVM, [1, 1; 2, 1], []) %!error ... %! loss (CRSVM, [1, 1; 2, 1], {'a'; 'b'}) %!error ... %! loss (CRSVM, [1, 1; 2, 1], [2; 4; 6]) %!error ... %! loss (CRSVM, [1, 1; 2, 1], [2; 4], 'LossFun', 5) %!error ... %! loss (CRSVM, [1, 1; 2, 1], [2; 4], 'LossFun', 'mae') %!error ... %! loss (CRSVM, [1, 1; 2, 1], [2; 4], 'LossFun', @(y, yf, w) [1, 2]) %!error ... %! loss (CRSVM, [1, 1; 2, 1], [2; 4], 'Weights', {'a'}) %!error ... %! loss (CRSVM, [1, 1; 2, 1], [2; 4], 'Weights', ones (2, 2)) %!error ... %! loss (CRSVM, [1, 1; 2, 1], [2; 4], 'Weights', [1; 2; 3]) %!error ... %! loss (CRSVM, [1, 1; 2, 1], [2; 4], 'Nope', 1) ## Test input validation for savemodel %!error ... %! savemodel (CRSVM) %!error ... %! savemodel (CRSVM, 5) %!error ... %! CRSVM.ResponseTransform = 'nope'; ## A fitted model survives savemodel and loadmodel: the properties come ## back as they were and it predicts the same. %!test %! load fisheriris %! X = meas(:,2:4); %! Y = meas(:,1); %! Mdl = compact (fitrsvm (X, Y)); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2), 'CompactRegressionSVM'); %! assert_equal (M2.PredictorNames, Mdl.PredictorNames); %! assert_equal (class (M2.ResponseTransform), class (Mdl.ResponseTransform)); %! assert_equal (predict (M2, X(1:5,:)), predict (Mdl, X(1:5,:)), 1e-12); ## KernelParameters comes across with the compact form. %!test %! load fisheriris %! Mdl = fitrsvm (meas(:,1:3), meas(:,4)); %! CMdl = compact (Mdl); %! assert_equal (CMdl.KernelParameters, Mdl.KernelParameters); %! assert_equal (CMdl.KernelParameters.Function, 'linear'); ## Every documented response transform reaches the response that is reported. %!test %! load fisheriris %! Mdl = compact (fitrsvm (meas(:,2:4), meas(:,1))); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! T = {'identity', @(x) x; 'exp', @(x) exp (x); 'log', @(x) log (x)}; %! for i = 1:rows (T) %! Mdl.ResponseTransform = T{i,1}; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, T{i,2}(raw), 1e-12); %! endfor ## A function handle is taken as given and applied to the response. %!test %! load fisheriris %! Mdl = compact (fitrsvm (meas(:,2:4), meas(:,1))); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! Mdl.ResponseTransform = @(x) x .^ 2; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, raw .^ 2, 1e-12); %!shared Xc, Dc, yr, yc, Xq, Dq %! k = (0:119)'; %! c1 = mod (k, 3) + 1; %! x2 = sin (k); %! c3 = 10 * (mod (floor (k / 2), 2) + 1); %! Xc = [c1, x2, c3]; %! Dc = [c1 == 1, c1 == 2, c1 == 3, x2, c3 == 10, c3 == 20]; %! yr = 5 * (c1 == 2) + 0.5 * x2 - 3 * (c3 == 20) + 0.1 * cos (k); %! yc = yr > 1; %! Xq = [1, 0, 10; 2, 0.5, 20]; %! Dq = [1, 0, 0, 0, 1, 0; 0, 1, 0, 0.5, 0, 1]; %!test # a compact model keeps the coding %! Full = fitrsvm (Xc, yr, 'CategoricalPredictors', [1, 3]); %! Mdl = compact (Full); %! assert_equal (Mdl.ExpandedPredictorNames, Full.ExpandedPredictorNames); %! assert_equal (predict (Mdl, Xq), predict (Full, Xq)); %! fname = [tempname(), '.mdl']; %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (predict (M2, Xq), predict (Mdl, Xq)); ## A table at prediction %!test # the levels a predictor was coded through travel with the model %! load fisheriris %! T = table (meas(:,2), meas(:,3), 'VariableNames', {'SW', 'PL'}); %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! T.SL = meas(:,1); %! CMdl = compact (fitrsvm (T, 'SL')); %! assert_equal (numel (CMdl.PredictorLevels), 3); %! assert_equal (CMdl.PredictorLevels{3}, {'narrow', 'wide'}); %!test # predict takes a table, read by name and not by position %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'SL'}); %! CMdl = compact (fitrsvm (T, 'SL')); %! a = predict (CMdl, T); %! assert_equal (numel (a), 150); %! assert_equal (predict (CMdl, T(:, [3, 2, 1])), a); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,2:3); %! y = meas(:,1); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SW', 'PL'}); %! T.SL = y; %! Mdl = compact (fitrsvm (T, 'SL')); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'SL'), a); %! assert_equal (loss (Mdl, T), a); statistics-release-2.1.2/inst/Machine_Learning/CompactRegressionTree.m000066400000000000000000001247141527311104300261170ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef CompactRegressionTree < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} CompactRegressionTree ## ## Compact binary decision tree for regression ## ## A @code{CompactRegressionTree} object carries the tree a ## @code{RegressionTree} model grew and everything @code{predict} needs, but ## not the observations it was fitted on. It answers new data identically ## to the model it came from, and is far smaller to keep or to ship. ## ## Create one with the @code{compact} method of a @code{RegressionTree} ## object. Because it holds no training data, it has no @code{resub} ## methods and cannot be cross-validated, and it cannot be pruned: the ## pruning sequence is reported but taking a subtree out of it rewrites the ## node table, which is work for the model that still has its data. ## ## @seealso{RegressionTree, fitrtree} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} NumNodes ## ## Number of nodes in the tree ## ## A positive integer, the number of nodes the tree holds, branch nodes ## and leaves together. Nodes are numbered as they are created, so a ## parent always carries a lower number than either of its children. ## This property is read-only. ## ## @end deftp NumNodes = []; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} Children ## ## Child nodes of each node ## ## A @math{NumNodesx2} matrix naming the left and the right child of each ## node. A leaf carries a zero in both columns. This property is ## read-only. ## ## @end deftp Children = []; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} Parent ## ## Parent of each node ## ## A column vector naming the parent of each node. The root carries a ## zero. This property is read-only. ## ## @end deftp Parent = []; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} IsBranchNode ## ## Which nodes are branch nodes ## ## A logical column vector, true for each node that carries a split and ## false for each leaf. This property is read-only. ## ## @end deftp IsBranchNode = []; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} CutPredictor ## ## Name of the predictor each node cuts on ## ## A cell array of character vectors with one entry per node, holding the ## name of the predictor the node splits on and an empty character vector ## at a leaf. This property is read-only. ## ## @end deftp CutPredictor = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} CutPredictorIndex ## ## Index of the predictor each node cuts on ## ## A column vector holding, for each node, the column of @var{X} the node ## splits on, and zero at a leaf. This property is read-only. ## ## @end deftp CutPredictorIndex = []; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} CutPoint ## ## Cut point of each node ## ## A column vector holding, for each node, the value the split compares ## the predictor against: an observation goes left when its value is less ## than the cut point and right otherwise. A leaf and a categorical cut ## carry @qcode{NaN}. ## This property is read-only. ## ## @end deftp CutPoint = []; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} CutType ## ## Type of cut at each node ## ## A cell array of character vectors holding @qcode{'continuous'} at a ## branch node that cuts a numeric predictor at a point, ## @qcode{'categorical'} at one that splits a set of levels, and an empty ## character vector at a leaf. This property is read-only. ## ## @end deftp CutType = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} CutCategories ## ## Categories used at each branch ## ## A @math{NumNodesx2} cell array holding, for a node that cuts a ## categorical predictor, the levels sent left and the levels sent right. ## Every other entry is empty. This property is read-only. ## ## @end deftp CutCategories = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} CategoricalSplit ## ## Categorical splits of the tree ## ## A @math{Nx2} cell array with one row per categorical split, in node ## order, holding the levels sent left and the levels sent right. It is ## empty when no split is categorical. This property is read-only. ## ## @end deftp CategoricalSplit = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} NodeSize ## ## Number of observations at each node ## ## A column vector holding how many training observations reached each ## node. A row missing the predictor its node cuts on is counted at that ## node and at none below it, so a parent's size is not in general the ## sum of its children's. This property is read-only. ## ## @end deftp NodeSize = []; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} NodeMean ## ## Mean response at each node ## ## A column vector holding, for each node, the weighted mean of the ## response over the observations that reached it. It is what ## @code{predict} answers for a row that comes to rest there. This ## property is read-only. ## ## @end deftp NodeMean = []; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} NodeError ## ## Mean squared error of each node ## ## A column vector holding, for each node, the weighted mean squared ## error of the response about the node's mean. This property is ## read-only. ## ## @end deftp NodeError = []; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} NodeProbability ## ## Probability of reaching each node ## ## A column vector holding, for each node, the total weight of the ## observations that reached it, the weights being those in @code{W}. ## The root carries one. This property is read-only. ## ## @end deftp NodeProbability = []; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} NodeRisk ## ## Risk of each node ## ## A column vector holding, for each node, its mean squared error ## weighted by the probability of reaching it, which is the squared error ## the node contributes to the whole tree. This property is read-only. ## ## @end deftp NodeRisk = []; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} PruneList ## ## Pruning level of each node ## ## A column vector holding, for each branch node, the level of the cost ## complexity sequence at which it stops being a branch node, and zero at ## a leaf. Pruning the tree to level @var{L} turns every node whose ## level is between one and @var{L} into a leaf. It is empty when ## neither @qcode{'Prune'} nor @qcode{'MergeLeaves'} was asked for, since ## no sequence was then estimated. This property is read-only. ## ## @end deftp PruneList = []; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} PruneAlpha ## ## Cost complexity parameter of each pruning level ## ## A column vector with one element per level of the pruning sequence, ## the first of which is zero and stands for the unpruned tree. Level ## @var{L} is the smallest subtree that is optimal for every complexity ## parameter from @code{PruneAlpha(@var{L}+1)} up to the next one. This ## property is read-only. ## ## @end deftp PruneAlpha = []; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} SurrogateCutCategories ## ## Categories of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutCategories = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} SurrogateCutFlip ## ## Cut assignments of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutFlip = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} SurrogateCutPoint ## ## Cut points of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutPoint = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} SurrogateCutType ## ## Types of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutType = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} SurrogateCutPredictor ## ## Predictors of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutPredictor = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} SurrogatePredictorAssociation ## ## Predictive measures of association of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogatePredictorAssociation = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} PredictorNames ## ## Names of the predictor variables ## ## A cell array of character vectors with one name per column of ## @var{X}. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} ResponseName ## ## Name of the response variable ## ## A character vector naming the response. This property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A row vector of column indices into @var{X}, naming the predictors ## treated as categorical, empty when none is. This property is ## read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} ExpandedPredictorNames ## ## Expanded predictor names ## ## A cell array of character vectors. It differs from ## @code{PredictorNames} only when a categorical predictor has been ## expanded into one column per level, which this class does not do, so ## the two are always equal. This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {CompactRegressionTree} {property} ResponseTransform ## ## Transform applied to the predicted response ## ## A character vector naming the function @code{predict} applies to the ## response it predicts before returning it, or a function handle taking ## and returning an array of the same size. The default is ## @qcode{'none'}. ## ## @end deftp ResponseTransform = []; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ## The parsed ResponseTransform, applied to the response by predict. RTfun = []; ## The risk each branch node carries on account of the observations that ## stop there, missing the predictor it cuts on. Those observations are ## in neither child, so what a split buys is the drop in risk less this. HeldRisk = []; endproperties ## Set methods for the properties a user may assign after compacting. methods (Hidden) function this = set.ResponseTransform (this, val) [this.RTfun, this.ResponseTransform] = ... parseResponseTransform (val, 'CompactRegressionTree'); endfunction function disp (this) fprintf ('\n CompactRegressionTree\n\n'); fprintf ('%22s: %s\n', 'ResponseName', this.ResponseName); fprintf ('%22s: %s\n', 'CategoricalPredictors', ... mat2str (this.CategoricalPredictors)); fprintf ('%22s: %s\n', 'ResponseTransform', this.ResponseTransform); fprintf ('%22s: %d\n', 'NumNodes', this.NumNodes); fprintf ('\n'); endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionTree} {@var{obj} =} CompactRegressionTree (@var{Mdl}) ## ## Create a @code{CompactRegressionTree} object. ## ## @var{Mdl} is the @code{RegressionTree} object to compact. The ## documented way to reach this constructor is the @code{compact} method. ## ## @seealso{RegressionTree, fitrtree} ## @end deftypefn function this = CompactRegressionTree (Mdl) ## Input validation if (nargin < 1) error ("CompactRegressionTree: too few input arguments."); endif if (! isa (Mdl, 'RegressionTree')) error (strcat ("CompactRegressionTree: MDL must be a", ... " RegressionTree object.")); endif ## The levels a predictor read from a table was coded through ## travel with the model, so a compact one still reads a table this.PredictorLevels = Mdl.PredictorLevels; ## The node table and everything derived from it, then the transform. this.NumNodes = Mdl.NumNodes; this.Children = Mdl.Children; this.Parent = Mdl.Parent; this.CutPredictorIndex = Mdl.CutPredictorIndex; this.CutPoint = Mdl.CutPoint; this.CutCategories = Mdl.CutCategories; this.NodeSize = Mdl.NodeSize; this.NodeMean = Mdl.NodeMean; this.NodeError = Mdl.NodeError; this.NodeProbability = Mdl.NodeProbability; this.NodeRisk = Mdl.NodeRisk; this.HeldRisk = Mdl.HeldRisk; this.PruneList = Mdl.PruneList; this.PruneAlpha = Mdl.PruneAlpha; this.PredictorNames = Mdl.PredictorNames; this.ResponseName = Mdl.ResponseName; this.CategoricalPredictors = Mdl.CategoricalPredictors; this.ExpandedPredictorNames = Mdl.ExpandedPredictorNames; this = fillCuts (this); this.ResponseTransform = Mdl.ResponseTransform; endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {CompactRegressionTree} {@var{yFit} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {CompactRegressionTree} {[@var{yFit}, @var{node}] =} predict (@dots{}) ## ## Predict the response with a trained @code{CompactRegressionTree} object. ## ## @code{@var{yFit} = predict (@var{obj}, @var{XC})} sends each row of ## @var{XC} down the tree and returns the mean response of the node it ## comes to rest at, after @code{ResponseTransform}. @var{XC} must have ## as many columns as the predictor data the model was fitted on. ## ## @var{XC} may also be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: one ## the model was not fitted on is passed over, one it needs and cannot ## find is named, and a value holding a level is coded as that level was ## coded at fitting. ## ## @code{[@var{yFit}, @var{node}] = predict (@dots{})} also returns the ## number of the node each row landed in. ## ## A row missing the predictor a node cuts on is stopped at that node and ## takes its answer, rather than being sent down a branch on evidence the ## row does not carry. ## ## @seealso{CompactRegressionTree, fitrtree} ## @end deftypefn function [yFit, node] = predict (this, XC) ## Input validation if (nargin < 2) error ("CompactRegressionTree.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'CompactRegressionTree.predict', XC); if (isempty (XC)) error ("CompactRegressionTree.predict: XC is empty."); endif if (! (isnumeric (XC) && isreal (XC) && ismatrix (XC))) error (strcat ("CompactRegressionTree.predict: XC must be a", ... " real numeric matrix.")); endif if (numel (this.PredictorNames) != columns (XC)) error (strcat ("CompactRegressionTree.predict: XC must have the", ... " same number of predictors as the trained", ... " model.")); endif [yFit, node] = treepredict (XC, this.Children, ... this.CutPredictorIndex, this.CutPoint, ... this.NodeMean, ... this.CutCategories); yFit = this.RTfun (yFit); endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionTree} {@var{imp} =} predictorImportance (@var{obj}) ## ## Estimate the importance of each predictor. ## ## @code{@var{imp} = predictorImportance (@var{obj})} returns a row ## vector with one element per predictor, holding the total drop in risk ## the splits on that predictor bring about, divided by the number of ## branch nodes. A predictor the tree never splits on scores zero. ## ## The drop at a branch node is its @code{NodeRisk} less the risk of its ## two children and less what it holds back, so a predictor that is ## chosen often, high up, and on nodes it separates well, scores highest. ## The numbers are comparable between predictors of one tree and not ## between trees. ## ## @seealso{CompactRegressionTree, fitrtree, CompactRegressionTree.NodeRisk} ## @end deftypefn function imp = predictorImportance (this) imp = zeros (1, numel (this.PredictorNames)); branch = find (this.Children(:,1) > 0); if (isempty (branch)) return; endif for ii = 1:numel (branch) b = branch(ii); kids = this.Children(b,:); ## The rows that stop at the node are in neither child and are ## answered the same whether it is split or not, so they are no part ## of what the split buys. drop = this.NodeRisk(b) - this.NodeRisk(kids(1)) ... - this.NodeRisk(kids(2)) - this.HeldRisk(b); v = this.CutPredictorIndex(b); imp(v) = imp(v) + drop; endfor imp = imp / numel (branch); endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionTree} {@var{r} =} nodeVariableRange (@var{obj}, @var{node}) ## ## Range of each predictor at a node. ## ## @code{@var{r} = nodeVariableRange (@var{obj}, @var{node})} returns a ## structure with one field per predictor the path from the root to ## @var{node} cuts on, holding the two-element range of values that reach ## the node, or for a categorical predictor the levels that reach it. A ## predictor the path never cuts on is unconstrained and is left out, so ## the root gives a structure with no fields. ## ## @seealso{CompactRegressionTree, fitrtree} ## @end deftypefn function r = nodeVariableRange (this, node) ## Input validation if (nargin < 2) error (strcat ("CompactRegressionTree.nodeVariableRange: too few", ... " input arguments.")); endif if (! (isnumeric (node) && isscalar (node) && isreal (node) && node >= 1 && node <= this.NumNodes && node == fix (node))) error (strcat ("CompactRegressionTree.nodeVariableRange: NODE must", ... " be a positive integer no greater than the", ... " number of nodes in the tree.")); endif p = numel (this.PredictorNames); lo = -Inf (1, p); hi = Inf (1, p); touched = false (1, p); ## Walk up to the root, narrowing the range of whichever predictor ## each step cut on. Going up rather than down finds the path without ## a search, a node having exactly one parent. ## A categorical cut narrows its predictor to a set of levels instead, ## and the first such cut met on the way up is the narrowest. levels = cell (1, p); child = node; up = this.Parent(child); while (up > 0) v = this.CutPredictorIndex(up); side = 1 + (this.Children(up,1) != child); cats = this.CutCategories{up,side}; if (! isempty (cats)) if (! touched(v)) levels{v} = cats; endif elseif (side == 1) hi(v) = min (hi(v), this.CutPoint(up)); else lo(v) = max (lo(v), this.CutPoint(up)); endif touched(v) = true; child = up; up = this.Parent(child); endwhile r = struct (); for v = find (touched) if (isempty (levels{v})) r.(this.PredictorNames{v}) = [lo(v), hi(v)]; else r.(this.PredictorNames{v}) = levels{v}; endif endfor endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionTree} {} view (@var{obj}) ## ## Print the tree as text. ## ## @code{view (@var{obj})} prints one line per node: a branch node names ## the predictor it cuts on, the cut point, and the node each side leads ## to, and a leaf names the response it fits. A branch node's line ends ## with the response it would fit itself, which is the answer an ## observation missing that predictor gets. ## ## @seealso{CompactRegressionTree, fitrtree} ## @end deftypefn function view (this) ## The node numbers are right aligned on the widest of them, which is ## how MATLAB lays the listing out. w = numel (sprintf ("%d", this.NumNodes)); fprintf ("Decision tree for regression\n"); for ii = 1:this.NumNodes m = sprintf ("%g", this.NodeMean(ii)); if (this.Children(ii,1) == 0) fprintf ("%*d fit = %s\n", w, ii, m); else v = this.PredictorNames{this.CutPredictorIndex(ii)}; cc = this.CutCategories(ii,:); if (! isempty (cc{1})) fprintf ("%*d if %s then node %d elseif %s then node", w, ii, ... treeLevelText (v, cc{1}), this.Children(ii,1), ... treeLevelText (v, cc{2})); else c = sprintf ("%g", this.CutPoint(ii)); fprintf ("%*d if %s<%s then node %d elseif %s>=%s then node", ... w, ii, v, c, this.Children(ii,1), v, c); endif fprintf (" %d else %s\n", this.Children(ii,2), m); endif endfor endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionTree} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactRegressionTree} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactRegressionTree} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {CompactRegressionTree} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Regression loss on new data. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} returns the ## weighted mean squared error of the response the model predicts for ## @var{X} against the observed response @var{Y}. A row whose response ## is missing is dropped, as it is when fitting. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{L} = loss (@dots{}, @var{name}, @var{value})} takes the ## following options. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'LossFun'} @tab @qcode{'mse'}, the default, or a function ## handle taking the true response, the predicted response and the ## weights, and returning a numeric scalar. ## ## @item @qcode{'Weights'} @tab A numeric vector of observation weights, ## one per row of @var{X}, normalized to sum to one before it is applied. ## ## @end multitable ## ## @seealso{CompactRegressionTree, fitrtree, CompactRegressionTree.predict} ## @end deftypefn function L = loss (this, X, Y, varargin) ## Input validation if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactRegressionTree.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("CompactRegressionTree.loss: name-value arguments", ... " must be in pairs.")); endif if (! (isnumeric (X) && isreal (X) && ismatrix (X))) error ("CompactRegressionTree.loss: X must be a real numeric matrix."); endif if (! (isnumeric (Y) && isreal (Y) && isvector (Y))) error ("CompactRegressionTree.loss: Y must be a real numeric vector."); endif if (rows (X) != numel (Y)) error (strcat ("CompactRegressionTree.loss: number of rows in X", ... " and Y must be equal.")); endif ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights'}; dfValues = {'mse', []}; [LossFun, Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (is_function_handle (LossFun) || (ischar (LossFun) && isrow (LossFun)))) error (strcat ("CompactRegressionTree.loss: 'LossFun' must be a", ... " character vector or a function handle.")); endif if (ischar (LossFun) && ! strcmpi (LossFun, 'mse')) error ("CompactRegressionTree.loss: unsupported 'LossFun' value."); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("CompactRegressionTree.loss: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isvector (Weights) && isreal (Weights))) error (strcat ("CompactRegressionTree.loss: 'Weights' must be a", ... " real numeric vector.")); endif if (! isempty (Weights) && numel (Weights) != rows (X)) error (strcat ("CompactRegressionTree.loss: 'Weights' must have", ... " one element per observation.")); endif if (! isempty (args)) error ("CompactRegressionTree.loss: invalid optional paired argument."); endif if (isempty (Weights)) W = ones (rows (X), 1); else W = double (Weights(:)); endif ## An observation with no response is one the loss cannot be measured ## on, so it is dropped and the weights are normalized over what is ## left, which is what the fit itself does with such a row. Measured ## on R2024a, where the loss of a carsmall tree over the whole hundred ## rows is the loss over the ninety-four with a response. Y = double (Y(:)); keep = ! isnan (Y); Y = Y(keep); X = X(keep, :); W = W(keep); if (! (sum (W) > 0)) error (strcat ("CompactRegressionTree.loss: 'Weights' must not be", ... " zero for every observation with a response.")); endif ## Weights are normalized to sum to one, as MATLAB does, so a loss is ## a weighted average rather than a weighted sum. W = W / sum (W); yFit = predict (this, X); if (is_function_handle (LossFun)) L = LossFun (Y, yFit, W); if (! (isnumeric (L) && isscalar (L))) error (strcat ("CompactRegressionTree.loss:", ... " 'LossFun' must return a numeric scalar.")); endif else L = sum (W .* (Y - yFit) .^ 2); endif endfunction ## -*- texinfo -*- ## @deftypefn {CompactRegressionTree} {} savemodel (@var{obj}, @var{filename}) ## ## Save a CompactRegressionTree model to a file. ## ## @code{savemodel (@var{obj}, @var{filename})} saves each property of a ## CompactRegressionTree object into an Octave binary file, the name of ## which is specified in @var{filename}, along with an extra variable, ## which defines the type of regression object these variables ## constitute. Use @code{loadmodel} in order to load a regression object ## into Octave's workspace. ## ## @seealso{loadmodel, fitrtree, RegressionTree} ## @end deftypefn function savemodel (this, fname) ## Input validation if (nargin < 2) error (strcat ("CompactRegressionTree.savemodel: too few input", ... " arguments.")); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error (strcat ("CompactRegressionTree.savemodel: FNAME must be a", ... " character vector.")); endif ## Generate variable for class name classdef_name = 'CompactRegressionTree'; ## Create variables from model properties PredictorNames = this.PredictorNames; ResponseName = this.ResponseName; CategoricalPredictors = this.CategoricalPredictors; ExpandedPredictorNames = this.ExpandedPredictorNames; NumNodes = this.NumNodes; Children = this.Children; Parent = this.Parent; CutPredictorIndex = this.CutPredictorIndex; CutPoint = this.CutPoint; CutCategories = this.CutCategories; NodeSize = this.NodeSize; NodeMean = this.NodeMean; NodeError = this.NodeError; NodeProbability = this.NodeProbability; NodeRisk = this.NodeRisk; HeldRisk = this.HeldRisk; PruneList = this.PruneList; PruneAlpha = this.PruneAlpha; ResponseTransform = this.ResponseTransform; RTfun = this.RTfun; ## The cut descriptions are not saved: every one of them is re-derived ## from the node table above, so writing them out would only make a ## stale copy possible. save ('-binary', fname, 'classdef_name', 'PredictorNames', ... 'ResponseName', 'CategoricalPredictors', ... 'ExpandedPredictorNames', 'NumNodes', 'Children', 'Parent', ... 'CutPredictorIndex', 'CutPoint', 'CutCategories', ... 'NodeSize', 'NodeMean', ... 'NodeError', 'NodeProbability', 'NodeRisk', 'HeldRisk', ... 'PruneList', 'PruneAlpha', 'ResponseTransform', 'RTfun'); endfunction endmethods methods (Static, Hidden) function mdl = load_model (filename, data) ## The compact model is built from a full one and has no training data ## of its own, so the smallest fit the full class accepts is compacted ## and then filled property by property. mdl = CompactRegressionTree (RegressionTree ([1; 2], [1; 2])); ## Copy the saved data into the object. Iterate over what was saved ## rather than over fieldnames (mdl): a hidden property such as RTfun ## is written out by savemodel but is not reported by fieldnames, so ## comparing the two sets could never match. Assignment is legal here ## because this is a method of the class itself. names = fieldnames (data); order = {'ResponseTransform'}; late = ismember (names, order); tail = order(ismember (order, names)); names = [names(! late); tail(:)]; for ii = 1:numel (names) try mdl.(names{ii}) = data.(names{ii}); catch msg = strcat ("CompactRegressionTree.load_model: invalid model", ... " in '%s'."); error (msg, filename); end_try_catch endfor ## The cut descriptions were not saved, being derived mdl = fillCuts (mdl); endfunction endmethods methods (Access = private) ## The descriptions of the cuts, derived from the node table. function this = fillCuts (this) S = treeCutInfo (this.CutPredictorIndex, this.PredictorNames, ... this.CutCategories); for [val, name] = S this.(name) = val; endfor endfunction endmethods endclassdef ## Tests %!test # MATLAB parity: the surface a compact regression tree reports %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! CMdl = compact (RegressionTree (X, MPG)); %! assert_equal (class (CMdl), 'CompactRegressionTree'); %! assert_equal (numel (properties (CMdl)), 28); %! assert_equal (CMdl.NumNodes, 37); %! assert_equal (CMdl.ResponseName, 'Y'); %! assert_equal (CMdl.PredictorNames, {'x1', 'x2', 'x3'}); %! assert_equal (CMdl.ResponseTransform, 'none'); %! assert_equal (CMdl.CategoricalPredictors, []); %!test # The compact tree carries the node table the model it came from has %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG); %! CMdl = compact (Mdl); %! assert_equal (CMdl.Children, Mdl.Children); %! assert_equal (CMdl.Parent, Mdl.Parent); %! assert_equal (CMdl.NodeSize, Mdl.NodeSize); %! assert_equal (CMdl.NodeMean, Mdl.NodeMean, 1e-15); %! assert_equal (CMdl.NodeError, Mdl.NodeError, 1e-15); %! assert_equal (CMdl.NodeProbability, Mdl.NodeProbability, 1e-15); %! assert_equal (CMdl.NodeRisk, Mdl.NodeRisk, 1e-15); %! assert_equal (CMdl.PruneList, Mdl.PruneList); %! assert_equal (CMdl.PruneAlpha, Mdl.PruneAlpha, 1e-12); %! assert_equal (CMdl.CutPredictor, Mdl.CutPredictor); %! assert_equal (CMdl.CutType, Mdl.CutType); %!test # MATLAB parity: what a tree with no categories and no surrogates holds %! load carsmall %! CMdl = compact (RegressionTree ([Weight, Cylinders, Horsepower], MPG)); %! assert_equal (size (CMdl.CutCategories), [37, 2]); %! assert_equal (size (CMdl.CategoricalSplit), [0, 0]); %! assert_equal (size (CMdl.SurrogateCutPredictor), [0, 1]); %! assert_equal (size (CMdl.SurrogateCutPoint), [0, 0]); %!test # MATLAB parity: predict answers exactly as the full model does %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG); %! CMdl = compact (Mdl); %! [y1, n1] = predict (Mdl, X); %! [y2, n2] = predict (CMdl, X); %! assert_equal ({y1, n1}, {y2, n2}); %! assert_equal (predict (CMdl, X([1, 20, 60], :))', ... %! [17.25, 12.3333333333333, 29.1], 1e-12); %!test # MATLAB parity: loss, importance and the node ranges of a compact tree %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! CMdl = compact (RegressionTree (X, MPG)); %! assert_equal (loss (CMdl, X, MPG), 5.5828069902791, 1e-12); %! assert_equal (predictorImportance (CMdl), [2.5904, 0.1006, 0.5499], 1e-4); %! r = nodeVariableRange (CMdl, 4); %! assert_equal (r.x1, [-Inf, 3085.5], 1e-12); %! assert_equal (r.x3, [-Inf, 89], 1e-12); %! assert_equal (fieldnames (nodeVariableRange (CMdl, 1)), cell (0, 1)); %!test # MATLAB parity: the text form of a compact tree %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! CMdl = compact (RegressionTree (X, MPG, 'MinLeafSize', 15)); %! lines = strsplit (strtrim (evalc ('view (CMdl)')), "\n"); %! assert_equal (numel (lines), 10); %! assert_equal (lines{1}, 'Decision tree for regression'); %! assert_equal (lines{2}, ... %! '1 if x1<3085.5 then node 2 elseif x1>=3085.5 then node 3 else 23.7181'); %! assert_equal (lines{6}, '5 fit = 24.0882'); %!test # A node that holds rows back is discounted on the compact tree too %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG, 'MinLeafSize', 15); %! assert_equal (predictorImportance (compact (Mdl)), ... %! predictorImportance (Mdl), 1e-15); %! assert_equal (predictorImportance (compact (Mdl)), ... %! [11.253155105041, 0, 1.49831354224175], 1e-12); %!test # The response transform travels with the compact model %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! CMdl = compact (RegressionTree (X, MPG, 'ResponseTransform', 'exp')); %! assert_equal (CMdl.ResponseTransform, 'exp'); %! assert_equal (predict (CMdl, X(1, :)), exp (17.25), -1e-12); %! CMdl.ResponseTransform = @(y) 2 * y; %! assert_equal (predict (CMdl, X(1, :)), 34.5, 1e-12); %!test # A compact model saved and loaded answers exactly as it did %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! CMdl = compact (RegressionTree (X, MPG)); %! fname = tempname (); %! unwind_protect %! savemodel (CMdl, fname); %! New = loadmodel (fname); %! assert_equal (class (New), 'CompactRegressionTree'); %! assert_equal (New.NumNodes, CMdl.NumNodes); %! assert_equal (New.NodeRisk, CMdl.NodeRisk, 1e-15); %! assert_equal (New.CutPredictor, CMdl.CutPredictor); %! assert_equal (predict (New, X), predict (CMdl, X)); %! assert_equal (predictorImportance (New), ... %! predictorImportance (CMdl), 1e-15); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect ## Test input validation %!error %! CompactRegressionTree () %!error %! CompactRegressionTree (5) %!error %! predict (compact (RegressionTree (ones (4, 2), (1:4)'))) %!error %! predict (compact (RegressionTree (ones (4, 2), (1:4)')), []) %!error %! predict (compact (RegressionTree (ones (4, 2), (1:4)')), 'a') %!error %! predict (compact (RegressionTree (ones (4, 2), (1:4)')), ones (2, 5)) %!error %! loss (compact (RegressionTree (ones (4, 2), (1:4)')), ones (4, 2)) %!error %! loss (compact (RegressionTree (ones (4, 2), (1:4)')), ones (4, 2), ... %! (1:4)', 'LossFun', 'mad') %!error %! loss (compact (RegressionTree (ones (4, 2), (1:4)')), ones (4, 2), ... %! (1:4)', 'Weights', [1, 2]) %!error ... %! loss (compact (RegressionTree (ones (4, 2), (1:4)')), ones (4, 2), ... %! (1:4)', 'Bogus', 1) %!error %! nodeVariableRange (compact (RegressionTree (ones (4, 2), (1:4)')), 999) %!error %! savemodel (compact (RegressionTree (ones (4, 2), (1:4)'))) %!error %! savemodel (compact (RegressionTree (ones (4, 2), (1:4)')), 5) %!error %! CMdl = compact (RegressionTree (ones (4, 2), (1:4)')); %! CMdl.ResponseTransform = 'bogus'; %!test # a compact tree keeps and uses the level sets of its cuts %! k = (0:99)'; %! c = mod (k, 5) + 1; %! means = [3, 1, 4, 1.5, 5]; %! y = means(c)' + 0.1 * sin (k); %! Full = fitrtree (c, y, 'CategoricalPredictors', 1, 'MaxNumSplits', 2); %! Mdl = compact (Full); %! assert_equal (Mdl.CutCategories, Full.CutCategories); %! assert_equal (predict (Mdl, [1; 2; 6]), predict (Full, [1; 2; 6])); %! assert_equal (nodeVariableRange (Mdl, 3), struct ('x1', [1, 3, 5])); %! fname = [tempname(), '.mdl']; %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (predict (M2, [1; 2; 6]), predict (Mdl, [1; 2; 6])); ## A table at prediction %!shared crtT, crtM %! load fisheriris %! crtT = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! crtT.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! crtM = compact (fitrtree (crtT, 'SL')); %!test # the levels a predictor was coded through travel with the model %! assert_equal (numel (crtM.PredictorLevels), 4); %! assert_equal (crtM.PredictorLevels{4}, {'narrow', 'wide'}); %!test # predict takes a table, read by name and not by position %! a = predict (crtM, crtT); %! assert_equal (numel (a), 150); %! assert_equal (predict (crtM, crtT(:, [5, 4, 3, 2, 1])), a); %!test # a matrix is still taken %! load fisheriris %! CMdl = compact (fitrtree (meas(:,2:4), meas(:,1))); %! assert_equal (numel (predict (CMdl, meas(:,2:4))), 150); %!error ... %! predict (crtM, crtT(:, [1, 3, 4, 5])) ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,2:3); %! y = meas(:,1); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SW', 'PL'}); %! T.SL = y; %! Mdl = compact (fitrtree (T, 'SL')); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'SL'), a); %! assert_equal (loss (Mdl, T), a); ## Observation weights of class single or double %!error ... %! loss (compact (fitrtree ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)')), ones (4, ... %! 2), (1:4)', ... %! 'Weights', int8 ([1; 1; 1; 1])) statistics-release-2.1.2/inst/Machine_Learning/CompactTreeBagger.m000066400000000000000000001332241527311104300251620ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef CompactTreeBagger < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} CompactTreeBagger ## ## Compact ensemble of bagged decision trees ## ## A @code{CompactTreeBagger} object carries the trees a @code{TreeBagger} ## ensemble grew and what prediction needs, but not the observations it was ## fitted on, and so none of the out-of-bag information. It predicts new ## data identically to the ensemble it came from. ## ## Create one with the @code{compact} method of a @code{TreeBagger} object. ## Two compact ensembles fitted on the same classes are joined with ## @code{combine}. ## ## @seealso{TreeBagger, TreeBagger.compact, CompactClassificationTree, ## CompactRegressionTree} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CompactTreeBagger} {property} Method ## ## Type of the ensemble ## ## @qcode{'classification'} or @qcode{'regression'}. This property is ## read-only. ## ## @end deftp Method = 'classification'; ## -*- texinfo -*- ## @deftp {CompactTreeBagger} {property} NumTrees ## ## Number of trees ## ## A nonnegative integer, the number of trees in the ensemble. This ## property is read-only. ## ## @end deftp NumTrees = 0; ## -*- texinfo -*- ## @deftp {CompactTreeBagger} {property} Trees ## ## The trees of the ensemble ## ## A column cell array holding one @code{CompactClassificationTree} or ## @code{CompactRegressionTree} object per tree. This property is ## read-only. ## ## @end deftp Trees = {}; ## -*- texinfo -*- ## @deftp {CompactTreeBagger} {property} ClassNames ## ## Names of the classes ## ## The classes of a classification ensemble, in the type of the response ## it was fitted on and in the order its scores are laid out. Empty for a ## regression ensemble. This property is read-only. ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {CompactTreeBagger} {property} DefaultYfit ## ## Prediction for an observation no tree may answer for ## ## For classification, the class of greatest prior probability, in the ## type of the class names, or the missing label of that type after ## @code{setDefaultYfit} with @qcode{''}. For regression, the weighted ## mean of the training response unless set otherwise. This property is ## read-only; change it with @code{setDefaultYfit}. ## ## @end deftp DefaultYfit = []; ## -*- texinfo -*- ## @deftp {CompactTreeBagger} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors naming the columns of the predictor ## data. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {CompactTreeBagger} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A row vector of column indices into @var{X}, naming the predictors ## treated as categorical, empty when none is. This property is ## read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {CompactTreeBagger} {property} DeltaCriterionDecisionSplit ## ## Split criterion contributions of the predictors ## ## A row vector with one element per predictor, the mean over the trees ## of each tree's @code{predictorImportance}. This property is read-only. ## ## @end deftp DeltaCriterionDecisionSplit = []; ## -*- texinfo -*- ## @deftp {CompactTreeBagger} {property} NumPredictorSplit ## ## Decision splits on each predictor ## ## A row vector with one element per predictor, the sum over the trees of ## the share of each tree's branch nodes that split on the predictor. A ## tree without branch nodes adds nothing. This property is read-only. ## ## @end deftp NumPredictorSplit = []; ## -*- texinfo -*- ## @deftp {CompactTreeBagger} {property} SurrogateAssociation ## ## Predictive association between the predictors ## ## A square matrix with one row and one column per predictor. The trees ## grow no surrogate splits, so it is the identity matrix. This property ## is read-only. ## ## @end deftp SurrogateAssociation = []; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ResponseName = 'Y'; # name the table gave the response, which # a table call looks up; hidden as MATLAB's # TreeBagger carries no ResponseName TreeClassIdx = {}; # columns of ClassNames each tree's scores fill DefaultIndex = 0; # index of DefaultYfit into ClassNames, 0 if missing DefaultScore = []; # scores of an observation no tree may answer for endproperties methods (Hidden) function disp (this) fprintf ('\n CompactTreeBagger\n\n'); fprintf ('%16s: %s\n', 'Method', this.Method); fprintf ('%16s: %d\n', 'NumTrees', this.NumTrees); fprintf ('%16s: %d\n', 'NumPredictors', numel (this.PredictorNames)); if (strcmp (this.Method, 'classification')) fprintf ('%16s: %s\n', 'ClassNames', ... classNameListing (this.ClassNames)); endif fprintf ('\n'); endfunction ## -*- texinfo -*- ## @deftypefn {CompactTreeBagger} {@var{obj} =} CompactTreeBagger (@var{B}) ## ## Create a @code{CompactTreeBagger} object. ## ## @var{B} is the @code{TreeBagger} object to compact. The documented way ## to reach this constructor is the @code{compact} method. ## ## @seealso{TreeBagger, TreeBagger.compact} ## @end deftypefn function this = CompactTreeBagger (B) ## Input validation if (nargin < 1) error ("CompactTreeBagger: too few input arguments."); endif if (! isa (B, 'TreeBagger')) error ("CompactTreeBagger: B must be a TreeBagger object."); endif ## The levels a predictor read from a table was coded through ## travel with the model, so a compact one still reads a table, and ## so does the name the table gave the response this.PredictorLevels = B.PredictorLevels; this.ResponseName = B.ResponseName; this.Method = B.Method; this.NumTrees = B.NumTrees; this.Trees = B.Trees; this.ClassNames = B.ClassNames; this.DefaultYfit = B.DefaultYfit; this.PredictorNames = B.PredictorNames; this.CategoricalPredictors = B.CategoricalPredictors; this.DeltaCriterionDecisionSplit = B.DeltaCriterionDecisionSplit; this.NumPredictorSplit = B.NumPredictorSplit; this.SurrogateAssociation = B.SurrogateAssociation; this.TreeClassIdx = B.TreeClassIdx; this.DefaultIndex = B.DefaultIndex; this.DefaultScore = B.DefaultScore; endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {CompactTreeBagger} {@var{label} =} predict (@var{obj}, @var{X}) ## @deftypefnx {CompactTreeBagger} {[@var{label}, @var{scores}] =} predict (@dots{}) ## @deftypefnx {CompactTreeBagger} {[@var{label}, @var{scores}, @var{stdevs}] =} predict (@dots{}) ## @deftypefnx {CompactTreeBagger} {[@var{Yfit}, @var{stdevs}] =} predict (@dots{}) ## @deftypefnx {CompactTreeBagger} {@dots{} =} predict (@dots{}, @var{name}, @var{value}) ## ## Predict responses with a compact bagged ensemble. ## ## For a classification ensemble, @var{label} holds the predicted class of ## each row of @var{X}, in the type of @code{ClassNames}. @var{scores} is ## an @math{NxK} matrix, the weighted average over the trees of the class ## probability each tree gives, and @var{stdevs} holds their standard ## deviations over the trees. The label is the class of highest score, ## whatever cost matrix the trees were grown with, as MATLAB documents. ## ## For a regression ensemble, @var{Yfit} is the weighted average of the ## trees' predictions and @var{stdevs} their standard deviation. ## ## The standard deviations are population deviations, taken over the ## trees that answer for the observation with their weights. An ## observation no tree may answer for is given @code{DefaultYfit}, with ## the prior as its scores and @code{NaN} as its deviations. ## ## Name-Value arguments: ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Trees'} @tab @tab @qcode{'all'} (default) or a vector of ## indices of the trees to use. ## @item @qcode{'TreeWeights'} @tab @tab A nonnegative vector with one ## weight per tree used. The default weighs them equally. ## @item @qcode{'UseInstanceForTree'} @tab @tab An @math{NxNumTrees} ## logical matrix saying which tree may answer for which observation. The ## default lets every tree answer for every observation. ## @end multitable ## ## MATLAB returns the labels as a cell array of character vectors whatever ## the type of the response; they are returned here in the type of ## @code{ClassNames}, as by every other classifier in this package. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @seealso{CompactTreeBagger, CompactTreeBagger.error, TreeBagger.predict} ## @end deftypefn function [Yfit, scores, stdevs] = predict (this, X, varargin) if (nargin < 2) error ("CompactTreeBagger.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on X = tableColumns (this, 'CompactTreeBagger.predict', X); [Yfit, scores, stdevs] = bagPredict (this, X, varargin, ... 'CompactTreeBagger.predict', []); endfunction ## -*- texinfo -*- ## @deftypefn {CompactTreeBagger} {@var{err} =} error (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactTreeBagger} {@var{err} =} error (@dots{}, @var{name}, @var{value}) ## @deftypefnx {CompactTreeBagger} {@var{err} =} error (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactTreeBagger} {@var{err} =} error (@var{obj}, @var{Tbl}) ## ## Misclassification probability or mean squared error of the ensemble. ## ## For a classification ensemble @var{err} is the weighted share of the ## rows of @var{X} whose predicted class differs from @var{Y}; for a ## regression ensemble it is the weighted mean squared error. By default ## @var{err} is a column with one element per tree, the error of the ## first tree, then of the first two, and so on. ## ## An observation no tree may answer for is predicted as ## @code{DefaultYfit} and counted; when @code{DefaultYfit} is the missing ## label it has no prediction and is left out. ## ## Name-Value arguments: ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Mode'} @tab @tab @qcode{'cumulative'} (default), ## @qcode{'individual'} for the error of each tree on its own, or ## @qcode{'ensemble'} for a single error over every tree used. ## @item @qcode{'Trees'} @tab @tab @qcode{'all'} (default) or a vector of ## indices of the trees to use, in the order they are accumulated. ## @item @qcode{'TreeWeights'} @tab @tab A nonnegative vector with one ## weight per tree used. It may not be given in @qcode{'individual'} mode. ## @item @qcode{'UseInstanceForTree'} @tab @tab An @math{NxNumTrees} ## logical matrix saying which tree may answer for which observation. ## @item @qcode{'Weights'} @tab @tab A nonnegative vector with one weight ## per observation. The default is uniform. ## @end multitable ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{error (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{error (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{CompactTreeBagger, CompactTreeBagger.predict, ## CompactTreeBagger.meanMargin, TreeBagger.oobError} ## @end deftypefn function err = error (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactTreeBagger.error: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'error', X, Y, ... varargin, nargin > 2); err = bagLoss ('error', this, X, Y, varargin, ... 'CompactTreeBagger.error', [], []); endfunction ## -*- texinfo -*- ## @deftypefn {CompactTreeBagger} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactTreeBagger} {@var{m} =} margin (@dots{}, @var{name}, @var{value}) ## @deftypefnx {CompactTreeBagger} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactTreeBagger} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margin of each observation. ## ## The margin is the score of the true class less the highest score among ## the other classes. By default @var{m} has one row per observation and ## one column per tree, the margin of the first tree, then of the first ## two, and so on. An observation left without a prediction, as described ## under @code{CompactTreeBagger.error}, has a @code{NaN} margin. ## ## @qcode{'Mode'}, @qcode{'Trees'}, @qcode{'TreeWeights'} and ## @qcode{'UseInstanceForTree'} are taken as by ## @code{CompactTreeBagger.error}. A regression ensemble has no margins. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{CompactTreeBagger, CompactTreeBagger.meanMargin, ## CompactTreeBagger.error} ## @end deftypefn function m = margin (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactTreeBagger.margin: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'margin', X, Y, ... varargin, nargin > 2); m = bagLoss ('margin', this, X, Y, varargin, ... 'CompactTreeBagger.margin', [], []); endfunction ## -*- texinfo -*- ## @deftypefn {CompactTreeBagger} {@var{mm} =} meanMargin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {CompactTreeBagger} {@var{mm} =} meanMargin (@dots{}, @var{name}, @var{value}) ## @deftypefnx {CompactTreeBagger} {@var{mm} =} meanMargin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {CompactTreeBagger} {@var{mm} =} meanMargin (@var{obj}, @var{Tbl}) ## ## Weighted mean classification margin. ## ## @var{mm} is the weighted mean over the observations of the margins ## @code{CompactTreeBagger.margin} returns, a row with one element per ## tree by default. The Name-Value arguments are those of ## @code{CompactTreeBagger.error}, @qcode{'Weights'} included. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{meanMargin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} ## takes the response from the variable @var{ResponseVarName} names, and ## @code{meanMargin (@var{obj}, @var{Tbl})} from the variable the model ## was fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{CompactTreeBagger, CompactTreeBagger.margin} ## @end deftypefn function mm = meanMargin (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("CompactTreeBagger.meanMargin: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'meanMargin', X, Y, ... varargin, nargin > 2); mm = bagLoss ('meanMargin', this, X, Y, varargin, ... 'CompactTreeBagger.meanMargin', [], []); endfunction ## -*- texinfo -*- ## @deftypefn {CompactTreeBagger} {@var{C} =} combine (@var{C1}, @var{C2}) ## ## Join two compact ensembles. ## ## @var{C} holds the trees of @var{C1} followed by those of @var{C2}. The ## two must be of the same type and, for classification, have the same ## class names and the same default scores. @var{C} keeps the ## @code{DefaultYfit} of @var{C1}. ## ## @seealso{CompactTreeBagger, TreeBagger.append} ## @end deftypefn function this = combine (this, other) if (nargin < 2) error ("CompactTreeBagger.combine: too few input arguments."); endif if (! isa (other, 'CompactTreeBagger')) error (strcat ("CompactTreeBagger.combine: C2 must be a", ... " CompactTreeBagger object.")); endif if (! strcmp (this.Method, other.Method)) error (strcat ("CompactTreeBagger.combine: the two ensembles must", ... " be of the same type.")); endif if (! (isequal (this.ClassNames, other.ClassNames) && isequal (this.DefaultScore, other.DefaultScore))) error (strcat ("CompactTreeBagger.combine: the two ensembles must", ... " have the same classes and priors.")); endif if (! isequal (this.PredictorNames, other.PredictorNames)) error (strcat ("CompactTreeBagger.combine: the two ensembles must", ... " have the same predictors.")); endif this.Trees = [this.Trees; other.Trees]; this.TreeClassIdx = [this.TreeClassIdx; other.TreeClassIdx]; this.NumTrees = numel (this.Trees); [this.DeltaCriterionDecisionSplit, this.NumPredictorSplit] = ... bagSplitStats (this.Trees, numel (this.PredictorNames)); endfunction ## -*- texinfo -*- ## @deftypefn {CompactTreeBagger} {@var{C} =} setDefaultYfit (@var{obj}, @var{Yfit}) ## ## Set the prediction for an observation no tree may answer for. ## ## For a classification ensemble @var{Yfit} is @qcode{'MostPopular'}, the ## class of greatest prior probability, or @qcode{''}, the missing label, ## which leaves such an observation without a prediction and out of the ## error and the mean margin. A logical response has no missing label. ## For a regression ensemble @var{Yfit} is a numeric scalar. ## ## @seealso{CompactTreeBagger, CompactTreeBagger.predict} ## @end deftypefn function this = setDefaultYfit (this, Yfit) if (nargin < 2) error ("CompactTreeBagger.setDefaultYfit: too few input arguments."); endif if (strcmp (this.Method, 'classification')) if (ischar (Yfit) && isempty (Yfit)) if (islogical (this.ClassNames)) error (strcat ("CompactTreeBagger.setDefaultYfit: a logical", ... " response has no missing label, so YFIT must", ... " be 'MostPopular'.")); endif this.DefaultIndex = 0; this.DefaultYfit = missingLabels (this.ClassNames, 1); elseif (ischar (Yfit) && strcmpi (Yfit, 'MostPopular')) [~, this.DefaultIndex] = max (this.DefaultScore); this.DefaultYfit = labelsFromIndex (this.ClassNames, ... this.DefaultIndex); else error (strcat ("CompactTreeBagger.setDefaultYfit: YFIT must be", ... " '' or 'MostPopular' for a classification", ... " ensemble.")); endif else if (! (isnumeric (Yfit) && isscalar (Yfit) && isreal (Yfit))) error (strcat ("CompactTreeBagger.setDefaultYfit: YFIT must be", ... " a real numeric scalar for a regression", ... " ensemble.")); endif this.DefaultYfit = double (Yfit); endif endfunction ## -*- texinfo -*- ## @deftypefn {CompactTreeBagger} {@var{prox} =} proximity (@var{obj}, @var{X}) ## ## Proximity matrix of the observations. ## ## @var{prox} is a symmetric @math{NxN} matrix, @math{N} being the number ## of rows of @var{X}, whose element @math{(i,j)} is the share of the ## trees that bring observations @math{i} and @math{j} to the same leaf. ## Its diagonal holds ones. ## ## @var{X} may also be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: one ## the model was not fitted on is passed over, one it needs and cannot ## find is named, and a value holding a level is coded as that level was ## coded at fitting. ## ## @seealso{CompactTreeBagger, CompactTreeBagger.outlierMeasure, ## CompactTreeBagger.mdsprox, TreeBagger.fillprox} ## @end deftypefn function prox = proximity (this, X) if (nargin < 2) error ("CompactTreeBagger.proximity: too few input arguments."); endif o = proxArgs (this, X, {}, {}, 'CompactTreeBagger.proximity'); prox = o.P; endfunction ## -*- texinfo -*- ## @deftypefn {CompactTreeBagger} {@var{out} =} outlierMeasure (@var{obj}, @var{X}) ## @deftypefnx {CompactTreeBagger} {@var{out} =} outlierMeasure (@dots{}, @var{name}, @var{value}) ## ## Outlier measure of each observation. ## ## The raw measure of an observation is the size of its class divided by ## the sum of its squared proximities to the observations of the class, ## itself included, all the observations forming one class when no labels ## are given. @var{out} is a column holding, for each observation, the ## absolute deviation of its raw measure from the median of its class, ## divided by the median absolute deviation of the class. A large value ## marks an observation that the trees seldom group with the rest of its ## class. As in MATLAB, a class whose median absolute deviation is zero ## gives the raw measures themselves, and a class of one or two ## observations gives zeros. ## ## Name-Value arguments: ## ## @multitable @columnfractions 0.2 0.02 0.78 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Data'} @tab @tab @qcode{'predictors'} (default), for ## @var{X} holding predictor data, or @qcode{'proximity'}, for @var{X} ## holding a proximity matrix such as @code{proximity} returns. ## @item @qcode{'Labels'} @tab @tab The class label of each observation, ## each one of @code{ClassNames}. Classification only. ## @end multitable ## ## @var{X} holding predictor data may also be a table, whose variables ## are matched to the predictors the model was fitted on by name and not ## by position: one the model was not fitted on is passed over, one it ## needs and cannot find is named, and a value holding a level is coded as ## that level was coded at fitting. ## ## @seealso{CompactTreeBagger, CompactTreeBagger.proximity, ## TreeBagger.OutlierMeasure} ## @end deftypefn function out = outlierMeasure (this, X, varargin) if (nargin < 2) error ("CompactTreeBagger.outlierMeasure: too few input arguments."); endif o = proxArgs (this, X, varargin, {'Data', 'Labels'}, ... 'CompactTreeBagger.outlierMeasure'); out = bagOutlier (o.P, o.g); endfunction ## -*- texinfo -*- ## @deftypefn {CompactTreeBagger} {[@var{S}, @var{E}] =} mdsprox (@var{obj}, @var{X}) ## @deftypefnx {CompactTreeBagger} {[@var{S}, @var{E}] =} mdsprox (@dots{}, @var{name}, @var{value}) ## ## Multidimensional scaling of the proximity matrix. ## ## Applies classical multidimensional scaling, as @code{cmdscale} does, to ## the distances @code{1 - @var{prox}}, @var{prox} being the proximity ## matrix of the rows of @var{X}. @var{S} holds the scaled coordinates, ## one column per positive eigenvalue, and @var{E} the eigenvalues. ## ## Name-Value arguments: ## ## @multitable @columnfractions 0.25 0.02 0.73 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Data'} @tab @tab @qcode{'predictors'} (default) or ## @qcode{'proximity'}, as for @code{outlierMeasure}. ## @item @qcode{'Colors'} @tab @tab A character vector with one color ## letter per class. When given, the scaled coordinates are drawn as ## overlaid scatter plots, one per class, and a class beyond the number of ## letters is not drawn. ## @item @qcode{'Labels'} @tab @tab The class label of each observation, ## each one of @code{ClassNames}. Classification only. Without labels ## every observation is drawn in the first color. ## @item @qcode{'MDSCoordinates'} @tab @tab Two or three indices of the ## columns of @var{S} to draw. The default is @code{[1, 2]}. They must ## not exceed the number of columns of @var{S} even when nothing is drawn, ## as in MATLAB, whose documentation says otherwise. ## @end multitable ## ## @var{X} holding predictor data may also be a table, whose variables ## are matched to the predictors the model was fitted on by name and not ## by position: one the model was not fitted on is passed over, one it ## needs and cannot find is named, and a value holding a level is coded as ## that level was coded at fitting. ## ## @seealso{CompactTreeBagger, CompactTreeBagger.proximity, cmdscale, ## TreeBagger.mdsprox} ## @end deftypefn function [S, E] = mdsprox (this, X, varargin) if (nargin < 2) error ("CompactTreeBagger.mdsprox: too few input arguments."); endif o = proxArgs (this, X, varargin, ... {'Data', 'Colors', 'Labels', 'MDSCoordinates'}, ... 'CompactTreeBagger.mdsprox'); [S, E, errmsg] = bagMds (o.P, o.g, o.colors, o.coords); if (! isempty (errmsg)) error ("CompactTreeBagger.mdsprox: %s", errmsg); endif endfunction endmethods endclassdef ## The proximity matrix and the Name-Value arguments of the proximity methods. function o = proxArgs (M, X, args, allowed, caller) if (mod (numel (args), 2) != 0) error ("%s: name-value arguments must be in pairs.", caller); endif o = struct ('P', [], 'g', [], 'colors', '', 'coords', [1, 2]); isprox = false; labels = []; for i = 1:2:numel (args) name = args{i}; val = args{i+1}; if (! (ischar (name) && any (strcmpi (name, allowed)))) error ("%s: invalid parameter name in optional pair arguments.", ... caller); endif switch (tolower (name)) case 'data' if (! (ischar (val) && any (strcmpi (val, {'predictors', 'proximity'})))) error ("%s: 'Data' must be 'predictors' or 'proximity'.", caller); endif isprox = strcmpi (val, 'proximity'); case 'labels' labels = {val}; case 'colors' o.colors = val; case 'mdscoordinates' o.coords = val; endswitch endfor if (isprox) if (! (isnumeric (X) && isreal (X) && issquare (X) && ! isempty (X))) error (strcat ("%s: X must be a square real numeric matrix when", ... " 'Data' is 'proximity'."), caller); endif o.P = double (X); else ## A table is read by the names the model was fitted on X = tableToMatrix (M.PredictorNames, M.PredictorLevels, X, caller); if (! (isnumeric (X) && isreal (X) && ismatrix (X))) error ("%s: X must be a real numeric matrix.", caller); endif if (columns (X) != numel (M.PredictorNames)) error ("%s: X must have one column per predictor.", caller); endif o.P = bagProximity (M, X, 1:M.NumTrees); endif if (! isempty (labels)) lab = labels{1}; if (ischar (lab)) nL = rows (lab); else nL = numel (lab); endif if (! (isnumeric (lab) || islogical (lab) || ischar (lab) || iscellstr (lab) || isa (lab, 'categorical') || isa (lab, 'string')) || nL != rows (o.P)) error ("%s: 'Labels' must hold one class label per observation.", ... caller); endif if (! strcmp (M.Method, 'classification')) error ("%s: 'Labels' cannot be used with a regression ensemble.", ... caller); endif [o.g, errmsg] = labelIndices (M.ClassNames, lab); if (! isempty (errmsg)) error (strcat ("%s: 'Labels' must hold only classes the ensemble", ... " was trained on."), caller); endif endif endfunction %!demo %! ## Compact a random forest and classify new flowers with it. The compact %! ## ensemble keeps the trees but not the training data. %! load fisheriris %! rng (42); %! C = compact (TreeBagger (30, meas, species)); %! label = predict (C, [5.0, 3.4, 1.5, 0.2; 6.7, 3.0, 5.2, 2.3]) %!test # a compact ensemble predicts as the ensemble it came from %! load fisheriris %! rng (1); %! B = TreeBagger (5, meas, species); %! C = compact (B); %! assert_equal (class (C), 'CompactTreeBagger'); %! [la, sa] = predict (B, meas); %! [lc, sc] = predict (C, meas); %! assert_equal (lc, la); %! assert_equal (sc, sa); %! assert_equal (C.ClassNames, B.ClassNames); %! assert_equal (C.DefaultYfit, B.DefaultYfit); %!test # a compact ensemble keeps the ensemble's categorical predictors %! Xc = [1, 2; 2, 3; 3, 4; 1, 5; 2, 6; 3, 7; 1, 8; 2, 9]; %! yc = [1; 1; 2; 2; 1; 2; 1; 2]; %! C = compact (TreeBagger (3, Xc, yc, 'CategoricalPredictors', 1)); %! assert_equal (C.CategoricalPredictors, 1); %! C = compact (TreeBagger (3, Xc, yc)); %! assert_equal (C.CategoricalPredictors, []); %!test # MATLAB parity: combining keeps the first ensemble's default %! load fisheriris %! rng (1); %! C1 = setDefaultYfit (compact (TreeBagger (3, meas, species)), ''); %! C2 = compact (TreeBagger (4, meas, species)); %! C = combine (C1, C2); %! assert_equal (C.NumTrees, 7); %! assert_equal (C.DefaultYfit, {''}); %!test # a missing default leaves an observation without a prediction %! load fisheriris %! rng (1); %! C = setDefaultYfit (compact (TreeBagger (4, meas, species)), ''); %! U = true (150, 4); %! U(1,:) = false; %! label = predict (C, meas(1:2,:), 'UseInstanceForTree', U(1:2,:)); %! assert_equal (label(1), {''}); %! m = margin (C, meas(1:2,:), species(1:2), 'Mode', 'ensemble', ... %! 'UseInstanceForTree', U(1:2,:)); %! assert_equal (isnan (m), [true; false]); %! miss = ! strcmp (predict (C, meas(2:end,:)), species(2:end)); %! e = error (C, meas, species, 'Mode', 'ensemble', 'UseInstanceForTree', U); %! assert_equal (e, sum (miss) / 149, 1e-15); %!test # 'MostPopular' restores the class of greatest prior probability %! load fisheriris %! rng (1); %! C = setDefaultYfit (compact (TreeBagger (2, meas, species)), ''); %! C = setDefaultYfit (C, 'MostPopular'); %! assert_equal (C.DefaultYfit, {'setosa'}); %!test # a regression default is any numeric scalar %! load fisheriris %! rng (1); %! C = compact (TreeBagger (2, meas(:,2:4), meas(:,1), 'Method', 'regression')); %! C = setDefaultYfit (C, 3); %! assert_equal (C.DefaultYfit, 3); %!test # MATLAB parity: combining adds up the split counts %! load fisheriris %! rng (1); %! C1 = compact (TreeBagger (3, meas, species)); %! C2 = compact (TreeBagger (4, meas, species)); %! C = combine (C1, C2); %! assert_equal (C.NumPredictorSplit, ... %! C1.NumPredictorSplit + C2.NumPredictorSplit, 1e-14); %! assert_equal (C.DeltaCriterionDecisionSplit, ... %! (3 * C1.DeltaCriterionDecisionSplit ... %! + 4 * C2.DeltaCriterionDecisionSplit) / 7, 1e-14); %! assert_equal (C.SurrogateAssociation, eye (4)); %!test # MATLAB parity: proximity is the share of trees sharing a leaf %! load fisheriris %! rng (1); %! C = compact (TreeBagger (6, meas, species, 'MinLeafSize', 5)); %! X = meas(1:3:end,:); %! P = zeros (rows (X)); %! for t = 1:6 %! [~, ~, nd] = predict (C.Trees{t}, X); %! P += nd == nd'; %! endfor %! prox = proximity (C, X); %! assert_equal (prox, P / 6, 1e-15); %! assert_equal (diag (prox), ones (50, 1)); %!test # a regression ensemble has proximities too %! load fisheriris %! rng (1); %! C = compact (TreeBagger (3, meas(:,2:4), meas(:,1), 'Method', 'regression')); %! [~, n1] = predict (C.Trees{1}, meas(1:2,2:4)); %! [~, n2] = predict (C.Trees{2}, meas(1:2,2:4)); %! [~, n3] = predict (C.Trees{3}, meas(1:2,2:4)); %! p12 = mean ([n1(1) == n1(2), n2(1) == n2(2), n3(1) == n3(2)]); %! assert_equal (proximity (C, meas(1:2,2:4)), [1, p12; p12, 1], 1e-15); %!test # MATLAB parity: the outlier measure within each class %! load fisheriris %! rng (1); %! C = compact (TreeBagger (8, meas, species, 'MinLeafSize', 5)); %! P = proximity (C, meas); %! g = grp2idx (species); %! om = zeros (150, 1); %! for k = 2:3 %! raw = 50 ./ sum (P(g == k, g == k) .^ 2, 2); %! om(g == k) = abs (raw - median (raw)) / median (abs (raw - median (raw))); %! endfor %! out = outlierMeasure (C, meas, 'Labels', species); %! assert_equal (out(g > 1), om(g > 1), 1e-12); %!test # MATLAB parity: a proximity matrix may be given in place of the data %! load fisheriris %! rng (1); %! C = compact (TreeBagger (8, meas, species, 'MinLeafSize', 5)); %! P = proximity (C, meas); %! assert_equal (outlierMeasure (C, P, 'Data', 'proximity', ... %! 'Labels', species), ... %! outlierMeasure (C, meas, 'Labels', species)); %!test # MATLAB parity: without labels every observation is one class %! C = compact (TreeBagger (1, [1; 2; 3; 4], [1; 1; 2; 2])); %! P = [1, 0.2, 0.6, 0.1, 0.3; 0.2, 1, 0.4, 0.7, 0.5; ... %! 0.6, 0.4, 1, 0.2, 0.8; 0.1, 0.7, 0.2, 1, 0.3; ... %! 0.3, 0.5, 0.8, 0.3, 1]; %! assert_equal (outlierMeasure (C, P, 'Data', 'proximity'), ... %! [2.482051282051284; 0; 1; 1.609249646059462; ... %! 0.531400966183575], 1e-12); %!test # MATLAB parity: a zero median absolute deviation gives the raw measure %! C = compact (TreeBagger (1, [1; 2; 3; 4], [1; 1; 2; 2])); %! P = [1, 1, 1, 0.5; 1, 1, 1, 0.5; 1, 1, 1, 0.5; 0.5, 0.5, 0.5, 1]; %! assert_equal (outlierMeasure (C, P, 'Data', 'proximity'), ... %! [1.230769230769231; 1.230769230769231; ... %! 1.230769230769231; 2.285714285714286], 1e-12); %! assert_equal (outlierMeasure (C, [1, 1, 0; 1, 1, 0; 0, 0, 1], ... %! 'Data', 'proximity'), [1.5; 1.5; 3]); %!test # MATLAB parity: a class of one or two observations gives zeros %! load fisheriris %! C = compact (TreeBagger (1, meas, species)); %! P = [1, 1, 1, 0, 0, 0; 1, 1, 1, 0, 0, 0; 1, 1, 1, 0, 0, 0; ... %! 0, 0, 0, 1, 1, 1; 0, 0, 0, 1, 1, 1; 0, 0, 0, 1, 1, 1]; %! lab = {'setosa'; 'setosa'; 'virginica'; 'virginica'; 'virginica'; ... %! 'virginica'}; %! assert_equal (outlierMeasure (C, P, 'Data', 'proximity', 'Labels', lab), ... %! [0; 0; 4; 4/3; 4/3; 4/3], 1e-15); %! assert_equal (outlierMeasure (C, [1, 0.5; 0.5, 0.8], 'Data', ... %! 'proximity'), [0; 0]); %!test # MATLAB parity: scaling applies cmdscale to one less the proximity %! load fisheriris %! rng (1); %! C = compact (TreeBagger (5, meas, species, 'MinLeafSize', 5)); %! X = meas(1:5:end,:); %! [S, E] = mdsprox (C, X); %! [S0, E0] = cmdscale (1 - proximity (C, X)); %! assert_equal (S, S0); %! assert_equal (E, E0); %! [S1, E1] = mdsprox (C, proximity (C, X), 'Data', 'proximity'); %! assert_equal (S1, S0); %!test # the scaled coordinates are drawn one class per color %! load fisheriris %! rng (1); %! C = compact (TreeBagger (5, meas, species, 'MinLeafSize', 5)); %! h = figure ('visible', 'off'); %! unwind_protect %! mdsprox (C, meas, 'Colors', 'rb', 'Labels', species); %! kids = get (gca, 'children'); %! assert_equal (numel (kids), 2); %! assert_equal (sort (cellfun (@numel, get (kids, 'xdata'))), [50; 50]); %! assert_equal (ishold (), false); %! unwind_protect_cleanup %! close (h); %! end_unwind_protect %!test # three coordinates are drawn in three dimensions %! load fisheriris %! rng (1); %! C = compact (TreeBagger (5, meas, species, 'MinLeafSize', 5)); %! h = figure ('visible', 'off'); %! unwind_protect %! S = mdsprox (C, meas, 'Colors', 'k', 'MDSCoordinates', [1, 2, 3]); %! kids = get (gca, 'children'); %! assert_equal (numel (kids), 1); %! assert_equal (get (kids, 'zdata')(:), S(:,3)); %! unwind_protect_cleanup %! close (h); %! end_unwind_protect ## Test input validation %!shared x, y, C, R %! load fisheriris %! x = meas; %! y = species; %! C = compact (TreeBagger (2, x, y)); %! R = compact (TreeBagger (2, x(:,2:4), x(:,1), 'Method', 'regression')); %!error CompactTreeBagger () %!error CompactTreeBagger (1) %!error predict (C) %!error margin (C, x) %!error ... %! meanMargin (C, x) ## Octave's test.m cuts a message through its first 'error:' before matching ## it, so these patterns hold the message after the method's prefix. %!error error (C, x) %!error combine (C) %!error ... %! combine (C, 1) %!error ... %! combine (C, R) %!error ... %! combine (C, compact (TreeBagger (1, x, y, 'Prior', 'uniform', ... %! 'ClassNames', {'virginica'; 'versicolor'; 'setosa'}))) %!error ... %! combine (C, compact (TreeBagger (1, x, y, 'PredictorNames', ... %! {'a', 'b', 'c', 'd'}))) %!error ... %! setDefaultYfit (C) %!error ... %! setDefaultYfit (compact (TreeBagger (1, x, strcmp (y, 'setosa'))), '') %!error ... %! setDefaultYfit (C, 'setosa') %!error ... %! setDefaultYfit (R, 'mean') %!error proximity (C) %!error ... %! proximity (C, {1}) %!error ... %! proximity (C, ones (2, 3)) %!error ... %! outlierMeasure (C) %!error ... %! outlierMeasure (C, x, 'Data') %!error ... %! outlierMeasure (C, x, 'Colors', 'r') %!error ... %! outlierMeasure (C, x, 'Data', 'distance') %!error ... %! outlierMeasure (C, x, 'Data', 'proximity') %!error ... %! outlierMeasure (C, x, 'Labels', y(1:10)) %!error mdsprox (C) %!error ... %! mdsprox (C, x(1:10,:), 'Colors', 1) %!error ... %! mdsprox (C, x(1:10,:), 'MDSCoordinates', 1) %!error ... %! mdsprox (C, x(1:10,:), 'Colors', 'r', 'MDSCoordinates', [1, 200]) %!error ... %! mdsprox (C, x(1:10,:), 'MDSCoordinates', [1, 200]) %!error ... %! outlierMeasure (C, x(1:2,:), 'Labels', {'rose'; 'setosa'}) %!error ... %! outlierMeasure (C, x(1:2,:), 'Labels', [1; 2]) %!error ... %! outlierMeasure (R, x(1:2,2:4), 'Labels', [1; 2]) %!error ... %! mdsprox (C, x(1:2,:), 'Labels', {''; 'setosa'}) ## A table at prediction %!test # the levels travel with the model, and predict reads a table by name %! load fisheriris %! T = table (meas(:,1), meas(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! T.Species = categorical (species); %! B = TreeBagger (20, T, 'Species'); %! CB = compact (B); %! assert_equal (CB.PredictorLevels, B.PredictorLevels); %! assert_equal (predict (CB, T(:, [4, 3, 2, 1])), predict (CB, T)); ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (TreeBagger (20, T, 'Species')); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## A table at error and meanMargin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (TreeBagger (20, T, 'Species')); %! a = error (Mdl, X, y); %! assert_equal (error (Mdl, T(:,1:2), y), a); %! assert_equal (error (Mdl, T, 'Species'), a); %! assert_equal (error (Mdl, T), a); %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = compact (TreeBagger (20, T, 'Species')); %! a = meanMargin (Mdl, X, y); %! assert_equal (meanMargin (Mdl, T(:,1:2), y), a); %! assert_equal (meanMargin (Mdl, T, 'Species'), a); %! assert_equal (meanMargin (Mdl, T), a); ## A table at proximity, outlierMeasure and mdsprox %!test # a table is matched to the predictors by name %! load fisheriris %! T = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species); %! Mdl = compact (TreeBagger (20, T, 'Species')); %! a = proximity (Mdl, meas); %! assert_equal (proximity (Mdl, T(:,1:4)), a); %! assert_equal (proximity (Mdl, T(:,[5, 4, 2, 3, 1])), a); %!test # a table is matched to the predictors by name %! load fisheriris %! T = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species); %! Mdl = compact (TreeBagger (20, T, 'Species')); %! a = outlierMeasure (Mdl, meas, 'Labels', species); %! assert_equal (outlierMeasure (Mdl, T(:,1:4), 'Labels', species), a); %! assert_equal (outlierMeasure (Mdl, T(:,[5, 4, 2, 3, 1]), ... %! 'Labels', species), a); %!test # a table is matched to the predictors by name %! load fisheriris %! T = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species); %! Mdl = compact (TreeBagger (20, T, 'Species')); %! a = mdsprox (Mdl, meas); %! assert_equal (mdsprox (Mdl, T(:,1:4)), a); %! assert_equal (mdsprox (Mdl, T(:,[5, 4, 2, 3, 1])), a); statistics-release-2.1.2/inst/Machine_Learning/PredictiveModel.m000066400000000000000000000204411527311104300247170ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef (Abstract) PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} PredictiveModel ## ## A fitted model that answers for new observations. ## ## @code{PredictiveModel} is the superclass of every classification and ## regression model in this package that carries a @code{predict} method, ## and of nothing else. The models fitted by @code{fitlm} and its ## relatives derive from it as the learners do. The cross-validated ## classes answer through @code{kfoldPredict} over their folds rather than ## through @code{predict}, and so do not derive from it. ## ## The class is abstract and cannot be instantiated. It is where ## behaviour shared by all those models is written once rather than once ## per class, and @code{isa (@var{obj}, @qcode{'PredictiveModel'})} is how ## to ask whether an object is a model that can be told to predict. The ## only data it holds is the levels a predictor read from a table was ## coded through, which every model needs to read a table again at ## prediction and which MATLAB carries by keeping the table itself. ## ## @end deftp ## This file must live in a directory that sorts before every directory ## holding a subclass of it: Machine_Learning before Regression, today. ## Octave below 11.2.0 rebuilds every doc-cache when the package is ## installed, one directory at a time and with only that directory on the ## path, so a subclass in another directory can resolve this class only ## where the walk has already met it. genpath sorts, and a class stays ## resolved once it has been, so the earlier directory settles it. The ## last test in this file fails if that ever stops being true. ## ## The shared behaviour of the models goes here. The block stays while it ## is empty: a class body holding no block at all gets no help text, help ## synthesising a default constructor instead of reading the class block ## above. properties (GetAccess = public, SetAccess = protected, Hidden) ## One cell per predictor, holding the levels a predictor read from a ## table was coded through and empty for one read from numbers. It is ## hidden because MATLAB's models carry no such property, keeping the ## table itself instead. PredictorLevels = {}; endproperties methods (Access = protected) ## Resolve table input into the predictors and the response, keeping ## the levels the coding used. A constructor calls this before it ## validates anything, and a matrix passes through untouched. ORDNUM ## makes an ordinal column a numeric predictor, as MATLAB's trees, ## ensembles and generalized additive models take it. function [this, X, Y, args] = resolveTable (this, caller, X, Y, args, ... ordnum = false) if (! istable (X)) return; endif [X, Y, args, lev, errmsg] = tableFrame (X, Y, args, ordnum); if (! isempty (errmsg)) error ("%s: %s", caller, errmsg); endif this.PredictorLevels = lev; endfunction ## Map a table onto the predictors this model was fitted on. The work ## is a private function rather than this method alone, because an ## explainer holds a model without being one and needs the same mapping. function Z = tableColumns (this, caller, T) Z = tableToMatrix (this.PredictorNames, this.PredictorLevels, T, caller); endfunction ## Resolve table input into the predictors and the response that loss, ## margin, edge and their kin take. METH is the method's own name, ## GIVEN says whether the call gave a third argument at all, and ARGS ## are the ones after it. A matrix passes through untouched. function [X, Y, args] = tableResponse (this, meth, X, Y, args, given) if (! istable (X)) return; endif caller = sprintf ('%s.%s', class (this), meth); ## The argument after the table is the response where an odd number ## of arguments follows the table, and a name-value name where an ## even number does, pairs being even. Then the response is the ## variable the model was fitted on. if (mod (given + numel (args), 2) == 0) if (given) args = [{Y}, args]; endif Y = this.ResponseName; endif ## A name is looked up among the table's variables; anything else is ## the response itself, given beside the table if ((ischar (Y) && isrow (Y)) || (isa (Y, 'string') && isscalar (Y))) name = char (Y); if (! any (strcmp (X.Properties.VariableNames, name))) error ("%s: the table holds no variable '%s'.", caller, name); endif Y = X.(name); endif X = tableColumns (this, caller, X); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {PredictiveModel} {@var{pd} =} partialDependence (@var{obj}, @var{Vars}) ## @deftypefnx {PredictiveModel} {@var{pd} =} partialDependence (@var{obj}, @var{Vars}, @var{Labels}) ## @deftypefnx {PredictiveModel} {@var{pd} =} partialDependence (@dots{}, @var{Data}) ## @deftypefnx {PredictiveModel} {@var{pd} =} partialDependence (@dots{}, @var{name}, @var{value}) ## @deftypefnx {PredictiveModel} {[@var{pd}, @var{x}, @var{y}] =} partialDependence (@dots{}) ## ## Compute partial dependence. ## ## @code{partialDependence (@var{obj}, @var{Vars})} is the same call as ## @code{partialDependence} the function, written on the model instead of ## before it, and takes and returns exactly what it does. See ## @code{partialDependence} for the arguments and for what @var{pd}, ## @var{x} and @var{y} hold. ## ## @seealso{partialDependence, plotPartialDependence} ## @end deftypefn function varargout = partialDependence (this, varargin) [varargout{1:max(nargout, 1)}] = pdCompute ('partialDependence', ... this, varargin{:}); endfunction endmethods methods (Hidden) ## Print the variable name, then the subclass's own disp function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ('%s =\n', in_name); endif disp (this); endfunction endmethods endclassdef %!test # the class is abstract and cannot be instantiated %! fail ('PredictiveModel ()', 'abstract'); %!test # a model that predicts derives from it, a cross-validated one does not %! load fisheriris %! Mdl = fitctree (meas, species); %! assert_equal (isa (Mdl, 'PredictiveModel'), true); %! assert_equal (isa (compact (Mdl), 'PredictiveModel'), true); %! assert_equal (isa (crossval (Mdl, 'KFold', 2), 'PredictiveModel'), false); %!test # a model fitted by fitlm derives from it too, across directories %! X = [1, 2; 2, 3; 3, 4; 1, 5; 2, 6; 3, 7]; %! y = [2.5; 3.1; 4.8; 2.2; 3.9; 5.1]; %! assert_equal (isa (fitlm (X, y), 'PredictiveModel'), true); %!test # no subclass may sit in a directory sorting before this class's own %! base = fileparts (which ('PredictiveModel')); %! root = fileparts (base); %! here = base(numel (root) + 2:end); %! d = dir (root); %! subs = {d([d.isdir]).name}; %! subs = subs(! ismember (subs, {'.', '..', here})); %! for k = 1:numel (subs) %! f = dir (fullfile (root, subs{k}, '*.m')); %! for j = 1:numel (f) %! src = fileread (fullfile (root, subs{k}, f(j).name)); %! pat = '^classdef[^\n]*<[^\n]*PredictiveModel'; %! if (! isempty (regexp (src, pat, 'once', 'lineanchors'))) %! order = sort ({here, subs{k}}); %! assert_equal (order{1}, here); %! endif %! endfor %! endfor statistics-release-2.1.2/inst/Machine_Learning/RegressionBaggedEnsemble.m000066400000000000000000000621731527311104300265350ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef RegressionBaggedEnsemble < RegressionEnsemble ## -*- texinfo -*- ## @deftp {statistics} RegressionBaggedEnsemble ## ## Bagged ensemble of regression trees ## ## A @code{RegressionBaggedEnsemble} object holds regression trees each ## grown on a sample drawn from the training data in proportion to the ## observation weights, each split chosen from @code{ceil (P / 3)} ## predictors drawn afresh at every node. It predicts by averaging its ## trees. ## ## Create one with @code{fitrensemble} and @qcode{'Method'} set to ## @qcode{'Bag'}. It carries everything a @code{RegressionEnsemble} does, ## and which rows each tree drew. LSBoost that resamples, asked for with ## @qcode{'Resample'}, @qcode{'FResample'} or @qcode{'Replace'}, returns this ## class too: its trees are boosted and summed by their weights, each fitted ## on the rows it drew. ## ## @seealso{fitrensemble, RegressionEnsemble, CompactRegressionEnsemble, ## TreeBagger} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {RegressionBaggedEnsemble} {property} FResample ## ## Share of the observations each tree draws ## ## A number greater than 0 and no greater than 1, each tree drawing ## @code{ceil (FResample * N)} observations. This property is read-only. ## ## @end deftp FResample = 1; ## -*- texinfo -*- ## @deftp {RegressionBaggedEnsemble} {property} Replace ## ## Whether the trees draw with replacement ## ## A logical scalar, true by default. This property is read-only. ## ## @end deftp Replace = true; ## -*- texinfo -*- ## @deftp {RegressionBaggedEnsemble} {property} UseObsForLearner ## ## Which observations each tree drew ## ## An @math{NxNumTrained} logical matrix, true where a tree's sample holds ## an observation. This property is read-only. ## ## @end deftp UseObsForLearner = []; endproperties methods (Access = public) ## -*- texinfo -*- ## @deftypefn {RegressionBaggedEnsemble} {@var{obj} =} RegressionBaggedEnsemble (@var{X}, @var{Y}) ## @deftypefnx {RegressionBaggedEnsemble} {@var{obj} =} RegressionBaggedEnsemble (@dots{}, @var{name}, @var{value}) ## ## Fit a bagged ensemble of regression trees. ## ## @code{fitrensemble} with @qcode{'Method'} set to @qcode{'Bag'} is the ## documented way in, and its help lists the options both take. ## ## @seealso{fitrensemble, RegressionEnsemble} ## @end deftypefn function this = RegressionBaggedEnsemble (X, Y, varargin) if (nargin < 2) error ("RegressionBaggedEnsemble: too few input arguments."); endif ## A table names its own predictors and says which hold levels ## An ordinal column is a numeric predictor here, as in R2024a [this, X, Y, varargin] = resolveTable (this, 'RegressionBaggedEnsemble', ... X, Y, varargin, true); if (! any (cellfun (@(a) ischar (a) && strcmpi (a, 'Method'), ... varargin(1:2:end)))) varargin(end+1:end+2) = {'Method', 'Bag'}; endif this = this@RegressionEnsemble (X, Y, varargin{:}); this = bagProperties (this); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionBaggedEnsemble} {@var{CVMdl} =} crossval (@var{obj}) ## @deftypefnx {RegressionBaggedEnsemble} {@var{CVMdl} =} crossval (@dots{}, @var{name}, @var{value}) ## ## Cross-validate a bagged ensemble. ## ## Behaves as @code{RegressionEnsemble.crossval}, returning a ## @code{RegressionPartitionedEnsemble}. ## ## @seealso{RegressionBaggedEnsemble, RegressionPartitionedEnsemble} ## @end deftypefn function CVMdl = crossval (this, varargin) CVMdl = crossval@RegressionEnsemble (this, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionBaggedEnsemble} {@var{B} =} regularize (@var{obj}) ## @deftypefnx {RegressionBaggedEnsemble} {@var{B} =} regularize (@dots{}, @var{name}, @var{value}) ## ## Find lasso weights for the trees of a bagged ensemble. ## ## Behaves as @code{RegressionEnsemble.regularize}. ## ## @seealso{RegressionBaggedEnsemble, RegressionEnsemble.regularize} ## @end deftypefn function this = regularize (this, varargin) this = regularize@RegressionEnsemble (this, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionBaggedEnsemble} {@var{C} =} shrink (@var{obj}) ## @deftypefnx {RegressionBaggedEnsemble} {@var{C} =} shrink (@dots{}, @var{name}, @var{value}) ## ## Keep the trees of a bagged ensemble that a lasso weight retains. ## ## Behaves as @code{RegressionEnsemble.shrink}, returning a ## @code{CompactRegressionEnsemble}. ## ## @seealso{RegressionBaggedEnsemble, RegressionEnsemble.shrink} ## @end deftypefn function C = shrink (this, varargin) C = shrink@RegressionEnsemble (this, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionBaggedEnsemble} {[@var{vals}, @var{nlearn}] =} cvshrink (@var{obj}) ## @deftypefnx {RegressionBaggedEnsemble} {[@var{vals}, @var{nlearn}] =} cvshrink (@dots{}, @var{name}, @var{value}) ## ## Cross-validate the shrinking of a bagged ensemble. ## ## Behaves as @code{RegressionEnsemble.cvshrink}. ## ## @seealso{RegressionBaggedEnsemble, RegressionEnsemble.cvshrink} ## @end deftypefn function [vals, nlearn] = cvshrink (this, varargin) [vals, nlearn] = cvshrink@RegressionEnsemble (this, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionBaggedEnsemble} {@var{CMdl} =} compact (@var{obj}) ## ## Drop the training data from a bagged regression ensemble. ## ## Returns a @code{CompactRegressionEnsemble}, as ## @code{RegressionEnsemble.compact} does. ## ## @seealso{RegressionBaggedEnsemble, CompactRegressionEnsemble} ## @end deftypefn function CMdl = compact (this) CMdl = compact@RegressionEnsemble (this); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionBaggedEnsemble} {@var{B} =} resume (@var{obj}, @var{NumLearningCycles}) ## @deftypefnx {RegressionBaggedEnsemble} {@var{B} =} resume (@dots{}, 'NPrint', @var{n}) ## ## Grow more trees in a bagged regression ensemble. ## ## Behaves as @code{RegressionEnsemble.resume}, the new trees' samples ## added to @code{UseObsForLearner}. ## ## @seealso{RegressionBaggedEnsemble, RegressionEnsemble.resume} ## @end deftypefn function this = resume (this, varargin) this = resume@RegressionEnsemble (this, varargin{:}); this = bagProperties (this); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionBaggedEnsemble} {@var{yfit} =} predict (@var{obj}, @var{X}) ## @deftypefnx {RegressionBaggedEnsemble} {@var{yfit} =} predict (@dots{}, @var{name}, @var{value}) ## ## Predict the response with a bagged regression ensemble. ## ## Behaves as @code{CompactRegressionEnsemble.predict}. ## ## @seealso{RegressionBaggedEnsemble, CompactRegressionEnsemble.predict} ## @end deftypefn function yfit = predict (this, varargin) yfit = predict@RegressionEnsemble (this, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionBaggedEnsemble} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {RegressionBaggedEnsemble} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {RegressionBaggedEnsemble} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {RegressionBaggedEnsemble} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Regression loss of a bagged ensemble. ## ## Behaves as @code{CompactRegressionEnsemble.loss}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{RegressionBaggedEnsemble, CompactRegressionEnsemble.loss} ## @end deftypefn function L = loss (this, varargin) L = loss@RegressionEnsemble (this, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionBaggedEnsemble} {@var{yfit} =} resubPredict (@var{obj}) ## @deftypefnx {RegressionBaggedEnsemble} {@var{yfit} =} resubPredict (@dots{}, @var{name}, @var{value}) ## ## Predict the response of the training data with a bagged ensemble. ## ## Behaves as @code{RegressionEnsemble.resubPredict}. ## ## @seealso{RegressionBaggedEnsemble, RegressionEnsemble.resubPredict} ## @end deftypefn function yfit = resubPredict (this, varargin) yfit = resubPredict@RegressionEnsemble (this, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionBaggedEnsemble} {@var{L} =} resubLoss (@var{obj}) ## @deftypefnx {RegressionBaggedEnsemble} {@var{L} =} resubLoss (@dots{}, @var{name}, @var{value}) ## ## Regression loss of a bagged ensemble on the training data. ## ## Behaves as @code{RegressionEnsemble.resubLoss}. ## ## @seealso{RegressionBaggedEnsemble, RegressionEnsemble.resubLoss} ## @end deftypefn function L = resubLoss (this, varargin) L = resubLoss@RegressionEnsemble (this, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionBaggedEnsemble} {@var{imp} =} predictorImportance (@var{obj}) ## @deftypefnx {RegressionBaggedEnsemble} {[@var{imp}, @var{ma}] =} predictorImportance (@var{obj}) ## ## Estimate the importance of each predictor. ## ## The mean over the trees of each tree's @code{predictorImportance}, as ## @code{CompactRegressionEnsemble.predictorImportance} computes it. ## ## @seealso{RegressionBaggedEnsemble, ## RegressionBaggedEnsemble.oobPermutedPredictorImportance} ## @end deftypefn function [imp, ma] = predictorImportance (this) [imp, ma] = predictorImportance@RegressionEnsemble (this); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionBaggedEnsemble} {@var{yfit} =} oobPredict (@var{obj}) ## @deftypefnx {RegressionBaggedEnsemble} {@var{yfit} =} oobPredict (@dots{}, 'Learners', @var{idx}) ## ## Out-of-bag predictions for the training data. ## ## Each training observation is predicted by the trees whose samples left ## it out, as @code{predict} predicts it with @qcode{'UseObsForLearner'} ## set to @code{! UseObsForLearner}; one in the sample of every tree used ## is @code{NaN}. @qcode{'Learners'} restricts the trees. ## ## @seealso{RegressionBaggedEnsemble, RegressionBaggedEnsemble.oobLoss} ## @end deftypefn function yfit = oobPredict (this, varargin) caller = 'RegressionBaggedEnsemble.oobPredict'; args = oobArgs (varargin, {'Learners'}, caller); U = ! this.UseObsForLearner; yfit = ensemblePredict (compact (this), this.X, ... [{'UseObsForLearner', U}, args], caller); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionBaggedEnsemble} {@var{L} =} oobLoss (@var{obj}) ## @deftypefnx {RegressionBaggedEnsemble} {@var{L} =} oobLoss (@dots{}, @var{name}, @var{value}) ## ## Out-of-bag regression loss. ## ## The loss of the out-of-bag predictions against @code{Y}, weighted by ## @code{W}, an observation in the sample of every tree used being left ## out. @qcode{'LossFun'} and @qcode{'Mode'} are taken as by ## @code{CompactRegressionEnsemble.loss}, and @qcode{'Learners'} ## restricts the trees. ## ## @seealso{RegressionBaggedEnsemble, RegressionBaggedEnsemble.oobPredict} ## @end deftypefn function L = oobLoss (this, varargin) caller = 'RegressionBaggedEnsemble.oobLoss'; args = oobArgs (varargin, {'Learners', 'LossFun', 'Mode'}, caller); L = ensembleLoss (compact (this), this.X, this.Y, ... [{'UseObsForLearner', ! this.UseObsForLearner, ... 'Weights', this.W}, args], caller); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionBaggedEnsemble} {@var{imp} =} oobPermutedPredictorImportance (@var{obj}) ## @deftypefnx {RegressionBaggedEnsemble} {@var{imp} =} oobPermutedPredictorImportance (@dots{}, 'Learners', @var{idx}) ## ## Out-of-bag predictor importance by permutation. ## ## For each tree, the values of each predictor are permuted among the ## observations out of its bag, and the tree's mean squared error on them, ## weighted by @code{W}, is taken before and after. @var{imp} holds, for ## each predictor, the mean of the rise over the trees divided by its ## standard deviation over the trees, zero where the mean is zero. ## @qcode{'Learners'} restricts the trees. ## ## @seealso{RegressionBaggedEnsemble, ## RegressionBaggedEnsemble.predictorImportance} ## @end deftypefn function imp = oobPermutedPredictorImportance (this, varargin) caller = 'RegressionBaggedEnsemble.oobPermutedPredictorImportance'; if (mod (numel (varargin), 2) != 0) error ("%s: name-value arguments must be in pairs.", caller); endif ## Parse optional paired arguments; empty 'Learners' stands for every ## trained learner [Learners, Options, args] = ... parsePairedArguments ({'Learners', 'Options'}, {[], []}, ... varargin(:)); ## Validate optional paired arguments if (isempty (Learners)) learners = 1:this.NumTrained; else learners = oobLearners (Learners, this.NumTrained, caller); endif if (! isempty (Options)) error ("%s: 'Options' is not implemented.", caller); endif if (! isempty (args)) error ("%s: invalid optional paired argument.", caller); endif p = columns (this.X); D = zeros (numel (learners), p); for j = 1:numel (learners) t = learners(j); r = find (! this.UseObsForLearner(:,t)); w = double (this.W(r)); if (isempty (r) || ! (sum (w) > 0)) continue; endif w /= sum (w); tree = this.Trained{t}; Xo = this.X(r,:); yo = this.Y(r); e0 = sum (w .* (predict (tree, Xo) - yo) .^ 2); for v = 1:p Xp = Xo; Xp(:,v) = Xo(randperm (numel (r)), v); D(j,v) = sum (w .* (predict (tree, Xp) - yo) .^ 2) - e0; endfor endfor imp = mean (D, 1) ./ std (D, 0, 1); imp(mean (D, 1) == 0) = 0; endfunction endmethods methods (Access = private) ## The public bag properties, from what the fit keeps in the parent. function this = bagProperties (this) this.FResample = this.BagFResample; this.Replace = this.BagReplace; this.UseObsForLearner = this.BagInBag; endfunction endmethods endclassdef ## The Name-Value pairs of an out-of-bag method, refused unless in ALLOWED. function args = oobArgs (args, allowed, caller) if (mod (numel (args), 2) != 0) error ("%s: name-value arguments must be in pairs.", caller); endif for i = 1:2:numel (args) if (! (ischar (args{i}) && any (strcmpi (args{i}, allowed)))) error ("%s: invalid optional paired argument.", caller); endif endfor endfunction ## The indices of the learners 'Learners' names. function learners = oobLearners (val, T, caller) if (! (isnumeric (val) && isvector (val) && isreal (val) && all (val >= 1) && all (val <= T) && all (val == fix (val)))) error (strcat ("%s: 'Learners' must be a vector of indices of", ... " trained learners."), caller); endif learners = double (val(:)'); endfunction ## Test output %!test # MATLAB parity: the properties of a bagged regression ensemble %! load fisheriris %! Mdl = RegressionBaggedEnsemble (meas(:,2:4), meas(:,1), ... %! 'NumLearningCycles', 3); %! assert_equal (numel (properties (Mdl)), 27); %! assert_equal (Mdl.Method, 'Bag'); %! assert_equal (Mdl.TrainedWeights, [1; 1; 1]); %! assert_equal (Mdl.FitInfo, []); %! assert_equal (Mdl.FitInfoDescription, 'None'); %! assert_equal (Mdl.FResample, 1); %! assert_equal (Mdl.Replace, true); %! assert_equal (size (Mdl.UseObsForLearner), [150, 3]); %! assert_equal (isfield (Mdl.ModelParameters, 'LearnRate'), false); %!test # MATLAB parity: the prediction is the mean of the trees %! load fisheriris %! rng (5); %! Mdl = RegressionBaggedEnsemble (meas(:,2:4), meas(:,1), ... %! 'NumLearningCycles', 4); %! P = zeros (150, 1); %! for t = 1:4 %! P += predict (Mdl.Trained{t}, meas(:,2:4)); %! endfor %! assert_equal (predict (Mdl, meas(:,2:4)), P / 4, 1e-14); %! assert_equal (min (cellfun (@(t) min (t.NodeSize), Mdl.Trained)) >= 5, true); %!test # MATLAB parity: each tree draws FResample of the rows %! load fisheriris %! rng (5); %! Mdl = RegressionBaggedEnsemble (meas(:,2:4), meas(:,1), ... %! 'NumLearningCycles', 2, ... %! 'FResample', 0.5, 'Replace', 'off'); %! assert_equal (sum (Mdl.UseObsForLearner), [75, 75]); %! assert_equal (Mdl.Trained{1}.NodeSize(1), 75); %!test # MATLAB parity: the bootstrap draws in proportion to the weights %! load fisheriris %! rng (4); %! w = [10 * ones(50, 1); ones(100, 1)]; %! Mdl = RegressionBaggedEnsemble (meas(:,2:4), meas(:,1), ... %! 'NumLearningCycles', 20, 'Weights', w); %! assert_equal ([sum(Mdl.W(1:50)), sum(Mdl.W(51:150))], [5, 1] / 6, 1e-15); %! share = mean (sum (Mdl.UseObsForLearner(1:50,:)) / 50); %! assert_equal (share > 0.85, true); %!test # MATLAB parity: a row no tree may predict is NaN %! load fisheriris %! rng (5); %! Mdl = RegressionBaggedEnsemble (meas(:,2:4), meas(:,1), ... %! 'NumLearningCycles', 4); %! U = false (2, 4); %! U(2,1) = true; %! yf = predict (Mdl, meas(1:2,2:4), 'UseObsForLearner', U); %! assert_equal (isnan (yf(1)), true); %! assert_equal (yf(2), predict (Mdl.Trained{1}, meas(2,2:4)), 1e-15); %!test # resuming adds the new trees' samples %! load fisheriris %! Mdl = RegressionBaggedEnsemble (meas(:,2:4), meas(:,1), ... %! 'NumLearningCycles', 2); %! Mdl = resume (Mdl, 2); %! assert_equal (class (Mdl), 'RegressionBaggedEnsemble'); %! assert_equal (Mdl.NumTrained, 4); %! assert_equal (size (Mdl.UseObsForLearner), [150, 4]); %! assert_equal (class (compact (Mdl)), 'CompactRegressionEnsemble'); ## Test input validation %!error ... %! RegressionBaggedEnsemble (1) %!error ... %! load fisheriris %! RegressionBaggedEnsemble (meas(:,2:4), meas(:,1), 'Method', 'LSBoost') %!error ... %! load fisheriris %! RegressionBaggedEnsemble (meas(:,2:4), meas(:,1), 'LearnRate', 0.5) %!error ... %! load fisheriris %! predict (RegressionBaggedEnsemble (meas(:,2:4), meas(:,1), ... %! 'NumLearningCycles', 1)) %!test # MATLAB parity: out-of-bag predictions invert UseObsForLearner %! load fisheriris %! rng (5); %! Mdl = RegressionBaggedEnsemble (meas(:,2:4), meas(:,1), ... %! 'NumLearningCycles', 6); %! U = ! Mdl.UseObsForLearner; %! assert_equal (isequaln (oobPredict (Mdl), ... %! predict (Mdl, meas(:,2:4), ... %! 'UseObsForLearner', U)), true); %! assert_equal (oobLoss (Mdl), loss (Mdl, meas(:,2:4), meas(:,1), ... %! 'UseObsForLearner', U), 1e-15); %! assert_equal (oobLoss (Mdl, 'Mode', 'cumulative'), ... %! loss (Mdl, meas(:,2:4), meas(:,1), ... %! 'UseObsForLearner', U, 'Mode', 'cumulative'), 1e-14); %!test # MATLAB parity: the out-of-bag loss leaves out rows in every bag %! load fisheriris %! rng (5); %! Mdl = RegressionBaggedEnsemble (meas(:,2:4), meas(:,1), ... %! 'NumLearningCycles', 6); %! yf = oobPredict (Mdl); %! have = ! isnan (yf); %! assert_equal (oobLoss (Mdl), mean ((yf(have) - meas(have,1)) .^ 2), 1e-14); %!test # MATLAB parity: the importance of a bagged regression ensemble %! load fisheriris %! rng (6); %! Mdl = RegressionBaggedEnsemble (meas(:,2:4), meas(:,1), ... %! 'NumLearningCycles', 5); %! I = cell2mat (cellfun (@(t) predictorImportance (t), Mdl.Trained, ... %! 'UniformOutput', false)); %! assert_equal (predictorImportance (Mdl), mean (I), 1e-15); %!test # MATLAB parity: a constant predictor has zero permuted importance %! load fisheriris %! rng (7); %! Mdl = RegressionBaggedEnsemble ([meas(:,2:4), ones(150, 1)], meas(:,1), ... %! 'NumLearningCycles', 20); %! imp = oobPermutedPredictorImportance (Mdl); %! assert_equal (size (imp), [1, 4]); %! assert_equal (imp(4), 0); %! assert_equal (all (imp(1:3) > 0), true); %!error ... %! load fisheriris %! oobPredict (RegressionBaggedEnsemble (meas(:,2:4), meas(:,1), ... %! 'NumLearningCycles', 1), ... %! 'Mode', 'ensemble') %!error ... %! load fisheriris %! oobLoss (RegressionBaggedEnsemble (meas(:,2:4), meas(:,1), ... %! 'NumLearningCycles', 1), ... %! 'Weights', ones (150, 1)) %!error ... %! load fisheriris %! oobPermutedPredictorImportance (RegressionBaggedEnsemble (meas(:,2:4), ... %! meas(:,1), 'NumLearningCycles', 1), ... %! 'Options', struct ()) %!error ... %! load fisheriris %! oobPermutedPredictorImportance (RegressionBaggedEnsemble (meas(:,2:4), ... %! meas(:,1), 'NumLearningCycles', 1), ... %! 'Bogus', struct ()) %!shared Xr, yr, tr %! load fisheriris %! Xr = meas(:,2:4); %! yr = meas(:,1); %! tr = templateTree ('MaxNumSplits', 3); %!test # MATLAB parity: LSBoost drawing every row without replacement is plain %! M = RegressionBaggedEnsemble (Xr, yr, 'Method', 'LSBoost', ... %! 'NumLearningCycles', 4, 'Learners', tr, ... %! 'FResample', 1, 'Replace', 'off'); %! P = fitrensemble (Xr, yr, 'NumLearningCycles', 4, 'Learners', tr); %! assert_equal (M.FitInfo, P.FitInfo, 1e-12); %! assert_equal (predict (M, Xr(1:5,:)), predict (P, Xr(1:5,:)), 1e-12); %! assert_equal ([M.FResample, M.Replace], [1, false]); %!test # MATLAB parity: resampled LSBoost measures its fit over every row %! M = RegressionBaggedEnsemble (Xr, yr, 'Method', 'LSBoost', ... %! 'NumLearningCycles', 4, 'Learners', tr, ... %! 'Resample', 'on'); %! assert_equal (size (M.UseObsForLearner), [150, 4]); %! W = M.W / sum (M.W); %! F = zeros (150, 1); %! for t = 1:4 %! h = predict (M.Trained{t}, Xr); %! assert_equal (M.FitInfo(t), sum (W .* (yr - F - h) .^ 2), 1e-12); %! F += M.TrainedWeights(t) * h; %! endfor %!test # a resampled LSBoost cross-validates with its learning rate %! M = RegressionBaggedEnsemble (Xr, yr, 'Method', 'LSBoost', ... %! 'NumLearningCycles', 2, 'Learners', tr, ... %! 'FResample', 0.7, 'LearnRate', 0.5); %! CV = crossval (M, 'KFold', 3); %! assert_equal (CV.Trainable{1}.LearnRate, 0.5); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,2:3); %! y = meas(:,1); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SW', 'PL'}); %! T.SL = y; %! Mdl = fitrensemble (T, 'SL', 'Method', 'Bag'); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'SL'), a); %! assert_equal (loss (Mdl, T), a); statistics-release-2.1.2/inst/Machine_Learning/RegressionEnsemble.m000066400000000000000000001774541527311104300254540ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef RegressionEnsemble < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} RegressionEnsemble ## ## Boosted ensemble of regression trees ## ## A @code{RegressionEnsemble} object holds the regression trees LSBoost ## grew one after another, each fitted to the residual the trees before it ## left, together with the data it was fitted on. ## ## Create one with @code{fitrensemble}. A bagged ensemble is a ## @code{RegressionBaggedEnsemble}, and @code{compact} returns a ## @code{CompactRegressionEnsemble} without the data. ## ## @seealso{fitrensemble, CompactRegressionEnsemble, ## RegressionBaggedEnsemble} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} Regularization ## ## Result of regularizing the ensemble ## ## Empty until @code{regularize} fills it with a structure of lasso ## weights for the trees, and emptied again by @code{resume}. This ## property is read-only. ## ## @end deftp Regularization = []; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} Y ## ## Response data ## ## The response the ensemble was fitted on, a row missing a value having ## been left out. This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} X ## ## Predictor data ## ## The predictors the ensemble was fitted on, one row per observation. ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## This property is read-only. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} RowsUsed ## ## Rows of the data that were used ## ## A logical column over the rows as supplied. This property is ## read-only. ## ## @end deftp RowsUsed = []; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} W ## ## Observation weights ## ## The weights given, normalized to sum to one. It has the class of the ## @qcode{'Weights'} given, single or double. This property is read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} ModelParameters ## ## Parameters of the fit ## ## A structure with the fields @qcode{Type}, @qcode{Method}, ## @qcode{LearnerTemplates}, @qcode{NLearn}, the number of learning ## cycles asked for in all, and for LSBoost @qcode{LearnRate}. This ## property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} NumObservations ## ## Number of observations used ## ## This property is read-only. ## ## @end deftp NumObservations = 0; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} BinEdges ## ## Bin edges of the predictors ## ## Always empty, binning not being implemented. This property is ## read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} HyperparameterOptimizationResults ## ## Results of optimizing the hyperparameters ## ## Always empty, such optimization not being implemented. This property ## is read-only. ## ## @end deftp HyperparameterOptimizationResults = []; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} CategoricalPredictors ## ## Indices of categorical predictors ## ## The predictors every tree treats as categorical, empty when none ## is. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} ResponseName ## ## Name of the response variable ## ## This property is read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} ExpandedPredictorNames ## ## Names of the predictors as the learners saw them ## ## The same as @code{PredictorNames}. This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} Method ## ## Ensemble method ## ## @qcode{'LSBoost'} or @qcode{'Bag'}. This property is read-only. ## ## @end deftp Method = ''; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} LearnerNames ## ## Names of the weak learners ## ## Always @code{@{'Tree'@}}. This property is read-only. ## ## @end deftp LearnerNames = {'Tree'}; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} ReasonForTermination ## ## Why the fit stopped adding trees ## ## This property is read-only. ## ## @end deftp ReasonForTermination = ''; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} FitInfo ## ## Fit information ## ## For LSBoost, a column with the weighted mean squared error of each ## tree against the residual it was fitted to. Empty for Bag. This ## property is read-only. ## ## @end deftp FitInfo = []; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} FitInfoDescription ## ## Description of @code{FitInfo} ## ## This property is read-only. ## ## @end deftp FitInfoDescription = {}; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} UsePredForLearner ## ## Which predictors each learner uses ## ## Always empty, as MATLAB returns it for tree learners. This property ## is read-only. ## ## @end deftp UsePredForLearner = []; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} NumTrained ## ## Number of trained trees ## ## This property is read-only. ## ## @end deftp NumTrained = 0; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} Trained ## ## Trained trees ## ## A column cell array of @code{CompactRegressionTree} objects. This ## property is read-only. ## ## @end deftp Trained = {}; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} TrainedWeights ## ## Weights of the trained trees ## ## A column with one weight per tree. This property is read-only. ## ## @end deftp TrainedWeights = []; ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} CombineWeights ## ## How the trees are combined ## ## @qcode{'WeightedSum'} for LSBoost, @qcode{'WeightedAverage'} for Bag. ## This property is read-only. ## ## @end deftp CombineWeights = 'WeightedSum'; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {RegressionEnsemble} {property} ResponseTransform ## ## Transform applied to the predicted response ## ## See @code{CompactRegressionEnsemble.ResponseTransform}. ## ## @end deftp ResponseTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) RTfun = @(y) y; # the response transform as a function LearnRate = 1; # shrinkage of LSBoost TreeArgs = {}; # the options given in the tree template F = []; # what LSBoost has predicted so far, for resume BagFResample = 1; # share of the observations each bag draws BagReplace = true; # whether the bags draw with replacement BagInBag = []; # NxNumTrained logical, the rows each bag drew Resampling = false; # whether LSBoost resamples its rows endproperties methods (Hidden) function this = set.ResponseTransform (this, val) [this.RTfun, this.ResponseTransform] = ... parseResponseTransform (val, 'RegressionEnsemble'); endfunction function disp (this) fprintf ("\n %s\n\n", class (this)); fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); fprintf ("%+25s: %s\n", 'CategoricalPredictors', ... mat2str (this.CategoricalPredictors)); fprintf ("%+25s: '%s'\n", 'ResponseTransform', this.ResponseTransform); fprintf ("%+25s: %d\n", 'NumObservations', this.NumObservations); fprintf ("%+25s: %d\n", 'NumTrained', this.NumTrained); fprintf ("%+25s: '%s'\n", 'Method', this.Method); fprintf ("%+25s: '%s'\n", 'ReasonForTermination', ... this.ReasonForTermination); fprintf ("%+25s: [%dx%d double]\n", 'FitInfo', rows (this.FitInfo), ... columns (this.FitInfo)); if (isa (this, 'RegressionBaggedEnsemble')) fprintf ("%+25s: %g\n", 'FResample', this.FResample); fprintf ("%+25s: %d\n", 'Replace', this.Replace); fprintf ("%+25s: [%dx%d logical]\n", 'UseObsForLearner', ... rows (this.UseObsForLearner), ... columns (this.UseObsForLearner)); endif fprintf ("\n"); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {RegressionEnsemble} {@var{obj} =} RegressionEnsemble (@var{X}, @var{Y}) ## @deftypefnx {RegressionEnsemble} {@var{obj} =} RegressionEnsemble (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {RegressionEnsemble} {@var{obj} =} RegressionEnsemble (@var{Tbl}, @var{formula}) ## @deftypefnx {RegressionEnsemble} {@var{obj} =} RegressionEnsemble (@var{Tbl}, @var{Y}) ## @deftypefnx {RegressionEnsemble} {@var{obj} =} RegressionEnsemble (@dots{}, @var{name}, @var{value}) ## ## Fit an ensemble of regression trees by LSBoost. ## ## @code{fitrensemble} is the documented way in, and its help lists the ## options both take. A bagged ensemble is fitted by ## @code{RegressionBaggedEnsemble}. ## ## @seealso{fitrensemble, RegressionBaggedEnsemble} ## @end deftypefn function this = RegressionEnsemble (X, Y, varargin) if (nargin < 2) error ("RegressionEnsemble: too few input arguments."); endif ## A table names its own predictors and says which hold levels ## An ordinal column is a numeric predictor here, as in R2024a [this, X, Y, varargin] = resolveTable (this, 'RegressionEnsemble', ... X, Y, varargin, true); if (mod (numel (varargin), 2) != 0) error ("RegressionEnsemble: name-value arguments must be in pairs."); endif bagged = isa (this, 'RegressionBaggedEnsemble'); if (bagged) caller = 'RegressionBaggedEnsemble'; else caller = 'RegressionEnsemble'; endif ## Parse optional paired arguments. 'LearnRate', 'FResample' and ## 'Replace' stay empty when not given, which is how the checks below ## tell them apart from a value, and 'Resample' is off. optNames = {'Method', 'NumLearningCycles', 'Learners', 'LearnRate', ... 'NPrint', 'Weights', 'PredictorNames', 'ResponseName', ... 'ResponseTransform', 'FResample', 'Replace', 'Resample', ... 'CategoricalPredictors'}; dfValues = {'LSBoost', 100, 'tree', [], 'off', [], {}, 'Y', 'none', ... [], [], [], []}; [Method, NLearn, Learners, LearnRate, NPrint, Weights, ... PredictorNames, ResponseName, ResponseTransform, FResample, ... Replace, Resample, CatPreds, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (ischar (Method) && isrow (Method))) error ("%s: 'Method' must be a character vector.", caller); elseif (strcmpi (Method, 'LSBoost')) Method = 'LSBoost'; elseif (strcmpi (Method, 'Bag')) Method = 'Bag'; else error ("%s: '%s' is not a valid ensemble method.", caller, Method); endif if (! (isnumeric (NLearn) && isscalar (NLearn) && isreal (NLearn) && NLearn >= 1 && NLearn == fix (NLearn))) error ("%s: 'NumLearningCycles' must be a positive integer.", caller); endif NLearn = double (NLearn); if (! isempty (LearnRate) && ! (isnumeric (LearnRate) && isscalar (LearnRate) && isreal (LearnRate) && LearnRate > 0 && LearnRate <= 1)) error (strcat ("%s: 'LearnRate' must be a number greater than 0", ... " and no greater than 1."), caller); endif LearnRate = double (LearnRate); if (ischar (NPrint) && strcmpi (NPrint, 'off')) NPrint = 0; elseif (isnumeric (NPrint) && isscalar (NPrint) && isreal (NPrint) && NPrint >= 1 && NPrint == fix (NPrint)) NPrint = double (NPrint); else error ("%s: 'NPrint' must be a positive integer or 'off'.", caller); endif if (! (ischar (ResponseName) && isrow (ResponseName))) error ("%s: 'ResponseName' must be a character vector.", caller); endif if (! isempty (FResample) && ! (isnumeric (FResample) && isscalar (FResample) && isreal (FResample) && FResample > 0 && FResample <= 1)) error (strcat ("%s: 'FResample' must be a number greater than 0", ... " and no greater than 1."), caller); endif FResample = double (FResample); if (! isempty (Replace)) [Replace, ok] = onOff (Replace); if (! ok) error ("%s: 'Replace' must be 'on' or 'off'.", caller); endif endif if (isempty (Resample)) Resample = false; else [Resample, ok] = onOff (Resample); if (! ok) error ("%s: 'Resample' must be 'on' or 'off'.", caller); endif endif ## Options MATLAB takes that this class does not implement are named ## one by one, so that asking for one is refused rather than quietly ## doing nothing; anything else left over is unknown. notImpl = {'NumBins', 'OptimizeHyperparameters', ... 'HyperparameterOptimizationOptions', 'Options'}; for i = 1:2:numel (args) if (ischar (args{i}) && any (strcmpi (args{i}, notImpl))) error ("%s: '%s' is not implemented.", caller, args{i}); endif endfor if (! isempty (args)) error ("%s: invalid optional paired argument.", caller); endif if (ischar (Learners) && strcmpi (Learners, 'tree')) tmpl = templateTree (); elseif (isstruct (Learners) && isscalar (Learners) && isfield (Learners, 'Method') && strcmpi (Learners.Method, 'Tree')) tmpl = Learners; else error ("%s: 'Learners' must be 'tree' or a tree template.", caller); endif tmpl.Type = 'regression'; TreeArgs = {}; for [val, key] = tmpl if (! any (strcmp (key, {'Method', 'Type'}))) TreeArgs(end+1:end+2) = {key, val}; endif endfor isbag = strcmp (Method, 'Bag'); resampled = Resample || ! isempty (FResample) || ! isempty (Replace); if (isbag && ! bagged) error (strcat ("RegressionEnsemble: a bagged ensemble is fitted by", ... " RegressionBaggedEnsemble.")); elseif (! isbag && bagged && ! resampled) error (strcat ("RegressionBaggedEnsemble: 'Method' must be 'Bag'", ... " unless the ensemble resamples.")); elseif (! isbag && ! bagged && resampled) error (strcat ("RegressionEnsemble: a resampled ensemble is fitted", ... " by RegressionBaggedEnsemble.")); endif if (isbag) if (! isempty (LearnRate)) error ("%s: 'LearnRate' cannot be used with the 'Bag' method.", ... caller); endif if (isempty (FResample)) FResample = 1; endif if (isempty (Replace)) Replace = true; endif else if (resampled && isempty (FResample)) FResample = 1; endif if (resampled && isempty (Replace)) Replace = true; endif if (isempty (LearnRate)) LearnRate = 1; endif endif ## A row missing some predictors is kept, its learners taking the ## missing values, as in R2024a F = regFrame (X, Y, Weights, caller, 'partial'); if (any (F.Weights < 0) || ! (sum (F.Weights) > 0)) error (strcat ("%s: 'Weights' must be nonnegative and not all", ... " zero."), caller); endif if (isempty (PredictorNames)) PredictorNames = arrayfun (@(k) sprintf ('x%d', k), 1:F.p, ... 'UniformOutput', false); elseif (! (iscellstr (PredictorNames) && numel (PredictorNames) == F.p)) error (strcat ("%s: 'PredictorNames' must be a cell array of", ... " character vectors with one element per column", ... " of X."), caller); endif ## Categorical predictors reach every tree [Cod, errmsg] = dummyCoding (F.X, CatPreds, PredictorNames); if (! isempty (errmsg)) error ("%s: %s", caller, errmsg); endif CatIdx = []; if (! isempty (Cod.Index)) CatIdx = Cod.Index; TreeArgs = [{'CategoricalPredictors', CatIdx}, TreeArgs]; endif this.X = F.X; this.Y = double (F.Y); this.RowsUsed = F.RowsUsed; ## The weights keep their class in the model; every computation runs on ## them as double. this.W = cast (F.W, F.WeightsClass); this.NumObservations = F.n; this.PredictorNames = PredictorNames(:)'; this.ExpandedPredictorNames = this.PredictorNames; this.ResponseName = ResponseName; this.ResponseTransform = ResponseTransform; this.Method = Method; this.TreeArgs = TreeArgs; this.CategoricalPredictors = CatIdx; this.Trained = cell (0, 1); this.TrainedWeights = zeros (0, 1); this.ModelParameters = struct ('Type', 'regression', ... 'Method', Method, ... 'LearnerTemplates', tmpl, ... 'NLearn', 0); if (isbag) this.CombineWeights = 'WeightedAverage'; this.FitInfo = []; this.FitInfoDescription = 'None'; this.BagFResample = FResample; this.BagReplace = Replace; this.BagInBag = false (F.n, 0); else this.LearnRate = LearnRate; this.ModelParameters.LearnRate = LearnRate; this.Resampling = resampled; if (resampled) this.BagFResample = FResample; this.BagReplace = Replace; this.BagInBag = false (F.n, 0); endif this.FitInfo = zeros (0, 1); this.FitInfoDescription = ... {strcat("Vector of length NumTrained, where NumTrained is the", ... " number of learned weak hypotheses."); ... strcat("Element t of this vector is the weighted residual from", ... " learner t.")}; this.F = zeros (F.n, 1); endif this = growLearners (this, NLearn, NPrint); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionEnsemble} {@var{CVMdl} =} crossval (@var{obj}) ## @deftypefnx {RegressionEnsemble} {@var{CVMdl} =} crossval (@dots{}, @var{name}, @var{value}) ## ## Cross-validate an ensemble. ## ## @code{@var{CVMdl} = crossval (@var{obj})} refits the ensemble on the ## training part of each of ten folds, and returns a ## @code{RegressionPartitionedEnsemble}. One of @qcode{'KFold'}, an ## integer greater than 1, @qcode{'Holdout'}, a number between 0 and 1, ## @qcode{'Leaveout'}, @qcode{'on'} for one fold per observation, or ## @qcode{'CVPartition'}, a @code{cvpartition} object, may choose the ## partition instead. ## ## @seealso{RegressionEnsemble, RegressionPartitionedEnsemble, cvpartition} ## @end deftypefn function CVMdl = crossval (this, varargin) [P, errmsg] = ensemblePartition (varargin, this.Y, ... this.NumObservations, ... false); if (! isempty (errmsg)) error ("%s.crossval: %s", class (this), errmsg); endif if (isempty (P)) error (strcat ("%s.crossval: no partition was asked for; set", ... " 'CrossVal' to 'on' or give 'KFold'."), class (this)); endif CVMdl = RegressionPartitionedEnsemble (this, P); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionEnsemble} {@var{CMdl} =} compact (@var{obj}) ## ## Drop the training data from a regression ensemble. ## ## @code{@var{CMdl} = compact (@var{obj})} returns a ## @code{CompactRegressionEnsemble} holding the trees and what prediction ## needs. It predicts new data identically. ## ## @seealso{RegressionEnsemble, CompactRegressionEnsemble} ## @end deftypefn function CMdl = compact (this) CMdl = CompactRegressionEnsemble (this); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionEnsemble} {@var{B} =} resume (@var{obj}, @var{NumLearningCycles}) ## @deftypefnx {RegressionEnsemble} {@var{B} =} resume (@dots{}, 'NPrint', @var{n}) ## ## Grow more trees. ## ## @var{B} is the ensemble with @var{NumLearningCycles} further trees ## grown as though the fit had asked for them from the start. ## @qcode{'NPrint'} is taken as by @code{fitrensemble}. A ## @code{Regularization} is emptied, its weights no longer covering every ## tree; MATLAB keeps it and applies its weights to the first trees. ## ## @seealso{RegressionEnsemble, fitrensemble} ## @end deftypefn function this = resume (this, NumLearningCycles, varargin) caller = sprintf ('%s.resume', class (this)); if (nargin < 2) error ("%s: too few input arguments.", caller); endif if (! (isnumeric (NumLearningCycles) && isscalar (NumLearningCycles) && isreal (NumLearningCycles) && NumLearningCycles >= 1 && NumLearningCycles == fix (NumLearningCycles))) error ("%s: NUMLEARNINGCYCLES must be a positive integer.", caller); endif if (mod (numel (varargin), 2) != 0) error ("%s: name-value arguments must be in pairs.", caller); endif ## Parse optional paired arguments [NPrint, args] = parsePairedArguments ({'NPrint'}, {'off'}, ... varargin(:)); ## Validate optional paired arguments if (ischar (NPrint) && strcmpi (NPrint, 'off')) NPrint = 0; elseif (isnumeric (NPrint) && isscalar (NPrint) && isreal (NPrint) && NPrint >= 1 && NPrint == fix (NPrint)) NPrint = double (NPrint); else error ("%s: 'NPrint' must be a positive integer or 'off'.", caller); endif if (! isempty (args)) error ("%s: invalid optional paired argument.", caller); endif this = growLearners (this, double (NumLearningCycles), NPrint); this.Regularization = []; endfunction ## -*- texinfo -*- ## @deftypefn {RegressionEnsemble} {@var{yfit} =} predict (@var{obj}, @var{X}) ## @deftypefnx {RegressionEnsemble} {@var{yfit} =} predict (@dots{}, @var{name}, @var{value}) ## ## Predict the response with a regression ensemble. ## ## Behaves as @code{CompactRegressionEnsemble.predict} and takes the same ## Name-Value arguments. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @seealso{RegressionEnsemble, CompactRegressionEnsemble.predict} ## @end deftypefn function yfit = predict (this, X, varargin) if (nargin < 2) error ("%s.predict: too few input arguments.", class (this)); endif ## A table is read by the names the model was fitted on X = tableColumns (this, 'RegressionEnsemble.predict', X); yfit = ensemblePredict (compact (this), X, varargin, ... [class(this), '.predict']); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionEnsemble} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {RegressionEnsemble} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {RegressionEnsemble} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {RegressionEnsemble} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Regression loss of an ensemble. ## ## Behaves as @code{CompactRegressionEnsemble.loss} and takes the same ## Name-Value arguments. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{RegressionEnsemble, CompactRegressionEnsemble.loss} ## @end deftypefn function L = loss (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("%s.loss: too few input arguments.", class (this)); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); L = ensembleLoss (compact (this), X, Y, varargin, ... [class(this), '.loss']); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionEnsemble} {@var{imp} =} predictorImportance (@var{obj}) ## @deftypefnx {RegressionEnsemble} {[@var{imp}, @var{ma}] =} predictorImportance (@var{obj}) ## ## Estimate the importance of each predictor. ## ## Behaves as @code{CompactRegressionEnsemble.predictorImportance}. ## ## @seealso{RegressionEnsemble, ## CompactRegressionEnsemble.predictorImportance} ## @end deftypefn function [imp, ma] = predictorImportance (this) [imp, ma] = ensembleImportance (compact (this)); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionEnsemble} {@var{yfit} =} resubPredict (@var{obj}) ## @deftypefnx {RegressionEnsemble} {@var{yfit} =} resubPredict (@dots{}, @var{name}, @var{value}) ## ## Predict the response of the training data. ## ## @code{predict} on @code{X}, taking the same Name-Value arguments. ## ## @seealso{RegressionEnsemble, RegressionEnsemble.predict} ## @end deftypefn function yfit = resubPredict (this, varargin) yfit = ensemblePredict (compact (this), this.X, varargin, ... [class(this), '.resubPredict']); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionEnsemble} {@var{L} =} resubLoss (@var{obj}) ## @deftypefnx {RegressionEnsemble} {@var{L} =} resubLoss (@dots{}, @var{name}, @var{value}) ## ## Regression loss on the training data. ## ## @code{loss} on @code{X} and @code{Y}, weighted by @code{W} unless ## @qcode{'Weights'} are given. ## ## @seealso{RegressionEnsemble, RegressionEnsemble.loss} ## @end deftypefn function L = resubLoss (this, varargin) L = ensembleLoss (compact (this), this.X, this.Y, ... [{'Weights', this.W}, varargin], ... [class(this), '.resubLoss']); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionEnsemble} {@var{B} =} regularize (@var{obj}) ## @deftypefnx {RegressionEnsemble} {@var{B} =} regularize (@dots{}, @var{name}, @var{value}) ## ## Find lasso weights for the trees of a regression ensemble. ## ## @code{@var{B} = regularize (@var{obj})} fits the training response by ## the trees' predictions with a lasso that has no intercept and whose ## weights may not be negative, over a path of penalties, and returns the ## ensemble with the result in @code{Regularization}. For a penalty ## @var{lambda} the tree weights @var{a} minimize ## @code{sum (W .* (Y - P * a) .^ 2) / 2 + lambda * sum (a)}, where @var{P} ## holds one column of training predictions per tree and @code{W} is the ## observation weights, which sum to one. @code{TrainedWeights} is left as ## it was. ## ## @multitable @columnfractions 0.2 0.02 0.78 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Lambda'} @tab @tab A vector of non-negative penalties. The ## default is 0 followed by nine values spaced evenly on a log scale from ## @code{lmax / 1000} to @code{lmax}, the smallest penalty that sets every ## weight to zero, @code{lmax = max (abs (P' * (W .* Y)))}. ## @item @qcode{'MaxIter'} @tab @tab The most passes of coordinate descent ## over the trees for each penalty. The default is 1e5. ## @item @qcode{'RelTol'} @tab @tab The descent stops after a pass in which ## no weight changed by more than this times the largest weight, or this ## when that weight is below one. The default is 1e-10. ## @end multitable ## ## @code{Regularization} is a structure with the fields @qcode{Method}, ## @qcode{'Lasso'}; @qcode{TrainedWeights}, one column per penalty; ## @qcode{Lambda}; @qcode{ResubstitutionMSE}, the weighted mean squared ## error of each column; and @qcode{CombineWeights}, ## @qcode{'WeightedSum'}. ## ## MATLAB's solver can stop well short of the minimum at small penalties, ## so its weights there differ from these, which are the minimum. Its ## @qcode{CombineWeights} is a function handle, and it also takes ## @qcode{'Npass'} and @qcode{'Verbose'}, which are not taken here. ## ## @seealso{RegressionEnsemble, RegressionEnsemble.shrink, ## RegressionEnsemble.cvshrink, lasso} ## @end deftypefn function this = regularize (this, varargin) caller = sprintf ('%s.regularize', class (this)); [o, errmsg] = shrinkOptions (varargin, {'Lambda', 'MaxIter', 'RelTol'}); if (! isempty (errmsg)) error ("%s: %s", caller, errmsg); endif T = this.NumTrained; Y = this.Y; W = double (this.W) / sum (double (this.W)); P = zeros (numel (Y), T); for t = 1:T P(:,t) = predict (this.Trained{t}, this.X); endfor Lambda = o.Lambda; if (isempty (Lambda)) lmax = 0; if (T > 0) lmax = max (abs (P' * (W .* Y))); endif Lambda = [0, lmax * 10 .^ linspace(-3, 0, 9)]; endif ## From the largest penalty down, each starting from the last answer. L = numel (Lambda); TW = zeros (T, L); mse = zeros (1, L); [~, order] = sort (Lambda, 'descend'); w = zeros (T, 1); for k = order w = nnLasso (P, Y, W, Lambda(k), w, o.MaxIter, o.RelTol); TW(:,k) = w; mse(k) = sum (W .* (Y - P * w) .^ 2); endfor this.Regularization = struct ('Method', 'Lasso', ... 'TrainedWeights', TW, ... 'Lambda', Lambda, ... 'ResubstitutionMSE', mse, ... 'CombineWeights', 'WeightedSum'); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionEnsemble} {@var{C} =} shrink (@var{obj}) ## @deftypefnx {RegressionEnsemble} {@var{C} =} shrink (@dots{}, @var{name}, @var{value}) ## ## Keep the trees a lasso weight retains. ## ## @code{@var{C} = shrink (@var{obj})} returns a ## @code{CompactRegressionEnsemble} of the trees whose weight in a column ## of @code{Regularization.TrainedWeights} is above a threshold, ordered ## from the largest weight down, each carrying that weight and their ## predictions summed. An ensemble that has not been regularized is ## thresholded on its @code{TrainedWeights} and keeps its way of combining ## its trees. ## ## @multitable @columnfractions 0.2 0.02 0.78 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'WeightColumn'} @tab @tab The column of weights to use, a ## positive integer. The default is 1. ## @item @qcode{'Threshold'} @tab @tab A non-negative number; a tree whose ## weight is not above it is dropped. The default is 0. ## @item @qcode{'Lambda'} @tab @tab Penalties to regularize with first, as ## @code{regularize} does, which also takes @qcode{'MaxIter'} and ## @qcode{'RelTol'} here. ## @end multitable ## ## MATLAB accepts a @qcode{'WeightColumn'} that is not a whole number, ## which is refused here. ## ## @seealso{RegressionEnsemble, RegressionEnsemble.regularize, ## RegressionEnsemble.cvshrink} ## @end deftypefn function C = shrink (this, varargin) caller = sprintf ('%s.shrink', class (this)); [o, errmsg] = shrinkOptions (varargin, {'WeightColumn', 'Threshold', ... 'Lambda', 'MaxIter', 'RelTol'}); if (! isempty (errmsg)) error ("%s: %s", caller, errmsg); endif if (! isscalar (o.Threshold)) error ("%s: 'Threshold' must be a scalar.", caller); endif if (! isempty (o.Lambda)) this = regularize (this, 'Lambda', o.Lambda, 'MaxIter', o.MaxIter, ... 'RelTol', o.RelTol); endif C = compact (this); if (isempty (this.Regularization)) weights = this.TrainedWeights(:); combine = this.CombineWeights; else TW = this.Regularization.TrainedWeights; if (o.WeightColumn > columns (TW)) error (strcat ("%s: 'WeightColumn' must not exceed the number of", ... " values in 'Lambda'."), caller); endif weights = TW(:,o.WeightColumn); combine = 'WeightedSum'; endif keep = find (weights > o.Threshold); [~, order] = sort (weights(keep), 'descend'); idx = keep(order); C = keepLearners (C, idx, weights(idx), combine); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionEnsemble} {[@var{vals}, @var{nlearn}] =} cvshrink (@var{obj}) ## @deftypefnx {RegressionEnsemble} {[@var{vals}, @var{nlearn}] =} cvshrink (@dots{}, @var{name}, @var{value}) ## ## Cross-validate the shrinking of a regression ensemble. ## ## @code{[@var{vals}, @var{nlearn}] = cvshrink (@var{obj})} grows the ## ensemble again on the training part of each fold, as @code{crossval} ## does, regularizes it with each penalty, shrinks it at each threshold, ## and predicts the fold's held-out observations. @var{vals} holds one row ## per penalty and one column per threshold: the weighted mean squared ## error pooled over every held-out observation. @var{nlearn} holds the ## matching mean number of trees kept per fold. ## ## @multitable @columnfractions 0.2 0.02 0.78 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Lambda'} @tab @tab A vector of non-negative penalties. The ## default is @code{Regularization.Lambda}; an ensemble that has not been ## regularized must be given one. ## @item @qcode{'Threshold'} @tab @tab A vector of non-negative thresholds, ## as @code{shrink} takes them. The default is 0. ## @item @qcode{'KFold'}, @qcode{'Holdout'}, @qcode{'Leaveout'}, ## @qcode{'CVPartition'} @tab @tab The partition, as @code{crossval} takes ## it, only one of them. The default is ten folds. ## @item @qcode{'MaxIter'}, @qcode{'RelTol'} @tab @tab As @code{regularize} ## takes them. ## @end multitable ## ## MATLAB warns and returns empty outputs when an ensemble that has not ## been regularized is given no penalties; here that is an error. ## ## @seealso{RegressionEnsemble, RegressionEnsemble.shrink, ## RegressionEnsemble.crossval} ## @end deftypefn function [vals, nlearn] = cvshrink (this, varargin) caller = sprintf ('%s.cvshrink', class (this)); if (mod (numel (varargin), 2) != 0) error ("%s: name-value arguments must be in pairs.", caller); endif cvnames = {'kfold', 'holdout', 'leaveout', 'cvpartition'}; cvargs = {}; rest = {}; for i = 1:2:numel (varargin) if (ischar (varargin{i}) && any (strcmpi (varargin{i}, cvnames))) cvargs(end+1:end+2) = varargin(i:i+1); else rest(end+1:end+2) = varargin(i:i+1); endif endfor [o, errmsg] = shrinkOptions (rest, {'Lambda', 'Threshold', ... 'MaxIter', 'RelTol'}); if (! isempty (errmsg)) error ("%s: %s", caller, errmsg); endif Lambda = o.Lambda; if (isempty (Lambda)) if (isempty (this.Regularization)) error (strcat ("%s: 'Lambda' must be given for an ensemble that", ... " has not been regularized."), caller); endif Lambda = this.Regularization.Lambda; endif [P, errmsg] = ensemblePartition (cvargs, this.Y, this.NumObservations, ... false); if (! isempty (errmsg)) error ("%s: %s", caller, errmsg); endif CV = RegressionPartitionedEnsemble (this, P); L = numel (Lambda); M = numel (o.Threshold); sse = zeros (L, M); counts = zeros (L, M); wsum = 0; K = numel (CV.Trainable); for k = 1:K te = test (CV.Partition, k); Ek = regularize (CV.Trainable{k}, 'Lambda', Lambda, ... 'MaxIter', o.MaxIter, 'RelTol', o.RelTol); wsum += sum (double (this.W(te))); for a = 1:L for b = 1:M Ck = shrink (Ek, 'WeightColumn', a, 'Threshold', o.Threshold(b)); r = this.Y(te) - predict (Ck, this.X(te,:)); sse(a,b) += sum (double (this.W(te)) .* r .^ 2); counts(a,b) += Ck.NumTrained; endfor endfor endfor vals = sse / wsum; nlearn = counts / K; endfunction endmethods methods (Access = protected) ## Grow N trees, LSBoost carrying what it has predicted so far in F so ## that a resume continues the fit exactly. function this = growLearners (this, N, NPrint) n = this.NumObservations; p = columns (this.X); this.ModelParameters.NLearn += N; if (NPrint > 0) printf ("Training %s...\n", this.Method); endif for c = 1:N if (strcmp (this.Method, 'LSBoost')) r = this.Y - this.F; targs = {'PredictorNames', this.PredictorNames, ... 'MaxNumSplits', 10, 'MinParentSize', 10, ... 'MinLeafSize', 5, 'Prune', 'off', 'MergeLeaves', 'off', ... this.TreeArgs{:}}; if (this.Resampling) ## The tree sees the rows drawn; the prediction and the fit ## information run over every row, as in MATLAB R2024a. [idx, sw, cnt] = boostSample (double (this.W), ... ceil (this.BagFResample * n), ... this.BagReplace); T = compact (RegressionTree (this.X(idx,:), r(idx), ... 'Weights', sw, targs{:})); this.BagInBag(:,end+1) = cnt > 0; else T = compact (RegressionTree (this.X, r, ... 'Weights', double (this.W), ... targs{:})); endif h = predict (T, this.X); this.FitInfo(end+1,1) = sum (double (this.W) .* (r - h) .^ 2); this.F += this.LearnRate * h; this = addLearner (this, T, this.LearnRate); else m = ceil (this.BagFResample * n); if (this.BagReplace) cw = [0; cumsum(double (this.W))]; cw /= cw(end); idx = lookup (cw, rand (m, 1)); else [~, order] = sort (rand (n, 1) .^ (1 ./ double (this.W)), ... 'descend'); idx = order(1:m); endif T = compact (RegressionTree (this.X(idx,:), this.Y(idx), ... 'PredictorNames', ... this.PredictorNames, ... 'MinParentSize', 10, ... 'MinLeafSize', 5, 'Prune', 'off', ... 'MergeLeaves', 'off', ... 'NumVariablesToSample', ... ceil (p / 3), this.TreeArgs{:})); inbag = false (n, 1); inbag(idx) = true; this.BagInBag(:,end+1) = inbag; this = addLearner (this, T, 1); endif if (NPrint > 0 && mod (this.NumTrained, NPrint) == 0) printf ("Grown weak learners: %d\n", this.NumTrained); endif endfor this.ReasonForTermination = strcat ("Terminated normally after", ... " completing the requested", ... " number of training cycles."); endfunction function this = addLearner (this, mdl, a) this.Trained{end+1,1} = mdl; this.TrainedWeights(end+1,1) = a; this.NumTrained = numel (this.Trained); endfunction endmethods endclassdef ## The options of regularize, shrink and cvshrink that ALLOWED names, checked ## and with their defaults. ERRMSG is the body of the message the caller ## raises, or empty. function [o, errmsg] = shrinkOptions (args, allowed) o = struct ('Lambda', [], 'MaxIter', 1e5, 'RelTol', 1e-10, ... 'WeightColumn', 1, 'Threshold', 0); errmsg = ""; if (mod (numel (args), 2) != 0) errmsg = "name-value arguments must be in pairs."; return; endif ## Parse optional paired arguments. Each caller accepts the subset of ## them ALLOWED lists; an option left empty keeps the default above. optNames = {'Lambda', 'MaxIter', 'RelTol', 'WeightColumn', 'Threshold'}; [Lambda, MaxIter, RelTol, WeightColumn, Threshold, args] = ... parsePairedArguments (optNames, {[], [], [], [], []}, args(:)); given = optNames(! cellfun (@isempty, {Lambda, MaxIter, RelTol, ... WeightColumn, Threshold})); if (! isempty (args) || ! all (ismember (lower (given), lower (allowed)))) errmsg = "invalid optional paired argument."; return; endif ## Validate optional paired arguments if (! isempty (Lambda)) if (! (isnumeric (Lambda) && isreal (Lambda) && isvector (Lambda) && all (isfinite (Lambda)) && all (Lambda >= 0))) errmsg = "'Lambda' must be a vector of non-negative numbers."; return; endif o.Lambda = double (Lambda(:)'); endif if (! isempty (MaxIter)) if (! (isnumeric (MaxIter) && isscalar (MaxIter) && isreal (MaxIter) && MaxIter >= 1 && MaxIter == fix (MaxIter))) errmsg = "'MaxIter' must be a positive integer."; return; endif o.MaxIter = double (MaxIter); endif if (! isempty (RelTol)) if (! (isnumeric (RelTol) && isscalar (RelTol) && isreal (RelTol) && isfinite (RelTol) && RelTol > 0)) errmsg = "'RelTol' must be a positive number."; return; endif o.RelTol = double (RelTol); endif if (! isempty (WeightColumn)) if (! (isnumeric (WeightColumn) && isscalar (WeightColumn) && isreal (WeightColumn) && WeightColumn >= 1 && WeightColumn == fix (WeightColumn))) errmsg = "'WeightColumn' must be a positive integer."; return; endif o.WeightColumn = double (WeightColumn); endif if (! isempty (Threshold)) if (! (isnumeric (Threshold) && isreal (Threshold) && isvector (Threshold) && all (isfinite (Threshold)) && all (Threshold >= 0))) errmsg = "'Threshold' must hold non-negative numbers."; return; endif o.Threshold = double (Threshold(:)'); endif endfunction ## The non-negative weights W over the columns of P that minimize ## sum (V .* (y - P * w) .^ 2) / 2 + lambda * sum (w), V summing to one, by ## cyclic coordinate descent from the given W. function w = nnLasso (P, y, V, lambda, w, maxiter, reltol) VP = V .* P; d = sum (VP .* P, 1)'; r = y - P * w; for it = 1:maxiter step = 0; for j = 1:numel (w) if (d(j) <= 0) continue; endif new = max (0, (VP(:,j)' * r + d(j) * w(j) - lambda) / d(j)); if (new != w(j)) r -= P(:,j) * (new - w(j)); step = max (step, abs (new - w(j))); w(j) = new; endif endfor if (step <= reltol * max ([abs(w); 1])) break; endif endfor endfunction ## 'on' or 'off' as a logical scalar, OK false for anything else. function [tf, ok] = onOff (val) ok = ischar (val) && any (strcmpi (val, {'on', 'off'})); tf = ok && strcmpi (val, 'on'); endfunction ## Test output %!shared X, y, S %! load fisheriris %! X = meas(:,2:4); %! y = meas(:,1); %! S = templateTree ('MaxNumSplits', 1); %!test # MATLAB parity: the properties of a boosted regression ensemble %! Mdl = RegressionEnsemble (X, y, 'NumLearningCycles', 2, 'Learners', S); %! assert_equal (numel (properties (Mdl)), 24); %! assert_equal (Mdl.Regularization, []); %! assert_equal (Mdl.NumObservations, 150); %! assert_equal (Mdl.LearnerNames, {'Tree'}); %! assert_equal (Mdl.UsePredForLearner, []); %! assert_equal (Mdl.ReasonForTermination, ["Terminated normally after ", ... %! "completing the requested number of training cycles."]); %! assert_equal (Mdl.FitInfoDescription{2}, ["Element t of this vector ", ... %! "is the weighted residual from learner t."]); %!test # MATLAB parity: the parameters of an LSBoost fit %! Mdl = RegressionEnsemble (X, y, 'NumLearningCycles', 2, 'Learners', S, ... %! 'LearnRate', 0.5); %! mp = Mdl.ModelParameters; %! assert_equal (mp.Type, 'regression'); %! assert_equal (mp.Method, 'LSBoost'); %! assert_equal (mp.NLearn, 2); %! assert_equal (mp.LearnRate, 0.5); %!test # MATLAB parity: resuming continues the fit exactly %! M4 = RegressionEnsemble (X, y, 'NumLearningCycles', 4, 'Learners', S, ... %! 'LearnRate', 0.5); %! M2 = RegressionEnsemble (X, y, 'NumLearningCycles', 2, 'Learners', S, ... %! 'LearnRate', 0.5); %! R = resume (M2, 2); %! assert_equal (R.NumTrained, 4); %! assert_equal (R.ModelParameters.NLearn, 4); %! assert_equal (R.FitInfo, M4.FitInfo, 1e-14); %! assert_equal (R.TrainedWeights, M4.TrainedWeights); %!test # MATLAB parity: resubstitution with the training weights %! Mdl = RegressionEnsemble (X, y, 'NumLearningCycles', 3, 'Learners', S); %! assert_equal (resubLoss (Mdl), 0.176267398277276, 1e-13); %! yf = resubPredict (Mdl); %! assert_equal (yf(1:2), [4.953658536585365; 4.953658536585365], 1e-13); %! w = [5 * ones(50, 1); ones(100, 1)]; %! Mw = RegressionEnsemble (X, y, 'NumLearningCycles', 2, 'Learners', S, ... %! 'Weights', w); %! assert_equal (resubLoss (Mw), 0.147589707296207, 1e-13); %! assert_equal (loss (Mw, X, y), 0.181635131105396, 1e-13); %!test # MATLAB parity: a missing response drops its row from the fit %! yn = y; %! yn(3) = NaN; %! Mdl = RegressionEnsemble (X, yn, 'NumLearningCycles', 2, 'Learners', S); %! assert_equal (Mdl.NumObservations, 149); %! assert_equal (sum (Mdl.RowsUsed), 149); %!test # MATLAB parity: the response transform applies to predictions only %! M0 = RegressionEnsemble (X, y, 'NumLearningCycles', 3, 'Learners', S); %! Mt = RegressionEnsemble (X, y, 'NumLearningCycles', 3, 'Learners', S, ... %! 'ResponseTransform', @(z) 2 * z); %! assert_equal (Mt.FitInfo, M0.FitInfo); %! assert_equal (predict (Mt, X(1,:)), 9.907317073170731, 1e-13); %! assert_equal (loss (Mt, X, y), 35.174803723282551, 1e-11); %!test # the response transform can be set on a fitted ensemble %! Mdl = RegressionEnsemble (X, y, 'NumLearningCycles', 3, 'Learners', S); %! Mdl.ResponseTransform = @(z) z + 1; %! assert_equal (predict (Mdl, X(1,:)), 5.953658536585365, 1e-13); %! assert_equal (loss (Mdl, X, y), 1.176267398277276, 1e-12); %!test # compact keeps the trees and drops the data %! Mdl = RegressionEnsemble (X, y, 'NumLearningCycles', 3, 'Learners', S); %! C = compact (Mdl); %! assert_equal (class (C), 'CompactRegressionEnsemble'); %! assert_equal (predict (C, X), predict (Mdl, X)); ## Test input validation %!error RegressionEnsemble (X) %!error ... %! RegressionEnsemble (X, y, 'Method') %!error ... %! RegressionEnsemble (X, y, 'Foo', 1) %!error ... %! RegressionEnsemble (X, y, 1, 1) %!error ... %! RegressionEnsemble (X, y, 'Method', 1) %!error ... %! RegressionEnsemble (X, y, 'Method', 'AdaBoostM1') %!error ... %! RegressionEnsemble (X, y, 'NumLearningCycles', 0) %!error ... %! RegressionEnsemble (X, y, 'LearnRate', 0) %!error ... %! RegressionEnsemble (X, y, 'NPrint', 'on') %!error ... %! RegressionEnsemble (X, y, 'ResponseName', 1) %!error ... %! RegressionEnsemble (X, y, 'FResample', 2) %!error ... %! RegressionEnsemble (X, y, 'Replace', true) %!error ... %! RegressionEnsemble (X, y, 'Resample', true) %!error ... %! RegressionEnsemble (X, y, 'NumBins', 10) %!error ... %! RegressionEnsemble (X, y, 'Learners', 'svm') %!error ... %! RegressionEnsemble (X, y, 'Method', 'Bag') %!error ... %! RegressionEnsemble (X, y, 'Resample', 'on') %!error ... %! RegressionEnsemble (X, y, 'PredictorNames', {'a'}) %!error ... %! RegressionEnsemble (X, y, 'Weights', -ones (150, 1)) %!error ... %! resume (RegressionEnsemble (X, y, 'NumLearningCycles', 1)) %!error ... %! resume (RegressionEnsemble (X, y, 'NumLearningCycles', 1), 0) %!error ... %! resume (RegressionEnsemble (X, y, 'NumLearningCycles', 1), 1, 'NPrint') %!error ... %! resume (RegressionEnsemble (X, y, 'NumLearningCycles', 1), 1, 'Foo', 1) %!error ... %! resume (RegressionEnsemble (X, y, 'NumLearningCycles', 1), 1, ... %! 'NPrint', 0) %!error ... %! predict (RegressionEnsemble (X, y, 'NumLearningCycles', 1)) %!error ... %! loss (RegressionEnsemble (X, y, 'NumLearningCycles', 1), X) %!error ... %! resubLoss (RegressionEnsemble (X, y, 'NumLearningCycles', 1), 'Foo', 1) %!test # MATLAB parity: crossval equals cross-validating at fit time %! c = cvpartition (150, 'KFold', 4); %! M = RegressionEnsemble (X, y, 'NumLearningCycles', 2, 'Learners', S); %! CV = crossval (M, 'CVPartition', c); %! F = fitrensemble (X, y, 'NumLearningCycles', 2, 'Learners', S, ... %! 'CVPartition', c); %! assert_equal (class (CV), 'RegressionPartitionedEnsemble'); %! assert_equal (kfoldLoss (CV), kfoldLoss (F), 1e-15); %!error ... %! crossval (RegressionEnsemble (X, y, 'NumLearningCycles', 1), 'Foo', 1) %!shared X, y, tt, E %! load fisheriris %! X = meas(:,2:4); %! y = meas(:,1); %! tt = templateTree ('MaxNumSplits', 3); %! E = fitrensemble (X, y, 'Method', 'LSBoost', 'NumLearningCycles', 20, ... %! 'Learners', tt); %!test # MATLAB parity: the default penalties of regularize %! R = regularize (E).Regularization; %! assert_equal (fieldnames (R), {'Method'; 'TrainedWeights'; 'Lambda'; ... %! 'ResubstitutionMSE'; 'CombineWeights'}); %! assert_equal (R.Method, 'Lasso'); %! assert_equal (size (R.TrainedWeights), [20, 10]); %! assert_equal (R.Lambda(1), 0); %! assert_equal (R.Lambda([2, 10]), [0.0346843052641099, 34.6843052641099], ... %! 1e-12); %!test # MATLAB parity: lasso weights where R2024a converges %! R = regularize (E, 'Lambda', [0.01, 0.05, 0.1]).Regularization; %! assert_equal (R.TrainedWeights(:,[2, 3]), ... %! [0.998558425788861, 0.997116851577722; zeros(19, 2)], 1e-9); %! assert_equal (R.TrainedWeights(:,1), [0.99983699976111; ... %! 0.688155074556548; 0.205495438447582; 0.0983109617173514; ... %! 0; 0.314714964487384; zeros(14, 1)], 1e-6); %! assert_equal (R.ResubstitutionMSE(2:3), ... %! [0.141433481267387, 0.141649717399058], 1e-12); %! assert_equal (R.ResubstitutionMSE(1), 0.0981895827257545, 1e-6); %!test # MATLAB parity: observation weights enter the fit and the error %! Ew = fitrensemble (X, y, 'Method', 'LSBoost', 'NumLearningCycles', 20, ... %! 'Learners', tt, 'Weights', (1:150)' / 150); %! R = regularize (Ew, 'Lambda', 0.1).Regularization; %! assert_equal (R.TrainedWeights, [0.997413841751991; zeros(19, 1)], 1e-9); %! assert_equal (R.ResubstitutionMSE, 0.136057521685236, 1e-12); %! assert_equal (regularize (Ew).Regularization.Lambda(end), ... %! 38.6673940301219, 1e-12); %!test # MATLAB parity: the weights are never negative %! Elr = fitrensemble (X, y, 'Method', 'LSBoost', 'NumLearningCycles', 20, ... %! 'Learners', tt, 'LearnRate', 0.1); %! R = regularize (Elr, 'Lambda', [0, 0.01]).Regularization; %! assert_equal (all (R.TrainedWeights(:) >= 0), true); %!test # regularize leaves the trained weights as they were %! Er = regularize (E, 'Lambda', 0.1); %! assert_equal (Er.TrainedWeights, E.TrainedWeights); %!test # MATLAB parity: shrink keeps the weighted trees, largest first %! C = shrink (regularize (E, 'Lambda', 0.01)); %! assert_equal (class (C), 'CompactRegressionEnsemble'); %! assert_equal (C.CombineWeights, 'WeightedSum'); %! assert_equal (C.NumTrained, 5); %! assert_equal (C.TrainedWeights, [0.99983699976111; 0.688155074556548; ... %! 0.314714964487384; 0.205495438447582; ... %! 0.0983109617173514], 1e-6); %!test # MATLAB parity: a weight equal to the threshold is dropped %! Er = regularize (E, 'Lambda', 0.01); %! C = shrink (Er); %! assert_equal (shrink (Er, 'Threshold', C.TrainedWeights(5)).NumTrained, 4); %!test # MATLAB parity: shrink regularizes first when given penalties %! assert_equal (shrink (E, 'Lambda', 0.01).NumTrained, 5); %!test # MATLAB parity: an ensemble not regularized keeps its trees %! C = shrink (E); %! assert_equal (C.NumTrained, 20); %! assert_equal (C.TrainedWeights, ones (20, 1)); %!test # MATLAB parity: a shrunk bagged ensemble sums its weighted trees %! B = fitrensemble (X, y, 'Method', 'Bag', 'NumLearningCycles', 5); %! Br = regularize (B, 'Lambda', 0.001); %! C = shrink (Br); %! assert_equal (C.CombineWeights, 'WeightedSum'); %! P = zeros (3, 5); %! for t = 1:5 %! P(:,t) = predict (B.Trained{t}, X(1:3,:)); %! endfor %! assert_equal (predict (C, X(1:3,:)), ... %! P * Br.Regularization.TrainedWeights, 1e-12); %!test # MATLAB parity: cvshrink pools the held-out error over the folds %! cvp = cvpartition (150, 'KFold', 3); %! [vals, nlearn] = cvshrink (E, 'CVPartition', cvp, 'Lambda', [0.01, 0.1], ... %! 'Threshold', [0, 0.5]); %! sse = zeros (2, 2); %! counts = zeros (2, 2); %! for k = 1:3 %! tr = training (cvp, k); %! te = test (cvp, k); %! Ek = regularize (fitrensemble (X(tr,:), y(tr), 'Method', 'LSBoost', ... %! 'NumLearningCycles', 20, ... %! 'Learners', tt), 'Lambda', [0.01, 0.1]); %! for a = 1:2 %! for b = 1:2 %! thr = [0, 0.5]; %! Ck = shrink (Ek, 'WeightColumn', a, 'Threshold', thr(b)); %! sse(a,b) += sum ((y(te) - predict (Ck, X(te,:))) .^ 2) / 150; %! counts(a,b) += Ck.NumTrained; %! endfor %! endfor %! endfor %! assert_equal (vals, sse, 1e-12); %! assert_equal (nlearn, counts / 3); %!test # MATLAB parity: cvshrink takes the penalties of a regularized ensemble %! cvp = cvpartition (150, 'KFold', 3); %! vals = cvshrink (regularize (E, 'Lambda', [0.01, 0.1]), 'CVPartition', cvp); %! assert_equal (size (vals), [2, 1]); %!test # resume empties the regularization %! Er = resume (regularize (E, 'Lambda', 0.1), 2); %! assert_equal (Er.NumTrained, 22); %! assert_equal (Er.Regularization, []); %!error ... %! regularize (E, 'Lambda', -1) %!error ... %! regularize (E, 'MaxIter', 0) %!error ... %! regularize (E, 'RelTol', 0) %!error ... %! regularize (E, 'Npass', 3) %!error ... %! regularize (E, 'Threshold', 0.1) %!error ... %! regularize (E, 'Lambda') %!error ... %! shrink (E, 'WeightColumn', 1.5) %!error ... %! shrink (regularize (E, 'Lambda', 0.1), 'WeightColumn', 2) %!error ... %! shrink (E, 'Threshold', -1) %!error ... %! shrink (E, 'Threshold', [0, 1]) %!error ... %! cvshrink (E) %!error ... %! cvshrink (E, 'Lambda', 0.1, 'Foo', 1) %!shared X, yr %! k = (0:119)'; %! c = mod (k, 4) + 1; %! j = floor (k / 4); %! x2 = mod (k * 7, 10); %! X = [c, x2]; %! yb = (c == 1) | (c == 2 & mod (j, 4) != 0) | (c == 3 & mod (j, 4) == 0) ... %! | (x2 > 7); %! yr = [3; 1; 4; 1.5]; %! yr = yr(c) + 0.1 * sin (k) + 0.2 * x2; %! Xq = [1, 0; 3, 5; 5, 0; NaN, 2; 2.5, 9]; %!test # bagged regression trees and folds take the categorical predictors %! Mdl = fitrensemble (X, yr, 'Method', 'Bag', 'NumLearningCycles', 3, ... %! 'CategoricalPredictors', 1); %! assert_equal (Mdl.CategoricalPredictors, 1); %! assert_equal (Mdl.Trained{1}.CategoricalPredictors, 1); %! CV = crossval (Mdl, 'KFold', 3); %! assert_equal (CV.Trained{1}.Trained{1}.CategoricalPredictors, 1); %!error ... %! RegressionEnsemble (X, yr, 'CategoricalPredictors', 3) %!test # an ordinal column is a numeric predictor, as measured on R2024a %! o = categorical (repmat ({'lo'; 'hi'; 'lo'; 'mid'; 'hi'; 'lo'; 'mid'; ... %! 'hi'; 'lo'; 'mid'; 'hi'; 'lo'}, 2, 1), ... %! {'lo', 'mid', 'hi'}, 'Ordinal', true); %! y = 10 * double (o) + mod ((1:24)', 3); %! Mdl = fitrensemble (table (o, y), 'y', 'Method', 'Bag', ... %! 'NumLearningCycles', 3); %! assert_equal (Mdl.CategoricalPredictors, []); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,2:3); %! y = meas(:,1); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SW', 'PL'}); %! T.SL = y; %! Mdl = fitrensemble (T, 'SL'); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'SL'), a); %! assert_equal (loss (Mdl, T), a); ## Observation weights of class single or double %!error ... %! fitrensemble ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', 'Weights', ... %! int8 ([1; 1; 1; 1])) %!error ... %! fitrensemble ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', 'Weights', true (4, 1)) %!test %! ## Single weights are stored single, summing to one %! load fisheriris %! X = meas(:,2:4); %! y = meas(:,1); %! w = 1 + (1:150)' / 7; %! Mdl = fitrensemble (X, y, 'Weights', single (w), 'NumLearningCycles', 10); %! assert_equal (class (Mdl.W), 'single'); %! assert_equal (sum (double (Mdl.W)), 1, 1e-6); ## MATLAB parity: a row missing some predictors is kept, and dropped only ## when every predictor or the response is missing, as measured on R2024a %!shared emX, emY, emQ %! x1 = mod ((1:30)', 7) + (1:30)' / 10; %! x2 = mod ((1:30)', 5) * 2 - (1:30)' / 20; %! emX = [x1, x2]; %! emX([2, 9, 17], 1) = NaN; %! emX(23, 2) = NaN; %! emX(27, :) = NaN; %! emQ = [3, 1; 5, 4; 7, 2; NaN, 3]; %! emY = 2 * x1 - x2; %!test # boosting keeps the rows a predictor is missing from %! Mdl = fitrensemble (emX, emY, 'Method', 'LSBoost'); %! assert_equal (Mdl.NumObservations, 29); %! assert_equal (Mdl.RowsUsed, [true(26, 1); false; true(3, 1)]); %! assert_equal (rows (Mdl.X), 29); %!test # and so does bagging %! Mdl = fitrensemble (emX, emY, 'Method', 'Bag'); %! assert_equal (Mdl.NumObservations, 29); %!test # a row with a missing response is dropped %! y = emY; %! y(5) = NaN; %! Mdl = fitrensemble (emX, y); %! assert_equal (Mdl.NumObservations, 28); %! assert_equal (find (! Mdl.RowsUsed)', [5, 27]); %!test # the first two learners see the rows a predictor is missing from %! Mdl = fitrensemble (emX, emY, 'Method', 'LSBoost', 'NumLearningCycles', 2); %! assert_equal (predict (Mdl, emQ), [6.11640211640212; 6.10571428571429; ... %! 9.29183673469388; 5.04444114149821], ... %! 1e-12); statistics-release-2.1.2/inst/Machine_Learning/RegressionGAM.m000066400000000000000000004430161527311104300243140ustar00rootroot00000000000000## Copyright (C) 2023 Mohammed Azmat Khan ## Copyright (C) 2023-2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef RegressionGAM < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} RegressionGAM ## ## Generalized additive model for regression. ## ## A @qcode{RegressionGAM} object holds a generalized additive model fitted to ## a continuous response, and predicts the response for new data with the ## @code{predict} method. The model is a sum of one shape function per ## predictor, and optionally one per pair of predictors, each learned from the ## data rather than assumed. ## ## Two engines fit it. @qcode{'boostedtrees'}, the default, boosts shallow ## decision trees one predictor at a time; @qcode{'splines'} boosts smoothing ## splines. The properties describing the engine that was not used are empty. ## ## The object keeps its training data, which @code{resubPredict}, ## @code{resubLoss} and @code{crossval} work on; @code{compact} drops it and ## returns a @code{CompactRegressionGAM}, which still predicts. ## ## Create a @qcode{RegressionGAM} object with @code{fitrgam} or the class ## constructor. ## ## @seealso{fitrgam, CompactRegressionGAM, ClassificationGAM} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} X ## ## Predictor data ## ## A numeric matrix with one row per observation and one column per ## predictor of the training data. This property is read-only. ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} Y ## ## Response data ## ## A numeric column vector with one entry per observation of the ## training data. This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} NumObservations ## ## Number of observations ## ## A positive integer, the number of observations of the training data ## the model was fitted on, rows with a missing response excluded. This ## property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} RowsUsed ## ## Rows used for fitting ## ## A logical column vector with the same length as the observations in the ## original predictor data @var{X}, true for each row that was used for ## fitting the RegressionGAM model. It is empty, @qcode{[]}, ## when every observation was used, so a non-empty value means that rows ## holding missing values were dropped. This property is read-only. ## ## @end deftp RowsUsed = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} NumPredictors ## ## Number of predictors ## ## A positive integer, the number of predictors of the training data. ## This property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} PredictorNames ## ## Names of the predictor variables ## ## A cell array of character vectors naming the predictors, in the order ## they appear in the training data. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} ResponseName ## ## Response variable name ## ## A character vector naming the response variable @var{Y}. This ## property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A numeric vector holding the column of each predictor treated as ## categorical, and empty when none is. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} ExpandedPredictorNames ## ## Names of the expanded predictor variables ## ## A cell array of character vectors naming the predictors as the model ## sees them. It matches @code{PredictorNames} unless a categorical ## predictor was expanded into dummy variables. This property is ## read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} W ## ## Observation weights ## ## A numeric column vector with one entry per observation used for training, ## the @qcode{'Weights'} normalised to sum to one, and equal when none were ## given. It has the class of the @qcode{'Weights'} given, single or ## double. This property is read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} Intercept ## ## Intercept of the fitted model ## ## A numeric scalar, the mean of the response, which every additive term ## is measured against. This property is read-only. ## ## @end deftp Intercept = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} Formula ## ## Formula of the model ## ## A character vector naming the response and the terms of the model, as ## in @qcode{'Y ~ x1 + x2 + x1:x2'}, or empty when the model was not ## given one. This property is read-only. ## ## @end deftp Formula = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} Interactions ## ## Two-way interaction terms of the fitted model ## ## A @math{Kx2} matrix of predictor index pairs, one row per two-way term ## the model carries, and @code{zeros (0, 2)} when it carries none. It ## reports what was fitted rather than what was asked for, so a count of ## terms, @qcode{'all'}, a logical matrix and a formula all leave the same ## kind of value behind. This property is read-only. ## ## A main effect names one predictor and a higher-order term names three ## or more, and neither has a two-column form, so neither appears here. ## @code{IntMatrix} remains the complete record of every term fitted. ## ## @end deftp Interactions = zeros (0, 2); ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} Knots ## ## Knots of the spline fitting ## ## A numeric vector with one entry per predictor, the number of breaks ## the spline of that predictor is fitted over. This property is ## read-only. ## ## @end deftp Knots = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} Order ## ## Order of the spline fitting ## ## A numeric vector with one entry per predictor, the polynomial order of ## the spline of that predictor. This property is read-only. ## ## @end deftp Order = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} DoF ## ## Degrees of freedom of the spline fitting ## ## A numeric vector with one entry per predictor, the sum of its number ## of knots and its order. This property is read-only. ## ## @end deftp DoF = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} Tol ## ## Tolerance for convergence ## ## A positive scalar, the largest change in the residual sum of squares ## of a backfitting cycle that counts as converged. This property is ## read-only. ## ## @end deftp Tol = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} IsStandardDeviationFit ## ## Flag for a fitted standard deviation model ## ## A boolean flag, always @qcode{false}, as this class estimates the ## standard deviation of a prediction from the residuals of the fit ## rather than fitting a model for it. This property is read-only. ## ## @end deftp IsStandardDeviationFit = false; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} BaseModel ## ## Model without interaction terms ## ## A structure holding the intercept, the piecewise polynomial of each ## predictor, the number of backfitting cycles, the residuals and the ## residual sum of squares of the model fitted without interaction ## terms. This property is read-only. ## ## @end deftp BaseModel = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} ModelwInt ## ## Model with interaction terms ## ## A structure of the same fields as @code{BaseModel}, for the model ## fitted with the interaction terms, and empty when none was asked for. ## This property is read-only. ## ## @end deftp ModelwInt = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} IntMatrix ## ## Every term the model fits ## ## A logical matrix with one row per term and one column per predictor, ## true wherever the term multiplies that predictor. A row naming one ## predictor is a main effect, two an interaction, and three or more a ## higher-order term. This property is read-only. ## ## It is the complete record, where @code{Interactions} reports only the ## two-way terms, in the form MATLAB reports them. It is also the form ## the @qcode{'Interactions'} option takes back, so passing it to the ## constructor rebuilds a model over the same terms. ## ## @end deftp IntMatrix = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} BinEdges ## ## Bin edges of the predictors ## ## A cell array with one entry per predictor, holding that predictor's bin ## edges where the model discretized it before fitting. It is empty here ## and stays empty: this generalized additive model is built from splines, ## which take the predictors as they are, where MATLAB's is built from ## boosted trees and bins them. That difference is described in the class ## documentation. ## ## This property is read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} PairDetectionBinEdges ## ## Bin edges used to detect interactions ## ## A cell array with one row vector per predictor, holding the coarse cut ## points the residuals of the predictor phase were laid on while pairs ## were being tested. The grid is eight equal-frequency bins whatever the ## sample size, as MATLAB's is. It is empty when the model carries no ## interaction terms, and empty throughout under the spline engine, which ## does not bin. ## ## This property is read-only. ## ## @end deftp PairDetectionBinEdges = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} ModelParameters ## ## Parameters the model was fitted with ## ## A structure holding the fitting parameters. Under the boosted-tree ## engine it carries MATLAB's own fields, with @qcode{Type} reading ## @qcode{'regression'}; under the spline engine it describes that scheme ## instead, since none of the tree vocabulary applies to it. ## ## This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} ReasonForTermination ## ## Why each fitting phase stopped ## ## A structure with the fields @qcode{PredictorTrees} and ## @qcode{InteractionTrees}, each saying why that phase ended. A phase ## that never ran reports an empty character vector. It is empty under ## the spline engine, which has no tree budget to exhaust. ## ## This property is read-only. ## ## @end deftp ReasonForTermination = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} FitMethod ## ## Which engine fitted the model ## ## A character vector, either @qcode{'boostedtrees'} or ## @qcode{'splines'}. The default is @qcode{'boostedtrees'}, the scheme ## MATLAB's generalized additive model uses. @qcode{'splines'} selects ## the penalised-spline engine, an Octave extension with no MATLAB ## counterpart and the scheme this class fitted before version 1.9.0. The ## two engines take different arguments and an argument meant for one is ## refused by the other rather than ignored. ## ## This property is read-only. ## ## @end deftp FitMethod = 'boostedtrees'; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} TreeModel ## ## The fitted shape functions and interaction surfaces ## ## A structure with fields @qcode{ShapeValues}, @qcode{PairValues} and ## @qcode{Pairs}, holding what the boosted-tree engine fitted. MATLAB ## exposes no equivalent, reporting its bin edges but never the values on ## them, so this is an Octave extension. It is empty under the spline ## engine, whose fit lives in @code{BaseModel} and @code{ModelwInt}. ## ## This property is read-only. ## ## @end deftp TreeModel = []; ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} HyperparameterOptimizationResults ## ## Results of the hyperparameter optimization ## ## @strong{Always empty.} It is declared for MATLAB compatibility, where ## it holds what an automatic search over the hyperparameters found. This ## class fits the parameters it is given and runs no such search, so there ## is nothing to report. This property is read-only. ## ## @end deftp HyperparameterOptimizationResults = []; endproperties ## Properties a user may set after the model is built. Each one is ## validated by its set method below. properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {RegressionGAM} {property} ResponseTransform ## ## Transformation applied to the predicted response ## ## A function handle applied to the response the model predicts. Add or ## change it using dot notation, as in ## @qcode{@var{obj}.ResponseTransform = 'log'} or ## @qcode{@var{obj}.ResponseTransform = @@function_handle}. It defaults ## to @qcode{'none'}, the identity. ## ## @end deftp ResponseTransform = @(x) x; endproperties ## Readable by the counterpart class, which copies it, and kept out of ## the documented surface. properties (GetAccess = public, SetAccess = protected, Hidden) RTfun = @(y) y; ## How many trees each boosting phase actually fitted, which the budget ## in ModelParameters does not say: a phase may stop early. Hidden ## because MATLAB reports it on the partitioned classes and not on the ## model, and that is where this is read from. NumTrainedTrees = []; endproperties ## Set methods for the properties a user may assign. methods (Hidden) function this = set.ResponseTransform (this, val) [this.RTfun, this.ResponseTransform] = parseResponseTransform ... (val, 'RegressionGAM'); endfunction ## Custom display function disp (this) fprintf ("\n RegressionGAM\n\n"); ## Print selected properties fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); fprintf ("%+25s: %d\n", 'NumObservations', this.NumObservations); fprintf ("%+25s: %d\n", 'NumPredictors', this.NumPredictors); fprintf ("%+25s: '%s'\n", 'ResponseTransform', this.ResponseTransform); fprintf ("%+25s: %g\n", 'Intercept', this.Intercept); str = repmat ({'%d'}, 1, numel (this.Knots)); str = strcat ('[', strjoin (str, ' '), ']'); fprintf ("%+25s: %s\n", 'Knots', sprintf (str, this.Knots)); str = repmat ({'%d'}, 1, numel (this.Order)); str = strcat ('[', strjoin (str, ' '), ']'); fprintf ("%+25s: %s\n", 'Order', sprintf (str, this.Order)); fprintf ("%+25s: %g\n", 'Tol', this.Tol); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {RegressionGAM} {@var{obj} =} RegressionGAM (@var{X}, @var{Y}) ## @deftypefnx {RegressionGAM} {@var{obj} =} RegressionGAM (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {RegressionGAM} {@var{obj} =} RegressionGAM (@var{Tbl}, @var{formula}) ## @deftypefnx {RegressionGAM} {@var{obj} =} RegressionGAM (@var{Tbl}, @var{Y}) ## @deftypefnx {RegressionGAM} {@var{obj} =} RegressionGAM (@dots{}, @var{name}, @var{value}) ## ## Fit a generalized additive model for regression. ## ## @code{@var{obj} = RegressionGAM (@var{X}, @var{Y})} returns an object of ## class RegressionGAM, with matrix @var{X} containing the predictor data ## and vector @var{Y} containing the continuous response data. ## ## @itemize ## @item ## @var{X} must be a @math{N*P} numeric matrix of input data where rows ## correspond to observations and columns correspond to features or ## variables. @var{X} will be used to train the GAM model. ## @item ## @var{Y} must be @math{N*1} numeric vector containing the response data ## corresponding to the predictor data in @var{X}. @var{Y} must have same ## number of rows as @var{X}. ## @end itemize ## ## @code{@var{obj} = RegressionGAM (@dots{}, @var{name}, @var{value})} ## returns an object of class RegressionGAM with additional properties ## specified by @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Predictors'} @tab Predictor Variable names, specified as a ## row vector cell of strings with the same length as the columns in ## @var{X}. If omitted, the program will generate default variable names ## @qcode{(x1, x2, ..., xn)} for each column in @var{X}. ## ## @item @qcode{'responsename'} @tab Response Variable Name, specified as ## a string. If omitted, the default value is @qcode{'Y'}. ## ## @item @qcode{'formula'} @tab (spline option) a model specification given ## as a string in the form @qcode{'Y ~ terms'} where @qcode{Y} represents ## the response variable and @qcode{terms} the predictor variables. The ## formula can be used to specify a subset of variables for training model. ## For example: @qcode{'Y ~ x1 + x2 + x3 + x4 + x1:x2 + x2:x3'} specifies ## four linear terms for the first four columns of for predictor data, and ## @qcode{x1:x2} and @qcode{x2:x3} specify the two interaction terms for ## 1st-2nd and 3rd-4th columns respectively. Only these terms will be used ## for training the model, but @var{X} must have at least as many columns as ## referenced in the formula. If Predictor Variable names have been ## defined, then the terms in the formula must reference to those. When ## @qcode{'formula'} is specified, all terms used for training the model are ## referenced in the @qcode{IntMatrix} field of the @var{obj} class object ## as a matrix containing the column indexes for each term including both ## the predictors and the interactions used. ## ## @item @qcode{'interactions'} @tab a logical matrix, a positive integer ## scalar, or the string @qcode{'all'} for defining the interactions between ## predictor variables. When given a logical matrix, it must have the same ## number of columns as @var{X} and each row corresponds to a different ## interaction term combining the predictors indexed as @qcode{true}. Each ## interaction term is appended as a column vector after the available ## predictor column in @var{X}. When @qcode{'all'} is defined, then all ## possible combinations of interactions are appended in @var{X} before ## training. At the moment, parsing a positive integer has the same effect ## as the @qcode{'all'} option. When @qcode{'interactions'} is specified, ## only the interaction terms appended to @var{X} are referenced in the ## @qcode{IntMatrix} field of the @var{obj} class object. ## ## @item @qcode{'knots'} @tab (spline option) a scalar or a row vector with ## the same columns as @var{X}. It defines the knots for fitting a ## polynomial when training the GAM. As a scalar, it is expanded to a row ## vector. The default value is 5, hence expanded to ## @qcode{ones (1, columns (X)) * 5}. You can parse a row vector with ## different number of knots for each predictor variable to be fitted with, ## although not recommended. ## ## @item @qcode{'order'} @tab (spline option) a scalar or a row vector with ## the same columns as @var{X}. It defines the order of the polynomial when ## training the GAM. As a scalar, it is expanded to a row vector. The ## default values is 3, hence expanded to @qcode{ones (1, columns (X)) * 3}. ## You can parse a row vector with different number of polynomial order for ## each predictor variable to be fitted with, although not recommended. ## ## @item @qcode{'dof'} @tab (spline option) a scalar or a row vector with ## the same columns as @var{X}. It defines the degrees of freedom for ## fitting a polynomial when training the GAM. As a scalar, it is expanded ## to a row vector. The default value is 8, hence expanded to ## @qcode{ones (1, columns (X)) * 8}. You can parse a row vector with ## different degrees of freedom for each predictor variable to be fitted ## with, although not recommended. ## ## @item @qcode{'tol'} @tab (spline option) a positive scalar to set the ## tolerance for ## convergence during training. By default, it is set to @qcode{1e-3}. ## ## @end multitable ## ## A row marked @qcode{(spline option)} belongs to the spline ## engine and requires @qcode{'FitMethod', 'splines'}; passing one ## under the default boosted-tree engine is an error rather than ## being ignored. The boosted-tree engine's own options are ## documented under @code{fitrgam}. ## ## You can parse either a @qcode{'formula'} or an @qcode{'interactions'} ## optional parameter. Parsing both parameters will result an error. ## Accordingly, you can only pass up to two parameters among ## @qcode{'knots'}, @qcode{'order'}, and @qcode{'dof'} to define the ## required polynomial for training the GAM model. ## ## Two weak learners are available, selected by @code{FitMethod}. ## ## @qcode{'boostedtrees'}, the default, boosts one shallow decision tree per ## predictor in each round, which is the scheme MATLAB's generalized ## additive model uses. A second phase then boosts trees over pairs of ## predictors, where interactions are asked for. ## ## @qcode{'splines'} boosts a smoothing spline per predictor until the ## residual sum of squares changes by less than @qcode{'Tol'}. It has no ## MATLAB counterpart and is an Octave extension, kept because a smooth ## additive fit is a genuinely different and often better answer than a ## staircase of stumps. A standard deviation and a prediction interval are ## available from it alone. ## ## The two take different arguments, and an argument meant for one is ## refused by the other rather than ignored. ## ## The choice is visible in the properties. @code{Knots}, @code{Order}, ## @code{DoF}, @code{Formula}, @code{Tol}, @code{BaseModel}, ## @code{ModelwInt} and @code{IntMatrix} describe a spline fit and are empty ## under the boosted-tree engine, while @code{ModelParameters}, ## @code{ReasonForTermination}, @code{BinEdges}, ## @code{PairDetectionBinEdges} and @code{TreeModel} describe a tree fit and ## are empty under the spline engine. ## ## Fitted values are not expected to equal MATLAB's even under ## @qcode{'boostedtrees'}. The stopping rule and the step-reduction limit ## are not recoverable from anything MATLAB reports, so this engine ## documents its own; what the two share is the estimator and the reported ## surface, not the arithmetic. ## ## @seealso{fitrgam, regress, regress_gp} ## @end deftypefn function this = RegressionGAM (X, Y, varargin) ## Check for sufficient number of input arguments if (nargin < 2) error ("RegressionGAM: too few input arguments."); endif ## A table names its own predictors and says which hold levels ## An ordinal column is a numeric predictor here, as in R2024a [this, X, Y, varargin] = resolveTable (this, 'RegressionGAM', ... X, Y, varargin, true); ## Get training sample size and number of variables in training data nsample = rows (X); ndims_X = columns (X); ## Check correspondence between predictors and response if (nsample != rows (Y)) error ("RegressionGAM: number of rows in X and Y must be equal."); endif ## The response transform and the callable it names, unless one is ## given below ResponseTransform = 'none'; RTfun = @(y) y; ## Parse optional paired arguments optNames = {'PredictorNames', 'predictors', 'ResponseTransform', ... 'ResponseName', 'Formula', 'Interactions', 'Knots', ... 'Order', 'DoF', 'Tol', 'FitMethod', ... 'NumTreesPerPredictor', 'NumTreesPerInteraction', ... 'MaxNumSplitsPerPredictor', 'MaxNumSplitsPerInteraction', ... 'InitialLearnRateForPredictors', ... 'InitialLearnRateForInteractions', ... 'CategoricalPredictors', 'Weights', 'Verbose', 'NumPrint', ... 'MaxPValue'}; ## An empty default stands for one resolved once the options are ## known, so that giving an option can be told apart from leaving it ## out: 'Knots', 'Order' and 'DoF' are 5, 3 and 8 per predictor, any ## two determining the third, and 'Tol' is 1e-3. The boosted-tree ## options take MATLAB's own defaults, which ModelParameters reports and ## so are part of the surface being matched and not ours to improve: ## 300 and 100 trees, 1 and 4 splits, learning rates of 1, a ## 'MaxPValue' of 1, 'Verbose' 0 and 'NumPrint' 10. dfValues = {{}, [], [], [], [], [], [], [], [], [], 'boostedtrees', ... [], [], [], [], [], [], [], [], [], [], []}; [PredictorNames, PredictorsIn, RTin, ResponseName, Formula, ... Interactions, Knots, Order, DoF, Tol, FitMethod, ... NumTreesPerPredictor, NumTreesPerInteraction, ... MaxNumSplitsPerPredictor, MaxNumSplitsPerInteraction, ... InitialLearnRateForPredictors, InitialLearnRateForInteractions, ... CategoricalPredictors, Weights, Verbose, NumPrint, MaxPValue, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! isempty (ResponseName) && ! ischar (ResponseName)) error ("RegressionGAM: ResponseName must be a char string."); endif if (! isempty (Formula) && ! ischar (Formula) && ! islogical (Formula)) error ("RegressionGAM: Formula must be a string."); endif if (! isempty (Interactions) && ! ((isnumeric (Interactions) && isscalar (Interactions) && Interactions == fix (Interactions) && Interactions >= 0) || islogical (Interactions) || (ischar (Interactions) && strcmpi (Interactions, 'all')))) error ("RegressionGAM: invalid Interactions parameter."); endif if (! isempty (Knots) && (! isnumeric (Knots) || ! (isscalar (Knots) || isequal (size (Knots), [1, ndims_X])))) error ("RegressionGAM: invalid value for Knots."); endif if (! isempty (Order) && (! isnumeric (Order) || ! (isscalar (Order) || isequal (size (Order), [1, ndims_X])))) error ("RegressionGAM: invalid value for Order."); endif if (! isempty (DoF) && (! isnumeric (DoF) || ! (isscalar (DoF) || isequal (size (DoF), [1, ndims_X])))) error ("RegressionGAM: invalid value for DoF."); endif if (! isempty (Tol) && ! (isnumeric (Tol) && isscalar (Tol) && (Tol > 0))) error ("RegressionGAM: Tolerance must be a Positive scalar."); endif if (! (ischar (FitMethod) && isrow (FitMethod)) || ! any (strcmpi (FitMethod, {'boostedtrees', 'splines'}))) error (strcat ("RegressionGAM: 'FitMethod' must be", ... " 'boostedtrees' or 'splines'.")); endif FitMethod = tolower (FitMethod); if (! isempty (NumTreesPerPredictor) && (! isnumeric (NumTreesPerPredictor) || ! isscalar (NumTreesPerPredictor) || NumTreesPerPredictor < 1 || fix (NumTreesPerPredictor) != NumTreesPerPredictor)) error (strcat ("RegressionGAM: 'NumTreesPerPredictor'", ... " must be a positive integer value.")); endif if (! isempty (NumTreesPerInteraction) && (! isnumeric (NumTreesPerInteraction) || ! isscalar (NumTreesPerInteraction) || NumTreesPerInteraction < 1 || fix (NumTreesPerInteraction) != NumTreesPerInteraction)) error (strcat ("RegressionGAM: 'NumTreesPerInteraction'", ... " must be a positive integer value.")); endif if (! isempty (MaxNumSplitsPerPredictor) && (! isnumeric (MaxNumSplitsPerPredictor) || ! isscalar (MaxNumSplitsPerPredictor) || MaxNumSplitsPerPredictor < 1 || fix (MaxNumSplitsPerPredictor) != MaxNumSplitsPerPredictor)) error (strcat ("RegressionGAM:", ... " 'MaxNumSplitsPerPredictor' must be a", ... " positive integer value.")); endif if (! isempty (MaxNumSplitsPerInteraction) && (! isnumeric (MaxNumSplitsPerInteraction) || ! isscalar (MaxNumSplitsPerInteraction) || MaxNumSplitsPerInteraction < 1 || fix (MaxNumSplitsPerInteraction) != MaxNumSplitsPerInteraction)) error (strcat ("RegressionGAM:", ... " 'MaxNumSplitsPerInteraction' must be a", ... " positive integer value.")); endif if (! isempty (InitialLearnRateForPredictors) && (! isnumeric (InitialLearnRateForPredictors) || ! isscalar (InitialLearnRateForPredictors) || InitialLearnRateForPredictors <= 0 || InitialLearnRateForPredictors > 1)) error (strcat ("RegressionGAM:", ... " 'InitialLearnRateForPredictors' must be", ... " greater than 0 and at most 1.")); endif if (! isempty (InitialLearnRateForInteractions) && (! isnumeric (InitialLearnRateForInteractions) || ! isscalar (InitialLearnRateForInteractions) || InitialLearnRateForInteractions <= 0 || InitialLearnRateForInteractions > 1)) error (strcat ("RegressionGAM:", ... " 'InitialLearnRateForInteractions' must be", ... " greater than 0 and at most 1.")); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("RegressionGAM: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isreal (Weights) && isvector (Weights))) error ("RegressionGAM: 'Weights' must be a numeric vector."); endif if (! isempty (Weights) && numel (Weights) != rows (X)) error (strcat ("RegressionGAM: 'Weights' must have one", ... " element per row of X.")); endif if (! isempty (Weights) && (any (Weights(:) < 0) || ! all (isfinite (Weights(:))))) error (strcat ("RegressionGAM: 'Weights' must hold", ... " finite non-negative values.")); endif if (! isempty (Verbose) && (! isnumeric (Verbose) || ! isscalar (Verbose) || Verbose < 0 || fix (Verbose) != Verbose)) error (strcat ("RegressionGAM: 'Verbose' must be a", ... " non-negative integer value.")); endif if (! isempty (NumPrint) && (! isnumeric (NumPrint) || ! isscalar (NumPrint) || NumPrint < 1 || fix (NumPrint) != NumPrint)) error (strcat ("RegressionGAM: 'NumPrint' must be a positive", ... " integer value.")); endif if (! isempty (MaxPValue) && (! isnumeric (MaxPValue) || ! isscalar (MaxPValue) || MaxPValue < 0 || MaxPValue > 1)) error (strcat ("RegressionGAM: 'MaxPValue' must be", ... " between 0 and 1.")); endif ## 'Predictors' is another name for 'PredictorNames' if (isempty (PredictorNames)) PredictorNames = PredictorsIn; endif if (! isempty (PredictorNames)) if (! iscellstr (PredictorNames)) error (strcat ("RegressionGAM: PredictorNames must be a", ... " cellstring array.")); elseif (columns (PredictorNames) != columns (X)) error (strcat ("RegressionGAM: PredictorNames must have same", ... " number of columns as X.")); endif endif if (! isempty (RTin)) [RTfun, ResponseTransform] = parseResponseTransform (RTin, ... 'RegressionGAM'); endif if (! isempty (args)) error ("RegressionGAM: invalid optional paired argument."); endif ## An argument belongs to one engine or the other, and asking for one ## the chosen engine cannot honour is refused rather than ignored. ## An option was given when it is not empty, its default being filled ## in below. splineOnly = {'knots', Knots; 'order', Order; 'dof', DoF; ... 'formula', Formula; 'tol', Tol}; treeOnly = {'numtreesperpredictor', NumTreesPerPredictor; ... 'numtreesperinteraction', NumTreesPerInteraction; ... 'maxnumsplitsperpredictor', MaxNumSplitsPerPredictor; ... 'maxnumsplitsperinteraction', MaxNumSplitsPerInteraction; ... 'initiallearnrateforpredictors', ... InitialLearnRateForPredictors; ... 'initiallearnrateforinteractions', ... InitialLearnRateForInteractions; ... 'maxpvalue', MaxPValue; 'verbose', Verbose; ... 'numprint', NumPrint; 'weights', Weights; ... 'categoricalpredictors', CategoricalPredictors}; if (strcmp (FitMethod, 'boostedtrees')) clash = splineOnly(! cellfun (@isempty, splineOnly(:,2)), 1); if (! isempty (clash)) error (strcat ("RegressionGAM: '", clash{1}, "' is a parameter", ... " of the spline engine and cannot be used with", ... " 'FitMethod' 'boostedtrees'.")); endif else clash = treeOnly(! cellfun (@isempty, treeOnly(:,2)), 1); if (! isempty (clash)) error (strcat ("RegressionGAM: '", clash{1}, "' is a parameter", ... " of the boosted-tree engine and cannot be used", ... " with 'FitMethod' 'splines'.")); endif endif ## A model is described either by a formula or by its interactions if (! isempty (Formula) && ! isempty (Interactions)) error (strcat ("RegressionGAM: 'Formula' and 'Interactions'", ... " cannot be given together.")); endif ## Any two of 'Knots', 'Order' and 'DoF' determine the third, DoF being ## Knots plus Order; one given alone keeps the default of another. if (! isempty (Knots) && ! isempty (Order) && ! isempty (DoF)) error (strcat ("RegressionGAM: at most two of 'Knots', 'Order'", ... " and 'DoF' may be given.")); endif if (isempty (DoF)) if (isempty (Knots)) Knots = ones (1, ndims_X) * 5; endif if (isempty (Order)) Order = ones (1, ndims_X) * 3; endif DoF = Knots + Order; elseif (isempty (Knots)) if (isempty (Order)) Order = ones (1, ndims_X) * 3; endif Knots = DoF - Order; else Order = DoF - Knots; endif ## The defaults of the remaining engine options if (isempty (Tol)) Tol = 1e-3; endif if (isempty (NumTreesPerPredictor)) NumTreesPerPredictor = 300; endif if (isempty (NumTreesPerInteraction)) NumTreesPerInteraction = 100; endif if (isempty (MaxNumSplitsPerPredictor)) MaxNumSplitsPerPredictor = 1; endif if (isempty (MaxNumSplitsPerInteraction)) MaxNumSplitsPerInteraction = 4; endif if (isempty (InitialLearnRateForPredictors)) InitialLearnRateForPredictors = 1; endif if (isempty (InitialLearnRateForInteractions)) InitialLearnRateForInteractions = 1; endif if (isempty (MaxPValue)) MaxPValue = 1; endif if (isempty (Verbose)) Verbose = 0; endif if (isempty (NumPrint)) NumPrint = 10; endif ## Assign original X and Y data to the RegressionGAM object this.X = X; this.Y = Y; ## An observation is dropped only when its response is missing. A row ## whose predictors hold missing values is kept and reported as used, ## and the boosted-tree fit uses it, as R2024a does, the row sitting in ## the root of each tree it cannot be split by; the spline fit below ## draws on the complete observations alone. RowsUsed = ! isnan (Y(:)); Yret = Y(RowsUsed); Xret = X(RowsUsed, :); if (isempty (Weights)) Wret = ones (rows (Xret), 1); else Wret = Weights(RowsUsed); Wret = Wret(:); endif this.X = Xret; this.Y = Yret; if (strcmp (FitMethod, 'boostedtrees')) cobs = true (rows (Xret), 1); else cobs = ! any (isnan (Xret), 2); endif Y = Yret(cobs); X = Xret(cobs, :); ## Check X and Y contain valid data if (! isnumeric (X) || ! all (isfinite (X(:)) | isnan (X(:)))) error ("RegressionGAM: invalid values in X."); endif if (! isnumeric (Y) || ! all (isfinite (Y(:)))) error ("RegressionGAM: invalid values in Y."); endif ## Assign the number of observations and their corresponding indices ## on the original data, which will be used for training the model, ## to the RegressionGAM object this.NumObservations = rows (this.X); ## RowsUsed is left empty when every observation was used, as in MATLAB if (all (RowsUsed)) this.RowsUsed = []; else this.RowsUsed = RowsUsed; endif ## Assign the number of original predictors to the RegressionGAM object this.NumPredictors = ndims_X; ## Generate default predictors and response variable names (if necessary) if (isempty (PredictorNames)) for i = 1:ndims_X PredictorNames {i} = strcat ("x", num2str (i)); endfor endif if (isempty (ResponseName)) ResponseName = 'Y'; endif ## Assign predictors and response variable names this.PredictorNames = PredictorNames; this.ResponseName = ResponseName; ## A scalar 'Knots', 'Order' or 'DoF' applies to every predictor, which ## the validation above accepts but nothing expanded, so the fit indexed ## past the end of a scalar for the second predictor onwards. if (isscalar (Knots)) Knots = repmat (Knots, 1, ndims_X); endif if (isscalar (Order)) Order = repmat (Order, 1, ndims_X); endif DoF = Knots + Order; ## Assign remaining optional parameters this.Formula = Formula; this.Interactions = Interactions; this.Knots = Knots; this.Order = Order; this.DoF = DoF; this.Tol = Tol; this.ResponseTransform = ResponseTransform; this.RTfun = RTfun; ## Bookkeeping MATLAB reports alongside the fit ## A categorical predictor is fitted on the indices of its levels, ## which the engine is handed in place of the values. this.CategoricalPredictors = []; if (! isempty (CategoricalPredictors)) [CP, errmsg] = dummyCoding (X, CategoricalPredictors, ... this.PredictorNames); if (! isempty (errmsg)) error ("RegressionGAM: %s", errmsg); endif this.CategoricalPredictors = CP.Index; Levels = cell (1, columns (X)); Levels(CP.Index) = CP.Levels(CP.Index); this.TreeModel = struct ('CategoricalLevels', {Levels}); endif this.ExpandedPredictorNames = PredictorNames; ## The weights over their sum, as MATLAB reports them; the boosted fit ## sees them on the rows it is fitted to. ## The weights keep their class in the model; every computation runs on ## them as double. this.W = cast (double (Wret) / sum (double (Wret)), class (Wret)); this.IsStandardDeviationFit = false; this.FitMethod = FitMethod; if (strcmp (FitMethod, 'boostedtrees')) ## The spline parameters describe a scheme that did not run, so they ## are left empty rather than reporting numbers nothing used. this.Knots = []; this.Order = []; this.DoF = []; this.Tol = []; this = this.fitBoosted (X, Y, Interactions, ... NumTreesPerPredictor, ... NumTreesPerInteraction, ... MaxNumSplitsPerPredictor, ... MaxNumSplitsPerInteraction, ... InitialLearnRateForPredictors, ... InitialLearnRateForInteractions, MaxPValue, ... Verbose, NumPrint); return; endif ## Fit the basic model Inter = mean (Y); [iter, param, res, RSS] = this.fitGAM (X, Y, Inter, Knots, Order); this.BaseModel.Intercept = Inter; this.BaseModel.Parameters = param; this.BaseModel.Iterations = iter; this.BaseModel.Residuals = res; this.BaseModel.RSS = RSS; this.Intercept = Inter; ## Handle interaction terms (if given) if (! isempty (Formula) || ! isempty (Interactions)) this = this.fitModelwInt (X, Y, Inter, Knots, Order, DoF); endif ## The property MATLAB reports is the two-way terms the fitted model ## carries, as predictor index pairs, whatever form they were asked for ## in. The term matrix stays the complete record: it also holds the ## main effects a formula names and any term above two predictors, ## neither of which has a two-column form. this.Interactions = interactionPairs (this.IntMatrix); ## The spline scheme has no tree vocabulary to report, so its parameter ## struct describes itself instead. this.ModelParameters = struct ('Knots', this.Knots, ... 'Order', this.Order, ... 'DoF', this.DoF, ... 'Formula', this.Formula, ... 'Interactions', this.Interactions, ... 'Tol', this.Tol); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionGAM} {@var{obj} =} addInteractions (@var{obj}, @var{interactions}) ## ## Add interaction terms to a fitted model. ## ## @code{@var{obj} = addInteractions (@var{obj}, @var{interactions})} fits ## the interaction terms named by @var{interactions} on top of the terms ## the model already carries and returns the updated model. The univariate ## fit is left alone, so @code{predict} with ## @qcode{'IncludeInteractions'} set @qcode{false} answers exactly as it ## answered before. ## ## @var{interactions} takes the forms the constructor's ## @qcode{'Interactions'} option takes: a nonnegative integer count of ## terms, a logical matrix with a column per predictor, or @qcode{'all'}. ## ## A model already carrying interaction terms is not extended, which is ## what MATLAB refuses too. A model fitted from a @qcode{'Formula'} names ## every term it has, interactions among them, and is refused for the same ## reason. ## ## Which terms a count selects is this implementation's own: they are ## taken in the order @code{nchoosek} lists the pairs, where MATLAB ranks ## them by how much each contributes. The constructor's option chooses ## the same way, so the two agree with each other. ## ## @seealso{fitrgam, RegressionGAM} ## @end deftypefn function this = addInteractions (this, interactions) if (nargin != 2) print_usage (); endif ## Which store already holds interaction terms depends on the engine. hasInt = ! isempty (this.IntMatrix); if (strcmp (this.FitMethod, 'boostedtrees') && ! isempty (this.TreeModel)) hasInt = ! isempty (this.TreeModel.Pairs); endif if (hasInt) error (strcat ("RegressionGAM.addInteractions: adding interaction", ... " terms to a model that already includes them is", ... " not supported.")); endif if (! ((isnumeric (interactions) && isscalar (interactions) && interactions == fix (interactions) && interactions >= 0) || islogical (interactions) || (ischar (interactions) && strcmpi (interactions, 'all')))) error (strcat ("RegressionGAM.addInteractions: invalid", ... " 'Interactions' parameter.")); endif ## Under the boosted-tree engine the interaction phase simply runs now, ## starting from the predictor phase the model already carries, so a ## model with interactions added is the model the constructor would ## have built had it been asked for them. if (strcmp (this.FitMethod, 'boostedtrees')) cobs = true (rows (this.X), 1); [Xfit, E, cat] = gamCatCode (this.TreeModel, this.X(cobs, :), ... this.BinEdges); Yfit = this.Y(cobs); MP = this.ModelParameters; f = gamboostpredict (E, this.TreeModel.ShapeValues, ... Xfit, this.Intercept); res = Yfit - f; wanted = -1; pairs = zeros (0, 2); if (ischar (interactions)) wanted = Inf; elseif (isscalar (interactions) && ! isempty (interactions)) wanted = interactions; elseif (! isempty (interactions)) pairs = interactionPairs (logical (interactions)); endif if (wanted > 0 && columns (Xfit) > 1) S = gamboostpairs (Xfit, res, cat); pval = fcdf (S.F, S.DF1, S.DF2, 'upper'); pval(S.DF1 <= 0) = 1; [pval, ord] = sort (pval); ranked = S.Pairs(ord, :); ranked = ranked(pval <= MP.MaxPValue, :); if (isfinite (wanted) && rows (ranked) > wanted) ranked = ranked(1:wanted, :); endif pairs = ranked; ## As in fitBoosted: a single split is no interaction. if (MP.MaxNumSplitsPerInteraction < 2) pairs = zeros (0, 2); endif this.PairDetectionBinEdges = S.BinEdges(:); this.PairDetectionBinEdges(cat) = {[]}; endif reason = this.ReasonForTermination; ntrees = this.NumTrainedTrees; if (! isempty (pairs)) I = gamboostinter (Xfit, Yfit, f, 2, pairs, ... MP.NumTreesPerInteraction, ... MP.InitialLearnRateForInteractions, ... MP.MaxNumSplitsPerInteraction, ... double (this.W(cobs)), cat); ## A pair tree splitting on one predictor alone is a main effect and ## adds nothing, so a fit made only of such trees keeps no pair, as ## R2024a keeps none. if (I.NumTrees == 0) pairs = zeros (0, 2); warning (strcat ("RegressionGAM: model does not include", ... " interaction terms because all interaction", ... " terms have p-values greater than the", ... " 'MaxPValue' value, or the software was", ... " unable to improve the model fit.")); endif endif if (! isempty (pairs)) this.Intercept = this.Intercept + I.Intercept; if (wanted <= 0) this.PairDetectionBinEdges = I.PairBinEdges(:); this.PairDetectionBinEdges(cat) = {[]}; endif this.TreeModel.PairValues = I.PairValues; this.TreeModel.PairEdges = I.PairEdges; this.TreeModel.PairMissing = I.PairMissing; this.TreeModel.PairIntercept = I.Intercept; reason.InteractionTrees = I.ReasonForTermination; ntrees.InteractionTrees = I.NumTrees; endif this.TreeModel.Pairs = pairs; if (isempty (pairs)) this.TreeModel.PairIntercept = 0; endif this.Interactions = pairs; this.ReasonForTermination = reason; this.NumTrainedTrees = ntrees; MP.Interactions = interactions; this.ModelParameters = MP; return; endif ## parseInteractions reads the specification from the property, which ## afterwards holds the pairs the fit settled on, exactly as the ## constructor leaves it. this.Interactions = interactions; this.IntMatrix = this.parseInteractions (); ## The fit sees the complete observations, prepared as the constructor ## prepares them. Knots, Order and DoF are held unexpanded on the ## object and are widened to the interaction columns by fitModelwInt. cobs = ! any (isnan (this.X), 2); Xfit = this.X(cobs, :); Yfit = this.Y(cobs); this = this.fitModelwInt (Xfit, Yfit, mean (Yfit), this.Knots, ... this.Order, this.DoF); this.Interactions = interactionPairs (this.IntMatrix); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionGAM} {@var{yFit} =} predict (@var{obj}, @var{Xfit}) ## @deftypefnx {RegressionGAM} {@var{yFit} =} predict (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {RegressionGAM} {[@var{yFit}, @var{ySD}, @var{yInt}] =} predict (@dots{}) ## ## Predict new data points using generalized additive model regression ## object. ## ## @code{@var{yFit} = predict (@var{obj}, @var{Xfit}} returns a vector of ## predicted responses, @var{yFit}, for the predictor data in matrix ## @var{Xfit} based on the Generalized Additive Model in @var{obj}. ## @var{Xfit} must have the same number of features/variables as the ## training data in @var{obj}. Every row is predicted. Under boosted ## trees a missing value adds nothing from a main effect, and an ## interaction term takes the value its trees give a row missing that ## predictor, so a row missing every predictor predicts the intercept; ## under splines a row holding a missing value is predicted as ## @code{NaN}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{RegressionGAM} class object. ## @end itemize ## ## @code{[@var{yFit}, @var{ySD}, @var{yInt}] = predict (@var{obj}, ## @var{Xfit}} ## also returns the standard deviations, @var{ySD}, and prediction ## intervals, ## @var{yInt}, of the response variable @var{yFit}, evaluated at each ## observation in the predictor data @var{Xfit}. ## ## @code{@var{yFit} = predict (@dots{}, @var{Name}, @var{Value})} returns ## the ## aforementioned results with additional properties specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.28 0.7 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'alpha'} @tab significance level of the prediction ## intervals @var{yInt}, specified as scalar in range @qcode{[0,1]}. The ## default value is 0.05, which corresponds to 95% prediction intervals. ## ## @item @qcode{'includeinteractions'} @tab a boolean flag to include ## interactions to predict new values based on @var{Xfit}. By default, ## @qcode{'includeinteractions'} is @qcode{true} when the GAM model in ## @var{obj} ## contains a @qcode{obj.Formula} or @qcode{obj.Interactions} fields. ## Otherwise, is set to @qcode{false}. If set to @qcode{true} when no ## interactions are present in the trained model, it will result to an ## error. If set to ## @qcode{false} when using a model that includes interactions, the ## predictions ## will be made on the basic model without any interaction terms. This way ## you can make predictions from the same GAM model without having to ## retrain it. ## @end multitable ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: one ## the model was not fitted on is passed over, one it needs and cannot ## find is named, and a value holding a level is coded as that level ## was coded at fitting. ## @seealso{fitrgam, RegressionGAM} ## @end deftypefn function [yFit, ySD, yInt] = predict (this, Xfit, varargin) ## Check for sufficient input arguments if (nargin < 2) error ("RegressionGAM.predict: too few arguments."); endif ## A table is read by the names the model was fitted on Xfit = tableColumns (this, 'RegressionGAM.predict', Xfit); ## Check for valid XC if (isempty (Xfit)) error ("RegressionGAM.predict: Xfit is empty."); elseif (columns (this.X) != columns (Xfit)) error (strcat ("@RegressionGAM/predict: Xfit must have the same", ... " number of features (columns) as in the GAM model.")); endif ## Default values for Name-Value Pairs alpha = 0.05; ## Which store holds the interaction terms depends on the engine: the ## spline scheme keeps them as extra columns described by IntMatrix, ## the boosted-tree scheme as surfaces over predictor pairs. hasInt = ! isempty (this.IntMatrix); if (strcmp (this.FitMethod, 'boostedtrees') && ! isempty (this.TreeModel)) hasInt = ! isempty (this.TreeModel.Pairs); endif if (! hasInt) incInt = false; else incInt = true; endif ## Parse optional paired arguments; interactions are included when the ## model has them if (mod (numel (varargin), 2) != 0) error (strcat ("RegressionGAM.predict: optional arguments must be", ... " given in Name-Value pairs.")); endif optNames = {'IncludeInteractions', 'Alpha'}; dfValues = {incInt, alpha}; [incInt, alpha, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! islogical (incInt) || (incInt != 0 && incInt != 1)) error (strcat ("RegressionGAM.predict: includeinteractions must be", ... " a logical value.")); endif if (incInt && ! hasInt) error (strcat ("RegressionGAM.predict: trained model does not", ... " include any interactions.")); endif if (! (isnumeric (alpha) && isscalar (alpha) && alpha > 0 && alpha < 1)) error (strcat ("RegressionGAM.predict: alpha must be a scalar", ... " value between 0 and 1.")); endif if (! isempty (args)) error ("RegressionGAM.predict: invalid optional paired argument."); endif ## Choose whether interactions must be included. The reshaping is done ## by gamTerms rather than inline, because the training data has to be ## reshaped exactly the same way further down: a model built with ## interactions or with a formula has a term for every column of the ## matrix it was fitted on, and that is no longer the stored X. ## The boosted-tree engine keeps its fit as step functions over bins, so ## a term is a lookup rather than a spline evaluation and the whole ## prediction is one call. Everything after it is shared. if (strcmp (this.FitMethod, 'boostedtrees') && ! isempty (this.TreeModel)) ## Excluding the interactions means excluding the constant they ## handed the intercept as well. [Xb, E] = gamCatCode (this.TreeModel, Xfit, this.BinEdges); interc = this.Intercept; if (! incInt && isfield (this.TreeModel, 'PairIntercept')) interc = interc - this.TreeModel.PairIntercept; endif if (! incInt || isempty (this.TreeModel.Pairs)) yFit = gamboostpredict (E, ... this.TreeModel.ShapeValues, Xb, ... interc); else [PE, PM] = gamPairEdges (this.TreeModel, this.PairDetectionBinEdges); yFit = gamboostpredict (E, ... this.TreeModel.ShapeValues, Xb, ... interc, 0, PE, ... this.TreeModel.PairValues, ... this.TreeModel.Pairs, PM); endif yFit = this.RTfun (yFit); if (nargout > 1) error (strcat ("RegressionGAM.predict: a standard deviation is", ... " only available from a model fitted with", ... " 'FitMethod' 'splines'.")); endif return; endif if (incInt) Xfit = gamTerms (Xfit, this.IntMatrix, isempty (this.Formula)); ## Get parameters and intercept vectors from model with interactions params = this.ModelwInt.Parameters; Interc = this.ModelwInt.Intercept; ## Update length of DoF vector DoF = ones (1, columns (Xfit)) * this.DoF(1); else ## Get parameters and intercept vectors from base model params = this.BaseModel.Parameters; Interc = this.BaseModel.Intercept; ## Get DoF from model DoF = this.DoF; endif ## Predict values from testing data yFit = predict_val (params, Xfit, Interc); yFit = this.RTfun (yFit); ## Predict Standard Deviation and Intervals of estimated data if requested if (nargout > 1) ## Ensure that RowsUsed in the model are selected used = true (rows (this.X), 1); Y = this.Y(used); X = this.X(used, :); ## Reshape the training data as the prediction data was reshaped. It ## used to be passed as stored, so the terms and the columns disagreed ## whenever the model was built with interactions or with a formula: ## with more terms than columns the residuals came from a truncated ## model and ySD was several times too large, and with fewer terms than ## columns the call raised out of bound. if (incInt) X = gamTerms (X, this.IntMatrix, isempty (this.Formula)); endif ## Predict response from training predictor data with the trained model yrs = predict_val (params, X , Interc); yrs_fit = predict_val (params, Xfit, Interc); ## Get the residuals between predicted and actual response data rs = Y - yrs; var_rs = var (rs); t_mul = tinv (1 - alpha / 2, this.DoF); ySD = sqrt (var_rs) * ones (rows (yFit), 1); if (nargout > 2) moe = t_mul(1) * ySD; lower = this.RTfun (yrs_fit - moe); upper = this.RTfun (yrs_fit + moe); yInt = [lower, upper]; endif endif endfunction ## -*- texinfo -*- ## @deftypefn {RegressionGAM} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {RegressionGAM} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {RegressionGAM} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {RegressionGAM} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Regression loss of a generalized additive model. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} returns the weighted ## mean squared error of the model on the rows of @var{X} against the true ## response @var{Y}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{L} = loss (@dots{}, @var{name}, @var{value})} accepts the ## following name-value pairs: ## ## @itemize ## @item ## @qcode{"LossFun"} selects the loss, either @qcode{"mse"}, the default, ## or a function handle taking the true response, the predicted response ## and the weights, and returning a numeric scalar. ## ## @item ## @qcode{"Weights"} holds one weight per row of @var{X}, normalised to ## sum to one before it is applied. ## @end itemize ## ## @seealso{RegressionGAM, fitrgam, RegressionGAM.predict} ## @end deftypefn function L = loss (this, X, Y, varargin) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("RegressionGAM.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("RegressionGAM.loss: Name-Value arguments must", ... " be in pairs.")); endif [X, Y] = checkXY_ (this, X, Y, 'loss'); ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights'}; dfValues = {'mse', []}; [LossFun, W, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (is_function_handle (LossFun) || (ischar (LossFun) && isrow (LossFun)))) error (strcat ("RegressionGAM.loss: 'LossFun' must be a character", ... " vector or a function handle.")); endif if (ischar (LossFun) && ! strcmpi (LossFun, 'mse')) error ("RegressionGAM.loss: unsupported 'LossFun' value."); endif errmsg = weightsClass (W); if (! isempty (errmsg)) error ("RegressionGAM.loss: %s", errmsg); endif if (! isempty (W) && ! (isnumeric (W) && isvector (W))) error ("RegressionGAM.loss: 'Weights' must be a numeric vector."); endif if (! isempty (W) && numel (W) != rows (X)) error (strcat ("RegressionGAM.loss: size of 'Weights' must equal", ... " the number of rows in X.")); endif if (! isempty (args)) error ("RegressionGAM.loss: invalid optional paired argument."); endif if (isempty (W)) W = ones (rows (X), 1); endif ## Weights are normalized to sum to one, as MATLAB does, so a loss is ## a weighted average rather than a weighted sum. W = double (W(:)); W = W / sum (W); yFit = predict (this, X); Y = Y(:); if (is_function_handle (LossFun)) L = LossFun (Y, yFit, W); if (! (isnumeric (L) && isscalar (L))) error (strcat ("RegressionGAM.loss: 'LossFun' must return a", ... " numeric scalar.")); endif else L = sum (W .* (Y - yFit) .^ 2); endif endfunction ## -*- texinfo -*- ## @deftypefn {RegressionGAM} {@var{yFit} =} resubPredict (@var{obj}) ## ## Predict the training response with the model it was fitted on. ## ## @code{@var{yFit} = resubPredict (@var{obj})} is @code{predict} applied ## to the observations the model was fitted on. ## ## @seealso{RegressionGAM, fitrgam, RegressionGAM.predict} ## @end deftypefn function yFit = resubPredict (this) used = true (rows (this.X), 1); yFit = predict (this, this.X(used, :)); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionGAM} {@var{L} =} resubLoss (@var{obj}) ## @deftypefnx {RegressionGAM} {@var{L} =} resubLoss (@dots{}, @var{name}, @var{value}) ## ## Regression loss of a generalized additive model on its training data. ## ## @code{@var{L} = resubLoss (@var{obj})} returns the weighted mean ## squared error of the model on the data it was fitted on. It accepts ## the same @qcode{Name-Value} pairs as @code{loss}. ## ## @seealso{RegressionGAM, fitrgam, RegressionGAM.loss} ## @end deftypefn function L = resubLoss (this, varargin) used = true (rows (this.X), 1); L = loss (this, this.X(used, :), this.Y(used), varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionGAM} {@var{CVMdl} =} crossval (@var{obj}) ## @deftypefnx {RegressionGAM} {@var{CVMdl} =} crossval (@dots{}, @var{name}, @var{value}) ## ## Cross validate a Generalized Additive Model regression object. ## ## @code{@var{CVMdl} = crossval (@var{obj})} returns a cross-validated ## model object, @var{CVMdl}, from a trained model, @var{obj}, using ## 10-fold cross-validation by default. ## ## @code{@var{CVMdl} = crossval (@var{obj}, @var{name}, @var{value})} ## specifies additional name-value pair arguments to customize the ## cross-validation process. ## ## @multitable @columnfractions 0.28 0.7 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'KFold'} @tab Specify the number of folds to use in ## k-fold cross-validation. @code{"KFold", @var{k}}, where @var{k} is an ## integer greater than 1. ## ## @item @qcode{'Holdout'} @tab Specify the fraction of the data to ## hold out for testing. @code{"Holdout", @var{p}}, where @var{p} is a ## scalar in the range @math{(0,1)}. ## ## @item @qcode{'Leaveout'} @tab Specify whether to perform ## leave-one-out cross-validation. @code{"Leaveout", @var{Value}}, where ## @var{Value} is 'on' or 'off'. ## ## @item @qcode{'CVPartition'} @tab Specify a @qcode{cvpartition} ## object used for cross-validation. @code{"CVPartition", @var{cv}}, ## where @code{isa (@var{cv}, "cvpartition")} = 1. ## ## @end multitable ## ## @seealso{fitrgam, RegressionGAM, cvpartition, ## RegressionPartitionedModel} ## @end deftypefn function CVMdl = crossval (this, varargin) if (numel (varargin) == 1) error (strcat ("RegressionGAM.crossval: Name-Value", ... " arguments must be in pairs.")); elseif (numel (varargin) > 2) error (strcat ("RegressionGAM.crossval: specify only", ... " one of the optional Name-Value paired arguments.")); endif if (this.NumObservations < 10) numFolds = this.NumObservations; else numFolds = 10; endif Holdout = []; Leaveout = 'off'; CVPartition = []; while (numel (varargin) > 0) switch (tolower (varargin {1})) case 'kfold' numFolds = varargin{2}; if (! (isnumeric (numFolds) && isscalar (numFolds) && (numFolds == fix (numFolds)) && numFolds > 1)) error (strcat ("RegressionGAM.crossval: 'KFold'", ... " must be an integer value greater than 1.")); endif case 'holdout' Holdout = varargin{2}; if (! (isnumeric (Holdout) && isscalar (Holdout) && Holdout > 0 && Holdout < 1)) error (strcat ("RegressionGAM.crossval: 'Holdout'", ... " must be a numeric value between 0 and 1.")); endif case 'leaveout' Leaveout = varargin{2}; if (! (ischar (Leaveout) && (strcmpi (Leaveout, 'on') || strcmpi (Leaveout, 'off')))) error (strcat ("RegressionGAM.crossval: 'Leaveout'", ... " must be either 'on' or 'off'.")); endif case 'cvpartition' CVPartition = varargin{2}; if (! (isa (CVPartition, 'cvpartition'))) error (strcat ("RegressionGAM.crossval: 'CVPartition'", ... " must be a 'cvpartition' object.")); endif otherwise error (strcat ("RegressionGAM.crossval: invalid", ... " parameter name in optional paired arguments.")); endswitch varargin(1:2) = []; endwhile ## Determine the cross-validation method to use. The partition is ## built over the observations actually trained on, so its indices and ## the partitioned model's rows are the same set. n = this.NumObservations; if (! isempty (CVPartition)) partition = CVPartition; elseif (! isempty (Holdout)) partition = cvpartition (n, 'Holdout', Holdout); elseif (strcmpi (Leaveout, 'on')) partition = cvpartition (n, 'LeaveOut'); else partition = cvpartition (n, 'KFold', numFolds); endif ## Create a cross-validated model object CVMdl = RegressionPartitionedModel (this, partition); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionGAM} {@var{CMdl} =} compact (@var{obj}) ## ## Create a @qcode{CompactRegressionGAM} object. ## ## @code{@var{CMdl} = compact (@var{obj})} returns a compact version of ## the model, which predicts as it does but keeps no training data. ## ## @seealso{RegressionGAM, CompactRegressionGAM, fitrgam} ## @end deftypefn function CMdl = compact (this) CMdl = CompactRegressionGAM (this); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionGAM} {} savemodel (@var{obj}, @var{filename}) ## ## Save a RegressionGAM object. ## ## @code{savemodel (@var{obj}, @var{filename})} saves a RegressionGAM ## object into a file defined by @var{filename}. ## ## @seealso{loadmodel, fitrgam, RegressionGAM} ## @end deftypefn function savemodel (obj, fname) if (nargin < 2) error ("RegressionGAM.savemodel: too few input arguments."); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error ("RegressionGAM.savemodel: FNAME must be a character vector."); endif ## Generate variable for class name classdef_name = 'RegressionGAM'; ## Create variables from model properties X = obj.X; Y = obj.Y; NumObservations = obj.NumObservations; RowsUsed = obj.RowsUsed; BinEdges = obj.BinEdges; NumPredictors = obj.NumPredictors; PredictorNames = obj.PredictorNames; ResponseName = obj.ResponseName; Formula = obj.Formula; Interactions = obj.Interactions; Knots = obj.Knots; Order = obj.Order; DoF = obj.DoF; Tol = obj.Tol; BaseModel = obj.BaseModel; ModelwInt = obj.ModelwInt; IntMatrix = obj.IntMatrix; CategoricalPredictors = obj.CategoricalPredictors; ExpandedPredictorNames = obj.ExpandedPredictorNames; W = obj.W; ResponseTransform = obj.ResponseTransform; Intercept = obj.Intercept; IsStandardDeviationFit = obj.IsStandardDeviationFit; RTfun = obj.RTfun; FitMethod = obj.FitMethod; TreeModel = obj.TreeModel; PairDetectionBinEdges = obj.PairDetectionBinEdges; ModelParameters = obj.ModelParameters; ReasonForTermination = obj.ReasonForTermination; ## Save classdef name and all model properties as individual variables HyperparameterOptimizationResults = obj.HyperparameterOptimizationResults; save ('-binary', fname, 'classdef_name', 'X', 'Y', 'NumObservations', ... 'RowsUsed', 'BinEdges', 'NumPredictors', 'PredictorNames', ... 'ResponseName', ... 'Formula', 'Interactions', 'Knots', 'Order', 'DoF', 'Tol', ... 'BaseModel', 'ModelwInt', 'IntMatrix', 'CategoricalPredictors', ... 'ExpandedPredictorNames', 'W', 'ResponseTransform', ... 'Intercept', 'IsStandardDeviationFit', 'RTfun', ... 'FitMethod', 'TreeModel', 'PairDetectionBinEdges', ... 'ModelParameters', 'ReasonForTermination', ... 'HyperparameterOptimizationResults'); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionGAM} {@var{Mdl} =} resume (@var{obj}, @var{numTrees}) ## ## Resume training a generalized additive model. ## ## @code{@var{Mdl} = resume (@var{obj}, @var{numTrees})} adds ## @var{numTrees} more trees to @var{obj} and returns the result. The ## original model is not modified. ## ## Training continues in the phase that ran last, which is what MATLAB ## does: a model carrying interaction terms gains interaction trees and ## its predictor shape functions are left alone, while a model without ## them gains predictor trees. A round starts at its initial learning ## rate whatever its number, so the model this returns is the model a ## single fit of the combined budget would have produced. ## ## @var{numTrees} must be a positive integer scalar. Resuming raises ## where there is nothing left to gain, rather than returning the model ## unchanged, and it is not available under ## @qcode{'FitMethod', 'splines'}: a backfit that has converged to its ## tolerance has no budget to extend. ## ## @seealso{RegressionGAM, fitrgam, RegressionGAM.addInteractions} ## @end deftypefn function Mdl = resume (this, numTrees) if (nargin < 2) error ("RegressionGAM.resume: Not enough input arguments."); endif if (! strcmp (this.FitMethod, 'boostedtrees')) error (strcat ("RegressionGAM.resume: resuming is available", ... " only under 'FitMethod', 'boostedtrees'; a spline", ... " backfit stops at its tolerance and has no budget", ... " to extend.")); endif if (! (isnumeric (numTrees) && isscalar (numTrees) && isreal (numTrees) && numTrees > 0 && fix (numTrees) == numTrees)) error (strcat ("RegressionGAM.resume: NUMTREES must be a", ... " positive integer scalar.")); endif ## The rows the fit saw, which are all of them. cobs = true (rows (this.X), 1); [X, E, cat] = gamCatCode (this.TreeModel, this.X(cobs, :), ... this.BinEdges); Y = this.Y(cobs); Mdl = this; MP = this.ModelParameters; reason = this.ReasonForTermination; ntrees = this.NumTrainedTrees; if (isempty (this.TreeModel.Pairs)) ## No interaction phase ever ran, so the predictor phase is the one ## still open. The engine is handed the prediction reached so far and ## returns the increment to add to it. f = gamboostpredict (E, this.TreeModel.ShapeValues, X, ... this.Intercept); M = gamboosttrain (X, Y, 2, numTrees, ... MP.InitialLearnRateForPredictors, ... MP.MaxNumSplitsPerPredictor, 0, MP.NumPrint, ... f(:), double (this.W(cobs)), cat); if (M.NumTrees == 0) error (strcat ("RegressionGAM.resume: unable to resume", ... " training because the software was unable to", ... " improve the model fit.")); endif sv = this.TreeModel.ShapeValues; for j = 1:numel (sv) sv{j} = sv{j} + M.ShapeValues{j}; endfor Mdl.TreeModel.ShapeValues = sv; Mdl.Intercept = this.Intercept + M.Intercept; MP.NumTreesPerPredictor = MP.NumTreesPerPredictor + M.NumTrees; reason.PredictorTrees = M.ReasonForTermination; ntrees.PredictorTrees = ntrees.PredictorTrees + M.NumTrees; else ## The interaction phase ran last, so it is the one extended. The ## running prediction includes the surfaces already fitted, and the ## new ones are added to them. [PE, PM] = gamPairEdges (this.TreeModel, this.PairDetectionBinEdges); f = gamboostpredict (E, this.TreeModel.ShapeValues, X, ... this.Intercept, 0, PE, ... this.TreeModel.PairValues, ... this.TreeModel.Pairs, PM); I = gamboostinter (X, Y, f(:), 2, this.TreeModel.Pairs, numTrees, ... MP.InitialLearnRateForInteractions, ... MP.MaxNumSplitsPerInteraction, ... double (this.W(cobs)), cat); if (I.NumTrees == 0) error (strcat ("RegressionGAM.resume: unable to resume", ... " training because the software was unable to", ... " improve the model fit.")); endif ## The new trees cut where they cut, so the surfaces are added on the ## union of both grids. [pe, pv, pm] = gamPairMerge (PE, this.TreeModel.PairValues, PM, ... I.PairEdges, I.PairValues, ... I.PairMissing); Mdl.TreeModel.PairEdges = pe; Mdl.TreeModel.PairValues = pv; Mdl.TreeModel.PairMissing = pm; Mdl.TreeModel.PairIntercept = this.TreeModel.PairIntercept ... + I.Intercept; Mdl.Intercept = this.Intercept + I.Intercept; MP.NumTreesPerInteraction = MP.NumTreesPerInteraction + I.NumTrees; reason.InteractionTrees = I.ReasonForTermination; ntrees.InteractionTrees = ntrees.InteractionTrees + I.NumTrees; endif Mdl.ModelParameters = MP; Mdl.ReasonForTermination = reason; Mdl.NumTrainedTrees = ntrees; endfunction endmethods ## Helper functions methods(Access = private) ## Shared validation for the assessment methods, so each reports under ## its own name. function [X, Y] = checkXY_ (this, X, Y, caller) if (isempty (X)) error ("RegressionGAM.%s: X is empty.", caller); elseif (this.NumPredictors != columns (X)) error (strcat ("RegressionGAM.%s: X must have the same number of", ... " predictors as the trained model."), caller); endif if (isempty (Y)) error ("RegressionGAM.%s: Y is empty.", caller); elseif (rows (X) != rows (Y)) error (strcat ("RegressionGAM.%s: Y must have the same number of", ... " rows as X."), caller); endif endfunction ## Drive the boosted-tree engine: the predictor phase, then a search for ## interactions worth adding, then the interaction phase over whichever ## pairs survived. The two phases share a running fit, so the second ## continues from the prediction the first left rather than starting over. function this = fitBoosted (this, X, Y, Interactions, NTP, NTI, MSP, ... MSI, LRP, LRI, MaxPValue, Verb, NPrint) ## Method 2 boosts the squared error, which is what a regression fits, ## weighted by W over the rows the fit sees. Wfit = double (this.W); [X, ~, cat, Levels] = gamCatCode (this.TreeModel, X); M = gamboosttrain (X, Y, 2, NTP, LRP, MSP, Verb, NPrint, [], Wfit, ... cat); f = gamboostpredict (M.BinEdges, M.ShapeValues, X, M.Intercept); this.BinEdges = M.BinEdges(:); ## a column cell, as MATLAB reports it this.BinEdges(cat) = {[]}; this.Intercept = M.Intercept; reason = struct ('PredictorTrees', M.ReasonForTermination, ... 'InteractionTrees', ''); ntrees = struct ('PredictorTrees', M.NumTrees, ... 'InteractionTrees', 0); pairs = zeros (0, 2); pairValues = {}; pairEdges = {}; pairMissing = {}; pairShift = 0; wanted = -1; if (ischar (Interactions)) wanted = Inf; elseif (isscalar (Interactions) && ! isempty (Interactions)) wanted = Interactions; elseif (! isempty (Interactions)) pairs = interactionPairs (logical (Interactions)); endif if (wanted > 0 && columns (X) > 1) S = gamboostpairs (X, M.Residuals, cat); ## The F ratio becomes a probability through the package's own fcdf, ## which is verified against MATLAB; the engine deliberately does not ## carry a second incomplete beta of its own. pval = fcdf (S.F, S.DF1, S.DF2, 'upper'); pval(S.DF1 <= 0) = 1; [pval, ord] = sort (pval); ranked = S.Pairs(ord, :); ranked = ranked(pval <= MaxPValue, :); if (isfinite (wanted) && rows (ranked) > wanted) ranked = ranked(1:wanted, :); endif pairs = ranked; ## A pair tree allowed a single split can only cut along one ## predictor, which is a main effect and no interaction, so no pair is ## kept. R2024a selects none and warns as below. if (MSI < 2) pairs = zeros (0, 2); endif this.PairDetectionBinEdges = S.BinEdges(:); this.PairDetectionBinEdges(cat) = {[]}; if (isempty (pairs)) warning (strcat ("RegressionGAM: model does not include", ... " interaction terms because all interaction", ... " terms have p-values greater than the", ... " 'MaxPValue' value, or the software was unable", ... " to improve the model fit.")); endif endif if (! isempty (pairs)) I = gamboostinter (X, Y, f, 2, pairs, NTI, LRI, MSI, Wfit, cat); ## A pair tree splitting on one predictor alone is a main effect and ## adds nothing, so a fit made only of such trees keeps no pair, as ## R2024a keeps none. if (I.NumTrees == 0) pairs = zeros (0, 2); warning (strcat ("RegressionGAM: model does not include", ... " interaction terms because all interaction", ... " terms have p-values greater than the", ... " 'MaxPValue' value, or the software was unable", ... " to improve the model fit.")); endif endif if (! isempty (pairs)) this.Intercept = this.Intercept + I.Intercept; pairShift = I.Intercept; ## A requested pair list skips detection, so the grid it would have ## used comes from the interaction phase instead. if (wanted <= 0) this.PairDetectionBinEdges = I.PairBinEdges(:); this.PairDetectionBinEdges(cat) = {[]}; endif pairValues = I.PairValues; pairEdges = I.PairEdges; pairMissing = I.PairMissing; reason.InteractionTrees = I.ReasonForTermination; ntrees.InteractionTrees = I.NumTrees; endif this.Interactions = pairs; this.ReasonForTermination = reason; this.NumTrainedTrees = ntrees; ## The constant the interaction surfaces gave up when they were ## recentred is kept apart from the predictor phase's intercept. The ## Intercept property still reports their sum, as MATLAB's does, but ## predicting without the interactions has to take this part back out ## or it would answer with a constant the main effects never earned. this.TreeModel = struct ('ShapeValues', {M.ShapeValues}, ... 'PairValues', {pairValues}, ... 'PairEdges', {pairEdges}, ... 'PairMissing', {pairMissing}, ... 'Pairs', pairs, ... 'PairIntercept', pairShift, ... 'CategoricalLevels', {Levels}); if (ischar (Interactions)) request = Interactions; elseif (isempty (Interactions)) request = 0; else request = Interactions; endif this.ModelParameters = struct ( ... 'NumPrint', NPrint, ... 'MaxPValue', MaxPValue, ... 'InitialLearnRateForPredictors', LRP, ... 'InitialLearnRateForInteractions', LRI, ... 'NumTreesPerPredictor', NTP, ... 'NumTreesPerInteraction', NTI, ... 'MaxNumSplitsPerPredictor', MSP, ... 'MaxNumSplitsPerInteraction', MSI, ... 'VerbosityLevel', Verb, ... 'Interactions', request, ... 'Version', 1, ... 'Method', 'GAM', ... 'Type', 'regression'); endfunction ## Determine interactions from Interactions optional parameter ## Fit the model that carries the interaction terms. The constructor and ## addInteractions both arrive here with IntMatrix already decided and the ## predictors and response prepared as the fit wants them, so the two ## cannot drift: a model given its interactions after the fact is the ## model it would have been had they been asked for at the outset. function this = fitModelwInt (this, X, Y, Inter, Knots, Order, DoF) if (isempty (this.Formula)) ## Analyze Interactions optional parameter this.IntMatrix = this.parseInteractions (); ## Append interaction terms to the predictor matrix for i = 1:rows (this.IntMatrix) tindex = logical (this.IntMatrix(i,:)); Xterms = X(:,tindex); Xinter = ones (this.NumObservations, 1); for c = 1:sum (tindex) Xinter = Xinter .* Xterms(:,c); endfor ## Append interaction terms X = [X, Xinter]; endfor else ## Analyze Formula optional parameter this.IntMatrix = this.parseFormula (); ## Add selected predictors and interaction terms XN = []; for i = 1:rows (this.IntMatrix) tindex = logical (this.IntMatrix(i,:)); Xterms = X(:,tindex); Xinter = ones (this.NumObservations, 1); for c = 1:sum (tindex) Xinter = Xinter .* Xterms(:,c); endfor ## Append selected predictors and interaction terms XN = [XN, Xinter]; endfor X = XN; endif ## Update length of Knots, Order, and DoF vectors to match ## the columns of X with the interaction terms Knots = ones (1, columns (X)) * Knots(1); # Knots Order = ones (1, columns (X)) * Order(1); # Order of spline DoF = ones (1, columns (X)) * DoF(1); # Degrees of freedom ## Fit the model with interactions [iter, param, res, RSS] = this.fitGAM (X, Y, Inter, Knots, Order); this.ModelwInt.Intercept = Inter; this.ModelwInt.Parameters = param; this.ModelwInt.Iterations = iter; this.ModelwInt.Residuals = res; this.ModelwInt.RSS = RSS; endfunction function intMat = parseInteractions (this) if (islogical (this.Interactions)) ## Check that interaction matrix corresponds to predictors if (numel (this.PredictorNames) != columns (this.Interactions)) error (strcat ("RegressionGAM: columns in 'Interactions'", ... " matrix must equal to the number of predictors.")); endif intMat = this.Interactions; elseif (isnumeric (this.Interactions)) ## Need to measure the effect of all interactions to keep the best ## performing. Just check that the given number is not higher than ## p*(p-1)/2, where p is the number of predictors. p = this.NumPredictors; if (this.Interactions > p * (p - 1) / 2) error (strcat ("RegressionGAM: number of interaction terms", ... " requested is larger than all possible", ... " combinations of predictors in X.")); endif ## The pairs are not ranked by how much each contributes, so the ## first ones asked for are taken in the order nchoosek lists them. intMat = pairTerms (p)(1:this.Interactions, :); elseif (strcmpi (this.Interactions, 'all')) ## Calculate all p*(p-1)/2 interaction terms intMat = pairTerms (this.NumPredictors); endif endfunction ## Determine interactions from formula function intMat = parseFormula (this) intMat = []; ## Check formula for syntax if (isempty (strfind (this.Formula, '~'))) error ("RegressionGAM: invalid syntax in Formula."); endif ## Split formula and keep predictor terms formulaParts = strsplit (this.Formula, '~'); ## Check there is some string after '~' if (numel (formulaParts) < 2) error ("RegressionGAM: no predictor terms in Formula."); endif predictorString = strtrim (formulaParts{2}); if (isempty (predictorString)) error ("RegressionGAM: no predictor terms in Formula."); endif ## Split additive terms (between + sign) aterms = strtrim (strsplit (predictorString, '+')); ## Process all terms for i = 1:numel (aterms) ## Find individual terms (string missing ':') if (isempty (strfind (aterms(i), ':'){:})) ## Search PredictorNames to associate with column in X sterms = strcmp (this.PredictorNames, aterms(i)); ## Append to interactions matrix intMat = [intMat; sterms]; else ## Split interaction terms (string contains ':') mterms = strsplit (aterms{i}, ':'); ## Add each individual predictor to interaction term vector iterms = logical (zeros (1, this.NumPredictors)); for t = 1:numel (mterms) iterms = iterms | strcmp (this.PredictorNames, mterms(t)); endfor ## Check that all predictors have been identified if (sum (iterms) != t) error ("RegressionGAM: some predictors have not been identified."); endif ## Append to interactions matrix intMat = [intMat; iterms]; endif endfor ## Check that all terms have been identified if (! all (sum (intMat, 2) > 0)) error ("RegressionGAM: some terms have not been identified."); endif endfunction ## Fit the model function [iter, param, res, RSS] = fitGAM (this, X, Y, Inter, Knots, Order) ## The fit is performed by the shared spline engine, which builds and ## factorises each predictor's design once and reduces a backfitting ## cycle to two products against the factors per term. Mdl = gamtrain (X, Y, Knots, Order, 2, Inter, this.Tol, 1000); iter = Mdl.Iterations; param = Mdl.Parameters; res = Mdl.Residuals; RSS = Mdl.RSS; endfunction endmethods methods(Static, Hidden) function mdl = load_model (filename, data) ## Create a RegressionGAM object mdl = RegressionGAM (1, 1); ## Get fieldnames from DATA (including private properties) names = fieldnames (data); ## The set methods for these read other properties, and one of them ## rebuilds Coeffs, so they are assigned once everything else is in ## place rather than in the order the file happens to list them. late = ismember (names, {'Cost', 'Prior', 'ScoreTransform', ... 'ResponseTransform'}); names = [names(! late); names(late)]; ## Copy data into object for i = 1:numel (names) ## Check that fieldnames in DATA match properties in RegressionGAM try mdl.(names{i}) = data.(names{i}); catch error ("RegressionGAM.load_model: invalid model in '%s'.", ... filename) end_try_catch endfor ## A model written before RowsUsed became a mask stored it as a ## double, which is a valid subscript for nothing. An empty RowsUsed ## means every observation was used and stays an empty double. if (! isempty (mdl.RowsUsed)) mdl.RowsUsed = logical (mdl.RowsUsed); endif endfunction endmethods endclassdef ## Reshape a predictor matrix into the terms a model was fitted on. With ## APPEND true the interaction columns are added to the predictors, which is ## what an 'Interactions' model was fitted on; with it false they replace them, ## which is what a 'Formula' model was fitted on. Every column of the result ## carries one term of the model, which is what predict_val walks. function XA = gamTerms (X, IntMatrix, append) if (append) XA = X; else XA = []; endif for i = 1:rows (IntMatrix) tindex = logical (IntMatrix(i,:)); Xterms = X(:,tindex); Xinter = ones (rows (X), 1); for c = 1:sum (tindex) Xinter = Xinter .* Xterms(:,c); endfor XA = [XA, Xinter]; endfor endfunction ## Helper function for making prediction of new data based on GAM model function ypred = predict_val (params, X, intercept) ## The shared prediction engine evaluates every additive term and adds the ## intercept. ypred = gampredict (params, X, intercept); endfunction %!demo %! ## Train a RegressionGAM Model for synthetic values %! rng (42); %! f1 = @(x) cos (3 * x); %! f2 = @(x) x .^ 3; %! x1 = 2 * rand (50, 1) - 1; %! x2 = 2 * rand (50, 1) - 1; %! y = f1(x1) + f2(x2); %! y = y + y .* 0.2 .* rand (50,1); %! X = [x1, x2]; %! a = fitrgam (X, y) %!demo %! ## Declare two different functions %! rng (42); %! f1 = @(x) cos (3 * x); %! f2 = @(x) x .^ 3; %! %! ## Generate 80 samples for f1 and f2 %! x = [-4*pi:0.1*pi:4*pi-0.1*pi]'; %! X1 = f1(x); %! X2 = f2(x); %! %! ## Create a synthetic response by adding noise %! Ytrue = X1 + X2; %! Y = Ytrue + Ytrue .* 0.2 .* rand (80,1); %! %! ## Assemble predictor data %! X = [X1, X2]; %! %! ## Train the GAM and test on the same data %! ## A standard deviation and a prediction interval come from the spline %! ## engine, which fits one; the boosted-tree engine reports none. %! a = fitrgam (X, Y, 'FitMethod', 'splines', 'order', [5, 5]); %! [ypred, ySDsd, yInt] = predict (a, X); %! %! ## Plot the results %! figure %! [sortedY, indY] = sort (Ytrue); %! plot (sortedY, 'r-'); %! xlim ([0, 80]); %! hold on %! plot (ypred(indY), 'g+') %! plot (yInt(indY,1), 'k:') %! plot (yInt(indY,2), 'k:') %! xlabel ('Predictor samples'); %! ylabel ('Response'); %! title ('actual vs predicted values for function f1(x) = cos (3x) '); %! legend ({'Theoretical Response', 'Predicted Response', 'Prediction Intervals'}); %! %! ## Use 30% Holdout partitioning for training and testing data %! C = cvpartition (80, 'HoldOut', 0.3); %! [ypred, ySDsd, yInt] = predict (a, X(test (C),:)); %! %! ## Plot the results %! figure %! [sortedY, indY] = sort (Ytrue(test (C))); %! plot (sortedY, 'r-'); %! xlim ([0, sum(test(C))]); %! hold on %! plot (ypred(indY), 'g+') %! plot (yInt(indY,1),'k:') %! plot (yInt(indY,2),'k:') %! xlabel ('Predictor samples'); %! ylabel ('Response'); %! title ('actual vs predicted values for function f1(x) = cos (3x) '); %! legend ({'Theoretical Response', 'Predicted Response', 'Prediction Intervals'}); ## Test constructor %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = [1; 2; 3; 4]; %! a = RegressionGAM (x, y, 'FitMethod', 'splines'); %! assert_equal ({a.X, a.Y}, {x, y}) %! assert_equal ({a.BaseModel.Intercept}, {2.5000}) %! assert_equal ({a.Knots, a.Order, a.DoF}, {[5, 5, 5], [3, 3, 3], [8, 8, 8]}) %! assert_equal ({a.NumObservations, a.NumPredictors}, {4, 3}) %! assert_equal ({a.ResponseName, a.PredictorNames}, {'Y', {'x1', 'x2', 'x3'}}) %! assert_equal ({a.Formula}, {[]}) %!test %! x = [1, 2, 3, 4; 4, 5, 6, 7; 7, 8, 9, 1; 3, 2, 1, 2]; %! y = [1; 2; 3; 4]; %! pnames = {'A', 'B', 'C', 'D'}; %! formula = 'Y ~ A + B + C + D + A:C'; %! intMat = logical ([1,0,0,0;0,1,0,0;0,0,1,0;0,0,0,1;1,0,1,0]); %! a = RegressionGAM (x, y, 'FitMethod', 'splines', ... %! 'predictors', pnames, 'formula', formula); %! assert_equal (a.IntMatrix, double (intMat)) %! assert_equal ({a.ResponseName, a.PredictorNames}, {'Y', pnames}) %! assert_equal (a.Formula, formula) %!test %! ## Test that predict() executes correctly when interactions are present %! X = [1, 2; 3, 4; 5, 6; 7, 8]; %! Y = [10; 20; 30; 40]; %! mdl = RegressionGAM (X, Y, 'FitMethod', 'splines', ... %! 'formula', 'Y ~ x1 + x2 + x1:x2'); %! ypred = predict (mdl, X); %! assert_equal (isnumeric (ypred), true); %! assert_equal (size (ypred), [4, 1]); %! [ypred2, ySD, yInt] = predict (mdl, X, 'includeinteractions', true); %! assert_equal (size (ypred2), [4, 1]); %! assert_equal (size (ySD), [4, 1]); %! assert_equal (size (yInt), [4, 2]); %! ## The three terms fit these four points exactly, so the residuals are zero %! ## and so is ySD. This block asserted the three sizes and nothing else, %! ## which is why it stayed green while ySD was computed from two of the %! ## three terms: a size is true whatever the number is. %! assert_equal (ypred2, [10; 20; 30; 40], 1e-10); %! assert_equal (ySD, zeros (4, 1), 1e-10); %!test %! ## Verify ySD is based on training residual variance %! X = (1:10)'; %! Y = [2; 1; 4; 3; 6; 5; 8; 7; 10; 9]; %! mdl = RegressionGAM (X, Y, 'FitMethod', 'splines'); %! y_train = predict (mdl, X); %! rs = Y - y_train; %! expected_ySD = sqrt (var (rs)); %! [~, ySD] = predict (mdl, X(1:4,:)); %! assert_equal (ySD, expected_ySD * ones (4, 1), 1e-10); %!test %! ## Verify ySD remains the same for one or more prediction points %! X = (1:10)'; %! Y = [2; 1; 4; 3; 6; 5; 8; 7; 10; 9]; %! mdl = RegressionGAM (X, Y, 'FitMethod', 'splines'); %! y_train = predict (mdl, X); %! expected_ySD = sqrt (var (Y - y_train)); %! [~, ySD_1] = predict (mdl, X(1,:)); %! [~, ySD_3] = predict (mdl, X(1:3,:)); %! assert_equal (ySD_1, expected_ySD, 1e-10); %! assert_equal (ySD_3, expected_ySD * ones (3, 1), 1e-10); ## Test input validation for constructor ## Interactions reports the two-way terms the fitted model carries, as ## predictor index pairs, matching what R2024a's fitrgam returns. %!test %! k = (1:60)'; %! X = [mod(k*7,11)-5, mod(k*3,11)-5, mod(k*5,11)-5]; %! y = X(:,1) .* X(:,2) + 0.5 * X(:,3); %! Mdl = fitrgam (X, y, "Interactions", "all"); %! assert_equal (Mdl.Interactions, [1, 2; 1, 3; 2, 3]); ## No interactions is an empty list of pairs, keeping its two columns. %!test %! k = (1:60)'; %! X = [mod(k*7,11)-5, mod(k*3,11)-5, mod(k*5,11)-5]; %! Mdl = fitrgam (X, X(:,1) + X(:,3)); %! assert_equal (size (Mdl.Interactions), [0, 2]); ## A formula's main effects are terms of the model but not interactions. %!test %! k = (1:60)'; %! X = [mod(k*7,11)-5, mod(k*3,11)-5, mod(k*5,11)-5]; %! y = X(:,1) .* X(:,2) + 0.5 * X(:,3); %! Mdl = fitrgam (X, y, "FitMethod", "splines", ... %! "Formula", "Y ~ x1 + x2 + x1:x2"); %! assert_equal (Mdl.Interactions, [1, 2]); %! assert_equal (compact (Mdl).Interactions, [1, 2]); ## addInteractions fits the interaction terms onto a model that already has ## its univariate ones. The result is the model that would have been fitted ## had the terms been asked for at the outset: both go through one private ## method, so the two cannot drift apart. %!test %! load fisheriris %! bai = ! strcmp (species, "setosa"); %! Xai = meas(bai,2:4); Yai = meas(bai,1); %! Aai = addInteractions (fitrgam (Xai, Yai), "all"); %! Bai = fitrgam (Xai, Yai, "Interactions", "all"); %! assert_equal (Aai.Interactions, Bai.Interactions); %! assert_equal (Aai.ModelwInt, Bai.ModelwInt); %! assert_equal (predict (Aai, Xai), predict (Bai, Xai)); ## The univariate fit is left alone, which is what MATLAB leaves alone too. %!test %! load fisheriris %! bai = ! strcmp (species, "setosa"); %! Xai = meas(bai,2:4); Yai = meas(bai,1); %! Cai = fitrgam (Xai, Yai); %! Aai = addInteractions (Cai, "all"); %! assert_equal (predict (Aai, Xai, "IncludeInteractions", false), ... %! predict (Cai, Xai)); ## A count and a logical matrix name terms as the constructor's option does. %!test %! load fisheriris %! bai = ! strcmp (species, "setosa"); %! Xai = meas(bai,2:4); Yai = meas(bai,1); %! Aai = addInteractions (fitrgam (Xai, Yai), 2); %! assert_equal (size (Aai.Interactions), [2, 2]); %! assert_equal (sort (Aai.Interactions, 2), Aai.Interactions); %! Lai = addInteractions (fitrgam (Xai, Yai), logical ([1 1 0; 0 1 1])); %! assert_equal (Lai.Interactions, [1, 2; 2, 3]); ## A model that already carries interaction terms is not extended, and a ## model fitted from a formula names every term it has, interactions among ## them, so it is refused for the same reason. R2024a refuses both. ## resume continues the phase that ran last, and a model with no interactions ## has only the predictor phase open. Resuming reproduces the model a single ## fit of the combined budget would have produced, which is the oracle every ## test here uses. %!test %! load fisheriris %! X = meas; %! Y = meas(:,1) + 0.3 * meas(:,3); %! A = fitrgam (X, Y, 'NumTreesPerPredictor', 5); %! B = resume (A, 10); %! C = fitrgam (X, Y, 'NumTreesPerPredictor', 15); %! assert_equal (B.ModelParameters.NumTreesPerPredictor, 15); %! assert_equal (predict (B, X), predict (C, X), 1e-10); %!test %! ## A model carrying interactions gains interaction trees, and its predictor %! ## shape functions are left where they were. %! load fisheriris %! X = meas; %! Y = meas(:,1) + 0.3 * meas(:,3); %! A = fitrgam (X, Y, 'NumTreesPerPredictor', 5, 'Interactions', 3, ... %! 'NumTreesPerInteraction', 4); %! B = resume (A, 10); %! assert_equal (B.ModelParameters.NumTreesPerPredictor, 5); %! assert_equal (B.ModelParameters.NumTreesPerInteraction, 14); %! assert_equal (B.TreeModel.ShapeValues, A.TreeModel.ShapeValues); %! C = fitrgam (X, Y, 'NumTreesPerPredictor', 5, 'Interactions', 3, ... %! 'NumTreesPerInteraction', 14); %! assert_equal (predict (B, X), predict (C, X), 1e-10); %!test %! ## The selected pairs survive, and resuming twice accumulates. %! load fisheriris %! X = meas; %! Y = meas(:,1) + 0.3 * meas(:,3); %! A = fitrgam (X, Y, 'NumTreesPerPredictor', 5, 'Interactions', 3, ... %! 'NumTreesPerInteraction', 4); %! B = resume (resume (A, 10), 6); %! assert_equal (B.Interactions, A.Interactions); %! assert_equal (B.ModelParameters.NumTreesPerInteraction, 20); %! assert_equal (B.ModelParameters.NumTreesPerPredictor, 5); %!test %! ## The model handed in is not modified. %! load fisheriris %! X = meas; %! Y = meas(:,1) + 0.3 * meas(:,3); %! A = fitrgam (X, Y, 'NumTreesPerPredictor', 5); %! B = resume (A, 10); %! assert_equal (A.ModelParameters.NumTreesPerPredictor, 5); %! assert_equal (B.ModelParameters.NumTreesPerPredictor, 15); %!test # a row missing a predictor is kept, and its spline term has no value %! x = linspace (0, 1, 30)'; %! X = [x, cos(4 * x)]; %! y = sin (3 * x) + X(:,2); %! Mdl = RegressionGAM (X, y, 'FitMethod', 'splines'); %! yFit = predict (Mdl, [0.5, 0.2; NaN, 0.2; 0.5, 0.2]); %! assert_equal (size (yFit), [3, 1]); %! assert_equal (isnan (yFit)', [false, true, false]); %!test # MATLAB parity: a missing value adds nothing from its term %! k = (1:200)'; %! X = [sin(k), cos(2 * k), mod(k, 5)]; %! y = 2 * sin (k) + X(:,2) .^ 2 + 0.3 * X(:,3); %! Q = [0.5, 0.2, 1; NaN, 0.2, 1; 0.5, NaN, 1; NaN, NaN, NaN; 0.1, 0.2, 1; ... %! NaN, 0.7, 3; NaN, NaN, 1]; %! Mdl = RegressionGAM (X, y); %! yFit = predict (Mdl, Q); %! assert_equal (yFit, [1.4399946345; 0.675842807654; 1.60393628463; ... %! 1.09800542249; 1.33125338416; 1.62383199199; ... %! 0.839784457781], 1e-10); %! assert_equal (yFit(4), Mdl.Intercept); %!test # MATLAB parity: categorical predictors are split by sets of levels %! k = (0:119)'; %! c1 = mod (k, 3) + 1; %! x2 = sin (k); %! c3 = 10 * (mod (floor (k / 2), 2) + 1); %! X = [c1, x2, c3]; %! y = 5 * (c1 == 2) + 0.5 * x2 - 3 * (c3 == 20) + 0.1 * cos (k); %! Q = [1, 0, 10; 2, 0, 10; 3, 0, 10; 1, 0, 20; 1, 0.5, 10; 4, 0, 10; ... %! NaN, 0, 10; 2.5, 0, 20]; %! Mdl = RegressionGAM (X, y, 'CategoricalPredictors', [1, 3], ... %! 'NumTreesPerPredictor', 5, ... %! 'MaxNumSplitsPerPredictor', 2); %! assert_equal (Mdl.CategoricalPredictors, [1, 3]); %! assert_equal (predict (Mdl, Q), [-0.1559786927; 4.811738827; ... %! -0.1849962119; -3.091005709; ... %! 0.2642714677; 1.490254641; ... %! 1.490254641; -1.444772376], 1e-9); %!test # MATLAB parity: a categorical predictor reports no bin edges %! k = (0:119)'; %! c1 = mod (k, 3) + 1; %! x2 = sin (k); %! c3 = 10 * (mod (floor (k / 2), 2) + 1); %! X = [c1, x2, c3]; %! y = 5 * (c1 == 2) + 0.5 * x2 - 3 * (c3 == 20) + 0.1 * cos (k); %! Q = [1, 0, 10; 2, 0, 10; 3, 0, 10; 1, 0, 20; 1, 0.5, 10; 4, 0, 10; ... %! NaN, 0, 10; 2.5, 0, 20]; %! Mdl = RegressionGAM (X, y, 'CategoricalPredictors', [1, 3], ... %! 'NumTreesPerPredictor', 1); %! assert_equal (Mdl.Intercept, 0.1666859425, 1e-10); %! assert_equal (predict (Mdl, Q(6:8,:)), [1.312029301; 1.312029301; ... %! -1.640617244], 1e-9); %! assert_equal (Mdl.BinEdges{1}, []); %! assert_equal (Mdl.BinEdges{3}, []); %! assert_equal (numel (Mdl.BinEdges{2}), 119); %!test # MATLAB parity: pair trees split categorical predictors by level sets %! k = (0:149)'; %! c1 = 1 + mod (floor (k * 7 / 11), 4); %! x2 = sin (k); %! c3 = 10 * (1 + (mod (k, 7) > 3)) + 10 * (mod (k, 13) == 0); %! X = [c1, x2, c3]; %! y = 2 * (c1 == 2) - 1.5 * (c1 == 4) + 0.5 * x2 - 3 * (c3 == 20) ... %! + (c3 == 30) + (c1 == 3) .* (c3 == 20) + 0.1 * cos (k) ... %! + 0.05 * sin (3 * k); %! [A, B] = ndgrid ([1, 2, 3, 4], [10, 20, 30]); %! Q = [A(:), zeros(12, 1), B(:)]; %! Mdl = RegressionGAM (X, y, 'CategoricalPredictors', [1, 3], ... %! 'Interactions', logical ([1, 0, 1]), ... %! 'NumTreesPerInteraction', 5); %! d = predict (Mdl, Q) - predict (Mdl, Q, 'IncludeInteractions', false); %! assert_equal (d, [0.03701741101; 0.06202240762; -0.1556975657; ... %! 0.08736164669; -0.07100596369; -0.08113441688; ... %! 0.3496023158; -0.06710888279; 0.06027597832; ... %! 0.05025393807; -0.06227465812; 0.01256864956], 1e-9); %!test # MATLAB parity: a pair tree using one predictor alone adds nothing %! k = (0:149)'; %! c1 = 1 + mod (floor (k * 7 / 11), 4); %! x2 = sin (k); %! c3 = 10 * (1 + (mod (k, 7) > 3)) + 10 * (mod (k, 13) == 0); %! X = [c1, x2, c3]; %! y = 2 * (c1 == 2) - 1.5 * (c1 == 4) + 0.5 * x2 - 3 * (c3 == 20) ... %! + (c3 == 30) + (c1 == 3) .* (c3 == 20) + 0.1 * cos (k) ... %! + 0.05 * sin (3 * k); %! g = linspace (-1, 1, 5)'; %! [A, B, C] = ndgrid ([1, 2, 3, 4, NaN], g, [10, 20, 30, NaN]); %! Q = [A(:), B(:), C(:)]; %! Q = Q([1, 7, 13, 19, 28, 34, 45, 52, 59, 67],:); %! Mdl = RegressionGAM (X, y, 'CategoricalPredictors', [1, 3], ... %! 'Interactions', 'all', 'NumTreesPerInteraction', 5); %! assert_equal (Mdl.Interactions, [1, 3; 2, 3; 1, 2]); %! d = predict (Mdl, Q) - predict (Mdl, Q, 'IncludeInteractions', false); %! ## Five rounds, not twenty: a fit long enough to compound its rounding %! ## parts company with a toolchain that contracts multiply-add pairs, and %! ## then no tolerance holds, one element changing sign. Measured on R2024a, %! ## 2026-09-16, and agreeing to thirteen digits or better. %! assert_equal (d, [0.14496260366867; 0.00289829354386018; ... %! -0.125368787144964; 0.0691339453924824; ... %! 0.5053322741541; -0.0581889291447562; ... %! 0.00447142646759335; 0.15228433993982; ... %! 0.0385020050553703; 0.0838177432700107], 1e-9); %!test # MATLAB parity: a pair whose trees split one predictor alone is dropped %! ## One split per tree uses one predictor whatever the rounding. %! k = (0:119)'; %! c1 = mod (k, 3) + 1; %! x2 = sin (k); %! c3 = 10 * (mod (floor (k / 2), 2) + 1); %! X = [c1, x2, c3]; %! y = 5 * (c1 == 2) + 0.5 * x2 - 3 * (c3 == 20) + 0.1 * cos (k); %! S = warning ('off', 'all'); %! unwind_protect %! Mdl = RegressionGAM (X, y, 'CategoricalPredictors', [1, 3], ... %! 'Interactions', logical ([0, 1, 1]), ... %! 'NumTreesPerInteraction', 5, ... %! 'MaxNumSplitsPerInteraction', 1); %! unwind_protect_cleanup %! warning (S); %! end_unwind_protect %! assert_equal (size (Mdl.Interactions), [0, 2]); %!warning ... %! k = (0:119)'; %! RegressionGAM ([mod(k, 3) + 1, sin(k), 10 * (mod (floor (k / 2), 2) + 1)], ... %! 5 * (mod (k, 3) == 1) + 0.5 * sin (k) ... %! - 3 * (mod (floor (k / 2), 2) == 1) + 0.1 * cos (k), ... %! 'CategoricalPredictors', [1, 3], ... %! 'Interactions', logical ([0, 1, 1]), ... %! 'NumTreesPerInteraction', 5, ... %! 'MaxNumSplitsPerInteraction', 1); %!test # MATLAB parity: 'CategoricalPredictors', 'all' %! k = (0:59)'; %! X = [mod(k, 3) + 1, mod(k, 4)]; %! y = X(:,1) .^ 2 - X(:,2) + 0.1 * cos (k); %! Mdl = RegressionGAM (X, y, 'CategoricalPredictors', 'all', ... %! 'Interactions', 'all', 'NumTreesPerInteraction', 2); %! assert_equal (Mdl.CategoricalPredictors, [1, 2]); %! assert_equal (Mdl.BinEdges, {[]; []}); %! assert_equal (Mdl.PairDetectionBinEdges, {[]; []}); %!test # MATLAB parity: observation weights enter the fit %! k = (1:200)'; %! X = [sin(k), cos(2 * k), mod(k, 5)]; %! y = 2 * sin (k) + X(:,2) .^ 2 + 0.3 * X(:,3); %! w = 1 + mod (k, 3); %! Q = [0.5, 0.2, 1; 0.1, 0.7, 3; -0.4, -0.2, 0]; %! Mdl = RegressionGAM (X, y, 'Weights', w); %! assert_equal (Mdl.W, w / sum (w), 1e-15); %! assert_equal (Mdl.Intercept, 1.09607817728, 1e-10); %! assert_equal (predict (Mdl, Q), [1.32799066689; 2.22459947488; ... %! -0.705872562832], 1e-10); %! M5 = RegressionGAM (X, y, 'Weights', 5 * w); %! assert_equal (predict (M5, Q), predict (Mdl, Q), 1e-10); %!test # MATLAB parity: a row missing a predictor is fitted, not dropped %! k = (1:200)'; %! X = [sin(k), cos(2 * k), mod(k, 5)]; %! y = 2 * sin (k) + X(:,2) .^ 2 + 0.3 * X(:,3); %! X(1:5,1) = NaN; %! Q = [0.5, 0.2, 1; NaN, 0.2, 1; 0.5, NaN, 1; NaN, NaN, NaN; 0.1, 0.2, 1; ... %! NaN, 0.7, 3; NaN, NaN, 1]; %! Mdl = RegressionGAM (X, y); %! assert_equal (Mdl.NumObservations, 200); %! assert_equal (Mdl.Intercept, 1.28924934314, 1e-10); %! assert_equal (predict (Mdl, Q), [1.39295651846; 0.938063487647; ... %! 1.51847439653; 1.28924934314; ... %! 1.06837329164; 1.96571223429; ... %! 1.06358136571], 1e-10); %!test # MATLAB parity: a pair tree of one split selects no interaction %! k = (1:200)'; %! X = [sin(k), cos(2 * k), mod(k, 5)]; %! y = 2 * sin (k) + X(:,2) .^ 2 + 0.3 * X(:,3); %! S = warning ('off', 'all'); %! unwind_protect %! Mdl = RegressionGAM (X, y, 'Interactions', 1, ... %! 'MaxNumSplitsPerInteraction', 1); %! unwind_protect_cleanup %! warning (S); %! end_unwind_protect %! assert_equal (Mdl.Interactions, zeros (0, 2)); %! assert_equal (Mdl.Intercept, 1.09800542249, 1e-10); %!test # MATLAB parity: pair trees are fitted to the rows, on their own grid %! k = (1:200)'; %! X = [sin(k), cos(2 * k), mod(k, 5)]; %! y = 2 * sin (k) + X(:,2) .^ 2 + 0.3 * X(:,3); %! P = [0, -0.99, 0; 0, -0.42, 2; 0, 0.33, 1; 0, 0.78, 4]; %! Mdl = RegressionGAM (X, y, 'Interactions', 1, ... %! 'NumTreesPerInteraction', 1, ... %! 'MaxNumSplitsPerInteraction', 4); %! assert_equal (Mdl.Interactions, [2, 3]); %! pc = predict (Mdl, P) - predict (Mdl, P, 'IncludeInteractions', false); %! assert_equal (pc, [-0.014545869; 0.0020626874; 0.013767507; ... %! -0.0043992362], 1e-8); %!test # resuming the interaction phase adds trees a longer run would add %! k = (1:200)'; %! X = [sin(k), cos(2 * k), mod(k, 5)]; %! y = 2 * sin (k) + X(:,2) .^ 2 + 0.3 * X(:,3); %! P = [0, -0.99, 0; 0, -0.42, 2; 0, 0.33, 1; 0, 0.78, 4]; %! M1 = RegressionGAM (X, y, 'Interactions', 1, ... %! 'NumTreesPerInteraction', 1, ... %! 'MaxNumSplitsPerInteraction', 4); %! M2 = RegressionGAM (X, y, 'Interactions', 1, ... %! 'NumTreesPerInteraction', 2, ... %! 'MaxNumSplitsPerInteraction', 4); %! assert_equal (predict (resume (M1, 1), P), predict (M2, P), 1e-12); %!test # MATLAB parity: a missing value stops at the node that splits on it %! k = (1:200)'; %! X = [sin(k), cos(2 * k), mod(k, 5)]; %! y = 2 * sin (k) + X(:,2) .^ 2 + 0.3 * X(:,3); %! X(1:5,1) = NaN; %! X(6:10,3) = NaN; %! P = [-0.99, 0, 0; -0.33, 0, 2; 0.33, 0, 4; 0.99, 0, 1; ... %! NaN, 0, 0; -0.9, 0, NaN; 0.9, 0, NaN; NaN, 0, NaN]; %! Mdl = RegressionGAM (X, y, 'Interactions', logical ([1, 0, 1]), ... %! 'NumTreesPerInteraction', 1, ... %! 'MaxNumSplitsPerInteraction', 4); %! assert_equal (Mdl.Interactions, [1, 3]); %! assert_equal (Mdl.Intercept, 1.27402212251, 1e-10); %! pc = predict (Mdl, P) - predict (Mdl, P, 'IncludeInteractions', false); %! assert_equal (pc, [0.004044014047; -0.01436832448; 0.01780996877; ... %! -0.01436832448; -0.0001224643025; -0.002123518423; ... %! -0.002123518423; -0.0001224643025], 1e-9); %!test # resuming keeps what a row missing a pair predictor takes %! k = (1:200)'; %! X = [sin(k), cos(2 * k), mod(k, 5)]; %! y = 2 * sin (k) + X(:,2) .^ 2 + 0.3 * X(:,3); %! X(1:5,1) = NaN; %! X(6:10,3) = NaN; %! P = [-0.99, 0, 0; 0.33, 0, 4; NaN, 0, 0; -0.9, 0, NaN; NaN, 0, NaN]; %! M1 = RegressionGAM (X, y, 'Interactions', logical ([1, 0, 1]), ... %! 'NumTreesPerInteraction', 1, ... %! 'MaxNumSplitsPerInteraction', 4); %! M2 = RegressionGAM (X, y, 'Interactions', logical ([1, 0, 1]), ... %! 'NumTreesPerInteraction', 2, ... %! 'MaxNumSplitsPerInteraction', 4); %! assert_equal (predict (resume (M1, 1), P), predict (M2, P), 1e-12); %!test # MATLAB parity: the detection grid, with values missing %! k = (1:300)'; %! X = [sin(k), cos(2 * k), mod(k, 5), sin(0.7 * k), cos(0.3 * k)]; %! y = X(:,1) + X(:,2) .^ 2 + 0.3 * X(:,3) + 0.8 * X(:,1) .* X(:,4) ... %! + 0.5 * X(:,2) .* X(:,3) + 0.3 * X(:,4) .* X(:,5) + 0.05 * sin (13 * k); %! X(1:15,1) = NaN; %! X(16:30,3) = NaN; %! X(31:45,4) = NaN; %! Mdl = RegressionGAM (X, y, 'Interactions', 'all', ... %! 'NumTreesPerInteraction', 1); %! assert_equal (Mdl.PairDetectionBinEdges{4}(:)', ... %! [-0.92412955668638763, -0.67185165069256547, ... %! -0.37673775931105463, 0.064103966449078689, ... %! 0.43097453606213998, 0.73708146543750352, ... %! 0.93347437171529057], 1e-15); %!error ... %! RegressionGAM (ones (10, 2), (1:10)', 'CategoricalPredictors', 3) %!error ... %! RegressionGAM (ones (10, 2), (1:10)', 'CategoricalPredictors', true) %!error ... %! RegressionGAM (ones (10, 2), (1:10)', 'CategoricalPredictors', 0.5) %!error ... %! RegressionGAM (ones (10, 2), (1:10)', 'FitMethod', 'splines', ... %! 'CategoricalPredictors', 1) %!error ... %! RegressionGAM (ones (10, 2), (1:10)', 'Weights', 'a') %!error ... %! RegressionGAM (ones (10, 2), (1:10)', 'Weights', ones (2, 2)) %!error ... %! RegressionGAM (ones (10, 2), (1:10)', 'Weights', [1, 2]) %!error ... %! RegressionGAM (ones (10, 2), (1:10)', 'Weights', -ones (10, 1)) %!error ... %! RegressionGAM ([(1:10)', mod((1:10)', 3)], (1:10)', ... %! 'Weights', ones (10, 1), 'FitMethod', 'splines') %!error ... %! load fisheriris; ... %! resume (fitrgam (meas, meas(:,1), 'NumTreesPerPredictor', 5)) %!error ... %! load fisheriris; ... %! resume (fitrgam (meas, meas(:,1), 'FitMethod', 'splines'), 5) %!error ... %! load fisheriris; ... %! resume (fitrgam (meas, meas(:,1), 'NumTreesPerPredictor', 5), 0) %!error ... %! load fisheriris; ... %! resume (fitrgam (meas, meas(:,1), 'NumTreesPerPredictor', 5), 2.5) %!error ... %! load fisheriris; ... %! resume (fitrgam (meas, meas(:,1), 'NumTreesPerPredictor', 5), [1, 2]) %!error ... %! X = [ones(20,1)*[1, 2]; ones(20,1)*[3, 4]]; ... %! resume (fitrgam (X, ones (40, 1), 'NumTreesPerPredictor', 5), 5) %!error ... %! load fisheriris %! bai = ! strcmp (species, "setosa"); %! Mai = fitrgam (meas(bai,2:4), meas(bai,1), "Interactions", 2); %! addInteractions (Mai, "all") %!error ... %! load fisheriris %! bai = ! strcmp (species, "setosa"); %! addInteractions (fitrgam (meas(bai,2:4), meas(bai,1), ... %! "FitMethod", "splines", ... %! "Formula", "Y ~ x1 + x2 + x1:x2"), "all") %!error ... %! load fisheriris %! bai = ! strcmp (species, "setosa"); %! addInteractions (fitrgam (meas(bai,2:4), meas(bai,1)), {1}) %!error RegressionGAM () %!error RegressionGAM (ones (10,2)) %!error ... %! RegressionGAM (ones (10,2), ones (5,1)) %!error ... %! RegressionGAM ([1;2;3;'a';4], ones (5,1)) %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'some', 'some') %!error %! RegressionGAM (ones (10,2), ones (10,1), 'formula', {'y~x1+x2'}) %!error %! RegressionGAM (ones (10,2), ones (10,1), 'formula', [0, 1, 0]) %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'FitMethod', 'splines', ... %! 'formula', 'something') %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'FitMethod', 'splines', ... %! 'formula', 'something~') %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'FitMethod', 'splines', ... %! 'formula', 'something~') %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'FitMethod', 'splines', ... %! 'formula', 'something~x1:') %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'interactions', 'some') %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'interactions', -1) %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'interactions', [1 2 3 4]) %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'FitMethod', 'splines', ... %! 'interactions', 3) %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'FitMethod', 'splines', ... %! 'formula', 'y ~ x1 + x2', 'interactions', 1) %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'FitMethod', 'splines', ... %! 'interactions', 1, 'formula', 'y ~ x1 + x2') %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'knots', 'a') %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'FitMethod', 'splines', ... %! 'order', 3, 'dof', 2, 'knots', 5) %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'dof', 'a') %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'FitMethod', 'splines', ... %! 'knots', 5, 'order', 3, 'dof', 2) %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'order', 'a') %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'FitMethod', 'splines', ... %! 'knots', 5, 'dof', 2, 'order', 2) %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'tol', -1) %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'responsename', -1) %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'predictors', -1) %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'predictors', ['a','b','c']) %!error ... %! RegressionGAM (ones (10,2), ones (10,1), 'predictors', {'a','b','c'}) ## Test input validation for predict method %!error ... %! predict (RegressionGAM (ones (10,1), ones (10,1))) %!error ... %! predict (RegressionGAM (ones (10,1), ones (10,1)), []) %!error ... %! predict (RegressionGAM (ones (10,2), ones (10,1)), 2) %!error ... %! predict (RegressionGAM (ones (10,2), ones (10,1)), ones (10,2), 'some', 'some') %!error ... %! predict (RegressionGAM (ones (10,2), ones (10,1)), ones (10,2), 'Alpha') %!error ... %! predict (RegressionGAM (ones (10,2), ones (10,1)), ones (10,2), 'includeinteractions', 'some') %!error ... %! predict (RegressionGAM (ones (10,2), ones (10,1)), ones (10,2), 'includeinteractions', 5) %!error ... %! predict (RegressionGAM (ones (10,2), ones (10,1)), ones (10,2), 'alpha', 5) %!error ... %! predict (RegressionGAM (ones (10,2), ones (10,1)), ones (10,2), 'alpha', -1) %!error ... %! predict (RegressionGAM (ones (10,2), ones (10,1)), ones (10,2), 'alpha', 'a') %!error ... %! savemodel (RegressionGAM ([1, 2; 2, 3; 3, 4; 4, 5], [1; 2; 3; 4])) %!error ... %! savemodel (RegressionGAM ([1, 2; 2, 3; 3, 4; 4, 5], [1; 2; 3; 4]), 1) %!error ... %! savemodel (RegressionGAM ([1, 2; 2, 3; 3, 4; 4, 5], [1; 2; 3; 4]), ['ab'; 'cd']) ## The bookkeeping MATLAB reports alongside the fit is present. %!test %! load fisheriris %! Mdl = fitrgam (meas(:,1:3), meas(:,4), 'FitMethod', 'splines'); %! assert_equal (Mdl.Intercept, Mdl.BaseModel.Intercept); %! assert_equal (size (Mdl.W), [Mdl.NumObservations, 1]); %! assert_equal (sum (Mdl.W), 1, 1e-12); %! assert_equal (Mdl.CategoricalPredictors, []); %! assert_equal (Mdl.ExpandedPredictorNames, Mdl.PredictorNames); %! assert_equal (Mdl.IsStandardDeviationFit, false); ## A scalar Knots, Order or DoF applies to every predictor. %!test %! load fisheriris %! Mdl = fitrgam (meas(:,1:3), meas(:,4), 'FitMethod', 'splines', ... %! 'Knots', 4); %! assert_equal (Mdl.Knots, [4, 4, 4]); %! assert_equal (Mdl.Order, [3, 3, 3]); %! assert_equal (Mdl.DoF, [7, 7, 7]); ## A single non-finite value in X or Y is refused. %!error ... %! RegressionGAM ([1, 2; Inf, 4; 5, 6; 7, 8], [1; 2; 3; 4]) %!error ... %! RegressionGAM ([1, 2; 3, 4; 5, 6; 7, 8], [1; 2; Inf; 4]) ## 'Interactions' names every pairwise term, one row per term. %!test %! load fisheriris %! Mdl = fitrgam (meas(:,1:3), meas(:,4), 'FitMethod', 'splines', ... %! 'Interactions', 'all'); %! assert_equal (Mdl.IntMatrix, logical ([1, 1, 0; 1, 0, 1; 0, 1, 1])); %! assert_equal (sum (Mdl.IntMatrix(:)), 6); ## An assigned ResponseTransform reaches the predicted response. %!test %! load fisheriris %! X = meas(:,1:3); %! Mdl = fitrgam (X, meas(:,4)); %! y0 = predict (Mdl, X); %! Mdl.ResponseTransform = 'exp'; %! assert_equal (predict (Mdl, X), exp (y0), 1e-12); ## resubPredict and resubLoss are predict and loss on the training data. %!test %! load fisheriris %! X = meas(:,1:3); %! Y = meas(:,4); %! Mdl = fitrgam (X, Y); %! assert_equal (resubPredict (Mdl), predict (Mdl, X)); %! assert_equal (resubLoss (Mdl), loss (Mdl, X, Y)); ## loss is the weighted mean squared error, and takes a function of its own. %!test %! load fisheriris %! X = meas(:,1:3); %! Y = meas(:,4); %! Mdl = fitrgam (X, Y); %! r = Y - predict (Mdl, X); %! assert_equal (loss (Mdl, X, Y), mean (r .^ 2), 1e-12); %! w = [ones(75, 1); 3 * ones(75, 1)]; %! assert_equal (loss (Mdl, X, Y, 'Weights', w), ... %! sum (w .* r .^ 2) / sum (w), 1e-12); %! mae = @(y, yfit, wt) sum (wt .* abs (y - yfit)); %! assert_equal (loss (Mdl, X, Y, 'LossFun', mae), mean (abs (r)), 1e-12); ## A saved and reloaded model carries every property and predicts alike. %!test %! load fisheriris %! X = meas(:,1:3); %! Mdl = fitrgam (X, meas(:,4), 'Interactions', 'all'); %! Mdl.ResponseTransform = 'exp'; %! fname = tempname (); %! savemodel (Mdl, fname); %! Mdl2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (Mdl2), 'RegressionGAM'); %! assert_equal (Mdl2.Intercept, Mdl.Intercept); %! assert_equal (Mdl2.W, Mdl.W); %! assert_equal (Mdl2.IntMatrix, Mdl.IntMatrix); %! assert_equal (predict (Mdl2, X), predict (Mdl, X)); ## Test input validation for loss method %!shared xr, yr, Mr %! load fisheriris %! xr = meas(:,1:3); %! yr = meas(:,4); %! Mr = fitrgam (xr, yr); %!error ... %! loss (Mr, xr) %!error ... %! loss (Mr, xr, yr, 'Weights') %!error ... %! loss (Mr, [], yr) %!error ... %! loss (Mr, 1, yr) %!error ... %! loss (Mr, xr, yr(1:10)) %!error ... %! loss (Mr, xr, yr, 'LossFun', 'mad') %!error ... %! loss (Mr, xr, yr, 'Bogus', 1) %!error ... %! Mr.ResponseTransform = 'nonsense'; ## RowsUsed is empty when every observation was used. %!test %! load fisheriris %! X = meas(:,2:4); %! Y = meas(:,1); %! Mdl = fitrgam (X, Y); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (class (Mdl.RowsUsed), 'double'); %! assert_equal (Mdl.NumObservations, 150); %! assert_equal (rows (Mdl.X), 150); %! assert_equal (rows (Mdl.W), 150); ## A missing response drops its observation and RowsUsed marks it. %!test %! load fisheriris %! X = meas(:,2:4); %! Y = meas(:,1); %! Y(5) = NaN; %! Mdl = fitrgam (X, Y); %! assert_equal (class (Mdl.RowsUsed), 'logical'); %! assert_equal (size (Mdl.RowsUsed), [150, 1]); %! assert_equal (sum (Mdl.RowsUsed), 149); %! assert_equal (Mdl.RowsUsed(5), false); %! assert_equal (Mdl.NumObservations, 149); %! assert_equal (rows (Mdl.X), 149); %! assert_equal (rows (Mdl.W), 149); ## A missing predictor keeps its observation, so RowsUsed stays empty. %!test %! load fisheriris %! X = meas(:,2:4); %! X(3,2) = NaN; %! Y = meas(:,1); %! Mdl = fitrgam (X, Y); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (Mdl.NumObservations, 150); %! assert_equal (rows (Mdl.X), 150); %! assert_equal (sum (isnan (Mdl.X(:))), 1); ## A fitted model survives savemodel and loadmodel: the properties come ## back as they were and it predicts the same. %!test %! load fisheriris %! X = meas(:,2:4); %! Y = meas(:,1); %! Mdl = fitrgam (X, Y, 'FitMethod', 'splines'); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2), 'RegressionGAM'); %! assert_equal (M2.NumObservations, Mdl.NumObservations); %! assert_equal (M2.PredictorNames, Mdl.PredictorNames); %! assert_equal (class (M2.ResponseTransform), class (Mdl.ResponseTransform)); %! assert_equal (M2.BaseModel.Parameters(1).coefs, ... %! Mdl.BaseModel.Parameters(1).coefs); %! assert_equal (predict (M2, X(1:5,:)), predict (Mdl, X(1:5,:)), 1e-12); ## The same round trip under the boosted-tree engine. %!test %! load fisheriris %! X = meas(:,2:4); %! Y = meas(:,1); %! Mdl = fitrgam (X, Y, 'FitMethod', 'boostedtrees'); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.FitMethod, 'boostedtrees'); %! assert_equal (M2.TreeModel.ShapeValues, Mdl.TreeModel.ShapeValues); %! assert_equal (M2.BinEdges, Mdl.BinEdges); %! assert_equal (predict (M2, X(1:5,:)), predict (Mdl, X(1:5,:)), 1e-12); ## crossval refits one compact model per fold over the observations used. %!test %! load fisheriris %! Mdl = fitrgam (meas(:,1:3), meas(:,4)); %! CVMdl = crossval (Mdl, 'KFold', 3); %! assert_equal (class (CVMdl), 'RegressionPartitionedModel'); %! assert_equal (CVMdl.CrossValidatedModel, 'GAM'); %! assert_equal (class (CVMdl.Trained{1}), 'CompactRegressionGAM'); %! assert_equal (CVMdl.KFold, 3); %! assert_equal (numel (CVMdl.Trained), 3); %! assert_equal (CVMdl.NumObservations, 150); %!test %! load fisheriris %! CVMdl = crossval (fitrgam (meas(1:20,1:3), meas(1:20,4))); %! assert_equal (CVMdl.KFold, 10); %!test %! load fisheriris %! CVMdl = crossval (fitrgam (meas(1:20,1:3), meas(1:20,4)), 'Holdout', 0.25); %! assert_equal (CVMdl.KFold, 1); %!test %! load fisheriris %! CVMdl = crossval (fitrgam (meas(1:12,1:3), meas(1:12,4)), 'Leaveout', 'on'); %! assert_equal (CVMdl.KFold, 12); %!test %! load fisheriris %! cvp = cvpartition (20, 'KFold', 4); %! Mdl = fitrgam (meas(1:20,1:3), meas(1:20,4)); %! assert_equal (crossval (Mdl, 'CVPartition', cvp).KFold, 4); ## The fold models are refitted with the spline parameterisation of the model ## they came from. The compact fold does not report the parameters themselves. %!test %! load fisheriris %! Mdl = fitrgam (meas(1:20,1:3), meas(1:20,4), 'FitMethod', 'splines', ... %! 'Knots', 6, 'Order', 3); %! CVMdl = crossval (Mdl, 'KFold', 3); %! assert_equal (CVMdl.Trained{1}.FitMethod, 'splines'); %! assert_equal (isfield (CVMdl.Trained{1}.BaseModel, 'Intercept'), true); ## Held-out error exceeds the resubstitution error of the same fit. %!test %! load fisheriris %! Mdl = fitrgam (meas(:,1:3), meas(:,4)); %! assert (kfoldLoss (crossval (Mdl, 'KFold', 5)) > resubLoss (Mdl)); %!shared cvobj %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1; 4, 5, 6]; %! y = [1; 2; 3; 4; 5]; %! cvobj = fitrgam (x, y); %!error ... %! crossval (cvobj, 'kfold') %!error ... %! crossval (cvobj, 'kfold', 3, 'holdout', 0.2) %!error ... %! crossval (cvobj, 'kfold', 'a') %!error ... %! crossval (cvobj, 'holdout', 2) %!error ... %! crossval (cvobj, 'leaveout', 1) %!error ... %! crossval (cvobj, 'cvpartition', 1) %!error ... %! crossval (cvobj, 'bogus', 1) ## BinEdges is an empty cell, as MATLAB reports for every learner that ## does no binning. MATLAB's own generalized additive model fills it, ## being boosted trees where this one is splines. %!test %! load fisheriris %! Mdl = fitrgam (meas(:,1:3), meas(:,4)); %! assert_equal (class (Mdl.BinEdges), 'cell'); %! assert_equal (numel (Mdl.BinEdges), 3); %! Msp = fitrgam (meas(:,1:3), meas(:,4), 'FitMethod', 'splines'); %! assert_equal (Msp.BinEdges, {}); ## ySD is computed from the model's own terms, not from the stored predictors. ## The two disagree whenever the model was reshaped, and both directions were ## wrong: an 'Interactions' model has more terms than X has columns and the ## residuals came from a truncated model, and a 'Formula' model can have fewer ## and the call raised. %!test %! load fisheriris %! X = meas(:,1:3); %! Y = meas(:,4); %! Mdl = fitrgam (X, Y, 'FitMethod', 'splines', 'Interactions', 'all'); %! [~, ySD] = predict (Mdl, X(1:4,:)); %! ## Six terms against three stored columns. Truncating to three gave %! ## 0.9379137529, close to seven times the answer. %! assert_equal (ySD, 0.136050646147 * ones (4, 1), 1e-9); ## The same number, derived rather than pinned: ySD is the residual standard ## deviation of the full model over the training data. %!test %! load fisheriris %! X = meas(:,1:3); %! Y = meas(:,4); %! Mdl = fitrgam (X, Y, 'FitMethod', 'splines', 'Interactions', 'all'); %! Xa = X; %! for i = 1:rows (Mdl.IntMatrix) %! t = logical (Mdl.IntMatrix(i,:)); %! Xt = X(:,t); %! Xi = ones (rows (X), 1); %! for c = 1:sum (t) %! Xi = Xi .* Xt(:,c); %! endfor %! Xa = [Xa, Xi]; %! endfor %! yr = ones (rows (Xa), 1) * Mdl.ModelwInt.Intercept; %! for j = 1:columns (Xa) %! yr = yr + ppval (Mdl.ModelwInt.Parameters(j), Xa(:,j)); %! endfor %! [~, ySD] = predict (Mdl, X(1:4,:)); %! assert_equal (ySD, sqrt (var (Y - yr)) * ones (4, 1), 1e-12); ## A formula that selects fewer terms than there are predictors. Asking this ## model for a standard deviation used to raise out of bound, so the second ## and third outputs of predict were unreachable on any formula-built model of ## this shape. %!test %! load fisheriris %! X = meas(:,1:3); %! Y = meas(:,4); %! Mdl = fitrgam (X, Y, 'FitMethod', 'splines', 'Formula', 'Y ~ x1 + x2'); %! assert_equal (numel (Mdl.ModelwInt.Parameters), 2); %! assert_equal (columns (Mdl.X), 3); %! [~, ySD, yInt] = predict (Mdl, X(1:4,:)); %! assert_equal (ySD, 0.348625969903 * ones (4, 1), 1e-9); %! assert_equal (size (yInt), [4, 2]); ## yFit never had this defect and must not acquire one: the prediction matrix ## was always reshaped, and still is. %!test %! load fisheriris %! X = meas(:,1:3); %! Y = meas(:,4); %! Mdl = fitrgam (X, Y, 'FitMethod', 'splines', 'Interactions', 'all'); %! yA = predict (Mdl, X(1:3,:)); %! [yB, ~] = predict (Mdl, X(1:3,:)); %! assert_equal (yA, yB); %! assert_equal (yA, [0.255203457138; 0.210404303824; 0.162599243283], 1e-9); ## The boosted-tree engine is reachable by name while the default is still the ## spline engine, and it reports the surface MATLAB reports. %!test %! load fisheriris %! Mdl = fitrgam (meas(:,2:4), meas(:,1), 'FitMethod', 'boostedtrees'); %! assert_equal (Mdl.FitMethod, 'boostedtrees'); %! assert_equal (numel (Mdl.BinEdges), 3); %! assert_equal (numel (fieldnames (Mdl.ModelParameters)), 13); %! assert_equal (Mdl.ModelParameters.Type, 'regression'); ## A regression intercept is the response mean and boosting leaves it there, ## where a classifier's is fitted and moves. The asymmetry is real and no ## MATLAB-facing property reports it, so it is pinned here. %!test %! load fisheriris %! y = meas(:,1); %! Mdl = fitrgam (meas(:,2:4), y, 'FitMethod', 'boostedtrees'); %! assert_equal (Mdl.Intercept, mean (y), 1e-12); ## Interactions are detected on their own coarse grid. %!test %! load fisheriris %! Mdl = fitrgam (meas(:,2:4), meas(:,1), 'FitMethod', 'boostedtrees', ... %! 'Interactions', 'all'); %! assert_equal (rows (Mdl.Interactions), 3); %! assert_equal (numel (Mdl.PairDetectionBinEdges{1}), 7); ## A tree-fitted model predicts, and compact and loadmodel predict alike. %!test %! load fisheriris %! X = meas(:,2:4); %! Mdl = fitrgam (X, meas(:,1), 'FitMethod', 'boostedtrees'); %! CMdl = compact (Mdl); %! assert_equal (predict (CMdl, X), predict (Mdl, X)); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (predict (M2, X), predict (Mdl, X)); ## The spline engine is unchanged and still reachable by name. %!test %! load fisheriris %! Mdl = fitrgam (meas(:,2:4), meas(:,1), 'FitMethod', 'splines'); %! assert_equal (Mdl.FitMethod, 'splines'); %! assert_equal (Mdl.BinEdges, {}); %! assert_equal (Mdl.Knots, [5, 5, 5]); ## A standard deviation is a spline-engine capability: the boosted-tree engine ## fits no second model for it and says so rather than returning a wrong one. %!error ... %! load fisheriris %! Mdl = fitrgam (meas(:,2:4), meas(:,1), 'FitMethod', 'boostedtrees'); %! [y, ySD] = predict (Mdl, meas(1:4,2:4)); ## An argument belonging to the other engine is refused, not ignored. %!error ... %! fitrgam ([1;2;3;4], [1;2;3;4], 'FitMethod', 'nonsense') %!error ... %! fitrgam ([1;2;3;4], [1;2;3;4], 'FitMethod', 'boostedtrees', 'Knots', 4) %!error ... %! fitrgam ([1;2;3;4], [1;2;3;4], 'FitMethod', 'splines', 'MaxPValue', 0.5) %!error ... %! fitrgam ([1;2;3;4], [1;2;3;4], 'FitMethod', 'boostedtrees', ... %! 'NumTreesPerPredictor', 0) %!error ... %! fitrgam ([1;2;3;4], [1;2;3;4], 'FitMethod', 'boostedtrees', ... %! 'NumTreesPerInteraction', 1.5) %!error ... %! fitrgam ([1;2;3;4], [1;2;3;4], 'FitMethod', 'boostedtrees', ... %! 'MaxNumSplitsPerPredictor', -1) %!error ... %! fitrgam ([1;2;3;4], [1;2;3;4], 'FitMethod', 'boostedtrees', ... %! 'MaxNumSplitsPerInteraction', 'a') %!error ... %! fitrgam ([1;2;3;4], [1;2;3;4], 'FitMethod', 'boostedtrees', ... %! 'InitialLearnRateForPredictors', 0) %!error ... %! fitrgam ([1;2;3;4], [1;2;3;4], 'FitMethod', 'boostedtrees', ... %! 'InitialLearnRateForInteractions', 2) %!error ... %! fitrgam ([1;2;3;4], [1;2;3;4], 'FitMethod', 'boostedtrees', 'Verbose', -1) %!error ... %! fitrgam ([1;2;3;4], [1;2;3;4], 'FitMethod', 'boostedtrees', 'NumPrint', 0) %!error ... %! fitrgam ([1;2;3;4], [1;2;3;4], 'FitMethod', 'boostedtrees', 'MaxPValue', 2) ## HyperparameterOptimizationResults is declared for MATLAB compatibility and ## stays empty, this class running no search over its hyperparameters. %!test %! load fisheriris %! Mdl = fitrgam (meas(:,1:3), meas(:,4)); %! assert_equal (isempty (Mdl.HyperparameterOptimizationResults), true); ## Every documented response transform reaches the response that is reported. %!test %! load fisheriris %! Mdl = fitrgam (meas(:,2:4), meas(:,1)); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! T = {'identity', @(x) x; 'exp', @(x) exp (x); 'log', @(x) log (x)}; %! for i = 1:rows (T) %! Mdl.ResponseTransform = T{i,1}; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, T{i,2}(raw), 1e-12); %! endfor ## A function handle is taken as given and applied to the response. %!test %! load fisheriris %! Mdl = fitrgam (meas(:,2:4), meas(:,1)); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! Mdl.ResponseTransform = @(x) x .^ 2; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, raw .^ 2, 1e-12); %!test # an ordinal column is a numeric predictor, as measured on R2024a %! o = categorical (repmat ({'lo'; 'hi'; 'lo'; 'mid'; 'hi'; 'lo'; 'mid'; ... %! 'hi'; 'lo'; 'mid'; 'hi'; 'lo'}, 2, 1), ... %! {'lo', 'mid', 'hi'}, 'Ordinal', true); %! y = 10 * double (o) + mod ((1:24)', 3); %! Mdl = fitrgam (table (o, y), 'y'); %! assert_equal (Mdl.CategoricalPredictors, []); %! N = table (categorical ({'lo'; 'mid'; 'hi'; ''}, {'lo', 'mid', 'hi'}, ... %! 'Ordinal', true), 'VariableNames', {'o'}); %! assert_equal (predict (Mdl, N), [10.2; 21; 32; mean(y)], 1e-4); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,2:3); %! y = meas(:,1); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SW', 'PL'}); %! T.SL = y; %! Mdl = fitrgam (T, 'SL'); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'SL'), a); %! assert_equal (loss (Mdl, T), a); ## Any two of 'Knots', 'Order' and 'DoF' determine the third, whichever ## order they are given in %!test %! X = linspace (0, 1, 50)'; %! Y = sin (6 * X); %! M = fitrgam (X, Y, 'FitMethod', 'splines', 'Knots', 5, 'DoF', 9); %! assert_equal ([M.Knots, M.Order, M.DoF], [5, 4, 9]); %! M = fitrgam (X, Y, 'FitMethod', 'splines', 'DoF', 9, 'Knots', 5); %! assert_equal ([M.Knots, M.Order, M.DoF], [5, 4, 9]); %! M = fitrgam (X, Y, 'FitMethod', 'splines', 'Order', 2, 'DoF', 9); %! assert_equal ([M.Knots, M.Order, M.DoF], [7, 2, 9]); ## Observation weights of class single or double %!error ... %! RegressionGAM ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', 'Weights', ... %! int8 ([1; 1; 1; 1])) %!error ... %! RegressionGAM ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', 'Weights', true (4, 1)) %!error %! X = [1, 2; 3, 4; 5, 6; 7, 8]; %! y = (1:4)'; %! loss (RegressionGAM (X, y), X, y, 'Weights', int8 ([1; 1; 1; 1])) %!test %! ## Single weights are stored single, summing to one %! load fisheriris %! X = meas(:,2:4); %! y = meas(:,1); %! w = 1 + (1:150)' / 7; %! Mdl = RegressionGAM (X, y, 'Weights', single (w)); %! assert_equal (class (Mdl.W), 'single'); %! assert_equal (sum (double (Mdl.W)), 1, 1e-6); %!test %! ## Single weights compute as double, to the precision of the stored W %! load fisheriris %! X = meas(:,2:4); %! y = meas(:,1); %! w = 1 + (1:150)' / 7; %! A = RegressionGAM (X, y, 'Weights', single (w)); %! B = RegressionGAM (X, y, 'Weights', double (single (w))); %! assert_equal (predict (A, X), predict (B, X), 1e-8); statistics-release-2.1.2/inst/Machine_Learning/RegressionGP.m000066400000000000000000002677561527311104300242350ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef RegressionGP < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} RegressionGP ## ## Gaussian process regression model. ## ## A @qcode{RegressionGP} object holds a Gaussian process fitted to a ## continuous response, and predicts the response for new data with the ## @code{predict} method, which also returns the standard deviation of each ## prediction and a prediction interval. The response is modelled as an ## explicit basis times @qcode{Beta}, plus a draw from a process whose ## covariance is given by the kernel function, plus independent noise of ## standard deviation @qcode{Sigma}. The kernel parameters and @qcode{Sigma} ## are estimated by maximizing the log marginal likelihood. ## ## The object keeps its training data, which @code{resubPredict}, ## @code{resubLoss} and @code{crossval} work on; @code{compact} drops it and ## returns a @code{CompactRegressionGP}, which still predicts. ## ## Create a @qcode{RegressionGP} object with @code{fitrgp} or the class ## constructor. ## ## @seealso{fitrgp, CompactRegressionGP} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {RegressionGP} {property} X ## ## Predictor data ## ## An @math{NxP} numeric matrix, as it was supplied to the constructor. ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## This property is read-only. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} Y ## ## Response data ## ## An @math{Nx1} numeric vector, as it was supplied to the constructor. ## This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} NumObservations ## ## Number of observations used to train the model ## ## A positive integer scalar, counting the rows whose response is not ## missing. A row missing a predictor is counted, as in MATLAB, though the ## fit leaves it out. This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} RowsUsed ## ## Rows of the original data used to train the model ## ## A logical vector with one element per row of the data as supplied, true ## where the row was used, false where its response was missing. It is ## empty when no row was dropped. This property is read-only. ## ## @end deftp RowsUsed = []; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} W ## ## Observation weights ## ## An @math{Nx1} vector, one weight per observation used to train the ## model, scaled to sum to one and of the class of the @qcode{'Weights'} ## given, single or double. This property is read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} PredictorNames ## ## Predictor variable names ## ## A cell array of character vectors, one per column of @qcode{X}. This ## property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} ExpandedPredictorNames ## ## Expanded predictor variable names ## ## A cell array of character vectors. It differs from ## @qcode{PredictorNames} only where a categorical predictor has been ## expanded into indicator variables. This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} ResponseName ## ## Response variable name ## ## A character vector. This property is read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A vector of positive integers indexing the columns of @qcode{X} that ## hold categorical predictors, or empty when none does. This property is ## read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} BinEdges ## ## Bin edges of the predictors ## ## A cell array with one entry per predictor, holding that predictor's bin ## edges where the model discretized it before fitting. It is empty here ## and stays empty: a Gaussian process takes its predictors as they are. ## ## This property is read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} FitMethod ## ## Method used to estimate the parameters ## ## @qcode{'Exact'} when the covariance parameters and the noise were ## estimated by maximizing the log marginal likelihood, and @qcode{'None'} ## when they were kept at their initial values. This property is ## read-only. ## ## @end deftp FitMethod = 'Exact'; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} BasisFunction ## ## Explicit basis of the model ## ## @qcode{'None'}, @qcode{'Constant'}, @qcode{'Linear'}, ## @qcode{'PureQuadratic'}, or the function handle that was supplied. This ## property is read-only. ## ## @end deftp BasisFunction = 'Constant'; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} Beta ## ## Estimated coefficients of the explicit basis ## ## A numeric vector with one element per basis term, empty when the basis ## is @qcode{'None'}. This property is read-only. ## ## @end deftp Beta = []; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} Sigma ## ## Estimated noise standard deviation ## ## A positive scalar. This property is read-only. ## ## @end deftp Sigma = []; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} LogLikelihood ## ## Maximized log marginal likelihood ## ## A scalar, or empty when @qcode{FitMethod} is @qcode{'None'} and nothing ## was maximized. This property is read-only. ## ## @end deftp LogLikelihood = []; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} ModelParameters ## ## Parameters used to train the model ## ## A structure holding the options the fit was performed under. MATLAB ## returns an object of its own class here; a structure carries the same ## information and is what every other learner in this package returns. ## ## @qcode{Beta}, @qcode{Sigma} and @qcode{KernelParameters} are the ## @strong{starting values} the fit was given, empty or zero where it was ## given none, as they are in MATLAB. What the fit found is reported by ## the @qcode{Beta} and @qcode{Sigma} properties and by ## @qcode{KernelInformation}. @qcode{Beta} defaults to a zero for every ## column the basis contributes, so a @qcode{'linear'} basis over three ## predictors starts at four zeros. ## ## @qcode{SigmaLowerBound} is the exception and is reported as it was ## resolved. MATLAB publishes no top-level field of that name, keeping ## it inside an @qcode{Options} structure this class does not carry. ## ## The fields MATLAB reports for its approximate fitting methods ## (@qcode{ActiveSet}, @qcode{Options}, @qcode{OptimizerOptions}, ## @qcode{ConstantKernelParameters}, @qcode{InitialStepSize}, ## @qcode{InitialSigmaLowerBoundTolerance}, @qcode{Verbose} and ## @qcode{CacheSize}) are absent, this class implementing exact fitting ## alone. This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} KernelFunction ## ## Form of the covariance function ## ## A character vector naming the covariance function, or the function ## handle that was supplied. This property is read-only. ## ## @end deftp KernelFunction = 'SquaredExponential'; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} KernelInformation ## ## Covariance function and its parameters ## ## A structure with fields @qcode{Name}, @qcode{KernelParameters} and ## @qcode{KernelParameterNames}, the last naming each parameter in the ## order they are stored. This property is read-only. ## ## @end deftp KernelInformation = []; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} PredictMethod ## ## Method used to make predictions ## ## @qcode{'Exact'}. This property is read-only. ## ## @end deftp PredictMethod = 'Exact'; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} Alpha ## ## Weights the predictions are made from ## ## An @math{Nx1} numeric vector. A prediction is the basis term plus the ## covariance between the new point and the active set, weighted by these. ## This property is read-only. ## ## @end deftp Alpha = []; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} ActiveSetVectors ## ## Subset of the training data used for predictions ## ## An @math{MxP} numeric matrix, standardized where the model standardized ## its predictors. It is the whole of the training data, since only the ## exact method is implemented. This property is read-only. ## ## @end deftp ActiveSetVectors = []; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} ActiveSetMethod ## ## Method used to select the active set ## ## @qcode{'Random'}. This property is read-only. ## ## @end deftp ActiveSetMethod = 'Random'; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} ActiveSetSize ## ## Size of the active set ## ## A positive integer scalar. This property is read-only. ## ## @end deftp ActiveSetSize = []; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} IsActiveSetVector ## ## Which observations are in the active set ## ## A logical vector with one element per training observation. This ## property is read-only. ## ## @end deftp IsActiveSetVector = []; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} ActiveSetHistory ## ## History of the active set selection ## ## @strong{Always empty.} It is declared for MATLAB compatibility, where ## it records the active set chosen at each iteration by a fit method that ## builds one. This class implements the exact method alone, which uses ## the whole of the training data and selects nothing, so there is no ## history to record. This property is read-only. ## ## @end deftp ActiveSetHistory = []; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} BCDInformation ## ## Block coordinate descent information ## ## @strong{Always empty.} It is declared for MATLAB compatibility, where ## it records a block coordinate descent. This class does not use that ## method, so there is nothing to report. This property is read-only. ## ## @end deftp BCDInformation = []; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} PredictorLocation ## ## Means the predictors were centred by ## ## A @math{1xP} numeric vector when the model standardized its predictors, ## and empty when it did not. This property is read-only. ## ## @end deftp PredictorLocation = []; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} PredictorScale ## ## Standard deviations the predictors were scaled by ## ## A @math{1xP} numeric vector when the model standardized its predictors, ## and empty when it did not. This property is read-only. ## ## @end deftp PredictorScale = []; ## -*- texinfo -*- ## @deftp {RegressionGP} {property} HyperparameterOptimizationResults ## ## Results of the hyperparameter optimization ## ## @strong{Always empty.} It is declared for MATLAB compatibility, where ## it holds what an automatic search over the hyperparameters found. This ## class fits the parameters it is given and runs no such search, so there ## is nothing to report. This property is read-only. ## ## @end deftp HyperparameterOptimizationResults = []; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {RegressionGP} {property} ResponseTransform ## ## Transformation applied to the predicted response ## ## A character vector, or the text of the function handle that was ## supplied. Assigning to it accepts either. ## ## @end deftp ResponseTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ## The dummy coding of the categorical predictors, empty when none. Coding_ = []; ## The prediction for a row missing a predictor. MissingResponse_ = NaN; ## The callable behind ResponseTransform. The public property is the text ## MATLAB stores; this is what predict actually applies. RTfun = @(y) y; endproperties methods (Access = public) ## -*- texinfo -*- ## @deftypefn {RegressionGP} {@var{obj} =} RegressionGP (@var{X}, @var{Y}) ## @deftypefnx {RegressionGP} {@var{obj} =} RegressionGP (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {RegressionGP} {@var{obj} =} RegressionGP (@var{Tbl}, @var{formula}) ## @deftypefnx {RegressionGP} {@var{obj} =} RegressionGP (@var{Tbl}, @var{Y}) ## @deftypefnx {RegressionGP} {@var{obj} =} RegressionGP (@dots{}, @var{name}, @var{value}) ## ## Fit a Gaussian process regression model. ## ## @code{@var{obj} = RegressionGP (@var{X}, @var{Y})} returns a Gaussian ## process regression model, @var{obj}, with @var{X} being the predictor ## data and @var{Y} the continuous response of the observations in @var{X}. ## ## @itemize ## @item ## @var{X} must be an @math{NxP} numeric matrix of predictor data, where ## rows correspond to observations and columns to features. ## @item ## @var{Y} must be an @math{Nx1} numeric vector holding the response of the ## corresponding predictor data in @var{X}. @var{Y} must have the same ## number of rows as @var{X}. ## @end itemize ## ## A Gaussian process places a prior over functions, given by the covariance ## function, and conditions it on the observations. The response is ## modelled as @math{H*Beta} plus a draw from that process plus independent ## noise of standard deviation @qcode{Sigma}, where @math{H} is the explicit ## basis. The covariance parameters and @qcode{Sigma} are estimated by ## maximizing the log marginal likelihood, and @qcode{Beta} follows from ## them in closed form as the generalized least squares estimate. ## ## @code{@var{obj} = RegressionGP (@dots{}, @var{name}, @var{value})} ## returns a model with additional options specified by @qcode{Name-Value} ## pair arguments listed below. ## ## @multitable @columnfractions 0.32 0.68 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'KernelFunction'} @tab A character vector naming the ## covariance function, or a function handle taking two matrices of points ## and a parameter vector. The default is @qcode{'squaredexponential'}. ## The supported names are listed below. ## ## @item @qcode{'KernelParameters'} @tab A numeric vector of initial values ## for the covariance parameters. Its length depends on the covariance ## function. These are starting values for the optimization, not fixed ## values. ## ## @item @qcode{'BasisFunction'} @tab A character vector naming the explicit ## basis, one of @qcode{'none'}, @qcode{'constant'}, @qcode{'linear'} or ## @qcode{'pureQuadratic'}, or a function handle taking @var{X} and ## returning the basis matrix. The default is @qcode{'constant'}. ## ## @item @qcode{'Beta'} @tab A numeric vector of basis coefficients. These ## are used as known values only when @qcode{'FitMethod'} is @qcode{'none'}. ## ## @item @qcode{'Sigma'} @tab A positive scalar, the initial value of the ## noise standard deviation. The default is ## @code{std (@var{Y}) / sqrt (2)}. ## ## @item @qcode{'ConstantSigma'} @tab A logical scalar. When @qcode{true} ## the noise standard deviation is held at its initial value instead of ## being estimated. The default is @qcode{false}. ## ## @item @qcode{'SigmaLowerBound'} @tab A positive scalar bounding the noise ## standard deviation from below. The default is ## @code{1e-2 * std (@var{Y})}. ## ## @item @qcode{'FitMethod'} @tab A character vector, either @qcode{'exact'} ## to estimate the parameters or @qcode{'none'} to keep them at their ## initial values. The default is @qcode{'exact'}. ## ## @item @qcode{'PredictMethod'} @tab A character vector. Only ## @qcode{'exact'} is implemented, which is also the only method under which ## a standard deviation and a prediction interval are available. ## ## @item @qcode{'Optimizer'} @tab A character vector naming the optimizer ## used to maximize the log marginal likelihood. @qcode{'quasinewton'} and ## @qcode{'fminunc'} name the same dense solver and are the default, ## @qcode{'lbfgs'} selects limited-memory BFGS, which holds a fixed number ## of curvature pairs rather than a full inverse Hessian and is the cheaper ## choice when the kernel carries many parameters, and @qcode{'fminsearch'} ## is derivative-free. ## ## @item @qcode{'Standardize'} @tab A logical scalar specifying whether the ## predictor data should be centred and scaled before training. The same ## transformation is applied by @code{predict}. The default is ## @qcode{false}. ## ## @item @qcode{'CategoricalPredictors'} @tab The categorical predictors, as ## indices, as a logical vector with one element per predictor, or as ## @qcode{'all'}. Each is dummy coded in its place, one column of zeros and ## ones per distinct value it takes in the training data, named as in ## @qcode{'x1 == 2'}, and the coded columns are not standardized. @qcode{X} ## keeps the predictors as given. A row holding a value the training data ## did not is predicted as a row missing a predictor, the weighted lower ## median of the training response. A predictor may be named rather than ## indexed, as a character matrix of one padded name per row, a string array ## or a cellstr; a name must match an entry of @qcode{'PredictorNames'} ## exactly, its case included. ## ## @item @qcode{'Weights'} @tab An @math{Nx1} single or double vector of ## non-negative observation weights. The default is a vector of ones. The ## model's @code{W} keeps the class of the weights, while every computation ## runs in double. ## ## @item @qcode{'PredictorNames'} @tab A cell array of character vectors ## naming the predictors, in the order they appear in @var{X}. ## ## @item @qcode{'ResponseName'} @tab A character vector naming the response. ## The default is @qcode{'Y'}. ## ## @item @qcode{'ResponseTransform'} @tab A character vector or a function ## handle applied to the response the model predicts. The default is ## @qcode{'none'}. ## @end multitable ## ## The supported values for @qcode{'KernelFunction'} are: ## ## @multitable @columnfractions 0.4 0.6 ## @headitem @var{Value} @tab @var{Parameters} ## @item @qcode{'exponential'} @tab @qcode{[SigmaL; SigmaF]} ## @item @qcode{'squaredexponential'} @tab @qcode{[SigmaL; SigmaF]} ## @item @qcode{'matern32'} @tab @qcode{[SigmaL; SigmaF]} ## @item @qcode{'matern52'} @tab @qcode{[SigmaL; SigmaF]} ## @item @qcode{'rationalquadratic'} @tab @qcode{[SigmaL; AlphaRQ; SigmaF]} ## @item @qcode{'ardexponential'} @tab ## @qcode{[LengthScale1; @dots{}; SigmaF]} ## @item @qcode{'ardsquaredexponential'} @tab ## @qcode{[LengthScale1; @dots{}; SigmaF]} ## @item @qcode{'ardmatern32'} @tab @qcode{[LengthScale1; @dots{}; SigmaF]} ## @item @qcode{'ardmatern52'} @tab @qcode{[LengthScale1; @dots{}; SigmaF]} ## @item @qcode{'ardrationalquadratic'} @tab ## @qcode{[LengthScale1; @dots{}; AlphaRQ; SigmaF]} ## @end multitable ## ## The automatic relevance determination kernels carry one length scale per ## predictor, so a predictor the response does not depend on is given a ## large length scale and stops contributing. ## ## The supported values for @qcode{'ResponseTransform'} are: ## ## @multitable @columnfractions 0.3 0.7 ## @headitem @var{Value} @tab @var{Description} ## @item @qcode{'none'} @tab @math{x} (no transformation) ## @item @qcode{'identity'} @tab @math{x} (no transformation) ## @item @qcode{'exp'} @tab @math{exp (x)} ## @item @qcode{'log'} @tab @math{log (x)} ## @end multitable ## ## Two deviations from MATLAB are deliberate and documented. The distance ## between points is accumulated one predictor at a time instead of by the ## expanded form MATLAB uses by default, because the expanded form does not ## return exactly zero for a point against itself and the rough kernels ## amplify that residue through their square root. The approximate fitting ## and prediction methods, @qcode{'sd'}, @qcode{'sr'}, @qcode{'fic'} and ## @qcode{'bcd'}, together with the active set options that serve them, are ## not implemented and are refused rather than silently ignored. ## ## @seealso{fitrgp, CompactRegressionGP, RegressionSVM, RegressionGAM} ## @end deftypefn function this = RegressionGP (X, Y, varargin) ## Check for sufficient number of input arguments if (nargin < 2) error ("RegressionGP: too few input arguments."); endif ## A table names its own predictors and says which hold levels [this, X, Y, varargin] = resolveTable (this, 'RegressionGP', ... X, Y, varargin); ## Validate X and Y. A row whose response is missing is dropped; one ## missing a predictor stays in X and counts as an observation, as MATLAB ## R2024a keeps it, but takes no part in the fit. [X, Y, RowsUsed] = this.checkXY_ (X, Y, 'RegressionGP', true); [n, p] = size (X); fitRows = ! any (isnan (X), 2); if (mod (numel (varargin), 2) != 0) error (strcat ("RegressionGP: optional arguments must be given", ... " in Name-Value pairs.")); endif ## Parse optional paired arguments optNames = {'KernelFunction', 'KernelParameters', 'BasisFunction', ... 'Beta', 'Sigma', 'ConstantSigma', 'SigmaLowerBound', ... 'FitMethod', 'PredictMethod', 'Optimizer', 'Standardize', ... 'Weights', 'PredictorNames', 'ResponseName', ... 'ResponseTransform', 'CategoricalPredictors'}; ## An empty default stands for one resolved once the data are known: ## 'KernelParameters' take the documented defaults for the kernel, ## 'Beta' starts at zero, 'Sigma' at the standard deviation of the ## response over the root of two and 'SigmaLowerBound' at a hundredth ## of it; 'Weights' are uniform and 'PredictorNames' are x1, x2, ... dfValues = {'squaredexponential', [], 'constant', [], [], false, [], ... 'exact', 'exact', 'quasinewton', false, [], {}, 'Y', ... 'none', []}; [KernelFunction, KernelParameters, BasisFunction, BetaIn, SigmaIn, ... ConstantSigma, SigmaLowerBound, FitMethod, PredictMethod, Optimizer, ... Standardize, Weights, PredictorNames, ResponseName, ... ResponseTransform, CategoricalPredictors, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (ischar (KernelFunction) || ... is_function_handle (KernelFunction))) error (strcat ("RegressionGP: 'KernelFunction' must be a character", ... " vector or a function handle.")); endif if (ischar (KernelFunction) && ... ! any (strcmpi (KernelFunction, kernelNames ()))) error ("RegressionGP: unsupported 'KernelFunction' value."); endif if (! isempty (KernelParameters) && ! (isnumeric (KernelParameters) && ... isvector (KernelParameters) && ... all (KernelParameters > 0))) error (strcat ("RegressionGP: 'KernelParameters' must be a vector", ... " of positive values.")); endif KernelParameters = KernelParameters(:); if (! (ischar (BasisFunction) || ... is_function_handle (BasisFunction))) error (strcat ("RegressionGP: 'BasisFunction' must be a character", ... " vector or a function handle.")); endif if (ischar (BasisFunction) && ... ! any (strcmpi (BasisFunction, ... {'none', 'constant', 'linear', ... 'purequadratic'}))) error ("RegressionGP: unsupported 'BasisFunction' value."); endif if (! isempty (BetaIn) && ! (isnumeric (BetaIn) && isvector (BetaIn))) error ("RegressionGP: 'Beta' must be a numeric vector."); endif BetaIn = BetaIn(:); if (! isempty (SigmaIn) && ! (isnumeric (SigmaIn) && isscalar (SigmaIn) && SigmaIn > 0)) error ("RegressionGP: 'Sigma' must be a positive scalar."); endif if (! (islogical (ConstantSigma) && isscalar (ConstantSigma))) error ("RegressionGP: 'ConstantSigma' must be a logical scalar."); endif if (! isempty (SigmaLowerBound) && ! (isnumeric (SigmaLowerBound) && ... isscalar (SigmaLowerBound) && SigmaLowerBound > 0)) error ("RegressionGP: 'SigmaLowerBound' must be a positive scalar."); endif if (! ischar (FitMethod)) error ("RegressionGP: 'FitMethod' must be a character vector."); endif if (any (strcmpi (FitMethod, {'sd', 'sr', 'fic'}))) error (strcat ("RegressionGP: the approximate fitting methods are", ... " not implemented; 'FitMethod' must be either", ... " 'exact' or 'none'.")); endif if (! any (strcmpi (FitMethod, {'exact', 'none'}))) error ("RegressionGP: unsupported 'FitMethod' value."); endif if (! ischar (PredictMethod)) error ("RegressionGP: 'PredictMethod' must be a character vector."); endif if (any (strcmpi (PredictMethod, {'bcd', 'sd', 'sr', 'fic'}))) error (strcat ("RegressionGP: the approximate prediction methods", ... " are not implemented; 'PredictMethod' must be", ... " 'exact'.")); endif if (! strcmpi (PredictMethod, 'exact')) error ("RegressionGP: unsupported 'PredictMethod' value."); endif if (! ischar (Optimizer)) error ("RegressionGP: 'Optimizer' must be a character vector."); endif if (strcmpi (Optimizer, 'fmincon')) error (strcat ("RegressionGP: 'fmincon' is not available in core", ... " Octave; use 'quasinewton' or 'fminsearch'.")); endif if (! any (strcmpi (Optimizer, {'quasinewton', 'fminunc', ... 'lbfgs', 'fminsearch'}))) error ("RegressionGP: unsupported 'Optimizer' value."); endif if (! (islogical (Standardize) && isscalar (Standardize))) error ("RegressionGP: 'Standardize' must be a logical scalar."); endif if (! isempty (PredictorNames) && ! (iscellstr (PredictorNames) && ... numel (PredictorNames) == p)) error (strcat ("RegressionGP: 'PredictorNames' must be a cell", ... " array of character vectors with one name per", ... " column of X.")); endif if (! ischar (ResponseName)) error ("RegressionGP: 'ResponseName' must be a character vector."); endif if (! isempty (args)) error ("RegressionGP: invalid optional paired argument."); endif ## Store the data and its description this.X = X; this.Y = Y; this.NumObservations = n; this.RowsUsed = RowsUsed; ## The weights sum to one, as MATLAB reports them, and keep their class ## in the model; every computation runs on them as double. W = this.getWeights_ (Weights, n, 'RegressionGP'); if (sum (W) > 0) W = W / sum (W); endif Wclass = "double"; if (! isempty (Weights)) Wclass = class (Weights); endif this.W = cast (W, Wclass); this.MissingResponse_ = missingResponse (Y(fitRows), W(fitRows)); if (isempty (PredictorNames)) PredictorNames = arrayfun (@(k) sprintf ('x%d', k), 1:p, ... 'UniformOutput', false); endif this.PredictorNames = PredictorNames; this.ExpandedPredictorNames = PredictorNames; this.ResponseName = ResponseName; ## Dummy code the categorical predictors on the rows kept. X keeps ## the predictors as given, and the fit sees the coded columns. [Coding, errmsg] = dummyCoding (X, CategoricalPredictors, ... PredictorNames); if (! isempty (errmsg)) error ("RegressionGP: %s", errmsg); endif if (! isempty (Coding.Index)) this.ExpandedPredictorNames = Coding.ExpandedNames; this.CategoricalPredictors = Coding.Index; this.Coding_ = Coding; endif this.BinEdges = {}; this.ResponseTransform = ResponseTransform; ## Standardize the predictors, if asked. The location and scale are ## kept so that predict can apply the same transformation. XS = dummyCoding (X(fitRows,:), Coding); Y = Y(fitRows); n = rows (XS); if (Standardize) this.PredictorLocation = mean (XS, 1); s = std (XS, 0, 1); s(s == 0) = 1; ## A level's column is left as it is, as in MATLAB R2024a. this.PredictorLocation(Coding.Dummy) = 0; s(Coding.Dummy) = 1; this.PredictorScale = s; XS = (XS - this.PredictorLocation) ./ this.PredictorScale; endif ## Initial values, which are the documented defaults where the caller ## gave none ## Every documented default is a multiple of std (Y), which a constant ## response makes zero, and the logarithm of zero is not a starting ## point. A unit spread is used instead: the fit is degenerate either ## way, but it returns a model rather than a NaN. ## The record of the fit keeps what the caller gave, which the lines ## below are about to replace with resolved defaults. What the fit ## used is reported by the Sigma property and by KernelInformation. SigmaGiven = SigmaIn; KernelParametersGiven = KernelParameters; if (isempty (BetaIn)) BetaGiven = zeros (columns (gpBasis (XS(1,:), BasisFunction)), 1); else BetaGiven = BetaIn; endif sy = std (Y); if (sy == 0) sy = 1; endif if (isempty (SigmaIn)) SigmaIn = sy / sqrt (2); endif if (isempty (SigmaLowerBound)) SigmaLowerBound = 1e-2 * sy; endif if (isempty (KernelParameters)) KernelParameters = defaultKernelParameters (XS, Y, KernelFunction); endif ## Fit [theta, sigmaN, beta, LL] = ... gpFit (XS, Y, KernelFunction, KernelParameters, BasisFunction, ... BetaIn, SigmaIn, ConstantSigma, SigmaLowerBound, ... FitMethod, Optimizer); ## Store the fitted model, in the form MATLAB stores it this.FitMethod = properName (FitMethod); this.PredictMethod = properName (PredictMethod); this.BasisFunction = properName (BasisFunction); this.KernelFunction = properName (KernelFunction); kpnames = kernelParameterNames (KernelFunction, columns (XS)); KI = struct (); KI.Name = properName (KernelFunction); KI.KernelParameters = theta; KI.KernelParameterNames = kpnames; this.KernelInformation = KI; this.Beta = beta; this.Sigma = sigmaN; this.LogLikelihood = LL; this.ActiveSetVectors = XS; this.ActiveSetSize = n; this.IsActiveSetVector = fitRows; ## The fit as it was asked for, in MATLAB's field order. Beta, Sigma ## and KernelParameters are the starting values the caller gave, not ## what the fit found; SigmaLowerBound is the exception and is reported ## as it was resolved, MATLAB publishing no top-level field of that ## name and the fold refitting reading this one. this.ModelParameters = struct ('KernelFunction', ... properName (KernelFunction), ... 'KernelParameters', ... KernelParametersGiven, ... 'BasisFunction', ... properName (BasisFunction), ... 'Beta', BetaGiven, ... 'Sigma', SigmaGiven, ... 'FitMethod', properName (FitMethod), ... 'PredictMethod', ... properName (PredictMethod), ... 'ActiveSetSize', this.ActiveSetSize, ... 'ActiveSetMethod', ... this.ActiveSetMethod, ... 'Standardize', logical (Standardize), ... 'Optimizer', Optimizer, ... 'ConstantSigma', ... logical (ConstantSigma), ... 'SigmaLowerBound', SigmaLowerBound, ... 'Version', 1, 'Method', 'GP', ... 'Type', 'regression'); ## The prediction weights K = gpCovariance (XS, XS, KernelFunction, theta); H = gpBasis (XS, this.BasisFunction); A = K + sigmaN^2 * eye (n); if (isempty (beta)) this.Alpha = A \ Y; else this.Alpha = A \ (Y - H * beta); endif endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {RegressionGP} {@var{yFit} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {RegressionGP} {[@var{yFit}, @var{ySD}, @var{yInt}] =} predict (@var{obj}, @var{XC}) ## @deftypefnx {RegressionGP} {[@dots{}] =} predict (@dots{}, @qcode{'Alpha'}, @var{alpha}) ## ## Predict the response for new data with a Gaussian process model. ## ## @code{@var{yFit} = predict (@var{obj}, @var{XC})} returns the predicted ## response of the @qcode{RegressionGP} model @var{obj} at the points in ## @var{XC}, which must have as many columns as the model has predictors. ## ## @code{[@var{yFit}, @var{ySD}, @var{yInt}] = predict (@dots{})} also ## returns the standard deviation of each predicted response and the ## prediction intervals. The standard deviation is that of a new ## @emph{response}, so it carries the noise as well as the uncertainty of ## the latent function, and the interval is the normal quantile of the ## level times it. ## ## @code{[@dots{}] = predict (@dots{}, @qcode{'Alpha'}, @var{alpha})} sets ## the significance level of the intervals, so that they are ## @math{100 * (1 - @var{alpha})} per cent intervals. @var{alpha} must be ## a scalar in the range @math{[0, 1]} and defaults to @math{0.05}. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @end deftypefn function [yFit, ySD, yInt] = predict (this, XC, varargin) if (nargin < 2) error ("RegressionGP.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'RegressionGP.predict', XC); if (isempty (XC)) error ("RegressionGP.predict: XC is empty."); endif if (columns (XC) != columns (this.X)) error (strcat ("RegressionGP.predict: XC must have the same", ... " number of predictors as the trained model.")); endif ## Parse optional paired arguments optNames = {'Alpha'}; dfValues = {0.05}; [CIAlpha, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (isnumeric (CIAlpha) && isscalar (CIAlpha) && ... CIAlpha >= 0 && CIAlpha <= 1)) error (strcat ("RegressionGP.predict: 'Alpha' must be a scalar", ... " between 0 and 1.")); endif if (! isempty (args)) error ("RegressionGP.predict: invalid optional paired argument."); endif if (! isempty (this.Coding_)) XC = dummyCoding (XC, this.Coding_); endif M = this.predictModel_ (CIAlpha); ## A row missing a predictor is predicted as MATLAB R2024a predicts it, ## the weighted lower median of the training response. miss = any (isnan (XC), 2); if (nargout < 2) yFit = gpPredict (XC, M); elseif (nargout < 3) [yFit, ySD] = gpPredict (XC, M); else [yFit, ySD, yInt] = gpPredict (XC, M); yInt = this.RTfun (yInt); endif yFit(miss) = this.MissingResponse_; yFit = this.RTfun (yFit); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionGP} {@var{yFit} =} resubPredict (@var{obj}) ## @deftypefnx {RegressionGP} {[@var{yFit}, @var{ySD}, @var{yInt}] =} resubPredict (@var{obj}) ## ## Predict the response of the training data with a Gaussian process model. ## ## @code{@var{yFit} = resubPredict (@var{obj})} returns the response the ## @qcode{RegressionGP} model @var{obj} predicts at its own training data, ## and the further outputs are those of @code{predict}. ## ## @end deftypefn function [yFit, ySD, yInt] = resubPredict (this) if (nargout < 2) yFit = this.predict (this.X); elseif (nargout < 3) [yFit, ySD] = this.predict (this.X); else [yFit, ySD, yInt] = this.predict (this.X); endif endfunction ## -*- texinfo -*- ## @deftypefn {RegressionGP} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {RegressionGP} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {RegressionGP} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {RegressionGP} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Compute the regression loss of a Gaussian process model. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} returns the mean ## squared error of the model @var{obj} on the data @var{X} and @var{Y}. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{L} = loss (@dots{}, @var{name}, @var{value})} accepts ## @qcode{'LossFun'}, either @qcode{'mse'}, @qcode{'mae'}, ## @qcode{'epsiloninsensitive'} or a function handle taking the observed ## and the predicted response, and @qcode{'Weights'}, a vector of ## non-negative observation weights. ## ## @end deftypefn function L = loss (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("RegressionGP.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); [X, Y] = this.checkXY_ (X, Y, 'RegressionGP.loss'); ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights', 'Epsilon'}; dfValues = {'mse', [], 0}; [LossFun, Weights, Epsilon, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (ischar (LossFun) || is_function_handle (LossFun))) error (strcat ("RegressionGP.loss: 'LossFun' must be a character", ... " vector or a function handle.")); endif if (ischar (LossFun) && ... ! any (strcmpi (LossFun, {'mse', 'mae', ... 'epsiloninsensitive'}))) error ("RegressionGP.loss: unsupported 'LossFun' value."); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("RegressionGP.loss: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isvector (Weights) && ... numel (Weights) == rows (X) && all (Weights >= 0))) error (strcat ("RegressionGP.loss: 'Weights' must be a vector of", ... " non-negative values with one element per", ... " observation.")); endif Weights = double (Weights(:)); if (! isempty (args)) error ("RegressionGP.loss: invalid optional paired argument."); endif if (isempty (Weights)) Weights = ones (rows (X), 1); endif yFit = this.predict (X); if (is_function_handle (LossFun)) L = LossFun (Y, yFit); return; endif switch (lower (LossFun)) case 'mse' L = sum (Weights .* (Y - yFit) .^ 2) / sum (Weights); case 'mae' L = sum (Weights .* abs (Y - yFit)) / sum (Weights); case 'epsiloninsensitive' e = max (0, abs (Y - yFit) - Epsilon); L = sum (Weights .* e) / sum (Weights); endswitch endfunction ## -*- texinfo -*- ## @deftypefn {RegressionGP} {@var{L} =} resubLoss (@var{obj}) ## @deftypefnx {RegressionGP} {@var{L} =} resubLoss (@dots{}, @var{name}, @var{value}) ## ## Compute the resubstitution loss of a Gaussian process model. ## ## @code{@var{L} = resubLoss (@var{obj})} returns the loss of the model ## @var{obj} on the data it was trained on, and accepts the same ## Name-Value pairs as @code{loss}. ## ## @end deftypefn function L = resubLoss (this, varargin) L = this.loss (this.X, this.Y, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionGP} {[@var{loores}, @var{neff}] =} postFitStatistics (@var{obj}) ## ## Compute the leave-one-out residuals of a Gaussian process model. ## ## @code{[@var{loores}, @var{neff}] = postFitStatistics (@var{obj})} ## returns the @math{Nx1} vector of leave-one-out residuals of the model ## @var{obj}, and the number of effective parameters the fit uses. Neither ## requires refitting the model: both follow from the factorization the fit ## already produced. ## ## The coefficients of the explicit basis are treated as estimated, which ## is what @qcode{FitMethod} @qcode{'Exact'} makes them, while the ## covariance parameters and the noise are treated as known. ## ## @end deftypefn function [loores, neff] = postFitStatistics (this) if (! strcmpi (this.PredictMethod, 'Exact')) error (strcat ("RegressionGP.postFitStatistics: post-fit", ... " statistics are available only when PredictMethod", ... " is 'Exact'.")); endif n = this.NumObservations; K = gpCovariance (this.ActiveSetVectors, this.ActiveSetVectors, ... this.KernelFunction, ... this.KernelInformation.KernelParameters); A = K + this.Sigma^2 * eye (n); H = gpBasis (this.ActiveSetVectors, this.BasisFunction); [L, p] = chol (A, 'lower'); if (p != 0) error (strcat ("RegressionGP.postFitStatistics: the covariance", ... " matrix is not positive definite.")); endif Li = L \ eye (n); Ai = Li' * Li; if (isempty (H)) P = Ai; else P = Ai - Ai * H * ((H' * Ai * H) \ (H' * Ai)); endif loores = (P * this.Y) ./ diag (P); neff = columns (H) + trace (K * P); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionGP} {@var{CVMdl} =} crossval (@var{obj}) ## @deftypefnx {RegressionGP} {@var{CVMdl} =} crossval (@dots{}, @var{name}, @var{value}) ## ## Cross validate a Gaussian process model. ## ## @code{@var{CVMdl} = crossval (@var{obj})} returns a ## @qcode{RegressionPartitionedModel} built from the model @var{obj} by ## ten-fold cross validation. ## ## @code{@var{CVMdl} = crossval (@dots{}, @var{name}, @var{value})} accepts ## @qcode{'KFold'}, @qcode{'Holdout'}, @qcode{'Leaveout'} and ## @qcode{'CVPartition'}, of which at most one may be given. ## ## @end deftypefn function CVMdl = crossval (this, varargin) numFolds = 10; Holdout = []; Leaveout = 'off'; CVPartition = []; given = 0; while (numel (varargin) > 0) if (numel (varargin) < 2) error (strcat ("RegressionGP.crossval: optional arguments must", ... " be given in Name-Value pairs.")); endif switch (lower (varargin{1})) case 'kfold' numFolds = varargin{2}; if (! (isnumeric (numFolds) && isscalar (numFolds) && ... numFolds == fix (numFolds) && numFolds > 1)) error (strcat ("RegressionGP.crossval: 'KFold' must be an", ... " integer value greater than 1.")); endif given++; case 'holdout' Holdout = varargin{2}; if (! (isnumeric (Holdout) && isscalar (Holdout) && ... Holdout > 0 && Holdout < 1)) error (strcat ("RegressionGP.crossval: 'Holdout' must be a", ... " numeric value between 0 and 1.")); endif given++; case 'leaveout' Leaveout = varargin{2}; if (! (ischar (Leaveout) && ... any (strcmpi (Leaveout, {'on', 'off'})))) error (strcat ("RegressionGP.crossval: 'Leaveout' must be", ... " either 'on' or 'off'.")); endif given++; case 'cvpartition' CVPartition = varargin{2}; if (! (isa (CVPartition, 'cvpartition'))) error (strcat ("RegressionGP.crossval: 'CVPartition' must", ... " be a cvpartition object.")); endif given++; otherwise error (strcat ("RegressionGP.crossval: invalid parameter name", ... " in optional paired arguments.")); endswitch varargin(1:2) = []; endwhile if (given > 1) error (strcat ("RegressionGP.crossval: you can use only one of", ... " 'KFold', 'Holdout', 'Leaveout', or", ... " 'CVPartition' options.")); endif n = this.NumObservations; if (! isempty (CVPartition)) partition = CVPartition; elseif (! isempty (Holdout)) partition = cvpartition (n, 'Holdout', Holdout); elseif (strcmpi (Leaveout, 'on')) partition = cvpartition (n, 'LeaveOut'); else partition = cvpartition (n, 'KFold', numFolds); endif CVMdl = RegressionPartitionedModel (this, partition); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionGP} {@var{CMdl} =} compact (@var{obj}) ## ## Return a compact Gaussian process regression model. ## ## @code{@var{CMdl} = compact (@var{obj})} returns a ## @qcode{CompactRegressionGP} object holding what is needed to predict ## and nothing else: the training data, the response and everything that ## describes them are dropped. ## ## @end deftypefn function CMdl = compact (this) CMdl = CompactRegressionGP (this); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionGP} {} savemodel (@var{obj}, @var{filename}) ## ## Save a Gaussian process model to a file. ## ## @code{savemodel (@var{obj}, @var{filename})} saves the model @var{obj} ## into @var{filename} in a form @code{loadmodel} can read back. ## ## @end deftypefn function savemodel (obj, fname) classdef_name = 'RegressionGP'; X = obj.X; Y = obj.Y; NumObservations = obj.NumObservations; RowsUsed = obj.RowsUsed; W = obj.W; PredictorNames = obj.PredictorNames; ExpandedPredictorNames = obj.ExpandedPredictorNames; ResponseName = obj.ResponseName; CategoricalPredictors = obj.CategoricalPredictors; BinEdges = obj.BinEdges; FitMethod = obj.FitMethod; BasisFunction = obj.BasisFunction; Beta = obj.Beta; Sigma = obj.Sigma; LogLikelihood = obj.LogLikelihood; ModelParameters = obj.ModelParameters; KernelFunction = obj.KernelFunction; KernelInformation = obj.KernelInformation; PredictMethod = obj.PredictMethod; Alpha = obj.Alpha; ActiveSetVectors = obj.ActiveSetVectors; ActiveSetMethod = obj.ActiveSetMethod; ActiveSetSize = obj.ActiveSetSize; IsActiveSetVector = obj.IsActiveSetVector; ActiveSetHistory = obj.ActiveSetHistory; BCDInformation = obj.BCDInformation; PredictorLocation = obj.PredictorLocation; PredictorScale = obj.PredictorScale; ResponseTransform = obj.ResponseTransform; HyperparameterOptimizationResults = obj.HyperparameterOptimizationResults; Coding_ = obj.Coding_; MissingResponse_ = obj.MissingResponse_; save ('-binary', fname, 'classdef_name', 'X', 'Y', 'NumObservations', ... 'RowsUsed', 'W', 'PredictorNames', 'ExpandedPredictorNames', ... 'ResponseName', 'CategoricalPredictors', 'BinEdges', ... 'FitMethod', 'BasisFunction', 'Beta', 'Sigma', 'LogLikelihood', ... 'ModelParameters', 'KernelFunction', 'KernelInformation', ... 'PredictMethod', 'Alpha', 'ActiveSetVectors', ... 'ActiveSetMethod', 'ActiveSetSize', 'IsActiveSetVector', ... 'ActiveSetHistory', 'BCDInformation', ... 'PredictorLocation', 'PredictorScale', 'ResponseTransform', ... 'HyperparameterOptimizationResults', 'Coding_', 'MissingResponse_'); endfunction endmethods methods (Access = public, Hidden) function disp (this) printf ("\n RegressionGP\n\n"); printf ("%25s: '%s'\n", 'ResponseName', this.ResponseName); printf ("%25s: %d\n", 'NumObservations', this.NumObservations); printf ("%25s: %d\n", 'NumPredictors', columns (this.X)); printf ("%25s: '%s'\n", 'KernelFunction', ... nameOf (this.KernelFunction)); printf ("%25s: '%s'\n", 'BasisFunction', nameOf (this.BasisFunction)); printf ("%25s: '%s'\n", 'FitMethod', this.FitMethod); printf ("%25s: '%s'\n", 'PredictMethod', this.PredictMethod); printf ("%25s: %g\n", 'Sigma', this.Sigma); if (! isempty (this.LogLikelihood)) printf ("%25s: %g\n", 'LogLikelihood', this.LogLikelihood); endif printf ("\n"); endfunction ## The structure gpPredict consumes. It is assembled here so that the ## compact class and this one predict through the same code. function M = predictModel_ (this, CIAlpha) M = struct ('X', this.ActiveSetVectors, 'Alpha', this.Alpha, ... 'KernelFunction', this.KernelFunction, ... 'Theta', this.KernelInformation.KernelParameters, ... 'BasisFunction', this.BasisFunction, 'Beta', this.Beta, ... 'Sigma', this.Sigma, 'Location', this.PredictorLocation, ... 'Scale', this.PredictorScale, 'CIAlpha', CIAlpha); endfunction ## Custom setter, so that assigning a name or a handle updates both the ## text the property reports and the callable predict uses. function this = set.ResponseTransform (this, val) [this.RTfun, this.ResponseTransform] = ... parseResponseTransform (val, 'RegressionGP'); endfunction endmethods methods (Access = private) ## Validate the predictor and response data, and drop any row that is not ## complete in both. function [X, Y, used] = checkXY_ (this, X, Y, caller, keepX) if (! (isnumeric (X) && isreal (X) && ismatrix (X))) error ("%s: invalid values in X.", caller); endif if (! (isnumeric (Y) && isreal (Y) && isvector (Y))) error ("%s: invalid values in Y.", caller); endif Y = Y(:); if (rows (X) != rows (Y)) error ("%s: number of rows in X and Y must be equal.", caller); endif if (isempty (X)) error ("%s: X is empty.", caller); endif ## KEEPX keeps a row that is missing only a predictor, which the fit ## sets aside but the model still counts. if (nargin > 4 && keepX) used = ! isnan (Y); complete = used & ! any (isnan (X), 2); else used = ! (any (isnan (X), 2) | isnan (Y)); complete = used; endif if (! any (complete)) error ("%s: no complete observations in the data.", caller); endif if (all (used)) used = []; else X = X(used, :); Y = Y(used); endif endfunction ## Validate observation weights, defaulting to a vector of ones. function W = getWeights_ (this, Weights, n, caller) if (isempty (Weights)) W = ones (n, 1); return; endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("%s: %s", caller, errmsg); endif if (! (isnumeric (Weights) && isvector (Weights) && ... numel (Weights) == n && all (Weights >= 0))) error (strcat ("%s: 'Weights' must be a vector of non-negative", ... " values with one element per observation."), caller); endif W = double (Weights(:)); endfunction endmethods methods (Static, Hidden) function mdl = load_model (filename, data) mdl = RegressionGP (data.X, data.Y); fields = fieldnames (data); for k = 1:numel (fields) mdl.(fields{k}) = data.(fields{k}); endfor endfunction endmethods endclassdef ## The proper name of a value, as the model reports it: MATLAB accepts the ## lowercase spelling and stores the capitalized one. function s = properName (v) if (is_function_handle (v)) s = v; return; endif known = {'none', 'None'; 'exact', 'Exact'; 'constant', 'Constant'; ... 'linear', 'Linear'; 'purequadratic', 'PureQuadratic'; ... 'random', 'Random'; ... 'exponential', 'Exponential'; ... 'squaredexponential', 'SquaredExponential'; ... 'matern32', 'Matern32'; 'matern52', 'Matern52'; ... 'rationalquadratic', 'RationalQuadratic'; ... 'ardexponential', 'ARDExponential'; ... 'ardsquaredexponential', 'ARDSquaredExponential'; ... 'ardmatern32', 'ARDMatern32'; 'ardmatern52', 'ARDMatern52'; ... 'ardrationalquadratic', 'ARDRationalQuadratic'}; idx = find (strcmpi (v, known(:,1)), 1); if (isempty (idx)) s = v; else s = known{idx, 2}; endif endfunction function s = nameOf (v) if (is_function_handle (v)) s = func2str (v); else s = v; endif endfunction function names = kernelNames () names = {'exponential', 'squaredexponential', 'matern32', 'matern52', ... 'rationalquadratic', 'ardexponential', 'ardsquaredexponential', ... 'ardmatern32', 'ardmatern52', 'ardrationalquadratic'}; endfunction ## The names of a kernel's parameters, in the order they are stored. function names = kernelParameterNames (kern, d) if (is_function_handle (kern)) names = {}; return; endif if (strncmpi (kern, 'ard', 3)) names = arrayfun (@(k) sprintf ('LengthScale%d', k), 1:d, ... 'UniformOutput', false)'; if (strcmpi (kern, 'ardrationalquadratic')) names = [names; {'AlphaRQ'}]; endif names = [names; {'SigmaF'}]; else if (strcmpi (kern, 'rationalquadratic')) names = {'SigmaL'; 'AlphaRQ'; 'SigmaF'}; else names = {'SigmaL'; 'SigmaF'}; endif endif endfunction ## The documented initial values of the covariance parameters. function theta0 = defaultKernelParameters (X, Y, kern) if (is_function_handle (kern)) error (strcat ("RegressionGP: 'KernelParameters' must be given when", ... " 'KernelFunction' is a function handle.")); endif sf = std (Y) / sqrt (2); if (sf == 0) sf = 1 / sqrt (2); endif if (strncmpi (kern, 'ard', 3)) theta0 = std (X, 0, 1)'; if (strcmpi (kern, 'ardrationalquadratic')) theta0 = [theta0; 1]; endif theta0 = [theta0; sf]; else theta0 = mean (std (X, 0, 1)); if (strcmpi (kern, 'rationalquadratic')) theta0 = [theta0; 1]; endif theta0 = [theta0; sf]; endif theta0(theta0 <= 0) = 1; endfunction ## Covariance between two sets of points, built in or supplied. function K = gpCovariance (A, B, kern, theta) if (is_function_handle (kern)) K = kern (A, B, theta); else K = gpKernel (A, B, kern, theta); endif endfunction ## Fit the model. With FitMethod 'none' the initial values are kept and only ## Beta is computed; with 'exact' the covariance parameters and the noise are ## estimated by maximizing the log marginal likelihood, with Beta following in ## closed form at every step, so the optimizer never carries it. function [theta, sigmaN, beta, LL] = gpFit (X, Y, kern, theta0, basis, ... betaIn, sigma0, constSigma, ... sigmaLB, fitMethod, optimizer) H = gpBasis (X, basis); if (strcmpi (fitMethod, 'none')) theta = theta0; sigmaN = sigma0; if (isempty (betaIn)) beta = zeros (columns (H), 1); else if (numel (betaIn) != columns (H)) error (strcat ("RegressionGP: 'Beta' must have one element per", ... " basis term.")); endif beta = betaIn; endif LL = []; return; endif if (sigma0 <= sigmaLB) sigma0 = sigmaLB * (1 + 1e-6); endif ## The optimizer works on an unconstrained scale: the covariance parameters ## through their logarithm, the noise as the lower bound plus an exponential, ## which is the parameterization MATLAB documents. if (constSigma) u0 = log (theta0); else u0 = [log(theta0); log(sigma0 - sigmaLB)]; endif f = @(u) gpObjective (u, X, Y, kern, H, sigmaLB, constSigma, sigma0); if (strcmpi (optimizer, 'fminsearch')) opts = optimset ('MaxFunEvals', 5000, 'MaxIter', 2000, 'TolX', 1e-10, ... 'TolFun', 1e-10); u = fminsearch (@(u) f(u), u0, opts); elseif (strcmpi (optimizer, 'lbfgs')) ## LossTolerance is switched off rather than left at its default: the ## objective here is a negative log likelihood, which goes negative, and ## the test is on the value rather than on its change, so any default ## would stop the fit on the first step past zero. opts = struct ('IterationLimit', 1000, 'GradientTolerance', 1e-8, ... 'StepTolerance', 1e-12, 'LossTolerance', -Inf); u = __lbfgs__ (f, u0, opts); else opts = optimset ('GradObj', 'on', 'MaxFunEvals', 5000, ... 'MaxIter', 1000, 'TolX', 1e-12, 'TolFun', 1e-12); u = fminunc (f, u0, opts); endif if (constSigma) theta = exp (u); sigmaN = sigma0; else theta = exp (u(1:end-1)); sigmaN = sigmaLB + exp (u(end)); endif [nll, beta] = gpNegLogLik (u, X, Y, kern, H, sigmaLB, constSigma, sigma0); LL = -nll; endfunction ## The objective as an optimizer expects it: the value first and the gradient ## second, where gpNegLogLik returns Beta in between for the caller that wants ## it. function [nll, g] = gpObjective (u, X, Y, kern, H, sigmaLB, constSigma, sigma0) if (nargout > 1) [nll, ~, g] = gpNegLogLik (u, X, Y, kern, H, sigmaLB, constSigma, sigma0); else nll = gpNegLogLik (u, X, Y, kern, H, sigmaLB, constSigma, sigma0); endif endfunction ## The negative profile log marginal likelihood and its gradient. Beta sits at ## its generalized least squares optimum throughout, so by the envelope theorem ## it contributes nothing to the gradient. function [nll, beta, g] = gpNegLogLik (u, X, Y, kern, H, sigmaLB, ... constSigma, sigma0) n = rows (X); if (constSigma) theta = exp (u); sigmaN = sigma0; else theta = exp (u(1:end-1)); sigmaN = sigmaLB + exp (u(end)); endif K = gpCovariance (X, X, kern, theta); A = K + sigmaN^2 * eye (n); [L, p] = chol (A, 'lower'); if (p != 0) nll = 1e10; beta = zeros (columns (H), 1); g = zeros (size (u)); return; endif Li_Y = L \ Y; if (isempty (H)) beta = zeros (0, 1); v = Li_Y; else ## Beta is the least squares solution of the whitened system, taken ## directly rather than through the normal equations: forming Li_H' * Li_H ## squares the condition number, and a basis whose columns are nearly ## dependent then warns from inside the objective on every iteration. Li_H = L \ H; beta = Li_H \ Li_Y; v = Li_Y - Li_H * beta; endif nll = 0.5 * (v' * v) + sum (log (diag (L))) + 0.5 * n * log (2*pi); if (nargout < 3) return; endif r = Y; if (! isempty (H)) r = Y - H * beta; endif ## The inverse is taken through the factor that is already to hand: inv () ## on a covariance whose noise has been driven small warns about a ## conditioning the caller can do nothing about, and the triangular solves ## are both quieter and cheaper. Li = L \ eye (n); Ai = Li' * Li; a = Ai * r; W = Ai - a * a'; g = zeros (size (u)); if (! is_function_handle (kern)) dK = gpKernelDeriv (X, kern, theta, K); for j = 1:numel (theta) g(j) = 0.5 * sum (sum (W .* dK{j})) * theta(j); endfor else ## A supplied kernel has no derivative to hand, so its parameters are ## differenced instead. for j = 1:numel (theta) h = 1e-6 * max (1, abs (u(j))); up = u; up(j) += h; um = u; um(j) -= h; g(j) = (gpNegLogLik (up, X, Y, kern, H, sigmaLB, constSigma, sigma0) - ... gpNegLogLik (um, X, Y, kern, H, sigmaLB, constSigma, sigma0)) ... / (2*h); endfor endif if (! constSigma) g(end) = 0.5 * trace (W) * 2 * sigmaN * (sigmaN - sigmaLB); endif endfunction ## Derivatives of the covariance matrix with respect to each of its parameters. function dK = gpKernelDeriv (X, kern, theta, K) d = columns (X); ard = (numel (kern) > 3 && strncmpi (kern, 'ARD', 3)); base = lower (strrep (lower (kern), 'ard', '')); if (ard) L = theta(1:d)(:)'; if (strcmp (base, 'rationalquadratic')) alphaRQ = theta(d+1); sigmaF = theta(d+2); else sigmaF = theta(d+1); endif else L = repmat (theta(1), 1, d); if (strcmp (base, 'rationalquadratic')) alphaRQ = theta(2); sigmaF = theta(3); else sigmaF = theta(2); endif endif n = rows (X); S = cell (1, d); r2 = zeros (n, n); for j = 1:d S{j} = ((X(:,j) - X(:,j)') / L(j)) .^ 2; r2 += S{j}; endfor r = sqrt (r2); zero = (r2 == 0); switch (base) case 'exponential' dKdr2 = -K ./ (2 * r); dKdr2(zero) = 0; case 'squaredexponential' dKdr2 = -K / 2; case 'matern32' s = sqrt (3) * r; dKdr2 = -sigmaF^2 * (s .^ 2) .* exp (-s) ./ (2 * r2); dKdr2(zero) = -sigmaF^2 * 3 / 2; case 'matern52' s = sqrt (5) * r; dKdr2 = -sigmaF^2 * (s .^ 2) .* (1 + s) .* exp (-s) ./ (6 * r2); dKdr2(zero) = -sigmaF^2 * 5 / 6; case 'rationalquadratic' Q = 1 + r2 / (2 * alphaRQ); dKdr2 = -sigmaF^2 * Q .^ (-alphaRQ - 1) / 2; endswitch dK = {}; if (ard) for j = 1:d dK{end+1} = dKdr2 .* (-2 * S{j} / L(j)); endfor else dK{end+1} = dKdr2 .* (-2 * r2 / L(1)); endif if (strcmp (base, 'rationalquadratic')) Q = 1 + r2 / (2 * alphaRQ); dK{end+1} = K .* (-log (Q) + r2 ./ (2 * alphaRQ * Q)); endif dK{end+1} = 2 * K / sigmaF; endfunction ## Every expected value below was measured on MATLAB R2024a. The tolerances ## are set by what this implementation actually achieves: the covariance ## parameters and the noise agree to about nine digits, because the optimizer ## here converges to a log marginal likelihood a little above MATLAB's, and ## every prediction inherits that. %!demo %! ## Fit a Gaussian process to noisy observations of a smooth function and %! ## show the prediction interval widening away from the data. %! x = [0.1; 0.3; 0.4; 0.7; 0.9; 1.4; 1.6; 1.9]'; %! x = x(:); %! y = sin (3 * x) + 0.05 * cos (11 * x); %! Mdl = fitrgp (x, y); %! xq = linspace (-0.2, 2.2, 200)'; %! [yq, ~, yint] = predict (Mdl, xq); %! figure ('visible', 'off'); %! hold on; %! plot (xq, yint(:,1), 'r:'); %! plot (xq, yint(:,2), 'r:'); %! plot (xq, yq, 'b-'); %! plot (x, y, 'ko'); %! hold off; %! xlabel ('x'); %! ylabel ('y'); %! title ('Gaussian process regression with 95% prediction interval'); %!test %! ## The model reports the surface MATLAB reports %! x = linspace (0, 1, 20)'; %! y = sin (2*pi*x) + 0.1 * cos (7*x); %! Mdl = RegressionGP (x, y); %! assert_equal (class (Mdl), 'RegressionGP'); %! assert_equal (Mdl.NumObservations, 20); %! assert_equal (Mdl.FitMethod, 'Exact'); %! assert_equal (Mdl.PredictMethod, 'Exact'); %! assert_equal (Mdl.BasisFunction, 'Constant'); %! assert_equal (Mdl.KernelFunction, 'SquaredExponential'); %! assert_equal (Mdl.ResponseName, 'Y'); %! assert_equal (Mdl.PredictorNames, {'x1'}); %! assert_equal (Mdl.ResponseTransform, 'none'); %!test %! ## BinEdges is an empty cell, as it is for every learner that does no %! ## binning: a Gaussian process takes its predictors as they are %! x = linspace (0, 1, 20)'; %! Mdl = RegressionGP (x, sin (2*pi*x)); %! assert_equal (class (Mdl.BinEdges), 'cell'); %! assert_equal (Mdl.BinEdges, {}); %!test %! ## The fitted covariance parameters and noise match R2024a %! x = linspace (0, 1, 20)'; %! y = sin (2*pi*x) + 0.1 * cos (7*x); %! Mdl = RegressionGP (x, y); %! assert_equal (Mdl.KernelInformation.KernelParameters, ... %! [0.386370514454926; 1.505132511329997], 1e-8); %! assert_equal (Mdl.Beta, -0.072781321631854, 1e-7); %! assert_equal (Mdl.Sigma, 0.006877618228324, 1e-7); %! assert_equal (Mdl.LogLikelihood, 47.574057509534278, 1e-4); %!test %! ## 'lbfgs' finds the same optimum the dense solver does %! x = linspace (0, 1, 20)'; %! y = sin (2*pi*x) + 0.1 * cos (7*x); %! Mdl = RegressionGP (x, y, 'Optimizer', 'lbfgs'); %! ## Relative, and loose: the likelihood surface is flat here, so where the %! ## search stops is decided by the last bits of the arithmetic and moves by %! ## about 1e-6 between platforms. The likelihood is what pins this optimum, %! ## and the test below holds it to 1e-8. %! assert_equal (Mdl.KernelInformation.KernelParameters, ... %! [0.386370514454926; 1.505132511329997], -1e-4); %! assert_equal (Mdl.Beta, -0.072781321631854, -1e-4); %!test %! ## The likelihood agrees far more tightly than the parameters do, because %! ## the surface is flat there: a 2e-7 move in the parameters buys 2e-10 of %! ## likelihood. Comparing the parameters alone understates the agreement. %! x = linspace (0, 1, 20)'; %! y = sin (2*pi*x) + 0.1 * cos (7*x); %! Mq = RegressionGP (x, y, 'Optimizer', 'quasinewton'); %! Ml = RegressionGP (x, y, 'Optimizer', 'lbfgs'); %! assert_equal (Ml.LogLikelihood, Mq.LogLikelihood, 1e-8); %!test %! ## The optimizer that ran is recorded %! x = linspace (0, 1, 20)'; %! y = sin (2*pi*x) + 0.1 * cos (7*x); %! Mdl = RegressionGP (x, y, 'Optimizer', 'lbfgs'); %! assert_equal (Mdl.ModelParameters.Optimizer, 'lbfgs'); %!test %! ## predict reproduces the R2024a values, and the standard deviation is %! ## that of a response, so it carries the noise %! x = linspace (0, 1, 20)'; %! y = sin (2*pi*x) + 0.1 * cos (7*x); %! Mdl = RegressionGP (x, y); %! xq = [0.05; 0.33; 0.5; 0.77; 0.95]; %! [yp, ysd, yint] = predict (Mdl, xq); %! assert_equal (yp, [0.404401855406521; 0.809355242891053; ... %! -0.093708552144171; -0.928420803239529; ... %! -0.217104024154071], 1e-7); %! assert_equal (ysd, [0.008117035802727; 0.007747238553279; ... %! 0.007727093294465; 0.007796728197045; ... %! 0.008117035802754], 1e-6); %! assert_equal (size (yint), [5, 2]); %! assert (all (yint(:,1) < yp)); %! assert (all (yint(:,2) > yp)); %!test %! ## The interval is the normal quantile of the level times the standard %! ## deviation, so a looser level gives a narrower interval %! x = linspace (0, 1, 20)'; %! y = sin (2*pi*x) + 0.1 * cos (7*x); %! Mdl = RegressionGP (x, y); %! xq = [0.05; 0.33; 0.5]; %! [yp, ysd, yint] = predict (Mdl, xq); %! assert_equal (yint(:,2) - yp, norminv (0.975) * ysd, 1e-12); %! [~, ~, yint90] = predict (Mdl, xq, 'Alpha', 0.10); %! assert_equal (yint90(:,2) - yp, norminv (0.95) * ysd, 1e-12); %! assert (all (yint90(:,2) - yint90(:,1) < yint(:,2) - yint(:,1))); %!test %! ## FitMethod 'none' keeps the documented initial values, estimates nothing %! ## and has no likelihood to report %! x = linspace (0, 1, 20)'; %! y = sin (2*pi*x) + 0.1 * cos (7*x); %! Mdl = RegressionGP (x, y, 'FitMethod', 'none'); %! assert_equal (Mdl.FitMethod, 'None'); %! assert_equal (Mdl.Sigma, std (y) / sqrt (2), 1e-14); %! assert_equal (Mdl.KernelInformation.KernelParameters, ... %! [mean(std (x)); std(y) / sqrt(2)], 1e-14); %! assert_equal (Mdl.Beta, 0); %! assert_equal (Mdl.LogLikelihood, []); %!test %! ## The parameters of each covariance function are named as MATLAB names %! ## them, and there is one length scale per predictor for the ARD kernels %! X = [linspace(0, 1, 15)', cos(linspace(0, 3, 15))']; %! y = X(:,1) .^ 2 + 0.3 * X(:,2); %! M1 = RegressionGP (X, y, 'KernelFunction', 'squaredexponential'); %! assert_equal (M1.KernelInformation.KernelParameterNames, ... %! {'SigmaL'; 'SigmaF'}); %! M2 = RegressionGP (X, y, 'KernelFunction', 'rationalquadratic'); %! assert_equal (M2.KernelInformation.KernelParameterNames, ... %! {'SigmaL'; 'AlphaRQ'; 'SigmaF'}); %! M3 = RegressionGP (X, y, 'KernelFunction', 'ardmatern32'); %! assert_equal (M3.KernelInformation.KernelParameterNames, ... %! {'LengthScale1'; 'LengthScale2'; 'SigmaF'}); %! assert_equal (numel (M3.KernelInformation.KernelParameters), 3); %!test %! ## The name of the covariance function is stored capitalized, as MATLAB %! ## stores it, whatever spelling the caller used %! x = linspace (0, 1, 12)'; %! y = cos (3*x); %! Mdl = RegressionGP (x, y, 'KernelFunction', 'ardmatern52'); %! assert_equal (Mdl.KernelFunction, 'ARDMatern52'); %! assert_equal (Mdl.KernelInformation.Name, 'ARDMatern52'); %!test %! ## The explicit basis has one coefficient per term, and none has none %! X = [linspace(0, 1, 15)', cos(linspace(0, 3, 15))']; %! y = X(:,1) .^ 2 + 0.3 * X(:,2); %! assert_equal (numel (RegressionGP (X, y, 'BasisFunction', 'none').Beta), 0); %! assert_equal (numel (RegressionGP (X, y, ... %! 'BasisFunction', 'constant').Beta), 1); %! assert_equal (numel (RegressionGP (X, y, ... %! 'BasisFunction', 'linear').Beta), 3); %! assert_equal (numel (RegressionGP (X, y, ... %! 'BasisFunction', 'pureQuadratic').Beta), 5); %!test %! ## Standardizing records the location and scale it used, and predict %! ## applies the same transformation %! X = [linspace(0, 10, 20)', linspace(-5, 5, 20)']; %! y = 0.3 * X(:,1) - 0.2 * X(:,2); %! Mdl = RegressionGP (X, y, 'Standardize', true); %! assert_equal (Mdl.PredictorLocation, mean (X, 1), 1e-14); %! assert_equal (Mdl.PredictorScale, std (X, 0, 1), 1e-14); %! assert_equal (predict (Mdl, X(1:3,:)), y(1:3), 1e-3); %!test %! ## Without standardizing there is no location or scale to report %! x = linspace (0, 1, 12)'; %! Mdl = RegressionGP (x, cos (3*x)); %! assert_equal (Mdl.PredictorLocation, []); %! assert_equal (Mdl.PredictorScale, []); %!test %! ## A held noise is not estimated %! x = linspace (0, 1, 15)'; %! y = cos (3*x) + 0.1 * sin (11*x); %! Mdl = RegressionGP (x, y, 'ConstantSigma', true, 'Sigma', 0.3); %! assert_equal (Mdl.Sigma, 0.3, 1e-14); %!test %! ## The noise cannot go below its lower bound %! x = linspace (0, 1, 15)'; %! y = cos (3*x); %! Mdl = RegressionGP (x, y, 'SigmaLowerBound', 0.05); %! assert (Mdl.Sigma >= 0.05); %!test %! ## The active set of an exactly fitted model is the whole training data %! x = linspace (0, 1, 15)'; %! Mdl = RegressionGP (x, cos (3*x)); %! assert_equal (Mdl.ActiveSetSize, 15); %! assert_equal (Mdl.ActiveSetVectors, x); %! assert_equal (Mdl.IsActiveSetVector, true (15, 1)); %! assert_equal (Mdl.ActiveSetMethod, 'Random'); %!test %! ## resubPredict is predict on the training data %! x = linspace (0, 1, 15)'; %! y = cos (3*x) + 0.1 * sin (11*x); %! Mdl = RegressionGP (x, y); %! assert_equal (resubPredict (Mdl), predict (Mdl, x), 1e-14); %!test %! ## The default loss is the mean squared error, and it is small for a model %! ## that interpolates its own training data closely %! x = linspace (0, 1, 20)'; %! y = sin (2*pi*x) + 0.1 * cos (7*x); %! Mdl = RegressionGP (x, y); %! assert_equal (loss (Mdl, x, y), mean ((y - predict (Mdl, x)) .^ 2), 1e-14); %! assert_equal (resubLoss (Mdl), loss (Mdl, x, y), 1e-14); %! assert (resubLoss (Mdl) < 1e-4); %!test %! ## The other loss functions, and weights %! x = linspace (0, 1, 15)'; %! y = cos (3*x) + 0.1 * sin (11*x); %! Mdl = RegressionGP (x, y); %! r = y - predict (Mdl, x); %! assert_equal (loss (Mdl, x, y, 'LossFun', 'mae'), mean (abs (r)), 1e-14); %! w = linspace (1, 2, 15)'; %! assert_equal (loss (Mdl, x, y, 'Weights', w), ... %! sum (w .* r .^ 2) / sum (w), 1e-14); %! mae = @(a, b) mean (abs (a - b)); %! assert_equal (loss (Mdl, x, y, 'LossFun', mae), mean (abs (r)), 1e-14); %!test %! ## The leave-one-out residuals and the effective parameter count match %! ## R2024a, and neither refits the model %! x = linspace (0, 1, 20)'; %! y = sin (2*pi*x) + 0.1 * cos (7*x); %! Mdl = RegressionGP (x, y); %! [loores, neff] = postFitStatistics (Mdl); %! assert_equal (size (loores), [20, 1]); %! assert_equal (loores(1:4), [0.006483807954955; -0.002405391043015; ... %! -0.000899188010473; 0.000975297024791], 1e-6); %! assert_equal (neff, 7.186830203018070, 1e-5); %!test %! ## The leave-one-out residuals are larger than the fitted residuals, since %! ## each leaves out the observation it is about %! x = linspace (0, 1, 20)'; %! y = sin (2*pi*x) + 0.1 * cos (7*x); %! Mdl = RegressionGP (x, y); %! loores = postFitStatistics (Mdl); %! assert (all (abs (loores) >= abs (y - predict (Mdl, x)) - 1e-12)); %!test %! ## compact keeps what predicts and drops the rest, and predicts the same %! x = linspace (0, 1, 15)'; %! y = cos (3*x) + 0.1 * sin (11*x); %! Mdl = RegressionGP (x, y); %! CMdl = compact (Mdl); %! assert_equal (class (CMdl), 'CompactRegressionGP'); %! assert_equal (predict (CMdl, x), predict (Mdl, x), 1e-14); %! [y1, s1] = predict (Mdl, x); %! [y2, s2] = predict (CMdl, x); %! assert_equal (s1, s2, 1e-14); %!test %! ## crossval returns a partitioned model over the observations trained on %! x = linspace (0, 1, 20)'; %! y = cos (3*x) + 0.1 * sin (11*x); %! Mdl = RegressionGP (x, y); %! CVMdl = crossval (Mdl, 'KFold', 4); %! assert_equal (class (CVMdl), 'RegressionPartitionedModel'); %! assert_equal (CVMdl.KFold, 4); %! assert_equal (CVMdl.CrossValidatedModel, 'GP'); %! assert_equal (numel (CVMdl.Trained), 4); %! assert_equal (class (CVMdl.Trained{1}), 'CompactRegressionGP'); %! assert_equal (size (kfoldPredict (CVMdl)), [20, 1]); %!test %! ## A model saved and loaded predicts what it predicted before %! x = linspace (0, 1, 15)'; %! y = cos (3*x) + 0.1 * sin (11*x); %! Mdl = RegressionGP (x, y); %! fname = tempname (); %! savemodel (Mdl, fname); %! Mdl2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (Mdl2), 'RegressionGP'); %! assert_equal (Mdl2.NumObservations, Mdl.NumObservations); %! assert_equal (Mdl2.Beta, Mdl.Beta); %! assert_equal (Mdl2.Sigma, Mdl.Sigma); %! assert_equal (Mdl2.KernelInformation.KernelParameters, ... %! Mdl.KernelInformation.KernelParameters); %! assert_equal (Mdl2.LogLikelihood, Mdl.LogLikelihood); %! assert_equal (predict (Mdl2, x), predict (Mdl, x), 1e-14); %!test %! ## A response transform is applied to the prediction and reported as text %! x = linspace (0, 1, 12)'; %! y = cos (3*x); %! Mdl = RegressionGP (x, y, 'ResponseTransform', 'exp'); %! assert_equal (Mdl.ResponseTransform, 'exp'); %! Mdl2 = RegressionGP (x, y); %! assert_equal (predict (Mdl, x), exp (predict (Mdl2, x)), 1e-12); %!test %! ## A row with a missing response is dropped, and RowsUsed says which %! x = linspace (0, 1, 12)'; %! y = cos (3*x); %! y(4) = NaN; %! Mdl = RegressionGP (x, y); %! assert_equal (Mdl.NumObservations, 11); %! assert_equal (Mdl.RowsUsed, [true(3,1); false; true(8,1)]); %!test %! ## With no missing value RowsUsed is empty, as it is for every learner %! x = linspace (0, 1, 12)'; %! Mdl = RegressionGP (x, cos (3*x)); %! assert_equal (Mdl.RowsUsed, []); %!test %! ## Observation weights default to equal shares summing to one %! x = linspace (0, 1, 12)'; %! Mdl = RegressionGP (x, cos (3*x)); %! assert_equal (Mdl.W, ones (12, 1) / 12); %!test %! ## A supplied covariance function is used, and reproduces the built-in one %! ## it imitates %! x = linspace (0, 1, 15)'; %! y = cos (3*x) + 0.1 * sin (11*x); %! kfcn = @(a, b, th) th(2)^2 * exp (-0.5 * ((a - b') / th(1)) .^ 2); %! M1 = RegressionGP (x, y, 'KernelFunction', kfcn, ... %! 'KernelParameters', [0.4; 1.2], 'FitMethod', 'none'); %! M2 = RegressionGP (x, y, 'KernelFunction', 'squaredexponential', ... %! 'KernelParameters', [0.4; 1.2], 'FitMethod', 'none'); %! assert_equal (predict (M1, x), predict (M2, x), 1e-12); %!test # A row missing a predictor counts, but is left out of the fit %! X = [(1:10)', mod((1:10)', 3)]; %! X(1,1) = NaN; %! Mdl = RegressionGP (X, (1:10)'); %! assert_equal (Mdl.NumObservations, 10); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (Mdl.ActiveSetSize, 9); %! assert_equal (Mdl.IsActiveSetVector, [false; true(9, 1)]); %! assert_equal (predict (Mdl, [NaN, 1]), 6); %! yFit = resubPredict (Mdl); %! assert_equal (yFit(1), 6); ## Test input validation for the constructor %!error RegressionGP (ones (5, 2)) %!error ... %! RegressionGP (ones (5, 2), ones (4, 1)) %!error ... %! RegressionGP ('a', ones (5, 1)) %!error ... %! RegressionGP (ones (5, 2), 'a') %!error ... %! RegressionGP (ones (5, 2), ones (5, 1), 'Standardize') %!error ... %! RegressionGP (ones (5, 2), ones (5, 1), 'bogus', 1) %!error ... %! RegressionGP (ones (5, 2), ones (5, 1), 'KernelFunction', 5) %!error ... %! RegressionGP (ones (5, 2), ones (5, 1), 'KernelFunction', 'bogus') %!error ... %! RegressionGP (ones (5, 2), ones (5, 1), 'KernelParameters', [-1, 2]) %!error ... %! RegressionGP (ones (5, 2), ones (5, 1), 'BasisFunction', 5) %!error ... %! RegressionGP (ones (5, 2), ones (5, 1), 'BasisFunction', 'bogus') %!error ... %! RegressionGP (ones (5, 2), ones (5, 1), 'Sigma', -1) %!error ... %! RegressionGP (ones (5, 2), ones (5, 1), 'ConstantSigma', 5) %!error ... %! RegressionGP (ones (5, 2), ones (5, 1), 'SigmaLowerBound', 0) %!error ... %! RegressionGP (ones (5, 2), ones (5, 1), 'FitMethod', 'fic') %!error ... %! RegressionGP (ones (5, 2), ones (5, 1), 'FitMethod', 'bogus') %!error ... %! RegressionGP (ones (5, 2), ones (5, 1), 'PredictMethod', 'bcd') %!error ... %! RegressionGP (ones (5, 2), ones (5, 1), 'Optimizer', 'fmincon') %!error ... %! RegressionGP (ones (5, 2), ones (5, 1), 'Optimizer', 'bogus') %!error ... %! RegressionGP (ones (5, 2), ones (5, 1), 'Standardize', 5) %!error ... %! RegressionGP (ones (5, 2), ones (5, 1), 'PredictorNames', {'a'}) %!error ... %! RegressionGP (ones (5, 2), ones (5, 1), 'ResponseName', 5) ## Test input validation for the predict method %!error ... %! predict (RegressionGP (ones (5, 2), ones (5, 1))) %!error ... %! predict (RegressionGP (ones (5, 2), ones (5, 1)), []) %!error ... %! predict (RegressionGP (ones (5, 2), ones (5, 1)), ones (3, 3)) %!error ... %! predict (RegressionGP (ones (5, 2), ones (5, 1)), ones (3, 2), 'Alpha', 2) %!error ... %! predict (RegressionGP (ones (5, 2), ones (5, 1)), ones (3, 2), 'bogus', 1) ## Test input validation for the loss method %!error ... %! loss (RegressionGP (ones (5, 2), ones (5, 1)), ones (3, 2)) %!error ... %! loss (RegressionGP (ones (5, 2), ones (5, 1)), ones (3, 2), ... %! ones (3, 1), 'LossFun', 'bogus') %!error ... %! loss (RegressionGP (ones (5, 2), ones (5, 1)), ones (3, 2), ... %! ones (3, 1), 'bogus', 1) ## The two MATLAB-compatibility properties are declared and stay empty, the ## exact method selecting no active set and using no block coordinate descent. %!test %! load fisheriris %! Mdl = fitrgp (meas(:,1:3), meas(:,4)); %! assert_equal (isempty (Mdl.ActiveSetHistory), true); %! assert_equal (isempty (Mdl.BCDInformation), true); %! fname = tempname (); %! unwind_protect %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! assert_equal (isempty (M2.ActiveSetHistory), true); %! assert_equal (isempty (M2.BCDInformation), true); %! unwind_protect_cleanup %! if (exist (fname, 'file')) %! delete (fname); %! endif %! end_unwind_protect ## Test input validation for the crossval method %!error ... %! crossval (RegressionGP (ones (10, 2), ones (10, 1)), 'KFold', 1) %!error ... %! crossval (RegressionGP (ones (10, 2), ones (10, 1)), 'Holdout', 2) %!error ... %! crossval (RegressionGP (ones (10, 2), ones (10, 1)), 'Leaveout', 1) %!error ... %! crossval (RegressionGP (ones (10, 2), ones (10, 1)), 'CVPartition', 1) %!error ... %! crossval (RegressionGP (ones (10, 2), ones (10, 1)), 'KFold', 3, ... %! 'Holdout', 0.2) %!error ... %! crossval (RegressionGP (ones (10, 2), ones (10, 1)), 'bogus', 1) ## HyperparameterOptimizationResults is declared for MATLAB compatibility and ## stays empty, this class running no search over its hyperparameters. %!test %! load fisheriris %! Mdl = fitrgp (meas(:,1:3), meas(:,4)); %! assert_equal (isempty (Mdl.HyperparameterOptimizationResults), true); ## ModelParameters records the fit as it was asked for. The field list and ## its order are MATLAB's, less the approximate-fitting machinery this class ## does not implement. %!test %! load fisheriris %! Mdl = fitrgp (meas(:,2:4), meas(:,1)); %! assert_equal (fieldnames (Mdl.ModelParameters)', {'KernelFunction', ... %! 'KernelParameters', 'BasisFunction', 'Beta', 'Sigma', 'FitMethod', ... %! 'PredictMethod', 'ActiveSetSize', 'ActiveSetMethod', 'Standardize', ... %! 'Optimizer', 'ConstantSigma', 'SigmaLowerBound', 'Version', 'Method', ... %! 'Type'}); %!test %! load fisheriris %! MP = fitrgp (meas(:,2:4), meas(:,1)).ModelParameters; %! assert_equal (MP.Beta, 0); %! assert_equal (isempty (MP.Sigma), true); %! assert_equal (isempty (MP.KernelParameters), true); %! assert_equal (MP.ActiveSetSize, 150); %! assert_equal (MP.ActiveSetMethod, 'Random'); ## The three starting values are what the caller gave, not what the fit ## found, which the Beta and Sigma properties carry instead. %!test %! load fisheriris %! Mdl = fitrgp (meas(:,2:4), meas(:,1), 'Beta', 3, 'Sigma', 0.7, ... %! 'KernelParameters', [2; 0.5]); %! assert_equal (Mdl.ModelParameters.Beta, 3); %! assert_equal (Mdl.ModelParameters.Sigma, 0.7); %! assert_equal (Mdl.ModelParameters.KernelParameters, [2; 0.5]); %! assert_equal (abs (Mdl.Beta - 3) > 1, true); %! assert_equal (abs (Mdl.Sigma - 0.7) > 0.1, true); ## Beta starts at a zero for every column the basis contributes, so a linear ## basis over three predictors starts at four of them. %!test %! load fisheriris %! MP = fitrgp (meas(:,2:4), meas(:,1), ... %! 'BasisFunction', 'linear').ModelParameters; %! assert_equal (MP.Beta, zeros (4, 1)); ## SigmaLowerBound is the one field reported as resolved rather than as ## given, MATLAB keeping it inside an Options structure this class has not. %!test %! load fisheriris %! MP = fitrgp (meas(:,2:4), meas(:,1)).ModelParameters; %! assert_equal (MP.SigmaLowerBound > 0, true); %! assert_equal (MP.Version, 1); %! assert_equal (MP.Method, 'GP'); %! assert_equal (MP.Type, 'regression'); ## Every documented response transform reaches the response that is reported. %!test %! load fisheriris %! Mdl = fitrgp (meas(:,2:4), meas(:,1)); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! T = {'identity', @(x) x; 'exp', @(x) exp (x); 'log', @(x) log (x)}; %! for i = 1:rows (T) %! Mdl.ResponseTransform = T{i,1}; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, T{i,2}(raw), 1e-12); %! endfor ## A function handle is taken as given and applied to the response. %!test %! load fisheriris %! Mdl = fitrgp (meas(:,2:4), meas(:,1)); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! Mdl.ResponseTransform = @(x) x .^ 2; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, raw .^ 2, 1e-12); %!shared Xc, Dc, yc %! c1 = repmat ([1; 2; 3], 20, 1); %! x2 = sin ((1:60)'); %! c3 = repmat ([10; 10; 20; 20], 15, 1); %! Xc = [c1, x2, c3]; %! Dc = [c1 == 1, c1 == 2, c1 == 3, x2, c3 == 10, c3 == 20]; %! yc = 5 * (c1 == 2) + 0.5 * x2 - 3 * (c3 == 20) + 0.1 * cos ((1:60)'); %!test # MATLAB parity: a categorical predictor is dummy coded in its place %! Mdl = RegressionGP (Xc, yc, 'CategoricalPredictors', [1, 3]); %! H = RegressionGP (Dc, yc); %! assert_equal (Mdl.ExpandedPredictorNames, {'x1 == 1', 'x1 == 2', ... %! 'x1 == 3', 'x2', 'x3 == 10', 'x3 == 20'}); %! assert_equal (size (Mdl.X), [60, 3]); %! assert_equal (predict (Mdl, Xc(1:5,:)), predict (H, Dc(1:5,:)), 1e-8); %!test # MATLAB parity: an ARD kernel has a length scale per coded column %! Mdl = RegressionGP (Xc, yc, 'CategoricalPredictors', [1, 3], ... %! 'KernelFunction', 'ardsquaredexponential', ... %! 'Standardize', true); %! assert_equal (numel (Mdl.KernelInformation.KernelParameters), 7); %! assert_equal (Mdl.PredictorLocation([1:3, 5:6]), zeros (1, 5)); %! assert_equal (Mdl.PredictorScale([1:3, 5:6]), ones (1, 5)); %!test # a level the training data did not hold predicts the lower median %! Mdl = RegressionGP (Xc, yc, 'CategoricalPredictors', [1, 3]); %! yhat = predict (Mdl, [4, 0, 10; 2, 0, 20]); %! ys = sort (yc); %! assert_equal (yhat(1), ys(ceil (numel (ys) / 2))); %! assert_equal (isnan (yhat(2)), false); %!test # cross-validation folds keep the categorical predictors %! Mdl = RegressionGP (Xc, yc, 'CategoricalPredictors', [1, 3]); %! CV = crossval (Mdl, 'KFold', 3); %! assert_equal (CV.Trained{1}.CategoricalPredictors, [1, 3]); %!error ... %! RegressionGP (Xc, yc, 'CategoricalPredictors', 4) ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,2:3); %! y = meas(:,1); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SW', 'PL'}); %! T.SL = y; %! Mdl = fitrgp (T, 'SL'); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'SL'), a); %! assert_equal (loss (Mdl, T), a); ## Observation weights of class single or double %!error ... %! RegressionGP ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', 'Weights', ... %! int8 ([1; 1; 1; 1])) %!error ... %! RegressionGP ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', 'Weights', true (4, 1)) %!error %! X = [1, 2; 3, 4; 5, 6; 7, 8]; %! y = (1:4)'; %! loss (RegressionGP (X, y), X, y, 'Weights', int8 ([1; 1; 1; 1])) %!test %! ## Single weights are stored single, summing to one %! load fisheriris %! X = meas(:,2:4); %! y = meas(:,1); %! w = 1 + (1:150)' / 7; %! Mdl = RegressionGP (X, y, 'Weights', single (w)); %! assert_equal (class (Mdl.W), 'single'); %! assert_equal (sum (double (Mdl.W)), 1, 1e-6); statistics-release-2.1.2/inst/Machine_Learning/RegressionKernel.m000066400000000000000000001641101527311104300251230ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftp {statistics} RegressionKernel ## ## Gaussian kernel regression model for large data. ## ## A @qcode{RegressionKernel} object maps the predictors into a randomized ## feature space whose inner product approximates a Gaussian kernel, and then ## fits a linear model there. A kernel regression is therefore as nonlinear ## as a support vector machine with a Gaussian kernel, while costing what a ## linear fit costs: nothing of size @math{NxN} is ever formed. ## ## The expansion is the random Fourier basis of Rahimi and Recht, drawn once ## when the model is fitted and kept with it, so @code{predict} maps new data ## through the same basis. MATLAB approximates the same kernel by the ## Fastfood construction, which reaches the same distribution more cheaply; ## the two are interchangeable in distribution but not draw by draw, and the ## draws come from different generators in any case, so the predictions of a ## model fitted here and one fitted in MATLAB differ even from the same seed. ## What does not differ is what they estimate. ## ## Like @qcode{RegressionLinear} the object holds no copy of the training ## data. It does hold the basis and the coefficients, so it is bounded by ## the number of expansion dimensions rather than by the number of ## observations. ## ## Create a @qcode{RegressionKernel} object with @code{fitrkernel}. ## ## @seealso{fitrkernel, RegressionLinear, RegressionSVM} ## @end deftp classdef RegressionKernel < PredictiveModel properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {RegressionKernel} {property} Epsilon ## ## Half the width of the epsilon-insensitive band ## ## A nonnegative scalar for a support vector machine, and empty for a ## least squares fit, which has no such band. It defaults to the ## interquartile range of the response over 13.49, an estimate of its ## standard deviation, or to @qcode{0.1} when that range is zero. This ## property is read-only. ## ## @end deftp Epsilon = []; ## -*- texinfo -*- ## @deftp {RegressionKernel} {property} BoxConstraint ## ## Box constraint of the support vector machine ## ## A positive scalar. It is the reciprocal of the product of ## @qcode{Lambda} and the number of observations, so setting either of ## the two in the constructor fixes the other, and giving both is an ## error. This property is read-only. ## ## @end deftp BoxConstraint = 1; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {RegressionKernel} {property} ResponseTransform ## ## Transformation applied to the predicted response ## ## A character vector, or the text of the function handle that was ## supplied. Assigning to it accepts either. ## ## @end deftp ResponseTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {RegressionKernel} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors with one name per column of the ## training data, defaulting to @qcode{'x1'}, @qcode{'x2'} and so on. ## This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {RegressionKernel} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A row vector of column indices, empty when every predictor is ## numeric. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {RegressionKernel} {property} ResponseName ## ## Name of the response ## ## A character vector, defaulting to @qcode{'Y'}. This property is ## read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {RegressionKernel} {property} ExpandedPredictorNames ## ## Names of the predictors as the fit saw them ## ## A cell array of character vectors. It equals @qcode{PredictorNames} ## unless categorical predictors were dummy coded, each then named once ## per level, as in @qcode{'x1 == 2'}. The expansion dimensions have no ## names. This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {RegressionKernel} {property} NumExpansionDimensions ## ## Number of dimensions of the expanded space ## ## A positive integer scalar. It defaults to ## @code{2 .^ ceil (min (log2 (@var{p}) + 5, 15))} for @var{p} ## predictors, so four predictors give 128 dimensions. More dimensions ## approximate the kernel more closely and cost proportionally more. ## This property is read-only. ## ## @end deftp NumExpansionDimensions = []; ## -*- texinfo -*- ## @deftp {RegressionKernel} {property} FittedLoss ## ## Loss function the fit minimized ## ## @qcode{'epsiloninsensitive'} for a support vector machine and ## @qcode{'mse'} for a least squares fit. This property is read-only. ## ## @end deftp FittedLoss = 'epsiloninsensitive'; ## -*- texinfo -*- ## @deftp {RegressionKernel} {property} Lambda ## ## Regularization strength ## ## A nonnegative scalar, the reciprocal of the product of ## @qcode{BoxConstraint} and the number of observations. This property ## is read-only. ## ## @end deftp Lambda = []; ## -*- texinfo -*- ## @deftp {RegressionKernel} {property} ModelParameters ## ## Fitting options, as they were given ## ## A structure holding every parameter of the fit, with the ## @qcode{'auto'} values as they were given rather than as they were ## resolved. This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {RegressionKernel} {property} Regularization ## ## Penalty on the coefficients ## ## Always @qcode{'ridge (L2)'}: a kernel model fits in the expanded ## space, where a lasso penalty has nothing to select. This property is ## read-only. ## ## @end deftp Regularization = 'ridge (L2)'; ## -*- texinfo -*- ## @deftp {RegressionKernel} {property} KernelScale ## ## Scale of the Gaussian kernel ## ## A positive scalar dividing every predictor before the expansion, so a ## larger scale makes the kernel wider and the fit smoother. This ## property is read-only. ## ## @end deftp KernelScale = 1; ## -*- texinfo -*- ## @deftp {RegressionKernel} {property} Learner ## ## Linear model fitted in the expanded space ## ## Either @qcode{'svm'} or @qcode{'leastsquares'}. This property is ## read-only. ## ## @end deftp Learner = 'svm'; ## -*- texinfo -*- ## @deftp {RegressionKernel} {property} Mu ## ## Predictor means used to standardize ## ## A row vector with one element per predictor, or empty when the model ## was fitted without standardizing. This property is read-only. ## ## @end deftp Mu = []; ## -*- texinfo -*- ## @deftp {RegressionKernel} {property} Sigma ## ## Predictor standard deviations used to standardize ## ## A row vector with one element per predictor, or empty when the model ## was fitted without standardizing. This property is read-only. ## ## @end deftp Sigma = []; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ## The callable behind ResponseTransform. RTfun = @(y) y; ## The random basis and the coefficients fitted in the space it spans. ## MATLAB keeps all three out of sight, so they are hidden here too, but ## they are the model: without the basis the coefficients index nothing. Basis_ = []; Beta_ = []; Bias_ = []; ## Number of predictors and of observations the fit saw, neither of them ## a property of MATLAB's class. NumPredictors_ = []; Coding_ = []; MissingResponse_ = NaN; NumObservations_ = []; ## What the fit reported, so that fitrkernel can hand it back as its ## second output. FitInfo_ = []; endproperties methods (Access = public) ## -*- texinfo -*- ## @deftypefn {RegressionKernel} {@var{obj} =} RegressionKernel (@var{X}, @var{Y}) ## @deftypefnx {RegressionKernel} {@var{obj} =} RegressionKernel (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {RegressionKernel} {@var{obj} =} RegressionKernel (@var{Tbl}, @var{formula}) ## @deftypefnx {RegressionKernel} {@var{obj} =} RegressionKernel (@var{Tbl}, @var{Y}) ## @deftypefnx {RegressionKernel} {@var{obj} =} RegressionKernel (@dots{}, @var{name}, @var{value}) ## ## Fit a Gaussian kernel regression model. ## ## @code{@var{obj} = RegressionKernel (@var{X}, @var{Y})} fits a support ## vector machine in a randomized Gaussian kernel space to the @math{NxP} ## predictor matrix @var{X} and the @math{Nx1} continuous response ## @var{Y}. ## ## @code{@var{obj} = RegressionKernel (@dots{}, @var{name}, @var{value})} ## takes the following @qcode{Name-Value} pairs. ## ## @multitable @columnfractions 0.28 0.72 ## @headitem Name @tab Value ## ## @item @qcode{'Learner'} @tab @qcode{'svm'}, the default, or ## @qcode{'leastsquares'}. ## ## @item @qcode{'Epsilon'} @tab Half the width of the insensitive band, ## a nonnegative scalar or @qcode{'auto'}, which is the interquartile ## range of @var{Y} over 13.49. It applies to a support vector machine ## alone. ## ## @item @qcode{'NumExpansionDimensions'} @tab @qcode{'auto'}, the ## default, or a positive integer. ## ## @item @qcode{'KernelScale'} @tab @qcode{1} by default, a positive ## scalar, or @qcode{'auto'}, which takes the median distance between the ## observations. ## ## @item @qcode{'Lambda'} @tab @qcode{'auto'}, the default, which is the ## reciprocal of the number of observations, or a nonnegative scalar. It ## cannot be given beside @qcode{'BoxConstraint'}. ## ## @item @qcode{'BoxConstraint'} @tab A positive scalar, @qcode{1} by ## default. It applies to a support vector machine alone. ## ## @item @qcode{'Standardize'} @tab Whether to centre and scale the ## predictors, false by default. ## ## @item @qcode{'BetaTolerance'} @tab Relative tolerance on the ## coefficients, @qcode{1e-4} by default. ## ## @item @qcode{'GradientTolerance'} @tab Absolute tolerance on the ## gradient's infinity norm, @qcode{1e-6} by default. ## ## @item @qcode{'IterationLimit'} @tab Largest number of iterations, ## @qcode{1000} by default. ## ## @item @qcode{'HessianHistorySize'} @tab Number of curvature pairs the ## solver keeps, @qcode{15} by default. ## ## @item @qcode{'BlockSize'} @tab Memory the expansion may occupy, in ## megabytes, @qcode{4e3} by default. ## ## @item @qcode{'ResponseTransform'} @tab A transformation applied to the ## predicted response, named or given as a function handle. ## ## @item @qcode{'Weights'} @tab One nonnegative weight per observation, as a ## single or double vector. Every computation runs in double. ## ## @item @qcode{'PredictorNames'} @tab One name per predictor. ## ## @item @qcode{'ResponseName'} @tab A name for the response. ## ## @item @qcode{'CategoricalPredictors'} @tab The categorical predictors, ## as indices, as a logical vector with one element per predictor, or as ## @qcode{'all'}. Each is dummy coded in its place, one column of zeros ## and ones per distinct value it takes in the training data, named as in ## @qcode{'x1 == 2'}, and the coded columns are not standardized. A row ## holding a value the training data did not is predicted as a row ## missing a predictor, the weighted lower median of the training response. ## A predictor may be named rather than indexed, as a character matrix of ## one padded name per row, a string array or a cellstr; a name must match ## an entry of @qcode{'PredictorNames'} exactly, its case included. ## @end multitable ## ## The fit is always by limited-memory BFGS, the only solver MATLAB ## offers a kernel model, and always under a ridge penalty. ## ## @seealso{fitrkernel, RegressionLinear} ## @end deftypefn function this = RegressionKernel (X, Y, varargin) if (nargin < 2) error ("RegressionKernel: too few input arguments."); endif ## A table names its own predictors and says which hold levels [this, X, Y, varargin] = resolveTable (this, 'RegressionKernel', ... X, Y, varargin); if (mod (numel (varargin), 2) != 0) error (strcat ("RegressionKernel: optional arguments must be", ... " given in Name-Value pairs.")); endif ## Parse optional paired arguments optNames = {'Learner', 'Epsilon', 'NumExpansionDimensions', ... 'KernelScale', 'Lambda', 'BoxConstraint', 'Standardize', ... 'BetaTolerance', 'GradientTolerance', 'IterationLimit', ... 'HessianHistorySize', 'BlockSize', 'Verbose', ... 'ResponseTransform', 'Weights', 'PredictorNames', ... 'ResponseName', 'CategoricalPredictors'}; ## An empty default stands for one resolved once the data are known: ## 'Epsilon' is 'auto', the interquartile range of the response over ## 13.49, 'Lambda' is 'auto', 1/n, and 'BoxConstraint' is 1, each ## empty so that giving it is told apart from leaving it out; ## 'Weights' are uniform and 'PredictorNames' are x1, x2, ... dfValues = {'svm', [], 'auto', 1, [], [], false, 1e-4, 1e-6, 1000, ... 15, 4e3, 0, 'none', [], {}, 'Y', []}; [Learner, EpsilonIn, NumDimsIn, KernelScaleIn, LambdaIn, ... BoxConstraint, Standardize, BetaTolerance, GradientTolerance, ... IterationLimit, HessianHistorySize, BlockSize, Verbose, ... ResponseTransform, Weights, PredictorNames, ResponseName, ... CategoricalPredictors, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (ischar (Learner) && any (strcmpi (Learner, {'svm', 'leastsquares'})))) error (strcat ("RegressionKernel: 'Learner' must be either 'svm'", ... " or 'leastsquares'.")); endif Learner = lower (Learner); if (! isempty (EpsilonIn) && ! ((ischar (EpsilonIn) && strcmpi (EpsilonIn, 'auto')) || (isnumeric (EpsilonIn) && isscalar (EpsilonIn) && isreal (EpsilonIn) && EpsilonIn >= 0))) error (strcat ("RegressionKernel: 'Epsilon' must be 'auto' or a", ... " nonnegative scalar.")); endif if (! ((ischar (NumDimsIn) && strcmpi (NumDimsIn, 'auto')) || (isnumeric (NumDimsIn) && isscalar (NumDimsIn) && isreal (NumDimsIn) && NumDimsIn > 0 && fix (NumDimsIn) == NumDimsIn))) error (strcat ("RegressionKernel: 'NumExpansionDimensions' must be", ... " 'auto' or a positive integer scalar.")); endif if (! ((ischar (KernelScaleIn) && strcmpi (KernelScaleIn, 'auto')) || (isnumeric (KernelScaleIn) && isscalar (KernelScaleIn) && isreal (KernelScaleIn) && KernelScaleIn > 0))) error (strcat ("RegressionKernel: 'KernelScale' must be 'auto' or", ... " a positive scalar.")); endif if (! isempty (LambdaIn) && ! ((ischar (LambdaIn) && strcmpi (LambdaIn, 'auto')) || (isnumeric (LambdaIn) && isscalar (LambdaIn) && isreal (LambdaIn) && LambdaIn >= 0 && isfinite (LambdaIn)))) error (strcat ("RegressionKernel: 'Lambda' must be 'auto' or a", ... " nonnegative finite scalar.")); endif if (! isempty (BoxConstraint) && ! (isnumeric (BoxConstraint) && isscalar (BoxConstraint) && isreal (BoxConstraint) && BoxConstraint > 0 && isfinite (BoxConstraint))) error (strcat ("RegressionKernel: 'BoxConstraint' must be a", ... " positive finite scalar.")); endif if (! (islogical (Standardize) || (isnumeric (Standardize) && isscalar (Standardize) && any (Standardize == [0, 1])))) error ("RegressionKernel: 'Standardize' must be either true or false."); endif Standardize = logical (Standardize); if (! (isnumeric (BetaTolerance) && isscalar (BetaTolerance) && isreal (BetaTolerance) && BetaTolerance >= 0)) error (strcat ("RegressionKernel: 'BetaTolerance' must be a", ... " nonnegative scalar.")); endif if (! (isnumeric (GradientTolerance) && isscalar (GradientTolerance) && isreal (GradientTolerance) && GradientTolerance >= 0)) error (strcat ("RegressionKernel: 'GradientTolerance' must be a", ... " nonnegative scalar.")); endif if (! (isnumeric (IterationLimit) && isscalar (IterationLimit) && isreal (IterationLimit) && IterationLimit > 0 && fix (IterationLimit) == IterationLimit)) error (strcat ("RegressionKernel: 'IterationLimit' must be a", ... " positive integer scalar.")); endif if (! (isnumeric (HessianHistorySize) && isscalar (HessianHistorySize) && isreal (HessianHistorySize) && HessianHistorySize > 0 && fix (HessianHistorySize) == HessianHistorySize)) error (strcat ("RegressionKernel: 'HessianHistorySize' must be a", ... " positive integer scalar.")); endif if (! (isnumeric (BlockSize) && isscalar (BlockSize) && isreal (BlockSize) && BlockSize > 0)) error ("RegressionKernel: 'BlockSize' must be a positive scalar."); endif if (! (isnumeric (Verbose) && isscalar (Verbose) && isreal (Verbose) && any (Verbose == [0, 1]))) error ("RegressionKernel: 'Verbose' must be 0 or 1."); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("RegressionKernel: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isreal (Weights) && isvector (Weights) && all (Weights >= 0))) error (strcat ("RegressionKernel: 'Weights' must be a vector of", ... " nonnegative values.")); endif if (! isempty (PredictorNames) && ! (iscellstr (PredictorNames) && isvector (PredictorNames))) error (strcat ("RegressionKernel: 'PredictorNames' must be a cell", ... " array of character vectors.")); endif if (! (ischar (ResponseName) && isrow (ResponseName))) error ("RegressionKernel: 'ResponseName' must be a character vector."); endif if (! isempty (args)) error ("RegressionKernel: invalid optional paired argument."); endif if (! isempty (LambdaIn) && ! isempty (BoxConstraint)) error (strcat ("RegressionKernel: 'Lambda' and 'BoxConstraint'", ... " cannot be given together, one being the", ... " reciprocal of the other times the number of", ... " observations.")); endif if (! isempty (BoxConstraint) && strcmp (Learner, 'leastsquares')) error (strcat ("RegressionKernel: 'BoxConstraint' applies to a", ... " support vector machine only.")); endif ## Validate the data and resolve the weights. The four linear and ## kernel regression models share that opening, so it lives in one ## place. F = regFrame (X, Y, Weights, 'RegressionKernel'); X = F.X; Y = F.Y; W = F.W; n = F.n; p = F.p; ## Dummy code the categorical predictors on the rows kept; the fit ## sees the coded columns, and new data is coded the same way. if (isempty (PredictorNames)) PredictorNames = arrayfun (@(k) sprintf ("x%d", k), 1:p, ... 'UniformOutput', false); elseif (numel (PredictorNames) != p) error (strcat ("RegressionKernel: 'PredictorNames' must have one", ... " name per predictor.")); endif [Coding, errmsg] = dummyCoding (X, CategoricalPredictors, ... PredictorNames); if (! isempty (errmsg)) error ("RegressionKernel: %s", errmsg); endif X = dummyCoding (X, Coding); p = columns (X); ## What a row missing a predictor is predicted to be, as MATLAB R2024a ## predicts it: the weighted lower median of the training response. this.MissingResponse_ = missingResponse (Y, W); ## Epsilon belongs to the insensitive band, so it means nothing to a ## least squares fit and is refused there rather than ignored. if (strcmp (Learner, 'leastsquares')) if (! isempty (EpsilonIn)) error (strcat ("RegressionKernel: 'Epsilon' applies to a", ... " support vector machine only.")); endif Epsilon = []; elseif (isempty (EpsilonIn) || ischar (EpsilonIn)) r = iqr (Y); if (r == 0) Epsilon = 0.1; else Epsilon = r / 13.49; endif else Epsilon = EpsilonIn; endif ## Standardize before anything is measured off the predictors if (Standardize) this.Mu = mean (X, 1); this.Sigma = std (X, 0, 1); this.Sigma(this.Sigma == 0) = 1; ## A level's column is left as it is, as in MATLAB R2024a. this.Mu(Coding.Dummy) = 0; this.Sigma(Coding.Dummy) = 1; X = (X - this.Mu) ./ this.Sigma; endif ## Resolve the expansion if (ischar (NumDimsIn)) m = 2 .^ ceil (min (log2 (p) + 5, 15)); else m = NumDimsIn; endif if (ischar (KernelScaleIn)) sigma = autoKernelScale (X); else sigma = KernelScaleIn; endif ## Resolve Lambda and the box constraint from whichever was given if (! isempty (BoxConstraint)) Lambda = 1 / (n * BoxConstraint); elseif (! isempty (LambdaIn) && ! ischar (LambdaIn)) Lambda = LambdaIn; BoxConstraint = 1 / (n * Lambda); else Lambda = 1 / n; BoxConstraint = 1 / (n * Lambda); endif if (isempty (LambdaIn)) LambdaIn = 'auto'; endif if (isempty (EpsilonIn)) EpsilonIn = 'auto'; endif ## Draw the basis, map the data through it, and fit a linear model ## there. basis = kernelBasis (p, m, sigma); T = kernelExpand (X, basis); P = struct (); P.Learner = Learner; P.LossFunction = 'epsiloninsensitive'; if (strcmp (Learner, 'leastsquares')) P.LossFunction = 'mse'; endif P.Epsilon = Epsilon; P.Regularization = 'ridge'; P.Lambda = Lambda; P.Solver = 'lbfgs'; P.FitBias = true; P.PostFitBias = false; P.BetaTolerance = BetaTolerance; P.GradientTolerance = GradientTolerance; P.DeltaGradientTolerance = []; P.IterationLimit = IterationLimit; P.PassLimit = 1; P.BatchSize = 10; P.BatchLimit = []; P.LearnRate = 1; P.OptimizeLearnRate = true; P.TruncationPeriod = 10; P.NumCheckConvergence = 5; P.HessianHistorySize = HessianHistorySize; P.InitialBeta = zeros (m, 1); P.InitialBias = sum (W .* Y); [Beta, Bias, S] = linearSolve (T, Y, W, P); ## Fill in the model this.Epsilon = Epsilon; this.BoxConstraint = BoxConstraint; this.PredictorNames = PredictorNames; this.ExpandedPredictorNames = PredictorNames; if (! isempty (Coding.Index)) this.ExpandedPredictorNames = Coding.ExpandedNames; this.CategoricalPredictors = Coding.Index; this.Coding_ = Coding; endif this.ResponseName = ResponseName; this.NumExpansionDimensions = m; this.FittedLoss = P.LossFunction; this.Lambda = Lambda; this.KernelScale = sigma; this.Learner = Learner; this.ResponseTransform = ResponseTransform; this.Basis_ = basis; this.Beta_ = Beta; this.Bias_ = Bias; this.NumPredictors_ = Coding.NumPredictors; this.NumObservations_ = n; this.ModelParameters = struct ('BetaTolerance', BetaTolerance, ... 'BlockSize', BlockSize, 'BoxConstraint', BoxConstraint, ... 'Epsilon', EpsilonIn, 'NumExpansionDimensions', NumDimsIn, ... 'GradientTolerance', GradientTolerance, ... 'HessianHistorySize', HessianHistorySize, ... 'IterationLimit', IterationLimit, 'KernelScale', KernelScaleIn, ... 'Lambda', LambdaIn, 'Learner', Learner, ... 'LossFunction', P.LossFunction, 'Stream', [], ... 'VerbosityLevel', Verbose, 'StandardizeData', Standardize, ... 'Version', 1, 'Method', 'Kernel', 'Type', 'regression'); this.FitInfo_ = kernelFitInfo (S, P.LossFunction, Lambda, ... BetaTolerance, GradientTolerance); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {RegressionKernel} {@var{yFit} =} predict (@var{obj}, @var{XC}) ## ## Predict the response of new observations. ## ## @code{@var{yFit} = predict (@var{obj}, @var{XC})} maps each row of ## @var{XC} through the model's own random basis and returns the ## predicted response, with @qcode{ResponseTransform} applied. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @end deftypefn function yFit = predict (this, XC) if (nargin < 2) error ("RegressionKernel.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'RegressionKernel.predict', XC); if (isempty (XC)) error ("RegressionKernel.predict: XC is empty."); endif if (! (isnumeric (XC) && isreal (XC) && ismatrix (XC))) error ("RegressionKernel.predict: invalid values in XC."); endif if (columns (XC) != this.NumPredictors_) error (strcat ("RegressionKernel.predict: XC must have the same", ... " number of predictors as the trained model.")); endif if (! isempty (this.Coding_)) XC = dummyCoding (XC, this.Coding_); endif if (! isempty (this.Mu)) XC = (XC - this.Mu) ./ this.Sigma; endif f = kernelExpand (XC, this.Basis_) * this.Beta_ + this.Bias_; f(any (isnan (XC), 2)) = this.MissingResponse_; yFit = this.RTfun (f); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionKernel} {@var{l} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {RegressionKernel} {@var{l} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {RegressionKernel} {@var{l} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {RegressionKernel} {@var{l} =} loss (@dots{}, @var{name}, @var{value}) ## ## Regression loss on new data. ## ## @code{@var{l} = loss (@var{obj}, @var{X}, @var{Y})} returns the mean ## squared error. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{l} = loss (@dots{}, @var{name}, @var{value})} takes ## @qcode{'LossFun'}, either @qcode{'mse'} or ## @qcode{'epsiloninsensitive'}, and @qcode{'Weights'}. The ## epsilon-insensitive loss needs a band to be insensitive within, so it ## is offered by a support vector machine alone. ## ## @end deftypefn function l = loss (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("RegressionKernel.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("RegressionKernel.loss: optional arguments must", ... " be given in Name-Value pairs.")); endif ## Parse optional paired arguments optNames = {'LossFun', 'Weights'}; ## An empty 'Weights' stands for uniform weights dfValues = {'mse', []}; [LossFun, Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (ischar (LossFun) && any (strcmpi (LossFun, ... {'mse', 'epsiloninsensitive'})))) error (strcat ("RegressionKernel.loss: 'LossFun' must be", ... " either 'mse' or 'epsiloninsensitive'.")); endif LossFun = lower (LossFun); errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("RegressionKernel.loss: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isreal (Weights) && isvector (Weights) && all (Weights >= 0))) error (strcat ("RegressionKernel.loss: 'Weights' must be a", ... " vector of nonnegative values.")); endif if (! isempty (args)) error ("RegressionKernel.loss: invalid optional paired argument."); endif if (strcmp (LossFun, 'epsiloninsensitive') && isempty (this.Epsilon)) error (strcat ("RegressionKernel.loss: the", ... " 'epsiloninsensitive' loss applies to a support", ... " vector machine only.")); endif Y = Y(:); if (! (isnumeric (Y) && isreal (Y))) error ("RegressionKernel.loss: invalid values in Y."); endif if (rows (X) != numel (Y)) error (strcat ("RegressionKernel.loss: number of rows in X and Y", ... " must be equal.")); endif if (isempty (Weights)) w = ones (numel (Y), 1); else w = double (Weights(:)); if (numel (w) != numel (Y)) error (strcat ("RegressionKernel.loss: 'Weights' must have one", ... " element per observation.")); endif endif w = w / sum (w); r = Y - predict (this, X); if (strcmp (LossFun, 'mse')) l = sum (w .* (r .^ 2)); else l = sum (w .* max (0, abs (r) - this.Epsilon)); endif endfunction ## -*- texinfo -*- ## @deftypefn {RegressionKernel} {@var{obj} =} resume (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {RegressionKernel} {@var{obj} =} resume (@dots{}, @var{name}, @var{value}) ## @deftypefnx {RegressionKernel} {@var{obj} =} resume (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## ## Continue fitting a kernel regression model. ## ## @code{@var{obj} = resume (@var{obj}, @var{X}, @var{Y})} restarts the ## optimization from the coefficients the model already carries, through ## the basis it already holds. It takes @qcode{'BetaTolerance'}, ## @qcode{'GradientTolerance'} and @qcode{'IterationLimit'}, each ## defaulting to what the model was fitted with, and @qcode{'Weights'}. ## ## @var{X} and @var{Y} must be the data the model was fitted to; the ## object keeps no copy of them, which is what makes it small. Neither ## does it keep the observation weights, so a model fitted with ## @qcode{'Weights'} must be given them again here or it will resume ## against uniform ones. MATLAB behaves the same way: measured on ## R2024a, resuming a weighted fit without passing the weights back ## reaches the objective of the @emph{unweighted} fit. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## The response is then named by @var{ResponseVarName}, a variable of the ## table, or given beside the table as @var{Y}; unlike ## @code{RegressionKernel.loss}, it is never taken from the table ## unasked. ## ## @end deftypefn function this = resume (this, X, Y, varargin) if (nargin < 3) error ("RegressionKernel.resume: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error (strcat ("RegressionKernel.resume: optional arguments must", ... " be given in Name-Value pairs.")); endif ## Parse optional paired arguments optNames = {'Weights', 'BetaTolerance', 'GradientTolerance', ... 'IterationLimit'}; dfValues = {[], this.ModelParameters.BetaTolerance, ... this.ModelParameters.GradientTolerance, ... this.ModelParameters.IterationLimit}; [Weights, BetaTolerance, GradientTolerance, IterationLimit, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("RegressionKernel.resume: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isreal (Weights) && isvector (Weights) && all (Weights >= 0))) error (strcat ("RegressionKernel.resume: 'Weights' must", ... " be a vector of nonnegative values.")); endif if (! (isnumeric (BetaTolerance) && isscalar (BetaTolerance) && isreal (BetaTolerance) && BetaTolerance >= 0)) error (strcat ("RegressionKernel.resume: 'BetaTolerance'", ... " must be a nonnegative scalar.")); endif if (! (isnumeric (GradientTolerance) && isscalar (GradientTolerance) && isreal (GradientTolerance) && GradientTolerance >= 0)) error (strcat ("RegressionKernel.resume: 'GradientTolerance'", ... " must be a nonnegative scalar.")); endif if (! (isnumeric (IterationLimit) && isscalar (IterationLimit) && isreal (IterationLimit) && IterationLimit > 0 && fix (IterationLimit) == IterationLimit)) error (strcat ("RegressionKernel.resume: 'IterationLimit'", ... " must be a positive integer scalar.")); endif ## Handle table input if (istable (X)) [X, Y] = tableResponse (this, 'resume', X, Y, {}, true); endif if (! isempty (args)) error ("RegressionKernel.resume: invalid optional paired argument."); endif if (! (isnumeric (X) && isreal (X) && ismatrix (X))) error ("RegressionKernel.resume: invalid values in X."); endif if (columns (X) != this.NumPredictors_) error (strcat ("RegressionKernel.resume: X must have the same", ... " number of predictors as the trained model.")); endif Y = Y(:); if (! (isnumeric (Y) && isreal (Y))) error ("RegressionKernel.resume: invalid values in Y."); endif if (rows (X) != numel (Y)) error (strcat ("RegressionKernel.resume: number of rows in X and", ... " Y must be equal.")); endif if (! isempty (this.Coding_)) X = dummyCoding (X, this.Coding_); endif if (! isempty (this.Mu)) X = (X - this.Mu) ./ this.Sigma; endif T = kernelExpand (X, this.Basis_); if (isempty (Weights)) W = ones (numel (Y), 1); else W = double (Weights(:)); if (numel (W) != numel (Y)) error (strcat ("RegressionKernel.resume: 'Weights' must have", ... " one element per observation.")); endif endif W = W / sum (W); P = struct (); P.Learner = this.Learner; P.LossFunction = this.FittedLoss; P.Epsilon = this.Epsilon; P.Regularization = 'ridge'; P.Lambda = this.Lambda; P.Solver = 'lbfgs'; P.FitBias = true; P.PostFitBias = false; P.BetaTolerance = BetaTolerance; P.GradientTolerance = GradientTolerance; P.DeltaGradientTolerance = []; P.IterationLimit = IterationLimit; P.PassLimit = 1; P.BatchSize = 10; P.BatchLimit = []; P.LearnRate = 1; P.OptimizeLearnRate = true; P.TruncationPeriod = 10; P.NumCheckConvergence = 5; P.HessianHistorySize = this.ModelParameters.HessianHistorySize; P.InitialBeta = this.Beta_; P.InitialBias = this.Bias_; [Beta, Bias, S] = linearSolve (T, Y, W, P); this.Beta_ = Beta; this.Bias_ = Bias; this.ModelParameters.BetaTolerance = BetaTolerance; this.ModelParameters.GradientTolerance = GradientTolerance; this.ModelParameters.IterationLimit = IterationLimit; this.FitInfo_ = kernelFitInfo (S, this.FittedLoss, this.Lambda, ... BetaTolerance, GradientTolerance); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionKernel} {} savemodel (@var{obj}, @var{filename}) ## ## Save a kernel regression model to a file. ## ## @code{savemodel (@var{obj}, @var{filename})} saves the model ## @var{obj} into @var{filename} in a form @code{loadmodel} can read ## back, the random basis included. ## ## @end deftypefn function savemodel (obj, fname) classdef_name = 'RegressionKernel'; Epsilon = obj.Epsilon; BoxConstraint = obj.BoxConstraint; ResponseTransform = obj.ResponseTransform; PredictorNames = obj.PredictorNames; CategoricalPredictors = obj.CategoricalPredictors; ResponseName = obj.ResponseName; ExpandedPredictorNames = obj.ExpandedPredictorNames; NumExpansionDimensions = obj.NumExpansionDimensions; FittedLoss = obj.FittedLoss; Lambda = obj.Lambda; ModelParameters = obj.ModelParameters; Regularization = obj.Regularization; KernelScale = obj.KernelScale; Learner = obj.Learner; Mu = obj.Mu; Sigma = obj.Sigma; Basis_ = obj.Basis_; Beta_ = obj.Beta_; Bias_ = obj.Bias_; NumPredictors_ = obj.NumPredictors_; NumObservations_ = obj.NumObservations_; Coding_ = obj.Coding_; MissingResponse_ = obj.MissingResponse_; save ('-binary', fname, 'classdef_name', 'Epsilon', ... 'BoxConstraint', 'ResponseTransform', 'PredictorNames', ... 'CategoricalPredictors', 'ResponseName', ... 'ExpandedPredictorNames', 'NumExpansionDimensions', ... 'FittedLoss', 'Lambda', 'ModelParameters', 'Regularization', ... 'KernelScale', 'Learner', 'Mu', 'Sigma', 'Basis_', 'Beta_', ... 'Bias_', 'NumPredictors_', 'NumObservations_', 'Coding_', ... 'MissingResponse_'); endfunction endmethods methods (Access = public, Hidden) function disp (this) printf ("\n RegressionKernel\n\n"); printf ("%+26s: '%s'\n", 'ResponseName', this.ResponseName); printf ("%+26s: '%s'\n", 'Learner', this.Learner); printf ("%+26s: %d\n", 'NumExpansionDimensions', ... this.NumExpansionDimensions); printf ("%+26s: %g\n", 'KernelScale', this.KernelScale); printf ("%+26s: %g\n", 'Lambda', this.Lambda); printf ("%+26s: %g\n", 'BoxConstraint', this.BoxConstraint); if (! isempty (this.Epsilon)) printf ("%+26s: %g\n", 'Epsilon', this.Epsilon); endif printf ("\n"); endfunction ## What the fit reported, which fitrkernel returns as its second output. function S = fitInfo_ (this) S = this.FitInfo_; endfunction ## Custom setter, so that assigning a name or a handle updates both the ## text the property reports and the callable predict uses. function this = set.ResponseTransform (this, val) [this.RTfun, this.ResponseTransform] = ... parseResponseTransform (val, 'RegressionKernel'); endfunction endmethods methods (Static, Hidden) function mdl = load_model (filename, data) mdl = RegressionKernel ([0; 1], [0; 1]); fields = fieldnames (data); for k = 1:numel (fields) mdl.(fields{k}) = data.(fields{k}); endfor endfunction endmethods endclassdef %!demo %! ## Fit fuel consumption through a randomized Gaussian kernel, which %! ## bends where a linear model cannot. %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! Mdl = RegressionKernel (X(ok,:), MPG(ok)) %! yFit = predict (Mdl, X(find (ok, 3),:)) %!demo %! ## Standardizing matters more here than for a linear fit, since the %! ## kernel measures one distance across predictors of every scale. %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! plain = RegressionKernel (X(ok,:), MPG(ok)); %! scaled = RegressionKernel (X(ok,:), MPG(ok), 'Standardize', true); %! plainLoss = loss (plain, X(ok,:), MPG(ok)) %! scaledLoss = loss (scaled, X(ok,:), MPG(ok)) %!shared X, Y %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! X = X(ok,:); %! Y = MPG(ok); %!test %! ## The model reports the surface MATLAB reports %! Mdl = RegressionKernel (X, Y); %! assert_equal (class (Mdl), 'RegressionKernel'); %! assert_equal (Mdl.Learner, 'svm'); %! assert_equal (Mdl.FittedLoss, 'epsiloninsensitive'); %! assert_equal (Mdl.Regularization, 'ridge (L2)'); %! assert_equal (Mdl.ResponseTransform, 'none'); %! assert_equal (Mdl.KernelScale, 1); %! assert_equal (Mdl.BoxConstraint, 1); %! assert_equal (Mdl.NumExpansionDimensions, 128); %! assert_equal (Mdl.Mu, []); %! assert_equal (Mdl.Sigma, []); %! assert_equal (Mdl.PredictorNames, {'x1', 'x2', 'x3', 'x4'}); %!test %! ## The properties are the ones MATLAB lists, in its order %! Mdl = RegressionKernel (X, Y); %! assert_equal (sort (properties (Mdl)), ... %! sort ({'Epsilon'; 'BoxConstraint'; 'ResponseTransform'; ... %! 'PredictorNames'; 'CategoricalPredictors'; ... %! 'ResponseName'; 'ExpandedPredictorNames'; ... %! 'NumExpansionDimensions'; 'FittedLoss'; 'Lambda'; ... %! 'ModelParameters'; 'Regularization'; 'KernelScale'; ... %! 'Learner'; 'Mu'; 'Sigma'})); %!test %! ## Epsilon defaults to the interquartile range over 13.49, as it does for %! ## the linear model %! Mdl = RegressionKernel (X, Y); %! assert_equal (Mdl.Epsilon, 0.926612305411416, 1e-12); %! assert_equal (Mdl.Epsilon, iqr (Y) / 13.49, 1e-15); %!test %! ## Least squares has no insensitive band at all %! Mdl = RegressionKernel (X, Y, 'Learner', 'leastsquares'); %! assert_equal (Mdl.Epsilon, []); %! assert_equal (Mdl.FittedLoss, 'mse'); %!test %! ## Lambda and the box constraint are reciprocal through the number of %! ## observations, and either one may be the one that is given %! Mb = RegressionKernel (X, Y, 'BoxConstraint', 4); %! assert_equal (Mb.Lambda, 1 / (93 * 4), 1e-15); %! Ml = RegressionKernel (X, Y, 'Lambda', 0.02); %! assert_equal (Ml.BoxConstraint, 1 / (93 * 0.02), 1e-12); %!test %! ## Lambda defaults to the reciprocal of the observations that were used %! Mdl = RegressionKernel (X, Y); %! assert_equal (Mdl.Lambda, 1 / 93, 1e-15); %!test %! ## The default expansion is MATLAB's, two to the power of five more than %! ## the base two logarithm of the predictors %! assert_equal (RegressionKernel (X(:,1:2), Y).NumExpansionDimensions, 64); %! assert_equal (RegressionKernel (X, Y, ... %! 'NumExpansionDimensions', 50).NumExpansionDimensions, 50); %!test %! ## An odd expansion cannot be paired throughout, and the dimension left %! ## over still comes back as a dimension %! Mdl = RegressionKernel (X, Y, 'NumExpansionDimensions', 51); %! assert_equal (Mdl.NumExpansionDimensions, 51); %! assert_equal (numel (predict (Mdl, X)), 93); %!test %! ## Standardizing records the means and deviations of the predictors %! Mdl = RegressionKernel (X, Y, 'Standardize', true); %! assert_equal (Mdl.Mu, mean (X), 1e-12); %! assert_equal (Mdl.Sigma, std (X), 1e-12); %!test %! ## A kernel fit follows the response it was given %! Mdl = RegressionKernel (X, Y, 'Learner', 'leastsquares', ... %! 'Standardize', true, 'Lambda', 1e-4); %! assert_equal (loss (Mdl, X, Y) < var (Y), true); %! assert_equal (corr (predict (Mdl, X), Y) > 0.5, true); %!test %! ## Predicting through the model's own basis gives the same answer every %! ## time it is asked %! Mdl = RegressionKernel (X, Y); %! assert_equal (predict (Mdl, X(1:10,:)), predict (Mdl, X(1:10,:))); %!test %! ## resume continues from the coefficients the model already holds, so it %! ## cannot leave the objective higher than it found it %! Mdl = RegressionKernel (X, Y, 'IterationLimit', 3); %! before = Mdl.FitInfo_.ObjectiveValue; %! Mdl = resume (Mdl, X, Y, 'IterationLimit', 500); %! assert_equal (class (Mdl), 'RegressionKernel'); %! assert_equal (Mdl.FitInfo_.ObjectiveValue <= before, true); %! assert_equal (Mdl.ModelParameters.IterationLimit, 500); %!test %! ## The fit information is MATLAB's kernel structure %! Mdl = RegressionKernel (X, Y); %! F = Mdl.FitInfo_; %! assert_equal (fieldnames (F), {'Solver'; 'LossFunction'; 'Lambda'; ... %! 'BetaTolerance'; 'GradientTolerance'; ... %! 'ObjectiveValue'; 'GradientMagnitude'; ... %! 'RelativeChangeInBeta'; 'FitTime'; ... %! 'History'}); %! assert_equal (F.Solver, 'LBFGS-fast'); %! assert_equal (F.LossFunction, 'epsiloninsensitive'); %!test %! ## A response transform reaches predict %! Mdl = RegressionKernel (X, Y, 'Learner', 'leastsquares'); %! plain = predict (Mdl, X(1:5,:)); %! Mdl.ResponseTransform = 'exp'; %! assert_equal (predict (Mdl, X(1:5,:)), exp (plain), 1e-12); %!test %! ## A row with a missing predictor or a missing response is dropped %! Xn = X; %! Xn(3,2) = NaN; %! Mdl = RegressionKernel (Xn, Y); %! assert_equal (Mdl.Lambda, 1 / 92, 1e-15); %!test %! ## A saved model reads back as the same model, the random basis included %! Mdl = RegressionKernel (X, Y); %! fname = tempname (); %! savemodel (Mdl, fname); %! Mnew = loadmodel (fname); %! delete (fname); %! assert_equal (class (Mnew), 'RegressionKernel'); %! assert_equal (predict (Mnew, X(1:5,:)), predict (Mdl, X(1:5,:))); %!test %! ## resume keeps no observation weights, because the model keeps no data: %! ## passing them back restores the weighted fit, and omitting them %! ## continues against uniform ones %! Mdl = RegressionKernel (X, Y, 'IterationLimit', 5, 'Weights', (1:93)'); %! kept = resume (Mdl, X, Y, 'IterationLimit', 400, 'Weights', (1:93)'); %! lost = resume (Mdl, X, Y, 'IterationLimit', 400); %! assert_equal (kept.FitInfo_.ObjectiveValue ... %! != lost.FitInfo_.ObjectiveValue, true); %!test # A row missing a predictor predicts the lower median of the response %! X = [(1:10)', mod((1:10)', 3)]; %! Mdl = RegressionKernel (X, (1:10)'); %! assert_equal (predict (Mdl, [NaN, 1]), 5); ## Test input validation %!error RegressionKernel (ones (5, 2)) %!error ... %! RegressionKernel (ones (10, 2), ones (10, 1), 'Learner') %!error ... %! RegressionKernel (ones (10, 2), ones (10, 1), 'Learner', 'logistic') %!error ... %! RegressionKernel (ones (10, 2), ones (10, 1), 'Epsilon', -1) %!error ... %! RegressionKernel (ones (10, 2), ones (10, 1), 'Learner', 'leastsquares', ... %! 'Epsilon', 1) %!error ... %! RegressionKernel (ones (10, 2), ones (10, 1), 'NumExpansionDimensions', 2.5) %!error ... %! RegressionKernel (ones (10, 2), ones (10, 1), 'KernelScale', 0) %!error ... %! RegressionKernel (ones (10, 2), ones (10, 1), 'Lambda', Inf) %!error ... %! RegressionKernel (ones (10, 2), ones (10, 1), 'BoxConstraint', -2) %!error ... %! RegressionKernel (ones (10, 2), ones (10, 1), 'Lambda', 0.1, ... %! 'BoxConstraint', 2) %!error ... %! RegressionKernel (ones (10, 2), ones (10, 1), 'Learner', 'leastsquares', ... %! 'BoxConstraint', 2) %!error ... %! RegressionKernel (ones (10, 2), ones (10, 1), 'Standardize', 'yes') %!error ... %! RegressionKernel (ones (10, 2), ones (10, 1), 'Verbose', 2) %!error ... %! RegressionKernel (ones (10, 2), ones (10, 1), 'Nonsense', 1) %!error RegressionKernel ({1, 2; 3, 4}, [1; 2]) %!error RegressionKernel ([], []) %!error RegressionKernel (ones (10, 2), {1, 2}) %!error ... %! RegressionKernel (ones (10, 2), ones (3, 1)) %!error ... %! RegressionKernel (ones (10, 2), ones (10, 1), 'Weights', ones (3, 1)) %!error ... %! predict (RegressionKernel (ones (10, 2), ones (10, 1)), []) %!error ... %! predict (RegressionKernel (ones (10, 2), ones (10, 1)), ones (3, 5)) %!error ... %! loss (RegressionKernel (ones (10, 2), ones (10, 1)), ones (10, 2), ... %! ones (10, 1), 'LossFun', 'hinge') %!error ... %! loss (RegressionKernel (ones (10, 2), ones (10, 1)), ones (10, 2), ... %! ones (10, 1), 'Bogus', 1) %!error ... %! loss (RegressionKernel (ones (10, 2), ones (10, 1), 'Learner', ... %! 'leastsquares'), ones (10, 2), ones (10, 1), ... %! 'LossFun', 'epsiloninsensitive') %!error ... %! resume (RegressionKernel (ones (10, 2), ones (10, 1)), ones (10, 2)) %!error ... %! resume (RegressionKernel (ones (10, 2), ones (10, 1)), ones (10, 5), ... %! ones (10, 1)) %!error ... %! resume (RegressionKernel (ones (10, 2), ones (10, 1)), ones (10, 2), ... %! ones (10, 1), 'Nonsense', 1) %!error ... %! resume (RegressionKernel (ones (10, 2), ones (10, 1)), ones (10, 2), ... %! ones (10, 1), 'Weights', -1) ## Every documented response transform reaches the response that is reported. %!test %! load fisheriris %! Mdl = fitrkernel (meas(:,2:4), meas(:,1)); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! T = {'identity', @(x) x; 'exp', @(x) exp (x); 'log', @(x) log (x)}; %! for i = 1:rows (T) %! Mdl.ResponseTransform = T{i,1}; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, T{i,2}(raw), 1e-12); %! endfor ## A function handle is taken as given and applied to the response. %!test %! load fisheriris %! Mdl = fitrkernel (meas(:,2:4), meas(:,1)); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! Mdl.ResponseTransform = @(x) x .^ 2; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, raw .^ 2, 1e-12); %!shared Xc, Dc, yc %! c1 = repmat ([1; 2; 3], 20, 1); %! x2 = sin ((1:60)'); %! c3 = repmat ([10; 10; 20; 20], 15, 1); %! Xc = [c1, x2, c3]; %! Dc = [c1 == 1, c1 == 2, c1 == 3, x2, c3 == 10, c3 == 20]; %! yc = 5 * (c1 == 2) + 0.5 * x2 - 3 * (c3 == 20) + 0.1 * cos ((1:60)'); %!test # MATLAB parity: the kernel expands the dummy coded predictors %! randn ('seed', 9); %! rand ('seed', 9); %! Mdl = RegressionKernel (Xc, yc, 'CategoricalPredictors', [1, 3]); %! randn ('seed', 9); %! rand ('seed', 9); %! H = RegressionKernel (Dc, yc); %! assert_equal (Mdl.CategoricalPredictors, [1, 3]); %! assert_equal (Mdl.NumExpansionDimensions, 256); %! assert_equal (predict (Mdl, Xc(1:5,:)), predict (H, Dc(1:5,:)), 1e-12); %!test # MATLAB parity: the coded columns are not standardized %! Mdl = RegressionKernel (Xc, yc, 'CategoricalPredictors', [1, 3], ... %! 'Standardize', true); %! assert_equal (Mdl.Mu([1:3, 5:6]), zeros (1, 5)); %! assert_equal (Mdl.Sigma([1:3, 5:6]), ones (1, 5)); %! assert_equal (Mdl.Sigma(4), std (Xc(:,2)), 1e-12); %!test # resume codes the data as the fit did %! Mdl = RegressionKernel (Xc, yc, 'CategoricalPredictors', [1, 3]); %! Mdl = resume (Mdl, Xc, yc); %! yhat = predict (Mdl, [4, 0, 10; 2, 0, 20]); %! ys = sort (yc); %! assert_equal (yhat(1), ys(ceil (numel (ys) / 2))); %! assert_equal (isnan (yhat(2)), false); %!error ... %! RegressionKernel (Xc, yc, 'CategoricalPredictors', 4) ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,2:3); %! y = meas(:,1); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SW', 'PL'}); %! T.SL = y; %! Mdl = fitrkernel (T, 'SL'); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'SL'), a); %! assert_equal (loss (Mdl, T), a); ## A table at resume %!test # the response is named or given beside the table %! load fisheriris %! X = meas(:,2:3); %! y = meas(:,1); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SW', 'PL'}); %! T.SL = y; %! Mdl = fitrkernel (T, 'SL', 'IterationLimit', 5); %! a = predict (resume (Mdl, X, y, 'IterationLimit', 20), X); %! assert_equal (predict (resume (Mdl, T(:,1:2), y, ... %! 'IterationLimit', 20), X), a); %! assert_equal (predict (resume (Mdl, T, 'SL', 'IterationLimit', 20), X), a); %! assert_equal (predict (resume (Mdl, T(:,[3, 2, 1]), 'SL'), X), ... %! predict (resume (Mdl, X, y), X)); %!error ... %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'SL'}); %! resume (fitrkernel (T, 'SL'), T) %!error ... %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'SL'}); %! resume (fitrkernel (T, 'SL'), T, 'IterationLimit', 5) ## Observation weights of class single or double %!error ... %! fitrkernel ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', 'Weights', int8 ([1; 1; 1; 1])) %!error ... %! fitrkernel ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', 'Weights', true (4, 1)) %!error %! X = [1, 2; 3, 4; 5, 6; 7, 8]; %! y = (1:4)'; %! loss (fitrkernel (X, y), X, y, 'Weights', int8 ([1; 1; 1; 1])) %!error %! X = [1, 2; 3, 4; 5, 6; 7, 8]; %! y = (1:4)'; %! resume (fitrkernel (X, y), X, y, 'Weights', int8 ([1; 1; 1; 1])) %!test %! ## Single weights compute as double %! load fisheriris %! X = meas(:,2:4); %! y = meas(:,1); %! w = 1 + (1:150)' / 7; %! rand ('seed', 1); %! randn ('seed', 1); %! A = fitrkernel (X, y, 'Weights', single (w)); %! rand ('seed', 1); %! randn ('seed', 1); %! B = fitrkernel (X, y, 'Weights', double (single (w))); %! assert_equal (predict (A, X), predict (B, X)); statistics-release-2.1.2/inst/Machine_Learning/RegressionLinear.m000066400000000000000000001756661527311104300251370ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftp {statistics} RegressionLinear ## ## Linear regression model for high dimensional data. ## ## A @qcode{RegressionLinear} object fits a linear model, ## @code{@var{X} * Beta + Bias}, to a continuous response by minimizing a ## regularized average loss. The loss is the epsilon-insensitive loss for a ## support vector machine and the squared error for a least squares fit, and ## the penalty is either a ridge or a lasso one. ## ## Unlike the other regression models of this package the object holds no ## copy of the training data: the coefficients, the intercept and the fitting ## options are the whole model. That is what makes it suited to data with ## more predictors than a kernel matrix could carry, and it is why the class ## has no @code{compact} method and no resubstitution methods. ## ## A vector of regularization strengths fits one model per value in a single ## object. @qcode{Beta} is then a @math{PxL} matrix and @qcode{Bias} a ## @math{1xL} row, every method returns one column per strength, and ## @code{selectModels} narrows the object down to the strengths worth ## keeping. ## ## Create a @qcode{RegressionLinear} object with @code{fitrlinear}. ## ## @seealso{fitrlinear, RegressionKernel, RegressionSVM} ## @end deftp classdef RegressionLinear < PredictiveModel properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {RegressionLinear} {property} Epsilon ## ## Half the width of the epsilon-insensitive band ## ## A nonnegative scalar for a support vector machine, and empty for a ## least squares fit, which has no such band. It defaults to the ## interquartile range of the response over 13.49, an estimate of its ## standard deviation, or to @qcode{0.1} when that range is zero. This ## property is read-only. ## ## @end deftp Epsilon = []; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {RegressionLinear} {property} ResponseTransform ## ## Transformation applied to the predicted response ## ## A character vector, or the text of the function handle that was ## supplied. Assigning to it accepts either. ## ## @end deftp ResponseTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {RegressionLinear} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors with one name per column of the ## training data, defaulting to @qcode{'x1'}, @qcode{'x2'} and so on. ## This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {RegressionLinear} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A row vector of column indices, empty when every predictor is ## numeric. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {RegressionLinear} {property} ResponseName ## ## Name of the response ## ## A character vector, defaulting to @qcode{'Y'}. This property is ## read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {RegressionLinear} {property} ExpandedPredictorNames ## ## Names of the predictors as the fit saw them ## ## A cell array of character vectors. It equals @qcode{PredictorNames} ## unless categorical predictors were expanded into indicator variables. ## This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {RegressionLinear} {property} Learner ## ## Linear regression model that was fitted ## ## Either @qcode{'svm'} or @qcode{'leastsquares'}. This property is ## read-only. ## ## @end deftp Learner = 'svm'; ## -*- texinfo -*- ## @deftp {RegressionLinear} {property} Beta ## ## Fitted linear coefficients ## ## A @math{Px1} column, or a @math{PxL} matrix with one column per ## regularization strength when @qcode{Lambda} holds more than one. This ## property is read-only. ## ## @end deftp Beta = []; ## -*- texinfo -*- ## @deftp {RegressionLinear} {property} Bias ## ## Fitted intercept ## ## A scalar, or a @math{1xL} row with one element per regularization ## strength. It is zero throughout when the model was fitted with ## @qcode{'FitBias'} set to false. This property is read-only. ## ## @end deftp Bias = []; ## -*- texinfo -*- ## @deftp {RegressionLinear} {property} FittedLoss ## ## Loss function the fit minimized ## ## @qcode{'epsiloninsensitive'} for a support vector machine and ## @qcode{'mse'} for a least squares fit. This is the loss of the ## objective, which is not the loss @code{loss} reports unless it is ## asked for. This property is read-only. ## ## @end deftp FittedLoss = 'epsiloninsensitive'; ## -*- texinfo -*- ## @deftp {RegressionLinear} {property} Lambda ## ## Regularization strength ## ## A nonnegative scalar, or a @math{1xL} row of them in ascending order. ## It defaults to the reciprocal of the number of observations used to ## train the model. This property is read-only. ## ## @end deftp Lambda = []; ## -*- texinfo -*- ## @deftp {RegressionLinear} {property} ModelParameters ## ## Fitting options, as they were given ## ## A structure holding every parameter of the fit, including the ones ## that a different solver would have used and the @qcode{'auto'} values ## before they were resolved. This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {RegressionLinear} {property} Regularization ## ## Penalty on the coefficients ## ## @qcode{'ridge (L2)'} or @qcode{'lasso (L1)'}. This property is ## read-only. ## ## @end deftp Regularization = 'ridge (L2)'; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ## The callable behind ResponseTransform. The public property is the ## text MATLAB stores; this is what predict actually applies. RTfun = @(y) y; ## Number of predictors the fit saw, which the class does not report ## because MATLAB does not, but predict needs to validate its input. NumPredictors_ = []; Coding_ = []; MissingResponse_ = NaN; ## What the fit reported, so that fitrlinear can hand it back as its ## second output. MATLAB returns it from the fitting function rather ## than storing it on the model, and a constructor cannot have a second ## output argument. FitInfo_ = []; endproperties methods (Access = public) ## -*- texinfo -*- ## @deftypefn {RegressionLinear} {@var{obj} =} RegressionLinear (@var{X}, @var{Y}) ## @deftypefnx {RegressionLinear} {@var{obj} =} RegressionLinear (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {RegressionLinear} {@var{obj} =} RegressionLinear (@var{Tbl}, @var{formula}) ## @deftypefnx {RegressionLinear} {@var{obj} =} RegressionLinear (@var{Tbl}, @var{Y}) ## @deftypefnx {RegressionLinear} {@var{obj} =} RegressionLinear (@dots{}, @var{name}, @var{value}) ## ## Fit a linear regression model. ## ## @code{@var{obj} = RegressionLinear (@var{X}, @var{Y})} fits a linear ## support vector machine to the @math{NxP} predictor matrix @var{X} and ## the @math{Nx1} continuous response @var{Y}. ## ## @code{@var{obj} = RegressionLinear (@dots{}, @var{name}, ## @var{value})} takes the following @qcode{Name-Value} pairs. ## ## @multitable @columnfractions 0.28 0.72 ## @headitem Name @tab Value ## ## @item @qcode{'Learner'} @tab @qcode{'svm'}, the default, or ## @qcode{'leastsquares'}. The first minimizes the epsilon-insensitive ## loss and the second the squared error. ## ## @item @qcode{'Epsilon'} @tab Half the width of the insensitive band, ## a nonnegative scalar or @qcode{'auto'}, which is the interquartile ## range of @var{Y} over 13.49. It applies to a support vector machine ## alone. ## ## @item @qcode{'Regularization'} @tab @qcode{'ridge'} or ## @qcode{'lasso'}. It defaults to @qcode{'lasso'} when the solver is ## @qcode{'sparsa'} and to @qcode{'ridge'} otherwise. ## ## @item @qcode{'Lambda'} @tab @qcode{'auto'}, the default, which is the ## reciprocal of the number of observations, or a nonnegative scalar, or ## a vector of them. A vector fits one model per value. ## ## @item @qcode{'Solver'} @tab One of @qcode{'sgd'}, @qcode{'asgd'}, ## @qcode{'dual'}, @qcode{'bfgs'}, @qcode{'lbfgs'} and @qcode{'sparsa'}, ## or a cell array of them applied in turn, each warm starting the next. ## ## @item @qcode{'Beta'} @tab Initial coefficients, a @math{Px1} column or ## a @math{PxL} matrix. It defaults to zeros. ## ## @item @qcode{'Bias'} @tab Initial intercept, a scalar or a @math{1xL} ## row. It defaults to the weighted mean of @var{Y} for a least squares ## fit and to its weighted median for a support vector machine. ## ## @item @qcode{'FitBias'} @tab Whether to fit an intercept at all, true ## by default. ## ## @item @qcode{'PostFitBias'} @tab Whether to refit the intercept once ## the coefficients are settled, false by default. ## ## @item @qcode{'ObservationsIn'} @tab @qcode{'rows'}, the default, or ## @qcode{'columns'}, which transposes @var{X} before fitting. ## ## @item @qcode{'BetaTolerance'} @tab Relative tolerance on the ## coefficients, @qcode{1e-4} by default. ## ## @item @qcode{'GradientTolerance'} @tab Absolute tolerance on the ## gradient's infinity norm, @qcode{1e-6} by default. ## ## @item @qcode{'DeltaGradientTolerance'} @tab Tolerance on the ## complementarity gap of the @qcode{'dual'} solver, @qcode{0.1} by ## default. ## ## @item @qcode{'IterationLimit'} @tab Largest number of iterations, ## @qcode{1000} by default. ## ## @item @qcode{'PassLimit'} @tab Largest number of passes over the data ## for the stochastic solvers, @qcode{1} by default, and @qcode{10} for ## @qcode{'dual'}. ## ## @item @qcode{'BatchSize'} @tab Mini-batch size of the stochastic ## solvers, @qcode{10} by default. ## ## @item @qcode{'BatchLimit'} @tab Largest number of mini-batches. ## ## @item @qcode{'LearnRate'} @tab Step size of the stochastic solvers. ## ## @item @qcode{'OptimizeLearnRate'} @tab Whether to halve the step size ## when the objective rises, true by default. ## ## @item @qcode{'TruncationPeriod'} @tab Number of mini-batches between ## soft thresholdings under a lasso penalty, @qcode{10} by default. ## ## @item @qcode{'NumCheckConvergence'} @tab Number of passes between ## convergence checks of the @qcode{'dual'} solver, @qcode{2} by ## default. MathWorks documents @qcode{5}; R2024a and R2026a both ## report @qcode{2}. ## ## @item @qcode{'HessianHistorySize'} @tab Number of curvature pairs the ## quasi-Newton solvers keep, @qcode{15} by default. ## ## @item @qcode{'ResponseTransform'} @tab A transformation applied to the ## predicted response, named or given as a function handle. ## ## @item @qcode{'Weights'} @tab One nonnegative weight per observation, as a ## single or double vector. Every computation runs in double. ## ## @item @qcode{'PredictorNames'} @tab One name per predictor. ## ## @item @qcode{'ResponseName'} @tab A name for the response. ## ## @item @qcode{'CategoricalPredictors'} @tab The categorical predictors, ## as indices, as a logical vector with one element per predictor, or as ## @qcode{'all'}. Each is dummy coded in its place, one column of zeros ## and ones per distinct value it takes in the training data, named as in ## @qcode{'x1 == 2'}. A row holding a value the training data did not ## is predicted as a row missing a predictor, the weighted lower median ## of the training response. ## A predictor may be named rather than indexed, as a character matrix of ## one padded name per row, a string array or a cellstr; a name must match ## an entry of @qcode{'PredictorNames'} exactly, its case included. ## @end multitable ## ## The default solver is @qcode{'sparsa'} under a lasso penalty. Under a ## ridge penalty it is @qcode{'bfgs'} when there are no more than 100 ## predictors, and beyond that @qcode{'dual'} for a support vector ## machine and @qcode{'sgd'} for a least squares fit. ## ## @seealso{fitrlinear, RegressionKernel} ## @end deftypefn function this = RegressionLinear (X, Y, varargin) ## Check for sufficient number of input arguments if (nargin < 2) error ("RegressionLinear: too few input arguments."); endif ## A table names its own predictors and says which hold levels [this, X, Y, varargin] = resolveTable (this, 'RegressionLinear', ... X, Y, varargin); if (mod (numel (varargin), 2) != 0) error (strcat ("RegressionLinear: optional arguments must be", ... " given in Name-Value pairs.")); endif ## Parse optional paired arguments optNames = {'Learner', 'Epsilon', 'Regularization', 'Lambda', ... 'Solver', 'Beta', 'Bias', 'FitBias', 'PostFitBias', ... 'ObservationsIn', 'BetaTolerance', 'GradientTolerance', ... 'DeltaGradientTolerance', 'IterationLimit', 'PassLimit', ... 'BatchSize', 'BatchLimit', 'LearnRate', ... 'OptimizeLearnRate', 'TruncationPeriod', ... 'NumCheckConvergence', 'HessianHistorySize', 'Verbose', ... 'ResponseTransform', 'Weights', 'PredictorNames', ... 'ResponseName', 'CategoricalPredictors'}; ## An empty default stands for one resolved once the data are known: ## 'Epsilon' is the interquartile range of the response over 13.49, ## empty so that giving it is told apart from leaving it out; ## 'Regularization' is 'lasso' for the 'sparsa' solver and 'ridge' ## otherwise; 'Solver' is 'sparsa' for a lasso penalty, 'bfgs' up to ## 100 predictors, then 'dual' for an SVM and 'sgd' for least ## squares; 'Beta' is zero and 'Bias' the weighted median response, ## or the weighted mean for least squares; 'PassLimit' is 10 with the ## 'dual' solver and 1 otherwise; 'BatchLimit' sets no limit; ## 'LearnRate' is one over the root of one plus the largest squared ## row length; 'Weights' are uniform; 'PredictorNames' are x1, x2, ... dfValues = {'svm', [], [], 'auto', [], [], [], true, false, 'rows', ... 1e-4, 1e-6, 0.1, 1000, [], 10, [], [], true, 10, 2, 15, 0, ... 'none', [], {}, 'Y', []}; [Learner, EpsilonIn, Regularization, Lambda, Solver, BetaIn, BiasIn, ... FitBias, PostFitBias, ObservationsIn, BetaTolerance, ... GradientTolerance, DeltaGradientTolerance, IterationLimit, PassLimit, ... BatchSize, BatchLimit, LearnRate, OptimizeLearnRate, ... TruncationPeriod, NumCheckConvergence, HessianHistorySize, Verbose, ... ResponseTransform, Weights, PredictorNames, ResponseName, ... CategoricalPredictors, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (ischar (Learner) && any (strcmpi (Learner, {'svm', 'leastsquares'})))) error (strcat ("RegressionLinear: 'Learner' must be either", ... " 'svm' or 'leastsquares'.")); endif Learner = lower (Learner); if (! isempty (EpsilonIn) && ! ((ischar (EpsilonIn) && strcmpi (EpsilonIn, 'auto')) || (isnumeric (EpsilonIn) && isscalar (EpsilonIn) && isreal (EpsilonIn) && EpsilonIn >= 0))) error (strcat ("RegressionLinear: 'Epsilon' must be 'auto'", ... " or a nonnegative scalar.")); endif if (! isempty (Regularization) && ! (ischar (Regularization) && any (strcmpi (Regularization, {'ridge', 'lasso'})))) error (strcat ("RegressionLinear: 'Regularization' must be", ... " either 'ridge' or 'lasso'.")); endif Regularization = lower (Regularization); if (! ((ischar (Lambda) && strcmpi (Lambda, 'auto')) || (isnumeric (Lambda) && isreal (Lambda) && isvector (Lambda) && ! isempty (Lambda) && all (Lambda >= 0) && all (isfinite (Lambda))))) error (strcat ("RegressionLinear: 'Lambda' must be 'auto'", ... " or a vector of nonnegative finite values.")); endif if (ischar (Solver)) Solver = {Solver}; endif valid = {'sgd', 'asgd', 'dual', 'bfgs', 'lbfgs', 'sparsa'}; if (! isempty (Solver) && ! (iscellstr (Solver) && ! isempty (Solver) && all (cellfun (@(s) any (strcmpi (s, valid)), Solver)))) error (strcat ("RegressionLinear: 'Solver' must be one of", ... " 'sgd', 'asgd', 'dual', 'bfgs', 'lbfgs'", ... " and 'sparsa', or a cell array of them.")); endif Solver = lower (Solver); if (! isempty (BetaIn) && ! (isnumeric (BetaIn) && isreal (BetaIn) && ismatrix (BetaIn) && ! isempty (BetaIn))) error (strcat ("RegressionLinear: 'Beta' must be a real", ... " numeric matrix.")); endif if (! isempty (BiasIn) && ! (isnumeric (BiasIn) && isreal (BiasIn) && isvector (BiasIn) && ! isempty (BiasIn))) error (strcat ("RegressionLinear: 'Bias' must be a real", ... " numeric vector.")); endif if (! (islogical (FitBias) || (isnumeric (FitBias) && isscalar (FitBias) && any (FitBias == [0, 1])))) error (strcat ("RegressionLinear: 'FitBias' must be either", ... " true or false.")); endif FitBias = logical (FitBias); if (! (islogical (PostFitBias) || (isnumeric (PostFitBias) && isscalar (PostFitBias) && any (PostFitBias == [0, 1])))) error (strcat ("RegressionLinear: 'PostFitBias' must be", ... " either true or false.")); endif PostFitBias = logical (PostFitBias); if (! (ischar (ObservationsIn) && any (strcmpi (ObservationsIn, {'rows', 'columns'})))) error (strcat ("RegressionLinear: 'ObservationsIn' must be", ... " either 'rows' or 'columns'.")); endif ObservationsIn = lower (ObservationsIn); if (! (isnumeric (BetaTolerance) && isscalar (BetaTolerance) && isreal (BetaTolerance) && BetaTolerance >= 0)) error (strcat ("RegressionLinear: 'BetaTolerance' must be", ... " a nonnegative scalar.")); endif if (! (isnumeric (GradientTolerance) && isscalar (GradientTolerance) && isreal (GradientTolerance) && GradientTolerance >= 0)) error (strcat ("RegressionLinear: 'GradientTolerance'", ... " must be a nonnegative scalar.")); endif if (! (isnumeric (DeltaGradientTolerance) && isscalar (DeltaGradientTolerance) && isreal (DeltaGradientTolerance) && DeltaGradientTolerance >= 0)) error (strcat ("RegressionLinear:", ... " 'DeltaGradientTolerance' must be a", ... " nonnegative scalar.")); endif if (! (isnumeric (IterationLimit) && isscalar (IterationLimit) && isreal (IterationLimit) && IterationLimit > 0 && fix (IterationLimit) == IterationLimit)) error (strcat ("RegressionLinear: 'IterationLimit' must", ... " be a positive integer scalar.")); endif if (! isempty (PassLimit) && ! (isnumeric (PassLimit) && isscalar (PassLimit) && isreal (PassLimit) && PassLimit > 0 && fix (PassLimit) == PassLimit)) error (strcat ("RegressionLinear: 'PassLimit' must be a", ... " positive integer scalar.")); endif if (! (isnumeric (BatchSize) && isscalar (BatchSize) && isreal (BatchSize) && BatchSize > 0 && fix (BatchSize) == BatchSize)) error (strcat ("RegressionLinear: 'BatchSize' must be a", ... " positive integer scalar.")); endif if (! isempty (BatchLimit) && ! (isnumeric (BatchLimit) && isscalar (BatchLimit) && isreal (BatchLimit) && BatchLimit > 0 && fix (BatchLimit) == BatchLimit)) error (strcat ("RegressionLinear: 'BatchLimit' must be a", ... " positive integer scalar.")); endif if (! isempty (LearnRate) && ! (isnumeric (LearnRate) && isscalar (LearnRate) && isreal (LearnRate) && LearnRate > 0)) error (strcat ("RegressionLinear: 'LearnRate' must be a", ... " positive scalar.")); endif if (! (islogical (OptimizeLearnRate) || (isnumeric (OptimizeLearnRate) && isscalar (OptimizeLearnRate) && any (OptimizeLearnRate == [0, 1])))) error (strcat ("RegressionLinear: 'OptimizeLearnRate'", ... " must be either true or false.")); endif OptimizeLearnRate = logical (OptimizeLearnRate); if (! (isnumeric (TruncationPeriod) && isscalar (TruncationPeriod) && isreal (TruncationPeriod) && TruncationPeriod > 0 && fix (TruncationPeriod) == TruncationPeriod)) error (strcat ("RegressionLinear: 'TruncationPeriod' must", ... " be a positive integer scalar.")); endif if (! (isnumeric (NumCheckConvergence) && isscalar (NumCheckConvergence) && isreal (NumCheckConvergence) && NumCheckConvergence > 0 && fix (NumCheckConvergence) == NumCheckConvergence)) error (strcat ("RegressionLinear: 'NumCheckConvergence'", ... " must be a positive integer scalar.")); endif if (! (isnumeric (HessianHistorySize) && isscalar (HessianHistorySize) && isreal (HessianHistorySize) && HessianHistorySize > 0 && fix (HessianHistorySize) == HessianHistorySize)) error (strcat ("RegressionLinear: 'HessianHistorySize'", ... " must be a positive integer scalar.")); endif if (! (isnumeric (Verbose) && isscalar (Verbose) && isreal (Verbose) && any (Verbose == [0, 1, 2]))) error (strcat ("RegressionLinear: 'Verbose' must be 0, 1,", ... " or 2.")); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("RegressionLinear: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isreal (Weights) && isvector (Weights) && all (Weights >= 0))) error (strcat ("RegressionLinear: 'Weights' must be a", ... " vector of nonnegative values.")); endif if (! isempty (PredictorNames) && ! (iscellstr (PredictorNames) && isvector (PredictorNames))) error (strcat ("RegressionLinear: 'PredictorNames' must", ... " be a cell array of character vectors.")); endif if (! (ischar (ResponseName) && isrow (ResponseName))) error (strcat ("RegressionLinear: 'ResponseName' must be a", ... " character vector.")); endif if (! isempty (args)) error ("RegressionLinear: invalid optional paired argument."); endif ## Observations may be given down the columns, which only means the ## predictor matrix arrives transposed. if (strcmp (ObservationsIn, 'columns')) X = X'; endif ## Validate the data and resolve the weights. The four linear and ## kernel regression models share that opening, so it lives in one ## place. F = regFrame (X, Y, Weights, 'RegressionLinear'); X = F.X; Y = F.Y; W = F.W; n = F.n; p = F.p; ## Dummy code the categorical predictors on the rows kept; the fit ## sees the coded columns, and new data is coded the same way. if (isempty (PredictorNames)) PredictorNames = arrayfun (@(k) sprintf ("x%d", k), 1:p, ... 'UniformOutput', false); elseif (numel (PredictorNames) != p) error (strcat ("RegressionLinear: 'PredictorNames' must have one", ... " name per predictor.")); endif [Coding, errmsg] = dummyCoding (X, CategoricalPredictors, ... PredictorNames); if (! isempty (errmsg)) error ("RegressionLinear: %s", errmsg); endif X = dummyCoding (X, Coding); p = columns (X); ## What a row missing a predictor is predicted to be, as MATLAB R2024a ## predicts it: the weighted lower median of the training response. this.MissingResponse_ = missingResponse (Y, W); ## Epsilon belongs to the insensitive band, so it means nothing to a ## least squares fit and MATLAB refuses it there rather than ignoring ## it. if (strcmp (Learner, 'leastsquares')) if (! isempty (EpsilonIn)) error (strcat ("RegressionLinear: 'Epsilon' applies to a", ... " support vector machine only.")); endif Epsilon = []; elseif (isempty (EpsilonIn) || ischar (EpsilonIn)) r = iqr (Y); if (r == 0) Epsilon = 0.1; else Epsilon = r / 13.49; endif else Epsilon = EpsilonIn; endif if (isempty (EpsilonIn)) EpsilonIn = 'auto'; endif ## Resolve the penalty and the solver against one another, since each ## has a default that depends on the other. if (isempty (Regularization)) if (! isempty (Solver) && numel (Solver) == 1 && strcmp (Solver{1}, 'sparsa')) Regularization = 'lasso'; else Regularization = 'ridge'; endif endif if (isempty (Solver)) if (strcmp (Regularization, 'lasso')) Solver = {'sparsa'}; elseif (p <= 100) Solver = {'bfgs'}; elseif (strcmp (Learner, 'svm')) Solver = {'dual'}; else Solver = {'sgd'}; endif endif for k = 1:numel (Solver) if (strcmp (Regularization, 'lasso') && ! any (strcmp (Solver{k}, {'sgd', 'asgd', 'sparsa'}))) error (strcat ("RegressionLinear: the '%s' solver fits a ridge", ... " penalty only."), Solver{k}); endif if (strcmp (Regularization, 'ridge') && strcmp (Solver{k}, 'sparsa')) error (strcat ("RegressionLinear: the 'sparsa' solver fits a", ... " lasso penalty only.")); endif if (strcmp (Solver{k}, 'dual') && strcmp (Learner, 'leastsquares')) error (strcat ("RegressionLinear: the 'dual' solver fits an", ... " epsilon-insensitive loss only, so it needs", ... " 'Learner' set to 'svm'.")); endif endfor ## Resolve Lambda, which the class reports as a number even when it ## was given as 'auto', and which ModelParameters keeps as given. LambdaIn = Lambda; if (ischar (Lambda)) Lambda = 1 / n; else Lambda = sort (Lambda(:)'); endif L = numel (Lambda); ## Resolve the starting point if (isempty (BetaIn)) Beta0 = zeros (p, L); else if (rows (BetaIn) != p) error (strcat ("RegressionLinear: 'Beta' must have one row per", ... " predictor.")); endif if (columns (BetaIn) == 1) Beta0 = repmat (BetaIn, 1, L); elseif (columns (BetaIn) == L) Beta0 = BetaIn; else error (strcat ("RegressionLinear: 'Beta' must have one column,", ... " or one per value of 'Lambda'.")); endif endif if (isempty (BiasIn)) if (strcmp (Learner, 'leastsquares')) Bias0 = repmat (sum (W .* Y), 1, L); else Bias0 = repmat (weightedMedian (Y, W), 1, L); endif else BiasIn = BiasIn(:)'; if (numel (BiasIn) == 1) Bias0 = repmat (BiasIn, 1, L); elseif (numel (BiasIn) == L) Bias0 = BiasIn; else error (strcat ("RegressionLinear: 'Bias' must be a scalar, or", ... " hold one value per value of 'Lambda'.")); endif endif if (isempty (PassLimit)) if (any (strcmp (Solver, 'dual'))) PassLimit = 10; else PassLimit = 1; endif endif if (isempty (LearnRate)) ## MATLAB's default: the reciprocal root of one plus the largest ## squared length of an observation, so a step never overshoots the ## widest row of the data. LearnRate = 1 / sqrt (1 + max (sum (X .^ 2, 2))); endif ## Fit one model per regularization strength, each warm starting the ## next, which is what makes an ascending Lambda cheaper than the same ## values fitted apart. Beta = zeros (p, L); Bias = zeros (1, L); info = struct ([]); P = struct (); P.Learner = Learner; P.LossFunction = 'epsiloninsensitive'; if (strcmp (Learner, 'leastsquares')) P.LossFunction = 'mse'; endif P.Epsilon = Epsilon; P.Regularization = Regularization; P.FitBias = FitBias; P.PostFitBias = PostFitBias; P.BetaTolerance = BetaTolerance; P.GradientTolerance = GradientTolerance; P.DeltaGradientTolerance = DeltaGradientTolerance; P.IterationLimit = IterationLimit; P.PassLimit = PassLimit; P.BatchSize = BatchSize; P.BatchLimit = BatchLimit; P.LearnRate = LearnRate; P.OptimizeLearnRate = OptimizeLearnRate; P.TruncationPeriod = TruncationPeriod; P.NumCheckConvergence = NumCheckConvergence; P.HessianHistorySize = HessianHistorySize; for l = 1:L P.Lambda = Lambda(l); b = Beta0(:,l); b0 = Bias0(l); if (l > 1 && isempty (BetaIn)) b = Beta(:,l-1); b0 = Bias(l-1); endif for k = 1:numel (Solver) P.Solver = Solver{k}; P.InitialBeta = b; P.InitialBias = b0; [b, b0, S] = linearSolve (X, Y, W, P); endfor Beta(:,l) = b; Bias(l) = b0; S.Lambda = Lambda(l); if (l == 1) info = S; else info(l) = S; endif endfor ## Fill in the model this.Epsilon = Epsilon; this.PredictorNames = PredictorNames; this.ExpandedPredictorNames = PredictorNames; if (! isempty (Coding.Index)) this.ExpandedPredictorNames = Coding.ExpandedNames; this.CategoricalPredictors = Coding.Index; this.Coding_ = Coding; endif this.ResponseName = ResponseName; this.Learner = Learner; this.Beta = Beta; this.Bias = Bias; this.FittedLoss = P.LossFunction; this.Lambda = Lambda; this.NumPredictors_ = Coding.NumPredictors; this.ResponseTransform = ResponseTransform; if (strcmp (Regularization, 'ridge')) this.Regularization = 'ridge (L2)'; else this.Regularization = 'lasso (L1)'; endif this.ModelParameters = linearModelParams (P, Solver, LambdaIn, ... EpsilonIn, Beta0, Bias0, ... Verbose, 'regression'); this.FitInfo_ = linearFitInfo (info, BetaTolerance, ... GradientTolerance, ... DeltaGradientTolerance, ... IterationLimit, Solver, PassLimit, ... BatchLimit); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {RegressionLinear} {@var{yFit} =} predict (@var{obj}, @var{XC}) ## ## Predict the response of new observations. ## ## @code{@var{yFit} = predict (@var{obj}, @var{XC})} returns one ## predicted value per row of @var{XC}, and one column per regularization ## strength. @qcode{ResponseTransform} is applied to the result. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @end deftypefn function yFit = predict (this, XC) if (nargin < 2) error ("RegressionLinear.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'RegressionLinear.predict', XC); if (isempty (XC)) error ("RegressionLinear.predict: XC is empty."); endif if (! (isnumeric (XC) && isreal (XC) && ismatrix (XC))) error ("RegressionLinear.predict: invalid values in XC."); endif if (columns (XC) != this.NumPredictors_) error (strcat ("RegressionLinear.predict: XC must have the same", ... " number of predictors as the trained model.")); endif if (! isempty (this.Coding_)) XC = dummyCoding (XC, this.Coding_); endif f = XC * this.Beta + this.Bias; f(any (isnan (XC), 2),:) = this.MissingResponse_; yFit = this.RTfun (f); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionLinear} {@var{l} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {RegressionLinear} {@var{l} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {RegressionLinear} {@var{l} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {RegressionLinear} {@var{l} =} loss (@dots{}, @var{name}, @var{value}) ## ## Regression loss on new data. ## ## @code{@var{l} = loss (@var{obj}, @var{X}, @var{Y})} returns the mean ## squared error, one value per regularization strength. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{l} = loss (@dots{}, @var{name}, @var{value})} takes ## @qcode{'LossFun'}, either @qcode{'mse'} or ## @qcode{'epsiloninsensitive'}, and @qcode{'Weights'}. The ## epsilon-insensitive loss needs a band to be insensitive within, so it ## is offered by a support vector machine alone. ## ## @end deftypefn function l = loss (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("RegressionLinear.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("RegressionLinear.loss: optional arguments must", ... " be given in Name-Value pairs.")); endif ## Parse optional paired arguments optNames = {'LossFun', 'Weights'}; ## An empty 'Weights' stands for uniform weights dfValues = {'mse', []}; [LossFun, Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (ischar (LossFun) && any (strcmpi (LossFun, ... {'mse', 'epsiloninsensitive'})))) error (strcat ("RegressionLinear.loss: 'LossFun' must be", ... " either 'mse' or 'epsiloninsensitive'.")); endif LossFun = lower (LossFun); errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("RegressionLinear.loss: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isreal (Weights) && isvector (Weights) && all (Weights >= 0))) error (strcat ("RegressionLinear.loss: 'Weights' must be", ... " a vector of nonnegative values.")); endif if (! isempty (args)) error ("RegressionLinear.loss: invalid optional paired argument."); endif if (strcmp (LossFun, 'epsiloninsensitive') && isempty (this.Epsilon)) error (strcat ("RegressionLinear.loss: the", ... " 'epsiloninsensitive' loss applies to a support", ... " vector machine only.")); endif Y = Y(:); if (! (isnumeric (Y) && isreal (Y))) error ("RegressionLinear.loss: invalid values in Y."); endif if (rows (X) != numel (Y)) error (strcat ("RegressionLinear.loss: number of rows in X and Y", ... " must be equal.")); endif if (isempty (Weights)) w = ones (numel (Y), 1); else w = double (Weights(:)); if (numel (w) != numel (Y)) error (strcat ("RegressionLinear.loss: 'Weights' must have one", ... " element per observation.")); endif endif w = w / sum (w); yFit = predict (this, X); L = numel (this.Lambda); l = zeros (1, L); for k = 1:L r = Y - yFit(:,k); if (strcmp (LossFun, 'mse')) l(k) = sum (w .* (r .^ 2)); else l(k) = sum (w .* max (0, abs (r) - this.Epsilon)); endif endfor endfunction ## -*- texinfo -*- ## @deftypefn {RegressionLinear} {@var{sub} =} selectModels (@var{obj}, @var{idx}) ## ## Keep a subset of the fitted regularization strengths. ## ## @code{@var{sub} = selectModels (@var{obj}, @var{idx})} returns a model ## holding only the strengths @var{idx} names, which may be indices into ## @qcode{Lambda} or a logical vector over it. ## ## @end deftypefn function sub = selectModels (this, idx) if (nargin < 2) error (strcat ("RegressionLinear.selectModels: too few input", ... " arguments.")); endif L = numel (this.Lambda); if (islogical (idx)) if (numel (idx) != L) error (strcat ("RegressionLinear.selectModels: a logical IDX", ... " must have one element per value of 'Lambda'.")); endif idx = find (idx); endif if (! (isnumeric (idx) && isreal (idx) && isvector (idx) && ! isempty (idx) && all (fix (idx) == idx) && all (idx >= 1) && all (idx <= L))) error (strcat ("RegressionLinear.selectModels: IDX must hold", ... " integers between 1 and %d."), L); endif sub = this; sub.Lambda = this.Lambda(idx); sub.Beta = this.Beta(:,idx); sub.Bias = this.Bias(idx); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionLinear} {} savemodel (@var{obj}, @var{filename}) ## ## Save a linear regression model to a file. ## ## @code{savemodel (@var{obj}, @var{filename})} saves the model ## @var{obj} into @var{filename} in a form @code{loadmodel} can read ## back. ## ## @end deftypefn function savemodel (obj, fname) classdef_name = 'RegressionLinear'; Epsilon = obj.Epsilon; ResponseTransform = obj.ResponseTransform; PredictorNames = obj.PredictorNames; CategoricalPredictors = obj.CategoricalPredictors; ResponseName = obj.ResponseName; ExpandedPredictorNames = obj.ExpandedPredictorNames; Learner = obj.Learner; Beta = obj.Beta; Bias = obj.Bias; FittedLoss = obj.FittedLoss; Lambda = obj.Lambda; ModelParameters = obj.ModelParameters; Regularization = obj.Regularization; NumPredictors_ = obj.NumPredictors_; Coding_ = obj.Coding_; MissingResponse_ = obj.MissingResponse_; save ('-binary', fname, 'classdef_name', 'Epsilon', ... 'ResponseTransform', 'PredictorNames', ... 'CategoricalPredictors', 'ResponseName', ... 'ExpandedPredictorNames', 'Learner', 'Beta', 'Bias', ... 'FittedLoss', 'Lambda', 'ModelParameters', 'Regularization', ... 'NumPredictors_', 'Coding_', 'MissingResponse_'); endfunction endmethods methods (Access = public, Hidden) function disp (this) printf ("\n RegressionLinear\n\n"); printf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); printf ("%+25s: '%s'\n", 'ResponseTransform', this.ResponseTransform); printf ("%+25s: [%dx%d double]\n", 'Beta', rows (this.Beta), ... columns (this.Beta)); if (numel (this.Bias) == 1) printf ("%+25s: %g\n", 'Bias', this.Bias); printf ("%+25s: %g\n", 'Lambda', this.Lambda); else printf ("%+25s: [1x%d double]\n", 'Bias', numel (this.Bias)); printf ("%+25s: [1x%d double]\n", 'Lambda', numel (this.Lambda)); endif printf ("%+25s: '%s'\n", 'Learner', this.Learner); printf ("\n"); endfunction ## What the fit reported, which fitrlinear returns as its second output. function S = fitInfo_ (this) S = this.FitInfo_; endfunction ## Custom setter, so that assigning a name or a handle updates both the ## text the property reports and the callable predict uses. function this = set.ResponseTransform (this, val) [this.RTfun, this.ResponseTransform] = ... parseResponseTransform (val, 'RegressionLinear'); endfunction endmethods methods (Static, Hidden) function mdl = load_model (filename, data) mdl = RegressionLinear ([0; 1], [0; 1]); fields = fieldnames (data); for k = 1:numel (fields) mdl.(fields{k}) = data.(fields{k}); endfor endfunction endmethods endclassdef %!shared X, Y %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! X = X(ok,:); %! Y = MPG(ok); %!demo %! ## Fit fuel consumption on four engine measurements and read the %! ## coefficients and the insensitive band the fit chose for itself. %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! Mdl = RegressionLinear (X(ok,:), MPG(ok)) %! yFit = predict (Mdl, X(find (ok, 3),:)) %!demo %! ## Least squares has no insensitive band, so Epsilon is empty, and its %! ## loss is the mean squared error the fit minimizes. %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! Mdl = RegressionLinear (X(ok,:), MPG(ok), 'Learner', 'leastsquares'); %! band = Mdl.Epsilon %! mse = loss (Mdl, X(ok,:), MPG(ok)) %!test %! ## The model reports the surface MATLAB reports %! Mdl = RegressionLinear (X, Y); %! assert_equal (class (Mdl), 'RegressionLinear'); %! assert_equal (Mdl.Learner, 'svm'); %! assert_equal (Mdl.FittedLoss, 'epsiloninsensitive'); %! assert_equal (Mdl.Regularization, 'ridge (L2)'); %! assert_equal (Mdl.ResponseTransform, 'none'); %! assert_equal (Mdl.ResponseName, 'Y'); %! assert_equal (Mdl.PredictorNames, {'x1', 'x2', 'x3', 'x4'}); %! assert_equal (Mdl.ExpandedPredictorNames, {'x1', 'x2', 'x3', 'x4'}); %! assert_equal (Mdl.CategoricalPredictors, []); %! assert_equal (size (Mdl.Beta), [4, 1]); %!test %! ## The properties are the ones MATLAB lists, in its order %! Mdl = RegressionLinear (X, Y); %! assert_equal (sort (properties (Mdl)), ... %! sort ({'Epsilon'; 'ResponseTransform'; 'PredictorNames'; ... %! 'CategoricalPredictors'; 'ResponseName'; ... %! 'ExpandedPredictorNames'; 'Learner'; 'Beta'; ... %! 'Bias'; 'FittedLoss'; 'Lambda'; 'ModelParameters'; ... %! 'Regularization'})); %!test %! ## A least squares ridge fit reproduces R2024a's coefficients %! Mdl = RegressionLinear (X, Y, 'Learner', 'leastsquares', ... %! 'Solver', 'lbfgs', 'BetaTolerance', 0, ... %! 'GradientTolerance', 1e-12, ... %! 'IterationLimit', 20000); %! assert_equal (Mdl.Beta, [-0.060147206634135; -0.00667928082241265; ... %! -0.0375377897819825; -0.00608459830980712], 1e-7); %! assert_equal (Mdl.Bias, 48.1149099265466, 1e-6); %! assert_equal (Mdl.FittedLoss, 'mse'); %! assert_equal (Mdl.Epsilon, []); %! assert_equal (loss (Mdl, X, Y), 15.9484134801834, 1e-6); %!test %! ## Epsilon defaults to the interquartile range over 13.49, R2024a's own %! ## estimate of the standard deviation of the response %! Mdl = RegressionLinear (X, Y); %! assert_equal (Mdl.Epsilon, 0.926612305411416, 1e-12); %! assert_equal (Mdl.Epsilon, iqr (Y) / 13.49, 1e-15); %!test %! ## A response of no spread has no interquartile range to scale, and the %! ## band falls back on a tenth %! Mdl = RegressionLinear (ones (10, 2), 5 * ones (10, 1)); %! assert_equal (Mdl.Epsilon, 0.1); %!test %! ## The epsilon-insensitive loss is not differentiable at the edge of the %! ## band, so the line search gives up short of a minimum and where it stops %! ## follows the last bits: 2.8273 here, 3.4883 under clang and on macOS, %! ## against the 2.82717018548797 R2024a reaches. Assert the identity the %! ## reported objective holds at whichever point it stops. %! Mdl = RegressionLinear (X, Y, 'Learner', 'svm', 'Solver', 'lbfgs', ... %! 'BetaTolerance', 0, 'GradientTolerance', 1e-12, ... %! 'IterationLimit', 20000); %! assert_equal (Mdl.FitInfo_.Objective, ... %! loss (Mdl, X, Y, 'LossFun', 'epsiloninsensitive') ... %! + 0.5 * Mdl.Lambda * sum (Mdl.Beta .^ 2), 1e-12); %!test %! ## Lambda defaults to the reciprocal of the observations that were used %! Mdl = RegressionLinear (X, Y); %! assert_equal (Mdl.Lambda, 1 / 93, 1e-15); %!test %! ## A vector of strengths fits one model per value, sorted ascending, and %! ## every method reports one column per value %! Mdl = RegressionLinear (X, Y, 'Lambda', [0.1, 0.001, 0.01]); %! assert_equal (Mdl.Lambda, [0.001, 0.01, 0.1]); %! assert_equal (size (Mdl.Beta), [4, 3]); %! assert_equal (size (Mdl.Bias), [1, 3]); %! assert_equal (size (predict (Mdl, X(1:4,:))), [4, 3]); %! assert_equal (size (loss (Mdl, X, Y)), [1, 3]); %!test %! ## selectModels keeps the strengths it is given and drops the rest %! Mdl = RegressionLinear (X, Y, 'Lambda', [0.001, 0.01, 0.1]); %! sub = selectModels (Mdl, [1, 3]); %! assert_equal (sub.Lambda, [0.001, 0.1]); %! assert_equal (sub.Beta, Mdl.Beta(:,[1, 3])); %!test %! ## A lasso penalty drives coefficients to exactly zero %! Mdl = RegressionLinear (X, Y, 'Learner', 'leastsquares', ... %! 'Regularization', 'lasso', 'Lambda', 100); %! assert_equal (Mdl.Regularization, 'lasso (L1)'); %! assert_equal (sum (Mdl.Beta == 0) > 0, true); %!test %! ## With the penalty all but switched off, a least squares fit is ordinary %! ## least squares, whichever solver ran. This is what pins the solvers to %! ## a value that is known independently of either of them. %! b = [ones(rows (X), 1), X] \ Y; %! Ml = RegressionLinear (X, Y, 'Learner', 'leastsquares', ... %! 'Regularization', 'lasso', 'Lambda', 1e-8, ... %! 'BetaTolerance', 0, 'GradientTolerance', 1e-14, ... %! 'IterationLimit', 200000); %! Mr = RegressionLinear (X, Y, 'Learner', 'leastsquares', ... %! 'Solver', 'lbfgs', 'Lambda', 1e-8, ... %! 'BetaTolerance', 0, 'GradientTolerance', 1e-14, ... %! 'IterationLimit', 200000); %! ## Relative, so every coefficient is held to the same number of figures: %! ## an absolute tolerance is far stricter on the largest of them, and how %! ## close a solver stops to the closed form answer varies with the BLAS. %! assert_equal (Ml.Beta, b(2:end), -1e-3); %! assert_equal (Ml.Bias, b(1), 1e-4); %! assert_equal (Mr.Beta, b(2:end), -1e-3); %! assert_equal (Mr.Bias, b(1), 1e-4); %!test %! ## A response transform reaches predict %! Mdl = RegressionLinear (X, Y, 'Learner', 'leastsquares'); %! plain = predict (Mdl, X(1:5,:)); %! Mdl.ResponseTransform = 'exp'; %! assert_equal (Mdl.ResponseTransform, 'exp'); %! assert_equal (predict (Mdl, X(1:5,:)), exp (plain), 1e-12); %!test %! ## FitBias false leaves the intercept at zero %! Mdl = RegressionLinear (X, Y, 'FitBias', false); %! assert_equal (Mdl.Bias, 0); %!test %! ## Observations may be given down the columns instead %! Mr = RegressionLinear (X, Y, 'Learner', 'leastsquares'); %! Mc = RegressionLinear (X', Y, 'Learner', 'leastsquares', ... %! 'ObservationsIn', 'columns'); %! assert_equal (Mr.Beta, Mc.Beta, 1e-12); %!test %! ## A row with a missing predictor or a missing response is dropped, and %! ## Lambda follows the count that survived %! Xn = X; %! Xn(3,2) = NaN; %! Mdl = RegressionLinear (Xn, Y); %! assert_equal (Mdl.Lambda, 1 / 92, 1e-15); %!test %! ## A saved model reads back as the same model %! Mdl = RegressionLinear (X, Y, 'Learner', 'leastsquares'); %! fname = tempname (); %! savemodel (Mdl, fname); %! Mnew = loadmodel (fname); %! delete (fname); %! assert_equal (class (Mnew), 'RegressionLinear'); %! assert_equal (Mnew.Beta, Mdl.Beta); %! assert_equal (predict (Mnew, X(1:5,:)), predict (Mdl, X(1:5,:))); %!test %! ## LossTolerance: the engine's loss test is on the objective VALUE, not on %! ## its change, so linearSolve must switch it off with -Inf. This fit is %! ## the one that catches it: an exact linear relation at a vanishing %! ## penalty drives the objective through 1e-6 long before the coefficients %! ## are right, and with the engine's default the fit stops five iterations %! ## early with them out by 1.6e-4. If this test starts failing, look at %! ## opt.LossTolerance in linearSolve before anything else. %! randn ('seed', 7); %! Xe = randn (60, 3); %! btrue = [2; -3; 0.5]; %! Ye = Xe * btrue + 4; %! Mdl = RegressionLinear (Xe, Ye, 'Learner', 'leastsquares', ... %! 'Solver', 'lbfgs', 'Lambda', 1e-10, ... %! 'BetaTolerance', 0, 'GradientTolerance', 1e-14, ... %! 'IterationLimit', 20000); %! assert_equal (Mdl.Beta, btrue, 1e-8); %! assert_equal (Mdl.Bias, 4, 1e-8); %!test %! ## HessianHistorySize reaches the solver rather than only being recorded: %! ## a one-pair history takes several times the iterations a fifteen-pair %! ## one does on the same problem. Fifteen is MATLAB's documented default %! ## for fitrlinear, where the engine's own default is ten. %! bf = RegressionLinear (X, Y); %! assert_equal (bf.ModelParameters.Solver, {'bfgs'}); %! assert_equal (bf.ModelParameters.HessianHistorySize, []); %! lb = RegressionLinear (X, Y, 'Solver', 'lbfgs'); %! assert_equal (lb.ModelParameters.HessianHistorySize, 15); %! opts = {'Learner', 'leastsquares', 'Solver', 'lbfgs', 'BetaTolerance', 0, ... %! 'GradientTolerance', 1e-10, 'IterationLimit', 5000}; %! short = RegressionLinear (X, Y, opts{:}, 'HessianHistorySize', 1); %! long = RegressionLinear (X, Y, opts{:}, 'HessianHistorySize', 15); %! assert_equal (short.FitInfo_.NumIterations ... %! > long.FitInfo_.NumIterations, true); %!test %! ## The regression counterpart keeps the documented 0.1 where the %! ## classifier takes 1, confirmed on R2026a; the convergence check count %! ## is 2 on both, where the documentation says 5 %! Mdl = RegressionLinear (X, Y, 'Solver', 'dual'); %! assert_equal (Mdl.ModelParameters.DeltaGradientTolerance, 0.1); %! assert_equal (Mdl.ModelParameters.NumCheckConvergence, 2); %! assert_equal (Mdl.ModelParameters.PassLimit, 10); %!test # A row missing a predictor predicts the lower median of the response %! X = [(1:10)', mod((1:10)', 3)]; %! Mdl = RegressionLinear (X, (1:10)'); %! assert_equal (predict (Mdl, [NaN, 1]), 5); %!test # The lower median is weighted %! X = [(1:10)', mod((1:10)', 3)]; %! Mdl = RegressionLinear (X, (1:10)', 'Weights', [ones(9, 1); 10]); %! assert_equal (predict (Mdl, [NaN, 1]), 10); ## Test input validation %!error RegressionLinear (ones (5, 2)) %!error ... %! RegressionLinear (ones (10, 2), ones (10, 1), 'Learner') %!error ... %! RegressionLinear (ones (10, 2), ones (10, 1), 'Learner', 'logistic') %!error ... %! RegressionLinear (ones (10, 2), ones (10, 1), 'Epsilon', -1) %!error ... %! RegressionLinear (ones (10, 2), ones (10, 1), 'Learner', 'leastsquares', ... %! 'Epsilon', 1) %!error ... %! RegressionLinear (ones (10, 2), ones (10, 1), 'Regularization', 'elastic') %!error ... %! RegressionLinear (ones (10, 2), ones (10, 1), 'Lambda', Inf) %!error ... %! RegressionLinear (ones (10, 2), ones (10, 1), 'Solver', 'newton') %!error ... %! RegressionLinear (ones (10, 2), ones (10, 1), 'IterationLimit', 0) %!error ... %! RegressionLinear (ones (10, 2), ones (10, 1), 'Nonsense', 1) %!error RegressionLinear ({1, 2; 3, 4}, [1; 2]) %!error RegressionLinear ([], []) %!error ... %! RegressionLinear (ones (10, 2), {1, 2}) %!error ... %! RegressionLinear (ones (10, 2), ones (3, 1)) %!error ... %! RegressionLinear (ones (10, 2), ones (10, 1), 'Weights', ones (3, 1)) %!error ... %! RegressionLinear (ones (10, 2), ones (10, 1), 'Regularization', 'ridge', ... %! 'Solver', 'sparsa') %!error ... %! RegressionLinear (ones (10, 2), ones (10, 1), 'Regularization', 'lasso', ... %! 'Solver', 'lbfgs') %!error ... %! RegressionLinear (ones (10, 2), ones (10, 1), 'Learner', 'leastsquares', ... %! 'Solver', 'dual') %!error ... %! RegressionLinear (ones (10, 2), ones (10, 1), 'PredictorNames', {'a'}) %!error ... %! predict (RegressionLinear (ones (10, 2), ones (10, 1))) %!error ... %! predict (RegressionLinear (ones (10, 2), ones (10, 1)), []) %!error ... %! predict (RegressionLinear (ones (10, 2), ones (10, 1)), ones (3, 5)) %!error ... %! loss (RegressionLinear (ones (10, 2), ones (10, 1)), ones (10, 2)) %!error ... %! loss (RegressionLinear (ones (10, 2), ones (10, 1)), ones (10, 2), ... %! ones (10, 1), 'LossFun', 'hinge') %!error ... %! loss (RegressionLinear (ones (10, 2), ones (10, 1)), ones (10, 2), ... %! ones (10, 1), 'Bogus', 1) %!error ... %! loss (RegressionLinear (ones (10, 2), ones (10, 1), 'Learner', ... %! 'leastsquares'), ones (10, 2), ones (10, 1), ... %! 'LossFun', 'epsiloninsensitive') %!error ... %! loss (RegressionLinear (ones (10, 2), ones (10, 1)), ones (10, 2), ... %! ones (3, 1)) %!error ... %! selectModels (RegressionLinear (ones (10, 2), ones (10, 1), 'Lambda', ... %! [0.1, 0.2]), 5) ## Every documented response transform reaches the response that is reported. %!test %! load fisheriris %! Mdl = fitrlinear (meas(:,2:4), meas(:,1)); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! T = {'identity', @(x) x; 'exp', @(x) exp (x); 'log', @(x) log (x)}; %! for i = 1:rows (T) %! Mdl.ResponseTransform = T{i,1}; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, T{i,2}(raw), 1e-12); %! endfor ## A function handle is taken as given and applied to the response. %!test %! load fisheriris %! Mdl = fitrlinear (meas(:,2:4), meas(:,1)); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! Mdl.ResponseTransform = @(x) x .^ 2; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, raw .^ 2, 1e-12); %!shared Xc, Dc, yc %! c1 = repmat ([1; 2; 3], 20, 1); %! x2 = sin ((1:60)'); %! c3 = repmat ([10; 10; 20; 20], 15, 1); %! Xc = [c1, x2, c3]; %! Dc = [c1 == 1, c1 == 2, c1 == 3, x2, c3 == 10, c3 == 20]; %! yc = 5 * (c1 == 2) + 0.5 * x2 - 3 * (c3 == 20) + 0.1 * cos ((1:60)'); %!test # MATLAB parity: a categorical predictor is dummy coded in its place %! Mdl = RegressionLinear (Xc, yc, 'CategoricalPredictors', [1, 3]); %! assert_equal (Mdl.CategoricalPredictors, [1, 3]); %! assert_equal (Mdl.ExpandedPredictorNames, {'x1 == 1', 'x1 == 2', ... %! 'x1 == 3', 'x2', 'x3 == 10', 'x3 == 20'}); %! H = RegressionLinear (Dc, yc); %! assert_equal (Mdl.Beta, H.Beta, 1e-12); %! assert_equal (predict (Mdl, Xc(1:5,:)), predict (H, Dc(1:5,:)), 1e-12); %!test # MATLAB parity: 'all' codes every predictor, reported as indices %! Mdl = RegressionLinear (Xc(:,[1, 3]), yc, 'CategoricalPredictors', 'all'); %! assert_equal (Mdl.CategoricalPredictors, [1, 2]); %! assert_equal (numel (Mdl.ExpandedPredictorNames), 5); %!test # MATLAB parity: levels need not be integers %! Mdl = RegressionLinear ([Xc(:,1) + 0.5, Xc(:,2)], yc, ... %! 'CategoricalPredictors', 1); %! assert_equal (Mdl.ExpandedPredictorNames, {'x1 == 1.5', 'x1 == 2.5', ... %! 'x1 == 3.5', 'x2'}); %!test # a level the training data did not hold predicts the lower median %! Mdl = RegressionLinear (Xc, yc, 'CategoricalPredictors', [1, 3]); %! yhat = predict (Mdl, [4, 0, 10; 2.5, 0, 20; NaN, 0, 10; 2, 0, 20]); %! ys = sort (yc); %! assert_equal (yhat(1:3), repmat (ys(ceil (numel (ys) / 2)), 3, 1)); %! assert_equal (isnan (yhat(4)), false); %!test # a NaN in a categorical predictor leaves the row out of the fit %! X2 = Xc; %! X2(1,1) = NaN; %! Mdl = RegressionLinear (X2, yc, 'CategoricalPredictors', [1, 3]); %! H = RegressionLinear (Xc(2:end,:), yc(2:end), ... %! 'CategoricalPredictors', [1, 3]); %! assert_equal (Mdl.Beta, H.Beta, 1e-12); %!test # the coding travels with a saved model %! Mdl = RegressionLinear (Xc, yc, 'CategoricalPredictors', [1, 3]); %! fname = [tempname(), '.mdl']; %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (predict (M2, Xc(1:5,:)), predict (Mdl, Xc(1:5,:))); %!error ... %! RegressionLinear (Xc, yc, 'CategoricalPredictors', 4) %!error ... %! RegressionLinear (Xc, yc, 'CategoricalPredictors', logical ([1, 0])) %!error ... %! RegressionLinear (Xc, yc, 'CategoricalPredictors', 0) ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,2:3); %! y = meas(:,1); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SW', 'PL'}); %! T.SL = y; %! Mdl = fitrlinear (T, 'SL'); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'SL'), a); %! assert_equal (loss (Mdl, T), a); ## Observation weights of class single or double %!error ... %! fitrlinear ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', 'Weights', int8 ([1; 1; 1; 1])) %!error ... %! fitrlinear ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', 'Weights', true (4, 1)) %!error %! X = [1, 2; 3, 4; 5, 6; 7, 8]; %! y = (1:4)'; %! loss (fitrlinear (X, y), X, y, 'Weights', int8 ([1; 1; 1; 1])) %!test %! ## Single weights compute as double %! load fisheriris %! X = meas(:,2:4); %! y = meas(:,1); %! w = 1 + (1:150)' / 7; %! A = fitrlinear (X, y, 'Weights', single (w)); %! B = fitrlinear (X, y, 'Weights', double (single (w))); %! assert_equal (predict (A, X), predict (B, X)); statistics-release-2.1.2/inst/Machine_Learning/RegressionNeuralNetwork.m000066400000000000000000003102061527311104300265020ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef RegressionNeuralNetwork < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} RegressionNeuralNetwork ## ## Neural network regression model. ## ## A @qcode{RegressionNeuralNetwork} object holds a fully connected ## feedforward neural network fitted to a continuous response, and predicts ## the response for new data with the @code{predict} method. The network is ## trained against the mean squared error and its output layer applies the ## identity, so a prediction is an unrestricted real number; otherwise the ## engine is the one @code{ClassificationNeuralNetwork} uses. ## ## The object keeps its training data, which @code{resubPredict}, ## @code{resubLoss} and @code{crossval} work on; @code{compact} drops it and ## returns a @code{CompactRegressionNeuralNetwork}, which still predicts. ## ## Create a @qcode{RegressionNeuralNetwork} object with @code{fitrnet} or the ## class constructor. ## ## @seealso{fitrnet, CompactRegressionNeuralNetwork, ## ClassificationNeuralNetwork} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} X ## ## Predictor data ## ## An @math{NxP} numeric matrix, as it was supplied to the constructor, ## before any rows carrying missing values were dropped. This property is ## read-only. ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} Y ## ## Response data ## ## An @math{Nx1} numeric vector, as it was supplied to the constructor. ## This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} NumObservations ## ## Number of observations used to train the model ## ## A positive integer scalar, counting only the rows that survived the ## removal of missing values. This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} RowsUsed ## ## Rows used for fitting ## ## A logical column vector with the same length as the observations in the ## original predictor data @var{X}, true for each row that was used for ## fitting the RegressionNeuralNetwork model. It is empty, @qcode{[]}, ## when every observation was used, so a non-empty value means that rows ## holding missing values were dropped. This property is read-only. ## ## @end deftp RowsUsed = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} NumPredictors ## ## Number of predictors ## ## A positive integer scalar, the number of columns of @code{X}. This ## property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors, one per column of @code{X}. This ## property is read-only. ## ## @end deftp PredictorNames = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} ResponseName ## ## Name of the response variable ## ## A character vector. This property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} Sigma ## ## Standard deviation of the predictors ## ## A row vector with one entry per predictor, used for standardization. ## Empty when the predictor data were not standardized. This property is ## read-only. ## ## @end deftp Sigma = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} Mu ## ## Mean of the predictors ## ## A row vector with one entry per predictor, used for standardization. ## Empty when the predictor data were not standardized. This property is ## read-only. ## ## @end deftp Mu = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} LayerSizes ## ## Sizes of the fully connected hidden layers ## ## A row vector of positive integers, one per hidden layer. It does not ## include the output layer, whose width is the number of responses. This ## property is read-only. ## ## @end deftp LayerSizes = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} Activations ## ## Activation functions of the hidden layers ## ## A character vector, applying to every hidden layer, or a cell array of ## character vectors with one entry per hidden layer. This property is ## read-only. ## ## @end deftp Activations = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} OutputLayerActivation ## ## Activation function of the output layer ## ## A character vector. The default, @qcode{'none'}, applies the identity, ## so a prediction is an unrestricted real number. This property is ## read-only. ## ## @end deftp OutputLayerActivation = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} LearningRate ## ## Learning rate for gradient descent ## ## A positive scalar value defining the learning rate used by the gradient ## descent algorithm during training. This property is read-only. ## ## @end deftp LearningRate = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} IterationLimit ## ## Maximum number of training iterations ## ## A positive integer scalar. This property is read-only. ## ## @end deftp IterationLimit = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} ModelParameters ## ## Parameters of the trained network ## ## A structure holding the fit as it was asked for: @qcode{LayerSizes}, ## @qcode{Activations}, @qcode{OutputLayerActivation}, ## @qcode{LayerWeightsInitializers}, @qcode{Solver}, ## @qcode{LearningRate}, @qcode{IterationLimit}, ## @qcode{GradientTolerance}, @qcode{LossTolerance}, ## @qcode{StepTolerance}, @qcode{DisplayInfo}, @qcode{StandardizeData}, ## and the @qcode{Version}, @qcode{Method} and @qcode{Type} tags. ## ## What came out of the fit is elsewhere: the @qcode{LayerWeights} and ## @qcode{LayerBiases} properties hold the network, @qcode{TrainingHistory} ## the series and @qcode{ConvergenceInfo} where it stopped. ## ## @qcode{LayerWeightsInitializers} names the scheme each layer's weights ## were drawn with, the output layer last: @qcode{'he'} for a rectifying ## activation and @qcode{'glorot'} for a symmetric one. It is a report, ## not a setting, the engine choosing per layer from the activation and ## offering no way to override it. ## ## @qcode{OutputLayerActivation}, @qcode{Solver} and @qcode{LearningRate} ## are this package's own; MATLAB has no counterpart for them. The fields ## it reports that this class does not accept as arguments ## (@qcode{Lambda}, the validation set and its patience and frequency, ## @qcode{InitialStepSize} and the two initializer settings) are absent. ## This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} ConvergenceInfo ## ## Information recorded during training ## ## A structure with the fields @code{Time}, the seconds training took, and ## @code{TrainingLoss}, the mean squared error of the network at the end of ## each iteration. This property is read-only. ## ## ## Under @qcode{'lbfgs'} the structure carries @code{Gradient} and ## @code{Step}, the two quantities the solver measured to decide it had ## converged, and @code{ConvergenceCriterion}, naming the test that ## stopped it. It carries no @code{Accuracy}: MATLAB reports none, and ## measuring it would cost a pass over the whole training set at every ## iteration. ## @end deftp ConvergenceInfo = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} DisplayInfo ## ## Whether training printed its progress ## ## A logical scalar. This property is read-only. ## ## @end deftp DisplayInfo = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} Solver ## ## Solver used to train the network ## ## A character vector, either @qcode{'Gradient Descent'} for the ## stochastic solver or @qcode{'LBFGS'} for the full-batch one. ## This property is read-only. ## ## @end deftp Solver = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} LayerWeights ## ## Weights the network learned ## ## A cell array with one entry per layer, the output layer included. ## @code{LayerWeights@{i@}} has one row per unit of layer @math{i} and one ## column per input to that layer. This property is read-only. ## ## @end deftp LayerWeights = {}; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} LayerBiases ## ## Biases the network learned ## ## A cell array with one entry per layer, the output layer included. ## @code{LayerBiases@{i@}} is a column with one entry per unit of layer ## @math{i}. This property is read-only. ## ## @end deftp LayerBiases = {}; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} TrainingHistory ## ## Iteration by iteration record of the fit ## ## A @code{table} with the variables @code{Iteration} and ## @code{TrainingLoss}, one row per training iteration. This property is ## read-only. ## ## ## The columns follow the solver. Under @qcode{'sgd'} they are ## @code{Iteration} and @code{TrainingLoss}, with @code{TrainingAccuracy} ## for a classifier. Under @qcode{'lbfgs'} they are @code{Iteration}, ## @code{TrainingLoss}, @code{Gradient} and @code{Step}, as MATLAB's are. ## @end deftp TrainingHistory = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} W ## ## Observation weights ## ## A numeric column vector with one entry per training observation, ## summing to one, by which the training loss weighs each observation. It ## defaults to a uniform weight for every observation, and has the class of ## the @qcode{'Weights'} given, single or double. This property is ## read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A numeric vector of column indices into @code{X} naming the predictors ## treated as categorical, and empty when none is. This property is ## read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} ExpandedPredictorNames ## ## Names of the predictors as the model expanded them ## ## A cell array of character vectors. It matches @code{PredictorNames} ## unless a categorical predictor was expanded into indicator variables. ## This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} BinEdges ## ## Bin edges of the predictors ## ## A cell array with one entry per predictor, holding that predictor's bin ## edges where the learner discretized it before fitting. It is empty here ## and stays empty: this learner fits the predictors as they are, and ## MATLAB's reports an empty cell for it as well. ## ## This property is read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} HyperparameterOptimizationResults ## ## Results of the hyperparameter optimization ## ## @strong{Always empty.} It is declared for MATLAB compatibility, where ## it holds what an automatic search over the hyperparameters found. This ## class fits the parameters it is given and runs no such search, so there ## is nothing to report. This property is read-only. ## ## @end deftp HyperparameterOptimizationResults = []; endproperties ## Properties a user may set after the model is built. Each one is ## validated by its set method below. properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {RegressionNeuralNetwork} {property} ResponseTransform ## ## Transformation applied to the predicted response ## ## A function handle, applied by @code{predict} and @code{resubPredict} to ## the network's output. It defaults to the identity and may be set after ## construction, either to a handle or to the name of a supported ## transformation. ## ## @end deftp ResponseTransform = 'none'; endproperties ## Readable by the counterpart class, which copies it, and kept out of ## the documented surface. properties (GetAccess = public, SetAccess = protected, Hidden) ## The dummy coding of the categorical predictors, empty when none. Coding_ = []; ## The prediction for a row missing a predictor. MissingResponse_ = NaN; RTfun = @(y) y; endproperties ## Set methods for the properties a user may assign. methods (Hidden) function this = set.ResponseTransform (this, val) name = 'RegressionNeuralNetwork'; [this.RTfun, this.ResponseTransform] = ... parseResponseTransform (val, name); endfunction ## Custom display function disp (this) fprintf ("\n RegressionNeuralNetwork\n\n"); ## Print selected properties fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); fprintf ("%+25s: %d\n", 'NumObservations', this.NumObservations); fprintf ("%+25s: %d\n", 'NumPredictors', this.NumPredictors); str = repmat ({'%d'}, 1, numel (this.LayerSizes)); str = strcat ('[', strjoin (str, ' '), ']'); str = sprintf (str, this.LayerSizes); fprintf ("%+25s: %s\n", 'LayerSizes', str); if (iscellstr (this.Activations)) str = repmat ({'''%s'''}, 1, numel (this.Activations)); str = strcat ('{', strjoin (str, ' '), '}'); str = sprintf (str, this.Activations{:}); fprintf ("%+25s: %s\n", 'Activations', str); else # character vector fprintf ("%+25s: '%s'\n", 'Activations', this.Activations); endif fprintf ("%+25s: '%s'\n", 'OutputLayerActivation', ... this.OutputLayerActivation); fprintf ("%+25s: '%s'\n", 'ResponseTransform', this.ResponseTransform); fprintf ("%+25s: '%s'\n", 'Solver', this.Solver); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {RegressionNeuralNetwork} {@var{obj} =} RegressionNeuralNetwork (@var{X}, @var{Y}) ## @deftypefnx {RegressionNeuralNetwork} {@var{obj} =} RegressionNeuralNetwork (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {RegressionNeuralNetwork} {@var{obj} =} RegressionNeuralNetwork (@var{Tbl}, @var{formula}) ## @deftypefnx {RegressionNeuralNetwork} {@var{obj} =} RegressionNeuralNetwork (@var{Tbl}, @var{Y}) ## @deftypefnx {RegressionNeuralNetwork} {@var{obj} =} RegressionNeuralNetwork (@dots{}, @var{name}, @var{value}) ## ## Fit a neural network regression model. ## ## @code{@var{obj} = RegressionNeuralNetwork (@var{X}, @var{Y})} returns a ## neural network regression model, @var{obj}, with @var{X} being the ## predictor data and @var{Y} the continuous response of the observations in ## @var{X}. ## ## @itemize ## @item ## @var{X} must be an @math{NxP} numeric matrix of predictor data, where ## rows correspond to observations and columns to features. ## @item ## @var{Y} must be an @math{Nx1} numeric vector holding the response of the ## corresponding predictor data in @var{X}. @var{Y} must have the same ## number of rows as @var{X}. ## @end itemize ## ## The network is trained against the mean squared error, and its output ## layer applies the identity, so a prediction is an unrestricted real ## number rather than a score over classes. This is the only difference in ## the engine between this class and @code{ClassificationNeuralNetwork}; ## everything else, the layer sizes, the activations, the learning rate and ## the initialisation, behaves identically. ## ## @code{@var{obj} = RegressionNeuralNetwork (@dots{}, @var{name}, ## @var{value})} returns a model with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.32 0.68 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Standardize'} @tab A logical scalar specifying whether the ## predictor data should be centred and scaled before training. The same ## transformation is applied by @code{predict}. The default is ## @qcode{false}. ## ## @item @qcode{'CategoricalPredictors'} @tab The predictors whose values ## are levels, as indices, as a logical vector with one element per ## predictor, or as @qcode{'all'}. Each is dummy coded in its place, one ## column of zeros and ones per level seen in training, named as in ## @qcode{'x1 == 2'} in @code{ExpandedPredictorNames}, and the coded columns ## are not standardized. An observation holding a level the training data ## did not is predicted as a row missing a predictor, the lower median of ## the training response. A predictor may be named rather than indexed, as ## a character matrix of one padded name per row, a string array or a ## cellstr; a name must match an entry of @qcode{'PredictorNames'} exactly, ## its case included. ## ## @item @qcode{'PredictorNames'} @tab A cell array of character vectors ## naming the predictors, in the order they appear in @var{X}. ## ## @item @qcode{'ResponseName'} @tab A character vector naming the response. ## The default is @qcode{'Y'}. ## ## @item @qcode{'ResponseTransform'} @tab A character vector naming one of ## the supported transformations, or a function handle, applied to the ## predicted response by @code{predict} and @code{resubPredict}. The ## default is @qcode{'none'}. ## ## @item @qcode{'LayerSizes'} @tab A positive integer vector specifying the ## number of units in each fully connected hidden layer. The default is 10, ## one hidden layer of ten units. ## ## @item @qcode{'Activations'} @tab A character vector or cell array of ## character vectors specifying the activation of the hidden layers. The ## supported functions are @qcode{'linear'}, @qcode{'sigmoid'}, ## @qcode{'relu'}, @qcode{'tanh'}, @qcode{'lrelu'}, @qcode{'prelu'}, ## @qcode{'elu'}, @qcode{'gelu'} and @qcode{'none'}. The default is ## @qcode{'relu'}. ## ## @item @qcode{'OutputLayerActivation'} @tab A character vector specifying ## the activation of the output layer. The default is @qcode{'none'}, the ## identity, which is what a regression output calls for. The supported ## values are the same as for @qcode{'Activations'}. ## ## @item @qcode{'LearningRate'} @tab A positive scalar specifying the ## learning rate for gradient descent. The default is 0.003. A larger rate ## can drive every unit of a hidden layer negative, after which a rectifier ## passes no gradient and the network stops training. Applies only when ## @qcode{'Solver'} is @qcode{'sgd'}. ## ## @item @qcode{'Solver'} @tab A character vector naming the solver that ## trains the network, either @qcode{'lbfgs'} or @qcode{'sgd'}. The ## default is @qcode{'lbfgs'}, which minimizes the loss over the whole ## training set at once by limited-memory BFGS, as MATLAB does. It takes ## no learning rate, stops on the three tolerances below, and reaches a ## lower training loss in fewer passes over the data, though each of its ## iterations costs several passes where an epoch costs one. ## @qcode{'sgd'} visits the samples one at a time and steps down the ## gradient of each, running for @qcode{'IterationLimit'} epochs; it was ## the default before version 1.9.0. ## ## @item @qcode{'GradientTolerance'} @tab A nonnegative scalar. Training ## stops once the gradient's infinity norm falls to or below it, which is ## the quantity MATLAB tests too. The default is @qcode{1e-6}. Applies ## only when @qcode{'Solver'} is @qcode{'lbfgs'}. ## ## @item @qcode{'StepTolerance'} @tab A nonnegative scalar. Training ## stops once the step's infinity norm falls to or below it, which is the ## quantity MATLAB tests too. The default is @qcode{1e-6}. Applies only ## when @qcode{'Solver'} is @qcode{'lbfgs'}. ## ## @item @qcode{'LossTolerance'} @tab A real scalar. Training stops once ## the training loss falls to or below it. The test is on the loss ## itself and not on its change, matching MATLAB; pass @code{-Inf} to ## switch it off. The default is @qcode{1e-6}. Applies only when ## @qcode{'Solver'} is @qcode{'lbfgs'}. ## ## @item @qcode{'IterationLimit'} @tab A positive integer specifying the ## maximum number of training iterations. The default is 1000. ## Under @qcode{'sgd'} this counts epochs, under ## @qcode{'lbfgs'} solver iterations. ## ## @item @qcode{'DisplayInfo'} @tab A logical scalar specifying whether to ## print information during training. The default is @qcode{false}. ## @end multitable ## ## The supported values for @qcode{'ResponseTransform'} are: ## ## @multitable @columnfractions 0.3 0.7 ## @headitem @var{Value} @tab @var{Description} ## @item @qcode{'none'} @tab @math{x} (no transformation) ## @item @qcode{'identity'} @tab @math{x} (no transformation) ## @item @qcode{'exp'} @tab @math{exp (x)} ## @item @qcode{'log'} @tab @math{log (x)} ## @end multitable ## ## @seealso{fitrnet, ClassificationNeuralNetwork, fcnntrain, fcnnpredict} ## @end deftypefn function this = RegressionNeuralNetwork (X, Y, varargin) ## Check for sufficient number of input arguments if (nargin < 2) error ("RegressionNeuralNetwork: too few input arguments."); endif ## A table names its own predictors and says which hold levels [this, X, Y, varargin] = resolveTable (this, 'RegressionNeuralNetwork', ... X, Y, varargin); ## Check X and Y have the same number of observations if (rows (X) != rows (Y)) error (strcat ("RegressionNeuralNetwork: number of", ... " rows in X and Y must be equal.")); endif ## The response is continuous, so it must be numeric: a cellstr or a ## categorical response belongs to a classifier, and accepting one here ## would report a number for a question the model cannot answer. if (! (isnumeric (Y) && isreal (Y))) error (strcat ("RegressionNeuralNetwork: Y must be a", ... " real numeric vector.")); endif if (! (isvector (Y) || isempty (Y))) error ("RegressionNeuralNetwork: Y must be a vector."); endif ## Assign original X and Y data to the RegressionNeuralNetwork object this.X = X; this.Y = Y; ## Supported activation functions. 'none' is MATLAB's name for the ## identity and is what a regression output layer wants. acList = {'linear', 'none', 'sigmoid', 'relu', 'tanh', ... 'lrelu', 'prelu', 'elu', 'gelu'}; ## Parse optional paired arguments optNames = {'Standardize', 'PredictorNames', 'ResponseName', ... 'ResponseTransform', 'LayerSizes', 'LearningRate', ... 'Activations', 'OutputLayerActivation', 'IterationLimit', ... 'Solver', 'GradientTolerance', 'LossTolerance', ... 'StepTolerance', 'DisplayInfo', 'CategoricalPredictors', ... 'Weights'}; ## An empty default stands for one resolved once the data are known: ## 'PredictorNames' are x1, x2, ... and 'ResponseName' is 'Y'; no ## 'ResponseTransform' leaves the response as it is; 'LearningRate' is ## 0.003 and each tolerance 1e-6, empty so that giving one can be ## refused by name when the solver cannot use it. dfValues = {false, [], [], [], 10, [], 'relu', 'none', 1000, 'lbfgs', ... [], [], [], false, [], []}; [Standardize, PredictorNames, ResponseName, RTin, LayerSizes, ... LearningRate, Activations, OutputLayerActivation, IterationLimit, ... Solver, GradientTolerance, LossTolerance, StepTolerance, DisplayInfo, ... CatPreds, Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (Standardize == true || Standardize == false)) error (strcat ("RegressionNeuralNetwork: 'Standardize' must be", ... " either true or false.")); endif if (! isempty (PredictorNames) && ! iscellstr (PredictorNames)) error (strcat ("RegressionNeuralNetwork: 'PredictorNames' must be", ... " supplied as a cellstring array.")); elseif (! isempty (PredictorNames) && columns (PredictorNames) != columns (X)) error (strcat ("RegressionNeuralNetwork: 'PredictorNames' must", ... " have the same number of columns as X.")); endif if (! isempty (ResponseName) && ! ischar (ResponseName)) error (strcat ("RegressionNeuralNetwork: 'ResponseName' must be a", ... " character vector.")); endif if (! (isnumeric (LayerSizes) && isvector (LayerSizes) && all (LayerSizes > 0) && all (mod (LayerSizes, 1) == 0))) error (strcat ("RegressionNeuralNetwork: 'LayerSizes' must be a", ... " positive integer vector.")); endif if (! isempty (LearningRate) && ! (isnumeric (LearningRate) && isscalar (LearningRate) && LearningRate > 0)) error (strcat ("RegressionNeuralNetwork: 'LearningRate' must be a", ... " positive scalar.")); endif if (! (ischar (Activations) || iscellstr (Activations))) error (strcat ("RegressionNeuralNetwork: 'Activations' must be a", ... " character vector or a cellstring vector.")); endif if (ischar (Activations)) if (! any (strcmpi (Activations, acList))) error ("RegressionNeuralNetwork: unsupported 'Activation' function."); endif else if (! all (cell2mat (cellfun (@(x) any (strcmpi (x, acList)), Activations, 'UniformOutput', false)))) error (strcat ("RegressionNeuralNetwork: unsupported", ... " 'Activation' functions.")); endif endif Activations = tolower (Activations); if (! (ischar (OutputLayerActivation))) error (strcat ("RegressionNeuralNetwork: 'OutputLayerActivation'", ... " must be a character vector.")); endif if (! any (strcmpi (OutputLayerActivation, acList))) error (strcat ("RegressionNeuralNetwork: unsupported", ... " 'OutputLayerActivation' function.")); endif OutputLayerActivation = tolower (OutputLayerActivation); if (! (isnumeric (IterationLimit) && isscalar (IterationLimit) && (IterationLimit > 0) && mod (IterationLimit, 1) == 0)) error (strcat ("RegressionNeuralNetwork: 'IterationLimit' must be", ... " a positive integer.")); endif if (! (ischar (Solver) && any (strcmpi (Solver, {'sgd', ... 'lbfgs'})))) error (strcat ("RegressionNeuralNetwork: 'Solver' must be either", ... " 'sgd' or 'lbfgs'.")); endif Solver = tolower (Solver); if (! isempty (GradientTolerance) && ! (isnumeric (GradientTolerance) && isscalar (GradientTolerance) && GradientTolerance >= 0)) error (strcat ("RegressionNeuralNetwork: 'GradientTolerance' must", ... " be a nonnegative scalar.")); endif if (! isempty (LossTolerance) && ! (isnumeric (LossTolerance) && isscalar (LossTolerance) && ! isnan (LossTolerance))) error (strcat ("RegressionNeuralNetwork: 'LossTolerance' must be a", ... " real scalar.")); endif if (! isempty (StepTolerance) && ! (isnumeric (StepTolerance) && isscalar (StepTolerance) && StepTolerance >= 0)) error (strcat ("RegressionNeuralNetwork: 'StepTolerance' must be a", ... " nonnegative scalar.")); endif if (! (DisplayInfo == true || DisplayInfo == false)) error (strcat ("RegressionNeuralNetwork: 'DisplayInfo' must be", ... " either true or false.")); endif if (! isempty (RTin)) [this.RTfun, this.ResponseTransform] = ... parseResponseTransform (RTin, 'RegressionNeuralNetwork'); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("RegressionNeuralNetwork: %s", errmsg); endif if (! isempty (Weights) && ! (isvector (Weights) && numel (Weights) == rows (X))) error (strcat ("RegressionNeuralNetwork: 'Weights' must be a", ... " vector with one element per row of X.")); endif if (! isempty (Weights) && (any (Weights < 0) || ! (sum (Weights(! isnan (Weights))) > 0))) error (strcat ("RegressionNeuralNetwork: 'Weights' must be", ... " nonnegative and must not be all zero.")); endif if (! isempty (args)) error ("RegressionNeuralNetwork: invalid optional paired argument."); endif ## The solver-specific options the caller named, so that one meant for ## the other solver can be refused by name, then their defaults tolNames = {'GradientTolerance', 'LossTolerance', 'StepTolerance'}; GivenTols = tolNames(! cellfun (@isempty, {GradientTolerance, ... LossTolerance, ... StepTolerance})); LearningRateGiven = ! isempty (LearningRate); if (isempty (LearningRate)) LearningRate = 0.003; endif if (isempty (GradientTolerance)) GradientTolerance = 1e-6; endif if (isempty (LossTolerance)) LossTolerance = 1e-6; endif if (isempty (StepTolerance)) StepTolerance = 1e-6; endif ## Generate default predictors and response variable names (if necessary) NumPredictors = columns (X); if (isempty (PredictorNames)) for i = 1:NumPredictors PredictorNames {i} = strcat ("x", num2str (i)); endfor endif if (isempty (ResponseName)) ResponseName = 'Y'; endif ## Assign predictors and response variable names this.NumPredictors = NumPredictors; this.PredictorNames = PredictorNames; this.ExpandedPredictorNames = PredictorNames; this.ResponseName = ResponseName; ## An observation is dropped when its response is missing, or when its ## weight is zero or missing, as R2024a drops it; the weights keep their ## class in the model and every computation runs on them as double. A row ## whose predictors hold missing values is kept and reported as used, ## while the fit below draws on the complete observations alone. Wclass = "double"; Wall = ones (rows (Y), 1); if (! isempty (Weights)) Wclass = class (Weights); Wall = double (Weights(:)); endif RowsUsed = ! isnan (Y(:)) & ! isnan (Wall) & Wall > 0; wret = Wall(RowsUsed) / sum (Wall(RowsUsed)); Yret = Y(RowsUsed); Xret = X(RowsUsed, :); this.X = Xret; this.Y = Yret; cobs = ! any (isnan (Xret), 2); Y = Yret(cobs); X = Xret(cobs, :); wfit = wret(cobs); ## Dummy code the categorical predictors on the rows the fit draws on. ## X keeps the predictors as given; the fit and every prediction see ## the coded columns. cnames = PredictorNames; if (isempty (cnames)) cnames = arrayfun (@(k) sprintf ('x%d', k), 1:columns (X), ... 'UniformOutput', false); endif [Coding, errmsg] = dummyCoding (X, CatPreds, cnames); if (! isempty (errmsg)) error ("RegressionNeuralNetwork: %s", errmsg); endif ## What a row missing a predictor is predicted to be, as MATLAB R2024a ## predicts it: the lower median of the training response, every ## observation weighing the same here. this.MissingResponse_ = missingResponse (Y, wfit); X = dummyCoding (X, Coding); if (! isempty (Coding.Index)) this.CategoricalPredictors = Coding.Index; this.ExpandedPredictorNames = Coding.ExpandedNames; this.Coding_ = Coding; endif ## Check X and Y contain valid data if (! (isnumeric (X) && isfinite (X))) error ("RegressionNeuralNetwork: invalid values in X."); endif if (isempty (Y)) error ("RegressionNeuralNetwork: Y cannot be empty."); endif if (! all (isfinite (Y))) error ("RegressionNeuralNetwork: invalid values in Y."); endif ## Assign the number of observations and their corresponding indices ## on the original data, which will be used for training the model this.NumObservations = rows (this.X); ## RowsUsed is left empty when every observation was used, as in MATLAB if (all (RowsUsed)) this.RowsUsed = []; else this.RowsUsed = RowsUsed; endif ## Every observation carries the same weight this.W = cast (wret, Wclass); ## Handle the Standardize option. The network must be trained on the ## scale it predicts on, so X is transformed here as well as in ## predict. if (Standardize && ! isempty (Weights)) ## Weighted means and deviations, the deviation unbiased for the ## weights; a constant predictor is left unscaled. sw = wfit / sum (wfit); this.Mu = sum (sw .* X, 1); this.Sigma = sqrt (sum (sw .* (X - this.Mu) .^ 2, 1) ... / (1 - sum (sw .^ 2))); this.Sigma(this.Sigma == 0 | all (X == X(1,:), 1)) = 1; ## A level's column is left as it is, as in MATLAB R2024a. this.Mu(Coding.Dummy) = 0; this.Sigma(Coding.Dummy) = 1; X = (X - this.Mu) ./ this.Sigma; elseif (Standardize) this.Sigma = std (X, [], 1); this.Sigma(this.Sigma == 0) = 1; # predictor is constant this.Mu = mean (X, 1); ## A level's column is left as it is, as in MATLAB R2024a. this.Mu(Coding.Dummy) = 0; this.Sigma(Coding.Dummy) = 1; X = (X - this.Mu) ./ this.Sigma; else this.Sigma = []; this.Mu = []; endif ## An option that cannot act is refused rather than ignored. The three ## tolerances are how the lbfgs solver decides it has converged and mean ## nothing to the epoch loop, which runs to 'IterationLimit' whatever ## they say; a learning rate is what the epoch loop scales its step by ## and means nothing to a line search, which finds its own. if (strcmp (Solver, 'sgd') && ! isempty (GivenTols)) error (strcat ("RegressionNeuralNetwork: '", GivenTols{1}, ... "' applies only when 'Solver' is 'lbfgs'.")); endif if (strcmp (Solver, 'lbfgs') && LearningRateGiven) error (strcat ("RegressionNeuralNetwork: 'LearningRate'", ... " applies only when 'Solver' is 'sgd'.")); endif if (strcmp (Solver, 'lbfgs')) this.Solver = 'LBFGS'; else this.Solver = 'Gradient Descent'; endif ## Store training parameters this.LayerSizes = LayerSizes; this.Activations = Activations; this.OutputLayerActivation = OutputLayerActivation; this.LearningRate = LearningRate; this.IterationLimit = IterationLimit; this.DisplayInfo = DisplayInfo; ## Start the training process. LossFunction 2 is the mean squared error ## over a continuous response, which is what regression trains against. NumThreads = nproc (); rnn_timer_ = tic; SolverOptions = struct ('Solver', Solver, ... 'GradientTolerance', GradientTolerance, ... 'LossTolerance', LossTolerance, ... 'StepTolerance', StepTolerance); ## The loss weighs each observation by its weight, as R2024a weighs ## it; without weights it stays the plain mean. if (! isempty (Weights) && max (wfit) - min (wfit) > 1e-12 * max (wfit)) SolverOptions.Weights = wfit; endif ## The engine names the layers itself; this check stays here so the ## count is reported under the class rather than under fcnntrain. if (! ischar (Activations) && numel (LayerSizes) != numel (Activations)) error (strcat ("RegressionNeuralNetwork: 'Activations'", ... " vector does not match the number of layers.")); endif Mdl = fcnntrain (X, Y(:), LayerSizes, Activations, ... OutputLayerActivation, NumThreads, ... LearningRate, IterationLimit, DisplayInfo, 2, ... SolverOptions); ## The solver records a value per iteration. The series belongs to the ## history and ConvergenceInfo reports where the fit ended up, as ## MATLAB divides them; SERIES carries the columns to both. series = struct ('TrainingLoss', Mdl.Loss(:)); Mdl = rmfield (Mdl, 'Loss'); ## The lbfgs solver also reports the gradient and step it measured to ## decide it had stopped, and which test stopped it, as MATLAB does. criterion = ''; if (strcmp (Solver, 'lbfgs')) series.Gradient = Mdl.Gradient(:); series.Step = Mdl.Step(:); criterion = Mdl.Criterion; Mdl = rmfield (Mdl, {'Gradient', 'Step', 'Criterion'}); endif ConvergenceInfo = convergenceStruct_ (series, toc (rnn_timer_), ... criterion); ## The fit as it was asked for. What came out of it is the ## LayerWeights and LayerBiases properties, TrainingHistory and ## ConvergenceInfo; this structure holds what went in. The weight ## initializer of each layer is decided by that layer's activation ## inside the engine and cannot be chosen, so it is recorded rather ## than taken from an argument. initz = fcnnInitializers (this.Activations, numel (this.LayerSizes), ... this.OutputLayerActivation); this.ModelParameters = struct ( ... 'LayerSizes', this.LayerSizes, ... 'Activations', {this.Activations}, ... 'OutputLayerActivation', this.OutputLayerActivation, ... 'LayerWeightsInitializers', {initz}, ... 'Solver', this.Solver, ... 'LearningRate', this.LearningRate, ... 'IterationLimit', this.IterationLimit, ... 'GradientTolerance', GradientTolerance, ... 'LossTolerance', LossTolerance, ... 'StepTolerance', StepTolerance, ... 'DisplayInfo', logical (this.DisplayInfo), ... 'StandardizeData', logical (Standardize), ... 'Version', 1, 'Method', 'NeuralNetwork', ... 'Type', 'regression'); this.ConvergenceInfo = ConvergenceInfo; ## fcnntrain packs each neuron as [weights, bias] in one row, so the ## last column of every layer's matrix is its bias. nlay = numel (Mdl.LayerWeights); this.LayerWeights = cell (1, nlay); this.LayerBiases = cell (1, nlay); for i = 1:nlay Wb = Mdl.LayerWeights{i}; this.LayerWeights{i} = Wb(:, 1:end-1); this.LayerBiases{i} = Wb(:, end); endfor ## Iteration by iteration record of the fit this.TrainingHistory = trainingTable_ (series); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionNeuralNetwork} {@var{yFit} =} predict (@var{obj}, @var{XC}) ## ## Predict the response for new data with a neural network regression ## model. ## ## @code{@var{yFit} = predict (@var{obj}, @var{XC})} returns a column ## vector holding the predicted response for each row of @var{XC}, using ## the network stored in @var{obj}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{RegressionNeuralNetwork} class object. ## @item ## @var{XC} must be a numeric matrix with the same number of predictors as ## the data the model was trained on. ## @end itemize ## ## The transformation named by @code{ResponseTransform} is applied to the ## network's output before it is returned. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @seealso{RegressionNeuralNetwork, fitrnet} ## @end deftypefn function yFit = predict (this, XC) ## Check for sufficient input arguments if (nargin < 2) error ("RegressionNeuralNetwork.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'RegressionNeuralNetwork.predict', XC); ## Check for valid XC if (isempty (XC)) error ("RegressionNeuralNetwork.predict: XC is empty."); elseif (this.NumPredictors != columns (XC)) error (strcat ("RegressionNeuralNetwork.predict: XC must have", ... " the same number of predictors as the trained model.")); endif ## Code the categorical predictors and standardize (if necessary) if (! isempty (this.Coding_)) XC = dummyCoding (XC, this.Coding_); endif if (! isempty (this.Mu)) XC = (XC - this.Mu) ./ this.Sigma; endif ## The network's output is its second return value: the first is an ## index of the largest output, which a single regression unit makes ## constant and meaningless. NumThreads = nproc (); [~, yFit] = fcnnpredict (this.LayerWeights, this.LayerBiases, ... this.Activations, ... this.OutputLayerActivation, ... XC, NumThreads); ## A row missing a predictor, or holding a level the fit did not see, ## is predicted as MATLAB R2024a predicts it, not from a rectified ## missing value. yFit(any (isnan (XC), 2)) = this.MissingResponse_; ## Apply ResponseTransform yFit = this.RTfun (yFit); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionNeuralNetwork} {@var{yFit} =} resubPredict (@var{obj}) ## ## Predict the response of the training data with a neural network ## regression model. ## ## @code{@var{yFit} = resubPredict (@var{obj})} returns a column vector ## holding the predicted response for every observation the model was ## trained on, that is the rows of @code{obj.X} selected by ## @code{obj.RowsUsed}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{RegressionNeuralNetwork} class object. ## @end itemize ## ## @seealso{RegressionNeuralNetwork, fitrnet} ## @end deftypefn function yFit = resubPredict (this) ## Get used rows XC = this.X; ## Code the categorical predictors and standardize (if necessary) if (! isempty (this.Coding_)) XC = dummyCoding (XC, this.Coding_); endif if (! isempty (this.Mu)) XC = (XC - this.Mu) ./ this.Sigma; endif NumThreads = nproc (); [~, yFit] = fcnnpredict (this.LayerWeights, this.LayerBiases, ... this.Activations, ... this.OutputLayerActivation, ... XC, NumThreads); ## A row missing a predictor, or holding a level the fit did not see, ## is predicted as MATLAB R2024a predicts it, not from a rectified ## missing value. yFit(any (isnan (XC), 2)) = this.MissingResponse_; ## Apply ResponseTransform yFit = this.RTfun (yFit); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionNeuralNetwork} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {RegressionNeuralNetwork} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {RegressionNeuralNetwork} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {RegressionNeuralNetwork} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Compute the regression loss of a neural network model. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} returns the ## weighted mean squared error between the response @var{Y} and the ## response the model predicts for @var{X}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{RegressionNeuralNetwork} class object. ## @item ## @var{X} must be a numeric matrix with the same number of predictors as ## the data the model was trained on. ## @item ## @var{Y} must be a numeric vector with as many rows as @var{X}. ## @end itemize ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{L} = loss (@dots{}, @var{name}, @var{value})} accepts the ## following @qcode{Name-Value} pairs. ## ## @multitable @columnfractions 0.28 0.72 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'LossFun'} @tab @qcode{'mse'}, the default, or a function ## handle called as @code{@var{lossfun} (@var{Y}, @var{yFit}, @var{W})} ## and returning a scalar. ## ## @item @qcode{'Weights'} @tab A numeric vector of observation weights ## with one entry per row of @var{X}. It defaults to a uniform weight. ## The weights are normalized to sum to one before the loss is formed, so ## scaling them all by the same factor leaves the loss unchanged. ## @end multitable ## ## @seealso{RegressionNeuralNetwork, fitrnet} ## @end deftypefn function L = loss (this, X, Y, varargin) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("RegressionNeuralNetwork.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("RegressionNeuralNetwork.loss: Name-Value", ... " arguments must be in pairs.")); endif [X, Y] = checkXY_ (this, X, Y, 'loss'); ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights'}; dfValues = {'mse', []}; [LossFun, W, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (is_function_handle (LossFun) || (ischar (LossFun) && isrow (LossFun)))) error (strcat ("RegressionNeuralNetwork.loss: 'LossFun' must be a", ... " character vector or a function handle.")); endif if (ischar (LossFun) && ! strcmpi (LossFun, 'mse')) error ("RegressionNeuralNetwork.loss: unsupported 'LossFun' value."); endif errmsg = weightsClass (W); if (! isempty (errmsg)) error ("RegressionNeuralNetwork.loss: %s", errmsg); endif if (! isempty (W) && ! (isnumeric (W) && isvector (W))) error (strcat ("RegressionNeuralNetwork.loss: 'Weights' must be a", ... " numeric vector.")); endif if (! isempty (W) && numel (W) != rows (X)) error (strcat ("RegressionNeuralNetwork.loss: size of 'Weights'", ... " must equal the number of rows in X.")); endif if (! isempty (args)) error (strcat ("RegressionNeuralNetwork.loss: invalid optional", ... " paired argument.")); endif if (isempty (W)) W = ones (rows (X), 1); endif ## Weights are normalized to sum to one, as MATLAB does, so a loss is ## a weighted average rather than a weighted sum. W = double (W(:)); W = W / sum (W); yFit = predict (this, X); Y = Y(:); if (is_function_handle (LossFun)) L = LossFun (Y, yFit, W); if (! (isnumeric (L) && isscalar (L))) error (strcat ("RegressionNeuralNetwork.loss: 'LossFun' must", ... " return a numeric scalar.")); endif else L = sum (W .* (Y - yFit) .^ 2); endif endfunction ## -*- texinfo -*- ## @deftypefn {RegressionNeuralNetwork} {@var{L} =} resubLoss (@var{obj}) ## @deftypefnx {RegressionNeuralNetwork} {@var{L} =} resubLoss (@dots{}, @var{name}, @var{value}) ## ## Compute the resubstitution regression loss of a neural network model. ## ## @code{@var{L} = resubLoss (@var{obj})} returns the weighted mean ## squared error of the model on the data it was trained on. It accepts ## the same @qcode{Name-Value} pairs as @code{loss}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{RegressionNeuralNetwork} class object. ## @end itemize ## ## @seealso{RegressionNeuralNetwork, fitrnet} ## @end deftypefn function L = resubLoss (this, varargin) ## The model's own weights stand unless others are given, as in R2024a if (! any (strcmpi (varargin(1:2:end), 'Weights'))) varargin = [varargin, {'Weights', this.W}]; endif L = loss (this, this.X, this.Y, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionNeuralNetwork} {@var{CVMdl} =} crossval (@var{obj}) ## @deftypefnx {RegressionNeuralNetwork} {@var{CVMdl} =} crossval (@dots{}, @var{name}, @var{value}) ## ## Cross validate a neural network regression model. ## ## @code{@var{CVMdl} = crossval (@var{obj})} returns a ## @qcode{RegressionPartitionedModel} holding one refit of @var{obj} per ## fold of a ten-fold partition, or of an @math{n}-fold one where the ## model has fewer than ten observations. ## ## @itemize ## @item ## @var{obj} must be a @qcode{RegressionNeuralNetwork} class object. ## @end itemize ## ## @code{@var{CVMdl} = crossval (@dots{}, @var{name}, @var{value})} ## accepts one, and only one, of the following @qcode{Name-Value} pairs. ## ## @multitable @columnfractions 0.28 0.72 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'KFold'} @tab An integer greater than 1, the number of ## folds. ## @item @qcode{'Holdout'} @tab A scalar in @math{(0, 1)}, the fraction ## of observations held out for testing. ## @item @qcode{'Leaveout'} @tab @qcode{'on'} or @qcode{'off'}, whether ## to hold out one observation at a time. ## @item @qcode{'CVPartition'} @tab A @code{cvpartition} object over as ## many observations as the model was trained on. ## @end multitable ## ## @seealso{RegressionNeuralNetwork, RegressionPartitionedModel, ## cvpartition} ## @end deftypefn function CVMdl = crossval (this, varargin) if (numel (varargin) == 1) error (strcat ("RegressionNeuralNetwork.crossval: Name-Value", ... " arguments must be in pairs.")); elseif (numel (varargin) > 2) error (strcat ("RegressionNeuralNetwork.crossval: specify only", ... " one of the optional Name-Value paired arguments.")); endif if (this.NumObservations < 10) numFolds = this.NumObservations; else numFolds = 10; endif Holdout = []; Leaveout = 'off'; CVPartition = []; while (numel (varargin) > 0) switch (tolower (varargin {1})) case 'kfold' numFolds = varargin{2}; if (! (isnumeric (numFolds) && isscalar (numFolds) && (numFolds == fix (numFolds)) && numFolds > 1)) error (strcat ("RegressionNeuralNetwork.crossval: 'KFold'", ... " must be an integer value greater than 1.")); endif case 'holdout' Holdout = varargin{2}; if (! (isnumeric (Holdout) && isscalar (Holdout) && Holdout > 0 && Holdout < 1)) error (strcat ("RegressionNeuralNetwork.crossval: 'Holdout'", ... " must be a numeric value between 0 and 1.")); endif case 'leaveout' Leaveout = varargin{2}; if (! (ischar (Leaveout) && (strcmpi (Leaveout, 'on') || strcmpi (Leaveout, 'off')))) error (strcat ("RegressionNeuralNetwork.crossval: 'Leaveout'", ... " must be either 'on' or 'off'.")); endif case 'cvpartition' CVPartition = varargin{2}; if (! (isa (CVPartition, 'cvpartition'))) error (strcat ("RegressionNeuralNetwork.crossval:", ... " 'CVPartition' must be a 'cvpartition'", ... " object.")); endif otherwise error (strcat ("RegressionNeuralNetwork.crossval: invalid", ... " parameter name in optional paired arguments.")); endswitch varargin(1:2) = []; endwhile ## Determine the cross-validation method to use. The partition is ## built over the observations actually trained on, so its indices and ## the partitioned model's rows are the same set. n = this.NumObservations; if (! isempty (CVPartition)) partition = CVPartition; elseif (! isempty (Holdout)) partition = cvpartition (n, 'Holdout', Holdout); elseif (strcmpi (Leaveout, 'on')) partition = cvpartition (n, 'LeaveOut'); else partition = cvpartition (n, 'KFold', numFolds); endif ## Create a cross-validated model object CVMdl = RegressionPartitionedModel (this, partition); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionNeuralNetwork} {@var{CMdl} =} compact (@var{obj}) ## ## Create a @qcode{CompactRegressionNeuralNetwork} object. ## ## @code{@var{CMdl} = compact (@var{obj})} returns a compact version of ## the @qcode{RegressionNeuralNetwork} object @var{obj}, which keeps the ## trained network but drops the training data, so it predicts identically ## while carrying no observations. ## ## @seealso{fitrnet, RegressionNeuralNetwork, ## CompactRegressionNeuralNetwork} ## @end deftypefn function CMdl = compact (this) ## Create a compact model CMdl = CompactRegressionNeuralNetwork (this); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionNeuralNetwork} {} savemodel (@var{obj}, @var{filename}) ## ## Save a neural network regression model to a file. ## ## @code{savemodel (@var{obj}, @var{filename})} saves every property of ## the @qcode{RegressionNeuralNetwork} object @var{obj} into ## @var{filename} in binary format, so that it can be read back with ## @code{loadmodel}. ## ## @seealso{loadmodel, RegressionNeuralNetwork, fitrnet} ## @end deftypefn function savemodel (this, fname) if (nargin < 2) error ("RegressionNeuralNetwork.savemodel: too few input arguments."); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error (strcat ("RegressionNeuralNetwork.savemodel: FNAME must be", ... " a character vector.")); endif ## Generate variable for class name classdef_name = 'RegressionNeuralNetwork'; ## Create variables from model properties X = this.X; Y = this.Y; NumObservations = this.NumObservations; RowsUsed = this.RowsUsed; BinEdges = this.BinEdges; NumPredictors = this.NumPredictors; PredictorNames = this.PredictorNames; ResponseName = this.ResponseName; ResponseTransform = this.ResponseTransform; Sigma = this.Sigma; Mu = this.Mu; LayerSizes = this.LayerSizes; Activations = this.Activations; OutputLayerActivation = this.OutputLayerActivation; LearningRate = this.LearningRate; IterationLimit = this.IterationLimit; ModelParameters = this.ModelParameters; ConvergenceInfo = this.ConvergenceInfo; TrainingHistory = this.TrainingHistory; DisplayInfo = this.DisplayInfo; Solver = this.Solver; LayerWeights = this.LayerWeights; LayerBiases = this.LayerBiases; W = this.W; CategoricalPredictors = this.CategoricalPredictors; Coding_ = this.Coding_; MissingResponse_ = this.MissingResponse_; ExpandedPredictorNames = this.ExpandedPredictorNames; RTfun = this.RTfun; ## TrainingHistory is a table, and Octave cannot save a classdef object ## to a binary file, so it is left out here and rebuilt on loading from ## ConvergenceInfo, which holds the same numbers as a plain vector. ## Save classdef name and all model properties as individual variables ## The history is a table and ConvergenceInfo holds another; both lose ## their class on the way to the file and load_model rebuilds them, so ## the warning that says so is expected and is not shown. ws_ = warning ('off', 'Octave:save:classdef:unsupported'); unwind_protect HyperparameterOptimizationResults = this.HyperparameterOptimizationResults; save ('-binary', fname, 'classdef_name', 'X', 'Y', 'NumObservations', ... 'RowsUsed', 'BinEdges', 'NumPredictors', 'PredictorNames', ... 'ResponseName', ... 'ResponseTransform', 'Sigma', 'Mu', ... 'LayerSizes', 'Activations', 'OutputLayerActivation', ... 'LearningRate', 'IterationLimit', 'Solver', 'ModelParameters', ... 'ConvergenceInfo', 'TrainingHistory', 'DisplayInfo', ... 'LayerWeights', 'LayerBiases', ... 'W', 'CategoricalPredictors', 'Coding_', 'MissingResponse_', ... 'ExpandedPredictorNames', 'RTfun', ... 'HyperparameterOptimizationResults'); unwind_protect_cleanup warning (ws_); end_unwind_protect endfunction endmethods methods (Access = private) ## Shared validation for the assessment methods, so each reports under ## its own name. function [X, Y] = checkXY_ (this, X, Y, caller) if (isempty (X)) error ("RegressionNeuralNetwork.%s: X is empty.", caller); elseif (this.NumPredictors != columns (X)) error (strcat ("RegressionNeuralNetwork.%s: X must have the", ... " same number of predictors as the trained model."), ... caller); endif if (isempty (Y)) error ("RegressionNeuralNetwork.%s: Y is empty.", caller); elseif (! (isnumeric (Y) && isreal (Y))) error (strcat ("RegressionNeuralNetwork.%s: Y must be a real", ... " numeric vector."), caller); elseif (rows (X) != numel (Y)) error (strcat ("RegressionNeuralNetwork.%s: Y must have the", ... " same number of rows as X."), caller); endif endfunction endmethods methods(Static, Hidden) function mdl = load_model (filename, data) ## Create a RegressionNeuralNetwork object mdl = RegressionNeuralNetwork (1, 1); ## Get fieldnames from DATA (including private properties) names = fieldnames (data); ## The set methods for these read other properties, and one of them ## rebuilds Coeffs, so they are assigned once everything else is in ## place rather than in the order the file happens to list them. late = ismember (names, {'Cost', 'Prior', 'ScoreTransform', ... 'ResponseTransform'}); names = [names(! late); names(late)]; ## Copy data into object for i = 1:numel (names) ## Check fieldnames in DATA match properties in RegressionNeuralNetwork try mdl.(names{i}) = restore_tables (data.(names{i})); catch error (strcat ("RegressionNeuralNetwork.load_model:", ... " invalid model in '%s'."), filename) end_try_catch endfor ## Rebuild the TrainingHistory table, which savemodel cannot write out ## A model saved before the history was written out carries the series ## as vectors in ConvergenceInfo instead; rebuild from those, so that an ## older file still loads and loads as the current shape. if (isempty (mdl.TrainingHistory) && ! isempty (mdl.ConvergenceInfo)) mdl = restoreOlderModel_ (mdl); endif endfunction endmethods endclassdef ## Map an activation name to the code fcnntrain expects. 'none' and 'linear' ## are the same identity map; MATLAB spells the output layer's 'none'. ## The recorded history, whose columns follow the solver that produced it. ## Building it in one place keeps the fit and the model reloaded from disk ## from drifting apart. function T = trainingTable_ (series) iter = (1:numel (series.TrainingLoss))'; ## Time is NaN because neither solver records a per-iteration figure; the ## total the fit took is ConvergenceInfo.Time. The validation pair is NaN ## because no validation set can be given, which is what MATLAB reports ## when none is. Both are present so the columns are MATLAB's. pad = NaN (numel (iter), 1); if (isfield (series, 'Gradient')) T = table (iter, series.TrainingLoss(:), series.Gradient(:), ... series.Step(:), pad, pad, pad, 'VariableNames', ... {'Iteration', 'TrainingLoss', 'Gradient', 'Step', 'Time', ... 'ValidationLoss', 'ValidationChecks'}); else T = table (iter, series.TrainingLoss(:), pad, pad, pad, ... 'VariableNames', {'Iteration', 'TrainingLoss', 'Time', ... 'ValidationLoss', 'ValidationChecks'}); endif endfunction ## ConvergenceInfo reports where the fit ended: the last value of each series ## as a scalar, beside the whole series as History. MATLAB divides the two ## the same way, and a vector here would repeat what History already holds. function ci = convergenceStruct_ (series, elapsed, criterion) ## The fields are assigned in MATLAB's own order, which fieldnames reports. ci.Iterations = numel (series.TrainingLoss); ci.TrainingLoss = lastValue_ (series.TrainingLoss); if (isfield (series, 'Gradient')) ci.Gradient = lastValue_ (series.Gradient); ci.Step = lastValue_ (series.Step); endif ci.Time = elapsed; ci.ValidationLoss = NaN; ci.ValidationChecks = NaN; if (isfield (series, 'Gradient')) ci.ConvergenceCriterion = criterion; endif ci.History = trainingTable_ (series); endfunction ## Where the fit ended. A fit that took no iteration recorded nothing, so ## the value it ended at is empty rather than an error. function v = lastValue_ (x) if (isempty (x)) v = []; else v = x(end); endif endfunction ## The series as it was persisted, or, from a model saved before the series ## became a property of its own, rebuilt from the vectors ConvergenceInfo ## used to carry. ## Read a model written before ConvergenceInfo reported where the fit ended ## rather than the whole series: the vectors it carries are the history. function mdl = restoreOlderModel_ (mdl) ci = mdl.ConvergenceInfo; series = struct ('TrainingLoss', ci.TrainingLoss(:)); criterion = ''; if (isfield (ci, 'Gradient')) series.Gradient = ci.Gradient(:); series.Step = ci.Step(:); criterion = ci.ConvergenceCriterion; endif mdl.TrainingHistory = trainingTable_ (series); mdl.ConvergenceInfo = convergenceStruct_ (series, ci.Time, criterion); endfunction ## A fitted model carries the defaults MATLAB documents for fitrnet. %!test %! rand ('seed', 42); %! X = linspace (-1, 1, 40)'; %! Y = 2 * X + 0.5; %! Mdl = RegressionNeuralNetwork (X, Y, 'IterationLimit', 50); %! assert_equal (class (Mdl), 'RegressionNeuralNetwork'); %! assert_equal (Mdl.LayerSizes, 10); %! assert_equal (Mdl.Activations, 'relu'); %! assert_equal (Mdl.OutputLayerActivation, 'none'); %! assert_equal (Mdl.LearningRate, 0.003); %! assert_equal (Mdl.IterationLimit, 50); %! assert_equal (isempty (Mdl.Mu), true); %! assert_equal (Mdl.Solver, 'LBFGS'); %! assert_equal (Mdl.NumObservations, 40); %! assert_equal (Mdl.NumPredictors, 1); %! assert_equal (Mdl.ResponseName, 'Y'); %! assert_equal (Mdl.PredictorNames, {'x1'}); ## The output layer has one unit, and the identity leaves it unbounded, so a ## prediction is a real number rather than a score. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = linspace (-2, 2, 60)'; %! Y = 30 * X + 100; %! Mdl = RegressionNeuralNetwork (X, Y, 'IterationLimit', 400); %! assert_equal (rows (Mdl.LayerWeights{end}), 1); %! yFit = predict (Mdl, X); %! assert_equal (size (yFit), [60, 1]); %! assert_equal (max (yFit) > 50, true); ## A network recovers a smooth function to within the noise on it. %!test %! rand ('seed', 7); randn ('seed', 7); %! X = linspace (-2, 2, 80)'; %! Y = 3 * X.^2 - 1 + randn (80, 1) * 0.05; %! Mdl = RegressionNeuralNetwork (X, Y, 'LayerSizes', [12, 12], ... %! 'IterationLimit', 600); %! assert_equal (sqrt (resubLoss (Mdl)) < 0.2, true); ## The recorded history is the network's own loss, not a running average. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = linspace (0, 1, 50)'; %! Y = 4 * X - 2; %! Mdl = RegressionNeuralNetwork (X, Y, 'IterationLimit', 200, ... %! 'Solver', 'sgd'); %! h = Mdl.TrainingHistory; %! assert_equal (class (h), 'table'); %! assert_equal (h.Properties.VariableNames, ... %! {'Iteration', 'TrainingLoss', 'Time', 'ValidationLoss', ... %! 'ValidationChecks'}); %! assert_equal (rows (h), 200); %! assert_equal (h.Iteration', 1:200); %! assert_equal (h.TrainingLoss(end) < h.TrainingLoss(1), true); %! assert_equal (h.TrainingLoss(end), resubLoss (Mdl), 1e-12); ## ConvergenceInfo carries the same loss and the time the fit took. %!test %! rand ('seed', 42); %! X = linspace (0, 1, 30)'; %! Mdl = RegressionNeuralNetwork (X, 2 * X, 'Solver', 'sgd', ... %! 'IterationLimit', 25); %! assert_equal (fieldnames (Mdl.ConvergenceInfo), ... %! {'Iterations'; 'TrainingLoss'; 'Time'; ... %! 'ValidationLoss'; 'ValidationChecks'; 'History'}); %! assert_equal (numel (Mdl.ConvergenceInfo.TrainingLoss), 1); %! assert_equal (rows (Mdl.ConvergenceInfo.History), 25); %! assert_equal (Mdl.ConvergenceInfo.TrainingLoss, ... %! Mdl.ConvergenceInfo.History.TrainingLoss(end)); %! assert_equal (Mdl.ConvergenceInfo.Time > 0, true); ## predict on the training rows is resubPredict. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = [randn(30, 2); randn(30, 2) + 3]; %! Y = X(:,1) - 2 * X(:,2); %! Mdl = RegressionNeuralNetwork (X, Y, 'IterationLimit', 100); %! assert_equal (predict (Mdl, X), resubPredict (Mdl)); ## Standardize trains on the scale it predicts on. A model trained on raw ## data and asked about standardized data is not merely worse, it is wrong. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = [randn(60, 1), randn(60, 1) * 1000]; %! Y = X(:,1) + X(:,2) / 1000; %! Mdl = RegressionNeuralNetwork (X, Y, 'Standardize', true, ... %! 'IterationLimit', 300); %! assert_equal (size (Mdl.Mu), [1, 2]); %! assert_equal (size (Mdl.Sigma), [1, 2]); %! assert_equal (predict (Mdl, X), resubPredict (Mdl)); %! assert_equal (sqrt (resubLoss (Mdl)) < std (Y), true); ## A constant predictor gets a unit scale rather than a division by zero. %!test %! rand ('seed', 42); %! X = [linspace(0, 1, 20)', ones(20, 1)]; %! Mdl = RegressionNeuralNetwork (X, X(:,1), 'Standardize', true, ... %! 'IterationLimit', 50); %! assert_equal (Mdl.Sigma(2), 1); %! assert_equal (all (isfinite (resubPredict (Mdl))), true); ## Layer sizes and per-layer activations reach the network. %!test %! rand ('seed', 42); %! X = linspace (0, 1, 30)'; %! Mdl = RegressionNeuralNetwork (X, 2 * X, 'LayerSizes', [4, 6], ... %! 'Activations', {'tanh', 'sigmoid'}, ... %! 'IterationLimit', 20); %! assert_equal (Mdl.LayerSizes, [4, 6]); %! assert_equal (Mdl.Activations, {'tanh', 'sigmoid'}); %! assert_equal (numel (Mdl.LayerWeights), 3); %! assert_equal (size (Mdl.LayerWeights{1}), [4, 1]); %! assert_equal (size (Mdl.LayerWeights{2}), [6, 4]); %! assert_equal (size (Mdl.LayerWeights{3}), [1, 6]); %! assert_equal (Mdl.ModelParameters.Activations, {'tanh', 'sigmoid'}); %! assert_equal (Mdl.ModelParameters.OutputLayerActivation, 'none'); ## 'none' and 'linear' name the same identity output, which the two models ## predicting alike from the same seed is what the claim actually means. %!test %! X = linspace (0, 1, 20)'; %! rand ('seed', 42); %! M1 = RegressionNeuralNetwork (X, 2 * X, 'OutputLayerActivation', 'none', ... %! 'IterationLimit', 10); %! rand ('seed', 42); %! M2 = RegressionNeuralNetwork (X, 2 * X, 'OutputLayerActivation', ... %! 'linear', 'IterationLimit', 10); %! assert_equal (predict (M1, X), predict (M2, X)); ## Rows carrying a missing value are dropped from both X and Y. %!test %! rand ('seed', 42); %! X = [linspace(0, 1, 12)'; NaN; 0.5]; %! Y = [2 * linspace(0, 1, 12)'; 1; NaN]; %! Mdl = RegressionNeuralNetwork (X, Y, 'IterationLimit', 20); %! assert_equal (Mdl.NumObservations, 13); %! assert_equal (sum (Mdl.RowsUsed), 13); %! assert_equal (Mdl.RowsUsed(13:14), [true; false]); %! assert_equal (numel (resubPredict (Mdl)), 13); ## Observation weights default to a uniform weight summing to one. %!test %! rand ('seed', 42); %! X = linspace (0, 1, 25)'; %! Mdl = RegressionNeuralNetwork (X, 2 * X, 'IterationLimit', 10); %! assert_equal (size (Mdl.W), [25, 1]); %! assert_equal (sum (Mdl.W), 1, 1e-12); %! assert_equal (Mdl.W, ones (25, 1) / 25, 1e-12); ## loss defaults to the weighted mean squared error, and weights are ## normalized, so scaling every weight leaves the loss alone. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = linspace (0, 1, 30)'; %! Y = 3 * X + 1; %! Mdl = RegressionNeuralNetwork (X, Y, 'IterationLimit', 100); %! yFit = predict (Mdl, X); %! assert_equal (loss (Mdl, X, Y), mean ((Y - yFit) .^ 2), 1e-12); %! assert_equal (loss (Mdl, X, Y, 'LossFun', 'mse'), loss (Mdl, X, Y), 1e-12); %! w = rand (30, 1) + 0.1; %! assert_equal (loss (Mdl, X, Y, 'Weights', w), ... %! loss (Mdl, X, Y, 'Weights', 7 * w), 1e-12); %! assert_equal (loss (Mdl, X, Y, 'Weights', w), ... %! sum ((w / sum (w)) .* (Y - yFit) .^ 2), 1e-12); ## loss takes a function handle of the response, the fit and the weights. %!test %! rand ('seed', 42); %! X = linspace (0, 1, 20)'; %! Y = 2 * X; %! Mdl = RegressionNeuralNetwork (X, Y, 'IterationLimit', 50); %! f = @(y, yf, w) sum (w .* abs (y - yf)); %! yFit = predict (Mdl, X); %! assert_equal (loss (Mdl, X, Y, 'LossFun', f), ... %! mean (abs (Y - yFit)), 1e-12); ## resubLoss is loss on the training data. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = linspace (0, 1, 21)'; %! Y = [3 * linspace(0, 1, 20)'; NaN]; %! Mdl = RegressionNeuralNetwork (X, Y, 'IterationLimit', 80); %! Xu = X(Mdl.RowsUsed, :); %! Yu = Y(Mdl.RowsUsed); %! assert_equal (resubLoss (Mdl), loss (Mdl, Xu, Yu), 1e-12); %! assert_equal (resubLoss (Mdl, 'Weights', ones (20, 1)), ... %! loss (Mdl, Xu, Yu), 1e-12); ## ResponseTransform is applied to the prediction, by name or by handle. %!test %! rand ('seed', 42); %! X = linspace (0, 1, 20)'; %! Y = 2 * X + 1; %! Mdl = RegressionNeuralNetwork (X, Y, 'IterationLimit', 50); %! raw = predict (Mdl, X); %! Mdl.ResponseTransform = 'exp'; %! assert_equal (predict (Mdl, X), exp (raw), 1e-12); %! Mdl.ResponseTransform = @(y) 2 * y; %! assert_equal (predict (Mdl, X), 2 * raw, 1e-12); %! Mdl.ResponseTransform = 'none'; %! assert_equal (predict (Mdl, X), raw, 1e-12); ## The transform set at construction is the one predict uses. %!test %! rand ('seed', 42); %! X = linspace (0, 1, 20)'; %! Mdl = RegressionNeuralNetwork (X, 2 * X, 'ResponseTransform', 'identity', ... %! 'IterationLimit', 20); %! assert_equal (class (Mdl.ResponseTransform), 'char'); %! assert_equal (Mdl.ResponseTransform, 'none'); # identity is stored as none ## A saved model comes back carrying its own numbers. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = linspace (0, 1, 30)'; %! Y = 4 * X - 1; %! Mdl = RegressionNeuralNetwork (X, Y, 'LayerSizes', [6, 4], ... %! 'Solver', 'sgd', 'IterationLimit', 60); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2), 'RegressionNeuralNetwork'); %! assert_equal (M2.LayerWeights, Mdl.LayerWeights); %! assert_equal (M2.LayerBiases, Mdl.LayerBiases); %! assert_equal (M2.NumObservations, Mdl.NumObservations); %! assert_equal (M2.LayerSizes, Mdl.LayerSizes); %! assert_equal (M2.ResponseName, Mdl.ResponseName); %! assert_equal (M2.W, Mdl.W); %! assert_equal (predict (M2, X), predict (Mdl, X)); %! assert_equal (rows (M2.TrainingHistory), 60); %! assert_equal (M2.TrainingHistory.TrainingLoss, ... %! Mdl.TrainingHistory.TrainingLoss); ## compact drops the training data but predicts identically. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = [randn(40, 2); randn(40, 2) + 2]; %! Y = X(:,1) - X(:,2); %! Mdl = RegressionNeuralNetwork (X, Y, 'IterationLimit', 100); %! CMdl = compact (Mdl); %! assert_equal (class (CMdl), 'CompactRegressionNeuralNetwork'); %! assert_equal (predict (CMdl, X), predict (Mdl, X)); %! assert_equal (loss (CMdl, X, Y), loss (Mdl, X, Y)); ## crossval returns a partitioned model holding one fit per fold. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = randn (30, 2); %! Y = X(:,1) - X(:,2); %! Mdl = fitrnet (X, Y, 'IterationLimit', 20); %! CVMdl = crossval (Mdl, 'KFold', 3); %! assert_equal (class (CVMdl), 'RegressionPartitionedModel'); %! assert_equal (CVMdl.KFold, 3); %! assert_equal (CVMdl.CrossValidatedModel, 'NeuralNetwork'); %! assert_equal (numel (kfoldPredict (CVMdl)), 30); %! assert_equal (isfinite (kfoldLoss (CVMdl)), true); ## Test input validation for crossval ## The full-batch solver is selected by name and says so. %!test %! x = linspace (0, 1, 40)'; %! Mdl = fitrnet (x, sin (2*pi*x), "IterationLimit", 50, "Solver", "lbfgs"); %! assert_equal (Mdl.Solver, "LBFGS"); %! assert_equal (Mdl.TrainingHistory.Properties.VariableNames, ... %! {"Iteration", "TrainingLoss", "Gradient", "Step", ... %! "Time", "ValidationLoss", "ValidationChecks"}); ## It records what it measured to decide it had stopped. %!test %! x = linspace (0, 1, 40)'; %! Mdl = fitrnet (x, sin (2*pi*x), "IterationLimit", 50, "Solver", "lbfgs"); %! ci = Mdl.ConvergenceInfo; %! assert_equal (isfield (ci, "Gradient"), true); %! assert_equal (isfield (ci, "Step"), true); %! assert_equal (isfield (ci, "ConvergenceCriterion"), true); ## The stochastic solver is still reached by name, with its own two columns. %!test %! x = linspace (0, 1, 40)'; %! Mdl = fitrnet (x, sin (2*pi*x), "Solver", "sgd", "IterationLimit", 20); %! assert_equal (Mdl.Solver, "Gradient Descent"); %! assert_equal (columns (Mdl.TrainingHistory), 5); ## The default solver is lbfgs, and its history carries the four columns. %!test %! x = linspace (0, 1, 40)'; %! Mdl = fitrnet (x, sin (2*pi*x), "IterationLimit", 20); %! assert_equal (Mdl.Solver, "LBFGS"); %! assert_equal (Mdl.TrainingHistory.Properties.VariableNames, ... %! {"Iteration", "TrainingLoss", "Gradient", "Step", ... %! "Time", "ValidationLoss", "ValidationChecks"}); ## A model trained by lbfgs comes back off disk with its own four columns. %!test %! x = linspace (0, 1, 40)'; %! Mdl = fitrnet (x, sin (2*pi*x), "IterationLimit", 30, "Solver", "lbfgs"); %! fname = tempname (); %! savemodel (Mdl, fname); %! Mdl2 = loadmodel (fname); %! delete (fname); %! assert_equal (table2cell (Mdl2.TrainingHistory), ... %! table2cell (Mdl.TrainingHistory)); ## An option that cannot act is refused rather than ignored. %!test # A row missing a predictor predicts the lower median of the response %! X = [(1:10)', mod((1:10)', 3)]; %! Mdl = RegressionNeuralNetwork (X, (1:10)'); %! assert_equal (predict (Mdl, [NaN, 1]), 5); %!error ... %! fitrnet (ones (5, 2), [1; 2; 3; 4; 5], "Solver", "sgd", ... %! "GradientTolerance", 1e-8) %!error ... %! fitrnet (ones (5, 2), [1; 2; 3; 4; 5], "Solver", "lbfgs", "LearningRate", 0.1) %!error ... %! fitrnet (ones (5, 2), [1; 2; 3; 4; 5], "Solver", "bogus") %!error ... %! fitrnet (ones (5, 2), [1; 2; 3; 4; 5], "Solver", "lbfgs", "LossTolerance", NaN) %!error ... %! crossval (fitrnet (randn (12, 2), randn (12, 1), ... %! 'IterationLimit', 20), 'KFold') %!error ... %! crossval (fitrnet (randn (12, 2), randn (12, 1), ... %! 'IterationLimit', 20), ... %! 'KFold', 3, 'Leaveout', 'on') %!error ... %! crossval (fitrnet (randn (12, 2), randn (12, 1), ... %! 'IterationLimit', 20), 'KFold', 1) %!error ... %! crossval (fitrnet (randn (12, 2), randn (12, 1), ... %! 'IterationLimit', 20), 'Holdout', 1) %!error ... %! crossval (fitrnet (randn (12, 2), randn (12, 1), ... %! 'IterationLimit', 20), 'Leaveout', 1) %!error ... %! crossval (fitrnet (randn (12, 2), randn (12, 1), ... %! 'IterationLimit', 20), 'CVPartition', 1) %!error ... %! crossval (fitrnet (randn (12, 2), randn (12, 1), ... %! 'IterationLimit', 20), 'Nope', 1) ## Test input validation for the constructor %!error ... %! RegressionNeuralNetwork () %!error ... %! RegressionNeuralNetwork (ones (10, 2)) %!error ... %! RegressionNeuralNetwork (ones (10, 2), ones (5, 1)) %!error ... %! RegressionNeuralNetwork (ones (5, 2), {'a'; 'b'; 'c'; 'd'; 'e'}) %!error ... %! RegressionNeuralNetwork (ones (5, 2), complex (ones (5, 1))) %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 3)) %!error ... %! RegressionNeuralNetwork ([1; Inf; 3], [1; 2; 3]) %!error ... %! RegressionNeuralNetwork ([1; 2; 3], [1; Inf; 3]) %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), 'Standardize', 'yes') %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), 'PredictorNames', 'a') %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), 'PredictorNames', {'a'}) %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), 'ResponseName', 5) %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), 'ResponseTransform', 5) %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), ... %! 'ResponseTransform', 'nope') %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), ... %! 'ResponseTransform', @(y) [y; y]) %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), 'LayerSizes', -1) %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), 'LayerSizes', 2.5) %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), 'LearningRate', 0) %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), ... %! 'LearningRate', [0.1, 0.2]) %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), 'Activations', 5) %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), 'Activations', 'softmax') %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), ... %! 'Activations', {'relu', 'nope'}) %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), ... %! 'OutputLayerActivation', 5) %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), ... %! 'OutputLayerActivation', 'softmax') %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), 'IterationLimit', 0) %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), 'IterationLimit', 2.5) %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), 'DisplayInfo', 'yes') %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), 'Prior', 1) %!error ... %! RegressionNeuralNetwork (ones (5, 2), ones (5, 1), 'LayerSizes', [4, 4], ... %! 'Activations', {'relu', 'relu', 'relu'}) ## Test input validation for predict %!error ... %! predict (RegressionNeuralNetwork ([1; 2; 3], [1; 2; 3], ... %! 'IterationLimit', 5)) %!error ... %! predict (RegressionNeuralNetwork ([1; 2; 3], [1; 2; 3], ... %! 'IterationLimit', 5), []) %!error ... %! predict (RegressionNeuralNetwork ([1; 2; 3], [1; 2; 3], ... %! 'IterationLimit', 5), ones (2, 3)) ## Test input validation for loss %!shared RNNMdl %! rand ('seed', 42); %! RNNMdl = RegressionNeuralNetwork ([1; 2; 3; 4], [2; 4; 6; 8], ... %! 'IterationLimit', 10); %!error ... %! loss (RNNMdl) %!error ... %! loss (RNNMdl, [1; 2]) %!error ... %! loss (RNNMdl, [1; 2], [2; 4], 'Weights') %!error ... %! loss (RNNMdl, [], [2; 4]) %!error ... %! loss (RNNMdl, ones (2, 3), [2; 4]) %!error ... %! loss (RNNMdl, [1; 2], []) %!error ... %! loss (RNNMdl, [1; 2], {'a'; 'b'}) %!error ... %! loss (RNNMdl, [1; 2], [2; 4; 6]) %!error ... %! loss (RNNMdl, [1; 2], [2; 4], 'LossFun', 5) %!error ... %! loss (RNNMdl, [1; 2], [2; 4], 'LossFun', 'mae') %!error ... %! loss (RNNMdl, [1; 2], [2; 4], 'LossFun', @(y, yf, w) [1, 2]) %!error ... %! loss (RNNMdl, [1; 2], [2; 4], 'Weights', {'a'}) %!error ... %! loss (RNNMdl, [1; 2], [2; 4], 'Weights', ones (2, 2)) %!error ... %! loss (RNNMdl, [1; 2], [2; 4], 'Weights', [1; 2; 3]) %!error ... %! loss (RNNMdl, [1; 2], [2; 4], 'Nope', 1) %!error ... %! loss (RNNMdl, [1; 2], [2; 4], 5, 1) ## Test input validation for savemodel %!error ... %! savemodel (RNNMdl) %!error ... %! savemodel (RNNMdl, 5) %!error ... %! RNNMdl.ResponseTransform = 'nope'; ## RowsUsed is empty when every observation was used. %!test %! load fisheriris %! X = meas(:,2:4); %! Y = meas(:,1); %! Mdl = fitrnet (X, Y, 'IterationLimit', 20); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (class (Mdl.RowsUsed), 'double'); %! assert_equal (Mdl.NumObservations, 150); %! assert_equal (rows (Mdl.X), 150); %! assert_equal (rows (Mdl.W), 150); ## A missing response drops its observation and RowsUsed marks it. %!test %! load fisheriris %! X = meas(:,2:4); %! Y = meas(:,1); %! Y(5) = NaN; %! Mdl = fitrnet (X, Y, 'IterationLimit', 20); %! assert_equal (class (Mdl.RowsUsed), 'logical'); %! assert_equal (size (Mdl.RowsUsed), [150, 1]); %! assert_equal (sum (Mdl.RowsUsed), 149); %! assert_equal (Mdl.RowsUsed(5), false); %! assert_equal (Mdl.NumObservations, 149); %! assert_equal (rows (Mdl.X), 149); %! assert_equal (rows (Mdl.W), 149); ## A missing predictor keeps its observation, so RowsUsed stays empty. %!test %! load fisheriris %! X = meas(:,2:4); %! X(3,2) = NaN; %! Y = meas(:,1); %! Mdl = fitrnet (X, Y, 'IterationLimit', 20); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (Mdl.NumObservations, 150); %! assert_equal (rows (Mdl.X), 150); %! assert_equal (sum (isnan (Mdl.X(:))), 1); ## Standardizing summarizes the complete observations. With no classes the ## weights are uniform over them. Values from MATLAB R2024a. %!test %! load fisheriris %! X = meas(:,2:4); %! X(7,2) = NaN; X(120,3) = NaN; %! Mdl = fitrnet (X, meas(:,1), 'Standardize', true, 'IterationLimit', 20); %! assert_equal (Mdl.Mu, [3.0608108108108096, 3.7655405405405395, ... %! 1.203378378378378], 1e-13); %! assert_equal (Mdl.Sigma, [0.43214937187299296, 1.7636045663278643, ... %! 0.76339873235638711], 1e-13); ## A fitted model survives savemodel and loadmodel: the properties come ## back as they were and it predicts the same. %!test %! load fisheriris %! X = meas(:,2:4); %! Y = meas(:,1); %! Mdl = fitrnet (X, Y, 'IterationLimit', 20); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2), 'RegressionNeuralNetwork'); %! assert_equal (M2.NumObservations, Mdl.NumObservations); %! assert_equal (M2.PredictorNames, Mdl.PredictorNames); %! assert_equal (class (M2.ResponseTransform), class (Mdl.ResponseTransform)); %! assert_equal (predict (M2, X(1:5,:)), predict (Mdl, X(1:5,:)), 1e-12); ## BinEdges is an empty cell, which is what MATLAB reports for this ## learner as well: it fits the predictors as they are. %!test %! load fisheriris %! Mdl = fitrnet (meas(:,1:3), meas(:,4), 'IterationLimit', 10); %! assert_equal (class (Mdl.BinEdges), 'cell'); %! assert_equal (Mdl.BinEdges, {}); ## HyperparameterOptimizationResults is declared for MATLAB compatibility and ## stays empty, this class running no search over its hyperparameters. %!test %! load fisheriris %! Mdl = fitrnet (meas(:,1:3), meas(:,4), 'IterationLimit', 20); %! assert_equal (isempty (Mdl.HyperparameterOptimizationResults), true); ## ModelParameters records the fit as it was asked for, not the network that ## came out of it. %!test %! load fisheriris %! Mdl = fitrnet (meas(:,2:4), meas(:,1)); %! assert_equal (fieldnames (Mdl.ModelParameters)', {'LayerSizes', ... %! 'Activations', 'OutputLayerActivation', 'LayerWeightsInitializers', ... %! 'Solver', 'LearningRate', 'IterationLimit', 'GradientTolerance', ... %! 'LossTolerance', 'StepTolerance', 'DisplayInfo', 'StandardizeData', ... %! 'Version', 'Method', 'Type'}); %!test %! load fisheriris %! MP = fitrnet (meas(:,2:4), meas(:,1)).ModelParameters; %! assert_equal (MP.LayerSizes, 10); %! assert_equal (MP.Activations, 'relu'); %! assert_equal (MP.OutputLayerActivation, 'none'); %! assert_equal (MP.Version, 1); %! assert_equal (MP.Method, 'NeuralNetwork'); %! assert_equal (MP.Type, 'regression'); ## A per-layer Activations stays a cellstr rather than splitting the ## structure into an array. %!test %! load fisheriris %! MP = fitrnet (meas(:,2:4), meas(:,1), 'LayerSizes', [4, 4], ... %! 'Activations', {'gelu', 'sigmoid'}).ModelParameters; %! assert_equal (size (MP), [1, 1]); %! assert_equal (MP.Activations, {'gelu', 'sigmoid'}); %! assert_equal (MP.LayerWeightsInitializers, {'he', 'glorot', 'glorot'}); ## ModelParameters reports the weight initializer each layer was built with, ## the output layer last. The engine picks it from the activation and it ## cannot be chosen, so the report is the only way to see it. %!test %! load fisheriris %! MP = fitrnet (meas(:,2:4), meas(:,1)).ModelParameters; %! assert_equal (MP.LayerWeightsInitializers, {'he', 'glorot'}); ## The output layer defaults to 'none' here, which is symmetric and takes ## Glorot however many rectifying layers precede it. %!test %! load fisheriris %! Mdl = fitrnet (meas(:,2:4), meas(:,1), 'LayerSizes', [4, 4, 4], ... %! 'Activations', 'gelu'); %! assert_equal (Mdl.ModelParameters.LayerWeightsInitializers, ... %! {'he', 'he', 'he', 'glorot'}); ## Every documented response transform reaches the response that is reported. %!test %! load fisheriris %! Mdl = fitrnet (meas(:,2:4), meas(:,1)); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! T = {'identity', @(x) x; 'exp', @(x) exp (x); 'log', @(x) log (x)}; %! for i = 1:rows (T) %! Mdl.ResponseTransform = T{i,1}; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, T{i,2}(raw), 1e-12); %! endfor ## A function handle is taken as given and applied to the response. %!test %! load fisheriris %! Mdl = fitrnet (meas(:,2:4), meas(:,1)); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! Mdl.ResponseTransform = @(x) x .^ 2; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, raw .^ 2, 1e-12); %!shared Xc, Dc, yr, Xq, Dq %! k = (0:119)'; %! c1 = mod (k, 3) + 1; %! x2 = sin (k); %! c3 = 10 * (mod (floor (k / 2), 2) + 1); %! Xc = [c1, x2, c3]; %! Dc = [c1 == 1, c1 == 2, c1 == 3, x2, c3 == 10, c3 == 20]; %! yr = 5 * (c1 == 2) + 0.5 * x2 - 3 * (c3 == 20) + 0.1 * cos (k); %! yc = yr > 1; %! Xq = [1, 0, 10; 2, 0.5, 20]; %! Dq = [1, 0, 0, 0, 1, 0; 0, 1, 0, 0.5, 0, 1]; %!test # MATLAB parity: a categorical predictor is dummy coded in its place %! rand ('seed', 1); %! randn ('seed', 1); %! Mdl = RegressionNeuralNetwork (Xc, yr, 'CategoricalPredictors', [1, 3], ... %! 'LayerSizes', 4); %! rand ('seed', 1); %! randn ('seed', 1); %! H = RegressionNeuralNetwork (Dc, yr, 'LayerSizes', 4); %! assert_equal (Mdl.ExpandedPredictorNames, {'x1 == 1', 'x1 == 2', ... %! 'x1 == 3', 'x2', 'x3 == 10', 'x3 == 20'}); %! assert_equal (Mdl.LayerWeights{1}, H.LayerWeights{1}, 1e-12); %! assert_equal (predict (Mdl, Xq), predict (H, Dq), 1e-12); %! assert_equal (resubPredict (Mdl), resubPredict (H), 1e-12); %!test # a level the fit did not see, or a missing value, predicts the median %! Mdl = RegressionNeuralNetwork (Xc, yr, 'CategoricalPredictors', [1, 3], ... %! 'LayerSizes', 4); %! yhat = predict (Mdl, [4, 0, 10; 1, NaN, 10; 1, 0, 10]); %! ys = sort (yr); %! assert_equal (yhat(1:2), repmat (ys(ceil (numel (ys) / 2)), 2, 1)); %! assert_equal (isnan (yhat(3)), false); %!test # MATLAB parity: the coded columns are not standardized %! Mdl = RegressionNeuralNetwork (Xc, yr, 'CategoricalPredictors', [1, 3], ... %! 'LayerSizes', 4, 'Standardize', true); %! assert_equal (Mdl.Mu([1:3, 5:6]), zeros (1, 5)); %! assert_equal (Mdl.Sigma([1:3, 5:6]), ones (1, 5)); %!test # the coding travels with saved models and cross-validation folds %! Mdl = RegressionNeuralNetwork (Xc, yr, 'CategoricalPredictors', [1, 3], ... %! 'LayerSizes', 4); %! fname = [tempname(), '.mdl']; %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (predict (M2, Xq), predict (Mdl, Xq)); %! CV = crossval (Mdl, 'KFold', 3); %! assert_equal (CV.Trained{1}.CategoricalPredictors, [1, 3]); %!error ... %! RegressionNeuralNetwork (Xc, yr, 'CategoricalPredictors', 4) ## A table at prediction %!test # the class may be built from a table as the fitter builds it %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'SL'}); %! Mdl = RegressionNeuralNetwork (T, 'SL'); %! assert_equal (Mdl.PredictorNames, {'SW', 'PL'}); %! assert_equal (Mdl.ResponseName, 'SL'); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,2:3); %! y = meas(:,1); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SW', 'PL'}); %! T.SL = y; %! Mdl = fitrnet (T, 'SL'); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'SL'), a); %! assert_equal (loss (Mdl, T), a); ## Observation weights %!error ... %! RegressionNeuralNetwork ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', ... %! 'Weights', int8 ([1; 1; 1; 1])) %!error ... %! RegressionNeuralNetwork ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', ... %! 'Weights', true (4, 1)) %!error ... %! RegressionNeuralNetwork ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', ... %! 'Weights', [1; 1]) %!error ... %! RegressionNeuralNetwork ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', ... %! 'Weights', [1; -1; 1; 1]) %!test %! ## A weight of two fits as the observation given twice %! load fisheriris %! X = meas(51:130,2:4); %! y = meas(51:130,1); %! w = ones (80, 1); %! w(1:10) = 2; %! rand ('seed', 1); %! A = RegressionNeuralNetwork (X, y, 'LayerSizes', 1, 'Weights', w); %! rand ('seed', 1); %! B = RegressionNeuralNetwork ([X; X(1:10,:)], [y; y(1:10)], ... %! 'LayerSizes', 1); %! assert_equal (predict (A, X), predict (B, X), 1e-10); %!test %! ## Single weights are stored single, summing to one %! load fisheriris %! Mdl = RegressionNeuralNetwork (meas(:,2:4), meas(:,1), 'LayerSizes', 3, ... %! 'Weights', single (1 + (1:150)' / 7)); %! assert_equal (class (Mdl.W), 'single'); %! assert_equal (sum (double (Mdl.W)), 1, 1e-6); %!test %! ## Standardization weighs the observations, as R2024a does %! load fisheriris %! Mdl = RegressionNeuralNetwork (meas(:,2:4), meas(:,1), 'LayerSizes', 3, ... %! 'Weights', 1 + (1:150)' / 7, ... %! 'Standardize', true); %! assert_equal (Mdl.Mu, [2.965608080808081, 4.573050505050505, ... %! 1.55819797979798], 1e-14); %! assert_equal (Mdl.Sigma, [0.3809861139391035, 1.433169957562235, ... %! 0.6470320013330532], 1e-14); %!test %! ## resubLoss weighs the observations by W unless given weights %! load fisheriris %! w = 1 + (1:150)' / 7; %! Mdl = RegressionNeuralNetwork (meas(:,2:4), meas(:,1), 'LayerSizes', 3, ... %! 'Weights', w); %! assert_equal (resubLoss (Mdl), ... %! loss (Mdl, meas(:,2:4), meas(:,1), 'Weights', w), 1e-15); statistics-release-2.1.2/inst/Machine_Learning/RegressionPartitionedEnsemble.m000066400000000000000000000514301527311104300276400ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef RegressionPartitionedEnsemble ## -*- texinfo -*- ## @deftp {statistics} RegressionPartitionedEnsemble ## ## Cross-validated ensemble of regression trees ## ## A @code{RegressionPartitionedEnsemble} object holds one ensemble per fold ## of a partition, each fitted on the observations the fold keeps for ## training, and predicts every observation with the ensemble of the fold ## that held it out. ## ## Create one with @code{fitrensemble} given a cross-validation option, or ## with the @code{crossval} method of a @code{RegressionEnsemble} or ## @code{RegressionBaggedEnsemble}. ## ## @seealso{fitrensemble, RegressionEnsemble.crossval, ## CompactRegressionEnsemble, cvpartition} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {RegressionPartitionedEnsemble} {property} Trainable ## ## The full ensemble of each fold ## ## A column cell array with one @code{RegressionEnsemble} or ## @code{RegressionBaggedEnsemble} per fold, which @code{resume} grows. ## This property is read-only. ## ## @end deftp Trainable = {}; ## -*- texinfo -*- ## @deftp {RegressionPartitionedEnsemble} {property} NumTrainedPerFold ## ## Number of trees in each fold ## ## A row vector. This property is read-only. ## ## @end deftp NumTrainedPerFold = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedEnsemble} {property} CrossValidatedModel ## ## Method of the cross-validated ensemble ## ## @qcode{'LSBoost'} or @qcode{'Bag'}. This property is read-only. ## ## @end deftp CrossValidatedModel = ''; ## -*- texinfo -*- ## @deftp {RegressionPartitionedEnsemble} {property} PredictorNames ## ## Names of the predictors ## ## This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {RegressionPartitionedEnsemble} {property} CategoricalPredictors ## ## Indices of categorical predictors ## ## Always empty. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedEnsemble} {property} ResponseName ## ## Name of the response variable ## ## This property is read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {RegressionPartitionedEnsemble} {property} NumObservations ## ## Number of observations ## ## This property is read-only. ## ## @end deftp NumObservations = 0; ## -*- texinfo -*- ## @deftp {RegressionPartitionedEnsemble} {property} X ## ## Predictor data ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## This property is read-only. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedEnsemble} {property} Y ## ## Response data ## ## This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedEnsemble} {property} W ## ## Observation weights ## ## The weights of the ensemble that was cross-validated, by which the ## losses weigh the held-out observations. This property is read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedEnsemble} {property} ModelParameters ## ## Parameters of the cross-validation ## ## A structure with the fields @qcode{Type}, @qcode{Method}, ## @qcode{'PartitionedEnsemble'}, @qcode{LearnerTemplates}, the ## @code{ModelParameters} of the ensemble the folds were fitted as, and ## @qcode{NLearn}, the number of folds. This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedEnsemble} {property} Trained ## ## The compact ensemble of each fold ## ## A column cell array with one @code{CompactRegressionEnsemble} per fold. ## This property is read-only. ## ## @end deftp Trained = {}; ## -*- texinfo -*- ## @deftp {RegressionPartitionedEnsemble} {property} KFold ## ## Number of folds ## ## This property is read-only. ## ## @end deftp KFold = 0; ## -*- texinfo -*- ## @deftp {RegressionPartitionedEnsemble} {property} Partition ## ## The partition of the observations ## ## A @code{cvpartition} object. This property is read-only. ## ## @end deftp Partition = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedEnsemble} {property} BinEdges ## ## Bin edges of the predictors ## ## Always empty. This property is read-only. ## ## @end deftp BinEdges = {}; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {RegressionPartitionedEnsemble} {property} ResponseTransform ## ## Transform applied to the out-of-fold predictions ## ## The folds carry none; this one is applied once to what they predict. ## MATLAB R2024a leaves the transform on the folds as well and so applies ## it twice. ## ## @end deftp ResponseTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) RTfun = @(y) y; # the response transform as a function endproperties methods (Hidden) function this = set.ResponseTransform (this, val) [this.RTfun, this.ResponseTransform] = ... parseResponseTransform (val, 'RegressionPartitionedEnsemble'); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionPartitionedEnsemble} {@var{CVMdl} =} RegressionPartitionedEnsemble (@var{Mdl}, @var{Partition}) ## ## Cross-validate a regression ensemble. ## ## @var{Mdl} is the ensemble to cross-validate and @var{Partition} a ## @code{cvpartition} of its observations. The documented ways in are ## @code{fitrensemble} and @code{crossval}. ## ## @end deftypefn function this = RegressionPartitionedEnsemble (Mdl, Partition) if (nargin < 2) error (strcat ("RegressionPartitionedEnsemble: too few input", ... " arguments.")); endif if (! isa (Mdl, 'RegressionEnsemble')) error (strcat ("RegressionPartitionedEnsemble: MDL must be a", ... " 'RegressionEnsemble' object.")); endif if (! isa (Partition, 'cvpartition')) error (strcat ("RegressionPartitionedEnsemble: PARTITION must be a", ... " 'cvpartition' object.")); endif if (numel (test (Partition, 1)) != Mdl.NumObservations) error (strcat ("RegressionPartitionedEnsemble: PARTITION must", ... " partition the observations MDL was fitted on.")); endif this.CrossValidatedModel = Mdl.Method; this.PredictorNames = Mdl.PredictorNames; this.CategoricalPredictors = Mdl.CategoricalPredictors; this.ResponseName = Mdl.ResponseName; this.NumObservations = Mdl.NumObservations; this.X = Mdl.X; this.Y = Mdl.Y; this.W = Mdl.W; this.Partition = Partition; this.KFold = Partition.NumTestSets; this.ModelParameters = struct ('Type', 'regression', ... 'Method', 'PartitionedEnsemble', ... 'LearnerTemplates', ... Mdl.ModelParameters, ... 'NLearn', this.KFold); if (any (strcmp (Mdl.ResponseTransform, {'none', 'exp', 'log'}))) this.ResponseTransform = Mdl.ResponseTransform; else this.ResponseTransform = Mdl.RTfun; endif mp = Mdl.ModelParameters; fargs = {'Method', Mdl.Method, 'NumLearningCycles', mp.NLearn, ... 'Learners', mp.LearnerTemplates, ... 'PredictorNames', Mdl.PredictorNames, ... 'ResponseName', Mdl.ResponseName}; if (isa (Mdl, 'RegressionBaggedEnsemble')) onoff = {'off', 'on'}; fargs(end+1:end+4) = {'FResample', Mdl.FResample, ... 'Replace', onoff{Mdl.Replace + 1}}; endif if (strcmp (Mdl.Method, 'LSBoost')) fargs(end+1:end+2) = {'LearnRate', mp.LearnRate}; endif ## Assigned field by field: struct () given a cell array of labels ## would build one structure per label. F = struct (); F.X = Mdl.X; F.Y = Mdl.Y; F.Weights = Mdl.W; if (! isempty (Mdl.CategoricalPredictors)) fargs(end+1:end+2) = {'CategoricalPredictors', ... Mdl.CategoricalPredictors}; endif this.Trainable = foldModels (class (Mdl), F, Partition, fargs); this = compactFolds (this); endfunction function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ('%s =\n', in_name); endif disp (this); endfunction function disp (this) fprintf ("\n RegressionPartitionedEnsemble\n\n"); fprintf ("%+25s: '%s'\n", 'CrossValidatedModel', ... this.CrossValidatedModel); fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); fprintf ("%+25s: %d\n", 'NumObservations', this.NumObservations); fprintf ("%+25s: %d\n", 'KFold', this.KFold); fprintf ("%+25s: %s\n", 'NumTrainedPerFold', ... mat2str (this.NumTrainedPerFold)); fprintf ("%+25s: '%s'\n", 'ResponseTransform', this.ResponseTransform); fprintf ("\n"); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {RegressionPartitionedEnsemble} {@var{yfit} =} kfoldPredict (@var{obj}) ## ## Predict each observation with the fold that held it out. ## ## @var{yfit} holds each observation's prediction from the ensemble of the ## fold that held it out, after @code{ResponseTransform}, and @code{NaN} ## for an observation no fold held out. ## ## @seealso{RegressionPartitionedEnsemble, ## RegressionPartitionedEnsemble.kfoldLoss} ## @end deftypefn function yfit = kfoldPredict (this) yfit = this.RTfun (foldResponse (this, 1:this.KFold)); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionPartitionedEnsemble} {@var{L} =} kfoldLoss (@var{obj}) ## @deftypefnx {RegressionPartitionedEnsemble} {@var{L} =} kfoldLoss (@dots{}, @var{name}, @var{value}) ## ## Cross-validated regression loss. ## ## @var{L} is the weighted mean squared error of the out-of-fold ## predictions, the held-out observations weighted by @code{W}. ## ## Name-Value arguments: ## ## @multitable @columnfractions 0.2 0.02 0.78 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Folds'} @tab @tab The folds to use, pooled. The default ## is all of them. ## @item @qcode{'LossFun'} @tab @tab @qcode{'mse'} (default) or a function ## handle called as @code{lossfun (Y, Yfit, W)} with normalized weights. ## @item @qcode{'Mode'} @tab @tab @qcode{'average'} (default) for one loss ## over the observations of every fold used, @qcode{'individual'} for a ## column with the loss of each fold, or @qcode{'cumulative'} for a column ## whose element @math{t} uses the first @math{t} trees of every fold. ## @end multitable ## ## @seealso{RegressionPartitionedEnsemble, ## RegressionPartitionedEnsemble.kfoldPredict} ## @end deftypefn function L = kfoldLoss (this, varargin) o = kfoldOpts (varargin, {'mse'}, 'RegressionPartitionedEnsemble', ... 'kfoldLoss', this.KFold, ... {'average', 'individual', 'cumulative'}, true); LossFun = o.LossFun; if (isempty (LossFun)) LossFun = 'mse'; endif Folds = o.Folds; Mode = o.Mode; n = this.NumObservations; if (! strcmp (Mode, 'cumulative')) sets = foldSets (this.Partition, Folds, Mode, n); yf = foldResponse (this, Folds); L = zeros (numel (sets), 1); for i = 1:numel (sets) L(i) = setLoss (this, yf, sets{i}, LossFun); endfor return; endif ## The cumulative mode pools the folds used and grows every fold's ## trees together. pooled = foldSets (this.Partition, Folds, 'average', n){1}; T = max ([0, this.NumTrainedPerFold(Folds)]); Yf = NaN (n, T); for k = Folds te = test (this.Partition, k); Tk = this.NumTrainedPerFold(k); if (any (te) && Tk > 0) Yk = ensembleSteps (this.Trained{k}, this.X(te,:)); Yf(te,:) = [Yk, repmat(Yk(:,end), 1, T - Tk)]; endif endfor L = zeros (T, 1); for t = 1:T L(t) = setLoss (this, Yf(:,t), pooled, LossFun); endfor endfunction ## -*- texinfo -*- ## @deftypefn {RegressionPartitionedEnsemble} {@var{vals} =} kfoldfun (@var{obj}, @var{fun}) ## ## Apply a function to each fold. ## ## @var{fun} is called once per fold as ## @code{@var{fun} (CMP, Xtrain, Ytrain, Wtrain, Xtest, Ytest, Wtest)}, ## @var{CMP} being the fold's compact ensemble and the weights those of ## @code{W}, and must return a row. @var{vals} stacks the rows. ## ## @seealso{RegressionPartitionedEnsemble} ## @end deftypefn function vals = kfoldfun (this, fun) if (nargin < 2) error (strcat ("RegressionPartitionedEnsemble.kfoldfun: too few", ... " input arguments.")); endif if (! is_function_handle (fun)) error (strcat ("RegressionPartitionedEnsemble.kfoldfun: FUN must", ... " be a function handle.")); endif vals = []; for k = 1:this.KFold tr = training (this.Partition, k); te = test (this.Partition, k); v = fun (this.Trained{k}, this.X(tr,:), this.Y(tr), this.W(tr), ... this.X(te,:), this.Y(te), this.W(te)); vals = [vals; v]; endfor endfunction ## -*- texinfo -*- ## @deftypefn {RegressionPartitionedEnsemble} {@var{CVMdl} =} resume (@var{obj}, @var{NumLearningCycles}) ## @deftypefnx {RegressionPartitionedEnsemble} {@var{CVMdl} =} resume (@dots{}, 'NPrint', @var{n}) ## ## Grow more trees in every fold. ## ## Each fold's ensemble is resumed, as @code{RegressionEnsemble.resume} ## does, by @var{NumLearningCycles} trees. ## ## @seealso{RegressionPartitionedEnsemble, RegressionEnsemble.resume} ## @end deftypefn function this = resume (this, varargin) if (nargin < 2) error (strcat ("RegressionPartitionedEnsemble.resume: too few", ... " input arguments.")); endif for k = 1:this.KFold this.Trainable{k} = resume (this.Trainable{k}, varargin{:}); endfor this = compactFolds (this); endfunction endmethods methods (Access = private) function this = compactFolds (this) this.Trained = cellfun (@(M) compact (M), this.Trainable, ... 'UniformOutput', false); this.NumTrainedPerFold = cellfun (@(M) M.NumTrained, this.Trainable)'; endfunction ## The untransformed predictions for the rows the FOLDS held out, NaN ## elsewhere. function yf = foldResponse (this, folds) yf = NaN (this.NumObservations, 1); for k = folds te = test (this.Partition, k); if (any (te) && this.Trained{k}.NumTrained > 0) C = this.Trained{k}; C.ResponseTransform = 'none'; yf(te) = predict (C, this.X(te,:)); endif endfor endfunction ## The weighted loss of the untransformed predictions YF over ROWS. function L = setLoss (this, yf, rows, LossFun) f = this.RTfun (yf(rows)); y = this.Y(rows); w = double (this.W(rows)); have = ! isnan (f); w = w(have); if (! (sum (w) > 0)) L = NaN; return; endif w /= sum (w); if (is_function_handle (LossFun)) L = LossFun (y(have), f(have), w); else L = sum (w .* (f(have) - y(have)) .^ 2); endif endfunction endmethods endclassdef ## Test output %!shared X, y, S, c, CV %! load fisheriris %! X = meas(:,2:4); %! y = meas(:,1); %! S = templateTree ('MaxNumSplits', 1); %! c = cvpartition (150, 'KFold', 5); %! CV = fitrensemble (X, y, 'NumLearningCycles', 3, 'Learners', S, ... %! 'CVPartition', c); %!test # MATLAB parity: the properties of a cross-validated ensemble %! assert_equal (class (CV), 'RegressionPartitionedEnsemble'); %! assert_equal (numel (properties (CV)), 16); %! assert_equal (CV.KFold, 5); %! assert_equal (CV.NumTrainedPerFold, [3, 3, 3, 3, 3]); %! assert_equal (CV.CrossValidatedModel, 'LSBoost'); %! assert_equal (class (CV.Trained{1}), 'CompactRegressionEnsemble'); %! assert_equal (class (CV.Trainable{1}), 'RegressionEnsemble'); %!test # MATLAB parity: each row is predicted by the fold that held it out %! yf = zeros (150, 1); %! for k = 1:5 %! te = test (c, k); %! yf(te) = predict (CV.Trained{k}, X(te,:)); %! endfor %! assert_equal (kfoldPredict (CV), yf); %! assert_equal (kfoldLoss (CV), mean ((yf - y) .^ 2), 1e-14); %! Li = kfoldLoss (CV, 'Mode', 'individual'); %! assert_equal (Li(4), mean ((yf(test (c, 4)) - y(test (c, 4))) .^ 2), 1e-14); %!test # MATLAB parity: the cumulative loss grows the trees of every fold %! Lc = kfoldLoss (CV, 'Mode', 'cumulative'); %! assert_equal (size (Lc), [3, 1]); %! yf = zeros (150, 1); %! for k = 1:5 %! te = test (c, k); %! yf(te) = predict (CV.Trained{k}, X(te,:), 'Learners', 1:2); %! endfor %! assert_equal (Lc(2), mean ((yf - y) .^ 2), 1e-14); %! assert_equal (Lc(3), kfoldLoss (CV), 1e-14); %!test # the response transform is applied once, where MATLAB applies it twice %! M = fitrensemble (X, y, 'NumLearningCycles', 3, 'Learners', S, ... %! 'CVPartition', c, 'ResponseTransform', @(z) 2 * z); %! assert_equal (M.Trained{1}.ResponseTransform, 'none'); %! assert_equal (kfoldPredict (M), 2 * kfoldPredict (CV), 1e-14); %!test # MATLAB parity: kfoldfun and resume %! f = kfoldfun (CV, @(C, Xtr, Ytr, Wtr, Xte, Yte, Wte) [rows(Xtr), rows(Xte)]); %! assert_equal (f(1,:), [120, 30]); %! R = resume (CV, 1); %! assert_equal (R.NumTrainedPerFold, [4, 4, 4, 4, 4]); %!test # MATLAB parity: the losses weigh the held-out rows by W %! w = [5 * ones(50, 1); ones(100, 1)]; %! M = fitrensemble (X, y, 'NumLearningCycles', 3, 'Learners', S, ... %! 'CVPartition', c, 'Weights', w); %! yf = kfoldPredict (M); %! assert_equal (kfoldLoss (M), sum (M.W .* (yf - y) .^ 2) / sum (M.W), 1e-14); ## Test input validation %!error ... %! RegressionPartitionedEnsemble (1) %!error ... %! RegressionPartitionedEnsemble (1, c) %!error ... %! RegressionPartitionedEnsemble (CV.Trainable{1}, 1) %!error ... %! kfoldLoss (CV, 'Mode') %!error ... %! kfoldLoss (CV, 'Learners', 1) %!error ... %! kfoldLoss (CV, 'Mode', 'ensemble') %!error ... %! kfoldLoss (CV, 'Folds', 0) %!error ... %! kfoldLoss (CV, 'LossFun', 'mae') %!error ... %! kfoldfun (CV, 1) %!error ... %! resume (CV) statistics-release-2.1.2/inst/Machine_Learning/RegressionPartitionedKernel.m000066400000000000000000000561441527311104300273350ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftp {statistics} RegressionPartitionedKernel ## ## Cross-validated Gaussian kernel regression model. ## ## A @qcode{RegressionPartitionedKernel} object holds one ## @qcode{RegressionKernel} per fold of a partition, each fitted to the ## observations the fold trains on. @code{kfoldPredict} predicts each ## observation with the fold that held it @emph{out}, so what it returns is ## an out-of-sample prediction. ## ## A @qcode{RegressionKernel} stores no copy of its training data and so has ## no resubstitution methods and no @code{compact} form. This class is what ## takes their place: cross-validation is the way a kernel model is asked ## how it would do on data it has not seen. ## ## Every fold draws its own random basis, as it must, being its own fit. ## Two folds therefore approximate the same kernel through different ## expansions, which is a source of variation between folds over and above ## the data they were given. A larger @qcode{'NumExpansionDimensions'} ## narrows it. ## ## Create one with @code{fitrlinear} and a cross-validation option, or ## directly. ## ## @seealso{fitrlinear, RegressionKernel, RegressionPartitionedKernel} ## @end deftp classdef RegressionPartitionedKernel properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {RegressionPartitionedKernel} {property} ResponseTransform ## ## Transformation applied to the predicted response ## ## A character vector, or the text of the function handle that was ## supplied, which may be assigned after the model is built. The fold ## models carry no transform of their own; this one is applied once to ## the assembled predictions. ## ## @end deftp ResponseTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {RegressionPartitionedKernel} {property} CrossValidatedModel ## ## Name of the model that was cross-validated ## ## Always @qcode{'Linear'}, the short name MATLAB uses. This property is ## read-only. ## ## @end deftp CrossValidatedModel = 'Kernel'; ## -*- texinfo -*- ## @deftp {RegressionPartitionedKernel} {property} NumObservations ## ## Number of observations the partition covers ## ## A positive integer scalar, counting the rows that survived the removal ## of missing values. This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedKernel} {property} Y ## ## Response of the retained observations ## ## An @math{Nx1} numeric vector. This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedKernel} {property} W ## ## Observation weights ## ## An @math{Nx1} numeric vector summing to one. This property is ## read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedKernel} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {RegressionPartitionedKernel} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A row vector of column indices, empty when every predictor is ## numeric. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedKernel} {property} ResponseName ## ## Name of the response ## ## A character vector, defaulting to @qcode{'Y'}. This property is ## read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {RegressionPartitionedKernel} {property} Trained ## ## The models fitted to the folds ## ## A cell column with one @qcode{RegressionKernel} per fold, each fitted ## to the observations its fold trains on. This property is read-only. ## ## @end deftp Trained = {}; ## -*- texinfo -*- ## @deftp {RegressionPartitionedKernel} {property} KFold ## ## Number of folds ## ## A positive integer scalar. A holdout partition has one fold and a ## leave-one-out partition has as many as there are observations. This ## property is read-only. ## ## @end deftp KFold = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedKernel} {property} Partition ## ## The partition itself ## ## A @code{cvpartition} object over the retained observations. This ## property is read-only. ## ## @end deftp Partition = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedKernel} {property} ModelParameters ## ## What was cross-validated, and how ## ## A structure holding the parameters the folds were fitted with, carried ## through from the learner that was cross validated, beside ## @qcode{NLearn}, the number of folds, and the @qcode{Version}, ## @qcode{Method} and @qcode{Type} tags of this class, with ## @qcode{LearnerTemplates} naming the backing. The ## learner's own tags are replaced rather than kept, so a cross-validated ## SVM reports @qcode{Method} as @qcode{'PartitionedKernel'} and not ## @qcode{'SVM'}. ## ## @strong{Deviation from MATLAB.} MATLAB reports the parameter record of ## the cross-validation @emph{ensemble} here rather than of the learner, ## so it says nothing at all about how the folds were fitted: of its ## eighteen fields only the fold count, its partitioner and a fit template ## carry anything, and the rest are boosting settings left inert. Nor can ## the parameters be reached through the folds, a compact model carrying ## none in MATLAB. This class reports the fit instead, which is strictly ## more than MATLAB offers, and everything MATLAB's record does carry is ## published here as the @qcode{KFold}, @qcode{Partition}, @qcode{X}, ## @qcode{Y}, @qcode{W} and @qcode{CrossValidatedModel} properties. ## ## This property is read-only. ## ## @end deftp ModelParameters = []; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ## The callable behind ResponseTransform. RTfun = @(y) y; ## The predictors of the retained observations. MATLAB does not report ## them and neither do we, but the kfold methods have to predict from ## something and the fold models hold no data of their own. X_ = []; ## Number of regularization strengths the fold models carry. NumLambda_ = 1; ## Whether the fold models were fitted with an insensitive band, which ## decides whether kfoldLoss will offer the loss that reads it. HasEpsilon_ = false; endproperties methods (Access = public) ## -*- texinfo -*- ## @deftypefn {RegressionPartitionedKernel} {@var{obj} =} RegressionPartitionedKernel (@var{X}, @var{Y}) ## @deftypefnx {RegressionPartitionedKernel} {@var{obj} =} RegressionPartitionedKernel (@dots{}, @var{name}, @var{value}) ## ## Cross-validate a linear regression model. ## ## @code{@var{obj} = RegressionPartitionedKernel (@var{X}, @var{Y})} ## partitions the data into ten folds and fits a ## @qcode{RegressionKernel} to each. ## ## @code{@var{obj} = RegressionPartitionedKernel (@dots{}, @var{name}, ## @var{value})} takes one of @qcode{'KFold'}, @qcode{'Holdout'}, ## @qcode{'Leaveout'} and @qcode{'CVPartition'} to say how to partition, ## and any option @code{RegressionKernel} takes to say how to fit. ## @qcode{'CrossVal'} is accepted and has no effect here, this class ## being cross-validated by construction. ## ## Anything left as @qcode{'auto'} is resolved by each fold against its ## own training rows rather than once over the whole data, so ten folds ## of a hundred observations each get a @qcode{Lambda} of one ninetieth ## rather than one hundredth, and each its own @qcode{Epsilon} and ## @qcode{KernelScale}. Both are MATLAB's behaviour, measured. ## ## @seealso{fitrlinear, RegressionKernel} ## @end deftypefn function this = RegressionPartitionedKernel (X, Y, varargin) if (nargin < 2) error (strcat ("RegressionPartitionedKernel: too few input", ... " arguments.")); endif if (mod (numel (varargin), 2) != 0) error (strcat ("RegressionPartitionedKernel: optional arguments", ... " must be given in Name-Value pairs.")); endif ## A table names its own predictors and says which hold levels. This ## class answers over its folds rather than over new data, so the ## levels are not kept: there is no later prediction to code. if (istable (X)) [X, Y, varargin, ~, errmsg] = tableFrame (X, Y, varargin); if (! isempty (errmsg)) error ("RegressionPartitionedKernel: %s", errmsg); endif endif [P, args] = partitionedArgs (varargin, 'RegressionPartitionedKernel'); F = regFrame (X, Y, P.Weights, 'RegressionPartitionedKernel'); [part, args] = cvPartitionOf (args, [], F.n, ... 'RegressionPartitionedKernel'); ## Every fold is fitted without a response transform, so that the one ## the parent carries is applied exactly once to the assembled ## predictions. fargs = [args, {'ResponseTransform', 'none'}]; G = struct (); G.X = F.X; G.Y = F.Y; G.Weights = F.Weights; this.Trained = foldModels ('RegressionKernel', G, part, fargs); this.NumObservations = F.n; this.Y = F.Y; this.W = F.W; this.X_ = F.X; this.KFold = part.NumTestSets; this.Partition = part; this.NumLambda_ = numel (this.Trained{1}.Lambda); this.HasEpsilon_ = ! isempty (this.Trained{1}.Epsilon); this.PredictorNames = this.Trained{1}.PredictorNames; this.CategoricalPredictors = this.Trained{1}.CategoricalPredictors; this.ResponseName = this.Trained{1}.ResponseName; if (isempty (P.ResponseTransform)) P.ResponseTransform = 'none'; endif [this.RTfun, this.ResponseTransform] = ... parseResponseTransform (P.ResponseTransform, ... 'RegressionPartitionedKernel'); ## The learner's parameters, under this class's own tags. MATLAB ## reports an EnsembleParams here instead and so says nothing about ## the fit; see the ModelParameters property for the deviation. this.ModelParameters = partitionedModelParams (this.Trained{1}, ... this.KFold, 'PartitionedKernel', ... 'regression', 'Kernel'); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionPartitionedKernel} {@var{yFit} =} kfoldPredict (@var{obj}) ## ## Out-of-fold prediction for every observation. ## ## Each observation is predicted by the fold that held it out, so the ## predictions are out-of-sample. An observation that no fold held out, ## which under a holdout partition is most of them, comes back ## @qcode{NaN}. ## ## ## @end deftypefn function yFit = kfoldPredict (this) yFit = this.RTfun (kfoldResponse (this.Trained, this.Partition, ... this.X_, this.NumLambda_)); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionPartitionedKernel} {@var{l} =} kfoldLoss (@var{obj}) ## @deftypefnx {RegressionPartitionedKernel} {@var{l} =} kfoldLoss (@dots{}, @var{name}, @var{value}) ## ## Out-of-fold regression loss. ## ## @code{@var{l} = kfoldLoss (@var{obj})} returns the out-of-fold mean ## squared error. ## ## @code{@var{l} = kfoldLoss (@dots{}, @var{name}, @var{value})} takes ## @qcode{'LossFun'}, either @qcode{'mse'} or ## @qcode{'epsiloninsensitive'}; @qcode{'Folds'}, a subset of the folds ## to average over; and @qcode{'Mode'}, either @qcode{'average'}, the ## default, or @qcode{'individual'}, which returns one row per fold. ## ## @end deftypefn function l = kfoldLoss (this, varargin) O = kfoldOpts (varargin, {'mse', 'epsiloninsensitive'}, ... 'RegressionPartitionedKernel', 'kfoldLoss', this.KFold); if (isempty (O.LossFun)) O.LossFun = 'mse'; endif if (strcmp (O.LossFun, 'epsiloninsensitive') && ! this.HasEpsilon_) error (strcat ("RegressionPartitionedKernel.kfoldLoss: the", ... " 'epsiloninsensitive' loss applies to a support", ... " vector machine only.")); endif yFit = kfoldPredict (this); ## Each fold has its own insensitive band, so a residual is judged ## against the band of the fold that produced it. Assembled per ## observation, the band follows the prediction. band = nan (this.NumObservations, 1); for i = 1:this.KFold idx = test (this.Partition, i); if (any (idx) && ! isempty (this.Trained{i}.Epsilon)) band(idx) = this.Trained{i}.Epsilon; endif endfor sets = foldSets (this.Partition, O.Folds, O.Mode, ... this.NumObservations); l = zeros (numel (sets), this.NumLambda_); for i = 1:numel (sets) idx = sets{i}; w = this.W(idx); w = w / sum (w); for k = 1:this.NumLambda_ r = this.Y(idx) - yFit(idx,k); if (strcmp (O.LossFun, 'mse')) l(i,k) = sum (w .* (r .^ 2)); else l(i,k) = sum (w .* max (0, abs (r) - band(idx))); endif endfor endfor endfunction endmethods methods (Access = public, Hidden) function this = set.ResponseTransform (this, val) [f, nm] = parseResponseTransform (val, 'RegressionPartitionedKernel'); this.ResponseTransform = nm; this.RTfun = f; endfunction function display (this) in_name = inputname (1); if (! isempty (in_name)) printf ('%s =\n', in_name); endif disp (this); endfunction function disp (this) printf ("\n RegressionPartitionedKernel\n\n"); printf ("%+25s: '%s'\n", 'CrossValidatedModel', ... this.CrossValidatedModel); printf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); printf ("%+25s: '%s'\n", 'ResponseTransform', this.ResponseTransform); printf ("%+25s: %d\n", 'NumObservations', this.NumObservations); printf ("%+25s: %d\n", 'KFold', this.KFold); printf ("\n"); endfunction endmethods endclassdef %!demo %! ## Cross-validate a Gaussian kernel regression of fuel consumption and %! ## read the out-of-sample mean squared error. %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! CVMdl = RegressionPartitionedKernel (X(ok,:), MPG(ok), 'KFold', 5) %! outOfSample = kfoldLoss (CVMdl) %!shared X, Y %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! X = X(ok,:); %! Y = MPG(ok); %!test %! ## The model reports the surface MATLAB reports %! CVMdl = RegressionPartitionedKernel (X, Y, 'KFold', 4); %! assert_equal (class (CVMdl), 'RegressionPartitionedKernel'); %! assert_equal (CVMdl.CrossValidatedModel, 'Kernel'); %! assert_equal (CVMdl.KFold, 4); %! assert_equal (CVMdl.NumObservations, 93); %! assert_equal (CVMdl.ResponseTransform, 'none'); %! assert_equal (class (CVMdl.Trained{1}), 'RegressionKernel'); %! assert_equal (CVMdl.ModelParameters.Method, 'PartitionedKernel'); %! assert_equal (CVMdl.ModelParameters.LearnerTemplates, 'Kernel'); %!test %! ## The properties are the ones MATLAB lists, in its order %! CVMdl = RegressionPartitionedKernel (X, Y); %! assert_equal (sort (properties (CVMdl)), ... %! sort ({'ResponseTransform'; 'CrossValidatedModel'; ... %! 'NumObservations'; 'Y'; 'W'; 'PredictorNames'; ... %! 'CategoricalPredictors'; 'ResponseName'; 'Trained'; ... %! 'KFold'; 'Partition'; 'ModelParameters'})); %!test %! ## Each fold resolves its own strength and its own band from its own %! ## training rows %! CVMdl = RegressionPartitionedKernel (X, Y, 'KFold', 4); %! n1 = sum (training (CVMdl.Partition, 1)); %! assert_equal (CVMdl.Trained{1}.Lambda, 1 / n1, 1e-15); %! assert_equal (CVMdl.Trained{1}.NumExpansionDimensions, 128); %!test %! ## kfoldPredict predicts each observation with the fold that held it out %! part = cvpartition (93, 'KFold', 4); %! CVMdl = RegressionPartitionedKernel (X, Y, 'CVPartition', part); %! yFit = kfoldPredict (CVMdl); %! byhand = nan (93, 1); %! for k = 1:4 %! te = test (part, k); %! byhand(te) = predict (CVMdl.Trained{k}, X(te,:)); %! endfor %! assert_equal (yFit, byhand); %!test %! ## Averaging pools the observations rather than the per-fold values %! CVMdl = RegressionPartitionedKernel (X, Y, 'KFold', 4, ... %! 'Learner', 'leastsquares'); %! yFit = kfoldPredict (CVMdl); %! assert_equal (kfoldLoss (CVMdl), mean ((Y - yFit) .^ 2), 1e-10); %! assert_equal (size (kfoldLoss (CVMdl, 'Mode', 'individual')), [4, 1]); %!test %! ## The epsilon-insensitive loss is offered by a support vector machine %! ## alone, and judges each residual against the band of its own fold %! CVMdl = RegressionPartitionedKernel (X, Y, 'KFold', 4); %! assert_equal (isfinite (kfoldLoss (CVMdl, 'LossFun', ... %! 'epsiloninsensitive')), true); %!test %! ## Every fold draws its own basis, so two folds hold different %! ## expansions of the same kernel %! CVMdl = RegressionPartitionedKernel (X, Y, 'KFold', 4); %! p1 = predict (CVMdl.Trained{1}, X(1:3,:)); %! p2 = predict (CVMdl.Trained{2}, X(1:3,:)); %! assert_equal (isequal (p1, p2), false); %!test %! ## An observation that no fold held out comes back NaN %! CVMdl = RegressionPartitionedKernel (X, Y, 'Holdout', 0.3); %! assert_equal (CVMdl.KFold, 1); %! assert_equal (sum (isnan (kfoldPredict (CVMdl))), 65); %! assert_equal (isfinite (kfoldLoss (CVMdl)), true); %!test %! ## Standardizing reaches every fold %! CVMdl = RegressionPartitionedKernel (X, Y, 'KFold', 4, ... %! 'Standardize', true); %! assert_equal (isempty (CVMdl.Trained{1}.Mu), false); %! assert_equal (isempty (CVMdl.Trained{4}.Sigma), false); %!test %! ## It can be assigned after the model is built, and reaches kfoldPredict %! ## without being carried into the folds %! CVMdl = RegressionPartitionedKernel (X, Y, 'KFold', 4); %! y0 = kfoldPredict (CVMdl); %! CVMdl.ResponseTransform = @(y) y + 100; %! y1 = kfoldPredict (CVMdl); %! assert_equal (CVMdl.Trained{1}.ResponseTransform, 'none'); %! assert_equal (y1, y0 + 100, 1e-10); %!test %! ## And it reaches kfoldLoss, which is computed from those predictions %! CVMdl = RegressionPartitionedKernel (X, Y, 'KFold', 4); %! before = kfoldLoss (CVMdl); %! CVMdl.ResponseTransform = @(y) y + 100; %! assert (kfoldLoss (CVMdl) > before); %!test %! ## 'none' is the identity, so assigning it transforms nothing %! CVMdl = RegressionPartitionedKernel (X, Y, 'KFold', 4); %! y0 = kfoldPredict (CVMdl); %! CVMdl.ResponseTransform = 'none'; %! assert_equal (kfoldPredict (CVMdl), y0); %!error ... %! CVMdl = RegressionPartitionedKernel (X, Y, 'KFold', 4); %! CVMdl.ResponseTransform = 'nosuchtransform'; ## Test input validation %!error ... %! RegressionPartitionedKernel (ones (10, 2)) %!error ... %! RegressionPartitionedKernel (ones (10, 2), ones (10, 1), 'KFold') %!error ... %! RegressionPartitionedKernel (ones (10, 2), ones (10, 1), 'KFold', 1) %!error ... %! RegressionPartitionedKernel (ones (10, 2), ones (10, 1), 'KFold', 2, ... %! 'Leaveout', 'on') %!error ... %! kfoldLoss (RegressionPartitionedKernel (ones (10, 2), ones (10, 1), ... %! 'KFold', 2), 'LossFun', 'hinge') %!error ... %! kfoldLoss (RegressionPartitionedKernel (ones (10, 2), ones (10, 1), ... %! 'KFold', 2, 'Learner', 'leastsquares'), 'LossFun', ... %! 'epsiloninsensitive') ## ModelParameters carries the learner's parameters beside the tags this ## class reports for itself. %!test %! load fisheriris %! CVMdl = fitrkernel (meas(:,2:4), meas(:,1), 'KFold', 3); %! MP = CVMdl.ModelParameters; %! assert_equal (MP.Method, 'PartitionedKernel'); %! assert_equal (MP.LearnerTemplates, 'Kernel'); %! assert_equal (MP.NLearn, 3); %! assert_equal (MP.Learner, 'svm'); %! assert_equal (MP.BlockSize, 4000); ## Every documented response transform reaches the response that is reported. %!test %! load fisheriris %! Mdl = fitrkernel (meas(:,2:4), meas(:,1), 'KFold', 3); %! Mdl.ResponseTransform = 'none'; %! raw = kfoldPredict (Mdl); %! T = {'identity', @(x) x; 'exp', @(x) exp (x); 'log', @(x) log (x)}; %! for i = 1:rows (T) %! Mdl.ResponseTransform = T{i,1}; %! yhat = kfoldPredict (Mdl); %! assert_equal (yhat, T{i,2}(raw), 1e-12); %! endfor ## A function handle is taken as given and applied to the response. %!test %! load fisheriris %! Mdl = fitrkernel (meas(:,2:4), meas(:,1), 'KFold', 3); %! Mdl.ResponseTransform = 'none'; %! raw = kfoldPredict (Mdl); %! Mdl.ResponseTransform = @(x) x .^ 2; %! yhat = kfoldPredict (Mdl); %! assert_equal (yhat, raw .^ 2, 1e-12); ## Table input %!test # the predictors and the response may come from a table %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'SL'}); %! CVMdl = RegressionPartitionedKernel (T, 'SL', 'KFold', 3); %! assert_equal (class (CVMdl), 'RegressionPartitionedKernel'); %! assert_equal (CVMdl.KFold, 3); statistics-release-2.1.2/inst/Machine_Learning/RegressionPartitionedLinear.m000066400000000000000000000621521527311104300273230ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftp {statistics} RegressionPartitionedLinear ## ## Cross-validated linear regression model. ## ## A @qcode{RegressionPartitionedLinear} object holds one ## @qcode{RegressionLinear} per fold of a partition, each fitted to the ## observations the fold trains on. @code{kfoldPredict} predicts each ## observation with the fold that held it @emph{out}, so what it returns is ## an out-of-sample prediction. ## ## A @qcode{RegressionLinear} stores no copy of its training data and so has ## no resubstitution methods and no @code{compact} form. This class is what ## takes their place: cross-validation is the way a linear model is asked ## how it would do on data it has not seen. ## ## When the fold models carry a whole regularization path, both methods ## return one column per strength, in the order of the @qcode{'Lambda'} that ## was asked for. ## ## Create one with @code{fitrlinear} and a cross-validation option, or ## directly. ## ## @seealso{fitrlinear, RegressionLinear, RegressionPartitionedKernel} ## @end deftp classdef RegressionPartitionedLinear properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {RegressionPartitionedLinear} {property} ResponseTransform ## ## Transformation applied to the predicted response ## ## A character vector, or the text of the function handle that was ## supplied, which may be assigned after the model is built. The fold ## models carry no transform of their own; this one is applied once to ## the assembled predictions. ## ## @end deftp ResponseTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {RegressionPartitionedLinear} {property} CrossValidatedModel ## ## Name of the model that was cross-validated ## ## Always @qcode{'Linear'}, the short name MATLAB uses. This property is ## read-only. ## ## @end deftp CrossValidatedModel = 'Linear'; ## -*- texinfo -*- ## @deftp {RegressionPartitionedLinear} {property} NumObservations ## ## Number of observations the partition covers ## ## A positive integer scalar, counting the rows that survived the removal ## of missing values. This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedLinear} {property} Y ## ## Response of the retained observations ## ## An @math{Nx1} numeric vector. This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedLinear} {property} W ## ## Observation weights ## ## An @math{Nx1} numeric vector summing to one. This property is ## read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedLinear} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {RegressionPartitionedLinear} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A row vector of column indices, empty when every predictor is ## numeric. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedLinear} {property} ResponseName ## ## Name of the response ## ## A character vector, defaulting to @qcode{'Y'}. This property is ## read-only. ## ## @end deftp ResponseName = 'Y'; ## -*- texinfo -*- ## @deftp {RegressionPartitionedLinear} {property} Trained ## ## The models fitted to the folds ## ## A cell column with one @qcode{RegressionLinear} per fold, each fitted ## to the observations its fold trains on. This property is read-only. ## ## @end deftp Trained = {}; ## -*- texinfo -*- ## @deftp {RegressionPartitionedLinear} {property} KFold ## ## Number of folds ## ## A positive integer scalar. A holdout partition has one fold and a ## leave-one-out partition has as many as there are observations. This ## property is read-only. ## ## @end deftp KFold = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedLinear} {property} Partition ## ## The partition itself ## ## A @code{cvpartition} object over the retained observations. This ## property is read-only. ## ## @end deftp Partition = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedLinear} {property} ModelParameters ## ## What was cross-validated, and how ## ## A structure holding the parameters the folds were fitted with, carried ## through from the learner that was cross validated, beside ## @qcode{NLearn}, the number of folds, and the @qcode{Version}, ## @qcode{Method} and @qcode{Type} tags of this class, with ## @qcode{LearnerTemplates} naming the backing. The ## learner's own tags are replaced rather than kept, so a cross-validated ## SVM reports @qcode{Method} as @qcode{'PartitionedLinear'} and not ## @qcode{'SVM'}. ## ## @strong{Deviation from MATLAB.} MATLAB reports the parameter record of ## the cross-validation @emph{ensemble} here rather than of the learner, ## so it says nothing at all about how the folds were fitted: of its ## eighteen fields only the fold count, its partitioner and a fit template ## carry anything, and the rest are boosting settings left inert. Nor can ## the parameters be reached through the folds, a compact model carrying ## none in MATLAB. This class reports the fit instead, which is strictly ## more than MATLAB offers, and everything MATLAB's record does carry is ## published here as the @qcode{KFold}, @qcode{Partition}, @qcode{X}, ## @qcode{Y}, @qcode{W} and @qcode{CrossValidatedModel} properties. ## ## This property is read-only. ## ## @end deftp ModelParameters = []; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ## The callable behind ResponseTransform. RTfun = @(y) y; ## The predictors of the retained observations. MATLAB does not report ## them and neither do we, but the kfold methods have to predict from ## something and the fold models hold no data of their own. X_ = []; ## Number of regularization strengths the fold models carry. NumLambda_ = 1; ## Whether the fold models were fitted with an insensitive band, which ## decides whether kfoldLoss will offer the loss that reads it. HasEpsilon_ = false; endproperties methods (Access = public) ## -*- texinfo -*- ## @deftypefn {RegressionPartitionedLinear} {@var{obj} =} RegressionPartitionedLinear (@var{X}, @var{Y}) ## @deftypefnx {RegressionPartitionedLinear} {@var{obj} =} RegressionPartitionedLinear (@dots{}, @var{name}, @var{value}) ## ## Cross-validate a linear regression model. ## ## @code{@var{obj} = RegressionPartitionedLinear (@var{X}, @var{Y})} ## partitions the data into ten folds and fits a ## @qcode{RegressionLinear} to each. ## ## @code{@var{obj} = RegressionPartitionedLinear (@dots{}, @var{name}, ## @var{value})} takes one of @qcode{'KFold'}, @qcode{'Holdout'}, ## @qcode{'Leaveout'} and @qcode{'CVPartition'} to say how to partition, ## and any option @code{RegressionLinear} takes to say how to fit. ## @qcode{'CrossVal'} is accepted and has no effect here, this class ## being cross-validated by construction. ## ## Anything left as @qcode{'auto'} is resolved by each fold against its ## own training rows rather than once over the whole data, so ten folds ## of a hundred observations each get a @qcode{Lambda} of one ninetieth ## rather than one hundredth, and each its own @qcode{Epsilon}. Both are ## MATLAB's behaviour, measured. ## ## @seealso{fitrlinear, RegressionLinear} ## @end deftypefn function this = RegressionPartitionedLinear (X, Y, varargin) if (nargin < 2) error (strcat ("RegressionPartitionedLinear: too few input", ... " arguments.")); endif if (mod (numel (varargin), 2) != 0) error (strcat ("RegressionPartitionedLinear: optional arguments", ... " must be given in Name-Value pairs.")); endif ## A table names its own predictors and says which hold levels. This ## class answers over its folds rather than over new data, so the ## levels are not kept: there is no later prediction to code. if (istable (X)) [X, Y, varargin, ~, errmsg] = tableFrame (X, Y, varargin); if (! isempty (errmsg)) error ("RegressionPartitionedLinear: %s", errmsg); endif endif [P, args] = partitionedArgs (varargin, 'RegressionPartitionedLinear'); F = regFrame (X, Y, P.Weights, 'RegressionPartitionedLinear'); [part, args] = cvPartitionOf (args, [], F.n, ... 'RegressionPartitionedLinear'); ## Every fold is fitted without a response transform, so that the one ## the parent carries is applied exactly once to the assembled ## predictions. fargs = [args, {'ResponseTransform', 'none'}]; G = struct (); G.X = F.X; G.Y = F.Y; G.Weights = F.Weights; this.Trained = foldModels ('RegressionLinear', G, part, fargs); this.NumObservations = F.n; this.Y = F.Y; this.W = F.W; this.X_ = F.X; this.KFold = part.NumTestSets; this.Partition = part; this.NumLambda_ = numel (this.Trained{1}.Lambda); this.HasEpsilon_ = ! isempty (this.Trained{1}.Epsilon); this.PredictorNames = this.Trained{1}.PredictorNames; this.CategoricalPredictors = this.Trained{1}.CategoricalPredictors; this.ResponseName = this.Trained{1}.ResponseName; if (isempty (P.ResponseTransform)) P.ResponseTransform = 'none'; endif [this.RTfun, this.ResponseTransform] = ... parseResponseTransform (P.ResponseTransform, ... 'RegressionPartitionedLinear'); ## The learner's parameters, under this class's own tags. MATLAB ## reports an EnsembleParams here instead and so says nothing about ## the fit; see the ModelParameters property for the deviation. this.ModelParameters = partitionedModelParams (this.Trained{1}, ... this.KFold, 'PartitionedLinear', ... 'regression', 'Linear'); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionPartitionedLinear} {@var{yFit} =} kfoldPredict (@var{obj}) ## ## Out-of-fold prediction for every observation. ## ## Each observation is predicted by the fold that held it out, so the ## predictions are out-of-sample. An observation that no fold held out, ## which under a holdout partition is most of them, comes back ## @qcode{NaN}. ## ## With @math{L} regularization strengths @var{yFit} has one column per ## strength. ## ## @end deftypefn function yFit = kfoldPredict (this) yFit = this.RTfun (kfoldResponse (this.Trained, this.Partition, ... this.X_, this.NumLambda_)); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionPartitionedLinear} {@var{l} =} kfoldLoss (@var{obj}) ## @deftypefnx {RegressionPartitionedLinear} {@var{l} =} kfoldLoss (@dots{}, @var{name}, @var{value}) ## ## Out-of-fold regression loss. ## ## @code{@var{l} = kfoldLoss (@var{obj})} returns the out-of-fold mean ## squared error. ## ## @code{@var{l} = kfoldLoss (@dots{}, @var{name}, @var{value})} takes ## @qcode{'LossFun'}, either @qcode{'mse'} or ## @qcode{'epsiloninsensitive'}; @qcode{'Folds'}, a subset of the folds ## to average over; and @qcode{'Mode'}, either @qcode{'average'}, the ## default, or @qcode{'individual'}, which returns one row per fold. ## ## @end deftypefn function l = kfoldLoss (this, varargin) O = kfoldOpts (varargin, {'mse', 'epsiloninsensitive'}, ... 'RegressionPartitionedLinear', 'kfoldLoss', this.KFold); if (isempty (O.LossFun)) O.LossFun = 'mse'; endif if (strcmp (O.LossFun, 'epsiloninsensitive') && ! this.HasEpsilon_) error (strcat ("RegressionPartitionedLinear.kfoldLoss: the", ... " 'epsiloninsensitive' loss applies to a support", ... " vector machine only.")); endif yFit = kfoldPredict (this); ## Each fold has its own insensitive band, so a residual is judged ## against the band of the fold that produced it. Assembled per ## observation, the band follows the prediction. band = nan (this.NumObservations, 1); for i = 1:this.KFold idx = test (this.Partition, i); if (any (idx) && ! isempty (this.Trained{i}.Epsilon)) band(idx) = this.Trained{i}.Epsilon; endif endfor sets = foldSets (this.Partition, O.Folds, O.Mode, ... this.NumObservations); l = zeros (numel (sets), this.NumLambda_); for i = 1:numel (sets) idx = sets{i}; w = this.W(idx); w = w / sum (w); for k = 1:this.NumLambda_ r = this.Y(idx) - yFit(idx,k); if (strcmp (O.LossFun, 'mse')) l(i,k) = sum (w .* (r .^ 2)); else l(i,k) = sum (w .* max (0, abs (r) - band(idx))); endif endfor endfor endfunction endmethods methods (Access = public, Hidden) function this = set.ResponseTransform (this, val) [f, nm] = parseResponseTransform (val, 'RegressionPartitionedLinear'); this.ResponseTransform = nm; this.RTfun = f; endfunction function display (this) in_name = inputname (1); if (! isempty (in_name)) printf ('%s =\n', in_name); endif disp (this); endfunction function disp (this) printf ("\n RegressionPartitionedLinear\n\n"); printf ("%+25s: '%s'\n", 'CrossValidatedModel', ... this.CrossValidatedModel); printf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); printf ("%+25s: '%s'\n", 'ResponseTransform', this.ResponseTransform); printf ("%+25s: %d\n", 'NumObservations', this.NumObservations); printf ("%+25s: %d\n", 'KFold', this.KFold); printf ("\n"); endfunction endmethods endclassdef %!demo %! ## Cross-validate a linear regression of fuel consumption and read the %! ## out-of-sample mean squared error. %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! CVMdl = RegressionPartitionedLinear (X(ok,:), MPG(ok), 'KFold', 5) %! outOfSample = kfoldLoss (CVMdl) %!shared X, Y %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! X = X(ok,:); %! Y = MPG(ok); %!test %! ## The model reports the surface MATLAB reports %! CVMdl = RegressionPartitionedLinear (X, Y, 'KFold', 4); %! assert_equal (class (CVMdl), 'RegressionPartitionedLinear'); %! assert_equal (CVMdl.CrossValidatedModel, 'Linear'); %! assert_equal (CVMdl.KFold, 4); %! assert_equal (CVMdl.NumObservations, 93); %! assert_equal (CVMdl.ResponseTransform, 'none'); %! assert_equal (CVMdl.ResponseName, 'Y'); %! assert_equal (CVMdl.PredictorNames, {'x1', 'x2', 'x3', 'x4'}); %! assert_equal (size (CVMdl.Trained), [4, 1]); %! assert_equal (class (CVMdl.Trained{1}), 'RegressionLinear'); %! assert_equal (sum (CVMdl.W), 1, 1e-12); %! assert_equal (CVMdl.ModelParameters.Method, 'PartitionedLinear'); %!test %! ## The properties are the ones MATLAB lists, in its order %! CVMdl = RegressionPartitionedLinear (X, Y); %! assert_equal (sort (properties (CVMdl)), ... %! sort ({'ResponseTransform'; 'CrossValidatedModel'; ... %! 'NumObservations'; 'Y'; 'W'; 'PredictorNames'; ... %! 'CategoricalPredictors'; 'ResponseName'; 'Trained'; ... %! 'KFold'; 'Partition'; 'ModelParameters'})); %!test %! ## The default is ten folds %! CVMdl = RegressionPartitionedLinear (X, Y); %! assert_equal (CVMdl.KFold, 10); %! assert_equal (numel (CVMdl.Trained), 10); %!test %! ## Each fold resolves its own 'Lambda' and its own 'Epsilon' from its own %! ## training rows, which is why two folds report different bands %! CVMdl = RegressionPartitionedLinear (X, Y, 'KFold', 4); %! n1 = sum (training (CVMdl.Partition, 1)); %! assert_equal (CVMdl.Trained{1}.Lambda, 1 / n1, 1e-15); %! assert_equal (isequal (CVMdl.Trained{1}.Epsilon, ... %! CVMdl.Trained{4}.Epsilon), false); %!test %! ## kfoldPredict predicts each observation with the fold that held it out, %! ## and doing the same partition by hand gives the same numbers %! part = cvpartition (93, 'KFold', 4); %! CVMdl = RegressionPartitionedLinear (X, Y, 'CVPartition', part, ... %! 'Learner', 'leastsquares'); %! yFit = kfoldPredict (CVMdl); %! byhand = nan (93, 1); %! for k = 1:4 %! tr = training (part, k); %! te = test (part, k); %! m = RegressionLinear (X(tr,:), Y(tr), 'Learner', 'leastsquares'); %! byhand(te) = predict (m, X(te,:)); %! endfor %! assert_equal (yFit, byhand, 1e-10); %!test %! ## Averaging pools the observations rather than the per-fold values, and %! ## the two differ once the folds differ in size. The pooled reading is %! ## the one R2024a returns. %! part = cvpartition (93, 'KFold', 4); %! CVMdl = RegressionPartitionedLinear (X, Y, 'CVPartition', part, ... %! 'Learner', 'leastsquares'); %! yFit = kfoldPredict (CVMdl); %! assert_equal (kfoldLoss (CVMdl), mean ((Y - yFit) .^ 2), 1e-10); %! each = kfoldLoss (CVMdl, 'Mode', 'individual'); %! assert_equal (size (each), [4, 1]); %! assert_equal (isequal (kfoldLoss (CVMdl), mean (each)), false); %!test %! ## 'Folds' reports over the observations of the folds it names %! CVMdl = RegressionPartitionedLinear (X, Y, 'KFold', 4, ... %! 'Learner', 'leastsquares'); %! yFit = kfoldPredict (CVMdl); %! idx = test (CVMdl.Partition, 1) | test (CVMdl.Partition, 3); %! assert_equal (kfoldLoss (CVMdl, 'Folds', [1, 3]), ... %! mean ((Y(idx) - yFit(idx)) .^ 2), 1e-10); %!test %! ## The epsilon-insensitive loss judges each residual against the band of %! ## the fold that produced it, and is offered by a support vector machine %! ## alone %! CVMdl = RegressionPartitionedLinear (X, Y, 'KFold', 4); %! assert_equal (isfinite (kfoldLoss (CVMdl, 'LossFun', ... %! 'epsiloninsensitive')), true); %! assert_equal (kfoldLoss (CVMdl, 'LossFun', 'epsiloninsensitive') ... %! < kfoldLoss (CVMdl), true); %!test %! ## A whole regularization path gives one column per strength %! CVMdl = RegressionPartitionedLinear (X, Y, 'KFold', 4, ... %! 'Lambda', [0.001, 0.01, 0.1]); %! assert_equal (size (kfoldPredict (CVMdl)), [93, 3]); %! assert_equal (size (kfoldLoss (CVMdl)), [1, 3]); %! assert_equal (size (kfoldLoss (CVMdl, 'Mode', 'individual')), [4, 3]); %!test %! ## An observation that no fold held out comes back NaN rather than %! ## predicted %! CVMdl = RegressionPartitionedLinear (X, Y, 'Holdout', 0.3); %! assert_equal (CVMdl.KFold, 1); %! yFit = kfoldPredict (CVMdl); %! assert_equal (sum (isnan (yFit)), 65); %! assert_equal (isfinite (kfoldLoss (CVMdl)), true); %!test %! ## A response transform reaches the assembled predictions once, the fold %! ## models carrying none %! part = cvpartition (93, 'KFold', 4); %! plain = RegressionPartitionedLinear (X, Y, 'CVPartition', part, ... %! 'Learner', 'leastsquares'); %! CVexp = RegressionPartitionedLinear (X, Y, 'CVPartition', part, ... %! 'Learner', 'leastsquares', ... %! 'ResponseTransform', 'exp'); %! assert_equal (CVexp.ResponseTransform, 'exp'); %! assert_equal (CVexp.Trained{1}.ResponseTransform, 'none'); %! assert_equal (kfoldPredict (CVexp), exp (kfoldPredict (plain)), 1e-10); %!test %! ## A row with a missing value is dropped before the partition %! Xn = X; %! Xn(3,2) = NaN; %! CVMdl = RegressionPartitionedLinear (Xn, Y, 'KFold', 4); %! assert_equal (CVMdl.NumObservations, 92); %! assert_equal (CVMdl.Partition.NumObservations, 92); %!test %! ## It can be assigned after the model is built, and reaches kfoldPredict %! ## without being carried into the folds %! CVMdl = RegressionPartitionedLinear (X, Y, 'KFold', 4); %! y0 = kfoldPredict (CVMdl); %! CVMdl.ResponseTransform = @(y) y + 100; %! y1 = kfoldPredict (CVMdl); %! assert_equal (CVMdl.Trained{1}.ResponseTransform, 'none'); %! assert_equal (y1, y0 + 100, 1e-10); %!test %! ## And it reaches kfoldLoss, which is computed from those predictions %! CVMdl = RegressionPartitionedLinear (X, Y, 'KFold', 4); %! before = kfoldLoss (CVMdl); %! CVMdl.ResponseTransform = @(y) y + 100; %! assert (kfoldLoss (CVMdl) > before); %!test %! ## 'none' is the identity, so assigning it transforms nothing %! CVMdl = RegressionPartitionedLinear (X, Y, 'KFold', 4); %! y0 = kfoldPredict (CVMdl); %! CVMdl.ResponseTransform = 'none'; %! assert_equal (kfoldPredict (CVMdl), y0); %!error ... %! CVMdl = RegressionPartitionedLinear (X, Y, 'KFold', 4); %! CVMdl.ResponseTransform = 'nosuchtransform'; ## Test input validation %!error ... %! RegressionPartitionedLinear (ones (10, 2)) %!error ... %! RegressionPartitionedLinear (ones (10, 2), ones (10, 1), 'KFold') %!error ... %! RegressionPartitionedLinear (ones (10, 2), ones (10, 1), 'KFold', 1) %!error ... %! RegressionPartitionedLinear (ones (10, 2), ones (10, 1), 'KFold', 2, ... %! 'Holdout', 0.2) %!error ... %! RegressionPartitionedLinear (ones (10, 2), {1, 2}) %!error ... %! kfoldLoss (RegressionPartitionedLinear (ones (10, 2), ones (10, 1), ... %! 'KFold', 2), 'LossFun', 'hinge') %!error ... %! kfoldLoss (RegressionPartitionedLinear (ones (10, 2), ones (10, 1), ... %! 'KFold', 2, 'Learner', 'leastsquares'), 'LossFun', ... %! 'epsiloninsensitive') %!error ... %! kfoldLoss (RegressionPartitionedLinear (ones (10, 2), ones (10, 1), ... %! 'KFold', 2), 'Mode', 'each') ## Every documented response transform reaches the response that is reported. %!test %! load fisheriris %! Mdl = fitrlinear (meas(:,2:4), meas(:,1), 'KFold', 3); %! Mdl.ResponseTransform = 'none'; %! raw = kfoldPredict (Mdl); %! T = {'identity', @(x) x; 'exp', @(x) exp (x); 'log', @(x) log (x)}; %! for i = 1:rows (T) %! Mdl.ResponseTransform = T{i,1}; %! yhat = kfoldPredict (Mdl); %! assert_equal (yhat, T{i,2}(raw), 1e-12); %! endfor ## A function handle is taken as given and applied to the response. %!test %! load fisheriris %! Mdl = fitrlinear (meas(:,2:4), meas(:,1), 'KFold', 3); %! Mdl.ResponseTransform = 'none'; %! raw = kfoldPredict (Mdl); %! Mdl.ResponseTransform = @(x) x .^ 2; %! yhat = kfoldPredict (Mdl); %! assert_equal (yhat, raw .^ 2, 1e-12); ## Table input %!test # the predictors and the response may come from a table %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'SL'}); %! CVMdl = RegressionPartitionedLinear (T, 'SL', 'KFold', 3); %! assert_equal (class (CVMdl), 'RegressionPartitionedLinear'); %! assert_equal (CVMdl.KFold, 3); statistics-release-2.1.2/inst/Machine_Learning/RegressionPartitionedModel.m000066400000000000000000001714251527311104300271550ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef RegressionPartitionedModel ## -*- texinfo -*- ## @deftp {statistics} RegressionPartitionedModel ## ## Cross-validated regression model. ## ## A @qcode{RegressionPartitionedModel} object holds a regression model cross ## validated over a partition of its training data: one compact model per ## fold, in @code{Trained}, each fitted without the observations that fold ## tests on. @code{kfoldPredict}, @code{kfoldLoss} and @code{kfoldfun} answer ## for every observation with a model that never saw it. ## ## Create a @qcode{RegressionPartitionedModel} object with the @code{crossval} ## method of a regression model or with the class constructor. ## ## @seealso{crossval, cvpartition, ClassificationPartitionedModel} ## @end deftp properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {RegressionPartitionedModel} {property} ResponseTransform ## ## Transformation applied to the predicted response ## ## A function handle, carried over from the model that was cross ## validated. This property is read-only. ## ## @end deftp ResponseTransform = 'none'; endproperties properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {RegressionPartitionedModel} {property} CrossValidatedModel ## ## Name of the cross-validated model ## ## A character vector holding the short name of the learner that was ## cross validated, as MATLAB reports it: @qcode{'GAM'}, @qcode{'GP'}, ## @qcode{'NeuralNetwork'} or @qcode{'SVM'}. It is not the class name of ## that learner, and the classification side uses the same names. This ## property is read-only. ## ## @end deftp CrossValidatedModel = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedModel} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors. This property is read-only. ## ## @end deftp PredictorNames = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedModel} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A numeric vector of column indices, and empty when none is. This ## property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedModel} {property} ResponseName ## ## Name of the response variable ## ## A character vector. This property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedModel} {property} NumObservations ## ## Number of observations ## ## A positive integer scalar. This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedModel} {property} X ## ## Predictor data ## ## A numeric matrix holding the observations the model was trained on, ## the rows carrying missing values already removed. This property is ## read-only. ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedModel} {property} Y ## ## Response data ## ## A numeric column vector with one entry per row of @code{X}. This ## property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedModel} {property} W ## ## Observation weights ## ## A numeric column vector with one entry per observation. This property ## is read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedModel} {property} ModelParameters ## ## Parameters the folds were fitted with ## ## A structure holding the parameters the folds were fitted with, carried ## through from the learner that was cross validated, beside ## @qcode{NLearn}, the number of folds, and the @qcode{Version}, ## @qcode{Method} and @qcode{Type} tags of this class. The ## learner's own tags are replaced rather than kept, so a cross-validated ## SVM reports @qcode{Method} as @qcode{'PartitionedModel'} and not ## @qcode{'SVM'}. ## ## @strong{Deviation from MATLAB.} MATLAB reports the parameter record of ## the cross-validation @emph{ensemble} here rather than of the learner, ## so it says nothing at all about how the folds were fitted: of its ## eighteen fields only the fold count, its partitioner and a fit template ## carry anything, and the rest are boosting settings left inert. Nor can ## the parameters be reached through the folds, a compact model carrying ## none in MATLAB. This class reports the fit instead, which is strictly ## more than MATLAB offers, and everything MATLAB's record does carry is ## published here as the @qcode{KFold}, @qcode{Partition}, @qcode{X}, ## @qcode{Y}, @qcode{W} and @qcode{CrossValidatedModel} properties. ## ## This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedModel} {property} Trained ## ## The models fitted to each fold ## ## A cell array with one compact model per fold, each fitted on the ## observations its fold holds out of the test set. This property is ## read-only. ## ## @end deftp Trained = {}; ## -*- texinfo -*- ## @deftp {RegressionPartitionedModel} {property} KFold ## ## Number of folds ## ## A positive integer scalar. This property is read-only. ## ## @end deftp KFold = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedModel} {property} Partition ## ## The partition the folds came from ## ## A @code{cvpartition} object. This property is read-only. ## ## @end deftp Partition = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedModel} {property} BinEdges ## ## Bin edges of the predictors ## ## A cell array with one entry per predictor, holding that predictor's bin ## edges where the learner discretized it before fitting. It is carried ## over from the model that was cross validated, and is empty whenever that ## model did no binning, which is every learner this package implements: ## MATLAB fills it only for its generalized additive model, which bins ## because it is built from boosted trees where ours is built from splines. ## ## This property is read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {RegressionPartitionedModel} {property} IsStandardDeviationFit ## ## Whether the folds fitted a standard deviation model ## ## A logical scalar for a generalized additive model backing, taken from ## the model that was cross validated, and empty for every other backing. ## ## MATLAB carries this on @code{RegressionPartitionedGAM}, one of five ## per-learner partitioned classes this package deliberately does not have ## (see @code{crossval}). With one class serving every backing the ## property has to be declared for all of them, so it is empty where it ## does not apply. It is placed last rather than first, where MATLAB's ## subclass shows it, because that subclass also moves ## @qcode{ResponseTransform} to the end and no single order can match both ## of MATLAB's classes; matching the general one and appending is the only ## coherent choice. ## ## This property is read-only. ## ## @end deftp IsStandardDeviationFit = []; ## -*- texinfo -*- ## @deftp {RegressionPartitionedModel} {property} NumTrainedPerFold ## ## How many trees each fold fitted ## ## A scalar structure with fields @qcode{PredictorTrees} and ## @qcode{InteractionTrees}, each a row with one entry per fold, for a ## generalized additive model backing, and empty for every other. ## ## It reports what each fold actually fitted, which the budget in ## @qcode{ModelParameters} does not: a phase stops early when it can no ## longer improve the fit, and the folds need not stop at the same place. ## ## MATLAB carries this on its per-learner partitioned GAM classes, which ## this package deliberately does not have (see @code{crossval}), so like ## @qcode{IsStandardDeviationFit} it is declared here for every backing ## and left empty where it does not apply. ## ## This property is read-only. ## ## @end deftp NumTrainedPerFold = []; endproperties ## Copied from the parent model and kept out of the documented surface. properties (GetAccess = public, SetAccess = protected, Hidden) RTfun = @(y) y; endproperties ## Set methods for the properties a user may assign. methods (Hidden) function this = set.ResponseTransform (this, val) [f, nm] = parseResponseTransform (val, 'RegressionPartitionedModel'); this.ResponseTransform = nm; this.RTfun = f; endfunction ## Custom display function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ('%s =\n', in_name); endif disp (this); endfunction ## Custom display function disp (this) fprintf ("\n RegressionPartitionedModel\n\n"); fprintf ("%+25s: '%s'\n", 'CrossValidatedModel', ... this.CrossValidatedModel); fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); fprintf ("%+25s: %d\n", 'NumObservations', this.NumObservations); fprintf ("%+25s: %d\n", 'KFold', this.KFold); fprintf ("%+25s: '%s'\n", 'ResponseTransform', this.ResponseTransform); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {RegressionPartitionedModel} {@var{obj} =} RegressionPartitionedModel (@var{Mdl}, @var{Partition}) ## ## Cross-validate a regression model over a partition of its training data. ## ## @code{@var{obj} = RegressionPartitionedModel (@var{Mdl}, ## @var{Partition})} refits @var{Mdl} once per fold of @var{Partition}, ## each time on the observations that fold holds out of its test set, and ## stores the compact form of every fit in @code{Trained}. It is normally ## reached through @code{crossval (@var{Mdl})} rather than called directly. ## ## @itemize ## @item ## @var{Mdl} must be a @qcode{RegressionGAM}, a @qcode{RegressionGP}, a ## @qcode{RegressionNeuralNetwork}, a @qcode{RegressionSVM} or a ## @qcode{RegressionTree} object. ## @item ## @var{Partition} must be a @qcode{cvpartition} object over as many ## observations as @var{Mdl} was trained on. ## @end itemize ## ## Every observation is held out by exactly one fold under @math{k}-fold or ## leave-one-out partitioning, so @code{kfoldPredict} can answer for it with ## a model that never saw it. Under a holdout partition only the test set ## is answered for, and the rest come back @code{NaN}. ## ## @seealso{crossval, cvpartition, RegressionGAM, RegressionGP, ## RegressionNeuralNetwork, RegressionSVM, RegressionTree} ## @end deftypefn function this = RegressionPartitionedModel (Mdl, Partition) ## Check input arguments if (nargin < 2) error ("RegressionPartitionedModel: too few input arguments."); endif ## Check for valid Regression object validTypes = {'RegressionGAM', 'RegressionGP', ... 'RegressionNeuralNetwork', 'RegressionSVM', ... 'RegressionTree'}; if (! any (strcmp (class (Mdl), validTypes))) error ("RegressionPartitionedModel: unsupported model type."); endif ## Check for valid cvpartition object if (! strcmp (class (Partition), 'cvpartition')) error (strcat ("RegressionPartitionedModel: invalid", ... " 'cvpartition' object.")); endif ## The partition indexes the observations actually used for training, ## so the rows dropped for missing values are removed here as well and ## every index below refers to the same set. X = Mdl.X; Y = Mdl.Y; Y = Y(:); if (Partition.NumObservations != rows (X)) error (strcat ("RegressionPartitionedModel: 'cvpartition' object", ... " must be defined over the %d observations the", ... " model was trained on."), rows (X)); endif ## Set properties this.X = X; this.Y = Y; this.W = Mdl.W; this.BinEdges = Mdl.BinEdges; this.KFold = Partition.NumTestSets; this.Trained = cell (this.KFold, 1); this.ResponseName = Mdl.ResponseName; this.NumObservations = rows (X); this.PredictorNames = Mdl.PredictorNames; this.CategoricalPredictors = Mdl.CategoricalPredictors; this.Partition = Partition; ## MATLAB stores a short name here, shared with the classification ## side: 'GAM', 'GP', 'NeuralNetwork', 'SVM'. All measured. this.CrossValidatedModel = strrep (class (Mdl), 'Regression', ''); this.ResponseTransform = Mdl.ResponseTransform; this.RTfun = Mdl.RTfun; ## The learner's parameters, under this class's own tags. MATLAB ## reports an EnsembleParams here instead and so says nothing about the ## fit; see the ModelParameters property for the deviation. this.ModelParameters = partitionedModelParams (Mdl, this.KFold, ... 'PartitionedModel', 'regression'); if (any (strcmp (properties (Mdl), 'IsStandardDeviationFit'))) this.IsStandardDeviationFit = Mdl.IsStandardDeviationFit; endif ## Switch Regression object types switch (this.CrossValidatedModel) case 'GAM' ## Knots, Order and DoF are three views of one parameterisation and ## the constructor accepts any two, recomputing the third, so only ## Knots and Order are passed on. args = {}; ## Which parameters a fold takes depends on which engine fitted ## the parent: the two have disjoint argument surfaces and each ## refuses the other's, so the fold is refitted with its own. if (strcmp (Mdl.FitMethod, 'boostedtrees')) GAMparams = {'PredictorNames', 'ResponseName', ... 'CategoricalPredictors'}; else GAMparams = {'PredictorNames', 'ResponseName', 'Formula', ... 'Knots', 'Order', 'Tol'}; endif args = [args, {'FitMethod', Mdl.FitMethod}]; for i = 1:numel (GAMparams) paramName = GAMparams{i}; paramValue = Mdl.(paramName); if (! isempty (paramValue)) args = [args, {paramName, paramValue}]; endif endfor ## Interactions now holds the fitted pairs, which the constructor ## does not take as a specification. The term matrix does, and it ## reproduces the parent's terms exactly rather than re-selecting ## them. A formula names its own terms and is passed instead, so ## this must not be passed alongside one. if (isempty (Mdl.Formula) && ! isempty (Mdl.IntMatrix)) args = [args, {'Interactions', Mdl.IntMatrix}]; endif for k = 1:this.KFold idx = training (this.Partition, k); ## A boosted-tree fold keeps the weights of the rows it holds. fargs = args; if (strcmp (Mdl.FitMethod, 'boostedtrees')) fargs = [fargs, {'Weights', this.W(idx)}]; endif tmp = fitrgam (X(idx, :), Y(idx), fargs{:}); this.Trained{k} = compact (tmp); endfor case 'NeuralNetwork' ## Computed before the cell literal: inside braces a space before ## the paren would split the call from its argument. stdz = ! isempty (Mdl.Mu); args = {'LayerSizes', Mdl.LayerSizes, ... 'Activations', Mdl.Activations, ... 'OutputLayerActivation', Mdl.OutputLayerActivation, ... 'IterationLimit', Mdl.IterationLimit, ... 'Standardize', stdz, ... 'CategoricalPredictors', Mdl.CategoricalPredictors, ... 'ResponseName', Mdl.ResponseName, ... 'PredictorNames', Mdl.PredictorNames}; ## As in ClassificationPartitionedModel: the folds are trained by ## the parent's solver, and a learning rate goes only with 'sgd'. if (strcmp (Mdl.Solver, 'LBFGS')) args = [args, {'Solver', 'lbfgs'}]; else args = [args, {'Solver', 'sgd', 'LearningRate', Mdl.LearningRate}]; endif ## A model fitted with weights passes each fold the weights of its ## rows; without them W is uniform and the folds need none. wAll = []; Wd = double (this.W); if (max (Wd) - min (Wd) > 1e-12 * max (Wd)) wAll = this.W; endif for k = 1:this.KFold idx = training (this.Partition, k); fargs = args; if (! isempty (wAll)) fargs = [fargs, {'Weights', wAll(idx)}]; endif tmp = fitrnet (X(idx, :), Y(idx), fargs{:}); this.Trained{k} = compact (tmp); endfor case 'GP' p = Mdl.ModelParameters; args = {'KernelFunction', Mdl.KernelFunction, ... 'BasisFunction', Mdl.BasisFunction, ... 'FitMethod', p.FitMethod, ... 'PredictMethod', p.PredictMethod, ... 'Optimizer', p.Optimizer, ... 'ConstantSigma', p.ConstantSigma, ... 'SigmaLowerBound', p.SigmaLowerBound, ... 'Standardize', p.Standardize, ... 'CategoricalPredictors', Mdl.CategoricalPredictors, ... 'ResponseName', Mdl.ResponseName, ... 'PredictorNames', Mdl.PredictorNames}; for k = 1:this.KFold idx = training (this.Partition, k); tmp = fitrgp (X(idx, :), Y(idx), args{:}); this.Trained{k} = compact (tmp); endfor case 'SVM' p = Mdl.ModelParameters; stdz = ! isempty (Mdl.Mu); ## The polynomial order is recorded for the polynomial kernel ## alone, so it is passed only when the parent carries one. korder = {}; if (! isempty (p.KernelPolynomialOrder)) korder = {'PolynomialOrder', p.KernelPolynomialOrder}; endif args = {'SVMtype', p.SVMtype, ... 'KernelFunction', p.KernelFunction, ... korder{:}, ... 'KernelScale', p.KernelScale, ... 'KernelOffset', p.KernelOffset, ... 'BoxConstraint', p.BoxConstraint, ... 'Epsilon', p.Epsilon, 'Nu', p.Nu, ... 'CacheSize', p.CacheSize, ... 'Tolerance', p.Tolerance, ... 'Shrinking', p.Shrinking, ... 'Standardize', stdz, ... 'CategoricalPredictors', Mdl.CategoricalPredictors, ... 'ResponseName', Mdl.ResponseName, ... 'PredictorNames', Mdl.PredictorNames}; ## A fold keeps the weights of the rows it holds. for k = 1:this.KFold idx = training (this.Partition, k); tmp = fitrsvm (X(idx, :), Y(idx), args{:}, ... 'Weights', this.W(idx)); this.Trained{k} = compact (tmp); endfor case 'Tree' ## The arguments a fold is grown with, which the classification ## tree's own cross-validation shares. ResponseTransform is not ## among them: the parent applies it once to the assembled ## prediction, so a fold carrying it too would apply it twice. args = treeFoldArgs (Mdl); ## Train model according to partition object. The fold is stored ## compact, as MATLAB stores it: measured on R2024a, where ## Trained{k} is a CompactRegressionTree. The weights are sliced ## per fold, this being the one backing whose constructor takes ## them and reports them. W = Mdl.RawWeights; for k = 1:this.KFold idx = training (this.Partition, k); tmp = fitrtree (X(idx, :), Y(idx), args{:}, 'Weights', W(idx)); this.Trained{k} = compact (tmp); endfor ## The model's own, rather than a list of names restated here. endswitch ## Gathered across the folds, which is the shape MATLAB reports: one ## structure carrying a row per phase rather than a structure per fold. if (strcmp (this.CrossValidatedModel, 'GAM')) pt = zeros (1, this.KFold); it = zeros (1, this.KFold); boosted = true; for k = 1:this.KFold nt = this.Trained{k}.NumTrainedTrees; ## A spline fit counts no trees, so there is nothing to report and ## the property stays empty rather than claiming zero of them. if (isempty (nt)) boosted = false; break; endif pt(k) = nt.PredictorTrees; it(k) = nt.InteractionTrees; endfor if (boosted) this.NumTrainedPerFold = struct ('PredictorTrees', pt, ... 'InteractionTrees', it); endif endif ## No fold carries the transform: the parent applies it once to the ## assembled prediction. Cleared here rather than at each fit call so ## that a backing added later cannot reintroduce a double application ## by inheriting a class default of its own. ## ## Safe because the transform here is a caller's preference and not ## fitted content. for k = 1:this.KFold T = this.Trained{k}; T.ResponseTransform = 'none'; this.Trained{k} = T; endfor endfunction ## -*- texinfo -*- ## @deftypefn {RegressionPartitionedModel} {@var{yFit} =} kfoldPredict (@var{obj}) ## @deftypefnx {RegressionPartitionedModel} {[@var{yFit}, @var{ySD}, @var{yInt}] =} kfoldPredict (@var{obj}) ## @deftypefnx {RegressionPartitionedModel} {[@dots{}] =} kfoldPredict (@dots{}, @qcode{'Alpha'}, @var{alpha}) ## ## Predict the response of every observation from the fold that held it ## out. ## ## @code{@var{yFit} = kfoldPredict (@var{obj})} returns a column vector ## with one entry per observation, each predicted by the fold's model that ## did not see it during training. An observation no fold tests, which a ## holdout partition leaves outside its test set, comes back @code{NaN}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{RegressionPartitionedModel} class object. ## @end itemize ## ## @code{[@var{yFit}, @var{ySD}, @var{yInt}] = kfoldPredict (@var{obj})} ## also returns the standard deviation @var{ySD} of each predicted ## response and the two-column matrix @var{yInt} of prediction intervals, ## each answered for by the fold that held the observation out. A ## @qcode{RegressionGP} backing is the only one that fits the uncertainty ## its predictions carry, so any other raises here. An untested ## observation is @code{NaN} in all three. ## ## @code{[@dots{}] = kfoldPredict (@dots{}, @qcode{'Alpha'}, @var{alpha})} ## sets the significance level of the prediction intervals, which default ## to 95 per cent at an @var{alpha} of 0.05. ## ## @var{ySD} does not follow @qcode{ResponseTransform} and the other two ## outputs do, the same rule @code{RegressionGP.predict} applies: a ## predicted response and an interval endpoint are on the response scale ## and a standard deviation is not. ## ## @seealso{RegressionPartitionedModel, ## RegressionPartitionedModel.kfoldLoss} ## @end deftypefn function [yFit, ySD, yInt] = kfoldPredict (this, varargin) ## Only the GP fits the uncertainty around its predictions, so it is the ## only backing whose folds can be asked for one. MATLAB refuses the ## network and the SVM outright and refuses the GAM unless it was fitted ## with 'FitStandardDeviation', an option this package does not offer, so ## every backing but the GP is refused here under one message. is_gp = strcmp (this.CrossValidatedModel, 'GP'); if (nargout > 1 && ! is_gp) error (strcat ("RegressionPartitionedModel.kfoldPredict: a", ... " standard deviation and a prediction interval are", ... " only available for a cross-validated", ... " RegressionGP.")); endif ## Validated here rather than in the fold's predict, so the message ## names the method the user called. if (numel (varargin) > 0 && ! is_gp) error (strcat ("RegressionPartitionedModel.kfoldPredict:", ... " optional arguments are only accepted for a", ... " cross-validated RegressionGP.")); endif if (mod (numel (varargin), 2) != 0) error (strcat ("RegressionPartitionedModel.kfoldPredict:", ... " optional arguments must be given in Name-Value", ... " pairs.")); endif ## Parse optional paired arguments optNames = {'Alpha'}; dfValues = {0.05}; [CIAlpha, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (isnumeric (CIAlpha) && isscalar (CIAlpha) && ... CIAlpha >= 0 && CIAlpha <= 1)) error (strcat ("RegressionPartitionedModel.kfoldPredict: 'Alpha'", ... " must be a scalar between 0 and 1.")); endif if (! isempty (args)) error (strcat ("RegressionPartitionedModel.kfoldPredict: invalid", ... " optional paired argument.")); endif yFit = nan (this.NumObservations, 1); if (nargout > 1) ySD = nan (this.NumObservations, 1); yInt = nan (this.NumObservations, 2); endif for k = 1:this.KFold testIdx = test (this.Partition, k); if (! any (testIdx)) continue; endif if (nargout > 2) [yFit(testIdx), ySD(testIdx), yInt(testIdx,:)] = ... predict (this.Trained{k}, this.X(testIdx, :), 'Alpha', CIAlpha); elseif (nargout > 1) [yFit(testIdx), ySD(testIdx)] = ... predict (this.Trained{k}, this.X(testIdx, :)); else yFit(testIdx) = predict (this.Trained{k}, this.X(testIdx, :)); endif endfor ## As on the classification side, the folds never carry the transform and ## it is applied once to the assembled predictions. MathWorks documents ## ResponseTransform as the function for transforming the predicted ## response values and documents assigning it by dot notation, so this is ## what the property is for; it used to be stored and never read. yFit = this.RTfun (yFit); if (nargout > 2) yInt = this.RTfun (yInt); endif endfunction ## -*- texinfo -*- ## @deftypefn {RegressionPartitionedModel} {@var{L} =} kfoldLoss (@var{obj}) ## @deftypefnx {RegressionPartitionedModel} {@var{L} =} kfoldLoss (@dots{}, @var{name}, @var{value}) ## ## Compute the cross-validated regression loss. ## ## @code{@var{L} = kfoldLoss (@var{obj})} returns the weighted mean ## squared error between the response and the out-of-fold predictions of ## @code{kfoldPredict}, over every observation some fold tests. ## ## @itemize ## @item ## @var{obj} must be a @qcode{RegressionPartitionedModel} class object. ## @end itemize ## ## @code{@var{L} = kfoldLoss (@dots{}, @var{name}, @var{value})} accepts ## the following @qcode{Name-Value} pairs. ## ## @multitable @columnfractions 0.24 0.76 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'LossFun'} @tab @qcode{'mse'}, the default, ## @qcode{'epsiloninsensitive'}, or a function handle called as ## @code{@var{lossfun} (@var{Y}, @var{yFit}, @var{W})} returning a scalar. ## The @math{epsilon}-insensitive loss belongs to a support vector model ## and is refused for any other, there being no tube to measure against. ## ## @item @qcode{'Mode'} @tab @qcode{'average'}, the default, which returns ## one number over the observations of every fold asked for, or ## @qcode{'individual'}, which returns one number per fold. ## ## @item @qcode{'Folds'} @tab A vector of fold indices to restrict the ## loss to. It defaults to every fold. ## @end multitable ## ## @seealso{RegressionPartitionedModel, ## RegressionPartitionedModel.kfoldPredict} ## @end deftypefn function L = kfoldLoss (this, varargin) if (mod (numel (varargin), 2) != 0) error (strcat ("RegressionPartitionedModel.kfoldLoss: Name-Value", ... " arguments must be in pairs.")); endif ## Parse optional paired arguments optNames = {'LossFun', 'Mode', 'Folds'}; dfValues = {'mse', 'average', 1:this.KFold}; [LossFun, Mode, Folds, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (is_function_handle (LossFun) || (ischar (LossFun) && isrow (LossFun)))) error (strcat ("RegressionPartitionedModel.kfoldLoss: 'LossFun'", ... " must be a character vector or a function handle.")); endif if (ischar (LossFun) && ! any (strcmpi (LossFun, ... {'mse', 'epsiloninsensitive'}))) error (strcat ("RegressionPartitionedModel.kfoldLoss: unsupported", ... " 'LossFun' value.")); endif if (! (ischar (Mode) && isrow (Mode) && any (strcmpi (Mode, {'average', 'individual'})))) error (strcat ("RegressionPartitionedModel.kfoldLoss: 'Mode' must", ... " be either 'average' or 'individual'.")); endif if (! (isnumeric (Folds) && isvector (Folds) && all (Folds == fix (Folds)) && all (Folds >= 1) && all (Folds <= this.KFold))) error (strcat ("RegressionPartitionedModel.kfoldLoss: 'Folds' must", ... " be a vector of fold indices between 1 and KFold.")); endif if (! isempty (args)) error (strcat ("RegressionPartitionedModel.kfoldLoss: invalid", ... " optional paired argument.")); endif ## The insensitive tube is a property of a support vector model, so any ## other cross-validated model has nothing to measure against. Answering ## anyway would report a number for a quantity the model does not have. if (ischar (LossFun) && strcmpi (LossFun, 'epsiloninsensitive') && ! strcmp (this.CrossValidatedModel, 'SVM')) error (strcat ("RegressionPartitionedModel.kfoldLoss: the", ... " 'epsiloninsensitive' loss applies to a", ... " RegressionSVM model only.")); endif yFit = kfoldPredict (this); if (strcmpi (Mode, 'individual')) L = nan (numel (Folds), 1); for i = 1:numel (Folds) idx = test (this.Partition, Folds(i)); L(i) = foldLoss_ (this, idx, yFit, LossFun, Folds(i)); endfor else idx = false (this.NumObservations, 1); for i = 1:numel (Folds) idx = idx | test (this.Partition, Folds(i)); endfor L = foldLoss_ (this, idx, yFit, LossFun, Folds(1)); endif endfunction ## -*- texinfo -*- ## @deftypefn {RegressionPartitionedModel} {@var{vals} =} kfoldfun (@var{obj}, @var{fun}) ## ## Apply a function to each fold of a cross-validated model. ## ## @code{@var{vals} = kfoldfun (@var{obj}, @var{fun})} calls @var{fun} once ## per fold and returns a @math{K*M} numeric matrix whose row @math{k} is ## what @var{fun} returned for fold @math{k}. ## ## @var{fun} is a function handle taking seven inputs and returning a ## numeric vector of the same length every time it is called: ## ## @example ## @var{testvals} = @var{fun} (@var{M}, @var{Xtrain}, @var{Ytrain}, @dots{} ## @var{Wtrain}, @var{Xtest}, @var{Ytest}, @var{Wtest}) ## @end example ## ## @var{M} is the model the fold was fitted with, taken from ## @code{@var{obj}.Trained@{k@}}; @var{Xtrain}, @var{Ytrain} and ## @var{Wtrain} are the predictors, response and weights of the ## observations that fold was trained on, and @var{Xtest}, @var{Ytest} and ## @var{Wtest} those of the observations it held out. ## ## @seealso{RegressionPartitionedModel, ## RegressionPartitionedModel.kfoldPredict, ## RegressionPartitionedModel.kfoldLoss} ## @end deftypefn function vals = kfoldfun (this, fun) if (nargin < 2) error ("RegressionPartitionedModel.kfoldfun: too few input arguments."); endif if (! is_function_handle (fun)) error ("RegressionPartitionedModel.kfoldfun: FUN must be a function handle."); endif vals = []; for k = 1:this.KFold trIdx = training (this.Partition, k); teIdx = test (this.Partition, k); tv = fun (this.Trained{k}, this.X(trIdx,:), this.Y(trIdx,:), ... this.W(trIdx), this.X(teIdx,:), this.Y(teIdx,:), ... this.W(teIdx)); ## The returned values become one row, so they have to be numeric and ## of one length: a fold answering with a different width could not be ## stacked with the others, and finding that out at the concatenation ## would name neither the fold nor the reason. if (! ((isnumeric (tv) || islogical (tv)) && isvector (tv))) error (strcat ("RegressionPartitionedModel.kfoldfun: FUN must", ... " return a numeric vector; fold %d returned a %s."), ... k, class (tv)); endif tv = tv(:)'; if (! isempty (vals) && numel (tv) != columns (vals)) error (strcat ("RegressionPartitionedModel.kfoldfun: FUN must", ... " return the same number of values for every fold;", ... " fold %d returned %d where the first returned", ... " %d."), k, numel (tv), columns (vals)); endif vals = [vals; tv]; endfor endfunction endmethods methods (Access = private) ## Loss over the observations selected by IDX, weighted by W normalized ## over that selection so a subset is an average rather than a sum. function L = foldLoss_ (this, idx, yFit, LossFun, fold) if (! any (idx)) L = NaN; return; endif y = this.Y(idx); f = yFit(idx); w = double (this.W(idx)); w = w(:) / sum (w); if (is_function_handle (LossFun)) L = LossFun (y(:), f(:), w); if (! (isnumeric (L) && isscalar (L))) error (strcat ("RegressionPartitionedModel.kfoldLoss: 'LossFun'", ... " must return a numeric scalar.")); endif elseif (strcmpi (LossFun, 'epsiloninsensitive')) ## Every fold was fitted with the same Epsilon, so any trained model ## reports it; the one belonging to this fold is used for clarity. eps_ = this.Trained{fold}.Epsilon; L = sum (w .* max (0, abs (y(:) - f(:)) - eps_)); else L = sum (w .* (y(:) - f(:)) .^ 2); endif endfunction endmethods endclassdef ## crossval builds one compact model per fold, over the observations used. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = randn (40, 2); %! Y = X(:,1) - 2 * X(:,2); %! Mdl = fitrnet (X, Y, 'IterationLimit', 30); %! CVMdl = crossval (Mdl, 'KFold', 4); %! assert_equal (class (CVMdl), 'RegressionPartitionedModel'); %! assert_equal (CVMdl.KFold, 4); %! assert_equal (numel (CVMdl.Trained), 4); %! assert_equal (class (CVMdl.Trained{1}), 'CompactRegressionNeuralNetwork'); %! assert_equal (CVMdl.CrossValidatedModel, 'NeuralNetwork'); %! assert_equal (CVMdl.NumObservations, 40); %! assert_equal (CVMdl.ResponseName, 'Y'); %! assert_equal (class (CVMdl.Partition), 'cvpartition'); ## The same for a support vector model. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = randn (40, 2); %! Y = X(:,1) + X(:,2); %! CVMdl = crossval (fitrsvm (X, Y), 'KFold', 4); %! assert_equal (class (CVMdl.Trained{1}), 'CompactRegressionSVM'); %! assert_equal (CVMdl.CrossValidatedModel, 'SVM'); ## The same for a generalized additive model, which carries no parameter ## struct, so that property keeps its default. %!test %! load fisheriris %! CVMdl = crossval (fitrgam (meas(1:20,1:3), meas(1:20,4)), 'KFold', 4); %! assert_equal (class (CVMdl.Trained{1}), 'CompactRegressionGAM'); %! assert_equal (CVMdl.CrossValidatedModel, 'GAM'); %! assert_equal (CVMdl.NumObservations, 20); %! ## The learner's thirteen fields come through, with NLearn added and the %! ## three tags reissued for this class, so fourteen in all. %! assert_equal (isstruct (CVMdl.ModelParameters), true); %! assert_equal (numfields (CVMdl.ModelParameters), 14); %! assert_equal (isfield (CVMdl.ModelParameters, 'NumTreesPerPredictor'), true); %! assert_equal (CVMdl.ModelParameters.Method, 'PartitionedModel'); %!test # a GAM fold is fitted with the weights of the rows it holds %! k = (1:60)'; %! X = [sin(k), cos(2 * k)]; %! y = 2 * sin (k) + X(:,2) .^ 2; %! w = 1 + mod (k, 3); %! CVMdl = crossval (RegressionGAM (X, y, 'Weights', w), 'KFold', 3); %! idx = training (CVMdl.Partition, 1); %! F = fitrgam (X(idx,:), y(idx), 'Weights', w(idx)); %! assert_equal (CVMdl.Trained{1}.Intercept, F.Intercept, 1e-10); %!error ... %! RegressionPartitionedModel (1, cvpartition (10, 'KFold', 2)) ## Every observation is answered for by the fold that held it out. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = randn (30, 2); %! Y = X(:,1) * 2; %! CVMdl = crossval (fitrsvm (X, Y), 'KFold', 5); %! yFit = kfoldPredict (CVMdl); %! assert_equal (size (yFit), [30, 1]); %! assert_equal (any (isnan (yFit)), false); ## A holdout partition tests only its test set, and leaves the rest NaN. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = randn (40, 2); %! Y = X(:,1); %! CVMdl = crossval (fitrsvm (X, Y), 'Holdout', 0.25); %! yFit = kfoldPredict (CVMdl); %! assert_equal (CVMdl.KFold, 1); %! assert_equal (sum (! isnan (yFit)), sum (test (CVMdl.Partition, 1))); %! assert_equal (sum (isnan (yFit)), sum (training (CVMdl.Partition, 1))); ## Leaveout holds out one observation at a time. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = randn (12, 2); %! Y = X(:,1) + 1; %! CVMdl = crossval (fitrsvm (X, Y), 'Leaveout', 'on'); %! assert_equal (CVMdl.KFold, 12); %! assert_equal (any (isnan (kfoldPredict (CVMdl))), false); ## A cvpartition object is taken as given. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = randn (30, 2); %! Y = X(:,2); %! cvp = cvpartition (30, 'KFold', 3); %! CVMdl = crossval (fitrsvm (X, Y), 'CVPartition', cvp); %! assert_equal (CVMdl.KFold, 3); %! assert_equal (CVMdl.Partition.NumObservations, 30); ## kfoldLoss is the weighted mean squared error of the out-of-fold answers. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = randn (40, 2); %! Y = X(:,1) - X(:,2); %! CVMdl = crossval (fitrsvm (X, Y), 'KFold', 4); %! yFit = kfoldPredict (CVMdl); %! assert_equal (kfoldLoss (CVMdl), mean ((Y - yFit) .^ 2), 1e-12); %! assert_equal (kfoldLoss (CVMdl, 'LossFun', 'mse'), ... %! kfoldLoss (CVMdl), 1e-12); ## 'individual' returns one loss per fold, and each is that fold's own. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = randn (40, 2); %! Y = X(:,1) * 3; %! CVMdl = crossval (fitrsvm (X, Y), 'KFold', 4); %! yFit = kfoldPredict (CVMdl); %! L = kfoldLoss (CVMdl, 'Mode', 'individual'); %! assert_equal (size (L), [4, 1]); %! idx = test (CVMdl.Partition, 2); %! assert_equal (L(2), mean ((Y(idx) - yFit(idx)) .^ 2), 1e-12); ## 'Folds' restricts the loss to the folds named. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = randn (40, 2); %! Y = X(:,2) - 1; %! CVMdl = crossval (fitrsvm (X, Y), 'KFold', 4); %! yFit = kfoldPredict (CVMdl); %! idx = test (CVMdl.Partition, 1) | test (CVMdl.Partition, 3); %! assert_equal (kfoldLoss (CVMdl, 'Folds', [1, 3]), ... %! mean ((Y(idx) - yFit(idx)) .^ 2), 1e-12); %! assert_equal (numel (kfoldLoss (CVMdl, 'Folds', [2, 4], ... %! 'Mode', 'individual')), 2); ## The epsilon-insensitive loss uses the tube the folds were fitted with. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = randn (40, 2); %! Y = X(:,1) * 2; %! CVMdl = crossval (fitrsvm (X, Y, 'Epsilon', 0.4), 'KFold', 4); %! yFit = kfoldPredict (CVMdl); %! assert_equal (kfoldLoss (CVMdl, 'LossFun', 'epsiloninsensitive'), ... %! mean (max (0, abs (Y - yFit) - 0.4)), 1e-12); ## kfoldLoss takes a function handle of the response, the fit and the weights. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = randn (30, 2); %! Y = X(:,1); %! CVMdl = crossval (fitrsvm (X, Y), 'KFold', 3); %! yFit = kfoldPredict (CVMdl); %! f = @(y, yf, w) sum (w .* abs (y - yf)); %! assert_equal (kfoldLoss (CVMdl, 'LossFun', f), ... %! mean (abs (Y - yFit)), 1e-12); ## Rows dropped for missing values are outside the partition entirely. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = [randn(20, 2); NaN, 1]; %! Y = [randn(20, 1); 3]; %! Mdl = fitrsvm (X, Y); %! CVMdl = crossval (Mdl, 'KFold', 4); %! assert_equal (CVMdl.NumObservations, 21); %! assert_equal (rows (CVMdl.X), 21); %! assert_equal (numel (kfoldPredict (CVMdl)), 21); ## A GP backing answers for the uncertainty of its out-of-fold predictions. %!test %! load fisheriris %! CVMdl = crossval (fitrgp (meas(:,1:3), meas(:,4)), 'KFold', 5); %! [yFit, ySD, yInt] = kfoldPredict (CVMdl); %! assert_equal (size (yFit), [150, 1]); %! assert_equal (size (ySD), [150, 1]); %! assert_equal (size (yInt), [150, 2]); %! assert_equal (all (ySD > 0), true); %! assert_equal (yFit, kfoldPredict (CVMdl), 1e-12); ## The interval is the prediction plus and minus a normal quantile of the ## standard deviation, and 'Alpha' sets which quantile. %!test %! load fisheriris %! CVMdl = crossval (fitrgp (meas(:,1:3), meas(:,4)), 'KFold', 5); %! [yFit, ySD, yInt] = kfoldPredict (CVMdl); %! z = norminv (0.975); %! assert_equal (yInt, [yFit - z * ySD, yFit + z * ySD], 1e-12); %! [~, ~, yInt90] = kfoldPredict (CVMdl, 'Alpha', 0.10); %! z90 = norminv (0.95); %! assert_equal (yInt90, [yFit - z90 * ySD, yFit + z90 * ySD], 1e-12); %! assert_equal (all (diff (yInt90, 1, 2) < diff (yInt, 1, 2)), true); ## Each observation is answered for by the fold that held it out, in all three ## outputs, and one no fold tests is NaN in all three. %!test %! load fisheriris %! CVMdl = crossval (fitrgp (meas(:,1:3), meas(:,4)), 'KFold', 5); %! [yFit, ySD, yInt] = kfoldPredict (CVMdl); %! idx = test (CVMdl.Partition, 2); %! [p, s, i] = predict (CVMdl.Trained{2}, meas(idx,1:3)); %! assert_equal (p, yFit(idx), 1e-12); %! assert_equal (s, ySD(idx), 1e-12); %! assert_equal (i, yInt(idx,:), 1e-12); %!test %! load fisheriris %! CVMdl = crossval (fitrgp (meas(:,1:3), meas(:,4)), 'Holdout', 0.3); %! [yFit, ySD, yInt] = kfoldPredict (CVMdl); %! untested = isnan (yFit); %! assert_equal (any (untested), true); %! assert_equal (isnan (ySD), untested); %! assert_equal (isnan (yInt), [untested, untested]); ## ResponseTransform reaches the two outputs on the response scale and not the ## standard deviation, the rule RegressionGP.predict applies. %!test %! load fisheriris %! CVMdl = crossval (fitrgp (meas(:,1:3), meas(:,4)), 'KFold', 5); %! [yFit, ySD, yInt] = kfoldPredict (CVMdl); %! CVMdl.ResponseTransform = @(x) 2 * x; %! [yFit2, ySD2, yInt2] = kfoldPredict (CVMdl); %! assert_equal (yFit2, 2 * yFit, 1e-12); %! assert_equal (ySD2, ySD, 1e-12); %! assert_equal (yInt2, 2 * yInt, 1e-12); ## Measured against R2024a over the same folds, an explicit CustomPartition ## naming them in both engines so the assembled values are comparable at all. %!test %! load fisheriris %! cvp = cvpartition ('CustomPartition', repmat ((1:5)', 30, 1)); %! CVMdl = crossval (fitrgp (meas(:,1:3), meas(:,4)), 'CVPartition', cvp); %! [yFit, ySD, yInt] = kfoldPredict (CVMdl); %! assert_equal (yFit(1), 0.21819014717001, 1e-9); %! assert_equal (ySD(1), 0.17986522125163, 1e-9); %! assert_equal (yInt(1,:), [-0.13433920855451, 0.57071950289454], 1e-9); %! assert_equal (sum (abs (yFit)), 179.61497470708, 1e-6); %! assert_equal (sum (abs (ySD)), 27.747043040728, 1e-6); %! [~, ~, yInt90] = kfoldPredict (CVMdl, 'Alpha', 0.10); %! assert_equal (yInt90(1,:), [-0.077661814368161, 0.51404210870819], 1e-9); ## No other backing fits the uncertainty around its predictions, so none of ## them may be asked for it. %!error ... %! load fisheriris; ... %! CVMdl = crossval (fitrsvm (meas(:,1:3), meas(:,4)), 'KFold', 3); ... %! [yFit, ySD] = kfoldPredict (CVMdl); %!error ... %! load fisheriris; ... %! CVMdl = crossval (fitrgam (meas(:,1:3), meas(:,4)), 'KFold', 3); ... %! [yFit, ySD, yInt] = kfoldPredict (CVMdl); %!error ... %! load fisheriris; ... %! CVMdl = crossval (fitrsvm (meas(:,1:3), meas(:,4)), 'KFold', 3); ... %! kfoldPredict (CVMdl, 'Alpha', 0.1); %!error ... %! load fisheriris; ... %! CVMdl = crossval (fitrgp (meas(:,1:3), meas(:,4)), 'KFold', 3); ... %! kfoldPredict (CVMdl, 'Alpha'); %!error ... %! load fisheriris; ... %! CVMdl = crossval (fitrgp (meas(:,1:3), meas(:,4)), 'KFold', 3); ... %! kfoldPredict (CVMdl, 'Alpha', 2); %!error ... %! load fisheriris; ... %! CVMdl = crossval (fitrgp (meas(:,1:3), meas(:,4)), 'KFold', 3); ... %! kfoldPredict (CVMdl, 'Bogus', 1); %!test # MATLAB parity: a cross-validated regression tree, over compact folds %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! a = fitrtree (X, MPG); %! cvModel = crossval (a, 'KFold', 5); %! assert_equal (class (cvModel), "RegressionPartitionedModel"); %! assert_equal (cvModel.CrossValidatedModel, "Tree"); %! assert_equal (class (cvModel.Trained{1}), "CompactRegressionTree"); %! assert_equal (cvModel.KFold, 5); %! assert_equal (cvModel.NumObservations, 94); %! assert_equal (cvModel.ResponseTransform, 'none'); %!test # A tree fold is grown with the parameters the parent was grown with %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! a = fitrtree (X, MPG, 'MinLeafSize', 15, 'QuadraticErrorTolerance', 0.01); %! cvModel = crossval (a, 'KFold', 3); %! assert_equal (cvModel.ModelParameters.SplitCriterion, 'mse'); %! assert_equal (cvModel.ModelParameters.MinLeaf, 15); %! assert_equal (cvModel.ModelParameters.QEToler, 0.01); %! assert_equal (min (cvModel.Trained{1}.NodeSize) >= 15, true); %!test # kfoldPredict answers every observation of a cross-validated tree %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! cvModel = crossval (fitrtree (X, MPG), 'KFold', 5); %! yFit = kfoldPredict (cvModel); %! assert_equal (size (yFit), [94, 1]); %! assert_equal (any (isnan (yFit)), false); %! assert_equal (kfoldLoss (cvModel) > 0, true); %! assert_equal (size (kfoldLoss (cvModel, 'Mode', 'individual')), [5, 1]); %!error %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! [y, sd] = kfoldPredict (crossval (fitrtree (X, MPG), 'KFold', 3)); ## Test input validation for the constructor %!error ... %! RegressionPartitionedModel () %!error ... %! RegressionPartitionedModel (fitrsvm (ones (5, 2), [1; 2; 3; 4; 5])) %!error ... %! RegressionPartitionedModel (5, cvpartition (5, 'KFold', 2)) %!error ... %! RegressionPartitionedModel (fitcnet (ones (4, 2), [1; 1; 2; 2]), ... %! cvpartition (4, 'KFold', 2)) %!error ... %! RegressionPartitionedModel (fitrsvm (ones (5, 2), [1; 2; 3; 4; 5]), 5) %!error ... %! RegressionPartitionedModel (fitrsvm (ones (5, 2), [1; 2; 3; 4; 5]), ... %! cvpartition (9, 'KFold', 3)) ## Test input validation for kfoldLoss %!shared CVR %! rand ('seed', 42); randn ('seed', 42); %! CVR = crossval (fitrsvm (randn (20, 2), randn (20, 1)), 'KFold', 4); %!error ... %! kfoldLoss (CVR, 'Mode') %!error ... %! kfoldLoss (CVR, 5, 1) %!error ... %! kfoldLoss (CVR, 'LossFun', 5) %!error ... %! kfoldLoss (CVR, 'LossFun', 'mae') %!error ... %! kfoldLoss (CVR, 'LossFun', @(y, yf, w) [1, 2]) %!error ... %! kfoldLoss (CVR, 'Mode', 'nope') %!error ... %! kfoldLoss (CVR, 'Folds', 0) %!error ... %! kfoldLoss (CVR, 'Folds', 9) %!error ... %! kfoldLoss (CVR, 'Nope', 1) ## The insensitive tube belongs to a support vector model only. %!error ... %! kfoldLoss (crossval (fitrnet (randn (20, 2), randn (20, 1), ... %! 'IterationLimit', 5), 'KFold', 4), ... %! 'LossFun', 'epsiloninsensitive') ## BinEdges is an empty cell, and a cell rather than an empty matrix, matching ## the classification counterpart and MATLAB. %!test %! load fisheriris %! CVMdl = crossval (fitrsvm (meas(:,1:3), meas(:,4)), 'KFold', 3); %! assert_equal (class (CVMdl.BinEdges), 'cell'); %! assert_equal (CVMdl.BinEdges, {}); ## No fold carries the transform, whichever model was cross validated. The ## parent applies it once to the assembled prediction, so a fold that kept one ## of its own would apply it twice. %!test %! load fisheriris %! CVMdl = crossval (fitrgam (meas(:,1:3), meas(:,4), ... %! 'ResponseTransform', 'exp'), 'KFold', 3); %! assert_equal (CVMdl.ResponseTransform, 'exp'); %! assert_equal (CVMdl.Trained{1}.ResponseTransform, 'none'); %!test %! load fisheriris %! CVMdl = crossval (fitrnet (meas(:,1:3), meas(:,4), ... %! 'ResponseTransform', 'exp'), 'KFold', 3); %! assert_equal (CVMdl.ResponseTransform, 'exp'); %! assert_equal (CVMdl.Trained{1}.ResponseTransform, 'none'); %!test %! load fisheriris %! CVMdl = crossval (fitrgp (meas(:,1:3), meas(:,4), ... %! 'ResponseTransform', 'exp'), 'KFold', 3); %! assert_equal (CVMdl.ResponseTransform, 'exp'); %! assert_equal (CVMdl.Trained{1}.ResponseTransform, 'none'); ## ResponseTransform is applied to the assembled predictions, not carried into ## the folds. MathWorks documents it as the function for transforming the ## predicted response values; it used to be stored here and never read. %!test %! load fisheriris %! CVMdl = crossval (fitrsvm (meas(:,1:3), meas(:,4)), 'KFold', 3); %! y0 = kfoldPredict (CVMdl); %! CVMdl.ResponseTransform = @(x) x + 100; %! y1 = kfoldPredict (CVMdl); %! assert_equal (CVMdl.Trained{1}.ResponseTransform, 'none'); %! assert_equal (y1, y0 + 100, 1e-12); ## And it reaches kfoldLoss, which is computed from those predictions. %!test %! load fisheriris %! CVMdl = crossval (fitrsvm (meas(:,1:3), meas(:,4)), 'KFold', 3); %! before = kfoldLoss (CVMdl); %! CVMdl.ResponseTransform = @(x) x + 100; %! assert (kfoldLoss (CVMdl) > before); ## 'none' is the identity, so the default transforms nothing. %!test %! load fisheriris %! CVMdl = crossval (fitrnet (meas(:,1:3), meas(:,4)), 'KFold', 3); %! y0 = kfoldPredict (CVMdl); %! CVMdl.ResponseTransform = 'none'; %! assert_equal (kfoldPredict (CVMdl), y0); ## foldLoss_ is a private helper and stays out of the method list. %!test %! assert_equal (any (strcmp (methods ("RegressionPartitionedModel"), ... %! "foldLoss_")), false); ## A fold is refitted with the argument set of the engine that fitted the ## parent, so a tree-fitted parent must not hand its folds Knots and Order. %!test %! load fisheriris %! Mdl = fitrgam (meas(:,2:4), meas(:,1), 'FitMethod', 'boostedtrees'); %! CVMdl = crossval (Mdl, 'KFold', 3); %! assert_equal (class (CVMdl), 'RegressionPartitionedModel'); %! assert_equal (numel (CVMdl.Trained), 3); %! assert_equal (CVMdl.Trained{1}.FitMethod, 'boostedtrees'); %! assert_equal (numel (kfoldPredict (CVMdl)), rows (meas)); ## A spline-fitted parent is unaffected: its folds take the spline parameters ## and are fitted by the same engine. They do not report the parameters ## themselves, a compact model keeping no record of its fitting. %!test %! load fisheriris %! Mdl = fitrgam (meas(1:60,1:3), meas(1:60,4), 'FitMethod', 'splines', ... %! 'Knots', 4); %! CVMdl = crossval (Mdl, 'KFold', 3); %! assert_equal (CVMdl.Trained{1}.FitMethod, 'splines'); %! assert_equal (isfield (CVMdl.Trained{1}.BaseModel, 'Intercept'), true); %!test %! ## kfoldfun hands over seven arguments, the fold's model first. %! load fisheriris %! CV = crossval (fitrgam (meas(:,2:4), meas(:,1)), "KFold", 3); %! seen = kfoldfun (CV, @(M, Xtr, Ytr, Wtr, Xte, Yte, Wte) ... %! [rows(Xtr), rows(Yte), columns(Xtr)]); %! assert_equal (size (seen), [3, 3]); %! assert_equal (seen(:,1) + seen(:,2), repmat (150, 3, 1)); %! assert_equal (seen(:,3), repmat (3, 3, 1)); %!test %! ## The use it exists for: a held-out mean squared error per fold. %! load fisheriris %! CV = crossval (fitrgam (meas(:,2:4), meas(:,1)), "KFold", 3); %! f = @(M, Xtr, Ytr, Wtr, Xte, Yte, Wte) mean ((predict (M, Xte) - Yte) .^ 2); %! mse = kfoldfun (CV, f); %! assert_equal (size (mse), [3, 1]); %! assert_equal (all (mse > 0), true); %!error ... %! kfoldfun (crossval (fitrgam (ones (8, 2), (1:8)'), "KFold", 2)) %!error ... %! kfoldfun (crossval (fitrgam (ones (8, 2), (1:8)'), "KFold", 2), "nope") %!test %! ## The property order is MATLAB's, measured on R2024a. %! load fisheriris %! CVMdl = crossval (fitrsvm (meas(:,2:4), meas(:,1)), "KFold", 3); %! assert_equal (sort (properties (CVMdl)), ... %! sort ({'ResponseTransform'; 'CrossValidatedModel'; ... %! 'PredictorNames'; 'CategoricalPredictors'; ... %! 'ResponseName'; 'NumObservations'; 'X'; 'Y'; 'W'; ... %! 'ModelParameters'; 'Trained'; 'KFold'; 'Partition'; ... %! 'BinEdges'; 'IsStandardDeviationFit'; ... %! 'NumTrainedPerFold'})); %!test %! ## IsStandardDeviationFit comes from the model for a GAM backing and is %! ## empty for every other, one class serving all of them. %! load fisheriris %! CVg = crossval (fitrgam (meas(:,2:4), meas(:,1)), "KFold", 3); %! assert_equal (islogical (CVg.IsStandardDeviationFit), true); %! assert_equal (CVg.IsStandardDeviationFit, false); %! CVs = crossval (fitrsvm (meas(:,2:4), meas(:,1)), "KFold", 3); %! assert_equal (isempty (CVs.IsStandardDeviationFit), true); %!test %! ## NumTrainedPerFold reports what each fold actually fitted. On this %! ## fixture every fold uses its whole budget, which is what R2024a reports %! ## for it as well. %! load fisheriris %! CVMdl = crossval (fitrgam (meas(:,2:4), meas(:,1)), "KFold", 3); %! n = CVMdl.NumTrainedPerFold; %! assert_equal (sort (fieldnames (n)), {"InteractionTrees"; "PredictorTrees"}); %! assert_equal (n.PredictorTrees, [300, 300, 300]); %! assert_equal (n.InteractionTrees, [0, 0, 0]); %!test %! ## Empty for a backing that fits no trees. %! load fisheriris %! CVMdl = crossval (fitrsvm (meas(:,2:4), meas(:,1)), "KFold", 3); %! assert_equal (isempty (CVMdl.NumTrainedPerFold), true); ## A polynomial-kernel SVM refits its folds through the recorded order, which ## the parent carries only under that kernel. %!test %! load fisheriris %! Mdl = fitrsvm (meas(:,2:4), meas(:,1), 'KernelFunction', 'polynomial', ... %! 'PolynomialOrder', 2); %! CVMdl = crossval (Mdl, 'KFold', 3); %! assert_equal (CVMdl.ModelParameters.KernelPolynomialOrder, 2); %! assert_equal (numel (CVMdl.Trained), 3); ## ModelParameters carries the learner's parameters under this class's own ## tags. %!test %! load fisheriris %! CVMdl = crossval (fitrsvm (meas(:,2:4), meas(:,1)), 'KFold', 3); %! MP = CVMdl.ModelParameters; %! assert_equal (MP.Method, 'PartitionedModel'); %! assert_equal (MP.Type, 'regression'); %! assert_equal (MP.Version, 1); %! assert_equal (MP.NLearn, 3); %! assert_equal (MP.SVMtype, 'eps_svr'); %!test %! load fisheriris %! MP = crossval (fitrgp (meas(:,2:4), meas(:,1)), 'KFold', 3).ModelParameters; %! assert_equal (MP.FitMethod, 'Exact'); %! assert_equal (MP.Method, 'PartitionedModel'); ## Every documented response transform reaches the response that is reported. %!test %! load fisheriris %! Mdl = crossval (fitrsvm (meas(:,2:4), meas(:,1)), 'KFold', 3); %! Mdl.ResponseTransform = 'none'; %! raw = kfoldPredict (Mdl); %! T = {'identity', @(x) x; 'exp', @(x) exp (x); 'log', @(x) log (x)}; %! for i = 1:rows (T) %! Mdl.ResponseTransform = T{i,1}; %! yhat = kfoldPredict (Mdl); %! assert_equal (yhat, T{i,2}(raw), 1e-12); %! endfor ## A function handle is taken as given and applied to the response. %!test %! load fisheriris %! Mdl = crossval (fitrsvm (meas(:,2:4), meas(:,1)), 'KFold', 3); %! Mdl.ResponseTransform = 'none'; %! raw = kfoldPredict (Mdl); %! Mdl.ResponseTransform = @(x) x .^ 2; %! yhat = kfoldPredict (Mdl); %! assert_equal (yhat, raw .^ 2, 1e-12); %!test %! ## A support vector regression fold is fitted with its rows' weights %! load fisheriris %! X = meas(:,2:4); %! y = meas(:,1); %! w = 1 + (1:150)' / 7; %! CVMdl = crossval (fitrsvm (X, y, 'Weights', w), 'KFold', 3); %! idx = training (CVMdl.Partition, 1); %! Mdl = fitrsvm (X(idx,:), y(idx), 'Weights', CVMdl.W(idx), ... %! 'Epsilon', CVMdl.ModelParameters.Epsilon); %! assert_equal (predict (CVMdl.Trained{1}, X), predict (Mdl, X), 1e-14); %!test %! ## A neural network fold is fitted with its rows' weights %! load fisheriris %! w = 1 + (1:150)' / 7; %! Mdl = fitrnet (meas(:,2:4), meas(:,1), 'LayerSizes', 3, 'Weights', w); %! rand ('seed', 7); %! CVMdl = crossval (Mdl, 'KFold', 3); %! idx = training (CVMdl.Partition, 1); %! rand ('seed', 7); %! cvpartition (150, 'KFold', 3); %! F = fitrnet (meas(idx,2:4), meas(idx,1), 'LayerSizes', 3, ... %! 'Weights', Mdl.W(idx)); %! assert_equal (predict (CVMdl.Trained{1}, meas(:,2:4)), ... %! predict (F, meas(:,2:4)), 1e-10); statistics-release-2.1.2/inst/Machine_Learning/RegressionSVM.m000066400000000000000000002632521527311104300243570ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef RegressionSVM < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} RegressionSVM ## ## Support vector machine regression model. ## ## A @qcode{RegressionSVM} object holds a support vector machine fitted to a ## continuous response by @math{epsilon}-insensitive regression, and predicts ## the response for new data with the @code{predict} method. Errors smaller ## than @qcode{Epsilon} cost nothing, so only the observations outside that ## tube become support vectors, and a prediction is a weighted sum of kernel ## evaluations against them. The fit is carried out by LIBSVM. ## ## The object keeps its training data, which @code{resubPredict}, ## @code{resubLoss} and @code{crossval} work on; @code{compact} drops it and ## returns a @code{CompactRegressionSVM}, which still predicts. ## ## Create a @qcode{RegressionSVM} object with @code{fitrsvm} or the class ## constructor. ## ## @seealso{fitrsvm, CompactRegressionSVM, ClassificationSVM} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} X ## ## Predictor data ## ## An @math{NxP} numeric matrix, as it was supplied to the constructor. ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## This property is read-only. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} Y ## ## Response data ## ## An @math{Nx1} numeric vector, as it was supplied to the constructor. ## This property is read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} NumObservations ## ## Number of observations used to train the model ## ## A positive integer scalar, counting only the rows that survived the ## removal of missing values. This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} RowsUsed ## ## Rows used for fitting ## ## A logical column vector with the same length as the observations in the ## original predictor data @var{X}, true for each row that was used for ## fitting the RegressionSVM model. It is empty, @qcode{[]}, ## when every observation was used, so a non-empty value means that rows ## holding missing values were dropped. This property is read-only. ## ## @end deftp RowsUsed = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} NumPredictors ## ## Number of predictors ## ## A positive integer scalar. This property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors. This property is read-only. ## ## @end deftp PredictorNames = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} ResponseName ## ## Name of the response variable ## ## A character vector. This property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} Epsilon ## ## Half-width of the insensitive tube ## ## A non-negative scalar. An error smaller than @code{Epsilon} costs ## nothing, so only observations outside the tube become support vectors. ## This property is read-only. ## ## @end deftp Epsilon = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} Sigma ## ## Standard deviation of the predictors ## ## A row vector with one entry per predictor, used for standardization. ## Empty when the predictor data were not standardized. This property is ## read-only. ## ## @end deftp Sigma = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} Mu ## ## Mean of the predictors ## ## A row vector with one entry per predictor, used for standardization. ## Empty when the predictor data were not standardized. This property is ## read-only. ## ## @end deftp Mu = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} ModelParameters ## ## Parameters the model was fitted with ## ## A structure holding the SVM formulation, the kernel and its parameters, ## the box constraint, @code{Epsilon} and the solver settings. The engine ## is LIBSVM and the record is LIBSVM's, so @qcode{SVMtype} names its ## formulation and @qcode{Tolerance} and @qcode{Shrinking} are its own ## controls; the parameters MathWorks reports for its SMO and ISDA solvers ## are absent, this class running neither. ## ## @qcode{KernelPolynomialOrder} belongs to the polynomial kernel alone ## and is empty under every other, as it is in MATLAB. @qcode{Nu} is ## reported here where MATLAB leaves it empty on a regression model, this ## class offering @qcode{'nu_svr'} through @qcode{SVMtype} and the value ## being a real one. This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} Alpha ## ## Dual coefficients of the support vectors ## ## A numeric column vector with one entry per support vector, holding the ## difference of the two multipliers each observation carries. Unlike a ## classifier's, these are signed: there are no labels to take the sign ## into, so an observation above the tube and one below it are told apart ## by the sign of its coefficient. This property is read-only. ## ## @end deftp Alpha = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} Beta ## ## Primal coefficients, one per predictor ## ## A numeric column vector, equal to ## @code{(obj.SupportVectors / s)' * obj.Alpha}, where @math{s} is the ## kernel scale, so that a prediction is @code{(@var{x} / s) * Beta + Bias}, ## as in MATLAB. It exists only for a linear kernel; for any other kernel ## there is no primal representation and this is empty. This property is ## read-only. ## ## @end deftp Beta = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} Bias ## ## Intercept of the fitted function ## ## A numeric scalar. With a linear kernel the prediction is ## @code{X * obj.Beta + obj.Bias}. This property is read-only. ## ## @end deftp Bias = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} IsSupportVector ## ## Which training observations are support vectors ## ## A logical column vector with one entry per training observation. This ## property is read-only. ## ## @end deftp IsSupportVector = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} SupportVectors ## ## The support vectors themselves ## ## A numeric matrix with one row per support vector, on the scale the ## model was trained on, standardized where @code{Mu} is non-empty. ## This property is read-only. ## ## @end deftp SupportVectors = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} KernelParameters ## ## Parameters of the kernel function ## ## A structure with fields @qcode{Function} and @qcode{Scale}, and ## @qcode{Order} for a polynomial kernel. @qcode{Function} names the ## kernel as MATLAB names it, so a radial basis kernel reports ## @qcode{'gaussian'} whichever spelling was given; the kernel the fit was ## handed is unchanged in @qcode{ModelParameters}. This property is ## read-only. ## ## @end deftp KernelParameters = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} BoxConstraints ## ## Box constraints ## ## A numeric column vector with one entry per observation, holding the box ## constraint the fit applied to it: @math{n} times @qcode{BoxConstraint} ## times the observation's weight in @qcode{W}, which is ## @qcode{BoxConstraint} for every observation when no weights were given. ## An observation missing a predictor is not fitted and holds @code{NaN}. ## This property is read-only. ## ## @end deftp BoxConstraints = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A numeric vector of column indices, and empty when none is. This ## property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} ExpandedPredictorNames ## ## Names of the predictors as the model expanded them ## ## A cell array of character vectors. This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} W ## ## Observation weights ## ## A numeric column vector with one entry per training observation, ## normalized to sum to one, as MATLAB reports it. It has the class of the ## @qcode{'Weights'} given, single or double. This property is read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} BinEdges ## ## Bin edges of the predictors ## ## A cell array with one entry per predictor, holding that predictor's bin ## edges where the learner discretized it before fitting. It is empty here ## and stays empty: this learner fits the predictors as they are, and ## MATLAB's reports an empty cell for it as well. ## ## This property is read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} HyperparameterOptimizationResults ## ## Results of the hyperparameter optimization ## ## @strong{Always empty.} It is declared for MATLAB compatibility, where ## it holds what an automatic search over the hyperparameters found. This ## class fits the parameters it is given and runs no such search, so there ## is nothing to report. This property is read-only. ## ## @end deftp HyperparameterOptimizationResults = []; endproperties ## The LIBSVM structure the engine works in. It is ours alone, with no ## MATLAB counterpart, so it is kept out of the property listing while ## staying readable for anyone who needs the raw model. properties (Hidden, GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} Model ## ## The trained LIBSVM model ## ## A structure as returned by @code{svmtrain} and consumed by ## @code{svmpredict}. This property is read-only. ## ## It is the engine's own structure and has no MATLAB counterpart, ## so it is kept out of @code{properties} and out of the online ## documentation. Reading it works exactly as it always did. ## ## @end deftp Model = []; endproperties ## Properties a user may set after the model is built. Each one is ## validated by its set method below. properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {RegressionSVM} {property} ResponseTransform ## ## Transformation applied to the predicted response ## ## A function handle, applied by @code{predict} and @code{resubPredict} to ## the model's output. It defaults to the identity and may be set after ## construction, either to a handle or to the name of a supported ## transformation. ## ## @end deftp ResponseTransform = 'none'; endproperties ## Readable by the counterpart class, which copies it, and kept out of ## the documented surface. properties (GetAccess = public, SetAccess = protected, Hidden) ## The dummy coding of the categorical predictors, empty when none. Coding_ = []; ## The prediction for a row missing a predictor. MissingResponse_ = NaN; RTfun = @(y) y; endproperties ## Set methods for the properties a user may assign. methods (Hidden) function this = set.ResponseTransform (this, val) name = 'RegressionSVM'; [this.RTfun, this.ResponseTransform] = parseResponseTransform (val, name); endfunction ## Custom display function disp (this) fprintf ("\n RegressionSVM\n\n"); ## Print selected properties fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName); fprintf ("%+25s: %d\n", 'NumObservations', this.NumObservations); fprintf ("%+25s: %d\n", 'NumPredictors', this.NumPredictors); fprintf ("%+25s: '%s'\n", 'ResponseTransform', this.ResponseTransform); fprintf ("%+25s: %g\n", 'Epsilon', this.Epsilon); fprintf ("%+25s: [%dx1 double]\n", 'Alpha', numel (this.Alpha)); if (! isempty (this.Beta)) fprintf ("%+25s: [%dx1 double]\n", 'Beta', numel (this.Beta)); endif fprintf ("%+25s: %f\n", 'Bias', this.Bias); fprintf ("%+25s: '%s'\n", 'KernelFunction', ... this.ModelParameters.KernelFunction); if (! isempty (this.Mu)) fprintf ("%+25s: [1x%d double]\n", 'Mu', numel (this.Mu)); fprintf ("%+25s: [1x%d double]\n", 'Sigma', numel (this.Sigma)); endif endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {RegressionSVM} {@var{obj} =} RegressionSVM (@var{X}, @var{Y}) ## @deftypefnx {RegressionSVM} {@var{obj} =} RegressionSVM (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {RegressionSVM} {@var{obj} =} RegressionSVM (@var{Tbl}, @var{formula}) ## @deftypefnx {RegressionSVM} {@var{obj} =} RegressionSVM (@var{Tbl}, @var{Y}) ## @deftypefnx {RegressionSVM} {@var{obj} =} RegressionSVM (@dots{}, @var{name}, @var{value}) ## ## Fit a support vector machine regression model. ## ## @code{@var{obj} = RegressionSVM (@var{X}, @var{Y})} returns a support ## vector regression model, @var{obj}, with @var{X} being the predictor data ## and @var{Y} the continuous response of the observations in @var{X}. ## ## @itemize ## @item ## @var{X} must be an @math{NxP} numeric matrix of predictor data, where ## rows correspond to observations and columns to features. ## @item ## @var{Y} must be an @math{Nx1} numeric vector holding the response of the ## corresponding predictor data in @var{X}. @var{Y} must have the same ## number of rows as @var{X}. ## @end itemize ## ## The model is fitted by @math{epsilon}-insensitive regression: errors ## smaller than @qcode{Epsilon} cost nothing, so only the observations ## outside that tube become support vectors. @qcode{Epsilon} defaults to ## @code{iqr (@var{Y}) / 13.49}, a robust estimate of a tenth of the ## response's standard deviation, which is what MATLAB uses. ## ## @code{@var{obj} = RegressionSVM (@dots{}, @var{name}, @var{value})} ## returns a model with additional options specified by @qcode{Name-Value} ## pair arguments listed below. ## ## @multitable @columnfractions 0.32 0.68 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Standardize'} @tab A logical scalar specifying whether the ## predictor data should be centred and scaled before training. The same ## transformation is applied by @code{predict}. The default is ## @qcode{false}. ## ## @item @qcode{'CategoricalPredictors'} @tab The predictors whose values ## are levels, as indices, as a logical vector with one element per ## predictor, or as @qcode{'all'}. Each is dummy coded in its place, one ## column of zeros and ones per level seen in training, named as in ## @qcode{'x1 == 2'} in @code{ExpandedPredictorNames}, and the coded columns ## are not standardized. An observation holding a level the training data ## did not is predicted as a row missing a predictor, the weighted lower ## median of the training response. A predictor may be named rather than ## indexed, as a character matrix of one padded name per row, a string ## array or a cellstr; a name must match an entry of ## @qcode{'PredictorNames'} exactly, its case included. ## ## @item @qcode{'PredictorNames'} @tab A cell array of character vectors ## naming the predictors, in the order they appear in @var{X}. ## ## @item @qcode{'ResponseName'} @tab A character vector naming the response. ## The default is @qcode{'Y'}. ## ## @item @qcode{'ResponseTransform'} @tab A character vector naming one of ## the supported transformations, or a function handle, applied to the ## predicted response by @code{predict} and @code{resubPredict}. The ## default is @qcode{'none'}. ## ## @item @qcode{'Epsilon'} @tab A non-negative scalar, the half-width of the ## insensitive tube. The default is @code{iqr (@var{Y}) / 13.49}, or ## @math{0.1} where that is zero. ## ## @item @qcode{'BoxConstraint'} @tab A positive scalar bounding the dual ## coefficients, the cost of an error outside the tube. The default is ## @code{iqr (@var{Y}) / 1.349} for a Gaussian kernel, or 1 where that is ## zero, and 1 for any other kernel. ## ## @item @qcode{'Weights'} @tab A nonnegative single or double vector of ## observation weights, one per row of @var{X}. An observation's box ## constraint is @math{n} times @qcode{BoxConstraint} times its weight, the ## weights scaled to sum to one; standardization uses weighted means and ## standard deviations, and a row of zero or missing weight is left out. ## The model's @code{W} keeps the class of the weights, while every ## computation runs in double. The default is uniform. ## ## @item @qcode{'KernelFunction'} @tab A character vector naming the kernel, ## one of @qcode{'linear'}, the default, @qcode{'rbf'}, @qcode{'gaussian'}, ## @qcode{'polynomial'} or @qcode{'sigmoid'}. ## ## @item @qcode{'PolynomialOrder'} @tab A positive integer, the order of the ## polynomial kernel. The default is 3. It is ignored by every other ## kernel. ## ## @item @qcode{'KernelScale'} @tab A positive scalar dividing every ## predictor before any kernel is applied, as MATLAB does, so that with ## @math{u} and @math{v} the divided predictors the kernels are @math{u'v}, ## @math{exp (-||u - v||^2)}, @math{(1 + u'v)^q} and @math{tanh (u'v + c)}, ## @math{c} being @qcode{'KernelOffset'}. The default is 1. ## ## @item @qcode{'KernelOffset'} @tab A non-negative scalar, the constant ## @math{c} of the sigmoid kernel, which MATLAB does not have. MATLAB adds ## it to every element of the Gram matrix, which leaves the fitted model ## unchanged, so it changes no other kernel here. The default is 0. ## ## @item @qcode{'SVMtype'} @tab A character vector selecting the ## formulation, either @qcode{'eps_svr'}, the default, or @qcode{'nu_svr'}. ## MATLAB fits only the @math{epsilon} form; @qcode{'nu_svr'} is an Octave ## extension, in which @qcode{Nu} bounds the fraction of support vectors and ## @qcode{Epsilon} is determined by the fit rather than given. ## ## @item @qcode{'Nu'} @tab A scalar in @math{(0, 1]} used by ## @qcode{'nu_svr'}. The default is 0.5. ## ## @item @qcode{'CacheSize'} @tab A positive scalar, the kernel cache in ## megabytes. The default is 1000. ## ## @item @qcode{'Tolerance'} @tab A non-negative scalar, the tolerance of ## the termination criterion. The default is @math{1e-6}. ## ## @item @qcode{'Shrinking'} @tab Either 0 or 1, whether to use the ## shrinking heuristic. The default is 1. ## @end multitable ## ## The supported values for @qcode{'ResponseTransform'} are: ## ## @multitable @columnfractions 0.3 0.7 ## @headitem @var{Value} @tab @var{Description} ## @item @qcode{'none'} @tab @math{x} (no transformation) ## @item @qcode{'identity'} @tab @math{x} (no transformation) ## @item @qcode{'exp'} @tab @math{exp (x)} ## @item @qcode{'log'} @tab @math{log (x)} ## @end multitable ## ## @seealso{fitrsvm, ClassificationSVM, RegressionNeuralNetwork} ## @end deftypefn function this = RegressionSVM (X, Y, varargin) ## Check for sufficient number of input arguments if (nargin < 2) error ("RegressionSVM: too few input arguments."); endif ## A table names its own predictors and says which hold levels [this, X, Y, varargin] = resolveTable (this, 'RegressionSVM', ... X, Y, varargin); ## Check X and Y have the same number of observations if (rows (X) != rows (Y)) error ("RegressionSVM: number of rows in X and Y must be equal."); endif ## The response is continuous, so it must be numeric if (! (isnumeric (Y) && isreal (Y))) error ("RegressionSVM: Y must be a real numeric vector."); endif if (! (isvector (Y) || isempty (Y))) error ("RegressionSVM: Y must be a vector."); endif ## Assign original X and Y data to the RegressionSVM object this.X = X; this.Y = Y; ## Parse optional paired arguments optNames = {'Standardize', 'PredictorNames', 'ResponseName', ... 'ResponseTransform', 'SVMtype', 'Epsilon', ... 'KernelFunction', 'PolynomialOrder', 'KernelScale', ... 'KernelOffset', 'BoxConstraint', 'Nu', 'CacheSize', ... 'Tolerance', 'Shrinking', 'CategoricalPredictors', ... 'Weights'}; ## An empty default stands for one resolved once the data are known: ## 'Epsilon' is the interquartile range of the response over 13.49, ## 'BoxConstraint' the same range over 1.349 for a Gaussian kernel and 1 ## for any other, 'PredictorNames' are x1, x2, ... and 'ResponseName' is ## 'Y', and no 'ResponseTransform' leaves the response as it is. dfValues = {false, [], [], [], 'eps_svr', [], 'linear', 3, 1, 0, [], ... 0.5, 1000, 1e-6, 1, [], []}; [Standardize, PredictorNames, ResponseName, RTin, SVMtype, Epsilon, ... KernelFunction, PolynomialOrder, KernelScale, KernelOffset, ... BoxConstraint, Nu, CacheSize, Tolerance, Shrinking, CatPreds, ... Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (Standardize == true || Standardize == false)) error ("RegressionSVM: 'Standardize' must be either true or false."); endif if (! isempty (PredictorNames) && ! iscellstr (PredictorNames)) error (strcat ("RegressionSVM: 'PredictorNames' must be supplied", ... " as a cellstring array.")); elseif (! isempty (PredictorNames) && columns (PredictorNames) != columns (X)) error (strcat ("RegressionSVM: 'PredictorNames' must have the same", ... " number of columns as X.")); endif if (! isempty (ResponseName) && ! ischar (ResponseName)) error ("RegressionSVM: 'ResponseName' must be a character vector."); endif if (! (ischar (SVMtype) && isrow (SVMtype))) error ("RegressionSVM: 'SVMtype' must be a character vector."); endif SVMtype = tolower (SVMtype); if (! any (strcmp (SVMtype, {'eps_svr', 'nu_svr'}))) error ("RegressionSVM: unsupported 'SVMtype'."); endif if (! isempty (Epsilon) && ! (isnumeric (Epsilon) && isscalar (Epsilon) && Epsilon >= 0)) error ("RegressionSVM: 'Epsilon' must be a non-negative scalar."); endif if (! ischar (KernelFunction)) error ("RegressionSVM: 'KernelFunction' must be a character vector."); endif KernelFunction = tolower (KernelFunction); if (! any (strcmpi (KernelFunction, ... {'linear', 'rbf', 'gaussian', 'polynomial', 'sigmoid'}))) error ("RegressionSVM: unsupported Kernel function."); endif if (! (isnumeric (PolynomialOrder) && isscalar (PolynomialOrder) && PolynomialOrder > 0 && mod (PolynomialOrder, 1) == 0)) error ("RegressionSVM: 'PolynomialOrder' must be a positive integer."); endif if (! (isscalar (KernelScale) && KernelScale > 0)) error ("RegressionSVM: 'KernelScale' must be a positive scalar."); endif if (! (isnumeric (KernelOffset) && isscalar (KernelOffset) && KernelOffset >= 0)) error ("RegressionSVM: 'KernelOffset' must be a non-negative scalar."); endif if (! isempty (BoxConstraint) && ! (isscalar (BoxConstraint) && BoxConstraint > 0)) error ("RegressionSVM: 'BoxConstraint' must be a positive scalar."); endif if (! (isscalar (Nu) && Nu > 0 && Nu <= 1)) error (strcat ("RegressionSVM: 'Nu' must be a positive scalar in", ... " the range 0 < Nu <= 1.")); endif if (! (isscalar (CacheSize) && CacheSize > 0)) error ("RegressionSVM: 'CacheSize' must be a positive scalar."); endif if (! (isscalar (Tolerance) && Tolerance >= 0)) error ("RegressionSVM: 'Tolerance' must be a positive scalar."); endif if (! (ismember (Shrinking, [0, 1]) && isscalar (Shrinking))) error ("RegressionSVM: 'Shrinking' must be either 0 or 1."); endif if (! isempty (RTin)) [this.RTfun, this.ResponseTransform] = ... parseResponseTransform (RTin, 'RegressionSVM'); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("RegressionSVM: %s", errmsg); endif if (! isempty (Weights) && ! (isvector (Weights) && numel (Weights) == rows (Y))) error (strcat ("RegressionSVM: 'Weights' must be a vector with one", ... " element per row of X.")); endif if (! isempty (Weights) && (any (Weights < 0) || ! (sum (Weights(! isnan (Weights))) > 0))) error (strcat ("RegressionSVM: 'Weights' must be nonnegative and", ... " must not be all zero.")); endif if (! isempty (args)) error ("RegressionSVM: invalid optional paired argument."); endif ## Get number of variables in training data ndims_X = columns (X); this.NumPredictors = ndims_X; ## Generate default predictors and response variable names if (isempty (PredictorNames)) for i = 1:ndims_X PredictorNames {i} = strcat ("x", num2str (i)); endfor endif if (isempty (ResponseName)) ResponseName = 'Y'; endif this.PredictorNames = PredictorNames; this.ExpandedPredictorNames = PredictorNames; this.ResponseName = ResponseName; this.CategoricalPredictors = []; ## The weights keep their class in the model; every computation runs on ## them as double. Wclass = "double"; if (isempty (Weights)) Wall = ones (rows (Y), 1); else Wclass = class (Weights); Wall = double (Weights(:)); endif ## An observation is dropped when its response is missing, or when its ## weight is zero or missing, as R2024a drops it. A row whose ## predictors hold missing values is kept and reported as used, while ## the fit below draws on the complete observations alone. RowsUsed = ! isnan (Y(:)) & ! isnan (Wall) & Wall > 0; Yret = Y(RowsUsed); Xret = X(RowsUsed, :); Wret = Wall(RowsUsed) / sum (Wall(RowsUsed)); this.X = Xret; this.Y = Yret; cobs = ! any (isnan (Xret), 2); Y = Yret(cobs); X = Xret(cobs, :); wfit = Wret(cobs); ## Dummy code the categorical predictors on the rows the fit draws on. ## X keeps the predictors as given; the fit and every prediction see ## the coded columns. cnames = PredictorNames; if (isempty (cnames)) cnames = arrayfun (@(k) sprintf ('x%d', k), 1:columns (X), ... 'UniformOutput', false); endif [Coding, errmsg] = dummyCoding (X, CatPreds, cnames); if (! isempty (errmsg)) error ("RegressionSVM: %s", errmsg); endif ## What a row missing a predictor is predicted to be, as MATLAB R2024a ## predicts it: the weighted lower median of the training response. this.MissingResponse_ = missingResponse (Y, wfit); X = dummyCoding (X, Coding); if (! isempty (Coding.Index)) this.CategoricalPredictors = Coding.Index; this.ExpandedPredictorNames = Coding.ExpandedNames; this.Coding_ = Coding; endif ## Check X and Y contain valid data if (! (isnumeric (X) && isfinite (X))) error ("RegressionSVM: invalid values in X."); endif if (isempty (Y)) error ("RegressionSVM: Y cannot be empty."); endif if (! all (isfinite (Y))) error ("RegressionSVM: invalid values in Y."); endif this.NumObservations = rows (this.X); ## RowsUsed is left empty when every observation was used, as in MATLAB if (all (RowsUsed)) this.RowsUsed = []; else this.RowsUsed = RowsUsed; endif this.W = cast (Wret, Wclass); ## Handle the Standardize option. The model must be fitted on the ## scale it predicts on: predict and resubPredict standardize their ## input from Mu and Sigma, so the training data is standardized here ## as well. if (Standardize) ## Mu and Sigma weigh the complete observations by their weights, the ## deviation unbiased for those weights, as R2024a reports them. sw = wfit / sum (wfit); this.Mu = sum (sw .* X, 1); this.Sigma = sqrt (sum (sw .* (X - this.Mu) .^ 2, 1) ... / (1 - sum (sw .^ 2))); ## A constant predictor is left unscaled; its weighted mean can miss ## the constant by one rounding, so its deviation need not be zero. this.Sigma(this.Sigma == 0 | all (X == X(1,:), 1)) = 1; ## A level's column is left as it is, as in MATLAB R2024a. this.Mu(Coding.Dummy) = 0; this.Sigma(Coding.Dummy) = 1; X = (X - this.Mu) ./ this.Sigma; else this.Sigma = []; this.Mu = []; endif ## Epsilon defaults to a robust tenth of the response's spread, which ## is what MATLAB uses. A constant response gives a zero interquartile ## range, and a zero-width tube would make every observation a support ## vector, so it falls back to LIBSVM's own default. if (isempty (Epsilon)) Epsilon = iqr (Y) / 13.49; if (Epsilon == 0) Epsilon = 0.1; endif endif this.Epsilon = Epsilon; ## BoxConstraint defaults to ten times that robust tenth for a Gaussian ## kernel and to 1 for any other, as in MATLAB R2024a, which also falls ## back to 1 on a zero interquartile range. if (isempty (BoxConstraint)) BoxConstraint = 1; if (any (strcmp (KernelFunction, {'rbf', 'gaussian'})) && iqr (Y) > 0) BoxConstraint = iqr (Y) / 1.349; endif endif ## Set svmtrain parameters for SVMtype and KernelFunction switch (SVMtype) case 'eps_svr' s = 3; case 'nu_svr' s = 4; endswitch switch (KernelFunction) case 'linear' t = 0; case 'polynomial' t = 1; case {'rbf', 'gaussian'} t = 2; case 'sigmoid' t = 3; endswitch ## MATLAB divides the predictors by KernelScale for every kernel, so the ## fit sees X / KernelScale with gamma 1. Its polynomial kernel is ## (1 + x'z) ^ q, and it adds KernelOffset to the Gram matrix, which ## leaves the fitted model unchanged, so the offset reaches only the ## sigmoid kernel, which is ours alone. Xu = X; X = X / KernelScale; g = 1; r = 0; if (t == 1) r = 1; elseif (t == 3) r = KernelOffset; endif ## Build options string for svmtrain function str_options = strcat ("-s %d -t %d -g %.16g -d %d -r %.16g", ... " -c %.16g -n %.16g -p %.16g -m %.16g", ... " -e %.16g -h %d -q"); svm_options = sprintf (str_options, s, t, g, PolynomialOrder, ... r, BoxConstraint, Nu, Epsilon, ... CacheSize, Tolerance, Shrinking); ## The weights enter the fit through one box constraint per ## observation, n * C * w, with w summing to one over the observations ## used, as R2024a sets them. Without weights every constraint is C. instW = this.NumObservations * wfit; ## Train the SVM model using svmtrain from libsvm [Model, converged] = svmtrain (Y, X, svm_options, instW); if (! converged) warning (strcat ("RegressionSVM: the solver stopped at its", ... " iteration limit without converging;", ... " standardizing the predictors may help.")); endif ## Populate the model properties. For regression LIBSVM's sv_coef is ## already the difference of the two multipliers, so it is signed and ## kept as it stands: unlike a classifier there are no labels to carry ## the sign into. this.Alpha = Model.sv_coef; ## LIBSVM evaluates sum_i coef_i * K(sv_i, x) - rho, so the intercept ## MATLAB reports is the negated rho. Measured against svmpredict. this.Bias = -Model.rho; this.IsSupportVector = false (this.NumObservations, 1); ## LIBSVM counts the observations the fit saw, the complete ones fitted = find (cobs); this.IsSupportVector(fitted(Model.sv_indices)) = true; this.SupportVectors = Xu(Model.sv_indices,:); ## BETA holds the primal coefficients, one per predictor, and exists ## only for a linear kernel; for any other there is no primal ## representation and MATLAB leaves it empty. It weighs the divided ## predictors, as MATLAB's does. if (t == 0) this.Beta = (this.SupportVectors / KernelScale)' * this.Alpha; else this.Beta = []; endif ## Re-express the engine's model on the undivided predictors, which is ## what every prediction hands it: the scale moves into gamma, or into ## the coefficients of a linear kernel, which has no gamma. Model.SVs = sparse (this.SupportVectors); if (t == 0) Model.sv_coef = Model.sv_coef / KernelScale ^ 2; else Model.Parameters(4) = 1 / KernelScale ^ 2; endif this.Model = Model; ## The kernel and the per-observation box constraints, in the shapes ## MATLAB reports them: an observation missing a predictor has none. this.KernelParameters = svmKernelParams (KernelFunction, KernelScale, ... PolynomialOrder); this.BoxConstraints = BoxConstraint * this.NumObservations * Wret; this.BoxConstraints(! cobs) = NaN; ## Populate ModelParameters structure. The polynomial order belongs to ## the polynomial kernel alone and is reported under no other, as ## MATLAB reports it. if (strcmpi (KernelFunction, 'polynomial')) KPOrder = PolynomialOrder; else KPOrder = []; endif this.ModelParameters = struct ('SVMtype', SVMtype, 'BoxConstraint', ... BoxConstraint, 'CacheSize', CacheSize, ... 'KernelScale', KernelScale, ... 'KernelOffset', KernelOffset, ... 'KernelFunction', KernelFunction, ... 'KernelPolynomialOrder', KPOrder, ... 'Epsilon', Epsilon, 'Nu', Nu, ... 'Tolerance', Tolerance, ... 'Shrinking', Shrinking, ... 'StandardizeData', ... logical (Standardize), ... 'Version', 1, 'Method', 'SVM', ... 'Type', 'regression'); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionSVM} {@var{obj} =} discardSupportVectors (@var{obj}) ## ## Discard the support vectors of a linear SVM model. ## ## @code{@var{obj} = discardSupportVectors (@var{obj})} empties ## @code{Alpha} and @code{SupportVectors}, leaving @code{Beta} and ## @code{Bias} to decide every prediction. A linear kernel needs ## nothing else, so the returned model predicts what it predicted ## before while carrying one vector in place of many. ## ## The kernel must be linear. Under any other the support vectors are ## part of the decision function and cannot be dropped. Discarding twice ## is not an error and changes nothing. ## ## @seealso{fitrsvm, RegressionSVM, CompactRegressionSVM} ## @end deftypefn function this = discardSupportVectors (this) if (nargin != 1) print_usage (); endif if (! strcmpi (this.ModelParameters.KernelFunction, 'linear')) error (strcat ("RegressionSVM.discardSupportVectors: you", ... " cannot discard support vectors for a non-linear", ... " kernel.")); endif ## The engine keeps its own copy of the support vectors, so emptying ## the properties alone would free nothing. Collapsing the model onto ## the single vector that decides it leaves every scoring path as it ## was, svmpredict going on being the engine over one vector. this.Model = discardSVs (this.Model); this.Alpha = []; this.SupportVectors = []; endfunction ## -*- texinfo -*- ## @deftypefn {RegressionSVM} {@var{yFit} =} predict (@var{obj}, @var{XC}) ## ## Predict the response for new data with a support vector regression ## model. ## ## @code{@var{yFit} = predict (@var{obj}, @var{XC})} returns a column ## vector holding the predicted response for each row of @var{XC}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{RegressionSVM} class object. ## @item ## @var{XC} must be a numeric matrix with the same number of predictors as ## the data the model was trained on. ## @end itemize ## ## The transformation named by @code{ResponseTransform} is applied to the ## model's output before it is returned. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @seealso{RegressionSVM, fitrsvm} ## @end deftypefn function yFit = predict (this, XC) ## Check for sufficient input arguments if (nargin < 2) error ("RegressionSVM.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'RegressionSVM.predict', XC); ## Check for valid XC if (isempty (XC)) error ("RegressionSVM.predict: XC is empty."); elseif (this.NumPredictors != columns (XC)) error (strcat ("RegressionSVM.predict: XC must have the same", ... " number of predictors as the trained model.")); endif ## Code the categorical predictors and standardize (if necessary) if (! isempty (this.Coding_)) XC = dummyCoding (XC, this.Coding_); endif if (! isempty (this.Mu)) XC = (XC - this.Mu) ./ this.Sigma; endif ## LIBSVM returns the fitted response as its first output for a ## regression model, there being no label to decide. yFit = svmpredict (zeros (rows (XC), 1), XC, this.Model, '-q'); yFit(any (isnan (XC), 2)) = this.MissingResponse_; ## Apply ResponseTransform yFit = this.RTfun (yFit); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionSVM} {@var{yFit} =} resubPredict (@var{obj}) ## ## Predict the response of the training data with a support vector ## regression model. ## ## @code{@var{yFit} = resubPredict (@var{obj})} returns a column vector ## holding the predicted response for every observation the model was ## trained on. ## ## @itemize ## @item ## @var{obj} must be a @qcode{RegressionSVM} class object. ## @end itemize ## ## @seealso{RegressionSVM, fitrsvm} ## @end deftypefn function yFit = resubPredict (this) used = true (rows (this.X), 1); yFit = predict (this, this.X(used, :)); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionSVM} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {RegressionSVM} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {RegressionSVM} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {RegressionSVM} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Compute the regression loss of a support vector machine model. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} returns the ## weighted mean squared error between the response @var{Y} and the ## response the model predicts for @var{X}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{RegressionSVM} class object. ## @item ## @var{X} must be a numeric matrix with the same number of predictors as ## the data the model was trained on. ## @item ## @var{Y} must be a numeric vector with as many rows as @var{X}. ## @end itemize ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{L} = loss (@dots{}, @var{name}, @var{value})} accepts the ## following @qcode{Name-Value} pairs. ## ## @multitable @columnfractions 0.28 0.72 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'LossFun'} @tab @qcode{'mse'}, the default, ## @qcode{'epsiloninsensitive'}, or a function handle called as ## @code{@var{lossfun} (@var{Y}, @var{yFit}, @var{W})} returning a scalar. ## The @math{epsilon}-insensitive loss charges nothing for an error inside ## the tube, @code{max (0, abs (@var{Y} - @var{yFit}) - Epsilon)}, which is ## the quantity the fit itself minimizes. ## ## @item @qcode{'Weights'} @tab A numeric vector of observation weights ## with one entry per row of @var{X}. It defaults to a uniform weight. ## The weights are normalized to sum to one before the loss is formed. ## @end multitable ## ## @seealso{RegressionSVM, fitrsvm} ## @end deftypefn function L = loss (this, X, Y, varargin) ## Check for sufficient input arguments if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("RegressionSVM.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("RegressionSVM.loss: Name-Value arguments", ... " must be in pairs.")); endif [X, Y] = checkXY_ (this, X, Y, 'loss'); ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights'}; dfValues = {'mse', []}; [LossFun, W, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (is_function_handle (LossFun) || (ischar (LossFun) && isrow (LossFun)))) error (strcat ("RegressionSVM.loss: 'LossFun' must be a character", ... " vector or a function handle.")); endif if (ischar (LossFun) && ! any (strcmpi (LossFun, ... {'mse', 'epsiloninsensitive'}))) error ("RegressionSVM.loss: unsupported 'LossFun' value."); endif errmsg = weightsClass (W); if (! isempty (errmsg)) error ("RegressionSVM.loss: %s", errmsg); endif if (! isempty (W) && ! (isnumeric (W) && isvector (W))) error ("RegressionSVM.loss: 'Weights' must be a numeric vector."); endif if (! isempty (W) && numel (W) != rows (X)) error (strcat ("RegressionSVM.loss: size of 'Weights' must equal", ... " the number of rows in X.")); endif if (! isempty (args)) error ("RegressionSVM.loss: invalid optional paired argument."); endif if (isempty (W)) W = ones (rows (X), 1); endif ## Weights are normalized to sum to one, as MATLAB does, so a loss is ## a weighted average rather than a weighted sum. W = double (W(:)); W = W / sum (W); yFit = predict (this, X); Y = Y(:); if (is_function_handle (LossFun)) L = LossFun (Y, yFit, W); if (! (isnumeric (L) && isscalar (L))) error (strcat ("RegressionSVM.loss: 'LossFun' must return", ... " a numeric scalar.")); endif elseif (strcmpi (LossFun, 'epsiloninsensitive')) L = sum (W .* max (0, abs (Y - yFit) - this.Epsilon)); else L = sum (W .* (Y - yFit) .^ 2); endif endfunction ## -*- texinfo -*- ## @deftypefn {RegressionSVM} {@var{L} =} resubLoss (@var{obj}) ## @deftypefnx {RegressionSVM} {@var{L} =} resubLoss (@dots{}, @var{name}, @var{value}) ## ## Compute the resubstitution regression loss of a support vector machine ## model. ## ## @code{@var{L} = resubLoss (@var{obj})} returns the weighted mean ## squared error of the model on the data it was trained on. It accepts ## the same @qcode{Name-Value} pairs as @code{loss}. ## ## @itemize ## @item ## @var{obj} must be a @qcode{RegressionSVM} class object. ## @end itemize ## ## @seealso{RegressionSVM, fitrsvm} ## @end deftypefn function L = resubLoss (this, varargin) ## The model's own weights stand unless others are given, as in R2024a if (! any (strcmpi (varargin(1:2:end), 'Weights'))) varargin = [varargin, {'Weights', this.W}]; endif L = loss (this, this.X, this.Y, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionSVM} {@var{CVMdl} =} crossval (@var{obj}) ## @deftypefnx {RegressionSVM} {@var{CVMdl} =} crossval (@dots{}, @var{name}, @var{value}) ## ## Cross validate a support vector regression model. ## ## @code{@var{CVMdl} = crossval (@var{obj})} returns a ## @qcode{RegressionPartitionedModel} holding one refit of @var{obj} per ## fold of a ten-fold partition, or of an @math{n}-fold one where the ## model has fewer than ten observations. ## ## @itemize ## @item ## @var{obj} must be a @qcode{RegressionSVM} class object. ## @end itemize ## ## @code{@var{CVMdl} = crossval (@dots{}, @var{name}, @var{value})} ## accepts one, and only one, of the following @qcode{Name-Value} pairs. ## ## @multitable @columnfractions 0.28 0.72 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'KFold'} @tab An integer greater than 1, the number of ## folds. ## @item @qcode{'Holdout'} @tab A scalar in @math{(0, 1)}, the fraction ## of observations held out for testing. ## @item @qcode{'Leaveout'} @tab @qcode{'on'} or @qcode{'off'}, whether ## to hold out one observation at a time. ## @item @qcode{'CVPartition'} @tab A @code{cvpartition} object over as ## many observations as the model was trained on. ## @end multitable ## ## @seealso{RegressionSVM, RegressionPartitionedModel, cvpartition} ## @end deftypefn function CVMdl = crossval (this, varargin) if (numel (varargin) == 1) error (strcat ("RegressionSVM.crossval: Name-Value", ... " arguments must be in pairs.")); elseif (numel (varargin) > 2) error (strcat ("RegressionSVM.crossval: specify only", ... " one of the optional Name-Value paired arguments.")); endif if (this.NumObservations < 10) numFolds = this.NumObservations; else numFolds = 10; endif Holdout = []; Leaveout = 'off'; CVPartition = []; while (numel (varargin) > 0) switch (tolower (varargin {1})) case 'kfold' numFolds = varargin{2}; if (! (isnumeric (numFolds) && isscalar (numFolds) && (numFolds == fix (numFolds)) && numFolds > 1)) error (strcat ("RegressionSVM.crossval: 'KFold'", ... " must be an integer value greater than 1.")); endif case 'holdout' Holdout = varargin{2}; if (! (isnumeric (Holdout) && isscalar (Holdout) && Holdout > 0 && Holdout < 1)) error (strcat ("RegressionSVM.crossval: 'Holdout'", ... " must be a numeric value between 0 and 1.")); endif case 'leaveout' Leaveout = varargin{2}; if (! (ischar (Leaveout) && (strcmpi (Leaveout, 'on') || strcmpi (Leaveout, 'off')))) error (strcat ("RegressionSVM.crossval: 'Leaveout'", ... " must be either 'on' or 'off'.")); endif case 'cvpartition' CVPartition = varargin{2}; if (! (isa (CVPartition, 'cvpartition'))) error (strcat ("RegressionSVM.crossval:", ... " 'CVPartition' must be a 'cvpartition'", ... " object.")); endif otherwise error (strcat ("RegressionSVM.crossval: invalid", ... " parameter name in optional paired arguments.")); endswitch varargin(1:2) = []; endwhile ## Determine the cross-validation method to use. The partition is ## built over the observations actually trained on, so its indices and ## the partitioned model's rows are the same set. n = this.NumObservations; if (! isempty (CVPartition)) partition = CVPartition; elseif (! isempty (Holdout)) partition = cvpartition (n, 'Holdout', Holdout); elseif (strcmpi (Leaveout, 'on')) partition = cvpartition (n, 'LeaveOut'); else partition = cvpartition (n, 'KFold', numFolds); endif ## Create a cross-validated model object CVMdl = RegressionPartitionedModel (this, partition); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionSVM} {@var{CMdl} =} compact (@var{obj}) ## ## Create a @qcode{CompactRegressionSVM} object. ## ## @code{@var{CMdl} = compact (@var{obj})} returns a compact version of ## the @qcode{RegressionSVM} object @var{obj}, which keeps the support ## vectors and their coefficients but drops the training data, so it ## predicts identically while carrying no observations. ## ## @seealso{fitrsvm, RegressionSVM, CompactRegressionSVM} ## @end deftypefn function CMdl = compact (this) ## Create a compact model CMdl = CompactRegressionSVM (this); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionSVM} {} savemodel (@var{obj}, @var{filename}) ## ## Save a support vector regression model to a file. ## ## @code{savemodel (@var{obj}, @var{filename})} saves every property of ## the @qcode{RegressionSVM} object @var{obj} into @var{filename} in ## binary format, so that it can be read back with @code{loadmodel}. ## ## @seealso{loadmodel, RegressionSVM, fitrsvm} ## @end deftypefn function savemodel (this, fname) if (nargin < 2) error ("RegressionSVM.savemodel: too few input arguments."); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error (strcat ("RegressionSVM.savemodel: FNAME must be", ... " a character vector.")); endif ## Generate variable for class name classdef_name = 'RegressionSVM'; ## Create variables from model properties X = this.X; Y = this.Y; NumObservations = this.NumObservations; RowsUsed = this.RowsUsed; BinEdges = this.BinEdges; NumPredictors = this.NumPredictors; PredictorNames = this.PredictorNames; ResponseName = this.ResponseName; ResponseTransform = this.ResponseTransform; Epsilon = this.Epsilon; Sigma = this.Sigma; Mu = this.Mu; ModelParameters = this.ModelParameters; KernelParameters = this.KernelParameters; BoxConstraints = this.BoxConstraints; Model = this.Model; Alpha = this.Alpha; Beta = this.Beta; Bias = this.Bias; IsSupportVector = this.IsSupportVector; SupportVectors = this.SupportVectors; CategoricalPredictors = this.CategoricalPredictors; Coding_ = this.Coding_; MissingResponse_ = this.MissingResponse_; ExpandedPredictorNames = this.ExpandedPredictorNames; W = this.W; RTfun = this.RTfun; ## Save classdef name and all model properties as individual variables HyperparameterOptimizationResults = this.HyperparameterOptimizationResults; save ('-binary', fname, 'classdef_name', 'X', 'Y', ... 'NumObservations', 'RowsUsed', 'BinEdges', 'NumPredictors', ... 'PredictorNames', 'ResponseName', 'ResponseTransform', ... 'Epsilon', 'Sigma', 'Mu', 'ModelParameters', ... 'Model', 'Alpha', 'Beta', 'Bias', 'IsSupportVector', ... 'SupportVectors', 'CategoricalPredictors', 'Coding_', ... 'MissingResponse_', ... 'ExpandedPredictorNames', 'KernelParameters', ... 'BoxConstraints', 'W', 'RTfun', ... 'HyperparameterOptimizationResults'); endfunction endmethods methods (Access = private) ## Shared validation for the assessment methods, so each reports under ## its own name. function [X, Y] = checkXY_ (this, X, Y, caller) if (isempty (X)) error ("RegressionSVM.%s: X is empty.", caller); elseif (this.NumPredictors != columns (X)) error (strcat ("RegressionSVM.%s: X must have the same number", ... " of predictors as the trained model."), caller); endif if (isempty (Y)) error ("RegressionSVM.%s: Y is empty.", caller); elseif (! (isnumeric (Y) && isreal (Y))) error (strcat ("RegressionSVM.%s: Y must be a real numeric", ... " vector."), caller); elseif (rows (X) != numel (Y)) error (strcat ("RegressionSVM.%s: Y must have the same number", ... " of rows as X."), caller); endif endfunction endmethods methods(Static, Hidden) function mdl = load_model (filename, data) ## Create a RegressionSVM object mdl = RegressionSVM ([1; 2; 3], [1; 2; 3]); ## Get fieldnames from DATA (including private properties) names = fieldnames (data); ## The set methods for these read other properties, and one of them ## rebuilds Coeffs, so they are assigned once everything else is in ## place rather than in the order the file happens to list them. late = ismember (names, {'Cost', 'Prior', 'ScoreTransform', ... 'ResponseTransform'}); names = [names(! late); names(late)]; ## Copy data into object for i = 1:numel (names) ## Check fieldnames in DATA match the class properties try mdl.(names{i}) = data.(names{i}); catch error ("RegressionSVM.load_model: invalid model in '%s'.", filename) end_try_catch endfor endfunction endmethods endclassdef ## A fitted model carries the defaults MATLAB documents for fitrsvm. %!test %! X = [linspace(0, 1, 40)', linspace(2, 3, 40)']; %! Y = 2 * X(:,1) + 0.5; %! Mdl = RegressionSVM (X, Y); %! assert_equal (class (Mdl), 'RegressionSVM'); %! assert_equal (Mdl.ModelParameters.KernelFunction, 'linear'); %! assert_equal (Mdl.ModelParameters.SVMtype, 'eps_svr'); %! assert_equal (Mdl.ModelParameters.BoxConstraint, 1); %! assert_equal (Mdl.ModelParameters.KernelScale, 1); %! assert_equal (isempty (Mdl.ModelParameters.KernelPolynomialOrder), true); %! assert_equal (isempty (Mdl.Mu), true); %! assert_equal (Mdl.NumObservations, 40); %! assert_equal (Mdl.NumPredictors, 2); %! assert_equal (Mdl.ResponseName, 'Y'); %! assert_equal (Mdl.PredictorNames, {'x1', 'x2'}); ## Epsilon defaults to a robust tenth of the response's spread. %!test %! randn ('seed', 42); %! X = randn (60, 2); %! Y = X(:,1) * 3 + randn (60, 1); %! Mdl = RegressionSVM (X, Y); %! assert_equal (Mdl.Epsilon, iqr (Y) / 13.49, 1e-12); %! M2 = RegressionSVM (X, Y, 'Epsilon', 0.25); %! assert_equal (M2.Epsilon, 0.25); ## A constant response has no spread, so Epsilon falls back rather than ## collapsing the tube to zero width. %!test %! X = [linspace(0, 1, 20)', linspace(1, 2, 20)']; %! Mdl = RegressionSVM (X, ones (20, 1)); %! assert_equal (Mdl.Epsilon, 0.1); ## With a linear kernel the model is a plain linear function of its input. %!test %! randn ('seed', 42); %! X = randn (50, 3); %! Y = X * [2; -1; 0.5] + 1; %! Mdl = RegressionSVM (X, Y); %! assert_equal (size (Mdl.Beta), [3, 1]); %! assert_equal (isscalar (Mdl.Bias), true); %! assert_equal (X * Mdl.Beta + Mdl.Bias, resubPredict (Mdl), 1e-8); %! assert_equal (Mdl.Beta, Mdl.SupportVectors' * Mdl.Alpha, 1e-12); ## A non-linear kernel has no primal representation, so Beta is empty. %!test %! randn ('seed', 42); %! X = randn (40, 2); %! Y = sum (X .^ 2, 2); %! Mdl = RegressionSVM (X, Y, 'KernelFunction', 'rbf'); %! assert_equal (isempty (Mdl.Beta), true); %! assert_equal (numel (resubPredict (Mdl)), 40); ## Alpha is signed, one entry per support vector, and IsSupportVector marks ## the training rows they came from. %!test %! randn ('seed', 42); %! X = randn (60, 2); %! Y = X(:,1) - X(:,2) + randn (60, 1) * 0.5; %! Mdl = RegressionSVM (X, Y); %! nsv = rows (Mdl.SupportVectors); %! assert_equal (size (Mdl.Alpha), [nsv, 1]); %! assert_equal (any (Mdl.Alpha < 0), true); %! assert_equal (class (Mdl.IsSupportVector), 'logical'); %! assert_equal (numel (Mdl.IsSupportVector), 60); %! assert_equal (sum (Mdl.IsSupportVector), nsv); ## A wider tube is fitted by fewer support vectors. %!test %! randn ('seed', 42); %! X = randn (60, 2); %! Y = X(:,1) * 2 + randn (60, 1); %! narrow = RegressionSVM (X, Y, 'Epsilon', 0.05); %! wide = RegressionSVM (X, Y, 'Epsilon', 3); %! assert_equal (sum (wide.IsSupportVector) < ... %! sum (narrow.IsSupportVector), true); ## Standardize fits on the scale it predicts on. A model fitted on raw data ## and asked about standardized data is not merely worse, it is wrong. %!test %! randn ('seed', 42); %! X = [randn(60, 1), randn(60, 1) * 1000]; %! Y = X(:,1) + X(:,2) / 1000; %! Mdl = RegressionSVM (X, Y, 'Standardize', true, 'Epsilon', 0.01); %! assert_equal (size (Mdl.Mu), [1, 2]); %! assert_equal (size (Mdl.Sigma), [1, 2]); %! assert_equal (predict (Mdl, X), resubPredict (Mdl)); %! assert_equal (sqrt (resubLoss (Mdl)) < std (Y), true); ## A constant predictor gets a unit scale rather than a division by zero. %!test %! X = [linspace(0, 1, 20)', ones(20, 1)]; %! Mdl = RegressionSVM (X, X(:,1), 'Standardize', true); %! assert_equal (Mdl.Sigma(2), 1); %! assert_equal (all (isfinite (resubPredict (Mdl))), true); ## Rows carrying a missing value are dropped from both X and Y. %!test %! X = [linspace(0, 1, 12)', linspace(1, 2, 12)'; NaN, 1; 0.5, 1]; %! Y = [2 * linspace(0, 1, 12)'; 1; NaN]; %! Mdl = RegressionSVM (X, Y); %! assert_equal (Mdl.NumObservations, 13); %! assert_equal (sum (Mdl.RowsUsed), 13); %! assert_equal (Mdl.RowsUsed(13:14), [true; false]); %! assert_equal (numel (resubPredict (Mdl)), 13); ## predict on the training rows is resubPredict. %!test %! randn ('seed', 42); %! X = randn (40, 2); %! Y = X(:,1) + 2 * X(:,2); %! Mdl = RegressionSVM (X, Y); %! assert_equal (predict (Mdl, X), resubPredict (Mdl)); ## loss defaults to the weighted mean squared error, and weights are ## normalized, so scaling every weight leaves the loss alone. %!test %! randn ('seed', 42); %! X = randn (30, 2); %! Y = X(:,1) * 3 + 1; %! Mdl = RegressionSVM (X, Y); %! yFit = predict (Mdl, X); %! assert_equal (loss (Mdl, X, Y), mean ((Y - yFit) .^ 2), 1e-12); %! assert_equal (loss (Mdl, X, Y, 'LossFun', 'mse'), loss (Mdl, X, Y), 1e-12); %! w = rand (30, 1) + 0.1; %! assert_equal (loss (Mdl, X, Y, 'Weights', w), ... %! loss (Mdl, X, Y, 'Weights', 7 * w), 1e-12); %! assert_equal (loss (Mdl, X, Y, 'Weights', w), ... %! sum ((w / sum (w)) .* (Y - yFit) .^ 2), 1e-12); ## The epsilon-insensitive loss charges nothing inside the tube. %!test %! randn ('seed', 42); %! X = randn (30, 2); %! Y = X(:,1) * 3 + 1; %! Mdl = RegressionSVM (X, Y, 'Epsilon', 0.5); %! yFit = predict (Mdl, X); %! L = loss (Mdl, X, Y, 'LossFun', 'epsiloninsensitive'); %! assert_equal (L, mean (max (0, abs (Y - yFit) - 0.5)), 1e-12); %! assert_equal (L <= loss (Mdl, X, Y, 'LossFun', 'mse') + 1, true); %! assert_equal (L >= 0, true); ## loss takes a function handle of the response, the fit and the weights. %!test %! randn ('seed', 42); %! X = randn (20, 2); %! Y = X(:,1) + 1; %! Mdl = RegressionSVM (X, Y); %! f = @(y, yf, w) sum (w .* abs (y - yf)); %! assert_equal (loss (Mdl, X, Y, 'LossFun', f), ... %! mean (abs (Y - predict (Mdl, X))), 1e-12); ## resubLoss is loss on the training data. %!test %! randn ('seed', 42); %! X = randn (21, 2); %! Y = [randn(20, 1); NaN]; %! Mdl = RegressionSVM (X, Y); %! Xu = X(Mdl.RowsUsed, :); %! Yu = Y(Mdl.RowsUsed); %! assert_equal (resubLoss (Mdl), loss (Mdl, Xu, Yu), 1e-12); %! assert_equal (resubLoss (Mdl, 'LossFun', 'epsiloninsensitive'), ... %! loss (Mdl, Xu, Yu, 'LossFun', 'epsiloninsensitive'), 1e-12); ## ResponseTransform is applied to the prediction, by name or by handle. %!test %! X = [linspace(0, 1, 20)', linspace(1, 2, 20)']; %! Y = 2 * X(:,1) + 1; %! Mdl = RegressionSVM (X, Y); %! raw = predict (Mdl, X); %! Mdl.ResponseTransform = 'exp'; %! assert_equal (predict (Mdl, X), exp (raw), 1e-12); %! Mdl.ResponseTransform = @(y) 2 * y; %! assert_equal (predict (Mdl, X), 2 * raw, 1e-12); %! Mdl.ResponseTransform = 'none'; %! assert_equal (predict (Mdl, X), raw, 1e-12); ## nu-SVR is an Octave extension the LIBSVM engine already provides. %!test %! randn ('seed', 42); %! X = randn (50, 2); %! Y = X(:,1) - X(:,2); %! Mdl = RegressionSVM (X, Y, 'SVMtype', 'nu_svr', 'Nu', 0.3); %! assert_equal (Mdl.ModelParameters.SVMtype, 'nu_svr'); %! assert_equal (Mdl.ModelParameters.Nu, 0.3); %! assert_equal (Mdl.Model.Parameters(1), 4); %! assert_equal (all (isfinite (resubPredict (Mdl))), true); ## Every kernel trains and predicts finitely. %!test %! randn ('seed', 42); %! X = randn (40, 2); %! Y = X(:,1) + X(:,2); %! names = {'linear', 'rbf', 'gaussian', 'polynomial', 'sigmoid'}; %! for k = 1:numel (names) %! Mdl = RegressionSVM (X, Y, 'KernelFunction', names{k}); %! assert_equal (all (isfinite (resubPredict (Mdl))), true); %! endfor ## A saved model comes back carrying its own numbers. %!test %! randn ('seed', 42); %! X = randn (30, 2); %! Y = X(:,1) * 4 - 1; %! Mdl = RegressionSVM (X, Y, 'Standardize', true); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2), 'RegressionSVM'); %! assert_equal (M2.Alpha, Mdl.Alpha); %! assert_equal (M2.Beta, Mdl.Beta); %! assert_equal (M2.Bias, Mdl.Bias); %! assert_equal (M2.Epsilon, Mdl.Epsilon); %! assert_equal (M2.SupportVectors, Mdl.SupportVectors); %! assert_equal (M2.NumObservations, Mdl.NumObservations); %! assert_equal (predict (M2, X), predict (Mdl, X)); ## compact drops the training data but predicts identically. %!test %! randn ('seed', 42); %! X = randn (40, 2); %! Y = X(:,1) - X(:,2); %! Mdl = RegressionSVM (X, Y); %! CMdl = compact (Mdl); %! assert_equal (class (CMdl), 'CompactRegressionSVM'); %! assert_equal (predict (CMdl, X), predict (Mdl, X)); %! assert_equal (loss (CMdl, X, Y), loss (Mdl, X, Y)); ## crossval returns a partitioned model holding one fit per fold. %!test %! rand ('seed', 42); randn ('seed', 42); %! X = randn (30, 2); %! Y = X(:,1) - X(:,2); %! Mdl = fitrsvm (X, Y); %! CVMdl = crossval (Mdl, 'KFold', 3); %! assert_equal (class (CVMdl), 'RegressionPartitionedModel'); %! assert_equal (CVMdl.KFold, 3); %! assert_equal (CVMdl.CrossValidatedModel, 'SVM'); %! assert_equal (numel (kfoldPredict (CVMdl)), 30); %! assert_equal (isfinite (kfoldLoss (CVMdl)), true); ## Test input validation for crossval ## discardSupportVectors empties what R2024a empties and keeps what it ## keeps: Alpha and the support vectors go, Beta, Bias and IsSupportVector ## stay, and the class is unchanged. %!test %! load fisheriris %! keep = ! strcmp (species, "setosa"); %! X = meas(keep,2:4); y = meas(keep,1); %! Mdl = fitrsvm (X, y, "KernelFunction", "linear"); %! D = discardSupportVectors (Mdl); %! assert_equal (class (D), "RegressionSVM"); %! assert_equal (isempty (D.Alpha), true); %! assert_equal (isempty (D.SupportVectors), true); %! assert_equal (D.Beta, Mdl.Beta); %! assert_equal (D.Bias, Mdl.Bias); %! assert_equal (D.IsSupportVector, Mdl.IsSupportVector); ## A linear decision needs only Beta and Bias, so the model predicts what it ## predicted before. %!test %! load fisheriris %! keep = ! strcmp (species, "setosa"); %! X = meas(keep,2:4); y = meas(keep,1); %! Mdl = fitrsvm (X, y, "KernelFunction", "linear"); %! D = discardSupportVectors (Mdl); %! assert_equal (predict (D, X), predict (Mdl, X), 1e-10); ## The saving is real rather than cosmetic: the engine keeps its own copy of ## the support vectors, and it collapses to the one vector that decides a ## linear model. Emptying the properties alone would free nothing. %!test %! load fisheriris %! keep = ! strcmp (species, "setosa"); %! X = meas(keep,2:4); y = meas(keep,1); %! Mdl = fitrsvm (X, y, "KernelFunction", "linear"); %! D = discardSupportVectors (Mdl); %! assert_equal (rows (Mdl.Model.SVs) > 1, true); %! assert_equal (rows (D.Model.SVs), 1); %! assert_equal (predict (discardSupportVectors (D), X), predict (D, X)); %!test # A row missing a predictor predicts the lower median of the response %! X = [(1:10)', mod((1:10)', 3)]; %! Mdl = RegressionSVM (X, (1:10)'); %! assert_equal (predict (Mdl, [NaN, 1]), 5); ## A fit stopped at the solver's iteration limit says so. %!warning ... %! RegressionSVM ((1:20)' * 1000, (1:20)' * 10, 'KernelFunction', 'polynomial'); %!error ... %! load fisheriris %! keep = ! strcmp (species, "setosa"); %! X = meas(keep,2:4); y = meas(keep,1); %! discardSupportVectors (fitrsvm (X, y, "KernelFunction", "rbf")) %!error ... %! crossval (fitrsvm (randn (12, 2), randn (12, 1)), 'KFold') %!error ... %! crossval (fitrsvm (randn (12, 2), randn (12, 1)), ... %! 'KFold', 3, 'Leaveout', 'on') %!error ... %! crossval (fitrsvm (randn (12, 2), randn (12, 1)), 'KFold', 1) %!error ... %! crossval (fitrsvm (randn (12, 2), randn (12, 1)), 'Holdout', 1) %!error ... %! crossval (fitrsvm (randn (12, 2), randn (12, 1)), 'Leaveout', 1) %!error ... %! crossval (fitrsvm (randn (12, 2), randn (12, 1)), 'CVPartition', 1) %!error ... %! crossval (fitrsvm (randn (12, 2), randn (12, 1)), 'Nope', 1) ## Test input validation for the constructor %!error ... %! RegressionSVM () %!error ... %! RegressionSVM (ones (10, 2)) %!error ... %! RegressionSVM (ones (10, 2), ones (5, 1)) %!error ... %! RegressionSVM (ones (5, 2), {'a'; 'b'; 'c'; 'd'; 'e'}) %!error ... %! RegressionSVM (ones (5, 2), ones (5, 3)) %!error ... %! RegressionSVM ([1, 1; Inf, 1; 3, 1], [1; 2; 3]) %!error ... %! RegressionSVM ([1, 1; 2, 1; 3, 1], [1; Inf; 3]) %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'Standardize', 'yes') %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'PredictorNames', 'a') %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'PredictorNames', {'a'}) %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'ResponseName', 5) %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'ResponseTransform', 5) %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'ResponseTransform', 'nope') %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'SVMtype', 5) %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'SVMtype', 'c_svc') %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'Epsilon', -1) %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'KernelFunction', 5) %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'KernelFunction', 'nope') %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'PolynomialOrder', 2.5) %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'KernelScale', 0) %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'KernelOffset', -1) %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'BoxConstraint', 0) %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'Nu', 0) %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'CacheSize', 0) %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'Tolerance', -1) %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'Shrinking', 2) %!error ... %! RegressionSVM (ones (5, 2), ones (5, 1), 'Prior', 1) ## Test input validation for predict and loss %!shared RSVM %! RSVM = RegressionSVM ([1, 1; 2, 1; 3, 2; 4, 2], [2; 4; 6; 8]); %!error ... %! predict (RSVM) %!error ... %! predict (RSVM, []) %!error ... %! predict (RSVM, ones (2, 3)) %!error ... %! loss (RSVM) %!error ... %! loss (RSVM, [1, 1; 2, 1], [2; 4], 'Weights') %!error ... %! loss (RSVM, [], [2; 4]) %!error ... %! loss (RSVM, ones (2, 3), [2; 4]) %!error ... %! loss (RSVM, [1, 1; 2, 1], []) %!error ... %! loss (RSVM, [1, 1; 2, 1], {'a'; 'b'}) %!error ... %! loss (RSVM, [1, 1; 2, 1], [2; 4; 6]) %!error ... %! loss (RSVM, [1, 1; 2, 1], [2; 4], 'LossFun', 5) %!error ... %! loss (RSVM, [1, 1; 2, 1], [2; 4], 'LossFun', 'mae') %!error ... %! loss (RSVM, [1, 1; 2, 1], [2; 4], 'LossFun', @(y, yf, w) [1, 2]) %!error ... %! loss (RSVM, [1, 1; 2, 1], [2; 4], 'Weights', {'a'}) %!error ... %! loss (RSVM, [1, 1; 2, 1], [2; 4], 'Weights', ones (2, 2)) %!error ... %! loss (RSVM, [1, 1; 2, 1], [2; 4], 'Weights', [1; 2; 3]) %!error ... %! loss (RSVM, [1, 1; 2, 1], [2; 4], 'Nope', 1) ## Test input validation for savemodel %!error ... %! savemodel (RSVM) %!error ... %! savemodel (RSVM, 5) %!error ... %! RSVM.ResponseTransform = 'nope'; ## RowsUsed is empty when every observation was used. %!test %! load fisheriris %! X = meas(:,2:4); %! Y = meas(:,1); %! Mdl = fitrsvm (X, Y); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (class (Mdl.RowsUsed), 'double'); %! assert_equal (Mdl.NumObservations, 150); %! assert_equal (rows (Mdl.X), 150); %! assert_equal (rows (Mdl.W), 150); ## A missing response drops its observation and RowsUsed marks it. %!test %! load fisheriris %! X = meas(:,2:4); %! Y = meas(:,1); %! Y(5) = NaN; %! Mdl = fitrsvm (X, Y); %! assert_equal (class (Mdl.RowsUsed), 'logical'); %! assert_equal (size (Mdl.RowsUsed), [150, 1]); %! assert_equal (sum (Mdl.RowsUsed), 149); %! assert_equal (Mdl.RowsUsed(5), false); %! assert_equal (Mdl.NumObservations, 149); %! assert_equal (rows (Mdl.X), 149); %! assert_equal (rows (Mdl.W), 149); ## A missing predictor keeps its observation, so RowsUsed stays empty. %!test %! load fisheriris %! X = meas(:,2:4); %! X(3,2) = NaN; %! Y = meas(:,1); %! Mdl = fitrsvm (X, Y); %! assert_equal (Mdl.RowsUsed, []); %! assert_equal (Mdl.NumObservations, 150); %! assert_equal (rows (Mdl.X), 150); %! assert_equal (sum (isnan (Mdl.X(:))), 1); ## Standardizing summarizes the complete observations. With no classes the ## weights are uniform over them. Values from MATLAB R2024a. %!test %! load fisheriris %! X = meas(:,2:4); %! X(7,2) = NaN; X(120,3) = NaN; %! Mdl = fitrsvm (X, meas(:,1), 'Standardize', true); %! assert_equal (Mdl.Mu, [3.0608108108108096, 3.7655405405405395, ... %! 1.203378378378378], 1e-13); %! assert_equal (Mdl.Sigma, [0.43214937187299296, 1.7636045663278643, ... %! 0.76339873235638711], 1e-13); ## A fitted model survives savemodel and loadmodel: the properties come ## back as they were and it predicts the same. %!test %! load fisheriris %! X = meas(:,2:4); %! Y = meas(:,1); %! Mdl = fitrsvm (X, Y); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (class (M2), 'RegressionSVM'); %! assert_equal (M2.NumObservations, Mdl.NumObservations); %! assert_equal (M2.PredictorNames, Mdl.PredictorNames); %! assert_equal (class (M2.ResponseTransform), class (Mdl.ResponseTransform)); %! assert_equal (predict (M2, X(1:5,:)), predict (Mdl, X(1:5,:)), 1e-12); ## BinEdges is an empty cell, which is what MATLAB reports for this ## learner as well: it fits the predictors as they are. %!test %! load fisheriris %! Mdl = fitrsvm (meas(:,1:3), meas(:,4)); %! assert_equal (class (Mdl.BinEdges), 'cell'); %! assert_equal (Mdl.BinEdges, {}); ## KernelParameters and BoxConstraints, measured on MATLAB R2024a. A ## regression has no classes to reweight, so the scalar applies throughout. %!test %! load fisheriris %! Mdl = fitrsvm (meas(:,1:3), meas(:,4)); %! assert_equal (Mdl.KernelParameters, struct ('Function', 'linear', ... %! 'Scale', 1)); %! assert_equal (Mdl.BoxConstraints, ones (150, 1)); %!test %! load fisheriris %! Mdl = fitrsvm (meas(:,1:3), meas(:,4), 'KernelFunction', 'rbf', ... %! 'BoxConstraint', 2); %! assert_equal (Mdl.KernelParameters.Function, 'gaussian'); %! assert_equal (unique (Mdl.BoxConstraints), 2); ## The Gaussian kernel's default BoxConstraint, measured on MATLAB R2024a. %!test %! load carsmall %! X = [Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! Mdl = fitrsvm (X(ok,:), MPG(ok), 'KernelFunction', 'gaussian'); %! assert_equal (Mdl.ModelParameters.BoxConstraint, 9.266123054, 1e-9); %! assert_equal (Mdl.BoxConstraints(2), 9.266123054, 1e-9); %!test %! load carsmall %! X = [Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! Mdl = fitrsvm (X(ok,:), MPG(ok), 'KernelFunction', 'rbf'); %! assert_equal (Mdl.ModelParameters.BoxConstraint, 9.266123054, 1e-9); %!test %! load carsmall %! X = [Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! w = (1:sum (ok))'; %! Mdl = fitrsvm (X(ok,:), MPG(ok), 'KernelFunction', 'gaussian', ... %! 'Weights', w); %! assert_equal (Mdl.ModelParameters.BoxConstraint, 9.266123054, 1e-9); %! assert_equal (Mdl.BoxConstraints(2), 0.3943031087, 1e-9); %!test %! load carsmall %! X = [Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! Y = MPG(ok); %! Y(1) = 100; %! w = [0; (2:numel(Y))']; %! Mdl = fitrsvm (X(ok,:), Y, 'KernelFunction', 'gaussian', 'Weights', w); %! assert_equal (Mdl.ModelParameters.BoxConstraint, 9.266123054, 1e-9); %!test %! load carsmall %! X = [Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! Mdl = fitrsvm (X(ok,:), 5 * ones (sum (ok), 1), ... %! 'KernelFunction', 'gaussian'); %! assert_equal (Mdl.ModelParameters.BoxConstraint, 1); %!test %! load carsmall %! X = [Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! Mdl = fitrsvm (X(ok,:), MPG(ok), 'KernelFunction', 'polynomial', ... %! 'Standardize', true); %! assert_equal (Mdl.ModelParameters.BoxConstraint, 1); ## KernelScale divides every predictor, as in MATLAB; expected values are ## R2024a's. %!test %! load fisheriris %! Mdl = fitrsvm (meas(:,1:3), meas(:,4), 'KernelScale', 2); %! assert_equal (predict (Mdl, meas([1, 75, 150], 1:3)), ... %! [0.222015641544; 1.35505851032; 1.83323561521], 2e-3); %!test %! load fisheriris %! Mdl = fitrsvm (meas(:,1:3), meas(:,4), 'KernelFunction', 'gaussian', ... %! 'KernelScale', 2); %! assert_equal (predict (Mdl, meas([1, 75, 150], 1:3)), ... %! [0.220070671683; 1.2611886798; 1.9057640742], 3e-3); %!test %! load fisheriris %! Mdl = fitrsvm (meas(:,1:3), meas(:,4), 'KernelScale', 2); %! Q = meas([1, 75, 150], 1:3); %! assert_equal (predict (Mdl, Q), (Q / 2) * Mdl.Beta + Mdl.Bias, 1e-12); %!test %! load fisheriris %! Mdl = fitrsvm (meas(:,1:3), meas(:,4), 'KernelFunction', 'gaussian', ... %! 'KernelScale', 2); %! assert_equal (Mdl.SupportVectors, meas(Mdl.IsSupportVector, 1:3)); %!test %! load fisheriris %! Mdl = fitrsvm (meas(:,1:3), meas(:,4)); %! fname = tempname (); %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (M2.KernelParameters, Mdl.KernelParameters); %! assert_equal (M2.BoxConstraints, Mdl.BoxConstraints); ## HyperparameterOptimizationResults is declared for MATLAB compatibility and ## stays empty, this class running no search over its hyperparameters. %!test %! load fisheriris %! Mdl = fitrsvm (meas(:,1:3), meas(:,4)); %! assert_equal (isempty (Mdl.HyperparameterOptimizationResults), true); ## ModelParameters records what LIBSVM was given. The field list and its ## order are ours, MATLAB's SMO and ISDA parameters having no counterpart ## here. %!test %! load fisheriris %! Mdl = fitrsvm (meas(:,2:4), meas(:,1)); %! assert_equal (fieldnames (Mdl.ModelParameters)', {'SVMtype', ... %! 'BoxConstraint', 'CacheSize', 'KernelScale', 'KernelOffset', ... %! 'KernelFunction', 'KernelPolynomialOrder', 'Epsilon', 'Nu', ... %! 'Tolerance', 'Shrinking', 'StandardizeData', 'Version', 'Method', ... %! 'Type'}); %!test %! load fisheriris %! MP = fitrsvm (meas(:,2:4), meas(:,1), 'Standardize', true).ModelParameters; %! assert_equal (MP.StandardizeData, true); %! assert_equal (MP.Version, 1); %! assert_equal (MP.Method, 'SVM'); %! assert_equal (MP.Type, 'regression'); ## Epsilon is reported as the fit resolved it, and Nu is reported where ## MATLAB leaves it empty on a regression model, this class offering ## 'nu_svr' and the value being a real one. %!test %! load fisheriris %! MP = fitrsvm (meas(:,2:4), meas(:,1)).ModelParameters; %! assert_equal (MP.Epsilon, 0.0963676797627873, 1e-15); %! assert_equal (MP.Nu, 0.5); ## Every documented response transform reaches the response that is reported. %!test %! load fisheriris %! Mdl = fitrsvm (meas(:,2:4), meas(:,1)); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! T = {'identity', @(x) x; 'exp', @(x) exp (x); 'log', @(x) log (x)}; %! for i = 1:rows (T) %! Mdl.ResponseTransform = T{i,1}; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, T{i,2}(raw), 1e-12); %! endfor ## A function handle is taken as given and applied to the response. %!test %! load fisheriris %! Mdl = fitrsvm (meas(:,2:4), meas(:,1)); %! Mdl.ResponseTransform = 'none'; %! raw = predict (Mdl, meas([1, 60, 120],2:4)); %! Mdl.ResponseTransform = @(x) x .^ 2; %! yhat = predict (Mdl, meas([1, 60, 120],2:4)); %! assert_equal (yhat, raw .^ 2, 1e-12); %!shared Xc, Dc, yr, Xq, Dq %! k = (0:119)'; %! c1 = mod (k, 3) + 1; %! x2 = sin (k); %! c3 = 10 * (mod (floor (k / 2), 2) + 1); %! Xc = [c1, x2, c3]; %! Dc = [c1 == 1, c1 == 2, c1 == 3, x2, c3 == 10, c3 == 20]; %! yr = 5 * (c1 == 2) + 0.5 * x2 - 3 * (c3 == 20) + 0.1 * cos (k); %! yc = yr > 1; %! Xq = [1, 0, 10; 2, 0.5, 20]; %! Dq = [1, 0, 0, 0, 1, 0; 0, 1, 0, 0.5, 0, 1]; %!test # MATLAB parity: a categorical predictor is dummy coded in its place %! Mdl = RegressionSVM (Xc, yr, 'CategoricalPredictors', [1, 3]); %! H = RegressionSVM (Dc, yr); %! assert_equal (Mdl.CategoricalPredictors, [1, 3]); %! assert_equal (Mdl.ExpandedPredictorNames, {'x1 == 1', 'x1 == 2', ... %! 'x1 == 3', 'x2', 'x3 == 10', 'x3 == 20'}); %! assert_equal (Mdl.Beta, H.Beta, 1e-12); %! assert_equal (predict (Mdl, Xq), predict (H, Dq), 1e-12); %! ys = sort (yr); %! assert_equal (predict (Mdl, [4, 0, 10]), ys(ceil (numel (ys) / 2))); %!test # MATLAB parity: the coded columns are not standardized %! Mdl = RegressionSVM (Xc, yr, 'CategoricalPredictors', [1, 3], ... %! 'Standardize', true); %! assert_equal (Mdl.Mu([1:3, 5:6]), zeros (1, 5)); %! assert_equal (Mdl.Sigma([1:3, 5:6]), ones (1, 5)); %!test # the coding travels with saved models and cross-validation folds %! Mdl = RegressionSVM (Xc, yr, 'CategoricalPredictors', [1, 3]); %! fname = [tempname(), '.mdl']; %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (predict (M2, Xq), predict (Mdl, Xq)); %! CV = crossval (Mdl, 'KFold', 3); %! assert_equal (CV.Trained{1}.CategoricalPredictors, [1, 3]); %!error ... %! RegressionSVM (Xc, yr, 'CategoricalPredictors', 4) ## A table at prediction %!test # the class may be built from a table as the fitter builds it %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'SL'}); %! Mdl = RegressionSVM (T, 'SL'); %! assert_equal (Mdl.PredictorNames, {'SW', 'PL'}); %! assert_equal (Mdl.ResponseName, 'SL'); %!test # a missing value and an unseen level predict as missing, as R2024a %! k = categorical (repmat ({'a'; 'b'; 'c'}, 8, 1)); %! x = (1:24)'; %! y = double (k) * 10 + x / 10; %! Mdl = fitrsvm (table (k, x, y), 'y'); %! N = table (categorical ({'a'; ''; 'z'}), [5; 5; 5], ... %! 'VariableNames', {'k', 'x'}); %! yFit = predict (Mdl, N); %! assert_equal (yFit(2:3), predict (Mdl, [NaN, 5; NaN, 5])); %! assert_equal (yFit(2:3), [21.1; 21.1], 1e-4); ## A table at loss %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,2:3); %! y = meas(:,1); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SW', 'PL'}); %! T.SL = y; %! Mdl = fitrsvm (T, 'SL'); %! a = loss (Mdl, X, y); %! assert_equal (loss (Mdl, T(:,1:2), y), a); %! assert_equal (loss (Mdl, T, 'SL'), a); %! assert_equal (loss (Mdl, T), a); ## Observation weights %!error ... %! RegressionSVM ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', 'Weights', int8 ([1; 1; 1; 1])) %!error ... %! RegressionSVM ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', 'Weights', true (4, 1)) %!error ... %! RegressionSVM ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', 'Weights', [1; 1]) %!error ... %! RegressionSVM ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', 'Weights', [1; -1; 1; 1]) %!error ... %! RegressionSVM ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', 'Weights', zeros (4, 1)) %!test %! ## Box constraints are n * C * W, as R2024a sets them %! load fisheriris %! Mdl = RegressionSVM (meas(:,2:4), meas(:,1), 'Weights', 1 + (1:150)' / 7); %! assert_equal (sum (Mdl.W), 1, 1e-15); %! assert_equal (Mdl.BoxConstraints([1, 150]), ... %! [0.09696969696969696; 1.903030303030303], 1e-15); %!test %! ## Single weights are stored single, summing to one %! load fisheriris %! Mdl = RegressionSVM (meas(:,2:4), meas(:,1), ... %! 'Weights', single (1 + (1:150)' / 7)); %! assert_equal (class (Mdl.W), 'single'); %! assert_equal (class (Mdl.BoxConstraints), 'double'); %! assert_equal (sum (double (Mdl.W)), 1, 1e-6); %!test %! ## Rows of zero weight are left out, as R2024a leaves them %! load fisheriris %! w = 1 + (1:150)' / 7; %! w(5) = 0; %! Mdl = RegressionSVM (meas(:,2:4), meas(:,1), 'Weights', w); %! assert_equal (Mdl.NumObservations, 149); %! assert_equal (Mdl.RowsUsed(5), false); %!test %! ## Standardization weighs the observations, as R2024a does %! load fisheriris %! Mdl = RegressionSVM (meas(:,2:4), meas(:,1), 'Standardize', true, ... %! 'Weights', 1 + (1:150)' / 7); %! assert_equal (Mdl.Mu, [2.965608080808081, 4.573050505050505, ... %! 1.55819797979798], 1e-14); %! assert_equal (Mdl.Sigma, [0.3809861139391035, 1.433169957562235, ... %! 0.6470320013330532], 1e-14); %!test %! ## A row missing a predictor is predicted as the weighted lower median %! load fisheriris %! X = meas(:,2:4); %! X(7,2) = NaN; %! Mdl = RegressionSVM (X, meas(:,1), 'Weights', 1 + (1:150)' / 7); %! assert_equal (predict (Mdl, [NaN, 1, 1]), 6.2); %! assert_equal (isnan (Mdl.BoxConstraints(7)), true); %! assert_equal (Mdl.IsSupportVector(7), false); %! assert_equal (sum (Mdl.IsSupportVector), rows (Mdl.SupportVectors)); %!test %! ## resubLoss weighs the observations by W unless given weights %! load fisheriris %! w = 1 + (1:150)' / 7; %! Mdl = RegressionSVM (meas(:,2:4), meas(:,1), 'Weights', w); %! assert_equal (resubLoss (Mdl), ... %! loss (Mdl, meas(:,2:4), meas(:,1), 'Weights', w), 1e-15); statistics-release-2.1.2/inst/Machine_Learning/RegressionTree.m000066400000000000000000003351601527311104300246070ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef RegressionTree < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} RegressionTree ## ## Binary decision tree for regression ## ## The @code{RegressionTree} class implements a CART binary decision tree. ## Growth splits each node on the single predictor and cut point that lower ## the squared error of the response the most, and stops when a node is too ## small to be a parent, has no split leaving enough observations on both ## sides, or already accounts for all but a tolerance of the error the root ## carried. The grown tree is then optionally reduced, first by merging ## the leaves that buy no accuracy and then by cost complexity pruning, ## which orders the branch nodes by how little error their subtrees remove ## and records that order so a subtree of any size can be recovered ## afterwards with @code{prune}. ## ## Create a @code{RegressionTree} object by using the @code{fitrtree} ## function or the class constructor. ## ## The fit is carried out by the compiled engine @code{treetrain} and ## predictions by @code{treepredict}, which the classification tree shares. ## ## An observation missing the predictor a node cuts on descends to neither ## child. It is counted in that node and in every node above it, and ## @code{predict} stops it there and gives it that node's answer, so a row ## is never sent down a branch on evidence it does not carry. ## Choosing a split, a predictor known for only part of a node is scored on ## the rows it is known for, by how much the split lowers their impurity, ## and that is scaled by their share of the node, as C4.5 does, so a ## predictor known for few rows is not preferred for that alone. ## ## A categorical predictor is split into two sets of levels by ordering the ## levels by their mean response, and an observation whose level a node did ## not see stops there, as one missing the predictor does. The lower means ## go left; MATLAB does not always keep that side, and equally good splits ## may be chosen differently, so a node's sides and numbering may differ. ## ## @strong{What this class does not do yet.} Surrogate splits are not ## implemented, and an option asking for them is refused rather than ## quietly ignored. The six @code{Surrogate} properties are therefore ## always empty, as they are in MATLAB without surrogate splits. ## ## @seealso{fitrtree, ClassificationTree, treetrain, treepredict} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {RegressionTree} {property} X ## ## Predictor data ## ## A numeric matrix holding the predictor data the model was fitted on. ## Each column is one predictor and each row one observation. This ## property is read-only. ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} Y ## ## Response data ## ## A numeric column vector with one element per row of @var{X}, holding ## the observed response of each observation. This property is ## read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} NumObservations ## ## Number of observations ## ## A positive integer, the number of observations the model was fitted ## on. It counts the rows kept, so it is smaller than the number of rows ## given whenever a response was missing. This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} RowsUsed ## ## Rows used for fitting ## ## A logical column vector with one element per row of the predictor data ## as it was given, true for each row used for fitting. It is empty, ## @qcode{[]}, when every row was used, so a non-empty value means that ## rows were dropped. A row is dropped when its response is missing, when ## every predictor is missing, or when its weight is zero; a row missing ## only some of its predictors is kept. This property is read-only. ## ## @end deftp RowsUsed = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} W ## ## Observation weights ## ## A numeric column vector of the weights the fit used, one per retained ## observation. They are the weights given, scaled to sum to one. It has ## the class of the @qcode{'Weights'} given, single or double. This ## property is read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} PredictorNames ## ## Names of the predictor variables ## ## A cell array of character vectors with one name per column of ## @var{X}. This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} ResponseName ## ## Name of the response variable ## ## A character vector naming the response. This property is read-only. ## ## @end deftp ResponseName = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A row vector of column indices into @var{X}, naming the predictors ## treated as categorical, empty when none is. This property is ## read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} ExpandedPredictorNames ## ## Expanded predictor names ## ## A cell array of character vectors. It differs from ## @code{PredictorNames} only when a categorical predictor has been ## expanded into one column per level, which this class does not do, so ## the two are always equal. This property is read-only. ## ## @end deftp ExpandedPredictorNames = {}; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} BinEdges ## ## Bin edges of the predictors ## ## A cell array with one column vector of bin edges per predictor, empty ## unless the predictors were binned before fitting. Binning is not ## implemented, so this is always empty. This property is read-only. ## ## @end deftp BinEdges = {}; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} ModelParameters ## ## Parameters the fit was run with ## ## A structure recording the options the tree was grown under: ## @qcode{SplitCriterion}, @qcode{MinParent}, @qcode{MinLeaf}, ## @qcode{MaxSplits}, @qcode{NVarToSample}, @qcode{MergeLeaves}, ## @qcode{Prune}, @qcode{PruneCriterion}, @qcode{QEToler}, ## @qcode{NSurrogate}, @qcode{MaxCat}, @qcode{AlgCat}, ## @qcode{PredictorSelection}, @qcode{Method} and @qcode{Type}. ## @qcode{SplitCriterion} and @qcode{PruneCriterion} are both ## @qcode{'mse'}, the only criterion a regression tree has, and ## @qcode{QEToler} is the tolerance growth stops at. ## ## @qcode{MinParent} is the value the fit used, which is ## @code{max (MinParentSize, 2 * MinLeafSize)} and so may exceed the ## @qcode{'MinParentSize'} asked for. This property is read-only. ## ## @end deftp ModelParameters = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} HyperparameterOptimizationResults ## ## Results of a hyperparameter optimization ## ## Hyperparameter optimization is not implemented, so this is always ## empty. This property is read-only. ## ## @end deftp HyperparameterOptimizationResults = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} NumNodes ## ## Number of nodes in the tree ## ## A positive integer, the number of nodes the tree holds, branch nodes ## and leaves together. Nodes are numbered as they are created, so a ## parent always carries a lower number than either of its children. ## This property is read-only. ## ## @end deftp NumNodes = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} Children ## ## Child nodes of each node ## ## A @math{NumNodesx2} matrix naming the left and the right child of each ## node. A leaf carries a zero in both columns. This property is ## read-only. ## ## @end deftp Children = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} Parent ## ## Parent of each node ## ## A column vector naming the parent of each node. The root carries a ## zero. This property is read-only. ## ## @end deftp Parent = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} IsBranchNode ## ## Which nodes are branch nodes ## ## A logical column vector, true for each node that carries a split and ## false for each leaf. This property is read-only. ## ## @end deftp IsBranchNode = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} CutPredictor ## ## Name of the predictor each node cuts on ## ## A cell array of character vectors with one entry per node, holding the ## name of the predictor the node splits on and an empty character vector ## at a leaf. This property is read-only. ## ## @end deftp CutPredictor = {}; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} CutPredictorIndex ## ## Index of the predictor each node cuts on ## ## A column vector holding, for each node, the column of @var{X} the node ## splits on, and zero at a leaf. This property is read-only. ## ## @end deftp CutPredictorIndex = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} CutPoint ## ## Cut point of each node ## ## A column vector holding, for each node, the value the split compares ## the predictor against: an observation goes left when its value is less ## than the cut point and right otherwise. A leaf and a categorical cut ## carry @qcode{NaN}. ## This property is read-only. ## ## @end deftp CutPoint = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} CutType ## ## Type of cut at each node ## ## A cell array of character vectors holding @qcode{'continuous'} at a ## branch node that cuts a numeric predictor at a point, ## @qcode{'categorical'} at one that splits a set of levels, and an empty ## character vector at a leaf. This property is read-only. ## ## @end deftp CutType = {}; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} CutCategories ## ## Categories used at each branch ## ## A @math{NumNodesx2} cell array holding, for a node that cuts a ## categorical predictor, the levels sent left and the levels sent right. ## Every other entry is empty. This property is read-only. ## ## @end deftp CutCategories = {}; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} CategoricalSplit ## ## Categorical splits of the tree ## ## A @math{Nx2} cell array with one row per categorical split, in node ## order, holding the levels sent left and the levels sent right. It is ## empty when no split is categorical. This property is read-only. ## ## @end deftp CategoricalSplit = {}; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} NodeSize ## ## Number of observations at each node ## ## A column vector holding how many training observations reached each ## node. A row missing the predictor its node cuts on is counted at that ## node and at none below it, so a parent's size is not in general the ## sum of its children's. This property is read-only. ## ## @end deftp NodeSize = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} NodeMean ## ## Mean response at each node ## ## A column vector holding, for each node, the weighted mean of the ## response over the observations that reached it. It is what ## @code{predict} answers for a row that comes to rest there. This ## property is read-only. ## ## @end deftp NodeMean = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} NodeError ## ## Mean squared error of each node ## ## A column vector holding, for each node, the weighted mean squared ## error of the response about the node's mean. This property is ## read-only. ## ## @end deftp NodeError = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} NodeProbability ## ## Probability of reaching each node ## ## A column vector holding, for each node, the total weight of the ## observations that reached it, the weights being those in @code{W}. ## The root carries one. This property is read-only. ## ## @end deftp NodeProbability = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} NodeRisk ## ## Risk of each node ## ## A column vector holding, for each node, its mean squared error ## weighted by the probability of reaching it, which is the squared error ## the node contributes to the whole tree. This property is read-only. ## ## @end deftp NodeRisk = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} PruneList ## ## Pruning level of each node ## ## A column vector holding, for each branch node, the level of the cost ## complexity sequence at which it stops being a branch node, and zero at ## a leaf. Pruning the tree to level @var{L} turns every node whose ## level is between one and @var{L} into a leaf. It is empty when ## neither @qcode{'Prune'} nor @qcode{'MergeLeaves'} was asked for, since ## no sequence was then estimated. This property is read-only. ## ## @end deftp PruneList = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} PruneAlpha ## ## Cost complexity parameter of each pruning level ## ## A column vector with one element per level of the pruning sequence, ## the first of which is zero and stands for the unpruned tree. Level ## @var{L} is the smallest subtree that is optimal for every complexity ## parameter from @code{PruneAlpha(@var{L}+1)} up to the next one. This ## property is read-only. ## ## @end deftp PruneAlpha = []; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} SurrogateCutCategories ## ## Categories of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutCategories = {}; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} SurrogateCutFlip ## ## Cut assignments of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutFlip = {}; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} SurrogateCutPoint ## ## Cut points of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutPoint = {}; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} SurrogateCutType ## ## Types of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutType = {}; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} SurrogateCutPredictor ## ## Predictors of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogateCutPredictor = {}; ## -*- texinfo -*- ## @deftp {RegressionTree} {property} SurrogatePredictorAssociation ## ## Predictive measures of association of the surrogate splits ## ## Surrogate splits are not implemented, so this is always empty. This ## property is read-only. ## ## @end deftp SurrogatePredictorAssociation = {}; endproperties properties (GetAccess = public, SetAccess = public) ## -*- texinfo -*- ## @deftp {RegressionTree} {property} ResponseTransform ## ## Transform applied to the predicted response ## ## A character vector naming the function @code{predict} applies to the ## response it predicts before returning it, or a function handle taking ## and returning an array of the same size. The default is ## @qcode{'none'}. ## ## @end deftp ResponseTransform = []; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ## The parsed ResponseTransform, applied to the response by predict. RTfun = []; ## The total weight of the observations that reached each node, before it ## was divided by the root's to give NodeProbability. NodeWeight = []; ## The risk each branch node carries on account of the observations that ## stop there, missing the predictor it cuts on. Those observations are ## in neither child, so a subtree's risk is its children's plus this. It ## is zero at a leaf and zero throughout a tree fitted on data with no ## missing values. HeldRisk = []; ## The observation weights as they were given, before they were scaled to ## sum to one. A fold of a cross-validated model is grown with a slice ## of these. RawWeights = []; endproperties ## Set methods for the properties a user may assign after fitting. methods (Hidden) function this = set.ResponseTransform (this, val) [this.RTfun, this.ResponseTransform] = ... parseResponseTransform (val, 'RegressionTree'); endfunction function disp (this) fprintf ('\n RegressionTree\n\n'); fprintf ('%22s: %s\n', 'ResponseName', this.ResponseName); fprintf ('%22s: %s\n', 'CategoricalPredictors', ... mat2str (this.CategoricalPredictors)); fprintf ('%22s: %s\n', 'ResponseTransform', this.ResponseTransform); fprintf ('%22s: %d\n', 'NumObservations', this.NumObservations); fprintf ('%22s: %d\n', 'NumNodes', this.NumNodes); fprintf ('\n'); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {RegressionTree} {@var{obj} =} RegressionTree (@var{X}, @var{Y}) ## @deftypefnx {RegressionTree} {@var{obj} =} RegressionTree (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {RegressionTree} {@var{obj} =} RegressionTree (@var{Tbl}, @var{formula}) ## @deftypefnx {RegressionTree} {@var{obj} =} RegressionTree (@var{Tbl}, @var{Y}) ## @deftypefnx {RegressionTree} {@var{obj} =} RegressionTree (@dots{}, @var{name}, @var{value}) ## ## Grow a binary decision tree for regression. ## ## @code{@var{obj} = RegressionTree (@var{X}, @var{Y})} grows a tree on ## the @math{NxP} numeric matrix @var{X} of predictor data and the ## @math{Nx1} numeric response @var{Y}, and returns it as a ## @code{RegressionTree} object. ## ## @code{@var{obj} = RegressionTree (@dots{}, @var{name}, @var{value})} ## takes the options below. ## ## @multitable @columnfractions 0.24 0.74 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'CategoricalPredictors'} @tab The predictors whose values ## are levels, as indices, as a logical vector with one element per ## predictor, or as @qcode{'all'}. ## A predictor may be named rather than indexed, as a character matrix of ## one padded name per row, a string array or a cellstr; a name must match ## an entry of @qcode{'PredictorNames'} exactly, its case included. ## ## @item @qcode{'MaxNumCategories'} @tab A nonnegative integer, recorded ## in @code{ModelParameters}. The default is 10. A regression orders ## the levels by their mean response, which finds the best split whatever ## the number of levels. ## ## @item @qcode{'MaxNumSplits'} @tab A nonnegative integer, the largest ## number of branch nodes the tree may take. The default is one less ## than the number of observations, which is as many as a tree can have. ## ## @item @qcode{'MergeLeaves'} @tab @qcode{'on'} (default) or ## @qcode{'off'}. When on, a pair of leaves whose parent is no worse ## than the two of them together is merged back into that parent. ## ## @item @qcode{'MinLeafSize'} @tab A positive integer, the fewest ## observations a leaf may hold. The default is 1. A split leaving ## fewer than this on either side is not taken. ## ## @item @qcode{'MinParentSize'} @tab A positive integer, the fewest ## observations a node must hold to be split at all. The default is 10. ## The value the fit uses is @code{max (MinParentSize, 2 * MinLeafSize)}, ## since a smaller node cannot give both children a legal leaf. ## ## @item @qcode{'PredictorNames'} @tab A cell array of character vectors ## naming the columns of @var{X}. ## ## @item @qcode{'Prune'} @tab @qcode{'on'} (default) or @qcode{'off'}. ## When on, the cost complexity pruning sequence is estimated and ## reported in @code{PruneList} and @code{PruneAlpha}. The tree returned ## is the unpruned one either way; @code{prune} takes a subtree out of ## the sequence. ## ## @item @qcode{'PruneCriterion'} @tab @qcode{'mse'}, the only criterion ## a regression tree has. ## ## @item @qcode{'QuadraticErrorTolerance'} @tab A positive scalar. A ## node whose squared error has fallen to this fraction of the root's is ## not split further. The default is 1e-6. ## ## @item @qcode{'ResponseName'} @tab A character vector naming the ## response. The default is @qcode{'Y'}. ## ## @item @qcode{'ResponseTransform'} @tab A character vector naming a ## transform to apply to the predicted response, or a function handle. ## The default is @qcode{'none'}. ## ## @item @qcode{'SplitCriterion'} @tab @qcode{'mse'}, the only criterion ## a regression tree has. ## ## @item @qcode{'Weights'} @tab A nonnegative single or double vector with ## one element per observation. The default is uniform. The model's ## @code{W} keeps the class of the weights, while every computation runs in ## double, so the predictions are double where MATLAB returns single. ## ## @end multitable ## ## @seealso{fitrtree, ClassificationTree, treetrain, treepredict} ## @end deftypefn function this = RegressionTree (X, Y, varargin) ## Input validation if (nargin < 2) error ("RegressionTree: too few input arguments."); endif ## A table names its own predictors and says which hold levels ## An ordinal column is a numeric predictor here, as in R2024a [this, X, Y, varargin] = resolveTable (this, 'RegressionTree', ... X, Y, varargin, true); if (mod (numel (varargin), 2) != 0) error (strcat ("RegressionTree: name-value arguments must be in", ... " pairs.")); endif if (! (isnumeric (X) && isreal (X) && ismatrix (X) && ! isempty (X))) error (strcat ("RegressionTree: X must be a non-empty real numeric", ... " matrix.")); endif if (! (isnumeric (Y) && isreal (Y) && isvector (Y))) error ("RegressionTree: Y must be a real numeric vector."); endif if (rows (X) != numel (Y)) error (strcat ("RegressionTree: number of rows in X and Y must be", ... " equal.")); endif Y = double (Y(:)); ## Parse optional paired arguments optNames = {'PredictorNames', 'ResponseName', 'ResponseTransform', ... 'Weights', 'MaxNumSplits', 'MinLeafSize', 'MinParentSize', ... 'NumVariablesToSample', 'MergeLeaves', 'Prune', ... 'QuadraticErrorTolerance', 'SplitCriterion', ... 'PruneCriterion', 'CategoricalPredictors', ... 'MaxNumCategories'}; ## An empty default stands for one resolved once the data are known: ## 'PredictorNames' are x1, x2, ... and 'ResponseName' is 'Y'; ## 'Weights' are uniform; 'MaxNumSplits' is one less than the number ## of rows kept; and no predictor is categorical. dfValues = {{}, [], 'none', [], [], 1, 10, 'all', 'on', 'on', 1e-6, ... 'mse', 'mse', [], 10}; [PredictorNames, ResponseName, ResponseTransform, Weights, ... MaxNumSplits, MinLeafSize, MinParentSize, NumVarSample, MergeLeaves, ... Prune, QEToler, SplitCrit, PruneCrit, CatPreds, MaxNumCat, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! isempty (PredictorNames) && ! iscellstr (PredictorNames)) error (strcat ("RegressionTree: 'PredictorNames' must be supplied", ... " as a cellstring array.")); elseif (! isempty (PredictorNames) && numel (PredictorNames) != columns (X)) error (strcat ("RegressionTree: 'PredictorNames' must equal the", ... " number of columns in X.")); endif if (! isempty (ResponseName) && ! (ischar (ResponseName) && isrow (ResponseName))) error ("RegressionTree: 'ResponseName' must be a character vector."); endif this.ResponseTransform = ResponseTransform; errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("RegressionTree: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isvector (Weights) && isreal (Weights))) error ("RegressionTree: 'Weights' must be a real numeric vector."); endif if (! isempty (Weights) && numel (Weights) != rows (X)) error ("RegressionTree: 'Weights' must have one element per row in X."); endif if (! isempty (Weights) && (any (Weights < 0) || ! (sum (Weights) > 0))) error (strcat ("RegressionTree: 'Weights' must be nonnegative and", ... " must not be all zero.")); endif if (! isempty (MaxNumSplits) && ! (isnumeric (MaxNumSplits) && isscalar (MaxNumSplits) && isreal (MaxNumSplits) && MaxNumSplits >= 0 && MaxNumSplits == fix (MaxNumSplits))) error ("RegressionTree: 'MaxNumSplits' must be a nonnegative integer."); endif if (! (isnumeric (MinLeafSize) && isscalar (MinLeafSize) && isreal (MinLeafSize) && MinLeafSize >= 1 && MinLeafSize == fix (MinLeafSize))) error ("RegressionTree: 'MinLeafSize' must be a positive integer."); endif if (! (isnumeric (MinParentSize) && isscalar (MinParentSize) && isreal (MinParentSize) && MinParentSize >= 1 && MinParentSize == fix (MinParentSize))) error ("RegressionTree: 'MinParentSize' must be a positive integer."); endif if (! ((ischar (NumVarSample) && strcmpi (NumVarSample, 'all')) || (isnumeric (NumVarSample) && isscalar (NumVarSample) && isreal (NumVarSample) && NumVarSample >= 1 && NumVarSample == fix (NumVarSample)))) error (strcat ("RegressionTree: 'NumVariablesToSample' must be a", ... " positive integer or 'all'.")); endif if (! (ischar (MergeLeaves) && any (strcmpi (MergeLeaves, {'on', 'off'})))) error ("RegressionTree: 'MergeLeaves' must be either 'on' or 'off'."); endif if (! (ischar (Prune) && any (strcmpi (Prune, {'on', 'off'})))) error ("RegressionTree: 'Prune' must be either 'on' or 'off'."); endif if (! (isnumeric (QEToler) && isscalar (QEToler) && isreal (QEToler) && QEToler > 0)) error (strcat ("RegressionTree: 'QuadraticErrorTolerance' must be", ... " a positive scalar.")); endif ## A regression tree has one criterion either way, so the two names are ## taken and checked rather than refused outright. if (! (ischar (SplitCrit) && strcmpi (SplitCrit, 'mse'))) error (strcat ("RegressionTree: 'SplitCriterion' must be 'mse' for", ... " a regression tree.")); endif if (! (ischar (PruneCrit) && strcmpi (PruneCrit, 'mse'))) error (strcat ("RegressionTree: 'PruneCriterion' must be 'mse' for", ... " a regression tree.")); endif if (! (isnumeric (MaxNumCat) && isscalar (MaxNumCat) && isreal (MaxNumCat) && MaxNumCat >= 0 && (MaxNumCat == fix (MaxNumCat) || isinf (MaxNumCat)))) error (strcat ("RegressionTree: 'MaxNumCategories' must be a", ... " nonnegative integer.")); endif ## Options MATLAB takes that this class does not implement are named ## one by one, so that asking for one is refused rather than quietly ## doing nothing; anything else left over is unknown. notImpl = {'Surrogate', 'PredictorSelection', 'NumBins', ... 'OptimizeHyperparameters', ... 'HyperparameterOptimizationOptions'}; cvNames = {'CrossVal', 'CVPartition', 'Holdout', 'KFold', 'Leaveout'}; for i = 1:2:numel (args) if (ischar (args{i}) && any (strcmpi (args{i}, notImpl))) error ("RegressionTree: '%s' is not implemented.", args{i}); elseif (ischar (args{i}) && any (strcmpi (args{i}, cvNames))) error (strcat ("RegressionTree: '%s' is not implemented; fit", ... " the model and cross-validate it afterwards."), ... args{i}); endif endfor if (! isempty (args)) error ("RegressionTree: invalid optional paired argument."); endif ## Default predictor and response names if (isempty (PredictorNames)) PredictorNames = cell (1, columns (X)); for ii = 1:columns (X) PredictorNames{ii} = sprintf ("x%d", ii); endfor endif if (isempty (ResponseName)) ResponseName = 'Y'; endif this.PredictorNames = PredictorNames; this.ResponseName = ResponseName; ## The categorical predictors, checked as every learner checks them if (ischar (CatPreds) && strcmpi (CatPreds, 'none')) CatPreds = []; endif [C, errmsg] = dummyCoding (X, CatPreds, PredictorNames); if (! isempty (errmsg)) error ("RegressionTree: %s", errmsg); endif this.CategoricalPredictors = []; if (! isempty (C.Index)) this.CategoricalPredictors = C.Index; endif this.ExpandedPredictorNames = PredictorNames; ## An observation is dropped when its response is missing, when every ## predictor is missing, or when its weight is zero, as R2024a drops ## it. A row missing only some predictors is kept and reported as ## used; the fit sends it as far down the tree as the predictors it ## does carry allow. RowsUsed = ! isnan (Y); if (! any (RowsUsed)) error ("RegressionTree: no observations with a known response."); endif RowsUsed &= ! all (isnan (X), 2); if (! isempty (Weights)) RowsUsed &= Weights(:) != 0; endif if (! any (RowsUsed)) error (strcat ("RegressionTree: no observation has a known", ... " response, a known predictor and a nonzero weight.")); endif Y = Y(RowsUsed); X = X(RowsUsed, :); if (! isempty (Weights)) Weights = Weights(RowsUsed); endif this.X = X; this.Y = Y; this.NumObservations = rows (X); ## RowsUsed is left empty when every observation was used, as in MATLAB if (all (RowsUsed)) this.RowsUsed = []; else this.RowsUsed = RowsUsed; endif ## The raw weights, kept so that a fold can be given a slice of them if (isempty (Weights)) RawWeights = ones (this.NumObservations, 1); else RawWeights = Weights(:); endif ## The weights keep their class in the model; every computation runs on ## them as double. this.RawWeights = RawWeights; RawWeights = double (RawWeights); ## A regression carries no prior, so the weights are simply scaled to ## sum to one, which is the W MATLAB reports. W = RawWeights / sum (RawWeights); this.W = cast (W, class (this.RawWeights)); if (isempty (MaxNumSplits)) MaxNumSplits = max (this.NumObservations - 1, 0); endif ## A node smaller than two leaves cannot be split whatever the parent ## size asked for, so the fit uses the larger of the two. MinParent = max (MinParentSize, 2 * MinLeafSize); mergeOn = strcmpi (MergeLeaves, 'on'); pruneOn = strcmpi (Prune, 'on'); ## A number covering every predictor samples them all, and is reported ## as 'all', as MATLAB reports it. The engine's seed is drawn only when ## predictors are sampled, so an unsampled fit leaves the generator alone. if (isnumeric (NumVarSample) && NumVarSample >= columns (X)) NumVarSample = 'all'; endif opts = struct ('NumClasses', 1, ... 'MinParent', MinParent, ... 'MinLeaf', MinLeafSize, ... 'MaxSplits', MaxNumSplits, ... 'SplitCriterion', 'mse', ... 'MergeLeaves', mergeOn, ... 'QEToler', QEToler); if (isnumeric (NumVarSample)) opts.NumVariablesToSample = NumVarSample; opts.Seed = randi ([0, 4294967295]); endif if (! isempty (this.CategoricalPredictors)) opts.CategoricalPredictors = this.CategoricalPredictors; opts.MaxNumCategories = MaxNumCat; endif T = treetrain (X, this.Y, W, opts); ## The node table the engine returns this.NumNodes = T.NumNodes; this.Children = T.Children; this.Parent = T.Parent; this.CutPredictorIndex = T.CutPredictorIndex; this.CutPoint = T.CutPoint; this.CutCategories = T.CutCategories; this.NodeSize = T.NodeSize; this.NodeWeight = T.NodeWeight; this.NodeMean = T.NodeMean; this.NodeError = T.NodeError; this = fillCuts (this); this = deriveNodes (this); this = deriveHeld (this); ## Merging leaves is the first step of the cost complexity sequence, so ## a merged tree carries one whether or not pruning was asked for. ## Measured on R2024a: only turning both off leaves the two pruning ## properties empty. if (pruneOn || mergeOn) this = pruneSequence (this); else this.PruneList = []; this.PruneAlpha = []; endif this.ModelParameters = struct ('SplitCriterion', 'mse', ... 'MinParent', MinParent, ... 'MinLeaf', MinLeafSize, ... 'MaxSplits', MaxNumSplits, ... 'NVarToSample', NumVarSample, ... 'MergeLeaves', tolower (MergeLeaves), ... 'Prune', tolower (Prune), ... 'PruneCriterion', 'mse', ... 'QEToler', QEToler, ... 'NSurrogate', 0, ... 'MaxCat', MaxNumCat, ... 'AlgCat', 'auto', ... 'PredictorSelection', 'allsplits', ... 'Method', 'Tree', ... 'Type', 'regression'); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionTree} {@var{yFit} =} predict (@var{obj}, @var{XC}) ## @deftypefnx {RegressionTree} {[@var{yFit}, @var{node}] =} predict (@dots{}) ## ## Predict the response with a trained @code{RegressionTree} object. ## ## @code{@var{yFit} = predict (@var{obj}, @var{XC})} sends each row of ## @var{XC} down the tree and returns the mean response of the node it ## comes to rest at, after @code{ResponseTransform}. @var{XC} must have ## as many columns as the predictor data the model was fitted on. ## ## @var{XC} may also be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: one ## the model was not fitted on is passed over, one it needs and cannot ## find is named, and a value holding a level is coded as that level was ## coded at fitting. ## ## @code{[@var{yFit}, @var{node}] = predict (@dots{})} also returns the ## number of the node each row landed in. ## ## A row missing the predictor a node cuts on is stopped at that node and ## takes its answer, rather than being sent down a branch on evidence the ## row does not carry. ## ## @seealso{RegressionTree, fitrtree} ## @end deftypefn function [yFit, node] = predict (this, XC) ## Input validation if (nargin < 2) error ("RegressionTree.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on XC = tableColumns (this, 'RegressionTree.predict', XC); if (isempty (XC)) error ("RegressionTree.predict: XC is empty."); endif if (! (isnumeric (XC) && isreal (XC) && ismatrix (XC))) error (strcat ("RegressionTree.predict: XC must be a real numeric", ... " matrix.")); endif if (numel (this.PredictorNames) != columns (XC)) error (strcat ("RegressionTree.predict: XC must have the same", ... " number of predictors as the trained model.")); endif [yFit, node] = treepredict (XC, this.Children, ... this.CutPredictorIndex, this.CutPoint, ... this.NodeMean, ... this.CutCategories); yFit = this.RTfun (yFit); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionTree} {@var{obj2} =} prune (@var{obj}) ## @deftypefnx {RegressionTree} {@var{obj2} =} prune (@var{obj}, @qcode{'Level'}, @var{L}) ## @deftypefnx {RegressionTree} {@var{obj2} =} prune (@var{obj}, @qcode{'Alpha'}, @var{A}) ## @deftypefnx {RegressionTree} {@var{obj2} =} prune (@var{obj}, @qcode{'Nodes'}, @var{N}) ## ## Take a subtree out of the pruning sequence. ## ## @code{@var{obj2} = prune (@var{obj})} returns the tree unchanged. ## ## @code{@var{obj2} = prune (@var{obj}, @qcode{'Level'}, @var{L})} turns ## every branch node whose @code{PruneList} level is between one and ## @var{L} into a leaf and discards everything below it. Level zero is ## the tree itself and the largest level is the root alone. A level ## above the largest prunes to the root and warns. ## ## @code{@var{obj2} = prune (@var{obj}, @qcode{'Alpha'}, @var{A})} prunes ## to the smallest subtree that is optimal for the cost complexity ## parameter @var{A}, which is the largest level whose @code{PruneAlpha} ## does not exceed it. ## ## @code{@var{obj2} = prune (@var{obj}, @qcode{'Nodes'}, @var{N})} turns ## the branch nodes named in @var{N} into leaves, along with everything ## below them, and leaves the rest of the tree alone. ## ## Pruning renumbers the nodes, so the properties of the returned tree ## are those of a tree of that shape and not a subset of the original's. ## ## @seealso{RegressionTree, fitrtree, RegressionTree.PruneList, ## RegressionTree.PruneAlpha} ## @end deftypefn function this = prune (this, varargin) ## Input validation if (mod (numel (varargin), 2) != 0) error (strcat ("RegressionTree.prune: name-value arguments must", ... " be in pairs.")); endif if (numel (varargin) > 2) error (strcat ("RegressionTree.prune: specify only one of the", ... " optional name-value paired arguments.")); endif nodes = []; if (numel (varargin) == 2) Value = varargin{2}; switch (tolower (varargin{1})) case 'level' if (! (isnumeric (Value) && isscalar (Value) && isreal (Value) && Value >= 0 && Value == fix (Value))) error (strcat ("RegressionTree.prune: 'Level' must be a", ... " nonnegative integer.")); endif nodes = this.nodesAtLevel (Value, 'prune'); case 'alpha' if (! (isnumeric (Value) && isscalar (Value) && isreal (Value) && Value >= 0)) error (strcat ("RegressionTree.prune: 'Alpha' must be a", ... " nonnegative scalar.")); endif if (isempty (this.PruneAlpha)) error (strcat ("RegressionTree.prune: the tree carries no", ... " pruning sequence.")); endif ## The largest level whose parameter the value reaches L = sum (this.PruneAlpha <= Value) - 1; nodes = this.nodesAtLevel (max (L, 0), 'prune'); case 'nodes' if (! (isnumeric (Value) && isreal (Value) && isvector (Value) && all (Value >= 1) && all (Value <= this.NumNodes) && all (Value == fix (Value)))) error (strcat ("RegressionTree.prune: 'Nodes' must hold", ... " indices of nodes of the tree.")); endif nodes = Value(:); otherwise error (strcat ("RegressionTree.prune: invalid parameter name", ... " in optional pair arguments.")); endswitch endif if (isempty (nodes)) return; endif this = collapseNodes (this, nodes); ## The sequence of the tree that is left, not a slice of the one the ## tree came with: pruning renumbers the nodes and a subtree of an ## optimal sequence is the optimal sequence of that subtree. this = pruneSequence (this); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionTree} {@var{CMdl} =} compact (@var{obj}) ## ## Drop the training data from a trained model. ## ## @code{@var{CMdl} = compact (@var{obj})} returns a ## @code{CompactRegressionTree} object carrying the tree and everything ## @code{predict} needs, but not the observations the model was fitted ## on. It answers new data identically and is far smaller to keep or to ## ship. ## ## @seealso{CompactRegressionTree, RegressionTree} ## @end deftypefn function CMdl = compact (this) CMdl = CompactRegressionTree (this); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionTree} {@var{CVMdl} =} crossval (@var{obj}) ## @deftypefnx {RegressionTree} {@var{CVMdl} =} crossval (@dots{}, @var{name}, @var{value}) ## ## Cross-validate a trained decision tree. ## ## @code{@var{CVMdl} = crossval (@var{obj})} partitions the training data ## into ten folds, or into as many folds as there are observations when ## there are fewer than ten, grows a tree on the training part of each ## and returns them as a @code{RegressionPartitionedModel}. ## ## @code{@var{CVMdl} = crossval (@dots{}, @var{name}, @var{value})} takes ## one of the following, and one only. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'KFold'} @tab An integer greater than 1, the number of ## folds. ## ## @item @qcode{'Holdout'} @tab A value between 0 and 1, the fraction of ## the data held out for testing, which gives a single fold. ## ## @item @qcode{'Leaveout'} @tab @qcode{'on'} or @qcode{'off'}, one fold ## per observation. ## ## @item @qcode{'CVPartition'} @tab A @code{cvpartition} object. ## ## @end multitable ## ## Every fold is grown with the growth parameters the parent was grown ## with and a slice of its observation weights. ## ## @seealso{RegressionPartitionedModel, RegressionTree, cvpartition} ## @end deftypefn function CVMdl = crossval (this, varargin) ## Input validation if (numel (varargin) == 1) error (strcat ("RegressionTree.crossval: Name-Value arguments", ... " must be in pairs.")); elseif (numel (varargin) > 2) error (strcat ("RegressionTree.crossval: specify only one of the", ... " optional Name-Value paired arguments.")); endif if (this.NumObservations < 10) numFolds = this.NumObservations; else numFolds = 10; endif Holdout = []; Leaveout = 'off'; CVPartition = []; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'kfold' numFolds = varargin{2}; if (! (isnumeric (numFolds) && isscalar (numFolds) && (numFolds == fix (numFolds)) && numFolds > 1)) error (strcat ("RegressionTree.crossval: 'KFold' must be an", ... " integer value greater than 1.")); endif case 'holdout' Holdout = varargin{2}; if (! (isnumeric (Holdout) && isscalar (Holdout) && Holdout > 0 && Holdout < 1)) error (strcat ("RegressionTree.crossval: 'Holdout' must be", ... " a numeric value between 0 and 1.")); endif case 'leaveout' Leaveout = varargin{2}; if (! (ischar (Leaveout) && any (strcmpi (Leaveout, {'on', 'off'})))) error (strcat ("RegressionTree.crossval: 'Leaveout' must be", ... " either 'on' or 'off'.")); endif case 'cvpartition' CVPartition = varargin{2}; if (! (isa (CVPartition, 'cvpartition'))) error (strcat ("RegressionTree.crossval: 'CVPartition' must", ... " be a 'cvpartition' object.")); endif otherwise error (strcat ("RegressionTree.crossval: invalid parameter", ... " name in optional paired arguments.")); endswitch varargin(1:2) = []; endwhile ## The partition covers the observations actually trained on: a row ## dropped for a missing response is not one the folds can use. A ## regression has no classes to stratify over, so the partition is ## drawn over their number. if (! isempty (CVPartition)) partition = CVPartition; elseif (! isempty (Holdout)) partition = cvpartition (this.NumObservations, 'Holdout', Holdout); elseif (strcmpi (Leaveout, 'on')) partition = cvpartition (this.NumObservations, 'LeaveOut'); else partition = cvpartition (this.NumObservations, 'KFold', numFolds); endif CVMdl = RegressionPartitionedModel (this, partition); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionTree} {@var{E} =} cvloss (@var{obj}) ## @deftypefnx {RegressionTree} {[@var{E}, @var{SE}, @var{Nleaf}, @var{BestLevel}] =} cvloss (@var{obj}) ## @deftypefnx {RegressionTree} {[@dots{}] =} cvloss (@dots{}, @var{name}, @var{value}) ## ## Cross-validated loss of a tree and of its subtrees. ## ## @code{@var{E} = cvloss (@var{obj})} partitions the training data into ## ten folds, grows a tree on the training part of each, and returns the ## mean squared error of the held-out part. ## ## @code{[@var{E}, @var{SE}, @var{Nleaf}, @var{BestLevel}] = cvloss ## (@dots{})} also returns @var{SE}, the standard error of @var{E} over ## the folds, @var{Nleaf}, the number of leaves each subtree holds, and ## @var{BestLevel}, the pruning level chosen by @qcode{'TreeSize'}. Each ## has one element per subtree asked for. ## ## @code{[@dots{}] = cvloss (@dots{}, @var{name}, @var{value})} takes the ## options below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'SubTrees'} @tab A vector of pruning levels in ascending ## order, or @qcode{'all'} for every level of the sequence. The default ## is 0, the unpruned tree. ## ## @item @qcode{'TreeSize'} @tab @qcode{'se'} (default), the smallest ## subtree whose loss is within one standard error of the smallest loss, ## or @qcode{'min'}, the smallest subtree of least loss. ## ## @item @qcode{'KFold'} @tab An integer greater than 1, the number of ## folds. The default is 10. A value above the number of observations ## is reduced to it. ## ## @end multitable ## ## A fold's tree is pruned to the level its own sequence gives for the ## geometric mean of the parent's two neighbouring complexity parameters, ## which is the classical way a fold is matched to a subtree of the whole ## tree. The last level takes every fold's tree back to its root. The ## partition is drawn over the observations rather than over a response ## there is nothing to stratify, and the loss is weighed by the model's ## own weights. ## ## @strong{The standard error is not MATLAB's.} This is the standard ## error of the loss over the folds, which is what the name means. Its ## value is not MATLAB's, whose formula is not recoverable from what it ## reports; @var{E}, @var{Nleaf} and @var{BestLevel} are measured and ## match. ## ## @seealso{RegressionTree, RegressionTree.prune, RegressionTree.crossval, ## RegressionTree.loss} ## @end deftypefn function [E, SE, Nleaf, BestLevel] = cvloss (this, varargin) ## Input validation if (mod (numel (varargin), 2) != 0) error (strcat ("RegressionTree.cvloss: name-value arguments must", ... " be in pairs.")); endif if (isempty (this.PruneAlpha)) error (strcat ("RegressionTree.cvloss: the tree carries no pruning", ... " sequence; fit it with 'Prune' or 'MergeLeaves'", ... " on.")); endif maxLevel = numel (this.PruneAlpha) - 1; ## Parse optional paired arguments; 'SubTrees' defaults to the tree ## as fitted, level 0 optNames = {'SubTrees', 'TreeSize', 'KFold'}; dfValues = {0, 'se', 10}; [SubTrees, TreeSize, KFold, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (ischar (SubTrees) && strcmpi (SubTrees, 'all')) SubTrees = 0:maxLevel; elseif (isnumeric (SubTrees) && isreal (SubTrees) && isvector (SubTrees) && ! isempty (SubTrees) && all (SubTrees >= 0) && all (SubTrees == fix (SubTrees)) && all (diff (SubTrees(:)') > 0)) SubTrees = SubTrees(:)'; else error (strcat ("RegressionTree.cvloss: 'SubTrees' must be 'all' or", ... " a vector of nonnegative integers in ascending", ... " order.")); endif if (! (ischar (TreeSize) && any (strcmpi (TreeSize, {'se', 'min'})))) error (strcat ("RegressionTree.cvloss: 'TreeSize' must be either", ... " 'se' or 'min'.")); endif TreeSize = tolower (TreeSize); if (! (isnumeric (KFold) && isscalar (KFold) && isreal (KFold) && KFold == fix (KFold) && KFold > 1)) error (strcat ("RegressionTree.cvloss: 'KFold' must be an integer", ... " value greater than 1.")); endif if (! isempty (args)) error ("RegressionTree.cvloss: invalid optional paired argument."); endif if (any (SubTrees > maxLevel)) error (strcat ("RegressionTree.cvloss: 'SubTrees' must not exceed", ... " the largest pruning level, %d."), maxLevel); endif if (KFold > this.NumObservations) warning (strcat ("RegressionTree.cvloss: 'KFold' is greater than", ... " the number of observations and is reduced to", ... " %d."), this.NumObservations); KFold = this.NumObservations; endif ## The complexity parameter each subtree is matched to in a fold: the ## geometric mean of the two the parent's own sequence brackets it ## with, and infinity for the last, which takes a fold back to its ## root. Measured on R2024a, which the classification tree measured ## first and which holds here too: matching a fold by level index or ## by the parent's parameter itself both give different answers. alpha = this.PruneAlpha(:)'; ## Formed outside the brackets: inside them the space before the paren ## would split the call off into an element of its own. geo = sqrt (alpha(1:end-1) .* alpha(2:end)); geo(end+1) = Inf; nsub = numel (SubTrees); n = this.NumObservations; partition = cvpartition (n, 'KFold', KFold); args = treeFoldArgs (this); ## The squared error of every observation, from the fold that did not ## train on it L = zeros (n, nsub); fold = zeros (n, 1); for k = 1:KFold tr = training (partition, k); te = test (partition, k); fold(te) = k; ft = RegressionTree (this.X(tr,:), this.Y(tr), args{:}, ... 'Weights', this.RawWeights(tr)); fa = ft.PruneAlpha(:)'; for j = 1:nsub lv = sum (fa <= geo(SubTrees(j) + 1)) - 1; ## The nodes are collapsed without re-estimating the fold's own ## sequence, which prune would do and which nothing here reads. sub = collapseNodes (ft, nodesAtLevel (ft, max (lv, 0), 'cvloss')); ## The fold carries no transform, so the parent's is applied here ## rather than twice over. yf = this.RTfun (predict (sub, this.X(te,:))); L(te,j) = (this.Y(te) - yf) .^ 2; endfor endfor ## The loss is weighed by the model's own weights, which sum to one. W = double (this.W(:)); E = W' * L; ## The standard error over the folds, each fold's loss being its ## observations' share of the whole. foldloss = zeros (KFold, nsub); for k = 1:KFold idx = fold == k; sw = sum (W(idx)); if (sw > 0) foldloss(k,:) = (W(idx)' * L(idx,:)) / sw; endif endfor SE = std (foldloss, 0, 1) / sqrt (KFold); ## The leaves each subtree of the whole tree holds Nleaf = zeros (1, nsub); for j = 1:nsub Nleaf(j) = sum (! prune (this, 'Level', SubTrees(j)).IsBranchNode); endfor ## The smallest subtree the rule allows, which is the largest level if (strcmp (TreeSize, 'min')) best = find (E == min (E), 1, 'last'); else [~, i] = min (E); best = find (E <= E(i) + SE(i), 1, 'last'); endif BestLevel = SubTrees(best); E = E(:); SE = SE(:); Nleaf = Nleaf(:); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionTree} {@var{imp} =} predictorImportance (@var{obj}) ## ## Estimate the importance of each predictor. ## ## @code{@var{imp} = predictorImportance (@var{obj})} returns a row ## vector with one element per predictor, holding the total drop in risk ## the splits on that predictor bring about, divided by the number of ## branch nodes. A predictor the tree never splits on scores zero. ## ## The drop at a branch node is its @code{NodeRisk} less the risk of its ## two children and less what it holds back, so a predictor that is ## chosen often, high up, and on nodes it separates well, scores highest. ## The numbers are comparable between predictors of one tree and not ## between trees. ## ## @seealso{RegressionTree, fitrtree, RegressionTree.NodeRisk} ## @end deftypefn function imp = predictorImportance (this) imp = zeros (1, numel (this.PredictorNames)); branch = find (this.Children(:,1) > 0); if (isempty (branch)) return; endif for ii = 1:numel (branch) b = branch(ii); kids = this.Children(b,:); ## The rows that stop at the node are in neither child and are ## answered the same whether it is split or not, so they are no part ## of what the split buys. drop = this.NodeRisk(b) - this.NodeRisk(kids(1)) ... - this.NodeRisk(kids(2)) - this.HeldRisk(b); v = this.CutPredictorIndex(b); imp(v) = imp(v) + drop; endfor imp = imp / numel (branch); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionTree} {@var{r} =} nodeVariableRange (@var{obj}, @var{node}) ## ## Range of each predictor at a node. ## ## @code{@var{r} = nodeVariableRange (@var{obj}, @var{node})} returns a ## structure with one field per predictor the path from the root to ## @var{node} cuts on, holding the two-element range of values that reach ## the node, or for a categorical predictor the levels that reach it. A ## predictor the path never cuts on is unconstrained and is left out, so ## the root gives a structure with no fields. ## ## @seealso{RegressionTree, fitrtree} ## @end deftypefn function r = nodeVariableRange (this, node) ## Input validation if (nargin < 2) error (strcat ("RegressionTree.nodeVariableRange: too few input", ... " arguments.")); endif if (! (isnumeric (node) && isscalar (node) && isreal (node) && node >= 1 && node <= this.NumNodes && node == fix (node))) error (strcat ("RegressionTree.nodeVariableRange: NODE must be a", ... " positive integer no greater than the number of", ... " nodes in the tree.")); endif p = numel (this.PredictorNames); lo = -Inf (1, p); hi = Inf (1, p); touched = false (1, p); ## Walk up to the root, narrowing the range of whichever predictor ## each step cut on. Going up rather than down finds the path without ## a search, a node having exactly one parent. ## A categorical cut narrows its predictor to a set of levels instead, ## and the first such cut met on the way up is the narrowest. levels = cell (1, p); child = node; up = this.Parent(child); while (up > 0) v = this.CutPredictorIndex(up); side = 1 + (this.Children(up,1) != child); cats = this.CutCategories{up,side}; if (! isempty (cats)) if (! touched(v)) levels{v} = cats; endif elseif (side == 1) hi(v) = min (hi(v), this.CutPoint(up)); else lo(v) = max (lo(v), this.CutPoint(up)); endif touched(v) = true; child = up; up = this.Parent(child); endwhile r = struct (); for v = find (touched) if (isempty (levels{v})) r.(this.PredictorNames{v}) = [lo(v), hi(v)]; else r.(this.PredictorNames{v}) = levels{v}; endif endfor endfunction ## -*- texinfo -*- ## @deftypefn {RegressionTree} {} view (@var{obj}) ## ## Print the tree as text. ## ## @code{view (@var{obj})} prints one line per node: a branch node names ## the predictor it cuts on, the cut point, and the node each side leads ## to, and a leaf names the response it fits. A branch node's line ends ## with the response it would fit itself, which is the answer an ## observation missing that predictor gets. ## ## @seealso{RegressionTree, fitrtree} ## @end deftypefn function view (this) ## The node numbers are right aligned on the widest of them, which is ## how MATLAB lays the listing out. w = numel (sprintf ("%d", this.NumNodes)); fprintf ("Decision tree for regression\n"); for ii = 1:this.NumNodes m = sprintf ("%g", this.NodeMean(ii)); if (this.Children(ii,1) == 0) fprintf ("%*d fit = %s\n", w, ii, m); else v = this.PredictorNames{this.CutPredictorIndex(ii)}; cc = this.CutCategories(ii,:); if (! isempty (cc{1})) fprintf ("%*d if %s then node %d elseif %s then node", w, ii, ... treeLevelText (v, cc{1}), this.Children(ii,1), ... treeLevelText (v, cc{2})); else c = sprintf ("%g", this.CutPoint(ii)); fprintf ("%*d if %s<%s then node %d elseif %s>=%s then node", ... w, ii, v, c, this.Children(ii,1), v, c); endif fprintf (" %d else %s\n", this.Children(ii,2), m); endif endfor endfunction ## -*- texinfo -*- ## @deftypefn {RegressionTree} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {RegressionTree} {@var{L} =} loss (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {RegressionTree} {@var{L} =} loss (@var{obj}, @var{Tbl}) ## @deftypefnx {RegressionTree} {@var{L} =} loss (@dots{}, @var{name}, @var{value}) ## ## Regression loss on new data. ## ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} returns the ## weighted mean squared error of the response the model predicts for ## @var{X} against the observed response @var{Y}. A row whose response ## is missing is dropped, as it is when fitting. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{loss (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{loss (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @code{@var{L} = loss (@dots{}, @var{name}, @var{value})} takes the ## following options. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'LossFun'} @tab @qcode{'mse'}, the default, or a function ## handle taking the true response, the predicted response and the ## weights, and returning a numeric scalar. ## ## @item @qcode{'Weights'} @tab A numeric vector of observation weights, ## one per row of @var{X}, normalized to sum to one before it is applied. ## ## @end multitable ## ## @seealso{RegressionTree, fitrtree, RegressionTree.predict} ## @end deftypefn function L = loss (this, X, Y, varargin) ## Input validation if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("RegressionTree.loss: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'loss', X, Y, varargin, ... nargin > 2); if (mod (numel (varargin), 2) != 0) error (strcat ("RegressionTree.loss: name-value arguments must be", ... " in pairs.")); endif if (! (isnumeric (X) && isreal (X) && ismatrix (X))) error ("RegressionTree.loss: X must be a real numeric matrix."); endif if (! (isnumeric (Y) && isreal (Y) && isvector (Y))) error ("RegressionTree.loss: Y must be a real numeric vector."); endif if (rows (X) != numel (Y)) error (strcat ("RegressionTree.loss: number of rows in X and Y", ... " must be equal.")); endif ## Parse optional paired arguments; an empty 'Weights' stands for ## uniform weights optNames = {'LossFun', 'Weights'}; dfValues = {'mse', []}; [LossFun, Weights, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (! (is_function_handle (LossFun) || (ischar (LossFun) && isrow (LossFun)))) error (strcat ("RegressionTree.loss: 'LossFun' must be a character", ... " vector or a function handle.")); endif if (ischar (LossFun) && ! strcmpi (LossFun, 'mse')) error ("RegressionTree.loss: unsupported 'LossFun' value."); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("RegressionTree.loss: %s", errmsg); endif if (! isempty (Weights) && ! (isnumeric (Weights) && isvector (Weights) && isreal (Weights))) error ("RegressionTree.loss: 'Weights' must be a real numeric vector."); endif if (! isempty (Weights) && numel (Weights) != rows (X)) error (strcat ("RegressionTree.loss: 'Weights' must have one", ... " element per observation.")); endif if (! isempty (args)) error ("RegressionTree.loss: invalid optional paired argument."); endif if (isempty (Weights)) W = ones (rows (X), 1); else W = double (Weights(:)); endif ## An observation with no response is one the loss cannot be measured ## on, so it is dropped and the weights are normalized over what is ## left, which is what the fit itself does with such a row. Measured ## on R2024a, where the loss of a carsmall tree over the whole hundred ## rows is the loss over the ninety-four with a response. Y = double (Y(:)); keep = ! isnan (Y); Y = Y(keep); X = X(keep, :); W = W(keep); if (! (sum (W) > 0)) error (strcat ("RegressionTree.loss: 'Weights' must not be zero", ... " for every observation with a response.")); endif ## Weights are normalized to sum to one, as MATLAB does, so a loss is ## a weighted average rather than a weighted sum. W = W / sum (W); yFit = predict (this, X); if (is_function_handle (LossFun)) L = LossFun (Y, yFit, W); if (! (isnumeric (L) && isscalar (L))) error (strcat ("RegressionTree.loss: 'LossFun' must return a", ... " numeric scalar.")); endif else L = sum (W .* (Y - yFit) .^ 2); endif endfunction ## -*- texinfo -*- ## @deftypefn {RegressionTree} {@var{yFit} =} resubPredict (@var{obj}) ## ## Predict the training response with the model fitted to it. ## ## @code{@var{yFit} = resubPredict (@var{obj})} is ## @code{predict (@var{obj}, @var{obj}.X)}. ## ## @seealso{RegressionTree, RegressionTree.predict} ## @end deftypefn function yFit = resubPredict (this) yFit = predict (this, this.X); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionTree} {@var{L} =} resubLoss (@var{obj}) ## @deftypefnx {RegressionTree} {@var{L} =} resubLoss (@dots{}, @var{name}, @var{value}) ## ## Regression loss on the training data. ## ## @code{@var{L} = resubLoss (@var{obj})} is @code{loss} over the ## training data, weighed as the fit weighed it, and takes the same ## @qcode{'LossFun'} option. Giving @qcode{'Weights'} weighs the ## training data some other way instead. ## ## @seealso{RegressionTree, RegressionTree.loss} ## @end deftypefn function L = resubLoss (this, varargin) ## The training data is weighed as the fit weighed it. Measured on ## R2024a: a weighted fit reports a resubstitution loss over its own ## weights, 6.51484752783409 on a carsmall tree weighted 1 to 100, ## where uniform weights give 7.44189886464653. if (! any (strcmpi (varargin(1:2:end), 'weights'))) varargin = [varargin, {'Weights', this.RawWeights}]; endif L = loss (this, this.X, this.Y, varargin{:}); endfunction ## -*- texinfo -*- ## @deftypefn {RegressionTree} {} savemodel (@var{obj}, @var{filename}) ## ## Save a RegressionTree model to a file. ## ## @code{savemodel (@var{obj}, @var{filename})} saves each property of a ## RegressionTree object into an Octave binary file, the name of which is ## specified in @var{filename}, along with an extra variable, which ## defines the type of regression object these variables constitute. Use ## @code{loadmodel} in order to load a regression object into Octave's ## workspace. ## ## @seealso{loadmodel, fitrtree, RegressionTree} ## @end deftypefn function savemodel (this, fname) ## Input validation if (nargin < 2) error ("RegressionTree.savemodel: too few input arguments."); endif if (! (ischar (fname) && isrow (fname) && ! isempty (fname))) error (strcat ("RegressionTree.savemodel: FNAME must be a", ... " character vector.")); endif ## Generate variable for class name classdef_name = 'RegressionTree'; ## Create variables from model properties X = this.X; Y = this.Y; RowsUsed = this.RowsUsed; W = this.W; RawWeights = this.RawWeights; NumObservations = this.NumObservations; PredictorNames = this.PredictorNames; ResponseName = this.ResponseName; CategoricalPredictors = this.CategoricalPredictors; ExpandedPredictorNames = this.ExpandedPredictorNames; BinEdges = this.BinEdges; ModelParameters = this.ModelParameters; NumNodes = this.NumNodes; Children = this.Children; Parent = this.Parent; CutPredictorIndex = this.CutPredictorIndex; CutPoint = this.CutPoint; CutCategories = this.CutCategories; NodeSize = this.NodeSize; NodeWeight = this.NodeWeight; NodeMean = this.NodeMean; NodeError = this.NodeError; PruneList = this.PruneList; PruneAlpha = this.PruneAlpha; ResponseTransform = this.ResponseTransform; RTfun = this.RTfun; HyperparameterOptimizationResults = ... this.HyperparameterOptimizationResults; ## The cut descriptions, the two node probabilities and the held risk ## are not saved: every one of them is re-derived from the node table ## above, so writing them out would only make a stale copy possible. save ('-binary', fname, 'classdef_name', 'X', 'Y', 'RowsUsed', 'W', ... 'RawWeights', 'NumObservations', 'PredictorNames', ... 'ResponseName', 'CategoricalPredictors', ... 'ExpandedPredictorNames', 'BinEdges', 'ModelParameters', ... 'NumNodes', 'Children', 'Parent', 'CutPredictorIndex', ... 'CutPoint', 'CutCategories', 'NodeSize', 'NodeWeight', ... 'NodeMean', 'NodeError', ... 'PruneList', 'PruneAlpha', 'ResponseTransform', 'RTfun', ... 'HyperparameterOptimizationResults'); endfunction endmethods methods (Static, Hidden) function mdl = load_model (filename, data) ## The smallest fit the class accepts, filled property by property ## below. Nothing of the stub survives the copy. mdl = RegressionTree ([1; 2], [1; 2]); ## Copy the saved data into the object. Iterate over what was saved ## rather than over fieldnames (mdl): a hidden property such as RTfun ## is written out by savemodel but is not reported by fieldnames, so ## comparing the two sets could never match. Assignment is legal here ## because this is a method of the class itself. names = fieldnames (data); order = {'ResponseTransform'}; late = ismember (names, order); tail = order(ismember (order, names)); names = [names(! late); tail(:)]; for ii = 1:numel (names) try mdl.(names{ii}) = data.(names{ii}); catch msg = 'RegressionTree.load_model: invalid model in ''%s''.'; error (msg, filename); end_try_catch endfor ## The cut descriptions and the derived node quantities were not saved mdl = fillCuts (mdl); mdl = deriveNodes (mdl); mdl = deriveHeld (mdl); endfunction endmethods methods (Access = private) ## The two node quantities that are derived from the weights the engine ## accumulated rather than reported by it. function this = deriveNodes (this) if (isempty (this.NodeWeight)) return; endif if (this.NodeWeight(1) > 0) this.NodeProbability = this.NodeWeight / this.NodeWeight(1); else this.NodeProbability = zeros (size (this.NodeWeight)); endif ## Measured on R2024a, where NodeRisk and NodeProbability times ## NodeError agree to every digit on every node of a carsmall tree. this.NodeRisk = this.NodeProbability .* this.NodeError; endfunction ## The descriptions of the cuts, derived from the node table. function this = fillCuts (this) S = treeCutInfo (this.CutPredictorIndex, this.PredictorNames, ... this.CutCategories); for [val, name] = S this.(name) = val; endfor endfunction ## The risk each branch node carries on account of the observations that ## stop there, missing the predictor it cuts on. Those observations are ## in neither child, and MATLAB charges them the node's own mean squared ## error rather than their own: measured on R2024a, where one row of ## carsmall missing its horsepower gives a node of 58 observations two ## children of 40 and 17, and that node's alpha comes to ## 5.99325416896717, which is its risk less its children's less one ## observation's weight times its own NodeError. function this = deriveHeld (this) this.HeldRisk = zeros (this.NumNodes, 1); br = find (this.Children(:,1) > 0); if (isempty (br)) return; endif kids = this.Children(br,:); wheld = this.NodeProbability(br) - this.NodeProbability(kids(:,1)) ... - this.NodeProbability(kids(:,2)); this.HeldRisk(br) = wheld .* this.NodeError(br); endfunction ## The branch nodes a pruning level turns into leaves. function nodes = nodesAtLevel (this, L, caller) if (isempty (this.PruneList)) error (strcat ("RegressionTree.%s: the tree carries no pruning", ... " sequence."), caller); endif maxL = max (this.PruneList); if (L > maxL) warning (strcat ("RegressionTree.%s: pruning level %d is greater", ... " than the largest level %d; the tree will be", ... " pruned to its root."), caller, L, maxL); L = maxL; endif nodes = find (this.PruneList >= 1 & this.PruneList <= L); endfunction ## Turn the named branch nodes into leaves, discard everything below ## them, and renumber what is left. function this = collapseNodes (this, nodes) if (isempty (nodes(this.Children(nodes,1) > 0))) return; endif S = treeCollapse (this.Children, this.Parent, ... this.CutPredictorIndex, this.CutPoint, nodes, ... this.CutCategories); this.NumNodes = numel (S.keep); this.Children = S.Children; this.Parent = S.Parent; this.CutPredictorIndex = S.CutPredictorIndex; this.CutPoint = S.CutPoint; this.CutCategories = S.CutCategories; this.NodeSize = this.NodeSize(S.keep); this.NodeWeight = this.NodeWeight(S.keep); this.NodeMean = this.NodeMean(S.keep); this.NodeError = this.NodeError(S.keep); this.PruneList = zeros (this.NumNodes, 1); this.PruneAlpha = []; this = fillCuts (this); this = deriveNodes (this); ## A node that became a leaf now holds back nothing, so this is ## re-derived rather than subset. this = deriveHeld (this); endfunction ## The cost complexity pruning sequence, on the squared error each node ## would carry as a leaf. function this = pruneSequence (this) risk = this.NodeProbability .* this.NodeError; [this.PruneList, this.PruneAlpha] = ... __treeprune__ (this.Children, this.Parent, risk, this.HeldRisk); endfunction endmethods endclassdef ## Tests %!test # MATLAB parity: the surface a default fit reports %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG); %! assert_equal (class (Mdl), 'RegressionTree'); %! assert_equal (numel (properties (Mdl)), 36); %! assert_equal (Mdl.NumObservations, 94); %! assert_equal (Mdl.ResponseName, 'Y'); %! assert_equal (Mdl.PredictorNames, {'x1', 'x2', 'x3'}); %! assert_equal (Mdl.ExpandedPredictorNames, {'x1', 'x2', 'x3'}); %! assert_equal (Mdl.ResponseTransform, 'none'); %! assert_equal (Mdl.CategoricalPredictors, []); %! assert_equal (sum (Mdl.W), 1, 1e-14); %! assert_equal (Mdl.W(1), 1 / 94, 1e-15); %!test # MATLAB parity: the parameters a default fit reports %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! MP = RegressionTree (X, MPG).ModelParameters; %! assert_equal (MP.SplitCriterion, 'mse'); %! assert_equal (MP.PruneCriterion, 'mse'); %! assert_equal ({MP.MinParent, MP.MinLeaf, MP.MaxSplits}, {10, 1, 93}); %! assert_equal ({MP.MergeLeaves, MP.Prune}, {'on', 'on'}); %! assert_equal (MP.QEToler, 1e-6); %! assert_equal ({MP.NSurrogate, MP.MaxCat, MP.AlgCat}, {0, 10, 'auto'}); %! assert_equal ({MP.Method, MP.Type}, {'Tree', 'regression'}); %!test # MATLAB parity: what a tree with no categories and no surrogates holds %! load carsmall %! Mdl = RegressionTree ([Weight, Cylinders, Horsepower], MPG); %! assert_equal (size (Mdl.CutCategories), [37, 2]); %! assert_equal (size (Mdl.CategoricalSplit), [0, 0]); %! assert_equal (size (Mdl.SurrogateCutPredictor), [0, 1]); %! assert_equal (size (Mdl.SurrogateCutPoint), [0, 0]); %! assert_equal (Mdl.BinEdges, {}); %! assert_equal (Mdl.HyperparameterOptimizationResults, []); %!test # A missing response drops its row, a missing predictor does not %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG); %! assert_equal (Mdl.NumObservations, 94); %! assert_equal (numel (Mdl.RowsUsed), 100); %! assert_equal (sum (Mdl.RowsUsed), 94); %! assert_equal (Mdl.NodeSize(1), 94); %! ## the row with no horsepower is kept, and node 2 cuts on horsepower %! assert_equal (Mdl.NodeSize(2) - sum (Mdl.NodeSize(Mdl.Children(2,:))), 1); %!test # MATLAB parity: a row missing the split predictor stops at that node %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG); %! [yFit, node] = predict (Mdl, [NaN, NaN, NaN]); %! assert_equal (node, 1); %! assert_equal (yFit, Mdl.NodeMean(1), 1e-15); %! [yFit, node] = predict (Mdl, [2000, 4, NaN]); %! assert_equal (node, 2); %! assert_equal (yFit, Mdl.NodeMean(2), 1e-15); %!test # MATLAB parity: prune takes a subtree out of the sequence %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG, 'MinLeafSize', 15); %! sub = prune (Mdl, 'Level', 1); %! assert_equal (sub.NumNodes, 7); %! assert_equal (sub.NodeSize', [94, 58, 36, 40, 17, 18, 22]); %! assert_equal (sub.PruneList', [3, 2, 0, 1, 0, 0, 0]); %! assert_equal (sub.PruneAlpha', [0, 1.95238622931442, ... %! 5.99325416896717, 41.4954735525515], 1e-11); %!test # MATLAB parity: a level of two, and level zero changing nothing %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG, 'MinLeafSize', 15); %! assert_equal (prune (Mdl, 'Level', 2).NumNodes, 5); %! assert_equal (prune (Mdl, 'Level', 0).NumNodes, 9); %! assert_equal (prune (Mdl).NumNodes, 9); %! assert_equal (prune (Mdl, 'Alpha', 100).NumNodes, 1); %! assert_equal (prune (Mdl, 'Alpha', 0).NumNodes, 9); %! assert_equal (prune (Mdl, 'Nodes', 2).NumNodes, 5); %!warning %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! sub = prune (RegressionTree (X, MPG, 'MinLeafSize', 15), 'Level', 9); %!test # MATLAB parity: the range of each predictor on the path to a node %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG); %! assert_equal (fieldnames (nodeVariableRange (Mdl, 1)), cell (0, 1)); %! r = nodeVariableRange (Mdl, 4); %! assert_equal (r.x1, [-Inf, 3085.5], 1e-12); %! assert_equal (r.x3, [-Inf, 89], 1e-12); %!test # MATLAB parity: the text form of the tree %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG, 'MinLeafSize', 15); %! lines = strsplit (strtrim (evalc ('view (Mdl)')), "\n"); %! assert_equal (numel (lines), 10); %! assert_equal (lines{1}, 'Decision tree for regression'); %! assert_equal (lines{2}, ... %! '1 if x1<3085.5 then node 2 elseif x1>=3085.5 then node 3 else 23.7181'); %! assert_equal (lines{6}, '5 fit = 24.0882'); %!test # The node numbers are right aligned on the widest of them %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! lines = strsplit (strtrim (evalc ('view (RegressionTree (X, MPG))')), "\n"); %! assert_equal (numel (lines), 38); %! assert_equal (lines{2}(1:2), ' 1'); %! assert_equal (lines{11}(1:2), '10'); %!test # The loss takes a function handle over the true, fitted and weights %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG); %! r = Mdl.Y - resubPredict (Mdl); %! mae = @(y, yf, w) sum (w .* abs (y - yf)); %! assert_equal (loss (Mdl, Mdl.X, Mdl.Y, 'LossFun', mae), ... %! mean (abs (r)), 1e-12); %! assert_equal (loss (Mdl, Mdl.X, Mdl.Y, 'LossFun', 'mse'), ... %! resubLoss (Mdl), 1e-14); %!test # resubPredict answers exactly as predict on the training data %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG); %! assert_equal (resubPredict (Mdl), predict (Mdl, Mdl.X)); %!test # The response transform is parsed by name as well as by handle %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG, 'ResponseTransform', 'exp'); %! assert_equal (Mdl.ResponseTransform, 'exp'); %! assert_equal (predict (Mdl, X(1, :)), exp (17.25), -1e-12); %! Mdl.ResponseTransform = 'none'; %! assert_equal (predict (Mdl, X(1, :)), 17.25, 1e-12); %!test # A tree of one node answers its mean everywhere %! Mdl = RegressionTree ((1:5)', [2; 2; 2; 2; 2]); %! assert_equal (Mdl.NumNodes, 1); %! assert_equal (Mdl.NodeMean, 2); %! assert_equal (Mdl.NodeError, 0); %! assert_equal (Mdl.NodeRisk, 0); %! assert_equal (predict (Mdl, 99), 2); %! assert_equal (predictorImportance (Mdl), 0); %!test # A model saved and loaded answers exactly as it did %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG); %! fname = tempname (); %! unwind_protect %! savemodel (Mdl, fname); %! New = loadmodel (fname); %! assert_equal (class (New), 'RegressionTree'); %! assert_equal (New.NumNodes, Mdl.NumNodes); %! assert_equal (New.NodeRisk, Mdl.NodeRisk, 1e-15); %! assert_equal (New.PruneAlpha, Mdl.PruneAlpha, 1e-12); %! assert_equal (New.CutPredictor, Mdl.CutPredictor); %! assert_equal (predict (New, X), predict (Mdl, X)); %! unwind_protect_cleanup %! delete (fname); %! end_unwind_protect %!test # MATLAB parity: a small fixture, every node and every alpha %! x = (1:12)'; %! y = [1; 1; 1; 1; 5; 5; 5; 5; 20; 20; 20; 20]; %! Mdl = RegressionTree (x, y, 'MinParentSize', 2); %! assert_equal (Mdl.NumNodes, 5); %! assert_equal (Mdl.NodeSize', [12, 8, 4, 4, 4]); %! assert_equal (Mdl.NodeMean', [8.66666666666666, 3, 20, 1, 5], 1e-13); %! assert_equal (Mdl.NodeError', [66.8888888888889, 4, 0, 0, 0], 1e-12); %! assert_equal (Mdl.NodeRisk', [66.8888888888889, 2.66666666666667, ... %! 0, 0, 0], 1e-12); %! assert_equal (Mdl.PruneList', [2, 1, 0, 0, 0]); %! assert_equal (Mdl.PruneAlpha', [0, 2.66666666666667, ... %! 64.2222222222222], 1e-12); %! assert_equal (predictorImportance (Mdl), 33.4444444444444, 1e-12); %!test # MATLAB parity: a fixture grown to one observation per leaf %! x = (1:8)'; %! y = [1; 2; 10; 11; 30; 31; 60; 61]; %! Mdl = RegressionTree (x, y, 'MinParentSize', 2); %! assert_equal (Mdl.NumNodes, 15); %! assert_equal (Mdl.NodeSize', [8, 6, 2, 4, 2, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1]); %! assert_equal (Mdl.NodeMean(1:5)', [25.75, 14.1666666666667, 60.5, ... %! 6, 30.5], 1e-13); %! assert_equal (Mdl.NodeRisk(1:5)', [512.9375, 110.354166666667, 0.0625, ... %! 10.25, 0.0625], 1e-12); %! assert_equal (Mdl.PruneList', [4, 3, 1, 2, 1, 0, 0, 1, 1, 0, 0, 0, 0, ... %! 0, 0]); %! assert_equal (Mdl.PruneAlpha', [0, 0.0625, 10.125, 100.041666666667, ... %! 402.520833333333], 1e-11); %! assert_equal (predictorImportance (Mdl), 73.2767857142857, 1e-12); %!test # MATLAB parity: compact drops the data and answers identically %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG); %! CMdl = compact (Mdl); %! assert_equal (class (CMdl), 'CompactRegressionTree'); %! assert_equal (numel (properties (CMdl)), 28); %! assert_equal (predict (CMdl, X), predict (Mdl, X)); %! assert_equal (CMdl.NodeRisk, Mdl.NodeRisk, 1e-15); %! assert_equal (predictorImportance (CMdl), predictorImportance (Mdl), 1e-15); %!test # MATLAB parity: crossval returns a partitioned model over compacts %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG); %! CVMdl = crossval (Mdl); %! assert_equal (class (CVMdl), 'RegressionPartitionedModel'); %! assert_equal (CVMdl.CrossValidatedModel, 'Tree'); %! assert_equal (class (CVMdl.Trained{1}), 'CompactRegressionTree'); %! assert_equal (CVMdl.KFold, 10); %! assert_equal (CVMdl.NumObservations, 94); %! assert_equal (CVMdl.ResponseName, 'Y'); %! assert_equal (CVMdl.W, Mdl.W, 1e-15); %!test # Each way of asking for a partition gives the folds it names %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG); %! assert_equal (crossval (Mdl, 'KFold', 5).KFold, 5); %! assert_equal (crossval (Mdl, 'Holdout', 0.3).KFold, 1); %! assert_equal (crossval (Mdl, 'Leaveout', 'on').KFold, 94); %! assert_equal (crossval (Mdl, 'CVPartition', ... %! cvpartition (94, 'KFold', 4)).KFold, 4); %!test # A fold is grown with the parameters the parent was grown with %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG, 'MinLeafSize', 7, 'MergeLeaves', 'off', ... %! 'QuadraticErrorTolerance', 0.01); %! CVMdl = crossval (Mdl, 'KFold', 3); %! assert_equal (CVMdl.ModelParameters.MinLeaf, 7); %! assert_equal (CVMdl.ModelParameters.MergeLeaves, 'off'); %! assert_equal (CVMdl.ModelParameters.QEToler, 0.01); %! assert_equal (CVMdl.Trained{1}.PredictorNames, Mdl.PredictorNames); %!test # kfoldPredict answers every observation out of fold %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! CVMdl = crossval (RegressionTree (X, MPG), 'KFold', 5); %! yFit = kfoldPredict (CVMdl); %! assert_equal (size (yFit), [94, 1]); %! assert_equal (any (isnan (yFit)), false); %! assert_equal (kfoldLoss (CVMdl) > 0, true); %!test # cvloss over a fixture whose folds all answer alike %! ## The two groups are separated by a gap no fold can straddle, so every %! ## fold grows the same tree and answers the held-out row exactly. Left %! ## with one leaf, a fold's tree answers the mean of the thirty-nine rows %! ## it saw, which is 200/39 away from whichever value was held out, %! ## whichever group that row came from. %! x = [(1:20)'; (101:120)']; %! y = [zeros(20, 1); 10 * ones(20, 1)]; %! Mdl = RegressionTree (x, y); %! assert_equal (Mdl.NumNodes, 3); %! [E, SE, Nleaf, BestLevel] = cvloss (Mdl, 'SubTrees', 'all', 'KFold', 40); %! assert_equal (E', [0, (200 / 39) ^ 2], 1e-12); %! assert_equal (SE', [0, 0], 1e-12); %! assert_equal (Nleaf', [2, 1]); %! assert_equal (BestLevel, 0); %!test # cvloss reports one row per subtree asked for %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG, 'MinLeafSize', 15); %! [E, SE, Nleaf, BestLevel] = cvloss (Mdl, 'SubTrees', 'all'); %! assert_equal (size (E), [5, 1]); %! assert_equal (size (SE), [5, 1]); %! assert_equal (Nleaf', [5, 4, 3, 2, 1]); %! assert_equal (all (E > 0), true); %! assert_equal (BestLevel >= 0 && BestLevel <= 4, true); %! ## the root alone answers the mean, so it can do no better than the whole %! assert_equal (E(5) > E(1), true); %!test # cvloss defaults to the unpruned tree alone %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG, 'MinLeafSize', 15); %! [E, SE, Nleaf, BestLevel] = cvloss (Mdl); %! assert_equal (size (E), [1, 1]); %! assert_equal (Nleaf, 5); %! assert_equal (BestLevel, 0); %! assert_equal (E > 0, true); %!test # A subset of the levels is answered in the order it was asked for %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG, 'MinLeafSize', 15); %! [E, SE, Nleaf, BestLevel] = cvloss (Mdl, 'SubTrees', [0, 2, 4]); %! assert_equal (Nleaf', [5, 3, 1]); %! assert_equal (any (BestLevel == [0, 2, 4]), true); %!test # The fold count and the tree size rule are the ones asked for %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = RegressionTree (X, MPG, 'MinLeafSize', 15); %! assert_equal (size (cvloss (Mdl, 'KFold', 5)), [1, 1]); %! [~, ~, ~, bmin] = cvloss (Mdl, 'SubTrees', 'all', 'TreeSize', 'min'); %! [~, ~, ~, bse] = cvloss (Mdl, 'SubTrees', 'all', 'TreeSize', 'se'); %! assert_equal (bmin >= 0 && bmin <= 4, true); %! assert_equal (bse >= 0 && bse <= 4, true); %!test # cvloss weighs the loss as the fit weighed it %! ## A weighted fit whose folds all answer alike. The heavier group pulls %! ## the one-leaf answer towards itself, and the loss counts each row by %! ## its weight rather than by its share of the rows. %! x = [(1:20)'; (101:120)']; %! y = [zeros(20, 1); 10 * ones(20, 1)]; %! Mdl = RegressionTree (x, y, 'Weights', [ones(20, 1); 3 * ones(20, 1)]); %! E = cvloss (Mdl, 'SubTrees', 'all', 'KFold', 40); %! assert_equal (E(1), 0, 1e-12); %! assert_equal (E(2) > 0 && E(2) < 100, true); %!test # More folds than observations are reduced to one fold each %! x = [(1:20)'; (101:120)']; %! y = [zeros(20, 1); 10 * ones(20, 1)]; %! Mdl = RegressionTree (x, y); %! ws = warning ('off', 'all'); %! unwind_protect %! E = cvloss (Mdl, 'KFold', 500, 'SubTrees', 'all'); %! unwind_protect_cleanup %! warning (ws); %! end_unwind_protect %! assert_equal (E', [0, (200 / 39) ^ 2], 1e-12); ## Test input validation %!error RegressionTree () %!error RegressionTree (ones (4, 2)) %!error %! RegressionTree (ones (4, 2), (1:4)', 'K') %!error %! RegressionTree ('a', (1:4)') %!error %! RegressionTree (ones (4, 2), {'a'; 'b'; 'c'; 'd'}) %!error %! RegressionTree (ones (4, 2), (1:3)') %!error %! RegressionTree (ones (4, 2), (1:4)', 'PredictorNames', 'a') %!error %! RegressionTree (ones (4, 2), (1:4)', 'PredictorNames', {'a'}) %!error %! RegressionTree (ones (4, 2), (1:4)', 'ResponseName', 5) %!error %! RegressionTree (ones (4, 2), (1:4)', 'ResponseTransform', 5) %!error %! RegressionTree (ones (4, 2), (1:4)', 'ResponseTransform', 'bogus') %!error %! RegressionTree (ones (4, 2), (1:4)', 'Weights', 'a') %!error %! RegressionTree (ones (4, 2), (1:4)', 'Weights', ones (2, 2)) %!error %! RegressionTree (ones (4, 2), (1:4)', 'Weights', [1, 2, 3]) %!error %! RegressionTree (ones (4, 2), (1:4)', 'Weights', -ones (4, 1)) %!error %! RegressionTree (ones (4, 2), (1:4)', 'MaxNumSplits', -1) %!error %! RegressionTree (ones (4, 2), (1:4)', 'MinLeafSize', 0) %!error %! RegressionTree (ones (4, 2), (1:4)', 'MinParentSize', 0) %!error %! RegressionTree (ones (4, 2), (1:4)', 'MergeLeaves', 'x') %!error %! RegressionTree (ones (4, 2), (1:4)', 'Prune', 'x') %!error %! RegressionTree (ones (4, 2), (1:4)', 'QuadraticErrorTolerance', -1) %!error %! RegressionTree (ones (4, 2), (1:4)', 'SplitCriterion', 'gdi') %!error %! RegressionTree (ones (4, 2), (1:4)', 'PruneCriterion', 'error') %!error %! RegressionTree (ones (4, 2), (1:4)', 'Surrogate', 'on') %!error %! RegressionTree (ones (4, 2), (1:4)', 'KFold', 5) %!error %! RegressionTree (ones (4, 2), (1:4)', 'Bogus', 1) %!error %! RegressionTree (ones (4, 2), nan (4, 1)) %!error %! predict (RegressionTree (ones (4, 2), (1:4)')) %!error %! predict (RegressionTree (ones (4, 2), (1:4)'), []) %!error %! predict (RegressionTree (ones (4, 2), (1:4)'), 'a') %!error %! predict (RegressionTree (ones (4, 2), (1:4)'), ones (2, 5)) %!error %! loss (RegressionTree (ones (4, 2), (1:4)'), ones (4, 2)) %!error %! loss (RegressionTree (ones (4, 2), (1:4)'), ones (4, 2), (1:4)', 'LossFun') %!error %! loss (RegressionTree (ones (4, 2), (1:4)'), ones (4, 2), (1:4)', ... %! 'LossFun', 5) %!error %! loss (RegressionTree (ones (4, 2), (1:4)'), ones (4, 2), (1:4)', ... %! 'LossFun', 'mad') %!error %! loss (RegressionTree (ones (4, 2), (1:4)'), ones (4, 2), (1:4)', ... %! 'LossFun', @(y, f, w) [1, 2]) %!error %! loss (RegressionTree (ones (4, 2), (1:4)'), ones (4, 2), (1:4)', ... %! 'Weights', 'a') %!error %! loss (RegressionTree (ones (4, 2), (1:4)'), ones (4, 2), (1:4)', ... %! 'Weights', ones (2, 2)) %!error %! loss (RegressionTree (ones (4, 2), (1:4)'), ones (4, 2), (1:4)', ... %! 'Weights', [1, 2]) %!error %! loss (RegressionTree (ones (4, 2), (1:4)'), ones (4, 2), (1:4)', 'Bogus', 1) %!error %! loss (RegressionTree (ones (4, 2), (1:4)'), ones (4, 2), (1:3)') %!error %! prune (RegressionTree (ones (4, 2), (1:4)'), 'Level') %!error %! prune (RegressionTree (ones (4, 2), (1:4)'), 'Level', 1, 'Alpha', 1) %!error %! prune (RegressionTree (ones (4, 2), (1:4)'), 'Level', -1) %!error %! prune (RegressionTree (ones (4, 2), (1:4)'), 'Alpha', -1) %!error %! prune (RegressionTree (ones (4, 2), (1:4)'), 'Nodes', 999) %!error %! prune (RegressionTree (ones (4, 2), (1:4)'), 'Bogus', 1) %!error %! nodeVariableRange (RegressionTree (ones (4, 2), (1:4)')) %!error %! nodeVariableRange (RegressionTree (ones (4, 2), (1:4)'), 999) %!error %! cvloss (RegressionTree (ones (4, 2), (1:4)'), 'SubTrees') %!error %! load carsmall %! cvloss (RegressionTree ([Weight, Cylinders], MPG), 'SubTrees', -1) %!error %! load carsmall %! cvloss (RegressionTree ([Weight, Cylinders], MPG), 'SubTrees', [2, 1]) %!error %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! cvloss (RegressionTree (X, MPG, 'MinLeafSize', 15), 'SubTrees', 99) %!error %! load carsmall %! cvloss (RegressionTree ([Weight, Cylinders], MPG), 'TreeSize', 'x') %!error %! load carsmall %! cvloss (RegressionTree ([Weight, Cylinders], MPG), 'KFold', 1) %!error %! load carsmall %! cvloss (RegressionTree ([Weight, Cylinders], MPG), 'Bogus', 1) %!error %! load carsmall %! cvloss (RegressionTree ([Weight, Cylinders], MPG, 'Prune', 'off', ... %! 'MergeLeaves', 'off')) %!error %! crossval (RegressionTree (ones (4, 2), (1:4)'), 'KFold') %!error %! crossval (RegressionTree (ones (4, 2), (1:4)'), 'KFold', 2, 'Holdout', 0.3) %!error %! crossval (RegressionTree (ones (4, 2), (1:4)'), 'KFold', 1) %!error %! crossval (RegressionTree (ones (4, 2), (1:4)'), 'Holdout', 2) %!error %! crossval (RegressionTree (ones (4, 2), (1:4)'), 'Leaveout', 'x') %!error %! crossval (RegressionTree (ones (4, 2), (1:4)'), 'CVPartition', 5) %!error %! crossval (RegressionTree (ones (4, 2), (1:4)'), 'Bogus', 1) %!error %! savemodel (RegressionTree (ones (4, 2), (1:4)')) %!error %! savemodel (RegressionTree (ones (4, 2), (1:4)'), 5) %!error %! RegressionTree (ones (4, 2), [1; 2; 3; 4], 'NumVariablesToSample', 0) %!error %! RegressionTree (ones (4, 2), [1; 2; 3; 4], 'NumVariablesToSample', 1.5) %!error %! RegressionTree (ones (4, 2), [1; 2; 3; 4], 'NumVariablesToSample', 'some') %!shared Xr, yr %! k = (0:99)'; %! c = mod (k, 5) + 1; %! means = [3, 1, 4, 1.5, 5]; %! yr = means(c)' + 0.1 * sin (k); %! Xr = [c, mod(k * 3, 8)]; %!test # MATLAB parity: levels are ordered by their mean response %! Mdl = RegressionTree (Xr, yr, 'CategoricalPredictors', 1); %! assert_equal (Mdl.NumNodes, 39); %! assert_equal (Mdl.CutCategories(1,:), {[2, 4], [1, 3, 5]}); %! [yhat, nd] = predict (Mdl, [1, 0; 3, 0; 6, 0; NaN, 0]); %! assert_equal (yhat', [2.9817303, 4.0402625, 2.9003792, 2.9003792], 1e-7); %! assert_equal (nd', [12, 24, 1, 1]); %!test # a categorical predictor may be named rather than indexed %! Mdl = RegressionTree (Xr, yr, 'PredictorNames', {'grp', 'val'}, ... %! 'CategoricalPredictors', {'grp'}); %! assert_equal (Mdl.CategoricalPredictors, 1); %! assert_equal (Mdl.CutCategories(1,:), {[2, 4], [1, 3, 5]}); %!test # MATLAB parity: view and nodeVariableRange of a categorical cut %! Mdl = RegressionTree (Xr(:,1), yr, 'CategoricalPredictors', 1, ... %! 'MaxNumSplits', 2); %! txt = evalc ('view (Mdl)'); %! assert_equal (! isempty (strfind (txt, ['1 if x1 in {2 4} then node 2', ... %! ' elseif x1 in {1 3 5} then node 3 else 2.90038'])), true); %! assert_equal (! isempty (strfind (txt, ['3 if x1=1 then node 4', ... %! ' elseif x1 in {3 5} then node 5 else 4.00008'])), true); %! assert_equal (nodeVariableRange (Mdl, 3), struct ('x1', [1, 3, 5])); %!test # MaxNumCategories is recorded, and the level sets are saved %! Mdl = RegressionTree (Xr, yr, 'CategoricalPredictors', 1, ... %! 'MaxNumCategories', 2); %! assert_equal (Mdl.ModelParameters.MaxCat, 2); %! assert_equal (Mdl.NumNodes, 39); %! fname = [tempname(), '.mdl']; %! savemodel (Mdl, fname); %! M2 = loadmodel (fname); %! delete (fname); %! assert_equal (predict (M2, Xr), predict (Mdl, Xr)); %!error ... %! RegressionTree (Xr, yr, 'CategoricalPredictors', 3) %!error ... %! RegressionTree (Xr, yr, 'MaxNumCategories', 1.5) %!error ... %! RegressionTree (Xr, yr, 'AlgorithmForCategorical', 'pca') ## A table at prediction %!shared rtT, rtM %! load fisheriris %! rtT = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! rtT.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! rtM = fitrtree (rtT, 'SL'); %!test # the class may be built from a table as the fitter builds it %! Mdl = RegressionTree (rtT, 'SL'); %! assert_equal (Mdl.PredictorNames, rtM.PredictorNames); %! assert_equal (Mdl.CategoricalPredictors, rtM.CategoricalPredictors); %!test # predict takes a table %! yFit = predict (rtM, rtT); %! assert_equal (numel (yFit), 150); %! assert_equal (isnumeric (yFit), true); ## MATLAB parity: a table is read by name, so the order of its columns does ## not matter and a column the model was not fitted on is passed over %!test %! a = predict (rtM, rtT); %! b = predict (rtM, rtT(:, [5, 4, 3, 2, 1])); %! assert_equal (a, b); %! T = rtT; %! T.Extra = (1:150)'; %! assert_equal (predict (rtM, T), a); %!test # a matrix is still taken, as the model was fitted from one before %! load fisheriris %! Mdl = fitrtree (meas(:,2:4), meas(:,1)); %! assert_equal (numel (predict (Mdl, meas(:,2:4))), 150); %!test # the levels travel with the model when it is made compact %! CMdl = compact (rtM); %! assert_equal (CMdl.PredictorLevels, rtM.PredictorLevels); %! assert_equal (predict (CMdl, rtT), predict (rtM, rtT)); %!error ... %! predict (rtM, rtT(:, [1, 3, 4, 5])) ## MATLAB parity: a missing value, and a level the model was not fitted on, ## predict as missing, as measured on R2024a %!shared mkM, mtM %! k = categorical (repmat ({'a'; 'b'}, 6, 1)); %! y = repmat ([1; 10], 6, 1); %! mkM = fitrtree (table (k, y), 'y', 'MinParentSize', 2); %! kt = cellstr (k); %! mtM = fitrtree (table (kt, y), 'y', 'MinParentSize', 2); %!test # an undefined categorical value %! N = table (categorical ({'a'; ''}), 'VariableNames', {'k'}); %! assert_equal (predict (mkM, N), [1; 5.5], 1e-14); %!test # a categorical level the model was not fitted on %! N = table (categorical ({'b'; 'z'}), 'VariableNames', {'k'}); %! assert_equal (predict (mkM, N), [10; 5.5], 1e-14); %!test # empty text, and text the model was not fitted on %! N = table ({'a'; ''; 'z'}, 'VariableNames', {'kt'}); %! assert_equal (predict (mtM, N), [1; 5.5; 5.5], 1e-14); %!test # a missing string %! s = string ({'a'; 'b'; 'b'}); %! s(2) = missing; %! assert_equal (predict (mtM, table (s, 'VariableNames', {'kt'})), ... %! [1; 5.5; 10], 1e-14); ## MATLAB parity: 'Weights' may name a table variable, which is then no ## predictor, as measured on R2024a %!shared wtT, wtW %! x1 = (1:12)'; %! w = [1; 1; 2; 2; 1; 1; 3; 3; 1; 1; 2; 2]; %! y = x1; %! wtT = table (x1, w, y); %! wtW = [1; 1; 2; 2; 1; 1; 3; 3; 1; 1; 2; 2] / 20; %!test # the response named %! Mdl = fitrtree (wtT, 'y', 'Weights', 'w'); %! assert_equal (Mdl.PredictorNames, {'x1'}); %! assert_equal (Mdl.W, wtW, 1e-15); %!test # a model formula %! Mdl = fitrtree (wtT, 'y ~ x1', 'Weights', 'w'); %! assert_equal (Mdl.PredictorNames, {'x1'}); %! assert_equal (Mdl.W, wtW, 1e-15); %!test # the response given beside the table, and the name as a string %! Mdl = fitrtree (wtT(:, [1, 2]), (1:12)', 'Weights', string ('w')); %! assert_equal (Mdl.PredictorNames, {'x1'}); %!error ... %! fitrtree (wtT, 'y', 'Weights', 'v') ## MATLAB parity: an ordinal categorical column is a numeric predictor, ## split in the order of its categories, as measured on R2024a %!shared orT, orN %! o = categorical (repmat ({'lo'; 'hi'; 'lo'; 'mid'; 'hi'; 'lo'; 'mid'; ... %! 'hi'; 'lo'; 'mid'; 'hi'; 'lo'}, 2, 1), ... %! {'lo', 'mid', 'hi'}, 'Ordinal', true); %! y = 10 * double (o) + mod ((1:24)', 3); %! orT = table (o, y); %! orN = table (categorical ({'lo'; 'mid'; 'hi'; ''}, {'lo', 'mid', 'hi'}, ... %! 'Ordinal', true), 'VariableNames', {'o'}); %!test # numeric, cut between the codes of its categories %! Mdl = fitrtree (orT, 'y', 'MinParentSize', 2); %! assert_equal (Mdl.CategoricalPredictors, []); %! assert_equal (Mdl.CutPoint', [1.5, NaN, 2.5, NaN, NaN]); %!test # predictions by level, and the root mean for an undefined value %! Mdl = fitrtree (orT, 'y', 'MinParentSize', 2); %! assert_equal (predict (Mdl, orN), [10.2; 21; 32; mean(orT.y)], 1e-12); %!test # named in 'CategoricalPredictors', it is categorical %! Mdl = fitrtree (orT, 'y', 'MinParentSize', 2, 'CategoricalPredictors', 1); %! assert_equal (Mdl.CategoricalPredictors, 1); %! assert_equal (Mdl.CutPoint', NaN (1, 5)); ## A table at loss %!shared lrtT, lrtM %! load fisheriris %! lrtT = table (meas(:,2), meas(:,3), 'VariableNames', {'SW', 'PL'}); %! lrtT.SL = meas(:,1); %! lrtM = fitrtree (lrtT, 'SL'); %!test # the response is named, left out, or given beside the table %! a = loss (lrtM, [lrtT.SW, lrtT.PL], lrtT.SL); %! assert_equal (loss (lrtM, lrtT(:,1:2), lrtT.SL), a); %! assert_equal (loss (lrtM, lrtT, 'SL'), a); %! assert_equal (loss (lrtM, lrtT), a); %!test # an even number of arguments after the table is all name-value %! a = loss (lrtM, lrtT, 'LossFun', 'mse'); %! assert_equal (loss (lrtM, lrtT, 'SL', 'LossFun', 'mse'), a); %!test # a table is read by name, so the order of its columns does not matter %! assert_equal (loss (lrtM, lrtT(:, [3, 2, 1])), loss (lrtM, lrtT)); %!error ... %! loss (lrtM, lrtT, 'NoSuch') %!error ... %! loss (lrtM, lrtT(:,1:2)) ## Observation weights of class single or double %!error ... %! RegressionTree ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', 'Weights', ... %! int8 ([1; 1; 1; 1])) %!error ... %! RegressionTree ([1, 2; 3, 4; 5, 6; 7, 8], (1:4)', 'Weights', true (4, 1)) %!test %! ## Single weights are stored single, summing to one %! load fisheriris %! X = meas(:,2:4); %! y = meas(:,1); %! w = 1 + (1:150)' / 7; %! Mdl = RegressionTree (X, y, 'Weights', single (w)); %! assert_equal (class (Mdl.W), 'single'); %! assert_equal (sum (double (Mdl.W)), 1, 1e-6); %!test %! ## Single weights compute as double %! load fisheriris %! X = meas(:,2:4); %! y = meas(:,1); %! w = 1 + (1:150)' / 7; %! A = RegressionTree (X, y, 'Weights', single (w)); %! B = RegressionTree (X, y, 'Weights', double (single (w))); %! assert_equal (predict (A, X), predict (B, X)); ## MATLAB parity: a row missing every predictor, or of weight zero, is ## dropped, as measured on R2024a %!shared trX, trY %! x1 = mod ((1:30)', 7) + (1:30)' / 10; %! x2 = mod ((1:30)', 5) * 2 - (1:30)' / 20; %! trX = [x1, x2]; %! trX([2, 9, 17], 1) = NaN; %! trX(23, 2) = NaN; %! trX(27, :) = NaN; %! trY = 2 * x1 - x2; %!test # the row missing every predictor is not used %! Mdl = fitrtree (trX, trY); %! assert_equal (Mdl.NumObservations, 29); %! assert_equal (find (! Mdl.RowsUsed)', 27); %! assert_equal ([rows(Mdl.X), numel(Mdl.W)], [29, 29]); %!test # a row of weight zero is not used %! Mdl = fitrtree (trX, trY, 'Weights', [0; ones(29, 1)]); %! assert_equal (Mdl.NumObservations, 28); %!error ... %! fitrtree ([NaN, NaN; NaN, NaN; 1, 2], [1; 2; 3], 'Weights', [1; 1; 0]) statistics-release-2.1.2/inst/Machine_Learning/TreeBagger.m000066400000000000000000003656101527311104300236610ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef TreeBagger < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} TreeBagger ## ## Ensemble of bagged decision trees ## ## A @code{TreeBagger} object is a random forest: an ensemble of decision ## trees, each grown on a bootstrap sample of the training data and each ## choosing every split from a random subset of the predictors. The ## ensemble predicts by averaging its trees, their class probabilities for ## classification and their responses for regression. ## ## Every observation a tree's sample leaves out is out of bag for that tree, ## and the out-of-bag methods judge the ensemble on those observations ## alone, giving an estimate of its error on new data without a separate ## test set. ## ## Create one with the @code{TreeBagger} constructor. The @code{compact} ## method drops the training data and returns a @code{CompactTreeBagger}. ## ## @seealso{CompactTreeBagger, ClassificationTree, RegressionTree, fitctree, ## fitrtree} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {TreeBagger} {property} Method ## ## Type of the ensemble ## ## @qcode{'classification'} or @qcode{'regression'}. This property is ## read-only. ## ## @end deftp Method = 'classification'; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} NumTrees ## ## Number of trees ## ## A positive integer, the number of trees in the ensemble. This property ## is read-only. ## ## @end deftp NumTrees = 0; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} Trees ## ## The trees of the ensemble ## ## A column cell array holding one @code{CompactClassificationTree} or ## @code{CompactRegressionTree} object per tree. This property is ## read-only. ## ## @end deftp Trees = {}; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} X ## ## Predictor data ## ## The predictors the ensemble was fitted on, one row per observation. A ## row whose response is missing is not kept. This property is ## read-only. ## ## Where the model was fitted from a table, the predictors are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the model. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} Y ## ## Response data ## ## The response the ensemble was fitted on, in the type it was given in, ## without the observations whose response is missing. This property is ## read-only. ## ## @end deftp Y = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} W ## ## Observation weights ## ## A column of weights summing to one, one per observation. For ## classification each class's weights sum to its prior. The bootstrap ## draws observations in proportion to these weights, and the out-of-bag ## error is weighted by them. It has the class of the @qcode{'Weights'} ## given, single or double. This property is read-only. ## ## @end deftp W = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} ClassNames ## ## Names of the classes ## ## The classes of a classification ensemble, in the type of the response ## and in the order its scores are laid out: sorted, or the order given by ## @qcode{'ClassNames'}. Empty for a regression ensemble. This property ## is read-only. ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} Prior ## ## Prior probabilities of the classes ## ## A row vector with one probability per class, in the order of ## @code{ClassNames}. Every tree is grown with this prior, restricted to ## the classes its sample holds. Empty for a regression ensemble. This ## property is read-only. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} Cost ## ## Misclassification costs ## ## A square matrix, @code{Cost(i,j)} being the cost of classifying an ## observation of class @math{i} as class @math{j}. The trees are grown ## with it; the ensemble's label is the class of highest average score, as ## MATLAB documents. Empty for a regression ensemble. This property is ## read-only. ## ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} DefaultYfit ## ## Prediction for an observation no tree may answer for ## ## For classification, the class of greatest prior probability, in the ## type of @code{ClassNames}; for regression, the weighted mean of the ## response. An out-of-bag prediction takes it for an observation that is ## in the sample of every tree. This property is read-only. ## ## @end deftp DefaultYfit = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors naming the columns of @code{X}. This ## property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} CategoricalPredictors ## ## Indices of the categorical predictors ## ## A row vector of column indices into @var{X}, naming the predictors ## treated as categorical, empty when none is. This property is ## read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} NumPredictorsToSample ## ## Predictors each split is chosen from ## ## A positive integer, or @qcode{'all'}. The default is the square root ## of the number of predictors for classification and a third of it for ## regression, rounded up. This property is read-only. ## ## @end deftp NumPredictorsToSample = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} MinLeafSize ## ## Fewest observations a leaf may hold ## ## A positive integer, 1 by default for classification and 5 for ## regression. A node is split only when it holds at least twice as many. ## This property is read-only. ## ## @end deftp MinLeafSize = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} InBagFraction ## ## Size of each tree's sample ## ## A number greater than 0 and no greater than 1. Each tree is grown on ## @code{ceil (InBagFraction * N)} observations. This property is ## read-only. ## ## @end deftp InBagFraction = 1; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} SampleWithReplacement ## ## Whether the samples are drawn with replacement ## ## A logical scalar, true by default. This property is read-only. ## ## @end deftp SampleWithReplacement = true; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} ComputeOOBPrediction ## ## Whether out-of-bag information is kept ## ## A logical scalar, false unless the ensemble was fitted with ## @qcode{'OOBPrediction'} set to @qcode{'on'}. The out-of-bag methods ## need it. This property is read-only. ## ## @end deftp ComputeOOBPrediction = false; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} ComputeOOBPredictorImportance ## ## Whether out-of-bag predictor importance is computed ## ## A logical scalar, false unless the ensemble was fitted with ## @qcode{'OOBPredictorImportance'} set to @qcode{'on'}, which keeps the ## out-of-bag information too. This property is read-only. ## ## @end deftp ComputeOOBPredictorImportance = false; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} OOBIndices ## ## Which observations each tree left out ## ## An @math{NxNumTrees} logical matrix, true where an observation is not ## in a tree's sample. Empty unless @code{ComputeOOBPrediction} is true. ## This property is read-only. ## ## @end deftp OOBIndices = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} OOBInstanceWeight ## ## Number of trees each observation is out of bag for ## ## A column with one count per observation. Empty unless ## @code{ComputeOOBPrediction} is true. This property is read-only. ## ## @end deftp OOBInstanceWeight = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} DeltaCriterionDecisionSplit ## ## Split criterion contributions of the predictors ## ## A row vector with one element per predictor, the mean over the trees ## of each tree's @code{predictorImportance}. This property is read-only. ## ## @end deftp DeltaCriterionDecisionSplit = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} NumPredictorSplit ## ## Decision splits on each predictor ## ## A row vector with one element per predictor, the sum over the trees of ## the share of each tree's branch nodes that split on the predictor. A ## tree without branch nodes adds nothing. This property is read-only. ## ## @end deftp NumPredictorSplit = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} SurrogateAssociation ## ## Predictive association between the predictors ## ## A square matrix with one row and one column per predictor. The trees ## grow no surrogate splits, so it is the identity matrix. This property ## is read-only. ## ## @end deftp SurrogateAssociation = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} OOBPermutedPredictorDeltaError ## ## Rise in out-of-bag error when a predictor is permuted ## ## A row vector with one element per predictor. For each tree, the ## values of the predictor are permuted among the observations out of its ## bag, and the tree's error on them, the misclassification share or the ## mean squared error weighted by @code{W}, is taken before and after. ## The element is the mean of the rise over the trees divided by its ## standard deviation over the trees, zero when the mean is zero. Reading ## it is an error unless @code{ComputeOOBPredictorImportance} is true. ## This property is read-only. ## ## @end deftp OOBPermutedPredictorDeltaError = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} OOBPermutedPredictorDeltaMeanMargin ## ## Fall in out-of-bag mean margin when a predictor is permuted ## ## A row vector with one element per predictor, computed as ## @code{OOBPermutedPredictorDeltaError} is from each tree's weighted mean ## classification margin on its out-of-bag observations, before the ## permutation less after it. Empty for a regression ensemble. Reading ## it is an error unless @code{ComputeOOBPredictorImportance} is true. ## This property is read-only. ## ## @end deftp OOBPermutedPredictorDeltaMeanMargin = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} OOBPermutedPredictorCountRaiseMargin ## ## Margins lowered less margins raised when a predictor is permuted ## ## A row vector with one element per predictor, computed as ## @code{OOBPermutedPredictorDeltaError} is from the number of each tree's ## out-of-bag observations whose margin the permutation lowers less the ## number whose margin it raises. Empty for a regression ensemble. ## Reading it is an error unless @code{ComputeOOBPredictorImportance} is ## true. This property is read-only. ## ## @end deftp OOBPermutedPredictorCountRaiseMargin = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} Proximity ## ## Proximity of the training observations ## ## A symmetric @math{NxN} matrix whose element @math{(i,j)} is the share ## of the trees that bring training observations @math{i} and @math{j} to ## the same leaf. Reading it is an error until @code{fillprox} fills it, ## and @code{growTrees} and @code{append} empty it again. This property ## is read-only. ## ## @end deftp Proximity = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} OutlierMeasure ## ## Outlier measure of the training observations ## ## A column with one element per observation, computed from ## @code{Proximity} as @code{CompactTreeBagger.outlierMeasure} computes ## it, within each class for classification and over every observation ## for regression. Reading it is an error until @code{fillprox} fills it, ## and @code{growTrees} and @code{append} empty it again. This property ## is read-only. ## ## @end deftp OutlierMeasure = []; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} MergeLeaves ## ## Whether the trees merge leaves ## ## A logical scalar, false unless @qcode{'MergeLeaves'} was set to ## @qcode{'on'}. This property is read-only. ## ## @end deftp MergeLeaves = false; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} Prune ## ## Whether the trees estimate a pruning sequence ## ## A logical scalar, false unless @qcode{'Prune'} was set to @qcode{'on'}. ## The trees are never pruned. This property is read-only. ## ## @end deftp Prune = false; ## -*- texinfo -*- ## @deftp {TreeBagger} {property} TreeArguments ## ## Options given to the trees ## ## A cell array of the Name-Value pairs given to the constructor that are ## passed on to every tree, in the order they were given. This property is ## read-only. ## ## @end deftp TreeArguments = {}; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ResponseName = 'Y'; # name the table gave the response, which # a table call looks up; hidden as MATLAB's # TreeBagger carries no ResponseName TreeClassIdx = {}; # columns of ClassNames each tree's scores fill DefaultIndex = 0; # index of DefaultYfit into ClassNames DefaultScore = []; # scores of an observation no tree may answer for GrowArgs = {}; # Name-Value pairs every tree is grown with gY = []; # class index of each observation InBag = []; # sparse NxNumTrees count of each observation in bag PermDelta = []; # NumTreesxPx3 per-tree permutation deltas endproperties methods function v = get.OOBPermutedPredictorDeltaError (this) requireImportance (this); v = this.OOBPermutedPredictorDeltaError; endfunction function v = get.OOBPermutedPredictorDeltaMeanMargin (this) requireImportance (this); v = this.OOBPermutedPredictorDeltaMeanMargin; endfunction function v = get.OOBPermutedPredictorCountRaiseMargin (this) requireImportance (this); v = this.OOBPermutedPredictorCountRaiseMargin; endfunction function v = get.Proximity (this) requireProximity (this, this.Proximity); v = this.Proximity; endfunction function v = get.OutlierMeasure (this) requireProximity (this, this.OutlierMeasure); v = this.OutlierMeasure; endfunction endmethods methods (Hidden) function disp (this) fprintf ('\n TreeBagger\n\n'); fprintf ('%30s: %s\n', 'Method', this.Method); fprintf ('%30s: %d\n', 'NumTrees', this.NumTrees); fprintf ('%30s: [%dx%d]\n', 'X', rows (this.X), columns (this.X)); fprintf ('%30s: %d\n', 'NumPredictors', columns (this.X)); fprintf ('%30s: %s\n', 'NumPredictorsToSample', ... num2str (this.NumPredictorsToSample)); fprintf ('%30s: %d\n', 'MinLeafSize', this.MinLeafSize); fprintf ('%30s: %g\n', 'InBagFraction', this.InBagFraction); fprintf ('%30s: %d\n', 'SampleWithReplacement', ... this.SampleWithReplacement); fprintf ('%30s: %d\n', 'ComputeOOBPrediction', ... this.ComputeOOBPrediction); if (strcmp (this.Method, 'classification')) fprintf ('%30s: %s\n', 'ClassNames', ... classNameListing (this.ClassNames)); endif fprintf ('\n'); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {TreeBagger} {@var{B} =} TreeBagger (@var{NumTrees}, @var{X}, @var{Y}) ## @deftypefnx {TreeBagger} {@var{B} =} TreeBagger (@var{NumTrees}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {TreeBagger} {@var{B} =} TreeBagger (@var{NumTrees}, @var{Tbl}, @var{formula}) ## @deftypefnx {TreeBagger} {@var{B} =} TreeBagger (@var{NumTrees}, @var{Tbl}, @var{Y}) ## @deftypefnx {TreeBagger} {@var{B} =} TreeBagger (@dots{}, @var{name}, @var{value}) ## ## Grow an ensemble of bagged decision trees. ## ## @code{@var{B} = TreeBagger (@var{NumTrees}, @var{X}, @var{Y})} grows ## @var{NumTrees} classification trees on the @math{NxP} predictor matrix ## @var{X} and the response @var{Y}. @var{Y} holds a class label per row, ## as a numeric or logical vector, a categorical, string or character ## array, or a cell array of character vectors. An observation whose ## response is missing is left out; one missing a predictor is kept. ## ## Each tree is grown on a sample of @code{ceil (InBagFraction * N)} ## observations, drawn with replacement in proportion to the weights, and ## chooses every split from @code{NumPredictorsToSample} predictors drawn ## afresh at each node. The trees are neither pruned nor merged unless ## asked. The random numbers come from Octave's generator, so @code{rng} ## reproduces an ensemble. ## ## Name-Value arguments of the ensemble: ## ## @multitable @columnfractions 0.3 0.02 0.68 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Method'} @tab @tab @qcode{'classification'} (default) or ## @qcode{'regression'}, for a real numeric response. ## @item @qcode{'NumPredictorsToSample'} @tab @tab A positive integer or ## @qcode{'all'}. The default is @code{ceil (sqrt (P))} for classification ## and @code{ceil (P / 3)} for regression. ## @item @qcode{'MinLeafSize'} @tab @tab A positive integer, 1 by default ## for classification and 5 for regression. ## @item @qcode{'InBagFraction'} @tab @tab The share of the observations in ## each sample, greater than 0 and no greater than 1. The default is 1. ## @item @qcode{'SampleWithReplacement'} @tab @tab @qcode{'on'} (default) ## or @qcode{'off'}. ## @item @qcode{'OOBPrediction'} @tab @tab @qcode{'off'} (default) or ## @qcode{'on'}, to keep what the out-of-bag methods need. Sampling ## without replacement at an @qcode{'InBagFraction'} of 1 leaves nothing ## out of bag, so the two are refused together. ## @item @qcode{'OOBPredictorImportance'} @tab @tab @qcode{'off'} (default) ## or @qcode{'on'}, to estimate the importance of each predictor by ## permuting it among each tree's out-of-bag observations. It turns ## @qcode{'OOBPrediction'} on. ## @item @qcode{'Weights'} @tab @tab A nonnegative single or double vector ## with one weight per observation. The default is uniform. The model's ## @code{W} keeps the class of the weights, while every computation runs in ## double. ## @item @qcode{'Prior'} @tab @tab @qcode{'empirical'} (default), ## @qcode{'uniform'}, a vector with one probability per class, or a ## structure with fields @qcode{ClassNames} and @qcode{ClassProbs}. ## Classification only. ## @item @qcode{'Cost'} @tab @tab A square matrix of misclassification ## costs, or a structure with fields @qcode{ClassNames} and ## @qcode{ClassificationCosts}. Classification only. ## @item @qcode{'ClassNames'} @tab @tab The classes to fit, in the order ## their scores are to be laid out; observations of other classes are left ## out. Classification only. ## @item @qcode{'PredictorNames'} @tab @tab A cell array of character ## vectors naming the columns of @var{X}. ## @item @qcode{'NumPrint'} @tab @tab A nonnegative integer. After every ## that many trees a line saying how many are done is printed. The ## default, 0, prints nothing. ## @end multitable ## ## @qcode{'CategoricalPredictors'}, @qcode{'MaxNumCategories'}, ## @qcode{'MaxNumSplits'}, @qcode{'MergeLeaves'}, @qcode{'Prune'}, ## @qcode{'PruneCriterion'} and @qcode{'SplitCriterion'} are passed on to ## every tree, and so are @qcode{'AlgorithmForCategorical'} for ## classification and @qcode{'QuadraticErrorTolerance'} for regression; ## see @code{fitctree} and @code{fitrtree}. Merging leaves is allowed but ## warned against. ## ## Surrogate splits, parallel growth and tall arrays are not implemented, ## and an option asking for one of them is refused. ## ## MATLAB returns classification labels as a cell array of character ## vectors whatever the type of the response; this ensemble returns them, ## and its @code{ClassNames} and @code{DefaultYfit}, in the type of the ## response, as every other classifier in this package does. Code that ## converts MATLAB's labels with @code{str2double} will get @code{NaN} ## from a numeric response here, its labels already being numbers. ## ## @seealso{TreeBagger, CompactTreeBagger, fitctree, fitrtree} ## @end deftypefn function this = TreeBagger (NumTrees, X, Y, varargin) ## Input validation if (nargin < 3) error ("TreeBagger: too few input arguments."); endif if (! (isnumeric (NumTrees) && isscalar (NumTrees) && isreal (NumTrees) && NumTrees >= 1 && NumTrees == fix (NumTrees))) error ("TreeBagger: NUMTREES must be a positive integer."); endif ## A table names its own predictors and says which hold levels. This ## class carries no public ResponseName, as MATLAB's does not either, ## so the name the table gave the response is kept in the hidden one ## rather than passed on to the trees. ## An ordinal column is a numeric predictor here, as in R2024a [this, X, Y, varargin] = resolveTable (this, 'TreeBagger', X, Y, ... varargin, true); for k = numel (varargin) - 1:-1:1 if (ischar (varargin{k}) && strcmpi (varargin{k}, 'ResponseName')) this.ResponseName = varargin{k+1}; varargin(k:k+1) = []; endif endfor if (! (isnumeric (X) && isreal (X) && ismatrix (X) && ! isempty (X))) error ("TreeBagger: X must be a non-empty real numeric matrix."); endif if (ischar (Y)) nY = rows (Y); else nY = numel (Y); endif if (! (isvector (Y) || ischar (Y)) || nY != rows (X)) error (strcat ("TreeBagger: Y must be a vector with one element", ... " per row in X.")); endif if (! ischar (Y)) Y = Y(:); endif if (mod (numel (varargin), 2) != 0) error ("TreeBagger: name-value arguments must be in pairs."); endif ## Defaults Method = 'classification'; NumVarSample = []; MinLeafSize = []; InBagFraction = 1; WithReplacement = true; OOBPrediction = false; Importance = false; Cost = []; Prior = 'empirical'; ClassNames = []; Weights = []; PredictorNames = {}; CatSpec = []; NumPrint = 0; TreeArgs = {}; givenClass = {}; givenReg = {}; ## Parse optional parameters for i = 1:2:numel (varargin) name = varargin{i}; Value = varargin{i+1}; if (! ischar (name)) error (strcat ("TreeBagger: invalid parameter name in optional", ... " pair arguments.")); endif switch (tolower (name)) case 'method' if (! (ischar (Value) && any (strcmpi (Value, {'classification', ... 'regression'})))) error (strcat ("TreeBagger: 'Method' must be", ... " 'classification' or 'regression'.")); endif Method = tolower (Value); case 'numpredictorstosample' if (ischar (Value) && strcmpi (Value, 'all')) NumVarSample = 'all'; elseif (isnumeric (Value) && isscalar (Value) && isreal (Value) && Value >= 1 && Value == fix (Value)) NumVarSample = double (Value); else error (strcat ("TreeBagger: 'NumPredictorsToSample' must be", ... " a positive integer or 'all'.")); endif case 'minleafsize' if (! (isnumeric (Value) && isscalar (Value) && isreal (Value) && Value >= 1 && Value == fix (Value))) error (strcat ("TreeBagger: 'MinLeafSize' must be a", ... " positive integer.")); endif MinLeafSize = double (Value); TreeArgs(end+1:end+2) = {name, Value}; case 'inbagfraction' if (! (isnumeric (Value) && isscalar (Value) && isreal (Value) && Value > 0 && Value <= 1)) error (strcat ("TreeBagger: 'InBagFraction' must be a", ... " number greater than 0 and no greater", ... " than 1.")); endif InBagFraction = double (Value); case 'samplewithreplacement' [WithReplacement, ok] = onOff (Value); if (! ok) error (strcat ("TreeBagger: 'SampleWithReplacement' must", ... " be 'on' or 'off'.")); endif case 'oobprediction' [OOBPrediction, ok] = onOff (Value); if (! ok) error ("TreeBagger: 'OOBPrediction' must be 'on' or 'off'."); endif case 'oobpredictorimportance' [Importance, ok] = onOff (Value); if (! ok) error (strcat ("TreeBagger: 'OOBPredictorImportance' must", ... " be 'on' or 'off'.")); endif case 'cost' Cost = Value; givenClass{end+1} = name; case 'prior' Prior = Value; givenClass{end+1} = name; case 'classnames' ClassNames = Value; givenClass{end+1} = name; case 'weights' Weights = Value; case 'predictornames' if (! (iscellstr (Value) && numel (Value) == columns (X))) error (strcat ("TreeBagger: 'PredictorNames' must be a", ... " cell array of character vectors with one", ... " element per column of X.")); endif PredictorNames = Value(:)'; case 'categoricalpredictors' CatSpec = Value; case 'numprint' if (! (isnumeric (Value) && isscalar (Value) && isreal (Value) && Value >= 0 && Value == fix (Value))) error (strcat ("TreeBagger: 'NumPrint' must be a", ... " nonnegative integer.")); endif NumPrint = double (Value); case {'maxnumsplits', 'mergeleaves', 'prune', 'prunecriterion', ... 'splitcriterion', 'maxnumcategories', ... 'algorithmforcategorical'} TreeArgs(end+1:end+2) = {name, Value}; case 'quadraticerrortolerance' TreeArgs(end+1:end+2) = {name, Value}; givenReg{end+1} = name; case {'surrogate', 'predictorselection', 'options', 'chunksize'} error ("TreeBagger: '%s' is not implemented.", name); otherwise error (strcat ("TreeBagger: invalid parameter name in", ... " optional pair arguments.")); endswitch endfor isclass = strcmp (Method, 'classification'); OOBPrediction = OOBPrediction || Importance; if (isclass && ! isempty (givenReg)) error ("TreeBagger: '%s' applies only to regression.", givenReg{1}); endif if (! isclass && ! isempty (givenClass)) error ("TreeBagger: '%s' applies only to classification.", ... givenClass{1}); endif if (! WithReplacement && InBagFraction == 1 && OOBPrediction) error (strcat ("TreeBagger: sampling without replacement at an", ... " 'InBagFraction' of 1 leaves no observation out", ... " of bag for 'OOBPrediction'.")); endif p = columns (X); if (isempty (PredictorNames)) PredictorNames = arrayfun (@(k) sprintf ('x%d', k), 1:p, ... 'UniformOutput', false); endif ## The categorical predictors are resolved once the predictor names are ## known, so that they may be named as well as indexed, and before any ## row is left out, so that a level is not lost with the row carrying it. [Cod, errmsg] = dummyCoding (X, CatSpec, PredictorNames); if (! isempty (errmsg)) error ("TreeBagger: %s", errmsg); endif CatPred = []; if (! isempty (Cod.Index)) CatPred = Cod.Index; TreeArgs(end+1:end+2) = {'CategoricalPredictors', CatPred}; endif ## Observation weights, before any row is left out N = rows (X); errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("TreeBagger: %s", errmsg); endif ## The weights keep their class in the model; every computation runs on ## them as double. Wclass = "double"; if (isempty (Weights)) RawW = ones (N, 1); else Wclass = class (Weights); if (! (isnumeric (Weights) && isvector (Weights) && isreal (Weights) && numel (Weights) == N && all (Weights >= 0))) error (strcat ("TreeBagger: 'Weights' must be a nonnegative", ... " numeric vector with one element per row in X.")); endif RawW = double (Weights(:)); endif if (isclass) ## The classes, in the type of the response, and the class of each ## observation. A named subset keeps its own order and leaves the ## other classes' observations out, as MATLAB does. g0 = grp2idx (Y); U = uniqueLabels (Y(! isnan (g0),:)); if (isempty (ClassNames)) C = U; else if (! (iscellstr (ClassNames) || isnumeric (ClassNames) || islogical (ClassNames) || ischar (ClassNames) || isa (ClassNames, 'categorical') || isa (ClassNames, 'string'))) error (strcat ("TreeBagger: 'ClassNames' must be a", ... " categorical array, a character array, a", ... " string array, a logical vector, a numeric", ... " vector, or a cell array of character", ... " vectors.")); endif [loc, errmsg] = namedClasses (U, ClassNames); if (! isempty (errmsg)) error ("TreeBagger: %s", errmsg); endif C = labelsFromIndex (U, loc(:)); endif gY = labelIndices (C, Y); keep = gY > 0; if (! any (keep)) error ("TreeBagger: no observations with a known class."); endif X = X(keep,:); Y = Y(keep,:); gY = gY(keep); RawW = RawW(keep); if (! (sum (RawW) > 0)) error (strcat ("TreeBagger: 'Weights' must not be zero for", ... " every observation used.")); endif K = classCount (C); if (isempty (Cost)) Cost = 1 - eye (K); else [Cost, errmsg] = costMatrix (Cost, C); if (! isempty (errmsg)) error ("TreeBagger: %s", errmsg); endif endif if (isstruct (Prior)) P = priorFromStruct (Prior, C, 'TreeBagger'); elseif (ischar (Prior) && strcmpi (Prior, 'empirical')) P = accumarray (gY(:), RawW, [K, 1])'; elseif (ischar (Prior) && strcmpi (Prior, 'uniform')) P = ones (1, K); elseif (isnumeric (Prior) && isvector (Prior) && isreal (Prior) && numel (Prior) == K && all (Prior >= 0) && sum (Prior) > 0) P = double (Prior(:)'); else error (strcat ("TreeBagger: 'Prior' must be 'empirical',", ... " 'uniform', a structure, or a nonnegative", ... " vector with one element per class.")); endif this.Prior = P / sum (P); this.Cost = Cost; this.ClassNames = C; this.W = cast (priorNormalize (RawW, gY, this.Prior), Wclass); this.gY = gY; [~, this.DefaultIndex] = max (this.Prior); this.DefaultYfit = labelsFromIndex (C, this.DefaultIndex); this.DefaultScore = this.Prior; else if (! (isnumeric (Y) && isreal (Y))) error (strcat ("TreeBagger: Y must be a real numeric vector for", ... " a regression ensemble.")); endif Y = double (Y); keep = ! isnan (Y); if (! any (keep)) error ("TreeBagger: no observations with a known response."); endif X = X(keep,:); Y = Y(keep); RawW = RawW(keep); if (! (sum (RawW) > 0)) error (strcat ("TreeBagger: 'Weights' must not be zero for", ... " every observation used.")); endif this.W = cast (RawW / sum (RawW), Wclass); this.DefaultYfit = sum (RawW .* Y) / sum (RawW); endif if (isempty (NumVarSample)) if (isclass) NumVarSample = ceil (sqrt (p)); else NumVarSample = ceil (p / 3); endif endif if (isempty (MinLeafSize)) MinLeafSize = 1 + 4 * ! isclass; endif ## The pairs every tree is grown with. The tree options given come ## last, so that they override the ensemble's own defaults. this.GrowArgs = [{'PredictorNames', PredictorNames, ... 'MinLeafSize', MinLeafSize, ... 'MinParentSize', 2 * MinLeafSize, ... 'MergeLeaves', 'off', 'Prune', 'off', ... 'NumVariablesToSample', NumVarSample}, TreeArgs]; this.MergeLeaves = lastOnOff (TreeArgs, 'mergeleaves'); this.Prune = lastOnOff (TreeArgs, 'prune'); if (this.MergeLeaves) warning (strcat ("TreeBagger: merging leaves for bagged trees", ... " is not recommended.")); endif this.Method = Method; this.X = X; this.Y = Y; this.PredictorNames = PredictorNames; this.CategoricalPredictors = CatPred; this.NumPredictorsToSample = NumVarSample; this.MinLeafSize = MinLeafSize; this.InBagFraction = InBagFraction; this.SampleWithReplacement = WithReplacement; this.ComputeOOBPrediction = OOBPrediction; this.ComputeOOBPredictorImportance = Importance; this.TreeArguments = TreeArgs; this.InBag = sparse (rows (X), 0); if (OOBPrediction) this.OOBIndices = false (rows (X), 0); endif if (Importance) this.PermDelta = zeros (0, p, 3); endif this = growForest (this, NumTrees, NumPrint); endfunction ## -*- texinfo -*- ## @deftypefn {TreeBagger} {@var{label} =} predict (@var{obj}, @var{X}) ## @deftypefnx {TreeBagger} {[@var{label}, @var{scores}] =} predict (@dots{}) ## @deftypefnx {TreeBagger} {[@var{label}, @var{scores}, @var{stdevs}] =} predict (@dots{}) ## @deftypefnx {TreeBagger} {[@var{Yfit}, @var{stdevs}] =} predict (@dots{}) ## @deftypefnx {TreeBagger} {@dots{} =} predict (@dots{}, @var{name}, @var{value}) ## ## Predict responses with a bagged ensemble. ## ## Behaves as @code{CompactTreeBagger.predict}, and takes the same ## @qcode{'Trees'}, @qcode{'TreeWeights'} and @qcode{'UseInstanceForTree'} ## Name-Value arguments. ## ## ## The new data may be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: ## one the model was not fitted on is passed over, one it needs and ## cannot find is named, and a value holding a level is coded as that ## level was coded at fitting. ## @seealso{TreeBagger, TreeBagger.oobPredict, CompactTreeBagger.predict} ## @end deftypefn function [Yfit, scores, stdevs] = predict (this, X, varargin) if (nargin < 2) error ("TreeBagger.predict: too few input arguments."); endif ## A table is read by the names the model was fitted on X = tableColumns (this, 'TreeBagger.predict', X); [Yfit, scores, stdevs] = bagPredict (this, X, varargin, ... 'TreeBagger.predict', []); endfunction ## -*- texinfo -*- ## @deftypefn {TreeBagger} {@var{label} =} oobPredict (@var{obj}) ## @deftypefnx {TreeBagger} {[@var{label}, @var{scores}, @var{stdevs}] =} oobPredict (@dots{}) ## @deftypefnx {TreeBagger} {[@var{Yfit}, @var{stdevs}] =} oobPredict (@dots{}) ## @deftypefnx {TreeBagger} {@dots{} =} oobPredict (@dots{}, 'Trees', @var{trees}) ## ## Out-of-bag predictions for the training data. ## ## Each observation is predicted by the trees whose samples left it out, ## as @code{TreeBagger.predict} would with those trees alone. An ## observation in the sample of every tree used takes @code{DefaultYfit}, ## with the prior as its scores. @qcode{'Trees'} restricts the trees. ## The ensemble must have been fitted with @qcode{'OOBPrediction'} on. ## ## @seealso{TreeBagger, TreeBagger.oobError, TreeBagger.predict} ## @end deftypefn function [Yfit, scores, stdevs] = oobPredict (this, varargin) requireOOB (this, 'TreeBagger.oobPredict'); [Yfit, scores, stdevs] = bagPredict (this, this.X, varargin, ... 'TreeBagger.oobPredict', ... this.OOBIndices); endfunction ## -*- texinfo -*- ## @deftypefn {TreeBagger} {@var{err} =} error (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {TreeBagger} {@var{err} =} error (@dots{}, @var{name}, @var{value}) ## @deftypefnx {TreeBagger} {@var{err} =} error (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {TreeBagger} {@var{err} =} error (@var{obj}, @var{Tbl}) ## ## Misclassification probability or mean squared error of the ensemble. ## ## Behaves as @code{CompactTreeBagger.error} and takes the same Name-Value ## arguments. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{error (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{error (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{TreeBagger, TreeBagger.oobError, CompactTreeBagger.error} ## @end deftypefn function err = error (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("TreeBagger.error: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'error', X, Y, ... varargin, nargin > 2); err = bagLoss ('error', this, X, Y, varargin, 'TreeBagger.error', ... [], []); endfunction ## -*- texinfo -*- ## @deftypefn {TreeBagger} {@var{err} =} oobError (@var{obj}) ## @deftypefnx {TreeBagger} {@var{err} =} oobError (@dots{}, @var{name}, @var{value}) ## ## Out-of-bag misclassification probability or mean squared error. ## ## The error of the out-of-bag predictions on the training data, weighted ## by @code{W}. In @qcode{'individual'} mode each tree is judged on its ## own, the observations in its sample taking @code{DefaultYfit}, and ## every observation counts in every mode, as MATLAB counts it. ## @qcode{'Mode'}, @qcode{'Trees'}, @qcode{'TreeWeights'} and ## @qcode{'Weights'} are taken as by @code{CompactTreeBagger.error}. The ## ensemble must have been fitted with @qcode{'OOBPrediction'} on. ## ## @seealso{TreeBagger, TreeBagger.oobPredict, TreeBagger.oobMeanMargin} ## @end deftypefn function err = oobError (this, varargin) requireOOB (this, 'TreeBagger.oobError'); err = bagLoss ('error', this, this.X, this.Y, varargin, ... 'TreeBagger.oobError', this.OOBIndices, this.W); endfunction ## -*- texinfo -*- ## @deftypefn {TreeBagger} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {TreeBagger} {@var{m} =} margin (@dots{}, @var{name}, @var{value}) ## @deftypefnx {TreeBagger} {@var{m} =} margin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {TreeBagger} {@var{m} =} margin (@var{obj}, @var{Tbl}) ## ## Classification margin of each observation. ## ## Behaves as @code{CompactTreeBagger.margin} and takes the same Name-Value ## arguments. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{margin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable @var{ResponseVarName} names, and ## @code{margin (@var{obj}, @var{Tbl})} from the variable the model was ## fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{TreeBagger, TreeBagger.oobMargin, CompactTreeBagger.margin} ## @end deftypefn function m = margin (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("TreeBagger.margin: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'margin', X, Y, ... varargin, nargin > 2); m = bagLoss ('margin', this, X, Y, varargin, 'TreeBagger.margin', ... [], []); endfunction ## -*- texinfo -*- ## @deftypefn {TreeBagger} {@var{m} =} oobMargin (@var{obj}) ## @deftypefnx {TreeBagger} {@var{m} =} oobMargin (@dots{}, @var{name}, @var{value}) ## ## Out-of-bag classification margin of each training observation. ## ## The margins of the out-of-bag predictions, with @qcode{'Mode'}, ## @qcode{'Trees'} and @qcode{'TreeWeights'} taken as by ## @code{CompactTreeBagger.margin}. The ensemble must have been fitted ## with @qcode{'OOBPrediction'} on. ## ## @seealso{TreeBagger, TreeBagger.oobMeanMargin, TreeBagger.margin} ## @end deftypefn function m = oobMargin (this, varargin) requireOOB (this, 'TreeBagger.oobMargin'); m = bagLoss ('margin', this, this.X, this.Y, varargin, ... 'TreeBagger.oobMargin', this.OOBIndices, []); endfunction ## -*- texinfo -*- ## @deftypefn {TreeBagger} {@var{mm} =} meanMargin (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {TreeBagger} {@var{mm} =} meanMargin (@dots{}, @var{name}, @var{value}) ## @deftypefnx {TreeBagger} {@var{mm} =} meanMargin (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {TreeBagger} {@var{mm} =} meanMargin (@var{obj}, @var{Tbl}) ## ## Weighted mean classification margin. ## ## Behaves as @code{CompactTreeBagger.meanMargin} and takes the same ## Name-Value arguments. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{meanMargin (@var{obj}, @var{Tbl}, @var{ResponseVarName})} ## takes the response from the variable @var{ResponseVarName} names, and ## @code{meanMargin (@var{obj}, @var{Tbl})} from the variable the model ## was fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{TreeBagger, TreeBagger.oobMeanMargin, ## CompactTreeBagger.meanMargin} ## @end deftypefn function mm = meanMargin (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("TreeBagger.meanMargin: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'meanMargin', X, Y, ... varargin, nargin > 2); mm = bagLoss ('meanMargin', this, X, Y, varargin, ... 'TreeBagger.meanMargin', [], []); endfunction ## -*- texinfo -*- ## @deftypefn {TreeBagger} {@var{mm} =} oobMeanMargin (@var{obj}) ## @deftypefnx {TreeBagger} {@var{mm} =} oobMeanMargin (@dots{}, @var{name}, @var{value}) ## ## Out-of-bag mean classification margin. ## ## The mean of @code{TreeBagger.oobMargin}, weighted by @code{W} unless ## @qcode{'Weights'} are given. The ensemble must have been fitted with ## @qcode{'OOBPrediction'} on. ## ## @seealso{TreeBagger, TreeBagger.oobMargin, TreeBagger.oobError} ## @end deftypefn function mm = oobMeanMargin (this, varargin) requireOOB (this, 'TreeBagger.oobMeanMargin'); mm = bagLoss ('meanMargin', this, this.X, this.Y, varargin, ... 'TreeBagger.oobMeanMargin', this.OOBIndices, this.W); endfunction ## -*- texinfo -*- ## @deftypefn {TreeBagger} {@var{YFit} =} quantilePredict (@var{obj}, @var{X}) ## @deftypefnx {TreeBagger} {[@var{YFit}, @var{YW}] =} quantilePredict (@dots{}) ## @deftypefnx {TreeBagger} {@dots{} =} quantilePredict (@dots{}, @var{name}, @var{value}) ## ## Predict quantiles of the response with a regression ensemble. ## ## Each tree gives every training observation a weight for each row of ## @var{X}: the number of times the tree's sample holds the observation, ## divided by the size of the leaf the row comes to rest at, when the ## observation is in that leaf, and zero otherwise. The weights are ## averaged over the trees, with their tree weights, into @var{YW}, a ## sparse @math{NxM} matrix with one row per training observation and one ## column per row of @var{X}, each column summing to one. ## ## @var{YFit} is an @math{MxQ} matrix holding, for each row of @var{X} and ## each quantile probability, the quantile of the training responses under ## those weights. The responses are sorted, each keeping its own weight, ## and the quantile is interpolated linearly between them at their ## cumulative weights less half their own weight, taking the smallest or ## the largest response beyond either end. A row no tree may answer for ## takes @code{quantile (@var{obj}.Y, @var{tau})}, and its column of ## @var{YW} weighs every training observation equally. The observation ## weights enter only through the samples they drew. ## ## Name-Value arguments: ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Quantile'} @tab @tab A vector of probabilities @var{tau} ## between 0 and 1. The default is 0.5, the median. ## @item @qcode{'Trees'} @tab @tab @qcode{'all'} (default) or a vector of ## indices of the trees to use. ## @item @qcode{'TreeWeights'} @tab @tab A nonnegative vector with one ## weight per tree used. The default weighs them equally. ## @item @qcode{'UseInstanceForTree'} @tab @tab An @math{MxNumTrees} ## logical matrix saying which tree may answer for which row. ## @end multitable ## ## @var{X} may also be a table, whose variables are matched to the ## predictors the model was fitted on by name and not by position: one ## the model was not fitted on is passed over, one it needs and cannot ## find is named, and a value holding a level is coded as that level was ## coded at fitting. ## ## @seealso{TreeBagger, TreeBagger.oobQuantilePredict, ## TreeBagger.quantileError, TreeBagger.predict} ## @end deftypefn function [YFit, YW] = quantilePredict (this, X, varargin) if (nargin < 2) error ("TreeBagger.quantilePredict: too few input arguments."); endif ## A table is read by the names the model was fitted on X = tableColumns (this, 'TreeBagger.quantilePredict', X); [tau, o] = quantileArgs (this, X, varargin, ... {'Trees', 'TreeWeights', ... 'UseInstanceForTree'}, ... 'TreeBagger.quantilePredict', false); [YFit, YW] = quantileSteps (this, X, o.trees, o.tw, o.use, tau, ... 'ensemble'); endfunction ## -*- texinfo -*- ## @deftypefn {TreeBagger} {@var{YFit} =} oobQuantilePredict (@var{obj}) ## @deftypefnx {TreeBagger} {[@var{YFit}, @var{YW}] =} oobQuantilePredict (@dots{}) ## @deftypefnx {TreeBagger} {@dots{} =} oobQuantilePredict (@dots{}, @var{name}, @var{value}) ## ## Out-of-bag quantile predictions for the training data. ## ## Each training observation is predicted as by ## @code{TreeBagger.quantilePredict}, by the trees whose samples left it ## out. An observation in the sample of every tree used takes ## @code{quantile (@var{obj}.Y, @var{tau})}. @var{YW} is @math{NxN}. ## @qcode{'Quantile'}, @qcode{'Trees'} and @qcode{'TreeWeights'} are taken ## as by @code{TreeBagger.quantilePredict}. The ensemble must have been ## fitted with @qcode{'OOBPrediction'} on. ## ## @seealso{TreeBagger, TreeBagger.quantilePredict, ## TreeBagger.oobQuantileError} ## @end deftypefn function [YFit, YW] = oobQuantilePredict (this, varargin) requireOOB (this, 'TreeBagger.oobQuantilePredict'); [tau, o] = quantileArgs (this, this.X, varargin, ... {'Trees', 'TreeWeights'}, ... 'TreeBagger.oobQuantilePredict', false); [YFit, YW] = quantileSteps (this, this.X, o.trees, o.tw, ... this.OOBIndices(:,o.trees), tau, ... 'ensemble'); endfunction ## -*- texinfo -*- ## @deftypefn {TreeBagger} {@var{err} =} quantileError (@var{obj}, @var{X}, @var{Y}) ## @deftypefnx {TreeBagger} {@var{err} =} quantileError (@dots{}, @var{name}, @var{value}) ## @deftypefnx {TreeBagger} {@var{err} =} quantileError (@var{obj}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {TreeBagger} {@var{err} =} quantileError (@var{obj}, @var{Tbl}) ## ## Quantile loss of a regression ensemble. ## ## For each quantile probability @math{tau}, @var{err} is the weighted ## mean over the observations of the pinball loss, @math{tau (y - q)} ## where the response @math{y} is not below the predicted quantile ## @math{q} and @math{(1 - tau) (q - y)} where it is. The quantiles are ## predicted as by @code{TreeBagger.quantilePredict}. ## ## In @qcode{'ensemble'} mode, the default, @var{err} is a row with one ## element per quantile. In @qcode{'cumulative'} mode it has one row per ## tree, the loss of the first tree, then of the first two, and so on, and ## in @qcode{'individual'} mode one row per tree, each on its own. ## ## Name-Value arguments: ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Mode'} @tab @tab @qcode{'ensemble'} (default), ## @qcode{'cumulative'} or @qcode{'individual'}. ## @item @qcode{'Quantile'} @tab @tab A vector of probabilities between 0 ## and 1. The default is 0.5. ## @item @qcode{'Weights'} @tab @tab A nonnegative vector with one weight ## per observation. The default is uniform. ## @end multitable ## ## @qcode{'Trees'}, @qcode{'TreeWeights'} and @qcode{'UseInstanceForTree'} ## are taken as by @code{TreeBagger.quantilePredict}, and ## @qcode{'TreeWeights'} may not be given in @qcode{'individual'} mode. ## ## @var{X} may also be a table @var{Tbl}, whose variables are matched to ## the predictors the model was fitted on by name and not by position. ## @code{quantileError (@var{obj}, @var{Tbl}, @var{ResponseVarName})} ## takes the response from the variable @var{ResponseVarName} names, and ## @code{quantileError (@var{obj}, @var{Tbl})} from the variable the model ## was fitted on. The response may also be given beside the table as ## @var{Y}. ## ## @seealso{TreeBagger, TreeBagger.quantilePredict, ## TreeBagger.oobQuantileError, TreeBagger.error} ## @end deftypefn function err = quantileError (this, X, Y, varargin) if (nargin < 3 && ! (nargin > 1 && istable (X))) error ("TreeBagger.quantileError: too few input arguments."); endif ## A table carries the response: named in the call, given beside ## the table, or the variable the model was fitted on if (nargin < 3) Y = []; endif [X, Y, varargin] = tableResponse (this, 'quantileError', X, Y, ... varargin, nargin > 2); [tau, o] = quantileArgs (this, X, varargin, ... {'Mode', 'Trees', 'TreeWeights', ... 'UseInstanceForTree', 'Weights'}, ... 'TreeBagger.quantileError', true); if (! (isnumeric (Y) && isreal (Y) && isvector (Y) && numel (Y) == rows (X))) error (strcat ("TreeBagger.quantileError: Y must be a real", ... " numeric vector with one element per row in X.")); endif if (isempty (o.w)) w = ones (rows (X), 1); else w = o.w; endif q = quantileSteps (this, X, o.trees, o.tw, o.use, tau, o.mode); err = pinballLoss (double (Y(:)), q, tau, w); endfunction ## -*- texinfo -*- ## @deftypefn {TreeBagger} {@var{err} =} oobQuantileError (@var{obj}) ## @deftypefnx {TreeBagger} {@var{err} =} oobQuantileError (@dots{}, @var{name}, @var{value}) ## ## Out-of-bag quantile loss of a regression ensemble. ## ## The loss of @code{TreeBagger.quantileError} on the training data, the ## quantiles predicted out of bag as by ## @code{TreeBagger.oobQuantilePredict} and the observations weighted by ## @code{W}. @qcode{'Mode'}, @qcode{'Quantile'}, @qcode{'Trees'} and ## @qcode{'TreeWeights'} are taken as by @code{TreeBagger.quantileError}. ## The ensemble must have been fitted with @qcode{'OOBPrediction'} on. ## ## In @qcode{'individual'} mode each tree is judged on the observations ## out of its own bag alone, and a tree that left nothing out has a ## @code{NaN} loss. MATLAB R2024a fails with an indexing error in that ## mode. ## ## @seealso{TreeBagger, TreeBagger.oobQuantilePredict, ## TreeBagger.quantileError, TreeBagger.oobError} ## @end deftypefn function err = oobQuantileError (this, varargin) requireOOB (this, 'TreeBagger.oobQuantileError'); [tau, o] = quantileArgs (this, this.X, varargin, ... {'Mode', 'Trees', 'TreeWeights'}, ... 'TreeBagger.oobQuantileError', true); use = this.OOBIndices(:,o.trees); q = quantileSteps (this, this.X, o.trees, o.tw, use, tau, o.mode); if (strcmp (o.mode, 'individual')) err = zeros (numel (o.trees), numel (tau)); for j = 1:numel (o.trees) err(j,:) = pinballLoss (this.Y, q(:,:,j), tau, ... double (this.W) .* use(:,j)); endfor else err = pinballLoss (this.Y, q, tau, double (this.W)); endif endfunction ## -*- texinfo -*- ## @deftypefn {TreeBagger} {@var{B} =} fillprox (@var{obj}) ## @deftypefnx {TreeBagger} {@var{B} =} fillprox (@dots{}, @var{name}, @var{value}) ## ## Fill the proximity matrix of the training data. ## ## @var{B} is the ensemble with @code{Proximity} holding the share of the ## trees that bring each pair of training observations to the same leaf, ## and @code{OutlierMeasure} the outlier measure computed from it. ## @qcode{'Trees'} is @qcode{'all'} (default) or a vector of indices of ## the trees to use. @qcode{'NumPrint'} is a nonnegative integer; after ## every that many trees a line saying how many are done is printed. ## ## @seealso{TreeBagger, TreeBagger.mdsprox, CompactTreeBagger.proximity, ## CompactTreeBagger.outlierMeasure} ## @end deftypefn function this = fillprox (this, varargin) [NumPrint, args] = takePair (varargin, 'NumPrint', 0); if (! (isnumeric (NumPrint) && isscalar (NumPrint) && isreal (NumPrint) && NumPrint >= 0 && NumPrint == fix (NumPrint))) error (strcat ("TreeBagger.fillprox: 'NumPrint' must be a", ... " nonnegative integer.")); endif [o, errmsg] = bagArgs (args, this.NumTrees, rows (this.X), {'Trees'}); if (! isempty (errmsg)) error ("TreeBagger.fillprox: %s", errmsg); endif this.Proximity = bagProximity (this, this.X, o.trees, NumPrint); this.OutlierMeasure = bagOutlier (this.Proximity, this.gY); endfunction ## -*- texinfo -*- ## @deftypefn {TreeBagger} {[@var{S}, @var{E}] =} mdsprox (@var{obj}) ## @deftypefnx {TreeBagger} {[@var{S}, @var{E}] =} mdsprox (@dots{}, @var{name}, @var{value}) ## ## Multidimensional scaling of the proximity matrix. ## ## Applies classical multidimensional scaling, as @code{cmdscale} does, to ## the distances @code{1 - Proximity}. @var{S} holds the scaled ## coordinates, one column per positive eigenvalue, and @var{E} the ## eigenvalues. @code{fillprox} must have filled @code{Proximity} first. ## ## Name-Value arguments: ## ## @multitable @columnfractions 0.25 0.02 0.73 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Keep'} @tab @tab @qcode{'all'} (default), or a vector of ## indices or a logical vector selecting the training observations to ## scale. ## @item @qcode{'Colors'} @tab @tab A character vector with one color ## letter per class. When given, the scaled coordinates are drawn as ## overlaid scatter plots, one per class, a class beyond the number of ## letters not drawn; a regression ensemble is drawn in the first color. ## @item @qcode{'MDSCoordinates'} @tab @tab Two or three indices of the ## columns of @var{S} to draw. The default is @code{[1, 2]}. They must ## not exceed the number of columns of @var{S} even when nothing is drawn, ## as in MATLAB, whose documentation says otherwise. ## @end multitable ## ## @seealso{TreeBagger, TreeBagger.fillprox, CompactTreeBagger.mdsprox, ## cmdscale} ## @end deftypefn function [S, E] = mdsprox (this, varargin) P = this.Proximity; if (mod (numel (varargin), 2) != 0) error ("TreeBagger.mdsprox: name-value arguments must be in pairs."); endif N = rows (this.X); ## Parse optional paired arguments optNames = {'Keep', 'Colors', 'MDSCoordinates'}; dfValues = {'all', '', [1, 2]}; [Keep, colors, coords, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Validate optional paired arguments if (ischar (Keep) && strcmpi (Keep, 'all')) keep = 1:N; elseif (islogical (Keep) && isvector (Keep) && numel (Keep) == N) keep = find (Keep); elseif (isnumeric (Keep) && isvector (Keep) && isreal (Keep) && all (Keep >= 1) && all (Keep <= N) && all (Keep == fix (Keep))) keep = double (Keep(:)'); else error (strcat ("TreeBagger.mdsprox: 'Keep' must be 'all', a", ... " vector of indices of observations, or a logical", ... " vector with one element per observation.")); endif if (! isempty (args)) error ("TreeBagger.mdsprox: invalid optional paired argument."); endif g = []; if (strcmp (this.Method, 'classification')) g = this.gY(keep); endif [S, E, errmsg] = bagMds (P(keep,keep), g, colors, coords); if (! isempty (errmsg)) error ("TreeBagger.mdsprox: %s", errmsg); endif endfunction ## -*- texinfo -*- ## @deftypefn {TreeBagger} {@var{C} =} compact (@var{obj}) ## ## Drop the training data from an ensemble. ## ## @code{@var{C} = compact (@var{obj})} returns a ## @code{CompactTreeBagger} object holding the trees and what prediction ## needs. It predicts new data identically, and has no out-of-bag ## methods. ## ## @seealso{CompactTreeBagger, TreeBagger} ## @end deftypefn function C = compact (this) C = CompactTreeBagger (this); endfunction ## -*- texinfo -*- ## @deftypefn {TreeBagger} {@var{B} =} growTrees (@var{obj}, @var{NumTrees}) ## @deftypefnx {TreeBagger} {@var{B} =} growTrees (@dots{}, 'NumPrint', @var{n}) ## ## Grow more trees in an ensemble. ## ## @var{B} is the ensemble with @var{NumTrees} further trees grown exactly ## as the first were, their out-of-bag information added. A proximity ## matrix filled by @code{fillprox} is emptied, as it no longer describes ## the ensemble; MATLAB keeps it unchanged. ## @qcode{'NumPrint'} is taken as by the @code{TreeBagger} constructor. ## ## @seealso{TreeBagger, TreeBagger.append} ## @end deftypefn function this = growTrees (this, NumTrees, varargin) if (nargin < 2) error ("TreeBagger.growTrees: too few input arguments."); endif if (! (isnumeric (NumTrees) && isscalar (NumTrees) && isreal (NumTrees) && NumTrees >= 1 && NumTrees == fix (NumTrees))) error ("TreeBagger.growTrees: NUMTREES must be a positive integer."); endif if (mod (numel (varargin), 2) != 0) error (strcat ("TreeBagger.growTrees: name-value arguments must", ... " be in pairs.")); endif ## Parse optional paired arguments [NumPrint, Options, args] = ... parsePairedArguments ({'NumPrint', 'Options'}, {0, []}, ... varargin(:)); ## Validate optional paired arguments if (! (isnumeric (NumPrint) && isscalar (NumPrint) && isreal (NumPrint) && NumPrint >= 0 && NumPrint == fix (NumPrint))) error (strcat ("TreeBagger.growTrees: 'NumPrint' must be a", ... " nonnegative integer.")); endif if (! isempty (Options)) error ("TreeBagger.growTrees: 'Options' is not implemented."); endif if (! isempty (args)) error ("TreeBagger.growTrees: invalid optional paired argument."); endif this = growForest (this, NumTrees, NumPrint); endfunction ## -*- texinfo -*- ## @deftypefn {TreeBagger} {@var{B} =} append (@var{B1}, @var{B2}) ## ## Add the trees of one ensemble to another. ## ## @var{B} is @var{B1} with the trees of @var{B2} appended. The two must ## be of the same type, fitted on the same number of observations, with ## the same classes and priors, and must agree on whether they keep ## out-of-bag information and predictor importance, which are then ## joined. A proximity matrix filled by @code{fillprox} is emptied. ## ## @seealso{TreeBagger, TreeBagger.growTrees, CompactTreeBagger.combine} ## @end deftypefn function this = append (this, other) if (nargin < 2) error ("TreeBagger.append: too few input arguments."); endif if (! isa (other, 'TreeBagger')) error ("TreeBagger.append: B2 must be a TreeBagger object."); endif if (! strcmp (this.Method, other.Method)) error (strcat ("TreeBagger.append: the two ensembles must be of", ... " the same type.")); endif if (rows (this.X) != rows (other.X) || ! isequal (this.PredictorNames, other.PredictorNames) || ! isequal (this.CategoricalPredictors, other.CategoricalPredictors)) error (strcat ("TreeBagger.append: the two ensembles must be", ... " fitted on the same observations and predictors.")); endif if (! (isequal (this.ClassNames, other.ClassNames) && isequal (this.Prior, other.Prior))) error (strcat ("TreeBagger.append: the two ensembles must have", ... " the same classes and priors.")); endif if (this.ComputeOOBPrediction != other.ComputeOOBPrediction || (this.ComputeOOBPredictorImportance != other.ComputeOOBPredictorImportance)) error (strcat ("TreeBagger.append: the two ensembles have", ... " incompatible out-of-bag flags.")); endif this.Trees = [this.Trees; other.Trees]; this.TreeClassIdx = [this.TreeClassIdx; other.TreeClassIdx]; this.NumTrees = numel (this.Trees); this.InBag = [this.InBag, other.InBag]; if (this.ComputeOOBPrediction) this.OOBIndices = [this.OOBIndices, other.OOBIndices]; this.OOBInstanceWeight = sum (this.OOBIndices, 2); endif if (this.ComputeOOBPredictorImportance) this.PermDelta = [this.PermDelta; other.PermDelta]; endif this = updateStats (this); endfunction endmethods methods (Access = private) ## Grow n trees and add them to the ensemble. Each sample is drawn in ## proportion to W, with replacement through the cumulative weights and ## without it through exponential keys, which leave a zero weight ## undrawn in both. A tree is grown on the classes its sample holds, in ## the order the tree sorts them, and TreeClassIdx records where its ## scores go among the ensemble's classes. function this = growForest (this, n, NumPrint) N = rows (this.X); m = ceil (this.InBagFraction * N); isclass = strcmp (this.Method, 'classification'); if (this.SampleWithReplacement) c = cumsum (double (this.W)); c = [0; c / c(end)]; endif for t = 1:n if (this.SampleWithReplacement) idx = lookup (c, rand (m, 1)); else [~, order] = sort (rand (N, 1) .^ (1 ./ double (this.W)), ... 'descend'); idx = order(1:m); endif if (isclass) present = unique (this.gY(idx)); treeC = uniqueLabels (labelsFromIndex (this.ClassNames, present)); tidx = labelIndices (this.ClassNames, treeC); Mdl = ClassificationTree (this.X(idx,:), this.Y(idx,:), ... 'ClassNames', treeC, ... 'Prior', this.Prior(tidx), ... 'Cost', this.Cost(tidx, tidx), ... this.GrowArgs{:}); this.TreeClassIdx{end+1,1} = tidx(:)'; else Mdl = RegressionTree (this.X(idx,:), this.Y(idx), ... this.GrowArgs{:}); this.TreeClassIdx{end+1,1} = []; endif this.Trees{end+1,1} = compact (Mdl); counts = accumarray (idx(:), 1, [N, 1]); this.InBag(:,end+1) = sparse (counts); if (this.ComputeOOBPrediction) this.OOBIndices(:,end+1) = counts == 0; endif if (this.ComputeOOBPredictorImportance) this.PermDelta(end+1,:,:) = permDelta (this, this.Trees{end}, ... this.TreeClassIdx{end}, ... counts == 0); endif this.NumTrees = numel (this.Trees); if (NumPrint > 0 && mod (this.NumTrees, NumPrint) == 0) printf ("Tree %d done.\n", this.NumTrees); endif endfor if (this.ComputeOOBPrediction) this.OOBInstanceWeight = sum (this.OOBIndices, 2); endif this = updateStats (this); endfunction ## What permuting each predictor among a tree's out-of-bag observations ## does to the tree: a 1xPx3 array holding the rise in weighted error, ## the fall in weighted mean margin and the number of margins lowered ## less the number raised, all zero when nothing is out of bag. function d = permDelta (this, tree, tidx, oob) p = columns (this.X); d = zeros (1, p, 3); r = find (oob); w = double (this.W(r)); if (isempty (r) || ! (sum (w) > 0)) return; endif w /= sum (w); X = this.X(r,:); n = numel (r); if (strcmp (this.Method, 'classification')) g = this.gY(r); K = classCount (this.ClassNames); [e0, m0] = treeMissMargin (tree, X, g, tidx, K); for v = 1:p Xp = X; Xp(:,v) = X(randperm (n), v); [e1, m1] = treeMissMargin (tree, Xp, g, tidx, K); d(1,v,1) = sum (w .* (e1 - e0)); d(1,v,2) = sum (w .* (m0 - m1)); d(1,v,3) = sum (m1 < m0) - sum (m1 > m0); endfor else y = this.Y(r); e0 = (predict (tree, X) - y) .^ 2; for v = 1:p Xp = X; Xp(:,v) = X(randperm (n), v); d(1,v,1) = sum (w .* ((predict (tree, Xp) - y) .^ 2 - e0)); endfor endif endfunction ## The statistics derived from all the trees: the split contributions and ## counts, and the permuted importances, each the mean over the trees ## divided by the standard deviation over the trees. A filled proximity ## matrix describes other trees, and is emptied. function this = updateStats (this) p = columns (this.X); [this.DeltaCriterionDecisionSplit, this.NumPredictorSplit] = ... bagSplitStats (this.Trees, p); this.SurrogateAssociation = eye (p); if (this.ComputeOOBPredictorImportance) mu = mean (this.PermDelta, 1); sd = std (this.PermDelta, 0, 1); imp = mu ./ sd; imp(mu == 0) = 0; imp = reshape (imp, p, 3)'; this.OOBPermutedPredictorDeltaError = imp(1,:); if (strcmp (this.Method, 'classification')) this.OOBPermutedPredictorDeltaMeanMargin = imp(2,:); this.OOBPermutedPredictorCountRaiseMargin = imp(3,:); else this.OOBPermutedPredictorDeltaMeanMargin = zeros (1, 0); this.OOBPermutedPredictorCountRaiseMargin = zeros (1, 0); endif endif this.Proximity = []; this.OutlierMeasure = []; endfunction ## Quantiles of the response at the rows of Xq, for the trees given, their ## weights and the MxT matrix of which tree may answer for which row. ## MODE 'ensemble' gives an MxQ matrix, 'cumulative' and 'individual' ## an MxQxT array, the trees taken up to each one or each alone. YW is ## the response weight matrix of the last step. function [q, YW] = quantileSteps (this, Xq, trees, tw, use, tau, mode) N = rows (this.X); n = rows (Xq); T = numel (trees); Ltr = bagLeaves (this, this.X, trees); Lq = bagLeaves (this, Xq, trees); sample = quantile (this.Y, tau); sample = sample(:)'; if (strcmp (mode, 'ensemble')) q = zeros (n, numel (tau)); else q = zeros (n, numel (tau), T); endif num = sparse (N, n); den = zeros (1, n); for j = 1:T [i, ~, c] = find (this.InBag(:,trees(j))); nn = max ([Ltr(:,j); Lq(:,j)]); mass = accumarray (Ltr(i,j), c, [nn, 1]); A = sparse (i, Ltr(i,j), c ./ mass(Ltr(i,j)), N, nn); k = find (use(:,j) & mass(Lq(:,j)) > 0)'; Cj = A * sparse (Lq(k,j), k, tw(j), nn, n); dj = zeros (1, n); dj(k) = tw(j); if (strcmp (mode, 'individual')) [q(:,:,j), YW] = weighQuantiles (this.Y, Cj, dj, tau, sample); else num += Cj; den += dj; if (strcmp (mode, 'cumulative')) [q(:,:,j), YW] = weighQuantiles (this.Y, num, den, tau, sample); endif endif endfor if (strcmp (mode, 'ensemble')) [q, YW] = weighQuantiles (this.Y, num, den, tau, sample); endif endfunction function requireImportance (this) if (! this.ComputeOOBPredictorImportance) error (strcat ("TreeBagger: out-of-bag permutations were not", ... " kept; fit with 'OOBPredictorImportance' set to", ... " 'on'.")); endif endfunction function requireProximity (this, val) if (isempty (val)) error (strcat ("TreeBagger: the proximity matrix was not filled;", ... " call fillprox first.")); endif endfunction function requireOOB (this, caller) if (! this.ComputeOOBPrediction) error (strcat ("%s: out-of-bag information was not kept; fit", ... " with 'OOBPrediction' set to 'on'."), caller); endif endfunction endmethods endclassdef ## 'on' or 'off' as a logical scalar, OK false for anything else. function [tf, ok] = onOff (val) ok = ischar (val) && any (strcmpi (val, {'on', 'off'})); tf = ok && strcmpi (val, 'on'); endfunction ## The value of a Name-Value pair, removed from ARGS, or DEF when it is not ## given. Pairs that are not well formed are left for bagArgs to refuse. function [val, args] = takePair (args, name, def) val = def; if (mod (numel (args), 2) != 0) return; endif keep = true (size (args)); for i = 1:2:numel (args) if (ischar (args{i}) && strcmpi (args{i}, name)) val = args{i+1}; keep(i:i+1) = false; endif endfor args = args(keep); endfunction ## Validate a quantile method's call: a regression ensemble, the predictors, ## 'Quantile' and the pairs bagArgs takes. An error method defaults 'Mode' to ## 'ensemble'. function [tau, o] = quantileArgs (M, X, args, allowed, caller, isError) if (! strcmp (M.Method, 'regression')) error ("%s: quantiles are defined only for regression.", caller); endif if (! (isnumeric (X) && isreal (X) && ismatrix (X))) error ("%s: X must be a real numeric matrix.", caller); endif if (columns (X) != numel (M.PredictorNames)) error ("%s: X must have one column per predictor.", caller); endif [tau, args] = takePair (args, 'Quantile', 0.5); if (! (isnumeric (tau) && isreal (tau) && isvector (tau) && all (tau >= 0 & tau <= 1))) error (strcat ("%s: 'Quantile' must be a vector of probabilities", ... " between 0 and 1."), caller); endif tau = double (tau(:)'); if (isError && mod (numel (args), 2) == 0 && ! any (cellfun (@(a) ischar (a) && strcmpi (a, 'Mode'), ... args(1:2:end)))) args(end+1:end+2) = {'Mode', 'ensemble'}; endif [o, errmsg] = bagArgs (args, M.NumTrees, rows (X), allowed); if (! isempty (errmsg)) error ("%s: %s", caller, errmsg); endif endfunction ## Weighted quantiles of Y for each column of the weights NUM ./ DEN, and the ## normalized weights YW. A column without weight takes SAMPLE and weighs ## every observation equally. function [q, YW] = weighQuantiles (y, num, den, tau, sample) [N, n] = size (num); q = repmat (sample, n, 1); have = den > 0; dinv = zeros (n, 1); dinv(have) = 1 ./ den(have); YW = num * spdiags (dinv, 0, n, n); for k = find (have) [i, ~, w] = find (YW(:,k)); [ys, o] = sort (y(i)); w = w(o); if (numel (ys) == 1) q(k,:) = ys; continue; endif F = cumsum (w) - w / 2; qk = interp1 (F, ys, tau); qk(tau <= F(1)) = ys(1); qk(tau >= F(end)) = ys(end); q(k,:) = qk; endfor if (! all (have)) YW(:,! have) = 1 / N; endif endfunction ## Weighted mean pinball loss of the quantiles Q, MxQxL, against Y, as an LxQ ## matrix. An observation with a missing response or zero weight is left ## out. function err = pinballLoss (y, q, tau, w) w = w(:); w(isnan (y)) = 0; d = y - q; loss = max (tau .* d, (tau - 1) .* d); loss(w == 0,:,:) = 0; err = permute (sum (w .* loss, 1), [3, 2, 1]) / sum (w); endfunction ## Whether a tree misclassifies each observation, of class index G, and the ## margin it gives it, its scores laid out over the K classes of the ensemble. function [miss, m] = treeMissMargin (tree, X, g, tidx, K) [~, s] = predict (tree, X); S = zeros (rows (X), K); S(:,tidx) = s; [~, k] = max (S, [], 2); miss = double (k != g); m = marginsOf (S, g, 1); endfunction ## The last 'on' or 'off' given for a tree option, false when none was. function tf = lastOnOff (args, name) tf = false; for i = numel (args) - 1:-2:1 if (strcmpi (args{i}, name)) tf = ischar (args{i+1}) && strcmpi (args{i+1}, 'on'); return; endif endfor endfunction %!demo %! ## Grow a random forest on the iris data, estimate its error from the %! ## observations each tree left out, and classify a new flower. %! load fisheriris %! rng (42); %! B = TreeBagger (50, meas, species, 'OOBPrediction', 'on'); %! err = oobError (B); %! err(end) %! label = predict (B, [5.8, 2.8, 4.5, 1.4]) %!demo %! ## A regression forest predicting sepal length from the other three %! ## measurements, with the spread of its trees around each prediction. %! load fisheriris %! rng (42); %! B = TreeBagger (50, meas(:,2:4), meas(:,1), 'Method', 'regression'); %! [yfit, sd] = predict (B, meas([1, 51, 101], 2:4)) %!demo %! ## A 90% prediction interval for sepal length from the other three %! ## measurements, read off the quantiles of a regression forest. %! load fisheriris %! rng (42); %! B = TreeBagger (100, meas(:,2:4), meas(:,1), 'Method', 'regression'); %! q = quantilePredict (B, meas([1, 51, 101], 2:4), ... %! 'Quantile', [0.05, 0.5, 0.95]) %!demo %! ## Which measurements a forest relies on: permuting a petal measurement %! ## among each tree's out-of-bag flowers raises the error the most. %! load fisheriris %! rng (42); %! B = TreeBagger (50, meas, species, 'OOBPredictorImportance', 'on'); %! bar (B.OOBPermutedPredictorDeltaError); %! set (gca, 'xticklabel', {'SL', 'SW', 'PL', 'PW'}); %! ylabel ('Rise in out-of-bag error'); %!demo %! ## Scale the proximities of a random forest to two dimensions and draw %! ## the flowers one color per species. %! load fisheriris %! rng (42); %! B = fillprox (TreeBagger (50, meas, species)); %! mdsprox (B, 'Colors', 'rgb'); %!test # MATLAB parity: the defaults of a classification ensemble %! load fisheriris %! rng (1); %! B = TreeBagger (5, meas, species); %! assert_equal (B.NumTrees, 5); %! assert_equal (B.Method, 'classification'); %! assert_equal (B.NumPredictorsToSample, 2); %! assert_equal (B.MinLeafSize, 1); %! assert_equal (B.InBagFraction, 1); %! assert_equal (B.SampleWithReplacement, true); %! assert_equal (B.ComputeOOBPrediction, false); %! assert_equal (B.MergeLeaves, false); %! assert_equal (B.Prune, false); %! assert_equal (B.TreeArguments, {}); %! assert_equal (class (B.Trees{1}), 'CompactClassificationTree'); %!test # MATLAB parity: the number of predictors sampled by default %! rng (1); %! X5 = rand (30, 5); %! X7 = rand (30, 7); %! y = [ones(15, 1); 2 * ones(15, 1)]; %! assert_equal (TreeBagger (1, X5, y).NumPredictorsToSample, 3); %! assert_equal (TreeBagger (1, X7, y).NumPredictorsToSample, 3); %! r = rand (30, 1); %! assert_equal (TreeBagger (1, X5, r, 'Method', ... %! 'regression').NumPredictorsToSample, 2); %! assert_equal (TreeBagger (1, X7, r, 'Method', ... %! 'regression').NumPredictorsToSample, 3); %!test # MATLAB parity: a regression leaf holds five observations by default %! rng (1); %! B = TreeBagger (1, rand (30, 3), rand (30, 1), 'Method', 'regression'); %! assert_equal (B.MinLeafSize, 5); %! assert_equal (class (B.Trees{1}), 'CompactRegressionTree'); %!test # MATLAB parity: without sampling a tree is the single tree %! load fisheriris %! B = TreeBagger (2, meas, species, 'SampleWithReplacement', 'off', ... %! 'InBagFraction', 1, 'NumPredictorsToSample', 'all'); %! T = ClassificationTree (meas, species, 'Prune', 'off', ... %! 'MergeLeaves', 'off', 'MinParentSize', 2); %! assert_equal (B.Trees{2}.NumNodes, 17); %! assert_equal (B.Trees{2}.CutPredictorIndex, T.CutPredictorIndex); %! assert_equal (isequaln (B.Trees{2}.CutPoint, T.CutPoint), true); %!test # MATLAB parity: without sampling a regression tree is the single tree %! load fisheriris %! B = TreeBagger (1, meas(:,2:4), meas(:,1), 'Method', 'regression', ... %! 'SampleWithReplacement', 'off', 'InBagFraction', 1, ... %! 'NumPredictorsToSample', 'all'); %! T = RegressionTree (meas(:,2:4), meas(:,1), 'Prune', 'off', ... %! 'MergeLeaves', 'off', 'MinLeafSize', 5); %! assert_equal (B.Trees{1}.NumNodes, 47); %! assert_equal (isequaln (B.Trees{1}.CutPoint, T.CutPoint), true); %!test # MATLAB parity: scores average the trees and deviations divide by N %! load fisheriris %! rng (1); %! B = TreeBagger (10, meas, species, 'MinLeafSize', 5); %! [~, s, sd] = predict (B, meas); %! S = zeros (150, 3, 10); %! for t = 1:10 %! [~, st] = predict (B.Trees{t}, meas); %! S(:, B.TreeClassIdx{t}, t) = st; %! endfor %! assert_equal (s, mean (S, 3), 1e-14); %! assert_equal (sd, std (S, 1, 3), 1e-12); %!test # MATLAB parity: a regression forest averages its trees %! load fisheriris %! rng (1); %! B = TreeBagger (10, meas(:,2:4), meas(:,1), 'Method', 'regression'); %! [yfit, sd] = predict (B, meas(:,2:4)); %! R = zeros (150, 10); %! for t = 1:10 %! R(:,t) = predict (B.Trees{t}, meas(:,2:4)); %! endfor %! assert_equal (yfit, mean (R, 2), 1e-12); %! assert_equal (sd, std (R, 1, 2), 1e-12); %!test # MATLAB parity: the label is the class of highest score under a cost %! load fisheriris %! rng (1); %! B = TreeBagger (10, meas, species, 'MinLeafSize', 15, ... %! 'Cost', [0, 1, 1; 20, 0, 1; 1, 1, 0]); %! [label, s] = predict (B, meas); %! [~, k] = max (s, [], 2); %! assert_equal (label, B.ClassNames(k)); %!test # labels take the type of the response, where MATLAB gives cellstr %! load fisheriris %! rng (1); %! B = TreeBagger (3, meas, categorical (species)); %! assert_equal (class (predict (B, meas(1,:))), 'categorical'); %! assert_equal (class (B.ClassNames), 'categorical'); %! assert_equal (class (B.DefaultYfit), 'categorical'); %!test # MATLAB parity: a sample holds ceil (InBagFraction * N) observations %! load fisheriris %! rng (1); %! B = TreeBagger (1, meas, species, 'InBagFraction', 0.334); %! assert_equal (B.Trees{1}.NodeSize(1), 51); %!test # MATLAB parity: the default class is the class of most weight %! load fisheriris %! w = [20 * ones(20, 1); ones(100, 1)]; %! B = TreeBagger (1, meas([1:20, 51:150],:), species([1:20, 51:150]), ... %! 'Weights', w); %! assert_equal (B.DefaultYfit, {'setosa'}); %! assert_equal (sum (B.W), 1, 1e-15); %!test # MATLAB parity: the regression default is the weighted mean %! load fisheriris %! w = [5 * ones(50, 1); ones(100, 1)]; %! B = TreeBagger (1, meas(:,2:4), meas(:,1), 'Method', 'regression', ... %! 'Weights', w); %! assert_equal (B.DefaultYfit, sum (w .* meas(:,1)) / sum (w), 1e-12); %!test # MATLAB parity: a missing response drops its row, a missing X does not %! load fisheriris %! y = meas(:,1); %! y([3, 7]) = NaN; %! X = meas(:,2:4); %! X(5, 1) = NaN; %! B = TreeBagger (1, X, y, 'Method', 'regression', 'OOBPrediction', 'on'); %! assert_equal (size (B.X), [148, 3]); %! assert_equal (size (B.OOBIndices), [148, 1]); %!test # MATLAB parity: 'ClassNames' keeps its order and drops other classes %! load fisheriris %! B = TreeBagger (1, meas, species, 'ClassNames', {'virginica'; 'setosa'}); %! assert_equal (B.ClassNames, {'virginica'; 'setosa'}); %! assert_equal (rows (B.X), 100); %!test # MATLAB parity: the out-of-bag error in its three modes %! load fisheriris %! rng (1); %! B = TreeBagger (8, meas, species, 'OOBPrediction', 'on', 'MinLeafSize', 5); %! e = oobError (B); %! assert_equal (size (e), [8, 1]); %! assert_equal (size (oobError (B, 'Mode', 'individual')), [8, 1]); %! assert_equal (e(end), oobError (B, 'Mode', 'ensemble'), 1e-15); %! assert_equal (B.OOBInstanceWeight, sum (B.OOBIndices, 2)); %!test # MATLAB parity: a tree judged alone gives its sample the default %! load fisheriris %! rng (1); %! B = TreeBagger (4, meas, species, 'OOBPrediction', 'on', 'MinLeafSize', 5); %! l = predict (B.Trees{3}, meas); %! l(! B.OOBIndices(:,3)) = B.DefaultYfit; %! e = oobError (B, 'Mode', 'individual'); %! assert_equal (e(3), sum (B.W .* ! strcmp (l, species)), 1e-14); %!test # MATLAB parity: an observation in every sample takes the default %! load fisheriris %! rng (5); %! B = TreeBagger (1, meas, species, 'OOBPrediction', 'on', 'MinLeafSize', 15); %! r = find (! B.OOBIndices, 1); %! [label, s] = oobPredict (B); %! assert_equal (label(r), B.DefaultYfit); %! assert_equal (s(r,:), B.Prior, 1e-15); %!test # MATLAB parity: margins by tree and their means %! load fisheriris %! rng (1); %! B = TreeBagger (6, meas, species, 'OOBPrediction', 'on'); %! assert_equal (size (margin (B, meas, species)), [150, 6]); %! assert_equal (size (meanMargin (B, meas, species)), [1, 6]); %! assert_equal (size (oobMargin (B)), [150, 6]); %! assert_equal (size (oobMeanMargin (B, 'Mode', 'ensemble')), [1, 1]); %!test # MATLAB parity: weights make the error a weighted share %! load fisheriris %! rng (1); %! B = TreeBagger (6, meas, species, 'MinLeafSize', 15); %! w = [5 * ones(50, 1); ones(100, 1)]; %! miss = ! strcmp (predict (B, meas), species); %! e = error (B, meas, species, 'Mode', 'ensemble', 'Weights', w); %! assert_equal (e, sum (w .* miss) / sum (w), 1e-15); %!test # MATLAB parity: an observation no tree may use takes the default %! load fisheriris %! rng (1); %! B = TreeBagger (4, meas, species); %! U = true (2, 4); %! U(1,:) = false; %! [label, s, sd] = predict (B, meas(1:2,:), 'UseInstanceForTree', U); %! assert_equal (label(1), B.DefaultYfit); %! assert_equal (s(1,:), B.Prior, 1e-15); %! assert_equal (sd(1,:), NaN (1, 3)); %!test # tree weights give a weighted average of the trees chosen %! load fisheriris %! rng (1); %! B = TreeBagger (6, meas, species, 'MinLeafSize', 15); %! [~, s] = predict (B, meas(1:5,:), 'Trees', [2, 5], 'TreeWeights', [3, 1]); %! [~, a] = predict (B.Trees{2}, meas(1:5,:)); %! [~, b] = predict (B.Trees{5}, meas(1:5,:)); %! A = zeros (5, 3); %! A(:, B.TreeClassIdx{2}) = a; %! Bs = zeros (5, 3); %! Bs(:, B.TreeClassIdx{5}) = b; %! assert_equal (s, (3 * A + Bs) / 4, 1e-15); %!test # the state of the generator reproduces an ensemble %! load fisheriris %! rng (3); %! A = TreeBagger (3, meas, species, 'OOBPrediction', 'on'); %! rng (3); %! B = TreeBagger (3, meas, species, 'OOBPrediction', 'on'); %! assert_equal (A.OOBIndices, B.OOBIndices); %! assert_equal (A.Trees{3}.CutPredictorIndex, B.Trees{3}.CutPredictorIndex); %!test # MATLAB parity: growing and appending extend the out-of-bag matrix %! load fisheriris %! rng (1); %! B = TreeBagger (3, meas, species, 'OOBPrediction', 'on'); %! B = growTrees (B, 2); %! assert_equal (B.NumTrees, 5); %! assert_equal (size (B.OOBIndices), [150, 5]); %! B = append (B, TreeBagger (2, meas, species, 'OOBPrediction', 'on')); %! assert_equal (B.NumTrees, 7); %! assert_equal (size (B.OOBIndices), [150, 7]); %!test # MATLAB parity: the tree options given are kept as given %! load fisheriris %! B = TreeBagger (1, meas, species, 'MinLeafSize', 3, 'MaxNumSplits', 7); %! assert_equal (B.TreeArguments, {'MinLeafSize', 3, 'MaxNumSplits', 7}); %!test # MATLAB parity: progress is printed after every NumPrint trees %! load fisheriris %! out = evalc ("TreeBagger (2, meas, species, 'NumPrint', 1);"); %! assert_equal (out, sprintf ("Tree 1 done.\nTree 2 done.\n")); %!warning ... %! TreeBagger (1, [1; 2; 3; 4], [1; 1; 2; 2], 'MergeLeaves', 'on'); %!test # MATLAB parity: the split statistics of a regression ensemble %! load fisheriris %! B = TreeBagger (2, meas(:,2:4), meas(:,1), 'Method', 'regression', ... %! 'SampleWithReplacement', 'off', 'InBagFraction', 1, ... %! 'NumPredictorsToSample', 'all'); %! assert_equal (B.DeltaCriterionDecisionSplit, ... %! [0.001540487516843, 0.025160029558674, ... %! 0.000497162933685], 1e-14); %! assert_equal (B.NumPredictorSplit, ... %! [0.521739130434783, 1.043478260869565, ... %! 0.434782608695652], 1e-14); %! assert_equal (B.SurrogateAssociation, eye (3)); %!test # MATLAB parity: the split statistics of a classification ensemble %! load fisheriris %! B = TreeBagger (2, meas, species, 'SampleWithReplacement', 'off', ... %! 'InBagFraction', 1, 'NumPredictorsToSample', 'all', ... %! 'MinLeafSize', 5); %! assert_equal (B.DeltaCriterionDecisionSplit, ... %! [0.000477777777778, 0, 0.072985238862050, ... %! 0.051959205582394], 1e-14); %! assert_equal (B.NumPredictorSplit, [0.4, 0, 1.2, 0.4], 1e-14); %!test # MATLAB parity: split statistics are per-tree shares and means %! load fisheriris %! rng (1); %! B = growTrees (TreeBagger (3, meas, species), 2); %! n = zeros (1, 4); %! d = zeros (1, 4); %! for t = 1:5 %! tr = B.Trees{t}; %! br = find (tr.Children(:,1) > 0); %! n += accumarray (tr.CutPredictorIndex(br), 1, [4, 1])' / numel (br); %! d += predictorImportance (tr) / 5; %! endfor %! assert_equal (B.NumPredictorSplit, n, 1e-14); %! assert_equal (B.DeltaCriterionDecisionSplit, d, 1e-14); %!test # MATLAB parity: a tree without splits adds nothing to the statistics %! load fisheriris %! B = TreeBagger (3, meas, species, 'MinLeafSize', 100); %! assert_equal (B.NumPredictorSplit, zeros (1, 4)); %! assert_equal (B.DeltaCriterionDecisionSplit, zeros (1, 4)); %!test # MATLAB parity: the quantiles of a regression ensemble %! load fisheriris %! B = TreeBagger (2, meas(:,2:4), meas(:,1), 'Method', 'regression', ... %! 'SampleWithReplacement', 'off', 'InBagFraction', 1, ... %! 'NumPredictorsToSample', 'all'); %! q = quantilePredict (B, meas([1, 51, 101, 150], 2:4), ... %! 'Quantile', [0, 0.05, 0.25, 0.5, 0.75, 0.95, 1]); %! assert_equal (q, [4.6, 4.6, 4.9, 5.0, 5.1, 5.5, 5.5; ... %! 6.4, 6.4, 6.625, 6.7, 6.925, 7.0, 7.0; ... %! 6.3, 6.3, 6.45, 6.7, 6.825, 6.9, 6.9; ... %! 5.9, 5.9, 5.975, 6.1, 6.225, 6.3, 6.3], 1e-14); %!test # MATLAB parity: observation weights enter only through the sample %! load fisheriris %! w = [10 * ones(50, 1); ones(100, 1)]; %! B = TreeBagger (1, meas(:,2:4), meas(:,1), 'Method', 'regression', ... %! 'SampleWithReplacement', 'off', 'InBagFraction', 1, ... %! 'NumPredictorsToSample', 'all', 'Weights', w); %! q = quantilePredict (B, meas([1, 51, 101], 2:4), ... %! 'Quantile', [0.25, 0.5, 0.75]); %! assert_equal (q, [4.9, 5.0, 5.1; 6.625, 6.7, 6.925; ... %! 6.45, 6.7, 6.825], 1e-14); %!test # response weights are sample counts over leaf sizes, averaged %! load fisheriris %! rng (1); %! B = TreeBagger (4, meas(:,2:4), meas(:,1), 'Method', 'regression'); %! [~, YW] = quantilePredict (B, meas(1:3,2:4)); %! W = zeros (150, 3); %! for t = 1:4 %! [~, ntr] = predict (B.Trees{t}, B.X); %! [~, nq] = predict (B.Trees{t}, meas(1:3,2:4)); %! for k = 1:3 %! in = (ntr == nq(k)) .* full (B.InBag(:,t)); %! W(:,k) += in / B.Trees{t}.NodeSize(nq(k)) / 4; %! endfor %! endfor %! assert_equal (issparse (YW), true); %! assert_equal (full (YW), W, 1e-15); %!test # quantiles interpolate sorted responses at mid cumulative weights %! load fisheriris %! rng (1); %! B = TreeBagger (5, meas(:,2:4), meas(:,1), 'Method', 'regression'); %! tau = [0.001, 0.3, 0.62, 0.999]; %! [q, YW] = quantilePredict (B, meas(7,2:4), 'Quantile', tau); %! [i, ~, w] = find (YW); %! [ys, o] = sort (B.Y(i)); %! w = w(o); %! F = cumsum (w) - w / 2; %! e = interp1 (F, ys, tau); %! e(tau <= F(1)) = ys(1); %! e(tau >= F(end)) = ys(end); %! assert_equal (q, e, 1e-14); %!test # MATLAB parity: a row no tree may answer for takes the sample quantile %! load fisheriris %! rng (1); %! B = TreeBagger (3, meas(:,2:4), meas(:,1), 'Method', 'regression'); %! U = true (2, 3); %! U(1,:) = false; %! [q, YW] = quantilePredict (B, meas(1:2,2:4), ... %! 'Quantile', [0.1, 0.5, 0.9], ... %! 'UseInstanceForTree', U); %! assert_equal (q(1,:), [4.8, 5.8, 6.9], 1e-14); %! assert_equal (full (YW(:,1)), ones (150, 1) / 150, 1e-15); %!test # MATLAB parity: an observation in every bag takes the sample quantile %! load fisheriris %! rng (22); %! B = TreeBagger (1, meas(:,2:4), meas(:,1), 'Method', 'regression', ... %! 'OOBPrediction', 'on'); %! r = find (! B.OOBIndices, 1); %! q = oobQuantilePredict (B, 'Quantile', [0.1, 0.5, 0.9]); %! assert_equal (q(r,:), [4.8, 5.8, 6.9], 1e-14); %!test # out-of-bag quantiles use only the trees that left a row out %! load fisheriris %! rng (1); %! B = TreeBagger (6, meas(:,2:4), meas(:,1), 'Method', 'regression', ... %! 'OOBPrediction', 'on'); %! tau = [0.2, 0.8]; %! [qo, Wo] = oobQuantilePredict (B, 'Quantile', tau); %! [q, W] = quantilePredict (B, B.X, 'Quantile', tau, ... %! 'UseInstanceForTree', B.OOBIndices); %! assert_equal (qo, q); %! assert_equal (Wo, W); %!test # MATLAB parity: the quantile loss is the mean pinball loss %! load fisheriris %! B = TreeBagger (2, meas(:,2:4), meas(:,1), 'Method', 'regression', ... %! 'SampleWithReplacement', 'off', 'InBagFraction', 1, ... %! 'NumPredictorsToSample', 'all'); %! e = quantileError (B, meas(:,2:4), meas(:,1), 'Quantile', [0.25, 0.5, 0.9]); %! assert_equal (e, [0.069916666666667, 0.088666666666667, ... %! 0.035026666666667], 1e-14); %!test # MATLAB parity: the quantile loss weighs the observations %! load fisheriris %! B = TreeBagger (2, meas(:,2:4), meas(:,1), 'Method', 'regression', ... %! 'SampleWithReplacement', 'off', 'InBagFraction', 1, ... %! 'NumPredictorsToSample', 'all'); %! w = [5 * ones(50, 1); ones(100, 1)]; %! e = quantileError (B, meas(:,2:4), meas(:,1), 'Quantile', [0.25, 0.9], ... %! 'Weights', w); %! assert_equal (e, [0.064821428571429, 0.036268571428571], 1e-14); %!test # MATLAB parity: the three modes of the quantile loss %! load fisheriris %! rng (1); %! B = TreeBagger (4, meas(:,2:4), meas(:,1), 'Method', 'regression'); %! X = meas(:,2:4); %! y = meas(:,1); %! tau = [0.25, 0.75]; %! e = quantileError (B, X, y, 'Quantile', tau); %! c = quantileError (B, X, y, 'Quantile', tau, 'Mode', 'cumulative'); %! i = quantileError (B, X, y, 'Quantile', tau, 'Mode', 'individual'); %! assert_equal (size (e), [1, 2]); %! assert_equal (size (c), [4, 2]); %! assert_equal (c(end,:), e, 1e-15); %! assert_equal (i(3,:), quantileError (B, X, y, 'Quantile', tau, ... %! 'Trees', 3), 1e-15); %!test # MATLAB parity: the out-of-bag quantile loss in ensemble mode %! load fisheriris %! rng (1); %! B = TreeBagger (6, meas(:,2:4), meas(:,1), 'Method', 'regression', ... %! 'OOBPrediction', 'on'); %! tau = [0.1, 0.5, 0.9]; %! d = B.Y - oobQuantilePredict (B, 'Quantile', tau); %! L = mean (max (tau .* d, (tau - 1) .* d)); %! assert_equal (oobQuantileError (B, 'Quantile', tau), L, 1e-15); %! assert_equal (size (oobQuantileError (B, 'Mode', 'cumulative')), [6, 1]); %!test # each tree's out-of-bag quantile loss is on its own out-of-bag rows %! load fisheriris %! rng (1); %! B = TreeBagger (3, meas(:,2:4), meas(:,1), 'Method', 'regression', ... %! 'OOBPrediction', 'on'); %! e = oobQuantileError (B, 'Mode', 'individual'); %! r = B.OOBIndices(:,2); %! d = B.Y(r) - quantilePredict (B, B.X(r,:), 'Trees', 2); %! assert_equal (size (e), [3, 1]); %! assert_equal (e(2), mean (abs (d)) / 2, 1e-15); %!test # MATLAB parity: out-of-bag importance turns out-of-bag prediction on %! load fisheriris %! rng (1); %! B = TreeBagger (3, meas, species, 'OOBPredictorImportance', 'on'); %! assert_equal (B.ComputeOOBPrediction, true); %! assert_equal (B.ComputeOOBPredictorImportance, true); %! assert_equal (size (B.OOBPermutedPredictorDeltaError), [1, 4]); %! assert_equal (size (B.OOBPermutedPredictorDeltaMeanMargin), [1, 4]); %! assert_equal (size (B.OOBPermutedPredictorCountRaiseMargin), [1, 4]); %!test # MATLAB parity: a regression ensemble has no margin importances %! load fisheriris %! rng (1); %! B = TreeBagger (3, meas(:,2:4), meas(:,1), 'Method', 'regression', ... %! 'OOBPredictorImportance', 'on'); %! assert_equal (size (B.OOBPermutedPredictorDeltaError), [1, 3]); %! assert_equal (B.OOBPermutedPredictorDeltaMeanMargin, zeros (1, 0)); %! assert_equal (B.OOBPermutedPredictorCountRaiseMargin, zeros (1, 0)); %!test # MATLAB parity: importance is the mean change over its deviation %! load fisheriris %! rng (1); %! B = TreeBagger (5, meas, species, 'OOBPredictorImportance', 'on'); %! D = B.PermDelta; %! assert_equal (B.OOBPermutedPredictorDeltaError, ... %! mean (D(:,:,1)) ./ std (D(:,:,1)), 1e-14); %! assert_equal (B.OOBPermutedPredictorDeltaMeanMargin, ... %! mean (D(:,:,2)) ./ std (D(:,:,2)), 1e-14); %! assert_equal (B.OOBPermutedPredictorCountRaiseMargin, ... %! mean (D(:,:,3)) ./ std (D(:,:,3)), 1e-14); %!test # MATLAB parity: a predictor no tree splits on has zero importance %! load fisheriris %! rng (1); %! B = TreeBagger (10, [meas, ones(150, 1)], species, ... %! 'OOBPredictorImportance', 'on'); %! assert_equal (B.NumPredictorSplit(5), 0); %! assert_equal (B.OOBPermutedPredictorDeltaError(5), 0); %! assert_equal (B.OOBPermutedPredictorDeltaMeanMargin(5), 0); %! assert_equal (B.OOBPermutedPredictorCountRaiseMargin(5), 0); %!test # MATLAB parity: the importance of a single tree is zero or infinite %! load fisheriris %! rng (9); %! B = TreeBagger (1, meas, species, 'OOBPredictorImportance', 'on'); %! imp = B.OOBPermutedPredictorDeltaError; %! assert_equal (all (imp == 0 | isinf (imp)), true); %!test # MATLAB parity: permuting a petal measurement matters most %! load fisheriris %! rng (1); %! B = TreeBagger (30, meas, species, 'OOBPredictorImportance', 'on'); %! imp = B.OOBPermutedPredictorDeltaError; %! assert_equal (min (imp(3:4)) > max (imp(1:2)), true); %! assert_equal (all (B.OOBPermutedPredictorCountRaiseMargin(3:4) > 0), true); %!test # growing and appending extend the importances %! load fisheriris %! rng (1); %! B = TreeBagger (3, meas, species, 'OOBPredictorImportance', 'on'); %! B = growTrees (B, 2); %! assert_equal (size (B.PermDelta), [5, 4, 3]); %! B = append (B, TreeBagger (2, meas, species, ... %! 'OOBPredictorImportance', 'on')); %! assert_equal (size (B.PermDelta), [7, 4, 3]); %! D = B.PermDelta(:,:,1); %! assert_equal (B.OOBPermutedPredictorDeltaError, mean (D) ./ std (D), 1e-14); %!test # MATLAB parity: the proximity of the training observations %! load fisheriris %! B = TreeBagger (2, meas, species, 'SampleWithReplacement', 'off', ... %! 'InBagFraction', 1, 'NumPredictorsToSample', 'all', ... %! 'MinLeafSize', 5); %! B = fillprox (B); %! k = [1, 51, 101, 150]; %! assert_equal (B.Proximity(k,k), [1, 0, 0, 0; 0, 1, 0, 0; ... %! 0, 0, 1, 1; 0, 0, 1, 1]); %!test # MATLAB parity: fillprox over chosen trees %! load fisheriris %! rng (1); %! B = fillprox (TreeBagger (6, meas, species, 'MinLeafSize', 5), ... %! 'Trees', [2, 5]); %! P = zeros (150); %! for t = [2, 5] %! [~, ~, nd] = predict (B.Trees{t}, meas); %! P += nd == nd'; %! endfor %! assert_equal (B.Proximity, P / 2); %!test # the filled matrices agree with the compact ensemble's methods %! load fisheriris %! rng (1); %! B = fillprox (TreeBagger (4, meas, species, 'MinLeafSize', 5)); %! C = compact (B); %! assert_equal (B.Proximity, proximity (C, meas)); %! assert_equal (B.OutlierMeasure, outlierMeasure (C, meas, 'Labels', species)); %!test # MATLAB parity: a regression outlier measure is over every row %! load fisheriris %! rng (1); %! B = fillprox (TreeBagger (4, meas(:,2:4), meas(:,1), ... %! 'Method', 'regression')); %! assert_equal (B.OutlierMeasure, outlierMeasure (compact (B), meas(:,2:4))); %!test # MATLAB parity: fillprox prints its progress %! load fisheriris %! B = TreeBagger (3, meas, species); %! out = evalc ("fillprox (B, 'NumPrint', 1);"); %! assert_equal (out, sprintf ("Tree 1 done.\nTree 2 done.\nTree 3 done.\n")); %!test # MATLAB parity: the scaling of the proximity matrix %! load fisheriris %! B = TreeBagger (2, meas, species, 'SampleWithReplacement', 'off', ... %! 'InBagFraction', 1, 'NumPredictorsToSample', 'all', ... %! 'MinLeafSize', 5); %! [S, E] = mdsprox (fillprox (B)); %! assert_equal (size (S), [150, 5]); %! assert_equal (E(1:5)', [23.511509991334723, 20.652625175036377, ... %! 5.054560979007729, 3, 2.627970521287837], 1e-12); %! assert_equal (abs (S([1, 51, 101], 1:2)), ... %! [0.539357634260452, 0.083561333480090; ... %! 0.399117306477463, 0.428703363387418; ... %! 0.228627699102420, 0.556632583919985], 1e-12); %!test # MATLAB parity: 'Keep' scales the chosen observations alone %! load fisheriris %! rng (1); %! B = fillprox (TreeBagger (10, meas, species, 'MinLeafSize', 5)); %! k = 1:3:150; %! [S, E] = mdsprox (B, 'Keep', k); %! [S0, E0] = cmdscale (1 - B.Proximity(k,k)); %! assert_equal (S, S0); %! assert_equal (E, E0); %! assert_equal (mdsprox (B, 'Keep', mod (1:150, 3) == 1), S0); %!test # the scaled coordinates are drawn one class per color %! load fisheriris %! rng (1); %! B = fillprox (TreeBagger (5, meas, species, 'MinLeafSize', 5)); %! h = figure ('visible', 'off'); %! unwind_protect %! mdsprox (B, 'Colors', 'rgb'); %! assert_equal (numel (get (gca, 'children')), 3); %! unwind_protect_cleanup %! close (h); %! end_unwind_protect %!test # a regression ensemble is drawn in the first color %! load fisheriris %! rng (1); %! B = fillprox (TreeBagger (5, meas(:,2:4), meas(:,1), ... %! 'Method', 'regression')); %! h = figure ('visible', 'off'); %! unwind_protect %! mdsprox (B, 'Colors', 'rgb'); %! kids = get (gca, 'children'); %! assert_equal (numel (kids), 1); %! assert_equal (get (kids, 'color'), [1, 0, 0]); %! unwind_protect_cleanup %! close (h); %! end_unwind_protect %!error ... %! load fisheriris %! B = growTrees (fillprox (TreeBagger (2, meas, species)), 1); %! B.Proximity ## Test input validation %!shared x, y, B, C, R, Q %! load fisheriris %! x = meas; %! y = species; %! B = TreeBagger (3, x, y, 'OOBPrediction', 'on'); %! C = compact (B); %! R = TreeBagger (2, x(:,2:4), x(:,1), 'Method', 'regression'); %! Q = TreeBagger (2, x(:,2:4), x(:,1), 'Method', 'regression', ... %! 'OOBPrediction', 'on'); %!error TreeBagger (1, x) %!error TreeBagger (0, x, y) %!error ... %! TreeBagger (1, {1}, y) %!error ... %! TreeBagger (1, x, y(1:10)) %!error ... %! TreeBagger (1, x, y, 'Method') %!error ... %! TreeBagger (1, x, y, 1, 2) %!error ... %! TreeBagger (1, x, y, 'Foo', 2) %!error ... %! TreeBagger (1, x, y, 'Method', 'cluster') %!error ... %! TreeBagger (1, x, y, 'NumPredictorsToSample', 0) %!error ... %! TreeBagger (1, x, y, 'MinLeafSize', 0) %!error ... %! TreeBagger (1, x, y, 'InBagFraction', 1.5) %!error ... %! TreeBagger (1, x, y, 'SampleWithReplacement', 1) %!error ... %! TreeBagger (1, x, y, 'OOBPrediction', true) %!error ... %! TreeBagger (1, x, y, 'OOBPredictorImportance', 1) %!error ... %! TreeBagger (1, x, y, 'PredictorNames', {'a'}) %!error ... %! TreeBagger (1, x, y, 'NumPrint', -1) %!error ... %! TreeBagger (1, x, y, 'Surrogate', 'on') %!error ... %! TreeBagger (1, x, y, 'Options', struct ()) %!error ... %! TreeBagger (1, x, y, 'QuadraticErrorTolerance', 1e-3) %!error ... %! TreeBagger (1, x(:,2:4), x(:,1), 'Method', 'regression', ... %! 'Cost', [0, 1; 1, 0]) %!error ... %! TreeBagger (1, x, y, 'SampleWithReplacement', 'off', 'OOBPrediction', 'on') %!error ... %! TreeBagger (1, x, y, 'Weights', -ones (150, 1)) %!error ... %! TreeBagger (1, x, y, 'ClassNames', {1}) %!error ... %! TreeBagger (1, x, y, 'ClassNames', {'rose'}) %!error ... %! TreeBagger (1, x(1:3,:), NaN (3, 1)) %!error ... %! TreeBagger (1, x, y, 'Weights', zeros (150, 1)) %!error ... %! TreeBagger (1, x, y, 'Cost', [0, 1; 1, 0]) %!error ... %! TreeBagger (1, x, y, 'Prior', [1, 2]) %!error ... %! TreeBagger (1, x, y, 'Prior', struct ('a', 1)) %!error ... %! TreeBagger (1, x, y, 'Method', 'regression') %!error ... %! TreeBagger (1, x(1:3,:), NaN (3, 1), 'Method', 'regression') %!error ... %! oobError (TreeBagger (1, x, y)) %!error ... %! oobPredict (TreeBagger (1, x, y)) %!error ... %! oobMargin (TreeBagger (1, x, y)) %!error ... %! oobMeanMargin (TreeBagger (1, x, y)) %!error predict (B) %!error predict (B, {1}) %!error ... %! predict (B, ones (2, 3)) %!error ... %! predict (B, x, 'Trees') %!error ... %! predict (B, x, 'Mode', 'ensemble') %!error ... %! predict (B, x, 'Trees', 4) %!error ... %! predict (B, x, 'TreeWeights', [1, 1]) %!error ... %! predict (B, x, 'UseInstanceForTree', true (2, 3)) %!error ... %! oobPredict (B, 'UseInstanceForTree', true (150, 3)) %!error margin (B, x) %!error ... %! margin (B, x, y, 'Weights', ones (150, 1)) %!error meanMargin (B, x) %!error ... %! margin (R, x(:,2:4), x(:,1)) ## Octave's test.m cuts a message through its first 'error:' before matching ## it, so these patterns hold the message after the method's prefix. %!error error (B, x) %!error error (B, x, y(1:3)) %!error<'Weights' must be a nonnegative numeric vector with one element per observation, not all zero.> ... %! error (B, x, y, 'Weights', ones (3, 1)) %!error<'Mode' must be 'cumulative', 'individual' or 'ensemble'.> ... %! error (B, x, y, 'Mode', 'all') %!error<'TreeWeights' cannot be used in 'individual' mode.> ... %! error (B, x, y, 'Mode', 'individual', 'TreeWeights', [1, 1, 1]) %!error ... %! error (B, x(1:2,:), {'rose'; 'setosa'}) %!error error (R, x(1:2,2:4), {'a'; 'b'}) %!error growTrees (B) %!error ... %! growTrees (B, 0) %!error ... %! growTrees (B, 1, 'NumPrint') %!error ... %! growTrees (B, 1, 'NumPrint', -1) %!error ... %! growTrees (B, 1, 'Options', 1) %!error ... %! growTrees (B, 1, 'Foo', 1) %!error append (B) %!error append (B, C) %!error ... %! append (B, TreeBagger (1, x, x(:,1), 'Method', 'regression')) %!error ... %! append (B, TreeBagger (1, x(1:100,:), y(1:100), 'OOBPrediction', 'on')) %!error ... %! append (B, TreeBagger (1, x, y, 'OOBPrediction', 'on', ... %! 'Prior', 'uniform', ... %! 'ClassNames', {'virginica'; 'versicolor'; 'setosa'})) %!error ... %! append (B, TreeBagger (1, x, y)) %!error ... %! append (B, TreeBagger (1, x, y, 'OOBPredictorImportance', 'on')) %!error ... %! B.OOBPermutedPredictorDeltaError %!error ... %! B.OOBPermutedPredictorDeltaMeanMargin %!error ... %! B.OOBPermutedPredictorCountRaiseMargin %!error ... %! B.Proximity %!error ... %! B.OutlierMeasure %!error ... %! quantilePredict (R) %!error ... %! quantilePredict (B, x) %!error ... %! quantilePredict (R, {1}) %!error ... %! quantilePredict (R, x) %!error ... %! quantilePredict (R, x(:,2:4), 'Quantile', 2) %!error ... %! quantilePredict (R, x(:,2:4), 'Trees') %!error ... %! quantilePredict (R, x(:,2:4), 'Mode', 'ensemble') %!error ... %! oobQuantilePredict (R) %!error ... %! oobQuantilePredict (Q, 'UseInstanceForTree', true (150, 2)) %!error ... %! quantileError (R, x) %!error ... %! quantileError (R, x(:,2:4), y) %!error ... %! quantileError (R, x(:,2:4), x(:,1), 'Mode', 'all') %!error ... %! quantileError (R, x(:,2:4), x(:,1), 'Mode', 'individual', ... %! 'TreeWeights', [1, 1]) %!error ... %! oobQuantileError (R) %!error ... %! oobQuantileError (B) %!error ... %! oobQuantileError (Q, 'Weights', ones (150, 1)) %!error ... %! fillprox (B, 'NumPrint', -1) %!error ... %! fillprox (B, 'Trees') %!error ... %! fillprox (B, 'Trees', 9) %!error ... %! fillprox (B, 'Keep', 1) %!error ... %! mdsprox (B) %!error ... %! mdsprox (fillprox (B), 'Keep') %!error ... %! mdsprox (fillprox (B), 'Data', 'proximity') %!error ... %! mdsprox (fillprox (B), 'Keep', 0) %!error ... %! mdsprox (fillprox (B), 'Colors', 1) %!error ... %! mdsprox (fillprox (B), 'MDSCoordinates', [1, 2, 3, 4]) %!error ... %! mdsprox (fillprox (B), 'MDSCoordinates', [1, 500]) %!error ... %! load fisheriris %! TreeBagger (2, meas, species, 'ClassNames', [3, 1, 2]) %!test # classes named as text for numeric labels keep the labels' type %! load fisheriris %! Y = [ones(50, 1); 2 * ones(50, 1); 3 * ones(50, 1)]; %! B = TreeBagger (2, meas, Y, 'ClassNames', {'3', '1', '2'}); %! assert_equal (B.ClassNames, [3; 1; 2]); %!shared X, yb, yr %! k = (0:119)'; %! c = mod (k, 4) + 1; %! j = floor (k / 4); %! x2 = mod (k * 7, 10); %! X = [c, x2]; %! yb = (c == 1) | (c == 2 & mod (j, 4) != 0) | (c == 3 & mod (j, 4) == 0) ... %! | (x2 > 7); %! yr = [3; 1; 4; 1.5]; %! yr = yr(c) + 0.1 * sin (k) + 0.2 * x2; %! Xq = [1, 0; 3, 5; 5, 0; NaN, 2; 2.5, 9]; %!test # every bagged tree takes the categorical predictors and options %! B = TreeBagger (3, X, yb, 'CategoricalPredictors', 1, ... %! 'MaxNumCategories', 3, 'AlgorithmForCategorical', 'pca'); %! assert_equal (B.Trees{1}.CategoricalPredictors, 1); %! assert_equal (any (strcmpi (B.TreeArguments, 'MaxNumCategories')), true); %! assert_equal (any (strcmpi (B.TreeArguments, 'AlgorithmForCategorical')), ... %! true); %! Br = TreeBagger (3, X, yr, 'Method', 'regression', ... %! 'CategoricalPredictors', 1); %! assert_equal (Br.Trees{1}.CategoricalPredictors, 1); %!test # the property records the resolved indices, empty when none is named %! B = TreeBagger (3, X, yb); %! assert_equal (B.CategoricalPredictors, []); %! B = TreeBagger (3, X, yb, 'CategoricalPredictors', 1); %! assert_equal (B.CategoricalPredictors, 1); %!test # a logical vector and 'all' are resolved to indices %! B = TreeBagger (3, X, yb, 'CategoricalPredictors', [true, false]); %! assert_equal (B.CategoricalPredictors, 1); %! B = TreeBagger (3, X, yb, 'CategoricalPredictors', 'all'); %! assert_equal (B.CategoricalPredictors, [1, 2]); %!test # a categorical predictor may be named rather than indexed %! B = TreeBagger (3, X, yb, 'PredictorNames', {'grp', 'val'}, ... %! 'CategoricalPredictors', {'grp'}); %! assert_equal (B.CategoricalPredictors, 1); %! assert_equal (B.Trees{1}.CategoricalPredictors, 1); %!error ... %! TreeBagger (3, X, yb, 'CategoricalPredictors', 3) %!error ... %! append (TreeBagger (3, X, yb, 'CategoricalPredictors', 1), ... %! TreeBagger (3, X, yb)) ## Table input %!test # the predictors and the response come from a table %! load fisheriris %! T = table (meas(:,1), meas(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! T.Species = categorical (species); %! B = TreeBagger (20, T, 'Species'); %! assert_equal (B.PredictorNames, {'SL', 'SW', 'Wide'}); %! assert_equal (B.CategoricalPredictors, 3); %!test # a model formula names the response and the predictors together %! load fisheriris %! T = table (meas(:,1), meas(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = categorical (species); %! B = TreeBagger (20, T, 'Species ~ SW + SL'); %! assert_equal (B.PredictorNames, {'SW', 'SL'}); %!test # predict takes a table, matched by name and not by position %! load fisheriris %! T = table (meas(:,1), meas(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = categorical (species); %! B = TreeBagger (20, T, 'Species'); %! a = predict (B, T); %! assert_equal (predict (B, T(:, [3, 2, 1])), a); %!error ... %! TreeBagger (10, table (rand (6, 1), rand (6, 1)), 'NoSuch') ## A table at margin %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = TreeBagger (20, T, 'Species'); %! a = margin (Mdl, X, y); %! assert_equal (margin (Mdl, T(:,1:2), y), a); %! assert_equal (margin (Mdl, T, 'Species'), a); %! assert_equal (margin (Mdl, T), a); ## A table at error, meanMargin and quantileError %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = TreeBagger (20, T, 'Species'); %! a = error (Mdl, X, y); %! assert_equal (error (Mdl, T(:,1:2), y), a); %! assert_equal (error (Mdl, T, 'Species'), a); %! assert_equal (error (Mdl, T), a); %! assert_equal (error (Mdl, T, 'Mode', 'ensemble'), a(end)); %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,1:2); %! y = categorical (species); %! T = table (X(:,1), X(:,2), 'VariableNames', {'SL', 'SW'}); %! T.Species = y; %! Mdl = TreeBagger (20, T, 'Species'); %! a = meanMargin (Mdl, X, y); %! assert_equal (meanMargin (Mdl, T(:,1:2), y), a); %! assert_equal (meanMargin (Mdl, T, 'Species'), a); %! assert_equal (meanMargin (Mdl, T), a); %!test # the response is named, left out, or given beside the table %! load fisheriris %! X = meas(:,2:4); %! y = meas(:,1); %! T = table (X(:,1), X(:,2), X(:,3), y, ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! Mdl = TreeBagger (20, T, 'SL', 'Method', 'regression'); %! a = quantileError (Mdl, X, y); %! assert_equal (quantileError (Mdl, T(:,1:3), y), a); %! assert_equal (quantileError (Mdl, T, 'SL'), a); %! assert_equal (quantileError (Mdl, T), a); %! assert_equal (quantileError (Mdl, T(:,[3, 1, 2, 4])), a); ## A table at quantilePredict %!test # a table is matched to the predictors by name %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! Mdl = TreeBagger (20, T, 'SL', 'Method', 'regression'); %! a = quantilePredict (Mdl, meas(:,2:4), 'Quantile', [0.25, 0.75]); %! assert_equal (quantilePredict (Mdl, T(:,1:3), 'Quantile', [0.25, 0.75]), a); %! assert_equal (quantilePredict (Mdl, T(:,[4, 3, 1, 2]), ... %! 'Quantile', [0.25, 0.75]), a); ## Observation weights of class single or double %!error ... %! TreeBagger (3, [1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], ... %! 'Weights', int8 ([1; 1; 1; 1])) %!error ... %! TreeBagger (3, [1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], ... %! 'Weights', true (4, 1)) %!error <'Weights' must be a real vector of class single or double.> ... %! error (TreeBagger (3, [1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2]), ... %! [1, 2; 3, 4; 5, 6; 7, 8], [1; 1; 2; 2], 'Weights', ... %! int8 ([1; 1; 1; 1])) %!test %! ## Single weights are stored single, summing to one %! load fisheriris %! w = 1 + (1:150)' / 7; %! Mdl = TreeBagger (5, meas, species, 'Weights', single (w)); %! assert_equal (class (Mdl.W), 'single'); %! assert_equal (sum (double (Mdl.W)), 1, 1e-6); statistics-release-2.1.2/inst/Machine_Learning/designecoc.m000066400000000000000000000313171527311104300237470ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{M} =} designecoc (@var{K}, @var{name}) ## @deftypefnx {statistics} {@var{M} =} designecoc (@dots{}, @qcode{'NumTrials'}, @var{n}) ## ## Coding design matrix for an error correcting output codes model. ## ## @code{@var{M} = designecoc (@var{K}, @var{name})} returns the coding design ## for @var{K} classes named by @var{name}. @var{M} is a @math{KxL} matrix of ## -1, 0 and +1 with one row per class and one column per binary learner: a ## learner is trained to tell the classes marked +1 in its column from those ## marked -1, and a class marked 0 takes no part in it. ## ## @var{K} must be an integer of at least 2. @var{name} must be one of: ## ## @multitable @columnfractions 0.23 0.17 0.60 ## @headitem Design @tab Columns @tab Description ## ## @item @qcode{'onevsone'} @tab @math{K(K-1)/2} @tab One column per pair of ## classes, the earlier class +1 and the later -1, pairs taken in order. ## ## @item @qcode{'onevsall'} @tab @math{K} @tab One column per class, that ## class +1 and every other -1. ## ## @item @qcode{'binarycomplete'} @tab @math{2^(K-1)-1} @tab Every way of ## splitting the classes into two non-empty groups, with the first class ## always +1. ## ## @item @qcode{'ternarycomplete'} @tab @math{(3^K-2^(K+1)+1)/2} @tab Every ## way of splitting into two non-empty groups while leaving any classes out. ## It grows fast: 28501 columns at @math{K = 10}. ## ## @item @qcode{'ordinal'} @tab @math{K-1} @tab Column @math{j} separates the ## first @math{j} classes from the rest, for classes that are ordered. ## ## @item @qcode{'denserandom'} @tab about @math{10log_2 K} @tab Random -1 and ## +1, no class left out. ## ## @item @qcode{'sparserandom'} @tab about @math{15log_2 K} @tab Random -1, 0 ## and +1, a class left out of a column with probability @math{0.5}. ## @end multitable ## ## @code{@var{M} = designecoc (@dots{}, @qcode{'NumTrials'}, @var{n})} draws ## @var{n} random designs and keeps the one whose rows are furthest apart, ## which is what makes a random design correct errors. The default is 10000. ## It is accepted but does nothing for the five designs that are not random. ## ## @math{K = 2} gives the single column @code{[-1; 1]} whatever the design, ## there being only one way to tell two classes apart. ## ## @subheading Deviation from MATLAB ## ## The two random designs cannot be reproduced from MATLAB and neither can ## their width. Measured on R2024a: five runs at @math{K = 10} gave ## @qcode{'denserandom'} 38, 38, 38, 40 and 40 columns and ## @qcode{'sparserandom'} 57, 55, 55 and 54, so the number of columns varies ## between runs of MATLAB itself. The five other designs are exact. ## ## @seealso{fitcecoc, ClassificationECOC} ## @end deftypefn function M = designecoc (K, name, varargin) ## Input validation if (nargin < 2) error ("designecoc: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error ("designecoc: name-value arguments must be in pairs."); endif if (! (isnumeric (K) && isscalar (K) && isreal (K) && K == fix (K) && K >= 2)) error ("designecoc: K must be an integer of at least 2."); endif if (! (ischar (name) && isrow (name))) error ("designecoc: NAME must be a character vector."); endif ## Parse optional paired arguments [NumTrials, args] = parsePairedArguments ({'NumTrials'}, {10000}, ... varargin(:)); ## Validate optional paired arguments if (! (isnumeric (NumTrials) && isscalar (NumTrials) && isreal (NumTrials) && NumTrials == fix (NumTrials) && NumTrials >= 1)) error ("designecoc: 'NumTrials' must be a positive integer."); endif if (! isempty (args)) error ("designecoc: invalid optional paired argument."); endif designs = {'onevsone', 'onevsall', 'binarycomplete', 'ternarycomplete', ... 'ordinal', 'denserandom', 'sparserandom'}; design = tolower (name); if (! any (strcmp (design, designs))) error ("designecoc: '%s' is not a coding design.", name); endif ## Two classes are told apart one way however the design is named, and ## every rule below would otherwise give a wider matrix saying the same ## thing twice. Measured on R2024a. if (K == 2) M = [-1; 1]; return; endif switch (design) case 'onevsone' L = K * (K - 1) / 2; M = zeros (K, L); c = 0; for i = 1:K-1 for j = i+1:K c++; M(i, c) = 1; M(j, c) = -1; endfor endfor case 'onevsall' M = 2 * eye (K) - 1; case 'ordinal' M = ones (K, K - 1); for j = 1:K-1 M(1:j, j) = -1; endfor case 'binarycomplete' ## The first class is +1 throughout, so a column is a subset of the ## other K-1 and the columns run from all but the last down to none, ## the second class being the most significant bit. L = 2 ^ (K - 1) - 1; M = ones (K, L); for c = 1:L m = L - c; for r = 2:K M(r, c) = 2 * bitget (m, K - r + 1) - 1; endfor endfor case 'ternarycomplete' ## A column is taken only one way round, so the last class taking part ## is always +1 and everything after it sits out. For each position of ## that class, the classes before it run through -1, 0 and +1 with the ## first moving fastest, and a column with no -1 in it is not a split. cols = {}; for p = 2:K digits = p - 1; for t = 0:(3 ^ digits - 1) v = zeros (digits, 1); rem_ = t; for d = 1:digits v(d) = mod (rem_, 3) - 1; rem_ = floor (rem_ / 3); endfor if (! any (v == -1)) continue; endif cols{end+1} = [v; 1; zeros(K - p, 1)]; endfor endfor M = cell2mat (cols); otherwise M = randomDesign (K, design, NumTrials); endswitch endfunction ## The two random designs. Columns are drawn at random, those that say ## nothing or repeat another are dropped, and the draw is repeated so that the ## design kept is the one whose rows stand furthest apart, which is what lets ## a random design correct an error. function M = randomDesign (K, design, NumTrials) if (strcmp (design, 'denserandom')) if (K <= 5) warning (strcat ("designecoc: use 'binarycomplete' instead of", ... " 'denserandom' for 5 or fewer classes.")); endif L = ceil (10 * log2 (K)); p0 = 0; else if (K <= 4) warning (strcat ("designecoc: use 'ternarycomplete' instead of", ... " 'sparserandom' for 4 or fewer classes.")); endif L = ceil (15 * log2 (K)); p0 = 0.5; endif M = []; best = -Inf; for t = 1:NumTrials C = sign (rand (K, L) - 0.5); if (p0 > 0) C(rand (K, L) < p0) = 0; endif C = keepUsableColumns (C); if (isempty (C) || any (all (C >= 0, 2)) || any (all (C <= 0, 2))) continue; endif d = minRowDistance (C); if (d > best) best = d; M = C; endif endfor if (isempty (M)) error (strcat ("designecoc: could not draw a usable '%s' design for", ... " %d classes."), design, K); endif endfunction ## A column with no +1 or no -1 trains nothing, and one that repeats another ## up to sign trains the same learner twice. Taking each column with its ## first class on the +1 side makes a column and its mirror the same row, so ## the repeats fall out of one call rather than a comparison of every pair. function C = keepUsableColumns (C) C = C(:, any (C > 0, 1) & any (C < 0, 1)); if (isempty (C)) return; endif [~, first] = max (C != 0, [], 1); lead = C(sub2ind (size (C), first, 1:columns (C))); [~, keep] = unique ((C .* lead)', 'rows', 'first'); C = C(:, sort (keep)); endfunction ## How far apart the two closest rows are, counting a position where one row ## sits out as half a step, which is how an error correcting code is scored. ## Written as one cross product: the distance between rows a and b is ## (L - C(a,:) * C(b,:)') / 2, so the whole table is (L - C*C') / 2. function d = minRowDistance (C) D = (columns (C) - C * C.') / 2; D(logical (eye (rows (C)))) = Inf; d = min (D(:)); endfunction ## Tests %!test # MATLAB parity: the pairwise design, three and four classes %! assert_equal (designecoc (3, 'onevsone'), [1, 1, 0; -1, 0, 1; 0, -1, -1]); %! assert_equal (designecoc (4, 'onevsone'), ... %! [1, 1, 1, 0, 0, 0; -1, 0, 0, 1, 1, 0; ... %! 0, -1, 0, -1, 0, 1; 0, 0, -1, 0, -1, -1]); %!test # MATLAB parity: one class against the rest %! assert_equal (designecoc (3, 'onevsall'), [1, -1, -1; -1, 1, -1; -1, -1, 1]); %! assert_equal (designecoc (5, 'onevsall'), 2 * eye (5) - 1); %!test # MATLAB parity: every split in two, the first class always +1 %! assert_equal (designecoc (3, 'binarycomplete'), ... %! [1, 1, 1; 1, -1, -1; -1, 1, -1]); %! assert_equal (designecoc (4, 'binarycomplete'), ... %! [1, 1, 1, 1, 1, 1, 1; 1, 1, 1, -1, -1, -1, -1; ... %! 1, -1, -1, 1, 1, -1, -1; -1, 1, -1, 1, -1, 1, -1]); %!test # MATLAB parity: ordered classes, cut after each in turn %! assert_equal (designecoc (3, 'ordinal'), [-1, -1; 1, -1; 1, 1]); %! assert_equal (designecoc (4, 'ordinal'), ... %! [-1, -1, -1; 1, -1, -1; 1, 1, -1; 1, 1, 1]); %!test # MATLAB parity: every split with classes left out %! assert_equal (designecoc (3, 'ternarycomplete'), ... %! [-1, -1, 0, 1, -1, -1; 1, -1, -1, -1, 0, 1; ... %! 0, 1, 1, 1, 1, 1]); %!test # MATLAB parity: the widths of the five exact designs %! for K = [4, 5, 6, 10] %! assert_equal (columns (designecoc (K, 'onevsone')), K * (K - 1) / 2); %! assert_equal (columns (designecoc (K, 'onevsall')), K); %! assert_equal (columns (designecoc (K, 'ordinal')), K - 1); %! assert_equal (columns (designecoc (K, 'binarycomplete')), 2 ^ (K - 1) - 1); %! endfor %!test # MATLAB parity: the ternary design grows as its closed form says %! assert_equal (columns (designecoc (6, 'ternarycomplete')), 301); %!test # MATLAB parity: two classes are one column whatever the design %! for d = {'onevsone', 'onevsall', 'binarycomplete', 'ternarycomplete', ... %! 'ordinal'} %! assert_equal (designecoc (2, d{1}), [-1; 1]); %! endfor %!test # a dense random design leaves no class out %! M = designecoc (10, 'denserandom', 'NumTrials', 20); %! assert_equal (rows (M), 10); %! assert_equal (any (M(:) == 0), false); %!test # a sparse random design does leave classes out %! M = designecoc (10, 'sparserandom', 'NumTrials', 20); %! assert_equal (rows (M), 10); %! assert_equal (any (M(:) == 0), true); %!test # every column of a random design trains a real learner %! M = designecoc (8, 'denserandom', 'NumTrials', 20); %! assert_equal (all (any (M > 0, 1) & any (M < 0, 1)), true); %!test # a design is named without regard to case %! assert_equal (designecoc (4, 'OneVsAll'), designecoc (4, 'onevsall')); %!warning ... %! designecoc (4, 'denserandom', 'NumTrials', 5); %!warning ... %! designecoc (4, 'sparserandom', 'NumTrials', 5); ## Test input validation %!error designecoc (3) %!error ... %! designecoc (3, 'onevsone', 'NumTrials') %!error designecoc (1, 'onevsone') %!error designecoc (2.5, 'onevsone') %!error designecoc ('a', 'onevsone') %!error designecoc (3, 42) %!error designecoc (3, 'nosuch') %!error ... %! designecoc (3, 'onevsone', 'NoSuch', 1) %!error ... %! designecoc (3, 'denserandom', 'NumTrials', 0) statistics-release-2.1.2/inst/Machine_Learning/doc-cache000066400000000000000000046017551527311104300232340ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 1495 # name: # type: sq_string # elements: 1 # length: 28 ClassificationBaggedEnsemble # name: # type: sq_string # elements: 1 # length: 808 statistics: ClassificationBaggedEnsemble Bagged ensemble of decision trees for classification A ClassificationBaggedEnsemble object holds decision trees each grown on a sample drawn from the training data in proportion to the observation weights, each split chosen from ceil (sqrt (P)) predictors drawn afresh at every node. It predicts by averaging its trees' class probabilities. Create one with fitcensemble and 'Method' set to 'Bag'. It carries everything a ClassificationEnsemble does, and which rows each tree drew. A boosting method that resamples, asked for with 'Resample', 'FResample' or 'Replace', returns this class too: its trees are boosted and summed by their weights, each grown on the rows it drew. See also: fitcensemble, ClassificationEnsemble, CompactClassificationEnsemble, TreeBagger # name: # type: sq_string # elements: 1 # length: 52 Bagged ensemble of decision trees for classification # name: # type: sq_string # elements: 1 # length: 37 ClassificationBaggedEnsemble.BinEdges # name: # type: sq_string # elements: 1 # length: 143 ClassificationEnsemble: property BinEdges Bin edges of the predictors Always empty, binning not being implemented. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Bin edges of the predictors # name: # type: sq_string # elements: 1 # length: 50 ClassificationBaggedEnsemble.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 186 ClassificationEnsemble: property CategoricalPredictors Indices of categorical predictors The predictors every tree treats as categorical, empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Indices of categorical predictors # name: # type: sq_string # elements: 1 # length: 39 ClassificationBaggedEnsemble.ClassNames # name: # type: sq_string # elements: 1 # length: 243 ClassificationEnsemble: property ClassNames Names of the classes The classes, in the type of the response, sorted or in the order given by the 'ClassNames' option; the columns of the scores take them in that order. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Names of the classes # name: # type: sq_string # elements: 1 # length: 57 ClassificationBaggedEnsemble.ClassificationBaggedEnsemble # name: # type: sq_string # elements: 1 # length: 353 ClassificationBaggedEnsemble: obj = ClassificationBaggedEnsemble (X, Y) ClassificationBaggedEnsemble: obj = ClassificationBaggedEnsemble (..., name, value) Fit a bagged ensemble of decision trees. fitcensemble with 'Method' set to 'Bag' is the documented way in, and its help lists the options both take. See also: fitcensemble, ClassificationEnsemble # name: # type: sq_string # elements: 1 # length: 40 Fit a bagged ensemble of decision trees. # name: # type: sq_string # elements: 1 # length: 43 ClassificationBaggedEnsemble.CombineWeights # name: # type: sq_string # elements: 1 # length: 166 ClassificationEnsemble: property CombineWeights How the weak learners are combined 'WeightedSum' for boosting, 'WeightedAverage' for Bag. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 How the weak learners are combined # name: # type: sq_string # elements: 1 # length: 33 ClassificationBaggedEnsemble.Cost # name: # type: sq_string # elements: 1 # length: 308 ClassificationEnsemble: property Cost Misclassification costs A square matrix, Cost(i,j) being the cost of classifying an observation of class i as class j. A boosting method starts from observation weights multiplied by the total cost of misclassifying each observation's class. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Misclassification costs # name: # type: sq_string # elements: 1 # length: 51 ClassificationBaggedEnsemble.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 161 ClassificationEnsemble: property ExpandedPredictorNames Names of the predictors as the learners saw them The same as PredictorNames. This property is read-only. # name: # type: sq_string # elements: 1 # length: 48 Names of the predictors as the learners saw them # name: # type: sq_string # elements: 1 # length: 38 ClassificationBaggedEnsemble.FResample # name: # type: sq_string # elements: 1 # length: 219 ClassificationBaggedEnsemble: property FResample Share of the observations each tree draws A number greater than 0 and no greater than 1, each tree drawing ceil (FResample * N) observations. This property is read-only. # name: # type: sq_string # elements: 1 # length: 41 Share of the observations each tree draws # name: # type: sq_string # elements: 1 # length: 36 ClassificationBaggedEnsemble.FitInfo # name: # type: sq_string # elements: 1 # length: 445 ClassificationEnsemble: property FitInfo Fit information A column with one element per learner: the weighted classification error for AdaBoostM1, the weighted pseudo-loss for AdaBoostM2 and RUSBoost, and the weighted mean squared error of the regression tree for GentleBoost and LogitBoost. For LPBoost and TotalBoost a matrix with a row per learner: its margin on each observation, and its edge last. Empty for Bag. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Fit information # name: # type: sq_string # elements: 1 # length: 47 ClassificationBaggedEnsemble.FitInfoDescription # name: # type: sq_string # elements: 1 # length: 103 ClassificationEnsemble: property FitInfoDescription Description of FitInfo This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Description of FitInfo # name: # type: sq_string # elements: 1 # length: 62 ClassificationBaggedEnsemble.HyperparameterOptimizationResults # name: # type: sq_string # elements: 1 # length: 192 ClassificationEnsemble: property HyperparameterOptimizationResults Results of optimizing the hyperparameters Always empty, such optimization not being implemented. This property is read-only. # name: # type: sq_string # elements: 1 # length: 41 Results of optimizing the hyperparameters # name: # type: sq_string # elements: 1 # length: 41 ClassificationBaggedEnsemble.LearnerNames # name: # type: sq_string # elements: 1 # length: 156 ClassificationEnsemble: property LearnerNames Names of the weak learners {'Tree'}, or for Subspace {'KNN'} or {'Discriminant'}. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Names of the weak learners # name: # type: sq_string # elements: 1 # length: 35 ClassificationBaggedEnsemble.Method # name: # type: sq_string # elements: 1 # length: 199 ClassificationEnsemble: property Method Ensemble method 'AdaBoostM1', 'AdaBoostM2', 'GentleBoost', 'LogitBoost', 'RUSBoost', 'LPBoost', 'TotalBoost', 'Bag' or 'Subspace'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Ensemble method # name: # type: sq_string # elements: 1 # length: 44 ClassificationBaggedEnsemble.ModelParameters # name: # type: sq_string # elements: 1 # length: 278 ClassificationEnsemble: property ModelParameters Parameters of the fit A structure with the fields Type, Method, LearnerTemplates, the tree template the learners were grown from, NLearn, the number of learning cycles asked for in all, and LearnRate. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Parameters of the fit # name: # type: sq_string # elements: 1 # length: 44 ClassificationBaggedEnsemble.NumObservations # name: # type: sq_string # elements: 1 # length: 105 ClassificationEnsemble: property NumObservations Number of observations used This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Number of observations used # name: # type: sq_string # elements: 1 # length: 39 ClassificationBaggedEnsemble.NumTrained # name: # type: sq_string # elements: 1 # length: 104 ClassificationEnsemble: property NumTrained Number of trained weak learners This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Number of trained weak learners # name: # type: sq_string # elements: 1 # length: 43 ClassificationBaggedEnsemble.PredictorNames # name: # type: sq_string # elements: 1 # length: 135 ClassificationEnsemble: property PredictorNames Names of the predictors A cell array of character vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 34 ClassificationBaggedEnsemble.Prior # name: # type: sq_string # elements: 1 # length: 147 ClassificationEnsemble: property Prior Prior probabilities of the classes A row vector with one probability per class. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Prior probabilities of the classes # name: # type: sq_string # elements: 1 # length: 49 ClassificationBaggedEnsemble.ReasonForTermination # name: # type: sq_string # elements: 1 # length: 138 ClassificationEnsemble: property ReasonForTermination Why the fit stopped adding learners A character vector. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Why the fit stopped adding learners # name: # type: sq_string # elements: 1 # length: 36 ClassificationBaggedEnsemble.Replace # name: # type: sq_string # elements: 1 # length: 150 ClassificationBaggedEnsemble: property Replace Whether the trees draw with replacement A logical scalar, true by default. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Whether the trees draw with replacement # name: # type: sq_string # elements: 1 # length: 41 ClassificationBaggedEnsemble.ResponseName # name: # type: sq_string # elements: 1 # length: 104 ClassificationEnsemble: property ResponseName Name of the response variable This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 37 ClassificationBaggedEnsemble.RowsUsed # name: # type: sq_string # elements: 1 # length: 146 ClassificationEnsemble: property RowsUsed Rows of the data that were used A logical column over the rows as supplied. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Rows of the data that were used # name: # type: sq_string # elements: 1 # length: 43 ClassificationBaggedEnsemble.ScoreTransform # name: # type: sq_string # elements: 1 # length: 140 ClassificationEnsemble: property ScoreTransform Transform applied to the predicted scores See CompactClassificationEnsemble.ScoreTransform. # name: # type: sq_string # elements: 1 # length: 41 Transform applied to the predicted scores # name: # type: sq_string # elements: 1 # length: 36 ClassificationBaggedEnsemble.Trained # name: # type: sq_string # elements: 1 # length: 187 ClassificationEnsemble: property Trained Trained weak learners A column cell array of compact trees, as described under CompactClassificationEnsemble.Trained. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Trained weak learners # name: # type: sq_string # elements: 1 # length: 43 ClassificationBaggedEnsemble.TrainedWeights # name: # type: sq_string # elements: 1 # length: 151 ClassificationEnsemble: property TrainedWeights Weights of the trained weak learners A column with one weight per learner. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Weights of the trained weak learners # name: # type: sq_string # elements: 1 # length: 45 ClassificationBaggedEnsemble.UseObsForLearner # name: # type: sq_string # elements: 1 # length: 199 ClassificationBaggedEnsemble: property UseObsForLearner Which observations each tree drew An NxNumTrained logical matrix, true where a tree's sample holds an observation. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Which observations each tree drew # name: # type: sq_string # elements: 1 # length: 46 ClassificationBaggedEnsemble.UsePredForLearner # name: # type: sq_string # elements: 1 # length: 260 ClassificationEnsemble: property UsePredForLearner Which predictors each learner uses For the 'Subspace' method, a logical matrix with one row per predictor and one column per learner. Empty for tree learners, as MATLAB returns it. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Which predictors each learner uses # name: # type: sq_string # elements: 1 # length: 30 ClassificationBaggedEnsemble.W # name: # type: sq_string # elements: 1 # length: 276 ClassificationEnsemble: property W Observation weights The weights given, scaled so that the observations of each class sum to its prior probability. A cost matrix is not folded into them. It has the class of the 'Weights' given, single or double. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 30 ClassificationBaggedEnsemble.X # name: # type: sq_string # elements: 1 # length: 393 ClassificationEnsemble: property X Predictor data The predictors the ensemble was fitted on, one row per observation, a row missing a value or a class having been left out. This property is read-only. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 30 ClassificationBaggedEnsemble.Y # name: # type: sq_string # elements: 1 # length: 147 ClassificationEnsemble: property Y Class labels The labels the ensemble was fitted on, in the type they were given in. This property is read-only. # name: # type: sq_string # elements: 1 # length: 12 Class labels # name: # type: sq_string # elements: 1 # length: 36 ClassificationBaggedEnsemble.compact # name: # type: sq_string # elements: 1 # length: 250 ClassificationBaggedEnsemble: CMdl = compact (obj) Drop the training data from a bagged ensemble. Returns a CompactClassificationEnsemble, as ClassificationEnsemble.compact does. See also: ClassificationBaggedEnsemble, CompactClassificationEnsemble # name: # type: sq_string # elements: 1 # length: 46 Drop the training data from a bagged ensemble. # name: # type: sq_string # elements: 1 # length: 37 ClassificationBaggedEnsemble.crossval # name: # type: sq_string # elements: 1 # length: 319 ClassificationBaggedEnsemble: CVMdl = crossval (obj) ClassificationBaggedEnsemble: CVMdl = crossval (..., name, value) Cross-validate a bagged ensemble. Behaves as ClassificationEnsemble.crossval, returning a ClassificationPartitionedEnsemble. See also: ClassificationBaggedEnsemble, ClassificationPartitionedEnsemble # name: # type: sq_string # elements: 1 # length: 33 Cross-validate a bagged ensemble. # name: # type: sq_string # elements: 1 # length: 33 ClassificationBaggedEnsemble.edge # name: # type: sq_string # elements: 1 # length: 728 ClassificationBaggedEnsemble: e = edge (obj, X, Y) ClassificationBaggedEnsemble: e = edge (..., name, value) ClassificationBaggedEnsemble: e = edge (obj, Tbl, ResponseVarName) ClassificationBaggedEnsemble: e = edge (obj, Tbl) Classification edge of a bagged ensemble. Behaves as CompactClassificationEnsemble.edge. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: ClassificationBaggedEnsemble, CompactClassificationEnsemble.edge # name: # type: sq_string # elements: 1 # length: 41 Classification edge of a bagged ensemble. # name: # type: sq_string # elements: 1 # length: 33 ClassificationBaggedEnsemble.loss # name: # type: sq_string # elements: 1 # length: 728 ClassificationBaggedEnsemble: L = loss (obj, X, Y) ClassificationBaggedEnsemble: L = loss (obj, Tbl, ResponseVarName) ClassificationBaggedEnsemble: L = loss (obj, Tbl) ClassificationBaggedEnsemble: L = loss (..., name, value) Classification loss of a bagged ensemble. Behaves as CompactClassificationEnsemble.loss. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: ClassificationBaggedEnsemble, CompactClassificationEnsemble.loss # name: # type: sq_string # elements: 1 # length: 41 Classification loss of a bagged ensemble. # name: # type: sq_string # elements: 1 # length: 35 ClassificationBaggedEnsemble.margin # name: # type: sq_string # elements: 1 # length: 747 ClassificationBaggedEnsemble: m = margin (obj, X, Y) ClassificationBaggedEnsemble: m = margin (..., name, value) ClassificationBaggedEnsemble: m = margin (obj, Tbl, ResponseVarName) ClassificationBaggedEnsemble: m = margin (obj, Tbl) Classification margins of a bagged ensemble. Behaves as CompactClassificationEnsemble.margin. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: ClassificationBaggedEnsemble, CompactClassificationEnsemble.margin # name: # type: sq_string # elements: 1 # length: 44 Classification margins of a bagged ensemble. # name: # type: sq_string # elements: 1 # length: 36 ClassificationBaggedEnsemble.oobEdge # name: # type: sq_string # elements: 1 # length: 328 ClassificationBaggedEnsemble: e = oobEdge (obj) ClassificationBaggedEnsemble: e = oobEdge (..., name, value) Out-of-bag classification edge. The weighted mean of the out-of-bag margins, weighted by W. 'Mode' and 'Learners' are taken as by oobLoss. See also: ClassificationBaggedEnsemble, ClassificationBaggedEnsemble.oobMargin # name: # type: sq_string # elements: 1 # length: 31 Out-of-bag classification edge. # name: # type: sq_string # elements: 1 # length: 36 ClassificationBaggedEnsemble.oobLoss # name: # type: sq_string # elements: 1 # length: 455 ClassificationBaggedEnsemble: L = oobLoss (obj) ClassificationBaggedEnsemble: L = oobLoss (..., name, value) Out-of-bag classification loss. The loss of the out-of-bag scores against Y, weighted by W, an observation in the sample of every tree used being left out. 'LossFun' and 'Mode' are taken as by CompactClassificationEnsemble.loss, and 'Learners' restricts the trees. See also: ClassificationBaggedEnsemble, ClassificationBaggedEnsemble.oobPredict # name: # type: sq_string # elements: 1 # length: 31 Out-of-bag classification loss. # name: # type: sq_string # elements: 1 # length: 38 ClassificationBaggedEnsemble.oobMargin # name: # type: sq_string # elements: 1 # length: 345 ClassificationBaggedEnsemble: m = oobMargin (obj) ClassificationBaggedEnsemble: m = oobMargin (..., 'Learners', idx) Out-of-bag classification margins. The margin of each training observation under its out-of-bag scores, NaN for one in the sample of every tree used. See also: ClassificationBaggedEnsemble, ClassificationBaggedEnsemble.oobEdge # name: # type: sq_string # elements: 1 # length: 34 Out-of-bag classification margins. # name: # type: sq_string # elements: 1 # length: 59 ClassificationBaggedEnsemble.oobPermutedPredictorImportance # name: # type: sq_string # elements: 1 # length: 664 ClassificationBaggedEnsemble: imp = oobPermutedPredictorImportance (obj) ClassificationBaggedEnsemble: imp = oobPermutedPredictorImportance (..., 'Learners', idx) Out-of-bag predictor importance by permutation. For each tree, the values of each predictor are permuted among the observations out of its bag, and the tree's misclassification rate on them, weighted by W, is taken before and after. imp holds, for each predictor, the mean of the rise over the trees divided by its standard deviation over the trees, zero where the mean is zero. 'Learners' restricts the trees. See also: ClassificationBaggedEnsemble, ClassificationBaggedEnsemble.predictorImportance # name: # type: sq_string # elements: 1 # length: 47 Out-of-bag predictor importance by permutation. # name: # type: sq_string # elements: 1 # length: 39 ClassificationBaggedEnsemble.oobPredict # name: # type: sq_string # elements: 1 # length: 621 ClassificationBaggedEnsemble: label = oobPredict (obj) ClassificationBaggedEnsemble: [label, scores] = oobPredict (...) ClassificationBaggedEnsemble: [...] = oobPredict (..., 'Learners', idx) Out-of-bag predictions for the training data. Each training observation is classified by the trees whose samples left it out, as predict classifies it with 'UseObsForLearner' set to ! UseObsForLearner. An observation in the sample of every tree used has NaN scores and is given the class of greatest prior probability. 'Learners' restricts the trees. See also: ClassificationBaggedEnsemble, ClassificationBaggedEnsemble.oobLoss # name: # type: sq_string # elements: 1 # length: 45 Out-of-bag predictions for the training data. # name: # type: sq_string # elements: 1 # length: 36 ClassificationBaggedEnsemble.predict # name: # type: sq_string # elements: 1 # length: 353 ClassificationBaggedEnsemble: label = predict (obj, X) ClassificationBaggedEnsemble: [label, scores] = predict (...) ClassificationBaggedEnsemble: [...] = predict (..., name, value) Classify new data with a bagged ensemble. Behaves as CompactClassificationEnsemble.predict. See also: ClassificationBaggedEnsemble, CompactClassificationEnsemble.predict # name: # type: sq_string # elements: 1 # length: 41 Classify new data with a bagged ensemble. # name: # type: sq_string # elements: 1 # length: 48 ClassificationBaggedEnsemble.predictorImportance # name: # type: sq_string # elements: 1 # length: 400 ClassificationBaggedEnsemble: imp = predictorImportance (obj) ClassificationBaggedEnsemble: [imp, ma] = predictorImportance (obj) Estimate the importance of each predictor. The mean over the trees of each tree's predictorImportance, as CompactClassificationEnsemble.predictorImportance computes it. See also: ClassificationBaggedEnsemble, ClassificationBaggedEnsemble.oobPermutedPredictorImportance # name: # type: sq_string # elements: 1 # length: 42 Estimate the importance of each predictor. # name: # type: sq_string # elements: 1 # length: 38 ClassificationBaggedEnsemble.resubEdge # name: # type: sq_string # elements: 1 # length: 295 ClassificationBaggedEnsemble: e = resubEdge (obj) ClassificationBaggedEnsemble: e = resubEdge (..., name, value) Classification edge of a bagged ensemble on the training data. Behaves as ClassificationEnsemble.resubEdge. See also: ClassificationBaggedEnsemble, ClassificationEnsemble.resubEdge # name: # type: sq_string # elements: 1 # length: 62 Classification edge of a bagged ensemble on the training data. # name: # type: sq_string # elements: 1 # length: 38 ClassificationBaggedEnsemble.resubLoss # name: # type: sq_string # elements: 1 # length: 295 ClassificationBaggedEnsemble: L = resubLoss (obj) ClassificationBaggedEnsemble: L = resubLoss (..., name, value) Classification loss of a bagged ensemble on the training data. Behaves as ClassificationEnsemble.resubLoss. See also: ClassificationBaggedEnsemble, ClassificationEnsemble.resubLoss # name: # type: sq_string # elements: 1 # length: 62 Classification loss of a bagged ensemble on the training data. # name: # type: sq_string # elements: 1 # length: 40 ClassificationBaggedEnsemble.resubMargin # name: # type: sq_string # elements: 1 # length: 306 ClassificationBaggedEnsemble: m = resubMargin (obj) ClassificationBaggedEnsemble: m = resubMargin (..., name, value) Classification margins of the training data of a bagged ensemble. Behaves as ClassificationEnsemble.resubMargin. See also: ClassificationBaggedEnsemble, ClassificationEnsemble.resubMargin # name: # type: sq_string # elements: 1 # length: 65 Classification margins of the training data of a bagged ensemble. # name: # type: sq_string # elements: 1 # length: 41 ClassificationBaggedEnsemble.resubPredict # name: # type: sq_string # elements: 1 # length: 300 ClassificationBaggedEnsemble: label = resubPredict (obj) ClassificationBaggedEnsemble: [label, scores] = resubPredict (...) Classify the training data with a bagged ensemble. Behaves as ClassificationEnsemble.resubPredict. See also: ClassificationBaggedEnsemble, ClassificationEnsemble.resubPredict # name: # type: sq_string # elements: 1 # length: 50 Classify the training data with a bagged ensemble. # name: # type: sq_string # elements: 1 # length: 35 ClassificationBaggedEnsemble.resume # name: # type: sq_string # elements: 1 # length: 327 ClassificationBaggedEnsemble: B = resume (obj, NumLearningCycles) ClassificationBaggedEnsemble: B = resume (..., 'NPrint', n) Grow more trees in a bagged ensemble. Behaves as ClassificationEnsemble.resume, the new trees' samples added to UseObsForLearner. See also: ClassificationBaggedEnsemble, ClassificationEnsemble.resume # name: # type: sq_string # elements: 1 # length: 37 Grow more trees in a bagged ensemble. # name: # type: sq_string # elements: 1 # length: 26 ClassificationDiscriminant # name: # type: sq_string # elements: 1 # length: 1440 statistics: ClassificationDiscriminant Discriminant analysis classification The ClassificationDiscriminant class implements a discriminant analysis classifier object, which can predict responses for new data using the predict method. Discriminant analysis classification is a statistical method used to classify observations into predefined groups based on their characteristics. It estimates the parameters of different distributions for each class and predicts the class of new observations by finding the one with the smallest misclassification cost. Create a ClassificationDiscriminant object by using the fitcdiscr function or the class constructor. Six discriminant types are available, in two families. The linear family, 'linear', 'diagLinear' and 'pseudoLinear', pools one covariance across the classes and separates them with a hyperplane. The quadratic family, 'quadratic', 'diagQuadratic' and 'pseudoQuadratic', estimates a covariance per class and separates them with a quadric. A 'diag' type keeps only the variances, which is the same model as a Gamma of 1, and a 'pseudo' type inverts a singular covariance rather than refusing it. DiscrimType may be assigned after fitting, but only within its own family: the family is fixed when the model is fitted, because it decides which covariances the fit has to estimate. Assigning it, or Gamma, re-derives Sigma, LogDetSigma and Coeffs without refitting. See also: fitcdiscr # name: # type: sq_string # elements: 1 # length: 36 Discriminant analysis classification # name: # type: sq_string # elements: 1 # length: 39 ClassificationDiscriminant.BetweenSigma # name: # type: sq_string # elements: 1 # length: 742 ClassificationDiscriminant: property BetweenSigma Between-class covariance matrix A P-by-P matrix holding the covariance of the class means about the overall mean, weighted by how many observations each class contributes. With n_k observations in class k, p_k = n_k / n and \bar{\mu} = \sum_k p_k \mu_k, it is BetweenSigma = sum_k n_k (Mu(k,:) - mubar)' * (Mu(k,:) - mubar) / (n * (1 - sum_k p_k^2)) The denominator is the unbiased one for a weighted covariance, so a balanced fit divides by n (K-1) / K. It reads the class sizes, not Prior: assigning a prior leaves it where it was. It is estimated for every discriminant type, the quadratic family included, since it describes the classes rather than the fit. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Between-class covariance matrix # name: # type: sq_string # elements: 1 # length: 35 ClassificationDiscriminant.BinEdges # name: # type: sq_string # elements: 1 # length: 361 ClassificationDiscriminant: property BinEdges Bin edges of the predictors A cell array with one entry per predictor, holding that predictor's bin edges where the learner discretized it before fitting. It is empty here and stays empty: this learner fits the predictors as they are, and MATLAB's reports an empty cell for it as well. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Bin edges of the predictors # name: # type: sq_string # elements: 1 # length: 48 ClassificationDiscriminant.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 237 ClassificationDiscriminant: property CategoricalPredictors Indices of the categorical predictors A numeric vector of column indices into X naming the predictors treated as categorical, and empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 37 ClassificationDiscriminant.ClassNames # name: # type: sq_string # elements: 1 # length: 408 ClassificationDiscriminant: property ClassNames Names of classes in the response variable An array of unique values of the response variable Y, which has the same data types as the data in Y, sorted or in the order given by the 'ClassNames' option. This property is read-only. ClassNames can have any of the following datatypes: Cell array of character vectors Character array Logical vector Numeric vector # name: # type: sq_string # elements: 1 # length: 41 Names of classes in the response variable # name: # type: sq_string # elements: 1 # length: 53 ClassificationDiscriminant.ClassificationDiscriminant # name: # type: sq_string # elements: 1 # length: 3808 ClassificationDiscriminant: obj = ClassificationDiscriminant (X, Y) ClassificationDiscriminant: obj = ClassificationDiscriminant (Tbl, ResponseVarName) ClassificationDiscriminant: obj = ClassificationDiscriminant (Tbl, formula) ClassificationDiscriminant: obj = ClassificationDiscriminant (Tbl, Y) ClassificationDiscriminant: obj = ClassificationDiscriminant (..., name, value) Create a ClassificationDiscriminant class object containing a discriminant analysis model. obj = ClassificationDiscriminant (X, Y) returns a ClassificationDiscriminant object, with X as the predictor data and Y containing the class labels of observations in X. X must be a NxP numeric matrix of input data where rows correspond to observations and columns correspond to features or variables. X will be used to train the discriminant model. Y is Nx1 matrix or cell matrix containing the class labels of corresponding predictor data in X. Y can contain any type of categorical data. Y must have the same number of rows as X. obj = ClassificationDiscriminant (..., name, value) returns a ClassificationDiscriminant object with parameters specified by the following name, value paired input arguments: Name Value 'PredictorNames' A cell array of character vectors specifying the names of the predictors. The length of this array must match the number of columns in X. 'ResponseName' A character vector specifying the name of the response variable. 'ClassNames' Names of the classes in the class labels, Y, used for fitting the Discriminant model. ClassNames are of the same type as the class labels in Y. 'Cost' An NxR numeric matrix containing misclassification cost for the corresponding instances in X, where R is the number of unique categories in Y. If an instance is correctly classified into its category the cost is calculated to be 1, otherwise 0. The cost matrix can be altered by using Mdl.cost = somecost. By default, its value is cost = ones (rows (X), numel (unique (Y))). 'Prior' A numeric vector specifying the prior probabilities for each class. The order of the elements in Prior corresponds to the order of the classes in ClassNames. Alternatively, you can specify 'empirical' to use the empirical class probabilities or 'uniform' to assume equal class probabilities. 'Weights' A single or double vector of nonnegative observation weights, one per row of X. They weigh the class means and covariances, the covariances being unbiased for them, and an empirical prior sums them per class. Only their proportions matter, and a row of zero weight is left out of the fit. The model's W keeps the class of the weights, while every computation runs in double, so Prior and the predictions are double where MATLAB returns single. 'ScoreTransform' A user-defined function handle or a character vector specifying one of the following builtin functions specifying the transformation applied to predicted classification scores. Supported values include 'doublelogit', 'invlogit', 'ismax', 'logit', 'none', 'identity', 'sign', 'symmetric', 'symmetricismax', and 'symmetriclogit'. 'DiscrimType' A character vector or string scalar specifying the type of discriminant analysis to perform. The only supported value is 'linear'. 'FillCoeffs' A character vector or string scalar with values 'on' or 'off' specifying whether to fill the coefficients after fitting. If set to 'on', the coefficients are computed during model fitting, which can be useful for prediction. 'Gamma' A numeric scalar specifying the regularization parameter for the covariance matrix. It adjusts the linear discriminant analysis to make the model more stable in the presence of multicollinearity or small sample sizes. A value of 0 corresponds to no regularization, while a value of 1 corresponds to a completely regularized model. See also: fitcdiscr # name: # type: sq_string # elements: 1 # length: 90 Create a ClassificationDiscriminant class object containing a discriminant analysis model. # name: # type: sq_string # elements: 1 # length: 33 ClassificationDiscriminant.Coeffs # name: # type: sq_string # elements: 1 # length: 859 ClassificationDiscriminant: property Coeffs Coefficient matrices A KxK structure containing the coefficient matrices, where K is the number of classes. If the 'FillCoeffs' parameter was set to 'off' in either the fitcdiscr function or the ClassificationDiscriminant constructor, then Coeffs is empty ([]). This property is read-only. Coeffs(i,j) contains the coefficients of the boundary between the classes i and j in the following fields: DiscrimType - A character vector Class1 - ClassNames(i) Class2 - ClassNames(j) Const - A scalar Linear - A vector with length as the number of predictors. Quadratic - The quadratic family only. A PxP matrix, or a 1xP vector for 'diagQuadratic', following the shape of Sigma. The diagonal entries carry the two class names and nothing else. The structure is rebuilt whenever DiscrimType, Gamma or Prior is assigned. # name: # type: sq_string # elements: 1 # length: 20 Coefficient matrices # name: # type: sq_string # elements: 1 # length: 31 ClassificationDiscriminant.Cost # name: # type: sq_string # elements: 1 # length: 1197 ClassificationDiscriminant: property Cost Cost of Misclassification A square matrix specifying the cost of misclassification of a point. Cost(i,j) is the cost of classifying a point into class j if its true class is i (that is, the rows correspond to the true class and the columns correspond to the predicted class). The order of the rows and columns in Cost corresponds to the order of the classes in ClassNames. The number of rows and columns in Cost is the number of unique classes in the response. By default, Cost(i,j) = 1 if i != j, and Cost(i,j) = 0 if i = j. In other words, the cost is 0 for correct classification and 1 for incorrect classification. Add or change the Cost property using dot notation as in: obj.Cost = costMatrix A cost may also be given as a struct with the fields ClassNames and ClassificationCosts, which names the order its own matrix is written in. That matrix is permuted into the order of ClassNames above, so a caller need not know which order the classes were sorted into. It must name every class. A cost must be floating point, not sparse, not complex, non-negative and zero down its diagonal, and must hold no NaN or Inf. A single is widened to double. # name: # type: sq_string # elements: 1 # length: 25 Cost of Misclassification # name: # type: sq_string # elements: 1 # length: 32 ClassificationDiscriminant.Delta # name: # type: sq_string # elements: 1 # length: 807 ClassificationDiscriminant: property Delta Delta threshold for the linear coefficients A nonnegative scalar that eliminates predictors. A per-class linear coefficient is set to zero when it falls below Delta, and the comparison is made on the standardized coefficient, the coefficient times the within-class standard deviation of its predictor. Scaling matters here: a threshold on the raw coefficients would depend on the units each predictor is measured in, so the same model in centimetres and in metres would drop different predictors. DeltaPredictor reports, per predictor, the value at which it drops out of every class at once. It applies to the linear family only, a quadratic discriminant having no linear coefficients to eliminate. Assigning it rebuilds Coeffs and changes what predict answers. # name: # type: sq_string # elements: 1 # length: 43 Delta threshold for the linear coefficients # name: # type: sq_string # elements: 1 # length: 41 ClassificationDiscriminant.DeltaPredictor # name: # type: sq_string # elements: 1 # length: 475 ClassificationDiscriminant: property DeltaPredictor Minimum Delta at which each predictor drops out A row vector with one entry per predictor, the value of Delta at which that predictor's coefficient is zero for every class and the predictor leaves the model altogether. It is all zeros for the quadratic family, which has no linear coefficients to eliminate. This property is read-only, and it describes the fit rather than the threshold: assigning Delta does not move it. # name: # type: sq_string # elements: 1 # length: 47 Minimum Delta at which each predictor drops out # name: # type: sq_string # elements: 1 # length: 38 ClassificationDiscriminant.DiscrimType # name: # type: sq_string # elements: 1 # length: 786 ClassificationDiscriminant: property DiscrimType Discriminant type A character vector naming the discriminant model, one of 'linear', 'quadratic', 'diagLinear', 'diagQuadratic', 'pseudoLinear' or 'pseudoQuadratic'. A linear type pools one covariance across the classes; a quadratic type estimates one per class. A 'diag' type keeps only the variances, and a 'pseudo' type inverts a singular covariance instead of refusing it. This property may be assigned, but only within its own family: the three linear types interchange freely and so do the three quadratic ones, while no assignment moves a model between the two. The family is fixed when the model is fitted, because it decides which covariances the fit has to estimate. Assigning re-derives Sigma, LogDetSigma, Gamma and Coeffs. # name: # type: sq_string # elements: 1 # length: 17 Discriminant type # name: # type: sq_string # elements: 1 # length: 49 ClassificationDiscriminant.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 270 ClassificationDiscriminant: property ExpandedPredictorNames Names of the predictors as the model expanded them A cell array of character vectors. It matches PredictorNames unless a categorical predictor was expanded into indicator variables. This property is read-only. # name: # type: sq_string # elements: 1 # length: 50 Names of the predictors as the model expanded them # name: # type: sq_string # elements: 1 # length: 32 ClassificationDiscriminant.Gamma # name: # type: sq_string # elements: 1 # length: 509 ClassificationDiscriminant: property Gamma Gamma regularization parameter A scalar from 0 to 1 shrinking the covariance towards its diagonal. Gamma and DiscrimType are one state: a value of 1 is the diagonal type, so assigning it renames DiscrimType to 'diagLinear' or 'diagQuadratic', and assigning a diagonal type sets Gamma to 1. The quadratic family admits 0 and 1 only. A value below MinGamma is refused, since it would leave the covariance singular. Assigning re-derives Sigma, LogDetSigma and Coeffs. # name: # type: sq_string # elements: 1 # length: 30 Gamma regularization parameter # name: # type: sq_string # elements: 1 # length: 60 ClassificationDiscriminant.HyperparameterOptimizationResults # name: # type: sq_string # elements: 1 # length: 368 ClassificationDiscriminant: property HyperparameterOptimizationResults Results of the hyperparameter optimization Always empty. It is declared for MATLAB compatibility, where it holds what an automatic search over the hyperparameters found. This class fits the parameters it is given and runs no such search, so there is nothing to report. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Results of the hyperparameter optimization # name: # type: sq_string # elements: 1 # length: 38 ClassificationDiscriminant.LogDetSigma # name: # type: sq_string # elements: 1 # length: 617 ClassificationDiscriminant: property LogDetSigma Logarithm of the determinant of the within-class covariance matrix A scalar for the linear family and a Kx1 vector for the quadratic one, one entry per class. It is computed in correlation space, as the sum of the logarithms of the predictor variances plus the log determinant of the correlation matrix, which is far better conditioned than the covariance when the data are nearly collinear. A predictor with no variance contributes nothing rather than an infinity, and the 'pseudo' types sum only over the directions that carry variance. This property is read-only. # name: # type: sq_string # elements: 1 # length: 66 Logarithm of the determinant of the within-class covariance matrix # name: # type: sq_string # elements: 1 # length: 35 ClassificationDiscriminant.MinGamma # name: # type: sq_string # elements: 1 # length: 443 ClassificationDiscriminant: property MinGamma Minimum value for the Gamma regularization parameter A scalar from 0 to 1, the least regularization that leaves the correlation matrix invertible. It is 0 when the matrix is already invertible, and positive when the predictors are collinear, in which case a plain 'linear' or 'quadratic' fit is raised to it rather than failing. Assigning a Gamma below it is refused. This property is read-only. # name: # type: sq_string # elements: 1 # length: 52 Minimum value for the Gamma regularization parameter # name: # type: sq_string # elements: 1 # length: 42 ClassificationDiscriminant.ModelParameters # name: # type: sq_string # elements: 1 # length: 471 ClassificationDiscriminant: property ModelParameters Fitting options, as they were given A structure holding the parameters of the fit: DiscrimType, Gamma, Delta, FillCoeffs, and the Version, Method and Type tags. MATLAB reports a SaveMemory field beside these. This class has no such option and always stores the full covariance, so there is no setting to report and the field is absent rather than answering for a knob that does not exist. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Fitting options, as they were given # name: # type: sq_string # elements: 1 # length: 29 ClassificationDiscriminant.Mu # name: # type: sq_string # elements: 1 # length: 263 ClassificationDiscriminant: property Mu Class means A KxP numeric matrix specifying the mean of the multivariate normal distribution of each corresponding class, where K is the number of classes and P is the number of predictors in X. This property is read-only. # name: # type: sq_string # elements: 1 # length: 11 Class means # name: # type: sq_string # elements: 1 # length: 42 ClassificationDiscriminant.NumObservations # name: # type: sq_string # elements: 1 # length: 247 ClassificationDiscriminant: property NumObservations Number of observations A positive integer value specifying the number of observations in the training dataset used for training the ClassificationDiscriminant model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 40 ClassificationDiscriminant.NumPredictors # name: # type: sq_string # elements: 1 # length: 241 ClassificationDiscriminant: property NumPredictors Number of predictors A positive integer value specifying the number of predictors in the training dataset used for training the ClassificationDiscriminant model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of predictors # name: # type: sq_string # elements: 1 # length: 41 ClassificationDiscriminant.PredictorNames # name: # type: sq_string # elements: 1 # length: 265 ClassificationDiscriminant: property PredictorNames Names of predictor variables A cell array of character vectors specifying the names of the predictor variables. The names are in the order in which they appear in the training dataset. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Names of predictor variables # name: # type: sq_string # elements: 1 # length: 32 ClassificationDiscriminant.Prior # name: # type: sq_string # elements: 1 # length: 613 ClassificationDiscriminant: property Prior Prior probability for each class A numeric vector specifying the prior probabilities for each class. The order of the elements in Prior corresponds to the order of the classes in ClassNames. Add or change the Prior property using dot notation as in: obj.Prior = priorVector Specified as a row vector with one entry per class, in the order of ClassNames, and rescaled to sum to one. It may be given as 'empirical', 'uniform', a numeric vector, or a structure with ClassNames and ClassProbs fields, which assigns each probability by class name rather than by position. # name: # type: sq_string # elements: 1 # length: 32 Prior probability for each class # name: # type: sq_string # elements: 1 # length: 39 ClassificationDiscriminant.ResponseName # name: # type: sq_string # elements: 1 # length: 168 ClassificationDiscriminant: property ResponseName Response variable name A character vector specifying the name of the response variable Y. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Response variable name # name: # type: sq_string # elements: 1 # length: 35 ClassificationDiscriminant.RowsUsed # name: # type: sq_string # elements: 1 # length: 401 ClassificationDiscriminant: property RowsUsed Rows used for fitting A logical column vector with the same length as the observations in the original predictor data X, true for each row that was used for fitting the ClassificationDiscriminant model. It is empty, [], when every observation was used, so a non-empty value means that rows holding missing values were dropped. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Rows used for fitting # name: # type: sq_string # elements: 1 # length: 41 ClassificationDiscriminant.ScoreTransform # name: # type: sq_string # elements: 1 # length: 991 ClassificationDiscriminant: property ScoreTransform Transformation function for classification scores Specified as a function handle for transforming the classification scores. Add or change the ScoreTransform property using dot notation as in: obj.ScoreTransform = 'function_name' obj.ScoreTransform = @function_handle When specified as a character vector, it can be any of the following built-in functions. Nevertheless, the ScoreTransform property always stores their function handle equivalent. Value Description 'doublelogit' 1 ./ (1 + exp (-2 x x)) 'invlogit' log (x ./ (1 - x)) 'ismax' Sets the score for the class with the largest score to 1, and for all other classes to 0 'logit' 1 ./ (1 + exp (-x)) 'none' x (no transformation) 'identity' x (no transformation) 'sign' -1 for x < 0, 0 for x = 0, 1 for x > 0 'symmetric' 2 x x - 1 'symmetricismax' Sets the score for the class with the largest score to 1, and for all other classes to -1 'symmetriclogit' 2 ./ (1 + exp (-x)) - 1 # name: # type: sq_string # elements: 1 # length: 49 Transformation function for classification scores # name: # type: sq_string # elements: 1 # length: 32 ClassificationDiscriminant.Sigma # name: # type: sq_string # elements: 1 # length: 508 ClassificationDiscriminant: property Sigma Within-class covariance A numeric array whose shape follows DiscrimType, with P predictors and K classes: DiscrimType Sigma LogDetSigma 'linear', 'pseudoLinear' PxP scalar 'quadratic', 'pseudoQuadratic' PxPxK Kx1 'diagLinear' 1xP scalar 'diagQuadratic' 1xPxK Kx1 The linear family pools one covariance across the classes and the quadratic family estimates one per class. This property is read-only, but it is re-derived whenever DiscrimType or Gamma is assigned. # name: # type: sq_string # elements: 1 # length: 23 Within-class covariance # name: # type: sq_string # elements: 1 # length: 28 ClassificationDiscriminant.W # name: # type: sq_string # elements: 1 # length: 311 ClassificationDiscriminant: property W Observation weights A numeric column vector with one entry per observation used for fitting: the 'Weights' given, or equal weights, scaled to sum to one. The prior does not enter them. It has the class of the 'Weights' given, single or double. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 28 ClassificationDiscriminant.X # name: # type: sq_string # elements: 1 # length: 432 ClassificationDiscriminant: property X Predictor data A numeric matrix containing the unstandardized predictor data. Each column of X represents one predictor (variable), and each row represents one observation. This property is read-only. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 36 ClassificationDiscriminant.XCentered # name: # type: sq_string # elements: 1 # length: 216 ClassificationDiscriminant: property XCentered Predictor data with class means subtracted A matrix of the same size as X and the values in X with the corresponding class means subtracted. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Predictor data with class means subtracted # name: # type: sq_string # elements: 1 # length: 28 ClassificationDiscriminant.Y # name: # type: sq_string # elements: 1 # length: 316 ClassificationDiscriminant: property Y Class labels Specified as a logical or numeric column vector, or as a character array or a cell array of character vectors with the same number of rows as the predictor data. Each row in Y is the observed class label for the corresponding row in X. This property is read-only. # name: # type: sq_string # elements: 1 # length: 12 Class labels # name: # type: sq_string # elements: 1 # length: 34 ClassificationDiscriminant.compact # name: # type: sq_string # elements: 1 # length: 280 ClassificationDiscriminant: CVMdl = compact (obj) Create a CompactClassificationDiscriminant object. CVMdl = compact (obj) creates a compact version of the ClassificationDiscriminant object, obj. See also: fitcdiscr, ClassificationDiscriminant, CompactClassificationDiscriminant # name: # type: sq_string # elements: 1 # length: 50 Create a CompactClassificationDiscriminant object. # name: # type: sq_string # elements: 1 # length: 35 ClassificationDiscriminant.crossval # name: # type: sq_string # elements: 1 # length: 1027 ClassificationDiscriminant: CVMdl = crossval (obj) ClassificationDiscriminant: CVMdl = crossval (..., Name, Value) Cross Validate a Discriminant classification object. CVMdl = crossval (obj) returns a cross-validated model object, CVMdl, from a trained model, obj, using 10-fold cross-validation by default. CVMdl = crossval (obj, name, value) specifies additional name-value pair arguments to customize the cross-validation process. Name Value 'KFold' Specify the number of folds to use in k-fold cross-validation. "KFold", k, where k is an integer greater than 1. 'Holdout' Specify the fraction of the data to hold out for testing. "Holdout", p, where p is a scalar in the range (0,1). 'Leaveout' Specify whether to perform leave-one-out cross-validation. "Leaveout", Value, where Value is 'on' or 'off'. 'CVPartition' Specify a cvpartition object used for cross-validation. "CVPartition", cv, where isa (cv, "cvpartition") = 1. See also: fitcdiscr, ClassificationDiscriminant, cvpartition, ClassificationPartitionedModel # name: # type: sq_string # elements: 1 # length: 52 Cross Validate a Discriminant classification object. # name: # type: sq_string # elements: 1 # length: 35 ClassificationDiscriminant.cvshrink # name: # type: sq_string # elements: 1 # length: 1833 ClassificationDiscriminant: err = cvshrink (obj) ClassificationDiscriminant: [err, gamma] = cvshrink (obj) ClassificationDiscriminant: [err, gamma, delta] = cvshrink (obj) ClassificationDiscriminant: [err, gamma, delta, numpred] = cvshrink (obj) ClassificationDiscriminant: [...] = cvshrink (..., Name, Value) Cross validate the regularization of a discriminant. err = cvshrink (obj) cross validates obj over a grid of Gamma values and returns the misclassification rate at each of them, so that a regularization can be chosen by what it costs on held-out data rather than on the data it was fitted to. [err, gamma, delta, numpred] = cvshrink (obj) also returns the grid itself and the number of predictors surviving at each point of it. gamma is a column with one entry per Gamma; err, delta and numpred carry one row per Gamma and one column per Delta. Name Value 'NumGamma' The number of Gamma intervals, a positive integer, 10 by default, giving NumGamma + 1 values evenly spaced from 0 to 1. 'NumDelta' The number of Delta intervals, a non-negative integer, 0 by default. For each Gamma the Delta values run from 0 to the point at which every predictor has been eliminated, so the grid is not the same in every row. 'Gamma' The Gamma values to try, given explicitly as a vector, in place of 'NumGamma'. 'Delta' The Delta values to try, given explicitly, in place of 'NumDelta': a vector used for every Gamma, or a matrix with one row per Gamma. Every point of the grid is cross validated against the same partition, so the errors differ by the regularization and not by the split. The partition is drawn at random, so err is not reproducible across runs and does not match MATLAB's; gamma, delta and numpred are deterministic and do. See also: ClassificationDiscriminant, fitcdiscr, ClassificationDiscriminant.nLinearCoeffs # name: # type: sq_string # elements: 1 # length: 52 Cross validate the regularization of a discriminant. # name: # type: sq_string # elements: 1 # length: 31 ClassificationDiscriminant.edge # name: # type: sq_string # elements: 1 # length: 1041 ClassificationDiscriminant: e = edge (obj, X, Y) ClassificationDiscriminant: e = edge (..., "Weights", w) ClassificationDiscriminant: e = edge (obj, Tbl, ResponseVarName) ClassificationDiscriminant: e = edge (obj, Tbl) Classification edge, the mean of the classification margins. e = edge (obj, X, Y) reduces the vector that margin returns to a single number, the mean margin over the rows of X. It says how far the model puts the true class ahead of its nearest rival on average, so a larger edge is a better model, and unlike a loss it is not bounded above and rewards confidence rather than bare correctness. e = edge (..., "Weights", w) takes the weighted mean instead, with one weight per row of X. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. # name: # type: sq_string # elements: 1 # length: 60 Classification edge, the mean of the classification margins. # name: # type: sq_string # elements: 1 # length: 31 ClassificationDiscriminant.logp # name: # type: sq_string # elements: 1 # length: 968 ClassificationDiscriminant: lp = logp (obj, X) Log unconditional probability density of the observations. lp = logp (obj, X) returns an Nx1 vector holding, for each row of X, the natural logarithm of P(x) = sum_k P(k) P(x|k), the density of the observation summed over the classes with each class weighted by its prior P(k). Each P(x|k) is the multivariate normal density of class k. obj must be a ClassificationDiscriminant object. X must be an NxP numeric matrix with one column per predictor of the trained model, or a table holding them. An unusually low value marks an observation the model finds unlikely under every class, which is what makes this an outlier test rather than a classification. A table's variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. # name: # type: sq_string # elements: 1 # length: 58 Log unconditional probability density of the observations. # name: # type: sq_string # elements: 1 # length: 31 ClassificationDiscriminant.loss # name: # type: sq_string # elements: 1 # length: 2220 ClassificationDiscriminant: L = loss (obj, X, Y) ClassificationDiscriminant: L = loss (obj, Tbl, ResponseVarName) ClassificationDiscriminant: L = loss (obj, Tbl) ClassificationDiscriminant: L = loss (..., name, value) Compute loss for a trained ClassificationDiscriminant object. L = loss (obj, X, Y) computes the loss, L, using the default loss function 'mincost'. obj is a ClassificationDiscriminant object trained on X and Y. X must be a NxP numeric matrix of input data where rows correspond to observations and columns correspond to features or variables. Y is Nx1 matrix or cell matrix containing the class labels of corresponding predictor data in X. Y must have same numbers of Rows as X. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. L = loss (..., name, value) allows additional options specified by name-value pairs: Name Value 'LossFun' Specifies the loss function to use. Can be a function handle with four input arguments (C, S, W, Cost) which returns a scalar value or one of: 'binodeviance', 'classifcost', 'classiferror', 'exponential', 'hinge', 'logit','mincost', 'quadratic'. C is a logical matrix of size NxK, where N is the number of observations and K is the number of classes. The element C(i,j) is true if the class label of the i-th observation is equal to the j-th class. S is a numeric matrix of size NxK, where each element represents the classification score for the corresponding class. W is a numeric vector of length N, representing the observation weights. Cost is a KxK matrix representing the misclassification costs. 'Weights' Specifies observation weights, must be a numeric vector of length equal to the number of rows in X. Default is ones (size (X, 1)). loss normalizes the weights so that observation weights in each class sum to the prior probability of that class. When you supply Weights, loss computes the weighted classification loss. See also: ClassificationDiscriminant # name: # type: sq_string # elements: 1 # length: 61 Compute loss for a trained ClassificationDiscriminant object. # name: # type: sq_string # elements: 1 # length: 32 ClassificationDiscriminant.mahal # name: # type: sq_string # elements: 1 # length: 1272 ClassificationDiscriminant: M = mahal (obj, X) ClassificationDiscriminant: M = mahal (..., 'ClassLabels', labels) Squared Mahalanobis distance to the class means. M = mahal (obj, X) returns an NxK matrix whose element (i,j) is the squared Mahalanobis distance from observation i to the mean of class j, measured against the covariance that class carries: the one shared covariance for a linear discriminant and the class's own for a quadratic one. obj must be a ClassificationDiscriminant object. X must be an NxP numeric matrix with one column per predictor of the trained model, or a table holding them. M = mahal (..., 'ClassLabels', labels) returns an Nx1 vector instead, holding for each observation the distance to the mean of the class labels names for it. labels must have one entry per row of X, each of them one of ClassNames. The distance is measured against the covariance the model reports, so a regularized model is measured against its regularized covariance. The prior does not enter it. A table's variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. # name: # type: sq_string # elements: 1 # length: 48 Squared Mahalanobis distance to the class means. # name: # type: sq_string # elements: 1 # length: 33 ClassificationDiscriminant.margin # name: # type: sq_string # elements: 1 # length: 1275 ClassificationDiscriminant: m = margin (obj, X, Y) ClassificationDiscriminant: m = margin (obj, Tbl, ResponseVarName) ClassificationDiscriminant: m = margin (obj, Tbl) Classification margins for discriminant analysis classifier. m = margin (obj, X, Y) returns the classification margins for obj with data X and classification Y. m is a numeric vector of length size (X,1). obj is a ClassificationDiscriminant object trained on X and Y. X must be a NxP numeric matrix of input data where rows correspond to observations and columns correspond to features or variables. Y is Nx1 matrix or cell matrix containing the class labels of corresponding predictor data in X. Y must have same numbers of Rows as X. The classification margin for each observation is the difference between the classification score for the true class and the maximal classification score for the false classes. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: fitcdiscr, ClassificationDiscriminant # name: # type: sq_string # elements: 1 # length: 60 Classification margins for discriminant analysis classifier. # name: # type: sq_string # elements: 1 # length: 40 ClassificationDiscriminant.nLinearCoeffs # name: # type: sq_string # elements: 1 # length: 867 ClassificationDiscriminant: n = nLinearCoeffs (obj) ClassificationDiscriminant: n = nLinearCoeffs (obj, delta) Number of nonzero linear coefficients at a regularization threshold. n = nLinearCoeffs (obj) returns the number of predictors the discriminant keeps at its own Delta. n = nLinearCoeffs (obj, delta) returns the number it would keep at each threshold in delta, as a column vector however delta is shaped. A predictor survives a threshold when its DeltaPredictor reaches it, the comparison including equality, so delta at exactly a predictor's own value still counts it. A threshold above every DeltaPredictor therefore leaves nothing and returns zero. The count is taken whatever the DiscrimType, as MATLAB takes it, even though Delta regularizes the linear types alone. See also: fitcdiscr, ClassificationDiscriminant, CompactClassificationDiscriminant # name: # type: sq_string # elements: 1 # length: 68 Number of nonzero linear coefficients at a regularization threshold. # name: # type: sq_string # elements: 1 # length: 34 ClassificationDiscriminant.predict # name: # type: sq_string # elements: 1 # length: 1778 ClassificationDiscriminant: label = predict (obj, XC) ClassificationDiscriminant: [label, score, cost] = predict (obj, XC) Classify new data points into categories using the discriminant analysis model from a ClassificationDiscriminant object. label = predict (obj, XC) returns the vector of labels predicted for the corresponding instances in XC, using the predictor data in obj.X and corresponding labels, obj.Y, stored in the ClassificationDiscriminant model, obj. obj must be a ClassificationDiscriminant class object. XC must be an MxP numeric matrix with the same number of features P as the corresponding predictors of the discriminant model in obj. [label, score, cost] = predict (obj, XC) also returns score, which contains the predicted class scores or posterior probabilities for each instance of the corresponding unique classes, and cost, which is a matrix containing the expected cost of the classifications. A query with a missing value has NaN scores and costs and takes the class of largest prior. The score matrix contains the posterior probabilities for each class, calculated using the multivariate normal probability density function and the prior probabilities of each class. These scores are normalized to ensure they sum to 1 for each observation. The cost matrix contains the expected classification cost for each class, computed based on the posterior probabilities and the specified misclassification costs. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: ClassificationDiscriminant, fitcdiscr # name: # type: sq_string # elements: 1 # length: 120 Classify new data points into categories using the discriminant analysis model from a ClassificationDiscriminant object. # name: # type: sq_string # elements: 1 # length: 36 ClassificationDiscriminant.resubEdge # name: # type: sq_string # elements: 1 # length: 246 ClassificationDiscriminant: e = resubEdge (obj) Classification edge of the model on its own training data. e = resubEdge (obj) is edge applied to the observations the model was fitted on, the mean of resubMargin weighted by the training weights. # name: # type: sq_string # elements: 1 # length: 58 Classification edge of the model on its own training data. # name: # type: sq_string # elements: 1 # length: 36 ClassificationDiscriminant.resubLoss # name: # type: sq_string # elements: 1 # length: 609 ClassificationDiscriminant: L = resubLoss (obj) ClassificationDiscriminant: L = resubLoss (..., name, value) Classification loss of the model on its own training data. L = resubLoss (obj) is loss applied to the observations the model was fitted on, defaulting to 'mincost' and to the training weights, and it accepts the same Name-Value pairs. Being a resubstitution quantity it is a lower bound on the error rather than an estimate of it. It is worth least on a lazy learner: a one-neighbour ClassificationKNN has a resubstitution loss of exactly zero, every training point being its own nearest neighbour. # name: # type: sq_string # elements: 1 # length: 58 Classification loss of the model on its own training data. # name: # type: sq_string # elements: 1 # length: 38 ClassificationDiscriminant.resubMargin # name: # type: sq_string # elements: 1 # length: 291 ClassificationDiscriminant: m = resubMargin (obj) Classification margins of the model on its own training data. m = resubMargin (obj) is margin applied to the observations the model was fitted on, one number per observation. Being a resubstitution quantity it is optimistic by construction. # name: # type: sq_string # elements: 1 # length: 61 Classification margins of the model on its own training data. # name: # type: sq_string # elements: 1 # length: 39 ClassificationDiscriminant.resubPredict # name: # type: sq_string # elements: 1 # length: 611 ClassificationDiscriminant: label = resubPredict (obj) ClassificationDiscriminant: [label, score, cost] = resubPredict (obj) Classify the training data with the model fitted to it. label = resubPredict (obj) is predict applied to the observations the model was fitted on, which it holds in X. Handing them over yourself is not the same thing: a row dropped for a missing response is not in X, so the original matrix and the model's own are different data. The result measures fit and not generalization, and is optimistic by construction. crossval is what estimates performance on data the model has not seen. # name: # type: sq_string # elements: 1 # length: 55 Classify the training data with the model fitted to it. # name: # type: sq_string # elements: 1 # length: 36 ClassificationDiscriminant.savemodel # name: # type: sq_string # elements: 1 # length: 496 ClassificationDiscriminant: savemodel (obj, filename) Save a ClassificationDiscriminant object. savemodel (obj, filename) saves each property of a ClassificationDiscriminant object into an Octave binary file, the name of which is specified in filename, along with an extra variable, which defines the type classification object these variables constitute. Use loadmodel in order to load a classification object into Octave's workspace. See also: loadmodel, fitcdiscr, ClassificationDiscriminant # name: # type: sq_string # elements: 1 # length: 41 Save a ClassificationDiscriminant object. # name: # type: string # elements: 1 # length: 18 ClassificationECOC # name: # type: sq_string # elements: 1 # length: 682 statistics: ClassificationECOC A multiclass model built from binary learners. An error correcting output codes model turns a problem of K classes into a set of two class problems. A coding matrix gives one column per binary learner saying which classes that learner calls +1, which it calls -1, and which sit it out; a new observation is sent to every learner and given the class whose column of the matrix its scores match most closely. The fit is carried out by the learners themselves, whichever fitcecoc was asked for, and the decoding by CompactClassificationECOC, which this class holds the data of a fit on top of. See also: fitcecoc, CompactClassificationECOC, designecoc # name: # type: sq_string # elements: 1 # length: 46 A multiclass model built from binary learners. # name: # type: sq_string # elements: 1 # length: 27 ClassificationECOC.BinEdges # name: # type: sq_string # elements: 1 # length: 159 ClassificationECOC: property BinEdges The bin edges of the predictors, empty unless the learners binned them, which none here does. This property is read-only. # name: # type: sq_string # elements: 1 # length: 93 The bin edges of the predictors, empty unless the learners binned them, which none here does. # name: # type: sq_string # elements: 1 # length: 33 ClassificationECOC.BinaryLearners # name: # type: sq_string # elements: 1 # length: 132 ClassificationECOC: property BinaryLearners The trained binary learners, one per column of CodingMatrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 60 The trained binary learners, one per column of CodingMatrix. # name: # type: sq_string # elements: 1 # length: 29 ClassificationECOC.BinaryLoss # name: # type: sq_string # elements: 1 # length: 139 ClassificationECOC: property BinaryLoss The loss that turns a binary learner's score into a cost. See CompactClassificationECOC.BinaryLoss. # name: # type: sq_string # elements: 1 # length: 57 The loss that turns a binary learner's score into a cost. # name: # type: sq_string # elements: 1 # length: 26 ClassificationECOC.BinaryY # name: # type: sq_string # elements: 1 # length: 220 ClassificationECOC: property BinaryY What each observation was to each binary learner An NxL matrix of -1, 0 and +1, row n being the row of CodingMatrix belonging to that observation's class. This property is read-only. # name: # type: sq_string # elements: 1 # length: 48 What each observation was to each binary learner # name: # type: sq_string # elements: 1 # length: 40 ClassificationECOC.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 179 ClassificationECOC: property CategoricalPredictors The columns holding categorical predictors, empty when none is. Every binary learner was given them. This property is read-only. # name: # type: sq_string # elements: 1 # length: 63 The columns holding categorical predictors, empty when none is. # name: # type: sq_string # elements: 1 # length: 29 ClassificationECOC.ClassNames # name: # type: sq_string # elements: 1 # length: 217 ClassificationECOC: property ClassNames The distinct class labels, sorted or in the order given by the 'ClassNames' option; the rows of CodingMatrix, Prior and Cost take them in that order. This property is read-only. # name: # type: sq_string # elements: 1 # length: 149 The distinct class labels, sorted or in the order given by the 'ClassNames' option; the rows of CodingMatrix, Prior and Cost take them in that order. # name: # type: sq_string # elements: 1 # length: 37 ClassificationECOC.ClassificationECOC # name: # type: sq_string # elements: 1 # length: 630 ClassificationECOC: obj = ClassificationECOC (X, Y) ClassificationECOC: obj = ClassificationECOC (Tbl, ResponseVarName) ClassificationECOC: obj = ClassificationECOC (Tbl, formula) ClassificationECOC: obj = ClassificationECOC (Tbl, Y) ClassificationECOC: obj = ClassificationECOC (..., name, value) Fit a multiclass model from binary learners. obj = ClassificationECOC (X, Y) fits one binary learner per column of a one against one coding design and returns them as a ClassificationECOC object. fitcecoc is the documented way in, and its help lists the options both take. See also: fitcecoc, CompactClassificationECOC, designecoc # name: # type: sq_string # elements: 1 # length: 44 Fit a multiclass model from binary learners. # name: # type: sq_string # elements: 1 # length: 31 ClassificationECOC.CodingMatrix # name: # type: sq_string # elements: 1 # length: 118 ClassificationECOC: property CodingMatrix The coding design, a KxL matrix of -1, 0 and +1. This property is read-only. # name: # type: sq_string # elements: 1 # length: 48 The coding design, a KxL matrix of -1, 0 and +1. # name: # type: sq_string # elements: 1 # length: 29 ClassificationECOC.CodingName # name: # type: sq_string # elements: 1 # length: 146 ClassificationECOC: property CodingName The name of the coding design, or 'custom' when the matrix was given outright. This property is read-only. # name: # type: sq_string # elements: 1 # length: 78 The name of the coding design, or 'custom' when the matrix was given outright. # name: # type: sq_string # elements: 1 # length: 23 ClassificationECOC.Cost # name: # type: sq_string # elements: 1 # length: 78 ClassificationECOC: property Cost The cost of misclassification, a KxK matrix. # name: # type: sq_string # elements: 1 # length: 44 The cost of misclassification, a KxK matrix. # name: # type: sq_string # elements: 1 # length: 41 ClassificationECOC.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 133 ClassificationECOC: property ExpandedPredictorNames The names of the predictors as the learners saw them. This property is read-only. # name: # type: sq_string # elements: 1 # length: 53 The names of the predictors as the learners saw them. # name: # type: sq_string # elements: 1 # length: 52 ClassificationECOC.HyperparameterOptimizationResults # name: # type: sq_string # elements: 1 # length: 195 ClassificationECOC: property HyperparameterOptimizationResults The result of optimizing the hyperparameters, always empty here, no such optimization being implemented. This property is read-only. # name: # type: sq_string # elements: 1 # length: 104 The result of optimizing the hyperparameters, always empty here, no such optimization being implemented. # name: # type: sq_string # elements: 1 # length: 33 ClassificationECOC.LearnerWeights # name: # type: sq_string # elements: 1 # length: 136 ClassificationECOC: property LearnerWeights The total observation weight each binary learner was trained on. This property is read-only. # name: # type: sq_string # elements: 1 # length: 64 The total observation weight each binary learner was trained on. # name: # type: sq_string # elements: 1 # length: 34 ClassificationECOC.ModelParameters # name: # type: sq_string # elements: 1 # length: 126 ClassificationECOC: property ModelParameters A structure of the options the model was fitted with. This property is read-only. # name: # type: sq_string # elements: 1 # length: 53 A structure of the options the model was fitted with. # name: # type: sq_string # elements: 1 # length: 34 ClassificationECOC.NumObservations # name: # type: sq_string # elements: 1 # length: 105 ClassificationECOC: property NumObservations The number of observations used. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 The number of observations used. # name: # type: sq_string # elements: 1 # length: 33 ClassificationECOC.PredictorNames # name: # type: sq_string # elements: 1 # length: 100 ClassificationECOC: property PredictorNames The names of the predictors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 The names of the predictors. # name: # type: sq_string # elements: 1 # length: 24 ClassificationECOC.Prior # name: # type: sq_string # elements: 1 # length: 71 ClassificationECOC: property Prior The prior probability of each class. # name: # type: sq_string # elements: 1 # length: 36 The prior probability of each class. # name: # type: sq_string # elements: 1 # length: 31 ClassificationECOC.ResponseName # name: # type: sq_string # elements: 1 # length: 104 ClassificationECOC: property ResponseName The name of the response variable. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 The name of the response variable. # name: # type: sq_string # elements: 1 # length: 27 ClassificationECOC.RowsUsed # name: # type: sq_string # elements: 1 # length: 348 ClassificationECOC: property RowsUsed A logical column marking the rows of the original data that were used. A row is dropped when its class is missing, when every predictor is missing, or when its weight is zero; a row missing only some predictors is used, each binary learner taking the missing values by its own rule. This property is read-only. # name: # type: sq_string # elements: 1 # length: 70 A logical column marking the rows of the original data that were used. # name: # type: sq_string # elements: 1 # length: 33 ClassificationECOC.ScoreTransform # name: # type: sq_string # elements: 1 # length: 90 ClassificationECOC: property ScoreTransform The transform applied to the predicted scores. # name: # type: sq_string # elements: 1 # length: 46 The transform applied to the predicted scores. # name: # type: sq_string # elements: 1 # length: 20 ClassificationECOC.W # name: # type: sq_string # elements: 1 # length: 213 ClassificationECOC: property W The observation weights, scaled so that each class carries its prior and the whole sums to one. It has the class of the 'Weights' given, single or double. This property is read-only. # name: # type: sq_string # elements: 1 # length: 95 The observation weights, scaled so that each class carries its prior and the whole sums to one. # name: # type: sq_string # elements: 1 # length: 20 ClassificationECOC.X # name: # type: sq_string # elements: 1 # length: 320 ClassificationECOC: property X The predictor data the model was fitted on, one row per observation. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 68 The predictor data the model was fitted on, one row per observation. # name: # type: sq_string # elements: 1 # length: 20 ClassificationECOC.Y # name: # type: sq_string # elements: 1 # length: 100 ClassificationECOC: property Y The class labels the model was fitted on. This property is read-only. # name: # type: sq_string # elements: 1 # length: 41 The class labels the model was fitted on. # name: # type: sq_string # elements: 1 # length: 26 ClassificationECOC.compact # name: # type: sq_string # elements: 1 # length: 353 ClassificationECOC: CMdl = compact (obj) Drop the training data from a ClassificationECOC. CMdl = compact (obj) returns a CompactClassificationECOC carrying the binary learners and the coding matrix but not X, Y or W, so it predicts and scores new data but cannot be refitted or cross validated. See also: ClassificationECOC, CompactClassificationECOC # name: # type: sq_string # elements: 1 # length: 49 Drop the training data from a ClassificationECOC. # name: # type: sq_string # elements: 1 # length: 27 ClassificationECOC.crossval # name: # type: sq_string # elements: 1 # length: 776 ClassificationECOC: CVMdl = crossval (obj) ClassificationECOC: CVMdl = crossval (..., name, value) Cross validate a ClassificationECOC. CVMdl = crossval (obj) partitions the training data into ten folds, fits the same model to each fold's training part, and returns them as a ClassificationPartitionedECOC. Name Value 'KFold' The number of folds, an integer greater than one. The default is ten, or the number of observations when there are fewer than ten. 'Holdout' The share of the data held out, a number strictly between 0 and 1. 'Leaveout' 'on' for one fold per observation, 'off' otherwise. 'CVPartition' A cvpartition object, which names the folds outright. Only one of the four may be given. See also: ClassificationECOC, ClassificationPartitionedECOC, cvpartition # name: # type: sq_string # elements: 1 # length: 36 Cross validate a ClassificationECOC. # name: # type: sq_string # elements: 1 # length: 40 ClassificationECOC.discardSupportVectors # name: # type: sq_string # elements: 1 # length: 596 ClassificationECOC: obj = discardSupportVectors (obj) Give up the support vectors of the binary learners. obj = discardSupportVectors (obj) empties Alpha, SupportVectors and SupportVectorLabels on every binary learner that is a support vector machine on a linear kernel, whose linear model stands in for them exactly, so nothing the model answers changes. Any other learner is left as it is, a code being free to mix them, and a model with no linear support vector machine among its learners warns and is returned unchanged. See also: ClassificationECOC, ClassificationSVM.discardSupportVectors # name: # type: sq_string # elements: 1 # length: 51 Give up the support vectors of the binary learners. # name: # type: sq_string # elements: 1 # length: 23 ClassificationECOC.edge # name: # type: sq_string # elements: 1 # length: 597 ClassificationECOC: e = edge (obj, X, Y) ClassificationECOC: e = edge (obj, Tbl, ResponseVarName) ClassificationECOC: e = edge (obj, Tbl) Classification edge of a ClassificationECOC. See CompactClassificationECOC.edge. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: CompactClassificationECOC.edge # name: # type: sq_string # elements: 1 # length: 44 Classification edge of a ClassificationECOC. # name: # type: sq_string # elements: 1 # length: 23 ClassificationECOC.loss # name: # type: sq_string # elements: 1 # length: 597 ClassificationECOC: L = loss (obj, X, Y) ClassificationECOC: L = loss (obj, Tbl, ResponseVarName) ClassificationECOC: L = loss (obj, Tbl) Classification loss of a ClassificationECOC. See CompactClassificationECOC.loss. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: CompactClassificationECOC.loss # name: # type: sq_string # elements: 1 # length: 44 Classification loss of a ClassificationECOC. # name: # type: sq_string # elements: 1 # length: 25 ClassificationECOC.margin # name: # type: sq_string # elements: 1 # length: 613 ClassificationECOC: m = margin (obj, X, Y) ClassificationECOC: m = margin (obj, Tbl, ResponseVarName) ClassificationECOC: m = margin (obj, Tbl) Classification margin of a ClassificationECOC. See CompactClassificationECOC.margin. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: CompactClassificationECOC.margin # name: # type: sq_string # elements: 1 # length: 46 Classification margin of a ClassificationECOC. # name: # type: sq_string # elements: 1 # length: 26 ClassificationECOC.predict # name: # type: sq_string # elements: 1 # length: 695 ClassificationECOC: label = predict (obj, XC) ClassificationECOC: [label, NegLoss, PBScore] = predict (...) ClassificationECOC: [...] = predict (..., name, value) Classify new data with a trained ClassificationECOC. It takes and returns exactly what CompactClassificationECOC.predict does, the training data playing no part in a prediction. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: ClassificationECOC, CompactClassificationECOC.predict # name: # type: sq_string # elements: 1 # length: 52 Classify new data with a trained ClassificationECOC. # name: # type: sq_string # elements: 1 # length: 28 ClassificationECOC.resubEdge # name: # type: sq_string # elements: 1 # length: 116 ClassificationECOC: e = resubEdge (obj) Classification edge on the training data. See also: ClassificationECOC.edge # name: # type: sq_string # elements: 1 # length: 41 Classification edge on the training data. # name: # type: sq_string # elements: 1 # length: 28 ClassificationECOC.resubLoss # name: # type: sq_string # elements: 1 # length: 116 ClassificationECOC: L = resubLoss (obj) Classification loss on the training data. See also: ClassificationECOC.loss # name: # type: sq_string # elements: 1 # length: 41 Classification loss on the training data. # name: # type: sq_string # elements: 1 # length: 30 ClassificationECOC.resubMargin # name: # type: sq_string # elements: 1 # length: 122 ClassificationECOC: m = resubMargin (obj) Classification margin on the training data. See also: ClassificationECOC.margin # name: # type: sq_string # elements: 1 # length: 43 Classification margin on the training data. # name: # type: sq_string # elements: 1 # length: 31 ClassificationECOC.resubPredict # name: # type: sq_string # elements: 1 # length: 146 ClassificationECOC: label = resubPredict (obj) Classify the training data with a trained ClassificationECOC. See also: ClassificationECOC.predict # name: # type: sq_string # elements: 1 # length: 61 Classify the training data with a trained ClassificationECOC. # name: # type: sq_string # elements: 1 # length: 31 ClassificationECOC.selectModels # name: # type: sq_string # elements: 1 # length: 409 ClassificationECOC: sub = selectModels (obj, idx) Keep a subset of the fitted regularization strengths. sub = selectModels (obj, idx) narrows every binary learner to the strengths idx names, which may be indices into the learner's Lambda or a logical vector over it. Only a linear learner is fitted over several strengths, so any other raises. See also: ClassificationECOC, ClassificationLinear.selectModels # name: # type: sq_string # elements: 1 # length: 53 Keep a subset of the fitted regularization strengths. # name: # type: sq_string # elements: 1 # length: 22 ClassificationEnsemble # name: # type: sq_string # elements: 1 # length: 619 statistics: ClassificationEnsemble Boosted ensemble of decision trees for classification A ClassificationEnsemble object holds the weak learners a boosting method grew one after another, each on the observations reweighted by the errors of those before it, together with the data it was fitted on. AdaBoostM1, GentleBoost and LogitBoost fit two classes, AdaBoostM2 more than two. Create one with fitcensemble. A bagged ensemble is a ClassificationBaggedEnsemble, and compact returns a CompactClassificationEnsemble without the data. See also: fitcensemble, CompactClassificationEnsemble, ClassificationBaggedEnsemble # name: # type: sq_string # elements: 1 # length: 53 Boosted ensemble of decision trees for classification # name: # type: sq_string # elements: 1 # length: 31 ClassificationEnsemble.BinEdges # name: # type: sq_string # elements: 1 # length: 143 ClassificationEnsemble: property BinEdges Bin edges of the predictors Always empty, binning not being implemented. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Bin edges of the predictors # name: # type: sq_string # elements: 1 # length: 44 ClassificationEnsemble.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 186 ClassificationEnsemble: property CategoricalPredictors Indices of categorical predictors The predictors every tree treats as categorical, empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Indices of categorical predictors # name: # type: sq_string # elements: 1 # length: 33 ClassificationEnsemble.ClassNames # name: # type: sq_string # elements: 1 # length: 243 ClassificationEnsemble: property ClassNames Names of the classes The classes, in the type of the response, sorted or in the order given by the 'ClassNames' option; the columns of the scores take them in that order. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Names of the classes # name: # type: sq_string # elements: 1 # length: 45 ClassificationEnsemble.ClassificationEnsemble # name: # type: sq_string # elements: 1 # length: 576 ClassificationEnsemble: obj = ClassificationEnsemble (X, Y) ClassificationEnsemble: obj = ClassificationEnsemble (Tbl, ResponseVarName) ClassificationEnsemble: obj = ClassificationEnsemble (Tbl, formula) ClassificationEnsemble: obj = ClassificationEnsemble (Tbl, Y) ClassificationEnsemble: obj = ClassificationEnsemble (..., name, value) Fit a boosted ensemble of decision trees. fitcensemble is the documented way in, and its help lists the options both take. A bagged ensemble is fitted by ClassificationBaggedEnsemble. See also: fitcensemble, ClassificationBaggedEnsemble # name: # type: sq_string # elements: 1 # length: 41 Fit a boosted ensemble of decision trees. # name: # type: sq_string # elements: 1 # length: 37 ClassificationEnsemble.CombineWeights # name: # type: sq_string # elements: 1 # length: 166 ClassificationEnsemble: property CombineWeights How the weak learners are combined 'WeightedSum' for boosting, 'WeightedAverage' for Bag. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 How the weak learners are combined # name: # type: sq_string # elements: 1 # length: 27 ClassificationEnsemble.Cost # name: # type: sq_string # elements: 1 # length: 308 ClassificationEnsemble: property Cost Misclassification costs A square matrix, Cost(i,j) being the cost of classifying an observation of class i as class j. A boosting method starts from observation weights multiplied by the total cost of misclassifying each observation's class. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Misclassification costs # name: # type: sq_string # elements: 1 # length: 45 ClassificationEnsemble.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 161 ClassificationEnsemble: property ExpandedPredictorNames Names of the predictors as the learners saw them The same as PredictorNames. This property is read-only. # name: # type: sq_string # elements: 1 # length: 48 Names of the predictors as the learners saw them # name: # type: sq_string # elements: 1 # length: 30 ClassificationEnsemble.FitInfo # name: # type: sq_string # elements: 1 # length: 445 ClassificationEnsemble: property FitInfo Fit information A column with one element per learner: the weighted classification error for AdaBoostM1, the weighted pseudo-loss for AdaBoostM2 and RUSBoost, and the weighted mean squared error of the regression tree for GentleBoost and LogitBoost. For LPBoost and TotalBoost a matrix with a row per learner: its margin on each observation, and its edge last. Empty for Bag. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Fit information # name: # type: sq_string # elements: 1 # length: 41 ClassificationEnsemble.FitInfoDescription # name: # type: sq_string # elements: 1 # length: 103 ClassificationEnsemble: property FitInfoDescription Description of FitInfo This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Description of FitInfo # name: # type: sq_string # elements: 1 # length: 56 ClassificationEnsemble.HyperparameterOptimizationResults # name: # type: sq_string # elements: 1 # length: 192 ClassificationEnsemble: property HyperparameterOptimizationResults Results of optimizing the hyperparameters Always empty, such optimization not being implemented. This property is read-only. # name: # type: sq_string # elements: 1 # length: 41 Results of optimizing the hyperparameters # name: # type: sq_string # elements: 1 # length: 35 ClassificationEnsemble.LearnerNames # name: # type: sq_string # elements: 1 # length: 156 ClassificationEnsemble: property LearnerNames Names of the weak learners {'Tree'}, or for Subspace {'KNN'} or {'Discriminant'}. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Names of the weak learners # name: # type: sq_string # elements: 1 # length: 29 ClassificationEnsemble.Method # name: # type: sq_string # elements: 1 # length: 199 ClassificationEnsemble: property Method Ensemble method 'AdaBoostM1', 'AdaBoostM2', 'GentleBoost', 'LogitBoost', 'RUSBoost', 'LPBoost', 'TotalBoost', 'Bag' or 'Subspace'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Ensemble method # name: # type: sq_string # elements: 1 # length: 38 ClassificationEnsemble.ModelParameters # name: # type: sq_string # elements: 1 # length: 278 ClassificationEnsemble: property ModelParameters Parameters of the fit A structure with the fields Type, Method, LearnerTemplates, the tree template the learners were grown from, NLearn, the number of learning cycles asked for in all, and LearnRate. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Parameters of the fit # name: # type: sq_string # elements: 1 # length: 38 ClassificationEnsemble.NumObservations # name: # type: sq_string # elements: 1 # length: 105 ClassificationEnsemble: property NumObservations Number of observations used This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Number of observations used # name: # type: sq_string # elements: 1 # length: 33 ClassificationEnsemble.NumTrained # name: # type: sq_string # elements: 1 # length: 104 ClassificationEnsemble: property NumTrained Number of trained weak learners This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Number of trained weak learners # name: # type: sq_string # elements: 1 # length: 37 ClassificationEnsemble.PredictorNames # name: # type: sq_string # elements: 1 # length: 135 ClassificationEnsemble: property PredictorNames Names of the predictors A cell array of character vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 28 ClassificationEnsemble.Prior # name: # type: sq_string # elements: 1 # length: 147 ClassificationEnsemble: property Prior Prior probabilities of the classes A row vector with one probability per class. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Prior probabilities of the classes # name: # type: sq_string # elements: 1 # length: 43 ClassificationEnsemble.ReasonForTermination # name: # type: sq_string # elements: 1 # length: 138 ClassificationEnsemble: property ReasonForTermination Why the fit stopped adding learners A character vector. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Why the fit stopped adding learners # name: # type: sq_string # elements: 1 # length: 35 ClassificationEnsemble.ResponseName # name: # type: sq_string # elements: 1 # length: 104 ClassificationEnsemble: property ResponseName Name of the response variable This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 31 ClassificationEnsemble.RowsUsed # name: # type: sq_string # elements: 1 # length: 146 ClassificationEnsemble: property RowsUsed Rows of the data that were used A logical column over the rows as supplied. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Rows of the data that were used # name: # type: sq_string # elements: 1 # length: 37 ClassificationEnsemble.ScoreTransform # name: # type: sq_string # elements: 1 # length: 140 ClassificationEnsemble: property ScoreTransform Transform applied to the predicted scores See CompactClassificationEnsemble.ScoreTransform. # name: # type: sq_string # elements: 1 # length: 41 Transform applied to the predicted scores # name: # type: sq_string # elements: 1 # length: 30 ClassificationEnsemble.Trained # name: # type: sq_string # elements: 1 # length: 187 ClassificationEnsemble: property Trained Trained weak learners A column cell array of compact trees, as described under CompactClassificationEnsemble.Trained. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Trained weak learners # name: # type: sq_string # elements: 1 # length: 37 ClassificationEnsemble.TrainedWeights # name: # type: sq_string # elements: 1 # length: 151 ClassificationEnsemble: property TrainedWeights Weights of the trained weak learners A column with one weight per learner. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Weights of the trained weak learners # name: # type: sq_string # elements: 1 # length: 40 ClassificationEnsemble.UsePredForLearner # name: # type: sq_string # elements: 1 # length: 260 ClassificationEnsemble: property UsePredForLearner Which predictors each learner uses For the 'Subspace' method, a logical matrix with one row per predictor and one column per learner. Empty for tree learners, as MATLAB returns it. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Which predictors each learner uses # name: # type: sq_string # elements: 1 # length: 24 ClassificationEnsemble.W # name: # type: sq_string # elements: 1 # length: 276 ClassificationEnsemble: property W Observation weights The weights given, scaled so that the observations of each class sum to its prior probability. A cost matrix is not folded into them. It has the class of the 'Weights' given, single or double. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 24 ClassificationEnsemble.X # name: # type: sq_string # elements: 1 # length: 393 ClassificationEnsemble: property X Predictor data The predictors the ensemble was fitted on, one row per observation, a row missing a value or a class having been left out. This property is read-only. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 24 ClassificationEnsemble.Y # name: # type: sq_string # elements: 1 # length: 147 ClassificationEnsemble: property Y Class labels The labels the ensemble was fitted on, in the type they were given in. This property is read-only. # name: # type: sq_string # elements: 1 # length: 12 Class labels # name: # type: sq_string # elements: 1 # length: 30 ClassificationEnsemble.compact # name: # type: sq_string # elements: 1 # length: 294 ClassificationEnsemble: CMdl = compact (obj) Drop the training data from an ensemble. CMdl = compact (obj) returns a CompactClassificationEnsemble holding the learners and what prediction needs. It predicts new data identically. See also: ClassificationEnsemble, CompactClassificationEnsemble # name: # type: sq_string # elements: 1 # length: 40 Drop the training data from an ensemble. # name: # type: sq_string # elements: 1 # length: 31 ClassificationEnsemble.crossval # name: # type: sq_string # elements: 1 # length: 574 ClassificationEnsemble: CVMdl = crossval (obj) ClassificationEnsemble: CVMdl = crossval (..., name, value) Cross-validate an ensemble. CVMdl = crossval (obj) refits the ensemble on the training part of each of ten folds, stratified by class, and returns a ClassificationPartitionedEnsemble. One of 'KFold', an integer greater than 1, 'Holdout', a number between 0 and 1, 'Leaveout', 'on' for one fold per observation, or 'CVPartition', a cvpartition object, may choose the partition instead. See also: ClassificationEnsemble, ClassificationPartitionedEnsemble, cvpartition # name: # type: sq_string # elements: 1 # length: 27 Cross-validate an ensemble. # name: # type: sq_string # elements: 1 # length: 27 ClassificationEnsemble.edge # name: # type: sq_string # elements: 1 # length: 732 ClassificationEnsemble: e = edge (obj, X, Y) ClassificationEnsemble: e = edge (..., name, value) ClassificationEnsemble: e = edge (obj, Tbl, ResponseVarName) ClassificationEnsemble: e = edge (obj, Tbl) Classification edge of an ensemble. Behaves as CompactClassificationEnsemble.edge and takes the same Name-Value arguments. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: ClassificationEnsemble, CompactClassificationEnsemble.edge # name: # type: sq_string # elements: 1 # length: 35 Classification edge of an ensemble. # name: # type: sq_string # elements: 1 # length: 27 ClassificationEnsemble.loss # name: # type: sq_string # elements: 1 # length: 732 ClassificationEnsemble: L = loss (obj, X, Y) ClassificationEnsemble: L = loss (obj, Tbl, ResponseVarName) ClassificationEnsemble: L = loss (obj, Tbl) ClassificationEnsemble: L = loss (..., name, value) Classification loss of an ensemble. Behaves as CompactClassificationEnsemble.loss and takes the same Name-Value arguments. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: ClassificationEnsemble, CompactClassificationEnsemble.loss # name: # type: sq_string # elements: 1 # length: 35 Classification loss of an ensemble. # name: # type: sq_string # elements: 1 # length: 29 ClassificationEnsemble.margin # name: # type: sq_string # elements: 1 # length: 751 ClassificationEnsemble: m = margin (obj, X, Y) ClassificationEnsemble: m = margin (..., name, value) ClassificationEnsemble: m = margin (obj, Tbl, ResponseVarName) ClassificationEnsemble: m = margin (obj, Tbl) Classification margins of an ensemble. Behaves as CompactClassificationEnsemble.margin and takes the same Name-Value arguments. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: ClassificationEnsemble, CompactClassificationEnsemble.margin # name: # type: sq_string # elements: 1 # length: 38 Classification margins of an ensemble. # name: # type: sq_string # elements: 1 # length: 30 ClassificationEnsemble.predict # name: # type: sq_string # elements: 1 # length: 652 ClassificationEnsemble: label = predict (obj, X) ClassificationEnsemble: [label, scores] = predict (...) ClassificationEnsemble: [...] = predict (..., name, value) Classify new data with an ensemble. Behaves as CompactClassificationEnsemble.predict and takes the same Name-Value arguments. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: ClassificationEnsemble, CompactClassificationEnsemble.predict # name: # type: sq_string # elements: 1 # length: 35 Classify new data with an ensemble. # name: # type: sq_string # elements: 1 # length: 42 ClassificationEnsemble.predictorImportance # name: # type: sq_string # elements: 1 # length: 308 ClassificationEnsemble: imp = predictorImportance (obj) ClassificationEnsemble: [imp, ma] = predictorImportance (obj) Estimate the importance of each predictor. Behaves as CompactClassificationEnsemble.predictorImportance. See also: ClassificationEnsemble, CompactClassificationEnsemble.predictorImportance # name: # type: sq_string # elements: 1 # length: 42 Estimate the importance of each predictor. # name: # type: sq_string # elements: 1 # length: 32 ClassificationEnsemble.resubEdge # name: # type: sq_string # elements: 1 # length: 265 ClassificationEnsemble: e = resubEdge (obj) ClassificationEnsemble: e = resubEdge (..., name, value) Classification edge on the training data. edge on X and Y, weighted by W unless 'Weights' are given. See also: ClassificationEnsemble, ClassificationEnsemble.edge # name: # type: sq_string # elements: 1 # length: 41 Classification edge on the training data. # name: # type: sq_string # elements: 1 # length: 32 ClassificationEnsemble.resubLoss # name: # type: sq_string # elements: 1 # length: 265 ClassificationEnsemble: L = resubLoss (obj) ClassificationEnsemble: L = resubLoss (..., name, value) Classification loss on the training data. loss on X and Y, weighted by W unless 'Weights' are given. See also: ClassificationEnsemble, ClassificationEnsemble.loss # name: # type: sq_string # elements: 1 # length: 41 Classification loss on the training data. # name: # type: sq_string # elements: 1 # length: 34 ClassificationEnsemble.resubMargin # name: # type: sq_string # elements: 1 # length: 272 ClassificationEnsemble: m = resubMargin (obj) ClassificationEnsemble: m = resubMargin (..., name, value) Classification margins of the training data. margin on X and Y, taking the same Name-Value arguments. See also: ClassificationEnsemble, ClassificationEnsemble.margin # name: # type: sq_string # elements: 1 # length: 44 Classification margins of the training data. # name: # type: sq_string # elements: 1 # length: 35 ClassificationEnsemble.resubPredict # name: # type: sq_string # elements: 1 # length: 322 ClassificationEnsemble: label = resubPredict (obj) ClassificationEnsemble: [label, scores] = resubPredict (...) ClassificationEnsemble: [...] = resubPredict (..., name, value) Classify the training data. predict on X, taking the same Name-Value arguments. See also: ClassificationEnsemble, ClassificationEnsemble.predict # name: # type: sq_string # elements: 1 # length: 27 Classify the training data. # name: # type: sq_string # elements: 1 # length: 29 ClassificationEnsemble.resume # name: # type: sq_string # elements: 1 # length: 653 ClassificationEnsemble: B = resume (obj, NumLearningCycles) ClassificationEnsemble: B = resume (..., 'NPrint', n) Grow more weak learners. B is the ensemble with up to NumLearningCycles further learners grown as though the fit had asked for them from the start: the boosting weights carry on from where it stopped. An ensemble whose fit ended on a learner that classified the data perfectly grows no more. 'NPrint' is taken as by fitcensemble. MATLAB R2024a restarts the mislabel weights of AdaBoostM2 on a resume, so its learner weights then differ from those of one longer fit; here they are the same. See also: ClassificationEnsemble, fitcensemble # name: # type: sq_string # elements: 1 # length: 24 Grow more weak learners. # name: # type: sq_string # elements: 1 # length: 17 ClassificationGAM # name: # type: sq_string # elements: 1 # length: 1997 statistics: ClassificationGAM Generalized additive model classification The ClassificationGAM class implements a gradient boosting algorithm for classification. This approach allows the model to capture non-linear relationships between predictors and the binary response variable. Generalized additive model classification is a statistical method that extends linear models by allowing non-linear relationships between each predictor and the response variable through smooth functions. It combines the interpretability of linear models with the flexibility of non-parametric methods. Create a ClassificationGAM object by using the fitcgam function or the class constructor. Two weak learners are available, selected by FitMethod. 'boostedtrees', the default, boosts one shallow decision tree per predictor in each round, which is the scheme MATLAB's generalized additive model uses. A second phase then boosts trees over pairs of predictors, where interactions are asked for. 'splines' boosts a smoothing spline per predictor over NumIterations passes. It has no MATLAB counterpart and is an Octave extension, kept because a smooth additive fit is a genuinely different and often better answer than a staircase of stumps. The two take different arguments, and an argument meant for one is refused by the other rather than ignored. The choice is visible in the properties. Knots, Order, DoF, Formula, LearningRate, NumIterations, BaseModel, ModelwInt and IntMatrix describe a spline fit and are empty under the boosted-tree engine, while ModelParameters, ReasonForTermination, BinEdges, PairDetectionBinEdges and TreeModel describe a tree fit and are empty under the spline engine. Fitted values are not expected to equal MATLAB's even under 'boostedtrees'. The stopping rule and the step-reduction limit are not recoverable from anything MATLAB reports, so this engine documents its own; what the two share is the estimator and the reported surface, not the arithmetic. See also: fitcgam # name: # type: sq_string # elements: 1 # length: 41 Generalized additive model classification # name: # type: sq_string # elements: 1 # length: 27 ClassificationGAM.BaseModel # name: # type: sq_string # elements: 1 # length: 287 ClassificationGAM: property BaseModel Base model parameters A structure containing the parameters of the base model without any interaction terms. The base model represents the generalized additive model with only the main effects (predictor terms) included. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Base model parameters # name: # type: sq_string # elements: 1 # length: 26 ClassificationGAM.BinEdges # name: # type: sq_string # elements: 1 # length: 462 ClassificationGAM: property BinEdges Bin edges of the predictors A cell array with one entry per predictor, holding that predictor's bin edges where the model discretized it before fitting. It is empty here and stays empty: this generalized additive model is built from splines, which take the predictors as they are, where MATLAB's is built from boosted trees and bins them. That difference is described in the class documentation. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Bin edges of the predictors # name: # type: sq_string # elements: 1 # length: 39 ClassificationGAM.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 218 ClassificationGAM: property CategoricalPredictors Indices of the categorical predictors A numeric vector holding the column of each predictor treated as categorical, and empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 28 ClassificationGAM.ClassNames # name: # type: sq_string # elements: 1 # length: 399 ClassificationGAM: property ClassNames Names of classes in the response variable An array of unique values of the response variable Y, which has the same data types as the data in Y, sorted or in the order given by the 'ClassNames' option. This property is read-only. ClassNames can have any of the following datatypes: Cell array of character vectors Character array Logical vector Numeric vector # name: # type: sq_string # elements: 1 # length: 41 Names of classes in the response variable # name: # type: sq_string # elements: 1 # length: 35 ClassificationGAM.ClassificationGAM # name: # type: sq_string # elements: 1 # length: 4076 ClassificationGAM: obj = ClassificationGAM (X, Y) ClassificationGAM: obj = ClassificationGAM (Tbl, ResponseVarName) ClassificationGAM: obj = ClassificationGAM (Tbl, formula) ClassificationGAM: obj = ClassificationGAM (Tbl, Y) ClassificationGAM: obj = ClassificationGAM (..., name, value) Create a ClassificationGAM class object containing a generalized additive classification model. obj = ClassificationGAM (X, Y) returns a ClassificationGAM object, with X as the predictor data and Y containing the class labels of observations in X. X must be a NxP numeric matrix of input data where rows correspond to observations and columns correspond to features or variables. X will be used to train the GAM model. Y is Nx1 matrix or cell matrix containing the class labels of corresponding predictor data in X. Y can contain any type of categorical data. Y must have the same number of rows as X. obj = ClassificationGAM (..., name, value) returns a ClassificationGAM object with parameters specified by the following name, value paired input arguments: Name Value 'PredictorNames' A cell array of character vectors specifying the names of the predictors. The length of this array must match the number of columns in X. 'ResponseName' A character vector specifying the name of the response variable. 'ClassNames' Names of the classes in the class labels, Y, used for fitting the GAM model. ClassNames are of the same type as the class labels in Y. The model keeps the classes in this order; by default they are sorted. 'Cost' An NxR numeric matrix containing misclassification cost for the corresponding instances in X, where R is the number of unique categories in Y. If an instance is correctly classified into its category the cost is calculated to be 1, otherwise 0. The cost matrix can be altered by using Mdl.cost = somecost. By default, its value is cost = ones (rows (X), numel (unique (Y))). 'Prior' A numeric vector specifying the prior probabilities for each class. The order of the elements in Prior corresponds to the order of the classes in ClassNames. Alternatively, you can specify 'empirical' to use the empirical class probabilities or 'uniform' to assume equal class probabilities. The prior weighs the boosted-tree fit: each class carries its prior, spread over its observations in proportion to 'Weights', and an empirical prior is each class's share of the weight. A class whose prior is 0 is removed from the data before fitting, with a warning, as in MATLAB. 'ScoreTransform' A user-defined function handle or a character vector specifying one of the following builtin functions specifying the transformation applied to predicted classification scores. Supported values include 'doublelogit', 'invlogit', 'ismax', 'logit', 'none', 'identity', 'sign', 'symmetric', 'symmetricismax', and 'symmetriclogit'. 'Formula' (spline option) A character vector specifying the model formula in the form 'Y ~ terms' where Y represents the response variable and terms specifies the predictor variables and interaction terms. 'Interactions' A logical matrix, a positive integer scalar, or the string 'all' for defining the interactions between predictor variables. 'Knots' (spline option) A scalar or row vector specifying the number of knots for each predictor variable in the spline fitting. 'Order' (spline option) A scalar or row vector specifying the order of the spline for each predictor variable. 'DoF' (spline option) A scalar or row vector specifying the degrees of freedom for each predictor variable in the spline fitting. 'LearningRate' (spline option) A scalar value between 0 and 1 specifying the learning rate used in the gradient boosting algorithm. 'NumIterations' (spline option) A positive integer specifying the maximum number of iterations for the gradient boosting algorithm. A row marked (spline option) belongs to the spline engine and requires 'FitMethod', 'splines'; passing one under the default boosted-tree engine is an error rather than being ignored. The boosted-tree engine's own options are documented under fitcgam. See also: fitcgam # name: # type: sq_string # elements: 1 # length: 95 Create a ClassificationGAM class object containing a generalized additive classification model. # name: # type: sq_string # elements: 1 # length: 22 ClassificationGAM.Cost # name: # type: sq_string # elements: 1 # length: 1188 ClassificationGAM: property Cost Cost of Misclassification A square matrix specifying the cost of misclassification of a point. Cost(i,j) is the cost of classifying a point into class j if its true class is i (that is, the rows correspond to the true class and the columns correspond to the predicted class). The order of the rows and columns in Cost corresponds to the order of the classes in ClassNames. The number of rows and columns in Cost is the number of unique classes in the response. By default, Cost(i,j) = 1 if i != j, and Cost(i,j) = 0 if i = j. In other words, the cost is 0 for correct classification and 1 for incorrect classification. Add or change the Cost property using dot notation as in: obj.Cost = costMatrix A cost may also be given as a struct with the fields ClassNames and ClassificationCosts, which names the order its own matrix is written in. That matrix is permuted into the order of ClassNames above, so a caller need not know which order the classes were sorted into. It must name every class. A cost must be floating point, not sparse, not complex, non-negative and zero down its diagonal, and must hold no NaN or Inf. A single is widened to double. # name: # type: sq_string # elements: 1 # length: 25 Cost of Misclassification # name: # type: sq_string # elements: 1 # length: 21 ClassificationGAM.DoF # name: # type: sq_string # elements: 1 # length: 206 ClassificationGAM: property DoF Degrees of freedom for spline fitting A scalar or row vector specifying the degrees of freedom for each predictor variable in the spline fitting. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Degrees of freedom for spline fitting # name: # type: sq_string # elements: 1 # length: 40 ClassificationGAM.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 293 ClassificationGAM: property ExpandedPredictorNames Names of the expanded predictor variables A cell array of character vectors naming the predictors as the model sees them. It matches PredictorNames unless a categorical predictor was expanded into dummy variables. This property is read-only. # name: # type: sq_string # elements: 1 # length: 41 Names of the expanded predictor variables # name: # type: sq_string # elements: 1 # length: 27 ClassificationGAM.FitMethod # name: # type: sq_string # elements: 1 # length: 708 ClassificationGAM: property FitMethod Which engine fitted the model A character vector, either 'boostedtrees' or 'splines'. The default is 'boostedtrees', which is the scheme MATLAB's generalized additive model uses and the one the tree-shaped properties above describe. 'splines' selects the penalised-spline engine instead, which is an Octave extension with no MATLAB counterpart. It is the scheme this class fitted before version 1.9.0, and it is kept because a smooth additive fit is a genuinely different and often better answer than a staircase of stumps. The two engines take different arguments and an argument meant for one is refused by the other rather than ignored. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Which engine fitted the model # name: # type: sq_string # elements: 1 # length: 25 ClassificationGAM.Formula # name: # type: sq_string # elements: 1 # length: 272 ClassificationGAM: property Formula Model specification formula A character vector specifying the model formula in the form 'Y ~ terms' where Y represents the response variable and terms specifies the predictor variables and interaction terms. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Model specification formula # name: # type: sq_string # elements: 1 # length: 51 ClassificationGAM.HyperparameterOptimizationResults # name: # type: sq_string # elements: 1 # length: 359 ClassificationGAM: property HyperparameterOptimizationResults Results of the hyperparameter optimization Always empty. It is declared for MATLAB compatibility, where it holds what an automatic search over the hyperparameters found. This class fits the parameters it is given and runs no such search, so there is nothing to report. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Results of the hyperparameter optimization # name: # type: sq_string # elements: 1 # length: 27 ClassificationGAM.IntMatrix # name: # type: sq_string # elements: 1 # length: 556 ClassificationGAM: property IntMatrix Every term the model fits A logical matrix with one row per term and one column per predictor, true wherever the term multiplies that predictor. A row naming one predictor is a main effect, two an interaction, and three or more a higher-order term. This property is read-only. It is the complete record, where Interactions reports only the two-way terms, in the form MATLAB reports them. It is also the form the 'Interactions' option takes back, so passing it to the constructor rebuilds a model over the same terms. # name: # type: sq_string # elements: 1 # length: 25 Every term the model fits # name: # type: sq_string # elements: 1 # length: 30 ClassificationGAM.Interactions # name: # type: sq_string # elements: 1 # length: 596 ClassificationGAM: property Interactions Two-way interaction terms of the fitted model A Kx2 matrix of predictor index pairs, one row per two-way term the model carries, and zeros (0, 2) when it carries none. It reports what was fitted rather than what was asked for, so a count of terms, 'all', a logical matrix and a formula all leave the same kind of value behind. This property is read-only. A main effect names one predictor and a higher-order term names three or more, and neither has a two-column form, so neither appears here. IntMatrix remains the complete record of every term fitted. # name: # type: sq_string # elements: 1 # length: 45 Two-way interaction terms of the fitted model # name: # type: sq_string # elements: 1 # length: 27 ClassificationGAM.Intercept # name: # type: sq_string # elements: 1 # length: 196 ClassificationGAM: property Intercept Intercept of the fitted model A numeric scalar, the log-odds of the response mean, which every additive term is measured against. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Intercept of the fitted model # name: # type: sq_string # elements: 1 # length: 23 ClassificationGAM.Knots # name: # type: sq_string # elements: 1 # length: 192 ClassificationGAM: property Knots Knots for spline fitting A scalar or row vector specifying the number of knots for each predictor variable in the spline fitting. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Knots for spline fitting # name: # type: sq_string # elements: 1 # length: 30 ClassificationGAM.LearningRate # name: # type: sq_string # elements: 1 # length: 206 ClassificationGAM: property LearningRate Learning rate for gradient boosting A scalar value between 0 and 1 specifying the learning rate used in the gradient boosting algorithm. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Learning rate for gradient boosting # name: # type: sq_string # elements: 1 # length: 33 ClassificationGAM.ModelParameters # name: # type: sq_string # elements: 1 # length: 792 ClassificationGAM: property ModelParameters Parameters the model was fitted with A structure holding the fitting parameters. Under the boosted-tree engine it carries MATLAB's own fields: NumPrint, MaxPValue, InitialLearnRateForPredictors, InitialLearnRateForInteractions, NumTreesPerPredictor, NumTreesPerInteraction, MaxNumSplitsPerPredictor, MaxNumSplitsPerInteraction, VerbosityLevel, Interactions, Version, Method and Type. Interactions here is the request as it was made, a count or 'all', where the Interactions property of the model is the pairs actually selected. Under the spline engine it describes that scheme instead, carrying Knots, Order, DoF, Formula, Interactions, LearningRate and NumIterations, since none of the tree vocabulary applies to it. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Parameters the model was fitted with # name: # type: sq_string # elements: 1 # length: 27 ClassificationGAM.ModelwInt # name: # type: sq_string # elements: 1 # length: 267 ClassificationGAM: property ModelwInt Model parameters with interactions A structure containing the parameters of the model that includes interaction terms. This model extends the base model by adding interaction terms between predictors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Model parameters with interactions # name: # type: sq_string # elements: 1 # length: 31 ClassificationGAM.NumIterations # name: # type: sq_string # elements: 1 # length: 199 ClassificationGAM: property NumIterations Maximum number of iterations A positive integer specifying the maximum number of iterations for the gradient boosting algorithm. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Maximum number of iterations # name: # type: sq_string # elements: 1 # length: 33 ClassificationGAM.NumObservations # name: # type: sq_string # elements: 1 # length: 229 ClassificationGAM: property NumObservations Number of observations A positive integer value specifying the number of observations in the training dataset used for training the ClassificationGAM model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 31 ClassificationGAM.NumPredictors # name: # type: sq_string # elements: 1 # length: 223 ClassificationGAM: property NumPredictors Number of predictors A positive integer value specifying the number of predictors in the training dataset used for training the ClassificationGAM model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of predictors # name: # type: sq_string # elements: 1 # length: 23 ClassificationGAM.Order # name: # type: sq_string # elements: 1 # length: 173 ClassificationGAM: property Order Order of spline fitting A scalar or row vector specifying the order of the spline for each predictor variable. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Order of spline fitting # name: # type: sq_string # elements: 1 # length: 39 ClassificationGAM.PairDetectionBinEdges # name: # type: sq_string # elements: 1 # length: 480 ClassificationGAM: property PairDetectionBinEdges Bin edges used to detect interactions A cell array with one row vector per predictor, holding the coarse cut points the residuals of the predictor phase were laid on while pairs were being tested. The grid is eight equal-frequency bins whatever the sample size, as MATLAB's is. It is empty when the model carries no interaction terms, and empty throughout under the spline engine, which does not bin. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Bin edges used to detect interactions # name: # type: sq_string # elements: 1 # length: 32 ClassificationGAM.PredictorNames # name: # type: sq_string # elements: 1 # length: 256 ClassificationGAM: property PredictorNames Names of predictor variables A cell array of character vectors specifying the names of the predictor variables. The names are in the order in which they appear in the training dataset. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Names of predictor variables # name: # type: sq_string # elements: 1 # length: 23 ClassificationGAM.Prior # name: # type: sq_string # elements: 1 # length: 557 ClassificationGAM: property Prior Prior probability for each class A 2-element numeric vector specifying the prior probabilities for each class. The order of the elements in Prior corresponds to the order of the classes in ClassNames. This property is read-only. Specified as a row vector with one entry per class, in the order of ClassNames, and rescaled to sum to one. It may be given as 'empirical', 'uniform', a numeric vector, or a structure with ClassNames and ClassProbs fields, which assigns each probability by class name rather than by position. # name: # type: sq_string # elements: 1 # length: 32 Prior probability for each class # name: # type: sq_string # elements: 1 # length: 38 ClassificationGAM.ReasonForTermination # name: # type: sq_string # elements: 1 # length: 537 ClassificationGAM: property ReasonForTermination Why each fitting phase stopped A structure with the fields PredictorTrees and InteractionTrees, each a character vector saying why that phase of the fit ended: that it trained the trees it was asked for, or that it could no longer improve the model. A phase that never ran reports an empty character vector, which is what a model with no interaction terms shows for the second field. It is empty under the spline engine, which has no tree budget to exhaust. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 Why each fitting phase stopped # name: # type: sq_string # elements: 1 # length: 30 ClassificationGAM.ResponseName # name: # type: sq_string # elements: 1 # length: 159 ClassificationGAM: property ResponseName Response variable name A character vector specifying the name of the response variable Y. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Response variable name # name: # type: sq_string # elements: 1 # length: 26 ClassificationGAM.RowsUsed # name: # type: sq_string # elements: 1 # length: 383 ClassificationGAM: property RowsUsed Rows used for fitting A logical column vector with the same length as the observations in the original predictor data X, true for each row that was used for fitting the ClassificationGAM model. It is empty, [], when every observation was used, so a non-empty value means that rows holding missing values were dropped. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Rows used for fitting # name: # type: sq_string # elements: 1 # length: 32 ClassificationGAM.ScoreTransform # name: # type: sq_string # elements: 1 # length: 1328 ClassificationGAM: property ScoreTransform Transformation function for classification scores Specified as a function handle for transforming the classification scores. Add or change the ScoreTransform property using dot notation as in: obj.ScoreTransform = 'function_name' obj.ScoreTransform = @function_handle When specified as a character vector, it can be any of the following built-in functions. Nevertheless, the ScoreTransform property always stores their function handle equivalent. Value Description 'doublelogit' 1 ./ (1 + exp (-2 x x)) 'invlogit' log (x ./ (1 - x)) 'ismax' Sets the score for the class with the largest score to 1, and for all other classes to 0 'logit' 1 ./ (1 + exp (-x)) 'none' x (no transformation) 'identity' x (no transformation) 'sign' -1 for x < 0, 0 for x = 0, 1 for x > 0 'symmetric' 2 x x - 1 'symmetricismax' Sets the score for the class with the largest score to 1, and for all other classes to -1 'symmetriclogit' 2 ./ (1 + exp (-x)) - 1 The default is 'logit', as in MATLAB. This model's raw score is a log-odds, reported as the pair [-f, f] whose two columns sum to zero, and the transform is what turns it into the posterior probabilities that sum to one. Every transform therefore composes on the log-odds and not on the probabilities, so 'none' returns the log-odds themselves. # name: # type: sq_string # elements: 1 # length: 49 Transformation function for classification scores # name: # type: sq_string # elements: 1 # length: 27 ClassificationGAM.TreeModel # name: # type: sq_string # elements: 1 # length: 766 ClassificationGAM: property TreeModel The fitted shape functions and interaction surfaces A structure holding what the boosted-tree engine fitted, with fields ShapeValues, one column vector per predictor giving that predictor's contribution in each of its bins, PairValues, one matrix per selected pair, and Pairs, the predictor indices those matrices belong to. A shape function is a step function, so these are the whole of the fit however many trees produced them. MATLAB exposes no equivalent: it reports the bin edges but never the values on them, so its shape functions can only be reached through predict. This property is an Octave extension, and it is empty under the spline engine, whose fit lives in BaseModel and ModelwInt. This property is read-only. # name: # type: sq_string # elements: 1 # length: 51 The fitted shape functions and interaction surfaces # name: # type: sq_string # elements: 1 # length: 19 ClassificationGAM.W # name: # type: sq_string # elements: 1 # length: 368 ClassificationGAM: property W Observation weights A numeric column vector with one entry per observation used for training, normalised to sum to one. It has the class of the 'Weights' given, single or double. This property is read-only. Each class carries its prior, spread over its own observations in proportion to their 'Weights', and evenly when none were given. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 19 ClassificationGAM.X # name: # type: sq_string # elements: 1 # length: 423 ClassificationGAM: property X Predictor data A numeric matrix containing the unstandardized predictor data. Each column of X represents one predictor (variable), and each row represents one observation. This property is read-only. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 19 ClassificationGAM.Y # name: # type: sq_string # elements: 1 # length: 307 ClassificationGAM: property Y Class labels Specified as a logical or numeric column vector, or as a character array or a cell array of character vectors with the same number of rows as the predictor data. Each row in Y is the observed class label for the corresponding row in X. This property is read-only. # name: # type: sq_string # elements: 1 # length: 12 Class labels # name: # type: sq_string # elements: 1 # length: 33 ClassificationGAM.addInteractions # name: # type: sq_string # elements: 1 # length: 1061 ClassificationGAM: obj = addInteractions (obj, interactions) Add interaction terms to a fitted model. obj = addInteractions (obj, interactions) fits the interaction terms named by interactions on top of the terms the model already carries and returns the updated model. The univariate fit is left alone, so predict with 'IncludeInteractions' set false answers exactly as it answered before. interactions takes the forms the constructor's 'Interactions' option takes: a nonnegative integer count of terms, a logical matrix with a column per predictor, or 'all'. A model already carrying interaction terms is not extended, which is what MATLAB refuses too. A model fitted from a 'Formula' names every term it has, interactions among them, and is refused for the same reason. Which terms a count selects is this implementation's own: they are taken in the order nchoosek lists the pairs, where MATLAB ranks them by how much each contributes. The constructor's option chooses the same way, so the two agree with each other. See also: fitcgam, ClassificationGAM # name: # type: sq_string # elements: 1 # length: 40 Add interaction terms to a fitted model. # name: # type: sq_string # elements: 1 # length: 25 ClassificationGAM.compact # name: # type: sq_string # elements: 1 # length: 233 ClassificationGAM: CVMdl = compact (obj) Create a CompactClassificationGAM object. CVMdl = compact (obj) creates a compact version of the ClassificationGAM object, obj. See also: fitcgam, ClassificationGAM, CompactClassificationGAM # name: # type: sq_string # elements: 1 # length: 41 Create a CompactClassificationGAM object. # name: # type: sq_string # elements: 1 # length: 26 ClassificationGAM.crossval # name: # type: sq_string # elements: 1 # length: 1012 ClassificationGAM: CVMdl = crossval (obj) ClassificationGAM: CVMdl = crossval (..., name, value) Cross Validate a Generalized Additive Model classification object. CVMdl = crossval (obj) returns a cross-validated model object, CVMdl, from a trained model, obj, using 10-fold cross-validation by default. CVMdl = crossval (obj, name, value) specifies additional name-value pair arguments to customize the cross-validation process. Name Value 'KFold' Specify the number of folds to use in k-fold cross-validation. "KFold", k, where k is an integer greater than 1. 'Holdout' Specify the fraction of the data to hold out for testing. "Holdout", p, where p is a scalar in the range (0,1). 'Leaveout' Specify whether to perform leave-one-out cross-validation. "Leaveout", Value, where Value is 'on' or 'off'. 'CVPartition' Specify a cvpartition object used for cross-validation. "CVPartition", cv, where isa (cv, "cvpartition") = 1. See also: fitcgam, ClassificationGAM, cvpartition, ClassificationPartitionedModel # name: # type: sq_string # elements: 1 # length: 66 Cross Validate a Generalized Additive Model classification object. # name: # type: sq_string # elements: 1 # length: 22 ClassificationGAM.edge # name: # type: sq_string # elements: 1 # length: 860 ClassificationGAM: e = edge (obj, X, Y) ClassificationGAM: e = edge (..., "Weights", w) ClassificationGAM: e = edge (obj, Tbl, ResponseVarName) ClassificationGAM: e = edge (obj, Tbl) Classification edge of a generalized additive model. e = edge (obj, X, Y) returns the mean of the classification margins over the rows of X. e = edge (..., "Weights", w) takes the weighted mean instead, with one weight per row of X. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: ClassificationGAM, ClassificationGAM.margin, ClassificationGAM.loss, ClassificationGAM.predict # name: # type: sq_string # elements: 1 # length: 52 Classification edge of a generalized additive model. # name: # type: sq_string # elements: 1 # length: 22 ClassificationGAM.loss # name: # type: sq_string # elements: 1 # length: 1346 ClassificationGAM: L = loss (obj, X, Y) ClassificationGAM: L = loss (obj, Tbl, ResponseVarName) ClassificationGAM: L = loss (obj, Tbl) ClassificationGAM: L = loss (..., name, value) Classification loss of a generalized additive model. L = loss (obj, X, Y) returns the loss of the model on the rows of X against the true labels Y. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. L = loss (..., name, value) accepts the following name-value pairs: "LossFun" selects the loss. Supported values are "mincost", the default, "binodeviance", "classifcost", "classiferror", "exponential", "hinge", "logit" and "quadratic". "mincost" assigns each observation to the class of least expected cost and charges what that assignment costs, so it reads the scores as a posterior, which is what this model returns; "classifcost" charges what the model's own prediction costs. "Weights" holds one weight per row of X, normalised to sum to one before it is applied. See also: ClassificationGAM, ClassificationGAM.margin, ClassificationGAM.edge, ClassificationGAM.predict # name: # type: sq_string # elements: 1 # length: 52 Classification loss of a generalized additive model. # name: # type: sq_string # elements: 1 # length: 24 ClassificationGAM.margin # name: # type: sq_string # elements: 1 # length: 914 ClassificationGAM: m = margin (obj, X, Y) ClassificationGAM: m = margin (obj, Tbl, ResponseVarName) ClassificationGAM: m = margin (obj, Tbl) Classification margin of a generalized additive model. m = margin (obj, X, Y) returns a column vector holding, for each row of X, the score the model gives its true class in Y less the score it gives the other class. A positive margin means the observation is classified correctly, and the larger it is the more confidently so. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: ClassificationGAM, ClassificationGAM.edge, ClassificationGAM.loss, ClassificationGAM.predict # name: # type: sq_string # elements: 1 # length: 54 Classification margin of a generalized additive model. # name: # type: sq_string # elements: 1 # length: 25 ClassificationGAM.predict # name: # type: sq_string # elements: 1 # length: 1636 ClassificationGAM: label = predict (obj, XC) ClassificationGAM: [label, score] = predict (obj, XC) ClassificationGAM: [label, score] = predict (..., 'IncludeInteractions', includeInteractions) Predict labels for new data using the Generalized Additive Model (GAM) stored in a ClassificationGAM object. label = predict (obj, XC) returns the predicted labels for the data in XC based on the model stored in the ClassificationGAM object, obj. [label, score] = predict (obj, XC) also returns score, which contains the predicted class scores or posterior probabilities for each observation. Every row is predicted. Under boosted trees a missing value adds nothing from a main effect, and an interaction term takes the value its trees give a row missing that predictor; under splines a row holding a missing value is scored NaN and takes the class of largest prior. [label, score] = predict (obj, XC, 'IncludeInteractions', includeInteractions) allows you to specify whether interaction terms should be included when making predictions. obj must be a ClassificationGAM class object. XC must be an MxP numeric matrix where each row is an observation and each column corresponds to a predictor variable. includeInteractions is a logical scalar indicating whether to include interaction terms in the predictions. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: ClassificationGAM, fitcgam # name: # type: sq_string # elements: 1 # length: 108 Predict labels for new data using the Generalized Additive Model (GAM) stored in a ClassificationGAM object. # name: # type: sq_string # elements: 1 # length: 27 ClassificationGAM.resubEdge # name: # type: sq_string # elements: 1 # length: 165 ClassificationGAM: e = resubEdge (obj) Classification edge of a generalized additive model on its training data. See also: ClassificationGAM, ClassificationGAM.edge # name: # type: sq_string # elements: 1 # length: 73 Classification edge of a generalized additive model on its training data. # name: # type: sq_string # elements: 1 # length: 27 ClassificationGAM.resubLoss # name: # type: sq_string # elements: 1 # length: 217 ClassificationGAM: L = resubLoss (obj) ClassificationGAM: L = resubLoss (..., name, value) Classification loss of a generalized additive model on its training data. See also: ClassificationGAM, ClassificationGAM.loss # name: # type: sq_string # elements: 1 # length: 73 Classification loss of a generalized additive model on its training data. # name: # type: sq_string # elements: 1 # length: 29 ClassificationGAM.resubMargin # name: # type: sq_string # elements: 1 # length: 171 ClassificationGAM: m = resubMargin (obj) Classification margin of a generalized additive model on its training data. See also: ClassificationGAM, ClassificationGAM.margin # name: # type: sq_string # elements: 1 # length: 75 Classification margin of a generalized additive model on its training data. # name: # type: sq_string # elements: 1 # length: 30 ClassificationGAM.resubPredict # name: # type: sq_string # elements: 1 # length: 329 ClassificationGAM: label = resubPredict (obj) ClassificationGAM: [label, score] = resubPredict (obj) Classify the training data with the generalized additive model it was fitted on. label = resubPredict (obj) is predict applied to the observations the model was fitted on. See also: ClassificationGAM, ClassificationGAM.predict # name: # type: sq_string # elements: 1 # length: 80 Classify the training data with the generalized additive model it was fitted on. # name: # type: sq_string # elements: 1 # length: 24 ClassificationGAM.resume # name: # type: sq_string # elements: 1 # length: 949 ClassificationGAM: Mdl = resume (obj, numTrees) Resume training a generalized additive model. Mdl = resume (obj, numTrees) adds numTrees more trees to obj and returns the result. The original model is not modified. Training continues in the phase that ran last, which is what MATLAB does: a model carrying interaction terms gains interaction trees and its predictor shape functions are left alone, while a model without them gains predictor trees. A round starts at its initial learning rate whatever its number, so the model this returns is the model a single fit of the combined budget would have produced. numTrees must be a positive integer scalar. Resuming raises where there is nothing left to gain, rather than returning the model unchanged, and it is not available under 'FitMethod', 'splines': a backfit that has converged to its tolerance has no budget to extend. See also: ClassificationGAM, fitcgam, ClassificationGAM.addInteractions # name: # type: sq_string # elements: 1 # length: 45 Resume training a generalized additive model. # name: # type: sq_string # elements: 1 # length: 27 ClassificationGAM.savemodel # name: # type: sq_string # elements: 1 # length: 458 ClassificationGAM: savemodel (obj, filename) Save a ClassificationGAM object. savemodel (obj, filename) saves each property of a ClassificationGAM object into an Octave binary file, the name of which is specified in filename, along with an extra variable, which defines the type classification object these variables constitute. Use loadmodel in order to load a classification object into Octave's workspace. See also: loadmodel, fitcgam, ClassificationGAM # name: # type: sq_string # elements: 1 # length: 32 Save a ClassificationGAM object. # name: # type: string # elements: 1 # length: 17 ClassificationKNN # name: # type: sq_string # elements: 1 # length: 706 statistics: ClassificationKNN K-nearest neighbors classification The ClassificationKNN class implements a K-nearest neighbor classifier object, which can predict responses for new data using the predict method. The implemented algorithm allows you choose a range of different distance metrics, the number of nearest neighbors, as well as the searching algorithm. The K-nearest neighbors (k-NN) classifier is a simple, non-parametric machine learning algorithm used for classification tasks. It classifies a data point based on the majority class of its k closest neighbors in the feature space. Create a ClassificationKNN object by using the fitcknn function or the class constructor. See also: fitcknn # name: # type: sq_string # elements: 1 # length: 34 K-nearest neighbors classification # name: # type: sq_string # elements: 1 # length: 26 ClassificationKNN.BinEdges # name: # type: sq_string # elements: 1 # length: 352 ClassificationKNN: property BinEdges Bin edges of the predictors A cell array with one entry per predictor, holding that predictor's bin edges where the learner discretized it before fitting. It is empty here and stays empty: this learner fits the predictors as they are, and MATLAB's reports an empty cell for it as well. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Bin edges of the predictors # name: # type: sq_string # elements: 1 # length: 27 ClassificationKNN.BreakTies # name: # type: sq_string # elements: 1 # length: 944 ClassificationKNN: property BreakTies Tie-breaking algorithm A character vector specifying the tie-breaking algorithm used by the predict method, when multiple classes have the same smallest cost. It can be one of the following: 'smallest' (default), which favors the class with the smallest index among the tied groups, i.e. the one that appears first in the training labelled data. 'nearest', which favors the class with the nearest neighbor among the tied groups, i.e. the class with the closest member point according to the distance metric used. 'random', which randomly picks one class among the tied groups. The tie-breaking algorithm is only used when IncludeTies is false. Change the BreakTies property using dot notation as in: obj.BreakTies = algorithm This property may be assigned after fitting. It decides the label when two classes hold the same weight among the neighbours, and it applies whether or not IncludeTies is set. # name: # type: sq_string # elements: 1 # length: 22 Tie-breaking algorithm # name: # type: sq_string # elements: 1 # length: 28 ClassificationKNN.BucketSize # name: # type: sq_string # elements: 1 # length: 323 ClassificationKNN: property BucketSize Maximum data points in each node A positive integer scalar specifying the maximum number of data points in the leaf node of the Kd-tree. BucketSize only applies when the NSMethod property is 'kdtree'. Change the BucketSize property using dot notation as in: obj.BucketSize = maxnum # name: # type: sq_string # elements: 1 # length: 32 Maximum data points in each node # name: # type: sq_string # elements: 1 # length: 27 ClassificationKNN.CacheSize # name: # type: sq_string # elements: 1 # length: 659 ClassificationKNN: property CacheSize Size of the Gram matrix cache A positive scalar giving the cache size in megabytes, 1000 by default. Change the CacheSize property using dot notation as in: obj.CacheSize = newCacheSize This property is stored and reported for compatibility and does not affect the fit or any prediction. A nearest-neighbour model keeps no Gram matrix to cache: it holds the training data and computes each distance when asked. Assigning it changes nothing but the value read back. MATLAB carries the same property and hides it from properties, where this package reports it, so that a value a user may set is a value a user can find. # name: # type: sq_string # elements: 1 # length: 29 Size of the Gram matrix cache # name: # type: sq_string # elements: 1 # length: 39 ClassificationKNN.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 228 ClassificationKNN: property CategoricalPredictors Indices of the categorical predictors A numeric vector of column indices into X naming the predictors treated as categorical, and empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 28 ClassificationKNN.ClassNames # name: # type: sq_string # elements: 1 # length: 399 ClassificationKNN: property ClassNames Names of classes in the response variable An array of unique values of the response variable Y, which has the same data types as the data in Y, sorted or in the order given by the 'ClassNames' option. This property is read-only. ClassNames can have any of the following datatypes: Cell array of character vectors Character array Logical vector Numeric vector # name: # type: sq_string # elements: 1 # length: 41 Names of classes in the response variable # name: # type: sq_string # elements: 1 # length: 35 ClassificationKNN.ClassificationKNN # name: # type: sq_string # elements: 1 # length: 5900 ClassificationKNN: obj = ClassificationKNN (X, Y) ClassificationKNN: obj = ClassificationKNN (Tbl, ResponseVarName) ClassificationKNN: obj = ClassificationKNN (Tbl, formula) ClassificationKNN: obj = ClassificationKNN (Tbl, Y) ClassificationKNN: obj = ClassificationKNN (..., name, value) Create a ClassificationKNN class object containing a k-Nearest Neighbor classification model. obj = ClassificationKNN (X, Y) returns a ClassificationKNN object, with X as the predictor data and Y containing the class labels of observations in X. X must be a NxP numeric matrix of input data where rows correspond to observations and columns correspond to features or variables. X will be used to train the kNN model. Y is Nx1 matrix or cell matrix containing the class labels of corresponding predictor data in X. Y can contain any type of categorical data. Y must have same numbers of Rows as X. obj = ClassificationKNN (..., name, value) returns a ClassificationKNN object with parameters specified by the following name, value paired input arguments: Name Value 'PredictorNames' A cell array of character vectors specifying the names of the predictors. The length of this array must match the number of columns in X. 'ResponseName' A character vector specifying the name of the response variable. 'ClassNames' Names of the classes in the class labels, Y, used for fitting the GAM model. ClassNames are of the same type as the class labels in Y. 'Cost' An NxR numeric matrix containing misclassification cost for the corresponding instances in X, where R is the number of unique categories in Y. If an instance is correctly classified into its category the cost is calculated to be 1, otherwise 0. The cost matrix can be altered by using Mdl.cost = somecost. By default, its value is cost = ones (rows (X), numel (unique (Y))). 'Prior' A numeric vector specifying the prior probabilities for each class. The order of the elements in Prior corresponds to the order of the classes in ClassNames. Alternatively, you can specify 'empirical' to use the empirical class probabilities or 'uniform' to assume equal class probabilities. A class whose prior is 0 is removed from the data before fitting, with a warning, as in MATLAB. 'Weights' A single or double vector of nonnegative observation weights, one per row of X. Each class carries its prior, spread over its observations in proportion to their weights, and a neighbour votes with that weight. An empirical prior sums the weights per class, standardization uses weighted means and standard deviations, and a row of zero weight is left out. The model's W keeps the class of the weights, while every computation runs in double, so Prior is double where MATLAB returns single. 'ScoreTransform' A user-defined function handle or a character vector specifying one of the following builtin functions specifying the transformation applied to predicted classification scores. Supported values include 'doublelogit', 'invlogit', 'ismax', 'logit', 'none', 'identity', 'sign', 'symmetric', 'symmetricismax', and 'symmetriclogit'. 'BreakTies' A character vector specifying the tie-breaking algorithm used by predict method, when multiple classes have the same smallest cost. Available options are 'smallest' (default), which uses the smallest index among tied groups, 'nearest', which uses the class with the nearest neighbor among tied groups, and 'random', which randomly selects one of the tied groups. 'NumNeighbors' A positive integer value that specifies the number of nearest neighbors to be found in the kNN search algorithm for classifying each point during prediction. By default, it is 1. 'Distance' Any valid distance metric supported by the pdist2 function. Note that the allowable distance metrics depend on the selected nearest neighbor search method. 'DistanceWeight' Either a distance weighting function, specified either as a function handle, which accepts a matrix of nonnegative distances and returns a matrix the same size containing nonnegative distance weights, or a character vector with one of the following values: 'equal', which corresponds to no weighting; 'inverse', which corresponds to a weight equal to 1/distance; 'squaredinverse', which corresponds to a weight equal to 1/distance^2. 'Cov' A square matrix with the same number of columns X specifying the covariance matrix for computing the mahalanobis distance. This must be a positive definite matrix matching. This argument is only valid when the selected distance metric is 'mahalanobis'. 'Exponent' A positive scalar (usually an integer) specifying the Minkowski distance exponent. This argument is only valid when the selected distance metric is 'minkowski'. By default, it is 2. An exponent of Inf gives the Chebychev distance. 'Scale' A nonnegative numeric vector specifying the scale parameters for the standardized Euclidean distance. The vector length must be equal to the number of columns in X. This argument is only valid when the selected distance metric is 'seuclidean', in which case each coordinate of X is scaled by the corresponding element of 'scale', as is each query point in Y. By default, the scale parameter is the standard deviation of each coordinate in X. If a variable in X is constant, i.e. zero variance, this value is forced to 1 to avoid division by zero. This is the equivalent of this variable not being standardized. 'NSMethod' A character vector specifying the nearest neighbor search method used by knnsearch, which can be 'kdtree' or 'exhaustive'. See knnsearch for more information about default values and allowable distance metrics for each search method. 'BucketSize' A positive integer value specifying the maximum number of data points in the leaf node of the Kd-tree. This argument is meaningful only when the selected nearest neighbor search method is 'kdtree'. By default, it is 50. See also: fitcknn, knnsearch, rangesearch, pdist2 # name: # type: sq_string # elements: 1 # length: 93 Create a ClassificationKNN class object containing a k-Nearest Neighbor classification model. # name: # type: sq_string # elements: 1 # length: 22 ClassificationKNN.Cost # name: # type: sq_string # elements: 1 # length: 1178 ClassificationKNN: property Cost Cost of Misclassification A square matrix specifying the cost of misclassification of a point. Cost(i,j) is the cost of classifying a point into class j if its true class is i (that is, the rows correspond to the true class and the columns correspond to the predicted class). The order of the rows and columns in Cost corresponds to the order of the classes in ClassNames. The number of rows and columns in Cost is the number of unique classes in the response. By default, Cost(i,j) = 1 if i != j, and Cost(i,j) = 0 if i = j. In other words, the cost is 0 for correct classification and 1 for incorrect classification. Add or change the Cost property using dot notation as in: obj.Cost = costMatrix A cost may also be given as a struct with the fields ClassNames and ClassificationCosts, which names the order its own matrix is written in. That matrix is permuted into the order of ClassNames above, so a caller need not know which order the classes are in. It must name every class. A cost must be floating point, not sparse, not complex, non-negative and zero down its diagonal, and must hold no NaN or Inf. A single is widened to double. # name: # type: sq_string # elements: 1 # length: 25 Cost of Misclassification # name: # type: sq_string # elements: 1 # length: 31 ClassificationKNN.DistParameter # name: # type: sq_string # elements: 1 # length: 1293 ClassificationKNN: property DistParameter Parameter for distance metric A positive definite covariance matrix, a positive scalar, or a vector of positive scale values specifying the parameter for the corresponding distance metric as shown below: 'mahalanobis' accepts a positive definite covariance matrix. 'minkowski' accepts a positive scalar as the Minkowski distance exponent; Inf gives the Chebychev distance. 'seuclidean' accepts a vector of positive scale values of equal length as the number of predictors in X. For any other distance metric, DistParameter is empty ([]). Change the DistParameter property using dot notation as in: obj.DistParameter = distParam This property may be assigned after fitting, but only under the three metrics that carry one: 'minkowski', 'seuclidean' and 'mahalanobis'. Under any other metric there is nothing for it to mean and the assignment is refused. Deviation from MATLAB. A 'seuclidean' scale of zeros is refused here. MATLAB accepts it, then warns from inside its distance routine at predict time and answers anyway, which contradicts its own message that the scale must hold positive values. A zero scale divides that predictor by nothing, so it is rejected where it is given rather than surfacing later as a warning attached to an answer. # name: # type: sq_string # elements: 1 # length: 29 Parameter for distance metric # name: # type: sq_string # elements: 1 # length: 26 ClassificationKNN.Distance # name: # type: sq_string # elements: 1 # length: 1036 ClassificationKNN: property Distance Distance metric A character vector specifying the distance metric used by the neighbor-searcher method, or a function handle to a custom distance function. See the available distance metrics in knnsearch for more info. A custom distance function must have the form D2 = distfun (ZI, ZJ), where ZI is a 1xN vector containing one row of the predictor data, ZJ is an M2xN matrix containing multiple rows of the predictor data, and D2 is an M2x1 vector of distances whose k-th element is the distance between the observations ZI and ZJ(k,:). A custom distance function carries no DistParameter. Change the Distance property using dot notation as in: obj.Distance = newDistance This property may be assigned after fitting. NSMethod is read-only and constrains it: a 'kdtree' model takes 'euclidean', 'cityblock', 'chebychev' and 'minkowski' only, and never a function handle. Assigning a different metric recomputes DistParameter, since a parameter belonging to one metric means nothing under another. # name: # type: sq_string # elements: 1 # length: 15 Distance metric # name: # type: sq_string # elements: 1 # length: 32 ClassificationKNN.DistanceWeight # name: # type: sq_string # elements: 1 # length: 716 ClassificationKNN: property DistanceWeight Distance weighting function A character vector or a function handle specifying the distance weighting function, which can be any of the following values: 'equal', which corresponds to @(d) d. 'inverse', which corresponds to @(d) 1/d. 'squaredinverse', which corresponds to @(d) 1/d.^2. @fcn, which is a function handle that accepts a matrix of nonnegative distances, and returns a matrix the same size containing nonnegative distance weights. Change the DistanceWeight property using dot notation as in: obj.DistanceWeight = newDistanceWeight A character vector naming the weight, or the func2str form of a supplied handle. This property may be assigned after fitting. # name: # type: sq_string # elements: 1 # length: 27 Distance weighting function # name: # type: sq_string # elements: 1 # length: 40 ClassificationKNN.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 261 ClassificationKNN: property ExpandedPredictorNames Names of the predictors as the model expanded them A cell array of character vectors. It matches PredictorNames unless a categorical predictor was expanded into indicator variables. This property is read-only. # name: # type: sq_string # elements: 1 # length: 50 Names of the predictors as the model expanded them # name: # type: sq_string # elements: 1 # length: 51 ClassificationKNN.HyperparameterOptimizationResults # name: # type: sq_string # elements: 1 # length: 359 ClassificationKNN: property HyperparameterOptimizationResults Results of the hyperparameter optimization Always empty. It is declared for MATLAB compatibility, where it holds what an automatic search over the hyperparameters found. This class fits the parameters it is given and runs no such search, so there is nothing to report. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Results of the hyperparameter optimization # name: # type: sq_string # elements: 1 # length: 29 ClassificationKNN.IncludeTies # name: # type: sq_string # elements: 1 # length: 445 ClassificationKNN: property IncludeTies Flag for handling ties A logical scalar specifying whether prediction includes all the neighbors whose distance values are equal to the k^th smallest distance. If IncludeTies is true, prediction includes all of these neighbors. Otherwise, prediction uses exactly k neighbors. Change the IncludeTies property using dot notation as in: obj.IncludeTies = flag This property may be assigned after fitting. # name: # type: sq_string # elements: 1 # length: 22 Flag for handling ties # name: # type: sq_string # elements: 1 # length: 33 ClassificationKNN.ModelParameters # name: # type: sq_string # elements: 1 # length: 784 ClassificationKNN: property ModelParameters Fitting options, as they were given A structure holding the parameters of the fit: NumNeighbors, NSMethod, Distance, BucketSize, IncludeTies, DistanceWeight, BreakTies, Exponent, Cov, Scale, StandardizeData, and the Version, Method and Type tags. Each of the three distance parameters belongs to one metric and is empty under the others: Exponent to 'minkowski', Cov to 'mahalanobis' and Scale to 'seuclidean'. Cov and Scale hold what was passed and stay empty otherwise, while Exponent carries its default of 2 for a 'minkowski' fit that did not name one. What the fit used in every case is the DistParameter property. BucketSize is likewise empty unless the search is 'kdtree', the only method that reads it. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Fitting options, as they were given # name: # type: sq_string # elements: 1 # length: 20 ClassificationKNN.Mu # name: # type: sq_string # elements: 1 # length: 435 ClassificationKNN: property Mu Predictor means A numeric vector of the same length as the columns in X with the mean values corresponding to each predictor. If the predictor variables have not been standardized, then 'obj.Mu' is empty. This property is read-only. Each predictor is summarized from every observation where that predictor is present, so a row holding a missing value in another predictor still contributes to this one. # name: # type: sq_string # elements: 1 # length: 15 Predictor means # name: # type: sq_string # elements: 1 # length: 26 ClassificationKNN.NSMethod # name: # type: sq_string # elements: 1 # length: 402 ClassificationKNN: property NSMethod Nearest neighbor search method A character vector specified as either 'kdtree', which creates and uses a Kd-tree to find nearest neighbors, or 'exhaustive', which uses the exhaustive search algorithm by computing the distance values from all points in X to find nearest neighbors. Change the NSMethod property using dot notation as in: obj.NSMethod = newNSMethod # name: # type: sq_string # elements: 1 # length: 30 Nearest neighbor search method # name: # type: sq_string # elements: 1 # length: 30 ClassificationKNN.NumNeighbors # name: # type: sq_string # elements: 1 # length: 399 ClassificationKNN: property NumNeighbors Number of nearest neighbors A positive integer value specifyingNumber of nearest neighbors in X used to classify each point during prediction. Change the NumNeighbors property using dot notation as in: obj.NumNeighbors = newNumNeighbors This property may be assigned after fitting. A value larger than NumObservations is reduced to it rather than refused. # name: # type: sq_string # elements: 1 # length: 27 Number of nearest neighbors # name: # type: sq_string # elements: 1 # length: 33 ClassificationKNN.NumObservations # name: # type: sq_string # elements: 1 # length: 229 ClassificationKNN: property NumObservations Number of observations A positive integer value specifying the number of observations in the training dataset used for training the ClassificationKNN model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 31 ClassificationKNN.NumPredictors # name: # type: sq_string # elements: 1 # length: 223 ClassificationKNN: property NumPredictors Number of predictors A positive integer value specifying the number of predictors in the training dataset used for training the ClassificationKNN model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of predictors # name: # type: sq_string # elements: 1 # length: 32 ClassificationKNN.PredictorNames # name: # type: sq_string # elements: 1 # length: 256 ClassificationKNN: property PredictorNames Names of predictor variables A cell array of character vectors specifying the names of the predictor variables. The names are in the order in which they appear in the training dataset. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Names of predictor variables # name: # type: sq_string # elements: 1 # length: 23 ClassificationKNN.Prior # name: # type: sq_string # elements: 1 # length: 604 ClassificationKNN: property Prior Prior probability for each class A numeric vector specifying the prior probabilities for each class. The order of the elements in Prior corresponds to the order of the classes in ClassNames. Add or change the Prior property using dot notation as in: obj.Prior = priorVector Specified as a row vector with one entry per class, in the order of ClassNames, and rescaled to sum to one. It may be given as 'empirical', 'uniform', a numeric vector, or a structure with ClassNames and ClassProbs fields, which assigns each probability by class name rather than by position. # name: # type: sq_string # elements: 1 # length: 32 Prior probability for each class # name: # type: sq_string # elements: 1 # length: 30 ClassificationKNN.ResponseName # name: # type: sq_string # elements: 1 # length: 159 ClassificationKNN: property ResponseName Response variable name A character vector specifying the name of the response variable Y. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Response variable name # name: # type: sq_string # elements: 1 # length: 26 ClassificationKNN.RowsUsed # name: # type: sq_string # elements: 1 # length: 458 ClassificationKNN: property RowsUsed Rows used for fitting A logical column vector with the same length as the observations in the original predictor data X, true for each row that was used for fitting the ClassificationKNN model. It is empty, [], when every observation was used. A row is dropped when its class is missing, when every predictor is missing, or when its weight is zero; a row missing only some predictors is used. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Rows used for fitting # name: # type: sq_string # elements: 1 # length: 32 ClassificationKNN.ScoreTransform # name: # type: sq_string # elements: 1 # length: 982 ClassificationKNN: property ScoreTransform Transformation function for classification scores Specified as a function handle for transforming the classification scores. Add or change the ScoreTransform property using dot notation as in: obj.ScoreTransform = 'function_name' obj.ScoreTransform = @function_handle When specified as a character vector, it can be any of the following built-in functions. Nevertheless, the ScoreTransform property always stores their function handle equivalent. Value Description 'doublelogit' 1 ./ (1 + exp (-2 x x)) 'invlogit' log (x ./ (1 - x)) 'ismax' Sets the score for the class with the largest score to 1, and for all other classes to 0 'logit' 1 ./ (1 + exp (-x)) 'none' x (no transformation) 'identity' x (no transformation) 'sign' -1 for x < 0, 0 for x = 0, 1 for x > 0 'symmetric' 2 x x - 1 'symmetricismax' Sets the score for the class with the largest score to 1, and for all other classes to -1 'symmetriclogit' 2 ./ (1 + exp (-x)) - 1 # name: # type: sq_string # elements: 1 # length: 49 Transformation function for classification scores # name: # type: sq_string # elements: 1 # length: 23 ClassificationKNN.Sigma # name: # type: sq_string # elements: 1 # length: 463 ClassificationKNN: property Sigma Predictor standard deviations A numeric vector of the same length as the columns in X with the standard deviations corresponding to each predictor. If the predictor variables have not been standardized, then 'obj.Sigma' is empty. This property is read-only. Each predictor is summarized from every observation where that predictor is present, so a row holding a missing value in another predictor still contributes to this one. # name: # type: sq_string # elements: 1 # length: 29 Predictor standard deviations # name: # type: sq_string # elements: 1 # length: 19 ClassificationKNN.W # name: # type: sq_string # elements: 1 # length: 370 ClassificationKNN: property W Observation weights A numeric column vector with one entry per observation used for fitting. Each class carries its prior, spread over its own observations in proportion to the 'Weights' given, or evenly when none were. Reassigning Prior re-derives it. It has the class of the 'Weights' given, single or double. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 19 ClassificationKNN.X # name: # type: sq_string # elements: 1 # length: 423 ClassificationKNN: property X Predictor data A numeric matrix containing the unstandardized predictor data. Each column of X represents one predictor (variable), and each row represents one observation. This property is read-only. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 19 ClassificationKNN.Y # name: # type: sq_string # elements: 1 # length: 307 ClassificationKNN: property Y Class labels Specified as a logical or numeric column vector, or as a character array or a cell array of character vectors with the same number of rows as the predictor data. Each row in Y is the observed class label for the corresponding row in X. This property is read-only. # name: # type: sq_string # elements: 1 # length: 12 Class labels # name: # type: sq_string # elements: 1 # length: 26 ClassificationKNN.crossval # name: # type: sq_string # elements: 1 # length: 988 ClassificationKNN: CVMdl = crossval (obj) ClassificationKNN: CVMdl = crossval (..., Name, Value) Cross Validate a ClassificationKNN object. CVMdl = crossval (obj) returns a cross-validated model object, CVMdl, from a trained model, obj, using 10-fold cross-validation by default. CVMdl = crossval (obj, name, value) specifies additional name-value pair arguments to customize the cross-validation process. Name Value 'KFold' Specify the number of folds to use in k-fold cross-validation. "KFold", k, where k is an integer greater than 1. 'Holdout' Specify the fraction of the data to hold out for testing. "Holdout", p, where p is a scalar in the range (0,1). 'Leaveout' Specify whether to perform leave-one-out cross-validation. "Leaveout", Value, where Value is 'on' or 'off'. 'CVPartition' Specify a cvpartition object used for cross-validation. "CVPartition", cv, where isa (cv, "cvpartition") = 1. See also: fitcknn, ClassificationKNN, cvpartition, ClassificationPartitionedModel # name: # type: sq_string # elements: 1 # length: 42 Cross Validate a ClassificationKNN object. # name: # type: sq_string # elements: 1 # length: 22 ClassificationKNN.edge # name: # type: sq_string # elements: 1 # length: 1005 ClassificationKNN: e = edge (obj, X, Y) ClassificationKNN: e = edge (..., "Weights", w) ClassificationKNN: e = edge (obj, Tbl, ResponseVarName) ClassificationKNN: e = edge (obj, Tbl) Classification edge, the mean of the classification margins. e = edge (obj, X, Y) reduces the vector that margin returns to a single number, the mean margin over the rows of X. It says how far the model puts the true class ahead of its nearest rival on average, so a larger edge is a better model, and unlike a loss it is not bounded above and rewards confidence rather than bare correctness. e = edge (..., "Weights", w) takes the weighted mean instead, with one weight per row of X. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. # name: # type: sq_string # elements: 1 # length: 60 Classification edge, the mean of the classification margins. # name: # type: sq_string # elements: 1 # length: 22 ClassificationKNN.loss # name: # type: sq_string # elements: 1 # length: 2166 ClassificationKNN: L = loss (obj, X, Y) ClassificationKNN: L = loss (obj, Tbl, ResponseVarName) ClassificationKNN: L = loss (obj, Tbl) ClassificationKNN: L = loss (..., name, value) Compute loss for a trained ClassificationKNN object. L = loss (obj, X, Y) computes the loss, L, using the default loss function 'mincost'. obj is a ClassificationKNN object trained on X and Y. X must be a NxP numeric matrix of input data where rows correspond to observations and columns correspond to features or variables. Y is Nx1 matrix or cell matrix containing the class labels of corresponding predictor data in X. Y must have same numbers of Rows as X. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. L = loss (..., name, value) allows additional options specified by name-value pairs: Name Value 'LossFun' Specifies the loss function to use. Can be a function handle with four input arguments (C, S, W, Cost) which returns a scalar value or one of: 'binodeviance', 'classifcost', 'classiferror', 'exponential', 'hinge', 'logit','mincost', 'quadratic'. C is a logical matrix of size NxK, where N is the number of observations and K is the number of classes. The element C(i,j) is true if the class label of the i-th observation is equal to the j-th class. S is a numeric matrix of size NxK, where each element represents the classification score for the corresponding class. W is a numeric vector of length N, representing the observation weights. Cost is a KxK matrix representing the misclassification costs. 'Weights' Specifies observation weights, must be a numeric vector of length equal to the number of rows in X. Default is ones (size (X, 1)). loss normalizes the weights so that observation weights in each class sum to the prior probability of that class. When you supply Weights, loss computes the weighted classification loss. See also: fitcknn, ClassificationKNN # name: # type: sq_string # elements: 1 # length: 52 Compute loss for a trained ClassificationKNN object. # name: # type: sq_string # elements: 1 # length: 24 ClassificationKNN.margin # name: # type: sq_string # elements: 1 # length: 1166 ClassificationKNN: m = margin (obj, X, Y) ClassificationKNN: m = margin (obj, Tbl, ResponseVarName) ClassificationKNN: m = margin (obj, Tbl) m = margin (obj, X, Y) returns the classification margins for obj with data X and classification Y. m is a numeric vector of length size (X,1). obj is a ClassificationKNN object trained on X and Y. X must be a NxP numeric matrix of input data where rows correspond to observations and columns correspond to features or variables. Y is Nx1 matrix or cell matrix containing the class labels of corresponding predictor data in X. Y must have same numbers of Rows as X. The classification margin for each observation is the difference between the classification score for the true class and the maximal classification score for the false classes. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: fitcknn, ClassificationKNN # name: # type: sq_string # elements: 1 # length: 99 m = margin (obj, X, Y) returns the classification margins for obj with data X and classification Y. # name: # type: sq_string # elements: 1 # length: 25 ClassificationKNN.predict # name: # type: sq_string # elements: 1 # length: 1546 ClassificationKNN: labels = predict (obj, XC) ClassificationKNN: [labels, scores, cost] = predict (obj, XC) Classify new data points into categories using the kNN algorithm from a k-Nearest Neighbor classification model. labels = predict (obj, XC) returns the matrix of labels predicted for the corresponding instances in XC, using the predictor data in obj.X and corresponding labels, obj.Y, stored in the k-Nearest Neighbor classification model, obj. obj must be a ClassificationKNN class object. XC must be an MxP numeric matrix with the same number of features P as the corresponding predictors of the SVM model in obj. [labels, scores, cost] = predict (obj, XC) also returns scores, which contains the predicted class scores or posterior probabilities for each instance of the corresponding unique classes, and cost, which is a matrix containing the expected cost of the classifications. By default, scores returns the posterior probabilities for KNN models, unless a specific ScoreTransform function has been specified. See fitcknn for more info. Note! predict is explicitly using 'exhaustive' as the nearest search method due to the very slow implementation of 'kdtree' in the knnsearch function. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: fitcknn, ClassificationKNN, knnsearch # name: # type: sq_string # elements: 1 # length: 112 Classify new data points into categories using the kNN algorithm from a k-Nearest Neighbor classification model. # name: # type: sq_string # elements: 1 # length: 27 ClassificationKNN.resubEdge # name: # type: sq_string # elements: 1 # length: 204 ClassificationKNN: e = resubEdge (obj) Classification edge of the model on its own training data. e = resubEdge (obj) is edge applied to the observations the model was fitted on, the mean of resubMargin. # name: # type: sq_string # elements: 1 # length: 58 Classification edge of the model on its own training data. # name: # type: sq_string # elements: 1 # length: 27 ClassificationKNN.resubLoss # name: # type: sq_string # elements: 1 # length: 563 ClassificationKNN: L = resubLoss (obj) ClassificationKNN: L = resubLoss (..., name, value) Classification loss of the model on its own training data. L = resubLoss (obj) is loss applied to the observations the model was fitted on, defaulting to 'mincost', and it accepts the same Name-Value pairs. Being a resubstitution quantity it is a lower bound on the error rather than an estimate of it. It is worth least on a lazy learner: a one-neighbour ClassificationKNN has a resubstitution loss of exactly zero, every training point being its own nearest neighbour. # name: # type: sq_string # elements: 1 # length: 58 Classification loss of the model on its own training data. # name: # type: sq_string # elements: 1 # length: 29 ClassificationKNN.resubMargin # name: # type: sq_string # elements: 1 # length: 282 ClassificationKNN: m = resubMargin (obj) Classification margins of the model on its own training data. m = resubMargin (obj) is margin applied to the observations the model was fitted on, one number per observation. Being a resubstitution quantity it is optimistic by construction. # name: # type: sq_string # elements: 1 # length: 61 Classification margins of the model on its own training data. # name: # type: sq_string # elements: 1 # length: 30 ClassificationKNN.resubPredict # name: # type: sq_string # elements: 1 # length: 593 ClassificationKNN: label = resubPredict (obj) ClassificationKNN: [label, score, cost] = resubPredict (obj) Classify the training data with the model fitted to it. label = resubPredict (obj) is predict applied to the observations the model was fitted on, which it holds in X. Handing them over yourself is not the same thing: a row dropped for a missing response is not in X, so the original matrix and the model's own are different data. The result measures fit and not generalization, and is optimistic by construction. crossval is what estimates performance on data the model has not seen. # name: # type: sq_string # elements: 1 # length: 55 Classify the training data with the model fitted to it. # name: # type: sq_string # elements: 1 # length: 27 ClassificationKNN.savemodel # name: # type: sq_string # elements: 1 # length: 458 ClassificationKNN: savemodel (obj, filename) Save a ClassificationKNN object. savemodel (obj, filename) saves each property of a ClassificationKNN object into an Octave binary file, the name of which is specified in filename, along with an extra variable, which defines the type classification object these variables constitute. Use loadmodel in order to load a classification object into Octave's workspace. See also: loadmodel, fitcknn, ClassificationKNN # name: # type: sq_string # elements: 1 # length: 32 Save a ClassificationKNN object. # name: # type: sq_string # elements: 1 # length: 20 ClassificationKernel # name: # type: sq_string # elements: 1 # length: 1306 statistics: ClassificationKernel Gaussian kernel binary classifier for large data. A ClassificationKernel object maps the predictors into a randomized feature space whose inner product approximates a Gaussian kernel, and then fits a linear model there. A kernel classifier is therefore as nonlinear as a support vector machine with a Gaussian kernel, while costing what a linear fit costs: nothing of size NxN is ever formed. The expansion is the random Fourier basis of Rahimi and Recht, drawn once when the model is fitted and kept with it, so predict maps new data through the same basis. MATLAB approximates the same kernel by the Fastfood construction, which reaches the same distribution more cheaply; the two are interchangeable in distribution but not draw by draw, and the draws come from different generators in any case, so the scores of a model fitted here and one fitted in MATLAB differ even from the same seed. What does not differ is what they estimate. Like ClassificationLinear the object holds no copy of the training data. It does hold the basis and the coefficients, so it is bounded by the number of expansion dimensions rather than by the number of observations. Create a ClassificationKernel object with fitckernel. See also: fitckernel, ClassificationLinear, ClassificationSVM # name: # type: sq_string # elements: 1 # length: 49 Gaussian kernel binary classifier for large data. # name: # type: sq_string # elements: 1 # length: 34 ClassificationKernel.BoxConstraint # name: # type: sq_string # elements: 1 # length: 309 ClassificationKernel: property BoxConstraint Box constraint of the support vector machine A positive scalar. It is the reciprocal of the product of Lambda and the number of observations, so setting either of the two in the constructor fixes the other, and giving both is an error. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Box constraint of the support vector machine # name: # type: sq_string # elements: 1 # length: 42 ClassificationKernel.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 190 ClassificationKernel: property CategoricalPredictors Indices of the categorical predictors A row vector of column indices, empty when every predictor is numeric. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 31 ClassificationKernel.ClassNames # name: # type: sq_string # elements: 1 # length: 305 ClassificationKernel: property ClassNames Names of the two classes A column of the same type as the response supplied to the constructor, sorted or in the order given by the 'ClassNames' option. The second of the two is the positive class, the one a positive score belongs to. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Names of the two classes # name: # type: sq_string # elements: 1 # length: 41 ClassificationKernel.ClassificationKernel # name: # type: sq_string # elements: 1 # length: 3261 ClassificationKernel: obj = ClassificationKernel (X, Y) ClassificationKernel: obj = ClassificationKernel (Tbl, ResponseVarName) ClassificationKernel: obj = ClassificationKernel (Tbl, formula) ClassificationKernel: obj = ClassificationKernel (Tbl, Y) ClassificationKernel: obj = ClassificationKernel (..., name, value) Fit a Gaussian kernel binary classifier. obj = ClassificationKernel (X, Y) fits a support vector machine in a randomized Gaussian kernel space to the NxP predictor matrix X and the Nx1 response Y, which must name exactly two classes. obj = ClassificationKernel (..., name, value) takes the following Name-Value pairs. Name Value 'Learner' 'svm', the default, or 'logistic'. 'NumExpansionDimensions' 'auto', the default, or a positive integer. 'KernelScale' 1 by default, a positive scalar, or 'auto', which takes the median distance between the observations. 'Lambda' 'auto', the default, which is the reciprocal of the number of observations, or a nonnegative scalar. It cannot be given beside 'BoxConstraint'. 'BoxConstraint' A positive scalar, 1 by default. It applies to a support vector machine alone. 'Standardize' Whether to centre and scale the predictors, false by default. 'BetaTolerance' Relative tolerance on the coefficients, 1e-4 by default. 'GradientTolerance' Absolute tolerance on the gradient's infinity norm, 1e-6 by default. 'IterationLimit' Largest number of iterations, 1000 by default. 'HessianHistorySize' Number of curvature pairs the solver keeps, 15 by default. 'BlockSize' Memory the expansion may occupy, in megabytes, 4e3 by default. 'ClassNames' The classes to keep, given in the type of Y, in the order the model keeps them; by default they are sorted. 'Cost' A square misclassification cost matrix. Or a structure with fields ClassNames and ClassificationCosts, which names its own order of the classes and is permuted to the model's. 'Prior' 'empirical', the default, 'uniform', a vector of probabilities, or a structure with ClassNames and ClassProbs fields. A class whose prior is 0, or whose misclassification costs are all 0, is removed from the data before fitting, with a warning, as in MATLAB. 'ScoreTransform' A transformation applied to the scores, named or given as a function handle. 'Weights' One nonnegative weight per observation, as a single or double vector. Every computation runs in double. 'PredictorNames' One name per predictor. 'ResponseName' A name for the response. 'CategoricalPredictors' The categorical predictors, as indices, as a logical vector with one element per predictor, or as 'all'. Each is dummy coded in its place, one column of zeros and ones per distinct value it takes in the training data, named as in 'x1 == 2', and the coded columns are not standardized. A row holding a value the training data did not is scored NaN and labelled with the class of largest prior, as a row missing a predictor is. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. The fit is always by limited-memory BFGS, the only solver MATLAB offers a kernel model, and always under a ridge penalty. See also: fitckernel, ClassificationLinear # name: # type: sq_string # elements: 1 # length: 40 Fit a Gaussian kernel binary classifier. # name: # type: sq_string # elements: 1 # length: 25 ClassificationKernel.Cost # name: # type: sq_string # elements: 1 # length: 594 ClassificationKernel: property Cost Cost of misclassifying an observation A square numeric matrix with one row and one column per class, whose (i,j) element is the cost of classifying an observation of class i into class j. It defaults to one everywhere except the diagonal, which is zero. This property is read-only, as it is in MATLAB; a cost matrix is given to the constructor instead. The costs are folded into the prior before the observations are weighted, so a class that is costlier to misclassify weighs more in the fit. They are read again by the 'mincost' and 'classifcost' losses. # name: # type: sq_string # elements: 1 # length: 37 Cost of misclassifying an observation # name: # type: sq_string # elements: 1 # length: 43 ClassificationKernel.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 323 ClassificationKernel: property ExpandedPredictorNames Names of the predictors as the fit saw them A cell array of character vectors. It equals PredictorNames unless categorical predictors were dummy coded, each then named once per level, as in 'x1 == 2'. The expansion dimensions have no names. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Names of the predictors as the fit saw them # name: # type: sq_string # elements: 1 # length: 31 ClassificationKernel.FittedLoss # name: # type: sq_string # elements: 1 # length: 178 ClassificationKernel: property FittedLoss Loss function the fit minimized 'hinge' for a support vector machine and 'logit' for a logistic regression. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Loss function the fit minimized # name: # type: sq_string # elements: 1 # length: 32 ClassificationKernel.KernelScale # name: # type: sq_string # elements: 1 # length: 235 ClassificationKernel: property KernelScale Scale of the Gaussian kernel A positive scalar dividing every predictor before the expansion, so a larger scale makes the kernel wider and the classifier smoother. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Scale of the Gaussian kernel # name: # type: sq_string # elements: 1 # length: 27 ClassificationKernel.Lambda # name: # type: sq_string # elements: 1 # length: 191 ClassificationKernel: property Lambda Regularization strength A nonnegative scalar, the reciprocal of the product of BoxConstraint and the number of observations. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Regularization strength # name: # type: sq_string # elements: 1 # length: 28 ClassificationKernel.Learner # name: # type: sq_string # elements: 1 # length: 137 ClassificationKernel: property Learner Linear model fitted in the expanded space Either 'svm' or 'logistic'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 41 Linear model fitted in the expanded space # name: # type: sq_string # elements: 1 # length: 36 ClassificationKernel.ModelParameters # name: # type: sq_string # elements: 1 # length: 236 ClassificationKernel: property ModelParameters Fitting options, as they were given A structure holding every parameter of the fit, with the 'auto' values as they were given rather than as they were resolved. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Fitting options, as they were given # name: # type: sq_string # elements: 1 # length: 23 ClassificationKernel.Mu # name: # type: sq_string # elements: 1 # length: 201 ClassificationKernel: property Mu Predictor means used to standardize A row vector with one element per predictor, or empty when the model was fitted without standardizing. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Predictor means used to standardize # name: # type: sq_string # elements: 1 # length: 43 ClassificationKernel.NumExpansionDimensions # name: # type: sq_string # elements: 1 # length: 342 ClassificationKernel: property NumExpansionDimensions Number of dimensions of the expanded space A positive integer scalar. It defaults to 2 .^ ceil (min (log2 (p) + 5, 15)) for p predictors, so four predictors give 128 dimensions. More dimensions approximate the kernel more closely and cost proportionally more. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Number of dimensions of the expanded space # name: # type: sq_string # elements: 1 # length: 35 ClassificationKernel.PredictorNames # name: # type: sq_string # elements: 1 # length: 215 ClassificationKernel: property PredictorNames Names of the predictors A cell array of character vectors with one name per column of the training data, defaulting to 'x1', 'x2' and so on. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 26 ClassificationKernel.Prior # name: # type: sq_string # elements: 1 # length: 252 ClassificationKernel: property Prior Prior probability of each class A numeric row vector with one element per class, in the order of ClassNames and summing to one. It defaults to the class frequencies of the training data. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Prior probability of each class # name: # type: sq_string # elements: 1 # length: 35 ClassificationKernel.Regularization # name: # type: sq_string # elements: 1 # length: 211 ClassificationKernel: property Regularization Penalty on the coefficients Always 'ridge (L2)': a kernel model fits in the expanded space, where a lasso penalty has nothing to select. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Penalty on the coefficients # name: # type: sq_string # elements: 1 # length: 33 ClassificationKernel.ResponseName # name: # type: sq_string # elements: 1 # length: 132 ClassificationKernel: property ResponseName Name of the response A character vector, defaulting to 'Y'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Name of the response # name: # type: sq_string # elements: 1 # length: 35 ClassificationKernel.ScoreTransform # name: # type: sq_string # elements: 1 # length: 313 ClassificationKernel: property ScoreTransform Transformation applied to the predicted scores A character vector naming a transformation, or the text of the function handle that was supplied. Assigning to it accepts either. It defaults to 'logit' for a logistic learner and to 'none' for a support vector machine. # name: # type: sq_string # elements: 1 # length: 46 Transformation applied to the predicted scores # name: # type: sq_string # elements: 1 # length: 26 ClassificationKernel.Sigma # name: # type: sq_string # elements: 1 # length: 218 ClassificationKernel: property Sigma Predictor standard deviations used to standardize A row vector with one element per predictor, or empty when the model was fitted without standardizing. This property is read-only. # name: # type: sq_string # elements: 1 # length: 49 Predictor standard deviations used to standardize # name: # type: sq_string # elements: 1 # length: 25 ClassificationKernel.edge # name: # type: sq_string # elements: 1 # length: 668 ClassificationKernel: e = edge (obj, X, Y) ClassificationKernel: e = edge (..., 'Weights', W) ClassificationKernel: e = edge (obj, Tbl, ResponseVarName) ClassificationKernel: e = edge (obj, Tbl) Weighted mean of the classification margins. The weights are normalized within each class to that class's prior before they are applied. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. # name: # type: sq_string # elements: 1 # length: 44 Weighted mean of the classification margins. # name: # type: sq_string # elements: 1 # length: 25 ClassificationKernel.loss # name: # type: sq_string # elements: 1 # length: 796 ClassificationKernel: l = loss (obj, X, Y) ClassificationKernel: l = loss (obj, Tbl, ResponseVarName) ClassificationKernel: l = loss (obj, Tbl) ClassificationKernel: l = loss (..., name, value) Classification loss on new data. l = loss (obj, X, Y) returns the misclassification rate. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. l = loss (..., name, value) takes 'LossFun', one of 'binodeviance', 'classifcost', 'classiferror', 'exponential', 'hinge', 'logit', 'mincost' and 'quadratic', and 'Weights'. # name: # type: sq_string # elements: 1 # length: 32 Classification loss on new data. # name: # type: sq_string # elements: 1 # length: 27 ClassificationKernel.margin # name: # type: sq_string # elements: 1 # length: 673 ClassificationKernel: m = margin (obj, X, Y) ClassificationKernel: m = margin (obj, Tbl, ResponseVarName) ClassificationKernel: m = margin (obj, Tbl) Classification margin of each observation. m = margin (obj, X, Y) returns the score of the true class less the score of the other one. A positive margin is a correct classification. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. # name: # type: sq_string # elements: 1 # length: 42 Classification margin of each observation. # name: # type: sq_string # elements: 1 # length: 28 ClassificationKernel.predict # name: # type: sq_string # elements: 1 # length: 687 ClassificationKernel: labels = predict (obj, XC) ClassificationKernel: [labels, scores] = predict (obj, XC) Classify new observations. labels = predict (obj, XC) maps each row of XC through the model's own random basis and returns the class of largest score. [labels, scores] = predict (obj, XC) also returns the Nx2 scores, whose columns follow ClassNames, after ScoreTransform has been applied. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. # name: # type: sq_string # elements: 1 # length: 26 Classify new observations. # name: # type: sq_string # elements: 1 # length: 27 ClassificationKernel.resume # name: # type: sq_string # elements: 1 # length: 1228 ClassificationKernel: obj = resume (obj, X, Y) ClassificationKernel: obj = resume (..., name, value) ClassificationKernel: obj = resume (obj, Tbl, ResponseVarName) Continue fitting a kernel classifier. obj = resume (obj, X, Y) restarts the optimization from the coefficients the model already carries, through the basis it already holds, and returns the model it reaches. It takes 'BetaTolerance', 'GradientTolerance' and 'IterationLimit', each defaulting to what the model was fitted with, and 'Weights'. X and Y must be the data the model was fitted to; the object keeps no copy of them, which is what makes it small. Neither does it keep the observation weights, so a model fitted with 'Weights' must be given them again here or it will resume against uniform ones. MATLAB behaves the same way: measured on R2024a, resuming a weighted fit without passing the weights back reaches the objective of the unweighted fit. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. The response is then named by ResponseVarName, a variable of the table, or given beside the table as Y; unlike ClassificationKernel.loss, it is never taken from the table unasked. # name: # type: sq_string # elements: 1 # length: 37 Continue fitting a kernel classifier. # name: # type: sq_string # elements: 1 # length: 30 ClassificationKernel.savemodel # name: # type: sq_string # elements: 1 # length: 206 ClassificationKernel: savemodel (obj, filename) Save a kernel classifier to a file. savemodel (obj, filename) saves the model obj into filename in a form loadmodel can read back, the random basis included. # name: # type: sq_string # elements: 1 # length: 35 Save a kernel classifier to a file. # name: # type: sq_string # elements: 1 # length: 20 ClassificationLinear # name: # type: sq_string # elements: 1 # length: 1084 statistics: ClassificationLinear Linear binary classifier for high dimensional data. A ClassificationLinear object fits a linear model, X * Beta + Bias, to a two class problem by minimizing a regularized average loss. The loss is the hinge loss for a support vector machine and the deviance for a logistic regression, and the penalty is either a ridge or a lasso one. Unlike the other classifiers of this package the object holds no copy of the training data: the coefficients, the intercept and the fitting options are the whole model. That is what makes it suited to data with more predictors than an in memory kernel matrix could carry, and it is why the class has no compact method and no resubstitution methods. A vector of regularization strengths fits one model per value in a single object. Beta is then a PxL matrix and Bias a 1xL row, every method returns one column per strength, and selectModels narrows the object down to the strengths worth keeping. Create a ClassificationLinear object with fitclinear. See also: fitclinear, ClassificationKernel, ClassificationSVM # name: # type: sq_string # elements: 1 # length: 51 Linear binary classifier for high dimensional data. # name: # type: sq_string # elements: 1 # length: 25 ClassificationLinear.Beta # name: # type: sq_string # elements: 1 # length: 198 ClassificationLinear: property Beta Fitted linear coefficients A Px1 column, or a PxL matrix with one column per regularization strength when Lambda holds more than one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Fitted linear coefficients # name: # type: sq_string # elements: 1 # length: 25 ClassificationLinear.Bias # name: # type: sq_string # elements: 1 # length: 227 ClassificationLinear: property Bias Fitted intercept A scalar, or a 1xL row with one element per regularization strength. It is zero throughout when the model was fitted with 'FitBias' set to false. This property is read-only. # name: # type: sq_string # elements: 1 # length: 16 Fitted intercept # name: # type: sq_string # elements: 1 # length: 42 ClassificationLinear.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 190 ClassificationLinear: property CategoricalPredictors Indices of the categorical predictors A row vector of column indices, empty when every predictor is numeric. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 31 ClassificationLinear.ClassNames # name: # type: sq_string # elements: 1 # length: 398 ClassificationLinear: property ClassNames Names of the two classes A column of the same type as the response supplied to the constructor: a cell array of character vectors, a numeric vector, a logical vector or a character matrix, sorted or in the order given by the 'ClassNames' option. The second of the two is the positive class, the one a positive score belongs to. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Names of the two classes # name: # type: sq_string # elements: 1 # length: 41 ClassificationLinear.ClassificationLinear # name: # type: sq_string # elements: 1 # length: 4750 ClassificationLinear: obj = ClassificationLinear (X, Y) ClassificationLinear: obj = ClassificationLinear (Tbl, ResponseVarName) ClassificationLinear: obj = ClassificationLinear (Tbl, formula) ClassificationLinear: obj = ClassificationLinear (Tbl, Y) ClassificationLinear: obj = ClassificationLinear (..., name, value) Fit a linear binary classifier. obj = ClassificationLinear (X, Y) fits a linear support vector machine to the NxP predictor matrix X and the Nx1 response Y, which must name exactly two classes. obj = ClassificationLinear (..., name, value) takes the following Name-Value pairs. Name Value 'Learner' 'svm', the default, or 'logistic'. The first minimizes the hinge loss and the second the deviance. 'Regularization' 'ridge' or 'lasso'. It defaults to 'lasso' when the solver is 'sparsa' and to 'ridge' otherwise. 'Lambda' 'auto', the default, which is the reciprocal of the number of observations, or a nonnegative scalar, or a vector of them. A vector fits one model per value. 'Solver' One of 'sgd', 'asgd', 'dual', 'bfgs', 'lbfgs' and 'sparsa', or a cell array of them applied in turn, each warm starting the next. The default depends on the data and the penalty, as described below. 'Beta' Initial coefficients, a Px1 column or a PxL matrix. It defaults to zeros. 'Bias' Initial intercept, a scalar or a 1xL row. It defaults to the weighted average of the class labels for a logistic learner and to zero for a support vector machine. 'FitBias' Whether to fit an intercept at all, true by default. 'PostFitBias' Whether to refit the intercept once the coefficients are settled, false by default. 'ObservationsIn' 'rows', the default, or 'columns', which transposes X before fitting. 'BetaTolerance' Relative tolerance on the coefficients, 1e-4 by default. 'GradientTolerance' Absolute tolerance on the gradient's infinity norm, 1e-6 by default. 'DeltaGradientTolerance' Tolerance on the complementarity gap of the 'dual' solver, 1 by default for a hinge loss. MathWorks documents 0.1, which is the default of the regression counterpart; R2024a and R2026a both report 1 here. 'IterationLimit' Largest number of iterations, 1000 by default. 'PassLimit' Largest number of passes over the data for the stochastic solvers, 1 by default, and 10 for 'dual'. 'BatchSize' Mini-batch size of the stochastic solvers, 10 by default. 'BatchLimit' Largest number of mini-batches. 'LearnRate' Step size of the stochastic solvers. 'OptimizeLearnRate' Whether to halve the step size when the objective rises, true by default. 'TruncationPeriod' Number of mini-batches between soft thresholdings under a lasso penalty, 10 by default. 'NumCheckConvergence' Number of passes between convergence checks of the 'dual' solver, 2 by default. MathWorks documents 5; R2024a and R2026a both report 2, so the documentation is stale rather than the releases being inconsistent. 'HessianHistorySize' Number of curvature pairs the quasi-Newton solvers keep, 15 by default. 'ClassNames' The classes to keep, given in the type of Y. Observations of any other class are dropped. The model keeps the classes in this order; by default they are sorted. 'Cost' A square misclassification cost matrix. Or a structure with fields ClassNames and ClassificationCosts, which names its own order of the classes and is permuted to the model's. 'Prior' 'empirical', the default, 'uniform', a vector of probabilities, or a structure with ClassNames and ClassProbs fields. A class whose prior is 0, or whose misclassification costs are all 0, is removed from the data before fitting, with a warning, as in MATLAB. 'ScoreTransform' A transformation applied to the scores, named or given as a function handle. 'Weights' One nonnegative weight per observation, as a single or double vector. Every computation runs in double. 'PredictorNames' One name per predictor. 'ResponseName' A name for the response. 'CategoricalPredictors' The categorical predictors, as indices, as a logical vector with one element per predictor, or as 'all'. Each is dummy coded in its place, one column of zeros and ones per distinct value it takes in the training data, named as in 'x1 == 2'. A row holding a value the training data did not is scored NaN and labelled with the class of largest prior, as a row missing a predictor is. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. The default solver is 'sparsa' under a lasso penalty. Under a ridge penalty it is 'bfgs' when there are no more than 100 predictors, and beyond that 'dual' for a support vector machine and 'sgd' for a logistic regression. See also: fitclinear, ClassificationKernel # name: # type: sq_string # elements: 1 # length: 31 Fit a linear binary classifier. # name: # type: sq_string # elements: 1 # length: 25 ClassificationLinear.Cost # name: # type: sq_string # elements: 1 # length: 631 ClassificationLinear: property Cost Cost of misclassifying an observation A square numeric matrix with one row and one column per class, whose (i,j) element is the cost of classifying an observation of class i into class j. It defaults to one everywhere except the diagonal, which is zero. This property is read-only: MATLAB refuses an assignment into it on this class, as it does on the support vector machine, so a cost matrix is given to the constructor instead. The cost matrix takes no part in the fit and none in predict, which returns the class of largest score. It is read by the 'mincost' and 'classifcost' losses alone. # name: # type: sq_string # elements: 1 # length: 37 Cost of misclassifying an observation # name: # type: sq_string # elements: 1 # length: 43 ClassificationLinear.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 256 ClassificationLinear: property ExpandedPredictorNames Names of the predictors as the fit saw them A cell array of character vectors. It equals PredictorNames unless categorical predictors were expanded into indicator variables. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Names of the predictors as the fit saw them # name: # type: sq_string # elements: 1 # length: 31 ClassificationLinear.FittedLoss # name: # type: sq_string # elements: 1 # length: 272 ClassificationLinear: property FittedLoss Loss function the fit minimized 'hinge' for a support vector machine and 'logit' for a logistic regression. This is the loss of the objective, which is not the loss loss reports unless it is asked for. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Loss function the fit minimized # name: # type: sq_string # elements: 1 # length: 27 ClassificationLinear.Lambda # name: # type: sq_string # elements: 1 # length: 238 ClassificationLinear: property Lambda Regularization strength A nonnegative scalar, or a 1xL row of them in ascending order. It defaults to the reciprocal of the number of observations used to train the model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Regularization strength # name: # type: sq_string # elements: 1 # length: 28 ClassificationLinear.Learner # name: # type: sq_string # elements: 1 # length: 139 ClassificationLinear: property Learner Linear classification model that was fitted Either 'svm' or 'logistic'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Linear classification model that was fitted # name: # type: sq_string # elements: 1 # length: 36 ClassificationLinear.ModelParameters # name: # type: sq_string # elements: 1 # length: 267 ClassificationLinear: property ModelParameters Fitting options, as they were given A structure holding every parameter of the fit, including the ones that a different solver would have used and the 'auto' values before they were resolved. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Fitting options, as they were given # name: # type: sq_string # elements: 1 # length: 35 ClassificationLinear.PredictorNames # name: # type: sq_string # elements: 1 # length: 215 ClassificationLinear: property PredictorNames Names of the predictors A cell array of character vectors with one name per column of the training data, defaulting to 'x1', 'x2' and so on. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 26 ClassificationLinear.Prior # name: # type: sq_string # elements: 1 # length: 252 ClassificationLinear: property Prior Prior probability of each class A numeric row vector with one element per class, in the order of ClassNames and summing to one. It defaults to the class frequencies of the training data. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Prior probability of each class # name: # type: sq_string # elements: 1 # length: 35 ClassificationLinear.Regularization # name: # type: sq_string # elements: 1 # length: 132 ClassificationLinear: property Regularization Penalty on the coefficients 'ridge (L2)' or 'lasso (L1)'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Penalty on the coefficients # name: # type: sq_string # elements: 1 # length: 33 ClassificationLinear.ResponseName # name: # type: sq_string # elements: 1 # length: 132 ClassificationLinear: property ResponseName Name of the response A character vector, defaulting to 'Y'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Name of the response # name: # type: sq_string # elements: 1 # length: 35 ClassificationLinear.ScoreTransform # name: # type: sq_string # elements: 1 # length: 367 ClassificationLinear: property ScoreTransform Transformation applied to the predicted scores A character vector naming a transformation, or the text of the function handle that was supplied. Assigning to it accepts either. It defaults to 'logit' for a logistic learner, which turns the scores into posterior probabilities, and to 'none' for a support vector machine. # name: # type: sq_string # elements: 1 # length: 46 Transformation applied to the predicted scores # name: # type: sq_string # elements: 1 # length: 25 ClassificationLinear.edge # name: # type: sq_string # elements: 1 # length: 736 ClassificationLinear: e = edge (obj, X, Y) ClassificationLinear: e = edge (..., 'Weights', W) ClassificationLinear: e = edge (obj, Tbl, ResponseVarName) ClassificationLinear: e = edge (obj, Tbl) Weighted mean of the classification margins. e = edge (obj, X, Y) returns one value per regularization strength. The weights are normalized within each class to that class's prior before they are applied. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. # name: # type: sq_string # elements: 1 # length: 44 Weighted mean of the classification margins. # name: # type: sq_string # elements: 1 # length: 25 ClassificationLinear.loss # name: # type: sq_string # elements: 1 # length: 835 ClassificationLinear: l = loss (obj, X, Y) ClassificationLinear: l = loss (obj, Tbl, ResponseVarName) ClassificationLinear: l = loss (obj, Tbl) ClassificationLinear: l = loss (..., name, value) Classification loss on new data. l = loss (obj, X, Y) returns the misclassification rate, one value per regularization strength. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. l = loss (..., name, value) takes 'LossFun', one of 'binodeviance', 'classifcost', 'classiferror', 'exponential', 'hinge', 'logit', 'mincost' and 'quadratic', and 'Weights'. # name: # type: sq_string # elements: 1 # length: 32 Classification loss on new data. # name: # type: sq_string # elements: 1 # length: 27 ClassificationLinear.margin # name: # type: sq_string # elements: 1 # length: 741 ClassificationLinear: m = margin (obj, X, Y) ClassificationLinear: m = margin (obj, Tbl, ResponseVarName) ClassificationLinear: m = margin (obj, Tbl) Classification margin of each observation. m = margin (obj, X, Y) returns the score of the true class less the score of the other one, one row per observation and one column per regularization strength. A positive margin is a correct classification. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. # name: # type: sq_string # elements: 1 # length: 42 Classification margin of each observation. # name: # type: sq_string # elements: 1 # length: 28 ClassificationLinear.predict # name: # type: sq_string # elements: 1 # length: 875 ClassificationLinear: labels = predict (obj, XC) ClassificationLinear: [labels, scores] = predict (obj, XC) Classify new observations. labels = predict (obj, XC) returns the class of largest score for each row of XC, in the type of the response the model was fitted to. With L regularization strengths labels has one column per strength. [labels, scores] = predict (obj, XC) also returns the scores, an Nx2 matrix whose columns follow ClassNames, or an Nx2xL array with more than one strength. The scores are -f and +f for the raw model value f, after ScoreTransform has been applied. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. # name: # type: sq_string # elements: 1 # length: 26 Classify new observations. # name: # type: sq_string # elements: 1 # length: 30 ClassificationLinear.savemodel # name: # type: sq_string # elements: 1 # length: 179 ClassificationLinear: savemodel (obj, filename) Save a linear classifier to a file. savemodel (obj, filename) saves the model obj into filename in a form loadmodel can read back. # name: # type: sq_string # elements: 1 # length: 35 Save a linear classifier to a file. # name: # type: sq_string # elements: 1 # length: 33 ClassificationLinear.selectModels # name: # type: sq_string # elements: 1 # length: 252 ClassificationLinear: sub = selectModels (obj, idx) Keep a subset of the fitted regularization strengths. sub = selectModels (obj, idx) returns a model holding only the strengths idx names, which may be indices into Lambda or a logical vector over it. # name: # type: sq_string # elements: 1 # length: 53 Keep a subset of the fitted regularization strengths. # name: # type: string # elements: 1 # length: 24 ClassificationNaiveBayes # name: # type: sq_string # elements: 1 # length: 1210 statistics: ClassificationNaiveBayes Naive Bayes classification The ClassificationNaiveBayes class implements a naive Bayes classifier object, which can predict responses for new data using the predict method. A naive Bayes classifier estimates one univariate density per class and per predictor, and treats the predictors as conditionally independent given the class. The joint likelihood of an observation is therefore the product of its per-predictor densities, and the posterior follows from the class prior by Bayes' rule. The independence assumption is rarely true, but it costs only one density per predictor rather than one joint density over all of them, which is what makes the model usable when the predictors are many and the observations few. Create a ClassificationNaiveBayes object by using the fitcnb function or the class constructor. Each predictor carries its own distribution, named in DistributionNames, and the fitted parameters of class k and predictor j are held in DistributionParameters{k,j}. A 'normal' predictor stores a two element column vector, the class conditional mean and standard deviation; a 'kernel' predictor stores a prob.KernelDistribution object. See also: fitcnb # name: # type: sq_string # elements: 1 # length: 26 Naive Bayes classification # name: # type: sq_string # elements: 1 # length: 33 ClassificationNaiveBayes.BinEdges # name: # type: sq_string # elements: 1 # length: 487 ClassificationNaiveBayes: property BinEdges Bin edges A cell array with one entry per predictor, holding that predictor's bin edges where the learner discretized it before fitting. A naive Bayes model fits a density to each predictor as it stands and bins nothing, so this is always an empty cell. It is kept because the cross-validated model carries it across, and because code that reaches into it with cellfun must find a cell rather than an empty matrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 9 Bin edges # name: # type: sq_string # elements: 1 # length: 42 ClassificationNaiveBayes.CategoricalLevels # name: # type: sq_string # elements: 1 # length: 246 ClassificationNaiveBayes: property CategoricalLevels Levels of the categorical predictors A cell array with one entry per predictor, holding the distinct levels of each categorical predictor and empty for every other. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Levels of the categorical predictors # name: # type: sq_string # elements: 1 # length: 46 ClassificationNaiveBayes.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 210 ClassificationNaiveBayes: property CategoricalPredictors Categorical predictor indices A numeric row vector of the column indices of X treated as categorical, or empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Categorical predictor indices # name: # type: sq_string # elements: 1 # length: 35 ClassificationNaiveBayes.ClassNames # name: # type: sq_string # elements: 1 # length: 303 ClassificationNaiveBayes: property ClassNames Class labels of the fitted model A cell array of character vectors, a logical or numeric column vector, or a character array, holding the distinct classes the model was fitted on, in the order the other per-class properties use. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Class labels of the fitted model # name: # type: sq_string # elements: 1 # length: 49 ClassificationNaiveBayes.ClassificationNaiveBayes # name: # type: sq_string # elements: 1 # length: 641 ClassificationNaiveBayes: obj = ClassificationNaiveBayes (X, Y) ClassificationNaiveBayes: obj = ClassificationNaiveBayes (Tbl, ResponseVarName) ClassificationNaiveBayes: obj = ClassificationNaiveBayes (Tbl, formula) ClassificationNaiveBayes: obj = ClassificationNaiveBayes (Tbl, Y) ClassificationNaiveBayes: obj = ClassificationNaiveBayes (..., name, value) Create a ClassificationNaiveBayes object. obj = ClassificationNaiveBayes (X, Y) fits a naive Bayes classifier to the predictor data X and the class labels Y. The supported Name/Value pairs are those of fitcnb, which is the documented way to reach this constructor. See also: fitcnb # name: # type: sq_string # elements: 1 # length: 41 Create a ClassificationNaiveBayes object. # name: # type: sq_string # elements: 1 # length: 29 ClassificationNaiveBayes.Cost # name: # type: sq_string # elements: 1 # length: 392 ClassificationNaiveBayes: property Cost Misclassification cost A square numeric matrix with one row and column per class, in the order of ClassNames. Cost(i,j) is the cost of classifying an observation of class i into class j, and the default is one off the diagonal and zero on it. It may be assigned after fitting, as a matrix or as a structure carrying ClassNames and ClassificationCosts. # name: # type: sq_string # elements: 1 # length: 22 Misclassification cost # name: # type: sq_string # elements: 1 # length: 42 ClassificationNaiveBayes.DistributionNames # name: # type: sq_string # elements: 1 # length: 229 ClassificationNaiveBayes: property DistributionNames Predictor distributions A cell array of character vectors with one entry per predictor, naming the distribution fitted to it: 'normal' or 'kernel'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Predictor distributions # name: # type: sq_string # elements: 1 # length: 47 ClassificationNaiveBayes.DistributionParameters # name: # type: sq_string # elements: 1 # length: 419 ClassificationNaiveBayes: property DistributionParameters Fitted distribution parameters A cell array with one row per class and one column per predictor. DistributionParameters{k,j} holds the parameters fitted to predictor j within class k: a two element column vector, the mean and the standard deviation, for a 'normal' predictor, and a prob.KernelDistribution object for a 'kernel' one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 Fitted distribution parameters # name: # type: sq_string # elements: 1 # length: 47 ClassificationNaiveBayes.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 300 ClassificationNaiveBayes: property ExpandedPredictorNames Expanded predictor variable names A cell array of character vectors naming the predictors as the model sees them. It equals PredictorNames unless a categorical predictor has been expanded into indicator variables. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Expanded predictor variable names # name: # type: sq_string # elements: 1 # length: 58 ClassificationNaiveBayes.HyperparameterOptimizationResults # name: # type: sq_string # elements: 1 # length: 366 ClassificationNaiveBayes: property HyperparameterOptimizationResults Results of the hyperparameter optimization Always empty. It is declared for MATLAB compatibility, where it holds what an automatic search over the hyperparameters found. This class fits the parameters it is given and runs no such search, so there is nothing to report. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Results of the hyperparameter optimization # name: # type: sq_string # elements: 1 # length: 31 ClassificationNaiveBayes.Kernel # name: # type: sq_string # elements: 1 # length: 224 ClassificationNaiveBayes: property Kernel Kernel smoothing functions A cell array with one entry per predictor naming the smoothing kernel used by a 'kernel' predictor, and empty for every other. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Kernel smoothing functions # name: # type: sq_string # elements: 1 # length: 40 ClassificationNaiveBayes.ModelParameters # name: # type: sq_string # elements: 1 # length: 575 ClassificationNaiveBayes: property ModelParameters What was fitted, and how A structure carrying DistributionNames, Kernel, Support, Width, StandardizeData, Version, Method and Type. It records the arguments as they were given, where the properties of the same name record what they were resolved to: a model fitted with no 'DistributionNames' argument reports the single name 'normal' here and one name per predictor there. The kernel settings are filled in with their defaults when a kernel density was asked for, and left empty when none was. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 What was fitted, and how # name: # type: sq_string # elements: 1 # length: 27 ClassificationNaiveBayes.Mu # name: # type: sq_string # elements: 1 # length: 263 ClassificationNaiveBayes: property Mu Predictor means The means used to center the predictors, when the model standardizes them, and empty otherwise. These are not the class conditional means, which are held in DistributionParameters. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Predictor means # name: # type: sq_string # elements: 1 # length: 40 ClassificationNaiveBayes.NumObservations # name: # type: sq_string # elements: 1 # length: 333 ClassificationNaiveBayes: property NumObservations Number of observations A positive integer specifying the number of observations used to train the model, the rows dropped as RowsUsed describes left out. Each predictor is fitted on the rows it is known for, and an empirical prior counts every row used. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 39 ClassificationNaiveBayes.PredictorNames # name: # type: sq_string # elements: 1 # length: 245 ClassificationNaiveBayes: property PredictorNames Predictor variable names A cell array of character vectors naming the predictors, in the order in which they appear in X. The default names are 'x1', 'x2', and so on. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Predictor variable names # name: # type: sq_string # elements: 1 # length: 30 ClassificationNaiveBayes.Prior # name: # type: sq_string # elements: 1 # length: 323 ClassificationNaiveBayes: property Prior Class prior probabilities A numeric row vector with one entry per class, in the order of ClassNames, summing to one. It may be assigned after fitting, as a numeric vector, as a structure carrying ClassNames and ClassProbs, or as 'empirical' or 'uniform'. Assigning it re-derives W. # name: # type: sq_string # elements: 1 # length: 25 Class prior probabilities # name: # type: sq_string # elements: 1 # length: 37 ClassificationNaiveBayes.ResponseName # name: # type: sq_string # elements: 1 # length: 164 ClassificationNaiveBayes: property ResponseName Response variable name A character vector naming the response variable, 'Y' by default. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Response variable name # name: # type: sq_string # elements: 1 # length: 33 ClassificationNaiveBayes.RowsUsed # name: # type: sq_string # elements: 1 # length: 567 ClassificationNaiveBayes: property RowsUsed Rows used for fitting A logical column vector with the same length as the observations in the original predictor data X, true for each row that was used for fitting the model. It is empty, [], when every observation was used. A row is dropped when its class is missing, when every predictor is missing, or when its weight is zero or missing. A row missing only some predictors is used; with the 'mn' distribution it counts in the prior but not in the multinomial, which describes the whole row. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Rows used for fitting # name: # type: sq_string # elements: 1 # length: 39 ClassificationNaiveBayes.ScoreTransform # name: # type: sq_string # elements: 1 # length: 278 ClassificationNaiveBayes: property ScoreTransform Score transformation A character vector naming the function applied to the posterior returned by predict, or a function handle taking and returning a matrix of the same size. The default, 'none', leaves the posterior untouched. # name: # type: sq_string # elements: 1 # length: 20 Score transformation # name: # type: sq_string # elements: 1 # length: 30 ClassificationNaiveBayes.Sigma # name: # type: sq_string # elements: 1 # length: 307 ClassificationNaiveBayes: property Sigma Predictor standard deviations The standard deviations used to scale the predictors, when the model standardizes them, and empty otherwise. These are not the class conditional standard deviations, which are held in DistributionParameters. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Predictor standard deviations # name: # type: sq_string # elements: 1 # length: 32 ClassificationNaiveBayes.Support # name: # type: sq_string # elements: 1 # length: 220 ClassificationNaiveBayes: property Support Kernel smoothing supports A cell array with one entry per predictor giving the support of a 'kernel' predictor's density, and empty for every other. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Kernel smoothing supports # name: # type: sq_string # elements: 1 # length: 26 ClassificationNaiveBayes.W # name: # type: sq_string # elements: 1 # length: 376 ClassificationNaiveBayes: property W Observation weights A numeric column vector with one entry per observation used for fitting, summing to one. Each class contributes its prior, spread over the observations belonging to it in proportion to the 'Weights' given, or evenly when none were. It has the class of the 'Weights' given, single or double. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 30 ClassificationNaiveBayes.Width # name: # type: sq_string # elements: 1 # length: 264 ClassificationNaiveBayes: property Width Kernel smoothing bandwidths A numeric matrix with one row per class and one column per predictor, giving the bandwidth of each 'kernel' predictor's density, and empty when no predictor uses one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Kernel smoothing bandwidths # name: # type: sq_string # elements: 1 # length: 26 ClassificationNaiveBayes.X # name: # type: sq_string # elements: 1 # length: 503 ClassificationNaiveBayes: property X Predictor data A numeric matrix containing the predictor data the model was fitted on. Each column of X represents one predictor (variable), and each row represents one observation; a row dropped from the fit, as RowsUsed describes, is not kept. This property is read-only. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 26 ClassificationNaiveBayes.Y # name: # type: sq_string # elements: 1 # length: 297 ClassificationNaiveBayes: property Y Class labels Specified as a logical or numeric column vector, or as a character array or a cell array of character vectors with the same number of rows as X. Each row in Y is the observed class label for the corresponding row in X. This property is read-only. # name: # type: sq_string # elements: 1 # length: 12 Class labels # name: # type: sq_string # elements: 1 # length: 32 ClassificationNaiveBayes.compact # name: # type: sq_string # elements: 1 # length: 389 ClassificationNaiveBayes: CMdl = compact (obj) Drop the training data from a trained model. CMdl = compact (obj) returns a CompactClassificationNaiveBayes object carrying the fitted densities and everything predict needs, but not the observations the model was fitted on. It classifies new data identically and is far smaller to keep or to ship. See also: CompactClassificationNaiveBayes # name: # type: sq_string # elements: 1 # length: 44 Drop the training data from a trained model. # name: # type: sq_string # elements: 1 # length: 33 ClassificationNaiveBayes.crossval # name: # type: sq_string # elements: 1 # length: 556 ClassificationNaiveBayes: CVMdl = crossval (obj) ClassificationNaiveBayes: CVMdl = crossval (..., name, value) Cross-validate a trained naive Bayes model. CVMdl = crossval (obj) partitions the training data into ten folds, or into as many folds as there are observations when there are fewer than ten, refits the model on each fold's training part and returns a ClassificationPartitionedModel. CVMdl = crossval (..., name, value) takes exactly one of 'KFold', 'Holdout', 'Leaveout' or 'CVPartition'. See also: ClassificationPartitionedModel, cvpartition # name: # type: sq_string # elements: 1 # length: 43 Cross-validate a trained naive Bayes model. # name: # type: sq_string # elements: 1 # length: 29 ClassificationNaiveBayes.edge # name: # type: sq_string # elements: 1 # length: 811 ClassificationNaiveBayes: e = edge (obj, X, Y) ClassificationNaiveBayes: e = edge (..., 'Weights', w) ClassificationNaiveBayes: e = edge (obj, Tbl, ResponseVarName) ClassificationNaiveBayes: e = edge (obj, Tbl) Classification edge on new data. e = edge (obj, X, Y) returns the weighted mean of the margins, a single number summarising how confidently the model classifies the data. The weights are normalized within each class to that class's prior before they are applied. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. # name: # type: sq_string # elements: 1 # length: 32 Classification edge on new data. # name: # type: sq_string # elements: 1 # length: 29 ClassificationNaiveBayes.logp # name: # type: sq_string # elements: 1 # length: 674 ClassificationNaiveBayes: lp = logp (obj, X) Log unconditional probability density of new data. lp = logp (obj, X) returns one value per observation, the logarithm of its density under the fitted model taken over all the classes, each weighted by its prior. A markedly low value marks an observation the model finds unlike anything it was trained on, whatever class it would be assigned to. X may also be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. # name: # type: sq_string # elements: 1 # length: 50 Log unconditional probability density of new data. # name: # type: sq_string # elements: 1 # length: 29 ClassificationNaiveBayes.loss # name: # type: sq_string # elements: 1 # length: 926 ClassificationNaiveBayes: l = loss (obj, X, Y) ClassificationNaiveBayes: l = loss (obj, Tbl, ResponseVarName) ClassificationNaiveBayes: l = loss (obj, Tbl) ClassificationNaiveBayes: l = loss (..., name, value) Classification loss on new data. l = loss (obj, X, Y) returns the minimum expected misclassification cost. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. l = loss (..., name, value) takes the following options. Name Value 'LossFun' One of 'binodeviance', 'classifcost', 'classiferror', 'exponential', 'hinge', 'logit', 'mincost' (default) or 'quadratic'. 'Weights' A numeric vector of observation weights, one per row of X. # name: # type: sq_string # elements: 1 # length: 32 Classification loss on new data. # name: # type: sq_string # elements: 1 # length: 31 ClassificationNaiveBayes.margin # name: # type: sq_string # elements: 1 # length: 833 ClassificationNaiveBayes: m = margin (obj, X, Y) ClassificationNaiveBayes: m = margin (obj, Tbl, ResponseVarName) ClassificationNaiveBayes: m = margin (obj, Tbl) Classification margin on new data. m = margin (obj, X, Y) returns one margin per observation: the posterior the model gives the observation's true class, less the largest posterior it gives any other class. A positive margin means the observation is classified correctly, and a larger one means it is classified more confidently. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. # name: # type: sq_string # elements: 1 # length: 34 Classification margin on new data. # name: # type: sq_string # elements: 1 # length: 32 ClassificationNaiveBayes.predict # name: # type: sq_string # elements: 1 # length: 1153 ClassificationNaiveBayes: label = predict (obj, XC) ClassificationNaiveBayes: [label, score, cost] = predict (obj, XC) Classify new data with a trained ClassificationNaiveBayes object. label = predict (obj, XC) returns the predicted class label for each row of XC, which must have as many columns as the predictor data the model was fitted on. [label, score, cost] = predict (obj, XC) also returns score, the posterior probability of each class, and cost, the expected misclassification cost of assigning each observation to each class. The label of an observation is the class of least expected cost. A missing predictor is skipped and the observation classified on the rest. Under the 'mn' distribution, which describes the whole row at once, an observation missing any count has posterior and cost NaN and takes the class of largest prior, whatever the cost. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. # name: # type: sq_string # elements: 1 # length: 65 Classify new data with a trained ClassificationNaiveBayes object. # name: # type: sq_string # elements: 1 # length: 34 ClassificationNaiveBayes.resubEdge # name: # type: sq_string # elements: 1 # length: 87 ClassificationNaiveBayes: e = resubEdge (obj) Classification edge on the training data. # name: # type: sq_string # elements: 1 # length: 41 Classification edge on the training data. # name: # type: sq_string # elements: 1 # length: 34 ClassificationNaiveBayes.resubLoss # name: # type: sq_string # elements: 1 # length: 179 ClassificationNaiveBayes: l = resubLoss (obj) ClassificationNaiveBayes: l = resubLoss (..., name, value) Classification loss on the training data. Takes the same options as loss. # name: # type: sq_string # elements: 1 # length: 41 Classification loss on the training data. # name: # type: sq_string # elements: 1 # length: 36 ClassificationNaiveBayes.resubMargin # name: # type: sq_string # elements: 1 # length: 91 ClassificationNaiveBayes: m = resubMargin (obj) Classification margin on the training data. # name: # type: sq_string # elements: 1 # length: 43 Classification margin on the training data. # name: # type: sq_string # elements: 1 # length: 37 ClassificationNaiveBayes.resubPredict # name: # type: sq_string # elements: 1 # length: 303 ClassificationNaiveBayes: label = resubPredict (obj) ClassificationNaiveBayes: [label, score, cost] = resubPredict (obj) Classify the training data with the trained model. The same as calling predict on the data the model was fitted on, with the rows that were dropped, as RowsUsed describes, left out. # name: # type: sq_string # elements: 1 # length: 50 Classify the training data with the trained model. # name: # type: sq_string # elements: 1 # length: 34 ClassificationNaiveBayes.savemodel # name: # type: sq_string # elements: 1 # length: 485 ClassificationNaiveBayes: savemodel (obj, filename) Save a ClassificationNaiveBayes object. savemodel (obj, filename) saves each property of a ClassificationNaiveBayes object into an Octave binary file, the name of which is specified in filename, along with an extra variable, which defines the type classification object these variables constitute. Use loadmodel in order to load a classification object into Octave's workspace. See also: loadmodel, fitcnb, ClassificationNaiveBayes # name: # type: sq_string # elements: 1 # length: 39 Save a ClassificationNaiveBayes object. # name: # type: sq_string # elements: 1 # length: 27 ClassificationNeuralNetwork # name: # type: sq_string # elements: 1 # length: 590 statistics: ClassificationNeuralNetwork Neural network classification The ClassificationNeuralNetwork class implements a neural network classifier object, which can predict responses for new data using the predict method. Neural network classification is a machine learning method that uses interconnected nodes in multiple layers to learn complex patterns in data. It processes inputs through hidden layers with activation functions to produce classification outputs. Create a ClassificationNeuralNetwork object by using the fitcnet function or the class constructor. See also: fitcnet # name: # type: sq_string # elements: 1 # length: 29 Neural network classification # name: # type: sq_string # elements: 1 # length: 39 ClassificationNeuralNetwork.Activations # name: # type: sq_string # elements: 1 # length: 381 ClassificationNeuralNetwork: property Activations Activation functions for hidden layers A character vector or cell array of character vectors specifying the activation functions used in the hidden layers of the neural network. Supported activation functions include: 'linear', 'sigmoid', 'relu', 'tanh', 'softmax', 'lrelu', 'prelu', 'elu', and 'gelu'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Activation functions for hidden layers # name: # type: sq_string # elements: 1 # length: 36 ClassificationNeuralNetwork.BinEdges # name: # type: sq_string # elements: 1 # length: 362 ClassificationNeuralNetwork: property BinEdges Bin edges of the predictors A cell array with one entry per predictor, holding that predictor's bin edges where the learner discretized it before fitting. It is empty here and stays empty: this learner fits the predictors as they are, and MATLAB's reports an empty cell for it as well. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Bin edges of the predictors # name: # type: sq_string # elements: 1 # length: 49 ClassificationNeuralNetwork.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 238 ClassificationNeuralNetwork: property CategoricalPredictors Indices of the categorical predictors A numeric vector of column indices into X naming the predictors treated as categorical, and empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 38 ClassificationNeuralNetwork.ClassNames # name: # type: sq_string # elements: 1 # length: 409 ClassificationNeuralNetwork: property ClassNames Names of classes in the response variable An array of unique values of the response variable Y, which has the same data types as the data in Y, sorted or in the order given by the 'ClassNames' option. This property is read-only. ClassNames can have any of the following datatypes: Cell array of character vectors Character array Logical vector Numeric vector # name: # type: sq_string # elements: 1 # length: 41 Names of classes in the response variable # name: # type: sq_string # elements: 1 # length: 55 ClassificationNeuralNetwork.ClassificationNeuralNetwork # name: # type: sq_string # elements: 1 # length: 5528 ClassificationNeuralNetwork: obj = ClassificationNeuralNetwork (X, Y) ClassificationNeuralNetwork: obj = ClassificationNeuralNetwork (Tbl, ResponseVarName) ClassificationNeuralNetwork: obj = ClassificationNeuralNetwork (Tbl, formula) ClassificationNeuralNetwork: obj = ClassificationNeuralNetwork (Tbl, Y) ClassificationNeuralNetwork: obj = ClassificationNeuralNetwork (..., name, value) Create a ClassificationNeuralNetwork class object containing a neural network classification model. obj = ClassificationNeuralNetwork (X, Y) returns a ClassificationNeuralNetwork object, with X as the predictor data and Y containing the class labels of observations in X. X must be a NxP numeric matrix of input data where rows correspond to observations and columns correspond to features or variables. X will be used to train the neural network model. Y is Nx1 matrix or cell matrix containing the class labels of corresponding predictor data in X. Y can contain any type of categorical data. Y must have the same number of rows as X. obj = ClassificationNeuralNetwork (..., name, value) returns a ClassificationNeuralNetwork object with parameters specified by the following name, value paired input arguments: Name Value 'CategoricalPredictors' The predictors whose values are levels, as indices, as a logical vector with one element per predictor, or as 'all'. Each is dummy coded in its place, one column of zeros and ones per level seen in training, named as in 'x1 == 2' in ExpandedPredictorNames, and the coded columns are not standardized. An observation holding a level the training data did not has no score and takes the class of largest prior, as a row missing a predictor does. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. 'PredictorNames' A cell array of character vectors specifying the names of the predictors. The length of this array must match the number of columns in X. 'ResponseName' A character vector specifying the name of the response variable. 'ClassNames' Names of the classes in the class labels, Y, used for fitting the neural network model. ClassNames are of the same type as the class labels in Y. 'ScoreTransform' A user-defined function handle or a character vector specifying one of the following builtin functions specifying the transformation applied to predicted classification scores. Supported values include 'doublelogit', 'invlogit', 'ismax', 'logit', 'none', 'identity', 'sign', 'symmetric', 'symmetricismax', and 'symmetriclogit'. 'Standardize' A logical scalar specifying whether to standardize the predictor data. When true, the predictors are centered and scaled to have zero mean and unit variance. 'LayerSizes' A positive integer vector specifying the sizes of the fully connected layers in the neural network. The default is 10. 'Activations' A character vector or cell array of character vectors specifying the activation functions for the hidden layers. Supported values include 'linear', 'sigmoid', 'relu', 'tanh', 'softmax', 'lrelu', 'prelu', 'elu', and 'gelu'. The default is 'relu', whose gradient is one wherever a unit is active and so does not shrink as it passes back through the layers, where a sigmoid multiplies it by at most a quarter at every one. 'OutputLayerActivation' A character vector specifying the activation function for the output layer. Supported values are the same as for 'Activations'. The default is 'softmax', which makes the scores a probability over the classes and trains the network against cross entropy; any other value trains it against the mean squared error. 'LearningRate' A positive scalar specifying the learning rate for gradient descent. The default is 0.003. A larger rate can drive every unit of a hidden layer negative, after which a rectifier passes no gradient and the network stops training. Applies only when 'Solver' is 'sgd'. 'Solver' A character vector naming the solver that trains the network, either 'lbfgs' or 'sgd'. The default is 'lbfgs', which minimizes the loss over the whole training set at once by limited-memory BFGS, as MATLAB does. It takes no learning rate, stops on the three tolerances below, and reaches a lower training loss in fewer passes over the data, though each of its iterations costs several passes where an epoch costs one. 'sgd' visits the samples one at a time and steps down the gradient of each, running for 'IterationLimit' epochs; it was the default before version 1.9.0. 'GradientTolerance' A nonnegative scalar. Training stops once the gradient's infinity norm falls to or below it, which is the quantity MATLAB tests too. The default is 1e-6. Applies only when 'Solver' is 'lbfgs'. 'StepTolerance' A nonnegative scalar. Training stops once the step's infinity norm falls to or below it, which is the quantity MATLAB tests too. The default is 1e-6. Applies only when 'Solver' is 'lbfgs'. 'LossTolerance' A real scalar. Training stops once the training loss falls to or below it. The test is on the loss itself and not on its change, matching MATLAB; pass -Inf to switch it off. The default is 1e-6. Applies only when 'Solver' is 'lbfgs'. 'IterationLimit' A positive integer specifying the maximum number of training iterations. The default is 1000. Under 'sgd' this counts epochs, under 'lbfgs' solver iterations. 'DisplayInfo' A logical scalar specifying whether to display training information. The default is false. See also: fitcnet # name: # type: sq_string # elements: 1 # length: 99 Create a ClassificationNeuralNetwork class object containing a neural network classification model. # name: # type: sq_string # elements: 1 # length: 43 ClassificationNeuralNetwork.ConvergenceInfo # name: # type: sq_string # elements: 1 # length: 745 ClassificationNeuralNetwork: property ConvergenceInfo Training convergence information A structure containing convergence information of the neural network classifier model with the following fields: Accuracy - The prediction accuracy at each iteration during training TrainingLoss - The loss value recorded at each iteration during training Time - The cumulative time taken for all iterations in seconds This property is read-only. Under 'lbfgs' the structure carries Gradient and Step, the two quantities the solver measured to decide it had converged, and ConvergenceCriterion, naming the test that stopped it. It carries no Accuracy: MATLAB reports none, and measuring it would cost a pass over the whole training set at every iteration. # name: # type: sq_string # elements: 1 # length: 32 Training convergence information # name: # type: sq_string # elements: 1 # length: 32 ClassificationNeuralNetwork.Cost # name: # type: sq_string # elements: 1 # length: 779 ClassificationNeuralNetwork: property Cost Cost of misclassification A numeric matrix with one row and one column per class, where Cost(i,j) is the cost of classifying an observation of class i as class j. The default has zeros on the diagonal and ones elsewhere. Change it on a trained model with dot notation, as in obj.Cost = cost. A cost may also be given as a struct with the fields ClassNames and ClassificationCosts, which names the order its own matrix is written in. That matrix is permuted into the order of ClassNames above, so a caller need not know which order the classes are in. It must name every class. A cost must be floating point, not sparse, not complex, non-negative and zero down its diagonal, and must hold no NaN or Inf. A single is widened to double. # name: # type: sq_string # elements: 1 # length: 25 Cost of misclassification # name: # type: sq_string # elements: 1 # length: 39 ClassificationNeuralNetwork.DisplayInfo # name: # type: sq_string # elements: 1 # length: 184 ClassificationNeuralNetwork: property DisplayInfo Display training information flag A boolean flag indicating whether to print information during training. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Display training information flag # name: # type: sq_string # elements: 1 # length: 50 ClassificationNeuralNetwork.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 271 ClassificationNeuralNetwork: property ExpandedPredictorNames Names of the predictors as the model expanded them A cell array of character vectors. It matches PredictorNames unless a categorical predictor was expanded into indicator variables. This property is read-only. # name: # type: sq_string # elements: 1 # length: 50 Names of the predictors as the model expanded them # name: # type: sq_string # elements: 1 # length: 61 ClassificationNeuralNetwork.HyperparameterOptimizationResults # name: # type: sq_string # elements: 1 # length: 369 ClassificationNeuralNetwork: property HyperparameterOptimizationResults Results of the hyperparameter optimization Always empty. It is declared for MATLAB compatibility, where it holds what an automatic search over the hyperparameters found. This class fits the parameters it is given and runs no such search, so there is nothing to report. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Results of the hyperparameter optimization # name: # type: sq_string # elements: 1 # length: 42 ClassificationNeuralNetwork.IterationLimit # name: # type: sq_string # elements: 1 # length: 206 ClassificationNeuralNetwork: property IterationLimit Maximum number of training iterations A positive integer value defining the maximum number of epochs for training the model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Maximum number of training iterations # name: # type: sq_string # elements: 1 # length: 39 ClassificationNeuralNetwork.LayerBiases # name: # type: sq_string # elements: 1 # length: 239 ClassificationNeuralNetwork: property LayerBiases Learned bias of each fully connected layer A cell array holding one column vector per layer, the output layer included, with one entry per neuron of that layer. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Learned bias of each fully connected layer # name: # type: sq_string # elements: 1 # length: 38 ClassificationNeuralNetwork.LayerSizes # name: # type: sq_string # elements: 1 # length: 479 ClassificationNeuralNetwork: property LayerSizes Sizes of fully connected layers A positive integer vector specifying the sizes of the fully connected layers in the neural network model. The i-th element of LayerSizes is the number of outputs in the i-th fully connected layer of the neural network model. LayerSizes does not include the size of the final fully connected layer. This layer always has K outputs, where K is the number of classes in Y. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Sizes of fully connected layers # name: # type: sq_string # elements: 1 # length: 40 ClassificationNeuralNetwork.LayerWeights # name: # type: sq_string # elements: 1 # length: 290 ClassificationNeuralNetwork: property LayerWeights Learned weights of each fully connected layer A cell array holding one weight matrix per layer, the output layer included. LayerWeights{i} has one row per neuron of layer i and one column per input it receives. This property is read-only. # name: # type: sq_string # elements: 1 # length: 45 Learned weights of each fully connected layer # name: # type: sq_string # elements: 1 # length: 40 ClassificationNeuralNetwork.LearningRate # name: # type: sq_string # elements: 1 # length: 221 ClassificationNeuralNetwork: property LearningRate Learning rate for gradient descent A positive scalar value defining the learning rate used by the gradient descent algorithm during training. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Learning rate for gradient descent # name: # type: sq_string # elements: 1 # length: 43 ClassificationNeuralNetwork.ModelParameters # name: # type: sq_string # elements: 1 # length: 1145 ClassificationNeuralNetwork: property ModelParameters Neural network model parameters A structure holding the fit as it was asked for: LayerSizes, Activations, OutputLayerActivation, LayerWeightsInitializers, Solver, LearningRate, IterationLimit, GradientTolerance, LossTolerance, StepTolerance, DisplayInfo, StandardizeData, and the Version, Method and Type tags. What came out of the fit is elsewhere: the LayerWeights and LayerBiases properties hold the network, TrainingHistory the series and ConvergenceInfo where it stopped. LayerWeightsInitializers names the scheme each layer's weights were drawn with, the output layer last: 'he' for a rectifying activation and 'glorot' for a symmetric one. It is a report, not a setting, the engine choosing per layer from the activation and offering no way to override it. OutputLayerActivation, Solver and LearningRate are this package's own; MATLAB has no counterpart for them. The fields it reports that this class does not accept as arguments (Lambda, the validation set and its patience and frequency, InitialStepSize and the two initializer settings) are absent. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Neural network model parameters # name: # type: sq_string # elements: 1 # length: 30 ClassificationNeuralNetwork.Mu # name: # type: sq_string # elements: 1 # length: 409 ClassificationNeuralNetwork: property Mu Predictor means A numeric vector containing the means of the predictors used for standardization. Empty when the predictor data were not standardized. This property is read-only. Only observations with no missing predictor enter the estimate, and they are weighted so that each class keeps the share of the observation weight it carried before any row was set aside. # name: # type: sq_string # elements: 1 # length: 15 Predictor means # name: # type: sq_string # elements: 1 # length: 43 ClassificationNeuralNetwork.NumObservations # name: # type: sq_string # elements: 1 # length: 249 ClassificationNeuralNetwork: property NumObservations Number of observations A positive integer value specifying the number of observations in the training dataset used for training the ClassificationNeuralNetwork model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 41 ClassificationNeuralNetwork.NumPredictors # name: # type: sq_string # elements: 1 # length: 243 ClassificationNeuralNetwork: property NumPredictors Number of predictors A positive integer value specifying the number of predictors in the training dataset used for training the ClassificationNeuralNetwork model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of predictors # name: # type: sq_string # elements: 1 # length: 49 ClassificationNeuralNetwork.OutputLayerActivation # name: # type: sq_string # elements: 1 # length: 447 ClassificationNeuralNetwork: property OutputLayerActivation Activation function for output layer A character vector specifying the activation function of the output layer of the neural network. Supported activation functions are the same as for the Activations property. The default, softmax, reports a probability over the classes; the network is then trained against cross entropy rather than the mean squared error. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Activation function for output layer # name: # type: sq_string # elements: 1 # length: 42 ClassificationNeuralNetwork.PredictorNames # name: # type: sq_string # elements: 1 # length: 266 ClassificationNeuralNetwork: property PredictorNames Names of predictor variables A cell array of character vectors specifying the names of the predictor variables. The names are in the order in which they appear in the training dataset. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Names of predictor variables # name: # type: sq_string # elements: 1 # length: 33 ClassificationNeuralNetwork.Prior # name: # type: sq_string # elements: 1 # length: 618 ClassificationNeuralNetwork: property Prior Prior probability of each class A numeric vector with one entry per class, in the order of ClassNames, summing to one. It defaults to the relative frequency of each class in the training data. This property is read-only, as MATLAB documents it; pass 'Prior' to fitcnet to set it. Specified as a row vector with one entry per class, in the order of ClassNames, and rescaled to sum to one. It may be given as 'empirical', 'uniform', a numeric vector, or a structure with ClassNames and ClassProbs fields, which assigns each probability by class name rather than by position. # name: # type: sq_string # elements: 1 # length: 31 Prior probability of each class # name: # type: sq_string # elements: 1 # length: 40 ClassificationNeuralNetwork.ResponseName # name: # type: sq_string # elements: 1 # length: 169 ClassificationNeuralNetwork: property ResponseName Response variable name A character vector specifying the name of the response variable Y. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Response variable name # name: # type: sq_string # elements: 1 # length: 36 ClassificationNeuralNetwork.RowsUsed # name: # type: sq_string # elements: 1 # length: 403 ClassificationNeuralNetwork: property RowsUsed Rows used for fitting A logical column vector with the same length as the observations in the original predictor data X, true for each row that was used for fitting the ClassificationNeuralNetwork model. It is empty, [], when every observation was used, so a non-empty value means that rows holding missing values were dropped. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Rows used for fitting # name: # type: sq_string # elements: 1 # length: 42 ClassificationNeuralNetwork.ScoreTransform # name: # type: sq_string # elements: 1 # length: 992 ClassificationNeuralNetwork: property ScoreTransform Transformation function for classification scores Specified as a function handle for transforming the classification scores. Add or change the ScoreTransform property using dot notation as in: obj.ScoreTransform = 'function_name' obj.ScoreTransform = @function_handle When specified as a character vector, it can be any of the following built-in functions. Nevertheless, the ScoreTransform property always stores their function handle equivalent. Value Description 'doublelogit' 1 ./ (1 + exp (-2 x x)) 'invlogit' log (x ./ (1 - x)) 'ismax' Sets the score for the class with the largest score to 1, and for all other classes to 0 'logit' 1 ./ (1 + exp (-x)) 'none' x (no transformation) 'identity' x (no transformation) 'sign' -1 for x < 0, 0 for x = 0, 1 for x > 0 'symmetric' 2 x x - 1 'symmetricismax' Sets the score for the class with the largest score to 1, and for all other classes to -1 'symmetriclogit' 2 ./ (1 + exp (-x)) - 1 # name: # type: sq_string # elements: 1 # length: 49 Transformation function for classification scores # name: # type: sq_string # elements: 1 # length: 33 ClassificationNeuralNetwork.Sigma # name: # type: sq_string # elements: 1 # length: 440 ClassificationNeuralNetwork: property Sigma Predictor standard deviations A numeric vector containing the standard deviations of the predictors used for standardization. Empty when the predictor data were not standardized. This property is read-only. Only observations with no missing predictor enter the estimate, and they are weighted so that each class keeps the share of the observation weight it carried before any row was set aside. # name: # type: sq_string # elements: 1 # length: 29 Predictor standard deviations # name: # type: sq_string # elements: 1 # length: 34 ClassificationNeuralNetwork.Solver # name: # type: sq_string # elements: 1 # length: 276 ClassificationNeuralNetwork: property Solver Solver used for training A character vector specifying the solver algorithm used to train the neural network model, either 'Gradient Descent' for the stochastic solver or 'LBFGS' for the full-batch one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Solver used for training # name: # type: sq_string # elements: 1 # length: 43 ClassificationNeuralNetwork.TrainingHistory # name: # type: sq_string # elements: 1 # length: 455 ClassificationNeuralNetwork: property TrainingHistory Iteration by iteration record of training A table with one row per iteration, holding the iteration number, the training loss and the training accuracy recorded at it. This property is read-only. The columns follow the solver. Under 'sgd' they are Iteration and TrainingLoss, with TrainingAccuracy for a classifier. Under 'lbfgs' they are Iteration, TrainingLoss, Gradient and Step, as MATLAB's are. # name: # type: sq_string # elements: 1 # length: 41 Iteration by iteration record of training # name: # type: sq_string # elements: 1 # length: 29 ClassificationNeuralNetwork.W # name: # type: sq_string # elements: 1 # length: 517 ClassificationNeuralNetwork: property W Observation weights A numeric column vector with one entry per training observation, summing to one, by which the training loss weighs each observation. It has the class of the 'Weights' given, single or double. This property is read-only. Each class carries its prior spread over its own observations in proportion to the 'Weights' given, or evenly when none were, so that an observation of a class weighs Prior for that class divided by the number of observations it holds. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 29 ClassificationNeuralNetwork.X # name: # type: sq_string # elements: 1 # length: 433 ClassificationNeuralNetwork: property X Predictor data A numeric matrix containing the unstandardized predictor data. Each column of X represents one predictor (variable), and each row represents one observation. This property is read-only. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 29 ClassificationNeuralNetwork.Y # name: # type: sq_string # elements: 1 # length: 317 ClassificationNeuralNetwork: property Y Class labels Specified as a logical or numeric column vector, or as a character array or a cell array of character vectors with the same number of rows as the predictor data. Each row in Y is the observed class label for the corresponding row in X. This property is read-only. # name: # type: sq_string # elements: 1 # length: 12 Class labels # name: # type: sq_string # elements: 1 # length: 35 ClassificationNeuralNetwork.compact # name: # type: sq_string # elements: 1 # length: 283 ClassificationNeuralNetwork: CVMdl = compact (obj) Create a CompactClassificationNeuralNetwork object. CVMdl = compact (obj) creates a compact version of the ClassificationNeuralNetwork object, obj. See also: fitcnet, ClassificationNeuralNetwork, CompactClassificationNeuralNetwork # name: # type: sq_string # elements: 1 # length: 51 Create a CompactClassificationNeuralNetwork object. # name: # type: sq_string # elements: 1 # length: 36 ClassificationNeuralNetwork.crossval # name: # type: sq_string # elements: 1 # length: 1030 ClassificationNeuralNetwork: CVMdl = crossval (obj) ClassificationNeuralNetwork: CVMdl = crossval (..., Name, Value) Cross Validate a Neural Network classification object. CVMdl = crossval (obj) returns a cross-validated model object, CVMdl, from a trained model, obj, using 10-fold cross-validation by default. CVMdl = crossval (obj, name, value) specifies additional name-value pair arguments to customize the cross-validation process. Name Value 'KFold' Specify the number of folds to use in k-fold cross-validation. "KFold", k, where k is an integer greater than 1. 'Holdout' Specify the fraction of the data to hold out for testing. "Holdout", p, where p is a scalar in the range (0,1). 'Leaveout' Specify whether to perform leave-one-out cross-validation. "Leaveout", Value, where Value is 'on' or 'off'. 'CVPartition' Specify a cvpartition object used for cross-validation. "CVPartition", cv, where isa (cv, "cvpartition") = 1. See also: fitcnet, ClassificationNeuralNetwork, cvpartition, ClassificationPartitionedModel # name: # type: sq_string # elements: 1 # length: 54 Cross Validate a Neural Network classification object. # name: # type: sq_string # elements: 1 # length: 32 ClassificationNeuralNetwork.edge # name: # type: sq_string # elements: 1 # length: 1010 ClassificationNeuralNetwork: e = edge (obj, X, Y) ClassificationNeuralNetwork: e = edge (..., "Weights", w) ClassificationNeuralNetwork: e = edge (obj, Tbl, ResponseVarName) ClassificationNeuralNetwork: e = edge (obj, Tbl) Classification edge of a neural network classifier. e = edge (obj, X, Y) returns the mean of the classification margins over the rows of X. e = edge (..., "Weights", w) takes the weighted mean instead, w holding one weight per row of X. The weights are normalised to sum to one before they are applied. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: ClassificationNeuralNetwork, ClassificationNeuralNetwork.margin, ClassificationNeuralNetwork.loss, ClassificationNeuralNetwork.predict # name: # type: sq_string # elements: 1 # length: 51 Classification edge of a neural network classifier. # name: # type: sq_string # elements: 1 # length: 32 ClassificationNeuralNetwork.loss # name: # type: sq_string # elements: 1 # length: 1614 ClassificationNeuralNetwork: L = loss (obj, X, Y) ClassificationNeuralNetwork: L = loss (obj, Tbl, ResponseVarName) ClassificationNeuralNetwork: L = loss (obj, Tbl) ClassificationNeuralNetwork: L = loss (..., name, value) Classification loss of a neural network classifier. L = loss (obj, X, Y) returns the proportion of the rows of X the model misclassifies against the true labels Y. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. L = loss (..., name, value) accepts the following name-value pairs: "LossFun" selects the loss. Supported values are "mincost", the default, "binodeviance", "classifcost", "classiferror", "crossentropy", "exponential", "hinge", "logit" and "quadratic". "mincost" assigns each observation to the class of least expected cost and charges what that assignment costs, so it reads the scores as a posterior; "classifcost" charges what the model's own prediction costs. "crossentropy" is defined for a network only. Note that the default differs from the other classifiers in this package, which default to "classiferror", and follows MATLAB's for this class. "Weights" holds one weight per row of X, normalised to sum to one before it is applied. See also: ClassificationNeuralNetwork, ClassificationNeuralNetwork.margin, ClassificationNeuralNetwork.edge, ClassificationNeuralNetwork.predict # name: # type: sq_string # elements: 1 # length: 51 Classification loss of a neural network classifier. # name: # type: sq_string # elements: 1 # length: 34 ClassificationNeuralNetwork.margin # name: # type: sq_string # elements: 1 # length: 991 ClassificationNeuralNetwork: m = margin (obj, X, Y) ClassificationNeuralNetwork: m = margin (obj, Tbl, ResponseVarName) ClassificationNeuralNetwork: m = margin (obj, Tbl) Classification margin of a neural network classifier. m = margin (obj, X, Y) returns a column vector holding, for each row of X, the score the model gives its true class in Y less the largest score it gives any other class. A positive margin means the observation is classified correctly, and the larger it is the more confidently so. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: ClassificationNeuralNetwork, ClassificationNeuralNetwork.edge, ClassificationNeuralNetwork.loss, ClassificationNeuralNetwork.predict # name: # type: sq_string # elements: 1 # length: 53 Classification margin of a neural network classifier. # name: # type: sq_string # elements: 1 # length: 35 ClassificationNeuralNetwork.predict # name: # type: sq_string # elements: 1 # length: 1491 ClassificationNeuralNetwork: label = predict (obj, XC) ClassificationNeuralNetwork: [label, score] = predict (obj, XC) Classify new data points into categories using the neural network classification model from a ClassificationNeuralNetwork object. label = predict (obj, XC) returns the vector of labels predicted for the corresponding instances in XC, using the predictor data in obj.X and corresponding labels, obj.Y, stored in the ClassificationNeuralNetwork model, obj. obj must be a ClassificationNeuralNetwork class object. XC must be an MxP numeric matrix with the same number of features P as the corresponding predictors of the neural network model in obj. [label, score] = predict (obj, XC) also returns score, which contains the predicted class scores or posterior probabilities for each instance of the corresponding unique classes. The score matrix contains the classification scores for each class. For each observation in XC, the predicted class label is the one with the highest score among all classes. If the ScoreTransform property is set to a transformation function, the scores are transformed accordingly before being returned. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: ClassificationNeuralNetwork, fitcnet # name: # type: sq_string # elements: 1 # length: 129 Classify new data points into categories using the neural network classification model from a ClassificationNeuralNetwork object. # name: # type: sq_string # elements: 1 # length: 37 ClassificationNeuralNetwork.resubEdge # name: # type: sq_string # elements: 1 # length: 295 ClassificationNeuralNetwork: e = resubEdge (obj) Classification edge of a neural network classifier on its training data. e = resubEdge (obj) is edge applied to the observations the model was fitted on, weighted by obj.W. See also: ClassificationNeuralNetwork, ClassificationNeuralNetwork.edge # name: # type: sq_string # elements: 1 # length: 72 Classification edge of a neural network classifier on its training data. # name: # type: sq_string # elements: 1 # length: 37 ClassificationNeuralNetwork.resubLoss # name: # type: sq_string # elements: 1 # length: 402 ClassificationNeuralNetwork: L = resubLoss (obj) ClassificationNeuralNetwork: L = resubLoss (..., name, value) Classification loss of a neural network classifier on its training data. L = resubLoss (obj) is loss applied to the observations the model was fitted on, weighted by obj.W. It takes the same "LossFun" name-value pair. See also: ClassificationNeuralNetwork, ClassificationNeuralNetwork.loss # name: # type: sq_string # elements: 1 # length: 72 Classification loss of a neural network classifier on its training data. # name: # type: sq_string # elements: 1 # length: 39 ClassificationNeuralNetwork.resubMargin # name: # type: sq_string # elements: 1 # length: 286 ClassificationNeuralNetwork: m = resubMargin (obj) Classification margin of a neural network classifier on its training data. m = resubMargin (obj) is margin applied to the observations the model was fitted on. See also: ClassificationNeuralNetwork, ClassificationNeuralNetwork.margin # name: # type: sq_string # elements: 1 # length: 74 Classification margin of a neural network classifier on its training data. # name: # type: sq_string # elements: 1 # length: 40 ClassificationNeuralNetwork.resubPredict # name: # type: sq_string # elements: 1 # length: 733 ClassificationNeuralNetwork: label = resubPredict (obj) ClassificationNeuralNetwork: [label, score] = resubPredict (obj) Classify the training data using the trained neural network classification object. label = resubPredict (obj) returns the vector of labels predicted for the corresponding instances in the training data, using the predictor data in obj.X and corresponding labels, obj.Y, stored in the neural network classification model, obj. obj must be a ClassificationNeuralNetwork class object. [label, score] = resubPredict (obj) also returns score, which contains the predicted class scores or posterior probabilities for each instance of the corresponding unique classes. See also: ClassificationNeuralNetwork, fitcnet # name: # type: sq_string # elements: 1 # length: 82 Classify the training data using the trained neural network classification object. # name: # type: sq_string # elements: 1 # length: 37 ClassificationNeuralNetwork.savemodel # name: # type: sq_string # elements: 1 # length: 498 ClassificationNeuralNetwork: savemodel (obj, filename) Save a ClassificationNeuralNetwork object. savemodel (obj, filename) saves each property of a ClassificationNeuralNetwork object into an Octave binary file, the name of which is specified in filename, along with an extra variable, which defines the type classification object these variables constitute. Use loadmodel in order to load a classification object into Octave's workspace. See also: loadmodel, fitcnet, ClassificationNeuralNetwork # name: # type: sq_string # elements: 1 # length: 42 Save a ClassificationNeuralNetwork object. # name: # type: sq_string # elements: 1 # length: 29 ClassificationPartitionedECOC # name: # type: sq_string # elements: 1 # length: 764 statistics: ClassificationPartitionedECOC A cross-validated multiclass model built from binary learners. Each fold holds out part of the data, fits an error correcting output codes model on the rest, and answers the part it held out, so every observation is classified by a model that never saw it. It comes from crossval on a ClassificationECOC, and from fitcecoc given any of 'KFold', 'Holdout', 'Leaveout' or 'CVPartition'. This is the one cross-validated class in the package that is not the general ClassificationPartitionedModel. It carries CodingMatrix, BinaryLoss and BinaryY, three things the general class has nowhere to put and without which a fold's scores cannot be decoded at all. See also: fitcecoc, ClassificationECOC, CompactClassificationECOC # name: # type: sq_string # elements: 1 # length: 62 A cross-validated multiclass model built from binary learners. # name: # type: sq_string # elements: 1 # length: 38 ClassificationPartitionedECOC.BinEdges # name: # type: sq_string # elements: 1 # length: 128 ClassificationPartitionedECOC: property BinEdges The bin edges of the predictors, always empty here. This property is read-only. # name: # type: sq_string # elements: 1 # length: 51 The bin edges of the predictors, always empty here. # name: # type: sq_string # elements: 1 # length: 40 ClassificationPartitionedECOC.BinaryLoss # name: # type: sq_string # elements: 1 # length: 130 ClassificationPartitionedECOC: property BinaryLoss The loss the binary scores of a fold are read with. This property is read-only. # name: # type: sq_string # elements: 1 # length: 51 The loss the binary scores of a fold are read with. # name: # type: sq_string # elements: 1 # length: 37 ClassificationPartitionedECOC.BinaryY # name: # type: sq_string # elements: 1 # length: 156 ClassificationPartitionedECOC: property BinaryY What each observation was to each binary learner, an NxL matrix of -1, 0 and +1. This property is read-only. # name: # type: sq_string # elements: 1 # length: 80 What each observation was to each binary learner, an NxL matrix of -1, 0 and +1. # name: # type: sq_string # elements: 1 # length: 51 ClassificationPartitionedECOC.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 153 ClassificationPartitionedECOC: property CategoricalPredictors The columns holding categorical predictors, empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 63 The columns holding categorical predictors, empty when none is. # name: # type: sq_string # elements: 1 # length: 40 ClassificationPartitionedECOC.ClassNames # name: # type: sq_string # elements: 1 # length: 105 ClassificationPartitionedECOC: property ClassNames The distinct class labels. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 The distinct class labels. # name: # type: sq_string # elements: 1 # length: 59 ClassificationPartitionedECOC.ClassificationPartitionedECOC # name: # type: sq_string # elements: 1 # length: 306 ClassificationPartitionedECOC: obj = ClassificationPartitionedECOC (Mdl, Partition) Create a ClassificationPartitionedECOC object. Mdl is the ClassificationECOC to cross validate and Partition the cvpartition saying which rows each fold holds out. The documented way to reach this constructor is crossval. # name: # type: sq_string # elements: 1 # length: 46 Create a ClassificationPartitionedECOC object. # name: # type: sq_string # elements: 1 # length: 42 ClassificationPartitionedECOC.CodingMatrix # name: # type: sq_string # elements: 1 # length: 138 ClassificationPartitionedECOC: property CodingMatrix The coding design every fold was fitted on, a KxL matrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 57 The coding design every fold was fitted on, a KxL matrix. # name: # type: sq_string # elements: 1 # length: 34 ClassificationPartitionedECOC.Cost # name: # type: sq_string # elements: 1 # length: 103 ClassificationPartitionedECOC: property Cost The cost of misclassification. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 The cost of misclassification. # name: # type: sq_string # elements: 1 # length: 49 ClassificationPartitionedECOC.CrossValidatedModel # name: # type: sq_string # elements: 1 # length: 144 ClassificationPartitionedECOC: property CrossValidatedModel The kind of model that was cross-validated, 'ECOC' here. This property is read-only. # name: # type: sq_string # elements: 1 # length: 56 The kind of model that was cross-validated, 'ECOC' here. # name: # type: sq_string # elements: 1 # length: 35 ClassificationPartitionedECOC.KFold # name: # type: sq_string # elements: 1 # length: 94 ClassificationPartitionedECOC: property KFold The number of folds. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 The number of folds. # name: # type: sq_string # elements: 1 # length: 45 ClassificationPartitionedECOC.ModelParameters # name: # type: sq_string # elements: 1 # length: 138 ClassificationPartitionedECOC: property ModelParameters A structure of the options the folds were fitted with. This property is read-only. # name: # type: sq_string # elements: 1 # length: 54 A structure of the options the folds were fitted with. # name: # type: sq_string # elements: 1 # length: 45 ClassificationPartitionedECOC.NumObservations # name: # type: sq_string # elements: 1 # length: 116 ClassificationPartitionedECOC: property NumObservations The number of observations used. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 The number of observations used. # name: # type: sq_string # elements: 1 # length: 39 ClassificationPartitionedECOC.Partition # name: # type: sq_string # elements: 1 # length: 141 ClassificationPartitionedECOC: property Partition The cvpartition object that says which rows each fold held out. This property is read-only. # name: # type: sq_string # elements: 1 # length: 63 The cvpartition object that says which rows each fold held out. # name: # type: sq_string # elements: 1 # length: 44 ClassificationPartitionedECOC.PredictorNames # name: # type: sq_string # elements: 1 # length: 111 ClassificationPartitionedECOC: property PredictorNames The names of the predictors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 The names of the predictors. # name: # type: sq_string # elements: 1 # length: 35 ClassificationPartitionedECOC.Prior # name: # type: sq_string # elements: 1 # length: 110 ClassificationPartitionedECOC: property Prior The prior probability of each class. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 The prior probability of each class. # name: # type: sq_string # elements: 1 # length: 42 ClassificationPartitionedECOC.ResponseName # name: # type: sq_string # elements: 1 # length: 115 ClassificationPartitionedECOC: property ResponseName The name of the response variable. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 The name of the response variable. # name: # type: sq_string # elements: 1 # length: 44 ClassificationPartitionedECOC.ScoreTransform # name: # type: sq_string # elements: 1 # length: 206 ClassificationPartitionedECOC: property ScoreTransform The transform applied to the assembled out-of-fold scores. The folds never carry it: it is applied once to what they return, which is what R2024a does. # name: # type: sq_string # elements: 1 # length: 58 The transform applied to the assembled out-of-fold scores. # name: # type: sq_string # elements: 1 # length: 37 ClassificationPartitionedECOC.Trained # name: # type: sq_string # elements: 1 # length: 264 ClassificationPartitionedECOC: property Trained The models the folds were fitted to A cell column with one CompactClassificationECOC per fold, the training data having no place in a model that only ever answers the rows it did not see. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 The models the folds were fitted to # name: # type: sq_string # elements: 1 # length: 31 ClassificationPartitionedECOC.W # name: # type: sq_string # elements: 1 # length: 94 ClassificationPartitionedECOC: property W The observation weights. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 The observation weights. # name: # type: sq_string # elements: 1 # length: 31 ClassificationPartitionedECOC.X # name: # type: sq_string # elements: 1 # length: 306 ClassificationPartitionedECOC: property X The predictor data the folds were fitted on. This property is read-only. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. # name: # type: sq_string # elements: 1 # length: 44 The predictor data the folds were fitted on. # name: # type: sq_string # elements: 1 # length: 31 ClassificationPartitionedECOC.Y # name: # type: sq_string # elements: 1 # length: 112 ClassificationPartitionedECOC: property Y The class labels the folds were fitted on. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 The class labels the folds were fitted on. # name: # type: sq_string # elements: 1 # length: 39 ClassificationPartitionedECOC.kfoldEdge # name: # type: sq_string # elements: 1 # length: 170 ClassificationPartitionedECOC: e = kfoldEdge (obj) Out-of-fold classification edge, the weighted mean of the margins. See also: ClassificationPartitionedECOC.kfoldMargin # name: # type: sq_string # elements: 1 # length: 66 Out-of-fold classification edge, the weighted mean of the margins. # name: # type: sq_string # elements: 1 # length: 39 ClassificationPartitionedECOC.kfoldLoss # name: # type: sq_string # elements: 1 # length: 339 ClassificationPartitionedECOC: L = kfoldLoss (obj) ClassificationPartitionedECOC: L = kfoldLoss (..., name, value) Out-of-fold classification loss. Name Value 'LossFun' 'classiferror' (default), 'classifcost', 'mincost', 'binodeviance', 'exponential', 'hinge', 'logit' or 'quadratic'. See also: ClassificationPartitionedECOC.kfoldPredict # name: # type: sq_string # elements: 1 # length: 32 Out-of-fold classification loss. # name: # type: sq_string # elements: 1 # length: 41 ClassificationPartitionedECOC.kfoldMargin # name: # type: sq_string # elements: 1 # length: 203 ClassificationPartitionedECOC: m = kfoldMargin (obj) Out-of-fold classification margin of every observation. See also: ClassificationPartitionedECOC.kfoldPredict, ClassificationPartitionedECOC.kfoldEdge # name: # type: sq_string # elements: 1 # length: 55 Out-of-fold classification margin of every observation. # name: # type: sq_string # elements: 1 # length: 42 ClassificationPartitionedECOC.kfoldPredict # name: # type: sq_string # elements: 1 # length: 498 ClassificationPartitionedECOC: label = kfoldPredict (obj) ClassificationPartitionedECOC: [label, NegLoss] = kfoldPredict (obj) Out-of-fold class of every observation. Each observation is classified by the fold that held it out, so the labels are out-of-sample. An observation no fold held out, which under a holdout partition is most of them, comes back missing rather than classified, and its NegLoss row comes back NaN. See also: ClassificationPartitionedECOC, CompactClassificationECOC.predict # name: # type: sq_string # elements: 1 # length: 39 Out-of-fold class of every observation. # name: # type: sq_string # elements: 1 # length: 38 ClassificationPartitionedECOC.kfoldfun # name: # type: sq_string # elements: 1 # length: 319 ClassificationPartitionedECOC: vals = kfoldfun (obj, fun) Apply a function to every fold. fun is called once per fold as fun (CMdl, Xtrain, Ytrain, Wtrain, Xtest, Ytest, Wtest) and must return a numeric row of the same length every time. vals has one row per fold. See also: ClassificationPartitionedECOC.kfoldPredict # name: # type: sq_string # elements: 1 # length: 31 Apply a function to every fold. # name: # type: sq_string # elements: 1 # length: 33 ClassificationPartitionedEnsemble # name: # type: sq_string # elements: 1 # length: 591 statistics: ClassificationPartitionedEnsemble Cross-validated ensemble of decision trees for classification A ClassificationPartitionedEnsemble object holds one ensemble per fold of a partition, each fitted on the observations the fold keeps for training, and answers for every observation with the ensemble of the fold that held it out. Create one with fitcensemble given a cross-validation option, or with the crossval method of a ClassificationEnsemble or ClassificationBaggedEnsemble. See also: fitcensemble, ClassificationEnsemble.crossval, CompactClassificationEnsemble, cvpartition # name: # type: sq_string # elements: 1 # length: 61 Cross-validated ensemble of decision trees for classification # name: # type: sq_string # elements: 1 # length: 42 ClassificationPartitionedEnsemble.BinEdges # name: # type: sq_string # elements: 1 # length: 123 ClassificationPartitionedEnsemble: property BinEdges Bin edges of the predictors Always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Bin edges of the predictors # name: # type: sq_string # elements: 1 # length: 55 ClassificationPartitionedEnsemble.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 142 ClassificationPartitionedEnsemble: property CategoricalPredictors Indices of categorical predictors Always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Indices of categorical predictors # name: # type: sq_string # elements: 1 # length: 44 ClassificationPartitionedEnsemble.ClassNames # name: # type: sq_string # elements: 1 # length: 104 ClassificationPartitionedEnsemble: property ClassNames Names of the classes This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Names of the classes # name: # type: sq_string # elements: 1 # length: 67 ClassificationPartitionedEnsemble.ClassificationPartitionedEnsemble # name: # type: sq_string # elements: 1 # length: 277 ClassificationPartitionedEnsemble: CVMdl = ClassificationPartitionedEnsemble (Mdl, Partition) Cross-validate a classification ensemble. Mdl is the ensemble to cross-validate and Partition a cvpartition of its observations. The documented ways in are fitcensemble and crossval. # name: # type: sq_string # elements: 1 # length: 41 Cross-validate a classification ensemble. # name: # type: sq_string # elements: 1 # length: 38 ClassificationPartitionedEnsemble.Cost # name: # type: sq_string # elements: 1 # length: 101 ClassificationPartitionedEnsemble: property Cost Misclassification costs This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Misclassification costs # name: # type: sq_string # elements: 1 # length: 53 ClassificationPartitionedEnsemble.CrossValidatedModel # name: # type: sq_string # elements: 1 # length: 190 ClassificationPartitionedEnsemble: property CrossValidatedModel Method of the cross-validated ensemble The Method of the ensemble, such as 'AdaBoostM1' or 'Bag'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Method of the cross-validated ensemble # name: # type: sq_string # elements: 1 # length: 39 ClassificationPartitionedEnsemble.KFold # name: # type: sq_string # elements: 1 # length: 94 ClassificationPartitionedEnsemble: property KFold Number of folds This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Number of folds # name: # type: sq_string # elements: 1 # length: 49 ClassificationPartitionedEnsemble.ModelParameters # name: # type: sq_string # elements: 1 # length: 301 ClassificationPartitionedEnsemble: property ModelParameters Parameters of the cross-validation A structure with the fields Type, Method, 'PartitionedEnsemble', LearnerTemplates, the ModelParameters of the ensemble the folds were fitted as, and NLearn, the number of folds. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Parameters of the cross-validation # name: # type: sq_string # elements: 1 # length: 49 ClassificationPartitionedEnsemble.NumObservations # name: # type: sq_string # elements: 1 # length: 111 ClassificationPartitionedEnsemble: property NumObservations Number of observations This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 51 ClassificationPartitionedEnsemble.NumTrainedPerFold # name: # type: sq_string # elements: 1 # length: 136 ClassificationPartitionedEnsemble: property NumTrainedPerFold Number of learners in each fold A row vector. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Number of learners in each fold # name: # type: sq_string # elements: 1 # length: 43 ClassificationPartitionedEnsemble.Partition # name: # type: sq_string # elements: 1 # length: 138 ClassificationPartitionedEnsemble: property Partition The partition of the observations A cvpartition object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 The partition of the observations # name: # type: sq_string # elements: 1 # length: 48 ClassificationPartitionedEnsemble.PredictorNames # name: # type: sq_string # elements: 1 # length: 111 ClassificationPartitionedEnsemble: property PredictorNames Names of the predictors This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 39 ClassificationPartitionedEnsemble.Prior # name: # type: sq_string # elements: 1 # length: 113 ClassificationPartitionedEnsemble: property Prior Prior probabilities of the classes This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Prior probabilities of the classes # name: # type: sq_string # elements: 1 # length: 46 ClassificationPartitionedEnsemble.ResponseName # name: # type: sq_string # elements: 1 # length: 115 ClassificationPartitionedEnsemble: property ResponseName Name of the response variable This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 48 ClassificationPartitionedEnsemble.ScoreTransform # name: # type: sq_string # elements: 1 # length: 177 ClassificationPartitionedEnsemble: property ScoreTransform Transform applied to the out-of-fold scores The folds carry none; this one is applied once to the scores they return. # name: # type: sq_string # elements: 1 # length: 43 Transform applied to the out-of-fold scores # name: # type: sq_string # elements: 1 # length: 43 ClassificationPartitionedEnsemble.Trainable # name: # type: sq_string # elements: 1 # length: 227 ClassificationPartitionedEnsemble: property Trainable The full ensemble of each fold A column cell array with one ClassificationEnsemble or ClassificationBaggedEnsemble per fold, which resume grows. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 The full ensemble of each fold # name: # type: sq_string # elements: 1 # length: 41 ClassificationPartitionedEnsemble.Trained # name: # type: sq_string # elements: 1 # length: 183 ClassificationPartitionedEnsemble: property Trained The compact ensemble of each fold A column cell array with one CompactClassificationEnsemble per fold. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 The compact ensemble of each fold # name: # type: sq_string # elements: 1 # length: 35 ClassificationPartitionedEnsemble.W # name: # type: sq_string # elements: 1 # length: 210 ClassificationPartitionedEnsemble: property W Observation weights The weights of the ensemble that was cross-validated. The losses and edges weigh the held-out observations by them. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 35 ClassificationPartitionedEnsemble.X # name: # type: sq_string # elements: 1 # length: 281 ClassificationPartitionedEnsemble: property X Predictor data Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 35 ClassificationPartitionedEnsemble.Y # name: # type: sq_string # elements: 1 # length: 87 ClassificationPartitionedEnsemble: property Y Class labels This property is read-only. # name: # type: sq_string # elements: 1 # length: 12 Class labels # name: # type: sq_string # elements: 1 # length: 43 ClassificationPartitionedEnsemble.kfoldEdge # name: # type: sq_string # elements: 1 # length: 403 ClassificationPartitionedEnsemble: e = kfoldEdge (obj) ClassificationPartitionedEnsemble: e = kfoldEdge (..., name, value) Cross-validated classification edge. The weighted mean of the out-of-fold margins, weighted by W, NaN if a held-out observation has no score. 'Folds' and 'Mode' are taken as by kfoldLoss. See also: ClassificationPartitionedEnsemble, ClassificationPartitionedEnsemble.kfoldMargin # name: # type: sq_string # elements: 1 # length: 36 Cross-validated classification edge. # name: # type: sq_string # elements: 1 # length: 43 ClassificationPartitionedEnsemble.kfoldLoss # name: # type: sq_string # elements: 1 # length: 900 ClassificationPartitionedEnsemble: L = kfoldLoss (obj) ClassificationPartitionedEnsemble: L = kfoldLoss (..., name, value) Cross-validated classification loss. L is the loss of the out-of-fold scores, the held-out observations weighted by W. An observation no fold held out is left out. One held out but scored by no learner, for its missing values, stays, as in CompactClassificationEnsemble.loss. Name-Value arguments: Name Value 'Folds' The folds to use, pooled. The default is all of them. 'LossFun' A loss CompactClassificationEnsemble.loss accepts. The default is 'classiferror'. 'Mode' 'average' (default) for one loss over the observations of every fold used, 'individual' for a column with the loss of each fold, or 'cumulative' for a column whose element t uses the first t learners of every fold. See also: ClassificationPartitionedEnsemble, ClassificationPartitionedEnsemble.kfoldEdge # name: # type: sq_string # elements: 1 # length: 36 Cross-validated classification loss. # name: # type: sq_string # elements: 1 # length: 45 ClassificationPartitionedEnsemble.kfoldMargin # name: # type: sq_string # elements: 1 # length: 281 ClassificationPartitionedEnsemble: m = kfoldMargin (obj) Cross-validated classification margins. m holds each observation's margin under its out-of-fold scores, NaN for one no fold held out. See also: ClassificationPartitionedEnsemble, ClassificationPartitionedEnsemble.kfoldEdge # name: # type: sq_string # elements: 1 # length: 39 Cross-validated classification margins. # name: # type: sq_string # elements: 1 # length: 46 ClassificationPartitionedEnsemble.kfoldPredict # name: # type: sq_string # elements: 1 # length: 552 ClassificationPartitionedEnsemble: label = kfoldPredict (obj) ClassificationPartitionedEnsemble: [label, scores] = kfoldPredict (obj) Classify each observation with the fold that held it out. scores holds each observation's scores from the ensemble of the fold that held it out, after ScoreTransform, and label the class of highest score. An observation no fold held out, as under a holdout partition, has NaN scores and the class of greatest prior probability. See also: ClassificationPartitionedEnsemble, ClassificationPartitionedEnsemble.kfoldLoss # name: # type: sq_string # elements: 1 # length: 57 Classify each observation with the fold that held it out. # name: # type: sq_string # elements: 1 # length: 42 ClassificationPartitionedEnsemble.kfoldfun # name: # type: sq_string # elements: 1 # length: 336 ClassificationPartitionedEnsemble: vals = kfoldfun (obj, fun) Apply a function to each fold. fun is called once per fold as fun (CMP, Xtrain, Ytrain, Wtrain, Xtest, Ytest, Wtest), CMP being the fold's compact ensemble and the weights those of W, and must return a row. vals stacks the rows. See also: ClassificationPartitionedEnsemble # name: # type: sq_string # elements: 1 # length: 30 Apply a function to each fold. # name: # type: sq_string # elements: 1 # length: 40 ClassificationPartitionedEnsemble.resume # name: # type: sq_string # elements: 1 # length: 357 ClassificationPartitionedEnsemble: CVMdl = resume (obj, NumLearningCycles) ClassificationPartitionedEnsemble: CVMdl = resume (..., 'NPrint', n) Grow more learners in every fold. Each fold's ensemble is resumed, as ClassificationEnsemble.resume does, by NumLearningCycles learners. See also: ClassificationPartitionedEnsemble, ClassificationEnsemble.resume # name: # type: sq_string # elements: 1 # length: 33 Grow more learners in every fold. # name: # type: sq_string # elements: 1 # length: 31 ClassificationPartitionedKernel # name: # type: sq_string # elements: 1 # length: 923 statistics: ClassificationPartitionedKernel Cross-validated Gaussian kernel binary classifier. A ClassificationPartitionedKernel object holds one ClassificationKernel per fold of a partition, each fitted to the observations the fold trains on. Every kfold method predicts each observation with the fold that held it out, so the estimate it returns is an out-of-sample one. A ClassificationKernel stores no copy of its training data and so has no resubstitution methods and no compact form. This class is what takes their place: cross-validation is the way a linear model is asked how it would do on data it has not seen. When the fold models carry a whole regularization path, every method returns one column per strength, in the order of the 'Lambda' that was asked for. Create one with fitclinear and a cross-validation option, or directly. See also: fitclinear, ClassificationKernel, ClassificationPartitionedKernel # name: # type: sq_string # elements: 1 # length: 50 Cross-validated Gaussian kernel binary classifier. # name: # type: sq_string # elements: 1 # length: 53 ClassificationPartitionedKernel.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 201 ClassificationPartitionedKernel: property CategoricalPredictors Indices of the categorical predictors A row vector of column indices, empty when every predictor is numeric. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 42 ClassificationPartitionedKernel.ClassNames # name: # type: sq_string # elements: 1 # length: 171 ClassificationPartitionedKernel: property ClassNames Names of the two classes A column of the same type as the response, shared by every fold. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Names of the two classes # name: # type: sq_string # elements: 1 # length: 63 ClassificationPartitionedKernel.ClassificationPartitionedKernel # name: # type: sq_string # elements: 1 # length: 1040 ClassificationPartitionedKernel: obj = ClassificationPartitionedKernel (X, Y) ClassificationPartitionedKernel: obj = ClassificationPartitionedKernel (..., name, value) Cross-validate a linear binary classifier. obj = ClassificationPartitionedKernel (X, Y) partitions the data into ten stratified folds and fits a ClassificationKernel to each. obj = ClassificationPartitionedKernel (..., name, value) takes one of 'KFold', 'Holdout', 'Leaveout' and 'CVPartition' to say how to partition, and any option ClassificationKernel takes to say how to fit. 'CrossVal' is accepted and has no effect here, this class being cross-validated by construction. The classes, the prior and the cost are resolved once over the whole data and handed to every fold. Anything left as 'auto' is not: each fold resolves 'Lambda' and 'KernelScale' against its own training rows, so ten folds of a hundred observations each get a Lambda of one ninetieth rather than one hundredth. Both are MATLAB's behaviour, measured. See also: fitclinear, ClassificationKernel # name: # type: sq_string # elements: 1 # length: 42 Cross-validate a linear binary classifier. # name: # type: sq_string # elements: 1 # length: 36 ClassificationPartitionedKernel.Cost # name: # type: sq_string # elements: 1 # length: 235 ClassificationPartitionedKernel: property Cost Cost of misclassifying an observation A square numeric matrix with one row and one column per class. It is handed to every fold rather than re-derived by each. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Cost of misclassifying an observation # name: # type: sq_string # elements: 1 # length: 51 ClassificationPartitionedKernel.CrossValidatedModel # name: # type: sq_string # elements: 1 # length: 178 ClassificationPartitionedKernel: property CrossValidatedModel Name of the model that was cross-validated Always 'Linear', the short name MATLAB uses. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Name of the model that was cross-validated # name: # type: sq_string # elements: 1 # length: 37 ClassificationPartitionedKernel.KFold # name: # type: sq_string # elements: 1 # length: 221 ClassificationPartitionedKernel: property KFold Number of folds A positive integer scalar. A holdout partition has one fold and a leave-one-out partition has as many as there are observations. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Number of folds # name: # type: sq_string # elements: 1 # length: 47 ClassificationPartitionedKernel.ModelParameters # name: # type: sq_string # elements: 1 # length: 1135 ClassificationPartitionedKernel: property ModelParameters What was cross-validated, and how A structure holding the parameters the folds were fitted with, carried through from the learner that was cross validated, beside NLearn, the number of folds, and the Version, Method and Type tags of this class, with LearnerTemplates naming the backing. The learner's own tags are replaced rather than kept, so a cross-validated SVM reports Method as 'PartitionedKernel' and not 'SVM'. Deviation from MATLAB. MATLAB reports the parameter record of the cross-validation ensemble here rather than of the learner, so it says nothing at all about how the folds were fitted: of its eighteen fields only the fold count, its partitioner and a fit template carry anything, and the rest are boosting settings left inert. Nor can the parameters be reached through the folds, a compact model carrying none in MATLAB. This class reports the fit instead, which is strictly more than MATLAB offers, and everything MATLAB's record does carry is published here as the KFold, Partition, X, Y, W and CrossValidatedModel properties. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 What was cross-validated, and how # name: # type: sq_string # elements: 1 # length: 47 ClassificationPartitionedKernel.NumObservations # name: # type: sq_string # elements: 1 # length: 220 ClassificationPartitionedKernel: property NumObservations Number of observations the partition covers A positive integer scalar, counting the rows that survived the removal of missing values. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Number of observations the partition covers # name: # type: sq_string # elements: 1 # length: 41 ClassificationPartitionedKernel.Partition # name: # type: sq_string # elements: 1 # length: 154 ClassificationPartitionedKernel: property Partition The partition itself A cvpartition object over the retained observations. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 The partition itself # name: # type: sq_string # elements: 1 # length: 46 ClassificationPartitionedKernel.PredictorNames # name: # type: sq_string # elements: 1 # length: 144 ClassificationPartitionedKernel: property PredictorNames Names of the predictors A cell array of character vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 37 ClassificationPartitionedKernel.Prior # name: # type: sq_string # elements: 1 # length: 313 ClassificationPartitionedKernel: property Prior Prior probability of each class A numeric row vector summing to one, in the order of ClassNames. Like the cost it is the parent's and is handed to every fold, so a fold of an unbalanced problem does not quietly adopt a prior of its own. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Prior probability of each class # name: # type: sq_string # elements: 1 # length: 44 ClassificationPartitionedKernel.ResponseName # name: # type: sq_string # elements: 1 # length: 143 ClassificationPartitionedKernel: property ResponseName Name of the response A character vector, defaulting to 'Y'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Name of the response # name: # type: sq_string # elements: 1 # length: 46 ClassificationPartitionedKernel.ScoreTransform # name: # type: sq_string # elements: 1 # length: 458 ClassificationPartitionedKernel: property ScoreTransform Transformation applied to the predicted scores A character vector naming a transformation, or the text of the function handle that was supplied, which may be assigned after the model is built. It is applied once to the assembled scores and is not handed to the folds. A transform the learner implies, as 'logistic' implies 'logit', does stay with the folds, and this one is then applied on top of it. # name: # type: sq_string # elements: 1 # length: 46 Transformation applied to the predicted scores # name: # type: sq_string # elements: 1 # length: 39 ClassificationPartitionedKernel.Trained # name: # type: sq_string # elements: 1 # length: 215 ClassificationPartitionedKernel: property Trained The models fitted to the folds A cell column with one ClassificationKernel per fold, each fitted to the observations its fold trains on. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 The models fitted to the folds # name: # type: sq_string # elements: 1 # length: 33 ClassificationPartitionedKernel.W # name: # type: sq_string # elements: 1 # length: 196 ClassificationPartitionedKernel: property W Observation weights An Nx1 numeric vector summing to one, normalized within each class to that class's cost-adjusted prior. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 33 ClassificationPartitionedKernel.Y # name: # type: sq_string # elements: 1 # length: 142 ClassificationPartitionedKernel: property Y Response of the retained observations In the type it was supplied in. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Response of the retained observations # name: # type: sq_string # elements: 1 # length: 41 ClassificationPartitionedKernel.kfoldEdge # name: # type: sq_string # elements: 1 # length: 363 ClassificationPartitionedKernel: e = kfoldEdge (obj) ClassificationPartitionedKernel: e = kfoldEdge (..., name, value) Weighted mean of the out-of-fold classification margins. e = kfoldEdge (..., name, value) takes 'Folds', a subset of the folds to average over, and 'Mode', either 'average', the default, or 'individual', which returns one row per fold instead. # name: # type: sq_string # elements: 1 # length: 56 Weighted mean of the out-of-fold classification margins. # name: # type: sq_string # elements: 1 # length: 41 ClassificationPartitionedKernel.kfoldLoss # name: # type: sq_string # elements: 1 # length: 406 ClassificationPartitionedKernel: l = kfoldLoss (obj) ClassificationPartitionedKernel: l = kfoldLoss (..., name, value) Out-of-fold classification loss. l = kfoldLoss (obj) returns the out-of-fold misclassification rate. l = kfoldLoss (..., name, value) takes 'LossFun', one of 'binodeviance', 'classifcost', 'classiferror', 'exponential', 'hinge', 'logit', 'mincost' and 'quadratic'; 'Folds'; and 'Mode'. # name: # type: sq_string # elements: 1 # length: 32 Out-of-fold classification loss. # name: # type: sq_string # elements: 1 # length: 43 ClassificationPartitionedKernel.kfoldMargin # name: # type: sq_string # elements: 1 # length: 252 ClassificationPartitionedKernel: m = kfoldMargin (obj) Out-of-fold classification margin of every observation. The score the out-of-fold model gives the true class, less the score it gives the other one. An observation no fold held out comes back NaN. # name: # type: sq_string # elements: 1 # length: 55 Out-of-fold classification margin of every observation. # name: # type: sq_string # elements: 1 # length: 44 ClassificationPartitionedKernel.kfoldPredict # name: # type: sq_string # elements: 1 # length: 426 ClassificationPartitionedKernel: labels = kfoldPredict (obj) ClassificationPartitionedKernel: [labels, scores] = kfoldPredict (obj) Out-of-fold class of every observation. Each observation is classified by the fold that held it out, so the labels are out-of-sample. An observation that no fold held out, which under a holdout partition is most of them, comes back missing rather than classified, and its scores come back NaN. # name: # type: sq_string # elements: 1 # length: 39 Out-of-fold class of every observation. # name: # type: sq_string # elements: 1 # length: 31 ClassificationPartitionedLinear # name: # type: sq_string # elements: 1 # length: 914 statistics: ClassificationPartitionedLinear Cross-validated linear binary classifier. A ClassificationPartitionedLinear object holds one ClassificationLinear per fold of a partition, each fitted to the observations the fold trains on. Every kfold method predicts each observation with the fold that held it out, so the estimate it returns is an out-of-sample one. A ClassificationLinear stores no copy of its training data and so has no resubstitution methods and no compact form. This class is what takes their place: cross-validation is the way a linear model is asked how it would do on data it has not seen. When the fold models carry a whole regularization path, every method returns one column per strength, in the order of the 'Lambda' that was asked for. Create one with fitclinear and a cross-validation option, or directly. See also: fitclinear, ClassificationLinear, ClassificationPartitionedKernel # name: # type: sq_string # elements: 1 # length: 41 Cross-validated linear binary classifier. # name: # type: sq_string # elements: 1 # length: 53 ClassificationPartitionedLinear.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 201 ClassificationPartitionedLinear: property CategoricalPredictors Indices of the categorical predictors A row vector of column indices, empty when every predictor is numeric. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 42 ClassificationPartitionedLinear.ClassNames # name: # type: sq_string # elements: 1 # length: 171 ClassificationPartitionedLinear: property ClassNames Names of the two classes A column of the same type as the response, shared by every fold. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Names of the two classes # name: # type: sq_string # elements: 1 # length: 63 ClassificationPartitionedLinear.ClassificationPartitionedLinear # name: # type: sq_string # elements: 1 # length: 1010 ClassificationPartitionedLinear: obj = ClassificationPartitionedLinear (X, Y) ClassificationPartitionedLinear: obj = ClassificationPartitionedLinear (..., name, value) Cross-validate a linear binary classifier. obj = ClassificationPartitionedLinear (X, Y) partitions the data into ten stratified folds and fits a ClassificationLinear to each. obj = ClassificationPartitionedLinear (..., name, value) takes one of 'KFold', 'Holdout', 'Leaveout' and 'CVPartition' to say how to partition, and any option ClassificationLinear takes to say how to fit. 'CrossVal' is accepted and has no effect here, this class being cross-validated by construction. The classes, the prior and the cost are resolved once over the whole data and handed to every fold. Anything left as 'auto' is not: each fold resolves 'Lambda' against its own training rows, so ten folds of a hundred observations each get one ninetieth rather than one hundredth. Both are MATLAB's behaviour, measured. See also: fitclinear, ClassificationLinear # name: # type: sq_string # elements: 1 # length: 42 Cross-validate a linear binary classifier. # name: # type: sq_string # elements: 1 # length: 36 ClassificationPartitionedLinear.Cost # name: # type: sq_string # elements: 1 # length: 235 ClassificationPartitionedLinear: property Cost Cost of misclassifying an observation A square numeric matrix with one row and one column per class. It is handed to every fold rather than re-derived by each. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Cost of misclassifying an observation # name: # type: sq_string # elements: 1 # length: 51 ClassificationPartitionedLinear.CrossValidatedModel # name: # type: sq_string # elements: 1 # length: 178 ClassificationPartitionedLinear: property CrossValidatedModel Name of the model that was cross-validated Always 'Linear', the short name MATLAB uses. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Name of the model that was cross-validated # name: # type: sq_string # elements: 1 # length: 37 ClassificationPartitionedLinear.KFold # name: # type: sq_string # elements: 1 # length: 221 ClassificationPartitionedLinear: property KFold Number of folds A positive integer scalar. A holdout partition has one fold and a leave-one-out partition has as many as there are observations. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Number of folds # name: # type: sq_string # elements: 1 # length: 47 ClassificationPartitionedLinear.ModelParameters # name: # type: sq_string # elements: 1 # length: 1135 ClassificationPartitionedLinear: property ModelParameters What was cross-validated, and how A structure holding the parameters the folds were fitted with, carried through from the learner that was cross validated, beside NLearn, the number of folds, and the Version, Method and Type tags of this class, with LearnerTemplates naming the backing. The learner's own tags are replaced rather than kept, so a cross-validated SVM reports Method as 'PartitionedLinear' and not 'SVM'. Deviation from MATLAB. MATLAB reports the parameter record of the cross-validation ensemble here rather than of the learner, so it says nothing at all about how the folds were fitted: of its eighteen fields only the fold count, its partitioner and a fit template carry anything, and the rest are boosting settings left inert. Nor can the parameters be reached through the folds, a compact model carrying none in MATLAB. This class reports the fit instead, which is strictly more than MATLAB offers, and everything MATLAB's record does carry is published here as the KFold, Partition, X, Y, W and CrossValidatedModel properties. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 What was cross-validated, and how # name: # type: sq_string # elements: 1 # length: 47 ClassificationPartitionedLinear.NumObservations # name: # type: sq_string # elements: 1 # length: 220 ClassificationPartitionedLinear: property NumObservations Number of observations the partition covers A positive integer scalar, counting the rows that survived the removal of missing values. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Number of observations the partition covers # name: # type: sq_string # elements: 1 # length: 41 ClassificationPartitionedLinear.Partition # name: # type: sq_string # elements: 1 # length: 154 ClassificationPartitionedLinear: property Partition The partition itself A cvpartition object over the retained observations. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 The partition itself # name: # type: sq_string # elements: 1 # length: 46 ClassificationPartitionedLinear.PredictorNames # name: # type: sq_string # elements: 1 # length: 144 ClassificationPartitionedLinear: property PredictorNames Names of the predictors A cell array of character vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 37 ClassificationPartitionedLinear.Prior # name: # type: sq_string # elements: 1 # length: 313 ClassificationPartitionedLinear: property Prior Prior probability of each class A numeric row vector summing to one, in the order of ClassNames. Like the cost it is the parent's and is handed to every fold, so a fold of an unbalanced problem does not quietly adopt a prior of its own. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Prior probability of each class # name: # type: sq_string # elements: 1 # length: 44 ClassificationPartitionedLinear.ResponseName # name: # type: sq_string # elements: 1 # length: 143 ClassificationPartitionedLinear: property ResponseName Name of the response A character vector, defaulting to 'Y'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Name of the response # name: # type: sq_string # elements: 1 # length: 46 ClassificationPartitionedLinear.ScoreTransform # name: # type: sq_string # elements: 1 # length: 458 ClassificationPartitionedLinear: property ScoreTransform Transformation applied to the predicted scores A character vector naming a transformation, or the text of the function handle that was supplied, which may be assigned after the model is built. It is applied once to the assembled scores and is not handed to the folds. A transform the learner implies, as 'logistic' implies 'logit', does stay with the folds, and this one is then applied on top of it. # name: # type: sq_string # elements: 1 # length: 46 Transformation applied to the predicted scores # name: # type: sq_string # elements: 1 # length: 39 ClassificationPartitionedLinear.Trained # name: # type: sq_string # elements: 1 # length: 215 ClassificationPartitionedLinear: property Trained The models fitted to the folds A cell column with one ClassificationLinear per fold, each fitted to the observations its fold trains on. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 The models fitted to the folds # name: # type: sq_string # elements: 1 # length: 33 ClassificationPartitionedLinear.W # name: # type: sq_string # elements: 1 # length: 196 ClassificationPartitionedLinear: property W Observation weights An Nx1 numeric vector summing to one, normalized within each class to that class's cost-adjusted prior. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 33 ClassificationPartitionedLinear.Y # name: # type: sq_string # elements: 1 # length: 142 ClassificationPartitionedLinear: property Y Response of the retained observations In the type it was supplied in. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Response of the retained observations # name: # type: sq_string # elements: 1 # length: 41 ClassificationPartitionedLinear.kfoldEdge # name: # type: sq_string # elements: 1 # length: 363 ClassificationPartitionedLinear: e = kfoldEdge (obj) ClassificationPartitionedLinear: e = kfoldEdge (..., name, value) Weighted mean of the out-of-fold classification margins. e = kfoldEdge (..., name, value) takes 'Folds', a subset of the folds to average over, and 'Mode', either 'average', the default, or 'individual', which returns one row per fold instead. # name: # type: sq_string # elements: 1 # length: 56 Weighted mean of the out-of-fold classification margins. # name: # type: sq_string # elements: 1 # length: 41 ClassificationPartitionedLinear.kfoldLoss # name: # type: sq_string # elements: 1 # length: 406 ClassificationPartitionedLinear: l = kfoldLoss (obj) ClassificationPartitionedLinear: l = kfoldLoss (..., name, value) Out-of-fold classification loss. l = kfoldLoss (obj) returns the out-of-fold misclassification rate. l = kfoldLoss (..., name, value) takes 'LossFun', one of 'binodeviance', 'classifcost', 'classiferror', 'exponential', 'hinge', 'logit', 'mincost' and 'quadratic'; 'Folds'; and 'Mode'. # name: # type: sq_string # elements: 1 # length: 32 Out-of-fold classification loss. # name: # type: sq_string # elements: 1 # length: 43 ClassificationPartitionedLinear.kfoldMargin # name: # type: sq_string # elements: 1 # length: 315 ClassificationPartitionedLinear: m = kfoldMargin (obj) Out-of-fold classification margin of every observation. The score the out-of-fold model gives the true class, less the score it gives the other one. An observation no fold held out comes back NaN. With L regularization strengths m has one column per strength. # name: # type: sq_string # elements: 1 # length: 55 Out-of-fold classification margin of every observation. # name: # type: sq_string # elements: 1 # length: 44 ClassificationPartitionedLinear.kfoldPredict # name: # type: sq_string # elements: 1 # length: 515 ClassificationPartitionedLinear: labels = kfoldPredict (obj) ClassificationPartitionedLinear: [labels, scores] = kfoldPredict (obj) Out-of-fold class of every observation. Each observation is classified by the fold that held it out, so the labels are out-of-sample. An observation that no fold held out, which under a holdout partition is most of them, comes back missing rather than classified, and its scores come back NaN. With L regularization strengths labels has one column per strength and scores is Nx2xL. # name: # type: sq_string # elements: 1 # length: 39 Out-of-fold class of every observation. # name: # type: sq_string # elements: 1 # length: 30 ClassificationPartitionedModel # name: # type: sq_string # elements: 1 # length: 406 statistics: ClassificationPartitionedModel Cross-validated classification model The ClassificationPartitionedModel class stores cross-validated classification models trained on different partitions of the data. It can predict responses for observations not used for training using the kfoldPredict method. Create a ClassificationPartitionedModel object by using the crossval function. See also: crossval # name: # type: sq_string # elements: 1 # length: 36 Cross-validated classification model # name: # type: sq_string # elements: 1 # length: 39 ClassificationPartitionedModel.BinEdges # name: # type: sq_string # elements: 1 # length: 491 ClassificationPartitionedModel: property BinEdges Bin edges A cell array with one entry per predictor, holding that predictor's bin edges where the learner discretized it before fitting. It is carried over from the model that was cross validated, and is empty whenever that model did no binning, which is every learner this package implements: MATLAB fills it only for its GAM, which bins because it is built from boosted trees where ours is built from splines. This property is read-only. # name: # type: sq_string # elements: 1 # length: 9 Bin edges # name: # type: sq_string # elements: 1 # length: 52 ClassificationPartitionedModel.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 205 ClassificationPartitionedModel: property CategoricalPredictors Indices of categorical predictors A vector of positive integers specifying the indices of categorical predictors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Indices of categorical predictors # name: # type: sq_string # elements: 1 # length: 41 ClassificationPartitionedModel.ClassNames # name: # type: sq_string # elements: 1 # length: 412 ClassificationPartitionedModel: property ClassNames Names of classes in the response variable An array of unique values of the response variable Y, which has the same data types as the data in Y, sorted or in the order given by the 'ClassNames' option. This property is read-only. ClassNames can have any of the following datatypes: Cell array of character vectors Character array Logical vector Numeric vector # name: # type: sq_string # elements: 1 # length: 41 Names of classes in the response variable # name: # type: sq_string # elements: 1 # length: 61 ClassificationPartitionedModel.ClassificationPartitionedModel # name: # type: sq_string # elements: 1 # length: 444 ClassificationPartitionedModel: this = ClassificationPartitionedModel (Mdl, Partition) Create a ClassificationPartitionedModel class object for cross-validation of classification models. this = ClassificationPartitionedModel (Mdl, Partition) returns a ClassificationPartitionedModel object, with Mdl as the trained classification model object and Partition as the partitioning object obtained using cvpartition function. See also: cvpartition # name: # type: sq_string # elements: 1 # length: 99 Create a ClassificationPartitionedModel class object for cross-validation of classification models. # name: # type: sq_string # elements: 1 # length: 35 ClassificationPartitionedModel.Cost # name: # type: sq_string # elements: 1 # length: 1429 ClassificationPartitionedModel: property Cost Cost of Misclassification A square matrix specifying the cost of misclassification of a point. Cost(i,j) is the cost of classifying a point into class j if its true class is i (that is, the rows correspond to the true class and the columns correspond to the predicted class). The order of the rows and columns in Cost corresponds to the order of the classes in ClassNames. The number of rows and columns in Cost is the number of unique classes in the response. By default, Cost(i,j) = 1 if i != j, and Cost(i,j) = 0 if i = j. In other words, the cost is 0 for correct classification and 1 for incorrect classification. Assigning Cost rebuilds it on every fold in Trained, so kfoldPredict and kfoldLoss answer under the new costs. It is refused on a cross-validated ClassificationSVM, whose costs enter the box constraint while it is being fitted: a model already fitted under one cost matrix cannot be made to describe another. A cost may also be given as a struct with the fields ClassNames and ClassificationCosts, which names the order its own matrix is written in. That matrix is permuted into the order of ClassNames above, so a caller need not know which order the classes were sorted into. It must name every class. A cost must be floating point, not sparse, not complex, non-negative and zero down its diagonal, and must hold no NaN or Inf. A single is widened to double. # name: # type: sq_string # elements: 1 # length: 25 Cost of Misclassification # name: # type: sq_string # elements: 1 # length: 50 ClassificationPartitionedModel.CrossValidatedModel # name: # type: sq_string # elements: 1 # length: 365 ClassificationPartitionedModel: property CrossValidatedModel Cross-validated model class A character vector holding the short name of the learner that was cross validated, as MATLAB reports it: 'Discriminant', 'GAM', 'KNN', 'NeuralNetwork' or 'SVM'. It is not the class name of that learner, and the regression side uses the same names. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Cross-validated model class # name: # type: sq_string # elements: 1 # length: 36 ClassificationPartitionedModel.KFold # name: # type: sq_string # elements: 1 # length: 180 ClassificationPartitionedModel: property KFold Number of cross-validated folds A positive integer value specifying the number of cross-validated folds. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Number of cross-validated folds # name: # type: sq_string # elements: 1 # length: 46 ClassificationPartitionedModel.ModelParameters # name: # type: sq_string # elements: 1 # length: 1074 ClassificationPartitionedModel: property ModelParameters Model parameters A structure holding the parameters the folds were fitted with, carried through from the learner that was cross validated, beside NLearn, the number of folds, and the Version, Method and Type tags of this class. The learner's own tags are replaced rather than kept, so a cross-validated SVM reports Method as 'PartitionedModel' and not 'SVM'. Deviation from MATLAB. MATLAB reports the parameter record of the cross-validation ensemble here rather than of the learner, so it says nothing at all about how the folds were fitted: of its eighteen fields only the fold count, its partitioner and a fit template carry anything, and the rest are boosting settings left inert. Nor can the parameters be reached through the folds, a compact model carrying none in MATLAB. This class reports the fit instead, which is strictly more than MATLAB offers, and everything MATLAB's record does carry is published here as the KFold, Partition, X, Y, W and CrossValidatedModel properties. This property is read-only. # name: # type: sq_string # elements: 1 # length: 16 Model parameters # name: # type: sq_string # elements: 1 # length: 46 ClassificationPartitionedModel.NumObservations # name: # type: sq_string # elements: 1 # length: 240 ClassificationPartitionedModel: property NumObservations Number of observations A positive integer value specifying the number of observations in the training dataset used for training the cross-validated model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 48 ClassificationPartitionedModel.NumTrainedPerFold # name: # type: sq_string # elements: 1 # length: 725 ClassificationPartitionedModel: property NumTrainedPerFold How many trees each fold fitted A scalar structure with fields PredictorTrees and InteractionTrees, each a row with one entry per fold, for a generalized additive model backing, and empty for every other. It reports what each fold actually fitted, which the budget in ModelParameters does not: a phase stops early when it can no longer improve the fit, and the folds need not stop at the same place. MATLAB carries this on its per-learner partitioned GAM classes, which this package deliberately does not have (see crossval), so like IsStandardDeviationFit it is declared here for every backing and left empty where it does not apply. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 How many trees each fold fitted # name: # type: sq_string # elements: 1 # length: 40 ClassificationPartitionedModel.Partition # name: # type: sq_string # elements: 1 # length: 306 ClassificationPartitionedModel: property Partition Partition configuration A cvpartition object specifying the partition configuration used for cross-validation. This field stores the cvpartition instance that describes how the data was split into training and validation sets. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Partition configuration # name: # type: sq_string # elements: 1 # length: 45 ClassificationPartitionedModel.PredictorNames # name: # type: sq_string # elements: 1 # length: 269 ClassificationPartitionedModel: property PredictorNames Names of predictor variables A cell array of character vectors specifying the names of the predictor variables. The names are in the order in which they appear in the training dataset. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Names of predictor variables # name: # type: sq_string # elements: 1 # length: 36 ClassificationPartitionedModel.Prior # name: # type: sq_string # elements: 1 # length: 665 ClassificationPartitionedModel: property Prior Prior probability for each class A numeric vector specifying the prior probabilities for each class. The order of the elements in Prior corresponds to the order of the classes in ClassNames. It may be assigned only on a cross-validated ClassificationDiscriminant or ClassificationNaiveBayes, the two learners that score from the priors they are given rather than consuming them while they fit: the discriminant re-derives its coefficients from them and the naive Bayes weights its class densities by them. Every other learner cannot revisit them afterwards. Assigning it rebuilds the priors on every fold in Trained. # name: # type: sq_string # elements: 1 # length: 32 Prior probability for each class # name: # type: sq_string # elements: 1 # length: 43 ClassificationPartitionedModel.ResponseName # name: # type: sq_string # elements: 1 # length: 172 ClassificationPartitionedModel: property ResponseName Response variable name A character vector specifying the name of the response variable Y. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Response variable name # name: # type: sq_string # elements: 1 # length: 45 ClassificationPartitionedModel.ScoreTransform # name: # type: sq_string # elements: 1 # length: 334 ClassificationPartitionedModel: property ScoreTransform Transformation function for classification scores Specified as a function handle for transforming the classification scores. fitSVMPosterior stores here the transformation from scores to posterior probabilities it fits for an SVM, as ClassificationSVM.ScoreTransform describes. # name: # type: sq_string # elements: 1 # length: 49 Transformation function for classification scores # name: # type: sq_string # elements: 1 # length: 38 ClassificationPartitionedModel.Trained # name: # type: sq_string # elements: 1 # length: 300 ClassificationPartitionedModel: property Trained Models trained on each fold A cell array of models trained on each fold. Each cell contains a model trained on the minus-one fold of the data (all but one fold used for training and the remaining fold used for validation). This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Models trained on each fold # name: # type: sq_string # elements: 1 # length: 32 ClassificationPartitionedModel.W # name: # type: sq_string # elements: 1 # length: 201 ClassificationPartitionedModel: property W Observation weights A numeric column vector with one entry per observation, carried over from the model that was cross validated. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 32 ClassificationPartitionedModel.X # name: # type: sq_string # elements: 1 # length: 436 ClassificationPartitionedModel: property X Predictor data A numeric matrix containing the unstandardized predictor data. Each column of X represents one predictor (variable), and each row represents one observation. This property is read-only. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 32 ClassificationPartitionedModel.Y # name: # type: sq_string # elements: 1 # length: 320 ClassificationPartitionedModel: property Y Class labels Specified as a logical or numeric column vector, or as a character array or a cell array of character vectors with the same number of rows as the predictor data. Each row in Y is the observed class label for the corresponding row in X. This property is read-only. # name: # type: sq_string # elements: 1 # length: 12 Class labels # name: # type: sq_string # elements: 1 # length: 40 ClassificationPartitionedModel.kfoldEdge # name: # type: sq_string # elements: 1 # length: 874 ClassificationPartitionedModel: e = kfoldEdge (obj) ClassificationPartitionedModel: e = kfoldEdge (..., name, value) Classification edge of the cross-validated observations. e = kfoldEdge (obj) returns the mean of the classification margins over every cross-validated observation, which is the mean of kfoldMargin (obj). obj must be a ClassificationPartitionedModel object. e = kfoldEdge (..., name, value) accepts the following Name-Value pairs. Name Value 'Mode' 'average', the default, which returns one number over the observations of every fold asked for, or 'individual', which returns one number per fold. 'Folds' A vector of fold indices to restrict the edge to. It defaults to every fold. The observations of a selection are weighted uniformly and normalized over that selection, so a subset of folds is an average rather than a sum, exactly as kfoldLoss does. # name: # type: sq_string # elements: 1 # length: 56 Classification edge of the cross-validated observations. # name: # type: sq_string # elements: 1 # length: 40 ClassificationPartitionedModel.kfoldLoss # name: # type: sq_string # elements: 1 # length: 1068 ClassificationPartitionedModel: L = kfoldLoss (obj) ClassificationPartitionedModel: L = kfoldLoss (..., name, value) Compute the cross-validated classification loss. L = kfoldLoss (obj) returns the fraction of observations the folds misclassify, each answered for by the fold's model that did not see it, which is what kfoldPredict returns. obj must be a ClassificationPartitionedModel object. L = kfoldLoss (..., name, value) accepts the following Name-Value pairs. Name Value 'LossFun' 'classiferror', the default, 'classifcost', 'mincost', or a function handle called as lossfun (C, S, W, Cost), where C is a logical matrix with one true per row marking the true class, S the scores, W the weights and Cost the misclassification cost. 'Mode' 'average', the default, which returns one number over the observations of every fold asked for, or 'individual', which returns one number per fold. 'Folds' A vector of fold indices to restrict the loss to. It defaults to every fold. See also: ClassificationPartitionedModel, ClassificationPartitionedModel.kfoldPredict # name: # type: sq_string # elements: 1 # length: 48 Compute the cross-validated classification loss. # name: # type: sq_string # elements: 1 # length: 42 ClassificationPartitionedModel.kfoldMargin # name: # type: sq_string # elements: 1 # length: 705 ClassificationPartitionedModel: m = kfoldMargin (obj) Classification margins of the cross-validated observations. m = kfoldMargin (obj) returns an Nx1 vector holding, for every observation, the score its own fold's model gave the true class less the largest score that model gave any other class. A larger margin is a more confident correct answer and a negative one is a misclassification. Every observation is scored by the fold that held it out, so no model answers for a row it was trained on. obj must be a ClassificationPartitionedModel object. Where the fold that held an observation out produced no score for it, the margin is NaN. This method takes no optional arguments, as MATLAB's does not. # name: # type: sq_string # elements: 1 # length: 59 Classification margins of the cross-validated observations. # name: # type: sq_string # elements: 1 # length: 43 ClassificationPartitionedModel.kfoldPredict # name: # type: sq_string # elements: 1 # length: 2132 ClassificationPartitionedModel: label = kfoldPredict (this) ClassificationPartitionedModel: [label, score, cost] = kfoldPredict (this) Predict responses for observations not used for training in a cross-validated classification model. [label, Score, Cost] = kfoldPredict (this) returns the predicted class labels, classification scores, and classification costs for the data used to train the cross-validated model this. this is a ClassificationPartitionedModel object. The function predicts the response for each observation that was held out during training in the cross-validation process. An observation that no fold held out is not predicted at all: its scores and costs are NaN and its label is missing, an empty character vector for a cell array of strings and NaN for a numeric response. Under a 'Holdout' partition that is every observation outside the holdout set. This differs from MATLAB, which reports NaN scores for those rows as we do but labels every one of them with the first class, whatever their response: that label is the least-cost class of a row of NaN costs rather than a prediction any model made, and naming a class for an observation nothing scored would be wrong. A logical response has no missing value to give, so those rows stay false. Output Description label Predicted class labels, returned as a vector or cell array. The type of label matches the type of Y in the original training data. Each element of label corresponds to the predicted class label for the corresponding row in X. Score Classification scores, returned as a numeric matrix. Each row of Score corresponds to an observation, and each column corresponds to a class. The value in row i and column j is the classification score for class j for observation i. Cost Classification costs, returned as a numeric matrix. Each row of Cost corresponds to an observation, and each column corresponds to a class. The value in row i and column j is the classification cost for class j for observation i. This output is optional and only returned if requested. See also: ClassificationKNN, ClassificationSVM, ClassificationPartitionedModel # name: # type: sq_string # elements: 1 # length: 99 Predict responses for observations not used for training in a cross-validated classification model. # name: # type: sq_string # elements: 1 # length: 39 ClassificationPartitionedModel.kfoldfun # name: # type: sq_string # elements: 1 # length: 899 ClassificationPartitionedModel: vals = kfoldfun (obj, fun) Apply a function to each fold of a cross-validated model. vals = kfoldfun (obj, fun) calls fun once per fold and returns a KxM numeric matrix whose row k is what fun returned for fold k. fun is a function handle taking seven inputs and returning a numeric vector of the same length every time it is called: testvals = fun (M, Xtrain, Ytrain, ... Wtrain, Xtest, Ytest, Wtest) M is the model the fold was fitted with, taken from obj.Trained{k}; Xtrain, Ytrain and Wtrain are the predictors, response and weights of the observations that fold was trained on, and Xtest, Ytest and Wtest those of the observations it held out. See also: ClassificationPartitionedModel, ClassificationPartitionedModel.kfoldPredict, ClassificationPartitionedModel.kfoldLoss, ClassificationPartitionedModel.kfoldMargin, ClassificationPartitionedModel.kfoldEdge # name: # type: sq_string # elements: 1 # length: 57 Apply a function to each fold of a cross-validated model. # name: # type: string # elements: 1 # length: 17 ClassificationSVM # name: # type: sq_string # elements: 1 # length: 708 statistics: ClassificationSVM Support Vector Machine classification The ClassificationSVM class implements a Support Vector Machine classifier object for one-class or two-class problems, which can predict responses for new data using the predict method. Support Vector Machine classification is a supervised learning method used for classification tasks. It works by finding the optimal hyperplane that separates classes in the feature space with the maximum margin. For non-linearly separable data, it uses kernel functions to map data to a higher-dimensional space where separation is possible. Create a ClassificationSVM object by using the fitcsvm function or the class constructor. See also: fitcsvm # name: # type: sq_string # elements: 1 # length: 37 Support Vector Machine classification # name: # type: sq_string # elements: 1 # length: 23 ClassificationSVM.Alpha # name: # type: sq_string # elements: 1 # length: 459 ClassificationSVM: property Alpha Trained classifier coefficients The coefficients of the trained SVM classifier specified as an sx1 numeric vector, where s is the number of support vectors equal to sum (obj.IsSupportVector). They are the magnitudes of the dual coefficients and are never negative; the class each belongs to is given by the corresponding entry of SupportVectorLabels. Alpha is populated for every kernel function. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Trained classifier coefficients # name: # type: sq_string # elements: 1 # length: 22 ClassificationSVM.Beta # name: # type: sq_string # elements: 1 # length: 572 ClassificationSVM: property Beta Linear predictor coefficients The linear predictor coefficients specified as a px1 numeric vector, where p is the number of predictors. Beta is the primal representation of the fitted hyperplane and exists only when the SVM classifier was trained with a 'linear' kernel function; for any other kernel there is no such representation and Beta is empty. It equals (obj.SupportVectors / s)' * (obj.Alpha .* obj.SupportVectorLabels), where s is the kernel scale, and a score is (x / s) * Beta + Bias, as in MATLAB. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Linear predictor coefficients # name: # type: sq_string # elements: 1 # length: 22 ClassificationSVM.Bias # name: # type: sq_string # elements: 1 # length: 108 ClassificationSVM: property Bias Bias term The bias term specified as a scalar. This property is read-only. # name: # type: sq_string # elements: 1 # length: 9 Bias term # name: # type: sq_string # elements: 1 # length: 26 ClassificationSVM.BinEdges # name: # type: sq_string # elements: 1 # length: 352 ClassificationSVM: property BinEdges Bin edges of the predictors A cell array with one entry per predictor, holding that predictor's bin edges where the learner discretized it before fitting. It is empty here and stays empty: this learner fits the predictors as they are, and MATLAB's reports an empty cell for it as well. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Bin edges of the predictors # name: # type: sq_string # elements: 1 # length: 32 ClassificationSVM.BoxConstraints # name: # type: sq_string # elements: 1 # length: 396 ClassificationSVM: property BoxConstraints Box constraints A numeric column vector with one entry per observation, holding the box constraint the fit applied to it. It is BoxConstraint for every observation unless Prior or Cost reweighted the classes, in which case each class is scaled by the weight it carried into the fit, normalized so the weights average to one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Box constraints # name: # type: sq_string # elements: 1 # length: 39 ClassificationSVM.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 228 ClassificationSVM: property CategoricalPredictors Indices of the categorical predictors A numeric vector of column indices into X naming the predictors treated as categorical, and empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 28 ClassificationSVM.ClassNames # name: # type: sq_string # elements: 1 # length: 399 ClassificationSVM: property ClassNames Names of classes in the response variable An array of unique values of the response variable Y, which has the same data types as the data in Y, sorted or in the order given by the 'ClassNames' option. This property is read-only. ClassNames can have any of the following datatypes: Cell array of character vectors Character array Logical vector Numeric vector # name: # type: sq_string # elements: 1 # length: 41 Names of classes in the response variable # name: # type: sq_string # elements: 1 # length: 35 ClassificationSVM.ClassificationSVM # name: # type: sq_string # elements: 1 # length: 5394 ClassificationSVM: obj = ClassificationSVM (X, Y) ClassificationSVM: obj = ClassificationSVM (Tbl, ResponseVarName) ClassificationSVM: obj = ClassificationSVM (Tbl, formula) ClassificationSVM: obj = ClassificationSVM (Tbl, Y) ClassificationSVM: obj = ClassificationSVM (..., name, value) Create a ClassificationSVM class object containing a Support Vector Machine classification model for one-class or two-class problems. obj = ClassificationSVM (X, Y) returns a ClassificationSVM object, with X as the predictor data and Y containing the class labels of observations in X. X must be a NxP numeric matrix of input data where rows correspond to observations and columns correspond to features or variables. X will be used to train the SVM model. Y is Nx1 matrix or cell matrix containing the class labels of corresponding predictor data in X. Y can be either numeric, logical, or cell array of character vectors. It must have same numbers of rows as X. obj = ClassificationSVM (..., name, value) returns a ClassificationSVM object with parameters specified by the following name, value paired input arguments: Name Value 'CategoricalPredictors' The predictors whose values are levels, as indices, as a logical vector with one element per predictor, or as 'all'. Each is dummy coded in its place, one column of zeros and ones per level seen in training, named as in 'x1 == 2' in ExpandedPredictorNames, and the coded columns are not standardized. An observation holding a level the training data did not has no score and takes the class of largest prior, as a row missing a predictor does. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. 'PredictorNames' A cell array of character vectors specifying the names of the predictors. The length of this array must match the number of columns in X. 'ResponseName' A character vector specifying the name of the response variable. 'ClassNames' Names of the classes in the class labels, Y, used for fitting the SVM model. ClassNames are of the same type as the class labels in Y. The model keeps the classes in this order; by default they are sorted. 'ScoreTransform' A user-defined function handle or a character vector specifying one of the following builtin functions specifying the transformation applied to predicted classification scores. Supported values include 'doublelogit', 'invlogit', 'ismax', 'logit', 'none', 'identity', 'sign', 'symmetric', 'symmetricismax', and 'symmetriclogit'. 'Standardize' A logical scalar specifying whether to standardize the predictor variables. Default is false. 'SVMtype' A character vector specifying the type of SVM to use. Supported values are 'c_svc' (C-support vector classification), 'nu_svc' (nu-support vector classification), and 'one_class_svm' (one-class SVM). 'KernelFunction' A character vector specifying the kernel function to use. Supported values are 'linear', 'rbf' or 'gaussian', 'polynomial', and 'sigmoid'. 'PolynomialOrder' A positive integer specifying the order of the polynomial kernel function. Default is 3. 'KernelScale' A positive scalar dividing every predictor before any kernel is applied, as MATLAB does, so that with u and v the divided predictors the kernels are u'v, exp (-||u - v||^2), (1 + u'v)^q and tanh (u'v + c), c being 'KernelOffset'. The default is 1. 'KernelOffset' A non-negative scalar, the constant c of the sigmoid kernel, which MATLAB does not have. MATLAB adds it to every element of the Gram matrix, which leaves the fitted model unchanged, so it changes no other kernel here. The default is 0. 'Weights' A single or double vector of nonnegative observation weights, one per row of X. Each observation's box constraint is n times BoxConstraint times its weight, the weights scaled so that each class carries its prior times the cost of misclassifying it. An empirical prior sums the weights per class, standardization uses weighted means and standard deviations, and a row of zero weight is left out. The model's W keeps the class of the weights, while every computation runs in double, so Prior is double where MATLAB returns single. 'Prior' 'empirical' (default), 'uniform', a vector with one element per class, or a structure with fields ClassNames and ClassProbs. A class whose prior is 0, or whose misclassification costs are all 0, is removed from the data before fitting, with a warning, as in MATLAB. 'Cost' A square matrix of misclassification costs, Cost(i,j) being the cost of classifying into class j an observation of class i, 1 - eye (2) by default; or a structure with fields ClassNames and ClassificationCosts, which names its own order of the classes and is permuted to the model's. 'BoxConstraint' A positive scalar specifying the box constraint parameter. Default is 1. 'Nu' A positive scalar in the range (0,1] specifying the nu parameter for nu-SVM and one-class SVM. Default is 0.5. 'CacheSize' A positive scalar specifying the cache size in MB. Default is 1000. 'Tolerance' A positive scalar specifying the tolerance of termination criterion. Default is 1e-6. 'Shrinking' Either 0 or 1 specifying whether to use the shrinking heuristics. Default is 1. 'OutlierFraction' A positive scalar in the range [0,1) specifying the fraction of outliers for one-class SVM. See also: fitcsvm # name: # type: sq_string # elements: 1 # length: 133 Create a ClassificationSVM class object containing a Support Vector Machine classification model for one-class or two-class problems. # name: # type: sq_string # elements: 1 # length: 22 ClassificationSVM.Cost # name: # type: sq_string # elements: 1 # length: 248 ClassificationSVM: property Cost Cost of misclassification A numeric square matrix, where Cost(i,j) is the cost of classifying an observation of class i as class j. It defaults to zero on the diagonal and one elsewhere. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Cost of misclassification # name: # type: sq_string # elements: 1 # length: 40 ClassificationSVM.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 261 ClassificationSVM: property ExpandedPredictorNames Names of the predictors as the model expanded them A cell array of character vectors. It matches PredictorNames unless a categorical predictor was expanded into indicator variables. This property is read-only. # name: # type: sq_string # elements: 1 # length: 50 Names of the predictors as the model expanded them # name: # type: sq_string # elements: 1 # length: 51 ClassificationSVM.HyperparameterOptimizationResults # name: # type: sq_string # elements: 1 # length: 359 ClassificationSVM: property HyperparameterOptimizationResults Results of the hyperparameter optimization Always empty. It is declared for MATLAB compatibility, where it holds what an automatic search over the hyperparameters found. This class fits the parameters it is given and runs no such search, so there is nothing to report. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Results of the hyperparameter optimization # name: # type: sq_string # elements: 1 # length: 33 ClassificationSVM.IsSupportVector # name: # type: sq_string # elements: 1 # length: 270 ClassificationSVM: property IsSupportVector Support vector indicator An Nx1 logical vector that flags whether a corresponding observation in the predictor data matrix is a Support Vector. N is the number of observations in the training data. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Support vector indicator # name: # type: sq_string # elements: 1 # length: 34 ClassificationSVM.KernelParameters # name: # type: sq_string # elements: 1 # length: 369 ClassificationSVM: property KernelParameters Parameters of the kernel function A structure with fields Function and Scale, and Order for a polynomial kernel. Function names the kernel as MATLAB names it, so a radial basis kernel reports 'gaussian' whichever spelling was given; the kernel the fit was handed is unchanged in ModelParameters. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Parameters of the kernel function # name: # type: sq_string # elements: 1 # length: 33 ClassificationSVM.ModelParameters # name: # type: sq_string # elements: 1 # length: 717 ClassificationSVM: property ModelParameters SVM training parameters A structure holding the parameters the fit was given. The engine is LIBSVM and the record is LIBSVM's, so SVMtype names its formulation and Tolerance and Shrinking are its own controls; the parameters MathWorks reports for its SMO and ISDA solvers are absent, this class running neither. KernelPolynomialOrder belongs to the polynomial kernel alone and is empty under every other, as it is in MATLAB. A structure containing the parameters used to train the SVM model with the following fields: SVMtype, BoxConstraint, CacheSize, KernelScale, KernelOffset, KernelFunction, PolynomialOrder, Nu, Tolerance, and Shrinking. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 SVM training parameters # name: # type: sq_string # elements: 1 # length: 20 ClassificationSVM.Mu # name: # type: sq_string # elements: 1 # length: 438 ClassificationSVM: property Mu Predictor means A numeric vector of the same length as the columns in X containing the means of predictor variables. If the predictor variables have not been standardized, then Mu is empty. This property is read-only. Only observations with no missing predictor enter the estimate, and they are weighted so that each class keeps the share of the observation weight it carried before any row was set aside. # name: # type: sq_string # elements: 1 # length: 15 Predictor means # name: # type: sq_string # elements: 1 # length: 20 ClassificationSVM.Nu # name: # type: sq_string # elements: 1 # length: 195 ClassificationSVM: property Nu Nu parameter for one-class learning A positive scalar, and empty unless the model is a one-class learner, which is what MATLAB reports. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Nu parameter for one-class learning # name: # type: sq_string # elements: 1 # length: 33 ClassificationSVM.NumObservations # name: # type: sq_string # elements: 1 # length: 229 ClassificationSVM: property NumObservations Number of observations A positive integer value specifying the number of observations in the training dataset used for training the ClassificationSVM model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 31 ClassificationSVM.NumPredictors # name: # type: sq_string # elements: 1 # length: 223 ClassificationSVM: property NumPredictors Number of predictors A positive integer value specifying the number of predictors in the training dataset used for training the ClassificationSVM model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of predictors # name: # type: sq_string # elements: 1 # length: 33 ClassificationSVM.OutlierFraction # name: # type: sq_string # elements: 1 # length: 529 ClassificationSVM: property OutlierFraction Expected fraction of outliers in the training data A scalar in [0, 1), zero unless one was asked for. Deviation from MATLAB. The value is reported as it was given, but it reaches the fit by a different route: MATLAB removes outliers iteratively and reports Solver as 'ISDA', where a nonzero fraction here selects LIBSVM's \nu-SVC, in which \nu bounds the fraction of margin errors. The two agree on what the number means and not on how the fit reaches it. This property is read-only. # name: # type: sq_string # elements: 1 # length: 50 Expected fraction of outliers in the training data # name: # type: sq_string # elements: 1 # length: 32 ClassificationSVM.PredictorNames # name: # type: sq_string # elements: 1 # length: 256 ClassificationSVM: property PredictorNames Names of predictor variables A cell array of character vectors specifying the names of the predictor variables. The names are in the order in which they appear in the training dataset. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Names of predictor variables # name: # type: sq_string # elements: 1 # length: 23 ClassificationSVM.Prior # name: # type: sq_string # elements: 1 # length: 581 ClassificationSVM: property Prior Prior probabilities of the classes A numeric row vector with one entry per class, in the order of ClassNames, summing to one. It defaults to the class frequencies of the training data, a row missing some predictors included. This property is read-only. Specified as a row vector with one entry per class, in the order of ClassNames, and rescaled to sum to one. It may be given as 'empirical', 'uniform', a numeric vector, or a structure with ClassNames and ClassProbs fields, which assigns each probability by class name rather than by position. # name: # type: sq_string # elements: 1 # length: 34 Prior probabilities of the classes # name: # type: sq_string # elements: 1 # length: 30 ClassificationSVM.ResponseName # name: # type: sq_string # elements: 1 # length: 159 ClassificationSVM: property ResponseName Response variable name A character vector specifying the name of the response variable Y. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Response variable name # name: # type: sq_string # elements: 1 # length: 26 ClassificationSVM.RowsUsed # name: # type: sq_string # elements: 1 # length: 572 ClassificationSVM: property RowsUsed Rows used for fitting A logical column vector with the same length as the observations in the original predictor data X, true for each row that was used for fitting the ClassificationSVM model. It is empty, [], when every observation was used. A row is dropped when its class is missing, when every predictor is missing, or when its weight is zero. A row missing only some predictors is used: it counts in the prior and in W, while the support vector machine is fitted on the complete rows, as MATLAB does. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Rows used for fitting # name: # type: sq_string # elements: 1 # length: 32 ClassificationSVM.ScoreTransform # name: # type: sq_string # elements: 1 # length: 1244 ClassificationSVM: property ScoreTransform Transformation function for classification scores Specified as a function handle for transforming the classification scores. Add or change the ScoreTransform property using dot notation as in: obj.ScoreTransform = 'function_name' obj.ScoreTransform = @function_handle When specified as a character vector, it can be any of the following built-in functions. Nevertheless, the ScoreTransform property always stores their function handle equivalent. Value Description 'doublelogit' 1 ./ (1 + exp (-2 x x)) 'invlogit' log (x ./ (1 - x)) 'ismax' Sets the score for the class with the largest score to 1, and for all other classes to 0 'logit' 1 ./ (1 + exp (-x)) 'none' x (no transformation) 'identity' x (no transformation) 'sign' -1 for x < 0, 0 for x = 0, 1 for x > 0 'symmetric' 2 x x - 1 'symmetricismax' Sets the score for the class with the largest score to 1, and for all other classes to -1 'symmetriclogit' 2 ./ (1 + exp (-x)) - 1 fitPosterior stores the transformation from scores to posterior probabilities it fits as '@(S)sigmoid(S,A,B)', '@(S)step(S,LowerBound,UpperBound,PositiveClassProbability)' or '@(S)constant(S,K)'. Such a text may also be assigned directly, which MATLAB refuses. # name: # type: sq_string # elements: 1 # length: 49 Transformation function for classification scores # name: # type: sq_string # elements: 1 # length: 23 ClassificationSVM.Sigma # name: # type: sq_string # elements: 1 # length: 472 ClassificationSVM: property Sigma Predictor standard deviations A numeric vector of the same length as the columns in X containing the standard deviations of predictor variables. If the predictor variables have not been standardized, then Sigma is empty. This property is read-only. Only observations with no missing predictor enter the estimate, and they are weighted so that each class keeps the share of the observation weight it carried before any row was set aside. # name: # type: sq_string # elements: 1 # length: 29 Predictor standard deviations # name: # type: sq_string # elements: 1 # length: 37 ClassificationSVM.SupportVectorLabels # name: # type: sq_string # elements: 1 # length: 540 ClassificationSVM: property SupportVectorLabels Support vector class labels The support vector class labels specified as an sx1 numeric vector, where s is the number of support vectors equal to sum (obj.IsSupportVector). A value of +1 in SupportVectorLabels indicates that the corresponding support vector belongs to the positive class (ClassNames{2}). A value of -1 indicates that the corresponding support vector belongs to the negative class (ClassNames{1}). A one-class model labels every support vector +1. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Support vector class labels # name: # type: sq_string # elements: 1 # length: 32 ClassificationSVM.SupportVectors # name: # type: sq_string # elements: 1 # length: 313 ClassificationSVM: property SupportVectors Support vectors The support vectors of the trained SVM classifier specified an sxp numeric matrix, where s is the number of support vectors equal to sum (obj.IsSupportVector), and p is the number of predictor variables in the predictor data. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Support vectors # name: # type: sq_string # elements: 1 # length: 19 ClassificationSVM.W # name: # type: sq_string # elements: 1 # length: 378 ClassificationSVM: property W Observation weights A numeric column vector with one entry per training observation, normalized to sum to one, as MATLAB reports it. It has the class of the 'Weights' given, single or double. This property is read-only. Each class carries its prior, spread over its own observations in proportion to the 'Weights' given, or evenly when none were. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 19 ClassificationSVM.X # name: # type: sq_string # elements: 1 # length: 423 ClassificationSVM: property X Predictor data A numeric matrix containing the unstandardized predictor data. Each column of X represents one predictor (variable), and each row represents one observation. This property is read-only. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 19 ClassificationSVM.Y # name: # type: sq_string # elements: 1 # length: 307 ClassificationSVM: property Y Class labels Specified as a logical or numeric column vector, or as a character array or a cell array of character vectors with the same number of rows as the predictor data. Each row in Y is the observed class label for the corresponding row in X. This property is read-only. # name: # type: sq_string # elements: 1 # length: 12 Class labels # name: # type: sq_string # elements: 1 # length: 25 ClassificationSVM.compact # name: # type: sq_string # elements: 1 # length: 233 ClassificationSVM: CVMdl = compact (obj) Create a CompactClassificationSVM object. CVMdl = compact (obj) creates a compact version of the ClassificationSVM object, obj. See also: fitcsvm, ClassificationSVM, CompactClassificationSVM # name: # type: sq_string # elements: 1 # length: 41 Create a CompactClassificationSVM object. # name: # type: sq_string # elements: 1 # length: 26 ClassificationSVM.crossval # name: # type: sq_string # elements: 1 # length: 1008 ClassificationSVM: CVMdl = crossval (obj) ClassificationSVM: CVMdl = crossval (..., name, value) Cross Validate a Support Vector Machine classification object. CVMdl = crossval (obj) returns a cross-validated model object, CVMdl, from a trained model, obj, using 10-fold cross-validation by default. CVMdl = crossval (obj, name, value) specifies additional name-value pair arguments to customize the cross-validation process. Name Value 'KFold' Specify the number of folds to use in k-fold cross-validation. "KFold", k, where k is an integer greater than 1. 'Holdout' Specify the fraction of the data to hold out for testing. "Holdout", p, where p is a scalar in the range (0,1). 'Leaveout' Specify whether to perform leave-one-out cross-validation. "Leaveout", Value, where Value is 'on' or 'off'. 'CVPartition' Specify a cvpartition object used for cross-validation. "CVPartition", cv, where isa (cv, "cvpartition") = 1. See also: fitcsvm, ClassificationSVM, cvpartition, ClassificationPartitionedModel # name: # type: sq_string # elements: 1 # length: 62 Cross Validate a Support Vector Machine classification object. # name: # type: sq_string # elements: 1 # length: 39 ClassificationSVM.discardSupportVectors # name: # type: sq_string # elements: 1 # length: 618 ClassificationSVM: obj = discardSupportVectors (obj) Discard the support vectors of a linear SVM model. obj = discardSupportVectors (obj) empties Alpha, SupportVectors and SupportVectorLabels, leaving Beta and Bias to decide every prediction. A linear kernel needs nothing else, so the returned model predicts what it predicted before while carrying one vector in place of many. The kernel must be linear. Under any other the support vectors are part of the decision function and cannot be dropped. Discarding twice is not an error and changes nothing. See also: fitcsvm, ClassificationSVM, CompactClassificationSVM # name: # type: sq_string # elements: 1 # length: 50 Discard the support vectors of a linear SVM model. # name: # type: sq_string # elements: 1 # length: 22 ClassificationSVM.edge # name: # type: sq_string # elements: 1 # length: 1005 ClassificationSVM: e = edge (obj, X, Y) ClassificationSVM: e = edge (..., "Weights", w) ClassificationSVM: e = edge (obj, Tbl, ResponseVarName) ClassificationSVM: e = edge (obj, Tbl) Classification edge, the mean of the classification margins. e = edge (obj, X, Y) reduces the vector that margin returns to a single number, the mean margin over the rows of X. It says how far the model puts the true class ahead of its nearest rival on average, so a larger edge is a better model, and unlike a loss it is not bounded above and rewards confidence rather than bare correctness. e = edge (..., "Weights", w) takes the weighted mean instead, with one weight per row of X. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. # name: # type: sq_string # elements: 1 # length: 60 Classification edge, the mean of the classification margins. # name: # type: sq_string # elements: 1 # length: 30 ClassificationSVM.fitPosterior # name: # type: sq_string # elements: 1 # length: 2629 ClassificationSVM: ScoreSVMModel = fitPosterior (obj) ClassificationSVM: ScoreSVMModel = fitPosterior (obj, name, value) ClassificationSVM: [ScoreSVMModel, ScoreTransform] = fitPosterior (...) Fit the transformation from scores to posterior probabilities. ScoreSVMModel = fitPosterior (obj) returns the two-class model obj with the transformation from the score of the second class, obj.ClassNames(2), to its posterior probability stored in its ScoreTransform property, so that predict and resubPredict return posterior probabilities, the two columns summing to 1. The transformation is fitted to scores the model gives observations it was not trained on, by 10-fold cross-validation unless one of the following options says otherwise. Name Value 'KFold' The number of folds, an integer greater than 1. The default is 10. 'Holdout' The fraction of the data held out, a scalar in the range (0,1); the transformation is fitted to the scores of the observations held out. 'Leaveout' 'on' for leave-one-out cross-validation, or 'off' (default). 'CVPartition' A cvpartition object over the observations of obj. Only one of these options may be given. The transformation is one of three: A sigmoid 1 / (1 + exp (A * s + B)) of the score s when the scores of the two classes overlap, with A and B fitted by maximum likelihood, as a logistic regression of the class on the score. Neither the observation weights nor the prior enter the fit, as in MATLAB. A step function when the scores separate the classes: 0 below the largest score of the first class, 1 above the smallest score of the second, and obj.Prior(2) between, with a warning. A constant when one class has prior probability 0, posterior 1 for the other class. A ScoreTransform already set on obj is reset to 'none' first, with a warning. The property holds the transformation as '@(S)sigmoid(S,A,B)', '@(S)step(S,LowerBound,UpperBound,PositiveClassProbability)' or '@(S)constant(S,K)', K being the index of the predicted class. Each parameter is written exactly, in the fewest significant digits that read back to it, where MATLAB shows seven, so a model saved and reloaded predicts as before. Such a text may also be assigned to the property directly, which MATLAB refuses. [ScoreSVMModel, ScoreTransform] = fitPosterior (...) also returns the parameters in a structure whose field Type is 'sigmoid', with fields Slope and Intercept, 'step', with fields LowerBound, UpperBound and PositiveClassProbability, or 'constant', with the field PredictedClass. See also: fitSVMPosterior, CompactClassificationSVM.fitPosterior, ClassificationSVM.predict, fitcsvm, cvpartition # name: # type: sq_string # elements: 1 # length: 62 Fit the transformation from scores to posterior probabilities. # name: # type: sq_string # elements: 1 # length: 22 ClassificationSVM.loss # name: # type: sq_string # elements: 1 # length: 2162 ClassificationSVM: L = loss (obj, X, Y) ClassificationSVM: L = loss (obj, Tbl, ResponseVarName) ClassificationSVM: L = loss (obj, Tbl) ClassificationSVM: L = loss (..., name, value) Compute loss for a trained ClassificationSVM object. L = loss (obj, X, Y) computes the loss, L, using the default loss function 'classiferror'. obj is a ClassificationSVM object trained on X and Y. X must be a NxP numeric matrix of input data where rows correspond to observations and columns correspond to features or variables. Y is Nx1 matrix or cell matrix containing the class labels of corresponding predictor data in X. Y must have same numbers of Rows as X. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. L = loss (..., name, value) allows additional options specified by name-value pairs: Name Value 'LossFun' Specifies the loss function to use. Can be a function handle with four input arguments (C, S, W, Cost) which returns a scalar value or one of: 'binodeviance', 'classifcost', 'classiferror', 'exponential', 'hinge', 'logit','mincost', 'quadratic'. C is a logical matrix of size NxK, where N is the number of observations and K is the number of classes. The element C(i,j) is true if the class label of the i-th observation is equal to the j-th class. S is a numeric matrix of size NxK, where each element represents the classification score for the corresponding class. W is a numeric vector of length N, representing the observation weights. Cost is a KxK matrix representing the misclassification costs. 'Weights' Specifies observation weights, must be a numeric vector of length equal to the number of rows in X. Default is ones (size (X, 1)). loss normalizes the weights so that observation weights in each class sum to the prior probability of that class. When you supply Weights, loss computes the weighted classification loss. See also: ClassificationSVM # name: # type: sq_string # elements: 1 # length: 52 Compute loss for a trained ClassificationSVM object. # name: # type: sq_string # elements: 1 # length: 24 ClassificationSVM.margin # name: # type: sq_string # elements: 1 # length: 1229 ClassificationSVM: m = margin (obj, X, Y) ClassificationSVM: m = margin (obj, Tbl, ResponseVarName) ClassificationSVM: m = margin (obj, Tbl) Classification margins for Support Vector Machine classifier. m = margin (obj, X, Y) returns the classification margins for obj with data X and classification Y. m is a numeric vector of length size (X,1). obj is a ClassificationSVM object trained on X and Y. X must be a NxP numeric matrix of input data where rows correspond to observations and columns correspond to features or variables. Y is Nx1 matrix or cell matrix containing the class labels of corresponding predictor data in X. Y must have same numbers of Rows as X. The classification margin for each observation is the difference between the classification score for the true class and the maximal classification score for the false classes. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: fitcsvm, ClassificationSVM # name: # type: sq_string # elements: 1 # length: 61 Classification margins for Support Vector Machine classifier. # name: # type: sq_string # elements: 1 # length: 25 ClassificationSVM.predict # name: # type: sq_string # elements: 1 # length: 1948 ClassificationSVM: label = predict (obj, XC) ClassificationSVM: [label, score] = predict (obj, XC) ClassificationSVM: [label, score, cost] = predict (obj, XC) Classify new data points into categories using the Support Vector Machine classification model from a ClassificationSVM object. label = predict (obj, XC) returns the vector of labels predicted for the corresponding instances in XC, using the predictor data in obj.X and corresponding labels, obj.Y, stored in the ClassificationSVM model, obj. For one-class SVM model, +1 or -1 is returned. obj must be a ClassificationSVM class object. XC must be an MxP numeric matrix with the same number of features P as the corresponding predictors of the SVM model in obj. [label, score] = predict (obj, XC) also returns score, which contains the decision values for each prediction. A ScoreTransform assigned to obj is applied to them, so score holds whatever that transform returns. Posterior probabilities need a transform fitted to the model, which this package does not compute yet. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. Deviation from MATLAB. cost is the expected cost of each assignment, sum_j P(j) Cost(j,k). An SVM score is a signed distance to the boundary and not a posterior, so the only distribution available is the one concentrated on the predicted class and cost is the row of Cost belonging to it. MATLAB returns the column instead, which is the same matrix read the wrong way and contradicts its own ClassificationKNN, ClassificationDiscriminant and ClassificationNaiveBayes on any asymmetric cost matrix; the two agree wherever Cost is symmetric, the default included. Measured on R2024a. See also: ClassificationSVM, fitcsvm # name: # type: sq_string # elements: 1 # length: 127 Classify new data points into categories using the Support Vector Machine classification model from a ClassificationSVM object. # name: # type: sq_string # elements: 1 # length: 27 ClassificationSVM.resubEdge # name: # type: sq_string # elements: 1 # length: 204 ClassificationSVM: e = resubEdge (obj) Classification edge of the model on its own training data. e = resubEdge (obj) is edge applied to the observations the model was fitted on, the mean of resubMargin. # name: # type: sq_string # elements: 1 # length: 58 Classification edge of the model on its own training data. # name: # type: sq_string # elements: 1 # length: 27 ClassificationSVM.resubLoss # name: # type: sq_string # elements: 1 # length: 1498 ClassificationSVM: L = resubLoss (obj) ClassificationSVM: L = resubLoss (..., name, value) Compute resubstitution loss for a trained ClassificationSVM object. L = resubLoss (obj) computes the resubstitution loss, L, using the default loss function 'classiferror'. obj is a ClassificationSVM object trained on X and Y. L = resubLoss (..., name, value) allows additional options specified by name-value pairs: Name Value 'LossFun' Specifies the loss function to use. Can be a function handle with four input arguments (C, S, W, Cost) which returns a scalar value or one of: 'binodeviance', 'classifcost', 'classiferror', 'exponential', 'hinge', 'logit','mincost', 'quadratic'. C is a logical matrix of size NxK, where N is the number of observations and K is the number of classes. The element C(i,j) is true if the class label of the i-th observation is equal to the j-th class. S is a numeric matrix of size NxK, where each element represents the classification score for the corresponding class. W is a numeric vector of length N, representing the observation weights. Cost is a KxK matrix representing the misclassification costs. 'Weights' Specifies observation weights, must be a numeric vector of length equal to the number of rows in X. Default is ones (size (X, 1)). loss normalizes the weights so that observation weights in each class sum to the prior probability of that class. When you supply Weights, loss computes the weighted classification loss. See also: ClassificationSVM # name: # type: sq_string # elements: 1 # length: 67 Compute resubstitution loss for a trained ClassificationSVM object. # name: # type: sq_string # elements: 1 # length: 29 ClassificationSVM.resubMargin # name: # type: sq_string # elements: 1 # length: 282 ClassificationSVM: m = resubMargin (obj) Classification margins of the model on its own training data. m = resubMargin (obj) is margin applied to the observations the model was fitted on, one number per observation. Being a resubstitution quantity it is optimistic by construction. # name: # type: sq_string # elements: 1 # length: 61 Classification margins of the model on its own training data. # name: # type: sq_string # elements: 1 # length: 30 ClassificationSVM.resubPredict # name: # type: sq_string # elements: 1 # length: 1515 ClassificationSVM: label = resubPredict (obj) ClassificationSVM: [label, score] = resubPredict (obj) ClassificationSVM: [label, score, cost] = resubPredict (obj) Classify the training data using the trained Support Vector Machine classification object. label = resubPredict (obj) returns the vector of labels predicted for the corresponding instances in the training data, using the predictor data in obj.X and corresponding labels, obj.Y, stored in the Support Vector Machine classification model, obj. For one-class model, +1 or -1 is returned. obj must be a ClassificationSVM class object. [label, scores] = resubPredict (obj also returns scores, which contains the decision values for each prediction. A ScoreTransform assigned to obj is applied to them, so scores holds whatever that transform returns. Posterior probabilities need a transform fitted to the model, which this package does not compute yet. Deviation from MATLAB. cost is the expected cost of each assignment, sum_j P(j) Cost(j,k). An SVM score is a signed distance to the boundary and not a posterior, so the only distribution available is the one concentrated on the predicted class and cost is the row of Cost belonging to it. MATLAB returns the column instead, which is the same matrix read the wrong way and contradicts its own ClassificationKNN, ClassificationDiscriminant and ClassificationNaiveBayes on any asymmetric cost matrix; the two agree wherever Cost is symmetric, the default included. Measured on R2024a. See also: fitcsvm # name: # type: sq_string # elements: 1 # length: 90 Classify the training data using the trained Support Vector Machine classification object. # name: # type: sq_string # elements: 1 # length: 27 ClassificationSVM.savemodel # name: # type: sq_string # elements: 1 # length: 458 ClassificationSVM: savemodel (obj, filename) Save a ClassificationSVM object. savemodel (obj, filename) saves each property of a ClassificationSVM object into an Octave binary file, the name of which is specified in filename, along with an extra variable, which defines the type classification object these variables constitute. Use loadmodel in order to load a classification object into Octave's workspace. See also: loadmodel, fitcsvm, ClassificationSVM # name: # type: sq_string # elements: 1 # length: 32 Save a ClassificationSVM object. # name: # type: string # elements: 1 # length: 18 ClassificationTree # name: # type: sq_string # elements: 1 # length: 1895 statistics: ClassificationTree Binary decision tree for classification The ClassificationTree class implements a CART binary decision tree. Growth splits each node on the single predictor and cut point that lower the impurity of the response the most, and stops when a node is pure, too small to be a parent, or has no split leaving enough observations on both sides. The grown tree is then optionally reduced, first by merging the leaves that buy no accuracy and then by cost complexity pruning, which orders the branch nodes by how little risk their subtrees remove and records that order so a subtree of any size can be recovered afterwards with prune. Create a ClassificationTree object by using the fitctree function or the class constructor. The fit is carried out by the compiled engine treetrain and predictions by treepredict, which the regression tree shares. An observation missing the predictor a node cuts on descends to neither child. It is counted in that node and in every node above it, and predict stops it there and gives it that node's answer, so a row is never sent down a branch on evidence it does not carry. Choosing a split, a predictor known for only part of a node is scored on the rows it is known for, by how much the split lowers their impurity, and that is scaled by their share of the node, as C4.5 does, so a predictor known for few rows is not preferred for that alone. A categorical predictor is split into two sets of levels, and an observation whose level a node did not see stops there, as one missing the predictor does. What this class does not do yet. Surrogate splits and the 'twoing' split criterion are not implemented, and an option asking for one of them is refused rather than quietly ignored. The six Surrogate properties are therefore always empty, as they are in MATLAB without surrogate splits. See also: fitctree, treetrain, treepredict # name: # type: sq_string # elements: 1 # length: 39 Binary decision tree for classification # name: # type: sq_string # elements: 1 # length: 27 ClassificationTree.BinEdges # name: # type: sq_string # elements: 1 # length: 267 ClassificationTree: property BinEdges Bin edges of the predictors A cell array with one column vector of bin edges per predictor, empty unless the predictors were binned before fitting. Binning is not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Bin edges of the predictors # name: # type: sq_string # elements: 1 # length: 40 ClassificationTree.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 222 ClassificationTree: property CategoricalPredictors Indices of the categorical predictors A row vector of column indices into X, naming the predictors treated as categorical, empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 35 ClassificationTree.CategoricalSplit # name: # type: sq_string # elements: 1 # length: 271 ClassificationTree: property CategoricalSplit Categorical splits of the tree A Nx2 cell array with one row per categorical split, in node order, holding the levels sent left and the levels sent right. It is empty when no split is categorical. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 Categorical splits of the tree # name: # type: sq_string # elements: 1 # length: 27 ClassificationTree.Children # name: # type: sq_string # elements: 1 # length: 200 ClassificationTree: property Children Child nodes of each node A NumNodesx2 matrix naming the left and the right child of each node. A leaf carries a zero in both columns. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Child nodes of each node # name: # type: sq_string # elements: 1 # length: 29 ClassificationTree.ClassCount # name: # type: sq_string # elements: 1 # length: 250 ClassificationTree: property ClassCount Class counts at each node A NumNodesxK matrix holding how many training observations of each class reached each node. These are counts and take no notice of the observation weights. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Class counts at each node # name: # type: sq_string # elements: 1 # length: 29 ClassificationTree.ClassNames # name: # type: sq_string # elements: 1 # length: 212 ClassificationTree: property ClassNames Names of the classes The distinct class labels, in the type the response was given in, sorted or in the order given by the 'ClassNames' option. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Names of the classes # name: # type: sq_string # elements: 1 # length: 35 ClassificationTree.ClassProbability # name: # type: sq_string # elements: 1 # length: 287 ClassificationTree: property ClassProbability Class probabilities at each node A NumNodesxK matrix holding, for each node, the weight of each class among the observations that reached it, as a proportion of the node's total weight. The root row is the prior. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Class probabilities at each node # name: # type: sq_string # elements: 1 # length: 37 ClassificationTree.ClassificationTree # name: # type: sq_string # elements: 1 # length: 4163 ClassificationTree: obj = ClassificationTree (X, Y) ClassificationTree: obj = ClassificationTree (Tbl, ResponseVarName) ClassificationTree: obj = ClassificationTree (Tbl, formula) ClassificationTree: obj = ClassificationTree (Tbl, Y) ClassificationTree: obj = ClassificationTree (..., name, value) Grow a binary decision tree for classification. obj = ClassificationTree (X, Y) grows a tree on the NxP numeric matrix X of predictor data and the Nx1 response Y, and returns it as a ClassificationTree object. Y may be a numeric or logical vector, a character array, or a cell array of character vectors, and the class names come back in the type it was given in. obj = ClassificationTree (..., name, value) takes the options below. Name Value 'AlgorithmForCategorical' How a node with three or more classes splits a categorical predictor: 'exact', 'pullleft', 'pca' or 'ovabyclass'. By default the exact search is taken when the node holds at most 'MaxNumCategories' levels, and otherwise the best split of 'ovabyclass', 'pca' and 'pullleft', leaving 'ovabyclass' out above four classes. 'CategoricalPredictors' The predictors whose values are levels, as indices, as a logical vector with one element per predictor, or as 'all'. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. 'ClassNames' The classes to fit, of the same type as Y. Observations of any other class are dropped. The model keeps the classes in this order; by default they are sorted. 'Cost' A square matrix with one row and column per class, where element (i,j) is the cost of classifying an observation of class i into class j, or a structure with fields ClassNames and ClassificationCosts. The default is 1 - eye (K). 'MaxNumCategories' A nonnegative integer, the most levels a node with three or more classes searches exactly by default. The default is 10. 'MaxNumSplits' A nonnegative integer, the largest number of branch nodes the tree may take. The default is one less than the number of observations, which is as many as a tree can have. 'MergeLeaves' 'on' (default) or 'off'. When on, a pair of leaves whose parent is no worse than the two of them together is merged back into that parent. 'MinLeafSize' A positive integer, the fewest observations a leaf may hold. The default is 1. A split leaving fewer than this on either side is not taken. 'MinParentSize' A positive integer, the fewest observations a node must hold to be split at all. The default is 10. The value the fit uses is max (MinParentSize, 2 * MinLeafSize), since a smaller node cannot give both children a legal leaf. 'PredictorNames' A cell array of character vectors naming the columns of X. 'Prior' 'empirical' (default), 'uniform', a numeric vector with one element per class, or a structure with fields ClassNames and ClassProbs. A class whose prior is 0, or whose misclassification costs are all 0, is removed from the data before fitting, with a warning, as in MATLAB. 'Prune' 'on' (default) or 'off'. When on, the cost complexity pruning sequence is estimated and reported in PruneList and PruneAlpha. The tree returned is the unpruned one either way; prune takes a subtree out of the sequence. 'PruneCriterion' 'error', the only criterion implemented. 'ResponseName' A character vector naming the response. The default is 'Y'. 'ScoreTransform' A character vector naming a transform to apply to the scores, or a function handle. The default is 'none'. 'SplitCriterion' 'gdi' (default), the Gini diversity index, or 'deviance', the cross entropy. 'Weights' A nonnegative single or double vector with one element per observation. The default is uniform. The model's W keeps the class of the weights, while every computation runs in double, so Prior and the predictions are double where MATLAB returns single. On a node with three or more classes and more than 'MaxNumCategories' levels, the heuristic splits, the choice between equally good partitions and which side each set of levels takes may differ from MATLAB's. See also: fitctree, treetrain, treepredict # name: # type: sq_string # elements: 1 # length: 47 Grow a binary decision tree for classification. # name: # type: sq_string # elements: 1 # length: 23 ClassificationTree.Cost # name: # type: sq_string # elements: 1 # length: 458 ClassificationTree: property Cost Misclassification cost A square numeric matrix with one row and column per class, where Cost(i,j) is the cost of classifying an observation of class i into class j. It defaults to 1 - eye (K) and may be reassigned after fitting. Reassigning it re-derives NodeClass, NodeError and NodeRisk, and changes what predict answers. The shape of the tree does not follow, having been decided by the cost in force when it was grown. # name: # type: sq_string # elements: 1 # length: 22 Misclassification cost # name: # type: sq_string # elements: 1 # length: 32 ClassificationTree.CutCategories # name: # type: sq_string # elements: 1 # length: 257 ClassificationTree: property CutCategories Categories used at each branch A NumNodesx2 cell array holding, for a node that cuts a categorical predictor, the levels sent left and the levels sent right. Every other entry is empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 Categories used at each branch # name: # type: sq_string # elements: 1 # length: 27 ClassificationTree.CutPoint # name: # type: sq_string # elements: 1 # length: 309 ClassificationTree: property CutPoint Cut point of each node A column vector holding, for each node, the value the split compares the predictor against: an observation goes left when its value is less than the cut point and right otherwise. A leaf and a categorical cut carry NaN. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Cut point of each node # name: # type: sq_string # elements: 1 # length: 31 ClassificationTree.CutPredictor # name: # type: sq_string # elements: 1 # length: 263 ClassificationTree: property CutPredictor Name of the predictor each node cuts on A cell array of character vectors with one entry per node, holding the name of the predictor the node splits on and an empty character vector at a leaf. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Name of the predictor each node cuts on # name: # type: sq_string # elements: 1 # length: 36 ClassificationTree.CutPredictorIndex # name: # type: sq_string # elements: 1 # length: 212 ClassificationTree: property CutPredictorIndex Index of the predictor each node cuts on A column vector holding, for each node, the column of X the node splits on, and zero at a leaf. This property is read-only. # name: # type: sq_string # elements: 1 # length: 40 Index of the predictor each node cuts on # name: # type: sq_string # elements: 1 # length: 26 ClassificationTree.CutType # name: # type: sq_string # elements: 1 # length: 295 ClassificationTree: property CutType Type of cut at each node A cell array of character vectors holding 'continuous' at a branch node that cuts a numeric predictor at a point, 'categorical' at one that splits a set of levels, and an empty character vector at a leaf. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Type of cut at each node # name: # type: sq_string # elements: 1 # length: 41 ClassificationTree.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 309 ClassificationTree: property ExpandedPredictorNames Expanded predictor names A cell array of character vectors. It differs from PredictorNames only when a categorical predictor has been expanded into one column per level, which this class does not do, so the two are always equal. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Expanded predictor names # name: # type: sq_string # elements: 1 # length: 52 ClassificationTree.HyperparameterOptimizationResults # name: # type: sq_string # elements: 1 # length: 205 ClassificationTree: property HyperparameterOptimizationResults Results of a hyperparameter optimization Hyperparameter optimization is not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 40 Results of a hyperparameter optimization # name: # type: sq_string # elements: 1 # length: 31 ClassificationTree.IsBranchNode # name: # type: sq_string # elements: 1 # length: 189 ClassificationTree: property IsBranchNode Which nodes are branch nodes A logical column vector, true for each node that carries a split and false for each leaf. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Which nodes are branch nodes # name: # type: sq_string # elements: 1 # length: 34 ClassificationTree.ModelParameters # name: # type: sq_string # elements: 1 # length: 469 ClassificationTree: property ModelParameters Parameters the fit was run with A structure recording the options the tree was grown under: SplitCriterion, MinParent, MinLeaf, MaxSplits, NVarToSample, MergeLeaves, Prune, PruneCriterion, QEToler, NSurrogate, MaxCat, AlgCat, PredictorSelection, Method and Type. MinParent is the value the fit used, which is max (MinParentSize, 2 * MinLeafSize) and so may exceed the 'MinParentSize' asked for. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Parameters the fit was run with # name: # type: sq_string # elements: 1 # length: 28 ClassificationTree.NodeClass # name: # type: sq_string # elements: 1 # length: 353 ClassificationTree: property NodeClass Class assigned to each node A cell array of character vectors naming, for each node, the class of least expected misclassification cost given the node's class probabilities. Under the default cost that is simply the most probable class, with the first of the class names kept on a tie. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Class assigned to each node # name: # type: sq_string # elements: 1 # length: 28 ClassificationTree.NodeError # name: # type: sq_string # elements: 1 # length: 333 ClassificationTree: property NodeError Misclassification cost of each node A column vector holding, for each node, the expected misclassification cost of the class the node is assigned. Under the default cost that is the probability that the node's class is wrong, one less the largest class probability. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Misclassification cost of each node # name: # type: sq_string # elements: 1 # length: 34 ClassificationTree.NodeProbability # name: # type: sq_string # elements: 1 # length: 253 ClassificationTree: property NodeProbability Probability of reaching each node A column vector holding, for each node, the total weight of the observations that reached it, the weights being those in W. The root carries one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Probability of reaching each node # name: # type: sq_string # elements: 1 # length: 27 ClassificationTree.NodeRisk # name: # type: sq_string # elements: 1 # length: 485 ClassificationTree: property NodeRisk Risk of each node A column vector holding, for each node, the impurity of the node weighted by the probability of reaching it, measured by whichever of 'gdi' and 'deviance' the tree was grown under. A non-default Cost enters here rather than through the class probabilities: the weights are scaled class by class by the total cost of misclassifying that class, and the impurity is measured on the scaled distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 17 Risk of each node # name: # type: sq_string # elements: 1 # length: 27 ClassificationTree.NodeSize # name: # type: sq_string # elements: 1 # length: 331 ClassificationTree: property NodeSize Number of observations at each node A column vector holding how many training observations reached each node. A row missing the predictor its node cuts on is counted at that node and at none below it, so a parent's size is not in general the sum of its children's. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Number of observations at each node # name: # type: sq_string # elements: 1 # length: 27 ClassificationTree.NumNodes # name: # type: sq_string # elements: 1 # length: 295 ClassificationTree: property NumNodes Number of nodes in the tree A positive integer, the number of nodes the tree holds, branch nodes and leaves together. Nodes are numbered as they are created, so a parent always carries a lower number than either of its children. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Number of nodes in the tree # name: # type: sq_string # elements: 1 # length: 34 ClassificationTree.NumObservations # name: # type: sq_string # elements: 1 # length: 273 ClassificationTree: property NumObservations Number of observations A positive integer, the number of observations the model was fitted on. It counts the rows kept, so it is smaller than the number of rows given whenever a response was missing. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 25 ClassificationTree.Parent # name: # type: sq_string # elements: 1 # length: 157 ClassificationTree: property Parent Parent of each node A column vector naming the parent of each node. The root carries a zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Parent of each node # name: # type: sq_string # elements: 1 # length: 33 ClassificationTree.PredictorNames # name: # type: sq_string # elements: 1 # length: 170 ClassificationTree: property PredictorNames Names of the predictor variables A cell array of character vectors with one name per column of X. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Names of the predictor variables # name: # type: sq_string # elements: 1 # length: 24 ClassificationTree.Prior # name: # type: sq_string # elements: 1 # length: 502 ClassificationTree: property Prior Prior probability of each class A numeric row vector with one element per class, summing to one. It defaults to the weight each class carries in the training data, and may be reassigned after fitting. Reassigning it re-derives W and every node statistic that depends on the class weights, so ClassProbability, NodeProbability, NodeClass, NodeError and NodeRisk all follow. The shape of the tree does not, having been decided by the prior in force when it was grown. # name: # type: sq_string # elements: 1 # length: 31 Prior probability of each class # name: # type: sq_string # elements: 1 # length: 29 ClassificationTree.PruneAlpha # name: # type: sq_string # elements: 1 # length: 365 ClassificationTree: property PruneAlpha Cost complexity parameter of each pruning level A column vector with one element per level of the pruning sequence, the first of which is zero and stands for the unpruned tree. Level L is the smallest subtree that is optimal for every complexity parameter from PruneAlpha(L+1) up to the next one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 47 Cost complexity parameter of each pruning level # name: # type: sq_string # elements: 1 # length: 28 ClassificationTree.PruneList # name: # type: sq_string # elements: 1 # length: 437 ClassificationTree: property PruneList Pruning level of each node A column vector holding, for each branch node, the level of the cost complexity sequence at which it stops being a branch node, and zero at a leaf. Pruning the tree to level L turns every node whose level is between one and L into a leaf. It is empty when neither 'Prune' nor 'MergeLeaves' was asked for, since no sequence was then estimated. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Pruning level of each node # name: # type: sq_string # elements: 1 # length: 31 ClassificationTree.ResponseName # name: # type: sq_string # elements: 1 # length: 140 ClassificationTree: property ResponseName Name of the response variable A character vector naming the response. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 27 ClassificationTree.RowsUsed # name: # type: sq_string # elements: 1 # length: 464 ClassificationTree: property RowsUsed Rows used for fitting A logical column vector with one element per row of the predictor data as it was given, true for each row used for fitting. It is empty, [], when every row was used, so a non-empty value means that rows were dropped. A row is dropped when its response is missing, when every predictor is missing, or when its weight is zero; a row missing only some of its predictors is kept. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Rows used for fitting # name: # type: sq_string # elements: 1 # length: 33 ClassificationTree.ScoreTransform # name: # type: sq_string # elements: 1 # length: 226 ClassificationTree: property ScoreTransform Transform applied to the scores A character vector naming the function predict applies to the class probabilities before returning them, or a function handle. The default is 'none'. # name: # type: sq_string # elements: 1 # length: 31 Transform applied to the scores # name: # type: sq_string # elements: 1 # length: 41 ClassificationTree.SurrogateCutCategories # name: # type: sq_string # elements: 1 # length: 178 ClassificationTree: property SurrogateCutCategories Categories of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Categories of the surrogate splits # name: # type: sq_string # elements: 1 # length: 35 ClassificationTree.SurrogateCutFlip # name: # type: sq_string # elements: 1 # length: 177 ClassificationTree: property SurrogateCutFlip Cut assignments of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Cut assignments of the surrogate splits # name: # type: sq_string # elements: 1 # length: 36 ClassificationTree.SurrogateCutPoint # name: # type: sq_string # elements: 1 # length: 173 ClassificationTree: property SurrogateCutPoint Cut points of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Cut points of the surrogate splits # name: # type: sq_string # elements: 1 # length: 40 ClassificationTree.SurrogateCutPredictor # name: # type: sq_string # elements: 1 # length: 177 ClassificationTree: property SurrogateCutPredictor Predictors of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Predictors of the surrogate splits # name: # type: sq_string # elements: 1 # length: 35 ClassificationTree.SurrogateCutType # name: # type: sq_string # elements: 1 # length: 167 ClassificationTree: property SurrogateCutType Types of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Types of the surrogate splits # name: # type: sq_string # elements: 1 # length: 48 ClassificationTree.SurrogatePredictorAssociation # name: # type: sq_string # elements: 1 # length: 209 ClassificationTree: property SurrogatePredictorAssociation Predictive measures of association of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 58 Predictive measures of association of the surrogate splits # name: # type: sq_string # elements: 1 # length: 20 ClassificationTree.W # name: # type: sq_string # elements: 1 # length: 367 ClassificationTree: property W Observation weights A numeric column vector of the weights the fit used, one per retained observation. They are the weights given, scaled so that the observations of each class sum to that class's prior, and so that all of them together sum to one. It has the class of the 'Weights' given, single or double. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 20 ClassificationTree.X # name: # type: sq_string # elements: 1 # length: 394 ClassificationTree: property X Predictor data A numeric matrix holding the predictor data the model was fitted on. Each column is one predictor and each row one observation. This property is read-only. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 20 ClassificationTree.Y # name: # type: sq_string # elements: 1 # length: 245 ClassificationTree: property Y Class labels A logical or numeric column vector, a character array, or a cell array of character vectors with one row per row of X, holding the observed class label of each observation. This property is read-only. # name: # type: sq_string # elements: 1 # length: 12 Class labels # name: # type: sq_string # elements: 1 # length: 26 ClassificationTree.compact # name: # type: sq_string # elements: 1 # length: 379 ClassificationTree: CMdl = compact (obj) Drop the training data from a trained model. CMdl = compact (obj) returns a CompactClassificationTree object carrying the tree and everything predict needs, but not the observations the model was fitted on. It classifies new data identically and is far smaller to keep or to ship. See also: CompactClassificationTree, ClassificationTree # name: # type: sq_string # elements: 1 # length: 44 Drop the training data from a trained model. # name: # type: sq_string # elements: 1 # length: 27 ClassificationTree.crossval # name: # type: sq_string # elements: 1 # length: 1024 ClassificationTree: CVMdl = crossval (obj) ClassificationTree: CVMdl = crossval (..., name, value) Cross-validate a trained decision tree. CVMdl = crossval (obj) partitions the training data into ten folds, or into as many folds as there are observations when there are fewer than ten, grows a tree on the training part of each and returns them as a ClassificationPartitionedModel. CVMdl = crossval (..., name, value) takes one of the following, and one only. Name Value 'KFold' An integer greater than 1, the number of folds. 'Holdout' A value between 0 and 1, the fraction of the data held out for testing, which gives a single fold. 'Leaveout' 'on' or 'off', one fold per observation. 'CVPartition' A cvpartition object. Every fold is grown with the parent's class names, prior, cost and observation weights rather than being left to re-derive them from its own rows, so a fold reports the prior of the whole data and not its own frequencies. See also: ClassificationPartitionedModel, ClassificationTree, cvpartition # name: # type: sq_string # elements: 1 # length: 39 Cross-validate a trained decision tree. # name: # type: sq_string # elements: 1 # length: 25 ClassificationTree.cvloss # name: # type: sq_string # elements: 1 # length: 2072 ClassificationTree: E = cvloss (obj) ClassificationTree: [E, SE, Nleaf, BestLevel] = cvloss (obj) ClassificationTree: [...] = cvloss (..., name, value) Cross-validated loss of a tree and of its subtrees. E = cvloss (obj) partitions the training data into ten stratified folds, grows a tree on the training part of each, and returns the loss of the held-out part. [E, SE, Nleaf, BestLevel] = cvloss (...) also returns SE, the standard error of E over the folds, Nleaf, the number of leaves each subtree holds, and BestLevel, the pruning level chosen by 'TreeSize'. Each has one element per subtree asked for. [...] = cvloss (..., name, value) takes the options below. Name Value 'SubTrees' A vector of pruning levels in ascending order, or 'all' for every level of the sequence. The default is 0, the unpruned tree. 'TreeSize' 'se' (default), the smallest subtree whose loss is within one standard error of the smallest loss, or 'min', the smallest subtree of least loss. 'KFold' An integer greater than 1, the number of folds. The default is 10. A value above the number of observations is reduced to it. A fold's tree is pruned to the level its own sequence gives for the geometric mean of the parent's two neighbouring complexity parameters, which is the classical way a fold is matched to a subtree of the whole tree. The last level takes every fold's tree back to its root. Every fold is grown with the parent's class names, prior, cost and observation weights, and the loss is weighed by the model's own weights. The standard error is not MATLAB's. This is the standard error of the loss over the folds, which is what the name means and which is zero when every fold answers alike, as MATLAB's is. Its value is not MATLAB's: on the iris tree MATLAB reports 0.019956 where the folds give 0.024343, and no formula over the fold losses, the observation losses or the loss itself reproduces MATLAB's number. E, Nleaf and BestLevel are measured and match. See also: ClassificationTree, ClassificationTree.prune, ClassificationTree.crossval, ClassificationTree.loss # name: # type: sq_string # elements: 1 # length: 51 Cross-validated loss of a tree and of its subtrees. # name: # type: sq_string # elements: 1 # length: 23 ClassificationTree.edge # name: # type: sq_string # elements: 1 # length: 898 ClassificationTree: e = edge (obj, X, Y) ClassificationTree: e = edge (..., 'Weights', w) ClassificationTree: e = edge (obj, Tbl, ResponseVarName) ClassificationTree: e = edge (obj, Tbl) Classification edge on new data. e = edge (obj, X, Y) returns the weighted mean of the margins, a single number summarising how confidently the model classifies the data. The weights are normalized within each class to that class's prior before they are applied. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: ClassificationTree, ClassificationTree.margin, ClassificationTree.loss, ClassificationTree.predict # name: # type: sq_string # elements: 1 # length: 32 Classification edge on new data. # name: # type: sq_string # elements: 1 # length: 23 ClassificationTree.loss # name: # type: sq_string # elements: 1 # length: 1012 ClassificationTree: l = loss (obj, X, Y) ClassificationTree: l = loss (obj, Tbl, ResponseVarName) ClassificationTree: l = loss (obj, Tbl) ClassificationTree: l = loss (..., name, value) Classification loss on new data. l = loss (obj, X, Y) returns the minimum expected misclassification cost. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. l = loss (..., name, value) takes the following options. Name Value 'LossFun' One of 'binodeviance', 'classifcost', 'classiferror', 'exponential', 'hinge', 'logit', 'mincost' (default) or 'quadratic'. 'Weights' A numeric vector of observation weights, one per row of X. See also: ClassificationTree, ClassificationTree.margin, ClassificationTree.edge, ClassificationTree.predict # name: # type: sq_string # elements: 1 # length: 32 Classification loss on new data. # name: # type: sq_string # elements: 1 # length: 25 ClassificationTree.margin # name: # type: sq_string # elements: 1 # length: 915 ClassificationTree: m = margin (obj, X, Y) ClassificationTree: m = margin (obj, Tbl, ResponseVarName) ClassificationTree: m = margin (obj, Tbl) Classification margin on new data. m = margin (obj, X, Y) returns one margin per observation: the score the model gives the observation's true class, less the largest score it gives any other class. A positive margin means the observation is classified correctly, and a larger one means it is classified more confidently. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: ClassificationTree, ClassificationTree.edge, ClassificationTree.loss, ClassificationTree.predict # name: # type: sq_string # elements: 1 # length: 34 Classification margin on new data. # name: # type: sq_string # elements: 1 # length: 36 ClassificationTree.nodeVariableRange # name: # type: sq_string # elements: 1 # length: 485 ClassificationTree: r = nodeVariableRange (obj, node) Range of each predictor at a node. r = nodeVariableRange (obj, node) returns a structure with one field per predictor the path from the root to node cuts on, holding the two-element range of values that reach the node, or for a categorical predictor the levels that reach it. A predictor the path never cuts on is unconstrained and is left out, so the root gives a structure with no fields. See also: ClassificationTree, fitctree # name: # type: sq_string # elements: 1 # length: 34 Range of each predictor at a node. # name: # type: sq_string # elements: 1 # length: 26 ClassificationTree.predict # name: # type: sq_string # elements: 1 # length: 1434 ClassificationTree: label = predict (obj, XC) ClassificationTree: [label, score] = predict (...) ClassificationTree: [label, score, node] = predict (...) ClassificationTree: [label, score, node, cnum] = predict (...) Classify new data with a trained ClassificationTree object. label = predict (obj, XC) sends each row of XC down the tree and returns the class of the node it comes to rest at. XC must have as many columns as the predictor data the model was fitted on. XC may also be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. [label, score] = predict (...) also returns score, an NxK matrix holding the class probabilities of the node each row landed in, after ScoreTransform. [label, score, node] = predict (...) also returns the number of the node each row landed in, and [label, score, node, cnum] = predict (...) the index of the predicted class into ClassNames. The label is the class of least expected misclassification cost, which under the default Cost is the most probable class of the node. A row missing the predictor a node cuts on is stopped at that node and takes its answer, rather than being sent down a branch on evidence the row does not carry. See also: ClassificationTree, fitctree # name: # type: sq_string # elements: 1 # length: 59 Classify new data with a trained ClassificationTree object. # name: # type: sq_string # elements: 1 # length: 38 ClassificationTree.predictorImportance # name: # type: sq_string # elements: 1 # length: 692 ClassificationTree: imp = predictorImportance (obj) Estimate the importance of each predictor. imp = predictorImportance (obj) returns a row vector with one element per predictor, holding the total drop in risk the splits on that predictor bring about, divided by the number of branch nodes. A predictor the tree never splits on scores zero. The drop at a branch node is its NodeRisk less the risk of its two children and less what it holds back, so a predictor that is chosen often, high up, and on nodes it separates well, scores highest. The numbers are comparable between predictors of one tree and not between trees. See also: ClassificationTree, fitctree, ClassificationTree.NodeRisk # name: # type: sq_string # elements: 1 # length: 42 Estimate the importance of each predictor. # name: # type: sq_string # elements: 1 # length: 24 ClassificationTree.prune # name: # type: sq_string # elements: 1 # length: 1127 ClassificationTree: obj2 = prune (obj) ClassificationTree: obj2 = prune (obj, 'Level', L) ClassificationTree: obj2 = prune (obj, 'Alpha', A) ClassificationTree: obj2 = prune (obj, 'Nodes', N) Take a subtree out of the pruning sequence. obj2 = prune (obj) returns the tree unchanged. obj2 = prune (obj, 'Level', L) turns every branch node whose PruneList level is between one and L into a leaf and discards everything below it. Level zero is the tree itself and the largest level is the root alone. A level above the largest prunes to the root and warns. obj2 = prune (obj, 'Alpha', A) prunes to the smallest subtree that is optimal for the cost complexity parameter A, which is the largest level whose PruneAlpha does not exceed it. obj2 = prune (obj, 'Nodes', N) turns the branch nodes named in N into leaves, along with everything below them, and leaves the rest of the tree alone. Pruning renumbers the nodes, so the properties of the returned tree are those of a tree of that shape and not a subset of the original's. See also: ClassificationTree, fitctree, ClassificationTree.PruneList, ClassificationTree.PruneAlpha # name: # type: sq_string # elements: 1 # length: 43 Take a subtree out of the pruning sequence. # name: # type: sq_string # elements: 1 # length: 28 ClassificationTree.resubEdge # name: # type: sq_string # elements: 1 # length: 220 ClassificationTree: e = resubEdge (obj) Classification edge on the training data. e = resubEdge (obj) is edge over the training data, weighed as the fit weighed it. See also: ClassificationTree, ClassificationTree.edge # name: # type: sq_string # elements: 1 # length: 41 Classification edge on the training data. # name: # type: sq_string # elements: 1 # length: 28 ClassificationTree.resubLoss # name: # type: sq_string # elements: 1 # length: 418 ClassificationTree: l = resubLoss (obj) ClassificationTree: l = resubLoss (..., name, value) Classification loss on the training data. l = resubLoss (obj) is loss over the training data, weighed as the fit weighed it, and takes the same 'LossFun' option. Giving 'Weights' weighs the training data some other way instead, which MATLAB refuses rather than honours. See also: ClassificationTree, ClassificationTree.loss # name: # type: sq_string # elements: 1 # length: 41 Classification loss on the training data. # name: # type: sq_string # elements: 1 # length: 30 ClassificationTree.resubMargin # name: # type: sq_string # elements: 1 # length: 196 ClassificationTree: m = resubMargin (obj) Classification margin on the training data. m = resubMargin (obj) is margin (obj, obj.X, obj.Y). See also: ClassificationTree, ClassificationTree.margin # name: # type: sq_string # elements: 1 # length: 43 Classification margin on the training data. # name: # type: sq_string # elements: 1 # length: 31 ClassificationTree.resubPredict # name: # type: sq_string # elements: 1 # length: 309 ClassificationTree: label = resubPredict (obj) ClassificationTree: [label, score, node, cnum] = resubPredict (obj) Classify the training data with the model fitted to it. label = resubPredict (obj) is predict (obj, obj.X), and takes the same outputs. See also: ClassificationTree, ClassificationTree.predict # name: # type: sq_string # elements: 1 # length: 55 Classify the training data with the model fitted to it. # name: # type: sq_string # elements: 1 # length: 28 ClassificationTree.savemodel # name: # type: sq_string # elements: 1 # length: 475 ClassificationTree: savemodel (obj, filename) Save a ClassificationTree model to a file. savemodel (obj, filename) saves each property of a ClassificationTree object into an Octave binary file, the name of which is specified in filename, along with an extra variable, which defines the type of classification object these variables constitute. Use loadmodel in order to load a classification object into Octave's workspace. See also: loadmodel, fitctree, ClassificationTree # name: # type: sq_string # elements: 1 # length: 42 Save a ClassificationTree model to a file. # name: # type: sq_string # elements: 1 # length: 23 ClassificationTree.view # name: # type: sq_string # elements: 1 # length: 394 ClassificationTree: view (obj) Print the tree as text. view (obj) prints one line per node: a branch node names the predictor it cuts on, the cut point, and the node each side leads to, and a leaf names the class it assigns. A branch node's line ends with the class it would assign itself, which is the answer an observation missing that predictor gets. See also: ClassificationTree, fitctree # name: # type: sq_string # elements: 1 # length: 23 Print the tree as text. # name: # type: sq_string # elements: 1 # length: 33 CompactClassificationDiscriminant # name: # type: sq_string # elements: 1 # length: 1655 statistics: CompactClassificationDiscriminant Compact discriminant analysis classification The CompactClassificationDiscriminant class implements a compact version of a linear discriminant analysis classifier object, which can predict responses for new data using the predict method but does not store the training data. A CompactClassificationDiscriminant object is a compact version of a discriminant analysis model, ClassificationDiscriminant. It does not include the training data resulting in a smaller classifier size, which can be used for making predictions from new data, but not for tasks such as cross validation. It can only be created from a ClassificationDiscriminant model by using the compact object method. Create a CompactClassificationDiscriminant object by using the compact method of a ClassificationDiscriminant object. Six discriminant types are available, in two families. The linear family, 'linear', 'diagLinear' and 'pseudoLinear', pools one covariance across the classes and separates them with a hyperplane. The quadratic family, 'quadratic', 'diagQuadratic' and 'pseudoQuadratic', estimates a covariance per class and separates them with a quadric. A 'diag' type keeps only the variances, which is the same model as a Gamma of 1, and a 'pseudo' type inverts a singular covariance rather than refusing it. DiscrimType may be assigned after fitting, but only within its own family: the family is fixed when the model is fitted, because it decides which covariances the fit has to estimate. Assigning it, or Gamma, re-derives Sigma, LogDetSigma and Coeffs without refitting. See also: fitcdiscr, ClassificationDiscriminant # name: # type: sq_string # elements: 1 # length: 44 Compact discriminant analysis classification # name: # type: sq_string # elements: 1 # length: 46 CompactClassificationDiscriminant.BetweenSigma # name: # type: sq_string # elements: 1 # length: 749 CompactClassificationDiscriminant: property BetweenSigma Between-class covariance matrix A P-by-P matrix holding the covariance of the class means about the overall mean, weighted by how many observations each class contributes. With n_k observations in class k, p_k = n_k / n and \bar{\mu} = \sum_k p_k \mu_k, it is BetweenSigma = sum_k n_k (Mu(k,:) - mubar)' * (Mu(k,:) - mubar) / (n * (1 - sum_k p_k^2)) The denominator is the unbiased one for a weighted covariance, so a balanced fit divides by n (K-1) / K. It reads the class sizes, not Prior: assigning a prior leaves it where it was. It is estimated for every discriminant type, the quadratic family included, since it describes the classes rather than the fit. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Between-class covariance matrix # name: # type: sq_string # elements: 1 # length: 55 CompactClassificationDiscriminant.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 244 CompactClassificationDiscriminant: property CategoricalPredictors Indices of the categorical predictors A numeric vector of column indices into X naming the predictors treated as categorical, and empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 44 CompactClassificationDiscriminant.ClassNames # name: # type: sq_string # elements: 1 # length: 358 CompactClassificationDiscriminant: property ClassNames Names of classes in the response variable An array of unique values of the response variable Y, which has the same data types as the data in Y. This property is read-only. ClassNames can have any of the following datatypes: Cell array of character vectors Character array Logical vector Numeric vector # name: # type: sq_string # elements: 1 # length: 41 Names of classes in the response variable # name: # type: sq_string # elements: 1 # length: 40 CompactClassificationDiscriminant.Coeffs # name: # type: sq_string # elements: 1 # length: 836 CompactClassificationDiscriminant: property Coeffs Coefficient matrices A KxK structure containing the coefficient matrices, where K is the number of classes. If the 'FillCoeffs' parameter was set to 'off' in the original ClassificationDiscriminant model, then Coeffs is empty ([]). This property is read-only. Coeffs(i,j) contains the coefficients of the boundary between the classes i and j in the following fields: DiscrimType - A character vector Class1 - ClassNames(i) Class2 - ClassNames(j) Const - A scalar Linear - A vector with length as the number of predictors. Quadratic - The quadratic family only. A PxP matrix, or a 1xP vector for 'diagQuadratic', following the shape of Sigma. The diagonal entries carry the two class names and nothing else. The structure is rebuilt whenever DiscrimType, Gamma or Prior is assigned. # name: # type: sq_string # elements: 1 # length: 20 Coefficient matrices # name: # type: sq_string # elements: 1 # length: 67 CompactClassificationDiscriminant.CompactClassificationDiscriminant # name: # type: sq_string # elements: 1 # length: 480 CompactClassificationDiscriminant: obj = CompactClassificationDiscriminant (Mdl) CompactClassificationDiscriminant: obj = CompactClassificationDiscriminant () Create a CompactClassificationDiscriminant object. Mdl is the ClassificationDiscriminant object to compact. The documented way to reach this constructor is the compact method. Called with no arguments it returns an object with its properties empty, which is how a saved model is rebuilt before its values are filled in. # name: # type: sq_string # elements: 1 # length: 50 Create a CompactClassificationDiscriminant object. # name: # type: sq_string # elements: 1 # length: 38 CompactClassificationDiscriminant.Cost # name: # type: sq_string # elements: 1 # length: 1151 CompactClassificationDiscriminant: property Cost Cost of Misclassification A square matrix specifying the cost of misclassification of a point. Cost(i,j) is the cost of classifying a point into class j if its true class is i (that is, the rows correspond to the true class and the columns correspond to the predicted class). The order of the rows and columns in Cost corresponds to the order of the classes in ClassNames. The number of rows and columns in Cost is the number of unique classes in the response. By default, Cost(i,j) = 1 if i != j, and Cost(i,j) = 0 if i = j. In other words, the cost is 0 for correct classification and 1 for incorrect classification. This property is read-only. A cost may also be given as a struct with the fields ClassNames and ClassificationCosts, which names the order its own matrix is written in. That matrix is permuted into the order of ClassNames above, so a caller need not know which order the classes were sorted into. It must name every class. A cost must be floating point, not sparse, not complex, non-negative and zero down its diagonal, and must hold no NaN or Inf. A single is widened to double. # name: # type: sq_string # elements: 1 # length: 25 Cost of Misclassification # name: # type: sq_string # elements: 1 # length: 39 CompactClassificationDiscriminant.Delta # name: # type: sq_string # elements: 1 # length: 814 CompactClassificationDiscriminant: property Delta Delta threshold for the linear coefficients A nonnegative scalar that eliminates predictors. A per-class linear coefficient is set to zero when it falls below Delta, and the comparison is made on the standardized coefficient, the coefficient times the within-class standard deviation of its predictor. Scaling matters here: a threshold on the raw coefficients would depend on the units each predictor is measured in, so the same model in centimetres and in metres would drop different predictors. DeltaPredictor reports, per predictor, the value at which it drops out of every class at once. It applies to the linear family only, a quadratic discriminant having no linear coefficients to eliminate. Assigning it rebuilds Coeffs and changes what predict answers. # name: # type: sq_string # elements: 1 # length: 43 Delta threshold for the linear coefficients # name: # type: sq_string # elements: 1 # length: 48 CompactClassificationDiscriminant.DeltaPredictor # name: # type: sq_string # elements: 1 # length: 482 CompactClassificationDiscriminant: property DeltaPredictor Minimum Delta at which each predictor drops out A row vector with one entry per predictor, the value of Delta at which that predictor's coefficient is zero for every class and the predictor leaves the model altogether. It is all zeros for the quadratic family, which has no linear coefficients to eliminate. This property is read-only, and it describes the fit rather than the threshold: assigning Delta does not move it. # name: # type: sq_string # elements: 1 # length: 47 Minimum Delta at which each predictor drops out # name: # type: sq_string # elements: 1 # length: 45 CompactClassificationDiscriminant.DiscrimType # name: # type: sq_string # elements: 1 # length: 793 CompactClassificationDiscriminant: property DiscrimType Discriminant type A character vector naming the discriminant model, one of 'linear', 'quadratic', 'diagLinear', 'diagQuadratic', 'pseudoLinear' or 'pseudoQuadratic'. A linear type pools one covariance across the classes; a quadratic type estimates one per class. A 'diag' type keeps only the variances, and a 'pseudo' type inverts a singular covariance instead of refusing it. This property may be assigned, but only within its own family: the three linear types interchange freely and so do the three quadratic ones, while no assignment moves a model between the two. The family is fixed when the model is fitted, because it decides which covariances the fit has to estimate. Assigning re-derives Sigma, LogDetSigma, Gamma and Coeffs. # name: # type: sq_string # elements: 1 # length: 17 Discriminant type # name: # type: sq_string # elements: 1 # length: 56 CompactClassificationDiscriminant.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 277 CompactClassificationDiscriminant: property ExpandedPredictorNames Names of the predictors as the model expanded them A cell array of character vectors. It matches PredictorNames unless a categorical predictor was expanded into indicator variables. This property is read-only. # name: # type: sq_string # elements: 1 # length: 50 Names of the predictors as the model expanded them # name: # type: sq_string # elements: 1 # length: 39 CompactClassificationDiscriminant.Gamma # name: # type: sq_string # elements: 1 # length: 516 CompactClassificationDiscriminant: property Gamma Gamma regularization parameter A scalar from 0 to 1 shrinking the covariance towards its diagonal. Gamma and DiscrimType are one state: a value of 1 is the diagonal type, so assigning it renames DiscrimType to 'diagLinear' or 'diagQuadratic', and assigning a diagonal type sets Gamma to 1. The quadratic family admits 0 and 1 only. A value below MinGamma is refused, since it would leave the covariance singular. Assigning re-derives Sigma, LogDetSigma and Coeffs. # name: # type: sq_string # elements: 1 # length: 30 Gamma regularization parameter # name: # type: sq_string # elements: 1 # length: 45 CompactClassificationDiscriminant.LogDetSigma # name: # type: sq_string # elements: 1 # length: 624 CompactClassificationDiscriminant: property LogDetSigma Logarithm of the determinant of the within-class covariance matrix A scalar for the linear family and a Kx1 vector for the quadratic one, one entry per class. It is computed in correlation space, as the sum of the logarithms of the predictor variances plus the log determinant of the correlation matrix, which is far better conditioned than the covariance when the data are nearly collinear. A predictor with no variance contributes nothing rather than an infinity, and the 'pseudo' types sum only over the directions that carry variance. This property is read-only. # name: # type: sq_string # elements: 1 # length: 66 Logarithm of the determinant of the within-class covariance matrix # name: # type: sq_string # elements: 1 # length: 42 CompactClassificationDiscriminant.MinGamma # name: # type: sq_string # elements: 1 # length: 450 CompactClassificationDiscriminant: property MinGamma Minimum value for the Gamma regularization parameter A scalar from 0 to 1, the least regularization that leaves the correlation matrix invertible. It is 0 when the matrix is already invertible, and positive when the predictors are collinear, in which case a plain 'linear' or 'quadratic' fit is raised to it rather than failing. Assigning a Gamma below it is refused. This property is read-only. # name: # type: sq_string # elements: 1 # length: 52 Minimum value for the Gamma regularization parameter # name: # type: sq_string # elements: 1 # length: 36 CompactClassificationDiscriminant.Mu # name: # type: sq_string # elements: 1 # length: 265 CompactClassificationDiscriminant: property Mu Class means A KxP numeric matrix specifying the mean of the multivariate normal distribution of each corresponding class, where K is the number of classes and P is the number of predictors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 11 Class means # name: # type: sq_string # elements: 1 # length: 47 CompactClassificationDiscriminant.NumPredictors # name: # type: sq_string # elements: 1 # length: 255 CompactClassificationDiscriminant: property NumPredictors Number of predictors A positive integer value specifying the number of predictors in the training dataset used for training the CompactClassificationDiscriminant model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of predictors # name: # type: sq_string # elements: 1 # length: 48 CompactClassificationDiscriminant.PredictorNames # name: # type: sq_string # elements: 1 # length: 272 CompactClassificationDiscriminant: property PredictorNames Names of predictor variables A cell array of character vectors specifying the names of the predictor variables. The names are in the order in which they appear in the training dataset. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Names of predictor variables # name: # type: sq_string # elements: 1 # length: 39 CompactClassificationDiscriminant.Prior # name: # type: sq_string # elements: 1 # length: 564 CompactClassificationDiscriminant: property Prior Prior probability for each class A numeric vector specifying the prior probabilities for each class. The order of the elements in Prior corresponds to the order of the classes in ClassNames. This property is read-only. Specified as a row vector with one entry per class, in the order of ClassNames, and rescaled to sum to one. It may be given as 'empirical', 'uniform', a numeric vector, or a structure with ClassNames and ClassProbs fields, which assigns each probability by class name rather than by position. # name: # type: sq_string # elements: 1 # length: 32 Prior probability for each class # name: # type: sq_string # elements: 1 # length: 46 CompactClassificationDiscriminant.ResponseName # name: # type: sq_string # elements: 1 # length: 175 CompactClassificationDiscriminant: property ResponseName Response variable name A character vector specifying the name of the response variable Y. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Response variable name # name: # type: sq_string # elements: 1 # length: 48 CompactClassificationDiscriminant.ScoreTransform # name: # type: sq_string # elements: 1 # length: 882 CompactClassificationDiscriminant: property ScoreTransform Transformation function for classification scores Specified as a function handle for transforming the classification scores. This property is read-only. When specified as a character vector, it can be any of the following built-in functions. Nevertheless, the ScoreTransform property always stores their function handle equivalent. Value Description 'doublelogit' 1 ./ (1 + exp (-2 x x)) 'invlogit' log (x ./ (1 - x)) 'ismax' Sets the score for the class with the largest score to 1, and for all other classes to 0 'logit' 1 ./ (1 + exp (-x)) 'none' x (no transformation) 'identity' x (no transformation) 'sign' -1 for x < 0, 0 for x = 0, 1 for x > 0 'symmetric' 2 x x - 1 'symmetricismax' Sets the score for the class with the largest score to 1, and for all other classes to -1 'symmetriclogit' 2 ./ (1 + exp (-x)) - 1 # name: # type: sq_string # elements: 1 # length: 49 Transformation function for classification scores # name: # type: sq_string # elements: 1 # length: 39 CompactClassificationDiscriminant.Sigma # name: # type: sq_string # elements: 1 # length: 515 CompactClassificationDiscriminant: property Sigma Within-class covariance A numeric array whose shape follows DiscrimType, with P predictors and K classes: DiscrimType Sigma LogDetSigma 'linear', 'pseudoLinear' PxP scalar 'quadratic', 'pseudoQuadratic' PxPxK Kx1 'diagLinear' 1xP scalar 'diagQuadratic' 1xPxK Kx1 The linear family pools one covariance across the classes and the quadratic family estimates one per class. This property is read-only, but it is re-derived whenever DiscrimType or Gamma is assigned. # name: # type: sq_string # elements: 1 # length: 23 Within-class covariance # name: # type: sq_string # elements: 1 # length: 38 CompactClassificationDiscriminant.edge # name: # type: sq_string # elements: 1 # length: 1069 CompactClassificationDiscriminant: e = edge (obj, X, Y) CompactClassificationDiscriminant: e = edge (..., "Weights", w) CompactClassificationDiscriminant: e = edge (obj, Tbl, ResponseVarName) CompactClassificationDiscriminant: e = edge (obj, Tbl) Classification edge, the mean of the classification margins. e = edge (obj, X, Y) reduces the vector that margin returns to a single number, the mean margin over the rows of X. It says how far the model puts the true class ahead of its nearest rival on average, so a larger edge is a better model, and unlike a loss it is not bounded above and rewards confidence rather than bare correctness. e = edge (..., "Weights", w) takes the weighted mean instead, with one weight per row of X. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. # name: # type: sq_string # elements: 1 # length: 60 Classification edge, the mean of the classification margins. # name: # type: sq_string # elements: 1 # length: 38 CompactClassificationDiscriminant.logp # name: # type: sq_string # elements: 1 # length: 982 CompactClassificationDiscriminant: lp = logp (obj, X) Log unconditional probability density of the observations. lp = logp (obj, X) returns an Nx1 vector holding, for each row of X, the natural logarithm of P(x) = sum_k P(k) P(x|k), the density of the observation summed over the classes with each class weighted by its prior P(k). Each P(x|k) is the multivariate normal density of class k. obj must be a CompactClassificationDiscriminant object. X must be an NxP numeric matrix with one column per predictor of the trained model, or a table holding them. An unusually low value marks an observation the model finds unlikely under every class, which is what makes this an outlier test rather than a classification. A table's variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. # name: # type: sq_string # elements: 1 # length: 58 Log unconditional probability density of the observations. # name: # type: sq_string # elements: 1 # length: 38 CompactClassificationDiscriminant.loss # name: # type: sq_string # elements: 1 # length: 2250 CompactClassificationDiscriminant: L = loss (obj, X, Y) CompactClassificationDiscriminant: L = loss (obj, Tbl, ResponseVarName) CompactClassificationDiscriminant: L = loss (obj, Tbl) CompactClassificationDiscriminant: L = loss (..., name, value) Compute loss for a trained CompactClassificationDiscriminant object. L = loss (obj, X, Y) computes the loss, L, using the default loss function 'mincost'. obj is a CompactClassificationDiscriminant object. X must be a NxP numeric matrix of input data where rows correspond to observations and columns correspond to features or variables. Y is Nx1 matrix or cell matrix containing the class labels of corresponding predictor data in X. Y must have same numbers of rows as X. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. L = loss (..., name, value) allows additional options specified by name-value pairs: Name Value 'LossFun' Specifies the loss function to use. Can be a function handle with four input arguments (C, S, W, Cost) which returns a scalar value or one of: 'binodeviance', 'classifcost', 'classiferror', 'exponential', 'hinge', 'logit','mincost', 'quadratic'. C is a logical matrix of size NxK, where N is the number of observations and K is the number of classes. The element C(i,j) is true if the class label of the i-th observation is equal to the j-th class. S is a numeric matrix of size NxK, where each element represents the classification score for the corresponding class. W is a numeric vector of length N, representing the observation weights. Cost is a KxK matrix representing the misclassification costs. 'Weights' Specifies observation weights, must be a numeric vector of length equal to the number of rows in X. Default is ones (size (X, 1)). loss normalizes the weights so that observation weights in each class sum to the prior probability of that class. When you supply Weights, loss computes the weighted classification loss. See also: CompactClassificationDiscriminant # name: # type: sq_string # elements: 1 # length: 68 Compute loss for a trained CompactClassificationDiscriminant object. # name: # type: sq_string # elements: 1 # length: 39 CompactClassificationDiscriminant.mahal # name: # type: sq_string # elements: 1 # length: 1293 CompactClassificationDiscriminant: M = mahal (obj, X) CompactClassificationDiscriminant: M = mahal (..., 'ClassLabels', labels) Squared Mahalanobis distance to the class means. M = mahal (obj, X) returns an NxK matrix whose element (i,j) is the squared Mahalanobis distance from observation i to the mean of class j, measured against the covariance that class carries: the one shared covariance for a linear discriminant and the class's own for a quadratic one. obj must be a CompactClassificationDiscriminant object. X must be an NxP numeric matrix with one column per predictor of the trained model, or a table holding them. M = mahal (..., 'ClassLabels', labels) returns an Nx1 vector instead, holding for each observation the distance to the mean of the class labels names for it. labels must have one entry per row of X, each of them one of ClassNames. The distance is measured against the covariance the model reports, so a regularized model is measured against its regularized covariance. The prior does not enter it. A table's variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. # name: # type: sq_string # elements: 1 # length: 48 Squared Mahalanobis distance to the class means. # name: # type: sq_string # elements: 1 # length: 40 CompactClassificationDiscriminant.margin # name: # type: sq_string # elements: 1 # length: 1291 CompactClassificationDiscriminant: m = margin (obj, X, Y) CompactClassificationDiscriminant: m = margin (obj, Tbl, ResponseVarName) CompactClassificationDiscriminant: m = margin (obj, Tbl) Classification margins for discriminant analysis classifier. m = margin (obj, X, Y) returns the classification margins for obj with data X and classification Y. m is a numeric vector of length size (X,1). obj is a CompactClassificationDiscriminant object. X must be a NxP numeric matrix of input data where rows correspond to observations and columns correspond to features or variables. Y is Nx1 matrix or cell matrix containing the class labels of corresponding predictor data in X. Y must have same numbers of rows as X. The classification margin for each observation is the difference between the classification score for the true class and the maximal classification score for the false classes. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: fitcdiscr, CompactClassificationDiscriminant # name: # type: sq_string # elements: 1 # length: 60 Classification margins for discriminant analysis classifier. # name: # type: sq_string # elements: 1 # length: 47 CompactClassificationDiscriminant.nLinearCoeffs # name: # type: sq_string # elements: 1 # length: 881 CompactClassificationDiscriminant: n = nLinearCoeffs (obj) CompactClassificationDiscriminant: n = nLinearCoeffs (obj, delta) Number of nonzero linear coefficients at a regularization threshold. n = nLinearCoeffs (obj) returns the number of predictors the discriminant keeps at its own Delta. n = nLinearCoeffs (obj, delta) returns the number it would keep at each threshold in delta, as a column vector however delta is shaped. A predictor survives a threshold when its DeltaPredictor reaches it, the comparison including equality, so delta at exactly a predictor's own value still counts it. A threshold above every DeltaPredictor therefore leaves nothing and returns zero. The count is taken whatever the DiscrimType, as MATLAB takes it, even though Delta regularizes the linear types alone. See also: fitcdiscr, ClassificationDiscriminant, CompactClassificationDiscriminant # name: # type: sq_string # elements: 1 # length: 68 Number of nonzero linear coefficients at a regularization threshold. # name: # type: sq_string # elements: 1 # length: 41 CompactClassificationDiscriminant.predict # name: # type: sq_string # elements: 1 # length: 1781 CompactClassificationDiscriminant: label = predict (obj, XC) CompactClassificationDiscriminant: [label, score, cost] = predict (obj, XC) Classify new data points into categories using the discriminant analysis model from a CompactClassificationDiscriminant object. label = predict (obj, XC) returns the vector of labels predicted for the corresponding instances in XC, using the corresponding labels from the trained ClassificationDiscriminant, model, obj. obj must be a CompactClassificationDiscriminant class object. XC must be an MxP numeric matrix with the same number of features P as the corresponding predictors of the discriminant model in obj. [label, score, cost] = predict (obj, XC) also returns score, which contains the predicted class scores or posterior probabilities for each instance of the corresponding unique classes, and cost, which is a matrix containing the expected cost of the classifications. A query with a missing value has NaN scores and costs and takes the class of largest prior. The score matrix contains the posterior probabilities for each class, calculated using the multivariate normal probability density function and the prior probabilities of each class. These scores are normalized to ensure they sum to 1 for each observation. The cost matrix contains the expected classification cost for each class, computed based on the posterior probabilities and the specified misclassification costs. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: CompactClassificationDiscriminant, fitcdiscr # name: # type: sq_string # elements: 1 # length: 127 Classify new data points into categories using the discriminant analysis model from a CompactClassificationDiscriminant object. # name: # type: sq_string # elements: 1 # length: 43 CompactClassificationDiscriminant.savemodel # name: # type: sq_string # elements: 1 # length: 517 CompactClassificationDiscriminant: savemodel (obj, filename) Save a CompactClassificationDiscriminant object. savemodel (obj, filename) saves each property of a CompactClassificationDiscriminant object into an Octave binary file, the name of which is specified in filename, along with an extra variable, which defines the type classification object these variables constitute. Use loadmodel in order to load a classification object into Octave's workspace. See also: loadmodel, fitcdiscr, ClassificationDiscriminant # name: # type: sq_string # elements: 1 # length: 48 Save a CompactClassificationDiscriminant object. # name: # type: sq_string # elements: 1 # length: 25 CompactClassificationECOC # name: # type: sq_string # elements: 1 # length: 667 statistics: CompactClassificationECOC A multiclass model built from binary learners, without its training data. A CompactClassificationECOC carries the binary learners of an error correcting output codes model and the coding matrix that says what each of them was trained to tell apart, and nothing else: the predictor data, the labels and the weights are gone, so it predicts and scores new data but cannot be refitted or cross validated. It comes from compact on a ClassificationECOC, and from fitcecoc itself when the binary learners are linear or kernel classifiers, which carry no training data of their own. See also: fitcecoc, ClassificationECOC, designecoc # name: # type: sq_string # elements: 1 # length: 73 A multiclass model built from binary learners, without its training data. # name: # type: sq_string # elements: 1 # length: 40 CompactClassificationECOC.BinaryLearners # name: # type: sq_string # elements: 1 # length: 242 CompactClassificationECOC: property BinaryLearners The trained binary learners A cell column with one trained model per column of CodingMatrix, each telling the classes that column marks +1 from those it marks -1. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 The trained binary learners # name: # type: sq_string # elements: 1 # length: 36 CompactClassificationECOC.BinaryLoss # name: # type: sq_string # elements: 1 # length: 512 CompactClassificationECOC: property BinaryLoss The loss that turns a binary learner's score into a cost One of 'binodeviance', 'exponential', 'hamming', 'hinge', 'linear', 'logit' or 'quadratic'. A learner scoring on (-Inf,+Inf) takes every one of them but 'quadratic', and one scoring on [0,1] takes only 'hamming' and 'quadratic': a loss reads a score against the interval it was written for, and the other way round it would read a posterior as a signed score. Assigning one the learners cannot take raises. # name: # type: sq_string # elements: 1 # length: 56 The loss that turns a binary learner's score into a cost # name: # type: sq_string # elements: 1 # length: 47 CompactClassificationECOC.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 173 CompactClassificationECOC: property CategoricalPredictors The columns of the training data that held categorical predictors, empty when none did. This property is read-only. # name: # type: sq_string # elements: 1 # length: 87 The columns of the training data that held categorical predictors, empty when none did. # name: # type: sq_string # elements: 1 # length: 36 CompactClassificationECOC.ClassNames # name: # type: sq_string # elements: 1 # length: 204 CompactClassificationECOC: property ClassNames The class labels The distinct labels seen in the training data, in the order the rows of CodingMatrix, Prior and Cost take them. This property is read-only. # name: # type: sq_string # elements: 1 # length: 16 The class labels # name: # type: sq_string # elements: 1 # length: 38 CompactClassificationECOC.CodingMatrix # name: # type: sq_string # elements: 1 # length: 229 CompactClassificationECOC: property CodingMatrix The coding design A KxL matrix of -1, 0 and +1 with one row per class and one column per binary learner. A class marked 0 took no part in that learner. This property is read-only. # name: # type: sq_string # elements: 1 # length: 17 The coding design # name: # type: sq_string # elements: 1 # length: 51 CompactClassificationECOC.CompactClassificationECOC # name: # type: sq_string # elements: 1 # length: 407 CompactClassificationECOC: obj = CompactClassificationECOC (Mdl) CompactClassificationECOC: obj = CompactClassificationECOC () Create a CompactClassificationECOC object. Mdl is the ClassificationECOC object to compact. The documented way to reach this constructor is the compact method. Called with no arguments it returns an object with its properties at their defaults, which is what loadmodel fills in. # name: # type: sq_string # elements: 1 # length: 42 Create a CompactClassificationECOC object. # name: # type: sq_string # elements: 1 # length: 30 CompactClassificationECOC.Cost # name: # type: sq_string # elements: 1 # length: 168 CompactClassificationECOC: property Cost The cost of misclassification A KxK matrix whose (i,j) element is the cost of calling a member of class i a member of class j. # name: # type: sq_string # elements: 1 # length: 29 The cost of misclassification # name: # type: sq_string # elements: 1 # length: 48 CompactClassificationECOC.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 252 CompactClassificationECOC: property ExpandedPredictorNames The names of the predictors as the learners saw them. It differs from PredictorNames only where a categorical predictor was expanded, which this package does not do. This property is read-only. # name: # type: sq_string # elements: 1 # length: 53 The names of the predictors as the learners saw them. # name: # type: sq_string # elements: 1 # length: 40 CompactClassificationECOC.LearnerWeights # name: # type: sq_string # elements: 1 # length: 239 CompactClassificationECOC: property LearnerWeights The weight each binary learner was trained on A row with one element per binary learner, the total observation weight of the classes that learner took part in. This property is read-only. # name: # type: sq_string # elements: 1 # length: 45 The weight each binary learner was trained on # name: # type: sq_string # elements: 1 # length: 40 CompactClassificationECOC.PredictorNames # name: # type: sq_string # elements: 1 # length: 144 CompactClassificationECOC: property PredictorNames The names of the predictors, one per column of the training data. This property is read-only. # name: # type: sq_string # elements: 1 # length: 65 The names of the predictors, one per column of the training data. # name: # type: sq_string # elements: 1 # length: 31 CompactClassificationECOC.Prior # name: # type: sq_string # elements: 1 # length: 125 CompactClassificationECOC: property Prior The prior probability of each class, in the order of ClassNames and summing to one. # name: # type: sq_string # elements: 1 # length: 83 The prior probability of each class, in the order of ClassNames and summing to one. # name: # type: sq_string # elements: 1 # length: 38 CompactClassificationECOC.ResponseName # name: # type: sq_string # elements: 1 # length: 111 CompactClassificationECOC: property ResponseName The name of the response variable. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 The name of the response variable. # name: # type: sq_string # elements: 1 # length: 40 CompactClassificationECOC.ScoreTransform # name: # type: sq_string # elements: 1 # length: 196 CompactClassificationECOC: property ScoreTransform The transform applied to the predicted scores A character vector naming one of the transforms parseScoreTransform accepts, or a function handle. # name: # type: sq_string # elements: 1 # length: 45 The transform applied to the predicted scores # name: # type: sq_string # elements: 1 # length: 47 CompactClassificationECOC.discardSupportVectors # name: # type: sq_string # elements: 1 # length: 610 CompactClassificationECOC: obj = discardSupportVectors (obj) Give up the support vectors of the binary learners. obj = discardSupportVectors (obj) empties Alpha, SupportVectors and SupportVectorLabels on every binary learner that is a support vector machine on a linear kernel, whose linear model stands in for them exactly, so nothing the model answers changes. Any other learner is left as it is, a code being free to mix them, and a model with no linear support vector machine among its learners warns and is returned unchanged. See also: CompactClassificationECOC, ClassificationSVM.discardSupportVectors # name: # type: sq_string # elements: 1 # length: 51 Give up the support vectors of the binary learners. # name: # type: sq_string # elements: 1 # length: 30 CompactClassificationECOC.edge # name: # type: sq_string # elements: 1 # length: 829 CompactClassificationECOC: e = edge (obj, X, Y) CompactClassificationECOC: e = edge (..., name, value) CompactClassificationECOC: e = edge (obj, Tbl, ResponseVarName) CompactClassificationECOC: e = edge (obj, Tbl) Classification edge of a CompactClassificationECOC. e = edge (obj, X, Y) returns the weighted mean of the margins, one number for the whole of X. It takes 'Weights' beside the arguments predict takes. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: CompactClassificationECOC.margin, CompactClassificationECOC.loss # name: # type: sq_string # elements: 1 # length: 51 Classification edge of a CompactClassificationECOC. # name: # type: sq_string # elements: 1 # length: 30 CompactClassificationECOC.loss # name: # type: sq_string # elements: 1 # length: 1055 CompactClassificationECOC: L = loss (obj, X, Y) CompactClassificationECOC: L = loss (obj, Tbl, ResponseVarName) CompactClassificationECOC: L = loss (obj, Tbl) CompactClassificationECOC: L = loss (..., name, value) Classification loss of a CompactClassificationECOC. L = loss (obj, X, Y) returns the weighted classification loss of X against the true labels Y. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. Name Value 'LossFun' 'classiferror' (default), 'classifcost', 'mincost', 'binodeviance', 'exponential', 'hinge', 'logit' or 'quadratic'. 'Weights' One nonnegative weight per row of X. The default is uniform. It also takes the 'BinaryLoss' and 'Decoding' arguments predict takes. See also: CompactClassificationECOC.predict, CompactClassificationECOC.edge # name: # type: sq_string # elements: 1 # length: 51 Classification loss of a CompactClassificationECOC. # name: # type: sq_string # elements: 1 # length: 32 CompactClassificationECOC.margin # name: # type: sq_string # elements: 1 # length: 1038 CompactClassificationECOC: m = margin (obj, X, Y) CompactClassificationECOC: m = margin (..., name, value) CompactClassificationECOC: m = margin (obj, Tbl, ResponseVarName) CompactClassificationECOC: m = margin (obj, Tbl) Classification margin of a CompactClassificationECOC. m = margin (obj, X, Y) returns one margin per row of X: the negated loss of the true class less the largest negated loss among the others. A positive margin means the row was classified correctly, and the larger it is the further the decision was from going the other way. It takes the same 'BinaryLoss' and 'Decoding' arguments predict does. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: CompactClassificationECOC.predict, CompactClassificationECOC.edge # name: # type: sq_string # elements: 1 # length: 53 Classification margin of a CompactClassificationECOC. # name: # type: sq_string # elements: 1 # length: 33 CompactClassificationECOC.predict # name: # type: sq_string # elements: 1 # length: 1520 CompactClassificationECOC: label = predict (obj, XC) CompactClassificationECOC: [label, NegLoss] = predict (...) CompactClassificationECOC: [label, NegLoss, PBScore] = predict (...) CompactClassificationECOC: [...] = predict (..., name, value) Classify new data with a trained CompactClassificationECOC. label = predict (obj, XC) sends each row of XC to every binary learner, turns the scores they return into a cost per class, and returns the class of least cost. XC must have as many columns as the data the model was fitted on. [label, NegLoss] = predict (...) also returns the NxK negated average loss, the largest entry of a row naming the class that row was given. [label, NegLoss, PBScore] = predict (...) also returns the NxL scores the binary learners gave the class each was trained to call +1. Name Value 'BinaryLoss' The loss to read the binary scores with, overriding the BinaryLoss property for this call. 'Decoding' 'lossweighted' (default) or 'lossbased'. The first averages the loss over the learners a class took part in, the second over every learner, a class that sat a column out costing the same there as a class at the decision boundary. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: CompactClassificationECOC, ClassificationECOC, fitcecoc # name: # type: sq_string # elements: 1 # length: 59 Classify new data with a trained CompactClassificationECOC. # name: # type: sq_string # elements: 1 # length: 38 CompactClassificationECOC.selectModels # name: # type: sq_string # elements: 1 # length: 423 CompactClassificationECOC: sub = selectModels (obj, idx) Keep a subset of the fitted regularization strengths. sub = selectModels (obj, idx) narrows every binary learner to the strengths idx names, which may be indices into the learner's Lambda or a logical vector over it. Only a linear learner is fitted over several strengths, so any other raises. See also: CompactClassificationECOC, ClassificationLinear.selectModels # name: # type: sq_string # elements: 1 # length: 53 Keep a subset of the fitted regularization strengths. # name: # type: sq_string # elements: 1 # length: 29 CompactClassificationEnsemble # name: # type: sq_string # elements: 1 # length: 557 statistics: CompactClassificationEnsemble Compact ensemble of weak learners for classification A CompactClassificationEnsemble object carries the trained weak learners of a boosted or bagged ensemble and what prediction needs, but not the observations it was fitted on. It predicts new data identically to the ensemble it came from, and weak learners can be removed from it. Create one with the compact method of a ClassificationEnsemble or ClassificationBaggedEnsemble object. See also: fitcensemble, ClassificationEnsemble, ClassificationBaggedEnsemble # name: # type: sq_string # elements: 1 # length: 52 Compact ensemble of weak learners for classification # name: # type: sq_string # elements: 1 # length: 51 CompactClassificationEnsemble.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 193 CompactClassificationEnsemble: property CategoricalPredictors Indices of categorical predictors The predictors every tree treats as categorical, empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Indices of categorical predictors # name: # type: sq_string # elements: 1 # length: 40 CompactClassificationEnsemble.ClassNames # name: # type: sq_string # elements: 1 # length: 195 CompactClassificationEnsemble: property ClassNames Names of the classes The classes, in the type of the response and in the order the columns of the scores take them. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Names of the classes # name: # type: sq_string # elements: 1 # length: 44 CompactClassificationEnsemble.CombineWeights # name: # type: sq_string # elements: 1 # length: 336 CompactClassificationEnsemble: property CombineWeights How the weak learners are combined 'WeightedSum' for a boosted ensemble, whose scores are the sum of each learner's output times its weight, or 'WeightedAverage' for a bagged one, whose scores are the weighted average of its trees' class probabilities. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 How the weak learners are combined # name: # type: sq_string # elements: 1 # length: 59 CompactClassificationEnsemble.CompactClassificationEnsemble # name: # type: sq_string # elements: 1 # length: 273 CompactClassificationEnsemble: obj = CompactClassificationEnsemble (Mdl) Create a CompactClassificationEnsemble object. Mdl is the ClassificationEnsemble or ClassificationBaggedEnsemble object to compact. The documented way to reach this constructor is the compact method. # name: # type: sq_string # elements: 1 # length: 46 Create a CompactClassificationEnsemble object. # name: # type: sq_string # elements: 1 # length: 34 CompactClassificationEnsemble.Cost # name: # type: sq_string # elements: 1 # length: 192 CompactClassificationEnsemble: property Cost Misclassification costs A square matrix, Cost(i,j) being the cost of classifying an observation of class i as class j. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Misclassification costs # name: # type: sq_string # elements: 1 # length: 52 CompactClassificationEnsemble.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 197 CompactClassificationEnsemble: property ExpandedPredictorNames Names of the predictors as the learners saw them The same as PredictorNames, no predictor being expanded. This property is read-only. # name: # type: sq_string # elements: 1 # length: 48 Names of the predictors as the learners saw them # name: # type: sq_string # elements: 1 # length: 40 CompactClassificationEnsemble.NumTrained # name: # type: sq_string # elements: 1 # length: 134 CompactClassificationEnsemble: property NumTrained Number of trained weak learners A nonnegative integer. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Number of trained weak learners # name: # type: sq_string # elements: 1 # length: 44 CompactClassificationEnsemble.PredictorNames # name: # type: sq_string # elements: 1 # length: 142 CompactClassificationEnsemble: property PredictorNames Names of the predictors A cell array of character vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 35 CompactClassificationEnsemble.Prior # name: # type: sq_string # elements: 1 # length: 182 CompactClassificationEnsemble: property Prior Prior probabilities of the classes A row vector with one probability per class, in the order of ClassNames. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Prior probabilities of the classes # name: # type: sq_string # elements: 1 # length: 42 CompactClassificationEnsemble.ResponseName # name: # type: sq_string # elements: 1 # length: 131 CompactClassificationEnsemble: property ResponseName Name of the response variable A character vector. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 44 CompactClassificationEnsemble.ScoreTransform # name: # type: sq_string # elements: 1 # length: 290 CompactClassificationEnsemble: property ScoreTransform Transform applied to the predicted scores A character vector naming a built-in transform, such as 'none' (default) or 'doublelogit', or a function handle. The labels, losses, edges and margins are computed from the transformed scores. # name: # type: sq_string # elements: 1 # length: 41 Transform applied to the predicted scores # name: # type: sq_string # elements: 1 # length: 37 CompactClassificationEnsemble.Trained # name: # type: sq_string # elements: 1 # length: 419 CompactClassificationEnsemble: property Trained Trained weak learners A column cell array with one compact model per learner: a CompactClassificationTree for AdaBoostM1, AdaBoostM2, RUSBoost and Bag, and a CompactRegressionTree for GentleBoost and LogitBoost, which fit regression trees. MATLAB wraps those regression trees in a classifier object of its own; here they are held as they are. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Trained weak learners # name: # type: sq_string # elements: 1 # length: 44 CompactClassificationEnsemble.TrainedWeights # name: # type: sq_string # elements: 1 # length: 158 CompactClassificationEnsemble: property TrainedWeights Weights of the trained weak learners A column with one weight per learner. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Weights of the trained weak learners # name: # type: sq_string # elements: 1 # length: 47 CompactClassificationEnsemble.UsePredForLearner # name: # type: sq_string # elements: 1 # length: 267 CompactClassificationEnsemble: property UsePredForLearner Which predictors each learner uses For the 'Subspace' method, a logical matrix with one row per predictor and one column per learner. Empty for tree learners, as MATLAB returns it. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Which predictors each learner uses # name: # type: sq_string # elements: 1 # length: 34 CompactClassificationEnsemble.edge # name: # type: sq_string # elements: 1 # length: 914 CompactClassificationEnsemble: e = edge (obj, X, Y) CompactClassificationEnsemble: e = edge (..., name, value) CompactClassificationEnsemble: e = edge (obj, Tbl, ResponseVarName) CompactClassificationEnsemble: e = edge (obj, Tbl) Classification edge of a compact ensemble. e is the weighted mean of the margins of the rows of X, the weights normalized so that each class carries its prior probability. 'Mode' and 'Weights' are taken as by loss, and 'Learners' and 'UseObsForLearner' as by predict. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: CompactClassificationEnsemble, CompactClassificationEnsemble.margin # name: # type: sq_string # elements: 1 # length: 42 Classification edge of a compact ensemble. # name: # type: sq_string # elements: 1 # length: 34 CompactClassificationEnsemble.loss # name: # type: sq_string # elements: 1 # length: 1856 CompactClassificationEnsemble: L = loss (obj, X, Y) CompactClassificationEnsemble: L = loss (obj, Tbl, ResponseVarName) CompactClassificationEnsemble: L = loss (obj, Tbl) CompactClassificationEnsemble: L = loss (..., name, value) Classification loss of a compact ensemble. L is the weighted loss of the scores predict gives the rows of X against the labels Y. The weights are normalized so that each class carries its prior probability. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. Name-Value arguments: Name Value 'LossFun' 'classiferror' (default), 'binodeviance', 'classifcost', 'exponential', 'hinge', 'logit', 'mincost', 'quadratic', or a function handle called as lossfun (C, S, W, Cost), C being an NxK logical matrix marking each row's class, S the scores and W the normalized weights. 'Mode' 'ensemble' (default) for one loss over the learners used, 'cumulative' for a column whose element j uses the first j of them, or 'individual' for a column with the loss of each on its own. 'Weights' A nonnegative vector with one weight per row. The default is uniform. 'Learners' and 'UseObsForLearner' are taken as by predict. A row 'UseObsForLearner' hides from every learner used is left out and the weights are renormalized over the rest. A row no learner can score for its missing values stays: it counts as the class of greatest prior under 'classiferror', 'classifcost' and 'mincost', and any other loss is NaN. edge treats the two alike, its result being NaN for the second. See also: CompactClassificationEnsemble, CompactClassificationEnsemble.edge # name: # type: sq_string # elements: 1 # length: 42 Classification loss of a compact ensemble. # name: # type: sq_string # elements: 1 # length: 36 CompactClassificationEnsemble.margin # name: # type: sq_string # elements: 1 # length: 865 CompactClassificationEnsemble: m = margin (obj, X, Y) CompactClassificationEnsemble: m = margin (..., name, value) CompactClassificationEnsemble: m = margin (obj, Tbl, ResponseVarName) CompactClassificationEnsemble: m = margin (obj, Tbl) Classification margins of a compact ensemble. m holds, for each row of X, the score of its true class less the highest score among the other classes. 'Learners' and 'UseObsForLearner' are taken as by predict. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: CompactClassificationEnsemble, CompactClassificationEnsemble.edge # name: # type: sq_string # elements: 1 # length: 45 Classification margins of a compact ensemble. # name: # type: sq_string # elements: 1 # length: 37 CompactClassificationEnsemble.predict # name: # type: sq_string # elements: 1 # length: 1535 CompactClassificationEnsemble: label = predict (obj, X) CompactClassificationEnsemble: [label, scores] = predict (...) CompactClassificationEnsemble: [...] = predict (..., name, value) Classify new data with a compact ensemble. label holds the class of highest score for each row of X, in the type of ClassNames, and scores the NxK scores after ScoreTransform. The scores of a boosted ensemble are the sum over the learners of each learner's weight times its output. For AdaBoostM1 the output is +1 for the class the learner predicts and -1 for the other; for GentleBoost and LogitBoost it is the regression tree's prediction for the first class and its negative for the second; for AdaBoostM2 and RUSBoost it is the learner's class probabilities. The scores of a bagged ensemble are the average of its trees' class probabilities. A row that no learner may score has NaN scores and is given the class of greatest prior probability. Name-Value arguments: Name Value 'Learners' A vector of indices of the learners to use. The default is all of them. 'UseObsForLearner' An NxNumTrained logical matrix saying which learner may score which row. The default lets every learner score every row. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: CompactClassificationEnsemble, fitcensemble # name: # type: sq_string # elements: 1 # length: 42 Classify new data with a compact ensemble. # name: # type: sq_string # elements: 1 # length: 49 CompactClassificationEnsemble.predictorImportance # name: # type: sq_string # elements: 1 # length: 558 CompactClassificationEnsemble: imp = predictorImportance (obj) CompactClassificationEnsemble: [imp, ma] = predictorImportance (obj) Estimate the importance of each predictor. imp is a row vector with one element per predictor, the average over the trees of each tree's predictorImportance, weighted by TrainedWeights. GentleBoost and LogitBoost ensembles take the importance of their regression trees. ma, the predictive measure of association between the predictors, is empty, the trees growing no surrogate splits. See also: CompactClassificationEnsemble # name: # type: sq_string # elements: 1 # length: 42 Estimate the importance of each predictor. # name: # type: sq_string # elements: 1 # length: 44 CompactClassificationEnsemble.removeLearners # name: # type: sq_string # elements: 1 # length: 284 CompactClassificationEnsemble: C = removeLearners (obj, idx) Remove weak learners from a compact ensemble. C is the ensemble without the learners whose indices idx holds, their weights and their columns of UsePredForLearner removed with them. See also: CompactClassificationEnsemble # name: # type: sq_string # elements: 1 # length: 45 Remove weak learners from a compact ensemble. # name: # type: sq_string # elements: 1 # length: 24 CompactClassificationGAM # name: # type: sq_string # elements: 1 # length: 951 statistics: CompactClassificationGAM Compact generalized additive model classification The CompactClassificationGAM class is a compact version of a Generalized Additive Model classifier, ClassificationGAM. It does not include the training data, resulting in a smaller classifier size that can be used for making predictions from new data, but not for tasks such as cross validation. A CompactClassificationGAM object can only be created from a ClassificationGAM model by using the compact method. The engine that fitted the model is carried over in FitMethod, and the compact model predicts by the same scheme the full one did. Under 'boostedtrees', the default, the fit is described by TreeModel, BinEdges and PairDetectionBinEdges. Under 'splines' it is described by Formula, BaseModel, ModelwInt and IntMatrix, which MATLAB's compact model does not carry. Whichever fitted the model, the other set is empty. See also: ClassificationGAM, fitcgam # name: # type: sq_string # elements: 1 # length: 49 Compact generalized additive model classification # name: # type: sq_string # elements: 1 # length: 34 CompactClassificationGAM.BaseModel # name: # type: sq_string # elements: 1 # length: 294 CompactClassificationGAM: property BaseModel Base model parameters A structure containing the parameters of the base model without any interaction terms. The base model represents the generalized additive model with only the main effects (predictor terms) included. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Base model parameters # name: # type: sq_string # elements: 1 # length: 33 CompactClassificationGAM.BinEdges # name: # type: sq_string # elements: 1 # length: 290 CompactClassificationGAM: property BinEdges Bin edges of the fitted shape functions A cell array with one row vector per predictor, holding the cut points the boosted-tree engine binned it at. It is the empty cell under the spline engine, which does no binning. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Bin edges of the fitted shape functions # name: # type: sq_string # elements: 1 # length: 46 CompactClassificationGAM.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 225 CompactClassificationGAM: property CategoricalPredictors Indices of the categorical predictors A numeric vector holding the column of each predictor treated as categorical, and empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 35 CompactClassificationGAM.ClassNames # name: # type: sq_string # elements: 1 # length: 349 CompactClassificationGAM: property ClassNames Names of classes in the response variable An array of unique values of the response variable Y, which has the same data types as the data in Y. This property is read-only. ClassNames can have any of the following datatypes: Cell array of character vectors Character array Logical vector Numeric vector # name: # type: sq_string # elements: 1 # length: 41 Names of classes in the response variable # name: # type: sq_string # elements: 1 # length: 49 CompactClassificationGAM.CompactClassificationGAM # name: # type: sq_string # elements: 1 # length: 426 CompactClassificationGAM: obj = CompactClassificationGAM (Mdl) CompactClassificationGAM: obj = CompactClassificationGAM () Create a CompactClassificationGAM object. Mdl is the ClassificationGAM object to compact. The documented way to reach this constructor is the compact method. Called with no arguments it returns an object with its properties empty, which is how a saved model is rebuilt before its values are filled in. # name: # type: sq_string # elements: 1 # length: 41 Create a CompactClassificationGAM object. # name: # type: sq_string # elements: 1 # length: 29 CompactClassificationGAM.Cost # name: # type: sq_string # elements: 1 # length: 1195 CompactClassificationGAM: property Cost Cost of Misclassification A square matrix specifying the cost of misclassification of a point. Cost(i,j) is the cost of classifying a point into class j if its true class is i (that is, the rows correspond to the true class and the columns correspond to the predicted class). The order of the rows and columns in Cost corresponds to the order of the classes in ClassNames. The number of rows and columns in Cost is the number of unique classes in the response. By default, Cost(i,j) = 1 if i != j, and Cost(i,j) = 0 if i = j. In other words, the cost is 0 for correct classification and 1 for incorrect classification. Add or change the Cost property using dot notation as in: obj.Cost = costMatrix A cost may also be given as a struct with the fields ClassNames and ClassificationCosts, which names the order its own matrix is written in. That matrix is permuted into the order of ClassNames above, so a caller need not know which order the classes were sorted into. It must name every class. A cost must be floating point, not sparse, not complex, non-negative and zero down its diagonal, and must hold no NaN or Inf. A single is widened to double. # name: # type: sq_string # elements: 1 # length: 25 Cost of Misclassification # name: # type: sq_string # elements: 1 # length: 47 CompactClassificationGAM.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 300 CompactClassificationGAM: property ExpandedPredictorNames Names of the expanded predictor variables A cell array of character vectors naming the predictors as the model sees them. It matches PredictorNames unless a categorical predictor was expanded into dummy variables. This property is read-only. # name: # type: sq_string # elements: 1 # length: 41 Names of the expanded predictor variables # name: # type: sq_string # elements: 1 # length: 34 CompactClassificationGAM.FitMethod # name: # type: sq_string # elements: 1 # length: 186 CompactClassificationGAM: property FitMethod Which engine fitted the model Either 'boostedtrees' or 'splines', as the model it was compacted from was fitted. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Which engine fitted the model # name: # type: sq_string # elements: 1 # length: 32 CompactClassificationGAM.Formula # name: # type: sq_string # elements: 1 # length: 279 CompactClassificationGAM: property Formula Model specification formula A character vector specifying the model formula in the form 'Y ~ terms' where Y represents the response variable and terms specifies the predictor variables and interaction terms. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Model specification formula # name: # type: sq_string # elements: 1 # length: 34 CompactClassificationGAM.IntMatrix # name: # type: sq_string # elements: 1 # length: 563 CompactClassificationGAM: property IntMatrix Every term the model fits A logical matrix with one row per term and one column per predictor, true wherever the term multiplies that predictor. A row naming one predictor is a main effect, two an interaction, and three or more a higher-order term. This property is read-only. It is the complete record, where Interactions reports only the two-way terms, in the form MATLAB reports them. It is also the form the 'Interactions' option takes back, so passing it to the constructor rebuilds a model over the same terms. # name: # type: sq_string # elements: 1 # length: 25 Every term the model fits # name: # type: sq_string # elements: 1 # length: 37 CompactClassificationGAM.Interactions # name: # type: sq_string # elements: 1 # length: 603 CompactClassificationGAM: property Interactions Two-way interaction terms of the fitted model A Kx2 matrix of predictor index pairs, one row per two-way term the model carries, and zeros (0, 2) when it carries none. It reports what was fitted rather than what was asked for, so a count of terms, 'all', a logical matrix and a formula all leave the same kind of value behind. This property is read-only. A main effect names one predictor and a higher-order term names three or more, and neither has a two-column form, so neither appears here. IntMatrix remains the complete record of every term fitted. # name: # type: sq_string # elements: 1 # length: 45 Two-way interaction terms of the fitted model # name: # type: sq_string # elements: 1 # length: 34 CompactClassificationGAM.Intercept # name: # type: sq_string # elements: 1 # length: 203 CompactClassificationGAM: property Intercept Intercept of the fitted model A numeric scalar, the log-odds of the response mean, which every additive term is measured against. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Intercept of the fitted model # name: # type: sq_string # elements: 1 # length: 34 CompactClassificationGAM.ModelwInt # name: # type: sq_string # elements: 1 # length: 274 CompactClassificationGAM: property ModelwInt Model parameters with interactions A structure containing the parameters of the model that includes interaction terms. This model extends the base model by adding interaction terms between predictors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Model parameters with interactions # name: # type: sq_string # elements: 1 # length: 38 CompactClassificationGAM.NumPredictors # name: # type: sq_string # elements: 1 # length: 230 CompactClassificationGAM: property NumPredictors Number of predictors A positive integer value specifying the number of predictors in the training dataset used for training the ClassificationGAM model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of predictors # name: # type: sq_string # elements: 1 # length: 46 CompactClassificationGAM.PairDetectionBinEdges # name: # type: sq_string # elements: 1 # length: 275 CompactClassificationGAM: property PairDetectionBinEdges Bin edges used to detect interactions A cell array with one row vector per predictor, the eight equal-frequency bins pairs were tested on, empty when the model carries no interaction terms. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Bin edges used to detect interactions # name: # type: sq_string # elements: 1 # length: 39 CompactClassificationGAM.PredictorNames # name: # type: sq_string # elements: 1 # length: 263 CompactClassificationGAM: property PredictorNames Names of predictor variables A cell array of character vectors specifying the names of the predictor variables. The names are in the order in which they appear in the training dataset. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Names of predictor variables # name: # type: sq_string # elements: 1 # length: 30 CompactClassificationGAM.Prior # name: # type: sq_string # elements: 1 # length: 270 CompactClassificationGAM: property Prior Prior probability for each class A 2-element numeric vector specifying the prior probabilities for each class. The order of the elements in Prior corresponds to the order of the classes in ClassNames. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Prior probability for each class # name: # type: sq_string # elements: 1 # length: 37 CompactClassificationGAM.ResponseName # name: # type: sq_string # elements: 1 # length: 166 CompactClassificationGAM: property ResponseName Response variable name A character vector specifying the name of the response variable Y. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Response variable name # name: # type: sq_string # elements: 1 # length: 39 CompactClassificationGAM.ScoreTransform # name: # type: sq_string # elements: 1 # length: 1335 CompactClassificationGAM: property ScoreTransform Transformation function for classification scores Specified as a function handle for transforming the classification scores. Add or change the ScoreTransform property using dot notation as in: obj.ScoreTransform = 'function_name' obj.ScoreTransform = @function_handle When specified as a character vector, it can be any of the following built-in functions. Nevertheless, the ScoreTransform property always stores their function handle equivalent. Value Description 'doublelogit' 1 ./ (1 + exp (-2 x x)) 'invlogit' log (x ./ (1 - x)) 'ismax' Sets the score for the class with the largest score to 1, and for all other classes to 0 'logit' 1 ./ (1 + exp (-x)) 'none' x (no transformation) 'identity' x (no transformation) 'sign' -1 for x < 0, 0 for x = 0, 1 for x > 0 'symmetric' 2 x x - 1 'symmetricismax' Sets the score for the class with the largest score to 1, and for all other classes to -1 'symmetriclogit' 2 ./ (1 + exp (-x)) - 1 The default is 'logit', as in MATLAB. This model's raw score is a log-odds, reported as the pair [-f, f] whose two columns sum to zero, and the transform is what turns it into the posterior probabilities that sum to one. Every transform therefore composes on the log-odds and not on the probabilities, so 'none' returns the log-odds themselves. # name: # type: sq_string # elements: 1 # length: 49 Transformation function for classification scores # name: # type: sq_string # elements: 1 # length: 34 CompactClassificationGAM.TreeModel # name: # type: sq_string # elements: 1 # length: 222 CompactClassificationGAM: property TreeModel The fitted shape functions and interaction surfaces The structure the full model reports, carried over unchanged, and empty under the spline engine. This property is read-only. # name: # type: sq_string # elements: 1 # length: 51 The fitted shape functions and interaction surfaces # name: # type: sq_string # elements: 1 # length: 29 CompactClassificationGAM.edge # name: # type: sq_string # elements: 1 # length: 943 CompactClassificationGAM: e = edge (obj, X, Y) CompactClassificationGAM: e = edge (..., "Weights", w) CompactClassificationGAM: e = edge (obj, Tbl, ResponseVarName) CompactClassificationGAM: e = edge (obj, Tbl) Classification edge of a compact generalized additive model. e = edge (obj, X, Y) returns the mean of the classification margins over the rows of X. e = edge (..., "Weights", w) takes the weighted mean instead, with one weight per row of X. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: CompactClassificationGAM, ClassificationGAM, CompactClassificationGAM.margin, CompactClassificationGAM.loss, CompactClassificationGAM.predict # name: # type: sq_string # elements: 1 # length: 60 Classification edge of a compact generalized additive model. # name: # type: sq_string # elements: 1 # length: 29 CompactClassificationGAM.loss # name: # type: sq_string # elements: 1 # length: 1429 CompactClassificationGAM: L = loss (obj, X, Y) CompactClassificationGAM: L = loss (obj, Tbl, ResponseVarName) CompactClassificationGAM: L = loss (obj, Tbl) CompactClassificationGAM: L = loss (..., name, value) Classification loss of a compact generalized additive model. L = loss (obj, X, Y) returns the loss of the model on the rows of X against the true labels Y. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. L = loss (..., name, value) accepts the following name-value pairs: "LossFun" selects the loss. Supported values are "mincost", the default, "binodeviance", "classifcost", "classiferror", "exponential", "hinge", "logit" and "quadratic". "mincost" assigns each observation to the class of least expected cost and charges what that assignment costs, so it reads the scores as a posterior, which is what this model returns; "classifcost" charges what the model's own prediction costs. "Weights" holds one weight per row of X, normalised to sum to one before it is applied. See also: CompactClassificationGAM, ClassificationGAM, CompactClassificationGAM.margin, CompactClassificationGAM.edge, CompactClassificationGAM.predict # name: # type: sq_string # elements: 1 # length: 60 Classification loss of a compact generalized additive model. # name: # type: sq_string # elements: 1 # length: 31 CompactClassificationGAM.margin # name: # type: sq_string # elements: 1 # length: 990 CompactClassificationGAM: m = margin (obj, X, Y) CompactClassificationGAM: m = margin (obj, Tbl, ResponseVarName) CompactClassificationGAM: m = margin (obj, Tbl) Classification margin of a compact generalized additive model. m = margin (obj, X, Y) returns a column vector holding, for each row of X, the score the model gives its true class in Y less the score it gives the other class. A positive margin means the observation is classified correctly, and the larger it is the more confidently so. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: CompactClassificationGAM, ClassificationGAM, CompactClassificationGAM.edge, CompactClassificationGAM.loss, CompactClassificationGAM.predict # name: # type: sq_string # elements: 1 # length: 62 Classification margin of a compact generalized additive model. # name: # type: sq_string # elements: 1 # length: 32 CompactClassificationGAM.predict # name: # type: sq_string # elements: 1 # length: 1704 CompactClassificationGAM: label = predict (obj, XC) CompactClassificationGAM: [label, score] = predict (obj, XC) CompactClassificationGAM: [label, score] = predict (..., 'IncludeInteractions', includeInteractions) Predict labels for new data using the Generalized Additive Model (GAM) stored in a CompactClassificationGAM object. label = predict (obj, XC) returns the predicted labels for the data in XC based on the model stored in the CompactClassificationGAM object, obj. [label, score] = predict (obj, XC) also returns score, which contains the predicted class scores or posterior probabilities for each observation. Every row is predicted. Under boosted trees a missing value adds nothing from a main effect, and an interaction term takes the value its trees give a row missing that predictor; under splines a row holding a missing value is scored NaN and takes the class of largest prior. [label, score] = predict (obj, XC, 'IncludeInteractions', includeInteractions) allows you to specify whether interaction terms should be included when making predictions. obj must be a CompactClassificationGAM class object. XC must be an MxP numeric matrix where each row is an observation and each column corresponds to a predictor variable. includeInteractions is a logical scalar indicating whether to include interaction terms in the predictions. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: CompactClassificationGAM, ClassificationGAM, fitcgam # name: # type: sq_string # elements: 1 # length: 115 Predict labels for new data using the Generalized Additive Model (GAM) stored in a CompactClassificationGAM object. # name: # type: sq_string # elements: 1 # length: 34 CompactClassificationGAM.savemodel # name: # type: sq_string # elements: 1 # length: 505 CompactClassificationGAM: savemodel (obj, filename) Save a CompactClassificationGAM object. savemodel (obj, filename) saves each property of a CompactClassificationGAM object into an Octave binary file, the name of which is specified in filename, along with an extra variable, which defines the type classification object these variables constitute. Use loadmodel in order to load a classification object into Octave's workspace. See also: loadmodel, fitcgam, ClassificationGAM, CompactClassificationGAM # name: # type: sq_string # elements: 1 # length: 39 Save a CompactClassificationGAM object. # name: # type: string # elements: 1 # length: 31 CompactClassificationNaiveBayes # name: # type: sq_string # elements: 1 # length: 574 statistics: CompactClassificationNaiveBayes Compact naive Bayes classification A CompactClassificationNaiveBayes object carries the fitted densities of a ClassificationNaiveBayes model and everything predict needs, but not the observations the model was fitted on. It classifies new data identically to the model it came from, and is far smaller to keep or to ship. Create one with the compact method of a ClassificationNaiveBayes object. Because it holds no training data, it has no resub methods and cannot be cross-validated. See also: ClassificationNaiveBayes, fitcnb # name: # type: sq_string # elements: 1 # length: 34 Compact naive Bayes classification # name: # type: sq_string # elements: 1 # length: 49 CompactClassificationNaiveBayes.CategoricalLevels # name: # type: sq_string # elements: 1 # length: 253 CompactClassificationNaiveBayes: property CategoricalLevels Levels of the categorical predictors A cell array with one entry per predictor, holding the distinct levels of each categorical predictor and empty for every other. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Levels of the categorical predictors # name: # type: sq_string # elements: 1 # length: 53 CompactClassificationNaiveBayes.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 188 CompactClassificationNaiveBayes: property CategoricalPredictors Categorical predictor indices The column indices treated as categorical, or empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Categorical predictor indices # name: # type: sq_string # elements: 1 # length: 42 CompactClassificationNaiveBayes.ClassNames # name: # type: sq_string # elements: 1 # length: 209 CompactClassificationNaiveBayes: property ClassNames Class labels of the fitted model The distinct classes the model was fitted on, in the order the other per-class properties use. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Class labels of the fitted model # name: # type: sq_string # elements: 1 # length: 63 CompactClassificationNaiveBayes.CompactClassificationNaiveBayes # name: # type: sq_string # elements: 1 # length: 285 CompactClassificationNaiveBayes: obj = CompactClassificationNaiveBayes (Mdl) Create a CompactClassificationNaiveBayes object. Mdl is the ClassificationNaiveBayes object to compact. The documented way to reach this constructor is the compact method. See also: ClassificationNaiveBayes # name: # type: sq_string # elements: 1 # length: 48 Create a CompactClassificationNaiveBayes object. # name: # type: sq_string # elements: 1 # length: 36 CompactClassificationNaiveBayes.Cost # name: # type: sq_string # elements: 1 # length: 177 CompactClassificationNaiveBayes: property Cost Misclassification cost A square numeric matrix where Cost(i,j) is the cost of classifying an observation of class i into class j. # name: # type: sq_string # elements: 1 # length: 22 Misclassification cost # name: # type: sq_string # elements: 1 # length: 49 CompactClassificationNaiveBayes.DistributionNames # name: # type: sq_string # elements: 1 # length: 214 CompactClassificationNaiveBayes: property DistributionNames Predictor distributions A cell array of character vectors with one entry per predictor, naming the distribution fitted to it. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Predictor distributions # name: # type: sq_string # elements: 1 # length: 54 CompactClassificationNaiveBayes.DistributionParameters # name: # type: sq_string # elements: 1 # length: 229 CompactClassificationNaiveBayes: property DistributionParameters Fitted distribution parameters A cell array with one row per class and one column per predictor, holding the parameters fitted to each. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 Fitted distribution parameters # name: # type: sq_string # elements: 1 # length: 54 CompactClassificationNaiveBayes.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 207 CompactClassificationNaiveBayes: property ExpandedPredictorNames Expanded predictor variable names A cell array of character vectors naming the predictors as the model sees them. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Expanded predictor variable names # name: # type: sq_string # elements: 1 # length: 38 CompactClassificationNaiveBayes.Kernel # name: # type: sq_string # elements: 1 # length: 198 CompactClassificationNaiveBayes: property Kernel Kernel smoothing functions A cell array naming the smoothing kernel of each kernel predictor, and empty for every other. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Kernel smoothing functions # name: # type: sq_string # elements: 1 # length: 34 CompactClassificationNaiveBayes.Mu # name: # type: sq_string # elements: 1 # length: 185 CompactClassificationNaiveBayes: property Mu Predictor means The means used to center the predictors, when the model standardizes them, and empty otherwise. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Predictor means # name: # type: sq_string # elements: 1 # length: 46 CompactClassificationNaiveBayes.PredictorNames # name: # type: sq_string # elements: 1 # length: 167 CompactClassificationNaiveBayes: property PredictorNames Predictor variable names A cell array of character vectors naming the predictors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Predictor variable names # name: # type: sq_string # elements: 1 # length: 37 CompactClassificationNaiveBayes.Prior # name: # type: sq_string # elements: 1 # length: 165 CompactClassificationNaiveBayes: property Prior Class prior probabilities A numeric row vector with one entry per class, in the order of ClassNames, summing to one. # name: # type: sq_string # elements: 1 # length: 25 Class prior probabilities # name: # type: sq_string # elements: 1 # length: 44 CompactClassificationNaiveBayes.ResponseName # name: # type: sq_string # elements: 1 # length: 155 CompactClassificationNaiveBayes: property ResponseName Response variable name A character vector naming the response variable. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Response variable name # name: # type: sq_string # elements: 1 # length: 46 CompactClassificationNaiveBayes.ScoreTransform # name: # type: sq_string # elements: 1 # length: 185 CompactClassificationNaiveBayes: property ScoreTransform Score transformation A character vector naming the function applied to the posterior returned by predict, or a function handle. # name: # type: sq_string # elements: 1 # length: 20 Score transformation # name: # type: sq_string # elements: 1 # length: 37 CompactClassificationNaiveBayes.Sigma # name: # type: sq_string # elements: 1 # length: 215 CompactClassificationNaiveBayes: property Sigma Predictor standard deviations The standard deviations used to scale the predictors, when the model standardizes them, and empty otherwise. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Predictor standard deviations # name: # type: sq_string # elements: 1 # length: 39 CompactClassificationNaiveBayes.Support # name: # type: sq_string # elements: 1 # length: 199 CompactClassificationNaiveBayes: property Support Kernel smoothing supports A cell array giving the support of each kernel predictor's density, and empty for every other. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Kernel smoothing supports # name: # type: sq_string # elements: 1 # length: 37 CompactClassificationNaiveBayes.Width # name: # type: sq_string # elements: 1 # length: 225 CompactClassificationNaiveBayes: property Width Kernel smoothing bandwidths A numeric matrix with one row per class and one column per predictor, and empty when no predictor uses a kernel density. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Kernel smoothing bandwidths # name: # type: sq_string # elements: 1 # length: 36 CompactClassificationNaiveBayes.edge # name: # type: sq_string # elements: 1 # length: 607 CompactClassificationNaiveBayes: e = edge (obj, X, Y) CompactClassificationNaiveBayes: e = edge (..., 'Weights', w) CompactClassificationNaiveBayes: e = edge (obj, Tbl, ResponseVarName) CompactClassificationNaiveBayes: e = edge (obj, Tbl) Classification edge on new data. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. # name: # type: sq_string # elements: 1 # length: 32 Classification edge on new data. # name: # type: sq_string # elements: 1 # length: 36 CompactClassificationNaiveBayes.logp # name: # type: sq_string # elements: 1 # length: 681 CompactClassificationNaiveBayes: lp = logp (obj, X) Log unconditional probability density of new data. lp = logp (obj, X) returns one value per observation, the logarithm of its density under the fitted model taken over all the classes, each weighted by its prior. A markedly low value marks an observation the model finds unlike anything it was trained on, whatever class it would be assigned to. X may also be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. # name: # type: sq_string # elements: 1 # length: 50 Log unconditional probability density of new data. # name: # type: sq_string # elements: 1 # length: 36 CompactClassificationNaiveBayes.loss # name: # type: sq_string # elements: 1 # length: 692 CompactClassificationNaiveBayes: l = loss (obj, X, Y) CompactClassificationNaiveBayes: l = loss (obj, Tbl, ResponseVarName) CompactClassificationNaiveBayes: l = loss (obj, Tbl) CompactClassificationNaiveBayes: l = loss (..., name, value) Classification loss on new data. Takes the 'LossFun' and 'Weights' options that ClassificationNaiveBayes.loss takes. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. # name: # type: sq_string # elements: 1 # length: 32 Classification loss on new data. # name: # type: sq_string # elements: 1 # length: 38 CompactClassificationNaiveBayes.margin # name: # type: sq_string # elements: 1 # length: 558 CompactClassificationNaiveBayes: m = margin (obj, X, Y) CompactClassificationNaiveBayes: m = margin (obj, Tbl, ResponseVarName) CompactClassificationNaiveBayes: m = margin (obj, Tbl) Classification margin on new data. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. # name: # type: sq_string # elements: 1 # length: 34 Classification margin on new data. # name: # type: sq_string # elements: 1 # length: 39 CompactClassificationNaiveBayes.predict # name: # type: sq_string # elements: 1 # length: 906 CompactClassificationNaiveBayes: label = predict (obj, XC) CompactClassificationNaiveBayes: [label, score, cost] = predict (obj, XC) Classify new data with a compact naive Bayes model. The same classification the model it came from would give: the label of least expected cost, the posterior of each class, and the expected misclassification cost of each. A missing predictor is skipped and the observation classified on the rest. Under the 'mn' distribution, which describes the whole row at once, an observation missing any count has posterior and cost NaN and takes the class of largest prior, whatever the cost. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. # name: # type: sq_string # elements: 1 # length: 51 Classify new data with a compact naive Bayes model. # name: # type: sq_string # elements: 1 # length: 41 CompactClassificationNaiveBayes.savemodel # name: # type: sq_string # elements: 1 # length: 513 CompactClassificationNaiveBayes: savemodel (obj, filename) Save a CompactClassificationNaiveBayes object. savemodel (obj, filename) saves each property of a CompactClassificationNaiveBayes object into an Octave binary file, the name of which is specified in filename, along with an extra variable, which defines the type classification object these variables constitute. Use loadmodel in order to load a classification object into Octave's workspace. See also: loadmodel, fitcnb, CompactClassificationNaiveBayes # name: # type: sq_string # elements: 1 # length: 46 Save a CompactClassificationNaiveBayes object. # name: # type: sq_string # elements: 1 # length: 34 CompactClassificationNeuralNetwork # name: # type: sq_string # elements: 1 # length: 757 statistics: CompactClassificationNeuralNetwork Compact neural network classification The CompactClassificationNeuralNetwork class implements a compact version of the neural network classifier object, which can predict responses for new data using the predict method, but does not store the training data. A compact neural network classification model is a smaller version of the full ClassificationNeuralNetwork model that does not include the training data. It consumes less memory than the full model, but cannot perform tasks that require the training data, such as cross-validation. Create a CompactClassificationNeuralNetwork object by using the compact method on a ClassificationNeuralNetwork object. See also: ClassificationNeuralNetwork, fitcnet # name: # type: sq_string # elements: 1 # length: 37 Compact neural network classification # name: # type: sq_string # elements: 1 # length: 46 CompactClassificationNeuralNetwork.Activations # name: # type: sq_string # elements: 1 # length: 388 CompactClassificationNeuralNetwork: property Activations Activation functions for hidden layers A character vector or cell array of character vectors specifying the activation functions used in the hidden layers of the neural network. Supported activation functions include: 'linear', 'sigmoid', 'relu', 'tanh', 'softmax', 'lrelu', 'prelu', 'elu', and 'gelu'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Activation functions for hidden layers # name: # type: sq_string # elements: 1 # length: 56 CompactClassificationNeuralNetwork.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 235 CompactClassificationNeuralNetwork: property CategoricalPredictors Indices of the categorical predictors A numeric vector holding the column of each predictor treated as categorical, and empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 45 CompactClassificationNeuralNetwork.ClassNames # name: # type: sq_string # elements: 1 # length: 359 CompactClassificationNeuralNetwork: property ClassNames Names of classes in the response variable An array of unique values of the response variable Y, which has the same data types as the data in Y. This property is read-only. ClassNames can have any of the following datatypes: Cell array of character vectors Character array Logical vector Numeric vector # name: # type: sq_string # elements: 1 # length: 41 Names of classes in the response variable # name: # type: sq_string # elements: 1 # length: 69 CompactClassificationNeuralNetwork.CompactClassificationNeuralNetwork # name: # type: sq_string # elements: 1 # length: 486 CompactClassificationNeuralNetwork: obj = CompactClassificationNeuralNetwork (Mdl) CompactClassificationNeuralNetwork: obj = CompactClassificationNeuralNetwork () Create a CompactClassificationNeuralNetwork object. Mdl is the ClassificationNeuralNetwork object to compact. The documented way to reach this constructor is the compact method. Called with no arguments it returns an object with its properties empty, which is how a saved model is rebuilt before its values are filled in. # name: # type: sq_string # elements: 1 # length: 51 Create a CompactClassificationNeuralNetwork object. # name: # type: sq_string # elements: 1 # length: 39 CompactClassificationNeuralNetwork.Cost # name: # type: sq_string # elements: 1 # length: 754 CompactClassificationNeuralNetwork: property Cost Cost of misclassification A numeric matrix with one row and one column per class, where Cost(i,j) is the cost of classifying an observation of class i as class j. It is taken from the model this object was compacted from. This property is read-only. A cost may also be given as a struct with the fields ClassNames and ClassificationCosts, which names the order its own matrix is written in. That matrix is permuted into the order of ClassNames above, so a caller need not know which order the classes were sorted into. It must name every class. A cost must be floating point, not sparse, not complex, non-negative and zero down its diagonal, and must hold no NaN or Inf. A single is widened to double. # name: # type: sq_string # elements: 1 # length: 25 Cost of misclassification # name: # type: sq_string # elements: 1 # length: 57 CompactClassificationNeuralNetwork.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 310 CompactClassificationNeuralNetwork: property ExpandedPredictorNames Names of the expanded predictor variables A cell array of character vectors naming the predictors as the model sees them. It matches PredictorNames unless a categorical predictor was expanded into dummy variables. This property is read-only. # name: # type: sq_string # elements: 1 # length: 41 Names of the expanded predictor variables # name: # type: sq_string # elements: 1 # length: 46 CompactClassificationNeuralNetwork.LayerBiases # name: # type: sq_string # elements: 1 # length: 246 CompactClassificationNeuralNetwork: property LayerBiases Learned bias of each fully connected layer A cell array holding one column vector per layer, the output layer included, with one entry per neuron of that layer. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Learned bias of each fully connected layer # name: # type: sq_string # elements: 1 # length: 45 CompactClassificationNeuralNetwork.LayerSizes # name: # type: sq_string # elements: 1 # length: 486 CompactClassificationNeuralNetwork: property LayerSizes Sizes of fully connected layers A positive integer vector specifying the sizes of the fully connected layers in the neural network model. The i-th element of LayerSizes is the number of outputs in the i-th fully connected layer of the neural network model. LayerSizes does not include the size of the final fully connected layer. This layer always has K outputs, where K is the number of classes in Y. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Sizes of fully connected layers # name: # type: sq_string # elements: 1 # length: 47 CompactClassificationNeuralNetwork.LayerWeights # name: # type: sq_string # elements: 1 # length: 275 CompactClassificationNeuralNetwork: property LayerWeights Learned weights of each fully connected layer A cell array holding one matrix per layer, the output layer included, with one row per neuron of that layer and one column per input it takes. This property is read-only. # name: # type: sq_string # elements: 1 # length: 45 Learned weights of each fully connected layer # name: # type: sq_string # elements: 1 # length: 37 CompactClassificationNeuralNetwork.Mu # name: # type: sq_string # elements: 1 # length: 227 CompactClassificationNeuralNetwork: property Mu Predictor means A numeric vector containing the means of the predictors used for standardization. Empty when the predictor data were not standardized. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Predictor means # name: # type: sq_string # elements: 1 # length: 48 CompactClassificationNeuralNetwork.NumPredictors # name: # type: sq_string # elements: 1 # length: 237 CompactClassificationNeuralNetwork: property NumPredictors Number of predictors A positive integer value specifying the number of predictors in the training dataset used for training the neural network model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of predictors # name: # type: sq_string # elements: 1 # length: 56 CompactClassificationNeuralNetwork.OutputLayerActivation # name: # type: sq_string # elements: 1 # length: 306 CompactClassificationNeuralNetwork: property OutputLayerActivation Activation function for output layer A character vector specifying the activation function of the output layer of the neural network. Supported activation functions are the same as for the Activations property. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Activation function for output layer # name: # type: sq_string # elements: 1 # length: 49 CompactClassificationNeuralNetwork.PredictorNames # name: # type: sq_string # elements: 1 # length: 273 CompactClassificationNeuralNetwork: property PredictorNames Names of predictor variables A cell array of character vectors specifying the names of the predictor variables. The names are in the order in which they appear in the training dataset. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Names of predictor variables # name: # type: sq_string # elements: 1 # length: 40 CompactClassificationNeuralNetwork.Prior # name: # type: sq_string # elements: 1 # length: 257 CompactClassificationNeuralNetwork: property Prior Prior probability of each class A numeric vector with one entry per class, in the order of ClassNames, summing to one. It is taken from the model this object was compacted from. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Prior probability of each class # name: # type: sq_string # elements: 1 # length: 47 CompactClassificationNeuralNetwork.ResponseName # name: # type: sq_string # elements: 1 # length: 176 CompactClassificationNeuralNetwork: property ResponseName Response variable name A character vector specifying the name of the response variable Y. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Response variable name # name: # type: sq_string # elements: 1 # length: 49 CompactClassificationNeuralNetwork.ScoreTransform # name: # type: sq_string # elements: 1 # length: 999 CompactClassificationNeuralNetwork: property ScoreTransform Transformation function for classification scores Specified as a function handle for transforming the classification scores. Add or change the ScoreTransform property using dot notation as in: obj.ScoreTransform = 'function_name' obj.ScoreTransform = @function_handle When specified as a character vector, it can be any of the following built-in functions. Nevertheless, the ScoreTransform property always stores their function handle equivalent. Value Description 'doublelogit' 1 ./ (1 + exp (-2 x x)) 'invlogit' log (x ./ (1 - x)) 'ismax' Sets the score for the class with the largest score to 1, and for all other classes to 0 'logit' 1 ./ (1 + exp (-x)) 'none' x (no transformation) 'identity' x (no transformation) 'sign' -1 for x < 0, 0 for x = 0, 1 for x > 0 'symmetric' 2 x x - 1 'symmetricismax' Sets the score for the class with the largest score to 1, and for all other classes to -1 'symmetriclogit' 2 ./ (1 + exp (-x)) - 1 # name: # type: sq_string # elements: 1 # length: 49 Transformation function for classification scores # name: # type: sq_string # elements: 1 # length: 40 CompactClassificationNeuralNetwork.Sigma # name: # type: sq_string # elements: 1 # length: 258 CompactClassificationNeuralNetwork: property Sigma Predictor standard deviations A numeric vector containing the standard deviations of the predictors used for standardization. Empty when the predictor data were not standardized. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Predictor standard deviations # name: # type: sq_string # elements: 1 # length: 39 CompactClassificationNeuralNetwork.edge # name: # type: sq_string # elements: 1 # length: 1032 CompactClassificationNeuralNetwork: e = edge (obj, X, Y) CompactClassificationNeuralNetwork: e = edge (..., "Weights", w) CompactClassificationNeuralNetwork: e = edge (obj, Tbl, ResponseVarName) CompactClassificationNeuralNetwork: e = edge (obj, Tbl) Classification edge of a compact neural network classifier. e = edge (obj, X, Y) returns the mean of the classification margins over the rows of X. e = edge (..., "Weights", w) takes the weighted mean instead, with one weight per row of X. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: CompactClassificationNeuralNetwork, ClassificationNeuralNetwork, CompactClassificationNeuralNetwork.margin, CompactClassificationNeuralNetwork.loss, CompactClassificationNeuralNetwork.predict # name: # type: sq_string # elements: 1 # length: 59 Classification edge of a compact neural network classifier. # name: # type: sq_string # elements: 1 # length: 39 CompactClassificationNeuralNetwork.loss # name: # type: sq_string # elements: 1 # length: 1690 CompactClassificationNeuralNetwork: L = loss (obj, X, Y) CompactClassificationNeuralNetwork: L = loss (obj, Tbl, ResponseVarName) CompactClassificationNeuralNetwork: L = loss (obj, Tbl) CompactClassificationNeuralNetwork: L = loss (..., name, value) Classification loss of a compact neural network classifier. L = loss (obj, X, Y) returns the loss of the model on the rows of X against the true labels Y. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. L = loss (..., name, value) accepts the following name-value pairs: "LossFun" selects the loss. Supported values are "mincost", the default, "binodeviance", "classifcost", "classiferror", "crossentropy", "exponential", "hinge", "logit" and "quadratic". "mincost" assigns each observation to the class of least expected cost and charges what that assignment costs, so it reads the scores as a posterior; "classifcost" charges what the model's own prediction costs. "crossentropy" is defined for a network only. Note that the default differs from the other classifiers in this package, which default to "classiferror", and follows MATLAB's for this class. "Weights" holds one weight per row of X, normalised to sum to one before it is applied. See also: CompactClassificationNeuralNetwork, ClassificationNeuralNetwork, CompactClassificationNeuralNetwork.margin, CompactClassificationNeuralNetwork.edge, CompactClassificationNeuralNetwork.predict # name: # type: sq_string # elements: 1 # length: 59 Classification loss of a compact neural network classifier. # name: # type: sq_string # elements: 1 # length: 41 CompactClassificationNeuralNetwork.margin # name: # type: sq_string # elements: 1 # length: 1077 CompactClassificationNeuralNetwork: m = margin (obj, X, Y) CompactClassificationNeuralNetwork: m = margin (obj, Tbl, ResponseVarName) CompactClassificationNeuralNetwork: m = margin (obj, Tbl) Classification margin of a compact neural network classifier. m = margin (obj, X, Y) returns a column vector holding, for each row of X, the score the model gives its true class in Y less the largest score it gives any other class. A positive margin means the observation is classified correctly, and the larger it is the more confidently so. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: CompactClassificationNeuralNetwork, ClassificationNeuralNetwork, CompactClassificationNeuralNetwork.edge, CompactClassificationNeuralNetwork.loss, CompactClassificationNeuralNetwork.predict # name: # type: sq_string # elements: 1 # length: 61 Classification margin of a compact neural network classifier. # name: # type: sq_string # elements: 1 # length: 42 CompactClassificationNeuralNetwork.predict # name: # type: sq_string # elements: 1 # length: 1526 CompactClassificationNeuralNetwork: label = predict (obj, XC) CompactClassificationNeuralNetwork: [label, score] = predict (obj, XC) Classify new data points into categories using the neural network classification model from a CompactClassificationNeuralNetwork object. label = predict (obj, XC) returns the vector of labels predicted for the corresponding instances in XC, using the neural network model stored in the CompactClassificationNeuralNetwork model, obj. obj must be a CompactClassificationNeuralNetwork class object. XC must be an MxP numeric matrix with the same number of features P as the corresponding predictors of the neural network model in obj. [label, score] = predict (obj, XC) also returns score, which contains the predicted class scores or posterior probabilities for each instance of the corresponding unique classes. The score matrix contains the classification scores for each class. For each observation in XC, the predicted class label is the one with the highest score among all classes. If the ScoreTransform property is set to a transformation function, the scores are transformed accordingly before being returned. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: CompactClassificationNeuralNetwork, ClassificationNeuralNetwork, fitcnet # name: # type: sq_string # elements: 1 # length: 136 Classify new data points into categories using the neural network classification model from a CompactClassificationNeuralNetwork object. # name: # type: sq_string # elements: 1 # length: 44 CompactClassificationNeuralNetwork.savemodel # name: # type: sq_string # elements: 1 # length: 519 CompactClassificationNeuralNetwork: savemodel (obj, filename) Save a CompactClassificationNeuralNetwork object. savemodel (obj, filename) saves each property of a CompactClassificationNeuralNetwork object into an Octave binary file, the name of which is specified in filename, along with an extra variable, which defines the type classification object these variables constitute. Use loadmodel in order to load a classification object into Octave's workspace. See also: loadmodel, fitcnet, ClassificationNeuralNetwork # name: # type: sq_string # elements: 1 # length: 49 Save a CompactClassificationNeuralNetwork object. # name: # type: sq_string # elements: 1 # length: 24 CompactClassificationSVM # name: # type: sq_string # elements: 1 # length: 704 statistics: CompactClassificationSVM Compact Support Vector Machine classification The CompactClassificationSVM class implements a compact version of a Support Vector Machine classifier object for one-class or two-class problems, which can predict responses for new data using the predict method. A CompactClassificationSVM object is a compact version of a support vector machine model, ClassificationSVM. It does not include the training data resulting in a smaller classifier size, which can be used for making predictions from new data, but not for tasks such as cross validation. It can only be created from a ClassificationSVM model by using the compact object method. See also: ClassificationSVM # name: # type: sq_string # elements: 1 # length: 45 Compact Support Vector Machine classification # name: # type: sq_string # elements: 1 # length: 30 CompactClassificationSVM.Alpha # name: # type: sq_string # elements: 1 # length: 352 CompactClassificationSVM: property Alpha Trained classifier coefficients The coefficients of the trained SVM classifier specified as an sx1 numeric vector, where s is the number of support vectors, rows (obj.SupportVectors). If the SVM classifier was trained with a kernel function other than 'linear', then Alpha is empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Trained classifier coefficients # name: # type: sq_string # elements: 1 # length: 29 CompactClassificationSVM.Beta # name: # type: sq_string # elements: 1 # length: 579 CompactClassificationSVM: property Beta Linear predictor coefficients The linear predictor coefficients specified as a px1 numeric vector, where p is the number of predictors. Beta is the primal representation of the fitted hyperplane and exists only when the SVM classifier was trained with a 'linear' kernel function; for any other kernel there is no such representation and Beta is empty. It equals (obj.SupportVectors / s)' * (obj.Alpha .* obj.SupportVectorLabels), where s is the kernel scale, and a score is (x / s) * Beta + Bias, as in MATLAB. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Linear predictor coefficients # name: # type: sq_string # elements: 1 # length: 29 CompactClassificationSVM.Bias # name: # type: sq_string # elements: 1 # length: 115 CompactClassificationSVM: property Bias Bias term The bias term specified as a scalar. This property is read-only. # name: # type: sq_string # elements: 1 # length: 9 Bias term # name: # type: sq_string # elements: 1 # length: 46 CompactClassificationSVM.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 235 CompactClassificationSVM: property CategoricalPredictors Indices of the categorical predictors A numeric vector of column indices into X naming the predictors treated as categorical, and empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 35 CompactClassificationSVM.ClassNames # name: # type: sq_string # elements: 1 # length: 349 CompactClassificationSVM: property ClassNames Names of classes in the response variable An array of unique values of the response variable Y, which has the same data types as the data in Y. This property is read-only. ClassNames can have any of the following datatypes: Cell array of character vectors Character array Logical vector Numeric vector # name: # type: sq_string # elements: 1 # length: 41 Names of classes in the response variable # name: # type: sq_string # elements: 1 # length: 49 CompactClassificationSVM.CompactClassificationSVM # name: # type: sq_string # elements: 1 # length: 426 CompactClassificationSVM: obj = CompactClassificationSVM (Mdl) CompactClassificationSVM: obj = CompactClassificationSVM () Create a CompactClassificationSVM object. Mdl is the ClassificationSVM object to compact. The documented way to reach this constructor is the compact method. Called with no arguments it returns an object with its properties empty, which is how a saved model is rebuilt before its values are filled in. # name: # type: sq_string # elements: 1 # length: 41 Create a CompactClassificationSVM object. # name: # type: sq_string # elements: 1 # length: 29 CompactClassificationSVM.Cost # name: # type: sq_string # elements: 1 # length: 200 CompactClassificationSVM: property Cost Cost of misclassification A numeric square matrix, where Cost(i,j) is the cost of classifying an observation of class i as class j. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Cost of misclassification # name: # type: sq_string # elements: 1 # length: 47 CompactClassificationSVM.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 268 CompactClassificationSVM: property ExpandedPredictorNames Names of the predictors as the model expanded them A cell array of character vectors. It matches PredictorNames unless a categorical predictor was expanded into indicator variables. This property is read-only. # name: # type: sq_string # elements: 1 # length: 50 Names of the predictors as the model expanded them # name: # type: sq_string # elements: 1 # length: 41 CompactClassificationSVM.KernelParameters # name: # type: sq_string # elements: 1 # length: 313 CompactClassificationSVM: property KernelParameters Parameters of the kernel function A structure with fields Function and Scale, and Order for a polynomial kernel. Function names the kernel as MATLAB names it, so a radial basis kernel reports 'gaussian' whichever spelling was given. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Parameters of the kernel function # name: # type: sq_string # elements: 1 # length: 27 CompactClassificationSVM.Mu # name: # type: sq_string # elements: 1 # length: 256 CompactClassificationSVM: property Mu Predictor means A numeric vector of the same length as the columns in X containing the means of predictor variables. If the predictor variables have not been standardized, then Mu is empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Predictor means # name: # type: sq_string # elements: 1 # length: 38 CompactClassificationSVM.NumPredictors # name: # type: sq_string # elements: 1 # length: 216 CompactClassificationSVM: property NumPredictors Number of predictors A positive integer value specifying the number of predictors in the training dataset used for training the SVM model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of predictors # name: # type: sq_string # elements: 1 # length: 39 CompactClassificationSVM.PredictorNames # name: # type: sq_string # elements: 1 # length: 263 CompactClassificationSVM: property PredictorNames Names of predictor variables A cell array of character vectors specifying the names of the predictor variables. The names are in the order in which they appear in the training dataset. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Names of predictor variables # name: # type: sq_string # elements: 1 # length: 30 CompactClassificationSVM.Prior # name: # type: sq_string # elements: 1 # length: 195 CompactClassificationSVM: property Prior Prior probabilities of the classes A numeric row vector with one entry per class, in the order of ClassNames, summing to one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Prior probabilities of the classes # name: # type: sq_string # elements: 1 # length: 37 CompactClassificationSVM.ResponseName # name: # type: sq_string # elements: 1 # length: 166 CompactClassificationSVM: property ResponseName Response variable name A character vector specifying the name of the response variable Y. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Response variable name # name: # type: sq_string # elements: 1 # length: 39 CompactClassificationSVM.ScoreTransform # name: # type: sq_string # elements: 1 # length: 1251 CompactClassificationSVM: property ScoreTransform Transformation function for classification scores Specified as a function handle for transforming the classification scores. Add or change the ScoreTransform property using dot notation as in: obj.ScoreTransform = 'function_name' obj.ScoreTransform = @function_handle When specified as a character vector, it can be any of the following built-in functions. Nevertheless, the ScoreTransform property always stores their function handle equivalent. Value Description 'doublelogit' 1 ./ (1 + exp (-2 x x)) 'invlogit' log (x ./ (1 - x)) 'ismax' Sets the score for the class with the largest score to 1, and for all other classes to 0 'logit' 1 ./ (1 + exp (-x)) 'none' x (no transformation) 'identity' x (no transformation) 'sign' -1 for x < 0, 0 for x = 0, 1 for x > 0 'symmetric' 2 x x - 1 'symmetricismax' Sets the score for the class with the largest score to 1, and for all other classes to -1 'symmetriclogit' 2 ./ (1 + exp (-x)) - 1 fitPosterior stores the transformation from scores to posterior probabilities it fits as '@(S)sigmoid(S,A,B)', '@(S)step(S,LowerBound,UpperBound,PositiveClassProbability)' or '@(S)constant(S,K)'. Such a text may also be assigned directly, which MATLAB refuses. # name: # type: sq_string # elements: 1 # length: 49 Transformation function for classification scores # name: # type: sq_string # elements: 1 # length: 30 CompactClassificationSVM.Sigma # name: # type: sq_string # elements: 1 # length: 290 CompactClassificationSVM: property Sigma Predictor standard deviations A numeric vector of the same length as the columns in X containing the standard deviations of predictor variables. If the predictor variables have not been standardized, then Sigma is empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Predictor standard deviations # name: # type: sq_string # elements: 1 # length: 44 CompactClassificationSVM.SupportVectorLabels # name: # type: sq_string # elements: 1 # length: 539 CompactClassificationSVM: property SupportVectorLabels Support vector class labels The support vector class labels specified as an sx1 numeric vector, where s is the number of support vectors, rows (obj.SupportVectors). A value of +1 in SupportVectorLabels indicates that the corresponding support vector belongs to the positive class (ClassNames{2}). A value of -1 indicates that the corresponding support vector belongs to the negative class (ClassNames{1}). A one-class model labels every support vector +1. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Support vector class labels # name: # type: sq_string # elements: 1 # length: 39 CompactClassificationSVM.SupportVectors # name: # type: sq_string # elements: 1 # length: 312 CompactClassificationSVM: property SupportVectors Support vectors The support vectors of the trained SVM classifier specified an sxp numeric matrix, where s is the number of support vectors, rows (obj.SupportVectors), and p is the number of predictor variables in the predictor data. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Support vectors # name: # type: sq_string # elements: 1 # length: 46 CompactClassificationSVM.discardSupportVectors # name: # type: sq_string # elements: 1 # length: 625 CompactClassificationSVM: obj = discardSupportVectors (obj) Discard the support vectors of a linear SVM model. obj = discardSupportVectors (obj) empties Alpha, SupportVectors and SupportVectorLabels, leaving Beta and Bias to decide every prediction. A linear kernel needs nothing else, so the returned model predicts what it predicted before while carrying one vector in place of many. The kernel must be linear. Under any other the support vectors are part of the decision function and cannot be dropped. Discarding twice is not an error and changes nothing. See also: fitcsvm, ClassificationSVM, CompactClassificationSVM # name: # type: sq_string # elements: 1 # length: 50 Discard the support vectors of a linear SVM model. # name: # type: sq_string # elements: 1 # length: 29 CompactClassificationSVM.edge # name: # type: sq_string # elements: 1 # length: 1033 CompactClassificationSVM: e = edge (obj, X, Y) CompactClassificationSVM: e = edge (..., "Weights", w) CompactClassificationSVM: e = edge (obj, Tbl, ResponseVarName) CompactClassificationSVM: e = edge (obj, Tbl) Classification edge, the mean of the classification margins. e = edge (obj, X, Y) reduces the vector that margin returns to a single number, the mean margin over the rows of X. It says how far the model puts the true class ahead of its nearest rival on average, so a larger edge is a better model, and unlike a loss it is not bounded above and rewards confidence rather than bare correctness. e = edge (..., "Weights", w) takes the weighted mean instead, with one weight per row of X. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. # name: # type: sq_string # elements: 1 # length: 60 Classification edge, the mean of the classification margins. # name: # type: sq_string # elements: 1 # length: 37 CompactClassificationSVM.fitPosterior # name: # type: sq_string # elements: 1 # length: 2222 CompactClassificationSVM: ScoreSVMModel = fitPosterior (obj, X, Y) CompactClassificationSVM: ScoreSVMModel = fitPosterior (obj, Tbl, ResponseVarName) CompactClassificationSVM: ScoreSVMModel = fitPosterior (obj, Tbl, Y) CompactClassificationSVM: ScoreSVMModel = fitPosterior (obj, Tbl) CompactClassificationSVM: [ScoreSVMModel, ScoreTransform] = fitPosterior (...) Fit the transformation from scores to posterior probabilities. ScoreSVMModel = fitPosterior (obj, X, Y) returns the two-class model obj with the transformation from the score of the second class, obj.ClassNames(2), to its posterior probability stored in its ScoreTransform property, so that predict returns posterior probabilities, the two columns summing to 1. A compact model holds no training data, so the transformation is fitted to the scores it gives the predictors X, whose classes are Y, without cross-validation; data the model was not trained on give the better estimate. Rows whose score is NaN are left out. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. The classes are then the variable ResponseVarName names, Y given beside the table, or the variable the model was fitted on. The transformation is a sigmoid 1 / (1 + exp (A * s + B)) of the score s when the scores of the two classes overlap, fitted by maximum likelihood without the prior or any weights; a step function when the scores separate the classes, 0 below the largest score of the first class, 1 above the smallest score of the second and obj.Prior(2) between, with a warning; and a constant, posterior 1 for the other class, when one class has prior probability 0. A ScoreTransform already set on obj is reset to 'none' first, with a warning. The property holds the transformation as ClassificationSVM.fitPosterior describes. [ScoreSVMModel, ScoreTransform] = fitPosterior (...) also returns the parameters in a structure whose field Type is 'sigmoid', with fields Slope and Intercept, 'step', with fields LowerBound, UpperBound and PositiveClassProbability, or 'constant', with the field PredictedClass. See also: fitSVMPosterior, ClassificationSVM.fitPosterior, CompactClassificationSVM.predict # name: # type: sq_string # elements: 1 # length: 62 Fit the transformation from scores to posterior probabilities. # name: # type: sq_string # elements: 1 # length: 29 CompactClassificationSVM.loss # name: # type: sq_string # elements: 1 # length: 2192 CompactClassificationSVM: L = loss (obj, X, Y) CompactClassificationSVM: L = loss (obj, Tbl, ResponseVarName) CompactClassificationSVM: L = loss (obj, Tbl) CompactClassificationSVM: L = loss (..., name, value) Compute loss for a trained CompactClassificationSVM object. L = loss (obj, X, Y) computes the loss, L, using the default loss function 'classiferror'. obj is a CompactClassificationSVM object. X must be a NxP numeric matrix of input data where rows correspond to observations and columns correspond to features or variables. Y is Nx1 matrix or cell matrix containing the class labels of corresponding predictor data in X. Y must have same numbers of Rows as X. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. L = loss (..., name, value) allows additional options specified by name-value pairs: Name Value 'LossFun' Specifies the loss function to use. Can be a function handle with four input arguments (C, S, W, Cost) which returns a scalar value or one of: 'binodeviance', 'classifcost', 'classiferror', 'exponential', 'hinge', 'logit','mincost', 'quadratic'. C is a logical matrix of size NxK, where N is the number of observations and K is the number of classes. The element C(i,j) is true if the class label of the i-th observation is equal to the j-th class. S is a numeric matrix of size NxK, where each element represents the classification score for the corresponding class. W is a numeric vector of length N, representing the observation weights. Cost is a KxK matrix representing the misclassification costs. 'Weights' Specifies observation weights, must be a numeric vector of length equal to the number of rows in X. Default is ones (size (X, 1)). loss normalizes the weights so that observation weights in each class sum to the prior probability of that class. When you supply Weights, loss computes the weighted classification loss. See also: CompactClassificationSVM # name: # type: sq_string # elements: 1 # length: 59 Compute loss for a trained CompactClassificationSVM object. # name: # type: sq_string # elements: 1 # length: 31 CompactClassificationSVM.margin # name: # type: sq_string # elements: 1 # length: 1236 CompactClassificationSVM: m = margin (obj, X, Y) CompactClassificationSVM: m = margin (obj, Tbl, ResponseVarName) CompactClassificationSVM: m = margin (obj, Tbl) Classification margins for Support Vector Machine classifier. m = margin (obj, X, Y) returns the classification margins for obj with data X and classification Y. m is a numeric vector of length size (X,1). obj is a CompactClassificationSVM object. X must be a NxP numeric matrix of input data where rows correspond to observations and columns correspond to features or variables. Y is Nx1 matrix or cell matrix containing the class labels of corresponding predictor data in X. Y must have same numbers of Rows as X. The classification margin for each observation is the difference between the classification score for the true class and the maximal classification score for the false classes. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: CompactClassificationSVM # name: # type: sq_string # elements: 1 # length: 61 Classification margins for Support Vector Machine classifier. # name: # type: sq_string # elements: 1 # length: 32 CompactClassificationSVM.predict # name: # type: sq_string # elements: 1 # length: 1958 CompactClassificationSVM: label = predict (obj, XC) CompactClassificationSVM: [label, score] = predict (obj, XC) CompactClassificationSVM: [label, score, cost] = predict (obj, XC) Classify new data points into categories using the Support Vector Machine classification model from a CompactClassificationSVM object. label = predict (obj, XC) returns the vector of labels predicted for the corresponding instances in XC, using the predictor data in the CompactClassificationSVM model, obj. For one-class SVM model, +1 or -1 is returned. obj must be a CompactClassificationSVM class object. XC must be an MxP numeric matrix with the same number of features P as the corresponding predictors of the SVM model in obj. [label, score] = predict (obj, XC) also returns score, which contains the decision values for each prediction. A ScoreTransform assigned to obj is applied to them, so score holds whatever that transform returns. Posterior probabilities need a transform fitted to the model, which this package does not compute yet. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. Deviation from MATLAB. cost is the expected cost of each assignment, sum_j P(j) Cost(j,k). An SVM score is a signed distance to the boundary and not a posterior, so the only distribution available is the one concentrated on the predicted class and cost is the row of Cost belonging to it. MATLAB returns the column instead, which is the same matrix read the wrong way and contradicts its own ClassificationKNN, ClassificationDiscriminant and ClassificationNaiveBayes on any asymmetric cost matrix; the two agree wherever Cost is symmetric, the default included. Measured on R2024a. See also: CompactClassificationSVM, ClassificationSVM # name: # type: sq_string # elements: 1 # length: 134 Classify new data points into categories using the Support Vector Machine classification model from a CompactClassificationSVM object. # name: # type: sq_string # elements: 1 # length: 34 CompactClassificationSVM.savemodel # name: # type: sq_string # elements: 1 # length: 496 CompactClassificationSVM: savemodel (obj, filename) Save a CompactClassificationSVM object. savemodel (obj, filename) saves each property of a CompactClassificationSVM object into an Octave binary file, the name of which is specified in filename, along with an extra variable, which defines the type classification object these variables constitute. Use loadmodel in order to load a classification object into Octave's workspace. See also: loadmodel, ClassificationSVM, CompactClassificationSVM # name: # type: sq_string # elements: 1 # length: 39 Save a CompactClassificationSVM object. # name: # type: sq_string # elements: 1 # length: 25 CompactClassificationTree # name: # type: sq_string # elements: 1 # length: 709 statistics: CompactClassificationTree Compact binary decision tree for classification A CompactClassificationTree object carries the tree a ClassificationTree model grew and everything predict needs, but not the observations it was fitted on. It classifies new data identically to the model it came from, and is far smaller to keep or to ship. Create one with the compact method of a ClassificationTree object. Because it holds no training data, it has no resub methods and cannot be cross-validated, and it cannot be pruned: the pruning sequence is reported but taking a subtree out of it rewrites the node table, which is work for the model that still has its data. See also: ClassificationTree, fitctree # name: # type: sq_string # elements: 1 # length: 47 Compact binary decision tree for classification # name: # type: sq_string # elements: 1 # length: 47 CompactClassificationTree.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 229 CompactClassificationTree: property CategoricalPredictors Indices of the categorical predictors A row vector of column indices into X, naming the predictors treated as categorical, empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 42 CompactClassificationTree.CategoricalSplit # name: # type: sq_string # elements: 1 # length: 278 CompactClassificationTree: property CategoricalSplit Categorical splits of the tree A Nx2 cell array with one row per categorical split, in node order, holding the levels sent left and the levels sent right. It is empty when no split is categorical. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 Categorical splits of the tree # name: # type: sq_string # elements: 1 # length: 34 CompactClassificationTree.Children # name: # type: sq_string # elements: 1 # length: 207 CompactClassificationTree: property Children Child nodes of each node A NumNodesx2 matrix naming the left and the right child of each node. A leaf carries a zero in both columns. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Child nodes of each node # name: # type: sq_string # elements: 1 # length: 36 CompactClassificationTree.ClassCount # name: # type: sq_string # elements: 1 # length: 257 CompactClassificationTree: property ClassCount Class counts at each node A NumNodesxK matrix holding how many training observations of each class reached each node. These are counts and take no notice of the observation weights. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Class counts at each node # name: # type: sq_string # elements: 1 # length: 36 CompactClassificationTree.ClassNames # name: # type: sq_string # elements: 1 # length: 173 CompactClassificationTree: property ClassNames Names of the classes The distinct class labels, in the type the response was given in and sorted. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Names of the classes # name: # type: sq_string # elements: 1 # length: 42 CompactClassificationTree.ClassProbability # name: # type: sq_string # elements: 1 # length: 294 CompactClassificationTree: property ClassProbability Class probabilities at each node A NumNodesxK matrix holding, for each node, the weight of each class among the observations that reached it, as a proportion of the node's total weight. The root row is the prior. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Class probabilities at each node # name: # type: sq_string # elements: 1 # length: 51 CompactClassificationTree.CompactClassificationTree # name: # type: sq_string # elements: 1 # length: 265 CompactClassificationTree: obj = CompactClassificationTree (Mdl) Create a CompactClassificationTree object. Mdl is the ClassificationTree object to compact. The documented way to reach this constructor is the compact method. See also: ClassificationTree, fitctree # name: # type: sq_string # elements: 1 # length: 42 Create a CompactClassificationTree object. # name: # type: sq_string # elements: 1 # length: 30 CompactClassificationTree.Cost # name: # type: sq_string # elements: 1 # length: 624 CompactClassificationTree: property Cost Misclassification cost A square numeric matrix with one row and column per class, where Cost(i,j) is the cost of classifying an observation of class i into class j. It defaults to 1 - eye (K) and may be reassigned after fitting. Reassigning it re-derives NodeClass, NodeError and NodeRisk, and changes what predict answers. The shape of the tree does not follow, having been decided by the cost in force when it was grown. MATLAB reports this property read-only on a compact tree and refuses the assignment; it is settable here, as it is on the package's other compact classifiers. # name: # type: sq_string # elements: 1 # length: 22 Misclassification cost # name: # type: sq_string # elements: 1 # length: 39 CompactClassificationTree.CutCategories # name: # type: sq_string # elements: 1 # length: 264 CompactClassificationTree: property CutCategories Categories used at each branch A NumNodesx2 cell array holding, for a node that cuts a categorical predictor, the levels sent left and the levels sent right. Every other entry is empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 Categories used at each branch # name: # type: sq_string # elements: 1 # length: 34 CompactClassificationTree.CutPoint # name: # type: sq_string # elements: 1 # length: 316 CompactClassificationTree: property CutPoint Cut point of each node A column vector holding, for each node, the value the split compares the predictor against: an observation goes left when its value is less than the cut point and right otherwise. A leaf and a categorical cut carry NaN. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Cut point of each node # name: # type: sq_string # elements: 1 # length: 38 CompactClassificationTree.CutPredictor # name: # type: sq_string # elements: 1 # length: 270 CompactClassificationTree: property CutPredictor Name of the predictor each node cuts on A cell array of character vectors with one entry per node, holding the name of the predictor the node splits on and an empty character vector at a leaf. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Name of the predictor each node cuts on # name: # type: sq_string # elements: 1 # length: 43 CompactClassificationTree.CutPredictorIndex # name: # type: sq_string # elements: 1 # length: 219 CompactClassificationTree: property CutPredictorIndex Index of the predictor each node cuts on A column vector holding, for each node, the column of X the node splits on, and zero at a leaf. This property is read-only. # name: # type: sq_string # elements: 1 # length: 40 Index of the predictor each node cuts on # name: # type: sq_string # elements: 1 # length: 33 CompactClassificationTree.CutType # name: # type: sq_string # elements: 1 # length: 302 CompactClassificationTree: property CutType Type of cut at each node A cell array of character vectors holding 'continuous' at a branch node that cuts a numeric predictor at a point, 'categorical' at one that splits a set of levels, and an empty character vector at a leaf. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Type of cut at each node # name: # type: sq_string # elements: 1 # length: 48 CompactClassificationTree.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 316 CompactClassificationTree: property ExpandedPredictorNames Expanded predictor names A cell array of character vectors. It differs from PredictorNames only when a categorical predictor has been expanded into one column per level, which this class does not do, so the two are always equal. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Expanded predictor names # name: # type: sq_string # elements: 1 # length: 38 CompactClassificationTree.IsBranchNode # name: # type: sq_string # elements: 1 # length: 196 CompactClassificationTree: property IsBranchNode Which nodes are branch nodes A logical column vector, true for each node that carries a split and false for each leaf. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Which nodes are branch nodes # name: # type: sq_string # elements: 1 # length: 35 CompactClassificationTree.NodeClass # name: # type: sq_string # elements: 1 # length: 360 CompactClassificationTree: property NodeClass Class assigned to each node A cell array of character vectors naming, for each node, the class of least expected misclassification cost given the node's class probabilities. Under the default cost that is simply the most probable class, with the first of the class names kept on a tie. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Class assigned to each node # name: # type: sq_string # elements: 1 # length: 35 CompactClassificationTree.NodeError # name: # type: sq_string # elements: 1 # length: 340 CompactClassificationTree: property NodeError Misclassification cost of each node A column vector holding, for each node, the expected misclassification cost of the class the node is assigned. Under the default cost that is the probability that the node's class is wrong, one less the largest class probability. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Misclassification cost of each node # name: # type: sq_string # elements: 1 # length: 41 CompactClassificationTree.NodeProbability # name: # type: sq_string # elements: 1 # length: 294 CompactClassificationTree: property NodeProbability Probability of reaching each node A column vector holding, for each node, the total weight of the observations that reached it, the weights being those the model it came from was fitted with. The root carries one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Probability of reaching each node # name: # type: sq_string # elements: 1 # length: 34 CompactClassificationTree.NodeRisk # name: # type: sq_string # elements: 1 # length: 492 CompactClassificationTree: property NodeRisk Risk of each node A column vector holding, for each node, the impurity of the node weighted by the probability of reaching it, measured by whichever of 'gdi' and 'deviance' the tree was grown under. A non-default Cost enters here rather than through the class probabilities: the weights are scaled class by class by the total cost of misclassifying that class, and the impurity is measured on the scaled distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 17 Risk of each node # name: # type: sq_string # elements: 1 # length: 34 CompactClassificationTree.NodeSize # name: # type: sq_string # elements: 1 # length: 338 CompactClassificationTree: property NodeSize Number of observations at each node A column vector holding how many training observations reached each node. A row missing the predictor its node cuts on is counted at that node and at none below it, so a parent's size is not in general the sum of its children's. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Number of observations at each node # name: # type: sq_string # elements: 1 # length: 34 CompactClassificationTree.NumNodes # name: # type: sq_string # elements: 1 # length: 302 CompactClassificationTree: property NumNodes Number of nodes in the tree A positive integer, the number of nodes the tree holds, branch nodes and leaves together. Nodes are numbered as they are created, so a parent always carries a lower number than either of its children. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Number of nodes in the tree # name: # type: sq_string # elements: 1 # length: 32 CompactClassificationTree.Parent # name: # type: sq_string # elements: 1 # length: 164 CompactClassificationTree: property Parent Parent of each node A column vector naming the parent of each node. The root carries a zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Parent of each node # name: # type: sq_string # elements: 1 # length: 40 CompactClassificationTree.PredictorNames # name: # type: sq_string # elements: 1 # length: 177 CompactClassificationTree: property PredictorNames Names of the predictor variables A cell array of character vectors with one name per column of X. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Names of the predictor variables # name: # type: sq_string # elements: 1 # length: 31 CompactClassificationTree.Prior # name: # type: sq_string # elements: 1 # length: 662 CompactClassificationTree: property Prior Prior probability of each class A numeric row vector with one element per class, summing to one. It defaults to the weight each class carries in the training data, and may be reassigned after fitting. Reassigning it re-derives every node statistic that depends on the class weights, so ClassProbability, NodeProbability, NodeClass, NodeError and NodeRisk all follow. The shape of the tree does not, having been decided by the prior in force when it was grown. MATLAB reports this property read-only on a compact tree and refuses the assignment; it is settable here, as it is on the package's other compact classifiers. # name: # type: sq_string # elements: 1 # length: 31 Prior probability of each class # name: # type: sq_string # elements: 1 # length: 36 CompactClassificationTree.PruneAlpha # name: # type: sq_string # elements: 1 # length: 372 CompactClassificationTree: property PruneAlpha Cost complexity parameter of each pruning level A column vector with one element per level of the pruning sequence, the first of which is zero and stands for the unpruned tree. Level L is the smallest subtree that is optimal for every complexity parameter from PruneAlpha(L+1) up to the next one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 47 Cost complexity parameter of each pruning level # name: # type: sq_string # elements: 1 # length: 35 CompactClassificationTree.PruneList # name: # type: sq_string # elements: 1 # length: 444 CompactClassificationTree: property PruneList Pruning level of each node A column vector holding, for each branch node, the level of the cost complexity sequence at which it stops being a branch node, and zero at a leaf. Pruning the tree to level L turns every node whose level is between one and L into a leaf. It is empty when neither 'Prune' nor 'MergeLeaves' was asked for, since no sequence was then estimated. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Pruning level of each node # name: # type: sq_string # elements: 1 # length: 38 CompactClassificationTree.ResponseName # name: # type: sq_string # elements: 1 # length: 147 CompactClassificationTree: property ResponseName Name of the response variable A character vector naming the response. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 40 CompactClassificationTree.ScoreTransform # name: # type: sq_string # elements: 1 # length: 233 CompactClassificationTree: property ScoreTransform Transform applied to the scores A character vector naming the function predict applies to the class probabilities before returning them, or a function handle. The default is 'none'. # name: # type: sq_string # elements: 1 # length: 31 Transform applied to the scores # name: # type: sq_string # elements: 1 # length: 48 CompactClassificationTree.SurrogateCutCategories # name: # type: sq_string # elements: 1 # length: 185 CompactClassificationTree: property SurrogateCutCategories Categories of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Categories of the surrogate splits # name: # type: sq_string # elements: 1 # length: 42 CompactClassificationTree.SurrogateCutFlip # name: # type: sq_string # elements: 1 # length: 184 CompactClassificationTree: property SurrogateCutFlip Cut assignments of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Cut assignments of the surrogate splits # name: # type: sq_string # elements: 1 # length: 43 CompactClassificationTree.SurrogateCutPoint # name: # type: sq_string # elements: 1 # length: 180 CompactClassificationTree: property SurrogateCutPoint Cut points of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Cut points of the surrogate splits # name: # type: sq_string # elements: 1 # length: 47 CompactClassificationTree.SurrogateCutPredictor # name: # type: sq_string # elements: 1 # length: 184 CompactClassificationTree: property SurrogateCutPredictor Predictors of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Predictors of the surrogate splits # name: # type: sq_string # elements: 1 # length: 42 CompactClassificationTree.SurrogateCutType # name: # type: sq_string # elements: 1 # length: 174 CompactClassificationTree: property SurrogateCutType Types of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Types of the surrogate splits # name: # type: sq_string # elements: 1 # length: 55 CompactClassificationTree.SurrogatePredictorAssociation # name: # type: sq_string # elements: 1 # length: 216 CompactClassificationTree: property SurrogatePredictorAssociation Predictive measures of association of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 58 Predictive measures of association of the surrogate splits # name: # type: sq_string # elements: 1 # length: 30 CompactClassificationTree.edge # name: # type: sq_string # elements: 1 # length: 954 CompactClassificationTree: e = edge (obj, X, Y) CompactClassificationTree: e = edge (..., 'Weights', w) CompactClassificationTree: e = edge (obj, Tbl, ResponseVarName) CompactClassificationTree: e = edge (obj, Tbl) Classification edge on new data. e = edge (obj, X, Y) returns the weighted mean of the margins, a single number summarising how confidently the model classifies the data. The weights are normalized within each class to that class's prior before they are applied. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. edge (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and edge (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: CompactClassificationTree, CompactClassificationTree.margin, CompactClassificationTree.loss, CompactClassificationTree.predict # name: # type: sq_string # elements: 1 # length: 32 Classification edge on new data. # name: # type: sq_string # elements: 1 # length: 30 CompactClassificationTree.loss # name: # type: sq_string # elements: 1 # length: 1068 CompactClassificationTree: l = loss (obj, X, Y) CompactClassificationTree: l = loss (obj, Tbl, ResponseVarName) CompactClassificationTree: l = loss (obj, Tbl) CompactClassificationTree: l = loss (..., name, value) Classification loss on new data. l = loss (obj, X, Y) returns the minimum expected misclassification cost. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. l = loss (..., name, value) takes the following options. Name Value 'LossFun' One of 'binodeviance', 'classifcost', 'classiferror', 'exponential', 'hinge', 'logit', 'mincost' (default) or 'quadratic'. 'Weights' A numeric vector of observation weights, one per row of X. See also: CompactClassificationTree, CompactClassificationTree.margin, CompactClassificationTree.edge, CompactClassificationTree.predict # name: # type: sq_string # elements: 1 # length: 32 Classification loss on new data. # name: # type: sq_string # elements: 1 # length: 32 CompactClassificationTree.margin # name: # type: sq_string # elements: 1 # length: 964 CompactClassificationTree: m = margin (obj, X, Y) CompactClassificationTree: m = margin (obj, Tbl, ResponseVarName) CompactClassificationTree: m = margin (obj, Tbl) Classification margin on new data. m = margin (obj, X, Y) returns one margin per observation: the score the model gives the observation's true class, less the largest score it gives any other class. A positive margin means the observation is classified correctly, and a larger one means it is classified more confidently. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: CompactClassificationTree, CompactClassificationTree.edge, CompactClassificationTree.loss, CompactClassificationTree.predict # name: # type: sq_string # elements: 1 # length: 34 Classification margin on new data. # name: # type: sq_string # elements: 1 # length: 43 CompactClassificationTree.nodeVariableRange # name: # type: sq_string # elements: 1 # length: 499 CompactClassificationTree: r = nodeVariableRange (obj, node) Range of each predictor at a node. r = nodeVariableRange (obj, node) returns a structure with one field per predictor the path from the root to node cuts on, holding the two-element range of values that reach the node, or for a categorical predictor the levels that reach it. A predictor the path never cuts on is unconstrained and is left out, so the root gives a structure with no fields. See also: CompactClassificationTree, fitctree # name: # type: sq_string # elements: 1 # length: 34 Range of each predictor at a node. # name: # type: sq_string # elements: 1 # length: 33 CompactClassificationTree.predict # name: # type: sq_string # elements: 1 # length: 1476 CompactClassificationTree: label = predict (obj, XC) CompactClassificationTree: [label, score] = predict (...) CompactClassificationTree: [label, score, node] = predict (...) CompactClassificationTree: [label, score, node, cnum] = predict (...) Classify new data with a trained CompactClassificationTree object. label = predict (obj, XC) sends each row of XC down the tree and returns the class of the node it comes to rest at. XC must have as many columns as the predictor data the model was fitted on. XC may also be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. [label, score] = predict (...) also returns score, an NxK matrix holding the class probabilities of the node each row landed in, after ScoreTransform. [label, score, node] = predict (...) also returns the number of the node each row landed in, and [label, score, node, cnum] = predict (...) the index of the predicted class into ClassNames. The label is the class of least expected misclassification cost, which under the default Cost is the most probable class of the node. A row missing the predictor a node cuts on is stopped at that node and takes its answer, rather than being sent down a branch on evidence the row does not carry. See also: CompactClassificationTree, fitctree # name: # type: sq_string # elements: 1 # length: 66 Classify new data with a trained CompactClassificationTree object. # name: # type: sq_string # elements: 1 # length: 45 CompactClassificationTree.predictorImportance # name: # type: sq_string # elements: 1 # length: 685 CompactClassificationTree: imp = predictorImportance (obj) Estimate the importance of each predictor. imp = predictorImportance (obj) returns a row vector with one element per predictor, holding the total drop in risk the splits on that predictor bring about, divided by the number of branch nodes. A predictor the tree never splits on scores zero. The drop at a branch node is its NodeRisk less the risk of its two children, so a predictor that is chosen often, high up, and on nodes it separates well, scores highest. The numbers are comparable between predictors of one tree and not between trees. See also: CompactClassificationTree, fitctree, CompactClassificationTree.NodeRisk # name: # type: sq_string # elements: 1 # length: 42 Estimate the importance of each predictor. # name: # type: sq_string # elements: 1 # length: 35 CompactClassificationTree.savemodel # name: # type: sq_string # elements: 1 # length: 496 CompactClassificationTree: savemodel (obj, filename) Save a CompactClassificationTree model to a file. savemodel (obj, filename) saves each property of a CompactClassificationTree object into an Octave binary file, the name of which is specified in filename, along with an extra variable, which defines the type of classification object these variables constitute. Use loadmodel in order to load a classification object into Octave's workspace. See also: loadmodel, fitctree, ClassificationTree # name: # type: sq_string # elements: 1 # length: 49 Save a CompactClassificationTree model to a file. # name: # type: sq_string # elements: 1 # length: 30 CompactClassificationTree.view # name: # type: sq_string # elements: 1 # length: 408 CompactClassificationTree: view (obj) Print the tree as text. view (obj) prints one line per node: a branch node names the predictor it cuts on, the cut point, and the node each side leads to, and a leaf names the class it assigns. A branch node's line ends with the class it would assign itself, which is the answer an observation missing that predictor gets. See also: CompactClassificationTree, fitctree # name: # type: sq_string # elements: 1 # length: 23 Print the tree as text. # name: # type: sq_string # elements: 1 # length: 25 CompactRegressionEnsemble # name: # type: sq_string # elements: 1 # length: 512 statistics: CompactRegressionEnsemble Compact ensemble of regression trees A CompactRegressionEnsemble object carries the trained trees of a boosted or bagged regression ensemble and what prediction needs, but not the observations it was fitted on. It predicts new data identically to the ensemble it came from, and trees can be removed from it. Create one with the compact method of a RegressionEnsemble or RegressionBaggedEnsemble object. See also: fitrensemble, RegressionEnsemble, RegressionBaggedEnsemble # name: # type: sq_string # elements: 1 # length: 36 Compact ensemble of regression trees # name: # type: sq_string # elements: 1 # length: 47 CompactRegressionEnsemble.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 189 CompactRegressionEnsemble: property CategoricalPredictors Indices of categorical predictors The predictors every tree treats as categorical, empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Indices of categorical predictors # name: # type: sq_string # elements: 1 # length: 40 CompactRegressionEnsemble.CombineWeights # name: # type: sq_string # elements: 1 # length: 291 CompactRegressionEnsemble: property CombineWeights How the trees are combined 'WeightedSum' for LSBoost, whose prediction is the sum of each tree's prediction times its weight, or 'WeightedAverage' for Bag, whose prediction is the weighted average of its trees'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 How the trees are combined # name: # type: sq_string # elements: 1 # length: 51 CompactRegressionEnsemble.CompactRegressionEnsemble # name: # type: sq_string # elements: 1 # length: 253 CompactRegressionEnsemble: obj = CompactRegressionEnsemble (Mdl) Create a CompactRegressionEnsemble object. Mdl is the RegressionEnsemble or RegressionBaggedEnsemble object to compact. The documented way to reach this constructor is the compact method. # name: # type: sq_string # elements: 1 # length: 42 Create a CompactRegressionEnsemble object. # name: # type: sq_string # elements: 1 # length: 48 CompactRegressionEnsemble.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 164 CompactRegressionEnsemble: property ExpandedPredictorNames Names of the predictors as the learners saw them The same as PredictorNames. This property is read-only. # name: # type: sq_string # elements: 1 # length: 48 Names of the predictors as the learners saw them # name: # type: sq_string # elements: 1 # length: 36 CompactRegressionEnsemble.NumTrained # name: # type: sq_string # elements: 1 # length: 99 CompactRegressionEnsemble: property NumTrained Number of trained trees This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Number of trained trees # name: # type: sq_string # elements: 1 # length: 40 CompactRegressionEnsemble.PredictorNames # name: # type: sq_string # elements: 1 # length: 138 CompactRegressionEnsemble: property PredictorNames Names of the predictors A cell array of character vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 38 CompactRegressionEnsemble.ResponseName # name: # type: sq_string # elements: 1 # length: 107 CompactRegressionEnsemble: property ResponseName Name of the response variable This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 43 CompactRegressionEnsemble.ResponseTransform # name: # type: sq_string # elements: 1 # length: 204 CompactRegressionEnsemble: property ResponseTransform Transform applied to the predicted response 'none' (default), 'exp', 'log' or a function handle. Predictions and losses use the transformed response. # name: # type: sq_string # elements: 1 # length: 43 Transform applied to the predicted response # name: # type: sq_string # elements: 1 # length: 33 CompactRegressionEnsemble.Trained # name: # type: sq_string # elements: 1 # length: 140 CompactRegressionEnsemble: property Trained Trained trees A column cell array of CompactRegressionTree objects. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Trained trees # name: # type: sq_string # elements: 1 # length: 40 CompactRegressionEnsemble.TrainedWeights # name: # type: sq_string # elements: 1 # length: 143 CompactRegressionEnsemble: property TrainedWeights Weights of the trained trees A column with one weight per tree. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Weights of the trained trees # name: # type: sq_string # elements: 1 # length: 43 CompactRegressionEnsemble.UsePredForLearner # name: # type: sq_string # elements: 1 # length: 171 CompactRegressionEnsemble: property UsePredForLearner Which predictors each learner uses Always empty, as MATLAB returns it for tree learners. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Which predictors each learner uses # name: # type: sq_string # elements: 1 # length: 30 CompactRegressionEnsemble.loss # name: # type: sq_string # elements: 1 # length: 1505 CompactRegressionEnsemble: L = loss (obj, X, Y) CompactRegressionEnsemble: L = loss (obj, Tbl, ResponseVarName) CompactRegressionEnsemble: L = loss (obj, Tbl) CompactRegressionEnsemble: L = loss (..., name, value) Regression loss of a compact ensemble. L is the weighted mean squared error of the predictions for the rows of X against Y, the weights normalized to sum to one over the rows that have a response; a row whose response is missing is left out. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. Name-Value arguments: Name Value 'LossFun' 'mse' (default) or a function handle called as lossfun (Y, Yfit, W), with column vectors of the responses, the predictions and the normalized weights, returning a scalar. 'Mode' 'ensemble' (default) for one loss over the trees used, 'cumulative' for a column whose element j uses the first j of them, or 'individual' for a column with the loss of each on its own. 'Weights' A nonnegative vector with one weight per row. The default is uniform. 'Learners' and 'UseObsForLearner' are taken as by predict. A row that no tree may predict is left out and the weights are renormalized over the rest. See also: CompactRegressionEnsemble, CompactRegressionEnsemble.predict # name: # type: sq_string # elements: 1 # length: 38 Regression loss of a compact ensemble. # name: # type: sq_string # elements: 1 # length: 33 CompactRegressionEnsemble.predict # name: # type: sq_string # elements: 1 # length: 995 CompactRegressionEnsemble: yfit = predict (obj, X) CompactRegressionEnsemble: yfit = predict (..., name, value) Predict the response with a compact regression ensemble. yfit holds, for each row of X, the sum over the trees of each tree's prediction times its weight for LSBoost, or the weighted average of the trees' predictions for Bag, after ResponseTransform. A row that no tree may predict is NaN. Name-Value arguments: Name Value 'Learners' A vector of indices of the trees to use. The default is all of them. 'UseObsForLearner' An NxNumTrained logical matrix saying which tree may predict which row. The default lets every tree predict every row. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: CompactRegressionEnsemble, fitrensemble # name: # type: sq_string # elements: 1 # length: 56 Predict the response with a compact regression ensemble. # name: # type: sq_string # elements: 1 # length: 45 CompactRegressionEnsemble.predictorImportance # name: # type: sq_string # elements: 1 # length: 462 CompactRegressionEnsemble: imp = predictorImportance (obj) CompactRegressionEnsemble: [imp, ma] = predictorImportance (obj) Estimate the importance of each predictor. imp is a row vector with one element per predictor, the average over the trees of each tree's predictorImportance, weighted by TrainedWeights. ma, the predictive measure of association between the predictors, is empty, the trees growing no surrogate splits. See also: CompactRegressionEnsemble # name: # type: sq_string # elements: 1 # length: 42 Estimate the importance of each predictor. # name: # type: sq_string # elements: 1 # length: 40 CompactRegressionEnsemble.removeLearners # name: # type: sq_string # elements: 1 # length: 237 CompactRegressionEnsemble: C = removeLearners (obj, idx) Remove trees from a compact regression ensemble. C is the ensemble without the trees whose indices idx holds, their weights removed with them. See also: CompactRegressionEnsemble # name: # type: sq_string # elements: 1 # length: 48 Remove trees from a compact regression ensemble. # name: # type: sq_string # elements: 1 # length: 20 CompactRegressionGAM # name: # type: sq_string # elements: 1 # length: 1108 statistics: CompactRegressionGAM Compact generalized additive model regression The CompactRegressionGAM class implements a compact version of the generalized additive model regression object, which predicts responses for new data with the predict method but does not store the training data. A compact model consumes less memory than the full RegressionGAM model, but cannot perform tasks that need the training data, such as computing a resubstitution loss or the standard deviation of a prediction. Create a CompactRegressionGAM object by using the compact method on a RegressionGAM object. The engine that fitted the model is carried over in FitMethod, and the compact model predicts by the same scheme the full one did. Under 'boostedtrees', the default, the fit is described by TreeModel, BinEdges and PairDetectionBinEdges. Under 'splines' it is described by Formula, BaseModel, ModelwInt and IntMatrix, which MATLAB's compact model does not carry. Whichever fitted the model, the other set is empty. A standard deviation is available from the spline engine alone. See also: RegressionGAM, fitrgam # name: # type: sq_string # elements: 1 # length: 45 Compact generalized additive model regression # name: # type: sq_string # elements: 1 # length: 30 CompactRegressionGAM.BaseModel # name: # type: sq_string # elements: 1 # length: 307 CompactRegressionGAM: property BaseModel Model without interaction terms A structure holding the intercept, the piecewise polynomial of each predictor, the number of backfitting cycles, the residuals and the residual sum of squares of the model fitted without interaction terms. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Model without interaction terms # name: # type: sq_string # elements: 1 # length: 29 CompactRegressionGAM.BinEdges # name: # type: sq_string # elements: 1 # length: 139 CompactRegressionGAM: property BinEdges Bin edges of the fitted shape functions, empty under the spline engine. This property is read-only. # name: # type: sq_string # elements: 1 # length: 71 Bin edges of the fitted shape functions, empty under the spline engine. # name: # type: sq_string # elements: 1 # length: 42 CompactRegressionGAM.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 221 CompactRegressionGAM: property CategoricalPredictors Indices of the categorical predictors A numeric vector holding the column of each predictor treated as categorical, and empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 41 CompactRegressionGAM.CompactRegressionGAM # name: # type: sq_string # elements: 1 # length: 402 CompactRegressionGAM: obj = CompactRegressionGAM (Mdl) CompactRegressionGAM: obj = CompactRegressionGAM () Create a CompactRegressionGAM object. Mdl is the RegressionGAM object to compact. The documented way to reach this constructor is the compact method. Called with no arguments it returns an object with its properties empty, which is how a saved model is rebuilt before its values are filled in. # name: # type: sq_string # elements: 1 # length: 37 Create a CompactRegressionGAM object. # name: # type: sq_string # elements: 1 # length: 43 CompactRegressionGAM.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 296 CompactRegressionGAM: property ExpandedPredictorNames Names of the expanded predictor variables A cell array of character vectors naming the predictors as the model sees them. It matches PredictorNames unless a categorical predictor was expanded into dummy variables. This property is read-only. # name: # type: sq_string # elements: 1 # length: 41 Names of the expanded predictor variables # name: # type: sq_string # elements: 1 # length: 30 CompactRegressionGAM.FitMethod # name: # type: sq_string # elements: 1 # length: 128 CompactRegressionGAM: property FitMethod Which engine fitted the model, 'boostedtrees' or 'splines'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 59 Which engine fitted the model, 'boostedtrees' or 'splines'. # name: # type: sq_string # elements: 1 # length: 28 CompactRegressionGAM.Formula # name: # type: sq_string # elements: 1 # length: 227 CompactRegressionGAM: property Formula Formula of the model A character vector naming the response and the terms of the model, as in 'Y ~ x1 + x2 + x1:x2', or empty when the model was not given one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Formula of the model # name: # type: sq_string # elements: 1 # length: 30 CompactRegressionGAM.IntMatrix # name: # type: sq_string # elements: 1 # length: 559 CompactRegressionGAM: property IntMatrix Every term the model fits A logical matrix with one row per term and one column per predictor, true wherever the term multiplies that predictor. A row naming one predictor is a main effect, two an interaction, and three or more a higher-order term. This property is read-only. It is the complete record, where Interactions reports only the two-way terms, in the form MATLAB reports them. It is also the form the 'Interactions' option takes back, so passing it to the constructor rebuilds a model over the same terms. # name: # type: sq_string # elements: 1 # length: 25 Every term the model fits # name: # type: sq_string # elements: 1 # length: 33 CompactRegressionGAM.Interactions # name: # type: sq_string # elements: 1 # length: 599 CompactRegressionGAM: property Interactions Two-way interaction terms of the fitted model A Kx2 matrix of predictor index pairs, one row per two-way term the model carries, and zeros (0, 2) when it carries none. It reports what was fitted rather than what was asked for, so a count of terms, 'all', a logical matrix and a formula all leave the same kind of value behind. This property is read-only. A main effect names one predictor and a higher-order term names three or more, and neither has a two-column form, so neither appears here. IntMatrix remains the complete record of every term fitted. # name: # type: sq_string # elements: 1 # length: 45 Two-way interaction terms of the fitted model # name: # type: sq_string # elements: 1 # length: 30 CompactRegressionGAM.Intercept # name: # type: sq_string # elements: 1 # length: 190 CompactRegressionGAM: property Intercept Intercept of the fitted model A numeric scalar, the mean of the response, which every additive term is measured against. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Intercept of the fitted model # name: # type: sq_string # elements: 1 # length: 43 CompactRegressionGAM.IsStandardDeviationFit # name: # type: sq_string # elements: 1 # length: 284 CompactRegressionGAM: property IsStandardDeviationFit Flag for a fitted standard deviation model A boolean flag, always false, as this class estimates the standard deviation of a prediction from the residuals of the fit rather than fitting a model for it. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Flag for a fitted standard deviation model # name: # type: sq_string # elements: 1 # length: 30 CompactRegressionGAM.ModelwInt # name: # type: sq_string # elements: 1 # length: 227 CompactRegressionGAM: property ModelwInt Model with interaction terms A structure of the same fields as BaseModel, for the model fitted with the interaction terms, and empty when none was asked for. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Model with interaction terms # name: # type: sq_string # elements: 1 # length: 34 CompactRegressionGAM.NumPredictors # name: # type: sq_string # elements: 1 # length: 161 CompactRegressionGAM: property NumPredictors Number of predictors A positive integer, the number of predictors of the training data. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of predictors # name: # type: sq_string # elements: 1 # length: 42 CompactRegressionGAM.PairDetectionBinEdges # name: # type: sq_string # elements: 1 # length: 196 CompactRegressionGAM: property PairDetectionBinEdges Bin edges used to detect interactions, eight equal-frequency bins per predictor, empty when the model carries none. This property is read-only. # name: # type: sq_string # elements: 1 # length: 115 Bin edges used to detect interactions, eight equal-frequency bins per predictor, empty when the model carries none. # name: # type: sq_string # elements: 1 # length: 35 CompactRegressionGAM.PredictorNames # name: # type: sq_string # elements: 1 # length: 211 CompactRegressionGAM: property PredictorNames Names of the predictor variables A cell array of character vectors naming the predictors, in the order they appear in the training data. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Names of the predictor variables # name: # type: sq_string # elements: 1 # length: 33 CompactRegressionGAM.ResponseName # name: # type: sq_string # elements: 1 # length: 146 CompactRegressionGAM: property ResponseName Response variable name A character vector naming the response variable Y. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Response variable name # name: # type: sq_string # elements: 1 # length: 38 CompactRegressionGAM.ResponseTransform # name: # type: sq_string # elements: 1 # length: 315 CompactRegressionGAM: property ResponseTransform Transformation applied to the predicted response A function handle applied to the response the model predicts. Add or change it using dot notation, as in obj.ResponseTransform = 'log' or obj.ResponseTransform = @function_handle. It defaults to 'none', the identity. # name: # type: sq_string # elements: 1 # length: 48 Transformation applied to the predicted response # name: # type: sq_string # elements: 1 # length: 30 CompactRegressionGAM.TreeModel # name: # type: sq_string # elements: 1 # length: 152 CompactRegressionGAM: property TreeModel The fitted shape functions and interaction surfaces, empty under the spline engine. This property is read-only. # name: # type: sq_string # elements: 1 # length: 83 The fitted shape functions and interaction surfaces, empty under the spline engine. # name: # type: sq_string # elements: 1 # length: 25 CompactRegressionGAM.loss # name: # type: sq_string # elements: 1 # length: 1119 CompactRegressionGAM: L = loss (obj, X, Y) CompactRegressionGAM: L = loss (obj, Tbl, ResponseVarName) CompactRegressionGAM: L = loss (obj, Tbl) CompactRegressionGAM: L = loss (..., name, value) Regression loss of a generalized additive model. L = loss (obj, X, Y) returns the weighted mean squared error of the model on the rows of X against the true response Y. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. L = loss (..., name, value) accepts the following name-value pairs: "LossFun" selects the loss, either "mse", the default, or a function handle taking the true response, the predicted response and the weights, and returning a numeric scalar. "Weights" holds one weight per row of X, normalised to sum to one before it is applied. See also: CompactRegressionGAM, RegressionGAM, fitrgam, CompactRegressionGAM.predict # name: # type: sq_string # elements: 1 # length: 48 Regression loss of a generalized additive model. # name: # type: sq_string # elements: 1 # length: 28 CompactRegressionGAM.predict # name: # type: sq_string # elements: 1 # length: 2261 CompactRegressionGAM: yFit = predict (obj, Xfit) CompactRegressionGAM: yFit = predict (..., Name, Value) CompactRegressionGAM: [yFit, ySD, yInt] = predict (...) Predict new data points using generalized additive model regression object. yFit = predict (obj, Xfit returns a vector of predicted responses, yFit, for the predictor data in matrix Xfit based on the Generalized Additive Model in obj. Xfit must have the same number of features/variables as the training data in obj. Every row is predicted. Under boosted trees a missing value adds nothing from a main effect, and an interaction term takes the value its trees give a row missing that predictor, so a row missing every predictor predicts the intercept; under splines a row holding a missing value is predicted as NaN. obj must be a CompactRegressionGAM class object. [yFit, ySD, yInt] = predict (obj, Xfit also returns the standard deviations, ySD, and prediction intervals, yInt, of the response variable yFit, evaluated at each observation in the predictor data Xfit. yFit = predict (..., Name, Value) returns the aforementioned results with additional properties specified by Name-Value pair arguments listed below. Name Value 'alpha' significance level of the prediction intervals yInt, specified as scalar in range [0,1]. The default value is 0.05, which corresponds to 95% prediction intervals. 'includeinteractions' a boolean flag to include interactions to predict new values based on Xfit. By default, 'includeinteractions' is true when the GAM model in obj contains a obj.Formula or obj.Interactions fields. Otherwise, is set to false. If set to true when no interactions are present in the trained model, it will result to an error. If set to false when using a model that includes interactions, the predictions will be made on the basic model without any interaction terms. This way you can make predictions from the same GAM model without having to retrain it. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: fitrgam, RegressionGAM # name: # type: sq_string # elements: 1 # length: 75 Predict new data points using generalized additive model regression object. # name: # type: sq_string # elements: 1 # length: 30 CompactRegressionGAM.savemodel # name: # type: sq_string # elements: 1 # length: 430 CompactRegressionGAM: savemodel (obj, filename) Save a CompactRegressionGAM object. savemodel (obj, filename) saves each property of a CompactRegressionGAM object into an Octave binary file, the name of which is specified in filename, along with an extra variable which defines the type of object these variables constitute. Use loadmodel in order to load the object back into Octave. See also: loadmodel, fitrgam, RegressionGAM # name: # type: sq_string # elements: 1 # length: 35 Save a CompactRegressionGAM object. # name: # type: sq_string # elements: 1 # length: 19 CompactRegressionGP # name: # type: sq_string # elements: 1 # length: 1036 statistics: CompactRegressionGP Compact Gaussian process regression A CompactRegressionGP object holds a Gaussian process regression model without its training data, keeping what is needed to predict and dropping the rest. Create a CompactRegressionGP object by using the compact method of a RegressionGP object. A compact model keeps the active set it predicts from, the prediction weights, the covariance function and its parameters, the explicit basis and its coefficients, the noise standard deviation and the standardizing location and scale. It drops the response, the observation weights, the rows used, the count of observations and the maximized log likelihood, so it can predict but cannot be cross validated, refitted, or asked for its resubstitution loss or its post-fit statistics. The standard deviation and the prediction intervals remain available, because the active set of an exactly fitted model is the whole of the training predictors and the factorization can be rebuilt from it. See also: RegressionGP, fitrgp # name: # type: sq_string # elements: 1 # length: 35 Compact Gaussian process regression # name: # type: sq_string # elements: 1 # length: 35 CompactRegressionGP.ActiveSetMethod # name: # type: sq_string # elements: 1 # length: 121 CompactRegressionGP: property ActiveSetMethod Method used to select the active set 'Random'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Method used to select the active set # name: # type: sq_string # elements: 1 # length: 33 CompactRegressionGP.ActiveSetSize # name: # type: sq_string # elements: 1 # length: 122 CompactRegressionGP: property ActiveSetSize Size of the active set A positive integer scalar. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Size of the active set # name: # type: sq_string # elements: 1 # length: 36 CompactRegressionGP.ActiveSetVectors # name: # type: sq_string # elements: 1 # length: 205 CompactRegressionGP: property ActiveSetVectors Subset of the training data used for predictions An MxP numeric matrix, standardized where the model standardized its predictors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 48 Subset of the training data used for predictions # name: # type: sq_string # elements: 1 # length: 25 CompactRegressionGP.Alpha # name: # type: sq_string # elements: 1 # length: 158 CompactRegressionGP: property Alpha Weights the predictions are made from A numeric vector with one weight per active set vector. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Weights the predictions are made from # name: # type: sq_string # elements: 1 # length: 33 CompactRegressionGP.BasisFunction # name: # type: sq_string # elements: 1 # length: 141 CompactRegressionGP: property BasisFunction Explicit basis of the model A character vector or a function handle. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Explicit basis of the model # name: # type: sq_string # elements: 1 # length: 24 CompactRegressionGP.Beta # name: # type: sq_string # elements: 1 # length: 158 CompactRegressionGP: property Beta Estimated coefficients of the explicit basis A numeric vector, empty when the basis is 'None'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Estimated coefficients of the explicit basis # name: # type: sq_string # elements: 1 # length: 41 CompactRegressionGP.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 159 CompactRegressionGP: property CategoricalPredictors Indices of the categorical predictors A vector of positive integers, or empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 39 CompactRegressionGP.CompactRegressionGP # name: # type: sq_string # elements: 1 # length: 279 CompactRegressionGP: obj = CompactRegressionGP (Mdl) Create a CompactRegressionGP object. Mdl is the RegressionGP object to compact, and is required: the compact model has no training data to build itself from. The documented way to reach this constructor is the compact method. # name: # type: sq_string # elements: 1 # length: 36 Create a CompactRegressionGP object. # name: # type: sq_string # elements: 1 # length: 42 CompactRegressionGP.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 150 CompactRegressionGP: property ExpandedPredictorNames Expanded predictor variable names A cell array of character vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Expanded predictor variable names # name: # type: sq_string # elements: 1 # length: 29 CompactRegressionGP.FitMethod # name: # type: sq_string # elements: 1 # length: 126 CompactRegressionGP: property FitMethod Method used to estimate the parameters 'Exact' or 'None'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Method used to estimate the parameters # name: # type: sq_string # elements: 1 # length: 34 CompactRegressionGP.KernelFunction # name: # type: sq_string # elements: 1 # length: 146 CompactRegressionGP: property KernelFunction Form of the covariance function A character vector or a function handle. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Form of the covariance function # name: # type: sq_string # elements: 1 # length: 37 CompactRegressionGP.KernelInformation # name: # type: sq_string # elements: 1 # length: 188 CompactRegressionGP: property KernelInformation Covariance function and its parameters A structure with fields Name, KernelParameters and KernelParameterNames. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Covariance function and its parameters # name: # type: sq_string # elements: 1 # length: 33 CompactRegressionGP.PredictMethod # name: # type: sq_string # elements: 1 # length: 113 CompactRegressionGP: property PredictMethod Method used to make predictions 'Exact'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Method used to make predictions # name: # type: sq_string # elements: 1 # length: 37 CompactRegressionGP.PredictorLocation # name: # type: sq_string # elements: 1 # length: 145 CompactRegressionGP: property PredictorLocation Means the predictors were centred by A 1xP numeric vector, or empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Means the predictors were centred by # name: # type: sq_string # elements: 1 # length: 34 CompactRegressionGP.PredictorNames # name: # type: sq_string # elements: 1 # length: 133 CompactRegressionGP: property PredictorNames Predictor variable names A cell array of character vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Predictor variable names # name: # type: sq_string # elements: 1 # length: 34 CompactRegressionGP.PredictorScale # name: # type: sq_string # elements: 1 # length: 155 CompactRegressionGP: property PredictorScale Standard deviations the predictors were scaled by A 1xP numeric vector, or empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 49 Standard deviations the predictors were scaled by # name: # type: sq_string # elements: 1 # length: 32 CompactRegressionGP.ResponseName # name: # type: sq_string # elements: 1 # length: 114 CompactRegressionGP: property ResponseName Response variable name A character vector. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Response variable name # name: # type: sq_string # elements: 1 # length: 37 CompactRegressionGP.ResponseTransform # name: # type: sq_string # elements: 1 # length: 203 CompactRegressionGP: property ResponseTransform Transformation applied to the predicted response A character vector, or the text of the function handle that was supplied. Assigning to it accepts either. # name: # type: sq_string # elements: 1 # length: 48 Transformation applied to the predicted response # name: # type: sq_string # elements: 1 # length: 25 CompactRegressionGP.Sigma # name: # type: sq_string # elements: 1 # length: 118 CompactRegressionGP: property Sigma Estimated noise standard deviation A positive scalar. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Estimated noise standard deviation # name: # type: sq_string # elements: 1 # length: 24 CompactRegressionGP.loss # name: # type: sq_string # elements: 1 # length: 757 CompactRegressionGP: L = loss (obj, X, Y) CompactRegressionGP: L = loss (obj, Tbl, ResponseVarName) CompactRegressionGP: L = loss (obj, Tbl) CompactRegressionGP: L = loss (..., name, value) Compute the regression loss of a compact Gaussian process model. L = loss (obj, X, Y) returns the mean squared error of the model obj on the data X and Y, and accepts the same 'LossFun' and 'Weights' pairs the full model accepts. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. # name: # type: sq_string # elements: 1 # length: 64 Compute the regression loss of a compact Gaussian process model. # name: # type: sq_string # elements: 1 # length: 27 CompactRegressionGP.predict # name: # type: sq_string # elements: 1 # length: 788 CompactRegressionGP: yFit = predict (obj, XC) CompactRegressionGP: [yFit, ySD, yInt] = predict (obj, XC) CompactRegressionGP: [...] = predict (..., 'Alpha', alpha) Predict the response for new data with a compact Gaussian process model. yFit = predict (obj, XC) returns the predicted response of the CompactRegressionGP model obj at the points in XC, and the further outputs are the standard deviation of each predicted response and the prediction intervals, exactly as the full model returns them. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. # name: # type: sq_string # elements: 1 # length: 72 Predict the response for new data with a compact Gaussian process model. # name: # type: sq_string # elements: 1 # length: 29 CompactRegressionGP.savemodel # name: # type: sq_string # elements: 1 # length: 191 CompactRegressionGP: savemodel (obj, filename) Save a compact Gaussian process model to a file. savemodel (obj, filename) saves the model obj into filename in a form loadmodel can read back. # name: # type: sq_string # elements: 1 # length: 48 Save a compact Gaussian process model to a file. # name: # type: sq_string # elements: 1 # length: 30 CompactRegressionNeuralNetwork # name: # type: sq_string # elements: 1 # length: 874 statistics: CompactRegressionNeuralNetwork Compact neural network regression A CompactRegressionNeuralNetwork object holds a neural network regression model that has dropped its training data. Create a CompactRegressionNeuralNetwork object by using the compact method of a RegressionNeuralNetwork object. The compact model keeps what is needed to answer about new data, the layer weights and biases, the activations, the standardization and the response transform, and drops what only describes the fit: the predictor and response data, the observation weights, the rows used, the number of observations and the iteration by iteration training history. predict and loss therefore agree with the full model to the last digit, while resubPredict and resubLoss do not exist here, there being no training data left to resubstitute. See also: RegressionNeuralNetwork, fitrnet # name: # type: sq_string # elements: 1 # length: 33 Compact neural network regression # name: # type: sq_string # elements: 1 # length: 42 CompactRegressionNeuralNetwork.Activations # name: # type: sq_string # elements: 1 # length: 213 CompactRegressionNeuralNetwork: property Activations Activation functions of the hidden layers A character vector, or a cell array of character vectors with one entry per hidden layer. This property is read-only. # name: # type: sq_string # elements: 1 # length: 41 Activation functions of the hidden layers # name: # type: sq_string # elements: 1 # length: 52 CompactRegressionNeuralNetwork.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 189 CompactRegressionNeuralNetwork: property CategoricalPredictors Indices of the categorical predictors A numeric vector of column indices, and empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 61 CompactRegressionNeuralNetwork.CompactRegressionNeuralNetwork # name: # type: sq_string # elements: 1 # length: 462 CompactRegressionNeuralNetwork: obj = CompactRegressionNeuralNetwork (Mdl) CompactRegressionNeuralNetwork: obj = CompactRegressionNeuralNetwork () Create a CompactRegressionNeuralNetwork object. Mdl is the RegressionNeuralNetwork object to compact. The documented way to reach this constructor is the compact method. Called with no arguments it returns an object with its properties empty, which is how a saved model is rebuilt before its values are filled in. # name: # type: sq_string # elements: 1 # length: 47 Create a CompactRegressionNeuralNetwork object. # name: # type: sq_string # elements: 1 # length: 53 CompactRegressionNeuralNetwork.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 178 CompactRegressionNeuralNetwork: property ExpandedPredictorNames Names of the predictors as the model expanded them A cell array of character vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 50 Names of the predictors as the model expanded them # name: # type: sq_string # elements: 1 # length: 42 CompactRegressionNeuralNetwork.LayerBiases # name: # type: sq_string # elements: 1 # length: 174 CompactRegressionNeuralNetwork: property LayerBiases Biases the network learned A cell array with one entry per layer, the output layer included. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Biases the network learned # name: # type: sq_string # elements: 1 # length: 41 CompactRegressionNeuralNetwork.LayerSizes # name: # type: sq_string # elements: 1 # length: 180 CompactRegressionNeuralNetwork: property LayerSizes Sizes of the fully connected hidden layers A row vector of positive integers, one per hidden layer. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Sizes of the fully connected hidden layers # name: # type: sq_string # elements: 1 # length: 43 CompactRegressionNeuralNetwork.LayerWeights # name: # type: sq_string # elements: 1 # length: 176 CompactRegressionNeuralNetwork: property LayerWeights Weights the network learned A cell array with one entry per layer, the output layer included. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Weights the network learned # name: # type: sq_string # elements: 1 # length: 33 CompactRegressionNeuralNetwork.Mu # name: # type: sq_string # elements: 1 # length: 217 CompactRegressionNeuralNetwork: property Mu Mean of the predictors A row vector with one entry per predictor, used for standardization. Empty when the predictor data were not standardized. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Mean of the predictors # name: # type: sq_string # elements: 1 # length: 44 CompactRegressionNeuralNetwork.NumPredictors # name: # type: sq_string # elements: 1 # length: 131 CompactRegressionNeuralNetwork: property NumPredictors Number of predictors A positive integer scalar. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of predictors # name: # type: sq_string # elements: 1 # length: 52 CompactRegressionNeuralNetwork.OutputLayerActivation # name: # type: sq_string # elements: 1 # length: 228 CompactRegressionNeuralNetwork: property OutputLayerActivation Activation function of the output layer A character vector. 'none' applies the identity, so a prediction is an unrestricted real number. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Activation function of the output layer # name: # type: sq_string # elements: 1 # length: 45 CompactRegressionNeuralNetwork.PredictorNames # name: # type: sq_string # elements: 1 # length: 143 CompactRegressionNeuralNetwork: property PredictorNames Names of the predictors A cell array of character vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 43 CompactRegressionNeuralNetwork.ResponseName # name: # type: sq_string # elements: 1 # length: 132 CompactRegressionNeuralNetwork: property ResponseName Name of the response variable A character vector. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 48 CompactRegressionNeuralNetwork.ResponseTransform # name: # type: sq_string # elements: 1 # length: 270 CompactRegressionNeuralNetwork: property ResponseTransform Transformation applied to the predicted response A function handle, applied by predict to the network's output. It may be set after construction, either to a handle or to the name of a supported transformation. # name: # type: sq_string # elements: 1 # length: 48 Transformation applied to the predicted response # name: # type: sq_string # elements: 1 # length: 36 CompactRegressionNeuralNetwork.Sigma # name: # type: sq_string # elements: 1 # length: 234 CompactRegressionNeuralNetwork: property Sigma Standard deviation of the predictors A row vector with one entry per predictor, used for standardization. Empty when the predictor data were not standardized. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Standard deviation of the predictors # name: # type: sq_string # elements: 1 # length: 35 CompactRegressionNeuralNetwork.loss # name: # type: sq_string # elements: 1 # length: 1408 CompactRegressionNeuralNetwork: L = loss (obj, X, Y) CompactRegressionNeuralNetwork: L = loss (obj, Tbl, ResponseVarName) CompactRegressionNeuralNetwork: L = loss (obj, Tbl) CompactRegressionNeuralNetwork: L = loss (..., name, value) Compute the regression loss of a compact neural network model. L = loss (obj, X, Y) returns the weighted mean squared error between the response Y and the response the model predicts for X. obj must be a CompactRegressionNeuralNetwork class object. X must be a numeric matrix with the same number of predictors as the data the model was trained on. Y must be a numeric vector with as many rows as X. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. L = loss (..., name, value) accepts the following Name-Value pairs. Name Value 'LossFun' 'mse', the default, or a function handle called as lossfun (Y, yFit, W) and returning a scalar. 'Weights' A numeric vector of observation weights with one entry per row of X. It defaults to a uniform weight. The weights are normalized to sum to one before the loss is formed. See also: CompactRegressionNeuralNetwork, RegressionNeuralNetwork # name: # type: sq_string # elements: 1 # length: 62 Compute the regression loss of a compact neural network model. # name: # type: sq_string # elements: 1 # length: 38 CompactRegressionNeuralNetwork.predict # name: # type: sq_string # elements: 1 # length: 819 CompactRegressionNeuralNetwork: yFit = predict (obj, XC) Predict the response for new data with a compact neural network regression model. yFit = predict (obj, XC) returns a column vector holding the predicted response for each row of XC. It agrees with the full model this object was compacted from. obj must be a CompactRegressionNeuralNetwork class object. XC must be a numeric matrix with the same number of predictors as the data the model was trained on. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: CompactRegressionNeuralNetwork, RegressionNeuralNetwork # name: # type: sq_string # elements: 1 # length: 81 Predict the response for new data with a compact neural network regression model. # name: # type: sq_string # elements: 1 # length: 40 CompactRegressionNeuralNetwork.savemodel # name: # type: sq_string # elements: 1 # length: 341 CompactRegressionNeuralNetwork: savemodel (obj, filename) Save a compact neural network regression model to a file. savemodel (obj, filename) saves every property of the CompactRegressionNeuralNetwork object obj into filename in binary format, so that it can be read back with loadmodel. See also: loadmodel, CompactRegressionNeuralNetwork # name: # type: sq_string # elements: 1 # length: 57 Save a compact neural network regression model to a file. # name: # type: sq_string # elements: 1 # length: 20 CompactRegressionSVM # name: # type: sq_string # elements: 1 # length: 851 statistics: CompactRegressionSVM Compact Support Vector Machine regression A CompactRegressionSVM object holds a support vector regression model that has dropped its training data. Create a CompactRegressionSVM object by using the compact method of a RegressionSVM object. The compact model keeps what is needed to answer about new data, the support vectors and their coefficients, the intercept, the kernel, the standardization and the response transform, and drops what only describes the fit: the predictor and response data, the observation weights, the rows used, the observation count, and which training rows became support vectors. predict and loss therefore agree with the full model to the last digit, while resubPredict and resubLoss do not exist here, there being no training data left to resubstitute. See also: RegressionSVM, fitrsvm # name: # type: sq_string # elements: 1 # length: 41 Compact Support Vector Machine regression # name: # type: sq_string # elements: 1 # length: 26 CompactRegressionSVM.Alpha # name: # type: sq_string # elements: 1 # length: 218 CompactRegressionSVM: property Alpha Dual coefficients of the support vectors A numeric column vector with one entry per support vector, signed, as in the model this one was compacted from. This property is read-only. # name: # type: sq_string # elements: 1 # length: 40 Dual coefficients of the support vectors # name: # type: sq_string # elements: 1 # length: 25 CompactRegressionSVM.Beta # name: # type: sq_string # elements: 1 # length: 312 CompactRegressionSVM: property Beta Primal coefficients, one per predictor A numeric column vector, equal to (obj.SupportVectors / s)' * obj.Alpha, where s is the kernel scale, so that a prediction is (x / s) * Beta + Bias, as in MATLAB. It is empty for any kernel other than linear. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Primal coefficients, one per predictor # name: # type: sq_string # elements: 1 # length: 25 CompactRegressionSVM.Bias # name: # type: sq_string # elements: 1 # length: 179 CompactRegressionSVM: property Bias Intercept of the fitted function A numeric scalar. With a linear kernel the prediction is X * obj.Beta + obj.Bias. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Intercept of the fitted function # name: # type: sq_string # elements: 1 # length: 42 CompactRegressionSVM.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 179 CompactRegressionSVM: property CategoricalPredictors Indices of the categorical predictors A numeric vector of column indices, and empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 41 CompactRegressionSVM.CompactRegressionSVM # name: # type: sq_string # elements: 1 # length: 402 CompactRegressionSVM: obj = CompactRegressionSVM (Mdl) CompactRegressionSVM: obj = CompactRegressionSVM () Create a CompactRegressionSVM object. Mdl is the RegressionSVM object to compact. The documented way to reach this constructor is the compact method. Called with no arguments it returns an object with its properties empty, which is how a saved model is rebuilt before its values are filled in. # name: # type: sq_string # elements: 1 # length: 37 Create a CompactRegressionSVM object. # name: # type: sq_string # elements: 1 # length: 28 CompactRegressionSVM.Epsilon # name: # type: sq_string # elements: 1 # length: 240 CompactRegressionSVM: property Epsilon Half-width of the insensitive tube A non-negative scalar, carried over from the model this one was compacted from. It is what the 'epsiloninsensitive' loss charges against. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Half-width of the insensitive tube # name: # type: sq_string # elements: 1 # length: 43 CompactRegressionSVM.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 168 CompactRegressionSVM: property ExpandedPredictorNames Names of the predictors as the model expanded them A cell array of character vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 50 Names of the predictors as the model expanded them # name: # type: sq_string # elements: 1 # length: 37 CompactRegressionSVM.KernelParameters # name: # type: sq_string # elements: 1 # length: 309 CompactRegressionSVM: property KernelParameters Parameters of the kernel function A structure with fields Function and Scale, and Order for a polynomial kernel. Function names the kernel as MATLAB names it, so a radial basis kernel reports 'gaussian' whichever spelling was given. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Parameters of the kernel function # name: # type: sq_string # elements: 1 # length: 23 CompactRegressionSVM.Mu # name: # type: sq_string # elements: 1 # length: 207 CompactRegressionSVM: property Mu Mean of the predictors A row vector with one entry per predictor, used for standardization. Empty when the predictor data were not standardized. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Mean of the predictors # name: # type: sq_string # elements: 1 # length: 34 CompactRegressionSVM.NumPredictors # name: # type: sq_string # elements: 1 # length: 121 CompactRegressionSVM: property NumPredictors Number of predictors A positive integer scalar. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of predictors # name: # type: sq_string # elements: 1 # length: 35 CompactRegressionSVM.PredictorNames # name: # type: sq_string # elements: 1 # length: 133 CompactRegressionSVM: property PredictorNames Names of the predictors A cell array of character vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 33 CompactRegressionSVM.ResponseName # name: # type: sq_string # elements: 1 # length: 122 CompactRegressionSVM: property ResponseName Name of the response variable A character vector. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 38 CompactRegressionSVM.ResponseTransform # name: # type: sq_string # elements: 1 # length: 258 CompactRegressionSVM: property ResponseTransform Transformation applied to the predicted response A function handle, applied by predict to the model's output. It may be set after construction, either to a handle or to the name of a supported transformation. # name: # type: sq_string # elements: 1 # length: 48 Transformation applied to the predicted response # name: # type: sq_string # elements: 1 # length: 26 CompactRegressionSVM.Sigma # name: # type: sq_string # elements: 1 # length: 224 CompactRegressionSVM: property Sigma Standard deviation of the predictors A row vector with one entry per predictor, used for standardization. Empty when the predictor data were not standardized. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Standard deviation of the predictors # name: # type: sq_string # elements: 1 # length: 35 CompactRegressionSVM.SupportVectors # name: # type: sq_string # elements: 1 # length: 194 CompactRegressionSVM: property SupportVectors The support vectors themselves A numeric matrix with one row per support vector, on the scale the model was trained on. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 The support vectors themselves # name: # type: sq_string # elements: 1 # length: 42 CompactRegressionSVM.discardSupportVectors # name: # type: sq_string # elements: 1 # length: 592 CompactRegressionSVM: obj = discardSupportVectors (obj) Discard the support vectors of a linear SVM model. obj = discardSupportVectors (obj) empties Alpha and SupportVectors, leaving Beta and Bias to decide every prediction. A linear kernel needs nothing else, so the returned model predicts what it predicted before while carrying one vector in place of many. The kernel must be linear. Under any other the support vectors are part of the decision function and cannot be dropped. Discarding twice is not an error and changes nothing. See also: fitrsvm, RegressionSVM, CompactRegressionSVM # name: # type: sq_string # elements: 1 # length: 50 Discard the support vectors of a linear SVM model. # name: # type: sq_string # elements: 1 # length: 25 CompactRegressionSVM.loss # name: # type: sq_string # elements: 1 # length: 1364 CompactRegressionSVM: L = loss (obj, X, Y) CompactRegressionSVM: L = loss (obj, Tbl, ResponseVarName) CompactRegressionSVM: L = loss (obj, Tbl) CompactRegressionSVM: L = loss (..., name, value) Compute the regression loss of a compact support vector machine model. L = loss (obj, X, Y) returns the weighted mean squared error between the response Y and the response the model predicts for X. obj must be a CompactRegressionSVM class object. X must be a numeric matrix with the same number of predictors as the data the model was trained on. Y must be a numeric vector with as many rows as X. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. L = loss (..., name, value) accepts the following Name-Value pairs. Name Value 'LossFun' 'mse', the default, 'epsiloninsensitive', or a function handle called as lossfun (Y, yFit, W) returning a scalar. 'Weights' A numeric vector of observation weights with one entry per row of X. It defaults to a uniform weight. The weights are normalized to sum to one before the loss is formed. See also: CompactRegressionSVM, RegressionSVM # name: # type: sq_string # elements: 1 # length: 70 Compute the regression loss of a compact support vector machine model. # name: # type: sq_string # elements: 1 # length: 28 CompactRegressionSVM.predict # name: # type: sq_string # elements: 1 # length: 779 CompactRegressionSVM: yFit = predict (obj, XC) Predict the response for new data with a compact support vector regression model. yFit = predict (obj, XC) returns a column vector holding the predicted response for each row of XC. It agrees with the full model this object was compacted from. obj must be a CompactRegressionSVM class object. XC must be a numeric matrix with the same number of predictors as the data the model was trained on. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: CompactRegressionSVM, RegressionSVM # name: # type: sq_string # elements: 1 # length: 81 Predict the response for new data with a compact support vector regression model. # name: # type: sq_string # elements: 1 # length: 30 CompactRegressionSVM.savemodel # name: # type: sq_string # elements: 1 # length: 311 CompactRegressionSVM: savemodel (obj, filename) Save a compact support vector regression model to a file. savemodel (obj, filename) saves every property of the CompactRegressionSVM object obj into filename in binary format, so that it can be read back with loadmodel. See also: loadmodel, CompactRegressionSVM # name: # type: sq_string # elements: 1 # length: 57 Save a compact support vector regression model to a file. # name: # type: sq_string # elements: 1 # length: 21 CompactRegressionTree # name: # type: sq_string # elements: 1 # length: 682 statistics: CompactRegressionTree Compact binary decision tree for regression A CompactRegressionTree object carries the tree a RegressionTree model grew and everything predict needs, but not the observations it was fitted on. It answers new data identically to the model it came from, and is far smaller to keep or to ship. Create one with the compact method of a RegressionTree object. Because it holds no training data, it has no resub methods and cannot be cross-validated, and it cannot be pruned: the pruning sequence is reported but taking a subtree out of it rewrites the node table, which is work for the model that still has its data. See also: RegressionTree, fitrtree # name: # type: sq_string # elements: 1 # length: 43 Compact binary decision tree for regression # name: # type: sq_string # elements: 1 # length: 43 CompactRegressionTree.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 225 CompactRegressionTree: property CategoricalPredictors Indices of the categorical predictors A row vector of column indices into X, naming the predictors treated as categorical, empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 38 CompactRegressionTree.CategoricalSplit # name: # type: sq_string # elements: 1 # length: 274 CompactRegressionTree: property CategoricalSplit Categorical splits of the tree A Nx2 cell array with one row per categorical split, in node order, holding the levels sent left and the levels sent right. It is empty when no split is categorical. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 Categorical splits of the tree # name: # type: sq_string # elements: 1 # length: 30 CompactRegressionTree.Children # name: # type: sq_string # elements: 1 # length: 203 CompactRegressionTree: property Children Child nodes of each node A NumNodesx2 matrix naming the left and the right child of each node. A leaf carries a zero in both columns. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Child nodes of each node # name: # type: sq_string # elements: 1 # length: 43 CompactRegressionTree.CompactRegressionTree # name: # type: sq_string # elements: 1 # length: 245 CompactRegressionTree: obj = CompactRegressionTree (Mdl) Create a CompactRegressionTree object. Mdl is the RegressionTree object to compact. The documented way to reach this constructor is the compact method. See also: RegressionTree, fitrtree # name: # type: sq_string # elements: 1 # length: 38 Create a CompactRegressionTree object. # name: # type: sq_string # elements: 1 # length: 35 CompactRegressionTree.CutCategories # name: # type: sq_string # elements: 1 # length: 260 CompactRegressionTree: property CutCategories Categories used at each branch A NumNodesx2 cell array holding, for a node that cuts a categorical predictor, the levels sent left and the levels sent right. Every other entry is empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 Categories used at each branch # name: # type: sq_string # elements: 1 # length: 30 CompactRegressionTree.CutPoint # name: # type: sq_string # elements: 1 # length: 312 CompactRegressionTree: property CutPoint Cut point of each node A column vector holding, for each node, the value the split compares the predictor against: an observation goes left when its value is less than the cut point and right otherwise. A leaf and a categorical cut carry NaN. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Cut point of each node # name: # type: sq_string # elements: 1 # length: 34 CompactRegressionTree.CutPredictor # name: # type: sq_string # elements: 1 # length: 266 CompactRegressionTree: property CutPredictor Name of the predictor each node cuts on A cell array of character vectors with one entry per node, holding the name of the predictor the node splits on and an empty character vector at a leaf. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Name of the predictor each node cuts on # name: # type: sq_string # elements: 1 # length: 39 CompactRegressionTree.CutPredictorIndex # name: # type: sq_string # elements: 1 # length: 215 CompactRegressionTree: property CutPredictorIndex Index of the predictor each node cuts on A column vector holding, for each node, the column of X the node splits on, and zero at a leaf. This property is read-only. # name: # type: sq_string # elements: 1 # length: 40 Index of the predictor each node cuts on # name: # type: sq_string # elements: 1 # length: 29 CompactRegressionTree.CutType # name: # type: sq_string # elements: 1 # length: 298 CompactRegressionTree: property CutType Type of cut at each node A cell array of character vectors holding 'continuous' at a branch node that cuts a numeric predictor at a point, 'categorical' at one that splits a set of levels, and an empty character vector at a leaf. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Type of cut at each node # name: # type: sq_string # elements: 1 # length: 44 CompactRegressionTree.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 312 CompactRegressionTree: property ExpandedPredictorNames Expanded predictor names A cell array of character vectors. It differs from PredictorNames only when a categorical predictor has been expanded into one column per level, which this class does not do, so the two are always equal. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Expanded predictor names # name: # type: sq_string # elements: 1 # length: 34 CompactRegressionTree.IsBranchNode # name: # type: sq_string # elements: 1 # length: 192 CompactRegressionTree: property IsBranchNode Which nodes are branch nodes A logical column vector, true for each node that carries a split and false for each leaf. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Which nodes are branch nodes # name: # type: sq_string # elements: 1 # length: 31 CompactRegressionTree.NodeError # name: # type: sq_string # elements: 1 # length: 213 CompactRegressionTree: property NodeError Mean squared error of each node A column vector holding, for each node, the weighted mean squared error of the response about the node's mean. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Mean squared error of each node # name: # type: sq_string # elements: 1 # length: 30 CompactRegressionTree.NodeMean # name: # type: sq_string # elements: 1 # length: 272 CompactRegressionTree: property NodeMean Mean response at each node A column vector holding, for each node, the weighted mean of the response over the observations that reached it. It is what predict answers for a row that comes to rest there. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Mean response at each node # name: # type: sq_string # elements: 1 # length: 37 CompactRegressionTree.NodeProbability # name: # type: sq_string # elements: 1 # length: 256 CompactRegressionTree: property NodeProbability Probability of reaching each node A column vector holding, for each node, the total weight of the observations that reached it, the weights being those in W. The root carries one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Probability of reaching each node # name: # type: sq_string # elements: 1 # length: 30 CompactRegressionTree.NodeRisk # name: # type: sq_string # elements: 1 # length: 261 CompactRegressionTree: property NodeRisk Risk of each node A column vector holding, for each node, its mean squared error weighted by the probability of reaching it, which is the squared error the node contributes to the whole tree. This property is read-only. # name: # type: sq_string # elements: 1 # length: 17 Risk of each node # name: # type: sq_string # elements: 1 # length: 30 CompactRegressionTree.NodeSize # name: # type: sq_string # elements: 1 # length: 334 CompactRegressionTree: property NodeSize Number of observations at each node A column vector holding how many training observations reached each node. A row missing the predictor its node cuts on is counted at that node and at none below it, so a parent's size is not in general the sum of its children's. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Number of observations at each node # name: # type: sq_string # elements: 1 # length: 30 CompactRegressionTree.NumNodes # name: # type: sq_string # elements: 1 # length: 298 CompactRegressionTree: property NumNodes Number of nodes in the tree A positive integer, the number of nodes the tree holds, branch nodes and leaves together. Nodes are numbered as they are created, so a parent always carries a lower number than either of its children. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Number of nodes in the tree # name: # type: sq_string # elements: 1 # length: 28 CompactRegressionTree.Parent # name: # type: sq_string # elements: 1 # length: 160 CompactRegressionTree: property Parent Parent of each node A column vector naming the parent of each node. The root carries a zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Parent of each node # name: # type: sq_string # elements: 1 # length: 36 CompactRegressionTree.PredictorNames # name: # type: sq_string # elements: 1 # length: 173 CompactRegressionTree: property PredictorNames Names of the predictor variables A cell array of character vectors with one name per column of X. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Names of the predictor variables # name: # type: sq_string # elements: 1 # length: 32 CompactRegressionTree.PruneAlpha # name: # type: sq_string # elements: 1 # length: 368 CompactRegressionTree: property PruneAlpha Cost complexity parameter of each pruning level A column vector with one element per level of the pruning sequence, the first of which is zero and stands for the unpruned tree. Level L is the smallest subtree that is optimal for every complexity parameter from PruneAlpha(L+1) up to the next one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 47 Cost complexity parameter of each pruning level # name: # type: sq_string # elements: 1 # length: 31 CompactRegressionTree.PruneList # name: # type: sq_string # elements: 1 # length: 440 CompactRegressionTree: property PruneList Pruning level of each node A column vector holding, for each branch node, the level of the cost complexity sequence at which it stops being a branch node, and zero at a leaf. Pruning the tree to level L turns every node whose level is between one and L into a leaf. It is empty when neither 'Prune' nor 'MergeLeaves' was asked for, since no sequence was then estimated. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Pruning level of each node # name: # type: sq_string # elements: 1 # length: 34 CompactRegressionTree.ResponseName # name: # type: sq_string # elements: 1 # length: 143 CompactRegressionTree: property ResponseName Name of the response variable A character vector naming the response. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 39 CompactRegressionTree.ResponseTransform # name: # type: sq_string # elements: 1 # length: 290 CompactRegressionTree: property ResponseTransform Transform applied to the predicted response A character vector naming the function predict applies to the response it predicts before returning it, or a function handle taking and returning an array of the same size. The default is 'none'. # name: # type: sq_string # elements: 1 # length: 43 Transform applied to the predicted response # name: # type: sq_string # elements: 1 # length: 44 CompactRegressionTree.SurrogateCutCategories # name: # type: sq_string # elements: 1 # length: 181 CompactRegressionTree: property SurrogateCutCategories Categories of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Categories of the surrogate splits # name: # type: sq_string # elements: 1 # length: 38 CompactRegressionTree.SurrogateCutFlip # name: # type: sq_string # elements: 1 # length: 180 CompactRegressionTree: property SurrogateCutFlip Cut assignments of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Cut assignments of the surrogate splits # name: # type: sq_string # elements: 1 # length: 39 CompactRegressionTree.SurrogateCutPoint # name: # type: sq_string # elements: 1 # length: 176 CompactRegressionTree: property SurrogateCutPoint Cut points of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Cut points of the surrogate splits # name: # type: sq_string # elements: 1 # length: 43 CompactRegressionTree.SurrogateCutPredictor # name: # type: sq_string # elements: 1 # length: 180 CompactRegressionTree: property SurrogateCutPredictor Predictors of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Predictors of the surrogate splits # name: # type: sq_string # elements: 1 # length: 38 CompactRegressionTree.SurrogateCutType # name: # type: sq_string # elements: 1 # length: 170 CompactRegressionTree: property SurrogateCutType Types of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Types of the surrogate splits # name: # type: sq_string # elements: 1 # length: 51 CompactRegressionTree.SurrogatePredictorAssociation # name: # type: sq_string # elements: 1 # length: 212 CompactRegressionTree: property SurrogatePredictorAssociation Predictive measures of association of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 58 Predictive measures of association of the surrogate splits # name: # type: sq_string # elements: 1 # length: 26 CompactRegressionTree.loss # name: # type: sq_string # elements: 1 # length: 1175 CompactRegressionTree: L = loss (obj, X, Y) CompactRegressionTree: L = loss (obj, Tbl, ResponseVarName) CompactRegressionTree: L = loss (obj, Tbl) CompactRegressionTree: L = loss (..., name, value) Regression loss on new data. L = loss (obj, X, Y) returns the weighted mean squared error of the response the model predicts for X against the observed response Y. A row whose response is missing is dropped, as it is when fitting. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. L = loss (..., name, value) takes the following options. Name Value 'LossFun' 'mse', the default, or a function handle taking the true response, the predicted response and the weights, and returning a numeric scalar. 'Weights' A numeric vector of observation weights, one per row of X, normalized to sum to one before it is applied. See also: CompactRegressionTree, fitrtree, CompactRegressionTree.predict # name: # type: sq_string # elements: 1 # length: 28 Regression loss on new data. # name: # type: sq_string # elements: 1 # length: 39 CompactRegressionTree.nodeVariableRange # name: # type: sq_string # elements: 1 # length: 491 CompactRegressionTree: r = nodeVariableRange (obj, node) Range of each predictor at a node. r = nodeVariableRange (obj, node) returns a structure with one field per predictor the path from the root to node cuts on, holding the two-element range of values that reach the node, or for a categorical predictor the levels that reach it. A predictor the path never cuts on is unconstrained and is left out, so the root gives a structure with no fields. See also: CompactRegressionTree, fitrtree # name: # type: sq_string # elements: 1 # length: 34 Range of each predictor at a node. # name: # type: sq_string # elements: 1 # length: 29 CompactRegressionTree.predict # name: # type: sq_string # elements: 1 # length: 966 CompactRegressionTree: yFit = predict (obj, XC) CompactRegressionTree: [yFit, node] = predict (...) Predict the response with a trained CompactRegressionTree object. yFit = predict (obj, XC) sends each row of XC down the tree and returns the mean response of the node it comes to rest at, after ResponseTransform. XC must have as many columns as the predictor data the model was fitted on. XC may also be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. [yFit, node] = predict (...) also returns the number of the node each row landed in. A row missing the predictor a node cuts on is stopped at that node and takes its answer, rather than being sent down a branch on evidence the row does not carry. See also: CompactRegressionTree, fitrtree # name: # type: sq_string # elements: 1 # length: 65 Predict the response with a trained CompactRegressionTree object. # name: # type: sq_string # elements: 1 # length: 41 CompactRegressionTree.predictorImportance # name: # type: sq_string # elements: 1 # length: 701 CompactRegressionTree: imp = predictorImportance (obj) Estimate the importance of each predictor. imp = predictorImportance (obj) returns a row vector with one element per predictor, holding the total drop in risk the splits on that predictor bring about, divided by the number of branch nodes. A predictor the tree never splits on scores zero. The drop at a branch node is its NodeRisk less the risk of its two children and less what it holds back, so a predictor that is chosen often, high up, and on nodes it separates well, scores highest. The numbers are comparable between predictors of one tree and not between trees. See also: CompactRegressionTree, fitrtree, CompactRegressionTree.NodeRisk # name: # type: sq_string # elements: 1 # length: 42 Estimate the importance of each predictor. # name: # type: sq_string # elements: 1 # length: 31 CompactRegressionTree.savemodel # name: # type: sq_string # elements: 1 # length: 472 CompactRegressionTree: savemodel (obj, filename) Save a CompactRegressionTree model to a file. savemodel (obj, filename) saves each property of a CompactRegressionTree object into an Octave binary file, the name of which is specified in filename, along with an extra variable, which defines the type of regression object these variables constitute. Use loadmodel in order to load a regression object into Octave's workspace. See also: loadmodel, fitrtree, RegressionTree # name: # type: sq_string # elements: 1 # length: 45 Save a CompactRegressionTree model to a file. # name: # type: sq_string # elements: 1 # length: 26 CompactRegressionTree.view # name: # type: sq_string # elements: 1 # length: 400 CompactRegressionTree: view (obj) Print the tree as text. view (obj) prints one line per node: a branch node names the predictor it cuts on, the cut point, and the node each side leads to, and a leaf names the response it fits. A branch node's line ends with the response it would fit itself, which is the answer an observation missing that predictor gets. See also: CompactRegressionTree, fitrtree # name: # type: sq_string # elements: 1 # length: 23 Print the tree as text. # name: # type: sq_string # elements: 1 # length: 17 CompactTreeBagger # name: # type: sq_string # elements: 1 # length: 546 statistics: CompactTreeBagger Compact ensemble of bagged decision trees A CompactTreeBagger object carries the trees a TreeBagger ensemble grew and what prediction needs, but not the observations it was fitted on, and so none of the out-of-bag information. It predicts new data identically to the ensemble it came from. Create one with the compact method of a TreeBagger object. Two compact ensembles fitted on the same classes are joined with combine. See also: TreeBagger, TreeBagger.compact, CompactClassificationTree, CompactRegressionTree # name: # type: sq_string # elements: 1 # length: 41 Compact ensemble of bagged decision trees # name: # type: sq_string # elements: 1 # length: 39 CompactTreeBagger.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 221 CompactTreeBagger: property CategoricalPredictors Indices of the categorical predictors A row vector of column indices into X, naming the predictors treated as categorical, empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 28 CompactTreeBagger.ClassNames # name: # type: sq_string # elements: 1 # length: 250 CompactTreeBagger: property ClassNames Names of the classes The classes of a classification ensemble, in the type of the response it was fitted on and in the order its scores are laid out. Empty for a regression ensemble. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Names of the classes # name: # type: sq_string # elements: 1 # length: 35 CompactTreeBagger.CompactTreeBagger # name: # type: sq_string # elements: 1 # length: 231 CompactTreeBagger: obj = CompactTreeBagger (B) Create a CompactTreeBagger object. B is the TreeBagger object to compact. The documented way to reach this constructor is the compact method. See also: TreeBagger, TreeBagger.compact # name: # type: sq_string # elements: 1 # length: 34 Create a CompactTreeBagger object. # name: # type: sq_string # elements: 1 # length: 29 CompactTreeBagger.DefaultYfit # name: # type: sq_string # elements: 1 # length: 390 CompactTreeBagger: property DefaultYfit Prediction for an observation no tree may answer for For classification, the class of greatest prior probability, in the type of the class names, or the missing label of that type after setDefaultYfit with ''. For regression, the weighted mean of the training response unless set otherwise. This property is read-only; change it with setDefaultYfit. # name: # type: sq_string # elements: 1 # length: 52 Prediction for an observation no tree may answer for # name: # type: sq_string # elements: 1 # length: 45 CompactTreeBagger.DeltaCriterionDecisionSplit # name: # type: sq_string # elements: 1 # length: 237 CompactTreeBagger: property DeltaCriterionDecisionSplit Split criterion contributions of the predictors A row vector with one element per predictor, the mean over the trees of each tree's predictorImportance. This property is read-only. # name: # type: sq_string # elements: 1 # length: 47 Split criterion contributions of the predictors # name: # type: sq_string # elements: 1 # length: 24 CompactTreeBagger.Method # name: # type: sq_string # elements: 1 # length: 118 CompactTreeBagger: property Method Type of the ensemble 'classification' or 'regression'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Type of the ensemble # name: # type: sq_string # elements: 1 # length: 35 CompactTreeBagger.NumPredictorSplit # name: # type: sq_string # elements: 1 # length: 288 CompactTreeBagger: property NumPredictorSplit Decision splits on each predictor A row vector with one element per predictor, the sum over the trees of the share of each tree's branch nodes that split on the predictor. A tree without branch nodes adds nothing. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Decision splits on each predictor # name: # type: sq_string # elements: 1 # length: 26 CompactTreeBagger.NumTrees # name: # type: sq_string # elements: 1 # length: 141 CompactTreeBagger: property NumTrees Number of trees A nonnegative integer, the number of trees in the ensemble. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Number of trees # name: # type: sq_string # elements: 1 # length: 32 CompactTreeBagger.PredictorNames # name: # type: sq_string # elements: 1 # length: 171 CompactTreeBagger: property PredictorNames Names of the predictors A cell array of character vectors naming the columns of the predictor data. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 38 CompactTreeBagger.SurrogateAssociation # name: # type: sq_string # elements: 1 # length: 248 CompactTreeBagger: property SurrogateAssociation Predictive association between the predictors A square matrix with one row and one column per predictor. The trees grow no surrogate splits, so it is the identity matrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 45 Predictive association between the predictors # name: # type: sq_string # elements: 1 # length: 23 CompactTreeBagger.Trees # name: # type: sq_string # elements: 1 # length: 188 CompactTreeBagger: property Trees The trees of the ensemble A column cell array holding one CompactClassificationTree or CompactRegressionTree object per tree. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 The trees of the ensemble # name: # type: sq_string # elements: 1 # length: 25 CompactTreeBagger.combine # name: # type: sq_string # elements: 1 # length: 309 CompactTreeBagger: C = combine (C1, C2) Join two compact ensembles. C holds the trees of C1 followed by those of C2. The two must be of the same type and, for classification, have the same class names and the same default scores. C keeps the DefaultYfit of C1. See also: CompactTreeBagger, TreeBagger.append # name: # type: sq_string # elements: 1 # length: 27 Join two compact ensembles. # name: # type: sq_string # elements: 1 # length: 23 CompactTreeBagger.error # name: # type: sq_string # elements: 1 # length: 1732 CompactTreeBagger: err = error (obj, X, Y) CompactTreeBagger: err = error (..., name, value) CompactTreeBagger: err = error (obj, Tbl, ResponseVarName) CompactTreeBagger: err = error (obj, Tbl) Misclassification probability or mean squared error of the ensemble. For a classification ensemble err is the weighted share of the rows of X whose predicted class differs from Y; for a regression ensemble it is the weighted mean squared error. By default err is a column with one element per tree, the error of the first tree, then of the first two, and so on. An observation no tree may answer for is predicted as DefaultYfit and counted; when DefaultYfit is the missing label it has no prediction and is left out. Name-Value arguments: Name Value 'Mode' 'cumulative' (default), 'individual' for the error of each tree on its own, or 'ensemble' for a single error over every tree used. 'Trees' 'all' (default) or a vector of indices of the trees to use, in the order they are accumulated. 'TreeWeights' A nonnegative vector with one weight per tree used. It may not be given in 'individual' mode. 'UseInstanceForTree' An NxNumTrees logical matrix saying which tree may answer for which observation. 'Weights' A nonnegative vector with one weight per observation. The default is uniform. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. error (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and error (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: CompactTreeBagger, CompactTreeBagger.predict, CompactTreeBagger.meanMargin, TreeBagger.oobError # name: # type: sq_string # elements: 1 # length: 68 Misclassification probability or mean squared error of the ensemble. # name: # type: sq_string # elements: 1 # length: 24 CompactTreeBagger.margin # name: # type: sq_string # elements: 1 # length: 1118 CompactTreeBagger: m = margin (obj, X, Y) CompactTreeBagger: m = margin (..., name, value) CompactTreeBagger: m = margin (obj, Tbl, ResponseVarName) CompactTreeBagger: m = margin (obj, Tbl) Classification margin of each observation. The margin is the score of the true class less the highest score among the other classes. By default m has one row per observation and one column per tree, the margin of the first tree, then of the first two, and so on. An observation left without a prediction, as described under CompactTreeBagger.error, has a NaN margin. 'Mode', 'Trees', 'TreeWeights' and 'UseInstanceForTree' are taken as by CompactTreeBagger.error. A regression ensemble has no margins. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: CompactTreeBagger, CompactTreeBagger.meanMargin, CompactTreeBagger.error # name: # type: sq_string # elements: 1 # length: 42 Classification margin of each observation. # name: # type: sq_string # elements: 1 # length: 25 CompactTreeBagger.mdsprox # name: # type: sq_string # elements: 1 # length: 1450 CompactTreeBagger: [S, E] = mdsprox (obj, X) CompactTreeBagger: [S, E] = mdsprox (..., name, value) Multidimensional scaling of the proximity matrix. Applies classical multidimensional scaling, as cmdscale does, to the distances 1 - prox, prox being the proximity matrix of the rows of X. S holds the scaled coordinates, one column per positive eigenvalue, and E the eigenvalues. Name-Value arguments: Name Value 'Data' 'predictors' (default) or 'proximity', as for outlierMeasure. 'Colors' A character vector with one color letter per class. When given, the scaled coordinates are drawn as overlaid scatter plots, one per class, and a class beyond the number of letters is not drawn. 'Labels' The class label of each observation, each one of ClassNames. Classification only. Without labels every observation is drawn in the first color. 'MDSCoordinates' Two or three indices of the columns of S to draw. The default is [1, 2]. They must not exceed the number of columns of S even when nothing is drawn, as in MATLAB, whose documentation says otherwise. X holding predictor data may also be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: CompactTreeBagger, CompactTreeBagger.proximity, cmdscale, TreeBagger.mdsprox # name: # type: sq_string # elements: 1 # length: 49 Multidimensional scaling of the proximity matrix. # name: # type: sq_string # elements: 1 # length: 28 CompactTreeBagger.meanMargin # name: # type: sq_string # elements: 1 # length: 873 CompactTreeBagger: mm = meanMargin (obj, X, Y) CompactTreeBagger: mm = meanMargin (..., name, value) CompactTreeBagger: mm = meanMargin (obj, Tbl, ResponseVarName) CompactTreeBagger: mm = meanMargin (obj, Tbl) Weighted mean classification margin. mm is the weighted mean over the observations of the margins CompactTreeBagger.margin returns, a row with one element per tree by default. The Name-Value arguments are those of CompactTreeBagger.error, 'Weights' included. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. meanMargin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and meanMargin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: CompactTreeBagger, CompactTreeBagger.margin # name: # type: sq_string # elements: 1 # length: 36 Weighted mean classification margin. # name: # type: sq_string # elements: 1 # length: 32 CompactTreeBagger.outlierMeasure # name: # type: sq_string # elements: 1 # length: 1434 CompactTreeBagger: out = outlierMeasure (obj, X) CompactTreeBagger: out = outlierMeasure (..., name, value) Outlier measure of each observation. The raw measure of an observation is the size of its class divided by the sum of its squared proximities to the observations of the class, itself included, all the observations forming one class when no labels are given. out is a column holding, for each observation, the absolute deviation of its raw measure from the median of its class, divided by the median absolute deviation of the class. A large value marks an observation that the trees seldom group with the rest of its class. As in MATLAB, a class whose median absolute deviation is zero gives the raw measures themselves, and a class of one or two observations gives zeros. Name-Value arguments: Name Value 'Data' 'predictors' (default), for X holding predictor data, or 'proximity', for X holding a proximity matrix such as proximity returns. 'Labels' The class label of each observation, each one of ClassNames. Classification only. X holding predictor data may also be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: CompactTreeBagger, CompactTreeBagger.proximity, TreeBagger.OutlierMeasure # name: # type: sq_string # elements: 1 # length: 36 Outlier measure of each observation. # name: # type: sq_string # elements: 1 # length: 25 CompactTreeBagger.predict # name: # type: sq_string # elements: 1 # length: 1967 CompactTreeBagger: label = predict (obj, X) CompactTreeBagger: [label, scores] = predict (...) CompactTreeBagger: [label, scores, stdevs] = predict (...) CompactTreeBagger: [Yfit, stdevs] = predict (...) CompactTreeBagger: ... = predict (..., name, value) Predict responses with a compact bagged ensemble. For a classification ensemble, label holds the predicted class of each row of X, in the type of ClassNames. scores is an NxK matrix, the weighted average over the trees of the class probability each tree gives, and stdevs holds their standard deviations over the trees. The label is the class of highest score, whatever cost matrix the trees were grown with, as MATLAB documents. For a regression ensemble, Yfit is the weighted average of the trees' predictions and stdevs their standard deviation. The standard deviations are population deviations, taken over the trees that answer for the observation with their weights. An observation no tree may answer for is given DefaultYfit, with the prior as its scores and NaN as its deviations. Name-Value arguments: Name Value 'Trees' 'all' (default) or a vector of indices of the trees to use. 'TreeWeights' A nonnegative vector with one weight per tree used. The default weighs them equally. 'UseInstanceForTree' An NxNumTrees logical matrix saying which tree may answer for which observation. The default lets every tree answer for every observation. MATLAB returns the labels as a cell array of character vectors whatever the type of the response; they are returned here in the type of ClassNames, as by every other classifier in this package. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: CompactTreeBagger, CompactTreeBagger.error, TreeBagger.predict # name: # type: sq_string # elements: 1 # length: 49 Predict responses with a compact bagged ensemble. # name: # type: sq_string # elements: 1 # length: 27 CompactTreeBagger.proximity # name: # type: sq_string # elements: 1 # length: 663 CompactTreeBagger: prox = proximity (obj, X) Proximity matrix of the observations. prox is a symmetric NxN matrix, N being the number of rows of X, whose element (i,j) is the share of the trees that bring observations i and j to the same leaf. Its diagonal holds ones. X may also be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: CompactTreeBagger, CompactTreeBagger.outlierMeasure, CompactTreeBagger.mdsprox, TreeBagger.fillprox # name: # type: sq_string # elements: 1 # length: 37 Proximity matrix of the observations. # name: # type: sq_string # elements: 1 # length: 32 CompactTreeBagger.setDefaultYfit # name: # type: sq_string # elements: 1 # length: 477 CompactTreeBagger: C = setDefaultYfit (obj, Yfit) Set the prediction for an observation no tree may answer for. For a classification ensemble Yfit is 'MostPopular', the class of greatest prior probability, or '', the missing label, which leaves such an observation without a prediction and out of the error and the mean margin. A logical response has no missing label. For a regression ensemble Yfit is a numeric scalar. See also: CompactTreeBagger, CompactTreeBagger.predict # name: # type: sq_string # elements: 1 # length: 61 Set the prediction for an observation no tree may answer for. # name: # type: sq_string # elements: 1 # length: 15 PredictiveModel # name: # type: sq_string # elements: 1 # length: 886 statistics: PredictiveModel A fitted model that answers for new observations. PredictiveModel is the superclass of every classification and regression model in this package that carries a predict method, and of nothing else. The models fitted by fitlm and its relatives derive from it as the learners do. The cross-validated classes answer through kfoldPredict over their folds rather than through predict, and so do not derive from it. The class is abstract and cannot be instantiated. It is where behaviour shared by all those models is written once rather than once per class, and isa (obj, 'PredictiveModel') is how to ask whether an object is a model that can be told to predict. The only data it holds is the levels a predictor read from a table was coded through, which every model needs to read a table again at prediction and which MATLAB carries by keeping the table itself. # name: # type: sq_string # elements: 1 # length: 49 A fitted model that answers for new observations. # name: # type: sq_string # elements: 1 # length: 33 PredictiveModel.partialDependence # name: # type: sq_string # elements: 1 # length: 597 PredictiveModel: pd = partialDependence (obj, Vars) PredictiveModel: pd = partialDependence (obj, Vars, Labels) PredictiveModel: pd = partialDependence (..., Data) PredictiveModel: pd = partialDependence (..., name, value) PredictiveModel: [pd, x, y] = partialDependence (...) Compute partial dependence. partialDependence (obj, Vars) is the same call as partialDependence the function, written on the model instead of before it, and takes and returns exactly what it does. See partialDependence for the arguments and for what pd, x and y hold. See also: partialDependence, plotPartialDependence # name: # type: sq_string # elements: 1 # length: 27 Compute partial dependence. # name: # type: sq_string # elements: 1 # length: 24 RegressionBaggedEnsemble # name: # type: sq_string # elements: 1 # length: 740 statistics: RegressionBaggedEnsemble Bagged ensemble of regression trees A RegressionBaggedEnsemble object holds regression trees each grown on a sample drawn from the training data in proportion to the observation weights, each split chosen from ceil (P / 3) predictors drawn afresh at every node. It predicts by averaging its trees. Create one with fitrensemble and 'Method' set to 'Bag'. It carries everything a RegressionEnsemble does, and which rows each tree drew. LSBoost that resamples, asked for with 'Resample', 'FResample' or 'Replace', returns this class too: its trees are boosted and summed by their weights, each fitted on the rows it drew. See also: fitrensemble, RegressionEnsemble, CompactRegressionEnsemble, TreeBagger # name: # type: sq_string # elements: 1 # length: 35 Bagged ensemble of regression trees # name: # type: sq_string # elements: 1 # length: 33 RegressionBaggedEnsemble.BinEdges # name: # type: sq_string # elements: 1 # length: 139 RegressionEnsemble: property BinEdges Bin edges of the predictors Always empty, binning not being implemented. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Bin edges of the predictors # name: # type: sq_string # elements: 1 # length: 46 RegressionBaggedEnsemble.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 182 RegressionEnsemble: property CategoricalPredictors Indices of categorical predictors The predictors every tree treats as categorical, empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Indices of categorical predictors # name: # type: sq_string # elements: 1 # length: 39 RegressionBaggedEnsemble.CombineWeights # name: # type: sq_string # elements: 1 # length: 153 RegressionEnsemble: property CombineWeights How the trees are combined 'WeightedSum' for LSBoost, 'WeightedAverage' for Bag. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 How the trees are combined # name: # type: sq_string # elements: 1 # length: 47 RegressionBaggedEnsemble.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 157 RegressionEnsemble: property ExpandedPredictorNames Names of the predictors as the learners saw them The same as PredictorNames. This property is read-only. # name: # type: sq_string # elements: 1 # length: 48 Names of the predictors as the learners saw them # name: # type: sq_string # elements: 1 # length: 34 RegressionBaggedEnsemble.FResample # name: # type: sq_string # elements: 1 # length: 215 RegressionBaggedEnsemble: property FResample Share of the observations each tree draws A number greater than 0 and no greater than 1, each tree drawing ceil (FResample * N) observations. This property is read-only. # name: # type: sq_string # elements: 1 # length: 41 Share of the observations each tree draws # name: # type: sq_string # elements: 1 # length: 32 RegressionBaggedEnsemble.FitInfo # name: # type: sq_string # elements: 1 # length: 207 RegressionEnsemble: property FitInfo Fit information For LSBoost, a column with the weighted mean squared error of each tree against the residual it was fitted to. Empty for Bag. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Fit information # name: # type: sq_string # elements: 1 # length: 43 RegressionBaggedEnsemble.FitInfoDescription # name: # type: sq_string # elements: 1 # length: 99 RegressionEnsemble: property FitInfoDescription Description of FitInfo This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Description of FitInfo # name: # type: sq_string # elements: 1 # length: 58 RegressionBaggedEnsemble.HyperparameterOptimizationResults # name: # type: sq_string # elements: 1 # length: 188 RegressionEnsemble: property HyperparameterOptimizationResults Results of optimizing the hyperparameters Always empty, such optimization not being implemented. This property is read-only. # name: # type: sq_string # elements: 1 # length: 41 Results of optimizing the hyperparameters # name: # type: sq_string # elements: 1 # length: 37 RegressionBaggedEnsemble.LearnerNames # name: # type: sq_string # elements: 1 # length: 114 RegressionEnsemble: property LearnerNames Names of the weak learners Always {'Tree'}. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Names of the weak learners # name: # type: sq_string # elements: 1 # length: 31 RegressionBaggedEnsemble.Method # name: # type: sq_string # elements: 1 # length: 100 RegressionEnsemble: property Method Ensemble method 'LSBoost' or 'Bag'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Ensemble method # name: # type: sq_string # elements: 1 # length: 40 RegressionBaggedEnsemble.ModelParameters # name: # type: sq_string # elements: 1 # length: 238 RegressionEnsemble: property ModelParameters Parameters of the fit A structure with the fields Type, Method, LearnerTemplates, NLearn, the number of learning cycles asked for in all, and for LSBoost LearnRate. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Parameters of the fit # name: # type: sq_string # elements: 1 # length: 40 RegressionBaggedEnsemble.NumObservations # name: # type: sq_string # elements: 1 # length: 101 RegressionEnsemble: property NumObservations Number of observations used This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Number of observations used # name: # type: sq_string # elements: 1 # length: 35 RegressionBaggedEnsemble.NumTrained # name: # type: sq_string # elements: 1 # length: 92 RegressionEnsemble: property NumTrained Number of trained trees This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Number of trained trees # name: # type: sq_string # elements: 1 # length: 39 RegressionBaggedEnsemble.PredictorNames # name: # type: sq_string # elements: 1 # length: 131 RegressionEnsemble: property PredictorNames Names of the predictors A cell array of character vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 45 RegressionBaggedEnsemble.ReasonForTermination # name: # type: sq_string # elements: 1 # length: 111 RegressionEnsemble: property ReasonForTermination Why the fit stopped adding trees This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Why the fit stopped adding trees # name: # type: sq_string # elements: 1 # length: 49 RegressionBaggedEnsemble.RegressionBaggedEnsemble # name: # type: sq_string # elements: 1 # length: 335 RegressionBaggedEnsemble: obj = RegressionBaggedEnsemble (X, Y) RegressionBaggedEnsemble: obj = RegressionBaggedEnsemble (..., name, value) Fit a bagged ensemble of regression trees. fitrensemble with 'Method' set to 'Bag' is the documented way in, and its help lists the options both take. See also: fitrensemble, RegressionEnsemble # name: # type: sq_string # elements: 1 # length: 42 Fit a bagged ensemble of regression trees. # name: # type: sq_string # elements: 1 # length: 39 RegressionBaggedEnsemble.Regularization # name: # type: sq_string # elements: 1 # length: 218 RegressionEnsemble: property Regularization Result of regularizing the ensemble Empty until regularize fills it with a structure of lasso weights for the trees, and emptied again by resume. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Result of regularizing the ensemble # name: # type: sq_string # elements: 1 # length: 32 RegressionBaggedEnsemble.Replace # name: # type: sq_string # elements: 1 # length: 146 RegressionBaggedEnsemble: property Replace Whether the trees draw with replacement A logical scalar, true by default. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Whether the trees draw with replacement # name: # type: sq_string # elements: 1 # length: 37 RegressionBaggedEnsemble.ResponseName # name: # type: sq_string # elements: 1 # length: 100 RegressionEnsemble: property ResponseName Name of the response variable This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 42 RegressionBaggedEnsemble.ResponseTransform # name: # type: sq_string # elements: 1 # length: 140 RegressionEnsemble: property ResponseTransform Transform applied to the predicted response See CompactRegressionEnsemble.ResponseTransform. # name: # type: sq_string # elements: 1 # length: 43 Transform applied to the predicted response # name: # type: sq_string # elements: 1 # length: 33 RegressionBaggedEnsemble.RowsUsed # name: # type: sq_string # elements: 1 # length: 142 RegressionEnsemble: property RowsUsed Rows of the data that were used A logical column over the rows as supplied. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Rows of the data that were used # name: # type: sq_string # elements: 1 # length: 32 RegressionBaggedEnsemble.Trained # name: # type: sq_string # elements: 1 # length: 133 RegressionEnsemble: property Trained Trained trees A column cell array of CompactRegressionTree objects. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Trained trees # name: # type: sq_string # elements: 1 # length: 39 RegressionBaggedEnsemble.TrainedWeights # name: # type: sq_string # elements: 1 # length: 136 RegressionEnsemble: property TrainedWeights Weights of the trained trees A column with one weight per tree. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Weights of the trained trees # name: # type: sq_string # elements: 1 # length: 41 RegressionBaggedEnsemble.UseObsForLearner # name: # type: sq_string # elements: 1 # length: 195 RegressionBaggedEnsemble: property UseObsForLearner Which observations each tree drew An NxNumTrained logical matrix, true where a tree's sample holds an observation. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Which observations each tree drew # name: # type: sq_string # elements: 1 # length: 42 RegressionBaggedEnsemble.UsePredForLearner # name: # type: sq_string # elements: 1 # length: 164 RegressionEnsemble: property UsePredForLearner Which predictors each learner uses Always empty, as MATLAB returns it for tree learners. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Which predictors each learner uses # name: # type: sq_string # elements: 1 # length: 26 RegressionBaggedEnsemble.W # name: # type: sq_string # elements: 1 # length: 183 RegressionEnsemble: property W Observation weights The weights given, normalized to sum to one. It has the class of the 'Weights' given, single or double. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 26 RegressionBaggedEnsemble.X # name: # type: sq_string # elements: 1 # length: 335 RegressionEnsemble: property X Predictor data The predictors the ensemble was fitted on, one row per observation. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 26 RegressionBaggedEnsemble.Y # name: # type: sq_string # elements: 1 # length: 158 RegressionEnsemble: property Y Response data The response the ensemble was fitted on, a row missing a value having been left out. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Response data # name: # type: sq_string # elements: 1 # length: 32 RegressionBaggedEnsemble.compact # name: # type: sq_string # elements: 1 # length: 241 RegressionBaggedEnsemble: CMdl = compact (obj) Drop the training data from a bagged regression ensemble. Returns a CompactRegressionEnsemble, as RegressionEnsemble.compact does. See also: RegressionBaggedEnsemble, CompactRegressionEnsemble # name: # type: sq_string # elements: 1 # length: 57 Drop the training data from a bagged regression ensemble. # name: # type: sq_string # elements: 1 # length: 33 RegressionBaggedEnsemble.crossval # name: # type: sq_string # elements: 1 # length: 295 RegressionBaggedEnsemble: CVMdl = crossval (obj) RegressionBaggedEnsemble: CVMdl = crossval (..., name, value) Cross-validate a bagged ensemble. Behaves as RegressionEnsemble.crossval, returning a RegressionPartitionedEnsemble. See also: RegressionBaggedEnsemble, RegressionPartitionedEnsemble # name: # type: sq_string # elements: 1 # length: 33 Cross-validate a bagged ensemble. # name: # type: sq_string # elements: 1 # length: 33 RegressionBaggedEnsemble.cvshrink # name: # type: sq_string # elements: 1 # length: 285 RegressionBaggedEnsemble: [vals, nlearn] = cvshrink (obj) RegressionBaggedEnsemble: [vals, nlearn] = cvshrink (..., name, value) Cross-validate the shrinking of a bagged ensemble. Behaves as RegressionEnsemble.cvshrink. See also: RegressionBaggedEnsemble, RegressionEnsemble.cvshrink # name: # type: sq_string # elements: 1 # length: 50 Cross-validate the shrinking of a bagged ensemble. # name: # type: sq_string # elements: 1 # length: 29 RegressionBaggedEnsemble.loss # name: # type: sq_string # elements: 1 # length: 696 RegressionBaggedEnsemble: L = loss (obj, X, Y) RegressionBaggedEnsemble: L = loss (obj, Tbl, ResponseVarName) RegressionBaggedEnsemble: L = loss (obj, Tbl) RegressionBaggedEnsemble: L = loss (..., name, value) Regression loss of a bagged ensemble. Behaves as CompactRegressionEnsemble.loss. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: RegressionBaggedEnsemble, CompactRegressionEnsemble.loss # name: # type: sq_string # elements: 1 # length: 37 Regression loss of a bagged ensemble. # name: # type: sq_string # elements: 1 # length: 32 RegressionBaggedEnsemble.oobLoss # name: # type: sq_string # elements: 1 # length: 436 RegressionBaggedEnsemble: L = oobLoss (obj) RegressionBaggedEnsemble: L = oobLoss (..., name, value) Out-of-bag regression loss. The loss of the out-of-bag predictions against Y, weighted by W, an observation in the sample of every tree used being left out. 'LossFun' and 'Mode' are taken as by CompactRegressionEnsemble.loss, and 'Learners' restricts the trees. See also: RegressionBaggedEnsemble, RegressionBaggedEnsemble.oobPredict # name: # type: sq_string # elements: 1 # length: 27 Out-of-bag regression loss. # name: # type: sq_string # elements: 1 # length: 55 RegressionBaggedEnsemble.oobPermutedPredictorImportance # name: # type: sq_string # elements: 1 # length: 644 RegressionBaggedEnsemble: imp = oobPermutedPredictorImportance (obj) RegressionBaggedEnsemble: imp = oobPermutedPredictorImportance (..., 'Learners', idx) Out-of-bag predictor importance by permutation. For each tree, the values of each predictor are permuted among the observations out of its bag, and the tree's mean squared error on them, weighted by W, is taken before and after. imp holds, for each predictor, the mean of the rise over the trees divided by its standard deviation over the trees, zero where the mean is zero. 'Learners' restricts the trees. See also: RegressionBaggedEnsemble, RegressionBaggedEnsemble.predictorImportance # name: # type: sq_string # elements: 1 # length: 47 Out-of-bag predictor importance by permutation. # name: # type: sq_string # elements: 1 # length: 35 RegressionBaggedEnsemble.oobPredict # name: # type: sq_string # elements: 1 # length: 463 RegressionBaggedEnsemble: yfit = oobPredict (obj) RegressionBaggedEnsemble: yfit = oobPredict (..., 'Learners', idx) Out-of-bag predictions for the training data. Each training observation is predicted by the trees whose samples left it out, as predict predicts it with 'UseObsForLearner' set to ! UseObsForLearner; one in the sample of every tree used is NaN. 'Learners' restricts the trees. See also: RegressionBaggedEnsemble, RegressionBaggedEnsemble.oobLoss # name: # type: sq_string # elements: 1 # length: 45 Out-of-bag predictions for the training data. # name: # type: sq_string # elements: 1 # length: 32 RegressionBaggedEnsemble.predict # name: # type: sq_string # elements: 1 # length: 283 RegressionBaggedEnsemble: yfit = predict (obj, X) RegressionBaggedEnsemble: yfit = predict (..., name, value) Predict the response with a bagged regression ensemble. Behaves as CompactRegressionEnsemble.predict. See also: RegressionBaggedEnsemble, CompactRegressionEnsemble.predict # name: # type: sq_string # elements: 1 # length: 55 Predict the response with a bagged regression ensemble. # name: # type: sq_string # elements: 1 # length: 44 RegressionBaggedEnsemble.predictorImportance # name: # type: sq_string # elements: 1 # length: 380 RegressionBaggedEnsemble: imp = predictorImportance (obj) RegressionBaggedEnsemble: [imp, ma] = predictorImportance (obj) Estimate the importance of each predictor. The mean over the trees of each tree's predictorImportance, as CompactRegressionEnsemble.predictorImportance computes it. See also: RegressionBaggedEnsemble, RegressionBaggedEnsemble.oobPermutedPredictorImportance # name: # type: sq_string # elements: 1 # length: 42 Estimate the importance of each predictor. # name: # type: sq_string # elements: 1 # length: 35 RegressionBaggedEnsemble.regularize # name: # type: sq_string # elements: 1 # length: 271 RegressionBaggedEnsemble: B = regularize (obj) RegressionBaggedEnsemble: B = regularize (..., name, value) Find lasso weights for the trees of a bagged ensemble. Behaves as RegressionEnsemble.regularize. See also: RegressionBaggedEnsemble, RegressionEnsemble.regularize # name: # type: sq_string # elements: 1 # length: 54 Find lasso weights for the trees of a bagged ensemble. # name: # type: sq_string # elements: 1 # length: 34 RegressionBaggedEnsemble.resubLoss # name: # type: sq_string # elements: 1 # length: 271 RegressionBaggedEnsemble: L = resubLoss (obj) RegressionBaggedEnsemble: L = resubLoss (..., name, value) Regression loss of a bagged ensemble on the training data. Behaves as RegressionEnsemble.resubLoss. See also: RegressionBaggedEnsemble, RegressionEnsemble.resubLoss # name: # type: sq_string # elements: 1 # length: 58 Regression loss of a bagged ensemble on the training data. # name: # type: sq_string # elements: 1 # length: 37 RegressionBaggedEnsemble.resubPredict # name: # type: sq_string # elements: 1 # length: 296 RegressionBaggedEnsemble: yfit = resubPredict (obj) RegressionBaggedEnsemble: yfit = resubPredict (..., name, value) Predict the response of the training data with a bagged ensemble. Behaves as RegressionEnsemble.resubPredict. See also: RegressionBaggedEnsemble, RegressionEnsemble.resubPredict # name: # type: sq_string # elements: 1 # length: 65 Predict the response of the training data with a bagged ensemble. # name: # type: sq_string # elements: 1 # length: 31 RegressionBaggedEnsemble.resume # name: # type: sq_string # elements: 1 # length: 318 RegressionBaggedEnsemble: B = resume (obj, NumLearningCycles) RegressionBaggedEnsemble: B = resume (..., 'NPrint', n) Grow more trees in a bagged regression ensemble. Behaves as RegressionEnsemble.resume, the new trees' samples added to UseObsForLearner. See also: RegressionBaggedEnsemble, RegressionEnsemble.resume # name: # type: sq_string # elements: 1 # length: 48 Grow more trees in a bagged regression ensemble. # name: # type: sq_string # elements: 1 # length: 31 RegressionBaggedEnsemble.shrink # name: # type: sq_string # elements: 1 # length: 304 RegressionBaggedEnsemble: C = shrink (obj) RegressionBaggedEnsemble: C = shrink (..., name, value) Keep the trees of a bagged ensemble that a lasso weight retains. Behaves as RegressionEnsemble.shrink, returning a CompactRegressionEnsemble. See also: RegressionBaggedEnsemble, RegressionEnsemble.shrink # name: # type: sq_string # elements: 1 # length: 64 Keep the trees of a bagged ensemble that a lasso weight retains. # name: # type: sq_string # elements: 1 # length: 18 RegressionEnsemble # name: # type: sq_string # elements: 1 # length: 473 statistics: RegressionEnsemble Boosted ensemble of regression trees A RegressionEnsemble object holds the regression trees LSBoost grew one after another, each fitted to the residual the trees before it left, together with the data it was fitted on. Create one with fitrensemble. A bagged ensemble is a RegressionBaggedEnsemble, and compact returns a CompactRegressionEnsemble without the data. See also: fitrensemble, CompactRegressionEnsemble, RegressionBaggedEnsemble # name: # type: sq_string # elements: 1 # length: 36 Boosted ensemble of regression trees # name: # type: sq_string # elements: 1 # length: 27 RegressionEnsemble.BinEdges # name: # type: sq_string # elements: 1 # length: 139 RegressionEnsemble: property BinEdges Bin edges of the predictors Always empty, binning not being implemented. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Bin edges of the predictors # name: # type: sq_string # elements: 1 # length: 40 RegressionEnsemble.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 182 RegressionEnsemble: property CategoricalPredictors Indices of categorical predictors The predictors every tree treats as categorical, empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Indices of categorical predictors # name: # type: sq_string # elements: 1 # length: 33 RegressionEnsemble.CombineWeights # name: # type: sq_string # elements: 1 # length: 153 RegressionEnsemble: property CombineWeights How the trees are combined 'WeightedSum' for LSBoost, 'WeightedAverage' for Bag. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 How the trees are combined # name: # type: sq_string # elements: 1 # length: 41 RegressionEnsemble.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 157 RegressionEnsemble: property ExpandedPredictorNames Names of the predictors as the learners saw them The same as PredictorNames. This property is read-only. # name: # type: sq_string # elements: 1 # length: 48 Names of the predictors as the learners saw them # name: # type: sq_string # elements: 1 # length: 26 RegressionEnsemble.FitInfo # name: # type: sq_string # elements: 1 # length: 207 RegressionEnsemble: property FitInfo Fit information For LSBoost, a column with the weighted mean squared error of each tree against the residual it was fitted to. Empty for Bag. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Fit information # name: # type: sq_string # elements: 1 # length: 37 RegressionEnsemble.FitInfoDescription # name: # type: sq_string # elements: 1 # length: 99 RegressionEnsemble: property FitInfoDescription Description of FitInfo This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Description of FitInfo # name: # type: sq_string # elements: 1 # length: 52 RegressionEnsemble.HyperparameterOptimizationResults # name: # type: sq_string # elements: 1 # length: 188 RegressionEnsemble: property HyperparameterOptimizationResults Results of optimizing the hyperparameters Always empty, such optimization not being implemented. This property is read-only. # name: # type: sq_string # elements: 1 # length: 41 Results of optimizing the hyperparameters # name: # type: sq_string # elements: 1 # length: 31 RegressionEnsemble.LearnerNames # name: # type: sq_string # elements: 1 # length: 114 RegressionEnsemble: property LearnerNames Names of the weak learners Always {'Tree'}. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Names of the weak learners # name: # type: sq_string # elements: 1 # length: 25 RegressionEnsemble.Method # name: # type: sq_string # elements: 1 # length: 100 RegressionEnsemble: property Method Ensemble method 'LSBoost' or 'Bag'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Ensemble method # name: # type: sq_string # elements: 1 # length: 34 RegressionEnsemble.ModelParameters # name: # type: sq_string # elements: 1 # length: 238 RegressionEnsemble: property ModelParameters Parameters of the fit A structure with the fields Type, Method, LearnerTemplates, NLearn, the number of learning cycles asked for in all, and for LSBoost LearnRate. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Parameters of the fit # name: # type: sq_string # elements: 1 # length: 34 RegressionEnsemble.NumObservations # name: # type: sq_string # elements: 1 # length: 101 RegressionEnsemble: property NumObservations Number of observations used This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Number of observations used # name: # type: sq_string # elements: 1 # length: 29 RegressionEnsemble.NumTrained # name: # type: sq_string # elements: 1 # length: 92 RegressionEnsemble: property NumTrained Number of trained trees This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Number of trained trees # name: # type: sq_string # elements: 1 # length: 33 RegressionEnsemble.PredictorNames # name: # type: sq_string # elements: 1 # length: 131 RegressionEnsemble: property PredictorNames Names of the predictors A cell array of character vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 39 RegressionEnsemble.ReasonForTermination # name: # type: sq_string # elements: 1 # length: 111 RegressionEnsemble: property ReasonForTermination Why the fit stopped adding trees This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Why the fit stopped adding trees # name: # type: sq_string # elements: 1 # length: 37 RegressionEnsemble.RegressionEnsemble # name: # type: sq_string # elements: 1 # length: 534 RegressionEnsemble: obj = RegressionEnsemble (X, Y) RegressionEnsemble: obj = RegressionEnsemble (Tbl, ResponseVarName) RegressionEnsemble: obj = RegressionEnsemble (Tbl, formula) RegressionEnsemble: obj = RegressionEnsemble (Tbl, Y) RegressionEnsemble: obj = RegressionEnsemble (..., name, value) Fit an ensemble of regression trees by LSBoost. fitrensemble is the documented way in, and its help lists the options both take. A bagged ensemble is fitted by RegressionBaggedEnsemble. See also: fitrensemble, RegressionBaggedEnsemble # name: # type: sq_string # elements: 1 # length: 47 Fit an ensemble of regression trees by LSBoost. # name: # type: sq_string # elements: 1 # length: 33 RegressionEnsemble.Regularization # name: # type: sq_string # elements: 1 # length: 218 RegressionEnsemble: property Regularization Result of regularizing the ensemble Empty until regularize fills it with a structure of lasso weights for the trees, and emptied again by resume. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Result of regularizing the ensemble # name: # type: sq_string # elements: 1 # length: 31 RegressionEnsemble.ResponseName # name: # type: sq_string # elements: 1 # length: 100 RegressionEnsemble: property ResponseName Name of the response variable This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 36 RegressionEnsemble.ResponseTransform # name: # type: sq_string # elements: 1 # length: 140 RegressionEnsemble: property ResponseTransform Transform applied to the predicted response See CompactRegressionEnsemble.ResponseTransform. # name: # type: sq_string # elements: 1 # length: 43 Transform applied to the predicted response # name: # type: sq_string # elements: 1 # length: 27 RegressionEnsemble.RowsUsed # name: # type: sq_string # elements: 1 # length: 142 RegressionEnsemble: property RowsUsed Rows of the data that were used A logical column over the rows as supplied. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Rows of the data that were used # name: # type: sq_string # elements: 1 # length: 26 RegressionEnsemble.Trained # name: # type: sq_string # elements: 1 # length: 133 RegressionEnsemble: property Trained Trained trees A column cell array of CompactRegressionTree objects. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Trained trees # name: # type: sq_string # elements: 1 # length: 33 RegressionEnsemble.TrainedWeights # name: # type: sq_string # elements: 1 # length: 136 RegressionEnsemble: property TrainedWeights Weights of the trained trees A column with one weight per tree. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Weights of the trained trees # name: # type: sq_string # elements: 1 # length: 36 RegressionEnsemble.UsePredForLearner # name: # type: sq_string # elements: 1 # length: 164 RegressionEnsemble: property UsePredForLearner Which predictors each learner uses Always empty, as MATLAB returns it for tree learners. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Which predictors each learner uses # name: # type: sq_string # elements: 1 # length: 20 RegressionEnsemble.W # name: # type: sq_string # elements: 1 # length: 183 RegressionEnsemble: property W Observation weights The weights given, normalized to sum to one. It has the class of the 'Weights' given, single or double. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 20 RegressionEnsemble.X # name: # type: sq_string # elements: 1 # length: 335 RegressionEnsemble: property X Predictor data The predictors the ensemble was fitted on, one row per observation. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 20 RegressionEnsemble.Y # name: # type: sq_string # elements: 1 # length: 158 RegressionEnsemble: property Y Response data The response the ensemble was fitted on, a row missing a value having been left out. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Response data # name: # type: sq_string # elements: 1 # length: 26 RegressionEnsemble.compact # name: # type: sq_string # elements: 1 # length: 285 RegressionEnsemble: CMdl = compact (obj) Drop the training data from a regression ensemble. CMdl = compact (obj) returns a CompactRegressionEnsemble holding the trees and what prediction needs. It predicts new data identically. See also: RegressionEnsemble, CompactRegressionEnsemble # name: # type: sq_string # elements: 1 # length: 50 Drop the training data from a regression ensemble. # name: # type: sq_string # elements: 1 # length: 27 RegressionEnsemble.crossval # name: # type: sq_string # elements: 1 # length: 533 RegressionEnsemble: CVMdl = crossval (obj) RegressionEnsemble: CVMdl = crossval (..., name, value) Cross-validate an ensemble. CVMdl = crossval (obj) refits the ensemble on the training part of each of ten folds, and returns a RegressionPartitionedEnsemble. One of 'KFold', an integer greater than 1, 'Holdout', a number between 0 and 1, 'Leaveout', 'on' for one fold per observation, or 'CVPartition', a cvpartition object, may choose the partition instead. See also: RegressionEnsemble, RegressionPartitionedEnsemble, cvpartition # name: # type: sq_string # elements: 1 # length: 27 Cross-validate an ensemble. # name: # type: sq_string # elements: 1 # length: 27 RegressionEnsemble.cvshrink # name: # type: sq_string # elements: 1 # length: 1225 RegressionEnsemble: [vals, nlearn] = cvshrink (obj) RegressionEnsemble: [vals, nlearn] = cvshrink (..., name, value) Cross-validate the shrinking of a regression ensemble. [vals, nlearn] = cvshrink (obj) grows the ensemble again on the training part of each fold, as crossval does, regularizes it with each penalty, shrinks it at each threshold, and predicts the fold's held-out observations. vals holds one row per penalty and one column per threshold: the weighted mean squared error pooled over every held-out observation. nlearn holds the matching mean number of trees kept per fold. Name Value 'Lambda' A vector of non-negative penalties. The default is Regularization.Lambda; an ensemble that has not been regularized must be given one. 'Threshold' A vector of non-negative thresholds, as shrink takes them. The default is 0. 'KFold', 'Holdout', 'Leaveout', 'CVPartition' The partition, as crossval takes it, only one of them. The default is ten folds. 'MaxIter', 'RelTol' As regularize takes them. MATLAB warns and returns empty outputs when an ensemble that has not been regularized is given no penalties; here that is an error. See also: RegressionEnsemble, RegressionEnsemble.shrink, RegressionEnsemble.crossval # name: # type: sq_string # elements: 1 # length: 54 Cross-validate the shrinking of a regression ensemble. # name: # type: sq_string # elements: 1 # length: 23 RegressionEnsemble.loss # name: # type: sq_string # elements: 1 # length: 700 RegressionEnsemble: L = loss (obj, X, Y) RegressionEnsemble: L = loss (obj, Tbl, ResponseVarName) RegressionEnsemble: L = loss (obj, Tbl) RegressionEnsemble: L = loss (..., name, value) Regression loss of an ensemble. Behaves as CompactRegressionEnsemble.loss and takes the same Name-Value arguments. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: RegressionEnsemble, CompactRegressionEnsemble.loss # name: # type: sq_string # elements: 1 # length: 31 Regression loss of an ensemble. # name: # type: sq_string # elements: 1 # length: 26 RegressionEnsemble.predict # name: # type: sq_string # elements: 1 # length: 587 RegressionEnsemble: yfit = predict (obj, X) RegressionEnsemble: yfit = predict (..., name, value) Predict the response with a regression ensemble. Behaves as CompactRegressionEnsemble.predict and takes the same Name-Value arguments. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: RegressionEnsemble, CompactRegressionEnsemble.predict # name: # type: sq_string # elements: 1 # length: 48 Predict the response with a regression ensemble. # name: # type: sq_string # elements: 1 # length: 38 RegressionEnsemble.predictorImportance # name: # type: sq_string # elements: 1 # length: 288 RegressionEnsemble: imp = predictorImportance (obj) RegressionEnsemble: [imp, ma] = predictorImportance (obj) Estimate the importance of each predictor. Behaves as CompactRegressionEnsemble.predictorImportance. See also: RegressionEnsemble, CompactRegressionEnsemble.predictorImportance # name: # type: sq_string # elements: 1 # length: 42 Estimate the importance of each predictor. # name: # type: sq_string # elements: 1 # length: 29 RegressionEnsemble.regularize # name: # type: sq_string # elements: 1 # length: 1696 RegressionEnsemble: B = regularize (obj) RegressionEnsemble: B = regularize (..., name, value) Find lasso weights for the trees of a regression ensemble. B = regularize (obj) fits the training response by the trees' predictions with a lasso that has no intercept and whose weights may not be negative, over a path of penalties, and returns the ensemble with the result in Regularization. For a penalty lambda the tree weights a minimize sum (W .* (Y - P * a) .^ 2) / 2 + lambda * sum (a), where P holds one column of training predictions per tree and W is the observation weights, which sum to one. TrainedWeights is left as it was. Name Value 'Lambda' A vector of non-negative penalties. The default is 0 followed by nine values spaced evenly on a log scale from lmax / 1000 to lmax, the smallest penalty that sets every weight to zero, lmax = max (abs (P' * (W .* Y))). 'MaxIter' The most passes of coordinate descent over the trees for each penalty. The default is 1e5. 'RelTol' The descent stops after a pass in which no weight changed by more than this times the largest weight, or this when that weight is below one. The default is 1e-10. Regularization is a structure with the fields Method, 'Lasso'; TrainedWeights, one column per penalty; Lambda; ResubstitutionMSE, the weighted mean squared error of each column; and CombineWeights, 'WeightedSum'. MATLAB's solver can stop well short of the minimum at small penalties, so its weights there differ from these, which are the minimum. Its CombineWeights is a function handle, and it also takes 'Npass' and 'Verbose', which are not taken here. See also: RegressionEnsemble, RegressionEnsemble.shrink, RegressionEnsemble.cvshrink, lasso # name: # type: sq_string # elements: 1 # length: 58 Find lasso weights for the trees of a regression ensemble. # name: # type: sq_string # elements: 1 # length: 28 RegressionEnsemble.resubLoss # name: # type: sq_string # elements: 1 # length: 245 RegressionEnsemble: L = resubLoss (obj) RegressionEnsemble: L = resubLoss (..., name, value) Regression loss on the training data. loss on X and Y, weighted by W unless 'Weights' are given. See also: RegressionEnsemble, RegressionEnsemble.loss # name: # type: sq_string # elements: 1 # length: 37 Regression loss on the training data. # name: # type: sq_string # elements: 1 # length: 31 RegressionEnsemble.resubPredict # name: # type: sq_string # elements: 1 # length: 258 RegressionEnsemble: yfit = resubPredict (obj) RegressionEnsemble: yfit = resubPredict (..., name, value) Predict the response of the training data. predict on X, taking the same Name-Value arguments. See also: RegressionEnsemble, RegressionEnsemble.predict # name: # type: sq_string # elements: 1 # length: 42 Predict the response of the training data. # name: # type: sq_string # elements: 1 # length: 25 RegressionEnsemble.resume # name: # type: sq_string # elements: 1 # length: 451 RegressionEnsemble: B = resume (obj, NumLearningCycles) RegressionEnsemble: B = resume (..., 'NPrint', n) Grow more trees. B is the ensemble with NumLearningCycles further trees grown as though the fit had asked for them from the start. 'NPrint' is taken as by fitrensemble. A Regularization is emptied, its weights no longer covering every tree; MATLAB keeps it and applies its weights to the first trees. See also: RegressionEnsemble, fitrensemble # name: # type: sq_string # elements: 1 # length: 16 Grow more trees. # name: # type: sq_string # elements: 1 # length: 25 RegressionEnsemble.shrink # name: # type: sq_string # elements: 1 # length: 968 RegressionEnsemble: C = shrink (obj) RegressionEnsemble: C = shrink (..., name, value) Keep the trees a lasso weight retains. C = shrink (obj) returns a CompactRegressionEnsemble of the trees whose weight in a column of Regularization.TrainedWeights is above a threshold, ordered from the largest weight down, each carrying that weight and their predictions summed. An ensemble that has not been regularized is thresholded on its TrainedWeights and keeps its way of combining its trees. Name Value 'WeightColumn' The column of weights to use, a positive integer. The default is 1. 'Threshold' A non-negative number; a tree whose weight is not above it is dropped. The default is 0. 'Lambda' Penalties to regularize with first, as regularize does, which also takes 'MaxIter' and 'RelTol' here. MATLAB accepts a 'WeightColumn' that is not a whole number, which is refused here. See also: RegressionEnsemble, RegressionEnsemble.regularize, RegressionEnsemble.cvshrink # name: # type: sq_string # elements: 1 # length: 38 Keep the trees a lasso weight retains. # name: # type: sq_string # elements: 1 # length: 13 RegressionGAM # name: # type: sq_string # elements: 1 # length: 872 statistics: RegressionGAM Generalized additive model for regression. A RegressionGAM object holds a generalized additive model fitted to a continuous response, and predicts the response for new data with the predict method. The model is a sum of one shape function per predictor, and optionally one per pair of predictors, each learned from the data rather than assumed. Two engines fit it. 'boostedtrees', the default, boosts shallow decision trees one predictor at a time; 'splines' boosts smoothing splines. The properties describing the engine that was not used are empty. The object keeps its training data, which resubPredict, resubLoss and crossval work on; compact drops it and returns a CompactRegressionGAM, which still predicts. Create a RegressionGAM object with fitrgam or the class constructor. See also: fitrgam, CompactRegressionGAM, ClassificationGAM # name: # type: sq_string # elements: 1 # length: 42 Generalized additive model for regression. # name: # type: sq_string # elements: 1 # length: 23 RegressionGAM.BaseModel # name: # type: sq_string # elements: 1 # length: 300 RegressionGAM: property BaseModel Model without interaction terms A structure holding the intercept, the piecewise polynomial of each predictor, the number of backfitting cycles, the residuals and the residual sum of squares of the model fitted without interaction terms. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Model without interaction terms # name: # type: sq_string # elements: 1 # length: 22 RegressionGAM.BinEdges # name: # type: sq_string # elements: 1 # length: 458 RegressionGAM: property BinEdges Bin edges of the predictors A cell array with one entry per predictor, holding that predictor's bin edges where the model discretized it before fitting. It is empty here and stays empty: this generalized additive model is built from splines, which take the predictors as they are, where MATLAB's is built from boosted trees and bins them. That difference is described in the class documentation. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Bin edges of the predictors # name: # type: sq_string # elements: 1 # length: 35 RegressionGAM.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 214 RegressionGAM: property CategoricalPredictors Indices of the categorical predictors A numeric vector holding the column of each predictor treated as categorical, and empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 17 RegressionGAM.DoF # name: # type: sq_string # elements: 1 # length: 190 RegressionGAM: property DoF Degrees of freedom of the spline fitting A numeric vector with one entry per predictor, the sum of its number of knots and its order. This property is read-only. # name: # type: sq_string # elements: 1 # length: 40 Degrees of freedom of the spline fitting # name: # type: sq_string # elements: 1 # length: 36 RegressionGAM.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 289 RegressionGAM: property ExpandedPredictorNames Names of the expanded predictor variables A cell array of character vectors naming the predictors as the model sees them. It matches PredictorNames unless a categorical predictor was expanded into dummy variables. This property is read-only. # name: # type: sq_string # elements: 1 # length: 41 Names of the expanded predictor variables # name: # type: sq_string # elements: 1 # length: 23 RegressionGAM.FitMethod # name: # type: sq_string # elements: 1 # length: 498 RegressionGAM: property FitMethod Which engine fitted the model A character vector, either 'boostedtrees' or 'splines'. The default is 'boostedtrees', the scheme MATLAB's generalized additive model uses. 'splines' selects the penalised-spline engine, an Octave extension with no MATLAB counterpart and the scheme this class fitted before version 1.9.0. The two engines take different arguments and an argument meant for one is refused by the other rather than ignored. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Which engine fitted the model # name: # type: sq_string # elements: 1 # length: 21 RegressionGAM.Formula # name: # type: sq_string # elements: 1 # length: 220 RegressionGAM: property Formula Formula of the model A character vector naming the response and the terms of the model, as in 'Y ~ x1 + x2 + x1:x2', or empty when the model was not given one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Formula of the model # name: # type: sq_string # elements: 1 # length: 47 RegressionGAM.HyperparameterOptimizationResults # name: # type: sq_string # elements: 1 # length: 355 RegressionGAM: property HyperparameterOptimizationResults Results of the hyperparameter optimization Always empty. It is declared for MATLAB compatibility, where it holds what an automatic search over the hyperparameters found. This class fits the parameters it is given and runs no such search, so there is nothing to report. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Results of the hyperparameter optimization # name: # type: sq_string # elements: 1 # length: 23 RegressionGAM.IntMatrix # name: # type: sq_string # elements: 1 # length: 552 RegressionGAM: property IntMatrix Every term the model fits A logical matrix with one row per term and one column per predictor, true wherever the term multiplies that predictor. A row naming one predictor is a main effect, two an interaction, and three or more a higher-order term. This property is read-only. It is the complete record, where Interactions reports only the two-way terms, in the form MATLAB reports them. It is also the form the 'Interactions' option takes back, so passing it to the constructor rebuilds a model over the same terms. # name: # type: sq_string # elements: 1 # length: 25 Every term the model fits # name: # type: sq_string # elements: 1 # length: 26 RegressionGAM.Interactions # name: # type: sq_string # elements: 1 # length: 592 RegressionGAM: property Interactions Two-way interaction terms of the fitted model A Kx2 matrix of predictor index pairs, one row per two-way term the model carries, and zeros (0, 2) when it carries none. It reports what was fitted rather than what was asked for, so a count of terms, 'all', a logical matrix and a formula all leave the same kind of value behind. This property is read-only. A main effect names one predictor and a higher-order term names three or more, and neither has a two-column form, so neither appears here. IntMatrix remains the complete record of every term fitted. # name: # type: sq_string # elements: 1 # length: 45 Two-way interaction terms of the fitted model # name: # type: sq_string # elements: 1 # length: 23 RegressionGAM.Intercept # name: # type: sq_string # elements: 1 # length: 183 RegressionGAM: property Intercept Intercept of the fitted model A numeric scalar, the mean of the response, which every additive term is measured against. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Intercept of the fitted model # name: # type: sq_string # elements: 1 # length: 36 RegressionGAM.IsStandardDeviationFit # name: # type: sq_string # elements: 1 # length: 277 RegressionGAM: property IsStandardDeviationFit Flag for a fitted standard deviation model A boolean flag, always false, as this class estimates the standard deviation of a prediction from the residuals of the fit rather than fitting a model for it. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Flag for a fitted standard deviation model # name: # type: sq_string # elements: 1 # length: 19 RegressionGAM.Knots # name: # type: sq_string # elements: 1 # length: 199 RegressionGAM: property Knots Knots of the spline fitting A numeric vector with one entry per predictor, the number of breaks the spline of that predictor is fitted over. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Knots of the spline fitting # name: # type: sq_string # elements: 1 # length: 29 RegressionGAM.ModelParameters # name: # type: sq_string # elements: 1 # length: 351 RegressionGAM: property ModelParameters Parameters the model was fitted with A structure holding the fitting parameters. Under the boosted-tree engine it carries MATLAB's own fields, with Type reading 'regression'; under the spline engine it describes that scheme instead, since none of the tree vocabulary applies to it. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Parameters the model was fitted with # name: # type: sq_string # elements: 1 # length: 23 RegressionGAM.ModelwInt # name: # type: sq_string # elements: 1 # length: 220 RegressionGAM: property ModelwInt Model with interaction terms A structure of the same fields as BaseModel, for the model fitted with the interaction terms, and empty when none was asked for. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Model with interaction terms # name: # type: sq_string # elements: 1 # length: 29 RegressionGAM.NumObservations # name: # type: sq_string # elements: 1 # length: 223 RegressionGAM: property NumObservations Number of observations A positive integer, the number of observations of the training data the model was fitted on, rows with a missing response excluded. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 27 RegressionGAM.NumPredictors # name: # type: sq_string # elements: 1 # length: 154 RegressionGAM: property NumPredictors Number of predictors A positive integer, the number of predictors of the training data. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of predictors # name: # type: sq_string # elements: 1 # length: 19 RegressionGAM.Order # name: # type: sq_string # elements: 1 # length: 187 RegressionGAM: property Order Order of the spline fitting A numeric vector with one entry per predictor, the polynomial order of the spline of that predictor. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Order of the spline fitting # name: # type: sq_string # elements: 1 # length: 35 RegressionGAM.PairDetectionBinEdges # name: # type: sq_string # elements: 1 # length: 476 RegressionGAM: property PairDetectionBinEdges Bin edges used to detect interactions A cell array with one row vector per predictor, holding the coarse cut points the residuals of the predictor phase were laid on while pairs were being tested. The grid is eight equal-frequency bins whatever the sample size, as MATLAB's is. It is empty when the model carries no interaction terms, and empty throughout under the spline engine, which does not bin. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Bin edges used to detect interactions # name: # type: sq_string # elements: 1 # length: 28 RegressionGAM.PredictorNames # name: # type: sq_string # elements: 1 # length: 204 RegressionGAM: property PredictorNames Names of the predictor variables A cell array of character vectors naming the predictors, in the order they appear in the training data. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Names of the predictor variables # name: # type: sq_string # elements: 1 # length: 34 RegressionGAM.ReasonForTermination # name: # type: sq_string # elements: 1 # length: 336 RegressionGAM: property ReasonForTermination Why each fitting phase stopped A structure with the fields PredictorTrees and InteractionTrees, each saying why that phase ended. A phase that never ran reports an empty character vector. It is empty under the spline engine, which has no tree budget to exhaust. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 Why each fitting phase stopped # name: # type: sq_string # elements: 1 # length: 27 RegressionGAM.RegressionGAM # name: # type: sq_string # elements: 1 # length: 6217 RegressionGAM: obj = RegressionGAM (X, Y) RegressionGAM: obj = RegressionGAM (Tbl, ResponseVarName) RegressionGAM: obj = RegressionGAM (Tbl, formula) RegressionGAM: obj = RegressionGAM (Tbl, Y) RegressionGAM: obj = RegressionGAM (..., name, value) Fit a generalized additive model for regression. obj = RegressionGAM (X, Y) returns an object of class RegressionGAM, with matrix X containing the predictor data and vector Y containing the continuous response data. X must be a NxP numeric matrix of input data where rows correspond to observations and columns correspond to features or variables. X will be used to train the GAM model. Y must be Nx1 numeric vector containing the response data corresponding to the predictor data in X. Y must have same number of rows as X. obj = RegressionGAM (..., name, value) returns an object of class RegressionGAM with additional properties specified by Name-Value pair arguments listed below. Name Value 'Predictors' Predictor Variable names, specified as a row vector cell of strings with the same length as the columns in X. If omitted, the program will generate default variable names (x1, x2, ..., xn) for each column in X. 'responsename' Response Variable Name, specified as a string. If omitted, the default value is 'Y'. 'formula' (spline option) a model specification given as a string in the form 'Y ~ terms' where Y represents the response variable and terms the predictor variables. The formula can be used to specify a subset of variables for training model. For example: 'Y ~ x1 + x2 + x3 + x4 + x1:x2 + x2:x3' specifies four linear terms for the first four columns of for predictor data, and x1:x2 and x2:x3 specify the two interaction terms for 1st-2nd and 3rd-4th columns respectively. Only these terms will be used for training the model, but X must have at least as many columns as referenced in the formula. If Predictor Variable names have been defined, then the terms in the formula must reference to those. When 'formula' is specified, all terms used for training the model are referenced in the IntMatrix field of the obj class object as a matrix containing the column indexes for each term including both the predictors and the interactions used. 'interactions' a logical matrix, a positive integer scalar, or the string 'all' for defining the interactions between predictor variables. When given a logical matrix, it must have the same number of columns as X and each row corresponds to a different interaction term combining the predictors indexed as true. Each interaction term is appended as a column vector after the available predictor column in X. When 'all' is defined, then all possible combinations of interactions are appended in X before training. At the moment, parsing a positive integer has the same effect as the 'all' option. When 'interactions' is specified, only the interaction terms appended to X are referenced in the IntMatrix field of the obj class object. 'knots' (spline option) a scalar or a row vector with the same columns as X. It defines the knots for fitting a polynomial when training the GAM. As a scalar, it is expanded to a row vector. The default value is 5, hence expanded to ones (1, columns (X)) * 5. You can parse a row vector with different number of knots for each predictor variable to be fitted with, although not recommended. 'order' (spline option) a scalar or a row vector with the same columns as X. It defines the order of the polynomial when training the GAM. As a scalar, it is expanded to a row vector. The default values is 3, hence expanded to ones (1, columns (X)) * 3. You can parse a row vector with different number of polynomial order for each predictor variable to be fitted with, although not recommended. 'dof' (spline option) a scalar or a row vector with the same columns as X. It defines the degrees of freedom for fitting a polynomial when training the GAM. As a scalar, it is expanded to a row vector. The default value is 8, hence expanded to ones (1, columns (X)) * 8. You can parse a row vector with different degrees of freedom for each predictor variable to be fitted with, although not recommended. 'tol' (spline option) a positive scalar to set the tolerance for convergence during training. By default, it is set to 1e-3. A row marked (spline option) belongs to the spline engine and requires 'FitMethod', 'splines'; passing one under the default boosted-tree engine is an error rather than being ignored. The boosted-tree engine's own options are documented under fitrgam. You can parse either a 'formula' or an 'interactions' optional parameter. Parsing both parameters will result an error. Accordingly, you can only pass up to two parameters among 'knots', 'order', and 'dof' to define the required polynomial for training the GAM model. Two weak learners are available, selected by FitMethod. 'boostedtrees', the default, boosts one shallow decision tree per predictor in each round, which is the scheme MATLAB's generalized additive model uses. A second phase then boosts trees over pairs of predictors, where interactions are asked for. 'splines' boosts a smoothing spline per predictor until the residual sum of squares changes by less than 'Tol'. It has no MATLAB counterpart and is an Octave extension, kept because a smooth additive fit is a genuinely different and often better answer than a staircase of stumps. A standard deviation and a prediction interval are available from it alone. The two take different arguments, and an argument meant for one is refused by the other rather than ignored. The choice is visible in the properties. Knots, Order, DoF, Formula, Tol, BaseModel, ModelwInt and IntMatrix describe a spline fit and are empty under the boosted-tree engine, while ModelParameters, ReasonForTermination, BinEdges, PairDetectionBinEdges and TreeModel describe a tree fit and are empty under the spline engine. Fitted values are not expected to equal MATLAB's even under 'boostedtrees'. The stopping rule and the step-reduction limit are not recoverable from anything MATLAB reports, so this engine documents its own; what the two share is the estimator and the reported surface, not the arithmetic. See also: fitrgam, regress, regress_gp # name: # type: sq_string # elements: 1 # length: 48 Fit a generalized additive model for regression. # name: # type: sq_string # elements: 1 # length: 26 RegressionGAM.ResponseName # name: # type: sq_string # elements: 1 # length: 139 RegressionGAM: property ResponseName Response variable name A character vector naming the response variable Y. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Response variable name # name: # type: sq_string # elements: 1 # length: 31 RegressionGAM.ResponseTransform # name: # type: sq_string # elements: 1 # length: 308 RegressionGAM: property ResponseTransform Transformation applied to the predicted response A function handle applied to the response the model predicts. Add or change it using dot notation, as in obj.ResponseTransform = 'log' or obj.ResponseTransform = @function_handle. It defaults to 'none', the identity. # name: # type: sq_string # elements: 1 # length: 48 Transformation applied to the predicted response # name: # type: sq_string # elements: 1 # length: 22 RegressionGAM.RowsUsed # name: # type: sq_string # elements: 1 # length: 375 RegressionGAM: property RowsUsed Rows used for fitting A logical column vector with the same length as the observations in the original predictor data X, true for each row that was used for fitting the RegressionGAM model. It is empty, [], when every observation was used, so a non-empty value means that rows holding missing values were dropped. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Rows used for fitting # name: # type: sq_string # elements: 1 # length: 17 RegressionGAM.Tol # name: # type: sq_string # elements: 1 # length: 200 RegressionGAM: property Tol Tolerance for convergence A positive scalar, the largest change in the residual sum of squares of a backfitting cycle that counts as converged. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Tolerance for convergence # name: # type: sq_string # elements: 1 # length: 23 RegressionGAM.TreeModel # name: # type: sq_string # elements: 1 # length: 416 RegressionGAM: property TreeModel The fitted shape functions and interaction surfaces A structure with fields ShapeValues, PairValues and Pairs, holding what the boosted-tree engine fitted. MATLAB exposes no equivalent, reporting its bin edges but never the values on them, so this is an Octave extension. It is empty under the spline engine, whose fit lives in BaseModel and ModelwInt. This property is read-only. # name: # type: sq_string # elements: 1 # length: 51 The fitted shape functions and interaction surfaces # name: # type: sq_string # elements: 1 # length: 15 RegressionGAM.W # name: # type: sq_string # elements: 1 # length: 279 RegressionGAM: property W Observation weights A numeric column vector with one entry per observation used for training, the 'Weights' normalised to sum to one, and equal when none were given. It has the class of the 'Weights' given, single or double. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 15 RegressionGAM.X # name: # type: sq_string # elements: 1 # length: 358 RegressionGAM: property X Predictor data A numeric matrix with one row per observation and one column per predictor of the training data. This property is read-only. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 15 RegressionGAM.Y # name: # type: sq_string # elements: 1 # length: 145 RegressionGAM: property Y Response data A numeric column vector with one entry per observation of the training data. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Response data # name: # type: sq_string # elements: 1 # length: 29 RegressionGAM.addInteractions # name: # type: sq_string # elements: 1 # length: 1053 RegressionGAM: obj = addInteractions (obj, interactions) Add interaction terms to a fitted model. obj = addInteractions (obj, interactions) fits the interaction terms named by interactions on top of the terms the model already carries and returns the updated model. The univariate fit is left alone, so predict with 'IncludeInteractions' set false answers exactly as it answered before. interactions takes the forms the constructor's 'Interactions' option takes: a nonnegative integer count of terms, a logical matrix with a column per predictor, or 'all'. A model already carrying interaction terms is not extended, which is what MATLAB refuses too. A model fitted from a 'Formula' names every term it has, interactions among them, and is refused for the same reason. Which terms a count selects is this implementation's own: they are taken in the order nchoosek lists the pairs, where MATLAB ranks them by how much each contributes. The constructor's option chooses the same way, so the two agree with each other. See also: fitrgam, RegressionGAM # name: # type: sq_string # elements: 1 # length: 40 Add interaction terms to a fitted model. # name: # type: sq_string # elements: 1 # length: 21 RegressionGAM.compact # name: # type: sq_string # elements: 1 # length: 245 RegressionGAM: CMdl = compact (obj) Create a CompactRegressionGAM object. CMdl = compact (obj) returns a compact version of the model, which predicts as it does but keeps no training data. See also: RegressionGAM, CompactRegressionGAM, fitrgam # name: # type: sq_string # elements: 1 # length: 37 Create a CompactRegressionGAM object. # name: # type: sq_string # elements: 1 # length: 22 RegressionGAM.crossval # name: # type: sq_string # elements: 1 # length: 992 RegressionGAM: CVMdl = crossval (obj) RegressionGAM: CVMdl = crossval (..., name, value) Cross validate a Generalized Additive Model regression object. CVMdl = crossval (obj) returns a cross-validated model object, CVMdl, from a trained model, obj, using 10-fold cross-validation by default. CVMdl = crossval (obj, name, value) specifies additional name-value pair arguments to customize the cross-validation process. Name Value 'KFold' Specify the number of folds to use in k-fold cross-validation. "KFold", k, where k is an integer greater than 1. 'Holdout' Specify the fraction of the data to hold out for testing. "Holdout", p, where p is a scalar in the range (0,1). 'Leaveout' Specify whether to perform leave-one-out cross-validation. "Leaveout", Value, where Value is 'on' or 'off'. 'CVPartition' Specify a cvpartition object used for cross-validation. "CVPartition", cv, where isa (cv, "cvpartition") = 1. See also: fitrgam, RegressionGAM, cvpartition, RegressionPartitionedModel # name: # type: sq_string # elements: 1 # length: 62 Cross validate a Generalized Additive Model regression object. # name: # type: sq_string # elements: 1 # length: 18 RegressionGAM.loss # name: # type: sq_string # elements: 1 # length: 1062 RegressionGAM: L = loss (obj, X, Y) RegressionGAM: L = loss (obj, Tbl, ResponseVarName) RegressionGAM: L = loss (obj, Tbl) RegressionGAM: L = loss (..., name, value) Regression loss of a generalized additive model. L = loss (obj, X, Y) returns the weighted mean squared error of the model on the rows of X against the true response Y. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. L = loss (..., name, value) accepts the following name-value pairs: "LossFun" selects the loss, either "mse", the default, or a function handle taking the true response, the predicted response and the weights, and returning a numeric scalar. "Weights" holds one weight per row of X, normalised to sum to one before it is applied. See also: RegressionGAM, fitrgam, RegressionGAM.predict # name: # type: sq_string # elements: 1 # length: 48 Regression loss of a generalized additive model. # name: # type: sq_string # elements: 1 # length: 21 RegressionGAM.predict # name: # type: sq_string # elements: 1 # length: 2233 RegressionGAM: yFit = predict (obj, Xfit) RegressionGAM: yFit = predict (..., Name, Value) RegressionGAM: [yFit, ySD, yInt] = predict (...) Predict new data points using generalized additive model regression object. yFit = predict (obj, Xfit returns a vector of predicted responses, yFit, for the predictor data in matrix Xfit based on the Generalized Additive Model in obj. Xfit must have the same number of features/variables as the training data in obj. Every row is predicted. Under boosted trees a missing value adds nothing from a main effect, and an interaction term takes the value its trees give a row missing that predictor, so a row missing every predictor predicts the intercept; under splines a row holding a missing value is predicted as NaN. obj must be a RegressionGAM class object. [yFit, ySD, yInt] = predict (obj, Xfit also returns the standard deviations, ySD, and prediction intervals, yInt, of the response variable yFit, evaluated at each observation in the predictor data Xfit. yFit = predict (..., Name, Value) returns the aforementioned results with additional properties specified by Name-Value pair arguments listed below. Name Value 'alpha' significance level of the prediction intervals yInt, specified as scalar in range [0,1]. The default value is 0.05, which corresponds to 95% prediction intervals. 'includeinteractions' a boolean flag to include interactions to predict new values based on Xfit. By default, 'includeinteractions' is true when the GAM model in obj contains a obj.Formula or obj.Interactions fields. Otherwise, is set to false. If set to true when no interactions are present in the trained model, it will result to an error. If set to false when using a model that includes interactions, the predictions will be made on the basic model without any interaction terms. This way you can make predictions from the same GAM model without having to retrain it. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: fitrgam, RegressionGAM # name: # type: sq_string # elements: 1 # length: 75 Predict new data points using generalized additive model regression object. # name: # type: sq_string # elements: 1 # length: 23 RegressionGAM.resubLoss # name: # type: sq_string # elements: 1 # length: 356 RegressionGAM: L = resubLoss (obj) RegressionGAM: L = resubLoss (..., name, value) Regression loss of a generalized additive model on its training data. L = resubLoss (obj) returns the weighted mean squared error of the model on the data it was fitted on. It accepts the same Name-Value pairs as loss. See also: RegressionGAM, fitrgam, RegressionGAM.loss # name: # type: sq_string # elements: 1 # length: 69 Regression loss of a generalized additive model on its training data. # name: # type: sq_string # elements: 1 # length: 26 RegressionGAM.resubPredict # name: # type: sq_string # elements: 1 # length: 251 RegressionGAM: yFit = resubPredict (obj) Predict the training response with the model it was fitted on. yFit = resubPredict (obj) is predict applied to the observations the model was fitted on. See also: RegressionGAM, fitrgam, RegressionGAM.predict # name: # type: sq_string # elements: 1 # length: 62 Predict the training response with the model it was fitted on. # name: # type: sq_string # elements: 1 # length: 20 RegressionGAM.resume # name: # type: sq_string # elements: 1 # length: 937 RegressionGAM: Mdl = resume (obj, numTrees) Resume training a generalized additive model. Mdl = resume (obj, numTrees) adds numTrees more trees to obj and returns the result. The original model is not modified. Training continues in the phase that ran last, which is what MATLAB does: a model carrying interaction terms gains interaction trees and its predictor shape functions are left alone, while a model without them gains predictor trees. A round starts at its initial learning rate whatever its number, so the model this returns is the model a single fit of the combined budget would have produced. numTrees must be a positive integer scalar. Resuming raises where there is nothing left to gain, rather than returning the model unchanged, and it is not available under 'FitMethod', 'splines': a backfit that has converged to its tolerance has no budget to extend. See also: RegressionGAM, fitrgam, RegressionGAM.addInteractions # name: # type: sq_string # elements: 1 # length: 45 Resume training a generalized additive model. # name: # type: sq_string # elements: 1 # length: 23 RegressionGAM.savemodel # name: # type: sq_string # elements: 1 # length: 203 RegressionGAM: savemodel (obj, filename) Save a RegressionGAM object. savemodel (obj, filename) saves a RegressionGAM object into a file defined by filename. See also: loadmodel, fitrgam, RegressionGAM # name: # type: sq_string # elements: 1 # length: 28 Save a RegressionGAM object. # name: # type: sq_string # elements: 1 # length: 12 RegressionGP # name: # type: sq_string # elements: 1 # length: 838 statistics: RegressionGP Gaussian process regression model. A RegressionGP object holds a Gaussian process fitted to a continuous response, and predicts the response for new data with the predict method, which also returns the standard deviation of each prediction and a prediction interval. The response is modelled as an explicit basis times Beta, plus a draw from a process whose covariance is given by the kernel function, plus independent noise of standard deviation Sigma. The kernel parameters and Sigma are estimated by maximizing the log marginal likelihood. The object keeps its training data, which resubPredict, resubLoss and crossval work on; compact drops it and returns a CompactRegressionGP, which still predicts. Create a RegressionGP object with fitrgp or the class constructor. See also: fitrgp, CompactRegressionGP # name: # type: sq_string # elements: 1 # length: 34 Gaussian process regression model. # name: # type: sq_string # elements: 1 # length: 29 RegressionGP.ActiveSetHistory # name: # type: sq_string # elements: 1 # length: 392 RegressionGP: property ActiveSetHistory History of the active set selection Always empty. It is declared for MATLAB compatibility, where it records the active set chosen at each iteration by a fit method that builds one. This class implements the exact method alone, which uses the whole of the training data and selects nothing, so there is no history to record. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 History of the active set selection # name: # type: sq_string # elements: 1 # length: 28 RegressionGP.ActiveSetMethod # name: # type: sq_string # elements: 1 # length: 114 RegressionGP: property ActiveSetMethod Method used to select the active set 'Random'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Method used to select the active set # name: # type: sq_string # elements: 1 # length: 26 RegressionGP.ActiveSetSize # name: # type: sq_string # elements: 1 # length: 115 RegressionGP: property ActiveSetSize Size of the active set A positive integer scalar. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Size of the active set # name: # type: sq_string # elements: 1 # length: 29 RegressionGP.ActiveSetVectors # name: # type: sq_string # elements: 1 # length: 280 RegressionGP: property ActiveSetVectors Subset of the training data used for predictions An MxP numeric matrix, standardized where the model standardized its predictors. It is the whole of the training data, since only the exact method is implemented. This property is read-only. # name: # type: sq_string # elements: 1 # length: 48 Subset of the training data used for predictions # name: # type: sq_string # elements: 1 # length: 18 RegressionGP.Alpha # name: # type: sq_string # elements: 1 # length: 230 RegressionGP: property Alpha Weights the predictions are made from An Nx1 numeric vector. A prediction is the basis term plus the covariance between the new point and the active set, weighted by these. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Weights the predictions are made from # name: # type: sq_string # elements: 1 # length: 27 RegressionGP.BCDInformation # name: # type: sq_string # elements: 1 # length: 271 RegressionGP: property BCDInformation Block coordinate descent information Always empty. It is declared for MATLAB compatibility, where it records a block coordinate descent. This class does not use that method, so there is nothing to report. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Block coordinate descent information # name: # type: sq_string # elements: 1 # length: 26 RegressionGP.BasisFunction # name: # type: sq_string # elements: 1 # length: 182 RegressionGP: property BasisFunction Explicit basis of the model 'None', 'Constant', 'Linear', 'PureQuadratic', or the function handle that was supplied. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Explicit basis of the model # name: # type: sq_string # elements: 1 # length: 17 RegressionGP.Beta # name: # type: sq_string # elements: 1 # length: 183 RegressionGP: property Beta Estimated coefficients of the explicit basis A numeric vector with one element per basis term, empty when the basis is 'None'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Estimated coefficients of the explicit basis # name: # type: sq_string # elements: 1 # length: 21 RegressionGP.BinEdges # name: # type: sq_string # elements: 1 # length: 301 RegressionGP: property BinEdges Bin edges of the predictors A cell array with one entry per predictor, holding that predictor's bin edges where the model discretized it before fitting. It is empty here and stays empty: a Gaussian process takes its predictors as they are. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Bin edges of the predictors # name: # type: sq_string # elements: 1 # length: 34 RegressionGP.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 226 RegressionGP: property CategoricalPredictors Indices of the categorical predictors A vector of positive integers indexing the columns of X that hold categorical predictors, or empty when none does. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 35 RegressionGP.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 253 RegressionGP: property ExpandedPredictorNames Expanded predictor variable names A cell array of character vectors. It differs from PredictorNames only where a categorical predictor has been expanded into indicator variables. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Expanded predictor variable names # name: # type: sq_string # elements: 1 # length: 22 RegressionGP.FitMethod # name: # type: sq_string # elements: 1 # length: 267 RegressionGP: property FitMethod Method used to estimate the parameters 'Exact' when the covariance parameters and the noise were estimated by maximizing the log marginal likelihood, and 'None' when they were kept at their initial values. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Method used to estimate the parameters # name: # type: sq_string # elements: 1 # length: 46 RegressionGP.HyperparameterOptimizationResults # name: # type: sq_string # elements: 1 # length: 354 RegressionGP: property HyperparameterOptimizationResults Results of the hyperparameter optimization Always empty. It is declared for MATLAB compatibility, where it holds what an automatic search over the hyperparameters found. This class fits the parameters it is given and runs no such search, so there is nothing to report. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Results of the hyperparameter optimization # name: # type: sq_string # elements: 1 # length: 30 RegressionGP.IsActiveSetVector # name: # type: sq_string # elements: 1 # length: 170 RegressionGP: property IsActiveSetVector Which observations are in the active set A logical vector with one element per training observation. This property is read-only. # name: # type: sq_string # elements: 1 # length: 40 Which observations are in the active set # name: # type: sq_string # elements: 1 # length: 27 RegressionGP.KernelFunction # name: # type: sq_string # elements: 1 # length: 191 RegressionGP: property KernelFunction Form of the covariance function A character vector naming the covariance function, or the function handle that was supplied. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Form of the covariance function # name: # type: sq_string # elements: 1 # length: 30 RegressionGP.KernelInformation # name: # type: sq_string # elements: 1 # length: 242 RegressionGP: property KernelInformation Covariance function and its parameters A structure with fields Name, KernelParameters and KernelParameterNames, the last naming each parameter in the order they are stored. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Covariance function and its parameters # name: # type: sq_string # elements: 1 # length: 26 RegressionGP.LogLikelihood # name: # type: sq_string # elements: 1 # length: 170 RegressionGP: property LogLikelihood Maximized log marginal likelihood A scalar, or empty when FitMethod is 'None' and nothing was maximized. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Maximized log marginal likelihood # name: # type: sq_string # elements: 1 # length: 28 RegressionGP.ModelParameters # name: # type: sq_string # elements: 1 # length: 1110 RegressionGP: property ModelParameters Parameters used to train the model A structure holding the options the fit was performed under. MATLAB returns an object of its own class here; a structure carries the same information and is what every other learner in this package returns. Beta, Sigma and KernelParameters are the starting values the fit was given, empty or zero where it was given none, as they are in MATLAB. What the fit found is reported by the Beta and Sigma properties and by KernelInformation. Beta defaults to a zero for every column the basis contributes, so a 'linear' basis over three predictors starts at four zeros. SigmaLowerBound is the exception and is reported as it was resolved. MATLAB publishes no top-level field of that name, keeping it inside an Options structure this class does not carry. The fields MATLAB reports for its approximate fitting methods (ActiveSet, Options, OptimizerOptions, ConstantKernelParameters, InitialStepSize, InitialSigmaLowerBoundTolerance, Verbose and CacheSize) are absent, this class implementing exact fitting alone. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Parameters used to train the model # name: # type: sq_string # elements: 1 # length: 28 RegressionGP.NumObservations # name: # type: sq_string # elements: 1 # length: 272 RegressionGP: property NumObservations Number of observations used to train the model A positive integer scalar, counting the rows whose response is not missing. A row missing a predictor is counted, as in MATLAB, though the fit leaves it out. This property is read-only. # name: # type: sq_string # elements: 1 # length: 46 Number of observations used to train the model # name: # type: sq_string # elements: 1 # length: 26 RegressionGP.PredictMethod # name: # type: sq_string # elements: 1 # length: 106 RegressionGP: property PredictMethod Method used to make predictions 'Exact'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Method used to make predictions # name: # type: sq_string # elements: 1 # length: 30 RegressionGP.PredictorLocation # name: # type: sq_string # elements: 1 # length: 198 RegressionGP: property PredictorLocation Means the predictors were centred by A 1xP numeric vector when the model standardized its predictors, and empty when it did not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Means the predictors were centred by # name: # type: sq_string # elements: 1 # length: 27 RegressionGP.PredictorNames # name: # type: sq_string # elements: 1 # length: 147 RegressionGP: property PredictorNames Predictor variable names A cell array of character vectors, one per column of X. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Predictor variable names # name: # type: sq_string # elements: 1 # length: 27 RegressionGP.PredictorScale # name: # type: sq_string # elements: 1 # length: 208 RegressionGP: property PredictorScale Standard deviations the predictors were scaled by A 1xP numeric vector when the model standardized its predictors, and empty when it did not. This property is read-only. # name: # type: sq_string # elements: 1 # length: 49 Standard deviations the predictors were scaled by # name: # type: sq_string # elements: 1 # length: 25 RegressionGP.RegressionGP # name: # type: sq_string # elements: 1 # length: 5914 RegressionGP: obj = RegressionGP (X, Y) RegressionGP: obj = RegressionGP (Tbl, ResponseVarName) RegressionGP: obj = RegressionGP (Tbl, formula) RegressionGP: obj = RegressionGP (Tbl, Y) RegressionGP: obj = RegressionGP (..., name, value) Fit a Gaussian process regression model. obj = RegressionGP (X, Y) returns a Gaussian process regression model, obj, with X being the predictor data and Y the continuous response of the observations in X. X must be an NxP numeric matrix of predictor data, where rows correspond to observations and columns to features. Y must be an Nx1 numeric vector holding the response of the corresponding predictor data in X. Y must have the same number of rows as X. A Gaussian process places a prior over functions, given by the covariance function, and conditions it on the observations. The response is modelled as HxBeta plus a draw from that process plus independent noise of standard deviation Sigma, where H is the explicit basis. The covariance parameters and Sigma are estimated by maximizing the log marginal likelihood, and Beta follows from them in closed form as the generalized least squares estimate. obj = RegressionGP (..., name, value) returns a model with additional options specified by Name-Value pair arguments listed below. Name Value 'KernelFunction' A character vector naming the covariance function, or a function handle taking two matrices of points and a parameter vector. The default is 'squaredexponential'. The supported names are listed below. 'KernelParameters' A numeric vector of initial values for the covariance parameters. Its length depends on the covariance function. These are starting values for the optimization, not fixed values. 'BasisFunction' A character vector naming the explicit basis, one of 'none', 'constant', 'linear' or 'pureQuadratic', or a function handle taking X and returning the basis matrix. The default is 'constant'. 'Beta' A numeric vector of basis coefficients. These are used as known values only when 'FitMethod' is 'none'. 'Sigma' A positive scalar, the initial value of the noise standard deviation. The default is std (Y) / sqrt (2). 'ConstantSigma' A logical scalar. When true the noise standard deviation is held at its initial value instead of being estimated. The default is false. 'SigmaLowerBound' A positive scalar bounding the noise standard deviation from below. The default is 1e-2 * std (Y). 'FitMethod' A character vector, either 'exact' to estimate the parameters or 'none' to keep them at their initial values. The default is 'exact'. 'PredictMethod' A character vector. Only 'exact' is implemented, which is also the only method under which a standard deviation and a prediction interval are available. 'Optimizer' A character vector naming the optimizer used to maximize the log marginal likelihood. 'quasinewton' and 'fminunc' name the same dense solver and are the default, 'lbfgs' selects limited-memory BFGS, which holds a fixed number of curvature pairs rather than a full inverse Hessian and is the cheaper choice when the kernel carries many parameters, and 'fminsearch' is derivative-free. 'Standardize' A logical scalar specifying whether the predictor data should be centred and scaled before training. The same transformation is applied by predict. The default is false. 'CategoricalPredictors' The categorical predictors, as indices, as a logical vector with one element per predictor, or as 'all'. Each is dummy coded in its place, one column of zeros and ones per distinct value it takes in the training data, named as in 'x1 == 2', and the coded columns are not standardized. X keeps the predictors as given. A row holding a value the training data did not is predicted as a row missing a predictor, the weighted lower median of the training response. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. 'Weights' An Nx1 single or double vector of non-negative observation weights. The default is a vector of ones. The model's W keeps the class of the weights, while every computation runs in double. 'PredictorNames' A cell array of character vectors naming the predictors, in the order they appear in X. 'ResponseName' A character vector naming the response. The default is 'Y'. 'ResponseTransform' A character vector or a function handle applied to the response the model predicts. The default is 'none'. The supported values for 'KernelFunction' are: Value Parameters 'exponential' [SigmaL; SigmaF] 'squaredexponential' [SigmaL; SigmaF] 'matern32' [SigmaL; SigmaF] 'matern52' [SigmaL; SigmaF] 'rationalquadratic' [SigmaL; AlphaRQ; SigmaF] 'ardexponential' [LengthScale1; ...; SigmaF] 'ardsquaredexponential' [LengthScale1; ...; SigmaF] 'ardmatern32' [LengthScale1; ...; SigmaF] 'ardmatern52' [LengthScale1; ...; SigmaF] 'ardrationalquadratic' [LengthScale1; ...; AlphaRQ; SigmaF] The automatic relevance determination kernels carry one length scale per predictor, so a predictor the response does not depend on is given a large length scale and stops contributing. The supported values for 'ResponseTransform' are: Value Description 'none' x (no transformation) 'identity' x (no transformation) 'exp' exp (x) 'log' log (x) Two deviations from MATLAB are deliberate and documented. The distance between points is accumulated one predictor at a time instead of by the expanded form MATLAB uses by default, because the expanded form does not return exactly zero for a point against itself and the rough kernels amplify that residue through their square root. The approximate fitting and prediction methods, 'sd', 'sr', 'fic' and 'bcd', together with the active set options that serve them, are not implemented and are refused rather than silently ignored. See also: fitrgp, CompactRegressionGP, RegressionSVM, RegressionGAM # name: # type: sq_string # elements: 1 # length: 40 Fit a Gaussian process regression model. # name: # type: sq_string # elements: 1 # length: 25 RegressionGP.ResponseName # name: # type: sq_string # elements: 1 # length: 107 RegressionGP: property ResponseName Response variable name A character vector. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Response variable name # name: # type: sq_string # elements: 1 # length: 30 RegressionGP.ResponseTransform # name: # type: sq_string # elements: 1 # length: 196 RegressionGP: property ResponseTransform Transformation applied to the predicted response A character vector, or the text of the function handle that was supplied. Assigning to it accepts either. # name: # type: sq_string # elements: 1 # length: 48 Transformation applied to the predicted response # name: # type: sq_string # elements: 1 # length: 21 RegressionGP.RowsUsed # name: # type: sq_string # elements: 1 # length: 281 RegressionGP: property RowsUsed Rows of the original data used to train the model A logical vector with one element per row of the data as supplied, true where the row was used, false where its response was missing. It is empty when no row was dropped. This property is read-only. # name: # type: sq_string # elements: 1 # length: 49 Rows of the original data used to train the model # name: # type: sq_string # elements: 1 # length: 18 RegressionGP.Sigma # name: # type: sq_string # elements: 1 # length: 111 RegressionGP: property Sigma Estimated noise standard deviation A positive scalar. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Estimated noise standard deviation # name: # type: sq_string # elements: 1 # length: 14 RegressionGP.W # name: # type: sq_string # elements: 1 # length: 220 RegressionGP: property W Observation weights An Nx1 vector, one weight per observation used to train the model, scaled to sum to one and of the class of the 'Weights' given, single or double. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 14 RegressionGP.X # name: # type: sq_string # elements: 1 # length: 323 RegressionGP: property X Predictor data An NxP numeric matrix, as it was supplied to the constructor. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 14 RegressionGP.Y # name: # type: sq_string # elements: 1 # length: 129 RegressionGP: property Y Response data An Nx1 numeric vector, as it was supplied to the constructor. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Response data # name: # type: sq_string # elements: 1 # length: 20 RegressionGP.compact # name: # type: sq_string # elements: 1 # length: 277 RegressionGP: CMdl = compact (obj) Return a compact Gaussian process regression model. CMdl = compact (obj) returns a CompactRegressionGP object holding what is needed to predict and nothing else: the training data, the response and everything that describes them are dropped. # name: # type: sq_string # elements: 1 # length: 51 Return a compact Gaussian process regression model. # name: # type: sq_string # elements: 1 # length: 21 RegressionGP.crossval # name: # type: sq_string # elements: 1 # length: 373 RegressionGP: CVMdl = crossval (obj) RegressionGP: CVMdl = crossval (..., name, value) Cross validate a Gaussian process model. CVMdl = crossval (obj) returns a RegressionPartitionedModel built from the model obj by ten-fold cross validation. CVMdl = crossval (..., name, value) accepts 'KFold', 'Holdout', 'Leaveout' and 'CVPartition', of which at most one may be given. # name: # type: sq_string # elements: 1 # length: 40 Cross validate a Gaussian process model. # name: # type: sq_string # elements: 1 # length: 17 RegressionGP.loss # name: # type: sq_string # elements: 1 # length: 866 RegressionGP: L = loss (obj, X, Y) RegressionGP: L = loss (obj, Tbl, ResponseVarName) RegressionGP: L = loss (obj, Tbl) RegressionGP: L = loss (..., name, value) Compute the regression loss of a Gaussian process model. L = loss (obj, X, Y) returns the mean squared error of the model obj on the data X and Y. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. L = loss (..., name, value) accepts 'LossFun', either 'mse', 'mae', 'epsiloninsensitive' or a function handle taking the observed and the predicted response, and 'Weights', a vector of non-negative observation weights. # name: # type: sq_string # elements: 1 # length: 56 Compute the regression loss of a Gaussian process model. # name: # type: sq_string # elements: 1 # length: 30 RegressionGP.postFitStatistics # name: # type: sq_string # elements: 1 # length: 559 RegressionGP: [loores, neff] = postFitStatistics (obj) Compute the leave-one-out residuals of a Gaussian process model. [loores, neff] = postFitStatistics (obj) returns the Nx1 vector of leave-one-out residuals of the model obj, and the number of effective parameters the fit uses. Neither requires refitting the model: both follow from the factorization the fit already produced. The coefficients of the explicit basis are treated as estimated, which is what FitMethod 'Exact' makes them, while the covariance parameters and the noise are treated as known. # name: # type: sq_string # elements: 1 # length: 64 Compute the leave-one-out residuals of a Gaussian process model. # name: # type: sq_string # elements: 1 # length: 20 RegressionGP.predict # name: # type: sq_string # elements: 1 # length: 1186 RegressionGP: yFit = predict (obj, XC) RegressionGP: [yFit, ySD, yInt] = predict (obj, XC) RegressionGP: [...] = predict (..., 'Alpha', alpha) Predict the response for new data with a Gaussian process model. yFit = predict (obj, XC) returns the predicted response of the RegressionGP model obj at the points in XC, which must have as many columns as the model has predictors. [yFit, ySD, yInt] = predict (...) also returns the standard deviation of each predicted response and the prediction intervals. The standard deviation is that of a new response, so it carries the noise as well as the uncertainty of the latent function, and the interval is the normal quantile of the level times it. [...] = predict (..., 'Alpha', alpha) sets the significance level of the intervals, so that they are 100 x (1 - alpha) per cent intervals. alpha must be a scalar in the range [0, 1] and defaults to 0.05. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. # name: # type: sq_string # elements: 1 # length: 64 Predict the response for new data with a Gaussian process model. # name: # type: sq_string # elements: 1 # length: 22 RegressionGP.resubLoss # name: # type: sq_string # elements: 1 # length: 274 RegressionGP: L = resubLoss (obj) RegressionGP: L = resubLoss (..., name, value) Compute the resubstitution loss of a Gaussian process model. L = resubLoss (obj) returns the loss of the model obj on the data it was trained on, and accepts the same Name-Value pairs as loss. # name: # type: sq_string # elements: 1 # length: 60 Compute the resubstitution loss of a Gaussian process model. # name: # type: sq_string # elements: 1 # length: 25 RegressionGP.resubPredict # name: # type: sq_string # elements: 1 # length: 321 RegressionGP: yFit = resubPredict (obj) RegressionGP: [yFit, ySD, yInt] = resubPredict (obj) Predict the response of the training data with a Gaussian process model. yFit = resubPredict (obj) returns the response the RegressionGP model obj predicts at its own training data, and the further outputs are those of predict. # name: # type: sq_string # elements: 1 # length: 72 Predict the response of the training data with a Gaussian process model. # name: # type: sq_string # elements: 1 # length: 22 RegressionGP.savemodel # name: # type: sq_string # elements: 1 # length: 176 RegressionGP: savemodel (obj, filename) Save a Gaussian process model to a file. savemodel (obj, filename) saves the model obj into filename in a form loadmodel can read back. # name: # type: sq_string # elements: 1 # length: 40 Save a Gaussian process model to a file. # name: # type: sq_string # elements: 1 # length: 16 RegressionKernel # name: # type: sq_string # elements: 1 # length: 1286 statistics: RegressionKernel Gaussian kernel regression model for large data. A RegressionKernel object maps the predictors into a randomized feature space whose inner product approximates a Gaussian kernel, and then fits a linear model there. A kernel regression is therefore as nonlinear as a support vector machine with a Gaussian kernel, while costing what a linear fit costs: nothing of size NxN is ever formed. The expansion is the random Fourier basis of Rahimi and Recht, drawn once when the model is fitted and kept with it, so predict maps new data through the same basis. MATLAB approximates the same kernel by the Fastfood construction, which reaches the same distribution more cheaply; the two are interchangeable in distribution but not draw by draw, and the draws come from different generators in any case, so the predictions of a model fitted here and one fitted in MATLAB differ even from the same seed. What does not differ is what they estimate. Like RegressionLinear the object holds no copy of the training data. It does hold the basis and the coefficients, so it is bounded by the number of expansion dimensions rather than by the number of observations. Create a RegressionKernel object with fitrkernel. See also: fitrkernel, RegressionLinear, RegressionSVM # name: # type: sq_string # elements: 1 # length: 48 Gaussian kernel regression model for large data. # name: # type: sq_string # elements: 1 # length: 30 RegressionKernel.BoxConstraint # name: # type: sq_string # elements: 1 # length: 305 RegressionKernel: property BoxConstraint Box constraint of the support vector machine A positive scalar. It is the reciprocal of the product of Lambda and the number of observations, so setting either of the two in the constructor fixes the other, and giving both is an error. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Box constraint of the support vector machine # name: # type: sq_string # elements: 1 # length: 38 RegressionKernel.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 186 RegressionKernel: property CategoricalPredictors Indices of the categorical predictors A row vector of column indices, empty when every predictor is numeric. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 24 RegressionKernel.Epsilon # name: # type: sq_string # elements: 1 # length: 361 RegressionKernel: property Epsilon Half the width of the epsilon-insensitive band A nonnegative scalar for a support vector machine, and empty for a least squares fit, which has no such band. It defaults to the interquartile range of the response over 13.49, an estimate of its standard deviation, or to 0.1 when that range is zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 46 Half the width of the epsilon-insensitive band # name: # type: sq_string # elements: 1 # length: 39 RegressionKernel.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 319 RegressionKernel: property ExpandedPredictorNames Names of the predictors as the fit saw them A cell array of character vectors. It equals PredictorNames unless categorical predictors were dummy coded, each then named once per level, as in 'x1 == 2'. The expansion dimensions have no names. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Names of the predictors as the fit saw them # name: # type: sq_string # elements: 1 # length: 27 RegressionKernel.FittedLoss # name: # type: sq_string # elements: 1 # length: 183 RegressionKernel: property FittedLoss Loss function the fit minimized 'epsiloninsensitive' for a support vector machine and 'mse' for a least squares fit. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Loss function the fit minimized # name: # type: sq_string # elements: 1 # length: 28 RegressionKernel.KernelScale # name: # type: sq_string # elements: 1 # length: 224 RegressionKernel: property KernelScale Scale of the Gaussian kernel A positive scalar dividing every predictor before the expansion, so a larger scale makes the kernel wider and the fit smoother. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Scale of the Gaussian kernel # name: # type: sq_string # elements: 1 # length: 23 RegressionKernel.Lambda # name: # type: sq_string # elements: 1 # length: 187 RegressionKernel: property Lambda Regularization strength A nonnegative scalar, the reciprocal of the product of BoxConstraint and the number of observations. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Regularization strength # name: # type: sq_string # elements: 1 # length: 24 RegressionKernel.Learner # name: # type: sq_string # elements: 1 # length: 137 RegressionKernel: property Learner Linear model fitted in the expanded space Either 'svm' or 'leastsquares'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 41 Linear model fitted in the expanded space # name: # type: sq_string # elements: 1 # length: 32 RegressionKernel.ModelParameters # name: # type: sq_string # elements: 1 # length: 232 RegressionKernel: property ModelParameters Fitting options, as they were given A structure holding every parameter of the fit, with the 'auto' values as they were given rather than as they were resolved. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Fitting options, as they were given # name: # type: sq_string # elements: 1 # length: 19 RegressionKernel.Mu # name: # type: sq_string # elements: 1 # length: 197 RegressionKernel: property Mu Predictor means used to standardize A row vector with one element per predictor, or empty when the model was fitted without standardizing. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Predictor means used to standardize # name: # type: sq_string # elements: 1 # length: 39 RegressionKernel.NumExpansionDimensions # name: # type: sq_string # elements: 1 # length: 338 RegressionKernel: property NumExpansionDimensions Number of dimensions of the expanded space A positive integer scalar. It defaults to 2 .^ ceil (min (log2 (p) + 5, 15)) for p predictors, so four predictors give 128 dimensions. More dimensions approximate the kernel more closely and cost proportionally more. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Number of dimensions of the expanded space # name: # type: sq_string # elements: 1 # length: 31 RegressionKernel.PredictorNames # name: # type: sq_string # elements: 1 # length: 211 RegressionKernel: property PredictorNames Names of the predictors A cell array of character vectors with one name per column of the training data, defaulting to 'x1', 'x2' and so on. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 33 RegressionKernel.RegressionKernel # name: # type: sq_string # elements: 1 # length: 2810 RegressionKernel: obj = RegressionKernel (X, Y) RegressionKernel: obj = RegressionKernel (Tbl, ResponseVarName) RegressionKernel: obj = RegressionKernel (Tbl, formula) RegressionKernel: obj = RegressionKernel (Tbl, Y) RegressionKernel: obj = RegressionKernel (..., name, value) Fit a Gaussian kernel regression model. obj = RegressionKernel (X, Y) fits a support vector machine in a randomized Gaussian kernel space to the NxP predictor matrix X and the Nx1 continuous response Y. obj = RegressionKernel (..., name, value) takes the following Name-Value pairs. Name Value 'Learner' 'svm', the default, or 'leastsquares'. 'Epsilon' Half the width of the insensitive band, a nonnegative scalar or 'auto', which is the interquartile range of Y over 13.49. It applies to a support vector machine alone. 'NumExpansionDimensions' 'auto', the default, or a positive integer. 'KernelScale' 1 by default, a positive scalar, or 'auto', which takes the median distance between the observations. 'Lambda' 'auto', the default, which is the reciprocal of the number of observations, or a nonnegative scalar. It cannot be given beside 'BoxConstraint'. 'BoxConstraint' A positive scalar, 1 by default. It applies to a support vector machine alone. 'Standardize' Whether to centre and scale the predictors, false by default. 'BetaTolerance' Relative tolerance on the coefficients, 1e-4 by default. 'GradientTolerance' Absolute tolerance on the gradient's infinity norm, 1e-6 by default. 'IterationLimit' Largest number of iterations, 1000 by default. 'HessianHistorySize' Number of curvature pairs the solver keeps, 15 by default. 'BlockSize' Memory the expansion may occupy, in megabytes, 4e3 by default. 'ResponseTransform' A transformation applied to the predicted response, named or given as a function handle. 'Weights' One nonnegative weight per observation, as a single or double vector. Every computation runs in double. 'PredictorNames' One name per predictor. 'ResponseName' A name for the response. 'CategoricalPredictors' The categorical predictors, as indices, as a logical vector with one element per predictor, or as 'all'. Each is dummy coded in its place, one column of zeros and ones per distinct value it takes in the training data, named as in 'x1 == 2', and the coded columns are not standardized. A row holding a value the training data did not is predicted as a row missing a predictor, the weighted lower median of the training response. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. The fit is always by limited-memory BFGS, the only solver MATLAB offers a kernel model, and always under a ridge penalty. See also: fitrkernel, RegressionLinear # name: # type: sq_string # elements: 1 # length: 39 Fit a Gaussian kernel regression model. # name: # type: sq_string # elements: 1 # length: 31 RegressionKernel.Regularization # name: # type: sq_string # elements: 1 # length: 207 RegressionKernel: property Regularization Penalty on the coefficients Always 'ridge (L2)': a kernel model fits in the expanded space, where a lasso penalty has nothing to select. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Penalty on the coefficients # name: # type: sq_string # elements: 1 # length: 29 RegressionKernel.ResponseName # name: # type: sq_string # elements: 1 # length: 128 RegressionKernel: property ResponseName Name of the response A character vector, defaulting to 'Y'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Name of the response # name: # type: sq_string # elements: 1 # length: 34 RegressionKernel.ResponseTransform # name: # type: sq_string # elements: 1 # length: 200 RegressionKernel: property ResponseTransform Transformation applied to the predicted response A character vector, or the text of the function handle that was supplied. Assigning to it accepts either. # name: # type: sq_string # elements: 1 # length: 48 Transformation applied to the predicted response # name: # type: sq_string # elements: 1 # length: 22 RegressionKernel.Sigma # name: # type: sq_string # elements: 1 # length: 214 RegressionKernel: property Sigma Predictor standard deviations used to standardize A row vector with one element per predictor, or empty when the model was fitted without standardizing. This property is read-only. # name: # type: sq_string # elements: 1 # length: 49 Predictor standard deviations used to standardize # name: # type: sq_string # elements: 1 # length: 21 RegressionKernel.loss # name: # type: sq_string # elements: 1 # length: 816 RegressionKernel: l = loss (obj, X, Y) RegressionKernel: l = loss (obj, Tbl, ResponseVarName) RegressionKernel: l = loss (obj, Tbl) RegressionKernel: l = loss (..., name, value) Regression loss on new data. l = loss (obj, X, Y) returns the mean squared error. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. l = loss (..., name, value) takes 'LossFun', either 'mse' or 'epsiloninsensitive', and 'Weights'. The epsilon-insensitive loss needs a band to be insensitive within, so it is offered by a support vector machine alone. # name: # type: sq_string # elements: 1 # length: 28 Regression loss on new data. # name: # type: sq_string # elements: 1 # length: 24 RegressionKernel.predict # name: # type: sq_string # elements: 1 # length: 524 RegressionKernel: yFit = predict (obj, XC) Predict the response of new observations. yFit = predict (obj, XC) maps each row of XC through the model's own random basis and returns the predicted response, with ResponseTransform applied. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. # name: # type: sq_string # elements: 1 # length: 41 Predict the response of new observations. # name: # type: sq_string # elements: 1 # length: 23 RegressionKernel.resume # name: # type: sq_string # elements: 1 # length: 1184 RegressionKernel: obj = resume (obj, X, Y) RegressionKernel: obj = resume (..., name, value) RegressionKernel: obj = resume (obj, Tbl, ResponseVarName) Continue fitting a kernel regression model. obj = resume (obj, X, Y) restarts the optimization from the coefficients the model already carries, through the basis it already holds. It takes 'BetaTolerance', 'GradientTolerance' and 'IterationLimit', each defaulting to what the model was fitted with, and 'Weights'. X and Y must be the data the model was fitted to; the object keeps no copy of them, which is what makes it small. Neither does it keep the observation weights, so a model fitted with 'Weights' must be given them again here or it will resume against uniform ones. MATLAB behaves the same way: measured on R2024a, resuming a weighted fit without passing the weights back reaches the objective of the unweighted fit. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. The response is then named by ResponseVarName, a variable of the table, or given beside the table as Y; unlike RegressionKernel.loss, it is never taken from the table unasked. # name: # type: sq_string # elements: 1 # length: 43 Continue fitting a kernel regression model. # name: # type: sq_string # elements: 1 # length: 26 RegressionKernel.savemodel # name: # type: sq_string # elements: 1 # length: 208 RegressionKernel: savemodel (obj, filename) Save a kernel regression model to a file. savemodel (obj, filename) saves the model obj into filename in a form loadmodel can read back, the random basis included. # name: # type: sq_string # elements: 1 # length: 41 Save a kernel regression model to a file. # name: # type: sq_string # elements: 1 # length: 16 RegressionLinear # name: # type: sq_string # elements: 1 # length: 1077 statistics: RegressionLinear Linear regression model for high dimensional data. A RegressionLinear object fits a linear model, X * Beta + Bias, to a continuous response by minimizing a regularized average loss. The loss is the epsilon-insensitive loss for a support vector machine and the squared error for a least squares fit, and the penalty is either a ridge or a lasso one. Unlike the other regression models of this package the object holds no copy of the training data: the coefficients, the intercept and the fitting options are the whole model. That is what makes it suited to data with more predictors than a kernel matrix could carry, and it is why the class has no compact method and no resubstitution methods. A vector of regularization strengths fits one model per value in a single object. Beta is then a PxL matrix and Bias a 1xL row, every method returns one column per strength, and selectModels narrows the object down to the strengths worth keeping. Create a RegressionLinear object with fitrlinear. See also: fitrlinear, RegressionKernel, RegressionSVM # name: # type: sq_string # elements: 1 # length: 50 Linear regression model for high dimensional data. # name: # type: sq_string # elements: 1 # length: 21 RegressionLinear.Beta # name: # type: sq_string # elements: 1 # length: 194 RegressionLinear: property Beta Fitted linear coefficients A Px1 column, or a PxL matrix with one column per regularization strength when Lambda holds more than one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Fitted linear coefficients # name: # type: sq_string # elements: 1 # length: 21 RegressionLinear.Bias # name: # type: sq_string # elements: 1 # length: 223 RegressionLinear: property Bias Fitted intercept A scalar, or a 1xL row with one element per regularization strength. It is zero throughout when the model was fitted with 'FitBias' set to false. This property is read-only. # name: # type: sq_string # elements: 1 # length: 16 Fitted intercept # name: # type: sq_string # elements: 1 # length: 38 RegressionLinear.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 186 RegressionLinear: property CategoricalPredictors Indices of the categorical predictors A row vector of column indices, empty when every predictor is numeric. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 24 RegressionLinear.Epsilon # name: # type: sq_string # elements: 1 # length: 361 RegressionLinear: property Epsilon Half the width of the epsilon-insensitive band A nonnegative scalar for a support vector machine, and empty for a least squares fit, which has no such band. It defaults to the interquartile range of the response over 13.49, an estimate of its standard deviation, or to 0.1 when that range is zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 46 Half the width of the epsilon-insensitive band # name: # type: sq_string # elements: 1 # length: 39 RegressionLinear.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 252 RegressionLinear: property ExpandedPredictorNames Names of the predictors as the fit saw them A cell array of character vectors. It equals PredictorNames unless categorical predictors were expanded into indicator variables. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Names of the predictors as the fit saw them # name: # type: sq_string # elements: 1 # length: 27 RegressionLinear.FittedLoss # name: # type: sq_string # elements: 1 # length: 277 RegressionLinear: property FittedLoss Loss function the fit minimized 'epsiloninsensitive' for a support vector machine and 'mse' for a least squares fit. This is the loss of the objective, which is not the loss loss reports unless it is asked for. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Loss function the fit minimized # name: # type: sq_string # elements: 1 # length: 23 RegressionLinear.Lambda # name: # type: sq_string # elements: 1 # length: 234 RegressionLinear: property Lambda Regularization strength A nonnegative scalar, or a 1xL row of them in ascending order. It defaults to the reciprocal of the number of observations used to train the model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Regularization strength # name: # type: sq_string # elements: 1 # length: 24 RegressionLinear.Learner # name: # type: sq_string # elements: 1 # length: 135 RegressionLinear: property Learner Linear regression model that was fitted Either 'svm' or 'leastsquares'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Linear regression model that was fitted # name: # type: sq_string # elements: 1 # length: 32 RegressionLinear.ModelParameters # name: # type: sq_string # elements: 1 # length: 263 RegressionLinear: property ModelParameters Fitting options, as they were given A structure holding every parameter of the fit, including the ones that a different solver would have used and the 'auto' values before they were resolved. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Fitting options, as they were given # name: # type: sq_string # elements: 1 # length: 31 RegressionLinear.PredictorNames # name: # type: sq_string # elements: 1 # length: 211 RegressionLinear: property PredictorNames Names of the predictors A cell array of character vectors with one name per column of the training data, defaulting to 'x1', 'x2' and so on. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 33 RegressionLinear.RegressionLinear # name: # type: sq_string # elements: 1 # length: 3987 RegressionLinear: obj = RegressionLinear (X, Y) RegressionLinear: obj = RegressionLinear (Tbl, ResponseVarName) RegressionLinear: obj = RegressionLinear (Tbl, formula) RegressionLinear: obj = RegressionLinear (Tbl, Y) RegressionLinear: obj = RegressionLinear (..., name, value) Fit a linear regression model. obj = RegressionLinear (X, Y) fits a linear support vector machine to the NxP predictor matrix X and the Nx1 continuous response Y. obj = RegressionLinear (..., name, value) takes the following Name-Value pairs. Name Value 'Learner' 'svm', the default, or 'leastsquares'. The first minimizes the epsilon-insensitive loss and the second the squared error. 'Epsilon' Half the width of the insensitive band, a nonnegative scalar or 'auto', which is the interquartile range of Y over 13.49. It applies to a support vector machine alone. 'Regularization' 'ridge' or 'lasso'. It defaults to 'lasso' when the solver is 'sparsa' and to 'ridge' otherwise. 'Lambda' 'auto', the default, which is the reciprocal of the number of observations, or a nonnegative scalar, or a vector of them. A vector fits one model per value. 'Solver' One of 'sgd', 'asgd', 'dual', 'bfgs', 'lbfgs' and 'sparsa', or a cell array of them applied in turn, each warm starting the next. 'Beta' Initial coefficients, a Px1 column or a PxL matrix. It defaults to zeros. 'Bias' Initial intercept, a scalar or a 1xL row. It defaults to the weighted mean of Y for a least squares fit and to its weighted median for a support vector machine. 'FitBias' Whether to fit an intercept at all, true by default. 'PostFitBias' Whether to refit the intercept once the coefficients are settled, false by default. 'ObservationsIn' 'rows', the default, or 'columns', which transposes X before fitting. 'BetaTolerance' Relative tolerance on the coefficients, 1e-4 by default. 'GradientTolerance' Absolute tolerance on the gradient's infinity norm, 1e-6 by default. 'DeltaGradientTolerance' Tolerance on the complementarity gap of the 'dual' solver, 0.1 by default. 'IterationLimit' Largest number of iterations, 1000 by default. 'PassLimit' Largest number of passes over the data for the stochastic solvers, 1 by default, and 10 for 'dual'. 'BatchSize' Mini-batch size of the stochastic solvers, 10 by default. 'BatchLimit' Largest number of mini-batches. 'LearnRate' Step size of the stochastic solvers. 'OptimizeLearnRate' Whether to halve the step size when the objective rises, true by default. 'TruncationPeriod' Number of mini-batches between soft thresholdings under a lasso penalty, 10 by default. 'NumCheckConvergence' Number of passes between convergence checks of the 'dual' solver, 2 by default. MathWorks documents 5; R2024a and R2026a both report 2. 'HessianHistorySize' Number of curvature pairs the quasi-Newton solvers keep, 15 by default. 'ResponseTransform' A transformation applied to the predicted response, named or given as a function handle. 'Weights' One nonnegative weight per observation, as a single or double vector. Every computation runs in double. 'PredictorNames' One name per predictor. 'ResponseName' A name for the response. 'CategoricalPredictors' The categorical predictors, as indices, as a logical vector with one element per predictor, or as 'all'. Each is dummy coded in its place, one column of zeros and ones per distinct value it takes in the training data, named as in 'x1 == 2'. A row holding a value the training data did not is predicted as a row missing a predictor, the weighted lower median of the training response. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. The default solver is 'sparsa' under a lasso penalty. Under a ridge penalty it is 'bfgs' when there are no more than 100 predictors, and beyond that 'dual' for a support vector machine and 'sgd' for a least squares fit. See also: fitrlinear, RegressionKernel # name: # type: sq_string # elements: 1 # length: 30 Fit a linear regression model. # name: # type: sq_string # elements: 1 # length: 31 RegressionLinear.Regularization # name: # type: sq_string # elements: 1 # length: 128 RegressionLinear: property Regularization Penalty on the coefficients 'ridge (L2)' or 'lasso (L1)'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Penalty on the coefficients # name: # type: sq_string # elements: 1 # length: 29 RegressionLinear.ResponseName # name: # type: sq_string # elements: 1 # length: 128 RegressionLinear: property ResponseName Name of the response A character vector, defaulting to 'Y'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Name of the response # name: # type: sq_string # elements: 1 # length: 34 RegressionLinear.ResponseTransform # name: # type: sq_string # elements: 1 # length: 200 RegressionLinear: property ResponseTransform Transformation applied to the predicted response A character vector, or the text of the function handle that was supplied. Assigning to it accepts either. # name: # type: sq_string # elements: 1 # length: 48 Transformation applied to the predicted response # name: # type: sq_string # elements: 1 # length: 21 RegressionLinear.loss # name: # type: sq_string # elements: 1 # length: 855 RegressionLinear: l = loss (obj, X, Y) RegressionLinear: l = loss (obj, Tbl, ResponseVarName) RegressionLinear: l = loss (obj, Tbl) RegressionLinear: l = loss (..., name, value) Regression loss on new data. l = loss (obj, X, Y) returns the mean squared error, one value per regularization strength. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. l = loss (..., name, value) takes 'LossFun', either 'mse' or 'epsiloninsensitive', and 'Weights'. The epsilon-insensitive loss needs a band to be insensitive within, so it is offered by a support vector machine alone. # name: # type: sq_string # elements: 1 # length: 28 Regression loss on new data. # name: # type: sq_string # elements: 1 # length: 24 RegressionLinear.predict # name: # type: sq_string # elements: 1 # length: 530 RegressionLinear: yFit = predict (obj, XC) Predict the response of new observations. yFit = predict (obj, XC) returns one predicted value per row of XC, and one column per regularization strength. ResponseTransform is applied to the result. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. # name: # type: sq_string # elements: 1 # length: 41 Predict the response of new observations. # name: # type: sq_string # elements: 1 # length: 26 RegressionLinear.savemodel # name: # type: sq_string # elements: 1 # length: 181 RegressionLinear: savemodel (obj, filename) Save a linear regression model to a file. savemodel (obj, filename) saves the model obj into filename in a form loadmodel can read back. # name: # type: sq_string # elements: 1 # length: 41 Save a linear regression model to a file. # name: # type: sq_string # elements: 1 # length: 29 RegressionLinear.selectModels # name: # type: sq_string # elements: 1 # length: 248 RegressionLinear: sub = selectModels (obj, idx) Keep a subset of the fitted regularization strengths. sub = selectModels (obj, idx) returns a model holding only the strengths idx names, which may be indices into Lambda or a logical vector over it. # name: # type: sq_string # elements: 1 # length: 53 Keep a subset of the fitted regularization strengths. # name: # type: sq_string # elements: 1 # length: 23 RegressionNeuralNetwork # name: # type: sq_string # elements: 1 # length: 795 statistics: RegressionNeuralNetwork Neural network regression model. A RegressionNeuralNetwork object holds a fully connected feedforward neural network fitted to a continuous response, and predicts the response for new data with the predict method. The network is trained against the mean squared error and its output layer applies the identity, so a prediction is an unrestricted real number; otherwise the engine is the one ClassificationNeuralNetwork uses. The object keeps its training data, which resubPredict, resubLoss and crossval work on; compact drops it and returns a CompactRegressionNeuralNetwork, which still predicts. Create a RegressionNeuralNetwork object with fitrnet or the class constructor. See also: fitrnet, CompactRegressionNeuralNetwork, ClassificationNeuralNetwork # name: # type: sq_string # elements: 1 # length: 32 Neural network regression model. # name: # type: sq_string # elements: 1 # length: 35 RegressionNeuralNetwork.Activations # name: # type: sq_string # elements: 1 # length: 238 RegressionNeuralNetwork: property Activations Activation functions of the hidden layers A character vector, applying to every hidden layer, or a cell array of character vectors with one entry per hidden layer. This property is read-only. # name: # type: sq_string # elements: 1 # length: 41 Activation functions of the hidden layers # name: # type: sq_string # elements: 1 # length: 32 RegressionNeuralNetwork.BinEdges # name: # type: sq_string # elements: 1 # length: 358 RegressionNeuralNetwork: property BinEdges Bin edges of the predictors A cell array with one entry per predictor, holding that predictor's bin edges where the learner discretized it before fitting. It is empty here and stays empty: this learner fits the predictors as they are, and MATLAB's reports an empty cell for it as well. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Bin edges of the predictors # name: # type: sq_string # elements: 1 # length: 45 RegressionNeuralNetwork.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 234 RegressionNeuralNetwork: property CategoricalPredictors Indices of the categorical predictors A numeric vector of column indices into X naming the predictors treated as categorical, and empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 39 RegressionNeuralNetwork.ConvergenceInfo # name: # type: sq_string # elements: 1 # length: 572 RegressionNeuralNetwork: property ConvergenceInfo Information recorded during training A structure with the fields Time, the seconds training took, and TrainingLoss, the mean squared error of the network at the end of each iteration. This property is read-only. Under 'lbfgs' the structure carries Gradient and Step, the two quantities the solver measured to decide it had converged, and ConvergenceCriterion, naming the test that stopped it. It carries no Accuracy: MATLAB reports none, and measuring it would cost a pass over the whole training set at every iteration. # name: # type: sq_string # elements: 1 # length: 36 Information recorded during training # name: # type: sq_string # elements: 1 # length: 35 RegressionNeuralNetwork.DisplayInfo # name: # type: sq_string # elements: 1 # length: 130 RegressionNeuralNetwork: property DisplayInfo Whether training printed its progress A logical scalar. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Whether training printed its progress # name: # type: sq_string # elements: 1 # length: 46 RegressionNeuralNetwork.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 267 RegressionNeuralNetwork: property ExpandedPredictorNames Names of the predictors as the model expanded them A cell array of character vectors. It matches PredictorNames unless a categorical predictor was expanded into indicator variables. This property is read-only. # name: # type: sq_string # elements: 1 # length: 50 Names of the predictors as the model expanded them # name: # type: sq_string # elements: 1 # length: 57 RegressionNeuralNetwork.HyperparameterOptimizationResults # name: # type: sq_string # elements: 1 # length: 365 RegressionNeuralNetwork: property HyperparameterOptimizationResults Results of the hyperparameter optimization Always empty. It is declared for MATLAB compatibility, where it holds what an automatic search over the hyperparameters found. This class fits the parameters it is given and runs no such search, so there is nothing to report. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Results of the hyperparameter optimization # name: # type: sq_string # elements: 1 # length: 38 RegressionNeuralNetwork.IterationLimit # name: # type: sq_string # elements: 1 # length: 142 RegressionNeuralNetwork: property IterationLimit Maximum number of training iterations A positive integer scalar. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Maximum number of training iterations # name: # type: sq_string # elements: 1 # length: 35 RegressionNeuralNetwork.LayerBiases # name: # type: sq_string # elements: 1 # length: 230 RegressionNeuralNetwork: property LayerBiases Biases the network learned A cell array with one entry per layer, the output layer included. LayerBiases{i} is a column with one entry per unit of layer i. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Biases the network learned # name: # type: sq_string # elements: 1 # length: 34 RegressionNeuralNetwork.LayerSizes # name: # type: sq_string # elements: 1 # length: 251 RegressionNeuralNetwork: property LayerSizes Sizes of the fully connected hidden layers A row vector of positive integers, one per hidden layer. It does not include the output layer, whose width is the number of responses. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Sizes of the fully connected hidden layers # name: # type: sq_string # elements: 1 # length: 36 RegressionNeuralNetwork.LayerWeights # name: # type: sq_string # elements: 1 # length: 257 RegressionNeuralNetwork: property LayerWeights Weights the network learned A cell array with one entry per layer, the output layer included. LayerWeights{i} has one row per unit of layer i and one column per input to that layer. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Weights the network learned # name: # type: sq_string # elements: 1 # length: 36 RegressionNeuralNetwork.LearningRate # name: # type: sq_string # elements: 1 # length: 217 RegressionNeuralNetwork: property LearningRate Learning rate for gradient descent A positive scalar value defining the learning rate used by the gradient descent algorithm during training. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Learning rate for gradient descent # name: # type: sq_string # elements: 1 # length: 39 RegressionNeuralNetwork.ModelParameters # name: # type: sq_string # elements: 1 # length: 1143 RegressionNeuralNetwork: property ModelParameters Parameters of the trained network A structure holding the fit as it was asked for: LayerSizes, Activations, OutputLayerActivation, LayerWeightsInitializers, Solver, LearningRate, IterationLimit, GradientTolerance, LossTolerance, StepTolerance, DisplayInfo, StandardizeData, and the Version, Method and Type tags. What came out of the fit is elsewhere: the LayerWeights and LayerBiases properties hold the network, TrainingHistory the series and ConvergenceInfo where it stopped. LayerWeightsInitializers names the scheme each layer's weights were drawn with, the output layer last: 'he' for a rectifying activation and 'glorot' for a symmetric one. It is a report, not a setting, the engine choosing per layer from the activation and offering no way to override it. OutputLayerActivation, Solver and LearningRate are this package's own; MATLAB has no counterpart for them. The fields it reports that this class does not accept as arguments (Lambda, the validation set and its patience and frequency, InitialStepSize and the two initializer settings) are absent. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Parameters of the trained network # name: # type: sq_string # elements: 1 # length: 26 RegressionNeuralNetwork.Mu # name: # type: sq_string # elements: 1 # length: 210 RegressionNeuralNetwork: property Mu Mean of the predictors A row vector with one entry per predictor, used for standardization. Empty when the predictor data were not standardized. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Mean of the predictors # name: # type: sq_string # elements: 1 # length: 39 RegressionNeuralNetwork.NumObservations # name: # type: sq_string # elements: 1 # length: 220 RegressionNeuralNetwork: property NumObservations Number of observations used to train the model A positive integer scalar, counting only the rows that survived the removal of missing values. This property is read-only. # name: # type: sq_string # elements: 1 # length: 46 Number of observations used to train the model # name: # type: sq_string # elements: 1 # length: 37 RegressionNeuralNetwork.NumPredictors # name: # type: sq_string # elements: 1 # length: 152 RegressionNeuralNetwork: property NumPredictors Number of predictors A positive integer scalar, the number of columns of X. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of predictors # name: # type: sq_string # elements: 1 # length: 45 RegressionNeuralNetwork.OutputLayerActivation # name: # type: sq_string # elements: 1 # length: 235 RegressionNeuralNetwork: property OutputLayerActivation Activation function of the output layer A character vector. The default, 'none', applies the identity, so a prediction is an unrestricted real number. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Activation function of the output layer # name: # type: sq_string # elements: 1 # length: 38 RegressionNeuralNetwork.PredictorNames # name: # type: sq_string # elements: 1 # length: 157 RegressionNeuralNetwork: property PredictorNames Names of the predictors A cell array of character vectors, one per column of X. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 47 RegressionNeuralNetwork.RegressionNeuralNetwork # name: # type: sq_string # elements: 1 # length: 5262 RegressionNeuralNetwork: obj = RegressionNeuralNetwork (X, Y) RegressionNeuralNetwork: obj = RegressionNeuralNetwork (Tbl, ResponseVarName) RegressionNeuralNetwork: obj = RegressionNeuralNetwork (Tbl, formula) RegressionNeuralNetwork: obj = RegressionNeuralNetwork (Tbl, Y) RegressionNeuralNetwork: obj = RegressionNeuralNetwork (..., name, value) Fit a neural network regression model. obj = RegressionNeuralNetwork (X, Y) returns a neural network regression model, obj, with X being the predictor data and Y the continuous response of the observations in X. X must be an NxP numeric matrix of predictor data, where rows correspond to observations and columns to features. Y must be an Nx1 numeric vector holding the response of the corresponding predictor data in X. Y must have the same number of rows as X. The network is trained against the mean squared error, and its output layer applies the identity, so a prediction is an unrestricted real number rather than a score over classes. This is the only difference in the engine between this class and ClassificationNeuralNetwork; everything else, the layer sizes, the activations, the learning rate and the initialisation, behaves identically. obj = RegressionNeuralNetwork (..., name, value) returns a model with additional options specified by Name-Value pair arguments listed below. Name Value 'Standardize' A logical scalar specifying whether the predictor data should be centred and scaled before training. The same transformation is applied by predict. The default is false. 'CategoricalPredictors' The predictors whose values are levels, as indices, as a logical vector with one element per predictor, or as 'all'. Each is dummy coded in its place, one column of zeros and ones per level seen in training, named as in 'x1 == 2' in ExpandedPredictorNames, and the coded columns are not standardized. An observation holding a level the training data did not is predicted as a row missing a predictor, the lower median of the training response. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. 'PredictorNames' A cell array of character vectors naming the predictors, in the order they appear in X. 'ResponseName' A character vector naming the response. The default is 'Y'. 'ResponseTransform' A character vector naming one of the supported transformations, or a function handle, applied to the predicted response by predict and resubPredict. The default is 'none'. 'LayerSizes' A positive integer vector specifying the number of units in each fully connected hidden layer. The default is 10, one hidden layer of ten units. 'Activations' A character vector or cell array of character vectors specifying the activation of the hidden layers. The supported functions are 'linear', 'sigmoid', 'relu', 'tanh', 'lrelu', 'prelu', 'elu', 'gelu' and 'none'. The default is 'relu'. 'OutputLayerActivation' A character vector specifying the activation of the output layer. The default is 'none', the identity, which is what a regression output calls for. The supported values are the same as for 'Activations'. 'LearningRate' A positive scalar specifying the learning rate for gradient descent. The default is 0.003. A larger rate can drive every unit of a hidden layer negative, after which a rectifier passes no gradient and the network stops training. Applies only when 'Solver' is 'sgd'. 'Solver' A character vector naming the solver that trains the network, either 'lbfgs' or 'sgd'. The default is 'lbfgs', which minimizes the loss over the whole training set at once by limited-memory BFGS, as MATLAB does. It takes no learning rate, stops on the three tolerances below, and reaches a lower training loss in fewer passes over the data, though each of its iterations costs several passes where an epoch costs one. 'sgd' visits the samples one at a time and steps down the gradient of each, running for 'IterationLimit' epochs; it was the default before version 1.9.0. 'GradientTolerance' A nonnegative scalar. Training stops once the gradient's infinity norm falls to or below it, which is the quantity MATLAB tests too. The default is 1e-6. Applies only when 'Solver' is 'lbfgs'. 'StepTolerance' A nonnegative scalar. Training stops once the step's infinity norm falls to or below it, which is the quantity MATLAB tests too. The default is 1e-6. Applies only when 'Solver' is 'lbfgs'. 'LossTolerance' A real scalar. Training stops once the training loss falls to or below it. The test is on the loss itself and not on its change, matching MATLAB; pass -Inf to switch it off. The default is 1e-6. Applies only when 'Solver' is 'lbfgs'. 'IterationLimit' A positive integer specifying the maximum number of training iterations. The default is 1000. Under 'sgd' this counts epochs, under 'lbfgs' solver iterations. 'DisplayInfo' A logical scalar specifying whether to print information during training. The default is false. The supported values for 'ResponseTransform' are: Value Description 'none' x (no transformation) 'identity' x (no transformation) 'exp' exp (x) 'log' log (x) See also: fitrnet, ClassificationNeuralNetwork, fcnntrain, fcnnpredict # name: # type: sq_string # elements: 1 # length: 38 Fit a neural network regression model. # name: # type: sq_string # elements: 1 # length: 36 RegressionNeuralNetwork.ResponseName # name: # type: sq_string # elements: 1 # length: 125 RegressionNeuralNetwork: property ResponseName Name of the response variable A character vector. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 41 RegressionNeuralNetwork.ResponseTransform # name: # type: sq_string # elements: 1 # length: 309 RegressionNeuralNetwork: property ResponseTransform Transformation applied to the predicted response A function handle, applied by predict and resubPredict to the network's output. It defaults to the identity and may be set after construction, either to a handle or to the name of a supported transformation. # name: # type: sq_string # elements: 1 # length: 48 Transformation applied to the predicted response # name: # type: sq_string # elements: 1 # length: 32 RegressionNeuralNetwork.RowsUsed # name: # type: sq_string # elements: 1 # length: 395 RegressionNeuralNetwork: property RowsUsed Rows used for fitting A logical column vector with the same length as the observations in the original predictor data X, true for each row that was used for fitting the RegressionNeuralNetwork model. It is empty, [], when every observation was used, so a non-empty value means that rows holding missing values were dropped. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Rows used for fitting # name: # type: sq_string # elements: 1 # length: 29 RegressionNeuralNetwork.Sigma # name: # type: sq_string # elements: 1 # length: 227 RegressionNeuralNetwork: property Sigma Standard deviation of the predictors A row vector with one entry per predictor, used for standardization. Empty when the predictor data were not standardized. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Standard deviation of the predictors # name: # type: sq_string # elements: 1 # length: 30 RegressionNeuralNetwork.Solver # name: # type: sq_string # elements: 1 # length: 209 RegressionNeuralNetwork: property Solver Solver used to train the network A character vector, either 'Gradient Descent' for the stochastic solver or 'LBFGS' for the full-batch one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Solver used to train the network # name: # type: sq_string # elements: 1 # length: 39 RegressionNeuralNetwork.TrainingHistory # name: # type: sq_string # elements: 1 # length: 411 RegressionNeuralNetwork: property TrainingHistory Iteration by iteration record of the fit A table with the variables Iteration and TrainingLoss, one row per training iteration. This property is read-only. The columns follow the solver. Under 'sgd' they are Iteration and TrainingLoss, with TrainingAccuracy for a classifier. Under 'lbfgs' they are Iteration, TrainingLoss, Gradient and Step, as MATLAB's are. # name: # type: sq_string # elements: 1 # length: 40 Iteration by iteration record of the fit # name: # type: sq_string # elements: 1 # length: 25 RegressionNeuralNetwork.W # name: # type: sq_string # elements: 1 # length: 332 RegressionNeuralNetwork: property W Observation weights A numeric column vector with one entry per training observation, summing to one, by which the training loss weighs each observation. It defaults to a uniform weight for every observation, and has the class of the 'Weights' given, single or double. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 25 RegressionNeuralNetwork.X # name: # type: sq_string # elements: 1 # length: 387 RegressionNeuralNetwork: property X Predictor data An NxP numeric matrix, as it was supplied to the constructor, before any rows carrying missing values were dropped. This property is read-only. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 25 RegressionNeuralNetwork.Y # name: # type: sq_string # elements: 1 # length: 140 RegressionNeuralNetwork: property Y Response data An Nx1 numeric vector, as it was supplied to the constructor. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Response data # name: # type: sq_string # elements: 1 # length: 31 RegressionNeuralNetwork.compact # name: # type: sq_string # elements: 1 # length: 380 RegressionNeuralNetwork: CMdl = compact (obj) Create a CompactRegressionNeuralNetwork object. CMdl = compact (obj) returns a compact version of the RegressionNeuralNetwork object obj, which keeps the trained network but drops the training data, so it predicts identically while carrying no observations. See also: fitrnet, RegressionNeuralNetwork, CompactRegressionNeuralNetwork # name: # type: sq_string # elements: 1 # length: 47 Create a CompactRegressionNeuralNetwork object. # name: # type: sq_string # elements: 1 # length: 32 RegressionNeuralNetwork.crossval # name: # type: sq_string # elements: 1 # length: 887 RegressionNeuralNetwork: CVMdl = crossval (obj) RegressionNeuralNetwork: CVMdl = crossval (..., name, value) Cross validate a neural network regression model. CVMdl = crossval (obj) returns a RegressionPartitionedModel holding one refit of obj per fold of a ten-fold partition, or of an n-fold one where the model has fewer than ten observations. obj must be a RegressionNeuralNetwork class object. CVMdl = crossval (..., name, value) accepts one, and only one, of the following Name-Value pairs. Name Value 'KFold' An integer greater than 1, the number of folds. 'Holdout' A scalar in (0, 1), the fraction of observations held out for testing. 'Leaveout' 'on' or 'off', whether to hold out one observation at a time. 'CVPartition' A cvpartition object over as many observations as the model was trained on. See also: RegressionNeuralNetwork, RegressionPartitionedModel, cvpartition # name: # type: sq_string # elements: 1 # length: 49 Cross validate a neural network regression model. # name: # type: sq_string # elements: 1 # length: 28 RegressionNeuralNetwork.loss # name: # type: sq_string # elements: 1 # length: 1408 RegressionNeuralNetwork: L = loss (obj, X, Y) RegressionNeuralNetwork: L = loss (obj, Tbl, ResponseVarName) RegressionNeuralNetwork: L = loss (obj, Tbl) RegressionNeuralNetwork: L = loss (..., name, value) Compute the regression loss of a neural network model. L = loss (obj, X, Y) returns the weighted mean squared error between the response Y and the response the model predicts for X. obj must be a RegressionNeuralNetwork class object. X must be a numeric matrix with the same number of predictors as the data the model was trained on. Y must be a numeric vector with as many rows as X. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. L = loss (..., name, value) accepts the following Name-Value pairs. Name Value 'LossFun' 'mse', the default, or a function handle called as lossfun (Y, yFit, W) and returning a scalar. 'Weights' A numeric vector of observation weights with one entry per row of X. It defaults to a uniform weight. The weights are normalized to sum to one before the loss is formed, so scaling them all by the same factor leaves the loss unchanged. See also: RegressionNeuralNetwork, fitrnet # name: # type: sq_string # elements: 1 # length: 54 Compute the regression loss of a neural network model. # name: # type: sq_string # elements: 1 # length: 31 RegressionNeuralNetwork.predict # name: # type: sq_string # elements: 1 # length: 850 RegressionNeuralNetwork: yFit = predict (obj, XC) Predict the response for new data with a neural network regression model. yFit = predict (obj, XC) returns a column vector holding the predicted response for each row of XC, using the network stored in obj. obj must be a RegressionNeuralNetwork class object. XC must be a numeric matrix with the same number of predictors as the data the model was trained on. The transformation named by ResponseTransform is applied to the network's output before it is returned. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: RegressionNeuralNetwork, fitrnet # name: # type: sq_string # elements: 1 # length: 73 Predict the response for new data with a neural network regression model. # name: # type: sq_string # elements: 1 # length: 33 RegressionNeuralNetwork.resubLoss # name: # type: sq_string # elements: 1 # length: 420 RegressionNeuralNetwork: L = resubLoss (obj) RegressionNeuralNetwork: L = resubLoss (..., name, value) Compute the resubstitution regression loss of a neural network model. L = resubLoss (obj) returns the weighted mean squared error of the model on the data it was trained on. It accepts the same Name-Value pairs as loss. obj must be a RegressionNeuralNetwork class object. See also: RegressionNeuralNetwork, fitrnet # name: # type: sq_string # elements: 1 # length: 69 Compute the resubstitution regression loss of a neural network model. # name: # type: sq_string # elements: 1 # length: 36 RegressionNeuralNetwork.resubPredict # name: # type: sq_string # elements: 1 # length: 411 RegressionNeuralNetwork: yFit = resubPredict (obj) Predict the response of the training data with a neural network regression model. yFit = resubPredict (obj) returns a column vector holding the predicted response for every observation the model was trained on, that is the rows of obj.X selected by obj.RowsUsed. obj must be a RegressionNeuralNetwork class object. See also: RegressionNeuralNetwork, fitrnet # name: # type: sq_string # elements: 1 # length: 81 Predict the response of the training data with a neural network regression model. # name: # type: sq_string # elements: 1 # length: 33 RegressionNeuralNetwork.savemodel # name: # type: sq_string # elements: 1 # length: 321 RegressionNeuralNetwork: savemodel (obj, filename) Save a neural network regression model to a file. savemodel (obj, filename) saves every property of the RegressionNeuralNetwork object obj into filename in binary format, so that it can be read back with loadmodel. See also: loadmodel, RegressionNeuralNetwork, fitrnet # name: # type: sq_string # elements: 1 # length: 49 Save a neural network regression model to a file. # name: # type: sq_string # elements: 1 # length: 29 RegressionPartitionedEnsemble # name: # type: sq_string # elements: 1 # length: 547 statistics: RegressionPartitionedEnsemble Cross-validated ensemble of regression trees A RegressionPartitionedEnsemble object holds one ensemble per fold of a partition, each fitted on the observations the fold keeps for training, and predicts every observation with the ensemble of the fold that held it out. Create one with fitrensemble given a cross-validation option, or with the crossval method of a RegressionEnsemble or RegressionBaggedEnsemble. See also: fitrensemble, RegressionEnsemble.crossval, CompactRegressionEnsemble, cvpartition # name: # type: sq_string # elements: 1 # length: 44 Cross-validated ensemble of regression trees # name: # type: sq_string # elements: 1 # length: 38 RegressionPartitionedEnsemble.BinEdges # name: # type: sq_string # elements: 1 # length: 119 RegressionPartitionedEnsemble: property BinEdges Bin edges of the predictors Always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Bin edges of the predictors # name: # type: sq_string # elements: 1 # length: 51 RegressionPartitionedEnsemble.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 138 RegressionPartitionedEnsemble: property CategoricalPredictors Indices of categorical predictors Always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Indices of categorical predictors # name: # type: sq_string # elements: 1 # length: 49 RegressionPartitionedEnsemble.CrossValidatedModel # name: # type: sq_string # elements: 1 # length: 147 RegressionPartitionedEnsemble: property CrossValidatedModel Method of the cross-validated ensemble 'LSBoost' or 'Bag'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Method of the cross-validated ensemble # name: # type: sq_string # elements: 1 # length: 35 RegressionPartitionedEnsemble.KFold # name: # type: sq_string # elements: 1 # length: 90 RegressionPartitionedEnsemble: property KFold Number of folds This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Number of folds # name: # type: sq_string # elements: 1 # length: 45 RegressionPartitionedEnsemble.ModelParameters # name: # type: sq_string # elements: 1 # length: 297 RegressionPartitionedEnsemble: property ModelParameters Parameters of the cross-validation A structure with the fields Type, Method, 'PartitionedEnsemble', LearnerTemplates, the ModelParameters of the ensemble the folds were fitted as, and NLearn, the number of folds. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Parameters of the cross-validation # name: # type: sq_string # elements: 1 # length: 45 RegressionPartitionedEnsemble.NumObservations # name: # type: sq_string # elements: 1 # length: 107 RegressionPartitionedEnsemble: property NumObservations Number of observations This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 47 RegressionPartitionedEnsemble.NumTrainedPerFold # name: # type: sq_string # elements: 1 # length: 129 RegressionPartitionedEnsemble: property NumTrainedPerFold Number of trees in each fold A row vector. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Number of trees in each fold # name: # type: sq_string # elements: 1 # length: 39 RegressionPartitionedEnsemble.Partition # name: # type: sq_string # elements: 1 # length: 134 RegressionPartitionedEnsemble: property Partition The partition of the observations A cvpartition object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 The partition of the observations # name: # type: sq_string # elements: 1 # length: 44 RegressionPartitionedEnsemble.PredictorNames # name: # type: sq_string # elements: 1 # length: 107 RegressionPartitionedEnsemble: property PredictorNames Names of the predictors This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 59 RegressionPartitionedEnsemble.RegressionPartitionedEnsemble # name: # type: sq_string # elements: 1 # length: 265 RegressionPartitionedEnsemble: CVMdl = RegressionPartitionedEnsemble (Mdl, Partition) Cross-validate a regression ensemble. Mdl is the ensemble to cross-validate and Partition a cvpartition of its observations. The documented ways in are fitrensemble and crossval. # name: # type: sq_string # elements: 1 # length: 37 Cross-validate a regression ensemble. # name: # type: sq_string # elements: 1 # length: 42 RegressionPartitionedEnsemble.ResponseName # name: # type: sq_string # elements: 1 # length: 111 RegressionPartitionedEnsemble: property ResponseName Name of the response variable This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 47 RegressionPartitionedEnsemble.ResponseTransform # name: # type: sq_string # elements: 1 # length: 257 RegressionPartitionedEnsemble: property ResponseTransform Transform applied to the out-of-fold predictions The folds carry none; this one is applied once to what they predict. MATLAB R2024a leaves the transform on the folds as well and so applies it twice. # name: # type: sq_string # elements: 1 # length: 48 Transform applied to the out-of-fold predictions # name: # type: sq_string # elements: 1 # length: 39 RegressionPartitionedEnsemble.Trainable # name: # type: sq_string # elements: 1 # length: 215 RegressionPartitionedEnsemble: property Trainable The full ensemble of each fold A column cell array with one RegressionEnsemble or RegressionBaggedEnsemble per fold, which resume grows. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 The full ensemble of each fold # name: # type: sq_string # elements: 1 # length: 37 RegressionPartitionedEnsemble.Trained # name: # type: sq_string # elements: 1 # length: 175 RegressionPartitionedEnsemble: property Trained The compact ensemble of each fold A column cell array with one CompactRegressionEnsemble per fold. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 The compact ensemble of each fold # name: # type: sq_string # elements: 1 # length: 31 RegressionPartitionedEnsemble.W # name: # type: sq_string # elements: 1 # length: 197 RegressionPartitionedEnsemble: property W Observation weights The weights of the ensemble that was cross-validated, by which the losses weigh the held-out observations. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 31 RegressionPartitionedEnsemble.X # name: # type: sq_string # elements: 1 # length: 277 RegressionPartitionedEnsemble: property X Predictor data Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 31 RegressionPartitionedEnsemble.Y # name: # type: sq_string # elements: 1 # length: 84 RegressionPartitionedEnsemble: property Y Response data This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Response data # name: # type: sq_string # elements: 1 # length: 39 RegressionPartitionedEnsemble.kfoldLoss # name: # type: sq_string # elements: 1 # length: 762 RegressionPartitionedEnsemble: L = kfoldLoss (obj) RegressionPartitionedEnsemble: L = kfoldLoss (..., name, value) Cross-validated regression loss. L is the weighted mean squared error of the out-of-fold predictions, the held-out observations weighted by W. Name-Value arguments: Name Value 'Folds' The folds to use, pooled. The default is all of them. 'LossFun' 'mse' (default) or a function handle called as lossfun (Y, Yfit, W) with normalized weights. 'Mode' 'average' (default) for one loss over the observations of every fold used, 'individual' for a column with the loss of each fold, or 'cumulative' for a column whose element t uses the first t trees of every fold. See also: RegressionPartitionedEnsemble, RegressionPartitionedEnsemble.kfoldPredict # name: # type: sq_string # elements: 1 # length: 32 Cross-validated regression loss. # name: # type: sq_string # elements: 1 # length: 42 RegressionPartitionedEnsemble.kfoldPredict # name: # type: sq_string # elements: 1 # length: 355 RegressionPartitionedEnsemble: yfit = kfoldPredict (obj) Predict each observation with the fold that held it out. yfit holds each observation's prediction from the ensemble of the fold that held it out, after ResponseTransform, and NaN for an observation no fold held out. See also: RegressionPartitionedEnsemble, RegressionPartitionedEnsemble.kfoldLoss # name: # type: sq_string # elements: 1 # length: 56 Predict each observation with the fold that held it out. # name: # type: sq_string # elements: 1 # length: 38 RegressionPartitionedEnsemble.kfoldfun # name: # type: sq_string # elements: 1 # length: 328 RegressionPartitionedEnsemble: vals = kfoldfun (obj, fun) Apply a function to each fold. fun is called once per fold as fun (CMP, Xtrain, Ytrain, Wtrain, Xtest, Ytest, Wtest), CMP being the fold's compact ensemble and the weights those of W, and must return a row. vals stacks the rows. See also: RegressionPartitionedEnsemble # name: # type: sq_string # elements: 1 # length: 30 Apply a function to each fold. # name: # type: sq_string # elements: 1 # length: 36 RegressionPartitionedEnsemble.resume # name: # type: sq_string # elements: 1 # length: 331 RegressionPartitionedEnsemble: CVMdl = resume (obj, NumLearningCycles) RegressionPartitionedEnsemble: CVMdl = resume (..., 'NPrint', n) Grow more trees in every fold. Each fold's ensemble is resumed, as RegressionEnsemble.resume does, by NumLearningCycles trees. See also: RegressionPartitionedEnsemble, RegressionEnsemble.resume # name: # type: sq_string # elements: 1 # length: 30 Grow more trees in every fold. # name: # type: sq_string # elements: 1 # length: 27 RegressionPartitionedKernel # name: # type: sq_string # elements: 1 # length: 1020 statistics: RegressionPartitionedKernel Cross-validated Gaussian kernel regression model. A RegressionPartitionedKernel object holds one RegressionKernel per fold of a partition, each fitted to the observations the fold trains on. kfoldPredict predicts each observation with the fold that held it out, so what it returns is an out-of-sample prediction. A RegressionKernel stores no copy of its training data and so has no resubstitution methods and no compact form. This class is what takes their place: cross-validation is the way a kernel model is asked how it would do on data it has not seen. Every fold draws its own random basis, as it must, being its own fit. Two folds therefore approximate the same kernel through different expansions, which is a source of variation between folds over and above the data they were given. A larger 'NumExpansionDimensions' narrows it. Create one with fitrlinear and a cross-validation option, or directly. See also: fitrlinear, RegressionKernel, RegressionPartitionedKernel # name: # type: sq_string # elements: 1 # length: 49 Cross-validated Gaussian kernel regression model. # name: # type: sq_string # elements: 1 # length: 49 RegressionPartitionedKernel.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 197 RegressionPartitionedKernel: property CategoricalPredictors Indices of the categorical predictors A row vector of column indices, empty when every predictor is numeric. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 47 RegressionPartitionedKernel.CrossValidatedModel # name: # type: sq_string # elements: 1 # length: 174 RegressionPartitionedKernel: property CrossValidatedModel Name of the model that was cross-validated Always 'Linear', the short name MATLAB uses. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Name of the model that was cross-validated # name: # type: sq_string # elements: 1 # length: 33 RegressionPartitionedKernel.KFold # name: # type: sq_string # elements: 1 # length: 217 RegressionPartitionedKernel: property KFold Number of folds A positive integer scalar. A holdout partition has one fold and a leave-one-out partition has as many as there are observations. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Number of folds # name: # type: sq_string # elements: 1 # length: 43 RegressionPartitionedKernel.ModelParameters # name: # type: sq_string # elements: 1 # length: 1131 RegressionPartitionedKernel: property ModelParameters What was cross-validated, and how A structure holding the parameters the folds were fitted with, carried through from the learner that was cross validated, beside NLearn, the number of folds, and the Version, Method and Type tags of this class, with LearnerTemplates naming the backing. The learner's own tags are replaced rather than kept, so a cross-validated SVM reports Method as 'PartitionedKernel' and not 'SVM'. Deviation from MATLAB. MATLAB reports the parameter record of the cross-validation ensemble here rather than of the learner, so it says nothing at all about how the folds were fitted: of its eighteen fields only the fold count, its partitioner and a fit template carry anything, and the rest are boosting settings left inert. Nor can the parameters be reached through the folds, a compact model carrying none in MATLAB. This class reports the fit instead, which is strictly more than MATLAB offers, and everything MATLAB's record does carry is published here as the KFold, Partition, X, Y, W and CrossValidatedModel properties. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 What was cross-validated, and how # name: # type: sq_string # elements: 1 # length: 43 RegressionPartitionedKernel.NumObservations # name: # type: sq_string # elements: 1 # length: 216 RegressionPartitionedKernel: property NumObservations Number of observations the partition covers A positive integer scalar, counting the rows that survived the removal of missing values. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Number of observations the partition covers # name: # type: sq_string # elements: 1 # length: 37 RegressionPartitionedKernel.Partition # name: # type: sq_string # elements: 1 # length: 150 RegressionPartitionedKernel: property Partition The partition itself A cvpartition object over the retained observations. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 The partition itself # name: # type: sq_string # elements: 1 # length: 42 RegressionPartitionedKernel.PredictorNames # name: # type: sq_string # elements: 1 # length: 140 RegressionPartitionedKernel: property PredictorNames Names of the predictors A cell array of character vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 55 RegressionPartitionedKernel.RegressionPartitionedKernel # name: # type: sq_string # elements: 1 # length: 942 RegressionPartitionedKernel: obj = RegressionPartitionedKernel (X, Y) RegressionPartitionedKernel: obj = RegressionPartitionedKernel (..., name, value) Cross-validate a linear regression model. obj = RegressionPartitionedKernel (X, Y) partitions the data into ten folds and fits a RegressionKernel to each. obj = RegressionPartitionedKernel (..., name, value) takes one of 'KFold', 'Holdout', 'Leaveout' and 'CVPartition' to say how to partition, and any option RegressionKernel takes to say how to fit. 'CrossVal' is accepted and has no effect here, this class being cross-validated by construction. Anything left as 'auto' is resolved by each fold against its own training rows rather than once over the whole data, so ten folds of a hundred observations each get a Lambda of one ninetieth rather than one hundredth, and each its own Epsilon and KernelScale. Both are MATLAB's behaviour, measured. See also: fitrlinear, RegressionKernel # name: # type: sq_string # elements: 1 # length: 41 Cross-validate a linear regression model. # name: # type: sq_string # elements: 1 # length: 40 RegressionPartitionedKernel.ResponseName # name: # type: sq_string # elements: 1 # length: 139 RegressionPartitionedKernel: property ResponseName Name of the response A character vector, defaulting to 'Y'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Name of the response # name: # type: sq_string # elements: 1 # length: 45 RegressionPartitionedKernel.ResponseTransform # name: # type: sq_string # elements: 1 # length: 331 RegressionPartitionedKernel: property ResponseTransform Transformation applied to the predicted response A character vector, or the text of the function handle that was supplied, which may be assigned after the model is built. The fold models carry no transform of their own; this one is applied once to the assembled predictions. # name: # type: sq_string # elements: 1 # length: 48 Transformation applied to the predicted response # name: # type: sq_string # elements: 1 # length: 35 RegressionPartitionedKernel.Trained # name: # type: sq_string # elements: 1 # length: 207 RegressionPartitionedKernel: property Trained The models fitted to the folds A cell column with one RegressionKernel per fold, each fitted to the observations its fold trains on. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 The models fitted to the folds # name: # type: sq_string # elements: 1 # length: 29 RegressionPartitionedKernel.W # name: # type: sq_string # elements: 1 # length: 126 RegressionPartitionedKernel: property W Observation weights An Nx1 numeric vector summing to one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 29 RegressionPartitionedKernel.Y # name: # type: sq_string # elements: 1 # length: 129 RegressionPartitionedKernel: property Y Response of the retained observations An Nx1 numeric vector. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Response of the retained observations # name: # type: sq_string # elements: 1 # length: 37 RegressionPartitionedKernel.kfoldLoss # name: # type: sq_string # elements: 1 # length: 433 RegressionPartitionedKernel: l = kfoldLoss (obj) RegressionPartitionedKernel: l = kfoldLoss (..., name, value) Out-of-fold regression loss. l = kfoldLoss (obj) returns the out-of-fold mean squared error. l = kfoldLoss (..., name, value) takes 'LossFun', either 'mse' or 'epsiloninsensitive'; 'Folds', a subset of the folds to average over; and 'Mode', either 'average', the default, or 'individual', which returns one row per fold. # name: # type: sq_string # elements: 1 # length: 28 Out-of-fold regression loss. # name: # type: sq_string # elements: 1 # length: 40 RegressionPartitionedKernel.kfoldPredict # name: # type: sq_string # elements: 1 # length: 302 RegressionPartitionedKernel: yFit = kfoldPredict (obj) Out-of-fold prediction for every observation. Each observation is predicted by the fold that held it out, so the predictions are out-of-sample. An observation that no fold held out, which under a holdout partition is most of them, comes back NaN. # name: # type: sq_string # elements: 1 # length: 45 Out-of-fold prediction for every observation. # name: # type: sq_string # elements: 1 # length: 27 RegressionPartitionedLinear # name: # type: sq_string # elements: 1 # length: 881 statistics: RegressionPartitionedLinear Cross-validated linear regression model. A RegressionPartitionedLinear object holds one RegressionLinear per fold of a partition, each fitted to the observations the fold trains on. kfoldPredict predicts each observation with the fold that held it out, so what it returns is an out-of-sample prediction. A RegressionLinear stores no copy of its training data and so has no resubstitution methods and no compact form. This class is what takes their place: cross-validation is the way a linear model is asked how it would do on data it has not seen. When the fold models carry a whole regularization path, both methods return one column per strength, in the order of the 'Lambda' that was asked for. Create one with fitrlinear and a cross-validation option, or directly. See also: fitrlinear, RegressionLinear, RegressionPartitionedKernel # name: # type: sq_string # elements: 1 # length: 40 Cross-validated linear regression model. # name: # type: sq_string # elements: 1 # length: 49 RegressionPartitionedLinear.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 197 RegressionPartitionedLinear: property CategoricalPredictors Indices of the categorical predictors A row vector of column indices, empty when every predictor is numeric. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 47 RegressionPartitionedLinear.CrossValidatedModel # name: # type: sq_string # elements: 1 # length: 174 RegressionPartitionedLinear: property CrossValidatedModel Name of the model that was cross-validated Always 'Linear', the short name MATLAB uses. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Name of the model that was cross-validated # name: # type: sq_string # elements: 1 # length: 33 RegressionPartitionedLinear.KFold # name: # type: sq_string # elements: 1 # length: 217 RegressionPartitionedLinear: property KFold Number of folds A positive integer scalar. A holdout partition has one fold and a leave-one-out partition has as many as there are observations. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Number of folds # name: # type: sq_string # elements: 1 # length: 43 RegressionPartitionedLinear.ModelParameters # name: # type: sq_string # elements: 1 # length: 1131 RegressionPartitionedLinear: property ModelParameters What was cross-validated, and how A structure holding the parameters the folds were fitted with, carried through from the learner that was cross validated, beside NLearn, the number of folds, and the Version, Method and Type tags of this class, with LearnerTemplates naming the backing. The learner's own tags are replaced rather than kept, so a cross-validated SVM reports Method as 'PartitionedLinear' and not 'SVM'. Deviation from MATLAB. MATLAB reports the parameter record of the cross-validation ensemble here rather than of the learner, so it says nothing at all about how the folds were fitted: of its eighteen fields only the fold count, its partitioner and a fit template carry anything, and the rest are boosting settings left inert. Nor can the parameters be reached through the folds, a compact model carrying none in MATLAB. This class reports the fit instead, which is strictly more than MATLAB offers, and everything MATLAB's record does carry is published here as the KFold, Partition, X, Y, W and CrossValidatedModel properties. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 What was cross-validated, and how # name: # type: sq_string # elements: 1 # length: 43 RegressionPartitionedLinear.NumObservations # name: # type: sq_string # elements: 1 # length: 216 RegressionPartitionedLinear: property NumObservations Number of observations the partition covers A positive integer scalar, counting the rows that survived the removal of missing values. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 Number of observations the partition covers # name: # type: sq_string # elements: 1 # length: 37 RegressionPartitionedLinear.Partition # name: # type: sq_string # elements: 1 # length: 150 RegressionPartitionedLinear: property Partition The partition itself A cvpartition object over the retained observations. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 The partition itself # name: # type: sq_string # elements: 1 # length: 42 RegressionPartitionedLinear.PredictorNames # name: # type: sq_string # elements: 1 # length: 140 RegressionPartitionedLinear: property PredictorNames Names of the predictors A cell array of character vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 55 RegressionPartitionedLinear.RegressionPartitionedLinear # name: # type: sq_string # elements: 1 # length: 926 RegressionPartitionedLinear: obj = RegressionPartitionedLinear (X, Y) RegressionPartitionedLinear: obj = RegressionPartitionedLinear (..., name, value) Cross-validate a linear regression model. obj = RegressionPartitionedLinear (X, Y) partitions the data into ten folds and fits a RegressionLinear to each. obj = RegressionPartitionedLinear (..., name, value) takes one of 'KFold', 'Holdout', 'Leaveout' and 'CVPartition' to say how to partition, and any option RegressionLinear takes to say how to fit. 'CrossVal' is accepted and has no effect here, this class being cross-validated by construction. Anything left as 'auto' is resolved by each fold against its own training rows rather than once over the whole data, so ten folds of a hundred observations each get a Lambda of one ninetieth rather than one hundredth, and each its own Epsilon. Both are MATLAB's behaviour, measured. See also: fitrlinear, RegressionLinear # name: # type: sq_string # elements: 1 # length: 41 Cross-validate a linear regression model. # name: # type: sq_string # elements: 1 # length: 40 RegressionPartitionedLinear.ResponseName # name: # type: sq_string # elements: 1 # length: 139 RegressionPartitionedLinear: property ResponseName Name of the response A character vector, defaulting to 'Y'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Name of the response # name: # type: sq_string # elements: 1 # length: 45 RegressionPartitionedLinear.ResponseTransform # name: # type: sq_string # elements: 1 # length: 331 RegressionPartitionedLinear: property ResponseTransform Transformation applied to the predicted response A character vector, or the text of the function handle that was supplied, which may be assigned after the model is built. The fold models carry no transform of their own; this one is applied once to the assembled predictions. # name: # type: sq_string # elements: 1 # length: 48 Transformation applied to the predicted response # name: # type: sq_string # elements: 1 # length: 35 RegressionPartitionedLinear.Trained # name: # type: sq_string # elements: 1 # length: 207 RegressionPartitionedLinear: property Trained The models fitted to the folds A cell column with one RegressionLinear per fold, each fitted to the observations its fold trains on. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 The models fitted to the folds # name: # type: sq_string # elements: 1 # length: 29 RegressionPartitionedLinear.W # name: # type: sq_string # elements: 1 # length: 126 RegressionPartitionedLinear: property W Observation weights An Nx1 numeric vector summing to one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 29 RegressionPartitionedLinear.Y # name: # type: sq_string # elements: 1 # length: 129 RegressionPartitionedLinear: property Y Response of the retained observations An Nx1 numeric vector. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Response of the retained observations # name: # type: sq_string # elements: 1 # length: 37 RegressionPartitionedLinear.kfoldLoss # name: # type: sq_string # elements: 1 # length: 433 RegressionPartitionedLinear: l = kfoldLoss (obj) RegressionPartitionedLinear: l = kfoldLoss (..., name, value) Out-of-fold regression loss. l = kfoldLoss (obj) returns the out-of-fold mean squared error. l = kfoldLoss (..., name, value) takes 'LossFun', either 'mse' or 'epsiloninsensitive'; 'Folds', a subset of the folds to average over; and 'Mode', either 'average', the default, or 'individual', which returns one row per fold. # name: # type: sq_string # elements: 1 # length: 28 Out-of-fold regression loss. # name: # type: sq_string # elements: 1 # length: 40 RegressionPartitionedLinear.kfoldPredict # name: # type: sq_string # elements: 1 # length: 369 RegressionPartitionedLinear: yFit = kfoldPredict (obj) Out-of-fold prediction for every observation. Each observation is predicted by the fold that held it out, so the predictions are out-of-sample. An observation that no fold held out, which under a holdout partition is most of them, comes back NaN. With L regularization strengths yFit has one column per strength. # name: # type: sq_string # elements: 1 # length: 45 Out-of-fold prediction for every observation. # name: # type: sq_string # elements: 1 # length: 26 RegressionPartitionedModel # name: # type: sq_string # elements: 1 # length: 571 statistics: RegressionPartitionedModel Cross-validated regression model. A RegressionPartitionedModel object holds a regression model cross validated over a partition of its training data: one compact model per fold, in Trained, each fitted without the observations that fold tests on. kfoldPredict, kfoldLoss and kfoldfun answer for every observation with a model that never saw it. Create a RegressionPartitionedModel object with the crossval method of a regression model or with the class constructor. See also: crossval, cvpartition, ClassificationPartitionedModel # name: # type: sq_string # elements: 1 # length: 33 Cross-validated regression model. # name: # type: sq_string # elements: 1 # length: 35 RegressionPartitionedModel.BinEdges # name: # type: sq_string # elements: 1 # length: 528 RegressionPartitionedModel: property BinEdges Bin edges of the predictors A cell array with one entry per predictor, holding that predictor's bin edges where the learner discretized it before fitting. It is carried over from the model that was cross validated, and is empty whenever that model did no binning, which is every learner this package implements: MATLAB fills it only for its generalized additive model, which bins because it is built from boosted trees where ours is built from splines. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Bin edges of the predictors # name: # type: sq_string # elements: 1 # length: 48 RegressionPartitionedModel.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 185 RegressionPartitionedModel: property CategoricalPredictors Indices of the categorical predictors A numeric vector of column indices, and empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 46 RegressionPartitionedModel.CrossValidatedModel # name: # type: sq_string # elements: 1 # length: 354 RegressionPartitionedModel: property CrossValidatedModel Name of the cross-validated model A character vector holding the short name of the learner that was cross validated, as MATLAB reports it: 'GAM', 'GP', 'NeuralNetwork' or 'SVM'. It is not the class name of that learner, and the classification side uses the same names. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Name of the cross-validated model # name: # type: sq_string # elements: 1 # length: 49 RegressionPartitionedModel.IsStandardDeviationFit # name: # type: sq_string # elements: 1 # length: 815 RegressionPartitionedModel: property IsStandardDeviationFit Whether the folds fitted a standard deviation model A logical scalar for a generalized additive model backing, taken from the model that was cross validated, and empty for every other backing. MATLAB carries this on RegressionPartitionedGAM, one of five per-learner partitioned classes this package deliberately does not have (see crossval). With one class serving every backing the property has to be declared for all of them, so it is empty where it does not apply. It is placed last rather than first, where MATLAB's subclass shows it, because that subclass also moves ResponseTransform to the end and no single order can match both of MATLAB's classes; matching the general one and appending is the only coherent choice. This property is read-only. # name: # type: sq_string # elements: 1 # length: 51 Whether the folds fitted a standard deviation model # name: # type: sq_string # elements: 1 # length: 32 RegressionPartitionedModel.KFold # name: # type: sq_string # elements: 1 # length: 114 RegressionPartitionedModel: property KFold Number of folds A positive integer scalar. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Number of folds # name: # type: sq_string # elements: 1 # length: 42 RegressionPartitionedModel.ModelParameters # name: # type: sq_string # elements: 1 # length: 1091 RegressionPartitionedModel: property ModelParameters Parameters the folds were fitted with A structure holding the parameters the folds were fitted with, carried through from the learner that was cross validated, beside NLearn, the number of folds, and the Version, Method and Type tags of this class. The learner's own tags are replaced rather than kept, so a cross-validated SVM reports Method as 'PartitionedModel' and not 'SVM'. Deviation from MATLAB. MATLAB reports the parameter record of the cross-validation ensemble here rather than of the learner, so it says nothing at all about how the folds were fitted: of its eighteen fields only the fold count, its partitioner and a fit template carry anything, and the rest are boosting settings left inert. Nor can the parameters be reached through the folds, a compact model carrying none in MATLAB. This class reports the fit instead, which is strictly more than MATLAB offers, and everything MATLAB's record does carry is published here as the KFold, Partition, X, Y, W and CrossValidatedModel properties. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Parameters the folds were fitted with # name: # type: sq_string # elements: 1 # length: 42 RegressionPartitionedModel.NumObservations # name: # type: sq_string # elements: 1 # length: 131 RegressionPartitionedModel: property NumObservations Number of observations A positive integer scalar. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 44 RegressionPartitionedModel.NumTrainedPerFold # name: # type: sq_string # elements: 1 # length: 721 RegressionPartitionedModel: property NumTrainedPerFold How many trees each fold fitted A scalar structure with fields PredictorTrees and InteractionTrees, each a row with one entry per fold, for a generalized additive model backing, and empty for every other. It reports what each fold actually fitted, which the budget in ModelParameters does not: a phase stops early when it can no longer improve the fit, and the folds need not stop at the same place. MATLAB carries this on its per-learner partitioned GAM classes, which this package deliberately does not have (see crossval), so like IsStandardDeviationFit it is declared here for every backing and left empty where it does not apply. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 How many trees each fold fitted # name: # type: sq_string # elements: 1 # length: 36 RegressionPartitionedModel.Partition # name: # type: sq_string # elements: 1 # length: 131 RegressionPartitionedModel: property Partition The partition the folds came from A cvpartition object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 The partition the folds came from # name: # type: sq_string # elements: 1 # length: 41 RegressionPartitionedModel.PredictorNames # name: # type: sq_string # elements: 1 # length: 139 RegressionPartitionedModel: property PredictorNames Names of the predictors A cell array of character vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 53 RegressionPartitionedModel.RegressionPartitionedModel # name: # type: sq_string # elements: 1 # length: 1009 RegressionPartitionedModel: obj = RegressionPartitionedModel (Mdl, Partition) Cross-validate a regression model over a partition of its training data. obj = RegressionPartitionedModel (Mdl, Partition) refits Mdl once per fold of Partition, each time on the observations that fold holds out of its test set, and stores the compact form of every fit in Trained. It is normally reached through crossval (Mdl) rather than called directly. Mdl must be a RegressionGAM, a RegressionGP, a RegressionNeuralNetwork, a RegressionSVM or a RegressionTree object. Partition must be a cvpartition object over as many observations as Mdl was trained on. Every observation is held out by exactly one fold under k-fold or leave-one-out partitioning, so kfoldPredict can answer for it with a model that never saw it. Under a holdout partition only the test set is answered for, and the rest come back NaN. See also: crossval, cvpartition, RegressionGAM, RegressionGP, RegressionNeuralNetwork, RegressionSVM, RegressionTree # name: # type: sq_string # elements: 1 # length: 72 Cross-validate a regression model over a partition of its training data. # name: # type: sq_string # elements: 1 # length: 39 RegressionPartitionedModel.ResponseName # name: # type: sq_string # elements: 1 # length: 128 RegressionPartitionedModel: property ResponseName Name of the response variable A character vector. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 44 RegressionPartitionedModel.ResponseTransform # name: # type: sq_string # elements: 1 # length: 205 RegressionPartitionedModel: property ResponseTransform Transformation applied to the predicted response A function handle, carried over from the model that was cross validated. This property is read-only. # name: # type: sq_string # elements: 1 # length: 48 Transformation applied to the predicted response # name: # type: sq_string # elements: 1 # length: 34 RegressionPartitionedModel.Trained # name: # type: sq_string # elements: 1 # length: 218 RegressionPartitionedModel: property Trained The models fitted to each fold A cell array with one compact model per fold, each fitted on the observations its fold holds out of the test set. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 The models fitted to each fold # name: # type: sq_string # elements: 1 # length: 28 RegressionPartitionedModel.W # name: # type: sq_string # elements: 1 # length: 143 RegressionPartitionedModel: property W Observation weights A numeric column vector with one entry per observation. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 28 RegressionPartitionedModel.X # name: # type: sq_string # elements: 1 # length: 392 RegressionPartitionedModel: property X Predictor data A numeric matrix holding the observations the model was trained on, the rows carrying missing values already removed. This property is read-only. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 28 RegressionPartitionedModel.Y # name: # type: sq_string # elements: 1 # length: 134 RegressionPartitionedModel: property Y Response data A numeric column vector with one entry per row of X. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Response data # name: # type: sq_string # elements: 1 # length: 36 RegressionPartitionedModel.kfoldLoss # name: # type: sq_string # elements: 1 # length: 1042 RegressionPartitionedModel: L = kfoldLoss (obj) RegressionPartitionedModel: L = kfoldLoss (..., name, value) Compute the cross-validated regression loss. L = kfoldLoss (obj) returns the weighted mean squared error between the response and the out-of-fold predictions of kfoldPredict, over every observation some fold tests. obj must be a RegressionPartitionedModel class object. L = kfoldLoss (..., name, value) accepts the following Name-Value pairs. Name Value 'LossFun' 'mse', the default, 'epsiloninsensitive', or a function handle called as lossfun (Y, yFit, W) returning a scalar. The epsilon-insensitive loss belongs to a support vector model and is refused for any other, there being no tube to measure against. 'Mode' 'average', the default, which returns one number over the observations of every fold asked for, or 'individual', which returns one number per fold. 'Folds' A vector of fold indices to restrict the loss to. It defaults to every fold. See also: RegressionPartitionedModel, RegressionPartitionedModel.kfoldPredict # name: # type: sq_string # elements: 1 # length: 44 Compute the cross-validated regression loss. # name: # type: sq_string # elements: 1 # length: 39 RegressionPartitionedModel.kfoldPredict # name: # type: sq_string # elements: 1 # length: 1405 RegressionPartitionedModel: yFit = kfoldPredict (obj) RegressionPartitionedModel: [yFit, ySD, yInt] = kfoldPredict (obj) RegressionPartitionedModel: [...] = kfoldPredict (..., 'Alpha', alpha) Predict the response of every observation from the fold that held it out. yFit = kfoldPredict (obj) returns a column vector with one entry per observation, each predicted by the fold's model that did not see it during training. An observation no fold tests, which a holdout partition leaves outside its test set, comes back NaN. obj must be a RegressionPartitionedModel class object. [yFit, ySD, yInt] = kfoldPredict (obj) also returns the standard deviation ySD of each predicted response and the two-column matrix yInt of prediction intervals, each answered for by the fold that held the observation out. A RegressionGP backing is the only one that fits the uncertainty its predictions carry, so any other raises here. An untested observation is NaN in all three. [...] = kfoldPredict (..., 'Alpha', alpha) sets the significance level of the prediction intervals, which default to 95 per cent at an alpha of 0.05. ySD does not follow ResponseTransform and the other two outputs do, the same rule RegressionGP.predict applies: a predicted response and an interval endpoint are on the response scale and a standard deviation is not. See also: RegressionPartitionedModel, RegressionPartitionedModel.kfoldLoss # name: # type: sq_string # elements: 1 # length: 73 Predict the response of every observation from the fold that held it out. # name: # type: sq_string # elements: 1 # length: 35 RegressionPartitionedModel.kfoldfun # name: # type: sq_string # elements: 1 # length: 797 RegressionPartitionedModel: vals = kfoldfun (obj, fun) Apply a function to each fold of a cross-validated model. vals = kfoldfun (obj, fun) calls fun once per fold and returns a KxM numeric matrix whose row k is what fun returned for fold k. fun is a function handle taking seven inputs and returning a numeric vector of the same length every time it is called: testvals = fun (M, Xtrain, Ytrain, ... Wtrain, Xtest, Ytest, Wtest) M is the model the fold was fitted with, taken from obj.Trained{k}; Xtrain, Ytrain and Wtrain are the predictors, response and weights of the observations that fold was trained on, and Xtest, Ytest and Wtest those of the observations it held out. See also: RegressionPartitionedModel, RegressionPartitionedModel.kfoldPredict, RegressionPartitionedModel.kfoldLoss # name: # type: sq_string # elements: 1 # length: 57 Apply a function to each fold of a cross-validated model. # name: # type: sq_string # elements: 1 # length: 13 RegressionSVM # name: # type: sq_string # elements: 1 # length: 760 statistics: RegressionSVM Support vector machine regression model. A RegressionSVM object holds a support vector machine fitted to a continuous response by epsilon-insensitive regression, and predicts the response for new data with the predict method. Errors smaller than Epsilon cost nothing, so only the observations outside that tube become support vectors, and a prediction is a weighted sum of kernel evaluations against them. The fit is carried out by LIBSVM. The object keeps its training data, which resubPredict, resubLoss and crossval work on; compact drops it and returns a CompactRegressionSVM, which still predicts. Create a RegressionSVM object with fitrsvm or the class constructor. See also: fitrsvm, CompactRegressionSVM, ClassificationSVM # name: # type: sq_string # elements: 1 # length: 40 Support vector machine regression model. # name: # type: sq_string # elements: 1 # length: 19 RegressionSVM.Alpha # name: # type: sq_string # elements: 1 # length: 411 RegressionSVM: property Alpha Dual coefficients of the support vectors A numeric column vector with one entry per support vector, holding the difference of the two multipliers each observation carries. Unlike a classifier's, these are signed: there are no labels to take the sign into, so an observation above the tube and one below it are told apart by the sign of its coefficient. This property is read-only. # name: # type: sq_string # elements: 1 # length: 40 Dual coefficients of the support vectors # name: # type: sq_string # elements: 1 # length: 18 RegressionSVM.Beta # name: # type: sq_string # elements: 1 # length: 369 RegressionSVM: property Beta Primal coefficients, one per predictor A numeric column vector, equal to (obj.SupportVectors / s)' * obj.Alpha, where s is the kernel scale, so that a prediction is (x / s) * Beta + Bias, as in MATLAB. It exists only for a linear kernel; for any other kernel there is no primal representation and this is empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Primal coefficients, one per predictor # name: # type: sq_string # elements: 1 # length: 18 RegressionSVM.Bias # name: # type: sq_string # elements: 1 # length: 172 RegressionSVM: property Bias Intercept of the fitted function A numeric scalar. With a linear kernel the prediction is X * obj.Beta + obj.Bias. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Intercept of the fitted function # name: # type: sq_string # elements: 1 # length: 22 RegressionSVM.BinEdges # name: # type: sq_string # elements: 1 # length: 348 RegressionSVM: property BinEdges Bin edges of the predictors A cell array with one entry per predictor, holding that predictor's bin edges where the learner discretized it before fitting. It is empty here and stays empty: this learner fits the predictors as they are, and MATLAB's reports an empty cell for it as well. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Bin edges of the predictors # name: # type: sq_string # elements: 1 # length: 28 RegressionSVM.BoxConstraints # name: # type: sq_string # elements: 1 # length: 385 RegressionSVM: property BoxConstraints Box constraints A numeric column vector with one entry per observation, holding the box constraint the fit applied to it: n times BoxConstraint times the observation's weight in W, which is BoxConstraint for every observation when no weights were given. An observation missing a predictor is not fitted and holds NaN. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Box constraints # name: # type: sq_string # elements: 1 # length: 35 RegressionSVM.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 172 RegressionSVM: property CategoricalPredictors Indices of the categorical predictors A numeric vector of column indices, and empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 21 RegressionSVM.Epsilon # name: # type: sq_string # elements: 1 # length: 225 RegressionSVM: property Epsilon Half-width of the insensitive tube A non-negative scalar. An error smaller than Epsilon costs nothing, so only observations outside the tube become support vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Half-width of the insensitive tube # name: # type: sq_string # elements: 1 # length: 36 RegressionSVM.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 161 RegressionSVM: property ExpandedPredictorNames Names of the predictors as the model expanded them A cell array of character vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 50 Names of the predictors as the model expanded them # name: # type: sq_string # elements: 1 # length: 47 RegressionSVM.HyperparameterOptimizationResults # name: # type: sq_string # elements: 1 # length: 355 RegressionSVM: property HyperparameterOptimizationResults Results of the hyperparameter optimization Always empty. It is declared for MATLAB compatibility, where it holds what an automatic search over the hyperparameters found. This class fits the parameters it is given and runs no such search, so there is nothing to report. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Results of the hyperparameter optimization # name: # type: sq_string # elements: 1 # length: 29 RegressionSVM.IsSupportVector # name: # type: sq_string # elements: 1 # length: 181 RegressionSVM: property IsSupportVector Which training observations are support vectors A logical column vector with one entry per training observation. This property is read-only. # name: # type: sq_string # elements: 1 # length: 47 Which training observations are support vectors # name: # type: sq_string # elements: 1 # length: 30 RegressionSVM.KernelParameters # name: # type: sq_string # elements: 1 # length: 365 RegressionSVM: property KernelParameters Parameters of the kernel function A structure with fields Function and Scale, and Order for a polynomial kernel. Function names the kernel as MATLAB names it, so a radial basis kernel reports 'gaussian' whichever spelling was given; the kernel the fit was handed is unchanged in ModelParameters. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Parameters of the kernel function # name: # type: sq_string # elements: 1 # length: 29 RegressionSVM.ModelParameters # name: # type: sq_string # elements: 1 # length: 727 RegressionSVM: property ModelParameters Parameters the model was fitted with A structure holding the SVM formulation, the kernel and its parameters, the box constraint, Epsilon and the solver settings. The engine is LIBSVM and the record is LIBSVM's, so SVMtype names its formulation and Tolerance and Shrinking are its own controls; the parameters MathWorks reports for its SMO and ISDA solvers are absent, this class running neither. KernelPolynomialOrder belongs to the polynomial kernel alone and is empty under every other, as it is in MATLAB. Nu is reported here where MATLAB leaves it empty on a regression model, this class offering 'nu_svr' through SVMtype and the value being a real one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Parameters the model was fitted with # name: # type: sq_string # elements: 1 # length: 16 RegressionSVM.Mu # name: # type: sq_string # elements: 1 # length: 200 RegressionSVM: property Mu Mean of the predictors A row vector with one entry per predictor, used for standardization. Empty when the predictor data were not standardized. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Mean of the predictors # name: # type: sq_string # elements: 1 # length: 29 RegressionSVM.NumObservations # name: # type: sq_string # elements: 1 # length: 210 RegressionSVM: property NumObservations Number of observations used to train the model A positive integer scalar, counting only the rows that survived the removal of missing values. This property is read-only. # name: # type: sq_string # elements: 1 # length: 46 Number of observations used to train the model # name: # type: sq_string # elements: 1 # length: 27 RegressionSVM.NumPredictors # name: # type: sq_string # elements: 1 # length: 114 RegressionSVM: property NumPredictors Number of predictors A positive integer scalar. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of predictors # name: # type: sq_string # elements: 1 # length: 28 RegressionSVM.PredictorNames # name: # type: sq_string # elements: 1 # length: 126 RegressionSVM: property PredictorNames Names of the predictors A cell array of character vectors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 27 RegressionSVM.RegressionSVM # name: # type: sq_string # elements: 1 # length: 4749 RegressionSVM: obj = RegressionSVM (X, Y) RegressionSVM: obj = RegressionSVM (Tbl, ResponseVarName) RegressionSVM: obj = RegressionSVM (Tbl, formula) RegressionSVM: obj = RegressionSVM (Tbl, Y) RegressionSVM: obj = RegressionSVM (..., name, value) Fit a support vector machine regression model. obj = RegressionSVM (X, Y) returns a support vector regression model, obj, with X being the predictor data and Y the continuous response of the observations in X. X must be an NxP numeric matrix of predictor data, where rows correspond to observations and columns to features. Y must be an Nx1 numeric vector holding the response of the corresponding predictor data in X. Y must have the same number of rows as X. The model is fitted by epsilon-insensitive regression: errors smaller than Epsilon cost nothing, so only the observations outside that tube become support vectors. Epsilon defaults to iqr (Y) / 13.49, a robust estimate of a tenth of the response's standard deviation, which is what MATLAB uses. obj = RegressionSVM (..., name, value) returns a model with additional options specified by Name-Value pair arguments listed below. Name Value 'Standardize' A logical scalar specifying whether the predictor data should be centred and scaled before training. The same transformation is applied by predict. The default is false. 'CategoricalPredictors' The predictors whose values are levels, as indices, as a logical vector with one element per predictor, or as 'all'. Each is dummy coded in its place, one column of zeros and ones per level seen in training, named as in 'x1 == 2' in ExpandedPredictorNames, and the coded columns are not standardized. An observation holding a level the training data did not is predicted as a row missing a predictor, the weighted lower median of the training response. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. 'PredictorNames' A cell array of character vectors naming the predictors, in the order they appear in X. 'ResponseName' A character vector naming the response. The default is 'Y'. 'ResponseTransform' A character vector naming one of the supported transformations, or a function handle, applied to the predicted response by predict and resubPredict. The default is 'none'. 'Epsilon' A non-negative scalar, the half-width of the insensitive tube. The default is iqr (Y) / 13.49, or 0.1 where that is zero. 'BoxConstraint' A positive scalar bounding the dual coefficients, the cost of an error outside the tube. The default is iqr (Y) / 1.349 for a Gaussian kernel, or 1 where that is zero, and 1 for any other kernel. 'Weights' A nonnegative single or double vector of observation weights, one per row of X. An observation's box constraint is n times BoxConstraint times its weight, the weights scaled to sum to one; standardization uses weighted means and standard deviations, and a row of zero or missing weight is left out. The model's W keeps the class of the weights, while every computation runs in double. The default is uniform. 'KernelFunction' A character vector naming the kernel, one of 'linear', the default, 'rbf', 'gaussian', 'polynomial' or 'sigmoid'. 'PolynomialOrder' A positive integer, the order of the polynomial kernel. The default is 3. It is ignored by every other kernel. 'KernelScale' A positive scalar dividing every predictor before any kernel is applied, as MATLAB does, so that with u and v the divided predictors the kernels are u'v, exp (-||u - v||^2), (1 + u'v)^q and tanh (u'v + c), c being 'KernelOffset'. The default is 1. 'KernelOffset' A non-negative scalar, the constant c of the sigmoid kernel, which MATLAB does not have. MATLAB adds it to every element of the Gram matrix, which leaves the fitted model unchanged, so it changes no other kernel here. The default is 0. 'SVMtype' A character vector selecting the formulation, either 'eps_svr', the default, or 'nu_svr'. MATLAB fits only the epsilon form; 'nu_svr' is an Octave extension, in which Nu bounds the fraction of support vectors and Epsilon is determined by the fit rather than given. 'Nu' A scalar in (0, 1] used by 'nu_svr'. The default is 0.5. 'CacheSize' A positive scalar, the kernel cache in megabytes. The default is 1000. 'Tolerance' A non-negative scalar, the tolerance of the termination criterion. The default is 1e-6. 'Shrinking' Either 0 or 1, whether to use the shrinking heuristic. The default is 1. The supported values for 'ResponseTransform' are: Value Description 'none' x (no transformation) 'identity' x (no transformation) 'exp' exp (x) 'log' log (x) See also: fitrsvm, ClassificationSVM, RegressionNeuralNetwork # name: # type: sq_string # elements: 1 # length: 46 Fit a support vector machine regression model. # name: # type: sq_string # elements: 1 # length: 26 RegressionSVM.ResponseName # name: # type: sq_string # elements: 1 # length: 115 RegressionSVM: property ResponseName Name of the response variable A character vector. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 31 RegressionSVM.ResponseTransform # name: # type: sq_string # elements: 1 # length: 297 RegressionSVM: property ResponseTransform Transformation applied to the predicted response A function handle, applied by predict and resubPredict to the model's output. It defaults to the identity and may be set after construction, either to a handle or to the name of a supported transformation. # name: # type: sq_string # elements: 1 # length: 48 Transformation applied to the predicted response # name: # type: sq_string # elements: 1 # length: 22 RegressionSVM.RowsUsed # name: # type: sq_string # elements: 1 # length: 375 RegressionSVM: property RowsUsed Rows used for fitting A logical column vector with the same length as the observations in the original predictor data X, true for each row that was used for fitting the RegressionSVM model. It is empty, [], when every observation was used, so a non-empty value means that rows holding missing values were dropped. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Rows used for fitting # name: # type: sq_string # elements: 1 # length: 19 RegressionSVM.Sigma # name: # type: sq_string # elements: 1 # length: 217 RegressionSVM: property Sigma Standard deviation of the predictors A row vector with one entry per predictor, used for standardization. Empty when the predictor data were not standardized. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Standard deviation of the predictors # name: # type: sq_string # elements: 1 # length: 28 RegressionSVM.SupportVectors # name: # type: sq_string # elements: 1 # length: 223 RegressionSVM: property SupportVectors The support vectors themselves A numeric matrix with one row per support vector, on the scale the model was trained on, standardized where Mu is non-empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 The support vectors themselves # name: # type: sq_string # elements: 1 # length: 15 RegressionSVM.W # name: # type: sq_string # elements: 1 # length: 246 RegressionSVM: property W Observation weights A numeric column vector with one entry per training observation, normalized to sum to one, as MATLAB reports it. It has the class of the 'Weights' given, single or double. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 15 RegressionSVM.X # name: # type: sq_string # elements: 1 # length: 324 RegressionSVM: property X Predictor data An NxP numeric matrix, as it was supplied to the constructor. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 15 RegressionSVM.Y # name: # type: sq_string # elements: 1 # length: 130 RegressionSVM: property Y Response data An Nx1 numeric vector, as it was supplied to the constructor. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Response data # name: # type: sq_string # elements: 1 # length: 21 RegressionSVM.compact # name: # type: sq_string # elements: 1 # length: 353 RegressionSVM: CMdl = compact (obj) Create a CompactRegressionSVM object. CMdl = compact (obj) returns a compact version of the RegressionSVM object obj, which keeps the support vectors and their coefficients but drops the training data, so it predicts identically while carrying no observations. See also: fitrsvm, RegressionSVM, CompactRegressionSVM # name: # type: sq_string # elements: 1 # length: 37 Create a CompactRegressionSVM object. # name: # type: sq_string # elements: 1 # length: 22 RegressionSVM.crossval # name: # type: sq_string # elements: 1 # length: 847 RegressionSVM: CVMdl = crossval (obj) RegressionSVM: CVMdl = crossval (..., name, value) Cross validate a support vector regression model. CVMdl = crossval (obj) returns a RegressionPartitionedModel holding one refit of obj per fold of a ten-fold partition, or of an n-fold one where the model has fewer than ten observations. obj must be a RegressionSVM class object. CVMdl = crossval (..., name, value) accepts one, and only one, of the following Name-Value pairs. Name Value 'KFold' An integer greater than 1, the number of folds. 'Holdout' A scalar in (0, 1), the fraction of observations held out for testing. 'Leaveout' 'on' or 'off', whether to hold out one observation at a time. 'CVPartition' A cvpartition object over as many observations as the model was trained on. See also: RegressionSVM, RegressionPartitionedModel, cvpartition # name: # type: sq_string # elements: 1 # length: 49 Cross validate a support vector regression model. # name: # type: sq_string # elements: 1 # length: 35 RegressionSVM.discardSupportVectors # name: # type: sq_string # elements: 1 # length: 585 RegressionSVM: obj = discardSupportVectors (obj) Discard the support vectors of a linear SVM model. obj = discardSupportVectors (obj) empties Alpha and SupportVectors, leaving Beta and Bias to decide every prediction. A linear kernel needs nothing else, so the returned model predicts what it predicted before while carrying one vector in place of many. The kernel must be linear. Under any other the support vectors are part of the decision function and cannot be dropped. Discarding twice is not an error and changes nothing. See also: fitrsvm, RegressionSVM, CompactRegressionSVM # name: # type: sq_string # elements: 1 # length: 50 Discard the support vectors of a linear SVM model. # name: # type: sq_string # elements: 1 # length: 18 RegressionSVM.loss # name: # type: sq_string # elements: 1 # length: 1466 RegressionSVM: L = loss (obj, X, Y) RegressionSVM: L = loss (obj, Tbl, ResponseVarName) RegressionSVM: L = loss (obj, Tbl) RegressionSVM: L = loss (..., name, value) Compute the regression loss of a support vector machine model. L = loss (obj, X, Y) returns the weighted mean squared error between the response Y and the response the model predicts for X. obj must be a RegressionSVM class object. X must be a numeric matrix with the same number of predictors as the data the model was trained on. Y must be a numeric vector with as many rows as X. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. L = loss (..., name, value) accepts the following Name-Value pairs. Name Value 'LossFun' 'mse', the default, 'epsiloninsensitive', or a function handle called as lossfun (Y, yFit, W) returning a scalar. The epsilon-insensitive loss charges nothing for an error inside the tube, max (0, abs (Y - yFit) - Epsilon), which is the quantity the fit itself minimizes. 'Weights' A numeric vector of observation weights with one entry per row of X. It defaults to a uniform weight. The weights are normalized to sum to one before the loss is formed. See also: RegressionSVM, fitrsvm # name: # type: sq_string # elements: 1 # length: 62 Compute the regression loss of a support vector machine model. # name: # type: sq_string # elements: 1 # length: 21 RegressionSVM.predict # name: # type: sq_string # elements: 1 # length: 785 RegressionSVM: yFit = predict (obj, XC) Predict the response for new data with a support vector regression model. yFit = predict (obj, XC) returns a column vector holding the predicted response for each row of XC. obj must be a RegressionSVM class object. XC must be a numeric matrix with the same number of predictors as the data the model was trained on. The transformation named by ResponseTransform is applied to the model's output before it is returned. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: RegressionSVM, fitrsvm # name: # type: sq_string # elements: 1 # length: 73 Predict the response for new data with a support vector regression model. # name: # type: sq_string # elements: 1 # length: 23 RegressionSVM.resubLoss # name: # type: sq_string # elements: 1 # length: 388 RegressionSVM: L = resubLoss (obj) RegressionSVM: L = resubLoss (..., name, value) Compute the resubstitution regression loss of a support vector machine model. L = resubLoss (obj) returns the weighted mean squared error of the model on the data it was trained on. It accepts the same Name-Value pairs as loss. obj must be a RegressionSVM class object. See also: RegressionSVM, fitrsvm # name: # type: sq_string # elements: 1 # length: 77 Compute the resubstitution regression loss of a support vector machine model. # name: # type: sq_string # elements: 1 # length: 26 RegressionSVM.resubPredict # name: # type: sq_string # elements: 1 # length: 329 RegressionSVM: yFit = resubPredict (obj) Predict the response of the training data with a support vector regression model. yFit = resubPredict (obj) returns a column vector holding the predicted response for every observation the model was trained on. obj must be a RegressionSVM class object. See also: RegressionSVM, fitrsvm # name: # type: sq_string # elements: 1 # length: 81 Predict the response of the training data with a support vector regression model. # name: # type: sq_string # elements: 1 # length: 23 RegressionSVM.savemodel # name: # type: sq_string # elements: 1 # length: 291 RegressionSVM: savemodel (obj, filename) Save a support vector regression model to a file. savemodel (obj, filename) saves every property of the RegressionSVM object obj into filename in binary format, so that it can be read back with loadmodel. See also: loadmodel, RegressionSVM, fitrsvm # name: # type: sq_string # elements: 1 # length: 49 Save a support vector regression model to a file. # name: # type: string # elements: 1 # length: 14 RegressionTree # name: # type: sq_string # elements: 1 # length: 2140 statistics: RegressionTree Binary decision tree for regression The RegressionTree class implements a CART binary decision tree. Growth splits each node on the single predictor and cut point that lower the squared error of the response the most, and stops when a node is too small to be a parent, has no split leaving enough observations on both sides, or already accounts for all but a tolerance of the error the root carried. The grown tree is then optionally reduced, first by merging the leaves that buy no accuracy and then by cost complexity pruning, which orders the branch nodes by how little error their subtrees remove and records that order so a subtree of any size can be recovered afterwards with prune. Create a RegressionTree object by using the fitrtree function or the class constructor. The fit is carried out by the compiled engine treetrain and predictions by treepredict, which the classification tree shares. An observation missing the predictor a node cuts on descends to neither child. It is counted in that node and in every node above it, and predict stops it there and gives it that node's answer, so a row is never sent down a branch on evidence it does not carry. Choosing a split, a predictor known for only part of a node is scored on the rows it is known for, by how much the split lowers their impurity, and that is scaled by their share of the node, as C4.5 does, so a predictor known for few rows is not preferred for that alone. A categorical predictor is split into two sets of levels by ordering the levels by their mean response, and an observation whose level a node did not see stops there, as one missing the predictor does. The lower means go left; MATLAB does not always keep that side, and equally good splits may be chosen differently, so a node's sides and numbering may differ. What this class does not do yet. Surrogate splits are not implemented, and an option asking for them is refused rather than quietly ignored. The six Surrogate properties are therefore always empty, as they are in MATLAB without surrogate splits. See also: fitrtree, ClassificationTree, treetrain, treepredict # name: # type: sq_string # elements: 1 # length: 35 Binary decision tree for regression # name: # type: sq_string # elements: 1 # length: 23 RegressionTree.BinEdges # name: # type: sq_string # elements: 1 # length: 263 RegressionTree: property BinEdges Bin edges of the predictors A cell array with one column vector of bin edges per predictor, empty unless the predictors were binned before fitting. Binning is not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Bin edges of the predictors # name: # type: sq_string # elements: 1 # length: 36 RegressionTree.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 218 RegressionTree: property CategoricalPredictors Indices of the categorical predictors A row vector of column indices into X, naming the predictors treated as categorical, empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 31 RegressionTree.CategoricalSplit # name: # type: sq_string # elements: 1 # length: 267 RegressionTree: property CategoricalSplit Categorical splits of the tree A Nx2 cell array with one row per categorical split, in node order, holding the levels sent left and the levels sent right. It is empty when no split is categorical. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 Categorical splits of the tree # name: # type: sq_string # elements: 1 # length: 23 RegressionTree.Children # name: # type: sq_string # elements: 1 # length: 196 RegressionTree: property Children Child nodes of each node A NumNodesx2 matrix naming the left and the right child of each node. A leaf carries a zero in both columns. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Child nodes of each node # name: # type: sq_string # elements: 1 # length: 28 RegressionTree.CutCategories # name: # type: sq_string # elements: 1 # length: 253 RegressionTree: property CutCategories Categories used at each branch A NumNodesx2 cell array holding, for a node that cuts a categorical predictor, the levels sent left and the levels sent right. Every other entry is empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 Categories used at each branch # name: # type: sq_string # elements: 1 # length: 23 RegressionTree.CutPoint # name: # type: sq_string # elements: 1 # length: 305 RegressionTree: property CutPoint Cut point of each node A column vector holding, for each node, the value the split compares the predictor against: an observation goes left when its value is less than the cut point and right otherwise. A leaf and a categorical cut carry NaN. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Cut point of each node # name: # type: sq_string # elements: 1 # length: 27 RegressionTree.CutPredictor # name: # type: sq_string # elements: 1 # length: 259 RegressionTree: property CutPredictor Name of the predictor each node cuts on A cell array of character vectors with one entry per node, holding the name of the predictor the node splits on and an empty character vector at a leaf. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Name of the predictor each node cuts on # name: # type: sq_string # elements: 1 # length: 32 RegressionTree.CutPredictorIndex # name: # type: sq_string # elements: 1 # length: 208 RegressionTree: property CutPredictorIndex Index of the predictor each node cuts on A column vector holding, for each node, the column of X the node splits on, and zero at a leaf. This property is read-only. # name: # type: sq_string # elements: 1 # length: 40 Index of the predictor each node cuts on # name: # type: sq_string # elements: 1 # length: 22 RegressionTree.CutType # name: # type: sq_string # elements: 1 # length: 291 RegressionTree: property CutType Type of cut at each node A cell array of character vectors holding 'continuous' at a branch node that cuts a numeric predictor at a point, 'categorical' at one that splits a set of levels, and an empty character vector at a leaf. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Type of cut at each node # name: # type: sq_string # elements: 1 # length: 37 RegressionTree.ExpandedPredictorNames # name: # type: sq_string # elements: 1 # length: 305 RegressionTree: property ExpandedPredictorNames Expanded predictor names A cell array of character vectors. It differs from PredictorNames only when a categorical predictor has been expanded into one column per level, which this class does not do, so the two are always equal. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Expanded predictor names # name: # type: sq_string # elements: 1 # length: 48 RegressionTree.HyperparameterOptimizationResults # name: # type: sq_string # elements: 1 # length: 201 RegressionTree: property HyperparameterOptimizationResults Results of a hyperparameter optimization Hyperparameter optimization is not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 40 Results of a hyperparameter optimization # name: # type: sq_string # elements: 1 # length: 27 RegressionTree.IsBranchNode # name: # type: sq_string # elements: 1 # length: 185 RegressionTree: property IsBranchNode Which nodes are branch nodes A logical column vector, true for each node that carries a split and false for each leaf. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Which nodes are branch nodes # name: # type: sq_string # elements: 1 # length: 30 RegressionTree.ModelParameters # name: # type: sq_string # elements: 1 # length: 603 RegressionTree: property ModelParameters Parameters the fit was run with A structure recording the options the tree was grown under: SplitCriterion, MinParent, MinLeaf, MaxSplits, NVarToSample, MergeLeaves, Prune, PruneCriterion, QEToler, NSurrogate, MaxCat, AlgCat, PredictorSelection, Method and Type. SplitCriterion and PruneCriterion are both 'mse', the only criterion a regression tree has, and QEToler is the tolerance growth stops at. MinParent is the value the fit used, which is max (MinParentSize, 2 * MinLeafSize) and so may exceed the 'MinParentSize' asked for. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Parameters the fit was run with # name: # type: sq_string # elements: 1 # length: 24 RegressionTree.NodeError # name: # type: sq_string # elements: 1 # length: 206 RegressionTree: property NodeError Mean squared error of each node A column vector holding, for each node, the weighted mean squared error of the response about the node's mean. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Mean squared error of each node # name: # type: sq_string # elements: 1 # length: 23 RegressionTree.NodeMean # name: # type: sq_string # elements: 1 # length: 265 RegressionTree: property NodeMean Mean response at each node A column vector holding, for each node, the weighted mean of the response over the observations that reached it. It is what predict answers for a row that comes to rest there. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Mean response at each node # name: # type: sq_string # elements: 1 # length: 30 RegressionTree.NodeProbability # name: # type: sq_string # elements: 1 # length: 249 RegressionTree: property NodeProbability Probability of reaching each node A column vector holding, for each node, the total weight of the observations that reached it, the weights being those in W. The root carries one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Probability of reaching each node # name: # type: sq_string # elements: 1 # length: 23 RegressionTree.NodeRisk # name: # type: sq_string # elements: 1 # length: 254 RegressionTree: property NodeRisk Risk of each node A column vector holding, for each node, its mean squared error weighted by the probability of reaching it, which is the squared error the node contributes to the whole tree. This property is read-only. # name: # type: sq_string # elements: 1 # length: 17 Risk of each node # name: # type: sq_string # elements: 1 # length: 23 RegressionTree.NodeSize # name: # type: sq_string # elements: 1 # length: 327 RegressionTree: property NodeSize Number of observations at each node A column vector holding how many training observations reached each node. A row missing the predictor its node cuts on is counted at that node and at none below it, so a parent's size is not in general the sum of its children's. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Number of observations at each node # name: # type: sq_string # elements: 1 # length: 23 RegressionTree.NumNodes # name: # type: sq_string # elements: 1 # length: 291 RegressionTree: property NumNodes Number of nodes in the tree A positive integer, the number of nodes the tree holds, branch nodes and leaves together. Nodes are numbered as they are created, so a parent always carries a lower number than either of its children. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Number of nodes in the tree # name: # type: sq_string # elements: 1 # length: 30 RegressionTree.NumObservations # name: # type: sq_string # elements: 1 # length: 269 RegressionTree: property NumObservations Number of observations A positive integer, the number of observations the model was fitted on. It counts the rows kept, so it is smaller than the number of rows given whenever a response was missing. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 21 RegressionTree.Parent # name: # type: sq_string # elements: 1 # length: 153 RegressionTree: property Parent Parent of each node A column vector naming the parent of each node. The root carries a zero. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Parent of each node # name: # type: sq_string # elements: 1 # length: 29 RegressionTree.PredictorNames # name: # type: sq_string # elements: 1 # length: 166 RegressionTree: property PredictorNames Names of the predictor variables A cell array of character vectors with one name per column of X. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Names of the predictor variables # name: # type: sq_string # elements: 1 # length: 25 RegressionTree.PruneAlpha # name: # type: sq_string # elements: 1 # length: 361 RegressionTree: property PruneAlpha Cost complexity parameter of each pruning level A column vector with one element per level of the pruning sequence, the first of which is zero and stands for the unpruned tree. Level L is the smallest subtree that is optimal for every complexity parameter from PruneAlpha(L+1) up to the next one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 47 Cost complexity parameter of each pruning level # name: # type: sq_string # elements: 1 # length: 24 RegressionTree.PruneList # name: # type: sq_string # elements: 1 # length: 433 RegressionTree: property PruneList Pruning level of each node A column vector holding, for each branch node, the level of the cost complexity sequence at which it stops being a branch node, and zero at a leaf. Pruning the tree to level L turns every node whose level is between one and L into a leaf. It is empty when neither 'Prune' nor 'MergeLeaves' was asked for, since no sequence was then estimated. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Pruning level of each node # name: # type: sq_string # elements: 1 # length: 29 RegressionTree.RegressionTree # name: # type: sq_string # elements: 1 # length: 2941 RegressionTree: obj = RegressionTree (X, Y) RegressionTree: obj = RegressionTree (Tbl, ResponseVarName) RegressionTree: obj = RegressionTree (Tbl, formula) RegressionTree: obj = RegressionTree (Tbl, Y) RegressionTree: obj = RegressionTree (..., name, value) Grow a binary decision tree for regression. obj = RegressionTree (X, Y) grows a tree on the NxP numeric matrix X of predictor data and the Nx1 numeric response Y, and returns it as a RegressionTree object. obj = RegressionTree (..., name, value) takes the options below. Name Value 'CategoricalPredictors' The predictors whose values are levels, as indices, as a logical vector with one element per predictor, or as 'all'. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. 'MaxNumCategories' A nonnegative integer, recorded in ModelParameters. The default is 10. A regression orders the levels by their mean response, which finds the best split whatever the number of levels. 'MaxNumSplits' A nonnegative integer, the largest number of branch nodes the tree may take. The default is one less than the number of observations, which is as many as a tree can have. 'MergeLeaves' 'on' (default) or 'off'. When on, a pair of leaves whose parent is no worse than the two of them together is merged back into that parent. 'MinLeafSize' A positive integer, the fewest observations a leaf may hold. The default is 1. A split leaving fewer than this on either side is not taken. 'MinParentSize' A positive integer, the fewest observations a node must hold to be split at all. The default is 10. The value the fit uses is max (MinParentSize, 2 * MinLeafSize), since a smaller node cannot give both children a legal leaf. 'PredictorNames' A cell array of character vectors naming the columns of X. 'Prune' 'on' (default) or 'off'. When on, the cost complexity pruning sequence is estimated and reported in PruneList and PruneAlpha. The tree returned is the unpruned one either way; prune takes a subtree out of the sequence. 'PruneCriterion' 'mse', the only criterion a regression tree has. 'QuadraticErrorTolerance' A positive scalar. A node whose squared error has fallen to this fraction of the root's is not split further. The default is 1e-6. 'ResponseName' A character vector naming the response. The default is 'Y'. 'ResponseTransform' A character vector naming a transform to apply to the predicted response, or a function handle. The default is 'none'. 'SplitCriterion' 'mse', the only criterion a regression tree has. 'Weights' A nonnegative single or double vector with one element per observation. The default is uniform. The model's W keeps the class of the weights, while every computation runs in double, so the predictions are double where MATLAB returns single. See also: fitrtree, ClassificationTree, treetrain, treepredict # name: # type: sq_string # elements: 1 # length: 43 Grow a binary decision tree for regression. # name: # type: sq_string # elements: 1 # length: 27 RegressionTree.ResponseName # name: # type: sq_string # elements: 1 # length: 136 RegressionTree: property ResponseName Name of the response variable A character vector naming the response. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 32 RegressionTree.ResponseTransform # name: # type: sq_string # elements: 1 # length: 283 RegressionTree: property ResponseTransform Transform applied to the predicted response A character vector naming the function predict applies to the response it predicts before returning it, or a function handle taking and returning an array of the same size. The default is 'none'. # name: # type: sq_string # elements: 1 # length: 43 Transform applied to the predicted response # name: # type: sq_string # elements: 1 # length: 23 RegressionTree.RowsUsed # name: # type: sq_string # elements: 1 # length: 460 RegressionTree: property RowsUsed Rows used for fitting A logical column vector with one element per row of the predictor data as it was given, true for each row used for fitting. It is empty, [], when every row was used, so a non-empty value means that rows were dropped. A row is dropped when its response is missing, when every predictor is missing, or when its weight is zero; a row missing only some of its predictors is kept. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Rows used for fitting # name: # type: sq_string # elements: 1 # length: 37 RegressionTree.SurrogateCutCategories # name: # type: sq_string # elements: 1 # length: 174 RegressionTree: property SurrogateCutCategories Categories of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Categories of the surrogate splits # name: # type: sq_string # elements: 1 # length: 31 RegressionTree.SurrogateCutFlip # name: # type: sq_string # elements: 1 # length: 173 RegressionTree: property SurrogateCutFlip Cut assignments of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Cut assignments of the surrogate splits # name: # type: sq_string # elements: 1 # length: 32 RegressionTree.SurrogateCutPoint # name: # type: sq_string # elements: 1 # length: 169 RegressionTree: property SurrogateCutPoint Cut points of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Cut points of the surrogate splits # name: # type: sq_string # elements: 1 # length: 36 RegressionTree.SurrogateCutPredictor # name: # type: sq_string # elements: 1 # length: 173 RegressionTree: property SurrogateCutPredictor Predictors of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Predictors of the surrogate splits # name: # type: sq_string # elements: 1 # length: 31 RegressionTree.SurrogateCutType # name: # type: sq_string # elements: 1 # length: 163 RegressionTree: property SurrogateCutType Types of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Types of the surrogate splits # name: # type: sq_string # elements: 1 # length: 44 RegressionTree.SurrogatePredictorAssociation # name: # type: sq_string # elements: 1 # length: 205 RegressionTree: property SurrogatePredictorAssociation Predictive measures of association of the surrogate splits Surrogate splits are not implemented, so this is always empty. This property is read-only. # name: # type: sq_string # elements: 1 # length: 58 Predictive measures of association of the surrogate splits # name: # type: sq_string # elements: 1 # length: 16 RegressionTree.W # name: # type: sq_string # elements: 1 # length: 267 RegressionTree: property W Observation weights A numeric column vector of the weights the fit used, one per retained observation. They are the weights given, scaled to sum to one. It has the class of the 'Weights' given, single or double. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 16 RegressionTree.X # name: # type: sq_string # elements: 1 # length: 390 RegressionTree: property X Predictor data A numeric matrix holding the predictor data the model was fitted on. Each column is one predictor and each row one observation. This property is read-only. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 16 RegressionTree.Y # name: # type: sq_string # elements: 1 # length: 175 RegressionTree: property Y Response data A numeric column vector with one element per row of X, holding the observed response of each observation. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Response data # name: # type: sq_string # elements: 1 # length: 22 RegressionTree.compact # name: # type: sq_string # elements: 1 # length: 360 RegressionTree: CMdl = compact (obj) Drop the training data from a trained model. CMdl = compact (obj) returns a CompactRegressionTree object carrying the tree and everything predict needs, but not the observations the model was fitted on. It answers new data identically and is far smaller to keep or to ship. See also: CompactRegressionTree, RegressionTree # name: # type: sq_string # elements: 1 # length: 44 Drop the training data from a trained model. # name: # type: sq_string # elements: 1 # length: 23 RegressionTree.crossval # name: # type: sq_string # elements: 1 # length: 895 RegressionTree: CVMdl = crossval (obj) RegressionTree: CVMdl = crossval (..., name, value) Cross-validate a trained decision tree. CVMdl = crossval (obj) partitions the training data into ten folds, or into as many folds as there are observations when there are fewer than ten, grows a tree on the training part of each and returns them as a RegressionPartitionedModel. CVMdl = crossval (..., name, value) takes one of the following, and one only. Name Value 'KFold' An integer greater than 1, the number of folds. 'Holdout' A value between 0 and 1, the fraction of the data held out for testing, which gives a single fold. 'Leaveout' 'on' or 'off', one fold per observation. 'CVPartition' A cvpartition object. Every fold is grown with the growth parameters the parent was grown with and a slice of its observation weights. See also: RegressionPartitionedModel, RegressionTree, cvpartition # name: # type: sq_string # elements: 1 # length: 39 Cross-validate a trained decision tree. # name: # type: sq_string # elements: 1 # length: 21 RegressionTree.cvloss # name: # type: sq_string # elements: 1 # length: 1873 RegressionTree: E = cvloss (obj) RegressionTree: [E, SE, Nleaf, BestLevel] = cvloss (obj) RegressionTree: [...] = cvloss (..., name, value) Cross-validated loss of a tree and of its subtrees. E = cvloss (obj) partitions the training data into ten folds, grows a tree on the training part of each, and returns the mean squared error of the held-out part. [E, SE, Nleaf, BestLevel] = cvloss (...) also returns SE, the standard error of E over the folds, Nleaf, the number of leaves each subtree holds, and BestLevel, the pruning level chosen by 'TreeSize'. Each has one element per subtree asked for. [...] = cvloss (..., name, value) takes the options below. Name Value 'SubTrees' A vector of pruning levels in ascending order, or 'all' for every level of the sequence. The default is 0, the unpruned tree. 'TreeSize' 'se' (default), the smallest subtree whose loss is within one standard error of the smallest loss, or 'min', the smallest subtree of least loss. 'KFold' An integer greater than 1, the number of folds. The default is 10. A value above the number of observations is reduced to it. A fold's tree is pruned to the level its own sequence gives for the geometric mean of the parent's two neighbouring complexity parameters, which is the classical way a fold is matched to a subtree of the whole tree. The last level takes every fold's tree back to its root. The partition is drawn over the observations rather than over a response there is nothing to stratify, and the loss is weighed by the model's own weights. The standard error is not MATLAB's. This is the standard error of the loss over the folds, which is what the name means. Its value is not MATLAB's, whose formula is not recoverable from what it reports; E, Nleaf and BestLevel are measured and match. See also: RegressionTree, RegressionTree.prune, RegressionTree.crossval, RegressionTree.loss # name: # type: sq_string # elements: 1 # length: 51 Cross-validated loss of a tree and of its subtrees. # name: # type: sq_string # elements: 1 # length: 19 RegressionTree.loss # name: # type: sq_string # elements: 1 # length: 1133 RegressionTree: L = loss (obj, X, Y) RegressionTree: L = loss (obj, Tbl, ResponseVarName) RegressionTree: L = loss (obj, Tbl) RegressionTree: L = loss (..., name, value) Regression loss on new data. L = loss (obj, X, Y) returns the weighted mean squared error of the response the model predicts for X against the observed response Y. A row whose response is missing is dropped, as it is when fitting. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. loss (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and loss (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. L = loss (..., name, value) takes the following options. Name Value 'LossFun' 'mse', the default, or a function handle taking the true response, the predicted response and the weights, and returning a numeric scalar. 'Weights' A numeric vector of observation weights, one per row of X, normalized to sum to one before it is applied. See also: RegressionTree, fitrtree, RegressionTree.predict # name: # type: sq_string # elements: 1 # length: 28 Regression loss on new data. # name: # type: sq_string # elements: 1 # length: 32 RegressionTree.nodeVariableRange # name: # type: sq_string # elements: 1 # length: 477 RegressionTree: r = nodeVariableRange (obj, node) Range of each predictor at a node. r = nodeVariableRange (obj, node) returns a structure with one field per predictor the path from the root to node cuts on, holding the two-element range of values that reach the node, or for a categorical predictor the levels that reach it. A predictor the path never cuts on is unconstrained and is left out, so the root gives a structure with no fields. See also: RegressionTree, fitrtree # name: # type: sq_string # elements: 1 # length: 34 Range of each predictor at a node. # name: # type: sq_string # elements: 1 # length: 22 RegressionTree.predict # name: # type: sq_string # elements: 1 # length: 938 RegressionTree: yFit = predict (obj, XC) RegressionTree: [yFit, node] = predict (...) Predict the response with a trained RegressionTree object. yFit = predict (obj, XC) sends each row of XC down the tree and returns the mean response of the node it comes to rest at, after ResponseTransform. XC must have as many columns as the predictor data the model was fitted on. XC may also be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. [yFit, node] = predict (...) also returns the number of the node each row landed in. A row missing the predictor a node cuts on is stopped at that node and takes its answer, rather than being sent down a branch on evidence the row does not carry. See also: RegressionTree, fitrtree # name: # type: sq_string # elements: 1 # length: 58 Predict the response with a trained RegressionTree object. # name: # type: sq_string # elements: 1 # length: 34 RegressionTree.predictorImportance # name: # type: sq_string # elements: 1 # length: 680 RegressionTree: imp = predictorImportance (obj) Estimate the importance of each predictor. imp = predictorImportance (obj) returns a row vector with one element per predictor, holding the total drop in risk the splits on that predictor bring about, divided by the number of branch nodes. A predictor the tree never splits on scores zero. The drop at a branch node is its NodeRisk less the risk of its two children and less what it holds back, so a predictor that is chosen often, high up, and on nodes it separates well, scores highest. The numbers are comparable between predictors of one tree and not between trees. See also: RegressionTree, fitrtree, RegressionTree.NodeRisk # name: # type: sq_string # elements: 1 # length: 42 Estimate the importance of each predictor. # name: # type: sq_string # elements: 1 # length: 20 RegressionTree.prune # name: # type: sq_string # elements: 1 # length: 1099 RegressionTree: obj2 = prune (obj) RegressionTree: obj2 = prune (obj, 'Level', L) RegressionTree: obj2 = prune (obj, 'Alpha', A) RegressionTree: obj2 = prune (obj, 'Nodes', N) Take a subtree out of the pruning sequence. obj2 = prune (obj) returns the tree unchanged. obj2 = prune (obj, 'Level', L) turns every branch node whose PruneList level is between one and L into a leaf and discards everything below it. Level zero is the tree itself and the largest level is the root alone. A level above the largest prunes to the root and warns. obj2 = prune (obj, 'Alpha', A) prunes to the smallest subtree that is optimal for the cost complexity parameter A, which is the largest level whose PruneAlpha does not exceed it. obj2 = prune (obj, 'Nodes', N) turns the branch nodes named in N into leaves, along with everything below them, and leaves the rest of the tree alone. Pruning renumbers the nodes, so the properties of the returned tree are those of a tree of that shape and not a subset of the original's. See also: RegressionTree, fitrtree, RegressionTree.PruneList, RegressionTree.PruneAlpha # name: # type: sq_string # elements: 1 # length: 43 Take a subtree out of the pruning sequence. # name: # type: sq_string # elements: 1 # length: 24 RegressionTree.resubLoss # name: # type: sq_string # elements: 1 # length: 356 RegressionTree: L = resubLoss (obj) RegressionTree: L = resubLoss (..., name, value) Regression loss on the training data. L = resubLoss (obj) is loss over the training data, weighed as the fit weighed it, and takes the same 'LossFun' option. Giving 'Weights' weighs the training data some other way instead. See also: RegressionTree, RegressionTree.loss # name: # type: sq_string # elements: 1 # length: 37 Regression loss on the training data. # name: # type: sq_string # elements: 1 # length: 27 RegressionTree.resubPredict # name: # type: sq_string # elements: 1 # length: 202 RegressionTree: yFit = resubPredict (obj) Predict the training response with the model fitted to it. yFit = resubPredict (obj) is predict (obj, obj.X). See also: RegressionTree, RegressionTree.predict # name: # type: sq_string # elements: 1 # length: 58 Predict the training response with the model fitted to it. # name: # type: sq_string # elements: 1 # length: 24 RegressionTree.savemodel # name: # type: sq_string # elements: 1 # length: 451 RegressionTree: savemodel (obj, filename) Save a RegressionTree model to a file. savemodel (obj, filename) saves each property of a RegressionTree object into an Octave binary file, the name of which is specified in filename, along with an extra variable, which defines the type of regression object these variables constitute. Use loadmodel in order to load a regression object into Octave's workspace. See also: loadmodel, fitrtree, RegressionTree # name: # type: sq_string # elements: 1 # length: 38 Save a RegressionTree model to a file. # name: # type: sq_string # elements: 1 # length: 19 RegressionTree.view # name: # type: sq_string # elements: 1 # length: 386 RegressionTree: view (obj) Print the tree as text. view (obj) prints one line per node: a branch node names the predictor it cuts on, the cut point, and the node each side leads to, and a leaf names the response it fits. A branch node's line ends with the response it would fit itself, which is the answer an observation missing that predictor gets. See also: RegressionTree, fitrtree # name: # type: sq_string # elements: 1 # length: 23 Print the tree as text. # name: # type: sq_string # elements: 1 # length: 10 TreeBagger # name: # type: sq_string # elements: 1 # length: 808 statistics: TreeBagger Ensemble of bagged decision trees A TreeBagger object is a random forest: an ensemble of decision trees, each grown on a bootstrap sample of the training data and each choosing every split from a random subset of the predictors. The ensemble predicts by averaging its trees, their class probabilities for classification and their responses for regression. Every observation a tree's sample leaves out is out of bag for that tree, and the out-of-bag methods judge the ensemble on those observations alone, giving an estimate of its error on new data without a separate test set. Create one with the TreeBagger constructor. The compact method drops the training data and returns a CompactTreeBagger. See also: CompactTreeBagger, ClassificationTree, RegressionTree, fitctree, fitrtree # name: # type: sq_string # elements: 1 # length: 33 Ensemble of bagged decision trees # name: # type: sq_string # elements: 1 # length: 32 TreeBagger.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 214 TreeBagger: property CategoricalPredictors Indices of the categorical predictors A row vector of column indices into X, naming the predictors treated as categorical, empty when none is. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Indices of the categorical predictors # name: # type: sq_string # elements: 1 # length: 21 TreeBagger.ClassNames # name: # type: sq_string # elements: 1 # length: 270 TreeBagger: property ClassNames Names of the classes The classes of a classification ensemble, in the type of the response and in the order its scores are laid out: sorted, or the order given by 'ClassNames'. Empty for a regression ensemble. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Names of the classes # name: # type: sq_string # elements: 1 # length: 31 TreeBagger.ComputeOOBPrediction # name: # type: sq_string # elements: 1 # length: 230 TreeBagger: property ComputeOOBPrediction Whether out-of-bag information is kept A logical scalar, false unless the ensemble was fitted with 'OOBPrediction' set to 'on'. The out-of-bag methods need it. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Whether out-of-bag information is kept # name: # type: sq_string # elements: 1 # length: 40 TreeBagger.ComputeOOBPredictorImportance # name: # type: sq_string # elements: 1 # length: 273 TreeBagger: property ComputeOOBPredictorImportance Whether out-of-bag predictor importance is computed A logical scalar, false unless the ensemble was fitted with 'OOBPredictorImportance' set to 'on', which keeps the out-of-bag information too. This property is read-only. # name: # type: sq_string # elements: 1 # length: 51 Whether out-of-bag predictor importance is computed # name: # type: sq_string # elements: 1 # length: 15 TreeBagger.Cost # name: # type: sq_string # elements: 1 # length: 316 TreeBagger: property Cost Misclassification costs A square matrix, Cost(i,j) being the cost of classifying an observation of class i as class j. The trees are grown with it; the ensemble's label is the class of highest average score, as MATLAB documents. Empty for a regression ensemble. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Misclassification costs # name: # type: sq_string # elements: 1 # length: 22 TreeBagger.DefaultYfit # name: # type: sq_string # elements: 1 # length: 343 TreeBagger: property DefaultYfit Prediction for an observation no tree may answer for For classification, the class of greatest prior probability, in the type of ClassNames; for regression, the weighted mean of the response. An out-of-bag prediction takes it for an observation that is in the sample of every tree. This property is read-only. # name: # type: sq_string # elements: 1 # length: 52 Prediction for an observation no tree may answer for # name: # type: sq_string # elements: 1 # length: 38 TreeBagger.DeltaCriterionDecisionSplit # name: # type: sq_string # elements: 1 # length: 230 TreeBagger: property DeltaCriterionDecisionSplit Split criterion contributions of the predictors A row vector with one element per predictor, the mean over the trees of each tree's predictorImportance. This property is read-only. # name: # type: sq_string # elements: 1 # length: 47 Split criterion contributions of the predictors # name: # type: sq_string # elements: 1 # length: 24 TreeBagger.InBagFraction # name: # type: sq_string # elements: 1 # length: 198 TreeBagger: property InBagFraction Size of each tree's sample A number greater than 0 and no greater than 1. Each tree is grown on ceil (InBagFraction * N) observations. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Size of each tree's sample # name: # type: sq_string # elements: 1 # length: 22 TreeBagger.MergeLeaves # name: # type: sq_string # elements: 1 # length: 154 TreeBagger: property MergeLeaves Whether the trees merge leaves A logical scalar, false unless 'MergeLeaves' was set to 'on'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 Whether the trees merge leaves # name: # type: sq_string # elements: 1 # length: 17 TreeBagger.Method # name: # type: sq_string # elements: 1 # length: 111 TreeBagger: property Method Type of the ensemble 'classification' or 'regression'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Type of the ensemble # name: # type: sq_string # elements: 1 # length: 22 TreeBagger.MinLeafSize # name: # type: sq_string # elements: 1 # length: 230 TreeBagger: property MinLeafSize Fewest observations a leaf may hold A positive integer, 1 by default for classification and 5 for regression. A node is split only when it holds at least twice as many. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Fewest observations a leaf may hold # name: # type: sq_string # elements: 1 # length: 28 TreeBagger.NumPredictorSplit # name: # type: sq_string # elements: 1 # length: 281 TreeBagger: property NumPredictorSplit Decision splits on each predictor A row vector with one element per predictor, the sum over the trees of the share of each tree's branch nodes that split on the predictor. A tree without branch nodes adds nothing. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Decision splits on each predictor # name: # type: sq_string # elements: 1 # length: 32 TreeBagger.NumPredictorsToSample # name: # type: sq_string # elements: 1 # length: 262 TreeBagger: property NumPredictorsToSample Predictors each split is chosen from A positive integer, or 'all'. The default is the square root of the number of predictors for classification and a third of it for regression, rounded up. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Predictors each split is chosen from # name: # type: sq_string # elements: 1 # length: 19 TreeBagger.NumTrees # name: # type: sq_string # elements: 1 # length: 131 TreeBagger: property NumTrees Number of trees A positive integer, the number of trees in the ensemble. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Number of trees # name: # type: sq_string # elements: 1 # length: 21 TreeBagger.OOBIndices # name: # type: sq_string # elements: 1 # length: 224 TreeBagger: property OOBIndices Which observations each tree left out An NxNumTrees logical matrix, true where an observation is not in a tree's sample. Empty unless ComputeOOBPrediction is true. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Which observations each tree left out # name: # type: sq_string # elements: 1 # length: 28 TreeBagger.OOBInstanceWeight # name: # type: sq_string # elements: 1 # length: 202 TreeBagger: property OOBInstanceWeight Number of trees each observation is out of bag for A column with one count per observation. Empty unless ComputeOOBPrediction is true. This property is read-only. # name: # type: sq_string # elements: 1 # length: 50 Number of trees each observation is out of bag for # name: # type: sq_string # elements: 1 # length: 47 TreeBagger.OOBPermutedPredictorCountRaiseMargin # name: # type: sq_string # elements: 1 # length: 475 TreeBagger: property OOBPermutedPredictorCountRaiseMargin Margins lowered less margins raised when a predictor is permuted A row vector with one element per predictor, computed as OOBPermutedPredictorDeltaError is from the number of each tree's out-of-bag observations whose margin the permutation lowers less the number whose margin it raises. Empty for a regression ensemble. Reading it is an error unless ComputeOOBPredictorImportance is true. This property is read-only. # name: # type: sq_string # elements: 1 # length: 64 Margins lowered less margins raised when a predictor is permuted # name: # type: sq_string # elements: 1 # length: 41 TreeBagger.OOBPermutedPredictorDeltaError # name: # type: sq_string # elements: 1 # length: 598 TreeBagger: property OOBPermutedPredictorDeltaError Rise in out-of-bag error when a predictor is permuted A row vector with one element per predictor. For each tree, the values of the predictor are permuted among the observations out of its bag, and the tree's error on them, the misclassification share or the mean squared error weighted by W, is taken before and after. The element is the mean of the rise over the trees divided by its standard deviation over the trees, zero when the mean is zero. Reading it is an error unless ComputeOOBPredictorImportance is true. This property is read-only. # name: # type: sq_string # elements: 1 # length: 53 Rise in out-of-bag error when a predictor is permuted # name: # type: sq_string # elements: 1 # length: 46 TreeBagger.OOBPermutedPredictorDeltaMeanMargin # name: # type: sq_string # elements: 1 # length: 461 TreeBagger: property OOBPermutedPredictorDeltaMeanMargin Fall in out-of-bag mean margin when a predictor is permuted A row vector with one element per predictor, computed as OOBPermutedPredictorDeltaError is from each tree's weighted mean classification margin on its out-of-bag observations, before the permutation less after it. Empty for a regression ensemble. Reading it is an error unless ComputeOOBPredictorImportance is true. This property is read-only. # name: # type: sq_string # elements: 1 # length: 59 Fall in out-of-bag mean margin when a predictor is permuted # name: # type: sq_string # elements: 1 # length: 25 TreeBagger.OutlierMeasure # name: # type: sq_string # elements: 1 # length: 394 TreeBagger: property OutlierMeasure Outlier measure of the training observations A column with one element per observation, computed from Proximity as CompactTreeBagger.outlierMeasure computes it, within each class for classification and over every observation for regression. Reading it is an error until fillprox fills it, and growTrees and append empty it again. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Outlier measure of the training observations # name: # type: sq_string # elements: 1 # length: 25 TreeBagger.PredictorNames # name: # type: sq_string # elements: 1 # length: 147 TreeBagger: property PredictorNames Names of the predictors A cell array of character vectors naming the columns of X. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 16 TreeBagger.Prior # name: # type: sq_string # elements: 1 # length: 277 TreeBagger: property Prior Prior probabilities of the classes A row vector with one probability per class, in the order of ClassNames. Every tree is grown with this prior, restricted to the classes its sample holds. Empty for a regression ensemble. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Prior probabilities of the classes # name: # type: sq_string # elements: 1 # length: 20 TreeBagger.Proximity # name: # type: sq_string # elements: 1 # length: 315 TreeBagger: property Proximity Proximity of the training observations A symmetric NxN matrix whose element (i,j) is the share of the trees that bring training observations i and j to the same leaf. Reading it is an error until fillprox fills it, and growTrees and append empty it again. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Proximity of the training observations # name: # type: sq_string # elements: 1 # length: 16 TreeBagger.Prune # name: # type: sq_string # elements: 1 # length: 185 TreeBagger: property Prune Whether the trees estimate a pruning sequence A logical scalar, false unless 'Prune' was set to 'on'. The trees are never pruned. This property is read-only. # name: # type: sq_string # elements: 1 # length: 45 Whether the trees estimate a pruning sequence # name: # type: sq_string # elements: 1 # length: 32 TreeBagger.SampleWithReplacement # name: # type: sq_string # elements: 1 # length: 153 TreeBagger: property SampleWithReplacement Whether the samples are drawn with replacement A logical scalar, true by default. This property is read-only. # name: # type: sq_string # elements: 1 # length: 46 Whether the samples are drawn with replacement # name: # type: sq_string # elements: 1 # length: 31 TreeBagger.SurrogateAssociation # name: # type: sq_string # elements: 1 # length: 241 TreeBagger: property SurrogateAssociation Predictive association between the predictors A square matrix with one row and one column per predictor. The trees grow no surrogate splits, so it is the identity matrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 45 Predictive association between the predictors # name: # type: sq_string # elements: 1 # length: 24 TreeBagger.TreeArguments # name: # type: sq_string # elements: 1 # length: 216 TreeBagger: property TreeArguments Options given to the trees A cell array of the Name-Value pairs given to the constructor that are passed on to every tree, in the order they were given. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Options given to the trees # name: # type: sq_string # elements: 1 # length: 21 TreeBagger.TreeBagger # name: # type: sq_string # elements: 1 # length: 3626 TreeBagger: B = TreeBagger (NumTrees, X, Y) TreeBagger: B = TreeBagger (NumTrees, Tbl, ResponseVarName) TreeBagger: B = TreeBagger (NumTrees, Tbl, formula) TreeBagger: B = TreeBagger (NumTrees, Tbl, Y) TreeBagger: B = TreeBagger (..., name, value) Grow an ensemble of bagged decision trees. B = TreeBagger (NumTrees, X, Y) grows NumTrees classification trees on the NxP predictor matrix X and the response Y. Y holds a class label per row, as a numeric or logical vector, a categorical, string or character array, or a cell array of character vectors. An observation whose response is missing is left out; one missing a predictor is kept. Each tree is grown on a sample of ceil (InBagFraction * N) observations, drawn with replacement in proportion to the weights, and chooses every split from NumPredictorsToSample predictors drawn afresh at each node. The trees are neither pruned nor merged unless asked. The random numbers come from Octave's generator, so rng reproduces an ensemble. Name-Value arguments of the ensemble: Name Value 'Method' 'classification' (default) or 'regression', for a real numeric response. 'NumPredictorsToSample' A positive integer or 'all'. The default is ceil (sqrt (P)) for classification and ceil (P / 3) for regression. 'MinLeafSize' A positive integer, 1 by default for classification and 5 for regression. 'InBagFraction' The share of the observations in each sample, greater than 0 and no greater than 1. The default is 1. 'SampleWithReplacement' 'on' (default) or 'off'. 'OOBPrediction' 'off' (default) or 'on', to keep what the out-of-bag methods need. Sampling without replacement at an 'InBagFraction' of 1 leaves nothing out of bag, so the two are refused together. 'OOBPredictorImportance' 'off' (default) or 'on', to estimate the importance of each predictor by permuting it among each tree's out-of-bag observations. It turns 'OOBPrediction' on. 'Weights' A nonnegative single or double vector with one weight per observation. The default is uniform. The model's W keeps the class of the weights, while every computation runs in double. 'Prior' 'empirical' (default), 'uniform', a vector with one probability per class, or a structure with fields ClassNames and ClassProbs. Classification only. 'Cost' A square matrix of misclassification costs, or a structure with fields ClassNames and ClassificationCosts. Classification only. 'ClassNames' The classes to fit, in the order their scores are to be laid out; observations of other classes are left out. Classification only. 'PredictorNames' A cell array of character vectors naming the columns of X. 'NumPrint' A nonnegative integer. After every that many trees a line saying how many are done is printed. The default, 0, prints nothing. 'CategoricalPredictors', 'MaxNumCategories', 'MaxNumSplits', 'MergeLeaves', 'Prune', 'PruneCriterion' and 'SplitCriterion' are passed on to every tree, and so are 'AlgorithmForCategorical' for classification and 'QuadraticErrorTolerance' for regression; see fitctree and fitrtree. Merging leaves is allowed but warned against. Surrogate splits, parallel growth and tall arrays are not implemented, and an option asking for one of them is refused. MATLAB returns classification labels as a cell array of character vectors whatever the type of the response; this ensemble returns them, and its ClassNames and DefaultYfit, in the type of the response, as every other classifier in this package does. Code that converts MATLAB's labels with str2double will get NaN from a numeric response here, its labels already being numbers. See also: TreeBagger, CompactTreeBagger, fitctree, fitrtree # name: # type: sq_string # elements: 1 # length: 42 Grow an ensemble of bagged decision trees. # name: # type: sq_string # elements: 1 # length: 16 TreeBagger.Trees # name: # type: sq_string # elements: 1 # length: 181 TreeBagger: property Trees The trees of the ensemble A column cell array holding one CompactClassificationTree or CompactRegressionTree object per tree. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 The trees of the ensemble # name: # type: sq_string # elements: 1 # length: 12 TreeBagger.W # name: # type: sq_string # elements: 1 # length: 356 TreeBagger: property W Observation weights A column of weights summing to one, one per observation. For classification each class's weights sum to its prior. The bootstrap draws observations in proportion to these weights, and the out-of-bag error is weighted by them. It has the class of the 'Weights' given, single or double. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 12 TreeBagger.X # name: # type: sq_string # elements: 1 # length: 371 TreeBagger: property X Predictor data The predictors the ensemble was fitted on, one row per observation. A row whose response is missing is not kept. This property is read-only. Where the model was fitted from a table, the predictors are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the model. # name: # type: sq_string # elements: 1 # length: 14 Predictor data # name: # type: sq_string # elements: 1 # length: 12 TreeBagger.Y # name: # type: sq_string # elements: 1 # length: 187 TreeBagger: property Y Response data The response the ensemble was fitted on, in the type it was given in, without the observations whose response is missing. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Response data # name: # type: sq_string # elements: 1 # length: 17 TreeBagger.append # name: # type: sq_string # elements: 1 # length: 453 TreeBagger: B = append (B1, B2) Add the trees of one ensemble to another. B is B1 with the trees of B2 appended. The two must be of the same type, fitted on the same number of observations, with the same classes and priors, and must agree on whether they keep out-of-bag information and predictor importance, which are then joined. A proximity matrix filled by fillprox is emptied. See also: TreeBagger, TreeBagger.growTrees, CompactTreeBagger.combine # name: # type: sq_string # elements: 1 # length: 41 Add the trees of one ensemble to another. # name: # type: sq_string # elements: 1 # length: 18 TreeBagger.compact # name: # type: sq_string # elements: 1 # length: 275 TreeBagger: C = compact (obj) Drop the training data from an ensemble. C = compact (obj) returns a CompactTreeBagger object holding the trees and what prediction needs. It predicts new data identically, and has no out-of-bag methods. See also: CompactTreeBagger, TreeBagger # name: # type: sq_string # elements: 1 # length: 40 Drop the training data from an ensemble. # name: # type: sq_string # elements: 1 # length: 16 TreeBagger.error # name: # type: sq_string # elements: 1 # length: 718 TreeBagger: err = error (obj, X, Y) TreeBagger: err = error (..., name, value) TreeBagger: err = error (obj, Tbl, ResponseVarName) TreeBagger: err = error (obj, Tbl) Misclassification probability or mean squared error of the ensemble. Behaves as CompactTreeBagger.error and takes the same Name-Value arguments. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. error (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and error (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: TreeBagger, TreeBagger.oobError, CompactTreeBagger.error # name: # type: sq_string # elements: 1 # length: 68 Misclassification probability or mean squared error of the ensemble. # name: # type: sq_string # elements: 1 # length: 19 TreeBagger.fillprox # name: # type: sq_string # elements: 1 # length: 593 TreeBagger: B = fillprox (obj) TreeBagger: B = fillprox (..., name, value) Fill the proximity matrix of the training data. B is the ensemble with Proximity holding the share of the trees that bring each pair of training observations to the same leaf, and OutlierMeasure the outlier measure computed from it. 'Trees' is 'all' (default) or a vector of indices of the trees to use. 'NumPrint' is a nonnegative integer; after every that many trees a line saying how many are done is printed. See also: TreeBagger, TreeBagger.mdsprox, CompactTreeBagger.proximity, CompactTreeBagger.outlierMeasure # name: # type: sq_string # elements: 1 # length: 47 Fill the proximity matrix of the training data. # name: # type: sq_string # elements: 1 # length: 20 TreeBagger.growTrees # name: # type: sq_string # elements: 1 # length: 448 TreeBagger: B = growTrees (obj, NumTrees) TreeBagger: B = growTrees (..., 'NumPrint', n) Grow more trees in an ensemble. B is the ensemble with NumTrees further trees grown exactly as the first were, their out-of-bag information added. A proximity matrix filled by fillprox is emptied, as it no longer describes the ensemble; MATLAB keeps it unchanged. 'NumPrint' is taken as by the TreeBagger constructor. See also: TreeBagger, TreeBagger.append # name: # type: sq_string # elements: 1 # length: 31 Grow more trees in an ensemble. # name: # type: sq_string # elements: 1 # length: 17 TreeBagger.margin # name: # type: sq_string # elements: 1 # length: 693 TreeBagger: m = margin (obj, X, Y) TreeBagger: m = margin (..., name, value) TreeBagger: m = margin (obj, Tbl, ResponseVarName) TreeBagger: m = margin (obj, Tbl) Classification margin of each observation. Behaves as CompactTreeBagger.margin and takes the same Name-Value arguments. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. margin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and margin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: TreeBagger, TreeBagger.oobMargin, CompactTreeBagger.margin # name: # type: sq_string # elements: 1 # length: 42 Classification margin of each observation. # name: # type: sq_string # elements: 1 # length: 18 TreeBagger.mdsprox # name: # type: sq_string # elements: 1 # length: 1052 TreeBagger: [S, E] = mdsprox (obj) TreeBagger: [S, E] = mdsprox (..., name, value) Multidimensional scaling of the proximity matrix. Applies classical multidimensional scaling, as cmdscale does, to the distances 1 - Proximity. S holds the scaled coordinates, one column per positive eigenvalue, and E the eigenvalues. fillprox must have filled Proximity first. Name-Value arguments: Name Value 'Keep' 'all' (default), or a vector of indices or a logical vector selecting the training observations to scale. 'Colors' A character vector with one color letter per class. When given, the scaled coordinates are drawn as overlaid scatter plots, one per class, a class beyond the number of letters not drawn; a regression ensemble is drawn in the first color. 'MDSCoordinates' Two or three indices of the columns of S to draw. The default is [1, 2]. They must not exceed the number of columns of S even when nothing is drawn, as in MATLAB, whose documentation says otherwise. See also: TreeBagger, TreeBagger.fillprox, CompactTreeBagger.mdsprox, cmdscale # name: # type: sq_string # elements: 1 # length: 49 Multidimensional scaling of the proximity matrix. # name: # type: sq_string # elements: 1 # length: 21 TreeBagger.meanMargin # name: # type: sq_string # elements: 1 # length: 727 TreeBagger: mm = meanMargin (obj, X, Y) TreeBagger: mm = meanMargin (..., name, value) TreeBagger: mm = meanMargin (obj, Tbl, ResponseVarName) TreeBagger: mm = meanMargin (obj, Tbl) Weighted mean classification margin. Behaves as CompactTreeBagger.meanMargin and takes the same Name-Value arguments. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. meanMargin (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and meanMargin (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: TreeBagger, TreeBagger.oobMeanMargin, CompactTreeBagger.meanMargin # name: # type: sq_string # elements: 1 # length: 36 Weighted mean classification margin. # name: # type: sq_string # elements: 1 # length: 19 TreeBagger.oobError # name: # type: sq_string # elements: 1 # length: 606 TreeBagger: err = oobError (obj) TreeBagger: err = oobError (..., name, value) Out-of-bag misclassification probability or mean squared error. The error of the out-of-bag predictions on the training data, weighted by W. In 'individual' mode each tree is judged on its own, the observations in its sample taking DefaultYfit, and every observation counts in every mode, as MATLAB counts it. 'Mode', 'Trees', 'TreeWeights' and 'Weights' are taken as by CompactTreeBagger.error. The ensemble must have been fitted with 'OOBPrediction' on. See also: TreeBagger, TreeBagger.oobPredict, TreeBagger.oobMeanMargin # name: # type: sq_string # elements: 1 # length: 63 Out-of-bag misclassification probability or mean squared error. # name: # type: sq_string # elements: 1 # length: 20 TreeBagger.oobMargin # name: # type: sq_string # elements: 1 # length: 387 TreeBagger: m = oobMargin (obj) TreeBagger: m = oobMargin (..., name, value) Out-of-bag classification margin of each training observation. The margins of the out-of-bag predictions, with 'Mode', 'Trees' and 'TreeWeights' taken as by CompactTreeBagger.margin. The ensemble must have been fitted with 'OOBPrediction' on. See also: TreeBagger, TreeBagger.oobMeanMargin, TreeBagger.margin # name: # type: sq_string # elements: 1 # length: 62 Out-of-bag classification margin of each training observation. # name: # type: sq_string # elements: 1 # length: 24 TreeBagger.oobMeanMargin # name: # type: sq_string # elements: 1 # length: 327 TreeBagger: mm = oobMeanMargin (obj) TreeBagger: mm = oobMeanMargin (..., name, value) Out-of-bag mean classification margin. The mean of TreeBagger.oobMargin, weighted by W unless 'Weights' are given. The ensemble must have been fitted with 'OOBPrediction' on. See also: TreeBagger, TreeBagger.oobMargin, TreeBagger.oobError # name: # type: sq_string # elements: 1 # length: 38 Out-of-bag mean classification margin. # name: # type: sq_string # elements: 1 # length: 21 TreeBagger.oobPredict # name: # type: sq_string # elements: 1 # length: 606 TreeBagger: label = oobPredict (obj) TreeBagger: [label, scores, stdevs] = oobPredict (...) TreeBagger: [Yfit, stdevs] = oobPredict (...) TreeBagger: ... = oobPredict (..., 'Trees', trees) Out-of-bag predictions for the training data. Each observation is predicted by the trees whose samples left it out, as TreeBagger.predict would with those trees alone. An observation in the sample of every tree used takes DefaultYfit, with the prior as its scores. 'Trees' restricts the trees. The ensemble must have been fitted with 'OOBPrediction' on. See also: TreeBagger, TreeBagger.oobError, TreeBagger.predict # name: # type: sq_string # elements: 1 # length: 45 Out-of-bag predictions for the training data. # name: # type: sq_string # elements: 1 # length: 27 TreeBagger.oobQuantileError # name: # type: sq_string # elements: 1 # length: 754 TreeBagger: err = oobQuantileError (obj) TreeBagger: err = oobQuantileError (..., name, value) Out-of-bag quantile loss of a regression ensemble. The loss of TreeBagger.quantileError on the training data, the quantiles predicted out of bag as by TreeBagger.oobQuantilePredict and the observations weighted by W. 'Mode', 'Quantile', 'Trees' and 'TreeWeights' are taken as by TreeBagger.quantileError. The ensemble must have been fitted with 'OOBPrediction' on. In 'individual' mode each tree is judged on the observations out of its own bag alone, and a tree that left nothing out has a NaN loss. MATLAB R2024a fails with an indexing error in that mode. See also: TreeBagger, TreeBagger.oobQuantilePredict, TreeBagger.quantileError, TreeBagger.oobError # name: # type: sq_string # elements: 1 # length: 50 Out-of-bag quantile loss of a regression ensemble. # name: # type: sq_string # elements: 1 # length: 29 TreeBagger.oobQuantilePredict # name: # type: sq_string # elements: 1 # length: 627 TreeBagger: YFit = oobQuantilePredict (obj) TreeBagger: [YFit, YW] = oobQuantilePredict (...) TreeBagger: ... = oobQuantilePredict (..., name, value) Out-of-bag quantile predictions for the training data. Each training observation is predicted as by TreeBagger.quantilePredict, by the trees whose samples left it out. An observation in the sample of every tree used takes quantile (obj.Y, tau). YW is NxN. 'Quantile', 'Trees' and 'TreeWeights' are taken as by TreeBagger.quantilePredict. The ensemble must have been fitted with 'OOBPrediction' on. See also: TreeBagger, TreeBagger.quantilePredict, TreeBagger.oobQuantileError # name: # type: sq_string # elements: 1 # length: 54 Out-of-bag quantile predictions for the training data. # name: # type: sq_string # elements: 1 # length: 18 TreeBagger.predict # name: # type: sq_string # elements: 1 # length: 751 TreeBagger: label = predict (obj, X) TreeBagger: [label, scores] = predict (...) TreeBagger: [label, scores, stdevs] = predict (...) TreeBagger: [Yfit, stdevs] = predict (...) TreeBagger: ... = predict (..., name, value) Predict responses with a bagged ensemble. Behaves as CompactTreeBagger.predict, and takes the same 'Trees', 'TreeWeights' and 'UseInstanceForTree' Name-Value arguments. The new data may be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: TreeBagger, TreeBagger.oobPredict, CompactTreeBagger.predict # name: # type: sq_string # elements: 1 # length: 41 Predict responses with a bagged ensemble. # name: # type: sq_string # elements: 1 # length: 24 TreeBagger.quantileError # name: # type: sq_string # elements: 1 # length: 1613 TreeBagger: err = quantileError (obj, X, Y) TreeBagger: err = quantileError (..., name, value) TreeBagger: err = quantileError (obj, Tbl, ResponseVarName) TreeBagger: err = quantileError (obj, Tbl) Quantile loss of a regression ensemble. For each quantile probability tau, err is the weighted mean over the observations of the pinball loss, tau (y - q) where the response y is not below the predicted quantile q and (1 - tau) (q - y) where it is. The quantiles are predicted as by TreeBagger.quantilePredict. In 'ensemble' mode, the default, err is a row with one element per quantile. In 'cumulative' mode it has one row per tree, the loss of the first tree, then of the first two, and so on, and in 'individual' mode one row per tree, each on its own. Name-Value arguments: Name Value 'Mode' 'ensemble' (default), 'cumulative' or 'individual'. 'Quantile' A vector of probabilities between 0 and 1. The default is 0.5. 'Weights' A nonnegative vector with one weight per observation. The default is uniform. 'Trees', 'TreeWeights' and 'UseInstanceForTree' are taken as by TreeBagger.quantilePredict, and 'TreeWeights' may not be given in 'individual' mode. X may also be a table Tbl, whose variables are matched to the predictors the model was fitted on by name and not by position. quantileError (obj, Tbl, ResponseVarName) takes the response from the variable ResponseVarName names, and quantileError (obj, Tbl) from the variable the model was fitted on. The response may also be given beside the table as Y. See also: TreeBagger, TreeBagger.quantilePredict, TreeBagger.oobQuantileError, TreeBagger.error # name: # type: sq_string # elements: 1 # length: 39 Quantile loss of a regression ensemble. # name: # type: sq_string # elements: 1 # length: 26 TreeBagger.quantilePredict # name: # type: sq_string # elements: 1 # length: 1970 TreeBagger: YFit = quantilePredict (obj, X) TreeBagger: [YFit, YW] = quantilePredict (...) TreeBagger: ... = quantilePredict (..., name, value) Predict quantiles of the response with a regression ensemble. Each tree gives every training observation a weight for each row of X: the number of times the tree's sample holds the observation, divided by the size of the leaf the row comes to rest at, when the observation is in that leaf, and zero otherwise. The weights are averaged over the trees, with their tree weights, into YW, a sparse NxM matrix with one row per training observation and one column per row of X, each column summing to one. YFit is an MxQ matrix holding, for each row of X and each quantile probability, the quantile of the training responses under those weights. The responses are sorted, each keeping its own weight, and the quantile is interpolated linearly between them at their cumulative weights less half their own weight, taking the smallest or the largest response beyond either end. A row no tree may answer for takes quantile (obj.Y, tau), and its column of YW weighs every training observation equally. The observation weights enter only through the samples they drew. Name-Value arguments: Name Value 'Quantile' A vector of probabilities tau between 0 and 1. The default is 0.5, the median. 'Trees' 'all' (default) or a vector of indices of the trees to use. 'TreeWeights' A nonnegative vector with one weight per tree used. The default weighs them equally. 'UseInstanceForTree' An MxNumTrees logical matrix saying which tree may answer for which row. X may also be a table, whose variables are matched to the predictors the model was fitted on by name and not by position: one the model was not fitted on is passed over, one it needs and cannot find is named, and a value holding a level is coded as that level was coded at fitting. See also: TreeBagger, TreeBagger.oobQuantilePredict, TreeBagger.quantileError, TreeBagger.predict # name: # type: sq_string # elements: 1 # length: 61 Predict quantiles of the response with a regression ensemble. # name: # type: sq_string # elements: 1 # length: 10 designecoc # name: # type: sq_string # elements: 1 # length: 2008 statistics: M = designecoc (K, name) statistics: M = designecoc (..., 'NumTrials', n) Coding design matrix for an error correcting output codes model. M = designecoc (K, name) returns the coding design for K classes named by name. M is a KxL matrix of -1, 0 and +1 with one row per class and one column per binary learner: a learner is trained to tell the classes marked +1 in its column from those marked -1, and a class marked 0 takes no part in it. K must be an integer of at least 2. name must be one of: Design Columns Description 'onevsone' K(K-1)/2 One column per pair of classes, the earlier class +1 and the later -1, pairs taken in order. 'onevsall' K One column per class, that class +1 and every other -1. 'binarycomplete' 2^(K-1)-1 Every way of splitting the classes into two non-empty groups, with the first class always +1. 'ternarycomplete' (3^K-2^(K+1)+1)/2 Every way of splitting into two non-empty groups while leaving any classes out. It grows fast: 28501 columns at K = 10. 'ordinal' K-1 Column j separates the first j classes from the rest, for classes that are ordered. 'denserandom' about 10log_2 K Random -1 and +1, no class left out. 'sparserandom' about 15log_2 K Random -1, 0 and +1, a class left out of a column with probability 0.5. M = designecoc (..., 'NumTrials', n) draws n random designs and keeps the one whose rows are furthest apart, which is what makes a random design correct errors. The default is 10000. It is accepted but does nothing for the five designs that are not random. K = 2 gives the single column [-1; 1] whatever the design, there being only one way to tell two classes apart. Deviation from MATLAB The two random designs cannot be reproduced from MATLAB and neither can their width. Measured on R2024a: five runs at K = 10 gave 'denserandom' 38, 38, 38, 40 and 40 columns and 'sparserandom' 57, 55, 55 and 54, so the number of columns varies between runs of MATLAB itself. The five other designs are exact. See also: fitcecoc, ClassificationECOC # name: # type: sq_string # elements: 1 # length: 64 Coding design matrix for an error correcting output codes model. # name: # type: sq_string # elements: 1 # length: 15 fitSVMPosterior # name: # type: sq_string # elements: 1 # length: 3841 statistics: ScoreSVMModel = fitSVMPosterior (SVMModel) statistics: ScoreSVMModel = fitSVMPosterior (SVMModel, name, value) statistics: ScoreSVMModel = fitSVMPosterior (SVMModel, X, Y) statistics: ScoreSVMModel = fitSVMPosterior (SVMModel, Tbl, ResponseVarName) statistics: ScoreSVMModel = fitSVMPosterior (SVMModel, Tbl, Y) statistics: ScoreSVMModel = fitSVMPosterior (SVMModel, Tbl) statistics: [ScoreSVMModel, ScoreTransform] = fitSVMPosterior (...) Fit the transformation from SVM scores to posterior probabilities. ScoreSVMModel = fitSVMPosterior (SVMModel) returns the two-class SVM classifier SVMModel with the transformation from the score of the second class, SVMModel.ClassNames(2), to its posterior probability stored in its ScoreTransform property, so that predict, or kfoldPredict for a cross-validated model, returns posterior probabilities, the two columns summing to 1. SVMModel is a ClassificationSVM, a CompactClassificationSVM or a ClassificationPartitionedModel cross-validating an SVM, and ScoreSVMModel is of the same class. The transformation is fitted to scores of observations the model was not trained on: For a ClassificationSVM, scores from cross-validating it, 10-fold unless one of the options below says otherwise. For a ClassificationPartitionedModel, its own out-of-fold scores, from the partition it was built with; no option may be given. With predictors X and their classes Y, the scores the model gives X, without cross-validation; a CompactClassificationSVM holds no training data and needs them. X may also be a table Tbl, matched to the predictors by variable name, with the classes in the variable ResponseVarName names, in Y, or in the variable the model was fitted on. Rows whose score is NaN are left out. Name Value 'KFold' The number of folds, an integer greater than 1. The default is 10. 'Holdout' The fraction of the data held out, a scalar in the range (0,1). 'Leaveout' 'on' for leave-one-out cross-validation, or 'off' (default). 'CVPartition' A cvpartition object over the observations of SVMModel. Only one of these options may be given, and only with a ClassificationSVM and no data. The transformation is one of three: A sigmoid 1 / (1 + exp (A * s + B)) of the score s when the scores of the two classes overlap, with A and B fitted by maximum likelihood, as a logistic regression of the class on the score. Neither the observation weights nor the prior enter the fit, as in MATLAB. A step function when the scores separate the classes: 0 below the largest score of the first class, 1 above the smallest score of the second, and SVMModel.Prior(2) between, with a warning. A constant when one class has prior probability 0, posterior 1 for the other class. A ScoreTransform already set on SVMModel is reset to 'none' first, with a warning. The property holds the transformation as '@(S)sigmoid(S,A,B)', '@(S)step(S,LowerBound,UpperBound,PositiveClassProbability)' or '@(S)constant(S,K)', K being the index of the predicted class. Each parameter is written exactly, in the fewest significant digits that read back to it, where MATLAB shows seven, so a model saved and reloaded predicts as before. The folds of a cross-validated model keep 'none', the transformation being applied to the scores they give together. [ScoreSVMModel, ScoreTransform] = fitSVMPosterior (...) also returns the parameters in a structure whose field Type is 'sigmoid', with fields Slope and Intercept, 'step', with fields LowerBound, UpperBound and PositiveClassProbability, or 'constant', with the field PredictedClass. Reference: J. Platt. Probabilistic outputs for support vector machines and comparisons to regularized likelihood methods. In Advances in Large Margin Classifiers, MIT Press, 2000. See also: ClassificationSVM.fitPosterior, CompactClassificationSVM.fitPosterior, fitcsvm, cvpartition # name: # type: sq_string # elements: 1 # length: 66 Fit the transformation from SVM scores to posterior probabilities. # name: # type: sq_string # elements: 1 # length: 9 fitcdiscr # name: # type: sq_string # elements: 1 # length: 3716 statistics: Mdl = fitcdiscr (X, Y) statistics: Mdl = fitcdiscr (Tbl, ResponseVarName) statistics: Mdl = fitcdiscr (Tbl, formula) statistics: Mdl = fitcdiscr (Tbl, Y) statistics: Mdl = fitcdiscr (..., name, value) Fit a Linear Discriminant Analysis classification model. Mdl = fitcdiscr (X, Y) returns a Linear Discriminant Analysis (LDA) classification model, Mdl, with X being the predictor data, and Y the class labels of observations in X. X must be a NxP numeric matrix of predictor data where rows correspond to observations and columns correspond to features or variables. Y is Nx1 matrix or cell matrix containing the class labels of corresponding predictor data in X. Y can be numerical, logical, char array or cell array of character vectors. Y must have same number of rows as X. Mdl = fitcdiscr (..., name, value) returns a Linear Discriminant Analysis model with additional options specified by Name-Value pair arguments listed below. Model Parameters Name Value 'PredictorNames' A cell array of character vectors specifying the names of the predictors. The length of this array must match the number of columns in X. 'ResponseName' A character vector specifying the name of the response variable. 'ClassNames' Names of the classes in the class labels, Y, used for fitting the Discriminant model. ClassNames are of the same type as the class labels in Y. The model keeps the classes in this order; by default they are sorted. 'Prior' A numeric vector specifying the prior probabilities for each class. The order of the elements in Prior corresponds to the order of the classes in ClassNames. Alternatively, you can specify 'empirical' to use the empirical class probabilities or 'uniform' to assume equal class probabilities. 'Weights' A single or double vector of nonnegative observation weights, one per row of X. They weigh the class means and covariances, the covariances being unbiased for them, and an empirical prior sums them per class. Only their proportions matter, and a row of zero weight is left out of the fit. The model's W keeps the class of the weights, while every computation runs in double, so Prior and the predictions are double where MATLAB returns single. 'Cost' A NxR numeric matrix containing misclassification cost for the corresponding instances in X where R is the number of unique categories in Y. If an instance is correctly classified into its category the cost is calculated to be 1, otherwise 0. cost matrix can be altered use Mdl.cost = somecost. default value cost = ones(rows(X),numel(unique(Y))). 'DiscrimType' A character vector naming the type of discriminant analysis to perform, one of 'linear' (default), 'quadratic', 'diagLinear', 'diagQuadratic', 'pseudoLinear' or 'pseudoQuadratic'. A linear type pools one covariance across the classes and a quadratic type estimates one per class; a 'diag' type keeps only the variances, and a 'pseudo' type inverts a singular covariance rather than refusing it. The property may be reassigned after fitting, but only within its own family, since the family decides which covariances the fit estimates. 'FillCoeffs' A character vector or string scalar with values 'on' or 'off' specifying whether to fill the coefficients after fitting. If set to 'on', the coefficients are computed during model fitting, which can be useful for prediction. 'Gamma' A numeric scalar specifying the regularization parameter for the covariance matrix. It adjusts the linear discriminant analysis to make the model more stable in the presence of multicollinearity or small sample sizes. A value of 0 corresponds to no regularization, while a value of 1 corresponds to a completely regularized model. See also: ClassificationDiscriminant # name: # type: sq_string # elements: 1 # length: 56 Fit a Linear Discriminant Analysis classification model. # name: # type: sq_string # elements: 1 # length: 8 fitcecoc # name: # type: sq_string # elements: 1 # length: 4697 statistics: Mdl = fitcecoc (X, Y) statistics: Mdl = fitcecoc (Tbl, ResponseVarName) statistics: Mdl = fitcecoc (Tbl, formula) statistics: Mdl = fitcecoc (Tbl, Y) statistics: Mdl = fitcecoc (..., name, value) Fit a multiclass model from binary learners. Mdl = fitcecoc (X, Y) turns the multiclass problem in X and Y into a set of two class problems, fits one binary learner to each, and returns them as a ClassificationECOC object. A coding matrix says which classes each learner calls +1, which it calls -1, and which sit it out; a new observation is sent to every learner and given the class whose row of that matrix its scores match most closely. X must be a NxP numeric matrix of predictor data. Y must be a Nx1 vector of class labels, of any type ClassNames accepts, with at least two distinct values. Name Value 'Learners' The binary learner, either a name, 'svm' (default), 'tree', 'knn', 'naivebayes', 'discriminant', 'linear', 'kernel' or 'ensemble', a LogitBoost ensemble of 100 trees, or a template from templateSVM and its siblings, templateEnsemble among them, which also carries the options that learner is to be fitted with. 'Coding' The coding design, either a name designecoc accepts, 'onevsone' by default, or a coding matrix given outright, which sets CodingName to 'custom'. 'BinaryLoss' The loss the binary scores are read with. The default follows the learner: 'exponential' for an AdaBoostM1 or GentleBoost ensemble, 'binodeviance' for a LogitBoost one, and otherwise 'hinge' for one scoring on (-Inf,+Inf) and 'quadratic' for one scoring on [0,1], as a bagged, random subspace or RUSBoost ensemble does. 'ClassNames' The classes to fit, and the order the rows of the coding matrix, Prior and Cost take them in. By default they are sorted. 'Cost' A KxK matrix of misclassification costs. The default is 1 - eye (K). Or a structure with fields ClassNames and ClassificationCosts, which names its own order of the classes and is permuted to the model's. 'Prior' 'empirical' (default), 'uniform', or a vector with one element per class. A class whose prior is 0 is removed from the data before fitting, with a warning, as in MATLAB. 'Weights' A nonnegative single or double vector with one element per observation. The default is uniform. The model's W keeps the class of the weights, while every computation runs in double, so Prior is double where MATLAB returns single. 'CategoricalPredictors' The predictors whose values are levels, as indices, a logical vector or 'all', passed as given to every binary learner, which codes them its own way; a nearest neighbour learner takes only 'all' and a discriminant learner none. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. 'PredictorNames' A cellstr of predictor names. 'ResponseName' The name of the response variable. 'ScoreTransform' A transform applied to the returned scores. The default is 'none'. A linear or kernel learner carries no training data of its own, so fitcecoc returns a CompactClassificationECOC for those two and a ClassificationECOC for the other five, which is what R2024a does. 'FitPosterior' is refused rather than quietly ignored: it installs a fitted score transform on each binary learner, which needs the posterior fitting this package does not have yet. An ensemble template may name any classification method of fitcensemble; a regression template is refused, as in MATLAB. A RUSBoost ensemble scores each class with the weighted sum of its trees' class probabilities, which no binary loss can read: MATLAB sets the loss to 'unknown' and then cannot predict. Here those scores, and the binary scores predict returns, are divided by the total weight of the trees, which gives the weighted mean of their class probabilities, and are read with 'quadratic' as a bagged ensemble's are. This follows R's ebmc and adabag packages and scikit-learn, whose AdaBoostClassifier and imbalanced-learn's RUSBoostClassifier built on it scale a boosted ensemble's weighted votes the same way. A binary learner that cannot be fitted, such as an AdaBoostM2 ensemble, which needs three classes, stops the fit with its own error, where MATLAB warns and predicts the majority class. An AdaBoostM1 learner whose first tree separates its two classes keeps that tree, as fitcensemble does; MATLAB keeps no tree and never predicts those classes from it. Each binary learner is fitted with the weight its observations carry, which the prior and any 'Weights' decide. See also: ClassificationECOC, CompactClassificationECOC, designecoc, templateSVM, templateTree # name: # type: sq_string # elements: 1 # length: 44 Fit a multiclass model from binary learners. # name: # type: sq_string # elements: 1 # length: 12 fitcensemble # name: # type: sq_string # elements: 1 # length: 10608 statistics: Mdl = fitcensemble (X, Y) statistics: Mdl = fitcensemble (Tbl, ResponseVarName) statistics: Mdl = fitcensemble (Tbl, formula) statistics: Mdl = fitcensemble (Tbl, Y) statistics: Mdl = fitcensemble (..., name, value) Fit an ensemble of decision trees for classification. Mdl = fitcensemble (X, Y) grows 100 boosted decision trees on the NxP predictor matrix X and the class labels Y, LogitBoost for two classes and AdaBoostM2 for more, and returns a ClassificationEnsemble. With 'Method' set to 'Bag', or with a boosting method that resamples, it returns a ClassificationBaggedEnsemble. Y holds a class label per row of X, as a numeric or logical vector, a categorical, string or character array, or a cell array of character vectors. A row missing a predictor or a class is left out. The boosting methods, y being +1 for the first class and -1 for the second, d the observation weights, which start at W times the total cost of misclassifying each observation's class, and eta the learning rate: 'AdaBoostM1' Two classes. Each tree is grown with the weights d; h is +1 where it predicts the first class and -1 elsewhere, e its weighted error, and its weight eta * log ((1 - e) / e) / 2. The weights are then multiplied by exp (-weight * y .* h). 'AdaBoostM2' More than two classes. A weight is kept for each observation and each class other than its own, and each tree is grown with their sums; its pseudo-loss e over its class probabilities gives its weight as for AdaBoostM1, and the scores are the weighted sums of the class probabilities. 'RUSBoost' Two or more classes, for classes of unequal size. Each tree is grown without weights on a sample of every class, round (r * m) rows, m being the size of the smallest class and r the class's element of 'RatioToSmallest', drawn in proportion to d and with replacement only when the class holds fewer rows. Its pseudo-loss e over all the observations, the weight of each spread evenly over the classes other than its own, gives its weight as for AdaBoostM1, the weights are then multiplied by the mean over those classes of exp (-weight * (1 + h_true - h_k)), and the scores are as for AdaBoostM2. A perfect tree is kept as for AdaBoostM2. 'LPBoost' Two or more classes. Each tree is grown with the weights d, and its margins and edge are as for TotalBoost. A linear program over the trees with the new one, solved by GLPK, gives the least over distributions of their largest edge; when the smallest edge is no more than 'MarginPrecision' above it, the new tree is not kept and the fit stops. Otherwise the program gives the learner weights that maximise the smallest margin, and as its dual the next weights d. The scores are as for TotalBoost. Where the program has several solutions, MATLAB and this package may choose different ones: the learner weights, and the weights d with the trees grown on them, then differ, and so may the number of trees kept. MATLAB R2024a keeps a tree only when that gap is also above 0.01, so a 'MarginPrecision' below 0.01 acts there as 0.01; here it is taken as given. 'TotalBoost' Two or more classes. Each tree is grown with the weights d; its margin on an observation is the probability it gives the observation's class less the largest it gives another, and its edge is those margins weighted by d. The fit stops, without that tree, once the least over distributions of the largest edge of the trees exceeds the smallest edge less 'MarginPrecision'. Otherwise d takes one quadratic step towards the least relative entropy to the starting weights, every edge held at most the smallest edge less 'MarginPrecision', and the learner weights are those that maximise the smallest margin, a linear program solved by GLPK. A tree scores each class with twice its probability less one. Where several learner weights maximise that margin equally, MATLAB and this package may choose different ones, and the scores then differ. 'GentleBoost' Two classes. Each regression tree is fitted to y with the weights d, its prediction h added to the score times eta, and the weights multiplied by exp (-eta * y .* h). 'LogitBoost' Two classes. With p the probability of the first class, each regression tree is fitted to (y01 - p) ./ (p .* (1 - p)) with the weights d .* p .* (1 - p); its prediction times eta / 2 is added to the score f, and p = 1 ./ (1 + exp (-f)). The 'Subspace' method fits each learner, a nearest neighbour or discriminant classifier, on NPredToSample predictors drawn at random without replacement, or on every combination of that many with 'NumLearningCycles' set to 'AllPredictorCombinations', and scores each observation with the plain average of the learners' class probabilities. UsePredForLearner records the predictors of each learner; every combination is taken in the order of nchoosek, which MATLAB R2024a reverses for some subset sizes, changing the order of the learners but not the scores. MATLAB takes observation weights here and passes them to the learners; the learners in this package take none, so weights that are not uniform are refused. A boosting method resamples when 'Resample' is 'on' or 'FResample' or 'Replace' is given; RUSBoost and Subspace cannot. Each learner is then grown on ceil (FResample * N) rows, drawn with replacement in proportion to d, each draw weighing the same, or without replacement uniformly, each row keeping its weight in d. Its error is taken on those rows with those weights, and only they are reweighted, rescaled to the weight they carried over their draws. AdaBoostM2 then keeps one weight per observation, spread evenly over the classes other than its own, as RUSBoost does, and LogitBoost advances the score of the rows drawn only. The ensemble is a ClassificationBaggedEnsemble, which records the rows each learner drew and estimates the out-of-bag error. These rules reproduce MATLAB R2024a's fits on its own draws, except LogitBoost with replacement, whose reweighting in MATLAB was not identified; here it follows the others. A boosting method whose tree classifies the data without error, zero error for AdaBoostM1 or zero pseudo-loss for AdaBoostM2, keeps that tree with the weight an error of eps gives and stops; one whose error is greater than 0.5 is not kept and stops. MATLAB discards a perfect tree, so a fit whose first tree is perfect is empty there. GentleBoost applies the learning rate to the fit itself, as the other boosting methods do; MATLAB scales only the learner weights, growing the same trees whatever the rate. Name-Value arguments: Name Value 'Method' 'AdaBoostM1', 'AdaBoostM2', 'RUSBoost', 'GentleBoost', 'LogitBoost', 'LPBoost', 'TotalBoost', 'Bag' or 'Subspace'. The default is 'LogitBoost' for two classes and 'AdaBoostM2' for more. 'NumLearningCycles' A positive integer, the number of learners to grow, or for Subspace 'AllPredictorCombinations'. The default is 100. 'NPredToSample' A positive integer less than the number of predictors, the predictors each Subspace learner is fitted on. The default is 1. Subspace only. 'Learners' For Subspace 'knn' (default), 'discriminant', or a template from templateKNN or templateDiscriminant. Otherwise 'tree' (default) or a template from templateTree, whose options override the defaults: for boosting MaxNumSplits 10, MinParentSize 2 and MinLeafSize 1, the regression trees of GentleBoost and LogitBoost taking MinParentSize 10; for Bag unlimited splits, MinParentSize 2, MinLeafSize 1 and NumVariablesToSample ceil (sqrt (P)). 'LearnRate' A number greater than 0 and no greater than 1. The default is 1. Boosting only. 'RatioToSmallest' A nonnegative number, or a vector with one per class, the size of each class's sample relative to the smallest class. The default is 1 for every class. RUSBoost only. 'MarginPrecision' A number from 0 to 1, how far above the linear program LPBoost needs the smallest edge to stay, and how far below the smallest edge TotalBoost holds every edge. The default is 0.01. LPBoost and TotalBoost only. 'CategoricalPredictors' The predictors whose values are levels, as indices, as a logical vector with one element per predictor, or as 'all'. Every tree splits them into sets of levels, as fitctree does. Not with 'Subspace'. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. 'FResample' The share of the observations each learner draws, greater than 0 and no greater than 1. The default is 1. Given with a boosting method, the ensemble resamples. 'Replace' 'on' (default) or 'off', whether the learners draw with replacement. Given with a boosting method, the ensemble resamples. 'Resample' 'off' (default) or 'on', whether a boosting method resamples. Bag always does. 'NPrint' 'off' (default) or a positive integer n, to print a line after every n trees. 'ClassNames' The classes to fit, in the order their scores are laid out; by default they are sorted. 'Cost' A KxK matrix of misclassification costs. The default is 1 - eye (K). Or a structure with fields ClassNames and ClassificationCosts, which names its own order of the classes and is permuted to the model's. 'Prior' 'empirical' (default), 'uniform', or a vector with one element per class. A class whose prior is 0 is removed from the data before fitting, with a warning, as in MATLAB. A row of misclassification costs with no positive cost is refused. 'Weights' A nonnegative single or double vector with one weight per observation. The default is uniform. The model's W keeps the class of the weights, while every computation runs in double, so Prior is double where MATLAB returns single. 'PredictorNames' A cell array of character vectors naming the columns of X. 'ResponseName' The name of the response variable. 'ScoreTransform' A transform applied to the returned scores. The default is 'none'. 'CrossVal' set to 'on', 'KFold', 'Holdout', 'Leaveout' or 'CVPartition', only one of them, fits the ensemble and cross-validates it as crossval does, returning a ClassificationPartitionedEnsemble. The method 'RobustBoost', binning and hyperparameter optimization are not implemented, and an option asking for one of them is refused. 'CategoricalPredictors' cannot be used with 'Subspace', its nearest neighbour and discriminant learners taking no categorical predictors here; MATLAB passes them to its nearest neighbour learners and drops them, with a warning, from its discriminant ones. See also: ClassificationEnsemble, ClassificationBaggedEnsemble, CompactClassificationEnsemble, templateTree, TreeBagger # name: # type: sq_string # elements: 1 # length: 53 Fit an ensemble of decision trees for classification. # name: # type: sq_string # elements: 1 # length: 7 fitcgam # name: # type: sq_string # elements: 1 # length: 8154 statistics: Mdl = fitcgam (X, Y) statistics: Mdl = fitcgam (Tbl, ResponseVarName) statistics: Mdl = fitcgam (Tbl, formula) statistics: Mdl = fitcgam (Tbl, Y) statistics: Mdl = fitcgam (..., name, value) Fit a Generalized Additive Model (GAM) for binary classification. Mdl = fitcgam (X, Y) returns a GAM classification model, Mdl, with X being the predictor data, and Y the binary class labels of observations in X. X must be a NxP numeric matrix of predictor data where rows correspond to observations and columns correspond to features or variables. Y is Nx1 numeric vector containing binary class labels, typically 0 or 1. Tbl may stand in place of X, the response named by one of its variables, written into a model formula holding main effects, 'Y ~ x1 + x2', or given beside it as Y. A variable holding levels rather than numbers is a categorical predictor without being named one, and 'CategoricalPredictors' adds to that set rather than replacing it. PredictorNames and ResponseName come from the table. Mdl = fitcgam (..., name, value) returns a GAM classification model with additional options specified by Name-Value pair arguments listed below. Model Parameters Name Value 'FitMethod' A character vector selecting the weak learner, either 'boostedtrees' or 'splines'. The default is 'boostedtrees', which boosts one shallow decision tree per predictor and is the scheme MATLAB uses. 'splines' boosts a smoothing spline per predictor instead and is an Octave extension. The two take different options and an option meant for one is refused by the other rather than ignored, so the rows below say which engine each belongs to. 'PredictorNames' A cell array of character vectors specifying the names of the predictors. The length of this array must match the number of columns in X. 'ResponseName' A character vector specifying the name of the response variable. 'ClassNames' Names of the classes in the class labels, Y, used for fitting the Discriminant model. ClassNames are of the same type as the class labels in Y. The model keeps the classes in this order; by default they are sorted. 'Cost' A NxR numeric matrix containing misclassification cost for the corresponding instances in X where R is the number of unique categories in Y. If an instance is correctly classified into its category the cost is calculated to be 1, otherwise 0. cost matrix can be altered use Mdl.cost = somecost. default value cost = ones(rows(X),numel(unique(Y))). 'Formula' (spline option) A model specification given as a string in the form 'Y ~ terms' where Y represents the response variable and terms the predictor variables. The formula can be used to specify a subset of variables for training model. For example: 'Y ~ x1 + x2 + x3 + x4 + x1:x2 + x2:x3' specifies four linear terms for the first four columns of for predictor data, and x1:x2 and x2:x3 specify the two interaction terms for 1st-2nd and 3rd-4th columns respectively. Only these terms will be used for training the model, but X must have at least as many columns as referenced in the formula. If Predictor Variable names have been defined, then the terms in the formula must reference to those. When 'formula' is specified, all terms used for training the model are referenced in the IntMatrix field of the obj class object as a matrix containing the column indexes for each term including both the predictors and the interactions used. 'Interactions' A logical matrix, a positive integer scalar, or the string 'all' for defining the interactions between predictor variables. When given a logical matrix, it must have the same number of columns as X and each row corresponds to a different interaction term combining the predictors indexed as true. Each interaction term is appended as a column vector after the available predictor column in X. When 'all' is defined, then all possible combinations of interactions are appended in X before training. At the moment, parsing a positive integer has the same effect as the 'all' option. When 'interactions' is specified, only the interaction terms appended to X are referenced in the IntMatrix field of the obj class object. 'Knots' (spline option) A scalar or a row vector with the same columns as X. It defines the knots for fitting a polynomial when training the GAM. As a scalar, it is expanded to a row vector. The default value is 5, hence expanded to ones (1, columns (X)) * 5. You can parse a row vector with different number of knots for each predictor variable to be fitted with, although not recommended. 'Order' (spline option) A scalar or a row vector with the same columns as X. It defines the order of the polynomial when training the GAM. As a scalar, it is expanded to a row vector. The default values is 3, hence expanded to ones (1, columns (X)) * 3. You can parse a row vector with different number of polynomial order for each predictor variable to be fitted with, although not recommended. 'DoF' (spline option) A scalar or a row vector with the same columns as X. It defines the degrees of freedom for fitting a polynomial when training the GAM. As a scalar, it is expanded to a row vector. The default value is 8, hence expanded to ones (1, columns (X)) * 8. You can parse a row vector with different degrees of freedom for each predictor variable to be fitted with, although not recommended. The rows above marked as spline options require 'FitMethod', 'splines'. The remaining options belong to the boosted-tree engine and require 'FitMethod', 'boostedtrees', which is the default. Name Value 'NumTreesPerPredictor' A positive integer, the number of boosting rounds of the predictor phase. It is a budget rather than a count: a fit that stops improving ends earlier and reports so. The default is 300. 'NumTreesPerInteraction' A positive integer, the same budget for the interaction phase. The default is 100. 'MaxNumSplitsPerPredictor' A positive integer, the largest number of splits any one predictor tree may make. The default is 1, which makes each tree a stump. 'MaxNumSplitsPerInteraction' The same limit for a tree over a pair of predictors. The default is 4. 'InitialLearnRateForPredictors' A value greater than 0 and at most 1, the step a round of the predictor phase starts at. A round that fails to improve the fit is retried at half the step, so this is an initial value rather than a fixed one. The default is 1. 'InitialLearnRateForInteractions' The same for the interaction phase. The default is 1. 'MaxPValue' A value between 0 and 1. A candidate pair of predictors is kept only if its interaction test gives a p-value no larger than this. The default is 1, which keeps every pair asked for. 'Verbose' A non-negative integer. Greater than zero prints a trace of the fit. The default is 0. 'NumPrint' A positive integer, how often the trace reports: the first round and then every NumPrint rounds. The default is 10. 'CategoricalPredictors' The predictors to treat as categorical: a vector of column indices, a logical vector with one element per predictor, or 'all'. A tree splits a categorical predictor into two sets of levels, and a level not seen in training predicts as a missing value. Its BinEdges and PairDetectionBinEdges are empty. The default is none. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. 'Weights' A single or double vector of non-negative observation weights, one per row of X. Each class carries its 'Prior', spread over its observations in proportion to their weights. The model's W keeps the class of the weights, while every computation runs in double, so Prior and the predictions are double where MATLAB returns single. A class whose prior is 0 is removed from the data before fitting, with a warning, as in MATLAB. You can parse either a 'Formula' or an 'Interactions' optional parameter. Parsing both parameters will result an error. Accordingly, you can only pass up to two parameters among 'Knots', 'Order', and 'DoF' to define the required polynomial for training the GAM model. See also: ClassificationGAM # name: # type: sq_string # elements: 1 # length: 65 Fit a Generalized Additive Model (GAM) for binary classification. # name: # type: sq_string # elements: 1 # length: 10 fitckernel # name: # type: sq_string # elements: 1 # length: 1250 statistics: Mdl = fitckernel (X, Y) statistics: Mdl = fitckernel (Tbl, ResponseVarName) statistics: Mdl = fitckernel (Tbl, formula) statistics: Mdl = fitckernel (Tbl, Y) statistics: Mdl = fitckernel (..., name, value) statistics: [Mdl, FitInfo] = fitckernel (...) Fit a Gaussian kernel binary classifier. Mdl = fitckernel (X, Y) returns a ClassificationKernel object fitted to the predictor data X and the two class response Y, where X is an NxP numeric matrix and Y has as many rows as X. Mdl = fitckernel (..., name, value) passes the given Name-Value pairs to the model. They are documented under ClassificationKernel, and the ones most often wanted are 'Learner', 'NumExpansionDimensions', 'KernelScale', 'Lambda', 'BoxConstraint' and 'Standardize'. [Mdl, FitInfo] = fitckernel (...) also returns a structure describing the optimization: the objective it reached, the gradient it left, and the tolerances it was given. Mdl = fitckernel (..., cvopt, value) returns a ClassificationPartitionedKernel instead when one of 'CrossVal', 'KFold', 'Holdout', 'Leaveout' and 'CVPartition' is given. A cross-validated model describes no single fit, so FitInfo is not available beside it. See also: ClassificationKernel, ClassificationLinear, fitclinear # name: # type: sq_string # elements: 1 # length: 40 Fit a Gaussian kernel binary classifier. # name: # type: sq_string # elements: 1 # length: 7 fitcknn # name: # type: sq_string # elements: 1 # length: 11457 statistics: Mdl = fitcknn (X, Y) statistics: Mdl = fitcknn (Tbl, ResponseVarName) statistics: Mdl = fitcknn (Tbl, formula) statistics: Mdl = fitcknn (Tbl, Y) statistics: Mdl = fitcknn (..., name, value) Fit a k-Nearest Neighbor classification model. Mdl = fitcknn (X, Y) returns a k-Nearest Neighbor classification model, Mdl, with X being the predictor data, and Y the class labels of observations in X. X must be a NxP numeric matrix of predictor data where rows correspond to observations and columns correspond to features or variables. Y is Nx1 matrix or cell matrix containing the class labels of corresponding predictor data in X. Y can be numerical, logical, char array or cell array of character vectors. Y must have same number of rows as X. Mdl = fitcknn (..., name, value) returns a k-Nearest Neighbor classification model with additional options specified by Name-Value pair arguments listed below. Model Parameters Name Value 'Standardize' A boolean flag indicating whether the data in X should be standardized prior to training. 'CategoricalPredictors' Empty (default) or 'all', which takes every predictor as categorical and makes 'hamming' the default distance, so observations are compared by the levels they share. A list of some of the predictors is refused, as MATLAB refuses it. 'PredictorNames' A cell array of character vectors specifying the predictor variable names. The variable names are assumed to be in the same order as they appear in the training data X. 'ResponseName' A character vector specifying the name of the response variable. 'ClassNames' Names of the classes in the class labels, Y, used for fitting the kNN model. ClassNames are of the same type as the class labels in Y. The model keeps the classes in this order; by default they are sorted. 'Prior' A numeric vector specifying the prior probabilities for each class. The order of the elements in Prior corresponds to the order of the classes in ClassNames. A class whose prior is 0 is removed from the data before fitting, with a warning, as in MATLAB. 'Weights' A single or double vector of nonnegative observation weights, one per row of X. Each class carries its prior, spread over its observations in proportion to their weights, and a neighbour votes with that weight. An empirical prior sums the weights per class, standardization uses weighted means and standard deviations, and a row of zero weight is left out. The model's W keeps the class of the weights, while every computation runs in double, so Prior is double where MATLAB returns single. 'Cost' A NxR numeric matrix containing misclassification cost for the corresponding instances in X where R is the number of unique categories in Y. If an instance is correctly classified into its category the cost is calculated to be 1, otherwise 0. cost matrix can be altered use Mdl.cost = somecost. default value cost = ones(rows(X),numel(unique(Y))). 'ScoreTransform' A character vector defining one of the following functions or a user defined function handle, which is used for transforming the prediction scores returned by the predict and resubPredict methods. Default value is 'none'. Value Description 'doublelogit' 1 ./ (1 + exp (-2 x x)) 'invlogit' log (x ./ (1 - x)) 'ismax' Sets the score for the class with the largest score to 1, and sets the scores for all other classes to 0 'logit' 1 ./ (1 + exp (-x)) 'none' x (no transformation) 'identity' x (no transformation) 'sign' -1 for x < 0, 0 for x = 0, 1 for x > 0 'symmetric' 2 x x - 1 'symmetricismax' Sets the score for the class with the largest score to 1, and sets the scores for all other classes to -1 'symmetriclogit' 2 ./ (1 + exp (-x)) - 1 Name Value 'BreakTies' Tie-breaking algorithm used by predict when multiple classes have the same smallest cost. By default, ties occur when multiple classes have the same number of nearest points among the k nearest neighbors. The available options are specified by the following character arrays: Value Description 'smallest' This is the default and it favors the class with the smallest index among the tied groups, i.e. the one that appears first in the training labelled data. 'nearest' This favors the class with the nearest neighbor among the tied groups, i.e. the class with the closest member point according to the distance metric used. 'random' This randomly picks one class among the tied groups. Name Value 'BucketSize' The maximum number of data points in the leaf node of the Kd-tree and it must be a positive integer. By default, it is 50. This argument is meaningful only when the selected search method is 'kdtree'. 'NumNeighbors' A positive integer value specifying the number of nearest neighbors to be found in the kNN search. By default, it is 1. 'Exponent' A positive scalar (usually an integer) specifying the Minkowski distance exponent. This argument is only valid when the selected distance metric is 'minkowski'. By default it is 2. An exponent of Inf gives the Chebychev distance. 'Scale' A nonnegative numeric vector specifying the scale parameters for the standardized Euclidean distance. The vector length must be equal to the number of columns in X. This argument is only valid when the selected distance metric is 'seuclidean', in which case each coordinate of X is scaled by the corresponding element of 'scale', as is each query point in Y. By default, the scale parameter is the standard deviation of each coordinate in X. If a variable in X is constant, i.e. zero variance, this value is forced to 1 to avoid division by zero. This is the equivalent of this variable not being standardized. 'Cov' A square matrix with the same number of columns as X specifying the covariance matrix for computing the mahalanobis distance. This must be a positive definite matrix matching. This argument is only valid when the selected distance metric is 'mahalanobis'. 'Distance' is the distance metric used by knnsearch as specified below: Value Description 'euclidean' Euclidean distance. 'seuclidean' standardized Euclidean distance. Each coordinate difference between the rows in X and the query matrix Y is scaled by dividing by the corresponding element of the standard deviation computed from X. To specify a different scaling, use the 'Scale' name-value argument. 'cityblock' City block distance. 'chebychev' Chebychev distance (maximum coordinate difference). 'minkowski' Minkowski distance. The default exponent is 2. To specify a different exponent, use the 'P' name-value argument. 'mahalanobis' Mahalanobis distance, computed using a positive definite covariance matrix. To change the value of the covariance matrix, use the 'Cov' name-value argument. 'cosine' Cosine distance. 'correlation' One minus the sample linear correlation between observations (treated as sequences of values). 'spearman' One minus the sample Spearman's rank correlation between observations (treated as sequences of values). 'hamming' Hamming distance, which is the percentage of coordinates that differ. 'jaccard' One minus the Jaccard coefficient, which is the percentage of nonzero coordinates that differ. @distfun Custom distance function handle. A distance function of the form function D2 = distfun (XI, YI), where XI is a 1xP vector containing a single observation in P-dimensional space, YI is an NxP matrix containing an arbitrary number of observations in the same P-dimensional space, and D2 is an NxP vector of distances, where (D2k) is the distance between observations XI and (YIk,:). Name Value 'DistanceWeight' A distance weighting function, specified either as a function handle, which accepts a matrix of nonnegative distances and returns a matrix the same size containing nonnegative distance weights, or one of the following values: 'equal', which corresponds to no weighting; 'inverse', which corresponds to a weight equal to 1/distance; 'squaredinverse', which corresponds to a weight equal to 1/distance^2. 'CacheSize' A positive scalar, the cache size in megabytes, 1000 by default. It is stored and reported for compatibility and does not affect the fit or any prediction: a nearest-neighbour model keeps no Gram matrix to cache, holding its training data and computing each distance when asked. MATLAB hides the same property from properties, where this package reports it. 'IncludeTies' A boolean flag to indicate if the returned values should contain the indices that have same distance as the K^th neighbor. When false, knnsearch chooses the observation with the smallest index among the observations that have the same distance from a query point. When true, knnsearch includes all nearest neighbors whose distances are equal to the K^th smallest distance in the output arguments. To specify K, use the 'K' name-value pair argument. 'NSMethod' is the nearest neighbor search method used by knnsearch as specified below. Value Description 'kdtree' Creates and uses a Kd-tree to find nearest neighbors. 'kdtree' is the default value when the number of columns in X is less than or equal to 10, X is not sparse, and the distance metric is 'euclidean', 'cityblock', 'manhattan', 'chebychev', or 'minkowski'. Otherwise, the default value is 'exhaustive'. This argument is only valid when the distance metric is one of the four aforementioned metrics. 'exhaustive' Uses the exhaustive search algorithm by computing the distance values from all the points in X to each point in Y. Cross Validation Options Name Value 'Crossval' Cross-validation flag specified as 'on' or 'off'. If 'on' is specified, a 10-fold cross validation is performed and a ClassificationPartitionedModel is returned in Mdl. To override this cross-validation setting, use only one of the following Name-Value pair arguments. 'CVPartition' A cvpartition object that specifies the type of cross-validation and the indexing for the training and validation sets. A ClassificationPartitionedModel is returned in Mdl and the trained model is stored in the Trained property. 'Holdout' Fraction of the data used for holdout validation, specified as a scalar value in the range [0,1]. When specified, a randomly selected percentage is reserved as validation data and the remaining set is used for training. The trained model is stored in the Trained property of the ClassificationPartitionedModel returned in Mdl. 'Holdout' partitioning attempts to ensure that each partition represents the classes proportionately. 'KFold' Number of folds to use in the cross-validated model, specified as a positive integer value greater than 1. When specified, then the data is randomly partitioned in k sets and for each set, the set is reserved as validation data while the remaining k-1 sets are used for training. The trained models are stored in the Trained property of the ClassificationPartitionedModel returned in Mdl. 'KFold' partitioning attempts to ensure that each partition represents the classes proportionately. 'Leaveout' Leave-one-out cross-validation flag specified as 'on' or 'off'. If 'on' is specified, then for each of the n observations (where n is the number of observations, excluding missing observations, specified in the NumObservations property of the model), one observation is reserved as validation data while the remaining observations are used for training. The trained models are stored in the Trained property of the ClassificationPartitionedModel returned in Mdl. See also: ClassificationKNN, ClassificationPartitionedModel, knnsearch, rangesearch, pdist2 # name: # type: sq_string # elements: 1 # length: 46 Fit a k-Nearest Neighbor classification model. # name: # type: sq_string # elements: 1 # length: 10 fitclinear # name: # type: sq_string # elements: 1 # length: 1320 statistics: Mdl = fitclinear (X, Y) statistics: Mdl = fitclinear (Tbl, ResponseVarName) statistics: Mdl = fitclinear (Tbl, formula) statistics: Mdl = fitclinear (Tbl, Y) statistics: Mdl = fitclinear (..., name, value) statistics: [Mdl, FitInfo] = fitclinear (...) Fit a linear binary classifier. Mdl = fitclinear (X, Y) returns a ClassificationLinear object fitted to the predictor data X and the two class response Y, where X is an NxP numeric matrix and Y has as many rows as X. Mdl = fitclinear (..., name, value) passes the given Name-Value pairs to the model. They are documented under ClassificationLinear, and the ones most often wanted are 'Learner', 'Regularization', 'Lambda', 'Solver' and 'ObservationsIn'. [Mdl, FitInfo] = fitclinear (...) also returns a structure describing the optimization: what it converged to, how far it got, and which tolerance stopped it. Its fields follow the solver, so a dual fit reports the dual variables and a mini-batch fit the batch it stopped on. Mdl = fitclinear (..., cvopt, value) returns a ClassificationPartitionedLinear instead when one of 'CrossVal', 'KFold', 'Holdout', 'Leaveout' and 'CVPartition' is given. A cross-validated model describes no single fit, so FitInfo is not available beside it. See also: ClassificationLinear, ClassificationKernel, fitckernel # name: # type: sq_string # elements: 1 # length: 31 Fit a linear binary classifier. # name: # type: sq_string # elements: 1 # length: 6 fitcnb # name: # type: sq_string # elements: 1 # length: 4286 statistics: Mdl = fitcnb (X, Y) statistics: Mdl = fitcnb (Tbl, ResponseVarName) statistics: Mdl = fitcnb (Tbl, formula) statistics: Mdl = fitcnb (Tbl, Y) statistics: Mdl = fitcnb (..., name, value) Fit a naive Bayes classification model. Mdl = fitcnb (X, Y) returns a naive Bayes classification model, Mdl, with X being the predictor data and Y the class labels of the observations in X. X must be a NxP numeric matrix of predictor data where rows correspond to observations and columns correspond to features or variables. Y is an Nx1 matrix or cell matrix containing the class labels of the corresponding predictor data in X. Y can be numeric, logical, a character array or a cell array of character vectors. Y must have the same number of rows as X. A naive Bayes model fits one univariate density to each predictor within each class, and treats the predictors as conditionally independent given the class. An observation's likelihood under a class is therefore the product of its per-predictor densities, and its posterior follows by Bayes' rule from the class prior. Mdl = fitcnb (..., name, value) returns a naive Bayes model with additional options specified by Name-Value pair arguments listed below. Model Parameters Name Value 'PredictorNames' A cell array of character vectors specifying the names of the predictors. The length of this array must match the number of columns in X. 'ResponseName' A character vector specifying the name of the response variable. 'ClassNames' Names of the classes in the class labels, Y, used for fitting the model. ClassNames are of the same type as the class labels in Y. Naming a subset of the classes keeps only the observations belonging to them. The model keeps the classes in this order; by default they are sorted. 'Prior' A numeric vector specifying the prior probability of each class, in the order of ClassNames, or the character vector 'empirical' (default) to take the class frequencies, or 'uniform' to give every class the same probability. A class whose prior is 0 is removed from the data before fitting, with a warning, as in MATLAB. 'Cost' A square numeric matrix of misclassification costs, where Cost(i,j) is the cost of classifying an observation of class i into class j. The default is one off the diagonal and zero on it. 'ScoreTransform' A character vector naming a transform applied to the posterior returned by predict, or a function handle taking and returning a matrix of the same size. The default is 'none'. 'DistributionNames' A character vector naming the distribution fitted to every predictor, or a cell array of character vectors naming one per predictor. Supported are 'normal' (default), 'kernel', 'mvmn' for a categorical predictor, and 'mn' for token counts. 'mn' describes the whole predictor vector at once and so cannot be named for only some predictors. 'Kernel' The smoothing kernel of the predictors fitted with a kernel density, one of 'normal' (default), 'box', 'epanechnikov' or 'triangle', given once for every predictor or once per predictor. 'Support' The support of the kernel densities, either 'unbounded' (default), 'positive', or a two element numeric vector giving finite bounds. 'Width' The bandwidth of the kernel densities, given as a scalar, as one value per predictor, as one per class, or as a matrix of one per class and predictor. By default each density chooses its own. 'Weights' A nonnegative single or double vector of observation weights, one per row of X. An empirical prior sums them per class; a normal density takes weighted means and standard deviations, a kernel density weighs its observations but chooses its bandwidth from them alone, and the multinomials count each observation by its weight. A row of zero or missing weight is left out. The model's W keeps the class of the weights, while every computation runs in double, so Prior is double where MATLAB returns single. The default is uniform. A predictor that takes one value throughout a class has no normal density to fit, and that combination of class and predictor is refused rather than answered. Only the combination is refused, not the model: giving that predictor a 'kernel' or a 'mvmn' distribution fits the same data, and leaves the other predictors normal. See also: ClassificationNaiveBayes # name: # type: sq_string # elements: 1 # length: 39 Fit a naive Bayes classification model. # name: # type: sq_string # elements: 1 # length: 7 fitcnet # name: # type: sq_string # elements: 1 # length: 7222 statistics: Mdl = fitcnet (X, Y) statistics: Mdl = fitcnet (Tbl, ResponseVarName) statistics: Mdl = fitcnet (Tbl, formula) statistics: Mdl = fitcnet (Tbl, Y) statistics: Mdl = fitcnet (..., name, value) Fit a Neural Network classification model. Mdl = fitcnet (X, Y) returns a Neural Network classification model, Mdl, with X being the predictor data, and Y the class labels of observations in X. X must be a NxP numeric matrix of predictor data where rows correspond to observations and columns correspond to features or variables. Y is Nx1 matrix or cell matrix containing the class labels of corresponding predictor data in X. Y can contain any type of categorical data. Y must have same numbers of rows as X. Mdl = fitcnet (..., name, value) returns a Neural Network classification model with additional options specified by Name-Value pair arguments listed below. Model Parameters Name Value 'Standardize' A boolean flag indicating whether the data in X should be standardized prior to training. 'CategoricalPredictors' The predictors whose values are levels, as indices, as a logical vector with one element per predictor, or as 'all'. Each is dummy coded in its place, one column of zeros and ones per level seen in training, named as in 'x1 == 2' in ExpandedPredictorNames, and the coded columns are not standardized. An observation holding a level the training data did not has no score. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. 'PredictorNames' A cell array of character vectors specifying the predictor variable names. The variable names are assumed to be in the same order as they appear in the training data X. 'ResponseName' A character vector specifying the name of the response variable. 'ClassNames' Names of the classes in the class labels, Y, used for fitting the Neural Network model. ClassNames are of the same type as the class labels in Y. The model keeps the classes in this order; by default they are sorted. 'Prior' A numeric vector specifying the prior probabilities for each class. The order of the elements in Prior corresponds to the order of the classes in ClassNames. A class whose prior is 0 is removed from the data before fitting, with a warning, as in MATLAB. 'LayerSizes' A vector of positive integers that defines the sizes of the fully connected layers in the neural network model. Each element in LayerSizes corresponds to the number of outputs for the respective fully connected layer in the neural network model. The default value is 10. 'LearningRate' A positive scalar value that defines the learning rate during the gradient descent. Default value is 0.003. A larger rate can drive every unit of a hidden layer negative, after which a rectifier passes no gradient and the network stops training. Applies only when 'Solver' is 'sgd'. 'Solver' A character vector naming the solver that trains the network, either 'lbfgs' or 'sgd'. The default is 'lbfgs', which minimizes the loss over the whole training set at once by limited-memory BFGS, as MATLAB does. It takes no learning rate, stops on the three tolerances below, and reaches a lower training loss in fewer passes over the data, though each of its iterations costs several passes where an epoch costs one. 'sgd' visits the samples one at a time and steps down the gradient of each, running for 'IterationLimit' epochs; it was the default before version 1.9.0. 'GradientTolerance' A nonnegative scalar. Training stops once the gradient's infinity norm falls to or below it, which is the quantity MATLAB tests too. The default is 1e-6. Applies only when 'Solver' is 'lbfgs'. 'StepTolerance' A nonnegative scalar. Training stops once the step's infinity norm falls to or below it, which is the quantity MATLAB tests too. The default is 1e-6. Applies only when 'Solver' is 'lbfgs'. 'LossTolerance' A real scalar. Training stops once the training loss falls to or below it. The test is on the loss itself and not on its change, matching MATLAB; pass -Inf to switch it off. The default is 1e-6. Applies only when 'Solver' is 'lbfgs'. 'Activations' A character vector or a cellstr vector specifying the activation functions for the hidden layers of the neural network (excluding the output layer). The available activation functions are 'linear', 'sigmoid', 'relu', 'tanh', 'softmax', 'lrelu', 'prelu', 'elu', 'gelu', and 'none'. The default value is 'relu'. 'OutputLayerActivation' A character vector specifying the activation function for the output layer of the neural network. The available activation functions are the same as for 'Activations'. The default value is 'softmax', which makes the returned scores a probability over the classes and trains the network against cross entropy; any other value trains it against the mean squared error. 'IterationLimit' A positive integer scalar that specifies the maximum number of training iterations. The default value is 1000. Under 'sgd' this counts epochs, under 'lbfgs' solver iterations. 'DisplayInfo' A boolean flag indicating whether to print information during training. Default is false. 'Weights' A nonnegative single or double vector of observation weights, one per row of X. An empirical prior sums them per class, and each class carries its prior spread over its observations in proportion to them; the network is trained on the loss so weighted, which is how a prior other than the empirical one enters the fit as well. Standardization weighs the observations the same way, and a row of zero or missing weight is left out. The model's W keeps the class of the weights, while every computation runs in double, so Prior is double where MATLAB returns single. The default is uniform. 'ScoreTransform' A character vector defining one of the following functions or a user defined function handle, which is used for transforming the prediction scores returned by the predict and resubPredict methods. Default value is 'none'. Value Description 'doublelogit' 1 ./ (1 + exp (-2 x x)) 'invlogit' log (x ./ (1 - x)) 'ismax' Sets the score for the class with the largest score to 1, and sets the scores for all other classes to 0 'logit' 1 ./ (1 + exp (-x)) 'none' x (no transformation) 'identity' x (no transformation) 'sign' -1 for x < 0, 0 for x = 0, 1 for x > 0 'symmetric' 2 x x - 1 'symmetricismax' Sets the score for the class with the largest score to 1, and sets the scores for all other classes to -1 'symmetriclogit' 2 ./ (1 + exp (-x)) - 1 The weights of each layer are drawn from a uniform range whose half-width is set by that layer's activation, and the scheme cannot be chosen: a rectifying activation ('relu', 'lrelu', 'prelu', 'elu', 'gelu') takes the He range sqrt (6 / fan_in), because it passes only half of its input, and the remaining activations take the Glorot range sqrt (6 / (fan_in + fan_out)), which accounts for the backward pass as well. A network whose layers do not share an activation is therefore built with both schemes. What each layer was given is reported by the LayerWeightsInitializers field of the fitted model's ModelParameters. See also: ClassificationNeuralNetwork # name: # type: sq_string # elements: 1 # length: 42 Fit a Neural Network classification model. # name: # type: sq_string # elements: 1 # length: 7 fitcsvm # name: # type: sq_string # elements: 1 # length: 9166 statistics: Mdl = fitcsvm (X, Y) statistics: Mdl = fitcsvm (Tbl, ResponseVarName) statistics: Mdl = fitcsvm (Tbl, formula) statistics: Mdl = fitcsvm (Tbl, Y) statistics: Mdl = fitcsvm (..., name, value) Fit a Support Vector Machine classification model. Mdl = fitcsvm (X, Y) returns a Support Vector Machine classification model, Mdl, with X being the predictor data, and Y the class labels of observations in X. X must be a NxP numeric matrix of predictor data where rows correspond to observations and columns correspond to features or variables. Y is Nx1 matrix or cell matrix containing the class labels of corresponding predictor data in X. Y can be numerical, logical, char array or cell array of character vectors. Y must have same number of rows as X. Mdl = fitcsvm (..., name, value) returns a Support Vector Machine model with additional options specified by Name-Value pair arguments listed below. Model Parameters Name Value 'Standardize' A boolean flag indicating whether the data in X should be standardized prior to training. 'CategoricalPredictors' The predictors whose values are levels, as indices, as a logical vector with one element per predictor, or as 'all'. Each is dummy coded in its place, one column of zeros and ones per level seen in training, named as in 'x1 == 2' in ExpandedPredictorNames, and the coded columns are not standardized. An observation holding a level the training data did not has no score. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. 'PredictorNames' A cell array of character vectors specifying the predictor variable names. The variable names are assumed to be in the same order as they appear in the training data X. 'ResponseName' A character vector specifying the name of the response variable. 'ClassNames' Names of the classes in the class labels, Y, used for fitting the kNN model. ClassNames are of the same type as the class labels in Y. The model keeps the classes in this order; by default they are sorted. 'SVMtype' Specifies the type of SVM used for training the ClassificationSVM model. By default, the type of SVM is defined by setting other parameters and/or by the data itself. Setting the 'SVMtype' parameter overrides the default behavior and it accepts the following options: Value Description 'C_SVC' It is the standard SVM formulation for classification tasks. It aims to find the optimal hyperplane that separates different classes by maximizing the margin between them while allowing some misclassifications. The parameter 'C' controls the trade-off between maximizing the margin and minimizing the classification error. It is the default type, unless otherwise specified. 'nu_SVC' It is a variation of the standard SVM that introduces a parameter ν (nu) as an upper bound on the fraction of margin errors and a lower bound on the fraction of support vectors. This formulation provides more control over the number of support vectors and the margin errors, making it useful for specific classification scenarios. It is the default type, when the 'OutlierFraction' parameter is set. 'one_class_SVM' It is used for anomaly detection and novelty detection tasks. It aims to separate the data points of a single class from the origin in a high-dimensional feature space. This method is particularly useful for identifying outliers or unusual patterns in the data. It is the default type, when the 'Nu' parameter is set or when there is a single class in Y. When 'one_class_SVM' is set by the 'SVMtype' pair argument, Y has no effect and any classes are ignored. Name Value 'OutlierFraction' The expected proportion of outliers in the training data, specified as a scalar value in the range [0,1]. When specified, the type of SVM model is switched to 'nu_SVC' and 'OutlierFraction' defines the ν (nu) parameter. 'KernelFunction' A character vector specifying the method for computing elements of the Gram matrix. The available kernel functions are 'gaussian' or 'rbf', 'linear', 'polynomial', and 'sigmoid'. For one-class learning, the default Kernel function is 'rbf'. For two-class learning the default is 'linear'. 'PolynomialOrder' A positive integer that specifies the order of polynomial in kernel function. The default value is 3. Unless the 'KernelFunction' is set to 'polynomial', this parameter is ignored. 'KernelScale' A positive scalar dividing every predictor before any kernel is applied, as MATLAB does, so that with u and v the divided predictors the kernels are u'v, exp (-||u - v||^2), (1 + u'v)^q and tanh (u'v + c), c being 'KernelOffset'. The default is 1. 'KernelOffset' A non-negative scalar, the constant c of the sigmoid kernel, which MATLAB does not have. MATLAB adds it to every element of the Gram matrix, which leaves the fitted model unchanged, so it changes no other kernel here. The default is 0. 'Weights' A single or double vector of nonnegative observation weights, one per row of X. Each observation's box constraint is n times BoxConstraint times its weight, the weights scaled so that each class carries its prior times the cost of misclassifying it. An empirical prior sums the weights per class, standardization uses weighted means and standard deviations, and a row of zero weight is left out. The model's W keeps the class of the weights, while every computation runs in double, so Prior is double where MATLAB returns single. 'Prior' 'empirical' (default), 'uniform', a vector with one element per class, or a structure with fields ClassNames and ClassProbs. A class whose prior is 0, or whose misclassification costs are all 0, is removed from the data before fitting, with a warning, as in MATLAB. 'Cost' A square matrix of misclassification costs, Cost(i,j) being the cost of classifying into class j an observation of class i, 1 - eye (2) by default; or a structure with fields ClassNames and ClassificationCosts, which names its own order of the classes and is permuted to the model's. 'BoxConstraint' A positive scalar that specifies the upper bound of the Lagrange multipliers, i.e. the parameter C, which is used for training 'C_SVC' and 'one_class_SVM' type of models. It determines the trade-off between maximizing the margin and minimizing the classification error. The default value for 'BoxConstraint' is 1. 'Nu' A positive scalar, in the range (0,1] that specifies the parameter ν (nu) for training 'nu_SVC' and 'one_class_SVM' type of models. Unless overridden by setting the 'SVMtype' parameter, setting the 'Nu' parameter always forces the training model type to 'one_class_SVM', in which case, the number of classes in Y is ignored. The default value for 'Nu' is 1. 'CacheSize' A positive scalar that specifies the memory requirements (in MB) for storing the Gram matrix. The default is 1000. 'Tolerance' A nonnegative scalar that specifies the tolerance of termination criterion. The default value is 1e-6. 'Shrinking' Specifies whether to use shrinking heuristics. It accepts either 0 or 1. The default value is 1. Cross Validation Options Name Value 'Crossval' Cross-validation flag specified as 'on' or 'off'. If 'on' is specified, a 10-fold cross validation is performed and a ClassificationPartitionedModel is returned in Mdl. To override this cross-validation setting, use only one of the following Name-Value pair arguments. 'CVPartition' A cvpartition object that specifies the type of cross-validation and the indexing for the training and validation sets. A ClassificationPartitionedModel is returned in Mdl and the trained model is stored in the Trained property. 'Holdout' Fraction of the data used for holdout validation, specified as a scalar value in the range [0,1]. When specified, a randomly selected percentage is reserved as validation data and the remaining set is used for training. The trained model is stored in the Trained property of the ClassificationPartitionedModel returned in Mdl. 'Holdout' partitioning attempts to ensure that each partition represents the classes proportionately. 'KFold' Number of folds to use in the cross-validated model, specified as a positive integer value greater than 1. When specified, then the data is randomly partitioned in k sets and for each set, the set is reserved as validation data while the remaining k-1 sets are used for training. The trained models are stored in the Trained property of the ClassificationPartitionedModel returned in Mdl. 'KFold' partitioning attempts to ensure that each partition represents the classes proportionately. 'Leaveout' Leave-one-out cross-validation flag specified as 'on' or 'off'. If 'on' is specified, then for each of the n observations (where n is the number of observations, excluding missing observations, specified in the NumObservations property of the model), one observation is reserved as validation data while the remaining observations are used for training. The trained models are stored in the Trained property of the ClassificationPartitionedModel returned in Mdl. See also: ClassificationSVM, ClassificationPartitionedModel, svmtrain, svmpredict # name: # type: sq_string # elements: 1 # length: 50 Fit a Support Vector Machine classification model. # name: # type: string # elements: 1 # length: 8 fitctree # name: # type: sq_string # elements: 1 # length: 5899 statistics: Mdl = fitctree (X, Y) statistics: Mdl = fitctree (Tbl, ResponseVarName) statistics: Mdl = fitctree (Tbl, formula) statistics: Mdl = fitctree (Tbl, Y) statistics: Mdl = fitctree (..., name, value) Fit a binary decision tree for classification. Mdl = fitctree (X, Y) grows a binary decision tree on the predictor data X and the class labels Y, and returns it as a ClassificationTree object. X must be a NxP numeric matrix of predictor data, where rows correspond to observations and columns to predictors. Y must be a Nx1 numeric or logical vector, a character array with one class name per row, or a cell array of character vectors, holding the class label of each observation in X. The class names come back in the type Y was given in. Mdl = fitctree (Tbl, ResponseVarName) takes both from the table Tbl: ResponseVarName names the variable holding the class labels and every other variable is a predictor. Mdl = fitctree (Tbl, formula) names them with a model formula instead, 'Y ~ x1 + x2', which holds main effects only and so takes no product, power or wildcard. Mdl = fitctree (Tbl, Y) takes the predictors from the table and the class labels from Y. A table variable holding levels rather than numbers, which is a categorical, a logical, a character array or a cell array of character vectors, is a categorical predictor without being named one, and 'CategoricalPredictors' adds to that set rather than replacing it. PredictorNames and ResponseName come from the table, and a level is coded at prediction as it was coded at fitting, so a table given to predict may hold only some of the levels. An observation whose class label is missing, which is missing every predictor, or whose weight is zero is dropped, and the rows kept are reported in RowsUsed. An observation missing only some of its predictors is kept: it descends the tree as far as the predictors it does carry allow and is answered there. Mdl = fitctree (..., name, value) takes the options below. Name Value 'AlgorithmForCategorical' How a node with three or more classes splits a categorical predictor: 'exact', 'pullleft', 'pca' or 'ovabyclass'. By default the exact search is taken when the node holds at most 'MaxNumCategories' levels, and otherwise the best split of 'ovabyclass', 'pca' and 'pullleft', leaving 'ovabyclass' out above four classes. 'CategoricalPredictors' The predictors whose values are levels, as indices, as a logical vector with one element per predictor, or as 'all'. Such a predictor is split into two sets of levels, and an observation whose level a node did not see stops there. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. 'ClassNames' The classes to fit, of the same type as Y. Observations of any other class are dropped. The model keeps the classes in this order; by default they are sorted. 'Cost' A square matrix with one row and column per class, where element (i,j) is the cost of classifying an observation of class i into class j, or a structure with fields ClassNames and ClassificationCosts. The default is 1 - eye (K). A non-default cost changes the shape of the tree, not only what it predicts. 'MaxNumCategories' A nonnegative integer, the most levels a node with three or more classes searches exactly by default. The default is 10. 'MaxNumSplits' A nonnegative integer, the largest number of branch nodes the tree may take. The default is one less than the number of observations. 'MergeLeaves' 'on' (default) or 'off'. When on, a pair of leaves whose parent is no worse than the two of them together is merged back into that parent. 'MinLeafSize' A positive integer, the fewest observations a leaf may hold. The default is 1. 'MinParentSize' A positive integer, the fewest observations a node must hold to be split. The default is 10. The value the fit uses is max (MinParentSize, 2 * MinLeafSize). 'NumVariablesToSample' A positive integer, the number of predictors each split is chosen from, drawn afresh at every node, or 'all' (default). A number no smaller than the number of predictors samples them all and is reported as 'all'. MATLAB also accepts a fractional number, which is refused here. 'PredictorNames' A cell array of character vectors naming the columns of X. 'Prior' 'empirical' (default), 'uniform', a numeric vector with one element per class, or a structure with fields ClassNames and ClassProbs. A class whose prior is 0, or whose misclassification costs are all 0, is removed from the data before fitting, with a warning, as in MATLAB. 'Prune' 'on' (default) or 'off'. When on, the cost complexity pruning sequence is estimated and reported in PruneList and PruneAlpha. The tree returned is the unpruned one either way; prune takes a subtree out of the sequence. 'PruneCriterion' 'error', the only criterion implemented. 'ResponseName' A character vector naming the response. The default is 'Y'. 'ScoreTransform' A character vector naming a transform to apply to the scores, or a function handle. The default is 'none'. 'SplitCriterion' 'gdi' (default), the Gini diversity index, or 'deviance', the cross entropy. 'Weights' A nonnegative single or double vector with one element per observation. The default is uniform. The model's W keeps the class of the weights, while every computation runs in double, so Prior and the predictions are double where MATLAB returns single. Surrogate splits and the 'twoing' split criterion are not implemented, and an option asking for one of them is refused rather than quietly ignored. On a node with three or more classes and more than 'MaxNumCategories' levels, the heuristic splits, the choice between equally good partitions and which side each set of levels takes may differ from MATLAB's. See also: ClassificationTree, treetrain, treepredict # name: # type: sq_string # elements: 1 # length: 46 Fit a binary decision tree for classification. # name: # type: sq_string # elements: 1 # length: 12 fitrensemble # name: # type: sq_string # elements: 1 # length: 3623 statistics: Mdl = fitrensemble (X, Y) statistics: Mdl = fitrensemble (Tbl, ResponseVarName) statistics: Mdl = fitrensemble (Tbl, formula) statistics: Mdl = fitrensemble (Tbl, Y) statistics: Mdl = fitrensemble (..., name, value) Fit an ensemble of regression trees. Mdl = fitrensemble (X, Y) grows 100 regression trees by LSBoost on the NxP predictor matrix X and the numeric response Y, and returns a RegressionEnsemble. With 'Method' set to 'Bag', or with LSBoost resampling, it returns a RegressionBaggedEnsemble. A row missing a predictor or the response is left out. LSBoost starts from a prediction of zero. Each tree is fitted, with the observation weights, to the residual of the trees before it, and the prediction grows by the learning rate times that tree's prediction, the learning rate being the tree's weight. The first tree therefore fits the response itself. Bag grows each tree on a sample drawn in proportion to the weights and averages them. LSBoost resamples when 'Resample' is 'on' or 'FResample' or 'Replace' is given. Each tree is then fitted to the residual of ceil (FResample * N) rows, drawn with replacement in proportion to the weights or without replacement uniformly, while the prediction and FitInfo run over every row, as in MATLAB R2024a. The ensemble is a RegressionBaggedEnsemble, which records the rows each tree drew and estimates the out-of-bag error. Name-Value arguments: Name Value 'Method' 'LSBoost' (default) or 'Bag'. 'NumLearningCycles' A positive integer, the number of trees to grow. The default is 100. 'Learners' 'tree' (default) or a template from templateTree, whose options override the defaults: for LSBoost MaxNumSplits 10, MinParentSize 10 and MinLeafSize 5; for Bag unlimited splits, MinParentSize 10, MinLeafSize 5 and NumVariablesToSample ceil (P / 3). 'LearnRate' A number greater than 0 and no greater than 1. The default is 1. LSBoost only. 'FResample' The share of the observations each tree draws, greater than 0 and no greater than 1. The default is 1. Given with LSBoost, the ensemble resamples. 'Replace' 'on' (default) or 'off', whether the trees draw with replacement. Given with LSBoost, the ensemble resamples. 'Resample' 'off' (default) or 'on', whether LSBoost resamples. Bag always does. 'NPrint' 'off' (default) or a positive integer n, to print a line after every n trees. 'Weights' A nonnegative single or double vector with one weight per observation. The default is uniform. The model's W keeps the class of the weights, while every computation runs in double, so the predictions are double where MATLAB returns single. 'PredictorNames' A cell array of character vectors naming the columns of X. 'ResponseName' The name of the response variable. 'ResponseTransform' 'none' (default), 'exp', 'log' or a function handle, applied to the predictions. MATLAB R2024a accepts only a function handle here, failing on the named transforms. 'CrossVal' set to 'on', 'KFold', 'Holdout', 'Leaveout' or 'CVPartition', only one of them, fits the ensemble and cross-validates it as crossval does, returning a RegressionPartitionedEnsemble. Binning and hyperparameter optimization are not implemented, and an option asking for one of them is refused. 'CategoricalPredictors', as indices, as a logical vector with one element per predictor, or as 'all', is passed on to every tree, which splits those predictors into sets of levels as fitrtree does. An ensemble is regularized and shrunk afterwards with the regularize, shrink and cvshrink methods. See also: RegressionEnsemble, RegressionBaggedEnsemble, CompactRegressionEnsemble, templateTree, TreeBagger # name: # type: sq_string # elements: 1 # length: 36 Fit an ensemble of regression trees. # name: # type: sq_string # elements: 1 # length: 7 fitrgam # name: # type: sq_string # elements: 1 # length: 7728 statistics: obj = fitrgam (X, Y) statistics: obj = fitrgam (Tbl, ResponseVarName) statistics: obj = fitrgam (Tbl, formula) statistics: obj = fitrgam (Tbl, Y) statistics: obj = fitrgam (X, Y, name, value) Fit a Generalized Additive Model (GAM) for regression. obj = fitrgam (X, Y) returns an object of class RegressionGAM, with matrix X containing the predictor data and vector Y containing the continuous response data. X must be a NxP numeric matrix of input data where rows correspond to observations and columns correspond to features or variables. X will be used to train the GAM model. Y must be Nx1 numeric vector containing the response data corresponding to the predictor data in X. Y must have same number of rows as X. Tbl may stand in place of X, the response named by one of its variables, written into a model formula holding main effects, 'Y ~ x1 + x2', or given beside it as Y. A variable holding levels rather than numbers is a categorical predictor without being named one, and 'CategoricalPredictors' adds to that set rather than replacing it. PredictorNames and ResponseName come from the table. obj = fitrgam (..., name, value) returns an object of class RegressionGAM with additional properties specified by Name-Value pair arguments listed below. Name Value 'FitMethod' A character vector selecting the weak learner, either 'boostedtrees' or 'splines'. The default is 'boostedtrees', which boosts one shallow decision tree per predictor and is the scheme MATLAB uses. 'splines' boosts a smoothing spline per predictor instead and is an Octave extension. The two take different options and an option meant for one is refused by the other rather than ignored, so the rows below say which engine each belongs to. 'predictors' Predictor Variable names, specified as a row vector cell of strings with the same length as the columns in X. If omitted, the program will generate default variable names (x1, x2, ..., xn) for each column in X. 'responsename' Response Variable Name, specified as a string. If omitted, the default value is 'Y'. 'formula' (spline option) a model specification given as a string in the form 'Y ~ terms' where Y represents the response variable and terms the predictor variables. The formula can be used to specify a subset of variables for training model. For example: 'Y ~ x1 + x2 + x3 + x4 + x1:x2 + x2:x3' specifies four linear terms for the first four columns of for predictor data, and x1:x2 and x2:x3 specify the two interaction terms for 1st-2nd and 3rd-4th columns respectively. Only these terms will be used for training the model, but X must have at least as many columns as referenced in the formula. If Predictor Variable names have been defined, then the terms in the formula must reference to those. When 'formula' is specified, all terms used for training the model are referenced in the IntMatrix field of the obj class object as a matrix containing the column indexes for each term including both the predictors and the interactions used. 'interactions' a logical matrix, a positive integer scalar, or the string 'all' for defining the interactions between predictor variables. When given a logical matrix, it must have the same number of columns as X and each row corresponds to a different interaction term combining the predictors indexed as true. Each interaction term is appended as a column vector after the available predictor column in X. When 'all' is defined, then all possible combinations of interactions are appended in X before training. At the moment, parsing a positive integer has the same effect as the 'all' option. When 'interactions' is specified, only the interaction terms appended to X are referenced in the IntMatrix field of the obj class object. 'knots' (spline option) a scalar or a row vector with the same columns as X. It defines the knots for fitting a polynomial when training the GAM. As a scalar, it is expanded to a row vector. The default value is 5, hence expanded to ones (1, columns (X)) * 5. You can parse a row vector with different number of knots for each predictor variable to be fitted with, although not recommended. 'order' (spline option) a scalar or a row vector with the same columns as X. It defines the order of the polynomial when training the GAM. As a scalar, it is expanded to a row vector. The default values is 3, hence expanded to ones (1, columns (X)) * 3. You can parse a row vector with different number of polynomial order for each predictor variable to be fitted with, although not recommended. 'dof' (spline option) a scalar or a row vector with the same columns as X. It defines the degrees of freedom for fitting a polynomial when training the GAM. As a scalar, it is expanded to a row vector. The default value is 8, hence expanded to ones (1, columns (X)) * 8. You can parse a row vector with different degrees of freedom for each predictor variable to be fitted with, although not recommended. 'tol' (spline option) a positive scalar to set the tolerance for convergence during training. By default, it is set to 1e-3. The rows above marked as spline options require 'FitMethod', 'splines'. The remaining options belong to the boosted-tree engine and require 'FitMethod', 'boostedtrees', which is the default. Name Value 'NumTreesPerPredictor' A positive integer, the number of boosting rounds of the predictor phase. It is a budget rather than a count: a fit that stops improving ends earlier and reports so. The default is 300. 'NumTreesPerInteraction' A positive integer, the same budget for the interaction phase. The default is 100. 'MaxNumSplitsPerPredictor' A positive integer, the largest number of splits any one predictor tree may make. The default is 1, which makes each tree a stump. 'MaxNumSplitsPerInteraction' The same limit for a tree over a pair of predictors. The default is 4. 'InitialLearnRateForPredictors' A value greater than 0 and at most 1, the step a round of the predictor phase starts at. A round that fails to improve the fit is retried at half the step, so this is an initial value rather than a fixed one. The default is 1. 'InitialLearnRateForInteractions' The same for the interaction phase. The default is 1. 'MaxPValue' A value between 0 and 1. A candidate pair of predictors is kept only if its interaction test gives a p-value no larger than this. The default is 1, which keeps every pair asked for. 'Verbose' A non-negative integer. Greater than zero prints a trace of the fit. The default is 0. 'NumPrint' A positive integer, how often the trace reports: the first round and then every NumPrint rounds. The default is 10. 'CategoricalPredictors' The predictors to treat as categorical: a vector of column indices, a logical vector with one element per predictor, or 'all'. A tree splits a categorical predictor into two sets of levels, and a level not seen in training predicts as a missing value. Its BinEdges and PairDetectionBinEdges are empty. The default is none. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. 'Weights' A single or double vector of non-negative observation weights, one per row of X. Only their proportions matter. The model's W keeps the class of the weights, while every computation runs in double, so the predictions are double where MATLAB returns single. You can parse either a 'formula' or an 'interactions' optional parameter. Parsing both parameters will result an error. Accordingly, you can only pass up to two parameters among 'knots', 'order', and 'dof' to define the required polynomial for training the GAM model. See also: RegressionGAM, regress, regress_gp # name: # type: sq_string # elements: 1 # length: 54 Fit a Generalized Additive Model (GAM) for regression. # name: # type: sq_string # elements: 1 # length: 6 fitrgp # name: # type: sq_string # elements: 1 # length: 942 statistics: Mdl = fitrgp (X, Y) statistics: Mdl = fitrgp (Tbl, ResponseVarName) statistics: Mdl = fitrgp (Tbl, formula) statistics: Mdl = fitrgp (Tbl, Y) statistics: Mdl = fitrgp (..., name, value) Fit a Gaussian process regression model. Mdl = fitrgp (X, Y) returns a RegressionGP object fitted to the predictor data X and the continuous response Y, where X is an NxP numeric matrix and Y an Nx1 numeric vector with as many rows as X. Mdl = fitrgp (..., name, value) passes the given Name-Value pairs to the model. They are documented under RegressionGP, and the ones most often wanted are 'KernelFunction', 'BasisFunction', 'Standardize', 'Sigma' and 'FitMethod'. When any of 'CrossVal', 'KFold', 'Holdout', 'Leaveout' or 'CVPartition' is given, a cross validated model is returned instead, as a RegressionPartitionedModel. Only one of them may be given at a time. See also: RegressionGP, CompactRegressionGP, RegressionPartitionedModel # name: # type: sq_string # elements: 1 # length: 40 Fit a Gaussian process regression model. # name: # type: sq_string # elements: 1 # length: 10 fitrkernel # name: # type: sq_string # elements: 1 # length: 1249 statistics: Mdl = fitrkernel (X, Y) statistics: Mdl = fitrkernel (Tbl, ResponseVarName) statistics: Mdl = fitrkernel (Tbl, formula) statistics: Mdl = fitrkernel (Tbl, Y) statistics: Mdl = fitrkernel (..., name, value) statistics: [Mdl, FitInfo] = fitrkernel (...) Fit a Gaussian kernel regression model. Mdl = fitrkernel (X, Y) returns a RegressionKernel object fitted to the predictor data X and the continuous response Y, where X is an NxP numeric matrix and Y an Nx1 numeric vector with as many rows as X. Mdl = fitrkernel (..., name, value) passes the given Name-Value pairs to the model. They are documented under RegressionKernel, and the ones most often wanted are 'Learner', 'Epsilon', 'NumExpansionDimensions', 'KernelScale', 'Lambda' and 'BoxConstraint'. [Mdl, FitInfo] = fitrkernel (...) also returns a structure describing the optimization: the objective it reached, the gradient it left, and the tolerances it was given. Mdl = fitrkernel (..., cvopt, value) returns a RegressionPartitionedKernel instead when one of 'CrossVal', 'KFold', 'Holdout', 'Leaveout' and 'CVPartition' is given. A cross-validated model describes no single fit, so FitInfo is not available beside it. See also: RegressionKernel, RegressionLinear, fitrlinear # name: # type: sq_string # elements: 1 # length: 39 Fit a Gaussian kernel regression model. # name: # type: sq_string # elements: 1 # length: 10 fitrlinear # name: # type: sq_string # elements: 1 # length: 1316 statistics: Mdl = fitrlinear (X, Y) statistics: Mdl = fitrlinear (Tbl, ResponseVarName) statistics: Mdl = fitrlinear (Tbl, formula) statistics: Mdl = fitrlinear (Tbl, Y) statistics: Mdl = fitrlinear (..., name, value) statistics: [Mdl, FitInfo] = fitrlinear (...) Fit a linear regression model. Mdl = fitrlinear (X, Y) returns a RegressionLinear object fitted to the predictor data X and the continuous response Y, where X is an NxP numeric matrix and Y an Nx1 numeric vector with as many rows as X. Mdl = fitrlinear (..., name, value) passes the given Name-Value pairs to the model. They are documented under RegressionLinear, and the ones most often wanted are 'Learner', 'Epsilon', 'Regularization', 'Lambda' and 'Solver'. [Mdl, FitInfo] = fitrlinear (...) also returns a structure describing the optimization: what it converged to, how far it got, and which tolerance stopped it. Its fields follow the solver, so a dual fit reports the dual variables and a mini-batch fit the batch it stopped on. Mdl = fitrlinear (..., cvopt, value) returns a RegressionPartitionedLinear instead when one of 'CrossVal', 'KFold', 'Holdout', 'Leaveout' and 'CVPartition' is given. A cross-validated model describes no single fit, so FitInfo is not available beside it. See also: RegressionLinear, RegressionKernel, fitrkernel # name: # type: sq_string # elements: 1 # length: 30 Fit a linear regression model. # name: # type: sq_string # elements: 1 # length: 7 fitrnet # name: # type: sq_string # elements: 1 # length: 6064 statistics: Mdl = fitrnet (X, Y) statistics: Mdl = fitrnet (Tbl, ResponseVarName) statistics: Mdl = fitrnet (Tbl, formula) statistics: Mdl = fitrnet (Tbl, Y) statistics: Mdl = fitrnet (..., name, value) Fit a neural network regression model. Mdl = fitrnet (X, Y) returns a neural network regression model, Mdl, with X being the predictor data and Y the continuous response of the observations in X. X must be an NxP numeric matrix of predictor data, where rows correspond to observations and columns to features or variables. Y must be an Nx1 numeric vector holding the response of the corresponding predictor data in X. Y must have the same number of rows as X. The network is trained against the mean squared error and its output layer applies the identity, so a prediction is an unrestricted real number. Use fitcnet where the response names a class rather than a quantity. Mdl = fitrnet (..., name, value) returns a neural network regression model with additional options specified by Name-Value pair arguments listed below. Model Parameters Name Value 'Standardize' A logical scalar indicating whether the data in X should be centred and scaled before training. The same transformation is applied by predict. The default is false. 'CategoricalPredictors' The predictors whose values are levels, as indices, as a logical vector with one element per predictor, or as 'all'. Each is dummy coded in its place, one column of zeros and ones per level seen in training, named as in 'x1 == 2' in ExpandedPredictorNames, and the coded columns are not standardized. An observation holding a level the training data did not has no prediction. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. 'PredictorNames' A cell array of character vectors specifying the predictor variable names, in the order they appear in X. 'ResponseName' A character vector specifying the name of the response variable. The default is 'Y'. 'ResponseTransform' A character vector naming one of 'none', 'identity', 'exp' or 'log', or a function handle of one argument, applied to the predicted response by predict and resubPredict. The default is 'none'. 'LayerSizes' A vector of positive integers defining the number of units in each fully connected hidden layer. The default value is 10, a single hidden layer of ten units. 'LearningRate' A positive scalar value that defines the learning rate during the gradient descent. Default value is 0.003. A larger rate can drive every unit of a hidden layer negative, after which a rectifier passes no gradient and the network stops training. Applies only when 'Solver' is 'sgd'. 'Solver' A character vector naming the solver that trains the network, either 'lbfgs' or 'sgd'. The default is 'lbfgs', which minimizes the loss over the whole training set at once by limited-memory BFGS, as MATLAB does. It takes no learning rate, stops on the three tolerances below, and reaches a lower training loss in fewer passes over the data, though each of its iterations costs several passes where an epoch costs one. 'sgd' visits the samples one at a time and steps down the gradient of each, running for 'IterationLimit' epochs; it was the default before version 1.9.0. 'GradientTolerance' A nonnegative scalar. Training stops once the gradient's infinity norm falls to or below it, which is the quantity MATLAB tests too. The default is 1e-6. Applies only when 'Solver' is 'lbfgs'. 'StepTolerance' A nonnegative scalar. Training stops once the step's infinity norm falls to or below it, which is the quantity MATLAB tests too. The default is 1e-6. Applies only when 'Solver' is 'lbfgs'. 'LossTolerance' A real scalar. Training stops once the training loss falls to or below it. The test is on the loss itself and not on its change, matching MATLAB; pass -Inf to switch it off. The default is 1e-6. Applies only when 'Solver' is 'lbfgs'. 'Activations' A character vector or a cellstr vector specifying the activation functions for the hidden layers of the neural network, excluding the output layer. The available activation functions are 'linear', 'none', 'sigmoid', 'relu', 'tanh', 'lrelu', 'prelu', 'elu' and 'gelu'. The default value is 'relu'. 'OutputLayerActivation' A character vector specifying the activation function for the output layer. The available functions are the same as for 'Activations'. The default value is 'none', the identity, which is what a regression output calls for; anything else bounds the prediction to that function's range. 'IterationLimit' A positive integer scalar specifying the maximum number of training iterations. The default value is 1000. Under 'sgd' this counts epochs, under 'lbfgs' solver iterations. 'DisplayInfo' A logical scalar indicating whether to print information during training. Default is false. 'Weights' A nonnegative single or double vector of observation weights, one per row of X. The network is trained on the loss weighted by them, standardization and the prediction for a row missing a predictor weigh the observations the same way, and a row of zero or missing weight is left out. The model's W keeps the class of the weights, while every computation runs in double, so the predictions are double where MATLAB returns single. The default is uniform. The weights of each layer are drawn from a uniform range whose half-width is set by that layer's activation, and the scheme cannot be chosen: a rectifying activation ('relu', 'lrelu', 'prelu', 'elu', 'gelu') takes the He range sqrt (6 / fan_in), because it passes only half of its input, and the remaining activations take the Glorot range sqrt (6 / (fan_in + fan_out)), which accounts for the backward pass as well. A network whose layers do not share an activation is therefore built with both schemes. What each layer was given is reported by the LayerWeightsInitializers field of the fitted model's ModelParameters. See also: RegressionNeuralNetwork, fitcnet, fcnntrain, fcnnpredict # name: # type: sq_string # elements: 1 # length: 38 Fit a neural network regression model. # name: # type: sq_string # elements: 1 # length: 7 fitrsvm # name: # type: sq_string # elements: 1 # length: 4504 statistics: Mdl = fitrsvm (X, Y) statistics: Mdl = fitrsvm (Tbl, ResponseVarName) statistics: Mdl = fitrsvm (Tbl, formula) statistics: Mdl = fitrsvm (Tbl, Y) statistics: Mdl = fitrsvm (..., name, value) Fit a support vector machine regression model. Mdl = fitrsvm (X, Y) returns a support vector regression model, Mdl, with X being the predictor data and Y the continuous response of the observations in X. X must be an NxP numeric matrix of predictor data, where rows correspond to observations and columns to features or variables. Y must be an Nx1 numeric vector holding the response of the corresponding predictor data in X. Y must have the same number of rows as X. The model is fitted by epsilon-insensitive regression: an error smaller than Epsilon costs nothing, so only the observations outside that tube become support vectors. Use fitcsvm where the response names a class rather than a quantity. Mdl = fitrsvm (..., name, value) returns a model with additional options specified by Name-Value pair arguments listed below. Model Parameters Name Value 'Standardize' A logical scalar indicating whether the data in X should be centred and scaled before training. The same transformation is applied by predict. The default is false. 'CategoricalPredictors' The predictors whose values are levels, as indices, as a logical vector with one element per predictor, or as 'all'. Each is dummy coded in its place, one column of zeros and ones per level seen in training, named as in 'x1 == 2' in ExpandedPredictorNames, and the coded columns are not standardized. An observation holding a level the training data did not has no prediction. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. 'PredictorNames' A cell array of character vectors specifying the predictor variable names, in the order they appear in X. 'ResponseName' A character vector specifying the name of the response variable. The default is 'Y'. 'ResponseTransform' A character vector naming one of 'none', 'identity', 'exp' or 'log', or a function handle of one argument, applied to the predicted response. The default is 'none'. 'Epsilon' A non-negative scalar, the half-width of the insensitive tube. The default is iqr (Y) / 13.49, a robust estimate of a tenth of the response's standard deviation, which is what MATLAB uses; where that is zero it falls back to 0.1. 'BoxConstraint' A positive scalar bounding the dual coefficients, the cost of an error outside the tube. The default is iqr (Y) / 1.349 for a Gaussian kernel, or 1 where that is zero, and 1 for any other kernel. 'Weights' A nonnegative single or double vector of observation weights, one per row of X. An observation's box constraint is n times BoxConstraint times its weight, the weights scaled to sum to one; standardization uses weighted means and standard deviations, and a row of zero or missing weight is left out. The model's W keeps the class of the weights, while every computation runs in double. The default is uniform. 'KernelFunction' A character vector naming the kernel, one of 'linear', the default, 'rbf', 'gaussian', 'polynomial' or 'sigmoid'. 'PolynomialOrder' A positive integer, the order of the polynomial kernel. The default is 3. It is ignored by every other kernel. 'KernelScale' A positive scalar dividing every predictor before any kernel is applied, as MATLAB does, so that with u and v the divided predictors the kernels are u'v, exp (-||u - v||^2), (1 + u'v)^q and tanh (u'v + c), c being 'KernelOffset'. The default is 1. 'KernelOffset' A non-negative scalar, the constant c of the sigmoid kernel, which MATLAB does not have. MATLAB adds it to every element of the Gram matrix, which leaves the fitted model unchanged, so it changes no other kernel here. The default is 0. 'SVMtype' A character vector selecting the formulation, either 'eps_svr', the default, or 'nu_svr'. MATLAB fits only the epsilon form; 'nu_svr' is an Octave extension. 'Nu' A scalar in (0, 1] used by 'nu_svr', bounding the fraction of support vectors. The default is 0.5. 'CacheSize' A positive scalar, the kernel cache in megabytes. The default is 1000. 'Tolerance' A non-negative scalar, the tolerance of the termination criterion. The default is 1e-6. 'Shrinking' Either 0 or 1, whether to use the shrinking heuristic. The default is 1. See also: RegressionSVM, fitcsvm, fitrnet, svmtrain, svmpredict # name: # type: sq_string # elements: 1 # length: 46 Fit a support vector machine regression model. # name: # type: string # elements: 1 # length: 8 fitrtree # name: # type: sq_string # elements: 1 # length: 4681 statistics: Mdl = fitrtree (X, Y) statistics: Mdl = fitrtree (Tbl, ResponseVarName) statistics: Mdl = fitrtree (Tbl, formula) statistics: Mdl = fitrtree (Tbl, Y) statistics: Mdl = fitrtree (..., name, value) Fit a binary decision tree for regression. Mdl = fitrtree (X, Y) grows a binary decision tree on the predictor data X and the response Y, and returns it as a RegressionTree object. X must be a NxP numeric matrix of predictor data, where rows correspond to observations and columns to predictors. Y must be a Nx1 numeric vector holding the response of each observation in X. Mdl = fitrtree (Tbl, ResponseVarName) takes both from the table Tbl: ResponseVarName names the variable holding the response and every other variable is a predictor. Mdl = fitrtree (Tbl, formula) names them with a model formula instead, 'Y ~ x1 + x2', which holds main effects only and so takes no product, power or wildcard. Mdl = fitrtree (Tbl, Y) takes the predictors from the table and the response from Y. A table variable holding levels rather than numbers, which is a categorical, a logical, a character array or a cell array of character vectors, is a categorical predictor without being named one, and 'CategoricalPredictors' adds to that set rather than replacing it. PredictorNames and ResponseName come from the table, and a level is coded at prediction as it was coded at fitting, so a table given to predict may hold only some of the levels. An observation whose response is missing, which is missing every predictor, or whose weight is zero is dropped, and the rows kept are reported in RowsUsed. An observation missing only some of its predictors is kept: it descends the tree as far as the predictors it does carry allow and is answered there. Mdl = fitrtree (..., name, value) takes the options below. Name Value 'CategoricalPredictors' The predictors whose values are levels, as indices, as a logical vector with one element per predictor, or as 'all'. Such a predictor is split into two sets of levels, the lower mean response on the left, and an observation whose level a node did not see stops there. MATLAB does not always keep that side, and equally good splits may be chosen differently. A predictor may be named rather than indexed, as a character matrix of one padded name per row, a string array or a cellstr; a name must match an entry of 'PredictorNames' exactly, its case included. 'MaxNumCategories' A nonnegative integer, recorded in ModelParameters. The default is 10. Ordering the levels by their mean response finds the best split whatever the number of levels. 'MaxNumSplits' A nonnegative integer, the largest number of branch nodes the tree may take. The default is one less than the number of observations. 'MergeLeaves' 'on' (default) or 'off'. When on, a pair of leaves whose parent is no worse than the two of them together is merged back into that parent. 'MinLeafSize' A positive integer, the fewest observations a leaf may hold. The default is 1. 'MinParentSize' A positive integer, the fewest observations a node must hold to be split. The default is 10. The value the fit uses is max (MinParentSize, 2 * MinLeafSize). 'NumVariablesToSample' A positive integer, the number of predictors each split is chosen from, drawn afresh at every node, or 'all' (default). A number no smaller than the number of predictors samples them all and is reported as 'all'. MATLAB also accepts a fractional number, which is refused here. 'PredictorNames' A cell array of character vectors naming the columns of X. 'Prune' 'on' (default) or 'off'. When on, the cost complexity pruning sequence is estimated and reported in PruneList and PruneAlpha. The tree returned is the unpruned one either way; prune takes a subtree out of the sequence. 'PruneCriterion' 'mse', the only criterion a regression tree has. 'QuadraticErrorTolerance' A positive scalar. A node whose squared error has fallen to this fraction of the root's is not split further. The default is 1e-6. 'ResponseName' A character vector naming the response. The default is 'Y'. 'ResponseTransform' A character vector naming a transform to apply to the predicted response, or a function handle. The default is 'none'. 'SplitCriterion' 'mse', the only criterion a regression tree has. 'Weights' A nonnegative single or double vector with one element per observation. The default is uniform. The model's W keeps the class of the weights, while every computation runs in double, so the predictions are double where MATLAB returns single. Surrogate splits are not implemented, and an option asking for them is refused rather than quietly ignored. See also: RegressionTree, fitctree, treetrain, treepredict # name: # type: sq_string # elements: 1 # length: 42 Fit a binary decision tree for regression. # name: # type: string # elements: 1 # length: 7 fscchi2 # name: # type: sq_string # elements: 1 # length: 4752 statistics: idx = fscchi2 (Tbl, ResponseVarName) statistics: idx = fscchi2 (Tbl, formula) statistics: idx = fscchi2 (Tbl, Y) statistics: idx = fscchi2 (X, Y) statistics: idx = fscchi2 (..., name, value) statistics: [idx, scores] = fscchi2 (...) Rank the predictors for classification with chi-square tests. idx = fscchi2 (X, Y) tests each predictor, a column of the matrix X, for independence from the class labels Y with a chi-square test, and ranks the predictors by the result. idx is a row vector of column indices, from the most important predictor to the least; predictors of equal score keep their order. X is a real matrix of type double or single with one row per observation. Y holds one class label per row of X: a numeric or logical vector, a categorical vector, a cell array of character vectors, a string array, or a character matrix with one row per observation. idx = fscchi2 (Tbl, ResponseVarName) takes the predictors and the class labels from the table Tbl: ResponseVarName names the variable holding the labels and every other variable is a predictor. idx = fscchi2 (Tbl, formula) names them with a model formula instead, 'Y ~ x1 + x2', which holds main effects only, and ranks only the predictors it names, idx indexing them in the order the table holds them. idx = fscchi2 (Tbl, Y) takes the predictors from the table and the labels from Y. A logical, categorical, text or string variable of the table is a categorical predictor; an ordinal categorical one is continuous, its categories in their order. [idx, scores] = fscchi2 (...) also returns the score of each predictor, a row vector in the order of the predictors, so that scores(idx) is in decreasing order. The score is -log (p), p being the p-value of the predictor's test, so a larger score marks a more important predictor; it is Inf where p is 0. A predictor that cannot separate the classes, being constant or wholly missing, scores 0, and so does every predictor when the labels hold a single class. A continuous predictor is first binned. Its n known values are sorted, the upper edge of bin k is the value at position ceil (k * n / NumBins), and a value goes to the first bin whose edge it does not exceed. The bins so hold equal numbers of observations as nearly as the ties allow, the first ones taking any remainder, and equal values always share a bin, which may leave fewer bins than asked for. A categorical predictor takes its categories as they are. The test is Pearson's chi-square test of independence on the table of weighted counts of the bins or categories against the classes, the empty ones left out. Each predictor is tested on the observations it is known for. Their weights are scaled within each class so that the class carries its prior probability, and over all so that they sum to the number of those observations. The empirical prior, the default, is the weighted share of each class among those observations, which leaves the weights as given. An observation whose class label is missing is left out of every test. The following Name-Value pairs are supported: Name Value 'CategoricalPredictors' The categorical predictors: a vector of their indices, a logical vector with one value per predictor, or 'all'. With a table they are added to those the table makes categorical, and they may also be named. The default is none for a matrix. 'ClassNames' The classes to use, in the type of Y. Observations of other classes are left out, and the order of the names is the order of a 'Prior' vector. The default is every class of Y, sorted. 'NumBins' The number of bins of a continuous predictor, a positive integer. The default is 10. 'Prior' The prior probabilities of the classes: 'empirical', the default; 'uniform'; a vector with one nonnegative value per class, in the order of the classes; or a structure with the fields ClassNames and ClassProbs. The values are normalized to sum to 1. 'UseMissing' false, the default, to test each predictor on the observations it is known for, or true to test it on every observation, the missing values of a categorical predictor forming one more category and those of a continuous predictor one more bin. MATLAB R2024a and R2026b document this option but ignore it in fscchi2, scoring every predictor alike either way; Octave does what they document. 'Weights' The observation weights, a nonnegative finite vector of type double or single with one value per row, or with a table the name of one of its variables, which is then no predictor. The default is equal weights. The option names are not case sensitive but must be given in full. With a matrix, the categorical predictors cannot be named, the predictors having no names, as in MATLAB. See also: fsrftest, relieff, sequentialfs, crosstab # name: # type: sq_string # elements: 1 # length: 61 Rank the predictors for classification with chi-square tests. # name: # type: string # elements: 1 # length: 8 fsrftest # name: # type: sq_string # elements: 1 # length: 4080 statistics: idx = fsrftest (Tbl, ResponseVarName) statistics: idx = fsrftest (Tbl, formula) statistics: idx = fsrftest (Tbl, Y) statistics: idx = fsrftest (X, Y) statistics: idx = fsrftest (..., name, value) statistics: [idx, scores] = fsrftest (...) Rank the predictors for regression with F-tests. idx = fsrftest (X, Y) tests each predictor, a column of the matrix X, with a one-way analysis of variance: whether the response Y has the same mean in every group of observations the predictor forms. It ranks the predictors by the result. idx is a row vector of column indices, from the most important predictor to the least; predictors of equal score keep their order. X is a real matrix of type double or single with one row per observation, and Y a real vector of type double or single with one response per row of X. idx = fsrftest (Tbl, ResponseVarName) takes the predictors and the response from the table Tbl: ResponseVarName names the variable holding the response and every other variable is a predictor. idx = fsrftest (Tbl, formula) names them with a model formula instead, 'Y ~ x1 + x2', which holds main effects only, and ranks only the predictors it names, idx indexing them in the order the table holds them. idx = fsrftest (Tbl, Y) takes the predictors from the table and the response from Y. A logical, categorical, text or string variable of the table is a categorical predictor; an ordinal categorical one is continuous, its categories in their order. [idx, scores] = fsrftest (...) also returns the score of each predictor, a row vector of type double in the order of the predictors, so that scores(idx) is in decreasing order. The score is -log (p), p being the p-value of the predictor's F-test, so a larger score marks a more important predictor; it is Inf where p is 0. A predictor that forms a single group, or as many groups as observations, scores 0, and so does every predictor when the response is constant. A continuous predictor is first binned. Its n known values are sorted, the upper edge of bin k is the value at position ceil (k * n / NumBins), and a value goes to the first bin whose edge it does not exceed. The bins so hold equal numbers of observations as nearly as the ties allow, the first ones taking any remainder, and equal values always share a bin, which may leave fewer bins than asked for. A categorical predictor takes its categories as they are. The F-test weighs each observation by its weight, scaling the weights not mattering. Each predictor is tested on the observations it is known for. An observation whose response is missing is left out of every test. The following Name-Value pairs are supported: Name Value 'CategoricalPredictors' The categorical predictors: a vector of their indices, a logical vector with one value per predictor, or 'all'. With a table they are added to those the table makes categorical, and they may also be named. The default is none for a matrix. 'NumBins' The number of bins of a continuous predictor, a positive integer. The default is 10. 'UseMissing' false, the default, to test each predictor on the observations it is known for, or true to test it on every observation, the missing values of a categorical predictor forming one more category and those of a continuous predictor one more bin. MATLAB R2024a and R2026b document this option but ignore it in fsrftest, scoring every predictor alike either way; Octave does what they document. 'Weights' The observation weights, a nonnegative finite vector of type double or single with one value per row, or with a table the name of one of its variables, which is then no predictor. The default is equal weights. The option names are not case sensitive but must be given in full. With a matrix, the categorical predictors cannot be named, the predictors having no names, as in MATLAB. The test is computed in double precision whatever the type of the data; MATLAB R2024a gives a different score for a response of type single than for the same values as double, which Octave does not. See also: fscchi2, relieff, sequentialfs, anova1 # name: # type: sq_string # elements: 1 # length: 48 Rank the predictors for regression with F-tests. # name: # type: sq_string # elements: 1 # length: 4 lime # name: # type: sq_string # elements: 1 # length: 2893 statistics: lime Local interpretable model-agnostic explanations for a fitted model. A lime object explains one prediction by fitting a simple model, a linear one or a shallow decision tree, over observations drawn around the query point and weighted by how near they lie to it. The simple model is readable where the fitted one is not, and it is accurate near the query point rather than everywhere. explainer = lime (Mdl) builds an explainer for the fitted model Mdl over the observations it was fitted on. A compact model keeps none, so it must be given them as X, and so must a function handle. The observations to fit the simple model on are drawn at once, and nothing is explained until a query point is given, either to the constructor as 'QueryPoint' or afterwards to fit. explainer = lime (Mdl, X) takes the observations the draw is fitted to as X, a real numeric matrix of one column per predictor. X may also be a table, and so may 'QueryPoint', 'CustomSyntheticData' and what fit is given. Where the model names its predictors the table is read by those names and not by the order its columns come in, so a column the model was not fitted on is passed over and a value holding a level is coded as that level was coded at fitting. A function handle names nothing, so a table given for one names the predictors itself and its columns are taken in the order they come. explainer = lime (fun, X) takes a function handle in place of a model. fun is called with a matrix of observations and answers with one column holding one value for each, and 'Type' must say whether those values are a response or a label. Name Value 'Type' Whether the model answers with a response, 'regression', or with a label, 'classification'. It is taken from a fitted model and is required for a function handle. 'DataLocality' Where the observations are drawn from: 'global', the default, fits the distribution to the whole of X; 'local' fits it to the 'NumNeighbors' observations nearest the query point, which must then be known. 'NumNeighbors' How many neighbours 'local' fits to, 1500 by default. 'NumSyntheticData' How many observations to draw, 5000 by default. 'CustomSyntheticData' Observations to use instead of drawing any, one row each. Nothing is drawn where it is given. 'CategoricalPredictors' The predictors whose values are levels, taken as by every learner of this package. It applies only to a function handle, a model being asked for its own. 'QueryPoint' The observation to explain. Given with 'NumImportantPredictors' it is explained at once, otherwise it is left to fit. 'NumImportantPredictors' How many predictors the simple model is fitted on. Every option fit takes may also be given here, in which case it stands as the default for every later fit. 'UseParallel' is not implemented and is refused rather than ignored. See also: shapley, partialDependence, PredictiveModel # name: # type: sq_string # elements: 1 # length: 67 Local interpretable model-agnostic explanations for a fitted model. # name: # type: sq_string # elements: 1 # length: 19 lime.BlackboxFitted # name: # type: sq_string # elements: 1 # length: 110 lime: property BlackboxFitted What the explained model answers at the query point This property is read-only. # name: # type: sq_string # elements: 1 # length: 51 What the explained model answers at the query point # name: # type: sq_string # elements: 1 # length: 18 lime.BlackboxModel # name: # type: sq_string # elements: 1 # length: 153 lime: property BlackboxModel The model being explained The fitted model, or the function handle, the explainer was built on. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 The model being explained # name: # type: sq_string # elements: 1 # length: 26 lime.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 175 lime: property CategoricalPredictors The categorical predictors The indices of the predictors whose values are levels, empty where there are none. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 The categorical predictors # name: # type: sq_string # elements: 1 # length: 17 lime.DataLocality # name: # type: sq_string # elements: 1 # length: 223 lime: property DataLocality Where the observations were drawn from 'global' where the distribution was fitted to the whole of X, 'local' where it was fitted to the neighbours of the query point. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Where the observations were drawn from # name: # type: sq_string # elements: 1 # length: 11 lime.Fitted # name: # type: sq_string # elements: 1 # length: 245 lime: property Fitted What the model answers over the drawn observations One response per observation for a regression model, or one label for a classifier, keeping the type of the response the model was fitted with. This property is read-only. # name: # type: sq_string # elements: 1 # length: 50 What the model answers over the drawn observations # name: # type: sq_string # elements: 1 # length: 24 lime.ImportantPredictors # name: # type: sq_string # elements: 1 # length: 181 lime: property ImportantPredictors The predictors the simple model was fitted on Their indices, in increasing order, empty until a query point is given. This property is read-only. # name: # type: sq_string # elements: 1 # length: 45 The predictors the simple model was fitted on # name: # type: sq_string # elements: 1 # length: 27 lime.NumImportantPredictors # name: # type: sq_string # elements: 1 # length: 204 lime: property NumImportantPredictors How many predictors the simple model was asked for Empty until a query point is given. Fewer may be used, where a predictor adds nothing. This property is read-only. # name: # type: sq_string # elements: 1 # length: 50 How many predictors the simple model was asked for # name: # type: sq_string # elements: 1 # length: 21 lime.NumSyntheticData # name: # type: sq_string # elements: 1 # length: 93 lime: property NumSyntheticData How many observations were drawn This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 How many observations were drawn # name: # type: sq_string # elements: 1 # length: 15 lime.QueryPoint # name: # type: sq_string # elements: 1 # length: 157 lime: property QueryPoint The observation explained One row holding one value per predictor, empty until a query point is given. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 The observation explained # name: # type: sq_string # elements: 1 # length: 16 lime.SimpleModel # name: # type: sq_string # elements: 1 # length: 476 lime: property SimpleModel The simple model fitted around the query point A RegressionLinear, ClassificationLinear, RegressionTree or ClassificationTree, fitted on the important predictors alone and weighted by nearness to the query point. For a classifier it answers 1 for the class the explained model predicted and -1 for any other, which is what makes it a single model however many classes there are. It is empty until a query point is given. This property is read-only. # name: # type: sq_string # elements: 1 # length: 46 The simple model fitted around the query point # name: # type: sq_string # elements: 1 # length: 22 lime.SimpleModelFitted # name: # type: sq_string # elements: 1 # length: 205 lime: property SimpleModelFitted What the simple model answers at the query point Where the two agree the simple model is worth reading; where they do not, it explains nothing. This property is read-only. # name: # type: sq_string # elements: 1 # length: 48 What the simple model answers at the query point # name: # type: sq_string # elements: 1 # length: 18 lime.SyntheticData # name: # type: sq_string # elements: 1 # length: 193 lime: property SyntheticData The observations the simple model is fitted over One row each, drawn around the query point or given outright as 'CustomSyntheticData'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 48 The observations the simple model is fitted over # name: # type: sq_string # elements: 1 # length: 9 lime.Type # name: # type: sq_string # elements: 1 # length: 110 lime: property Type What the model answers with 'regression' or 'classification'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 What the model answers with # name: # type: sq_string # elements: 1 # length: 6 lime.X # name: # type: sq_string # elements: 1 # length: 352 lime: property X The observations the draw was fitted to A real numeric matrix of one row per observation and one column per predictor. This property is read-only. Where the observations were given as a table, they are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the object. # name: # type: sq_string # elements: 1 # length: 39 The observations the draw was fitted to # name: # type: sq_string # elements: 1 # length: 8 lime.fit # name: # type: sq_string # elements: 1 # length: 2492 lime: obj = fit (obj, queryPoint, numImportantPredictors) lime: obj = fit (..., name, value) Fit the simple model around one query point. queryPoint is one row holding one value per predictor, or a table read by the names the explainer holds, and numImportantPredictors how many predictors the simple model is fitted on. Fewer are used where a predictor adds nothing, and ImportantPredictors says which were. Name Value 'Distance' How nearness to the query point is measured. Where the predictors hold numbers it is one of 'euclidean', the default, 'squaredeuclidean', 'seuclidean', 'mahalanobis', 'cityblock', 'minkowski', 'chebychev', 'cosine', 'correlation' and 'spearman'; where some hold levels it is 'goodall3', the default, or 'ofd'. Both count level frequencies over the drawn observations and the query point together. 'goodall3' is the 'goodall3' measure of nomdist; 'ofd' is one less the mean similarity of its 'of' measure, D / (1 + D) of the D it returns. A function handle is also taken, which MATLAB does not; see below. 'KernelWidth' How sharply the weight falls away with distance, from just above 0 to 1. The default is 0.75. 'SimpleModelType' 'linear', the default, or 'tree'. 'BetaTolerance' Relative tolerance the linear simple model is fitted to, 10^{-4} by default. 'Cov', 'P', 'Scale' Passed to the distance that takes them, as pdist2 takes them. The weight of a drawn observation is exp (-0.5 (d / D / w)^2), with d its distance from the query point and w the kernel width. D is the square root of the number of predictors for 'goodall3' and 'ofd', and otherwise the largest distance between two drawn observations. A predictor holding levels enters a linear simple model as one column for each level other than the query point's, saying whether an observation holds that level, so each coefficient is read against the query point's level. Where the predictors mix numbers and levels, 'goodall3' and 'ofd' score a predictor holding numbers by one less its absolute difference over its range, the query point included, and average that with the similarities of the predictors holding levels. MATLAB does so by a rule it does not document, which none of the constructions tried here reproduces, so on such data the explanation differs from MATLAB's. On levels alone 'goodall3' agrees with MATLAB; MATLAB does not document 'ofd' beyond scoring a match 0, and its coefficients differ from those here by up to 0.006 on the fixture measured. See also: lime, lime.plot # name: # type: sq_string # elements: 1 # length: 44 Fit the simple model around one query point. # name: # type: sq_string # elements: 1 # length: 9 lime.lime # name: # type: sq_string # elements: 1 # length: 445 lime: obj = lime (Mdl) lime: obj = lime (Mdl, X) lime: obj = lime (fun, X) lime: obj = lime (..., name, value) Build a local interpretable model-agnostic explainer. The arguments are those described for the class. The observations the simple model is fitted over are drawn here; where 'QueryPoint' and 'NumImportantPredictors' are both given the explanation is computed at once, otherwise it is left to fit. See also: lime, lime.fit, lime.plot # name: # type: sq_string # elements: 1 # length: 53 Build a local interpretable model-agnostic explainer. # name: # type: sq_string # elements: 1 # length: 9 lime.plot # name: # type: sq_string # elements: 1 # length: 329 lime: plot (obj) lime: f = plot (obj) Plot what the simple model says about the query point. A horizontal bar per important predictor, holding the coefficient of a linear simple model or the predictor importance of a tree, in the order the predictors come in rather than by size. f is the figure drawn. See also: lime, lime.fit # name: # type: sq_string # elements: 1 # length: 54 Plot what the simple model says about the query point. # name: # type: sq_string # elements: 1 # length: 17 partialDependence # name: # type: sq_string # elements: 1 # length: 2971 statistics: pd = partialDependence (Mdl, Vars) statistics: pd = partialDependence (Mdl, Vars, Labels) statistics: pd = partialDependence (..., Data) statistics: pd = partialDependence (fun, Vars, Data) statistics: pd = partialDependence (..., name, value) statistics: [pd, x, y] = partialDependence (...) Compute partial dependence. pd = partialDependence (Mdl, Vars) returns the partial dependence of the response of the regression model Mdl on the predictors named by Vars, averaged over the observations Mdl was fitted on. Vars names one predictor or two, by column index or by name, and a model that does not keep its observations must be given them as Data. pd = partialDependence (Mdl, Vars, Labels) does the same for a classification model, averaging the score of each class named by Labels rather than a response. Labels is required for such a model and refused for any other. pd = partialDependence (fun, Vars, Data) takes a function handle in place of a model. fun is called with a matrix of observations and answers with one row for each, and Data is then required. pd is a 1xnumX vector for a regression model varying one predictor and a numYxnumX matrix for two, where numX and numY count the query points of the first and second. For a classification model it gains a leading dimension of one row per class, giving numxnumX and numxnumYxnumX. x and y hold the query points of the first and the second predictor, y empty where only one was named. Where a predictor is categorical they are its levels. Name Value 'QueryPoints' The points to answer at, as a vector for one predictor and, for two, either a matrix of one column each or a cell holding a vector for each, which is how they may differ in length. The default is 100 points evenly spaced between the smallest and the largest value the predictor takes among the observations averaged over, and the levels themselves where it is categorical. 'NumObservationsToSample' How many observations to draw, without replacement, from those averaged over. The default is all of them, and so is a number reaching or exceeding how many there are. The default query points span what was drawn. 'CategoricalPredictors' The predictors whose values are levels, taken as by every learner of this package. It applies only to a function handle, a model being asked for its own. 'IncludeInteractions' Whether the interaction terms of a generalized additive model are included. It applies only to such a model, and the default is the model's own. 'IncludeIntercept' Whether the intercept of a generalized additive model is included, true by default. Excluding it takes the intercept off the result. 'OutputColumns' Which of the columns a function handle answers with are wanted, as indices or as 'all', which is the default. It applies only to a function handle. 'UseParallel' and 'PredictionForMissingValue' are not implemented and are refused rather than ignored. See also: plotPartialDependence, PredictiveModel # name: # type: sq_string # elements: 1 # length: 27 Compute partial dependence. # name: # type: sq_string # elements: 1 # length: 21 plotPartialDependence # name: # type: sq_string # elements: 1 # length: 1794 statistics: plotPartialDependence (Mdl, Vars) statistics: plotPartialDependence (Mdl, Vars, Labels) statistics: plotPartialDependence (..., Data) statistics: plotPartialDependence (fun, Vars, Data) statistics: plotPartialDependence (..., name, value) statistics: ax = plotPartialDependence (...) Plot partial dependence and individual conditional expectation. plotPartialDependence (Mdl, Vars) draws the partial dependence of the model Mdl on the predictors named by Vars: a line where one is named and a surface where two are. The arguments are those of partialDependence, which computes what is drawn, and Labels is required for a classification model in the same way. ax is the axes drawn into. Name Value 'Conditional' What to draw: 'none' (default) draws the partial dependence alone; 'absolute' draws a curve per observation, a marker where each one sits on its own curve, and the mean of those curves over them; 'centered' draws the same with every curve shifted to start at zero. Only one predictor may be varied, and a classification model may name only one class. 'Parent' The axes to draw into. The default is gca. Every other name-value pair is passed to partialDependence; see there for 'QueryPoints', 'NumObservationsToSample' and the rest. The line a conditional plot draws is the mean of the curves drawn with it, which is not always what partialDependence returns. A decision tree, an ensemble of bagged trees and a generalized additive model are answered over the distribution they were fitted on, while a curve belongs to one observation and must come from predict, so the two part company for those models whenever the observations are not the ones the model was fitted on. MATLAB R2024a draws it this way and so does this. See also: partialDependence, PredictiveModel # name: # type: sq_string # elements: 1 # length: 63 Plot partial dependence and individual conditional expectation. # name: # type: sq_string # elements: 1 # length: 7 shapley # name: # type: sq_string # elements: 1 # length: 3339 statistics: shapley Shapley values for a fitted model at one or more query points. A Shapley value says how much one predictor contributed to the deviation of a prediction from the average prediction. The values of a query point sum to that deviation exactly, which is the property the computation is built to keep. explainer = shapley (Mdl) builds an explainer for the fitted model Mdl over the observations it was fitted on. A compact model keeps none, so it must be given them as X, and so must a function handle. Nothing is computed until query points are given, either to the constructor as 'QueryPoints' or afterwards to fit. explainer = shapley (Mdl, X) takes the observations to average over as X, a real numeric matrix of one column per predictor. X may also be a table, and so may 'QueryPoints' and what fit is given. Where the model names its predictors the table is read by those names and not by the order its columns come in, so a column the model was not fitted on is passed over and a value holding a level is coded as that level was coded at fitting. A function handle names nothing, so a table given for one names the predictors itself and its columns are taken in the order they come. explainer = shapley (fun, X) takes a function handle in place of a model. fun is called with a matrix of observations and answers with one real numeric column holding one value for each, so an explainer built on a handle always has a single column of values. Name Value 'QueryPoints' The observations to explain, one per row, with one column per predictor. The default is none, which leaves the values unfitted. 'NumObservationsToSample' How many observations to draw, without replacement, from those averaged over, or 'all' for every one of them. The default is 100, and so is any number reaching or exceeding how many there are. A drawn sample makes the values differ from one call to the next; 'all' is what makes them reproducible. 'CategoricalPredictors' The predictors whose values are levels, taken as by every learner of this package. It applies only to a function handle, a model being asked for its own. 'MaxNumSubsets' How many predictor subsets at most to compute over, an integer above 1. The default is the lesser of 2^M, which is every subset of the M predictors, and 1024. Every subset gives the values exactly; fewer estimates them, and fewer than 2M+2 estimates them poorly enough to warn about. Giving it at all asks for the subsets, so a linear model or a decision tree that would otherwise be answered from its own structure is answered over them instead. 'Method' The algorithm, 'interventional' by default, which averages over the observations as they stand. 'conditional' averages instead over the tenth of them lying nearest the query point in the predictors being held, which stands in for conditioning on those predictors. It asks more of the data and is the dearer of the two. Observations at the same distance are taken in their order in the data, and distances equal to within rounding count as tied, so the result does not depend on the platform. MATLAB lets rounding decide such ties, so on data on a coarse grid the values can differ slightly from MATLAB's. 'UseParallel' is not implemented and is refused rather than ignored. See also: partialDependence, plotPartialDependence, PredictiveModel # name: # type: sq_string # elements: 1 # length: 62 Shapley values for a fitted model at one or more query points. # name: # type: sq_string # elements: 1 # length: 22 shapley.BlackboxFitted # name: # type: sq_string # elements: 1 # length: 338 shapley: property BlackboxFitted What the model answers at the query points The response of a regression model or the predicted label of a classifier, one for each query point, empty until query points are given. A label keeps the type of the response the model was fitted with, as everywhere in this package. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 What the model answers at the query points # name: # type: sq_string # elements: 1 # length: 21 shapley.BlackboxModel # name: # type: sq_string # elements: 1 # length: 156 shapley: property BlackboxModel The model being explained The fitted model, or the function handle, the explainer was built on. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 The model being explained # name: # type: sq_string # elements: 1 # length: 29 shapley.CategoricalPredictors # name: # type: sq_string # elements: 1 # length: 178 shapley: property CategoricalPredictors The categorical predictors The indices of the predictors whose values are levels, empty where there are none. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 The categorical predictors # name: # type: sq_string # elements: 1 # length: 17 shapley.Intercept # name: # type: sq_string # elements: 1 # length: 324 shapley: property Intercept The average prediction The mean of what the model answers over the observations averaged over, a scalar for a regression model or a function handle and one value per class for a classifier. The values of a query point sum to the deviation of its prediction from this. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 The average prediction # name: # type: sq_string # elements: 1 # length: 27 shapley.MeanAbsoluteShapley # name: # type: sq_string # elements: 1 # length: 257 shapley: property MeanAbsoluteShapley The mean absolute Shapley value of each predictor A table laid out as Shapley, holding the mean over the query points of the absolute values. With one query point it is their absolute value. This property is read-only. # name: # type: sq_string # elements: 1 # length: 49 The mean absolute Shapley value of each predictor # name: # type: sq_string # elements: 1 # length: 14 shapley.Method # name: # type: sq_string # elements: 1 # length: 727 shapley: property Method The algorithm the values were computed with A character vector. 'interventional-linear' where the model predicts a weighted sum of its predictors, which is answered from the weights alone; 'interventional-tree' for a decision tree and for an ensemble of them, which is answered leaf by leaf; 'interventional-kernel' for every other model, which enumerates every subset of the predictors where the budget allows it and estimates the values by weighted least squares where it does not; and 'conditional-kernel' where 'Method' asked for conditioning. A tree and a linear model are answered exactly however many predictors they have, where the budget stops the subsets at 1024. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 The algorithm the values were computed with # name: # type: sq_string # elements: 1 # length: 18 shapley.NumSubsets # name: # type: sq_string # elements: 1 # length: 224 shapley: property NumSubsets How many predictor subsets the values were computed over The lesser of what 'MaxNumSubsets' allowed and two raised to the number of predictors, which is every subset. This property is read-only. # name: # type: sq_string # elements: 1 # length: 56 How many predictor subsets the values were computed over # name: # type: sq_string # elements: 1 # length: 19 shapley.QueryPoints # name: # type: sq_string # elements: 1 # length: 171 shapley: property QueryPoints The observations explained A real numeric matrix of one row per query point, empty until query points are given. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 The observations explained # name: # type: sq_string # elements: 1 # length: 33 shapley.SampledObservationIndices # name: # type: sq_string # elements: 1 # length: 218 shapley: property SampledObservationIndices The rows of X that were averaged over A sorted column of indices into X, holding every row where 'NumObservationsToSample' did not draw a sample. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 The rows of X that were averaged over # name: # type: sq_string # elements: 1 # length: 15 shapley.Shapley # name: # type: sq_string # elements: 1 # length: 377 shapley: property Shapley The Shapley values A table of one row per predictor, holding the predictor names in Predictor and the values in Value for a regression model or a function handle, and in one variable per class, named after it, for a classifier. Each such variable holds one column per query point. It is empty until query points are given. This property is read-only. # name: # type: sq_string # elements: 1 # length: 18 The Shapley values # name: # type: sq_string # elements: 1 # length: 9 shapley.X # name: # type: sq_string # elements: 1 # length: 449 shapley: property X The observations averaged over A real numeric matrix of one row per observation and one column per predictor, either given outright or taken from the model. It is the whole of what was given, before any sampling. This property is read-only. Where the observations were given as a table, they are the coded matrix and not the table: a variable holding levels is stored as its level codes, and the coding is kept with the object. # name: # type: sq_string # elements: 1 # length: 30 The observations averaged over # name: # type: sq_string # elements: 1 # length: 16 shapley.boxchart # name: # type: sq_string # elements: 1 # length: 934 shapley: boxchart (obj) shapley: boxchart (obj, name, value) shapley: boxchart (ax, ...) shapley: b = boxchart (...) Draw a box chart of the Shapley values over the query points. One box per predictor, the least important at the bottom, spread over the query points the values were fitted at. The chart is titled 'Shapley Summary Plot' and lies horizontally. ax is the axes to draw into, the current one where none is given. b is the stats.chart.BoxChart drawn. Name Value 'NumImportantPredictors' How many predictors to draw, the ten most important by default. Whatever is left over is left out rather than summed, a box over a sum meaning nothing. 'ClassName' The one class to draw, for a classification model. The default is the first class of the model. 'JitterOutliers' Whether outlier markers are spread across the width of the box, 'off' by default. See also: shapley, shapley.plot, shapley.swarmchart, stats.chart.BoxChart # name: # type: sq_string # elements: 1 # length: 61 Draw a box chart of the Shapley values over the query points. # name: # type: sq_string # elements: 1 # length: 11 shapley.fit # name: # type: sq_string # elements: 1 # length: 307 shapley: obj = fit (obj, QueryPoints) Compute the Shapley values at the given query points. QueryPoints is a real numeric matrix of one row per query point and one column per predictor, or a table read by the names the explainer holds. The values already held are replaced, not added to. See also: shapley # name: # type: sq_string # elements: 1 # length: 53 Compute the Shapley values at the given query points. # name: # type: sq_string # elements: 1 # length: 12 shapley.plot # name: # type: sq_string # elements: 1 # length: 1041 shapley: plot (obj) shapley: plot (obj, name, value) shapley: plot (ax, ...) shapley: b = plot (...) Plot the Shapley values as a horizontal bar chart. One bar per predictor, the least important at the bottom. Over one query point the bars hold the values themselves and the chart is titled 'Shapley Explanation'; over several they hold the mean of the absolute values and it is titled 'Shapley Importance Plot'. ax is the axes to draw into, the current one where none is given. b holds one bar series per class drawn. Name Value 'NumImportantPredictors' How many predictors to draw on their own, the ten most important by default. Over several query points whatever is left over is drawn as one further bar holding its sum; over one query point it is left out. 'ClassNames' The classes to draw, for a classification model. The default is the predicted class over one query point and every class over several. 'QueryPointIndices' Which query points to draw, all of them by default. See also: shapley, shapley.boxchart, shapley.swarmchart # name: # type: sq_string # elements: 1 # length: 50 Plot the Shapley values as a horizontal bar chart. # name: # type: sq_string # elements: 1 # length: 22 shapley.plotDependence # name: # type: sq_string # elements: 1 # length: 1291 shapley: plotDependence (obj, predictor) shapley: plotDependence (obj, predictor, name, value) shapley: plotDependence (ax, ...) shapley: p = plotDependence (...) Draw the Shapley values of one predictor against its own values. predictor names or indexes the predictor. Where it holds numbers the chart is a scatter of its value at each query point against the value it was given there; where it holds levels the chart is a box of the values at each level. The chart is titled 'Shapley Dependence Plot' and its vertical axis is labelled after the predictor drawn. ax is the axes to draw into, the current one where none is given. p is the scatter object, or the stats.chart.BoxChart, drawn. Name Value 'ClassName' The one class to draw, for a classification model. The default is the first class of the model. 'ColorPredictor' A second predictor to colour the points by, none by default. Its values are read as they stand, the range of the axes carrying the scale, and a colour bar is drawn beside the chart. It applies only where the predictor drawn holds numbers. 'ColorMap' The colour map the colouring predictor is read through, as a name or as a matrix of one RGB triplet per row. The default is the one the axes already carries. See also: shapley, shapley.plot, shapley.swarmchart # name: # type: sq_string # elements: 1 # length: 64 Draw the Shapley values of one predictor against its own values. # name: # type: sq_string # elements: 1 # length: 15 shapley.shapley # name: # type: sq_string # elements: 1 # length: 337 shapley: obj = shapley (Mdl) shapley: obj = shapley (Mdl, X) shapley: obj = shapley (fun, X) shapley: obj = shapley (..., name, value) Build a Shapley explainer. The arguments are those described for the class. Where 'QueryPoints' is given the values are computed at once, otherwise they are left to fit. See also: shapley, shapley.fit # name: # type: sq_string # elements: 1 # length: 26 Build a Shapley explainer. # name: # type: sq_string # elements: 1 # length: 18 shapley.swarmchart # name: # type: sq_string # elements: 1 # length: 1189 shapley: swarmchart (obj) shapley: swarmchart (obj, name, value) shapley: swarmchart (ax, ...) shapley: s = swarmchart (...) Draw a swarm chart of the Shapley values over the query points. One row of points per predictor, the least important at the bottom, one point per query point spread vertically by how crowded its neighbourhood is. Each point is coloured by the value the predictor takes at that query point, the least of them at one end of the colour map and the greatest at the other. The chart is titled 'Shapley Summary Plot'. ax is the axes to draw into, the current one where none is given. s holds one scatter object per predictor drawn. Name Value 'NumImportantPredictors' How many predictors to draw, the ten most important by default. 'ClassName' The one class to draw, for a classification model. The default is the first class of the model. 'YJitter' How the points of a row are spread, 'density' by default, or 'rand', 'randn' or 'none'. 'ColorMap' The colour map the predictor values are read through, as a name or as a matrix of one RGB triplet per row. The default is the one the axes already carries. See also: shapley, shapley.plot, shapley.boxchart, swarmchart # name: # type: sq_string # elements: 1 # length: 63 Draw a swarm chart of the Shapley values over the query points. # name: # type: sq_string # elements: 1 # length: 20 templateDiscriminant # name: # type: sq_string # elements: 1 # length: 1341 statistics: T = templateDiscriminant () statistics: T = templateDiscriminant (name, value) Create a template for a discriminant analysis classifier. T = templateDiscriminant () returns a template carrying the default options of ClassificationDiscriminant. A template names a learner and the options it is to be fitted with, without fitting anything: it is given to a function that fits many models, such as fitcecoc, which uses it for every binary learner it trains. T = templateDiscriminant (name, value) also stores the given options. They are the name-value arguments of fitcdiscr, and any of them may be given here instead. T = templateDiscriminant ('DiscrimType', 'quadratic'); Mdl = fitcecoc (X, Y, 'Learners', T); T is a structure carrying Method, Type and one field per option given. Deviation from MATLAB MATLAB returns an object of a class whose name we cannot use, which has no public properties and one method this package declines package wide, so a structure carries everything a user can observe. This is what ModelParameters already does throughout the package. An option name is not checked here. ClassificationDiscriminant owns the list of options it takes and checks it when the template is used, so a name it does not know is refused then rather than now. See also: fitcecoc, ClassificationDiscriminant, fitcdiscr # name: # type: sq_string # elements: 1 # length: 57 Create a template for a discriminant analysis classifier. # name: # type: sq_string # elements: 1 # length: 16 templateEnsemble # name: # type: sq_string # elements: 1 # length: 1863 statistics: T = templateEnsemble (Method, NLearn, Learners) statistics: T = templateEnsemble (..., name, value) Create a template for an ensemble learner. T = templateEnsemble (Method, NLearn, Learners) returns a template for an ensemble grown by Method from NLearn learners, each fitted as Learners says. A template names a learner and the options it is to be fitted with, without fitting anything: it is given to fitcecoc, which grows one such ensemble for every binary learner it trains. Method is one of the methods of fitcensemble or fitrensemble, in any letter case. NLearn is the number of learning cycles and Learners a learner name, such as 'tree', or a template of one, such as templateTree returns; they are the 'NumLearningCycles' and 'Learners' options of fitcensemble. T = templateEnsemble (..., name, value) also stores the given options. They are the name-value arguments of fitcensemble, such as 'LearnRate'. T = templateEnsemble ('GentleBoost', 50, templateTree ('MaxNumSplits', 1)); Mdl = fitcecoc (X, Y, 'Learners', T); T is a structure carrying Method, Type, LearnerTemplates, NLearn and one field per option given. Type is 'regression' for LSBoost and 'classification' for every other method; a 'Type' option may choose it for Bag, which serves both. Deviation from MATLAB MATLAB returns an object of a class whose name we cannot use, which has no public properties and one method this package declines package wide, so a structure carries everything a user can observe. This is what ModelParameters already does throughout the package. Only the method is checked here. The ensemble owns the list of options it takes and checks them, NLearn and Learners included, when the template is used, so a value it refuses is refused then rather than now. MATLAB checks 'LearnRate' already here. See also: fitcecoc, fitcensemble, templateTree # name: # type: sq_string # elements: 1 # length: 42 Create a template for an ensemble learner. # name: # type: sq_string # elements: 1 # length: 11 templateKNN # name: # type: sq_string # elements: 1 # length: 1277 statistics: T = templateKNN () statistics: T = templateKNN (name, value) Create a template for a nearest neighbour classifier. T = templateKNN () returns a template carrying the default options of ClassificationKNN. A template names a learner and the options it is to be fitted with, without fitting anything: it is given to a function that fits many models, such as fitcecoc, which uses it for every binary learner it trains. T = templateKNN (name, value) also stores the given options. They are the name-value arguments of fitcknn, and any of them may be given here instead. T = templateKNN ('NumNeighbors', 5, 'Distance', 'cityblock'); Mdl = fitcecoc (X, Y, 'Learners', T); T is a structure carrying Method, Type and one field per option given. Deviation from MATLAB MATLAB returns an object of a class whose name we cannot use, which has no public properties and one method this package declines package wide, so a structure carries everything a user can observe. This is what ModelParameters already does throughout the package. An option name is not checked here. ClassificationKNN owns the list of options it takes and checks it when the template is used, so a name it does not know is refused then rather than now. See also: fitcecoc, ClassificationKNN, fitcknn # name: # type: sq_string # elements: 1 # length: 53 Create a template for a nearest neighbour classifier. # name: # type: sq_string # elements: 1 # length: 14 templateKernel # name: # type: sq_string # elements: 1 # length: 1337 statistics: T = templateKernel () statistics: T = templateKernel (name, value) Create a template for a kernel classifier using random feature expansion. T = templateKernel () returns a template carrying the default options of ClassificationKernel. A template names a learner and the options it is to be fitted with, without fitting anything: it is given to a function that fits many models, such as fitcecoc, which uses it for every binary learner it trains. T = templateKernel (name, value) also stores the given options. They are the name-value arguments of fitckernel, and any of them may be given here instead. T = templateKernel ('Learner', 'logistic', 'NumExpansionDimensions', 128); Mdl = fitcecoc (X, Y, 'Learners', T); T is a structure carrying Method, Type and one field per option given. Deviation from MATLAB MATLAB returns an object of a class whose name we cannot use, which has no public properties and one method this package declines package wide, so a structure carries everything a user can observe. This is what ModelParameters already does throughout the package. An option name is not checked here. ClassificationKernel owns the list of options it takes and checks it when the template is used, so a name it does not know is refused then rather than now. See also: fitcecoc, ClassificationKernel, fitckernel # name: # type: sq_string # elements: 1 # length: 73 Create a template for a kernel classifier using random feature expansion. # name: # type: sq_string # elements: 1 # length: 14 templateLinear # name: # type: sq_string # elements: 1 # length: 1317 statistics: T = templateLinear () statistics: T = templateLinear (name, value) Create a template for a linear classifier for high dimensional data. T = templateLinear () returns a template carrying the default options of ClassificationLinear. A template names a learner and the options it is to be fitted with, without fitting anything: it is given to a function that fits many models, such as fitcecoc, which uses it for every binary learner it trains. T = templateLinear (name, value) also stores the given options. They are the name-value arguments of fitclinear, and any of them may be given here instead. T = templateLinear ('Learner', 'logistic', 'Lambda', 1e-4); Mdl = fitcecoc (X, Y, 'Learners', T); T is a structure carrying Method, Type and one field per option given. Deviation from MATLAB MATLAB returns an object of a class whose name we cannot use, which has no public properties and one method this package declines package wide, so a structure carries everything a user can observe. This is what ModelParameters already does throughout the package. An option name is not checked here. ClassificationLinear owns the list of options it takes and checks it when the template is used, so a name it does not know is refused then rather than now. See also: fitcecoc, ClassificationLinear, fitclinear # name: # type: sq_string # elements: 1 # length: 68 Create a template for a linear classifier for high dimensional data. # name: # type: sq_string # elements: 1 # length: 18 templateNaiveBayes # name: # type: sq_string # elements: 1 # length: 1312 statistics: T = templateNaiveBayes () statistics: T = templateNaiveBayes (name, value) Create a template for a naive Bayes classifier. T = templateNaiveBayes () returns a template carrying the default options of ClassificationNaiveBayes. A template names a learner and the options it is to be fitted with, without fitting anything: it is given to a function that fits many models, such as fitcecoc, which uses it for every binary learner it trains. T = templateNaiveBayes (name, value) also stores the given options. They are the name-value arguments of fitcnb, and any of them may be given here instead. T = templateNaiveBayes ('DistributionNames', 'kernel'); Mdl = fitcecoc (X, Y, 'Learners', T); T is a structure carrying Method, Type and one field per option given. Deviation from MATLAB MATLAB returns an object of a class whose name we cannot use, which has no public properties and one method this package declines package wide, so a structure carries everything a user can observe. This is what ModelParameters already does throughout the package. An option name is not checked here. ClassificationNaiveBayes owns the list of options it takes and checks it when the template is used, so a name it does not know is refused then rather than now. See also: fitcecoc, ClassificationNaiveBayes, fitcnb # name: # type: sq_string # elements: 1 # length: 47 Create a template for a naive Bayes classifier. # name: # type: sq_string # elements: 1 # length: 11 templateSVM # name: # type: sq_string # elements: 1 # length: 1272 statistics: T = templateSVM () statistics: T = templateSVM (name, value) Create a template for a support vector machine. T = templateSVM () returns a template carrying the default options of ClassificationSVM. A template names a learner and the options it is to be fitted with, without fitting anything: it is given to a function that fits many models, such as fitcecoc, which uses it for every binary learner it trains. T = templateSVM (name, value) also stores the given options. They are the name-value arguments of fitcsvm, and any of them may be given here instead. T = templateSVM ('KernelFunction', 'rbf', 'BoxConstraint', 2); Mdl = fitcecoc (X, Y, 'Learners', T); T is a structure carrying Method, Type and one field per option given. Deviation from MATLAB MATLAB returns an object of a class whose name we cannot use, which has no public properties and one method this package declines package wide, so a structure carries everything a user can observe. This is what ModelParameters already does throughout the package. An option name is not checked here. ClassificationSVM owns the list of options it takes and checks it when the template is used, so a name it does not know is refused then rather than now. See also: fitcecoc, ClassificationSVM, fitcsvm # name: # type: sq_string # elements: 1 # length: 47 Create a template for a support vector machine. # name: # type: sq_string # elements: 1 # length: 12 templateTree # name: # type: sq_string # elements: 1 # length: 1272 statistics: T = templateTree () statistics: T = templateTree (name, value) Create a template for a binary decision tree. T = templateTree () returns a template carrying the default options of ClassificationTree. A template names a learner and the options it is to be fitted with, without fitting anything: it is given to a function that fits many models, such as fitcecoc, which uses it for every binary learner it trains. T = templateTree (name, value) also stores the given options. They are the name-value arguments of fitctree, and any of them may be given here instead. T = templateTree ('MaxNumSplits', 5, 'MinLeafSize', 3); Mdl = fitcecoc (X, Y, 'Learners', T); T is a structure carrying Method, Type and one field per option given. Deviation from MATLAB MATLAB returns an object of a class whose name we cannot use, which has no public properties and one method this package declines package wide, so a structure carries everything a user can observe. This is what ModelParameters already does throughout the package. An option name is not checked here. ClassificationTree owns the list of options it takes and checks it when the template is used, so a name it does not know is refused then rather than now. See also: fitcecoc, ClassificationTree, fitctree # name: # type: sq_string # elements: 1 # length: 45 Create a template for a binary decision tree. statistics-release-2.1.2/inst/Machine_Learning/fitSVMPosterior.m000066400000000000000000000432541527311104300247260ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{ScoreSVMModel} =} fitSVMPosterior (@var{SVMModel}) ## @deftypefnx {statistics} {@var{ScoreSVMModel} =} fitSVMPosterior (@var{SVMModel}, @var{name}, @var{value}) ## @deftypefnx {statistics} {@var{ScoreSVMModel} =} fitSVMPosterior (@var{SVMModel}, @var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{ScoreSVMModel} =} fitSVMPosterior (@var{SVMModel}, @var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{ScoreSVMModel} =} fitSVMPosterior (@var{SVMModel}, @var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{ScoreSVMModel} =} fitSVMPosterior (@var{SVMModel}, @var{Tbl}) ## @deftypefnx {statistics} {[@var{ScoreSVMModel}, @var{ScoreTransform}] =} fitSVMPosterior (@dots{}) ## ## Fit the transformation from SVM scores to posterior probabilities. ## ## @code{@var{ScoreSVMModel} = fitSVMPosterior (@var{SVMModel})} returns the ## two-class SVM classifier @var{SVMModel} with the transformation from the ## score of the second class, @code{@var{SVMModel}.ClassNames(2)}, to its ## posterior probability stored in its @qcode{ScoreTransform} property, so ## that @code{predict}, or @code{kfoldPredict} for a cross-validated model, ## returns posterior probabilities, the two columns summing to 1. ## @var{SVMModel} is a @code{ClassificationSVM}, a ## @code{CompactClassificationSVM} or a @code{ClassificationPartitionedModel} ## cross-validating an SVM, and @var{ScoreSVMModel} is of the same class. ## ## The transformation is fitted to scores of observations the model was not ## trained on: ## ## @itemize ## @item For a @code{ClassificationSVM}, scores from cross-validating it, ## 10-fold unless one of the options below says otherwise. ## @item For a @code{ClassificationPartitionedModel}, its own out-of-fold ## scores, from the partition it was built with; no option may be given. ## @item With predictors @var{X} and their classes @var{Y}, the scores the ## model gives @var{X}, without cross-validation; a ## @code{CompactClassificationSVM} holds no training data and needs them. ## @var{X} may also be a table @var{Tbl}, matched to the predictors by ## variable name, with the classes in the variable @var{ResponseVarName} ## names, in @var{Y}, or in the variable the model was fitted on. Rows whose ## score is @qcode{NaN} are left out. ## @end itemize ## ## @multitable @columnfractions 0.28 0.7 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'KFold'} @tab The number of folds, an integer greater ## than 1. The default is 10. ## ## @item @qcode{'Holdout'} @tab The fraction of the data held out, a ## scalar in the range @math{(0,1)}. ## ## @item @qcode{'Leaveout'} @tab @qcode{'on'} for leave-one-out ## cross-validation, or @qcode{'off'} (default). ## ## @item @qcode{'CVPartition'} @tab A @qcode{cvpartition} object over the ## observations of @var{SVMModel}. ## @end multitable ## ## Only one of these options may be given, and only with a ## @code{ClassificationSVM} and no data. The transformation is one of three: ## ## @itemize ## @item A sigmoid @code{1 / (1 + exp (A * s + B))} of the score @math{s} ## when the scores of the two classes overlap, with @math{A} and @math{B} ## fitted by maximum likelihood, as a logistic regression of the class on the ## score. Neither the observation weights nor the prior enter the fit, as in ## MATLAB. ## @item A step function when the scores separate the classes: 0 below the ## largest score of the first class, 1 above the smallest score of the second, ## and @code{@var{SVMModel}.Prior(2)} between, with a warning. ## @item A constant when one class has prior probability 0, posterior 1 for ## the other class. ## @end itemize ## ## A @qcode{ScoreTransform} already set on @var{SVMModel} is reset to ## @qcode{'none'} first, with a warning. The property holds the ## transformation as @qcode{'@@(S)sigmoid(S,A,B)'}, ## @qcode{'@@(S)step(S,LowerBound,UpperBound,PositiveClassProbability)'} or ## @qcode{'@@(S)constant(S,K)'}, @math{K} being the index of the predicted ## class. Each parameter is written exactly, in the fewest significant ## digits that read back to it, where MATLAB shows seven, so a model saved and ## reloaded predicts as before. The folds of a cross-validated model keep ## @qcode{'none'}, the transformation being applied to the scores they give ## together. ## ## @code{[@var{ScoreSVMModel}, @var{ScoreTransform}] = fitSVMPosterior ## (@dots{})} also returns the parameters in a structure whose field ## @qcode{Type} is @qcode{'sigmoid'}, with fields @qcode{Slope} and ## @qcode{Intercept}, @qcode{'step'}, with fields @qcode{LowerBound}, ## @qcode{UpperBound} and @qcode{PositiveClassProbability}, or ## @qcode{'constant'}, with the field @qcode{PredictedClass}. ## ## Reference: J. Platt. Probabilistic outputs for support vector machines and ## comparisons to regularized likelihood methods. In @emph{Advances in Large ## Margin Classifiers}, MIT Press, 2000. ## ## @seealso{ClassificationSVM.fitPosterior, ## CompactClassificationSVM.fitPosterior, fitcsvm, cvpartition} ## @end deftypefn function [ScoreSVMModel, ScoreTransform] = fitSVMPosterior (SVMModel, varargin) if (nargin < 1) print_usage (); endif ispart = isa (SVMModel, 'ClassificationPartitionedModel'); isfull = isa (SVMModel, 'ClassificationSVM'); if (! (isfull || isa (SVMModel, 'CompactClassificationSVM') || (ispart && strcmp (SVMModel.CrossValidatedModel, 'SVM')))) error (strcat ("fitSVMPosterior: SVMMODEL must be an SVM classifier", ... " or a cross-validated SVM classifier.")); endif if (classCount (SVMModel.ClassNames) != 2) error ("fitSVMPosterior: SVMMODEL must have exactly two classes."); endif ## Data given after the model, rather than Name-Value options hasdata = (numel (varargin) > 0 && (isnumeric (varargin{1}) || islogical (varargin{1}) || istable (varargin{1}))); if (ispart && numel (varargin) > 0) error (strcat ("fitSVMPosterior: no optional argument may be given", ... " for a cross-validated model.")); endif if (! (isfull || ispart || hasdata)) error (strcat ("fitSVMPosterior: a compact model needs the", ... " predictors X and their classes Y.")); endif if (hasdata) X = varargin{1}; if (istable (X)) if (numel (varargin) > 2) error ("fitSVMPosterior: too many input arguments."); elseif (numel (varargin) == 2 && (ischar (varargin{2}) || (isstring (varargin{2}) && isscalar (varargin{2})))) name = char (varargin{2}); if (! any (strcmp (X.Properties.VariableNames, name))) error ("fitSVMPosterior: '%s' is not a variable of TBL.", name); endif Y = X.(name); elseif (numel (varargin) == 2) Y = varargin{2}; elseif (any (strcmp (X.Properties.VariableNames, SVMModel.ResponseName))) Y = X.(SVMModel.ResponseName); else error (strcat ("fitSVMPosterior: TBL must hold the response,", ... " or Y or ResponseVarName must be given.")); endif else if (numel (varargin) < 2) error (strcat ("fitSVMPosterior: the predictors X must be given", ... " with their classes Y.")); elseif (numel (varargin) > 2) error ("fitSVMPosterior: too many input arguments."); endif Y = varargin{2}; endif if (rows (X) != rows (Y) && rows (X) != numel (Y)) error ("fitSVMPosterior: Y must have one class for each row of X."); endif endif ## Only the cross-validation options, which cvPartitionOf builds the ## partition from; it takes 'CrossVal' too, which is not one of them if (isfull && ! hasdata) if (any (strcmpi (varargin(1:2:end), 'CrossVal'))) error ("fitSVMPosterior: unknown parameter name 'CrossVal'."); endif [part, rest] = cvPartitionOf (varargin, SVMModel.Y, ... SVMModel.NumObservations, "fitSVMPosterior"); if (! isempty (rest)) if (ischar (rest{1})) error ("fitSVMPosterior: unknown parameter name '%s'.", rest{1}); endif error (strcat ("fitSVMPosterior: optional arguments must be given", ... " in Name-Value pairs.")); endif endif if (! strcmp (SVMModel.ScoreTransform, 'none')) warning (strcat ("fitSVMPosterior: resetting 'ScoreTransform' to", ... " 'none' before fitting the transformation.")); SVMModel.ScoreTransform = 'none'; endif ## Scores of observations the model was not trained on, which a prior of ## zero makes unnecessary s = []; k = []; if (all (SVMModel.Prior > 0)) if (hasdata) [~, scores] = predict (SVMModel, X); [k, errmsg] = labelIndices (SVMModel.ClassNames, Y); if (! isempty (errmsg)) error ("fitSVMPosterior: %s", errmsg); endif elseif (ispart) [~, scores] = kfoldPredict (SVMModel); k = labelIndices (SVMModel.ClassNames, SVMModel.Y); else CVMdl = crossval (SVMModel, 'CVPartition', part); [~, scores] = kfoldPredict (CVMdl); k = labelIndices (SVMModel.ClassNames, SVMModel.Y); endif s = scores(:,2); endif [st, ScoreTransform, separated] = svmPosterior (s, k, SVMModel.Prior, ... SVMModel.ClassNames); if (separated) warning (strcat ("fitSVMPosterior: the classes are perfectly", ... " separated, so the transformation is a step function.")); endif SVMModel.ScoreTransform = st; ScoreSVMModel = SVMModel; endfunction %!demo %! ## Posterior probabilities for two overlapping species %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! SVMModel = fitcsvm (X, Y, 'KernelFunction', 'linear'); %! [ScoreSVMModel, ScoreTransform] = fitSVMPosterior (SVMModel) %! [label, posterior] = predict (ScoreSVMModel, X([1, 2, 60, 61], :)) %!demo %! ## Two species the scores separate get a step function %! load fisheriris %! SVMModel = fitcsvm (meas(1:100, 3:4), species(1:100)); %! [~, ScoreTransform] = fitSVMPosterior (SVMModel) ## Test output ## Values checked against MATLAB R2024a, whose SMO solver gives scores up to ## 1.3e-3 away from LIBSVM's out of fold %!test %! load fisheriris %! Mdl = fitcsvm (meas(51:150, 3:4), species(51:150), ... %! 'KernelFunction', 'linear'); %! [M, ST] = fitSVMPosterior (Mdl, 'Leaveout', 'on'); %! assert_equal (class (M), 'ClassificationSVM'); %! assert_equal (ST.Type, 'sigmoid'); %! assert_equal (ST.Slope, -3.4011027773863747, 1e-3); %! assert_equal (ST.Intercept, -0.092575940016464561, 1e-4); %! [~, ST2] = fitPosterior (Mdl, 'Leaveout', 'on'); %! assert_equal (ST, ST2); %!test %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! Mdl = fitcsvm (X, Y, 'KernelFunction', 'linear'); %! [M, ST] = fitSVMPosterior (Mdl, X, Y); %! assert_equal (class (M), 'ClassificationSVM'); %! assert_equal (ST.Slope, -3.3591642562674378, 1e-4); %! assert_equal (ST.Intercept, -0.1511873140962528, 1e-4); %!test %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! C = compact (fitcsvm (X, Y, 'KernelFunction', 'linear')); %! [M, ST] = fitSVMPosterior (C, X, Y); %! assert_equal (class (M), 'CompactClassificationSVM'); %! assert_equal (ST.Slope, -3.3591642562674378, 1e-4); %! [~, ST2] = fitPosterior (C, X, Y); %! assert_equal (ST, ST2); %!test %! load fisheriris %! Mdl = fitcsvm (meas(51:150, 3:4), species(51:150), ... %! 'KernelFunction', 'linear'); %! CVMdl = crossval (Mdl, 'Leaveout', 'on'); %! [M, ST] = fitSVMPosterior (CVMdl); %! assert_equal (class (M), 'ClassificationPartitionedModel'); %! assert_equal (ST.Slope, -3.4011027773863747, 1e-3); %! tok = regexp (M.ScoreTransform, '^@\(S\)sigmoid\(S,([^,]+),([^)]+)\)$', ... %! 'tokens', 'once'); %! assert_equal (str2double (tok)', [ST.Slope, ST.Intercept]); %! assert_equal (M.Trained{1}.ScoreTransform, 'none'); %! [~, p] = kfoldPredict (M); %! assert_equal (p(1:3,:), [0.9326612658405814, 0.067338734159418587; ... %! 0.9825175955481934, 0.017482404451806603; ... %! 0.69049950170233787, 0.30950049829766219], 1e-3); %!test %! load fisheriris %! T = table (meas(51:150, 3), meas(51:150, 4), species(51:150), ... %! 'VariableNames', {'PL', 'PW', 'Species'}); %! C = compact (fitcsvm (T, 'Species', 'KernelFunction', 'linear')); %! [~, ST] = fitSVMPosterior (C, meas(51:150, 3:4), species(51:150)); %! [~, ST1] = fitSVMPosterior (C, T, 'Species'); %! [~, ST2] = fitSVMPosterior (C, T); %! [~, ST3] = fitSVMPosterior (C, T(:,1:2), species(51:150)); %! assert_equal (ST1, ST); %! assert_equal (ST2, ST); %! assert_equal (ST3, ST); %!warning ... %! load fisheriris %! Mdl = fitcsvm (meas(1:100, 3:4), species(1:100), 'KernelFunction', 'linear'); %! [~, ST] = fitSVMPosterior (Mdl, 'Leaveout', 'on'); %! assert_equal (ST.Type, 'step'); %! assert_equal (ST.LowerBound, -0.8446461538461536, 1e-6); %! assert_equal (ST.UpperBound, 0.68965517241379271, 1e-6); %! assert_equal (ST.PositiveClassProbability, 0.5); %!warning ... %! load fisheriris %! X = meas(51:150, 3:4); %! Y = species(51:150); %! Mdl = fitcsvm (X, Y, 'KernelFunction', 'linear'); %! Mdl.ScoreTransform = 'logit'; %! [~, ST] = fitSVMPosterior (Mdl, X, Y); %! assert_equal (ST.Slope, -3.3591642562674378, 1e-4); %!test %! load fisheriris %! Mdl = fitcsvm (meas(51:150, 3:4), species(51:150), 'Prior', [0, 1]); %! [M, ST] = fitSVMPosterior (Mdl); %! assert_equal (ST.Type, 'constant'); %! assert_equal (ST.PredictedClass, {'virginica'}); %! assert_equal (M.ScoreTransform, '@(S)constant(S,2)'); ## Test input validation %!error fitSVMPosterior () %!error ... %! load fisheriris %! fitSVMPosterior (fitcknn (meas(51:150, 3:4), species(51:150))) %!error ... %! load fisheriris %! fitSVMPosterior (crossval (fitcknn (meas(51:150, 3:4), species(51:150)))) %!error ... %! load fisheriris %! fitSVMPosterior (fitcsvm (meas(51:150, 3:4), ones (100, 1))) %!error ... %! load fisheriris %! CVMdl = crossval (fitcsvm (meas(51:150, 3:4), species(51:150))); %! fitSVMPosterior (CVMdl, 'KFold', 5) %!error ... %! load fisheriris %! fitSVMPosterior (compact (fitcsvm (meas(51:150, 3:4), species(51:150)))) %!error ... %! load fisheriris %! C = compact (fitcsvm (meas(51:150, 3:4), species(51:150))); %! fitSVMPosterior (C, meas(51:150, 3:4)) %!error ... %! load fisheriris %! C = compact (fitcsvm (meas(51:150, 3:4), species(51:150))); %! fitSVMPosterior (C, meas(51:150, 3:4), species(51:150), 'Leaveout', 'on') %!error ... %! load fisheriris %! C = compact (fitcsvm (meas(51:150, 3:4), species(51:150))); %! fitSVMPosterior (C, meas(51:150, 3:4), species(51:100)) %!error ... %! load fisheriris %! C = compact (fitcsvm (meas(51:150, 3:4), species(51:150))); %! fitSVMPosterior (C, meas(1:100, 3:4), species(1:100)) %!error ... %! load fisheriris %! T = table (meas(51:150, 3), meas(51:150, 4), species(51:150), ... %! 'VariableNames', {'PL', 'PW', 'Species'}); %! C = compact (fitcsvm (T, 'Species')); %! fitSVMPosterior (C, T, 'Kind') %!error ... %! load fisheriris %! T = table (meas(51:150, 3), meas(51:150, 4), species(51:150), ... %! 'VariableNames', {'PL', 'PW', 'Species'}); %! C = compact (fitcsvm (T, 'Species')); %! fitSVMPosterior (C, T(:,1:2)) %!error ... %! load fisheriris %! fitSVMPosterior (fitcsvm (meas(51:150, 3:4), species(51:150)), 'Foo', 1) %!error ... %! load fisheriris %! fitSVMPosterior (fitcsvm (meas(51:150, 3:4), species(51:150)), ... %! 'CrossVal', 'on') %!error ... %! load fisheriris %! fitSVMPosterior (fitcsvm (meas(51:150, 3:4), species(51:150)), ... %! 'KFold', 5, 'Leaveout', 'on') %!error ... %! load fisheriris %! fitSVMPosterior (fitcsvm (meas(51:150, 3:4), species(51:150)), ... %! 'Holdout', 1) statistics-release-2.1.2/inst/Machine_Learning/fitcdiscr.m000066400000000000000000000274251527311104300236230ustar00rootroot00000000000000## Copyright (C) 2024 Ruchika Sonagote ## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} fitcdiscr (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcdiscr (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcdiscr (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcdiscr (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcdiscr (@dots{}, @var{name}, @var{value}) ## ## Fit a Linear Discriminant Analysis classification model. ## ## @code{@var{Mdl} = fitcdiscr (@var{X}, @var{Y})} returns a Linear Discriminant ## Analysis (LDA) classification model, @var{Mdl}, with @var{X} being the ## predictor data, and @var{Y} the class labels of observations in @var{X}. ## ## @itemize ## @item ## @code{X} must be a @math{N*P} numeric matrix of predictor data where rows ## correspond to observations and columns correspond to features or variables. ## @item ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels of ## corresponding predictor data in @var{X}. @var{Y} can be numerical, logical, ## char array or cell array of character vectors. @var{Y} must have same number ## of rows as @var{X}. ## @end itemize ## ## @code{@var{Mdl} = fitcdiscr (@dots{}, @var{name}, @var{value})} returns a ## Linear Discriminant Analysis model with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @subheading Model Parameters ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PredictorNames'} @tab A cell array of character vectors ## specifying the names of the predictors. The length of this array must match ## the number of columns in @var{X}. ## ## @item @qcode{'ResponseName'} @tab A character vector specifying the ## name of the response variable. ## ## @item @qcode{'ClassNames'} @tab Names of the classes in the class ## labels, @var{Y}, used for fitting the Discriminant model. @qcode{ClassNames} ## are of the same type as the class labels in @var{Y}. The model keeps ## the classes in this order; by default they are sorted. ## ## @item @qcode{'Prior'} @tab A numeric vector specifying the prior ## probabilities for each class. The order of the elements in @qcode{Prior} ## corresponds to the order of the classes in @qcode{ClassNames}. ## Alternatively, you can specify @qcode{'empirical'} to use the empirical ## class probabilities or @qcode{'uniform'} to assume equal class probabilities. ## ## @item @qcode{'Weights'} @tab A single or double vector of nonnegative ## observation weights, one per row of @var{X}. They weigh the class means and ## covariances, the covariances being unbiased for them, and an empirical prior ## sums them per class. Only their proportions matter, and a row of zero weight ## is left out of the fit. The model's @code{W} keeps the class of the weights, ## while every computation runs in double, so @code{Prior} and the predictions ## are double where MATLAB returns single. ## ## @item @qcode{'Cost'} @tab A @math{N*R} numeric matrix containing ## misclassification cost for the corresponding instances in @var{X} where ## @math{R} is the number of unique categories in @var{Y}. If an instance is ## correctly classified into its category the cost is calculated to be 1, ## otherwise 0. cost matrix can be altered use @code{@var{Mdl.cost} = somecost}. ## default value @qcode{@var{cost} = ones(rows(X),numel(unique(Y)))}. ## ## @item @qcode{'DiscrimType'} @tab A character vector naming the type of ## discriminant analysis to perform, one of @qcode{'linear'} (default), ## @qcode{'quadratic'}, @qcode{'diagLinear'}, @qcode{'diagQuadratic'}, ## @qcode{'pseudoLinear'} or @qcode{'pseudoQuadratic'}. A linear type pools ## one covariance across the classes and a quadratic type estimates one per ## class; a @qcode{'diag'} type keeps only the variances, and a ## @qcode{'pseudo'} type inverts a singular covariance rather than refusing ## it. The property may be reassigned after fitting, but only within its own ## family, since the family decides which covariances the fit estimates. ## ## @item @qcode{'FillCoeffs'} @tab A character vector or string scalar ## with values @qcode{'on'} or @qcode{'off'} specifying whether to fill the ## coefficients after fitting. If set to @qcode{'on'}, the coefficients are ## computed during model fitting, which can be useful for prediction. ## ## @item @qcode{'Gamma'} @tab A numeric scalar specifying the ## regularization parameter for the covariance matrix. It adjusts the linear ## discriminant analysis to make the model more stable in the presence of ## multicollinearity or small sample sizes. A value of 0 corresponds to no ## regularization, while a value of 1 corresponds to ## a completely regularized model. ## ## @end multitable ## @seealso{ClassificationDiscriminant} ## @end deftypefn function obj = fitcdiscr (X, Y, varargin) ## Check input parameters if (nargin < 2) error ("fitcdiscr: too few arguments."); endif if (mod (nargin, 2) != 0) error ("fitcdiscr: name-value arguments must be in pairs."); endif ## Check predictor data and labels have equal rows if (! istable (X) && rows (X) != rows (Y)) error ("fitcdiscr: number of rows in X and Y must be equal."); endif ## Parse arguments to class def function obj = ClassificationDiscriminant (X, Y, varargin{:}); endfunction %!demo %! ## Train a linear discriminant classifier for Gamma = 0.5 %! ## and plot the decision boundaries. %! %! load fisheriris %! idx = ! strcmp (species, 'setosa'); %! X = meas(idx,3:4); %! Y = cast (strcmpi (species(idx), 'virginica'), 'double'); %! obj = fitcdiscr (X, Y, 'Gamma', 0.5) %! x1 = [min(X(:,1)):0.03:max(X(:,1))]; %! x2 = [min(X(:,2)):0.02:max(X(:,2))]; %! [x1G, x2G] = meshgrid (x1, x2); %! XGrid = [x1G(:), x2G(:)]; %! pred = predict (obj, XGrid); %! gidx = logical (pred); %! %! figure %! scatter (XGrid(gidx,1), XGrid(gidx,2), 'markerfacecolor', 'magenta'); %! hold on %! scatter (XGrid(! gidx,1), XGrid(! gidx,2), 'markerfacecolor', 'red'); %! plot (X(Y == 0, 1), X(Y == 0, 2), 'ko', X(Y == 1, 1), X(Y == 1, 2), 'kx'); %! xlabel ('Petal length (cm)'); %! ylabel ('Petal width (cm)'); %! title ('Linear Discriminant Analysis Decision Boundary'); %! legend ({'Versicolor Region', 'Virginica Region', ... %! 'Sampled Versicolor', 'Sampled Virginica'}, ... %! 'location', 'northwest') %! axis tight %! hold off ## Tests %!demo %! ## Fit from a table, and predict on one %! %! load fisheriris %! T = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species); %! %! ## A model formula names the response and the predictors together, and %! ## holds main effects only %! Mdl = fitcdiscr (T, 'Species ~ PL + PW'); %! Mdl.PredictorNames %! Mdl.ResponseName %! %! ## predict matches the table's variables by name, so a column the model %! ## was not fitted on is passed over %! label = predict (Mdl, T(1:5,:)); %! label' %!test %! load fisheriris %! Mdl = fitcdiscr (meas, species, 'Gamma', 0.5); %! [label, score, cost] = predict (Mdl, [2, 2, 2, 2]); %! assert_equal (label, {'versicolor'}) %! assert_equal (score, [0, 0.9999, 0.0001], 1e-4) %! assert_equal (cost, [1, 0.0001, 0.9999], 1e-4) %! [label, score, cost] = predict (Mdl, [2.5, 2.5, 2.5, 2.5]); %! assert_equal (label, {'versicolor'}) %! assert_equal (score, [0, 0.6368, 0.3632], 1e-4) %! assert_equal (cost, [1, 0.3632, 0.6368], 1e-4) %! assert_equal (class (Mdl), "ClassificationDiscriminant"); %! assert_equal ({Mdl.X, Mdl.Y, Mdl.NumObservations}, {meas, species, 150}) %! assert_equal ({Mdl.DiscrimType, Mdl.ResponseName}, {'linear', 'Y'}) %! assert_equal ({Mdl.Gamma, Mdl.MinGamma}, {0.5, 0}) %! assert_equal (Mdl.ClassNames, unique (species)) %! sigma = [0.265008, 0.046361, 0.083757, 0.019201; ... %! 0.046361, 0.115388, 0.027622, 0.016355; ... %! 0.083757, 0.027622, 0.185188, 0.021333; ... %! 0.019201, 0.016355, 0.021333, 0.041882]; %! assert_equal (Mdl.Sigma, sigma, 1e-6) %! mu = [5.0060, 3.4280, 1.4620, 0.2460; ... %! 5.9360, 2.7700, 4.2600, 1.3260; ... %! 6.5880, 2.9740, 5.5520, 2.0260]; %! assert_equal (Mdl.Mu, mu, 1e-14) %! assert_equal (Mdl.LogDetSigma, -8.6884, 1e-4) ## Test input validation %!error fitcdiscr () %!error fitcdiscr (ones (4,1)) %!error %! fitcdiscr (ones (4,2), ones (4, 1), 'K') %!error %! fitcdiscr (ones (4,2), ones (3, 1)) %!error %! fitcdiscr (ones (4,2), ones (3, 1), 'K', 2) %!test # MATLAB parity: classes given as text are sorted %! load fisheriris %! k = [101:150, 1:50, 51:100]; %! Mdl = fitcdiscr (meas(k,:), species(k)); %! assert_equal (Mdl.ClassNames, {'setosa'; 'versicolor'; 'virginica'}); %!test # MATLAB parity: a given ClassNames order is kept %! load fisheriris %! Mdl = fitcdiscr (meas(1:150,:), species(1:150), ... %! 'ClassNames', {'virginica'; 'setosa'; 'versicolor'}); %! assert_equal (Mdl.ClassNames, {'virginica'; 'setosa'; 'versicolor'}); %!error ... %! load fisheriris %! fitcdiscr (meas, species, 'ClassNames', [3, 1, 2]) ## Table input %!shared fdiT %! load fisheriris %! fdiT = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! fdiT.Species = categorical (species); %!test # the response is named by a column and the rest are predictors %! Mdl = fitcdiscr (fdiT, 'Species'); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW', 'PL', 'PW'}); %! assert_equal (Mdl.ResponseName, 'Species'); %!test # a model formula names the response and the predictors together %! Mdl = fitcdiscr (fdiT, 'Species ~ PL + PW'); %! assert_equal (Mdl.PredictorNames, {'PL', 'PW'}); %!test # predict takes a table, matched by name and not by position %! Mdl = fitcdiscr (fdiT, 'Species'); %! a = predict (Mdl, fdiT); %! assert_equal (class (a), 'categorical'); %! assert_equal (predict (Mdl, fdiT(:, [5, 4, 3, 2, 1])), a); %!test # the levels travel with the model when it is made compact %! Mdl = fitcdiscr (fdiT, 'Species'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.PredictorLevels, Mdl.PredictorLevels); %! assert_equal (predict (CMdl, fdiT), predict (Mdl, fdiT)); ## This learner takes no predictor holding levels, so a table carrying one ## is refused by its own rule rather than coded %!error %! load fisheriris %! M = table (meas(:,1), 'VariableNames', {'SL'}); %! M.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! M.Species = categorical (species); %! fitcdiscr (M, 'Species'); %!error ... %! fitcdiscr (fdiT, 'NoSuch') %!error ... %! predict (fitcdiscr (fdiT, 'Species'), fdiT(:, [1, 3, 4, 5])) statistics-release-2.1.2/inst/Machine_Learning/fitcecoc.m000066400000000000000000000476241527311104300234330ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} fitcecoc (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcecoc (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcecoc (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcecoc (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcecoc (@dots{}, @var{name}, @var{value}) ## ## Fit a multiclass model from binary learners. ## ## @code{@var{Mdl} = fitcecoc (@var{X}, @var{Y})} turns the multiclass ## problem in @var{X} and @var{Y} into a set of two class problems, fits one ## binary learner to each, and returns them as a @code{ClassificationECOC} ## object. A coding matrix says which classes each learner calls +1, which ## it calls -1, and which sit it out; a new observation is sent to every ## learner and given the class whose row of that matrix its scores match most ## closely. ## ## @itemize ## @item ## @var{X} must be a @math{NxP} numeric matrix of predictor data. ## @item ## @var{Y} must be a @math{Nx1} vector of class labels, of any type ## @code{ClassNames} accepts, with at least two distinct values. ## @end itemize ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## ## @item @qcode{'Learners'} @tab @tab The binary learner, either a name, ## @qcode{'svm'} (default), @qcode{'tree'}, @qcode{'knn'}, ## @qcode{'naivebayes'}, @qcode{'discriminant'}, @qcode{'linear'}, ## @qcode{'kernel'} or @qcode{'ensemble'}, a LogitBoost ensemble of 100 ## trees, or a template from @code{templateSVM} and its siblings, ## @code{templateEnsemble} among them, which also carries the options that ## learner is to be fitted with. ## ## @item @qcode{'Coding'} @tab @tab The coding design, either a name ## @code{designecoc} accepts, @qcode{'onevsone'} by default, or a coding ## matrix given outright, which sets @code{CodingName} to @qcode{'custom'}. ## ## @item @qcode{'BinaryLoss'} @tab @tab The loss the binary scores are read ## with. The default follows the learner: @qcode{'exponential'} for an ## AdaBoostM1 or GentleBoost ensemble, @qcode{'binodeviance'} for a ## LogitBoost one, and otherwise @qcode{'hinge'} for one scoring on ## @math{(-Inf,+Inf)} and @qcode{'quadratic'} for one scoring on @math{[0,1]}, ## as a bagged, random subspace or RUSBoost ensemble does. ## ## @item @qcode{'ClassNames'} @tab @tab The classes to fit, and the order ## the rows of the coding matrix, @code{Prior} and @code{Cost} take them in. ## By default they are sorted. ## ## @item @qcode{'Cost'} @tab @tab A @math{KxK} matrix of misclassification ## costs. The default is @code{1 - eye (K)}. ## Or a structure with fields @qcode{ClassNames} and ## @qcode{ClassificationCosts}, which names its own order of the classes and is ## permuted to the model's. ## ## @item @qcode{'Prior'} @tab @tab @qcode{'empirical'} (default), ## @qcode{'uniform'}, or a vector with one element per class. ## A class whose prior is 0 is removed from the data before fitting, with a ## warning, as in MATLAB. ## ## @item @qcode{'Weights'} @tab @tab A nonnegative single or double vector with ## one element per observation. The default is uniform. The model's @code{W} ## keeps the class of the weights, while every computation runs in double, so ## @code{Prior} is double where MATLAB returns single. ## ## @item @qcode{'CategoricalPredictors'} @tab @tab The predictors whose values ## are levels, as indices, a logical vector or @qcode{'all'}, passed as given ## to every binary learner, which codes them its own way; a nearest neighbour ## learner takes only @qcode{'all'} and a discriminant learner none. ## A predictor may be named rather than indexed, as a character matrix of one ## padded name per row, a string array or a cellstr; a name must match an entry ## of @qcode{'PredictorNames'} exactly, its case included. ## ## @item @qcode{'PredictorNames'} @tab @tab A cellstr of predictor names. ## ## @item @qcode{'ResponseName'} @tab @tab The name of the response variable. ## ## @item @qcode{'ScoreTransform'} @tab @tab A transform applied to the ## returned scores. The default is @qcode{'none'}. ## @end multitable ## ## A linear or kernel learner carries no training data of its own, so ## @code{fitcecoc} returns a @code{CompactClassificationECOC} for those two ## and a @code{ClassificationECOC} for the other five, which is what R2024a ## does. ## ## @code{'FitPosterior'} is refused rather than quietly ignored: it installs ## a fitted score transform on each binary learner, which needs the posterior ## fitting this package does not have yet. ## ## An ensemble template may name any classification method of ## @code{fitcensemble}; a regression template is refused, as in MATLAB. ## ## A RUSBoost ensemble scores each class with the weighted sum of its trees' ## class probabilities, which no binary loss can read: MATLAB sets the loss ## to @qcode{'unknown'} and then cannot predict. Here those scores, and the ## binary scores @code{predict} returns, are divided by the total weight of ## the trees, which gives the weighted mean of their class probabilities, and ## are read with @qcode{'quadratic'} as a bagged ensemble's are. This follows ## R's @code{ebmc} and @code{adabag} packages and scikit-learn, whose ## @code{AdaBoostClassifier} and imbalanced-learn's @code{RUSBoostClassifier} ## built on it scale a boosted ensemble's weighted votes the same way. ## ## A binary learner that cannot be fitted, such as an AdaBoostM2 ensemble, which ## needs three classes, stops the fit with its own error, where MATLAB warns and ## predicts the majority class. An AdaBoostM1 learner whose first tree ## separates its two classes keeps that tree, as @code{fitcensemble} does; ## MATLAB keeps no tree and never predicts those classes from it. ## ## Each binary learner is fitted with the weight its observations carry, which ## the prior and any @qcode{'Weights'} decide. ## ## @seealso{ClassificationECOC, CompactClassificationECOC, designecoc, ## templateSVM, templateTree} ## @end deftypefn function Mdl = fitcecoc (X, Y, varargin) ## Input validation if (nargin < 2) error ("fitcecoc: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error ("fitcecoc: name-value arguments must be in pairs."); endif if (! istable (X) && rows (X) != rows (Y)) error ("fitcecoc: number of rows in X and Y must be equal."); endif ## A cross-validation option makes the fit a cross-validated one: the ## model is fitted on all the data first, then partitioned, which is how ## every other fitc* here reads these four. cvnames = {'kfold', 'holdout', 'leaveout', 'cvpartition'}; iscv = cellfun (@(a) ischar (a) && isrow (a) ... && any (strcmpi (a, cvnames)), varargin(1:2:end)); cvargs = {}; if (any (iscv)) if (sum (iscv) > 1) error (strcat ("fitcecoc: specify only one of 'KFold', 'Holdout',", ... " 'Leaveout' and 'CVPartition'.")); endif at = 2 * find (iscv) - 1; cvargs = varargin(at:at+1); varargin(at:at+1) = []; endif Mdl = ClassificationECOC (X, Y, varargin{:}); if (! isempty (cvargs)) Mdl = crossval (Mdl, cvargs{:}); return; endif ## A learner that keeps no training data leaves nothing for the full class ## to hold, so the fit gives back the compact one. Measured on R2024a. if (any (strcmpi (Mdl.ModelParameters.BinaryLearners.Method, ... {'Linear', 'Kernel'}))) Mdl = compact (Mdl); endif endfunction ## Tests %!demo %! ## Fit from a table, and predict on one %! %! load fisheriris %! T = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species); %! %! ## A column holding levels is a categorical predictor without being named %! ## one %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! %! ## The response is named by its column, and everything else is a predictor %! Mdl = fitcecoc (T, 'Species'); %! Mdl.PredictorNames %! Mdl.CategoricalPredictors %! %! ## A model formula names them instead, holding main effects only %! Mdl2 = fitcecoc (T, 'Species ~ PL + PW'); %! Mdl2.PredictorNames %! %! ## predict reads a table by the names the model was fitted on, so the %! ## columns may come in any order %! label = predict (Mdl, T(1:5, [6, 5, 4, 3, 2, 1])); %! label' %!test # MATLAB parity: the default fit and what it reports %! load fisheriris %! Mdl = fitcecoc (meas, species); %! assert_equal (class (Mdl), 'ClassificationECOC'); %! assert_equal (numel (properties (Mdl)), 22); %! assert_equal (Mdl.CodingName, 'onevsone'); %! assert_equal (Mdl.BinaryLoss, 'hinge'); %! assert_equal (Mdl.CodingMatrix, [1, 1, 0; -1, 0, 1; 0, -1, -1]); %! assert_equal (Mdl.LearnerWeights, [2/3, 2/3, 2/3], 1e-12); %! assert_equal (Mdl.NumObservations, 150); %!test # MATLAB parity: the loss of the default fit %! ## Our binary SVM is LIBSVM where MATLAB's is SMO, so the scores differ in %! ## the fourth digit on the pair that is not separable; the labels they %! ## lead to do not. %! load fisheriris %! Mdl = fitcecoc (meas, species); %! assert_equal (resubLoss (Mdl), 0.0066666666666667, 1e-12); %!test # MATLAB parity: a tree code reproduces the decoding exactly %! ## Our trees match MATLAB's on this fixture, so the whole path can be %! ## compared and not only the labels. Measured on R2024a. %! load fisheriris %! Mdl = fitcecoc (meas, species, 'Learners', 'tree'); %! assert_equal (Mdl.BinaryLoss, 'quadratic'); %! [~, NegLoss] = predict (Mdl, meas([1, 20, 51, 70, 101, 130], :)); %! assert_equal (NegLoss, ... %! [0, -1, -2; ... %! 0, -1, -2; ... %! -2, 0, -1; ... %! -2, 0, -1; ... %! -2, -0.956994328922495, -0.000472589792060491; ... %! -2, -0.444444444444445, -0.111111111111111], 1e-12); %!test # MATLAB parity: the default binary loss follows the learner %! load fisheriris %! for p = {{'svm', 'hinge'}, {'tree', 'quadratic'}, {'knn', 'quadratic'}, ... %! {'naivebayes', 'quadratic'}, {'discriminant', 'quadratic'}} %! Mdl = fitcecoc (meas, species, 'Learners', p{1}{1}); %! assert_equal (Mdl.BinaryLoss, p{1}{2}); %! endfor %!test # MATLAB parity: a learner keeping no data gives a compact model %! load fisheriris %! assert_equal (class (fitcecoc (meas, species, 'Learners', 'linear')), ... %! 'CompactClassificationECOC'); %! assert_equal (class (fitcecoc (meas, species, 'Learners', 'kernel')), ... %! 'CompactClassificationECOC'); %!test # a template carries the options its learner is fitted with %! load fisheriris %! Mdl = fitcecoc (meas, species, 'Learners', templateTree ('MaxNumSplits', 2)); %! assert_equal (Mdl.BinaryLearners{1}.ModelParameters.MaxSplits, 2); %!test # MATLAB parity: the coding design is taken by name %! load fisheriris %! Mdl = fitcecoc (meas, species, 'Coding', 'onevsall'); %! assert_equal (Mdl.CodingName, 'onevsall'); %! assert_equal (Mdl.CodingMatrix, 2 * eye (3) - 1); %! assert_equal (numel (Mdl.BinaryLearners), 3); %!test # MATLAB parity: a coding matrix given outright is 'custom' %! load fisheriris %! M = [1, 0, -1; -1, 1, 0; 0, -1, 1]; %! Mdl = fitcecoc (meas, species, 'Coding', M); %! assert_equal (Mdl.CodingName, 'custom'); %! assert_equal (Mdl.CodingMatrix, M); %!test # what an observation was to each learner is its class's row %! load fisheriris %! Mdl = fitcecoc (meas, species); %! assert_equal (Mdl.BinaryY(1,:), [1, 1, 0]); %! assert_equal (Mdl.BinaryY(51,:), [-1, 0, 1]); %! assert_equal (Mdl.BinaryY(101,:), [0, -1, -1]); %!test # MATLAB parity: a cross-validation option gives a partitioned model %! load fisheriris %! CV = fitcecoc (meas, species, 'KFold', 5); %! assert_equal (class (CV), 'ClassificationPartitionedECOC'); %! assert_equal (CV.KFold, 5); %! assert_equal (class (fitcecoc (meas, species, 'Holdout', 0.3)), ... %! 'ClassificationPartitionedECOC'); ## Test input validation %!test # MATLAB parity: a character matrix response counts a class per row %! load fisheriris %! Mdl = fitcecoc (meas, char (species)); %! assert_equal (size (Mdl.CodingMatrix), [3, 3]); %! assert_equal (predict (Mdl, meas([1, 60, 120], :)), ... %! char ({'setosa'; 'versicolor'; 'virginica'})); %!error ... %! fitcecoc (ones (8, 2), [1; 2; 1; 2; 1; 2; 1; 2], 'KFold', 2, 'Holdout', 0.3) %!error fitcecoc (ones (4, 2)) %!error ... %! fitcecoc (ones (4, 2), [1; 2; 1; 2], 'Coding') %!error ... %! fitcecoc (ones (4, 2), [1; 2; 1]) %!error ... %! fitcecoc (ones (4, 2), [1; 2; 1; 2], 'Learners', 'nosuch') %!error ... %! fitcecoc (ones (4, 2), [1; 2; 1; 2], 'FitPosterior', true) %!error ... %! fitcecoc (ones (4, 2), [1; 2; 1; 2], 'NoSuch', 1) %!test # MATLAB parity: classes given as text are sorted %! load fisheriris %! k = [101:150, 1:50, 51:100]; %! Mdl = fitcecoc (meas(k,:), species(k)); %! assert_equal (Mdl.ClassNames, {'setosa'; 'versicolor'; 'virginica'}); %!test # MATLAB parity: a given ClassNames order is kept %! load fisheriris %! Mdl = fitcecoc (meas(1:150,:), species(1:150), ... %! 'ClassNames', {'virginica'; 'setosa'; 'versicolor'}); %! assert_equal (Mdl.ClassNames, {'virginica'; 'setosa'; 'versicolor'}); %!error ... %! load fisheriris %! fitcecoc (meas, species, 'ClassNames', [3, 1, 2]) %!error ... %! load fisheriris %! fitcecoc (meas, species, 'ClassNames', {'setosa'; 'rose'}) %!test # MATLAB parity: GentleBoost ensembles as binary learners %! load fisheriris %! T = templateEnsemble ('GentleBoost', 5, templateTree ('MaxNumSplits', 1)); %! Mdl = fitcecoc (meas, species, 'Learners', T); %! assert_equal (Mdl.BinaryLoss, 'exponential'); %! assert_equal (resubLoss (Mdl), 1/30, 1e-14); %! [~, NegLoss] = predict (Mdl, meas([51, 120],:)); %! assert_equal (NegLoss, [-74.2065795512883, -0.017199167383083, ... %! -4.030127061700748; -74.2065795512883, ... %! -0.197045791294161, -0.32160454535994], 1e-12); %!test # MATLAB parity: LogitBoost ensembles as binary learners %! load fisheriris %! T = templateEnsemble ('LogitBoost', 5, templateTree ('MaxNumSplits', 1)); %! Mdl = fitcecoc (meas, species, 'Learners', T); %! assert_equal (Mdl.BinaryLoss, 'binodeviance'); %! assert_equal (resubLoss (Mdl), 1/30, 1e-14); %! [~, NegLoss] = predict (Mdl, meas([51, 120],:)); %! assert_equal (NegLoss, [-4.844912733252615, -0.002477430762383, ... %! -1.86787026045674; -4.844912733252615, ... %! -0.163235262337723, -0.365681737695726], 1e-12); %!test # MATLAB parity: the weights of an AdaBoostM1 binary learner %! load fisheriris %! T = templateEnsemble ('AdaBoostM1', 5, templateTree ('MaxNumSplits', 1)); %! Mdl = fitcecoc (meas, species, 'Learners', T); %! assert_equal (Mdl.BinaryLoss, 'exponential'); %! assert_equal (Mdl.BinaryLearners{3}.TrainedWeights', ... %! [1.375767656520975, 0.993534110774411, ... %! 0.883434373403998, 0.554364192108758, ... %! 0.268194447432047], 1e-12); %!test # MATLAB parity: bagged and random subspace ensembles read posteriors %! load fisheriris %! Mdl = fitcecoc (meas, species, 'Learners', ... %! templateEnsemble ('Bag', 3, 'tree')); %! assert_equal (Mdl.BinaryLoss, 'quadratic'); %! assert_equal (class (Mdl.BinaryLearners{1}), 'ClassificationBaggedEnsemble'); %! Mdl = fitcecoc (meas, species, 'Learners', ... %! templateEnsemble ('Subspace', 3, 'knn', ... %! 'NPredToSample', 2)); %! assert_equal (Mdl.BinaryLoss, 'quadratic'); %!test # MATLAB parity: an ensemble named as the learner is LogitBoost %! load fisheriris %! Mdl = fitcecoc (meas, species, 'Learners', 'ensemble'); %! assert_equal (Mdl.ModelParameters.BinaryLearners.Method, 'LogitBoost'); %! assert_equal (Mdl.ModelParameters.BinaryLearners.NLearn, 100); %! assert_equal (Mdl.BinaryLoss, 'binodeviance'); %! assert_equal (Mdl.BinaryLearners{1}.NumTrained, 100); %!test # an ensemble of binary learners cross-validates %! load fisheriris %! T = templateEnsemble ('GentleBoost', 3, templateTree ('MaxNumSplits', 1)); %! CV = crossval (fitcecoc (meas, species, 'Learners', T), 'KFold', 3); %! assert_equal (class (CV), 'ClassificationPartitionedECOC'); %!test # RUSBoost binary scores are the weighted mean of tree probabilities %! load fisheriris %! T = templateEnsemble ('RUSBoost', 5, templateTree ('MaxNumSplits', 1)); %! Mdl = fitcecoc (meas, species, 'Learners', T); %! assert_equal (Mdl.BinaryLoss, 'quadratic'); %! B = Mdl.BinaryLearners{1}; %! [~, s] = predict (B, meas(51,:)); %! [~, ~, PBScore] = predict (Mdl, meas(51,:)); %! assert_equal (PBScore(1), s(2) / sum (B.TrainedWeights), 1e-14); %! assert_equal (all (PBScore >= 0 & PBScore <= 1), true); %! assert_equal (isfinite (resubLoss (Mdl)), true); %!error ... %! load fisheriris %! fitcecoc (meas, species, 'Learners', templateEnsemble ('LSBoost', 5, 'tree')) ## Table input %!shared fecT %! load fisheriris %! fecT = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! fecT.Species = categorical (species); %! fecT.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %!test # the response is named by a column and the rest are predictors %! Mdl = fitcecoc (fecT, 'Species'); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW', 'PL', 'PW', 'Wide'}); %! assert_equal (Mdl.ResponseName, 'Species'); %! assert_equal (Mdl.CategoricalPredictors, 5); %!test # a model formula names the response and the predictors together %! Mdl = fitcecoc (fecT, 'Species ~ PW + SL'); %! assert_equal (Mdl.PredictorNames, {'PW', 'SL'}); %! assert_equal (isempty (Mdl.CategoricalPredictors), true); %!test # the response may be given beside a table of predictors %! Mdl = fitcecoc (fecT(:,1:4), fecT.Species); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW', 'PL', 'PW'}); %! assert_equal (Mdl.ResponseName, 'Y'); %!test # predict takes a table, matched by name and not by position %! Mdl = fitcecoc (fecT, 'Species'); %! a = predict (Mdl, fecT); %! assert_equal (class (a), 'categorical'); %! assert_equal (predict (Mdl, fecT(:, [6, 5, 4, 3, 2, 1])), a); %!test # the levels travel with the model when it is made compact %! Mdl = fitcecoc (fecT, 'Species'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.PredictorLevels, Mdl.PredictorLevels); %! assert_equal (predict (CMdl, fecT), predict (Mdl, fecT)); %!error ... %! fitcecoc (fecT, 'NoSuch') %!error ... %! fitcecoc (fecT, 'Species ~ SL*PW') %!error ... %! predict (fitcecoc (fecT, 'Species'), fecT(:, [1, 3, 4, 5, 6])) statistics-release-2.1.2/inst/Machine_Learning/fitcensemble.m000066400000000000000000001470411527311104300243060ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} fitcensemble (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcensemble (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcensemble (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcensemble (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcensemble (@dots{}, @var{name}, @var{value}) ## ## Fit an ensemble of decision trees for classification. ## ## @code{@var{Mdl} = fitcensemble (@var{X}, @var{Y})} grows 100 boosted ## decision trees on the @math{NxP} predictor matrix @var{X} and the class ## labels @var{Y}, LogitBoost for two classes and AdaBoostM2 for more, and ## returns a @code{ClassificationEnsemble}. With @qcode{'Method'} set to ## @qcode{'Bag'}, or with a boosting method that resamples, it returns a ## @code{ClassificationBaggedEnsemble}. ## ## @var{Y} holds a class label per row of @var{X}, as a numeric or logical ## vector, a categorical, string or character array, or a cell array of ## character vectors. A row missing a predictor or a class is left out. ## ## The boosting methods, @var{y} being +1 for the first class and -1 for the ## second, @var{d} the observation weights, which start at @code{W} times the ## total cost of misclassifying each observation's class, and @var{eta} the ## learning rate: ## ## @table @asis ## @item @qcode{'AdaBoostM1'} ## Two classes. Each tree is grown with the weights @var{d}; @var{h} is +1 ## where it predicts the first class and -1 elsewhere, @var{e} its weighted ## error, and its weight @code{eta * log ((1 - e) / e) / 2}. The weights are ## then multiplied by @code{exp (-weight * y .* h)}. ## @item @qcode{'AdaBoostM2'} ## More than two classes. A weight is kept for each observation and each ## class other than its own, and each tree is grown with their sums; its ## pseudo-loss @var{e} over its class probabilities gives its weight as for ## AdaBoostM1, and the scores are the weighted sums of the class ## probabilities. ## @item @qcode{'RUSBoost'} ## Two or more classes, for classes of unequal size. Each tree is grown ## without weights on a sample of every class, @code{round (r * m)} rows, ## @var{m} being the size of the smallest class and @var{r} the class's ## element of @qcode{'RatioToSmallest'}, drawn in proportion to @var{d} and ## with replacement only when the class holds fewer rows. Its pseudo-loss ## @var{e} over all the observations, the weight of each spread evenly over ## the classes other than its own, gives its weight as for AdaBoostM1, the ## weights are then multiplied by the mean over those classes of ## @code{exp (-weight * (1 + h_true - h_k))}, and the scores are as for ## AdaBoostM2. A perfect tree is kept as for AdaBoostM2. ## @item @qcode{'LPBoost'} ## Two or more classes. Each tree is grown with the weights @var{d}, and its ## margins and edge are as for TotalBoost. A linear program over the trees ## with the new one, solved by GLPK, gives the least over distributions of ## their largest edge; when the smallest edge is no more than ## @qcode{'MarginPrecision'} above it, the new tree is not kept and the fit ## stops. Otherwise the program gives the learner weights that maximise the ## smallest margin, and as its dual the next weights @var{d}. The scores are ## as for TotalBoost. Where the program has several solutions, MATLAB and ## this package may choose different ones: the learner weights, and the ## weights @var{d} with the trees grown on them, then differ, and so may the ## number of trees kept. MATLAB R2024a keeps a tree only when that gap is also ## above 0.01, so a @qcode{'MarginPrecision'} below 0.01 acts there as 0.01; ## here it is taken as given. ## @item @qcode{'TotalBoost'} ## Two or more classes. Each tree is grown with the weights @var{d}; its ## margin on an observation is the probability it gives the observation's ## class less the largest it gives another, and its edge is those margins ## weighted by @var{d}. The fit stops, without that tree, once the least over ## distributions of the largest edge of the trees exceeds the smallest edge ## less @qcode{'MarginPrecision'}. Otherwise @var{d} takes one quadratic step ## towards the least relative entropy to the starting weights, every edge held ## at most the smallest edge less @qcode{'MarginPrecision'}, and the learner ## weights are those that maximise the smallest margin, a linear program solved ## by GLPK@. A tree scores each class with twice its probability less one. ## Where several learner weights maximise that margin equally, MATLAB and this ## package may choose different ones, and the scores then differ. ## @item @qcode{'GentleBoost'} ## Two classes. Each regression tree is fitted to @var{y} with the weights ## @var{d}, its prediction @var{h} added to the score times @var{eta}, and the ## weights multiplied by @code{exp (-eta * y .* h)}. ## @item @qcode{'LogitBoost'} ## Two classes. With @var{p} the probability of the first class, each ## regression tree is fitted to @code{(y01 - p) ./ (p .* (1 - p))} with the ## weights @code{d .* p .* (1 - p)}; its prediction times @code{eta / 2} is ## added to the score @var{f}, and @code{p = 1 ./ (1 + exp (-f))}. ## @end table ## ## The @qcode{'Subspace'} method fits each learner, a nearest neighbour or ## discriminant classifier, on @code{NPredToSample} predictors drawn at random ## without replacement, or on every combination of that many with ## @qcode{'NumLearningCycles'} set to @qcode{'AllPredictorCombinations'}, and ## scores each observation with the plain average of the learners' class ## probabilities. @code{UsePredForLearner} records the predictors of each ## learner; every combination is taken in the order of @code{nchoosek}, which ## MATLAB R2024a reverses for some subset sizes, changing the order of the ## learners but not the scores. MATLAB takes observation weights here and ## passes them to the learners; the learners in this package take none, so ## weights that are not uniform are refused. ## ## A boosting method resamples when @qcode{'Resample'} is @qcode{'on'} or ## @qcode{'FResample'} or @qcode{'Replace'} is given; RUSBoost and Subspace ## cannot. Each learner is then grown on @code{ceil (FResample * N)} rows, ## drawn with replacement in proportion to @var{d}, each draw weighing the ## same, or without replacement uniformly, each row keeping its weight in ## @var{d}. Its error is taken on those rows with those weights, and only ## they are reweighted, rescaled to the weight they carried over their draws. ## AdaBoostM2 then keeps one weight per observation, spread evenly over the ## classes other than its own, as RUSBoost does, and LogitBoost advances the ## score of the rows drawn only. The ensemble is a ## @code{ClassificationBaggedEnsemble}, which records the rows each learner ## drew and estimates the out-of-bag error. These rules reproduce MATLAB ## R2024a's fits on its own draws, except LogitBoost with replacement, whose ## reweighting in MATLAB was not identified; here it follows the others. ## ## A boosting method whose tree classifies the data without error, zero error ## for AdaBoostM1 or zero pseudo-loss for AdaBoostM2, keeps that tree with the ## weight an error of @code{eps} gives and stops; one whose error is greater ## than 0.5 is not kept and stops. MATLAB discards a perfect tree, so a fit ## whose first tree is perfect is empty there. GentleBoost applies the ## learning rate to the fit itself, as the other boosting methods do; MATLAB ## scales only the learner weights, growing the same trees whatever the ## rate. ## ## Name-Value arguments: ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Method'} @tab @tab @qcode{'AdaBoostM1'}, ## @qcode{'AdaBoostM2'}, @qcode{'RUSBoost'}, @qcode{'GentleBoost'}, ## @qcode{'LogitBoost'}, @qcode{'LPBoost'}, @qcode{'TotalBoost'}, ## @qcode{'Bag'} or @qcode{'Subspace'}. The default is ## @qcode{'LogitBoost'} for two classes and @qcode{'AdaBoostM2'} for more. ## @item @qcode{'NumLearningCycles'} @tab @tab A positive integer, the number ## of learners to grow, or for Subspace @qcode{'AllPredictorCombinations'}. ## The default is 100. ## @item @qcode{'NPredToSample'} @tab @tab A positive integer less than the ## number of predictors, the predictors each Subspace learner is fitted on. ## The default is 1. Subspace only. ## @item @qcode{'Learners'} @tab @tab For Subspace @qcode{'knn'} (default), ## @qcode{'discriminant'}, or a template from @code{templateKNN} or ## @code{templateDiscriminant}. Otherwise @qcode{'tree'} (default) or a ## template from @code{templateTree}, whose options override the defaults: for ## boosting @code{MaxNumSplits} 10, @code{MinParentSize} 2 and ## @code{MinLeafSize} 1, the regression trees of GentleBoost and LogitBoost ## taking @code{MinParentSize} 10; for Bag unlimited splits, ## @code{MinParentSize} 2, @code{MinLeafSize} 1 and ## @code{NumVariablesToSample} @code{ceil (sqrt (P))}. ## @item @qcode{'LearnRate'} @tab @tab A number greater than 0 and no greater ## than 1. The default is 1. Boosting only. ## @item @qcode{'RatioToSmallest'} @tab @tab A nonnegative number, or a vector ## with one per class, the size of each class's sample relative to the ## smallest class. The default is 1 for every class. RUSBoost only. ## @item @qcode{'MarginPrecision'} @tab @tab A number from 0 to 1, how far ## above the linear program LPBoost needs the smallest edge to stay, and how ## far below the smallest edge TotalBoost holds every edge. The default is ## 0.01. LPBoost and TotalBoost only. ## @item @qcode{'CategoricalPredictors'} @tab @tab The predictors whose values ## are levels, as indices, as a logical vector with one element per ## predictor, or as @qcode{'all'}. Every tree splits them into sets of ## levels, as @code{fitctree} does. Not with @qcode{'Subspace'}. ## A predictor may be named rather than indexed, as a character matrix of one ## padded name per row, a string array or a cellstr; a name must match an entry ## of @qcode{'PredictorNames'} exactly, its case included. ## @item @qcode{'FResample'} @tab @tab The share of the observations each ## learner draws, greater than 0 and no greater than 1. The default is 1. ## Given with a boosting method, the ensemble resamples. ## @item @qcode{'Replace'} @tab @tab @qcode{'on'} (default) or @qcode{'off'}, ## whether the learners draw with replacement. Given with a boosting method, ## the ensemble resamples. ## @item @qcode{'Resample'} @tab @tab @qcode{'off'} (default) or ## @qcode{'on'}, whether a boosting method resamples. Bag always does. ## @item @qcode{'NPrint'} @tab @tab @qcode{'off'} (default) or a positive ## integer @var{n}, to print a line after every @var{n} trees. ## @item @qcode{'ClassNames'} @tab @tab The classes to fit, in the order their ## scores are laid out; by default they are sorted. ## @item @qcode{'Cost'} @tab @tab A @math{KxK} matrix of misclassification ## costs. The default is @code{1 - eye (K)}. ## Or a structure with fields @qcode{ClassNames} and ## @qcode{ClassificationCosts}, which names its own order of the classes and is ## permuted to the model's. ## @item @qcode{'Prior'} @tab @tab @qcode{'empirical'} (default), ## @qcode{'uniform'}, or a vector with one element per class. ## A class whose prior is 0 is removed from the data before fitting, with a ## warning, as in MATLAB. ## A row of misclassification costs with no positive cost is refused. ## @item @qcode{'Weights'} @tab @tab A nonnegative single or double vector with ## one weight per observation. The default is uniform. The model's @code{W} ## keeps the class of the weights, while every computation runs in double, so ## @code{Prior} is double where MATLAB returns single. ## @item @qcode{'PredictorNames'} @tab @tab A cell array of character vectors ## naming the columns of @var{X}. ## @item @qcode{'ResponseName'} @tab @tab The name of the response variable. ## @item @qcode{'ScoreTransform'} @tab @tab A transform applied to the ## returned scores. The default is @qcode{'none'}. ## @end multitable ## ## @qcode{'CrossVal'} set to @qcode{'on'}, @qcode{'KFold'}, ## @qcode{'Holdout'}, @qcode{'Leaveout'} or @qcode{'CVPartition'}, only one of ## them, fits the ensemble and cross-validates it as @code{crossval} does, ## returning a @code{ClassificationPartitionedEnsemble}. ## ## The method @qcode{'RobustBoost'}, binning and hyperparameter optimization ## are not implemented, and an option asking for one of them is refused. ## @qcode{'CategoricalPredictors'} cannot be used with @qcode{'Subspace'}, its ## nearest neighbour and discriminant learners taking no categorical ## predictors here; MATLAB passes them to its nearest neighbour learners and ## drops them, with a warning, from its discriminant ones. ## ## @seealso{ClassificationEnsemble, ClassificationBaggedEnsemble, ## CompactClassificationEnsemble, templateTree, TreeBagger} ## @end deftypefn function Mdl = fitcensemble (X, Y, varargin) if (nargin < 2) error ("fitcensemble: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error ("fitcensemble: name-value arguments must be in pairs."); endif ## The cross-validation options make the fit a cross-validated one: the ## ensemble is fitted on all the data first, then on each fold. cv = {'crossval', 'kfold', 'holdout', 'leaveout', 'cvpartition'}; iscv = false (size (varargin)); method = []; resampled = false; for i = 1:2:numel (varargin) if (ischar (varargin{i}) && any (strcmpi (varargin{i}, cv))) iscv(i:i+1) = true; elseif (ischar (varargin{i}) && strcmpi (varargin{i}, 'Method')) method = varargin{i+1}; elseif (ischar (varargin{i}) && any (strcmpi (varargin{i}, {'FResample', 'Replace'}))) resampled = true; elseif (ischar (varargin{i}) && strcmpi (varargin{i}, 'Resample')) resampled = resampled || (ischar (varargin{i+1}) && strcmpi (varargin{i+1}, 'on')); endif endfor cvargs = varargin(iscv); varargin = varargin(! iscv); ## A boosting method that resamples is a bagged ensemble too, as in MATLAB; ## RUSBoost and Subspace refuse to resample in the ensemble itself. named = ischar (method) && isrow (method); isbag = named && strcmpi (method, 'Bag'); if (resampled && ! (named && any (strcmpi (method, {'Bag', 'RUSBoost', ... 'LPBoost', ... 'TotalBoost', ... 'Subspace'})))) isbag = true; if (isempty (method)) varargin(end+1:end+2) = {'Method', ''}; endif endif if (isbag) Mdl = ClassificationBaggedEnsemble (X, Y, varargin{:}); else Mdl = ClassificationEnsemble (X, Y, varargin{:}); endif if (! isempty (cvargs)) [P, errmsg] = ensemblePartition (cvargs, Mdl.Y, Mdl.NumObservations, true); if (! isempty (errmsg)) error ("fitcensemble: %s", errmsg); endif if (! isempty (P)) Mdl = ClassificationPartitionedEnsemble (Mdl, P); endif endif endfunction %!demo %! ## Boost decision stumps to tell versicolor from virginica, and see how %! ## the training error falls as the stumps accumulate. %! load fisheriris %! X = meas(51:150,:); %! Y = species(51:150); %! Mdl = fitcensemble (X, Y, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 20, ... %! 'Learners', templateTree ('MaxNumSplits', 1)); %! err = loss (Mdl, X, Y, 'Mode', 'cumulative'); %! plot (err); %! xlabel ('Number of stumps'); %! ylabel ('Training error'); %!demo %! ## A bagged ensemble of trees classifies all three species, and scores %! ## each flower with the average of its trees' class probabilities. %! load fisheriris %! rng (42); %! Mdl = fitcensemble (meas, species, 'Method', 'Bag', ... %! 'NumLearningCycles', 30); %! [label, scores] = predict (Mdl, [5.0, 3.4, 1.5, 0.2; 6.7, 3.0, 5.2, 2.3]) ## Test output %!demo %! ## Fit from a table, and predict on one %! %! load fisheriris %! T = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species); %! %! ## A column holding levels is a categorical predictor without being named %! ## one %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! %! ## The response is named by its column, and everything else is a predictor %! Mdl = fitcensemble (T, 'Species'); %! Mdl.PredictorNames %! Mdl.CategoricalPredictors %! %! ## predict reads a table by the names the model was fitted on, so the %! ## columns may come in any order %! label = predict (Mdl, T(1:5, [6, 5, 4, 3, 2, 1])); %! label' %!shared X2, Y2, S %! load fisheriris %! X2 = meas(51:150,:); %! Y2 = species(51:150); %! S = templateTree ('MaxNumSplits', 1); %!test # MATLAB parity: AdaBoostM1 on decision stumps %! Mdl = fitcensemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 5, 'Learners', S); %! assert_equal (Mdl.TrainedWeights, [1.375767656520975; 0.993534110774411; ... %! 0.883434373403998; 0.554364192108758; ... %! 0.268194447432047], 1e-13); %! assert_equal (Mdl.FitInfo, [0.06; 0.120567375886525; 0.145932163187856; ... %! 0.248108033048671; 0.369028016476793], 1e-13); %! cuts = cellfun (@(t) t.CutPoint(1), Mdl.Trained); %! assert_equal (cuts, [1.75; 4.95; 4.45; 4.95; 5.15], 1e-12); %!test # MATLAB parity: the scores of AdaBoostM1 are signed learner weights %! Mdl = fitcensemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 5, 'Learners', S); %! [label, s] = predict (Mdl, X2([1, 21, 51],:)); %! assert_equal (label, {'versicolor'; 'virginica'; 'virginica'}); %! assert_equal (s(:,1), [1.772037138568098; -0.979498174473852; ... %! -2.966566396022673], 1e-13); %! assert_equal (s(:,2), -s(:,1)); %!test # MATLAB parity: the learning rate shrinks AdaBoostM1 %! Mdl = fitcensemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 4, 'Learners', S, ... %! 'LearnRate', 0.5); %! assert_equal (Mdl.TrainedWeights, [0.687883828260487; 0.595450620008897; ... %! 0.406803001473525; 0.320537560668013], 1e-13); %! assert_equal (Mdl.FitInfo(2:4), [0.084570915580601; 0.164212646508385; ... %! 0.217184427004775], 1e-13); %!test # MATLAB parity: a uniform prior sets the starting weights %! load fisheriris %! Yv = strcmp (species(41:150), 'virginica'); %! Mdl = fitcensemble (meas(41:150,:), Yv, ... %! 'Method', 'AdaBoostM1', 'NumLearningCycles', 3, ... %! 'Learners', S, 'Prior', 'uniform'); %! assert_equal (Mdl.W([1, 110])', [0.5 / 60, 0.5 / 50], 1e-15); %! assert_equal (Mdl.TrainedWeights, [1.406116776793511; 1.083890239183778; ... %! 0.818511561052510], 1e-13); %!test # MATLAB parity: a cost multiplies the starting weights %! Mdl = fitcensemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 3, 'Learners', S, ... %! 'Cost', [0, 1; 5, 0]); %! assert_equal (Mdl.W([1, 51])', [0.01, 0.01], 1e-15); %! assert_equal (Mdl.FitInfo, [0.036666666666667; 0.125511167033658; ... %! 0.225018706839040], 1e-13); %! [~, s] = predict (Mdl, X2(1,:)); %! assert_equal (s(1), 3.223215778029441, 1e-13); %!test # MATLAB parity: AdaBoostM2 on decision stumps %! load fisheriris %! Mdl = fitcensemble (meas, species, 'Method', 'AdaBoostM2', ... %! 'NumLearningCycles', 4, 'Learners', S); %! assert_equal (Mdl.TrainedWeights, [0.549306144334059; 0.496958723838212; ... %! 0.495870843211405; 0.456824643135089], 1e-13); %! assert_equal (Mdl.FitInfo, [0.25; 0.270139003161550; 0.270568199265314; ... %! 0.286253661231616], 1e-13); %! [~, s] = predict (Mdl, meas([1, 101],:)); %! assert_equal (s, [1.393875268391962, 0.557069431281369, ... %! 0.048015654845434; 0, 0.600192648565954, ... %! 1.398767705952811], 1e-13); %!test # MATLAB parity: GentleBoost on decision stumps %! Mdl = fitcensemble (X2, Y2, 'Method', 'GentleBoost', ... %! 'NumLearningCycles', 3, 'Learners', S); %! assert_equal (class (Mdl.Trained{1}), 'CompactRegressionTree'); %! assert_equal (Mdl.TrainedWeights, [1; 1; 1]); %! assert_equal (Mdl.FitInfo, [0.220611916264090; 0.327133481554914; ... %! 0.606925714189402], 1e-13); %! [~, s] = predict (Mdl, X2([1, 21, 51],:)); %! assert_equal (s(:,1), [2.112004327552702; -0.903893073252190; ... %! -2.537346885979990], 1e-13); %!test # MATLAB parity: GentleBoost with its default trees %! Mdl = fitcensemble (X2, Y2, 'Method', 'GentleBoost', ... %! 'NumLearningCycles', 4); %! assert_equal (Mdl.FitInfo, [0.08; 0.157657738520697; 0.188119628350251; ... %! 0.290921726710326], 1e-13); %! assert_equal (resubLoss (Mdl, 'LossFun', 'exponential'), ... %! 0.054188751279988, 1e-13); %! assert_equal (resubEdge (Mdl), 7.127687492916048, 1e-12); %!test # the learning rate shrinks GentleBoost, where MATLAB only rescales %! Mdl = fitcensemble (X2, Y2, 'Method', 'GentleBoost', ... %! 'NumLearningCycles', 3, 'Learners', S, ... %! 'LearnRate', 0.5); %! assert_equal (Mdl.TrainedWeights, [0.5; 0.5; 0.5]); %! assert_equal (Mdl.FitInfo(1), 0.220611916264090, 1e-13); %! assert_equal (abs (Mdl.FitInfo(2) - 0.327133481554914) > 1e-3, true); %!test # MATLAB parity: LogitBoost on decision stumps %! Mdl = fitcensemble (X2, Y2, 'Method', 'LogitBoost', ... %! 'NumLearningCycles', 3, 'Learners', S); %! assert_equal (Mdl.TrainedWeights, [0.5; 0.5; 0.5]); %! assert_equal (Mdl.FitInfo, [0.882447665056360; 0.883091315814675; ... %! 1.164564532959708], 1e-13); %! [~, s] = predict (Mdl, X2([1, 21, 51],:)); %! assert_equal (s(:,1), [1.906010018452977; 0.134673464507728; ... %! -2.170659689353640], 1e-13); %!test # MATLAB parity: the learning rate shrinks LogitBoost %! Mdl = fitcensemble (X2, Y2, 'Method', 'LogitBoost', ... %! 'NumLearningCycles', 3, 'Learners', S, ... %! 'LearnRate', 0.5); %! assert_equal (Mdl.TrainedWeights, [0.25; 0.25; 0.25]); %! assert_equal (Mdl.FitInfo, [0.882447665056360; 0.826287268663811; ... %! 0.840635802435344], 1e-13); %!test # MATLAB parity: LogitBoost with its default trees %! Mdl = fitcensemble (X2, Y2, 'Method', 'LogitBoost', ... %! 'NumLearningCycles', 6); %! assert_equal (Mdl.FitInfo, [0.32; 0.341307860560587; ... %! 0.516570570389474; 0.548550758138539; ... %! 0.890006883416469; 0.341774284666034], 1e-12); %! [~, s] = predict (Mdl, X2([1, 2, 21, 100],:)); %! assert_equal (s(:,1), [4.582296394974635; 4.239663719918038; ... %! 0.504944771289154; -2.839026377180836], 1e-12); %!test # MATLAB parity: an error of exactly one half does not stop the fit %! X = repmat ([0, 0; 0, 1; 1, 0; 1, 1], 10, 1); %! Y = repmat ([0; 1; 1; 0], 10, 1); %! Mdl = fitcensemble (X, Y, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 3, 'Learners', S); %! assert_equal (Mdl.NumTrained, 3); %! assert_equal (Mdl.FitInfo, [0.5; 0.5; 0.5], 1e-14); %!test # a perfect learner is kept and ends the fit, where MATLAB drops it %! load fisheriris %! Mdl = fitcensemble (meas, strcmp (species, 'setosa'), ... %! 'Method', 'AdaBoostM1', 'NumLearningCycles', 3, ... %! 'Learners', S); %! assert_equal (Mdl.NumTrained, 1); %! assert_equal (Mdl.ReasonForTermination, ... %! 'Classification error from the last weak learner is zero.'); %! assert_equal (Mdl.TrainedWeights, log ((1 - eps) / eps) / 2, 1e-12); %! assert_equal (resubLoss (Mdl), 0); %!test # a perfect AdaBoostM2 learner is kept too %! load fisheriris %! Mdl = fitcensemble (meas, species, 'NumLearningCycles', 5); %! assert_equal (Mdl.Method, 'AdaBoostM2'); %! assert_equal (Mdl.NumTrained, 1); %! assert_equal (Mdl.ReasonForTermination, ... %! 'Pseudo-loss from the last weak learner is zero.'); %! assert_equal (resubLoss (Mdl), 0); %!test # MATLAB parity: LogitBoost is the default for two classes %! Mdl = fitcensemble (X2, Y2, 'NumLearningCycles', 2); %! assert_equal (class (Mdl), 'ClassificationEnsemble'); %! assert_equal (Mdl.Method, 'LogitBoost'); %! assert_equal (Mdl.CombineWeights, 'WeightedSum'); %!test # MATLAB parity: 'Bag' returns a bagged ensemble %! load fisheriris %! Mdl = fitcensemble (meas, species, 'Method', 'Bag', ... %! 'NumLearningCycles', 2); %! assert_equal (class (Mdl), 'ClassificationBaggedEnsemble'); %! assert_equal (Mdl.CombineWeights, 'WeightedAverage'); %!test # MATLAB parity: progress is printed after every NPrint learners %! out = evalc (["fitcensemble (X2, Y2, 'Method', 'AdaBoostM1', ", ... %! "'NumLearningCycles', 4, 'Learners', S, 'NPrint', 2);"]); %! assert_equal (out, sprintf (["Training AdaBoostM1...\n", ... %! "Grown weak learners: 2\n", ... %! "Grown weak learners: 4\n"])); ## Test input validation %!error fitcensemble (X2) %!error ... %! fitcensemble (X2, Y2, 'Method') %!test # MATLAB parity: 'CrossVal' gives ten folds %! CV = fitcensemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 2, 'Learners', S, 'CrossVal', 'on'); %! assert_equal (class (CV), 'ClassificationPartitionedEnsemble'); %! assert_equal (CV.KFold, 10); %! M = fitcensemble (X2, Y2, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 2, 'Learners', S, 'CrossVal', 'off'); %! assert_equal (class (M), 'ClassificationEnsemble'); %!error ... %! fitcensemble (X2, Y2, 'KFold', 5, 'Holdout', 0.2) %!error ... %! fitcensemble (X2, Y2, 'KFold', 1) %!error ... %! fitcensemble (X2, Y2, 'Holdout', 1) %!error ... %! fitcensemble (X2, Y2, 'CrossVal', true) %!error ... %! fitcensemble (X2, Y2, 'Leaveout', 1) %!error ... %! fitcensemble (X2, Y2, 'CVPartition', 5) %!error ... %! fitcensemble (X2, Y2, 'CVPartition', cvpartition (50, 'KFold', 5)) %!test # Subspace of discriminants over every pair, in nchoosek order %! load fisheriris %! Mdl = fitcensemble (meas, species, 'Method', 'Subspace', ... %! 'NumLearningCycles', 'AllPredictorCombinations', ... %! 'NPredToSample', 2, 'Learners', 'discriminant'); %! assert_equal (Mdl.NumTrained, 6); %! assert_equal (Mdl.CombineWeights, 'WeightedAverage'); %! assert_equal (Mdl.TrainedWeights, ones (6, 1)); %! assert_equal (Mdl.FitInfo, []); %! assert_equal (Mdl.LearnerNames, {'Discriminant'}); %! assert_equal (Mdl.UsePredForLearner, logical ([1, 1, 1, 0, 0, 0; ... %! 1, 0, 0, 1, 1, 0; ... %! 0, 1, 0, 1, 0, 1; ... %! 0, 0, 1, 0, 1, 1])); %! assert_equal (class (Mdl.Trained{1}), 'CompactClassificationDiscriminant'); %! assert_equal (Mdl.Trained{1}.PredictorNames, {'x1', 'x2'}); %!test # MATLAB parity: Subspace scores are the mean of the learners %! load fisheriris %! Mdl = fitcensemble (meas, species, 'Method', 'Subspace', ... %! 'NumLearningCycles', 'AllPredictorCombinations', ... %! 'NPredToSample', 2, 'Learners', 'discriminant'); %! [~, s] = predict (Mdl, meas([51, 120],:)); %! assert_equal (s, [0.000000674895093, 0.834981379863075, ... %! 0.165017945241832; 0.000000043609969, ... %! 0.560291808349626, 0.439708148040405], 1e-12); %! assert_equal (resubLoss (Mdl), 0.046666666666667, 1e-13); %!test # MATLAB parity: Subspace of nearest neighbours is the default %! load fisheriris %! Mdl = fitcensemble (meas, species, 'Method', 'Subspace', ... %! 'NumLearningCycles', 'AllPredictorCombinations', ... %! 'NPredToSample', 2); %! assert_equal (Mdl.LearnerNames, {'KNN'}); %! [~, s] = predict (Mdl, meas([101, 120],:)); %! assert_equal (s, [0, 1/6, 5/6; 0, 0.5, 0.5], 1e-15); %! assert_equal (resubLoss (Mdl), 0.006666666666667, 1e-13); %!test # MATLAB parity: a nearest neighbour template over every triple %! load fisheriris %! Mdl = fitcensemble (meas, species, 'Method', 'Subspace', ... %! 'NumLearningCycles', 'AllPredictorCombinations', ... %! 'NPredToSample', 3, ... %! 'Learners', templateKNN ('NumNeighbors', 5)); %! assert_equal (Mdl.NumTrained, 4); %! [~, s] = predict (Mdl, meas([71, 120],:)); %! assert_equal (s, [0, 0.4, 0.6; 0, 0.55, 0.45], 1e-15); %!test # MATLAB parity: single predictors are taken in order %! load fisheriris %! Mdl = fitcensemble (meas(51:150,:), species(51:150), ... %! 'Method', 'Subspace', ... %! 'NumLearningCycles', 'AllPredictorCombinations', ... %! 'Learners', 'discriminant'); %! assert_equal (Mdl.UsePredForLearner, logical (eye (4))); %! [~, s] = predict (Mdl, meas([51, 101],:)); %! assert_equal (s, [0.558349136121729, 0.441650863878271; ... %! 0.195748544656556, 0.804251455343444], 1e-12); %! assert_equal (resubLoss (Mdl), 0.06, 1e-15); %!test # random subspaces draw distinct predictors %! load fisheriris %! rng (9); %! Mdl = fitcensemble (meas, species, 'Method', 'Subspace', ... %! 'NumLearningCycles', 8, 'NPredToSample', 2, ... %! 'Learners', 'discriminant'); %! assert_equal (sum (Mdl.UsePredForLearner), 2 * ones (1, 8)); %! D = fitcensemble (meas, species, 'Method', 'Subspace', ... %! 'NumLearningCycles', 3); %! assert_equal (sum (D.UsePredForLearner), ones (1, 3)); %!test # MATLAB parity: the score transform applies to the averaged scores %! load fisheriris %! Mdl = fitcensemble (meas, species, 'Method', 'Subspace', ... %! 'NumLearningCycles', 'AllPredictorCombinations', ... %! 'NPredToSample', 2, 'Learners', 'discriminant'); %! [~, s0] = predict (Mdl, meas(1,:)); %! Mdl.ScoreTransform = 'logit'; %! [~, s] = predict (Mdl, meas(1,:)); %! assert_equal (s, 1 ./ (1 + exp (-s0)), 1e-15); %!test # MATLAB parity: a cross-validated Subspace refits every combination %! load fisheriris %! CV = fitcensemble (meas, species, 'Method', 'Subspace', ... %! 'NumLearningCycles', 'AllPredictorCombinations', ... %! 'NPredToSample', 2, 'Learners', 'discriminant', ... %! 'KFold', 3); %! assert_equal (CV.NumTrainedPerFold, [6, 6, 6]); %!shared Xi, Yi, T1 %! load fisheriris %! Xi = meas([1:10, 51:100, 101:130],:); %! Yi = species([1:10, 51:100, 101:130]); %! T1 = templateTree ('MaxNumSplits', 1); %!test # MATLAB parity: RUSBoost sampling every row of imbalanced classes %! Mdl = fitcensemble (Xi, Yi, 'Method', 'RUSBoost', 'Learners', T1, ... %! 'NumLearningCycles', 6, 'RatioToSmallest', [1, 5, 3]); %! assert_equal (Mdl.FitInfo', [0.195372503840246, 0.273203444497188, ... %! 0.342324146410416, 0.391679747239235, ... %! 0.424713594905408, 0.446828084418694], 1e-13); %! assert_equal (Mdl.TrainedWeights', [0.707735710111105, ... %! 0.489214930830132, ... %! 0.326477050756789, ... %! 0.220128470123600, ... %! 0.151726482595071, ... %! 0.106747454637321], 1e-13); %! assert_equal (Mdl.CombineWeights, 'WeightedSum'); %! assert_equal (Mdl.FitInfoDescription{2}, ... %! strcat ('Element t of this vector is the weighted loss', ... %! ' from hypothesis t.')); %!test # MATLAB parity: RUSBoost scores are weighted sums of probabilities %! Mdl = fitcensemble (Xi, Yi, 'Method', 'RUSBoost', 'Learners', T1, ... %! 'NumLearningCycles', 6, 'RatioToSmallest', [1, 5, 3]); %! [~, s] = predict (Mdl, Xi([1, 61],:)); %! assert_equal (s, [0.322908080492584, 1.582249594413660, ... %! 0.096872424147775; 0, 0.071501074966215, ... %! 1.930529024087804], 1e-13); %!test # MATLAB parity: RUSBoost starts from the observation weights %! Mdl = fitcensemble (Xi, Yi, 'Method', 'RUSBoost', 'Learners', T1, ... %! 'NumLearningCycles', 5, 'RatioToSmallest', [1, 5, 3], ... %! 'Weights', (1:90)'); %! assert_equal (Mdl.FitInfo', [0.135468889789166, 0.227297593785240, ... %! 0.325572187192910, 0.392977365900961, ... %! 0.433450832378125], 1e-13); %!test # MATLAB parity: RUSBoost starts from the prior %! Mdl = fitcensemble (Xi, Yi, 'Method', 'RUSBoost', 'Learners', T1, ... %! 'NumLearningCycles', 5, 'RatioToSmallest', [1, 5, 3], ... %! 'Prior', 'uniform'); %! assert_equal (Mdl.FitInfo', [0.297695852534562, 0.363195690083740, ... %! 0.405887261979897, 0.433634399973484, ... %! 0.452182830272981], 1e-13); %!test # MATLAB parity: RUSBoost starts from the cost %! Mdl = fitcensemble (Xi, Yi, 'Method', 'RUSBoost', 'Learners', T1, ... %! 'NumLearningCycles', 5, 'RatioToSmallest', [1, 5, 3], ... %! 'Cost', [0, 1, 1; 2, 0, 1; 1, 3, 0]); %! assert_equal (Mdl.FitInfo', [0.170084816462736, 0.253339506885676, ... %! 0.332687545823290, 0.389034394725378, ... %! 0.425567217510485], 1e-13); %!test # MATLAB parity: RUSBoost on two classes %! load fisheriris %! Mdl = fitcensemble (meas(51:140,:), species(51:140), ... %! 'Method', 'RUSBoost', 'Learners', T1, ... %! 'NumLearningCycles', 6, 'RatioToSmallest', [1.25, 1]); %! assert_equal (Mdl.FitInfo', [0.122427983539095, 0.303342803047074, ... %! 0.440725113503495, 0.483823466199863, ... %! 0.495606380578646, 0.498805931770484], 1e-13); %!test # MATLAB parity: the learning rate shrinks RUSBoost %! load fisheriris %! Mdl = fitcensemble (meas, species, 'Method', 'RUSBoost', 'Learners', T1, ... %! 'NumLearningCycles', 6, 'LearnRate', 0.5); %! assert_equal (Mdl.FitInfo', [0.25, 0.266721039894768, ... %! 0.280862713503484, 0.292850544622122, ... %! 0.303050532217282, 0.311767644857868], 1e-13); %! assert_equal (Mdl.TrainedWeights', [0.274653072167027, ... %! 0.252830721199463, ... %! 0.235046573619133, ... %! 0.220394911259144, ... %! 0.208203335984824, ... %! 0.197967081018013], 1e-13); %!test # MATLAB parity: each class draws RatioToSmallest times the smallest %! Mdl = fitcensemble (Xi, Yi, 'Method', 'RUSBoost', 'Learners', T1, ... %! 'NumLearningCycles', 1, 'RatioToSmallest', 0.5); %! assert_equal (Mdl.Trained{1}.ClassCount(1,:), [5, 5, 5]); %!test # MATLAB parity: sample sizes round half away from zero %! load fisheriris %! k = [101:110, 51:100]; %! Mdl = fitcensemble (meas(k,:), species(k), 'Method', 'RUSBoost', ... %! 'Learners', T1, 'NumLearningCycles', 1, ... %! 'RatioToSmallest', [1, 1.25]); %! assert_equal (Mdl.Trained{1}.ClassCount(1,:), [10, 13]); %!test # MATLAB parity: a class smaller than its sample is oversampled %! Mdl = fitcensemble (Xi, Yi, 'Method', 'RUSBoost', 'Learners', T1, ... %! 'NumLearningCycles', 1, 'RatioToSmallest', [2, 1, 1]); %! assert_equal (Mdl.Trained{1}.ClassCount(1,:), [20, 10, 10]); %!test # MATLAB parity: RUSBoost samples in proportion to the weights %! load fisheriris %! k = [101:110, 51:90]; %! w = [ones(20, 1); 1e-8 * ones(30, 1)]; %! Mdl = fitcensemble (meas(k,:), species(k), 'Method', 'RUSBoost', ... %! 'NumLearningCycles', 1, 'Weights', w, ... %! 'Learners', templateTree ('MaxNumSplits', 3)); %! assert_equal (Mdl.Trained{1}.CutPoint(1), 1.65, 1e-14); %!test # a cross-validated RUSBoost keeps its ratios %! CV = fitcensemble (Xi, Yi, 'Method', 'RUSBoost', 'Learners', T1, ... %! 'NumLearningCycles', 3, 'RatioToSmallest', [1, 2, 2], ... %! 'KFold', 3); %! assert_equal (CV.Trainable{1}.RatioToSmallest, [1, 2, 2]); %! assert_equal (CV.NumTrainedPerFold, [3, 3, 3]); %!test # MATLAB parity: classes given as text are sorted %! load fisheriris %! k = [101:150, 51:100]; %! Mdl = fitcensemble (meas(k,:), species(k), 'NumLearningCycles', 2); %! assert_equal (Mdl.ClassNames, {'versicolor'; 'virginica'}); %!test # MATLAB parity: a given ClassNames order is kept %! load fisheriris %! Mdl = fitcensemble (meas(51:150,:), species(51:150), ... %! 'NumLearningCycles', 2, ... %! 'ClassNames', {'virginica'; 'versicolor'}); %! assert_equal (Mdl.ClassNames, {'virginica'; 'versicolor'}); %!test # the score columns follow a given ClassNames order %! load fisheriris %! Mdl = fitcensemble (meas(51:150,:), species(51:150), ... %! 'NumLearningCycles', 2, ... %! 'ClassNames', {'virginica'; 'versicolor'}); %! [label, s] = predict (Mdl, meas(51,:)); %! assert_equal (label, {'versicolor'}); %! assert_equal (s(2) > s(1), true); %!error ... %! load fisheriris %! fitcensemble (meas, species, 'ClassNames', [3, 1, 2]) %!error ... %! load fisheriris %! fitcensemble (meas, species, 'ClassNames', {'setosa'; 'rose'}) %!test # MATLAB parity: a boosting method that resamples is a bagged ensemble %! load fisheriris %! S = templateTree ('MaxNumSplits', 1); %! X2 = meas(51:150,:); %! Y2 = species(51:150); %! assert_equal (class (fitcensemble (X2, Y2, 'Learners', S, ... %! 'NumLearningCycles', 2, ... %! 'FResample', 0.5)), ... %! 'ClassificationBaggedEnsemble'); %! M = fitcensemble (X2, Y2, 'Learners', S, 'NumLearningCycles', 2, ... %! 'Replace', 'off'); %! assert_equal (class (M), 'ClassificationBaggedEnsemble'); %! assert_equal (M.Method, 'LogitBoost'); %! M = fitcensemble (meas, species, 'Learners', S, 'NumLearningCycles', 2, ... %! 'Resample', 'on'); %! assert_equal (M.Method, 'AdaBoostM2'); %!error ... %! load fisheriris %! fitcensemble (meas, species, 'Method', 'RUSBoost', 'Resample', 'on') %!shared Xt, Yt, St %! load fisheriris %! Xt = meas(51:150,:); %! Yt = species(51:150); %! St = templateTree ('MaxNumSplits', 1); %!test # MATLAB parity: TotalBoost edges, margins and termination %! Mdl = fitcensemble (Xt, Yt, 'Method', 'TotalBoost', ... %! 'NumLearningCycles', 20, 'Learners', St); %! assert_equal (Mdl.NumTrained, 20); %! assert_equal (size (Mdl.FitInfo), [20, 101]); %! assert_equal (Mdl.FitInfo(1,1), 0.814814814814815, 1e-14); %! assert_equal (Mdl.FitInfo(:,end)', [0.77938808373591, 0.76044474400507, ... %! 0.733474571057422, 0.713600406110363, 0.674985664146386, ... %! 0.617169871251258, 0.545304010166485, 0.481644597614076, ... %! 0.406905671288418, 0.305738947836862, 0.248186856286276, ... %! 0.223985203507107, 0.206293794856975, 0.190855934857519, ... %! 0.174375765970188, 0.164345799335436, 0.152347022022222, ... %! 0.156330779459034, 0.138184070638576, 0.13354613032089], 5e-5); %! assert_equal (sum (Mdl.TrainedWeights), 1, 1e-12); %! assert_equal (Mdl.ModelParameters.MarginPrecision, 0.01); %! assert_equal (Mdl.CombineWeights, 'WeightedSum'); %! assert_equal (numel (Mdl.FitInfoDescription), 4); %!test # MATLAB parity: TotalBoost stops and weights its learners %! Mdl = fitcensemble (Xt, Yt, 'Method', 'TotalBoost', 'NumLearningCycles', ... %! 20, 'Learners', St, 'MarginPrecision', 0.3); %! assert_equal (Mdl.NumTrained, 3); %! assert_equal (Mdl.ReasonForTermination, ... %! 'No improvement in the last iteration.'); %! assert_equal (Mdl.FitInfo(:,end)', [0.77938808373591, ... %! 0.676485226347579, 0.363399531924414], 1e-6); %! assert_equal (Mdl.TrainedWeights', [0.321673618833505, ... %! 0.344387693465888, 0.333938687700607], 1e-6); %!test # MATLAB parity: a larger MarginPrecision holds the edges lower %! Mdl = fitcensemble (Xt, Yt, 'Method', 'TotalBoost', 'NumLearningCycles', ... %! 20, 'Learners', St, 'MarginPrecision', 0.5); %! assert_equal (Mdl.FitInfo(:,end)', [0.77938808373591, ... %! 0.654956941202807, 0.314330389584896], 1e-6); %! assert_equal (Mdl.TrainedWeights', [0.312047586212885, ... %! 0.338803356427739, 0.349149057359376], 1e-6); %!test # MATLAB parity: TotalBoost with MarginPrecision 0.1 keeps ten trees %! ## R2024a's quadprog stops a little short of each step's optimum, and the %! ## shortfall compounds, so late edges agree only to a few parts in 1e4. %! Mdl = fitcensemble (Xt, Yt, 'Method', 'TotalBoost', 'NumLearningCycles', ... %! 20, 'Learners', St, 'MarginPrecision', 0.1); %! assert_equal (Mdl.NumTrained, 10); %! assert_equal (Mdl.FitInfo(:,end)', [0.77938808373591, ... %! 0.717802322505678, 0.566889939927384, 0.413011741373863, ... %! 0.254813551545311, 0.198534963191251, 0.248289227195743, ... %! 0.151913855148981, 0.18012529479998, 0.145075739302781], 1e-3); %!test # MATLAB parity: a first tree with no error leaves TotalBoost empty %! Mdl = fitcensemble (Xt, Yt, 'Method', 'TotalBoost', 'NumLearningCycles', 5); %! assert_equal (Mdl.NumTrained, 0); %! assert_equal (Mdl.ReasonForTermination, ... %! 'No improvement in the last iteration.'); %!test # MATLAB parity: TotalBoost starts from the observation weights %! Mdl = fitcensemble (Xt, Yt, 'Method', 'TotalBoost', 'NumLearningCycles', ... %! 6, 'Learners', St, 'Weights', (1:100)'); %! assert_equal (Mdl.FitInfo(:,end)', [0.853081762279423, ... %! 0.833229884995196, 0.794516592483876, 0.729979583810379, ... %! 0.701779537592642, 0.663396027611383], 1e-5); %!test # MATLAB parity: a three-class margin is the lead over the next class %! load fisheriris %! Mdl = fitcensemble (meas, species, 'Method', 'TotalBoost', ... %! 'NumLearningCycles', 8, 'Learners', St); %! assert_equal (Mdl.NumTrained, 8); %! assert_equal (Mdl.FitInfo(1,[1, 51, 101, 151]), [1, 0, 0, 1/3], 1e-12); %!test # MATLAB parity: TotalBoost scores are weighted sums of 2p - 1 %! Mdl = fitcensemble (Xt, Yt, 'Method', 'TotalBoost', 'NumLearningCycles', ... %! 20, 'Learners', St, 'MarginPrecision', 0.3); %! [~, s] = predict (Mdl, Xt([1, 51],:)); %! man = zeros (2, 2); %! for t = 1:Mdl.NumTrained %! [~, p] = predict (Mdl.Trained{t}, Xt([1, 51],:)); %! man += Mdl.TrainedWeights(t) * (2 * p - 1); %! endfor %! assert_equal (s, man, 1e-12); %!test # MATLAB parity: LPBoost edges and margins up to its first tie %! Mdl = fitcensemble (Xt, Yt, 'Method', 'LPBoost', 'NumLearningCycles', 4, ... %! 'Learners', St); %! assert_equal (Mdl.NumTrained, 4); %! assert_equal (size (Mdl.FitInfo), [4, 101]); %! assert_equal (Mdl.FitInfo(1,1), 0.814814814814815, 1e-14); %! assert_equal (Mdl.FitInfo(:,end)', [0.77938808373591, 1, ... %! 0.999999999999991, 0.999999999999966], 1e-12); %! assert_equal (Mdl.ModelParameters.MarginPrecision, 0.01); %! assert_equal (Mdl.CombineWeights, 'WeightedSum'); %! assert_equal (numel (Mdl.FitInfoDescription), 4); %!test # MATLAB parity: LPBoost stops on the gap to the linear program %! Y = [repmat({'a'}, 60, 1); repmat({'b'}, 40, 1)]; %! Mdl = fitcensemble (ones (100, 1), Y, 'Method', 'LPBoost', ... %! 'NumLearningCycles', 4, 'Learners', St); %! assert_equal (Mdl.NumTrained, 2); %! assert_equal (Mdl.ReasonForTermination, ... %! 'No improvement in the last iteration.'); %! assert_equal (Mdl.FitInfo(:,end)', [0.04, 1], 1e-12); %! assert_equal (Mdl.TrainedWeights', [5/6, 1/6], 1e-9); %!test # MATLAB parity: a gap within MarginPrecision leaves the tree out %! Y = [repmat({'a'}, 60, 1); repmat({'b'}, 40, 1)]; %! Mdl = fitcensemble (ones (100, 1), Y, 'Method', 'LPBoost', ... %! 'NumLearningCycles', 4, 'Learners', St, ... %! 'MarginPrecision', 0.05); %! assert_equal (Mdl.NumTrained, 1); %! assert_equal (Mdl.TrainedWeights, 1); %!test # MATLAB parity: the first LPBoost tree is held to the same gap %! Y = [repmat({'a'}, 52, 1); repmat({'b'}, 48, 1)]; %! Mdl = fitcensemble (ones (100, 1), Y, 'Method', 'LPBoost', ... %! 'NumLearningCycles', 4, 'Learners', St, ... %! 'MarginPrecision', 0.05); %! assert_equal (Mdl.NumTrained, 0); %! assert_equal (size (Mdl.FitInfo), [0, 101]); %!test # LPBoost weights maximise the smallest margin, summing to one %! Mdl = fitcensemble (Xt, Yt, 'Method', 'LPBoost', 'NumLearningCycles', ... %! 20, 'Learners', St, 'MarginPrecision', 0.5); %! assert_equal (sum (Mdl.TrainedWeights), 1, 1e-12); %! assert_equal (min (Mdl.FitInfo(:,1:end-1)' * Mdl.TrainedWeights), 0, 1e-12); %!test # MATLAB parity: a three-class LPBoost starts from the class margins %! load fisheriris %! Mdl = fitcensemble (meas, species, 'Method', 'LPBoost', ... %! 'NumLearningCycles', 3, 'Learners', St); %! assert_equal (Mdl.FitInfo(:,end)', [1/3, 1, 1], 1e-12); %!error ... %! fitcensemble (Xt, Yt, 'Method', 'LPBoost', 'LearnRate', 0.5) %!error ... %! fitcensemble (Xt, Yt, 'Method', 'LPBoost', 'Resample', 'on') %!error ... %! fitcensemble (Xt, Yt, 'Method', 'TotalBoost', 'LearnRate', 0.5) %!error ... %! fitcensemble (Xt, Yt, 'Method', 'TotalBoost', 'MarginPrecision', 2) %!error ... %! fitcensemble (Xt, Yt, 'Method', 'AdaBoostM1', 'MarginPrecision', 0.1) %!error ... %! fitcensemble (Xt, Yt, 'Method', 'TotalBoost', 'Resample', 'on') %!shared X, yb, Xq %! k = (0:119)'; %! c = mod (k, 4) + 1; %! j = floor (k / 4); %! x2 = mod (k * 7, 10); %! X = [c, x2]; %! yb = (c == 1) | (c == 2 & mod (j, 4) != 0) | (c == 3 & mod (j, 4) == 0) ... %! | (x2 > 7); %! yr = [3; 1; 4; 1.5]; %! yr = yr(c) + 0.1 * sin (k) + 0.2 * x2; %! Xq = [1, 0; 3, 5; 5, 0; NaN, 2; 2.5, 9]; %!test # MATLAB parity: AdaBoostM1 trees split a categorical predictor %! Mdl = fitcensemble (X, yb, 'Method', 'AdaBoostM1', 'NumLearningCycles', ... %! 5, 'CategoricalPredictors', 1); %! assert_equal (Mdl.CategoricalPredictors, 1); %! assert_equal (Mdl.Trained{1}.CategoricalPredictors, 1); %! assert_equal (Mdl.Trained{1}.CutCategories(1,:), {[1, 2], [3, 4]}); %! [~, s] = predict (Mdl, Xq); %! assert_equal (s, [-2.91316, 2.91316; 0.131366, -0.131366; ... %! -2.91316, 2.91316; -2.91316, 2.91316; ... %! -2.91316, 2.91316], 1e-5); %!test # MATLAB parity: 'all' names every predictor for the trees %! Mdl = fitcensemble (X, yb, 'Method', 'Bag', 'NumLearningCycles', 3, ... %! 'CategoricalPredictors', 'all'); %! assert_equal (Mdl.CategoricalPredictors, [1, 2]); %! assert_equal (Mdl.Trained{1}.CategoricalPredictors, [1, 2]); %! assert_equal (Mdl.ExpandedPredictorNames, {'x1', 'x2'}); %!error ... %! fitcensemble (X, yb, 'Method', 'Subspace', 'CategoricalPredictors', 1) ## Table input %!shared fceT %! load fisheriris %! fceT = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! fceT.Species = categorical (species); %! fceT.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %!test # the response is named by a column and the rest are predictors %! Mdl = fitcensemble (fceT, 'Species'); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW', 'PL', 'PW', 'Wide'}); %! assert_equal (Mdl.ResponseName, 'Species'); %! assert_equal (Mdl.CategoricalPredictors, 5); %!test # a model formula names the response and the predictors together %! Mdl = fitcensemble (fceT, 'Species ~ PW + SL'); %! assert_equal (Mdl.PredictorNames, {'PW', 'SL'}); %!test # predict takes a table, matched by name and not by position %! Mdl = fitcensemble (fceT, 'Species'); %! a = predict (Mdl, fceT); %! assert_equal (class (a), 'categorical'); %! assert_equal (predict (Mdl, fceT(:, [6, 5, 4, 3, 2, 1])), a); %!test # a bagged ensemble answers through its parent, so it reads a table too %! Mdl = fitcensemble (fceT, 'Species', 'Method', 'Bag'); %! assert_equal (class (Mdl), 'ClassificationBaggedEnsemble'); %! assert_equal (class (predict (Mdl, fceT)), 'categorical'); %!test # the levels travel with the model when it is made compact %! Mdl = fitcensemble (fceT, 'Species'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.PredictorLevels, Mdl.PredictorLevels); %! assert_equal (predict (CMdl, fceT), predict (Mdl, fceT)); %!error ... %! fitcensemble (fceT, 'NoSuch') %!error ... %! fitcensemble (fceT, 'Species ~ SL*PW') ## A class with prior 0 in the bagged and RUSBoost ensembles, its 50 ## observations removed, as in MATLAB R2024a %!warning ... %! load fisheriris %! Mdl = fitcensemble (meas, species, 'Method', 'Bag', 'Prior', [0.5, 0.5, 0]); %! assert_equal (Mdl.NumObservations, 100); %! assert_equal (predict (Mdl, meas([1, 51, 101], :)), ... %! {'setosa'; 'versicolor'; 'versicolor'}); %!warning ... %! load fisheriris %! Mdl = fitcensemble (meas, species, 'Method', 'RUSBoost', ... %! 'Prior', [0.5, 0.5, 0]); %! assert_equal (Mdl.NumObservations, 100); statistics-release-2.1.2/inst/Machine_Learning/fitcgam.m000066400000000000000000000437651527311104300232700ustar00rootroot00000000000000## Copyright (C) 2024 Ruchika Sonagote ## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} fitcgam (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcgam (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcgam (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcgam (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcgam (@dots{}, @var{name}, @var{value}) ## ## Fit a Generalized Additive Model (GAM) for binary classification. ## ## @code{@var{Mdl} = fitcgam (@var{X}, @var{Y})} returns a GAM classification ## model, @var{Mdl}, with @var{X} being the predictor data, and @var{Y} the ## binary class labels of observations in @var{X}. ## ## @itemize ## @item ## @code{X} must be a @math{N*P} numeric matrix of predictor data where rows ## correspond to observations and columns correspond to features or variables. ## @item ## @code{Y} is @math{N*1} numeric vector containing binary class labels, ## typically 0 or 1. ## @end itemize ## ## @var{Tbl} may stand in place of @var{X}, the response named by one of its ## variables, written into a model formula holding main effects, ## @qcode{'Y ~ x1 + x2'}, or given beside it as @var{Y}. A variable holding ## levels rather than numbers is a categorical predictor without being named ## one, and @qcode{'CategoricalPredictors'} adds to that set rather than ## replacing it. @code{PredictorNames} and @code{ResponseName} come from the ## table. ## ## @code{@var{Mdl} = fitcgam (@dots{}, @var{name}, @var{value})} returns a ## GAM classification model with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @subheading Model Parameters ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'FitMethod'} @tab A character vector selecting the weak ## learner, either @qcode{'boostedtrees'} or @qcode{'splines'}. The default ## is @qcode{'boostedtrees'}, which boosts one shallow decision tree per ## predictor and is the scheme MATLAB uses. @qcode{'splines'} boosts a ## smoothing spline per predictor instead and is an Octave extension. The ## two take different options and an option meant for one is refused by the ## other rather than ignored, so the rows below say which engine each ## belongs to. ## ## @item @qcode{'PredictorNames'} @tab A cell array of character vectors ## specifying the names of the predictors. The length of this array must match ## the number of columns in @var{X}. ## ## @item @qcode{'ResponseName'} @tab A character vector specifying the ## name of the response variable. ## ## @item @qcode{'ClassNames'} @tab Names of the classes in the class ## labels, @var{Y}, used for fitting the Discriminant model. @qcode{ClassNames} ## are of the same type as the class labels in @var{Y}. The model keeps ## the classes in this order; by default they are sorted. ## ## @item @qcode{'Cost'} @tab A @math{N*R} numeric matrix containing ## misclassification cost for the corresponding instances in @var{X} where ## @math{R} is the number of unique categories in @var{Y}. If an instance is ## correctly classified into its category the cost is calculated to be 1, ## otherwise 0. cost matrix can be altered use @code{@var{Mdl.cost} = somecost}. ## default value @qcode{@var{cost} = ones(rows(X),numel(unique(Y)))}. ## ## @item @qcode{'Formula'} @tab (spline option) A model specification given as a ## string in ## the form @qcode{'Y ~ terms'} where @qcode{Y} represents the response variable ## and @qcode{terms} the predictor variables. The formula can be used to ## specify a subset of variables for training model. For example: ## @qcode{'Y ~ x1 + x2 + x3 + x4 + x1:x2 + x2:x3'} specifies four linear terms ## for the first four columns of for predictor data, and @qcode{x1:x2} and ## @qcode{x2:x3} specify the two interaction terms for 1st-2nd and 3rd-4th ## columns respectively. Only these terms will be used for training the model, ## but @var{X} must have at least as many columns as referenced in the formula. ## If Predictor Variable names have been defined, then the terms in the formula ## must reference to those. When @qcode{'formula'} is specified, all terms used ## for training the model are referenced in the @qcode{IntMatrix} field of the ## @var{obj} class object as a matrix containing the column indexes for each ## term including both the predictors and the interactions used. ## ## @item @qcode{'Interactions'} @tab A logical matrix, a positive integer ## scalar, or the string @qcode{'all'} for defining the interactions between ## predictor variables. When given a logical matrix, it must have the same ## number of columns as @var{X} and each row corresponds to a different ## interaction term combining the predictors indexed as @qcode{true}. Each ## interaction term is appended as a column vector after the available predictor ## column in @var{X}. When @qcode{'all'} is defined, then all possible ## combinations of interactions are appended in @var{X} before training. At the ## moment, parsing a positive integer has the same effect as the @qcode{'all'} ## option. When @qcode{'interactions'} is specified, only the interaction terms ## appended to @var{X} are referenced in the @qcode{IntMatrix} field of the ## @var{obj} class object. ## ## @item @qcode{'Knots'} @tab (spline option) A scalar or a row vector ## with the same ## columns as @var{X}. It defines the knots for fitting a polynomial when ## training the GAM. As a scalar, it is expanded to a row vector. The default ## value is 5, hence expanded to @qcode{ones (1, columns (X)) * 5}. You can ## parse a row vector with different number of knots for each predictor ## variable to be fitted with, although not recommended. ## ## @item @qcode{'Order'} @tab (spline option) A scalar or a row vector ## with the same ## columns as @var{X}. It defines the order of the polynomial when training the ## GAM. As a scalar, it is expanded to a row vector. The default values is 3, ## hence expanded to @qcode{ones (1, columns (X)) * 3}. You can parse a row ## vector with different number of polynomial order for each predictor variable ## to be fitted with, although not recommended. ## ## @item @qcode{'DoF'} @tab (spline option) A scalar or a row vector ## with the same columns ## as @var{X}. It defines the degrees of freedom for fitting a polynomial when ## training the GAM. As a scalar, it is expanded to a row vector. The default ## value is 8, hence expanded to @qcode{ones (1, columns (X)) * 8}. You can ## parse a row vector with different degrees of freedom for each predictor ## variable to be fitted with, although not recommended. ## ## @end multitable ## ## The rows above marked as spline options require ## @qcode{'FitMethod', 'splines'}. The remaining options belong to the ## boosted-tree engine and require @qcode{'FitMethod', 'boostedtrees'}, which ## is the default. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'NumTreesPerPredictor'} @tab A positive integer, the number of ## boosting rounds of the predictor phase. It is a budget rather than a ## count: a fit that stops improving ends earlier and reports so. The default ## is 300. ## ## @item @qcode{'NumTreesPerInteraction'} @tab A positive integer, the same ## budget for the interaction phase. The default is 100. ## ## @item @qcode{'MaxNumSplitsPerPredictor'} @tab A positive integer, the ## largest number of splits any one predictor tree may make. The default is ## 1, which makes each tree a stump. ## ## @item @qcode{'MaxNumSplitsPerInteraction'} @tab The same limit for a tree ## over a pair of predictors. The default is 4. ## ## @item @qcode{'InitialLearnRateForPredictors'} @tab A value greater than 0 ## and at most 1, the step a round of the predictor phase starts at. A round ## that fails to improve the fit is retried at half the step, so this is an ## initial value rather than a fixed one. The default is 1. ## ## @item @qcode{'InitialLearnRateForInteractions'} @tab The same for the ## interaction phase. The default is 1. ## ## @item @qcode{'MaxPValue'} @tab A value between 0 and 1. A candidate pair ## of predictors is kept only if its interaction test gives a @math{p}-value ## no larger than this. The default is 1, which keeps every pair asked for. ## ## @item @qcode{'Verbose'} @tab A non-negative integer. Greater than zero ## prints a trace of the fit. The default is 0. ## ## @item @qcode{'NumPrint'} @tab A positive integer, how often the trace ## reports: the first round and then every @var{NumPrint} rounds. The ## default is 10. ## ## @item @qcode{'CategoricalPredictors'} @tab The predictors to treat as ## categorical: a vector of column indices, a logical vector with one element ## per predictor, or @qcode{'all'}. A tree splits a categorical predictor ## into two sets of levels, and a level not seen in training predicts as a ## missing value. Its @qcode{BinEdges} and @qcode{PairDetectionBinEdges} are ## empty. The default is none. ## A predictor may be named rather than indexed, as a character matrix of one ## padded name per row, a string array or a cellstr; a name must match an entry ## of @qcode{'PredictorNames'} exactly, its case included. ## ## @item @qcode{'Weights'} @tab A single or double vector of non-negative ## observation weights, one per row of @var{X}. Each class carries its ## @qcode{'Prior'}, spread over its observations in proportion to their weights. ## The model's @code{W} keeps the class of the weights, while every computation ## runs in double, so @code{Prior} and the predictions are double where MATLAB ## returns single. ## A class whose prior is 0 is removed from the data before fitting, with a ## warning, as in MATLAB. ## ## @end multitable ## You can parse either a @qcode{'Formula'} or an @qcode{'Interactions'} ## optional parameter. Parsing both parameters will result an error. ## Accordingly, you can only pass up to two parameters among @qcode{'Knots'}, ## @qcode{'Order'}, and @qcode{'DoF'} to define the required polynomial for ## training the GAM model. ## ## @seealso{ClassificationGAM} ## @end deftypefn function obj = fitcgam (X, Y, varargin) ## Check input parameters if (nargin < 2) error ("fitcgam: too few arguments."); endif if (mod (nargin, 2) != 0) error ("fitcgam: name-value arguments must be in pairs."); endif ## Check predictor data and labels have equal rows if (! istable (X) && rows (X) != rows (Y)) error ("fitcgam: number of rows in X and Y must be equal."); endif ## Parse arguments to class def function obj = ClassificationGAM (X, Y, varargin{:}); endfunction ## Demo %!demo %! ## Train a GAM classifier for binary classification %! ## using specific data and plot the decision boundaries. %! %! ## Define specific data %! X = [1, 2; 2, 3; 3, 3; 4, 5; 5, 5; ... %! 6, 7; 7, 8; 8, 8; 9, 9; 10, 10]; %! Y = [0; 0; 0; 0; 0; ... %! 1; 1; 1; 1; 1]; %! %! ## Train the GAM model %! obj = fitcgam (X, Y, 'Interactions', 'all'); %! %! ## Create a grid of values for prediction %! x1 = [min(X(:,1)):0.1:max(X(:,1))]; %! x2 = [min(X(:,2)):0.1:max(X(:,2))]; %! [x1G, x2G] = meshgrid (x1, x2); %! XGrid = [x1G(:), x2G(:)]; %! pred = predict (obj, XGrid); %! %! ## Plot decision boundaries and data points %! predNumeric = str2double (pred); %! gidx = predNumeric > 0.5; %! %! figure %! scatter (XGrid(gidx,1), XGrid(gidx,2), 'markerfacecolor', 'magenta'); %! hold on %! scatter (XGrid(! gidx,1), XGrid(! gidx,2), 'markerfacecolor', 'red'); %! plot (X(Y == 0, 1), X(Y == 0, 2), 'ko', X(Y == 1, 1), X(Y == 1, 2), 'kx'); %! xlabel ('Feature 1'); %! ylabel ('Feature 2'); %! title ('Generalized Additive Model (GAM) Decision Boundary'); %! legend ({'Class 1 Region', 'Class 0 Region', ... %! 'Class 1 Samples', 'Class 0 Samples'}, ... %! 'location', 'northwest') %! axis tight %! hold off ## Tests %!demo %! ## Fit from a table, and predict on one %! %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! X = meas(inds,:); %! T = table (X(:,1), X(:,2), X(:,3), X(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species(inds)); %! %! ## A column holding levels is a categorical predictor without being named %! ## one %! T.Wide = categorical (X(:,2) > 2.9, [false true], {'narrow', 'wide'}); %! %! ## The response is named by its column, and everything else is a predictor %! Mdl = fitcgam (T, 'Species'); %! Mdl.PredictorNames %! Mdl.CategoricalPredictors %! %! ## predict reads a table by the names the model was fitted on, so the %! ## columns may come in any order %! label = predict (Mdl, T(1:5, [6, 5, 4, 3, 2, 1])); %! label' %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = [0; 0; 1; 1]; %! PredictorNames = {'Feature1', 'Feature2', 'Feature3'}; %! a = fitcgam (x, y, 'FitMethod', 'splines', ... %! 'PredictorNames', PredictorNames); %! assert_equal (class (a), "ClassificationGAM"); %! assert_equal ({a.X, a.Y, a.NumObservations}, {x, y, 4}) %! assert_equal ({a.NumPredictors, a.ResponseName}, {3, 'Y'}) %! assert_equal (a.ClassNames, [0; 1]) %! assert_equal (a.PredictorNames, PredictorNames) %! assert_equal (a.BaseModel.Intercept, 0) %!test %! x = [1, 2; 3, 4; 5, 6; 7, 8; 9, 10]; %! y = [1; 0; 1; 0; 1]; %! a = fitcgam (x, y, 'FitMethod', 'splines', 'interactions', 'all'); %! assert_equal (class (a), "ClassificationGAM"); %! assert_equal ({a.X, a.Y, a.NumObservations}, {x, y, 5}) %! assert_equal ({a.NumPredictors, a.ResponseName}, {2, 'Y'}) %! assert_equal (a.ClassNames, [0; 1]) %! assert_equal (a.PredictorNames, {'x1', 'x2'}) %! assert_equal (a.ModelwInt.Intercept, 0.4055, 1e-1) %!test %! load fisheriris %! inds = strcmp (species,'versicolor') | strcmp (species,'virginica'); %! X = meas(inds, :); %! Y = species(inds, :)'; %! Y = strcmp (Y, 'virginica')'; %! a = fitcgam (X, Y, 'FitMethod', 'splines', ... %! 'Formula', 'Y ~ x1 + x2 + x3 + x4 + x1:x2 + x2:x3'); %! assert_equal (class (a), "ClassificationGAM"); %! assert_equal ({a.X, a.Y, a.NumObservations}, {X, Y, 100}) %! assert_equal ({a.NumPredictors, a.ResponseName}, {4, 'Y'}) %! assert_equal (a.ClassNames, logical ([0; 1])) %! assert_equal (a.Formula, 'Y ~ x1 + x2 + x3 + x4 + x1:x2 + x2:x3') %! assert_equal (a.PredictorNames, {'x1', 'x2', 'x3', 'x4'}) %! assert_equal (a.ModelwInt.Intercept, 0) ## Test input validation %!error fitcgam () %!error fitcgam (ones (4,1)) %!error %! fitcgam (ones (4,2), ones (4, 1), 'K') %!error %! fitcgam (ones (4,2), ones (3, 1)) %!error %! fitcgam (ones (4,2), ones (3, 1), 'K', 2) %!test # MATLAB parity: classes given as text are sorted %! load fisheriris %! k = [101:150, 51:100]; %! Mdl = fitcgam (meas(k,:), species(k)); %! assert_equal (Mdl.ClassNames, {'versicolor'; 'virginica'}); %!test # MATLAB parity: a given ClassNames order is kept %! load fisheriris %! Mdl = fitcgam (meas(51:150,:), species(51:150), ... %! 'ClassNames', {'virginica'; 'versicolor'}); %! assert_equal (Mdl.ClassNames, {'virginica'; 'versicolor'}); %!error ... %! load fisheriris %! fitcgam (meas(51:150,:), species(51:150), 'ClassNames', [3, 2]) ## Table input %!shared fcgT %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! fcgX = meas(inds,:); %! fcgT = table (fcgX(:,1), fcgX(:,2), fcgX(:,3), fcgX(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! fcgT.Species = categorical (species(inds)); %! fcgT.Wide = categorical (fcgX(:,2) > 2.9, [false true], ... %! {'narrow', 'wide'}); %!test # the response is named by a column and the rest are predictors %! Mdl = fitcgam (fcgT, 'Species'); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW', 'PL', 'PW', 'Wide'}); %! assert_equal (Mdl.ResponseName, 'Species'); %!test # a column holding levels is a categorical predictor of its own accord %! Mdl = fitcgam (fcgT, 'Species'); %! assert_equal (Mdl.CategoricalPredictors, 5); %!test # a model formula names the response and the predictors together %! Mdl = fitcgam (fcgT, 'Species ~ SL + Wide'); %! assert_equal (Mdl.PredictorNames, {'SL', 'Wide'}); %! assert_equal (Mdl.CategoricalPredictors, 2); %!test # the response may be given beside a table of predictors %! Mdl = fitcgam (fcgT(:,1:4), fcgT.Species); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW', 'PL', 'PW'}); %! assert_equal (Mdl.ResponseName, 'Y'); %!test # predict takes a table, matched by name and not by position %! Mdl = fitcgam (fcgT, 'Species'); %! a = predict (Mdl, fcgT); %! assert_equal (class (a), 'categorical'); %! assert_equal (predict (Mdl, fcgT(:, [6, 5, 4, 3, 2, 1])), a); %!test # the levels travel with the model when it is made compact %! Mdl = fitcgam (fcgT, 'Species'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.PredictorLevels, Mdl.PredictorLevels); %! assert_equal (predict (CMdl, fcgT), predict (Mdl, fcgT)); %!error ... %! fitcgam (fcgT, 'NoSuch') %!error ... %! fitcgam (fcgT, 'Species ~ SL*PW') %!error ... %! predict (fitcgam (fcgT, 'Species'), fcgT(:, [1, 3, 4, 5, 6])) statistics-release-2.1.2/inst/Machine_Learning/fitckernel.m000066400000000000000000000226311527311104300237710ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} fitckernel (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitckernel (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{Mdl} =} fitckernel (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{Mdl} =} fitckernel (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitckernel (@dots{}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{Mdl}, @var{FitInfo}] =} fitckernel (@dots{}) ## ## Fit a Gaussian kernel binary classifier. ## ## @code{@var{Mdl} = fitckernel (@var{X}, @var{Y})} returns a ## @qcode{ClassificationKernel} object fitted to the predictor data @var{X} ## and the two class response @var{Y}, where @var{X} is an @math{NxP} ## numeric matrix and @var{Y} has as many rows as @var{X}. ## ## @code{@var{Mdl} = fitckernel (@dots{}, @var{name}, @var{value})} passes ## the given @qcode{Name-Value} pairs to the model. They are documented ## under @code{ClassificationKernel}, and the ones most often wanted are ## @qcode{'Learner'}, @qcode{'NumExpansionDimensions'}, ## @qcode{'KernelScale'}, @qcode{'Lambda'}, @qcode{'BoxConstraint'} and ## @qcode{'Standardize'}. ## ## @code{[@var{Mdl}, @var{FitInfo}] = fitckernel (@dots{})} also returns a ## structure describing the optimization: the objective it reached, the ## gradient it left, and the tolerances it was given. ## ## @code{@var{Mdl} = fitckernel (@dots{}, @var{cvopt}, @var{value})} returns a ## @code{ClassificationPartitionedKernel} ## instead when one of @qcode{'CrossVal'}, @qcode{'KFold'}, ## @qcode{'Holdout'}, @qcode{'Leaveout'} and @qcode{'CVPartition'} is ## given. A cross-validated model describes no single fit, so ## @var{FitInfo} is not available beside it. ## ## @seealso{ClassificationKernel, ClassificationLinear, fitclinear} ## @end deftypefn function [Mdl, FitInfo] = fitckernel (X, Y, varargin) ## Check input parameters if (nargin < 2) error ("fitckernel: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error ("fitckernel: name-value arguments must be in pairs."); endif ## A cross-validation option asks for a partitioned model rather than a ## fitted one, and the two are different classes with different methods. ## MATLAB refuses a second output there, having no single fit to describe, ## and so does this. ## 'CrossVal' is the one that carries a value saying whether to cross ## validate at all; the other four ask for it by being present. cvNames = {'kfold', 'holdout', 'leaveout', 'cvpartition'}; crossval = false; for k = 1:2:numel (varargin) if (! ischar (varargin{k})) continue; endif if (any (strcmpi (varargin{k}, cvNames))) crossval = true; elseif (strcmpi (varargin{k}, 'crossval')) val = varargin{k+1}; if (! (ischar (val) && any (strcmpi (val, {'on', 'off'})))) error ("%s: 'CrossVal' must be either 'on' or 'off'.", 'fitckernel'); endif crossval = crossval || strcmpi (val, 'on'); endif endfor if (crossval) if (nargout > 1) error (strcat ("fitckernel: a cross validated model has no", ... " FitInfo to return; ask for the model alone.")); endif Mdl = ClassificationPartitionedKernel (X, Y, varargin{:}); return; endif ## 'CrossVal', 'off' has said its piece and the learner does not take it. keep = true (1, numel (varargin)); for k = 1:2:numel (varargin) if (ischar (varargin{k}) && strcmpi (varargin{k}, 'crossval')) keep(k:k+1) = false; endif endfor varargin = varargin(keep); Mdl = ClassificationKernel (X, Y, varargin{:}); if (nargout > 1) FitInfo = fitInfo_ (Mdl); endif endfunction %!demo %! ## Fit a Gaussian kernel classifier to the two overlapping iris species %! ## and read what the optimization did. %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! [Mdl, FitInfo] = fitckernel (X, Y) %!demo %! ## Fit from a table, and predict on one %! %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! X = meas(inds,:); %! T = table (X(:,1), X(:,2), X(:,3), X(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species(inds)); %! %! ## A column holding levels is a categorical predictor without being named %! ## one %! T.Wide = categorical (X(:,2) > 2.9, [false true], {'narrow', 'wide'}); %! %! ## The response is named by its column, and everything else is a predictor %! Mdl = fitckernel (T, 'Species'); %! Mdl.PredictorNames %! Mdl.CategoricalPredictors %! %! ## A model formula names them instead, holding main effects only %! Mdl2 = fitckernel (T, 'Species ~ PL + Wide'); %! Mdl2.PredictorNames %! %! ## predict reads a table by the names the model was fitted on, so the %! ## columns may come in any order and may carry more than the model needs %! label = predict (Mdl, T(1:5, [6, 5, 4, 3, 2, 1])); %! label' %!test %! ## The driver returns a kernel classifier %! load fisheriris %! Mdl = fitckernel (meas(51:end,:), species(51:end)); %! assert_equal (class (Mdl), 'ClassificationKernel'); %! assert_equal (Mdl.NumExpansionDimensions, 128); %!test %! ## The options reach the model %! load fisheriris %! Mdl = fitckernel (meas(51:end,:), species(51:end), ... %! 'Learner', 'logistic', 'KernelScale', 2, ... %! 'NumExpansionDimensions', 64); %! assert_equal (Mdl.Learner, 'logistic'); %! assert_equal (Mdl.KernelScale, 2); %! assert_equal (Mdl.NumExpansionDimensions, 64); %!test %! ## The second output describes the optimization %! load fisheriris %! [~, FitInfo] = fitckernel (meas(51:end,:), species(51:end)); %! assert_equal (FitInfo.Solver, 'LBFGS-fast'); %! assert_equal (FitInfo.LossFunction, 'hinge'); %! assert_equal (FitInfo.Lambda, 0.01); %!test %! ## A cross-validation option returns a partitioned model instead %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! CVMdl = fitckernel (X, Y, 'KFold', 5); %! assert_equal (class (CVMdl), 'ClassificationPartitionedKernel'); %! assert_equal (CVMdl.KFold, 5); %! assert_equal (numel (CVMdl.Trained), 5); %!test %! ## 'CrossVal' on gives the ten folds it defaults to, and 'off' the model %! ## itself %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! CVMdl = fitckernel (X, Y, 'CrossVal', 'on'); %! assert_equal (CVMdl.KFold, 10); %! assert_equal (class (fitckernel (X, Y, 'CrossVal', 'off')), ... %! 'ClassificationKernel'); %!error ... %! [Mdl, FitInfo] = fitckernel (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'KFold', 3); ## Test input validation %!error fitckernel (ones (5, 2)) %!error ... %! fitckernel (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Learner') %!error ... %! fitckernel (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Learner', 'tree') %!test # MATLAB parity: classes given as text are sorted %! load fisheriris %! k = [101:150, 51:100]; %! Mdl = fitckernel (meas(k,:), species(k)); %! assert_equal (Mdl.ClassNames, {'versicolor'; 'virginica'}); %!test # MATLAB parity: a given ClassNames order is kept %! load fisheriris %! Mdl = fitckernel (meas(51:150,:), species(51:150), ... %! 'ClassNames', {'virginica'; 'versicolor'}); %! assert_equal (Mdl.ClassNames, {'virginica'; 'versicolor'}); %!error ... %! load fisheriris %! fitckernel (meas(51:150,:), species(51:150), 'ClassNames', [3, 2]) %!error ... %! load fisheriris %! fitckernel (meas(51:150,:), species(51:150), ... %! 'ClassNames', {'virginica'; 'rose'}) ## Table input %!shared fckT %! load fisheriris %! fckI = ! strcmp (species, 'setosa'); %! fckX = meas(fckI,:); %! fckT = table (fckX(:,1), fckX(:,2), 'VariableNames', {'SL', 'SW'}); %! fckT.Species = categorical (species(fckI)); %!test # the response is named by a column and the rest are predictors %! Mdl = fitckernel (fckT, 'Species'); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW'}); %! assert_equal (Mdl.ResponseName, 'Species'); %!test # predict takes a table, matched by name and not by position %! Mdl = fitckernel (fckT, 'Species'); %! a = predict (Mdl, fckT); %! assert_equal (predict (Mdl, fckT(:, [3, 2, 1])), a); %!test # a cross-validated fit takes a table too %! Mdl = fitckernel (fckT, 'Species', 'KFold', 3); %! assert_equal (class (Mdl), 'ClassificationPartitionedKernel'); %!error ... %! fitckernel (fckT, 'NoSuch') statistics-release-2.1.2/inst/Machine_Learning/fitcknn.m000066400000000000000000000755451527311104300233130ustar00rootroot00000000000000## Copyright (C) 2023 Mohammed Azmat Khan ## Copyright (C) 2023-2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} fitcknn (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcknn (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcknn (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcknn (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcknn (@dots{}, @var{name}, @var{value}) ## ## Fit a k-Nearest Neighbor classification model. ## ## @code{@var{Mdl} = fitcknn (@var{X}, @var{Y})} returns a k-Nearest Neighbor ## classification model, @var{Mdl}, with @var{X} being the predictor data, and ## @var{Y} the class labels of observations in @var{X}. ## ## @itemize ## @item ## @code{X} must be a @math{N*P} numeric matrix of predictor data where rows ## correspond to observations and columns correspond to features or variables. ## @item ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels of ## corresponding predictor data in @var{X}. @var{Y} can be numerical, logical, ## char array or cell array of character vectors. @var{Y} must have same number ## of rows as @var{X}. ## @end itemize ## ## @code{@var{Mdl} = fitcknn (@dots{}, @var{name}, @var{value})} returns a ## k-Nearest Neighbor classification model with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @subheading Model Parameters ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Standardize'} @tab A boolean flag indicating whether ## the data in @var{X} should be standardized prior to training. ## ## @item @qcode{'CategoricalPredictors'} @tab Empty (default) or @qcode{'all'}, ## which takes every predictor as categorical and makes @qcode{'hamming'} the ## default distance, so observations are compared by the levels they share. ## A list of some of the predictors is refused, as MATLAB refuses it. ## ## @item @qcode{'PredictorNames'} @tab A cell array of character vectors ## specifying the predictor variable names. The variable names are assumed to ## be in the same order as they appear in the training data @var{X}. ## ## @item @qcode{'ResponseName'} @tab A character vector specifying the name ## of the response variable. ## ## @item @qcode{'ClassNames'} @tab Names of the classes in the class ## labels, @var{Y}, used for fitting the kNN model. @qcode{ClassNames} are of ## the same type as the class labels in @var{Y}. The model keeps the classes ## in this order; by default they are sorted. ## ## @item @qcode{'Prior'} @tab A numeric vector specifying the prior ## probabilities for each class. The order of the elements in @qcode{Prior} ## corresponds to the order of the classes in @qcode{ClassNames}. ## A class whose prior is 0 is removed from the data before fitting, with a ## warning, as in MATLAB. ## ## @item @qcode{'Weights'} @tab A single or double vector of nonnegative ## observation weights, one per row of @var{X}. Each class carries its prior, ## spread over its observations in proportion to their weights, and a neighbour ## votes with that weight. An empirical prior sums the weights per class, ## standardization uses weighted means and standard deviations, and a row of ## zero weight is left out. The model's @code{W} keeps the class of the ## weights, while every computation runs in double, so @code{Prior} is double ## where MATLAB returns single. ## ## @item @qcode{'Cost'} @tab A @math{N*R} numeric matrix containing ## misclassification cost for the corresponding instances in @var{X} where ## @math{R} is the number of unique categories in @var{Y}. If an instance is ## correctly classified into its category the cost is calculated to be 1, ## otherwise 0. cost matrix can be altered use @code{@var{Mdl.cost} = somecost}. ## default value @qcode{@var{cost} = ones(rows(X),numel(unique(Y)))}. ## ## @item @qcode{'ScoreTransform'} @tab A character vector defining one of ## the following functions or a user defined function handle, which is used ## for transforming the prediction scores returned by the @code{predict} and ## @code{resubPredict} methods. Default value is @qcode{'none'}. ## @end multitable ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Value} @tab @var{Description} ## @item @qcode{'doublelogit'} @tab @math{1 ./ (1 + exp (-2 * x))} ## @item @qcode{'invlogit'} @tab @math{log (x ./ (1 - x))} ## @item @qcode{'ismax'} @tab Sets the score for the class with the largest ## score to 1, and sets the scores for all other classes to 0 ## @item @qcode{'logit'} @tab @math{1 ./ (1 + exp (-x))} ## @item @qcode{'none'} @tab @math{x} (no transformation) ## @item @qcode{'identity'} @tab @math{x} (no transformation) ## @item @qcode{'sign'} @tab @math{-1 for x < 0, 0 for x = 0, 1 for x > 0} ## @item @qcode{'symmetric'} @tab @math{2 * x - 1} ## @item @qcode{'symmetricismax'} @tab Sets the score for the class with ## the largest score to 1, and sets the scores for all other classes to -1 ## @item @qcode{'symmetriclogit'} @tab @math{2 ./ (1 + exp (-x)) - 1} ## @end multitable ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'BreakTies'} @tab Tie-breaking algorithm used by predict ## when multiple classes have the same smallest cost. By default, ties occur ## when multiple classes have the same number of nearest points among the ## @math{k} nearest neighbors. The available options are specified by the ## following character arrays: ## @end multitable ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Value} @tab @var{Description} ## ## @item @qcode{'smallest'} @tab This is the default and it favors the ## class with the smallest index among the tied groups, i.e. the one that ## appears first in the training labelled data. ## @item @qcode{'nearest'} @tab This favors the class with the nearest ## neighbor among the tied groups, i.e. the class with the closest member point ## according to the distance metric used. ## @item @qcode{'random'} @tab This randomly picks one class among the ## tied groups. ## @end multitable ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'BucketSize'} @tab The maximum number of data points in the ## leaf node of the Kd-tree and it must be a positive integer. By default, it ## is 50. This argument is meaningful only when the selected search method is ## @qcode{'kdtree'}. ## ## @item @qcode{'NumNeighbors'} @tab A positive integer value specifying ## the number of nearest neighbors to be found in the kNN search. By default, ## it is 1. ## ## @item @qcode{'Exponent'} @tab A positive scalar (usually an integer) ## specifying the Minkowski distance exponent. This argument is only valid when ## the selected distance metric is @qcode{'minkowski'}. By default it is 2. ## An exponent of @code{Inf} gives the Chebychev distance. ## ## @item @qcode{'Scale'} @tab A nonnegative numeric vector specifying the ## scale parameters for the standardized Euclidean distance. The vector length ## must be equal to the number of columns in @var{X}. This argument is only ## valid when the selected distance metric is @qcode{'seuclidean'}, in which ## case each coordinate of @var{X} is scaled by the corresponding element of ## @qcode{'scale'}, as is each query point in @var{Y}. By default, the scale ## parameter is the standard deviation of each coordinate in @var{X}. If a ## variable in @var{X} is constant, i.e. zero variance, this value is forced ## to 1 to avoid division by zero. This is the equivalent of this variable not ## being standardized. ## ## @item @qcode{'Cov'} @tab A square matrix with the same number of columns ## as @var{X} specifying the covariance matrix for computing the mahalanobis ## distance. This must be a positive definite matrix matching. This argument ## is only valid when the selected distance metric is @qcode{'mahalanobis'}. ## ## @item @qcode{'Distance'} @tab is the distance metric used by ## @code{knnsearch} as specified below: ## @end multitable ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Value} @tab @var{Description} ## ## @item @qcode{'euclidean'} @tab Euclidean distance. ## @item @qcode{'seuclidean'} @tab standardized Euclidean distance. Each ## coordinate difference between the rows in @var{X} and the query matrix ## @var{Y} is scaled by dividing by the corresponding element of the standard ## deviation computed from @var{X}. To specify a different scaling, use the ## @qcode{'Scale'} name-value argument. ## @item @qcode{'cityblock'} @tab City block distance. ## @item @qcode{'chebychev'} @tab Chebychev distance (maximum coordinate ## difference). ## @item @qcode{'minkowski'} @tab Minkowski distance. The default exponent ## is 2. To specify a different exponent, use the @qcode{'P'} name-value ## argument. ## @item @qcode{'mahalanobis'} @tab Mahalanobis distance, computed using a ## positive definite covariance matrix. To change the value of the covariance ## matrix, use the @qcode{'Cov'} name-value argument. ## @item @qcode{'cosine'} @tab Cosine distance. ## @item @qcode{'correlation'} @tab One minus the sample linear correlation ## between observations (treated as sequences of values). ## @item @qcode{'spearman'} @tab One minus the sample Spearman's rank ## correlation between observations (treated as sequences of values). ## @item @qcode{'hamming'} @tab Hamming distance, which is the percentage ## of coordinates that differ. ## @item @qcode{'jaccard'} @tab One minus the Jaccard coefficient, which is ## the percentage of nonzero coordinates that differ. ## @item @var{@@distfun} @tab Custom distance function handle. A distance ## function of the form @code{function @var{D2} = distfun (@var{XI}, @var{YI})}, ## where @var{XI} is a @math{1*P} vector containing a single observation in ## @math{P}-dimensional space, @var{YI} is an @math{N*P} matrix containing an ## arbitrary number of observations in the same @math{P}-dimensional space, and ## @var{D2} is an @math{N*P} vector of distances, where @qcode{(@var{D2}k)} is ## the distance between observations @var{XI} and @qcode{(@var{YI}k,:)}. ## @end multitable ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'DistanceWeight'} @tab A distance weighting function, ## specified either as a function handle, which accepts a matrix of nonnegative ## distances and returns a matrix the same size containing nonnegative distance ## weights, or one of the following values: @qcode{'equal'}, which corresponds ## to no weighting; @qcode{'inverse'}, which corresponds to a weight equal to ## @math{1/distance}; @qcode{'squaredinverse'}, which corresponds to a weight ## equal to @math{1/distance^2}. ## ## @item @qcode{'CacheSize'} @tab A positive scalar, the cache size in ## megabytes, 1000 by default. It is stored and reported for compatibility ## and @strong{does not affect the fit or any prediction}: a nearest-neighbour ## model keeps no Gram matrix to cache, holding its training data and ## computing each distance when asked. MATLAB hides the same property from ## @code{properties}, where this package reports it. ## ## @item @qcode{'IncludeTies'} @tab A boolean flag to indicate if the ## returned values should contain the indices that have same distance as the ## @math{K^th} neighbor. When @qcode{false}, @code{knnsearch} chooses the ## observation with the smallest index among the observations that have the same ## distance from a query point. When @qcode{true}, @code{knnsearch} includes ## all nearest neighbors whose distances are equal to the @math{K^th} smallest ## distance in the output arguments. To specify @math{K}, use the @qcode{'K'} ## name-value pair argument. ## ## @item @qcode{'NSMethod'} @tab is the nearest neighbor search method used ## by @code{knnsearch} as specified below. ## @end multitable ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Value} @tab @var{Description} ## ## @item @qcode{'kdtree'} @tab Creates and uses a Kd-tree to find nearest ## neighbors. @qcode{'kdtree'} is the default value when the number of columns ## in @var{X} is less than or equal to 10, @var{X} is not sparse, and the ## distance metric is @qcode{'euclidean'}, @qcode{'cityblock'}, ## @qcode{'manhattan'}, @qcode{'chebychev'}, or @qcode{'minkowski'}. Otherwise, ## the default value is @qcode{'exhaustive'}. This argument is only valid when ## the distance metric is one of the four aforementioned metrics. ## @item @qcode{'exhaustive'} @tab Uses the exhaustive search algorithm by ## computing the distance values from all the points in @var{X} to each point in ## @var{Y}. ## @end multitable ## ## @subheading Cross Validation Options ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Crossval'} @tab Cross-validation flag specified as ## @qcode{'on'} or @qcode{'off'}. If @qcode{'on'} is specified, a 10-fold ## cross validation is performed and a @code{ClassificationPartitionedModel} is ## returned in @var{Mdl}. To override this cross-validation setting, use only ## one of the following Name-Value pair arguments. ## ## @item @qcode{'CVPartition'} @tab A @code{cvpartition} object that ## specifies the type of cross-validation and the indexing for the training and ## validation sets. A @code{ClassificationPartitionedModel} is returned in ## @var{Mdl} and the trained model is stored in the @code{Trained} property. ## ## @item @qcode{'Holdout'} @tab Fraction of the data used for holdout ## validation, specified as a scalar value in the range @math{[0,1]}. When ## specified, a randomly selected percentage is reserved as validation data and ## the remaining set is used for training. The trained model is stored in the ## @code{Trained} property of the @code{ClassificationPartitionedModel} returned ## in @var{Mdl}. @qcode{'Holdout'} partitioning attempts to ensure that each ## partition represents the classes proportionately. ## ## @item @qcode{'KFold'} @tab Number of folds to use in the cross-validated ## model, specified as a positive integer value greater than 1. When specified, ## then the data is randomly partitioned in @math{k} sets and for each set, the ## set is reserved as validation data while the remaining @math{k-1} sets are ## used for training. The trained models are stored in the @code{Trained} ## property of the @code{ClassificationPartitionedModel} returned in @var{Mdl}. ## @qcode{'KFold'} partitioning attempts to ensure that each partition ## represents the classes proportionately. ## ## @item @qcode{'Leaveout'} @tab Leave-one-out cross-validation flag ## specified as @qcode{'on'} or @qcode{'off'}. If @qcode{'on'} is specified, ## then for each of the @math{n} observations (where @math{n} is the number of ## observations, excluding missing observations, specified in the ## @code{NumObservations} property of the model), one observation is reserved as ## validation data while the remaining observations are used for training. The ## trained models are stored in the @code{Trained} property of the ## @code{ClassificationPartitionedModel} returned in @var{Mdl}. ## @end multitable ## ## @seealso{ClassificationKNN, ClassificationPartitionedModel, knnsearch, ## rangesearch, pdist2} ## @end deftypefn function Mdl = fitcknn (X, Y, varargin) ## Check input parameters if (nargin < 2) error ("fitcknn: too few arguments."); endif if (mod (nargin, 2) != 0) error ("fitcknn: Name-Value arguments must be in pairs."); endif ## Check predictor data and labels have equal rows if (! istable (X) && rows (X) != rows (Y)) error ("fitcknn: number of rows in X and Y must be equal."); endif ## Check optional input parameters for cross-validation options cv_opt = false; cv_arg = 0; args = {}; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'crossval' CrossVal = varargin{2}; if (! any (strcmp (CrossVal, {'off', 'on'}))) error ("fitcknn: 'CrossVal' must be either 'off' or 'on'."); endif if (strcmp (CrossVal, 'on')) cv_opt = true; endif case 'kfold' Name = 'KFold'; Value = varargin{2}; cv_arg += 1; cv_opt = true; case 'holdout' Name = 'Holdout'; Value = varargin{2}; cv_arg += 1; cv_opt = true; case 'leaveout' Name = 'Leaveout'; Value = varargin{2}; cv_arg += 1; cv_opt = true; case 'cvpartition' Name = 'CVPartition'; Value = varargin{2}; cv_arg += 1; cv_opt = true; otherwise args = [args, {varargin{1}, varargin{2}}]; endswitch varargin(1:2) = []; endwhile ## Check for multiple cross-validation paired arguments if (cv_arg > 1) error (strcat ("fitcknn: You can use only one cross-validation", ... " name-value pair argument at a time to create a", ... " cross-validated model.")); endif ## Parse arguments to class def function Mdl = ClassificationKNN (X, Y, args{:}); ## If cross validation has been requested, ## return a ClassificationPartitionedModel if (cv_opt) if (cv_arg) Mdl = crossval (Mdl, Name, Value); else Mdl = crossval (Mdl); endif endif endfunction %!demo %! ## Train a k-nearest neighbor classifier for k = 10 %! ## and plot the decision boundaries. %! %! load fisheriris %! idx = ! strcmp (species, 'setosa'); %! X = meas(idx,3:4); %! Y = cast (strcmpi (species(idx), 'virginica'), 'double'); %! obj = fitcknn (X, Y, 'Standardize', 1, 'NumNeighbors', 10, 'NSMethod', 'exhaustive') %! x1 = [min(X(:,1)):0.03:max(X(:,1))]; %! x2 = [min(X(:,2)):0.02:max(X(:,2))]; %! [x1G, x2G] = meshgrid (x1, x2); %! XGrid = [x1G(:), x2G(:)]; %! pred = predict (obj, XGrid); %! gidx = logical (pred); %! %! figure %! scatter (XGrid(gidx,1), XGrid(gidx,2), 'markerfacecolor', 'magenta'); %! hold on %! scatter (XGrid(! gidx,1), XGrid(! gidx,2), 'markerfacecolor', 'red'); %! plot (X(Y == 0, 1), X(Y == 0, 2), 'ko', X(Y == 1, 1), X(Y == 1, 2), 'kx'); %! xlabel ('Petal length (cm)'); %! ylabel ('Petal width (cm)'); %! title ('5-Nearest Neighbor Classifier Decision Boundary'); %! legend ({'Versicolor Region', 'Virginica Region', ... %! 'Sampled Versicolor', 'Sampled Virginica'}, ... %! 'location', 'northwest') %! axis tight %! hold off ## Test Output %!demo %! ## Fit from a table, and predict on one %! %! load fisheriris %! T = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species); %! %! ## The response is named by its column, and everything else is a predictor %! Mdl = fitcknn (T, 'Species'); %! Mdl.PredictorNames %! Mdl.ResponseName %! %! ## predict reads a table by the names the model was fitted on, so the %! ## columns may come in any order %! label = predict (Mdl, T(1:5, [5, 4, 3, 2, 1])); %! label' %! %! ## This learner takes every predictor as holding levels or none of them, %! ## so a table whose columns all hold levels is measured by the Hamming %! ## distance instead %! C = table (categorical (meas(:,1) > 5.8, [false true], {'short', 'long'}), ... %! categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}), ... %! 'VariableNames', {'Long', 'Wide'}); %! C.Species = categorical (species); %! CMdl = fitcknn (C, 'Species'); %! CMdl.Distance %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = fitcknn (x, y); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 1}) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = fitcknn (x, y, 'NSMethod', 'exhaustive'); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 1}) %! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! k = 10; %! a = fitcknn (x, y, 'NumNeighbors' ,k); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 4}) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = ones (4, 11); %! y = ['a'; 'a'; 'b'; 'b']; %! k = 10; %! a = fitcknn (x, y, 'NumNeighbors' ,k); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 4}) %! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! k = 10; %! a = fitcknn (x, y, 'NumNeighbors' ,k, 'NSMethod', 'exhaustive'); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 4}) %! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! k = 10; %! a = fitcknn (x, y, 'NumNeighbors' ,k, 'Distance', 'hamming'); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 4}) %! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'hamming'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! weights = ones (4,1); %! a = fitcknn (x, y, 'Standardize', 1); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 1}) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %! assert_equal ({a.Sigma}, {std(x, [], 1)}) %! assert_equal ({a.Mu}, {[3.75, 4.25, 4.75]}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! weights = ones (4,1); %! a = fitcknn (x, y, 'Standardize', false); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 1}) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %! assert_equal ({a.Sigma}, {[]}) %! assert_equal ({a.Mu}, {[]}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! s = ones (1, 3); %! a = fitcknn (x, y, 'Scale' , s, 'Distance', 'seuclidean'); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.DistParameter}, {s}) %! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'seuclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = fitcknn (x, y, 'Exponent' , 5, 'Distance', 'minkowski'); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal (a.DistParameter, 5) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'minkowski'}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = fitcknn (x, y, 'Exponent' , 5, 'Distance', 'minkowski', ... %! 'NSMethod', 'exhaustive'); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal (a.DistParameter, 5) %! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'minkowski'}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = fitcknn (x, y, 'BucketSize' , 20, 'distance', 'mahalanobis'); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'mahalanobis'}) %! assert_equal ({a.BucketSize}, {20}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = fitcknn (x, y, 'IncludeTies', true); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal (a.IncludeTies, true); %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = fitcknn (x, y); %! assert_equal (class (a), "ClassificationKNN"); %! assert_equal (a.IncludeTies, false); %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! a = fitcknn (x, y); %! assert_equal (class (a), "ClassificationKNN") %! assert_equal (a.Prior, [0.5, 0.5]) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! prior = [0.5, 0.5]; %! a = fitcknn (x, y, 'Prior', 'empirical'); %! assert_equal (class (a), "ClassificationKNN") %! assert_equal (a.Prior, prior) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'a'; 'b']; %! prior = [0.75, 0.25]; %! a = fitcknn (x, y, 'Prior', 'empirical'); %! assert_equal (class (a), "ClassificationKNN") %! assert_equal (a.Prior, prior) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'a'; 'b']; %! prior = [0.5, 0.5]; %! a = fitcknn (x, y, 'Prior', 'uniform'); %! assert_equal (class (a), "ClassificationKNN") %! assert_equal (a.Prior, prior) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! cost = [0, 1; 1, 0]; %! a = fitcknn (x, y, 'Cost', cost); %! assert_equal (class (a), "ClassificationKNN") %! assert_equal (a.Cost, [0, 1; 1, 0]) %! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! cost = [0, 1; 1, 0]; %! a = fitcknn (x, y, 'Cost', cost, 'Distance', 'hamming' ); %! assert_equal (class (a), "ClassificationKNN") %! assert_equal (a.Cost, [0, 1; 1, 0]) %! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'hamming'}) %! assert_equal ({a.BucketSize}, {50}) %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = ['a'; 'a'; 'b'; 'b']; %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! a = fitcknn (x, y, 'NSMethod', 'exhaustive', 'CrossVal', 'on'); %! warning (status); %! assert_equal (class (a), "ClassificationPartitionedModel"); %! assert_equal ({a.X, a.Y, a.Trained{1}.NumNeighbors}, {x, y, 1}) %! assert_equal (a.ModelParameters.NSMethod, "exhaustive") %! assert_equal (a.ModelParameters.Distance, "euclidean") %! assert_equal ({a.Trained{1}.BucketSize}, {50}) ## Test input validation %!error fitcknn () %!error fitcknn (ones (4,1)) %!error %! fitcknn (ones (4,2), ones (4, 1), 'K') %!error %! fitcknn (ones (4,2), ones (3, 1)) %!error %! fitcknn (ones (4,2), ones (3, 1), 'K', 2) %!error %! fitcknn (ones (4,2), ones (4, 1), 'CrossVal', 2) %!error %! fitcknn (ones (4,2), ones (4, 1), 'CrossVal', 'a') %!error ... %! fitcknn (ones (4,2), ones (4, 1), 'KFold', 10, 'Holdout', 0.3) %!test # 'Leaveout' leaves one observation out of each fold %! load fisheriris %! CVMdl = fitcknn (meas, species, 'Leaveout', 'on'); %! assert_equal (CVMdl.KFold, 150); %!test # MATLAB parity: classes given as text are sorted %! load fisheriris %! k = [101:150, 1:50, 51:100]; %! Mdl = fitcknn (meas(k,:), species(k)); %! assert_equal (Mdl.ClassNames, {'setosa'; 'versicolor'; 'virginica'}); %!test # MATLAB parity: a given ClassNames order is kept %! load fisheriris %! Mdl = fitcknn (meas(1:150,:), species(1:150), ... %! 'ClassNames', {'virginica'; 'setosa'; 'versicolor'}); %! assert_equal (Mdl.ClassNames, {'virginica'; 'setosa'; 'versicolor'}); %!error ... %! load fisheriris %! fitcknn (meas, species, 'ClassNames', [3, 1, 2]) ## Table input %!shared fknT %! load fisheriris %! fknT = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! fknT.Species = categorical (species); %!test # the response is named by a column and the rest are predictors %! Mdl = fitcknn (fknT, 'Species'); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW', 'PL', 'PW'}); %! assert_equal (Mdl.ResponseName, 'Species'); %!test # a model formula names the response and the predictors together %! Mdl = fitcknn (fknT, 'Species ~ PW + SL'); %! assert_equal (Mdl.PredictorNames, {'PW', 'SL'}); %!test # predict takes a table, matched by name and not by position %! Mdl = fitcknn (fknT, 'Species'); %! a = predict (Mdl, fknT); %! assert_equal (class (a), 'categorical'); %! assert_equal (predict (Mdl, fknT(:, [5, 4, 3, 2, 1])), a); ## This learner takes every predictor as holding levels or none of them, so ## a table whose columns all hold levels names them all and is taken %!test %! load fisheriris %! C = table (categorical (meas(:,1) > 5.8, [false true], ... %! {'short', 'long'}), ... %! categorical (meas(:,2) > 3, [false true], ... %! {'narrow', 'wide'}), ... %! 'VariableNames', {'Long', 'Wide'}); %! C.Species = categorical (species); %! Mdl = fitcknn (C, 'Species'); %! assert_equal (Mdl.CategoricalPredictors, [1, 2]); %! assert_equal (Mdl.Distance, 'hamming'); ## A table mixing levels with numbers is refused, as it always was: this ## learner takes every predictor as holding levels or none of them %!error %! load fisheriris %! M = table (meas(:,1), 'VariableNames', {'SL'}); %! M.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! M.Species = categorical (species); %! fitcknn (M, 'Species'); %!error ... %! fitcknn (fknT, 'NoSuch') %!error ... %! fitcknn (fknT, 'Species ~ SL*PW') %!error ... %! predict (fitcknn (fknT, 'Species'), fknT(:, [1, 3, 4, 5])) statistics-release-2.1.2/inst/Machine_Learning/fitclinear.m000066400000000000000000000250631527311104300237650ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} fitclinear (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitclinear (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{Mdl} =} fitclinear (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{Mdl} =} fitclinear (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitclinear (@dots{}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{Mdl}, @var{FitInfo}] =} fitclinear (@dots{}) ## ## Fit a linear binary classifier. ## ## @code{@var{Mdl} = fitclinear (@var{X}, @var{Y})} returns a ## @qcode{ClassificationLinear} object fitted to the predictor data @var{X} ## and the two class response @var{Y}, where @var{X} is an @math{NxP} ## numeric matrix and @var{Y} has as many rows as @var{X}. ## ## @code{@var{Mdl} = fitclinear (@dots{}, @var{name}, @var{value})} passes ## the given @qcode{Name-Value} pairs to the model. They are documented ## under @code{ClassificationLinear}, and the ones most often wanted are ## @qcode{'Learner'}, @qcode{'Regularization'}, @qcode{'Lambda'}, ## @qcode{'Solver'} and @qcode{'ObservationsIn'}. ## ## @code{[@var{Mdl}, @var{FitInfo}] = fitclinear (@dots{})} also returns a ## structure describing the optimization: what it converged to, how far it ## got, and which tolerance stopped it. Its fields follow the solver, so a ## dual fit reports the dual variables and a mini-batch fit the batch it ## stopped on. ## ## @code{@var{Mdl} = fitclinear (@dots{}, @var{cvopt}, @var{value})} returns a ## @code{ClassificationPartitionedLinear} ## instead when one of @qcode{'CrossVal'}, @qcode{'KFold'}, ## @qcode{'Holdout'}, @qcode{'Leaveout'} and @qcode{'CVPartition'} is ## given. A cross-validated model describes no single fit, so ## @var{FitInfo} is not available beside it. ## ## @seealso{ClassificationLinear, ClassificationKernel, fitckernel} ## @end deftypefn function [Mdl, FitInfo] = fitclinear (X, Y, varargin) ## Check input parameters if (nargin < 2) error ("fitclinear: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error ("fitclinear: name-value arguments must be in pairs."); endif ## A cross-validation option asks for a partitioned model rather than a ## fitted one, and the two are different classes with different methods. ## MATLAB refuses a second output there, having no single fit to describe, ## and so does this. ## 'CrossVal' is the one that carries a value saying whether to cross ## validate at all; the other four ask for it by being present. cvNames = {'kfold', 'holdout', 'leaveout', 'cvpartition'}; crossval = false; for k = 1:2:numel (varargin) if (! ischar (varargin{k})) continue; endif if (any (strcmpi (varargin{k}, cvNames))) crossval = true; elseif (strcmpi (varargin{k}, 'crossval')) val = varargin{k+1}; if (! (ischar (val) && any (strcmpi (val, {'on', 'off'})))) error ("%s: 'CrossVal' must be either 'on' or 'off'.", 'fitclinear'); endif crossval = crossval || strcmpi (val, 'on'); endif endfor if (crossval) if (nargout > 1) error (strcat ("fitclinear: a cross validated model has no", ... " FitInfo to return; ask for the model alone.")); endif Mdl = ClassificationPartitionedLinear (X, Y, varargin{:}); return; endif ## 'CrossVal', 'off' has said its piece and the learner does not take it. keep = true (1, numel (varargin)); for k = 1:2:numel (varargin) if (ischar (varargin{k}) && strcmpi (varargin{k}, 'crossval')) keep(k:k+1) = false; endif endfor varargin = varargin(keep); Mdl = ClassificationLinear (X, Y, varargin{:}); if (nargout > 1) FitInfo = fitInfo_ (Mdl); endif endfunction %!demo %! ## Fit a linear classifier to the two overlapping iris species and read %! ## what the optimization did. %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! [Mdl, FitInfo] = fitclinear (X, Y, 'Learner', 'logistic') %!demo %! ## Fit from a table, and predict on one %! %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! X = meas(inds,:); %! T = table (X(:,1), X(:,2), X(:,3), X(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = species(inds); %! %! ## The response is named by its column, and everything else is a predictor %! Mdl = fitclinear (T, 'Species'); %! Mdl.PredictorNames %! Mdl.ResponseName %! %! ## predict reads a table by the names the model was fitted on, so the %! ## columns may come in any order %! label = predict (Mdl, T(1:5, [5, 4, 3, 2, 1])); %! label' %!test %! ## The driver returns what the class constructor returns %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! M1 = fitclinear (X, Y); %! M2 = ClassificationLinear (X, Y); %! assert_equal (class (M1), 'ClassificationLinear'); %! assert_equal (M1.Beta, M2.Beta); %! assert_equal (M1.Bias, M2.Bias); %!test %! ## The options reach the model %! load fisheriris %! Mdl = fitclinear (meas(51:end,:), species(51:end), ... %! 'Learner', 'logistic', 'Lambda', 0.05, ... %! 'ResponseName', 'species'); %! assert_equal (Mdl.Learner, 'logistic'); %! assert_equal (Mdl.Lambda, 0.05); %! assert_equal (Mdl.ResponseName, 'species'); %!test %! ## The second output describes the optimization %! load fisheriris %! [~, FitInfo] = fitclinear (meas(51:end,:), species(51:end)); %! assert_equal (FitInfo.Lambda, 0.01); %! assert_equal (FitInfo.Solver, {'bfgs'}); %! assert_equal (FitInfo.BetaTolerance, 1e-4); %! assert_equal (FitInfo.GradientTolerance, 1e-6); %! assert_equal (isfield (FitInfo, 'TerminationStatus'), true); %!test %! ## A dual fit reports the passes it took and the dual variables it left %! load fisheriris %! [~, FitInfo] = fitclinear (meas(51:end,:), species(51:end), ... %! 'Solver', 'dual', 'PassLimit', 20); %! assert_equal (isfield (FitInfo, 'Alpha'), true); %! assert_equal (isfield (FitInfo, 'NumPasses'), true); %! assert_equal (isfield (FitInfo, 'IterationLimit'), false); %! assert_equal (FitInfo.GradientTolerance, 0); %! assert_equal (isnan (FitInfo.GradientNorm), true); %!test %! ## A mini-batch fit reports the batch it stopped on and the rate it used %! load fisheriris %! [~, FitInfo] = fitclinear (meas(51:end,:), species(51:end), ... %! 'Solver', 'sgd', 'PassLimit', 5); %! assert_equal (isfield (FitInfo, 'BatchIndex'), true); %! assert_equal (isfield (FitInfo, 'OptimalLearnRate'), true); %!test %! ## A cross-validation option returns a partitioned model instead %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! CVMdl = fitclinear (X, Y, 'KFold', 5); %! assert_equal (class (CVMdl), 'ClassificationPartitionedLinear'); %! assert_equal (CVMdl.KFold, 5); %! assert_equal (numel (CVMdl.Trained), 5); %!test %! ## 'CrossVal' on gives the ten folds it defaults to, and 'off' the model %! ## itself %! load fisheriris %! X = meas(51:end,:); %! Y = species(51:end); %! CVMdl = fitclinear (X, Y, 'CrossVal', 'on'); %! assert_equal (CVMdl.KFold, 10); %! assert_equal (class (fitclinear (X, Y, 'CrossVal', 'off')), ... %! 'ClassificationLinear'); %!error ... %! [Mdl, FitInfo] = fitclinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], ... %! 'KFold', 3); ## Test input validation %!error fitclinear (ones (5, 2)) %!error ... %! fitclinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Learner') %!error ... %! fitclinear (ones (10, 2), [ones(5,1); 2*ones(5,1)], 'Learner', 'tree') %!test # MATLAB parity: classes given as text are sorted %! load fisheriris %! k = [101:150, 51:100]; %! Mdl = fitclinear (meas(k,:), species(k)); %! assert_equal (Mdl.ClassNames, {'versicolor'; 'virginica'}); %!test # MATLAB parity: a given ClassNames order is kept %! load fisheriris %! Mdl = fitclinear (meas(51:150,:), species(51:150), ... %! 'ClassNames', {'virginica'; 'versicolor'}); %! assert_equal (Mdl.ClassNames, {'virginica'; 'versicolor'}); %!error ... %! load fisheriris %! fitclinear (meas(51:150,:), species(51:150), 'ClassNames', [3, 2]) %!error ... %! load fisheriris %! fitclinear (meas(51:150,:), species(51:150), ... %! 'ClassNames', {'virginica'; 'rose'}) ## Table input %!shared fclT %! load fisheriris %! fclI = ! strcmp (species, 'setosa'); %! fclX = meas(fclI,:); %! fclT = table (fclX(:,1), fclX(:,2), fclX(:,3), fclX(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! fclT.Species = species(fclI); %!test # the response is named by a column and the rest are predictors %! Mdl = fitclinear (fclT, 'Species'); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW', 'PL', 'PW'}); %! assert_equal (Mdl.ResponseName, 'Species'); %!test # a model formula names the response and the predictors together %! Mdl = fitclinear (fclT, 'Species ~ PW + SL'); %! assert_equal (Mdl.PredictorNames, {'PW', 'SL'}); %!test # predict takes a table, matched by name and not by position %! Mdl = fitclinear (fclT, 'Species'); %! a = predict (Mdl, fclT); %! assert_equal (predict (Mdl, fclT(:, [5, 4, 3, 2, 1])), a); %!test # a cross-validated fit takes a table too %! Mdl = fitclinear (fclT, 'Species', 'KFold', 3); %! assert_equal (class (Mdl), 'ClassificationPartitionedLinear'); %! assert_equal (Mdl.KFold, 3); %!error ... %! fitclinear (fclT, 'NoSuch') %!error ... %! fitclinear (fclT, 'Species ~ SL*PW') statistics-release-2.1.2/inst/Machine_Learning/fitcnb.m000066400000000000000000000276241527311104300231170ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} fitcnb (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcnb (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcnb (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcnb (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcnb (@dots{}, @var{name}, @var{value}) ## ## Fit a naive Bayes classification model. ## ## @code{@var{Mdl} = fitcnb (@var{X}, @var{Y})} returns a naive Bayes ## classification model, @var{Mdl}, with @var{X} being the predictor data and ## @var{Y} the class labels of the observations in @var{X}. ## ## @itemize ## @item ## @var{X} must be a @math{N*P} numeric matrix of predictor data where rows ## correspond to observations and columns correspond to features or variables. ## @item ## @var{Y} is an @math{N*1} matrix or cell matrix containing the class labels ## of the corresponding predictor data in @var{X}. @var{Y} can be numeric, ## logical, a character array or a cell array of character vectors. @var{Y} ## must have the same number of rows as @var{X}. ## @end itemize ## ## A naive Bayes model fits one univariate density to each predictor within ## each class, and treats the predictors as conditionally independent given the ## class. An observation's likelihood under a class is therefore the product ## of its per-predictor densities, and its posterior follows by Bayes' rule ## from the class prior. ## ## @code{@var{Mdl} = fitcnb (@dots{}, @var{name}, @var{value})} returns a naive ## Bayes model with additional options specified by @qcode{Name-Value} pair ## arguments listed below. ## ## @subheading Model Parameters ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'PredictorNames'} @tab A cell array of character vectors ## specifying the names of the predictors. The length of this array must match ## the number of columns in @var{X}. ## ## @item @qcode{'ResponseName'} @tab A character vector specifying the name of ## the response variable. ## ## @item @qcode{'ClassNames'} @tab Names of the classes in the class labels, ## @var{Y}, used for fitting the model. @qcode{ClassNames} are of the same ## type as the class labels in @var{Y}. Naming a subset of the classes keeps ## only the observations belonging to them. The model keeps the classes in ## this order; by default they are sorted. ## ## @item @qcode{'Prior'} @tab A numeric vector specifying the prior probability ## of each class, in the order of @qcode{ClassNames}, or the character vector ## @qcode{'empirical'} (default) to take the class frequencies, or ## @qcode{'uniform'} to give every class the same probability. ## A class whose prior is 0 is removed from the data before fitting, with a ## warning, as in MATLAB. ## ## @item @qcode{'Cost'} @tab A square numeric matrix of misclassification ## costs, where @code{Cost(i,j)} is the cost of classifying an observation of ## class @math{i} into class @math{j}. The default is one off the diagonal and ## zero on it. ## ## @item @qcode{'ScoreTransform'} @tab A character vector naming a transform ## applied to the posterior returned by @code{predict}, or a function handle ## taking and returning a matrix of the same size. The default is ## @qcode{'none'}. ## ## @item @qcode{'DistributionNames'} @tab A character vector naming the ## distribution fitted to every predictor, or a cell array of character vectors ## naming one per predictor. Supported are @qcode{'normal'} (default), ## @qcode{'kernel'}, @qcode{'mvmn'} for a categorical predictor, and ## @qcode{'mn'} for token counts. @qcode{'mn'} describes the whole predictor ## vector at once and so cannot be named for only some predictors. ## ## @item @qcode{'Kernel'} @tab The smoothing kernel of the predictors fitted ## with a kernel density, one of @qcode{'normal'} (default), @qcode{'box'}, ## @qcode{'epanechnikov'} or @qcode{'triangle'}, given once for every predictor ## or once per predictor. ## ## @item @qcode{'Support'} @tab The support of the kernel densities, either ## @qcode{'unbounded'} (default), @qcode{'positive'}, or a two element numeric ## vector giving finite bounds. ## ## @item @qcode{'Width'} @tab The bandwidth of the kernel densities, given as a ## scalar, as one value per predictor, as one per class, or as a matrix of one ## per class and predictor. By default each density chooses its own. ## ## @item @qcode{'Weights'} @tab A nonnegative single or double vector of ## observation weights, one per row of @var{X}. An empirical prior sums them ## per class; a normal density takes weighted means and standard deviations, a ## kernel density weighs its observations but chooses its bandwidth from them ## alone, and the multinomials count each observation by its weight. A row of ## zero or missing weight is left out. The model's @code{W} keeps the class of ## the weights, while every computation runs in double, so @code{Prior} is ## double where MATLAB returns single. The default is uniform. ## ## @end multitable ## ## A predictor that takes one value throughout a class has no normal density ## to fit, and that combination of class and predictor is refused rather than ## answered. Only the combination is refused, not the model: giving that ## predictor a @qcode{'kernel'} or a @qcode{'mvmn'} distribution fits the same ## data, and leaves the other predictors normal. ## ## @seealso{ClassificationNaiveBayes} ## @end deftypefn function Mdl = fitcnb (X, Y, varargin) ## Check input parameters if (nargin < 2) error ("fitcnb: too few arguments."); endif if (mod (nargin, 2) != 0) error ("fitcnb: name-value arguments must be in pairs."); endif ## Check predictor data and labels have equal rows if (! istable (X) && rows (X) != rows (Y)) error ("fitcnb: number of rows in X and Y must be equal."); endif ## Parse arguments to class def function Mdl = ClassificationNaiveBayes (X, Y, varargin{:}); endfunction %!demo %! ## Fit a naive Bayes classifier to Fisher's iris data and see how often it %! ## classifies a training observation into its own species. %! %! load fisheriris %! Mdl = fitcnb (meas, species) %! printf ("resubstitution loss: %g\n", resubLoss (Mdl)); %!demo %! ## The petal measurements separate the species far better than the sepal %! ## ones, and a kernel density follows a skewed predictor where a normal %! ## one cannot. %! %! load fisheriris %! normalMdl = fitcnb (meas, species); %! kernelMdl = fitcnb (meas, species, 'DistributionNames', 'kernel'); %! printf ("normal : %g\n", resubLoss (normalMdl)); %! printf ("kernel : %g\n", resubLoss (kernelMdl)); ## Tests %!demo %! ## Fit from a table, and predict on one %! %! load fisheriris %! T = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species); %! %! ## A column holding levels is a categorical predictor without being named %! ## one %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! %! ## The response is named by its column, and everything else is a predictor %! Mdl = fitcnb (T, 'Species'); %! Mdl.PredictorNames %! Mdl.CategoricalPredictors %! %! ## A model formula names them instead, holding main effects only %! Mdl2 = fitcnb (T, 'Species ~ PL + PW'); %! Mdl2.PredictorNames %! %! ## predict reads a table by the names the model was fitted on, so the %! ## columns may come in any order %! label = predict (Mdl, T(1:5, [6, 5, 4, 3, 2, 1])); %! label' %!test # the driver returns what the constructor returns %! load fisheriris %! Mdl = fitcnb (meas, species); %! assert_equal (class (Mdl), 'ClassificationNaiveBayes'); %! assert_equal (Mdl.NumObservations, 150); %! assert_equal (Mdl.ClassNames, unique (species)); %!test # MATLAB parity: the fitted parameters and the resubstitution loss %! load fisheriris %! Mdl = fitcnb (meas, species); %! assert_equal (Mdl.DistributionParameters{1,1}, ... %! [5.005999999999998; 0.352489687213451], 1e-13); %! assert_equal (resubLoss (Mdl), 0.04, 1e-14); %!test # name-value arguments reach the constructor %! load fisheriris %! Mdl = fitcnb (meas, species, 'Prior', 'uniform', 'ResponseName', 'flower'); %! assert_equal (Mdl.Prior, [1/3, 1/3, 1/3], 1e-15); %! assert_equal (Mdl.ResponseName, 'flower'); ## Test input validation %!error fitcnb () %!error fitcnb (ones (4, 1)) %!error ... %! fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], ones (4, 1), 'Prior') %!error ... %! fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], ones (3, 1)) %!error ... %! fitcnb ([1, 2; 2, 3; 3, 4; 4, 5], ones (3, 1), 'Prior', 'uniform') %!error ... %! fitcnb ([1, 2; 2, 3; 3, 4; 10, 20], [1; 1; 1; 2]) %!error ... %! load fisheriris %! fitcnb (meas, species, 'ClassNames', [3, 1, 2]) %!test # classes named as text for numeric labels keep the labels' type %! load fisheriris %! Y = [ones(50, 1); 2 * ones(50, 1); 3 * ones(50, 1)]; %! Mdl = fitcnb (meas, Y, 'ClassNames', {'3', '1', '2'}); %! assert_equal (Mdl.ClassNames, [3; 1; 2]); ## Table input %!shared fnbT %! load fisheriris %! fnbT = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! fnbT.Species = categorical (species); %! fnbT.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %!test # the response is named by a column and the rest are predictors %! Mdl = fitcnb (fnbT, 'Species'); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW', 'PL', 'PW', 'Wide'}); %! assert_equal (Mdl.ResponseName, 'Species'); %! assert_equal (Mdl.CategoricalPredictors, 5); %!test # a model formula names the response and the predictors together %! Mdl = fitcnb (fnbT, 'Species ~ PW + SL'); %! assert_equal (Mdl.PredictorNames, {'PW', 'SL'}); %! assert_equal (isempty (Mdl.CategoricalPredictors), true); %!test # the response may be given beside a table of predictors %! Mdl = fitcnb (fnbT(:,1:4), fnbT.Species); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW', 'PL', 'PW'}); %! assert_equal (Mdl.ResponseName, 'Y'); %!test # predict takes a table, matched by name and not by position %! Mdl = fitcnb (fnbT, 'Species'); %! a = predict (Mdl, fnbT); %! assert_equal (class (a), 'categorical'); %! assert_equal (predict (Mdl, fnbT(:, [6, 5, 4, 3, 2, 1])), a); %!test # the levels travel with the model when it is made compact %! Mdl = fitcnb (fnbT, 'Species'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.PredictorLevels, Mdl.PredictorLevels); %! assert_equal (predict (CMdl, fnbT), predict (Mdl, fnbT)); %!error ... %! fitcnb (fnbT, 'NoSuch') %!error ... %! fitcnb (fnbT, 'Species ~ SL*PW') %!error ... %! predict (fitcnb (fnbT, 'Species'), fnbT(:, [1, 3, 4, 5, 6])) statistics-release-2.1.2/inst/Machine_Learning/fitcnet.m000066400000000000000000000412741527311104300233030ustar00rootroot00000000000000## Copyright (C) 2024 Pallav Purbia ## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} fitcnet (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcnet (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcnet (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcnet (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcnet (@dots{}, @var{name}, @var{value}) ## ## Fit a Neural Network classification model. ## ## @code{@var{Mdl} = fitcnet (@var{X}, @var{Y})} returns a Neural Network ## classification model, @var{Mdl}, with @var{X} being the predictor data, and ## @var{Y} the class labels of observations in @var{X}. ## ## @itemize ## @item ## @code{X} must be a @math{N*P} numeric matrix of predictor data where rows ## correspond to observations and columns correspond to features or variables. ## @item ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels of ## corresponding predictor data in @var{X}. @var{Y} can contain any type of ## categorical data. @var{Y} must have same numbers of rows as @var{X}. ## @end itemize ## ## @code{@var{Mdl} = fitcnet (@dots{}, @var{name}, @var{value})} returns a ## Neural Network classification model with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @subheading Model Parameters ## ## @multitable @columnfractions 0.32 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Standardize'} @tab A boolean flag indicating whether ## the data in @var{X} should be standardized prior to training. ## ## @item @qcode{'CategoricalPredictors'} @tab The predictors whose values are ## levels, as indices, as a logical vector with one element per predictor, or as ## @qcode{'all'}. Each is dummy coded in its place, one column of zeros and ## ones per level seen in training, named as in @qcode{'x1 == 2'} in ## @code{ExpandedPredictorNames}, and the coded columns are not standardized. ## An observation holding a level the training data did not has no score. ## A predictor may be named rather than indexed, as a character matrix of one ## padded name per row, a string array or a cellstr; a name must match an entry ## of @qcode{'PredictorNames'} exactly, its case included. ## ## @item @qcode{'PredictorNames'} @tab A cell array of character vectors ## specifying the predictor variable names. The variable names are assumed to ## be in the same order as they appear in the training data @var{X}. ## ## @item @qcode{'ResponseName'} @tab A character vector specifying the name ## of the response variable. ## ## @item @qcode{'ClassNames'} @tab Names of the classes in the class ## labels, @var{Y}, used for fitting the Neural Network model. ## @qcode{ClassNames} are of the same type as the class labels in @var{Y}. ## The model keeps the classes in this order; by default they are sorted. ## ## @item @qcode{'Prior'} @tab A numeric vector specifying the prior ## probabilities for each class. The order of the elements in @qcode{Prior} ## corresponds to the order of the classes in @qcode{ClassNames}. ## A class whose prior is 0 is removed from the data before fitting, with a ## warning, as in MATLAB. ## ## @item @qcode{'LayerSizes'} @tab A vector of positive integers that ## defines the sizes of the fully connected layers in the neural network model. ## Each element in LayerSizes corresponds to the number of outputs for the ## respective fully connected layer in the neural network model. ## The default value is 10. ## ## @item @qcode{'LearningRate'} @tab A positive scalar value that defines ## the learning rate during the gradient descent. Default value is 0.003. ## A larger rate can drive every unit of a hidden layer negative, after which ## a rectifier passes no gradient and the network stops training. ## Applies only when @qcode{'Solver'} is @qcode{'sgd'}. ## ## @item @qcode{'Solver'} @tab A character vector naming the solver that ## trains the network, either @qcode{'lbfgs'} or @qcode{'sgd'}. The ## default is @qcode{'lbfgs'}, which minimizes the loss over the whole ## training set at once by limited-memory BFGS, as MATLAB does. It takes ## no learning rate, stops on the three tolerances below, and reaches a ## lower training loss in fewer passes over the data, though each of its ## iterations costs several passes where an epoch costs one. ## @qcode{'sgd'} visits the samples one at a time and steps down the ## gradient of each, running for @qcode{'IterationLimit'} epochs; it was ## the default before version 1.9.0. ## ## @item @qcode{'GradientTolerance'} @tab A nonnegative scalar. Training ## stops once the gradient's infinity norm falls to or below it, which is ## the quantity MATLAB tests too. The default is @qcode{1e-6}. Applies ## only when @qcode{'Solver'} is @qcode{'lbfgs'}. ## ## @item @qcode{'StepTolerance'} @tab A nonnegative scalar. Training ## stops once the step's infinity norm falls to or below it, which is the ## quantity MATLAB tests too. The default is @qcode{1e-6}. Applies only ## when @qcode{'Solver'} is @qcode{'lbfgs'}. ## ## @item @qcode{'LossTolerance'} @tab A real scalar. Training stops once ## the training loss falls to or below it. The test is on the loss ## itself and not on its change, matching MATLAB; pass @code{-Inf} to ## switch it off. The default is @qcode{1e-6}. Applies only when ## @qcode{'Solver'} is @qcode{'lbfgs'}. ## ## @item @qcode{'Activations'} @tab A character vector or a cellstr vector ## specifying the activation functions for the hidden layers of the neural ## network (excluding the output layer). The available activation functions ## are @qcode{'linear'}, @qcode{'sigmoid'}, @qcode{'relu'}, @qcode{'tanh'}, ## @qcode{'softmax'}, @qcode{'lrelu'}, @qcode{'prelu'}, @qcode{'elu'}, ## @qcode{'gelu'}, and @qcode{'none'}. The default value is @qcode{'relu'}. ## ## @item @qcode{'OutputLayerActivation'} @tab A character vector specifying ## the activation function for the output layer of the neural network. The ## available activation functions are the same as for @qcode{'Activations'}. ## The default value is @qcode{'softmax'}, which makes the returned scores a ## probability over the classes and trains the network against cross entropy; ## any other value trains it against the mean squared error. ## ## @item @qcode{'IterationLimit'} @tab A positive integer scalar that ## specifies the maximum number of training iterations. The default value is ## 1000. ## Under @qcode{'sgd'} this counts epochs, under ## @qcode{'lbfgs'} solver iterations. ## ## @item @qcode{'DisplayInfo'} @tab A boolean flag indicating whether to ## print information during training. Default is @qcode{false}. ## ## @item @qcode{'Weights'} @tab A nonnegative single or double vector of ## observation weights, one per row of @var{X}. An empirical prior sums them ## per class, and each class carries its prior spread over its observations in ## proportion to them; the network is trained on the loss so weighted, which ## is how a prior other than the empirical one enters the fit as well. ## Standardization weighs the observations the same way, and a row of zero or ## missing weight is left out. The model's @code{W} keeps the class of the ## weights, while every computation runs in double, so @code{Prior} is double ## where MATLAB returns single. The default is uniform. ## ## @item @qcode{'ScoreTransform'} @tab A character vector defining one of ## the following functions or a user defined function handle, which is used ## for transforming the prediction scores returned by the @code{predict} and ## @code{resubPredict} methods. Default value is @qcode{'none'}. ## @end multitable ## ## @multitable @columnfractions 0.3 0.75 ## @headitem @var{Value} @tab @var{Description} ## @item @qcode{'doublelogit'} @tab @math{1 ./ (1 + exp (-2 * x))} ## @item @qcode{'invlogit'} @tab @math{log (x ./ (1 - x))} ## @item @qcode{'ismax'} @tab Sets the score for the class with the largest ## score to 1, and sets the scores for all other classes to 0 ## @item @qcode{'logit'} @tab @math{1 ./ (1 + exp (-x))} ## @item @qcode{'none'} @tab @math{x} (no transformation) ## @item @qcode{'identity'} @tab @math{x} (no transformation) ## @item @qcode{'sign'} @tab @math{-1 for x < 0, 0 for x = 0, 1 for x > 0} ## @item @qcode{'symmetric'} @tab @math{2 * x - 1} ## @item @qcode{'symmetricismax'} @tab Sets the score for the class with ## the largest score to 1, and sets the scores for all other classes to -1 ## @item @qcode{'symmetriclogit'} @tab @math{2 ./ (1 + exp (-x)) - 1} ## @end multitable ## ## ## The weights of each layer are drawn from a uniform range whose half-width ## is set by that layer's activation, and the scheme cannot be chosen: a ## rectifying activation (@qcode{'relu'}, @qcode{'lrelu'}, @qcode{'prelu'}, ## @qcode{'elu'}, @qcode{'gelu'}) takes the He range ## @math{sqrt (6 / fan_in)}, because it passes only half of its input, and ## the remaining activations take the Glorot range ## @math{sqrt (6 / (fan_in + fan_out))}, which accounts for the backward pass ## as well. A network whose layers do not share an activation is therefore ## built with both schemes. What each layer was given is reported by the ## @qcode{LayerWeightsInitializers} field of the fitted model's ## @qcode{ModelParameters}. ## ## @seealso{ClassificationNeuralNetwork} ## @end deftypefn function obj = fitcnet (X, Y, varargin) ## Check input parameters if (nargin < 2) error ("fitcnet: too few arguments."); endif if (mod (nargin, 2) != 0) error ("fitcnet: Name-Value arguments must be in pairs."); endif ## Check predictor data and labels have equal rows if (! istable (X) && rows (X) != rows (Y)) error ("fitcnet: number of rows in X and Y must be equal."); endif ## Parse arguments to classdef constructor obj = ClassificationNeuralNetwork (X, Y, varargin{:}); endfunction %!demo %! ## 1. Train a network on Fisher's iris data and see what it got right %! %! load fisheriris %! Mdl = fitcnet (meas, species); %! pred_species = resubPredict (Mdl); %! confusionchart (species, pred_species, 'Title', ... %! 'Neural network classification of Fisher''s iris data'); %!demo %! ## 2. Watching the fit converge %! %! load fisheriris %! Mdl = fitcnet (meas, species, 'IterationLimit', 400); %! %! ## TrainingHistory records what the solver converges on. The default %! ## solver is lbfgs, so that is the loss and the gradient norm; under %! ## 'sgd' it is the loss and the accuracy instead. %! h = Mdl.TrainingHistory; %! plotyy (h.Iteration, h.TrainingLoss, h.Iteration, h.Gradient); %! xlabel ('Iteration'); %! title ('Training loss, left, and gradient norm, right'); %!demo %! ## 3. Rectified hidden layers train faster than sigmoid ones %! %! load fisheriris %! iters = [5, 10, 25, 50, 100, 200, 400]; %! L = zeros (2, numel (iters)); %! for k = 1:numel (iters) %! for a = 1:2 %! act = {'relu', 'sigmoid'}{a}; %! m = fitcnet (meas, species, 'Activations', act, ... %! 'IterationLimit', iters(k)); %! L(a,k) = loss (m, meas, species, 'LossFun', 'classiferror'); %! endfor %! endfor %! semilogx (iters, L(1,:), 'o-', iters, L(2,:), 's-', 'linewidth', 1.5); %! xlabel ('Iteration limit'); %! ylabel ('Misclassification rate'); %! legend ({'relu', 'sigmoid'}); %! title ('A sigmoid shrinks the gradient at every layer'); %!demo %! ## 4. What the network learned, over two predictors %! %! load fisheriris %! X = meas(:,3:4); %! Mdl = fitcnet (X, species, 'LayerSizes', [12, 12], 'IterationLimit', 400); %! %! ## Ask about a grid and paint each point by the answer %! [gx, gy] = meshgrid (linspace (0.5, 7.5, 120), linspace (0, 3, 120)); %! [~, ~, region] = unique (predict (Mdl, [gx(:), gy(:)])); %! contourf (gx, gy, reshape (region, size (gx)), [1 2 3]); %! colormap (summer); %! hold on; %! gscatter (X(:,1), X(:,2), species, 'krb', 'ox+'); %! hold off; %! xlabel ('Petal length'); %! ylabel ('Petal width'); %! title ('Decision regions of a two-layer network'); ## Test constructor %!demo %! ## Fit from a table, and predict on one %! %! load fisheriris %! T = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species); %! %! ## A column holding levels is a categorical predictor without being named %! ## one %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! %! ## The response is named by its column, and everything else is a predictor %! Mdl = fitcnet (T, 'Species'); %! Mdl.PredictorNames %! Mdl.CategoricalPredictors %! %! ## predict reads a table by the names the model was fitted on, so the %! ## columns may come in any order %! label = predict (Mdl, T(1:5, [6, 5, 4, 3, 2, 1])); %! label' %!test %! load fisheriris %! x = meas; %! y = grp2idx (species); %! Mdl = fitcnet (x, y, 'IterationLimit', 50); %! assert_equal (class (Mdl), "ClassificationNeuralNetwork"); %! assert_equal (numel (Mdl.LayerWeights), 2); %! assert_equal (size (Mdl.LayerWeights{1}), [10, 4]); %! assert_equal (size (Mdl.LayerBiases{1}), [10, 1]); %! assert_equal (size (Mdl.LayerWeights{2}), [3, 10]); %! assert_equal (size (Mdl.LayerBiases{2}), [3, 1]); ## Test input validation %!error fitcnet () %!error fitcnet (ones (4,1)) %!error %! fitcnet (ones (4,2), ones (4, 1), 'LayerSizes') %!error %! fitcnet (ones (4,2), ones (3, 1)) %!error %! fitcnet (ones (4,2), ones (3, 1), 'LayerSizes', 2) %!test # MATLAB parity: classes given as text are sorted %! load fisheriris %! k = [101:150, 1:50, 51:100]; %! Mdl = fitcnet (meas(k,:), species(k)); %! assert_equal (Mdl.ClassNames, {'setosa'; 'versicolor'; 'virginica'}); %!test # MATLAB parity: a given ClassNames order is kept %! load fisheriris %! Mdl = fitcnet (meas(1:150,:), species(1:150), ... %! 'ClassNames', {'virginica'; 'setosa'; 'versicolor'}); %! assert_equal (Mdl.ClassNames, {'virginica'; 'setosa'; 'versicolor'}); %!error ... %! load fisheriris %! fitcnet (meas, species, 'ClassNames', [3, 1, 2]) ## Table input %!shared fcnT %! load fisheriris %! fcnT = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! fcnT.Species = categorical (species); %! fcnT.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %!test # the response is named by a column and the rest are predictors %! Mdl = fitcnet (fcnT, 'Species'); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW', 'PL', 'PW', 'Wide'}); %! assert_equal (Mdl.ResponseName, 'Species'); %! assert_equal (Mdl.CategoricalPredictors, 5); %!test # a model formula names the response and the predictors together %! Mdl = fitcnet (fcnT, 'Species ~ PW + SL'); %! assert_equal (Mdl.PredictorNames, {'PW', 'SL'}); %! assert_equal (isempty (Mdl.CategoricalPredictors), true); %!test # the response may be given beside a table of predictors %! Mdl = fitcnet (fcnT(:,1:4), fcnT.Species); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW', 'PL', 'PW'}); %! assert_equal (Mdl.ResponseName, 'Y'); %!test # predict takes a table, matched by name and not by position %! Mdl = fitcnet (fcnT, 'Species'); %! a = predict (Mdl, fcnT); %! assert_equal (class (a), 'categorical'); %! assert_equal (predict (Mdl, fcnT(:, [6, 5, 4, 3, 2, 1])), a); %!test # the levels travel with the model when it is made compact %! Mdl = fitcnet (fcnT, 'Species'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.PredictorLevels, Mdl.PredictorLevels); %! assert_equal (predict (CMdl, fcnT), predict (Mdl, fcnT)); %!error ... %! fitcnet (fcnT, 'NoSuch') %!error ... %! fitcnet (fcnT, 'Species ~ SL*PW') %!error ... %! predict (fitcnet (fcnT, 'Species'), fcnT(:, [1, 3, 4, 5, 6])) statistics-release-2.1.2/inst/Machine_Learning/fitcsvm.m000066400000000000000000000546471527311104300233320ustar00rootroot00000000000000## Copyright (C) 2024 Pallav Purbia ## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} fitcsvm (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcsvm (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcsvm (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcsvm (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitcsvm (@dots{}, @var{name}, @var{value}) ## ## Fit a Support Vector Machine classification model. ## ## @code{@var{Mdl} = fitcsvm (@var{X}, @var{Y})} returns a Support Vector ## Machine classification model, @var{Mdl}, with @var{X} being the predictor ## data, and @var{Y} the class labels of observations in @var{X}. ## ## @itemize ## @item ## @code{X} must be a @math{N*P} numeric matrix of predictor data where rows ## correspond to observations and columns correspond to features or variables. ## @item ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels of ## corresponding predictor data in @var{X}. @var{Y} can be numerical, logical, ## char array or cell array of character vectors. @var{Y} must have same number ## of rows as @var{X}. ## @end itemize ## ## @code{@var{Mdl} = fitcsvm (@dots{}, @var{name}, @var{value})} returns a ## Support Vector Machine model with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @subheading Model Parameters ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Standardize'} @tab A boolean flag indicating whether ## the data in @var{X} should be standardized prior to training. ## ## @item @qcode{'CategoricalPredictors'} @tab The predictors whose values are ## levels, as indices, as a logical vector with one element per predictor, or as ## @qcode{'all'}. Each is dummy coded in its place, one column of zeros and ## ones per level seen in training, named as in @qcode{'x1 == 2'} in ## @code{ExpandedPredictorNames}, and the coded columns are not standardized. ## An observation holding a level the training data did not has no score. ## A predictor may be named rather than indexed, as a character matrix of one ## padded name per row, a string array or a cellstr; a name must match an entry ## of @qcode{'PredictorNames'} exactly, its case included. ## ## @item @qcode{'PredictorNames'} @tab A cell array of character vectors ## specifying the predictor variable names. The variable names are assumed to ## be in the same order as they appear in the training data @var{X}. ## ## @item @qcode{'ResponseName'} @tab A character vector specifying the name ## of the response variable. ## ## @item @qcode{'ClassNames'} @tab Names of the classes in the class ## labels, @var{Y}, used for fitting the kNN model. @qcode{ClassNames} are of ## the same type as the class labels in @var{Y}. The model keeps the classes ## in this order; by default they are sorted. ## ## @item @qcode{'SVMtype'} @tab Specifies the type of SVM used for training ## the @code{ClassificationSVM} model. By default, the type of SVM is defined ## by setting other parameters and/or by the data itself. Setting the ## @qcode{'SVMtype'} parameter overrides the default behavior and it accepts the ## following options: ## @end multitable ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Value} @tab @var{Description} ## @item @qcode{'C_SVC'} @tab It is the standard SVM formulation for ## classification tasks. It aims to find the optimal hyperplane that separates ## different classes by maximizing the margin between them while allowing some ## misclassifications. The parameter @qcode{'C'} controls the trade-off between ## maximizing the margin and minimizing the classification error. It is the ## default type, unless otherwise specified. ## @item @qcode{'nu_SVC'} @tab It is a variation of the standard SVM that ## introduces a parameter @math{ν} (nu) as an upper bound on the fraction of ## margin errors and a lower bound on the fraction of support vectors. This ## formulation provides more control over the number of support vectors and the ## margin errors, making it useful for specific classification scenarios. It is ## the default type, when the @qcode{'OutlierFraction'} parameter is set. ## @item @qcode{'one_class_SVM'} @tab It is used for anomaly detection and ## novelty detection tasks. It aims to separate the data points of a single ## class from the origin in a high-dimensional feature space. This method is ## particularly useful for identifying outliers or unusual patterns in the data. ## It is the default type, when the @qcode{'Nu'} parameter is set or when there ## is a single class in @var{Y}. When @qcode{'one_class_SVM'} is set by the ## @qcode{'SVMtype'} pair argument, @var{Y} has no effect and any classes are ## ignored. ## @end multitable ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'OutlierFraction'} @tab The expected proportion of outliers ## in the training data, specified as a scalar value in the range @math{[0,1]}. ## When specified, the type of SVM model is switched to @qcode{'nu_SVC'} and ## @qcode{'OutlierFraction'} defines the @math{ν} (nu) parameter. ## ## @item @qcode{'KernelFunction'} @tab A character vector specifying the ## method for computing elements of the Gram matrix. The available kernel ## functions are @qcode{'gaussian'} or @qcode{'rbf'}, @qcode{'linear'}, ## @qcode{'polynomial'}, and @qcode{'sigmoid'}. For one-class learning, the ## default Kernel function is @qcode{'rbf'}. For two-class learning the default ## is @qcode{'linear'}. ## ## @item @qcode{'PolynomialOrder'} @tab A positive integer that specifies ## the order of polynomial in kernel function. The default value is 3. Unless ## the @qcode{'KernelFunction'} is set to @qcode{'polynomial'}, this parameter ## is ignored. ## ## @item @qcode{'KernelScale'} @tab A positive scalar dividing every predictor ## before any kernel is applied, as MATLAB does, so that with @math{u} and ## @math{v} the divided predictors the kernels are @math{u'v}, ## @math{exp (-||u - v||^2)}, @math{(1 + u'v)^q} and @math{tanh (u'v + c)}, ## @math{c} being @qcode{'KernelOffset'}. The default is 1. ## ## @item @qcode{'KernelOffset'} @tab A non-negative scalar, the constant ## @math{c} of the sigmoid kernel, which MATLAB does not have. MATLAB adds ## it to every element of the Gram matrix, which leaves the fitted model ## unchanged, so it changes no other kernel here. The default is 0. ## ## @item @qcode{'Weights'} @tab A single or double vector of nonnegative ## observation weights, one per row of @var{X}. Each observation's box ## constraint is @math{n} times @qcode{BoxConstraint} times its weight, the ## weights scaled so that each class carries its prior times the cost of ## misclassifying it. An empirical prior sums the weights per class, ## standardization uses weighted means and standard deviations, and a row of ## zero weight is left out. The model's @code{W} keeps the class of the ## weights, while every computation runs in double, so @code{Prior} is double ## where MATLAB returns single. ## ## @item @qcode{'Prior'} @tab @qcode{'empirical'} (default), @qcode{'uniform'}, ## a vector with one element per class, or a structure with fields ## @qcode{ClassNames} and @qcode{ClassProbs}. A class whose prior is 0, or ## whose misclassification costs are all 0, is removed from the data before ## fitting, with a warning, as in MATLAB. ## ## @item @qcode{'Cost'} @tab A square matrix of misclassification costs, ## @code{Cost(i,j)} being the cost of classifying into class @math{j} an ## observation of class @math{i}, @code{1 - eye (2)} by default; or a structure ## with fields @qcode{ClassNames} and @qcode{ClassificationCosts}, which names ## its own order of the classes and is permuted to the model's. ## ## @item @qcode{'BoxConstraint'} @tab A positive scalar that specifies the ## upper bound of the Lagrange multipliers, i.e. the parameter C, which is used ## for training @qcode{'C_SVC'} and @qcode{'one_class_SVM'} type of models. It ## determines the trade-off between maximizing the margin and minimizing the ## classification error. The default value for @qcode{'BoxConstraint'} is 1. ## ## @item @qcode{'Nu'} @tab A positive scalar, in the range @math{(0,1]} ## that specifies the parameter @math{ν} (nu) for training @qcode{'nu_SVC'} and ## @qcode{'one_class_SVM'} type of models. Unless overridden by setting the ## @qcode{'SVMtype'} parameter, setting the @qcode{'Nu'} parameter always forces ## the training model type to @qcode{'one_class_SVM'}, in which case, the number ## of classes in @var{Y} is ignored. The default value for @qcode{'Nu'} is 1. ## ## @item @qcode{'CacheSize'} @tab A positive scalar that specifies the ## memory requirements (in MB) for storing the Gram matrix. The default is 1000. ## ## @item @qcode{'Tolerance'} @tab A nonnegative scalar that specifies ## the tolerance of termination criterion. The default value is 1e-6. ## ## @item @qcode{'Shrinking'} @tab Specifies whether to use shrinking ## heuristics. It accepts either 0 or 1. The default value is 1. ## @end multitable ## ## @subheading Cross Validation Options ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Crossval'} @tab Cross-validation flag specified as ## @qcode{'on'} or @qcode{'off'}. If @qcode{'on'} is specified, a 10-fold ## cross validation is performed and a @code{ClassificationPartitionedModel} is ## returned in @var{Mdl}. To override this cross-validation setting, use only ## one of the following Name-Value pair arguments. ## ## @item @qcode{'CVPartition'} @tab A @code{cvpartition} object that ## specifies the type of cross-validation and the indexing for the training and ## validation sets. A @code{ClassificationPartitionedModel} is returned in ## @var{Mdl} and the trained model is stored in the @code{Trained} property. ## ## @item @qcode{'Holdout'} @tab Fraction of the data used for holdout ## validation, specified as a scalar value in the range @math{[0,1]}. When ## specified, a randomly selected percentage is reserved as validation data and ## the remaining set is used for training. The trained model is stored in the ## @code{Trained} property of the @code{ClassificationPartitionedModel} returned ## in @var{Mdl}. @qcode{'Holdout'} partitioning attempts to ensure that each ## partition represents the classes proportionately. ## ## @item @qcode{'KFold'} @tab Number of folds to use in the cross-validated ## model, specified as a positive integer value greater than 1. When specified, ## then the data is randomly partitioned in @math{k} sets and for each set, the ## set is reserved as validation data while the remaining @math{k-1} sets are ## used for training. The trained models are stored in the @code{Trained} ## property of the @code{ClassificationPartitionedModel} returned in @var{Mdl}. ## @qcode{'KFold'} partitioning attempts to ensure that each partition ## represents the classes proportionately. ## ## @item @qcode{'Leaveout'} @tab Leave-one-out cross-validation flag ## specified as @qcode{'on'} or @qcode{'off'}. If @qcode{'on'} is specified, ## then for each of the @math{n} observations (where @math{n} is the number of ## observations, excluding missing observations, specified in the ## @code{NumObservations} property of the model), one observation is reserved as ## validation data while the remaining observations are used for training. The ## trained models are stored in the @code{Trained} property of the ## @code{ClassificationPartitionedModel} returned in @var{Mdl}. ## @end multitable ## ## @seealso{ClassificationSVM, ClassificationPartitionedModel, svmtrain, ## svmpredict} ## @end deftypefn function Mdl = fitcsvm (X, Y, varargin) ## Check input parameters if (nargin < 2) error ("fitcsvm: too few arguments."); endif if (mod (nargin, 2) != 0) error ("fitcsvm: Name-Value arguments must be in pairs."); endif ## Check predictor data and labels have equal rows if (! istable (X) && rows (X) != rows (Y)) error ("fitcsvm: number of rows in X and Y must be equal."); endif ## Check optional input parameters for cross-validation options cv_opt = false; cv_arg = 0; args = {}; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'crossval' CrossVal = varargin{2}; if (! any (strcmp (CrossVal, {'off', 'on'}))) error ("fitcsvm: 'CrossVal' must be either 'off' or 'on'."); endif if (strcmp (CrossVal, 'on')) cv_opt = true; endif case 'kfold' Name = 'KFold'; Value = varargin{2}; cv_arg += 1; cv_opt = true; case 'holdout' Name = 'Holdout'; Value = varargin{2}; cv_arg += 1; cv_opt = true; case 'leaveout' Name = 'Leaveout'; Value = varargin{2}; cv_arg += 1; cv_opt = true; case 'cvpartition' Name = 'CVPartition'; Value = varargin{2}; cv_arg += 1; cv_opt = true; otherwise args = [args, {varargin{1}, varargin{2}}]; endswitch varargin(1:2) = []; endwhile ## Check for multiple cross-validation paired arguments if (cv_arg > 1) error (strcat ("fitcsvm: You can use only one cross-validation", ... " name-value pair argument at a time to create a", ... " cross-validated model.")); endif ## Parse arguments to classdef constructor Mdl = ClassificationSVM (X, Y, args{:}); ## If cross validation has been requested, ## return a ClassificationPartitionedModel if (cv_opt) if (cv_arg) Mdl = crossval (Mdl, Name, Value); else Mdl = crossval (Mdl); endif endif endfunction %!demo %! ## Use a subset of Fisher's iris data set %! %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! X = meas(inds, [3,4]); %! Y = species(inds); %! %! ## Train a linear SVM classifier %! SVMModel = fitcsvm (X, Y) %! %! ## Plot a scatter diagram of the data and circle the support vectors. %! sv = SVMModel.SupportVectors; %! figure %! gscatter (X(:,1), X(:,2), Y) %! hold on %! plot (sv(:,1), sv(:,2), 'ko', 'MarkerSize', 10) %! legend ('versicolor', 'virginica', 'Support Vector') %! hold off ## Test constructor %!demo %! ## Fit from a table, and predict on one %! %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! X = meas(inds,:); %! T = table (X(:,1), X(:,2), X(:,3), X(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species(inds)); %! %! ## A column holding levels is a categorical predictor without being named %! ## one %! T.Wide = categorical (X(:,2) > 2.9, [false true], {'narrow', 'wide'}); %! %! ## The response is named by its column, and everything else is a predictor %! Mdl = fitcsvm (T, 'Species'); %! Mdl.PredictorNames %! Mdl.CategoricalPredictors %! %! ## predict reads a table by the names the model was fitted on, so the %! ## columns may come in any order %! label = predict (Mdl, T(1:5, [6, 5, 4, 3, 2, 1])); %! label' %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = {'a'; 'a'; 'b'; 'b'}; %! a = fitcsvm (x, y); %! assert_equal (class (a), "ClassificationSVM"); %! assert_equal ({a.X, a.Y}, {x, y}) %! assert_equal (a.NumObservations, 4) %! assert_equal ({a.ResponseName, a.PredictorNames}, {'Y', {'x1', 'x2', 'x3'}}) %! assert_equal (a.ModelParameters.SVMtype, "c_svc") %! assert_equal (a.ClassNames, {'a'; 'b'}) ## Test Output %!test %! x = [1, 2; 2, 3; 3, 4; 4, 5; 2, 3; 3, 4; 2, 3; 3, 4; 2, 3; 3, 4]; %! y = [1; 1; -1; -1; 1; -1; -1; -1; -1; -1]; %! a = fitcsvm (x, y); %! assert_equal (class (a), "ClassificationSVM"); %! assert_equal ({a.X, a.Y, a.ModelParameters.KernelFunction}, {x, y, 'linear'}) %! assert_equal (a.ModelParameters.BoxConstraint, 1) %! assert_equal (a.ModelParameters.KernelOffset, 0) %! assert_equal (a.ClassNames, [-1; 1]) %!test %! x = [1, 2; 2, 3; 3, 4; 4, 5; 2, 3; 3, 4; 2, 3; 3, 4; 2, 3; 3, 4]; %! y = [1; 1; -1; -1; 1; -1; -1; -1; -1; -1]; %! a = fitcsvm (x, y, 'KernelFunction', 'rbf', 'BoxConstraint', 2, ... %! 'KernelOffset', 2); %! assert_equal (class (a), "ClassificationSVM"); %! assert_equal ({a.X, a.Y, a.ModelParameters.KernelFunction}, {x, y, 'rbf'}) %! assert_equal (a.ModelParameters.BoxConstraint, 2) %! assert_equal (a.ModelParameters.KernelOffset, 2) %! assert_equal (isempty (a.Alpha), false) %! assert_equal (isempty (a.Beta), true) %!test %! x = [1, 2; 2, 3; 3, 4; 4, 5; 2, 3; 3, 4; 2, 3; 3, 4; 2, 3; 3, 4]; %! y = [1; 1; -1; -1; 1; -1; -1; -1; -1; -1]; %! a = fitcsvm (x, y, 'KernelFunction', 'polynomial', 'PolynomialOrder', 3); %! assert_equal (class (a), "ClassificationSVM"); %! assert_equal ({a.X, a.Y, a.ModelParameters.KernelFunction}, {x, y, 'polynomial'}) %! assert_equal (a.ModelParameters.KernelPolynomialOrder, 3) %! assert_equal (isempty (a.Alpha), false) %! assert_equal (isempty (a.Beta), true) %!test %! x = [1, 2; 2, 3; 3, 4; 4, 5; 2, 3; 3, 4; 2, 3; 3, 4; 2, 3; 3, 4]; %! y = [1; 1; -1; -1; 1; -1; -1; -1; -1; -1]; %! a = fitcsvm (x, y, 'KernelFunction', 'linear', 'PolynomialOrder', 3); %! assert_equal (class (a), "ClassificationSVM"); %! assert_equal ({a.X, a.Y, a.ModelParameters.KernelFunction}, {x, y, 'linear'}) %! assert_equal (isempty (a.ModelParameters.KernelPolynomialOrder), true) %! assert_equal (isempty (a.Alpha), false) %! assert_equal (isempty (a.Beta), false) %! assert_equal (size (a.Beta), [2, 1]) %!test %! x = [1, 2; 2, 3; 3, 4; 4, 5; 2, 3; 3, 4; 2, 3; 3, 4; 2, 3; 3, 4]; %! y = [1; 1; -1; -1; 1; -1; -1; -1; -1; -1]; %! status = warning; %! warning ('off'); %! rand ('seed', 23); %! a = fitcsvm (x, y, 'KernelFunction', 'linear', 'CrossVal', 'on'); %! warning (status); %! assert_equal (class (a), "ClassificationPartitionedModel"); %! assert_equal ({a.X, a.Y, a.ModelParameters.KernelFunction}, {x, y, 'linear'}) %! assert_equal (isempty (a.ModelParameters.KernelPolynomialOrder), true) %! assert_equal (isempty (a.Trained{1}.Alpha), false) %! assert_equal (isempty (a.Trained{1}.Beta), false) ## Test input validation %!error fitcsvm () %!error fitcsvm (ones (4,1)) %!error %! fitcsvm (ones (4,2), ones (4, 1), 'KFold') %!error %! fitcsvm (ones (4,2), ones (3, 1)) %!error %! fitcsvm (ones (4,2), ones (3, 1), 'KFold', 2) %!error %! fitcsvm (ones (4,2), ones (4, 1), 'CrossVal', 2) %!error %! fitcsvm (ones (4,2), ones (4, 1), 'CrossVal', 'a') %!error ... %! fitcsvm (ones (4,2), ones (4, 1), 'KFold', 10, 'Holdout', 0.3) %!test # 'Leaveout' leaves one observation out of each fold %! load fisheriris %! k = [51:70, 101:120]; %! CVMdl = fitcsvm (meas(k,:), species(k), 'Leaveout', 'on'); %! assert_equal (CVMdl.KFold, 40); %!test # MATLAB parity: classes given as text are sorted %! load fisheriris %! k = [101:150, 51:100]; %! Mdl = fitcsvm (meas(k,:), species(k)); %! assert_equal (Mdl.ClassNames, {'versicolor'; 'virginica'}); %!test # MATLAB parity: a given ClassNames order is kept %! load fisheriris %! Mdl = fitcsvm (meas(51:150,:), species(51:150), ... %! 'ClassNames', {'virginica'; 'versicolor'}); %! assert_equal (Mdl.ClassNames, {'virginica'; 'versicolor'}); %!test # the score columns follow a given ClassNames order %! load fisheriris %! Mdl = fitcsvm (meas(51:150,:), species(51:150), ... %! 'ClassNames', {'virginica'; 'versicolor'}); %! [label, s] = predict (Mdl, meas(51,:)); %! assert_equal (label, {'versicolor'}); %! assert_equal (s(2) > s(1), true); %!error ... %! load fisheriris %! fitcsvm (meas(51:150,:), species(51:150), 'ClassNames', [3, 2]) ## Table input %!shared fcsT %! load fisheriris %! fcsI = ! strcmp (species, 'setosa'); %! fcsX = meas(fcsI,:); %! fcsT = table (fcsX(:,1), fcsX(:,2), fcsX(:,3), fcsX(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! fcsT.Species = categorical (species(fcsI)); %! fcsT.Wide = categorical (fcsX(:,2) > 2.9, [false true], ... %! {'narrow', 'wide'}); %!test # the response is named by a column and the rest are predictors %! Mdl = fitcsvm (fcsT, 'Species'); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW', 'PL', 'PW', 'Wide'}); %! assert_equal (Mdl.ResponseName, 'Species'); %! assert_equal (Mdl.CategoricalPredictors, 5); %!test # a model formula names the response and the predictors together %! Mdl = fitcsvm (fcsT, 'Species ~ PW + SL'); %! assert_equal (Mdl.PredictorNames, {'PW', 'SL'}); %! assert_equal (isempty (Mdl.CategoricalPredictors), true); %!test # the response may be given beside a table of predictors %! Mdl = fitcsvm (fcsT(:,1:4), fcsT.Species); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW', 'PL', 'PW'}); %! assert_equal (Mdl.ResponseName, 'Y'); %!test # predict takes a table, matched by name and not by position %! Mdl = fitcsvm (fcsT, 'Species'); %! a = predict (Mdl, fcsT); %! assert_equal (class (a), 'categorical'); %! assert_equal (predict (Mdl, fcsT(:, [6, 5, 4, 3, 2, 1])), a); %!test # the levels travel with the model when it is made compact %! Mdl = fitcsvm (fcsT, 'Species'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.PredictorLevels, Mdl.PredictorLevels); %! assert_equal (predict (CMdl, fcsT), predict (Mdl, fcsT)); %!error ... %! fitcsvm (fcsT, 'NoSuch') %!error ... %! fitcsvm (fcsT, 'Species ~ SL*PW') %!error ... %! predict (fitcsvm (fcsT, 'Species'), fcsT(:, [1, 3, 4, 5, 6])) statistics-release-2.1.2/inst/Machine_Learning/fitctree.m000066400000000000000000000643731527311104300234610ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} fitctree (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitctree (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{Mdl} =} fitctree (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{Mdl} =} fitctree (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitctree (@dots{}, @var{name}, @var{value}) ## ## Fit a binary decision tree for classification. ## ## @code{@var{Mdl} = fitctree (@var{X}, @var{Y})} grows a binary decision tree ## on the predictor data @var{X} and the class labels @var{Y}, and returns it ## as a @code{ClassificationTree} object. ## ## @itemize ## @item ## @var{X} must be a @math{NxP} numeric matrix of predictor data, where rows ## correspond to observations and columns to predictors. ## @item ## @var{Y} must be a @math{Nx1} numeric or logical vector, a character array ## with one class name per row, or a cell array of character vectors, holding ## the class label of each observation in @var{X}. The class names come back ## in the type @var{Y} was given in. ## @end itemize ## ## @code{@var{Mdl} = fitctree (@var{Tbl}, @var{ResponseVarName})} takes both ## from the table @var{Tbl}: @var{ResponseVarName} names the variable holding ## the class labels and every other variable is a predictor. ## @code{@var{Mdl} = fitctree (@var{Tbl}, @var{formula})} names them with a ## model formula instead, @qcode{'Y ~ x1 + x2'}, which holds main effects ## only and so takes no product, power or wildcard. ## @code{@var{Mdl} = fitctree (@var{Tbl}, @var{Y})} takes the predictors from ## the table and the class labels from @var{Y}. ## ## A table variable holding levels rather than numbers, which is a ## @code{categorical}, a @code{logical}, a character array or a cell array of ## character vectors, is a categorical predictor without being named one, and ## @qcode{'CategoricalPredictors'} adds to that set rather than replacing it. ## @code{PredictorNames} and @code{ResponseName} come from the table, and a ## level is coded at prediction as it was coded at fitting, so a table given ## to @code{predict} may hold only some of the levels. ## ## An observation whose class label is missing, which is missing every ## predictor, or whose weight is zero is dropped, and the rows kept are ## reported in @code{RowsUsed}. An observation missing only some of its ## predictors is kept: it descends the tree as far as the predictors it does ## carry allow and is answered there. ## ## @code{@var{Mdl} = fitctree (@dots{}, @var{name}, @var{value})} takes the ## options below. ## ## @multitable @columnfractions 0.20 0.78 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'AlgorithmForCategorical'} @tab How a node with three or more ## classes splits a categorical predictor: @qcode{'exact'}, ## @qcode{'pullleft'}, @qcode{'pca'} or @qcode{'ovabyclass'}. By default the ## exact search is taken when the node holds at most @qcode{'MaxNumCategories'} ## levels, and otherwise the best split of @qcode{'ovabyclass'}, ## @qcode{'pca'} and @qcode{'pullleft'}, leaving @qcode{'ovabyclass'} out above ## four classes. ## ## @item @qcode{'CategoricalPredictors'} @tab The predictors whose values are ## levels, as indices, as a logical vector with one element per predictor, or ## as @qcode{'all'}. Such a predictor is split into two sets of levels, and an ## observation whose level a node did not see stops there. ## A predictor may be named rather than indexed, as a character matrix of one ## padded name per row, a string array or a cellstr; a name must match an entry ## of @qcode{'PredictorNames'} exactly, its case included. ## ## @item @qcode{'ClassNames'} @tab The classes to fit, of the same type as ## @var{Y}. Observations of any other class are dropped. The model keeps ## the classes in this order; by default they are sorted. ## ## @item @qcode{'Cost'} @tab A square matrix with one row and column per ## class, where element @math{(i,j)} is the cost of classifying an ## observation of class @math{i} into class @math{j}, or a structure with ## fields @qcode{ClassNames} and @qcode{ClassificationCosts}. The default is ## @code{1 - eye (K)}. A non-default cost changes the shape of the tree, not ## only what it predicts. ## ## @item @qcode{'MaxNumCategories'} @tab A nonnegative integer, the most ## levels a node with three or more classes searches exactly by default. The ## default is 10. ## ## @item @qcode{'MaxNumSplits'} @tab A nonnegative integer, the largest number ## of branch nodes the tree may take. The default is one less than the number ## of observations. ## ## @item @qcode{'MergeLeaves'} @tab @qcode{'on'} (default) or @qcode{'off'}. ## When on, a pair of leaves whose parent is no worse than the two of them ## together is merged back into that parent. ## ## @item @qcode{'MinLeafSize'} @tab A positive integer, the fewest ## observations a leaf may hold. The default is 1. ## ## @item @qcode{'MinParentSize'} @tab A positive integer, the fewest ## observations a node must hold to be split. The default is 10. The value ## the fit uses is @code{max (MinParentSize, 2 * MinLeafSize)}. ## ## @item @qcode{'NumVariablesToSample'} @tab A positive integer, the number ## of predictors each split is chosen from, drawn afresh at every node, or ## @qcode{'all'} (default). A number no smaller than the number of ## predictors samples them all and is reported as @qcode{'all'}. MATLAB also ## accepts a fractional number, which is refused here. ## ## @item @qcode{'PredictorNames'} @tab A cell array of character vectors ## naming the columns of @var{X}. ## ## @item @qcode{'Prior'} @tab @qcode{'empirical'} (default), ## @qcode{'uniform'}, a numeric vector with one element per class, or a ## structure with fields @qcode{ClassNames} and @qcode{ClassProbs}. ## A class whose prior is 0, or whose misclassification costs are all 0, is ## removed from the data before fitting, with a warning, as in MATLAB. ## ## @item @qcode{'Prune'} @tab @qcode{'on'} (default) or @qcode{'off'}. When ## on, the cost complexity pruning sequence is estimated and reported in ## @code{PruneList} and @code{PruneAlpha}. The tree returned is the unpruned ## one either way; @code{prune} takes a subtree out of the sequence. ## ## @item @qcode{'PruneCriterion'} @tab @qcode{'error'}, the only criterion ## implemented. ## ## @item @qcode{'ResponseName'} @tab A character vector naming the response. ## The default is @qcode{'Y'}. ## ## @item @qcode{'ScoreTransform'} @tab A character vector naming a transform ## to apply to the scores, or a function handle. The default is ## @qcode{'none'}. ## ## @item @qcode{'SplitCriterion'} @tab @qcode{'gdi'} (default), the Gini ## diversity index, or @qcode{'deviance'}, the cross entropy. ## ## @item @qcode{'Weights'} @tab A nonnegative single or double vector with one ## element per observation. The default is uniform. The model's @code{W} keeps ## the class of the weights, while every computation runs in double, so ## @code{Prior} and the predictions are double where MATLAB returns single. ## ## @end multitable ## ## Surrogate splits and the @qcode{'twoing'} split criterion are not ## implemented, and an option asking for one of them is refused rather than ## quietly ignored. On a node with three or more classes and more than ## @qcode{'MaxNumCategories'} levels, the heuristic splits, the choice between ## equally good partitions and which side each set of levels takes may differ ## from MATLAB's. ## ## @seealso{ClassificationTree, treetrain, treepredict} ## @end deftypefn function Mdl = fitctree (X, Y, varargin) ## Input validation if (nargin < 2) error ("fitctree: too few arguments."); endif if (mod (nargin, 2) != 0) error ("fitctree: name-value arguments must be in pairs."); endif if (! istable (X) && rows (X) != rows (Y)) error ("fitctree: number of rows in X and Y must be equal."); endif ## Parse arguments to the class constructor Mdl = ClassificationTree (X, Y, varargin{:}); endfunction ## Demo %!demo %! ## Grow a classification tree on Fisher's iris data and look at it %! %! load fisheriris %! Mdl = fitctree (meas, species); %! %! ## The tree as text: a branch names its cut, a leaf names its class %! view (Mdl); %! %! ## How much each predictor contributed %! predictorImportance (Mdl) %! %! ## The resubstitution error of the whole tree and of its subtrees %! resubLoss (Mdl) %!demo %! ## Prune a tree back and watch the error rise as it gets smaller %! %! load fisheriris %! Mdl = fitctree (meas, species); %! %! levels = 0:numel (Mdl.PruneAlpha) - 1; %! leaves = zeros (size (levels)); %! err = zeros (size (levels)); %! for ii = 1:numel (levels) %! sub = prune (Mdl, 'Level', levels(ii)); %! leaves(ii) = sum (! sub.IsBranchNode); %! err(ii) = resubLoss (sub); %! endfor %! [leaves(:), err(:)] ## Tests %!demo %! ## Fit from a table, and predict on one %! %! load fisheriris %! T = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! categorical (species), 'VariableNames', ... %! {'SL', 'SW', 'PL', 'PW', 'Species'}); %! %! ## A column holding levels is a categorical predictor without being named %! ## one, so the width of the sepal is split into two and comes back as the %! ## fifth predictor rather than as a number %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! %! ## The response is named by its column, and everything else is a predictor %! Mdl = fitctree (T, 'Species'); %! Mdl.PredictorNames %! Mdl.CategoricalPredictors %! %! ## A model formula names them instead, holding main effects only %! Mdl2 = fitctree (T, 'Species ~ PL + Wide'); %! Mdl2.PredictorNames %! %! ## predict reads a table by the names the model was fitted on, so the %! ## columns may come in any order and may carry more than the model needs %! label = predict (Mdl, T(1:5, [6, 4, 3, 2, 1, 5])); %! label' %!test # MATLAB parity: the tree a default fit grows on fisheriris %! load fisheriris %! Mdl = fitctree (meas, species); %! assert_equal (class (Mdl), 'ClassificationTree'); %! assert_equal (Mdl.NumNodes, 9); %! assert_equal (Mdl.NodeSize', [150, 50, 100, 54, 46, 48, 6, 47, 1]); %! assert_equal (Mdl.Parent', [0, 1, 1, 3, 3, 4, 4, 6, 6]); %! assert_equal (Mdl.CutPredictorIndex', [3, 0, 4, 3, 0, 4, 0, 0, 0]); %!test # MATLAB parity: the cut points and the predictors they name %! load fisheriris %! Mdl = fitctree (meas, species); %! assert_equal (Mdl.CutPoint([1, 3, 4, 6])', [2.45, 1.75, 4.95, 1.65]); %! assert_equal (isnan (Mdl.CutPoint([2, 5, 7, 8, 9]))', true (1, 5)); %! assert_equal (Mdl.CutPredictor', {'x3', '', 'x4', 'x3', '', 'x4', ... %! '', '', ''}); %! assert_equal (Mdl.CutType', {'continuous', '', 'continuous', ... %! 'continuous', '', 'continuous', '', '', ''}); %!test # MATLAB parity: the class the fit reports for every node %! load fisheriris %! Mdl = fitctree (meas, species); %! assert_equal (Mdl.NodeClass', {'setosa', 'setosa', 'versicolor', ... %! 'versicolor', 'virginica', 'versicolor', ... %! 'virginica', 'versicolor', 'virginica'}); %! assert_equal (Mdl.ClassCount, [50, 50, 50; 50, 0, 0; 0, 50, 50; ... %! 0, 49, 5; 0, 1, 45; 0, 47, 1; ... %! 0, 2, 4; 0, 47, 0; 0, 0, 1]); %!test # MATLAB parity: predict, its four outputs and its scores %! load fisheriris %! Mdl = fitctree (meas, species); %! [label, score, node, cnum] = predict (Mdl, meas([1, 60, 120], :)); %! assert_equal (label, {'setosa'; 'versicolor'; 'virginica'}); %! assert_equal (node', [2, 8, 7]); %! assert_equal (cnum', [1, 2, 3]); %! assert_equal (score, [1, 0, 0; 0, 1, 0; 0, 1/3, 2/3], 1e-14); %!test # MATLAB parity: the losses on the training data %! load fisheriris %! Mdl = fitctree (meas, species); %! assert_equal (resubLoss (Mdl), 0.02, 1e-14); %! assert_equal (loss (Mdl, meas, species), 0.02, 1e-14); %! assert_equal (resubEdge (Mdl), 0.9384, 1e-4); %!test # MATLAB parity: a fit that keeps every split it made %! load fisheriris %! Mdl = fitctree (meas, species, 'MergeLeaves', 'off'); %! assert_equal (Mdl.NumNodes, 11); %! assert_equal (isempty (Mdl.PruneList), false); %!test # MATLAB parity: turning both reductions off leaves no sequence %! load fisheriris %! Mdl = fitctree (meas, species, 'MergeLeaves', 'off', 'Prune', 'off'); %! assert_equal (Mdl.NumNodes, 11); %! assert_equal (isempty (Mdl.PruneList), true); %! assert_equal (isempty (Mdl.PruneAlpha), true); %!test # MATLAB parity: a merged tree carries a sequence with Prune off %! load fisheriris %! Mdl = fitctree (meas, species, 'Prune', 'off'); %! assert_equal (Mdl.PruneList', [4, 0, 3, 2, 0, 1, 0, 0, 0]); %! assert_equal (Mdl.PruneAlpha', [0, 1/150, 2/150, 44/150, 50/150], 1e-14); %!test # MATLAB parity: MinParentSize is raised to two leaves %! load fisheriris %! Mdl = fitctree (meas, species, 'MinParentSize', 3, 'MinLeafSize', 2); %! assert_equal (Mdl.ModelParameters.MinParent, 4); %! assert_equal (Mdl.NodeSize', [150, 50, 100, 54, 46, 48, 6, 3, 3]); %!test # MATLAB parity: a leaf size that stops the tree early %! load fisheriris %! Mdl = fitctree (meas, species, 'MinLeafSize', 20); %! assert_equal (Mdl.NumNodes, 5); %! assert_equal (Mdl.NodeSize', [150, 50, 100, 54, 46]); %!test # MATLAB parity: a budget of two splits %! load fisheriris %! Mdl = fitctree (meas, species, 'MaxNumSplits', 2); %! assert_equal (Mdl.NumNodes, 5); %! assert_equal (Mdl.NodeSize', [150, 50, 100, 54, 46]); %!test # MATLAB parity: the deviance criterion and the risk it reports %! load fisheriris %! Mdl = fitctree (meas, species, 'SplitCriterion', 'deviance'); %! assert_equal (Mdl.NumNodes, 9); %! assert_equal (Mdl.CutPoint([1, 3, 4, 6])', [2.45, 1.75, 4.95, 1.65]); %! assert_equal (Mdl.NodeRisk(1), 0.792481250360578, 1e-12); %! assert_equal (Mdl.NodeRisk(4), 0.0801, 1e-4); %!test # MATLAB parity: the prior a weighted fit reports %! load fisheriris %! Mdl = fitctree (meas, species, 'Weights', (1:150)'); %! assert_equal (Mdl.Prior, [1275, 3775, 6275] / 11325, 1e-14); %! assert_equal (sum (Mdl.W), 1, 1e-14); %! assert_equal (Mdl.NumNodes, 7); %! assert_equal (Mdl.NodeSize', [150, 95, 55, 50, 45, 44, 1]); %!test # MATLAB parity: a uniform prior over an unbalanced sample %! ## The shape of this tree is not asserted. A uniform prior over 50, 20 %! ## and 10 observations makes the split that isolates setosa and the one %! ## that isolates virginica exactly equal, at a gain of 1/3 each, and the %! ## two engines keep opposite sides of the tie. %! load fisheriris %! inds = [1:50, 51:70, 101:110]; %! Mdl = fitctree (meas(inds, :), species(inds), 'Prior', 'uniform'); %! assert_equal (Mdl.Prior, [1/3, 1/3, 1/3], 1e-14); %! assert_equal (Mdl.W([1, 51, 71])', [1/150, 1/60, 1/30], 1e-14); %! assert_equal (sum (Mdl.W), 1, 1e-14); %! assert_equal (Mdl.NodeSize(1), 80); %! assert_equal (resubLoss (Mdl), 0, 1e-14); %!test # MATLAB parity: a cost matrix reshapes the tree %! load fisheriris %! Mdl = fitctree (meas, species, 'Cost', [0, 1, 10; 1, 0, 1; 10, 1, 0]); %! assert_equal (Mdl.NumNodes, 9); %! assert_equal (Mdl.NodeSize', [150, 50, 100, 45, 55, 44, 1, 9, 46]); %! assert_equal (Mdl.CutPoint([1, 3, 4, 5])', [2.45, 4.75, 1.65, 1.75]); %! assert_equal (Mdl.NodeClass{1}, 'versicolor'); %! assert_equal (Mdl.NodeRisk(1), 0.572916666666667, 1e-12); %! assert_equal (Mdl.PruneAlpha', [0, 1/150, 43/150, 50/150], 1e-12); %!test # MATLAB parity: the label type is the response's own %! load fisheriris %! y = strcmp (species, 'setosa'); %! Mdl = fitctree (meas, y); %! assert_equal (class (Mdl.ClassNames), 'logical'); %! assert_equal (class (predict (Mdl, meas(1, :))), 'logical'); %! Mdl = fitctree (meas, grp2idx (species)); %! assert_equal (Mdl.ClassNames, [1; 2; 3]); %! assert_equal (Mdl.NodeClass{1}, '1'); %!test # MATLAB parity: a categorical response gives categorical labels %! load fisheriris %! Mdl = fitctree (meas, categorical (species)); %! label = predict (Mdl, meas([1, 60, 120], :)); %! assert_equal (label, categorical ({'setosa'; 'versicolor'; 'virginica'})); %! assert_equal (Mdl.NodeClass{1}, 'setosa'); %!test # A string response gives string labels, where MATLAB gives cellstr %! load fisheriris %! Mdl = fitctree (meas, string (species)); %! label = predict (Mdl, meas([1, 60, 120], :)); %! assert_equal (label, string ({'setosa'; 'versicolor'; 'virginica'})); %! assert_equal (Mdl.NodeClass{1}, 'setosa'); %!test # MATLAB parity: a missing response drops its row, a missing X does not %! load fisheriris %! x = meas; %! x(1:10, 4) = NaN; %! Mdl = fitctree (x, species); %! assert_equal (Mdl.NumObservations, 150); %! assert_equal (Mdl.RowsUsed, []); %! y = species; %! y(1:10) = {''}; %! Mdl = fitctree (meas, y); %! assert_equal (Mdl.NumObservations, 140); %! assert_equal (sum (Mdl.RowsUsed), 140); %!test # MATLAB parity: ClassNames keeps only the classes it names %! load fisheriris %! Mdl = fitctree (meas, species, 'ClassNames', {'setosa', 'versicolor'}); %! assert_equal (Mdl.ClassNames, {'setosa'; 'versicolor'}); %! assert_equal (Mdl.NumObservations, 100); %! assert_equal (Mdl.NodeSize', [100, 50, 50]); %! assert_equal (Mdl.Prior, [0.5, 0.5], 1e-14); %!test # MATLAB parity: a number below the predictor count is kept %! load fisheriris %! Mdl = fitctree (meas, species, 'NumVariablesToSample', 2); %! assert_equal (Mdl.ModelParameters.NVarToSample, 2); %!test # MATLAB parity: a number covering every predictor is reported 'all' %! load fisheriris %! Mdl = fitctree (meas, species, 'NumVariablesToSample', 5); %! assert_equal (Mdl.ModelParameters.NVarToSample, 'all'); %!test # the generator's state reproduces a sampled tree %! load fisheriris %! rng (1); %! A = fitctree (meas, species, 'NumVariablesToSample', 1); %! rng (1); %! B = fitctree (meas, species, 'NumVariablesToSample', 1); %! assert_equal (A.CutPredictorIndex, B.CutPredictorIndex); %! assert_equal (isequaln (A.CutPoint, B.CutPoint), true); %!test # MATLAB parity: a split budget spent inside a layer keeps the best %! load fisheriris %! T = fitctree (meas(:,1:2), species, 'MaxNumSplits', 7); %! assert_equal (T.CutPredictorIndex', [1, 2, 1, 0, 0, 2, 1, 0, 0, 2, 0, 0, 0]); %! assert_equal (T.NodeSize', [150, 52, 98, 7, 45, 43, 55, 38, 5, 43, 12, ... %! 2, 41]); %! assert_equal (T.CutPoint([1, 2, 3, 6, 7, 10])', ... %! [5.45, 2.8, 6.15, 3.45, 7.05, 2.4], 1e-12); %!test # MATLAB parity: the budget ranks weighted splits by their gain %! load fisheriris %! w = [5 * ones(50, 1); (1:100)' / 20]; %! T = fitctree (meas(:,1:2), species, 'MaxNumSplits', 6, 'Weights', w); %! assert_equal (T.CutPredictorIndex', [1, 2, 1, 1, 0, 2, 2, 0, 0, 0, 0, ... %! 0, 0]); %! assert_equal (T.NodeSize', [150, 59, 91, 12, 47, 36, 55, 3, 9, 33, 3, ... %! 2, 53]); %! assert_equal (T.CutPoint([1, 2, 3, 4, 6, 7])', ... %! [5.55, 2.8, 6.15, 4.95, 3.6, 2.4], 1e-12); %!test # MATLAB parity: the budget ranks deviance splits by their gain %! load fisheriris %! T = fitctree (meas, species, 'MaxNumSplits', 4, ... %! 'SplitCriterion', 'deviance', 'MinParentSize', 2); %! assert_equal (T.CutPredictorIndex', [3, 0, 4, 3, 0, 0, 0]); %! assert_equal (T.NodeSize', [150, 50, 100, 54, 46, 48, 6]); %! assert_equal (T.CutPoint([1, 3, 4])', [2.45, 1.75, 4.95], 1e-12); ## Test input validation %!error fitctree () %!error fitctree (ones (4, 1)) %!error %! fitctree (ones (4, 2), ones (4, 1), 'K') %!error %! fitctree (ones (4, 2), ones (3, 1)) %!error %! fitctree (ones (4, 2), ones (3, 1), 'K', 2) %!test # MATLAB parity: classes given as text are sorted %! load fisheriris %! k = [101:150, 51:100]; %! Mdl = fitctree (meas(k,:), species(k)); %! assert_equal (Mdl.ClassNames, {'versicolor'; 'virginica'}); %!test # MATLAB parity: a given ClassNames order is kept %! load fisheriris %! Mdl = fitctree (meas(51:150,:), species(51:150), ... %! 'ClassNames', {'virginica'; 'versicolor'}); %! assert_equal (Mdl.ClassNames, {'virginica'; 'versicolor'}); %!test # the score columns follow a given ClassNames order %! load fisheriris %! Mdl = fitctree (meas(51:150,:), species(51:150), ... %! 'ClassNames', {'virginica'; 'versicolor'}); %! [label, s] = predict (Mdl, meas(51,:)); %! assert_equal (label, {'versicolor'}); %! assert_equal (s(2) > s(1), true); %!test # classes named as text for numeric labels keep the given order %! load fisheriris %! Y = [ones(50, 1); 2 * ones(50, 1); 3 * ones(50, 1)]; %! Mdl = fitctree (meas, Y, 'ClassNames', {'3', '1', '2'}); %! assert_equal (Mdl.ClassNames, [3; 1; 2]); %!error ... %! load fisheriris %! fitctree (meas, species, 'ClassNames', [3, 1, 2]) %!test # classes named 'true' and 'false' for logical labels %! load fisheriris %! Y = [false(50, 1); true(50, 1)]; %! Mdl = fitctree (meas(51:150,:), Y, 'ClassNames', {'true'; 'false'}); %! assert_equal (Mdl.ClassNames, [true; false]); %!error ... %! load fisheriris %! fitctree (meas(51:150,:), [false(50, 1); true(50, 1)], ... %! 'ClassNames', {'1'; '0'}) %!test # MATLAB parity: 'all' and a logical vector name categorical predictors %! k = (0:79)'; %! c = mod (k, 4) + 1; %! j = floor (k / 4); %! y = (c == 1) | (c == 2 & mod (j, 4) != 0) | (c == 3 & mod (j, 4) == 0); %! Mdl = fitctree ([c, mod(k * 7, 10)], y, 'CategoricalPredictors', 'all'); %! assert_equal (Mdl.CategoricalPredictors, [1, 2]); %! Mdl = fitctree ([c, mod(k * 7, 10)], y, 'CategoricalPredictors', ... %! logical ([1, 0])); %! assert_equal (Mdl.CategoricalPredictors, 1); %! assert_equal (Mdl.CutCategories(1,:), {[1, 2], [3, 4]}); %!test # MATLAB parity: levels need not be integers %! k = (0:79)'; %! c = mod (k, 4) + 1; %! j = floor (k / 4); %! y = (c == 1) | (c == 2 & mod (j, 4) != 0) | (c == 3 & mod (j, 4) == 0); %! Mdl = fitctree ([c + 0.5, mod(k * 7, 10)], y, 'CategoricalPredictors', 1); %! assert_equal (Mdl.CutCategories(1,:), {[1.5, 2.5], [3.5, 4.5]}); ## Table input %!shared ftT %! load fisheriris %! ftT = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! ftT.Species = categorical (species); %! ftT.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! ftT.Flag = meas(:,1) > 5.8; %!test # the response is named by a column and the rest are predictors %! Mdl = fitctree (ftT, 'Species'); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW', 'PL', 'PW', 'Wide', 'Flag'}); %! assert_equal (Mdl.ResponseName, 'Species'); %! assert_equal (class (Mdl), 'ClassificationTree'); ## MATLAB parity: a column holding levels is a categorical predictor without ## being named one, and a logical column is one too %!test %! Mdl = fitctree (ftT, 'Species'); %! assert_equal (Mdl.CategoricalPredictors, [5, 6]); %!test # 'CategoricalPredictors' adds to what the table says, not replaces it %! Mdl = fitctree (ftT, 'Species', 'CategoricalPredictors', 1); %! assert_equal (Mdl.CategoricalPredictors, [1, 5, 6]); %!test # a model formula names the response and the predictors together %! Mdl = fitctree (ftT, 'Species ~ SL + Wide'); %! assert_equal (Mdl.PredictorNames, {'SL', 'Wide'}); %! assert_equal (Mdl.ResponseName, 'Species'); %! assert_equal (Mdl.CategoricalPredictors, 2); %!test # the predictors keep the order the formula wrote them in %! Mdl = fitctree (ftT, 'Species ~ PW + SL'); %! assert_equal (Mdl.PredictorNames, {'PW', 'SL'}); %!test # the response may be given beside a table of predictors %! Mdl = fitctree (ftT(:,1:4), categorical (ftT.Species)); %! assert_equal (Mdl.PredictorNames, {'SL', 'SW', 'PL', 'PW'}); %! assert_equal (Mdl.ResponseName, 'Y'); %! assert_equal (isempty (Mdl.CategoricalPredictors), true); %!test # 'PredictorNames' takes a subset of the table's variables %! Mdl = fitctree (ftT, 'Species', 'PredictorNames', {'SL', 'PW'}); %! assert_equal (Mdl.PredictorNames, {'SL', 'PW'}); %!test # a column of text is a categorical predictor as a categorical is %! T = table (ftT.SL, 'VariableNames', {'SL'}); %! T.g = repmat ({'x'; 'y'; 'z'}, 50, 1); %! T.y = ftT.Species; %! Mdl = fitctree (T, 'y'); %! assert_equal (Mdl.PredictorNames, {'SL', 'g'}); %! assert_equal (Mdl.CategoricalPredictors, 2); %!test # a matrix is taken as it always was %! load fisheriris %! Mdl = fitctree (meas, species); %! assert_equal (columns (Mdl.X), 4); %! assert_equal (isempty (Mdl.PredictorLevels), true); ## Table input validation %!error ... %! fitctree (ftT, 'NoSuch') %!error ... %! fitctree (ftT(:,5), 'Species') %!error ... %! fitctree (ftT, 'Species ~ SL*PW') %!error ... %! fitctree (ftT, 'Species ~ SL + SL^2') %!error ... %! fitctree (ftT, 'Species ~ nope') %!error ... %! fitctree (ftT, 'SL + PW') %!error ... %! fitctree (ftT, 'Species', 'PredictorNames', {'nope'}) %!error ... %! fitctree (ftT, 'Species ~ SL', 'PredictorNames', {'SL'}) statistics-release-2.1.2/inst/Machine_Learning/fitrensemble.m000066400000000000000000000361061527311104300243240ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} fitrensemble (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrensemble (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrensemble (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrensemble (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrensemble (@dots{}, @var{name}, @var{value}) ## ## Fit an ensemble of regression trees. ## ## @code{@var{Mdl} = fitrensemble (@var{X}, @var{Y})} grows 100 regression ## trees by LSBoost on the @math{NxP} predictor matrix @var{X} and the numeric ## response @var{Y}, and returns a @code{RegressionEnsemble}. With ## @qcode{'Method'} set to @qcode{'Bag'}, or with LSBoost resampling, it ## returns a @code{RegressionBaggedEnsemble}. A row missing a predictor or the ## response is left out. ## ## LSBoost starts from a prediction of zero. Each tree is fitted, with the ## observation weights, to the residual of the trees before it, and the ## prediction grows by the learning rate times that tree's prediction, the ## learning rate being the tree's weight. The first tree therefore fits the ## response itself. Bag grows each tree on a sample drawn in proportion to the ## weights and averages them. ## ## LSBoost resamples when @qcode{'Resample'} is @qcode{'on'} or ## @qcode{'FResample'} or @qcode{'Replace'} is given. Each tree is then fitted ## to the residual of @code{ceil (FResample * N)} rows, drawn with replacement ## in proportion to the weights or without replacement uniformly, while the ## prediction and @code{FitInfo} run over every row, as in MATLAB R2024a. The ## ensemble is a @code{RegressionBaggedEnsemble}, which records the rows each ## tree drew and estimates the out-of-bag error. ## ## Name-Value arguments: ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Method'} @tab @tab @qcode{'LSBoost'} (default) or ## @qcode{'Bag'}. ## @item @qcode{'NumLearningCycles'} @tab @tab A positive integer, the number ## of trees to grow. The default is 100. ## @item @qcode{'Learners'} @tab @tab @qcode{'tree'} (default) or a template ## from @code{templateTree}, whose options override the defaults: for LSBoost ## @code{MaxNumSplits} 10, @code{MinParentSize} 10 and @code{MinLeafSize} 5; ## for Bag unlimited splits, @code{MinParentSize} 10, @code{MinLeafSize} 5 ## and @code{NumVariablesToSample} @code{ceil (P / 3)}. ## @item @qcode{'LearnRate'} @tab @tab A number greater than 0 and no greater ## than 1. The default is 1. LSBoost only. ## @item @qcode{'FResample'} @tab @tab The share of the observations each ## tree draws, greater than 0 and no greater than 1. The default is 1. Given ## with LSBoost, the ensemble resamples. ## @item @qcode{'Replace'} @tab @tab @qcode{'on'} (default) or @qcode{'off'}, ## whether the trees draw with replacement. Given with LSBoost, the ensemble ## resamples. ## @item @qcode{'Resample'} @tab @tab @qcode{'off'} (default) or ## @qcode{'on'}, whether LSBoost resamples. Bag always does. ## @item @qcode{'NPrint'} @tab @tab @qcode{'off'} (default) or a positive ## integer @var{n}, to print a line after every @var{n} trees. ## @item @qcode{'Weights'} @tab @tab A nonnegative single or double vector with ## one weight per observation. The default is uniform. The model's @code{W} ## keeps the class of the weights, while every computation runs in double, so ## the predictions are double where MATLAB returns single. ## @item @qcode{'PredictorNames'} @tab @tab A cell array of character vectors ## naming the columns of @var{X}. ## @item @qcode{'ResponseName'} @tab @tab The name of the response variable. ## @item @qcode{'ResponseTransform'} @tab @tab @qcode{'none'} (default), ## @qcode{'exp'}, @qcode{'log'} or a function handle, applied to the ## predictions. MATLAB R2024a accepts only a function handle here, failing ## on the named transforms. ## @end multitable ## ## @qcode{'CrossVal'} set to @qcode{'on'}, @qcode{'KFold'}, ## @qcode{'Holdout'}, @qcode{'Leaveout'} or @qcode{'CVPartition'}, only one of ## them, fits the ensemble and cross-validates it as @code{crossval} does, ## returning a @code{RegressionPartitionedEnsemble}. ## ## Binning and hyperparameter optimization are not implemented, and an option ## asking for one of them is refused. @qcode{'CategoricalPredictors'}, as ## indices, as a logical vector with one element per predictor, or as ## @qcode{'all'}, is passed on to every tree, which splits those predictors ## into sets of levels as @code{fitrtree} does. An ensemble is ## regularized and shrunk afterwards with the @code{regularize}, @code{shrink} ## and @code{cvshrink} methods. ## ## @seealso{RegressionEnsemble, RegressionBaggedEnsemble, ## CompactRegressionEnsemble, templateTree, TreeBagger} ## @end deftypefn function Mdl = fitrensemble (X, Y, varargin) if (nargin < 2) error ("fitrensemble: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error ("fitrensemble: name-value arguments must be in pairs."); endif ## The cross-validation options make the fit a cross-validated one: the ## ensemble is fitted on all the data first, then on each fold. cv = {'crossval', 'kfold', 'holdout', 'leaveout', 'cvpartition'}; iscv = false (size (varargin)); method = []; resampled = false; for i = 1:2:numel (varargin) if (ischar (varargin{i}) && any (strcmpi (varargin{i}, cv))) iscv(i:i+1) = true; elseif (ischar (varargin{i}) && strcmpi (varargin{i}, 'Method')) method = varargin{i+1}; elseif (ischar (varargin{i}) && any (strcmpi (varargin{i}, {'FResample', 'Replace'}))) resampled = true; elseif (ischar (varargin{i}) && strcmpi (varargin{i}, 'Resample')) resampled = resampled || (ischar (varargin{i+1}) && strcmpi (varargin{i+1}, 'on')); endif endfor cvargs = varargin(iscv); varargin = varargin(! iscv); ## LSBoost that resamples is a bagged ensemble too, as in MATLAB. isbag = ischar (method) && strcmpi (method, 'Bag'); if (resampled && ! isbag) isbag = true; if (isempty (method)) varargin(end+1:end+2) = {'Method', 'LSBoost'}; endif endif if (isbag) Mdl = RegressionBaggedEnsemble (X, Y, varargin{:}); else Mdl = RegressionEnsemble (X, Y, varargin{:}); endif if (! isempty (cvargs)) [P, errmsg] = ensemblePartition (cvargs, Mdl.Y, Mdl.NumObservations, false); if (! isempty (errmsg)) error ("fitrensemble: %s", errmsg); endif if (! isempty (P)) Mdl = RegressionPartitionedEnsemble (Mdl, P); endif endif endfunction %!demo %! ## Boost regression trees to predict sepal length from the other three %! ## measurements, and watch the training error fall as trees are added. %! load fisheriris %! X = meas(:,2:4); %! y = meas(:,1); %! Mdl = fitrensemble (X, y, 'NumLearningCycles', 50, 'LearnRate', 0.1); %! plot (loss (Mdl, X, y, 'Mode', 'cumulative')); %! xlabel ('Number of trees'); %! ylabel ('Training mean squared error'); %!demo %! ## A bagged ensemble of regression trees predicts by averaging its trees. %! load fisheriris %! rng (42); %! Mdl = fitrensemble (meas(:,2:4), meas(:,1), 'Method', 'Bag', ... %! 'NumLearningCycles', 30); %! yfit = predict (Mdl, meas([1, 51, 101], 2:4)) ## Test output %!demo %! ## Fit from a table, and predict on one %! %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! %! ## A model formula names the response and the predictors together, and %! ## holds main effects only %! Mdl = fitrensemble (T, 'SL ~ PL + Wide'); %! Mdl.PredictorNames %! Mdl.ResponseName %! %! ## predict matches the table's variables by name, so a column the model %! ## was not fitted on is passed over %! yFit = predict (Mdl, T(1:5,:)); %! yFit' %!shared X, y, S %! load fisheriris %! X = meas(:,2:4); %! y = meas(:,1); %! S = templateTree ('MaxNumSplits', 1); %!test # MATLAB parity: LSBoost on decision stumps %! Mdl = fitrensemble (X, y, 'NumLearningCycles', 4, 'Learners', S); %! assert_equal (class (Mdl), 'RegressionEnsemble'); %! assert_equal (Mdl.TrainedWeights, ones (4, 1)); %! assert_equal (Mdl.FitInfo, [0.263279369032793; 0.185334478476685; ... %! 0.176267398277275; 0.159714716160712], 1e-13); %! assert_equal (Mdl.Trained{2}.CutPoint(1), 6.05, 1e-12); %! assert_equal (Mdl.Trained{2}.NodeMean(2:3)', ... %! [-0.070535138620251, 1.105050505050500], 1e-12); %!test # MATLAB parity: the first tree fits the response itself %! Mdl = fitrensemble (X, y, 'NumLearningCycles', 4, 'Learners', S); %! assert_equal (Mdl.Trained{1}.NodeMean(1), mean (y), 1e-12); %! assert_equal (predict (Mdl, X([1, 51],:)), ... %! [5.085793612916077; 6.335321158531990], 1e-13); %!test # MATLAB parity: the learning rate shrinks each tree's step %! Mdl = fitrensemble (X, y, 'NumLearningCycles', 3, 'Learners', S, ... %! 'LearnRate', 0.1); %! assert_equal (Mdl.TrainedWeights, 0.1 * ones (3, 1), 1e-15); %! assert_equal (Mdl.FitInfo, [0.263279369032793; 0.258079095922867; ... %! 0.253496179589799], 1e-13); %! assert_equal (Mdl.Trained{2}.CutPoint(1), 3.95, 1e-12); %! assert_equal (predict (Mdl, X([1, 51],:)), ... %! [1.391715765572467; 1.746807161347196], 1e-13); %!test # MATLAB parity: observation weights enter every tree %! w = [5 * ones(50, 1); ones(100, 1)]; %! Mdl = fitrensemble (X, y, 'NumLearningCycles', 2, 'Learners', S, ... %! 'Weights', w); %! assert_equal (Mdl.W([1, 51])', [5 / 350, 1 / 350], 1e-15); %! assert_equal (Mdl.FitInfo, [0.185825250456633; 0.147589707296206], 1e-13); %! assert_equal (predict (Mdl, X([1, 51],:)), ... %! [4.988922545877216; 6.342390194075585], 1e-13); %!test # MATLAB parity: LSBoost with its default trees %! Mdl = fitrensemble (X, y, 'NumLearningCycles', 3); %! assert_equal (Mdl.FitInfo, [0.079108234913235; 0.058774516561726; ... %! 0.050203817681049], 1e-13); %! assert_equal (predict (Mdl, X([1, 2, 51, 52, 101, 150],:)), ... %! [5.09375; 4.58949494949495; 6.874251054542892; ... %! 6.212370889253637; 6.607394901394901; ... %! 6.032614996997739], 1e-12); %!test # MATLAB parity: the defaults of a regression ensemble %! Mdl = fitrensemble (X, y, 'NumLearningCycles', 2); %! assert_equal (Mdl.Method, 'LSBoost'); %! assert_equal (Mdl.ModelParameters.LearnRate, 1); %! assert_equal (Mdl.CombineWeights, 'WeightedSum'); %! assert_equal (Mdl.ResponseTransform, 'none'); %!test # MATLAB parity: 'Bag' returns a bagged ensemble %! Mdl = fitrensemble (X, y, 'Method', 'Bag', 'NumLearningCycles', 2); %! assert_equal (class (Mdl), 'RegressionBaggedEnsemble'); %! assert_equal (Mdl.CombineWeights, 'WeightedAverage'); %!test # MATLAB parity: progress is printed after every NPrint trees %! out = evalc (["fitrensemble (X, y, 'NumLearningCycles', 4, ", ... %! "'Learners', S, 'NPrint', 2);"]); %! assert_equal (out, sprintf (["Training LSBoost...\n", ... %! "Grown weak learners: 2\n", ... %! "Grown weak learners: 4\n"])); ## Test input validation %!error fitrensemble (X) %!error ... %! fitrensemble (X, y, 'Method') %!test # MATLAB parity: 'KFold' returns a cross-validated ensemble %! CV = fitrensemble (X, y, 'NumLearningCycles', 2, 'Learners', S, 'KFold', 3); %! assert_equal (class (CV), 'RegressionPartitionedEnsemble'); %! assert_equal (CV.KFold, 3); %!error ... %! fitrensemble (X, y, 'KFold', 5, 'CrossVal', 'on') %!error ... %! fitrensemble (X, y, 'KFold', 0) %!test # MATLAB parity: LSBoost that resamples is a bagged ensemble %! load fisheriris %! M = fitrensemble (meas(:,2:4), meas(:,1), 'NumLearningCycles', 3, ... %! 'FResample', 0.5); %! assert_equal (class (M), 'RegressionBaggedEnsemble'); %! assert_equal (M.Method, 'LSBoost'); %! assert_equal (M.CombineWeights, 'WeightedSum'); %!shared X, yr, Xq %! k = (0:119)'; %! c = mod (k, 4) + 1; %! j = floor (k / 4); %! x2 = mod (k * 7, 10); %! X = [c, x2]; %! yb = (c == 1) | (c == 2 & mod (j, 4) != 0) | (c == 3 & mod (j, 4) == 0) ... %! | (x2 > 7); %! yr = [3; 1; 4; 1.5]; %! yr = yr(c) + 0.1 * sin (k) + 0.2 * x2; %! Xq = [1, 0; 3, 5; 5, 0; NaN, 2; 2.5, 9]; %!test # MATLAB parity: LSBoost trees split a categorical predictor %! Mdl = fitrensemble (X, yr, 'Method', 'LSBoost', 'NumLearningCycles', 4, ... %! 'CategoricalPredictors', 1); %! assert_equal (Mdl.CategoricalPredictors, 1); %! assert_equal (Mdl.Trained{1}.CutCategories(1,:), {[2, 4], [1, 3]}); %! ## The first two rows pass a node of the fourth tree where two partitions %! ## gain the same to 5e-15, which rounding decides; the others do not. %! yhat = predict (Mdl, Xq); %! assert_equal (yhat(3:5)', [3.2692634, 3.2028144, 3.4104563], 1e-6); ## Table input %!shared freT %! load fisheriris %! freT = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! freT.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %!test # the response is named by a column and the rest are predictors %! Mdl = fitrensemble (freT, 'SL'); %! assert_equal (Mdl.PredictorNames, {'SW', 'PL', 'PW', 'Wide'}); %! assert_equal (Mdl.ResponseName, 'SL'); %! assert_equal (Mdl.CategoricalPredictors, 4); %!test # a model formula names the response and the predictors together %! Mdl = fitrensemble (freT, 'SL ~ PL + Wide'); %! assert_equal (Mdl.PredictorNames, {'PL', 'Wide'}); %!test # predict takes a table, matched by name and not by position %! Mdl = fitrensemble (freT, 'SL'); %! a = predict (Mdl, freT); %! assert_equal (numel (a), 150); %! assert_equal (predict (Mdl, freT(:, [5, 4, 3, 2, 1])), a); %!test # the levels travel with the model when it is made compact %! Mdl = fitrensemble (freT, 'SL'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.PredictorLevels, Mdl.PredictorLevels); %! assert_equal (predict (CMdl, freT), predict (Mdl, freT)); %!error ... %! fitrensemble (freT, 'NoSuch') statistics-release-2.1.2/inst/Machine_Learning/fitrgam.m000066400000000000000000000357171527311104300233050ustar00rootroot00000000000000## Copyright (C) 2023 Mohammed Azmat Khan ## Copyright (C) 2023-2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{obj} =} fitrgam (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{obj} =} fitrgam (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{obj} =} fitrgam (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{obj} =} fitrgam (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{obj} =} fitrgam (@var{X}, @var{Y}, @var{name}, @var{value}) ## ## Fit a Generalized Additive Model (GAM) for regression. ## ## @code{@var{obj} = fitrgam (@var{X}, @var{Y})} returns an object of ## class RegressionGAM, with matrix @var{X} containing the predictor data and ## vector @var{Y} containing the continuous response data. ## ## @itemize ## @item ## @var{X} must be a @math{N*P} numeric matrix of input data where rows ## correspond to observations and columns correspond to features or variables. ## @var{X} will be used to train the GAM model. ## @item ## @var{Y} must be @math{N*1} numeric vector containing the response data ## corresponding to the predictor data in @var{X}. @var{Y} must have same ## number of rows as @var{X}. ## @end itemize ## ## @var{Tbl} may stand in place of @var{X}, the response named by one of its ## variables, written into a model formula holding main effects, ## @qcode{'Y ~ x1 + x2'}, or given beside it as @var{Y}. A variable holding ## levels rather than numbers is a categorical predictor without being named ## one, and @qcode{'CategoricalPredictors'} adds to that set rather than ## replacing it. @code{PredictorNames} and @code{ResponseName} come from the ## table. ## ## @code{@var{obj} = fitrgam (@dots{}, @var{name}, @var{value})} returns ## an object of class RegressionGAM with additional properties specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'FitMethod'} @tab A character vector selecting the weak ## learner, either @qcode{'boostedtrees'} or @qcode{'splines'}. The default ## is @qcode{'boostedtrees'}, which boosts one shallow decision tree per ## predictor and is the scheme MATLAB uses. @qcode{'splines'} boosts a ## smoothing spline per predictor instead and is an Octave extension. The ## two take different options and an option meant for one is refused by the ## other rather than ignored, so the rows below say which engine each ## belongs to. ## ## @item @qcode{'predictors'} @tab Predictor Variable names, specified as ## a row vector cell of strings with the same length as the columns in @var{X}. ## If omitted, the program will generate default variable names ## @qcode{(x1, x2, ..., xn)} for each column in @var{X}. ## ## @item @qcode{'responsename'} @tab Response Variable Name, specified as ## a string. If omitted, the default value is @qcode{'Y'}. ## ## @item @qcode{'formula'} @tab (spline option) a model specification given as a ## string in ## the form @qcode{'Y ~ terms'} where @qcode{Y} represents the response variable ## and @qcode{terms} the predictor variables. The formula can be used to ## specify a subset of variables for training model. For example: ## @qcode{'Y ~ x1 + x2 + x3 + x4 + x1:x2 + x2:x3'} specifies four linear terms ## for the first four columns of for predictor data, and @qcode{x1:x2} and ## @qcode{x2:x3} specify the two interaction terms for 1st-2nd and 3rd-4th ## columns respectively. Only these terms will be used for training the model, ## but @var{X} must have at least as many columns as referenced in the formula. ## If Predictor Variable names have been defined, then the terms in the formula ## must reference to those. When @qcode{'formula'} is specified, all terms used ## for training the model are referenced in the @qcode{IntMatrix} field of the ## @var{obj} class object as a matrix containing the column indexes for each ## term including both the predictors and the interactions used. ## ## @item @qcode{'interactions'} @tab a logical matrix, a positive integer ## scalar, or the string @qcode{'all'} for defining the interactions between ## predictor variables. When given a logical matrix, it must have the same ## number of columns as @var{X} and each row corresponds to a different ## interaction term combining the predictors indexed as @qcode{true}. Each ## interaction term is appended as a column vector after the available predictor ## column in @var{X}. When @qcode{'all'} is defined, then all possible ## combinations of interactions are appended in @var{X} before training. At the ## moment, parsing a positive integer has the same effect as the @qcode{'all'} ## option. When @qcode{'interactions'} is specified, only the interaction terms ## appended to @var{X} are referenced in the @qcode{IntMatrix} field of the ## @var{obj} class object. ## ## @item @qcode{'knots'} @tab (spline option) a scalar or a row vector with the ## same ## columns as @var{X}. It defines the knots for fitting a polynomial when ## training the GAM. As a scalar, it is expanded to a row vector. The default ## value is 5, hence expanded to @qcode{ones (1, columns (X)) * 5}. You can ## parse a row vector with different number of knots for each predictor ## variable to be fitted with, although not recommended. ## ## @item @qcode{'order'} @tab (spline option) a scalar or a row vector with the ## same ## columns as @var{X}. It defines the order of the polynomial when training the ## GAM. As a scalar, it is expanded to a row vector. The default values is 3, ## hence expanded to @qcode{ones (1, columns (X)) * 3}. You can parse a row ## vector with different number of polynomial order for each predictor variable ## to be fitted with, although not recommended. ## ## @item @qcode{'dof'} @tab (spline option) a scalar or a row vector with the ## same columns ## as @var{X}. It defines the degrees of freedom for fitting a polynomial when ## training the GAM. As a scalar, it is expanded to a row vector. The default ## value is 8, hence expanded to @qcode{ones (1, columns (X)) * 8}. You can ## parse a row vector with different degrees of freedom for each predictor ## variable to be fitted with, although not recommended. ## ## @item @qcode{'tol'} @tab (spline option) a positive scalar to set the ## tolerance for ## convergence during training. By default, it is set to @qcode{1e-3}. ## @end multitable ## ## The rows above marked as spline options require ## @qcode{'FitMethod', 'splines'}. The remaining options belong to the ## boosted-tree engine and require @qcode{'FitMethod', 'boostedtrees'}, which ## is the default. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'NumTreesPerPredictor'} @tab A positive integer, the number of ## boosting rounds of the predictor phase. It is a budget rather than a ## count: a fit that stops improving ends earlier and reports so. The default ## is 300. ## ## @item @qcode{'NumTreesPerInteraction'} @tab A positive integer, the same ## budget for the interaction phase. The default is 100. ## ## @item @qcode{'MaxNumSplitsPerPredictor'} @tab A positive integer, the ## largest number of splits any one predictor tree may make. The default is ## 1, which makes each tree a stump. ## ## @item @qcode{'MaxNumSplitsPerInteraction'} @tab The same limit for a tree ## over a pair of predictors. The default is 4. ## ## @item @qcode{'InitialLearnRateForPredictors'} @tab A value greater than 0 ## and at most 1, the step a round of the predictor phase starts at. A round ## that fails to improve the fit is retried at half the step, so this is an ## initial value rather than a fixed one. The default is 1. ## ## @item @qcode{'InitialLearnRateForInteractions'} @tab The same for the ## interaction phase. The default is 1. ## ## @item @qcode{'MaxPValue'} @tab A value between 0 and 1. A candidate pair ## of predictors is kept only if its interaction test gives a @math{p}-value ## no larger than this. The default is 1, which keeps every pair asked for. ## ## @item @qcode{'Verbose'} @tab A non-negative integer. Greater than zero ## prints a trace of the fit. The default is 0. ## ## @item @qcode{'NumPrint'} @tab A positive integer, how often the trace ## reports: the first round and then every @var{NumPrint} rounds. The ## default is 10. ## ## @item @qcode{'CategoricalPredictors'} @tab The predictors to treat as ## categorical: a vector of column indices, a logical vector with one element ## per predictor, or @qcode{'all'}. A tree splits a categorical predictor ## into two sets of levels, and a level not seen in training predicts as a ## missing value. Its @qcode{BinEdges} and @qcode{PairDetectionBinEdges} are ## empty. The default is none. ## A predictor may be named rather than indexed, as a character matrix of one ## padded name per row, a string array or a cellstr; a name must match an entry ## of @qcode{'PredictorNames'} exactly, its case included. ## ## @item @qcode{'Weights'} @tab A single or double vector of non-negative ## observation weights, one per row of @var{X}. Only their proportions matter. ## The model's @code{W} keeps the class of the weights, while every computation ## runs in double, so the predictions are double where MATLAB returns single. ## ## @end multitable ## ## You can parse either a @qcode{'formula'} or an @qcode{'interactions'} ## optional parameter. Parsing both parameters will result an error. ## Accordingly, you can only pass up to two parameters among @qcode{'knots'}, ## @qcode{'order'}, and @qcode{'dof'} to define the required polynomial for ## training the GAM model. ## ## @seealso{RegressionGAM, regress, regress_gp} ## @end deftypefn function obj = fitrgam (X, Y, varargin) ## Check input parameters if (nargin < 2) error ("fitrgam: too few arguments."); endif if (mod (nargin, 2) != 0) error ("fitrgam: Name-Value arguments must be in pairs."); endif ## Check predictor data and labels have equal rows if (! istable (X) && rows (X) != rows (Y)) error ("fitrgam: number of rows in X and Y must be equal."); endif ## Parse arguments to class def function obj = RegressionGAM (X, Y, varargin{:}); endfunction %!demo %! rng (42); %! # Train a RegressionGAM Model for synthetic values %! %! f1 = @(x) cos (3 *x); %! f2 = @(x) x .^ 3; %! %! # generate x1 and x2 for f1 and f2 %! x1 = 2 * rand (50, 1) - 1; %! x2 = 2 * rand (50, 1) - 1; %! %! # calculate y %! y = f1(x1) + f2(x2); %! %! # add noise %! y = y + y .* 0.2 .* rand (50,1); %! X = [x1, x2]; %! %! # create an object %! a = fitrgam (X, y, 'FitMethod', 'splines', 'tol', 1e-3) ## Test constructor %!demo %! ## Fit from a table, and predict on one %! %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! %! ## A column holding levels is a categorical predictor without being named %! ## one %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! %! ## A model formula names the response and the predictors together, and %! ## holds main effects only %! Mdl = fitrgam (T, 'SL ~ PL + Wide'); %! Mdl.PredictorNames %! Mdl.ResponseName %! %! ## predict matches the table's variables by name, so a column the model %! ## was not fitted on is passed over %! yFit = predict (Mdl, T(1:5,:)); %! yFit' %!test %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = [1; 2; 3; 4]; %! a = fitrgam (x, y, 'FitMethod', 'splines'); %! assert_equal ({a.X, a.Y}, {x, y}) %! assert_equal ({a.BaseModel.Intercept}, {2.5000}) %! assert_equal ({a.Knots, a.Order, a.DoF}, {[5, 5, 5], [3, 3, 3], [8, 8, 8]}) %! assert_equal ({a.NumObservations, a.NumPredictors}, {4, 3}) %! assert_equal ({a.ResponseName, a.PredictorNames}, {'Y', {'x1', 'x2', 'x3'}}) %! assert_equal ({a.Formula}, {[]}) %!test %! x = [1, 2, 3, 4; 4, 5, 6, 7; 7, 8, 9, 1; 3, 2, 1, 2]; %! y = [1; 2; 3; 4]; %! pnames = {'A', 'B', 'C', 'D'}; %! formula = 'Y ~ A + B + C + D + A:C'; %! intMat = logical ([1,0,0,0;0,1,0,0;0,0,1,0;0,0,0,1;1,0,1,0]); %! a = fitrgam (x, y, 'FitMethod', 'splines', ... %! 'predictors', pnames, 'formula', formula); %! assert_equal (a.IntMatrix, double (intMat)) %! assert_equal ({a.ResponseName, a.PredictorNames}, {'Y', pnames}) %! assert_equal (a.Formula, formula) ## Test input validation %!error fitrgam () %!error fitrgam (ones (10,2)) %!error %! fitrgam (ones (4,2), ones (4, 1), 'K') %!error %! fitrgam (ones (4,2), ones (3, 1)) %!error %! fitrgam (ones (4,2), ones (3, 1), 'K', 2) ## Table input %!shared frgT %! load fisheriris %! frgT = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! frgT.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %!test # the response is named by a column and the rest are predictors %! Mdl = fitrgam (frgT, 'SL'); %! assert_equal (Mdl.PredictorNames, {'SW', 'PL', 'PW', 'Wide'}); %! assert_equal (Mdl.ResponseName, 'SL'); %! assert_equal (Mdl.CategoricalPredictors, 4); %!test # a model formula names the response and the predictors together %! Mdl = fitrgam (frgT, 'SL ~ PL + Wide'); %! assert_equal (Mdl.PredictorNames, {'PL', 'Wide'}); %! assert_equal (Mdl.CategoricalPredictors, 2); %!test # the response may be given beside a table of predictors %! Mdl = fitrgam (frgT(:,1:3), frgT.SL); %! assert_equal (Mdl.PredictorNames, {'SW', 'PL', 'PW'}); %! assert_equal (Mdl.ResponseName, 'Y'); %!test # predict takes a table, matched by name and not by position %! Mdl = fitrgam (frgT, 'SL'); %! a = predict (Mdl, frgT); %! assert_equal (numel (a), 150); %! assert_equal (predict (Mdl, frgT(:, [5, 4, 3, 2, 1])), a); %!test # the levels travel with the model when it is made compact %! Mdl = fitrgam (frgT, 'SL'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.PredictorLevels, Mdl.PredictorLevels); %! assert_equal (predict (CMdl, frgT), predict (Mdl, frgT)); %!error ... %! fitrgam (frgT, 'NoSuch') %!error ... %! fitrgam (frgT, 'SL ~ PL*PW') %!error ... %! predict (fitrgam (frgT, 'SL'), frgT(:, [1, 3, 4, 5])) statistics-release-2.1.2/inst/Machine_Learning/fitrgp.m000066400000000000000000000202741527311104300231370ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} fitrgp (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrgp (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrgp (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrgp (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrgp (@dots{}, @var{name}, @var{value}) ## ## Fit a Gaussian process regression model. ## ## @code{@var{Mdl} = fitrgp (@var{X}, @var{Y})} returns a @qcode{RegressionGP} ## object fitted to the predictor data @var{X} and the continuous response ## @var{Y}, where @var{X} is an @math{NxP} numeric matrix and @var{Y} an ## @math{Nx1} numeric vector with as many rows as @var{X}. ## ## @code{@var{Mdl} = fitrgp (@dots{}, @var{name}, @var{value})} passes the ## given @qcode{Name-Value} pairs to the model. They are documented under ## @code{RegressionGP}, and the ones most often wanted are ## @qcode{'KernelFunction'}, @qcode{'BasisFunction'}, @qcode{'Standardize'}, ## @qcode{'Sigma'} and @qcode{'FitMethod'}. ## ## When any of @qcode{'CrossVal'}, @qcode{'KFold'}, @qcode{'Holdout'}, ## @qcode{'Leaveout'} or @qcode{'CVPartition'} is given, a cross validated ## model is returned instead, as a @qcode{RegressionPartitionedModel}. Only ## one of them may be given at a time. ## ## @seealso{RegressionGP, CompactRegressionGP, RegressionPartitionedModel} ## @end deftypefn function Mdl = fitrgp (X, Y, varargin) ## Check input parameters if (nargin < 2) error ("fitrgp: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error ("fitrgp: name-value arguments must be in pairs."); endif ## Pull out the cross validation options, which the model does not take: ## they say what to do with the model once it exists, not how to fit it. CrossVal = false; cvArgs = {}; given = 0; args = {}; while (numel (varargin) > 0) switch (lower (varargin{1})) case 'crossval' val = varargin{2}; if (! (ischar (val) && any (strcmpi (val, {'on', 'off'})))) error ("fitrgp: 'CrossVal' must be either 'on' or 'off'."); endif CrossVal = strcmpi (val, 'on'); case {'kfold', 'holdout', 'leaveout', 'cvpartition'} cvArgs = [cvArgs, varargin(1:2)]; CrossVal = true; given++; otherwise args = [args, varargin(1:2)]; endswitch varargin(1:2) = []; endwhile if (given > 1) error (strcat ("fitrgp: you can use only one of 'KFold', 'Holdout',", ... " 'Leaveout', or 'CVPartition' options.")); endif Mdl = RegressionGP (X, Y, args{:}); if (CrossVal) Mdl = crossval (Mdl, cvArgs{:}); endif endfunction %!demo %! ## Fit a Gaussian process to a noisy sine and predict on a fine grid. %! x = linspace (0, 2*pi, 30)'; %! y = sin (x) + 0.1 * cos (7*x); %! Mdl = fitrgp (x, y) %! xq = linspace (0, 2*pi, 5)'; %! [yq, ysd] = predict (Mdl, xq) %!demo %! ## Fit from a table, and predict on one %! %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! %! ## A column holding levels is a categorical predictor without being named %! ## one %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! %! ## The response is named by its column, and everything else is a predictor %! Mdl = fitrgp (T, 'SL'); %! Mdl.PredictorNames %! Mdl.ResponseName %! Mdl.CategoricalPredictors %! %! ## A model formula names them instead, holding main effects only %! Mdl2 = fitrgp (T, 'SL ~ PL + Wide'); %! Mdl2.PredictorNames %! %! ## predict reads a table by the names the model was fitted on, so the %! ## columns may come in any order %! yFit = predict (Mdl, T(1:5, [5, 4, 3, 2, 1])); %! yFit' %!test %! ## fitrgp returns the model the class constructor returns %! x = linspace (0, 1, 15)'; %! y = cos (3*x) + 0.1 * sin (11*x); %! M1 = fitrgp (x, y); %! M2 = RegressionGP (x, y); %! assert_equal (class (M1), 'RegressionGP'); %! assert_equal (M1.Beta, M2.Beta); %! assert_equal (M1.Sigma, M2.Sigma); %! assert_equal (predict (M1, x), predict (M2, x), 1e-14); %!test %! ## The options reach the model %! x = linspace (0, 1, 15)'; %! y = cos (3*x) + 0.1 * sin (11*x); %! Mdl = fitrgp (x, y, 'KernelFunction', 'matern52', ... %! 'BasisFunction', 'linear', 'ResponseName', 'temp'); %! assert_equal (Mdl.KernelFunction, 'Matern52'); %! assert_equal (Mdl.BasisFunction, 'Linear'); %! assert_equal (Mdl.ResponseName, 'temp'); %! assert_equal (numel (Mdl.Beta), 2); %!test %! ## A cross validation option returns a partitioned model instead %! x = linspace (0, 1, 20)'; %! y = cos (3*x) + 0.1 * sin (11*x); %! CVMdl = fitrgp (x, y, 'KFold', 4); %! assert_equal (class (CVMdl), 'RegressionPartitionedModel'); %! assert_equal (CVMdl.KFold, 4); %! assert_equal (CVMdl.CrossValidatedModel, 'GP'); %!test %! ## 'CrossVal' on gives the ten folds it defaults to %! x = linspace (0, 1, 20)'; %! y = cos (3*x) + 0.1 * sin (11*x); %! CVMdl = fitrgp (x, y, 'CrossVal', 'on'); %! assert_equal (class (CVMdl), 'RegressionPartitionedModel'); %! assert_equal (CVMdl.KFold, 10); %!test %! ## 'CrossVal' off is the model itself %! x = linspace (0, 1, 15)'; %! y = cos (3*x); %! assert_equal (class (fitrgp (x, y, 'CrossVal', 'off')), 'RegressionGP'); ## Test input validation %!error fitrgp (ones (5, 2)) %!error ... %! fitrgp (ones (5, 2), ones (5, 1), 'Standardize') %!error ... %! fitrgp (ones (5, 2), ones (5, 1), 'CrossVal', 5) %!error ... %! fitrgp (ones (20, 2), ones (20, 1), 'KFold', 3, 'Holdout', 0.2) ## Table input %!shared fgpT %! load fisheriris %! fgpT = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! fgpT.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %!test # the response is named by a column and the rest are predictors %! Mdl = fitrgp (fgpT, 'SL'); %! assert_equal (Mdl.PredictorNames, {'SW', 'PL', 'PW', 'Wide'}); %! assert_equal (Mdl.ResponseName, 'SL'); %! assert_equal (Mdl.CategoricalPredictors, 4); %!test # a model formula names the response and the predictors together %! Mdl = fitrgp (fgpT, 'SL ~ PL + Wide'); %! assert_equal (Mdl.PredictorNames, {'PL', 'Wide'}); %! assert_equal (Mdl.CategoricalPredictors, 2); %!test # the response may be given beside a table of predictors %! Mdl = fitrgp (fgpT(:,1:3), fgpT.SL); %! assert_equal (Mdl.PredictorNames, {'SW', 'PL', 'PW'}); %! assert_equal (Mdl.ResponseName, 'Y'); %!test # predict takes a table, matched by name and not by position %! Mdl = fitrgp (fgpT, 'SL'); %! a = predict (Mdl, fgpT); %! assert_equal (numel (a), 150); %! assert_equal (predict (Mdl, fgpT(:, [5, 4, 3, 2, 1])), a); %!test # the levels travel with the model when it is made compact %! Mdl = fitrgp (fgpT, 'SL'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.PredictorLevels, Mdl.PredictorLevels); %! assert_equal (predict (CMdl, fgpT), predict (Mdl, fgpT)); %!error ... %! fitrgp (fgpT, 'NoSuch') %!error ... %! fitrgp (fgpT, 'SL ~ PL*PW') %!error ... %! predict (fitrgp (fgpT, 'SL'), fgpT(:, [1, 3, 4, 5])) statistics-release-2.1.2/inst/Machine_Learning/fitrkernel.m000066400000000000000000000210641527311104300240070ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} fitrkernel (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrkernel (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrkernel (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrkernel (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrkernel (@dots{}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{Mdl}, @var{FitInfo}] =} fitrkernel (@dots{}) ## ## Fit a Gaussian kernel regression model. ## ## @code{@var{Mdl} = fitrkernel (@var{X}, @var{Y})} returns a ## @qcode{RegressionKernel} object fitted to the predictor data @var{X} and ## the continuous response @var{Y}, where @var{X} is an @math{NxP} numeric ## matrix and @var{Y} an @math{Nx1} numeric vector with as many rows as ## @var{X}. ## ## @code{@var{Mdl} = fitrkernel (@dots{}, @var{name}, @var{value})} passes ## the given @qcode{Name-Value} pairs to the model. They are documented ## under @code{RegressionKernel}, and the ones most often wanted are ## @qcode{'Learner'}, @qcode{'Epsilon'}, ## @qcode{'NumExpansionDimensions'}, @qcode{'KernelScale'}, ## @qcode{'Lambda'} and @qcode{'BoxConstraint'}. ## ## @code{[@var{Mdl}, @var{FitInfo}] = fitrkernel (@dots{})} also returns a ## structure describing the optimization: the objective it reached, the ## gradient it left, and the tolerances it was given. ## ## @code{@var{Mdl} = fitrkernel (@dots{}, @var{cvopt}, @var{value})} returns a ## @code{RegressionPartitionedKernel} ## instead when one of @qcode{'CrossVal'}, @qcode{'KFold'}, ## @qcode{'Holdout'}, @qcode{'Leaveout'} and @qcode{'CVPartition'} is ## given. A cross-validated model describes no single fit, so ## @var{FitInfo} is not available beside it. ## ## @seealso{RegressionKernel, RegressionLinear, fitrlinear} ## @end deftypefn function [Mdl, FitInfo] = fitrkernel (X, Y, varargin) ## Check input parameters if (nargin < 2) error ("fitrkernel: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error ("fitrkernel: name-value arguments must be in pairs."); endif ## A cross-validation option asks for a partitioned model rather than a ## fitted one, and the two are different classes with different methods. ## MATLAB refuses a second output there, having no single fit to describe, ## and so does this. ## 'CrossVal' is the one that carries a value saying whether to cross ## validate at all; the other four ask for it by being present. cvNames = {'kfold', 'holdout', 'leaveout', 'cvpartition'}; crossval = false; for k = 1:2:numel (varargin) if (! ischar (varargin{k})) continue; endif if (any (strcmpi (varargin{k}, cvNames))) crossval = true; elseif (strcmpi (varargin{k}, 'crossval')) val = varargin{k+1}; if (! (ischar (val) && any (strcmpi (val, {'on', 'off'})))) error ("%s: 'CrossVal' must be either 'on' or 'off'.", 'fitrkernel'); endif crossval = crossval || strcmpi (val, 'on'); endif endfor if (crossval) if (nargout > 1) error (strcat ("fitrkernel: a cross validated model has no", ... " FitInfo to return; ask for the model alone.")); endif Mdl = RegressionPartitionedKernel (X, Y, varargin{:}); return; endif ## 'CrossVal', 'off' has said its piece and the learner does not take it. keep = true (1, numel (varargin)); for k = 1:2:numel (varargin) if (ischar (varargin{k}) && strcmpi (varargin{k}, 'crossval')) keep(k:k+1) = false; endif endfor varargin = varargin(keep); Mdl = RegressionKernel (X, Y, varargin{:}); if (nargout > 1) FitInfo = fitInfo_ (Mdl); endif endfunction %!demo %! ## Fit a Gaussian kernel regression to fuel consumption and read what the %! ## optimization did. %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! [Mdl, FitInfo] = fitrkernel (X(ok,:), MPG(ok)) %!demo %! ## Fit from a table, and predict on one %! %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! %! ## A model formula names the response and the predictors together, and %! ## holds main effects only %! Mdl = fitrkernel (T, 'SL ~ PL + PW'); %! Mdl.PredictorNames %! Mdl.ResponseName %! %! ## predict matches the table's variables by name, so a column the model %! ## was not fitted on is passed over %! yFit = predict (Mdl, T(1:5,:)); %! yFit' %!test %! ## The driver returns a kernel regression model %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! Mdl = fitrkernel (X(ok,:), MPG(ok)); %! assert_equal (class (Mdl), 'RegressionKernel'); %! assert_equal (Mdl.Epsilon, 0.926612305411416, 1e-12); %!test %! ## The options reach the model %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! Mdl = fitrkernel (X(ok,:), MPG(ok), 'Learner', 'leastsquares', ... %! 'Standardize', true, 'NumExpansionDimensions', 64); %! assert_equal (Mdl.Learner, 'leastsquares'); %! assert_equal (Mdl.NumExpansionDimensions, 64); %! assert_equal (Mdl.Mu, mean (X(ok,:)), 1e-12); %!test %! ## The second output describes the optimization %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! [~, FitInfo] = fitrkernel (X(ok,:), MPG(ok)); %! assert_equal (FitInfo.Solver, 'LBFGS-fast'); %! assert_equal (FitInfo.LossFunction, 'epsiloninsensitive'); %!test %! ## A cross-validation option returns a partitioned model instead %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! X = X(ok,:); %! Y = MPG(ok); %! CVMdl = fitrkernel (X, Y, 'KFold', 5); %! assert_equal (class (CVMdl), 'RegressionPartitionedKernel'); %! assert_equal (CVMdl.KFold, 5); %! assert_equal (numel (CVMdl.Trained), 5); %!test %! ## 'CrossVal' on gives the ten folds it defaults to, and 'off' the model %! ## itself %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! X = X(ok,:); %! Y = MPG(ok); %! CVMdl = fitrkernel (X, Y, 'CrossVal', 'on'); %! assert_equal (CVMdl.KFold, 10); %! assert_equal (class (fitrkernel (X, Y, 'CrossVal', 'off')), ... %! 'RegressionKernel'); %!error ... %! [Mdl, FitInfo] = fitrkernel (ones (10, 2), ones (10, 1), 'KFold', 3); ## Test input validation %!error fitrkernel (ones (5, 2)) %!error ... %! fitrkernel (ones (10, 2), ones (10, 1), 'Learner') %!error ... %! fitrkernel (ones (10, 2), ones (10, 1), 'Learner', 'logistic') ## Table input %!shared frkT %! load fisheriris %! frkT = table (meas(:,2), meas(:,3), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'SL'}); %!test # the response is named by a column and the rest are predictors %! Mdl = fitrkernel (frkT, 'SL'); %! assert_equal (Mdl.PredictorNames, {'SW', 'PL'}); %! assert_equal (Mdl.ResponseName, 'SL'); %!test # a model formula names the response and the predictors together %! Mdl = fitrkernel (frkT, 'SL ~ PL'); %! assert_equal (Mdl.PredictorNames, {'PL'}); %!test # predict takes a table, matched by name and not by position %! Mdl = fitrkernel (frkT, 'SL'); %! a = predict (Mdl, frkT); %! assert_equal (predict (Mdl, frkT(:, [3, 2, 1])), a); %!test # a cross-validated fit takes a table too %! Mdl = fitrkernel (frkT, 'SL', 'KFold', 3); %! assert_equal (class (Mdl), 'RegressionPartitionedKernel'); %!error ... %! fitrkernel (frkT, 'NoSuch') statistics-release-2.1.2/inst/Machine_Learning/fitrlinear.m000066400000000000000000000221001527311104300237710ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} fitrlinear (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrlinear (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrlinear (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrlinear (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrlinear (@dots{}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{Mdl}, @var{FitInfo}] =} fitrlinear (@dots{}) ## ## Fit a linear regression model. ## ## @code{@var{Mdl} = fitrlinear (@var{X}, @var{Y})} returns a ## @qcode{RegressionLinear} object fitted to the predictor data @var{X} and ## the continuous response @var{Y}, where @var{X} is an @math{NxP} numeric ## matrix and @var{Y} an @math{Nx1} numeric vector with as many rows as ## @var{X}. ## ## @code{@var{Mdl} = fitrlinear (@dots{}, @var{name}, @var{value})} passes ## the given @qcode{Name-Value} pairs to the model. They are documented ## under @code{RegressionLinear}, and the ones most often wanted are ## @qcode{'Learner'}, @qcode{'Epsilon'}, @qcode{'Regularization'}, ## @qcode{'Lambda'} and @qcode{'Solver'}. ## ## @code{[@var{Mdl}, @var{FitInfo}] = fitrlinear (@dots{})} also returns a ## structure describing the optimization: what it converged to, how far it ## got, and which tolerance stopped it. Its fields follow the solver, so a ## dual fit reports the dual variables and a mini-batch fit the batch it ## stopped on. ## ## @code{@var{Mdl} = fitrlinear (@dots{}, @var{cvopt}, @var{value})} returns a ## @code{RegressionPartitionedLinear} ## instead when one of @qcode{'CrossVal'}, @qcode{'KFold'}, ## @qcode{'Holdout'}, @qcode{'Leaveout'} and @qcode{'CVPartition'} is ## given. A cross-validated model describes no single fit, so ## @var{FitInfo} is not available beside it. ## ## @seealso{RegressionLinear, RegressionKernel, fitrkernel} ## @end deftypefn function [Mdl, FitInfo] = fitrlinear (X, Y, varargin) ## Check input parameters if (nargin < 2) error ("fitrlinear: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error ("fitrlinear: name-value arguments must be in pairs."); endif ## A cross-validation option asks for a partitioned model rather than a ## fitted one, and the two are different classes with different methods. ## MATLAB refuses a second output there, having no single fit to describe, ## and so does this. ## 'CrossVal' is the one that carries a value saying whether to cross ## validate at all; the other four ask for it by being present. cvNames = {'kfold', 'holdout', 'leaveout', 'cvpartition'}; crossval = false; for k = 1:2:numel (varargin) if (! ischar (varargin{k})) continue; endif if (any (strcmpi (varargin{k}, cvNames))) crossval = true; elseif (strcmpi (varargin{k}, 'crossval')) val = varargin{k+1}; if (! (ischar (val) && any (strcmpi (val, {'on', 'off'})))) error ("%s: 'CrossVal' must be either 'on' or 'off'.", 'fitrlinear'); endif crossval = crossval || strcmpi (val, 'on'); endif endfor if (crossval) if (nargout > 1) error (strcat ("fitrlinear: a cross validated model has no", ... " FitInfo to return; ask for the model alone.")); endif Mdl = RegressionPartitionedLinear (X, Y, varargin{:}); return; endif ## 'CrossVal', 'off' has said its piece and the learner does not take it. keep = true (1, numel (varargin)); for k = 1:2:numel (varargin) if (ischar (varargin{k}) && strcmpi (varargin{k}, 'crossval')) keep(k:k+1) = false; endif endfor varargin = varargin(keep); Mdl = RegressionLinear (X, Y, varargin{:}); if (nargout > 1) FitInfo = fitInfo_ (Mdl); endif endfunction %!demo %! ## Fit a linear regression to fuel consumption and read what the %! ## optimization did. %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! [Mdl, FitInfo] = fitrlinear (X(ok,:), MPG(ok), 'Learner', 'leastsquares') %!demo %! ## Fit from a table, and predict on one %! %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! %! ## A column holding levels is a categorical predictor without being named %! ## one %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! %! ## The response is named by its column, and everything else is a predictor %! Mdl = fitrlinear (T, 'SL'); %! Mdl.PredictorNames %! Mdl.CategoricalPredictors %! %! ## A model formula names them instead, holding main effects only %! Mdl2 = fitrlinear (T, 'SL ~ PL + Wide'); %! Mdl2.PredictorNames %! %! ## predict reads a table by the names the model was fitted on, so the %! ## columns may come in any order and may carry more than the model needs %! yFit = predict (Mdl, T(1:5, [5, 4, 3, 2, 1])); %! yFit' %!test %! ## The driver returns what the class constructor returns %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! M1 = fitrlinear (X(ok,:), MPG(ok)); %! M2 = RegressionLinear (X(ok,:), MPG(ok)); %! assert_equal (class (M1), 'RegressionLinear'); %! assert_equal (M1.Beta, M2.Beta); %! assert_equal (M1.Epsilon, M2.Epsilon); %!test %! ## The options reach the model %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! Mdl = fitrlinear (X(ok,:), MPG(ok), 'Learner', 'leastsquares', ... %! 'Lambda', 0.02, 'ResponseName', 'mpg'); %! assert_equal (Mdl.Learner, 'leastsquares'); %! assert_equal (Mdl.Lambda, 0.02); %! assert_equal (Mdl.ResponseName, 'mpg'); %!test %! ## The second output describes the optimization %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! [~, FitInfo] = fitrlinear (X(ok,:), MPG(ok)); %! assert_equal (FitInfo.Solver, {'bfgs'}); %! assert_equal (FitInfo.Lambda, 1 / 93, 1e-15); %! assert_equal (isfield (FitInfo, 'Objective'), true); %!test %! ## A cross-validation option returns a partitioned model instead %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! X = X(ok,:); %! Y = MPG(ok); %! CVMdl = fitrlinear (X, Y, 'KFold', 5); %! assert_equal (class (CVMdl), 'RegressionPartitionedLinear'); %! assert_equal (CVMdl.KFold, 5); %! assert_equal (numel (CVMdl.Trained), 5); %!test %! ## 'CrossVal' on gives the ten folds it defaults to, and 'off' the model %! ## itself %! load carsmall %! X = [Acceleration, Displacement, Horsepower, Weight]; %! ok = ! any (isnan ([X, MPG]), 2); %! X = X(ok,:); %! Y = MPG(ok); %! CVMdl = fitrlinear (X, Y, 'CrossVal', 'on'); %! assert_equal (CVMdl.KFold, 10); %! assert_equal (class (fitrlinear (X, Y, 'CrossVal', 'off')), ... %! 'RegressionLinear'); %!error ... %! [Mdl, FitInfo] = fitrlinear (ones (10, 2), ones (10, 1), 'KFold', 3); ## Test input validation %!error fitrlinear (ones (5, 2)) %!error ... %! fitrlinear (ones (10, 2), ones (10, 1), 'Learner') %!error ... %! fitrlinear (ones (10, 2), ones (10, 1), 'Learner', 'logistic') ## Table input %!shared frlT %! load fisheriris %! frlT = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %!test # the response is named by a column and the rest are predictors %! Mdl = fitrlinear (frlT, 'SL'); %! assert_equal (Mdl.PredictorNames, {'SW', 'PL', 'PW'}); %! assert_equal (Mdl.ResponseName, 'SL'); %!test # a model formula names the response and the predictors together %! Mdl = fitrlinear (frlT, 'SL ~ PL + PW'); %! assert_equal (Mdl.PredictorNames, {'PL', 'PW'}); %!test # predict takes a table, matched by name and not by position %! Mdl = fitrlinear (frlT, 'SL'); %! a = predict (Mdl, frlT); %! assert_equal (predict (Mdl, frlT(:, [4, 3, 2, 1])), a); %!test # a cross-validated fit takes a table too %! Mdl = fitrlinear (frlT, 'SL', 'KFold', 3); %! assert_equal (class (Mdl), 'RegressionPartitionedLinear'); %!error ... %! fitrlinear (frlT, 'NoSuch') statistics-release-2.1.2/inst/Machine_Learning/fitrnet.m000066400000000000000000000360171527311104300233210ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} fitrnet (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrnet (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrnet (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrnet (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrnet (@dots{}, @var{name}, @var{value}) ## ## Fit a neural network regression model. ## ## @code{@var{Mdl} = fitrnet (@var{X}, @var{Y})} returns a neural network ## regression model, @var{Mdl}, with @var{X} being the predictor data and ## @var{Y} the continuous response of the observations in @var{X}. ## ## @itemize ## @item ## @var{X} must be an @math{NxP} numeric matrix of predictor data, where rows ## correspond to observations and columns to features or variables. ## @item ## @var{Y} must be an @math{Nx1} numeric vector holding the response of the ## corresponding predictor data in @var{X}. @var{Y} must have the same number ## of rows as @var{X}. ## @end itemize ## ## The network is trained against the mean squared error and its output layer ## applies the identity, so a prediction is an unrestricted real number. Use ## @code{fitcnet} where the response names a class rather than a quantity. ## ## @code{@var{Mdl} = fitrnet (@dots{}, @var{name}, @var{value})} returns a ## neural network regression model with additional options specified by ## @qcode{Name-Value} pair arguments listed below. ## ## @subheading Model Parameters ## ## @multitable @columnfractions 0.32 0.68 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Standardize'} @tab A logical scalar indicating whether the ## data in @var{X} should be centred and scaled before training. The same ## transformation is applied by @code{predict}. The default is @qcode{false}. ## ## @item @qcode{'CategoricalPredictors'} @tab The predictors whose values are ## levels, as indices, as a logical vector with one element per predictor, or as ## @qcode{'all'}. Each is dummy coded in its place, one column of zeros and ## ones per level seen in training, named as in @qcode{'x1 == 2'} in ## @code{ExpandedPredictorNames}, and the coded columns are not standardized. ## An observation holding a level the training data did not has no prediction. ## A predictor may be named rather than indexed, as a character matrix of one ## padded name per row, a string array or a cellstr; a name must match an entry ## of @qcode{'PredictorNames'} exactly, its case included. ## ## @item @qcode{'PredictorNames'} @tab A cell array of character vectors ## specifying the predictor variable names, in the order they appear in ## @var{X}. ## ## @item @qcode{'ResponseName'} @tab A character vector specifying the name of ## the response variable. The default is @qcode{'Y'}. ## ## @item @qcode{'ResponseTransform'} @tab A character vector naming one of ## @qcode{'none'}, @qcode{'identity'}, @qcode{'exp'} or @qcode{'log'}, or a ## function handle of one argument, applied to the predicted response by ## @code{predict} and @code{resubPredict}. The default is @qcode{'none'}. ## ## @item @qcode{'LayerSizes'} @tab A vector of positive integers defining the ## number of units in each fully connected hidden layer. The default value is ## 10, a single hidden layer of ten units. ## ## @item @qcode{'LearningRate'} @tab A positive scalar value that defines the ## learning rate during the gradient descent. Default value is 0.003. A ## larger rate can drive every unit of a hidden layer negative, after which a ## rectifier passes no gradient and the network stops training. ## Applies only when @qcode{'Solver'} is @qcode{'sgd'}. ## ## @item @qcode{'Solver'} @tab A character vector naming the solver that ## trains the network, either @qcode{'lbfgs'} or @qcode{'sgd'}. The ## default is @qcode{'lbfgs'}, which minimizes the loss over the whole ## training set at once by limited-memory BFGS, as MATLAB does. It takes ## no learning rate, stops on the three tolerances below, and reaches a ## lower training loss in fewer passes over the data, though each of its ## iterations costs several passes where an epoch costs one. ## @qcode{'sgd'} visits the samples one at a time and steps down the ## gradient of each, running for @qcode{'IterationLimit'} epochs; it was ## the default before version 1.9.0. ## ## @item @qcode{'GradientTolerance'} @tab A nonnegative scalar. Training ## stops once the gradient's infinity norm falls to or below it, which is ## the quantity MATLAB tests too. The default is @qcode{1e-6}. Applies ## only when @qcode{'Solver'} is @qcode{'lbfgs'}. ## ## @item @qcode{'StepTolerance'} @tab A nonnegative scalar. Training ## stops once the step's infinity norm falls to or below it, which is the ## quantity MATLAB tests too. The default is @qcode{1e-6}. Applies only ## when @qcode{'Solver'} is @qcode{'lbfgs'}. ## ## @item @qcode{'LossTolerance'} @tab A real scalar. Training stops once ## the training loss falls to or below it. The test is on the loss ## itself and not on its change, matching MATLAB; pass @code{-Inf} to ## switch it off. The default is @qcode{1e-6}. Applies only when ## @qcode{'Solver'} is @qcode{'lbfgs'}. ## ## @item @qcode{'Activations'} @tab A character vector or a cellstr vector ## specifying the activation functions for the hidden layers of the neural ## network, excluding the output layer. The available activation functions ## are @qcode{'linear'}, @qcode{'none'}, @qcode{'sigmoid'}, @qcode{'relu'}, ## @qcode{'tanh'}, @qcode{'lrelu'}, @qcode{'prelu'}, @qcode{'elu'} and ## @qcode{'gelu'}. The default value is @qcode{'relu'}. ## ## @item @qcode{'OutputLayerActivation'} @tab A character vector specifying ## the activation function for the output layer. The available functions are ## the same as for @qcode{'Activations'}. The default value is ## @qcode{'none'}, the identity, which is what a regression output calls for; ## anything else bounds the prediction to that function's range. ## ## @item @qcode{'IterationLimit'} @tab A positive integer scalar specifying ## the maximum number of training iterations. The default value is 1000. ## Under @qcode{'sgd'} this counts epochs, under ## @qcode{'lbfgs'} solver iterations. ## ## @item @qcode{'DisplayInfo'} @tab A logical scalar indicating whether to ## print information during training. Default is @qcode{false}. ## ## @item @qcode{'Weights'} @tab A nonnegative single or double vector of ## observation weights, one per row of @var{X}. The network is trained on the ## loss weighted by them, standardization and the prediction for a row missing ## a predictor weigh the observations the same way, and a row of zero or ## missing weight is left out. The model's @code{W} keeps the class of the ## weights, while every computation runs in double, so the predictions are ## double where MATLAB returns single. The default is uniform. ## @end multitable ## ## ## The weights of each layer are drawn from a uniform range whose half-width ## is set by that layer's activation, and the scheme cannot be chosen: a ## rectifying activation (@qcode{'relu'}, @qcode{'lrelu'}, @qcode{'prelu'}, ## @qcode{'elu'}, @qcode{'gelu'}) takes the He range ## @math{sqrt (6 / fan_in)}, because it passes only half of its input, and ## the remaining activations take the Glorot range ## @math{sqrt (6 / (fan_in + fan_out))}, which accounts for the backward pass ## as well. A network whose layers do not share an activation is therefore ## built with both schemes. What each layer was given is reported by the ## @qcode{LayerWeightsInitializers} field of the fitted model's ## @qcode{ModelParameters}. ## ## @seealso{RegressionNeuralNetwork, fitcnet, fcnntrain, fcnnpredict} ## @end deftypefn function obj = fitrnet (X, Y, varargin) ## Check input parameters if (nargin < 2) error ("fitrnet: too few arguments."); endif if (mod (nargin, 2) != 0) error ("fitrnet: Name-Value arguments must be in pairs."); endif ## Check predictor data and response have equal rows if (! istable (X) && rows (X) != rows (Y)) error ("fitrnet: number of rows in X and Y must be equal."); endif ## Parse arguments to classdef constructor obj = RegressionNeuralNetwork (X, Y, varargin{:}); endfunction %!demo %! ## 1. Predict fuel economy from engine power and weight %! %! load carsmall %! X = [Horsepower, Weight]; %! Mdl = fitrnet (X, MPG, 'Standardize', true, 'IterationLimit', 500); %! %! ## Rows carrying a missing value were dropped, so ask about the ones used %! used = Mdl.RowsUsed; %! yFit = predict (Mdl, X(used,:)); %! plot (MPG(used), yFit, 'o', [5, 45], [5, 45], 'k-'); %! axis equal; %! xlabel ('Observed MPG'); %! ylabel ('Predicted MPG'); %! title (sprintf ('Neural network fit, RMSE %.2f', sqrt (resubLoss (Mdl)))); %!demo %! ## 2. Watching the fit converge %! %! load carsmall %! Mdl = fitrnet ([Horsepower, Weight], MPG, 'Standardize', true, ... %! 'IterationLimit', 400); %! %! ## TrainingHistory records the mean squared error at every iteration %! h = Mdl.TrainingHistory; %! semilogy (h.Iteration, h.TrainingLoss, 'linewidth', 1.5); %! xlabel ('Iteration'); %! ylabel ('Training MSE'); %! title ('The loss recorded is the network''s own, not a running average'); %!demo %! ## 3. Standardizing matters when the predictors differ in scale %! %! ## Horsepower runs to a few hundred and Weight to a few thousand, so the %! ## heavier column dominates the first layer until both are put on one scale. %! load carsmall %! X = [Horsepower, Weight]; %! raw = fitrnet (X, MPG, 'IterationLimit', 400); %! std_ = fitrnet (X, MPG, 'Standardize', true, 'IterationLimit', 400); %! printf ('RMSE, raw predictors : %.2f\n', sqrt (resubLoss (raw))); %! printf ('RMSE, standardized : %.2f\n', sqrt (resubLoss (std_))); %!demo %! ## 4. A network recovers a curve a straight line cannot %! %! rng (42); %! x = linspace (-3, 3, 120)'; %! y = sin (x) + randn (120, 1) * 0.1; %! Mdl = fitrnet (x, y, 'LayerSizes', [16, 16], 'IterationLimit', 800); %! plot (x, y, 'o', 'markersize', 4); %! hold on; %! plot (x, predict (Mdl, x), 'r-', 'linewidth', 2); %! plot (x, [ones(120,1), x] * ([ones(120,1), x] \ y), 'k--', 'linewidth', 1.5); %! hold off; %! legend ({'data', 'neural network', 'least squares line'}); %! title ('Two hidden layers of sixteen units'); ## Test constructor %!demo %! ## Fit from a table, and predict on one %! %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! %! ## A model formula names the response and the predictors together, and %! ## holds main effects only %! Mdl = fitrnet (T, 'SL ~ PL + Wide'); %! Mdl.PredictorNames %! Mdl.ResponseName %! Mdl.CategoricalPredictors %! %! ## predict matches the table's variables by name, so a column the model %! ## was not fitted on is passed over %! yFit = predict (Mdl, T(1:5,:)); %! yFit' %!test %! rand ('seed', 42); %! X = linspace (-1, 1, 40)'; %! Y = 2 * X + 0.5; %! Mdl = fitrnet (X, Y, 'IterationLimit', 50); %! assert_equal (class (Mdl), 'RegressionNeuralNetwork'); %! assert_equal (numel (Mdl.LayerWeights), 2); %! assert_equal (size (Mdl.LayerWeights{1}), [10, 1]); %! assert_equal (size (Mdl.LayerBiases{1}), [10, 1]); %! assert_equal (size (Mdl.LayerWeights{2}), [1, 10]); %! assert_equal (size (Mdl.LayerBiases{2}), [1, 1]); ## The driver passes its Name-Value pairs straight through %!test %! rand ('seed', 42); %! X = linspace (0, 1, 30)'; %! Mdl = fitrnet (X, 3 * X, 'LayerSizes', [5, 5], 'Solver', 'sgd', ... %! 'LearningRate', 0.01, 'IterationLimit', 40, ... %! 'ResponseName', 'speed'); %! assert_equal (Mdl.LayerSizes, [5, 5]); %! assert_equal (Mdl.LearningRate, 0.01); %! assert_equal (Mdl.IterationLimit, 40); %! assert_equal (Mdl.ResponseName, 'speed'); ## A model on real data with missing values trains on the rows that remain %!test %! rand ('seed', 42); %! load carsmall %! X = [Horsepower, Weight]; %! Mdl = fitrnet (X, MPG, 'Standardize', true, 'IterationLimit', 200); %! keep = ! isnan (MPG); %! assert_equal (Mdl.NumObservations, sum (keep)); %! assert_equal (Mdl.RowsUsed, keep); %! assert_equal (numel (resubPredict (Mdl)), sum (keep)); ## Test input validation %!error fitrnet () %!error fitrnet (ones (4, 1)) %!error ... %! fitrnet (ones (4, 2), ones (4, 1), 'LayerSizes') %!error ... %! fitrnet (ones (4, 2), ones (3, 1)) %!error ... %! fitrnet (ones (4, 2), ones (3, 1), 'LayerSizes', 2) ## Table input %!shared frnT %! load fisheriris %! frnT = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! frnT.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %!test # the response is named by a column and the rest are predictors %! Mdl = fitrnet (frnT, 'SL'); %! assert_equal (Mdl.PredictorNames, {'SW', 'PL', 'PW', 'Wide'}); %! assert_equal (Mdl.ResponseName, 'SL'); %! assert_equal (Mdl.CategoricalPredictors, 4); %!test # a model formula names the response and the predictors together %! Mdl = fitrnet (frnT, 'SL ~ PL + Wide'); %! assert_equal (Mdl.PredictorNames, {'PL', 'Wide'}); %! assert_equal (Mdl.CategoricalPredictors, 2); %!test # the response may be given beside a table of predictors %! Mdl = fitrnet (frnT(:,1:3), frnT.SL); %! assert_equal (Mdl.PredictorNames, {'SW', 'PL', 'PW'}); %! assert_equal (Mdl.ResponseName, 'Y'); %!test # predict takes a table, matched by name and not by position %! Mdl = fitrnet (frnT, 'SL'); %! a = predict (Mdl, frnT); %! assert_equal (numel (a), 150); %! assert_equal (predict (Mdl, frnT(:, [5, 4, 3, 2, 1])), a); %!test # the levels travel with the model when it is made compact %! Mdl = fitrnet (frnT, 'SL'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.PredictorLevels, Mdl.PredictorLevels); %! assert_equal (predict (CMdl, frnT), predict (Mdl, frnT)); %!error ... %! fitrnet (frnT, 'NoSuch') %!error ... %! fitrnet (frnT, 'SL ~ PL*PW') %!error ... %! predict (fitrnet (frnT, 'SL'), frnT(:, [1, 3, 4, 5])) statistics-release-2.1.2/inst/Machine_Learning/fitrsvm.m000066400000000000000000000315021527311104300233320ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} fitrsvm (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrsvm (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrsvm (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrsvm (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrsvm (@dots{}, @var{name}, @var{value}) ## ## Fit a support vector machine regression model. ## ## @code{@var{Mdl} = fitrsvm (@var{X}, @var{Y})} returns a support vector ## regression model, @var{Mdl}, with @var{X} being the predictor data and ## @var{Y} the continuous response of the observations in @var{X}. ## ## @itemize ## @item ## @var{X} must be an @math{NxP} numeric matrix of predictor data, where rows ## correspond to observations and columns to features or variables. ## @item ## @var{Y} must be an @math{Nx1} numeric vector holding the response of the ## corresponding predictor data in @var{X}. @var{Y} must have the same number ## of rows as @var{X}. ## @end itemize ## ## The model is fitted by @math{epsilon}-insensitive regression: an error ## smaller than @qcode{Epsilon} costs nothing, so only the observations ## outside that tube become support vectors. Use @code{fitcsvm} where the ## response names a class rather than a quantity. ## ## @code{@var{Mdl} = fitrsvm (@dots{}, @var{name}, @var{value})} returns a ## model with additional options specified by @qcode{Name-Value} pair ## arguments listed below. ## ## @subheading Model Parameters ## ## @multitable @columnfractions 0.32 0.68 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Standardize'} @tab A logical scalar indicating whether the ## data in @var{X} should be centred and scaled before training. The same ## transformation is applied by @code{predict}. The default is @qcode{false}. ## ## @item @qcode{'CategoricalPredictors'} @tab The predictors whose values are ## levels, as indices, as a logical vector with one element per predictor, or as ## @qcode{'all'}. Each is dummy coded in its place, one column of zeros and ## ones per level seen in training, named as in @qcode{'x1 == 2'} in ## @code{ExpandedPredictorNames}, and the coded columns are not standardized. ## An observation holding a level the training data did not has no prediction. ## A predictor may be named rather than indexed, as a character matrix of one ## padded name per row, a string array or a cellstr; a name must match an entry ## of @qcode{'PredictorNames'} exactly, its case included. ## ## @item @qcode{'PredictorNames'} @tab A cell array of character vectors ## specifying the predictor variable names, in the order they appear in ## @var{X}. ## ## @item @qcode{'ResponseName'} @tab A character vector specifying the name of ## the response variable. The default is @qcode{'Y'}. ## ## @item @qcode{'ResponseTransform'} @tab A character vector naming one of ## @qcode{'none'}, @qcode{'identity'}, @qcode{'exp'} or @qcode{'log'}, or a ## function handle of one argument, applied to the predicted response. The ## default is @qcode{'none'}. ## ## @item @qcode{'Epsilon'} @tab A non-negative scalar, the half-width of the ## insensitive tube. The default is @code{iqr (@var{Y}) / 13.49}, a robust ## estimate of a tenth of the response's standard deviation, which is what ## MATLAB uses; where that is zero it falls back to @math{0.1}. ## ## @item @qcode{'BoxConstraint'} @tab A positive scalar bounding the dual ## coefficients, the cost of an error outside the tube. The default is ## @code{iqr (@var{Y}) / 1.349} for a Gaussian kernel, or 1 where that is ## zero, and 1 for any other kernel. ## ## @item @qcode{'Weights'} @tab A nonnegative single or double vector of ## observation weights, one per row of @var{X}. An observation's box ## constraint is @math{n} times @qcode{BoxConstraint} times its weight, the ## weights scaled to sum to one; standardization uses weighted means and ## standard deviations, and a row of zero or missing weight is left out. The ## model's @code{W} keeps the class of the weights, while every computation ## runs in double. The default is uniform. ## ## @item @qcode{'KernelFunction'} @tab A character vector naming the kernel, ## one of @qcode{'linear'}, the default, @qcode{'rbf'}, @qcode{'gaussian'}, ## @qcode{'polynomial'} or @qcode{'sigmoid'}. ## ## @item @qcode{'PolynomialOrder'} @tab A positive integer, the order of the ## polynomial kernel. The default is 3. It is ignored by every other kernel. ## ## @item @qcode{'KernelScale'} @tab A positive scalar dividing every predictor ## before any kernel is applied, as MATLAB does, so that with @math{u} and ## @math{v} the divided predictors the kernels are @math{u'v}, ## @math{exp (-||u - v||^2)}, @math{(1 + u'v)^q} and @math{tanh (u'v + c)}, ## @math{c} being @qcode{'KernelOffset'}. The default is 1. ## ## @item @qcode{'KernelOffset'} @tab A non-negative scalar, the constant ## @math{c} of the sigmoid kernel, which MATLAB does not have. MATLAB adds ## it to every element of the Gram matrix, which leaves the fitted model ## unchanged, so it changes no other kernel here. The default is 0. ## ## @item @qcode{'SVMtype'} @tab A character vector selecting the formulation, ## either @qcode{'eps_svr'}, the default, or @qcode{'nu_svr'}. MATLAB fits ## only the @math{epsilon} form; @qcode{'nu_svr'} is an Octave extension. ## ## @item @qcode{'Nu'} @tab A scalar in @math{(0, 1]} used by ## @qcode{'nu_svr'}, bounding the fraction of support vectors. The default ## is 0.5. ## ## @item @qcode{'CacheSize'} @tab A positive scalar, the kernel cache in ## megabytes. The default is 1000. ## ## @item @qcode{'Tolerance'} @tab A non-negative scalar, the tolerance of the ## termination criterion. The default is @math{1e-6}. ## ## @item @qcode{'Shrinking'} @tab Either 0 or 1, whether to use the shrinking ## heuristic. The default is 1. ## @end multitable ## ## @seealso{RegressionSVM, fitcsvm, fitrnet, svmtrain, svmpredict} ## @end deftypefn function obj = fitrsvm (X, Y, varargin) ## Check input parameters if (nargin < 2) error ("fitrsvm: too few arguments."); endif if (mod (nargin, 2) != 0) error ("fitrsvm: Name-Value arguments must be in pairs."); endif ## Check predictor data and response have equal rows if (! istable (X) && rows (X) != rows (Y)) error ("fitrsvm: number of rows in X and Y must be equal."); endif ## Parse arguments to classdef constructor obj = RegressionSVM (X, Y, varargin{:}); endfunction %!demo %! ## 1. Predict fuel economy from engine power and weight %! %! load carsmall %! X = [Horsepower, Weight]; %! Mdl = fitrsvm (X, MPG, 'Standardize', true); %! %! ## Rows carrying a missing value were dropped, so ask about the ones used %! used = Mdl.RowsUsed; %! yFit = predict (Mdl, X(used,:)); %! plot (MPG(used), yFit, 'o', [5, 45], [5, 45], 'k-'); %! axis equal; %! xlabel ('Observed MPG'); %! ylabel ('Predicted MPG'); %! title (sprintf ('Linear SVR, RMSE %.2f', sqrt (resubLoss (Mdl)))); %!demo %! ## 2. The insensitive tube decides who becomes a support vector %! %! ## Errors smaller than Epsilon cost nothing, so a wider tube is fitted by %! ## fewer observations and a narrower one by almost all of them. %! load carsmall %! X = [Horsepower, Weight]; %! eps_ = [0.1, 0.5, 1, 2, 4, 8]; %! nsv = zeros (size (eps_)); %! for k = 1:numel (eps_) %! m = fitrsvm (X, MPG, 'Standardize', true, 'Epsilon', eps_(k)); %! nsv(k) = sum (m.IsSupportVector); %! endfor %! plot (eps_, nsv, 'o-', 'linewidth', 1.5); %! xlabel ('Epsilon'); %! ylabel ('Number of support vectors'); %! title ('A wider tube needs fewer support vectors'); %!demo %! ## 3. A radial kernel fits a curve a linear one cannot %! %! rng (42); %! x = linspace (-3, 3, 120)'; %! y = sin (x) + randn (120, 1) * 0.1; %! lin = fitrsvm (x, y); %! rbf = fitrsvm (x, y, 'KernelFunction', 'rbf', 'BoxConstraint', 10); %! plot (x, y, 'o', 'markersize', 4); %! hold on; %! plot (x, predict (lin, x), 'k--', 'linewidth', 1.5); %! plot (x, predict (rbf, x), 'r-', 'linewidth', 2); %! hold off; %! legend ({'data', 'linear kernel', 'rbf kernel'}); %! title ('Support vector regression'); %!demo %! ## 4. With a linear kernel the model is a plain linear function %! %! load carsmall %! X = [Horsepower, Weight]; %! Mdl = fitrsvm (X, MPG, 'Standardize', true); %! used = Mdl.RowsUsed; %! Xs = (X(used,:) - Mdl.Mu) ./ Mdl.Sigma; %! printf ('max |X*Beta + Bias - predict| = %g\n', ... %! max (abs (Xs * Mdl.Beta + Mdl.Bias - resubPredict (Mdl)))); ## Test constructor %!demo %! ## Fit from a table, and predict on one %! %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! %! ## A model formula names the response and the predictors together, and %! ## holds main effects only %! Mdl = fitrsvm (T, 'SL ~ PL + Wide'); %! Mdl.PredictorNames %! Mdl.ResponseName %! Mdl.CategoricalPredictors %! %! ## predict matches the table's variables by name, so a column the model %! ## was not fitted on is passed over %! yFit = predict (Mdl, T(1:5,:)); %! yFit' %!test %! load carsmall %! X = [Horsepower, Weight]; %! Mdl = fitrsvm (X, MPG, 'Standardize', true); %! assert_equal (class (Mdl), 'RegressionSVM'); %! assert_equal (Mdl.NumPredictors, 2); %! assert_equal (Mdl.ModelParameters.KernelFunction, 'linear'); %! assert_equal (Mdl.ModelParameters.SVMtype, 'eps_svr'); %! assert_equal (Mdl.Epsilon, iqr (MPG(Mdl.RowsUsed)) / 13.49, 1e-12); ## The driver passes its Name-Value pairs straight through %!test %! X = [linspace(0, 1, 30)', linspace(1, 2, 30)']; %! Y = 3 * X(:,1) + 1; %! Mdl = fitrsvm (X, Y, 'KernelFunction', 'rbf', 'BoxConstraint', 5, ... %! 'Epsilon', 0.05, 'ResponseName', 'speed'); %! assert_equal (Mdl.ModelParameters.KernelFunction, 'rbf'); %! assert_equal (Mdl.ModelParameters.BoxConstraint, 5); %! assert_equal (Mdl.Epsilon, 0.05); %! assert_equal (Mdl.ResponseName, 'speed'); %! assert_equal (isempty (Mdl.Beta), true); ## Test input validation %!error fitrsvm () %!error fitrsvm (ones (4, 1)) %!error ... %! fitrsvm (ones (4, 2), ones (4, 1), 'KernelFunction') %!error ... %! fitrsvm (ones (4, 2), ones (3, 1)) %!error ... %! fitrsvm (ones (4, 2), ones (3, 1), 'Epsilon', 1) ## Table input %!shared frsT %! load fisheriris %! frsT = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! frsT.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %!test # the response is named by a column and the rest are predictors %! Mdl = fitrsvm (frsT, 'SL'); %! assert_equal (Mdl.PredictorNames, {'SW', 'PL', 'PW', 'Wide'}); %! assert_equal (Mdl.ResponseName, 'SL'); %! assert_equal (Mdl.CategoricalPredictors, 4); %!test # a model formula names the response and the predictors together %! Mdl = fitrsvm (frsT, 'SL ~ PL + Wide'); %! assert_equal (Mdl.PredictorNames, {'PL', 'Wide'}); %! assert_equal (Mdl.CategoricalPredictors, 2); %!test # the response may be given beside a table of predictors %! Mdl = fitrsvm (frsT(:,1:3), frsT.SL); %! assert_equal (Mdl.PredictorNames, {'SW', 'PL', 'PW'}); %! assert_equal (Mdl.ResponseName, 'Y'); %!test # predict takes a table, matched by name and not by position %! Mdl = fitrsvm (frsT, 'SL'); %! a = predict (Mdl, frsT); %! assert_equal (numel (a), 150); %! assert_equal (predict (Mdl, frsT(:, [5, 4, 3, 2, 1])), a); %!test # the levels travel with the model when it is made compact %! Mdl = fitrsvm (frsT, 'SL'); %! CMdl = compact (Mdl); %! assert_equal (CMdl.PredictorLevels, Mdl.PredictorLevels); %! assert_equal (predict (CMdl, frsT), predict (Mdl, frsT)); %!error ... %! fitrsvm (frsT, 'NoSuch') %!error ... %! fitrsvm (frsT, 'SL ~ PL*PW') %!error ... %! predict (fitrsvm (frsT, 'SL'), frsT(:, [1, 3, 4, 5])) statistics-release-2.1.2/inst/Machine_Learning/fitrtree.m000066400000000000000000000443151527311104300234720ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{Mdl} =} fitrtree (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrtree (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrtree (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrtree (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{Mdl} =} fitrtree (@dots{}, @var{name}, @var{value}) ## ## Fit a binary decision tree for regression. ## ## @code{@var{Mdl} = fitrtree (@var{X}, @var{Y})} grows a binary decision tree ## on the predictor data @var{X} and the response @var{Y}, and returns it as a ## @code{RegressionTree} object. ## ## @itemize ## @item ## @var{X} must be a @math{NxP} numeric matrix of predictor data, where rows ## correspond to observations and columns to predictors. ## @item ## @var{Y} must be a @math{Nx1} numeric vector holding the response of each ## observation in @var{X}. ## @end itemize ## ## @code{@var{Mdl} = fitrtree (@var{Tbl}, @var{ResponseVarName})} takes both ## from the table @var{Tbl}: @var{ResponseVarName} names the variable holding ## the response and every other variable is a predictor. ## @code{@var{Mdl} = fitrtree (@var{Tbl}, @var{formula})} names them with a ## model formula instead, @qcode{'Y ~ x1 + x2'}, which holds main effects ## only and so takes no product, power or wildcard. ## @code{@var{Mdl} = fitrtree (@var{Tbl}, @var{Y})} takes the predictors from ## the table and the response from @var{Y}. ## ## A table variable holding levels rather than numbers, which is a ## @code{categorical}, a @code{logical}, a character array or a cell array of ## character vectors, is a categorical predictor without being named one, and ## @qcode{'CategoricalPredictors'} adds to that set rather than replacing it. ## @code{PredictorNames} and @code{ResponseName} come from the table, and a ## level is coded at prediction as it was coded at fitting, so a table given ## to @code{predict} may hold only some of the levels. ## ## An observation whose response is missing, which is missing every ## predictor, or whose weight is zero is dropped, and the rows kept are ## reported in @code{RowsUsed}. An observation missing only some of its ## predictors is kept: it descends the tree as far as the predictors it does ## carry allow and is answered there. ## ## @code{@var{Mdl} = fitrtree (@dots{}, @var{name}, @var{value})} takes the ## options below. ## ## @multitable @columnfractions 0.24 0.74 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'CategoricalPredictors'} @tab The predictors whose values are ## levels, as indices, as a logical vector with one element per predictor, or ## as @qcode{'all'}. Such a predictor is split into two sets of levels, the ## lower mean response on the left, and an observation whose level a node did ## not see stops there. MATLAB does not always keep that side, and equally ## good splits may be chosen differently. ## A predictor may be named rather than indexed, as a character matrix of one ## padded name per row, a string array or a cellstr; a name must match an entry ## of @qcode{'PredictorNames'} exactly, its case included. ## ## @item @qcode{'MaxNumCategories'} @tab A nonnegative integer, recorded in ## @code{ModelParameters}. The default is 10. Ordering the levels by their ## mean response finds the best split whatever the number of levels. ## ## @item @qcode{'MaxNumSplits'} @tab A nonnegative integer, the largest number ## of branch nodes the tree may take. The default is one less than the number ## of observations. ## ## @item @qcode{'MergeLeaves'} @tab @qcode{'on'} (default) or @qcode{'off'}. ## When on, a pair of leaves whose parent is no worse than the two of them ## together is merged back into that parent. ## ## @item @qcode{'MinLeafSize'} @tab A positive integer, the fewest ## observations a leaf may hold. The default is 1. ## ## @item @qcode{'MinParentSize'} @tab A positive integer, the fewest ## observations a node must hold to be split. The default is 10. The value ## the fit uses is @code{max (MinParentSize, 2 * MinLeafSize)}. ## ## @item @qcode{'NumVariablesToSample'} @tab A positive integer, the number ## of predictors each split is chosen from, drawn afresh at every node, or ## @qcode{'all'} (default). A number no smaller than the number of ## predictors samples them all and is reported as @qcode{'all'}. MATLAB also ## accepts a fractional number, which is refused here. ## ## @item @qcode{'PredictorNames'} @tab A cell array of character vectors ## naming the columns of @var{X}. ## ## @item @qcode{'Prune'} @tab @qcode{'on'} (default) or @qcode{'off'}. When ## on, the cost complexity pruning sequence is estimated and reported in ## @code{PruneList} and @code{PruneAlpha}. The tree returned is the unpruned ## one either way; @code{prune} takes a subtree out of the sequence. ## ## @item @qcode{'PruneCriterion'} @tab @qcode{'mse'}, the only criterion a ## regression tree has. ## ## @item @qcode{'QuadraticErrorTolerance'} @tab A positive scalar. A node ## whose squared error has fallen to this fraction of the root's is not split ## further. The default is 1e-6. ## ## @item @qcode{'ResponseName'} @tab A character vector naming the response. ## The default is @qcode{'Y'}. ## ## @item @qcode{'ResponseTransform'} @tab A character vector naming a ## transform to apply to the predicted response, or a function handle. The ## default is @qcode{'none'}. ## ## @item @qcode{'SplitCriterion'} @tab @qcode{'mse'}, the only criterion a ## regression tree has. ## ## @item @qcode{'Weights'} @tab A nonnegative single or double vector with one ## element per observation. The default is uniform. The model's @code{W} keeps ## the class of the weights, while every computation runs in double, so the ## predictions are double where MATLAB returns single. ## ## @end multitable ## ## Surrogate splits are not implemented, and an option asking for them is ## refused rather than quietly ignored. ## ## @seealso{RegressionTree, fitctree, treetrain, treepredict} ## @end deftypefn function Mdl = fitrtree (X, Y, varargin) ## Input validation if (nargin < 2) error ("fitrtree: too few arguments."); endif if (mod (nargin, 2) != 0) error ("fitrtree: name-value arguments must be in pairs."); endif if (! istable (X) && rows (X) != numel (Y)) error ("fitrtree: number of rows in X and Y must be equal."); endif ## Parse arguments to the class constructor Mdl = RegressionTree (X, Y, varargin{:}); endfunction ## Demo %!demo %! ## Grow a regression tree on the carsmall data and look at it %! %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = fitrtree (X, MPG, 'MinLeafSize', 15); %! %! ## The tree as text: a branch names its cut, a leaf names what it fits %! view (Mdl); %! %! ## How much each predictor contributed %! predictorImportance (Mdl) %! %! ## The mean squared error on the data it was fitted to %! resubLoss (Mdl) %!demo %! ## Prune a tree back and watch the error rise as it gets smaller %! %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = fitrtree (X, MPG, 'MinLeafSize', 15); %! %! levels = 0:numel (Mdl.PruneAlpha) - 1; %! leaves = zeros (size (levels)); %! err = zeros (size (levels)); %! for ii = 1:numel (levels) %! sub = prune (Mdl, 'Level', levels(ii)); %! leaves(ii) = sum (! sub.IsBranchNode); %! err(ii) = resubLoss (sub); %! endfor %! [leaves(:), err(:)] ## Tests %!demo %! ## Fit from a table, and predict on one %! %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! %! ## A column holding levels is a categorical predictor without being named %! ## one %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! %! ## The response is named by its column, and everything else is a predictor %! Mdl = fitrtree (T, 'SL'); %! Mdl.PredictorNames %! Mdl.ResponseName %! Mdl.CategoricalPredictors %! %! ## A model formula names them instead %! Mdl2 = fitrtree (T, 'SL ~ PL + Wide'); %! Mdl2.PredictorNames %! %! ## predict matches the table's variables by name, so their order does not %! ## matter and a column the model was not fitted on is passed over %! yFit = predict (Mdl, T(1:5, [5, 3, 2, 1, 4])); %! yFit' %!test # MATLAB parity: the tree a default fit grows on carsmall %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = fitrtree (X, MPG); %! assert_equal (class (Mdl), 'RegressionTree'); %! assert_equal (Mdl.NumNodes, 37); %! assert_equal (Mdl.NumObservations, 94); %! assert_equal (sum (Mdl.RowsUsed), 94); %! assert_equal (Mdl.NodeSize(1:9)', [94, 58, 36, 40, 17, 8, 28, 18, 22]); %! assert_equal (Mdl.CutPredictorIndex(1:5)', [1, 3, 3, 1, 2]); %! assert_equal (Mdl.CutPoint(1:5)', [3085.5, 89, 115, 2162, 5], 1e-12); %!test # MATLAB parity: the node statistics of the carsmall tree %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = fitrtree (X, MPG); %! assert_equal (Mdl.NodeMean(1:5)', [23.7181, 28.7931, 15.5417, 30.9375, ... %! 24.0882], 1e-4); %! assert_equal (Mdl.NodeError(1:5)', [63.8859, 30.4400, 9.4219, 24.9648, ... %! 10.1834], 1e-4); %! assert_equal (Mdl.NodeProbability(1:3)', [94, 58, 36] / 94, 1e-14); %! assert_equal (Mdl.NodeRisk, Mdl.NodeProbability .* Mdl.NodeError, 1e-14); %!test # MATLAB parity: predict, its two outputs and the loss %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = fitrtree (X, MPG); %! [yFit, node] = predict (Mdl, X([1, 20, 60], :)); %! assert_equal (node', [20, 13, 36]); %! assert_equal (yFit', [17.25, 12.3333333333333, 29.1], 1e-12); %! assert_equal (resubLoss (Mdl), 5.5828069902791, 1e-12); %! assert_equal (loss (Mdl, X, MPG), 5.5828069902791, 1e-12); %!test # MATLAB parity: the pruning sequence of the carsmall tree %! ## One row of carsmall has no horsepower, and node 2 cuts on horsepower, %! ## so that row stops there and belongs to neither child. Its share of %! ## the node's error is part of the subtree's risk, which is what puts %! ## this alpha at 5.99 rather than 6.32. %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = fitrtree (X, MPG); %! assert_equal (Mdl.NodeSize(2) - sum (Mdl.NodeSize(Mdl.Children(2,:))), 1); %! assert_equal (Mdl.PruneList(1:5)', [17, 16, 14, 15, 13]); %! assert_equal (numel (Mdl.PruneAlpha), 18); %! assert_equal (Mdl.PruneAlpha(17), 5.99325416896717, 1e-12); %! assert_equal (Mdl.PruneAlpha(18), 41.4954735525515, 1e-11); %!test # MATLAB parity: predictor importance discounts what a node holds back %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = fitrtree (X, MPG); %! assert_equal (predictorImportance (Mdl), ... %! [2.5904, 0.1006, 0.5499], 1e-4); %! Sub = fitrtree (X, MPG, 'MinLeafSize', 15); %! assert_equal (predictorImportance (Sub), ... %! [11.253155105041, 0, 1.49831354224175], 1e-12); %!test # MATLAB parity: a smaller tree, its sequence and its text form %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = fitrtree (X, MPG, 'MinLeafSize', 15); %! assert_equal (Mdl.NumNodes, 9); %! assert_equal (Mdl.NodeSize', [94, 58, 36, 40, 17, 15, 21, 18, 22]); %! assert_equal (Mdl.PruneList', [4, 3, 1, 2, 0, 0, 0, 0, 0]); %! assert_equal (Mdl.PruneAlpha', [0, 1.56476063829787, 1.95238622931442, ... %! 5.99325416896717, 41.4954735525515], 1e-11); %!test # MATLAB parity: the growth options that stop the tree early %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! assert_equal (fitrtree (X, MPG, 'MinParentSize', 20).NumNodes, 15); %! assert_equal (fitrtree (X, MPG, 'MinLeafSize', 15).NumNodes, 9); %! assert_equal (fitrtree (X, MPG, 'MaxNumSplits', 3).NumNodes, 7); %! assert_equal (fitrtree (X, MPG, 'MergeLeaves', 'off').NumNodes, 37); %! assert_equal (fitrtree (X, MPG, ... %! 'QuadraticErrorTolerance', 0.01).NumNodes, 27); %!test # MATLAB parity: turning both reductions off leaves no sequence %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = fitrtree (X, MPG, 'MergeLeaves', 'off', 'Prune', 'off'); %! assert_equal (isempty (Mdl.PruneList), true); %! assert_equal (isempty (Mdl.PruneAlpha), true); %!test # MATLAB parity: the weights a weighted fit reports and weighs by %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = fitrtree (X, MPG, 'Weights', (1:100)'); %! assert_equal (sum (Mdl.W), 1, 1e-14); %! assert_equal (Mdl.W(1), 0.000201328769881216, 1e-15); %! assert_equal (Mdl.NumNodes, 37); %! assert_equal (Mdl.NodeSize(1:5)', [94, 56, 38, 27, 29]); %! assert_equal (Mdl.NodeMean(1:3)', [26.469398026978, 30.3411058363586, ... %! 16.4660894660895], 1e-12); %! assert_equal (resubLoss (Mdl), 6.51484752783409, 1e-12); %! assert_equal (loss (Mdl, Mdl.X, Mdl.Y), 7.44189886464653, 1e-12); %!test # MATLAB parity: the response transform reaches the prediction %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! Mdl = fitrtree (X, MPG, 'ResponseTransform', @(y) 2 * y); %! assert_equal (Mdl.ResponseTransform, '@(y) 2 * y'); %! assert_equal (predict (Mdl, X([1, 20, 60], :))', ... %! [34.5, 24.6666666666667, 58.2], 1e-12); %! assert_equal (resubLoss (Mdl), 633.531443696713, 1e-10); %!test # MATLAB parity: a number below the predictor count is kept %! load fisheriris %! Mdl = fitrtree (meas(:, 2:4), meas(:, 1), 'NumVariablesToSample', 2); %! assert_equal (Mdl.ModelParameters.NVarToSample, 2); %!test # MATLAB parity: a number covering every predictor is reported 'all' %! load fisheriris %! Mdl = fitrtree (meas(:, 2:4), meas(:, 1), 'NumVariablesToSample', 5); %! assert_equal (Mdl.ModelParameters.NVarToSample, 'all'); %!test # the generator's state reproduces a sampled tree %! load fisheriris %! rng (1); %! A = fitrtree (meas(:, 2:4), meas(:, 1), 'NumVariablesToSample', 1); %! rng (1); %! B = fitrtree (meas(:, 2:4), meas(:, 1), 'NumVariablesToSample', 1); %! assert_equal (A.CutPredictorIndex, B.CutPredictorIndex); %! assert_equal (isequaln (A.CutPoint, B.CutPoint), true); %!test # MATLAB parity: a split budget spent inside a layer keeps the best %! load fisheriris %! T = fitrtree (meas(:,2:4), meas(:,1), 'MaxNumSplits', 10, 'MinLeafSize', 5); %! assert_equal (T.CutPredictorIndex', [2, 2, 2, 1, 2, 2, 0, 2, 1, 0, 0, ... %! 1, 2, 0, 0, 0, 0, 0, 0, 0, 0]); %! assert_equal (T.NodeSize', [150, 73, 77, 53, 20, 68, 9, 20, 33, 8, 12, ... %! 43, 25, 11, 9, 20, 13, 33, 10, 15, 10]); %! assert_equal (T.CutPoint(13), 5.65, 1e-12); ## Test input validation %!error fitrtree () %!error fitrtree (ones (4, 1)) %!error %! fitrtree (ones (4, 2), ones (4, 1), 'K') %!error %! fitrtree (ones (4, 2), ones (3, 1)) %!error %! fitrtree (ones (4, 2), ones (3, 1), 'K', 2) %!test # MATLAB parity: a level absent from a node's rows stops there %! k = (0:95)'; %! xa = mod (k, 2); %! ca = mod (k, 4) + 1; %! ca(xa == 0 & ca == 4) = 3; %! y = 2 * xa + (ca >= 3) + 0.05 * cos (k); %! Mdl = fitrtree ([xa, ca], y, 'CategoricalPredictors', 2); %! assert_equal (Mdl.NumNodes, 7); %! assert_equal (Mdl.CutCategories(2,:), {1, 3}); %! [yhat, nd] = predict (Mdl, [0, 4; 1, 4; 0, 1]); %! assert_equal (nd', [2, 7, 4]); %! assert_equal (yhat', [0.50094374, 2.9994709, 0.00076071259], 1e-7); ## Table input %!shared frT %! load fisheriris %! frT = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! frT.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! frT.Flag = meas(:,1) > 5.8; %!test # the response is named by a column and the rest are predictors %! Mdl = fitrtree (frT, 'SL'); %! assert_equal (Mdl.PredictorNames, {'SW', 'PL', 'PW', 'Wide', 'Flag'}); %! assert_equal (Mdl.ResponseName, 'SL'); %! assert_equal (class (Mdl), 'RegressionTree'); ## MATLAB parity: a column holding levels is a categorical predictor without ## being named one, indexed into the predictors and not into the table %!test %! Mdl = fitrtree (frT, 'SL'); %! assert_equal (Mdl.CategoricalPredictors, [4, 5]); %!test # 'CategoricalPredictors' adds to what the table says, not replaces it %! Mdl = fitrtree (frT, 'SL', 'CategoricalPredictors', 2); %! assert_equal (Mdl.CategoricalPredictors, [2, 4, 5]); %!test # a model formula names the response and the predictors together %! Mdl = fitrtree (frT, 'SL ~ PL + Wide'); %! assert_equal (Mdl.PredictorNames, {'PL', 'Wide'}); %! assert_equal (Mdl.ResponseName, 'SL'); %! assert_equal (Mdl.CategoricalPredictors, 2); %!test # the response may be given beside a table of predictors %! Mdl = fitrtree (frT(:,1:3), frT.SL); %! assert_equal (Mdl.PredictorNames, {'SW', 'PL', 'PW'}); %! assert_equal (Mdl.ResponseName, 'Y'); %!test # a matrix is taken as it always was %! load fisheriris %! Mdl = fitrtree (meas(:,2:4), meas(:,1)); %! assert_equal (columns (Mdl.X), 3); %! assert_equal (isempty (Mdl.PredictorLevels), true); ## Table input validation %!error ... %! fitrtree (frT, 'NoSuch') %!error ... %! fitrtree (frT, 'SL ~ PL*PW') %!error ... %! fitrtree (frT, 'SL ~ nope') %!error ... %! fitrtree (frT, 'SL ~ PL', 'PredictorNames', {'PL'}) statistics-release-2.1.2/inst/Machine_Learning/fscchi2.m000066400000000000000000000563621527311104300231740ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{idx} =} fscchi2 (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{idx} =} fscchi2 (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{idx} =} fscchi2 (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{idx} =} fscchi2 (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{idx} =} fscchi2 (@dots{}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{idx}, @var{scores}] =} fscchi2 (@dots{}) ## ## Rank the predictors for classification with chi-square tests. ## ## @code{@var{idx} = fscchi2 (@var{X}, @var{Y})} tests each predictor, a ## column of the matrix @var{X}, for independence from the class labels ## @var{Y} with a chi-square test, and ranks the predictors by the result. ## @var{idx} is a row vector of column indices, from the most important ## predictor to the least; predictors of equal score keep their order. ## ## @var{X} is a real matrix of type double or single with one row per ## observation. @var{Y} holds one class label per row of @var{X}: a numeric ## or logical vector, a categorical vector, a cell array of character vectors, ## a string array, or a character matrix with one row per observation. ## ## @code{@var{idx} = fscchi2 (@var{Tbl}, @var{ResponseVarName})} takes the ## predictors and the class labels from the table @var{Tbl}: ## @var{ResponseVarName} names the variable holding the labels and every other ## variable is a predictor. @code{@var{idx} = fscchi2 (@var{Tbl}, ## @var{formula})} names them with a model formula instead, ## @qcode{'Y ~ x1 + x2'}, which holds main effects only, and ranks only the ## predictors it names, @var{idx} indexing them in the order the table holds ## them. @code{@var{idx} = fscchi2 (@var{Tbl}, @var{Y})} takes the predictors ## from the table and the labels from @var{Y}. A logical, categorical, text ## or string variable of the table is a categorical predictor; an ordinal ## categorical one is continuous, its categories in their order. ## ## @code{[@var{idx}, @var{scores}] = fscchi2 (@dots{})} also returns the score ## of each predictor, a row vector in the order of the predictors, so that ## @code{@var{scores}(@var{idx})} is in decreasing order. The score is ## @math{-log (p)}, @math{p} being the p-value of the predictor's test, so a ## larger score marks a more important predictor; it is @code{Inf} where ## @math{p} is 0. A predictor that cannot separate the classes, being ## constant or wholly missing, scores 0, and so does every predictor when the ## labels hold a single class. ## ## A continuous predictor is first binned. Its @math{n} known values are ## sorted, the upper edge of bin @math{k} is the value at position ## @code{ceil (@var{k} * @var{n} / @var{NumBins})}, and a value goes to the ## first bin whose edge it does not exceed. The bins so hold equal numbers of ## observations as nearly as the ties allow, the first ones taking any ## remainder, and equal values always share a bin, which may leave fewer bins ## than asked for. A categorical predictor takes its categories as they ## are. The test is Pearson's chi-square test of ## independence on the table of weighted counts of the bins or categories ## against the classes, the empty ones left out. ## ## Each predictor is tested on the observations it is known for. Their ## weights are scaled within each class so that the class carries its prior ## probability, and over all so that they sum to the number of those ## observations. The empirical prior, the default, is the weighted share of ## each class among those observations, which leaves the weights as given. An ## observation whose class label is missing is left out of every test. ## ## The following @qcode{Name-Value} pairs are supported: ## ## @multitable @columnfractions 0.3 0.7 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'CategoricalPredictors'} @tab The categorical predictors: a ## vector of their indices, a logical vector with one value per predictor, or ## @qcode{'all'}. With a table they are added to those the table makes ## categorical, and they may also be named. The default is none for a matrix. ## ## @item @qcode{'ClassNames'} @tab The classes to use, in the type of @var{Y}. ## Observations of other classes are left out, and the order of the names is ## the order of a @qcode{'Prior'} vector. The default is every class of ## @var{Y}, sorted. ## ## @item @qcode{'NumBins'} @tab The number of bins of a continuous predictor, a ## positive integer. The default is 10. ## ## @item @qcode{'Prior'} @tab The prior probabilities of the classes: ## @qcode{'empirical'}, the default; @qcode{'uniform'}; a vector with one ## nonnegative value per class, in the order of the classes; or a structure ## with the fields @qcode{ClassNames} and @qcode{ClassProbs}. The values are ## normalized to sum to 1. ## ## @item @qcode{'UseMissing'} @tab @qcode{false}, the default, to test each ## predictor on the observations it is known for, or @qcode{true} to test it ## on every observation, the missing values of a categorical predictor forming ## one more category and those of a continuous predictor one more bin. MATLAB ## R2024a and R2026b document this option but ignore it in @code{fscchi2}, ## scoring every predictor alike either way; Octave does what they document. ## ## @item @qcode{'Weights'} @tab The observation weights, a nonnegative finite ## vector of type double or single with one value per row, or with a table the ## name of one of its variables, which is then no predictor. The default is ## equal weights. ## @end multitable ## ## The option names are not case sensitive but must be given in full. With a ## matrix, the categorical predictors cannot be named, the predictors having no ## names, as in MATLAB. ## ## @seealso{fsrftest, relieff, sequentialfs, crosstab} ## @end deftypefn function [idx, scores] = fscchi2 (X, Y, varargin) if (nargin < 2) print_usage (); endif ## Every option is named; a table brings its own predictor names, which the ## call may not set optNames = {'CategoricalPredictors', 'ClassNames', 'NumBins', 'Prior', ... 'UseMissing', 'Weights'}; if (mod (numel (varargin), 2) != 0) error ("fscchi2: optional arguments must be Name-Value pairs."); endif for k = 1:2:numel (varargin) if (! (ischar (varargin{k}) && any (strcmpi (varargin{k}, optNames)))) if (ischar (varargin{k})) error ("fscchi2: unknown parameter name '%s'.", varargin{k}); endif error ("fscchi2: optional arguments must be Name-Value pairs."); endif endfor ## A table is resolved into the predictor matrix, its categorical columns ## and the response; an ordinal column is continuous, as in MATLAB catGiven = []; if (istable (X)) tnames = X.Properties.VariableNames; [X, Y, args, ~, errmsg] = tableFrame (X, Y, varargin, true); if (! isempty (errmsg)) error ("fscchi2: %s", errmsg); endif [pnames, ~, catGiven, args] = parsePairedArguments ( ... {'PredictorNames', 'ResponseName', ... 'CategoricalPredictors'}, {[], [], []}, args); ## The predictors are ranked in the order the table holds them, a ## formula naming only which, as in MATLAB [~, pos] = ismember (pnames, tnames); [~, order] = sort (pos); X = X(:,order); iscatT = false (1, numel (pnames)); iscatT(catGiven) = true; catGiven = find (iscatT(order)); else args = varargin; endif dfValues = {[], [], 10, 'empirical', false, []}; [CatPreds, ClassNames, NumBins, Prior, UseMissing, Weights] = ... parsePairedArguments (optNames, dfValues, args); ## Predictors and response if (! (isfloat (X) && isreal (X) && ismatrix (X) && ! isempty (X))) error (strcat ("fscchi2: X must be a nonempty real matrix of type", ... " double or single.")); endif X = double (X); [n, p] = size (X); if (isempty (Y) || rows (Y) != n || (! ischar (Y) && ! isvector (Y))) error ("fscchi2: X and Y must have the same number of observations."); endif ## Options if (! (isnumeric (NumBins) && isreal (NumBins) && isscalar (NumBins) && NumBins >= 1 && NumBins == fix (NumBins))) error ("fscchi2: 'NumBins' must be a positive integer."); endif if (! ((islogical (UseMissing) || isnumeric (UseMissing)) && isscalar (UseMissing) && any (UseMissing == [0, 1]))) error ("fscchi2: 'UseMissing' must be a logical scalar."); endif UseMissing = logical (UseMissing); if (isempty (Weights)) Weights = ones (n, 1); else errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("fscchi2: %s", errmsg); endif if (! (isvector (Weights) && numel (Weights) == n)) error ("fscchi2: 'Weights' must have one value per observation."); endif Weights = double (Weights(:)); if (! (all (isfinite (Weights)) && all (Weights >= 0) && sum (Weights) > 0)) error (strcat ("fscchi2: 'Weights' must be nonnegative, finite and", ... " not all zero.")); endif endif ## The categorical predictors: those a table gave and those the call named, ## the call naming them by index, by a logical vector or as 'all' iscat = false (1, p); iscat(catGiven) = true; if (! isempty (CatPreds)) if (ischar (CatPreds) && isrow (CatPreds) && strcmpi (CatPreds, 'all')) iscat(:) = true; elseif (islogical (CatPreds) && isvector (CatPreds)) if (numel (CatPreds) != p) error (strcat ("fscchi2: 'CategoricalPredictors' as a logical", ... " vector must have one value per predictor.")); endif iscat |= CatPreds(:)'; elseif (isnumeric (CatPreds) && isvector (CatPreds)) if (! (isreal (CatPreds) && all (CatPreds == fix (CatPreds)) && all (CatPreds >= 1) && all (CatPreds <= p))) error (strcat ("fscchi2: 'CategoricalPredictors' must index the", ... " predictors.")); endif iscat(CatPreds) = true; else error (strcat ("fscchi2: 'CategoricalPredictors' must be indices, a", ... " logical vector or 'all'; naming the predictors", ... " needs a table.")); endif endif ## The classes, sorted or in the order ClassNames gives them. A missing ## label leaves its observation out, and so does a class ClassNames omits. rowsKept = ! isnan (grp2idx (Y)); Yk = Y(rowsKept,:); if (! isempty (ClassNames)) [~, errmsg] = namedClasses (uniqueLabels (Yk), ClassNames); if (! isempty (errmsg)) error ("fscchi2: %s", errmsg); endif endif [C, gY] = classOrder (Yk, ClassNames); K = classCount (C); inC = gY > 0; used = find (rowsKept); used = used(inC); gY = gY(inC); X = X(used,:); w = Weights(used); ## The prior: empirical is taken per predictor, from the observations its ## test uses; any other is the same for every predictor if (isstruct (Prior)) Prior = priorFromStruct (Prior, C, 'fscchi2'); elseif (ischar (Prior) && isrow (Prior) && strcmpi (Prior, 'uniform')) Prior = ones (1, K) / K; elseif (ischar (Prior) && isrow (Prior) && strcmpi (Prior, 'empirical')) Prior = []; elseif (isnumeric (Prior) && isvector (Prior)) if (numel (Prior) != K) error ("fscchi2: 'Prior' must have one value per class."); endif if (! (isreal (Prior) && all (isfinite (Prior)) && all (Prior >= 0) && sum (Prior) > 0)) error (strcat ("fscchi2: 'Prior' must be nonnegative, finite and not", ... " all zero.")); endif Prior = double (Prior(:)') / sum (Prior); else error (strcat ("fscchi2: 'Prior' must be 'empirical', 'uniform', a", ... " numeric vector or a structure.")); endif ## One test per predictor scores = zeros (1, p); for j = 1:p x = X(:,j); known = ! isnan (x); if (UseMissing) inTest = true (rows (x), 1); else inTest = known; endif if (! any (known & inTest)) continue; endif ## Bins of a continuous predictor, bounded by the values at equal steps ## through the sorted values, a value going to the first bin whose upper ## edge it does not exceed; categories of a categorical one. The missing ## values, when they are used, are one more group. xk = x(known); if (iscat(j)) [~, ~, g] = unique (xk); else sx = sort (xk); edges = sx(ceil ((1:NumBins-1) * numel (xk) / NumBins)); g = 1 + sum (xk > edges(:)', 2); endif group = zeros (rows (x), 1); group(known) = g; group(! known) = max (g) + 1; ## The weights of the observations tested, each class carrying its prior ## and the whole summing to their number gt = gY(inTest); wt = w(inTest); if (! isempty (Prior)) for k = 1:K m = gt == k; if (any (m) && sum (wt(m)) > 0) wt(m) = Prior(k) * wt(m) / sum (wt(m)); endif endfor endif if (! (sum (wt) > 0)) continue; endif wt = wt / sum (wt) * numel (wt); ## Pearson's chi-square test on the table of weighted counts, the empty ## groups and classes left out O = accumarray ([group(inTest), gt], wt); O = O(any (O, 2), any (O, 1)); df = (rows (O) - 1) * (columns (O) - 1); if (df < 1) continue; endif E = sum (O, 2) * sum (O, 1) / sum (O(:)); stat = sum ((O(:) - E(:)) .^ 2 ./ E(:)); scores(j) = -log (chi2cdf (stat, df, 'upper')); endfor [~, idx] = sort (scores, 'descend'); endfunction ## Test output: the binning, measured on R2024a %!shared xb, c2, c3, w %! xb = ((1:40) .^ 2)'; %! c2 = [repmat({'p'}, 12, 1); repmat({'q'}, 10, 1); ... %! repmat({'p'}, 8, 1); repmat({'q'}, 10, 1)]; %! c3 = repmat ({'a'; 'b'; 'c'; 'a'; 'b'}, 8, 1); %! w = 1 + mod ((1:40)', 3); %!test # ten bins, the default %! [idx, s] = fscchi2 (xb, c2); %! assert_equal (idx, 1); %! assert_equal (s, 8.52155983968549, 1e-13); %!test # the remainder goes to the first bins %! [~, s] = fscchi2 (xb, c2, 'NumBins', 3); %! assert_equal (s, 5.8021978021978, 1e-13); %!test %! [~, s] = fscchi2 (xb, c2, 'NumBins', 7); %! assert_equal (s, 11.6724134015208, 1e-13); %!test # ties share a bin %! xt = [ones(15, 1); 2 * ones(10, 1); (3:17)']; %! [~, s] = fscchi2 (xt, c2, 'NumBins', 3); %! assert_equal (s, 4.75128205128205, 1e-13); %!test # few distinct values, kept apart by these edges %! xu = [repmat([1; 2; 3], 13, 1); 1]; %! [~, s] = fscchi2 (xu, c2); %! assert_equal (s, 0.219780219780219, 1e-13); %!test # equal values share a bin, here the two smallest %! [~, s] = fscchi2 (mod (xb, 7), c3, 'NumBins', 4); %! assert_equal (s, 0.0761861991737764, 1e-14); %!test # and so does naming the predictor categorical %! xu = [repmat([1; 2; 3], 13, 1); 1]; %! [~, s] = fscchi2 (xu, c2, 'CategoricalPredictors', 1); %! assert_equal (s, 0.219780219780219, 1e-13); ## Weights and the prior, measured on R2024a %!test # weights %! [~, s] = fscchi2 (xb, c2, 'Weights', w); %! assert_equal (s, 8.57212519846196, 1e-13); %!test # the uniform prior %! [~, s] = fscchi2 (xb, c3, 'Prior', 'uniform'); %! assert_equal (s, 0.00381024182234503, 1e-15); %!test # a given prior %! [~, s] = fscchi2 (xb, c3, 'Prior', [0.6, 0.3, 0.1]); %! assert_equal (s, 0.00021661173625569, 1e-15); %!test # a prior as a structure %! P = struct ('ClassNames', {{'c', 'a', 'b'}}, 'ClassProbs', [0.1, 0.6, 0.3]); %! [~, s] = fscchi2 (xb, c3, 'Prior', P); %! assert_equal (s, 0.00021661173625569, 1e-15); %!test # weights and a prior %! [~, s] = fscchi2 (xb, c3, 'Weights', w, 'Prior', 'uniform'); %! assert_equal (s, 0.100179060091692, 1e-13); ## Missing values, measured on R2024a %!test # a predictor is tested on the observations it is known for %! xn = xb; %! xn([3, 17, 29]) = NaN; %! [~, s] = fscchi2 (xn, c2); %! assert_equal (s, 6.95245500311045, 1e-13); %!test # with weights, the empirical prior taken over those observations %! xn = xb; %! xn([3, 17, 29]) = NaN; %! [~, s] = fscchi2 (xn, c2, 'Weights', w); %! assert_equal (s, 7.41096325113626, 1e-13); %!test # a given prior is carried by those observations %! xn = xb; %! xn([3, 17, 29]) = NaN; %! [~, s] = fscchi2 (xn, c3, 'Prior', [0.6, 0.3, 0.1]); %! assert_equal (s, 0.000358852938452796, 1e-15); %!test # an observation of missing class is left out %! cm = c3; %! cm([4, 9]) = {''}; %! [~, s] = fscchi2 (xb, cm); %! assert_equal (s, 0.00110436213552064, 1e-15); %!test # and its weight with it %! cm = c3; %! cm([4, 9]) = {''}; %! [~, s] = fscchi2 (xb, cm, 'Weights', w); %! assert_equal (s, 0.162559576661729, 1e-13); ## Octave tests a predictor's missing values as one more group when asked, ## as MATLAB documents; R2024a and R2026b ignore the option %!test # a categorical predictor: chi-square 6 on 2 degrees of freedom %! x = [1; 1; 2; 2; NaN; NaN]; %! y = {'a'; 'a'; 'b'; 'b'; 'b'; 'b'}; %! [~, s] = fscchi2 (x, y, 'CategoricalPredictors', 1, 'UseMissing', true); %! assert_equal (s, 3, 1e-14); %!test # without it, chi-square 4 on 1 degree of freedom %! x = [1; 1; 2; 2; NaN; NaN]; %! y = {'a'; 'a'; 'b'; 'b'; 'b'; 'b'}; %! [~, s] = fscchi2 (x, y, 'CategoricalPredictors', 1); %! assert_equal (s, -log (chi2cdf (4, 1, 'upper')), 1e-14); %!test # a continuous predictor's missing values form one more bin %! x = [1; 2; 3; 4; NaN; NaN]; %! y = {'a'; 'a'; 'b'; 'b'; 'b'; 'b'}; %! [~, s] = fscchi2 (x, y, 'NumBins', 2, 'UseMissing', true); %! assert_equal (s, 3, 1e-14); ## What cannot separate the classes scores 0, measured on R2024a %!test # ranking keeps the order of equal scores %! [idx, s] = fscchi2 ([xb, ones(40, 1), NaN(40, 1), xb], c2); %! assert_equal (idx, [1, 4, 2, 3]); %! assert_equal (s, [8.52155983968549, 0, 0, 8.52155983968549], 1e-13); %!test %! [idx, s] = fscchi2 ([xb, xb, mod(xb, 7)], c3, 'NumBins', 4); %! assert_equal (idx, [3, 1, 2]); %! assert_equal (s, [0, 0, 0.0761861991737764], 1e-14); %!test # a single bin %! [~, s] = fscchi2 (xb, c3, 'NumBins', 1); %! assert_equal (s, 0); %!test # a single class %! [~, s] = fscchi2 (xb, repmat ({'a'}, 40, 1)); %! assert_equal (s, 0); %!test # more bins than distinct ranks %! [~, s] = fscchi2 (xb, c3, 'NumBins', 100); %! assert_equal (s, 0.877012088871555, 1e-13); ## The response, measured on R2024a %!test # the type of the labels does not matter %! [~, s] = fscchi2 (xb, c3); %! assert_equal (s, 0.001140903415465, 1e-15); %! assert_equal (nthargout (2, @fscchi2, xb, categorical (c3)), s); %! assert_equal (nthargout (2, @fscchi2, xb, char (c3)), s); %! assert_equal (nthargout (2, @fscchi2, xb, double (categorical (c3))), s); %!test # logical labels %! [~, s] = fscchi2 (xb, mod ((1:40)', 2) == 0); %! assert_equal (s, 0); %!test # a subset of the classes %! [~, s] = fscchi2 (xb, c3, 'ClassNames', {'a', 'b'}); %! assert_equal (s, 0.0242309863346623, 1e-15); %!test # categorical predictors given by index and as a logical vector %! X = [xb, mod((1:40)', 4)]; %! [~, s] = fscchi2 (X, c3, 'CategoricalPredictors', 2); %! assert_equal (s, [0.001140903415465, 0], 1e-15); %! [~, s] = fscchi2 (X, c3, 'CategoricalPredictors', [false, true]); %! assert_equal (s, [0.001140903415465, 0], 1e-15); ## A table, measured on R2024a %!test # a categorical variable with an undefined value %! k = categorical (repmat ({'u'; 'v'; 'w'; 'u'}, 10, 1)); %! k([2, 9, 30]) = missing; %! [~, s] = fscchi2 (table (k, xb, c3), 'c3'); %! assert_equal (s, [0.0566685023986232, 0.001140903415465], 1e-15); %!test # a formula ranks the predictors it names, in the table's order %! k = categorical (repmat ({'u'; 'v'; 'w'; 'u'}, 10, 1)); %! k([2, 9, 30]) = missing; %! [idx, s] = fscchi2 (table (xb, k, c3), 'c3 ~ k + xb'); %! assert_equal (idx, [2, 1]); %! assert_equal (s, [0.001140903415465, 0.0566685023986232], 1e-15); %!test # weights named in the table are no predictor %! x1 = (1:12)'; %! w = [1; 1; 2; 2; 1; 1; 3; 3; 1; 1; 2; 2]; %! lab = repmat ({'p'; 'q'}, 6, 1); %! [idx, s] = fscchi2 (table (x1, w, lab), 'lab', 'Weights', 'w'); %! assert_equal (idx, 1); %! assert_equal (s, 0.484012943062914, 1e-13); %!test # an ordinal variable is continuous, unless named categorical %! o12 = categorical ((1:12)', 1:12, 'Ordinal', true); %! n12 = categorical ((1:12)'); %! v = (1:12)'; %! lab = repmat ({'p'; 'q'; 'q'; 'p'}, 3, 1); %! T = table (o12, n12, v, lab); %! [~, s] = fscchi2 (T, 'lab'); %! assert_equal (s, [0.627085663064649, 1.01158205543499, ... %! 0.627085663064649], 1e-13); %! [~, s] = fscchi2 (T, 'lab', 'CategoricalPredictors', 3); %! assert_equal (s, [0.627085663064649, 1.01158205543499, ... %! 1.01158205543499], 1e-13); %! [~, s] = fscchi2 (T, 'lab', 'CategoricalPredictors', 'all'); %! assert_equal (s, [1.01158205543499, 1.01158205543499, ... %! 1.01158205543499], 1e-13); ## Test input validation %!error fscchi2 (1) %!error ... %! fscchi2 ([1; 2], {'a'; 'b'}, 'NumBins') %!error ... %! fscchi2 ([1; 2], {'a'; 'b'}, 'PredictorNames', {'x'}) %!error ... %! fscchi2 (int8 ([1; 2]), {'a'; 'b'}) %!error ... %! fscchi2 ([1; 2; 3], {'a'; 'b'}) %!error ... %! fscchi2 ([1; 2], {'a'; 'b'}, 'NumBins', 0) %!error ... %! fscchi2 ([1; 2], {'a'; 'b'}, 'NumBins', 2.5) %!error ... %! fscchi2 ([1; 2], {'a'; 'b'}, 'UseMissing', 2) %!error ... %! fscchi2 ([1; 2], {'a'; 'b'}, 'Weights', [1; 1; 1]) %!error ... %! fscchi2 ([1; 2], {'a'; 'b'}, 'Weights', [1; -1]) %!error ... %! fscchi2 ([1; 2], {'a'; 'b'}, 'Weights', [1; Inf]) %!error ... %! fscchi2 ([1; 2], {'a'; 'b'}, 'CategoricalPredictors', [true, false]) %!error ... %! fscchi2 ([1; 2], {'a'; 'b'}, 'CategoricalPredictors', 2) %!error ... %! fscchi2 ([1; 2], {'a'; 'b'}, 'CategoricalPredictors', 'x1') %!error ... %! fscchi2 ([1; 2], {'a'; 'b'}, 'Prior', [1, 2, 3]) %!error ... %! fscchi2 ([1; 2], {'a'; 'b'}, 'Prior', [1, -1]) %!error ... %! fscchi2 ([1; 2], {'a'; 'b'}, 'Prior', 'other') statistics-release-2.1.2/inst/Machine_Learning/fsrftest.m000066400000000000000000000444431527311104300235100ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{idx} =} fsrftest (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{idx} =} fsrftest (@var{Tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{idx} =} fsrftest (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{idx} =} fsrftest (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{idx} =} fsrftest (@dots{}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{idx}, @var{scores}] =} fsrftest (@dots{}) ## ## Rank the predictors for regression with F-tests. ## ## @code{@var{idx} = fsrftest (@var{X}, @var{Y})} tests each predictor, a ## column of the matrix @var{X}, with a one-way analysis of variance: whether ## the response @var{Y} has the same mean in every group of observations the ## predictor forms. It ranks the predictors by the result. @var{idx} is a row ## vector of column indices, from the most important predictor to the least; ## predictors of equal score keep their order. ## ## @var{X} is a real matrix of type double or single with one row per ## observation, and @var{Y} a real vector of type double or single with one ## response per row of @var{X}. ## ## @code{@var{idx} = fsrftest (@var{Tbl}, @var{ResponseVarName})} takes the ## predictors and the response from the table @var{Tbl}: @var{ResponseVarName} ## names the variable holding the response and every other variable is a ## predictor. @code{@var{idx} = fsrftest (@var{Tbl}, @var{formula})} names ## them with a model formula instead, @qcode{'Y ~ x1 + x2'}, which holds main ## effects only, and ranks only the predictors it names, @var{idx} indexing ## them in the order the table holds them. @code{@var{idx} = fsrftest ## (@var{Tbl}, @var{Y})} takes the predictors from the table and the response ## from @var{Y}. A logical, categorical, text or string variable of the table ## is a categorical predictor; an ordinal categorical one is continuous, its ## categories in their order. ## ## @code{[@var{idx}, @var{scores}] = fsrftest (@dots{})} also returns the ## score of each predictor, a row vector of type double in the order of the ## predictors, so that @code{@var{scores}(@var{idx})} is in decreasing order. ## The score is @math{-log (p)}, @math{p} being the p-value of the predictor's ## F-test, so a larger score marks a more important predictor; it is ## @code{Inf} where @math{p} is 0. A predictor that forms a single group, or ## as many groups as observations, scores 0, and so does every predictor when ## the response is constant. ## ## A continuous predictor is first binned. Its @math{n} known values are ## sorted, the upper edge of bin @math{k} is the value at position ## @code{ceil (@var{k} * @var{n} / @var{NumBins})}, and a value goes to the ## first bin whose edge it does not exceed. The bins so hold equal numbers of ## observations as nearly as the ties allow, the first ones taking any ## remainder, and equal values always share a bin, which may leave fewer bins ## than asked for. A categorical predictor takes its categories as they ## are. The F-test weighs each observation by its weight, scaling the ## weights not mattering. ## ## Each predictor is tested on the observations it is known for. An ## observation whose response is missing is left out of every test. ## ## The following @qcode{Name-Value} pairs are supported: ## ## @multitable @columnfractions 0.3 0.7 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'CategoricalPredictors'} @tab The categorical predictors: a ## vector of their indices, a logical vector with one value per predictor, or ## @qcode{'all'}. With a table they are added to those the table makes ## categorical, and they may also be named. The default is none for a matrix. ## ## @item @qcode{'NumBins'} @tab The number of bins of a continuous predictor, a ## positive integer. The default is 10. ## ## @item @qcode{'UseMissing'} @tab @qcode{false}, the default, to test each ## predictor on the observations it is known for, or @qcode{true} to test it ## on every observation, the missing values of a categorical predictor forming ## one more category and those of a continuous predictor one more bin. MATLAB ## R2024a and R2026b document this option but ignore it in @code{fsrftest}, ## scoring every predictor alike either way; Octave does what they document. ## ## @item @qcode{'Weights'} @tab The observation weights, a nonnegative finite ## vector of type double or single with one value per row, or with a table the ## name of one of its variables, which is then no predictor. The default is ## equal weights. ## @end multitable ## ## The option names are not case sensitive but must be given in full. With a ## matrix, the categorical predictors cannot be named, the predictors having no ## names, as in MATLAB. The test is computed in double precision whatever the ## type of the data; MATLAB R2024a gives a different score for a response of ## type single than for the same values as double, which Octave does not. ## ## @seealso{fscchi2, relieff, sequentialfs, anova1} ## @end deftypefn function [idx, scores] = fsrftest (X, Y, varargin) if (nargin < 2) print_usage (); endif ## Every option is named; a table brings its own predictor names, which the ## call may not set optNames = {'CategoricalPredictors', 'NumBins', 'UseMissing', 'Weights'}; if (mod (numel (varargin), 2) != 0) error ("fsrftest: optional arguments must be Name-Value pairs."); endif for k = 1:2:numel (varargin) if (! (ischar (varargin{k}) && any (strcmpi (varargin{k}, optNames)))) if (ischar (varargin{k})) error ("fsrftest: unknown parameter name '%s'.", varargin{k}); endif error ("fsrftest: optional arguments must be Name-Value pairs."); endif endfor ## A table is resolved into the predictor matrix, its categorical columns ## and the response; an ordinal column is continuous, as in MATLAB catGiven = []; if (istable (X)) tnames = X.Properties.VariableNames; [X, Y, args, ~, errmsg] = tableFrame (X, Y, varargin, true); if (! isempty (errmsg)) error ("fsrftest: %s", errmsg); endif [pnames, ~, catGiven, args] = parsePairedArguments ( ... {'PredictorNames', 'ResponseName', ... 'CategoricalPredictors'}, {[], [], []}, args); ## The predictors are ranked in the order the table holds them, a ## formula naming only which, as in MATLAB [~, pos] = ismember (pnames, tnames); [~, order] = sort (pos); X = X(:,order); iscatT = false (1, numel (pnames)); iscatT(catGiven) = true; catGiven = find (iscatT(order)); else args = varargin; endif dfValues = {[], 10, false, []}; [CatPreds, NumBins, UseMissing, Weights] = ... parsePairedArguments (optNames, dfValues, args); ## Predictors and response if (! (isfloat (X) && isreal (X) && ismatrix (X) && ! isempty (X))) error (strcat ("fsrftest: X must be a nonempty real matrix of type", ... " double or single.")); endif X = double (X); [n, p] = size (X); if (! (isfloat (Y) && isreal (Y) && isvector (Y))) error ("fsrftest: Y must be a real vector of type double or single."); endif if (numel (Y) != n) error ("fsrftest: X and Y must have the same number of observations."); endif Y = double (Y(:)); ## Options if (! (isnumeric (NumBins) && isreal (NumBins) && isscalar (NumBins) && NumBins >= 1 && NumBins == fix (NumBins))) error ("fsrftest: 'NumBins' must be a positive integer."); endif if (! ((islogical (UseMissing) || isnumeric (UseMissing)) && isscalar (UseMissing) && any (UseMissing == [0, 1]))) error ("fsrftest: 'UseMissing' must be a logical scalar."); endif UseMissing = logical (UseMissing); if (isempty (Weights)) Weights = ones (n, 1); else errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("fsrftest: %s", errmsg); endif if (! (isvector (Weights) && numel (Weights) == n)) error ("fsrftest: 'Weights' must have one value per observation."); endif Weights = double (Weights(:)); if (! (all (isfinite (Weights)) && all (Weights >= 0) && sum (Weights) > 0)) error (strcat ("fsrftest: 'Weights' must be nonnegative, finite and", ... " not all zero.")); endif endif ## The categorical predictors: those a table gave and those the call named, ## the call naming them by index, by a logical vector or as 'all' iscat = false (1, p); iscat(catGiven) = true; if (! isempty (CatPreds)) if (ischar (CatPreds) && isrow (CatPreds) && strcmpi (CatPreds, 'all')) iscat(:) = true; elseif (islogical (CatPreds) && isvector (CatPreds)) if (numel (CatPreds) != p) error (strcat ("fsrftest: 'CategoricalPredictors' as a logical", ... " vector must have one value per predictor.")); endif iscat |= CatPreds(:)'; elseif (isnumeric (CatPreds) && isvector (CatPreds)) if (! (isreal (CatPreds) && all (CatPreds == fix (CatPreds)) && all (CatPreds >= 1) && all (CatPreds <= p))) error (strcat ("fsrftest: 'CategoricalPredictors' must index the", ... " predictors.")); endif iscat(CatPreds) = true; else error (strcat ("fsrftest: 'CategoricalPredictors' must be indices, a", ... " logical vector or 'all'; naming the predictors", ... " needs a table.")); endif endif ## A missing response leaves its observation out used = ! isnan (Y); X = X(used,:); Y = Y(used); w = Weights(used); ## One test per predictor scores = zeros (1, p); for j = 1:p x = X(:,j); known = ! isnan (x); if (UseMissing) inTest = true (rows (x), 1); else inTest = known; endif if (! any (known & inTest)) continue; endif ## Bins of a continuous predictor, bounded by the values at equal steps ## through the sorted values, a value going to the first bin whose upper ## edge it does not exceed; categories of a categorical one. The missing ## values, when they are used, are one more group. xk = x(known); if (iscat(j)) [~, ~, g] = unique (xk); else sx = sort (xk); edges = sx(ceil ((1:NumBins-1) * numel (xk) / NumBins)); g = 1 + sum (xk > edges(:)', 2); endif group = zeros (rows (x), 1); group(known) = g; group(! known) = max (g) + 1; ## The weighted one-way analysis of variance of the response across the ## groups, the empty ones left out gt = group(inTest); yt = Y(inTest); wt = w(inTest); keep = wt > 0; gt = gt(keep); yt = yt(keep); wt = wt(keep); [~, ~, gt] = unique (gt); K = max ([0; gt]); nt = numel (yt); if (K < 2 || nt - K < 1) continue; endif m = sum (wt .* yt) / sum (wt); gw = accumarray (gt, wt); gm = accumarray (gt, wt .* yt) ./ gw; ssb = sum (gw .* (gm - m) .^ 2); ssw = sum (wt .* (yt - gm(gt)) .^ 2); if (! (ssb > 0)) continue; endif F = (ssb / (K - 1)) / (ssw / (nt - K)); scores(j) = -log (fcdf (F, K - 1, nt - K, 'upper')); endfor [~, idx] = sort (scores, 'descend'); endfunction ## Test output: the binning, measured on R2024a %!shared xb, yb, w %! xb = ((1:40) .^ 2)'; %! yb = mod ((1:40)', 7) + (1:40)' / 10; %! w = 1 + mod ((1:40)', 3); %!test # ten bins, the default %! [idx, s] = fsrftest (xb, yb); %! assert_equal (idx, 1); %! assert_equal (s, 3.28972104684277, 1e-13); %!test # the remainder goes to the first bins %! [~, s] = fsrftest (xb, yb, 'NumBins', 3); %! assert_equal (s, 3.96652214599653, 1e-13); %!test %! [~, s] = fsrftest (xb, yb, 'NumBins', 7); %! assert_equal (s, 2.73641811567188, 1e-13); %!test # ties share a bin %! xt = [ones(15, 1); 2 * ones(10, 1); (3:17)']; %! [~, s] = fsrftest (xt, yb); %! assert_equal (s, 3.63139675868502, 1e-13); %!test # equal values share a bin, here the two smallest %! [~, s] = fsrftest (mod (xb, 7), yb, 'NumBins', 4); %! assert_equal (s, 1.65333228183337, 1e-13); %!test # a categorical predictor takes its values as they are %! [~, s] = fsrftest ([xb, mod((1:40)', 4)], yb, 'CategoricalPredictors', 2); %! assert_equal (s, [3.28972104684277, 0.0395057912929183], 1e-13); %! [~, s] = fsrftest ([xb, mod((1:40)', 4)], yb, ... %! 'CategoricalPredictors', [false, true]); %! assert_equal (s, [3.28972104684277, 0.0395057912929183], 1e-13); ## Weights and missing values, measured on R2024a %!test # weights, whose scale does not matter %! [~, s] = fsrftest (xb, yb, 'Weights', w); %! assert_equal (s, 3.40381130018073, 1e-13); %! assert_equal (nthargout (2, @fsrftest, xb, yb, 'Weights', 3 * w), s, 1e-14); %!test # a predictor is tested on the observations it is known for %! xn = xb; %! xn([3, 17, 29]) = NaN; %! [~, s] = fsrftest (xn, yb); %! assert_equal (s, 3.39160408073151, 1e-13); %! [~, s] = fsrftest (xn, yb, 'Weights', w); %! assert_equal (s, 4.6530885484303, 1e-13); %!test # an observation of missing response is left out %! yn = yb; %! yn([5, 11]) = NaN; %! [~, s] = fsrftest (xb, yn); %! assert_equal (s, 2.46318862209335, 1e-13); %! [~, s] = fsrftest (xb, yn, 'Weights', w); %! assert_equal (s, 3.68126942373109, 1e-13); ## Octave tests a predictor's missing values as one more group when asked, ## as MATLAB documents; R2024a and R2026b ignore the option %!test # three groups: F = 64 on 2 and 3 degrees of freedom %! x = [1; 1; 2; 2; NaN; NaN]; %! y = [1; 2; 5; 6; 9; 10]; %! [~, s] = fsrftest (x, y, 'CategoricalPredictors', 1, 'UseMissing', true); %! assert_equal (s, -log (fcdf (64, 2, 3, 'upper')), 1e-14); %!test # without it, two: F = 32 on 1 and 2 %! x = [1; 1; 2; 2; NaN; NaN]; %! y = [1; 2; 5; 6; 9; 10]; %! [~, s] = fsrftest (x, y, 'CategoricalPredictors', 1); %! assert_equal (s, -log (fcdf (32, 1, 2, 'upper')), 1e-14); %!test # a continuous predictor's missing values form one more bin %! x = [1; 2; 3; 4; NaN; NaN]; %! y = [1; 2; 5; 6; 9; 10]; %! [~, s] = fsrftest (x, y, 'NumBins', 2, 'UseMissing', true); %! assert_equal (s, -log (fcdf (64, 2, 3, 'upper')), 1e-14); ## Scores of 0 and Inf, measured on R2024a %!test # ranking keeps the order of equal scores %! [idx, s] = fsrftest ([xb, ones(40, 1), NaN(40, 1), xb], yb); %! assert_equal (idx, [1, 4, 2, 3]); %! assert_equal (s, [3.28972104684277, 0, 0, 3.28972104684277], 1e-13); %!test # a single bin %! [~, s] = fsrftest (xb, yb, 'NumBins', 1); %! assert_equal (s, 0); %!test # as many groups as observations %! [~, s] = fsrftest (xb, yb, 'NumBins', 100); %! assert_equal (s, 0); %!test # a constant response %! [~, s] = fsrftest (xb, ones (40, 1)); %! assert_equal (s, 0); %!test # groups that separate the response wholly %! x = (1:400)'; %! [~, s] = fsrftest (x, [zeros(200, 1); ones(200, 1)] + mod (x, 3) / 100); %! assert_equal (s, Inf); %!test # single predictors are tested in double %! [~, s] = fsrftest (single (xb), yb); %! assert_equal (class (s), 'double'); %! assert_equal (s, 3.28972104684277, 1e-13); ## A table, measured on R2024a %!test # a categorical variable with an undefined value %! k = categorical (repmat ({'u'; 'v'; 'w'; 'u'}, 10, 1)); %! k([2, 9, 30]) = missing; %! [~, s] = fsrftest (table (k, xb, yb), 'yb'); %! assert_equal (s, [0.189033357981702, 3.28972104684277], 1e-13); %!test # the response given beside the table %! k = categorical (repmat ({'u'; 'v'; 'w'; 'u'}, 10, 1)); %! k([2, 9, 30]) = missing; %! [~, s] = fsrftest (table (k, xb), yb); %! assert_equal (s, [0.189033357981702, 3.28972104684277], 1e-13); %!test # a formula ranks the predictors it names, in the table's order %! k = categorical (repmat ({'u'; 'v'; 'w'; 'u'}, 10, 1)); %! k([2, 9, 30]) = missing; %! [idx, s] = fsrftest (table (xb, k, yb), 'yb ~ k + xb'); %! assert_equal (idx, [1, 2]); %! assert_equal (s, [3.28972104684277, 0.189033357981702], 1e-13); %!test # weights named in the table are no predictor %! x1 = (1:12)'; %! wt = [1; 1; 2; 2; 1; 1; 3; 3; 1; 1; 2; 2]; %! y12 = mod ((1:12)', 5) + (1:12)' / 7; %! [idx, s] = fsrftest (table (x1, wt, y12), 'y12', 'Weights', 'wt'); %! assert_equal (idx, 1); %! assert_equal (s, 1.0443158836381, 1e-13); ## Test input validation %!error fsrftest (1) %!error ... %! fsrftest ([1; 2], [1; 2], 'NumBins') %!error ... %! fsrftest ([1; 2], [1; 2], 'Prior', 'uniform') %!error ... %! fsrftest (int8 ([1; 2]), [1; 2]) %!error ... %! fsrftest ([1; 2], [true; false]) %!error ... %! fsrftest ([1; 2], {'a'; 'b'}) %!error ... %! fsrftest ([1; 2; 3], [1; 2]) %!error ... %! fsrftest ([1; 2], [1; 2], 'NumBins', 0) %!error ... %! fsrftest ([1; 2], [1; 2], 'UseMissing', 2) %!error ... %! fsrftest ([1; 2], [1; 2], 'Weights', [1; 1; 1]) %!error ... %! fsrftest ([1; 2], [1; 2], 'Weights', [1; -1]) %!error ... %! fsrftest ([1; 2], [1; 2], 'CategoricalPredictors', [true, false]) %!error ... %! fsrftest ([1; 2], [1; 2], 'CategoricalPredictors', 2) %!error ... %! fsrftest ([1; 2], [1; 2], 'CategoricalPredictors', 'x1') statistics-release-2.1.2/inst/Machine_Learning/lime.m000066400000000000000000002047411527311104300225750ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftp {statistics} lime ## ## Local interpretable model-agnostic explanations for a fitted model. ## ## A @code{lime} object explains one prediction by fitting a simple model, ## a linear one or a shallow decision tree, over observations drawn around ## the query point and weighted by how near they lie to it. The simple ## model is readable where the fitted one is not, and it is accurate near ## the query point rather than everywhere. ## ## @code{@var{explainer} = lime (@var{Mdl})} builds an explainer for the ## fitted model @var{Mdl} over the observations it was fitted on. A compact ## model keeps none, so it must be given them as @var{X}, and so must a ## function handle. The observations to fit the simple model on are drawn ## at once, and nothing is explained until a query point is given, either to ## the constructor as @qcode{'QueryPoint'} or afterwards to @code{fit}. ## ## @code{@var{explainer} = lime (@var{Mdl}, @var{X})} takes the observations ## the draw is fitted to as @var{X}, a real numeric matrix of one column per ## predictor. ## ## @var{X} may also be a table, and so may @qcode{'QueryPoint'}, ## @qcode{'CustomSyntheticData'} and what @code{fit} is given. Where the ## model names its predictors the table is read by those names and not by ## the order its columns come in, so a column the model was not fitted on is ## passed over and a value holding a level is coded as that level was coded ## at fitting. A function handle names nothing, so a table given for one ## names the predictors itself and its columns are taken in the order they ## come. ## ## @code{@var{explainer} = lime (@var{fun}, @var{X})} takes a function ## handle in place of a model. @var{fun} is called with a matrix of ## observations and answers with one column holding one value for each, and ## @qcode{'Type'} must say whether those values are a response or a label. ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## ## @item @qcode{'Type'} @tab @tab Whether the model answers with a response, ## @qcode{'regression'}, or with a label, @qcode{'classification'}. It is ## taken from a fitted model and is required for a function handle. ## ## @item @qcode{'DataLocality'} @tab @tab Where the observations are drawn ## from: @qcode{'global'}, the default, fits the distribution to the whole ## of @var{X}; @qcode{'local'} fits it to the @qcode{'NumNeighbors'} ## observations nearest the query point, which must then be known. ## ## @item @qcode{'NumNeighbors'} @tab @tab How many neighbours ## @qcode{'local'} fits to, 1500 by default. ## ## @item @qcode{'NumSyntheticData'} @tab @tab How many observations to draw, ## 5000 by default. ## ## @item @qcode{'CustomSyntheticData'} @tab @tab Observations to use instead ## of drawing any, one row each. Nothing is drawn where it is given. ## ## @item @qcode{'CategoricalPredictors'} @tab @tab The predictors whose ## values are levels, taken as by every learner of this package. It applies ## only to a function handle, a model being asked for its own. ## ## @item @qcode{'QueryPoint'} @tab @tab The observation to explain. Given ## with @qcode{'NumImportantPredictors'} it is explained at once, otherwise ## it is left to @code{fit}. ## ## @item @qcode{'NumImportantPredictors'} @tab @tab How many predictors the ## simple model is fitted on. ## @end multitable ## ## Every option @code{fit} takes may also be given here, in which case it ## stands as the default for every later @code{fit}. ## ## @code{'UseParallel'} is not implemented and is refused rather than ## ignored. ## ## @seealso{shapley, partialDependence, PredictiveModel} ## @end deftp classdef lime properties (SetAccess = protected) ## -*- texinfo -*- ## @deftp {lime} {property} BlackboxModel ## ## The model being explained ## ## The fitted model, or the function handle, the explainer was built on. ## This property is read-only. ## ## @end deftp BlackboxModel = []; ## -*- texinfo -*- ## @deftp {lime} {property} DataLocality ## ## Where the observations were drawn from ## ## @qcode{'global'} where the distribution was fitted to the whole of ## @qcode{X}, @qcode{'local'} where it was fitted to the neighbours of ## the query point. This property is read-only. ## ## @end deftp DataLocality = 'global'; ## -*- texinfo -*- ## @deftp {lime} {property} CategoricalPredictors ## ## The categorical predictors ## ## The indices of the predictors whose values are levels, empty where ## there are none. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {lime} {property} Type ## ## What the model answers with ## ## @qcode{'regression'} or @qcode{'classification'}. This property is ## read-only. ## ## @end deftp Type = ''; ## -*- texinfo -*- ## @deftp {lime} {property} X ## ## The observations the draw was fitted to ## ## A real numeric matrix of one row per observation and one column per ## predictor. This property is read-only. ## ## Where the observations were given as a table, they are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the object. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {lime} {property} QueryPoint ## ## The observation explained ## ## One row holding one value per predictor, empty until a query point is ## given. This property is read-only. ## ## @end deftp QueryPoint = []; ## -*- texinfo -*- ## @deftp {lime} {property} NumImportantPredictors ## ## How many predictors the simple model was asked for ## ## Empty until a query point is given. Fewer may be used, where a ## predictor adds nothing. This property is read-only. ## ## @end deftp NumImportantPredictors = []; ## -*- texinfo -*- ## @deftp {lime} {property} NumSyntheticData ## ## How many observations were drawn ## ## This property is read-only. ## ## @end deftp NumSyntheticData = 5000; ## -*- texinfo -*- ## @deftp {lime} {property} SyntheticData ## ## The observations the simple model is fitted over ## ## One row each, drawn around the query point or given outright as ## @qcode{'CustomSyntheticData'}. This property is read-only. ## ## @end deftp SyntheticData = []; ## -*- texinfo -*- ## @deftp {lime} {property} Fitted ## ## What the model answers over the drawn observations ## ## One response per observation for a regression model, or one label for ## a classifier, keeping the type of the response the model was fitted ## with. This property is read-only. ## ## @end deftp Fitted = []; ## -*- texinfo -*- ## @deftp {lime} {property} SimpleModel ## ## The simple model fitted around the query point ## ## A @code{RegressionLinear}, @code{ClassificationLinear}, ## @code{RegressionTree} or @code{ClassificationTree}, fitted on the ## important predictors alone and weighted by nearness to the query ## point. For a classifier it answers @math{1} for the class the ## explained model predicted and @math{-1} for any other, which is what ## makes it a single model however many classes there are. It is empty ## until a query point is given. This property is read-only. ## ## @end deftp SimpleModel = []; ## -*- texinfo -*- ## @deftp {lime} {property} ImportantPredictors ## ## The predictors the simple model was fitted on ## ## Their indices, in increasing order, empty until a query point is ## given. This property is read-only. ## ## @end deftp ImportantPredictors = []; ## -*- texinfo -*- ## @deftp {lime} {property} BlackboxFitted ## ## What the explained model answers at the query point ## ## This property is read-only. ## ## @end deftp BlackboxFitted = []; ## -*- texinfo -*- ## @deftp {lime} {property} SimpleModelFitted ## ## What the simple model answers at the query point ## ## Where the two agree the simple model is worth reading; where they do ## not, it explains nothing. This property is read-only. ## ## @end deftp SimpleModelFitted = []; endproperties properties (Access = protected, Hidden) PredictorNames_ = {}; # one name per column of X ClassNames_ = []; # the classes, in the model's own order Opts_ = []; # the fit options given at construction NumNeighbors_ = 1500; # how many neighbours 'local' fits to Custom_ = false; # whether the observations were given outright endproperties methods (Hidden) function disp (this) printf ('\n lime with properties:\n\n'); printf ('%26s: %s\n', 'Type', this.Type); printf ('%26s: %s\n', 'DataLocality', this.DataLocality); printf ('%26s: %d\n', 'NumSyntheticData', this.NumSyntheticData); if (isempty (this.SimpleModel)) printf ('%26s: %s\n', 'SimpleModel', '[]'); else printf ('%26s: %s\n', 'SimpleModel', class (this.SimpleModel)); printf ('%26s: %s\n', 'ImportantPredictors', ... mat2str (this.ImportantPredictors)); endif printf ('\n'); endfunction function display (this) inName = inputname (1); if (! isempty (inName)) printf ('%s =\n', inName); endif disp (this); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {lime} {@var{obj} =} lime (@var{Mdl}) ## @deftypefnx {lime} {@var{obj} =} lime (@var{Mdl}, @var{X}) ## @deftypefnx {lime} {@var{obj} =} lime (@var{fun}, @var{X}) ## @deftypefnx {lime} {@var{obj} =} lime (@dots{}, @var{name}, @var{value}) ## ## Build a local interpretable model-agnostic explainer. ## ## The arguments are those described for the class. The observations ## the simple model is fitted over are drawn here; where ## @qcode{'QueryPoint'} and @qcode{'NumImportantPredictors'} are both ## given the explanation is computed at once, otherwise it is left to ## @code{fit}. ## ## @seealso{lime, lime.fit, lime.plot} ## @end deftypefn function this = lime (blackbox, varargin) ## Input validation if (nargin < 1) error ("lime: too few input arguments."); endif if (! (isa (blackbox, 'PredictiveModel') || is_function_handle (blackbox))) error (strcat ("lime: BLACKBOX must be a fitted model that", ... " predicts, or a function handle.")); endif ## The observations come before the options, as MATLAB orders them args = varargin; Data = []; if (! isempty (args) && ! (ischar (args{1}) || isa (args{1}, 'string'))) Data = args{1}; args(1) = []; endif optNames = {'Type', 'DataLocality', 'NumNeighbors', ... 'NumSyntheticData', 'CustomSyntheticData', ... 'CategoricalPredictors', 'QueryPoint', ... 'NumImportantPredictors', 'Distance', 'KernelWidth', ... 'SimpleModelType', 'BetaTolerance', 'Cov', 'P', ... 'Scale', 'UseParallel'}; dfValues = repmat ({[]}, 1, numel (optNames)); [Type, Loc, NumNb, NumSyn, Custom, CatPred, QP, NIP, Dist, KW, ... SMT, BT, Cov, PP, Scale, Par, rem] = ... parsePairedArguments (optNames, dfValues, args); if (! isempty (rem)) error ("lime: unknown optional argument or misplaced value."); endif if (! isempty (Par)) error ("lime: 'UseParallel' is not implemented."); endif [F, errmsg] = limeFrame (blackbox, Data, CatPred, Type); if (! isempty (errmsg)) error ("lime: %s", errmsg); endif this.BlackboxModel = blackbox; this.X = F.X; this.Type = F.Type; this.CategoricalPredictors = F.Cat; this.PredictorNames_ = F.PredictorNames; this.ClassNames_ = F.ClassNames; M = columns (F.X); this.DataLocality = limeCheckWord (Loc, {'global', 'local'}, ... 'lime', 'DataLocality', 'global'); this.NumNeighbors_ = limeCheckCount (NumNb, 'lime', 'NumNeighbors', ... 1500); this.Opts_ = limeFitOpts (Dist, KW, SMT, BT, Cov, PP, Scale, ... 'lime', F); if (! isempty (QP)) this.QueryPoint = limeCheckPoint (QP, M, 'lime', ... this.PredictorNames_, ... limeLevels (blackbox)); endif ## The observations are drawn here, not at the explanation, so that ## an explainer carries them before any query point is given if (! isempty (Custom)) ## Given outright, and read by the same names as everything else if (istable (Custom)) Custom = tableToMatrix (this.PredictorNames_, ... limeLevels (blackbox), Custom, 'lime'); endif if (! (isnumeric (Custom) && isreal (Custom) && ismatrix (Custom) && ndims (Custom) == 2 && ! isempty (Custom))) error (strcat ("lime: 'CustomSyntheticData' must be a real", ... " numeric matrix.")); endif if (columns (Custom) != M) error (strcat ("lime: 'CustomSyntheticData' must have one", ... " column per predictor of the model.")); endif this.SyntheticData = double (Custom); this.NumSyntheticData = rows (Custom); this.Custom_ = true; else this.NumSyntheticData = limeCheckCount (NumSyn, 'lime', ... 'NumSyntheticData', 5000); this.SyntheticData = limeSynth (this); endif this.Fitted = limePredict (blackbox, this.SyntheticData, F.Type); ## A query point with a count of predictors is explained at once if (! isempty (QP) && ! isempty (NIP)) this = fit (this, this.QueryPoint, NIP); endif endfunction ## -*- texinfo -*- ## @deftypefn {lime} {@var{obj} =} fit (@var{obj}, @var{queryPoint}, @var{numImportantPredictors}) ## @deftypefnx {lime} {@var{obj} =} fit (@dots{}, @var{name}, @var{value}) ## ## Fit the simple model around one query point. ## ## @var{queryPoint} is one row holding one value per predictor, or a ## table read by the names the explainer holds, and ## @var{numImportantPredictors} how many predictors the simple model is ## fitted on. Fewer are used where a predictor adds nothing, and ## @code{ImportantPredictors} says which were. ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## ## @item @qcode{'Distance'} @tab @tab How nearness to the query point is ## measured. Where the predictors hold numbers it is one of ## @qcode{'euclidean'}, the default, @qcode{'squaredeuclidean'}, ## @qcode{'seuclidean'}, @qcode{'mahalanobis'}, @qcode{'cityblock'}, ## @qcode{'minkowski'}, @qcode{'chebychev'}, @qcode{'cosine'}, ## @qcode{'correlation'} and @qcode{'spearman'}; where some hold levels ## it is @qcode{'goodall3'}, the default, or @qcode{'ofd'}. Both count ## level frequencies over the drawn observations and the query point ## together. @qcode{'goodall3'} is the @qcode{'goodall3'} measure of ## @code{nomdist}; @qcode{'ofd'} is one less the mean similarity of its ## @qcode{'of'} measure, @math{D / (1 + D)} of the @math{D} it returns. ## A function handle is also taken, which MATLAB does not; see below. ## ## @item @qcode{'KernelWidth'} @tab @tab How sharply the weight falls ## away with distance, from just above 0 to 1. The default is 0.75. ## ## @item @qcode{'SimpleModelType'} @tab @tab @qcode{'linear'}, the ## default, or @qcode{'tree'}. ## ## @item @qcode{'BetaTolerance'} @tab @tab Relative tolerance the linear ## simple model is fitted to, @math{10^{-4}} by default. ## ## @item @qcode{'Cov'}, @qcode{'P'}, @qcode{'Scale'} @tab @tab Passed to ## the distance that takes them, as @code{pdist2} takes them. ## @end multitable ## ## The weight of a drawn observation is ## @math{exp (-0.5 (d / D / w)^2)}, with @math{d} its distance from the ## query point and @math{w} the kernel width. @math{D} is the square ## root of the number of predictors for @qcode{'goodall3'} and ## @qcode{'ofd'}, and otherwise the largest distance between two drawn ## observations. ## ## A predictor holding levels enters a linear simple model as one column ## for each level other than the query point's, saying whether an ## observation holds that level, so each coefficient is read against the ## query point's level. ## ## Where the predictors mix numbers and levels, @qcode{'goodall3'} and ## @qcode{'ofd'} score a predictor holding numbers by one less its ## absolute difference over its range, the query point included, and ## average that with the similarities of the predictors holding levels. ## MATLAB does so by a rule it does not document, which none of the ## constructions tried here reproduces, so on such data the explanation ## differs from MATLAB's. On levels alone @qcode{'goodall3'} agrees with ## MATLAB; MATLAB does not document @qcode{'ofd'} beyond scoring a match ## 0, and its coefficients differ from those here by up to @math{0.006} ## on the fixture measured. ## ## @seealso{lime, lime.plot} ## @end deftypefn function this = fit (this, QueryPoint, NumImportant, varargin) ## Input validation if (nargin < 3) error ("lime.fit: too few input arguments."); endif M = numel (this.PredictorNames_); QueryPoint = limeCheckPoint (QueryPoint, M, 'lime.fit', ... this.PredictorNames_, ... limeLevels (this.BlackboxModel)); if (! (isnumeric (NumImportant) && isscalar (NumImportant) && isreal (NumImportant) && isfinite (NumImportant) && NumImportant == fix (NumImportant) && NumImportant > 0)) error (strcat ("lime.fit: NUMIMPORTANTPREDICTORS must be a", ... " positive integer.")); endif if (NumImportant > M) error (strcat ("lime.fit: NUMIMPORTANTPREDICTORS must not exceed", ... " the %d predictors of the model."), M); endif optNames = {'Distance', 'KernelWidth', 'SimpleModelType', ... 'BetaTolerance', 'Cov', 'P', 'Scale'}; dfValues = repmat ({[]}, 1, numel (optNames)); [Dist, KW, SMT, BT, Cov, PP, Scale, rem] = ... parsePairedArguments (optNames, dfValues, varargin); if (! isempty (rem)) error ("lime.fit: unknown optional argument or misplaced value."); endif F = struct ('Cat', this.CategoricalPredictors, 'X', this.X); opts = limeFitOpts (Dist, KW, SMT, BT, Cov, PP, Scale, 'lime.fit', F); opts = limeMergeOpts (this.Opts_, opts); this.QueryPoint = QueryPoint; this.NumImportantPredictors = double (NumImportant); ## A local draw follows the query point, so it is taken again wherever ## the point has moved away from the one it was drawn around if (strcmp (this.DataLocality, 'local') && ! this.Custom_) this.SyntheticData = limeSynth (this); this.Fitted = limePredict (this.BlackboxModel, ... this.SyntheticData, this.Type); endif ## Nearness to the query point, and the weight it carries d = limeDistance (this.SyntheticData, QueryPoint, opts, ... this.CategoricalPredictors, 'lime.fit'); scale = limeScale (this.SyntheticData, opts, ... this.CategoricalPredictors, 'lime.fit'); w = limeWeights (d, scale, opts.KernelWidth); ## A classifier is explained one class at a time: the simple model ## separates the class the explained model predicted from every other bbf = limePredict (this.BlackboxModel, QueryPoint, this.Type); this.BlackboxFitted = bbf; if (strcmp (this.Type, 'classification')) y = 2 * double (limeSameLabel (this.Fitted, bbf)) - 1; else y = double (this.Fitted(:)); endif ## The predictors the simple model is fitted on, chosen a group at a ## time so that the levels of one predictor are taken or left together [A, grp] = limeExpand (this.SyntheticData, ... this.CategoricalPredictors, this.SyntheticData, ... QueryPoint); sel = limeOMP (A, y, w, grp, NumImportant); if (isempty (sel)) ## A predictor whose every drawn level is the query point's has no ## column, and cannot be the one taken if (isempty (grp)) error (strcat ("lime.fit: every drawn observation holds the", ... " query point's levels, so nothing can be", ... " explained.")); endif sel = grp(1); endif this.ImportantPredictors = sel(:); this.SimpleModel = limeSimple (this, sel, y, w, opts); this.SimpleModelFitted = limeSimpleFitted (this, sel, QueryPoint, bbf); endfunction ## -*- texinfo -*- ## @deftypefn {lime} {} plot (@var{obj}) ## @deftypefnx {lime} {@var{f} =} plot (@var{obj}) ## ## Plot what the simple model says about the query point. ## ## A horizontal bar per important predictor, holding the coefficient of ## a linear simple model or the predictor importance of a tree, in the ## order the predictors come in rather than by size. @var{f} is the ## figure drawn. ## ## @seealso{lime, lime.fit} ## @end deftypefn function varargout = plot (this) if (isempty (this.SimpleModel)) error (strcat ("lime.plot: the simple model is not fitted; use", ... " fit to compute it.")); endif [val, lab, ttl] = limePlotData (this); hf = figure (); ax = axes (hf); barh (ax, 1:numel (val), val); set (ax, 'ytick', 1:numel (val), 'yticklabel', lab); title (ax, ttl); if (strcmp (limeModelType (this), 'tree')) xlabel (ax, 'Predictor Importance'); else xlabel (ax, 'Coefficient'); endif ylabel (ax, 'Predictor'); if (nargout > 0) varargout{1} = hf; endif endfunction endmethods methods (Access = private) ## Draw the observations the simple model is fitted over. A predictor ## holding numbers is drawn from a normal fitted to the observations, and ## one holding levels from the levels as often as they appear among them. function S = limeSynth (this) X = this.X; n = this.NumSyntheticData; if (strcmp (this.DataLocality, 'local')) if (isempty (this.QueryPoint)) error (strcat ("lime: 'DataLocality' of 'local' needs a query", ... " point to draw around.")); endif k = min (this.NumNeighbors_, rows (X)); idx = knnsearch (X, this.QueryPoint, 'K', k); X = X(idx(:),:); endif M = columns (X); cat = this.CategoricalPredictors; num = setdiff (1:M, cat); S = zeros (n, M); if (! isempty (num)) mu = mean (X(:,num), 1); if (rows (X) > 1) sigma = cov (X(:,num)); else sigma = zeros (numel (num)); endif S(:,num) = mvnrnd (mu, sigma, n); endif for j = cat(:)' lev = unique (X(:,j)); cnt = arrayfun (@(v) sum (X(:,j) == v), lev); S(:,j) = lev(randsample (numel (lev), n, true, cnt)); endfor endfunction ## Fit the simple model over the predictors taken, weighted by nearness. function Mdl = limeSimple (this, sel, y, w, opts) cat = this.CategoricalPredictors; Z = this.SyntheticData(:,sel); catsel = find (ismember (sel, cat)); isclass = strcmp (this.Type, 'classification'); args = {}; if (! isempty (catsel)) args = {'CategoricalPredictors', catsel}; endif names = this.PredictorNames_(sel); if (strcmp (opts.SimpleModelType, 'tree')) if (isclass) Mdl = fitctree (Z, y, 'Weights', w, 'PredictorNames', names, args{:}); else Mdl = fitrtree (Z, y, 'Weights', w, 'PredictorNames', names, args{:}); endif return; endif ## A predictor holding levels is coded here rather than by the ## learner: a column to every level but the query point's, which the ## others are read against, as MATLAB reads them [A, grp] = limeExpand (this.SyntheticData(:,sel), catsel, ... this.SyntheticData(:,sel), this.QueryPoint(sel)); enames = cell (1, columns (A)); for k = 1:columns (A) enames{k} = names{grp(k)}; endfor if (isclass) Mdl = fitclinear (A, y, 'Lambda', 0, 'Learner', 'logistic', ... 'Weights', w, 'BetaTolerance', opts.BetaTolerance, ... 'PredictorNames', enames); else Mdl = fitrlinear (A, y, 'Lambda', 0, 'Learner', 'leastsquares', ... 'Weights', w, 'BetaTolerance', opts.BetaTolerance, ... 'PredictorNames', enames); endif endfunction ## What a plot of the simple model draws, and what it is titled. function [val, lab, ttl] = limePlotData (this) sel = this.ImportantPredictors(:)'; if (strcmp (limeModelType (this), 'tree')) val = predictorImportance (this.SimpleModel)(:); lab = this.PredictorNames_(sel); ttl = 'LIME with Decision Tree Model'; return; endif val = this.SimpleModel.Beta(:); ttl = 'LIME with Linear Model'; [~, grp] = limeExpand (this.SyntheticData(:,sel), ... find (ismember (sel, ... this.CategoricalPredictors)), ... this.SyntheticData(:,sel), this.QueryPoint(sel)); lab = cell (1, numel (val)); for k = 1:numel (val) lab{k} = this.PredictorNames_{sel(grp(k))}; endfor endfunction endmethods endclassdef ## A table as the numbers the explainer works over. A fitted model names ## the predictors and holds the levels they were coded through; a function ## handle has neither, so the table itself names them. function [X, tnames, errmsg] = limeReadTable (blackbox, isfh, T, caller) X = []; tnames = {}; errmsg = ''; if (isfh) [X, ~, args, ~, errmsg] = tableFrame (T, [], {}); if (isempty (errmsg)) tnames = args{2}; endif return; endif if (! any (strcmp (properties (blackbox), 'PredictorNames'))) errmsg = strcat ("X may be a table only for a model that names its", ... " predictors."); return; endif try X = tableToMatrix (blackbox.PredictorNames, limeLevels (blackbox), ... T, caller); catch err errmsg = regexprep (err.message, '^lime: ', ''); return; end_try_catch endfunction ## The levels a model's predictors were coded through, where it kept any. ## PredictorLevels is hidden, so properties () does not list it; the class ## that defines it is what says whether it is there. function lev = limeLevels (blackbox) lev = {}; if (isa (blackbox, 'PredictiveModel')) lev = blackbox.PredictorLevels; endif endfunction ## The model, the observations and what the model answers with. function [F, errmsg] = limeFrame (blackbox, Data, CatPred, Type) F = []; errmsg = ''; isfh = is_function_handle (blackbox); if (isfh) props = {}; else props = properties (blackbox); endif has = @(n) any (strcmp (props, n)); ## The observations, from the model where it kept them if (isempty (Data)) if (isfh) errmsg = "X is required when the model is a function handle."; return; endif if (! (has ('X') && ! isempty (blackbox.X))) errmsg = strcat ("X is required for a model that does not keep the", ... " observations it was fitted on."); return; endif Data = blackbox.X; endif ## A table is read by the names the model was fitted on, so a column it ## was not fitted on is passed over and a level carries the code it ## carried then. A handle has no model behind it, so its table names its ## own predictors and every column is one. tnames = {}; if (istable (Data)) [Data, tnames, errmsg] = limeReadTable (blackbox, isfh, Data, 'lime'); if (! isempty (errmsg)) return; endif endif if (! (isnumeric (Data) && isreal (Data) && ismatrix (Data) && ndims (Data) == 2 && ! isempty (Data))) errmsg = "X must be a real numeric matrix."; return; endif p = columns (Data); ## What the model answers with if (isfh) if (isempty (Type)) errmsg = strcat ("'Type' is required when the model is a function", ... " handle."); return; endif endif if (isempty (Type)) if (isa (blackbox, 'ClassificationPartitionedModel') || has ('ClassNames')) Type = 'classification'; else Type = 'regression'; endif endif if (! (ischar (Type) && isrow (Type) && any (strcmpi ({'regression', 'classification'}, Type)))) errmsg = "'Type' must be 'regression' or 'classification'."; return; endif Type = lower (Type); ## The predictor names, from the model where it has them if (has ('PredictorNames') && ! isempty (blackbox.PredictorNames)) pnames = blackbox.PredictorNames(:)'; if (numel (pnames) != p) errmsg = "X must have one column per predictor of the model."; return; endif elseif (! isempty (tnames)) pnames = tnames; else pnames = arrayfun (@(k) sprintf ('x%d', k), 1:p, 'UniformOutput', false); endif ## The categorical predictors: the model's own, or those named for a handle if (isfh) spec = CatPred; else if (has ('CategoricalPredictors')) spec = blackbox.CategoricalPredictors; else spec = []; endif if (! isempty (CatPred) && ! isequal (CatPred, spec)) errmsg = strcat ("'CategoricalPredictors' must match the", ... " CategoricalPredictors property of the model."); return; endif endif cat = []; if (! isempty (spec)) [C, msg] = dummyCoding (Data, spec, pnames); if (! isempty (msg)) errmsg = msg; return; endif cat = C.Index(:)'; endif cnames = []; if (strcmp (Type, 'classification') && has ('ClassNames')) cnames = blackbox.ClassNames; endif F = struct ('X', double (Data), 'Type', Type, 'Cat', cat, ... 'PredictorNames', {pnames}, 'ClassNames', {cnames}); endfunction ## What the model answers over a matrix of observations. function v = limePredict (blackbox, Z, Type) if (is_function_handle (blackbox)) v = blackbox (Z); if (! (isvector (v) && numel (v) == rows (Z))) error (strcat ("lime: the function must answer with one value for", ... " each observation.")); endif if (strcmp (Type, 'regression')) v = double (v(:)); else v = v(:); endif return; endif v = predict (blackbox, Z); if (strcmp (Type, 'regression')) v = double (v(:)); endif endfunction ## Whether each label is the one the explained model gave the query point. function tf = limeSameLabel (labels, one) if (iscellstr (labels)) tf = strcmp (labels(:), limeLabelText (one)); elseif (ischar (labels)) tf = strcmp (cellstr (labels), limeLabelText (one)); elseif (isa (labels, 'string') || isa (labels, 'categorical')) tf = strcmp (cellstr (labels(:)), limeLabelText (one)); else tf = (labels(:) == one(1)); endif endfunction ## One label as the text it compares as. function s = limeLabelText (one) if (iscellstr (one)) s = one{1}; elseif (ischar (one)) s = one; elseif (isa (one, 'string') || isa (one, 'categorical')) c = cellstr (one); s = c{1}; else s = one(1); endif endfunction ## The options a fit is made with, checked as they are given. function opts = limeFitOpts (Dist, KW, SMT, BT, Cov, PP, Scale, caller, F) opts = struct (); opts.Distance = []; if (! isempty (Dist)) opts.Distance = limeCheckDistance (Dist, F, caller); endif opts.KernelWidth = []; if (! isempty (KW)) if (! (isnumeric (KW) && isscalar (KW) && isreal (KW) && KW > 0 && KW <= 1)) error (strcat ("%s: 'KernelWidth' must be a scalar greater than 0", ... " and not greater than 1."), caller); endif opts.KernelWidth = double (KW); endif opts.SimpleModelType = []; if (! isempty (SMT)) opts.SimpleModelType = limeCheckWord (SMT, {'linear', 'tree'}, ... caller, 'SimpleModelType', []); endif opts.BetaTolerance = []; if (! isempty (BT)) if (! (isnumeric (BT) && isscalar (BT) && isreal (BT) && BT >= 0)) error ("%s: 'BetaTolerance' must be a nonnegative scalar.", caller); endif opts.BetaTolerance = double (BT); endif opts.Cov = Cov; opts.P = PP; opts.Scale = Scale; endfunction ## The options of a fit, over those the explainer was built with. function opts = limeMergeOpts (base, over) opts = base; for f = fieldnames (over)' if (! isempty (over.(f{1}))) opts.(f{1}) = over.(f{1}); endif endfor if (isempty (opts.KernelWidth)) opts.KernelWidth = 0.75; endif if (isempty (opts.SimpleModelType)) opts.SimpleModelType = 'linear'; endif if (isempty (opts.BetaTolerance)) opts.BetaTolerance = 1e-4; endif endfunction ## The named distances, which are two lists chosen by what the predictors ## hold, and a function handle, which is ours and not MATLAB's. function d = limeCheckDistance (Dist, F, caller) numeric = {'euclidean', 'squaredeuclidean', 'seuclidean', ... 'mahalanobis', 'cityblock', 'minkowski', 'chebychev', ... 'cosine', 'correlation', 'spearman'}; levels = {'goodall3', 'ofd'}; iscat = ! isempty (F.Cat); if (is_function_handle (Dist)) d = limeCheckHandle (Dist, F.X, caller); return; endif if (isa (Dist, 'string') && isscalar (Dist)) Dist = char (Dist); endif if (iscat) allowed = levels; else allowed = numeric; endif if (! (ischar (Dist) && isrow (Dist) && any (strcmpi (allowed, Dist)))) error ("%s: 'Distance' must be one of %s, or a function handle.", ... caller, strjoin (strcat ("'", allowed, "'"), ', ')); endif j = find (strcmpi (allowed, Dist), 1); d = allowed{j}; endfunction ## A distance given as a function handle is tried before it is trusted, as ## pdist tries one, so that a handle that cannot answer is caught before any ## observation is drawn. function h = limeCheckHandle (Dist, X, caller) if (rows (X) < 2) h = Dist; return; endif out = []; try out = Dist (X(1,:), X(2:end,:)); catch error ("%s: 'Distance' is not a usable function handle.", caller); end_try_catch if (! isequal (size (out), [rows(X) - 1, 1])) error (strcat ("%s: 'Distance' must answer with one column holding", ... " one distance per observation."), caller); endif h = Dist; endfunction ## How far each drawn observation lies from the query point. function d = limeDistance (S, q, opts, cat, caller) Dist = opts.Distance; if (isempty (Dist)) if (isempty (cat)) Dist = 'euclidean'; else Dist = 'goodall3'; endif endif if (is_function_handle (Dist)) d = Dist (q, S); if (! isequal (size (d), [rows(S), 1])) error (strcat ("%s: 'Distance' must answer with one column holding", ... " one distance per observation."), caller); endif d = double (d); return; endif switch (Dist) case 'goodall3' d = limeNominal (S, q, cat, 'goodall3'); case 'ofd' d = limeNominal (S, q, cat, 'of'); case 'seuclidean' d = limeNamed (S, q, 'seuclidean', opts.Scale); case 'mahalanobis' d = limeNamed (S, q, 'mahalanobis', opts.Cov); case 'minkowski' d = limeNamed (S, q, 'minkowski', opts.P); otherwise d = pdist2 (S, q, Dist); endswitch d = double (d(:)); endfunction ## A named distance that carries a parameter of its own. function d = limeNamed (S, q, name, par) if (isempty (par)) d = pdist2 (S, q, name); else d = pdist2 (S, q, name, par); endif endfunction ## The two measures for levels, those of nomdist with the query point ## counted into the frequencies, as MATLAB counts it. A predictor holding ## numbers scores one less its difference over its range, the query point ## included. The distance is one less the mean similarity, which for OF is ## D / (1 + D) of nomdist's own 1 / S - 1, as MATLAB describes it. function d = limeNominal (S, q, cat, name) K = columns (S); s = zeros (rows (S), 1); if (! isempty (cat)) D = nomdist2 (S(:,cat), q(cat), name, 'CountQuery', true); if (strcmp (name, 'goodall3')) s = numel (cat) * (1 - D); else s = numel (cat) ./ (1 + D); endif endif num = setdiff (1:K, cat); if (! isempty (num)) R = range ([S(:,num); q(num)], 1); R(R == 0) = 1; s += sum (1 - abs (S(:,num) - q(num)) ./ R, 2); endif d = 1 - s / K; endfunction ## The weight a drawn observation carries, which falls away with distance ## once the distances are put on a common scale. function w = limeWeights (d, scale, kw) if (! (scale > 0)) w = ones (size (d)); return; endif w = exp (-0.5 * (d ./ scale ./ kw) .^ 2); endfunction ## The scale distances are put on: the square root of the number of ## predictors for the two measures for levels, as MATLAB takes it, and ## otherwise how far apart the two furthest drawn observations lie, which ## belongs to the draw and not to the query point. function m = limeScale (S, opts, cat, caller) Dist = opts.Distance; if (isempty (Dist) && ! isempty (cat)) Dist = 'goodall3'; endif if (ischar (Dist) && any (strcmp (Dist, {'goodall3', 'ofd'}))) m = sqrt (columns (S)); else m = limeSpread (S, opts, cat, caller); endif endfunction ## How far apart the two furthest drawn observations lie. A draw of levels ## repeats itself, so only its distinct rows are ever the furthest apart and ## the frequencies still come from the whole of it; a draw of numbers is ## taken a block at a time, so that a large one is never held as a full ## matrix of pairs. function m = limeSpread (S, opts, cat, caller) n = rows (S); m = 0; if (n < 2) return; endif if (! isempty (cat) || is_function_handle (opts.Distance)) if (! isempty (cat)) U = unique (S, 'rows'); else U = S; endif for k = 1:rows (U) d = limeDistance (S, U(k,:), opts, cat, caller); m = max (m, max (d)); endfor return; endif Dist = opts.Distance; if (isempty (Dist)) Dist = 'euclidean'; endif step = max (1, min (n, floor (2e6 / n))); for i = 1:step:n j = min (i + step - 1, n); switch (Dist) case 'seuclidean' d = limeNamed (S, S(i:j,:), 'seuclidean', opts.Scale); case 'mahalanobis' d = limeNamed (S, S(i:j,:), 'mahalanobis', opts.Cov); case 'minkowski' d = limeNamed (S, S(i:j,:), 'minkowski', opts.P); otherwise d = pdist2 (S, S(i:j,:), Dist); endswitch m = max (m, max (d(:))); endfor endfunction ## The observations as the columns a linear model is fitted on: a predictor ## holding levels becomes one column per level of REF other than the query ## point's, each saying whether the observation carries that level. function [A, grp] = limeExpand (Z, cat, ref, q) M = columns (Z); A = zeros (rows (Z), 0); grp = []; for j = 1:M if (any (cat == j)) lev = unique (ref(:,j)); lev(lev == q(j)) = []; for l = lev(:)' A = [A, double(Z(:,j) == l)]; grp = [grp, j]; endfor else A = [A, double(Z(:,j))]; grp = [grp, j]; endif endfor endfunction ## Group orthogonal matching pursuit: take the predictor whose columns ## explain most of what is left unexplained, fit again over everything taken ## so far, and stop where nothing is left to explain. function sel = limeOMP (A, y, w, grp, k) sw = sqrt (w(:)); Aw = A .* sw; yw = double (y(:)) .* sw; one = sw; den = one' * one; proj = @(v) v - one * ((one' * v) / den); r = proj (yw); cand = unique (grp); sel = []; for step = 1:k best = 0; bestval = 0; for g = cand(:)' if (any (sel == g)) continue; endif B = proj (Aw(:, grp == g)); nb = norm (B, 'fro'); if (! (nb > 0)) continue; endif val = norm (B * (pinv (B) * r)); if (val > bestval) bestval = val; best = g; endif endfor if (best == 0 || bestval <= 1e-12 * max (1, norm (r))) break; endif sel(end+1) = best; S = [one, Aw(:, ismember (grp, sel))]; r = yw - S * (S \ yw); if (norm (r) <= 1e-12 * max (1, norm (yw))) break; endif endfor sel = sort (sel); endfunction ## What the simple model answers at the query point, put through the same ## coding the model was fitted on. function v = limeSimpleFitted (this, sel, q, bbf) if (any (strcmp (class (this.SimpleModel), ... {'RegressionTree', 'ClassificationTree'}))) z = q(sel); else catsel = find (ismember (sel, this.CategoricalPredictors)); z = limeExpand (q(sel), catsel, this.SyntheticData(:,sel), q(sel)); endif v = predict (this.SimpleModel, z); if (strcmp (this.Type, 'regression')) v = double (v(1)); else v = v(1); endif endfunction ## Whether the simple model is a tree or a weighted sum. function t = limeModelType (this) if (any (strcmp (class (this.SimpleModel), ... {'RegressionTree', 'ClassificationTree'}))) t = 'tree'; else t = 'linear'; endif endfunction ## One of a short list of words, or the default where none was given. function v = limeCheckWord (val, allowed, caller, name, dflt) if (isempty (val)) v = dflt; return; endif if (! (ischar (val) && isrow (val) && any (strcmpi (allowed, val)))) error ("%s: '%s' must be one of %s.", caller, name, ... strjoin (strcat ("'", allowed, "'"), ', ')); endif j = find (strcmpi (allowed, val), 1); v = allowed{j}; endfunction ## A count, or the default where none was given. function v = limeCheckCount (val, caller, name, dflt) if (isempty (val)) v = dflt; return; endif if (! (isnumeric (val) && isscalar (val) && isreal (val) && isfinite (val) && val == fix (val) && val > 0)) error ("%s: '%s' must be a positive integer.", caller, name); endif v = double (val); endfunction ## One observation to explain. function q = limeCheckPoint (val, M, caller, pnames, lev) ## A table is read by the names the explainer holds, as X was if (istable (val)) val = tableToMatrix (pnames, lev, val, caller); endif if (! (isnumeric (val) && isreal (val) && isvector (val) && numel (val) == M)) error (strcat ("%s: the query point must be a real numeric vector", ... " of %d predictors."), caller, M); endif q = double (val(:)'); endfunction %!demo %! ## Explain a model fitted from a table %! %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! Mdl = fitrtree (T, 'SL'); %! %! ## The observations and the query point may be tables too, read by the %! ## names the model was fitted on rather than by the order of the columns %! ex = lime (Mdl, T(:, [1, 2, 3, 5]), 'NumSyntheticData', 2000, ... %! 'QueryPoint', T(1, [5, 3, 2, 1]), 'NumImportantPredictors', 2); %! ex.ImportantPredictors' %! ex.SimpleModel.Beta' %! %! ## A column the model was not fitted on is passed over, so the whole %! ## table, response and all, may be handed over as it stands %! ex2 = fit (ex, T(1,:), 2); %! ex2.ImportantPredictors' ## The explanation, measured on R2024a 2026-09-22. Every fixture takes a ## function handle as the model: a fitted model would make the comparison ## test that model rather than this one. %!test # the observations are drawn when the explainer is built %! X = [1, 10; 2, 20; 3, 30; 4, 45; 5, 50; 6, 65]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2); %! L = lime (f, X, 'Type', 'regression', 'NumSyntheticData', 200); %! assert_equal (size (L.SyntheticData), [200, 2]); %! assert_equal (size (L.Fitted), [200, 1]); %! assert_equal (isempty (L.SimpleModel), true); %! assert_equal (isempty (L.QueryPoint), true); %!test # the drawn observations follow the observations they were fitted to %! rand ('seed', 42); %! randn ('seed', 42); %! X = [randn(400,1) * 2 + 5, randn(400,1) * 0.5 - 1]; %! f = @(Z) Z(:,1); %! L = lime (f, X, 'Type', 'regression', 'NumSyntheticData', 4000); %! assert_equal (mean (L.SyntheticData), mean (X), 0.4); %! assert_equal (std (L.SyntheticData), std (X), 0.4); ## MATLAB parity: the coefficients over five kernel widths %!test %! X = [0, 0, 0; 1, 0, 0; 2, 0, 0; 3, 0, 0; 4, 0, 0; 5, 0, 0; 0, 1, 0; ... %! 0, 2, 0; 0, 3, 0; 0, 0, 1; 0, 0, 2; 0, 0, 3; 1, 1, 1; 2, 2, 2; ... %! 3, 3, 3; 4, 4, 4; 5, 5, 5; 1, 2, 3; 3, 2, 1; 2, 3, 1]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.5 * Z(:,3) .^ 2; %! L = lime (f, X, 'Type', 'regression', 'CustomSyntheticData', X); %! a = fit (L, [0, 0, 0], 3, 'KernelWidth', 0.1, 'BetaTolerance', 1e-12); %! assert_equal (a.SimpleModel.Beta', ... %! [1.992780427426674, -3.006957351974981, ... %! 0.691563828807785], 1e-5); %! assert_equal (a.SimpleModel.Bias, -0.022034064280291, 1e-5); %! b = fit (L, [0, 0, 0], 3, 'KernelWidth', 1, 'BetaTolerance', 1e-12); %! assert_equal (b.SimpleModel.Beta', ... %! [2.211849727686012, -2.799187539649991, ... %! 1.725666244878087], 1e-5); %! assert_equal (b.SimpleModel.Bias, -0.941458529287273, 1e-5); ## MATLAB parity: a query point outside the drawn set, which is what says ## the weights are scaled by the spread of the draw rather than by the ## distance to the furthest observation from the query %!test %! X = [0, 0, 0; 1, 0, 0; 2, 0, 0; 3, 0, 0; 4, 0, 0; 5, 0, 0; 0, 1, 0; ... %! 0, 2, 0; 0, 3, 0; 0, 0, 1; 0, 0, 2; 0, 0, 3; 1, 1, 1; 2, 2, 2; ... %! 3, 3, 3; 4, 4, 4; 5, 5, 5; 1, 2, 3; 3, 2, 1; 2, 3, 1]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.5 * Z(:,3) .^ 2; %! L = lime (f, X, 'Type', 'regression', 'CustomSyntheticData', X); %! a = fit (L, [0.5, 0.5, 0.5], 3, 'BetaTolerance', 1e-12); %! assert_equal (a.SimpleModel.Beta', ... %! [2.197589328509646, -2.816490786994012, ... %! 1.701570881013631], 1e-5); %! assert_equal (a.SimpleModel.Bias, -0.887925297450557, 1e-5); ## MATLAB parity: which predictors the pursuit takes, and in what order %!test %! X = [0, 0, 0; 1, 0, 0; 2, 0, 0; 3, 0, 0; 4, 0, 0; 5, 0, 0; 0, 1, 0; ... %! 0, 2, 0; 0, 3, 0; 0, 0, 1; 0, 0, 2; 0, 0, 3; 1, 1, 1; 2, 2, 2; ... %! 3, 3, 3; 4, 4, 4; 5, 5, 5; 1, 2, 3; 3, 2, 1; 2, 3, 1]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.5 * Z(:,3) .^ 2; %! L = lime (f, X, 'Type', 'regression', 'CustomSyntheticData', X); %! a = fit (L, [0, 0, 0], 1, 'BetaTolerance', 1e-12); %! assert_equal (a.ImportantPredictors, 1); %! assert_equal (a.SimpleModel.Beta, 1.849047756469483, 1e-5); %! b = fit (L, [0, 0, 0], 2, 'BetaTolerance', 1e-12); %! assert_equal (b.ImportantPredictors', [1, 2]); %! assert_equal (b.SimpleModel.Beta', ... %! [2.280838361112699, -1.990784316697158], 1e-5); %!test # the simple model is fitted on the important predictors alone %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2); %! L = lime (f, X, 'Type', 'regression', 'CustomSyntheticData', X); %! a = fit (L, [3, 30, 120], 2); %! assert_equal (numel (a.SimpleModel.Beta), 2); %! assert_equal (a.NumImportantPredictors, 2); %!test # a linear model is explained by its own weights %! X = [1, 10; 2, 20; 3, 28; 4, 45; 5, 50; 6, 65; 2.5, 25; 3.5, 35]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 7; %! L = lime (f, X, 'Type', 'regression', 'CustomSyntheticData', X); %! a = fit (L, [3, 28], 2, 'BetaTolerance', 1e-12); %! assert_equal (a.SimpleModel.Beta', [2, -3], 1e-6); %! assert_equal (a.SimpleModel.Bias, 7, 1e-5); %! assert_equal (a.SimpleModelFitted, a.BlackboxFitted, 1e-5); %!test # a classifier is separated from every other class at once %! load fisheriris %! Mdl = fitcknn (meas, species); %! S = [meas(1:6,:); meas(51:56,:); meas(101:106,:)]; %! L = lime (Mdl, 'CustomSyntheticData', S); %! assert_equal (L.Type, 'classification'); %! a = fit (L, meas(1,:), 2); %! assert_equal (class (a.SimpleModel), 'ClassificationLinear'); %! assert_equal (a.SimpleModel.ClassNames', [-1, 1]); %! assert_equal (a.BlackboxFitted, {'setosa'}); %!test # a tree may stand in for the weighted sum %! X = [1, 10; 2, 20; 3, 28; 4, 45; 5, 50; 6, 65; 2.5, 25; 3.5, 35]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2); %! L = lime (f, X, 'Type', 'regression', 'CustomSyntheticData', X); %! a = fit (L, [3, 28], 2, 'SimpleModelType', 'tree'); %! assert_equal (class (a.SimpleModel), 'RegressionTree'); %!test # the draw may be taken around the query point rather than over all %! load fisheriris %! X = meas(:,2:4); %! f = @(Z) Z(:,1); %! L = lime (f, X, 'Type', 'regression', 'DataLocality', 'local', ... %! 'NumNeighbors', 20, 'NumSyntheticData', 500, ... %! 'QueryPoint', X(1,:)); %! assert_equal (L.DataLocality, 'local'); %! assert_equal (size (L.SyntheticData), [500, 3]); %! idx = knnsearch (X, X(1,:), 'K', 20); %! assert_equal (mean (L.SyntheticData), mean (X(idx,:)), 0.35); %!test # observations may be given outright rather than drawn %! X = [1, 10; 2, 20; 3, 28; 4, 45]; %! S = [1.5, 15; 2.5, 25; 3.5, 35]; %! f = @(Z) Z(:,1); %! L = lime (f, X, 'Type', 'regression', 'CustomSyntheticData', S); %! assert_equal (L.SyntheticData, S); %! assert_equal (L.NumSyntheticData, 3); %! assert_equal (L.Fitted, S(:,1)); %!test # a predictor holding levels becomes one column per level but one %! C = [1, 1; 1, 2; 2, 1; 2, 2; 3, 1; 3, 2; 1, 1; 2, 2]; %! f = @(Z) 5 * (Z(:,1) == 3) + Z(:,2); %! L = lime (f, C, 'Type', 'regression', 'CategoricalPredictors', [1, 2], ... %! 'CustomSyntheticData', C); %! a = fit (L, [1, 1], 1); %! assert_equal (numel (a.SimpleModel.Beta), 2); %!test # the two measures for levels answer differently %! C = [1, 1, 1; 1, 1, 2; 1, 1, 3; 1, 2, 1; 1, 2, 2; 2, 1, 1; 2, 1, 2; ... %! 2, 2, 3; 3, 1, 1; 3, 2, 2; 1, 1, 1; 1, 1, 2; 1, 2, 3; 2, 1, 1; ... %! 1, 1, 1; 1, 1, 1; 3, 2, 3; 2, 2, 1]; %! f = @(Z) 5 * ((Z(:,2) == 1) & (Z(:,3) == 1)) + 0.5 * Z(:,1); %! L = lime (f, C, 'Type', 'regression', ... %! 'CategoricalPredictors', [1, 2, 3], 'CustomSyntheticData', C); %! a = fit (L, [1, 1, 1], 3, 'Distance', 'goodall3'); %! b = fit (L, [1, 1, 1], 3, 'Distance', 'ofd'); %! assert_equal (isequal (a.SimpleModel.Beta, b.SimpleModel.Beta), false); %! c = fit (L, [1, 1, 1], 3); %! assert_equal (c.SimpleModel.Beta, a.SimpleModel.Beta); %!test # a distance given as a function handle is ours, not MATLAB's %! X = [1, 10; 2, 20; 3, 28; 4, 45; 5, 50; 6, 65]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2); %! L = lime (f, X, 'Type', 'regression', 'CustomSyntheticData', X); %! a = fit (L, [3, 28], 2, 'Distance', @(q, Z) sum (abs (Z - q), 2)); %! b = fit (L, [3, 28], 2, 'Distance', 'cityblock'); %! assert_equal (a.SimpleModel.Beta, b.SimpleModel.Beta, 1e-8); %!test # plot draws a bar per column of the model and gives the figure %! X = [1, 10; 2, 20; 3, 28; 4, 45; 5, 50; 6, 65]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2); %! L = lime (f, X, 'Type', 'regression', 'CustomSyntheticData', X); %! a = fit (L, [3, 28], 2); %! ## plot opens a figure of its own, kept off screen through the default %! vis = get (0, 'DefaultFigureVisible'); %! set (0, 'DefaultFigureVisible', 'off'); %! h = []; %! unwind_protect %! h = plot (a); %! assert_equal (strcmp (get (h, 'type'), 'figure'), true); %! ax = findobj (h, 'type', 'axes'); %! assert_equal (get (get (ax, 'title'), 'string'), ... %! 'LIME with Linear Model'); %! assert_equal (get (get (ax, 'xlabel'), 'string'), 'Coefficient'); %! assert_equal (get (get (ax, 'ylabel'), 'string'), 'Predictor'); %! unwind_protect_cleanup %! close (h); %! set (0, 'DefaultFigureVisible', vis); %! end_unwind_protect %!test # a tree is titled after what it is, over its predictor importance %! X = [1, 10; 2, 20; 3, 28; 4, 45; 5, 50; 6, 65]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2); %! L = lime (f, X, 'Type', 'regression', 'CustomSyntheticData', X); %! a = fit (L, [3, 28], 2, 'SimpleModelType', 'tree'); %! ## plot opens a figure of its own, kept off screen through the default %! vis = get (0, 'DefaultFigureVisible'); %! set (0, 'DefaultFigureVisible', 'off'); %! h = []; %! unwind_protect %! h = plot (a); %! ax = findobj (h, 'type', 'axes'); %! assert_equal (get (get (ax, 'title'), 'string'), ... %! 'LIME with Decision Tree Model'); %! assert_equal (get (get (ax, 'xlabel'), 'string'), 'Predictor Importance'); %! unwind_protect_cleanup %! close (h); %! set (0, 'DefaultFigureVisible', vis); %! end_unwind_protect %!test # 'P' reaches the distance that takes it %! X = [1, 10; 2, 20; 3, 28; 4, 45; 5, 50; 6, 65; 2.5, 25; 3.5, 35]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.01 * Z(:,2) .^ 2; %! L = lime (f, X, 'Type', 'regression', 'CustomSyntheticData', X); %! a = fit (L, [3, 28], 2, 'Distance', 'minkowski', 'P', 3); %! same = @(q, Z) sum (abs (Z - q) .^ 3, 2) .^ (1/3); %! b = fit (L, [3, 28], 2, 'Distance', same); %! assert_equal (a.SimpleModel.Beta, b.SimpleModel.Beta, 1e-8); %! c = fit (L, [3, 28], 2, 'Distance', 'minkowski', 'P', 1); %! assert_equal (isequal (a.SimpleModel.Beta, c.SimpleModel.Beta), false); %!test # 'Scale' reaches the distance that takes it %! X = [1, 10; 2, 20; 3, 28; 4, 45; 5, 50; 6, 65; 2.5, 25; 3.5, 35]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.01 * Z(:,2) .^ 2; %! L = lime (f, X, 'Type', 'regression', 'CustomSyntheticData', X); %! sc = [2, 20]; %! a = fit (L, [3, 28], 2, 'Distance', 'seuclidean', 'Scale', sc); %! same = @(q, Z) sqrt (sum (((Z - q) ./ sc) .^ 2, 2)); %! b = fit (L, [3, 28], 2, 'Distance', same); %! assert_equal (a.SimpleModel.Beta, b.SimpleModel.Beta, 1e-8); %! c = fit (L, [3, 28], 2, 'Distance', 'seuclidean'); %! assert_equal (isequal (a.SimpleModel.Beta, c.SimpleModel.Beta), false); %!test # 'Cov' reaches the distance that takes it %! X = [1, 10; 2, 20; 3, 28; 4, 45; 5, 50; 6, 65; 2.5, 25; 3.5, 35]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.01 * Z(:,2) .^ 2; %! L = lime (f, X, 'Type', 'regression', 'CustomSyntheticData', X); %! CV = [4, 1; 1, 400]; %! a = fit (L, [3, 28], 2, 'Distance', 'mahalanobis', 'Cov', CV); %! same = @(q, Z) sqrt (sum (((Z - q) / CV) .* (Z - q), 2)); %! b = fit (L, [3, 28], 2, 'Distance', same); %! assert_equal (a.SimpleModel.Beta, b.SimpleModel.Beta, 1e-8); %! c = fit (L, [3, 28], 2, 'Distance', 'mahalanobis'); %! assert_equal (isequal (a.SimpleModel.Beta, c.SimpleModel.Beta), false); ## MATLAB parity: a predictor that adds nothing is left out, so fewer are ## used than were asked for. The response is one no weighted sum can fit ## exactly, so the pursuit stops on the third predictor being worthless ## rather than on there being nothing left to explain. %!test %! X = [1, 9, 0; 2, 3, 0; 3, 7, 0; 4, 1, 0; 5, 8, 0; 6, 2, 0; 7, 5, 0; ... %! 8, 4, 0]; %! f = @(Z) Z(:,1) .^ 2 - 0.5 * Z(:,2); %! L = lime (f, X, 'Type', 'regression', 'CustomSyntheticData', X); %! a = fit (L, [3, 7, 0], 3); %! assert_equal (a.NumImportantPredictors, 3); %! assert_equal (a.ImportantPredictors', [1, 2]); %! assert_equal (numel (a.SimpleModel.Beta), 2); %!test # the pursuit stops where there is nothing left to explain %! X = [1, 9, 0; 2, 3, 0; 3, 7, 0; 4, 1, 0; 5, 8, 0; 6, 2, 0]; %! f = @(Z) 3 * Z(:,1); %! L = lime (f, X, 'Type', 'regression', 'CustomSyntheticData', X); %! a = fit (L, [3, 7, 0], 3); %! assert_equal (a.ImportantPredictors, 1); %!test # a classifier is drawn as its simple model, one bar per column %! load fisheriris %! Mdl = fitcknn (meas, species); %! S = [meas(1:8,:); meas(51:58,:); meas(101:108,:)]; %! a = fit (lime (Mdl, 'CustomSyntheticData', S), meas(1,:), 2); %! ## plot opens a figure of its own, kept off screen through the default %! vis = get (0, 'DefaultFigureVisible'); %! set (0, 'DefaultFigureVisible', 'off'); %! h = []; %! unwind_protect %! h = plot (a); %! ax = findobj (h, 'type', 'axes'); %! assert_equal (get (get (ax, 'title'), 'string'), ... %! 'LIME with Linear Model'); %! assert_equal (numel (get (ax, 'yticklabel')), ... %! numel (a.SimpleModel.Beta)); %! unwind_protect_cleanup %! close (h); %! set (0, 'DefaultFigureVisible', vis); %! end_unwind_protect ## Input validation %!error lime () %!error ... %! lime (42) %!error ... %! lime (@(Z) Z(:,1)) %!error ... %! lime (@(Z) Z(:,1), [1, 2; 3, 4]) %!error ... %! lime (@(Z) Z(:,1), {1, 2}, 'Type', 'regression') %!error ... %! lime (@(Z) Z(:,1), [1, 2; 3, 4], 'Type', 'regression', 'UseParallel', true) %!error ... %! lime (@(Z) Z(:,1), [1, 2; 3, 4], 'Type', 'regression', ... %! 'DataLocality', 'nearby') %!error ... %! lime (@(Z) Z(:,1), [1, 2; 3, 4], 'Type', 'regression', ... %! 'NumSyntheticData', 0) %!error ... %! lime (@(Z) Z(:,1), [1, 2; 3, 4], 'Type', 'regression', 'KernelWidth', 2) %!error ... %! lime (@(Z) Z(:,1), [1, 2; 3, 4], 'Type', 'regression', ... %! 'SimpleModelType', 'forest') %!error ... %! lime (@(Z) Z(:,1), [1, 2; 3, 4], 'Type', 'regression', ... %! 'CustomSyntheticData', [1, 2, 3]) %!error ... %! lime (@(Z) Z(:,1), [1, 2; 3, 4], 'Type', 'regression', 'NoSuchThing', 1) %!error ... %! lime (@(Z) Z(:,1), [1, 2; 3, 4], 'Type', 'regression', ... %! 'Distance', 'goodall3') %!error ... %! lime (@(Z) Z(:,1), [1, 2; 3, 4], 'Type', 'regression', ... %! 'Distance', @(a) a) ## The fixture needs three observations: over two, the one distance the ## trial call should answer with is a scalar, which a scalar satisfies %!error %! lime (@(Z) Z(:,1), [1, 2; 3, 4; 5, 6], 'Type', 'regression', ... %! 'Distance', @(a, B) 5) %!error ... %! fit (lime (@(Z) Z(:,1), [1, 2; 3, 4], 'Type', 'regression'), [1, 2]) %!error ... %! fit (lime (@(Z) Z(:,1), [1, 2; 3, 4], 'Type', 'regression'), [1, 2], 0) %!error ... %! fit (lime (@(Z) Z(:,1), [1, 2; 3, 4], 'Type', 'regression'), [1, 2], 5) %!error ... %! fit (lime (@(Z) Z(:,1), [1, 2; 3, 4], 'Type', 'regression'), [1, 2, 3], 1) %!error ... %! plot (lime (@(Z) Z(:,1), [1, 2; 3, 4], 'Type', 'regression')) ## Table input %!shared ltT, ltM %! load fisheriris %! ltT = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! ltT.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! ltM = fitrtree (ltT, 'SL'); %!test # the observations the draw is fitted to may be a table %! L = lime (ltM, ltT(:,[1, 2, 3, 5]), 'NumSyntheticData', 200); %! assert_equal (size (L.X), [150, 4]); %! assert_equal (size (L.SyntheticData), [200, 4]); %!test # the drawn observations may be given outright as a table %! L = lime (ltM, ltT(:,[1, 2, 3, 5]), ... %! 'CustomSyntheticData', ltT(1:30,[1, 2, 3, 5])); %! assert_equal (size (L.SyntheticData), [30, 4]); ## MATLAB parity: a table is read by the names the model was fitted on, so ## the order of its columns does not matter %!test %! L = lime (ltM, ltT(:,[1, 2, 3, 5]), ... %! 'CustomSyntheticData', ltT(1:30,[1, 2, 3, 5])); %! a = fit (L, ltT(1,[1, 2, 3, 5]), 2); %! b = fit (L, ltT(1,[5, 3, 2, 1]), 2); %! assert_equal (b.ImportantPredictors, a.ImportantPredictors); %! assert_equal (b.SimpleModel.Beta, a.SimpleModel.Beta, 1e-12); ## MATLAB parity: a column the model was not fitted on is passed over %!test %! L = lime (ltM, ltT(:,[1, 2, 3, 5]), ... %! 'CustomSyntheticData', ltT(1:30,[1, 2, 3, 5])); %! a = fit (L, ltT(1,[1, 2, 3, 5]), 2); %! b = fit (L, ltT(1,:), 2); %! assert_equal (b.SimpleModel.Beta, a.SimpleModel.Beta, 1e-12); %!test # a query point given at building is read the same way %! L = lime (ltM, ltT(:,[1, 2, 3, 5]), ... %! 'CustomSyntheticData', ltT(1:30,[1, 2, 3, 5]), ... %! 'QueryPoint', ltT(1,[5, 3, 2, 1]), ... %! 'NumImportantPredictors', 2); %! assert_equal (numel (L.ImportantPredictors), 2); %! assert_equal (size (L.QueryPoint), [1, 4]); ## A function handle names no predictors, so a table given for one names ## them itself and its columns are taken in the order they come %!test %! f = @(Z) Z(:,1); %! L = lime (f, ltT(:,1:3), 'Type', 'regression', ... %! 'CustomSyntheticData', ltT(1:30,1:3), ... %! 'QueryPoint', ltT(1,1:3), 'NumImportantPredictors', 1); %! assert_equal (L.SimpleModel.PredictorNames, {'SW'}); %!error ... %! lime (ltM, ltT(:,[1, 3, 5])) %!error ... %! fit (lime (ltM, ltT(:,[1, 2, 3, 5]), 'NumSyntheticData', 50), ... %! ltT(1,[1, 3, 5]), 2) ## Predictors holding levels, measured on R2024a 2026-09-29: a function ## handle as the model and the same drawn observations in both engines %!shared lgX, lgS, lgf %! lgX = [1, 1, 1; 1, 3, 2; 1, 2, 2; 1, 2, 1; 2, 2, 3; 1, 2, 3; 1, 2, 3; ... %! 2, 2, 1; 1, 1, 3; 1, 2, 1; 1, 3, 1; 1, 3, 3; 2, 2, 1; 1, 3, 3; ... %! 1, 2, 1; 1, 2, 3; 1, 3, 1; 1, 1, 3; 2, 3, 1; 2, 2, 1; 3, 2, 3; ... %! 1, 2, 3; 1, 3, 3; 1, 2, 3; 1, 2, 3; 1, 2, 1; 3, 2, 3; 1, 3, 3; ... %! 3, 2, 3; 1, 3, 1; 1, 2, 1; 1, 3, 1; 2, 2, 1; 1, 2, 3; 1, 2, 1; ... %! 2, 3, 1; 1, 2, 1; 3, 1, 3; 3, 3, 3; 1, 2, 3]; %! lgS = [2, 3, 2; 2, 1, 2; 1, 1, 2; 1, 3, 3; 2, 1, 1; 1, 1, 1; 3, 2, 2; ... %! 3, 3, 1; 3, 3, 2; 3, 3, 2; 3, 1, 1; 3, 2, 3; 3, 1, 1; 3, 3, 1; ... %! 1, 1, 2; 2, 3, 2; 1, 1, 2; 2, 1, 2; 2, 1, 3; 1, 3, 2; 2, 1, 2; ... %! 2, 1, 2; 1, 3, 1; 2, 1, 1; 2, 1, 2; 1, 3, 2; 1, 1, 2; 1, 1, 2; ... %! 1, 1, 2; 1, 2, 3; 2, 2, 3; 3, 1, 2; 3, 3, 1; 2, 1, 3; 2, 1, 2; ... %! 3, 1, 1; 3, 3, 2; 1, 2, 2; 1, 3, 2; 2, 1, 2; 1, 3, 3; 1, 3, 2; ... %! 3, 1, 3; 2, 1, 2; 2, 1, 1; 2, 1, 2; 3, 3, 2; 2, 1, 2; 1, 3, 2; ... %! 2, 1, 2; 2, 1, 2; 2, 3, 2; 3, 2, 1; 1, 3, 2; 3, 1, 3; 3, 1, 3; ... %! 1, 3, 2; 3, 2, 3; 3, 2, 2; 3, 3, 1]; %! lgf = @(Z) 3 * (Z(:,1) == 1) .* (Z(:,2) == 2) + (Z(:,3) == 3) ... %! + 0.5 * Z(:,1) .* (Z(:,3) == 1); %!test %! L = lime (lgf, lgX, 'Type', 'regression', 'CategoricalPredictors', ... %! [1, 2, 3], 'CustomSyntheticData', lgS); %! a = fit (L, [3, 1, 2], 3, 'Distance', 'goodall3', 'KernelWidth', 0.25); %! assert_equal (a.SimpleModel.Beta', [0.121118041142843, ... %! -0.000600506617868, 0.398788365579973, 0.021938631288382, ... %! 1.340450289008853, 1.006911791276333], 1e-4); %!test %! L = lime (lgf, lgX, 'Type', 'regression', 'CategoricalPredictors', ... %! [1, 2, 3], 'CustomSyntheticData', lgS); %! a = fit (L, [3, 1, 2], 3, 'KernelWidth', 0.75); %! assert_equal (a.SimpleModel.Beta', [0.228175170664997, ... %! 0.013506333846389, 0.753704128127034, -0.009766750012839, ... %! 1.230617170587539, 0.995575992275549], 1e-4); %!test %! ## Ours; MATLAB's undocumented rule gives [0.177564664766370, %! ## 0.005664618556746, 0.676928812402028, 0.004396374156961, %! ## 1.246265361445370, 0.993126581893467] %! L = lime (lgf, lgX, 'Type', 'regression', 'CategoricalPredictors', ... %! [1, 2, 3], 'CustomSyntheticData', lgS); %! a = fit (L, [3, 1, 2], 3, 'Distance', 'ofd', 'KernelWidth', 0.25); %! assert_equal (a.SimpleModel.Beta', [0.174628345510863, ... %! 0.00526956351961691, 0.671359079482785, ... %! 0.0052078819336837, 1.24767221788603, ... %! 0.993112744621723], 1e-6); %!test # each coefficient is read against the query point's level %! C = [1, 1; 2, 1; 3, 1; 1, 2; 2, 2; 3, 2; 3, 1; 1, 2]; %! f = @(Z) 5 * (Z(:,1) == 3); %! L = lime (f, C, 'Type', 'regression', 'CategoricalPredictors', [1, 2], ... %! 'CustomSyntheticData', C); %! a = fit (L, [3, 1], 1); %! assert_equal (a.ImportantPredictors, 1); %! assert_equal (a.SimpleModel.Beta', [-5, -5], 1e-3); %!test # a predictor held at the query point's level throughout is not taken %! C = [2, 1; 2, 2; 2, 3; 2, 1; 2, 2; 2, 3]; %! f = @(Z) Z(:,2); %! L = lime (f, C, 'Type', 'regression', 'CategoricalPredictors', [1, 2], ... %! 'CustomSyntheticData', C); %! a = fit (L, [2, 1], 2); %! assert_equal (a.ImportantPredictors, 2); %!error ... %! fit (lime (@(Z) Z(:,1), [1, 1; 1, 1], 'Type', 'regression', ... %! 'CategoricalPredictors', [1, 2]), [1, 1], 1) statistics-release-2.1.2/inst/Machine_Learning/partialDependence.m000066400000000000000000000403261527311104300252530ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{pd} =} partialDependence (@var{Mdl}, @var{Vars}) ## @deftypefnx {statistics} {@var{pd} =} partialDependence (@var{Mdl}, @var{Vars}, @var{Labels}) ## @deftypefnx {statistics} {@var{pd} =} partialDependence (@dots{}, @var{Data}) ## @deftypefnx {statistics} {@var{pd} =} partialDependence (@var{fun}, @var{Vars}, @var{Data}) ## @deftypefnx {statistics} {@var{pd} =} partialDependence (@dots{}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{pd}, @var{x}, @var{y}] =} partialDependence (@dots{}) ## ## Compute partial dependence. ## ## @code{@var{pd} = partialDependence (@var{Mdl}, @var{Vars})} returns the ## partial dependence of the response of the regression model @var{Mdl} on the ## predictors named by @var{Vars}, averaged over the observations @var{Mdl} ## was fitted on. @var{Vars} names one predictor or two, by column index or ## by name, and a model that does not keep its observations must be given ## them as @var{Data}. ## ## @code{@var{pd} = partialDependence (@var{Mdl}, @var{Vars}, @var{Labels})} ## does the same for a classification model, averaging the score of each class ## named by @var{Labels} rather than a response. @var{Labels} is required for ## such a model and refused for any other. ## ## @code{@var{pd} = partialDependence (@var{fun}, @var{Vars}, @var{Data})} ## takes a function handle in place of a model. @var{fun} is called with a ## matrix of observations and answers with one row for each, and @var{Data} is ## then required. ## ## @var{pd} is a @math{1xnumX} vector for a regression model varying one ## predictor and a @math{numYxnumX} matrix for two, where @var{numX} and ## @var{numY} count the query points of the first and second. For a ## classification model it gains a leading dimension of one row per class, ## giving @math{numxnumX} and @math{numxnumYxnumX}. ## ## @var{x} and @var{y} hold the query points of the first and the second ## predictor, @var{y} empty where only one was named. Where a predictor is ## categorical they are its levels. ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## ## @item @qcode{'QueryPoints'} @tab @tab The points to answer at, as a vector ## for one predictor and, for two, either a matrix of one column each or a ## cell holding a vector for each, which is how they may differ in length. ## The default is 100 points evenly spaced between the smallest and the ## largest value the predictor takes among the observations averaged over, ## and the levels themselves where it is categorical. ## ## @item @qcode{'NumObservationsToSample'} @tab @tab How many observations to ## draw, without replacement, from those averaged over. The default is all of ## them, and so is a number reaching or exceeding how many there are. The ## default query points span what was drawn. ## ## @item @qcode{'CategoricalPredictors'} @tab @tab The predictors whose values ## are levels, taken as by every learner of this package. It applies only to ## a function handle, a model being asked for its own. ## ## @item @qcode{'IncludeInteractions'} @tab @tab Whether the interaction terms ## of a generalized additive model are included. It applies only to such a ## model, and the default is the model's own. ## ## @item @qcode{'IncludeIntercept'} @tab @tab Whether the intercept of a ## generalized additive model is included, @code{true} by default. Excluding ## it takes the intercept off the result. ## ## @item @qcode{'OutputColumns'} @tab @tab Which of the columns a function ## handle answers with are wanted, as indices or as @qcode{'all'}, which is ## the default. It applies only to a function handle. ## @end multitable ## ## @code{'UseParallel'} and @code{'PredictionForMissingValue'} are not ## implemented and are refused rather than ignored. ## ## @seealso{plotPartialDependence, PredictiveModel} ## @end deftypefn function [pd, x, y] = partialDependence (Mdl, Vars, varargin) if (nargin < 2) error ("partialDependence: too few input arguments."); endif [pd, x, y] = pdCompute ('partialDependence', Mdl, Vars, varargin{:}); endfunction %!shared pdX, pdYr, pdYc, pdY3, pdQ, pdLo, pdHi %! x1 = repmat ([1;2;3], 8, 1); %! x2 = reshape (repmat (1:8, 3, 1), 24, 1); %! pdX = [x1, x2]; %! pdYr = 10 * x2 + x1; %! pdYc = (x2 > 4) + 1; %! pdY3 = repmat ({'a'; 'b'; 'c'}, 8, 1); %! pdQ = [1; 2; 3]; %! lo1 = repmat ([1;2;3], 4, 1); %! one = ones (12, 1); %! pdLo = [lo1, one]; %! pdHi = [lo1, 8 * one]; ## MATLAB parity: the values of a linear model, whose fit we reproduce exactly %!test %! Mdl = fitlm (pdX, pdYr); %! assert_equal (partialDependence (Mdl, 1, 'QueryPoints', pdQ), ... %! [46, 47, 48], 1e-10); %!test # and over the observations it is handed rather than its own %! Mdl = fitlm (pdX, pdYr); %! assert_equal (partialDependence (Mdl, 1, pdLo, 'QueryPoints', pdQ), ... %! [11, 12, 13], 1e-10); %!test # MATLAB parity: two variables give one row per point of the second %! Mdl = fitlm (pdX, pdYr); %! [pd, x, y] = partialDependence (Mdl, [1, 2], 'QueryPoints', {pdQ, [2;5]}); %! assert_equal (pd, [21, 22, 23; 51, 52, 53], 1e-10); %! assert_equal (size (pd), [2, 3]); %! assert_equal (x', [1, 2, 3]); %! assert_equal (y', [2, 5]); ## MATLAB parity: a tree answers over the distribution it was fitted on %!test %! Mdl = fitrtree (pdX, pdYr); %! assert_equal (partialDependence (Mdl, 1, 'QueryPoints', pdQ), ... %! [47, 47, 47], 1e-10); %!test # and does not move when the observations it is handed are replaced %! Mdl = fitrtree (pdX, pdYr); %! lo = partialDependence (Mdl, 1, pdLo, 'QueryPoints', pdQ); %! hi = partialDependence (Mdl, 1, pdHi, 'QueryPoints', pdQ); %! assert_equal (lo, hi); %! assert_equal (lo, [47, 47, 47], 1e-10); %!test # a compact tree keeps no observations and answers just the same %! Mdl = compact (fitrtree (pdX, pdYr)); %! assert_equal (partialDependence (Mdl, 1, pdLo, 'QueryPoints', pdQ), ... %! [47, 47, 47], 1e-10); %!test # the traversal equals averaging the tree over its training data %! Mdl = fitrtree (pdX, pdYr); %! pd = partialDependence (Mdl, 1, 'QueryPoints', pdQ); %! avg = zeros (1, 3); %! for k = 1:3 %! Z = pdX; %! Z(:,1) = pdQ(k); %! avg(k) = mean (predict (Mdl, Z)); %! endfor %! assert_equal (pd, avg, 1e-10); ## MATLAB parity: a categorical predictor is answered at its levels %!test %! xc = repmat ([1;2;3;4], 6, 1); %! x2 = reshape (repmat (1:8, 3, 1), 24, 1); %! y = 10 * (xc == 1) + 20 * (xc == 2) + 30 * (xc == 3) + 40 * (xc == 4) + x2; %! Mdl = fitrtree ([xc, x2], y, 'CategoricalPredictors', 1); %! [pd, x] = partialDependence (Mdl, 1); %! assert_equal (x', [1, 2, 3, 4]); %! assert_equal (pd, [14, 73/3, 104/3, 45], 1e-10); %!test # and takes its levels whatever query points are asked for %! xc = repmat ([1;2;3;4], 6, 1); %! x2 = reshape (repmat (1:8, 3, 1), 24, 1); %! y = 10 * (xc == 1) + 20 * (xc == 2) + 30 * (xc == 3) + 40 * (xc == 4) + x2; %! Mdl = fitrtree ([xc, x2], y, 'CategoricalPredictors', 1); %! [a, ax] = partialDependence (Mdl, 1); %! [b, bx] = partialDependence (Mdl, 1, 'QueryPoints', [1; 2]); %! assert_equal (a, b); %! assert_equal (ax, bx); ## MATLAB parity: a generalized additive model answers over its own %!test %! Mdl = fitrgam (pdX, pdYr); %! lo = partialDependence (Mdl, 1, pdLo, 'QueryPoints', pdQ); %! hi = partialDependence (Mdl, 1, pdHi, 'QueryPoints', pdQ); %! assert_equal (lo, hi); %! assert_equal (lo, [46, 47, 48], 1e-6); %!test # excluding the intercept takes it off the result %! Mdl = fitrgam (pdX, pdYr); %! with = partialDependence (Mdl, 1, 'QueryPoints', pdQ); %! without = partialDependence (Mdl, 1, 'QueryPoints', pdQ, ... %! 'IncludeIntercept', false); %! assert_equal (with - Mdl.Intercept, without, 1e-10); ## MATLAB parity: a bagged ensemble is answered over its trees, a boosted ## one over the observations %!test %! Mdl = fitrensemble (pdX, pdYr, 'Method', 'Bag', 'NumLearningCycles', 5); %! lo = partialDependence (Mdl, 1, pdLo, 'QueryPoints', pdQ); %! hi = partialDependence (Mdl, 1, pdHi, 'QueryPoints', pdQ); %! assert_equal (lo, hi); %!test # a compacted bagged ensemble carries no Method and is still bagged %! Mdl = compact (fitrensemble (pdX, pdYr, 'Method', 'Bag', ... %! 'NumLearningCycles', 5)); %! lo = partialDependence (Mdl, 1, pdLo, 'QueryPoints', pdQ); %! hi = partialDependence (Mdl, 1, pdHi, 'QueryPoints', pdQ); %! assert_equal (lo, hi); %!test # a boosted ensemble moves with the observations it is handed %! Mdl = fitrensemble (pdX, pdYr, 'Method', 'LSBoost', 'NumLearningCycles', 5); %! lo = partialDependence (Mdl, 1, pdLo, 'QueryPoints', pdQ); %! hi = partialDependence (Mdl, 1, pdHi, 'QueryPoints', pdQ); %! assert_equal (isequal (lo, hi), false); ## Shapes %!test # a regression model varying one predictor gives a row %! Mdl = fitrsvm (pdX, pdYr); %! assert_equal (size (partialDependence (Mdl, 1, 'QueryPoints', pdQ)), [1, 3]); %!test # a classifier gives one row per class named %! Mdl = fitctree (pdX, pdY3); %! pd3 = partialDependence (Mdl, 1, {'a', 'b', 'c'}, 'QueryPoints', pdQ); %! pd1 = partialDependence (Mdl, 1, 'b', 'QueryPoints', pdQ); %! assert_equal (size (pd3), [3, 3]); %! assert_equal (size (pd1), [1, 3]); %!test # and two predictors add a dimension between them %! Mdl = fitctree (pdX, pdY3); %! pd = partialDependence (Mdl, [1, 2], {'a', 'b'}, ... %! 'QueryPoints', {pdQ, [2; 5]}); %! assert_equal (size (pd), [2, 2, 3]); %!test # MATLAB parity: the rows follow the order the classes were named in %! Mdl = fitctree (pdX, pdY3); %! abc = partialDependence (Mdl, 1, {'a', 'b', 'c'}, 'QueryPoints', pdQ); %! ca = partialDependence (Mdl, 1, {'c', 'a'}, 'QueryPoints', pdQ); %! assert_equal (ca(1,:), abc(3,:)); %! assert_equal (ca(2,:), abc(1,:)); ## Query points and sampling %!test # MATLAB parity: a hundred points spanning what the observations hold %! Mdl = fitrsvm (pdX, pdYr); %! [~, x] = partialDependence (Mdl, 1); %! assert_equal (numel (x), 100); %! assert_equal ([min(x), max(x)], [1, 3]); %!test # the query points come from the observations handed over %! Mdl = fitrtree (pdX, pdYr); %! wide = [repmat([10;15;20], 4, 1), 4 * ones(12, 1)]; %! [~, x] = partialDependence (Mdl, 1, wide); %! assert_equal ([min(x), max(x)], [10, 20]); %!test # a second output is empty where only one predictor was named %! Mdl = fitrsvm (pdX, pdYr); %! [~, ~, y] = partialDependence (Mdl, 1, 'QueryPoints', pdQ); %! assert_equal (isempty (y), true); %!test # sampling draws from the observations and the answer stays finite %! Mdl = fitrsvm (pdX, pdYr); %! pd = partialDependence (Mdl, 1, 'QueryPoints', pdQ, ... %! 'NumObservationsToSample', 6); %! assert_equal (size (pd), [1, 3]); %! assert_equal (all (isfinite (pd)), true); %!test # asking for more observations than there are takes all of them %! Mdl = fitrsvm (pdX, pdYr); %! a = partialDependence (Mdl, 1, 'QueryPoints', pdQ); %! b = partialDependence (Mdl, 1, 'QueryPoints', pdQ, ... %! 'NumObservationsToSample', 500); %! assert_equal (a, b); ## Naming the predictors %!test # a predictor may be named rather than indexed %! Mdl = fitrsvm (pdX, pdYr, 'PredictorNames', {'a', 'b'}); %! assert_equal (partialDependence (Mdl, 'a', 'QueryPoints', pdQ), ... %! partialDependence (Mdl, 1, 'QueryPoints', pdQ)); %!test # and two may be, in a cellstr %! Mdl = fitrsvm (pdX, pdYr, 'PredictorNames', {'a', 'b'}); %! byname = partialDependence (Mdl, {'a', 'b'}, 'QueryPoints', {pdQ, [2;5]}); %! byidx = partialDependence (Mdl, [1, 2], 'QueryPoints', {pdQ, [2;5]}); %! assert_equal (byname, byidx); ## A function handle in place of a model %!test %! f = @(Z) 2 * Z(:,1) + Z(:,2); %! assert_equal (partialDependence (f, 1, pdX, 'QueryPoints', pdQ), ... %! [2 + 4.5, 4 + 4.5, 6 + 4.5], 1e-10); %!test # and its columns may be picked %! f = @(Z) [Z(:,1), 2 * Z(:,1)]; %! pd = partialDependence (f, 1, pdX, 'QueryPoints', pdQ, ... %! 'OutputColumns', 2); %! assert_equal (pd, [2, 4, 6], 1e-10); ## The two ways of writing the call answer alike %!test %! Mdl = fitrsvm (pdX, pdYr); %! [a, ax, ay] = partialDependence (Mdl, 1, 'QueryPoints', pdQ); %! [b, bx, by] = Mdl.partialDependence (1, 'QueryPoints', pdQ); %! assert_equal (a, b); %! assert_equal (ax, bx); %! assert_equal (ay, by); ## Input validation %!error ... %! partialDependence (1) %!error ... %! partialDependence (42, 1) %!error ... %! partialDependence (fitctree (pdX, pdYc), 1) %!error ... %! partialDependence (fitrsvm (pdX, pdYr), 1, {'a'}) %!error ... %! partialDependence (fitctree (pdX, pdY3), 1, {'z'}, 'QueryPoints', pdQ) %!error ... %! partialDependence (compact (fitrsvm (pdX, pdYr)), 1) %!error ... %! partialDependence (compact (fitrgam (pdX, pdYr)), 1) %!error ... %! partialDependence (fitctree (pdX, pdYc), 1, 1, {1, 2}) %!error ... %! partialDependence (fitrsvm (pdX, pdYr), 5) %!error ... %! partialDependence (fitrsvm (pdX, pdYr), 'z') %!error ... %! partialDependence (fitrsvm (pdX, pdYr), [1, 1]) %!error ... %! partialDependence (fitrsvm (pdX, pdYr), [1, 2, 1]) %!error ... %! partialDependence (fitrsvm (pdX, pdYr), 1, 'UseParallel', true) %!error ... %! partialDependence (fitrsvm (pdX, pdYr), 1, 'PredictionForMissingValue', 0) %!error ... %! partialDependence (fitrsvm (pdX, pdYr), 1, 'IncludeInteractions', true) %!error ... %! partialDependence (fitrsvm (pdX, pdYr), 1, 'IncludeIntercept', false) %!error ... %! partialDependence (fitrsvm (pdX, pdYr), 1, 'CategoricalPredictors', 1) %!error ... %! partialDependence (fitrsvm (pdX, pdYr), 1, 'OutputColumns', 1) %!error ... %! partialDependence (fitrsvm (pdX, pdYr), 1, 'QueryPoints', ones (3, 3)) %!error ... %! partialDependence (fitrsvm (pdX, pdYr), [1, 2], 'QueryPoints', ones (3, 3)) %!error ... %! partialDependence (fitrsvm (pdX, pdYr), 1, 'NumObservationsToSample', 2.5) %!error ... %! partialDependence (@(Z) Z(:,1), 1) statistics-release-2.1.2/inst/Machine_Learning/plotPartialDependence.m000066400000000000000000000353531527311104300261160ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} plotPartialDependence (@var{Mdl}, @var{Vars}) ## @deftypefnx {statistics} {} plotPartialDependence (@var{Mdl}, @var{Vars}, @var{Labels}) ## @deftypefnx {statistics} {} plotPartialDependence (@dots{}, @var{Data}) ## @deftypefnx {statistics} {} plotPartialDependence (@var{fun}, @var{Vars}, @var{Data}) ## @deftypefnx {statistics} {} plotPartialDependence (@dots{}, @var{name}, @var{value}) ## @deftypefnx {statistics} {@var{ax} =} plotPartialDependence (@dots{}) ## ## Plot partial dependence and individual conditional expectation. ## ## @code{plotPartialDependence (@var{Mdl}, @var{Vars})} draws the partial ## dependence of the model @var{Mdl} on the predictors named by @var{Vars}: ## a line where one is named and a surface where two are. The arguments are ## those of @code{partialDependence}, which computes what is drawn, and ## @var{Labels} is required for a classification model in the same way. ## ## @var{ax} is the axes drawn into. ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## ## @item @qcode{'Conditional'} @tab @tab What to draw: ## @qcode{'none'} (default) draws the partial dependence alone; ## @qcode{'absolute'} draws a curve per observation, a marker where each one ## sits on its own curve, and the mean of those curves over them; ## @qcode{'centered'} draws the same with every curve shifted to start at ## zero. Only one predictor may be varied, and a classification model may ## name only one class. ## ## @item @qcode{'Parent'} @tab @tab The axes to draw into. The default is ## @code{gca}. ## @end multitable ## ## Every other name-value pair is passed to @code{partialDependence}; see ## there for @qcode{'QueryPoints'}, @qcode{'NumObservationsToSample'} and the ## rest. ## ## The line a conditional plot draws is the mean of the curves drawn with it, ## which is not always what @code{partialDependence} returns. A decision ## tree, an ensemble of bagged trees and a generalized additive model are ## answered over the distribution they were fitted on, while a curve belongs ## to one observation and must come from @code{predict}, so the two part ## company for those models whenever the observations are not the ones the ## model was fitted on. MATLAB R2024a draws it this way and so does this. ## ## @seealso{partialDependence, PredictiveModel} ## @end deftypefn function ax = plotPartialDependence (Mdl, Vars, varargin) if (nargin < 2) error ("plotPartialDependence: too few input arguments."); endif [pd, x, y, opts] = pdCompute ('plotPartialDependence', Mdl, Vars, ... varargin{:}); F = opts.Frame; cond = opts.Conditional; if (isempty (cond)) cond = 'none'; endif if (! (ischar (cond) && isrow (cond) && any (strcmpi (cond, {'none', 'absolute', 'centered'})))) error (strcat ("plotPartialDependence: 'Conditional' must be 'none',", ... " 'absolute' or 'centered'.")); endif cond = lower (cond); two = ! isempty (y); if (! strcmp (cond, 'none')) if (two) error (strcat ("plotPartialDependence: a conditional plot varies", ... " one predictor, not two.")); endif if (F.IsClass && numel (F.LabelIdx) != 1) error (strcat ("plotPartialDependence: a conditional plot of a", ... " classification model names one class.")); endif endif ax = opts.Parent; if (isempty (ax)) ax = gca (); elseif (! (isscalar (ax) && ishghandle (ax) && isaxes (ax))) error ("plotPartialDependence: 'Parent' must be an axes handle."); endif if (strcmp (cond, 'none')) pdPlotPlain (ax, F, pd, x, y, two); else pdPlotIce (ax, Mdl, F, pd, x, cond, opts.PredArgs); endif xlabel (ax, F.PredictorNames{F.Vars(1)}); if (two) ylabel (ax, F.PredictorNames{F.Vars(2)}); zlabel (ax, pdResponseLabel (Mdl, F)); else ylabel (ax, pdResponseLabel (Mdl, F)); endif if (strcmp (cond, 'none')) title (ax, 'Partial Dependence Plot'); else title (ax, 'Individual Conditional Expectation Plot'); endif endfunction ## The partial dependence on its own: a line per class, or a surface. function pdPlotPlain (ax, F, pd, x, y, two) if (two) surf (ax, x, y, pd); return; endif if (! F.IsClass) plot (ax, x, pd(1,:)); return; endif k = numel (F.LabelIdx); co = get (ax, 'ColorOrder'); hold (ax, 'on'); names = cell (1, k); for j = 1:k plot (ax, x, pd(j,:), 'Color', co(mod (j - 1, rows (co)) + 1, :)); names{j} = pdClassName (F, j); endfor hold (ax, 'off'); if (k > 1) legend (ax, names{:}); endif endfunction ## The curves, a marker where each observation sits, and their mean over them. function pdPlotIce (ax, Mdl, F, pd, x, cond, predArgs) ice = pdIce (Mdl, F, predArgs); ice = ice(:,:,1); at = pdScoreAt (Mdl, F, predArgs); if (strcmp (cond, 'centered')) off = ice(:,1); ice -= off; at -= off; endif hold (ax, 'on'); for i = 1:rows (ice) plot (ax, x, ice(i,:), 'Color', [0.5, 0.5, 0.5]); endfor scatter (ax, F.X(:,F.Vars(1)), at, 'o'); plot (ax, x, mean (ice, 1), 'Color', [1, 0, 0]); hold (ax, 'off'); endfunction ## What the model answers for each observation as it stands. function s = pdScoreAt (Mdl, F, predArgs) if (is_function_handle (Mdl)) s = Mdl (F.X); if (! isempty (F.OutCols)) s = s(:, F.OutCols); endif s = s(:,1); elseif (F.IsClass) [~, sc] = predict (Mdl, F.X, predArgs{:}); s = sc(:, F.LabelIdx(1)); else s = predict (Mdl, F.X, predArgs{:}); s = s(:); endif endfunction ## What the vertical axis measures. function txt = pdResponseLabel (Mdl, F) if (F.IsClass) if (numel (F.LabelIdx) == 1) txt = sprintf ('Score of class %s', pdClassName (F, 1)); else txt = 'Scores'; endif return; endif txt = 'Y'; if (! is_function_handle (Mdl) && any (strcmp (properties (Mdl), 'ResponseName')) && ! isempty (Mdl.ResponseName)) r = Mdl.ResponseName; if (ischar (r) && isrow (r)) txt = r; endif endif endfunction ## One class name as text, whatever type ClassNames holds it in. function txt = pdClassName (F, j) lab = labelsFromIndex (F.ClassNames, F.LabelIdx(j)); if (iscellstr (lab)) txt = lab{1}; elseif (ischar (lab)) txt = lab; elseif (isnumeric (lab) || islogical (lab)) txt = num2str (lab); else txt = char (lab); if (! isrow (txt)) txt = txt(1,:); endif endif endfunction %!demo %! ## The partial dependence of a fitted tree on one predictor. The line is %! ## what the tree answers on average as that predictor is varied over its %! ## range, the others left as the data holds them. %! load fisheriris %! Mdl = fitrtree (meas(:,2:4), meas(:,1)); %! plotPartialDependence (Mdl, 1); %!demo %! ## The same tree, with one curve per observation. Each grey curve is what %! ## the model answers for a single flower as the predictor is varied; the %! ## red line is their mean and the circles mark where each flower sits. %! load fisheriris %! Mdl = fitrtree (meas(:,2:4), meas(:,1)); %! plotPartialDependence (Mdl, 1, 'Conditional', 'absolute'); %!demo %! ## Two predictors give a surface, and a classification model is told which %! ## class to answer for. %! load fisheriris %! Mdl = fitcsvm (meas(51:end,3:4), species(51:end)); %! plotPartialDependence (Mdl, [1, 2], 'versicolor'); %!shared ppX, ppYr, ppY3, ppQ, ppLo %! x1 = repmat ([1;2;3], 4, 1); %! x2 = reshape (repmat (1:4, 3, 1), 12, 1); %! ppX = [x1, x2]; %! ppYr = 10 * x2 + x1; %! ppY3 = repmat ({'a'; 'b'; 'c'}, 4, 1); %! ppQ = [1; 2; 3]; %! lo1 = repmat ([1;2;3], 2, 1); %! ppLo = [lo1, ones(6, 1)]; ## MATLAB parity: the plain plot draws what partialDependence returns %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! Mdl = fitrsvm (ppX, ppYr); %! ax = plotPartialDependence (Mdl, 1, 'QueryPoints', ppQ); %! ch = get (ax, 'children'); %! assert_equal (numel (ch), 1); %! assert_equal (get (ch(1), 'ydata'), ... %! partialDependence (Mdl, 1, 'QueryPoints', ppQ), 1e-12); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # two predictors give one surface, sized by the second then the first %! hf = figure ('visible', 'off'); %! unwind_protect %! Mdl = fitrsvm (ppX, ppYr); %! ax = plotPartialDependence (Mdl, [1, 2], 'QueryPoints', {ppQ, [2;4]}); %! ch = get (ax, 'children'); %! assert_equal (get (ch(1), 'type'), 'surface'); %! assert_equal (size (get (ch(1), 'zdata')), [2, 3]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a classifier draws one line per class named %! hf = figure ('visible', 'off'); %! unwind_protect %! Mdl = fitctree (ppX, ppY3); %! ax = plotPartialDependence (Mdl, 1, {'a', 'b'}, 'QueryPoints', ppQ); %! assert_equal (numel (get (ax, 'children')), 2); %! assert_equal (get (get (ax, 'ylabel'), 'string'), 'Scores'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # MATLAB parity: one class names the axis after it %! hf = figure ('visible', 'off'); %! unwind_protect %! Mdl = fitctree (ppX, ppY3); %! ax = plotPartialDependence (Mdl, 1, 'a', 'QueryPoints', ppQ, ... %! 'Conditional', 'absolute'); %! assert_equal (get (get (ax, 'ylabel'), 'string'), 'Score of class a'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a conditional plot draws a curve per observation, a marker, a line %! hf = figure ('visible', 'off'); %! unwind_protect %! Mdl = fitrsvm (ppX, ppYr); %! ax = plotPartialDependence (Mdl, 1, 'QueryPoints', ppQ, ... %! 'Conditional', 'absolute'); %! ch = get (ax, 'children'); %! assert_equal (numel (ch), rows (ppX) + 2); %! assert_equal (sum (arrayfun (@(h) isprop (h, 'cdata'), ch)), 1); %! ## Octave's scatter builds an hggroup under the gnuplot toolkit %! if (! strcmp (graphics_toolkit (), 'gnuplot')) %! assert_equal (sum (strcmp (get (ch, 'type'), 'scatter')), 1); %! endif %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # MATLAB parity: centering shifts every curve to start at zero %! hf = figure ('visible', 'off'); %! unwind_protect %! Mdl = fitrsvm (ppX, ppYr); %! ax = plotPartialDependence (Mdl, 1, 'QueryPoints', ppQ, ... %! 'Conditional', 'centered'); %! ch = get (ax, 'children'); %! for k = 1:numel (ch) %! if (strcmp (get (ch(k), 'type'), 'line')) %! yd = get (ch(k), 'ydata'); %! assert_equal (yd(1), 0, 1e-12); %! endif %! endfor %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## MATLAB parity: the line of a conditional plot is the mean of the curves it ## draws, which for a tree over other observations is not what ## partialDependence returns %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! Mdl = fitrtree (ppX, ppYr); %! ax = plotPartialDependence (Mdl, 1, ppLo, 'QueryPoints', ppQ, ... %! 'Conditional', 'absolute'); %! ch = get (ax, 'children'); %! red = []; %! grey = []; %! for k = 1:numel (ch) %! if (! strcmp (get (ch(k), 'type'), 'line')) %! continue; %! endif %! if (isequal (get (ch(k), 'color'), [1, 0, 0])) %! red = get (ch(k), 'ydata'); %! else %! grey(end+1,:) = get (ch(k), 'ydata'); %! endif %! endfor %! assert_equal (red, mean (grey, 1), 1e-12); %! assert_equal (red, [17, 17, 17], 1e-12); %! assert_equal (partialDependence (Mdl, 1, ppLo, 'QueryPoints', ppQ), ... %! [27, 27, 27], 1e-12); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # the titles say which plot it is %! hf = figure ('visible', 'off'); %! unwind_protect %! Mdl = fitrsvm (ppX, ppYr); %! ax = plotPartialDependence (Mdl, 1, 'QueryPoints', ppQ); %! assert_equal (get (get (ax, 'title'), 'string'), ... %! 'Partial Dependence Plot'); %! clf (hf); %! ax = plotPartialDependence (Mdl, 1, 'QueryPoints', ppQ, ... %! 'Conditional', 'centered'); %! assert_equal (get (get (ax, 'title'), 'string'), ... %! 'Individual Conditional Expectation Plot'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # the horizontal axis is named after the predictor varied %! hf = figure ('visible', 'off'); %! unwind_protect %! Mdl = fitrsvm (ppX, ppYr, 'PredictorNames', {'alpha', 'beta'}); %! ax = plotPartialDependence (Mdl, 'beta', 'QueryPoints', [2;4]); %! assert_equal (get (get (ax, 'xlabel'), 'string'), 'beta'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # MATLAB parity: Parent draws into the axes given and returns it %! hf = figure ('visible', 'off'); %! unwind_protect %! a1 = subplot (1, 2, 1); %! a2 = subplot (1, 2, 2); %! Mdl = fitrsvm (ppX, ppYr); %! ax = plotPartialDependence (Mdl, 1, 'QueryPoints', ppQ, 'Parent', a2); %! assert_equal (ax, a2); %! assert_equal (isempty (get (a2, 'children')), false); %! assert_equal (isempty (get (a1, 'children')), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Input validation %!error ... %! plotPartialDependence (1) %!error ... %! plotPartialDependence (fitrsvm (ppX, ppYr), [1, 2], ... %! 'Conditional', 'absolute') %!error ... %! plotPartialDependence (fitctree (ppX, ppY3), 1, {'a', 'b'}, ... %! 'Conditional', 'absolute') %!error ... %! plotPartialDependence (fitrsvm (ppX, ppYr), 1, 'Conditional', 'sideways') %!error ... %! plotPartialDependence (fitrsvm (ppX, ppYr), 1, 'Parent', 42) %!error ... %! partialDependence (fitrsvm (ppX, ppYr), 1, 'Conditional', 'absolute') statistics-release-2.1.2/inst/Machine_Learning/private/000077500000000000000000000000001527311104300231335ustar00rootroot00000000000000statistics-release-2.1.2/inst/Machine_Learning/private/autoKernelScale.m000066400000000000000000000035471527311104300264030ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{sigma} =} autoKernelScale (@var{X}) ## ## The kernel scale a kernel learner resolves @qcode{'auto'} to. ## ## @end deftypefn ## The kernel scale MATLAB calls 'auto'. MathWorks documents a heuristic ## that subsamples and warns that its estimates vary from one call to the ## next, so no particular number is the parity target. This one is the ## median distance between observations, which is the usual choice and is ## reproducible: a kernel that wide puts about half the pairs inside one ## bandwidth of each other. Beyond a thousand observations the median is ## taken over a random thousand of them, the full matrix being quadratic. function sigma = autoKernelScale (X) n = rows (X); if (n > 1000) X = X(randperm (n, 1000), :); n = 1000; endif if (n < 2) sigma = 1; return; endif d = zeros (n * (n - 1) / 2, 1); at = 0; for i = 1:n-1 k = n - i; d(at+1:at+k) = sqrt (sum ((X(i+1:end,:) - X(i,:)) .^ 2, 2)); at += k; endfor sigma = median (d); if (! (sigma > 0)) sigma = 1; endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/bagArgs.m000066400000000000000000000111271527311104300246610ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{o}, @var{errmsg}] =} bagArgs (@var{args}, @var{T}, @var{N}, @var{allowed}) ## ## Parse the Name-Value arguments of a bagged ensemble's prediction methods. ## ## @var{args} holds the pairs, @var{T} is the number of trees, @var{N} the ## number of observations, and @var{allowed} names the arguments the calling ## method takes, from @qcode{'Mode'}, @qcode{'Trees'}, @qcode{'TreeWeights'}, ## @qcode{'UseInstanceForTree'} and @qcode{'Weights'}. ## ## @var{o} has the fields @qcode{trees}, the indices of the trees to use; ## @qcode{tw}, a weight for each of them; @qcode{use}, an @math{NxT} logical ## matrix over those trees saying which may answer for which observation; ## @qcode{w}, the observation weights or empty; and @qcode{mode}. ## @var{errmsg} is the body of the message the caller should raise, or empty. ## ## @end deftypefn function [o, errmsg] = bagArgs (args, T, N, allowed) errmsg = ""; o = struct ('trees', 1:T, 'tw', [], 'use', [], 'w', [], ... 'mode', 'cumulative'); if (mod (numel (args), 2) != 0) errmsg = "name-value arguments must be in pairs."; return; endif tw = []; for i = 1:2:numel (args) name = args{i}; val = args{i+1}; if (! (ischar (name) && any (strcmpi (name, allowed)))) errmsg = "invalid parameter name in optional pair arguments."; return; endif switch (tolower (name)) case 'trees' if (ischar (val) && strcmpi (val, 'all')) o.trees = 1:T; elseif (isnumeric (val) && isvector (val) && isreal (val) && all (val >= 1) && all (val <= T) && all (val == fix (val))) o.trees = double (val(:)'); else errmsg = strcat ("'Trees' must be 'all' or a vector of", ... " indices of trees in the ensemble."); return; endif case 'treeweights' tw = val; case 'useinstancefortree' if (! (islogical (val) && isequal (size (val), [N, T]))) errmsg = strcat ("'UseInstanceForTree' must be a logical", ... " matrix with one row per observation and", ... " one column per tree."); return; endif o.use = val; case 'weights' errmsg = weightsClass (val); if (! isempty (errmsg)) return; endif if (! (isnumeric (val) && isvector (val) && isreal (val) && numel (val) == N && all (val >= 0) && sum (val) > 0)) errmsg = strcat ("'Weights' must be a nonnegative numeric", ... " vector with one element per observation,", ... " not all zero."); return; endif o.w = double (val(:)); case 'mode' if (! (ischar (val) && any (strcmpi (val, {'cumulative', 'individual', ... 'ensemble'})))) errmsg = strcat ("'Mode' must be 'cumulative', 'individual'", ... " or 'ensemble'."); return; endif o.mode = tolower (val); endswitch endfor if (isempty (tw)) o.tw = ones (1, numel (o.trees)); else if (strcmp (o.mode, 'individual')) errmsg = "'TreeWeights' cannot be used in 'individual' mode."; return; endif if (! (isnumeric (tw) && isvector (tw) && isreal (tw) && numel (tw) == numel (o.trees) && all (tw >= 0) && sum (tw) > 0)) errmsg = strcat ("'TreeWeights' must be a nonnegative numeric", ... " vector with one element per tree used, not all", ... " zero."); return; endif o.tw = double (tw(:)'); endif if (isempty (o.use)) o.use = true (N, numel (o.trees)); else o.use = o.use(:, o.trees); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/bagCombine.m000066400000000000000000000045201527311104300253400ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{A}, @var{SD}, @var{none}] =} bagCombine (@var{P}, @var{use}, @var{tw}, @var{mode}) ## ## Weighted mean and standard deviation of a bagged ensemble's tree outputs. ## ## @var{P} is the @math{NxKxT} array @code{bagTreeOutputs} returns, @var{use} ## an @math{NxT} logical matrix of which tree may answer for which ## observation, and @var{tw} a weight for each tree. @var{mode} is ## @qcode{'ensemble'}, averaging over every tree at once, @qcode{'cumulative'}, ## averaging over the first @math{t} trees for each @math{t}, or ## @qcode{'individual'}, taking each tree on its own. ## ## @var{A} and @var{SD} are @math{NxKxL}, with @math{L} one for ## @qcode{'ensemble'} and @math{T} otherwise. @var{SD} is the weighted ## population standard deviation over the trees used. @var{none} is an ## @math{NxL} logical matrix marking the observations no tree could answer ## for, whose entries of @var{A} are not numbers. ## ## @end deftypefn function [A, SD, none] = bagCombine (P, use, tw, mode) [N, K, T] = size (P); W3 = reshape (double (use) .* tw, N, 1, T); switch (mode) case 'ensemble' den = sum (W3, 3); num = sum (P .* W3, 3); sq = sum ((P .^ 2) .* W3, 3); case 'cumulative' den = cumsum (W3, 3); num = cumsum (P .* W3, 3); sq = cumsum ((P .^ 2) .* W3, 3); otherwise den = W3; num = P .* W3; sq = (P .^ 2) .* W3; endswitch none = reshape (den == 0, N, size (den, 3)); A = num ./ den; SD = sqrt (max (sq ./ den - A .^ 2, 0)); SD(isnan (A)) = NaN; endfunction statistics-release-2.1.2/inst/Machine_Learning/private/bagLeaves.m000066400000000000000000000030731527311104300252050ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{L} =} bagLeaves (@var{M}, @var{X}, @var{trees}) ## ## Node each observation comes to rest at in each tree of a bagged ensemble. ## ## @var{M} is a @code{TreeBagger} or @code{CompactTreeBagger} object, @var{X} ## the predictors and @var{trees} the indices of the trees to ask. @var{L} is ## an @math{NxT} matrix holding, for each observation and tree, the number of ## the node the tree's @code{predict} brings it to. ## ## @end deftypefn function L = bagLeaves (M, X, trees) L = zeros (rows (X), numel (trees)); isclass = strcmp (M.Method, 'classification'); for j = 1:numel (trees) if (isclass) [~, ~, L(:,j)] = predict (M.Trees{trees(j)}, X); else [~, L(:,j)] = predict (M.Trees{trees(j)}, X); endif endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/bagLoss.m000066400000000000000000000110061527311104300247010ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{out} =} bagLoss (@var{what}, @var{M}, @var{X}, @var{Y}, @var{args}, @var{classname}, @var{fixedUse}, @var{W0}) ## ## Error, margin or mean margin of a bagged ensemble. ## ## @var{what} is @qcode{'error'}, @qcode{'margin'} or @qcode{'meanMargin'}. ## @var{M} is a @code{TreeBagger} or @code{CompactTreeBagger} object, @var{X} ## and @var{Y} the data, @var{args} the Name-Value pairs of the call and ## @var{classname} the class and method named in the error messages. ## @var{fixedUse}, when not empty, is the out-of-bag matrix, and ## @qcode{'UseInstanceForTree'} may not then be given. @var{W0} weighs the ## observations when no @qcode{'Weights'} are given, and is empty for uniform ## weights. ## ## The error is the weighted share of misclassified observations, or the ## weighted mean squared error, over the observations that have a prediction. ## An observation no tree may answer for takes @code{DefaultYfit} with the ## prior as its scores; when @code{DefaultYfit} is the missing label it has no ## prediction and is left out. The error is a column with one element per ## step in @qcode{'cumulative'} and @qcode{'individual'} mode and a scalar in ## @qcode{'ensemble'} mode; the margin is @math{NxL} and the mean margin ## @math{1xL}. ## ## @end deftypefn function out = bagLoss (what, M, X, Y, args, classname, fixedUse, W0) isclass = strcmp (M.Method, 'classification'); if (! isclass && ! strcmp (what, 'error')) error ("%s: margins are defined only for classification.", classname); endif if (! (isnumeric (X) && isreal (X) && ismatrix (X))) error ("%s: X must be a real numeric matrix.", classname); endif if (columns (X) != numel (M.PredictorNames)) error ("%s: X must have one column per predictor.", classname); endif N = rows (X); if (rows (Y) != N) error ("%s: X and Y must have the same number of rows.", classname); endif allowed = {'Mode', 'Trees', 'TreeWeights'}; if (isempty (fixedUse)) allowed{end+1} = 'UseInstanceForTree'; endif if (! strcmp (what, 'margin')) allowed{end+1} = 'Weights'; endif [o, errmsg] = bagArgs (args, M.NumTrees, N, allowed); if (! isempty (errmsg)) error ("%s: %s", classname, errmsg); endif if (! isempty (fixedUse)) o.use = fixedUse(:, o.trees); endif if (! isempty (o.w)) w = o.w; elseif (! isempty (W0)) w = double (W0(:)); else w = ones (N, 1); endif P = bagTreeOutputs (M, X, o.trees); [A, ~, none] = bagCombine (P, o.use, o.tw, o.mode); L = columns (none); if (isclass) [gY, errmsg] = labelIndices (M.ClassNames, Y); if (! isempty (errmsg)) error ("%s: %s", classname, errmsg); endif gY = gY(:); K = columns (A); for l = 1:L A(none(:,l),:,l) = repmat (M.DefaultScore, sum (none(:,l)), 1); endfor if (strcmp (what, 'error')) [~, idx] = max (A, [], 2); idx = reshape (idx, N, L); idx(none) = M.DefaultIndex; valid = idx > 0; miss = double (idx != gY); out = sum (w .* miss .* valid, 1) ./ sum (w .* valid, 1); out = out(:); else m = marginsOf (A, gY, L); if (M.DefaultIndex == 0) m(none) = NaN; endif if (strcmp (what, 'margin')) out = m; else have = ! isnan (m); m(! have) = 0; out = sum (w .* m .* have, 1) ./ sum (w .* have, 1); endif endif else if (! (isnumeric (Y) && isreal (Y) && isvector (Y))) error ("%s: Y must be a real numeric vector.", classname); endif y = double (Y(:)); Yh = reshape (A, N, L); Yh(none) = M.DefaultYfit; valid = ! (isnan (Yh) | isnan (y)); d = Yh - y; d(! valid) = 0; out = sum (w .* d .^ 2 .* valid, 1) ./ sum (w .* valid, 1); out = out(:); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/bagMds.m000066400000000000000000000061351527311104300245130ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{S}, @var{E}, @var{errmsg}] =} bagMds (@var{P}, @var{g}, @var{colors}, @var{coords}) ## ## Classical multidimensional scaling of a proximity matrix, with its plot. ## ## @var{P} is an @math{NxN} proximity matrix, scaled as the distances ## @code{1 - @var{P}}. @var{S} holds the scaled coordinates, one column per ## positive eigenvalue, and @var{E} the eigenvalues, as @code{cmdscale} ## returns them. ## ## @var{coords} holds two or three column indices of @var{S}, which must exist ## whether or not anything is drawn. @var{colors}, when not empty, is a ## character vector with one color letter per class, and those coordinates are ## drawn as overlaid scatter plots, one per class. @var{g} holds ## the class index of each observation, or is empty to draw every observation ## in the first color; a class beyond the number of letters is not drawn. ## @var{errmsg} is the body of the message the caller should raise, or empty. ## ## @end deftypefn function [S, E, errmsg] = bagMds (P, g, colors, coords) S = []; E = []; errmsg = ""; if (isa (colors, 'string') && isscalar (colors)) colors = char (colors); endif if (! (isempty (colors) || (ischar (colors) && isrow (colors)))) errmsg = "'Colors' must be a character vector or a string scalar."; return; endif if (! (isnumeric (coords) && isvector (coords) && isreal (coords) && any (numel (coords) == [2, 3]) && all (coords >= 1) && all (coords == fix (coords)))) errmsg = strcat ("'MDSCoordinates' must be a vector of two or three", ... " positive integers."); return; endif [S, E] = cmdscale (1 - P); if (any (coords > columns (S))) S = []; E = []; errmsg = strcat ("'MDSCoordinates' must not exceed the number of", ... " scaled coordinates."); return; endif if (isempty (colors)) return; endif if (isempty (g)) g = ones (rows (S), 1); endif held = ishold (); for k = 1:min (numel (colors), max ([0; g(! isnan (g))])) idx = g == k; if (numel (coords) == 2) plot (S(idx,coords(1)), S(idx,coords(2)), [colors(k), '.']); else plot3 (S(idx,coords(1)), S(idx,coords(2)), S(idx,coords(3)), ... [colors(k), '.']); endif hold on; endfor if (! held) hold off; endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/bagOutlier.m000066400000000000000000000037301527311104300254110ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{out} =} bagOutlier (@var{P}, @var{g}) ## ## Outlier measure of each observation from a proximity matrix. ## ## @var{P} is an @math{NxN} proximity matrix and @var{g} a class index for ## each observation, or empty to treat every observation as one class. The ## raw measure of an observation is the size of its class divided by the sum ## of its squared proximities to the observations of the class, itself ## included. @var{out} is the absolute deviation of the raw measure from the ## median of its class, divided by the median absolute deviation of the ## class. When that deviation is zero, @var{out} is the raw measure itself, ## and in a class of one or two observations it is zero. ## ## @end deftypefn function out = bagOutlier (P, g) N = rows (P); if (isempty (g)) g = ones (N, 1); endif g = g(:); P2 = P .^ 2; out = zeros (N, 1); for k = unique (g)' idx = g == k; n = sum (idx); if (n <= 2) continue; endif raw = n ./ sum (P2(idx,idx), 2); dev = abs (raw - median (raw)); mdev = median (dev); if (mdev == 0) out(idx) = raw; else out(idx) = dev / mdev; endif endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/bagPredict.m000066400000000000000000000055531527311104300253650ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{Yfit}, @var{S}, @var{SD}] =} bagPredict (@var{M}, @var{X}, @var{args}, @var{classname}, @var{fixedUse}) ## ## Predictions of a bagged ensemble, for @code{predict} and @code{oobPredict}. ## ## @var{M} is a @code{TreeBagger} or @code{CompactTreeBagger} object and ## @var{args} the Name-Value pairs of the call. @var{classname} names the ## class and method in the error messages. @var{fixedUse}, when not empty, is ## the out-of-bag matrix that decides which tree answers for which ## observation, and only @qcode{'Trees'} may then be given. ## ## For classification @var{Yfit} holds the labels, @var{S} the scores and ## @var{SD} their standard deviations over the trees. For regression ## @var{Yfit} holds the responses and @var{S} their standard deviations, and ## @var{SD} is empty. An observation no tree may answer for takes ## @code{DefaultYfit}, with the prior as its scores and no deviation. ## ## @end deftypefn function [Yfit, S, SD] = bagPredict (M, X, args, classname, fixedUse) if (! (isnumeric (X) && isreal (X) && ismatrix (X))) error ("%s: X must be a real numeric matrix.", classname); endif if (columns (X) != numel (M.PredictorNames)) error ("%s: X must have one column per predictor.", classname); endif if (isempty (fixedUse)) allowed = {'Trees', 'TreeWeights', 'UseInstanceForTree'}; else allowed = {'Trees'}; endif [o, errmsg] = bagArgs (args, M.NumTrees, rows (X), allowed); if (! isempty (errmsg)) error ("%s: %s", classname, errmsg); endif if (! isempty (fixedUse)) o.use = fixedUse(:, o.trees); endif P = bagTreeOutputs (M, X, o.trees); [A, SD, none] = bagCombine (P, o.use, o.tw, 'ensemble'); if (strcmp (M.Method, 'classification')) A(none,:) = repmat (M.DefaultScore, sum (none), 1); [~, idx] = max (A, [], 2); idx(none) = M.DefaultIndex; Yfit = missingLabels (M.ClassNames, rows (X)); have = idx > 0; Yfit(have,:) = labelsFromIndex (M.ClassNames, idx(have)); S = A; else A(none) = M.DefaultYfit; Yfit = A; S = SD; SD = []; endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/bagProximity.m000066400000000000000000000033101527311104300257640ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{P} =} bagProximity (@var{M}, @var{X}, @var{trees}, @var{NumPrint}) ## ## Proximity of the observations under a bagged ensemble. ## ## @var{M} is a @code{TreeBagger} or @code{CompactTreeBagger} object, @var{X} ## the predictors and @var{trees} the indices of the trees to use. @var{P} is ## the symmetric @math{NxN} matrix whose element @math{(i,j)} is the share of ## those trees that bring observations @math{i} and @math{j} to the same node. ## When @var{NumPrint} is given and positive, a line saying how many trees are ## done is printed after every that many trees. ## ## @end deftypefn function P = bagProximity (M, X, trees, NumPrint = 0) N = rows (X); T = numel (trees); P = zeros (N); for j = 1:T L = bagLeaves (M, X, trees(j)); P += L == L'; if (NumPrint > 0 && mod (j, NumPrint) == 0) printf ("Tree %d done.\n", j); endif endfor P /= T; endfunction statistics-release-2.1.2/inst/Machine_Learning/private/bagSplitStats.m000066400000000000000000000034401527311104300260760ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{dcrit}, @var{nsplit}] =} bagSplitStats (@var{Trees}, @var{p}) ## ## Split criterion contributions and split counts of a bagged ensemble. ## ## @var{Trees} is the cell array of compact trees and @var{p} the number of ## predictors. @var{dcrit} is the mean over the trees of each tree's ## @code{predictorImportance}. @var{nsplit} is the sum over the trees of the ## share of each tree's branch nodes that split on each predictor; a tree ## without branch nodes adds nothing to either. Both are @math{1xP}. ## ## @end deftypefn function [dcrit, nsplit] = bagSplitStats (Trees, p) dcrit = zeros (1, p); nsplit = zeros (1, p); T = numel (Trees); for t = 1:T tr = Trees{t}; branch = find (tr.Children(:,1) > 0); if (isempty (branch)) continue; endif dcrit += predictorImportance (tr); nsplit += accumarray (tr.CutPredictorIndex(branch)(:), 1, [p, 1])' ... / numel (branch); endfor if (T > 0) dcrit /= T; endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/bagTreeOutputs.m000066400000000000000000000034221527311104300262670ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{P} =} bagTreeOutputs (@var{M}, @var{X}, @var{trees}) ## ## What each tree of a bagged ensemble says about each observation. ## ## @var{M} is a @code{TreeBagger} or @code{CompactTreeBagger} object, @var{X} ## the predictors and @var{trees} the indices of the trees to ask. For a ## classification ensemble @var{P} is @math{NxKxT}, the scores of each tree ## laid out in the ensemble's class order, a class absent from a tree's bag ## scoring zero. For a regression ensemble @var{P} is @math{Nx1xT}, the ## predicted responses. ## ## @end deftypefn function P = bagTreeOutputs (M, X, trees) N = rows (X); T = numel (trees); if (strcmp (M.Method, 'classification')) P = zeros (N, classCount (M.ClassNames), T); for j = 1:T [~, s] = predict (M.Trees{trees(j)}, X); P(:, M.TreeClassIdx{trees(j)}, j) = s; endfor else P = zeros (N, 1, T); for j = 1:T P(:, 1, j) = predict (M.Trees{trees(j)}, X); endfor endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/boostSample.m000066400000000000000000000035371527311104300256110ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{idx}, @var{sw}, @var{cnt}] =} boostSample (@var{w}, @var{m}, @var{replace}) ## The rows a resampling boosting method grows one learner on. ## ## With @var{replace}, @var{m} rows are drawn with replacement in proportion to ## the weights @var{w}, each draw carrying the same sample weight; without it, ## @var{m} rows are drawn uniformly without replacement, each carrying its ## weight from @var{w}. @var{idx} lists the rows drawn, @var{sw} their sample ## weights, which sum to one, and @var{cnt} how many times each row was drawn. ## These are the draws MATLAB R2024a makes. ## @end deftypefn function [idx, sw, cnt] = boostSample (w, m, replace) w = w(:); n = numel (w); if (replace) cw = [0; cumsum(w)]; cw /= cw(end); idx = lookup (cw, rand (m, 1)); sw = ones (m, 1) / m; else order = randperm (n); idx = sort (order(1:m)); idx = idx(:); sw = w(idx); if (sum (sw) > 0) sw /= sum (sw); else sw = ones (m, 1) / m; endif endif cnt = accumarray (idx, 1, [n, 1]); endfunction statistics-release-2.1.2/inst/Machine_Learning/private/classCount.m000066400000000000000000000024741527311104300254360ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{k} =} classCount (@var{C}) ## How many classes a @code{ClassNames} holds, whatever type it is in. ## ## A cell array of character vectors, a numeric column and a logical column ## each hold one class per element, but a character matrix holds one per ## @emph{row} and as many columns as the longest name, so @code{numel} counts ## its characters rather than its classes. ## @end deftypefn function k = classCount (C) if (ischar (C)) k = rows (C); else k = numel (C); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/classFrame.m000066400000000000000000000226331527311104300253770ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{F} =} classFrame (@var{X}, @var{Y}, @var{ClassNames}, @var{Prior}, @var{Cost}, @var{Weights}, @var{classname}) ## @deftypefnx {Private Function} {@var{F} =} classFrame (@dots{}, @var{binary}) ## @deftypefnx {Private Function} {@var{F} =} classFrame (@dots{}, @var{binary}, @var{costRule}) ## @deftypefnx {Private Function} {@var{F} =} classFrame (@dots{}, @var{binary}, @var{costRule}, @var{rowRule}) ## ## Resolve the classes, the prior, the cost and the observation weights of a ## classifier. ## ## The four linear and kernel classifiers all begin the same way: group the ## response, keep only the named classes, drop the rows that are not ## complete, and turn what survives into a signed response and a weight per ## observation. @var{F} is a structure carrying the results, so that the ## classifier and its cross-validated counterpart cannot drift apart. ## ## @var{binary} says whether exactly two classes are wanted, which is the ## default and what the four linear and kernel classifiers want, or two or ## more, which is what the ECOC model wants. ## ## @var{rowRule} says which rows are dropped for their predictors. ## @qcode{'complete'}, the default, drops a row missing any predictor. ## @qcode{'partial'} keeps it unless every predictor is missing, and drops a ## row of weight zero, as R2024a's ensembles do, whose learners take the ## missing values themselves. A negative weight is left to the caller to ## refuse. ## ## Fields: @qcode{X} and @qcode{Y}, the retained data; @qcode{RowsUsed}, a ## logical over the rows as supplied; @qcode{gY}, the class index of each ## retained observation; @qcode{ClassNames}, in the type of @var{Y}; ## @qcode{Prior} and @qcode{Cost}, as the model reports them; ## @qcode{Weights}, the retained weights before normalization, as double; ## @qcode{WeightsClass}, the class the weights were given in; @qcode{W}, ## normalized within each class to that class's cost-adjusted prior and ## summing to one; @qcode{y}, @math{+1} for the second class and @math{-1} ## for the first; and @qcode{n} and @qcode{p}. ## ## @var{classname} names the caller in every error message. ## ## @var{costRule} says what a class whose misclassification costs are all ## zero does, as MATLAB's learners differ: @qcode{'remove'}, the default, its ## observations removed like those of a class with prior 0, as the linear ## and kernel classifiers do; @qcode{'keep'}, as ECOC does; or ## @qcode{'refuse'}, an error, as the ensemble does. ## @end deftypefn function F = classFrame (X, Y, ClassNames, Prior, Cost, Weights, ... classname, binary = true, costRule = 'remove', ... rowRule = 'complete') if (! (isnumeric (X) && isreal (X) && ismatrix (X) && ndims (X) == 2)) error ("%s: invalid values in X.", classname); endif if (isempty (X)) error ("%s: X is empty.", classname); endif if (rows (X) != rows (Y)) error ("%s: number of rows in X and Y must be equal.", classname); endif [gY, gnY, glY] = grp2idx (Y); ## Keep only the named classes, if any were named. ## A named class with prior 0 is removed below, so it may be absent; so is ## one that costs nothing to misclassify, where the cost removes it too rmCost = []; if (strcmp (costRule, 'remove')) rmCost = Cost; endif exempt = []; if (! isempty (ClassNames)) exempt = zeroPriorNames (ClassNames, Prior, rmCost); [pos, errmsg] = namedClasses (glY, ClassNames, exempt); if (! isempty (errmsg)) error ("%s: %s", classname, errmsg); endif gY(! ismember (gY, pos)) = NaN; endif ## Weights are validated against the data as supplied, then follow it ## through the rows that are kept. errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("%s: %s", classname, errmsg); endif WeightsClass = "double"; if (isempty (Weights)) Weights = ones (rows (X), 1); else WeightsClass = class (Weights); Weights = double (Weights(:)); if (numel (Weights) != rows (X)) error ("%s: 'Weights' must have one element per observation.", ... classname); endif endif if (strcmp (rowRule, 'partial')) RowsUsed = ! (isnan (gY) | all (isnan (X), 2)) & Weights != 0; else RowsUsed = ! (isnan (gY) | any (isnan (X), 2)); endif ## Index the rows, not the elements: a character matrix of class names has ## one row per observation and several columns, and a linear index would ## flatten it into single characters. F = struct (); F.RowsUsed = RowsUsed; F.X = X(RowsUsed, :); F.Y = Y(RowsUsed, :); F.Weights = Weights(RowsUsed); F.WeightsClass = WeightsClass; if (isempty (F.Y)) error ("%s: no complete observations in the data.", classname); endif ## The classes, sorted or in the order ClassNames gives them. [F.ClassNames, gY] = classOrder (F.Y, ClassNames, exempt); nclasses = classCount (F.ClassNames); F.gY = gY; [F.n, F.p] = size (F.X); ## A binary model takes exactly two classes; a multiclass one takes two or ## more. BINARY says which is being built, and defaults to the binary ## case, which is what every caller but the ECOC model wants. if (binary && nclasses != 2) error (strcat ("%s: Y must name exactly two classes, this being a", ... " binary model; use fitcecoc for more than two."), ... classname); elseif (! binary && nclasses < 2) error ("%s: Y must name at least two classes.", classname); endif ## Prior defaults to the weighted frequencies of the training data and ## Cost to zero on the diagonal and one elsewhere. if (isstruct (Prior)) Prior = priorFromStruct (Prior, F.ClassNames, classname); endif freq = accumarray (gY(:), F.Weights(:), [nclasses, 1])' / sum (F.Weights); if (isempty (Prior) || (ischar (Prior) && strcmpi (Prior, 'empirical'))) Prior = freq; elseif (ischar (Prior) && strcmpi (Prior, 'uniform')) Prior = ones (1, nclasses) / nclasses; else if (numel (Prior) != nclasses) error ("%s: 'Prior' must have one entry per class.", classname); endif Prior = Prior(:)' / sum (Prior); endif ## A structure names its own order, and is permuted to the model's if (isstruct (Cost)) [Cost, errmsg] = costMatrix (Cost, F.ClassNames); if (! isempty (errmsg)) error ("%s: %s", classname, errmsg); endif endif if (isempty (Cost)) Cost = ones (nclasses) - eye (nclasses); elseif (rows (Cost) != nclasses || columns (Cost) != nclasses) error (strcat ("%s: the number of rows and columns in 'Cost' must", ... " correspond to the classes in Y."), classname); endif F.Cost = Cost; ## A class with prior 0, or, where COSTRULE says so, one that costs ## nothing to misclassify, is never predicted and weighs nothing, so its ## observations are removed. Boosting cannot weigh a class whose costs are ## all zero and refuses it, as MATLAB's ensemble does. if (strcmp (costRule, 'refuse') && nclasses > 1 && any (all (Cost == 0, 2))) error ("%s: every row of 'Cost' must hold a positive cost.", classname); endif if (strcmp (costRule, 'remove')) rmCost = Cost; endif [drop, Prior, msg] = removedClasses (Prior, rmCost, F.ClassNames, gY); if (all (drop)) error ("%s: every class has prior probability 0.", classname); elseif (any (drop)) if (! isempty (msg)) warning ("%s: %s", classname, msg); endif keep = ! drop(gY); used = find (RowsUsed); RowsUsed(used(! keep)) = false; F.RowsUsed = RowsUsed; F.X = F.X(keep,:); F.Y = F.Y(keep,:); F.Weights = F.Weights(keep); gY = gY(keep); F.gY = gY; [F.n, F.p] = size (F.X); endif F.Prior = Prior; ## MATLAB folds the cost matrix into the prior before it weights the ## observations: a class that is costlier to misclassify carries more ## weight, in proportion to the total cost of getting it wrong. The Prior ## the model reports is the one it was given, not this one. Measured on ## R2024a: a fit with Cost [0 4; 1 0] and one with Prior [0.8 0.2] return ## the same coefficients to every digit, and giving both together lands ## back on the empirical fit, since [0.2 0.8] scaled by [4 1] is uniform ## again. adjPrior = Prior .* sum (Cost, 2)'; if (sum (adjPrior) > 0) adjPrior = adjPrior / sum (adjPrior); else adjPrior = Prior; endif W = zeros (F.n, 1); for k = 1:nclasses idx = (gY == k); tot = sum (F.Weights(idx)); if (tot > 0) W(idx) = F.Weights(idx) / tot * adjPrior(k); endif endfor if (sum (W) > 0) W = W / sum (W); endif F.W = W; ## The positive class is the second of ClassNames, so a positive score ## belongs to it. F.y = -ones (F.n, 1); F.y(gY == 2) = 1; endfunction statistics-release-2.1.2/inst/Machine_Learning/private/classMembers.m000066400000000000000000000026751527311104300257430ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{tf} =} classMembers (@var{Y}, @var{C}, @var{i}) ## Which observations of a response belong to the @var{i}th class. ## ## The complement of @code{labelIndex}: that asks which class an observation ## is in, this asks which observations are in a class. A response naming its ## classes in the rows of a character matrix is matched as whole names, with ## the padding such a matrix carries stripped from both sides. ## @end deftypefn function tf = classMembers (Y, C, i) if (ischar (C) || ischar (Y)) tf = strcmp (cellstr (Y), strtrim (char (labelsFromIndex (C, i)))); else tf = ismember (Y, C(i)); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/classNameListing.m000066400000000000000000000031251527311104300265520ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{str} =} classNameListing (@var{ClassNames}) ## ## The class names as a model's display lists them, quoted when they are ## text and braced as a set. ## ## @end deftypefn ## The classes as the display lists them, quoted when they are text. function str = classNameListing (ClassNames) if (iscellstr (ClassNames)) fmt = repmat ({'''%s'''}, 1, numel (ClassNames)); fmt = strjoin (fmt, ' '); str = sprintf (fmt, ClassNames{:}); elseif (ischar (ClassNames)) cn = cellstr (ClassNames); fmt = repmat ({'''%s'''}, 1, numel (cn)); fmt = strjoin (fmt, ' '); str = sprintf (fmt, cn{:}); else fmt = repmat ({'%g'}, 1, numel (ClassNames)); fmt = strjoin (fmt, ' '); str = sprintf (fmt, ClassNames); endif str = ['{', str, '}']; endfunction statistics-release-2.1.2/inst/Machine_Learning/private/classOrder.m000066400000000000000000000050371527311104300254170ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{C}, @var{gY}] =} classOrder (@var{Y}, @var{ClassNames}, @var{exempt}) ## The classes of a response in the order a classifier lays them out, and the ## class of each observation. ## ## @var{C} holds the classes present in @var{Y}, in the type @var{Y} holds ## them: sorted, or in the order of @var{ClassNames} when that names any, a ## class it leaves out being left out of @var{C}. Names match the classes as ## @code{namedClasses} matches them. @var{gY} is the index into ## @var{C} of each observation, zero for one whose class was left out. ## @var{exempt}, a logical over @var{ClassNames} and empty by default, marks ## the classes removed for a prior of 0, which keep their place among the ## classes though absent from @var{Y}. ## @end deftypefn function [C, gY] = classOrder (Y, ClassNames, exempt = []) C = uniqueLabels (Y); ## A named class removed for its prior of 0 may be absent from Y; it is ## added to the labels the classes are drawn from, so it keeps its place if (! isempty (ClassNames) && any (exempt)) pos = namedClasses (C, ClassNames); absent = (pos(:) == 0) & exempt(:); if (any (absent)) if (ischar (ClassNames)) C = uniqueLabels (namesAsLabels (Y, ClassNames(absent,:))); else C = uniqueLabels (namesAsLabels (Y, ClassNames(absent))); endif endif endif if (! isempty (ClassNames)) pos = namedClasses (C, ClassNames); pos = pos(pos > 0); [~, first] = unique (pos, 'first'); C = C(pos(sort (first)),:); endif ## A categorical response reports only the categories it holds, as R2024a ## does: an unused category is not a class of the fit. if (isa (C, 'categorical')) C = removecats (C); endif gY = labelIndices (C, Y); endfunction statistics-release-2.1.2/inst/Machine_Learning/private/classificationLoss.m000066400000000000000000000056711527311104300271560ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{l} =} classificationLoss (@var{LossFun}, @var{s}, @var{gY}, @var{w}, @var{Cost}) ## ## One of MATLAB's classification losses, over an @math{NxK} score matrix. ## ## @var{s} holds the scores after @qcode{ScoreTransform}, @var{gY} the index ## into the class names of each observation's true class, @var{w} weights ## summing to one, and @var{Cost} the misclassification cost matrix. ## ## The rival of the true class is the best of the others, which for two ## classes is simply the other one, so a binary caller is unaffected. ## ## Every loss but the two cost-based ones is a function of the score the ## model gives the @emph{true} class, and not of the margin. Measured on ## R2024a across both learners: with an untransformed support vector machine ## the two readings differ by a factor of two, and with a logit transformed ## logistic regression they differ by an affine map, and the true-class score ## is the one that reproduces all six losses in both cases. ## ## @end deftypefn ## The classification losses MATLAB offers, all of them functions of the ## score the model gives the true class. function l = classificationLoss (LossFun, s, gY, w, Cost) n = rows (s); idx = sub2ind (size (s), (1:n)', gY); strue = s(idx); ## The best score among the classes that are not the true one so = s; so(idx) = -Inf; [sother, kother] = max (so, [], 2); switch (LossFun) case 'classiferror' l = sum (w .* (strue <= sother)); case 'hinge' l = sum (w .* max (0, 1 - strue)); case 'quadratic' l = sum (w .* ((1 - strue) .^ 2)); case 'logit' l = sum (w .* log (1 + exp (-strue))); case 'binodeviance' l = sum (w .* log (1 + exp (-2 * strue))); case 'exponential' l = sum (w .* exp (-strue)); case 'mincost' expected = s * Cost; [~, k] = min (expected, [], 2); l = sum (w .* Cost(sub2ind (size (Cost), gY, k))); case 'classifcost' ## The class the model would pick, the true one keeping a tie k = gY; worse = sother > strue; k(worse) = kother(worse); l = sum (w .* Cost(sub2ind (size (Cost), gY, k))); endswitch endfunction statistics-release-2.1.2/inst/Machine_Learning/private/costMatrix.m000066400000000000000000000130751527311104300254540ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{C}, @var{errmsg}] =} costMatrix (@var{val}, @var{ClassNames}) ## Resolve a misclassification cost to a plain double matrix in the model's ## own class order. ## ## @var{val} is a numeric matrix, taken in the order the model holds its ## classes, or a struct with the ## fields @qcode{ClassNames} and @qcode{ClassificationCosts}, whose matrix is ## written in the order that struct names and is permuted into the order of ## @var{ClassNames}. The struct form is how a caller supplies costs without ## having to know which order the model sorted its classes into. ## ## A cost must be floating point, not sparse, not complex, square with one row ## and column per class, non-negative, zero down the diagonal, and free of ## @qcode{NaN} and @qcode{Inf}. A @code{single} is widened to @code{double} ## rather than refused. All of it measured against MATLAB R2024a. ## ## @var{errmsg} is the body of the message the caller should raise, or empty ## when there is nothing wrong; the caller emits it under its own ## @code{class.method} name, as the package's shared validation helpers do. ## An empty @var{val} is returned unchanged, the caller owning what an empty ## cost resets to. ## @end deftypefn function [C, errmsg] = costMatrix (val, ClassNames) C = val; errmsg = ''; if (isempty (val)) return; endif K = classCount (ClassNames); ## A struct names the order its matrix is written in. Permuting it into the ## model's order is not a convenience: the two orders genuinely differ, and ## MATLAB permutes entry by entry, so this is measured behaviour and not a ## reading of the matrix as given. if (isstruct (val)) if (! (isfield (val, 'ClassNames') && isfield (val, 'ClassificationCosts'))) errmsg = strcat ("'Cost' given as a struct must have the fields", ... " 'ClassNames' and 'ClassificationCosts'."); return; endif given = val.ClassNames; C = val.ClassificationCosts; n = classCount (given); if (! isequal (size (C), [n, n])) errmsg = strcat ("'Cost' given as a struct must have one row and", ... " one column of 'ClassificationCosts' per name in", ... " 'ClassNames'."); return; endif ## Matching is by name, so the struct must name the classes in the type ## the model holds them in. A type it cannot compare reaches ismember ## and raises rather than coming back as a message, so it is caught here ## and named for what it is; labelIndices speaks of a response, which a ## cost is not. perm = []; try perm = labelIndices (given, ClassNames); catch perm = []; end_try_catch if (isempty (perm)) errmsg = strcat ("'Cost' given as a struct must name the classes in", ... " the same type as the model holds them."); return; endif ## A class of the model that the struct does not name has no cost to ## permute, which MATLAB refuses rather than defaulting. missing = find (perm == 0, 1); if (! isempty (missing)) errmsg = sprintf (strcat ("'Cost' given as a struct must name every", ... " class; there is no cost for class %d."), ... missing); return; endif C = C(perm, perm); endif ## Char, cell and logical are none of them costs. An integer type is a ## number but not a cost either: MATLAB refuses it in its own right, and ## says so differently, so the two are kept apart here as well. if (! isnumeric (C)) errmsg = strcat ("'Cost' must be a numeric matrix, or a struct with", ... " the fields 'ClassNames' and 'ClassificationCosts'."); return; endif if (isinteger (C)) errmsg = "'Cost' must be a floating point matrix."; return; endif if (issparse (C)) errmsg = "'Cost' must not be sparse."; return; endif ## Complex means a nonzero imaginary part, not the complex attribute: ## complex (C, 0) is accepted and stored as a double, measured on R2024a. ## Writing this as ! isreal (C) would refuse a value MATLAB takes. if (any (imag (C(:)) != 0)) errmsg = "'Cost' must not be complex."; return; endif ## Single is widened rather than refused, and the imaginary part of an ## accepted complex value is dropped with it. C = double (real (C)); if (! isequal (size (C), [K, K])) errmsg = strcat ("the number of rows and columns in 'Cost' must", ... " correspond to selected classes in Y."); return; endif if (any (C(:) < 0)) errmsg = "'Cost' must not contain negative values."; elseif (any (diag (C) != 0)) errmsg = "'Cost' must have zeros on its diagonal."; elseif (any (! isfinite (C(:)))) errmsg = "'Cost' must not contain NaN or Inf values."; endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/cvPartitionOf.m000066400000000000000000000111101527311104300260720ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{part}, @var{args}] =} cvPartitionOf (@var{args}, @var{Y}, @var{n}, @var{classname}) ## ## Take the cross-validation options out of an argument list and build the ## partition they ask for. ## ## @var{args} is the caller's @code{varargin}; what comes back is the same ## list with @qcode{'CrossVal'}, @qcode{'KFold'}, @qcode{'Holdout'}, ## @qcode{'Leaveout'} and @qcode{'CVPartition'} removed, so the remainder ## can be handed to the learner that fits each fold. Only one of the last ## four may be given. ## ## @var{Y} is the response, used to stratify a classification partition so ## that every fold holds the classes in the proportions the data does; pass ## an empty @var{Y} for a regression model, which is partitioned by count ## alone. @var{n} is the number of observations. ## ## The default is ten folds, which is what @qcode{'CrossVal', 'on'} asks ## for and what a caller that named no option at all gets. ## ## @end deftypefn function [part, args] = cvPartitionOf (args, Y, n, classname) numFolds = 10; Holdout = []; Leaveout = 'off'; CVPartition = []; given = 0; keep = {}; while (numel (args) > 0) if (numel (args) < 2) error (strcat ("%s: optional arguments must be given in Name-Value", ... " pairs."), classname); endif switch (lower (args{1})) case 'crossval' val = args{2}; if (! (ischar (val) && any (strcmpi (val, {'on', 'off'})))) error ("%s: 'CrossVal' must be either 'on' or 'off'.", classname); endif case 'kfold' numFolds = args{2}; if (! (isnumeric (numFolds) && isscalar (numFolds) && isreal (numFolds) && numFolds == fix (numFolds) && numFolds > 1)) error (strcat ("%s: 'KFold' must be an integer value greater", ... " than 1."), classname); endif given++; case 'holdout' Holdout = args{2}; if (! (isnumeric (Holdout) && isscalar (Holdout) && isreal (Holdout) && Holdout > 0 && Holdout < 1)) error (strcat ("%s: 'Holdout' must be a numeric value between", ... " 0 and 1."), classname); endif given++; case 'leaveout' Leaveout = args{2}; if (! (ischar (Leaveout) && any (strcmpi (Leaveout, {'on', 'off'})))) error ("%s: 'Leaveout' must be either 'on' or 'off'.", classname); endif given++; case 'cvpartition' CVPartition = args{2}; if (! isa (CVPartition, 'cvpartition')) error ("%s: 'CVPartition' must be a cvpartition object.", ... classname); endif given++; otherwise keep = [keep, args(1:2)]; endswitch args(1:2) = []; endwhile if (given > 1) error (strcat ("%s: you can use only one of 'KFold', 'Holdout',", ... " 'Leaveout', or 'CVPartition' options."), classname); endif if (! isempty (CVPartition)) part = CVPartition; if (part.NumObservations != n) error (strcat ("%s: 'CVPartition' must be built over the same", ... " number of observations as the data."), classname); endif elseif (! isempty (Holdout)) part = stratified (Y, n, 'Holdout', Holdout); elseif (strcmpi (Leaveout, 'on')) part = cvpartition (n, 'LeaveOut'); else part = stratified (Y, n, 'KFold', numFolds); endif args = keep; endfunction ## Partition by the response when there is one to stratify by, and by count ## alone otherwise. Leave-one-out has nothing to stratify: every fold holds ## exactly one observation. function part = stratified (Y, n, kind, value) if (isempty (Y)) part = cvpartition (n, kind, value); else part = cvpartition (Y, kind, value); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/decodeLabels.m000066400000000000000000000065731527311104300256720ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{data} =} decodeLabels (@var{data}) ## ## Rebuild the categorical and string arrays of a loaded model. ## ## @var{data} is the structure @code{load} returns for a saved model. A field ## written by @code{encodeLabels} is turned back into its categorical or string ## array. So is a field written by version 1.9.2, which saved such an array as ## the structure @code{save} falls back to for a classdef object: fields ## @qcode{cats}, @qcode{code}, @qcode{isMissing}, @qcode{isOrdinal} and ## @qcode{isProtected} for a categorical array, and @qcode{isMissing} and ## @qcode{strs} for a string array. A field holding a structure array is ## searched the same way, element by element, so class names a model keeps ## inside a structure are rebuilt too. Every other field is left as it is. ## ## @seealso{encodeLabels} ## @end deftypefn function data = decodeLabels (data) fn = fieldnames (data); for e = 1:numel (data) for j = 1:numel (fn) data(e).(fn{j}) = decodeValue (data(e).(fn{j})); endfor endfor endfunction ## One saved value: an encoded array rebuilt, a structure searched in turn. function v = decodeValue (v) if (! isstruct (v)) return; endif fn = sort (fieldnames (v)); if (isscalar (v) && isfield (v, 'LabelType') && strcmp (v.LabelType, 'categorical')) v = categoricalFrom (v.Codes, v.Categories, v.Ordinal, v.Protected, ... v.Size); elseif (isscalar (v) && isfield (v, 'LabelType') && strcmp (v.LabelType, 'string')) v = stringFrom (v.Text, v.Missing, v.Size); elseif (isscalar (v) && isequal (fn, {'cats'; 'code'; 'isMissing'; ... 'isOrdinal'; 'isProtected'})) codes = double (v.code); codes(v.isMissing) = NaN; v = categoricalFrom (codes, v.cats, v.isOrdinal, v.isProtected, ... size (v.code)); elseif (isscalar (v) && isequal (fn, {'isMissing'; 'strs'})) v = stringFrom (v.strs, v.isMissing, size (v.strs)); else v = decodeLabels (v); endif endfunction ## A categorical array from its codes, NaN marking an undefined element. function C = categoricalFrom (codes, cats, ordinal, protected, sz) text = repmat ({''}, numel (codes), 1); ok = ! isnan (codes(:)); text(ok) = cats(codes(ok)); C = categorical (text, cats, 'Ordinal', ordinal, 'Protected', protected); C = reshape (C, sz); endfunction ## A string array from its text and the mask of its missing elements. function S = stringFrom (text, missing, sz) S = string (text(:)); S(missing(:)) = string (NaN); S = reshape (S, sz); endfunction statistics-release-2.1.2/inst/Machine_Learning/private/discardSVs.m000066400000000000000000000036021527311104300253570ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{m} =} discardSVs (@var{m}) ## Collapse a linear LIBSVM model onto the one vector that decides it. ## ## A linear kernel makes the decision function @math{sum_i c_i - rho}, ## which is @math{ - rho} for @math{w = sum_i c_i s_i}. One support ## vector holding @math{w}, with a coefficient of one, therefore decides ## exactly what all of them did, and the rest need not be kept. ## ## This is what lets the support vectors be discarded without touching a ## single scoring path: @code{svmpredict} goes on being the engine, over a ## model of one vector instead of many. Emptying the properties alone would ## save nothing, the engine's own copy being where they are kept. ## ## The model must be linear. Callers check that, so that the error names the ## method the user called. Collapsing an already collapsed model returns it ## unchanged, one vector of @math{w} summing to @math{w}. ## @end deftypefn function m = discardSVs (m) w = m.sv_coef' * m.SVs; m.SVs = sparse (w); m.sv_coef = 1; m.totalSV = 1; m.nSV = [1; 0]; m.sv_indices = 1; endfunction statistics-release-2.1.2/inst/Machine_Learning/private/discrimcanon.m000066400000000000000000000041011527311104300257560ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{t} =} discrimcanon (@var{str}) ## @deftypefnx {Private Function} {[@var{t}, @var{fam}] =} discrimcanon (@var{str}) ## ## Canonical spelling and family of a discriminant type. ## ## @var{t} is @var{str} respelled as the discriminant classes report it, which ## is case sensitive: @qcode{'diagLinear'}, not @qcode{'diaglinear'}. It is ## empty when @var{str} names no discriminant type, which is how the callers ## detect an invalid value. ## ## @var{fam} is @qcode{'linear'} or @qcode{'quadratic'}, the family @var{t} ## belongs to, and empty alongside an empty @var{t}. The family decides which ## covariances a fit has to keep: a linear one pools across classes and a ## quadratic one estimates a covariance per class, and no assignment ever moves ## a model between the two. ## ## @end deftypefn function [t, fam] = discrimcanon (str) lin = {'linear', 'diagLinear', 'pseudoLinear'}; quad = {'quadratic', 'diagQuadratic', 'pseudoQuadratic'}; t = ''; fam = ''; if (! (ischar (str) && isrow (str))) return; endif idx = find (strcmpi (str, lin)); if (! isempty (idx)) t = lin{idx}; fam = 'linear'; return; endif idx = find (strcmpi (str, quad)); if (! isempty (idx)) t = quad{idx}; fam = 'quadratic'; endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/discrimcoeffs.m000066400000000000000000000057321527311104300261400ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{Coeffs} =} discrimcoeffs (@var{Mu}, @var{Sigma}, @var{LogDetSigma}, @var{Prior}, @var{DiscrimType}, @var{Delta}, @var{ClassNames}) ## ## Pairwise boundary coefficients of a discriminant, a @math{KxK} structure ## whose @math{(i,j)} entry separates class @math{i} from class @math{j}. ## ## The linear family reports @code{Const} and @code{Linear}; the quadratic one ## adds @code{Quadratic}, which follows @var{Sigma}'s shape and so is ## @math{1xP} for @qcode{'diagQuadratic'}. The diagonal entries carry the two ## class names and nothing else. ## ## @end deftypefn function Coeffs = discrimcoeffs (Mu, Sigma, LogDetSigma, Prior, ... DiscrimType, Delta, ClassNames) K = rows (Mu); [~, fam] = discrimcanon (DiscrimType); isquad = strcmp (fam, 'quadratic'); isdiag = strncmp (DiscrimType, 'diag', 4); SigmaInv = discriminv (Sigma, DiscrimType); if (! isquad) [Z, bk] = discrimlinear (Mu, Sigma, Prior, DiscrimType, Delta); endif Coeffs = struct (); for i = 1:K for j = 1:K Coeffs(i,j).DiscrimType = ''; Coeffs(i,j).Const = []; Coeffs(i,j).Linear = []; if (isquad) Coeffs(i,j).Quadratic = []; endif Coeffs(i,j).Class1 = ClassNames(i,:); Coeffs(i,j).Class2 = ClassNames(j,:); if (i == j) continue; endif Coeffs(i,j).DiscrimType = DiscrimType; if (isquad) Si = SigmaInv(:,:,i); Sj = SigmaInv(:,:,j); L = Si * Mu(i,:)' - Sj * Mu(j,:)'; C = -0.5 * (Mu(i,:) * Si * Mu(i,:)' - Mu(j,:) * Sj * Mu(j,:)') ... - 0.5 * (LogDetSigma(i) - LogDetSigma(j)) ... + log (Prior(i) / Prior(j)); Q = -0.5 * (Si - Sj); if (isdiag) ## A diagonal model reports the diagonal, matching Sigma's own shape. Q = diag (Q)'; endif Coeffs(i,j).Quadratic = Q; else ## Taken from the per-class coefficients, so a predictor Delta has ## eliminated is eliminated from every pair that names it. L = (Z(i,:) - Z(j,:))'; C = bk(i) - bk(j); endif Coeffs(i,j).Linear = L; Coeffs(i,j).Const = C; endfor endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/discrimderive.m000066400000000000000000000111721527311104300261440ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{Sigma}, @var{LogDetSigma}, @var{Gamma}] =} discrimderive (@var{Base}, @var{DiscrimType}, @var{Gamma}, @var{MinGamma}) ## ## Derive a discriminant's covariance from the covariance its fit estimated. ## ## @var{Base} is the unregularized within-class covariance, @math{PxP} for the ## linear family and @math{PxPxK} for the quadratic one. It is the state a fit ## keeps, and every type in the same family is derived from it, which is what ## makes @code{DiscrimType} assignable after the fit. ## ## @var{Sigma} takes the shape the type calls for: @math{PxP} or @math{PxPxK} ## for the plain and pseudo types, @math{1xP} or @math{1xPxK} for the diagonal ## ones. @var{LogDetSigma} is a scalar for the linear family and @math{Kx1} for ## the quadratic one. ## ## @var{Gamma} is returned as well as taken, because the type and the ## regularization are one state: a diagonal type is @math{Gamma = 1}, a pseudo ## type is @math{Gamma = 0}, and a plain linear type on a singular covariance is ## raised to @var{MinGamma}, which describes the data and is measured once by ## @code{discrimmingamma}. ## ## @end deftypefn function [Sigma, LogDetSigma, Gamma] = discrimderive (Base, DiscrimType, ... Gamma, MinGamma) P = columns (Base); K = size (Base, 3); LogDetSigma = zeros (K, 1); ## Every type takes its log determinant in correlation space, which is not a ## nicety: on a rank-3-of-4 iris fixture the direct log (det (Sigma)) gives ## -41.2988 where the oracle reports -41.3112, the correlation form matching ## to every digit. Splitting the scale off leaves a matrix of ones on the ## diagonal, which is far better conditioned to take a determinant of. if (strncmp (DiscrimType, 'diag', 4)) ## A diagonal type is Gamma = 1: regularizing all the way to the diagonal ## and naming the diagonal type are the same state, in both directions. ## Its correlation matrix is the identity, so only the scale term remains. Gamma = 1; Sigma = zeros (1, P, K); for k = 1:K d = diag (Base(:,:,k))'; Sigma(1,:,k) = d; LogDetSigma(k) = sum (log (d(d > 0))); endfor elseif (strncmp (DiscrimType, 'pseudo', 6)) ## The pseudo types never regularize. They invert whatever rank is there, ## so a singular covariance is a case they answer rather than refuse, and ## the determinant runs over the directions that carry variance. Gamma = 0; Sigma = Base; for k = 1:K [d, R] = correlate (Base(:,:,k)); ev = eig (R)'; tol = max (size (R)) * eps (max (ev)); LogDetSigma(k) = sum (log (d)) + sum (log (ev(ev > tol))); endfor else Sigma = zeros (P, P, K); ## MinGamma is the least regularization that makes the correlation matrix ## invertible, and a plain linear type is raised to it rather than failing. ## The quadratic family is not: it refuses a singular class covariance ## outright, so a bump there would only mask the error, and Gamma on a ## quadratic discriminant may be nothing but 0 or 1. lin = (K == 1); for k = 1:K S = Base(:,:,k); D = diag (diag (S)); if (lin) g = max (Gamma, MinGamma); else g = Gamma; endif Sigma(:,:,k) = S * (1 - g) + D * g; [d, R] = correlate (Sigma(:,:,k)); LogDetSigma(k) = sum (log (d)) + log (det (R)); endfor if (lin) Gamma = max (Gamma, MinGamma); endif endif endfunction ## Split a covariance into its scale and its correlation, keeping only the ## predictors that carry variance. A predictor with none contributes nothing ## to the determinant rather than driving it to -Inf. function [d, R] = correlate (S) d = diag (S)'; pos = d > 0; d = d(pos); if (isempty (d)) R = []; return; endif R = S(pos, pos) ./ sqrt (d' * d); R = (R + R') / 2; endfunction statistics-release-2.1.2/inst/Machine_Learning/private/discriminv.m000066400000000000000000000047101527311104300254620ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{SigmaInv} =} discriminv (@var{Sigma}, @var{DiscrimType}) ## ## Inverse within-class covariance of a discriminant, one @math{PxP} slice per ## class, whatever shape @var{Sigma} is stored in. ## ## The diagonal types invert elementwise, leaving a zero where a predictor has ## no variance, so that predictor contributes nothing rather than an infinity. ## The pseudo types take a pseudo-inverse in correlation space, which is the ## same convention their @code{LogDetSigma} uses. ## ## @end deftypefn function SigmaInv = discriminv (Sigma, DiscrimType) K = size (Sigma, 3); if (strncmp (DiscrimType, 'diag', 4)) P = columns (Sigma); SigmaInv = zeros (P, P, K); for k = 1:K d = Sigma(1,:,k); v = zeros (1, P); v(d > 0) = 1 ./ d(d > 0); SigmaInv(:,:,k) = diag (v); endfor elseif (strncmp (DiscrimType, 'pseudo', 6)) P = columns (Sigma); SigmaInv = zeros (P, P, K); for k = 1:K SigmaInv(:,:,k) = pseudoinverse (Sigma(:,:,k)); endfor else SigmaInv = zeros (size (Sigma)); for k = 1:K SigmaInv(:,:,k) = inv (Sigma(:,:,k)); endfor endif endfunction ## Pseudo-inverse taken on the correlation matrix and scaled back, the same ## convention the pseudo log determinant uses. A predictor with no variance is ## left out and comes back as a zero row and column, which is how the oracle ## reports its coefficient. function Si = pseudoinverse (S) P = columns (S); Si = zeros (P, P); d = diag (S)'; pos = d > 0; if (! any (pos)) return; endif dp = d(pos); R = S(pos, pos) ./ sqrt (dp' * dp); Si(pos, pos) = pinv ((R + R') / 2) ./ sqrt (dp' * dp); endfunction statistics-release-2.1.2/inst/Machine_Learning/private/discrimlinear.m000066400000000000000000000055351527311104300261460ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{Z}, @var{b}, @var{DeltaPredictor}] =} discrimlinear (@var{Mu}, @var{Sigma}, @var{Prior}, @var{DiscrimType}, @var{Delta}) ## ## Per-class linear coefficients of a discriminant, after @var{Delta}. ## ## A linear discriminant scores class @math{k} as @math{Z(k,:) * x' + b(k)}, ## which is the Mahalanobis form with the terms common to every class dropped. ## The coefficients are taken about the prior-weighted mean of the class means, ## ## @example ## @var{Z} = (@var{Mu} - @var{Prior} * @var{Mu}) / @var{Sigma} ## @end example ## ## @var{Delta} eliminates a predictor by zeroing coefficients, and it compares ## against the @strong{standardized} coefficient, @code{Z(k,j) * s(j)} with ## @math{s} the within-class standard deviations. Scaling matters: a threshold ## on the raw coefficients would depend on the units each predictor is measured ## in, so a centimetre and a metre would eliminate different predictors. ## ## @var{DeltaPredictor} is the @var{Delta} at which each predictor drops out ## altogether, @code{max (abs (Z(:,j)) * s(j))} over the classes. ## ## The quadratic family has no linear coefficients to eliminate and does not ## reach here. ## ## @end deftypefn function [Z, b, DeltaPredictor] = discrimlinear (Mu, Sigma, Prior, ... DiscrimType, Delta) K = rows (Mu); mubar = Prior(:)' * Mu; SigmaInv = discriminv (Sigma, DiscrimType); Z = (Mu - mubar) * SigmaInv(:,:,1); if (strncmp (DiscrimType, 'diag', 4)) sd = sqrt (Sigma(1,:,1)); else sd = sqrt (diag (Sigma(:,:,1)))'; endif DeltaPredictor = max (abs (Z) .* sd, [], 1); if (K == 1) DeltaPredictor = abs (Z) .* sd; endif if (Delta > 0) Z(abs (Z) .* sd <= Delta) = 0; endif ## The constant a shrunken centroid implies. At Delta of 0 this is the ## ordinary linear discriminant constant, the terms it drops being the same ## for every class and so lost in the normalization. b = zeros (1, K); for k = 1:K b(k) = -0.5 * (Z(k,:) * (Mu(k,:) + mubar)') + log (Prior(k)); endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/discrimlogp.m000066400000000000000000000040241527311104300256250ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{lp} =} discrimlogp (@var{X}, @var{Mu}, @var{Sigma}, @var{LogDetSigma}, @var{Prior}, @var{DiscrimType}) ## ## Log unconditional probability density of every row of @var{X} under a ## discriminant, returned as an @math{Nx1} vector. ## ## The density is @math{P(x) = sum_k P(k) P(x|k)} over the classes, with ## @math{P(k)} the prior and @math{P(x|k)} the multivariate normal density of ## class @math{k}. The class log densities are formed from the squared ## Mahalanobis distances and @var{LogDetSigma}, so the value follows the ## covariance the model reports rather than a second one derived here, and ## they are summed by their maximum so a well separated observation is a ## density rather than a sum of underflowed zeros. ## ## @end deftypefn function lp = discrimlogp (X, Mu, Sigma, LogDetSigma, Prior, DiscrimType) K = rows (Mu); P = columns (X); M = discrimmahal (X, Mu, Sigma, DiscrimType); logdet = LogDetSigma; if (isscalar (logdet)) logdet = repmat (logdet, K, 1); endif L = zeros (rows (X), K); for k = 1:K L(:, k) = -0.5 * (M(:, k) + logdet(k) + P * log (2 * pi)) ... + log (Prior(k)); endfor mx = max (L, [], 2); lp = mx + log (sum (exp (L - mx), 2)); endfunction statistics-release-2.1.2/inst/Machine_Learning/private/discrimmahal.m000066400000000000000000000033271527311104300257530ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{M} =} discrimmahal (@var{X}, @var{Mu}, @var{Sigma}, @var{DiscrimType}) ## ## Squared Mahalanobis distance from every row of @var{X} to every class mean ## of a discriminant, returned as an @math{NxK} matrix. ## ## @var{M}(i,j) is @math{(x_i - mu_j)' * inv (S_j) * (x_i - mu_j)}, where ## @math{S_j} is the class covariance for a quadratic type and the one shared ## covariance for a linear one. The inverse comes from @code{discriminv}, so ## the regularized covariance the model reports is the one measured against, ## and the diagonal and pseudo types keep their own conventions. ## ## @end deftypefn function M = discrimmahal (X, Mu, Sigma, DiscrimType) K = rows (Mu); SigmaInv = discriminv (Sigma, DiscrimType); nInv = size (SigmaInv, 3); M = zeros (rows (X), K); for k = 1:K Si = SigmaInv(:,:,min (k, nInv)); Z = X - Mu(k, :); M(:, k) = sum ((Z * Si) .* Z, 2); endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/discrimmingamma.m000066400000000000000000000036071527311104300264600ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{g} =} discrimmingamma (@var{S}) ## ## Least regularization that leaves a covariance invertible. ## ## @var{g} is the smallest multiple of @code{eps} for which ## @code{(1 - @var{g}) * R + @var{g} * I} is invertible, where @var{R} is ## @var{S}'s correlation matrix, and 0 when @var{R} already is. Multiples of ## @code{eps} are the quantization the oracle's own readings show, 0 on a well ## conditioned fit and 8 and 10 times @code{eps} on two singular ones. ## ## It describes the data rather than any one discriminant type, so it is taken ## once from the pooled within-class covariance and reported by every type, ## whether or not that type regularizes. ## ## @end deftypefn function g = discrimmingamma (S) g = 0; d = diag (S)'; pos = d > 0; if (! any (pos)) return; endif dp = d(pos); R = S(pos, pos) ./ sqrt (dp' * dp); R = (R + R') / 2; n = rows (R); I = eye (n); if (rcond (R) > n * eps) return; endif for m = 1:64 gm = m * eps; if (rcond (R * (1 - gm) + I * gm) > n * eps) g = gm; return; endif endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/dummyCoding.m000066400000000000000000000140271527311104300255740ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{C}, @var{errmsg}] =} dummyCoding (@var{X}, @var{spec}, @var{names}) ## @deftypefnx {Private Function} {@var{XD} =} dummyCoding (@var{X}, @var{C}) ## ## Dummy code the categorical predictors of a model. ## ## The first form learns the coding from the training predictors @var{X}. ## @var{spec} names the categorical predictors as indices, as a logical ## vector with one element per predictor, as @qcode{'all'}, or by name as a ## character matrix of one padded name per row, a string array or a cell ## array of character vectors, and @var{names} holds the predictor names. ## A name is matched against @var{names} exactly, so its case must agree; ## the padding blanks of a character matrix are stripped before matching ## and nothing else is. @qcode{'all'} is read as every predictor even where ## a predictor is named @qcode{'all'}. ## ## Three readings of a name depart from MATLAB R2024a, each deliberately. ## Repeated names give one index where MATLAB repeats it; the indices are a ## row for every form of @var{spec}, where MATLAB returns a column for a ## character matrix and a row for the rest; and a name is read against the ## default names as readily as against given ones, where MATLAB requires ## @qcode{'PredictorNames'} before it will read any name at all. ## ## @var{C} is a structure with the ## fields @qcode{Index}, the sorted indices of the categorical predictors; ## @qcode{Levels}, a cell array with the sorted levels of each predictor, ## empty for a numeric one; @qcode{NumPredictors}; @qcode{ExpandedNames}, ## the names of the coded columns; and @qcode{Dummy}, a logical row marking ## the coded columns that hold a level. A problem with ## @var{spec} leaves @var{C} empty and returns the message in @var{errmsg}. ## ## The second form codes @var{X} with @var{C}. Every predictor keeps its ## place: a numeric one is copied, and a categorical one becomes a column of ## zeros and ones per level, named @qcode{"x1 == 2"}, as MATLAB R2024a codes ## them. A value that is not one of the levels, a missing value included, ## makes every column of that predictor @code{NaN}. ## ## @end deftypefn function [C, errmsg] = dummyCoding (X, spec, names) if (isstruct (spec)) C = code (X, spec); return; endif C = []; errmsg = ''; p = columns (X); if (ischar (spec) && strcmpi (spec, 'all')) idx = 1:p; elseif (islogical (spec) && (isvector (spec) || isempty (spec))) if (numel (spec) != p) errmsg = strcat ("a logical 'CategoricalPredictors' must have one", ... " element per predictor."); return; endif idx = find (spec(:)'); elseif (isnumeric (spec) && (isvector (spec) || isempty (spec)) && all (spec == fix (spec)) && all (spec > 0)) if (any (spec > p)) errmsg = strcat ("'CategoricalPredictors' indices must not exceed", ... " the number of predictors."); return; endif idx = unique (double (spec(:)')); elseif (ischar (spec) || iscellstr (spec) || isa (spec, 'string')) ## A character matrix gives one name per row, its padding blanks stripped; ## a cell array or string array gives one name per element, kept whole. if (ischar (spec)) wanted = cellstr (spec)'; elseif (iscellstr (spec)) wanted = spec(:)'; else wanted = cellstr (spec(:)'); endif if (isempty (wanted)) idx = []; elseif (numel (names) != p) errmsg = strcat ("'CategoricalPredictors' can name a predictor only", ... " where the predictor names are known."); return; else idx = zeros (1, 0); for k = 1:numel (wanted) j = find (strcmp (names, wanted{k})); if (isempty (j)) errmsg = sprintf (strcat ("'CategoricalPredictors' does not name", ... " a predictor: '%s'"), wanted{k}); return; endif idx(end+1) = j(1); endfor idx = unique (idx); endif else errmsg = strcat ("'CategoricalPredictors' must be a vector of positive", ... " integers, a logical vector, a character matrix, a", ... " string array, a cell array of character vectors or", ... " 'all'."); return; endif C = struct ('Index', idx, 'NumPredictors', p); C.Levels = cell (1, p); C.ExpandedNames = {}; C.Dummy = false (1, 0); for j = 1:p if (any (idx == j)) v = X(:,j); C.Levels{j} = unique (v(! isnan (v)))'; C.Dummy = [C.Dummy, true(1, numel (C.Levels{j}))]; C.ExpandedNames = [C.ExpandedNames, arrayfun(@(l) sprintf ('%s == %s', ... names{j}, num2str (l)), ... C.Levels{j}, ... 'UniformOutput', false)]; else C.ExpandedNames{end+1} = names{j}; C.Dummy(end+1) = false; endif endfor endfunction function XD = code (X, C) if (isempty (C.Index)) XD = X; return; endif XD = zeros (rows (X), 0); for j = 1:C.NumPredictors lev = C.Levels{j}; if (isempty (lev) && ! any (C.Index == j)) XD = [XD, X(:,j)]; else D = double (X(:,j) == lev); D(! any (D, 2),:) = NaN; XD = [XD, D]; endif endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/ecocDecode.m000066400000000000000000000101361527311104300253270ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{NegLoss}, @var{errmsg}] =} ecocDecode (@var{S}, @var{M}, @var{lossname}, @var{decoding}, @var{range}) ## ## Decode the scores of the binary learners of an error correcting code. ## ## @var{S} is the @math{NxL} matrix of the scores each binary learner gave ## the class it was trained to call +1, @var{M} the @math{KxL} coding matrix, ## and @var{NegLoss} the @math{NxK} negated average loss, the largest entry of ## a row naming the class predicted. ## ## @var{range} is the interval the learners score on, @code{[-Inf, Inf]} for a ## margin and @code{[0, 1]} for a posterior. A score is read as ## @math{u = M(k,j) s} on the first and @math{u = M(k,j) (2s-1)} on the ## second, which is the whole of why a loss suits one range and not the other. ## ## Every loss is scaled so that it is exactly @math{0.5} at @math{u = 0}, so a ## class a column leaves out costs the same wherever it appears. Under ## @qcode{'lossbased'} the average runs over every column, those left out ## included, and under @qcode{'lossweighted'} it is weighted by ## @math{|M(k,j)|}, which drops them. ## ## @var{errmsg} is empty when the scores were decoded, and otherwise the body ## of a message the caller raises under its own name. ## ## @seealso{CompactClassificationECOC, fitcecoc} ## @end deftypefn function [NegLoss, errmsg] = ecocDecode (S, M, lossname, decoding, range) NegLoss = []; errmsg = ''; posterior = isequal (range, [0, 1]); if (posterior) allowed = {'hamming', 'quadratic'}; spread = " [0,1]"; else allowed = {'binodeviance', 'exponential', 'hamming', 'hinge', ... 'linear', 'logit'}; spread = " (-Inf,+Inf)"; endif if (! any (strcmp (lossname, allowed))) ## strcat strips trailing whitespace from every argument, so the space ## before the range rides on SPREAD rather than standing on its own. errmsg = strcat ("you cannot use '", lossname, "' loss for binary", ... " learners with response in the range", spread, "."); return; endif ## A posterior is read across the same interval a margin is, which is what ## the documented form of the quadratic loss already carries. if (posterior) F = 2 * S - 1; else F = S; endif K = rows (M); L = columns (M); N = rows (F); NegLoss = zeros (N, K); W = abs (M); for k = 1:K U = F .* M(k,:); G = binaryLoss (U, lossname); if (strcmp (decoding, 'lossbased')) NegLoss(:,k) = - sum (G, 2) / L; else NegLoss(:,k) = - (G * W(k,:)') / sum (W(k,:)); endif endfor endfunction ## The binary losses, each scaled so that it is 0.5 at u = 0. That scaling is ## what the halves and the 2*log (2) are for, and it is the only thing that ## makes a column a class sits out cost the same as any other. Note that the ## logit takes -u where the binomial deviance takes -2u; the two are otherwise ## the same expression. function G = binaryLoss (U, lossname) switch (lossname) case 'binodeviance' G = log (1 + exp (-2 * U)) / (2 * log (2)); case 'exponential' G = exp (-U) / 2; case 'hamming' G = (1 - sign (U)) / 2; case 'hinge' G = max (0, 1 - U) / 2; case 'linear' G = (1 - U) / 2; case 'logit' G = log (1 + exp (-U)) / (2 * log (2)); case 'quadratic' G = (1 - U) .^ 2 / 2; endswitch endfunction statistics-release-2.1.2/inst/Machine_Learning/private/ecocDiscardSVs.m000066400000000000000000000035521527311104300261550ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{learners}, @var{n}] =} ecocDiscardSVs (@var{learners}) ## ## Discard the support vectors of every binary learner that has any. ## ## @var{learners} is the cell of binary learners of an error correcting code. ## Only a support vector machine on a linear kernel can give its vectors up, ## the linear model standing in for them exactly; any other learner is left ## as it is rather than refused, a code being free to mix them. ## ## @var{n} is how many learners were collapsed, which is zero when none of ## them was a linear support vector machine and is what the caller warns on. ## ## @seealso{CompactClassificationECOC, ClassificationECOC} ## @end deftypefn function [learners, n] = ecocDiscardSVs (learners) n = 0; svm = {'ClassificationSVM', 'CompactClassificationSVM'}; for j = 1:numel (learners) L = learners{j}; if (! any (strcmp (class (L), svm))) continue; endif if (! strcmpi (L.KernelParameters.Function, 'linear')) continue; endif learners{j} = discardSupportVectors (L); n++; endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/ecocSelectModels.m000066400000000000000000000042441527311104300265320ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{learners}, @var{errmsg}] =} ecocSelectModels (@var{learners}, @var{idx}) ## ## Keep a subset of the regularization strengths of every binary learner. ## ## @var{learners} is the cell of binary learners of an error correcting code ## and @var{idx} names the strengths to keep, as indices or as a logical ## vector. Only a learner fitted over several strengths has any to choose ## between, which today is the linear one alone. ## ## @var{errmsg} is empty when every learner was narrowed, and otherwise the ## body of a message the caller raises under its own name. The learners are ## left untouched when it is not empty, so a code is never half narrowed. ## ## @seealso{CompactClassificationECOC, ClassificationECOC} ## @end deftypefn function [learners, errmsg] = ecocSelectModels (learners, idx) errmsg = ''; out = learners; for j = 1:numel (learners) if (! ismethod (learners{j}, 'selectModels')) errmsg = strcat ("the binary learners are", " '", ... class (learners{j}), "' models, which are fitted", ... " over one regularization strength and have none", ... " to select between."); return; endif try out{j} = selectModels (learners{j}, idx); catch err errmsg = err.message; return; end_try_catch endfor learners = out; endfunction statistics-release-2.1.2/inst/Machine_Learning/private/edgeWeights.m000066400000000000000000000103041527311104300255460ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{W} =} edgeWeights (@var{args}, @var{Y}, @var{ClassNames}, @var{Prior}, @var{classname}, @var{caller}) ## ## Normalized observation weights of an @code{edge} call. ## ## @var{args} is the @code{varargin} of the caller, which accepts a single ## @qcode{'Weights'} Name-Value pair; without one, or with an empty one, the ## weights are uniform. ## ## The weights are then normalized @strong{within each class to that class's ## prior}, which is what MATLAB does and is not the same as dividing by their ## total. It matters whenever the weights are not uniform: on a weighted iris ## edge the two readings are 0.9269539697 and 0.9438468986, and the latter is ## the oracle's. Normalizing this way keeps a class's total influence equal to ## its prior however the weights inside it are distributed, so reweighting ## observations cannot quietly reweight the classes. ## ## @var{classname} and @var{caller} name the class and the method in the error ## messages, so a class reports its own name rather than this helper's. ## ## @end deftypefn function W = edgeWeights (args, Y, ClassNames, Prior, classname, caller) n = rows (Y); if (mod (numel (args), 2) != 0) error ("%s.%s: optional arguments must be given in Name-Value pairs.", ... classname, caller); endif ## Parse optional paired arguments; an empty 'Weights' stands for uniform ## weights [W, args] = parsePairedArguments ({'Weights'}, {[]}, args(:)); ## Validate optional paired arguments errmsg = weightsClass (W); if (! isempty (errmsg)) error ("%s.%s: %s", classname, caller, errmsg); endif if (! isempty (W) && ! (isnumeric (W) && isvector (W))) error ("%s.%s: 'Weights' must be a numeric vector.", classname, caller); endif if (! isempty (args)) error ("%s.%s: invalid optional paired argument.", classname, caller); endif if (isempty (W)) W = ones (n, 1); elseif (numel (W) != n) error (strcat ("%s.%s: size of 'Weights' must equal the number", ... " of rows in X."), classname, caller); endif W = double (W(:)); ## Which class each observation belongs to. Matching against ClassNames is ## the general route; a response already in some other coding, such as the ## +1/-1 a support vector machine accepts, falls back to its own grouping. gY = classindex (Y, ClassNames); if (isempty (gY)) gY = grp2idx (Y); Prior = ones (1, max (gY)) / max (gY); endif for k = 1:numel (Prior) ck = (gY == k); if (any (ck) && sum (W(ck)) > 0) W(ck) = W(ck) / sum (W(ck)) * Prior(k); endif endfor endfunction ## Class index of every element of Y against ClassNames, empty when any ## element fails to match. function gY = classindex (Y, ClassNames) gY = []; n = rows (Y); idx = cell (n, 1); if (iscellstr (ClassNames)) if (! (iscellstr (Y) || ischar (Y))) return; endif Yc = cellstr (Y); for i = 1:n idx{i} = find (strcmp (Yc{i}, ClassNames), 1); endfor elseif (ischar (ClassNames)) CN = cellstr (ClassNames); if (! (iscellstr (Y) || ischar (Y))) return; endif Yc = cellstr (Y); for i = 1:n idx{i} = find (strcmp (Yc{i}, CN), 1); endfor else if (! (isnumeric (Y) || islogical (Y))) return; endif for i = 1:n idx{i} = find (Y(i) == ClassNames(:), 1); endfor endif if (any (cellfun (@isempty, idx))) return; endif gY = cell2mat (idx); endfunction statistics-release-2.1.2/inst/Machine_Learning/private/encodeLabels.m000066400000000000000000000041631527311104300256750ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{v} =} encodeLabels (@var{v}) ## ## A categorical or string array as a structure @code{save} can write. ## ## @code{save} cannot write a classdef object, so a model saving its class ## names or its response in either type writes this structure in their place, ## and @code{decodeLabels} rebuilds the array when the model is loaded. A ## categorical array keeps its codes, categories, order and protection, and a ## string array its text and which of its elements are missing. A structure ## array has every field of every element encoded in turn, which reaches class ## names a model keeps inside a structure. Any other value is returned as it ## is. ## ## @seealso{decodeLabels} ## @end deftypefn function v = encodeLabels (v) if (isa (v, 'categorical')) v = struct ('LabelType', 'categorical', 'Size', size (v), ... 'Codes', double (v), 'Categories', {categories(v)}, ... 'Ordinal', isordinal (v), 'Protected', isprotected (v)); elseif (isa (v, 'string')) v = struct ('LabelType', 'string', 'Size', size (v), ... 'Text', {cellstr(v)}, 'Missing', ismissing (v)); elseif (isstruct (v)) fn = fieldnames (v); for e = 1:numel (v) for j = 1:numel (fn) v(e).(fn{j}) = encodeLabels (v(e).(fn{j})); endfor endfor endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/ensemblePartition.m000066400000000000000000000071401527311104300267770ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{P}, @var{errmsg}] =} ensemblePartition (@var{args}, @var{Y}, @var{n}, @var{stratify}) ## ## The partition a cross-validated ensemble is fitted on. ## ## @var{args} holds the cross-validation pairs of the call: one of ## @qcode{'CrossVal'}, @qcode{'KFold'}, @qcode{'Holdout'}, @qcode{'Leaveout'} ## and @qcode{'CVPartition'}, or none, which gives ten folds. @var{Y} is the ## response and @var{n} the number of observations; @var{stratify} says ## whether the folds keep the class proportions of @var{Y}, as a classifier's ## do. @qcode{'CrossVal'} set to @qcode{'off'} gives an empty @var{P}. ## @var{errmsg} is the body of the message the caller should raise, or empty. ## ## @end deftypefn function [P, errmsg] = ensemblePartition (args, Y, n, stratify) P = []; errmsg = ""; if (mod (numel (args), 2) != 0) errmsg = "name-value arguments must be in pairs."; return; endif if (numel (args) > 2) errmsg = strcat ("specify only one of 'CrossVal', 'KFold', 'Holdout',", ... " 'Leaveout' and 'CVPartition'."); return; endif if (stratify) data = Y; else data = n; endif kfold = min (10, n); if (isempty (args)) P = cvpartition (data, 'KFold', kfold); return; endif name = args{1}; val = args{2}; if (! ischar (name)) errmsg = "invalid parameter name in optional pair arguments."; return; endif switch (tolower (name)) case 'crossval' if (! (ischar (val) && any (strcmpi (val, {'on', 'off'})))) errmsg = "'CrossVal' must be 'on' or 'off'."; elseif (strcmpi (val, 'on')) P = cvpartition (data, 'KFold', kfold); endif case 'kfold' if (! (isnumeric (val) && isscalar (val) && isreal (val) && val > 1 && val == fix (val))) errmsg = "'KFold' must be an integer greater than 1."; else P = cvpartition (data, 'KFold', val); endif case 'holdout' if (! (isnumeric (val) && isscalar (val) && isreal (val) && val > 0 && val < 1)) errmsg = "'Holdout' must be a number between 0 and 1."; else P = cvpartition (data, 'Holdout', val); endif case 'leaveout' if (! (ischar (val) && any (strcmpi (val, {'on', 'off'})))) errmsg = "'Leaveout' must be 'on' or 'off'."; elseif (strcmpi (val, 'on')) P = cvpartition (n, 'LeaveOut'); endif case 'cvpartition' if (! isa (val, 'cvpartition')) errmsg = "'CVPartition' must be a 'cvpartition' object."; elseif (numel (test (val, 1)) != n) errmsg = strcat ("'CVPartition' must partition the observations", ... " the ensemble was fitted on."); else P = val; endif otherwise errmsg = "invalid parameter name in optional pair arguments."; endswitch endfunction statistics-release-2.1.2/inst/Machine_Learning/private/fcnnInitializers.m000066400000000000000000000045731527311104300266350ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{init} =} fcnnInitializers (@var{acts}, @var{nlayers}, @var{outact}) ## ## The weight initializer each layer of a fully connected network is built ## with. ## ## @var{acts} names the hidden layers, either as one character vector applying ## to all @var{nlayers} of them or as a cellstring naming them one by one, and ## @var{outact} names the output layer. @var{init} is a cellstring of ## @var{nlayers}+1 entries, the output layer last, each one @qcode{'he'} or ## @qcode{'glorot'}. ## ## The scheme is not a setting and cannot be chosen: @code{init_scale} in ## @file{src/fcnn.cpp} picks it from the layer's activation alone, the ## rectifiers taking the wider He range because they pass only half their ## input and the symmetric activations taking Glorot, which accounts for the ## backward pass as well. This function restates that rule so the model can ## report what it was built with; the two must be changed together. ## ## @end deftypefn function init = fcnnInitializers (acts, nlayers, outact) ## The rectifiers, by the names fcnntrain accepts for them. Everything ## else the engine knows ('linear', 'none', 'sigmoid', 'tanh', 'softmax') ## takes Glorot. rectifiers = {'relu', 'lrelu', 'prelu', 'elu', 'gelu'}; if (ischar (acts)) names = repmat ({acts}, 1, nlayers); else names = cellstr (acts); names = names(:)'; endif names = [names, {outact}]; init = cell (1, numel (names)); for i = 1:numel (names) if (any (strcmpi (rectifiers, names{i}))) init{i} = 'he'; else init{i} = 'glorot'; endif endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/foldModels.m000066400000000000000000000046131527311104300254050ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{Trained} =} foldModels (@var{learner}, @var{F}, @var{Partition}, @var{args}) ## ## Fit one model per fold of a partition. ## ## @var{learner} names the class to fit, @var{F} is the frame ## @code{classFrame} returned (or, for a regression model, a structure ## carrying @qcode{X}, @qcode{Y} and @qcode{Weights} alone), @var{Partition} ## a @code{cvpartition} over the retained observations, and @var{args} the ## Name-Value pairs each fold is fitted with. ## ## Every fold is given the parent's class names, prior and cost rather than ## being left to re-derive them from its own rows. Measured on R2024a: the ## prior of a fold of an unbalanced problem is the parent's, not the fold's ## own frequencies. A fold that happened to hold one class only would ## otherwise renumber the classes, and every score assembled from it would ## be inverted without anything raising. ## ## What is @emph{not} passed down is any option that resolves to ## @qcode{'auto'}. Each fold works those out from its own training rows, so ## a fold of a hundred observations partitioned five ways gets ## @qcode{Lambda} of one eightieth rather than one hundredth, and its own ## @qcode{Epsilon}. Both were measured rather than assumed. ## ## @end deftypefn function Trained = foldModels (learner, F, Partition, args) K = Partition.NumTestSets; Trained = cell (K, 1); for k = 1:K idx = training (Partition, k); fargs = args; if (! isempty (F.Weights)) fargs = [fargs, {'Weights', F.Weights(idx)}]; endif Trained{k} = feval (learner, F.X(idx,:), F.Y(idx,:), fargs{:}); endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/foldSets.m000066400000000000000000000041021527311104300250710ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{sets} =} foldSets (@var{Partition}, @var{Folds}, @var{Mode}, @var{n}) ## ## The sets of observations a @code{kfold} method reports over. ## ## Under @qcode{'individual'} there is one set per named fold, holding the ## observations that fold held out. Under @qcode{'average'} there is one ## set holding the observations of all the named folds together. ## ## The distinction matters, and it is not the one the name suggests: ## averaging pools the observations rather than averaging the per-fold ## values, so folds of unequal size do not weigh equally. Measured on ## R2024a: a four-fold cross-validation of carsmall, whose folds hold 24, ## 23, 23 and 23 observations, reports 64.0992586069075 where the mean of ## its own four fold losses is 64.2539. The first is the mean squared error ## over all ninety-three out-of-fold predictions, and that is what MATLAB ## returns. ## ## @end deftypefn function sets = foldSets (Partition, Folds, Mode, n) if (strcmp (Mode, 'individual')) sets = cell (numel (Folds), 1); for i = 1:numel (Folds) sets{i} = test (Partition, Folds(i)); endfor else pooled = false (n, 1); for i = 1:numel (Folds) pooled = pooled | test (Partition, Folds(i)); endfor sets = {pooled}; endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/gamCatCode.m000066400000000000000000000043451527311104300253060ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{XC}, @var{E}, @var{cat}, @var{Levels}] =} gamCatCode (@var{TreeModel}, @var{X}, @var{BinEdges}) ## ## Code the categorical predictors of a boosted-tree GAM for its engine. ## ## @code{@var{TreeModel}.CategoricalLevels} has one element per column of ## @var{X}: empty for a continuous predictor, else the sorted levels a ## categorical predictor held in training. In @var{XC} each categorical column ## holds the index of its level, and @qcode{NaN} for a missing or unseen level, ## which the engine treats alike. @var{E} is @var{BinEdges} with the cut ## points between those indices in place of each categorical predictor's ## entry, @var{cat} flags the categorical columns and @var{Levels} is the cell ## of levels. A model without the field, fitted before categorical predictors ## were accepted or with none, leaves @var{X} and @var{BinEdges} unchanged. ## ## @end deftypefn function [XC, E, cat, Levels] = gamCatCode (TreeModel, X, BinEdges) XC = X; E = {}; if (nargin > 2) E = BinEdges; endif Levels = cell (1, columns (X)); if (isstruct (TreeModel) && isfield (TreeModel, 'CategoricalLevels')) Levels = TreeModel.CategoricalLevels; endif cat = ! cellfun ('isempty', Levels); for j = find (cat) [~, loc] = ismember (X(:,j), Levels{j}); code = loc; code(loc == 0) = NaN; XC(:,j) = code; if (nargin > 2) E{j} = (1:numel (Levels{j}) - 1) + 0.5; endif endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/gamPairEdges.m000066400000000000000000000044451527311104300256500ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{PE}, @var{PM}] =} gamPairEdges (@var{TreeModel}, @var{DetectionEdges}) ## ## The cut points and missing-value entries of a boosted-tree GAM's pairs. ## ## @var{PE} has one element per row of @code{@var{TreeModel}.Pairs}, each a ## @math{1x2} cell of the cut points on the pair's two predictors, and @var{PM} ## the matching @math{1x3} cells of what a row missing the first predictor, ## the second or both takes, as @code{gamboostinter} returns them in ## @qcode{PairEdges} and @qcode{PairMissing}. A model saved before pair terms ## had grids of their own holds its surfaces on the detection grid ## @var{DetectionEdges}, one row vector per predictor, which is then read for ## both predictors of each pair; a model saved before pairs held missing ## values gets zeros for them, so such a pair still contributes nothing. ## ## @end deftypefn function [PE, PM] = gamPairEdges (TreeModel, DetectionEdges) pairs = TreeModel.Pairs; np = rows (pairs); if (isfield (TreeModel, 'PairEdges') && numel (TreeModel.PairEdges) == np) PE = TreeModel.PairEdges; else PE = cell (1, np); for q = 1:np PE{q} = {DetectionEdges{pairs(q,1)}(:)', DetectionEdges{pairs(q,2)}(:)'}; endfor endif if (isfield (TreeModel, 'PairMissing') && numel (TreeModel.PairMissing) == np) PM = TreeModel.PairMissing; else PM = cell (1, np); for q = 1:np [nj, nk] = size (TreeModel.PairValues{q}); PM{q} = {zeros(1, nk), zeros(1, nj), 0}; endfor endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/gamPairMerge.m000066400000000000000000000056611527311104300256610ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{PE}, @var{PV}, @var{PM}] =} gamPairMerge (@var{PE1}, @var{PV1}, @var{PM1}, @var{PE2}, @var{PV2}, @var{PM2}) ## ## Add two sets of interaction surfaces held on different grids. ## ## Each pair's two surfaces are read on the union of their cut points, at a ## point strictly inside every cell, and summed, so the result predicts what ## the two sets predict together. What a row missing one predictor takes is ## read and summed the same way on the union of the other predictor's cut ## points, and what a row missing both takes is summed. @var{PE1} and ## @var{PE2} are cells of per-pair cut points and @var{PM1} and @var{PM2} of ## missing-value entries, as @code{gamPairEdges} returns them, and @var{PV1} ## and @var{PV2} the matching cells of surfaces. ## ## @end deftypefn function [PE, PV, PM] = gamPairMerge (PE1, PV1, PM1, PE2, PV2, PM2) np = numel (PV1); PE = cell (1, np); PV = cell (1, np); PM = cell (1, np); for q = 1:np ej = unique ([PE1{q}{1}(:); PE2{q}{1}(:)])'; ek = unique ([PE1{q}{2}(:); PE2{q}{2}(:)])'; xj = cellPoints (ej); xk = cellPoints (ek); PV{q} = surfaceAt (PE1{q}, PV1{q}, xj, xk) ... + surfaceAt (PE2{q}, PV2{q}, xj, xk); PE{q} = {ej, ek}; ## A row missing the first predictor is read by cell of the second, and ## the other way round. mj = lineAt (PE1{q}{2}, PM1{q}{1}, xk) + lineAt (PE2{q}{2}, PM2{q}{1}, xk); mk = lineAt (PE1{q}{1}, PM1{q}{2}, xj) + lineAt (PE2{q}{1}, PM2{q}{2}, xj); PM{q} = {mj, mk, PM1{q}{3} + PM2{q}{3}}; endfor endfunction ## A point strictly inside each cell the cut points E define. function x = cellPoints (e) if (isempty (e)) x = 0; else x = [e(1) - 1, (e(1:end-1) + e(2:end)) / 2, e(end) + 1]; endif endfunction ## The surface S held on the cut points E, read at every pair of XJ and XK. function V = surfaceAt (E, S, xj, xk) a = 1 + sum (xj(:) > E{1}(:)', 2); b = 1 + sum (xk(:) > E{2}(:)', 2); V = S(a, b); endfunction ## The values L held on the cut points E, read at every point of X. function v = lineAt (E, L, x) idx = 1 + sum (x(:) > E(:)', 2); v = L(idx); v = v(:)'; endfunction statistics-release-2.1.2/inst/Machine_Learning/private/gpBasis.m000066400000000000000000000040441527311104300247030ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} gpBasis (@var{X}, @var{basis}) ## ## Build the explicit basis matrix of a Gaussian process regression. ## ## @var{X} is @math{NxD} and the returned @var{H} is @math{NxP}, one column per ## basis term, so that the deterministic part of the model is @qcode{H * Beta}. ## @var{basis} is @qcode{'None'}, @qcode{'Constant'}, @qcode{'Linear'}, ## @qcode{'PureQuadratic'} as the model stores it, or a function handle taking ## @var{X} and returning the matrix itself. ## ## @qcode{'None'} gives a matrix with no columns, so the model has no ## deterministic part and @qcode{Beta} is empty. ## ## @end deftypefn function H = gpBasis (X, basis) n = rows (X); if (is_function_handle (basis)) H = basis (X); if (! isnumeric (H) || ! ismatrix (H) || rows (H) != n) error (strcat ("gpBasis: the basis function must return a matrix", ... " with one row per observation.")); endif return; endif switch (lower (basis)) case 'none' H = zeros (n, 0); case 'constant' H = ones (n, 1); case 'linear' H = [ones(n, 1), X]; case 'purequadratic' H = [ones(n, 1), X, X .^ 2]; otherwise error ("gpBasis: unrecognized basis function '%s'.", basis); endswitch endfunction statistics-release-2.1.2/inst/Machine_Learning/private/gpKernel.m000066400000000000000000000067731527311104300250750ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} gpKernel (@var{A}, @var{B}, @var{name}, @var{theta}) ## ## Evaluate a Gaussian process covariance function between two sets of points. ## ## @var{A} is @math{NxD} and @var{B} is @math{MxD}, and the returned matrix is ## @math{NxM} holding the covariance of every pair. @var{name} is one of the ## ten built-in covariance functions, spelled as the model stores it, and ## @var{theta} is its parameter vector. ## ## The isotropic kernels take @qcode{[SigmaL; SigmaF]}, the rational quadratic ## @qcode{[SigmaL; AlphaRQ; SigmaF]}, the automatic relevance determination ## kernels one length scale per predictor followed by @qcode{SigmaF}, and the ## ARD rational quadratic those length scales, @qcode{AlphaRQ} and ## @qcode{SigmaF}. Every kernel is stationary, so the covariance depends on ## the two points only through the distance between them, scaled by the length ## scale of each predictor. ## ## @end deftypefn function K = gpKernel (A, B, name, theta) d = columns (A); ard = (numel (name) > 3 && strncmpi (name, 'ARD', 3)); ## Split the parameter vector and scale each predictor by its length scale, ## which reduces every kernel below to a function of the scaled distance. if (ard) L = theta(1:d)(:)'; if (strcmpi (name, 'ARDRationalQuadratic')) alphaRQ = theta(d+1); sigmaF = theta(d+2); else sigmaF = theta(d+1); endif else L = repmat (theta(1), 1, d); if (strcmpi (name, 'RationalQuadratic')) alphaRQ = theta(2); sigmaF = theta(3); else sigmaF = theta(2); endif endif ## Squared distances, accumulated one predictor at a time. The expanded ## form, sum (A.^2) - 2*A*B' + sum (B.^2), is faster but does not return ## exactly zero for a point against itself: it leaves a residue of the order ## of eps, and the square root of 1.8e-15 is 4.2e-8, so a stationary kernel ## stops returning exactly SigmaF^2 on its own diagonal. The rough kernels ## take the square root and inherit that as an error eight orders larger ## than the one it came from. r2 = zeros (rows (A), rows (B)); for j = 1:d r2 += ((A(:,j) - B(:,j)') / L(j)) .^ 2; endfor switch (lower (strrep (lower (name), 'ard', ''))) case 'exponential' K = sigmaF^2 * exp (-sqrt (r2)); case 'squaredexponential' K = sigmaF^2 * exp (-0.5 * r2); case 'matern32' s = sqrt (3) * sqrt (r2); K = sigmaF^2 * (1 + s) .* exp (-s); case 'matern52' s = sqrt (5) * sqrt (r2); K = sigmaF^2 * (1 + s + (5/3) * r2) .* exp (-s); case 'rationalquadratic' K = sigmaF^2 * (1 + r2 / (2 * alphaRQ)) .^ (-alphaRQ); otherwise error ("gpKernel: unrecognized kernel function '%s'.", name); endswitch endfunction statistics-release-2.1.2/inst/Machine_Learning/private/gpPredict.m000066400000000000000000000073051527311104300252370ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} gpPredict (@var{XC}, @var{M}) ## @deftypefnx {Private Function} [@var{ypred}, @var{ysd}, @var{yint}] = gpPredict (@var{XC}, @var{M}) ## ## Evaluate a fitted Gaussian process at new points. ## ## @var{M} is a structure carrying the fitted model: the active set vectors it ## predicts from, the prediction weights @qcode{Alpha}, the covariance function ## and its parameters, the explicit basis and its coefficients, the noise ## standard deviation, the standardizing location and scale where the model ## standardized its predictors, and the significance level of the intervals. ## ## The mean is the basis term plus the covariance between the new points and ## the active set weighted by @qcode{Alpha}, which is what @qcode{Alpha} is ## for. The standard deviation is that of the @emph{response}, so it carries ## the noise as well as the uncertainty of the latent function, and the ## interval is the normal quantile of the level times it. The coefficients of ## the explicit basis are taken as known, which is measurably what MATLAB does: ## carrying their covariance as well moves the answer in the sixth decimal. ## ## @end deftypefn function [ypred, ysd, yint] = gpPredict (XC, M) ## Standardize the new points exactly as the training data was standardized if (! isempty (M.Location)) XC = (XC - M.Location) ./ M.Scale; endif ## Mean prediction Kq = gpCov (XC, M.X, M); HC = gpBasis (XC, M.BasisFunction); ypred = Kq * M.Alpha; if (! isempty (M.Beta)) ypred += HC * M.Beta(:); endif if (nargout < 2) return; endif ## The variance of a new response is the prior variance of the latent ## function, less what the training data explains of it, plus the noise. ## The factorisation is rebuilt here rather than stored: a compact model ## keeps its active set and nothing else, and this is the only method that ## needs it. n = rows (M.X); A = gpCov (M.X, M.X, M) + M.Sigma^2 * eye (n); V = A \ Kq'; kqq = gpSelfCov (XC, M); vy = kqq - sum (Kq' .* V, 1)' + M.Sigma^2; vy(vy < 0) = 0; ysd = sqrt (vy); if (nargout < 3) return; endif z = norminv (1 - M.CIAlpha / 2); yint = [ypred - z * ysd, ypred + z * ysd]; endfunction ## Covariance between two sets of points, built in or user supplied. function K = gpCov (A, B, M) if (is_function_handle (M.KernelFunction)) K = M.KernelFunction (A, B, M.Theta); else K = gpKernel (A, B, M.KernelFunction, M.Theta); endif endfunction ## The prior variance of a point with itself. Every built-in kernel is ## stationary, so this is the signal variance and does not depend on where the ## point is; a supplied kernel need not be, and is asked. function v = gpSelfCov (XC, M) if (is_function_handle (M.KernelFunction)) v = zeros (rows (XC), 1); for i = 1:rows (XC) v(i) = M.KernelFunction (XC(i,:), XC(i,:), M.Theta); endfor else v = repmat (M.Theta(end)^2, rows (XC), 1); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/interactionPairs.m000066400000000000000000000033501527311104300266300ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{P} =} interactionPairs (@var{M}) ## The predictor index pairs of the two-way terms in a term matrix. ## ## @var{M} is the term matrix a GAM fits, one row per term and one column per ## predictor, true wherever the term multiplies that predictor. @var{P} is ## the @math{Kx2} matrix of predictor indices of the rows naming exactly two ## predictors, in the order those rows appear, and @code{zeros (0, 2)} when ## there are none. ## ## A row naming one predictor is a main effect and a row naming three or more ## is a higher-order term. Neither has a two-column form, so neither appears ## here; the term matrix itself remains the complete record. ## @end deftypefn function P = interactionPairs (M) if (isempty (M)) P = zeros (0, 2); return; endif M = logical (M); keep = find (sum (M, 2) == 2); P = zeros (numel (keep), 2); for i = 1:numel (keep) P(i,:) = find (M(keep(i),:)); endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/kernelBasis.m000066400000000000000000000055111527311104300255550ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{B} =} kernelBasis (@var{p}, @var{m}, @var{sigma}) ## ## Draw a random basis for a Gaussian kernel approximation. ## ## @var{p} is the number of predictors, @var{m} the number of expansion ## dimensions and @var{sigma} the kernel scale. @var{B} is a structure and ## is what @code{kernelExpand} applies to a predictor matrix. ## ## The basis is the random Fourier expansion of Rahimi and Recht. Frequencies ## are drawn from @math{N(0, sigma^-2 I)} and each contributes a cosine and a ## sine of the same frequency, so that ## @code{T(x1) * T(x2)'} has expectation ## @math{exp (-||x1 - x2||^2 / (2 * sigma^2))}, the Gaussian kernel. ## ## The paired form is used rather than the single cosine with a uniform ## phase offset that MathWorks writes down. Both are unbiased for the same ## kernel and cost the same, but pairing removes the variance the random ## phase adds: measured over twenty draws on the iris predictors, the mean ## absolute error of the approximated kernel is 0.034 against 0.071 at 128 ## dimensions and 0.020 against 0.033 at 512, so the pairs approximate about ## twice as closely for the same @var{m}. An odd @var{m} cannot be paired ## throughout, and the odd dimension left over is a phase-offset cosine, ## which carries the same expectation. ## ## MATLAB approximates the kernel by the Fastfood construction, which ## replaces the Gaussian frequency matrix with a product of Hadamard, ## permutation and diagonal factors to reach the same distribution in ## @math{O(M log P)} time. Neither that difference nor this one is ## observable in a single model: the draws come from different generators, so ## a model fitted here and one fitted in MATLAB hold different bases and ## report different scores from any seed. ## ## @end deftypefn function B = kernelBasis (p, m, sigma) pairs = floor (m / 2); odd = m - 2 * pairs; B = struct (); B.Weights = randn (p, pairs) / sigma; B.OddWeights = randn (p, odd) / sigma; B.OddOffset = 2 * pi * rand (1, odd); B.NumDimensions = m; B.Scale = sigma; endfunction statistics-release-2.1.2/inst/Machine_Learning/private/kernelExpand.m000066400000000000000000000026501527311104300257340ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{T} =} kernelExpand (@var{X}, @var{B}) ## ## Map predictor data into the expanded feature space of a random basis. ## ## @var{X} is an @math{NxP} numeric matrix and @var{B} a basis structure from ## @code{kernelBasis}. @var{T} is @math{NxM}, and the inner product of two ## of its rows approximates the Gaussian kernel of the two rows of @var{X} ## they came from. ## ## @end deftypefn function T = kernelExpand (X, B) c = sqrt (2 / B.NumDimensions); Z = X * B.Weights; T = c * [cos(Z), sin(Z)]; if (! isempty (B.OddOffset)) T = [T, c * cos(X * B.OddWeights + B.OddOffset)]; endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/kernelFitInfo.m000066400000000000000000000036661527311104300260630ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{S} =} kernelFitInfo (@var{info}, @var{LossFun}, @var{Lambda}, @var{BetaTol}, @var{GradTol}) ## ## Assemble the @qcode{FitInfo} structure the kernel fitting functions ## return beside the model. ## ## It is not the structure the linear fitting functions return: a kernel ## model is always fitted by one solver under one regularization strength, ## so the fields are scalars and the ones that describe a choice of solver ## are absent. The names differ too, @qcode{ObjectiveValue} and ## @qcode{GradientMagnitude} here against @qcode{Objective} and ## @qcode{GradientNorm} there, and both spellings are MATLAB's. ## ## @end deftypefn function S = kernelFitInfo (info, LossFun, Lambda, BetaTol, GradTol) S = struct (); S.Solver = 'LBFGS-fast'; S.LossFunction = LossFun; S.Lambda = Lambda; S.BetaTolerance = BetaTol; S.GradientTolerance = GradTol; S.ObjectiveValue = info.Objective; S.GradientMagnitude = info.GradientNorm; if (isempty (info.RelativeChangeInBeta)) S.RelativeChangeInBeta = NaN; else S.RelativeChangeInBeta = info.RelativeChangeInBeta; endif S.FitTime = 0; S.History = []; endfunction statistics-release-2.1.2/inst/Machine_Learning/private/kfoldOpts.m000066400000000000000000000104211527311104300252540ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{O} =} kfoldOpts (@var{args}, @var{validLoss}, @var{classname}, @var{caller}, @var{K}) ## @deftypefnx {Private Function} {@var{O} =} kfoldOpts (@dots{}, @var{modes}, @var{handles}) ## ## Parse the options the @code{kfold} methods share. ## ## @var{O} carries @qcode{Mode}, one of @var{modes}, which default to ## @qcode{'average'} and @qcode{'individual'}, the first being the default ## mode; @qcode{Folds}, a row of fold indices defaulting to all @var{K} of ## them; and @qcode{LossFun}, empty unless one was given. ## ## @var{validLoss} lists the losses the caller accepts; pass an empty cell ## for a method that takes no @qcode{'LossFun'} at all, and the option is ## then refused like any other unknown name. @var{handles}, false by ## default, says whether a function handle is accepted as a loss as well. ## ## @end deftypefn function O = kfoldOpts (args, validLoss, classname, caller, K, ... modes = {'average', 'individual'}, handles = false) O = struct ('Mode', modes{1}, 'Folds', 1:K, 'LossFun', ''); if (mod (numel (args), 2) != 0) error (strcat ("%s.%s: optional arguments must be given in", ... " Name-Value pairs."), classname, caller); endif while (numel (args) > 0) switch (lower (args{1})) case 'mode' O.Mode = args{2}; if (! (ischar (O.Mode) && any (strcmpi (O.Mode, modes)))) if (numel (modes) == 2) error ("%s.%s: 'Mode' must be either '%s' or '%s'.", ... classname, caller, modes{:}); endif error ("%s.%s: 'Mode' must be %s.", classname, caller, ... listing (quoted (modes))); endif O.Mode = lower (O.Mode); case 'folds' O.Folds = args{2}; if (! (isnumeric (O.Folds) && isreal (O.Folds) && isvector (O.Folds) && ! isempty (O.Folds) && all (fix (O.Folds) == O.Folds) && all (O.Folds >= 1) && all (O.Folds <= K))) error (strcat ("%s.%s: 'Folds' must hold integers between 1", ... " and %d."), classname, caller, K); endif O.Folds = O.Folds(:)'; case 'lossfun' if (isempty (validLoss)) error (strcat ("%s.%s: invalid parameter name in optional pair", ... " arguments."), classname, caller); endif O.LossFun = args{2}; if (handles && is_function_handle (O.LossFun)) args(1:2) = []; continue; endif if (! (ischar (O.LossFun) && any (strcmpi (O.LossFun, validLoss)))) q = quoted (validLoss); if (handles) q{end+1} = 'a function handle'; endif error ("%s.%s: 'LossFun' must be %s.", classname, caller, ... listing (q)); endif O.LossFun = lower (O.LossFun); otherwise error (strcat ("%s.%s: invalid parameter name in optional pair", ... " arguments."), classname, caller); endswitch args(1:2) = []; endwhile endfunction ## The accepted values, quoted and listed the way an error message wants ## them, with 'or' before the last. function q = quoted (names) q = cellfun (@(n) sprintf ("'%s'", n), names, 'UniformOutput', false); endfunction function s = listing (q) if (numel (q) == 1) s = q{1}; elseif (numel (q) == 2) s = sprintf ("%s or %s", q{1}, q{2}); else s = sprintf ("%s, or %s", strjoin (q(1:end-1), ', '), q{end}); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/kfoldResponse.m000066400000000000000000000032301527311104300261250ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{yFit} =} kfoldResponse (@var{Trained}, @var{Partition}, @var{X}, @var{L}) ## ## Assemble the out-of-fold predictions of a cross-validated regression ## model. ## ## Each observation is predicted by the fold that held it out, and an ## observation no fold held out comes back @qcode{NaN} rather than ## predicted. Under a holdout partition that is most of them. @var{yFit} ## is @math{NxL}, one column per regularization strength. ## ## The predictions are untransformed: the fold models carry no response ## transform and the parent applies its own once. ## ## @end deftypefn function yFit = kfoldResponse (Trained, Partition, X, L) yFit = nan (rows (X), L); for k = 1:Partition.NumTestSets idx = test (Partition, k); if (! any (idx)) continue; endif yFit(idx,:) = predict (Trained{k}, X(idx,:)); endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/kfoldScores.m000066400000000000000000000052101527311104300255650ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{labels}, @var{scores}] =} kfoldScores (@var{Trained}, @var{Partition}, @var{X}, @var{Y}, @var{ClassNames}, @var{L}) ## ## Assemble the out-of-fold labels and scores of a cross-validated binary ## classifier. ## ## Each observation is predicted by the fold that held it out, and an ## observation no fold held out is left missing rather than given a class. ## Under a holdout partition that is most of them. @var{Y} is the response, ## used only for its type, so that the labels come back in it. ## ## @var{L} is the number of regularization strengths the fold models carry, ## which is one for a kernel model and one per value of @qcode{'Lambda'} for ## a linear one. @var{scores} is @math{Nx2xL}, or @math{Nx2} when @var{L} ## is one. ## ## The scores are whatever the fold models report, transform included: a ## logistic fold returns posteriors. MATLAB leaves the transform with the ## folds and has the cross-validated model report none of its own, so a ## parent that transformed again would apply it twice. ## ## @end deftypefn function [labels, scores] = kfoldScores (Trained, Partition, X, Y, ... ClassNames, L) n = rows (X); K = Partition.NumTestSets; if (iscellstr (Y)) labels = repmat ({''}, n, L); elseif (islogical (Y)) labels = false (n, L); elseif (ischar (Y)) labels = repmat (' ', n, columns (ClassNames)); else labels = nan (n, L); endif scores = nan (n, 2, L); for k = 1:K idx = test (Partition, k); if (! any (idx)) continue; endif [lab, sc] = predict (Trained{k}, X(idx,:)); if (ischar (Y)) labels(idx,:) = lab; else labels(idx,:) = lab; endif if (L == 1) scores(idx,:,1) = sc; else scores(idx,:,:) = sc; endif endfor if (L == 1) scores = scores(:,:,1); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/knndistparam.m000066400000000000000000000035211527311104300260050ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{p} =} knndistparam (@var{Distance}, @var{X}, @var{standardized}) ## ## Distance parameter a nearest-neighbour metric implies. ## ## Three metrics carry one: @qcode{'minkowski'} the exponent, defaulting to 2; ## @qcode{'seuclidean'} a per-predictor scale, the standard deviations of ## @var{X}, or ones where the predictors were standardized already; and ## @qcode{'mahalanobis'} the covariance of @var{X}. Every other metric has ## none and @var{p} is empty. ## ## It is recomputed rather than carried over whenever @code{Distance} changes, ## which is what the oracle does: a scale belonging to one metric means ## nothing under another. ## ## @end deftypefn function p = knndistparam (Distance, X, standardized) p = []; if (! ischar (Distance)) return; endif switch (lower (Distance)) case 'minkowski' p = 2; case 'seuclidean' if (standardized) p = ones (1, columns (X)); else p = std (X, [], 1); endif case 'mahalanobis' p = cov (X); endswitch endfunction statistics-release-2.1.2/inst/Machine_Learning/private/labelIndices.m000066400000000000000000000050211527311104300256650ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{gY}, @var{errmsg}] =} labelIndices (@var{C}, @var{Y}) ## The class each observation of a response belongs to, for the whole response. ## ## @var{gY} indexes @var{C} once per observation, and is zero where a label is ## not one of the classes. @var{errmsg} is the body of the message the caller ## should raise, or empty when there is nothing wrong; the caller emits it ## under its own @code{class.method} name, as the package's shared validation ## helpers do. ## ## Class names and labels given as text of any kind, a character matrix, a cell ## array of character vectors, a categorical or a string array, are matched as ## whole names, the padding a character matrix carries stripped. Resolving every ## observation in one comparison is what this is for: asking per observation ## inside a loop costs a pass over the classes each time. ## @end deftypefn function [gY, errmsg] = labelIndices (C, Y) errmsg = ""; ## Class names given as text of any kind are compared as text: a character ## matrix row by row rather than element by element, and a categorical or ## string array by the names it holds, whichever side it is on. istext = @(v) iscellstr (v) || ischar (v) || isa (v, 'categorical') ... || isa (v, 'string'); if (istext (C) && istext (Y) && ! (isa (C, 'categorical') && isa (Y, 'categorical'))) C = cellstr (C); Y = cellstr (Y); endif if (iscellstr (C) && ! iscellstr (Y)) gY = zeros (rows (Y), 1); errmsg = "Y must be of the same type as the class names."; return; endif [tf, gY] = ismember (Y(:), C(:)); gY = gY(:); if (! all (tf)) errmsg = "Y must hold only classes the model was trained on."; endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/labelsFromIndex.m000066400000000000000000000024451527311104300263740ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{L} =} labelsFromIndex (@var{C}, @var{idx}) ## Pick class labels out of a @code{ClassNames} by index, in its own type. ## ## @var{idx} numbers the classes, and @var{L} holds the label of each, in the ## type @var{C} is in. A character matrix is indexed by row, every other ## accepted type by element, which is the same thing for a column. ## @end deftypefn function L = labelsFromIndex (C, idx) if (ischar (C)) L = C(idx, :); else L = C(idx); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/labelsKnown.m000066400000000000000000000027331527311104300255750ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{tf} =} labelsKnown (@var{Y}, @var{C}) ## Whether every distinct label in a response is one of the known classes. ## ## @code{ismember} compares two character matrices character by character, so ## a response naming its classes in the rows of one is answered against the ## letters of the class names rather than the names. Anything textual is ## compared as whole names here; every other accepted type compares by value. ## @end deftypefn function tf = labelsKnown (Y, C) ## Delegated so the matching rules live in one place: labelIndices reports ## a zero for any label that is not one of the classes. tf = all (labelIndices (C, Y) > 0); endfunction statistics-release-2.1.2/inst/Machine_Learning/private/linearFitInfo.m000066400000000000000000000070231527311104300260440ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{S} =} linearFitInfo (@var{info}, @var{BetaTol}, @var{GradTol}, @var{DeltaTol}, @var{IterLimit}, @var{Solver}, @var{PassLimit}, @var{BatchLimit}) ## ## Assemble the @qcode{FitInfo} structure the linear fitting functions ## return beside the model. ## ## @var{info} holds one element per regularization strength, as ## @code{linearSolve} reported it, and every field of @var{S} is a row over ## those strengths, so a single strength gives scalars. ## ## The field set depends on the solver, as it does in MATLAB. A ## quasi-Newton or proximal fit reports @qcode{IterationLimit}; a ## mini-batch or dual fit reports @qcode{PassLimit}, @qcode{NumPasses} and ## @qcode{BatchLimit} in its place, and adds @qcode{Alpha} for the dual and ## @qcode{BatchIndex} with @qcode{OptimalLearnRate} for the mini-batch ## solvers. Neither of the latter two runs a gradient test, so both report ## a tolerance of zero and, by the rule that a tolerance of zero means the ## test did not run, a gradient of @qcode{NaN}. ## ## @end deftypefn function S = linearFitInfo (info, BetaTol, GradTol, DeltaTol, IterLimit, ... Solver, PassLimit, BatchLimit) stochastic = any (ismember (Solver, {'sgd', 'asgd', 'dual'})); S = struct (); S.Lambda = [info.Lambda]; S.Objective = [info.Objective]; if (stochastic) S.PassLimit = PassLimit; S.NumPasses = fieldOrNaN (info, 'NumPasses'); S.BatchLimit = BatchLimit; else S.IterationLimit = IterLimit; endif S.NumIterations = [info.NumIterations]; if (stochastic) S.GradientNorm = nan (1, numel (info)); S.GradientTolerance = 0; else S.GradientNorm = [info.GradientNorm]; S.GradientTolerance = GradTol; endif S.RelativeChangeInBeta = fieldOrNaN (info, 'RelativeChangeInBeta'); S.BetaTolerance = BetaTol; if (any (strcmp (Solver, 'dual'))) S.DeltaGradient = fieldOrNaN (info, 'DeltaGradient'); S.DeltaGradientTolerance = DeltaTol; else S.DeltaGradient = []; S.DeltaGradientTolerance = []; endif S.TerminationCode = [info.TerminationCode]; S.TerminationStatus = [info.TerminationStatus]; if (any (strcmp (Solver, 'dual'))) S.Alpha = [info.Alpha]; endif if (any (ismember (Solver, {'sgd', 'asgd'}))) S.BatchIndex = fieldOrNaN (info, 'BatchIndex'); S.OptimalLearnRate = fieldOrNaN (info, 'OptimalLearnRate'); endif S.History = []; S.FitTime = 0; S.Solver = Solver; endfunction ## One row over the regularization strengths, with NaN wherever the solver ## that ran did not report the field at all. function v = fieldOrNaN (info, name) v = nan (1, numel (info)); for k = 1:numel (info) if (isfield (info(k), name) && ! isempty (info(k).(name))) v(k) = info(k).(name); endif endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/linearLoss.m000066400000000000000000000051361527311104300254310ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{L}, @var{dL}] =} linearLoss (@var{f}, @var{y}, @var{lossfun}, @var{epsilon}) ## ## Per-observation fitted loss of a linear model, and its derivative. ## ## @var{f} is the raw score @code{X * Beta + Bias}, @var{y} the response: ## @math{+1}/@math{-1} for the two classification losses and the observed ## value for the two regression losses. @var{L} is the loss of each ## observation and @var{dL} its derivative with respect to @var{f}. ## ## The four losses are MATLAB's, and the constants are its own as measured on ## R2024a rather than the textbook ones: @qcode{'mse'} carries the factor of ## one half that makes the reported objective agree, while @qcode{'hinge'}, ## @qcode{'logit'} and @qcode{'epsiloninsensitive'} carry none. Note that ## this is the loss the @emph{fit} minimizes, which is not the loss the ## @code{loss} method reports by default. ## ## At the kink of a non-differentiable loss the derivative is taken to be the ## one-sided value that is zero, which is the subgradient of least norm. ## ## @end deftypefn function [L, dL] = linearLoss (f, y, lossfun, epsilon) switch (lossfun) case 'hinge' m = 1 - y .* f; active = m > 0; L = m .* active; dL = -y .* active; case 'logit' ## log (1 + exp (-y * f)) evaluated so that neither tail overflows: ## log (1 + exp (u)) is max (u, 0) + log1p (exp (-abs (u))). u = -(y .* f); L = max (u, 0) + log1p (exp (-abs (u))); dL = -y ./ (1 + exp (-u)); case 'mse' r = f - y; L = 0.5 * (r .^ 2); dL = r; case 'epsiloninsensitive' r = f - y; m = abs (r) - epsilon; active = m > 0; L = m .* active; dL = sign (r) .* active; otherwise error ("linearLoss: unknown loss function '%s'.", lossfun); endswitch endfunction statistics-release-2.1.2/inst/Machine_Learning/private/linearModelParams.m000066400000000000000000000122051527311104300267100ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{MP} =} linearModelParams (@var{P}, @var{Solver}, @var{LambdaIn}, @var{EpsilonIn}, @var{Beta0}, @var{Bias0}, @var{Verbose}, @var{Type}) ## ## The @qcode{ModelParameters} structure of a linear model. ## ## Every option is reported, but only the ones the chosen solver can act on ## carry a value: the rest come back empty. That is MATLAB's behaviour and ## it is more informative than reporting a default that nothing used. ## Measured on R2024a, one fit per solver: ## ## @multitable @columnfractions 0.30 0.14 0.14 0.14 0.14 0.14 ## @headitem field @tab bfgs @tab lbfgs @tab sparsa @tab sgd @tab dual ## @item @qcode{BatchIndex} @tab @tab @tab @tab 0 @tab ## @item @qcode{BatchLimit} @tab @tab @tab @tab 0 @tab ## @item @qcode{BatchSize} @tab @tab @tab @tab 10 @tab ## @item @qcode{DeltaGradientTolerance} @tab @tab @tab @tab @tab yes ## @item @qcode{GradientTolerance} @tab yes @tab yes @tab yes @tab 0 @tab 0 ## @item @qcode{HessianHistorySize} @tab @tab 15 @tab @tab @tab ## @item @qcode{IterationLimit} @tab yes @tab yes @tab yes @tab @tab ## @item @qcode{LearnRate} @tab @tab @tab @tab yes @tab ## @item @qcode{LineSearch} @tab yes @tab yes @tab @tab @tab ## @item @qcode{NumCheckConvergence} @tab @tab @tab @tab 0 @tab yes ## @item @qcode{OptimizeLearnRate} @tab @tab @tab @tab yes @tab ## @item @qcode{PassLimit} @tab @tab @tab @tab yes @tab yes ## @item @qcode{TruncationPeriod} @tab @tab @tab @tab @tab ## @end multitable ## ## Two of those are worth noticing. @qcode{HessianHistorySize} is empty for ## @qcode{'bfgs'} and 15 for @qcode{'lbfgs'}, a full quasi-Newton method ## having no limited memory to size. And @qcode{GradientTolerance} is ## reported as 0 by the two solvers that do not run a gradient test, which ## by the rule that a tolerance of 0 switches its test off is a true ## statement rather than a missing one. ## ## @qcode{LineSearch} reports @qcode{'weakwolfe'}, the search ## @code{__lbfgs__} runs for these solvers, as MATLAB reports it. ## ## @end deftypefn function MP = linearModelParams (P, Solver, LambdaIn, EpsilonIn, Beta0, ... Bias0, Verbose, Type) quasi = any (ismember (Solver, {'bfgs', 'lbfgs'})); batch = any (ismember (Solver, {'sgd', 'asgd'})); dual = any (strcmp (Solver, 'dual')); iter = quasi || any (strcmp (Solver, 'sparsa')); MP = struct (); MP.BatchIndex = pick (batch, 0); MP.BatchLimit = pick (batch, ifelse (isempty (P.BatchLimit), 0, ... P.BatchLimit)); MP.BatchSize = pick (batch, P.BatchSize); MP.BetaTolerance = P.BetaTolerance; MP.DeltaGradientTolerance = pick (dual, P.DeltaGradientTolerance); MP.Epsilon = EpsilonIn; MP.FitBias = P.FitBias; ## The two solvers that run no gradient test report a tolerance of zero, ## which is how a switched-off test is spelled throughout this layer. MP.GradientTolerance = P.GradientTolerance; if (batch || dual) MP.GradientTolerance = 0; endif MP.HessianHistorySize = pick (any (strcmp (Solver, 'lbfgs')), ... P.HessianHistorySize); MP.InitialBeta = Beta0; MP.InitialBias = Bias0; MP.IterationLimit = pick (iter, P.IterationLimit); MP.Learner = P.Learner; MP.Lambda = LambdaIn; MP.LearnRate = pick (batch, P.LearnRate); MP.LineSearch = pick (quasi, 'weakwolfe'); MP.LossFunction = P.LossFunction; MP.NumCheckConvergence = pick (batch || dual, P.NumCheckConvergence); if (batch) MP.NumCheckConvergence = 0; endif MP.OptimizeLearnRate = pick (batch, P.OptimizeLearnRate); MP.PassLimit = pick (batch || dual, P.PassLimit); MP.PostFitBias = P.PostFitBias; MP.Regularization = P.Regularization; ## Solver arrives as a cell array of names already; wrapping it again ## would nest it, which struct () would have unwrapped but a direct ## assignment does not. MP.Solver = Solver; ## We take no RandomStream, so there is never one to report. MP.Stream = []; MP.TruncationPeriod = pick (batch && strcmp (P.Regularization, 'lasso'), ... P.TruncationPeriod); MP.VerbosityLevel = Verbose; MP.Method = 'Linear'; MP.Type = Type; endfunction ## The value when the solver can act on it, and empty when it cannot. function v = pick (used, value) if (used) v = value; else v = []; endif endfunction function v = ifelse (c, a, b) if (c) v = a; else v = b; endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/linearSolve.m000066400000000000000000000476751527311104300256170ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{Beta}, @var{Bias}, @var{S}] =} linearSolve (@var{X}, @var{y}, @var{w}, @var{P}) ## ## Minimize the regularized linear objective shared by the four linear and ## kernel learners. ## ## The objective is ## ## @example ## sum (@var{w} .* L (@var{X} * Beta + Bias, @var{y})) + Lambda * R (Beta) ## @end example ## ## with @var{w} summing to one, @code{L} the fitted loss of ## @code{linearLoss}, and @code{R} either @math{||Beta||^2 / 2} for ridge or ## @math{||Beta||_1} for lasso. The bias is never regularized. ## ## @var{P} is a scalar struct holding the resolved fitting options; the ## caller has already validated them and turned every @qcode{'auto'} into a ## number. @var{S} reports the fit: @qcode{Objective}, ## @qcode{NumIterations}, @qcode{GradientNorm}, @qcode{RelativeChangeInBeta}, ## @qcode{DeltaGradient}, @qcode{TerminationCode}, @qcode{TerminationStatus}, ## @qcode{History} and @qcode{Solver}. ## ## @end deftypefn function [Beta, Bias, S] = linearSolve (X, y, w, P) Beta = P.InitialBeta(:); Bias = P.InitialBias; if (! P.FitBias) Bias = 0; endif switch (P.Solver) case {'lbfgs', 'bfgs'} [Beta, Bias, S] = solveQuasiNewton (X, y, w, P, Beta, Bias); case 'sparsa' [Beta, Bias, S] = solveSpaRSA (X, y, w, P, Beta, Bias); case {'sgd', 'asgd'} [Beta, Bias, S] = solveSGD (X, y, w, P, Beta, Bias); case 'dual' [Beta, Bias, S] = solveDual (X, y, w, P, Beta, Bias); otherwise error ("linearSolve: unknown solver '%s'.", P.Solver); endswitch ## An explicit refit of the bias against the fitted scores, once the ## coefficients are settled. MATLAB calls this PostFitBias and leaves it ## off by default. if (P.FitBias && P.PostFitBias) Bias = refitBias (X * Beta, y, w, P); endif S.Objective = linearObjective (X, y, w, P, Beta, Bias); S.Solver = P.Solver; ## A tolerance of exactly zero does not mean "test against zero", it means ## the test does not run, and MATLAB then reports the quantity it governs ## as NaN rather than as a number. Measured on R2024a across a twelve ## point sweep of fitclinear. Note that the engine's LossTolerance is ## switched off with -Inf instead: the two conventions differ because ## MATLAB's do, each mirroring the quantity it governs, and unifying them ## would create a divergence rather than close one. if (P.GradientTolerance == 0) S.GradientNorm = NaN; endif if (P.BetaTolerance == 0) S.RelativeChangeInBeta = NaN; endif endfunction ## Objective value alone, for reporting. function f = linearObjective (X, y, w, P, Beta, Bias) L = linearLoss (X * Beta + Bias, y, P.LossFunction, P.Epsilon); f = sum (w .* L) + P.Lambda * penalty (Beta, P.Regularization); endfunction function r = penalty (Beta, reg) if (strcmp (reg, 'ridge')) r = sum (Beta .^ 2) / 2; else r = sum (abs (Beta)); endif endfunction ## Value and gradient of the smooth part, over the packed parameter vector. ## The lasso penalty is not part of it: SpaRSA handles that one by its prox. function [f, g] = smoothPart (z, X, y, w, P) p = columns (X); Beta = z(1:p); if (P.FitBias) Bias = z(p+1); else Bias = 0; endif [L, dL] = linearLoss (X * Beta + Bias, y, P.LossFunction, P.Epsilon); f = sum (w .* L); v = w .* dL; gBeta = X' * v; if (strcmp (P.Regularization, 'ridge')) f += P.Lambda * sum (Beta .^ 2) / 2; gBeta += P.Lambda * Beta; endif if (P.FitBias) gBias = sum (v); g = [gBeta; gBias]; else g = gBeta; endif endfunction ## LBFGS and BFGS, both served by the package's __lbfgs__ engine. function [Beta, Bias, S] = solveQuasiNewton (X, y, w, P, Beta, Bias) p = columns (X); z0 = Beta; if (P.FitBias) z0 = [Beta; Bias]; endif opt = struct (); opt.IterationLimit = P.IterationLimit; opt.GradientTolerance = P.GradientTolerance; opt.StepTolerance = 0; ## LossTolerance MUST stay -Inf. The engine's test is on the objective ## VALUE rather than on its change, which is MATLAB's convention and not ## what the name suggests, so any fit whose objective passes below 1e-6 on ## its way down stops there with the coefficients still wrong. Measured: ## an exact linear relation fitted at Lambda 1e-10 stops after 5 ## iterations with the coefficients out by 1.6e-4, against 11 iterations ## and 2.6e-10 with the test switched off. The BIST named ## 'LossTolerance' in RegressionLinear.m fails if this line is removed. ## ## Note that BetaTolerance is switched off with 0 and this one with -Inf. ## The inconsistency is deliberate: each mirrors MATLAB's own convention ## for the quantity it governs, and unifying them would create a ## divergence rather than close one. opt.LossTolerance = -Inf; opt.HistorySize = P.HessianHistorySize; opt.BetaTolerance = P.BetaTolerance; ## MATLAB's 'bfgs' keeps the full inverse Hessian and its 'lbfgs' the ## curvature pairs; both search for a weak Wolfe step from a unit one. ## With these, R2024a's fitclinear histories are reproduced to the ## iteration and its coefficients to 6e-12. opt.Method = P.Solver; opt.LineSearch = 'weakwolfe'; fcn = @(z) smoothPart (z, X, y, w, P); [z, info] = __lbfgs__ (fcn, z0, opt); Beta = z(1:p); if (P.FitBias) Bias = z(p+1); endif S = struct (); S.NumIterations = info.Iterations; S.GradientNorm = info.Gradient; S.RelativeChangeInBeta = info.RelativeChangeInBeta; S.DeltaGradient = []; [S.TerminationCode, S.TerminationStatus] = terminationOf (info); S.History = []; endfunction ## SpaRSA, the separable-approximation proximal method MATLAB uses for the ## lasso penalty. Each step minimizes the loss's linear model plus an ## isotropic quadratic of curvature alpha, which is the soft threshold, with ## alpha set by the Barzilai-Borwein ratio and grown until a nonmonotone ## sufficient-decrease test passes. function [Beta, Bias, S] = solveSpaRSA (X, y, w, P, Beta, Bias) p = columns (X); ## Ten past objectives, measured on R2024a's fitclinear: with them its ## step lengths are reproduced exactly for 60 iterations and more, after ## which the non-monotone path amplifies rounding. Five part at 30. memory = 10; eta = 2; sigma = 0.01; alphaMin = 1e-30; alphaMax = 1e30; z = Beta; if (P.FitBias) z = [Beta; Bias]; endif [f, g] = smoothPart (z, X, y, w, P); obj = f + P.Lambda * penalty (z(1:p), P.Regularization); past = obj; alpha = 1; code = 0; relChange = Inf; iter = 0; for iter = 1:P.IterationLimit accepted = false; for ls = 1:60 zNew = z - g / alpha; zNew(1:p) = softThreshold (zNew(1:p), P.Lambda / alpha); d = zNew - z; [fNew, gNew] = smoothPart (zNew, X, y, w, P); objNew = fNew + P.Lambda * penalty (zNew(1:p), P.Regularization); if (objNew <= max (past) - sigma * alpha / 2 * sum (d .^ 2)) accepted = true; break; endif alpha *= eta; endfor ## No step decreasing the objective: R2024a reports -11 here, and counts ## only the iterations it completed. if (! accepted) code = -11; iter -= 1; break; endif ## Relative change in the coefficients, MATLAB's primary stopping test. relChange = relativeChange (zNew, z); ## Barzilai-Borwein curvature for the next step. dg = gNew - g; dd = sum (d .^ 2); if (dd > 0) alpha = (d' * dg) / dd; if (! isfinite (alpha) || alpha < alphaMin) alpha = alphaMin; elseif (alpha > alphaMax) alpha = alphaMax; endif endif z = zNew; f = fNew; g = gNew; obj = objNew; past = [past, obj]; if (numel (past) > memory) past(1) = []; endif ## The coefficient test, then the gradient test, as the quasi-Newton ## solvers order them. A tolerance of zero switches its test off rather ## than being met by a change of exactly zero: with both at zero R2024a ## runs until no step decreases the objective, and with only ## BetaTolerance at zero it stops on the gradient. if (P.BetaTolerance > 0 && relChange <= P.BetaTolerance) code = 1; break; endif if (P.GradientTolerance > 0 && max (abs (g)) <= P.GradientTolerance) code = 2; break; endif endfor Beta = z(1:p); if (P.FitBias) Bias = z(p+1); endif ## The gradient reported for a lasso fit is that of the smooth part alone, ## the penalty having no gradient at the zeros the fit is there to produce. S = struct (); S.NumIterations = iter; S.GradientNorm = max (abs (g)); S.RelativeChangeInBeta = relChange; S.DeltaGradient = []; [S.TerminationCode, S.TerminationStatus] = terminationOfCode (code); S.History = []; endfunction ## Stochastic gradient descent, and its averaged variant. The learning rate ## decays as LearnRate / (1 + Lambda * LearnRate * t) for a ridge penalty and ## stays put for a lasso one, which is soft-thresholded every ## TruncationPeriod mini-batches rather than every one of them. function [Beta, Bias, S] = solveSGD (X, y, w, P, Beta, Bias) [n, p] = size (X); averaged = strcmp (P.Solver, 'asgd'); batch = min (P.BatchSize, n); perPass = ceil (n / batch); limit = P.PassLimit * perPass; if (! isempty (P.BatchLimit)) limit = min (limit, P.BatchLimit); endif rate = P.LearnRate; ridge = strcmp (P.Regularization, 'ridge'); betaSum = zeros (p, 1); biasSum = 0; count = 0; t = 0; relChange = Inf; prev = Beta; if (P.FitBias) prev = [Beta; Bias]; endif lastObj = Inf; for pass = 1:P.PassLimit order = randperm (n); for b = 1:perPass t++; if (t > limit) break; endif idx = order((b - 1) * batch + 1 : min (b * batch, n)); Xb = X(idx,:); yb = y(idx); wb = w(idx); wb = wb / sum (wb); [~, dL] = linearLoss (Xb * Beta + Bias, yb, P.LossFunction, P.Epsilon); v = wb .* dL; gBeta = Xb' * v; if (ridge) gBeta += P.Lambda * Beta; step = rate / (1 + P.Lambda * rate * t); else step = rate; endif Beta -= step * gBeta; if (P.FitBias) Bias -= step * sum (v); endif if (! ridge && mod (t, P.TruncationPeriod) == 0) Beta = softThreshold (Beta, step * P.TruncationPeriod * P.Lambda); endif if (averaged) betaSum += Beta; biasSum += Bias; count++; endif endfor ## What the pass reached, which drives both the stopping test and, when ## OptimizeLearnRate is on, the step size of the next pass: MATLAB ## halves the rate whenever a pass leaves the objective higher than it ## found it. if (P.FitBias) relChange = relativeChange ([Beta; Bias], prev); prev = [Beta; Bias]; obj = linearObjective (X, y, w, P, Beta, Bias); else relChange = relativeChange (Beta, prev); prev = Beta; obj = linearObjective (X, y, w, P, Beta, 0); endif if (P.OptimizeLearnRate && mod (pass, P.NumCheckConvergence) == 0) if (obj > lastObj) rate /= 2; endif lastObj = obj; endif if (t > limit || relChange <= P.BetaTolerance) break; endif endfor if (averaged && count > 0) Beta = betaSum / count; Bias = biasSum / count; endif z = Beta; if (P.FitBias) z = [Beta; Bias]; endif [~, g] = smoothPart (z, X, y, w, P); S = struct (); S.NumIterations = t; S.NumPasses = pass; S.BatchIndex = t; S.OptimalLearnRate = rate; S.GradientNorm = max (abs (g)); S.RelativeChangeInBeta = relChange; S.DeltaGradient = []; [S.TerminationCode, S.TerminationStatus] = terminationOfCode (0); S.History = []; endfunction ## Dual coordinate descent, for a ridge-penalized hinge or ## epsilon-insensitive loss. One coordinate of the dual is minimized ## exactly at a time and the primal coefficients are carried alongside, so a ## pass costs one sweep over the observations. Convergence is judged by the ## largest violation of the coordinate optimality conditions, which is what ## MATLAB reports as DeltaGradient. ## ## The intercept enters as a constant predictor, so this solver penalizes it ## where the quasi-Newton solvers leave it free. That is what a coordinate ## dual can do without the equality constraint an unpenalized intercept ## imposes, it is what LIBLINEAR does, and MATLAB's own dual solver differs ## from its own quasi-Newton one in the same direction and by more: on the ## two overlapping iris species R2024a reaches 0.2760 by 'dual' against ## 0.1576 by 'bfgs'. Expect the two solvers to disagree, and prefer the ## quasi-Newton answer when the objective as written is what matters. function [Beta, Bias, S] = solveDual (X, y, w, P, Beta, Bias) [n, p] = size (X); classification = any (strcmp (P.LossFunction, {'hinge', 'logit'})); U = w / P.Lambda; sq = sum (X .^ 2, 2); ## The intercept enters as a constant predictor, and the value that ## constant takes decides how heavily the dual penalizes it: an intercept ## fitted through a feature of value B carries a penalty of (Bias / B)^2 / 2 ## rather than Bias^2 / 2. LIBLINEAR leaves that value at one and so ## regularizes the intercept as hard as a coefficient, which is visibly ## wrong when the response is far from zero: on carsmall, whose intercept ## is around 24, it costs more than the fit it buys. Setting it to the ## root mean square length of an observation makes the penalty negligible ## and the intercept effectively free, which is the objective as written. ## It is not free of cost: a constant that large enters every coordinate's ## curvature and slows the sweep down, so a fit whose intercept is far from ## zero wants a generous PassLimit. On carsmall the objective falls from ## 5.108 at 500 passes to 2.415 at 8000, the last of those below the 2.827 ## the quasi-Newton solvers stall at. biasScale = 1; if (P.FitBias) biasScale = max (1, sqrt (mean (sum (X .^ 2, 2)))); sq += biasScale ^ 2; endif sq = max (sq, eps); alpha = zeros (n, 1); Beta = zeros (p, 1); Bias = 0; delta = Inf; pass = 0; for pass = 1:P.PassLimit order = randperm (n); delta = 0; for k = order xk = X(k,:); f = xk * Beta + Bias; aOld = alpha(k); if (classification) ## The exact minimizer of the dual along this coordinate, clipped to ## its own box. g = y(k) * f - 1; aNew = min (max (aOld - g / sq(k), 0), U(k)); step = (aNew - aOld) * y(k); else ## The same, with the insensitive band entering as an absolute value ## that the step is soft thresholded by. g = f - y(k); u = aOld - g / sq(k); t = P.Epsilon / sq(k); aNew = sign (u) * max (abs (u) - t, 0); aNew = min (max (aNew, -U(k)), U(k)); step = aNew - aOld; endif ## How far the coordinate was from its own optimum, in the units of ## the gradient. A coordinate pinned against a bound by a gradient ## pushing it outwards does not move and is not in violation. delta = max (delta, abs (aNew - aOld) * sq(k)); if (step != 0) Beta += step * xk'; if (P.FitBias) Bias += step * biasScale; endif alpha(k) = aNew; endif endfor if (mod (pass, P.NumCheckConvergence) == 0 && delta <= P.DeltaGradientTolerance) break; endif endfor S = struct (); S.NumIterations = pass * n; S.NumPasses = pass; S.Alpha = alpha; z = Beta; if (P.FitBias) z = [Beta; Bias]; endif [~, g] = smoothPart (z, X, y, w, P); S.GradientNorm = max (abs (g)); S.RelativeChangeInBeta = []; S.DeltaGradient = delta; if (delta <= P.DeltaGradientTolerance) [S.TerminationCode, S.TerminationStatus] = terminationOfCode (4); else [S.TerminationCode, S.TerminationStatus] = terminationOfCode (0); endif S.History = []; endfunction function b = softThreshold (b, t) b = sign (b) .* max (abs (b) - t, 0); endfunction ## The relative change MATLAB tests BetaTolerance against. Measured on ## R2024a: the two-norm of the step over the two-norm of the iterate, the ## bias inside both vectors and no guard on the denominator. An iterate that ## is identically zero has not moved, so the quotient is 0/0 and NaN, which ## is the value MATLAB reports for this quantity whenever it is not tested. function r = relativeChange (new, old) d = new - old; r = norm (d) / norm (new); endfunction ## Refit the bias alone against the fitted linear part. Least squares and ## the logit have an interior optimum found by a scalar Newton step; the two ## non-smooth losses are minimized over the finite set of points at which ## their derivative can change sign. function Bias = refitBias (f, y, w, P) switch (P.LossFunction) case 'mse' Bias = sum (w .* (y - f)) / sum (w); case 'epsiloninsensitive' knots = unique ([y - f - P.Epsilon; y - f + P.Epsilon]); Bias = bestOf (knots, f, y, w, P); case 'hinge' knots = unique ((1 ./ y) - f); Bias = bestOf (knots, f, y, w, P); case 'logit' Bias = 0; for k = 1:50 [~, dL] = linearLoss (f + Bias, y, P.LossFunction, P.Epsilon); g = sum (w .* dL); s = 1 ./ (1 + exp (-(y .* (f + Bias)))); h = sum (w .* s .* (1 - s)); if (h <= 0) break; endif step = g / h; Bias -= step; if (abs (step) <= 1e-12) break; endif endfor endswitch endfunction function b = bestOf (knots, f, y, w, P) best = Inf; b = 0; for k = 1:numel (knots) L = linearLoss (f + knots(k), y, P.LossFunction, P.Epsilon); v = sum (w .* L); if (v < best) best = v; b = knots(k); endif endfor endfunction ## Map what the engine reports onto MATLAB's TerminationCode. The engine ## hands out a stable token rather than a sentence, because the wording is ## the caller's: fitcnet says 'Relative gradient tolerance reached.' where ## fitclinear says 'Tolerance on gradient satisfied.' for the same event. A ## build that offers no token has nothing to map, and reports the iteration ## limit. ## ## Measured on R2024a fitclinear: the coefficient tolerance is code 1, the ## gradient tolerance 2, the iteration limit 0, and a line search that cannot ## improve the objective -11, worded 'Unable to find a step decreasing the ## objective.' 'loss' and 'step' are unreachable from here: LossTolerance is ## -Inf and StepTolerance 0, and a failed line search returns before the step ## test is ever reached, so a step of exactly zero cannot arise. function [code, status] = terminationOf (info) token = ''; if (isfield (info, 'Criterion')) token = info.Criterion; endif switch (token) case 'beta' code = 1; case {'gradient', 'step'} code = 2; case 'linesearch' code = -11; otherwise code = 0; endswitch [code, status] = terminationOfCode (code); endfunction function [code, status] = terminationOfCode (code) switch (code) case 1 status = {'Tolerance on coefficients satisfied.'}; case 2 status = {'Tolerance on gradient satisfied.'}; case 4 status = {'Tolerance on the complementarity gap satisfied.'}; case -11 status = {'Unable to find a step decreasing the objective.'}; otherwise code = 0; status = {'Iteration limit exceeded.'}; endswitch endfunction statistics-release-2.1.2/inst/Machine_Learning/private/marginsOf.m000066400000000000000000000031221527311104300252340ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{m} =} marginsOf (@var{s}, @var{gY}, @var{L}) ## ## Classification margin of every observation, over one or more score ## matrices. ## ## @var{s} is @math{NxK} when @var{L} is one and @math{NxKxL} otherwise, ## @var{gY} the index into the class names of each observation's true class. ## @var{m} is @math{NxL}: the score of the true class less the best score ## among the others, which for two classes is simply the other one. ## ## @end deftypefn function m = marginsOf (s, gY, L) n = rows (s); m = zeros (n, L); rowidx = (1:n)'; for k = 1:L if (L == 1) sk = s; else sk = s(:,:,k); endif idx = sub2ind (size (sk), rowidx, gY); strue = sk(idx); so = sk; so(idx) = -Inf; m(:,k) = strue - max (so, [], 2); endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/missingLabels.m000066400000000000000000000035201527311104300261050ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{label} =} missingLabels (@var{Y}, @var{n}) ## ## A column of @var{n} missing labels in the type of the response @var{Y}. ## ## A cell array of character vectors gives empty names, a character matrix ## rows of blanks as wide as @var{Y}, a numeric response @code{NaN}, a ## categorical response @qcode{} keeping the categories of @var{Y}, ## and a string response @qcode{}. A logical response has no missing ## value and gives @code{false}. ## ## @end deftypefn function label = missingLabels (Y, n) if (iscellstr (Y)) label = repmat ({''}, n, 1); elseif (islogical (Y)) label = false (n, 1); elseif (isnumeric (Y)) label = nan (n, 1); elseif (ischar (Y)) label = repmat (' ', n, columns (Y)); elseif (isa (Y, 'categorical')) label = categorical (repmat ({''}, n, 1), categories (Y), ... 'Ordinal', isordinal (Y), ... 'Protected', isprotected (Y)); elseif (isa (Y, 'string')) label = string (nan (n, 1)); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/missingResponse.m000066400000000000000000000035471527311104300265120ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{y0} =} missingResponse (@var{Y}, @var{W}) ## ## The response a regression model predicts for a row missing a predictor. ## ## @var{y0} is the weighted lower median of the training response @var{Y} ## under the observation weights @var{W}: the smallest response at which the ## weight of the responses up to it reaches half the total. With equal ## weights and an even count it is the lower of the two middle values. ## Measured on MATLAB R2024a, whose linear, kernel, support vector, Gaussian ## process and neural network regressions all predict it. A missing ## response takes no part. ## ## @end deftypefn function y0 = missingResponse (Y, W) Y = Y(:); W = W(:); keep = ! (isnan (Y) | isnan (W)); Y = Y(keep); W = W(keep); if (isempty (Y) || ! (sum (W) > 0)) y0 = NaN; return; endif [Y, o] = sort (Y); cw = cumsum (W(o)); ## Equal weights reach one half exactly, and the cumulative sum of fifty ## hundredths falls short of it in the last bits. i = find (cw >= cw(end) / 2 * (1 - 1e-12), 1); y0 = Y(i); endfunction statistics-release-2.1.2/inst/Machine_Learning/private/namedClasses.m000066400000000000000000000050011527311104300257070ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{pos}, @var{errmsg}] =} namedClasses (@var{C}, @var{ClassNames}, @var{exempt}) ## Where each class a @qcode{'ClassNames'} option names sits among the classes ## of a response. ## ## @var{C} holds the distinct classes of the response and @var{pos} the index ## into it of each name in @var{ClassNames}, zero for a name it does not hold. ## Text matches text, and numbers and logicals match by value. A text name ## matches a numeric class by the text @code{num2str} writes and a logical ## class by @qcode{'true'} or @qcode{'false'}; a numeric or logical name never ## matches a text class. @var{errmsg} is empty when every name matched, and ## otherwise the body of the message the caller raises under its own name. ## @var{exempt}, a logical over @var{ClassNames} and empty by default, marks ## the classes that may be absent, those removed for a prior of 0; an absent ## class has position 0. ## @end deftypefn function [pos, errmsg] = namedClasses (C, ClassNames, exempt = []) errmsg = ""; textC = ! (isnumeric (C) || islogical (C)); textN = ! (isnumeric (ClassNames) || islogical (ClassNames)); if (textC && ! textN) pos = zeros (numel (ClassNames), 1); elseif (! textC && textN) if (islogical (C)) words = {'false'; 'true'}; T = words(double (C(:)) + 1); else T = arrayfun (@num2str, C(:), 'UniformOutput', false); endif pos = labelIndices (T, ClassNames); else pos = labelIndices (C, ClassNames); endif ## A class removed for its prior of 0 may be absent absent = (pos(:) == 0); if (! isempty (exempt)) absent &= ! exempt(:); endif if (isempty (pos) || any (absent)) errmsg = "not all 'ClassNames' are present in Y."; endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/namesAsLabels.m000066400000000000000000000037041527311104300260270ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{Yx} =} namesAsLabels (@var{Y}, @var{names}) ## ## The response @var{Y} with the class names @var{names} appended as labels in ## its own type. ## ## A class removed for its prior of 0 may be absent from the response while ## the model still names it; appending it lets the classes be ordered and ## counted as if it were present. Text names become numbers for a numeric ## response, as @code{namedClasses} matches them, and @qcode{'true'} or ## @qcode{'false'} for a logical one. ## @end deftypefn function Yx = namesAsLabels (Y, names) textN = ! (isnumeric (names) || islogical (names)); if (textN) names = cellstr (names); endif names = names(:); if (iscellstr (Y)) Yx = [Y(:); names]; elseif (ischar (Y)) Yx = char ([cellstr(Y); names]); elseif (isa (Y, 'string')) Yx = [Y(:); string(names)]; elseif (isa (Y, 'categorical')) Yx = [Y(:); categorical(names)]; elseif (islogical (Y)) if (textN) names = strcmp (names, 'true'); endif Yx = [Y(:); logical(names)]; else if (textN) names = str2double (names); endif Yx = [Y(:); cast(names, class (Y))]; endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/nbCategorical.m000066400000000000000000000046121527311104300260510ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{idx} =} nbCategorical (@var{val}, @var{p}, @var{classname}) ## ## Resolve the @qcode{CategoricalPredictors} argument into column indices. ## ## @var{val} is what the caller was given: empty for none, the character ## vector @qcode{'all'}, a logical vector with one element per predictor, or a ## numeric vector of column indices. @var{p} is the number of predictors and ## @var{classname} prefixes any error raised. ## ## @var{idx} is a sorted row vector of indices, without repeats. ## ## @seealso{ClassificationNaiveBayes, fitcnb} ## @end deftypefn function idx = nbCategorical (val, p, classname) if (isempty (val)) idx = []; return; endif if (ischar (val) && isrow (val)) if (! strcmpi (val, 'all')) error (strcat (classname, ": a character vector", ... " 'CategoricalPredictors' must be 'all'.")); endif idx = 1:p; return; endif if (islogical (val)) if (numel (val) != p) error (strcat (classname, ": a logical 'CategoricalPredictors'", ... " must have one element per predictor.")); endif idx = find (val(:)'); return; endif if (! (isnumeric (val) && isvector (val) && isreal (val))) error (strcat (classname, ": 'CategoricalPredictors' must be 'all', a", ... " logical vector, or a vector of column indices.")); endif idx = sort (unique (val(:)')); if (any (idx != fix (idx)) || any (idx < 1) || any (idx > p)) error (strcat (classname, ": 'CategoricalPredictors' must be column", ... " indices into X.")); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/nbDistNames.m000066400000000000000000000060071527311104300255230ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{D} =} nbDistNames (@var{val}, @var{p}, @var{classname}) ## ## Resolve the @qcode{DistributionNames} argument of a naive Bayes classifier ## into one distribution name per predictor. ## ## @var{val} is what the caller was given, either empty for the default, a ## character vector naming one distribution for every predictor, or a cell ## array of character vectors naming one per predictor. @var{p} is the number ## of predictors and @var{classname} prefixes any error raised. ## ## @var{D} is a cell array with one name per predictor, except for ## @qcode{'mn'}, which is returned as the character vector it was given as. ## The multinomial is a single distribution over the whole predictor vector ## rather than one per predictor, so it cannot be named for some predictors ## and not others, and there is no per-predictor list to return. ## ## @seealso{ClassificationNaiveBayes, fitcnb} ## @end deftypefn function D = nbDistNames (val, p, classname) known = {'kernel', 'mvmn', 'normal'}; if (isempty (val)) D = repmat ({'normal'}, 1, p); return; endif if (ischar (val) && isrow (val)) ## The name goes into a variable first: inside braces a space before a ## call's paren splits it into two elements and the call loses its argument. dname = lower (val); if (strcmp (dname, 'mn')) D = dname; return; endif D = repmat ({dname}, 1, p); elseif (iscellstr (val)) if (any (strcmpi (val, 'mn'))) error (strcat (classname, ": the 'mn' distribution applies to every", ... " predictor at once and cannot be named for some", ... " of them.")); endif if (numel (val) != p) error (strcat (classname, ": 'DistributionNames' must name one", ... " distribution per predictor.")); endif D = cellfun (@lower, val(:)', 'UniformOutput', false); else error (strcat (classname, ": 'DistributionNames' must be a character", ... " vector or a cell array of character vectors.")); endif bad = ! ismember (D, known); if (any (bad)) error (strcat (classname, ": unsupported distribution", ... sprintf (" '%s'.", D{find (bad, 1)}))); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/nbFit.m000066400000000000000000000204161527311104300243560ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{DP}, @var{K}, @var{S}, @var{W}] =} nbFit (@var{X}, @var{gY}, @var{k}, @var{D}, @var{w}, @var{Kernel}, @var{Support}, @var{Width}, @var{classname}) ## ## Fit one univariate density per class and per predictor for a naive Bayes ## classifier. ## ## @var{X} holds the retained observations, @var{gY} indexes the class of each ## and @var{k} counts them, @var{D} names the distribution of each predictor ## and @var{w} weights each observation. @var{CN} and @var{PN} name the ## classes and the predictors, and are used only to say which combination a ## refusal is about. @var{Kernel}, @var{Support} and ## @var{Width} are the kernel arguments as the caller was given them, each ## possibly empty. ## ## @var{DP} is the class-by-predictor cell of fitted parameters: a column ## vector holding the mean and the standard deviation for a @qcode{'normal'} ## predictor, and a @code{prob.KernelDistribution} for a @qcode{'kernel'} one. ## @var{K}, @var{S} and @var{W} report the kernel, the support and the ## bandwidth actually used, empty where a predictor is not a kernel one. ## ## The standard deviation is the weighted, bias-corrected one, which reduces ## to the sample standard deviation when the weights within a class are equal, ## as they are whenever the prior is spread evenly over a class's members. ## ## @seealso{ClassificationNaiveBayes, fitcnb} ## @end deftypefn function [DP, K, S, W, CL] = nbFit (X, gY, k, D, w, Kernel, Support, ... Width, classname, CN, PN) p = columns (X); DP = cell (k, p); K = cell (1, p); S = cell (1, p); CL = cell (1, p); W = []; ## The multinomial is one distribution over the whole predictor vector: a ## row is a vector of token counts, and a class is described by how it ## spends its tokens across the predictors. Each class's counts are pooled ## and smoothed by one, so a token a class never spent is improbable rather ## than impossible, which a product over predictors would otherwise make it. if (ischar (D)) for i = 1:k xk = X(gY == i, :); if (isempty (xk)) continue; endif ## The multinomial is fitted on the rows whose every count is known, as ## R2026b fits it; a row missing a count still counts in the prior wk = w(gY == i); full = ! any (isnan (xk), 2); if (! any (full)) error ("%s: class %s has no observation with every count known.", ... classname, nbLabelName (CN, i)); endif xk = xk(full,:); wk = wk(full); ## Weighted counts, the weights scaled to the class's size so that the ## smoothing weighs as it does without them, as R2024a counts wk = wk(:) / sum (wk) * rows (xk); cnt = sum (wk .* xk, 1); tot = sum (cnt) + p; for j = 1:p DP{i,j} = (cnt(j) + 1) / tot; endfor endfor return; endif anykernel = iscell (D) && any (strcmp (D, 'kernel')); if (anykernel) [Kname, Sname, Wgiven] = nbKernelArgs (Kernel, Support, Width, k, p, ... classname); W = zeros (k, p); else ## A predictor that is not kernel smoothed has no bandwidth, and MATLAB ## reports the absence as a matrix of NaN rather than as an empty one. ## It declares the matrix at its full size whatever the distributions. W = NaN (k, p); endif for j = 1:p for i = 1:k idx = gY == i; xij = X(idx, j); wij = w(idx); ## Only a class removed for its prior of 0 holds no observation, and ## its parameters stay empty, as MATLAB leaves them if (isempty (xij)) continue; endif ## Each predictor is fitted on the observations it is known for, so a ## row missing one predictor still informs the others. A class in ## which a predictor is never known has nothing to fit it on. obs = ! isnan (xij); if (! any (obs)) error ("%s: predictor %s has no known value in class %s.", ... classname, PN{j}, nbLabelName (CN, i)); endif xij = xij(obs); wij = wij(obs); switch (D{j}) case 'mvmn' ## One categorical distribution per class and predictor, over the ## levels the predictor takes across the whole training set. Every ## level's count is raised by one before it is normalized, so a ## level a class never took keeps a small probability instead of ## ruling that class out on this predictor alone. if (isempty (CL{j})) CL{j} = nbLevels (X(:,j)); endif nlev = numel (CL{j}); ## Weighted counts, the weights scaled to the class's size so that ## the smoothing weighs as it does without them, as R2024a counts wk = wij(:) / sum (wij) * numel (xij); cnt = zeros (nlev, 1); for l = 1:nlev cnt(l) = sum (wk(xij == CL{j}(l))); endfor DP{i,j} = (cnt + 1) / (numel (xij) + nlev); case 'normal' sw = sum (wij); mu = sum (wij .* xij) / sw; ## The bias correction of a reliability weight: with equal weights ## the denominator is the count less one. den = sw - sum (wij .^ 2) / sw; sigma = 0; if (den > 0) sigma = sqrt (sum (wij .* (xij - mu) .^ 2) / den); endif ## A predictor that does not vary within a class has no normal ## density to fit: every observation sits on the mean, and the ## limit is a spike carrying no scale. Refuse rather than answer, ## as MATLAB does, naming the combination it could not fit. A ## kernel or a multivariate multinomial has no such difficulty and ## is not refused, which is a route left open to the caller. ## Test the data rather than the computed spread: the weighted ## mean of identical values need not land exactly on them, and a ## variance of 1e-15 where the true one is zero would slip a ## degenerate fit past a test of sigma alone. The second half ## catches values that differ by so little that the squared ## deviation underflows. if (all (xij == xij(1)) || ! (sigma > 0)) error (strcat (classname, ": a normal distribution cannot be", ... " fit for the combination of class", ... sprintf (" %s and predictor %s.", ... nbLabelName (CN, i), PN{j}), ... " The data has zero variance.")); endif DP{i,j} = [mu; sigma]; case 'kernel' ## The density fits itself, so the default bandwidth rule and the ## transform a bounded support needs are ksdensity's own, not a ## second copy of them here. ## Weights shape the density but not its bandwidth, which R2024a ## chooses from the observations alone. if (isempty (Wgiven)) bw = prob.KernelDistribution.fit (xij, Kname{j}, ... Sname{j}).Bandwidth; else bw = Wgiven(i,j); endif freq = []; if (any (wij != wij(1))) freq = wij(:); endif pd = prob.KernelDistribution.fit (xij, Kname{j}, Sname{j}, bw, ... freq); W(i,j) = pd.Bandwidth; DP{i,j} = pd; K{j} = Kname{j}; S{j} = Sname{j}; endswitch endfor endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/nbGivenOr.m000066400000000000000000000024201527311104300252000ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{v} =} nbGivenOr (@var{given}, @var{dflt}) ## ## The argument as it was given, or the default when it was not given at all. ## ## Used to record a naive Bayes model's arguments in @qcode{ModelParameters}, ## which reports what was asked for rather than what it was resolved to. ## ## @seealso{ClassificationNaiveBayes} ## @end deftypefn function v = nbGivenOr (given, dflt) if (isempty (given)) v = dflt; else v = given; endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/nbKernelArgs.m000066400000000000000000000072141527311104300256720ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{K}, @var{S}, @var{W}] =} nbKernelArgs (@var{Kernel}, @var{Support}, @var{Width}, @var{k}, @var{p}, @var{classname}) ## ## Resolve the kernel arguments of a naive Bayes classifier into one kernel and ## one support per predictor, and a bandwidth per class and predictor. ## ## Each of @var{Kernel} and @var{Support} may be given once for every predictor ## or once per predictor. @var{W} is empty when no bandwidth was given, which ## leaves each density to choose its own. ## ## @seealso{ClassificationNaiveBayes, fitcnb} ## @end deftypefn function [K, S, W] = nbKernelArgs (Kernel, Support, Width, k, p, classname) known = {'box', 'epanechnikov', 'normal', 'triangle'}; ## Kernel if (isempty (Kernel)) K = repmat ({'normal'}, 1, p); elseif (ischar (Kernel) && isrow (Kernel)) kname = lower (Kernel); K = repmat ({kname}, 1, p); elseif (iscellstr (Kernel) && numel (Kernel) == p) K = cellfun (@lower, Kernel(:)', 'UniformOutput', false); else error (strcat (classname, ": 'Kernel' must be a character vector or a", ... " cell array of character vectors, one per predictor.")); endif bad = ! ismember (K, known); if (any (bad)) error (strcat (classname, ": unsupported kernel", ... sprintf (" '%s'.", K{find (bad, 1)}))); endif ## Support if (isempty (Support)) S = repmat ({'unbounded'}, 1, p); elseif (ischar (Support) && isrow (Support)) sname = lower (Support); S = repmat ({sname}, 1, p); elseif (isnumeric (Support) && numel (Support) == 2) sbnds = sort (Support(:)'); S = repmat ({sbnds}, 1, p); elseif (iscell (Support) && numel (Support) == p) S = Support(:)'; else error (strcat (classname, ": 'Support' must be 'unbounded',", ... " 'positive', a two element numeric vector, or a cell", ... " array holding one of those per predictor.")); endif for j = 1:p if (ischar (S{j}) && ! any (strcmpi (S{j}, {'positive', 'unbounded'}))) error (strcat (classname, ": a character vector 'Support' must be", ... " 'unbounded' or 'positive'.")); endif endfor ## Width if (isempty (Width)) W = []; return; endif if (! (isnumeric (Width) && isreal (Width) && all (Width(:) > 0))) error (strcat (classname, ": 'Width' must be positive and real.")); endif if (isscalar (Width)) W = repmat (Width, k, p); elseif (isequal (size (Width), [k, p])) W = Width; elseif (isrow (Width) && numel (Width) == p) W = repmat (Width(:)', k, 1); elseif (iscolumn (Width) && numel (Width) == k) W = repmat (Width(:), 1, p); else error (strcat (classname, ": 'Width' must be a scalar, one value per", ... " predictor, one per class, or a matrix of one per", ... " class and predictor.")); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/nbKernelPack.m000066400000000000000000000040311527311104300256460ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{DP} =} nbKernelPack (@var{DP}, @var{D}) ## ## Replace every fitted kernel density in @var{DP} by the sample it was fitted ## to, so that a naive Bayes model can be written to a file. A density fitted ## with weights becomes a two-column matrix, the sample beside its weights. ## ## @var{DP} is a @code{DistributionParameters} cell, class by predictor, and ## @var{D} the matching @code{DistributionNames}. A @qcode{'kernel'} predictor ## holds a @code{prob.KernelDistribution} object, which Octave's @code{save} ## cannot serialize: it warns and writes a struct that will not load back as an ## object. Every other distribution already holds plain numeric data and is ## left alone. ## ## The sample is enough to rebuild the density exactly, because the kernel, the ## support and the bandwidth actually used are stored beside it on the model. ## @code{nbKernelUnpack} does the rebuilding. ## ## @end deftypefn function DP = nbKernelPack (DP, D) for j = find (strcmp (D(:)', 'kernel')) for i = 1:rows (DP) if (isobject (DP{i,j})) ## A weighted density goes out as its sample beside its weights DP{i,j} = [DP{i,j}.InputData.data, DP{i,j}.InputData.freq]; endif endfor endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/nbKernelUnpack.m000066400000000000000000000040361527311104300262160ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{DP} =} nbKernelUnpack (@var{DP}, @var{D}, @var{K}, @var{S}, @var{W}) ## ## Rebuild the fitted kernel densities @code{nbKernelPack} wrote out as samples, ## with their weights where a second column carries them. ## ## @var{DP} is a @code{DistributionParameters} cell carrying a sample wherever ## @var{D} names a @qcode{'kernel'} predictor, and @var{K}, @var{S} and @var{W} ## are the model's @code{Kernel}, @code{Support} and @code{Width}. Each cell is ## refitted through @code{prob.KernelDistribution.fit} with the bandwidth the ## model recorded, so the density comes back identical rather than merely close: ## nothing is re-estimated, since the bandwidth is passed rather than chosen. ## ## A class with no observations of a predictor keeps its empty cell. ## ## @end deftypefn function DP = nbKernelUnpack (DP, D, K, S, W) for j = find (strcmp (D(:)', 'kernel')) for i = 1:rows (DP) if (! isempty (DP{i,j}) && ! isobject (DP{i,j})) freq = []; if (columns (DP{i,j}) == 2) freq = DP{i,j}(:,2); endif DP{i,j} = prob.KernelDistribution.fit (DP{i,j}(:,1), K{j}, S{j}, ... W(i,j), freq); endif endfor endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/nbLabelName.m000066400000000000000000000027741527311104300254630ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{s} =} nbLabelName (@var{C}, @var{i}) ## ## One class's name, as a message should print it. ## ## @var{C} is the model's @qcode{ClassNames} in any of the forms a response ## may take, and @var{i} indexes it. A textual class prints as its name and ## every other kind as its value, which is what MATLAB's own messages do: ## a numeric response names @qcode{class 2} where a cellstr one names ## @qcode{class beta}. ## ## @seealso{ClassificationNaiveBayes} ## @end deftypefn function s = nbLabelName (C, i) if (iscellstr (C)) s = C{i}; elseif (ischar (C)) s = strtrim (C(i,:)); elseif (islogical (C)) s = mat2str (C(i)); else s = num2str (C(i)); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/nbLevels.m000066400000000000000000000023221527311104300250620ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{L} =} nbLevels (@var{x}) ## ## The distinct levels of one categorical predictor, in ascending order. ## ## The order is the one every per-level quantity is reported in, so it is ## fixed here once rather than recovered wherever a level is looked up. ## ## @seealso{ClassificationNaiveBayes} ## @end deftypefn function L = nbLevels (x) L = unique (x(:)); L = L(! isnan (L)); endfunction statistics-release-2.1.2/inst/Machine_Learning/private/nbLogLik.m000066400000000000000000000102671527311104300250200ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{L} =} nbLogLik (@var{X}, @var{D}, @var{DP}, @var{k}) ## ## The class conditional log-likelihood of every observation under a fitted ## naive Bayes model. ## ## @var{L} is an observation-by-class matrix. The predictors are conditionally ## independent given the class, so a row is the sum over the predictors of each ## one's log density, which is where the model's cost in the number of ## predictors stays linear. ## ## The sum is taken in logs rather than as a product of densities: with even a ## moderate number of predictors the product underflows to zero for every ## class at once, and the posterior that follows would be 0/0 rather than a ## well determined ratio. ## ## A row can come back as @math{-Inf} for every class, when a categorical ## predictor takes a level the model never saw. That is a real answer, not a ## failure, and the caller reads it as such: the observation carries no ## information about the class and its posterior is the prior. ## ## A @qcode{NaN} predictor is skipped rather than propagated. The predictors ## are conditionally independent given the class, so an observation missing ## one of them is still described by the others, and only its own term drops ## out. An observation missing every predictor contributes nothing to any ## class and falls back to the prior, by the same arithmetic. Measured ## against R2024a. ## ## @seealso{ClassificationNaiveBayes, fitcnb} ## @end deftypefn function L = nbLogLik (X, D, DP, k, CL) n = rows (X); p = columns (X); L = zeros (n, k); ## The multinomial reads a row as token counts and scores it by how likely ## the class was to spend its tokens that way. The multinomial coefficient ## depends on the row alone, not on the class, so it cancels in the ## posterior and is not computed. if (ischar (D)) for i = 1:k ## A class removed for its prior of 0 has no parameters and no ## likelihood if (isempty (DP{i,1})) L(:,i) = -Inf; continue; endif for j = 1:p t = X(:,j) * log (DP{i,j}); t(isnan (X(:,j))) = 0; L(:,i) += t; endfor endfor return; endif for i = 1:k if (isempty (DP{i,1})) L(:,i) = -Inf; continue; endif for j = 1:p switch (D{j}) case 'mvmn' ## An observation is scored by the probability of the level it ## takes. A level the model never saw carries none, and makes the ## observation impossible under every class alike. lev = CL{j}; pr = DP{i,j}; lp = -Inf (n, 1); for l = 1:numel (lev) lp(X(:,j) == lev(l)) = log (pr(l)); endfor lp(isnan (X(:,j))) = 0; L(:,i) += lp; case 'normal' mu = DP{i,j}(1); sigma = DP{i,j}(2); if (sigma > 0) z = (X(:,j) - mu) / sigma; t = -0.5 * z .^ 2 - log (sigma) - 0.5 * log (2 * pi); t(isnan (X(:,j))) = 0; L(:,i) += t; else ## A class holding one distinct value has no spread: the density ## is a spike, so an observation matching it is certain and every ## other one impossible. L(X(:,j) != mu, i) = -Inf; endif case 'kernel' t = log (pdf (DP{i,j}, X(:,j))); t(isnan (X(:,j))) = 0; L(:,i) += t; endswitch endfor endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/pairTerms.m000066400000000000000000000024161527311104300252620ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{T} =} pairTerms (@var{p}) ## Every pairwise interaction of @var{p} predictors, as a logical matrix with ## one row per term and one column per predictor, two of them true. ## @end deftypefn function T = pairTerms (p) if (p < 2) T = false (0, p); return; endif pairs = nchoosek (1:p, 2); n = rows (pairs); T = false (n, p); T(sub2ind ([n, p], (1:n)', pairs(:,1))) = true; T(sub2ind ([n, p], (1:n)', pairs(:,2))) = true; endfunction statistics-release-2.1.2/inst/Machine_Learning/private/parseDistanceWeight.m000066400000000000000000000044211527311104300272470ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{f}, @var{dw}] =} parseDistanceWeight (@var{DistanceWeight}, @var{classname}) ## ## Parse a nearest-neighbour distance weight. ## ## @var{dw} is the weight as it is reported, a character vector naming a ## built-in weight or the @code{func2str} form of a supplied handle, and ## @var{f} is the callable that applies it. The property holds the text, as ## MATLAB does, and the callable is kept out of sight beside it. ## ## @end deftypefn function [f, dw] = parseDistanceWeight (DistanceWeight, classname) if (! (ischar (DistanceWeight) || is_function_handle (DistanceWeight))) error (strcat ("%s: 'DistanceWeight' must be a character vector or a", ... " function handle."), classname); endif if (is_function_handle (DistanceWeight)) m = eye (5); if (! isequal (size (m), size (DistanceWeight (m)))) error (strcat ("%s: function handle for 'DistanceWeight' must return", ... " the same size as its input."), classname); endif f = DistanceWeight; dw = func2str (DistanceWeight); return; endif switch (lower (DistanceWeight)) case 'equal' f = @(d) ones (size (d)); case 'inverse' f = @(d) d .^ (-1); case 'squaredinverse' f = @(d) d .^ (-2); otherwise error (strcat ("%s: 'DistanceWeight' must be 'equal', 'inverse',", ... " 'squaredinverse', or a function handle."), classname); endswitch dw = lower (DistanceWeight); endfunction statistics-release-2.1.2/inst/Machine_Learning/private/parseResponseTransform.m000066400000000000000000000040131527311104300300340ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} parseResponseTransform (@var{rt}, @var{classname}) ## Parse ResponseTransform for Regression objects. ## @end deftypefn function [f, rt] = parseResponseTransform (ResponseTransform, classname) if (is_function_handle (ResponseTransform)) ## nargin () raises on a handle to a built-in, so the handle is checked by ## what it does rather than by its declared arity. v = (1:5)'; if (! isequal (size (v), size (ResponseTransform (v)))) error (strcat ("%s: function handle for 'ResponseTransform' must", ... " return the same size as its input."), classname); endif f = ResponseTransform; rt = func2str (ResponseTransform); elseif (ischar (ResponseTransform) && isrow (ResponseTransform)) rt = tolower (ResponseTransform); switch (rt) case {'none', 'identity'} f = @(y) y; rt = 'none'; case 'exp' f = @(y) exp (y); case 'log' f = @(y) log (y); otherwise error ("%s: unrecognized 'ResponseTransform' function.", classname); endswitch else error (strcat ("%s: 'ResponseTransform' must be a character vector", ... " or a function handle."), classname); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/parseScoreTransform.m000066400000000000000000000123361527311104300273200ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} parseScoreTransform (@var{pd}, @var{classname}) ## ## Parse ScoreTransform for Classification objects. ## ## @end deftypefn function [f, st] = parseScoreTransform (ScoreTransform, classname) stList = {'doublelogit', 'invlogit', 'ismax', 'logit', 'none', ... 'identity', 'sign', 'symmetric', 'symmetricismax', ... 'symmetriclogit'}; if (! (ischar (ScoreTransform) || strcmp (class (ScoreTransform), 'function_handle'))) error (strcat ("%s: 'ScoreTransform' must be a character", ... " vector or a function handle."), classname); endif ## Handle ScoreTransform here if (is_function_handle (ScoreTransform)) m = eye (5); if (! isequal (size (m), size (ScoreTransform(m)))) error (strcat ("%s: function handle for 'ScoreTransform' must", ... " return the same size as its input."), classname); endif f = ScoreTransform; st = func2str (ScoreTransform); else ## The transformations fitPosterior and fitSVMPosterior estimate, written ## as MATLAB writes them, with the parameters in full precision tok = regexp (ScoreTransform, ... '^@\(S\)(sigmoid|step|constant)\(S,([^()]*)\)$', ... 'tokens', 'once'); if (! isempty (tok)) v = str2double (strsplit (tok{2}, ',')); nv = struct ('sigmoid', 2, 'step', 3, 'constant', 1).(tok{1}); if (numel (v) != nv || ! all (isfinite (v)) || (nv == 1 && ! any (v == [1, 2]))) error ("%s: unrecognized 'ScoreTransform' function.", classname); endif st = ScoreTransform; switch (tok{1}) case 'sigmoid' f = @(x) sigmoidPosterior (x, v(1), v(2)); case 'step' f = @(x) stepPosterior (x, v(1), v(2), v(3)); case 'constant' f = @(x) constantPosterior (x, v(1)); endswitch return; endif if (! ismember (ScoreTransform, stList)) error ("%s: unrecognized 'ScoreTransform' function.", classname); endif st = ScoreTransform; if (strcmpi ('doublelogit', ScoreTransform)) f = @(x) 1 ./ (1 + exp (-2 * x)); elseif (strcmpi ('invlogit', ScoreTransform)) f = @(x) log (x ./ (1 - x)); elseif (strcmpi ('ismax', ScoreTransform)) f = eval (sprintf ("@(x) ismax (x)")); elseif (strcmpi ('logit', ScoreTransform)) f = @(x) 1 ./ (1 + exp (-x)); elseif (any (strcmpi ({'identity', 'none'}, ScoreTransform))) ## A handle, like every other transform, so a caller can apply it ## without testing for this one by name f = @(x) x; st = 'none'; elseif (strcmpi ('sign', ScoreTransform)) f = @(x) sign (x); elseif (strcmpi ('symmetric', ScoreTransform)) f = @(x) 2 * x - 1; elseif (strcmpi ('symmetricismax', ScoreTransform)) f = eval (sprintf ("@(x) symmetricismax (x)")); elseif (strcmpi ('symmetriclogit', ScoreTransform)) f = @(x) 2 ./ (1 + exp (-x)) - 1; endif endif endfunction ## Helper functions for ScoreTransform ## The largest score *of each observation*, so the maximum is taken across ## the classes and not down the column. A bare max () ran over the ## observations instead, which is right only for a single row and silently ## wrong for every other. A tie goes to the first class, which is what max ## returns and what MATLAB does. function out = ismax (score) out = zeros (size (score), class (score)); [~, k] = max (score, [], 2); out(sub2ind (size (score), (1:rows (score))', k)) = 1; endfunction function out = symmetricismax (score) out = - ones (size (score), class (score)); [~, k] = max (score, [], 2); out(sub2ind (size (score), (1:rows (score))', k)) = 1; endfunction ## Posterior of the second class from its score in the second column of a ## two-class SCORE, P = 1 / (1 + exp (A * s + B)), returned with its ## complement for the first class. function out = sigmoidPosterior (score, A, B) P = 1 ./ (1 + exp (A * score(:,2) + B)); out = [1 - P, P]; endfunction ## 0 below LB, PI between LB and UB, and 1 above UB, for the second class. function out = stepPosterior (score, LB, UB, PI) s = score(:,2); P = PI * ones (size (s), class (score)); P(s < LB) = 0; P(s > UB) = 1; P(isnan (s)) = NaN; out = [1 - P, P]; endfunction ## Posterior 1 for class K and 0 for the other. function out = constantPosterior (score, K) out = zeros (rows (score), 2, class (score)); out(:,K) = 1; out(isnan (score(:,2)),:) = NaN; endfunction statistics-release-2.1.2/inst/Machine_Learning/private/partitionedArgs.m000066400000000000000000000047531527311104300264610ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{P}, @var{rest}] =} partitionedArgs (@var{args}, @var{classname}) ## ## Split the options a cross-validated model owns from the ones its folds ## are fitted with. ## ## @var{P} is a structure with fields @qcode{ClassNames}, @qcode{Prior}, ## @qcode{Cost}, @qcode{Weights}, @qcode{ScoreTransform} and ## @qcode{ResponseTransform}, each empty when it was not given. @var{rest} ## is everything else, in the order it was given, including the option that ## says how to partition. ## ## Those six belong to the parent because they describe the data or the ## reported scores rather than the fit: the classes, the prior and the cost ## are resolved once over the whole data and handed down, the weights are ## sliced per fold, and a transform is applied once to the assembled scores ## with the folds left carrying none. ## ## @end deftypefn function [P, rest] = partitionedArgs (args, classname) P = struct ('ClassNames', [], 'Prior', [], 'Cost', [], 'Weights', [], ... 'ScoreTransform', [], 'ResponseTransform', []); rest = {}; while (numel (args) > 0) if (numel (args) < 2) error (strcat ("%s: optional arguments must be given in Name-Value", ... " pairs."), classname); endif switch (lower (args{1})) case 'classnames' P.ClassNames = args{2}; case 'prior' P.Prior = args{2}; case 'cost' P.Cost = args{2}; case 'weights' P.Weights = args{2}; case 'scoretransform' P.ScoreTransform = args{2}; case 'responsetransform' P.ResponseTransform = args{2}; otherwise rest = [rest, args(1:2)]; endswitch args(1:2) = []; endwhile endfunction statistics-release-2.1.2/inst/Machine_Learning/private/partitionedModelParams.m000066400000000000000000000044241527311104300277640ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{MP} =} partitionedModelParams (@var{Mdl}, @var{kfold}, @var{method}, @var{type}) ## @deftypefnx {Private Function} {@var{MP} =} partitionedModelParams (@dots{}, @var{learner}) ## ## The @qcode{ModelParameters} of a cross-validated model. ## ## @var{Mdl} is the learner the folds were fitted from, @var{kfold} the number ## of folds, @var{method} the name the partitioned class reports and ## @var{type} either @qcode{'classification'} or @qcode{'regression'}. ## @var{learner} names the backing where the class publishes one. ## ## The learner's own parameters are carried through, so that a cross-validated ## model still says what its folds were fitted with. Its @qcode{Version}, ## @qcode{Method} and @qcode{Type} tags describe the learner rather than the ## partition and are reissued for the class doing the reporting, else a ## cross-validated SVM would answer @qcode{'SVM'} where it is a partitioned ## model. ## ## @end deftypefn function MP = partitionedModelParams (Mdl, kfold, method, type, learner) MP = Mdl.ModelParameters; if (! isstruct (MP)) MP = struct (); endif ## The tags belong to the reporting class, not to the learner. tags = {'Version', 'Method', 'Type'}; for i = 1:numel (tags) if (isfield (MP, tags{i})) MP = rmfield (MP, tags{i}); endif endfor if (nargin > 4 && ! isempty (learner)) MP.LearnerTemplates = learner; endif MP.NLearn = kfold; MP.Version = 1; MP.Method = method; MP.Type = type; endfunction statistics-release-2.1.2/inst/Machine_Learning/private/pdCompute.m000066400000000000000000000150631527311104300252560ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{pd}, @var{x}, @var{y}] =} pdCompute (@var{fname}, @var{Mdl}, @var{Vars}, @dots{}) ## ## Compute partial dependence, for whichever entry point was called. ## ## @code{partialDependence} and the method of the same name on ## @code{PredictiveModel} both come here, so that the two cannot drift apart. ## @var{fname} names the caller in every error message, and the rest is what ## that caller was given. @code{plotPartialDependence} comes here too, for ## the values it draws. ## ## @end deftypefn function [pd, x, y, opts] = pdCompute (fname, Mdl, Vars, varargin) if (! (isa (Mdl, 'PredictiveModel') || is_function_handle (Mdl))) error (strcat ("%s: MDL must be a fitted model that", ... " predicts, or a function handle."), fname); endif ## A classifier is told which classes to answer for, and the class names ## come before the data, as MATLAB orders them isclass = (! is_function_handle (Mdl) && any (strcmp (properties (Mdl), 'ClassNames')) && ! isempty (Mdl.ClassNames)); args = varargin; Labels = []; if (isclass) if (isempty (args)) error (strcat ("%s: LABELS is required for a", ... " classification model."), fname); endif Labels = args{1}; args(1) = []; endif Data = []; if (! isempty (args) && isnumeric (args{1})) Data = args{1}; args(1) = []; elseif (! isempty (args) && ! (ischar (args{1}) || isa (args{1}, 'string'))) ## Neither the data nor the name of an option: for a regression model the ## likeliest mistake is classes it has none of, and for a classifier, ## whose classes were taken above, it is the data if (isclass) error ("%s: DATA must be a real numeric matrix.", fname); else error (strcat ("%s: LABELS applies only to a classification", ... " model."), fname); endif endif optNames = {'QueryPoints', 'NumObservationsToSample', ... 'CategoricalPredictors', 'IncludeInteractions', ... 'IncludeIntercept', 'OutputColumns', 'UseParallel', ... 'PredictionForMissingValue', 'Conditional', 'Parent'}; dfValues = {[], [], [], [], true, 'all', [], [], [], []}; [QP, NumObs, CatPred, Inter, Icept, OutCols, Par, Miss, Cond, Ax, rem] = ... parsePairedArguments (optNames, dfValues, args); if (! isempty (rem)) error (strcat ("%s: unknown optional argument or", ... " misplaced value."), fname); endif if (! isempty (Par)) error ("%s: 'UseParallel' is not implemented.", fname); endif if (! isempty (Miss)) error (strcat ("%s: 'PredictionForMissingValue' is not", ... " implemented."), fname); endif ## Both entry points come here, and the method wins the dispatch whenever ## the first argument is a model, so this is the only place that sees them if (! strcmp (fname, 'plotPartialDependence') && (! isempty (Cond) || ! isempty (Ax))) error (strcat ("%s: 'Conditional' and 'Parent' belong to", ... " plotPartialDependence."), fname); endif [predArgs, subIcept, OutCols, errmsg] = pdOptions (Mdl, Inter, Icept, ... OutCols, CatPred); if (! isempty (errmsg)) error ("%s: %s", fname, errmsg); endif [F, errmsg] = pdFrame (Mdl, Vars, Labels, Data, QP, NumObs, CatPred, ... OutCols); if (! isempty (errmsg)) error ("%s: %s", fname, errmsg); endif pd = pdValues (Mdl, F, predArgs, subIcept); x = F.QP{1}; if (numel (F.Vars) == 2) y = F.QP{2}; else y = []; endif ## What the drawing needs and the values do not: the frame it averaged over, ## the pairs predict was given, and the two options only the plot takes opts.Conditional = Cond; opts.Parent = Ax; opts.PredArgs = predArgs; opts.Frame = F; endfunction ## The options that belong to a kind of model rather than to every one. function [predArgs, subIcept, OutCols, errmsg] = pdOptions (Mdl, Inter, ... Icept, OutCols, ... CatPred) predArgs = {}; subIcept = false; errmsg = ''; isfh = is_function_handle (Mdl); if (isfh) props = {}; else props = properties (Mdl); endif isgam = any (strcmp (props, 'Interactions')); if (! isempty (Inter)) if (! isgam) errmsg = strcat ("'IncludeInteractions' applies only to a", ... " generalized additive model."); return; endif if (! (islogical (Inter) && isscalar (Inter))) errmsg = "'IncludeInteractions' must be true or false."; return; endif predArgs = {'IncludeInteractions', Inter}; endif if (! (islogical (Icept) && isscalar (Icept))) errmsg = "'IncludeIntercept' must be true or false."; return; endif if (! Icept) if (! isgam) errmsg = strcat ("'IncludeIntercept' applies only to a generalized", ... " additive model."); return; endif subIcept = true; endif if (! isfh && ! isempty (CatPred)) errmsg = strcat ("'CategoricalPredictors' applies only to a function", ... " handle, a model carrying its own."); return; endif if (ischar (OutCols) && strcmpi (OutCols, 'all')) OutCols = []; elseif (isnumeric (OutCols) && isreal (OutCols) && isvector (OutCols) && all (OutCols == fix (OutCols)) && all (OutCols >= 1)) OutCols = double (OutCols(:)'); else errmsg = strcat ("'OutputColumns' must be a vector of positive", ... " integers or 'all'."); return; endif if (! isfh && ! isempty (OutCols)) errmsg = "'OutputColumns' applies only to a function handle."; endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/pdFrame.m000066400000000000000000000275771527311104300247110ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{F}, @var{errmsg}] =} pdFrame (@var{Mdl}, @var{Vars}, @var{Labels}, @var{Data}, @var{QP}, @var{NumObs}, @var{CatPred}) ## ## Resolve everything partial dependence is computed over. ## ## This is the one place the arguments of @code{partialDependence} and ## @code{plotPartialDependence} are read, so that the two cannot drift apart ## and so that a later change to what a model accepts as @var{Data} is made ## here and nowhere else. ## ## @var{Mdl} is a fitted model or a function handle. @var{Vars} names one or ## two predictors, by index or by name. @var{Labels} holds the classes to ## answer for and is empty for a regression model. @var{Data} is the data to ## average over, empty to take the model's own. @var{QP} holds the query ## points, empty to take the default. @var{NumObs} is how many observations ## to sample, empty for all. @var{CatPred} names the categorical predictors ## of a function handle, which has no model to carry them. ## ## @var{F} is a structure with the fields @qcode{IsClass}, whether scores are ## averaged rather than responses; @qcode{PredictorNames} and ## @qcode{NumPredictors}; @qcode{Cat}, the indices of the categorical ## predictors; @qcode{X}, the observations averaged over, already sampled; ## @qcode{Vars}, the one or two resolved indices; @qcode{QP}, a cell holding ## the query points of each; @qcode{ClassNames} and @qcode{LabelIdx}, the ## classes answered for and where they sit among the model's own. ## ## @var{errmsg} is the body of the message the caller should raise, empty when ## there is nothing wrong; the caller emits it under its own name. ## ## @end deftypefn function [F, errmsg] = pdFrame (Mdl, Vars, Labels, Data, QP, NumObs, ... CatPred, OutCols) F = []; errmsg = ''; if (nargin < 8) OutCols = []; endif isfh = is_function_handle (Mdl); ## What the model can tell us about itself if (isfh) props = {}; else props = properties (Mdl); endif has = @(n) any (strcmp (props, n)); ## A generalized additive model answers over the observations it was fitted ## on, so a compact one cannot answer at all, and DATA does not help: ## R2024a passes over DATA for such a model, measured 2026-09-17 if (! isfh && any (strcmp (props, 'Interactions')) && ! (any (strcmp (props, 'X')) && ! isempty (Mdl.X))) errmsg = strcat ("partial dependence of a generalized additive model", ... " is taken over the observations it was fitted on,", ... " which a compact one does not keep; use the model it", ... " was compacted from."); return; endif ## The data averaged over. A model that kept its training data supplies it, ## a compact one cannot and must be given it. if (isempty (Data)) if (isfh) errmsg = "DATA is required when the model is a function handle."; return; endif [Data, errmsg] = pdModelData (Mdl, props); if (! isempty (errmsg)) return; endif endif if (! (isnumeric (Data) && isreal (Data) && ismatrix (Data) && ndims (Data) == 2 && ! isempty (Data))) errmsg = "DATA must be a real numeric matrix."; return; endif p = columns (Data); ## The predictor names, from the model where it has them if (has ('PredictorNames') && ! isempty (Mdl.PredictorNames)) pnames = Mdl.PredictorNames(:)'; if (numel (pnames) != p) errmsg = strcat ("DATA must have one column per predictor of the", ... " model."); return; endif else pnames = arrayfun (@(k) sprintf ('x%d', k), 1:p, 'UniformOutput', false); endif ## The categorical predictors: the model's own, or those named for a handle if (isfh) spec = CatPred; elseif (has ('CategoricalPredictors')) spec = Mdl.CategoricalPredictors; else spec = []; endif cat = []; if (! isempty (spec)) [C, msg] = dummyCoding (Data, spec, pnames); if (! isempty (msg)) errmsg = msg; return; endif cat = C.Index; endif ## VARS, by index or by name, one predictor or two [vidx, errmsg] = pdVars (Vars, pnames, p); if (! isempty (errmsg)) return; endif ## A classifier answers with scores, and needs to be told for which classes isclass = has ('ClassNames') && ! isempty (Mdl.ClassNames); lidx = []; cn = []; if (isclass) cn = Mdl.ClassNames; if (isempty (Labels)) errmsg = "LABELS is required for a classification model."; return; endif ## namedClasses reports against a 'ClassNames' option, which is not what ## is being named here, so the message is this function's own [lidx, msg] = namedClasses (cn, Labels); if (! isempty (msg) || isempty (lidx) || any (lidx == 0)) errmsg = "LABELS names a class the model was not fitted with."; return; endif elseif (! isempty (Labels)) errmsg = "LABELS applies only to a classification model."; return; endif ## Sample the observations before the query points are taken from them, as ## the default query points span what was sampled and not what was given if (! isempty (NumObs)) if (! (isnumeric (NumObs) && isscalar (NumObs) && isreal (NumObs) && NumObs >= 1 && NumObs == fix (NumObs))) errmsg = "'NumObservationsToSample' must be a positive integer."; return; endif n = rows (Data); if (NumObs < n) Data = Data(randperm (n, double (NumObs)), :); endif endif ## The query points of each variable [qp, errmsg] = pdQueryPoints (QP, Data, vidx, cat); if (! isempty (errmsg)) return; endif ## Assigned field by field: struct () spreads a cell argument into a struct ## array, and a cellstr ClassNames would make one F.IsClass = isclass; F.PredictorNames = pnames; F.NumPredictors = p; F.Cat = cat; F.X = Data; F.Vars = vidx; F.QP = qp; F.ClassNames = cn; F.LabelIdx = lidx; F.OutCols = OutCols; endfunction ## The observations a model kept, which the learners hold as a matrix and the ## regression models fitted from a formula hold as a table of every variable. function [Data, errmsg] = pdModelData (Mdl, props) Data = []; errmsg = ''; has = @(n) any (strcmp (props, n)); if (has ('X') && ! isempty (Mdl.X)) Data = Mdl.X; return; endif if (has ('Variables') && has ('PredictorNames') && istable (Mdl.Variables)) pn = Mdl.PredictorNames(:)'; V = Mdl.Variables; cols = cell (1, numel (pn)); for k = 1:numel (pn) c = V.(pn{k}); if (! ((isnumeric (c) || islogical (c)) && isreal (c) && columns (c) == 1)) errmsg = strcat ("DATA is required where the predictors the model", ... " kept are not all numeric."); return; endif cols{k} = double (c); endfor Data = [cols{:}]; return; endif errmsg = strcat ("DATA is required for a model that does not keep the", ... " observations it was fitted on."); endfunction ## The query points a predictor takes from the observations themselves. function [q, errmsg] = pdDefaultPoints (Data, v, iscat) q = []; errmsg = ''; col = Data(:,v); col = col(! isnan (col)); if (isempty (col)) errmsg = "DATA holds no value of a variable to vary."; return; endif if (iscat) q = unique (col)(:); elseif (min (col) == max (col)) q = min (col); else q = linspace (min (col), max (col), 100)(:); endif endfunction ## VARS names one predictor or two, by index or by name. function [vidx, errmsg] = pdVars (Vars, pnames, p) vidx = []; errmsg = ''; if (isempty (Vars)) errmsg = "VARS must name one or two predictors."; return; endif if (isnumeric (Vars) && isreal (Vars) && isvector (Vars)) vidx = double (Vars(:)'); if (! (all (vidx == fix (vidx)) && all (vidx >= 1) && all (vidx <= p))) errmsg = "VARS must index the predictors of the model."; vidx = []; return; endif elseif (ischar (Vars) || iscellstr (Vars) || isa (Vars, 'string')) if (ischar (Vars)) wanted = cellstr (Vars)'; elseif (iscellstr (Vars)) wanted = Vars(:)'; else wanted = cellstr (Vars(:)'); endif vidx = zeros (1, numel (wanted)); for k = 1:numel (wanted) j = find (strcmp (pnames, wanted{k})); if (isempty (j)) errmsg = sprintf ("VARS does not name a predictor: '%s'", wanted{k}); vidx = []; return; endif vidx(k) = j(1); endfor else errmsg = strcat ("VARS must be one or two predictor indices or", ... " predictor names."); return; endif if (numel (vidx) < 1 || numel (vidx) > 2) errmsg = "VARS must name one or two predictors."; vidx = []; elseif (numel (vidx) == 2 && vidx(1) == vidx(2)) errmsg = "VARS must name two different predictors."; vidx = []; endif endfunction ## The query points of each variable, given or taken from the observations. function [qp, errmsg] = pdQueryPoints (QP, Data, vidx, cat) qp = {}; errmsg = ''; nv = numel (vidx); ## The levels of a categorical predictor are its query points, and stay so ## whatever was asked for: R2024a takes all of them and passes over what it ## was given, measured 2026-09-17 isc = false (1, nv); for k = 1:nv isc(k) = any (cat == vidx(k)); endfor if (isempty (QP) || all (isc)) qp = cell (1, nv); for k = 1:nv [qp{k}, errmsg] = pdDefaultPoints (Data, vidx(k), isc(k)); if (! isempty (errmsg)) qp = {}; return; endif endfor return; endif ## Given: a vector for one variable, and for two either a two-column matrix ## or a cell holding a vector each, which is how they may differ in length if (iscell (QP)) if (numel (QP) != nv) errmsg = strcat ("'QueryPoints' given as a cell must hold one vector", ... " per variable."); return; endif qp = cell (1, nv); for k = 1:nv if (! (isnumeric (QP{k}) && isreal (QP{k}) && isvector (QP{k}) && ! isempty (QP{k}))) errmsg = "each 'QueryPoints' vector must be a real numeric vector."; qp = {}; return; endif if (isc(k)) [qp{k}, errmsg] = pdDefaultPoints (Data, vidx(k), true); if (! isempty (errmsg)) qp = {}; return; endif else qp{k} = double (QP{k}(:)); endif endfor return; endif if (! (isnumeric (QP) && isreal (QP) && ! isempty (QP))) errmsg = "'QueryPoints' must be a real numeric vector or matrix."; return; endif if (nv == 1) if (! isvector (QP)) errmsg = "'QueryPoints' must be a vector for a single variable."; return; endif v = double (QP(:)); qp = {v}; else if (columns (QP) != 2) errmsg = strcat ("'QueryPoints' must have one column per variable,", ... " or be a cell holding a vector for each."); return; endif v1 = double (QP(:,1)); v2 = double (QP(:,2)); qp = {v1, v2}; for k = 1:2 if (isc(k)) [qp{k}, errmsg] = pdDefaultPoints (Data, vidx(k), true); if (! isempty (errmsg)) qp = {}; return; endif endif endfor endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/pdIce.m000066400000000000000000000047401527311104300243420ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{ice} =} pdIce (@var{Mdl}, @var{F}, @var{predArgs}) ## ## What a model answers for each observation on its own, one query point of the ## varied predictor at a time. ## ## These are the individual conditional expectation curves, one per row of the ## observations in @var{F}, and they are what ## @code{plotPartialDependence} draws in grey. They come from @code{predict} ## for every model, a decision tree included: a curve belongs to one ## observation, so there is no distribution to walk. ## ## The line drawn over them is their mean, which for a tree, a bagged ensemble ## or a generalized additive model is **not** what @code{partialDependence} ## returns unless the observations are the ones the model was fitted on. ## MATLAB R2024a draws the mean of the curves here and the walked value where ## @qcode{'Conditional'} is @qcode{'none'}, measured 2026-09-17, and this ## follows it. ## ## @var{ice} is @math{nxnumX} for a regression model and ## @math{nxnumXxnum} for a classifier, the last dimension holding a page per ## class named. Only one predictor may be varied, which is what MATLAB ## requires of a conditional plot. ## ## @end deftypefn function ice = pdIce (Mdl, F, predArgs) if (nargin < 3) predArgs = {}; endif qx = F.QP{1}; nx = numel (qx); v1 = F.Vars(1); n = rows (F.X); if (F.IsClass) m = numel (F.LabelIdx); else m = 1; endif ice = zeros (n, nx, m); Z = F.X; for ii = 1:nx Z(:,v1) = qx(ii); if (F.IsClass) [~, s] = predict (Mdl, Z, predArgs{:}); ice(:,ii,:) = reshape (s(:, F.LabelIdx), n, 1, m); else s = predict (Mdl, Z, predArgs{:}); ice(:,ii) = s(:); endif endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/pdTraverse.m000066400000000000000000000067011527311104300254340ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{v} =} pdTraverse (@var{T}, @var{vars}, @var{q}, @var{leaf}) ## ## What a decision tree answers on average, with some predictors held fixed. ## ## This is Friedman's weighted traversal, and it is what partial dependence ## means for a tree: the average of the tree's answer over the distribution it ## was fitted on, with the predictors named by @var{vars} held at @var{q}. It ## is not an average over any set of observations and takes none, which is why ## it answers for a compact model, and why MATLAB R2024a gives the same value ## for a tree whatever @qcode{Data} it is handed. ## ## A node that cuts one of the held predictors is followed to the child that ## value reaches. A node that cuts any other is followed **both** ways, each ## child taking the share of the training weight that reached it. A held ## value that matches no level of a categorical cut stops there, as it stops ## an observation in @code{predict}. ## ## @var{T} is the tree, @var{vars} the held predictors and @var{q} their ## values in the same order. @var{leaf} holds what the tree answers at each ## node, one row per node: a column of means for a regression tree and a ## column per class for a classifier. ## ## @var{v} is a row, one element per column of @var{leaf}. ## ## @end deftypefn function v = pdTraverse (T, vars, q, leaf) v = zeros (1, columns (leaf)); node = 1; w = 1; while (! isempty (node)) n = node(1); wn = w(1); node(1) = []; w(1) = []; j = T.CutPredictorIndex(n); if (j == 0) v += wn * leaf(n,:); continue; endif L = T.Children(n,1); R = T.Children(n,2); k = find (vars == j, 1); if (isempty (k)) ## Not held: both ways, in the proportion the training weight took pn = T.NodeProbability(n); if (pn > 0) pL = T.NodeProbability(L) / pn; else pL = 0.5; endif node(end+1:end+2) = [L, R]; w(end+1:end+2) = [wn * pL, wn * (1 - pL)]; else [child, stop] = pdChild (T, n, q(k), L, R); if (stop) v += wn * leaf(n,:); else node(end+1) = child; w(end+1) = wn; endif endif endwhile endfunction ## Which child a held value reaches, and whether it reaches neither. function [child, stop] = pdChild (T, n, val, L, R) stop = false; child = 0; if (! isempty (T.CutCategories) && ! isempty (T.CutCategories{n,1})) if (any (T.CutCategories{n,1} == val)) child = L; elseif (any (T.CutCategories{n,2} == val)) child = R; else stop = true; endif elseif (isnan (val)) stop = true; elseif (val < T.CutPoint(n)) child = L; else child = R; endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/pdTrees.m000066400000000000000000000077651527311104300247360ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{trees}, @var{leaf}, @var{w}] =} pdTrees (@var{Mdl}) ## ## The trees a model answers through, where partial dependence is taken over ## them rather than over a set of observations. ## ## MATLAB R2024a takes partial dependence over the fitted distribution, and ## not over the data it is handed, for a decision tree and for an ensemble of ## bagged trees. Measured 2026-09-17: the value does not move when the data ## is replaced, and differs from the average over it. Every other model ## averages over the data, a boosted ensemble and @code{TreeBagger} included, ## so this returns empty for them and the caller averages instead. ## ## @var{trees} is a cell array of the trees, @var{leaf} a cell array holding ## what each answers at each of its nodes, already transformed, and @var{w} ## the weight of each tree, summing to one. ## ## @end deftypefn function [trees, leaf, w] = pdTrees (Mdl) trees = {}; leaf = {}; w = []; if (is_function_handle (Mdl)) return; endif cls = class (Mdl); switch (cls) case {'ClassificationTree', 'CompactClassificationTree', ... 'RegressionTree', 'CompactRegressionTree'} trees = {Mdl}; leaf = {pdLeaf(Mdl)}; w = 1; case {'ClassificationBaggedEnsemble', 'RegressionBaggedEnsemble', ... 'ClassificationEnsemble', 'RegressionEnsemble', ... 'CompactClassificationEnsemble', 'CompactRegressionEnsemble'} ## Only a bagged ensemble is taken over its trees; a boosted one is ## averaged over the data, as MATLAB documents and as measured. ## 'CombineWeights' is what says which, and unlike 'Method' every one of ## the six classes carries it, the compact regression one included. props = properties (Mdl); if (any (strcmp (props, 'CombineWeights')) && strcmp (Mdl.CombineWeights, 'WeightedAverage')) [trees, leaf, w] = pdEnsembleTrees (Mdl); endif endswitch endfunction ## What a tree answers at each of its nodes, with its transform applied. function L = pdLeaf (T) props = properties (T); if (any (strcmp (props, 'ClassProbability'))) L = T.ClassProbability; if (any (strcmp (props, 'ScoreTransform')) && ! isempty (T.STfun)) L = T.STfun (L); endif else L = T.NodeMean(:); if (any (strcmp (props, 'ResponseTransform')) && ! isempty (T.RTfun)) L = T.RTfun (L); endif endif endfunction ## The learners of a bagged ensemble and the weight of each. function [trees, leaf, w] = pdEnsembleTrees (Mdl) trees = {}; leaf = {}; w = []; props = properties (Mdl); if (! any (strcmp (props, 'Trained')) || isempty (Mdl.Trained)) return; endif trees = Mdl.Trained(:)'; n = numel (trees); ## Bagging takes trees, but an ensemble is not obliged to hold them, and ## anything else is averaged over the data rather than walked istree = cellfun (@(t) any (strcmp (properties (t), 'Children')), trees); if (! all (istree)) trees = {}; return; endif leaf = cell (1, n); for k = 1:n leaf{k} = pdLeaf (trees{k}); endfor if (any (strcmp (props, 'TrainedWeights')) && ! isempty (Mdl.TrainedWeights)) w = Mdl.TrainedWeights(:)'; else w = ones (1, n); endif w = w / sum (w); endfunction statistics-release-2.1.2/inst/Machine_Learning/private/pdValues.m000066400000000000000000000117501527311104300251000ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{pd} =} pdValues (@var{Mdl}, @var{F}, @var{predArgs}, @var{subIntercept}) ## ## Average what a model answers over a set of observations, one query point of ## the varied predictors at a time. ## ## @var{F} is the frame @code{pdFrame} resolved. @var{predArgs} holds the ## pairs forwarded to @code{predict}, which is where a generalized additive ## model is told whether to include its interaction terms. @var{subIntercept} ## takes the model's intercept off the result, which is what excluding it ## means for such a model. ## ## @var{pd} is @math{1xnumX} for a regression model varying one predictor, ## @math{numYxnumX} for two, and gains a leading dimension of one row per ## class where the model is a classifier, as MATLAB R2024a returns them. ## ## @end deftypefn function pd = pdValues (Mdl, F, predArgs, subIntercept) if (nargin < 3) predArgs = {}; endif if (nargin < 4) subIntercept = false; endif qx = F.QP{1}; nx = numel (qx); v1 = F.Vars(1); two = numel (F.Vars) == 2; if (two) qy = F.QP{2}; ny = numel (qy); v2 = F.Vars(2); else ny = 1; endif ## One column of answers per class, or the single column of a response m = pdWidth (Mdl, F, predArgs); ## A tree, and an ensemble of bagged trees, answer over the distribution ## they were fitted on rather than over any set of observations [trees, leaf, tw] = pdTrees (Mdl); overTrees = ! isempty (trees); ## A generalized additive model answers over the distribution it was fitted ## on as a tree does, measured on R2024a 2026-09-17, but carries no node ## table to walk, so the observations it kept stand in for that ## distribution. The query points still come from the data given. own = pdOwnData (Mdl); pd = zeros (m, ny, nx); if (isempty (own)) Z = F.X; else Z = own; endif for ii = 1:nx if (! overTrees) Z(:,v1) = qx(ii); endif for jj = 1:ny if (overTrees) if (two) qv = [qx(ii), qy(jj)]; else qv = qx(ii); endif s = zeros (1, columns (leaf{1})); for t = 1:numel (trees) s += tw(t) * pdTraverse (trees{t}, F.Vars, qv, leaf{t}); endfor if (F.IsClass) s = s(F.LabelIdx); endif pd(:,jj,ii) = s(:); else if (two) Z(:,v2) = qy(jj); endif s = pdScore (Mdl, Z, F, predArgs); pd(:,jj,ii) = mean (s, 1)(:); endif endfor endfor if (subIntercept) pd -= Mdl.Intercept; endif ## A regression model keeps no leading dimension, and one variable no middle ## one, so that the result reads as MATLAB documents it if (m == 1) pd = reshape (pd, ny, nx); elseif (! two) pd = reshape (pd, m, nx); endif endfunction ## The observations a model takes its own average over, empty where it takes ## the average over the data it was given. function D = pdOwnData (Mdl) D = []; if (is_function_handle (Mdl)) return; endif props = properties (Mdl); isgam = any (strcmp (props, 'Interactions')); if (! isgam) return; endif if (any (strcmp (props, 'X')) && ! isempty (Mdl.X)) D = Mdl.X; endif endfunction ## How many columns of answers a single call gives back. function m = pdWidth (Mdl, F, predArgs) if (F.IsClass) m = numel (F.LabelIdx); elseif (is_function_handle (Mdl)) s = pdScore (Mdl, F.X, F, predArgs); m = columns (s); else m = 1; endif endfunction ## What the model answers for a set of observations, as a column per class. function s = pdScore (Mdl, Z, F, predArgs) if (is_function_handle (Mdl)) s = Mdl (Z); if (! (isnumeric (s) && isreal (s) && rows (s) == rows (Z))) error (strcat ("the function must answer with a real numeric column", ... " per observation.")); endif if (! isempty (F.OutCols)) if (any (F.OutCols > columns (s))) error (strcat ("'OutputColumns' must index the columns the", ... " function answers with.")); endif s = s(:, F.OutCols); endif elseif (F.IsClass) [~, s] = predict (Mdl, Z, predArgs{:}); s = s(:, F.LabelIdx); else s = predict (Mdl, Z, predArgs{:}); s = s(:); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/priorFromStruct.m000066400000000000000000000050231527311104300264750ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} priorFromStruct (@var{S}, @var{ClassNames}, @var{classname}) ## ## Resolve a structure form of @qcode{Prior} into a probability row vector. ## ## @var{S} carries a @qcode{ClassNames} field and a @qcode{ClassProbs} field. ## The probabilities are returned in the order of the model's @var{ClassNames} ## rather than the order they were given in, so naming the classes out of ## order still assigns each its own probability. ## ## @end deftypefn function pr = priorFromStruct (S, ClassNames, classname) if (! (isfield (S, 'ClassNames') && isfield (S, 'ClassProbs'))) error (strcat (classname, ": a structure 'Prior' must have", ... " 'ClassNames' and 'ClassProbs' fields.")); endif sn = S.ClassNames; sp = S.ClassProbs; if (classCount (sn) != numel (sp)) error (strcat (classname, ": 'ClassNames' and 'ClassProbs' must have", ... " the same number of elements.")); endif ## Textual names are matched whole. A character matrix holds one name per ## row padded with blanks, which cellstr removes, and a categorical or ## string array is compared by its text. textual = ! (isnumeric (ClassNames) || islogical (ClassNames)); if (textual) names = cellstr (ClassNames); if (ischar (sn) || isa (sn, 'categorical') || isa (sn, 'string')) sn = cellstr (sn); endif endif K = classCount (ClassNames); pr = zeros (1, K); for i = 1:K if (textual) j = find (strcmp (sn, names{i})); else j = find (sn == ClassNames(i)); endif if (isempty (j)) error (strcat (classname, ": 'ClassNames' in the 'Prior' structure", ... " must name every class of the model.")); endif pr(i) = sp(j(1)); endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/priorNormalize.m000066400000000000000000000031721527311104300263300ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{w} =} priorNormalize (@var{w}, @var{gY}, @var{Prior}) ## ## Scale observation weights so that each class carries its prior. ## ## Within a class the weights keep their relative sizes; across classes each ## class's weights sum to that class's prior. A loss computed on weights that ## were not normalized this way would let an over-represented class speak for ## more than its prior says it should. ## ## A class with no weight at all is left alone rather than divided by zero. ## ## @seealso{ClassificationNaiveBayes, edgeWeights} ## @end deftypefn function w = priorNormalize (w, gY, Prior) w = w(:); for i = 1:numel (Prior) idx = gY == i; sw = sum (w(idx)); if (sw > 0) w(idx) = w(idx) * Prior(i) / sw; endif endfor sw = sum (w); if (sw > 0) w = w / sw; endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/priorWeights.m000066400000000000000000000025511527311104300260020ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} priorWeights (@var{Prior}, @var{gY}, @var{n}) ## ## Spread each class prior evenly over the observations of that class. ## ## Every observation of class @var{k} carries @qcode{Prior(k) / n_k}, so the ## observations of a class always sum to its prior. @var{gY} holds the class ## index of each of the @var{n} observations. ## ## @end deftypefn function w = priorWeights (Prior, gY, n) w = zeros (n, 1); for k = 1:numel (Prior) idx = (gY == k); nk = sum (idx); if (nk > 0) w(idx) = Prior(k) / nk; endif endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/regFrame.m000066400000000000000000000066631527311104300250540ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{F} =} regFrame (@var{X}, @var{Y}, @var{Weights}, @var{classname}) ## @deftypefnx {Private Function} {@var{F} =} regFrame (@var{X}, @var{Y}, @var{Weights}, @var{classname}, @var{rowRule}) ## ## Validate the data of a regression model and resolve its observation ## weights. ## ## The counterpart of @code{classFrame} for the models with a continuous ## response: check the shapes, drop the rows that are not complete in both ## the predictors and the response, and normalize the weights to sum to one. ## ## Fields: @qcode{X} and @qcode{Y}, the retained data; @qcode{RowsUsed}, a ## logical over the rows as supplied; @qcode{Weights}, the retained weights ## before normalization, as double; @qcode{WeightsClass}, the class the ## weights were given in; @qcode{W}, the same normalized; and @qcode{n} and ## @qcode{p}. ## ## @var{rowRule} says which rows are dropped for their predictors. ## @qcode{'complete'}, the default, drops a row missing any predictor. ## @qcode{'partial'} keeps it unless every predictor is missing, and drops a ## row of weight zero, as R2024a's ensembles do, whose learners take the ## missing values themselves. A negative weight is left to the caller to ## refuse. ## ## @end deftypefn function F = regFrame (X, Y, Weights, classname, rowRule = 'complete') if (! (isnumeric (X) && isreal (X) && ismatrix (X) && ndims (X) == 2)) error ("%s: invalid values in X.", classname); endif if (isempty (X)) error ("%s: X is empty.", classname); endif if (! (isnumeric (Y) && isreal (Y) && isvector (Y))) error ("%s: invalid values in Y.", classname); endif Y = Y(:); if (rows (X) != rows (Y)) error ("%s: number of rows in X and Y must be equal.", classname); endif errmsg = weightsClass (Weights); if (! isempty (errmsg)) error ("%s: %s", classname, errmsg); endif WeightsClass = "double"; if (isempty (Weights)) Weights = ones (rows (X), 1); else WeightsClass = class (Weights); Weights = double (Weights(:)); if (numel (Weights) != rows (X)) error ("%s: 'Weights' must have one element per observation.", ... classname); endif endif F = struct (); if (strcmp (rowRule, 'partial')) F.RowsUsed = ! (isnan (Y) | all (isnan (X), 2)) & Weights != 0; else F.RowsUsed = ! (isnan (Y) | any (isnan (X), 2)); endif F.X = X(F.RowsUsed, :); F.Y = Y(F.RowsUsed); F.Weights = Weights(F.RowsUsed); F.WeightsClass = WeightsClass; if (isempty (F.Y)) error ("%s: no complete observations in the data.", classname); endif [F.n, F.p] = size (F.X); F.W = F.Weights; if (sum (F.W) > 0) F.W = F.W / sum (F.W); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/removedClasses.m000066400000000000000000000062031527311104300262710ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{drop}, @var{Prior}, @var{msg}] =} removedClasses (@var{Prior}, @var{Cost}, @var{ClassNames}, @var{gY}) ## ## The classes a classifier removes from its data before fitting. ## ## A class whose prior probability is 0, or whose row of misclassification ## costs is all zeros, can never be predicted and contributes nothing to the ## fit, its observations weighing nothing, so its observations are removed, ## as MATLAB removes them. @var{Prior} is the prior as a vector in the order ## of @var{ClassNames}, and @var{Cost} the cost matrix, or empty. @var{drop} ## is a logical row, true for each class removed; @var{Prior} is returned ## with those classes at 0, normalized to sum to 1. @var{gY} holds the class ## index of each observation; @var{msg} names the removed classes that hold ## any, the text of the warning the caller raises under its own name, and is ## empty when none does, a class absent from the data having nothing to ## remove. ## @end deftypefn function [drop, Prior, msg] = removedClasses (Prior, Cost, ClassNames, gY) drop = (Prior(:)' == 0); ## A row of zero costs means a class free to misclassify, which needs ## another class to be misclassified as: a model of one class has none if (! isempty (Cost) && rows (Cost) > 1) drop |= all (Cost == 0, 2)'; endif msg = ''; if (! any (drop)) return; endif Prior(drop) = 0; if (sum (Prior) > 0) Prior = Prior / sum (Prior); endif ## The removed classes named as text, whatever the type of ClassNames if (iscellstr (ClassNames)) names = ClassNames(:); elseif (ischar (ClassNames) || iscategorical (ClassNames) || isstring (ClassNames)) names = cellstr (ClassNames); else names = arrayfun (@num2str, ClassNames(:), 'UniformOutput', false); endif held = false (size (drop)); held(unique (gY(isfinite (gY) & gY > 0))) = true; names = names(drop & held(1:numel (drop))); if (isempty (names)) return; endif listed = strjoin (strcat ("'", names(:)', "'"), ', '); if (numel (names) == 1) msg = sprintf (strcat ("the observations of class %s are removed, its", ... " prior probability being 0."), listed); else msg = sprintf (strcat ("the observations of classes %s are removed,", ... " their prior probabilities being 0."), listed); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/restore_tables.m000066400000000000000000000047271527311104300263400ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{out} =} restore_tables (@var{in}) ## ## Rebuild any @code{table} that @code{load} handed back as a plain structure. ## ## Saving to a binary file keeps a table's contents but not its class, so it ## comes back as a structure carrying @qcode{VariableNames} and ## @qcode{VariableValues} among the table's other properties. This helper ## turns such a structure back into the table it was, and recurses through the ## fields of any other structure and the elements of any cell, so that a table ## nested inside one is restored with it. ## ## Anything that is not a saved table is returned unchanged. ## ## This helper is shared by the @code{load_model} methods of the learner ## classes, whose saved properties may hold tables. ## ## @end deftypefn function out = restore_tables (in) if (isstruct (in) && isscalar (in) && is_saved_table (in)) out = table (in.VariableValues{:}, 'VariableNames', in.VariableNames); if (! isempty (in.RowNames)) out.Properties.RowNames = in.RowNames; endif return; endif if (isstruct (in) && isscalar (in)) out = in; for [val, key] = in out.(key) = restore_tables (val); endfor return; endif if (iscell (in)) out = cellfun (@restore_tables, in, 'UniformOutput', false); return; endif out = in; endfunction ## A structure is a saved table when it carries the fields the table class ## writes out; testing for the pair that holds the data is enough, since no ## structure the models save has both. function tf = is_saved_table (s) tf = isfield (s, 'VariableNames') && isfield (s, 'VariableValues') ... && isfield (s, 'RowNames') && iscell (s.VariableValues); endfunction statistics-release-2.1.2/inst/Machine_Learning/private/svmKernelParams.m000066400000000000000000000032461527311104300264300ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{s} =} svmKernelParams (@var{fcn}, @var{scale}, @var{order}) ## ## Build the @qcode{KernelParameters} structure of an SVM model. ## ## @var{fcn} is the kernel as the class recorded it, @var{scale} its scale and ## @var{order} the polynomial order. The structure carries @qcode{Function} ## and @qcode{Scale}, and @qcode{Order} for a polynomial kernel only, which is ## the shape MATLAB reports. MATLAB names the radial basis kernel ## @qcode{'gaussian'} whatever the caller spelled it, so @qcode{'rbf'} is ## renamed here; the kernel the fit was given is unchanged and remains in ## @qcode{ModelParameters}. ## ## @end deftypefn function s = svmKernelParams (fcn, scale, order) if (strcmpi (fcn, 'rbf')) fcn = 'gaussian'; endif s = struct ('Function', fcn, 'Scale', scale); if (strcmpi (fcn, 'polynomial')) s.Order = order; endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/svmPlusMinus.m000066400000000000000000000053771527311104300260120ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{Ypm} =} svmPlusMinus (@var{Y}, @var{ClassNames}) ## ## Response of a two-class support vector machine in @math{+1/-1} coding. ## ## @var{Y} may hold the class labels, in whatever type @var{ClassNames} holds ## them, which is what @code{margin} and @code{loss} document and what MATLAB ## accepts. It may also already be numeric @math{+1/-1}, which is the coding ## the underlying solver works in, and is then passed through unchanged. ## ## The first class in @var{ClassNames} maps to @math{+1} and the second to ## @math{-1}, matching the sign convention the decision values carry. ## ## @end deftypefn function Ypm = svmPlusMinus (Y, ClassNames) ## Already the solver's own coding, so nothing to map. This is checked ## first and without consulting ClassNames on purpose: where the classes ## themselves are -1 and +1 the two readings coincide, and mapping through ## the sorted names would invert the sign of every margin. if (isnumeric (Y) && ! isempty (Y) && all (ismember (Y(:), [-1; 1]))) Ypm = double (Y(:)); return; endif ## Both sides go through cellstr so that the padding a character matrix ## carries is stripped from each consistently: comparing a padded row of Y ## against an unpadded class name matches nothing. if (iscellstr (ClassNames) || ischar (ClassNames)) idx = cellfun (@(v) find (strcmp (v, cellstr (ClassNames)), 1), ... cellstr (Y), 'UniformOutput', false); else ## A loop rather than arrayfun, which calls its function once with the ## whole array where the labels are a classdef object such as a ## categorical, instead of once per element. idx = cell (numel (Y), 1); for k = 1:numel (Y) idx{k} = find (Y(k) == ClassNames(:), 1); endfor endif if (any (cellfun (@isempty, idx))) error ("svmPlusMinus: Y contains a label that is not in ClassNames."); endif idx = cell2mat (idx(:)); Ypm = ones (numel (idx), 1); Ypm(idx == 2) = -1; endfunction statistics-release-2.1.2/inst/Machine_Learning/private/svmPosterior.m000066400000000000000000000067151527311104300260360ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{st}, @var{ST}, @var{separated}] =} svmPosterior (@var{s}, @var{k}, @var{prior}, @var{classnames}) ## ## Fit the score-to-posterior transformation of a two-class SVM. ## ## @var{s} holds the scores of the second class, @code{@var{classnames}(2)}, ## and @var{k} the class index, 1 or 2, of each observation; rows whose score ## is @qcode{NaN} or whose index is neither are left out. @var{prior} is the ## model's prior. ## ## A zero in @var{prior} gives the constant transformation, posterior 1 for ## the other class, and @var{s} and @var{k} are not read. Otherwise, scores ## of the two classes that do not overlap give the step function, 0 below the ## largest score of the first class, 1 above the smallest score of the second ## and @code{@var{prior}(2)} between, and @var{separated} is true. Any other ## data give the sigmoid @code{1 / (1 + exp (A * s + B))}, fitted by ## maximum likelihood with @code{glmfit} to the class of each observation, ## unweighted. ## ## @var{st} is the transformation as the @qcode{ScoreTransform} property ## takes it, each parameter written in the fewest significant digits, 15 to ## 17, that read back to the same double, and @var{ST} the structure ## @code{fitPosterior} returns. ## @end deftypefn function [st, ST, separated] = svmPosterior (s, k, prior, classnames) separated = false; ## One class cannot occur: every observation is the other if (any (prior == 0)) c = find (prior > 0, 1); st = sprintf ('@(S)constant(S,%d)', c); ST = struct ('Type', 'constant', 'PredictedClass', {classnames(c)}); return; endif keep = ! isnan (s(:)) & (k(:) == 1 | k(:) == 2); s = s(keep); y = (k(keep) == 2); ## Classes the scores separate: a step function if (any (y) && any (! y) && max (s(! y)) < min (s(y))) separated = true; lb = max (s(! y)); ub = min (s(y)); pp = prior(2); st = sprintf ('@(S)step(S,%s,%s,%s)', exact (lb), exact (ub), exact (pp)); ST = struct ('Type', 'step', 'LowerBound', lb, 'UpperBound', ub, ... 'PositiveClassProbability', pp); return; endif ## Overlapping classes: logistic regression of the class on the score b = glmfit (s, double (y), 'binomial'); A = -b(2); B = -b(1); st = sprintf ('@(S)sigmoid(S,%s,%s)', exact (A), exact (B)); ST = struct ('Type', 'sigmoid', 'Slope', A, 'Intercept', B); endfunction ## X in the fewest significant digits, from 15 to 17, that read back to X ## exactly, so the text is the parameter itself and stays short where it can. function t = exact (x) for d = 15:16 t = sprintf ('%.*g', d, x); if (str2double (t) == x) return; endif endfor t = sprintf ('%.17g', x); endfunction statistics-release-2.1.2/inst/Machine_Learning/private/tableFrame.m000066400000000000000000000302441527311104300253560ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {private} {[@var{X}, @var{Y}, @var{args}, @var{lev}, @var{errmsg}] =} tableFrame (@var{X}, @var{Y}, @var{args}) ## @deftypefnx {private} {[@var{X}, @var{Y}, @var{args}, @var{lev}, @var{errmsg}] =} tableFrame (@var{X}, @var{Y}, @var{args}, @var{ordnum}) ## ## Resolve table input into the predictors and the response a learner takes. ## ## @var{X} is the predictor data, a table or a numeric matrix, and @var{Y} ## either the response, the name of the variable holding it, a model ## formula naming the response and the predictors together, or empty where ## there is no response and every variable is a predictor, ## @qcode{'Y ~ x1 + x2'}. A formula holds main effects only: no wildcard, ## no products and no powers, as R2024a takes none of them. @var{args} are ## the name-value arguments the call carried. ## ## Where @var{X} is not a table nothing is resolved and the three come back as ## they went in, with @var{lev} empty. Where it is, @var{X} comes back as a ## real numeric matrix, @var{Y} as the response, and @var{args} carrying ## @qcode{'PredictorNames'}, @qcode{'ResponseName'} and ## @qcode{'CategoricalPredictors'} as the table gives them. Where ## @qcode{'Weights'} names a variable of the table, @var{args} carries that ## variable's values instead, and the variable is no predictor. @var{lev} is ## one cell per predictor, holding the levels of a predictor read from text and ## empty for one read from numbers, which is what lets a level map to the code ## at prediction that it carried at fitting. Empty text, a blank character ## row and a missing string are missing, as an undefined categorical value ## is: no level, and coded @qcode{NaN}. ## ## An ordinal categorical column is coded by the order of its categories and ## counts as a categorical predictor, unless @var{ordnum} is true, when it is ## a numeric one, as in MATLAB's trees, ensembles and generalized additive ## models. Naming it in @qcode{'CategoricalPredictors'} still makes it ## categorical. @var{ordnum} is false by default. ## ## This runs before any model exists, which is why it is here rather than on ## @code{PredictiveModel}: mapping a table onto a model that has already been ## fitted is that class's to do. ## ## @end deftypefn function [X, Y, args, lev, errmsg] = tableFrame (X, Y, args, ordnum = false) lev = {}; errmsg = ''; if (! istable (X)) return; endif names = X.Properties.VariableNames; ## The response is named where it is one of the table's own variables, ## and given outright otherwise respname = 'Y'; keep = true (1, numel (names)); formula = {}; if (ischar (Y) && isrow (Y) || (isa (Y, 'string') && isscalar (Y))) spec = char (Y); if (any (spec == '~')) [respname, formula, errmsg] = tfFormula (spec, names); if (! isempty (errmsg)) return; endif else respname = spec; j = find (strcmp (names, respname), 1); if (isempty (j)) ## A formula missing its tilde reads as a response name that no ## column carries, which is a poor way to be told of a typo if (any (ismember (spec, '+*:^'))) errmsg = sprintf (strcat ("'%s' is not a model formula: a", ... " formula names its response before", ... " a '~'."), spec); else errmsg = sprintf ("the table holds no variable '%s'.", respname); endif return; endif endif Y = X.(respname); keep(strcmp (names, respname)) = false; elseif (isempty (Y)) ## No response at all: every variable is a predictor. An explainer ## works over predictor data alone, there being nothing to predict. Y = []; elseif (rows (Y) != height (X)) errmsg = "the table must have one row per response."; return; endif ## Observation weights may name a table variable, which then gives the ## weights and is no predictor, as in MATLAB for j = 1:2:numel (args) - 1 w = args{j+1}; if (ischar (args{j}) && strcmpi (args{j}, 'Weights') && (ischar (w) && isrow (w) || (isa (w, 'string') && isscalar (w)))) wname = char (w); if (! any (strcmp (names, wname))) errmsg = sprintf ("the table holds no variable '%s'.", wname); return; endif args{j+1} = X.(wname); keep(strcmp (names, wname)) = false; endif endfor ## Whatever is left is a predictor, unless the formula or the call named ## a subset; the two together are a conflict, as R2024a calls it one pnames = names(keep); [pn, args] = tfTakeArg (args, 'PredictorNames'); if (! isempty (formula)) if (! isempty (pn)) errmsg = strcat ("'PredictorNames' cannot be given beside a model", ... " formula, which names the predictors itself."); return; endif pnames = formula; elseif (! isempty (pn)) [pnames, errmsg] = tfNameSubset (pn, pnames); if (! isempty (errmsg)) return; endif endif if (isempty (pnames)) errmsg = "the table holds no predictors."; return; endif ## A column of levels is a categorical predictor whether or not the call ## said so, and what the call said is added to that rather than replacing ## it, as R2024a adds it [cp, args] = tfTakeArg (args, 'CategoricalPredictors'); M = numel (pnames); iscat = false (1, M); lev = cell (1, M); Xn = zeros (height (X), M); for k = 1:M v = X.(pnames{k}); if (columns (v) != 1) errmsg = sprintf (strcat ("the table variable '%s' must hold one", ... " column."), pnames{k}); return; endif [Xn(:,k), lev{k}, iscat(k), errmsg] = tfColumn (v, pnames{k}, ordnum); if (! isempty (errmsg)) return; endif endfor idx = find (iscat); if (! isempty (cp)) [named, errmsg] = tfCatSpec (cp, pnames); if (! isempty (errmsg)) return; endif idx = unique ([idx, named]); endif X = Xn; args = [{'PredictorNames', pnames, 'ResponseName', respname, ... 'CategoricalPredictors', idx}, args]; endfunction ## One table variable as the numbers a learner takes, and the levels it was ## read through where it holds text. An ordinal column is numeric where ## ORDNUM says so, its codes following the order of its categories. function [v, lev, iscat, errmsg] = tfColumn (col, name, ordnum) v = []; lev = []; iscat = false; errmsg = ''; if (isnumeric (col) && isreal (col)) v = double (col); return; endif if (islogical (col)) v = double (col); iscat = true; return; endif if (isa (col, 'categorical')) lev = categories (col)(:)'; v = double (col); iscat = ! (ordnum && isordinal (col)); return; endif ## Empty text, a blank character row and a missing string are missing, as ## in MATLAB, so they are no level and code as NaN, as an undefined ## categorical value does if (ischar (col)) col = cellstr (col); elseif (isa (col, 'string')) col = cellstr (col); endif if (iscellstr (col)) miss = cellfun ('isempty', col(:)); lev = unique (col(! miss))'; v = NaN (numel (col), 1); for k = 1:numel (lev) v(strcmp (col(:), lev{k})) = k; endfor iscat = true; return; endif errmsg = sprintf (strcat ("the table variable '%s' must hold numbers,", ... " levels or text."), name); endfunction ## The response and the predictors a model formula names, read by ## parseWilkinsonFormula rather than by anything of our own. A learner ## takes main effects only, so a term over more than one variable is a ## product and a variable carrying a caret is a power, and R2024a refuses ## both. The parser is asked for the order the formula wrote the names in, ## which is the order a learner reports them in, rather than the sorted ## order it gives by default. function [respname, terms, errmsg] = tfFormula (spec, names) respname = ''; terms = {}; errmsg = ''; try F = parseWilkinsonFormula (spec, 'matrix', 'stable'); catch errmsg = sprintf ("'%s' is not a model formula.", spec); return; end_try_catch if (! (isfield (F, 'ResponseIdx') && isscalar (F.ResponseIdx))) errmsg = sprintf ("'%s' names no response.", spec); return; endif respname = F.VariableNames{F.ResponseIdx}; ## A term over more than one variable is a product if (any (sum (F.Terms, 2) > 1)) errmsg = strcat ("a model formula holds main effects only, so no", ... " products, powers or wildcards."); return; endif used = F.VariableNames(any (F.Terms == 1, 1)); if (any (cellfun (@(v) any (v == '^'), used))) errmsg = strcat ("a model formula holds main effects only, so no", ... " products, powers or wildcards."); return; endif for nm = [{respname}, used] if (! any (strcmp (names, nm{1}))) errmsg = sprintf (strcat ("the model formula names '%s', which the", ... " table does not hold."), nm{1}); return; endif endfor if (any (strcmp (used, respname))) errmsg = strcat ("a model formula cannot name the response among its", ... " predictors."); return; endif terms = used; endfunction ## Take one name-value pair out of the argument list, where it is there. function [val, args] = tfTakeArg (args, name) val = []; for k = 1:2:numel (args) - 1 if (ischar (args{k}) && strcmpi (args{k}, name)) val = args{k+1}; args(k:k+1) = []; return; endif endfor endfunction ## The predictors named, in the order the call named them. function [sub, errmsg] = tfNameSubset (pn, pnames) sub = {}; errmsg = ''; if (ischar (pn) && isrow (pn)) pn = {pn}; elseif (isa (pn, 'string')) pn = cellstr (pn); endif if (! iscellstr (pn)) errmsg = "'PredictorNames' must name variables of the table."; return; endif for k = 1:numel (pn) if (! any (strcmp (pnames, pn{k}))) errmsg = sprintf (strcat ("'PredictorNames' names no variable", ... " '%s' of the table."), pn{k}); return; endif endfor sub = pn(:)'; endfunction ## The categorical predictors a call named, as indices into the predictors. function [idx, errmsg] = tfCatSpec (cp, pnames) idx = []; errmsg = ''; M = numel (pnames); if (ischar (cp) && isrow (cp) && strcmpi (cp, 'all')) idx = 1:M; return; endif if (islogical (cp)) if (numel (cp) != M) errmsg = strcat ("'CategoricalPredictors' as a logical vector must", ... " have one value per predictor."); return; endif idx = find (cp(:)'); return; endif if (isnumeric (cp)) if (! (isreal (cp) && all (cp == fix (cp)) && all (cp >= 1) && all (cp <= M))) errmsg = strcat ("'CategoricalPredictors' must index the", ... " predictors of the table."); return; endif idx = unique (double (cp(:)')); return; endif if (ischar (cp)) cp = cellstr (cp); elseif (isa (cp, 'string')) cp = cellstr (cp); endif if (! iscellstr (cp)) errmsg = strcat ("'CategoricalPredictors' must name or index the", ... " predictors of the table."); return; endif for k = 1:numel (cp) j = find (strcmp (pnames, cp{k}), 1); if (isempty (j)) errmsg = sprintf (strcat ("'CategoricalPredictors' names no", ... " predictor '%s' of the table."), cp{k}); return; endif idx(end+1) = j; endfor idx = unique (idx); endfunction statistics-release-2.1.2/inst/Machine_Learning/private/tableToMatrix.m000066400000000000000000000072411527311104300260740ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {private} {@var{Z} =} tableToMatrix (@var{pnames}, @var{lev}, @var{T}, @var{caller}) ## ## Map a table onto a named set of predictors, by name and not by position. ## ## @var{pnames} names the predictors wanted and @var{lev} holds, one cell ## each, the levels a predictor read from text was coded through, empty for ## one read from numbers. A variable @var{pnames} does not name is passed ## over, one it names and the table does not hold is named in the error, and ## a value holding a level is coded as that level was coded before, so a ## table carrying only some of the levels still codes them the same way. A ## missing value, and a level the model was not fitted on, is coded ## @qcode{NaN}, which the model predicts as missing, as MATLAB does. ## ## A matrix passes through, its columns taken in the order they come. ## ## A fitted model reaches this through @code{PredictiveModel.tableColumns}. ## @code{shapley} and @code{lime} reach it directly: each holds a model ## without being one, so neither inherits that method, and the mapping is ## written once rather than three times. ## ## @end deftypefn function Z = tableToMatrix (pnames, lev, T, caller) if (! istable (T)) Z = T; return; endif if (ischar (pnames)) pnames = cellstr (pnames); endif have = T.Properties.VariableNames; Z = zeros (height (T), numel (pnames)); for k = 1:numel (pnames) j = find (strcmp (have, pnames{k}), 1); if (isempty (j)) error ("%s: the table holds no predictor '%s'.", caller, pnames{k}); endif col = T.(pnames{k}); if (numel (lev) >= k && ! isempty (lev{k})) Z(:,k) = ttmLevelCodes (col, lev{k}, caller, pnames{k}); else if (! (isnumeric (col) && isreal (col)) && ! islogical (col)) error (strcat ("%s: the table variable '%s' no longer holds what", ... " it held when the model was fitted."), caller, ... pnames{k}); endif Z(:,k) = double (col); endif endfor endfunction ## The codes a column of levels carried when the model was fitted, so that a ## table holding only some of them still codes them the same way. A missing ## value and an unseen level are NaN; a missing categorical or string value is ## found before conversion, since its text could spell a level. function v = ttmLevelCodes (col, lev, caller, name) miss = false (rows (col), 1); if (isa (col, 'categorical')) miss = isundefined (col(:)); col = cellstr (col); elseif (isa (col, 'string')) miss = ismissing (col(:)); col = cellstr (col); elseif (ischar (col)) col = cellstr (col); endif if (! iscellstr (col)) error (strcat ("%s: the table variable '%s' no longer holds what it", ... " held when the model was fitted."), caller, name); endif v = NaN (numel (col), 1); for k = 1:numel (lev) v(strcmp (col(:), lev{k})) = k; endfor v(miss) = NaN; endfunction statistics-release-2.1.2/inst/Machine_Learning/private/templateStruct.m000066400000000000000000000047201527311104300263340ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{T}, @var{errmsg}] =} templateStruct (@var{method}, @var{args}) ## ## Build a learner template from a list of name-value arguments. ## ## @var{method} names the learner and @var{args} is the caller's ## @code{varargin}. @var{T} is a structure carrying @qcode{Method}, ## @qcode{Type} and one field per name given, the value stored as it stands. ## ## The names themselves are not checked against the learner's own list. The ## learner owns that list and validates it when the template is consumed, so ## restating it here would give two lists to keep in step and a template that ## silently accepts what its learner refuses once they drift apart. What is ## checked here is only what must hold for a structure to be built at all. ## ## @var{errmsg} is empty when the template was built, and otherwise the body ## of a message the caller raises under its own name. ## ## @seealso{templateSVM, templateTree, templateKNN} ## @end deftypefn function [T, errmsg] = templateStruct (method, args) T = struct ('Method', method, 'Type', 'classification'); errmsg = ''; if (mod (numel (args), 2) != 0) errmsg = "name-value arguments must be in pairs."; return; endif for i = 1:2:numel (args) name = args{i}; if (! (ischar (name) && isrow (name))) errmsg = "parameter names must be character vectors."; return; endif ## A name that is not an identifier cannot be a field, and no learner ## takes one, so it is refused here rather than from inside setfield. if (! isvarname (name)) errmsg = strcat ("'", name, "' is not a valid parameter name."); return; endif T.(name) = args{i+1}; endfor endfunction statistics-release-2.1.2/inst/Machine_Learning/private/treeCollapse.m000066400000000000000000000054051527311104300257370ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{S} =} treeCollapse (@var{Children}, @var{Parent}, @var{CutPredictorIndex}, @var{CutPoint}, @var{nodes}) ## @deftypefnx {Private Function} {@var{S} =} treeCollapse (@dots{}, @var{CutCategories}) ## ## Turn the named branch nodes of a tree into leaves and renumber what is ## left. ## ## Everything below a collapsed node is discarded. The surviving nodes keep ## their order, so a parent still carries a lower number than its children, ## which is what lets one forward pass find what is reachable. ## ## @var{S} carries the rewritten @qcode{Children}, @qcode{Parent}, ## @qcode{CutPredictorIndex} and @qcode{CutPoint}, along with @qcode{keep}, ## the indices of the surviving nodes in the old numbering. Given ## @var{CutCategories}, the level sets of the cuts, @var{S} carries them ## rewritten too. A caller subsets whatever else it holds per node by ## @qcode{keep}. ## ## @seealso{ClassificationTree, RegressionTree} ## @end deftypefn function S = treeCollapse (Children, Parent, CutPredictorIndex, CutPoint, ... nodes, CutCategories) n = rows (Children); kid = Children; nodes = nodes(kid(nodes,1) > 0); kid(nodes,:) = 0; ## What is still reachable from the root keep = false (n, 1); keep(1) = true; for ii = 1:n if (keep(ii) && kid(ii,1) > 0) keep(kid(ii,1)) = true; keep(kid(ii,2)) = true; endif endfor idx = find (keep); ## Offset by one so that a zero, meaning no such node, renumbers to zero renum = zeros (n + 1, 1); renum(idx + 1) = 1:numel (idx); cutvar = CutPredictorIndex; cutval = CutPoint; cutvar(nodes) = 0; cutval(nodes) = NaN; S.keep = idx; S.Children = reshape (renum(kid(idx,:) + 1), numel (idx), 2); S.Parent = renum(Parent(idx) + 1); S.CutPredictorIndex = cutvar(idx); S.CutPoint = cutval(idx); if (nargin > 5 && rows (CutCategories) == n) cats = CutCategories; cats(nodes,:) = {zeros(0, 0)}; S.CutCategories = cats(idx,:); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/treeCutInfo.m000066400000000000000000000061601527311104300255430ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{S} =} treeCutInfo (@var{CutPredictorIndex}, @var{PredictorNames}) ## @deftypefnx {Private Function} {@var{S} =} treeCutInfo (@dots{}, @var{CutCategories}) ## ## The descriptions of a tree's cuts, derived from its node table. ## ## @var{S} is a structure carrying @qcode{IsBranchNode}, @qcode{CutPredictor}, ## @qcode{CutType} and @qcode{CutCategories}, along with ## @qcode{CategoricalSplit} and the six @qcode{Surrogate} properties, which ## are empty in the shapes MATLAB reports for a tree without surrogate splits. ## ## @var{CutCategories}, an @math{Mx2} cell array, holds the levels each ## categorical cut sends left and right, empty for any other node; a branch ## with levels is a @qcode{'categorical'} cut and every other branch a ## @qcode{'continuous'} one. @qcode{CategoricalSplit} lists the level sets of ## the categorical cuts, one row each in node order. Left out or empty, no ## cut is categorical. ## ## Deriving them rather than storing them is what keeps them from falling out ## of step with the node table, which pruning rewrites. ## ## @seealso{ClassificationTree, CompactClassificationTree} ## @end deftypefn function S = treeCutInfo (CutPredictorIndex, PredictorNames, CutCategories) n = numel (CutPredictorIndex); br = CutPredictorIndex(:) > 0; cutname = repmat ({''}, n, 1); cuttype = repmat ({''}, n, 1); if (any (br)) cutname(br) = PredictorNames(CutPredictorIndex(br)); cuttype(br) = {'continuous'}; endif cats = repmat ({zeros(0, 0)}, n, 2); if (nargin > 2 && rows (CutCategories) == n && columns (CutCategories) == 2) iscat = br & ! cellfun ('isempty', CutCategories(:,1)); cats(iscat,:) = CutCategories(iscat,:); cuttype(iscat) = {'categorical'}; else iscat = false (n, 1); endif S.IsBranchNode = br; S.CutPredictor = cutname; S.CutType = cuttype; S.CutCategories = cats; ## Surrogate splits are not implemented, and these are the shapes MATLAB ## reports for a tree that has none. if (any (iscat)) S.CategoricalSplit = cats(iscat,:); else S.CategoricalSplit = cell (0, 0); endif S.SurrogateCutCategories = cell (0, 0); S.SurrogateCutFlip = cell (0, 0); S.SurrogateCutPoint = cell (0, 0); S.SurrogateCutType = cell (0, 0); S.SurrogateCutPredictor = cell (0, 1); S.SurrogatePredictorAssociation = cell (0, 0); endfunction statistics-release-2.1.2/inst/Machine_Learning/private/treeFoldArgs.m000066400000000000000000000065461527311104300257050ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{args} =} treeFoldArgs (@var{Mdl}) ## ## The Name-Value arguments a fold of a @code{ClassificationTree} or a ## @code{RegressionTree} is grown with. ## ## A classifier's fold is given the parent's class names, prior and cost ## rather than being left to re-derive them from its own rows; both are given ## the growth parameters the parent actually used. The observation weights ## are not here, being sliced per fold by the caller. ## ## @qcode{'ScoreTransform'} and @qcode{'ResponseTransform'} are deliberately ## absent: a transform is applied once to the assembled answer, and a fold ## carrying one would apply it twice. ## ## @qcode{'MaxNumSplits'} defaults to one less than the number of observations, ## so a fold works its own out; a budget the caller actually asked for is passed ## on. @qcode{'NumVariablesToSample'} is passed on too, so a fold samples its ## predictors as the parent did. @qcode{'MinParentSize'} is given the value the ## fit settled on rather than the one asked for, which reproduces it: the ## constructor takes the larger of it and twice @qcode{'MinLeafSize'}, and the ## larger is already there. A tree with categorical predictors passes them on, ## with @qcode{'MaxNumCategories'} and, for a classifier that was given one, ## @qcode{'AlgorithmForCategorical'}. ## ## @seealso{ClassificationTree, ClassificationPartitionedModel} ## @end deftypefn function args = treeFoldArgs (Mdl) MP = Mdl.ModelParameters; args = {'PredictorNames', Mdl.PredictorNames, ... 'ResponseName', Mdl.ResponseName}; if (strcmp (MP.Type, 'classification')) args = [args, {'ClassNames', Mdl.ClassNames, ... 'Prior', Mdl.Prior, 'Cost', Mdl.Cost}]; else args = [args, {'QuadraticErrorTolerance', MP.QEToler}]; endif args = [args, {'SplitCriterion', MP.SplitCriterion, ... 'MinParentSize', MP.MinParent, ... 'MinLeafSize', MP.MinLeaf, ... 'MergeLeaves', MP.MergeLeaves, ... 'NumVariablesToSample', MP.NVarToSample, ... 'Prune', MP.Prune, ... 'PruneCriterion', MP.PruneCriterion}]; if (MP.MaxSplits != Mdl.NumObservations - 1) args = [args, {'MaxNumSplits', MP.MaxSplits}]; endif if (! isempty (Mdl.CategoricalPredictors)) args = [args, {'CategoricalPredictors', Mdl.CategoricalPredictors, ... 'MaxNumCategories', MP.MaxCat}]; if (strcmp (MP.Type, 'classification') && ! strcmpi (MP.AlgCat, 'auto')) args = [args, {'AlgorithmForCategorical', MP.AlgCat}]; endif endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/treeLevelText.m000066400000000000000000000024711527311104300261110ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{txt} =} treeLevelText (@var{name}, @var{levels}) ## ## The condition a categorical cut tests, as a tree's @code{view} prints it. ## ## @var{txt} reads @qcode{"x1=4"} for a single level and ## @qcode{"x1 in @{1 4 6@}"} for several, as MATLAB R2024a prints them. ## ## @end deftypefn function txt = treeLevelText (name, levels) if (isscalar (levels)) txt = sprintf ("%s=%g", name, levels); else txt = sprintf ("%s in {%s}", name, strtrim (sprintf ("%g ", levels))); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/treeNodeStats.m000066400000000000000000000101761527311104300261020ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{S} =} treeNodeStats (@var{ClassShare}, @var{Prior}, @var{Cost}, @var{crit}, @var{ClassNames}) ## ## The node statistics of a classification tree that depend on the prior or ## the cost. ## ## @var{ClassShare} holds, per node and per class, the share of that class's ## total weight which reached the node, and is free of both the prior and the ## cost: multiplying a class's column by a constant leaves it unchanged. ## Everything below is derived from it, so the same table serves whatever ## @var{Prior} and @var{Cost} are assigned afterwards. ## ## @var{crit} is the split criterion the tree was grown under, @qcode{'gdi'} ## or @qcode{'deviance'}, which is the impurity the risk is measured by. ## ## @var{S} is a structure carrying @qcode{NodeProbability}, ## @qcode{ClassProbability}, @qcode{NodeError}, @qcode{NodeClass}, ## @qcode{NodeRisk} and @qcode{NodeProbAdj}, the last being the probability ## of reaching each node measured on the cost-adjusted weights, which is the ## scale @qcode{NodeRisk} lives on. ## ## The cost enters the risk and nothing else. Measured on R2024a: a ## classification tree reports every other node statistic on the unadjusted ## weights and measures the risk on the distribution each class's weight is ## scaled by the total cost of misclassifying it, which is the classical way ## a cost matrix enters CART. Under the default cost every class is scaled ## alike and the adjustment falls away. ## ## @seealso{ClassificationTree, CompactClassificationTree} ## @end deftypefn function S = treeNodeStats (ClassShare, Prior, Cost, crit, ClassNames) cw = ClassShare .* Prior(:)'; nw = sum (cw, 2); CP = zeros (size (cw)); nz = nw > 0; CP(nz,:) = cw(nz,:) ./ nw(nz); S.NodeProbability = nw; S.ClassProbability = CP; ## The class of least expected misclassification cost, the first of the ## class names keeping a tie, which is what min returns [err, k] = min (CP * Cost, [], 2); S.NodeError = err; names = classText (ClassNames); S.NodeClass = names(k); ## The impurity of the cost-adjusted distribution, weighted by the adjusted ## probability of reaching the node aw = cw .* sum (Cost, 2)'; anw = sum (aw, 2); AP = zeros (size (aw)); nz = anw > 0; AP(nz,:) = aw(nz,:) ./ anw(nz); if (anw(1) > 0) S.NodeProbAdj = anw / anw(1); else S.NodeProbAdj = zeros (rows (aw), 1); endif S.NodeRisk = S.NodeProbAdj .* nodeImpurity (AP, crit); endfunction ## The class names as text, whatever type they are carried in, which is what ## NodeClass reports and what view prints. function s = classText (C) if (iscellstr (C)) s = C(:); elseif (ischar (C)) s = cellstr (C); elseif (isa (C, 'categorical') || isa (C, 'string')) s = cellstr (C(:)); else s = arrayfun (@(v) num2str (v), C(:), 'UniformOutput', false); endif endfunction ## The impurity of a distribution, by the criterion the tree was grown under. ## The deviance is the entropy in bits halved, which is the scale MATLAB ## reports NodeRisk on: measured on R2024a, an iris tree grown under ## 'deviance' reports a root risk of 0.792481250360577, which is log2 (3) / 2. function imp = nodeImpurity (P, crit) if (strcmp (crit, 'gdi')) imp = 1 - sum (P .^ 2, 2); else L = P; L(L <= 0) = 1; # a class of no weight contributes nothing imp = -sum (P .* log2 (L), 2) / 2; endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/uniqueLabels.m000066400000000000000000000032761527311104300257520ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{C}, @var{ia}, @var{ic}] =} uniqueLabels (@var{Y}) ## The distinct class labels of a response, whatever type it is given in. ## ## @code{unique} compares a character matrix element by element, so a response ## naming its classes in the rows of a character matrix comes back as the ## distinct @emph{letters} of those names rather than the names themselves. ## Comparing by rows is what that response needs, and it is what every other ## accepted type already gets: a cell array of character vectors, a numeric ## column and a logical column each hold one label per row already. ## ## The three outputs carry @code{unique}'s own meanings, with rows in place of ## elements where the response is a character matrix. ## @end deftypefn function [C, ia, ic] = uniqueLabels (Y) if (ischar (Y)) [C, ia, ic] = unique (Y, "rows"); else [C, ia, ic] = unique (Y); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/unscoredLoss.m000066400000000000000000000032071527311104300257760ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{L} =} unscoredLoss (@var{LossFun}, @var{S}, @var{gY}, @var{w}, @var{Cost}, @var{K}) ## ## Classification loss of scores that may hold rows without a score. ## ## A row of @var{S} holding @code{NaN} has no score but still has a label, the ## class @var{K}, so it counts under @qcode{'classiferror'}, ## @qcode{'classifcost'} and @qcode{'mincost'}, and makes any other loss, which ## reads the scores, @code{NaN}. The remaining arguments are those of ## @code{classificationLoss}. ## ## @end deftypefn function L = unscoredLoss (LossFun, S, gY, w, Cost, K) none = any (isnan (S), 2); if (any (none)) if (! any (strcmp (LossFun, {'classiferror', 'classifcost', 'mincost'}))) L = NaN; return; endif S(none,:) = 0; S(none,K) = 1; endif L = classificationLoss (LossFun, S, gY, w, Cost); endfunction statistics-release-2.1.2/inst/Machine_Learning/private/weightedMedian.m000066400000000000000000000025341527311104300262330ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{m} =} weightedMedian (@var{Y}, @var{W}) ## ## The weighted median of @var{Y}, the value at which half the weight lies ## on each side. ## ## @end deftypefn ## The weighted median, the value at which half the weight lies on each side. ## It is what a fit of the epsilon-insensitive loss starts its intercept ## from, that loss being an absolute deviation once the band is crossed. function m = weightedMedian (Y, W) [ys, k] = sort (Y); ws = W(k); c = cumsum (ws) / sum (ws); j = find (c >= 0.5, 1); m = ys(j); endfunction statistics-release-2.1.2/inst/Machine_Learning/private/weightsClass.m000066400000000000000000000026061527311104300257550ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{errmsg} =} weightsClass (@var{W}) ## Check that observation weights are single or double. ## ## @var{errmsg} is the body of the message the caller should raise, or empty ## when @var{W} is empty or a real single or double array. Logical and integer ## weights are refused, as MATLAB refuses them. The other checks on the ## weights stay with the caller. ## @end deftypefn function errmsg = weightsClass (W) errmsg = ""; if (! isempty (W) && ! (isfloat (W) && isreal (W))) errmsg = "'Weights' must be a real vector of class single or double."; endif endfunction statistics-release-2.1.2/inst/Machine_Learning/private/zeroPriorNames.m000066400000000000000000000042641527311104300262760ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{tf} =} zeroPriorNames (@var{ClassNames}, @var{Prior}, @var{Cost}) ## ## Which of the classes named in @qcode{'ClassNames'} the user gave a prior ## probability of 0, or a row of misclassification costs that is all zeros. ## ## Such a class is removed before fitting, so it may be absent from the ## response, where any other named class must be present. @var{Prior} and ## @var{Cost} are the options as the user gave them, before they are ## resolved: a numeric vector or a structure for the prior, a matrix or a ## structure for the cost, or anything else, which marks no class. ## @var{tf} is a logical column, one element per name. ## @end deftypefn function tf = zeroPriorNames (ClassNames, Prior, Cost) K = classCount (ClassNames); tf = false (K, 1); if (K == 0) return; endif if (isstruct (Prior) && isfield (Prior, 'ClassNames') && isfield (Prior, 'ClassProbs')) try Prior = priorFromStruct (Prior, ClassNames, ''); catch Prior = []; end_try_catch endif if (isnumeric (Prior) && isreal (Prior) && numel (Prior) == K) tf |= (Prior(:) == 0); endif if (isstruct (Cost)) [Cost, errmsg] = costMatrix (Cost, ClassNames); if (! isempty (errmsg)) Cost = []; endif endif if (isnumeric (Cost) && isreal (Cost) && isequal (size (Cost), [K, K]) && K > 1) tf |= all (Cost == 0, 2); endif endfunction statistics-release-2.1.2/inst/Machine_Learning/shapley.m000066400000000000000000003230221527311104300233060ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef shapley ## -*- texinfo -*- ## @deftp {statistics} shapley ## ## Shapley values for a fitted model at one or more query points. ## ## A Shapley value says how much one predictor contributed to the deviation ## of a prediction from the average prediction. The values of a query point ## sum to that deviation exactly, which is the property the computation is ## built to keep. ## ## @code{@var{explainer} = shapley (@var{Mdl})} builds an explainer for the ## fitted model @var{Mdl} over the observations it was fitted on. A compact ## model keeps none, so it must be given them as @var{X}, and so must a ## function handle. Nothing is computed until query points are given, either ## to the constructor as @qcode{'QueryPoints'} or afterwards to ## @code{fit}. ## ## @code{@var{explainer} = shapley (@var{Mdl}, @var{X})} takes the ## observations to average over as @var{X}, a real numeric matrix of one ## column per predictor. ## ## @var{X} may also be a table, and so may @qcode{'QueryPoints'} and what ## @code{fit} is given. Where the model names its predictors the table is ## read by those names and not by the order its columns come in, so a ## column the model was not fitted on is passed over and a value holding a ## level is coded as that level was coded at fitting. A function handle ## names nothing, so a table given for one names the predictors itself and ## its columns are taken in the order they come. ## ## @code{@var{explainer} = shapley (@var{fun}, @var{X})} takes a function ## handle in place of a model. @var{fun} is called with a matrix of ## observations and answers with one real numeric column holding one value ## for each, so an explainer built on a handle always has a single column of ## values. ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## ## @item @qcode{'QueryPoints'} @tab @tab The observations to explain, one per ## row, with one column per predictor. The default is none, which leaves the ## values unfitted. ## ## @item @qcode{'NumObservationsToSample'} @tab @tab How many observations to ## draw, without replacement, from those averaged over, or @qcode{'all'} for ## every one of them. The default is 100, and so is any number reaching or ## exceeding how many there are. A drawn sample makes the values differ from ## one call to the next; @qcode{'all'} is what makes them reproducible. ## ## @item @qcode{'CategoricalPredictors'} @tab @tab The predictors whose ## values are levels, taken as by every learner of this package. It applies ## only to a function handle, a model being asked for its own. ## ## @item @qcode{'MaxNumSubsets'} @tab @tab How many predictor subsets at ## most to compute over, an integer above 1. The default is the lesser of ## @math{2^M}, which is every subset of the @math{M} predictors, and 1024. ## Every subset gives the values exactly; fewer estimates them, and fewer ## than @math{2M+2} estimates them poorly enough to warn about. Giving it ## at all asks for the subsets, so a linear model or a decision tree that ## would otherwise be answered from its own structure is answered over ## them instead. ## ## @item @qcode{'Method'} @tab @tab The algorithm, @qcode{'interventional'} ## by default, which averages over the observations as they stand. ## @qcode{'conditional'} averages instead over the tenth of them lying ## nearest the query point in the predictors being held, which stands in ## for conditioning on those predictors. It asks more of the data and is ## the dearer of the two. Observations at the same distance are taken in ## their order in the data, and distances equal to within rounding count ## as tied, so the result does not depend on the platform. MATLAB lets ## rounding decide such ties, so on data on a coarse grid the values can ## differ slightly from MATLAB's. ## @end multitable ## ## @code{'UseParallel'} is not implemented and is refused rather than ## ignored. ## ## @seealso{partialDependence, plotPartialDependence, PredictiveModel} ## @end deftp properties (SetAccess = protected) ## -*- texinfo -*- ## @deftp {shapley} {property} BlackboxModel ## ## The model being explained ## ## The fitted model, or the function handle, the explainer was built on. ## This property is read-only. ## ## @end deftp BlackboxModel = []; ## -*- texinfo -*- ## @deftp {shapley} {property} X ## ## The observations averaged over ## ## A real numeric matrix of one row per observation and one column per ## predictor, either given outright or taken from the model. It is the ## whole of what was given, before any sampling. This property is ## read-only. ## ## Where the observations were given as a table, they are the coded ## matrix and not the table: a variable holding levels is stored as its ## level codes, and the coding is kept with the object. ## ## @end deftp X = []; ## -*- texinfo -*- ## @deftp {shapley} {property} QueryPoints ## ## The observations explained ## ## A real numeric matrix of one row per query point, empty until query ## points are given. This property is read-only. ## ## @end deftp QueryPoints = []; ## -*- texinfo -*- ## @deftp {shapley} {property} BlackboxFitted ## ## What the model answers at the query points ## ## The response of a regression model or the predicted label of a ## classifier, one for each query point, empty until query points are ## given. A label keeps the type of the response the model was fitted ## with, as everywhere in this package. This property is read-only. ## ## @end deftp BlackboxFitted = []; ## -*- texinfo -*- ## @deftp {shapley} {property} Shapley ## ## The Shapley values ## ## A table of one row per predictor, holding the predictor names in ## @qcode{Predictor} and the values in @qcode{Value} for a regression ## model or a function handle, and in one variable per class, named after ## it, for a classifier. Each such variable holds one column per query ## point. It is empty until query points are given. This property is ## read-only. ## ## @end deftp Shapley = []; ## -*- texinfo -*- ## @deftp {shapley} {property} MeanAbsoluteShapley ## ## The mean absolute Shapley value of each predictor ## ## A table laid out as @qcode{Shapley}, holding the mean over the query ## points of the absolute values. With one query point it is their ## absolute value. This property is read-only. ## ## @end deftp MeanAbsoluteShapley = []; ## -*- texinfo -*- ## @deftp {shapley} {property} Intercept ## ## The average prediction ## ## The mean of what the model answers over the observations averaged over, ## a scalar for a regression model or a function handle and one value per ## class for a classifier. The values of a query point sum to the ## deviation of its prediction from this. This property is read-only. ## ## @end deftp Intercept = []; ## -*- texinfo -*- ## @deftp {shapley} {property} Method ## ## The algorithm the values were computed with ## ## A character vector. @qcode{'interventional-linear'} where the model ## predicts a weighted sum of its predictors, which is answered from the ## weights alone; @qcode{'interventional-tree'} for a decision tree and ## for an ensemble of them, which is answered leaf by leaf; ## @qcode{'interventional-kernel'} for ## every other model, which enumerates every subset of the predictors ## where the budget allows it and estimates the values by weighted least ## squares where it does not; and @qcode{'conditional-kernel'} where ## @qcode{'Method'} asked for conditioning. A tree and a linear model ## are answered exactly however many predictors they have, where the ## budget stops the subsets at 1024. This property is read-only. ## ## @end deftp Method = ''; ## -*- texinfo -*- ## @deftp {shapley} {property} NumSubsets ## ## How many predictor subsets the values were computed over ## ## The lesser of what @qcode{'MaxNumSubsets'} allowed and two raised to ## the number of predictors, which is every subset. This property is ## read-only. ## ## @end deftp NumSubsets = []; ## -*- texinfo -*- ## @deftp {shapley} {property} CategoricalPredictors ## ## The categorical predictors ## ## The indices of the predictors whose values are levels, empty where ## there are none. This property is read-only. ## ## @end deftp CategoricalPredictors = []; ## -*- texinfo -*- ## @deftp {shapley} {property} SampledObservationIndices ## ## The rows of @qcode{X} that were averaged over ## ## A sorted column of indices into @qcode{X}, holding every row where ## @qcode{'NumObservationsToSample'} did not draw a sample. This property ## is read-only. ## ## @end deftp SampledObservationIndices = []; endproperties properties (Access = protected, Hidden) IsClass = false; # whether the model answers with class scores ClassNames = []; # the classes, in the model's own order PredictorNames = {}; # one name per column of X endproperties methods (Hidden) function disp (this) if (isempty (this.Shapley)) printf (" shapley explainer with no Shapley values fitted.\n"); printf (" Use the fit method to fit Shapley values.\n"); else printf (" shapley explainer with the following "); printf ("local Shapley values:\n\n"); disp (this.Shapley); endif endfunction function display (this) inName = inputname (1); if (! isempty (inName)) printf ("%s =\n\n", inName); endif disp (this); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {shapley} {@var{obj} =} shapley (@var{Mdl}) ## @deftypefnx {shapley} {@var{obj} =} shapley (@var{Mdl}, @var{X}) ## @deftypefnx {shapley} {@var{obj} =} shapley (@var{fun}, @var{X}) ## @deftypefnx {shapley} {@var{obj} =} shapley (@dots{}, @var{name}, @var{value}) ## ## Build a Shapley explainer. ## ## The arguments are those described for the class. Where ## @qcode{'QueryPoints'} is given the values are computed at once, ## otherwise they are left to @code{fit}. ## ## @seealso{shapley, shapley.fit} ## @end deftypefn function this = shapley (blackbox, varargin) ## Input validation if (nargin < 1) error ("shapley: too few input arguments."); endif if (! (isa (blackbox, 'PredictiveModel') || is_function_handle (blackbox))) error (strcat ("shapley: BLACKBOX must be a fitted model that", ... " predicts, or a function handle.")); endif ## The observations come before the options, as MATLAB orders them args = varargin; Data = []; if (! isempty (args) && ! (ischar (args{1}) || isa (args{1}, 'string'))) Data = args{1}; args(1) = []; endif optNames = {'QueryPoints', 'NumObservationsToSample', ... 'CategoricalPredictors', 'Method', 'MaxNumSubsets', ... 'UseParallel'}; dfValues = {[], [], [], [], [], []}; [QP, NumObs, CatPred, Method, MaxSub, Par, rem] = ... parsePairedArguments (optNames, dfValues, args); if (! isempty (rem)) error (strcat ("shapley: unknown optional argument or", ... " misplaced value.")); endif if (! isempty (Par)) error ("shapley: 'UseParallel' is not implemented."); endif [base, errmsg] = shapMethod (Method); if (! isempty (errmsg)) error ("shapley: %s", errmsg); endif [F, errmsg] = shapFrame (blackbox, Data, CatPred, NumObs, MaxSub); if (! isempty (errmsg)) error ("shapley: %s", errmsg); endif method = shapAlgorithm (base, F, MaxSub); this.BlackboxModel = blackbox; this.X = F.X; this.CategoricalPredictors = F.Cat; this.SampledObservationIndices = F.Idx; this.Method = method; this.NumSubsets = F.NumSubsets; this.IsClass = F.IsClass; this.ClassNames = F.ClassNames; this.PredictorNames = F.PredictorNames; if (! isempty (QP)) this = fit (this, QP); endif endfunction ## -*- texinfo -*- ## @deftypefn {shapley} {@var{obj} =} fit (@var{obj}, @var{QueryPoints}) ## ## Compute the Shapley values at the given query points. ## ## @var{QueryPoints} is a real numeric matrix of one row per query point ## and one column per predictor, or a table read by the names the ## explainer holds. The values already held are replaced, not added to. ## ## @seealso{shapley} ## @end deftypefn function this = fit (this, QueryPoints) ## Input validation if (nargin != 2) error ("shapley.fit: invalid number of input arguments."); endif M = numel (this.PredictorNames); ## A table is read by the names the explainer holds, as X was if (istable (QueryPoints)) QueryPoints = tableToMatrix (this.PredictorNames, ... shapLevels (this.BlackboxModel), ... QueryPoints, 'shapley.fit'); endif if (! (isnumeric (QueryPoints) && isreal (QueryPoints) && ismatrix (QueryPoints) && ndims (QueryPoints) == 2 && ! isempty (QueryPoints))) error (strcat ("shapley.fit: QUERYPOINTS must be a real numeric", ... " matrix.")); endif if (columns (QueryPoints) != M) error (strcat ("shapley.fit: QUERYPOINTS must have one column per", ... " predictor of the model.")); endif Xs = this.X(this.SampledObservationIndices,:); sfcn = shapScoreFcn (this.BlackboxModel, this.IsClass); K = columns (sfcn (Xs(1,:))); nq = rows (QueryPoints); ## A weighted sum of the predictors, and a decision tree, are each ## answered from what the model is rather than over its subsets. A ## shortcut is checked against the whole deviation before it is kept, ## so a model read wrongly falls back rather than answering wrongly. linear = strcmp (this.Method, 'interventional-linear'); tree = strcmp (this.Method, 'interventional-tree'); exact = (M <= 20 && this.NumSubsets == 2 ^ M); Tree = []; leafval = []; Z = []; treephi = []; if (tree) ## The compiled kernel answers every query point in one call [Tree, leafval] = shapTreesOf (this.BlackboxModel); treephi = reshape (__shapleytree__ (shapTreeStructs (Tree, leafval), ... Xs, QueryPoints), M, K, nq); tree = shapAccounts (sum (treephi(:,:,1), 1), sfcn, Xs, ... QueryPoints(1,:)); elseif (linear) Z = shapLinearMasks (M); V1 = shapSubsetValues (sfcn, Xs, QueryPoints(1,:), Z, K, []); parts = sum (V1(3:end,:) - repmat (V1(1,:), M, 1), 1); linear = shapAccounts (parts, sfcn, Xs, QueryPoints(1,:)); endif ## Whatever is left over is answered over the subsets if (! (linear || tree)) if (strcmp (this.Method, 'interventional-linear') || strcmp (this.Method, 'interventional-tree')) this.Method = 'interventional-kernel'; endif if (exact) Z = shapAllMasks (M); else Z = shapSelectMasks (M, this.NumSubsets); if (this.NumSubsets < 2 * M + 2) warning (strcat ("shapley.fit: the values may be unreliable", ... " because 'MaxNumSubsets' is too small.")); endif endif endif ## Conditioning stands the nearest observations in for the rest of the ## distribution, so what it needs is measured once over the sample cnd = shapConditioning (Xs, this.Method); phi = zeros (M, K, nq); icept = []; if (tree) phi = treephi; icept = mean (sfcn (Xs), 1); endif for ii = 1:nq if (tree) break; endif if (! isempty (cnd)) cnd.qz = (QueryPoints(ii,:) - cnd.Mu) ./ cnd.Sigma; endif V = shapSubsetValues (sfcn, Xs, QueryPoints(ii,:), Z, K, cnd); v0 = V(1,:); if (linear) phi(:,:,ii) = V(3:end,:) - repmat (v0, M, 1); elseif (exact) phi(:,:,ii) = shapFromValues (V, M, K); else phi(:,:,ii) = shapKernelSolve (Z, V, M, K); endif if (ii == 1) icept = v0; endif endfor this.QueryPoints = QueryPoints; this.Intercept = icept; this.BlackboxFitted = shapFitted (this.BlackboxModel, QueryPoints, ... this.IsClass); this.Shapley = shapTable (phi, this.PredictorNames, this.ClassNames, ... this.IsClass, false); this.MeanAbsoluteShapley = shapTable (phi, this.PredictorNames, ... this.ClassNames, this.IsClass, ... true); endfunction ## -*- texinfo -*- ## @deftypefn {shapley} {} plot (@var{obj}) ## @deftypefnx {shapley} {} plot (@var{obj}, @var{name}, @var{value}) ## @deftypefnx {shapley} {} plot (@var{ax}, @dots{}) ## @deftypefnx {shapley} {@var{b} =} plot (@dots{}) ## ## Plot the Shapley values as a horizontal bar chart. ## ## One bar per predictor, the least important at the bottom. Over one ## query point the bars hold the values themselves and the chart is ## titled @qcode{'Shapley Explanation'}; over several they hold the mean ## of the absolute values and it is titled ## @qcode{'Shapley Importance Plot'}. ## ## @var{ax} is the axes to draw into, the current one where none is ## given. @var{b} holds one bar series per class drawn. ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## ## @item @qcode{'NumImportantPredictors'} @tab @tab How many predictors ## to draw on their own, the ten most important by default. Over ## several query points whatever is left over is drawn as one further ## bar holding its sum; over one query point it is left out. ## ## @item @qcode{'ClassNames'} @tab @tab The classes to draw, for a ## classification model. The default is the predicted class over one ## query point and every class over several. ## ## @item @qcode{'QueryPointIndices'} @tab @tab Which query points to ## draw, all of them by default. ## @end multitable ## ## @seealso{shapley, shapley.boxchart, shapley.swarmchart} ## @end deftypefn function varargout = plot (varargin) [ax, this, args] = shapDrawArgs (varargin, 'plot'); optNames = {'NumImportantPredictors', 'ClassNames', ... 'QueryPointIndices'}; [nip, cn, qpi, rem] = parsePairedArguments (optNames, {[], [], []}, ... args); if (! isempty (rem)) error (strcat ("shapley.plot: unknown optional argument or", ... " misplaced value.")); endif cnames = shapClassVars (this); qpi = shapCheckPoints (qpi, rows (this.QueryPoints), 'plot'); idx = shapCheckClasses (cnames, cn, 'plot', 'ClassNames', ... shapDefaultClasses (cnames, this.BlackboxFitted, qpi)); V = shapDrawValues (this.Shapley, cnames, idx, qpi); M = rows (V); nip = shapCheckCount (nip, M, 'plot'); ## Over one query point the values stand as they are; over several it ## is the mean of their absolute values that MATLAB draws one = (numel (qpi) == 1); if (one) vals = reshape (V(:,1,:), M, []); else vals = reshape (mean (abs (V), 2), M, []); endif ## The least important at the bottom, and over several query points ## whatever is left over is summed into one bar of its own [~, ord] = sort (sum (abs (vals), 2), 'ascend'); names = cellstr (this.Shapley.Predictor); names = names(ord); vals = vals(ord,:); if (nip < M) kept = (M - nip + 1):M; if (one) vals = vals(kept,:); names = names(kept); else lump = sum (vals(1:(M - nip),:), 1); vals = [lump; vals(kept,:)]; label = sprintf ('Sum of other %d predictor(s)', M - nip); names = [{label}; names(kept)]; endif endif if (isempty (ax)) ax = gca (); endif n = rows (vals); h = barh (ax, 1:n, vals); set (ax, 'ytick', 1:n, 'yticklabel', names); if (one) title (ax, 'Shapley Explanation'); xlabel (ax, 'Shapley Value'); else title (ax, 'Shapley Importance Plot'); xlabel (ax, 'Mean of Absolute Shapley Values'); endif ylabel (ax, 'Predictor'); if (numel (idx) > 1) legend (ax, cnames{idx}); endif if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {shapley} {} boxchart (@var{obj}) ## @deftypefnx {shapley} {} boxchart (@var{obj}, @var{name}, @var{value}) ## @deftypefnx {shapley} {} boxchart (@var{ax}, @dots{}) ## @deftypefnx {shapley} {@var{b} =} boxchart (@dots{}) ## ## Draw a box chart of the Shapley values over the query points. ## ## One box per predictor, the least important at the bottom, spread over ## the query points the values were fitted at. The chart is titled ## @qcode{'Shapley Summary Plot'} and lies horizontally. ## ## @var{ax} is the axes to draw into, the current one where none is ## given. @var{b} is the @code{stats.chart.BoxChart} drawn. ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## ## @item @qcode{'NumImportantPredictors'} @tab @tab How many predictors ## to draw, the ten most important by default. Whatever is left over is ## left out rather than summed, a box over a sum meaning nothing. ## ## @item @qcode{'ClassName'} @tab @tab The one class to draw, for a ## classification model. The default is the first class of the model. ## ## @item @qcode{'JitterOutliers'} @tab @tab Whether outlier markers are ## spread across the width of the box, @qcode{'off'} by default. ## @end multitable ## ## @seealso{shapley, shapley.plot, shapley.swarmchart, stats.chart.BoxChart} ## @end deftypefn function varargout = boxchart (varargin) [ax, this, args] = shapDrawArgs (varargin, 'boxchart'); optNames = {'NumImportantPredictors', 'ClassName', 'JitterOutliers'}; [nip, cn, jit, rem] = parsePairedArguments (optNames, {[], [], []}, ... args); if (! isempty (rem)) error (strcat ("shapley.boxchart: unknown optional argument or", ... " misplaced value.")); endif if (! isempty (jit)) jit = shapCheckWord (jit, {'on', 'off'}, 'boxchart', ... 'JitterOutliers'); endif [vals, names] = shapOneClass (this.Shapley, shapClassVars (this), ... cn, nip, 'boxchart'); n = rows (vals); nq = columns (vals); if (isempty (ax)) ax = gca (); endif grp = categorical (repmat (names, 1, nq)(:), names); args = {'Orientation', 'horizontal', 'BoxWidth', 0.8}; if (! isempty (jit)) args = [args, {'JitterOutliers', jit}]; endif b = boxchart (ax, grp, vals(:), args{:}); set (ax, 'ytick', 1:n, 'yticklabel', names); title (ax, 'Shapley Summary Plot'); xlabel (ax, 'Shapley Value'); ylabel (ax, 'Predictor'); if (nargout > 0) varargout{1} = b; endif endfunction ## -*- texinfo -*- ## @deftypefn {shapley} {} swarmchart (@var{obj}) ## @deftypefnx {shapley} {} swarmchart (@var{obj}, @var{name}, @var{value}) ## @deftypefnx {shapley} {} swarmchart (@var{ax}, @dots{}) ## @deftypefnx {shapley} {@var{s} =} swarmchart (@dots{}) ## ## Draw a swarm chart of the Shapley values over the query points. ## ## One row of points per predictor, the least important at the bottom, ## one point per query point spread vertically by how crowded its ## neighbourhood is. Each point is coloured by the value the predictor ## takes at that query point, the least of them at one end of the ## colour map and the greatest at the other. The chart is titled ## @qcode{'Shapley Summary Plot'}. ## ## @var{ax} is the axes to draw into, the current one where none is ## given. @var{s} holds one scatter object per predictor drawn. ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## ## @item @qcode{'NumImportantPredictors'} @tab @tab How many predictors ## to draw, the ten most important by default. ## ## @item @qcode{'ClassName'} @tab @tab The one class to draw, for a ## classification model. The default is the first class of the model. ## ## @item @qcode{'YJitter'} @tab @tab How the points of a row are spread, ## @qcode{'density'} by default, or @qcode{'rand'}, @qcode{'randn'} or ## @qcode{'none'}. ## ## @item @qcode{'ColorMap'} @tab @tab The colour map the predictor ## values are read through, as a name or as a matrix of one RGB triplet ## per row. The default is the one the axes already carries. ## @end multitable ## ## @seealso{shapley, shapley.plot, shapley.boxchart, swarmchart} ## @end deftypefn function varargout = swarmchart (varargin) [ax, this, args] = shapDrawArgs (varargin, 'swarmchart'); optNames = {'NumImportantPredictors', 'ClassName', 'YJitter', ... 'ColorMap'}; [nip, cn, yj, cmap, rem] = parsePairedArguments (optNames, ... {[], [], [], []}, args); if (! isempty (rem)) error (strcat ("shapley.swarmchart: unknown optional argument or", ... " misplaced value.")); endif if (isempty (yj)) yj = 'density'; else yj = shapCheckWord (yj, {'none', 'density', 'rand', 'randn'}, ... 'swarmchart', 'YJitter'); endif if (! isempty (cmap)) cmap = shapCheckMap (cmap, 'swarmchart'); endif [vals, names, ord] = shapOneClass (this.Shapley, ... shapClassVars (this), cn, nip, 'swarmchart'); n = rows (vals); nq = columns (vals); if (isempty (ax)) ax = gca (); endif held = ishold (ax); hold (ax, 'on'); h = zeros (n, 1); unwind_protect for k = 1:n col = shapColorValues (this.QueryPoints(:,ord(k))); at = k * ones (nq, 1); h(k) = swarmchart (ax, vals(k,:)', at, 36, col, ... 'XJitter', 'none', 'YJitter', yj); endfor unwind_protect_cleanup if (! held) hold (ax, 'off'); endif end_unwind_protect set (ax, 'ytick', 1:n, 'yticklabel', names, 'clim', [0, 1]); if (! isempty (cmap)) colormap (ax, cmap); endif title (ax, 'Shapley Summary Plot'); xlabel (ax, 'Shapley Value'); ylabel (ax, 'Predictor'); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {shapley} {} plotDependence (@var{obj}, @var{predictor}) ## @deftypefnx {shapley} {} plotDependence (@var{obj}, @var{predictor}, @var{name}, @var{value}) ## @deftypefnx {shapley} {} plotDependence (@var{ax}, @dots{}) ## @deftypefnx {shapley} {@var{p} =} plotDependence (@dots{}) ## ## Draw the Shapley values of one predictor against its own values. ## ## @var{predictor} names or indexes the predictor. Where it holds ## numbers the chart is a scatter of its value at each query point ## against the value it was given there; where it holds levels the chart ## is a box of the values at each level. ## ## The chart is titled @qcode{'Shapley Dependence Plot'} and its ## vertical axis is labelled after the predictor drawn. ## ## @var{ax} is the axes to draw into, the current one where none is ## given. @var{p} is the scatter object, or the ## @code{stats.chart.BoxChart}, drawn. ## ## @multitable @columnfractions 0.28 0.02 0.7 ## @headitem @var{Name} @tab @tab @var{Value} ## ## @item @qcode{'ClassName'} @tab @tab The one class to draw, for a ## classification model. The default is the first class of the model. ## ## @item @qcode{'ColorPredictor'} @tab @tab A second predictor to colour ## the points by, none by default. Its values are read as they stand, ## the range of the axes carrying the scale, and a colour bar is drawn ## beside the chart. It applies only where the predictor drawn holds ## numbers. ## ## @item @qcode{'ColorMap'} @tab @tab The colour map the colouring ## predictor is read through, as a name or as a matrix of one RGB ## triplet per row. The default is the one the axes already carries. ## @end multitable ## ## @seealso{shapley, shapley.plot, shapley.swarmchart} ## @end deftypefn function varargout = plotDependence (varargin) [ax, this, args] = shapDrawArgs (varargin, 'plotDependence'); if (isempty (args)) error ("shapley.plotDependence: too few input arguments."); endif pred = args{1}; optNames = {'ClassName', 'ColorPredictor', 'ColorMap'}; [cn, cp, cmap, rem] = parsePairedArguments (optNames, {[], [], []}, ... args(2:end)); if (! isempty (rem)) error (strcat ("shapley.plotDependence: unknown optional argument", ... " or misplaced value.")); endif if (! isempty (cmap)) cmap = shapCheckMap (cmap, 'plotDependence'); endif names = cellstr (this.Shapley.Predictor); j = shapPredictorIndex (names, pred, 'plotDependence', 'PREDICTOR'); cnames = shapClassVars (this); idx = shapCheckClasses (cnames, cn, 'plotDependence', 'ClassName', ... shapFirstClass (cnames)); if (numel (idx) > 1) error ("shapley.plotDependence: 'ClassName' must name one class."); endif V = shapDrawValues (this.Shapley, cnames, idx, ... 1:rows (this.QueryPoints)); y = V(j,:)'; x = this.QueryPoints(:,j); lvl = any (this.CategoricalPredictors == j); if (isempty (ax)) ax = gca (); endif if (lvl) if (! isempty (cp)) error (strcat ("shapley.plotDependence: 'ColorPredictor' does", ... " not apply to a predictor holding levels.")); endif p = boxchart (ax, x, y); elseif (isempty (cp)) p = scatter (ax, x, y, 36); else k = shapPredictorIndex (names, cp, 'plotDependence', ... 'ColorPredictor'); ## The colouring predictor is read as it stands here, the range of ## the axes carrying the scale, where a swarm normalizes instead col = this.QueryPoints(:,k); p = scatter (ax, x, y, 36, col); lo = min (col); hi = max (col); if (hi > lo) set (ax, 'clim', [lo, hi]); endif if (! isempty (cmap)) colormap (ax, cmap); endif colorbar (ax); endif title (ax, 'Shapley Dependence Plot'); xlabel (ax, names{j}); ylabel (ax, sprintf ('Shapley Values for %s', names{j})); if (nargout > 0) varargout{1} = p; endif endfunction endmethods methods (Access = private) ## The variables the Shapley table holds one per class, empty for a ## regression model or a function handle, which hold one value only. function cnames = shapClassVars (this) cnames = {}; if (this.IsClass) cnames = shapClassText (this.ClassNames); endif endfunction endmethods endclassdef ## A table as the numbers the explainer works over. A fitted model names ## the predictors and holds the levels they were coded through; a function ## handle has neither, so the table itself names them. function [X, tnames, errmsg] = shapReadTable (blackbox, isfh, T, caller) X = []; tnames = {}; errmsg = ''; if (isfh) [X, ~, args, ~, errmsg] = tableFrame (T, [], {}); if (isempty (errmsg)) tnames = args{2}; endif return; endif props = properties (blackbox); if (! any (strcmp (props, 'PredictorNames'))) errmsg = strcat ("X may be a table only for a model that names its", ... " predictors."); return; endif ## PredictorLevels is hidden, so properties () does not list it; the ## class that defines it is what says whether it is there lev = {}; if (isa (blackbox, 'PredictiveModel')) lev = blackbox.PredictorLevels; endif try X = tableToMatrix (blackbox.PredictorNames, lev, T, caller); catch err errmsg = regexprep (err.message, '^shapley: ', ''); return; end_try_catch endfunction ## The levels a model's predictors were coded through, where it kept any. function lev = shapLevels (blackbox) lev = {}; if (isa (blackbox, 'PredictiveModel')) lev = blackbox.PredictorLevels; endif endfunction ## The algorithm asked for, of those implemented. function [base, errmsg] = shapMethod (Method) base = 'interventional'; errmsg = ''; if (isempty (Method)) return; endif if (! (ischar (Method) || isa (Method, 'string')) || ! isrow (char (Method))) errmsg = "'Method' must be a character vector or a string scalar."; return; endif switch (lower (char (Method))) case 'interventional' base = 'interventional'; case 'conditional' base = 'conditional'; otherwise errmsg = strcat ("'Method' must be 'interventional' or", ... " 'conditional'."); endswitch endfunction ## Which algorithm answers for this model. A model whose prediction is a ## weighted sum of its predictors is answered outright, and everything else ## through the subsets. Asking for a budget of subsets asks for the subsets. function method = shapAlgorithm (base, F, MaxSub) if (strcmp (base, 'conditional')) method = 'conditional-kernel'; return; endif method = 'interventional-kernel'; if (! (F.IsLinear || F.IsTree)) return; endif if (! isempty (MaxSub)) warning (strcat ("shapley: 'MaxNumSubsets' is given, so the values", ... " are taken over subsets rather than from the", ... " structure of the model.")); return; endif if (F.IsLinear) method = 'interventional-linear'; return; endif method = 'interventional-tree'; endfunction ## Resolve the observations, the predictors and the classes. function [F, errmsg] = shapFrame (blackbox, Data, CatPred, NumObs, MaxSub) F = []; errmsg = ''; isfh = is_function_handle (blackbox); if (isfh) props = {}; else props = properties (blackbox); endif has = @(n) any (strcmp (props, n)); ## The observations averaged over, from the model where it kept them if (isempty (Data)) if (isfh) errmsg = "X is required when the model is a function handle."; return; endif if (! (has ('X') && ! isempty (blackbox.X))) errmsg = strcat ("X is required for a model that does not keep the", ... " observations it was fitted on."); return; endif Data = blackbox.X; endif ## A table is read by the names the model was fitted on, so a column it ## was not fitted on is passed over and a level carries the code it ## carried then. A handle has no model behind it, so its table names its ## own predictors and every column is one. tnames = {}; if (istable (Data)) [Data, tnames, errmsg] = shapReadTable (blackbox, isfh, Data, 'shapley'); if (! isempty (errmsg)) return; endif endif if (! (isnumeric (Data) && isreal (Data) && ismatrix (Data) && ndims (Data) == 2 && ! isempty (Data))) errmsg = "X must be a real numeric matrix."; return; endif p = columns (Data); ## The predictor names, from the model where it has them if (has ('PredictorNames') && ! isempty (blackbox.PredictorNames)) pnames = blackbox.PredictorNames(:)'; if (numel (pnames) != p) errmsg = "X must have one column per predictor of the model."; return; endif elseif (! isempty (tnames)) pnames = tnames; else pnames = arrayfun (@(k) sprintf ('x%d', k), 1:p, 'UniformOutput', false); endif ## The categorical predictors: the model's own, or those named for a handle if (isfh) spec = CatPred; else if (has ('CategoricalPredictors')) spec = blackbox.CategoricalPredictors; else spec = []; endif if (! isempty (CatPred) && ! isequal (CatPred, spec)) errmsg = strcat ("'CategoricalPredictors' must match the", ... " CategoricalPredictors property of the model."); return; endif endif cat = []; if (! isempty (spec)) [C, msg] = dummyCoding (Data, spec, pnames); if (! isempty (msg)) errmsg = msg; return; endif cat = C.Index; endif ## How many subsets to compute over. Every one of them gives the values ## exactly, and MATLAB stops at 1024 of them by default. full = 2 ^ p; if (isempty (MaxSub)) nsub = min (full, 1024); elseif (isnumeric (MaxSub) && isscalar (MaxSub) && isreal (MaxSub) && MaxSub > 1 && MaxSub == fix (MaxSub)) nsub = min (full, double (MaxSub)); else errmsg = "'MaxNumSubsets' must be an integer greater than 1."; return; endif ## The observations drawn from those given n = rows (Data); if (isempty (NumObs)) k = min (100, n); elseif ((ischar (NumObs) || isa (NumObs, 'string')) && strcmpi (char (NumObs), 'all')) k = n; elseif (isnumeric (NumObs) && isscalar (NumObs) && isreal (NumObs) && NumObs >= 1 && NumObs == fix (NumObs)) k = min (double (NumObs), n); else errmsg = strcat ("'NumObservationsToSample' must be a positive", ... " integer or 'all'."); return; endif if (k < n) idx = sort (randperm (n, k))(:); else idx = (1:n)(:); endif ## Assigned field by field: struct () spreads a cell argument into a struct ## array, and a cellstr ClassNames would make one F.IsClass = has ('ClassNames') && ! isempty (blackbox.ClassNames); if (F.IsClass) F.ClassNames = blackbox.ClassNames; else F.ClassNames = []; endif F.PredictorNames = pnames; F.NumPredictors = p; F.NumSubsets = nsub; F.IsLinear = shapIsLinear (blackbox, isfh, has); F.NumTrees = numel (shapTreesOf (blackbox)); F.IsTree = (F.NumTrees > 0); F.Cat = cat; F.X = Data; F.Idx = idx; endfunction ## The decision trees a model answers through, and what each answers at each ## of its nodes, already carrying whatever weight the model gives it. Empty ## where the model does not come apart into trees, which is what sends it to ## the subsets instead. ## ## A single tree answers with its own leaves. An ensemble answers with the ## weighted sum of its learners' outputs, so it comes apart too, provided ## every learner is a tree, the learners are combined by a constant weight ## each, and nothing is applied to the total afterwards. function [trees, leafvals] = shapTreesOf (blackbox) trees = {}; leafvals = {}; if (is_function_handle (blackbox)) return; endif switch (class (blackbox)) case {'ClassificationTree', 'CompactClassificationTree', ... 'RegressionTree', 'CompactRegressionTree'} trees = {blackbox}; leafvals = {shapLeafValues(blackbox)}; case {'ClassificationEnsemble', 'CompactClassificationEnsemble', ... 'ClassificationBaggedEnsemble', 'RegressionEnsemble', ... 'CompactRegressionEnsemble', 'RegressionBaggedEnsemble'} [trees, leafvals] = shapEnsembleTrees (blackbox); endswitch endfunction ## What one tree answers at each of its nodes, transformed as it answers. function L = shapLeafValues (T) props = properties (T); if (any (strcmp (props, 'ClassProbability'))) L = T.ClassProbability; if (! isempty (T.STfun)) L = T.STfun (L); endif else L = T.NodeMean(:); if (! isempty (T.RTfun)) L = T.RTfun (L); endif endif endfunction ## The learners of an ensemble and what each contributes to its answer. ## ## A regression ensemble adds up what its learners predict, whatever method ## grew them. A classification ensemble adds up their class scores only for ## the methods that read a learner that way; the boosting methods that read a ## learner's label, or its margin, contribute something that is not the ## learner's own scores, and those are left to the subsets. Anything applied ## to the total, a transform of the score or of the response, would not come ## apart and is left to the subsets too. function [trees, leafvals] = shapEnsembleTrees (Mdl) trees = {}; leafvals = {}; props = properties (Mdl); has = @(n) any (strcmp (props, n)); if (! has ('Trained') || isempty (Mdl.Trained)) return; endif isclass = has ('ClassNames') && ! isempty (Mdl.ClassNames); if (isclass) trans = Mdl.ScoreTransform; ## The methods whose learner output is the learner's own class scores if (! any (strcmp (Mdl.Method, {'Bag', 'AdaBoostM2', 'RUSBoost'}))) return; endif else trans = Mdl.ResponseTransform; endif if (! (ischar (trans) && any (strcmpi (trans, {'none', 'identity'})))) return; endif learners = Mdl.Trained(:)'; T = numel (learners); istree = cellfun (@(t) any (strcmp (properties (t), 'Children')), learners); if (! all (istree)) return; endif w = Mdl.TrainedWeights(:)'; if (isempty (w)) w = ones (1, T); endif if (strcmp (Mdl.CombineWeights, 'WeightedAverage')) tot = sum (w); if (! (tot > 0)) return; endif w = w / tot; endif if (isclass) K = numel (Mdl.ClassNames); endif leafvals = cell (1, T); for t = 1:T L = shapLeafValues (learners{t}); if (isclass) ## A learner need not know every class of the ensemble, so its columns ## are put where the ensemble keeps them col = labelIndices (Mdl.ClassNames, learners{t}.ClassNames); if (any (col == 0)) trees = {}; leafvals = {}; return; endif G = zeros (rows (L), K); G(:,col) = L; L = G; endif leafvals{t} = w(t) * L; endfor trees = learners; endfunction ## The trees as the compiled kernel takes them: plain node tables, with the ## levels of a categorical cut carried beside the thresholds. function C = shapTreeStructs (trees, leafvals) C = cell (1, numel (trees)); for ti = 1:numel (trees) T = trees{ti}; st = struct (); st.Children = double (T.Children); st.CutVar = double (T.CutPredictorIndex(:)); st.CutPoint = double (T.CutPoint(:)); st.Leaf = double (leafvals{ti}); if (! isempty (T.CutCategories)) st.CatLeft = T.CutCategories(:,1); st.CatRight = T.CutCategories(:,2); else st.CatLeft = {}; st.CatRight = {}; endif C{ti} = st; endfor endfunction ## Whether the model predicts a weighted sum of its predictors. Beta holds ## the weights of the linear learners and of a support vector machine with a ## linear kernel, and is empty for every other kernel, so its presence is the ## question. A transform of the score breaks the sum, and a model carrying ## one is answered through the subsets instead. function tf = shapIsLinear (blackbox, isfh, has) tf = false; if (isfh || ! has ('Beta') || isempty (blackbox.Beta)) return; endif if (has ('ScoreTransform')) trans = blackbox.ScoreTransform; elseif (has ('ResponseTransform')) trans = blackbox.ResponseTransform; else trans = 'none'; endif tf = (ischar (trans) && any (strcmpi (trans, {'none', 'identity'}))); endfunction ## What the model answers for a set of observations, as a column per class. function sfcn = shapScoreFcn (blackbox, isclass) if (is_function_handle (blackbox)) sfcn = @(Z) shapHandleScore (blackbox, Z); elseif (isclass) sfcn = @(Z) shapClassScore (blackbox, Z); else sfcn = @(Z) shapResponse (blackbox, Z); endif endfunction function s = shapHandleScore (fun, Z) s = fun (Z); if (! (isnumeric (s) && isreal (s) && columns (s) == 1 && rows (s) == rows (Z))) error (strcat ("shapley: the function must answer with one real", ... " numeric column holding one value per observation.")); endif s = double (s); endfunction function s = shapClassScore (Mdl, Z) [~, s] = predict (Mdl, Z); s = double (s); endfunction function s = shapResponse (Mdl, Z) s = double (predict (Mdl, Z))(:); endfunction ## Every subset of the predictors, as one logical row each. Bit II of the row ## index, counted from zero, says whether predictor II is in the subset, which ## is the order shapFromValues reads them in. function Z = shapAllMasks (M) nS = 2 ^ M; Z = false (nS, M); for ii = 1:M Z(:,ii) = logical (bitget ((0:(nS - 1))', ii)); endfor endfunction ## The subsets a weighted sum of the predictors is answered from: the empty ## set, the full one, and each predictor on its own. Holding one predictor ## at the query point moves such a prediction by that predictor's whole ## contribution and by nothing else, so the difference from the average is ## the Shapley value itself and the other subsets say nothing new. function Z = shapLinearMasks (M) none = false (1, M); every = true (1, M); singles = logical (eye (M)); Z = [none; every; singles]; endfunction ## Whether a shortcut's values account for the whole deviation of the ## prediction from the average, which is what says the shortcut applies. function tf = shapAccounts (parts, sfcn, Xs, q) dev = sfcn (q) - mean (sfcn (Xs), 1); tf = all (abs (parts - dev) <= 1e-8 * max (1, max (abs (dev)))); endfunction ## The subsets to compute over when there is no room for all of them: the ## empty set and the full one, which carry the average prediction and the ## prediction itself, and then as many as the budget allows in decreasing ## order of their kernel weight. That weight depends only on how many ## predictors a subset holds and falls as it moves away from the two ## extremes, so the cardinalities are taken in the pairs (1, M-1), (2, M-2) ## and so on, the smaller one lexicographically and the larger one in the ## order of the complements. ## ## The single predictors are taken as far as the budget reaches whatever is ## left of it. A later pair is taken entire while it fits, and the first ## that does not is filled with complementary pairs of subsets instead, so ## that what is left of the budget still spans both ends rather than sitting ## at one of them. Measured against MATLAB R2024a at seven budgets for four ## predictors and ten for five. function Z = shapSelectMasks (M, nSub) none = false (1, M); every = true (1, M); Z = [none; every]; budget = nSub - 2; if (budget < 1 || M < 2) return; endif sel = zeros (budget, M); nr = 0; lo = 1; hi = M - 1; while (lo <= hi && nr < budget) left = budget - nr; nlo = shapLevelCount (M, lo, left); if (lo == hi) ## The middle cardinality is its own complement [sel, nr] = shapFillPairs (sel, nr, budget, M, lo, true); elseif (lo == 1 || 2 * nlo <= left) C = shapCombinations (M, lo, left); for ii = 1:rows (C) if (nr >= budget) break; endif nr++; sel(nr,:) = C(ii,:); endfor for ii = 1:rows (C) if (nr >= budget) break; endif nr++; sel(nr,:) = ! C(ii,:); endfor else [sel, nr] = shapFillPairs (sel, nr, budget, M, lo, false); endif lo++; hi--; endwhile taken = logical (sel(1:nr,:)); Z = [Z; taken]; endfunction ## Fill what is left of the budget with complementary pairs of subsets of S ## predictors, each subset followed by the one holding the rest. SELF says ## the complement has S predictors too, so a pair can be reached twice and the ## second reading of it is passed over. function [sel, nr] = shapFillPairs (sel, nr, budget, M, s, self) C = shapCombinations (M, s, budget); used = false (rows (C), 1); for ii = 1:rows (C) if (nr >= budget) break; endif if (self && used(ii)) continue; endif used(ii) = true; nr++; sel(nr,:) = C(ii,:); comp = ! C(ii,:); if (nr >= budget) break; endif if (self) jj = find (all (C == repmat (comp, rows (C), 1), 2), 1); if (isempty (jj) || used(jj)) continue; endif used(jj) = true; endif nr++; sel(nr,:) = comp; endfor endfunction ## How many subsets of S predictors there are out of M, counted up to CAP so ## that a wide model does not overflow the binomial coefficient it never ## needs the whole of. function n = shapLevelCount (M, s, cap) n = 1; for ii = 1:s n = n * (M - s + ii) / ii; if (n > cap) n = cap + 1; return; endif endfor n = round (n); endfunction ## The first N combinations of S indices out of M, in lexicographic order, as ## one logical row each. They are generated rather than enumerated, so a wide ## model does not build a list it has no room for. function C = shapCombinations (M, s, n) C = zeros (n, M); nr = 0; c = 1:s; while (nr < n) nr++; C(nr,c) = 1; jj = s; while (jj >= 1 && c(jj) == M - s + jj) jj--; endwhile if (jj < 1) break; endif c(jj)++; c((jj + 1):s) = c(jj) + (1:(s - jj)); endwhile C = C(1:nr,:); endfunction ## The value function over the given subsets. A subset's columns are held at ## the query point and the rest stay as the observations have them, which is ## what makes the average an interventional one. function V = shapSubsetValues (sfcn, Xs, q, Z, K, cnd) L = rows (Z); V = zeros (L, K); for ii = 1:L mask = Z(ii,:); if (isempty (cnd) || ! any (mask) || all (mask)) ## Conditioning on nothing, or on everything, leaves the whole sample: ## the first averages over it and the second replaces all of it W = Xs; else W = Xs(shapNeighbours (cnd, mask),:); endif if (any (mask)) W(:,mask) = repmat (q(mask), rows (W), 1); endif V(ii,:) = mean (sfcn (W), 1); endfor endfunction ## What conditioning needs, measured once over the observations: the column ## means and deviations the distances are taken in, so that a predictor does ## not count for more merely because it is recorded on a wider scale, and how ## many neighbours stand in for the conditional distribution. function cnd = shapConditioning (Xs, method) cnd = []; if (! strcmp (method, 'conditional-kernel')) return; endif n = rows (Xs); cnd.Mu = mean (Xs, 1); sd = std (Xs, 0, 1); sd(sd == 0 | ! isfinite (sd)) = 1; cnd.Sigma = sd; cnd.Xz = (Xs - repmat (cnd.Mu, n, 1)) ./ repmat (sd, n, 1); ## A tenth of the observations, rounded up. Divided rather than multiplied ## by a tenth, which is not exact in binary and would round a whole tenth up cnd.NumNeighbors = max (1, ceil (n / 10)); cnd.qz = []; endfunction ## The observations whose values of the predictors in MASK lie nearest the ## query point, which are the ones conditioning on those predictors keeps. function nb = shapNeighbours (cnd, mask) n = rows (cnd.Xz); D = cnd.Xz(:,mask) - repmat (cnd.qz(mask), n, 1); ## Distances equal to within rounding tie, and sort keeps the lower row d = sum (D .^ 2, 2); s = max (d); if (s > 0) d = round (d / s * 1e12); endif [~, ord] = sort (d); nb = ord(1:cnd.NumNeighbors); endfunction ## The Shapley values estimated from a budget of subsets, by the weighted ## least squares kernel SHAP solves. Row 1 of Z is the empty subset and row 2 ## the full one, so the values are tied to sum to the deviation of the ## prediction from the average; that is imposed by substitution rather than by ## a penalty, so it holds exactly however few subsets there are. function phi = shapKernelSolve (Z, V, M, K) v0 = V(1,:); c = V(2,:) - v0; Zi = Z(3:end,:); L = rows (Zi); if (L == 0) ## Nothing but the two extremes says nothing about any one predictor phi = NaN (M, K); return; endif if (M == 1) phi = c; return; endif ## The Shapley kernel, in logs so that a wide model does not overflow the ## binomial coefficient, and only up to a constant since it is a weight s = sum (Zi, 2); lchoose = gammaln (M + 1) - gammaln (s + 1) - gammaln (M - s + 1); lw = log (M - 1) - lchoose - log (s) - log (M - s); w = exp (lw - max (lw)); ## Substituting the last value out of the sum imposes the constraint last = Zi(:,M); A = Zi(:,1:(M - 1)) - repmat (last, 1, M - 1); b = (V(3:end,:) - repmat (v0, L, 1)) - last * c; sw = sqrt (w); ## A budget too small to determine every value leaves the system rank ## deficient, which is the case the caller has already warned about warning ("off", "Octave:singular-matrix", "local"); warning ("off", "Octave:rank-deficient-matrix", "local"); psi = (repmat (sw, 1, M - 1) .* A) \ (repmat (sw, 1, K) .* b); lastPhi = c - sum (psi, 1); phi = [psi; lastPhi]; endfunction ## The Shapley value of each predictor, summed over every subset that leaves ## it out. The weight of a subset of S predictors is 1 / (M * C(M-1, S)), ## which is the S! (M-S-1)! / M! of the definition written so that it does not ## overflow. function phi = shapFromValues (V, M, K) w = zeros (1, M); for s = 0:(M - 1) w(s + 1) = 1 / (M * nchoosek (M - 1, s)); endfor phi = zeros (M, K); nS = 2 ^ M; for ii = 1:M bit = 2 ^ (ii - 1); for m = 0:(nS - 1) if (bitand (m, bit) == 0) s = sum (bitget (m, 1:M)); phi(ii,:) += w(s + 1) * (V(m + bit + 1,:) - V(m + 1,:)); endif endfor endfor endfunction ## What the model answers at the query points. function fitted = shapFitted (blackbox, Q, isclass) if (is_function_handle (blackbox)) fitted = shapHandleScore (blackbox, Q); elseif (isclass) fitted = predict (blackbox, Q); else fitted = double (predict (blackbox, Q))(:); endif endfunction ## The values as a table of one row per predictor, holding one column per ## query point, or their mean absolute value where MEANABS is true. function T = shapTable (phi, pnames, cn, isclass, meanabs) M = rows (phi); K = columns (phi); if (meanabs) phi = mean (abs (phi), 3); endif P = string (pnames(:)); if (! isclass) T = table (P, reshape (phi(:,1,:), M, []), ... 'VariableNames', {'Predictor', 'Value'}); return; endif names = shapClassText (cn); cols = cell (1, K + 1); cols{1} = P; for k = 1:K cols{k + 1} = reshape (phi(:,k,:), M, []); endfor T = table (cols{:}, 'VariableNames', [{'Predictor'}, names]); endfunction ## The class names as the text a table variable is named with. function names = shapClassText (cn) if (iscellstr (cn)) names = cn(:)'; elseif (ischar (cn)) names = cellstr (cn)(:)'; elseif (isa (cn, 'string') || isa (cn, 'categorical')) names = cellstr (cn)(:)'; elseif (islogical (cn)) names = arrayfun (@(v) mat2str (v), cn(:)', 'UniformOutput', false); else names = arrayfun (@(v) sprintf ('%g', v), cn(:)', 'UniformOutput', false); endif endfunction ## The axes and the explainer, in whichever order the call put them. A ## method reached with an axes first has the explainer second, dispatch ## having found it wherever it sits. function [ax, this, args] = shapDrawArgs (in, caller) ax = []; if (numel (in) > 1 && isscalar (in{1}) && ishghandle (in{1}) && isaxes (in{1})) ax = in{1}; in(1) = []; endif this = in{1}; args = in(2:end); if (isempty (this.Shapley)) error (strcat ("shapley.%s: the Shapley values are not fitted; use", ... " fit to compute them."), caller); endif endfunction ## The values of the classes asked for at the query points asked for, as one ## page per class. A regression model and a function handle hold one value ## per predictor and give one page. function V = shapDrawValues (T, cnames, idx, qpi) if (isempty (cnames)) V = T.Value(:,qpi); return; endif M = size (T, 1); V = zeros (M, numel (qpi), numel (idx)); for k = 1:numel (idx) A = T.(cnames{idx(k)}); V(:,:,k) = A(:,qpi); endfor endfunction ## The classes drawn where none was named: the predicted class over one ## query point and every one of them over several, as MATLAB chooses them. function idx = shapDefaultClasses (cnames, fitted, qpi) idx = []; if (isempty (cnames)) return; endif if (numel (qpi) != 1) idx = 1:numel (cnames); return; endif at = shapClassText (fitted(qpi)); idx = find (strcmp (cnames, at{1}), 1); if (isempty (idx)) idx = 1; endif endfunction ## The class a chart of one class draws where none was named. function idx = shapFirstClass (cnames) idx = []; if (! isempty (cnames)) idx = 1; endif endfunction ## The classes named, as indices into the model's own order. function idx = shapCheckClasses (cnames, want, caller, optname, dflt) if (isempty (want)) idx = dflt; return; endif if (isempty (cnames)) error (strcat ("shapley.%s: '%s' is valid only for a classification", ... " model."), caller, optname); endif want = shapClassText (want); idx = zeros (1, numel (want)); for k = 1:numel (want) j = find (strcmp (cnames, want{k}), 1); if (isempty (j)) error ("shapley.%s: '%s' does not name a class of the model.", ... caller, want{k}); endif idx(k) = j; endfor endfunction ## The query points to draw, all of them where none were named. function qpi = shapCheckPoints (qpi, nq, caller) if (isempty (qpi)) qpi = 1:nq; return; endif if (! (isnumeric (qpi) && isreal (qpi) && isvector (qpi) && all (isfinite (qpi)) && all (qpi == fix (qpi)) && all (qpi >= 1))) error (strcat ("shapley.%s: 'QueryPointIndices' must be positive", ... " integers."), caller); endif if (any (qpi > nq)) error (strcat ("shapley.%s: 'QueryPointIndices' must not exceed the", ... " %d query points fitted."), caller, nq); endif qpi = double (qpi(:)'); endfunction ## How many predictors to draw on their own, ten of them by default, and ## never more than the model has. function nip = shapCheckCount (nip, M, caller) if (isempty (nip)) nip = min (M, 10); return; endif if (! (isnumeric (nip) && isscalar (nip) && isreal (nip) && isfinite (nip) && nip == fix (nip) && nip > 0)) error (strcat ("shapley.%s: 'NumImportantPredictors' must be a", ... " positive integer."), caller); endif nip = min (double (nip), M); endfunction ## The values of one class at every query point, the predictors ordered as ## the charts of one class order them and cut to the ones drawn. function [vals, names, ord] = shapOneClass (T, cnames, cn, nip, caller) idx = shapCheckClasses (cnames, cn, caller, 'ClassName', ... shapFirstClass (cnames)); if (numel (idx) > 1) error ("shapley.%s: 'ClassName' must name one class.", caller); endif if (isempty (cnames)) nq = columns (T.Value); else nq = columns (T.(cnames{1})); endif V = shapDrawValues (T, cnames, idx, 1:nq); M = size (T, 1); nip = shapCheckCount (nip, M, caller); [~, ord] = sort (mean (abs (V), 2), 'ascend'); ord = ord((M - nip + 1):M); names = cellstr (T.Predictor); names = names(ord); vals = V(ord,:); endfunction ## A predictor's values as the share of its range each one stands at, which ## is what a colour map is read with. A predictor that never changes sits ## in the middle of the map, having no range to spread over. function c = shapColorValues (v) v = double (v(:)); lo = min (v); hi = max (v); if (hi > lo) c = (v - lo) / (hi - lo); else c = 0.5 * ones (size (v)); endif endfunction ## One of a short list of words, named by the method that took it rather ## than by whatever the drawing is handed on to. function v = shapCheckWord (val, allowed, caller, optname) if (! (ischar (val) && isrow (val) && any (strcmpi (allowed, val)))) error ("shapley.%s: '%s' must be one of %s.", caller, optname, ... strjoin (strcat ("'", allowed, "'"), ', ')); endif j = find (strcmpi (allowed, val), 1); v = allowed{j}; endfunction ## A colour map, as a name or as one RGB triplet per row. function v = shapCheckMap (val, caller) if (ischar (val) && isrow (val)) v = val; return; endif if (isnumeric (val) && isreal (val) && ismatrix (val) && columns (val) == 3 && rows (val) > 0 && all (val(:) >= 0) && all (val(:) <= 1)) v = double (val); return; endif error (strcat ("shapley.%s: 'ColorMap' must be a colour map name or a", ... " matrix of RGB triplets."), caller); endfunction ## The one predictor named or indexed. function j = shapPredictorIndex (names, pred, caller, argname) if (isnumeric (pred) && isscalar (pred) && isreal (pred) && isfinite (pred) && pred == fix (pred) && pred >= 1 && pred <= numel (names)) j = double (pred); return; endif if (ischar (pred) && isrow (pred)) pred = {pred}; elseif (isa (pred, 'string') && isscalar (pred)) pred = cellstr (pred); endif if (iscellstr (pred) && isscalar (pred)) j = find (strcmp (names, pred{1}), 1); if (! isempty (j)) return; endif endif error ("shapley.%s: %s must name or index one predictor of the model.", ... caller, argname); endfunction %!demo %! ## Explain a model fitted from a table %! %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! T.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! Mdl = fitrtree (T, 'SL'); %! %! ## The observations and the query points may be tables too, read by the %! ## names the model was fitted on rather than by the order of the columns %! s = shapley (Mdl, T(:, [1, 2, 3, 5]), 'QueryPoints', T(1, [5, 3, 2, 1]), ... %! 'NumObservationsToSample', 'all'); %! s.Shapley %! %! ## A column the model was not fitted on is passed over, so the whole %! ## table, response and all, gives the same explanation %! t = shapley (Mdl, T, 'QueryPoints', T(1,:), ... %! 'NumObservationsToSample', 'all'); %! isequal (t.Shapley.Value, s.Shapley.Value) ## A linear function's exact Shapley value is the coefficient times the ## deviation of the predictor from its mean, which is an expectation the ## engine cannot influence. %!test %! X = [1, 10; 2, 20; 3, 30; 4, 45]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2); %! s = shapley (f, X, 'QueryPoints', [3, 20], ... %! 'NumObservationsToSample', 'all'); %! b = [2, -3]; %! q = [3, 20]; %! assert_equal (s.Shapley.Value, (b .* (q - mean (X)))', 1e-12); %!test # the values sum to the deviation of the prediction from the average %! X = [1, 10; 2, 20; 3, 30; 4, 45]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2); %! s = shapley (f, X, 'QueryPoints', [3, 20], ... %! 'NumObservationsToSample', 'all'); %! assert_equal (sum (s.Shapley.Value), f ([3, 20]) - s.Intercept, 1e-12); %!test # the intercept is the average prediction over the observations %! X = [1, 10; 2, 20; 3, 30; 4, 45]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2); %! s = shapley (f, X, 'QueryPoints', [3, 20], ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.Intercept, mean (f (X)), 1e-12); %!test # every subset is used, so the count is two to the predictors %! X = [1, 10; 2, 20; 3, 30; 4, 45]; %! f = @(Z) Z(:,1); %! s = shapley (f, X); %! assert_equal (s.NumSubsets, 4); %!test # a predictor the function ignores contributes nothing %! X = [1, 10; 2, 20; 3, 30; 4, 45]; %! f = @(Z) Z(:,1); %! s = shapley (f, X, 'QueryPoints', [3, 20], ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.Shapley.Value(2), 0, 1e-12); %!test # the algorithm is reported %! X = [1, 10; 2, 20; 3, 30; 4, 45]; %! s = shapley (@(Z) Z(:,1), X); %! assert_equal (s.Method, 'interventional-kernel'); %!test # a handle names its predictors as MATLAB does %! X = [1, 10; 2, 20; 3, 30; 4, 45]; %! s = shapley (@(Z) Z(:,1), X, 'QueryPoints', [3, 20]); %! assert_equal (cellstr (s.Shapley.Predictor), {'x1'; 'x2'}); %!test # the table names its one value variable as MATLAB R2026a does %! X = [1, 10; 2, 20; 3, 30; 4, 45]; %! s = shapley (@(Z) Z(:,1), X, 'QueryPoints', [3, 20]); %! assert_equal (s.Shapley.Properties.VariableNames, {'Predictor', 'Value'}); %!test # one column of values per query point %! X = [1, 10; 2, 20; 3, 30; 4, 45]; %! s = shapley (@(Z) Z(:,1), X, 'QueryPoints', [3, 20; 2, 30], ... %! 'NumObservationsToSample', 'all'); %! assert_equal (size (s.Shapley.Value), [2, 2]); %!test # with one query point the mean absolute value is the absolute value %! X = [1, 10; 2, 20; 3, 30; 4, 45]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2); %! s = shapley (f, X, 'QueryPoints', [3, 20], ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.MeanAbsoluteShapley.Value, abs (s.Shapley.Value), 1e-12); %!test # fit replaces the query points rather than adding to them %! X = [1, 10; 2, 20; 3, 30; 4, 45]; %! s = shapley (@(Z) Z(:,1), X, 'QueryPoints', [3, 20; 2, 30]); %! s = fit (s, [1, 10]); %! assert_equal (s.QueryPoints, [1, 10]); %! assert_equal (size (s.Shapley.Value), [2, 1]); %!test # 'all' averages over every row, and says which ones %! X = [1, 10; 2, 20; 3, 30; 4, 45]; %! s = shapley (@(Z) Z(:,1), X, 'NumObservationsToSample', 'all'); %! assert_equal (s.SampledObservationIndices, (1:4)'); %!test # a sample of the observations is drawn without replacement %! X = (1:200)'; %! X = [X, 2 * X]; %! s = shapley (@(Z) Z(:,1), X); %! assert_equal (numel (s.SampledObservationIndices), 100); %! assert_equal (numel (unique (s.SampledObservationIndices)), 100); %!test # a handle answers what it was asked at the query points %! X = [1, 10; 2, 20; 3, 30; 4, 45]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2); %! s = shapley (f, X, 'QueryPoints', [3, 20]); %! assert_equal (s.BlackboxFitted, f ([3, 20]), 1e-12); %!test # a model supplies the observations it was fitted on %! load fisheriris %! Mdl = fitrtree (meas(:,2:4), meas(:,1)); %! s = shapley (Mdl, 'NumObservationsToSample', 'all'); %! assert_equal (size (s.X), [150, 3]); %!test # and its own predictor names %! load fisheriris %! Mdl = fitrtree (meas(:,2:4), meas(:,1), ... %! 'PredictorNames', {'a', 'b', 'c'}); %! s = shapley (Mdl, 'QueryPoints', meas(1,2:4), ... %! 'NumObservationsToSample', 'all'); %! assert_equal (cellstr (s.Shapley.Predictor), {'a'; 'b'; 'c'}); %!test # a classifier names one variable per class %! load fisheriris %! Mdl = fitctree (meas, species); %! s = shapley (Mdl, 'QueryPoints', meas(1,:), ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.Shapley.Properties.VariableNames, ... %! {'Predictor', 'setosa', 'versicolor', 'virginica'}); %!test # the two classes of a binary classifier are exact negatives %! load fisheriris %! Mdl = fitctree (meas(51:150,:), species(51:150)); %! s = shapley (Mdl, 'QueryPoints', meas(51,:), ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.Shapley.versicolor, -s.Shapley.virginica, 1e-12); %!test # a classifier's values sum to the deviation of its scores %! load fisheriris %! Mdl = fitctree (meas, species); %! s = shapley (Mdl, 'QueryPoints', meas(1,:), ... %! 'NumObservationsToSample', 'all'); %! [~, sc] = predict (Mdl, meas(1,:)); %! v = [s.Shapley.setosa, s.Shapley.versicolor, s.Shapley.virginica]; %! assert_equal (sum (v, 1), sc - s.Intercept, 1e-12); ## MATLAB parity: the values of a classification tree, measured on R2024a and ## confirmed on R2026a with every observation averaged over %!test %! load fisheriris %! Mdl = fitctree (meas, species); %! s = shapley (Mdl, 'QueryPoints', meas(1,:), ... %! 'NumObservationsToSample', 'all'); %! v = [s.Shapley.setosa, s.Shapley.versicolor, s.Shapley.virginica]; %! assert_equal (v, [0, 0, 0; 0, 0, 0; ... %! 0.666666666666667, -0.397777777777778, ... %! -0.268888888888889; ... %! 0, 0.0644444444444445, -0.0644444444444444], 1e-12); ## MATLAB parity: a nearest-neighbour classifier, which is the model MATLAB ## answers for with the kernel algorithm this computes with %!test %! load fisheriris %! Mdl = fitcknn (meas, species); %! s = shapley (Mdl, 'QueryPoints', meas(1,:), ... %! 'NumObservationsToSample', 'all'); %! v = [s.Shapley.setosa, s.Shapley.versicolor, s.Shapley.virginica]; %! assert_equal (v, [0, -0.000555555555555559, 0.000555555555555538; ... %! 0.00111111111111099, 0.0283333333333334, ... %! -0.0294444444444444; ... %! 0.664444444444445, -0.433888888888889, ... %! -0.230555555555556; ... %! 0.00111111111111106, 0.0727777777777778, ... %! -0.0738888888888889], 1e-12); ## The budget is capped at every subset, and a model wider than ten ## predictors takes the default of 1024 rather than being refused %!test %! X = [1, 10; 2, 20; 3, 30; 4, 45]; %! s = shapley (@(Z) Z(:,1), X, 'MaxNumSubsets', 100); %! assert_equal (s.NumSubsets, 4); %!test # a wide model takes MATLAB's default budget %! X = repmat ((1:20)', 1, 16); %! s = shapley (@(Z) Z(:,1), X); %! assert_equal (s.NumSubsets, 1024); %!test # the weighted least squares reproduces the exact values on a linear %! # function, which it fits with no residual whatever subsets it is given %! X = [1, 10, 2; 2, 20, 5; 3, 30, 1; 4, 45, 7]; %! b = [2, -3, 5]; %! f = @(Z) Z * b'; %! q = [3, 20, 4]; %! s = shapley (f, X, 'QueryPoints', q, 'MaxNumSubsets', 6, ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.Shapley.Value, (b .* (q - mean (X)))', 1e-10); %!test # and a budget short of every subset still sums to the deviation %! load fisheriris %! Mdl = fitcknn (meas, species); %! s = shapley (Mdl, 'QueryPoints', meas(1,:), 'MaxNumSubsets', 10, ... %! 'NumObservationsToSample', 'all'); %! [~, sc] = predict (Mdl, meas(1,:)); %! v = [s.Shapley.setosa, s.Shapley.versicolor, s.Shapley.virginica]; %! assert_equal (sum (v, 1), sc - s.Intercept, 1e-10); %!test # a budget of every subset agrees with the exact enumeration %! load fisheriris %! Mdl = fitcknn (meas, species); %! s = shapley (Mdl, 'QueryPoints', meas(1,:), ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.Shapley.setosa(3), 0.664444444444445, 1e-12); ## MATLAB parity: a budget short of every subset, where the four predictors ## are covered but the pairs of them are not and are taken with their ## complements. Measured on R2024a %!test %! load fisheriris %! Mdl = fitcknn (meas, species); %! s = shapley (Mdl, 'QueryPoints', meas(1,:), 'MaxNumSubsets', 12, ... %! 'NumObservationsToSample', 'all'); %! v = [s.Shapley.setosa, s.Shapley.versicolor, s.Shapley.virginica]; %! assert_equal (v, [0.000555555555556, -0.007777777777778, ... %! 0.007222222222222; ... %! 0.000555555555556, 0.035555555555556, ... %! -0.036111111111111; ... %! 0.664444444444444, -0.435555555555555, ... %! -0.228888888888889; ... %! 0.001111111111111, 0.074444444444445, ... %! -0.075555555555555], 1e-12); ## MATLAB parity: five predictors, where the budget runs out inside the ## second pair of cardinalities. Measured on R2024a %!test %! load fisheriris %! X5 = [meas, meas(:,1) .* meas(:,4)]; %! Mdl = fitcknn (X5, species); %! s = shapley (Mdl, 'QueryPoints', X5(1,:), 'MaxNumSubsets', 24, ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.Shapley.setosa', ... %! [0.000625, 0.002239583333333, 0.003510416666667, ... %! 0.002843750000000, 0.657447916666667], 1e-12); ## MATLAB parity: conditioning on a predictor keeps the tenth of the ## observations nearest the query point in it. A hundred and one ## observations is what fixes that tenth as rounded up rather than to ## nearest. Measured on R2024a %!test %! t = (1:101)'; %! X = [t, mod(t * 37, 101), mod(t * 53, 101)]; %! s = shapley (@(Z) Z(:,1), X, 'QueryPoints', [10, 50, 90], ... %! 'Method', 'conditional', 'NumObservationsToSample', 'all'); %! assert_equal (s.Shapley.Value', [-45.560606060606048, ... %! 1.621212121212129, 2.939393939393916], 1e-10); ## MATLAB parity: a classifier answered for by conditioning. Measured on ## R2024a %!test %! load fisheriris %! Mdl = fitcknn (meas, species); %! s = shapley (Mdl, 'QueryPoints', meas(1,:), 'Method', 'conditional', ... %! 'NumObservationsToSample', 'all'); %! v = [s.Shapley.setosa, s.Shapley.versicolor, s.Shapley.virginica]; %! assert_equal (v, [0.15, -0.0666666666666667, -0.0833333333333334; ... %! 0.172222222222222, -0.0888888888888889, ... %! -0.0833333333333333; ... %! 0.172222222222222, -0.0888888888888889, ... %! -0.0833333333333333; ... %! 0.172222222222222, -0.0888888888888889, ... %! -0.0833333333333334], 1e-12); %!test # conditioning is reported as its own algorithm %! X = [1, 10; 2, 20; 3, 30; 4, 45]; %! s = shapley (@(Z) Z(:,1), X, 'Method', 'conditional'); %! assert_equal (s.Method, 'conditional-kernel'); %!test # and still sums to the deviation of the prediction from the average %! t = (1:100)'; %! X = [t, mod(t * 37, 101), mod(t * 53, 101)]; %! f = @(Z) Z(:,1); %! s = shapley (f, X, 'QueryPoints', [10, 50, 90], ... %! 'Method', 'conditional', 'NumObservationsToSample', 'all'); %! assert_equal (sum (s.Shapley.Value), f ([10, 50, 90]) - s.Intercept, 1e-10); %!test # a predictor recorded on a wider scale does not pull the neighbours %! t = (1:100)'; %! X = [t, mod(t * 37, 101), mod(t * 53, 101)]; %! W = X; %! W(:,2) = W(:,2) * 1000; %! s = shapley (@(Z) Z(:,1), X, 'QueryPoints', [10, 50, 90], ... %! 'Method', 'conditional', 'NumObservationsToSample', 'all'); %! r = shapley (@(Z) Z(:,1), W, 'QueryPoints', [10, 50000, 90], ... %! 'Method', 'conditional', 'NumObservationsToSample', 'all'); %! assert_equal (r.Shapley.Value, s.Shapley.Value, 1e-10); %!test # nothing but the two extremes says nothing about any one predictor %! X = [1, 10; 2, 20; 3, 30; 4, 45]; %! s = shapley (@(Z) Z(:,1), X, 'QueryPoints', [3, 20], ... %! 'MaxNumSubsets', 2, 'NumObservationsToSample', 'all'); %! assert_equal (s.Shapley.Value, [NaN; NaN]); %!test # a model predicting a weighted sum is answered from the weights %! load fisheriris %! Mdl = fitrlinear (meas(:,2:4), meas(:,1)); %! s = shapley (Mdl, meas(:,2:4), 'QueryPoints', meas(1,2:4), ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.Method, 'interventional-linear'); %!test # and gives what the subsets would have given %! load fisheriris %! Mdl = fitrlinear (meas(:,2:4), meas(:,1)); %! s = shapley (Mdl, meas(:,2:4), 'QueryPoints', meas(1,2:4), ... %! 'NumObservationsToSample', 'all'); %! k = shapley (Mdl, meas(:,2:4), 'QueryPoints', meas(1,2:4), ... %! 'MaxNumSubsets', 8, 'NumObservationsToSample', 'all'); %! assert_equal (s.Shapley.Value, k.Shapley.Value, 1e-10); %!test # which is each weight times the deviation of its predictor %! load fisheriris %! Mdl = fitrlinear (meas(:,2:4), meas(:,1)); %! s = shapley (Mdl, meas(:,2:4), 'QueryPoints', meas(1,2:4), ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.Shapley.Value, ... %! Mdl.Beta(:) .* (meas(1,2:4) - mean (meas(:,2:4)))', 1e-10); %!test # a support vector machine on any other kernel keeps no such weights %! load fisheriris %! Mdl = fitrsvm (meas(:,2:4), meas(:,1), 'KernelFunction', 'gaussian'); %! s = shapley (Mdl, meas(:,2:4), 'QueryPoints', meas(1,2:4), ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.Method, 'interventional-kernel'); ## A transform of the score breaks the weighted sum, so the subsets answer ## and the values still sum to the deviation. MATLAB keeps the short answer ## here and reports an intercept on the transformed scale beside values on ## the untransformed one, which do not sum to it %!test %! load fisheriris %! Mdl = fitcsvm (meas(51:150,:), species(51:150), ... %! 'ScoreTransform', 'logit'); %! s = shapley (Mdl, meas(51:150,:), 'QueryPoints', meas(51,:), ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.Method, 'interventional-kernel'); %! [~, sc] = predict (Mdl, meas(51,:)); %! v = [s.Shapley.versicolor, s.Shapley.virginica]; %! assert_equal (sum (v, 1), sc - s.Intercept, 1e-10); %!test # asking for a budget of subsets asks for the subsets %! load fisheriris %! Mdl = fitrlinear (meas(:,2:4), meas(:,1)); %! s = shapley (Mdl, meas(:,2:4), 'QueryPoints', meas(1,2:4), ... %! 'MaxNumSubsets', 8, 'NumObservationsToSample', 'all'); %! assert_equal (s.Method, 'interventional-kernel'); %!test # a decision tree is answered leaf by leaf %! load fisheriris %! Mdl = fitrtree (meas(:,2:4), meas(:,1)); %! s = shapley (Mdl, 'QueryPoints', meas(1,2:4), ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.Method, 'interventional-tree'); %!test # and gives what the subsets would have given %! load fisheriris %! Mdl = fitrtree (meas(:,2:4), meas(:,1)); %! s = shapley (Mdl, 'QueryPoints', meas(1,2:4), ... %! 'NumObservationsToSample', 'all'); %! k = shapley (Mdl, meas(:,2:4), 'QueryPoints', meas(1,2:4), ... %! 'MaxNumSubsets', 8, 'NumObservationsToSample', 'all'); %! assert_equal (s.Shapley.Value, k.Shapley.Value, 1e-10); %!test # for a classifier too, one column of values per class %! load fisheriris %! Mdl = fitctree (meas, species); %! s = shapley (Mdl, 'QueryPoints', meas(1,:), ... %! 'NumObservationsToSample', 'all'); %! k = shapley (Mdl, meas, 'QueryPoints', meas(1,:), ... %! 'MaxNumSubsets', 16, 'NumObservationsToSample', 'all'); %! assert_equal (s.Shapley.versicolor, k.Shapley.versicolor, 1e-10); %!test # and for a compact one, which keeps the nodes and not the data %! load fisheriris %! Mdl = compact (fitctree (meas, species)); %! s = shapley (Mdl, meas, 'QueryPoints', meas(1,:), ... %! 'NumObservationsToSample', 'all'); %! k = shapley (Mdl, meas, 'QueryPoints', meas(1,:), ... %! 'MaxNumSubsets', 16, 'NumObservationsToSample', 'all'); %! assert_equal (s.Shapley.setosa, k.Shapley.setosa, 1e-10); %!test # a cut on the levels of a categorical predictor is followed too %! t = (1:120)'; %! X = [mod(t, 4), double(t), mod(t * 7, 11)]; %! y = 3 * (X(:,1) == 1) - 2 * (X(:,1) == 3) + 0.01 * mod (t, 5); %! Mdl = fitrtree (X, y, 'CategoricalPredictors', 1); %! s = shapley (Mdl, 'QueryPoints', X(3,:), ... %! 'NumObservationsToSample', 'all'); %! k = shapley (Mdl, X, 'QueryPoints', X(3,:), 'MaxNumSubsets', 8, ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.Shapley.Value, k.Shapley.Value, 1e-10); %!test # a tree is exact however many predictors it has, where the subsets %! # would have stopped at the budget of 1024 %! rand ('seed', 7); %! randn ('seed', 7); %! X = randn (200, 14); %! y = X(:,1) + 2 * X(:,5) - X(:,9); %! Mdl = fitrtree (X, y); %! s = shapley (Mdl, 'QueryPoints', X(1,:), ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.Method, 'interventional-tree'); %! assert_equal (sum (s.Shapley.Value), ... %! predict (Mdl, X(1,:)) - s.Intercept, 1e-10); ## A transform of the score is applied to each leaf, and exactly one leaf ## answers for a row, so the tree still comes apart and the values still sum ## to the deviation. MATLAB reports an intercept on the transformed scale ## beside values on the untransformed one, which do not sum to it %!test %! load fisheriris %! Mdl = fitctree (meas(51:150,:), species(51:150), ... %! 'ScoreTransform', 'logit'); %! s = shapley (Mdl, 'QueryPoints', meas(51,:), ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.Method, 'interventional-tree'); %! [~, sc] = predict (Mdl, meas(51,:)); %! v = [s.Shapley.versicolor, s.Shapley.virginica]; %! assert_equal (sum (v, 1), sc - s.Intercept, 1e-10); %!test # asking for a budget of subsets asks for the subsets %! load fisheriris %! Mdl = fitrtree (meas(:,2:4), meas(:,1)); %! s = shapley (Mdl, meas(:,2:4), 'QueryPoints', meas(1,2:4), ... %! 'MaxNumSubsets', 8, 'NumObservationsToSample', 'all'); %! assert_equal (s.Method, 'interventional-kernel'); %!test # an ensemble of trees is walked leaf by leaf, as one tree is %! rand ('seed', 11); %! randn ('seed', 11); %! X = randn (60, 11); %! y = X(:,1) + 2 * X(:,2) - X(:,3); %! Mdl = fitrensemble (X, y, 'Method', 'Bag', 'NumLearningCycles', 4); %! s = shapley (Mdl, 'QueryPoints', X(1,:), ... %! 'NumObservationsToSample', 'all'); %! k = shapley (Mdl, X, 'QueryPoints', X(1,:), 'MaxNumSubsets', 2048, ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.Method, 'interventional-tree'); %! assert_equal (s.Shapley.Value, k.Shapley.Value, 1e-10); %!test # and a classifier's, where a learner's classes are put where the %! # ensemble keeps them %! rand ('seed', 11); %! randn ('seed', 11); %! X = randn (60, 11); %! g = repmat ({'a'; 'b'}, 30, 1); %! Mdl = fitcensemble (X, g, 'Method', 'Bag', 'NumLearningCycles', 4); %! s = shapley (Mdl, 'QueryPoints', X(1,:), ... %! 'NumObservationsToSample', 'all'); %! k = shapley (Mdl, X, 'QueryPoints', X(1,:), 'MaxNumSubsets', 2048, ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.Method, 'interventional-tree'); %! assert_equal (s.Shapley.a, k.Shapley.a, 1e-10); %!test # a narrow ensemble is walked too, and agrees with every subset %! load fisheriris %! Mdl = fitcensemble (meas, species, 'Method', 'Bag', ... %! 'NumLearningCycles', 10); %! s = shapley (Mdl, 'QueryPoints', meas(1,:), ... %! 'NumObservationsToSample', 'all'); %! k = shapley (Mdl, meas, 'QueryPoints', meas(1,:), 'MaxNumSubsets', 16, ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.Method, 'interventional-tree'); %! assert_equal (s.Shapley.setosa, k.Shapley.setosa, 1e-10); %!test # a boosting method reading a learner's label, not its scores, too %! rand ('seed', 3); %! randn ('seed', 3); %! X = randn (60, 11); %! g = repmat ({'a'; 'b'}, 30, 1); %! Mdl = fitcensemble (X, g, 'Method', 'AdaBoostM1', ... %! 'NumLearningCycles', 4); %! s = shapley (Mdl, 'QueryPoints', X(1,:), ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.Method, 'interventional-kernel'); %!test # a classifier's fitted label keeps the type of the response %! load fisheriris %! Mdl = fitctree (meas, species); %! s = shapley (Mdl, 'QueryPoints', meas(1,:), ... %! 'NumObservationsToSample', 'all'); %! assert_equal (s.BlackboxFitted, predict (Mdl, meas(1,:))); %!error shapley () %!error shapley (42) %!error shapley (@(Z) Z(:,1)) %!error shapley (@(Z) Z(:,1), {1, 2}) %!error shapley (@(Z) Z(:,1), [1, 2; 3, 4], 'UseParallel', true) %!error shapley (@(Z) Z(:,1), [1, 2; 3, 4], 'MaxNumSubsets', 1) %!error shapley (@(Z) Z(:,1), [1, 2; 3, 4], 'MaxNumSubsets', 2.5) %!error shapley (@(Z) Z(:,1), [1, 2; 3, 4], 'Method', 'marginal') %!error shapley (@(Z) Z(:,1), [1, 2; 3, 4], 'NumObservationsToSample', 0) %!error shapley (@(Z) Z(:,1), [1, 2; 3, 4], 'NoSuchThing', 1) %!error fit (shapley (@(Z) Z(:,1), [1, 2; 3, 4]), 'abc') %!error fit (shapley (@(Z) Z(:,1), [1, 2; 3, 4]), [1, 2, 3]) ## The four drawing methods, measured on R2024a 2026-09-22 %!test # one query point: one bar per predictor, the least important first %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.1 * Z(:,3); %! s = shapley (f, X, 'QueryPoints', X(1,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! b = plot (s); %! assert_equal (numel (b), 1); %! assert_equal (get (b, 'horizontal'), 'on'); %! assert_equal (get (b, 'ydata')', [-2.8333333333333333, -5, 80], 1e-12); %! assert_equal (get (gca (), 'yticklabel')', {'x3', 'x1', 'x2'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # one query point is titled as an explanation of that point %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.1 * Z(:,3); %! s = shapley (f, X, 'QueryPoints', X(1,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! plot (s); %! ax = gca (); %! assert_equal (get (get (ax, 'title'), 'string'), 'Shapley Explanation'); %! assert_equal (get (get (ax, 'xlabel'), 'string'), 'Shapley Value'); %! assert_equal (get (get (ax, 'ylabel'), 'string'), 'Predictor'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Several query points are summarised by the mean of the absolute values %!test %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.1 * Z(:,3); %! s = shapley (f, X, 'QueryPoints', X(1:4,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! b = plot (s); %! ax = gca (); %! assert_equal (get (get (ax, 'title'), 'string'), ... %! 'Shapley Importance Plot'); %! assert_equal (get (get (ax, 'xlabel'), 'string'), ... %! 'Mean of Absolute Shapley Values'); %! assert_equal (get (b, 'ydata')', [2.5, 2.75, 43.75], 1e-12); %! assert_equal (get (ax, 'yticklabel')', {'x1', 'x3', 'x2'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Over several query points whatever is left over is summed into one bar %!test %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.1 * Z(:,3); %! s = shapley (f, X, 'QueryPoints', X(1:4,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! b = plot (s, 'NumImportantPredictors', 2); %! assert_equal (get (b, 'ydata')', [2.5, 2.75, 43.75], 1e-12); %! assert_equal (get (gca (), 'yticklabel')', ... %! {'Sum of other 1 predictor(s)', 'x3', 'x2'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # over one query point it is left out rather than summed %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.1 * Z(:,3); %! s = shapley (f, X, 'QueryPoints', X(1,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! b = plot (s, 'NumImportantPredictors', 2); %! assert_equal (get (b, 'ydata')', [-5, 80], 1e-12); %! assert_equal (get (gca (), 'yticklabel')', {'x1', 'x2'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # naming one query point explains that point rather than summarising %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.1 * Z(:,3); %! s = shapley (f, X, 'QueryPoints', X(1:4,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! b = plot (s, 'QueryPointIndices', 2); %! ax = gca (); %! assert_equal (get (get (ax, 'title'), 'string'), 'Shapley Explanation'); %! assert_equal (get (b, 'ydata')', [2.1666666666666667, -3, 50], 1e-12); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # the axes to draw into may be given first, as for any plot %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.1 * Z(:,3); %! s = shapley (f, X, 'QueryPoints', X(1,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! a1 = subplot (1, 2, 1); %! a2 = subplot (1, 2, 2); %! b = plot (a2, s); %! assert_equal (get (b, 'parent'), a2); %! assert_equal (isempty (get (a1, 'children')), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## MATLAB parity: one query point of a classifier explains the class predicted %!test %! load fisheriris %! Mdl = fitcknn (meas, species); %! s = shapley (Mdl, 'QueryPoints', meas(1,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! b = plot (s); %! assert_equal (numel (b), 1); %! assert_equal (s.BlackboxFitted, {'setosa'}); %! assert_equal (sort (get (b, 'ydata')'), sort (s.Shapley.setosa'), 1e-12); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## MATLAB parity: several query points of a classifier draw every class %!test %! load fisheriris %! Mdl = fitcknn (meas, species); %! s = shapley (Mdl, 'QueryPoints', meas([1, 60, 120],:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! b = plot (s); %! assert_equal (numel (b), 3); %! assert_equal (get (b(1), 'displayname'), 'setosa'); %! assert_equal (get (b(3), 'displayname'), 'virginica'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # the classes to draw may be named %! load fisheriris %! Mdl = fitcknn (meas, species); %! s = shapley (Mdl, 'QueryPoints', meas([1, 60, 120],:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! b = plot (s, 'ClassNames', {'setosa', 'virginica'}); %! assert_equal (numel (b), 2); %! assert_equal (get (b(2), 'displayname'), 'virginica'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # boxchart draws one box per predictor, lying horizontally %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.1 * Z(:,3); %! s = shapley (f, X, 'QueryPoints', X(1:4,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! b = boxchart (s); %! assert_equal (class (b), 'stats.chart.BoxChart'); %! assert_equal (b.Orientation, 'horizontal'); %! assert_equal (class (b.XData), 'categorical'); %! assert_equal (numel (findobj (gca (), 'type', 'patch')), 3); %! assert_equal (get (gca (), 'yticklabel')', {'x1', 'x3', 'x2'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # boxchart is titled as a summary over the query points %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.1 * Z(:,3); %! s = shapley (f, X, 'QueryPoints', X(1:4,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! boxchart (s); %! ax = gca (); %! assert_equal (get (get (ax, 'title'), 'string'), 'Shapley Summary Plot'); %! assert_equal (get (get (ax, 'xlabel'), 'string'), 'Shapley Value'); %! assert_equal (get (get (ax, 'ylabel'), 'string'), 'Predictor'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## MATLAB parity: boxchart leaves the rest out rather than summing them %!test %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.1 * Z(:,3); %! s = shapley (f, X, 'QueryPoints', X(1:4,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! boxchart (s, 'NumImportantPredictors', 2); %! assert_equal (get (gca (), 'yticklabel')', {'x3', 'x2'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## MATLAB parity: a chart of one class orders the predictors by that class %!test %! load fisheriris %! Mdl = fitcknn (meas, species); %! s = shapley (Mdl, 'QueryPoints', meas([1, 60, 120],:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! boxchart (s, 'ClassName', 'virginica'); %! [~, ord] = sort (s.MeanAbsoluteShapley.virginica, 'ascend'); %! assert_equal (get (gca (), 'yticklabel')', ... %! cellstr (s.Shapley.Predictor(ord))'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # the outlier markers may be spread across the width of the box %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.1 * Z(:,3); %! s = shapley (f, X, 'QueryPoints', X(1:4,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! b = boxchart (s, 'JitterOutliers', 'on'); %! assert_equal (b.JitterOutliers, 'on'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # swarmchart draws one row of points per predictor %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.1 * Z(:,3); %! s = shapley (f, X, 'QueryPoints', X(1:4,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! h = swarmchart (s); %! assert_equal (numel (h), 3); %! assert_equal (get (h(1), 'xdata')', [-5, -3, -1, 1], 1e-12); %! assert_equal (get (gca (), 'yticklabel')', {'x1', 'x3', 'x2'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## MATLAB parity: a point is coloured by where the predictor's value ## stands in its own range over the query points %!test %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.1 * Z(:,3); %! s = shapley (f, X, 'QueryPoints', X(1:4,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! h = swarmchart (s); %! assert_equal (get (h(1), 'cdata')', [0, 1/3, 2/3, 1], 1e-12); %! assert_equal (get (gca (), 'clim'), [0, 1]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # the colour map the values are read through may be given %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.1 * Z(:,3); %! s = shapley (f, X, 'QueryPoints', X(1:4,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! swarmchart (s, 'ColorMap', [1, 0, 0; 0, 1, 0]); %! assert_equal (get (gca (), 'colormap'), [1, 0, 0; 0, 1, 0]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # how the points of a row are spread may be chosen %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.1 * Z(:,3); %! s = shapley (f, X, 'QueryPoints', X(1:4,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! h = swarmchart (s, 'YJitter', 'none'); %! assert_equal (get (h(1), 'ydata')', [1, 1, 1, 1]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # plotDependence draws one predictor against its own values %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.1 * Z(:,3); %! s = shapley (f, X, 'QueryPoints', X(1:4,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! p = plotDependence (s, 'x1'); %! assert_equal (get (p, 'xdata')', [1, 2, 3, 4]); %! assert_equal (get (p, 'ydata')', [-5, -3, -1, 1], 1e-12); %! ax = gca (); %! assert_equal (get (get (ax, 'title'), 'string'), ... %! 'Shapley Dependence Plot'); %! assert_equal (get (get (ax, 'xlabel'), 'string'), 'x1'); %! assert_equal (get (get (ax, 'ylabel'), 'string'), ... %! 'Shapley Values for x1'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a predictor may be indexed rather than named %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.1 * Z(:,3); %! s = shapley (f, X, 'QueryPoints', X(1:4,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! p = plotDependence (s, 2); %! assert_equal (get (p, 'xdata')', [10, 20, 30, 45]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## MATLAB parity: the colouring predictor is read as it stands, the range ## of the axes carrying the scale, where a swarm normalizes instead %!test %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.1 * Z(:,3); %! s = shapley (f, X, 'QueryPoints', X(1:4,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! p = plotDependence (s, 1, 'ColorPredictor', 'x2'); %! assert_equal (get (p, 'cdata')', [10, 20, 30, 45]); %! assert_equal (get (gca (), 'clim'), [10, 45]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a colour bar is drawn beside a chart whose points are coloured %! X = [1, 10, 100; 2, 20, 150; 3, 30, 120; 4, 45, 180; 5, 50, 90; ... %! 6, 65, 130]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2) + 0.1 * Z(:,3); %! s = shapley (f, X, 'QueryPoints', X(1:4,:), ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! plotDependence (s, 1); %! none = numel (findobj (hf, 'tag', 'colorbar')); %! plotDependence (s, 1, 'ColorPredictor', 'x2'); %! assert_equal (none, 0); %! assert_equal (numel (findobj (hf, 'tag', 'colorbar')), 1); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a predictor holding levels is drawn as a box over each level %! X = [1, 0; 2, 0; 3, 1; 4, 1; 5, 0; 6, 1]; %! f = @(Z) 2 * Z(:,1) - 3 * Z(:,2); %! s = shapley (f, X, 'QueryPoints', X(1:4,:), ... %! 'CategoricalPredictors', 2, ... %! 'NumObservationsToSample', 'all'); %! hf = figure ('visible', 'off'); %! unwind_protect %! p = plotDependence (s, 2); %! assert_equal (class (p), 'stats.chart.BoxChart'); %! assert_equal (numel (findobj (gca (), 'type', 'patch')), 2); %! assert_equal (get (get (gca (), 'title'), 'string'), ... %! 'Shapley Dependence Plot'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Input validation %!error %! plot (shapley (@(Z) Z(:,1), [1, 2; 3, 4])) %!error %! boxchart (shapley (@(Z) Z(:,1), [1, 2; 3, 4])) %!error %! swarmchart (shapley (@(Z) Z(:,1), [1, 2; 3, 4])) %!error %! plotDependence (shapley (@(Z) Z(:,1), [1, 2; 3, 4]), 1) %!error %! plot (shapley (@(Z) Z(:,1), [1, 2; 3, 4], 'QueryPoints', [1, 2]), ... %! 'NumImportantPredictors', 0) %!error %! plot (shapley (@(Z) Z(:,1), [1, 2; 3, 4], 'QueryPoints', [1, 2]), ... %! 'QueryPointIndices', 1.5) %!error %! plot (shapley (@(Z) Z(:,1), [1, 2; 3, 4], 'QueryPoints', [1, 2]), ... %! 'QueryPointIndices', 2) %!error %! plot (shapley (@(Z) Z(:,1), [1, 2; 3, 4], 'QueryPoints', [1, 2]), ... %! 'ClassNames', 'setosa') %!error %! boxchart (shapley (@(Z) Z(:,1), [1, 2; 3, 4], 'QueryPoints', [1, 2]), ... %! 'ClassName', 'setosa') %!error %! boxchart (shapley (@(Z) Z(:,1), [1, 2; 3, 4], 'QueryPoints', [1, 2]), ... %! 'JitterOutliers', 'maybe') %!error %! swarmchart (shapley (@(Z) Z(:,1), [1, 2; 3, 4], 'QueryPoints', [1, 2]), ... %! 'YJitter', 'sideways') %!error %! swarmchart (shapley (@(Z) Z(:,1), [1, 2; 3, 4], 'QueryPoints', [1, 2]), ... %! 'ColorMap', 5) %!error %! plotDependence (shapley (@(Z) Z(:,1), [1, 2; 3, 4], 'QueryPoints', [1, 2])) %!error %! plotDependence (shapley (@(Z) Z(:,1), [1, 2; 3, 4], ... %! 'QueryPoints', [1, 2]), 'nope') %!error %! plot (shapley (@(Z) Z(:,1), [1, 2; 3, 4], 'QueryPoints', [1, 2]), ... %! 'NoSuchThing', 1) ## Table input %!shared stT, stM %! load fisheriris %! stT = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! stT.Wide = categorical (meas(:,2) > 3, [false true], {'narrow', 'wide'}); %! stM = fitrtree (stT, 'SL'); %!test # the observations to average over may be given as a table %! s = shapley (stM, stT(:,[1, 2, 3, 5]), 'QueryPoints', ... %! stT(1,[1, 2, 3, 5]), 'NumObservationsToSample', 'all'); %! assert_equal (size (s.X), [150, 4]); %! assert_equal (cellstr (s.Shapley.Predictor)', ... %! {'SW', 'PL', 'PW', 'Wide'}); ## MATLAB parity: a table is read by the names the model was fitted on, so ## the order of its columns does not matter %!test %! a = shapley (stM, stT(:,[1, 2, 3, 5]), 'QueryPoints', ... %! stT(1,[1, 2, 3, 5]), 'NumObservationsToSample', 'all'); %! b = shapley (stM, stT(:,[5, 3, 2, 1]), 'QueryPoints', ... %! stT(1,[5, 3, 2, 1]), 'NumObservationsToSample', 'all'); %! assert_equal (b.Shapley.Value, a.Shapley.Value); ## MATLAB parity: a column the model was not fitted on is passed over %!test %! a = shapley (stM, stT(:,[1, 2, 3, 5]), 'QueryPoints', ... %! stT(1,[1, 2, 3, 5]), 'NumObservationsToSample', 'all'); %! b = shapley (stM, stT, 'QueryPoints', stT(1,:), ... %! 'NumObservationsToSample', 'all'); %! assert_equal (b.Shapley.Value, a.Shapley.Value); %!test # fit takes a table too, read the same way %! a = shapley (stM, stT(:,[1, 2, 3, 5]), 'QueryPoints', ... %! stT(1,[1, 2, 3, 5]), 'NumObservationsToSample', 'all'); %! b = shapley (stM, stT(:,[1, 2, 3, 5]), ... %! 'NumObservationsToSample', 'all'); %! b = fit (b, stT(1,[5, 3, 2, 1])); %! assert_equal (b.Shapley.Value, a.Shapley.Value); %!test # a level carries the code it carried at fitting %! T = stT(1:60,:); %! T.Wide = categorical (repmat ({'wide'}, 60, 1), {'narrow', 'wide'}); %! s = shapley (stM, T(:,[1, 2, 3, 5]), 'QueryPoints', ... %! T(1,[1, 2, 3, 5]), 'NumObservationsToSample', 'all'); %! assert_equal (rows (s.Shapley), 4); ## A function handle names no predictors, so a table given for one names ## them itself and its columns are taken in the order they come %!test %! f = @(Z) Z(:,1); %! s = shapley (f, stT(:,1:3), 'QueryPoints', stT(1,1:3), ... %! 'NumObservationsToSample', 'all'); %! assert_equal (cellstr (s.Shapley.Predictor)', {'SW', 'PL', 'PW'}); %! r = shapley (f, stT(:,[3, 2, 1]), 'QueryPoints', stT(1,[3, 2, 1]), ... %! 'NumObservationsToSample', 'all'); %! assert_equal (cellstr (r.Shapley.Predictor)', {'PW', 'PL', 'SW'}); %!error ... %! shapley (stM, stT(:,[1, 3, 5]), 'QueryPoints', stT(1,[1, 3, 5])) %!error ... %! fit (shapley (stM, stT(:,[1, 2, 3, 5]), ... %! 'NumObservationsToSample', 'all'), stT(1,[1, 3, 5])) statistics-release-2.1.2/inst/Machine_Learning/templateDiscriminant.m000066400000000000000000000071311527311104300260210ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{T} =} templateDiscriminant () ## @deftypefnx {statistics} {@var{T} =} templateDiscriminant (@var{name}, @var{value}) ## ## Create a template for a discriminant analysis classifier. ## ## @code{@var{T} = templateDiscriminant ()} returns a template carrying the ## default options of @code{ClassificationDiscriminant}. A template names a ## learner and the options it is to be fitted with, without fitting anything: it ## is given to a function that fits many models, such as @code{fitcecoc}, which ## uses it for every binary learner it trains. ## ## @code{@var{T} = templateDiscriminant (@var{name}, @var{value})} also stores ## the given options. They are the name-value arguments of ## @code{fitcdiscr}, and any of them may be given here instead. ## ## @example ## @group ## T = templateDiscriminant ('DiscrimType', 'quadratic'); ## Mdl = fitcecoc (X, Y, 'Learners', T); ## @end group ## @end example ## ## @var{T} is a structure carrying @qcode{Method}, @qcode{Type} and one ## field per option given. ## ## @subheading Deviation from MATLAB ## ## MATLAB returns an object of a class whose name we cannot use, which has no ## public properties and one method this package declines package wide, so a ## structure carries everything a user can observe. This is what ## @qcode{ModelParameters} already does throughout the package. ## ## An option name is not checked here. @code{ClassificationDiscriminant} owns ## the list of options it takes and checks it when the template is used, so a ## name it does not know is refused then rather than now. ## ## @seealso{fitcecoc, ClassificationDiscriminant, fitcdiscr} ## @end deftypefn function T = templateDiscriminant (varargin) [T, errmsg] = templateStruct ('Discriminant', varargin); if (! isempty (errmsg)) error ("templateDiscriminant: %s", errmsg); endif endfunction ## Tests %!test # the default template names its learner and nothing else %! T = templateDiscriminant (); %! assert_equal (class (T), 'struct'); %! assert_equal (T.Method, 'Discriminant'); %! assert_equal (T.Type, 'classification'); %! assert_equal (numfields (T), 2); %!test # an option given is stored under its own name, as it stands %! T = templateDiscriminant ('DiscrimType', 'quadratic'); %! assert_equal (numfields (T), 3); %! assert_equal (T.DiscrimType, 'quadratic'); %!test # a name the learner does not know is not refused here %! T = templateDiscriminant ('NoSuchOption', 42); %! assert_equal (T.NoSuchOption, 42); ## Test input validation %!error ... %! templateDiscriminant ('KernelScale') %!error ... %! templateDiscriminant (42, 1) %!error ... %! templateDiscriminant ('not a name', 1) statistics-release-2.1.2/inst/Machine_Learning/templateEnsemble.m000066400000000000000000000156511527311104300251350ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{T} =} templateEnsemble (@var{Method}, @var{NLearn}, @var{Learners}) ## @deftypefnx {statistics} {@var{T} =} templateEnsemble (@dots{}, @var{name}, @var{value}) ## ## Create a template for an ensemble learner. ## ## @code{@var{T} = templateEnsemble (@var{Method}, @var{NLearn}, ## @var{Learners})} returns a template for an ensemble grown by @var{Method} ## from @var{NLearn} learners, each fitted as @var{Learners} says. A template ## names a learner and the options it is to be fitted with, without fitting ## anything: it is given to @code{fitcecoc}, which grows one such ensemble for ## every binary learner it trains. ## ## @var{Method} is one of the methods of @code{fitcensemble} or ## @code{fitrensemble}, in any letter case. @var{NLearn} is the number of ## learning cycles and @var{Learners} a learner name, such as ## @qcode{'tree'}, or a template of one, such as @code{templateTree} returns; ## they are the @qcode{'NumLearningCycles'} and @qcode{'Learners'} options of ## @code{fitcensemble}. ## ## @code{@var{T} = templateEnsemble (@dots{}, @var{name}, @var{value})} ## also stores the given options. They are the name-value arguments of ## @code{fitcensemble}, such as @qcode{'LearnRate'}. ## ## @example ## @group ## T = templateEnsemble ('GentleBoost', 50, templateTree ('MaxNumSplits', 1)); ## Mdl = fitcecoc (X, Y, 'Learners', T); ## @end group ## @end example ## ## @var{T} is a structure carrying @qcode{Method}, @qcode{Type}, ## @qcode{LearnerTemplates}, @qcode{NLearn} and one field per option given. ## @qcode{Type} is @qcode{'regression'} for LSBoost and ## @qcode{'classification'} for every other method; a @qcode{'Type'} option ## may choose it for Bag, which serves both. ## ## @subheading Deviation from MATLAB ## ## MATLAB returns an object of a class whose name we cannot use, which has no ## public properties and one method this package declines package wide, so a ## structure carries everything a user can observe. This is what ## @qcode{ModelParameters} already does throughout the package. ## ## Only the method is checked here. The ensemble owns the list of options it ## takes and checks them, @var{NLearn} and @var{Learners} included, when the ## template is used, so a value it refuses is refused then rather than now. ## MATLAB checks @qcode{'LearnRate'} already here. ## ## @seealso{fitcecoc, fitcensemble, templateTree} ## @end deftypefn function T = templateEnsemble (Method, NLearn, Learners, varargin) if (nargin < 3) print_usage (); endif names = {'AdaBoostM1', 'AdaBoostM2', 'GentleBoost', 'LogitBoost', ... 'LPBoost', 'TotalBoost', 'RobustBoost', 'RUSBoost', ... 'Subspace', 'Bag', 'LSBoost'}; if (! (ischar (Method) && isrow (Method))) error ("templateEnsemble: METHOD must be a character vector."); endif k = find (strcmpi (Method, names)); if (isempty (k)) error ("templateEnsemble: '%s' is not an ensemble method.", Method); endif Method = names{k}; [opts, errmsg] = templateStruct (Method, varargin); if (! isempty (errmsg)) error ("templateEnsemble: %s", errmsg); endif Type = methodType (Method); given = find (strcmpi ('Type', varargin(1:2:end))); if (! isempty (given)) Type = varargin{2 * given(end)}; if (! (ischar (Type) && any (strcmp (Type, {'classification', ... 'regression'})))) error (strcat ("templateEnsemble: 'Type' must be 'classification'", ... " or 'regression'.")); endif if (! strcmp (Method, 'Bag') && ! strcmp (Type, methodType (Method))) error ("templateEnsemble: the '%s' method is for %s.", Method, ... methodType (Method)); endif endif T = struct ('Method', Method, 'Type', Type, ... 'LearnerTemplates', {Learners}, 'NLearn', NLearn); for [val, name] = opts if (! any (strcmpi (name, {'Method', 'Type'}))) T.(name) = val; endif endfor endfunction ## The kind of model a method grows, Bag growing either. function type = methodType (Method) if (strcmp (Method, 'LSBoost')) type = 'regression'; else type = 'classification'; endif endfunction ## Tests %!test # a template names its method, type, learners and cycles %! T = templateEnsemble ('AdaBoostM1', 20, 'tree'); %! assert_equal (class (T), 'struct'); %! assert_equal (fieldnames (T), ... %! {'Method'; 'Type'; 'LearnerTemplates'; 'NLearn'}); %! assert_equal (T.Method, 'AdaBoostM1'); %! assert_equal (T.Type, 'classification'); %! assert_equal (T.LearnerTemplates, 'tree'); %! assert_equal (T.NLearn, 20); %!test # the method is matched in any letter case %! T = templateEnsemble ('gentleboost', 10, 'tree'); %! assert_equal (T.Method, 'GentleBoost'); %!test # LSBoost grows a regression ensemble %! T = templateEnsemble ('LSBoost', 10, 'tree'); %! assert_equal (T.Type, 'regression'); %!test # Bag takes its type from the 'Type' option %! T = templateEnsemble ('Bag', 10, 'tree', 'Type', 'regression'); %! assert_equal (T.Type, 'regression'); %! assert_equal (isfield (T, 'Type'), true); %! assert_equal (numfields (T), 4); %!test # a learner template and the options are stored as they stand %! S = templateTree ('MaxNumSplits', 1); %! T = templateEnsemble ('GentleBoost', 5, S, 'LearnRate', 0.5); %! assert_equal (T.LearnerTemplates, S); %! assert_equal (T.LearnRate, 0.5); %!test # a value the ensemble would refuse is not refused here %! T = templateEnsemble ('AdaBoostM1', 0, 'svm'); %! assert_equal (T.NLearn, 0); ## Test input validation %!error templateEnsemble ('AdaBoostM1', 10) %!error ... %! templateEnsemble (1, 10, 'tree') %!error ... %! templateEnsemble ('Foo', 10, 'tree') %!error ... %! templateEnsemble ('AdaBoostM1', 10, 'tree', 'LearnRate') %!error ... %! templateEnsemble ('Bag', 10, 'tree', 'Type', 'both') %!error ... %! templateEnsemble ('LSBoost', 10, 'tree', 'Type', 'classification') statistics-release-2.1.2/inst/Machine_Learning/templateKNN.m000066400000000000000000000067261527311104300240340ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{T} =} templateKNN () ## @deftypefnx {statistics} {@var{T} =} templateKNN (@var{name}, @var{value}) ## ## Create a template for a nearest neighbour classifier. ## ## @code{@var{T} = templateKNN ()} returns a template carrying the default ## options of @code{ClassificationKNN}. A template names a learner and ## the options it is to be fitted with, without fitting anything: it is given ## to a function that fits many models, such as @code{fitcecoc}, which uses it ## for every binary learner it trains. ## ## @code{@var{T} = templateKNN (@var{name}, @var{value})} also stores ## the given options. They are the name-value arguments of ## @code{fitcknn}, and any of them may be given here instead. ## ## @example ## @group ## T = templateKNN ('NumNeighbors', 5, 'Distance', 'cityblock'); ## Mdl = fitcecoc (X, Y, 'Learners', T); ## @end group ## @end example ## ## @var{T} is a structure carrying @qcode{Method}, @qcode{Type} and one ## field per option given. ## ## @subheading Deviation from MATLAB ## ## MATLAB returns an object of a class whose name we cannot use, which has no ## public properties and one method this package declines package wide, so a ## structure carries everything a user can observe. This is what ## @qcode{ModelParameters} already does throughout the package. ## ## An option name is not checked here. @code{ClassificationKNN} owns ## the list of options it takes and checks it when the template is used, so a ## name it does not know is refused then rather than now. ## ## @seealso{fitcecoc, ClassificationKNN, fitcknn} ## @end deftypefn function T = templateKNN (varargin) [T, errmsg] = templateStruct ('KNN', varargin); if (! isempty (errmsg)) error ("templateKNN: %s", errmsg); endif endfunction ## Tests %!test # the default template names its learner and nothing else %! T = templateKNN (); %! assert_equal (class (T), 'struct'); %! assert_equal (T.Method, 'KNN'); %! assert_equal (T.Type, 'classification'); %! assert_equal (numfields (T), 2); %!test # an option given is stored under its own name, as it stands %! T = templateKNN ('NumNeighbors', 5, 'Distance', 'cityblock'); %! assert_equal (numfields (T), 4); %! assert_equal (T.NumNeighbors, 5); %! assert_equal (T.Distance, 'cityblock'); %!test # a name the learner does not know is not refused here %! T = templateKNN ('NoSuchOption', 42); %! assert_equal (T.NoSuchOption, 42); ## Test input validation %!error ... %! templateKNN ('KernelScale') %!error ... %! templateKNN (42, 1) %!error ... %! templateKNN ('not a name', 1) statistics-release-2.1.2/inst/Machine_Learning/templateKernel.m000066400000000000000000000071151527311104300246170ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{T} =} templateKernel () ## @deftypefnx {statistics} {@var{T} =} templateKernel (@var{name}, @var{value}) ## ## Create a template for a kernel classifier using random feature expansion. ## ## @code{@var{T} = templateKernel ()} returns a template carrying the default ## options of @code{ClassificationKernel}. A template names a learner and ## the options it is to be fitted with, without fitting anything: it is given ## to a function that fits many models, such as @code{fitcecoc}, which uses it ## for every binary learner it trains. ## ## @code{@var{T} = templateKernel (@var{name}, @var{value})} also stores ## the given options. They are the name-value arguments of ## @code{fitckernel}, and any of them may be given here instead. ## ## @example ## @group ## T = templateKernel ('Learner', 'logistic', 'NumExpansionDimensions', 128); ## Mdl = fitcecoc (X, Y, 'Learners', T); ## @end group ## @end example ## ## @var{T} is a structure carrying @qcode{Method}, @qcode{Type} and one ## field per option given. ## ## @subheading Deviation from MATLAB ## ## MATLAB returns an object of a class whose name we cannot use, which has no ## public properties and one method this package declines package wide, so a ## structure carries everything a user can observe. This is what ## @qcode{ModelParameters} already does throughout the package. ## ## An option name is not checked here. @code{ClassificationKernel} owns ## the list of options it takes and checks it when the template is used, so a ## name it does not know is refused then rather than now. ## ## @seealso{fitcecoc, ClassificationKernel, fitckernel} ## @end deftypefn function T = templateKernel (varargin) [T, errmsg] = templateStruct ('Kernel', varargin); if (! isempty (errmsg)) error ("templateKernel: %s", errmsg); endif endfunction ## Tests %!test # the default template names its learner and nothing else %! T = templateKernel (); %! assert_equal (class (T), 'struct'); %! assert_equal (T.Method, 'Kernel'); %! assert_equal (T.Type, 'classification'); %! assert_equal (numfields (T), 2); %!test # an option given is stored under its own name, as it stands %! T = templateKernel ('Learner', 'logistic', 'NumExpansionDimensions', 128); %! assert_equal (numfields (T), 4); %! assert_equal (T.Learner, 'logistic'); %! assert_equal (T.NumExpansionDimensions, 128); %!test # a name the learner does not know is not refused here %! T = templateKernel ('NoSuchOption', 42); %! assert_equal (T.NoSuchOption, 42); ## Test input validation %!error ... %! templateKernel ('KernelScale') %!error ... %! templateKernel (42, 1) %!error ... %! templateKernel ('not a name', 1) statistics-release-2.1.2/inst/Machine_Learning/templateLinear.m000066400000000000000000000070331527311104300246100ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{T} =} templateLinear () ## @deftypefnx {statistics} {@var{T} =} templateLinear (@var{name}, @var{value}) ## ## Create a template for a linear classifier for high dimensional data. ## ## @code{@var{T} = templateLinear ()} returns a template carrying the default ## options of @code{ClassificationLinear}. A template names a learner and ## the options it is to be fitted with, without fitting anything: it is given ## to a function that fits many models, such as @code{fitcecoc}, which uses it ## for every binary learner it trains. ## ## @code{@var{T} = templateLinear (@var{name}, @var{value})} also stores ## the given options. They are the name-value arguments of ## @code{fitclinear}, and any of them may be given here instead. ## ## @example ## @group ## T = templateLinear ('Learner', 'logistic', 'Lambda', 1e-4); ## Mdl = fitcecoc (X, Y, 'Learners', T); ## @end group ## @end example ## ## @var{T} is a structure carrying @qcode{Method}, @qcode{Type} and one ## field per option given. ## ## @subheading Deviation from MATLAB ## ## MATLAB returns an object of a class whose name we cannot use, which has no ## public properties and one method this package declines package wide, so a ## structure carries everything a user can observe. This is what ## @qcode{ModelParameters} already does throughout the package. ## ## An option name is not checked here. @code{ClassificationLinear} owns ## the list of options it takes and checks it when the template is used, so a ## name it does not know is refused then rather than now. ## ## @seealso{fitcecoc, ClassificationLinear, fitclinear} ## @end deftypefn function T = templateLinear (varargin) [T, errmsg] = templateStruct ('Linear', varargin); if (! isempty (errmsg)) error ("templateLinear: %s", errmsg); endif endfunction ## Tests %!test # the default template names its learner and nothing else %! T = templateLinear (); %! assert_equal (class (T), 'struct'); %! assert_equal (T.Method, 'Linear'); %! assert_equal (T.Type, 'classification'); %! assert_equal (numfields (T), 2); %!test # an option given is stored under its own name, as it stands %! T = templateLinear ('Learner', 'logistic', 'Lambda', 1e-4); %! assert_equal (numfields (T), 4); %! assert_equal (T.Learner, 'logistic'); %! assert_equal (T.Lambda, 1e-4); %!test # a name the learner does not know is not refused here %! T = templateLinear ('NoSuchOption', 42); %! assert_equal (T.NoSuchOption, 42); ## Test input validation %!error ... %! templateLinear ('KernelScale') %!error ... %! templateLinear (42, 1) %!error ... %! templateLinear ('not a name', 1) statistics-release-2.1.2/inst/Machine_Learning/templateNaiveBayes.m000066400000000000000000000070501527311104300254230ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{T} =} templateNaiveBayes () ## @deftypefnx {statistics} {@var{T} =} templateNaiveBayes (@var{name}, @var{value}) ## ## Create a template for a naive Bayes classifier. ## ## @code{@var{T} = templateNaiveBayes ()} returns a template carrying the ## default options of @code{ClassificationNaiveBayes}. A template names a ## learner and the options it is to be fitted with, without fitting anything: it ## is given to a function that fits many models, such as @code{fitcecoc}, which ## uses it for every binary learner it trains. ## ## @code{@var{T} = templateNaiveBayes (@var{name}, @var{value})} also stores ## the given options. They are the name-value arguments of ## @code{fitcnb}, and any of them may be given here instead. ## ## @example ## @group ## T = templateNaiveBayes ('DistributionNames', 'kernel'); ## Mdl = fitcecoc (X, Y, 'Learners', T); ## @end group ## @end example ## ## @var{T} is a structure carrying @qcode{Method}, @qcode{Type} and one ## field per option given. ## ## @subheading Deviation from MATLAB ## ## MATLAB returns an object of a class whose name we cannot use, which has no ## public properties and one method this package declines package wide, so a ## structure carries everything a user can observe. This is what ## @qcode{ModelParameters} already does throughout the package. ## ## An option name is not checked here. @code{ClassificationNaiveBayes} owns ## the list of options it takes and checks it when the template is used, so a ## name it does not know is refused then rather than now. ## ## @seealso{fitcecoc, ClassificationNaiveBayes, fitcnb} ## @end deftypefn function T = templateNaiveBayes (varargin) [T, errmsg] = templateStruct ('NaiveBayes', varargin); if (! isempty (errmsg)) error ("templateNaiveBayes: %s", errmsg); endif endfunction ## Tests %!test # the default template names its learner and nothing else %! T = templateNaiveBayes (); %! assert_equal (class (T), 'struct'); %! assert_equal (T.Method, 'NaiveBayes'); %! assert_equal (T.Type, 'classification'); %! assert_equal (numfields (T), 2); %!test # an option given is stored under its own name, as it stands %! T = templateNaiveBayes ('DistributionNames', 'kernel'); %! assert_equal (numfields (T), 3); %! assert_equal (T.DistributionNames, 'kernel'); %!test # a name the learner does not know is not refused here %! T = templateNaiveBayes ('NoSuchOption', 42); %! assert_equal (T.NoSuchOption, 42); ## Test input validation %!error ... %! templateNaiveBayes ('KernelScale') %!error ... %! templateNaiveBayes (42, 1) %!error ... %! templateNaiveBayes ('not a name', 1) statistics-release-2.1.2/inst/Machine_Learning/templateSVM.m000066400000000000000000000067231527311104300240500ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{T} =} templateSVM () ## @deftypefnx {statistics} {@var{T} =} templateSVM (@var{name}, @var{value}) ## ## Create a template for a support vector machine. ## ## @code{@var{T} = templateSVM ()} returns a template carrying the default ## options of @code{ClassificationSVM}. A template names a learner and ## the options it is to be fitted with, without fitting anything: it is given ## to a function that fits many models, such as @code{fitcecoc}, which uses it ## for every binary learner it trains. ## ## @code{@var{T} = templateSVM (@var{name}, @var{value})} also stores ## the given options. They are the name-value arguments of ## @code{fitcsvm}, and any of them may be given here instead. ## ## @example ## @group ## T = templateSVM ('KernelFunction', 'rbf', 'BoxConstraint', 2); ## Mdl = fitcecoc (X, Y, 'Learners', T); ## @end group ## @end example ## ## @var{T} is a structure carrying @qcode{Method}, @qcode{Type} and one ## field per option given. ## ## @subheading Deviation from MATLAB ## ## MATLAB returns an object of a class whose name we cannot use, which has no ## public properties and one method this package declines package wide, so a ## structure carries everything a user can observe. This is what ## @qcode{ModelParameters} already does throughout the package. ## ## An option name is not checked here. @code{ClassificationSVM} owns ## the list of options it takes and checks it when the template is used, so a ## name it does not know is refused then rather than now. ## ## @seealso{fitcecoc, ClassificationSVM, fitcsvm} ## @end deftypefn function T = templateSVM (varargin) [T, errmsg] = templateStruct ('SVM', varargin); if (! isempty (errmsg)) error ("templateSVM: %s", errmsg); endif endfunction ## Tests %!test # the default template names its learner and nothing else %! T = templateSVM (); %! assert_equal (class (T), 'struct'); %! assert_equal (T.Method, 'SVM'); %! assert_equal (T.Type, 'classification'); %! assert_equal (numfields (T), 2); %!test # an option given is stored under its own name, as it stands %! T = templateSVM ('KernelFunction', 'rbf', 'BoxConstraint', 2); %! assert_equal (numfields (T), 4); %! assert_equal (T.KernelFunction, 'rbf'); %! assert_equal (T.BoxConstraint, 2); %!test # a name the learner does not know is not refused here %! T = templateSVM ('NoSuchOption', 42); %! assert_equal (T.NoSuchOption, 42); ## Test input validation %!error ... %! templateSVM ('KernelScale') %!error ... %! templateSVM (42, 1) %!error ... %! templateSVM ('not a name', 1) statistics-release-2.1.2/inst/Machine_Learning/templateTree.m000066400000000000000000000067201527311104300242770ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{T} =} templateTree () ## @deftypefnx {statistics} {@var{T} =} templateTree (@var{name}, @var{value}) ## ## Create a template for a binary decision tree. ## ## @code{@var{T} = templateTree ()} returns a template carrying the default ## options of @code{ClassificationTree}. A template names a learner and ## the options it is to be fitted with, without fitting anything: it is given ## to a function that fits many models, such as @code{fitcecoc}, which uses it ## for every binary learner it trains. ## ## @code{@var{T} = templateTree (@var{name}, @var{value})} also stores ## the given options. They are the name-value arguments of ## @code{fitctree}, and any of them may be given here instead. ## ## @example ## @group ## T = templateTree ('MaxNumSplits', 5, 'MinLeafSize', 3); ## Mdl = fitcecoc (X, Y, 'Learners', T); ## @end group ## @end example ## ## @var{T} is a structure carrying @qcode{Method}, @qcode{Type} and one ## field per option given. ## ## @subheading Deviation from MATLAB ## ## MATLAB returns an object of a class whose name we cannot use, which has no ## public properties and one method this package declines package wide, so a ## structure carries everything a user can observe. This is what ## @qcode{ModelParameters} already does throughout the package. ## ## An option name is not checked here. @code{ClassificationTree} owns ## the list of options it takes and checks it when the template is used, so a ## name it does not know is refused then rather than now. ## ## @seealso{fitcecoc, ClassificationTree, fitctree} ## @end deftypefn function T = templateTree (varargin) [T, errmsg] = templateStruct ('Tree', varargin); if (! isempty (errmsg)) error ("templateTree: %s", errmsg); endif endfunction ## Tests %!test # the default template names its learner and nothing else %! T = templateTree (); %! assert_equal (class (T), 'struct'); %! assert_equal (T.Method, 'Tree'); %! assert_equal (T.Type, 'classification'); %! assert_equal (numfields (T), 2); %!test # an option given is stored under its own name, as it stands %! T = templateTree ('MaxNumSplits', 5, 'MinLeafSize', 3); %! assert_equal (numfields (T), 4); %! assert_equal (T.MaxNumSplits, 5); %! assert_equal (T.MinLeafSize, 3); %!test # a name the learner does not know is not refused here %! T = templateTree ('NoSuchOption', 42); %! assert_equal (T.NoSuchOption, 42); ## Test input validation %!error ... %! templateTree ('KernelScale') %!error ... %! templateTree (42, 1) %!error ... %! templateTree ('not a name', 1) statistics-release-2.1.2/inst/Markov_Models/000077500000000000000000000000001527311104300210405ustar00rootroot00000000000000statistics-release-2.1.2/inst/Markov_Models/doc-cache000066400000000000000000000372501527311104300226000ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 5 # name: # type: sq_string # elements: 1 # length: 9 hmmdecode # name: # type: sq_string # elements: 1 # length: 2433 statistics: pstates = hmmdecode (sequence, transprob, outprob) statistics: [pstates, logpseq] = hmmdecode (...) statistics: [pstates, logpseq, fs, bs, s] = hmmdecode (...) statistics: [...] = hmmdecode (..., "symbols", symbols) Posterior state probabilities of a hidden Markov model. Calculate the posterior state probabilities of the sequence sequence from a hidden Markov model. The posterior state probabilities are the conditional probabilities of being in each state given the whole observed sequence. The model assumes that the generation starts in state 1 at step 0 but does not include step 0 in the sequence. Arguments sequence is a vector of length len of given outputs. The outputs must be integers ranging from 1 to columns (outprob). transprob is the matrix of transition probabilities of the states. transprob(i, j) is the probability of a transition to state j given state i. outprob is the matrix of output probabilities. outprob(i, j) is the probability of generating output j given state i. Return values pstates is the matrix of posterior state probabilities. It has one row for each state and one column for each element of sequence. pstates(i, j) is the conditional probability that the model is in state i when it generates the j-th output of sequence, given that sequence is emitted. logpseq is the logarithm of the probability of the sequence sequence. fs and bs are the scaled forward and backward probabilities, respectively, and s is the vector of scale factors used to keep the computation numerically stable. If "symbols" is specified, then sequence is expected to be a sequence of the elements of symbols instead of integers ranging from 1 to columns (outprob). symbols can be a cell array. Examples transprob = [0.8, 0.2; 0.4, 0.6]; outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; [sequence, states] = hmmgenerate (25, transprob, outprob); pstates = hmmdecode (sequence, transprob, outprob); symbols = {"A", "B", "C"}; [sequence, states] = hmmgenerate (25, transprob, outprob, ... "symbols", symbols); pstates = hmmdecode (sequence, transprob, outprob, "symbols", symbols); References Wendy L. Martinez and Angel R. Martinez. Computational Statistics Handbook with MATLAB. Appendix E, pages 547-557, Chapman & Hall/CRC, 2001. Lawrence R. Rabiner. A Tutorial on Hidden Markov Models and Selected Applications in Speech Recognition. Proceedings of the IEEE, 77(2), pages 257-286, February 1989. # name: # type: sq_string # elements: 1 # length: 55 Posterior state probabilities of a hidden Markov model. # name: # type: sq_string # elements: 1 # length: 11 hmmestimate # name: # type: sq_string # elements: 1 # length: 3659 statistics: [transprobest, outprobest] = hmmestimate (sequence, states) statistics: [...] = hmmestimate (..., "statenames", statenames) statistics: [...] = hmmestimate (..., "symbols", symbols) statistics: [...] = hmmestimate (..., "pseudotransitions", pseudotransitions) statistics: [...] = hmmestimate (..., "pseudoemissions", pseudoemissions) Estimation of a hidden Markov model for a given sequence. Estimate the matrix of transition probabilities and the matrix of output probabilities of a given sequence of outputs and states generated by a hidden Markov model. The transition probabilities are estimated by counting the transitions that actually occur between consecutive states in states; the output probabilities are estimated from the outputs emitted by each state. Arguments sequence is a vector of a sequence of given outputs. The outputs must be integers ranging from 1 to the number of outputs of the hidden Markov model. states is a vector of the same length as sequence of given states. The states must be integers ranging from 1 to the number of states of the hidden Markov model. Return values transprobest is the matrix of the estimated transition probabilities of the states. transprobest(i, j) is the estimated probability of a transition to state j given state i. outprobest is the matrix of the estimated output probabilities. outprobest(i, j) is the estimated probability of generating output j given state i. If 'symbols' is specified, then sequence is expected to be a sequence of the elements of symbols instead of integers. symbols can be a cell array. If 'statenames' is specified, then states is expected to be a sequence of the elements of statenames instead of integers. statenames can be a cell array. If 'pseudotransitions' is specified then the integer matrix pseudotransitions is used as an initial number of counted transitions. pseudotransitions(i, j) is the initial number of counted transitions from state i to state j. transprobest will have the same size as pseudotransitions. Use this if you have transitions that are very unlikely to occur. If 'pseudoemissions' is specified then the integer matrix pseudoemissions is used as an initial number of counted outputs. pseudoemissions(i, j) is the initial number of counted outputs j given state i. If 'pseudotransitions' is also specified then the number of rows of pseudoemissions must be the same as the number of rows of pseudotransitions. outprobest will have the same size as pseudoemissions. Use this if you have outputs or states that are very unlikely to occur. Examples transprob = [0.8, 0.2; 0.4, 0.6]; outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; [sequence, states] = hmmgenerate (25, transprob, outprob); [transprobest, outprobest] = hmmestimate (sequence, states) symbols = {"A", "B", "C"}; statenames = {"One", "Two"}; [sequence, states] = hmmgenerate (25, transprob, outprob, ... "symbols", symbols, ... "statenames", statenames); [transprobest, outprobest] = hmmestimate (sequence, states, ... "symbols", symbols, ... "statenames", statenames) pseudotransitions = [8, 2; 4, 6]; pseudoemissions = [2, 4, 4; 7, 2, 1]; [sequence, states] = hmmgenerate (25, transprob, outprob); [transprobest, outprobest] = hmmestimate (sequence, states, ... "pseudotransitions", pseudotransitions, ... "pseudoemissions", pseudoemissions) References Wendy L. Martinez and Angel R. Martinez. Computational Statistics Handbook with MATLAB. Appendix E, pages 547-557, Chapman & Hall/CRC, 2001. Lawrence R. Rabiner. A Tutorial on Hidden Markov Models and Selected Applications in Speech Recognition. Proceedings of the IEEE, 77(2), pages 257-286, February 1989. # name: # type: sq_string # elements: 1 # length: 57 Estimation of a hidden Markov model for a given sequence. # name: # type: sq_string # elements: 1 # length: 11 hmmgenerate # name: # type: sq_string # elements: 1 # length: 2051 statistics: [sequence, states] = hmmgenerate (len, transprob, outprob) statistics: [...] = hmmgenerate (..., "symbols", symbols) statistics: [...] = hmmgenerate (..., "statenames", statenames) Output sequence and hidden states of a hidden Markov model. Generate an output sequence and hidden states of a hidden Markov model. The model starts in state 1 at step 0 but will not include step 0 in the generated states and sequence. Arguments len is the number of steps to generate. sequence and states will have len entries each. transprob is the matrix of transition probabilities of the states. transprob(i, j) is the probability of a transition to state j given state i. outprob is the matrix of output probabilities. outprob(i, j) is the probability of generating output j given state i. Return values sequence is a vector of length len of the generated outputs. The outputs are integers ranging from 1 to columns (outprob). states is a vector of length len of the generated hidden states. The states are integers ranging from 1 to columns (transprob). If "symbols" is specified, then the elements of symbols are used for the output sequence instead of integers ranging from 1 to columns (outprob). symbols can be a cell array. If "statenames" is specified, then the elements of statenames are used for the states instead of integers ranging from 1 to columns (transprob). statenames can be a cell array. Examples transprob = [0.8, 0.2; 0.4, 0.6]; outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; [sequence, states] = hmmgenerate (25, transprob, outprob) symbols = {"A", "B", "C"}; statenames = {"One", "Two"}; [sequence, states] = hmmgenerate (25, transprob, outprob, ... "symbols", symbols, ... "statenames", statenames) References Wendy L. Martinez and Angel R. Martinez. Computational Statistics Handbook with MATLAB. Appendix E, pages 547-557, Chapman & Hall/CRC, 2001. Lawrence R. Rabiner. A Tutorial on Hidden Markov Models and Selected Applications in Speech Recognition. Proceedings of the IEEE, 77(2), pages 257-286, February 1989. # name: # type: sq_string # elements: 1 # length: 59 Output sequence and hidden states of a hidden Markov model. # name: # type: sq_string # elements: 1 # length: 8 hmmtrain # name: # type: sq_string # elements: 1 # length: 4226 statistics: [esttr, estout] = hmmtrain (sequence, transguess, outguess) statistics: [...] = hmmtrain (..., "algorithm", algorithm) statistics: [...] = hmmtrain (..., "symbols", symbols) statistics: [...] = hmmtrain (..., "tolerance", tol) statistics: [...] = hmmtrain (..., "maxiterations", maxiter) statistics: [...] = hmmtrain (..., "pseudotransitions", pseudotransitions) statistics: [...] = hmmtrain (..., "pseudoemissions", pseudoemissions) statistics: [...] = hmmtrain (..., "verbose", vflag) Estimate the parameters of a hidden Markov model from emitted sequences. Given one or more observed output sequences and initial guesses for the transition and output probability matrices, hmmtrain finds maximum likelihood estimates of the two matrices using the Baum-Welch algorithm (the default) or Viterbi training. The model assumes that the generation starts in state 1 at step 0 but does not include step 0 in the sequence. Arguments sequence is a vector of a sequence of given outputs, or, for training from several sequences, a cell array of such vectors or a matrix whose rows are individual sequences. The outputs must be integers ranging from 1 to columns (outguess). transguess is the initial guess for the matrix of transition probabilities. transguess(i, j) is the probability of a transition to state j given state i. outguess is the initial guess for the matrix of output probabilities. outguess(i, j) is the probability of generating output j given state i. Return values esttr is the estimated matrix of transition probabilities. estout is the estimated matrix of output probabilities. Name-Value pair arguments "algorithm" selects the training algorithm, either "BaumWelch" (default) or "Viterbi". "BaumWelch" performs the standard forward-backward re-estimation and is recommended for most uses. "Viterbi" performs segmental (hard) re-estimation from the most likely state path of each sequence; it is faster but only approximates the maximum-likelihood estimate. "symbols" specifies the possible outputs. If given, sequence is expected to hold the elements of symbols instead of integers. symbols can be a cell array. "tolerance" is the convergence tolerance (default 1e-6). The algorithm terminates when the change in the log-likelihood and in both estimated matrices falls below tol. "maxiterations" is the maximum number of iterations (default 500). A warning is issued if the algorithm has not converged within this many iterations. "pseudotransitions" and "pseudoemissions" supply pseudo-count matrices for Viterbi training, used to keep transitions or outputs that are very unlikely to occur from collapsing to zero probability. "verbose", when true, prints the log-likelihood and the change in the estimates at each iteration. Examples transprob = [0.8, 0.2; 0.4, 0.6]; outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; sequence = hmmgenerate (100, transprob, outprob); transguess = [0.6, 0.4; 0.5, 0.5]; outguess = [0.3, 0.3, 0.4; 0.5, 0.3, 0.2]; [esttr, estout] = hmmtrain (sequence, transguess, outguess); Two results of Viterbi training differ from MATLAB's, deliberately. Given several sequences, the counts of every sequence are pooled and normalized once, so a sequence contributes in proportion to its length. MATLAB normalizes each sequence separately and averages the results, which weights an eight-symbol sequence as heavily as a twenty-four-symbol one and is not the maximum likelihood estimate. Its own Baum-Welch pools expected counts, as both algorithms do here. Given "pseudotransitions" or "pseudoemissions", the pseudo-counts are added to the counted transitions and outputs, once per iteration, before the row is normalized. MATLAB does the same on its first iteration; from its second it adds them to an estimate that has already been normalized, so its iterate mixes counts with probabilities and is no longer a count matrix of any state path. References Wendy L. Martinez and Angel R. Martinez. Computational Statistics Handbook with MATLAB. Appendix E, pages 547-557, Chapman & Hall/CRC, 2001. Lawrence R. Rabiner. A Tutorial on Hidden Markov Models and Selected Applications in Speech Recognition. Proceedings of the IEEE, 77(2), pages 257-286, February 1989. # name: # type: sq_string # elements: 1 # length: 72 Estimate the parameters of a hidden Markov model from emitted sequences. # name: # type: sq_string # elements: 1 # length: 10 hmmviterbi # name: # type: sq_string # elements: 1 # length: 2174 statistics: vpath = hmmviterbi (sequence, transprob, outprob) statistics: vpath = hmmviterbi (..., "symbols", symbols) statistics: vpath = hmmviterbi (..., "statenames", statenames) Viterbi path of a hidden Markov model. Use the Viterbi algorithm to find the Viterbi path of a hidden Markov model given a sequence of outputs. The model assumes that the generation starts in state 1 at step 0 but does not include step 0 in the generated states and sequence. Arguments sequence is the vector of length len of given outputs. The outputs must be integers ranging from 1 to columns (outprob). transprob is the matrix of transition probabilities of the states. transprob(i, j) is the probability of a transition to state j given state i. outprob is the matrix of output probabilities. outprob(i, j) is the probability of generating output j given state i. Return values vpath is the vector of the same length as sequence of the estimated hidden states. The states are integers ranging from 1 to columns (transprob). If "symbols" is specified, then sequence is expected to be a sequence of the elements of symbols instead of integers ranging from 1 to columns (outprob). symbols can be a cell array. If "statenames" is specified, then the elements of statenames are used for the states in vpath instead of integers ranging from 1 to columns (transprob). statenames can be a cell array. Examples transprob = [0.8, 0.2; 0.4, 0.6]; outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; [sequence, states] = hmmgenerate (25, transprob, outprob); vpath = hmmviterbi (sequence, transprob, outprob); symbols = {"A", "B", "C"}; statenames = {"One", "Two"}; [sequence, states] = hmmgenerate (25, transprob, outprob, ... "symbols", symbols, "statenames", statenames); vpath = hmmviterbi (sequence, transprob, outprob, ... "symbols", symbols, "statenames", statenames); References Wendy L. Martinez and Angel R. Martinez. Computational Statistics Handbook with MATLAB. Appendix E, pages 547-557, Chapman & Hall/CRC, 2001. Lawrence R. Rabiner. A Tutorial on Hidden Markov Models and Selected Applications in Speech Recognition. Proceedings of the IEEE, 77(2), pages 257-286, February 1989. # name: # type: sq_string # elements: 1 # length: 38 Viterbi path of a hidden Markov model. statistics-release-2.1.2/inst/Markov_Models/hmmdecode.m000066400000000000000000000265101527311104300231470ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{pstates} =} hmmdecode (@var{sequence}, @var{transprob}, @var{outprob}) ## @deftypefnx {statistics} {[@var{pstates}, @var{logpseq}] =} hmmdecode (@dots{}) ## @deftypefnx {statistics} {[@var{pstates}, @var{logpseq}, @var{fs}, @var{bs}, @var{s}] =} hmmdecode (@dots{}) ## @deftypefnx {statistics} {[@dots{}] =} hmmdecode (@dots{}, @code{"symbols"}, @var{symbols}) ## ## Posterior state probabilities of a hidden Markov model. ## ## Calculate the posterior state probabilities of the sequence @var{sequence} ## from a hidden Markov model. The posterior state probabilities are the ## conditional probabilities of being in each state given the whole observed ## sequence. The model assumes that the generation starts in state @code{1} ## at step @code{0} but does not include step @code{0} in the sequence. ## ## @subheading Arguments ## ## @itemize @bullet ## @item ## @var{sequence} is a vector of length @var{len} of given outputs. The ## outputs must be integers ranging from @code{1} to @code{columns (outprob)}. ## ## @item ## @var{transprob} is the matrix of transition probabilities of the states. ## @code{transprob(i, j)} is the probability of a transition to state ## @code{j} given state @code{i}. ## ## @item ## @var{outprob} is the matrix of output probabilities. ## @code{outprob(i, j)} is the probability of generating output @code{j} ## given state @code{i}. ## @end itemize ## ## @subheading Return values ## ## @itemize @bullet ## @item ## @var{pstates} is the matrix of posterior state probabilities. It has one ## row for each state and one column for each element of @var{sequence}. ## @code{pstates(i, j)} is the conditional probability that the model is in ## state @code{i} when it generates the @code{j}-th output of @var{sequence}, ## given that @var{sequence} is emitted. ## ## @item ## @var{logpseq} is the logarithm of the probability of the sequence ## @var{sequence}. ## ## @item ## @var{fs} and @var{bs} are the scaled forward and backward probabilities, ## respectively, and @var{s} is the vector of scale factors used to keep the ## computation numerically stable. ## @end itemize ## ## If @code{"symbols"} is specified, then @var{sequence} is expected to be a ## sequence of the elements of @var{symbols} instead of integers ranging from ## @code{1} to @code{columns (outprob)}. @var{symbols} can be a cell array. ## ## @subheading Examples ## ## @example ## @group ## transprob = [0.8, 0.2; 0.4, 0.6]; ## outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; ## [sequence, states] = hmmgenerate (25, transprob, outprob); ## pstates = hmmdecode (sequence, transprob, outprob); ## @end group ## ## @group ## symbols = @{"A", "B", "C"@}; ## [sequence, states] = hmmgenerate (25, transprob, outprob, ... ## "symbols", symbols); ## pstates = hmmdecode (sequence, transprob, outprob, "symbols", symbols); ## @end group ## @end example ## ## @subheading References ## ## @enumerate ## @item ## Wendy L. Martinez and Angel R. Martinez. @cite{Computational Statistics ## Handbook with MATLAB}. Appendix E, pages 547-557, Chapman & Hall/CRC, ## 2001. ## ## @item ## Lawrence R. Rabiner. A Tutorial on Hidden Markov Models and Selected ## Applications in Speech Recognition. @cite{Proceedings of the IEEE}, ## 77(2), pages 257-286, February 1989. ## @end enumerate ## @end deftypefn function [pstates, logpseq, fs, bs, s] = hmmdecode (sequence, transprob, ... outprob, varargin) # Check arguments if (nargin < 3 || mod (numel (varargin), 2) != 0) print_usage (); endif if (! ismatrix (transprob)) error ("hmmdecode: transprob must be a non-empty numeric matrix."); endif if (! ismatrix (outprob)) error ("hmmdecode: outprob must be a non-empty numeric matrix."); endif # nstate is the number of states of the hidden Markov model nstate = rows (transprob); # noutput is the number of different outputs that the hidden Markov model # can generate noutput = columns (outprob); # Check whether transprob and outprob are feasible for a hidden Markov model if (columns (transprob) != nstate) error ("hmmdecode: transprob must be a square matrix."); endif if (rows (outprob) != nstate) error (strcat ("hmmdecode: outprob must have the same number of", ... " rows as transprob.")); endif # Flag for symbols usesym = false; # Process varargin for i = 1:2:numel (varargin) # There must be an identifier: 'symbols' if (! ischar (varargin{i})) print_usage (); endif # Upper case is also fine lowerarg = lower (varargin{i}); if (strcmp (lowerarg, 'symbols')) if (numel (varargin{i + 1}) != noutput) error (strcat ("hmmdecode: number of symbols does not match", ... " number of possible outputs.")); endif usesym = true; # Use the following argument as symbols symbols = varargin{i + 1}; else error (strcat ("hmmdecode: expected 'symbols'", ... sprintf (" but found '%s'.", varargin{i}))); endif endfor len = numel (sequence); # Transform sequence from symbols to integers if necessary if (usesym) # sequenceint is used to build the transformed sequence sequenceint = zeros (1, len); for i = 1:noutput # Search for symbols(i) in the sequence; isequal will have 1 at # corresponding indices; i is the right integer for that symbol isequal = ismember (sequence, symbols(i)); # We do not want to change sequenceint if the symbol appears a second # time in symbols if (any ((sequenceint == 0) & (isequal == 1))) isequal *= i; sequenceint += isequal; endif endfor if (! all (sequenceint) && len > 0) index = max ((sequenceint == 0) .* (1:len)); error (strcat ("hmmdecode: sequence(", int2str (index), ... ") not in symbols.")); endif sequence = sequenceint; else if (! isvector (sequence) && ! isempty (sequence)) error ("hmmdecode: sequence must be a vector."); endif if (! all (ismember (sequence, 1:noutput))) index = max ((ismember (sequence, 1:noutput) == 0) .* (1:len)); error (strcat ("hmmdecode: sequence(", int2str (index), ... ") out of range.")); endif endif # Each row in transprob and outprob should contain probabilities # => scale so that the sum is 1. A zero row remains zero. # - for transprob ts = sum (transprob, 2); ts(ts == 0) = 1; transprob = transprob ./ ts; # - for outprob os = sum (outprob, 2); os(os == 0) = 1; outprob = outprob ./ os; # Prepend a dummy output so that the forward and backward recursions have a # clean starting column representing the initial state 1 at step 0. The # dummy column is stripped from PSTATES before returning. seq = [noutput + 1, sequence(:)']; L = len + 1; # Scaled forward probabilities. Column 1 holds the initial distribution: # the model starts in state 1 with probability 1. fs = zeros (nstate, L); fs(1, 1) = 1; s = ones (1, L); for count = 2:L fs(:, count) = outprob(:, seq(count)) .* (transprob' * fs(:, count - 1)); # The scale factor normalizes each forward column to sum to 1 s(count) = sum (fs(:, count)); fs(:, count) = fs(:, count) ./ s(count); endfor # Scaled backward probabilities using the same scale factors bs = ones (nstate, L); for count = L - 1:-1:1 bs(:, count) = (transprob * (bs(:, count + 1) .* ... outprob(:, seq(count + 1)))) ./ s(count + 1); endfor # The log probability of the sequence is the sum of the log scale factors logpseq = sum (log (s)); # Posterior state probabilities; strip the dummy starting column pstates = fs .* bs; pstates(:, 1) = []; endfunction %!demo %! ## Posterior probability of each state at every step of an observed sequence. %! %! transprob = [0.95, 0.05; 0.10, 0.90]; %! outprob = [1/6, 1/6, 1/6, 1/6, 1/6, 1/6; 1/10, 1/10, 1/10, 1/10, 1/10, 1/2]; %! sequence = hmmgenerate (10, transprob, outprob); %! [pstates, logpseq] = hmmdecode (sequence, transprob, outprob) %!test %! transprob = [0.8, 0.2; 0.4, 0.6]; %! outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; %! sequence = [1, 2, 1, 1, 1, 2, 2, 1, 2, 3]; %! pstates = hmmdecode (sequence, transprob, outprob); %! assert_equal (size (pstates), [2, 10]); %! assert_equal (all (abs (sum (pstates, 1) - 1) < 1e-10), true); %! assert_equal (all (pstates(:) >= 0 & pstates(:) <= 1), true); %!test %! transprob = [0.8, 0.2; 0.4, 0.6]; %! outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; %! sequence = [1, 2, 1, 1, 1, 2, 2, 1, 2, 3]; %! [pstates, logpseq] = hmmdecode (sequence, transprob, outprob); %! ## Independent brute-force forward algorithm for the log probability %! nstate = 2; %! alpha = transprob(1, :) .* outprob(:, sequence(1))'; %! for t = 2:numel (sequence) %! alpha = (alpha * transprob) .* outprob(:, sequence(t))'; %! endfor %! assert_equal (logpseq, log (sum (alpha)), 1e-10); %!test %! ## Symbols form must match the integer form %! transprob = [0.8, 0.2; 0.4, 0.6]; %! outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; %! sequence = [1, 2, 1, 1, 1, 2, 2, 1, 2, 3]; %! symbseq = {'A', 'B', 'A', 'A', 'A', 'B', 'B', 'A', 'B', 'C'}; %! p1 = hmmdecode (sequence, transprob, outprob); %! p2 = hmmdecode (symbseq, transprob, outprob, 'symbols', {'A', 'B', 'C'}); %! assert_equal (p1, p2, 1e-12); %!test %! ## Scaled forward/backward reproduce the posterior gamma %! transprob = [0.9, 0.1; 0.3, 0.7]; %! outprob = [0.5, 0.5; 0.1, 0.9]; %! sequence = [1, 2, 2, 1, 2]; %! [pstates, logpseq, fs, bs, s] = hmmdecode (sequence, transprob, outprob); %! recovered = fs .* bs; %! recovered(:, 1) = []; %! assert_equal (recovered, pstates, 1e-12); %! assert_equal (logpseq, sum (log (s)), 1e-12); %!test %! ## Empty sequence: no columns in the posterior, unit probability %! transprob = [0.8, 0.2; 0.4, 0.6]; %! outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; %! [pstates, logpseq] = hmmdecode ([], transprob, outprob); %! assert_equal (size (pstates), [2, 0]); %! assert_equal (logpseq, 0, 1e-12); %!error hmmdecode ([1, 2]) %!error ... %! hmmdecode ([1, 2], [0.8, 0.2; 0.4, 0.6; 0.1, 0.9], [0.5, 0.5; 0.1, 0.9]) %!error ... %! hmmdecode ([1, 2], [0.8, 0.2; 0.4, 0.6], [0.5, 0.5]) %!error ... %! hmmdecode ([1, 5], [0.8, 0.2; 0.4, 0.6], [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]) %!error ... %! hmmdecode ([1, 2], [0.8, 0.2; 0.4, 0.6], [0.2, 0.4, 0.4; 0.7, 0.2, 0.1], ... %! 'symbols', {'A', 'B'}) statistics-release-2.1.2/inst/Markov_Models/hmmestimate.m000066400000000000000000000355351527311104300235460ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{transprobest}, @var{outprobest}] =} hmmestimate (@var{sequence}, @var{states}) ## @deftypefnx {statistics} {[@dots{}] =} hmmestimate (@dots{}, @code{"statenames"}, @var{statenames}) ## @deftypefnx {statistics} {[@dots{}] =} hmmestimate (@dots{}, @code{"symbols"}, @var{symbols}) ## @deftypefnx {statistics} {[@dots{}] =} hmmestimate (@dots{}, @code{"pseudotransitions"}, @var{pseudotransitions}) ## @deftypefnx {statistics} {[@dots{}] =} hmmestimate (@dots{}, @code{"pseudoemissions"}, @var{pseudoemissions}) ## ## Estimation of a hidden Markov model for a given sequence. ## ## Estimate the matrix of transition probabilities and the matrix of output ## probabilities of a given sequence of outputs and states generated by a ## hidden Markov model. The transition probabilities are estimated by counting ## the transitions that actually occur between consecutive states in ## @var{states}; the output probabilities are estimated from the outputs ## emitted by each state. ## ## @subheading Arguments ## ## @itemize @bullet ## @item ## @var{sequence} is a vector of a sequence of given outputs. The outputs ## must be integers ranging from @code{1} to the number of outputs of the ## hidden Markov model. ## ## @item ## @var{states} is a vector of the same length as @var{sequence} of given ## states. The states must be integers ranging from @code{1} to the number ## of states of the hidden Markov model. ## @end itemize ## ## @subheading Return values ## ## @itemize @bullet ## @item ## @var{transprobest} is the matrix of the estimated transition ## probabilities of the states. @code{transprobest(i, j)} is the estimated ## probability of a transition to state @code{j} given state @code{i}. ## ## @item ## @var{outprobest} is the matrix of the estimated output probabilities. ## @code{outprobest(i, j)} is the estimated probability of generating ## output @code{j} given state @code{i}. ## @end itemize ## ## If @code{'symbols'} is specified, then @var{sequence} is expected to be a ## sequence of the elements of @var{symbols} instead of integers. ## @var{symbols} can be a cell array. ## ## If @code{'statenames'} is specified, then @var{states} is expected to be ## a sequence of the elements of @var{statenames} instead of integers. ## @var{statenames} can be a cell array. ## ## If @code{'pseudotransitions'} is specified then the integer matrix ## @var{pseudotransitions} is used as an initial number of counted ## transitions. @code{pseudotransitions(i, j)} is the initial number of ## counted transitions from state @code{i} to state @code{j}. ## @var{transprobest} will have the same size as @var{pseudotransitions}. ## Use this if you have transitions that are very unlikely to occur. ## ## If @code{'pseudoemissions'} is specified then the integer matrix ## @var{pseudoemissions} is used as an initial number of counted outputs. ## @code{pseudoemissions(i, j)} is the initial number of counted outputs ## @code{j} given state @code{i}. If @code{'pseudotransitions'} is also ## specified then the number of rows of @var{pseudoemissions} must be the ## same as the number of rows of @var{pseudotransitions}. @var{outprobest} ## will have the same size as @var{pseudoemissions}. Use this if you have ## outputs or states that are very unlikely to occur. ## ## @subheading Examples ## ## @example ## @group ## transprob = [0.8, 0.2; 0.4, 0.6]; ## outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; ## [sequence, states] = hmmgenerate (25, transprob, outprob); ## [transprobest, outprobest] = hmmestimate (sequence, states) ## @end group ## ## @group ## symbols = @{"A", "B", "C"@}; ## statenames = @{"One", "Two"@}; ## [sequence, states] = hmmgenerate (25, transprob, outprob, ... ## "symbols", symbols, ... ## "statenames", statenames); ## [transprobest, outprobest] = hmmestimate (sequence, states, ... ## "symbols", symbols, ... ## "statenames", statenames) ## @end group ## ## @group ## pseudotransitions = [8, 2; 4, 6]; ## pseudoemissions = [2, 4, 4; 7, 2, 1]; ## [sequence, states] = hmmgenerate (25, transprob, outprob); ## [transprobest, outprobest] = hmmestimate (sequence, states, ... ## "pseudotransitions", pseudotransitions, ... ## "pseudoemissions", pseudoemissions) ## @end group ## @end example ## ## @subheading References ## ## @enumerate ## @item ## Wendy L. Martinez and Angel R. Martinez. @cite{Computational Statistics ## Handbook with MATLAB}. Appendix E, pages 547-557, Chapman & Hall/CRC, ## 2001. ## ## @item ## Lawrence R. Rabiner. A Tutorial on Hidden Markov Models and Selected ## Applications in Speech Recognition. @cite{Proceedings of the IEEE}, ## 77(2), pages 257-286, February 1989. ## @end enumerate ## @end deftypefn function [transprobest, outprobest] = hmmestimate (sequence, states, varargin) # Check arguments if (nargin < 2 || mod (length (varargin), 2) != 0) print_usage (); endif len = length (sequence); if (length (states) != len) error ("hmmestimate: sequence and states must have equal length"); endif # Flag for symbols usesym = false; # Flag for statenames usesn = false; # Variables for return values transprobest = []; outprobest = []; # Process varargin for i = 1:2:length (varargin) # There must be an identifier: 'symbols', 'statenames', # 'pseudotransitions' or 'pseudoemissions' if (! ischar (varargin{i})) print_usage (); endif # Upper case is also fine lowerarg = lower (varargin{i}); if (strcmp (lowerarg, 'symbols')) usesym = true; # Use the following argument as symbols symbols = varargin{i + 1}; # The same for statenames elseif (strcmp (lowerarg, 'statenames')) usesn = true; # Use the following argument as statenames statenames = varargin{i + 1}; elseif (strcmp (lowerarg, 'pseudotransitions')) # Use the following argument as an initial count for transitions transprobest = varargin{i + 1}; if (! ismatrix (transprobest)) error (strcat ("hmmestimate: pseudotransitions must be a", ... " non-empty numeric matrix")); endif if (rows (transprobest) != columns (transprobest)) error ("hmmestimate: pseudotransitions must be a square matrix"); endif elseif (strcmp (lowerarg, 'pseudoemissions')) # Use the following argument as an initial count for outputs outprobest = varargin{i + 1}; if (! ismatrix (outprobest)) error (strcat ("hmmestimate: pseudoemissions must be a non-empty", ... " numeric matrix")); endif else error (strcat ("hmmestimate: expected 'symbols', 'statenames',", ... " 'pseudotransitions' or 'pseudoemissions' but", ... sprintf (" found '%s'", varargin{i}))); endif endfor # Transform sequence from symbols to integers if necessary if (usesym) # sequenceint is used to build the transformed sequence sequenceint = zeros (1, len); for i = 1:length (symbols) # Search for symbols(i) in the sequence, isequal will have 1 at # corresponding indices; i is the right integer for that symbol isequal = ismember (sequence, symbols(i)); # We do not want to change sequenceint if the symbol appears a second # time in symbols if (any ((sequenceint == 0) & (isequal == 1))) isequal *= i; sequenceint += isequal; endif endfor if (! all (sequenceint)) index = max ((sequenceint == 0) .* (1:len)); error (strcat ("hmmestimate: sequence(", int2str (index), ... ") not in symbols")); endif sequence = sequenceint; else if (! isvector (sequence)) error ("hmmestimate: sequence must be a non-empty vector"); endif if (! all (ismember (sequence, 1:max (sequence)))) index = max ((ismember (sequence, 1:max (sequence)) == 0) .* (1:len)); error (strcat ("hmmestimate: sequence(", int2str (index), ... ") not feasible")); endif endif # Transform states from statenames to integers if necessary if (usesn) # statesint is used to build the transformed states statesint = zeros (1, len); for i = 1:length (statenames) # Search for statenames(i) in states, isequal will have 1 at # corresponding indices; i is the right integer for that statename isequal = ismember (states, statenames(i)); # We do not want to change statesint if the statename appears a second # time in statenames if (any ((statesint == 0) & (isequal == 1))) isequal *= i; statesint += isequal; endif endfor if (! all (statesint)) index = max ((statesint == 0) .* (1:len)); error (strcat ("hmmestimate: states(", int2str (index), ... ") not in statenames")); endif states = statesint; else if (! isvector (states)) error ("hmmestimate: states must be a non-empty vector"); endif if (! all (ismember (states, 1:max (states)))) index = max ((ismember (states, 1:max (states)) == 0) .* (1:len)); error (strcat ("hmmestimate: states(", int2str (index), ... ") not feasible")); endif endif # Estimate the number of different states as the max of states nstate = max (states); # Estimate the number of different outputs as the max of sequence noutput = max (sequence); # transprobest is empty if pseudotransitions is not specified if (isempty (transprobest)) # outprobest is not empty if pseudoemissions is specified if (! isempty (outprobest)) if (nstate > rows (outprobest)) error ("hmmestimate: not enough rows in pseudoemissions"); endif # The number of states is specified by pseudoemissions nstate = rows (outprobest); endif transprobest = zeros (nstate, nstate); else if (nstate > rows (transprobest)) error ("hmmestimate: not enough rows in pseudotransitions"); endif # The number of states is given by pseudotransitions nstate = rows (transprobest); endif # outprobest is empty if pseudoemissions is not specified if (isempty (outprobest)) outprobest = zeros (nstate, noutput); else if (noutput > columns (outprobest)) error ("hmmestimate: not enough columns in pseudoemissions"); endif # Number of outputs is specified by pseudoemissions noutput = columns (outprobest); if (rows (outprobest) != nstate) error (strcat ("hmmestimate: pseudoemissions must have the same", ... " number of rows as pseudotransitions")); endif endif # Count the observed transitions between consecutive states and the outputs # emitted from each state. Only transitions that actually occur within the # given state sequence are counted; no transition out of an assumed initial # state is added (matching MATLAB). for i = 1:len # Count the number of outputs for each state output pair outprobest(states(i), sequence(i)) ++; # Count the number of transitions for each consecutive state pair if (i < len) transprobest(states(i), states(i + 1)) ++; endif endfor # transprobest and outprobest contain counted numbers # Each row in transprobest and outprobest should contain estimated # probabilities # => scale so that the sum is 1 # A zero row remains zero # - for transprobest s = sum (transprobest, 2); s(s == 0) = 1; transprobest = transprobest ./ (s * ones (1, nstate)); # - for outprobest s = sum (outprobest, 2); s(s == 0) = 1; outprobest = outprobest ./ (s * ones (1, noutput)); endfunction %!demo %! ## Recover the transition and output matrices of a model from a long %! ## sequence together with its known sequence of hidden states. %! %! transprob = [0.8, 0.2; 0.4, 0.6]; %! outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; %! [sequence, states] = hmmgenerate (1000, transprob, outprob); %! [transest, outest] = hmmestimate (sequence, states) %!test %! sequence = [1, 2, 1, 1, 1, 2, 2, 1, 2, 3, 3, ... %! 3, 3, 2, 3, 1, 1, 1, 1, 3, 3, 2, 3, 1, 3]; %! states = [1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, ... %! 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1]; %! [transprobest, outprobest] = hmmestimate (sequence, states); %! expectedtransprob = [0.88235, 0.11765; 0.28571, 0.71429]; %! expectedoutprob = [0.16667, 0.33333, 0.50000; 1.00000, 0.00000, 0.00000]; %! assert_equal (transprobest, expectedtransprob, 0.001); %! assert_equal (outprobest, expectedoutprob, 0.001); %!test %! sequence = {'A', 'B', 'A', 'A', 'A', 'B', 'B', 'A', 'B', 'C', 'C', 'C', ... %! 'C', 'B', 'C', 'A', 'A', 'A', 'A', 'C', 'C', 'B', 'C', 'A', 'C'}; %! states = {'One', 'One', 'Two', 'Two', 'Two', 'One', 'One', 'One', 'One', ... %! 'One', 'One', 'One', 'One', 'One', 'One', 'Two', 'Two', 'Two', ... %! 'Two', 'One', 'One', 'One', 'One', 'One', 'One'}; %! symbols = {'A', 'B', 'C'}; %! statenames = {'One', 'Two'}; %! [transprobest, outprobest] = hmmestimate (sequence, states, 'symbols', ... %! symbols, 'statenames', statenames); %! expectedtransprob = [0.88235, 0.11765; 0.28571, 0.71429]; %! expectedoutprob = [0.16667, 0.33333, 0.50000; 1.00000, 0.00000, 0.00000]; %! assert_equal (transprobest, expectedtransprob, 0.001); %! assert_equal (outprobest, expectedoutprob, 0.001); %!test %! sequence = [1, 2, 1, 1, 1, 2, 2, 1, 2, 3, 3, 3, ... %! 3, 2, 3, 1, 1, 1, 1, 3, 3, 2, 3, 1, 3]; %! states = [1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, ... %! 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1]; %! pseudotransitions = [8, 2; 4, 6]; %! pseudoemissions = [2, 4, 4; 7, 2, 1]; %! [transprobest, outprobest] = hmmestimate (sequence, states, ... %! 'pseudotransitions', pseudotransitions, 'pseudoemissions', pseudoemissions); %! expectedtransprob = [0.851852, 0.148148; 0.352941, 0.647059]; %! expectedoutprob = [0.178571, 0.357143, 0.464286; ... %! 0.823529, 0.117647, 0.058824]; %! assert_equal (transprobest, expectedtransprob, 0.001); %! assert_equal (outprobest, expectedoutprob, 0.001); statistics-release-2.1.2/inst/Markov_Models/hmmgenerate.m000066400000000000000000000227131527311104300235170ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{sequence}, @var{states}] =} hmmgenerate (@var{len}, @var{transprob}, @var{outprob}) ## @deftypefnx {statistics} {[@dots{}] =} hmmgenerate (@dots{}, @code{"symbols"}, @var{symbols}) ## @deftypefnx {statistics} {[@dots{}] =} hmmgenerate (@dots{}, @code{"statenames"}, @var{statenames}) ## ## Output sequence and hidden states of a hidden Markov model. ## ## Generate an output sequence and hidden states of a hidden Markov model. ## The model starts in state @code{1} at step @code{0} but will not include ## step @code{0} in the generated states and sequence. ## ## @subheading Arguments ## ## @itemize @bullet ## @item ## @var{len} is the number of steps to generate. @var{sequence} and ## @var{states} will have @var{len} entries each. ## ## @item ## @var{transprob} is the matrix of transition probabilities of the states. ## @code{transprob(i, j)} is the probability of a transition to state ## @code{j} given state @code{i}. ## ## @item ## @var{outprob} is the matrix of output probabilities. ## @code{outprob(i, j)} is the probability of generating output @code{j} ## given state @code{i}. ## @end itemize ## ## @subheading Return values ## ## @itemize @bullet ## @item ## @var{sequence} is a vector of length @var{len} of the generated ## outputs. The outputs are integers ranging from @code{1} to ## @code{columns (outprob)}. ## ## @item ## @var{states} is a vector of length @var{len} of the generated hidden ## states. The states are integers ranging from @code{1} to ## @code{columns (transprob)}. ## @end itemize ## ## If @code{"symbols"} is specified, then the elements of @var{symbols} are ## used for the output sequence instead of integers ranging from @code{1} to ## @code{columns (outprob)}. @var{symbols} can be a cell array. ## ## If @code{"statenames"} is specified, then the elements of ## @var{statenames} are used for the states instead of integers ranging from ## @code{1} to @code{columns (transprob)}. @var{statenames} can be a cell ## array. ## ## @subheading Examples ## ## @example ## @group ## transprob = [0.8, 0.2; 0.4, 0.6]; ## outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; ## [sequence, states] = hmmgenerate (25, transprob, outprob) ## @end group ## ## @group ## symbols = @{"A", "B", "C"@}; ## statenames = @{"One", "Two"@}; ## [sequence, states] = hmmgenerate (25, transprob, outprob, ... ## "symbols", symbols, ... ## "statenames", statenames) ## @end group ## @end example ## ## @subheading References ## ## @enumerate ## @item ## Wendy L. Martinez and Angel R. Martinez. @cite{Computational Statistics ## Handbook with MATLAB}. Appendix E, pages 547-557, Chapman & Hall/CRC, ## 2001. ## ## @item ## Lawrence R. Rabiner. A Tutorial on Hidden Markov Models and Selected ## Applications in Speech Recognition. @cite{Proceedings of the IEEE}, ## 77(2), pages 257-286, February 1989. ## @end enumerate ## @end deftypefn function [sequence, states] = hmmgenerate (len, transprob, outprob, varargin) # Check arguments if (nargin < 3 || mod (length (varargin), 2) != 0) print_usage (); endif if (! isscalar (len) || len < 0 || round (len) != len) error ("hmmgenerate: len must be a non-negative scalar integer.") endif if (! ismatrix (transprob)) error ("hmmgenerate: transprob must be a non-empty numeric matrix."); endif if (! ismatrix (outprob)) error ("hmmgenerate: outprob must be a non-empty numeric matrix."); endif # nstate is the number of states of the hidden Markov model nstate = rows (transprob); # noutput is the number of different outputs that the hidden Markov model # can generate noutput = columns (outprob); # Check whether transprob and outprob are feasible for a hidden Markov # model if (columns (transprob) != nstate) error ("hmmgenerate: transprob must be a square matrix."); endif if (rows (outprob) != nstate) error (strcat ("hmmgenerate: outprob must have the same number", ... " of rows as transprob.")); endif # Flag for symbols usesym = false; # Flag for statenames usesn = false; # Process varargin for i = 1:2:length (varargin) # There must be an identifier: 'symbols' or 'statenames' if (! ischar (varargin{i})) print_usage (); endif # Upper case is also fine lowerarg = lower (varargin{i}); if (strcmp (lowerarg, 'symbols')) if (length (varargin{i + 1}) != noutput) error (strcat ("hmmgenerate: number of symbols does not match", ... " number of possible outputs.")); endif usesym = true; # Use the following argument as symbols symbols = varargin{i + 1}; # The same for statenames elseif (strcmp (lowerarg, 'statenames')) if (length (varargin{i + 1}) != nstate) error (strcat ("hmmgenerate: number of statenames does not", ... " match number of states.")); endif usesn = true; # Use the following argument as statenames statenames = varargin{i + 1}; else error (strcat ("hmmgenerate: expected 'symbols' or 'statenames'", ... sprintf (" but found '%s'.", varargin{i}))); endif endfor # Each row in transprob and outprob should contain probabilities # => scale so that the sum is 1 # A zero row remains zero # - for transprob s = sum (transprob, 2); s(s == 0) = 1; transprob = transprob ./ repmat (s, 1, nstate); # - for outprob s = sum (outprob, 2); s(s == 0) = 1; outprob = outprob ./ repmat (s, 1, noutput); # Generate sequences of uniformly distributed random numbers between 0 and 1 # - for the state transitions transdraw = rand (1, len); # - for the outputs outdraw = rand (1, len); # Generate the return vectors # They remain unchanged if the according probability row of transprob # and outprob contain, respectively, only zeros sequence = ones (1, len); states = ones (1, len); if (len > 0) # Calculate cumulated probabilities backwards for easy comparison with # the generated random numbers # Cumulated probability in first column must always be 1 # We might have a zero row # - for transprob transprob(:, end:-1:1) = cumsum (transprob(:, end:-1:1), 2); transprob(:, 1) = 1; # - for outprob outprob(:, end:-1:1) = cumsum (outprob(:, end:-1:1), 2); outprob(:, 1) = 1; # cstate is the current state # Start in state 1 but do not include it in the states vector cstate = 1; for i = 1:len # Compare the random number i of transdraw to the cumulated # probability of the state transition and set the transition # accordingly states(i) = sum (transdraw(i) <= transprob(cstate, :)); cstate = states(i); endfor # Compare the random numbers of outdraw to the cumulated probabilities # of the outputs and set the sequence vector accordingly sequence = sum (repmat (outdraw, noutput, 1) <= outprob(states, :)', 1); # Transform default matrices into symbols/statenames if requested if (usesym) sequence = reshape (symbols(sequence), 1, len); endif if (usesn) states = reshape (statenames(states), 1, len); endif endif endfunction %!demo %! ## Generate an output sequence and its hidden states from a two-state model %! ## (a fair and a loaded die). %! %! transprob = [0.95, 0.05; 0.10, 0.90]; %! outprob = [1/6, 1/6, 1/6, 1/6, 1/6, 1/6; 1/10, 1/10, 1/10, 1/10, 1/10, 1/2]; %! [sequence, states] = hmmgenerate (15, transprob, outprob, ... %! "statenames", {"fair", "loaded"}) %!test %! len = 25; %! transprob = [0.8, 0.2; 0.4, 0.6]; %! outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; %! [sequence, states] = hmmgenerate (len, transprob, outprob); %! assert_equal (length (sequence), len); %! assert_equal (length (states), len); %! assert_equal (all ((min (sequence) >= 1)(:)), true); %! assert_equal (all ((max (sequence) <= columns (outprob))(:)), true); %! assert_equal (all ((min (states) >= 1)(:)), true); %! assert_equal (all ((max (states) <= rows (transprob))(:)), true); %!test %! len = 25; %! transprob = [0.8, 0.2; 0.4, 0.6]; %! outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; %! symbols = {'A', 'B', 'C'}; %! statenames = {'One', 'Two'}; %! [sequence, states] = hmmgenerate (len, transprob, outprob, ... %! 'symbols', symbols, 'statenames', statenames); %! assert_equal (length (sequence), len); %! assert_equal (length (states), len); %! assert_equal (all ((strcmp (sequence, 'A') + strcmp (sequence, 'B') + ... %! strcmp (sequence, 'C') == ones (1, len))(:)), true); %! assert_equal (all ((strcmp (states, 'One') + strcmp (states, 'Two') == ones (1, len))(:)), true); statistics-release-2.1.2/inst/Markov_Models/hmmtrain.m000066400000000000000000000535561527311104300230530ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{esttr}, @var{estout}] =} hmmtrain (@var{sequence}, @var{transguess}, @var{outguess}) ## @deftypefnx {statistics} {[@dots{}] =} hmmtrain (@dots{}, @code{"algorithm"}, @var{algorithm}) ## @deftypefnx {statistics} {[@dots{}] =} hmmtrain (@dots{}, @code{"symbols"}, @var{symbols}) ## @deftypefnx {statistics} {[@dots{}] =} hmmtrain (@dots{}, @code{"tolerance"}, @var{tol}) ## @deftypefnx {statistics} {[@dots{}] =} hmmtrain (@dots{}, @code{"maxiterations"}, @var{maxiter}) ## @deftypefnx {statistics} {[@dots{}] =} hmmtrain (@dots{}, @code{"pseudotransitions"}, @var{pseudotransitions}) ## @deftypefnx {statistics} {[@dots{}] =} hmmtrain (@dots{}, @code{"pseudoemissions"}, @var{pseudoemissions}) ## @deftypefnx {statistics} {[@dots{}] =} hmmtrain (@dots{}, @code{"verbose"}, @var{vflag}) ## ## Estimate the parameters of a hidden Markov model from emitted sequences. ## ## Given one or more observed output sequences and initial guesses for the ## transition and output probability matrices, @code{hmmtrain} finds maximum ## likelihood estimates of the two matrices using the Baum-Welch algorithm ## (the default) or Viterbi training. The model assumes that the generation ## starts in state @code{1} at step @code{0} but does not include step ## @code{0} in the sequence. ## ## @subheading Arguments ## ## @itemize @bullet ## @item ## @var{sequence} is a vector of a sequence of given outputs, or, for training ## from several sequences, a cell array of such vectors or a matrix whose rows ## are individual sequences. The outputs must be integers ranging from ## @code{1} to @code{columns (outguess)}. ## ## @item ## @var{transguess} is the initial guess for the matrix of transition ## probabilities. @code{transguess(i, j)} is the probability of a transition ## to state @code{j} given state @code{i}. ## ## @item ## @var{outguess} is the initial guess for the matrix of output probabilities. ## @code{outguess(i, j)} is the probability of generating output @code{j} ## given state @code{i}. ## @end itemize ## ## @subheading Return values ## ## @itemize @bullet ## @item ## @var{esttr} is the estimated matrix of transition probabilities. ## ## @item ## @var{estout} is the estimated matrix of output probabilities. ## @end itemize ## ## @subheading Name-Value pair arguments ## ## @itemize @bullet ## @item ## @code{"algorithm"} selects the training algorithm, either ## @code{"BaumWelch"} (default) or @code{"Viterbi"}. @code{"BaumWelch"} ## performs the standard forward-backward re-estimation and is recommended for ## most uses. @code{"Viterbi"} performs segmental (hard) re-estimation from ## the most likely state path of each sequence; it is faster but only ## approximates the maximum-likelihood estimate. ## ## @item ## @code{"symbols"} specifies the possible outputs. If given, @var{sequence} ## is expected to hold the elements of @var{symbols} instead of integers. ## @var{symbols} can be a cell array. ## ## @item ## @code{"tolerance"} is the convergence tolerance (default @code{1e-6}). The ## algorithm terminates when the change in the log-likelihood and in both ## estimated matrices falls below @var{tol}. ## ## @item ## @code{"maxiterations"} is the maximum number of iterations (default ## @code{500}). A warning is issued if the algorithm has not converged within ## this many iterations. ## ## @item ## @code{"pseudotransitions"} and @code{"pseudoemissions"} supply pseudo-count ## matrices for Viterbi training, used to keep transitions or outputs that are ## very unlikely to occur from collapsing to zero probability. ## ## @item ## @code{"verbose"}, when true, prints the log-likelihood and the change in the ## estimates at each iteration. ## @end itemize ## ## @subheading Examples ## ## @example ## @group ## transprob = [0.8, 0.2; 0.4, 0.6]; ## outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; ## sequence = hmmgenerate (100, transprob, outprob); ## transguess = [0.6, 0.4; 0.5, 0.5]; ## outguess = [0.3, 0.3, 0.4; 0.5, 0.3, 0.2]; ## [esttr, estout] = hmmtrain (sequence, transguess, outguess); ## @end group ## @end example ## ## Two results of Viterbi training differ from MATLAB's, deliberately. ## ## Given several sequences, the counts of every sequence are pooled and ## normalized once, so a sequence contributes in proportion to its length. ## MATLAB normalizes each sequence separately and averages the results, which ## weights an eight-symbol sequence as heavily as a twenty-four-symbol one and ## is not the maximum likelihood estimate. Its own Baum-Welch pools expected ## counts, as both algorithms do here. ## ## Given @code{"pseudotransitions"} or @code{"pseudoemissions"}, the ## pseudo-counts are added to the counted transitions and outputs, once per ## iteration, before the row is normalized. MATLAB does the same on its first ## iteration; from its second it adds them to an estimate that has already been ## normalized, so its iterate mixes counts with probabilities and is no longer ## a count matrix of any state path. ## ## @subheading References ## ## @enumerate ## @item ## Wendy L. Martinez and Angel R. Martinez. @cite{Computational Statistics ## Handbook with MATLAB}. Appendix E, pages 547-557, Chapman & Hall/CRC, ## 2001. ## ## @item ## Lawrence R. Rabiner. A Tutorial on Hidden Markov Models and Selected ## Applications in Speech Recognition. @cite{Proceedings of the IEEE}, ## 77(2), pages 257-286, February 1989. ## @end enumerate ## @end deftypefn function [esttr, estout] = hmmtrain (sequence, transguess, outguess, varargin) # Check arguments if (nargin < 3 || mod (numel (varargin), 2) != 0) print_usage (); endif if (! ismatrix (transguess)) error ("hmmtrain: transguess must be a non-empty numeric matrix."); endif if (! ismatrix (outguess)) error ("hmmtrain: outguess must be a non-empty numeric matrix."); endif # nstate is the number of states of the hidden Markov model nstate = rows (transguess); # noutput is the number of different outputs that the hidden Markov model # can generate noutput = columns (outguess); # Check whether transguess and outguess are feasible for a hidden Markov model if (columns (transguess) != nstate) error ("hmmtrain: transguess must be a square matrix."); endif if (rows (outguess) != nstate) error (strcat ("hmmtrain: outguess must have the same number of", ... " rows as transguess.")); endif # Defaults algorithm = 'baumwelch'; usesym = false; tol = 1e-6; maxiter = 500; verbose = false; pseudotr = []; pseudoout = []; # Process varargin for i = 1:2:numel (varargin) if (! ischar (varargin{i})) print_usage (); endif lowerarg = lower (varargin{i}); if (strcmp (lowerarg, 'algorithm')) if (! ischar (varargin{i + 1})) error ("hmmtrain: algorithm must be a string."); endif algorithm = lower (varargin{i + 1}); if (! any (strcmp (algorithm, {'baumwelch', 'viterbi'}))) error (strcat ("hmmtrain: expected 'BaumWelch' or 'Viterbi'", ... sprintf (" but found '%s'.", varargin{i + 1}))); endif elseif (strcmp (lowerarg, 'symbols')) if (numel (varargin{i + 1}) != noutput) error (strcat ("hmmtrain: number of symbols does not match", ... " number of possible outputs.")); endif usesym = true; symbols = varargin{i + 1}; elseif (strcmp (lowerarg, 'tolerance')) tol = varargin{i + 1}; if (! (isscalar (tol) && isnumeric (tol) && tol > 0)) error ("hmmtrain: tolerance must be a positive scalar."); endif elseif (strcmp (lowerarg, 'maxiterations')) maxiter = varargin{i + 1}; if (! (isscalar (maxiter) && maxiter >= 1 && round (maxiter) == maxiter)) error (strcat ("hmmtrain: maxiterations must be a positive", ... " scalar integer.")); endif elseif (strcmp (lowerarg, 'pseudotransitions')) pseudotr = varargin{i + 1}; if (! ismatrix (pseudotr) || rows (pseudotr) != columns (pseudotr)) error ("hmmtrain: pseudotransitions must be a square matrix."); endif if (rows (pseudotr) != nstate) error (strcat ("hmmtrain: pseudotransitions must have the same", ... " size as transguess.")); endif elseif (strcmp (lowerarg, 'pseudoemissions')) pseudoout = varargin{i + 1}; if (! ismatrix (pseudoout)) error ("hmmtrain: pseudoemissions must be a numeric matrix."); endif if (rows (pseudoout) != nstate || columns (pseudoout) != noutput) error (strcat ("hmmtrain: pseudoemissions must have the same", ... " size as outguess.")); endif elseif (strcmp (lowerarg, 'verbose')) verbose = logical (varargin{i + 1}); else error (strcat ("hmmtrain: unknown parameter name", ... sprintf (" '%s'.", varargin{i}))); endif endfor # Pseudo-count matrices default to zero (no smoothing) if (isempty (pseudotr)) pseudotr = zeros (nstate, nstate); endif if (isempty (pseudoout)) pseudoout = zeros (nstate, noutput); endif if (! usesym) symbols = {}; endif # Collect the sequences into a cell array of integer row vectors. A cell # array holds one sequence per element (symbol sequences must use this or a # single vector form); a numeric matrix with several rows is one sequence per # row; anything else is treated as a single sequence. if (iscell (sequence) && ! usesym) seqs = sequence; elseif (iscell (sequence) && usesym && ! isempty (sequence) ... && iscell (sequence{1})) seqs = sequence; elseif (isnumeric (sequence) && rows (sequence) > 1) seqs = num2cell (sequence, 2); else seqs = {sequence}; endif nseq = numel (seqs); for j = 1:nseq seqs{j} = checkseq (seqs{j}, usesym, symbols, noutput); endfor # Normalize the initial guesses so each row sums to 1 (a zero row stays zero) guesstr = normalizerows (transguess); guessout = normalizerows (outguess); usebaum = strcmp (algorithm, 'baumwelch'); converged = false; loglik = 1; for iter = 1:maxiter oldloglik = loglik; loglik = 0; oldguesstr = guesstr; oldguessout = guessout; if (usebaum) # Baum-Welch: accumulate expected transition and output counts TR = zeros (nstate, nstate); OUT = zeros (nstate, noutput); for j = 1:nseq seqj = seqs{j}; len = numel (seqj); if (len == 0) continue; endif [fs, bs, sc] = fwdback (seqj, guesstr, guessout, noutput); loglik += sum (log (sc)); # Expected transition counts (includes the forced transition out of # the initial state 1 via the padding column fs(:,1)) xi = zeros (nstate, nstate); for i = 1:len xi += (fs(:, i) * (bs(:, i + 1) .* guessout(:, seqj(i)))') ... / sc(i + 1); endfor TR += guesstr .* xi; # Expected output counts: sum of posteriors at positions per symbol for l = 1:noutput pos = find (seqj == l); if (! isempty (pos)) OUT(:, l) += sum (fs(:, pos + 1) .* bs(:, pos + 1), 2); endif endfor endfor else ## Viterbi training: count the transitions and outputs along the best ## path of each sequence. Only observed consecutive-state transitions are ## counted -- no transition out of the initial state -- matching ## hmmestimate and MATLAB's first Viterbi iteration. ## ## Counts are pooled across sequences and normalized once, the maximum ## likelihood estimate over the pooled data. MATLAB normalizes each ## sequence and averages, weighting every sequence equally whatever its ## length; its own Baum-Welch pools, as this does. Deliberate deviation. ## ## Pseudo-counts seed the count matrix, so they are counts throughout. ## From its second iteration MATLAB adds them to the normalized estimate ## instead, mixing counts with probabilities. Deliberate deviation. TR = pseudotr; OUT = pseudoout; for j = 1:nseq seqj = seqs{j}; len = numel (seqj); if (len == 0) continue; endif vpath = hmmviterbi (seqj, guesstr, guessout); # Path log-likelihood (used only for the convergence test); this does # include the forced initial transition out of state 1. lp = log (guesstr(1, vpath(1))) + log (guessout(vpath(1), seqj(1))); OUT(vpath(1), seqj(1)) += 1; for i = 2:len TR(vpath(i - 1), vpath(i)) += 1; OUT(vpath(i), seqj(i)) += 1; lp += log (guesstr(vpath(i - 1), vpath(i))) ... + log (guessout(vpath(i), seqj(i))); endfor loglik += lp; endfor endif # Normalize the accumulated counts into probability matrices guesstr = normalizerows (TR); guessout = normalizerows (OUT); # Relative changes used for convergence dll = abs (loglik - oldloglik) / (1 + abs (oldloglik)); dtr = norm (guesstr - oldguesstr, Inf) / nstate; dout = norm (guessout - oldguessout, Inf) / noutput; if (verbose) printf (strcat ("hmmtrain: iteration %d, log-likelihood = %g,", ... " rel. change = %g\n"), iter, loglik, dll); endif if (dll < tol && dtr < tol && dout < tol) converged = true; break; endif endfor if (! converged) warning (strcat ("hmmtrain: algorithm did not converge to within", ... sprintf (" tolerance %g in %d iterations.", ... tol, maxiter))); endif esttr = guesstr; estout = guessout; endfunction ## Scale each row of M to sum to 1; a row summing to zero is left unchanged. function M = normalizerows (M) s = sum (M, 2); s(s == 0) = 1; M = M ./ s; endfunction ## Padded, scaled forward-backward recursion (see hmmdecode). Returns FS and ## BS of size nstate-by-(len+1) with a leading column for the initial state 1, ## and the vector SC of scale factors (SC(1) = 1). function [fs, bs, sc] = fwdback (seqj, transprob, outprob, noutput) nstate = rows (transprob); len = numel (seqj); seq = [noutput + 1, seqj(:)']; L = len + 1; fs = zeros (nstate, L); fs(1, 1) = 1; sc = ones (1, L); for count = 2:L fs(:, count) = outprob(:, seq(count)) .* (transprob' * fs(:, count - 1)); sc(count) = sum (fs(:, count)); fs(:, count) = fs(:, count) ./ sc(count); endfor bs = ones (nstate, L); for count = L - 1:-1:1 bs(:, count) = (transprob * (bs(:, count + 1) .* ... outprob(:, seq(count + 1)))) ./ sc(count + 1); endfor endfunction ## Validate one sequence and, when USESYM, map its symbols to integers. function seqint = checkseq (seqj, usesym, symbols, noutput) len = numel (seqj); if (usesym) seqint = zeros (1, len); for i = 1:noutput isequal = ismember (seqj, symbols(i)); if (any ((seqint == 0) & (isequal == 1))) isequal *= i; seqint += isequal; endif endfor if (! all (seqint) && len > 0) index = max ((seqint == 0) .* (1:len)); error (strcat ("hmmtrain: sequence(", int2str (index), ... ") not in symbols.")); endif else if (! isvector (seqj) && ! isempty (seqj)) error ("hmmtrain: each sequence must be a vector."); endif if (! all (ismember (seqj, 1:noutput))) index = max ((ismember (seqj, 1:noutput) == 0) .* (1:len)); error (strcat ("hmmtrain: sequence(", int2str (index), ... ") out of range.")); endif seqint = seqj(:)'; endif endfunction %!demo %! ## Re-estimate a model with Baum-Welch, starting from rough initial guesses. %! %! transprob = [0.95, 0.05; 0.10, 0.90]; %! outprob = [1/6, 1/6, 1/6, 1/6, 1/6, 1/6; 1/10, 1/10, 1/10, 1/10, 1/10, 1/2]; %! sequence = hmmgenerate (1000, transprob, outprob); %! transguess = [0.8, 0.2; 0.2, 0.8]; %! outguess = [1/6, 1/6, 1/6, 1/6, 1/6, 1/6; 1/8, 1/8, 1/8, 1/8, 1/8, 3/8]; %! [esttr, estout] = hmmtrain (sequence, transguess, outguess) %!test %! ## Baum-Welch recovers matrices close to the generating model %! transprob = [0.9, 0.1; 0.1, 0.9]; %! outprob = [0.9, 0.1; 0.1, 0.9]; %! rand ("seed", 42); %! sequence = hmmgenerate (500, transprob, outprob); %! transguess = [0.8, 0.2; 0.2, 0.8]; %! outguess = [0.7, 0.3; 0.3, 0.7]; %! [esttr, estout] = hmmtrain (sequence, transguess, outguess); %! assert_equal (size (esttr), [2, 2]); %! assert_equal (size (estout), [2, 2]); %! assert_equal (all (abs (sum (esttr, 2) - 1) < 1e-10), true); %! assert_equal (all (abs (sum (estout, 2) - 1) < 1e-10), true); %!test %! ## Baum-Welch never decreases the data log-likelihood %! transprob = [0.8, 0.2; 0.4, 0.6]; %! outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; %! rand ("seed", 7); %! sequence = hmmgenerate (200, transprob, outprob); %! transguess = [0.5, 0.5; 0.5, 0.5]; %! outguess = [0.4, 0.3, 0.3; 0.2, 0.4, 0.4]; %! [~, ll0] = hmmdecode (sequence, transguess, outguess); %! [esttr, estout] = hmmtrain (sequence, transguess, outguess); %! [~, ll1] = hmmdecode (sequence, esttr, estout); %! assert_equal (ll1 >= ll0 - 1e-8, true); %!test %! ## Multiple sequences supplied as a cell array %! transprob = [0.8, 0.2; 0.4, 0.6]; %! outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; %! rand ("seed", 11); %! s1 = hmmgenerate (60, transprob, outprob); %! s2 = hmmgenerate (80, transprob, outprob); %! transguess = [0.6, 0.4; 0.5, 0.5]; %! outguess = [0.3, 0.3, 0.4; 0.5, 0.3, 0.2]; %! [esttr, estout] = hmmtrain ({s1, s2}, transguess, outguess); %! assert_equal (all (abs (sum (esttr, 2) - 1) < 1e-10), true); %! assert_equal (all (abs (sum (estout, 2) - 1) < 1e-10), true); %!test %! ## Viterbi training runs and returns stochastic matrices. Distinguishable %! ## states keep both visited; pseudo-counts guard against empty rows. %! transprob = [0.9, 0.1; 0.2, 0.8]; %! outprob = [0.8, 0.2; 0.2, 0.8]; %! rand ("seed", 3); %! sequence = hmmgenerate (300, transprob, outprob); %! transguess = [0.8, 0.2; 0.3, 0.7]; %! outguess = [0.7, 0.3; 0.3, 0.7]; %! [esttr, estout] = hmmtrain (sequence, transguess, outguess, ... %! 'algorithm', 'Viterbi', ... %! 'pseudotransitions', [1, 1; 1, 1], ... %! 'pseudoemissions', [1, 1; 1, 1]); %! assert_equal (all (abs (sum (esttr, 2) - 1) < 1e-10), true); %! assert_equal (all (abs (sum (estout, 2) - 1) < 1e-10), true); %!test %! ## Symbols form matches the integer form %! transprob = [0.8, 0.2; 0.4, 0.6]; %! outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; %! intseq = [1, 2, 1, 1, 3, 2, 2, 1, 3, 3, 1, 2, 1, 1, 2]; %! symbseq = {'A', 'B', 'A', 'A', 'C', 'B', 'B', 'A', 'C', 'C', ... %! 'A', 'B', 'A', 'A', 'B'}; %! transguess = [0.6, 0.4; 0.5, 0.5]; %! outguess = [0.3, 0.3, 0.4; 0.5, 0.3, 0.2]; %! [t1, o1] = hmmtrain (intseq, transguess, outguess, 'maxiterations', 5); %! [t2, o2] = hmmtrain (symbseq, transguess, outguess, 'maxiterations', 5, ... %! 'symbols', {'A', 'B', 'C'}); %! assert_equal (t1, t2, 1e-12); %! assert_equal (o1, o2, 1e-12); %!test %! ## Several sequences pool their counts, so the estimate follows sequence %! ## length. MATLAB averages the per-sequence estimates (deviation). %! s1 = [1, 2, 2, 3, 3, 3, 3, 3]; %! s2 = [2, 2, 2, 3, 2, 2, 1, 1, 1, 2, 1, 1, 3, 1, 1, 1, 2, 2, 3, 3, 3, 2, 3, 2]; %! transguess = [0.7, 0.3; 0.4, 0.6]; %! outguess = [0.5, 0.3, 0.2; 0.2, 0.3, 0.5]; %! [esttr, estout] = hmmtrain ({s1, s2}, transguess, outguess, ... %! 'algorithm', 'Viterbi', 'maxiterations', 1, ... %! 'tolerance', 1e6); %! assert_equal (esttr, [19/21, 2/21; 0, 1], 1e-14); %! assert_equal (estout, [9/21, 10/21, 2/21; 0, 2/11, 9/11], 1e-14); %!test %! ## Pseudo-counts stay counts on every iteration. MATLAB adds them to the %! ## normalized estimate from its second iteration on (deviation). %! seq = [2, 2, 2, 2, 3, 2, 3, 3, 1, 2, 3, 3, 3, 2, 3, 1, 2, 2, 1, 2, ... %! 3, 3, 1, 2, 3, 1, 3, 3, 2, 1]; %! transguess = [0.7, 0.3; 0.4, 0.6]; %! outguess = [0.5, 0.3, 0.2; 0.2, 0.3, 0.5]; %! [esttr, estout] = hmmtrain (seq, transguess, outguess, ... %! 'algorithm', 'Viterbi', ... %! 'pseudotransitions', [2, 1; 1, 3], ... %! 'pseudoemissions', [1, 2, 1; 3, 1, 2]); %! assert_equal (esttr, [5/7, 2/7; 1/29, 28/29], 1e-14); %! assert_equal (estout, [1/8, 6/8, 1/8; 9/32, 9/32, 14/32], 1e-14); %!warning ... %! transprob = [0.8, 0.2; 0.4, 0.6]; %! outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; %! rand ("seed", 1); %! sequence = hmmgenerate (50, transprob, outprob); %! hmmtrain (sequence, [0.5, 0.5; 0.5, 0.5], [0.4, 0.3, 0.3; 0.2, 0.4, 0.4], ... %! 'maxiterations', 1); %!error hmmtrain ([1, 2], [0.8, 0.2; 0.4, 0.6]) %!error ... %! hmmtrain ([1, 2], [0.8, 0.2; 0.4, 0.6; 0.1, 0.9], [0.5, 0.5; 0.1, 0.9]) %!error ... %! hmmtrain ([1, 2], [0.8, 0.2; 0.4, 0.6], [0.5, 0.5]) %!error ... %! hmmtrain ([1, 2], [0.8, 0.2; 0.4, 0.6], [0.5, 0.5; 0.1, 0.9], ... %! 'algorithm', 'nope') %!error ... %! hmmtrain ([1, 5], [0.8, 0.2; 0.4, 0.6], [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]) statistics-release-2.1.2/inst/Markov_Models/hmmviterbi.m000066400000000000000000000256601527311104300233750ustar00rootroot00000000000000## Copyright (C) 2006, 2007 Arno Onken ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{vpath} =} hmmviterbi (@var{sequence}, @var{transprob}, @var{outprob}) ## @deftypefnx {statistics} {@var{vpath} =} hmmviterbi (@dots{}, @code{"symbols"}, @var{symbols}) ## @deftypefnx {statistics} {@var{vpath} =} hmmviterbi (@dots{}, @code{"statenames"}, @var{statenames}) ## ## Viterbi path of a hidden Markov model. ## ## Use the Viterbi algorithm to find the Viterbi path of a hidden Markov ## model given a sequence of outputs. The model assumes that the generation ## starts in state @code{1} at step @code{0} but does not include step ## @code{0} in the generated states and sequence. ## ## @subheading Arguments ## ## @itemize @bullet ## @item ## @var{sequence} is the vector of length @var{len} of given outputs. The ## outputs must be integers ranging from @code{1} to ## @code{columns (outprob)}. ## ## @item ## @var{transprob} is the matrix of transition probabilities of the states. ## @code{transprob(i, j)} is the probability of a transition to state ## @code{j} given state @code{i}. ## ## @item ## @var{outprob} is the matrix of output probabilities. ## @code{outprob(i, j)} is the probability of generating output @code{j} ## given state @code{i}. ## @end itemize ## ## @subheading Return values ## ## @itemize @bullet ## @item ## @var{vpath} is the vector of the same length as @var{sequence} of the ## estimated hidden states. The states are integers ranging from @code{1} to ## @code{columns (transprob)}. ## @end itemize ## ## If @code{"symbols"} is specified, then @var{sequence} is expected to be a ## sequence of the elements of @var{symbols} instead of integers ranging ## from @code{1} to @code{columns (outprob)}. @var{symbols} can be a cell array. ## ## If @code{"statenames"} is specified, then the elements of ## @var{statenames} are used for the states in @var{vpath} instead of ## integers ranging from @code{1} to @code{columns (transprob)}. ## @var{statenames} can be a cell array. ## ## @subheading Examples ## ## @example ## @group ## transprob = [0.8, 0.2; 0.4, 0.6]; ## outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; ## [sequence, states] = hmmgenerate (25, transprob, outprob); ## vpath = hmmviterbi (sequence, transprob, outprob); ## @end group ## ## @group ## symbols = @{"A", "B", "C"@}; ## statenames = @{"One", "Two"@}; ## [sequence, states] = hmmgenerate (25, transprob, outprob, ... ## "symbols", symbols, "statenames", statenames); ## vpath = hmmviterbi (sequence, transprob, outprob, ... ## "symbols", symbols, "statenames", statenames); ## @end group ## @end example ## ## @subheading References ## ## @enumerate ## @item ## Wendy L. Martinez and Angel R. Martinez. @cite{Computational Statistics ## Handbook with MATLAB}. Appendix E, pages 547-557, Chapman & Hall/CRC, ## 2001. ## ## @item ## Lawrence R. Rabiner. A Tutorial on Hidden Markov Models and Selected ## Applications in Speech Recognition. @cite{Proceedings of the IEEE}, ## 77(2), pages 257-286, February 1989. ## @end enumerate ## @end deftypefn function vpath = hmmviterbi (sequence, transprob, outprob, varargin) # Check arguments if (nargin < 3 || mod (length (varargin), 2) != 0) print_usage (); endif if (! ismatrix (transprob)) error ("hmmviterbi: transprob must be a non-empty numeric matrix"); endif if (! ismatrix (outprob)) error ("hmmviterbi: outprob must be a non-empty numeric matrix"); endif len = length (sequence); # nstate is the number of states of the hidden Markov model nstate = rows (transprob); # noutput is the number of different outputs that the hidden Markov model # can generate noutput = columns (outprob); # Check whether transprob and outprob are feasible for a hidden Markov model if (columns (transprob) != nstate) error ("hmmviterbi: transprob must be a square matrix"); endif if (rows (outprob) != nstate) error (strcat ("hmmviterbi: outprob must have the same number of", ... " rows as transprob")); endif # Flag for symbols usesym = false; # Flag for statenames usesn = false; # Process varargin for i = 1:2:length (varargin) # There must be an identifier: 'symbols' or 'statenames' if (! ischar (varargin{i})) print_usage (); endif # Upper case is also fine lowerarg = lower (varargin{i}); if (strcmp (lowerarg, 'symbols')) if (length (varargin{i + 1}) != noutput) error (strcat ("hmmviterbi: number of symbols does not match", ... " number of possible outputs")); endif usesym = true; # Use the following argument as symbols symbols = varargin{i + 1}; # The same for statenames elseif (strcmp (lowerarg, 'statenames')) if (length (varargin{i + 1}) != nstate) error (strcat ("hmmviterbi: number of statenames does not match", ... " number of states")); endif usesn = true; # Use the following argument as statenames statenames = varargin{i + 1}; else error (strcat ("hmmviterbi: expected 'symbols' or 'statenames'", ... sprintf (" but found '%s'", varargin{i}))); endif endfor # Transform sequence from symbols to integers if necessary if (usesym) # sequenceint is used to build the transformed sequence sequenceint = zeros (1, len); for i = 1:noutput # Search for symbols(i) in the sequence, isequal will have 1 at # corresponding indices; i is the right integer for that symbol isequal = ismember (sequence, symbols(i)); # We do not want to change sequenceint if the symbol appears a second # time in symbols if (any ((sequenceint == 0) & (isequal == 1))) isequal *= i; sequenceint += isequal; endif endfor if (! all (sequenceint)) index = max ((sequenceint == 0) .* (1:len)); error (strcat ("hmmviterbi: sequence(", int2str (index), ... ") not in symbols")); endif sequence = sequenceint; else if (! isvector (sequence) && ! isempty (sequence)) error ("hmmviterbi: sequence must be a vector"); endif if (! all (ismember (sequence, 1:noutput))) index = max ((ismember (sequence, 1:noutput) == 0) .* (1:len)); error (strcat ("hmmviterbi: sequence(", int2str (index), ... ") out of range")); endif endif # Each row in transprob and outprob should contain log probabilities # => scale so that the sum is 1 and convert to log space # - for transprob s = sum (transprob, 2); s(s == 0) = 1; transprob = log (transprob ./ s); # - for outprob s = sum (outprob, 2); s(s == 0) = 1; outprob = log (outprob ./ s); # Viterbi recursion. The model starts in state 1 at step 0, so the first # observation is emitted after one transition from state 1. delta(k) holds # the log probability of the most likely path that ends in state k at the # current step; back(:, i) stores, for each state, the predecessor state on # that best path, used for the traceback. if (len == 0) vpath = zeros (1, 0); else delta = transprob(1, :)' + outprob(:, sequence(1)); back = zeros (nstate, len); for i = 2:len # best predecessor for each current state, then add the emission [delta, back(:, i)] = max (delta + transprob, [], 1); delta = delta' + outprob(:, sequence(i)); endfor # Trace back from the most likely final state vpath = zeros (1, len); [~, vpath(len)] = max (delta); for i = len - 1:-1:1 vpath(i) = back(vpath(i + 1), i + 1); endfor endif # Transform vpath into statenames if requested if (usesn) vpath = reshape (statenames(vpath), 1, len); endif endfunction %!demo %! ## Most likely (Viterbi) state path for a two-state, three-symbol model. %! %! transprob = [0.8, 0.2; 0.4, 0.6]; %! outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; %! sequence = [1, 2, 1, 1, 3, 2, 3, 1, 2, 3]; %! vpath = hmmviterbi (sequence, transprob, outprob) %!demo %! ## The state path can also be reported using custom state names. %! %! transprob = [0.95, 0.05; 0.10, 0.90]; %! outprob = [1/6, 1/6, 1/6, 1/6, 1/6, 1/6; 1/10, 1/10, 1/10, 1/10, 1/10, 1/2]; %! [sequence, states] = hmmgenerate (12, transprob, outprob, ... %! "statenames", {"fair", "loaded"}); %! vpath = hmmviterbi (sequence, transprob, outprob, ... %! "statenames", {"fair", "loaded"}) %!test %! sequence = [1, 2, 1, 1, 1, 2, 2, 1, 2, 3, 3, 3, ... %! 3, 2, 3, 1, 1, 1, 1, 3, 3, 2, 3, 1, 3]; %! transprob = [0.8, 0.2; 0.4, 0.6]; %! outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; %! vpath = hmmviterbi (sequence, transprob, outprob); %! expected = [1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, ... %! 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1]; %! assert_equal (vpath, expected); %!test %! sequence = {'A', 'B', 'A', 'A', 'A', 'B', 'B', 'A', 'B', 'C', 'C', 'C', ... %! 'C', 'B', 'C', 'A', 'A', 'A', 'A', 'C', 'C', 'B', 'C', 'A', 'C'}; %! transprob = [0.8, 0.2; 0.4, 0.6]; %! outprob = [0.2, 0.4, 0.4; 0.7, 0.2, 0.1]; %! symbols = {'A', 'B', 'C'}; %! statenames = {'One', 'Two'}; %! vpath = hmmviterbi (sequence, transprob, outprob, 'symbols', symbols, ... %! 'statenames', statenames); %! expected = {'One', 'One', 'Two', 'Two', 'Two', 'One', 'One', 'One', ... %! 'One', 'One', 'One', 'One', 'One', 'One', 'One', 'Two', ... %! 'Two', 'Two', 'Two', 'One', 'One', 'One', 'One', 'One', 'One'}; %! assert_equal (vpath, expected); %!test %! ## The returned path is the true maximum-probability path. A former bug %! ## scored a spurious transition out of the last state, biasing the final %! ## states of the path; these cases guard against a regression. %! transprob = [0.1854, 0.8146; 0.5948, 0.4052]; %! outprob = [0.5536, 0.4464; 0.2712, 0.7288]; %! assert_equal (hmmviterbi ([1, 2], transprob, outprob), [2, 2]); %!test %! transprob = [0.6056, 0.3944; 0.2036, 0.7964]; %! outprob = [0.6525, 0.3475; 0.2878, 0.7122]; %! assert_equal (hmmviterbi ([1, 1, 2, 1], transprob, outprob), [1, 1, 1, 1]); %!test %! ## An empty sequence yields an empty path %! vpath = hmmviterbi ([], [0.8, 0.2; 0.4, 0.6], [0.5, 0.5; 0.3, 0.7]); %! assert_equal (vpath, zeros (1, 0)); statistics-release-2.1.2/inst/Model_Evaluation/000077500000000000000000000000001527311104300215255ustar00rootroot00000000000000statistics-release-2.1.2/inst/Model_Evaluation/ConfusionMatrixChart.m000066400000000000000000001161111527311104300260160ustar00rootroot00000000000000## Copyright (C) 2020-2021 Stefano Guidoni ## Copyright (C) 2023-2024 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ConfusionMatrixChart < handle ## -*- texinfo -*- ## @deftp {statistics} ConfusionMatrixChart ## ## Confusion matrix chart for classification results ## ## The @code{ConfusionMatrixChart} class implements a confusion matrix chart ## object, which displays the classification performance of a classifier by ## showing the counts of true positive, true negative, false positive, and ## false negative predictions. ## ## A confusion matrix chart is a visual representation of the performance of ## a classification algorithm. The rows represent the true classes and the ## columns represent the predicted classes. The diagonal elements represent ## the correctly classified observations, while the off-diagonal elements ## represent the misclassified observations. ## ## Create a @code{ConfusionMatrixChart} object by using the ## @code{confusionchart} function. ## ## @seealso{confusionchart} ## @end deftp properties(Access = public) ## -*- texinfo -*- ## @deftp {ConfusionMatrixChart} {property} XLabel ## ## X-axis label ## ## A character vector specifying the label for the x-axis. Default is ## "Predicted Class". ## ## @end deftp XLabel = 'Predicted Class'; ## -*- texinfo -*- ## @deftp {ConfusionMatrixChart} {property} YLabel ## ## Y-axis label ## ## A character vector specifying the label for the y-axis. Default is ## "True Class". ## ## @end deftp YLabel = 'True Class'; ## -*- texinfo -*- ## @deftp {ConfusionMatrixChart} {property} Title ## ## Chart title ## ## A character vector specifying the title of the confusion matrix chart. ## Default is empty string. ## ## @end deftp Title = ''; ## -*- texinfo -*- ## @deftp {ConfusionMatrixChart} {property} FontName ## ## Font name for text elements ## ## A character vector specifying the font name used for all text elements ## in the chart. Default is empty string, which uses the axes font name. ## ## @end deftp FontName = ''; ## -*- texinfo -*- ## @deftp {ConfusionMatrixChart} {property} FontSize ## ## Font size for text elements ## ## A numeric scalar specifying the font size used for all text elements ## in the chart. Default is 0, which uses the axes font size. ## ## @end deftp FontSize = 0; ## -*- texinfo -*- ## @deftp {ConfusionMatrixChart} {property} DiagonalColor ## ## Color for diagonal elements ## ## A 1x3 RGB vector specifying the color for the diagonal elements of the ## confusion matrix, which represent correct classifications. Default is ## [0.0, 0.4471, 0.7412]. ## ## @end deftp DiagonalColor = [0 0.4471 0.7412]; ## -*- texinfo -*- ## @deftp {ConfusionMatrixChart} {property} OffDiagonalColor ## ## Color for off-diagonal elements ## ## A 1x3 RGB vector specifying the color for the off-diagonal elements of ## the confusion matrix, which represent misclassifications. Default is ## [0.8510, 0.3255, 0.0980]. ## ## @end deftp OffDiagonalColor = [0.8510 0.3255 0.0980]; ## -*- texinfo -*- ## @deftp {ConfusionMatrixChart} {property} Normalization ## ## Normalization method for confusion matrix values ## ## A character vector specifying how to normalize the confusion matrix ## values. Supported values are: ## ## @itemize ## @item @qcode{'absolute'} - Display absolute counts (default) ## @item @qcode{'column-normalized'} - Normalize by column totals ## @item @qcode{'row-normalized'} - Normalize by row totals ## @item @qcode{'total-normalized'} - Normalize by total number of ## observations ## @end itemize ## ## @end deftp Normalization = 'absolute'; ## -*- texinfo -*- ## @deftp {ConfusionMatrixChart} {property} ColumnSummary ## ## Column summary display ## ## A character vector specifying whether and how to display column ## summaries. Supported values are: ## ## @itemize ## @item @qcode{'off'} - Do not display column summary (default) ## @item @qcode{'absolute'} - Display absolute counts ## @item @qcode{'column-normalized'} - Display normalized by column ## @item @qcode{'total-normalized'} - Display normalized by total ## @end itemize ## ## @end deftp ColumnSummary = 'off'; ## -*- texinfo -*- ## @deftp {ConfusionMatrixChart} {property} RowSummary ## ## Row summary display ## ## A character vector specifying whether and how to display row summaries. ## Supported values are: ## ## @itemize ## @item @qcode{'off'} - Do not display row summary (default) ## @item @qcode{'absolute'} - Display absolute counts ## @item @qcode{'row-normalized'} - Display normalized by row ## @item @qcode{'total-normalized'} - Display normalized by total ## @end itemize ## ## @end deftp RowSummary = 'off'; ## -*- texinfo -*- ## @deftp {ConfusionMatrixChart} {property} GridVisible ## ## Grid visibility ## ## A character vector specifying whether to display grid lines in the ## confusion matrix. Supported values are: ## ## @itemize ## @item @qcode{'on'} - Display grid lines (default) ## @item @qcode{'off'} - Hide grid lines ## @end itemize ## ## @end deftp GridVisible = 'on'; ## -*- texinfo -*- ## @deftp {ConfusionMatrixChart} {property} HandleVisibility ## ## Handle visibility ## ## A character vector specifying the visibility of the object's handle. ## Supported values are @qcode{'on'}, @qcode{'off'}, and @qcode{'callback'}. ## ## @end deftp HandleVisibility = ''; ## -*- texinfo -*- ## @deftp {ConfusionMatrixChart} {property} OuterPosition ## ## Outer position of the chart ## ## A 1x4 numeric vector specifying the outer position of the chart in the ## format [left, bottom, width, height]. ## ## @end deftp OuterPosition = []; ## -*- texinfo -*- ## @deftp {ConfusionMatrixChart} {property} Position ## ## Position of the chart ## ## A 1x4 numeric vector specifying the position of the chart in the ## format [left, bottom, width, height]. ## ## @end deftp Position = []; ## -*- texinfo -*- ## @deftp {ConfusionMatrixChart} {property} Units ## ## Position units ## ## A character vector specifying the units for the position properties. ## Supported values are @qcode{'centimeters'}, @qcode{'characters'}, ## @qcode{'inches'}, @qcode{'normalized'}, @qcode{'pixels'}, and ## @qcode{'points'}. ## ## @end deftp Units = ''; endproperties properties(GetAccess = public, SetAccess = private) ## -*- texinfo -*- ## @deftp {ConfusionMatrixChart} {property} ClassLabels ## ## Class labels ## ## A cell array of character vectors containing the class labels used in ## the confusion matrix. This property is read-only. ## ## @end deftp ClassLabels = {}; ## -*- texinfo -*- ## @deftp {ConfusionMatrixChart} {property} NormalizedValues ## ## Normalized confusion matrix values ## ## A numeric matrix containing the normalized confusion matrix values ## according to the current normalization setting. This property is ## read-only. ## ## @end deftp NormalizedValues = []; ## -*- texinfo -*- ## @deftp {ConfusionMatrixChart} {property} Parent ## ## Parent object ## ## A handle to the parent figure or container object. This property is ## read-only. ## ## @end deftp Parent = 0; endproperties properties(Access = protected, Hidden) ## Axes handle hax = 0.0; ## Number of classes ClassN = 0; ## Absolute confusion matrix values AbsoluteValues = []; ## Column summary absolute values ColumnSummaryAbsoluteValues = []; ## Row summary absolute values RowSummaryAbsoluteValues = []; endproperties methods (Hidden) ## Custom display of the normalized values and the class labels. function disp (this) nv_sizes = size (this.NormalizedValues); cl_sizes = size (this.ClassLabels); printf ("%s with properties:\n\n", class (this)); printf ("\tNormalizedValues: [ %dx%d %s ]\n", nv_sizes(1), nv_sizes(2),... class (this.NormalizedValues)); printf ("\tClassLabels: { %dx%d %s }\n\n", cl_sizes(1), cl_sizes(2),... class (this.ClassLabels)); endfunction ## Set functions function set.XLabel (this, string) if (! ischar (string)) close (this.Parent); error ("confusionchart: XLabel must be a string."); endif this.XLabel = updateAxesProperties (this, 'xlabel', string); endfunction function set.YLabel (this, string) if (! ischar (string)) close (this.Parent); error ("confusionchart: YLabel must be a string."); endif this.YLabel = updateAxesProperties (this, 'ylabel', string); endfunction function set.Title (this, string) if (! ischar (string)) close (this.Parent); error ("confusionchart: Title must be a string."); endif this.Title = updateAxesProperties (this, 'title', string); endfunction function set.FontName (this, string) if (! ischar (string)) close (this.Parent); error ("confusionchart: FontName must be a string."); endif this.FontName = updateTextProperties (this, 'fontname', string); endfunction function set.FontSize (this, value) if (! isnumeric (value)) close (this.Parent); error ("confusionchart: FontSize must be numeric."); endif this.FontSize = updateTextProperties (this, 'fontsize', value); endfunction function set.DiagonalColor (this, color) if (ischar (color)) color = this.convertNamedColor (color); endif if (! (isvector (color) && length (color) == 3 )) close (this.Parent); error ("confusionchart: DiagonalColor must be a color."); endif this.DiagonalColor = color; updateColorMap (this); endfunction function set.OffDiagonalColor (this, color) if (ischar (color)) color = this.convertNamedColor (color); endif if (! (isvector (color) && length (color) == 3)) close (this.Parent); error ("confusionchart: OffDiagonalColor must be a color."); endif this.OffDiagonalColor = color; updateColorMap (this); endfunction function set.Normalization (this, string) if (! any (strcmp (string, {'absolute', 'column-normalized',... 'row-normalized', 'total-normalized'}))) close (this.Parent); error ("confusionchart: invalid value for Normalization."); endif this.Normalization = string; updateChart (this); endfunction function set.ColumnSummary (this, string) if (! any (strcmp (string, {'off', 'absolute', 'column-normalized',... 'total-normalized'}))) close (this.Parent); error ("confusionchart: invalid value for ColumnSummary."); endif this.ColumnSummary = string; updateChart (this); endfunction function set.RowSummary (this, string) if (! any (strcmp (string, {'off', 'absolute', 'row-normalized',... 'total-normalized'}))) close (this.Parent); error ("confusionchart: invalid value for RowSummary."); endif this.RowSummary = string; updateChart (this); endfunction function set.GridVisible (this, string) if (! any (strcmp (string, {'off', 'on'}))) close (this.Parent); error ("confusionchart: invalid value for GridVisible."); endif this.GridVisible = string; setGridVisibility (this); endfunction function set.HandleVisibility (this, string) if (! any (strcmp (string, {'off', 'on', 'callback'}))) close (this.Parent); error ("confusionchart: invalid value for HandleVisibility"); endif set (this.hax, 'handlevisibility', string); endfunction function set.OuterPosition (this, vector) if (! isvector (vector) || ! isnumeric (vector) || length (vector) != 4) close (this.Parent); error ("confusionchart: invalid value for OuterPosition"); endif set (this.hax, 'outerposition', vector); endfunction function set.Position (this, vector) if (! isvector (vector) || ! isnumeric (vector) || length (vector) != 4) close (this.Parent); error ("confusionchart: invalid value for Position"); endif set (this.hax, 'position', vector); endfunction function set.Units (this, string) if (! any (strcmp (string, {'centimeters', 'characters', 'inches', ... 'normalized', 'pixels', 'points'}))) close (this.Parent); error ("confusionchart: invalid value for Units"); endif set (this.hax, 'units', string); endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {ConfusionMatrixChart} {@var{cmc} =} ConfusionMatrixChart (@var{hax}, @var{cm}, @var{cl}) ## @deftypefnx {ConfusionMatrixChart} {@var{cmc} =} ConfusionMatrixChart (@dots{}, @var{name}, @var{value}) ## ## Create a @qcode{ConfusionMatrixChart} object for visualizing ## classification performance. ## ## @code{@var{cmc} = ConfusionMatrixChart (@var{hax}, @var{cm}, @var{cl})} ## returns a ConfusionMatrixChart object with parent axes @var{hax}, ## confusion matrix @var{cm}, and class labels @var{cl}. ## ## @itemize ## @item ## @code{hax} must be a valid axes handle where the chart will be displayed. ## @item ## @code{cm} must be a square numeric matrix containing the confusion matrix ## values, where rows represent true classes and columns represent predicted ## classes. ## @item ## @code{cl} must be a cell array of character vectors containing the class ## labels. The number of labels must match the size of the confusion matrix. ## @end itemize ## ## @code{@var{cmc} = ConfusionMatrixChart (@dots{}, @var{name}, ## @var{value})} ## returns a ConfusionMatrixChart object with additional parameters ## specified by @qcode{@var{name}, @var{value}} paired arguments: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'XLabel'} @tab A character vector specifying the ## x-axis label. Default is "Predicted Class". ## ## @item @qcode{'YLabel'} @tab A character vector specifying the ## y-axis label. Default is "True Class". ## ## @item @qcode{'Title'} @tab A character vector specifying the chart ## title. Default is empty string. ## ## @item @qcode{'FontName'} @tab A character vector specifying the ## font name for text elements. Default is the axes font name. ## ## @item @qcode{'FontSize'} @tab A numeric scalar specifying the ## font size for text elements. Default is the axes font size. ## ## @item @qcode{'DiagonalColor'} @tab A 1x3 RGB vector specifying ## the color for diagonal elements (correct classifications). Default is ## [0.0, 0.4471, 0.7412]. ## ## @item @qcode{'OffDiagonalColor'} @tab A 1x3 RGB vector specifying ## the color for off-diagonal elements (misclassifications). Default is ## [0.8510, 0.3255, 0.0980]. ## ## @item @qcode{'Normalization'} @tab A character vector specifying ## the normalization method. Supported values are @qcode{'absolute'}, ## @qcode{'column-normalized'}, @qcode{'row-normalized'}, and ## @qcode{'total-normalized'}. Default is @qcode{'absolute'}. ## ## @item @qcode{'ColumnSummary'} @tab A character vector specifying ## whether and how to display column summaries. Supported values are ## @qcode{'off'}, @qcode{'absolute'}, @qcode{'column-normalized'}, and ## @qcode{'total-normalized'}. Default is @qcode{'off'}. ## ## @item @qcode{'RowSummary'} @tab A character vector specifying ## whether and how to display row summaries. Supported values are ## @qcode{'off'}, @qcode{'absolute'}, @qcode{'row-normalized'}, and ## @qcode{'total-normalized'}. Default is @qcode{'off'}. ## ## @item @qcode{'GridVisible'} @tab A character vector specifying ## whether to display grid lines. Supported values are @qcode{'on'} and ## @qcode{'off'}. Default is @qcode{'on'}. ## ## @item @qcode{'HandleVisibility'} @tab A character vector specifying ## the handle visibility. Supported values are @qcode{'on'}, @qcode{'off'}, ## and @qcode{'callback'}. ## ## @item @qcode{'OuterPosition'} @tab A 1x4 numeric vector specifying ## the outer position of the chart. ## ## @item @qcode{'Position'} @tab A 1x4 numeric vector specifying ## the position of the chart. ## ## @item @qcode{'Units'} @tab A character vector specifying the ## position units. Supported values are @qcode{'centimeters'}, ## @qcode{'characters'}, @qcode{'inches'}, @qcode{'normalized'}, ## @qcode{'pixels'}, and @qcode{'points'}. ## @end multitable ## ## @seealso{confusionchart} ## @end deftypefn function this = ConfusionMatrixChart (hax, cm, cl, args) ## class initialization this.hax = hax; this.Parent = get (this.hax, 'parent'); this.ClassLabels = cl; this.NormalizedValues = cm; this.AbsoluteValues = cm; this.ClassN = rows (cm); this.FontName = get (this.hax, 'fontname'); this.FontSize = get (this.hax, 'fontsize'); set (this.hax, 'xlabel', this.XLabel); set (this.hax, 'ylabel', this.YLabel); ## draw the chart draw (this); ## apply paired properties if (! isempty (args)) pair_idx = 1; while (pair_idx < length (args)) switch (args{pair_idx}) case 'XLabel' this.XLabel = args{pair_idx + 1}; case 'YLabel' this.YLabel = args{pair_idx + 1}; case 'Title' this.Title = args{pair_idx + 1}; case 'FontName' this.FontName = args{pair_idx + 1}; case 'FontSize' this.FontSize = args{pair_idx + 1}; case 'DiagonalColor' this.DiagonalColor = args{pair_idx + 1}; case 'OffDiagonalColor' this.OffDiagonalColor = args{pair_idx + 1}; case 'Normalization' this.Normalization = args{pair_idx + 1}; case 'ColumnSummary' this.ColumnSummary = args{pair_idx + 1}; case 'RowSummary' this.RowSummary = args{pair_idx + 1}; case 'GridVisible' this.GridVisible = args{pair_idx + 1}; case 'HandleVisibility' this.HandleVisibility = args{pair_idx + 1}; case 'OuterPosition' this.OuterPosition = args{pair_idx + 1}; case 'Position' this.Position = args{pair_idx + 1}; case 'Units' this.Units = args{pair_idx + 1}; otherwise close (this.Parent); error ("confusionchart: invalid property %s", args{pair_idx}); endswitch pair_idx += 2; endwhile endif ## init the color map updateColorMap (this); endfunction ## -*- texinfo -*- ## @deftypefn {ConfusionMatrixChart} {} sortClasses (@var{cmc}, @var{order}) ## ## Sort the classes in the confusion matrix chart. ## ## @code{sortClasses (@var{cmc}, @var{order})} sorts the classes in the ## confusion matrix chart @var{cmc} according to the specified @var{order}. ## ## @var{order} can be: ## @itemize ## @item A cell array of class labels in the desired order ## @item @qcode{'auto'} - Sort class labels alphabetically ## @item @qcode{'ascending-diagonal'} - Sort by ascending diagonal values ## @item @qcode{'descending-diagonal'} - Sort by descending diagonal values ## @item @qcode{'cluster'} - Sort using hierarchical clustering ## @end itemize ## ## When using @qcode{'cluster'}, the classes are grouped based on similarity ## using hierarchical clustering, which can help identify groups of ## frequently confused classes. ## ## @seealso{confusionchart, linkage, pdist} ## @end deftypefn function sortClasses (this, order) ## check the input parameters if (nargin != 2) print_usage (); endif cl = this.ClassLabels; cm_size = this.ClassN; nv = this.NormalizedValues; av = this.AbsoluteValues; cv = this.ColumnSummaryAbsoluteValues; rv = this.RowSummaryAbsoluteValues; scl = {}; Idx = []; if (strcmp (order, 'auto')) [scl, Idx] = sort (cl); elseif (strcmp (order, 'ascending-diagonal')) [s, Idx] = sort (diag (nv)); scl = cl(Idx); elseif (strcmp (order, 'descending-diagonal')) [s, Idx] = sort (diag (nv)); Idx = flip (Idx); scl = cl(Idx); elseif (strcmp (order, 'cluster')) ## the classes are all grouped together ## this way one can visually evaluate which are the most similar classes ## according to the learning algorithm D = zeros (1, ((cm_size - 1) * cm_size / 2)); # a pdist like vector maxD = 2 * max (max (av)); k = 1; # better than computing the index at every cycle for i = 1 : (cm_size - 1) for j = (i + 1) : cm_size D(k++) = maxD - (av(i, j) + av(j, i)); # distance endfor endfor tree = linkage (D, 'average'); # clustering ## we could have optimal leaf ordering with Idx = optimalleaforder (tree, D); # optimal clustering ##[~, Idx] = sort (cluster (tree)); nodes_to_visit = 2 * cm_size - 1; nodecount = 0; while (! isempty (nodes_to_visit)) current_node = nodes_to_visit(1); nodes_to_visit(1) = []; if (current_node > cm_size) node = current_node - cm_size; nodes_to_visit = [tree(node,[2 1]) nodes_to_visit]; endif if (current_node <= cm_size) nodecount++; Idx(nodecount) = current_node; endif endwhile ## scl = cl(Idx); else ## must be an array or cell array of labels if (! iscellstr (order)) if (! ischar (order)) if (isrow (order)) order = vec (order); endif order = num2str (order); endif scl = cellstr (order); endif if (length (scl) != length (cl)) error ("sortClasses: wrong size for order.") endif Idx = zeros (length (scl), 1); for i = 1 : length (scl) Idx(i) = find (strcmp (cl, scl{i})); endfor endif ## rearrange the normalized values... nv = nv(Idx, :); nv = nv(:, Idx); this.NormalizedValues = nv; ## ...and the absolute values... av = av(Idx, :); av = av(:, Idx); this.AbsoluteValues = av; cv = cv([Idx ( Idx + cm_size )]); this.ColumnSummaryAbsoluteValues = cv; rv = rv([Idx ( Idx + cm_size )]); this.RowSummaryAbsoluteValues = rv; ## ...and the class labels this.ClassLabels = scl; ## update the axes set (this.hax, 'xtick', (0.5 : 1 : (cm_size - 0.5)), 'xticklabel', scl,... 'ytick', (0.5 : 1 : (cm_size - 0.5)), 'yticklabel', scl); ## get text and patch handles kids = get (this.hax, 'children'); t_kid = kids(find (isprop (kids, 'fontname'))); # hack to find texts m_kid = kids(find (strcmp (get (kids, 'userdata'), 'MainChart'))); c_kid = kids(find (strcmp (get (kids, 'userdata'), 'ColumnSummary'))); r_kid = kids(find (strcmp (get (kids, 'userdata'), 'RowSummary'))); ## re-assign colors to the main chart cdata_v = get (m_kid, 'cdata'); cdata_m = reshape (cdata_v, cm_size, cm_size); cdata_m = cdata_m(Idx, :); cdata_m = cdata_m(:, Idx); cdata_v = reshape (cdata_m, size (cdata_v)); set (m_kid, 'cdata', cdata_v); ## re-assign colors to the column summary cdata_v = get (c_kid, 'cdata'); cdata_m = reshape (transpose (cdata_v), cm_size, 2); cdata_m = cdata_m(Idx, :); cdata_v = reshape (cdata_m, size (cdata_v)); set (c_kid, 'cdata', cdata_v); ## re-assign colors to the row summary cdata_v = get (r_kid, 'cdata'); cdata_m = reshape (cdata_v, cm_size, 2); cdata_m = cdata_m(Idx, :); cdata_v = reshape (cdata_m, size (cdata_v)); set (r_kid, 'cdata', cdata_v); ## move the text labels for i = 1:length (t_kid) t_pos = get (t_kid(i), 'userdata'); if (t_pos(2) > cm_size) ## row summary t_pos(1) = find (Idx == (t_pos(1) + 1)) - 1; set (t_kid(i), 'userdata', t_pos); t_pos = t_pos([2 1]) + 0.5; set (t_kid(i), 'position', t_pos); elseif (t_pos(1) > cm_size) ## column summary t_pos(2) = find (Idx == (t_pos(2) + 1)) - 1; set (t_kid(i), 'userdata', t_pos); t_pos = t_pos([2 1]) + 0.5; set (t_kid(i), 'position', t_pos); else ## main chart t_pos(1) = find (Idx == (t_pos(1) + 1)) - 1; t_pos(2) = find (Idx == (t_pos(2) + 1)) - 1; set (t_kid(i), 'userdata', t_pos); t_pos = t_pos([2 1]) + 0.5; set (t_kid(i), 'position', t_pos); endif endfor updateChart (this); endfunction endmethods methods(Access = private) ## convertNamedColor ## convert a named colour to a colour triplet function ret = convertNamedColor (this, color) vColorNames = ['ymcrgbwk']'; vColorTriplets = [1 1 0; 1 0 1; 0 1 1; 1 0 0; 0 1 0; 0 0 1; 1 1 1; 0 0 0]; if (strcmp (color, 'black')) color = 'k'; endif index = find (vColorNames == color(1)); if (! isempty (index)) ret = vColorTriplets(index, :); else ret = []; # trigger an error message endif endfunction ## updateAxesProperties ## update the properties of the axes function ret = updateAxesProperties (this, prop, value) set (this.hax, prop, value); ret = value; endfunction ## updateTextProperties ## set the properties of the texts function ret = updateTextProperties (this, prop, value) hax_kids = get (this.hax, 'children'); text_kids = hax_kids(isprop (hax_kids , 'fontname')); # hack to find texts text_kids(end + 1) = get (this.hax, 'xlabel'); text_kids(end + 1) = get (this.hax, 'ylabel'); text_kids(end + 1) = get (this.hax, 'title'); updateAxesProperties (this, prop, value); set (text_kids, prop, value); ret = value; endfunction ## setGridVisibility ## toggle the visibility of the grid function setGridVisibility (this) kids = get (this.hax, 'children'); kids = kids(find (isprop (kids, 'linestyle'))); if (strcmp (this.GridVisible, 'on')) set (kids, 'linestyle', '-'); else set (kids, 'linestyle', 'none'); endif endfunction ## updateColorMap ## change the colormap and, accordingly, the text colors function updateColorMap (this) cm_size = this.ClassN; d_color = this.DiagonalColor; o_color = this.OffDiagonalColor; ## quick hack d_color(find (d_color == 1.0)) = 0.999; o_color(find (o_color == 1.0)) = 0.999; ## 64 shades for each color cm_colormap(1:64,:) = [1.0 : (-(1.0 - o_color(1)) / 63) : o_color(1);... 1.0 : (-(1.0 - o_color(2)) / 63) : o_color(2);... 1.0 : (-(1.0 - o_color(3)) / 63) : o_color(3)]'; cm_colormap(65:128,:) = [1.0 : (-(1.0 - d_color(1)) / 63) : d_color(1);... 1.0 : (-(1.0 - d_color(2)) / 63) : d_color(2);... 1.0 : (-(1.0 - d_color(3)) / 63) : d_color(3)]'; colormap (this.hax, cm_colormap); ## update text colors kids = get (this.hax, 'children'); t_kids = kids(find (isprop (kids, 'fontname'))); # hack to find texts m_patch = kids(find (strcmp (get (kids, 'userdata'), 'MainChart'))); c_patch = kids(find (strcmp (get (kids, 'userdata'), 'ColumnSummary'))); r_patch = kids(find (strcmp (get (kids, 'userdata'), 'RowSummary'))); m_colors = get (m_patch, 'cdata'); c_colors = get (c_patch, 'cdata'); r_colors = get (r_patch, 'cdata'); ## when a patch is dark, let's use a pale color for the text for i = 1 : length (t_kids) t_pos = get (t_kids(i), 'userdata'); color_idx = 1; if (t_pos(2) > cm_size) ## row summary idx = (t_pos(2) - cm_size - 1) * cm_size + t_pos(1) + 1; color_idx = r_colors(idx) + 1; elseif (t_pos(1) > cm_size) ## column summary idx = (t_pos(1) - cm_size - 1) * cm_size + t_pos(2) + 1; color_idx = c_colors(idx) + 1; else ## main chart idx = t_pos(2) * cm_size + t_pos(1) + 1; color_idx = m_colors(idx) + 1; endif if (sum (cm_colormap(color_idx, :)) < 1.8) set (t_kids(i), 'color', [.97 .97 1.0]); else set (t_kids(i), 'color', [.15 .15 .15]); endif endfor endfunction ## updateChart ## update the text labels and the NormalizedValues property function updateChart (this) cm_size = this.ClassN; cm = this.AbsoluteValues; l_cs = this.ColumnSummaryAbsoluteValues; l_rs = this.RowSummaryAbsoluteValues; kids = get (this.hax, 'children'); t_kids = kids(find (isprop (kids, 'fontname'))); # hack to find texts normalization = this.Normalization; column_summary = this.ColumnSummary; row_summary = this.RowSummary; ## normalization for labelling row_totals = sum (cm, 2); col_totals = sum (cm, 1); mat_total = sum (col_totals); cm_labels = cm; add_percent = true; if (strcmp (normalization, 'column-normalized')) for i = 1 : cm_size cm_labels(:,i) = cm_labels(:,i) ./ col_totals(i); endfor elseif (strcmp (normalization, 'row-normalized')) for i = 1 : cm_size cm_labels(i,:) = cm_labels(i,:) ./ row_totals(i); endfor elseif (strcmp (normalization, 'total-normalized')) cm_labels = cm_labels ./ mat_total; else add_percent = false; endif ## update NormalizedValues this.NormalizedValues = cm_labels; ## update axes last_row = cm_size; last_col = cm_size; userdata = cell2mat (get (t_kids, 'userdata')); cs_kids = t_kids(find (userdata(:,1) > cm_size)); cs_kids(end + 1) = kids(find (strcmp (get (kids, 'userdata'),... 'ColumnSummary'))); if (! strcmp ('off', column_summary)) set (cs_kids, 'visible', 'on'); last_row += 3; else set (cs_kids, 'visible', 'off'); endif rs_kids = t_kids(find (userdata(:,2) > cm_size)); rs_kids(end + 1) = kids(find (strcmp (get (kids, 'userdata'),... 'RowSummary'))); if (! strcmp ('off', row_summary)) set (rs_kids, 'visible', 'on'); last_col += 3; else set (rs_kids, 'visible', 'off'); endif axis (this.hax, [0 last_col 0 last_row]); ## update column summary data cs_add_percent = true; if (! strcmp (column_summary, 'off')) if (strcmp (column_summary, 'column-normalized')) for i = 1 : cm_size if (col_totals(i) == 0) ## avoid division by zero l_cs([i (cm_size + i)]) = 0; else l_cs([i, cm_size + i]) = l_cs([i, cm_size + i]) ./ col_totals(i); endif endfor elseif strcmp (column_summary, 'total-normalized') l_cs = l_cs ./ mat_total; else cs_add_percent = false; endif endif ## update row summary data rs_add_percent = true; if (! strcmp (row_summary, 'off')) if (strcmp (row_summary, 'row-normalized')) for i = 1 : cm_size if (row_totals(i) == 0) ## avoid division by zero l_rs([i (cm_size + i)]) = 0; else l_rs([i, cm_size + i]) = l_rs([i, cm_size + i]) ./ row_totals(i); endif endfor elseif (strcmp (row_summary, 'total-normalized')) l_rs = l_rs ./ mat_total; else rs_add_percent = false; endif endif ## update text label_list = vec (cm_labels); for i = 1 : length (t_kids) t_pos = get (t_kids(i), 'userdata'); new_string = ''; if (t_pos(2) > cm_size) ## this is the row summary idx = (t_pos(2) - cm_size - 1) * cm_size + t_pos(1) + 1; if (rs_add_percent) new_string = num2str (100.0 * l_rs(idx), '%3.1f'); new_string = [new_string '%']; else new_string = num2str (l_rs(idx)); endif elseif (t_pos(1) > cm_size) ## this is the column summary idx = (t_pos(1) - cm_size - 1) * cm_size + t_pos(2) + 1; if (cs_add_percent) new_string = num2str (100.0 * l_cs(idx), '%3.1f'); new_string = [new_string '%']; else new_string = num2str (l_cs(idx)); endif else ## this is the main chart idx = t_pos(2) * cm_size + t_pos(1) + 1; if (add_percent) new_string = num2str (100.0 * label_list(idx), '%3.1f'); new_string = [new_string '%']; else new_string = num2str (label_list(idx)); endif endif set (t_kids(i), 'string', new_string); endfor endfunction ## draw ## draw the chart function draw (this) cm = this.AbsoluteValues; cl = this.ClassLabels; cm_size = this.ClassN; ## set up the axes set (this.hax, 'xtick', (0.5 : 1 : (cm_size - 0.5)), 'xticklabel', cl,... 'ytick', (0.5 : 1 : (cm_size - 0.5)), 'yticklabel', cl ); axis ('ij'); axis (this.hax, [0 cm_size 0 cm_size]); ## prepare the patches indices_b = 0 : (cm_size -1); indices_v = repmat (indices_b, cm_size, 1); indices_vx = transpose (vec (indices_v)); indices_vy = vec (indices_v', 2); indices_ex = vec ((cm_size + 1) * [1; 2] .* ones (2, cm_size), 2); ## normalization for colorization ## it is used a colormap of 128 shades of two colors, 64 shades for each ## color normal = max (max (cm)); cm_norm = round (63 * cm ./ normal); cm_norm = cm_norm + 64 * eye (cm_size); ## default normalization: absolute cm_labels = vec (cm); ## the patches of the main chart x_patch = [indices_vx; ( indices_vx + 1 ); ( indices_vx + 1 ); indices_vx]; y_patch = [indices_vy; indices_vy; ( indices_vy + 1 ); ( indices_vy + 1 )]; c_patch = vec (cm_norm(1 : cm_size, 1 : cm_size)); ## display the patches ph = patch (this.hax, x_patch, y_patch, c_patch); set (ph, 'userdata', 'MainChart'); ## display the labels userdata = [indices_vy; indices_vx]'; nonzero_idx = find (cm_labels != 0); th = text ((x_patch(1, nonzero_idx) + 0.5), (y_patch(1, nonzero_idx) +... 0.5), num2str (cm_labels(nonzero_idx)), 'parent', this.hax ); set (th, 'horizontalalignment', 'center'); for i = 1 : length (nonzero_idx) set (th(i), 'userdata', userdata(nonzero_idx(i), :)); endfor ## patches for the summaries main_values = diag (cm); ct_values = sum (cm)'; rt_values = sum (cm, 2); cd_values = ct_values - main_values; rd_values = rt_values - main_values; ## column summary x_cs = [[indices_b indices_b]; ( [indices_b indices_b] + 1 ); ( [indices_b indices_b] + 1 ); [indices_b indices_b]]; y_cs = [(repmat ([1 1 2 2]', 1, cm_size)) (repmat ([2 2 3 3]', 1, cm_size))] +... cm_size; c_cs = [(round (63 * (main_values ./ ct_values)) + 64); (round (63 * (cd_values ./ ct_values)))]; c_cs(isnan (c_cs)) = 0; l_cs = [main_values; cd_values]; ph = patch (this.hax, x_cs, y_cs, c_cs); set (ph, 'userdata', 'ColumnSummary'); set (ph, 'visible', 'off' ); userdata = [y_cs(1,:); x_cs(1,:)]'; nonzero_idx = find (l_cs != 0); th = text ((x_cs(1,nonzero_idx) + 0.5), (y_cs(1,nonzero_idx) + 0.5),... num2str (l_cs(nonzero_idx)), 'parent', this.hax); set (th, 'horizontalalignment', 'center'); for i = 1 : length (nonzero_idx) set (th(i), 'userdata', userdata(nonzero_idx(i), :)); endfor set (th, 'visible', 'off'); ## row summary x_rs = y_cs; y_rs = x_cs; c_rs = [(round (63 * (main_values ./ rt_values)) + 64); (round (63 * (rd_values ./ rt_values)))]; c_rs(isnan (c_rs)) = 0; l_rs = [main_values; rd_values]; ph = patch (this.hax, x_rs, y_rs, c_rs); set (ph, 'userdata', 'RowSummary'); set (ph, 'visible', 'off'); userdata = [y_rs(1,:); x_rs(1,:)]'; nonzero_idx = find (l_rs != 0); th = text ((x_rs(1,nonzero_idx) + 0.5), (y_rs(1,nonzero_idx) + 0.5),... num2str (l_rs(nonzero_idx)), 'parent', this.hax); set (th, 'horizontalalignment', 'center'); for i = 1 : length (nonzero_idx) set (th(i), 'userdata', userdata(nonzero_idx(i), :)); endfor set (th, 'visible', 'off'); this.ColumnSummaryAbsoluteValues = l_cs; this.RowSummaryAbsoluteValues = l_rs; endfunction endmethods endclassdef %!demo %! ## Create a simple ConfusionMatrixChart Object %! %! cm = ConfusionMatrixChart (gca, [1 2; 1 2], {'A','B'}, {'XLabel','LABEL A'}) %! NormalizedValues = cm.NormalizedValues %! ClassLabels = cm.ClassLabels ## Test plotting %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! cm = ConfusionMatrixChart (gca, [1 2; 1 2], {'A','B'}, {'XLabel','LABEL A'}); %! assert_equal (isa (cm, 'ConfusionMatrixChart'), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect statistics-release-2.1.2/inst/Model_Evaluation/confusionchart.m000066400000000000000000000252441527311104300247370ustar00rootroot00000000000000## Copyright (C) 2020-2021 Stefano Guidoni ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} confusionchart (@var{trueLabels}, @var{predictedLabels}) ## @deftypefnx {statistics} {} confusionchart (@var{m}) ## @deftypefnx {statistics} {} confusionchart (@var{m}, @var{classLabels}) ## @deftypefnx {statistics} {} confusionchart (@var{parent}, @dots{}) ## @deftypefnx {statistics} {} confusionchart (@dots{}, @var{prop}, @var{val}, @dots{}) ## @deftypefnx {statistics} {@var{cm} =} confusionchart (@dots{}) ## ## Display a chart of a confusion matrix. ## ## The two vectors of values @var{trueLabels} and @var{predictedLabels}, which ## are used to compute the confusion matrix, must be defined with the same ## format as the inputs of @code{confusionmat}. ## Otherwise a confusion matrix @var{m} as computed by @code{confusionmat} can ## be given. ## ## @var{classLabels} is an array of labels, i.e. the list of the class names. ## ## If the first argument is a handle to a @code{figure} or to a @code{uipanel}, ## then the confusion matrix chart is displayed inside that object. ## ## Optional property/value pairs are passed directly to the underlying objects, ## e.g. @qcode{'xlabel'}, @qcode{'ylabel'}, @qcode{'title'}, @qcode{'fontname'}, ## @qcode{'fontsize'} etc. ## ## The optional return value @var{cm} is a @code{ConfusionMatrixChart} object. ## Specific properties of a @code{ConfusionMatrixChart} object are: ## @itemize @bullet ## @item @qcode{'DiagonalColor'} ## The color of the patches on the diagonal, default is [0.0, 0.4471, 0.7412]. ## ## @item @qcode{'OffDiagonalColor'} ## The color of the patches off the diagonal, default is [0.851, 0.3255, 0.098]. ## ## @item @qcode{'GridVisible'} ## Available values: @qcode{on} (default), @qcode{off}. ## ## @item @qcode{'Normalization'} ## Available values: @qcode{absolute} (default), @qcode{column-normalized}, ## @qcode{row-normalized}, @qcode{total-normalized}. ## ## @item @qcode{'ColumnSummary'} ## Available values: @qcode{off} (default), @qcode{absolute}, ## @qcode{column-normalized},@qcode{total-normalized}. ## ## @item @qcode{'RowSummary'} ## Available values: @qcode{off} (default), @qcode{absolute}, ## @qcode{row-normalized}, @qcode{total-normalized}. ## @end itemize ## ## Run @code{demo confusionchart} to see some examples. ## ## @seealso{confusionmat, ConfusionMatrixChart.sortClasses} ## @end deftypefn function cm = confusionchart (varargin) ## check the input parameters if (nargin < 1) print_usage (); endif p_i = 1; if (ishghandle (varargin{p_i})) ## parameter is a parent figure handle_type = get (varargin{p_i}, 'type'); if (strcmp (handle_type, 'figure')) h = figure (varargin{p_i}); hax = axes ('parent', h); elseif (strcmp (handle_type, 'uipanel')) h = varargin{p_i}; hax = axes ('parent', varargin{p_i}); else ## MATLAB compatibility: on MATLAB are also available Tab objects, ## TiledChartLayout objects, GridLayout objects error ("confusionchart: invalid handle to parent object"); endif p_i++; else h = figure (); hax = axes ('parent', h); endif if (ismatrix (varargin{p_i}) && rows (varargin{p_i}) == ... columns (varargin{p_i})) ## parameter is a confusion matrix conmat = varargin{p_i}; p_i++; if (p_i <= nargin && ((isvector (varargin{p_i}) && ... length (varargin{p_i}) == rows (conmat)) || ... (ischar ( varargin{p_i}) && rows (varargin{p_i}) == rows (conmat)) ... || iscellstr (varargin{p_i}))) ## parameter is an array of labels labarr = varargin{p_i}; if (isrow (labarr)) labarr = vec (labarr); endif p_i++; else labarr = [1 : (rows (conmat))]'; endif elseif (isvector (varargin{p_i})) ## parameter must be a group for confusionmat [conmat, labarr] = confusionmat (varargin{p_i}, varargin{p_i + 1}); p_i = p_i + 2; else close (h); error ("confusionchart: invalid argument"); endif ## remaining parameters are stored i = p_i; args = {}; while (i <= nargin) args{end + 1} = varargin{i++}; endwhile ## prepare the labels if (! iscellstr (labarr)) if (! ischar (labarr)) labarr = num2str (labarr); endif labarr = cellstr (labarr); endif ## MATLAB compatibility: labels are sorted [labarr, I] = sort (labarr); conmat = conmat(I, :); conmat = conmat(:, I); cm = ConfusionMatrixChart (hax, conmat, labarr, args); endfunction ## Demonstration using the confusion matrix example from ## R.Bonnin, "Machine Learning for Developers", pp. 55-56 %!demo %! close all %! ## Setting the chart properties %! Yt = [8 5 6 8 5 3 1 6 4 2 5 3 1 4]'; %! Yp = [8 5 6 8 5 2 3 4 4 5 5 7 2 6]'; %! confusionchart (Yt, Yp, 'Title', ... %! 'Demonstration with summaries','Normalization',... %! 'absolute','ColumnSummary', 'column-normalized','RowSummary',... %! 'row-normalized') ## example: confusion matrix and class labels %!demo %! close all %! ## Cellstr as inputs %! Yt = {'Positive', 'Positive', 'Positive', 'Negative', 'Negative'}; %! Yp = {'Positive', 'Positive', 'Negative', 'Negative', 'Negative'}; %! m = confusionmat (Yt, Yp); %! confusionchart (m, {'Positive', 'Negative'}); %! hold off ## example: editing the properties of an existing ConfusionMatrixChart object %!demo %! close all %! ## Editing the object properties %! Yt = {'Positive', 'Positive', 'Positive', 'Negative', 'Negative'}; %! Yp = {'Positive', 'Positive', 'Negative', 'Negative', 'Negative'}; %! cm = confusionchart (Yt, Yp); %! cm.Title = 'This is an example with a green diagonal'; %! cm.DiagonalColor = [0.4660, 0.6740, 0.1880]; %! hold off ## example: drawing the chart inside a uipanel %!demo %! close all %! ## Confusion chart in a uipanel %! h = uipanel (); %! Yt = {'Positive', 'Positive', 'Positive', 'Negative', 'Negative'}; %! Yp = {'Positive', 'Positive', 'Negative', 'Negative', 'Negative'}; %! cm = confusionchart (h, Yt, Yp); %! hold off ## example: sortClasses %!demo %! close all %! ## Sorting classes %! Yt = [8 5 6 8 5 3 1 6 4 2 5 3 1 4]'; %! Yp = [8 5 6 8 5 2 3 4 4 5 5 7 2 6]'; %! cm = confusionchart (Yt, Yp, 'Title', ... %! 'Classes are sorted in ascending order'); %! cm = confusionchart (Yt, Yp, 'Title', ... %! 'Classes are sorted according to clusters'); %! sortClasses (cm, 'cluster'); ## Test input validation ## Get current figure visibility so it can be restored after tests %!shared visibility_setting %! visibility_setting = get (0, 'DefaultFigureVisible'); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! fail ('confusionchart ()', 'Invalid call'); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! fail ('confusionchart ([1 1; 2 2; 3 3])', 'invalid argument'); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! fail ('confusionchart ([1 2], [0 1], ''xxx'', 1)', 'invalid property'); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! fail ('confusionchart ([1 2], [0 1], ''XLabel'', 1)', 'XLabel .* string'); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! fail ('confusionchart ([1 2], [0 1], ''YLabel'', [1 0])', ... %! '.* YLabel .* string'); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! fail ('confusionchart ([1 2], [0 1], ''Title'', .5)', '.* Title .* string'); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! fail ('confusionchart ([1 2], [0 1], ''FontName'', [])', ... %! '.* FontName .* string'); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! fail ('confusionchart ([1 2], [0 1], ''FontSize'', ''b'')', ... %! '.* FontSize .* numeric'); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! fail ('confusionchart ([1 2], [0 1], ''DiagonalColor'', ''h'')', ... %! '.* DiagonalColor .* color'); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! fail ('confusionchart ([1 2], [0 1], ''OffDiagonalColor'', [])', ... %! '.* OffDiagonalColor .* color'); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! fail ('confusionchart ([1 2], [0 1], ''Normalization'', '''')', ... %! '.* invalid .* Normalization'); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! fail ('confusionchart ([1 2], [0 1], ''ColumnSummary'', [])', ... %! '.* invalid .* ColumnSummary'); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! fail ('confusionchart ([1 2], [0 1], ''RowSummary'', 1)', ... %! '.* invalid .* RowSummary'); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! fail ('confusionchart ([1 2], [0 1], ''GridVisible'', .1)', ... %! '.* invalid .* GridVisible'); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! fail ('confusionchart ([1 2], [0 1], ''HandleVisibility'', .1)', ... %! '.* invalid .* HandleVisibility'); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! fail ('confusionchart ([1 2], [0 1], ''OuterPosition'', .1)', ... %! '.* invalid .* OuterPosition'); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! fail ('confusionchart ([1 2], [0 1], ''Position'', .1)', ... %! '.* invalid .* Position'); %! set (0, 'DefaultFigureVisible', visibility_setting); %!test %! set (0, 'DefaultFigureVisible', 'off'); %! fail ('confusionchart ([1 2], [0 1], ''Units'', .1)', '.* invalid .* Units'); %! set (0, 'DefaultFigureVisible', visibility_setting); statistics-release-2.1.2/inst/Model_Evaluation/confusionmat.m000066400000000000000000000437171527311104300244240ustar00rootroot00000000000000## Copyright (C) 2020 Stefano Guidoni ## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{C} =} confusionmat (@var{group}, @var{grouphat}) ## @deftypefnx {statistics} {@var{C} =} confusionmat (@var{group}, @var{grouphat}, "Order", @var{grouporder}) ## @deftypefnx {statistics} {[@var{C}, @var{order}] =} confusionmat (@var{group}, @var{grouphat}) ## ## Compute a confusion matrix for classification problems ## ## @code{confusionmat} returns the confusion matrix @var{C} for the group of ## actual values @var{group} and the group of predicted values @var{grouphat}. ## The row indices of the confusion matrix represent actual values, while the ## column indices represent predicted values. The indices are the same for both ## actual and predicted values, so the confusion matrix is a square matrix. ## Each element of the matrix represents the number of matches between a given ## actual value (row index) and a given predicted value (column index), hence ## correct matches lie on the main diagonal of the matrix. ## The order of the rows and columns is returned in @var{order}. ## ## @var{group} and @var{grouphat} must have the same number of observations ## and the same data type. ## Valid data types are numeric vectors, logical vectors, character arrays, ## string arrays, cell arrays of strings, and categorical arrays. ## ## The order of the rows and columns can be specified by setting the ## @var{grouporder} variable. The data type of @var{grouporder} must be the ## same of @var{group} and @var{grouphat}, except that a numeric ## @var{grouporder} for logical inputs is converted to logical. For logical ## inputs, the default order is @code{false} followed by @code{true}, and ## @var{grouporder} must contain both values. @var{grouporder} cannot contain ## duplicated values. ## ## @seealso{crosstab} ## @end deftypefn function [C, order] = confusionmat (group, grouphat, opt = 'Order', grouporder) ## check the input parameters if ((nargin < 2) || (nargin > 4)) print_usage (); endif y_true = group; y_pred = grouphat; if (! strcmp (class (y_true), class (y_pred))) error ("confusionmat: group and grouphat must be of the same data type."); endif obs_true = length (y_true); if (ischar (y_true)) obs_true = rows (y_true); endif obs_pred = length (y_pred); if (ischar (y_pred)) obs_pred = rows (y_pred); endif if (obs_true != obs_pred) error ("confusionmat: group and grouphat must be of the same length."); endif if ((nargin > 3) && strcmp (opt, 'Order')) unique_tokens = grouporder; ## A numeric order for logical data is taken as logical if (islogical (y_true) && isnumeric (unique_tokens)) unique_tokens = logical (unique_tokens); endif if (! strcmp (class (y_true), class (unique_tokens))) error ("confusionmat: group and grouporder must be of the same data type."); endif endif convert_order_to_char = false; ## Handle y_true if (ischar (y_true)) y_true = cellstr (y_true); convert_order_to_char = true; elseif (isvector (y_true)) y_true = y_true(:); else error ("confusionmat: group must be a vector or character array."); endif ## Handle y_pred if (ischar (y_pred)) y_pred = cellstr (y_pred); elseif (isvector (y_pred)) y_pred = y_pred(:); else error ("confusionmat: grouphat must be a vector or character array."); endif if (exist ( 'unique_tokens', 'var')) if (ischar (unique_tokens)) unique_tokens = cellstr (unique_tokens); elseif (isvector (unique_tokens)) unique_tokens = unique_tokens(:); else error ("confusionmat: grouporder must be a vector or character array."); endif if (numel (unique (unique_tokens)) < numel (unique_tokens)) error ("confusionmat: ORDER can't contain duplicated values."); endif endif ## compute the confusion matrix if (isa (y_true, 'numeric') || isa (y_true, 'logical')) ## Remove observations where EITHER group or grouphat is NaN nan_indices = isnan (y_true) | isnan (y_pred); y_true(nan_indices) = []; y_pred(nan_indices) = []; ## numeric and boolean values are sorted in ascending order if (! exist ('unique_tokens', 'var')) if (islogical (y_true)) unique_tokens = [false; true]; else unique_tokens = unique ([y_true; y_pred]); endif elseif (islogical (y_true) && (! any (unique_tokens) || all (unique_tokens))) error (strcat ("confusionmat: ORDER should contain all the labels", ... " appearing in G or GHAT.")); endif C_size = length (unique_tokens); C = zeros (C_size); for i = 1:length (y_true) row_index = find (unique_tokens == y_true(i)); col_index = find (unique_tokens == y_pred(i)); ## Check valid indices if (! isempty (row_index) && ! isempty (col_index)) C(row_index, col_index)++; endif endfor elseif (iscellstr (y_true)) ## string cells ## remove observations where EITHER input is empty empty_indices = cellfun ('isempty', y_true) | cellfun ('isempty', y_pred); y_true(empty_indices) = []; y_pred(empty_indices) = []; ## string values are sorted according to their ## first appearance in group and grouphat if (! exist ('unique_tokens', 'var')) all_tokens = [y_true; y_pred]; if (isempty (all_tokens)) unique_tokens = {}; else [~, idx] = unique (all_tokens, 'first'); unique_tokens = all_tokens(sort (idx)); endif endif C_size = length (unique_tokens); C = zeros (C_size); for i = 1:length (y_true) row_index = find (strcmp (y_true{i}, unique_tokens)); col_index = find (strcmp (y_pred{i}, unique_tokens)); if (! isempty (row_index) && ! isempty (col_index)) C(row_index, col_index)++; endif endfor elseif (ischar (y_true)) ## character values are sorted according to their ## first appearance in group and grouphat if (! exist ('unique_tokens', 'var')) all_tokens = vertcat (y_true, y_pred); unique_tokens = [all_tokens(1)]; for i = 2:length (all_tokens) if (! any (find (unique_tokens == all_tokens(i)))) unique_tokens = [unique_tokens; all_tokens(i)]; endif endfor endif C_size = length ( unique_tokens ); C = zeros ( C_size ); for i = 1:length ( y_true) row_index = find (unique_tokens == y_true(i)); col_index = find (unique_tokens == y_pred(i)); C(row_index, col_index)++; endfor elseif (isa (y_true, 'string')) ## 1. Filter Missing Values bad_indices = ismissing (y_true) | ismissing (y_pred); y_true(bad_indices) = []; y_pred(bad_indices) = []; ## 2. Determine Order ## String arrays are sorted ALPHABETICALLY by unique(). if (! exist ('unique_tokens', 'var')) all_tokens = [y_true; y_pred]; if (isempty (all_tokens)) unique_tokens = strings (0, 1); else unique_tokens = unique (all_tokens); endif endif C_size = length (unique_tokens); C = zeros (C_size); [~, row_indices] = ismember (y_true, unique_tokens); [~, col_indices] = ismember (y_pred, unique_tokens); valid_mask = (row_indices > 0) & (col_indices > 0); row_indices = row_indices(valid_mask); col_indices = col_indices(valid_mask); for i = 1:length (row_indices) C(row_indices(i), col_indices(i))++; endfor elseif (isa (y_true, 'categorical')) ## 1. Filter Undefined Values bad_indices = isundefined (y_true) | isundefined (y_pred); y_true(bad_indices) = []; y_pred(bad_indices) = []; ## 2. Determine Order if (! exist ('unique_tokens', 'var')) ## This ensures the matrix includes all defined categories cats_true = categories (y_true); cats_pred = categories (y_pred); ## Union of defined categories all_cats = union (cats_true, cats_pred, 'stable'); ## Create the reference order vector unique_tokens = categorical (all_cats, all_cats); endif C_size = length (unique_tokens); C = zeros (C_size); [~, row_indices] = ismember (y_true, unique_tokens); [~, col_indices] = ismember (y_pred, unique_tokens); valid_mask = (row_indices > 0) & (col_indices > 0); row_indices = row_indices(valid_mask); col_indices = col_indices(valid_mask); for i = 1:length (row_indices) C(row_indices(i), col_indices(i))++; endfor else error ("confusionmat: invalid data type."); endif order = unique_tokens; if (convert_order_to_char && iscellstr (order)) order = char (order); endif endfunction ## Test the confusion matrix example from ## R.Bonnin, "Machine Learning for Developers", pp. 55-56 %!test %! Yt = [8 5 6 8 5 3 1 6 4 2 5 3 1 4]'; %! Yp = [8 5 6 8 5 2 3 4 4 5 5 7 2 6]'; %! C = [0 1 1 0 0 0 0 0; 0 0 0 0 1 0 0 0; 0 1 0 0 0 0 1 0; 0 0 0 1 0 1 0 0; ... %! 0 0 0 0 3 0 0 0; 0 0 0 1 0 1 0 0; 0 0 0 0 0 0 0 0; 0 0 0 0 0 0 0 2]; %! assert_equal (confusionmat (Yt, Yp), C) ## Test 2: Basic Integers %!test %! g = [1; 2; 3; 1]; %! gh = [1; 2; 2; 1]; %! [C, order] = confusionmat (g, gh); %! assert_equal (C, [2 0 0; 0 1 0; 0 1 0]); %! assert_equal (order, [1; 2; 3]); ## Test 3: Logical Vectors %!test %! g = [true; false; true; false]; %! gh = [true; true; false; false]; %! [C, order] = confusionmat (g, gh); %! assert_equal (C, [1 1; 1 1]); %! assert_equal (order, [false; true]); ## Test 4: Floating Point Numbers %!test %! g = [1.1; 2.2; 1.1]; %! gh = [1.1; 2.2; 2.2]; %! [C, order] = confusionmat (g, gh); %! assert_equal (C, [1 1; 0 1]); %! assert_equal (order, [1.1; 2.2]); ## Test 5: Numeric with NaNs %!test %! g = [1; 2; NaN; 3]; %! gh = [1; 1; 2; 3]; %! [C, order] = confusionmat (g, gh); %! assert_equal (C, [1 0 0; 1 0 0; 0 0 1]); %! assert_equal (order, [1; 2; 3]); ## Test 6: Empty Inputs %!error %! confusionmat ([], []) ## Test 7: Scalar Inputs %!test %! [C, order] = confusionmat (1, 1); %! assert_equal (C, 1); %! assert_equal (order, 1); ## Test 8: Cell Array with Empty Strings %!test %! g = {'A'; ''; 'B'}; %! gh = {'A'; 'B'; 'B'}; %! [C, order] = confusionmat (g, gh); %! assert_equal (C, [1 0; 0 1]); %! assert_equal (order, {'A'; 'B'}); ## Test 9: Character Arrays %!test %! g = ['AA'; 'BB'; 'AA'; 'CC']; %! gh = ['AA'; 'BB'; 'BB'; 'CC']; %! [C, order] = confusionmat (g, gh); %! assert_equal (C, [1 1 0; 0 1 0; 0 0 1]); %! assert_equal (order, ['AA'; 'BB'; 'CC']); ## Test 10: Character Arrays (Whitespace Handling) %!test %! g = char ('A', 'B', 'A'); %! gh = char ('A', 'A', 'B'); %! [C, order] = confusionmat (g, gh); %! assert_equal (C, [1 1; 1 0]); %! assert_equal (order, char ('A', 'B')); ## Test 11: Cell Array of Strings %!test %! g = {'Cat'; 'Dog'; 'Cat'; 'Bird'}; %! gh = {'Cat'; 'Cat'; 'Bird'; 'Bird'}; %! [C, order] = confusionmat (g, gh); %! assert_equal (C, [1 0 1; 1 0 0; 0 0 1]); %! assert_equal (order, {'Cat'; 'Dog'; 'Bird'}); ## Test 12: String Arrays %!test %! g = ['Apple'; 'Banana'; 'Apple']; %! gh = ['Apple'; 'Apple'; 'Cherry']; %! [C, order] = confusionmat (g, gh); %! assert_equal (C, [1 0 1; 1 0 0; 0 0 0]); %! assert_equal (order, ['Apple'; 'Banana'; 'Cherry']); ## Test 13: String Arrays (Missing Values) %!test %! g = string ({'A'; 'B'; 'B'}); %! g(2) = missing; %! gh = string (['A'; 'B'; 'B']); %! [C, order] = confusionmat (g, gh); %! assert_equal (C, [1 0; 0 1]); %! assert_equal (isequal (order, string (['A'; 'B'])), true); ## Test 14: Categorical Arrays %!test %! g = categorical ({'Small', 'Medium', 'Large'}); %! gh = categorical ({'Small', 'Large', 'Large'}); %! [C, order] = confusionmat (g, gh); %! assert_equal (C, [1 0 0; 1 0 0; 0 0 1]); %! assert_equal (cellstr (char (order)), {'Large'; 'Medium'; 'Small'}); ## Test 15: Categorical (Undefined Values / NaN) %!test %! g = categorical ({'Red', 'Blue', 'Red'}); %! g(2) = missing; %! gh = categorical ({'Red', 'Blue', 'Red'}); %! [C, order] = confusionmat (g, gh); %! assert_equal (C, [0 0; 0 2]); %! assert_equal (cellstr (char (order)), {'Blue'; 'Red'}); ## Test 16: Categorical (Unused Categories) %!test %! vals = {'A', 'B', 'A'}; %! cats = {'A', 'B', 'C'}; %! g = categorical (vals, cats); %! gh = categorical (vals, cats); %! [C, order] = confusionmat (g, gh); %! assert_equal (size (C), [3 3]); %! assert_equal (C(3,3), 0); %! assert_equal (cellstr (char (order)), {'A'; 'B'; 'C'}); ## Test 17: Categorical (Union of Categories) %!test %! g = categorical ({'A'}, {'A', 'B'}); %! gh = categorical ({'A'}, {'A', 'C'}); %! [C, order] = confusionmat (g, gh); %! assert_equal (size (C), [3 3]); %! assert_equal (cellstr (char (order)), {'A'; 'B'; 'C'}); ## Test 18: Row vs Column Vector %!test %! g = [1, 2, 3]; %! gh = [1; 2; 3]; %! [C, order] = confusionmat (g, gh); %! assert_equal (C, eye (3)); %! assert_equal (order, [1; 2; 3]); ## Test 19: Custom Order %!test %! g = [1; 2; 3]; %! gh = [1; 2; 3]; %! myOrder = [3; 2; 1]; %! [C, order] = confusionmat (g, gh, 'Order', myOrder); %! assert_equal (C, [1 0 0; 0 1 0; 0 0 1]); %! assert_equal (order, [3; 2; 1]); ## Test 20: Custom Order (Reordering Strings) %!test %! g = {'A'; 'B'}; %! gh = {'A'; 'B'}; %! [C, order] = confusionmat (g, gh, 'Order', {'B'; 'A'}); %! assert_equal (C, [1 0; 0 1]); %! assert_equal (order, {'B'; 'A'}); ## Test 21: Custom Order (Subset / Filtering) %!test %! g = [1; 2; 3]; %! gh = [1; 2; 3]; %! [C, order] = confusionmat (g, gh, 'Order', [1; 2]); %! assert_equal (C, eye (2)); %! assert_equal (order, [1; 2]); ## Test 22: Custom Order (Superset / Adding empty rows) %!test %! g = [1; 2]; %! gh = [1; 2]; %! [C, order] = confusionmat (g, gh, 'Order', [1; 2; 4]); %! assert_equal (C, [1 0 0; 0 1 0; 0 0 0]); %! assert_equal (order, [1; 2; 4]); ## Test 23: All Mismatch %!test %! g = [1; 1; 1]; %! gh = [2; 2; 2]; %! [C, order] = confusionmat (g, gh); %! assert_equal (C, [0 3; 0 0]); %! assert_equal (order, [1; 2]); ## Test 24: Single Class Present %!test %! g = [1; 1; 1]; %! gh = [1; 1; 1]; %! [C, order] = confusionmat (g, gh); %! assert_equal (C, 3); %! assert_equal (order, 1); ## Test 25: Logical (Single Observation) %!test %! [C, order] = confusionmat (true, true); %! assert_equal (C, [0 0; 0 1]); %! assert_equal (order, [false; true]); ## Test 26: Logical (Only false Present) %!test %! [C, order] = confusionmat ([false; false], [false; false]); %! assert_equal (C, [2 0; 0 0]); %! assert_equal (order, [false; true]); ## Test 27: Logical (Empty Input) %!test %! [C, order] = confusionmat (true (0, 1), true (0, 1)); %! assert_equal (C, zeros (2)); %! assert_equal (order, [false; true]); ## Test 28: Logical (Custom Order) %!test %! [C, order] = confusionmat (true, true, 'Order', [true; false]); %! assert_equal (C, [1 0; 0 0]); %! assert_equal (order, [true; false]); ## Test 29: Logical (Numeric Order) %!test %! [C, order] = confusionmat (true, true, 'Order', [1; 0]); %! assert_equal (C, [1 0; 0 0]); %! assert_equal (order, [true; false]); %!test %! [C, order] = confusionmat ([true; false], [true; true], ... %! 'Order', int8 ([0; 1])); %! assert_equal (C, [0 1; 0 1]); %! assert_equal (order, [false; true]); %!test %! [C, order] = confusionmat ([true; false], [true; true], 'Order', [0; 2]); %! assert_equal (C, [0 1; 0 1]); %! assert_equal (order, [false; true]); ## Teset input validation %!error %! confusionmat ([1; 2], {'A'; 'B'}) %!error %! confusionmat ('A', [1]) %!error %! confusionmat ([1; 2; 3], [1; 2]) %!error %! confusionmat ([1; 2], [1; 2], 'Order', {'A'; 'B'}) %!error %! confusionmat ({'A'}, {'A'}, 'Order', [1]) %!error %! confusionmat (eye (2), eye (2)) %!error confusionmat ({1; 2}, {1; 2}) %!error ... %! confusionmat (true, true, 'Order', true) %!error ... %! confusionmat (true, true, 'Order', false) %!error ... %! confusionmat ([1; 2], [1; 2], 'Order', [2; 1; 2]) %!error ... %! confusionmat ({'a'; 'b'}, {'a'; 'b'}, 'Order', {'b'; 'a'; 'b'}) %!error ... %! confusionmat (['a'; 'b'], ['a'; 'b'], 'Order', ['b'; 'a'; 'b']) %!error ... %! confusionmat (string ({'a'; 'b'}), string ({'a'; 'b'}), ... %! 'Order', string ({'b'; 'a'; 'b'})) %!error ... %! confusionmat (categorical ({'a'; 'b'}), categorical ({'a'; 'b'}), ... %! 'Order', categorical ({'b'; 'a'; 'b'})) %!error ... %! confusionmat (true, true, 'Order', [true; true]) %!error ... %! confusionmat (true, true, 'Order', [1; 0; 1]) %!error ... %! confusionmat ([true; false], [true; true], 'Order', [0.5; 1]) statistics-release-2.1.2/inst/Model_Evaluation/crossval.m000066400000000000000000000322121527311104300235370ustar00rootroot00000000000000## Copyright (C) 2014 Nir Krakauer ## Copyright (C) 2025 Yassin Achengli ## Copyright (C) 2025 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{results} =} crossval (@var{f}, @var{X}, @var{y}) ## @deftypefnx {statistics} {@var{results} =} crossval (@var{f}, @var{X}, @var{y}, @var{name}, @var{value}) ## ## Perform cross validation on given data. ## ## @var{f} should be a function that takes 4 inputs @var{xtrain}, @var{ytrain}, ## @var{xtest}, @var{ytest}, fits a model based on @var{xtrain}, @var{ytrain}, ## applies the fitted model to @var{xtest}, and returns a goodness of fit ## measure based on comparing the predicted and actual @var{ytest}. ## @code{crossval} returns an array containing the values returned by @var{f} ## for every cross-validation fold or resampling applied to the given data. ## ## @var{X} should be an @var{n} by @var{m} matrix of predictor values ## ## @var{y} should be an @var{n} by @var{1} vector of predicand values ## ## Optional arguments may include name-value pairs as follows: ## ## @table @asis ## @item @qcode{'KFold'} ## Divide set into @var{k} equal-size subsets, using each one successively ## for validation. ## ## @item @qcode{'HoldOut'} ## Divide set into two subsets, training and validation. If the value ## @var{k} is a fraction, that is the fraction of values put in the ## validation subset (by default @var{k}=0.1); if it is a positive integer, ## that is the number of values in the validation subset. ## ## @item @qcode{'LeaveOut'} ## Leave-one-out partition (each element is placed in its own subset). ## The value is ignored, but it is required. ## ## @item @qcode{'Partition'} ## The value should be a @var{cvpartition} object. ## ## @item @qcode{'Given'} ## The value should be an @var{n} by @var{1} vector specifying in which ## partition to put each element. ## ## @item @qcode{'stratify'} ## The value should be an @var{n} by @var{1} vector containing class ## designations for the elements, in which case the @qcode{'KFold'} and ## @qcode{'HoldOut'} partitionings attempt to ensure each partition ## represents the classes proportionately. ## ## @item @qcode{'mcreps'} ## The value should be a positive integer specifying the number of times ## to resample based on different partitionings. Currently only works with ## the partition type @qcode{'HoldOut'}. ## ## @end table ## ## Only one of @qcode{'KFold'}, @qcode{'HoldOut'}, @qcode{'LeaveOut'}, ## @qcode{'Given'}, @qcode{'Partition'} should be specified. If none is ## specified, the default is @qcode{'KFold'} with @var{k} = 10. ## ## @seealso{cvpartition} ## @end deftypefn function results = crossval (f, varargin) ## Parse optional Name-Value paired arguments optNames = {'Holdout', 'KFold', 'Leaveout', 'MCReps', ... 'Partition', 'Stratify', 'Predfun'}; dfValues = {[], [], [], 1, [], [], []}; [Holdout, KFold, Leaveout, MCReps, Partition, Stratify, Predfun, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Check first input argument if (ischar (f)) ## Check for valid criterion if (! ismember (f, {'mse',',mcr'})) error ("crossval: criterion must be 'mse' or 'mcr'."); endif ## Check for user supplied prediction function handle if (! is_function_handle (Predfun)) error (strcat ("crossval: prediction function handle", ... " is required for error evaluation.")); endif ## At least two additional input arguments (X and y) are required nargs = numel (args); if (nargs < 2) error ("crossval: X and Y are required for error evaluation."); endif y = args{end}; if (nargs == 2) X = args{1}; # numeric matrix with single data variable n = size (X, 1); is_cellarray = false; else X = args(1:end-1); # cell array with multiple data variables n = size (args{1}, 1); is_cellarray = true; endif ## Check for valid prediction function handle on user data try if (is_cellarray) yFit = Predfun(X{:}, y, X{:}); else yFit = Predfun(X, y, X); endif catch error ("crossval: bad prediction function handle for error evaluation."); end_try_catch if (! iscolumn (yFit) || numel (yFit) != numel (y)) error (strcat ("crossval: prediction function handle must return", ... " a column vector with the same rows as XTest.")); endif elseif (is_function_handle (f)) ## At least one additional input argument (X) is required nargs = numel (args); if (nargs < 1) error ("crossval: X is required for values evaluation."); endif if (nargs == 1) X = args{1}; # numeric matrix with single data variable n = size (X, 1); is_cellarray = false; else X = args; # cell array with multiple data variables n = size (args{1}, 1); is_cellarray = true; endif ## Check for valid function handle on user data. Every data variable is ## passed for the training set and again for the test set, so a handle over ## X and Y is called as f (Xtrain, Ytrain, Xtest, Ytest). try if (is_cellarray) value = f(X{:}, X{:}); else value = f(X, X); endif catch error ("crossval: bad function handle to cross-validate."); end_try_catch if (isscalar (value)) is_scalar = true; elseif (isrow (value)) is_scalar = false; else error ("crossval: function handle must return a scalar or a row vector."); endif else error ("crossval: invalid first input argument."); endif ## Input validation for valid values are handled by the cvpartition class. ## Check for single paired argument for CV partition. vcpa = sum ([isempty(Holdout), isempty(KFold), ... isempty(Leaveout), isempty(Partition)]); if (vcpa < 3) # at least 3 must be empty error (strcat ("crossval: you can only set one", ... " cvpartition type in paired arguments.")); endif ## Check for Partition and Stratify paired arguments if (! isempty (Partition) && ! isempty (Stratify)) error ("crossval: you cannot specify both 'Partition' and 'Stratify'."); endif ## Construct the CV partition if (! isempty (Partition)) P = Partition; nSets = P.NumTestSets; if (P.IsCustom || ismember (P.Type, {'resubstitution', 'leaveout'})) MCReps = 1; endif elseif (! isempty (Leaveout)) P = cvpartition (n, 'LeaveOut'); nSets = P.NumTestSets; MCReps = 1; elseif (! isempty (Holdout)) if (isempty (Stratify)) P = cvpartition (n, 'HoldOut', Holdout); else P = cvpartition (Stratify, 'HoldOut', Holdout); endif nSets = P.NumTestSets; elseif (! isempty (KFold)) if (isempty (Stratify)) P = cvpartition (n, 'KFold', KFold); else P = cvpartition (Stratify, 'KFold', KFold); endif nSets = P.NumTestSets; else # KFold by default if (isempty (Stratify)) P = cvpartition (n, 'KFold'); else P = cvpartition (Stratify, 'KFold'); endif nSets = P.NumTestSets; endif ## Apply cross-validation scheme if (ischar (f)) # error evaluation results = nan (MCReps, nSets); for rep = 1:MCReps if (rep > 1) P = repartition (P); endif for idx = 1:nSets idx_train = training (P, idx); idx_test = test (P, idx); if (is_cellarray) Xtrain = cellfun (@(x) x(idx_train, :), X, 'UniformOutput', false); Xtest = cellfun (@(x) x(idx_test, :), X, 'UniformOutput', false); y_fit = Predfun(Xtrain{:}, y(idx_train), Xtest{:}); else y_fit = Predfun(X(idx_train, :), y(idx_train), X(idx_test, :)); endif if (strcmp (f, 'mse')) err = sum ((y_fit - y(idx_test)).^2) / numel (y_fit); results(rep, idx) = err; else # MCR err = sum (y_fit == y(idx_test)) / numel (y_fit); results(rep, idx) = err; endif endfor endfor results = mean (mean (results)); else # model execution if (is_scalar) results = nan (MCReps, nSets); for rep = 1:MCReps if (rep > 1) P = repartition (P); endif for idx = 1:nSets idx_train = training (P, idx); idx_test = test (P, idx); if (is_cellarray) Xtrain = cellfun (@(x) x(idx_train, :), X, 'UniformOutput', false); Xtest = cellfun (@(x) x(idx_test, :), X, 'UniformOutput', false); result = f(Xtrain{:}, Xtest{:}); else result = f(X(idx_train, :), X(idx_test, :)); endif results(rep, idx) = result; endfor endfor else # concatenate Monte Carlo repetitions along first dimension results = []; for rep = 1:MCReps if (rep > 1) P = repartition (P); endif tmpresults = []; for idx = 1:nSets idx_train = training (P, idx); idx_test = test (P, idx); if (is_cellarray) Xtrain = cellfun (@(x) x(idx_train, :), X, 'UniformOutput', false); Xtest = cellfun (@(x) x(idx_test, :), X, 'UniformOutput', false); result = f(Xtrain{:}, Xtest{:}); else result = f(X(idx_train, :), X(idx_test, :)); endif tmpresults = [tmpresults; result]; endfor results = [results; tmpresults]; endfor endif endif endfunction %!demo %! ## Determine the optimal number of clusters using cross-validation %! %! ## Declare a function to compute the sum of squared distances %! ## between data points and a varying number of clusters. %! function D = dist2clusters (X, Y, k) %! [Z, Zmu, Zstd] = zscore (X); %! [~, C] = kmeans (Z, k); %! ZY = (Y - Zmu) ./ Zstd; %! d = pdist2 (C, ZY, 'euclidean', 'Smallest', 1); %! D = sum (d .^ 2); %! endfunction %! %! load fisheriris %! for k = 1:8 %! fcn = @(X, Y) dist2clusters (X, Y, k); %! distances = crossval (fcn, meas); %! cvdist(k) = sum (distances); %! endfor %! %! plot (cvdist) %! xlabel ('Number of Clusters') %! ylabel ('CV Sum of Squared Distances') %! xlim ([1,8]); ## Test output %!test %! function yfit = regf (Xtrain, ytrain, Xtest) %! b = regress (ytrain, Xtrain); %! yfit = Xtest * b; %! endfunction %! %! load carsmall %! data = [Acceleration Horsepower Weight MPG]; %! data(any (isnan (data),2),:) = []; %! %! y = data(:,4); %! X = [ones(length(y),1) data(:,1:3)]; %! rand ('seed', 3); %! cvMSE = crossval ('mse',X,y,'Predfun',@regf); %! assert_equal (cvMSE, 18.720, 1e-3); ## Test input validation %!test %! ## With a response variable the handle is called over both, as %! ## f (Xtrain, Ytrain, Xtest, Ytest); Y used to be dropped and the handle %! ## called with two arguments, so any supervised handle was refused. %! X = [1, 2; 2, 3; 3, 4; 4, 5; 5, 6; 6, 7; 7, 8; 8, 9; 9, 10; 10, 11]; %! Y = (1:10)'; %! f = @(xtr, ytr, xte, yte) size (xtr, 1) + size (xte, 1) + numel (ytr) + numel (yte); %! r = crossval (f, X, Y, 'KFold', 5); %! assert_equal (size (r), [1, 5]); %! assert_equal (unique (r), 20); %!error ... %! crossval ('fe', rand (10, 1), rand (10, 1), 1); %!error ... %! crossval ('mse', rand (10, 1), rand (10, 1), 1); %!error ... %! crossval ('mse', rand (10, 1), 'Predfun', @(x,y) x + y); %!error ... %! crossval ('mse', rand (10, 3), rand (10, 1), 'Predfun', @(x,y) sum (x + y)); %!error ... %! crossval ('mse', rand (10, 3), rand (10, 1), 'Predfun', @(x,y,z) sum (x + y)); %!error crossval (@(x) x); %!error ... %! crossval (@(x) x, rand (10, 3), rand (10, 1)); %!error ... %! crossval (@(xtr, ytr, xte, yte) [xte, yte], rand (10, 3), rand (10, 1)); %!error crossval ({1}, 1, 1); %!error ... %! crossval (@(x,y) sum ([x; y]), rand (10, 3), 'Holdout', 0.1, 'Leaveout', true) %!error ... %! crossval (@(x,y) sum ([x; y]), rand (10, 3), 'Partition', cvpartition (10, 'Leaveout'), 'Stratify', true) statistics-release-2.1.2/inst/Model_Evaluation/cvpartition.m000066400000000000000000003477451527311104300242710ustar00rootroot00000000000000## Copyright (C) 2025 Andreas Bertsatos ## Copyright (C) 2025 Avanish Salunke ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef cvpartition ## -*- texinfo -*- ## @deftp {statistics} cvpartition ## ## Partition data for cross-validation ## ## The @code{cvpartition} class generates a partitioning scheme on a dataset ## to facilitate cross-validation of statistical models utilizing training and ## testing subsets of the dataset. ## ## @seealso{crossval} ## @end deftp properties(GetAccess = public, SetAccess = private) ## -*- texinfo -*- ## @deftp {cvpartition} {property} NumObservations ## ## Number of observations ## ## A positive integer scalar specifying the number of observations in the ## dataset (including any missing data, where applicable). This property ## is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {cvpartition} {property} NumTestSets ## ## Number of test sets ## ## A positive integer scalar specifying the number of folds for partition ## types @qcode{'kfold'} and @qcode{'leaveout'}. When partition type is ## @qcode{'holdout'} and @qcode{'resubstitution'}, then @qcode{NumTestSets} ## is 1. This property is read-only. ## ## @end deftp NumTestSets = []; ## -*- texinfo -*- ## @deftp {cvpartition} {property} TrainSize ## ## Size of each train set ## ## A positive integer scalar specifying the size of the train set for ## partition types @qcode{'holdout'} and @qcode{'resubstitution'} or a ## vector of positive integers specifying the size of each training set for ## partition types @qcode{'kfold'} and @qcode{'leaveout'}. This property ## is read-only. ## ## @end deftp TrainSize = []; ## -*- texinfo -*- ## @deftp {cvpartition} {property} TestSize ## ## Size of each test set ## ## A positive integer scalar specifying the size of the test set for ## partition types @qcode{'holdout'} and @qcode{'resubstitution'} or a ## vector of positive integers specifying the size of each testing set for ## partition types @qcode{'kfold'} and @qcode{'leaveout'}. This property ## is read-only. ## ## @end deftp TestSize = []; ## -*- texinfo -*- ## @deftp {cvpartition} {property} Type ## ## Type of validation partition ## ## A character vector specifying the type of the @qcode{cvpartition} object. ## It can be @qcode{kfold}, @qcode{holdout}, @qcode{leaveout}, or ## @qcode{resubstitution}. This property is read-only. ## ## @end deftp Type = ''; ## -*- texinfo -*- ## @deftp {cvpartition} {property} IsCustom ## ## Flag for custom partition ## ## A logical scalar specifying whether the @qcode{cvpartition} object ## was created using custom partition partitioning (@qcode{true}) or ## not (@qcode{false}). This property is read-only. ## ## @end deftp IsCustom = []; ## -*- texinfo -*- ## @deftp {cvpartition} {property} IsGrouped ## ## Flag for grouped partition ## ## A logical scalar specifying whether the @qcode{cvpartition} object was ## created using grouping variables (@qcode{true}) or not (@qcode{false}). ## This property is read-only. ## ## @end deftp IsGrouped = []; ## -*- texinfo -*- ## @deftp {cvpartition} {property} IsStratified ## ## Flag for stratified partition ## ## A logical scalar specifying whether the @qcode{cvpartition} object was ## created with a @qcode{'stratify'} value of @qcode{true}. ## This property is read-only. ## ## @end deftp IsStratified = []; endproperties properties(Access = private, Hidden) missidx = []; indices = []; cvptype = ''; classes = []; classID = []; grpvars = []; endproperties methods(Hidden) ## Custom display function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ('%s =\n', in_name); endif disp (this); endfunction ## Custom display function disp (this) fprintf ("\n%s\n", this.cvptype); ## Print selected properties fprintf ("%+25s: %d\n", 'NumObservations', this.NumObservations); fprintf ("%+25s: %d\n", 'NumTestSets', this.NumTestSets); vlen = numel (this.TrainSize); if (vlen <= 10) str = repmat ({'%d'}, 1, vlen); str = strcat ('[', strjoin (str, ' '), ']'); str1 = sprintf (str, this.TrainSize); str2 = sprintf (str, this.TestSize); else str = repmat ({'%d'}, 1, 10); str = strcat ('[', strjoin (str, ' '), ' ... ]'); str1 = sprintf (str, this.TrainSize(1:10)); str2 = sprintf (str, this.TestSize(1:10)); endif fprintf ("%+25s: %s\n", 'TrainSize', str1); fprintf ("%+25s: %s\n", 'TestSize', str2); fprintf ("%+25s: %d\n", 'IsCustom', this.IsCustom); fprintf ("%+25s: %d\n", 'IsGrouped', this.IsGrouped); fprintf ("%+25s: %d\n\n", 'IsStratified', this.IsStratified); endfunction ## Class specific subscripted reference function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); t = 'Invalid %s indexing for referencing values in a cvpartition object.'; switch (s.type) case '()' error (t, '()'); case '{}' error (t, '{}'); case '.' if (! ischar (s.subs)) error (strcat ("cvpartition.subsref: '.' indexing", ... " argument must be a character vector.")); endif try out = this.(s.subs); catch error ("cvpartition.subref: unrecognized property: '%s'", s.subs); end_try_catch endswitch ## Chained references if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction ## Class specific subscripted assignment function this = subsasgn (this, s, val) if (numel (s) > 1) error (strcat ("cvpartition.subsasgn:", ... " chained subscripts not allowed.")); endif t = 'Invalid %s indexing for assigning values to a cvpartition object.'; switch s.type case '()' error (t, '()'); case '{}' error (t, '{}'); case '.' if (! ischar (s.subs)) error (strcat ("cvpartition.subsasgn: '.' indexing", ... " argument must be a character vector.")); endif error (strcat ("cvpartition.subsasgn: unrecognized", ... " or read-only property: '%s'"), s.subs); endswitch endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {cvpartition} {@var{C} =} cvpartition (@var{n}, @qcode{'KFold'}) ## @deftypefnx {cvpartition} {@var{C} =} cvpartition (@var{n}, @qcode{'KFold'}, @var{k}) ## @deftypefnx {cvpartition} {@var{C} =} cvpartition (@var{n}, @qcode{'KFold'}, @var{k}, @qcode{'GroupingVariables'}, @var{grpvars}) ## @deftypefnx {cvpartition} {@var{C} =} cvpartition (@var{n}, @qcode{'Holdout'}) ## @deftypefnx {cvpartition} {@var{C} =} cvpartition (@var{n}, @qcode{'Holdout'}, @var{p}) ## @deftypefnx {cvpartition} {@var{C} =} cvpartition (@var{n}, @qcode{'Holdout'}, @var{p}, @qcode{'GroupingVariables'}, @var{grpvars}) ## @deftypefnx {cvpartition} {@var{C} =} cvpartition (@var{n}, @qcode{'Leaveout'}) ## @deftypefnx {cvpartition} {@var{C} =} cvpartition (@var{n}, @qcode{'Leaveout'}, @qcode{'GroupingVariables'}, @var{grpvars}) ## @deftypefnx {cvpartition} {@var{C} =} cvpartition (@var{n}, @qcode{'Resubstitution'}) ## @deftypefnx {cvpartition} {@var{C} =} cvpartition (@var{X}, @qcode{'KFold'}) ## @deftypefnx {cvpartition} {@var{C} =} cvpartition (@var{X}, @qcode{'KFold'}, @var{k}) ## @deftypefnx {cvpartition} {@var{C} =} cvpartition (@var{X}, @qcode{'KFold'}, @var{k}, @qcode{'Stratify'}, @var{opt}) ## @deftypefnx {cvpartition} {@var{C} =} cvpartition (@var{X}, @qcode{'Holdout'}) ## @deftypefnx {cvpartition} {@var{C} =} cvpartition (@var{X}, @qcode{'Holdout'}, @var{p}) ## @deftypefnx {cvpartition} {@var{C} =} cvpartition (@var{X}, @qcode{'Holdout'}, @var{p}, @qcode{'Stratify'}, @var{opt}) ## @deftypefnx {cvpartition} {@var{C} =} cvpartition (@qcode{'CustomPartition'}, @var{testSets}) ## ## Repartition data for cross-validation. ## ## @code{@var{C} = cvpartition (@var{n}, @qcode{'KFold'})} creates a ## @qcode{cvpartition} object @var{C}, which defines a random nonstratified ## partition for k-fold cross-validation on @var{n} observations with each ## fold (subsample) having approximately the same number of observations. ## The default number of folds is 10 for @code{@var{n} >= 10} or equal to ## @var{n} otherwise. ## ## @code{@var{C} = cvpartition (@var{n}, @qcode{'KFold'}, @var{k})} also ## creates a nonstratified random partition for k-fold cross-validation with ## the number of folds defined by @var{k}, which must be a positive integer ## scalar smaller than the number of observations @var{n}. ## ## @code{@var{C} = cvpartition (@var{n}, @qcode{'KFold'}, @var{k}, ## @qcode{'GroupingVariables'}, @var{grpvars})} creates a ## @qcode{cvpartition} ## object @var{C} that defines a random partition for k-fold ## cross-validation in which every observation sharing a group label, as ## defined by @var{grpvars}, is assigned to the same fold. No group is ## split between the training and test sets, so a fold holds out whole ## groups; this is the partition scikit-learn calls @code{GroupKFold}, and ## it is what you want when observations within a group are not ## independent, such as repeated measurements of one subject. It is not ## stratification: a fold does not contain a proportional mix of the group ## labels, and @qcode{'Stratify'} is the option for that. The grouping ## variables specified in @var{grpvars} can be one of the following: ## ## @itemize ## @item A numeric vector, logical vector, categorical vector, character ## array, string array, or cell array of character vectors containing one ## grouping variable. ## @item A numeric matrix or cell array containing two or more grouping ## variables. Each column in the matrix or array must correspond to one ## grouping variable. ## @end itemize ## ## @qcode{'GroupingVariables'} is an Octave extension: MATLAB's ## @code{cvpartition} has no such option. It follows the group-aware ## splitters of scikit-learn, and each partition type takes the analogue of ## its ungrouped self: ## ## @multitable @columnfractions 0.28 0.34 0.38 ## @headitem Partition type @tab scikit-learn analogue @tab Effect ## @item @qcode{'KFold'} @tab @code{GroupKFold} @tab whole groups fill each ## fold ## @item @qcode{'Holdout'} @tab @code{GroupShuffleSplit} @tab whole groups ## are held out ## @item @qcode{'Leaveout'} @tab @code{LeaveOneGroupOut} @tab one whole ## group is held out at a time ## @item @qcode{'Resubstitution'} @tab none @tab an error: nothing can be ## held out of a partition that holds everything ## @end multitable ## ## Given a stratification variable, @qcode{'GroupingVariables'} may ## follow @qcode{'Stratify'}, as in @code{cvpartition (@var{y}, ## @qcode{'KFold'}, @var{k}, @qcode{'Stratify'}, true, ## @qcode{'GroupingVariables'}, @var{grpvars})}. Each group is then ## kept whole while the classes of @var{y} are spread as evenly over ## the folds as the groups allow, which is scikit-learn's ## @code{StratifiedGroupKFold}. The two demands conflict, since a ## group carries whatever class mix it has, so the balance is ## approximate: groups are placed largest first, each into the fold ## whose class counts it disturbs least. ## ## In every case an observation never appears in both the training and the ## test set as one of its group fellows, which is what makes grouping worth ## asking for when observations within a group are not independent. ## ## @code{@var{C} = cvpartition (@var{n}, @qcode{'Holdout'})} creates a ## @qcode{cvpartition} object @var{C}, which defines a random nonstratified ## partition for holdout validation on @var{n} observations. 90% of the ## observations are assigned to the training set and the remaining 10% to ## the test set. ## ## @code{@var{C} = cvpartition (@var{n}, @qcode{'Holdout'}, @var{p})} also ## creates a nonstratified random partition for holdout validation with the ## percentage of training and test sets defined by @var{p}, which can be a ## scalar value in the range @math{(0,1)} or a positive integer scalar in ## the range @math{[1,@var{n})}. ## ## @code{@var{C} = cvpartition (@var{n}, @qcode{'Leaveout'})} creates a ## @qcode{cvpartition} object @var{C}, which defines a random partition for ## leave-one-out cross-validation on @var{n} observations. This is a ## special case of k-fold cross-validation with the number of folds equal to ## the number of observations. ## ## @code{@var{C} = cvpartition (@var{n}, @qcode{'Resubstitution'})} creates ## a @qcode{cvpartition} object @var{C} without partitioning the data and ## both training and test sets containing all observations @var{n}. ## ## @code{@var{C} = cvpartition (@var{X}, @qcode{'KFold'})} creates a ## @qcode{cvpartition} object @var{C}, which defines a stratified random ## partition for k-fold cross-validation according to the class proportions ## in @var{X}. @var{X} can be a numeric, logical, categorical, or string ## vector, or a character array or a cell array of character vectors. ## Missing values in @var{X} are discarded. The default number of folds is ## 10 for @code{numel (@var{X}) >= 10} or equal to @code{numel (@var{X})} ## otherwise. ## ## @code{@var{C} = cvpartition (@var{X}, @qcode{'KFold'}, @var{k})} also ## creates a stratified random partition for k-fold cross-validation with ## the number of folds defined by @var{k}, which must be a positive integer ## scalar smaller than the number of observations in @var{X}. ## ## @code{@var{C} = cvpartition (@var{X}, @qcode{'KFold'}, @var{k}, ## @qcode{'Stratify'}, @var{opt})} creates a random partition for k-fold ## cross-validation, which is stratified if @var{opt} is @qcode{true}, or ## nonstratified if @var{opt} is @qcode{false}. ## ## @code{@var{C} = cvpartition (@var{X}, @qcode{'Holdout'})} creates a ## @qcode{cvpartition} object @var{C}, which defines a stratified random ## partition for holdout validation while maintaining the class proportions ## in @var{X}. 90% of the observations are assigned to the training set and ## the remaining 10% to the test set. ## ## @code{@var{C} = cvpartition (@var{X}, @qcode{'Holdout'}, @var{p})} also ## creates a stratified random partition for holdout validation with the ## percentage of training and test sets defined by @var{p}, which can be a ## scalar value in the range @math{(0,1)} or a positive integer scalar in ## the range @math{[1,@var{n})}. ## ## @code{@var{C} = cvpartition (@var{X}, @qcode{'Holdout'}, @var{p}, ## @qcode{'Stratify'}, @var{opt})} creates a random partition for holdout ## validation, which is stratified if @var{opt} is @qcode{true}, or ## nonstratified if @var{opt} is @qcode{false}. ## ## @code{@var{C} = cvpartition (@qcode{'CustomPartition'}, @var{testSets})} ## creates a custom partition according to @var{testSets}, which can be a ## positive integer vector, a logical vector, or a logical matrix according ## to the following options: ## @itemize ## @item A positive integer vector of length @math{N} with values in the ## range @math{[1,K]}, where @math{K < N}, will specify a K-fold ## cross-validation partition, in which each value indicates the test set ## of each observation. Alternatively, the same vector with values in the ## range @math{[1,N]} will specify a leave-one-out cross-validation. ## @item A logical vector will specify a holdout validation, in which the ## @qcode{true} elements correspond to the test set and the @qcode{false} ## elements correspond to the training set. ## @item A logical matrix with @math{K} columns will specify a K-fold ## cross-validation partition, in which each column corresponds to a fold ## and each row to an observation. Alternatively, an @math{N*N} logical ## matrix will specify a leave-one-out cross-validation, where @math{N} is ## the number of observations. @qcode{true} elements correspond to the ## test set and the @qcode{false} elements correspond to the training set. ## @end itemize ## ## @seealso{cvpartition, cvpartition.summary, cvpartition.test, ## cvpartition.training} ## @end deftypefn function this = cvpartition (X, varargin) ## Check for appropriate number of input arguments if (nargin < 2) error ("cvpartition: too few input arguments."); endif if (nargin > 7) error ("cvpartition: too many input arguments."); endif ## Check for custom partition if (strcmpi (X, 'CustomPartition')) testSets = varargin{1}; ## Check for valid test set if (! (isnumeric (testSets) || islogical (testSets))) error ("cvpartition: TESTSETS must be numeric of logical."); endif if (isnumeric (testSets)) if (! isvector (testSets)) error ("cvpartition: TESTSETS must be a numeric vector."); endif [~, idx, inds] = unique (testSets); this.NumObservations = numel (testSets); this.NumTestSets = numel (idx); nvec = this.NumObservations * ones (1, this.NumTestSets); if (this.NumTestSets < this.NumObservations) this.indices = inds; for i = 1:this.NumTestSets this.TestSize(i) = sum (inds == i); endfor this.TrainSize = nvec - this.TestSize; this.Type = 'kfold'; this.cvptype = 'K-fold cross validation partition'; else this.TrainSize = nvec - 1; this.TestSize = nvec - this.TrainSize; this.Type = 'leaveout'; this.cvptype = 'Leave-one-out cross validation partition'; endif else # logical vector of matrix if (! ismatrix (testSets)) error ("cvpartition: TESTSETS must be a logical vector or matrix."); elseif (isvector (testSets)) this.NumObservations = numel (testSets); this.NumTestSets = 1; this.indices = testSets; this.TrainSize = sum (! testSets); this.TestSize = sum (testSets); this.Type = 'holdout'; this.cvptype = 'Hold-out cross validation partition'; else # logical matrix ## Each observation must be present in exactly one test set if (any (sum (testSets, 2) > 1)) error (strcat ("cvpartition: each observation in TESTSETS", ... " must be exactly one in each row.")); endif [this.NumObservations, this.NumTestSets] = size (testSets); nvec = this.NumObservations * ones (1, this.NumTestSets); if (this.NumTestSets < this.NumObservations) this.indices = zeros (this.NumObservations, 1); for i = 1:this.NumTestSets this.TestSize(i) = sum (testSets(:,i)); this.indices(testSets(:,i)) = i; endfor this.TrainSize = nvec - this.TestSize; this.Type = 'kfold'; this.cvptype = 'K-fold cross validation partition'; elseif (this.NumTestSets == this.NumObservations) this.TrainSize = nvec - 1; this.TestSize = nvec - this.TrainSize; this.Type = 'leaveout'; this.cvptype = 'Leave-one-out cross validation partition'; else error (strcat ("cvpartition: a logical matrix in TESTSETS", ... " must not have more columns that rows.")); endif endif endif this.IsCustom = true; this.IsGrouped = false; this.IsStratified = false; ## Check first input being a scalar value elseif (isscalar (X)) if (! (isnumeric (X) && X > 0 && fix (X) == X)) error ("cvpartition: X must be a scalar positive integer value."); endif ## Get number of observations and partition type this.NumObservations = X; type = varargin{1}; this.IsCustom = false; this.IsStratified = false; ## "Resubstitution" if (strcmpi (type, 'resubstitution')) if (nargin > 2 && ischar (varargin{2}) && strcmpi (varargin{2}, 'groupingvariables')) error (strcat ("cvpartition: 'GroupingVariables' does not", ... " apply to 'resubstitution': the training and", ... " test sets are both the whole sample, so no", ... " group can be held out of either.")); endif this.NumTestSets = 1; this.TrainSize = X; this.TestSize = X; this.Type = 'resubstitution'; this.cvptype = 'Resubstitution (no partition of data)'; this.IsGrouped = false; ## "Leaveout" elseif (strcmpi (type, 'leaveout')) this.Type = 'leaveout'; ## A non-character third argument has never meant anything here and ## is still ignored; a character one must name 'GroupingVariables'. if (nargin > 2 && ischar (varargin{2})) if (! strcmpi (varargin{2}, 'groupingvariables')) error (strcat ("cvpartition: invalid optional paired", ... " argument for 'GroupingVariables'.")); endif if (nargin < 4) error (strcat ("cvpartition: missing value for optional", ... " paired argument 'GroupingVariables'.")); endif [inds, NumGroups, GroupSize, this.missidx, grpvars] = ... __resolve_groups__ (varargin{3}); nobs = X - sum (this.missidx); if (nobs != numel (inds)) error (strcat ("cvpartition: grouping variable does", ... " not match the number of observations.")); endif this.grpvars = grpvars; ## Each group is held out in turn: leave-one-group-out this.indices = inds; this.NumTestSets = NumGroups; this.TestSize = GroupSize; this.TrainSize = X * ones (1, NumGroups) - this.TestSize; this.cvptype = 'Leave-one-group-out cross validation partition'; this.IsGrouped = true; else this.NumTestSets = X; this.TrainSize = (X - 1) * ones (1, X); this.TestSize = ones (1, X); this.cvptype = 'Leave-one-out cross validation partition'; this.IsGrouped = false; endif ## "Holdout" elseif (strcmpi (type, 'holdout')) if (nargin > 2) p = varargin{2}; if (! isnumeric (p) || ! isscalar (p)) error (strcat ("cvpartition: P value for 'holdout'", ... " must be a numeric scalar.")); endif if (! ((p > 0 && p < 1) || (p == fix (p) && p > 0 && p < X))) error (strcat ("cvpartition: P value for 'holdout' must be", ... " a scalar in the range (0,1) or an integer", ... " scalar in the range [1, N).")); endif else p = 0.1; endif this.NumTestSets = 1; if (p < 1) # target fraction to sample p = round (p * X); # number of samples endif this.Type = 'holdout'; ## 'GroupingVariables' holds out whole groups instead of individual ## observations, drawing groups at random until the test set is at ## least the requested size grouped = (nargin > 3 && ischar (varargin{3}) && strcmpi (varargin{3}, 'groupingvariables')); if (nargin > 3 && ! grouped) error (strcat ("cvpartition: invalid optional paired", ... " argument for 'GroupingVariables'.")); endif if (grouped) if (nargin < 5) error (strcat ("cvpartition: missing value for optional", ... " paired argument 'GroupingVariables'.")); endif [gidx, NumGroups, GroupSize, this.missidx, grpvars] = ... __resolve_groups__ (varargin{4}); nobs = X - sum (this.missidx); if (nobs != numel (gidx)) error (strcat ("cvpartition: grouping variable does", ... " not match the number of observations.")); endif this.grpvars = grpvars; order = randsample (NumGroups, NumGroups); held = false (NumGroups, 1); ntest = 0; for i = 1:NumGroups if (ntest >= p) break; endif held(order(i)) = true; ntest += GroupSize(order(i)); endfor if (all (held)) error (strcat ("cvpartition: holding out %d observations", ... " needs every group; leave a group for", ... " training."), p); endif inds = false (X, 1); inds(! this.missidx) = held(gidx); this.cvptype = 'Group hold-out cross validation partition'; this.IsGrouped = true; else inds = false (X, 1); inds(randsample (X, p)) = true; # indices for test set this.cvptype = 'Hold-out cross validation partition'; this.IsGrouped = false; endif this.indices = inds; this.TrainSize = sum (! inds); this.TestSize = sum (inds); ## "KFold" elseif (strcmpi (type, 'kfold')) this.Type = 'kfold'; if (nargin > 2) k = varargin{2}; if (! isnumeric (k) || ! isscalar (k)) error (strcat ("cvpartition: K value for 'kfold'", ... " must be a numeric scalar.")); endif else if (X < 10) k = X; else k = 10; endif endif ## No grouping variables if (nargin < 4) if (! (k == fix (k) && k > 0 && k <= X)) error (strcat ("cvpartition: K value for 'kfold' must be", ... " an integer scalar in the range [1, N].")); endif this.NumTestSets = k; indices = floor ((0:(X - 1))' * (k / X)) + 1; indices = randsample (indices, X); nvec = X * ones (1, k); for i = 1:k this.TestSize(i) = sum (indices == i); endfor this.indices = indices; this.TrainSize = nvec - this.TestSize; this.cvptype = 'K-fold cross validation partition'; this.IsGrouped = false; else # with grouping variables if (! strcmpi (varargin{3}, 'groupingvariables')) error (strcat ("cvpartition: invalid optional paired", ... " argument for 'GroupingVariables'.")); endif if (nargin < 5) error (strcat ("cvpartition: missing value for optional", ... " paired argument 'GroupingVariables'.")); endif grpvars = varargin{4}; [inds, NumGroups, GroupSize, this.missidx, grpvars] = ... __resolve_groups__ (grpvars); X -= sum (this.missidx); if (X != numel (inds)) error (strcat ("cvpartition: grouping variable does", ... " not match the number of observations.")); endif this.grpvars = grpvars; ## Compare k-fold to number of groups and reduce K accordingly if (k > NumGroups) warning (strcat ("cvpartition: number of folds K is greater", ... " than the groups in 'GroupingVariables'.", ... " K is set to the number of groups.")); k = NumGroups; endif ## If k == NumGroups, then each group becomes a test in a fold. ## If k < NumGroups, then cluster NumGroups to k folds. indices = zeros (X, 1); if (k == NumGroups) for i = 1:k indices(inds == i) = i; endfor else [GroupIdx, ~, GroupSz] = multiway (GroupSize, k, 'completeKK'); for i = 1:k idxGV = find (GroupIdx == i); vecGV = arrayfun (@(x) x == inds, idxGV, 'UniformOutput', false); index = vecGV{1}; if (numel (vecGV) > 1) for j = 2:numel (vecGV) index = index | vecGV{j}; endfor endif indices(index) = i; endfor endif ## Randomize the order of folds random_idx = randsample ([1:k], k); randomized = zeros (size (inds)); for i = 1:k randomized(indices == i) = random_idx(i); endfor ## Save values to properties this.indices = randomized; this.NumTestSets = k; nvec = X * ones (1, k); for i = 1:k this.TestSize(i) = sum (this.indices == i); endfor this.TrainSize = nvec - this.TestSize; this.cvptype = 'Group K-fold cross validation partition'; this.IsGrouped = true; endif ## Invalid paired argument else error ("cvpartition: invalid optional paired argument."); endif ## A grouping variable may name its groups in the rows of a character ## matrix, which is one label per row and not one per element. Such a ## matrix is not a vector, so it is turned into the cell array of names ## it stands for and taken down the vector branch below. A character ## vector is left alone: there each element is already an observation. elseif (ischar (X) && ! isvector (X)) this = cvpartition (cellstr (X), varargin{:}); return; ## Check first input being a vector for stratification elseif (isvector (X)) ## Get number of observations (including missing values) this.NumObservations = numel (X); ## Remove missing values from partitioning. ## Keep missing index to include them in the test indices. this.missidx = ismissing (X); X(this.missidx) = []; ## Get stratify option if (nargin < 4) this.IsStratified = true; else if (! strcmpi (varargin{3}, 'stratify')) error (strcat ("cvpartition: invalid optional paired", ... " argument for stratification.")); endif if (nargin < 5) error (strcat ("cvpartition: missing value for optional", ... " paired argument 'stratify'.")); endif if (! isscalar (varargin{4}) || ! islogical (varargin{4})) error (strcat ("cvpartition: invalid value for optional", ... " paired argument 'stratify'.")); endif this.IsStratified = varargin{4}; endif ## 'GroupingVariables' may follow 'Stratify', giving a partition that ## keeps each group whole and the classes balanced across folds grpvars = []; if (nargin > 5) if (! (ischar (varargin{5}) && strcmpi (varargin{5}, 'groupingvariables'))) error (strcat ("cvpartition: invalid optional paired", ... " argument for 'GroupingVariables'.")); endif if (nargin < 7) error (strcat ("cvpartition: missing value for optional", ... " paired argument 'GroupingVariables'.")); endif grpvars = varargin{6}; if (isempty (grpvars)) error (strcat ("cvpartition: invalid value for optional", ... " paired argument 'GroupingVariables'.")); endif endif ## Handle stratification if (this.IsStratified) [classID, idx, classes] = unique (X); NumClasses = numel (idx); for i = 1:NumClasses ClassSize(i) = sum (classes == i); endfor this.classes = classes; this.classID = classID; endif X = numel (X); ## Get partition type type = varargin{1}; this.IsCustom = false; this.IsGrouped = false; ## "Holdout" if (strcmpi (type, 'holdout')) this.Type = 'holdout'; if (nargin > 2) p = varargin{2}; if (! isnumeric (p) || ! isscalar (p)) error (strcat ("cvpartition: P value for 'holdout'", ... " must be a numeric scalar.")); endif if (! ((p > 0 && p < 1) || (p == fix (p) && p > 0 && p < X))) error (strcat ("cvpartition: P value for 'holdout' must be", ... " a scalar in the range (0,1) or an integer", ... " scalar in the range [1, N), where N is the", ... " number of nonmissing observations in X.")); endif else p = 0.1; endif this.NumTestSets = 1; if (this.IsStratified) if (p < 1) f = p; # target fraction to sample p = round (p * X); # number of test samples else f = p / X; endif inds = zeros (X, 1, 'logical'); k_check = 0; for i = 1:NumClasses ki = round (f * ClassSize(i)); inds(find (classes == i)(randsample (ClassSize(i), ki))) = true; k_check += ki; endfor if (k_check < p) # add random elements to test set to make it p inds(find (! inds)(randsample (X - k_check, p - k_check))) = true; elseif (k_check > p) # remove random elements from test set inds(find (inds)(randsample (k_check, k_check - p))) = false; endif this.cvptype = 'Stratified hold-out cross validation partition'; else if (p < 1) # target fraction to sample p = round (p * X); # number of samples endif inds = false (X, 1); inds(randsample (X, p)) = true; # indices for test set this.cvptype = 'Hold-out cross validation partition'; endif this.indices = inds; this.TrainSize = sum (! inds); this.TestSize = sum (inds); ## "KFold" elseif (strcmpi (type, 'kfold')) this.Type = 'kfold'; if (nargin > 2) k = varargin{2}; if (! isnumeric (k) || ! isscalar (k)) error (strcat ("cvpartition: K value for 'kfold'", ... " must be a numeric scalar.")); endif if (! (k == fix (k) && k > 0 && k <= X)) error (strcat ("cvpartition: K value for 'kfold' must be", ... " an integer scalar in the range [1, N],", ... " where N is the number of nonmissing", ... " observations in X.")); endif else if (X < 10) k = X; else k = 10; endif endif this.NumTestSets = k; if (this.IsStratified && ! isempty (grpvars)) ## Both: whole groups, classes balanced across folds [gidx, NumGroups, GroupSize] = __resolve_groups__ (grpvars); if (numel (gidx) != X) error (strcat ("cvpartition: grouping variable does", ... " not match the number of observations.")); endif if (k > NumGroups) warning (strcat ("cvpartition: number of folds K is greater", ... " than the groups in 'GroupingVariables'.", ... " K is set to the number of groups.")); k = NumGroups; this.NumTestSets = k; endif this.grpvars = grpvars; inds = __stratified_group_folds__ (this.classes, gidx, k); this.indices = inds; for i = 1:k this.TestSize(i) = sum (inds == i); endfor this.TrainSize = X * ones (1, k) - this.TestSize; this.cvptype = ... 'Stratified group K-fold cross validation partition'; this.IsGrouped = true; elseif (this.IsStratified) inds = nan (X, 1); pooled_idx = false (X, 1); do_warn = true; do_ceil = false; for i = 1:NumClasses cls_size = ClassSize(i); cls_k_eq = fix (cls_size / k) == (cls_size / k); ## Check that the elements in each class exceed the number of ## requested folds, otherwise emit a warning and add the class ## elements into a pooled class if (cls_size < k) if (do_warn) warning (strcat ("One or more of the unique class values", ... " in the stratification variable is not", ... " present in one or more folds.")); do_warn = false; endif pooled_idx = pooled_idx | classes == i; elseif (fix (X / k) == X / k) ## Make sure that when X / k = integer, all ## test/training sizes must be equal across all folds if (do_ceil && ! cls_k_eq) idx = ceil ((0:(cls_size - 1))' * (k / cls_size)); idx(idx == 0) = max (idx); do_ceil = false; else idx = floor ((0:(cls_size - 1))' * (k / cls_size)) + 1; tmp = arrayfun (@(x) numel (find (x == idx)), [1:k]); if (any (diff (tmp))) do_ceil = true; endif endif inds(classes == i) = randsample (idx, cls_size); else ## Alternate ordering over classes so that ## the subsets are more nearly the same size if (! do_ceil || cls_k_eq) idx = floor ((0:(cls_size - 1))' * (k / cls_size)) + 1; if (! cls_k_eq) do_ceil = true; endif else idx = floor (((cls_size - 1):-1:0)' * (k / cls_size)) + 1; do_ceil = false; endif inds(classes == i) = randsample (idx, cls_size); endif endfor ## Stratify pooled classes (if any). They must be distributed ## in a way to make the test/training sizes as equal as possible ## across folds. pooled_inds = find (pooled_idx); while (numel (pooled_inds) > 0) tmp = arrayfun (@(x) numel (find (x == inds)), [1:k]); [min_cls, min_idx] = min (tmp); [max_cls, max_idx] = max (tmp); if (min_cls != max_cls) inds(pooled_inds(1)) = min_idx; else inds(pooled_inds(1)) = randsample (k, 1); endif pooled_inds(1) = []; endwhile this.cvptype = 'Stratified K-fold cross validation partition'; elseif (! isempty (grpvars)) ## Groups without stratification: whole groups, and the ## stratification variable is not used, as 'Stratify' asked [gidx, NumGroups] = __resolve_groups__ (grpvars); if (numel (gidx) != X) error (strcat ("cvpartition: grouping variable does", ... " not match the number of observations.")); endif if (k > NumGroups) warning (strcat ("cvpartition: number of folds K is greater", ... " than the groups in 'GroupingVariables'.", ... " K is set to the number of groups.")); k = NumGroups; this.NumTestSets = k; endif this.grpvars = grpvars; gfold = randsample (NumGroups, NumGroups); gfold = mod (gfold - 1, k) + 1; inds = gfold(gidx); this.cvptype = 'Group K-fold cross validation partition'; this.IsGrouped = true; else inds = floor ((0:(X - 1))' * (k / X)) + 1; inds = randsample (inds, X); this.cvptype = 'K-fold cross validation partition'; endif this.indices = inds; nvec = X * ones (1, k); for i = 1:k this.TestSize(i) = sum (inds == i); endfor this.TrainSize = nvec - this.TestSize; ## Invalid paired argument else error ("cvpartition: invalid optional paired argument."); endif ## Otherwise first input is invalid else error ("cvpartition: invalid first input argument."); endif endfunction ## -*- texinfo -*- ## @deftypefn {cvpartition} {@var{Cnew} =} repartition (@var{C}) ## @deftypefnx {cvpartition} {@var{Cnew} =} repartition (@var{C}, @var{sval}) ## @deftypefnx {cvpartition} {@var{Cnew} =} repartition (@var{C}, @qcode{'legacy'}) ## ## Repartition data for cross-validation. ## ## @code{@var{Cnew} = repartition (@var{C})} creates a @qcode{cvpartition} ## object @var{Cnew} that defines a new random partition of the same type as ## the @qcode{cvpartition} @var{C}. ## ## @code{@var{Cnew} = repartition (@var{C}, @var{sval})} also uses the value ## of @var{sval} to set the state of the random generator used in ## repartitioning @var{C}. If @var{sval} is a vector, then the random ## generator is set using the @qcode{'state'} keyword as in ## @code{rand ("state", @var{sval})}. If @var{sval} is a scalar, then the ## @qcode{'seed'} keyword is used as in @code{rand ("seed", @var{sval})} to ## specify that old generators should be used. ## ## Seeding is confined to this call: the state of the random generator is ## saved beforehand and restored before @code{repartition} returns, so ## @var{sval} does not carry over into the random numbers the caller draws ## afterwards. @var{sval} is an Octave extension; MATLAB expects a ## @code{RandStream} object in this position, which Octave does not have. ## ## @code{@var{Cnew} = repartition (@var{C}, @qcode{'legacy'})} only applies ## to @qcode{cvpartition} objects @var{C} that use k-fold partitioning and ## it will repartition @var{C} in the same non-random manner that was ## previously used by the old-style @qcode{cvpartition} class of the ## statistics package. The @qcode{'legacy'} option does not apply to ## stratified or grouped partitions. ## ## @seealso{cvpartition, cvpartition.summary, cvpartition.test, ## cvpartition.training} ## @end deftypefn function this = repartition (this, sval = []) ## Emit error for custom partitions if (this.IsCustom) error ("cvpartition.repartition: cannot repartition a custom partition."); endif ## Handle legacy code with no randomization of kfold option if (strcmpi (sval, 'legacy')) if (strcmpi (this.Type, 'kfold')) X = this.NumObservations; k = this.NumTestSets; if (! (this.IsGrouped || this.IsStratified)) inds = floor ((0:(X - 1))' * (k / X)) + 1; this.indices = inds; nvec = X * ones (1, k); for i = 1:k this.TestSize(i) = sum (inds == i); endfor this.TrainSize = nvec - this.TestSize; else # legacy option does not apply for grouped or stratified error (strcat ("cvpartition.repartition: 'legacy' flag does", ... " not apply to stratified or grouped 'kfold'", ... " partitioned objects.")); endif return; else error (strcat ("cvpartition.repartition: 'legacy' flag is only", ... " valid for 'kfold' partitioned objects.")); endif endif ## Check sval if (! isempty (sval)) if (! (isvector (sval) && isnumeric (sval) && isreal (sval))) error (strcat ("cvpartition.repartition: SVAL must be", ... " a real scalar or vector.")); endif ## SVAL seeds this repartitioning only. The caller's generator is put ## back before returning -- on the error path too, hence onCleanup -- ## so that seeding a partition cannot silently make the rest of the ## session reproducible as well. Octave has no RandStream to confine ## the draw to, which is why the state has to be saved and restored. saved_rand_state = rand ('state'); rand_state_guard = onCleanup (@() rand ('state', saved_rand_state)); if (isscalar (sval)) rand ('seed', sval); else rand ('state', sval); endif endif ## Handle repartitioning of randomized holdout and kfold options if (strcmpi (this.Type, 'holdout')) p = this.TestSize; if (this.IsStratified) X = sum (! this.missidx); inds = false (X, 1); NumClasses = numel (this.classID); classes = this.classes; for i = 1:NumClasses ClassSize(i) = sum (classes == i); endfor f = p / X; k_check = 0; for i = 1:NumClasses ki = round (f * ClassSize(i)); inds(find (classes == i)(randsample (ClassSize(i), ki))) = true; k_check += ki; endfor if (k_check < p) # add random elements to test set to make it p inds(find (! inds)(randsample (X - k_check, p - k_check))) = true; elseif (k_check > p) # remove random elements from test set inds(find (inds)(randsample (k_check, k_check - p))) = false; endif else X = this.NumObservations; inds = false (X, 1); inds(randsample (X, p)) = true; # indices for test set endif this.indices = inds; elseif (strcmpi (this.Type, 'kfold')) k = this.NumTestSets; if (! (this.IsGrouped || this.IsStratified)) X = this.NumObservations; inds = floor ((0:(X - 1))' * (k / X)) + 1; inds = randsample (inds, X); this.indices = inds; nvec = X * ones (1, k); for i = 1:k this.TestSize(i) = sum (inds == i); endfor this.TrainSize = nvec - this.TestSize; elseif (this.IsGrouped) ## We only need resample the order of folds in this case ## Randomize the order of folds random_idx = randsample ([1:k], k); randomized = zeros (size (this.indices)); for i = 1:k randomized(this.indices == i) = random_idx(i); endfor ## Save values to properties this.indices = randomized; this.NumTestSets = k; nvec = sum (! this.missidx) * ones (1, k); for i = 1:k this.TestSize(i) = sum (this.indices == i); endfor this.TrainSize = nvec - this.TestSize; else # is stratified X = sum (! this.missidx); NumClasses = numel (this.classID); classes = this.classes; for i = 1:NumClasses ClassSize(i) = sum (classes == i); endfor inds = nan (X, 1); pooled_idx = false (X, 1); do_warn = true; do_ceil = false; for i = 1:NumClasses cls_size = ClassSize(i); cls_k_eq = fix (cls_size / k) == (cls_size / k); ## Check that the elements in each class exceed the number of ## requested folds, otherwise emit a warning and add the class ## elements into a pooled class if (cls_size < k) if (do_warn) warning (strcat ("One or more of the unique class values", ... " in the stratification variable is not", ... " present in one or more folds.")); do_warn = false; endif pooled_idx = pooled_idx | classes == i; elseif (fix (X / k) == X / k) ## Make sure that when X / k = integer, all ## test/training sizes must be equal across all folds if (do_ceil && ! cls_k_eq) idx = ceil ((0:(cls_size - 1))' * (k / cls_size)); idx(idx == 0) = max (idx); do_ceil = false; else idx = floor ((0:(cls_size - 1))' * (k / cls_size)) + 1; tmp = arrayfun (@(x) numel (find (x == idx)), [1:k]); if (any (diff (tmp))) do_ceil = true; endif endif inds(classes == i) = randsample (idx, cls_size); else ## Alternate ordering over classes so that ## the subsets are more nearly the same size if (! do_ceil || cls_k_eq) idx = floor ((0:(cls_size - 1))' * (k / cls_size)) + 1; if (! cls_k_eq) do_ceil = true; endif else idx = floor (((cls_size - 1):-1:0)' * (k / cls_size)) + 1; do_ceil = false; endif inds(classes == i) = randsample (idx, cls_size); endif endfor ## Stratify pooled classes (if any). They must be distributed ## in a way to make the test/training sizes as equal as possible ## across folds. pooled_inds = find (pooled_idx); while (numel (pooled_inds) > 0) tmp = arrayfun (@(x) numel (find (x == inds)), [1:k]); [min_cls, min_idx] = min (tmp); [max_cls, max_idx] = max (tmp); if (min_cls != max_cls) inds(pooled_inds(1)) = min_idx; else inds(pooled_inds(1)) = randsample (k, 1); endif pooled_inds(1) = []; endwhile this.indices = inds; nvec = X * ones (1, k); for i = 1:k this.TestSize(i) = sum (inds == i); endfor this.TrainSize = nvec - this.TestSize; endif endif endfunction ## -*- texinfo -*- ## @deftypefn {cvpartition} {@var{tbl} =} summary (@var{c}) ## ## Summarize stratified or grouped cross-validation partitions. ## ## @code{@var{tbl} = summary (@var{c})} returns a summary table @var{tbl} of ## the validation partition contained in the @code{cvpartition} object ## @var{c}. ## ## This method calculates the distribution of classes (if stratified) or ## groups (if grouped) across the entire dataset, as well as within every ## training and test set generated by the partition. ## ## @subheading Inputs ## @itemize ## @item @var{c} ## A @code{cvpartition} object. The object must satisfy two conditions: ## @enumerate ## @item The partition type (@code{c.Type}) must be @qcode{'kfold'} or ## @qcode{'holdout'}. ## @item The partition must be created with a stratification or grouping ## variable (i.e., @code{c.IsStratified} or @code{c.IsGrouped} must be ## @code{true}). ## @end enumerate ## @end itemize ## ## @subheading Outputs ## @itemize ## @item @var{tbl} ## A @code{table} object containing the summary statistics. The table ## contains one row for every unique label/group in every set (all, train, ## test). The columns are: ## @table @code ## @item Set ## The specific subset being described. Values include @qcode{'all'} (the ## full dataset), @qcode{'train1'}, @qcode{'test1'}, etc. ## @item SetSize ## The total number of observations in that specific set. ## @item Label ## The class or group identifier. If @code{c.IsStratified} is true, this ## column is named @code{StratificationLabel}. If @code{c.IsGrouped} is ## true, it is named @code{GroupLabel}. ## @item Count ## The number of observations of that label within the set. If stratified, ## this column is named @code{StratificationCount}; otherwise, ## @code{GroupCount}. ## @item PercentInSet ## The percentage of the set composed of that specific label. ## @end table ## @end itemize ## ## @seealso{cvpartition, cvpartition.repartition, cvpartition.test, ## cvpartition.training} ## @end deftypefn function tbl = summary (this) ## Validation Checks if (! (this.IsStratified || this.IsGrouped)) error ("cvpartition.summary: partition must be stratified or grouped."); endif if (! (strcmpi (this.Type, 'kfold') || strcmpi (this.Type, 'holdout'))) error ("cvpartition.summary: partition type must be 'kfold' or 'holdout'."); endif ## Prepare Labels and Data Map if (this.IsStratified) LabelVarName = 'StratificationLabel'; CountVarName = 'StratificationCount'; UniqueLabels = this.classID; DataMap = this.classes; else ## Grouped LabelVarName = 'GroupLabel'; CountVarName = 'GroupCount'; ## Use __unique__ internal helper to ensure stable rows if (isa (this.grpvars, 'categorical')) [UniqueLabels, ~, DataMap] = unique (this.grpvars, 'rows', 'stable'); else [UniqueLabels, ~, DataMap] = __unique__ (this.grpvars, 'rows', 'stable'); endif endif ## Calculate dimensions for preallocation NumLabels = size (UniqueLabels, 1); NumSets = 1 + (2 * this.NumTestSets); ## 1 ("all") + 2 * K (Train/Test) TotalRows = NumLabels * NumSets; ## Preallocate Columns col_Set = cell (TotalRows, 1); col_SetSize = zeros (TotalRows, 1); col_Count = zeros (TotalRows, 1); col_Percent = zeros (TotalRows, 1); ## Determine if Label column is text or numeric if (iscell (UniqueLabels) || isstring (UniqueLabels) || ischar (UniqueLabels)) col_Label = cell (TotalRows, 1); is_text_label = true; else col_Label = zeros (TotalRows, 1); is_text_label = false; endif ## Helper for populating data curr_idx = 1; ## Inline helper function to calculate stats function [c_set, c_size, c_lbl, c_cnt, c_pct, idx_next] = ... fill_rows (name, mask, map, u_lbl, n_lbl, ... c_set, c_size, c_lbl, c_cnt, c_pct, idx_start, is_txt) subset_map = map(mask); subset_size = numel (subset_map); for u = 1:n_lbl count = sum (subset_map == u); c_set{idx_start} = name; c_size(idx_start) = subset_size; if (is_txt) if (iscell (u_lbl)) c_lbl{idx_start} = u_lbl{u}; elseif (isstring (u_lbl)) ## Convert string object to char for cell storage c_lbl{idx_start} = char (u_lbl(u)); else c_lbl{idx_start} = u_lbl(u, :); endif else c_lbl(idx_start) = u_lbl(u); endif c_cnt(idx_start) = count; c_pct(idx_start) = (count / subset_size) * 100; idx_start = idx_start + 1; endfor idx_next = idx_start; endfunction ## Calculate Statistics ## --- Set: "all" --- all_mask = true (size (DataMap)); [col_Set, col_SetSize, col_Label, col_Count, col_Percent, curr_idx] = ... fill_rows ('all', all_mask, DataMap, UniqueLabels, NumLabels, ... col_Set, col_SetSize, col_Label, col_Count, col_Percent, ... curr_idx, is_text_label); ## --- Set: Folds --- for k = 1:this.NumTestSets if (strcmpi (this.Type, 'holdout')) test_mask = this.indices; else test_mask = (this.indices == k); endif train_name = sprintf ('train%d', k); [col_Set, col_SetSize, col_Label, col_Count, col_Percent, curr_idx] = ... fill_rows (train_name, ! test_mask, DataMap, UniqueLabels, NumLabels, ... col_Set, col_SetSize, col_Label, col_Count, col_Percent, ... curr_idx, is_text_label); test_name = sprintf ('test%d', k); [col_Set, col_SetSize, col_Label, col_Count, col_Percent, curr_idx] = ... fill_rows (test_name, test_mask, DataMap, UniqueLabels, NumLabels, ... col_Set, col_SetSize, col_Label, col_Count, col_Percent, ... curr_idx, is_text_label); endfor ## Construct Table if (exist ('string', 'class')) col_Set = string (col_Set); if (is_text_label) col_Label = string (col_Label); endif endif tbl = table (col_Set, col_SetSize, col_Label, col_Count, col_Percent, ... 'VariableNames', {'Set', 'SetSize', LabelVarName, ... CountVarName, 'PercentInSet'}); endfunction ## -*- texinfo -*- ## @deftypefn {cvpartition} {@var{idx} =} test (@var{C}) ## @deftypefnx {cvpartition} {@var{idx} =} test (@var{C}, @var{i}) ## @deftypefnx {cvpartition} {@var{idx} =} test (@var{C}, @qcode{'all'}) ## ## Test indices for cross-validation. ## ## @code{@var{idx} = test (@var{C})} returns a logical vector @var{idx} with ## @qcode{true} values indicating the elements corresponding to the test ## set defined in the @qcode{cvpartition} object @var{C}. For K-fold and ## leave-one-out partitions, the indices corresponding to the first test set ## are returned. ## ## @code{@var{idx} = test (@var{C}, @var{i})} returns a logical vector or ## matrix with the indices of the test set indicated by @var{i}. If @var{i} ## is a scalar, then @var{idx} is a logical vector with the indices of the ## @math{i-th} set. If @var{i} is a vector, then @var{idx} is a logical ## matrix in which @code{@var{idx}(:,j)} specified the observations in the ## test set @code{@var{i}(j)}. The value(s) in @var{i} must not exceed the ## number of tests in the @qcode{cvpartition} object @var{C}. ## ## @code{@var{idx} = test (@var{C}, @qcode{'all'})} returns a logical vector ## or matrix for all test sets defined in the @qcode{cvpartition} object ## @var{C}. For holdout and resubstitution partition types, a vector is ## returned. For K-fold and leave-one-out, a matrix is returned. ## ## @seealso{cvpartition, cvpartition.repartition, cvpartition.summary, ## cvpartition.training} ## @end deftypefn function idx = test (this, varargin) ## Check for sufficient input arguments if (nargin > 2) error ("cvpartition.test: too many input arguments."); elseif (nargin == 2) i = varargin{1}; if (strcmpi (i, 'all')) idx = logical ([]); switch (this.Type) case 'kfold' for i = 1:this.NumTestSets if (this.IsStratified || this.IsGrouped) cid = false (this.NumObservations, 1); cid(! this.missidx) = this.indices == i; else cid = this.indices == i; endif idx = [idx, cid]; endfor case 'leaveout' for i = 1:this.NumTestSets cid = false (this.NumObservations, 1); if (this.IsGrouped) cid(! this.missidx) = this.indices == i; else cid(i) = true; endif idx = [idx, cid]; endfor case 'holdout' if (this.IsStratified) idx = false (this.NumObservations, 1); idx(! this.missidx) = this.indices; else idx = this.indices; endif idx = this.indices; case 'resubstitution' # no stratification idx = true (this.NumObservations, 1); endswitch return elseif (isempty (i)) i = 1; endif else i = 1; endif if (! (isvector (i) && isnumeric (i) && all (fix (i) == i) && all (i > 0))) error ("cvpartition.test: set index must be a positive integer vector."); elseif (any (i > this.NumTestSets)) error ("cvpartition.test: set index exceeds 'NumTestSets'."); endif switch (this.Type) case 'kfold' if (isscalar (i)) if (this.IsStratified || this.IsGrouped) idx = false (this.NumObservations, 1); idx(! this.missidx) = this.indices == i; else idx = this.indices == i; endif else idx = logical ([]); for j = i if (this.IsStratified || this.IsGrouped) cid = false (this.NumObservations, 1); cid(! this.missidx) = this.indices == j; else cid = this.indices == j; endif idx = [idx, cid]; endfor endif case 'leaveout' if (isscalar (i)) idx = false (this.NumObservations, 1); if (this.IsGrouped) idx(! this.missidx) = this.indices == i; else idx(i) = true; endif else idx = logical ([]); for j = i new = false (this.NumObservations, 1); if (this.IsGrouped) new(! this.missidx) = this.indices == j; else new(j) = true; endif idx = [idx, new]; endfor endif case 'holdout' if (this.IsStratified) idx = false (this.NumObservations, 1); idx(! this.missidx) = this.indices; else idx = this.indices; endif case 'resubstitution' # no stratification idx = true (this.NumObservations, 1); endswitch endfunction ## -*- texinfo -*- ## @deftypefn {cvpartition} {@var{idx} =} training (@var{C}) ## @deftypefnx {cvpartition} {@var{idx} =} training (@var{C}, @var{i}) ## @deftypefnx {cvpartition} {@var{idx} =} training (@var{C}, @qcode{'all'}) ## ## Training indices for cross-validation. ## ## @code{@var{idx} = training (@var{C})} returns a logical vector @var{idx} ## with @qcode{true} values indicating the elements corresponding to the ## training set defined in the @qcode{cvpartition} object @var{C}. For ## K-fold and leave-one-out partitions, the indices corresponding to the ## first training set are returned. ## ## @code{@var{idx} = training (@var{C}, @var{i})} returns a logical vector ## or matrix with the indices of the training set indicated by @var{i}. If ## @var{i} is a scalar, then @var{idx} is a logical vector with the indices ## of the @math{i-th} set. If @var{i} is a vector, then @var{idx} is a ## logical matrix in which @code{@var{idx}(:,j)} specified the observations ## in the training set @code{@var{i}(j)}. The value(s) in @var{i} must not ## exceed the number of tests in the @qcode{cvpartition} object @var{C}. ## ## @code{@var{idx} = training (@var{C}, @qcode{'all'})} returns a logical ## vector or matrix for all training sets defined in the @qcode{cvpartition} ## object @var{C}. For holdout and resubstitution partition types, a vector ## is returned. For K-fold and leave-one-out, a matrix is returned. ## ## @seealso{cvpartition, cvpartition.repartition, cvpartition.summary, ## cvpartition.test} ## @end deftypefn function idx = training (this, varargin) ## Check for sufficient input arguments if (nargin > 2) error ("cvpartition.training: too many input arguments."); elseif (nargin == 2) i = varargin{1}; if (strcmpi (i, 'all')) idx = logical ([]); switch (this.Type) case 'kfold' for i = 1:this.NumTestSets if (this.IsStratified || this.IsGrouped) cid = false (this.NumObservations, 1); cid(! this.missidx) = this.indices != i; else cid = this.indices != i; endif idx = [idx, cid]; endfor case 'leaveout' for i = 1:this.NumTestSets cid = true (this.NumObservations, 1); if (this.IsGrouped) cid(! this.missidx) = this.indices != i; else cid(i) = false; endif idx = [idx, cid]; endfor case 'holdout' if (this.IsStratified) idx = false (this.NumObservations, 1); idx(! this.missidx) = ! this.indices; else idx = ! this.indices; endif case 'resubstitution' # no stratification idx = true (this.NumObservations, 1); endswitch return elseif (isempty (i)) i = 1; endif else i = 1; endif if (! (isvector (i) && isnumeric (i) && all (fix (i) == i) && all (i > 0))) error (strcat ("cvpartition.training: set index must", ... " be a positive integer vector.")); elseif (any (i > this.NumTestSets)) error ("cvpartition.training: set index exceeds 'NumTestSets'."); endif switch (this.Type) case 'kfold' if (isscalar (i)) if (this.IsStratified || this.IsGrouped) idx = false (this.NumObservations, 1); idx(! this.missidx) = this.indices != i; else idx = this.indices != i; endif else idx = logical ([]); for j = i if (this.IsStratified || this.IsGrouped) cid = false (this.NumObservations, 1); cid(! this.missidx) = this.indices != j; else cid = this.indices != j; endif idx = [idx, cid]; endfor endif case 'leaveout' if (isscalar (i)) idx = true (this.NumObservations, 1); if (this.IsGrouped) idx(! this.missidx) = this.indices != i; else idx(i) = false; endif else idx = logical ([]); for j = i new = true (this.NumObservations, 1); if (this.IsGrouped) new(! this.missidx) = this.indices != j; else new(j) = false; endif idx = [idx, new]; endfor endif case 'holdout' if (this.IsStratified) idx = false (this.NumObservations, 1); idx(! this.missidx) = ! this.indices; else idx = ! this.indices; endif case 'resubstitution' # no stratification idx = true (this.NumObservations, 1); endswitch endfunction endmethods endclassdef ## Test output results for custom partition %!test %! custom = [1, 1, 1, 2, 2, 2, 1, 2, 3, 2, 3, 3, 2, 1, 3]'; %! cv = cvpartition ('CustomPartition', custom); %! assert_equal (cv.Type, 'kfold'); %! assert_equal (cv.NumObservations, 15); %! assert_equal (cv.NumTestSets, 3); %! assert_equal (cv.TrainSize, [10, 9, 11]); %! assert_equal (cv.TestSize, [5, 6, 4]); %! assert_equal (cv.IsCustom, true); %! assert_equal (cv.IsGrouped, false); %! assert_equal (cv.IsStratified, false); %! idx = training (cv, 1); %! assert_equal (idx, custom != 1); %! idx = test (cv, 1); %! assert_equal (idx, custom == 1); %! idx = training (cv, 2); %! assert_equal (idx, custom != 2); %! idx = test (cv, 2); %! assert_equal (idx, custom == 2); %! idx = training (cv, 3); %! assert_equal (idx, custom != 3); %! idx = test (cv, 3); %! assert_equal (idx, custom == 3); %! idx1 = training (cv, 'all'); %! idx2 = test (cv, 'all'); %! assert_equal (idx1, ! idx2); %!test %! custom = logical ([1, 1, 1, 0, 0, 0, 1, 0, 1, 1])'; %! cv = cvpartition ('CustomPartition', custom); %! assert_equal (cv.Type, 'holdout'); %! assert_equal (cv.NumObservations, 10); %! assert_equal (cv.NumTestSets, 1); %! assert_equal (cv.TrainSize, 4); %! assert_equal (cv.TestSize, 6); %! assert_equal (cv.IsCustom, true); %! assert_equal (cv.IsGrouped, false); %! assert_equal (cv.IsStratified, false); %! idx = training (cv, 1); %! assert_equal (idx, custom != 1); %! assert_equal (idx, training (cv, 'all')); %! idx = test (cv, 1); %! assert_equal (idx, custom == 1); %! assert_equal (idx, test (cv, 'all')); %!test %! custom = logical ([1, 0, 0; 0, 1, 0; 1, 0, 0; 0, 0, 1]); %! cv = cvpartition ('CustomPartition', custom); %! assert_equal (cv.Type, 'kfold'); %! assert_equal (cv.NumObservations, 4); %! assert_equal (cv.NumTestSets, 3); %! assert_equal (cv.TrainSize, [2, 3, 3]); %! assert_equal (cv.TestSize, [2, 1, 1]); %! assert_equal (cv.IsCustom, true); %! assert_equal (cv.IsGrouped, false); %! assert_equal (cv.IsStratified, false); %! idx = training (cv, 1); %! assert_equal (idx, custom(:,1) == false); %! idx = test (cv, 1); %! assert_equal (idx, custom(:,1) == true); %! idx = training (cv, 2); %! assert_equal (idx, custom(:,2) == false); %! idx = test (cv, 2); %! assert_equal (idx, custom(:,2) == true); %! assert_equal (! custom, training (cv, 'all')); %! assert_equal (custom, test (cv, 'all')); %!test %! cv = cvpartition ('CustomPartition', [1:8]); %! assert_equal (cv.Type, 'leaveout'); %! assert_equal (cv.NumObservations, 8); %! assert_equal (cv.NumTestSets, 8); %! assert_equal (cv.TrainSize, [7, 7, 7, 7, 7, 7, 7, 7]); %! assert_equal (cv.TestSize, [1, 1, 1, 1, 1, 1, 1, 1]); %! assert_equal (cv.IsCustom, true); %! assert_equal (cv.IsGrouped, false); %! assert_equal (cv.IsStratified, false); %! assert_equal (class (training (cv, 1)), 'logical'); %! assert_equal (sum (training (cv, 1)), 7); %! assert_equal (sum (training (cv, 'all')), cv.TrainSize); %! assert_equal (class (test (cv, 1)), 'logical'); %! assert_equal (sum (test (cv, 1)), 1); %! assert_equal (sum (test (cv, 'all')), cv.TestSize); %! assert_equal (! training (cv, 'all'), test (cv, 'all')); %!test %! cv = cvpartition ('CustomPartition', logical (eye (8))); %! assert_equal (cv.Type, 'leaveout'); %! assert_equal (cv.NumObservations, 8); %! assert_equal (cv.NumTestSets, 8); %! assert_equal (cv.TrainSize, [7, 7, 7, 7, 7, 7, 7, 7]); %! assert_equal (cv.TestSize, [1, 1, 1, 1, 1, 1, 1, 1]); %! assert_equal (cv.IsCustom, true); %! assert_equal (cv.IsGrouped, false); %! assert_equal (cv.IsStratified, false); %! assert_equal (class (training (cv, 1)), 'logical'); %! assert_equal (sum (training (cv, 1)), 7); %! assert_equal (sum (training (cv, 'all')), cv.TrainSize); %! assert_equal (class (test (cv, 1)), 'logical'); %! assert_equal (sum (test (cv, 1)), 1); %! assert_equal (sum (test (cv, 'all')), cv.TestSize); %! assert_equal (! training (cv, 'all'), test (cv, 'all')); ## Test output results for scalar input N %!test %! cv = cvpartition (10, 'resubstitution'); %! assert_equal (cv.Type, 'resubstitution'); %! assert_equal (cv.NumObservations, 10); %! assert_equal (cv.NumTestSets, 1); %! assert_equal (cv.TrainSize, 10); %! assert_equal (cv.TestSize, 10); %! assert_equal (cv.IsCustom, false); %! assert_equal (cv.IsGrouped, false); %! assert_equal (cv.IsStratified, false); %! assert_equal (class (training (cv, 1)), 'logical'); %! assert_equal (sum (training (cv, 1)), 10); %! assert_equal (training (cv, 'all'), logical (ones (10, 1))); %! assert_equal (class (test (cv, 1)), 'logical'); %! assert_equal (sum (test (cv, 1)), 10); %! assert_equal (test (cv, 'all'), logical (ones (10, 1))); %! assert_equal (test (cv), training (cv)); %!test %! cv = cvpartition (10, 'leaveout'); %! assert_equal (cv.Type, 'leaveout'); %! assert_equal (cv.NumObservations, 10); %! assert_equal (cv.NumTestSets, 10); %! assert_equal (cv.TrainSize, ones (1, 10) * 9); %! assert_equal (cv.TestSize, ones (1, 10)); %! assert_equal (cv.IsCustom, false); %! assert_equal (cv.IsGrouped, false); %! assert_equal (cv.IsStratified, false); %! assert_equal (class (training (cv, 1)), 'logical'); %! assert_equal (sum (training (cv, 1)), 9); %! assert_equal (training (cv, 'all'), ! logical (eye (10))); %! assert_equal (class (test (cv, 1)), 'logical'); %! assert_equal (sum (test (cv, 1)), 1); %! assert_equal (test (cv, 'all'), logical (eye (10))); %! assert_equal (test (cv), ! training (cv)); %! assert_equal (test (cv, 'all'), ! training (cv, 'all')); %!test %! rand ('seed', 5); # for reproducibility %! cv = cvpartition (10, 'holdout', 0.3); %! assert_equal (cv.Type, 'holdout'); %! assert_equal (cv.NumObservations, 10); %! assert_equal (cv.NumTestSets, 1); %! assert_equal (cv.TrainSize, 7); %! assert_equal (cv.TestSize, 3); %! assert_equal (cv.IsCustom, false); %! assert_equal (cv.IsGrouped, false); %! assert_equal (cv.IsStratified, false); %! assert_equal (class (training (cv, 1)), 'logical'); %! assert_equal (sum (training (cv, 1)), 7); %! assert_equal (training (cv, 'all'), logical ([1, 0, 1, 1, 0, 1, 1, 1, 0, 1])'); %! assert_equal (class (test (cv, 1)), 'logical'); %! assert_equal (sum (test (cv, 1)), 3); %! assert_equal (test (cv, 'all'), logical ([0, 1, 0, 0, 1, 0, 0, 0, 1, 0])'); %! assert_equal (test (cv), ! training (cv)); %! assert_equal (test (cv, 'all'), ! training (cv, 'all')); %!test %! cv = cvpartition (10, 'holdout', 4); %! assert_equal (cv.Type, 'holdout'); %! assert_equal (cv.NumObservations, 10); %! assert_equal (cv.NumTestSets, 1); %! assert_equal (cv.TrainSize, 6); %! assert_equal (cv.TestSize, 4); %! assert_equal (cv.IsCustom, false); %! assert_equal (cv.IsGrouped, false); %! assert_equal (cv.IsStratified, false); %! assert_equal (class (training (cv, 1)), 'logical'); %! assert_equal (sum (training (cv, 1)), 6); %! assert_equal (class (test (cv, 1)), 'logical'); %! assert_equal (sum (test (cv, 1)), 4); %! assert_equal (test (cv), ! training (cv)); %! assert_equal (test (cv, 'all'), ! training (cv, 'all')); %!test %! cv = cvpartition (5, 'holdout', 4); %! assert_equal (cv.Type, 'holdout'); %! assert_equal (cv.NumObservations, 5); %! assert_equal (cv.NumTestSets, 1); %! assert_equal (cv.TrainSize, 1); %! assert_equal (cv.TestSize, 4); %! assert_equal (sum (test (cv, 1)), 4); %!test %! cv = cvpartition (5, 'holdout', 1); %! assert_equal (cv.Type, 'holdout'); %! assert_equal (cv.NumObservations, 5); %! assert_equal (cv.NumTestSets, 1); %! assert_equal (cv.TrainSize, 4); %! assert_equal (cv.TestSize, 1); %! assert_equal (sum (test (cv, 1)), 1); %!test %! cv = cvpartition (5, 'kfold'); %! assert_equal (cv.Type, 'kfold'); %! assert_equal (cv.NumObservations, 5); %! assert_equal (cv.NumTestSets, 5); %!test %! cv = cvpartition (20, 'kfold'); %! assert_equal (cv.Type, 'kfold'); %! assert_equal (cv.NumObservations, 20); %! assert_equal (cv.NumTestSets, 10); %!test %! cv = cvpartition (10, 'kfold', 5); %! assert_equal (cv.Type, 'kfold'); %! assert_equal (cv.NumObservations, 10); %! assert_equal (cv.NumTestSets, 5); %! assert_equal (cv.TrainSize, [8, 8, 8, 8, 8]); %! assert_equal (cv.TestSize, [2, 2, 2, 2, 2]); %! assert_equal (cv.IsCustom, false); %! assert_equal (cv.IsGrouped, false); %! assert_equal (cv.IsStratified, false); %! assert_equal (test (cv, 1), ! training (cv, 1)); %! assert_equal (test (cv, 'all'), ! training (cv, 'all')); %! assert_equal (size (test (cv, 'all')), [10, 5]); %!test %! grpvar = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 5, 5]; %! rand ('seed', 5); %! cv = cvpartition (12, 'kfold', 5, 'GroupingVariables', grpvar); %! assert_equal (cv.Type, 'kfold'); %! assert_equal (cv.NumObservations, 12); %! assert_equal (cv.NumTestSets, 5); %! assert_equal (cv.TrainSize, [10, 10, 10, 8, 10]); %! assert_equal (cv.TestSize, [2, 2, 2, 4, 2]); %! assert_equal (cv.IsCustom, false); %! assert_equal (cv.IsGrouped, true); %! assert_equal (cv.IsStratified, false); %! assert_equal (test (cv, 1), ! training (cv, 1)); %! assert_equal (test (cv, 'all'), ! training (cv, 'all')); %! assert_equal (size (test (cv, 'all')), [12, 5]); %! assert_equal (sum (test (cv, 'all')), [2, 2, 2, 4, 2]); %! assert_equal (sum (training (cv, 'all')), [10, 10, 10, 8, 10]); %!test %! grpvar = [1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3]; %! rand ('seed', 5); %! cv = cvpartition (12, 'kfold', 3, 'GroupingVariables', grpvar); %! assert_equal (cv.Type, 'kfold'); %! assert_equal (cv.NumObservations, 12); %! assert_equal (cv.NumTestSets, 3); %! assert_equal (cv.TrainSize, [9, 10, 5]); %! assert_equal (cv.TestSize, [3, 2, 7]); %! assert_equal (cv.IsCustom, false); %! assert_equal (cv.IsGrouped, true); %! assert_equal (cv.IsStratified, false); %! assert_equal (test (cv, 1), ! training (cv, 1)); %! assert_equal (test (cv, 'all'), ! training (cv, 'all')); %! assert_equal (size (test (cv, 'all')), [12, 3]); %! assert_equal (sum (test (cv, 'all')), [3, 2, 7]); %! assert_equal (sum (training (cv, 'all')), [9, 10, 5]); %!test %! grpvar = [1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3]; %! rand ('seed', 5); %! cv = cvpartition (12, 'kfold', 2, 'GroupingVariables', grpvar); %! assert_equal (cv.Type, 'kfold'); %! assert_equal (cv.NumObservations, 12); %! assert_equal (cv.NumTestSets, 2); %! assert_equal (cv.TrainSize, [6, 6]); %! assert_equal (cv.TestSize, [6, 6]); %! assert_equal (cv.IsCustom, false); %! assert_equal (cv.IsGrouped, true); %! assert_equal (cv.IsStratified, false); %! assert_equal (test (cv, 1), ! training (cv, 1)); %! assert_equal (test (cv, 'all'), ! training (cv, 'all')); %! assert_equal (size (test (cv, 'all')), [12, 2]); %! assert_equal (sum (test (cv, 'all')), [6, 6]); %! assert_equal (sum (training (cv, 'all')), [6, 6]); %!test %! grpvar = [1, 1, 1, 2, 2, 2, 2, NaN, 2, 3, 3, 3]; %! rand ('seed', 5); %! cv = cvpartition (12, 'kfold', 2, 'GroupingVariables', grpvar); %! assert_equal (cv.Type, 'kfold'); %! assert_equal (cv.NumObservations, 12); %! assert_equal (cv.NumTestSets, 2); %! assert_equal (cv.TrainSize, [6, 5]); %! assert_equal (cv.TestSize, [5, 6]); %! assert_equal (cv.IsCustom, false); %! assert_equal (cv.IsGrouped, true); %! assert_equal (cv.IsStratified, false); %! idx = ! isnan (grpvar); %! assert_equal (test (cv, 1)(idx), ! training (cv, 1)(idx)); %! assert_equal (test (cv, 'all')(idx, :), ! training (cv, 'all')(idx, :)); %! assert_equal (size (test (cv, 'all')), [12, 2]); %! assert_equal (sum (test (cv, 'all')), [5, 6]); %! assert_equal (sum (training (cv, 'all')), [6, 5]); %!test %! grpvar = [1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3]; %! rand ('seed', 5); %! cv = cvpartition (12, 'kfold', 2, 'GroupingVariables', grpvar); %! assert_equal (cv.Type, 'kfold'); %! assert_equal (cv.NumObservations, 12); %! assert_equal (cv.NumTestSets, 2); %! assert_equal (cv.TrainSize, [5, 7]); %! assert_equal (cv.TestSize, [7, 5]); %! assert_equal (cv.IsCustom, false); %! assert_equal (cv.IsGrouped, true); %! assert_equal (cv.IsStratified, false); %! assert_equal (test (cv, 1), ! training (cv, 1)); %! assert_equal (test (cv, 'all'), ! training (cv, 'all')); %! assert_equal (size (test (cv, 'all')), [12, 2]); %! assert_equal (sum (test (cv, 'all')), [7, 5]); %! assert_equal (sum (training (cv, 'all')), [5, 7]); %! assert_equal (test (cv, 1)', grpvar == 2); %! assert_equal (test (cv, 2)', grpvar != 2); %!test %! grpvar = [1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3]; %! rand ('seed', 5); %! cv = cvpartition (12, 'kfold', 2, 'GroupingVariables', grpvar); %! assert_equal (cv.Type, 'kfold'); %! assert_equal (cv.NumObservations, 12); %! assert_equal (cv.NumTestSets, 2); %! assert_equal (cv.TrainSize, [7, 5]); %! assert_equal (cv.TestSize, [5, 7]); %! assert_equal (cv.IsCustom, false); %! assert_equal (cv.IsGrouped, true); %! assert_equal (cv.IsStratified, false); %! assert_equal (test (cv, 1), ! training (cv, 1)); %! assert_equal (test (cv, 'all'), ! training (cv, 'all')); %! assert_equal (size (test (cv, 'all')), [12, 2]); %! assert_equal (sum (test (cv, 'all')), [5, 7]); %! assert_equal (sum (training (cv, 'all')), [7, 5]); %! assert_equal (test (cv, 1)', grpvar == 2); %! assert_equal (test (cv, 2)', grpvar != 2); %!test %! grpvar = [1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3]; %! rand ('seed', 5); %! cv = cvpartition (12, 'kfold', 2, 'GroupingVariables', grpvar); %! assert_equal (cv.Type, 'kfold'); %! assert_equal (cv.NumObservations, 12); %! assert_equal (cv.NumTestSets, 2); %! assert_equal (cv.TrainSize, [7, 5]); %! assert_equal (cv.TestSize, [5, 7]); %! assert_equal (cv.IsCustom, false); %! assert_equal (cv.IsGrouped, true); %! assert_equal (cv.IsStratified, false); %! assert_equal (test (cv, 1), ! training (cv, 1)); %! assert_equal (test (cv, 'all'), ! training (cv, 'all')); %! assert_equal (size (test (cv, 'all')), [12, 2]); %! assert_equal (sum (test (cv, 'all')), [5, 7]); %! assert_equal (sum (training (cv, 'all')), [7, 5]); %! assert_equal (test (cv, 1)', grpvar == 3); %! assert_equal (test (cv, 2)', grpvar != 3); %!test %! status = warning; %! warning ('off'); %! cv = cvpartition (5, 'kfold', 5, 'GroupingVariables', {'a';'a';'b';'b';''}); %! warning (status); %! assert_equal (cv.Type, 'kfold'); %! assert_equal (cv.NumObservations, 5); %! assert_equal (cv.NumTestSets, 2); %! assert_equal (cv.TrainSize, [2, 2]); %! assert_equal (cv.TestSize, [2, 2]); %! assert_equal (cv.IsCustom, false); %! assert_equal (cv.IsGrouped, true); %! assert_equal (cv.IsStratified, false); %! idx = ! ismissing ({'a';'a';'b';'b';''}); %! assert_equal (test (cv, 1)(idx), ! training (cv, 1)(idx)); %! assert_equal (test (cv, 'all')(idx,:), ! training (cv, 'all')(idx,:)); %! assert_equal (size (test (cv, 'all')), [5, 2]); %! assert_equal (sum (test (cv, 'all')), [2, 2]); %! assert_equal (sum (test (cv, 'all'), 2), [1; 1; 1; 1; 0]); ## Test output results for vector input X %!test %! rand ('seed', 5); %! cv = cvpartition ([1, 1, 1, 1, 1, 2, 2, 2, 2, 2], 'holdout', 3); %! assert_equal (cv.Type, 'holdout'); %! assert_equal (cv.NumObservations, 10); %! assert_equal (cv.NumTestSets, 1); %! assert_equal (cv.TrainSize, 7); %! assert_equal (cv.TestSize, 3); %! assert_equal (cv.IsCustom, false); %! assert_equal (cv.IsGrouped, false); %! assert_equal (cv.IsStratified, true); %! assert_equal (test (cv, 1), ! training (cv, 1)); %! assert_equal (test (cv), logical ([0, 0, 0, 0, 1, 0, 1, 0, 0, 1])'); %!test %! cv = cvpartition ([1, 1, 1, 1, 1, 2, 2, 2, 2, 2], 'holdout', 4); %! assert_equal (cv.Type, 'holdout'); %! assert_equal (cv.NumObservations, 10); %! assert_equal (cv.NumTestSets, 1); %! assert_equal (cv.TrainSize, 6); %! assert_equal (cv.TestSize, 4); %! assert_equal (cv.IsCustom, false); %! assert_equal (cv.IsGrouped, false); %! assert_equal (cv.IsStratified, true); %! assert_equal (test (cv, 1), ! training (cv, 1)); %! assert_equal (sum (test (cv)(1:5)), 2); %! assert_equal (sum (test (cv)(6:10)), 2); %!test %! grpvar = [1, 1, 1, 1, 1, 2, 2, 2, 2, 2]; %! rand ('seed', 5); %! cv = cvpartition (grpvar, 'holdout', 4, 'Stratify', false); %! assert_equal (cv.Type, 'holdout'); %! assert_equal (cv.NumObservations, 10); %! assert_equal (cv.NumTestSets, 1); %! assert_equal (cv.TrainSize, 6); %! assert_equal (cv.TestSize, 4); %! assert_equal (cv.IsCustom, false); %! assert_equal (cv.IsGrouped, false); %! assert_equal (cv.IsStratified, false); %! assert_equal (test (cv, 1), ! training (cv, 1)); %! assert_equal (sum (test (cv)(1:5)), 3); %! assert_equal (sum (test (cv)(6:10)), 1); %!test %! cv = cvpartition ([1 1 1 1 1 2 2 2 2 1], 'kfold', 2); %! assert_equal (cv.Type, 'kfold'); %! assert_equal (cv.NumObservations, 10); %! assert_equal (cv.NumTestSets, 2); %! assert_equal (cv.TrainSize, [5, 5]); %! assert_equal (cv.TestSize, [5, 5]); %! assert_equal (cv.IsCustom, false); %! assert_equal (cv.IsGrouped, false); %! assert_equal (cv.IsStratified, true); %! assert_equal (test (cv, 1), ! training (cv, 1)); %! assert_equal (test (cv, 'all'), ! training (cv, 'all')); %! assert_equal (sum (test (cv, 1)(1:5)), 3); %! assert_equal (sum (test (cv, 2)(1:5)), 2); %! assert_equal (sum (test (cv, 1)(6:10)), 2); %! assert_equal (sum (test (cv, 2)(6:10)), 3); %!test %! grpvar = [1 1 1 1 1 2 2 2 2 1]; %! rand ('seed', 5); %! cv = cvpartition (grpvar, 'kfold', 2, 'Stratify', false); %! assert_equal (cv.Type, 'kfold'); %! assert_equal (cv.NumObservations, 10); %! assert_equal (cv.NumTestSets, 2); %! assert_equal (cv.TrainSize, [5, 5]); %! assert_equal (cv.TestSize, [5, 5]); %! assert_equal (cv.IsCustom, false); %! assert_equal (cv.IsGrouped, false); %! assert_equal (cv.IsStratified, false); %! assert_equal (test (cv, 1), ! training (cv, 1)); %! assert_equal (test (cv, 'all'), ! training (cv, 'all')); %! assert_equal (sum (test (cv, 1)(1:5)), 4); %! assert_equal (sum (test (cv, 2)(1:5)), 1); %! assert_equal (sum (test (cv, 1)(6:10)), 1); %! assert_equal (sum (test (cv, 2)(6:10)), 4); %!test %! status = warning; %! warning ('off'); %! cv = cvpartition ({'a','a','b','b',''}, 'kfold'); %! warning (status); %! assert_equal (cv.Type, 'kfold'); %! assert_equal (cv.NumObservations, 5); %! assert_equal (cv.NumTestSets, 4); %! assert_equal (cv.TrainSize, [3, 3, 3, 3]); %! assert_equal (cv.TestSize, [1, 1, 1, 1]); %! assert_equal (cv.IsCustom, false); %! assert_equal (cv.IsGrouped, false); %! assert_equal (cv.IsStratified, true); %! idx = ! ismissing ({'a','a','b','b',''}); %! assert_equal (test (cv, 1)(idx), ! training (cv, 1)(idx)); %! assert_equal (test (cv, 'all')(idx,:), ! training (cv, 'all')(idx,:)); %! assert_equal (sum (test (cv, 'all'), 2), [1; 1; 1; 1; 0]); ## Test input validation %!test %! ## A vector of set indices returns one column per set, not every set. %! ## The k-fold loop used the whole index vector instead of the loop %! ## variable, so the comparison broadcast and each pass added a column %! ## per requested set. %! c = cvpartition (60, 'KFold', 4); %! t = test (c, [1, 2]); %! assert_equal (size (t), [60, 2]); %! assert_equal (t(:,1), test (c, 1)); %! assert_equal (t(:,2), test (c, 2)); %! assert_equal (any (t(:,1) & t(:,2)), false); %! assert_equal (size (test (c, [1, 3, 4])), [60, 3]); %!test %! ## training indexes the same way, and stays the complement of test %! c = cvpartition (60, 'KFold', 4); %! r = training (c, [1, 2]); %! assert_equal (size (r), [60, 2]); %! assert_equal (r(:,1), training (c, 1)); %! assert_equal (r, ! test (c, [1, 2])); %!test %! ## 'GroupingVariables' with 'HoldOut' holds out whole groups, as %! ## scikit-learn's GroupShuffleSplit does: no group is split between the %! ## training and test sets. %! g = [1 1 1 1 2 2 2 2 3 3 3 3]'; %! c = cvpartition (12, 'HoldOut', 0.25, 'GroupingVariables', g); %! assert_equal (c.IsGrouped, true); %! te = test (c); tr = training (c); %! assert_equal (any (ismember (unique (g(te)), unique (g(tr)))), false); %! assert_equal (tr, ! te); %!test %! ## 'GroupingVariables' with 'LeaveOut' leaves one whole group out at a %! ## time, as scikit-learn's LeaveOneGroupOut does. %! g = [1 1 1 1 2 2 2 2 3 3 3 3]'; %! c = cvpartition (12, 'LeaveOut', 'GroupingVariables', g); %! assert_equal (c.IsGrouped, true); %! assert_equal (c.NumTestSets, 3); %! assert_equal (c.TestSize, [4, 4, 4]); %! for s = 1:3 %! te = test (c, s); %! assert_equal (numel (unique (g(te))), 1); %! assert_equal (training (c, s), ! te); %! endfor %! assert_equal (size (test (c, 'all')), [12, 3]); %!test %! ## the ungrouped forms are untouched %! c = cvpartition (12, 'LeaveOut'); %! assert_equal (c.NumTestSets, 12); %! assert_equal (c.IsGrouped, false); %! c = cvpartition (12, 'HoldOut', 0.25); %! assert_equal (c.IsGrouped, false); %!test %! ## 'Stratify' and 'GroupingVariables' together keep each group whole %! ## while spreading the classes across the folds, as scikit-learn's %! ## StratifiedGroupKFold does. Six single-class groups, two classes. %! g = [1 1 2 2 3 3 4 4 5 5 6 6]'; %! y = [1 1 1 1 1 1 2 2 2 2 2 2]'; %! c = cvpartition (y, 'KFold', 3, 'Stratify', true, ... %! 'GroupingVariables', g); %! assert_equal (c.IsStratified, true); %! assert_equal (c.IsGrouped, true); %! assert_equal (c.NumTestSets, 3); %! assert_equal (c.TestSize, [4, 4, 4]); %! for s = 1:3 %! te = test (c, s); %! ## no group is split between training and test %! assert_equal (any (ismember (unique (g(te)), ... %! unique (g(training (c, s))))), false); %! ## and the fold still sees both classes %! assert_equal (numel (unique (y(te))), 2); %! endfor %!test %! ## 'GroupingVariables' with 'Stratify', false groups without stratifying: %! ## the stratification variable is simply not used, as asked. %! g = [1 1 2 2 3 3 4 4]'; %! y = [1 1 1 1 2 2 2 2]'; %! c = cvpartition (y, 'KFold', 2, 'Stratify', false, 'GroupingVariables', g); %! assert_equal (c.IsGrouped, true); %! assert_equal (c.IsStratified, false); %! for s = 1:c.NumTestSets %! te = test (c, s); %! assert_equal (any (ismember (unique (g(te)), ... %! unique (g(training (c, s))))), false); %! endfor ## A grouping variable may name its groups in the rows of a character ## matrix, which MATLAB accepts and reports the same partition for as the ## cell array of the same names. %!test %! load fisheriris %! c1 = cvpartition (char (species), "KFold", 3); %! c2 = cvpartition (species, "KFold", 3); %! assert_equal (c1.TestSize, c2.TestSize); %! assert_equal (c1.NumObservations, 150); ## A character vector is left alone: there each element is an observation ## already, and turning it into names would change what it partitions. %!test %! c = cvpartition ("aabbcc", "KFold", 3); %! assert_equal (c.NumObservations, 6); %!error ... %! cvpartition (12, 'Resubstitution', 'GroupingVariables', ... %! [1 1 2 2 3 3 1 1 2 2 3 3]) %!error cvpartition (2) %!error ... %! cvpartition (1, 2, 3, 4, 5, 6, 7, 8) %!error ... %! cvpartition ('CustomPartition', 'a') %!error ... %! cvpartition ('CustomPartition', [2, 3; 2, 3]) %!error ... %! cvpartition ('CustomPartition', false (3, 3, 3)) %!error ... %! cvpartition ('CustomPartition', [false, true; true, true; true, false]) %!error ... %! cvpartition ('CustomPartition', false (3, 5)) %!error ... %! cvpartition (-20, 'LeaveOut') %!error ... %! cvpartition (20.5, 'LeaveOut') %!error ... %! cvpartition (20, 'HoldOut', [0.2, 0.3]) %!error ... %! cvpartition (20, 'HoldOut', 'a') %!error ... %! cvpartition (20, 'HoldOut', 0) %!error ... %! cvpartition (20, 'HoldOut', -0.1) %!error ... %! cvpartition (20, 'HoldOut', 21) %!error ... %! cvpartition (20, 'kfold', [2, 3]) %!error ... %! cvpartition (20, 'kfold', 'a') %!error ... %! cvpartition (20, 'kfold', 2.5) %!error ... %! cvpartition (20, 'kfold', 21) %!error ... %! cvpartition (10, 'kfold', 3, 'Group') %!error ... %! cvpartition (10, 'kfold', 3, 'GroupingVariables') %!error ... %! cvpartition (10, 'kfold', 3, 'GroupingVariables', ones (3, 3, 3)) %!error ... %! cvpartition (10, 'kfold', 3, 'GroupingVariables', {'a', 'a', 'a', 'b', 'b'}) %!warning ... %! cvpartition (5, 'kfold', 3, 'GroupingVariables', {'a', 'a', 'a', 'b', 'b'}); %!error ... %! cvpartition (20, 'some') %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'kfold', 2, 'strat') %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'kfold', 2, 'stratify') %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'kfold', 2, 'stratify', [true, true]) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'kfold', 2, 'stratify', 'no') %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'holdout', 'a') %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'holdout', 'a', 'stratify', true) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'holdout', [0.2, 0.3]) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'holdout', [0.2, 0.3], 'stratify', true) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'holdout', 0) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'holdout', 0, 'stratify', true) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'holdout', -0.1) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'holdout', -0.1, 'stratify', true) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'holdout', 1.2) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'holdout', 1.2, 'stratify', false) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'holdout', 6) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'holdout', 6, 'stratify', false) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'kfold', 'a') %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'kfold', 'a', 'stratify', true) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'kfold', [2, 3]) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'kfold', [2, 3], 'stratify', false) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'kfold', 0) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'kfold', 0, 'stratify', true) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'kfold', 1.5) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'kfold', 1.5, 'stratify', true) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'kfold', 6) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'kfold', 6, 'stratify', true) %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'leaveout') %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'resubstitution') %!error ... %! cvpartition ([1, 1, 1, 2, 2], 'some') %!error ... %! cvpartition ({1, 1; 2, 2}, 'kfold') ## A scalar SVAL seeds the repartitioning. The keyword was misspelt 'sval' ## rather than 'seed', so every scalar-seeded call used to error out. %!test %! c = cvpartition (60, 'KFold', 5); %! assert_equal (test (repartition (c, 42), 1), test (repartition (c, 42), 1)); %! assert_equal (isequal (test (repartition (c, 42), 1), ... %! test (repartition (c, 43), 1)), false); %!test %! c = cvpartition (60, 'KFold', 5); %! assert_equal (test (repartition (c, [1 2 3]), 1), ... %! test (repartition (c, [1 2 3]), 1)); ## Seeding is scoped to the call: the caller's generator is restored, so the ## seed does not carry into the random numbers drawn afterwards. %!test %! c = cvpartition (60, 'KFold', 5); %! rand ('twister', 5); %! expect = rand (1, 4); %! rand ('twister', 5); %! repartition (c, 42); %! assert_equal (rand (1, 4), expect); %! rand ('twister', 5); %! repartition (c, [1 2 3]); %! assert_equal (rand (1, 4), expect); ## The seeded partition must not depend on the caller's state either. %!test %! c = cvpartition (60, 'KFold', 5); %! rand ('twister', 5); %! a = test (repartition (c, 42), 1); %! rand ('twister', 777); %! assert_equal (test (repartition (c, 42), 1), a); ## Without SVAL the call still consumes randomness, as it must. %!test %! c = cvpartition (60, 'KFold', 5); %! rand ('twister', 5); %! untouched = rand (1, 4); %! rand ('twister', 5); %! repartition (c); %! assert_equal (isequal (rand (1, 4), untouched), false); %!error ... %! repartition (cvpartition ('CustomPartition', [1,1,2,2,3,3])) %!error ... %! repartition (cvpartition ([1 1 1 1 1 2 2 2 2 1], 'kfold', 2, 'Stratify', true), 'legacy') %!error ... %! repartition (cvpartition (20, 'Leaveout', 0.2), 'legacy') %!error ... %! repartition (cvpartition (20, 'Leaveout', 0.2), 'asd') %!error ... %! repartition (cvpartition (20, 'Leaveout', 0.2), 2+i) %!error ... %! repartition (cvpartition (20, 'KFold', 5), [34, 56; 2, 3]) %!error ... %! test (cvpartition (20, 'kfold'), 2, 3) %!error ... %! test (cvpartition (20, 'kfold'), 0) %!error ... %! test (cvpartition (20, 'kfold'), 1.5) %!error ... %! test (cvpartition (20, 'kfold'), [1, 1.5]) %!error ... %! test (cvpartition (20, 'kfold'), [2, 3; 2, 3]) %!error ... %! test (cvpartition (20, 'kfold'), 21) %!error ... %! test (cvpartition (20, 'kfold'), [18, 21]) %!error ... %! training (cvpartition (20, 'kfold'), 2, 3) %!error ... %! training (cvpartition (20, 'kfold'), 0) %!error ... %! training (cvpartition (20, 'kfold'), 1.5) %!error ... %! training (cvpartition (20, 'kfold'), [1, 1.5]) %!error ... %! training (cvpartition (20, 'kfold'), [2, 3; 2, 3]) %!error ... %! training (cvpartition (20, 'kfold'), 21) %!error ... %! training (cvpartition (20, 'kfold'), [18, 21]) ## Test 'summary' method %!test %! ## 1. Stratified K-Fold: Basic Text Labels %! species = [repmat({'Setosa'}, 10, 1); repmat({'Versicolor'}, 10, 1)]; %! rand ('state', 42); %! c = cvpartition (species, 'KFold', 2); %! T = summary (c); %! assert_equal (height (T), 10); %! assert_equal (all (ismember ({'Set', 'SetSize', 'StratificationLabel', ... %! 'StratificationCount', 'PercentInSet'}, ... %! T.Properties.VariableNames)), true); %! %! ## Check Output Type (String Array) and Counts %! if (exist ('string', 'class')) %! assert_equal (isa (T.Set, 'string'), true); %! assert_equal (isa (T.StratificationLabel, 'string'), true); %! mask = (T.Set == 'all') & (T.StratificationLabel == 'Setosa'); %! else %! ## Fallback for older environments %! mask = strcmp (T.Set, 'all') & strcmp (T.StratificationLabel, 'Setosa'); %! endif %! assert_equal (T.StratificationCount(mask), 10); %!test %! ## 2. Grouped K-Fold: Basic Numeric Labels %! groups = [1; 1; 1; 2; 2; 3; 3; 3; 3; 3]; %! rand ('state', 100); %! c = cvpartition (numel (groups), 'KFold', 2, 'GroupingVariables', groups); %! T = summary (c); %! assert_equal (any (strcmp ('GroupLabel', T.Properties.VariableNames)), true); %! %! ## Verify Group Integrity %! if (iscell (T.GroupLabel)) %! vals = cell2mat (T.GroupLabel); %! else %! vals = T.GroupLabel; %! endif %! mask_g3 = (vals == 3); %! %! if (exist ('string', 'class')) %! mask_t1 = (T.Set == 'test1'); %! else %! mask_t1 = strcmp (T.Set, 'test1'); %! endif %! %! count_g3 = T.GroupCount(mask_g3 & mask_t1); %! assert_equal (count_g3 == 5 || count_g3 == 0, true); %!test %! ## 3. Grouped K-Fold: Matrix Grouping %! g1 = [1; 1; 1; 2; 2; 2]; %! g2 = [1; 1; 2; 1; 2; 2]; %! groups = [g1, g2]; %! c = cvpartition (6, 'KFold', 2, 'GroupingVariables', groups); %! T = summary (c); %! ## 4 unique groups * 5 sets (all + 2 train + 2 test) %! assert_equal (height (T), 20); %!test %! ## 4. Stratified Holdout: Basic %! species = [repmat({'A'}, 10, 1); repmat({'B'}, 10, 1)]; %! c = cvpartition (species, 'Holdout', 0.5); %! T = summary (c); %! sets = unique (T.Set); %! assert_equal (numel (sets), 3); ## all, train1, test1 %!test %! ## 5. Mathematical Consistency: Percentages %! classes = [1; 1; 2; 2; 3; 3]; %! c = cvpartition (classes, 'KFold', 2); %! T = summary (c); %! %! if (exist ('string', 'class')) %! mask_all = (T.Set == 'all'); %! mask_tr1 = (T.Set == 'train1'); %! else %! mask_all = strcmp (T.Set, 'all'); %! mask_tr1 = strcmp (T.Set, 'train1'); %! endif %! %! assert_equal (sum (T.PercentInSet(mask_all)), 100, 1e-10); %! assert_equal (sum (T.PercentInSet(mask_tr1)), 100, 1e-10); %!test %! ## 6. Mathematical Consistency: Set Sizes %! N = 20; %! c = cvpartition (ones (N, 1), 'KFold', 4); %! T = summary (c); %! %! if (exist ('string', 'class')) %! mask_tr1 = (T.Set == 'train1'); %! mask_ts1 = (T.Set == 'test1'); %! else %! mask_tr1 = strcmp (T.Set, 'train1'); %! mask_ts1 = strcmp (T.Set, 'test1'); %! endif %! %! size_tr1 = T.SetSize(find (mask_tr1, 1)); %! size_ts1 = T.SetSize(find (mask_ts1, 1)); %! assert_equal (size_tr1 + size_ts1, N); %!test %! ## 7. Logical Grouping Variables %! groups = [true; true; true; false; false]; %! c = cvpartition (5, 'KFold', 2, 'GroupingVariables', groups); %! T = summary (c); %! assert_equal (height (T), 2 * 5); %! if (iscell (T.GroupLabel)) %! u_labels = unique (cell2mat (T.GroupLabel)); %! else %! u_labels = unique (T.GroupLabel); %! endif %! assert_equal (numel (u_labels), 2); %!test %! ## 8. Char Array Grouping Variables %! groups = ['A'; 'A'; 'B'; 'B'; 'C']; %! c = cvpartition (5, 'KFold', 2, 'GroupingVariables', groups); %! T = summary (c); %! assert_equal (height (T), 3 * 5); %! assert_equal (any (strcmp ('GroupLabel', T.Properties.VariableNames)), true); %!test %! ## 9. Floating Point Grouping Variables %! groups = [1.1; 1.1; 2.2; 2.2]; %! c = cvpartition (4, 'KFold', 2, 'GroupingVariables', groups); %! T = summary (c); %! if (iscell (T.GroupLabel)) %! vals = cell2mat (T.GroupLabel); %! else %! vals = T.GroupLabel; %! endif %! assert_equal (any (abs (vals - 1.1) < 1e-10), true); %! assert_equal (any (abs (vals - 2.2) < 1e-10), true); %!test %! ## 10. Negative Numeric Grouping %! groups = [-5; -5; -10; -10]; %! c = cvpartition (4, 'KFold', 2, 'GroupingVariables', groups); %! T = summary (c); %! assert_equal (height (T), 2 * 5); %!test %! ## 11. Missing Values in Stratification (NaN) %! classes = [1; 1; 2; 2; NaN; NaN]; %! c = cvpartition (classes, 'KFold', 2); %! T = summary (c); %! if (exist ('string', 'class')) %! mask_all = (T.Set == 'all'); %! else %! mask_all = strcmp (T.Set, 'all'); %! endif %! total_obs = T.SetSize(find (mask_all, 1)); %! assert_equal (total_obs, 4); %!test %! ## 12. Missing Values in Grouping (NaN) %! groups = [1; 1; 2; 2; NaN]; %! c = cvpartition (5, 'KFold', 2, 'GroupingVariables', groups); %! T = summary (c); %! if (exist ('string', 'class')) %! mask_all = (T.Set == 'all'); %! else %! mask_all = strcmp (T.Set, 'all'); %! endif %! assert_equal (T.SetSize(find (mask_all, 1)), 4); %!test %! ## 13. Unbalanced Stratification %! species = [repmat({'C1'}, 90, 1); repmat({'C2'}, 10, 1)]; %! c = cvpartition (species, 'KFold', 2); %! T = summary (c); %! if (exist ('string', 'class')) %! mask_ts1 = (T.Set == 'test1'); %! subT = T(mask_ts1, :); %! c1_count = subT.StratificationCount(subT.StratificationLabel == 'C1'); %! c2_count = subT.StratificationCount(subT.StratificationLabel == 'C2'); %! else %! mask_ts1 = strcmp (T.Set, 'test1'); %! subT = T(mask_ts1, :); %! c1_count = subT.StratificationCount(strcmp (subT.StratificationLabel, 'C1')); %! c2_count = subT.StratificationCount(strcmp (subT.StratificationLabel, 'C2')); %! endif %! assert_equal (c1_count == 45, true); %! assert_equal (c2_count == 5, true); %!test %! ## 14. Single Observation per Group (Edge Case) %! groups = [1; 2; 3; 4]; %! c = cvpartition (4, 'KFold', 2, 'GroupingVariables', groups); %! T = summary (c); %! if (exist ('string', 'class')) %! mask_ts1 = (T.Set == 'test1'); %! else %! mask_ts1 = strcmp (T.Set, 'test1'); %! endif %! counts = T.GroupCount(mask_ts1); %! assert_equal (sum (counts == 1), 2); %! assert_equal (sum (counts == 0), 2); %!test %! ## 15. Set Name Generation Verification %! species = [1; 1; 2; 2]; %! c = cvpartition (species, 'KFold', 2); %! T = summary (c); %! set_names = unique (T.Set); %! expected = {'all'; 'train1'; 'test1'; 'train2'; 'test2'}; %! if (exist ('string', 'class')) %! ## Convert string array to cell for sort comparison %! assert_equal (sort (cellstr (set_names)), sort (expected)); %! else %! assert_equal (sort (set_names), sort (expected)); %! endif %!test %! ## 16. Label Column Consistency %! groups = ['A'; 'B']; %! c = cvpartition (2, 'KFold', 2, 'GroupingVariables', groups); %! T = summary (c); %! if (exist ('string', 'class')) %! assert_equal (isa (T.GroupLabel, 'string'), true); %! else %! assert_equal (iscellstr (T.GroupLabel), true); %! endif %!test %! ## 17. Valid "Blank" Labels (Space) - FIX APPLIED %! species = {'A'; 'A'; ' '; ' '}; %! c = cvpartition (species, 'KFold', 2); %! T = summary (c); %! if (exist ('string', 'class')) %! labels = cellstr (T.StratificationLabel); %! sets = cellstr (T.Set); %! assert_equal (any (strcmp (labels, ' ')), true); %! mask_space = strcmp (labels, ' '); %! mask_all = strcmp (sets, 'all'); %! else %! assert_equal (any (strcmp (T.StratificationLabel, ' ')), true); %! mask_space = strcmp (T.StratificationLabel, ' '); %! mask_all = strcmp (T.Set, 'all'); %! endif %! assert_equal (sum (T.StratificationCount(mask_space & mask_all)), 2); %!test %! ## 18. Large K (Leave-One-Out Simulation) - FIX APPLIED %! species = [1; 1; 2; 2]; %! warn_state = warning ("off", 'all'); %! c = cvpartition (species, 'KFold', 4); %! warning (warn_state); %! T = summary (c); %! assert_equal (height (T), 18); %! if (exist ('string', 'class')) %! mask_test = startsWith (cellstr(T.Set), 'test'); %! else %! mask_test = strncmp (T.Set, 'test', 4); %! endif %! assert_equal (all (T.SetSize(mask_test) == 1), true); %!test %! ## 19. Repeated Holdout Integrity %! species = [1; 1; 2; 2]; %! rand ('state', 42); %! c = cvpartition (species, 'Holdout', 0.5); %! T = summary (c); %! if (exist ('string', 'class')) %! mask_ts1 = (T.Set == 'test1'); %! else %! mask_ts1 = strcmp (T.Set, 'test1'); %! endif %! size_ts1 = T.SetSize(find (mask_ts1, 1)); %! assert_equal (size_ts1, 2); %!test %! ## 20. Empty String Handling (Missing Data) %! species = {'A'; 'A'; ''; ''}; %! c = cvpartition (species, 'KFold', 2); %! T = summary (c); %! if (exist ('string', 'class')) %! assert_equal (! any (T.StratificationLabel == ''), true); %! mask_all = (T.Set == 'all'); %! else %! assert_equal (! any (strcmp (T.StratificationLabel, '')), true); %! mask_all = strcmp (T.Set, 'all'); %! endif %! total_rows = T.SetSize(find (mask_all, 1)); %! assert_equal (total_rows, 2); %!test %! ## 21. Basic Unstacking (Stratified K-Fold) %! species = [repmat({'Alpha'}, 10, 1); repmat({'Beta'}, 10, 1)]; %! c = cvpartition (species, 'KFold', 2); %! T = summary (c); %! T_wide = unstack (T(:, 1:4), 'StratificationCount', 'StratificationLabel'); %! ## Check dimensions: 3 sets (all, train1, test1, etc) x (Set+SetSize + 2 Labels) %! assert_equal (height (T_wide), 5); %! assert_equal (width (T_wide), 4); %! assert_equal (all (ismember ({'Alpha', 'Beta'}, T_wide.Properties.VariableNames)), true); %!test %! ## 22. Data Integrity Check (Row Sums) %! species = [repmat({'Control'}, 20, 1); repmat({'Treatment'}, 80, 1)]; %! c = cvpartition (species, 'Holdout', 0.25); %! T = summary (c); %! T_wide = unstack (T(:, 1:4), 'StratificationCount', 'StratificationLabel'); %! row_sums = T_wide.Control + T_wide.Treatment; %! assert_equal (all (row_sums == T_wide.SetSize), true); %!test %! ## 23. Unstacking Grouped Data (Numeric Labels) %! groups = [1; 1; 2; 2; 2]; %! c = cvpartition (5, 'KFold', 2, 'GroupingVariables', groups); %! T = summary (c); %! T_wide = unstack (T(:, 1:4), 'GroupCount', 'GroupLabel'); %! ## Check if numeric columns were created successfully %! col_names = T_wide.Properties.VariableNames; %! assert_equal (any (cellfun (@(x) ! isempty (strfind (x, '1')), col_names)), true); %! assert_equal (any (cellfun (@(x) ! isempty (strfind (x, '2')), col_names)), true); %!test %! ## 24. Unstacking with Missing/NaN Groups %! groups = [1; 1; 2; 2; NaN]; %! c = cvpartition (5, 'KFold', 2, 'GroupingVariables', groups); %! T = summary (c); %! T_wide = unstack (T(:, 1:4), 'GroupCount', 'GroupLabel'); %! ## Should only have columns for 1 and 2, not NaN or 'undefined' %! assert_equal (width (T_wide), 4); ## Set, SetSize, x1, x2 %!test %! ## 25. Unstacking String Array Inputs %! species = {'Red'; 'Blue'; 'Red'; 'Blue'}; %! c = cvpartition (species, 'KFold', 2); %! T = summary (c); %! ## Verify input is actually string before unstacking checks %! if (exist ('string', 'class')) %! assert_equal (isa (T.Set, 'string'), true); %! endif %! T_wide = unstack (T(:, 1:4), 'StratificationCount', 'StratificationLabel'); %! ## Check the 'all' row count for Red %! assert_equal (T_wide.Red(strcmp(cellstr(T_wide.Set), 'all')) == 2, true); %!test %! ## 26. Large K Unstacking (Many Rows) %! species = [repmat({'High'}, 10, 1); repmat({'Low'}, 10, 1)]; %! c = cvpartition (species, 'KFold', 10); %! T = summary (c); %! T_wide = unstack (T(:, 1:4), 'StratificationCount', 'StratificationLabel'); %! ## 10 folds * 2 (train/test) + 1 (all) = 21 rows %! assert_equal (height (T_wide), 21); %!test %! ## 27. Unstacking with Special Characters in Labels %! species = {'Type A'; 'Type A'; 'Type-B'; 'Type-B'}; %! c = cvpartition (species, 'KFold', 2); %! T = summary (c); %! T_wide = unstack (T(:, 1:4), 'StratificationCount', 'StratificationLabel'); %! vnames = T_wide.Properties.VariableNames; %! ## Check if spaces/dashes were handled/preserved in some valid form %! assert_equal (numel (vnames), 4); %!test %! ## 28. Verification of 'all' row logic after Unstacking %! species = [repmat({'Yes'}, 50, 1); repmat({'No'}, 50, 1)]; %! c = cvpartition (species, 'Holdout', 0.2); %! T = summary (c); %! T_wide = unstack (T(:, 1:4), 'StratificationCount', 'StratificationLabel'); %! mask = strcmp (cellstr (T_wide.Set), 'all'); %! assert_equal (T_wide.Yes(mask) == 50, true); %! assert_equal (T_wide.No(mask) == 50, true); %!test %! ## 29. Robustness against re-ordering %! species = {'Left'; 'Left'; 'Right'; 'Right'}; %! c = cvpartition (species, 'Holdout', 0.5); %! T = summary (c); %! T_shuffled = T([3, 1, 2], :); %! T_wide = unstack (T_shuffled(:, 1:4), 'StratificationCount', 'StratificationLabel'); %! mask = strcmp (cellstr (T_wide.Set), 'all'); %! assert_equal (T_wide.Left(mask) == 2, true); %!error %! c = cvpartition (20, 'KFold', 5); %! summary (c); %!error %! c = cvpartition (10, 'LeaveOut'); %! summary (c); statistics-release-2.1.2/inst/Model_Evaluation/doc-cache000066400000000000000000001410431527311104300232610ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 52 # name: # type: sq_string # elements: 1 # length: 20 ConfusionMatrixChart # name: # type: sq_string # elements: 1 # length: 758 statistics: ConfusionMatrixChart Confusion matrix chart for classification results The ConfusionMatrixChart class implements a confusion matrix chart object, which displays the classification performance of a classifier by showing the counts of true positive, true negative, false positive, and false negative predictions. A confusion matrix chart is a visual representation of the performance of a classification algorithm. The rows represent the true classes and the columns represent the predicted classes. The diagonal elements represent the correctly classified observations, while the off-diagonal elements represent the misclassified observations. Create a ConfusionMatrixChart object by using the confusionchart function. See also: confusionchart # name: # type: sq_string # elements: 1 # length: 49 Confusion matrix chart for classification results # name: # type: sq_string # elements: 1 # length: 32 ConfusionMatrixChart.ClassLabels # name: # type: sq_string # elements: 1 # length: 176 ConfusionMatrixChart: property ClassLabels Class labels A cell array of character vectors containing the class labels used in the confusion matrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 12 Class labels # name: # type: sq_string # elements: 1 # length: 34 ConfusionMatrixChart.ColumnSummary # name: # type: sq_string # elements: 1 # length: 351 ConfusionMatrixChart: property ColumnSummary Column summary display A character vector specifying whether and how to display column summaries. Supported values are: 'off' - Do not display column summary (default) 'absolute' - Display absolute counts 'column-normalized' - Display normalized by column 'total-normalized' - Display normalized by total # name: # type: sq_string # elements: 1 # length: 22 Column summary display # name: # type: sq_string # elements: 1 # length: 41 ConfusionMatrixChart.ConfusionMatrixChart # name: # type: sq_string # elements: 1 # length: 2772 ConfusionMatrixChart: cmc = ConfusionMatrixChart (hax, cm, cl) ConfusionMatrixChart: cmc = ConfusionMatrixChart (..., name, value) Create a ConfusionMatrixChart object for visualizing classification performance. cmc = ConfusionMatrixChart (hax, cm, cl) returns a ConfusionMatrixChart object with parent axes hax, confusion matrix cm, and class labels cl. hax must be a valid axes handle where the chart will be displayed. cm must be a square numeric matrix containing the confusion matrix values, where rows represent true classes and columns represent predicted classes. cl must be a cell array of character vectors containing the class labels. The number of labels must match the size of the confusion matrix. cmc = ConfusionMatrixChart (..., name, value) returns a ConfusionMatrixChart object with additional parameters specified by name, value paired arguments: Name Value 'XLabel' A character vector specifying the x-axis label. Default is "Predicted Class". 'YLabel' A character vector specifying the y-axis label. Default is "True Class". 'Title' A character vector specifying the chart title. Default is empty string. 'FontName' A character vector specifying the font name for text elements. Default is the axes font name. 'FontSize' A numeric scalar specifying the font size for text elements. Default is the axes font size. 'DiagonalColor' A 1x3 RGB vector specifying the color for diagonal elements (correct classifications). Default is [0.0, 0.4471, 0.7412]. 'OffDiagonalColor' A 1x3 RGB vector specifying the color for off-diagonal elements (misclassifications). Default is [0.8510, 0.3255, 0.0980]. 'Normalization' A character vector specifying the normalization method. Supported values are 'absolute', 'column-normalized', 'row-normalized', and 'total-normalized'. Default is 'absolute'. 'ColumnSummary' A character vector specifying whether and how to display column summaries. Supported values are 'off', 'absolute', 'column-normalized', and 'total-normalized'. Default is 'off'. 'RowSummary' A character vector specifying whether and how to display row summaries. Supported values are 'off', 'absolute', 'row-normalized', and 'total-normalized'. Default is 'off'. 'GridVisible' A character vector specifying whether to display grid lines. Supported values are 'on' and 'off'. Default is 'on'. 'HandleVisibility' A character vector specifying the handle visibility. Supported values are 'on', 'off', and 'callback'. 'OuterPosition' A 1x4 numeric vector specifying the outer position of the chart. 'Position' A 1x4 numeric vector specifying the position of the chart. 'Units' A character vector specifying the position units. Supported values are 'centimeters', 'characters', 'inches', 'normalized', 'pixels', and 'points'. See also: confusionchart # name: # type: sq_string # elements: 1 # length: 80 Create a ConfusionMatrixChart object for visualizing classification performance. # name: # type: sq_string # elements: 1 # length: 34 ConfusionMatrixChart.DiagonalColor # name: # type: sq_string # elements: 1 # length: 237 ConfusionMatrixChart: property DiagonalColor Color for diagonal elements A 1x3 RGB vector specifying the color for the diagonal elements of the confusion matrix, which represent correct classifications. Default is [0.0, 0.4471, 0.7412]. # name: # type: sq_string # elements: 1 # length: 27 Color for diagonal elements # name: # type: sq_string # elements: 1 # length: 29 ConfusionMatrixChart.FontName # name: # type: sq_string # elements: 1 # length: 209 ConfusionMatrixChart: property FontName Font name for text elements A character vector specifying the font name used for all text elements in the chart. Default is empty string, which uses the axes font name. # name: # type: sq_string # elements: 1 # length: 27 Font name for text elements # name: # type: sq_string # elements: 1 # length: 29 ConfusionMatrixChart.FontSize # name: # type: sq_string # elements: 1 # length: 196 ConfusionMatrixChart: property FontSize Font size for text elements A numeric scalar specifying the font size used for all text elements in the chart. Default is 0, which uses the axes font size. # name: # type: sq_string # elements: 1 # length: 27 Font size for text elements # name: # type: sq_string # elements: 1 # length: 32 ConfusionMatrixChart.GridVisible # name: # type: sq_string # elements: 1 # length: 227 ConfusionMatrixChart: property GridVisible Grid visibility A character vector specifying whether to display grid lines in the confusion matrix. Supported values are: 'on' - Display grid lines (default) 'off' - Hide grid lines # name: # type: sq_string # elements: 1 # length: 15 Grid visibility # name: # type: sq_string # elements: 1 # length: 37 ConfusionMatrixChart.HandleVisibility # name: # type: sq_string # elements: 1 # length: 185 ConfusionMatrixChart: property HandleVisibility Handle visibility A character vector specifying the visibility of the object's handle. Supported values are 'on', 'off', and 'callback'. # name: # type: sq_string # elements: 1 # length: 17 Handle visibility # name: # type: sq_string # elements: 1 # length: 34 ConfusionMatrixChart.Normalization # name: # type: sq_string # elements: 1 # length: 395 ConfusionMatrixChart: property Normalization Normalization method for confusion matrix values A character vector specifying how to normalize the confusion matrix values. Supported values are: 'absolute' - Display absolute counts (default) 'column-normalized' - Normalize by column totals 'row-normalized' - Normalize by row totals 'total-normalized' - Normalize by total number of observations # name: # type: sq_string # elements: 1 # length: 48 Normalization method for confusion matrix values # name: # type: sq_string # elements: 1 # length: 37 ConfusionMatrixChart.NormalizedValues # name: # type: sq_string # elements: 1 # length: 226 ConfusionMatrixChart: property NormalizedValues Normalized confusion matrix values A numeric matrix containing the normalized confusion matrix values according to the current normalization setting. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Normalized confusion matrix values # name: # type: sq_string # elements: 1 # length: 37 ConfusionMatrixChart.OffDiagonalColor # name: # type: sq_string # elements: 1 # length: 246 ConfusionMatrixChart: property OffDiagonalColor Color for off-diagonal elements A 1x3 RGB vector specifying the color for the off-diagonal elements of the confusion matrix, which represent misclassifications. Default is [0.8510, 0.3255, 0.0980]. # name: # type: sq_string # elements: 1 # length: 31 Color for off-diagonal elements # name: # type: sq_string # elements: 1 # length: 34 ConfusionMatrixChart.OuterPosition # name: # type: sq_string # elements: 1 # length: 182 ConfusionMatrixChart: property OuterPosition Outer position of the chart A 1x4 numeric vector specifying the outer position of the chart in the format [left, bottom, width, height]. # name: # type: sq_string # elements: 1 # length: 27 Outer position of the chart # name: # type: sq_string # elements: 1 # length: 27 ConfusionMatrixChart.Parent # name: # type: sq_string # elements: 1 # length: 131 ConfusionMatrixChart: property Parent Parent object A handle to the parent figure or container object. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Parent object # name: # type: sq_string # elements: 1 # length: 29 ConfusionMatrixChart.Position # name: # type: sq_string # elements: 1 # length: 165 ConfusionMatrixChart: property Position Position of the chart A 1x4 numeric vector specifying the position of the chart in the format [left, bottom, width, height]. # name: # type: sq_string # elements: 1 # length: 21 Position of the chart # name: # type: sq_string # elements: 1 # length: 31 ConfusionMatrixChart.RowSummary # name: # type: sq_string # elements: 1 # length: 333 ConfusionMatrixChart: property RowSummary Row summary display A character vector specifying whether and how to display row summaries. Supported values are: 'off' - Do not display row summary (default) 'absolute' - Display absolute counts 'row-normalized' - Display normalized by row 'total-normalized' - Display normalized by total # name: # type: sq_string # elements: 1 # length: 19 Row summary display # name: # type: sq_string # elements: 1 # length: 26 ConfusionMatrixChart.Title # name: # type: sq_string # elements: 1 # length: 145 ConfusionMatrixChart: property Title Chart title A character vector specifying the title of the confusion matrix chart. Default is empty string. # name: # type: sq_string # elements: 1 # length: 11 Chart title # name: # type: sq_string # elements: 1 # length: 26 ConfusionMatrixChart.Units # name: # type: sq_string # elements: 1 # length: 219 ConfusionMatrixChart: property Units Position units A character vector specifying the units for the position properties. Supported values are 'centimeters', 'characters', 'inches', 'normalized', 'pixels', and 'points'. # name: # type: sq_string # elements: 1 # length: 14 Position units # name: # type: sq_string # elements: 1 # length: 27 ConfusionMatrixChart.XLabel # name: # type: sq_string # elements: 1 # length: 137 ConfusionMatrixChart: property XLabel X-axis label A character vector specifying the label for the x-axis. Default is "Predicted Class". # name: # type: sq_string # elements: 1 # length: 12 X-axis label # name: # type: sq_string # elements: 1 # length: 27 ConfusionMatrixChart.YLabel # name: # type: sq_string # elements: 1 # length: 132 ConfusionMatrixChart: property YLabel Y-axis label A character vector specifying the label for the y-axis. Default is "True Class". # name: # type: sq_string # elements: 1 # length: 12 Y-axis label # name: # type: sq_string # elements: 1 # length: 32 ConfusionMatrixChart.sortClasses # name: # type: sq_string # elements: 1 # length: 680 ConfusionMatrixChart: sortClasses (cmc, order) Sort the classes in the confusion matrix chart. sortClasses (cmc, order) sorts the classes in the confusion matrix chart cmc according to the specified order. order can be: A cell array of class labels in the desired order 'auto' - Sort class labels alphabetically 'ascending-diagonal' - Sort by ascending diagonal values 'descending-diagonal' - Sort by descending diagonal values 'cluster' - Sort using hierarchical clustering When using 'cluster', the classes are grouped based on similarity using hierarchical clustering, which can help identify groups of frequently confused classes. See also: confusionchart, linkage, pdist # name: # type: sq_string # elements: 1 # length: 47 Sort the classes in the confusion matrix chart. # name: # type: sq_string # elements: 1 # length: 14 confusionchart # name: # type: sq_string # elements: 1 # length: 1645 statistics: confusionchart (trueLabels, predictedLabels) statistics: confusionchart (m) statistics: confusionchart (m, classLabels) statistics: confusionchart (parent, ...) statistics: confusionchart (..., prop, val, ...) statistics: cm = confusionchart (...) Display a chart of a confusion matrix. The two vectors of values trueLabels and predictedLabels, which are used to compute the confusion matrix, must be defined with the same format as the inputs of confusionmat. Otherwise a confusion matrix m as computed by confusionmat can be given. classLabels is an array of labels, i.e. the list of the class names. If the first argument is a handle to a figure or to a uipanel, then the confusion matrix chart is displayed inside that object. Optional property/value pairs are passed directly to the underlying objects, e.g. 'xlabel', 'ylabel', 'title', 'fontname', 'fontsize' etc. The optional return value cm is a ConfusionMatrixChart object. Specific properties of a ConfusionMatrixChart object are: 'DiagonalColor' The color of the patches on the diagonal, default is [0.0, 0.4471, 0.7412]. 'OffDiagonalColor' The color of the patches off the diagonal, default is [0.851, 0.3255, 0.098]. 'GridVisible' Available values: on (default), off. 'Normalization' Available values: absolute (default), column-normalized, row-normalized, total-normalized. 'ColumnSummary' Available values: off (default), absolute, column-normalized,total-normalized. 'RowSummary' Available values: off (default), absolute, row-normalized, total-normalized. Run demo confusionchart to see some examples. See also: confusionmat, ConfusionMatrixChart.sortClasses # name: # type: sq_string # elements: 1 # length: 38 Display a chart of a confusion matrix. # name: # type: sq_string # elements: 1 # length: 12 confusionmat # name: # type: sq_string # elements: 1 # length: 1465 statistics: C = confusionmat (group, grouphat) statistics: C = confusionmat (group, grouphat, "Order", grouporder) statistics: [C, order] = confusionmat (group, grouphat) Compute a confusion matrix for classification problems confusionmat returns the confusion matrix C for the group of actual values group and the group of predicted values grouphat. The row indices of the confusion matrix represent actual values, while the column indices represent predicted values. The indices are the same for both actual and predicted values, so the confusion matrix is a square matrix. Each element of the matrix represents the number of matches between a given actual value (row index) and a given predicted value (column index), hence correct matches lie on the main diagonal of the matrix. The order of the rows and columns is returned in order. group and grouphat must have the same number of observations and the same data type. Valid data types are numeric vectors, logical vectors, character arrays, string arrays, cell arrays of strings, and categorical arrays. The order of the rows and columns can be specified by setting the grouporder variable. The data type of grouporder must be the same of group and grouphat, except that a numeric grouporder for logical inputs is converted to logical. For logical inputs, the default order is false followed by true, and grouporder must contain both values. grouporder cannot contain duplicated values. See also: crosstab # name: # type: sq_string # elements: 1 # length: 54 Compute a confusion matrix for classification problems # name: # type: sq_string # elements: 1 # length: 8 crossval # name: # type: sq_string # elements: 1 # length: 1858 statistics: results = crossval (f, X, y) statistics: results = crossval (f, X, y, name, value) Perform cross validation on given data. f should be a function that takes 4 inputs xtrain, ytrain, xtest, ytest, fits a model based on xtrain, ytrain, applies the fitted model to xtest, and returns a goodness of fit measure based on comparing the predicted and actual ytest. crossval returns an array containing the values returned by f for every cross-validation fold or resampling applied to the given data. X should be an n by m matrix of predictor values y should be an n by 1 vector of predicand values Optional arguments may include name-value pairs as follows: 'KFold' Divide set into k equal-size subsets, using each one successively for validation. 'HoldOut' Divide set into two subsets, training and validation. If the value k is a fraction, that is the fraction of values put in the validation subset (by default k=0.1); if it is a positive integer, that is the number of values in the validation subset. 'LeaveOut' Leave-one-out partition (each element is placed in its own subset). The value is ignored, but it is required. 'Partition' The value should be a cvpartition object. 'Given' The value should be an n by 1 vector specifying in which partition to put each element. 'stratify' The value should be an n by 1 vector containing class designations for the elements, in which case the 'KFold' and 'HoldOut' partitionings attempt to ensure each partition represents the classes proportionately. 'mcreps' The value should be a positive integer specifying the number of times to resample based on different partitionings. Currently only works with the partition type 'HoldOut'. Only one of 'KFold', 'HoldOut', 'LeaveOut', 'Given', 'Partition' should be specified. If none is specified, the default is 'KFold' with k = 10. See also: cvpartition # name: # type: sq_string # elements: 1 # length: 39 Perform cross validation on given data. # name: # type: sq_string # elements: 1 # length: 11 cvpartition # name: # type: sq_string # elements: 1 # length: 255 statistics: cvpartition Partition data for cross-validation The cvpartition class generates a partitioning scheme on a dataset to facilitate cross-validation of statistical models utilizing training and testing subsets of the dataset. See also: crossval # name: # type: sq_string # elements: 1 # length: 35 Partition data for cross-validation # name: # type: sq_string # elements: 1 # length: 20 cvpartition.IsCustom # name: # type: sq_string # elements: 1 # length: 215 cvpartition: property IsCustom Flag for custom partition A logical scalar specifying whether the cvpartition object was created using custom partition partitioning (true) or not (false). This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Flag for custom partition # name: # type: sq_string # elements: 1 # length: 21 cvpartition.IsGrouped # name: # type: sq_string # elements: 1 # length: 206 cvpartition: property IsGrouped Flag for grouped partition A logical scalar specifying whether the cvpartition object was created using grouping variables (true) or not (false). This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Flag for grouped partition # name: # type: sq_string # elements: 1 # length: 24 cvpartition.IsStratified # name: # type: sq_string # elements: 1 # length: 197 cvpartition: property IsStratified Flag for stratified partition A logical scalar specifying whether the cvpartition object was created with a 'stratify' value of true. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Flag for stratified partition # name: # type: sq_string # elements: 1 # length: 27 cvpartition.NumObservations # name: # type: sq_string # elements: 1 # length: 216 cvpartition: property NumObservations Number of observations A positive integer scalar specifying the number of observations in the dataset (including any missing data, where applicable). This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 23 cvpartition.NumTestSets # name: # type: sq_string # elements: 1 # length: 261 cvpartition: property NumTestSets Number of test sets A positive integer scalar specifying the number of folds for partition types 'kfold' and 'leaveout'. When partition type is 'holdout' and 'resubstitution', then NumTestSets is 1. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Number of test sets # name: # type: sq_string # elements: 1 # length: 20 cvpartition.TestSize # name: # type: sq_string # elements: 1 # length: 311 cvpartition: property TestSize Size of each test set A positive integer scalar specifying the size of the test set for partition types 'holdout' and 'resubstitution' or a vector of positive integers specifying the size of each testing set for partition types 'kfold' and 'leaveout'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Size of each test set # name: # type: sq_string # elements: 1 # length: 21 cvpartition.TrainSize # name: # type: sq_string # elements: 1 # length: 315 cvpartition: property TrainSize Size of each train set A positive integer scalar specifying the size of the train set for partition types 'holdout' and 'resubstitution' or a vector of positive integers specifying the size of each training set for partition types 'kfold' and 'leaveout'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Size of each train set # name: # type: sq_string # elements: 1 # length: 16 cvpartition.Type # name: # type: sq_string # elements: 1 # length: 205 cvpartition: property Type Type of validation partition A character vector specifying the type of the cvpartition object. It can be kfold, holdout, leaveout, or resubstitution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Type of validation partition # name: # type: sq_string # elements: 1 # length: 23 cvpartition.cvpartition # name: # type: sq_string # elements: 1 # length: 7301 cvpartition: C = cvpartition (n, 'KFold') cvpartition: C = cvpartition (n, 'KFold', k) cvpartition: C = cvpartition (n, 'KFold', k, 'GroupingVariables', grpvars) cvpartition: C = cvpartition (n, 'Holdout') cvpartition: C = cvpartition (n, 'Holdout', p) cvpartition: C = cvpartition (n, 'Holdout', p, 'GroupingVariables', grpvars) cvpartition: C = cvpartition (n, 'Leaveout') cvpartition: C = cvpartition (n, 'Leaveout', 'GroupingVariables', grpvars) cvpartition: C = cvpartition (n, 'Resubstitution') cvpartition: C = cvpartition (X, 'KFold') cvpartition: C = cvpartition (X, 'KFold', k) cvpartition: C = cvpartition (X, 'KFold', k, 'Stratify', opt) cvpartition: C = cvpartition (X, 'Holdout') cvpartition: C = cvpartition (X, 'Holdout', p) cvpartition: C = cvpartition (X, 'Holdout', p, 'Stratify', opt) cvpartition: C = cvpartition ('CustomPartition', testSets) Repartition data for cross-validation. C = cvpartition (n, 'KFold') creates a cvpartition object C, which defines a random nonstratified partition for k-fold cross-validation on n observations with each fold (subsample) having approximately the same number of observations. The default number of folds is 10 for n >= 10 or equal to n otherwise. C = cvpartition (n, 'KFold', k) also creates a nonstratified random partition for k-fold cross-validation with the number of folds defined by k, which must be a positive integer scalar smaller than the number of observations n. C = cvpartition (n, 'KFold', k, 'GroupingVariables', grpvars) creates a cvpartition object C that defines a random partition for k-fold cross-validation in which every observation sharing a group label, as defined by grpvars, is assigned to the same fold. No group is split between the training and test sets, so a fold holds out whole groups; this is the partition scikit-learn calls GroupKFold, and it is what you want when observations within a group are not independent, such as repeated measurements of one subject. It is not stratification: a fold does not contain a proportional mix of the group labels, and 'Stratify' is the option for that. The grouping variables specified in grpvars can be one of the following: A numeric vector, logical vector, categorical vector, character array, string array, or cell array of character vectors containing one grouping variable. A numeric matrix or cell array containing two or more grouping variables. Each column in the matrix or array must correspond to one grouping variable. 'GroupingVariables' is an Octave extension: MATLAB's cvpartition has no such option. It follows the group-aware splitters of scikit-learn, and each partition type takes the analogue of its ungrouped self: Partition type scikit-learn analogue Effect 'KFold' GroupKFold whole groups fill each fold 'Holdout' GroupShuffleSplit whole groups are held out 'Leaveout' LeaveOneGroupOut one whole group is held out at a time 'Resubstitution' none an error: nothing can be held out of a partition that holds everything Given a stratification variable, 'GroupingVariables' may follow 'Stratify', as in cvpartition (y, 'KFold', k, 'Stratify', true, 'GroupingVariables', grpvars). Each group is then kept whole while the classes of y are spread as evenly over the folds as the groups allow, which is scikit-learn's StratifiedGroupKFold. The two demands conflict, since a group carries whatever class mix it has, so the balance is approximate: groups are placed largest first, each into the fold whose class counts it disturbs least. In every case an observation never appears in both the training and the test set as one of its group fellows, which is what makes grouping worth asking for when observations within a group are not independent. C = cvpartition (n, 'Holdout') creates a cvpartition object C, which defines a random nonstratified partition for holdout validation on n observations. 90% of the observations are assigned to the training set and the remaining 10% to the test set. C = cvpartition (n, 'Holdout', p) also creates a nonstratified random partition for holdout validation with the percentage of training and test sets defined by p, which can be a scalar value in the range (0,1) or a positive integer scalar in the range [1,n). C = cvpartition (n, 'Leaveout') creates a cvpartition object C, which defines a random partition for leave-one-out cross-validation on n observations. This is a special case of k-fold cross-validation with the number of folds equal to the number of observations. C = cvpartition (n, 'Resubstitution') creates a cvpartition object C without partitioning the data and both training and test sets containing all observations n. C = cvpartition (X, 'KFold') creates a cvpartition object C, which defines a stratified random partition for k-fold cross-validation according to the class proportions in X. X can be a numeric, logical, categorical, or string vector, or a character array or a cell array of character vectors. Missing values in X are discarded. The default number of folds is 10 for numel (X) >= 10 or equal to numel (X) otherwise. C = cvpartition (X, 'KFold', k) also creates a stratified random partition for k-fold cross-validation with the number of folds defined by k, which must be a positive integer scalar smaller than the number of observations in X. C = cvpartition (X, 'KFold', k, 'Stratify', opt) creates a random partition for k-fold cross-validation, which is stratified if opt is true, or nonstratified if opt is false. C = cvpartition (X, 'Holdout') creates a cvpartition object C, which defines a stratified random partition for holdout validation while maintaining the class proportions in X. 90% of the observations are assigned to the training set and the remaining 10% to the test set. C = cvpartition (X, 'Holdout', p) also creates a stratified random partition for holdout validation with the percentage of training and test sets defined by p, which can be a scalar value in the range (0,1) or a positive integer scalar in the range [1,n). C = cvpartition (X, 'Holdout', p, 'Stratify', opt) creates a random partition for holdout validation, which is stratified if opt is true, or nonstratified if opt is false. C = cvpartition ('CustomPartition', testSets) creates a custom partition according to testSets, which can be a positive integer vector, a logical vector, or a logical matrix according to the following options: A positive integer vector of length N with values in the range [1,K], where K < N, will specify a K-fold cross-validation partition, in which each value indicates the test set of each observation. Alternatively, the same vector with values in the range [1,N] will specify a leave-one-out cross-validation. A logical vector will specify a holdout validation, in which the true elements correspond to the test set and the false elements correspond to the training set. A logical matrix with K columns will specify a K-fold cross-validation partition, in which each column corresponds to a fold and each row to an observation. Alternatively, an NxN logical matrix will specify a leave-one-out cross-validation, where N is the number of observations. true elements correspond to the test set and the false elements correspond to the training set. See also: cvpartition, cvpartition.summary, cvpartition.test, cvpartition.training # name: # type: sq_string # elements: 1 # length: 38 Repartition data for cross-validation. # name: # type: sq_string # elements: 1 # length: 23 cvpartition.repartition # name: # type: sq_string # elements: 1 # length: 1375 cvpartition: Cnew = repartition (C) cvpartition: Cnew = repartition (C, sval) cvpartition: Cnew = repartition (C, 'legacy') Repartition data for cross-validation. Cnew = repartition (C) creates a cvpartition object Cnew that defines a new random partition of the same type as the cvpartition C. Cnew = repartition (C, sval) also uses the value of sval to set the state of the random generator used in repartitioning C. If sval is a vector, then the random generator is set using the 'state' keyword as in rand ("state", sval). If sval is a scalar, then the 'seed' keyword is used as in rand ("seed", sval) to specify that old generators should be used. Seeding is confined to this call: the state of the random generator is saved beforehand and restored before repartition returns, so sval does not carry over into the random numbers the caller draws afterwards. sval is an Octave extension; MATLAB expects a RandStream object in this position, which Octave does not have. Cnew = repartition (C, 'legacy') only applies to cvpartition objects C that use k-fold partitioning and it will repartition C in the same non-random manner that was previously used by the old-style cvpartition class of the statistics package. The 'legacy' option does not apply to stratified or grouped partitions. See also: cvpartition, cvpartition.summary, cvpartition.test, cvpartition.training # name: # type: sq_string # elements: 1 # length: 38 Repartition data for cross-validation. # name: # type: sq_string # elements: 1 # length: 19 cvpartition.summary # name: # type: sq_string # elements: 1 # length: 1451 cvpartition: tbl = summary (c) Summarize stratified or grouped cross-validation partitions. tbl = summary (c) returns a summary table tbl of the validation partition contained in the cvpartition object c. This method calculates the distribution of classes (if stratified) or groups (if grouped) across the entire dataset, as well as within every training and test set generated by the partition. Inputs c A cvpartition object. The object must satisfy two conditions: The partition type (c.Type) must be 'kfold' or 'holdout'. The partition must be created with a stratification or grouping variable (i.e., c.IsStratified or c.IsGrouped must be true). Outputs tbl A table object containing the summary statistics. The table contains one row for every unique label/group in every set (all, train, test). The columns are: Set The specific subset being described. Values include 'all' (the full dataset), 'train1', 'test1', etc. SetSize The total number of observations in that specific set. Label The class or group identifier. If c.IsStratified is true, this column is named StratificationLabel. If c.IsGrouped is true, it is named GroupLabel. Count The number of observations of that label within the set. If stratified, this column is named StratificationCount; otherwise, GroupCount. PercentInSet The percentage of the set composed of that specific label. See also: cvpartition, cvpartition.repartition, cvpartition.test, cvpartition.training # name: # type: sq_string # elements: 1 # length: 60 Summarize stratified or grouped cross-validation partitions. # name: # type: sq_string # elements: 1 # length: 16 cvpartition.test # name: # type: sq_string # elements: 1 # length: 1091 cvpartition: idx = test (C) cvpartition: idx = test (C, i) cvpartition: idx = test (C, 'all') Test indices for cross-validation. idx = test (C) returns a logical vector idx with true values indicating the elements corresponding to the test set defined in the cvpartition object C. For K-fold and leave-one-out partitions, the indices corresponding to the first test set are returned. idx = test (C, i) returns a logical vector or matrix with the indices of the test set indicated by i. If i is a scalar, then idx is a logical vector with the indices of the i-th set. If i is a vector, then idx is a logical matrix in which idx(:,j) specified the observations in the test set i(j). The value(s) in i must not exceed the number of tests in the cvpartition object C. idx = test (C, 'all') returns a logical vector or matrix for all test sets defined in the cvpartition object C. For holdout and resubstitution partition types, a vector is returned. For K-fold and leave-one-out, a matrix is returned. See also: cvpartition, cvpartition.repartition, cvpartition.summary, cvpartition.training # name: # type: sq_string # elements: 1 # length: 34 Test indices for cross-validation. # name: # type: sq_string # elements: 1 # length: 20 cvpartition.training # name: # type: sq_string # elements: 1 # length: 1135 cvpartition: idx = training (C) cvpartition: idx = training (C, i) cvpartition: idx = training (C, 'all') Training indices for cross-validation. idx = training (C) returns a logical vector idx with true values indicating the elements corresponding to the training set defined in the cvpartition object C. For K-fold and leave-one-out partitions, the indices corresponding to the first training set are returned. idx = training (C, i) returns a logical vector or matrix with the indices of the training set indicated by i. If i is a scalar, then idx is a logical vector with the indices of the i-th set. If i is a vector, then idx is a logical matrix in which idx(:,j) specified the observations in the training set i(j). The value(s) in i must not exceed the number of tests in the cvpartition object C. idx = training (C, 'all') returns a logical vector or matrix for all training sets defined in the cvpartition object C. For holdout and resubstitution partition types, a vector is returned. For K-fold and leave-one-out, a matrix is returned. See also: cvpartition, cvpartition.repartition, cvpartition.summary, cvpartition.test # name: # type: sq_string # elements: 1 # length: 38 Training indices for cross-validation. # name: # type: sq_string # elements: 1 # length: 9 perfcurve # name: # type: sq_string # elements: 1 # length: 2402 statistics: [X, Y] = perfcurve (labels, scores, posclass) statistics: [X, Y, T, AUC, OPTROCPT] = perfcurve (...) statistics: [...] = perfcurve (..., Name, Value) Receiver operating characteristic (ROC) and other classifier performance curves. [X, Y] = perfcurve (labels, scores, posclass) returns the ROC curve for the classifier scores in scores given the true class labels and the positive class posclass. labels is a numeric vector or a cell array of character vectors; scores is a numeric vector of the same length, where larger values indicate stronger evidence for the positive class. By default X is the false positive rate and Y the true positive rate. [X, Y, T, AUC, OPTROCPT] = perfcurve (...) also returns the thresholds T on the scores, the area AUC under the (X, Y) curve, and the optimal operating point OPTROCPT = [FPR, TPR] of the ROC curve. The following Name-Value pairs are supported: Name Value 'XCrit' The criterion for X. The default is 'FPR'. 'YCrit' The criterion for Y. The default is 'TPR'. Supported criteria are 'TPR' ('sens', 'reca'), 'FNR', 'FPR' ('fall'), 'TNR' ('spec'), 'PPV' ('prec'), 'NPV', 'accu', the counts 'TP', 'FN', 'FP', 'TN', and the rates 'RPP', 'RNP'. 'NegClass' The negative class(es). The default, 'all', treats every label other than posclass as negative. 'Weights' A vector of non-negative observation weights. 'Cost' A 2x2 misclassification-cost matrix [C(P|P) C(N|P); C(P|N) C(N|N)] used for OPTROCPT. The default is [0 1; 1 0]. 'XVals' Values of the X criterion at which to return the curve. 'TVals' does the same for the thresholds. 'ProcessNaN' How to treat NaN scores: 'ignore' (default) or 'addtofalse'. 'NBoot' Number of bootstrap replicates for confidence bounds on Y and AUC. The default 0 computes no bounds. 'BootType' The bootstrap interval: 'bca' (default, bias-corrected and accelerated), 'percentile', or 'normal'. 'Alpha' The significance level for the bounds, so the confidence level is 1 - Alpha. The default is 0.05. With 'NBoot' greater than zero, Y is returned as an mx3 array [Y, Ylow, Yhigh] and AUC as [AUC, AUClow, AUChigh]. The bootstrap uses an independent random stream, so the bounds do not match MATLAB numerically. [..., SUBY, SUBYNAMES] = perfcurve (...) returns the Y values for each negative subclass and their names. When called with no output arguments the curve is plotted. See also: fitcsvm, fitcknn, glmfit # name: # type: sq_string # elements: 1 # length: 80 Receiver operating characteristic (ROC) and other classifier performance curves. # name: # type: sq_string # elements: 1 # length: 10 rocmetrics # name: # type: sq_string # elements: 1 # length: 773 statistics: rocmetrics Receiver operating characteristic (ROC) metrics for classifier output. The rocmetrics class evaluates a classifier's performance by computing, for each class, a one-versus-all ROC curve together with a set of threshold-dependent performance metrics. It stores the results in the Metrics table and the per-class area under the curve in AUC, and provides the addMetrics, average, and plot methods for follow-up analysis. For a problem with K classes and scores supplied as an N-by-K matrix, the discriminant score used for class k is the one-versus-all margin Scores(:,k) - max (Scores(:,j)) over j != k, matching MATLAB's rocmetrics. Every metric is evaluated at each distinct value of that margin. See also: perfcurve, confusionmat, confusionchart # name: # type: sq_string # elements: 1 # length: 70 Receiver operating characteristic (ROC) metrics for classifier output. # name: # type: sq_string # elements: 1 # length: 14 rocmetrics.AUC # name: # type: sq_string # elements: 1 # length: 178 rocmetrics: property AUC Area under the ROC curve Row vector holding the area under the one-versus-all ROC curve for each class, in ClassNames order. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Area under the ROC curve # name: # type: sq_string # elements: 1 # length: 21 rocmetrics.ClassNames # name: # type: sq_string # elements: 1 # length: 155 rocmetrics: property ClassNames Class names Class names for which the ROC metrics are computed, in the column order of Scores. This property is read-only. # name: # type: sq_string # elements: 1 # length: 11 Class names # name: # type: sq_string # elements: 1 # length: 15 rocmetrics.Cost # name: # type: sq_string # elements: 1 # length: 222 rocmetrics: property Cost Misclassification cost matrix Square misclassification-cost matrix, with zero diagonal and unit off-diagonal entries by default. It is used by the ExpectedCost metric. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Misclassification cost matrix # name: # type: sq_string # elements: 1 # length: 17 rocmetrics.Labels # name: # type: sq_string # elements: 1 # length: 140 rocmetrics: property Labels Observation labels True class labels supplied at construction, one per observation. This property is read-only. # name: # type: sq_string # elements: 1 # length: 18 Observation labels # name: # type: sq_string # elements: 1 # length: 18 rocmetrics.Metrics # name: # type: sq_string # elements: 1 # length: 383 rocmetrics: property Metrics Performance metrics table Table of performance metrics, vertically concatenated across the classes in ClassNames order with one row per distinct threshold. The standard variables are ClassName, Threshold, FalsePositiveRate, and TruePositiveRate, followed by one variable for each metric requested through 'AdditionalMetrics'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Performance metrics table # name: # type: sq_string # elements: 1 # length: 16 rocmetrics.Prior # name: # type: sq_string # elements: 1 # length: 159 rocmetrics: property Prior Prior class probabilities Row vector of prior class probabilities, in ClassNames order, summing to one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Prior class probabilities # name: # type: sq_string # elements: 1 # length: 17 rocmetrics.Scores # name: # type: sq_string # elements: 1 # length: 147 rocmetrics: property Scores Classification scores Classification scores supplied at construction, as an N-by-K matrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Classification scores # name: # type: sq_string # elements: 1 # length: 18 rocmetrics.Weights # name: # type: sq_string # elements: 1 # length: 162 rocmetrics: property Weights Observation weights Non-negative observation weights, one per observation. Defaults to a vector of ones. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Observation weights # name: # type: sq_string # elements: 1 # length: 21 rocmetrics.addMetrics # name: # type: sq_string # elements: 1 # length: 368 rocmetrics: obj = addMetrics (obj, metrics) Append additional performance metrics to an existing rocmetrics object. metrics is a metric name or a cell array of metric names, chosen from the list supported by the 'AdditionalMetrics' constructor argument. The named metrics are appended as new variables of the Metrics table; metrics already present are left unchanged. # name: # type: sq_string # elements: 1 # length: 71 Append additional performance metrics to an existing rocmetrics object. # name: # type: sq_string # elements: 1 # length: 18 rocmetrics.average # name: # type: sq_string # elements: 1 # length: 723 rocmetrics: [FPR, TPR, Thresholds, AUC] = average (obj, type) Compute an averaged ROC curve across the classes of a rocmetrics object. type selects the averaging method: 'macro' (unweighted mean of the per-class curves), 'micro' (a single curve pooling every one-versus-all instance), or 'weighted' (mean of the per-class curves weighted by Prior). The function returns the averaged false and true positive rates FPR and TPR, the corresponding Thresholds, and the area AUC under the averaged curve. The averaged curve is evaluated on the union of the per-class thresholds. MATLAB inserts additional staircase points when building the averaged curve, so the exact rows and the averaged AUC may differ slightly from MATLAB. # name: # type: sq_string # elements: 1 # length: 72 Compute an averaged ROC curve across the classes of a rocmetrics object. # name: # type: sq_string # elements: 1 # length: 15 rocmetrics.plot # name: # type: sq_string # elements: 1 # length: 260 rocmetrics: plot (obj) rocmetrics: h = plot (obj) Plot the per-class ROC curves of a rocmetrics object. Each class in ClassNames contributes one true-positive-rate versus false-positive-rate curve. A handle to the line objects is returned in h when requested. # name: # type: sq_string # elements: 1 # length: 53 Plot the per-class ROC curves of a rocmetrics object. # name: # type: sq_string # elements: 1 # length: 21 rocmetrics.rocmetrics # name: # type: sq_string # elements: 1 # length: 1827 rocmetrics: obj = rocmetrics (labels, scores, classnames) rocmetrics: obj = rocmetrics (..., Name, Value) Create a rocmetrics object from labels and classification scores. labels is a vector of true class labels with one element per observation; it may be numeric, logical, a character matrix, or a cell array of character vectors. scores is an N-by-K numeric matrix of classification scores, where scores(i,k) is the score of observation i for the class classnames(k). classnames lists the K classes in the column order of scores. The following Name-Value pairs are supported: Name Value 'AdditionalMetrics' A character vector or cell array of metric names to append to Metrics. Supported names are 'TruePositives', 'FalseNegatives', 'FalsePositives', 'TrueNegatives', 'SumOfTrueAndFalsePositives', 'RateOfPositivePredictions', 'RateOfNegativePredictions', 'Accuracy', 'FalseNegativeRate', 'TrueNegativeRate', 'PositivePredictiveValue', 'NegativePredictiveValue', 'ExpectedCost', and 'f1score'. 'Prior' Prior class probabilities, given as 'empirical' (default), 'uniform', or a numeric vector with one value per class. 'Cost' A K-by-K misclassification-cost matrix used by the ExpectedCost metric. The default has zero diagonal and unit off-diagonal entries. 'Weights' A vector of non-negative observation weights. The default is a vector of ones. 'NaNFlag' How to treat NaN scores: 'omitnan' (default) drops the affected observations, while 'includenan' treats them as always classified negative. 'FixedMetricValues' 'all' (default) to use every distinct threshold, or a numeric vector of threshold values at which to report the curve (nearest actual thresholds are returned). Construction from a trained model object, the 'FixedMetric' grids other than thresholds, and bootstrap confidence intervals are not implemented. # name: # type: sq_string # elements: 1 # length: 65 Create a rocmetrics object from labels and classification scores. statistics-release-2.1.2/inst/Model_Evaluation/perfcurve.m000066400000000000000000000575761527311104300237300ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{X}, @var{Y}] =} perfcurve (@var{labels}, @var{scores}, @var{posclass}) ## @deftypefnx {statistics} {[@var{X}, @var{Y}, @var{T}, @var{AUC}, @var{OPTROCPT}] =} perfcurve (@dots{}) ## @deftypefnx {statistics} {[@dots{}] =} perfcurve (@dots{}, @var{Name}, @var{Value}) ## ## Receiver operating characteristic (ROC) and other classifier performance ## curves. ## ## @code{[@var{X}, @var{Y}] = perfcurve (@var{labels}, @var{scores}, ## @var{posclass})} returns the ROC curve for the classifier scores in ## @var{scores} given the true class @var{labels} and the positive class ## @var{posclass}. @var{labels} is a numeric vector or a cell array of ## character vectors; @var{scores} is a numeric vector of the same length, where ## larger values indicate stronger evidence for the positive class. By default ## @var{X} is the false positive rate and @var{Y} the true positive rate. ## ## @code{[@var{X}, @var{Y}, @var{T}, @var{AUC}, @var{OPTROCPT}] = perfcurve ## (@dots{})} also returns the thresholds @var{T} on the scores, the area ## @var{AUC} under the (@var{X}, @var{Y}) curve, and the optimal operating point ## @var{OPTROCPT} @code{= [FPR, TPR]} of the ROC curve. ## ## The following @qcode{Name-Value} pairs are supported: ## ## @multitable @columnfractions 0.18 0.82 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'XCrit'} @tab The criterion for @var{X}. The default is ## @qcode{'FPR'}. ## ## @item @qcode{'YCrit'} @tab The criterion for @var{Y}. The default is ## @qcode{'TPR'}. Supported criteria are @qcode{'TPR'} (@qcode{'sens'}, ## @qcode{'reca'}), @qcode{'FNR'}, @qcode{'FPR'} (@qcode{'fall'}), @qcode{'TNR'} ## (@qcode{'spec'}), @qcode{'PPV'} (@qcode{'prec'}), @qcode{'NPV'}, ## @qcode{'accu'}, the counts @qcode{'TP'}, @qcode{'FN'}, @qcode{'FP'}, ## @qcode{'TN'}, and the rates @qcode{'RPP'}, @qcode{'RNP'}. ## ## @item @qcode{'NegClass'} @tab The negative class(es). The default, ## @qcode{'all'}, treats every label other than @var{posclass} as negative. ## ## @item @qcode{'Weights'} @tab A vector of non-negative observation weights. ## ## @item @qcode{'Cost'} @tab A @math{2*2} misclassification-cost matrix ## @code{[C(P|P) C(N|P); C(P|N) C(N|N)]} used for @var{OPTROCPT}. The default ## is @code{[0 1; 1 0]}. ## ## @item @qcode{'XVals'} @tab Values of the @var{X} criterion at which to return ## the curve. @qcode{'TVals'} does the same for the thresholds. ## ## @item @qcode{'ProcessNaN'} @tab How to treat @code{NaN} scores: ## @qcode{'ignore'} (default) or @qcode{'addtofalse'}. ## ## @item @qcode{'NBoot'} @tab Number of bootstrap replicates for confidence ## bounds on @var{Y} and @var{AUC}. The default @math{0} computes no bounds. ## ## @item @qcode{'BootType'} @tab The bootstrap interval: @qcode{'bca'} (default, ## bias-corrected and accelerated), @qcode{'percentile'}, or @qcode{'normal'}. ## ## @item @qcode{'Alpha'} @tab The significance level for the bounds, so the ## confidence level is @math{1 - @var{Alpha}}. The default is @math{0.05}. ## @end multitable ## ## With @qcode{'NBoot'} greater than zero, @var{Y} is returned as an ## @math{m*3} array @code{[@var{Y}, @var{Ylow}, @var{Yhigh}]} and @var{AUC} as ## @code{[@var{AUC}, @var{AUClow}, @var{AUChigh}]}. The bootstrap uses an ## independent random stream, so the bounds do not match MATLAB numerically. ## @code{[@dots{}, @var{SUBY}, @var{SUBYNAMES}] = perfcurve (@dots{})} returns ## the @var{Y} values for each negative subclass and their names. ## ## When called with no output arguments the curve is plotted. ## ## @seealso{fitcsvm, fitcknn, glmfit} ## @end deftypefn function [X, Y, T, AUC, OPTROCPT, SUBY, SUBYNAMES] = perfcurve (labels, ... scores, posclass, varargin) if (nargin < 3) print_usage (); endif if (! (isnumeric (scores) && isreal (scores) && isvector (scores))) error ("perfcurve: SCORES must be a vector of real values."); endif scores = scores(:); n = numel (scores); ## Check the class and the length separately, so that a type failure is not ## reported as a length failure if (! (isnumeric (labels) || islogical (labels) || iscellstr (labels) || ischar (labels))) error (strcat ("perfcurve: LABELS must be numeric, logical, a character", ... " matrix, or a cell array of character vectors.")); endif if (numel_labels (labels) != n) error ("perfcurve: LABELS must have one element per score."); endif ## Defaults and Name-Value parsing. xcrit = "FPR"; ycrit = "TPR"; negclass = "all"; weights = []; cost = [0 1; 1 0]; xvals = []; tvals = []; usenearest = true; prior = "empirical"; processnan = "ignore"; nboot = 0; boottype = "bca"; alpha = 0.05; if (mod (numel (varargin), 2) != 0) error ("perfcurve: optional arguments must be Name-Value pairs."); endif for k = 1:2:numel (varargin) if (! ischar (varargin{k})) error ("perfcurve: parameter names must be character vectors."); endif switch (lower (varargin{k})) case "xcrit" xcrit = varargin{k+1}; case "ycrit" ycrit = varargin{k+1}; case "negclass" negclass = varargin{k+1}; case "weights" weights = varargin{k+1}; case "cost" cost = varargin{k+1}; case "xvals" xvals = varargin{k+1}; case "tvals" tvals = varargin{k+1}; case "usenearest" uv = varargin{k+1}; usenearest = strcmpi (uv, "on") || isequal (uv, true); case "prior" prior = varargin{k+1}; case "processnan" processnan = lower (varargin{k+1}); case "nboot" nboot = varargin{k+1}; case "boottype" boottype = lower (varargin{k+1}); case "alpha" alpha = varargin{k+1}; otherwise error ("perfcurve: unknown parameter name '%s'.", varargin{k}); endswitch endfor if (! isempty (xvals) && ! isempty (tvals)) error ("perfcurve: 'XVals' and 'TVals' cannot be used together."); endif if (! (isnumeric (cost) && isequal (size (cost), [2, 2]))) error ("perfcurve: 'Cost' must be a 2-by-2 matrix."); endif if (! (isnumeric (nboot) && isscalar (nboot) && nboot >= 0 && nboot == fix (nboot))) error ("perfcurve: 'NBoot' must be a non-negative integer."); endif if (! (isnumeric (alpha) && isscalar (alpha) && alpha > 0 && alpha < 1)) error ("perfcurve: 'Alpha' must be a scalar in the range (0,1)."); endif ## Positive / negative membership (one-vs-all by default). ispos = label_match (labels, posclass); if (ischar (negclass) && strcmpi (negclass, "all")) isneg = ! ispos; else isneg = label_match (labels, negclass); endif SUBYNAMES = negclass_names (labels, isneg, negclass); ## Weights. if (isempty (weights)) w = ones (n, 1); else if (! (isnumeric (weights) && isreal (weights) && isvector (weights) && numel (weights) == n && all (weights >= 0))) error ("perfcurve: 'Weights' must be a non-negative vector with one element per score."); endif w = weights(:); endif ## NaN score handling. isnanscore = isnan (scores); if (any (isnanscore)) switch (processnan) case "ignore" keep = ! isnanscore; scores = scores(keep); ispos = ispos(keep); isneg = isneg(keep); w = w(keep); case "addtofalse" scores(isnanscore) = -Inf; ## always classified negative otherwise error ("perfcurve: unrecognised 'ProcessNaN' value."); endswitch endif ## Restrict to labelled (positive or negative) observations. use = ispos | isneg; scores = scores(use); ispos = ispos(use); isneg = isneg(use); w = w(use); if (! any (ispos) || ! any (isneg)) error ("perfcurve: LABELS must contain both positive and negative classes."); endif ## Optionally reweight the observations to a requested prior. [~, ~, w] = apply_prior (prior, sum (w(ispos)), sum (w(isneg)), w, ... ispos, isneg); ## Full-sample curve for the requested criteria plus the underlying ROC. [X, Y, T, tpr, fpr, P, N] = build_curve (scores, ispos, isneg, w, ... xcrit, ycrit); ## Area under the (X, Y) curve and the cost-optimal ROC operating point. AUC = trapz (X, Y); ecost = P * (1 - tpr) * cost(1,2) + N * fpr * cost(2,1); best = find (abs (ecost - min (ecost)) < 1e-12); [~, bk] = min (fpr(best)); OPTROCPT = [fpr(best(bk)), tpr(best(bk))]; ## Restrict the returned curve to requested X or threshold values. if (! isempty (xvals)) idx = select_xvals (X, Y, xvals(:), usenearest); elseif (! isempty (tvals)) idx = select_tvals (T, tvals(:)); else idx = (1:numel (X))'; endif X = X(idx); Y = Y(idx); T = T(idx); SUBY = Y; ## Bootstrap confidence bounds on Y and AUC (self-contained resampler). if (nboot > 0) [Ylo, Yhi, alo, ahi] = perfcurve_boot_ (scores, ispos, isneg, w, xcrit, ... ycrit, X, Y, AUC, nboot, boottype, alpha); Y = [Y, Ylo, Yhi]; SUBY = Y; AUC = [AUC, alo, ahi]; endif ## Plot when no output is requested. if (nargout == 0) plot (X, Y(:,1)); xlabel (xcrit); ylabel (ycrit); clear X Y T AUC OPTROCPT SUBY SUBYNAMES endif endfunction ## Build the full performance curve for a data subset: returns the X and Y ## criteria, thresholds T, the underlying ROC rates, and the class totals. function [X, Y, T, tpr, fpr, P, N] = build_curve (scores, ispos, isneg, w, ... xcrit, ycrit) P = sum (w(ispos)); N = sum (w(isneg)); [ss, ord] = sort (scores, "descend"); pw = cumsum (w(ord) .* ispos(ord)); nw = cumsum (w(ord) .* isneg(ord)); chg = find (diff (ss) != 0); last = [chg; numel(ss)]; ## last index of each score group tp = [0; pw(last)]; fp = [0; nw(last)]; T = [ss(1); ss(last)]; X = criterion (xcrit, tp, fp, P, N); Y = criterion (ycrit, tp, fp, P, N); tpr = tp / P; fpr = fp / N; endfunction ## Number of labels in a numeric vector or cell array of character vectors. function m = numel_labels (labels) if (ischar (labels)) m = rows (labels); else m = numel (labels); endif endfunction ## Logical membership of LABELS in CLS (numeric equality or string match). function tf = label_match (labels, cls) if (isnumeric (labels)) tf = ismember (labels(:), cls(:)); else if (ischar (labels)) labels = cellstr (labels); endif if (ischar (cls)) cls = {cls}; endif tf = ismember (labels(:), cls(:)); endif endfunction ## Rescale weights so the class totals follow a requested prior. function [P, N, w] = apply_prior (prior, P, N, w, ispos, isneg) if (ischar (prior)) switch (lower (prior)) case "empirical" return; case "uniform" pn = [0.5, 0.5]; otherwise error ("perfcurve: unrecognised 'Prior' value."); endswitch elseif (isnumeric (prior) && numel (prior) == 2 && all (prior > 0)) pn = prior(:)' / sum (prior); else error ("perfcurve: 'Prior' must be 'empirical', 'uniform', or [P N]."); endif w(ispos) = w(ispos) * (pn(1) / P); w(isneg) = w(isneg) * (pn(2) / N); P = pn(1); N = pn(2); endfunction ## Evaluate a named performance criterion from the count arrays. function v = criterion (name, tp, fp, P, N) tn = N - fp; fn = P - tp; total = P + N; pp = tp + fp; switch (lower (name)) case {"tpr", "sens", "reca", "recall"} v = tp / P; case {"fnr", "miss"} v = fn / P; case {"fpr", "fall"} v = fp / N; case {"tnr", "spec"} v = tn / N; case {"ppv", "prec", "precision"} v = tp ./ pp; case "npv" v = tn ./ (tn + fn); case {"accu", "acc"} v = (tp + tn) / total; case "tp" v = tp; case "fn" v = fn; case "fp" v = fp; case "tn" v = tn; case "rpp" v = pp / total; case "rnp" v = (tn + fn) / total; otherwise error ("perfcurve: unsupported criterion '%s'.", name); endswitch endfunction ## Indices of the curve returned for requested X values: the (0,0) start plus, ## for each value, the upper-envelope point at the largest actual X not above. function idx = select_xvals (X, Y, xvals, usenearest) idx = 1; for v = xvals' if (usenearest) cand = find (X <= v + 1e-12); if (isempty (cand)) cand = 1; endif xstar = max (X(cand)); here = find (abs (X - xstar) < 1e-12); idx(end+1) = here(end); ## upper envelope (largest Y at this X) else idx(end+1) = interp_index (X, v); endif endfor idx = idx(:); endfunction ## Indices for requested threshold values: nearest actual threshold (Inf -> the ## first, "reject all" point). function idx = select_tvals (T, tvals) idx = zeros (numel (tvals), 1); for k = 1:numel (tvals) if (tvals(k) >= T(1)) idx(k) = 1; else [~, idx(k)] = min (abs (T - tvals(k))); endif endfor endfunction ## Nearest index (used for UseNearest 'off' fallback). function i = interp_index (X, v) [~, i] = min (abs (X - v)); endfunction ## Names of the negative class(es), for SUBYNAMES. function names = negclass_names (labels, isneg, negclass) if (ischar (negclass) && strcmpi (negclass, "all")) if (isnumeric (labels)) names = arrayfun (@(v) num2str (v), unique (labels(isneg)), ... "UniformOutput", false); else if (ischar (labels)) labels = cellstr (labels); endif names = unique (labels(isneg)); endif elseif (isnumeric (negclass)) names = arrayfun (@(v) num2str (v), negclass(:), "UniformOutput", false); elseif (ischar (negclass)) names = {negclass}; else names = negclass(:); endif endfunction ## Nonparametric bootstrap confidence bounds on the Y criterion (at the returned ## reference X values REFX, point estimates REFY) and on the AUC. Observations ## are resampled with replacement; percentile or bias-corrected-and-accelerated ## (BCa) bounds are formed. Bounds do not match MATLAB numerically because the ## bootstrap uses an independent random stream. function [Ylo, Yhi, alo, ahi] = perfcurve_boot_ (scores, ispos, isneg, w, ... xcrit, ycrit, refX, refY, aucHat, ... nboot, boottype, alpha) n = numel (scores); m = numel (refX); Yb = zeros (nboot, m); aucB = zeros (nboot, 1); for b = 1:nboot ii = resample_both (n, ispos, isneg); [Xr, Yr] = build_curve (scores(ii), ispos(ii), isneg(ii), w(ii), ... xcrit, ycrit); Yb(b,:) = interp_curve (Xr, Yr, refX); aucB(b) = trapz (Xr, Yr); endfor switch (boottype) case {"percentile", "per"} [Ylo, Yhi] = ci_percentile (Yb, alpha); [alo, ahi] = ci_percentile (aucB, alpha); case "bca" Yj = zeros (n, m); aucJ = zeros (n, 1); valid = true (n, 1); for i = 1:n keep = true (n, 1); keep(i) = false; if (! any (ispos(keep)) || ! any (isneg(keep))) valid(i) = false; continue; endif [Xj, Yjc] = build_curve (scores(keep), ispos(keep), isneg(keep), ... w(keep), xcrit, ycrit); Yj(i,:) = interp_curve (Xj, Yjc, refX); aucJ(i) = trapz (Xj, Yjc); endfor [Ylo, Yhi] = ci_bca (Yb, Yj(valid,:), refY(:)', alpha); [alo, ahi] = ci_bca (aucB, aucJ(valid), aucHat, alpha); case {"normal", "norm"} z = norminv (1 - alpha / 2); Ylo = (mean (Yb) - z * std (Yb))'; Yhi = (mean (Yb) + z * std (Yb))'; alo = mean (aucB) - z * std (aucB); ahi = mean (aucB) + z * std (aucB); otherwise error ("perfcurve: unrecognised 'BootType' value."); endswitch Ylo = Ylo(:); Yhi = Yhi(:); endfunction ## Resample N observation indices with replacement, ensuring both classes are ## present in the draw. function ii = resample_both (n, ispos, isneg) ii = randi (n, n, 1); tries = 0; while ((! any (ispos(ii)) || ! any (isneg(ii))) && tries < 100) ii = randi (n, n, 1); tries++; endwhile endfunction ## Evaluate a curve (X, Y) at query abscissae XQ using its upper envelope (max Y ## at each distinct X) and linear interpolation, clamped to the endpoints. function yq = interp_curve (X, Y, xq) [xu, ~, ic] = unique (X(:)); yu = accumarray (ic, Y(:), [], @max); yq = interp1 (xu, yu, xq(:), "linear"); yq(xq(:) <= xu(1)) = yu(1); yq(xq(:) >= xu(end)) = yu(end); yq = yq'; endfunction ## Percentile confidence bounds (over rows) of the bootstrap replicates B. function [lo, hi] = ci_percentile (B, alpha) lo = quantile (B, alpha / 2, 1)'; hi = quantile (B, 1 - alpha / 2, 1)'; endfunction ## Bias-corrected and accelerated (BCa) confidence bounds. B is nboot-by-m, ## J the jackknife replicates (n-by-m), THETAHAT the point estimate (1-by-m). function [lo, hi] = ci_bca (B, J, thetahat, alpha) nb = rows (B); m = columns (B); frac = sum (B < thetahat, 1) / nb; frac = min (max (frac, 1 / (nb + 1)), nb / (nb + 1)); z0 = norminv (frac); d = mean (J, 1) - J; a = sum (d .^ 3, 1) ./ (6 * (sum (d .^ 2, 1)) .^ 1.5); a(! isfinite (a)) = 0; za = norminv (alpha / 2); zb = norminv (1 - alpha / 2); a1 = normcdf (z0 + (z0 + za) ./ (1 - a .* (z0 + za))); a2 = normcdf (z0 + (z0 + zb) ./ (1 - a .* (z0 + zb))); a1(! isfinite (a1)) = alpha / 2; a2(! isfinite (a2)) = 1 - alpha / 2; lo = zeros (m, 1); hi = zeros (m, 1); for j = 1:m lo(j) = quantile (B(:,j), a1(j)); hi(j) = quantile (B(:,j), a2(j)); endfor endfunction %!demo %! ## ROC curve for scores with a known positive class %! scores = [0.9 0.8 0.7 0.6 0.55 0.5 0.4 0.3 0.2 0.1]; %! labels = [1 1 0 1 0 1 0 0 1 0]; %! [X, Y, T, AUC] = perfcurve (labels, scores, 1); %! plot (X, Y, "b-o"); xlabel ("FPR"); ylabel ("TPR"); %! title (sprintf ("ROC curve (AUC = %.2f)", AUC)); %!shared labels, scores %! labels = [1 1 0 1 0 1 0 0 1 0]; %! scores = [0.95 0.9 0.85 0.7 0.7 0.6 0.55 0.4 0.35 0.2]; %!test # MATLAB parity: default ROC curve, thresholds, AUC and OPTROCPT %! [X, Y, T, AUC, OPT] = perfcurve (labels, scores, 1); %! assert_equal (X, [0 0 0 0.2 0.4 0.4 0.6 0.8 0.8 1]', 1e-12); %! assert_equal (Y, [0 0.2 0.4 0.4 0.6 0.8 0.8 0.8 1 1]', 1e-12); %! assert_equal (T, [0.95 0.95 0.9 0.85 0.7 0.6 0.55 0.4 0.35 0.2]', 1e-12); %! assert_equal (AUC, 0.7, 1e-12); %! assert_equal (OPT, [0 0.4], 1e-12); %!test # MATLAB parity: precision-recall and its NaN at the origin %! [Xr, Yr] = perfcurve (labels, scores, 1, "XCrit", "reca", "YCrit", "prec"); %! assert_equal (Xr, [0 0.2 0.4 0.4 0.6 0.8 0.8 0.8 1 1]', 1e-12); %! assert_equal (Yr(2:end), [1 1 2/3 0.6 2/3 4/7 0.5 5/9 0.5]', 1e-12); %! assert_equal (isnan (Yr(1)), true); %!test # MATLAB parity: accuracy and specificity/sensitivity criteria %! [~, Ya] = perfcurve (labels, scores, 1, "YCrit", "accu"); %! assert_equal (Ya, [0.5 0.6 0.7 0.6 0.6 0.7 0.6 0.5 0.6 0.5]', 1e-12); %! [Xs, Ys] = perfcurve (labels, scores, 1, "XCrit", "spec", "YCrit", "sens"); %! assert_equal (Xs, [1 1 1 0.8 0.6 0.6 0.4 0.2 0.2 0]', 1e-12); %! assert_equal (Ys, [0 0.2 0.4 0.4 0.6 0.8 0.8 0.8 1 1]', 1e-12); %!test # MATLAB parity: weighted curve, AUC and OPTROCPT %! w = [2 1 1 1 1 1 1 1 1 2]; %! [Xw, Yw, Tw, AUCw, OPTw] = perfcurve (labels, scores, 1, "Weights", w); %! assert_equal (Xw, [0 0 0 1 2 2 3 4 4 6]'/6, 1e-12); %! assert_equal (Yw, [0 2 3 3 4 5 5 5 6 6]'/6, 1e-12); %! assert_equal (AUCw, 0.791666666666667, 1e-12); %! assert_equal (OPTw, [0 0.5], 1e-12); %!test # MATLAB parity: OPTROCPT shifts with a non-default cost matrix %! [~, ~, ~, ~, OPTc] = perfcurve (labels, scores, 1, "Cost", [0 1; 2 0]); %! assert_equal (OPTc, [0 0.4], 1e-12); %!test # MATLAB parity: TVals selects nearest thresholds %! [Xt, Yt, Tt] = perfcurve (labels, scores, 1, "TVals", [Inf 0.8 0.6 0.4 0.2]); %! assert_equal (Xt, [0 0.2 0.4 0.8 1]', 1e-12); %! assert_equal (Yt, [0 0.4 0.8 0.8 1]', 1e-12); %! assert_equal (Tt, [0.95 0.85 0.6 0.4 0.2]', 1e-12); %!test # MATLAB parity: XVals selects floor points plus the origin %! [Xv, Yv, Tv] = perfcurve (labels, scores, 1, "XVals", [0 0.25 0.5 0.75 1]); %! assert_equal (Xv, [0 0 0.2 0.4 0.6 1]', 1e-12); %! assert_equal (Yv, [0 0.4 0.4 0.8 0.8 1]', 1e-12); %! assert_equal (Tv, [0.95 0.9 0.85 0.6 0.55 0.2]', 1e-12); %!test # MATLAB parity: cell-array string labels %! labels2 = {"g","g","b","g","b","g","b","b","g","b"}; %! [X, Y, T, AUC] = perfcurve (labels2, scores, "g"); %! assert_equal (AUC, 0.7, 1e-12); %! assert_equal (X, [0 0 0 0.2 0.4 0.4 0.6 0.8 0.8 1]', 1e-12); %!test # AUC of a perfectly separable problem is 1 %! sc = [0.9 0.8 0.7 0.6 0.3 0.2 0.1 0.05]; %! lb = [1 1 1 1 0 0 0 0]; %! [~, ~, ~, auc] = perfcurve (lb, sc, 1); %! assert_equal (auc, 1, 1e-12); %!test # bootstrap: shapes, point estimate preserved, bounds bracket it (BCa) %! rand ("seed", 42); randn ("seed", 42); %! [X, Y, T, AUC, OPT, SUBY, SUBYN] = ... %! perfcurve (labels, scores, 1, "NBoot", 200); %! assert_equal (columns (Y), 3); %! assert_equal (size (AUC), [1, 3]); %! assert_equal (Y(:,1), [0 0.2 0.4 0.4 0.6 0.8 0.8 0.8 1 1]', 1e-12); %! assert_equal (all (Y(:,2) <= Y(:,1) + 1e-9), true); # lower <= est %! assert_equal (all (Y(:,3) >= Y(:,1) - 1e-9), true); # upper >= est %! assert_equal (AUC(2) <= AUC(1) && AUC(1) <= AUC(3), true); %! assert_equal (isequal (SUBY, Y), true); %!test # bootstrap: percentile bounds also run and bracket the AUC estimate %! rand ("seed", 7); randn ("seed", 7); %! [~, Y, ~, AUC] = perfcurve (labels, scores, 1, "NBoot", 200, ... %! "BootType", "percentile"); %! assert_equal (columns (Y), 3); %! assert_equal (AUC(2) <= AUC(1) && AUC(1) <= AUC(3), true); ## Test input validation %!error perfcurve (1, 2) %!test %! ## Logical labels are accepted, as MATLAB does, and give the same curve as %! ## the equivalent numeric labels. %! s = [0.9; 0.8; 0.7; 0.6; 0.5; 0.4; 0.3; 0.2]; %! l = [1; 1; 1; 0; 1; 0; 0; 0]; %! [xn, yn, tn, an] = perfcurve (l, s, 1); %! [xg, yg, tg, ag] = perfcurve (logical (l), s, true); %! assert_equal (xg, xn); %! assert_equal (yg, yn); %! assert_equal (tg, tn); %! assert_equal (ag, an); %! ## and the curve is MATLAB's, not merely self-consistent %! assert_equal (xg(:)', [0, 0, 0, 0, 0.25, 0.25, 0.5, 0.75, 1], 1e-12); %!error ... %! perfcurve ([1 0], ones (2, 2), 1) %!error ... %! perfcurve ([1 0 1], [0.5 0.4], 1) %!error ... %! perfcurve (struct ('a', 1), [0.5 0.4], 1) %!error ... %! perfcurve ({1, 2}, [0.5 0.4], 1) %!error ... %! perfcurve ([1 1 1], [0.5 0.4 0.3], 1) %!error ... %! perfcurve ([1 0], [0.5 0.4], 1, "YCrit", "foo") %!error ... %! perfcurve ([1 0], [0.5 0.4], 1, "Cost", [0 1 0]) %!error ... %! perfcurve ([1 0], [0.5 0.4], 1, "NBoot", -5) %!error ... %! perfcurve ([1 0], [0.5 0.4], 1, "Alpha", 1.5) statistics-release-2.1.2/inst/Model_Evaluation/private/000077500000000000000000000000001527311104300231775ustar00rootroot00000000000000statistics-release-2.1.2/inst/Model_Evaluation/private/__resolve_groups__.m000066400000000000000000000046351527311104300272370ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{inds}, @var{ngroups}, @var{gsize}, @var{missidx}, @var{grpvars}] =} __resolve_groups__ (@var{grpvars}) ## ## Reduce grouping variables to a group index per observation. ## ## @var{inds} numbers each observation by the group it belongs to, @var{ngroups} ## counts the groups and @var{gsize} their sizes. @var{missidx} flags the ## observations dropped for holding a missing value, and @var{grpvars} is ## returned with those observations removed. ## ## One grouping variable is given as a vector, several as a matrix or cell ## array whose columns are the variables; a row then belongs to a group only if ## it matches on every one of them. ## ## @end deftypefn function [inds, ngroups, gsize, missidx, grpvars] = __resolve_groups__ (grpvars) if (isvector (grpvars)) missidx = ismissing (grpvars); if (any (missidx)) grpvars(missidx) = []; endif if (isa (grpvars, 'categorical')) [~, idx, inds] = unique (grpvars, 'stable'); else [~, idx, inds] = __unique__ (grpvars, 'stable'); endif elseif (ismatrix (grpvars)) missidx = any (ismissing (grpvars), 2); if (any (missidx)) grpvars(missidx, :) = []; endif if (isa (grpvars, 'categorical')) [~, idx, inds] = unique (grpvars, 'rows', 'stable'); else [~, idx, inds] = __unique__ (grpvars, 'rows', 'stable'); endif else error (strcat ("cvpartition: invalid value for optional", ... " paired argument 'GroupingVariables'.")); endif ngroups = numel (idx); gsize = zeros (1, ngroups); for i = 1:ngroups gsize(i) = sum (inds == i); endfor endfunction statistics-release-2.1.2/inst/Model_Evaluation/private/__stratified_group_folds__.m000066400000000000000000000054401527311104300307150ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{folds} =} __stratified_group_folds__ (@var{classes}, @var{gidx}, @var{k}) ## ## Assign whole groups to @var{k} folds while keeping the class balance even. ## ## @var{classes} numbers each observation by its class and @var{gidx} by its ## group; @var{folds} returns the fold each observation is assigned to. Every ## observation of a group lands in the same fold, so no group is split, and the ## folds are chosen to keep each class spread as evenly as the groups allow. ## ## The two demands conflict: once a group is atomic it carries whatever class ## mix it has, so the class balance can only be approximate. Groups are placed ## largest first, each into the fold whose class counts it disturbs least, ## which is the greedy assignment scikit-learn's @code{StratifiedGroupKFold} ## uses. ## ## @end deftypefn function folds = __stratified_group_folds__ (classes, gidx, k) nclasses = max (classes); ngroups = max (gidx); ## Class counts of every group gcount = zeros (ngroups, nclasses); for g = 1:ngroups for c = 1:nclasses gcount(g,c) = sum (gidx == g & classes == c); endfor endfor ## Largest groups first: they constrain the balance most, so they are placed ## while every fold is still empty enough to take them [~, order] = sort (sum (gcount, 2), 'descend'); fcount = zeros (k, nclasses); # class counts accumulated per fold gfold = zeros (ngroups, 1); for i = 1:ngroups g = order(i); best = 1; best_cost = Inf; for f = 1:k trial = fcount; trial(f,:) += gcount(g,:); ## How unevenly each class is spread over the folds, averaged cost = mean (std (trial, 0, 1)); ## Break ties toward the emptiest fold, so the folds stay similar in size if (cost < best_cost || (cost == best_cost && sum (trial(f,:)) < sum (fcount(best,:)))) best = f; best_cost = cost; endif endfor fcount(best,:) += gcount(g,:); gfold(g) = best; endfor folds = gfold(gidx); endfunction statistics-release-2.1.2/inst/Model_Evaluation/private/__unique__.m000066400000000000000000000132131527311104300254570ustar00rootroot00000000000000## Copyright (C) 2024-2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{B} =} __unique__ (@var{A}) ## @deftypefnx {Private Function} {@var{B} =} __unique__ (@var{A}, @var{setOrder}) ## @deftypefnx {Private Function} {@var{B} =} __unique__ (@var{A}, @var{occurrence}) ## @deftypefnx {Private Function} {@var{B} =} __unique__ (@var{A}, @var{setOrder}, @var{occurrence}) ## @deftypefnx {Private Function} {@var{B} =} __unique__ (@var{A}, @var{occurrence}, @var{setOrder}) ## @deftypefnx {Private Function} {@var{B} =} __unique__ (@var{A}, @dots{}, @qcode{'rows'}) ## @deftypefnx {Private Function} {@var{B} =} __unique__ (@var{A}, @qcode{'rows'}, @dots{}) ## @deftypefnx {Private Function} {[@var{B}, @var{ixA}, @var{ixB}] =} __unique__ (@dots{}) ## ## Return the unique elements of @var{x}. ## ## @end deftypefn function [y, i, j] = __unique__ (x, varargin) if (nargin < 1) print_usage (); elseif (! (isnumeric (x) || islogical (x) || ischar (x) || iscellstr (x))) error ("unique: X must be an array or cell array of strings."); endif if (nargin > 1) ## Parse options if (! iscellstr (varargin)) error ("unique: options must be strings."); endif optrows = any (strcmp ("rows", varargin)); optfirst = any (strcmp ("first", varargin)); optlast = any (strcmp ("last", varargin)); optsorted = any (strcmp ("sorted", varargin)); optstable = any (strcmp ("stable", varargin)); if (optrows && ndims (x) != 2) error ("unique: 'rows' applies only to 2-D matrices."); elseif (optfirst && optlast) error ("unique: cannot specify both 'first' and 'last'."); elseif (optsorted && optstable) error ("unique: cannot specify both 'sorted' and 'stable'."); elseif (optrows + optfirst + optlast + optsorted + optstable != nargin-1) error ("unique: invalid optional argument."); endif ## Set defaults if not set earlier. if (! optfirst && ! optlast) optfirst = true; endif if (! optsorted && ! optstable) optsorted = true; endif else optrows = false; optfirst = true; optsorted = true; endif if (optrows) n = rows (x); isrowvec = false; else n = numel (x); isrowvec = isrow (x); endif ## Special cases 0 and 1 if (n == 0) y = x; if (! optrows && any (size (x))) if (iscellstr (x)) y = cell (0, 1); else y = zeros (0, 1, class (x)); endif endif i = j = []; return; elseif (n == 1) y = x; i = j = 1; return; endif ## Calculate y output if (optrows) if (nargout > 1 || ! optsorted) [y, j] = sortrows (x); j = j(:); else y = sortrows (x); endif if (iscellstr (x)) match = all (cellfun (@isequal, y(1:n-1,:), y(2:n,:)), 2); else match = all (y(1:n-1,:) == y(2:n,:), 2); endif if (optsorted) y(match,:) = []; else y = x; if (optfirst) y(j([false; match]), :) = []; else y(j([match; false]), :) = []; endif endif else if (isvector (x)) y = x; else y = x(:); endif if (nargout > 1 || ! optsorted) [y, j] = sort (y); j = j(:); else y = sort (y); endif if (iscellstr (y)) match = strcmp (y(1:n-1), y(2:n)); else match = (y(1:n-1) == y(2:n)); endif if (optsorted) y(match) = []; else if (isvector (x)) y = x; else y = x(:); endif if (optfirst) y(j([false; match(:)])) = []; else y(j([match(:); false])) = []; endif endif endif ## Calculate 2nd and 3rd outputs if (nargout > 1) if (optsorted) idx = find (match); if (optfirst) idx += 1; endif i = j; i(idx) = []; if (nargout > 2) j(j) = cumsum (! [false; match(:)]); endif else ## Get inverse of sort index j so that sort(x)(k) = x(j)(k) = x. k = j; # cheap way to copy dimensions k(j) = 1:n; ## Generate logical index of sorted unique value locations. if (optfirst) uniquex = ! [false; match(:)]; else uniquex = ! [match(:); false]; endif ## Remap unique locations to unsorted x, such that y = x(i). i = find (uniquex(k)); if (nargout > 2) ni = numel (i); u = find (uniquex); # Linear index of unique elements of sort(x) p = j; # cheap way to copy dimensions if (optfirst) l = u(cumsum (uniquex)); # Expand u for all elements in sort(x) p(i) = 1:ni; # set p to contain the vector positions of i elseif (optrows) l = u(cumsum (uniquex, 'reverse')); p(i) = ni:-1:1; else l = u(cumsum (! [false; match(:)])); p(i) = 1:ni; endif j = p(j(l(k))); # Replace j with 3rd output mapping y->x. endif endif endif endfunction statistics-release-2.1.2/inst/Model_Evaluation/rocmetrics.m000066400000000000000000001033551527311104300240640ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef rocmetrics ## -*- texinfo -*- ## @deftp {statistics} rocmetrics ## ## Receiver operating characteristic (ROC) metrics for classifier output. ## ## The @code{rocmetrics} class evaluates a classifier's performance by ## computing, for each class, a one-versus-all ROC curve together with a set ## of threshold-dependent performance metrics. It stores the results in the ## @code{Metrics} table and the per-class area under the curve in @code{AUC}, ## and provides the @code{addMetrics}, @code{average}, and @code{plot} ## methods for follow-up analysis. ## ## For a problem with @math{K} classes and scores supplied as an ## @math{N}-by-@math{K} matrix, the discriminant score used for class ## @var{k} is the one-versus-all margin ## @code{Scores(:,k) - max (Scores(:,j))} over @code{j != k}, matching ## MATLAB's @code{rocmetrics}. Every metric is evaluated at each distinct ## value of that margin. ## ## @seealso{perfcurve, confusionmat, confusionchart} ## @end deftp properties (GetAccess = public, SetAccess = private) ## -*- texinfo -*- ## @deftp {rocmetrics} {property} Metrics ## ## Performance metrics table ## ## Table of performance metrics, vertically concatenated across the classes ## in @code{ClassNames} order with one row per distinct threshold. The ## standard variables are @code{ClassName}, @code{Threshold}, ## @code{FalsePositiveRate}, and @code{TruePositiveRate}, followed by one ## variable for each metric requested through @qcode{'AdditionalMetrics'}. ## This property is read-only. ## ## @end deftp Metrics = []; ## -*- texinfo -*- ## @deftp {rocmetrics} {property} AUC ## ## Area under the ROC curve ## ## Row vector holding the area under the one-versus-all ROC curve for each ## class, in @code{ClassNames} order. This property is read-only. ## ## @end deftp AUC = []; ## -*- texinfo -*- ## @deftp {rocmetrics} {property} ClassNames ## ## Class names ## ## Class names for which the ROC metrics are computed, in the column order ## of @code{Scores}. This property is read-only. ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {rocmetrics} {property} Cost ## ## Misclassification cost matrix ## ## Square misclassification-cost matrix, with zero diagonal and unit ## off-diagonal entries by default. It is used by the @code{ExpectedCost} ## metric. This property is read-only. ## ## @end deftp Cost = []; ## -*- texinfo -*- ## @deftp {rocmetrics} {property} Prior ## ## Prior class probabilities ## ## Row vector of prior class probabilities, in @code{ClassNames} order, ## summing to one. This property is read-only. ## ## @end deftp Prior = []; ## -*- texinfo -*- ## @deftp {rocmetrics} {property} Labels ## ## Observation labels ## ## True class labels supplied at construction, one per observation. This ## property is read-only. ## ## @end deftp Labels = []; ## -*- texinfo -*- ## @deftp {rocmetrics} {property} Scores ## ## Classification scores ## ## Classification scores supplied at construction, as an ## @math{N}-by-@math{K} matrix. This property is read-only. ## ## @end deftp Scores = []; ## -*- texinfo -*- ## @deftp {rocmetrics} {property} Weights ## ## Observation weights ## ## Non-negative observation weights, one per observation. Defaults to a ## vector of ones. This property is read-only. ## ## @end deftp Weights = []; endproperties properties (GetAccess = public, SetAccess = private, Hidden) ## Per-class raw curve data retained so that addMetrics can append columns ## without recomputing, and so average/plot can reuse the curves. Each ## element has fields Threshold, tp, fp, tn, fn, P, N, priorPos, cNP, cPN. ClassData_ = struct ([]); ## Names of the additional metrics currently materialised in Metrics. AddMetrics_ = {}; endproperties methods (Hidden) ## Custom display of the object summary. function disp (obj) printf (" rocmetrics with properties:\n\n"); printf (" ClassNames: %d class(es)\n", numel (obj.ClassData_)); printf (" AUC: [%s]\n", ... strtrim (sprintf ("%.4f ", obj.AUC))); printf (" Metrics: [%dx%d table]\n\n", size (obj.Metrics, 1), ... size (obj.Metrics, 2)); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {rocmetrics} {@var{obj} =} rocmetrics (@var{labels}, @var{scores}, @var{classnames}) ## @deftypefnx {rocmetrics} {@var{obj} =} rocmetrics (@dots{}, @var{Name}, @var{Value}) ## ## Create a @code{rocmetrics} object from labels and classification scores. ## ## @var{labels} is a vector of true class labels with one element per ## observation; it may be numeric, logical, a character matrix, or a cell ## array of character vectors. @var{scores} is an @math{N}-by-@math{K} ## numeric matrix of classification scores, where @code{scores(i,k)} is the ## score of observation @var{i} for the class @code{classnames(k)}. ## @var{classnames} lists the @math{K} classes in the column order of ## @var{scores}. ## ## The following @qcode{Name-Value} pairs are supported: ## ## @multitable @columnfractions 0.25 0.75 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'AdditionalMetrics'} @tab A character vector or cell array ## of metric names to append to @code{Metrics}. Supported names are ## @qcode{'TruePositives'}, @qcode{'FalseNegatives'}, ## @qcode{'FalsePositives'}, @qcode{'TrueNegatives'}, ## @qcode{'SumOfTrueAndFalsePositives'}, ## @qcode{'RateOfPositivePredictions'}, ## @qcode{'RateOfNegativePredictions'}, @qcode{'Accuracy'}, ## @qcode{'FalseNegativeRate'}, @qcode{'TrueNegativeRate'}, ## @qcode{'PositivePredictiveValue'}, @qcode{'NegativePredictiveValue'}, ## @qcode{'ExpectedCost'}, and @qcode{'f1score'}. ## ## @item @qcode{'Prior'} @tab Prior class probabilities, given as ## @qcode{'empirical'} (default), @qcode{'uniform'}, or a numeric vector ## with one value per class. ## ## @item @qcode{'Cost'} @tab A @math{K}-by-@math{K} misclassification-cost ## matrix used by the @code{ExpectedCost} metric. The default has zero ## diagonal and unit off-diagonal entries. ## ## @item @qcode{'Weights'} @tab A vector of non-negative observation ## weights. The default is a vector of ones. ## ## @item @qcode{'NaNFlag'} @tab How to treat @code{NaN} scores: ## @qcode{'omitnan'} (default) drops the affected observations, while ## @qcode{'includenan'} treats them as always classified negative. ## ## @item @qcode{'FixedMetricValues'} @tab @qcode{'all'} (default) to use ## every distinct threshold, or a numeric vector of threshold values at ## which to report the curve (nearest actual thresholds are returned). ## @end multitable ## ## Construction from a trained model object, the @qcode{'FixedMetric'} ## grids other than thresholds, and bootstrap confidence intervals are not ## implemented. ## ## @end deftypefn function obj = rocmetrics (labels, scores, classnames, varargin) if (nargin < 3) error ("rocmetrics: too few input arguments."); endif if (isa (labels, 'ClassificationSVM') || isobject (labels)) error (strcat (["rocmetrics: construction from a trained model"], ... [" object is not supported; supply labels, scores,"], ... [" and class names."])); endif ## Validate scores. if (! (isnumeric (scores) && isreal (scores) && ! isempty (scores))) error ("rocmetrics: SCORES must be a real numeric matrix."); endif if (isvector (scores)) scores = scores(:); endif [n, K] = size (scores); ## Validate labels. if (rocmetrics.numelLabels_ (labels) != n) error ("rocmetrics: LABELS must have one element per observation."); endif ## Validate class names against the score columns. cn = rocmetrics.tocol_ (classnames); if (numel (cn) != K) error (strcat (["rocmetrics: CLASSNAMES must have one element per"], ... [" column of SCORES."])); endif ## Defaults and Name-Value parsing. addmetrics = {}; prior = 'empirical'; cost = []; weights = []; nanflag = 'omitnan'; fixedvals = 'all'; if (mod (numel (varargin), 2) != 0) error ("rocmetrics: optional arguments must be Name-Value pairs."); endif for k = 1:2:numel (varargin) name = varargin{k}; if (! (ischar (name) && isrow (name))) error ("rocmetrics: parameter names must be character vectors."); endif value = varargin{k+1}; switch (lower (name)) case 'additionalmetrics' addmetrics = value; case 'prior' prior = value; case 'cost' cost = value; case 'weights' weights = value; case 'nanflag' nanflag = lower (value); case 'fixedmetricvalues' fixedvals = value; case 'fixedmetric' if (! (ischar (value) && strcmpi (value, 'Thresholds'))) error (strcat (["rocmetrics: only the 'Thresholds' value of"], ... [" 'FixedMetric' is supported."])); endif case {'numbootstraps', 'bootstraptype', 'bootstrapoptions'} error (strcat (["rocmetrics: bootstrap confidence intervals are"], ... [" not supported."])); otherwise error ("rocmetrics: unknown parameter name '%s'.", name); endswitch endfor addmetrics = rocmetrics.resolveMetricNames_ (addmetrics); if (! any (strcmpi (nanflag, {'omitnan', 'includenan'}))) error ("rocmetrics: 'NaNFlag' must be 'omitnan' or 'includenan'."); endif ## Weights. if (isempty (weights)) w = ones (n, 1); else if (! (isnumeric (weights) && isreal (weights) && isvector (weights) && numel (weights) == n && all (weights >= 0))) error (strcat (["rocmetrics: 'Weights' must be a non-negative"], ... [" vector with one element per observation."])); endif w = double (weights(:)); endif ## Prior probabilities (in ClassNames order). priorvec = rocmetrics.computePrior_ (prior, labels, cn, w, K); ## Misclassification-cost matrix. cost = rocmetrics.validateCost_ (cost, K); ## Build the per-class curves and assemble the Metrics table. obj.Labels = labels; obj.Scores = scores; obj.Weights = w; obj.ClassNames = classnames; obj.Prior = priorvec; obj.Cost = cost; obj.AddMetrics_ = addmetrics; obj = obj.buildCurves_ (cn, nanflag, fixedvals); obj = obj.assembleTable_ (); endfunction ## -*- texinfo -*- ## @deftypefn {rocmetrics} {@var{obj} =} addMetrics (@var{obj}, @var{metrics}) ## ## Append additional performance metrics to an existing @code{rocmetrics} ## object. ## ## @var{metrics} is a metric name or a cell array of metric names, chosen ## from the list supported by the @qcode{'AdditionalMetrics'} constructor ## argument. The named metrics are appended as new variables of the ## @code{Metrics} table; metrics already present are left unchanged. ## ## @end deftypefn function obj = addMetrics (obj, metrics) if (nargin != 2) error ("rocmetrics.addMetrics: METRICS input is required."); endif newmetrics = rocmetrics.resolveMetricNames_ (metrics); for k = 1:numel (newmetrics) if (! any (strcmp (newmetrics{k}, obj.AddMetrics_))) obj.AddMetrics_{end+1} = newmetrics{k}; endif endfor obj = obj.assembleTable_ (); endfunction ## -*- texinfo -*- ## @deftypefn {rocmetrics} {[@var{FPR}, @var{TPR}, @var{Thresholds}, @var{AUC}] =} average (@var{obj}, @var{type}) ## ## Compute an averaged ROC curve across the classes of a @code{rocmetrics} ## object. ## ## @var{type} selects the averaging method: @qcode{'macro'} (unweighted ## mean of the per-class curves), @qcode{'micro'} (a single curve pooling ## every one-versus-all instance), or @qcode{'weighted'} (mean of the ## per-class curves weighted by @code{Prior}). The function returns the ## averaged false and true positive rates @var{FPR} and @var{TPR}, the ## corresponding @var{Thresholds}, and the area @var{AUC} under the ## averaged curve. ## ## The averaged curve is evaluated on the union of the per-class ## thresholds. MATLAB inserts additional staircase points when building ## the averaged curve, so the exact rows and the averaged @var{AUC} may ## differ slightly from MATLAB. ## ## @end deftypefn function [FPR, TPR, Thresholds, AUC] = average (obj, type) if (nargin < 2) type = 'macro'; endif [FPR, TPR, Thresholds, AUC] = obj.averageCurve_ (lower (type)); endfunction ## -*- texinfo -*- ## @deftypefn {rocmetrics} {} plot (@var{obj}) ## @deftypefnx {rocmetrics} {@var{h} =} plot (@var{obj}) ## ## Plot the per-class ROC curves of a @code{rocmetrics} object. ## ## Each class in @code{ClassNames} contributes one true-positive-rate ## versus false-positive-rate curve. A handle to the line objects is ## returned in @var{h} when requested. ## ## @end deftypefn function varargout = plot (obj) cn = rocmetrics.tocol_ (obj.ClassNames); h = []; newplot (); hold_state = ishold (); hold ("on"); leg = cell (1, numel (obj.ClassData_)); for k = 1:numel (obj.ClassData_) cd = obj.ClassData_(k); h(end+1) = plot (cd.fp / cd.N, cd.tp / cd.P, '-o'); leg{k} = sprintf ("%s (AUC = %.4f)", ... rocmetrics.nameStr_ (cn, k), obj.AUC(k)); endfor plot ([0, 1], [0, 1], 'k:'); if (! hold_state) hold ("off"); endif xlabel ("False Positive Rate"); ylabel ("True Positive Rate"); legend (leg, "location", "southeast"); if (nargout > 0) varargout{1} = h; endif endfunction endmethods methods (Access = private) ## Build the one-versus-all curve data for every class. function obj = buildCurves_ (obj, cn, nanflag, fixedvals) scores = obj.Scores; [n, K] = size (scores); w = obj.Weights; cd = struct ("Threshold", {}, "tp", {}, "fp", {}, "tn", {}, ... "fn", {}, "P", {}, "N", {}, "priorPos", {}, ... "cNP", {}, "cPN", {}); auc = zeros (1, K); for k = 1:K ## One-versus-all margin score. if (K == 1) margin = scores(:,1); else other = max (scores(:,[1:k-1, k+1:K]), [], 2); margin = scores(:,k) - other; endif ## Collapse floating-point noise introduced by the margin so that ## thresholds that are equal in exact arithmetic group together. margin = rocmetrics.roundSig_ (margin, 14); ispos = rocmetrics.labelEq_ (obj.Labels, cn, k); isneg = ! ispos; wk = w; ## NaN score handling. isnanm = isnan (margin); if (any (isnanm)) if (strcmp (nanflag, "omitnan")) keep = ! isnanm; margin = margin(keep); ispos = ispos(keep); isneg = isneg(keep); wk = wk(keep); else margin(isnanm) = -Inf; endif endif if (! any (ispos) || ! any (isneg)) error (strcat (["rocmetrics: class '%s' must have both positive"], ... [" and negative observations."]), ... rocmetrics.nameStr_ (cn, k)); endif ## Prior-adjusted effective weights (verified MATLAB normalisation). pp = obj.Prior(k); pn = 1 - pp; Wpos = sum (wk(ispos)); Wneg = sum (wk(isneg)); S = 1 / (pp / Wpos + pn / Wneg); we = zeros (size (wk)); we(ispos) = wk(ispos) * (pp / Wpos) * S; we(isneg) = wk(isneg) * (pn / Wneg) * S; ## Cumulative counts at each distinct threshold, descending. [ss, ord] = sort (margin, "descend"); pw = cumsum (we(ord) .* ispos(ord)); nw = cumsum (we(ord) .* isneg(ord)); chg = find (diff (ss) != 0); last = [chg; numel(ss)]; tp = [0; pw(last)]; fp = [0; nw(last)]; T = [ss(1); ss(last)]; P = pp * S; N = pn * S; ## Optionally restrict to requested threshold values. if (! (ischar (fixedvals) && strcmpi (fixedvals, "all"))) idx = rocmetrics.selectThresholds_ (T, fixedvals(:)); tp = tp(idx); fp = fp(idx); T = T(idx); endif [cNP, cPN] = rocmetrics.ovaCost_ (obj.Cost, K, k); cd(k).Threshold = T; cd(k).tp = tp; cd(k).fp = fp; cd(k).tn = N - fp; cd(k).fn = P - tp; cd(k).P = P; cd(k).N = N; cd(k).priorPos = pp; cd(k).cNP = cNP; cd(k).cPN = cPN; auc(k) = trapz (fp / N, tp / P); endfor obj.ClassData_ = cd; obj.AUC = auc; endfunction ## Assemble the Metrics table from the per-class curve data. function obj = assembleTable_ (obj) cn = rocmetrics.tocol_ (obj.ClassNames); names = {'ClassName', 'Threshold', 'FalsePositiveRate', ... 'TruePositiveRate', obj.AddMetrics_{:}}; ncol = numel (names); cols = cell (1, ncol); for c = 1:ncol cols{c} = []; endfor classcol = {}; classnum = []; numericnames = isnumeric (cn) || islogical (cn); for k = 1:numel (obj.ClassData_) cd = obj.ClassData_(k); m = numel (cd.Threshold); if (numericnames) classnum = [classnum; repmat(double (cn(k)), m, 1)]; else classcol = [classcol; repmat({rocmetrics.nameStr_(cn, k)}, m, 1)]; endif cols{2} = [cols{2}; cd.Threshold]; cols{3} = [cols{3}; cd.fp / cd.N]; cols{4} = [cols{4}; cd.tp / cd.P]; for c = 5:ncol cols{c} = [cols{c}; rocmetrics.metricValue_(names{c}, cd)]; endfor endfor if (numericnames) cols{1} = classnum; else cols{1} = classcol; endif obj.Metrics = table (cols{:}, 'VariableNames', names); endfunction ## Averaged ROC curve for a given averaging type. function [FPR, TPR, Thresholds, AUC] = averageCurve_ (obj, type) cd = obj.ClassData_; K = numel (cd); if (strcmp (type, "micro")) ## Pool every one-versus-all instance into one binary problem. allthr = []; alltp = []; allfp = []; P = 0; N = 0; for k = 1:K allthr = [allthr; cd(k).Threshold]; P = P + cd(k).P; N = N + cd(k).N; endfor Thresholds = unique (allthr, "sorted"); Thresholds = flipud (Thresholds(:)); tp = zeros (size (Thresholds)); fp = zeros (size (Thresholds)); for k = 1:K tp = tp + rocmetrics.stepAt_ (cd(k).Threshold, cd(k).tp, Thresholds); fp = fp + rocmetrics.stepAt_ (cd(k).Threshold, cd(k).fp, Thresholds); endfor FPR = fp / N; TPR = tp / P; else ## Macro / weighted: mean of the per-class rates on the union grid. if (strcmp (type, "weighted")) wt = obj.Prior(:)'; elseif (strcmp (type, "macro")) wt = ones (1, K) / K; else error ("rocmetrics.average: unknown averaging type '%s'.", type); endif wt = wt / sum (wt); allthr = []; for k = 1:K allthr = [allthr; cd(k).Threshold]; endfor Thresholds = unique (allthr, "sorted"); Thresholds = flipud (Thresholds(:)); FPR = zeros (size (Thresholds)); TPR = zeros (size (Thresholds)); for k = 1:K fk = rocmetrics.stepAt_ (cd(k).Threshold, cd(k).fp / cd(k).N, ... Thresholds); tk = rocmetrics.stepAt_ (cd(k).Threshold, cd(k).tp / cd(k).P, ... Thresholds); FPR = FPR + wt(k) * fk; TPR = TPR + wt(k) * tk; endfor endif AUC = trapz (FPR, TPR); endfunction endmethods methods (Static, Access = private) ## Number of labels in a vector or character matrix. function m = numelLabels_ (labels) if (ischar (labels)) m = rows (labels); else m = numel (labels); endif endfunction ## Column form of a class-name list. function c = tocol_ (classnames) if (ischar (classnames)) c = cellstr (classnames); elseif (iscell (classnames)) c = classnames(:); else c = classnames(:); endif endfunction ## Logical membership of the labels in the k-th class. function tf = labelEq_ (labels, cn, k) if (isnumeric (cn) || islogical (cn)) tf = (labels(:) == cn(k)); else if (ischar (labels)) labels = cellstr (labels); endif tf = strcmp (labels(:), cn{k}); endif endfunction ## Printable name of the k-th class. function s = nameStr_ (cn, k) if (iscell (cn)) s = cn{k}; elseif (ischar (cn)) s = cn(k,:); else s = num2str (cn(k)); endif endfunction ## Prior probabilities in ClassNames order, summing to one. function p = computePrior_ (prior, labels, cn, w, K) if (ischar (prior)) switch (lower (prior)) case 'empirical' p = zeros (1, K); for k = 1:K p(k) = sum (w(rocmetrics.labelEq_ (labels, cn, k))); endfor if (sum (p) == 0) error ("rocmetrics: empirical prior is undefined (no labels)."); endif p = p / sum (p); case 'uniform' p = ones (1, K) / K; otherwise error ("rocmetrics: 'Prior' must be 'empirical', 'uniform', or a vector."); endswitch elseif (isnumeric (prior) && isvector (prior) && numel (prior) == K && all (prior >= 0) && sum (prior) > 0) p = prior(:)' / sum (prior); else error ("rocmetrics: 'Prior' must be 'empirical', 'uniform', or a vector."); endif endfunction ## Validate or default the misclassification-cost matrix. function cost = validateCost_ (cost, K) if (isempty (cost)) cost = ones (K) - eye (K); elseif (! (isnumeric (cost) && isequal (size (cost), [K, K]))) error ("rocmetrics: 'Cost' must be a %d-by-%d matrix.", K, K); endif endfunction ## One-versus-all costs C(N|P) and C(P|N) for the k-th class. function [cNP, cPN] = ovaCost_ (cost, K, k) if (K == 2) other = 3 - k; cNP = cost(k, other); cPN = cost(other, k); else ## For K != 2 only the default identity cost is validated; the ## off-diagonal cost of the k-th class is used symmetrically. off = cost(k, [1:k-1, k+1:K]); cNP = off(1); cPN = off(1); endif endfunction ## Value of a named metric for a class-data record. function v = metricValue_ (name, cd) tp = cd.tp; fp = cd.fp; tn = cd.tn; fn = cd.fn; P = cd.P; N = cd.N; total = P + N; pp = cd.priorPos; pn = 1 - pp; switch (name) case 'TruePositives' v = tp; case 'FalseNegatives' v = fn; case 'FalsePositives' v = fp; case 'TrueNegatives' v = tn; case 'SumOfTrueAndFalsePositives' v = tp + fp; case 'RateOfPositivePredictions' v = (tp + fp) / total; case 'RateOfNegativePredictions' v = (tn + fn) / total; case 'Accuracy' v = (tp + tn) / total; case 'FalseNegativeRate' v = fn / P; case 'TrueNegativeRate' v = tn / N; case 'PositivePredictiveValue' v = tp ./ (tp + fp); case 'NegativePredictiveValue' v = tn ./ (tn + fn); case 'ExpectedCost' v = pp * pn * (pp * (fn / P) * cd.cNP + pn * (fp / N) * cd.cPN); case 'f1score' v = 2 * tp ./ (2 * tp + fp + fn); otherwise error ("rocmetrics: unsupported metric '%s'.", name); endswitch endfunction ## Canonicalise a metric name or list of metric names. function names = resolveMetricNames_ (metrics) if (isempty (metrics)) names = {}; return; endif if (ischar (metrics)) metrics = {metrics}; elseif (! iscell (metrics)) error ("rocmetrics: metric names must be a string or cell array."); endif canon = {'TruePositives', 'FalseNegatives', 'FalsePositives', ... 'TrueNegatives', 'SumOfTrueAndFalsePositives', ... 'RateOfPositivePredictions', 'RateOfNegativePredictions', ... 'Accuracy', 'FalseNegativeRate', 'TrueNegativeRate', ... 'PositivePredictiveValue', 'NegativePredictiveValue', ... 'ExpectedCost', 'f1score'}; names = cell (1, numel (metrics)); for k = 1:numel (metrics) idx = find (strcmpi (metrics{k}, canon)); if (isempty (idx)) error ("rocmetrics: unrecognised metric '%s'.", metrics{k}); endif names{k} = canon{idx}; endfor endfunction ## Indices of the thresholds nearest the requested values. function idx = selectThresholds_ (T, vals) idx = zeros (numel (vals), 1); for k = 1:numel (vals) if (vals(k) >= T(1)) idx(k) = 1; else [~, idx(k)] = min (abs (T - vals(k))); endif endfor endfunction ## Round to SIG significant digits, collapsing floating-point noise while ## preserving genuinely distinct values. function y = roundSig_ (x, sig) y = x; nz = (x != 0) & isfinite (x); e = floor (log10 (abs (x(nz)))); f = 10 .^ (sig - 1 - e); y(nz) = round (x(nz) .* f) ./ f; endfunction ## Step-function value of a curve (thresholds T descending, values Y) at ## the query thresholds TQ, using the operating point for score >= TQ. function yq = stepAt_ (T, Y, TQ) yq = zeros (size (TQ)); for i = 1:numel (TQ) j = find (T >= TQ(i), 1, "last"); if (isempty (j)) yq(i) = Y(1); else yq(i) = Y(j); endif endfor endfunction endmethods endclassdef %!demo %! ## One-versus-all ROC curves for a three-class problem %! labels = [1 1 2 2 3 3]'; %! scores = [0.9 0.05 0.05; 0.6 0.3 0.1; 0.2 0.7 0.1; ... %! 0.1 0.6 0.3; 0.2 0.2 0.6; 0.1 0.3 0.6]; %! rocObj = rocmetrics (labels, scores, [1 2 3]); %! disp (rocObj.AUC) %! plot (rocObj); %!demo %! ## Binary ROC metrics with additional performance metrics %! labels = [1 1 1 1 0 0]'; %! p = [0.9 0.7 0.4 0.3 0.8 0.2]'; %! scores = [1-p p]; %! rocObj = rocmetrics (labels, scores, [0 1], ... %! "AdditionalMetrics", {"Accuracy", "ExpectedCost"}); %! head = rocObj.Metrics(1:4,:); %! disp (head); %!shared labels, scores, cn %! labels = [1 1 2 2 3 3]'; %! scores = [0.9 0.05 0.05; 0.6 0.3 0.1; 0.2 0.7 0.1; ... %! 0.1 0.6 0.3; 0.2 0.2 0.6; 0.1 0.3 0.6]; %! cn = [1 2 3]; %!test # MATLAB parity: default Metrics columns, thresholds, FPR/TPR, AUC %! r = rocmetrics (labels, scores, cn); %! assert_equal (r.Metrics.Properties.VariableNames, ... %! {'ClassName', 'Threshold', 'FalsePositiveRate', 'TruePositiveRate'}); %! assert_equal (double (r.Metrics.ClassName)', ... %! [1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 3], 0); %! assert_equal (r.Metrics.Threshold, ... %! [0.85;0.85;0.3;-0.4;-0.5;0.5;0.5;0.3;-0.3;-0.4;-0.85; ... %! 0.4;0.4;0.3;-0.3;-0.5;-0.6;-0.85], 1e-12); %! assert_equal (r.Metrics.TruePositiveRate, ... %! [0;0.5;1;1;1;0;0.5;1;1;1;1;0;0.5;1;1;1;1;1], 1e-12); %! assert_equal (r.AUC, [1 1 1], 1e-12); %! assert_equal (r.Prior, [1 1 1]/3, 1e-12); %!test # MATLAB parity: one-versus-all margin score sets the thresholds %! r = rocmetrics (labels, scores, cn); %! m1 = r.Metrics(double (r.Metrics.ClassName) == 1, :); %! assert_equal (m1.Threshold, [0.85;0.85;0.3;-0.4;-0.5], 1e-12); %! assert_equal (m1.FalsePositiveRate, [0;0;0;0.25;1], 1e-12); %!test # MATLAB parity: additional count and rate metrics (imbalanced binary) %! y = [1 1 1 1 0 0]'; %! p = [0.9 0.7 0.4 0.3 0.8 0.2]'; %! s = [1-p p]; %! mets = {"TruePositives","FalsePositives","TrueNegatives","FalseNegatives"}; %! r = rocmetrics (y, s, [0 1], "AdditionalMetrics", mets); %! m0 = r.Metrics(double (r.Metrics.ClassName) == 0, :); %! assert_equal (m0.TruePositives, [0;0.5;0.5;0.5;0.5;1;1], 1e-12); %! assert_equal (m0.FalsePositives, [0;0;0.5;1;1.5;1.5;2], 1e-12); %! assert_equal (m0.TrueNegatives, [2;2;1.5;1;0.5;0.5;0], 1e-12); %! assert_equal (m0.FalseNegatives, [1;0.5;0.5;0.5;0.5;0;0], 1e-12); %! assert_equal (r.AUC, [0.625 0.625], 1e-12); %!test # MATLAB parity: ExpectedCost with default and custom cost %! y = [1 1 1 1 0 0]'; %! p = [0.9 0.7 0.4 0.3 0.8 0.2]'; %! s = [1-p p]; %! r = rocmetrics (y, s, [0 1], "AdditionalMetrics", "ExpectedCost"); %! ec0 = r.Metrics.ExpectedCost(double (r.Metrics.ClassName) == 0); %! assert_equal (ec0(1), 2/27, 1e-12); %! assert_equal (ec0(5), 4/27, 1e-12); %! rc = rocmetrics (y, s, [0 1], "Cost", [0 2; 1 0], ... %! "AdditionalMetrics", "ExpectedCost"); %! ecc = rc.Metrics.ExpectedCost(double (rc.Metrics.ClassName) == 0); %! assert_equal (ecc(1), 4/27, 1e-12); %! assert_equal (ecc(5), 5/27, 1e-12); %!test # MATLAB parity: uniform prior reweights the counts %! y = [1 1 1 1 0 0]'; %! p = [0.9 0.7 0.4 0.3 0.8 0.2]'; %! s = [1-p p]; %! r = rocmetrics (y, s, [0 1], "Prior", "uniform", ... %! "AdditionalMetrics", "TruePositives"); %! assert_equal (r.Prior, [0.5 0.5], 1e-12); %! tp0 = r.Metrics.TruePositives(double (r.Metrics.ClassName) == 0); %! assert_equal (max (tp0), 4/3, 1e-12); %!test # explicit weights renormalise the effective counts %! y = [1 1 1 1 0 0]'; %! p = [0.9 0.7 0.4 0.3 0.8 0.2]'; %! s = [1-p p]; %! w = [1 1 1 1 2 2]'; %! r = rocmetrics (y, s, [0 1], "Weights", w, ... %! "AdditionalMetrics", "TruePositives"); %! assert_equal (r.Prior, [0.5 0.5], 1e-12); %! tp0 = r.Metrics.TruePositives(double (r.Metrics.ClassName) == 0); %! assert_equal (max (tp0), 2, 1e-12); %!test # addMetrics appends without recomputing the curve %! r = rocmetrics (labels, scores, cn); %! r = addMetrics (r, "Accuracy"); %! assert_equal (any (strcmp ("Accuracy", ... %! r.Metrics.Properties.VariableNames)), true); %! acc1 = r.Metrics.Accuracy(1); %! assert_equal (acc1, 2/3, 1e-12); %!test # PPV is NaN at the origin, NPV is NaN at the all-positive end %! r = rocmetrics (labels, scores, cn, "AdditionalMetrics", ... %! {"PositivePredictiveValue","NegativePredictiveValue"}); %! m1 = r.Metrics(double (r.Metrics.ClassName) == 1, :); %! assert_equal (isnan (m1.PositivePredictiveValue(1)), true); %! assert_equal (isnan (m1.NegativePredictiveValue(end)), true); %!test # cell-array string labels and names %! y = {"a","a","b","b","c","c"}; %! r = rocmetrics (y, scores, {"a","b","c"}); %! assert_equal (r.AUC, [1 1 1], 1e-12); %! assert_equal (iscellstr (r.Metrics.ClassName), true); %!test # FixedMetricValues selects nearest thresholds %! r = rocmetrics (labels, scores, cn, "FixedMetricValues", [Inf 0.4 -0.5]); %! m1 = r.Metrics(double (r.Metrics.ClassName) == 1, :); %! assert_equal (numel (m1.Threshold), 3); %! assert_equal (m1.Threshold(1), 0.85, 1e-12); %!test # average returns a valid curve for each averaging type %! r = rocmetrics (labels, scores, cn); %! [fpr, tpr, thr, auc] = average (r, "macro"); %! assert_equal (fpr(1), 0, 1e-12); %! assert_equal (tpr(end), 1, 1e-12); %! assert_equal (auc >= 0 && auc <= 1, true); %! [~, ~, ~, aucmi] = average (r, "micro"); %! assert_equal (aucmi >= 0 && aucmi <= 1, true); ## Test input validation %!error rocmetrics ([1 0], [0.4 0.6]) %!error ... %! rocmetrics ([1 0], {1, 2}, [0 1]) %!error ... %! rocmetrics ([1 0 1], [0.4 0.6; 0.5 0.5], [0 1]) %!error ... %! rocmetrics ([1 0], [0.4 0.6; 0.5 0.5], [0 1 2]) %!error ... %! rocmetrics ([1 0], [0.4 0.6; 0.5 0.5], [0 1], "AdditionalMetrics", "foo") %!error ... %! rocmetrics ([1 0], [0.4 0.6; 0.5 0.5], [0 1], "Prior", "bogus") %!error ... %! rocmetrics ([1 0], [0.4 0.6; 0.5 0.5], [0 1], "NumBootstraps", 100) %!error ... %! rocmetrics ([1 0], [0.4 0.6; 0.5 0.5], [0 1], "Zzz", 1) statistics-release-2.1.2/inst/Nearest_Neighbors/000077500000000000000000000000001527311104300216775ustar00rootroot00000000000000statistics-release-2.1.2/inst/Nearest_Neighbors/ExhaustiveSearcher.m000066400000000000000000001514031527311104300256630ustar00rootroot00000000000000## Copyright (C) 2025 Swayam Shah ## Copyright (C) 2025 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef ExhaustiveSearcher ## -*- texinfo -*- ## @deftp {statistics} ExhaustiveSearcher ## ## Exhaustive nearest neighbor searcher ## ## The @code{ExhaustiveSearcher} class implements an exhaustive search ## algorithm for nearest neighbor queries. It stores training data and ## supports various distance metrics along with their parameter values for ## performing an exhaustive search. The exhaustive search algorithm computes ## the distance from each query point to all the points in the training data ## and facilitates a nearest neighbor search using @code{knnsearch} or a ## radius search using @code{rangesearch}. ## ## You can either use the @code{ExhaustiveSearcher} class constructor or the ## @code{createns} function to create an @qcode{ExhaustiveSearcher} object. ## ## @seealso{createns, KDTreeSearcher, hnswSearcher, knnsearch, rangesearch} ## @end deftp properties(SetAccess = private) ## -*- texinfo -*- ## @deftp {ExhaustiveSearcher} {property} X ## ## Point data ## ## Point data, specified as an @math{N*P} numeric matrix where each row ## is an observation and each column is a feature. This property is private ## and cannot be modified after object creation. ## ## Data of class @qcode{single} is stored and searched in single ## precision, any other numeric class is converted to @qcode{double}. ## ## @end deftp X = [] endproperties properties ## -*- texinfo -*- ## @deftp {ExhaustiveSearcher} {property} Distance ## ## Distance metric ## ## Distance metric used for searches, specified as a character vector (e.g., ## @qcode{'euclidean'}, @qcode{'minkowski'}) or a function handle to a ## custom distance function. Default is @qcode{'euclidean'}. Supported ## metrics align with those in @code{pdist2}. ## ## @end deftp Distance = 'euclidean' ## -*- texinfo -*- ## @deftp {ExhaustiveSearcher} {property} DistParameter ## ## Distance parameter ## ## The type and value of the distance parameter depends on the selected ## @qcode{Distance} metric and can be any of the following: ## ## @itemize ## @item For @qcode{'minkowski'}, a positive scalar exponent (default 2); ## @code{Inf} gives the Chebychev distance. ## @item For @qcode{'seuclidean'}, a nonnegative vector of scaling factors ## matching the number of columns in @qcode{X} (default is standard ## deviation of @qcode{X}). ## @item For @qcode{'mahalanobis'}, a positive definite covariance matrix ## matching the dimensions of @qcode{X} (default is @code{cov (@var{X})}). ## @item Empty for other metrics or custom functions. ## @end itemize ## ## @end deftp DistParameter = [] endproperties methods (Hidden) ## Custom display function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ('%s =\n', in_name); endif disp (this); endfunction ## Custom display function disp (this) if (isscalar (this)) fprintf ("\n ExhaustiveSearcher with properties:\n\n"); fprintf ("%+25s: '%s'\n", 'Distance', this.Distance); if (! isempty (this.DistParameter)) if (isscalar (this.DistParameter)) fprintf ("%+25s: %g\n", 'DistParameter', this.DistParameter); elseif (isvector (this.DistParameter)) fprintf ("%+25s: %s\n", 'DistParameter', ... mat2str (this.DistParameter)); else fprintf ("%+25s: [%dx%d %s]\n", 'DistParameter', ... size (this.DistParameter), class (this.DistParameter)); endif else fprintf ("%+25s: []\n", 'DistParameter'); endif fprintf ("%+25s: [%dx%d %s]\n", 'X', size (this.X), class (this.X)); endif endfunction ## Class specific subscripted reference function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); switch (s.type) case '()' error ("ExhaustiveSearcher.subsref: () indexing not supported."); case '{}' error ("ExhaustiveSearcher.subsref: {} indexing not supported."); case '.' if (! ischar (s.subs)) error (strcat ("ExhaustiveSearcher.subsref: property", ... " name must be a character vector.")); endif try out = this.(s.subs); catch error (strcat ("ExhaustiveSearcher.subsref: unrecognized", ... " property: '%s'"), s.subs); end_try_catch endswitch ## Chained references if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction ## Class specific subscripted assignment function this = subsasgn (this, s, val) if (numel (s) > 1) error ("ExhaustiveSearcher.subsasgn: chained subscripts not allowed."); endif switch s.type case '()' error ("ExhaustiveSearcher.subsasgn: () indexing not supported."); case '{}' error ("ExhaustiveSearcher.subsasgn: {} indexing not supported."); case '.' if (! ischar (s.subs)) error (strcat ("ExhaustiveSearcher.subsasgn: property", ... " name must be a character vector.")); endif switch (s.subs) case 'X' error (strcat ("ExhaustiveSearcher.subsasgn: X is", ... " read-only and cannot be modified.")); case 'Distance' vm = {'euclidean', 'minkowski', 'seuclidean', 'mahalanobis', ... 'cityblock', 'manhattan', 'chebychev', 'cosine', ... 'correlation', 'spearman', 'hamming', 'jaccard'}; if (ischar (val)) if (! any (strcmpi (vm, val))) error (strcat ("ExhaustiveSearcher.subsasgn:", ... " unsupported distance metric '%s'."), val); endif this.Distance = val; elseif (isa (val, 'function_handle')) try D = val(this.X(1,:), this.X); catch error (strcat ("ExhaustiveSearcher.subsasgn:", ... " invalid distance function handle.")); end_try_catch if (! isvector (D) || length (D) != rows (this.X)) error (strcat ("ExhaustiveSearcher.subsasgn: custom", ... " distance function output invalid.")); endif this.Distance = val; else error (strcat ("ExhaustiveSearcher.subsasgn: Distance", ... " must be a string or function handle.")); endif case 'DistParameter' if (strcmpi (this.Distance, 'minkowski')) if (! (isscalar (val) && isnumeric (val) && val > 0)) error (strcat ("ExhaustiveSearcher.subsasgn:", ... " DistParameter must be a positive", ... " scalar for minkowski.")); endif elseif (strcmpi (this.Distance, 'seuclidean')) if (! (isvector (val) && isnumeric (val) && all (val >= 0) && all (isfinite (val)) && length (val) == columns (this.X))) error (strcat ("ExhaustiveSearcher.subsasgn:", ... " DistParameter must be a nonnegative", ... " vector matching X columns.")); endif elseif (strcmpi (this.Distance, 'mahalanobis')) if (! (ismatrix (val) && isnumeric (val) && all (isfinite (val(:))) && rows (val) == columns (val) && rows (val) == columns (this.X))) error (strcat ("ExhaustiveSearcher.subsasgn:", ... " DistParameter must be a square", ... " matrix matching X columns.")); endif [~, p] = chol (val); if (p != 0) error (strcat ("ExhaustiveSearcher.subsasgn:", ... " DistParameter must be positive", ... " definite for mahalanobis.")); endif else if (! isempty (val)) error (strcat ("ExhaustiveSearcher.subsasgn:", ... " DistParameter must be empty for this", ... " distance metric.")); endif endif this.DistParameter = val; otherwise error (strcat ("ExhaustiveSearcher.subsasgn:", ... " unrecognized property: '%s'"), s.subs); endswitch endswitch endfunction endmethods methods ## -*- texinfo -*- ## @deftypefn {ExhaustiveSearcher} {@var{obj} =} ExhaustiveSearcher (@var{X}) ## @deftypefnx {ExhaustiveSearcher} {@var{obj} =} ExhaustiveSearcher (@var{X}, @var{name}, @var{value}) ## ## Create an @qcode{ExhaustiveSearcher} object for nearest neighbor ## searches. ## ## @code{@var{obj} = ExhaustiveSearcher (@var{X})} constructs an ## @qcode{ExhaustiveSearcher} object with training data @var{X} using the ## default @qcode{'euclidean'} distance metric. @var{X} must be an ## @math{N*P} numeric matrix, where rows represent observations and columns ## represent features. ## ## @code{@var{obj} = ExhaustiveSearcher (@var{X}, @var{name}, @var{value})} ## allows customization through name-value pairs: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Distance'} @tab Distance metric, specified as a ## character vector (e.g., @qcode{'euclidean'}, @qcode{'minkowski'}) or a ## function handle. Default is @qcode{'euclidean'}. See @code{pdist2} for ## supported metrics. ## ## @item @qcode{'P'} @tab a positive scalar specifying the exponent for ## the Minkowski distance. Valid only when @qcode{'Distance'} is ## @qcode{'minkowski'}. Default is 2. An exponent of @code{Inf} gives ## the Chebychev distance. ## ## @item @qcode{'Scale'} @tab a nonnegative vector with the same number ## of elements as the columns in @var{X} specifying the scale parameter for ## the standardized Euclidean distance. Valid only when ## @qcode{'Distance'} is @qcode{'seuclidean'}. Default is @code{std (X)}. ## ## @item @qcode{'Cov'} @tab a positive definite matrix matching the ## number of columns in @var{X} specifying the covariance matrix for the ## Mahalanobis distance. Valid only when @qcode{'Distance'} is ## @qcode{'mahalanobis'}. Default is @code{cov (X)}. ## @end multitable ## ## @seealso{ExhaustiveSearcher, knnsearch, rangesearch, pdist2} ## ## @qcode{'Distance'}, @qcode{'P'}, @qcode{'Cov'} and @qcode{'Scale'} ## override the searcher's own metric for that call only; the ## @qcode{Distance} and @qcode{DistParameter} properties keep their values, ## as they do in MATLAB. ## @end deftypefn function obj = ExhaustiveSearcher (X, varargin) if (nargin < 1) error ("ExhaustiveSearcher: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error ("ExhaustiveSearcher: Name-Value arguments must be in pairs."); endif if (! (isnumeric (X) && ismatrix (X) && all (isfinite (X)(:)))) error ("ExhaustiveSearcher: X must be a finite numeric matrix."); endif ## Single precision is carried through, but every other class is ## converted up to double, as MATLAB does. Integer data would ## otherwise round each coordinate difference and corrupt distances. if (! isa (X, "single")) X = double (X); endif obj.X = X; ## Default values for optional parameters Distance = 'euclidean'; P = []; S = []; C = []; ## Parse optional parameters while (numel (varargin) > 0) switch (lower (varargin{1})) case 'distance' Distance = varargin{2}; case 'p' P = varargin{2}; case 'scale' S = varargin{2}; case 'cov' C = varargin{2}; otherwise error (strcat ("ExhaustiveSearcher: invalid parameter", ... " name: '%s'."), varargin{1}); endswitch varargin(1:2) = []; endwhile ## Validate and set distance metric valid_metrics = {'euclidean', 'minkowski', 'seuclidean', ... 'mahalanobis', 'cityblock', 'manhattan', ... 'chebychev', 'cosine', 'correlation', ... 'spearman', 'hamming', 'jaccard'}; if (ischar (Distance)) if (! any (strcmpi (valid_metrics, Distance))) error ("ExhaustiveSearcher: unsupported distance metric '%s'.", ... Distance); endif obj.Distance = Distance; elseif (isa (Distance, 'function_handle')) try D = Distance(X(1,:), X); catch error ("ExhaustiveSearcher: invalid distance function handle."); end_try_catch if (! isvector (D) || length (D) != rows (X)) error (strcat ("ExhaustiveSearcher: custom distance", ... " function output invalid.")); endif obj.Distance = Distance; else error (strcat ("ExhaustiveSearcher: Distance must", ... " be a string or function handle.")); endif if (! isempty (P) && ! strcmpi (obj.Distance, 'minkowski')) error ("ExhaustiveSearcher: 'P' applies only to the minkowski metric."); endif if (! isempty (S) && ! strcmpi (obj.Distance, 'seuclidean')) error (strcat ("ExhaustiveSearcher: 'Scale' applies only to the", ... " seuclidean metric.")); endif if (! isempty (C) && ! strcmpi (obj.Distance, 'mahalanobis')) error (strcat ("ExhaustiveSearcher: 'Cov' applies only to the", ... " mahalanobis metric.")); endif ## Set DistParameter based on Distance if (strcmpi (obj.Distance, 'minkowski')) if (isempty (P)) obj.DistParameter = 2; else if (! (isscalar (P) && isnumeric (P) && P > 0)) error ("ExhaustiveSearcher: P must be a positive scalar."); endif obj.DistParameter = P; endif elseif (strcmpi (obj.Distance, 'seuclidean')) if (isempty (S)) obj.DistParameter = std (X, [], 1); else if (! (isvector (S) && isnumeric (S) && all (S >= 0) && ... all (isfinite (S)) && length (S) == columns (X))) error (strcat ("ExhaustiveSearcher: Scale must be a", ... " nonnegative vector matching X columns.")); endif obj.DistParameter = S; endif elseif (strcmpi (obj.Distance, 'mahalanobis')) if (isempty (C)) obj.DistParameter = cov (X); else if (! (ismatrix (C) && isnumeric (C) && all (isfinite (C)(:)) && ... rows (C) == columns (C) && rows (C) == columns (X))) error (strcat ("ExhaustiveSearcher: Cov must be a square", ... " matrix matching X columns.")); endif [~, p] = chol (C); if (p != 0) error ("ExhaustiveSearcher: Cov must be positive definite."); endif obj.DistParameter = C; endif else obj.DistParameter = []; endif endfunction ## -*- texinfo -*- ## @deftypefn {ExhaustiveSearcher} {[@var{idx}, @var{D}] =} knnsearch (@var{obj}, @var{Y}) ## @deftypefnx {ExhaustiveSearcher} {[@var{idx}, @var{D}] =} knnsearch (@var{obj}, @var{Y}, @var{name}, @var{value}) ## ## Find the @math{K} nearest neighbors in the training data to query points. ## ## @code{[@var{idx}, @var{D}] = knnsearch (@var{obj}, @var{Y})} returns the ## indices @var{idx} and distances @var{D} of the nearest neighbor in ## @var{obj.X} to each point in @var{Y}, using the distance metric specified ## in @var{obj.Distance}. ## ## @itemize ## @item @var{obj} is an @qcode{ExhaustiveSearcher} object. ## @item @var{Y} is an @math{M*P} numeric matrix of query points, where ## @math{P} must match the number of columns in @var{obj.X}. ## @end itemize ## ## @var{idx} is always of class @qcode{double}. @var{D} is of class ## @qcode{single} when either @var{obj.X} or @var{Y} is @qcode{single}, ## in which case the distances are computed in single precision, and of ## class @qcode{double} otherwise. ## ## @code{[@var{idx}, @var{D}] = knnsearch (@var{obj}, @var{Y}, @var{name}, ## @var{value})} ## allows additional options via name-value pairs: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'K'} @tab A positive integer specifying the number of ## nearest neighbors to find. Default is 1. A value larger than the ## number of observations in the training data is answered with all of ## them, since there are no more neighbors to return. ## ## @item @qcode{'IncludeTies'} @tab Logical flag indicating whether to ## include all neighbors tied with the @math{K}th smallest distance. Default ## is @qcode{false}. If @qcode{true}, @var{idx} and @var{D} are cell arrays. ## @end multitable ## ## @var{idx} contains the indices of the nearest neighbors in @var{obj.X}. ## @var{D} contains the corresponding distances. ## ## @seealso{ExhaustiveSearcher, rangesearch, pdist2} ## @end deftypefn function [idx, D] = knnsearch (obj, Y, varargin) if (nargin < 2) error ("ExhaustiveSearcher.knnsearch: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error (strcat ("ExhaustiveSearcher.knnsearch:", ... " Name-Value arguments must be in pairs.")); endif if (! (isnumeric (Y) && ismatrix (Y) && all (isfinite (Y)(:)))) error (strcat ("ExhaustiveSearcher.knnsearch: Y", ... " must be a finite numeric matrix.")); endif if (size (obj.X, 2) != size (Y, 2)) error (strcat ("ExhaustiveSearcher.knnsearch: number", ... " of columns in X and Y must match.")); endif K = 1; IncludeTies = false; Dist = []; Pval = []; Cval = []; Sval = []; ## Parse options while (numel (varargin) > 0) switch (lower (varargin{1})) case 'k' K = varargin{2}; if (! (isscalar (K) && isnumeric (K) && K >= 1 && K == fix (K) && isfinite (K))) error (strcat ("ExhaustiveSearcher.knnsearch: K", ... " must be a positive integer.")); endif case 'includeties' IncludeTies = varargin{2}; if (! (islogical (IncludeTies) && isscalar (IncludeTies))) error (strcat ("ExhaustiveSearcher.knnsearch:", ... " IncludeTies must be a logical scalar.")); endif case 'distance' Dist = varargin{2}; case 'p' Pval = varargin{2}; case 'cov' Cval = varargin{2}; case 'scale' Sval = varargin{2}; otherwise error (strcat ("ExhaustiveSearcher.knnsearch:", ... " invalid parameter name: '%s'."), varargin{1}); endswitch varargin(1:2) = []; endwhile ## A metric given here applies to this call only: the searcher's ## own Distance and DistParameter are left untouched, as MATLAB ## leaves them. Distance = obj.Distance; DistParameter = obj.DistParameter; if (! (isempty (Dist) && isempty (Pval) && isempty (Cval) && isempty (Sval))) if (isempty (Dist)) Dist = Distance; endif [Distance, DistParameter] = __resolve_metric__ ( ... "ExhaustiveSearcher.knnsearch", obj.X, Dist, ... Pval, Cval, Sval, {"euclidean", "seuclidean", "mahalanobis", "minkowski", ... "cityblock", "manhattan", "chebychev", "cosine", ... "correlation", "spearman", "hamming", "jaccard"}); endif ## A Minkowski exponent of Inf is the Chebychev distance, its limit, which ## the search computes directly if (strcmpi (Distance, 'minkowski') && DistParameter == Inf) Distance = 'chebychev'; DistParameter = []; endif ## Determine block size based on memory target (128 MB) N = rows (obj.X); ## There are only N points to return, so a larger K is answered with all ## of them. Indexing the sorted list past its end raised an internal ## out-of-bound error instead. K = min (K, N); M = rows (Y); targetBytes = 128 * 1024^2; BlockSize = max (1, floor (targetBytes / (N * 8))); ## Distances are computed in single precision when either the training ## data or the query is single, and in double otherwise. The indices ## are always double. cls = "double"; if (isa (obj.X, "single") || isa (Y, "single")) cls = "single"; endif X = cast (obj.X, cls); Y = cast (Y, cls); ## Initialize outputs if (IncludeTies) idx = cell (M, 1); D = cell (M, 1); elseif (K == 1) idx = zeros (M, 1); D = zeros (M, 1, cls); else idx = zeros (M, K); D = zeros (M, K, cls); endif ## Process Y in blocks ## Where the metric is one the compiled search knows and no ties are ## wanted, the whole search runs there and no block of the distance ## matrix is formed at all. if (! IncludeTies && ischar (Distance) && any (strcmpi (Distance, {'euclidean', 'cityblock', ... 'chebychev', 'minkowski'}))) [idx, D] = __knnbrute__ (obj.X, Y, K, lower (Distance), DistParameter); return; endif for blk_start = 1:BlockSize:M blk_end = min (blk_start + BlockSize - 1, M); Y_blk = Y(blk_start:blk_end, :); blk_rows = blk_end - blk_start + 1; ## Compute distance matrix for this block if (ischar (Distance)) D_blk = pdist2 (Y_blk, X, Distance, DistParameter); else D_blk = pdist2 (X, Y_blk, Distance, DistParameter); D_blk = reshape (D_blk', blk_rows, N); endif ## Process block results if (K == 1 && ! IncludeTies) [D(blk_start:blk_end), idx(blk_start:blk_end)] = min (D_blk, [], 2); else if (IncludeTies) [sorted_D, sorted_idx] = sort (D_blk, 2); for i = 1:blk_rows if (K > columns (sorted_D)) kth_dist = sorted_D(i, end); else kth_dist = sorted_D(i, K); endif tie_idx = find (D_blk(i, :) <= kth_dist); [D{blk_start + i - 1}, order] = sort (D_blk(i, tie_idx)); idx{blk_start + i - 1} = tie_idx(order); endfor else ## Partial selection, not a full sort of every row. [bi, bd] = __knnselect__ (D_blk, K); idx(blk_start:blk_end, :) = bi; D(blk_start:blk_end, :) = bd; endif endif endfor endfunction ## -*- texinfo -*- ## @deftypefn {ExhaustiveSearcher} {[@var{idx}, @var{D}] =} rangesearch (@var{obj}, @var{Y}, @var{r}) ## @deftypefnx {ExhaustiveSearcher} {[@var{idx}, @var{D}] =} rangesearch (@var{obj}, @var{Y}, @var{r}, @var{name}, @var{value}) ## ## Find all neighbors within a specified radius of query points. ## ## @code{[@var{idx}, @var{D}] = rangesearch (@var{obj}, @var{Y}, @var{r})} ## returns the indices @var{idx} and distances @var{D} of all points in ## @var{obj.X} within radius @var{r} of each point in @var{Y}, using the ## distance metric specified in @var{obj.Distance}. ## ## @itemize ## @item @var{obj} is an @qcode{ExhaustiveSearcher} object. ## @item @var{Y} is an @math{M*P} numeric matrix of query points, where ## @math{P} must match the number of columns in @var{obj.X}. ## @item @var{r} is a nonnegative scalar specifying the search radius. ## @end itemize ## ## @var{idx} is always of class @qcode{double}. @var{D} is of class ## @qcode{single} when either @var{obj.X} or @var{Y} is @qcode{single}, ## in which case the distances are computed in single precision, and of ## class @qcode{double} otherwise. ## ## @code{[@var{idx}, @var{D}] = rangesearch (@dots{}, @var{name}, ## @var{value})} allows additional options via name-value pairs: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'SortIndices'} @tab Logical flag indicating whether to ## sort the indices by distance. Default is @qcode{true}. ## @end multitable ## ## @var{idx} and @var{D} are cell arrays where each cell contains the ## indices and distances for one query point in @var{Y}. ## ## @seealso{ExhaustiveSearcher, knnsearch, pdist2} ## @end deftypefn function [idx, D] = rangesearch (obj, Y, r, varargin) if (nargin < 3) error ("ExhaustiveSearcher.rangesearch: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error (strcat ("ExhaustiveSearcher.rangesearch:", ... " Name-Value arguments must be in pairs.")); endif if (! (isnumeric (Y) && ismatrix (Y) && all (isfinite (Y)(:)))) error (strcat ("ExhaustiveSearcher.rangesearch: Y", ... " must be a finite numeric matrix.")); endif if (size (obj.X, 2) != size (Y, 2)) error (strcat ("ExhaustiveSearcher.rangesearch: number", ... " of columns in X and Y must match.")); endif if (! (isscalar (r) && isnumeric (r) && r >= 0 && isfinite (r))) error (strcat ("ExhaustiveSearcher.rangesearch: R", ... " must be a nonnegative finite scalar.")); endif ## Parse options SortIndices = true; Dist = []; Pval = []; Cval = []; Sval = []; while (numel (varargin) > 0) switch (lower (varargin{1})) case 'sortindices' SortIndices = varargin{2}; if (! (islogical (SortIndices) && isscalar (SortIndices))) error (strcat ("ExhaustiveSearcher.rangesearch:", ... " SortIndices must be a logical scalar.")); endif case 'distance' Dist = varargin{2}; case 'p' Pval = varargin{2}; case 'cov' Cval = varargin{2}; case 'scale' Sval = varargin{2}; otherwise error (strcat ("ExhaustiveSearcher.rangesearch: invalid", ... " parameter name: '%s'."), varargin{1}); endswitch varargin(1:2) = []; endwhile ## A metric given here applies to this call only: the searcher's ## own Distance and DistParameter are left untouched, as MATLAB ## leaves them. Distance = obj.Distance; DistParameter = obj.DistParameter; if (! (isempty (Dist) && isempty (Pval) && isempty (Cval) && isempty (Sval))) if (isempty (Dist)) Dist = Distance; endif [Distance, DistParameter] = __resolve_metric__ ( ... "ExhaustiveSearcher.rangesearch", obj.X, Dist, ... Pval, Cval, Sval, {"euclidean", "seuclidean", "mahalanobis", "minkowski", ... "cityblock", "manhattan", "chebychev", "cosine", ... "correlation", "spearman", "hamming", "jaccard"}); endif ## A Minkowski exponent of Inf is the Chebychev distance, its limit, which ## the search computes directly if (strcmpi (Distance, 'minkowski') && DistParameter == Inf) Distance = 'chebychev'; DistParameter = []; endif ## Determine block size based on memory target (128 MB) N = rows (obj.X); M = rows (Y); targetBytes = 128 * 1024^2; BlockSize = max (1, floor (targetBytes / (N * 8))); ## Distances are computed in single precision when either the training ## data or the query is single, and in double otherwise. The indices ## are always double. cls = "double"; if (isa (obj.X, "single") || isa (Y, "single")) cls = "single"; endif X = cast (obj.X, cls); Y = cast (Y, cls); ## Initialize outputs idx = cell (M, 1); D = cell (M, 1); ## Process Y in blocks for blk_start = 1:BlockSize:M blk_end = min (blk_start + BlockSize - 1, M); Y_blk = Y(blk_start:blk_end, :); blk_rows = blk_end - blk_start + 1; ## Compute distance matrix for this block if (ischar (Distance)) D_blk = pdist2 (Y_blk, X, Distance, DistParameter); else D_blk = pdist2 (X, Y_blk, Distance, DistParameter); D_blk = reshape (D_blk', blk_rows, N); endif ## Process block results for i = 1:blk_rows within_r = find (D_blk(i, :) <= r); if (SortIndices) [sorted_D, sort_idx] = sort (D_blk(i, within_r)); idx{blk_start + i - 1} = within_r(sort_idx); D{blk_start + i - 1} = sorted_D; else idx{blk_start + i - 1} = within_r; D{blk_start + i - 1} = D_blk(i, within_r); endif endfor endfor endfunction endmethods endclassdef ## Demo Examples %!demo %! ## Demo to verify implementation using fisheriris dataset %! load fisheriris %! numSamples = size (meas, 1); %! queryIndices = [20, 95, 123, 136, 138]; %! dataPoints = meas(! ismember (1:numSamples, queryIndices), :); %! queryPoints = meas(queryIndices, :); %! searchModel = ExhaustiveSearcher (dataPoints, 'Distance', 'mahalanobis') %! mahalanobisParam = searchModel.DistParameter %! searchRadius = 3; %! nearestNeighbors = knnsearch (searchModel, queryPoints, 'K', 2) %! neighborsInRange = rangesearch (searchModel, queryPoints, searchRadius) %!demo %! ## Create an ExhaustiveSearcher with Euclidean distance %! X = [1, 2; 3, 4; 5, 6]; %! obj = ExhaustiveSearcher (X); %! ## Find the nearest neighbor to [2, 3] %! Y = [2, 3]; %! [idx, D] = knnsearch (obj, Y); %! disp ('Nearest neighbor index:'); disp (idx); %! disp ('Distance:'); disp (D); %! ## Find all points within radius 2 %! [idx, D] = rangesearch (obj, Y, 2); %! disp ('Indices within radius:'); disp (idx); %! disp ('Distances:'); disp (D); %!demo %! ## Create an ExhaustiveSearcher with Minkowski distance (P=1) %! X = [0, 0; 1, 0; 0, 1]; %! obj = ExhaustiveSearcher (X, 'Distance', 'minkowski', 'P', 1); %! ## Find the 2 nearest neighbors to [0.5, 0.5] %! Y = [0.5, 0.5]; %! [idx, D] = knnsearch (obj, Y, 'K', 2); %! disp ('Nearest neighbor indices:'); disp (idx); %! disp ('Distances:'); disp (D); %!demo %! rng (42); %! disp ('Demonstrating ExhaustiveSearcher'); %! %! n = 100; %! mu1 = [0.3, 0.3]; %! mu2 = [0.7, 0.7]; %! sigma = 0.1; %! X1 = mu1 + sigma * randn (n/2, 2); %! X2 = mu2 + sigma * randn (n/2, 2); %! X = [X1; X2]; %! %! obj = ExhaustiveSearcher (X); %! %! Y = [0.3, 0.3; 0.7, 0.7; 0.5, 0.5]; %! %! K = 5; %! [idx, D] = knnsearch (obj, Y, 'K', K); %! %! disp ('For the first query point:'); %! disp (['Query point: ', num2str(Y(1,:))]); %! disp ('Indices of nearest neighbors:'); %! disp (idx(1,:)); %! disp ('Distances:'); %! disp (D(1,:)); %! %! figure; %! scatter (X(:,1), X(:,2), 36, 'b', 'filled'); % Training points %! hold on; %! scatter (Y(:,1), Y(:,2), 36, 'r', 'filled'); % Query points %! for i = 1:size (Y,1) %! query = Y(i,:); %! neighbors = X(idx(i,:), :); %! for j = 1:K %! plot ([query(1), neighbors(j,1)], [query(2), neighbors(j,2)], 'k-'); %! endfor %! endfor %! hold off; %! title ('K Nearest Neighbors with ExhaustiveSearcher'); %! xlabel ('X1'); %! ylabel ('X2'); %! %! r = 0.15; %! [idx, D] = rangesearch (obj, Y, r); %! %! disp ('For the first query point in rangesearch:'); %! disp (['Query point: ', num2str(Y(1,:))]); %! disp ('Indices of points within radius:'); %! disp (idx{1}); %! disp ('Distances:'); %! disp (D{1}); %! %! figure; %! scatter (X(:,1), X(:,2), 36, 'b', 'filled'); %! hold on; %! scatter (Y(:,1), Y(:,2), 36, 'r', 'filled'); %! theta = linspace (0, 2*pi, 100); %! for i = 1:size (Y,1) %! center = Y(i,:); %! x_circle = center(1) + r * cos (theta); %! y_circle = center(2) + r * sin (theta); %! plot (x_circle, y_circle, 'g-'); %! % Highlight points within radius %! if ! isempty (idx{i}) %! in_radius = X(idx{i}, :); %! scatter (in_radius(:,1), in_radius(:,2), 36, 'g', 'filled'); %! endif %! endfor %! hold off; %! title ('Points within Radius with ExhaustiveSearcher'); %! xlabel ('X1'); %! ylabel ('X2'); ## Test Cases %!test %! ## Basic constructor with default Euclidean %! X = [1, 2; 3, 4; 5, 6]; %! obj = ExhaustiveSearcher (X); %! assert_equal (obj.X, X) %! assert_equal (obj.Distance, "euclidean") %! assert_equal (isempty (obj.DistParameter), true) %!test %! ## Minkowski distance with custom P %! X = [1, 2; 3, 4]; %! obj = ExhaustiveSearcher (X, 'Distance', 'minkowski', 'P', 3); %! assert_equal (obj.Distance, "minkowski") %! assert_equal (obj.DistParameter, 3) %!test %! ## Seuclidean distance with custom Scale %! X = [1, 2; 3, 4; 5, 6]; %! S = [1, 2]; %! obj = ExhaustiveSearcher (X, 'Distance', 'seuclidean', 'Scale', S); %! assert_equal (obj.Distance, "seuclidean") %! assert_equal (obj.DistParameter, S) %!test %! ## Mahalanobis distance with custom Cov %! X = [1, 2; 3, 4; 5, 6]; %! C = [1, 0; 0, 1]; %! obj = ExhaustiveSearcher (X, 'Distance', 'mahalanobis', 'Cov', C); %! assert_equal (obj.Distance, "mahalanobis") %! assert_equal (obj.DistParameter, C) %!test %! ## knnsearch with Euclidean distance %! X = [1, 2; 3, 4; 5, 6]; %! obj = ExhaustiveSearcher (X); %! Y = [2, 3]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (idx, 1) %! assert_equal (D, sqrt (2), 1e-10) %!test %! ## knnsearch with Cityblock distance %! X = [0, 0; 1, 1; 2, 2]; %! obj = ExhaustiveSearcher (X, 'Distance', 'cityblock'); %! Y = [1, 0]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (idx, 1) %! assert_equal (D, 1, 1e-10) %!test %! ## knnsearch with Chebychev distance %! X = [1, 1; 2, 3; 4, 2]; %! obj = ExhaustiveSearcher (X, 'Distance', 'chebychev'); %! Y = [2, 2]; %! [idx, D] = knnsearch (obj, Y); %! assert_equal (idx, 1) %! assert_equal (D, 1, 1e-10) %!test %! ## knnsearch with Cosine distance %! X = [1, 0; 0, 1; 1, 1]; %! obj = ExhaustiveSearcher (X, 'Distance', 'cosine'); %! Y = [1, 0.5]; %! [idx, D] = knnsearch (obj, Y); %! assert_equal (idx, 3) %! assert_equal (D < 0.1, true) %!test %! ## knnsearch with Minkowski P=1 (Manhattan) %! X = [0, 0; 1, 0; 0, 1]; %! obj = ExhaustiveSearcher (X, 'Distance', 'minkowski', 'P', 1); %! Y = [0.5, 0.5]; %! [idx, D] = knnsearch (obj, Y, 'K', 2, 'IncludeTies', true); %! assert_equal (iscell (idx), true) %! assert_equal (idx{1}, [1, 2, 3]) %! assert_equal (D{1}, [1, 1, 1], 1e-10) %!test %! ## rangesearch with Seuclidean %! X = [1, 1; 2, 2; 3, 3]; %! S = [1, 1]; %! obj = ExhaustiveSearcher (X, 'Distance', 'seuclidean', 'Scale', S); %! Y = [0, 0]; %! [idx, D] = rangesearch (obj, Y, 2); %! assert_equal (idx{1}, [1]) %! assert_equal (D{1}, [sqrt(2)], 1e-10) %!test %! ## rangesearch with Mahalanobis %! X = [1, 1; 2, 2; 3, 3]; %! C = [1, 0; 0, 1]; %! obj = ExhaustiveSearcher (X, 'Distance', 'mahalanobis', 'Cov', C); %! Y = [0, 0]; %! [idx, D] = rangesearch (obj, Y, 3, 'SortIndices', false); %! assert_equal (idx{1}, [1, 2]) %! assert_equal (D{1}, [sqrt(2), sqrt(8)], 1e-10) %!test %! ## rangesearch with Hamming distance %! X = [0, 1; 1, 0; 1, 1]; %! obj = ExhaustiveSearcher (X, 'Distance', 'hamming'); %! Y = [0, 0]; %! [idx, D] = rangesearch (obj, Y, 0.5); %! assert_equal (idx{1}, [1, 2]) %! assert_equal (D{1}, [0.5, 0.5], 1e-10) %!test %! ## Custom distance function %! X = [1, 2; 3, 4]; %! custom_dist = @(x, y) sum (abs (x - y)); %! obj = ExhaustiveSearcher (X, 'Distance', custom_dist); %! Y = [2, 3]; %! [idx, D] = knnsearch (obj, Y); %! assert_equal (idx, 1) %! assert_equal (D, 2, 1e-10) %!test %! ## IncludeTies returns all tied neighbors %! X = [0; 1; 2]; %! obj = ExhaustiveSearcher (X); %! Y = 1; %! [idx, D] = knnsearch (obj, Y, 'K', 2, 'IncludeTies', true); %! assert_equal (idx{1}, [2, 1, 3]) %! assert_equal (D{1}, [0, 1, 1]) %!test %! ## Custom distance function with vectorized output %! X = [1, 2; 3, 4]; %! f = @(x, y) sum (abs (x - y), 2); %! obj = ExhaustiveSearcher (X, 'Distance', f); %! Y = [2, 3]; %! [idx, D] = knnsearch (obj, Y); %! assert_equal (idx, 1) %! assert_equal (D, 2) %!test %! ## Euclidean with high-dimensional data %! X = [1, 2, 3; 4, 5, 6; 7, 8, 9; 10, 11, 12]; %! obj = ExhaustiveSearcher (X); %! Y = [5, 6, 7]; %! [idx, D] = knnsearch (obj, Y); %! assert_equal (idx, 2) %! assert_equal (D, sqrt (3), 1e-10) %!test %! ## Minkowski P=3 with scaled data %! X = [0, 1; 2, 3; 4, 5] * 10; %! obj = ExhaustiveSearcher (X, 'Distance', 'minkowski', 'P', 3); %! Y = [20, 30]; %! [idx, D] = knnsearch (obj, Y); %! assert_equal (idx, 2) %! assert_equal (D, 0, 1e-10) %!test %! ## Seuclidean with custom scales on diverse data %! X = [1, 10; 2, 20; 3, 30]; %! S = [1, 5]; %! obj = ExhaustiveSearcher (X, 'Distance', 'seuclidean', 'Scale', S); %! Y = [1.5, 15]; %! [idx, D] = knnsearch (obj, Y); %! assert_equal (idx, 1) %! assert_equal (D, sqrt ((0.5/1)^2 + (5/5)^2), 1e-10) %!test %! ## Mahalanobis with correlated data %! X = [1, 1; 2, 1.5; 3, 2]; %! C = [1, 0.5; 0.5, 1]; %! obj = ExhaustiveSearcher (X, 'Distance', 'mahalanobis', 'Cov', C); %! Y = [2, 1.5]; %! [idx, D] = knnsearch (obj, Y); %! assert_equal (idx, 2) %! assert_equal (D, 0, 1e-10) %!test %! ## Cityblock with sparse data %! X = [0, 0, 1; 1, 0, 0; 0, 1, 0]; %! obj = ExhaustiveSearcher (X, 'Distance', 'cityblock'); %! Y = [0, 0, 0]; %! [idx, D] = rangesearch (obj, Y, 1); %! assert_equal (idx{1}, [1, 2, 3]) %! assert_equal (D{1}, [1, 1, 1], 1e-10) %!test %! ## Chebychev with extreme values %! X = [0, 100; 50, 50; 100, 0]; %! obj = ExhaustiveSearcher (X, 'Distance', 'chebychev'); %! Y = [60, 60]; %! [idx, D] = knnsearch (obj, Y); %! assert_equal (idx, 2) %! assert_equal (D, 10, 1e-10) %!test %! ## Cosine with normalized data %! X = [1, 0; 0, 1; 1/sqrt(2), 1/sqrt(2)]; %! obj = ExhaustiveSearcher (X, 'Distance', 'cosine'); %! Y = [1, 1]; %! [idx, D] = knnsearch (obj, Y); %! assert_equal (idx, 3) %! assert_equal (D < 0.1, true) %!test %! ## Correlation with time-series-like data %! X = [1, 2, 3; 2, 4, 6; 1, 1, 1]; %! obj = ExhaustiveSearcher (X, 'Distance', 'correlation'); %! Y = [1.5, 3, 4.5]; %! [idx, D] = knnsearch (obj, Y); %! assert_equal (idx, 1) %! assert_equal (D < 0.1, true) %!test %! ## Spearman with ranked data %! X = [1, 2, 3; 3, 2, 1; 2, 1, 3]; %! obj = ExhaustiveSearcher (X, 'Distance', 'spearman'); %! Y = [1, 2, 3]; %! [idx, D] = knnsearch (obj, Y); %! assert_equal (idx, 1) %! assert_equal (D, 0, 1e-10) %!test %! ## Jaccard with binary sparse data %! X = [1, 0, 0; 0, 1, 0; 1, 1, 0]; %! obj = ExhaustiveSearcher (X, 'Distance', 'jaccard'); %! Y = [1, 0, 0]; %! [idx, D] = knnsearch (obj, Y); %! assert_equal (idx, 1) %! assert_equal (D, 0, 1e-10) %!test %! obj = ExhaustiveSearcher (ones (3,2)); %! assert_equal (obj.X, ones (3,2)) %! assert_equal (obj.Distance, "euclidean") %! assert_equal (isempty (obj.DistParameter), true) %!test %! obj = ExhaustiveSearcher (ones (3,2)); %! obj.Distance = 'minkowski'; %! assert_equal (obj.Distance, "minkowski") %!test %! obj = ExhaustiveSearcher (ones (3,2), 'Distance', 'minkowski'); %! obj.DistParameter = 3; %! assert_equal (obj.DistParameter, 3) %!test %! obj = ExhaustiveSearcher (ones (3,2), 'Distance', 'seuclidean'); %! obj.DistParameter = [1, 2]; %! assert_equal (obj.DistParameter, [1, 2]) %!test %! obj = ExhaustiveSearcher (ones (3,2), 'Distance', 'mahalanobis'); %! obj.DistParameter = eye (2); %! assert_equal (obj.DistParameter, eye (2)) ## Test Input Validation %!test %! ## A metric given per call overrides the searcher's own for that call, %! ## matching a searcher built with it, and leaves the object unchanged. %! X = [1, 1; 2, 2; 3, 3; 4, 4; 5, 5; 1, 5; 5, 1]; Y = [2, 2; 4, 4]; %! o = ExhaustiveSearcher (X); %! assert_equal (knnsearch (o, Y, 'K', 3, 'Distance', 'cityblock'), ... %! knnsearch (ExhaustiveSearcher (X, 'Distance', 'cityblock'), ... %! Y, 'K', 3)); %! assert_equal (rangesearch (o, Y, 3, 'Distance', 'cityblock'), ... %! rangesearch (ExhaustiveSearcher (X, 'Distance', ... %! 'cityblock'), Y, 3)); %! assert_equal (knnsearch (o, Y, 'K', 2, 'Distance', 'minkowski', 'P', 3), ... %! knnsearch (ExhaustiveSearcher (X, 'Distance', 'minkowski', ... %! 'P', 3), Y, 'K', 2)); %! ## the searcher keeps its own metric %! assert_equal (o.Distance, 'euclidean'); %! assert_equal (isempty (o.DistParameter), true); ## A Minkowski exponent of Inf is the Chebychev distance, as in MATLAB R2024a, ## and the searcher keeps reporting the metric and exponent it was given. %!test %! X = [1, 2; 4, 6; 0, 0; 3, 1; 5, 5; 2, 3]; %! obj = ExhaustiveSearcher (X, 'Distance', 'minkowski', 'P', Inf); %! [idx, D] = knnsearch (obj, [1, 1; 4, 4], 'K', 2); %! assert_equal (obj.Distance, 'minkowski'); %! assert_equal (obj.DistParameter, Inf); %! assert_equal (idx, [1, 3; 5, 2]); %! assert_equal (D, [1, 1; 1, 2]); %!test %! X = [1, 2; 4, 6; 0, 0; 3, 1; 5, 5; 2, 3]; %! obj = ExhaustiveSearcher (X); %! [idx, D] = knnsearch (obj, [1, 1; 4, 4], 'K', 2, 'Distance', ... %! 'minkowski', 'P', Inf); %! assert_equal (idx, [1, 3; 5, 2]); %! assert_equal (D, [1, 1; 1, 2]); %!test %! X = [1, 2; 4, 6; 0, 0; 3, 1; 5, 5; 2, 3]; %! obj = ExhaustiveSearcher (X, 'Distance', 'minkowski'); %! obj.DistParameter = Inf; %! [idx, D] = rangesearch (obj, [1, 1; 4, 4], 2); %! assert_equal (idx, {[1, 3, 4, 6]; [5, 2, 6]}); %! assert_equal (D, {[1, 1, 2, 2]; [1, 2, 2]}); %!error ... %! knnsearch (ExhaustiveSearcher ([1, 1; 2, 2]), [1, 1], 'K', 1, ... %! 'Distance', 'cityblock', 'P', 3) %!error ... %! knnsearch (ExhaustiveSearcher ([1, 1; 2, 2]), [1, 1], 'K', 1, ... %! 'Distance', 'bogus') %!error ... %! ExhaustiveSearcher () %!error ... %! ExhaustiveSearcher (ones (3,2), 'Distance') %!error ... %! ExhaustiveSearcher ('abc') %!error ... %! ExhaustiveSearcher ([1; Inf; 3]) %!error ... %! ExhaustiveSearcher (ones (3,2), 'foo', 'bar') %!error ... %! ExhaustiveSearcher (ones (3,2), 'Distance', 'invalid') %!error ... %! ExhaustiveSearcher (ones (3,2), 'Distance', @(x) x) %!error ... %! ExhaustiveSearcher (ones (3,2), 'Distance', 1) %!error ... %! ExhaustiveSearcher (ones (3,2), 'Distance', 'minkowski', 'P', -1) %!error ... %! ExhaustiveSearcher (ones (3,2), 'P', 3) %!error ... %! ExhaustiveSearcher (ones (3,2), 'Scale', [1, 1]) %!error ... %! ExhaustiveSearcher (ones (3,2), 'Distance', 'mahalanobis', 'Scale', [1, 1]) %!error ... %! ExhaustiveSearcher (ones (3,2), 'Cov', eye (2)) %!error ... %! ExhaustiveSearcher (ones (3,2), 'Distance', 'seuclidean', 'Cov', eye (2)) %!error ... %! ExhaustiveSearcher (ones (3,2), 'Distance', 'seuclidean', 'P', 3) %!error ... %! ExhaustiveSearcher (ones (3,2), 'Distance', @(x, Z) sum (abs (x - Z), 2), ... %! 'P', 3) %!error ... %! ExhaustiveSearcher (ones (3,2), 'Distance', 'seuclidean', 'Scale', [-1, 1]) %!error ... %! ExhaustiveSearcher (ones (3,2), 'Distance', 'mahalanobis', 'Cov', ones (3,3)) %!error ... %! ExhaustiveSearcher (ones (3,2), 'Distance', 'mahalanobis', 'Cov', -eye (2)) %!error ... %! knnsearch (ExhaustiveSearcher (ones (3,2))) %!error ... %! knnsearch (ExhaustiveSearcher (ones (3,2)), ones (3,2), 'IncludeTies') %!error ... %! knnsearch (ExhaustiveSearcher (ones (3,2)), 'abc') %!error ... %! knnsearch (ExhaustiveSearcher (ones (3,2)), ones (3,3)) %!error ... %! knnsearch (ExhaustiveSearcher (ones (3,2)), ones (3,2), 'K', 0) %!error ... %! knnsearch (ExhaustiveSearcher (ones (3,2)), ones (3,2), 'foo', 'bar') %!error ... %! knnsearch (ExhaustiveSearcher (ones (3,2)), ones (3,2), 'IncludeTies', 1) %!error ... %! rangesearch (ExhaustiveSearcher (ones (3,2))) %!error ... %! rangesearch (ExhaustiveSearcher (ones (3,2)), ones (3,2), 1, 'SortIndices') %!error ... %! rangesearch (ExhaustiveSearcher (ones (3,2)), 'abc', 1) %!error ... %! rangesearch (ExhaustiveSearcher (ones (3,2)), ones (3,3), 1) %!error ... %! rangesearch (ExhaustiveSearcher (ones (3,2)), ones (3,2), -1) %!error ... %! rangesearch (ExhaustiveSearcher (ones (3,2)), ones (3,2), 1, 'foo', 'bar') %!error ... %! rangesearch (ExhaustiveSearcher (ones (3,2)), ones (3,2), 1, 'SortIndices', 1) %!error ... %! obj = ExhaustiveSearcher (ones (3,2)); obj(1) %!error ... %! obj = ExhaustiveSearcher (ones (3,2)); obj{1} %!error ... %! obj = ExhaustiveSearcher (ones (3,2)); obj.(1) %!error ... %! obj = ExhaustiveSearcher (ones (3,2)); obj.invalid %!error ... %! obj = ExhaustiveSearcher (ones (3,2)); obj(1) = 1 %!error ... %! obj = ExhaustiveSearcher (ones (3,2)); obj{1} = 1 %!error ... %! obj = ExhaustiveSearcher (ones (3,2)); obj.X.Y = 1 %!error ... %! obj = ExhaustiveSearcher (ones (3,2)); obj.(1) = 1 %!error ... %! obj = ExhaustiveSearcher (ones (3,2)); obj.X = 1 %!error ... %! obj = ExhaustiveSearcher (ones (3,2)); obj.Distance = 'invalid' %!error ... %! obj = ExhaustiveSearcher (ones (3,2)); obj.Distance = @(x) x %!error ... %! obj = ExhaustiveSearcher (ones (3,2)); obj.Distance = @(x, y) [1; 1] %!error ... %! obj = ExhaustiveSearcher (ones (3,2)); obj.Distance = 1 %!error ... %! obj = ExhaustiveSearcher (ones (3,2), 'Distance', 'minkowski'); obj.DistParameter = -1 %!error ... %! obj = ExhaustiveSearcher (ones (3,2), 'Distance', 'seuclidean'); obj.DistParameter = [-1, 1] %!error ... %! obj = ExhaustiveSearcher (ones (3,2), 'Distance', 'mahalanobis'); obj.DistParameter = ones (3,3) %!error ... %! obj = ExhaustiveSearcher (ones (3,2), 'Distance', 'mahalanobis'); obj.DistParameter = -eye (2) %!error ... %! obj = ExhaustiveSearcher (ones (3,2), 'Distance', 'euclidean'); obj.DistParameter = 1 %!error ... %! obj = ExhaustiveSearcher (ones (3,2)); obj.invalid = 1 ## More neighbours than there are points is answered with all of them, where ## indexing the sorted list past its end used to raise an out-of-bound error. %!test %! randn ("seed", 4); %! X = randn (20, 2); Y = randn (5, 2); %! ex = ExhaustiveSearcher (X); %! for K = [20, 21, 100] %! [idx, D] = knnsearch (ex, Y, "K", K); %! assert_equal (size (idx), [5, 20]); %! assert_equal (size (D), [5, 20]); %! assert_equal (any (isnan (idx(:))), false); %! assert_equal (all (diff (D, 1, 2)(:) >= -1e-12), true); %! endfor ## Single precision is carried through to the distances, every other class is ## converted up to double, and the indices are always double. %!test %! X = [1, 2; 3, 4; 5, 6; 7, 8]; Y = [2, 3; 6, 7]; %! obj = ExhaustiveSearcher (single (X)); %! assert_equal (class (obj.X), 'single'); %! [idx, D] = knnsearch (obj, single (Y), "K", 2); %! assert_equal (class (idx), 'double'); %! assert_equal (class (D), 'single'); %! ## a double query against single data is still computed in single %! [~, Dm] = knnsearch (obj, Y, "K", 2); %! assert_equal (class (Dm), 'single'); %! assert_equal (Dm, D); %! ## and so is a single query against double data %! [~, Ds] = knnsearch (ExhaustiveSearcher (X), single (Y), "K", 2); %! assert_equal (class (Ds), 'single'); %! ## double throughout stays double %! [~, Dd] = knnsearch (ExhaustiveSearcher (X), Y, "K", 2); %! assert_equal (class (Dd), 'double'); ## Integer data is converted to double on the way in. Held as int32 the ## coordinate differences were rounded and the distances came out wrong. %!test %! X = int32 ([10, 20; 33, 41; 55, 62]); Y = [21, 33]; %! obj = ExhaustiveSearcher (X); %! assert_equal (class (obj.X), 'double'); %! [~, D] = knnsearch (obj, Y, "K", 1); %! assert_equal (D, min (sqrt (sum ((double (X) - Y) .^ 2, 2))), 1e-12); ## rangesearch follows the same rule: single distances, double indices. %!test %! X = [1, 2; 3, 4; 5, 6; 7, 8]; Y = [2, 3]; %! [idx, D] = rangesearch (ExhaustiveSearcher (single (X)), single (Y), 4); %! assert_equal (class (idx{1}), 'double'); %! assert_equal (class (D{1}), 'single'); statistics-release-2.1.2/inst/Nearest_Neighbors/KDTreeSearcher.m000066400000000000000000001603531527311104300246600ustar00rootroot00000000000000## Copyright (C) 2025 Swayam Shah ## Copyright (C) 2025 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef KDTreeSearcher ## -*- texinfo -*- ## @deftp {statistics} KDTreeSearcher ## ## KD-tree nearest neighbor searcher ## ## The @code{KDTreeSearcher} class implements a KD-tree search algorithm for ## nearest neighbor queries. It stores training data and supports various ## distance metrics along with their parameter values for performing a KD-tree ## search. The KD-tree algorithm partitions the training data into a ## hierarchical tree structure and performs search operations by traversing ## the tree to reduce the number of distance computations. It facilitates ## nearest neighbor queries using @code{knnsearch} and radius queries using ## @code{rangesearch}. ## ## You can either use the @code{KDTreeSearcher} class constructor or the ## @code{createns} function to create an @qcode{KDTreeSearcher} object. ## ## @seealso{createns, ExhaustiveSearcher, hnswSearcher, knnsearch, ## rangesearch} ## @end deftp properties(SetAccess = private, Hidden) KDTree # KD-tree structure endproperties properties(SetAccess = private) ## -*- texinfo -*- ## @deftp {KDTreeSearcher} {property} X ## ## Point data ## ## Point data, specified as an @math{N*P} numeric matrix where each row is ## an observation and each column is a feature. This property is private ## and cannot be modified after object creation. ## ## Data of class @qcode{single} is stored and searched in single ## precision, any other numeric class is converted to @qcode{double}. ## ## @end deftp X = [] ## -*- texinfo -*- ## @deftp {KDTreeSearcher} {property} BucketSize ## ## Maximum number of data points in each leaf node ## ## The maximum number of data points in the leaf node of the KD-tree. ## Default value is 50. This property is private and cannot be modified ## after object creation. ## ## @end deftp BucketSize = 50 endproperties properties ## -*- texinfo -*- ## @deftp {KDTreeSearcher} {property} Distance ## ## Distance metric ## ## Distance metric used for searches, specified as a character vector. ## Supported metrics are @qcode{'euclidean'}, @qcode{'cityblock'}, ## @qcode{'minkowski'}, and @qcode{'chebychev'}. Default value is ## @qcode{'euclidean'}. ## ## @end deftp Distance = 'euclidean' ## -*- texinfo -*- ## @deftp {KDTreeSearcher} {property} DistParameter ## ## Distance parameter ## ## The type and value of the distance parameter depends on the selected ## @qcode{Distance} metric and can be any of the following: ## ## @itemize ## @item For @qcode{'minkowski'}, a positive scalar exponent (default 2); ## @code{Inf} gives the Chebychev distance. ## @item Empty for other metrics (@qcode{'euclidean'}, @qcode{'cityblock'}, ## @qcode{'chebychev'}). Attempting to set a non-empty value for these ## metrics will result in an error. ## @end itemize ## ## @end deftp DistParameter = [] endproperties methods (Hidden) ## Custom display function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ('%s =\n', in_name); endif disp (this); endfunction ## Custom display function disp (this) if (isscalar (this)) fprintf ("\n KDTreeSearcher with properties:\n\n"); fprintf ("%+25s: %d\n", 'BucketSize', this.BucketSize); fprintf ("%+25s: '%s'\n", 'Distance', this.Distance); if (! isempty (this.DistParameter)) if (isscalar (this.DistParameter)) fprintf ("%+25s: %g\n", 'DistParameter', this.DistParameter); elseif (isvector (this.DistParameter)) fprintf ("%+25s: %s\n", 'DistParameter', ... mat2str (this.DistParameter)); else fprintf ("%+25s: [%dx%d %s]\n", 'DistParameter', ... size (this.DistParameter), class (this.DistParameter)); endif else fprintf ("%+25s: []\n", 'DistParameter'); endif fprintf ("%+25s: [%dx%d %s]\n", 'X', size (this.X), class (this.X)); endif endfunction ## Class specific subscripted reference function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); switch (s.type) case '()' error ("KDTreeSearcher.subsref: () indexing not supported."); case '{}' error ("KDTreeSearcher.subsref: {} indexing not supported."); case '.' if (! ischar (s.subs)) error (strcat ("KDTreeSearcher.subsref: property", ... " name must be a character vector.")); endif try out = this.(s.subs); catch error (strcat ("KDTreeSearcher.subsref: unrecognized", ... " property: '%s'."), s.subs); end_try_catch endswitch ## Chained references if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction ## Class specific subscripted assignment function this = subsasgn (this, s, val) if (numel (s) > 1) error ("KDTreeSearcher.subsasgn: chained subscripts not allowed."); endif switch s.type case '()' error ("KDTreeSearcher.subsasgn: () indexing not supported."); case '{}' error ("KDTreeSearcher.subsasgn: {} indexing not supported."); case '.' if (! ischar (s.subs)) error (strcat ("KDTreeSearcher.subsasgn: property", ... " name must be a character vector.")); endif switch (s.subs) case 'X' error (strcat ("KDTreeSearcher.subsasgn: 'X' is", ... " read-only and cannot be modified.")); case 'KDTree' error (strcat ("KDTreeSearcher.subsasgn: 'KDTree' is", ... " read-only and cannot be modified.")); case 'BucketSize' error (strcat ("KDTreeSearcher.subsasgn: 'BucketSize'", ... " is read-only and cannot be modified.")); case 'Distance' allowed_distances = {'euclidean', 'cityblock', 'minkowski', ... 'chebychev'}; if (ischar (val)) if (! any (strcmpi (allowed_distances, val))) error (strcat ("KDTreeSearcher.subsasgn:", ... " unsupported distance metric '%s'."), val); endif this.Distance = val; else error (strcat ("KDTreeSearcher.subsasgn: 'Distance'", ... " must be a string.")); endif case 'DistParameter' if (strcmpi (this.Distance, 'minkowski')) if (! (isscalar (val) && isnumeric (val) && val > 0)) error (strcat ("KDTreeSearcher.subsasgn: 'DistParameter'", ... " must be a positive scalar for", ... " Minkowski distance.")); endif this.DistParameter = val; else if (! isempty (val)) error (strcat ("KDTreeSearcher.subsasgn: 'DistParameter'", ... " must be empty for this distance metric.")); endif this.DistParameter = val; endif otherwise error ("KDTreeSearcher.subsasgn: unrecognized property: '%s'.",... s.subs); endswitch endswitch endfunction endmethods methods ## -*- texinfo -*- ## @deftypefn {KDTreeSearcher} {@var{obj} =} KDTreeSearcher (@var{X}) ## @deftypefnx {KDTreeSearcher} {@var{obj} =} KDTreeSearcher (@var{X}, @var{name}, @var{value}) ## ## Create a @qcode{KDTreeSearcher} object for nearest neighbor searches. ## ## @code{@var{obj} = KDTreeSearcher (@var{X})} constructs a ## @qcode{KDTreeSearcher} object with training data @var{X} using the ## default @qcode{'euclidean'} distance metric. @var{X} must be an ## @math{N*P} numeric matrix, where rows represent observations and columns ## represent features. ## ## @code{@var{obj} = KDTreeSearcher (@var{X}, @var{name}, @var{value})} ## allows customization through name-value pairs: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Distance'} @tab Distance metric, specified as a ## character vector (@qcode{'euclidean'}, @qcode{'cityblock'}, ## @qcode{'minkowski'}, @qcode{'chebychev'}). Default is ## @qcode{'euclidean'}. ## ## @item @qcode{'P'} @tab Minkowski distance exponent, a positive ## scalar. Valid only when @qcode{'Distance'} is @qcode{'minkowski'}. ## Default is 2. An exponent of @code{Inf} gives the Chebychev distance. ## ## @item @qcode{'BucketSize'} @tab Maximum number of data points in the ## leaf node of the KD-tree, a positive integer. Default is 50. ## @end multitable ## ## You can also create a @qcode{KDTreeSearcher} object using the ## @code{createns} function. ## ## @seealso{KDTreeSearcher, knnsearch, rangesearch, createns} ## ## @qcode{'Distance'} and @qcode{'P'} override the searcher's own metric for ## that call only; the @qcode{Distance} and @qcode{DistParameter} ## properties keep their values. The tree is built from the data alone, so ## changing the metric does not rebuild it. @qcode{'Cov'} and ## @qcode{'Scale'} are not accepted, since they belong to metrics a kd-tree ## cannot search. ## @end deftypefn function obj = KDTreeSearcher (X, varargin) if (nargin < 1) error ("KDTreeSearcher: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error ("KDTreeSearcher: Name-Value arguments must be in pairs."); endif if (! (isnumeric (X) && ismatrix (X) && all (isfinite (X)(:)))) error ("KDTreeSearcher: X must be a finite numeric matrix."); endif ## Single precision is carried through, but every other class is ## converted up to double, as MATLAB does. Integer data would ## otherwise round each coordinate difference and corrupt distances. if (! isa (X, "single")) X = double (X); endif obj.X = X; ## Default values Distance = 'euclidean'; P = 2; PG = false; BucketSize = 50; ## Parse optional parameters while (numel (varargin) > 0) switch (lower (varargin{1})) case 'distance' Distance = varargin{2}; case 'p' P = varargin{2}; PG = true; case 'bucketsize' BucketSize = varargin{2}; otherwise error (strcat ("KDTreeSearcher: invalid parameter", ... " name: '%s'."), varargin{1}); endswitch varargin(1:2) = []; endwhile ## Validate Distance allowed_distances = {'euclidean', 'cityblock', 'minkowski', 'chebychev'}; if (ischar (Distance)) if (! any (strcmpi (allowed_distances, Distance))) error ("KDTreeSearcher: unsupported distance metric '%s'.", Distance); endif obj.Distance = Distance; else error ("KDTreeSearcher: Distance must be a string."); endif ## Set DistParameter if (strcmpi (obj.Distance, 'minkowski')) if (! (isscalar (P) && isnumeric (P) && P > 0)) error ("KDTreeSearcher: P must be a positive scalar."); endif obj.DistParameter = P; else if (PG) error ("KDTreeSearcher: 'P' applies only to the minkowski metric."); endif obj.DistParameter = []; endif ## Set BucketSize if (! (isscalar (BucketSize) && isnumeric (BucketSize) && BucketSize > 0 && BucketSize == fix (BucketSize))) error ("KDTreeSearcher: BucketSize must be a positive integer."); endif obj.BucketSize = BucketSize; ## Build KDTree obj.KDTree = __build_kdtree__ (1:size (X,1), 0, X, BucketSize); endfunction ## -*- texinfo -*- ## @deftypefn {KDTreeSearcher} {[@var{idx}, @var{D}] =} knnsearch (@var{obj}, @var{Y}) ## @deftypefnx {KDTreeSearcher} {[@var{idx}, @var{D}] =} knnsearch (@var{obj}, @var{Y}, @var{name}, @var{value}) ## ## Find the @math{K} nearest neighbors in the training data to query points. ## ## @code{[@var{idx}, @var{D}] = knnsearch (@var{obj}, @var{Y}, @var{K})} ## returns the indices @var{idx} and distances @var{D} of the @math{K} ## nearest neighbors in @var{obj.X} to each point in @var{Y}, using the ## distance metric specified in @var{obj.Distance}. ## ## @itemize ## @item @var{obj} is a @qcode{KDTreeSearcher} object. ## @item @var{Y} is an @math{M*P} numeric matrix of query points, where ## @math{P} must match the number of columns in @var{obj.X}. ## @item @var{idx} contains the indices of the nearest neighbors in ## @var{obj.X}. ## @item @var{D} contains the corresponding distances. ## @end itemize ## ## @var{idx} is always of class @qcode{double}. @var{D} is of class ## @qcode{single} when either @var{obj.X} or @var{Y} is @qcode{single}, ## in which case the distances are computed in single precision, and of ## class @qcode{double} otherwise. ## ## @code{[@var{idx}, @var{D}] = knnsearch (@var{obj}, @var{Y}, @var{name}, ## @var{value})} allows additional options via name-value pairs: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'K'} @tab A positive integer specifying the number of ## nearest neighbors to find. Default is 1. A value larger than the ## number of observations in the training data is answered with all of ## them, since there are no more neighbors to return. ## ## @item @qcode{'IncludeTies'} @tab Logical flag indicating whether to ## include all neighbors tied with the @math{K}th smallest distance. Default ## is @qcode{false}. If @qcode{true}, @var{idx} and @var{D} are cell arrays. ## ## @item @qcode{'SortIndices'} @tab Logical flag indicating whether to ## sort the indices by distance. Default is @qcode{true}. ## @end multitable ## ## @seealso{KDTreeSearcher, rangesearch} ## @end deftypefn function [idx, D] = knnsearch (obj, Y, varargin) ## Initial input validation if (nargin < 2) error ("KDTreeSearcher.knnsearch: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error (strcat ("KDTreeSearcher.knnsearch:", ... " Name-Value arguments must be in pairs.")); endif ## Get training data size [N, C] = size (obj.X); ## Validate Y if (isempty (Y)) error ("KDTreeSearcher.knnsearch: Y cannot be empty."); endif if (! (isnumeric (Y) && ismatrix (Y) && all (isfinite (Y)(:)))) error ("KDTreeSearcher.knnsearch: Y must be a finite numeric matrix."); endif if (C != size (Y, 2)) error (strcat ("KDTreeSearcher.knnsearch: Y must have the same", ... " number of columns as the training data in OBJ.X.")); endif ## Default values K = 1; ## Parse options IncludeTies = false; SortIndices = true; Dist = []; Pval = []; while (numel (varargin) > 0) switch (lower (varargin{1})) case 'k' K = varargin{2}; if (! (isscalar (K) && isnumeric (K) && K >= 1 && K == fix (K) && isfinite (K))) error (strcat ("KDTreeSearcher.knnsearch: 'K' must", ... " be a positive integer.")); endif case 'includeties' IncludeTies = varargin{2}; if (! (islogical (IncludeTies) && isscalar (IncludeTies))) error (strcat ("KDTreeSearcher.knnsearch:", ... " IncludeTies must be a logical scalar.")); endif case 'sortindices' SortIndices = varargin{2}; if (! (islogical (SortIndices) && isscalar (SortIndices))) error (strcat ("KDTreeSearcher.knnsearch:", ... " SortIndices must be a logical scalar.")); endif case 'distance' Dist = varargin{2}; case 'p' Pval = varargin{2}; otherwise error (strcat ("KDTreeSearcher.knnsearch: invalid", ... " parameter name: '%s'."), varargin{1}); endswitch varargin(1:2) = []; endwhile ## A metric given here applies to this call only. The tree is ## built from the data alone, so changing the metric does not ## invalidate it; only the pruning bound changes. 'Cov' and ## 'Scale' are not accepted: they belong to metrics a kd-tree ## cannot search. Distance = obj.Distance; DistParameter = obj.DistParameter; if (! (isempty (Dist) && isempty (Pval))) if (isempty (Dist)) Dist = Distance; endif [Distance, DistParameter] = __resolve_metric__ ( ... "KDTreeSearcher.knnsearch", obj.X, Dist, ... Pval, [], [], {"euclidean", "cityblock", "chebychev", "minkowski"}); endif ## A Minkowski exponent of Inf is the Chebychev distance, its limit, which ## the search computes directly if (strcmpi (Distance, 'minkowski') && DistParameter == Inf) Distance = 'chebychev'; DistParameter = []; endif ## There are only as many points to return as the training data holds, ## so a larger K is answered with all of them. Building the result at ## the requested width raised an internal nonconformance instead. K = min (K, rows (obj.X)); ## Distances are computed in single precision when either the training ## data or the query is single, and in double otherwise. The indices ## are always double. cls = "double"; if (isa (obj.X, "single") || isa (Y, "single")) cls = "single"; endif Y = cast (Y, cls); if (IncludeTies) idx = cell (rows (Y), 1); D = cell (rows (Y), 1); for i = 1:rows (Y) [temp_idx, temp_D] = __search_kdtree__ (obj.KDTree, Y(i,:), K, obj.X, ... Distance, DistParameter, ... false); r = temp_D(end) + 1e-10; # Add small epsilon to capture ties [idx{i}, D{i}] = __search_kdtree__ (obj.KDTree, Y(i,:), Inf, obj.X, ... Distance, DistParameter, ... true, r); if (SortIndices) iv = idx{i}(:); [sorted_D, sort_idx] = sortrows ([D{i}(:), iv]); D{i} = sorted_D(:, 1); idx{i} = iv(sort_idx); endif ## One row per query point, as MATLAB and ExhaustiveSearcher return idx{i} = idx{i}(:)'; D{i} = D{i}(:)'; endfor else idx = zeros (rows (Y), K); D = zeros (rows (Y), K, cls); for i = 1:rows (Y) [temp_idx, temp_D] = __search_kdtree__ (obj.KDTree, Y(i,:), K, obj.X, ... Distance, DistParameter, ... false); if (SortIndices) [sorted_D, sort_idx] = sortrows ([temp_D, temp_idx]); idx(i,:) = temp_idx(sort_idx); D(i,:) = sorted_D(:,1)'; else idx(i,:) = temp_idx; D(i,:) = temp_D; endif endfor endif endfunction ## -*- texinfo -*- ## @deftypefn {KDTreeSearcher} {[@var{idx}, @var{D}] =} rangesearch (@var{obj}, @var{Y}, @var{r}) ## @deftypefnx {KDTreeSearcher} {[@var{idx}, @var{D}] =} rangesearch (@var{obj}, @var{Y}, @var{r}, @var{name}, @var{value}) ## ## Find all neighbors within a specified radius of query points. ## ## @code{[@var{idx}, @var{D}] = rangesearch (@var{obj}, @var{Y}, @var{r})} ## returns the indices @var{idx} and distances @var{D} of all points in ## @var{obj.X} within radius @var{r} of each point in @var{Y}, using the ## distance metric specified in @var{obj.Distance}. ## ## @itemize ## @item @var{obj} is a @qcode{KDTreeSearcher} object. ## @item @var{Y} is an @math{M*P} numeric matrix of query points, where ## @math{P} must match the number of columns in @var{obj.X}. ## @item @var{r} is a nonnegative scalar specifying the search radius. ## @end itemize ## ## @var{idx} is always of class @qcode{double}. @var{D} is of class ## @qcode{single} when either @var{obj.X} or @var{Y} is @qcode{single}, ## in which case the distances are computed in single precision, and of ## class @qcode{double} otherwise. ## ## @code{[@var{idx}, @var{D}] = rangesearch (@var{obj}, @var{Y}, @var{r}, ## @var{name}, ## @var{value})} ## allows additional options via name-value pairs: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'SortIndices'} @tab Logical flag indicating whether to ## sort the indices by distance. Default is @qcode{true}. ## @end multitable ## ## @var{idx} and @var{D} are cell arrays where each cell contains the ## indices and distances for one query point in @var{Y}. ## ## @seealso{KDTreeSearcher, knnsearch} ## @end deftypefn function [idx, D] = rangesearch (obj, Y, r, varargin) if (nargin < 3) error ("KDTreeSearcher.rangesearch: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error (strcat ("KDTreeSearcher.rangesearch:", ... " Name-Value arguments must be in pairs.")); endif Dist = []; Pval = []; if (! (isnumeric (Y) && ismatrix (Y) && all (isfinite (Y)(:)))) error (strcat ("KDTreeSearcher.rangesearch:", ... " Y must be a finite numeric matrix.")); endif if (size (obj.X, 2) != size (Y, 2)) error (strcat ("KDTreeSearcher.rangesearch:", ... " number of columns in X and Y must match.")); endif if (! (isscalar (r) && isnumeric (r) && r >= 0 && isfinite (r))) error (strcat ("KDTreeSearcher.rangesearch:", ... " R must be a nonnegative finite scalar.")); endif ## Parse options SortIndices = true; while (numel (varargin) > 0) switch (lower (varargin{1})) case 'sortindices' SortIndices = varargin{2}; if (! (islogical (SortIndices) && isscalar (SortIndices))) error (strcat ("KDTreeSearcher.rangesearch:", ... " SortIndices must be a logical scalar.")); endif case 'distance' Dist = varargin{2}; case 'p' Pval = varargin{2}; otherwise error (strcat ("KDTreeSearcher.rangesearch:", ... " invalid parameter name: '%s'."), varargin{1}); endswitch varargin(1:2) = []; endwhile ## A metric given here applies to this call only. The tree is ## built from the data alone, so changing the metric does not ## invalidate it; only the pruning bound changes. 'Cov' and ## 'Scale' are not accepted: they belong to metrics a kd-tree ## cannot search. Distance = obj.Distance; DistParameter = obj.DistParameter; if (! (isempty (Dist) && isempty (Pval))) if (isempty (Dist)) Dist = Distance; endif [Distance, DistParameter] = __resolve_metric__ ( ... "KDTreeSearcher.rangesearch", obj.X, Dist, ... Pval, [], [], {"euclidean", "cityblock", "chebychev", "minkowski"}); endif ## A Minkowski exponent of Inf is the Chebychev distance, its limit, which ## the search computes directly if (strcmpi (Distance, 'minkowski') && DistParameter == Inf) Distance = 'chebychev'; DistParameter = []; endif ## Distances are computed in single precision when either the training ## data or the query is single, and in double otherwise. The indices ## are always double. cls = "double"; if (isa (obj.X, "single") || isa (Y, "single")) cls = "single"; endif Y = cast (Y, cls); idx = cell (rows (Y), 1); D = cell (rows (Y), 1); for i = 1:rows (Y) [idx{i}, D{i}] = __search_kdtree__ (obj.KDTree, Y(i,:), Inf, obj.X, ... Distance, DistParameter, ... true, r); if (SortIndices) iv = idx{i}(:); [sorted_D, sort_idx] = sortrows ([D{i}(:), iv]); D{i} = sorted_D(:, 1); idx{i} = iv(sort_idx); endif ## One row per query point, as MATLAB and ExhaustiveSearcher return idx{i} = idx{i}(:)'; D{i} = D{i}(:)'; endfor endfunction endmethods endclassdef ## Private functions: ## Demo Examples %!demo %! ## Demo to verify implementation using fisheriris dataset %! load fisheriris %! numSamples = size (meas, 1); %! queryIndices = [1, 23, 46, 63, 109]; %! dataIndices = ! ismember (1:numSamples, queryIndices); %! queryPoints = meas(queryIndices, :); %! dataPoints = meas(dataIndices, :); %! searchRadius = 0.3; %! kdTree = KDTreeSearcher (dataPoints, 'Distance', 'minkowski') %! nearestNeighbors = knnsearch (kdTree, queryPoints, 'K', 2) %! neighborsInRange = rangesearch (kdTree, queryPoints, searchRadius) %!demo %! ## Create a KDTreeSearcher with Euclidean distance %! X = [1, 2; 3, 4; 5, 6]; %! obj = KDTreeSearcher (X); %! ## Find the nearest neighbor to [2, 3] %! Y = [2, 3]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! disp ('Nearest neighbor index:'); %! disp (idx); %! disp ('Distance:'); %! disp (D); %! ## Find all points within radius 2 %! [idx, D] = rangesearch (obj, Y, 2); %! disp ('Indices within radius:'); %! disp (idx); %! disp ('Distances:'); %! disp (D); %!demo %! ## Create a KDTreeSearcher with Minkowski distance (P=3) %! X = [0, 0; 1, 0; 2, 0]; %! obj = KDTreeSearcher (X, 'Distance', 'minkowski', 'P', 3); %! ## Find the nearest neighbor to [1, 0] %! Y = [1, 0]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! disp ('Nearest neighbor index:'); %! disp (idx); %! disp ('Distance:'); %! disp (D); %!demo %! rng (42); %! disp ('Demonstrating KDTreeSearcher'); %! %! n = 100; %! mu1 = [0.3, 0.3]; %! mu2 = [0.7, 0.7]; %! sigma = 0.1; %! X1 = mu1 + sigma * randn (n / 2, 2); %! X2 = mu2 + sigma * randn (n / 2, 2); %! X = [X1; X2]; %! %! obj = KDTreeSearcher (X); %! %! Y = [0.3, 0.3; 0.7, 0.7; 0.5, 0.5]; %! %! K = 5; %! [idx, D] = knnsearch (obj, Y, 'K', K); %! %! disp ('For the first query point:'); %! disp (['Query point: ', num2str(Y(1,:))]); %! disp ('Indices of nearest neighbors:'); %! disp (idx(1,:)); %! disp ('Distances:'); %! disp (D(1,:)); %! %! figure; %! scatter (X(:,1), X(:,2), 36, 'b', 'filled'); # Training points %! hold on; %! scatter (Y(:,1), Y(:,2), 36, 'r', 'filled'); # Query points %! for i = 1:size (Y, 1) %! query = Y(i,:); %! neighbors = X(idx(i,:), :); %! for j = 1:K %! plot ([query(1), neighbors(j,1)], [query(2), neighbors(j,2)], 'k-'); %! endfor %! endfor %! hold off; %! title ('K Nearest Neighbors with KDTreeSearcher'); %! xlabel ('X1'); %! ylabel ('X2'); %! %! r = 0.15; %! [idx, D] = rangesearch (obj, Y, r); %! %! disp ('For the first query point in rangesearch:'); %! disp (['Query point: ', num2str(Y(1,:))]); %! disp ('Indices of points within radius:'); %! disp (idx{1}); %! disp ('Distances:'); %! disp (D{1}); %! %! figure; %! scatter (X(:,1), X(:,2), 36, 'b', 'filled'); %! hold on; %! scatter (Y(:,1), Y(:,2), 36, 'r', 'filled'); %! theta = linspace (0, 2 * pi, 100); %! for i = 1:size (Y, 1) %! center = Y(i,:); %! x_circle = center(1) + r * cos (theta); %! y_circle = center(2) + r * sin (theta); %! plot (x_circle, y_circle, 'g-'); %! ## Highlight points within radius %! if (! isempty (idx{i})) %! in_radius = X(idx{i}, :); %! scatter (in_radius(:,1), in_radius(:,2), 36, 'g', 'filled'); %! endif %! endfor %! hold off %! title ('Points within Radius with KDTreeSearcher'); %! xlabel ('X1'); %! ylabel ('X2'); ## Test Cases %!test %! load fisheriris %! X = meas; %! obj = KDTreeSearcher (X); %! Y = X(1:5,:); %! [idx, D] = knnsearch (obj, Y, 'K', 3); %! assert_equal (idx, [[1, 18, 5]; [2, 35, 46]; [3, 48, 4]; [4, 48, 30]; [5, 38, 1]]) %! assert_equal (D, [[0, 0.1000, 0.1414]; [0, 0.1414, 0.1414]; [0, 0.1414, 0.2449]; %! [0, 0.1414, 0.1732]; [0, 0.1414, 0.1414]], 5e-5) %!test %! load fisheriris %! X = meas; %! obj = KDTreeSearcher (X, 'Distance', 'minkowski', 'P', 3); %! Y = X(10:15,:); %! [idx, D] = knnsearch (obj, Y, 'K', 2); %! assert_equal (idx, [[10, 35]; [11, 49]; [12, 30]; [13, 2]; [14, 39]; [15, 34]]) %! assert_equal (D, [[0, 0.1000]; [0, 0.1000]; [0, 0.2080]; [0, 0.1260]; [0, 0.2154]; %! [0, 0.3503]], 5e-5) %!test %! load fisheriris %! X = meas; %! obj = KDTreeSearcher (X, 'Distance', 'cityblock'); %! Y = X(20:25,:); %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (idx, [20; 21; 22; 23; 24; 25]) %! assert_equal (D, [0; 0; 0; 0; 0; 0]) %!test %! load fisheriris %! X = meas; %! obj = KDTreeSearcher (X, 'Distance', 'chebychev'); %! Y = X(30:35,:); %! [idx, D] = knnsearch (obj, Y, 'K', 4); %! assert_equal (idx, [[30, 31, 4, 12]; [31, 30, 10, 35]; [32, 21, 37, 28]; %! [33, 20, 34, 47]; [34, 16, 15, 33]; [35, 10, 2, 26]]) %! assert_equal (D, [[0, 0.1000, 0.1000, 0.2000]; [0, 0.1000, 0.1000, 0.1000]; %! [0, 0.2000, 0.2000, 0.2000]; [0, 0.3000, 0.3000, 0.3000]; %! [0, 0.2000, 0.3000, 0.3000]; [0, 0.1000, 0.1000, 0.1000]], 5e-15) %!test %! load fisheriris %! X = meas; %! obj = KDTreeSearcher (X, 'BucketSize', 20); %! Y = X(40:45,:); %! [idx, D] = knnsearch (obj, Y, 'K', 2); %! assert_equal (idx, [[40, 8]; [41, 18]; [42, 9]; [43, 39]; [44, 27]; [45, 47]]) %! assert_equal (D, [[0, 0.1000]; [0, 0.1414]; [0, 0.6245]; [0, 0.2000]; [0, 0.2236]; %! [0, 0.3606]], 4.7e-5) %!test %! load fisheriris %! X = meas; %! obj = KDTreeSearcher (X); %! Y = X(50:55,:); %! [idx, D] = knnsearch (obj, Y, 'K', 3, 'IncludeTies', true); %! assert_equal (idx, {[50, 8, 40]; [51, 53, 87]; [52, 57, 76]; ... %! [53, 51, 87]; [54, 90, 81]; [55, 59, 76]}) %! assert_equal (D, {[0, 0.1414, 0.1732]; [0, 0.2646, 0.3317]; ... %! [0, 0.2646, 0.3162]; [0, 0.2646, 0.2828]; ... %! [0, 0.2000, 0.3000]; [0, 0.2449, 0.3162]}, 5e-5) %!test %! load fisheriris %! X = meas; %! obj = KDTreeSearcher (X); %! Y = X(60:65,:); %! [idx, D] = rangesearch (obj, Y, 0.4); %! assert_equal (idx, {[60, 90]; [61, 94]; ... %! [62, 97, 79, 96, 100, 89, 98, 72]; [63]; ... %! [64, 92, 74, 79]; [65]}) %! assert_equal (D, {[0, 0.3873]; [0, 0.3606]; ... %! [0, 0.3000, 0.3317, 0.3606, 0.3606, 0.3742, 0.3873, ... %! 0.4000]; [0]; [0, 0.1414, 0.2236, 0.2449]; [0]}, 5e-5) %!test %! load fisheriris %! X = meas; %! obj = KDTreeSearcher (X, 'Distance', 'cityblock'); %! Y = X(70:72,:); %! [idx, D] = rangesearch (obj, Y, 1.0); %! assert_equal (idx, {[70, 81, 90, 82, 83, 93, 54, 68, 95, 80, 91, 100, ... %! 60, 65, 89, 63]; ... %! [71, 139, 128, 150, 127, 57, 86, 64, 79, 92, 124]; ... %! [72, 100, 98, 83, 93, 97, 75, 68, 62, 89, 95, 74, ... %! 56, 90, 79, 92, 96, 64, 63, 65]}) %! assert_equal (D, {[0, 0.3000, 0.4000, 0.5000, 0.5000, 0.5000, 0.6000, ... %! 0.7000, 0.7000, 0.7000, 0.8000, 0.8000, 0.9000, ... %! 0.9000, 0.9000, 0.9000]; ... %! [0, 0.3000, 0.5000, 0.5000, 0.7000, 0.8000, 0.8000, ... %! 1.0000, 1.0000, 1.0000, 1]; ... %! [0, 0.5000, 0.5000, 0.6000, 0.6000, 0.7000, 0.7000, ... %! 0.8000, 0.8000, 0.8000, 0.8000, 0.8000, 0.9000, ... %! 0.9000, 0.9000, 0.9000, 0.9000, 0.9000, 1.0000, 1]}, 5e-5) %!test %! load fisheriris %! X = meas; %! obj = KDTreeSearcher (X, 'Distance', 'minkowski', 'P', 3); %! Y = X(80:85,:); %! [idx, D] = rangesearch (obj, Y, 0.8); %! assert_equal (idx, {[80, 82, 81, 65, 70, 83, 93, 90, 54, 63, 68, 72, ... %! 100, 60, 89, 99, 95, 94, 97, 96]; [81, 82, 70, 54, ... %! 90, 93, 80, 83, 60, 68, 95, 100, 65, 63, 97, 61, 91, ... %! 94, 89, 96, 72, 58, 62, 56]; [82, 81, 70, 80, 54, ... %! 90, 93, 83, 68, 60, 65, 63, 100, 95, 94, 61, 58, 97, ... %! 89, 72, 96, 91, 99]; [83, 93, 100, 68, 70, 72, 95, ... %! 90, 97, 65, 89, 96, 81, 82, 80, 62, 54, 98, 63, 91, ... %! 56, 60, 79, 67, 75, 88, 85, 92, 69]; [84, 134, 102, ... %! 143, 150, 124, 128, 73, 127, 139, 147, 64, 112, 114, ... %! 120, 74, 135, 122, 71, 92, 104, 138, 148, 117, 79, ... %! 55, 56, 57, 67, 111, 129, 69, 78, 59, 52, 133, 85, ... %! 88, 87]; [85, 67, 56, 97, 95, 89, 96, 91, 100, 62, ... %! 71, 122, 79, 60, 107, 90, 139, 93, 68, 86, 83, 92, ... %! 64, 150, 102, 143, 74, 114, 70, 128, 84, 54, 72]}) %! assert_equal (D, {[0, 0.2884, 0.3530, 0.3826, 0.4062, 0.4198, 0.5117, ... %! 0.5440, 0.5718, 0.6000, 0.6018, 0.6073, 0.6308, ... %! 0.6333, 0.6753, 0.7000, 0.7192, 0.7230, 0.7350, ... %! 0.7459]; [0, 0.1260, 0.1442, 0.2571, 0.2571, 0.3530, ... %! 0.3530, 0.3826, 0.4344, 0.4344, 0.4642, 0.4747, ... %! 0.5217, 0.5217, 0.5896, 0.6009, 0.6082, 0.6316, ... %! 0.6316, 0.6611, 0.6664, 0.6993, 0.7417, 0.7507]; [0, ... %! 0.1260, 0.2224, 0.2884, 0.3803, 0.3803, 0.4121, ... %! 0.4121, 0.4905, 0.5013, 0.5360, 0.5429, 0.5463, ... %! 0.5646, 0.5749, 0.5819, 0.6542, 0.6581, 0.6753, ... %! 0.6938, 0.7094, 0.7107, 0.7423]; [0, 0.1260, 0.2224, ... %! 0.2520, 0.2571, 0.3107, 0.3302, 0.3332, 0.3332, ... %! 0.3530, 0.3530, 0.3803, 0.3826, 0.4121, 0.4198, ... %! 0.4344, 0.4531, 0.5155, 0.5217, 0.5348, 0.6028, ... %! 0.6073, 0.6374, 0.6527, 0.6611, 0.6804, 0.6938, ... %! 0.7399, 0.7560]; [0, 0.3072, 0.3271, 0.3271, 0.3302, ... %! 0.3503, 0.3530, 0.3530, 0.3530, 0.3958, 0.3979, ... %! 0.4327, 0.4626, 0.4642, 0.5027, 0.5066, 0.5130, ... %! 0.5155, 0.5440, 0.5440, 0.5518, 0.5848, 0.6009, ... %! 0.6073, 0.6082, 0.6316, 0.6471, 0.6746, 0.6753, ... %! 0.6797, 0.6804, 0.7047, 0.7192, 0.7218, 0.7405, ... %! 0.7405, 0.7719, 0.7725, 0.7786]; [0, 0.2000, 0.3503, ... %! 0.3979, 0.4121, 0.4309, 0.4327, 0.4531, 0.4747, ... %! 0.5337, 0.5718, 0.5896, 0.6009, 0.6316, 0.6366, ... %! 0.6374, 0.6463, 0.6542, 0.6542, 0.6550, 0.6938, ... %! 0.7014, 0.7067, 0.7166, 0.7186, 0.7186, 0.7281, ... %! 0.7380, 0.7447, 0.7571, 0.7719, 0.7813, 0.7851]}, 5e-5) %!test %! load fisheriris %! X = meas; %! obj = KDTreeSearcher (X, 'Distance', 'chebychev'); %! Y = X(90,:); %! [idx, D] = rangesearch (obj, Y, 0.7); %! assert_equal (idx, {[90, 70, 54, 81, 95, 60, 83, 93, 100, 68, 82, 65, ... %! 97, 91, 56, 61, 62, 63, 67, 79, 80, 85, 89, 96, 72, ... %! 92, 107]}) %! assert_equal (D, {[0, 0.2000, 0.2000, 0.2000, 0.2000, 0.3000, 0.3000, ... %! 0.3000, 0.3000, 0.3000, 0.3000, 0.4000, 0.4000, ... %! 0.4000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ... %! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.6000, ... %! 0.6000, 0.6000]}, 5e-5) %!test %! ## Constructor with single-point dataset %! X = [0, 0]; %! obj = KDTreeSearcher (X); %! assert_equal (obj.X, X); %! assert_equal (obj.Distance, "euclidean"); %! assert_equal (isempty (obj.DistParameter), true); %! assert_equal (obj.BucketSize, 50); %!test %! ## Constructor with duplicate points %! X = [0, 0; 0, 0; 1, 0]; %! obj = KDTreeSearcher (X, 'Distance', 'cityblock'); %! assert_equal (obj.X, X); %! assert_equal (obj.Distance, "cityblock"); %!test %! ## Constructor with 3D data %! X = [0, 0, 0; 1, 0, 0; 0, 1, 0]; %! obj = KDTreeSearcher (X, 'Distance', 'minkowski', 'P', 3); %! assert_equal (obj.X, X); %! assert_equal (obj.DistParameter, 3); %!test %! ## knnsearch with grid, K = 1 %! X = [0, 0; 0, 1; 1, 0; 1, 1]; %! obj = KDTreeSearcher (X, 'Distance', 'euclidean'); %! Y = [0.5, 0.5]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! D_true = pdist2 (X, Y, 'euclidean'); %! assert_equal (D, min (D_true), 1e-10); %! assert_equal (any (idx == find (D_true == min (D_true))), true); %!test %! ## knnsearch with IncludeTies, all points equidistant %! X = [0, 0; 0, 1; 1, 0; 1, 1]; %! obj = KDTreeSearcher (X); %! Y = [0.5, 0.5]; %! [idx, D] = knnsearch (obj, Y, 'K', 1, 'IncludeTies', true); %! D_true = pdist2 (X, Y, 'euclidean'); %! expected_idx = find (D_true == min (D_true)); %! assert_equal (sort (idx{1}(:)), sort (expected_idx)); %! assert_equal (D{1}(:)', repmat (min (D_true), 1, 4), 1e-10); %!test %! ## rangesearch with line dataset %! X = [0, 0; 1, 0; 2, 0; 3, 0]; %! obj = KDTreeSearcher (X); %! Y = [1.5, 0]; %! r = 1; %! [idx, D] = rangesearch (obj, Y, r); %! D_true = pdist2 (X, Y, 'euclidean'); %! expected_idx = find (D_true <= r); %! assert_equal (sort (idx{1}(:)), sort (expected_idx)); %! assert_equal (D{1}, sort (D_true(expected_idx))', 1e-10); %!test %! ## knnsearch with duplicates %! X = [0, 0; 0, 0; 1, 0]; %! obj = KDTreeSearcher (X, 'Distance', 'cityblock'); %! Y = [0, 0]; %! [idx, D] = knnsearch (obj, Y, 'K', 1, 'IncludeTies', true); %! assert_equal (sort (idx{1}(:))', [1, 2]); %! assert_equal (D{1}, [0, 0], 1e-10); %!test %! ## rangesearch with 3D data %! X = [0, 0, 0; 1, 0, 0; 0, 1, 0]; %! obj = KDTreeSearcher (X, 'Distance', 'cityblock'); %! Y = [0, 0, 0]; %! r = 1; %! [idx, D] = rangesearch (obj, Y, r); %! assert_equal (sort (idx{1}(:))', [1, 2, 3]); %! assert_equal (D{1}, [0, 1, 1], 1e-10); %!test %! ## knnsearch with P = 2 (Euclidean equivalent) %! X = [0, 0; 1, 1]; %! obj = KDTreeSearcher (X, 'Distance', 'minkowski', 'P', 2); %! Y = [0, 1]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (idx, 1); %! assert_equal (D, 1, 1e-10); %!test %! ## rangesearch with P = 3 %! X = [0, 0; 1, 0; 0, 1]; %! obj = KDTreeSearcher (X, 'Distance', 'minkowski', 'P', 3); %! Y = [0.5, 0.5]; %! r = 0.8; %! [idx, D] = rangesearch (obj, Y, r); %! D_true = pdist2 (X, Y, 'minkowski', 3); %! expected_idx = find (D_true <= r); %! assert_equal (sort (idx{1}(:)), sort (expected_idx)); %! assert_equal (D{1}, sort (D_true(expected_idx))', 1e-10); %!test %! ## knnsearch with P = 4, random data %! X = rand (5, 2); %! obj = KDTreeSearcher (X, 'Distance', 'minkowski', 'P', 4); %! Y = rand (1, 2); %! [idx, D] = knnsearch (obj, Y, 'K', 3); %! D_true = pdist2 (X, Y, 'minkowski', 4); %! [sorted_D, sort_idx] = sort (D_true); %! assert_equal (idx', sort_idx(1:3)); %! assert_equal (D', sorted_D(1:3), 1e-10); %!test %! ## knnsearch with all same points %! X = [1, 1; 1, 1; 1, 1]; %! obj = KDTreeSearcher (X, 'Distance', 'chebychev'); %! Y = [1, 1]; %! [idx, D] = knnsearch (obj, Y, 'K', 1, 'IncludeTies', true); %! assert_equal (sort (idx{1}(:))', [1, 2, 3]); %! assert_equal (D{1}, [0, 0, 0], 1e-10); %!test %! ## rangesearch with grid %! X = [0, 0; 0, 1; 1, 0; 1, 1]; %! obj = KDTreeSearcher (X, 'Distance', 'chebychev'); %! Y = [0.5, 0.5]; %! r = 0.5; %! [idx, D] = rangesearch (obj, Y, r); %! D_true = pdist2 (X, Y, 'chebychev'); %! expected_idx = find (D_true <= r); %! assert_equal (sort (idx{1}(:)), sort (expected_idx)); %! assert_equal (D{1}, D_true(expected_idx)', 1e-10); %!test %! ## Changing Distance and verifying search %! X = [0,0; 1,0]; %! obj = KDTreeSearcher (X, 'Distance', 'euclidean'); %! Y = [0,1]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (D, 1, 1e-10); %! obj.Distance = 'chebychev'; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (D, 1, 1e-10); %!test %! ## Changing DistParameter for minkowski %! X = [0,0; 1,0]; %! obj = KDTreeSearcher (X, 'Distance', 'minkowski', 'P', 1); %! Y = [0,1]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (D, 1, 1e-10); %! obj.DistParameter = 3; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (D, 1, 1e-10); %!test %! ## Different BucketSize values %! X = rand (20,2); %! obj1 = KDTreeSearcher (X, 'BucketSize', 5); %! obj2 = KDTreeSearcher (X, 'BucketSize', 15); %! Y = rand (1,2); %! [idx1, D1] = knnsearch (obj1, Y, 'K', 3); %! [idx2, D2] = knnsearch (obj2, Y, 'K', 3); %! assert_equal (idx1, idx2); %! assert_equal (D1, D2, 1e-10); %!test %! ## Basic constructor with default Euclidean %! X = [1, 2; 3, 4; 5, 6]; %! obj = KDTreeSearcher (X); %! assert_equal (obj.X, X); %! assert_equal (obj.Distance, "euclidean"); %! assert_equal (isempty (obj.DistParameter), true); %! assert_equal (obj.BucketSize, 50); %!test %! ## Minkowski distance with custom P %! X = [0, 0; 1, 1; 2, 2]; %! obj = KDTreeSearcher (X, 'Distance', 'minkowski', 'P', 3); %! assert_equal (obj.Distance, "minkowski"); %! assert_equal (obj.DistParameter, 3); %!test %! ## Cityblock distance %! X = [0, 0; 1, 0; 0, 1]; %! obj = KDTreeSearcher (X, 'Distance', 'cityblock'); %! assert_equal (obj.Distance, "cityblock"); %! assert_equal (isempty (obj.DistParameter), true); %!test %! ## Chebychev distance %! X = [1, 1; 2, 3; 4, 2]; %! obj = KDTreeSearcher (X, 'Distance', 'chebychev'); %! assert_equal (obj.Distance, "chebychev"); %! assert_equal (isempty (obj.DistParameter), true); %!test %! ## knnsearch with Euclidean distance %! X = [1, 2; 3, 4; 5, 6]; %! obj = KDTreeSearcher (X); %! Y = [2, 3]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (idx, 1); %! assert_equal (D, sqrt (2), 1e-10); %!test %! ## knnsearch with Cityblock distance %! X = [0, 0; 1, 1; 2, 2]; %! obj = KDTreeSearcher (X, 'Distance', 'cityblock'); %! Y = [1, 0]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (ismember (idx, [1, 2]), true); %! assert_equal (D, 1, 1e-10); %!test %! ## knnsearch with Chebychev distance %! X = [1, 1; 2, 3; 4, 2]; %! obj = KDTreeSearcher (X, 'Distance', 'chebychev'); %! Y = [2, 2]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (ismember (idx, [1, 2]), true); %! assert_equal (D, 1, 1e-10); %!test %! ## knnsearch with Minkowski P=3 %! X = [0, 0; 1, 0; 2, 0]; %! obj = KDTreeSearcher (X, 'Distance', 'minkowski', 'P', 3); %! Y = [1, 0]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (idx, 2); %! assert_equal (D, 0, 1e-10); %!test %! ## knnsearch with IncludeTies %! X = [0, 0; 1, 0; 0, 1]; %! obj = KDTreeSearcher (X); %! Y = [0.5, 0]; %! [idx, D] = knnsearch (obj, Y, 'K', 1, 'IncludeTies', true); %! assert_equal (iscell (idx), true); %! assert_equal (sort (idx{1}(:))', [1, 2]); %! assert_equal (sort (D{1}(:)), [0.5; 0.5], 1e-10); %!test %! ## rangesearch with Euclidean %! X = [1, 1; 2, 2; 3, 3]; %! obj = KDTreeSearcher (X); %! Y = [0, 0]; %! [idx, D] = rangesearch (obj, Y, 2); %! assert_equal (idx{1}, [1]); %! assert_equal (D{1}, [sqrt(2)], 1e-10); %!test %! ## rangesearch with Cityblock %! X = [0, 0; 1, 1; 2, 2]; %! obj = KDTreeSearcher (X, 'Distance', 'cityblock'); %! Y = [0, 0]; %! [idx, D] = rangesearch (obj, Y, 1); %! assert_equal (idx{1}, [1]); %! assert_equal (D{1}, [0], 1e-10); %!test %! ## rangesearch with Chebychev %! X = [1, 1; 2, 3; 4, 2]; %! obj = KDTreeSearcher (X, 'Distance', 'chebychev'); %! Y = [2, 2]; %! [idx, D] = rangesearch (obj, Y, 1); %! assert_equal (sort (idx{1}(:))', [1, 2]); %! assert_equal (sort (D{1}(:))', [1, 1], 1e-10); %!test %! ## rangesearch with Minkowski P=3 %! X = [0, 0; 1, 0; 2, 0]; %! obj = KDTreeSearcher (X, 'Distance', 'minkowski', 'P', 3); %! Y = [1, 0]; %! [idx, D] = rangesearch (obj, Y, 1); %! assert_equal (sort (idx{1}(:))', [1, 2, 3]); %! assert_equal (sort (D{1}(:))', [0, 1, 1], 1e-10); ## A Minkowski exponent of Inf is the Chebychev distance, as in MATLAB R2024a, ## and the searcher keeps reporting the metric and exponent it was given. %!test %! X = [1, 2; 4, 6; 0, 0; 3, 1; 5, 5; 2, 3]; %! obj = KDTreeSearcher (X, 'Distance', 'minkowski', 'P', Inf); %! [idx, D] = knnsearch (obj, [1, 1; 4, 4], 'K', 2); %! assert_equal (obj.Distance, 'minkowski'); %! assert_equal (obj.DistParameter, Inf); %! assert_equal (idx, [1, 3; 5, 2]); %! assert_equal (D, [1, 1; 1, 2]); %!test %! X = [1, 2; 4, 6; 0, 0; 3, 1; 5, 5; 2, 3]; %! obj = KDTreeSearcher (X); %! [idx, D] = knnsearch (obj, [1, 1; 4, 4], 'K', 2, 'Distance', 'minkowski', ... %! 'P', Inf); %! assert_equal (idx, [1, 3; 5, 2]); %! assert_equal (D, [1, 1; 1, 2]); %!test %! X = [1, 2; 4, 6; 0, 0; 3, 1; 5, 5; 2, 3]; %! obj = KDTreeSearcher (X, 'Distance', 'minkowski'); %! obj.DistParameter = Inf; %! [idx, D] = rangesearch (obj, [1, 1; 4, 4], 2); %! assert_equal (idx, {[1, 3, 4, 6]; [5, 2, 6]}); %! assert_equal (D, {[1, 1, 2, 2]; [1, 2, 2]}); %!test %! ## Diverse dataset with Euclidean %! X = [0, 10; 5, 5; 10, 0]; %! obj = KDTreeSearcher (X); %! Y = [5, 5]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (idx, 2); %! assert_equal (D, 0, 1e-10); %!test %! ## High-dimensional data with Cityblock %! X = [1, 2, 3; 4, 5, 6; 7, 8, 9]; %! obj = KDTreeSearcher (X, 'Distance', 'cityblock'); %! Y = [4, 5, 6]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (idx, 2); %! assert_equal (D, 0, 1e-10); ## Test Input Validation %!test %! ## Each cell holds one row per query point, as MATLAB returns and as %! ## ExhaustiveSearcher does; this searcher used to return columns, so a %! ## caller could not swap one searcher for the other. %! X = [1, 1; 2, 2; 3, 3; 4, 4; 5, 5; 1, 5; 5, 1]; %! Y = [2, 2; 4, 4]; %! [idx, D] = rangesearch (KDTreeSearcher (X), Y, 2); %! assert_equal (size (idx), [2, 1]); %! assert_equal (rows (idx{1}), 1); %! assert_equal (rows (D{1}), 1); %! [ie, de] = rangesearch (ExhaustiveSearcher (X), Y, 2); %! assert_equal (idx, ie); %! assert_equal (D, de, 1e-12); %!test %! ## knnsearch with ties returns cells in the same orientation %! X = [1, 1; 2, 2; 3, 3; 4, 4; 5, 5; 1, 5; 5, 1]; %! Y = [2, 2; 4, 4]; %! [idx, D] = knnsearch (KDTreeSearcher (X), Y, 'K', 2, 'IncludeTies', true); %! assert_equal (rows (idx{1}), 1); %! assert_equal (rows (D{1}), 1); %!test %! ## Distance and P may be overridden per call: the tree is built from the %! ## data alone, so only the pruning bound changes. The object is unchanged. %! X = [1, 1; 2, 2; 3, 3; 4, 4; 5, 5; 1, 5; 5, 1]; Y = [2, 2; 4, 4]; %! o = KDTreeSearcher (X); %! assert_equal (knnsearch (o, Y, 'K', 3, 'Distance', 'cityblock'), ... %! knnsearch (KDTreeSearcher (X, 'Distance', 'cityblock'), ... %! Y, 'K', 3)); %! assert_equal (rangesearch (o, Y, 3, 'Distance', 'cityblock'), ... %! rangesearch (KDTreeSearcher (X, 'Distance', 'cityblock'), ... %! Y, 3)); %! assert_equal (o.Distance, 'euclidean'); %!error ... %! knnsearch (KDTreeSearcher ([1, 1; 2, 2]), [1, 1], 'K', 1, 'Cov', eye (2)) %!error ... %! knnsearch (KDTreeSearcher ([1, 1; 2, 2]), [1, 1], 'K', 1, 'Scale', [1, 1]) %!error ... %! KDTreeSearcher () %!error ... %! KDTreeSearcher (ones (3,2), 'Distance') %!error ... %! KDTreeSearcher ('abc') %!error ... %! KDTreeSearcher ([1; Inf; 3]) %!error ... %! KDTreeSearcher (ones (3,2), 'foo', 'bar') %!error ... %! KDTreeSearcher (ones (3,2), 'Distance', 'invalid') %!error ... %! KDTreeSearcher (ones (3,2), 'Distance', 1) %!error ... %! KDTreeSearcher (ones (3,2), 'Distance', 'minkowski', 'P', -1) %!error ... %! KDTreeSearcher (ones (3,2), 'P', 3) %!error ... %! KDTreeSearcher (ones (3,2), 'Distance', 'chebychev', 'P', 3) %!error ... %! KDTreeSearcher (ones (3,2), 'BucketSize', 0) %!error ... %! KDTreeSearcher (ones (3,2), 'BucketSize', -1) %!error ... %! knnsearch (KDTreeSearcher (ones (3,2))) %!error ... %! knnsearch (KDTreeSearcher (ones (3,2)), ones (3,2), 'K', 1, 'IncludeTies') %!error ... %! knnsearch (KDTreeSearcher (ones (3,2)), 'abc', 'K', 1) %!error ... %! knnsearch (KDTreeSearcher (ones (3,2)), ones (3,3), 'K', 1) %!error ... %! knnsearch (KDTreeSearcher (ones (3,2)), ones (3,2), 'K', 0) %!error ... %! obj = KDTreeSearcher (ones (3,2)); knnsearch (obj, ones (1,2), 'K', Inf) %!error ... %! knnsearch (KDTreeSearcher (ones (3,2)), ones (3,2), 'K', 1, 'foo', 'bar') %!error ... %! knnsearch (KDTreeSearcher (ones (3,2)), ones (3,2), 'K', 1, 'IncludeTies', 1) %!error ... %! knnsearch (KDTreeSearcher (ones (3,2)), ones (3,2), 'K', 1, 'SortIndices', 1) %!error ... %! rangesearch (KDTreeSearcher (ones (3,2))) %!error ... %! rangesearch (KDTreeSearcher (ones (3,2)), ones (3,2), 1, 'SortIndices') %!error ... %! rangesearch (KDTreeSearcher (ones (3,2)), 'abc', 1) %!error ... %! rangesearch (KDTreeSearcher (ones (3,2)), ones (3,3), 1) %!error ... %! rangesearch (KDTreeSearcher (ones (3,2)), ones (3,2), -1) %!error ... %! obj = KDTreeSearcher (ones (3,2)); rangesearch (obj, ones (1,2), Inf) %!error ... %! rangesearch (KDTreeSearcher (ones (3,2)), ones (3,2), 1, 'foo', 'bar') %!error ... %! rangesearch (KDTreeSearcher (ones (3,2)), ones (3,2), 1, 'SortIndices', 1) %!error ... %! obj = KDTreeSearcher (ones (3,2)); obj(1) %!error ... %! obj = KDTreeSearcher (ones (3,2)); obj{1} %!error ... %! obj = KDTreeSearcher (ones (3,2)); obj.invalid %!error ... %! obj = KDTreeSearcher (ones (3,2)); obj(1) = 1 %!error ... %! obj = KDTreeSearcher (ones (3,2)); obj{1} = 1 %!error ... %! obj = KDTreeSearcher (ones (3,2)); obj.X.Y = 1 %!error ... %! obj = KDTreeSearcher (ones (3,2)); obj.X = 1 %!error ... %! obj = KDTreeSearcher (ones (3,2)); obj.KDTree = 1 %!error ... %! obj = KDTreeSearcher (ones (3,2)); obj.Distance = 'invalid' %!error ... %! obj = KDTreeSearcher (ones (3,2)); obj.Distance = 1 %!error ... %! obj = KDTreeSearcher (ones (3,2), 'Distance', 'minkowski'); obj.DistParameter = -1 %!error ... %! obj = KDTreeSearcher (ones (3,2)); obj.DistParameter = 1 %!error ... %! obj = KDTreeSearcher (ones (3,2)); obj.BucketSize = 0 %!error ... %! obj = KDTreeSearcher (ones (3,2)); obj.BucketSize = -1 %!error ... %! obj = KDTreeSearcher (ones (3,2)); obj.BucketSize = 1.5 %!error ... %! obj = KDTreeSearcher (ones (3,2)); obj.invalid = 1 ## More neighbours than there are points is answered with all of them, where ## it used to raise an internal nonconformance. MATLAB caps at the sample. %!test %! randn ("seed", 4); %! X = randn (20, 2); Y = randn (5, 2); %! kd = KDTreeSearcher (X); %! for K = [20, 21, 100] %! [idx, D] = knnsearch (kd, Y, "K", K); %! assert_equal (size (idx), [5, 20]); %! assert_equal (size (D), [5, 20]); %! assert_equal (any (isnan (idx(:))), false); %! endfor %! ## and it agrees with the exhaustive answer %! [~, Dk] = knnsearch (kd, Y, "K", 100); %! [~, De] = knnsearch (ExhaustiveSearcher (X), Y, "K", 100); %! assert_equal (Dk, De, 1e-12); ## Single precision is carried through to the distances, every other class is ## converted up to double, and the indices are always double. %!test %! X = [1, 2; 3, 4; 5, 6; 7, 8]; Y = [2, 3; 6, 7]; %! obj = KDTreeSearcher (single (X)); %! assert_equal (class (obj.X), 'single'); %! [idx, D] = knnsearch (obj, single (Y), "K", 2); %! assert_equal (class (idx), 'double'); %! assert_equal (class (D), 'single'); %! ## a double query against single data is still computed in single %! [~, Dm] = knnsearch (obj, Y, "K", 2); %! assert_equal (class (Dm), 'single'); %! assert_equal (Dm, D); %! ## and so is a single query against double data %! [~, Ds] = knnsearch (KDTreeSearcher (X), single (Y), "K", 2); %! assert_equal (class (Ds), 'single'); %! ## double throughout stays double %! [~, Dd] = knnsearch (KDTreeSearcher (X), Y, "K", 2); %! assert_equal (class (Dd), 'double'); ## Integer data is converted to double on the way in. Held as int32 the ## coordinate differences were rounded and the distances came out wrong. %!test %! X = int32 ([10, 20; 33, 41; 55, 62]); Y = [21, 33]; %! obj = KDTreeSearcher (X); %! assert_equal (class (obj.X), 'double'); %! [~, D] = knnsearch (obj, Y, "K", 1); %! assert_equal (D, min (sqrt (sum ((double (X) - Y) .^ 2, 2))), 1e-12); ## rangesearch follows the same rule: single distances, double indices. %!test %! X = [1, 2; 3, 4; 5, 6; 7, 8]; Y = [2, 3]; %! [idx, D] = rangesearch (KDTreeSearcher (single (X)), single (Y), 4); %! assert_equal (class (idx{1}), 'double'); %! assert_equal (class (D{1}), 'single'); statistics-release-2.1.2/inst/Nearest_Neighbors/createns.m000066400000000000000000000261141527311104300236650ustar00rootroot00000000000000## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{obj} =} createns (@var{X}) ## @deftypefnx {statistics} {@var{obj} =} createns (@var{X}, @var{name}, @var{value}, @dots{}) ## ## Create a nearest neighbor searcher object. ## ## @code{@var{obj} = createns (@var{X})} creates a nearest neighbor searcher ## object using the training data @var{X}. By default, it constructs a ## @code{KDTreeSearcher} object with the Euclidean distance metric. ## ## @code{@var{obj} = createns (@var{X}, @var{name}, @var{value}, @dots{})} ## allows customization of the searcher type and its properties through ## name-value pairs. The following name-value pair is supported to specify ## the searcher type: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'NSMethod'} @tab Specifies the nearest neighbor search ## method. Possible values are: ## @itemize @bullet ## @item @qcode{'exhaustive'}: Creates an @code{ExhaustiveSearcher} object. ## @item @qcode{'kdtree'}: Creates a @code{KDTreeSearcher} object. ## @item @qcode{'hnsw'}: Creates an @code{hnswSearcher} object. ## @end itemize ## By default it is @qcode{'kdtree'} when @var{X} has at most 10 columns, ## is not sparse, and the @qcode{'Distance'} is @qcode{'euclidean'}, ## @qcode{'cityblock'}, @qcode{'chebychev'} or @qcode{'minkowski'}, and ## @qcode{'exhaustive'} otherwise. @qcode{'BucketSize'} is then ignored, ## with a warning, where the columns or sparsity rule out the kd-tree. ## ## @end multitable ## ## Additional name-value pairs depend on the selected @qcode{'NSMethod'} and ## are passed directly to the constructor of the corresponding class: ## ## @itemize @bullet ## @item For @qcode{'exhaustive'}, see @code{ExhaustiveSearcher} documentation ## for parameters like @qcode{'Distance'}, @qcode{'P'}, @qcode{'Scale'}, and ## @qcode{'Cov'}. ## @item For @qcode{'kdtree'}, see @code{KDTreeSearcher} documentation for ## parameters like @qcode{'Distance'}, @qcode{'P'}, and @qcode{'BucketSize'}. ## @item For @qcode{'hnsw'}, see @code{hnswSearcher} documentation for ## parameters ## like @qcode{'Distance'}, @qcode{'P'}, @qcode{'Scale'}, @qcode{'Cov'}, ## @qcode{'MaxNumLinksPerNode'}, and @qcode{'TrainSetSize'}. ## @end itemize ## ## @strong{Input Arguments:} ## @itemize @bullet ## @item @var{X} - Training data, specified as an @math{N*P} numeric matrix ## where rows represent observations and columns represent features. Must be ## finite and numeric. ## @end itemize ## ## @strong{Output:} ## @itemize @bullet ## @item @var{obj} - A nearest neighbor searcher object of type ## @code{ExhaustiveSearcher}, @code{KDTreeSearcher}, or @code{hnswSearcher}, ## depending on the specified @qcode{'NSMethod'}. ## @end itemize ## ## @strong{Examples:} ## ## @example ## ## Create a KDTreeSearcher with default parameters ## X = [1, 2; 3, 4; 5, 6]; ## obj = createns (X); ## ## ## Create a KDTreeSearcher with Euclidean distance ## obj = createns (X, "NSMethod", "kdtree", "Distance", "euclidean"); ## ## ## Create an hnswSearcher with Minkowski distance and custom parameters ## obj = createns (X, "NSMethod", "hnsw", "Distance", "minkowski", "P", 3, ... ## "MaxNumLinksPerNode", 2); ## @end example ## ## @seealso{ExhaustiveSearcher, KDTreeSearcher, hnswSearcher, knnsearch, ## rangesearch} ## @end deftypefn function obj = createns (X, varargin) ## Input validation if (nargin < 1) error ("createns: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error ("createns: Name-Value arguments must be in pairs."); endif if (! (isnumeric (X) && ismatrix (X) && all (isfinite (X)(:)))) error ("createns: X must be a finite numeric matrix."); endif ## Resolved below when not given NSMethod = []; ## Extract 'NSMethod' from varargin and remove it names = varargin(1:2:end); idx = find (strcmpi (names, 'nsmethod')); if (! isempty (idx)) if (length (idx) > 1) warning ("createns: multiple 'NSMethod' specified, using the last one."); endif NSMethod = varargin{2*idx(end)}; if (! ischar (NSMethod)) error ("createns: 'NSMethod' must be a string."); endif NSMethod = lower (NSMethod); remove_idx = [2*idx-1, 2*idx]; varargin(remove_idx) = []; endif ## With no 'NSMethod', a kd-tree is built where one can serve the data and ## the metric, as MATLAB does: at most 10 columns, not sparse, and a metric ## the kd-tree searches. if (isempty (NSMethod)) Distance = 'euclidean'; idx = find (strcmpi (varargin(1:2:end), 'distance')); if (! isempty (idx)) Distance = varargin{2*idx(end)}; endif if (columns (X) <= 10 && ! issparse (X) && ischar (Distance) && any (strcmpi (Distance, {'euclidean', 'cityblock', ... 'chebychev', 'minkowski'}))) NSMethod = 'kdtree'; else NSMethod = 'exhaustive'; ## 'BucketSize' belongs to the kd-tree, so where the data rules one out ## it is dropped with a warning, as MATLAB drops it idx = find (strcmpi (varargin(1:2:end), 'bucketsize')); if (! isempty (idx) && (columns (X) > 10 || issparse (X))) warning (strcat ("createns: 'BucketSize' is ignored, since an", ... " ExhaustiveSearcher is created for data with", ... " more than 10 columns or sparse data.")); varargin([2*idx-1, 2*idx]) = []; endif endif endif ## Validate NSMethod value allowed_methods = {'exhaustive', 'kdtree', 'hnsw'}; if (! any (strcmp (NSMethod, allowed_methods))) error ("createns: invalid 'NSMethod' value: '%s'.", NSMethod); endif ## Instantiate the appropriate searcher object switch (NSMethod) case 'exhaustive' obj = ExhaustiveSearcher (X, varargin{:}); case 'kdtree' obj = KDTreeSearcher (X, varargin{:}); case 'hnsw' obj = hnswSearcher (X, varargin{:}); endswitch endfunction ## Test Cases %!test %! ## Default KDTreeSearcher, as in MATLAB R2024a %! X = [1, 2; 3, 4; 5, 6]; %! obj = createns (X); %! assert_equal (isa (obj, 'KDTreeSearcher'), true); %! assert_equal (obj.X, X); %! assert_equal (obj.Distance, "euclidean"); ## The default method is the kd-tree where it can serve the data and the ## metric, and exhaustive otherwise, each as measured on MATLAB R2024a. %!assert_equal (class (createns (magic (12)(:,1:10))), 'KDTreeSearcher') %!assert_equal (class (createns (magic (12)(:,1:11))), 'ExhaustiveSearcher') %!assert_equal (class (createns (sparse (magic (4)))), 'ExhaustiveSearcher') %!assert_equal (class (createns (single (magic (4)))), 'KDTreeSearcher') %!assert_equal (class (createns (zeros (0, 2))), 'KDTreeSearcher') %!assert_equal (class (createns (magic (4), 'Distance', 'cityblock')), ... %! 'KDTreeSearcher') %!assert_equal (class (createns (magic (4), 'Distance', 'chebychev')), ... %! 'KDTreeSearcher') %!assert_equal (class (createns (magic (4), 'Distance', 'minkowski', ... %! 'P', 3)), 'KDTreeSearcher') %!assert_equal (class (createns (magic (4), 'Distance', 'EUCLIDEAN')), ... %! 'KDTreeSearcher') %!assert_equal (class (createns (magic (4), 'Distance', 'seuclidean')), ... %! 'ExhaustiveSearcher') %!assert_equal (class (createns (magic (4), 'Distance', 'mahalanobis')), ... %! 'ExhaustiveSearcher') %!assert_equal (class (createns (magic (4), 'Distance', 'cosine')), ... %! 'ExhaustiveSearcher') %!assert_equal (class (createns (magic (4), 'Distance', 'hamming')), ... %! 'ExhaustiveSearcher') %!assert_equal (class (createns (magic (4), 'Distance', ... %! @(x, Z) sum (abs (x - Z), 2))), ... %! 'ExhaustiveSearcher') %!assert_equal (class (createns (magic (12)(:,1:11), 'Distance', ... %! 'minkowski')), 'ExhaustiveSearcher') %!assert_equal (createns (magic (4), 'BucketSize', 10).BucketSize, 10) %!test %! S = warning ('off', 'all'); %! obj = createns (magic (12)(:,1:11), 'BucketSize', 10); %! warning (S); %! assert_equal (class (obj), 'ExhaustiveSearcher'); %!warning ... %! createns (magic (12)(:,1:11), 'BucketSize', 10); %!test %! ## KDTreeSearcher with default parameters %! X = [1, 2; 3, 4; 5, 6]; %! obj = createns (X, 'NSMethod', 'kdtree'); %! assert_equal (isa (obj, 'KDTreeSearcher'), true); %! assert_equal (obj.X, X); %! assert_equal (obj.Distance, "euclidean"); %!test %! ## hnswSearcher with custom parameters %! X = [1, 2; 3, 4; 5, 6]; %! obj = createns (X, 'NSMethod', 'hnsw', 'MaxNumLinksPerNode', 2, 'TrainSetSize', 3); %! assert_equal (isa (obj, 'hnswSearcher'), true); %! assert_equal (obj.X, X); %! assert_equal (obj.MaxNumLinksPerNode, 2); %! assert_equal (obj.TrainSetSize, 3); %!test %! ## ExhaustiveSearcher with custom distance %! X = [1, 2; 3, 4]; %! obj = createns (X, 'NSMethod', 'exhaustive', 'Distance', 'cityblock'); %! assert_equal (isa (obj, 'ExhaustiveSearcher'), true); %! assert_equal (obj.Distance, "cityblock"); %!test %! ## A Minkowski exponent of Inf is taken, as in MATLAB R2024a %! X = [1, 2; 4, 6; 0, 0; 3, 1; 5, 5; 2, 3]; %! obj = createns (X, 'NSMethod', 'kdtree', 'Distance', 'minkowski', 'P', Inf); %! assert_equal (isa (obj, 'KDTreeSearcher'), true); %! assert_equal (obj.DistParameter, Inf); %! assert_equal (knnsearch (obj, [1, 1; 4, 4], 'K', 2), [1, 3; 5, 2]); %!error ... %! createns (magic (4), 'NSMethod', 'exhaustive', 'P', 3) %!error ... %! createns (magic (4), 'P', 3) %!error ... %! createns (magic (4), 'NSMethod', 'exhaustive', 'Scale', [1, 1, 1, 1]) %!error ... %! createns (magic (4), 'NSMethod', 'exhaustive', 'Cov', eye (4)) %!error %! createns () %!error %! X = [1, 2; 3, 4]; createns (X, 'NSMethod') %!error %! createns ([1; Inf; 3]) %!error %! X = [1, 2; 3, 4]; createns (X, 'NSMethod', 1) %!error %! X = [1, 2; 3, 4]; createns (X, 'NSMethod', 'invalid') statistics-release-2.1.2/inst/Nearest_Neighbors/doc-cache000066400000000000000000001524051527311104300234370ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 32 # name: # type: sq_string # elements: 1 # length: 18 ExhaustiveSearcher # name: # type: sq_string # elements: 1 # length: 713 statistics: ExhaustiveSearcher Exhaustive nearest neighbor searcher The ExhaustiveSearcher class implements an exhaustive search algorithm for nearest neighbor queries. It stores training data and supports various distance metrics along with their parameter values for performing an exhaustive search. The exhaustive search algorithm computes the distance from each query point to all the points in the training data and facilitates a nearest neighbor search using knnsearch or a radius search using rangesearch. You can either use the ExhaustiveSearcher class constructor or the createns function to create an ExhaustiveSearcher object. See also: createns, KDTreeSearcher, hnswSearcher, knnsearch, rangesearch # name: # type: sq_string # elements: 1 # length: 36 Exhaustive nearest neighbor searcher # name: # type: sq_string # elements: 1 # length: 32 ExhaustiveSearcher.DistParameter # name: # type: sq_string # elements: 1 # length: 557 ExhaustiveSearcher: property DistParameter Distance parameter The type and value of the distance parameter depends on the selected Distance metric and can be any of the following: For 'minkowski', a positive scalar exponent (default 2); Inf gives the Chebychev distance. For 'seuclidean', a nonnegative vector of scaling factors matching the number of columns in X (default is standard deviation of X). For 'mahalanobis', a positive definite covariance matrix matching the dimensions of X (default is cov (X)). Empty for other metrics or custom functions. # name: # type: sq_string # elements: 1 # length: 18 Distance parameter # name: # type: sq_string # elements: 1 # length: 27 ExhaustiveSearcher.Distance # name: # type: sq_string # elements: 1 # length: 276 ExhaustiveSearcher: property Distance Distance metric Distance metric used for searches, specified as a character vector (e.g., 'euclidean', 'minkowski') or a function handle to a custom distance function. Default is 'euclidean'. Supported metrics align with those in pdist2. # name: # type: sq_string # elements: 1 # length: 15 Distance metric # name: # type: sq_string # elements: 1 # length: 37 ExhaustiveSearcher.ExhaustiveSearcher # name: # type: sq_string # elements: 1 # length: 1520 ExhaustiveSearcher: obj = ExhaustiveSearcher (X) ExhaustiveSearcher: obj = ExhaustiveSearcher (X, name, value) Create an ExhaustiveSearcher object for nearest neighbor searches. obj = ExhaustiveSearcher (X) constructs an ExhaustiveSearcher object with training data X using the default 'euclidean' distance metric. X must be an NxP numeric matrix, where rows represent observations and columns represent features. obj = ExhaustiveSearcher (X, name, value) allows customization through name-value pairs: Name Value 'Distance' Distance metric, specified as a character vector (e.g., 'euclidean', 'minkowski') or a function handle. Default is 'euclidean'. See pdist2 for supported metrics. 'P' a positive scalar specifying the exponent for the Minkowski distance. Valid only when 'Distance' is 'minkowski'. Default is 2. An exponent of Inf gives the Chebychev distance. 'Scale' a nonnegative vector with the same number of elements as the columns in X specifying the scale parameter for the standardized Euclidean distance. Valid only when 'Distance' is 'seuclidean'. Default is std (X). 'Cov' a positive definite matrix matching the number of columns in X specifying the covariance matrix for the Mahalanobis distance. Valid only when 'Distance' is 'mahalanobis'. Default is cov (X). 'Distance', 'P', 'Cov' and 'Scale' override the searcher's own metric for that call only; the Distance and DistParameter properties keep their values, as they do in MATLAB. See also: ExhaustiveSearcher, knnsearch, rangesearch, pdist2 # name: # type: sq_string # elements: 1 # length: 66 Create an ExhaustiveSearcher object for nearest neighbor searches. # name: # type: sq_string # elements: 1 # length: 20 ExhaustiveSearcher.X # name: # type: sq_string # elements: 1 # length: 337 ExhaustiveSearcher: property X Point data Point data, specified as an NxP numeric matrix where each row is an observation and each column is a feature. This property is private and cannot be modified after object creation. Data of class single is stored and searched in single precision, any other numeric class is converted to double. # name: # type: sq_string # elements: 1 # length: 10 Point data # name: # type: sq_string # elements: 1 # length: 28 ExhaustiveSearcher.knnsearch # name: # type: sq_string # elements: 1 # length: 1319 ExhaustiveSearcher: [idx, D] = knnsearch (obj, Y) ExhaustiveSearcher: [idx, D] = knnsearch (obj, Y, name, value) Find the K nearest neighbors in the training data to query points. [idx, D] = knnsearch (obj, Y) returns the indices idx and distances D of the nearest neighbor in obj.X to each point in Y, using the distance metric specified in obj.Distance. obj is an ExhaustiveSearcher object. Y is an MxP numeric matrix of query points, where P must match the number of columns in obj.X. idx is always of class double. D is of class single when either obj.X or Y is single, in which case the distances are computed in single precision, and of class double otherwise. [idx, D] = knnsearch (obj, Y, name, value) allows additional options via name-value pairs: Name Value 'K' A positive integer specifying the number of nearest neighbors to find. Default is 1. A value larger than the number of observations in the training data is answered with all of them, since there are no more neighbors to return. 'IncludeTies' Logical flag indicating whether to include all neighbors tied with the Kth smallest distance. Default is false. If true, idx and D are cell arrays. idx contains the indices of the nearest neighbors in obj.X. D contains the corresponding distances. See also: ExhaustiveSearcher, rangesearch, pdist2 # name: # type: sq_string # elements: 1 # length: 66 Find the K nearest neighbors in the training data to query points. # name: # type: sq_string # elements: 1 # length: 30 ExhaustiveSearcher.rangesearch # name: # type: sq_string # elements: 1 # length: 1093 ExhaustiveSearcher: [idx, D] = rangesearch (obj, Y, r) ExhaustiveSearcher: [idx, D] = rangesearch (obj, Y, r, name, value) Find all neighbors within a specified radius of query points. [idx, D] = rangesearch (obj, Y, r) returns the indices idx and distances D of all points in obj.X within radius r of each point in Y, using the distance metric specified in obj.Distance. obj is an ExhaustiveSearcher object. Y is an MxP numeric matrix of query points, where P must match the number of columns in obj.X. r is a nonnegative scalar specifying the search radius. idx is always of class double. D is of class single when either obj.X or Y is single, in which case the distances are computed in single precision, and of class double otherwise. [idx, D] = rangesearch (..., name, value) allows additional options via name-value pairs: Name Value 'SortIndices' Logical flag indicating whether to sort the indices by distance. Default is true. idx and D are cell arrays where each cell contains the indices and distances for one query point in Y. See also: ExhaustiveSearcher, knnsearch, pdist2 # name: # type: sq_string # elements: 1 # length: 61 Find all neighbors within a specified radius of query points. # name: # type: sq_string # elements: 1 # length: 14 KDTreeSearcher # name: # type: sq_string # elements: 1 # length: 760 statistics: KDTreeSearcher KD-tree nearest neighbor searcher The KDTreeSearcher class implements a KD-tree search algorithm for nearest neighbor queries. It stores training data and supports various distance metrics along with their parameter values for performing a KD-tree search. The KD-tree algorithm partitions the training data into a hierarchical tree structure and performs search operations by traversing the tree to reduce the number of distance computations. It facilitates nearest neighbor queries using knnsearch and radius queries using rangesearch. You can either use the KDTreeSearcher class constructor or the createns function to create an KDTreeSearcher object. See also: createns, ExhaustiveSearcher, hnswSearcher, knnsearch, rangesearch # name: # type: sq_string # elements: 1 # length: 33 KD-tree nearest neighbor searcher # name: # type: sq_string # elements: 1 # length: 25 KDTreeSearcher.BucketSize # name: # type: sq_string # elements: 1 # length: 243 KDTreeSearcher: property BucketSize Maximum number of data points in each leaf node The maximum number of data points in the leaf node of the KD-tree. Default value is 50. This property is private and cannot be modified after object creation. # name: # type: sq_string # elements: 1 # length: 47 Maximum number of data points in each leaf node # name: # type: sq_string # elements: 1 # length: 28 KDTreeSearcher.DistParameter # name: # type: sq_string # elements: 1 # length: 412 KDTreeSearcher: property DistParameter Distance parameter The type and value of the distance parameter depends on the selected Distance metric and can be any of the following: For 'minkowski', a positive scalar exponent (default 2); Inf gives the Chebychev distance. Empty for other metrics ('euclidean', 'cityblock', 'chebychev'). Attempting to set a non-empty value for these metrics will result in an error. # name: # type: sq_string # elements: 1 # length: 18 Distance parameter # name: # type: sq_string # elements: 1 # length: 23 KDTreeSearcher.Distance # name: # type: sq_string # elements: 1 # length: 226 KDTreeSearcher: property Distance Distance metric Distance metric used for searches, specified as a character vector. Supported metrics are 'euclidean', 'cityblock', 'minkowski', and 'chebychev'. Default value is 'euclidean'. # name: # type: sq_string # elements: 1 # length: 15 Distance metric # name: # type: sq_string # elements: 1 # length: 29 KDTreeSearcher.KDTreeSearcher # name: # type: sq_string # elements: 1 # length: 1332 KDTreeSearcher: obj = KDTreeSearcher (X) KDTreeSearcher: obj = KDTreeSearcher (X, name, value) Create a KDTreeSearcher object for nearest neighbor searches. obj = KDTreeSearcher (X) constructs a KDTreeSearcher object with training data X using the default 'euclidean' distance metric. X must be an NxP numeric matrix, where rows represent observations and columns represent features. obj = KDTreeSearcher (X, name, value) allows customization through name-value pairs: Name Value 'Distance' Distance metric, specified as a character vector ('euclidean', 'cityblock', 'minkowski', 'chebychev'). Default is 'euclidean'. 'P' Minkowski distance exponent, a positive scalar. Valid only when 'Distance' is 'minkowski'. Default is 2. An exponent of Inf gives the Chebychev distance. 'BucketSize' Maximum number of data points in the leaf node of the KD-tree, a positive integer. Default is 50. You can also create a KDTreeSearcher object using the createns function. 'Distance' and 'P' override the searcher's own metric for that call only; the Distance and DistParameter properties keep their values. The tree is built from the data alone, so changing the metric does not rebuild it. 'Cov' and 'Scale' are not accepted, since they belong to metrics a kd-tree cannot search. See also: KDTreeSearcher, knnsearch, rangesearch, createns # name: # type: sq_string # elements: 1 # length: 61 Create a KDTreeSearcher object for nearest neighbor searches. # name: # type: sq_string # elements: 1 # length: 16 KDTreeSearcher.X # name: # type: sq_string # elements: 1 # length: 333 KDTreeSearcher: property X Point data Point data, specified as an NxP numeric matrix where each row is an observation and each column is a feature. This property is private and cannot be modified after object creation. Data of class single is stored and searched in single precision, any other numeric class is converted to double. # name: # type: sq_string # elements: 1 # length: 10 Point data # name: # type: sq_string # elements: 1 # length: 24 KDTreeSearcher.knnsearch # name: # type: sq_string # elements: 1 # length: 1395 KDTreeSearcher: [idx, D] = knnsearch (obj, Y) KDTreeSearcher: [idx, D] = knnsearch (obj, Y, name, value) Find the K nearest neighbors in the training data to query points. [idx, D] = knnsearch (obj, Y, K) returns the indices idx and distances D of the K nearest neighbors in obj.X to each point in Y, using the distance metric specified in obj.Distance. obj is a KDTreeSearcher object. Y is an MxP numeric matrix of query points, where P must match the number of columns in obj.X. idx contains the indices of the nearest neighbors in obj.X. D contains the corresponding distances. idx is always of class double. D is of class single when either obj.X or Y is single, in which case the distances are computed in single precision, and of class double otherwise. [idx, D] = knnsearch (obj, Y, name, value) allows additional options via name-value pairs: Name Value 'K' A positive integer specifying the number of nearest neighbors to find. Default is 1. A value larger than the number of observations in the training data is answered with all of them, since there are no more neighbors to return. 'IncludeTies' Logical flag indicating whether to include all neighbors tied with the Kth smallest distance. Default is false. If true, idx and D are cell arrays. 'SortIndices' Logical flag indicating whether to sort the indices by distance. Default is true. See also: KDTreeSearcher, rangesearch # name: # type: sq_string # elements: 1 # length: 66 Find the K nearest neighbors in the training data to query points. # name: # type: sq_string # elements: 1 # length: 26 KDTreeSearcher.rangesearch # name: # type: sq_string # elements: 1 # length: 1074 KDTreeSearcher: [idx, D] = rangesearch (obj, Y, r) KDTreeSearcher: [idx, D] = rangesearch (obj, Y, r, name, value) Find all neighbors within a specified radius of query points. [idx, D] = rangesearch (obj, Y, r) returns the indices idx and distances D of all points in obj.X within radius r of each point in Y, using the distance metric specified in obj.Distance. obj is a KDTreeSearcher object. Y is an MxP numeric matrix of query points, where P must match the number of columns in obj.X. r is a nonnegative scalar specifying the search radius. idx is always of class double. D is of class single when either obj.X or Y is single, in which case the distances are computed in single precision, and of class double otherwise. [idx, D] = rangesearch (obj, Y, r, name, value) allows additional options via name-value pairs: Name Value 'SortIndices' Logical flag indicating whether to sort the indices by distance. Default is true. idx and D are cell arrays where each cell contains the indices and distances for one query point in Y. See also: KDTreeSearcher, knnsearch # name: # type: sq_string # elements: 1 # length: 61 Find all neighbors within a specified radius of query points. # name: # type: sq_string # elements: 1 # length: 8 createns # name: # type: sq_string # elements: 1 # length: 2287 statistics: obj = createns (X) statistics: obj = createns (X, name, value, ...) Create a nearest neighbor searcher object. obj = createns (X) creates a nearest neighbor searcher object using the training data X. By default, it constructs a KDTreeSearcher object with the Euclidean distance metric. obj = createns (X, name, value, ...) allows customization of the searcher type and its properties through name-value pairs. The following name-value pair is supported to specify the searcher type: Name Value 'NSMethod' Specifies the nearest neighbor search method. Possible values are: 'exhaustive': Creates an ExhaustiveSearcher object. 'kdtree': Creates a KDTreeSearcher object. 'hnsw': Creates an hnswSearcher object. By default it is 'kdtree' when X has at most 10 columns, is not sparse, and the 'Distance' is 'euclidean', 'cityblock', 'chebychev' or 'minkowski', and 'exhaustive' otherwise. 'BucketSize' is then ignored, with a warning, where the columns or sparsity rule out the kd-tree. Additional name-value pairs depend on the selected 'NSMethod' and are passed directly to the constructor of the corresponding class: For 'exhaustive', see ExhaustiveSearcher documentation for parameters like 'Distance', 'P', 'Scale', and 'Cov'. For 'kdtree', see KDTreeSearcher documentation for parameters like 'Distance', 'P', and 'BucketSize'. For 'hnsw', see hnswSearcher documentation for parameters like 'Distance', 'P', 'Scale', 'Cov', 'MaxNumLinksPerNode', and 'TrainSetSize'. Input Arguments: X - Training data, specified as an NxP numeric matrix where rows represent observations and columns represent features. Must be finite and numeric. Output: obj - A nearest neighbor searcher object of type ExhaustiveSearcher, KDTreeSearcher, or hnswSearcher, depending on the specified 'NSMethod'. Examples: ## Create a KDTreeSearcher with default parameters X = [1, 2; 3, 4; 5, 6]; obj = createns (X); ## Create a KDTreeSearcher with Euclidean distance obj = createns (X, "NSMethod", "kdtree", "Distance", "euclidean"); ## Create an hnswSearcher with Minkowski distance and custom parameters obj = createns (X, "NSMethod", "hnsw", "Distance", "minkowski", "P", 3, ... "MaxNumLinksPerNode", 2); See also: ExhaustiveSearcher, KDTreeSearcher, hnswSearcher, knnsearch, rangesearch # name: # type: sq_string # elements: 1 # length: 42 Create a nearest neighbor searcher object. # name: # type: sq_string # elements: 1 # length: 12 hnswSearcher # name: # type: sq_string # elements: 1 # length: 669 statistics: hnswSearcher Hierarchical Navigable Small World (HNSW) nearest neighbor searcher class. The hnswSearcher class implements the HNSW algorithm for efficient nearest neighbor queries. It stores training data and supports various distance metrics for performing searches. The HNSW algorithm builds a multilayer graph structure that enables fast approximate nearest neighbor searches by navigating through the graph. It facilitates nearest neighbor queries search using knnsearch. You can either use the hnswSearcher class constructor or the createns function to create an hnswSearcher object. See also: createns, ExhaustiveSearcher, KDTreeSearcher, knnsearch # name: # type: sq_string # elements: 1 # length: 74 Hierarchical Navigable Small World (HNSW) nearest neighbor searcher class. # name: # type: sq_string # elements: 1 # length: 26 hnswSearcher.DistParameter # name: # type: sq_string # elements: 1 # length: 603 hnswSearcher: property DistParameter Distance parameter The type and value of the distance parameter depends on the selected Distance metric and can be any of the following: For 'minkowski', a positive scalar exponent (default 2); Inf gives the Chebychev distance. For 'seuclidean', a nonnegative vector of scaling factors matching the number of columns in X (default is standard deviation of X). For 'mahalanobis', a positive definite covariance matrix matching the dimensions of X (default is cov (X)). Empty for other metrics. This property is private and cannot be modified after object creation. # name: # type: sq_string # elements: 1 # length: 18 Distance parameter # name: # type: sq_string # elements: 1 # length: 21 hnswSearcher.Distance # name: # type: sq_string # elements: 1 # length: 303 hnswSearcher: property Distance Distance metric Distance metric used for searches, specified as a character vector (e.g., 'euclidean', 'minkowski', 'cityblock'). Default is 'euclidean'. Supported metrics align with those in pdist2. This property is private and cannot be modified after object creation. # name: # type: sq_string # elements: 1 # length: 15 Distance metric # name: # type: sq_string # elements: 1 # length: 31 hnswSearcher.MaxNumLinksPerNode # name: # type: sq_string # elements: 1 # length: 282 hnswSearcher: property MaxNumLinksPerNode Number of connections created for each node Maximum number of neighbors per node in the HNSW graph. Affects graph connectivity and search accuracy. Default value is 16. This property is private and cannot be modified after object creation. # name: # type: sq_string # elements: 1 # length: 43 Number of connections created for each node # name: # type: sq_string # elements: 1 # length: 25 hnswSearcher.TrainSetSize # name: # type: sq_string # elements: 1 # length: 294 hnswSearcher: property TrainSetSize Number of potential nearest neighbors Size of the dynamic candidate list during graph construction. Higher values improve accuracy at the cost of construction time. Default value is 200. This property is private and cannot be modified after object creation. # name: # type: sq_string # elements: 1 # length: 37 Number of potential nearest neighbors # name: # type: sq_string # elements: 1 # length: 14 hnswSearcher.X # name: # type: sq_string # elements: 1 # length: 331 hnswSearcher: property X Point data Point data, specified as an NxP numeric matrix where each row is an observation and each column is a feature. This property is private and cannot be modified after object creation. Data of class single is stored and searched in single precision, any other numeric class is converted to double. # name: # type: sq_string # elements: 1 # length: 10 Point data # name: # type: sq_string # elements: 1 # length: 25 hnswSearcher.hnswSearcher # name: # type: sq_string # elements: 1 # length: 1411 hnswSearcher: obj = hnswSearcher (X) hnswSearcher: obj = hnswSearcher (X, name, value) Create an hnswSearcher object for approximate nearest neighbor searches. obj = hnswSearcher (X) constructs an hnswSearcher object with training data X using the default 'euclidean' distance metric. X must be an NxP numeric matrix, where rows represent observations and columns represent features. obj = hnswSearcher (X, name, value) allows customization through name-value pairs: Name Value 'Distance' Distance metric, specified as a character vector (e.g., 'euclidean', 'minkowski', 'cityblock'). Default is 'euclidean'. See pdist2 for supported metrics. 'P' Minkowski distance exponent, a positive scalar. Valid only when 'Distance' is 'minkowski'. Default is 2. An exponent of Inf gives the Chebychev distance, where MATLAB returns wrong neighbours. 'Scale' Nonnegative vector of scaling factors matching the number of columns in X. Valid only when 'Distance' is 'seuclidean'. Default is std (X). 'Cov' Positive definite covariance matrix matching the number of columns in X. Valid only when 'Distance' is 'mahalanobis'. Default is cov (X). 'MaxNumLinksPerNode' Maximum number of neighbors per node in the HNSW graph, a positive integer. Default is 16. 'TrainSetSize' Size of the dynamic candidate list during graph construction, a positive integer. Default is 200. See also: hnswSearcher, knnsearch, createns, pdist2 # name: # type: sq_string # elements: 1 # length: 72 Create an hnswSearcher object for approximate nearest neighbor searches. # name: # type: sq_string # elements: 1 # length: 22 hnswSearcher.knnsearch # name: # type: sq_string # elements: 1 # length: 1429 hnswSearcher: [idx, D] = knnsearch (obj, Y) hnswSearcher: [idx, D] = knnsearch (obj, Y, name, value) Find the nearest neighbors in the training data to query points. [idx, D] = knnsearch (obj, Y) returns the indices idx and distances D of the nearest neighbor in obj.X to each point in Y, using the distance metric specified in obj.Distance. obj is an hnswSearcher object. Y is an MxP numeric matrix of query points, where P must match the number of columns in obj.X. idx contains the indices of the nearest neighbors in obj.X. D contains the corresponding distances. idx is always of class double. D is of class single when either obj.X or Y is single, in which case the distances are computed in single precision, and of class double otherwise. [idx, D] = knnsearch (obj, Y, name, value) allows additional options via name-value pairs: Name Value 'K' A positive integer specifying the number of nearest neighbors to find. Default is 1. A value larger than the number of observations in the training data is answered with all of them, since there are no more neighbors to return. 'SearchSetSize' A positive integer specifying the size of the candidate list of nearest neighbors for a single query point during the search process. Default is max (10, C), where C is the number of columns in obj.X. 'SearchSetSize' must be at least C and no more than the number of rows in training data obj.X. See also: hnswSearcher, pdist2 # name: # type: sq_string # elements: 1 # length: 64 Find the nearest neighbors in the training data to query points. # name: # type: sq_string # elements: 1 # length: 9 knnsearch # name: # type: sq_string # elements: 1 # length: 5182 statistics: idx = knnsearch (X, Y) statistics: [idx, D] = knnsearch (X, Y) statistics: [...] = knnsearch (..., name, value) Find k-nearest neighbors from input data. idx = knnsearch (X, Y) finds K nearest neighbors in X for Y. It returns idx which contains indices of K nearest neighbors of each row of Y, If not specified, K = 1. X must be an NxP numeric matrix of input data, where rows correspond to observations and columns correspond to features or variables. Y is an MxP numeric matrix with query points, which must have the same numbers of column as X. [idx, D] = knnsearch (X, Y) also returns the the distances, D, which correspond to the K nearest neighbour in X for each Y Additional parameters can be specified by Name-Value pair arguments. Name Value 'K' is the number of nearest neighbors to be found in the kNN search. It must be a positive integer value and by default it is 1. 'P' is the Minkowski distance exponent and it must be a positive scalar. This argument is only valid when the selected distance metric is 'minkowski'. By default it is 2. An exponent of Inf gives the Chebychev distance. 'Scale' is the scale parameter for the standardized Euclidean distance and it must be a nonnegative numeric vector of equal length to the number of columns in X. This argument is only valid when the selected distance metric is 'seuclidean', in which case each coordinate of X is scaled by the corresponding element of 'scale', as is each query point in Y. By default, the scale parameter is the standard deviation of each coordinate in X. 'Cov' is the covariance matrix for computing the mahalanobis distance and it must be a positive definite matrix matching the the number of columns in X. This argument is only valid when the selected distance metric is 'mahalanobis'. 'BucketSize' is the maximum number of data points in the leaf node of the Kd-tree and it must be a positive integer. This argument is only valid when the selected search method is 'kdtree'. 'SortIndices' is a boolean flag to sort the returned indices in ascending order by distance and it is true by default. When the selected search method is 'exhaustive' or the 'IncludeTies' flag is true, knnsearch always sorts the returned indices. 'Distance' is the distance metric used by knnsearch as specified below: 'euclidean' Euclidean distance. 'seuclidean' standardized Euclidean distance. Each coordinate difference between the rows in X and the query matrix Y is scaled by dividing by the corresponding element of the standard deviation computed from X. To specify a different scaling, use the 'Scale' name-value argument. 'cityblock' City block distance. 'chebychev' Chebychev distance (maximum coordinate difference). A difference of Inf - Inf is NaN, and so is the distance, as for every other metric; MATLAB passes over it. 'minkowski' Minkowski distance. The default exponent is 2. To specify a different exponent, use the 'P' name-value argument. 'mahalanobis' Mahalanobis distance, computed using a positive definite covariance matrix. To change the value of the covariance matrix, use the 'Cov' name-value argument. 'cosine' Cosine distance. 'correlation' One minus the sample linear correlation between observations (treated as sequences of values). 'spearman' One minus the sample Spearman's rank correlation between observations (treated as sequences of values). 'hamming' Hamming distance, which is the percentage of coordinates that differ. 'jaccard' One minus the Jaccard coefficient, which is the percentage of nonzero coordinates that differ. @distfun Custom distance function handle. A distance function of the form function D2 = distfun (XI, YI), where XI is a 1xP vector containing a single observation in P-dimensional space, YI is an NxP matrix containing an arbitrary number of observations in the same P-dimensional space, and D2 is an NxP vector of distances, where (D2k) is the distance between observations XI and (YIk,:). 'NSMethod' is the nearest neighbor search method used by knnsearch as specified below. 'kdtree' Creates and uses a Kd-tree to find nearest neighbors. 'kdtree' is the default value when the number of columns in X is less than or equal to 10, X is not sparse, and the distance metric is 'euclidean', 'cityblock', 'manhattan', 'chebychev', or 'minkowski'. Otherwise, the default value is 'exhaustive'. This argument is only valid when the distance metric is one of the four aforementioned metrics. 'exhaustive' Uses the exhaustive search algorithm by computing the distance values from all the points in X to each point in Y. 'IncludeTies' is a boolean flag to indicate if the returned values should contain the indices that have same distance as the K^th neighbor. When false, knnsearch chooses the observation with the smallest index among the observations that have the same distance from a query point. When true, knnsearch includes all nearest neighbors whose distances are equal to the K^th smallest distance in the output arguments. To specify K, use the 'K' name-value pair argument. In that case idx and D are M-by-1 cell arrays and each cell holds a row vector, whichever search method is used. See also: rangesearch, pdist2, fitcknn # name: # type: sq_string # elements: 1 # length: 41 Find k-nearest neighbors from input data. # name: # type: sq_string # elements: 1 # length: 5 mahal # name: # type: sq_string # elements: 1 # length: 442 statistics: d = mahal (y, x) Mahalanobis' D-square distance. Return the Mahalanobis' D-square distance of the points in y from the distribution implied by points x. Specifically, it uses a Cholesky decomposition to set answer(i) = (y(i,:) - mean (x)) * inv (A) * (y(i,:)-mean (x))' where A is the covariance of x. The data x and y must have the same number of components (columns), but may have a different number of observations (rows). # name: # type: sq_string # elements: 1 # length: 31 Mahalanobis' D-square distance. # name: # type: sq_string # elements: 1 # length: 7 nomdist # name: # type: sq_string # elements: 1 # length: 5087 statistics: D = nomdist (X) statistics: D = nomdist (X, measure) statistics: D = nomdist (..., Name, Value) Dissimilarity between every pair of rows of nominal data. D = nomdist (X) returns the Goodall 3 dissimilarity between each pair of rows of X, whose columns are nominal variables and whose values are their levels. D is a row vector in the order pdist returns, pairs (1,2), (1,3), ..., (2,3), ..., so squareform turns it into a matrix and linkage takes it as it stands. An X of one row gives a 1x0 D. X is a numeric, logical, categorical, string or cellstr matrix, each distinct value being a level, or a table whose every variable is one column of these. A missing value, NaN, an empty character vector, a missing string or an undefined category, is refused. The measures weigh a match or a mismatch by how often the levels involved occur, counted over the rows of X unless 'Reference' names another sample. D = nomdist (X, measure) uses the measure named, in any case, from those of Boriah, Chandola and Kumar (2008) and of Sulc and Rezankova (2019), as the R package nomclust 2.8.1 implements them. In the table, N is the number of rows the frequencies are counted over, f the count of a level, p = f/N its frequency, p2 = f(f-1)/(N(N-1)) the chance of drawing it twice, and n the number of levels the variable holds. A subscript names the level of either row, and a variable scores 0 wherever its entry names no score. measure Score of one variable 'anderberg' The similarity is \(\frac{\sum_{x = y} c/p_x^2} {\sum_{x = y} c/p_x^2 + \sum_{x \ne y} c/(2 p_x p_y)},\) with \(c = 2/(n(n+1)).\) 'burnaby' 1 on a match; on a mismatch \(\frac{B}{\log \frac{p_x p_y}{(1-p_x)(1-p_y)} + B},\) with \(B = \sum_q 2 \log (1 - p_q).\) 'eskin' 1 on a match, \(\frac{n^2}{n^2 + 2}\) on a mismatch. 'gambaryan' \(-\left(p_x \log_2 p_x + (1-p_x) \log_2 (1-p_x)\right)\) on a match, which peaks where p is 0.5. 'goodall1' On a match \(1 - \sum_{q:\, p_q \le p_x} \mathit{p2}_q.\) 'goodall2' On a match \(1 - \sum_{q:\, p_q \ge p_x} \mathit{p2}_q.\) 'goodall3' \(1 - \mathit{p2}_x\) on a match. 'goodall4' \(\mathit{p2}_x\) on a match. 'iof' 1 on a match, \(\frac{1}{1 + \log f_x \log f_y}\) on a mismatch. 'lin' \(2 \log p_x\) on a match, \(2 \log (p_x + p_y)\) on a mismatch, over the sum of \(\log p_x + \log p_y.\) 'lin1' On a match \(\log \sum_{q:\, p_q = p_x} p_q;\) on a mismatch \(2 \log \sum_{q:\, \min (p_x, p_y) \le p_q \le \max (p_x, p_y)} p_q;\) over the same sum as 'lin'. 'of' 1 on a match, \(\frac{1}{1 + \log (N/f_x) \log (N/f_y)}\) on a mismatch. 'sm' 1 on a match, the simple matching coefficient. 'smirnov' On a match \(2 + \frac{N - f_x}{f_x} + \sum_{q \ne x} \frac{f_q}{N - f_q};\) on a mismatch \(\sum_{q \ne x, y} \frac{f_q}{N - f_q}.\) 've' On a match the entropy of the variable, \(-\frac{1}{\log n} \sum_q p_q \log p_q.\) 'vm' On a match the Gini index of the variable, \(\frac{n}{n-1} \left(1 - \sum_q p_q^2\right).\) The scores of a pair are summed over the variables, each times its weight, and divided by the sum of the weights, except where the table names another divisor, and 'gambaryan' and 'smirnov' divide by the number of levels over every variable. With S that similarity, 'eskin', 'iof', 'lin', 'lin1' and 'of' return 1/S - 1, 'smirnov' returns 1/(1+S), and every other measure 1 - S. Several measures, the four Goodall ones among them, place two identical rows at a nonzero dissimilarity, a match on a common level saying less than one on a rare level. 'lin' and 'lin1' return Inf for a pair of similarity 0, such as two rows differing in every variable where each variable holds two levels. D = nomdist (..., Name, Value) takes the following options. Name Value 'Weights' One weight per variable, each from 0 to 1 and at least one positive; a variable of weight 0 is left out. Not taken by 'anderberg', 'gambaryan' and 'smirnov'. 'Reference' A sample to count level frequencies over in place of X, of the same kind: a matrix with as many columns, the same type in each, or a table holding every variable of X, read by name. It must hold every level X holds. Given the whole data, it measures a subsample by how rare its levels are in the whole. Where nomclust answers otherwise, the answer here is the limit of the measure's formula. 'lin' and 'lin1' sum the counts of levels, so a sum over every level is exactly 1 and a similarity of 0 is exactly 0, where nomclust rounds it to about 1e16 or replaces it with one more than the largest finite dissimilarity. 'gambaryan' scores a level every row holds as 0, where nomclust returns NaN. Where every variable holds one level, 'lin' returns 0 and 'lin1' 1, where nomclust returns NaN. References: Boriah, S., Chandola, V. and Kumar, V. (2008). Similarity measures for categorical data: a comparative evaluation. Proceedings of the 8th SIAM International Conference on Data Mining, 243-254. Sulc, Z. and Rezankova, H. (2019). Comparison of similarity measures for categorical data in hierarchical clustering. Journal of Classification, 36(1), 58-72. See also: nomdist2, pdist, squareform, linkage # name: # type: sq_string # elements: 1 # length: 57 Dissimilarity between every pair of rows of nominal data. # name: # type: sq_string # elements: 1 # length: 8 nomdist2 # name: # type: sq_string # elements: 1 # length: 2225 statistics: D = nomdist2 (X, Y) statistics: D = nomdist2 (X, Y, measure) statistics: D = nomdist2 (..., Name, Value) Dissimilarity between the rows of two samples of nominal data. D = nomdist2 (X, Y) returns the Goodall 3 dissimilarity of each row of X to each row of Y, as the NxM matrix D, where X has N rows and Y M. Their columns are the same nominal variables and their values the levels, of any type nomdist takes; where X is a table, Y must be one holding every variable of X, read by name. A Y of no rows gives an Nx0 D. Level frequencies are counted over X, the sample the rows of Y are measured against, so D is not symmetric in the two: nomdist2 (X, Y) is not in general nomdist2 (Y, X)'. nomdist2 (X, X) equals squareform (nomdist (X)) off the diagonal; on it, several measures give two identical rows a nonzero dissimilarity. D = nomdist2 (X, Y, measure) uses the measure named, one of 'anderberg', 'burnaby', 'eskin', 'gambaryan', 'goodall1', 'goodall2', 'goodall3', 'goodall4', 'iof', 'lin', 'lin1', 'of', 'sm', 'smirnov', 've' or 'vm', in any case, each defined as in nomdist. D = nomdist2 (..., Name, Value) takes the options nomdist takes. 'Weights' gives one weight per variable. 'Reference' names a sample to count level frequencies over in place of X, which must hold every level X holds; [X; Y] counts them over both samples. 'CountQuery', false by default, counts each row of Y into the reference while it is measured, so that its column of D takes level frequencies over the reference with that row appended. Y may hold a level the reference sample does not, unless 'CountQuery' is true, which counts that level once. Each measure then answers with the limit of its formula as that level's count falls to 0. 'sm', 'eskin', 'gambaryan', the four Goodall measures, 'smirnov', 've' and 'vm' never read that count and answer as usual. 'of' and 'burnaby' score the variable 0, 'anderberg' places the pair at 1, and 'lin' and 'lin1' at Inf. Refused, where the limit does not exist: 'iof', whose similarity rises as a level grows rarer, and 'of' and 'burnaby' where the reference holds that variable at a single level. A variable of weight 0 is left out, and so never refused. See also: nomdist, pdist2 # name: # type: sq_string # elements: 1 # length: 62 Dissimilarity between the rows of two samples of nominal data. # name: # type: sq_string # elements: 1 # length: 5 pdist # name: # type: sq_string # elements: 1 # length: 3120 statistics: D = pdist (X) statistics: D = pdist (X, Distance) statistics: D = pdist (X, Distance, DistParameter) Return the distance between any two rows in X. D = pdist (X calculates the euclidean distance between pairs of observations in X. X must be an MxP numeric matrix representing M points in P-dimensional space. This function computes the pairwise distances returned in D as an Mx(M-1)/P row vector. Use Z = squareform (D) to convert the row vector D into a an MxM symmetric matrix Z, where Z(i,j) corresponds to the pairwise distance between points i and j. D = pdist (X, Y, Distance) returns the distance between pairs of observations in X using the metric specified by Distance, which can be any of the following options. 'euclidean' Euclidean distance. 'fasteuclidean' Euclidean distance computed with an alternative algorithm which may be faster but might reduce accuracy. 'squaredeuclidean' Squared Euclidean distance. 'fastsquaredeuclidean' Euclidean distance computed with an alternative algorithm which may be faster but might reduce accuracy. 'seuclidean' standardized Euclidean distance. Each coordinate difference between the rows in X and the query matrix Y is scaled by dividing by the corresponding element of the standard deviation computed from X. A different scaling vector can be specified with the subsequent DistParameter input argument. 'mahalanobis' Mahalanobis distance, computed using a positive definite covariance matrix. A different covariance matrix can be specified with the subsequent DistParameter input argument. 'cityblock' City block distance. 'minkowski' Minkowski distance. The default exponent is 2. A different exponent can be specified with the subsequent DistParameter input argument. An exponent of Inf gives the Chebychev distance. 'chebychev' Chebychev distance (maximum coordinate difference). A difference of Inf - Inf is NaN, and so is the distance, as for every other metric; MATLAB passes over it. 'cosine' One minus the cosine of the included angle between points (treated as vectors). 'correlation' One minus the sample linear correlation between observations (treated as sequences of values). 'hamming' Hamming distance, which is the percentage of coordinates that differ. 'jaccard' One minus the Jaccard coefficient, which is the percentage of nonzero coordinates that differ. 'spearman' One minus the sample Spearman's rank correlation between observations (treated as sequences of values). @distfun Custom distance function handle. A distance function of the form function D2 = distfun (XI, YI), where XI is a 1xP vector containing a single observation in P-dimensional space, YI is an NxP matrix containing an arbitrary number of observations in the same P-dimensional space, and D2 is an NxP vector of distances, where (D2k) is the distance between observations XI and (YIk,:). D = pdist (X, Y, Distance, DistParameter) returns the distance using the metric specified by Distance and DistParameter. The latter one can only be specified when the selected Distance is 'seuclidean', 'minkowski', and 'mahalanobis'. See also: pdist2, squareform, linkage # name: # type: sq_string # elements: 1 # length: 46 Return the distance between any two rows in X. # name: # type: sq_string # elements: 1 # length: 6 pdist2 # name: # type: sq_string # elements: 1 # length: 3997 statistics: D = pdist2 (X, Y) statistics: D = pdist2 (X, Y, Distance) statistics: D = pdist2 (X, Y, Distance, DistParameter) statistics: D = pdist2 (..., Name, Value) statistics: [D, I] = pdist2 (..., Name, Value) Compute pairwise distance between two sets of vectors. D = pdist2 (X, Y) calculates the euclidean distance between each pair of observations in X and Y. Let X be an MxP matrix representing M points in P-dimensional space and Y be an NxP matrix representing another set of points in the same space. This function computes the MxN distance matrix D, where D(i,j) is the distance between X(i,:) and Y(j,:). D = pdist2 (X, Y, Distance) returns the distance between each pair of observations in X and Y using the metric specified by Distance, which can be any of the following options. 'euclidean' Euclidean distance. 'fasteuclidean' Euclidean distance computed with an alternative algorithm which may be faster but might reduce accuracy. 'squaredeuclidean' Squared Euclidean distance. 'fastsquaredeuclidean' Euclidean distance computed with an alternative algorithm which may be faster but might reduce accuracy. 'seuclidean' standardized Euclidean distance. Each coordinate difference between the rows in X and the query matrix Y is scaled by dividing by the corresponding element of the standard deviation computed from X. A different scaling vector can be specified with the subsequent DistParameter input argument. 'mahalanobis' Mahalanobis distance, computed using a positive definite covariance matrix. A different covariance matrix can be specified with the subsequent DistParameter input argument. 'cityblock' City block distance. 'minkowski' Minkowski distance. The default exponent is 2. A different exponent can be specified with the subsequent DistParameter input argument. An exponent of Inf gives the Chebychev distance. 'chebychev' Chebychev distance (maximum coordinate difference). A difference of Inf - Inf is NaN, and so is the distance, as for every other metric; MATLAB passes over it. 'cosine' One minus the cosine of the included angle between points (treated as vectors). 'correlation' One minus the sample linear correlation between observations (treated as sequences of values). 'hamming' Hamming distance, which is the percentage of coordinates that differ. 'jaccard' One minus the Jaccard coefficient, which is the percentage of nonzero coordinates that differ. 'spearman' One minus the sample Spearman's rank correlation between observations (treated as sequences of values). @distfun Custom distance function handle. A distance function of the form function D2 = distfun (XI, YI), where XI is a 1xP vector containing a single observation in P-dimensional space, YI is an NxP matrix containing an arbitrary number of observations in the same P-dimensional space, and D2 is an NxP vector of distances, where (D2k) is the distance between observations XI and (YIk,:). D = pdist2 (X, Y, Distance, DistParameter) returns the distance using the metric specified by Distance and DistParameter. The latter one can only be specified when the selected Distance is 'seuclidean', 'minkowski', and 'mahalanobis'. D = pdist2 (..., Name, Value) for any previous arguments, modifies the computation using Name-Value parameters. D = pdist2 (X, Y, Distance, 'Smallest', K) computes the distance using the metric specified by Distance and returns the K smallest pairwise distances to observations in X for each observation in Y in ascending order. D = pdist2 (X, Y, Distance, DistParameter, 'Largest', K) computes the distance using the metric specified by Distance and DistParameter and returns the K largest pairwise distances in descending order. [D, I] = pdist2 (..., Name, Value) also returns the matrix I, which contains the indices of the observations in X corresponding to the distances in D. You must specify either 'Smallest' or 'Largest' as an optional Name-Value pair argument to compute the second output argument. See also: pdist, knnsearch, rangesearch # name: # type: sq_string # elements: 1 # length: 54 Compute pairwise distance between two sets of vectors. # name: # type: sq_string # elements: 1 # length: 11 rangesearch # name: # type: sq_string # elements: 1 # length: 4875 statistics: idx = rangesearch (X, Y, r) statistics: [idx, D] = rangesearch (X, Y, r) statistics: [...] = rangesearch (..., name, value) Find all neighbors within specified distance from input data. idx = rangesearch (X, Y, r) returns all the points in X that are within distance r from the points in Y. X must be an NxP numeric matrix of input data, where rows correspond to observations and columns correspond to features or variables. Y is an MxP numeric matrix with query points, which must have the same numbers of column as X. r must be a nonnegative scalar value. idx is an Mx1 cell array, where M is the number of observations in Y. The vector Idx{j} contains the indices of observations (rows) in X whose distances to Y(j,:) are not greater than r. [idx, D] = rangesearch (X, Y, r) also returns the distances, D, which correspond to the points in X that are within distance r from the points in Y. D is an Mx1 cell array, where M is the number of observations in Y. The vector D{j} contains the distances of observations (rows) in X whose distances to Y(j,:) are not greater than r. Additional parameters can be specified by Name-Value pair arguments. Name Value 'P' is the Minkowski distance exponent and it must be a positive scalar. This argument is only valid when the selected distance metric is 'minkowski'. By default it is 2. An exponent of Inf gives the Chebychev distance. 'Scale' is the scale parameter for the standardized Euclidean distance and it must be a nonnegative numeric vector of equal length to the number of columns in X. This argument is only valid when the selected distance metric is 'seuclidean', in which case each coordinate of X is scaled by the corresponding element of 'scale', as is each query point in Y. By default, the scale parameter is the standard deviation of each coordinate in X. 'Cov' is the covariance matrix for computing the mahalanobis distance and it must be a positive definite matrix matching the the number of columns in X. This argument is only valid when the selected distance metric is 'mahalanobis'. 'BucketSize' is the maximum number of data points in the leaf node of the Kd-tree and it must be a positive integer. This argument is only valid when the selected search method is 'kdtree'. 'SortIndices' is a boolean flag to sort the returned indices in ascending order by distance and it is true by default. When the selected search method is 'exhaustive' or the 'IncludeTies' flag is true, rangesearch always sorts the returned indices. 'Distance' is the distance metric used by rangesearch as specified below: 'euclidean' Euclidean distance. 'seuclidean' standardized Euclidean distance. Each coordinate difference between the rows in X and the query matrix Y is scaled by dividing by the corresponding element of the standard deviation computed from X. To specify a different scaling, use the 'Scale' name-value argument. 'cityblock' City block distance. 'chebychev' Chebychev distance (maximum coordinate difference). A difference of Inf - Inf is NaN, and so is the distance, as for every other metric; MATLAB passes over it. 'minkowski' Minkowski distance. The default exponent is 2. To specify a different exponent, use the 'P' name-value argument. 'mahalanobis' Mahalanobis distance, computed using a positive definite covariance matrix. To change the value of the covariance matrix, use the 'Cov' name-value argument. 'cosine' Cosine distance. 'correlation' One minus the sample linear correlation between observations (treated as sequences of values). 'spearman' One minus the sample Spearman's rank correlation between observations (treated as sequences of values). 'hamming' Hamming distance, which is the percentage of coordinates that differ. 'jaccard' One minus the Jaccard coefficient, which is the percentage of nonzero coordinates that differ. @distfun Custom distance function handle. A distance function of the form function D2 = distfun (XI, YI), where XI is a 1xP vector containing a single observation in P-dimensional space, YI is an NxP matrix containing an arbitrary number of observations in the same P-dimensional space, and D2 is an NxP vector of distances, where (D2k) is the distance between observations XI and (YIk,:). 'NSMethod' is the nearest neighbor search method used by rangesearch as specified below. 'kdtree' Creates and uses a Kd-tree to find nearest neighbors. 'kdtree' is the default value when the number of columns in X is less than or equal to 10, X is not sparse, and the distance metric is 'euclidean', 'cityblock', 'manhattan', 'chebychev', or 'minkowski'. Otherwise, the default value is 'exhaustive'. This argument is only valid when the distance metric is one of the four aforementioned metrics. 'exhaustive' Uses the exhaustive search algorithm by computing the distance values from all the points in X to each point in Y. See also: knnsearch, pdist2 # name: # type: sq_string # elements: 1 # length: 61 Find all neighbors within specified distance from input data. # name: # type: sq_string # elements: 1 # length: 10 squareform # name: # type: sq_string # elements: 1 # length: 1431 statistics: zOut = squareform (yIn) statistics: yOut = squareform (zIn) statistics: zOut = squareform (yIn, 'tovector') statistics: yOut = squareform (zIn, 'tomatrix') Interchange between distance matrix and distance vector formats. Converts between a hollow (diagonal filled with zeros), square, and symmetric matrix and a vector of the lower triangular part. Its target application is the conversion of the vector returned by pdist into a distance matrix. It performs the opposite operation if input is a matrix. If x is a numeric or logical vector, its number of elements must fit into the triangular part of a matrix (main diagonal excluded). In other words, numel (x) = n * (n - 1) / 2 for some integer n. The resulting matrix will be n by n. An empty vector is the distance vector of a single observation, so it returns the 1-by-1 distance matrix 0 and squareform (pdist (x)) holds for a single row x. MATLAB returns an empty matrix instead. If x is a numeric or logical distance matrix, it must be square and the diagonal entries of x must all be zeros. If x is not symmetric, only the lower triangular part is used. The second argument is used to specify the output type in case the distance input is a scalar. Accepted values are 'tomatrix' (or 'tom') and 'tovector' (or 'tov'). If not specified, it defaults to 'tomatrix' otherwise. When 'tomatrix' is specified, the distance input must be a vector. See also: pdist # name: # type: sq_string # elements: 1 # length: 64 Interchange between distance matrix and distance vector formats. statistics-release-2.1.2/inst/Nearest_Neighbors/hnswSearcher.m000066400000000000000000001316511527311104300245200ustar00rootroot00000000000000## Copyright (C) 2025 Swayam Shah ## Copyright (C) 2025 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef hnswSearcher ## -*- texinfo -*- ## @deftp {statistics} hnswSearcher ## ## Hierarchical Navigable Small World (HNSW) nearest neighbor searcher class. ## ## The @code{hnswSearcher} class implements the HNSW algorithm for efficient ## nearest neighbor queries. It stores training data and supports various ## distance metrics for performing searches. The HNSW algorithm builds a ## multilayer graph structure that enables fast approximate nearest neighbor ## searches by navigating through the graph. It facilitates nearest neighbor ## queries search using @code{knnsearch}. ## ## You can either use the @code{hnswSearcher} class constructor or the ## @code{createns} function to create an @qcode{hnswSearcher} object. ## ## @seealso{createns, ExhaustiveSearcher, KDTreeSearcher, knnsearch} ## @end deftp properties(SetAccess = private, Hidden) HNSWGraph # HNSW graph structure endproperties properties(SetAccess = private) ## -*- texinfo -*- ## @deftp {hnswSearcher} {property} Distance ## ## Distance metric ## ## Distance metric used for searches, specified as a character vector (e.g., ## @qcode{'euclidean'}, @qcode{'minkowski'}, @qcode{'cityblock'}). Default ## is @qcode{'euclidean'}. Supported metrics align with those in ## @code{pdist2}. This property is private and cannot be modified after ## object creation. ## ## @end deftp Distance = 'euclidean' ## -*- texinfo -*- ## @deftp {hnswSearcher} {property} DistParameter ## ## Distance parameter ## ## The type and value of the distance parameter depends on the selected ## @qcode{Distance} metric and can be any of the following: ## ## @itemize ## @item For @qcode{'minkowski'}, a positive scalar exponent (default 2); ## @code{Inf} gives the Chebychev distance. ## @item For @qcode{'seuclidean'}, a nonnegative vector of scaling factors ## matching the number of columns in @qcode{X} (default is standard ## deviation of @qcode{X}). ## @item For @qcode{'mahalanobis'}, a positive definite covariance matrix ## matching the dimensions of @qcode{X} (default is @code{cov (@var{X})}). ## @item Empty for other metrics. ## @end itemize ## ## This property is private and cannot be modified after object creation. ## ## @end deftp DistParameter = [] ## -*- texinfo -*- ## @deftp {hnswSearcher} {property} MaxNumLinksPerNode ## ## Number of connections created for each node ## ## Maximum number of neighbors per node in the HNSW graph. Affects graph ## connectivity and search accuracy. Default value is 16. This property is ## private and cannot be modified after object creation. ## ## @end deftp MaxNumLinksPerNode = 16 ## -*- texinfo -*- ## @deftp {hnswSearcher} {property} TrainSetSize ## ## Number of potential nearest neighbors ## ## Size of the dynamic candidate list during graph construction. Higher ## values improve accuracy at the cost of construction time. Default value ## is 200. This property is private and cannot be modified after object ## creation. ## ## @end deftp TrainSetSize = 200 ## -*- texinfo -*- ## @deftp {hnswSearcher} {property} X ## ## Point data ## ## Point data, specified as an @math{N*P} numeric matrix where each row is ## an observation and each column is a feature. This property is private ## and cannot be modified after object creation. ## ## Data of class @qcode{single} is stored and searched in single ## precision, any other numeric class is converted to @qcode{double}. ## ## @end deftp X = [] endproperties methods (Hidden) ## Custom display function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ('%s =\n', in_name); endif disp (this); endfunction ## Custom display function disp (this) if (isscalar (this)) fprintf ("\n hnswSearcher with properties:\n\n"); fprintf ("%+25s: %d\n", 'MaxNumLinksPerNode', this.MaxNumLinksPerNode); fprintf ("%+25s: %d\n", 'TrainSetSize', this.TrainSetSize); fprintf ("%+25s: '%s'\n", 'Distance', this.Distance); if (! isempty (this.DistParameter)) if (isscalar (this.DistParameter)) fprintf ("%+25s: %g\n", 'DistParameter', this.DistParameter); elseif (isvector (this.DistParameter)) fprintf ("%+25s: %s\n", 'DistParameter', ... mat2str (this.DistParameter)); else fprintf ("%+25s: [%dx%d %s]\n", 'DistParameter', ... size (this.DistParameter), class (this.DistParameter)); endif else fprintf ("%+25s: []\n", 'DistParameter'); endif fprintf ("%+25s: [%dx%d %s]\n", 'X', size (this.X), class (this.X)); endif endfunction ## Class specific subscripted reference function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); switch (s.type) case '()' error ("hnswSearcher.subsref: () indexing not supported."); case '{}' error ("hnswSearcher.subsref: {} indexing not supported."); case '.' if (! ischar (s.subs)) error (strcat ("hnswSearcher.subsref: property", ... " name must be a character vector.")); endif try out = this.(s.subs); catch error (strcat ("hnswSearcher.subsref: unrecognized", ... " property: '%s'."), s.subs); end_try_catch endswitch ## Chained references if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction ## Class specific subscripted assignment function this = subsasgn (this, s, val) switch s.type case '()' error ("hnswSearcher.subsasgn: () indexing not supported."); case '{}' error ("hnswSearcher.subsasgn: {} indexing not supported."); case '.' if (! ischar (s.subs)) error (strcat ("hnswSearcher.subsasgn: property", ... " name must be a character vector.")); endif switch (s.subs) case 'X' error (strcat ("hnswSearcher.subsasgn: 'X' is", ... " read-only and cannot be modified.")); case 'HNSWGraph' error (strcat ("hnswSearcher.subsasgn: 'HNSWGraph' is", ... " read-only and cannot be modified.")); case 'Distance' error (strcat ("hnswSearcher.subsasgn: 'Distance' is", ... " read-only and cannot be modified.")); case 'DistParameter' error (strcat ("hnswSearcher.subsasgn: 'DistParameter'", ... " is read-only and cannot be modified.")); case 'MaxNumLinksPerNode' error (strcat ("hnswSearcher.subsasgn: 'MaxNumLinksPerNode'", ... " is read-only and cannot be modified.")); case 'TrainSetSize' error (strcat ("hnswSearcher.subsasgn: 'TrainSetSize'", ... " is read-only and cannot be modified.")); otherwise error (strcat ("hnswSearcher.subsasgn:", ... " unrecognized property: '%s'."), s.subs); endswitch endswitch endfunction endmethods methods ## -*- texinfo -*- ## @deftypefn {hnswSearcher} {@var{obj} =} hnswSearcher (@var{X}) ## @deftypefnx {hnswSearcher} {@var{obj} =} hnswSearcher (@var{X}, @var{name}, @var{value}) ## ## Create an @qcode{hnswSearcher} object for approximate nearest neighbor ## searches. ## ## @code{@var{obj} = hnswSearcher (@var{X})} constructs an ## @qcode{hnswSearcher} object with training data @var{X} using the ## default @qcode{'euclidean'} distance metric. @var{X} must be an ## @math{N*P} numeric matrix, where rows represent observations and columns ## represent features. ## ## @code{@var{obj} = hnswSearcher (@var{X}, @var{name}, @var{value})} ## allows customization through name-value pairs: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Distance'} @tab Distance metric, specified as a ## character vector (e.g., @qcode{'euclidean'}, @qcode{'minkowski'}, ## @qcode{'cityblock'}). Default is @qcode{'euclidean'}. See @code{pdist2} ## for supported metrics. ## ## @item @qcode{'P'} @tab Minkowski distance exponent, a positive ## scalar. Valid only when @qcode{'Distance'} is @qcode{'minkowski'}. ## Default is 2. An exponent of @code{Inf} gives the Chebychev distance, ## where MATLAB returns wrong neighbours. ## ## @item @qcode{'Scale'} @tab Nonnegative vector of scaling factors ## matching the number of columns in @var{X}. Valid only when ## @qcode{'Distance'} is @qcode{'seuclidean'}. Default is @code{std (X)}. ## ## @item @qcode{'Cov'} @tab Positive definite covariance matrix ## matching the number of columns in @var{X}. Valid only when ## @qcode{'Distance'} is @qcode{'mahalanobis'}. Default is @code{cov (X)}. ## ## @item @qcode{'MaxNumLinksPerNode'} @tab Maximum number of neighbors ## per node in the HNSW graph, a positive integer. Default is 16. ## ## @item @qcode{'TrainSetSize'} @tab Size of the dynamic candidate ## list during graph construction, a positive integer. Default is 200. ## @end multitable ## ## @seealso{hnswSearcher, knnsearch, createns, pdist2} ## @end deftypefn function obj = hnswSearcher (X, varargin) ## Initial input validation if (nargin < 1) error ("hnswSearcher: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error ("hnswSearcher: Name-Value arguments must be in pairs."); endif ## Validate X if (isempty (X)) error ("hnswSearcher: X cannot be empty."); endif if (! (isnumeric (X) && ismatrix (X) && all (isfinite (X(:))))) error ("hnswSearcher: X must be a finite numeric matrix."); endif ## Single precision is carried through, but every other class is ## converted up to double, as MATLAB does. Integer data would ## otherwise round each coordinate difference and corrupt distances. if (! isa (X, "single")) X = double (X); endif obj.X = X; N = size (X, 1); ## Default values Distance = 'euclidean'; P = []; S = []; C = []; MaxNumLinksPerNode = min (16, N); TrainSetSize = min (200, N); TrainSetSize = max (TrainSetSize, MaxNumLinksPerNode); ## Parse optional parameters while (numel (varargin) > 0) switch (lower (varargin{1})) case 'distance' Distance = varargin{2}; case 'p' P = varargin{2}; case 'scale' S = varargin{2}; case 'cov' C = varargin{2}; case 'maxnumlinkspernode' MaxNumLinksPerNode = varargin{2}; case 'trainsetsize' TrainSetSize = varargin{2}; otherwise error ("hnswSearcher: invalid parameter name: '%s'.", varargin{1}); endswitch varargin(1:2) = []; endwhile ## Validate Distance valid_metrics = {'euclidean', 'minkowski', 'seuclidean', ... 'mahalanobis', 'cityblock', 'manhattan', ... 'chebychev', 'cosine', 'correlation', ... 'spearman', 'hamming', 'jaccard'}; if (ischar (Distance)) if (! any (strcmpi (valid_metrics, Distance))) error ("hnswSearcher: unsupported distance metric '%s'.", Distance); endif obj.Distance = Distance; else error ("hnswSearcher: 'Distance' must be a string."); endif if (! isempty (P) && ! strcmpi (obj.Distance, 'minkowski')) error ("hnswSearcher: 'P' applies only to the minkowski metric."); endif if (! isempty (S) && ! strcmpi (obj.Distance, 'seuclidean')) error (strcat ("hnswSearcher: 'Scale' applies only to the", ... " seuclidean metric.")); endif if (! isempty (C) && ! strcmpi (obj.Distance, 'mahalanobis')) error (strcat ("hnswSearcher: 'Cov' applies only to the", ... " mahalanobis metric.")); endif ## Set DistParameter if (strcmpi (obj.Distance, 'minkowski')) if (isempty (P)) obj.DistParameter = 2; else ## An exponent of Inf is the Chebychev distance, which pdist2 ## computes for every distance the graph is built and searched by if (! (isscalar (P) && isnumeric (P) && P > 0)) error ("hnswSearcher: 'P' must be a positive scalar."); endif obj.DistParameter = P; endif elseif (strcmpi (obj.Distance, 'seuclidean')) if (isempty (S)) obj.DistParameter = std (X, [], 1); else if (! (isvector (S) && isnumeric (S) && all (S >= 0) && all (isfinite (S)) && length (S) == columns (X))) error (strcat ("hnswSearcher: 'Scale' must be a", ... " nonnegative vector matching X columns.")); endif obj.DistParameter = S; endif elseif (strcmpi (obj.Distance, 'mahalanobis')) if (isempty (C)) obj.DistParameter = cov (X); else if (! (ismatrix (C) && isnumeric (C) && all (isfinite (C)(:)) && rows (C) == columns (C) && rows (C) == columns (X))) error (strcat ("hnswSearcher: 'Cov' must be a square", ... " matrix matching X columns.")); endif if (! issymmetric (C)) error (strcat ("hnswSearcher: 'Cov' must be symmetric", ... " for mahalanobis.")); endif [~, p] = chol (C); if (p != 0) error (strcat ("hnswSearcher: 'Cov' must be positive", ... " definite for mahalanobis.")); endif obj.DistParameter = C; endif else obj.DistParameter = []; endif ## Validate MaxNumLinksPerNode and TrainSetSize if (! (isscalar (MaxNumLinksPerNode) && isnumeric (MaxNumLinksPerNode) && MaxNumLinksPerNode > 0 && MaxNumLinksPerNode == fix (MaxNumLinksPerNode))) error (strcat ("hnswSearcher: 'MaxNumLinksPerNode'", ... " must be a positive integer.")); endif if (! (isscalar (TrainSetSize) && isnumeric (TrainSetSize) && TrainSetSize > 0 && TrainSetSize == fix (TrainSetSize))) error ("hnswSearcher: 'TrainSetSize' must be a positive integer."); endif if (TrainSetSize > N) error (strcat ("hnswSearcher: 'TrainSetSize' cannot", ... " exceed the number of rows in X.")); endif if (MaxNumLinksPerNode > TrainSetSize) error (strcat ("hnswSearcher: 'MaxNumLinksPerNode'", ... " cannot exceed 'TrainSetSize'.")); endif obj.MaxNumLinksPerNode = MaxNumLinksPerNode; obj.TrainSetSize = TrainSetSize; ## Build HNSW graph obj.HNSWGraph = build_hnsw (X, obj.Distance, obj.DistParameter, ... MaxNumLinksPerNode, TrainSetSize); endfunction ## -*- texinfo -*- ## @deftypefn {hnswSearcher} {[@var{idx}, @var{D}] =} knnsearch (@var{obj}, @var{Y}) ## @deftypefnx {hnswSearcher} {[@var{idx}, @var{D}] =} knnsearch (@var{obj}, @var{Y}, @var{name}, @var{value}) ## ## Find the nearest neighbors in the training data to query points. ## ## @code{[@var{idx}, @var{D}] = knnsearch (@var{obj}, @var{Y})} returns the ## indices @var{idx} and distances @var{D} of the nearest neighbor in ## @var{obj.X} to each point in @var{Y}, using the distance metric specified ## in @var{obj.Distance}. ## ## @itemize ## @item @var{obj} is an @qcode{hnswSearcher} object. ## @item @var{Y} is an @math{M*P} numeric matrix of query points, where ## @math{P} must match the number of columns in @var{obj.X}. ## @item @var{idx} contains the indices of the nearest neighbors in ## @var{obj.X}. ## @item @var{D} contains the corresponding distances. ## @end itemize ## ## @var{idx} is always of class @qcode{double}. @var{D} is of class ## @qcode{single} when either @var{obj.X} or @var{Y} is @qcode{single}, ## in which case the distances are computed in single precision, and of ## class @qcode{double} otherwise. ## ## @code{[@var{idx}, @var{D}] = knnsearch (@var{obj}, @var{Y}, @var{name}, ## @var{value})} ## allows additional options via name-value pairs: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'K'} @tab A positive integer specifying the number of ## nearest neighbors to find. Default is 1. A value larger than the ## number of observations in the training data is answered with all of ## them, since there are no more neighbors to return. ## ## @item @qcode{'SearchSetSize'} @tab A positive integer specifying the ## size of the candidate list of nearest neighbors for a single query point ## during the search process. Default is @qcode{max (10, @var{C})}, where ## @var{C} is the number of columns in @var{obj.X}. @qcode{'SearchSetSize'} ## must be at least @var{C} and no more than the number of rows in training ## data @var{obj.X}. ## @end multitable ## ## @seealso{hnswSearcher, pdist2} ## @end deftypefn function [idx, D] = knnsearch (obj, Y, varargin) ## Initial input validation if (nargin < 2) error ("hnswSearcher.knnsearch: too few input arguments."); endif if (mod (numel (varargin), 2) != 0) error (strcat ("hnswSearcher.knnsearch:", ... " Name-Value arguments must be in pairs.")); endif ## Get training data size [N, C] = size (obj.X); ## Validate Y. A non-finite query is answered rather than refused: its ## distances come back Inf or NaN, which is the honest result and what ## MATLAB returns. An empty query is answered with an empty result. if (! (isnumeric (Y) && isreal (Y) && ismatrix (Y))) error ("hnswSearcher.knnsearch: Y must be a real numeric matrix."); endif if (C != size (Y, 2)) error (strcat ("hnswSearcher.knnsearch: Y must have the same", ... " number of columns as the training data in OBJ.X.")); endif ## Default values K = 1; SearchSetSize = max (10, C); ## Parse options while (numel (varargin) > 0) switch (lower (varargin{1})) case 'k' K = varargin{2}; if (! (isscalar (K) && isnumeric (K) && K >= 1 && K == fix (K) && isfinite (K))) error ("hnswSearcher.knnsearch: 'K' must be a positive integer."); endif case 'searchsetsize' SearchSetSize = varargin{2}; if (! (isscalar (SearchSetSize) && isnumeric (SearchSetSize) && SearchSetSize >= 1 && SearchSetSize == fix (SearchSetSize) && isfinite (SearchSetSize))) error (strcat ("hnswSearcher.knnsearch: 'SearchSetSize' must", ... " be a positive integer.")); endif if (SearchSetSize < C || SearchSetSize > N) error (strcat ("hnswSearcher.knnsearch: 'SearchSetSize' must", ... " be at least the number of features but no", ... " more than the sample size of the training data.")); endif otherwise error (strcat ("hnswSearcher.knnsearch: invalid", ... " parameter name: '%s'."), varargin{1}); endswitch varargin(1:2) = []; endwhile ## More neighbours cannot be returned than there are points to return, ## so asking for more is answered with all of them rather than with a ## matrix wider than the training data padded out with NaN. K = min (K, N); ## The search only ever inspects SearchSetSize candidates, so it cannot ## produce K neighbours while that is the smaller of the two: asking for ## every point used to come back mostly NaN. SearchSetSize = min (max (SearchSetSize, K), N); ## Distances are computed in single precision when either the training ## data or the query is single, and in double otherwise. The indices ## are always double. cls = "double"; if (isa (obj.X, "single") || isa (Y, "single")) cls = "single"; endif Y = cast (Y, cls); if (isempty (Y)) idx = zeros (rows (Y), K); D = zeros (rows (Y), K, cls); return; endif ## Search HNSW graph idx = cell (rows (Y), 1); D = cell (rows (Y), 1); for i = 1:rows (Y) [temp_idx, temp_D] = search_hnsw (obj.HNSWGraph, Y(i,:), obj.X, ... obj.Distance, obj.DistParameter, ... K, SearchSetSize); [sorted_D, sort_idx] = sort (temp_D); idx{i} = temp_idx(sort_idx); D{i} = sorted_D; endfor idx_mat = NaN (rows (Y), K); D_mat = NaN (rows (Y), K, cls); for i = 1:rows (Y) len = min (K, length (idx{i})); idx_mat(i, 1:len) = idx{i}(1:len); D_mat(i, 1:len) = D{i}(1:len); endfor idx = idx_mat; D = D_mat; endfunction endmethods endclassdef ## Private Function to Build HNSW graph function graph = build_hnsw (X, dist, param, MaxNumLinksPerNode, TrainSetSize) N = size (X, 1); if (N < 1) error ("build_hnsw: X must have at least one point."); endif max_layers = floor (log2 (max (N, 2))) + 1; graph.layers = cell (max_layers, 1); graph.entry_point = 1; mL = 1 / log (MaxNumLinksPerNode); ## Initialize graph with empty adjacency lists for L = 1:max_layers graph.layers{L} = cell (N, 1); endfor ## Handle single-point case if (N == 1) graph.layers{1}{1} = []; return; endif ## Add points to graph for i = 1:N layer = min (max_layers - 1, floor (-log (rand ()) * mL)); for L = 0:layer if (i == 1) graph.layers{L+1}{i} = []; continue; endif ## Find nearest neighbors in current layer [neighbors, dists] = search_hnsw_layer (graph, X(i,:), X, dist, param, ... MaxNumLinksPerNode, ... TrainSetSize, L, ... graph.entry_point); graph.layers{L+1}{i} = neighbors; ## Update neighbors' connections for j = neighbors if (length (graph.layers{L+1}{j}) < MaxNumLinksPerNode) graph.layers{L+1}{j} = [graph.layers{L+1}{j}, i]; else ## Select MaxNumLinksPerNode closest neighbors all_neighbors = [graph.layers{L+1}{j}, i]; dists_j = pdist2 (X(all_neighbors,:), X(j,:), dist, param); [~, sort_idx] = sort (dists_j); graph.layers{L+1}{j} = all_neighbors(sort_idx(1:MaxNumLinksPerNode)); endif endfor if (L == layer && i > 1) graph.entry_point = i; endif endfor endfor endfunction ## Private Function to Search HNSW graph for k nearest neighbors function [indices, distances] = search_hnsw (graph, Y, X, dist, param, ... K, SearchSetSize) max_layers = length (graph.layers); current_entry = graph.entry_point; ## Navigate to the lowest layer (from the top layer down to layer 1) for L = max_layers:-1:2 [new_candidates, new_dists] = search_hnsw_layer (graph, Y, X, ... dist, param, ... 1, SearchSetSize, L-1, ... current_entry); current_entry = new_candidates(1); endfor ## Search in the base layer [indices, distances] = search_hnsw_layer (graph, Y, X, dist, param, ... K, SearchSetSize, 0, current_entry); endfunction ## Private Function Search a single HNSW layer (optimized) function [indices, distances] = search_hnsw_layer (graph, Y, X, dist, param, ... Points, SetSize, L, ... entry_points) N = size (X, 1); visited = false (N, 1); ## Pre-allocate fixed-size arrays with generous capacity capacity = N; ## Candidate list (nodes to explore) cand_nodes = zeros (1, capacity); cand_dists = zeros (1, capacity); cand_count = 0; ## Best results list (top SetSize closest nodes, kept sorted) best_nodes = zeros (1, capacity); best_dists_arr = zeros (1, capacity); best_count = 0; ## Initialize with entry points entry_points = entry_points(:)'; num_entries = length (entry_points); init_dists = pdist2 (X(entry_points,:), Y, dist, param); for i = 1:num_entries ep = entry_points(i); visited(ep) = true; cand_count = cand_count + 1; cand_nodes(cand_count) = ep; cand_dists(cand_count) = init_dists(i); best_count = best_count + 1; best_nodes(best_count) = ep; best_dists_arr(best_count) = init_dists(i); endfor ## Cache layer adjacency list reference layer_adj = graph.layers{L+1}; ## Main search loop while (cand_count > 0) ## Find minimum distance candidate [~, idx] = min (cand_dists(1:cand_count)); closest = cand_nodes(idx); ## Swap-and-pop removal (O(1) instead of O(n) array shift) cand_nodes(idx) = cand_nodes(cand_count); cand_dists(idx) = cand_dists(cand_count); cand_count = cand_count - 1; ## Early termination check if (best_count > 0 && cand_count > 0) if (max (best_dists_arr(1:best_count)) < min (cand_dists(1:cand_count))) break; endif endif ## Get neighbors of closest node neighbors = layer_adj{closest}; if (isempty (neighbors)) continue; endif ## Filter to unvisited neighbors only unvisited_mask = ! visited(neighbors); new_neighbors = neighbors(unvisited_mask); if (isempty (new_neighbors)) continue; endif ## Mark all new neighbors as visited visited(new_neighbors) = true; ## Batch distance computation (single pdist2 call) new_dists = pdist2 (X(new_neighbors,:), Y, dist, param); ## Add each new neighbor to candidates and best lists num_new = length (new_neighbors); for i = 1:num_new n = new_neighbors(i); d = new_dists(i); ## Add to candidates cand_count = cand_count + 1; cand_nodes(cand_count) = n; cand_dists(cand_count) = d; ## Add to best list best_count = best_count + 1; best_nodes(best_count) = n; best_dists_arr(best_count) = d; endfor ## Trim best list once per batch instead of per element if (best_count > SetSize) [best_dists_arr(1:best_count), sort_idx] = sort (best_dists_arr(1:best_count)); best_nodes(1:best_count) = best_nodes(sort_idx); best_count = SetSize; endif endwhile ## Return top Points results if (best_count > Points) [best_dists_arr(1:best_count), sort_idx] = sort (best_dists_arr(1:best_count)); best_nodes(1:best_count) = best_nodes(sort_idx); indices = best_nodes(1:Points); distances = best_dists_arr(1:Points); else indices = best_nodes(1:best_count); distances = best_dists_arr(1:best_count); endif endfunction %!demo %! ## Create an hnswSearcher with Euclidean distance %! X = [1, 2; 3, 4; 5, 6]; %! obj = hnswSearcher (X); %! ## Find the nearest neighbor to [2, 3] %! Y = [2, 3]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! disp ('Nearest neighbor index:'); %! disp (idx); %! disp ('Distance:'); %! disp (D); %!demo %! ## Create an hnswSearcher with Minkowski distance (P=3) %! X = [0, 0; 1, 0; 2, 0]; %! obj = hnswSearcher (X, 'Distance', 'minkowski', 'P', 3); %! ## Find the nearest neighbor to [1, 0] %! Y = [1, 0]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! disp ('Nearest neighbor index:'); %! disp (idx); %! disp ('Distance:'); %! disp (D); ## Test Cases %!test %! ## the graph is drawn from rand, so seed it: the search is approximate %! rand ("seed", 42); %! load fisheriris %! X = meas; %! obj = hnswSearcher (X, 'Distance', 'chebychev'); %! Y = X(30:35,:); %! [idx, D] = knnsearch (obj, Y, 'K', 4); %! ## Under chebychev these queries have several equidistant neighbours, so %! ## which of them is listed first is not defined. Assert the distances, %! ## which are, and that each returned index really sits at the distance %! ## reported for it. %! assert_equal (D, [[0 0.1000 0.1000 0.2000]; [0 0.1000 0.1000 0.1000]; [0 0.2000 ... %! 0.2000 0.2000]; [0 0.3000 0.3000 0.3000]; [0 0.2000 0.3000 ... %! 0.3000]; [0 0.1000 0.1000 0.1000]], 5e-15) %! for i = 1:rows (Y) %! assert_equal (numel (unique (idx(i,:))), 4); %! for j = 1:4 %! assert_equal (max (abs (X(idx(i,j),:) - Y(i,:))), D(i,j), 5e-15); %! endfor %! endfor %!test %! ## the graph is drawn from rand, so seed it: the search is approximate %! rand ("seed", 42); %! load fisheriris %! X = meas; %! C = cov (X); %! obj = hnswSearcher (X, 'Distance', 'mahalanobis', 'Cov', C); %! Y = X(120:125,:); %! [idx, D] = knnsearch (obj, Y, 'K', 2); %! assert_equal (idx(1, :), [120 82]) %! assert_equal (idx(4, :), [123 106]) %! assert_equal (idx(5, :), [124 127]) %! assert_equal (idx(6, :), [125 57]) %! assert_equal (D(1, :), [0 0.7734], 1e-4) %! assert_equal (D(4, :), [0 0.8452], 1e-4) %! assert_equal (D(5, :), [0 0.4152], 1e-4) %! assert_equal (D(6, :), [0 0.7322], 1e-4) %!test %! ## Basic constructor with default Euclidean %! X = [1, 2; 3, 4; 5, 6]; %! obj = hnswSearcher (X); %! assert_equal (obj.X, X); %! assert_equal (obj.Distance, "euclidean"); %! assert_equal (isempty (obj.DistParameter), true); %!test %! ## Minkowski distance with custom P %! X = [0, 0; 1, 1; 2, 2]; %! obj = hnswSearcher (X, 'Distance', 'minkowski', 'P', 3); %! assert_equal (obj.Distance, "minkowski"); %! assert_equal (obj.DistParameter, 3); %!test %! ## Seuclidean distance with custom Scale %! X = [1, 2; 3, 4; 5, 6]; %! S = [1, 2]; %! obj = hnswSearcher (X, 'Distance', 'seuclidean', 'Scale', S); %! assert_equal (obj.Distance, "seuclidean"); %! assert_equal (obj.DistParameter, S); %!test %! ## Mahalanobis distance with custom Cov %! X = [1, 2; 3, 4; 5, 6]; %! C = [1, 0; 0, 1]; %! obj = hnswSearcher (X, 'Distance', 'mahalanobis', 'Cov', C); %! assert_equal (obj.Distance, "mahalanobis"); %! assert_equal (obj.DistParameter, C); %!test %! ## knnsearch with Euclidean distance %! X = [1, 2; 3, 4; 5, 6]; %! obj = hnswSearcher (X); %! Y = [2, 3]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (ismember (idx, [2]), true); %! assert_equal (abs (D - sqrt (2)) < 1e-2, true); %!test %! ## knnsearch with Cityblock distance %! X = [0, 0; 1, 1; 2, 2]; %! obj = hnswSearcher (X, 'Distance', 'cityblock'); %! Y = [1, 0]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (ismember (idx, [1, 2]), true); %! assert_equal (abs (D - 1) < 1e-2, true); %!test %! ## knnsearch with Chebychev distance %! X = [1, 1; 2, 3; 4, 2]; %! obj = hnswSearcher (X, 'Distance', 'chebychev'); %! Y = [2, 2]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (ismember (idx, [1, 2]), true); %! assert_equal (abs (D - 1) < 1e-2, true); %!test %! ## knnsearch with Minkowski P=3 %! X = [0, 0; 1, 0; 2, 0]; %! obj = hnswSearcher (X, 'Distance', 'minkowski', 'P', 3); %! Y = [1, 0]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (ismember (idx, [2]), true); %! assert_equal (abs (D - 0) < 1e-2, true); %!test %! ## Diverse dataset with Euclidean %! X = [0, 10; 5, 5; 10, 0]; %! obj = hnswSearcher (X); %! Y = [5, 5]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (ismember (idx, [2]), true); %! assert_equal (abs (D - 0) < 1e-2, true); %!test %! ## High-dimensional data with Cityblock %! X = [1, 2, 3; 4, 5, 6; 7, 8, 9]; %! obj = hnswSearcher (X, 'Distance', 'cityblock'); %! Y = [4, 5, 6]; %! [idx, D] = knnsearch (obj, Y, 'K', 1); %! assert_equal (ismember (idx, [2]), true); %! assert_equal (abs (D - 0) < 1e-2, true); ## Raising 'SearchSetSize' must not make the search worse. The early ## termination check read the last element of the best list on the assumption ## that the list was sorted, but the list is only sorted when it is trimmed, ## and it is only trimmed once it grows past 'SearchSetSize'. For a large ## enough 'SearchSetSize' the trim never ran, the check compared whatever ## distance happened to have been appended last, and the walk stopped at the ## entry point -- returning the same neighbour for every query. %!test %! ## both the data and the graph are drawn, so seed both: the search is %! ## approximate and an unlucky graph misses a true neighbour %! rand ("seed", 42); %! randn ("seed", 4); %! X = [randn(20,2); randn(20,2) + 3; randn(20,2) + [0, 6]]; %! Y = randn (8, 2); %! hn = hnswSearcher (X); %! es = ExhaustiveSearcher (X); %! truth = knnsearch (es, Y, 'K', 3); %! for ess = [5, 16, 31, 32, 33, 60] %! assert_equal (knnsearch (hn, Y, 'K', 3, 'SearchSetSize', ess), truth); %! endfor %!test %! ## the boundary that used to break was 2 * MaxNumLinksPerNode %! rand ("seed", 42); %! randn ("seed", 4); %! X = [randn(20,2); randn(20,2) + 3; randn(20,2) + [0, 6]]; %! Y = randn (8, 2); %! hn = hnswSearcher (X); %! assert_equal (knnsearch (hn, Y, 'K', 3, 'SearchSetSize', 31), ... %! knnsearch (hn, Y, 'K', 3, 'SearchSetSize', 32)); %!test %! ## distinct queries must not collapse onto one index %! rand ("seed", 42); %! randn ("seed", 4); %! X = [randn(20,2); randn(20,2) + 3; randn(20,2) + [0, 6]]; %! Y = randn (8, 2); %! idx = knnsearch (hnswSearcher (X), Y, 'K', 3, 'SearchSetSize', 60); %! assert_equal (numel (unique (idx(:,1))) > 1, true); ## A Minkowski exponent of Inf is the Chebychev distance: a searcher built ## with it and the same draws as a Chebychev one answers as that one does. ## MATLAB R2024a takes the exponent and returns wrong neighbours. %!test %! X = [1, 2; 4, 6; 0, 0; 3, 1; 5, 5; 2, 3]; %! Y = [1, 1; 4, 4]; %! rand ("seed", 7); %! A = hnswSearcher (X, 'Distance', 'minkowski', 'P', Inf); %! rand ("seed", 7); %! B = hnswSearcher (X, 'Distance', 'chebychev'); %! [ia, Da] = knnsearch (A, Y, 'K', 2); %! [ib, Db] = knnsearch (B, Y, 'K', 2); %! assert_equal (A.Distance, 'minkowski'); %! assert_equal (A.DistParameter, Inf); %! assert_equal (ia, ib); %! assert_equal (Da, [1, 1; 1, 2]); %! assert_equal (Db, [1, 1; 1, 2]); %!test %! X = [1, 2; 4, 6; 0, 0; 3, 1; 5, 5; 2, 3]; %! A = createns (X, 'NSMethod', 'hnsw', 'Distance', 'minkowski', 'P', Inf); %! assert_equal (class (A), 'hnswSearcher'); %! assert_equal (A.DistParameter, Inf); ## Test Input Validation %!error ... %! hnswSearcher () %!error ... %! hnswSearcher (ones (3,2), 'Distance') %!error ... %! hnswSearcher ([]) %!error ... %! hnswSearcher ('abc') %!error ... %! hnswSearcher ([1; Inf; 3]) %!error ... %! hnswSearcher (ones (3,2), 'foo', 'bar') %!error ... %! hnswSearcher (ones (3,2), 'Distance', 'invalid') %!error ... %! hnswSearcher (ones (3,2), 'Distance', 1) %!error ... %! hnswSearcher (ones (3,2), 'Distance', 'minkowski', 'P', -1) %!error ... %! hnswSearcher (ones (3,2), 'Distance', 'minkowski', 'P', NaN) %!error ... %! hnswSearcher (ones (3,2), 'P', 3) %!error ... %! hnswSearcher (ones (3,2), 'Distance', 'chebychev', 'P', 3) %!error ... %! hnswSearcher (ones (3,2), 'Scale', [1, 1]) %!error ... %! hnswSearcher (ones (3,2), 'Distance', 'mahalanobis', 'Scale', [1, 1]) %!error ... %! hnswSearcher (ones (3,2), 'Cov', eye (2)) %!error ... %! hnswSearcher (ones (3,2), 'Distance', 'seuclidean', 'Cov', eye (2)) %!error ... %! hnswSearcher (ones (3,2), 'Distance', 'seuclidean', 'Scale', [-1, 1]) %!error ... %! hnswSearcher (ones (3,2), 'Distance', 'mahalanobis', 'Cov', ones (3,3)) %!error ... %! hnswSearcher (ones (3,2), 'Distance', 'mahalanobis', 'Cov', [1, 2; 3, 4]) %!error ... %! hnswSearcher (ones (3,2), 'Distance', 'mahalanobis', 'Cov', -eye (2)) %!error ... %! hnswSearcher (ones (3,2), 'MaxNumLinksPerNode', 0) %!error ... %! hnswSearcher (ones (3,2), 'TrainSetSize', -1) %!error ... %! hnswSearcher (ones (3,2), 'TrainSetSize', 4) %!error ... %! hnswSearcher (ones (3,2), 'MaxNumLinksPerNode', 200, 'TrainSetSize', 100) %!error ... %! knnsearch (hnswSearcher (ones (3,2))) %!error ... %! knnsearch (hnswSearcher (ones (3,2)), ones (3,2), 'K') %!error ... %! knnsearch (hnswSearcher (ones (3,2)), {1, 2}) %!error ... %! knnsearch (hnswSearcher (ones (3,2)), ones (3,2), 'K', 0) %!error ... %! knnsearch (hnswSearcher (ones (3,2)), ones (3,2), 'foo', 'bar') %!error ... %! obj = hnswSearcher (ones (3,2)); obj(1) %!error ... %! obj = hnswSearcher (ones (3,2)); obj{1} %!error ... %! obj = hnswSearcher (ones (3,2)); obj.invalid %!error ... %! obj = hnswSearcher (ones (3,2)); obj(1) = 1 %!error ... %! obj = hnswSearcher (ones (3,2)); obj{1} = 1 %!error ... %! obj = hnswSearcher (ones (3,2)); obj.X = 1 %!error ... %! obj = hnswSearcher (ones (3,2)); obj.HNSWGraph = 1 %!error ... %! obj = hnswSearcher (ones (3,2)); obj.Distance = 'invalid' %!error ... %! obj = hnswSearcher (ones (3,2)); obj.Distance = 1 %!error ... %! obj = hnswSearcher (ones (3,2), 'Distance', 'minkowski'); obj.DistParameter = -1 %!error ... %! obj = hnswSearcher (ones (3,2), 'Distance', 'seuclidean'); obj.DistParameter = [-1, 1] %!error ... %! obj = hnswSearcher (ones (3,2), 'Distance', 'mahalanobis'); obj.DistParameter = ones (3,3) %!error ... %! obj = hnswSearcher (ones (3,2), 'Distance', 'mahalanobis'); obj.DistParameter = -eye (2) %!error ... %! obj = hnswSearcher (ones (3,2)); obj.DistParameter = 1 %!error ... %! obj = hnswSearcher (ones (3,2)); obj.MaxNumLinksPerNode = 0 %!error ... %! obj = hnswSearcher (ones (3,2)); obj.TrainSetSize = -1 %!error ... %! obj = hnswSearcher (ones (3,2)); obj.efSearch = 1.5 %!error ... %! obj = hnswSearcher (ones (3,2)); obj.invalid = 1 ## More neighbours than there are points, and queries that are empty or not ## finite, are answered rather than refused. Verified against MATLAB R2024a. %!shared Xs, Ys, hs %! ## the graph is built from rand, so seed it: the search is approximate and %! ## an unlucky graph misses a true neighbour %! rand ("seed", 42); %! randn ("seed", 4); %! Xs = [randn(20,2); randn(20,2) + 3; randn(20,2) + [0, 6]]; %! Ys = randn (8, 2); %! hs = hnswSearcher (Xs); %!test # asking for every point returns every point, not a padding of NaN %! [idx, D] = knnsearch (hs, Ys, "K", 60); %! assert_equal (size (idx), [8, 60]); %! assert_equal (any (isnan (idx(:))), false); %! for r = 1:8 %! assert_equal (sort (idx(r,:)), 1:60); %! endfor %!test # and the neighbours are the right ones, not merely present %! for K = [1, 5, 30, 60] %! [~, Dh] = knnsearch (hs, Ys, "K", K); %! [~, De] = knnsearch (ExhaustiveSearcher (Xs), Ys, "K", K); %! assert_equal (Dh, De, 1e-12); %! endfor %!test # more neighbours than points gives all of them, not a wider matrix %! for K = [61, 100, 1000] %! [idx, D] = knnsearch (hs, Ys, "K", K); %! assert_equal (size (idx), [8, 60]); %! assert_equal (size (D), [8, 60]); %! assert_equal (any (isnan (idx(:))), false); %! endfor %!test # an empty query is answered with an empty result %! [idx, D] = knnsearch (hs, zeros (0, 2), "K", 3); %! assert_equal (size (idx), [0, 3]); %! assert_equal (size (D), [0, 3]); %!test # a query that is not finite is answered, its distances saying so %! [idx, D] = knnsearch (hs, [Inf, 0], "K", 3); %! assert_equal (size (idx), [1, 3]); %! assert_equal (all (isinf (D)), true); %! [idx, D] = knnsearch (hs, [NaN, NaN], "K", 3); %! assert_equal (size (idx), [1, 3]); %! assert_equal (all (isnan (D)), true); ## Single precision is carried through to the distances, every other class is ## converted up to double, and the indices are always double. %!test %! rand ("seed", 42); %! X = [1, 2; 3, 4; 5, 6; 7, 8]; Y = [2, 3; 6, 7]; %! obj = hnswSearcher (single (X)); %! assert_equal (class (obj.X), 'single'); %! [idx, D] = knnsearch (obj, single (Y), "K", 2); %! assert_equal (class (idx), 'double'); %! assert_equal (class (D), 'single'); %! ## a double query against single data is still computed in single %! [~, Dm] = knnsearch (obj, Y, "K", 2); %! assert_equal (class (Dm), 'single'); %! assert_equal (Dm, D); %! ## and so is a single query against double data %! [~, Ds] = knnsearch (hnswSearcher (X), single (Y), "K", 2); %! assert_equal (class (Ds), 'single'); %! ## double throughout stays double %! [~, Dd] = knnsearch (hnswSearcher (X), Y, "K", 2); %! assert_equal (class (Dd), 'double'); ## Integer data is converted to double on the way in. Held as int32 the ## coordinate differences were rounded and the distances came out wrong. %!test %! rand ("seed", 42); %! X = int32 ([10, 20; 33, 41; 55, 62]); Y = [21, 33]; %! obj = hnswSearcher (X); %! assert_equal (class (obj.X), 'double'); %! [~, D] = knnsearch (obj, Y, "K", 1); %! assert_equal (D, min (sqrt (sum ((double (X) - Y) .^ 2, 2))), 1e-12); statistics-release-2.1.2/inst/Nearest_Neighbors/knnsearch.m000066400000000000000000001136131527311104300240360ustar00rootroot00000000000000## Copyright (C) 2023-2024 Andreas Bertsatos ## Copyright (C) 2023 Mohammed Azmat Khan ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{idx} =} knnsearch (@var{X}, @var{Y}) ## @deftypefnx {statistics} {[@var{idx}, @var{D}] =} knnsearch (@var{X}, @var{Y}) ## @deftypefnx {statistics} {[@dots{}] =} knnsearch (@dots{}, @var{name}, @var{value}) ## ## Find k-nearest neighbors from input data. ## ## @code{@var{idx} = knnsearch (@var{X}, @var{Y})} finds @math{K} nearest ## neighbors in @var{X} for @var{Y}. It returns @var{idx} which contains indices ## of @math{K} nearest neighbors of each row of @var{Y}, If not specified, ## @qcode{@var{K} = 1}. @var{X} must be an @math{N*P} numeric matrix of input ## data, where rows correspond to observations and columns correspond to ## features or variables. @var{Y} is an @math{M*P} numeric matrix with query ## points, which must have the same numbers of column as @var{X}. ## ## @code{[@var{idx}, @var{D}] = knnsearch (@var{X}, @var{Y})} also returns the ## the distances, @var{D}, which correspond to the @math{K} nearest neighbour in ## @var{X} for each @var{Y} ## ## Additional parameters can be specified by @qcode{Name-Value} pair arguments. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'K'} @tab is the number of nearest neighbors to be found ## in the kNN search. It must be a positive integer value and by default it is ## 1. ## ## @item @qcode{'P'} @tab is the Minkowski distance exponent and it must be ## a positive scalar. This argument is only valid when the selected distance ## metric is @qcode{'minkowski'}. By default it is 2. An exponent of ## @code{Inf} gives the Chebychev distance. ## ## @item @qcode{'Scale'} @tab is the scale parameter for the standardized ## Euclidean distance and it must be a nonnegative numeric vector of equal ## length to the number of columns in @var{X}. This argument is only valid when ## the selected distance metric is @qcode{'seuclidean'}, in which case each ## coordinate of @var{X} is scaled by the corresponding element of ## @qcode{'scale'}, as is each query point in @var{Y}. By default, the scale ## parameter is the standard deviation of each coordinate in @var{X}. ## ## @item @qcode{'Cov'} @tab is the covariance matrix for computing the ## mahalanobis distance and it must be a positive definite matrix matching the ## the number of columns in @var{X}. This argument is only valid when the ## selected distance metric is @qcode{'mahalanobis'}. ## ## @item @qcode{'BucketSize'} @tab is the maximum number of data points in ## the leaf node of the Kd-tree and it must be a positive integer. This ## argument is only valid when the selected search method is @qcode{'kdtree'}. ## ## @item @qcode{'SortIndices'} @tab is a boolean flag to sort the returned ## indices in ascending order by distance and it is @qcode{true} by default. ## When the selected search method is @qcode{'exhaustive'} or the ## @qcode{'IncludeTies'} flag is true, @code{knnsearch} always sorts the ## returned indices. ## ## @item @qcode{'Distance'} @tab is the distance metric used by ## @code{knnsearch} as specified below: ## @end multitable ## ## @multitable @columnfractions 0.2 0.75 ## @item @qcode{'euclidean'} @tab Euclidean distance. ## @item @qcode{'seuclidean'} @tab standardized Euclidean distance. Each ## coordinate difference between the rows in @var{X} and the query matrix ## @var{Y} is scaled by dividing by the corresponding element of the standard ## deviation computed from @var{X}. To specify a different scaling, use the ## @qcode{'Scale'} name-value argument. ## @item @qcode{'cityblock'} @tab City block distance. ## @item @qcode{'chebychev'} @tab Chebychev distance (maximum coordinate ## difference). A difference of @code{Inf - Inf} is @code{NaN}, and so is the ## distance, as for every other metric; MATLAB passes over it. ## @item @qcode{'minkowski'} @tab Minkowski distance. The default exponent ## is 2. To specify a different exponent, use the @qcode{'P'} name-value ## argument. ## @item @qcode{'mahalanobis'} @tab Mahalanobis distance, computed using a ## positive definite covariance matrix. To change the value of the covariance ## matrix, use the @qcode{'Cov'} name-value argument. ## @item @qcode{'cosine'} @tab Cosine distance. ## @item @qcode{'correlation'} @tab One minus the sample linear correlation ## between observations (treated as sequences of values). ## @item @qcode{'spearman'} @tab One minus the sample Spearman's rank ## correlation between observations (treated as sequences of values). ## @item @qcode{'hamming'} @tab Hamming distance, which is the percentage ## of coordinates that differ. ## @item @qcode{'jaccard'} @tab One minus the Jaccard coefficient, which is ## the percentage of nonzero coordinates that differ. ## @item @var{@@distfun} @tab Custom distance function handle. A distance ## function of the form @code{function @var{D2} = distfun (@var{XI}, @var{YI})}, ## where @var{XI} is a @math{1*P} vector containing a single observation in ## @math{P}-dimensional space, @var{YI} is an @math{N*P} matrix containing an ## arbitrary number of observations in the same @math{P}-dimensional space, and ## @var{D2} is an @math{N*P} vector of distances, where @qcode{(@var{D2}k)} is ## the distance between observations @var{XI} and @qcode{(@var{YI}k,:)}. ## @end multitable ## ## @multitable @columnfractions 0.18 0.8 ## @item @qcode{'NSMethod'} @tab is the nearest neighbor search method used ## by @code{knnsearch} as specified below. ## @end multitable ## ## @multitable @columnfractions 0.2 0.75 ## @item @qcode{'kdtree'} @tab Creates and uses a Kd-tree to find nearest ## neighbors. @qcode{'kdtree'} is the default value when the number of columns ## in @var{X} is less than or equal to 10, @var{X} is not sparse, and the ## distance metric is @qcode{'euclidean'}, @qcode{'cityblock'}, ## @qcode{'manhattan'}, @qcode{'chebychev'}, or @qcode{'minkowski'}. Otherwise, ## the default value is @qcode{'exhaustive'}. This argument is only valid when ## the distance metric is one of the four aforementioned metrics. ## @item @qcode{'exhaustive'} @tab Uses the exhaustive search algorithm by ## computing the distance values from all the points in @var{X} to each point in ## @var{Y}. ## @end multitable ## ## @multitable @columnfractions 0.18 0.8 ## @item @qcode{'IncludeTies'} @tab is a boolean flag to indicate if the ## returned values should contain the indices that have same distance as the ## @math{K^th} neighbor. When @qcode{false}, @code{knnsearch} chooses the ## observation with the smallest index among the observations that have the same ## distance from a query point. When @qcode{true}, @code{knnsearch} includes ## all nearest neighbors whose distances are equal to the @math{K^th} smallest ## distance in the output arguments. To specify @math{K}, use the @qcode{'K'} ## name-value pair argument. In that case @var{idx} and @var{D} are ## @math{M}-by-@math{1} cell arrays and each cell holds a @emph{row} vector, ## whichever search method is used. ## @end multitable ## ## @seealso{rangesearch, pdist2, fitcknn} ## @end deftypefn function [idx, dist] = knnsearch (X, Y, varargin) ## Check input data if (nargin < 2) error ("knnsearch: too few input arguments."); endif if (size (X, 2) != size (Y, 2)) error ("knnsearch: number of columns in X and Y must match."); endif ## Add default values K = 1; # Number of nearest neighbors P = 2; # Exponent for Minkowski distance S = []; # Scale for the standardized Euclidean distance C = []; # Covariance matrix for Mahalanobis distance BS = 50; # Maximum number of points per leaf node for Kd-tree SI = true; # Sort returned indices according to distance Distance = 'euclidean'; # Distance metric to be used NSMethod = []; # Nearest neighbor search method InclTies = false; # Include ties for distance with kth neighbor DistParameter = []; # Distance parameter for pdist2 ## Parse additional parameters in Name/Value pairs PSC = 0; PG = false; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'k' K = varargin{2}; case 'p' P = varargin{2}; PSC += 1; PG = true; case 'scale' S = varargin{2}; PSC += 1; case 'cov' C = varargin{2}; PSC += 1; case 'bucketsize' BS = varargin{2}; case 'sortindices' SI = varargin{2}; case 'distance' Distance = varargin{2}; ## 'manhattan' is this package's documented alias of 'cityblock' and ## is resolved here, so every search path takes it. MATLAB has no ## such name and refuses it. if (ischar (Distance) && strcmpi (Distance, 'manhattan')) Distance = 'cityblock'; endif case 'nsmethod' NSMethod = varargin{2}; case 'includeties' InclTies = varargin{2}; otherwise error ("knnsearch: invalid NAME in optional pairs of arguments."); endswitch varargin(1:2) = []; endwhile ## Check input parameters if (PSC > 1) error ("knnsearch: only a single distance parameter can be defined."); endif if (! isscalar (K) || ! isnumeric (K) || K < 1 || K != round (K)) error ("knnsearch: invalid value of K."); endif K = min (K, rows (X)); if (! (isscalar (P) && isnumeric (P) && P > 0)) error ("knnsearch: invalid value of Minkowski Exponent."); endif if (PG && ! (ischar (Distance) && strcmpi (Distance, 'minkowski'))) error ("knnsearch: 'P' applies only to the minkowski metric."); endif if (! isempty (S)) if (any (S) < 0 || numel (S) != columns (X) || ! strcmpi (Distance, 'seuclidean')) error ("knnsearch: invalid value in Scale or the size of Scale."); endif endif if (! isempty (C)) if (! strcmp (Distance, 'mahalanobis') || ! ismatrix (C) || ! isnumeric (C)) error (strcat ("knnsearch: invalid value in Cov, Cov can only", ... " be given for mahalanobis distance.")); endif endif if (! isscalar (BS) || ! isnumeric (BS) || BS < 0 || fix (BS) != BS) error ("knnsearch: invalid value of bucketsize."); endif ## Select the appropriate distance parameter if (strcmpi (Distance, 'minkowski')) DistParameter = P; ## A Minkowski exponent of Inf is the Chebychev distance, its limit, which ## every search below computes directly if (P == Inf) Distance = 'chebychev'; DistParameter = []; endif elseif (strcmpi (Distance, 'seuclidean')) DistParameter = S; elseif (strcmpi (Distance, 'mahalanobis')) DistParameter = C; endif ## Check NSMethod and set kdtree as default if the conditions match if (isempty (NSMethod)) ## Set default method 'kdtree' if conditions are satisfied; if (! issparse (X) && (columns (X) <= 10) && (strcmpi (Distance, 'euclidean') || strcmpi (Distance, 'cityblock') || strcmpi (Distance, 'manhattan') || strcmpi (Distance, 'minkowski') || strcmpi (Distance, 'chebychev'))) NSMethod = 'kdtree'; else NSMethod = 'exhaustive'; endif else ## Disallow kdtree with custom distance functions if (strcmpi (NSMethod, 'kdtree') && isa (Distance, 'function_handle')) error (strcat ("knnsearch: 'kdtree' cannot be used", ... " with custom distance functions.")); endif ## Check if kdtree can be used if (strcmpi (NSMethod, 'kdtree') && ! (strcmpi (Distance, 'euclidean') || strcmpi (Distance, 'cityblock') || strcmpi (Distance, 'minkowski') || strcmpi (Distance, 'chebychev'))) error (strcat ("knnsearch: 'kdtree' cannot be used", ... " with the given distance metric.")); endif endif ## Distances are computed in single precision when either the training data ## or the query is single, and in double otherwise. The indices are always ## double. cls = "double"; if (isa (X, "single") || isa (Y, "single")) cls = "single"; endif X = cast (X, cls); Y = cast (Y, cls); ## With no observations to search, or no query points to search for, there ## is nothing to compute. K is already capped at ROWS (X), so ROWS (Y) by K ## gives the shapes MATLAB returns in both cases and in the case where both ## are empty. This returns before the search, which cannot take an empty ## argument: the Kd-tree indexes its first point and the compiled exhaustive ## search refuses one outright. if (rows (X) == 0 || rows (Y) == 0) if (InclTies) idx = repmat ({zeros(1, 0)}, rows (Y), 1); dist = repmat ({zeros(1, 0, cls)}, rows (Y), 1); else idx = zeros (rows (Y), K); dist = zeros (rows (Y), K, cls); endif return; endif ## Check for NSMethod if (strcmpi (NSMethod, 'kdtree')) ## Build kdtree and search the query point kdtree = __build_kdtree__ (1:size (X,1), 0, X, BS); ## Check for ties and sortindices if (! InclTies) ## Only return k neighbors dist = zeros (rows (Y), K, cls); idx = zeros (rows (Y), K); for i = 1:rows (Y) [temp_idx, temp_D] = __search_kdtree__ (kdtree, Y(i,:), K, X, ... Distance, DistParameter, ... false); if (SI) [sorted_D, sort_idx] = sort (temp_D); idx(i,:) = temp_idx(sort_idx); dist(i,:) = sorted_D; else idx(i,:) = temp_idx; dist(i,:) = temp_D; endif endfor else ## Return all neighbors as cell dist = cell (rows (Y), 1); idx = cell (rows (Y), 1); for i = 1:rows (Y) [temp_idx, temp_D] = __search_kdtree__ (kdtree, Y(i,:), K, ... X, Distance, DistParameter, ... false); if (isnan (temp_D(end))) ## A NaN distance ties nothing, so the K found are the answer, laid ## in row order as the range search below returns them [tied_idx, o] = sort (temp_idx); tied_D = temp_D(o); else r = temp_D(end) + 1e-10; # Add small epsilon to capture ties [tied_idx, tied_D] = __search_kdtree__ (kdtree, Y(i,:), Inf, X, ... Distance, DistParameter, ... true, r); endif ## Each cell holds a row vector, as MATLAB returns and as both the ## exhaustive path below and RANGESEARCH already did. The kd-tree ## search returns columns, so they are laid down explicitly here. if (SI) [sorted_D, sort_idx] = sort (tied_D); idx{i} = tied_idx(sort_idx)(:).'; dist{i} = sorted_D(:).'; else idx{i} = tied_idx(:).'; dist{i} = tied_D(:).'; endif endfor endif else ## Where the metric is one the compiled search knows, the whole search ## runs there: each distance is compared against a running list of the K ## best and discarded, so the M-by-N matrix is never formed. That matrix ## is what puts a ceiling on N, being 128 MB at four thousand points ## against four thousand, and forming it cost about half of this branch. ## Every other metric keeps the older route, pdist2 followed by a partial ## selection. if (! InclTies && ischar (Distance) && any (strcmpi (Distance, {'euclidean', 'cityblock', ... 'chebychev', 'minkowski'}))) [idx, dist] = __knnbrute__ (X, Y, K, lower (Distance), DistParameter); ## Calculate all distances. The single-neighbour shortcut applies only ## when ties are not wanted: taking the minimum returns one neighbour ## whatever else sits at the same distance, so asking for K = 1 with ## 'IncludeTies' used to get the flag silently ignored, and a double where ## a cell was promised. elseif (K == 1 && ! InclTies) D = pdist2 (X, Y, Distance, DistParameter); D = reshape (D', size (Y, 1), size (X, 1)); [dist, idx] = min (D, [], 2); else # always sort indices in this case if (InclTies) dist = cell (rows (Y), 1); idx = cell (rows (Y), 1); for i = 1:rows (Y) D = pdist2 (X, Y(i,:), Distance, DistParameter); [dt, id] = sort (D); kth_dist = dt(K); tied_idx = (dt <= kth_dist); ## A NaN distance ties nothing, so the K nearest are the answer if (isnan (kth_dist)) tied_idx = ((1:numel (dt))' <= K); endif dist {i} = dt(tied_idx, :)'; idx {i} = id(tied_idx, :)'; endfor else ## No ties included. The K smallest come from a partial selection ## rather than from sorting every row and discarding the tail, which ## is where this branch used to spend most of its time. D = pdist2 (X, Y, Distance, DistParameter); D = reshape (D', size (Y, 1), size (X, 1)); [idx, dist] = __knnselect__ (D, K); endif endif endif endfunction %!demo %! ## find 10 nearest neighbour of a point using different distance metrics %! ## and compare the results by plotting %! load fisheriris %! X = meas(:,3:4); %! Y = species; %! point = [5, 1.45]; %! %! ## calculate 10 nearest-neighbours by minkowski distance %! [id, d] = knnsearch (X, point, 'K', 10); %! %! ## calculate 10 nearest-neighbours by minkowski distance %! [idm, dm] = knnsearch (X, point, 'K', 10, 'distance', 'minkowski', 'p', 5); %! %! ## calculate 10 nearest-neighbours by chebychev distance %! [idc, dc] = knnsearch (X, point, 'K', 10, 'distance', 'chebychev'); %! %! ## plotting the results %! gscatter (X(:,1), X(:,2), species, [.75 .75 0; 0 .75 .75; .75 0 .75], '.', 20); %! title ('Fisher''s Iris Data - Nearest Neighbors with different types of distance metrics'); %! xlabel ('Petal length (cm)'); %! ylabel ('Petal width (cm)'); %! %! line (point(1), point(2), 'marker', 'X', 'color', 'k', ... %! 'linewidth', 2, 'displayname', 'query point') %! line (X(id,1), X(id,2), 'color', [0.5 0.5 0.5], 'marker', 'o', ... %! 'linestyle', 'none', 'markersize', 10, 'displayname', 'euclidean') %! line (X(idm,1), X(idm,2), 'color', [0.5 0.5 0.5], 'marker', 'd', ... %! 'linestyle', 'none', 'markersize', 10, 'displayname', 'Minkowski') %! line (X(idc,1), X(idc,2), 'color', [0.5 0.5 0.5], 'marker', 'p', ... %! 'linestyle', 'none', 'markersize', 10, 'displayname', 'chebychev') %! xlim ([4.5 5.5]); %! ylim ([1 2]); %! axis square; %!demo %! ## knnsearch on iris dataset using kdtree method %! load fisheriris %! X = meas(:,3:4); %! gscatter (X(:,1), X(:,2), species, [.75 .75 0; 0 .75 .75; .75 0 .75], '.', 20); %! title ('Fisher''s iris dataset : Nearest Neighbors with kdtree search'); %! %! ## new point to be predicted %! point = [5 1.45]; %! %! line (point(1), point(2), 'marker', 'X', 'color', 'k', ... %! 'linewidth', 2, 'displayname', 'query point') %! %! ## knnsearch using kdtree method %! [idx, d] = knnsearch (X, point, 'K', 10, 'NSMethod', 'kdtree'); %! %! ## plotting predicted neighbours %! line (X(idx,1), X(idx,2), 'color', [0.5 0.5 0.5], 'marker', 'o', ... %! 'linestyle', 'none', 'markersize', 10, ... %! 'displayname', 'nearest neighbour') %! xlim ([4 6]) %! ylim ([1 3]) %! axis square %! ## details of predicted labels %! tabulate (species(idx)) %! %! ctr = point - d(end); %! diameter = 2 * d(end); %! ## Draw a circle around the 10 nearest neighbors. %! h = rectangle ('position', [ctr, diameter, diameter], 'curvature', [1 1]); %! %! ## here only 8 neighbours are plotted instead of 10 since the dataset %! ## contains duplicate values ## Test output %!shared X, Y %! X = [1, 2, 3, 4; 2, 3, 4, 5; 3, 4, 5, 6]; %! Y = [1, 2, 2, 3; 2, 3, 3, 4]; %!test %! [idx, D] = knnsearch (X, Y, 'Distance', 'euclidean'); %! assert_equal (idx, [1; 1]); %! assert_equal (D, ones (2, 1) * sqrt (2)); %!test %! eucldist = @(v,m) sqrt (sumsq (repmat (v,rows (m),1)-m,2)); %! [idx, D] = knnsearch (X, Y, 'Distance', eucldist); %! assert_equal (idx, [1; 1]); %! assert_equal (D, ones (2, 1) * sqrt (2)); %!test %! [idx, D] = knnsearch (X, Y, 'Distance', 'euclidean', 'includeties', true); %! assert_equal (iscell (idx), true); %! assert_equal (iscell (D), true) %! assert_equal (idx {1}, [1]); %! assert_equal (idx {2}, [1, 2]); %! assert_equal (D{1}, ones (1, 1) * sqrt (2)); %! assert_equal (D{2}, ones (1, 2) * sqrt (2)); %!test %! [idx, D] = knnsearch (X, Y, 'Distance', 'euclidean', 'k', 2); %! assert_equal (idx, [1, 2; 1, 2]); %! assert_equal (D, [sqrt(2), 3.162277660168380; sqrt(2), sqrt(2)], 1e-14); %!test %! [idx, D] = knnsearch (X, Y, 'Distance', 'seuclidean'); %! assert_equal (idx, [1; 1]); %! assert_equal (D, ones (2, 1) * sqrt (2)); %!test %! [idx, D] = knnsearch (X, Y, 'Distance', 'seuclidean', 'k', 2); %! assert_equal (idx, [1, 2; 1, 2]); %! assert_equal (D, [sqrt(2), 3.162277660168380; sqrt(2), sqrt(2)], 1e-14); %!test %! xx = [1, 2; 1, 3; 2, 4; 3, 6]; %! yy = [2, 4; 2, 6]; %! [idx, D] = knnsearch (xx, yy, 'Distance', 'mahalanobis'); %! assert_equal (idx, [3; 2]); %! assert_equal (D, [0; 3.162277660168377], 1e-14); %!test %! [idx, D] = knnsearch (X, Y, 'Distance', 'minkowski'); %! assert_equal (idx, [1; 1]); %! assert_equal (D, ones (2, 1) * sqrt (2)); %!test %! [idx, D] = knnsearch (X, Y, 'Distance', 'minkowski', 'p', 3); %! assert_equal (idx, [1; 1]); %! assert_equal (D, ones (2, 1) * 1.259921049894873, 1e-14); %!test %! [idx, D] = knnsearch (X, Y, 'Distance', 'cityblock'); %! assert_equal (idx, [1; 1]); %! assert_equal (D, [2; 2]); %!test %! [idx, D] = knnsearch (X, Y, 'Distance', 'chebychev'); %! assert_equal (idx, [1; 1]); %! assert_equal (D, [1; 1]); %!test %! [idx, D] = knnsearch (X, Y, 'Distance', 'cosine'); %! assert_equal (idx, [2; 3]); %! assert_equal (D, [0.005674536395645; 0.002911214328620], 1e-14); %!test %! [idx, D] = knnsearch (X, Y, 'Distance', 'correlation'); %! assert_equal (idx, [1; 1]); %! assert_equal (D, ones (2, 1) * 0.051316701949486, 1e-14); %!test %! [idx, D] = knnsearch (X, Y, 'Distance', 'spearman'); %! assert_equal (idx, [1; 1]); %! assert_equal (D, ones (2, 1) * 0.051316701949486, 1e-14); %!test %! [idx, D] = knnsearch (X, Y, 'Distance', 'hamming'); %! assert_equal (idx, [1; 1]); %! assert_equal (D, [0.5; 0.5]); %!test %! [idx, D] = knnsearch (X, Y, 'Distance', 'jaccard'); %! assert_equal (idx, [1; 1]); %! assert_equal (D, [0.5; 0.5]); %!test %! [idx, D] = knnsearch (X, Y, 'Distance', 'jaccard', 'k', 2); %! assert_equal (idx, [1, 2; 1, 2]); %! assert_equal (D, [0.5, 1; 0.5, 0.5]); %!test %! a = [1, 5; 1, 2; 2, 2; 1.5, 1.5; 5, 1; 2 -1.34; 1, -3; 4, -4; -3, 1; 8, 9]; %! b = [1, 1]; %! [idx, D] = knnsearch (a, b, 'K', 5, 'NSMethod', 'kdtree', 'includeties', true); %! assert_equal (iscell (idx), true); %! assert_equal (iscell (D), true) %! assert_equal (cell2mat (idx), [4, 2, 3, 6, 1, 5, 7, 9]); %! assert_equal (cell2mat (D), [0.7071, 1.0000, 1.4142, 2.5447, 4.0000, 4.0000, 4.0000, 4.0000], 1e-4); %!test %! a = [1, 5; 1, 2; 2, 2; 1.5, 1.5; 5, 1; 2 -1.34; 1, -3; 4, -4; -3, 1; 8, 9]; %! b = [1, 1]; %! [idx, D] = knnsearch (a, b, 'K', 5, 'NSMethod', 'exhaustive', 'includeties', true); %! assert_equal (iscell (idx), true); %! assert_equal (iscell (D), true) %! assert_equal (cell2mat (idx), [4, 2, 3, 6, 1, 5, 7, 9]); %! assert_equal (cell2mat (D), [0.7071, 1.0000, 1.4142, 2.5447, 4.0000, 4.0000, 4.0000, 4.0000], 1e-4); %!test %! a = [1, 5; 1, 2; 2, 2; 1.5, 1.5; 5, 1; 2 -1.34; 1, -3; 4, -4; -3, 1; 8, 9]; %! b = [1, 1]; %! [idx, D] = knnsearch (a, b, 'K', 5, 'NSMethod', 'kdtree', 'includeties', false); %! assert_equal (iscell (idx), false); %! assert_equal (iscell (D), false) %! assert_equal (idx, [4, 2, 3, 6, 1]); %! assert_equal (D, [0.7071, 1.0000, 1.4142, 2.5447, 4.0000], 1e-4); %!test %! a = [1, 5; 1, 2; 2, 2; 1.5, 1.5; 5, 1; 2 -1.34; 1, -3; 4, -4; -3, 1; 8, 9]; %! b = [1, 1]; %! [idx, D] = knnsearch (a, b, 'K', 5, 'NSMethod', 'exhaustive', 'includeties', false); %! assert_equal (iscell (idx), false); %! assert_equal (iscell (D), false) %! assert_equal (idx, [4, 2, 3, 6, 1]); %! assert_equal (D, [0.7071, 1.0000, 1.4142, 2.5447, 4.0000], 1e-4); %!test %! load fisheriris %! a = meas; %! b = min (meas); %! [idx, D] = knnsearch (a, b, 'K', 5, 'NSMethod', 'kdtree'); %! assert_equal (idx, [42, 9, 14, 39, 13]); %! assert_equal (D, [0.5099, 0.9950, 1.0050, 1.0536, 1.1874], 1e-4); %!test %! load fisheriris %! a = meas; %! b = mean (meas); %! [idx, D] = knnsearch (a, b, 'K', 5, 'NSMethod', 'kdtree'); %! assert_equal (idx, [65, 83, 89, 72, 100]); %! assert_equal (D, [0.3451, 0.3869, 0.4354, 0.4481, 0.4625], 1e-4); %!test %! load fisheriris %! a = meas; %! b = max (meas); %! [idx, D] = knnsearch (a, b, 'K', 5, 'NSMethod', 'kdtree'); %! assert_equal (idx, [118, 132, 110, 106, 136]); %! assert_equal (D, [0.7280, 0.9274, 1.3304, 1.5166, 1.6371], 1e-4); %! %!test %! load fisheriris %! a = meas; %! b = max (meas); %! [idx, D] = knnsearch (a, b, 'K', 5, 'includeties', true); %! assert_equal (iscell (idx), true); %! assert_equal (iscell (D), true); %! assert_equal (cell2mat (idx), [118, 132, 110, 106, 136]); %! assert_equal (cell2mat (D), [0.7280, 0.9274, 1.3304, 1.5166, 1.6371], 1e-4); %!test # IncludeTies gives a row per cell, whichever method is used %! a = [1, 5; 1, 2; 2, 2; 1.5, 1.5; 5, 1; 2 -1.34; 1, -3; 4, -4; -3, 1; 8, 9]; %! b = [1, 1]; %! [ik, dk] = knnsearch (a, b, 'K', 5, 'NSMethod', 'kdtree', 'includeties', true); %! [ie, de] = knnsearch (a, b, 'K', 5, 'NSMethod', 'exhaustive', 'includeties', true); %! assert_equal (size (ik{1}), [1, 8]); %! assert_equal (size (dk{1}), [1, 8]); %! assert_equal (ik{1}, ie{1}); %! assert_equal (dk{1}, de{1}, 1e-12); %!test # the cell array itself is one column, as MATLAB returns %! a = [1, 5; 1, 2; 2, 2; 1.5, 1.5; 5, 1; 2 -1.34; 1, -3; 4, -4; -3, 1; 8, 9]; %! [idx, D] = knnsearch (a, [1, 1; 2, 2], 'K', 3, 'includeties', true); %! assert_equal (size (idx), [2, 1]); %! assert_equal (size (D), [2, 1]); %! assert_equal (rows (idx{1}), 1); %! assert_equal (rows (idx{2}), 1); %!test %! [idx, D] = knnsearch ([1, 2; 3, 4; 5, 6], [1, 2], 'K', 5); %! assert_equal (idx, [1, 2, 3]); %! assert_equal (D, [0, 2*sqrt(2), 4*sqrt(2)], 1e-14); ## An empty X or Y returns empty, as R2024a does, rather than raising: the ## shape is rows (Y) by K, K being already capped at rows (X). %!test %! [idx, D] = knnsearch (zeros (0, 2), [1, 2; 3, 4], 'K', 3); %! assert_equal (size (idx), [2, 0]); %! assert_equal (size (D), [2, 0]); %!test %! [idx, D] = knnsearch (zeros (0, 2), [1, 2], 'NSMethod', 'exhaustive'); %! assert_equal (size (idx), [1, 0]); %! assert_equal (size (D), [1, 0]); %!test %! [idx, D] = knnsearch ([1, 2; 3, 4], zeros (0, 2), 'K', 3); %! assert_equal (size (idx), [0, 2]); %! assert_equal (size (D), [0, 2]); %!test %! [idx, D] = knnsearch (zeros (0, 2), zeros (0, 2), 'K', 3); %! assert_equal (size (idx), [0, 0]); %! assert_equal (size (D), [0, 0]); %!test %! [idx, D] = knnsearch (zeros (0, 2), [1, 2; 3, 4], 'K', 3, ... %! 'IncludeTies', true); %! assert_equal (size (idx), [2, 1]); %! assert_equal (idx{1}, zeros (1, 0)); %! assert_equal (D{2}, zeros (1, 0)); %!test %! [idx, D] = knnsearch (single (zeros (0, 2)), [1, 2]); %! assert_equal (class (idx), 'double'); %! assert_equal (class (D), 'single'); ## A Minkowski exponent of Inf is the Chebychev distance, as in MATLAB R2026b, ## with every search method and with ties, which both queries have. %!shared X, Y %! X = [1, 2; 4, 6; 0, 0; 3, 1; 5, 5; 2, 3]; %! Y = [1, 1; 4, 4]; %!test %! [idx, D] = knnsearch (X, Y, 'K', 2, 'Distance', 'minkowski', 'P', Inf, ... %! 'NSMethod', 'kdtree'); %! assert_equal (idx, [1, 3; 5, 2]); %! assert_equal (D, [1, 1; 1, 2]); %!test %! [idx, D] = knnsearch (X, Y, 'K', 2, 'Distance', 'minkowski', 'P', Inf, ... %! 'NSMethod', 'exhaustive'); %! assert_equal (idx, [1, 3; 5, 2]); %! assert_equal (D, [1, 1; 1, 2]); %!test %! [idx, D] = knnsearch (X, Y, 'K', 1, 'Distance', 'minkowski', 'P', Inf, ... %! 'IncludeTies', true, 'NSMethod', 'kdtree'); %! assert_equal (idx, {[1, 3]; 5}); %! assert_equal (D, {[1, 1]; 1}); %!test %! [idx, D] = knnsearch (X, Y, 'K', 1, 'Distance', 'minkowski', 'P', Inf, ... %! 'IncludeTies', true, 'NSMethod', 'exhaustive'); %! assert_equal (idx, {[1, 3]; 5}); %! assert_equal (D, {[1, 1]; 1}); %!test %! [idx, D] = knnsearch (X, Y, 'K', 2, 'Distance', 'minkowski', 'P', Inf); %! [idxc, Dc] = knnsearch (X, Y, 'K', 2, 'Distance', 'chebychev'); %! assert_equal ({idx, D}, {idxc, Dc}); ## A row holding a NaN is at distance NaN and comes last, in row order, as in ## MATLAB R2024a. The kd-tree measured the Chebychev distance over the other ## coordinates, and lost such a row from the tree where it split on the NaN. %!test %! Xn = [1, Inf; 4, 6; 0, Inf; NaN, 1; 2, 2]; %! [idx, D] = knnsearch (Xn, [1, 1], 'K', 5, 'Distance', 'chebychev'); %! assert_equal (idx, [5, 2, 1, 3, 4]); %! assert_equal (D, [1, 5, Inf, Inf, NaN]); %!test %! Xn = [1, Inf; 4, 6; 0, Inf; NaN, 1; 2, 2]; %! [idx, D] = knnsearch (Xn, [1, 1], 'K', 5, 'Distance', 'chebychev', ... %! 'NSMethod', 'exhaustive'); %! assert_equal (idx, [5, 2, 1, 3, 4]); %! assert_equal (D, [1, 5, Inf, Inf, NaN]); %!test %! Xg = [(1:60)', (1:60)']; %! Xg(7, 1) = NaN; Xg(30, 2) = NaN; Xg(45, :) = NaN; %! [idx, D] = knnsearch (Xg, [10, 10], 'K', 60); %! assert_equal (idx(55:60), [58, 59, 60, 7, 30, 45]); %! assert_equal (D(55:60), [sqrt([4608, 4802, 5000]), NaN, NaN, NaN], 1e-14); %!test %! Xg = [(1:60)', (1:60)']; %! Xg(7, 1) = NaN; Xg(30, 2) = NaN; Xg(45, :) = NaN; %! idx = knnsearch (Xg, [10, 10], 'K', 58); %! assert_equal (idx(55:58), [58, 59, 60, 7]); %!test %! Xg = [(1:60)', (1:60)']; %! Xg(7, 1) = NaN; Xg(30, 2) = NaN; Xg(45, :) = NaN; %! [idx, D] = knnsearch (Xg, [10, 10], 'K', 60, 'Distance', 'chebychev'); %! assert_equal (idx(55:60), [58, 59, 60, 7, 30, 45]); %! assert_equal (D(55:60), [48, 49, 50, NaN, NaN, NaN]); %!test %! Xg = [(1:60)', (1:60)']; %! Xg(7, 1) = NaN; Xg(30, 2) = NaN; Xg(45, :) = NaN; %! [idx, D] = knnsearch (Xg, [NaN, 10], 'K', 3); %! assert_equal (idx, [1, 2, 3]); %! assert_equal (D, [NaN, NaN, NaN]); %!test %! ## Inf - Inf is NaN, and so is the Chebychev distance, as for every other %! ## metric and on both methods; MATLAB R2024a's exhaustive search passes %! ## over it and gives [1, 2, 3] at distances [1, Inf, Inf]. %! [idx, D] = knnsearch ([1, Inf; 1, -Inf; 1, 5], [0, Inf], 'K', 3, ... %! 'Distance', 'chebychev'); %! assert_equal (idx, [2, 3, 1]); %! assert_equal (D, [Inf, Inf, NaN]); %!test %! [idx, D] = knnsearch ([1, Inf; 1, -Inf; 1, 5], [0, Inf], 'K', 3, ... %! 'Distance', 'chebychev', 'NSMethod', 'exhaustive'); %! assert_equal (idx, [2, 3, 1]); %! assert_equal (D, [Inf, Inf, NaN]); %!test %! [idx, D] = knnsearch ([1, Inf; 1, -Inf; 1, 5], [0, Inf], 'K', 3); %! assert_equal (idx, [2, 3, 1]); %! assert_equal (D, [Inf, Inf, NaN]); %!test %! ## A query and a split at the same infinity put the splitting plane at NaN, %! ## which bounds nothing: rows 1 and 4 are both at Inf and the earlier row %! ## comes first, as in the exhaustive search. The far side went unsearched. %! X = [-Inf, 4; -Inf, -Inf; 1, -Inf; 2, 1]; %! assert_equal (knnsearch (X, [3, -Inf], 'BucketSize', 1), 1); %! assert_equal (knnsearch (X, [3, -Inf], 'NSMethod', 'exhaustive'), 1); %!test %! ## A NaN distance ties nothing, so 'IncludeTies' stops at K there %! Xn = [1, Inf; 4, 6; 0, Inf; NaN, 1; 2, 2]; %! [idx, D] = knnsearch (Xn, [3, 3], 'K', 5, 'IncludeTies', true); %! assert_equal (idx, {[5, 2, 1, 3, 4]}); %! assert_equal (D, {[sqrt(2), sqrt(10), Inf, Inf, NaN]}); %!test %! Xn = [1, Inf; 4, 6; 0, Inf; NaN, 1; 2, 2]; %! [idx, D] = knnsearch (Xn, [3, 3], 'K', 5, 'IncludeTies', true, ... %! 'NSMethod', 'exhaustive'); %! assert_equal (idx, {[5, 2, 1, 3, 4]}); %! assert_equal (D, {[sqrt(2), sqrt(10), Inf, Inf, NaN]}); %!test %! Xg = [(1:60)', (1:60)']; %! Xg(7, 1) = NaN; Xg(30, 2) = NaN; Xg(45, :) = NaN; %! idx = knnsearch (Xg, [10, 10], 'K', 58, 'IncludeTies', true); %! assert_equal (idx{1}(55:end), [58, 59, 60, 7]); %!test %! Xg = [(1:60)', (1:60)']; %! Xg(7, 1) = NaN; Xg(30, 2) = NaN; Xg(45, :) = NaN; %! idx = knnsearch (Xg, [10, 10], 'K', 58, 'IncludeTies', true, ... %! 'NSMethod', 'exhaustive'); %! assert_equal (idx{1}(55:end), [58, 59, 60, 7]); %!test %! Xg = [(1:60)', (1:60)']; %! Xg(7, 1) = NaN; Xg(30, 2) = NaN; Xg(45, :) = NaN; %! idx = knnsearch (Xg, [NaN, 10], 'K', 2, 'IncludeTies', true); %! assert_equal (idx, {[1, 2]}); ## Test input validation ## Neighbours at equal distance come in row order, as R2024a returns them, ## and the K-th neighbour is the earlier of two tied rows. %!test %! load fisheriris %! X = meas(1:130,[1, 3]); %! [idx, D] = knnsearch (X, [6.3, 4.9], 'K', 5); %! assert_equal (idx, [73, 124, 127, 57, 128]); %! assert_equal (D(4), D(5)); %! assert_equal (knnsearch (X, [6.3, 4.9], 'K', 4), [73, 124, 127, 57]); %! idx = knnsearch (X, [6.3, 4.9], 'K', 4, 'NSMethod', 'exhaustive'); %! assert_equal (idx, [73, 124, 127, 57]); %!error knnsearch (1) %!error ... %! knnsearch (ones (4, 5), ones (4)) %!error ... %! knnsearch (ones (4, 2), ones (3, 2), 'Distance', 'euclidean', 'some', 'some') %!error ... %! knnsearch (ones (4, 5), ones (1, 5), 'scale', ones (1, 5), 'P', 3) %!error ... %! knnsearch (ones (4, 5), ones (1, 5), 'K', 0) %!error ... %! knnsearch (ones (4, 5), ones (1, 5), 'P', -2) %!error ... %! knnsearch (ones (4, 2), ones (1, 2), 'Distance', 'minkowski', 'P', NaN) %!error ... %! knnsearch (ones (4, 2), ones (1, 2), 'P', 3) %!error ... %! knnsearch (ones (4, 2), ones (1, 2), 'P', 3, 'NSMethod', 'exhaustive') %!error ... %! knnsearch (ones (4, 5), ones (1, 5), 'scale', ones (4,5), 'distance', 'euclidean') %!error ... %! knnsearch (ones (4, 5), ones (1, 5), 'cov', ['some' 'some']) %!error ... %! knnsearch (ones (4, 5), ones (1, 5), 'cov', ones (4,5), 'distance', 'euclidean') %!error ... %! knnsearch (ones (4, 5), ones (1, 5), 'bucketsize', -1) %!error ... %! knnsearch (ones (4, 5), ones (1, 5), 'bucketsize', 2.5) %!error ... %! knnsearch (ones (4, 5), ones (1, 5), 'NSmethod', 'kdtree', 'distance', 'cosine') %!error ... %! knnsearch (ones (4, 5), ones (1, 5), 'NSmethod', 'kdtree', 'distance', 'mahalanobis') %!error ... %! knnsearch (ones (4, 5), ones (1, 5), 'NSmethod', 'kdtree', 'distance', 'correlation') %!error ... %! knnsearch (ones (4, 5), ones (1, 5), 'NSmethod', 'kdtree', 'distance', 'seuclidean') %!error ... %! knnsearch (ones (4, 5), ones (1, 5), 'NSmethod', 'kdtree', 'distance', 'spearman') %!error ... %! knnsearch (ones (4, 5), ones (1, 5), 'NSmethod', 'kdtree', 'distance', 'hamming') %!error ... %! knnsearch (ones (4, 5), ones (1, 5), 'NSmethod', 'kdtree', 'distance', 'jaccard') ## Single precision is carried through to the distances, and the indices are ## always double, on both search methods. %!test %! X = [1, 2; 3, 4; 5, 6; 7, 8]; Y = [2, 3; 6, 7]; %! for m = {"kdtree", "exhaustive"} %! [idx, D] = knnsearch (single (X), single (Y), "K", 2, "NSMethod", m{1}); %! assert_equal (class (idx), 'double'); %! assert_equal (class (D), 'single'); %! [~, Dm] = knnsearch (single (X), Y, "K", 2, "NSMethod", m{1}); %! assert_equal (class (Dm), 'single'); %! assert_equal (Dm, D); %! [~, Dd] = knnsearch (X, Y, "K", 2, "NSMethod", m{1}); %! assert_equal (class (Dd), 'double'); %! endfor ## A single neighbour with 'IncludeTies' used to take a shortcut that ignored ## the flag, returning one index as a double where a cell of every tied ## neighbour was promised. The kd-tree path was unaffected. %!test %! X = [0, 0; 0, 0; 1, 1; 1, 1; 2, 2; 2, 2]; %! idx = knnsearch (X, [0.5, 0.5], 'k', 1, 'NSMethod', 'exhaustive', ... %! 'IncludeTies', true); %! assert_equal (class (idx), 'cell'); %! assert_equal (sort (idx{1}), [1, 2, 3, 4]); %!test %! X = [0, 0; 0, 0; 1, 1; 1, 1; 2, 2; 2, 2]; %! [i1, d1] = knnsearch (X, [0.5, 0.5], 'k', 1, 'NSMethod', 'exhaustive', ... %! 'IncludeTies', true); %! [i2, d2] = knnsearch (X, [0.5, 0.5], 'k', 1, 'NSMethod', 'kdtree', ... %! 'IncludeTies', true); %! assert_equal (sort (i1{1}), sort (i2{1})); %! assert_equal (sort (d1{1}), sort (d2{1}), 1e-12); statistics-release-2.1.2/inst/Nearest_Neighbors/mahal.m000066400000000000000000000051771527311104300231510ustar00rootroot00000000000000## Copyright (C) 2015 Lachlan Andrew ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{d} =} mahal (@var{y}, @var{x}) ## ## Mahalanobis' D-square distance. ## ## Return the Mahalanobis' D-square distance of the points in ## @var{y} from the distribution implied by points @var{x}. ## ## Specifically, it uses a Cholesky decomposition to set ## ## @example ## answer(i) = (@var{y}(i,:) - mean (@var{x})) * inv (A) * ## (@var{y}(i,:)-mean (@var{x}))' ## @end example ## ## where A is the covariance of @var{x}. ## ## The data @var{x} and @var{y} must have the same number of components ## (columns), but may have a different number of observations (rows). ## ## @end deftypefn function retval = mahal (y, x) if (nargin != 2) print_usage (); endif if (! (isnumeric (x) || islogical (x)) || ! (isnumeric (y) || islogical (y))) error ("mahal: X and Y must be numeric matrices or vectors"); endif if (! ismatrix (x) || ! ismatrix (y)) error ("mahal: X and Y must be 2-D matrices or vectors"); endif [xr, xc] = size (x); [yr, yc] = size (y); if (xc != yc) error ("mahal: X and Y must have the same number of columns"); endif if (isinteger (x)) x = double (x); endif xm = mean (x, 1); ## Center data by subtracting mean of x x = bsxfun (@minus, x, xm); y = bsxfun (@minus, y, xm); w = (x' * x) / (xr - 1); retval = sumsq (y / chol (w), 2); endfunction ## Test input validation %!error mahal () %!error mahal (1, 2, 3) %!error mahal ('A', 'B') %!error mahal ([1, 2], ['A', 'B']) %!error mahal (ones (2, 2, 2)) %!error mahal (ones (2, 2), ones (2, 2, 2)) %!error mahal (ones (2, 2), ones (2, 3)) %!test %! X = [1 0; 0 1; 1 1; 0 0]; %! assert_equal (mahal (X, X), [1.5; 1.5; 1.5; 1.5], 10*eps) %! assert_equal (mahal (X, X+1), [7.5; 7.5; 1.5; 13.5], 10*eps) %!assert_equal (mahal ([true; true], [false; true]), [0.5; 0.5], eps) statistics-release-2.1.2/inst/Nearest_Neighbors/nomdist.m000066400000000000000000000363401527311104300235400ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{D} =} nomdist (@var{X}) ## @deftypefnx {statistics} {@var{D} =} nomdist (@var{X}, @var{measure}) ## @deftypefnx {statistics} {@var{D} =} nomdist (@dots{}, @var{Name}, @var{Value}) ## ## Dissimilarity between every pair of rows of nominal data. ## ## @code{@var{D} = nomdist (@var{X})} returns the Goodall 3 dissimilarity ## between each pair of rows of @var{X}, whose columns are nominal variables ## and whose values are their levels. @var{D} is a row vector in the order ## @code{pdist} returns, pairs @math{(1,2)}, @math{(1,3)}, @dots{}, ## @math{(2,3)}, @dots{}, so @code{squareform} turns it into a matrix and ## @code{linkage} takes it as it stands. An @var{X} of one row gives a ## @math{1*0} @var{D}. ## ## @var{X} is a numeric, logical, categorical, string or cellstr matrix, each ## distinct value being a level, or a table whose every variable is one column ## of these. A missing value, @code{NaN}, an empty character vector, a ## missing string or an undefined category, is refused. ## ## The measures weigh a match or a mismatch by how often the levels involved ## occur, counted over the rows of @var{X} unless @qcode{'Reference'} names ## another sample. ## ## @code{@var{D} = nomdist (@var{X}, @var{measure})} uses the measure named, ## in any case, from those of Boriah, Chandola and Kumar (2008) and of Sulc and ## Rezankova (2019), as the R package @code{nomclust} 2.8.1 implements them. ## In the table, @math{N} is the number of rows the frequencies are counted ## over, @math{f} the count of a level, @math{p = f/N} its frequency, ## @math{p2 = f(f-1)/(N(N-1))} the chance of drawing it twice, and @math{n} ## the number of levels the variable holds. A subscript names the level of ## either row, and a variable scores 0 wherever its entry names no score. ## ## @multitable @columnfractions 0.16 0.02 0.82 ## @headitem @var{measure} @tab @tab Score of one variable ## @item @qcode{'anderberg'} @tab @tab ## The similarity is ## @tex ## \(\frac{\sum_{x = y} c/p_x^2} ## {\sum_{x = y} c/p_x^2 + \sum_{x \ne y} c/(2 p_x p_y)},\) ## @end tex ## @ifnottex ## the sum over the matches of @math{c/p^2}, divided by itself plus the sum over ## the mismatches of @math{c/(2 p_x p_y)}, ## @end ifnottex ## with ## @tex ## \(c = 2/(n(n+1)).\) ## @end tex ## @ifnottex ## @math{c = 2/(n(n+1))}. ## @end ifnottex ## @item @qcode{'burnaby'} @tab @tab ## 1 on a match; on a mismatch ## @tex ## \(\frac{B}{\log \frac{p_x p_y}{(1-p_x)(1-p_y)} + B},\) ## @end tex ## @ifnottex ## @math{B/(log (p_x p_y/((1-p_x)(1-p_y))) + B)}, ## @end ifnottex ## with ## @tex ## \(B = \sum_q 2 \log (1 - p_q).\) ## @end tex ## @ifnottex ## @math{B} the sum of @math{2 log (1-p)} over the levels. ## @end ifnottex ## @item @qcode{'eskin'} @tab @tab ## 1 on a match, ## @tex ## \(\frac{n^2}{n^2 + 2}\) ## @end tex ## @ifnottex ## @math{n^2/(n^2+2)} ## @end ifnottex ## on a mismatch. ## @item @qcode{'gambaryan'} @tab @tab ## @tex ## \(-\left(p_x \log_2 p_x + (1-p_x) \log_2 (1-p_x)\right)\) ## @end tex ## @ifnottex ## @math{-(p log2 (p) + (1-p) log2 (1-p))} ## @end ifnottex ## on a match, which peaks where @math{p} is 0.5. ## @item @qcode{'goodall1'} @tab @tab ## On a match ## @tex ## \(1 - \sum_{q:\, p_q \le p_x} \mathit{p2}_q.\) ## @end tex ## @ifnottex ## 1 less the sum of @math{p2} over the levels at most as frequent as the ## one matched. ## @end ifnottex ## @item @qcode{'goodall2'} @tab @tab ## On a match ## @tex ## \(1 - \sum_{q:\, p_q \ge p_x} \mathit{p2}_q.\) ## @end tex ## @ifnottex ## 1 less the sum of @math{p2} over the levels at least as frequent as the ## one matched. ## @end ifnottex ## @item @qcode{'goodall3'} @tab @tab ## @tex ## \(1 - \mathit{p2}_x\) ## @end tex ## @ifnottex ## @math{1 - p2} ## @end ifnottex ## on a match. ## @item @qcode{'goodall4'} @tab @tab ## @tex ## \(\mathit{p2}_x\) ## @end tex ## @ifnottex ## @math{p2} ## @end ifnottex ## on a match. ## @item @qcode{'iof'} @tab @tab ## 1 on a match, ## @tex ## \(\frac{1}{1 + \log f_x \log f_y}\) ## @end tex ## @ifnottex ## @math{1/(1 + log (f_x) log (f_y))} ## @end ifnottex ## on a mismatch. ## @item @qcode{'lin'} @tab @tab ## @tex ## \(2 \log p_x\) ## @end tex ## @ifnottex ## @math{2 log (p)} ## @end ifnottex ## on a match, ## @tex ## \(2 \log (p_x + p_y)\) ## @end tex ## @ifnottex ## @math{2 log (p_x + p_y)} ## @end ifnottex ## on a mismatch, over the sum of ## @tex ## \(\log p_x + \log p_y.\) ## @end tex ## @ifnottex ## @math{log (p_x) + log (p_y)}. ## @end ifnottex ## @item @qcode{'lin1'} @tab @tab ## On a match ## @tex ## \(\log \sum_{q:\, p_q = p_x} p_q;\) ## @end tex ## @ifnottex ## the log of the summed @math{p} of the levels as frequent as the one matched; ## @end ifnottex ## on a mismatch ## @tex ## \(2 \log \sum_{q:\, \min (p_x, p_y) \le p_q \le \max (p_x, p_y)} p_q;\) ## @end tex ## @ifnottex ## twice the log of the summed @math{p} of the levels whose frequency lies from ## @math{p_x} to @math{p_y}; ## @end ifnottex ## over the same sum as @qcode{'lin'}. ## @item @qcode{'of'} @tab @tab ## 1 on a match, ## @tex ## \(\frac{1}{1 + \log (N/f_x) \log (N/f_y)}\) ## @end tex ## @ifnottex ## @math{1/(1 + log (N/f_x) log (N/f_y))} ## @end ifnottex ## on a mismatch. ## @item @qcode{'sm'} @tab @tab ## 1 on a match, the simple matching coefficient. ## @item @qcode{'smirnov'} @tab @tab ## On a match ## @tex ## \(2 + \frac{N - f_x}{f_x} + \sum_{q \ne x} \frac{f_q}{N - f_q};\) ## @end tex ## @ifnottex ## @math{2 + (N-f)/f} plus the sum of @math{f/(N-f)} over the other levels; ## @end ifnottex ## on a mismatch ## @tex ## \(\sum_{q \ne x, y} \frac{f_q}{N - f_q}.\) ## @end tex ## @ifnottex ## that sum over the levels other than both. ## @end ifnottex ## @item @qcode{'ve'} @tab @tab ## On a match the entropy of the variable, ## @tex ## \(-\frac{1}{\log n} \sum_q p_q \log p_q.\) ## @end tex ## @ifnottex ## @math{-sum (p log (p))/log (n)}. ## @end ifnottex ## @item @qcode{'vm'} @tab @tab ## On a match the Gini index of the variable, ## @tex ## \(\frac{n}{n-1} \left(1 - \sum_q p_q^2\right).\) ## @end tex ## @ifnottex ## @math{(1 - sum (p^2)) n/(n-1)}. ## @end ifnottex ## @end multitable ## ## The scores of a pair are summed over the variables, each times its weight, ## and divided by the sum of the weights, except where the table names another ## divisor, and @qcode{'gambaryan'} and @qcode{'smirnov'} divide by the number ## of levels over every variable. With @math{S} that similarity, ## @qcode{'eskin'}, @qcode{'iof'}, @qcode{'lin'}, @qcode{'lin1'} and ## @qcode{'of'} return @math{1/S - 1}, @qcode{'smirnov'} returns ## @math{1/(1+S)}, and every other measure @math{1 - S}@. Several measures, ## the four Goodall ones among them, place two identical rows at a nonzero ## dissimilarity, a match on a common level saying less than one on a rare ## level. @qcode{'lin'} and @qcode{'lin1'} return @code{Inf} for a pair of ## similarity 0, such as two rows differing in every variable where each ## variable holds two levels. ## ## @code{@var{D} = nomdist (@dots{}, @var{Name}, @var{Value})} takes the ## following options. ## ## @multitable @columnfractions 0.18 0.02 0.8 ## @headitem @var{Name} @tab @tab @var{Value} ## @item @qcode{'Weights'} @tab @tab One weight per variable, each from 0 to ## 1 and at least one positive; a variable of weight 0 is left out. Not taken ## by @qcode{'anderberg'}, @qcode{'gambaryan'} and @qcode{'smirnov'}. ## @item @qcode{'Reference'} @tab @tab A sample to count level frequencies ## over in place of @var{X}, of the same kind: a matrix with as many columns, ## the same type in each, or a table holding every variable of @var{X}, read ## by name. It must hold every level @var{X} holds. Given the whole data, ## it measures a subsample by how rare its levels are in the whole. ## @end multitable ## ## Where @code{nomclust} answers otherwise, the answer here is the limit of ## the measure's formula. @qcode{'lin'} and @qcode{'lin1'} sum the counts ## of levels, so a sum over every level is exactly 1 and a similarity of 0 is ## exactly 0, where @code{nomclust} rounds it to about @math{1e16} or ## replaces it with one more than the largest finite dissimilarity. ## @qcode{'gambaryan'} scores a level every row holds as 0, where ## @code{nomclust} returns @code{NaN}. Where every variable holds one level, ## @qcode{'lin'} returns 0 and @qcode{'lin1'} 1, where @code{nomclust} ## returns @code{NaN}. ## ## References: ## @itemize ## @item Boriah, S., Chandola, V. and Kumar, V. (2008). Similarity measures ## for categorical data: a comparative evaluation. Proceedings of the 8th ## SIAM International Conference on Data Mining, 243-254. ## @item Sulc, Z. and Rezankova, H. (2019). Comparison of similarity ## measures for categorical data in hierarchical clustering. Journal of ## Classification, 36(1), 58-72. ## @end itemize ## ## @seealso{nomdist2, pdist, squareform, linkage} ## @end deftypefn function D = nomdist (X, varargin) ## Input validation if (nargin < 1) error ("nomdist: too few input arguments."); endif [C, measure, w, errmsg] = __nomprep__ (X, [], false, varargin); if (! isempty (errmsg)) error ("nomdist: %s", errmsg); endif D = __nomdist__ (C{1}, [], C{3}, measure, w); endfunction %!shared X %! X = [1, 1, 1; 1, 2, 1; 1, 1, 2; 2, 2, 2; 2, 1, 1; 3, 2, 2; 4, 1, 3]; %!test %! ## The default is Goodall 3; values from nomclust 2.8.1 %! D = [0.4285714285714286, 0.4761904761904762, 1, 0.4761904761904762, 1, ... %! 0.7619047619047619, 0.7142857142857143, 0.7142857142857143, ... %! 0.7142857142857143, 0.7142857142857143, 1, 0.7142857142857143, ... %! 0.7619047619047619, 0.7142857142857143, 0.7619047619047619, ... %! 0.6825396825396826, 0.4285714285714286, 1, 1, 0.7619047619047619, ... %! 1]; %! assert_equal (nomdist (X), D, -1e-14); %!test %! D = [0.3333333333333334, 0.3333333333333334, 1, 0.3333333333333334, 1, ... %! 0.6666666666666667, 0.6666666666666667, 0.6666666666666667, ... %! 0.6666666666666667, 0.6666666666666667, 1, 0.6666666666666667, ... %! 0.6666666666666667, 0.6666666666666667, 0.6666666666666667, ... %! 0.6666666666666667, 0.3333333333333334, 1, 1, 0.6666666666666667, ... %! 1]; %! assert_equal (nomdist (X, 'SM'), D, -1e-14); %!assert_equal (nomdist (X, "lin"), nomdist (X, 'lin')) %!test %! ## Weighted Lin; values from nomclust 2.8.1 %! D = nomdist (X, 'lin', 'Weights', [0.7, 1, 0.4]); %! assert_equal (D(1:3), [0.7547596113257813, 0.2283122503928992, ... %! 4.98065967640019], -1e-14); %!assert_equal (nomdist (X, 'Weights', [1, 1, 1]), nomdist (X)) %!assert_equal (nomdist (X, 'Weights', []), nomdist (X)) %!assert_equal (nomdist (X, 'lin', 'Weights', [1, 0, 1]), ... %! nomdist (X(:,[1, 3]), 'lin')) %!assert_equal (nomdist (10 * X - 25), nomdist (X)) %!assert_equal (nomdist (X == 1), nomdist (double (X == 1))) %!assert_equal (nomdist (int8 (X)), nomdist (X)) %!test %! C = {'a', 'b', 'c', 'd'}; %! assert_equal (nomdist (C(X)), nomdist (X)); %!test %! C = {'a', 'b', 'c', 'd'}; %! assert_equal (nomdist (string (C(X))), nomdist (X)); %!test %! C = {'a', 'b', 'c', 'd'}; %! assert_equal (nomdist (categorical (C(X))), nomdist (X)); %!test %! C = {'a', 'b', 'c', 'd'}; %! T = table (categorical (C(X(:,1))'), X(:,2) == 1, C(X(:,3))', ... %! 'VariableNames', {'A', 'B', 'C'}); %! assert_equal (nomdist (T, 'of'), nomdist (X, 'of')); %!assert_equal (nomdist ([1, 2, 3]), zeros (1, 0)) %!assert_equal (nomdist ([1; 1; 2], 'goodall3'), [1/3, 1, 1], -1e-14) %!assert_equal (nomdist ([1; 1; 2], 'goodall3', 'Reference', ... %! [1; 1; 1; 1; 2]), [0.6, 1, 1], -1e-14) %!assert_equal (nomdist ([1; 1; 2], 'Reference', []), nomdist ([1; 1; 2])) %!test %! T = table ([1; 1; 2], {'a'; 'b'; 'a'}, 'VariableNames', {'A', 'B'}); %! R = table ({'a'; 'b'; 'a'; 'a'}, [1; 1; 2; 1], 'VariableNames', {'B', 'A'}); %! assert_equal (nomdist (T, 'Reference', R), ... %! nomdist ([1, 1; 1, 2; 2, 1], 'Reference', ... %! [1, 1; 1, 2; 2, 1; 1, 1])); %!error nomdist () %!error ... %! nomdist ([1; 2], 'hamming') %!error ... %! nomdist ([1; 2], 1) %!error ... %! nomdist ([1; 2], 'Scale', 1) %!error ... %! nomdist ([1; 2], 'CountQuery', true) %!error ... %! nomdist (['ab'; 'cd']) %!error ... %! nomdist (ones (2, 2, 2)) %!error nomdist (zeros (0, 2)) %!error ... %! nomdist (table ([1, 2; 3, 4])) %!error nomdist ([1; NaN]) %!error nomdist ({'a'; ''}) %!error ... %! nomdist (table ([1; 2]), 'Reference', [1; 2]) %!error ... %! nomdist (table ([1; 2]), 'Reference', table ([1; 2], 'VariableNames', {'Z'})) %!error ... %! nomdist ([1; 2], 'Reference', table ([1; 2])) %!error ... %! nomdist ([1; 2], 'Reference', ['a'; 'b']) %!error ... %! nomdist ([1; 2], 'Reference', [1, 1; 2, 2]) %!error ... %! nomdist ([1; 2], 'Reference', {'a'; 'b'}) %!error ... %! nomdist ([1; 2], 'Reference', [1; 2; NaN]) %!error ... %! nomdist ([1; 2], 'Reference', [1; 1]) %!error ... %! nomdist ([1; 2], 'smirnov', 'Weights', 1) %!error ... %! nomdist ([1, 1; 2, 1], 'Weights', 1) %!error ... %! nomdist ([1, 1; 2, 1], 'Weights', [1, 2]) %!error ... %! nomdist ([1, 1; 2, 1], 'Weights', [0, 0]) statistics-release-2.1.2/inst/Nearest_Neighbors/nomdist2.m000066400000000000000000000240071527311104300236170ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{D} =} nomdist2 (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{D} =} nomdist2 (@var{X}, @var{Y}, @var{measure}) ## @deftypefnx {statistics} {@var{D} =} nomdist2 (@dots{}, @var{Name}, @var{Value}) ## ## Dissimilarity between the rows of two samples of nominal data. ## ## @code{@var{D} = nomdist2 (@var{X}, @var{Y})} returns the Goodall 3 ## dissimilarity of each row of @var{X} to each row of @var{Y}, as the ## @math{N*M} matrix @var{D}, where @var{X} has @math{N} rows and @var{Y} ## @math{M}@. Their columns are the same nominal variables and their values ## the levels, of any type @code{nomdist} takes; where @var{X} is a table, ## @var{Y} must be one holding every variable of @var{X}, read by name. A ## @var{Y} of no rows gives an @math{N*0} @var{D}. ## ## Level frequencies are counted over @var{X}, the sample the rows of @var{Y} ## are measured against, so @var{D} is not symmetric in the two: ## @code{nomdist2 (@var{X}, @var{Y})} is not in general ## @code{nomdist2 (@var{Y}, @var{X})'}. @code{nomdist2 (@var{X}, @var{X})} ## equals @code{squareform (nomdist (@var{X}))} off the diagonal; on it, ## several measures give two identical rows a nonzero dissimilarity. ## ## @code{@var{D} = nomdist2 (@var{X}, @var{Y}, @var{measure})} uses the ## measure named, one of @qcode{'anderberg'}, @qcode{'burnaby'}, ## @qcode{'eskin'}, @qcode{'gambaryan'}, @qcode{'goodall1'}, ## @qcode{'goodall2'}, @qcode{'goodall3'}, @qcode{'goodall4'}, @qcode{'iof'}, ## @qcode{'lin'}, @qcode{'lin1'}, @qcode{'of'}, @qcode{'sm'}, ## @qcode{'smirnov'}, @qcode{'ve'} or @qcode{'vm'}, in any case, each defined ## as in @code{nomdist}. ## ## @code{@var{D} = nomdist2 (@dots{}, @var{Name}, @var{Value})} takes the ## options @code{nomdist} takes. @qcode{'Weights'} gives one weight per ## variable. @qcode{'Reference'} names a sample to count level frequencies ## over in place of @var{X}, which must hold every level @var{X} holds; ## @code{[@var{X}; @var{Y}]} counts them over both samples. ## @qcode{'CountQuery'}, @code{false} by default, counts each row of @var{Y} ## into the reference while it is measured, so that its column of @var{D} ## takes level frequencies over the reference with that row appended. ## ## @var{Y} may hold a level the reference sample does not, unless ## @qcode{'CountQuery'} is true, which counts that level once. Each measure ## then answers with the limit of its formula as that level's count falls ## to 0. ## @qcode{'sm'}, @qcode{'eskin'}, @qcode{'gambaryan'}, the four Goodall ## measures, @qcode{'smirnov'}, @qcode{'ve'} and @qcode{'vm'} never read that ## count and answer as usual. @qcode{'of'} and @qcode{'burnaby'} score the ## variable 0, @qcode{'anderberg'} places the pair at 1, and @qcode{'lin'} and ## @qcode{'lin1'} at @code{Inf}. Refused, where the limit does not exist: ## @qcode{'iof'}, whose similarity rises as a level grows rarer, and ## @qcode{'of'} and @qcode{'burnaby'} where the reference holds that variable ## at a single level. A variable of weight 0 is left out, and so never ## refused. ## ## @seealso{nomdist, pdist2} ## @end deftypefn function D = nomdist2 (X, Y, varargin) ## Input validation if (nargin < 2) error ("nomdist2: too few input arguments."); endif [cq, args] = parsePairedArguments ({'CountQuery'}, {false}, varargin(:)); if (! ((islogical (cq) || isnumeric (cq)) && isscalar (cq) && (cq == 0 || cq == 1))) error ("nomdist2: 'CountQuery' must be a logical scalar."); endif cq = logical (cq); [C, measure, w, errmsg] = __nomprep__ (X, Y, true, args(:)'); if (! isempty (errmsg)) error ("nomdist2: %s", errmsg); endif [Xc, Yc, Rc] = C{:}; if (isempty (Yc)) D = zeros (rows (Xc), 0); return; endif ## A level of Y the reference does not hold, where no limit exists; a ## counted query is never absent from its own reference if (! cq && any (strcmp (measure, {'iof', 'of', 'burnaby'}))) if (isempty (Rc)) Rc = Xc; endif for k = find (w > 0) if (all (ismember (Yc(:,k), Rc(:,k)))) continue; endif if (strcmp (measure, 'iof')) error (strcat ("nomdist2: 'iof' cannot score a level of Y that", ... " the reference sample does not hold.")); endif if (all (Rc(:,k) == Rc(1,k))) error (strcat ("nomdist2: '%s' cannot score a level of Y that", ... " the reference sample does not hold, where that", ... " sample holds the variable at one level."), measure); endif endfor endif D = __nomdist__ (Xc, Yc, C{3}, measure, w, cq); endfunction %!shared X %! X = [1, 1, 1; 1, 2, 1; 1, 1, 2; 2, 2, 2; 2, 1, 1; 3, 2, 2; 4, 1, 3]; %!test %! D = squareform (nomdist (X)); %! D2 = nomdist2 (X, X); %! assert_equal (D2(! eye (7)), D(! eye (7))); %!test %! D = squareform (nomdist (X, 'lin1', 'Weights', [0.7, 1, 0.4])); %! D2 = nomdist2 (X, X, 'lin1', 'Weights', [0.7, 1, 0.4]); %! assert_equal (D2(! eye (7)), D(! eye (7))); %!test %! ## Identical rows need not be at 0: 1 - (3 - 24/42) / 3 %! D2 = nomdist2 (X, X); %! assert_equal (D2(1,1), 4 / 21, -1e-14); %!assert_equal (size (nomdist2 (X, X(1:2,:))), [7, 2]) %!assert_equal (nomdist2 (X, X(1:2,:), 'of'), nomdist2 (X, X, 'of')(:,1:2)) %!assert_equal (nomdist2 ([1; 1; 2], [1; 2; 2]), [1/3, 1, 1; 1/3, 1, 1; ... %! 1, 0, 0], -1e-14) %!assert_equal (nomdist2 ([1; 2; 2], [1; 1; 2]), [0, 0, 1; 1, 1, 1/3; ... %! 1, 1, 1/3], -1e-14) %!test %! A = [1, 1; 2, 1; 2, 2]; %! B = [1, 2; 3, 1]; %! D = squareform (nomdist ([A; B], 'lin')); %! assert_equal (nomdist2 (A, B, 'lin', 'Reference', [A; B]), D(1:3,4:5), ... %! -1e-14); %!assert_equal (nomdist2 (X, zeros (0, 3)), zeros (7, 0)) %!test %! T = table ([1; 2; 2], {'a'; 'a'; 'b'}, 'VariableNames', {'A', 'B'}); %! U = table ({'b'; 'a'}, [2; 1], 'VariableNames', {'B', 'A'}); %! assert_equal (nomdist2 (T, U, 'sm'), [1, 0; 0.5, 0.5; 0, 1]); %!test %! ## A new level of Y: OF scores its variable 0 %! s = 1 / (1 + log (3) * log (3 / 2)); %! D = nomdist2 ([1, 1; 2, 1; 2, 2], [3, 1; 3, 2], 'of'); %! assert_equal (D, [1, 2/s-1; 1, 2/s-1; 2/s-1, 1], -1e-14); %!assert_equal (nomdist2 ({'a'; 'b'}, {'c'; 'a'}, 'burnaby'), [1, 0; 1, 0]) %!assert_equal (nomdist2 ([1; 2], [3; 1], 'anderberg'), [1, 0; 1, 1]) %!assert_equal (nomdist2 ([1; 2], [3; 1], 'lin'), [Inf, 0; Inf, Inf]) %!assert_equal (nomdist2 ([1; 2], [3; 1], 'goodall4'), [1, 1; 1, 1]) %!assert_equal (nomdist2 ([1, 1; 1, 1; 2, 1; 2, 1], [1, 3], 'iof', ... %! 'Weights', [1, 0]), [0; 0; 1; 1] * log (2) ^ 2, ... %! -1e-14) %!assert_equal (nomdist2 ([1, 1; 1, 2], [2, 1], 'of', 'Weights', [0, 1]), ... %! [0; log(2) ^ 2], -1e-14) %!assert_equal (nomdist2 ([1; 2], [3; 1], 'iof', 'Reference', ... %! [1; 1; 2; 2; 3; 3]), [1, 0; 1, 1] * log (2) ^ 2, ... %! -1e-14) %!test %! A = [1; 1; 2; 2]; %! E = [nomdist2(A, 3, 'iof', 'Reference', [A; 3]), ... %! nomdist2(A, 1, 'iof', 'Reference', [A; 1])]; %! assert_equal (nomdist2 (A, [3; 1], 'iof', 'CountQuery', true), E); %!test %! Y = X([2, 6],:); %! E = [nomdist2(X, Y(1,:), 'Reference', [X; Y(1,:)]), ... %! nomdist2(X, Y(2,:), 'Reference', [X; Y(2,:)])]; %! assert_equal (nomdist2 (X, Y, 'CountQuery', true), E); %!test %! R = [X; 2, 2, 1]; %! Y = [1, 1, 3; 4, 2, 2]; %! E = [nomdist2(X, Y(1,:), 'lin', 'Reference', [R; Y(1,:)]), ... %! nomdist2(X, Y(2,:), 'lin', 'Reference', [R; Y(2,:)])]; %! assert_equal (nomdist2 (X, Y, 'lin', 'Reference', R, 'CountQuery', 1), E); %!assert_equal (nomdist2 (X, X, 'of', 'CountQuery', false), ... %! nomdist2 (X, X, 'of')) %!error nomdist2 ([1; 2]) %!error ... %! nomdist2 ([1; 2], {1; 2}) %!error ... %! nomdist2 ([1; 2], table ([1; 2])) %!error ... %! nomdist2 (table ([1; 2]), [1; 2]) %!error ... %! nomdist2 (table ([1; 2]), table ([1; 2], 'VariableNames', {'Z'})) %!error ... %! nomdist2 (table ([1; 2], 'VariableNames', {'A'}), ... %! table ([1, 2], 'VariableNames', {'A'})) %!error ... %! nomdist2 ([1; 2], [1, 2]) %!error ... %! nomdist2 ([1; 2], {'a'}) %!error ... %! nomdist2 ([1; 2], [1; NaN]) %!error ... %! nomdist2 ([1; 2], [3; 1], 'iof') %!error ... %! nomdist2 ([1; 1], 2, 'of') %!error ... %! nomdist2 ([1; 1], 2, 'burnaby') %!error ... %! nomdist2 ([1; 2], 1, 'CountQuery', 'yes') %!error ... %! nomdist2 ([1; 2], 1, 'CountQuery', 2) statistics-release-2.1.2/inst/Nearest_Neighbors/pdist.m000066400000000000000000000515151527311104300232070ustar00rootroot00000000000000## Copyright (C) 2008 Francesco Potortì ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{D} =} pdist (@var{X}) ## @deftypefnx {statistics} {@var{D} =} pdist (@var{X}, @var{Distance}) ## @deftypefnx {statistics} {@var{D} =} pdist (@var{X}, @var{Distance}, @var{DistParameter}) ## ## Return the distance between any two rows in @var{X}. ## ## @code{@var{D} = pdist (@var{X}} calculates the euclidean distance between ## pairs of observations in @var{X}. @var{X} must be an @math{M*P} numeric ## matrix representing @math{M} points in @math{P}-dimensional space. This ## function computes the pairwise distances returned in @var{D} as an ## @math{M*(M-1)/P} row vector. Use @code{@var{Z} = squareform (@var{D})} to ## convert the row vector @var{D} into a an @math{M*M} symmetric matrix @var{Z}, ## where @qcode{@var{Z}(i,j)} corresponds to the pairwise distance between ## points @qcode{i} and @qcode{j}. ## ## @code{@var{D} = pdist (@var{X}, @var{Y}, @var{Distance})} returns the ## distance between pairs of observations in @var{X} using the metric specified ## by @var{Distance}, which can be any of the following options. ## ## @multitable @columnfractions 0.23 0.65 ## @item @qcode{'euclidean'} @tab Euclidean distance. ## @item @qcode{'fasteuclidean'} @tab Euclidean distance computed with an ## alternative algorithm which may be faster but might reduce accuracy. ## @item @qcode{'squaredeuclidean'} @tab Squared Euclidean distance. ## @item @qcode{'fastsquaredeuclidean'} @tab Euclidean distance computed ## with an alternative algorithm which may be faster but might reduce accuracy. ## @item @qcode{'seuclidean'} @tab standardized Euclidean distance. Each ## coordinate difference between the rows in @var{X} and the query matrix ## @var{Y} is scaled by dividing by the corresponding element of the standard ## deviation computed from @var{X}. A different scaling vector can be specified ## with the subsequent @var{DistParameter} input argument. ## @item @qcode{'mahalanobis'} @tab Mahalanobis distance, computed using a ## positive definite covariance matrix. A different covariance matrix can be ## specified with the subsequent @var{DistParameter} input argument. ## @item @qcode{'cityblock'} @tab City block distance. ## @item @qcode{'minkowski'} @tab Minkowski distance. The default exponent ## is 2. A different exponent can be specified with the subsequent ## @var{DistParameter} input argument. An exponent of @code{Inf} gives the ## Chebychev distance. ## @item @qcode{'chebychev'} @tab Chebychev distance (maximum coordinate ## difference). A difference of @code{Inf - Inf} is @code{NaN}, and so is the ## distance, as for every other metric; MATLAB passes over it. ## @item @qcode{'cosine'} @tab One minus the cosine of the included angle ## between points (treated as vectors). ## @item @qcode{'correlation'} @tab One minus the sample linear correlation ## between observations (treated as sequences of values). ## @item @qcode{'hamming'} @tab Hamming distance, which is the percentage ## of coordinates that differ. ## @item @qcode{'jaccard'} @tab One minus the Jaccard coefficient, which is ## the percentage of nonzero coordinates that differ. ## @item @qcode{'spearman'} @tab One minus the sample Spearman's rank ## correlation between observations (treated as sequences of values). ## @item @var{@@distfun} @tab Custom distance function handle. A distance ## function of the form @code{function @var{D2} = distfun (@var{XI}, @var{YI})}, ## where @var{XI} is a @math{1*P} vector containing a single observation in ## @math{P}-dimensional space, @var{YI} is an @math{N*P} matrix containing an ## arbitrary number of observations in the same @math{P}-dimensional space, and ## @var{D2} is an @math{N*P} vector of distances, where @qcode{(@var{D2}k)} is ## the distance between observations @var{XI} and @qcode{(@var{YI}k,:)}. ## @end multitable ## ## @code{@var{D} = pdist (@var{X}, @var{Y}, @var{Distance}, ## @var{DistParameter})} ## returns the distance using the metric specified by @var{Distance} and ## @var{DistParameter}. The latter one can only be specified when the selected ## @var{Distance} is @qcode{'seuclidean'}, @qcode{'minkowski'}, and ## @qcode{'mahalanobis'}. ## ## @seealso{pdist2, squareform, linkage} ## @end deftypefn function D = pdist (X, varargin) ## Check input data if (nargin < 1) error ("pdist: too few input arguments."); endif if (! isnumeric (X) || isempty (X)) error ("pdist: X must be a nonempty numeric matrix."); endif if (ndims (X) != 2) error ("pdist: X must be a two-dimensional matrix."); endif if (rows (X) < 2) D = cast (zeros (1, 0), class (X)); return; endif ## Add default values Distance = 'euclidean'; # Distance metric DistParameter = []; # Distance parameter ## Parse additional Distance metric and Distance parameter (if available) DMs = {'euclidean', 'squaredeuclidean', 'seuclidean', ... 'fasteuclidean', 'fastsquaredeuclidean', ... 'chebychev', 'cityblock', 'cosine', 'correlation', ... 'mahalanobis', 'minkowski', 'hamming', 'jaccard', 'spearman'}; if (numel (varargin) > 0) if (any (strcmpi (DMs, varargin{1}))) Distance = tolower (varargin{1}); elseif (is_function_handle (varargin{1})) Distance = varargin{1}; else error ("pdist: invalid value for Distance input argument."); endif endif if (numel (varargin) > 1) if (isnumeric (varargin{2})) DistParameter = varargin{2}; else error ("pdist: invalid value for DistParameter input argument."); endif endif ## A Minkowski exponent of Inf is the Chebyshev distance, its limit. The ## Minkowski formula itself would raise every difference to Inf and back. if (strcmp (Distance, 'minkowski') && isscalar (DistParameter) && DistParameter == Inf) Distance = 'chebychev'; endif ## FAST PATH: Optimization for fast Euclidean algorithm fast_algs = {'fasteuclidean', 'fastsquaredeuclidean'}; if (ischar (Distance) && ismember (Distance, fast_algs)) ## sumsq avoids the temporary memory of X.^2 ## D = ||x||^2 + ||x||^2 - 2x.x D = sumsq (X, 2) + sumsq (X, 2).' - 2 * (X * X'); ## Remove possible numerical negative noise D = max (D, 0); if (strcmp (Distance, 'fasteuclidean')) D = sqrt (D); endif ## Handle SortOrder (Smallest/Largest) output if requested if (nargout > 1 || parcount) ## This finds K nearest X's for each Y. [D, I] = sort (D, 1, SortOrder); K = min (size (D, 1), K); D = D(1:K, :); I = I(1:K, :); endif return; endif ## Calculate selected distance N = rows (X); ## Handle a function handle (always row-by-row) if (is_function_handle (Distance)) D2 = []; try D2 = Distance(X(1,:), X([2:end],:)); catch ME error ("pdist: invalid function handle for distance metric."); end_try_catch Xrows = N - 1; if (! isequal (size (D2), [Xrows, 1])) error ("pdist: custom distance function produces wrong output size."); endif num_pairs = N * (N - 1) / 2; D = zeros (1, num_pairs); id_beg = 1; for r = 1:Xrows id_end = id_beg + (N - r) - 1; D(id_beg:id_end) = feval (Distance, X(r,:), X([r+1:end],:)); id_beg = id_end + 1; endfor return; endif ## Threshold for switching between vectorized and blocked computation N_threshold = 1000; ## For small N: use original vectorized implementation (fast, O(N^2) memory) if (N < N_threshold) order = nchoosek (1:N, 2); ix = order(:,1); iy = order(:,2); switch (Distance) case 'euclidean' D = sqrt (sum ((X(ix,:) - X(iy,:)) .^ 2, 2))'; case 'squaredeuclidean' D = sum ((X(ix,:) - X(iy,:)) .^ 2, 2)'; case 'seuclidean' if (isempty (DistParameter)) DistParameter = std (X, [], 1); else if (numel (DistParameter) != columns (X)) error (strcat ("pdist: DistParameter for standardized", ... " euclidean must be a vector of equal length", ... " to the number of columns in X.")); endif if (any (DistParameter < 0)) error (strcat ("pdist: DistParameter for standardized", ... " euclidean must be a nonnegative vector.")); endif endif DistParameter(DistParameter == 0) = 1; D = sqrt (sum (((X(ix,:) - X(iy,:)) ./ DistParameter) .^ 2, 2))'; case 'mahalanobis' if (isempty (DistParameter)) DistParameter = cov (X(! any (isnan (X), 2),:)); else if (columns (DistParameter) != columns (X)) error (strcat ("pdist: DistParameter for mahalanobis", ... " distance must be a covariance matrix with", ... " the same number of columns as X.")); endif [~, p] = chol (DistParameter); if (p != 0) error (strcat ("pdist: covariance matrix for mahalanobis", ... " distance must be symmetric and positive", ... " definite.")); endif endif dxx = X(ix,:) - X(iy,:); [DP_inv, rc] = inv (DistParameter); if (rc < eps) warning (sprintf (strcat ("pdist: matrix is close to singular", ... " or badly scaled.\n RCOND = %e. Results may be inaccurate."), rc)); endif D = sqrt (sum ((dxx * DP_inv) .* dxx, 2))'; case 'cityblock' D = sum (abs (X(ix,:) - X(iy,:)), 2)'; case 'minkowski' if (isempty (DistParameter)) DistParameter = 2; else if (! (isnumeric (DistParameter) && isscalar (DistParameter) && DistParameter > 0)) error (strcat ("pdist: DistParameter for minkowski distance", ... " must be a positive scalar.")); endif endif D = (sum (abs (X(ix,:) - X(iy,:)) .^ DistParameter, 2) .^ ... (1 / DistParameter))'; case 'chebychev' A = abs (X(ix,:) - X(iy,:)); D = max (A, [], 2)'; ## max passes over NaN, so a pair is marked by hand wherever a ## coordinate difference is NaN, from a NaN in either row or from ## Inf - Inf, as every other metric leaves it. MATLAB passes over ## the second. D(any (isnan (A), 2)) = NaN; case 'cosine' sx = sum (X .^ 2, 2) .^ (-1 / 2); D = (1 - sum (X(ix,:) .* X(iy,:), 2) .* sx(ix) .* sx(iy))'; ## A similarity a rounding step above one would give a negative ## distance, which MATLAB never returns. Only the sign is corrected: ## parallel rows still land a rounding step above zero in both ## engines. A NaN compares false and is left alone. D(D < 0) = 0; case 'correlation' mX = mean (X(ix,:), 2); mY = mean (X(iy,:), 2); xy = sum ((X(ix,:) - mX) .* (X(iy,:) - mY), 2); xx = sqrt (sum ((X(ix,:) - mX) .^ 2, 2)); yy = sqrt (sum ((X(iy,:) - mY) .^ 2, 2)); D = (1 - xy ./ (xx .* yy))'; D(D < 0) = 0; case 'hamming' D = mean (X(ix,:) != X(iy,:), 2)'; case 'jaccard' nz = (X(ix,:) != 0 | X(iy,:) != 0); D = (sum ((X(ix,:) != X(iy,:)) & nz, 2) ./ sum (nz, 2))'; case 'spearman' rX = zeros (size (X)); for i = 1:N rX(i,:) = tiedrank (X(i,:)); endfor rM = (columns (X) + 1) / 2; xy = sum ((rX(ix,:) - rM) .* (rX(iy,:) - rM), 2); xx = sqrt (sum ((rX(ix,:) - rM) .^ 2, 2)); yy = sqrt (sum ((rX(iy,:) - rM) .^ 2, 2)); D = (1 - xy ./ (xx .* yy))'; D(D < 0) = 0; endswitch ## For large N: one row at a time against every later row (O(N) memory). ## Each row's distances are one vectorized step, so the loop runs N - 1 ## times rather than once per pair; what does not depend on the pair, a ## row's centring, norm or ranks, is computed once beforehand. else num_pairs = N * (N - 1) / 2; D = zeros (1, num_pairs); switch (Distance) case 'seuclidean' if (isempty (DistParameter)) DistParameter = std (X, [], 1); else if (numel (DistParameter) != columns (X)) error (strcat ("pdist: DistParameter for standardized", ... " euclidean must be a vector of equal length", ... " to the number of columns in X.")); endif if (any (DistParameter < 0)) error (strcat ("pdist: DistParameter for standardized", ... " euclidean must be a nonnegative vector.")); endif endif DistParameter(DistParameter == 0) = 1; case 'mahalanobis' if (isempty (DistParameter)) DistParameter = cov (X(! any (isnan (X), 2),:)); else if (columns (DistParameter) != columns (X)) error (strcat ("pdist: DistParameter for mahalanobis", ... " distance must be a covariance matrix with", ... " the same number of columns as X.")); endif [~, p] = chol (DistParameter); if (p != 0) error (strcat ("pdist: covariance matrix for mahalanobis", ... " distance must be symmetric and positive", ... " definite.")); endif endif [DP_inv, rc] = inv (DistParameter); if (rc < eps) warning (sprintf (strcat ("pdist: matrix is close to singular", ... " or badly scaled.\n RCOND = %e. Results may be inaccurate."), rc)); endif case 'minkowski' if (isempty (DistParameter)) DistParameter = 2; else if (! (isnumeric (DistParameter) && isscalar (DistParameter) && DistParameter > 0)) error (strcat ("pdist: DistParameter for minkowski distance", ... " must be a positive scalar.")); endif endif p_exp = DistParameter; p_inv = 1 / DistParameter; case 'cosine' sx = sum (X .^ 2, 2) .^ (-1 / 2); case 'correlation' Xc = X - mean (X, 2); nc = sqrt (sum (Xc .^ 2, 2)); case 'spearman' rX = zeros (size (X)); for i = 1:N rX(i,:) = tiedrank (X(i,:)); endfor Xc = rX - (columns (X) + 1) / 2; nc = sqrt (sum (Xc .^ 2, 2)); endswitch idx = 0; for i = 1:(N-1) j = (i+1):N; switch (Distance) case 'euclidean' d = sqrt (sum ((X(i,:) - X(j,:)) .^ 2, 2)); case 'squaredeuclidean' d = sum ((X(i,:) - X(j,:)) .^ 2, 2); case 'seuclidean' d = sqrt (sum (((X(i,:) - X(j,:)) ./ DistParameter) .^ 2, 2)); case 'mahalanobis' Z = X(i,:) - X(j,:); d = sqrt (sum ((Z * DP_inv) .* Z, 2)); case 'cityblock' d = sum (abs (X(i,:) - X(j,:)), 2); case 'minkowski' d = sum (abs (X(i,:) - X(j,:)) .^ p_exp, 2) .^ p_inv; case 'chebychev' a = abs (X(i,:) - X(j,:)); d = max (a, [], 2); d(any (isnan (a), 2)) = NaN; case 'cosine' d = 1 - sum (X(i,:) .* X(j,:), 2) * sx(i) .* sx(j); case {'correlation', 'spearman'} d = 1 - sum (Xc(i,:) .* Xc(j,:), 2) ./ (nc(i) * nc(j)); case 'hamming' d = mean (X(i,:) != X(j,:), 2); case 'jaccard' nz = (X(i,:) != 0 | X(j,:) != 0); d = sum ((X(i,:) != X(j,:)) & nz, 2) ./ sum (nz, 2); endswitch D(idx + (1:(N-i))) = d; idx += N - i; endfor ## A similarity a rounding step above one would give a negative distance, ## which MATLAB never returns. A NaN compares false and is left alone. if (any (strcmp (Distance, {'cosine', 'correlation', 'spearman'}))) D(D < 0) = 0; endif endif endfunction ## Test output %!shared xy, t, eucl, x %! xy = [0 1; 0 2; 7 6; 5 6]; %! t = 1e-3; %! eucl = @(v,m) sqrt (sumsq (repmat (v,rows (m),1)-m,2)); %! x = [1 2 3; 4 5 6; 7 8 9; 3 2 1]; %!assert_equal (pdist (xy), [1.000 8.602 7.071 8.062 6.403 2.000], t); %!assert_equal (pdist (xy, eucl), [1.000 8.602 7.071 8.062 6.403 2.000], t); %!assert_equal (pdist (xy, 'euclidean'), [1.000 8.602 7.071 8.062 6.403 2.000], t); %!assert_equal (pdist (xy, 'seuclidean'), [0.380 2.735 2.363 2.486 2.070 0.561], t); %!assert_equal (pdist (xy, 'mahalanobis'), [1.384 1.967 2.446 2.384 1.535 2.045], t); %!assert_equal (pdist (xy, 'cityblock'), [1.000 12.00 10.00 11.00 9.000 2.000], t); %!assert_equal (pdist (xy, 'minkowski'), [1.000 8.602 7.071 8.062 6.403 2.000], t); %!assert_equal (pdist (xy, 'minkowski', 3), [1.000 7.763 6.299 7.410 5.738 2.000], t); %!assert_equal (pdist (xy, 'cosine'), [0.000 0.349 0.231 0.349 0.231 0.013], t); %!assert_equal (pdist (xy, 'correlation'), [0.000 2.000 0.000 2.000 0.000 2.000], t); %!assert_equal (pdist (xy, 'spearman'), [0.000 2.000 0.000 2.000 0.000 2.000], t); %!assert_equal (pdist (xy, 'hamming'), [0.500 1.000 1.000 1.000 1.000 0.500], t); %!assert_equal (pdist (xy, 'jaccard'), [1.000 1.000 1.000 1.000 1.000 0.500], t); %!assert_equal (pdist (xy, 'chebychev'), [1.000 7.000 5.000 7.000 5.000 2.000], t); %!assert_equal (pdist (x), [5.1962, 10.3923, 2.8284, 5.1962, 5.9161, 10.7703], 1e-4); %!assert_equal (pdist (x, 'euclidean'), ... %! [5.1962, 10.3923, 2.8284, 5.1962, 5.9161, 10.7703], 1e-4); %!assert_equal (pdist (x, eucl), ... %! [5.1962, 10.3923, 2.8284, 5.1962, 5.9161, 10.7703], 1e-4); %!assert_equal (pdist (x, 'squaredeuclidean'), [27, 108, 8, 27, 35, 116]); %!assert_equal (pdist (x, 'seuclidean'), ... %! [1.8071, 3.6142, 0.9831, 1.8071, 1.8143, 3.4854], 1e-4); %!warning ... %! pdist (x, 'mahalanobis'); %!assert_equal (pdist (x, 'cityblock'), [9, 18, 4, 9, 9, 18]); %!assert_equal (pdist (x, 'minkowski'), ... %! [5.1962, 10.3923, 2.8284, 5.1962, 5.9161, 10.7703], 1e-4); %!assert_equal (pdist (x, 'minkowski', 3), ... %! [4.3267, 8.6535, 2.5198, 4.3267, 5.3485, 9.2521], 1e-4); %!assert_equal (pdist (x, 'cosine'), ... %! [0.0254, 0.0406, 0.2857, 0.0018, 0.1472, 0.1173], 1e-4); %!assert_equal (pdist (x, 'correlation'), [0, 0, 2, 0, 2, 2], 1e-14); %!assert_equal (pdist (x, 'spearman'), [0, 0, 2, 0, 2, 2], 1e-14); %!assert_equal (pdist (x, 'hamming'), [1, 1, 2/3, 1, 1, 1]); %!assert_equal (pdist (x, 'jaccard'), [1, 1, 2/3, 1, 1, 1]); %!assert_equal (pdist (x, 'chebychev'), [3, 6, 2, 3, 5, 8]); %!test %! ## A Minkowski exponent of Inf is the Chebyshev distance, as in MATLAB %! ## R2026b, on the vectorised path and on the blocked one for large N. %! X = [1, 2; 4, 6; 0, 0]; %! assert_equal (pdist (X, 'minkowski', Inf), [4, 2, 6]); %! Y = repmat (X, 334, 1); %! assert_equal (pdist (Y, 'minkowski', Inf), pdist (Y, 'chebychev')); %!assert_equal (pdist ([1, NaN; 4, 6; 0, 0], 'chebychev'), [NaN, NaN, 6]); %!assert_equal (pdist ([1, NaN; 4, 6; 0, 0], 'minkowski', Inf), [NaN, NaN, 6]); %!test %! ## Inf - Inf is NaN, and so is the distance, as for every other metric; %! ## MATLAB R2024a passes over it and gives 1 for the second pair. %! assert_equal (pdist ([1, Inf; 4, 6; 0, Inf], 'chebychev'), [Inf, NaN, Inf]); %!test %! D = pdist ([1, Inf; 4, 6; 0, Inf], 'minkowski', Inf); %! assert_equal (D, [Inf, NaN, Inf]); %!test %! D = pdist ([1, NaN; zeros(1001,2)], 'chebychev'); %! assert_equal (find (isnan (D)), 1:1001); %!test %! D = pdist ([1, Inf; 0, Inf; zeros(1000,2)], 'chebychev'); %! assert_equal (find (isnan (D)), 1); %!test %! ## A row is never at a negative distance from itself, the similarity of %! ## identical rows being able to round a step above one. %! X = [-3, -3; -3, -3]; %! assert_equal (pdist (X, "cosine") >= 0, true); %!test %! ## Neither do the other two metrics built as one minus a similarity, on %! ## either the vectorised path or the blocked one taken for large N. %! Y = [1, 2, 4; 2, 4, 8; -1, -2, -4; 3, 6, 12]; %! for m = {"cosine", "correlation", "spearman"} %! assert_equal (any (pdist (Y, m{1}) < 0), false); %! assert_equal (any (pdist (repmat (Y, 300, 1), m{1}) < 0), false); %! endfor statistics-release-2.1.2/inst/Nearest_Neighbors/pdist2.m000066400000000000000000000716211527311104300232710ustar00rootroot00000000000000## Copyright (C) 2014-2019 Piotr Dollar ## Copyright (C) 2024-2025 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{D} =} pdist2 (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{D} =} pdist2 (@var{X}, @var{Y}, @var{Distance}) ## @deftypefnx {statistics} {@var{D} =} pdist2 (@var{X}, @var{Y}, @var{Distance}, @var{DistParameter}) ## @deftypefnx {statistics} {@var{D} =} pdist2 (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{D}, @var{I}] =} pdist2 (@dots{}, @var{Name}, @var{Value}) ## ## Compute pairwise distance between two sets of vectors. ## ## @code{@var{D} = pdist2 (@var{X}, @var{Y})} calculates the euclidean distance ## between each pair of observations in @var{X} and @var{Y}. Let @var{X} be an ## @math{M*P} matrix representing @math{M} points in @math{P}-dimensional space ## and @var{Y} be an @math{N*P} matrix representing another set of points in the ## same space. This function computes the @math{M*N} distance matrix @var{D}, ## where @qcode{@var{D}(i,j)} is the distance between @qcode{@var{X}(i,:)} and ## @qcode{@var{Y}(j,:)}. ## ## @code{@var{D} = pdist2 (@var{X}, @var{Y}, @var{Distance})} returns the ## distance between each pair of observations in @var{X} and @var{Y} using the ## metric specified by @var{Distance}, which can be any of the following ## options. ## ## @multitable @columnfractions 0.23 0.65 ## @item @qcode{'euclidean'} @tab Euclidean distance. ## @item @qcode{'fasteuclidean'} @tab Euclidean distance computed with an ## alternative algorithm which may be faster but might reduce accuracy. ## @item @qcode{'squaredeuclidean'} @tab Squared Euclidean distance. ## @item @qcode{'fastsquaredeuclidean'} @tab Euclidean distance computed ## with an alternative algorithm which may be faster but might reduce accuracy. ## @item @qcode{'seuclidean'} @tab standardized Euclidean distance. Each ## coordinate difference between the rows in @var{X} and the query matrix ## @var{Y} is scaled by dividing by the corresponding element of the standard ## deviation computed from @var{X}. A different scaling vector can be specified ## with the subsequent @var{DistParameter} input argument. ## @item @qcode{'mahalanobis'} @tab Mahalanobis distance, computed using a ## positive definite covariance matrix. A different covariance matrix can be ## specified with the subsequent @var{DistParameter} input argument. ## @item @qcode{'cityblock'} @tab City block distance. ## @item @qcode{'minkowski'} @tab Minkowski distance. The default exponent ## is 2. A different exponent can be specified with the subsequent ## @var{DistParameter} input argument. An exponent of @code{Inf} gives the ## Chebychev distance. ## @item @qcode{'chebychev'} @tab Chebychev distance (maximum coordinate ## difference). A difference of @code{Inf - Inf} is @code{NaN}, and so is the ## distance, as for every other metric; MATLAB passes over it. ## @item @qcode{'cosine'} @tab One minus the cosine of the included angle ## between points (treated as vectors). ## @item @qcode{'correlation'} @tab One minus the sample linear correlation ## between observations (treated as sequences of values). ## @item @qcode{'hamming'} @tab Hamming distance, which is the percentage ## of coordinates that differ. ## @item @qcode{'jaccard'} @tab One minus the Jaccard coefficient, which is ## the percentage of nonzero coordinates that differ. ## @item @qcode{'spearman'} @tab One minus the sample Spearman's rank ## correlation between observations (treated as sequences of values). ## @item @var{@@distfun} @tab Custom distance function handle. A distance ## function of the form @code{function @var{D2} = distfun (@var{XI}, @var{YI})}, ## where @var{XI} is a @math{1*P} vector containing a single observation in ## @math{P}-dimensional space, @var{YI} is an @math{N*P} matrix containing an ## arbitrary number of observations in the same @math{P}-dimensional space, and ## @var{D2} is an @math{N*P} vector of distances, where @qcode{(@var{D2}k)} is ## the distance between observations @var{XI} and @qcode{(@var{YI}k,:)}. ## @end multitable ## ## @code{@var{D} = pdist2 (@var{X}, @var{Y}, @var{Distance}, ## @var{DistParameter})} ## returns the distance using the metric specified by @var{Distance} and ## @var{DistParameter}. The latter one can only be specified when the selected ## @var{Distance} is @qcode{'seuclidean'}, @qcode{'minkowski'}, and ## @qcode{'mahalanobis'}. ## ## @code{@var{D} = pdist2 (@dots{}, @var{Name}, @var{Value})} for any previous ## arguments, modifies the computation using @var{Name}-@var{Value} parameters. ## @itemize ## @item ## @code{@var{D} = pdist2 (@var{X}, @var{Y}, @var{Distance}, @qcode{'Smallest'}, ## @var{K})} computes the distance using the metric specified by ## @var{Distance} and returns the @var{K} smallest pairwise distances to ## observations in @var{X} for each observation in @var{Y} in ascending order. ## @item ## @code{@var{D} = pdist2 (@var{X}, @var{Y}, @var{Distance}, ## @var{DistParameter}, ## @qcode{'Largest'}, @var{K})} computes the distance using the metric specified ## by @var{Distance} and @var{DistParameter} and returns the @var{K} largest ## pairwise distances in descending order. ## @end itemize ## ## @code{[@var{D}, @var{I}] = pdist2 (@dots{}, @var{Name}, @var{Value})} also ## returns the matrix @var{I}, which contains the indices of the observations in ## @var{X} corresponding to the distances in @var{D}. You must specify either ## @qcode{'Smallest'} or @qcode{'Largest'} as an optional @var{Name}-@var{Value} ## pair argument to compute the second output argument. ## ## @seealso{pdist, knnsearch, rangesearch} ## @end deftypefn function [D, I] = pdist2 (X, Y, varargin) ## Check input data if (nargin < 2) error ("pdist2: too few input arguments."); endif if (size (X, 2) != size (Y, 2)) error ("pdist2: X and Y must have equal number of columns."); endif if (ndims (X) != 2 || ndims (Y) != 2) error ("pdist2: X and Y must be 2 dimensional matrices."); endif ## Add default values Distance = 'euclidean'; # Distance metric DistParameter = []; # Distance parameter SortOrder = []; # Flag for sorting distances to find ## Parse additional Distance metric and Distance parameter (if available) DMs = {'euclidean', 'squaredeuclidean', 'seuclidean', ... 'fasteuclidean', 'fastsquaredeuclidean', ... 'chebychev', 'cityblock', 'cosine', 'correlation', ... 'mahalanobis', 'minkowski', 'hamming', 'jaccard', 'spearman'}; if (numel (varargin) > 0) if (any (strcmpi (DMs, varargin{1}))) Distance = tolower (varargin{1}); varargin(1) = []; if (numel (varargin) > 0) if (isnumeric (varargin{1})) DistParameter = varargin{1}; varargin(1) = []; endif endif elseif (is_function_handle (varargin{1})) Distance = varargin{1}; varargin(1) = []; if (numel (varargin) > 0) if (isnumeric (varargin{1})) DistParameter = varargin{1}; varargin(1) = []; endif endif endif endif ## Parse additional parameters in Name/Value pairs parcount = 0; while (numel (varargin) > 0) if (numel (varargin) < 2) error ("pdist2: missing value in optional name/value paired arguments."); endif switch (tolower (varargin{1})) case 'smallest' SortOrder = 'ascend'; K = varargin{2}; parcount += 1; case 'largest' SortOrder = 'descend'; K = varargin{2}; parcount += 1; otherwise error ("pdist2: invalid NAME in optional pairs of arguments."); endswitch varargin(1:2) = []; endwhile ## Check additional arguments if (parcount > 0) if (fix (K) != K || K < 1) error ("pdist2: K must be a positive integer value."); endif endif if (parcount > 1) error ("pdist2: you can only specify either 'Smallest' or 'Largest'."); endif if (isempty (SortOrder) && nargout > 1) error (strcat ("pdist2: 'Smallest' or 'Largest' must be", ... " specified to compute second output.")); endif ## A Minkowski exponent of Inf is the Chebyshev distance, its limit. The ## Minkowski formula itself would raise every difference to Inf and back. if (strcmp (Distance, 'minkowski') && isscalar (DistParameter) && DistParameter == Inf) Distance = 'chebychev'; endif ## FAST PATH: Optimization for fast Euclidean algorithm fast_algs = {'fasteuclidean', 'fastsquaredeuclidean'}; if (ischar (Distance) && ismember (Distance, fast_algs)) ## sumsq avoids the temporary memory of X.^2 ## D = ||x||^2 + ||y||^2 - 2x.y D = sumsq (X, 2) + sumsq (Y, 2).' - 2 * (X * Y'); ## Remove possible numerical negative noise D = max (D, 0); if (strcmp (Distance, 'fasteuclidean')) D = sqrt (D); endif ## Handle SortOrder (Smallest/Largest) output if requested if (nargout > 1 || parcount) ## This finds K nearest X's for each Y. [D, I] = sort (D, 1, SortOrder); K = min (size (D, 1), K); D = D(1:K, :); I = I(1:K, :); endif return; endif ## Threshold for switching between vectorized and blocked computation M = size (X, 1); Nrows = size (Y, 1); N_threshold = 500; ## Handle a function handle (always row-by-row) if (is_function_handle (Distance)) ## Check the input output sizes of the user function D2 = []; try D2 = Distance(X(1,:), Y); catch ME error ("pdist2: invalid function handle for distance metric."); end_try_catch if (! isequal (size (D2), [Nrows, 1])) error ("pdist2: custom distance function produces wrong output size."); endif ## Evaluate user defined distance metric function D = zeros (M, Nrows); for r = 1:M D(r,:) = feval (Distance, X(r,:), Y)'; endfor if (nargout > 1 || parcount) [D, I] = sort (D', 2, SortOrder); K = min (size (D, 2), K); D = D(:,1:K)'; I = I(:,1:K)'; endif return; endif ## For small M and N: use original vectorized implementation (fast, O(M*N*P) memory) ## For large M or N: use blocked row-by-row computation (slower, O(N*P) memory) if (max (M, Nrows) < N_threshold) ## Original vectorized implementation if (strcmp (Distance, 'cosine')) X_inv_norm = 1 ./ sqrt (sumsq (X, 2)); Y_inv_norm = 1 ./ sqrt (sumsq (Y, 2)); ## Normalize the matrices in-place (broadcasting) X = X .* X_inv_norm; Y = Y .* Y_inv_norm; ## Cosine distance = 1 - dot_product (normalized vectors) D = 1 - (X * Y'); ## A similarity a rounding step above one would give a negative ## distance, which MATLAB never returns. Only the sign is corrected: ## parallel rows still land a rounding step above zero in both ## engines. A NaN compares false and is left alone. D(D < 0) = 0; else ## Create indexing grid [ix, iy] = meshgrid (1:M, 1:Nrows); switch (Distance) case 'euclidean' D = sqrt (sum ((X(ix(:),:) - Y(iy(:),:)) .^ 2, 2)); case 'squaredeuclidean' D = sum ((X(ix(:),:) - Y(iy(:),:)) .^ 2, 2); case 'seuclidean' if (isempty (DistParameter)) DistParameter = std (X, [], 1); else if (numel (DistParameter) != columns (X)) error (strcat ("pdist2: DistParameter for standardized", ... " euclidean must be a vector of equal length", ... " to the number of columns in X.")); endif if (any (DistParameter < 0)) error (strcat ("pdist2: DistParameter for standardized", ... " euclidean must be a nonnegative vector.")); endif endif DistParameter(DistParameter == 0) = 1; # fix constant variable D = sqrt (sum (((X(ix(:),:) - Y(iy(:),:)) ./ DistParameter) .^ 2, 2)); case 'mahalanobis' if (isempty (DistParameter)) DistParameter = cov (X(! any (isnan (X), 2),:)); else if (columns (DistParameter) != columns (X)) error (strcat ("pdist2: DistParameter for mahalanobis", ... " distance must be a covariance matrix with", ... " the same number of columns as X.")); endif [~, p] = chol (DistParameter); if (p != 0) error (strcat ("pdist2: covariance matrix for mahalanobis", ... " distance must be symmetric and positive", ... " definite.")); endif endif ## Catch warning if matrix is close to singular or badly scaled. [DP_inv, rc] = inv (DistParameter); if (rc < eps) msg = sprintf (strcat ("pdist2: matrix is close to", ... " singular or badly scaled.\n RCOND = ", ... " %e. Results may be inaccurate."), rc); warning (msg); endif dxy = X(ix(:),:) - Y(iy(:),:); D = sqrt (sum ((dxy * DP_inv) .* dxy, 2)); case 'cityblock' D = sum (abs (X(ix(:),:) - Y(iy(:),:)), 2); case 'minkowski' if (isempty (DistParameter)) DistParameter = 2; else if (! (isnumeric (DistParameter) && isscalar (DistParameter) && DistParameter > 0)) error (strcat ("pdist2: DistParameter for minkowski distance", ... " must be a positive scalar.")); endif endif D = sum (abs (X(ix(:),:) - Y(iy(:),:)) .^ DistParameter, 2) .^ ... (1 / DistParameter); case 'chebychev' A = abs (X(ix(:),:) - Y(iy(:),:)); D = max (A, [], 2); ## max passes over NaN, so a pair is marked by hand wherever a ## coordinate difference is NaN, from a NaN in either row or from ## Inf - Inf, as every other metric leaves it. MATLAB passes over ## the second. D(any (isnan (A), 2)) = NaN; case 'correlation' mX = mean (X(ix(:),:), 2); mY = mean (Y(iy(:),:), 2); xy = sum ((X(ix(:),:) - mX) .* (Y(iy(:),:) - mY), 2); xx = sqrt (sum ((X(ix(:),:) - mX) .* (X(ix(:),:) - mX), 2)); yy = sqrt (sum ((Y(iy(:),:) - mY) .* (Y(iy(:),:) - mY), 2)); D = 1 - (xy ./ (xx .* yy)); D(D < 0) = 0; case 'hamming' D = mean (abs (X(ix(:),:) != Y(iy(:),:)), 2); case 'jaccard' xy0 = (X(ix(:),:) != 0 | Y(iy(:),:) != 0); D = sum ((X(ix(:),:) != Y(iy(:),:)) & xy0, 2) ./ sum (xy0, 2); case 'spearman' for i = 1:M rX(i,:) = tiedrank (X(i,:)); endfor for i = 1:Nrows rY(i,:) = tiedrank (Y(i,:)); endfor rM = (size (X, 2) + 1) / 2; xy = sum ((rX(ix(:),:) - rM) .* (rY(iy(:),:) - rM), 2); xx = sqrt (sum ((rX(ix(:),:) - rM) .* (rX(ix(:),:) - rM), 2)); yy = sqrt (sum ((rY(iy(:),:) - rM) .* (rY(iy(:),:) - rM), 2)); D = 1 - (xy ./ (xx .* yy)); D(D < 0) = 0; endswitch ## From vector to matrix D = reshape (D, Nrows, M)'; endif else ## Blocked row-by-row computation for large M or N (avoids O(M*N*P) memory) D = zeros (M, Nrows); ## Precompute metric-specific data switch (Distance) case 'seuclidean' if (isempty (DistParameter)) DistParameter = std (X, [], 1); else if (numel (DistParameter) != columns (X)) error (strcat ("pdist2: DistParameter for standardized", ... " euclidean must be a vector of equal length", ... " to the number of columns in X.")); endif if (any (DistParameter < 0)) error (strcat ("pdist2: DistParameter for standardized", ... " euclidean must be a nonnegative vector.")); endif endif DistParameter(DistParameter == 0) = 1; case 'mahalanobis' if (isempty (DistParameter)) DistParameter = cov (X(! any (isnan (X), 2),:)); else if (columns (DistParameter) != columns (X)) error (strcat ("pdist2: DistParameter for mahalanobis", ... " distance must be a covariance matrix with", ... " the same number of columns as X.")); endif [~, p] = chol (DistParameter); if (p != 0) error (strcat ("pdist2: covariance matrix for mahalanobis", ... " distance must be symmetric and positive", ... " definite.")); endif endif [DP_inv, rc] = inv (DistParameter); if (rc < eps) msg = sprintf (strcat ("pdist2: matrix is close to", ... " singular or badly scaled.\n RCOND = ", ... " %e. Results may be inaccurate."), rc); warning (msg); endif case 'minkowski' if (isempty (DistParameter)) DistParameter = 2; else if (! (isnumeric (DistParameter) && isscalar (DistParameter) && DistParameter > 0)) error (strcat ("pdist2: DistParameter for minkowski distance", ... " must be a positive scalar.")); endif endif case 'cosine' sx = sum (X .^ 2, 2) .^ (-1 / 2); sy = sum (Y .^ 2, 2) .^ (-1 / 2); case 'spearman' rX = zeros (size (X)); rY = zeros (size (Y)); for i = 1:M rX(i,:) = tiedrank (X(i,:)); endfor for i = 1:Nrows rY(i,:) = tiedrank (Y(i,:)); endfor rM = (size (X, 2) + 1) / 2; endswitch ## Row-by-row computation with switch outside loop switch (Distance) case 'euclidean' for i = 1:M D(i,:) = sqrt (sum ((X(i,:) - Y) .^ 2, 2))'; endfor case 'squaredeuclidean' for i = 1:M D(i,:) = sum ((X(i,:) - Y) .^ 2, 2)'; endfor case 'seuclidean' for i = 1:M D(i,:) = sqrt (sum (((X(i,:) - Y) ./ DistParameter) .^ 2, 2))'; endfor case 'mahalanobis' for i = 1:M dxy = X(i,:) - Y; D(i,:) = sqrt (sum ((dxy * DP_inv) .* dxy, 2))'; endfor case 'cityblock' for i = 1:M D(i,:) = sum (abs (X(i,:) - Y), 2)'; endfor case 'minkowski' for i = 1:M D(i,:) = (sum (abs (X(i,:) - Y) .^ DistParameter, 2) .^ (1 / DistParameter))'; endfor case 'chebychev' ## A NaN coordinate difference makes the distance NaN, as above for i = 1:M A = abs (X(i,:) - Y); d = max (A, [], 2); d(any (isnan (A), 2)) = NaN; D(i,:) = d'; endfor case 'cosine' for i = 1:M d = (1 - sum (X(i,:) .* Y, 2) .* sx(i) .* sy)'; d(d < 0) = 0; D(i,:) = d; endfor case 'correlation' for i = 1:M mXi = mean (X(i,:)); mY = mean (Y, 2); xy = sum ((X(i,:) - mXi) .* (Y - mY), 2); xx = sqrt (sum ((X(i,:) - mXi) .^ 2)); yy = sqrt (sum ((Y - mY) .^ 2, 2)); d = (1 - (xy ./ (xx .* yy)))'; d(d < 0) = 0; D(i,:) = d; endfor case 'hamming' for i = 1:M D(i,:) = mean (abs (X(i,:) != Y), 2)'; endfor case 'jaccard' for i = 1:M xy0 = (X(i,:) != 0 | Y != 0); D(i,:) = (sum ((X(i,:) != Y) & xy0, 2) ./ sum (xy0, 2))'; endfor case 'spearman' for i = 1:M xy = sum ((rX(i,:) - rM) .* (rY - rM), 2); xx = sqrt (sum ((rX(i,:) - rM) .^ 2)); yy = sqrt (sum ((rY - rM) .^ 2, 2)); d = (1 - (xy ./ (xx .* yy)))'; d(d < 0) = 0; D(i,:) = d; endfor endswitch endif if (nargout > 1 || parcount) [D, I] = sort (D', 2, SortOrder); K = min (size (D, 2), K); # fix max K to avoid out of bound error D = D(:,1:K)'; I = I(:,1:K)'; endif endfunction ## Test output %!shared x, y, xx %! x = [1, 1, 1; 2, 2, 2; 3, 3, 3]; %! y = [0, 0, 0; 1, 2, 3; 0, 2, 4; 4, 7, 1]; %! xx = [1 2 3; 4 5 6; 7 8 9; 3 2 1]; %!test %! d = sqrt ([3, 5, 11, 45; 12, 2, 8, 30; 27, 5, 11, 21]); %! assert_equal (pdist2 (x, y), d); %!test %! d = [5.1962, 2.2361, 3.3166, 6.7082; ... %! 3.4641, 2.2361, 3.3166, 5.4772]; %! i = [3, 1, 1, 1; 2, 3, 3, 2]; %! [D, I] = pdist2 (x, y, 'euclidean', 'largest', 2); %! assert_equal ({D, I}, {d, i}, 1e-4); %!test %! d = [1.7321, 1.4142, 2.8284, 4.5826; ... %! 3.4641, 2.2361, 3.3166, 5.4772]; %! i = [1, 2, 2, 3;2, 1, 1, 2]; %! [D, I] = pdist2 (x, y, 'euclidean', 'smallest', 2); %! assert_equal ({D, I}, {d, i}, 1e-4); %!test %! yy = [1 2 3;5 6 7;9 5 1]; %! d = [0, 6.1644, 5.3852; 1.4142, 6.9282, 8.7750; ... %! 3.7417, 7.0711, 9.9499; 6.1644, 10.4881, 10.3441]; %! i = [2, 4, 4; 3, 2, 2; 1, 3, 3; 4, 1, 1]; %! [D, I] = pdist2 (y, yy, 'euclidean', 'smallest', 4); %! assert_equal ({D, I}, {d, i}, 1e-4); %!test %! yy = [1 2 3;5 6 7;9 5 1]; %! d = [0, 38, 29; 2, 48, 77; 14, 50, 99; 38, 110, 107]; %! i = [2, 4, 4; 3, 2, 2; 1, 3, 3; 4, 1, 1]; %! [D, I] = pdist2 (y, yy, 'squaredeuclidean', 'smallest', 4); %! assert_equal ({D, I}, {d, i}, 1e-4); %!test %! yy = [1 2 3;5 6 7;9 5 1]; %! d = [0, 3.3256, 2.7249; 0.7610, 3.3453, 4.4799; ... %! 1.8514, 3.3869, 5.0703; 2.5525, 5.0709, 5.1297]; %! i = [2, 2, 4; 3, 4, 2; 1, 3, 1; 4, 1, 3]; %! [D, I] = pdist2 (y, yy, 'seuclidean', 'smallest', 4); %! assert_equal ({D, I}, {d, i}, 1e-4); %!test %! d = [2.1213, 4.2426, 6.3640; 1.2247, 2.4495, 4.4159; ... %! 3.2404, 4.8990, 6.8191; 2.7386, 4.2426, 6.1237]; %! assert_equal (pdist2 (y, x, 'mahalanobis'), d, 1e-4); %!test %! xx = [1, 3, 4; 3, 5, 4; 8, 7, 6]; %! d = [1.3053, 1.8257, 15.0499; 1.3053, 3.3665, 16.5680]; %! i = [2, 2, 2; 3, 4, 4]; %! [D, I] = pdist2 (y, xx, 'mahalanobis', 'smallest', 2); %! assert_equal ({D, I}, {d, i}, 1e-4); %!test %! d = [2.5240, 4.1633, 17.3638; 2.0905, 3.9158, 17.0147]; %! i = [1, 1, 3; 4, 3, 1]; %! [D, I] = pdist2 (y, xx, 'mahalanobis', 'largest', 2); %! assert_equal ({D, I}, {d, i}, 1e-4); %!test %! d = [3, 3, 5, 9; 6, 2, 4, 8; 9, 3, 5, 7]; %! assert_equal (pdist2 (x, y, 'cityblock'), d); %!test %! d = [1, 2, 3, 6; 2, 1, 2, 5; 3, 2, 3, 4]; %! assert_equal (pdist2 (x, y, 'chebychev'), d); %!test %! d = repmat ([NaN, 0.0742, 0.2254, 0.1472], [3, 1]); %! assert_equal (pdist2 (x, y, 'cosine'), d, 1e-4); %!test %! yy = [1 2 3;5 6 7;9 5 1]; %! d = [0, 0, 0.5; 0, 0, 2; 1.5, 1.5, 2; NaN, NaN, NaN]; %! i = [2, 2, 4; 3, 3, 2; 4, 4, 3; 1, 1, 1]; %! [D, I] = pdist2 (y, yy, 'correlation', 'smallest', 4); %! assert_equal ({D, I}, {d, i}, eps); %! [D, I] = pdist2 (y, yy, 'spearman', 'smallest', 4); %! assert_equal ({D, I}, {d, i}, eps); %!test %! d = [1, 2/3, 1, 1; 1, 2/3, 1, 1; 1, 2/3, 2/3, 2/3]; %! i = [1, 1, 1, 2; 2, 2, 3, 3; 3, 3, 2, 1]; %! [D, I] = pdist2 (x, y, 'hamming', 'largest', 4); %! assert_equal ({D, I}, {d, i}, eps); %! [D, I] = pdist2 (x, y, 'jaccard', 'largest', 4); %! assert_equal ({D, I}, {d, i}, eps); %!test %! xx = [1, 2, 3, 4; 2, 3, 4, 5; 3, 4, 5, 6]; %! yy = [1, 2, 2, 3; 2, 3, 3, 4]; %! [D, I] = pdist2 (x, y, 'euclidean', 'Smallest', 4); %! eucldist = @(v,m) sqrt (sumsq (repmat (v,rows (m),1)-m,2)); %! [d, i] = pdist2 (x, y, eucldist, 'Smallest', 4); %! assert_equal ({D, I}, {d, i}); %!warning ... %! pdist2 (xx, xx, 'mahalanobis'); %!test %! ## A Minkowski exponent of Inf is the Chebyshev distance, as in MATLAB %! ## R2026b, on the vectorised path and on the blocked one for large inputs. %! X = [1, 2; 4, 6; 0, 0]; %! assert_equal (pdist2 (X(1,:), X(2:3,:), 'minkowski', Inf), [4, 2]); %! Y = repmat (X, 200, 1); %! assert_equal (pdist2 (Y, X, 'minkowski', Inf), pdist2 (Y, X, 'chebychev')); %!test %! X = [1, 2; 4, 6; 0, 0]; %! D = pdist2 ([1, NaN; 4, 6; 0, 0], X, 'chebychev'); %! assert_equal (D, [NaN, NaN, NaN; 4, 0, 6; 2, 6, 0]); %!test %! X = [1, 2; 4, 6; 0, 0]; %! D = pdist2 ([1, NaN; 4, 6; 0, 0], X, 'minkowski', Inf); %! assert_equal (D, [NaN, NaN, NaN; 4, 0, 6; 2, 6, 0]); %!test %! X = [1, 2; 4, 6; 0, 0]; %! D = pdist2 ([1, Inf; 4, 6; 0, Inf], X, 'chebychev'); %! assert_equal (D, [Inf, Inf, Inf; 4, 0, 6; Inf, Inf, Inf]); %!test %! D = pdist2 ([4, 6; 0, 0], [zeros(499,2); 1, NaN], 'chebychev'); %! assert_equal (isnan (D), [false(2,499), true(2,1)]); %!test %! ## Inf - Inf is NaN, and so is the distance, as for every other metric; %! ## MATLAB R2024a passes over it and gives 1 for the first row. %! D = pdist2 ([1, Inf; 1, -Inf; 1, 5], [0, Inf], 'chebychev'); %! assert_equal (D, [NaN; Inf; Inf]); %!test %! D = pdist2 ([1, Inf; 1, -Inf; 1, 5], [0, Inf], 'minkowski', Inf); %! assert_equal (D, [NaN; Inf; Inf]); %!test %! D = pdist2 ([1, Inf; 1, -Inf; 1, 5], [0, Inf], 'euclidean'); %! assert_equal (D, [NaN; Inf; Inf]); %!test %! D = pdist2 ([0, Inf; 1, 1], [zeros(499,2); 1, Inf], 'chebychev'); %! assert_equal (isnan (D), [false(1,499), true; false(1,500)]); ## Test input validation %!test %! ## A row is never at a negative distance from itself. For identical rows %! ## the similarity can round a step above one, and one minus it is then %! ## below zero, which no distance may be. A distance a rounding step above %! ## zero is kept, MATLAB returning one there too. %! X = [-3, -3; -3, -3]; %! assert_equal (pdist2 (X, X, "cosine"), zeros (2)); %!test %! ## Neither do the other two metrics built as one minus a similarity. %! Y = [1, 2, 4; 2, 4, 8; -1, -2, -4]; %! for m = {"cosine", "correlation", "spearman"} %! D = pdist2 (Y, Y, m{1}); %! assert_equal (any (D(:) < 0), false); %! endfor %!test %! ## The clamp corrects the sign and leaves a NaN alone: a zero row has no %! ## direction, so its cosine distance is undefined rather than zero, and %! ## stays undefined. %! Z = [0, 0; 1, 1]; %! D = pdist2 (Z, Z, "cosine"); %! assert_equal (isnan (D), [true, true; true, false]); %! assert_equal (D(2,2) >= 0, true); %!error pdist2 (1) %!error ... %! pdist2 (ones (4, 5), ones (4)) %!error ... %! pdist2 (ones (4, 2, 3), ones (3, 2)) %!error ... %! pdist2 (ones (3), ones (3), 'euclidean', 'Largest') %!error ... %! pdist2 (ones (3), ones (3), 'minkowski', 3, 'Largest') %!error ... %! pdist2 (ones (3), ones (3), 'minkowski', 3, 'large', 4) %!error ... %! pdist2 (ones (3), ones (3), 'minkowski', 3, 'largest', 4.5) %!error ... %! pdist2 (ones (3), ones (3), 'minkowski', 3, 'Largest', 4, 'smallest', 5) %!error ... %! [d, i] = pdist2 (ones (3), ones (3), 'minkowski', 3) %!error ... %! pdist2 (ones (3), ones (3), 'seuclidean', 3) %!error ... %! pdist2 (ones (3), ones (3), 'seuclidean', [1, -1, 3]) %!error ... %! pdist2 (ones (3), eye (3), 'mahalanobis', eye (2)) %!error ... %! pdist2 (ones (3), eye (3), 'mahalanobis', ones (3)) %!error ... %! pdist2 (ones (3), eye (3), 'minkowski', 0) %!error ... %! pdist2 (ones (3), eye (3), 'minkowski', -5) %!error ... %! pdist2 (ones (3), eye (3), 'minkowski', [1, 2]) %!error ... %! pdist2 (ones (3), ones (3), @(v,m) sqrt (repmat (v,rows (m),1)-m,2)) %!error ... %! pdist2 (ones (3), ones (3), @(v,m) sqrt (sum (sumsq (repmat (v,rows (m),1)-m,2)))) statistics-release-2.1.2/inst/Nearest_Neighbors/private/000077500000000000000000000000001527311104300233515ustar00rootroot00000000000000statistics-release-2.1.2/inst/Nearest_Neighbors/private/__build_kdtree__.m000066400000000000000000000047701527311104300267700ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{node} =} __build_kdtree__ (@var{indices}, @var{depth}, @var{X}, @var{bucket_size}) ## ## Build a Kd-tree over the rows of @var{X} named by @var{indices}. Shared by ## @code{knnsearch}, @code{rangesearch} and @code{KDTreeSearcher}. ## ## A node is a leaf, carrying an @qcode{indices} field, or a split, carrying ## @qcode{axis}, @qcode{split_value}, @qcode{left} and @qcode{right}. The ## split axis cycles with the depth. ## ## @end deftypefn function node = __build_kdtree__ (indices, depth, X, bucket_size) if (length (indices) <= bucket_size) node = struct ('indices', indices); else k = size (X, 2); axis = mod (depth, k) + 1; values = X(indices, axis); sorted_values = sort (values); median_idx = floor ((length (indices) + 1) / 2); split_value = sorted_values(median_idx); left_indices = indices(values <= split_value); ## A NaN on this axis compares false both ways, so it is sent right ## rather than lost from both sides right_indices = indices(! (values <= split_value)); ## Points sharing the split value all go left, so a set that is constant ## on this axis cannot be divided here. Without this the recursion hands ## itself the same set forever, which more than BUCKET_SIZE identical rows ## used to do. if (isempty (left_indices) || isempty (right_indices)) node = struct ('indices', indices); return; endif left_node = __build_kdtree__ (left_indices, depth + 1, X, bucket_size); right_node = __build_kdtree__ (right_indices, depth + 1, X, bucket_size); node = struct ('axis', axis, 'split_value', split_value, ... 'left', left_node, 'right', right_node); endif endfunction statistics-release-2.1.2/inst/Nearest_Neighbors/private/__nomprep__.m000066400000000000000000000166401527311104300260120ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{C}, @var{measure}, @var{w}, @var{errmsg}] =} __nomprep__ (@var{X}, @var{Y}, @var{hasY}, @var{args}) ## ## Validate and code the input of @code{nomdist} and @code{nomdist2}. ## ## @var{C} holds the level codes of @var{X}, @var{Y} and the reference sample, ## positive integers shared across the three and one column per variable, ## the last empty where no @qcode{'Reference'} was given. @var{measure} is ## the measure named in lower case, @qcode{'goodall3'} where none was, and ## @var{w} a row of one weight per variable. @var{errmsg} is empty, or the ## body of the message the caller raises under its own name. ## ## @var{args} holds what followed @var{X} and @var{Y}: a measure where their ## number is odd, then @qcode{'Weights'} and @qcode{'Reference'} pairs. ## ## @end deftypefn function [C, measure, w, errmsg] = __nomprep__ (X, Y, hasY, args) C = {}; w = []; errmsg = ''; names = {'anderberg', 'burnaby', 'eskin', 'gambaryan', 'goodall1', ... 'goodall2', 'goodall3', 'goodall4', 'iof', 'lin', 'lin1', 'of', ... 'sm', 'smirnov', 've', 'vm'}; ## The measure, which an odd number of arguments leads with measure = 'goodall3'; if (mod (numel (args), 2) == 1) m = args{1}; if (isa (m, 'string') && isscalar (m)) m = char (m); endif if (! (ischar (m) && isrow (m) && any (strcmpi (m, names)))) errmsg = sprintf ("MEASURE must be one of %s.", ... strjoin (strcat ("'", names, "'"), ', ')); return; endif measure = lower (m); args(1) = []; endif ## An explicit empty value counts as not given [w, R, rem] = parsePairedArguments ({'Weights', 'Reference'}, {[], []}, ... args(:)); if (! isempty (rem)) errmsg = "invalid optional paired argument."; return; endif hasR = ! isempty (R); ## One column of levels per variable, a table read by X's variable names [xc, vars, errmsg] = nomColumns (X, 'X', X, {}); if (! isempty (errmsg)) return; endif if (size (X, 1) < 1 || isempty (xc)) errmsg = "X must not be empty."; return; endif K = numel (xc); yc = {}; rc = {}; if (hasY) [yc, ~, errmsg] = nomColumns (Y, 'Y', X, vars); if (! isempty (errmsg)) return; endif if (numel (yc) != K) errmsg = "Y must have as many columns as X."; return; endif endif if (hasR) [rc, ~, errmsg] = nomColumns (R, "'Reference'", X, vars); if (! isempty (errmsg)) return; endif if (numel (rc) != K) errmsg = "'Reference' must have as many columns as X."; return; endif endif ## Codes shared across the samples, one variable at a time nx = rows (xc{1}); ny = 0; nr = 0; if (hasY) ny = rows (yc{1}); endif if (hasR) nr = rows (rc{1}); endif Xc = zeros (nx, K); Yc = zeros (ny, K); Rc = zeros (nr, K); for k = 1:K f = levelType (xc{k}); parts = {xc{k}}; if (hasY) if (! strcmp (levelType (yc{k}), f)) errmsg = "Y must hold the same type as X in every variable."; return; endif parts{end+1} = yc{k}; endif if (hasR) if (! strcmp (levelType (rc{k}), f)) errmsg = "'Reference' must hold the same type as X in every variable."; return; endif parts{end+1} = rc{k}; endif if (strcmp (f, 'numeric')) parts = cellfun (@double, parts, 'UniformOutput', false); endif g = grp2idx (vertcat (parts{:})); Xc(:,k) = g(1:nx); Yc(:,k) = g(nx+1:nx+ny); Rc(:,k) = g(nx+ny+1:end); endfor if (any (isnan (Xc(:)))) errmsg = "X must not hold missing values."; return; endif if (any (isnan (Yc(:)))) errmsg = "Y must not hold missing values."; return; endif if (any (isnan (Rc(:)))) errmsg = "'Reference' must not hold missing values."; return; endif if (hasR) for k = 1:K if (! all (ismember (Xc(:,k), Rc(:,k)))) errmsg = "'Reference' must hold every level that X holds."; return; endif endfor else Rc = []; endif C = {Xc, Yc, Rc}; ## The weights, read by every measure but three if (isempty (w)) w = ones (1, K); return; endif if (any (strcmp (measure, {'anderberg', 'gambaryan', 'smirnov'}))) errmsg = sprintf ("'Weights' does not apply to the '%s' measure.", ... measure); return; endif if (! (isnumeric (w) && isreal (w) && isvector (w) && numel (w) == K && all (isfinite (w)) && all (w >= 0 & w <= 1))) errmsg = sprintf (strcat ("'Weights' must be a vector of %d values", ... " between 0 and 1."), K); return; endif if (! any (w > 0)) errmsg = "'Weights' must hold at least one positive value."; return; endif w = double (w(:)'); endfunction ## The columns of one sample as a cell of column vectors. X decides whether ## the samples are tables, and a table is read by X's variable names. function [cols, vars, errmsg] = nomColumns (A, name, X, vars) cols = {}; errmsg = ''; if (istable (X)) if (! istable (A)) errmsg = sprintf ("%s must be a table, as X is.", name); return; endif if (isempty (vars)) vars = A.Properties.VariableNames; elseif (! all (ismember (vars, A.Properties.VariableNames))) errmsg = sprintf ("%s must hold every variable of X.", name); return; endif cols = cell (1, numel (vars)); for k = 1:numel (vars) v = A.(vars{k}); if (! (isLevels (v) && columns (v) == 1 && ndims (v) == 2)) errmsg = sprintf (strcat ("every variable of %s must be one", ... " column of levels."), name); return; endif cols{k} = v; endfor return; endif if (istable (A)) errmsg = sprintf ("%s must be a matrix, as X is.", name); return; endif if (! isLevels (A) || ndims (A) != 2) errmsg = sprintf (strcat ("%s must be a numeric, logical, categorical,", ... " string or cellstr matrix, or a table."), name); return; endif cols = cell (1, columns (A)); for k = 1:columns (A) cols{k} = A(:,k); endfor endfunction ## Whether an array can hold levels. A char matrix cannot: its rows would ## be the observations and its columns meaningless. function tf = isLevels (v) tf = (isnumeric (v) && isreal (v)) || islogical (v) || iscellstr (v) ... || isa (v, 'categorical') || isa (v, 'string'); endfunction ## The kind of level a column holds, which must agree across the samples. function f = levelType (v) if (isnumeric (v)) f = 'numeric'; elseif (iscellstr (v)) f = 'cellstr'; else f = class (v); endif endfunction statistics-release-2.1.2/inst/Nearest_Neighbors/private/__resolve_metric__.m000066400000000000000000000072211527311104300273470ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{dist}, @var{param}] =} __resolve_metric__ (@var{fname}, @var{X}, @var{dist}, @var{P}, @var{C}, @var{S}, @var{metrics}) ## ## Validate a distance metric and its parameter for a per-call override. ## ## @var{fname} names the caller for the error messages, @var{X} is the training ## data a default parameter is derived from, and @var{dist} the metric to use. ## @var{P}, @var{C} and @var{S} carry the @qcode{'P'}, @qcode{'Cov'} and ## @qcode{'Scale'} values, each empty when not supplied, and @var{metrics} lists ## the metrics the caller accepts. ## ## The returned @var{param} is the distance parameter belonging to @var{dist}: ## the supplied value where there is one, otherwise the same default the ## constructor would derive from @var{X}. Nothing is written back to the ## searcher, so an override applies only to the call that asked for it. ## ## @end deftypefn function [dist, param] = __resolve_metric__ (fname, X, dist, P, C, S, metrics) if (! (ischar (dist) && any (strcmpi (dist, metrics)))) error ("%s: unsupported distance metric '%s'.", fname, dist); endif dist = lower (dist); if (strcmpi (dist, 'minkowski')) if (isempty (P)) param = 2; else if (! (isscalar (P) && isnumeric (P) && P > 0)) error ("%s: 'P' must be a positive scalar.", fname); endif param = P; endif elseif (strcmpi (dist, 'seuclidean')) if (isempty (S)) param = std (X, [], 1); else if (! (isvector (S) && isnumeric (S) && all (S >= 0) && numel (S) == columns (X))) error (strcat ("%s: 'Scale' must be a nonnegative vector", ... " matching the columns of X."), fname); endif param = S(:)'; endif elseif (strcmpi (dist, 'mahalanobis')) if (isempty (C)) param = cov (X); else if (! (isnumeric (C) && ismatrix (C) && rows (C) == columns (C) && rows (C) == columns (X))) error (strcat ("%s: 'Cov' must be a square matrix matching the", ... " columns of X."), fname); endif if (! issymmetric (C)) error ("%s: 'Cov' must be symmetric for mahalanobis.", fname); endif [~, p] = chol (C); if (p != 0) error ("%s: 'Cov' must be positive definite for mahalanobis.", fname); endif param = C; endif else param = []; endif ## A parameter that belongs to a different metric is a mistake, not a value ## to be quietly dropped if (! isempty (P) && ! strcmpi (dist, 'minkowski')) error ("%s: 'P' applies only to the minkowski metric.", fname); endif if (! isempty (S) && ! strcmpi (dist, 'seuclidean')) error ("%s: 'Scale' applies only to the seuclidean metric.", fname); endif if (! isempty (C) && ! strcmpi (dist, 'mahalanobis')) error ("%s: 'Cov' applies only to the mahalanobis metric.", fname); endif endfunction statistics-release-2.1.2/inst/Nearest_Neighbors/private/__search_kdtree__.m000066400000000000000000000146361527311104300271400ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{indices}, @var{distances}] =} __search_kdtree__ (@var{node}, @var{query}, @var{k}, @var{X}, @var{dist}, @var{distparam}, @var{is_range}) ## @deftypefnx {Private Function} {[@var{indices}, @var{distances}] =} __search_kdtree__ (@dots{}, @var{r}) ## ## Search a Kd-tree built by @code{__build_kdtree__} for one query point. ## Shared by @code{knnsearch}, @code{rangesearch} and @code{KDTreeSearcher}. ## ## With @var{is_range} false the @var{k} nearest neighbours are returned; with ## it true, every neighbour within the radius @var{r}. @var{dist} names the ## metric and @var{distparam} is the exponent for @qcode{'minkowski'} and empty ## otherwise. ## ## The metric is resolved to a function of a leaf's rows once per call rather ## than dispatched per leaf, which is what makes the walk affordable: a leaf ## holds a few dozen points and a general distance call costs far more than the ## arithmetic it performs on them. ## ## @end deftypefn function [indices, distances] = __search_kdtree__ (node, query, k, X, dist, ... distparam, is_range, r) if (nargin < 8) r = Inf; endif if (strcmpi (dist, 'minkowski')) if (! (isscalar (distparam) && isnumeric (distparam) && distparam > 0 && isfinite (distparam))) error (strcat ("__search_kdtree__: DISTPARAM must be a positive", ... " finite scalar for minkowski.")); endif elseif (! isempty (distparam)) error (strcat ("__search_kdtree__: DISTPARAM must be empty for", ... " non-minkowski metrics.")); endif ## The metric, resolved once. 'manhattan' is the documented alias of ## 'cityblock' and the search accepts it wherever the callers let it through. switch (lower (dist)) case 'euclidean' compute_dists = @(leaf_X) sqrt (sum ((leaf_X - query) .^ 2, 2)); case {'cityblock', 'manhattan'} compute_dists = @(leaf_X) sum (abs (leaf_X - query), 2); case 'chebychev' ## max passes over NaN, so a NaN coordinate difference, from a NaN in ## either row or from Inf - Inf, is set to NaN explicitly, as every other ## metric here leaves it compute_dists = @(leaf_X) chebychev_dists (leaf_X, query); case 'minkowski' p = distparam; compute_dists = @(leaf_X) sum (abs (leaf_X - query) .^ p, 2) .^ (1 / p); otherwise error ("__search_kdtree__: unsupported distance metric '%s'.", dist); endswitch indices = []; distances = []; search (node, 0); ## Neighbours at equal distance come in row order, as R2024a returns them: ## on fisheriris two points 0.20000000000000018 from a query come back as ## rows 57 then 128, and the K-th neighbour is the earlier row. A range ## search comes back in row order outright, which is what an unsorted ## search returns, and a caller sorting it by distance keeps ties in it. if (is_range) [indices, o] = sort (indices); distances = distances(o); endif function byDistanceThenRow () [~, o] = sortrows ([distances(:), indices(:)]); distances = distances(o); indices = indices(o); endfunction function search (node, depth) if (isempty (node)) return; endif if (isfield (node, 'indices')) leaf_indices = node.indices; dists = compute_dists (X(leaf_indices,:)); if (is_range) mask = dists <= r; indices = [indices; leaf_indices(mask)']; distances = [distances; dists(mask)]; elseif (length (distances) >= k) ## The list is already full, so only a candidate beating its worst ## member, or tying it from an earlier row, is worth merging, and most ## leaves supply none. ## A NaN at the bottom of the list ranks below every candidate. mask = dists <= distances(end) | isnan (distances(end)); if (any (mask)) indices = [indices; leaf_indices(mask)']; distances = [distances; dists(mask)]; byDistanceThenRow (); distances = distances(1:k); indices = indices(1:k); endif else indices = [indices; leaf_indices']; distances = [distances; dists]; if (length (distances) >= k) byDistanceThenRow (); if (length (distances) > k) distances = distances(1:k); indices = indices(1:k); endif endif endif else axis = node.axis; split_value = node.split_value; if (query(axis) <= split_value) nearer = node.left; further = node.right; else nearer = node.right; further = node.left; endif search (nearer, depth + 1); ## The far side can only hold something better if the splitting plane is ## itself closer than the worst neighbour held so far. A query and a ## split at the same infinity put the plane at NaN, which bounds nothing, ## so that side is searched. plane_dist = abs (query(axis) - split_value); if (is_range) if (plane_dist <= r || isnan (plane_dist)) search (further, depth + 1); endif elseif (length (distances) < k || plane_dist <= distances(end) || isnan (plane_dist) || isnan (distances(end))) ## A plane at exactly the worst distance may still hide a tie from an ## earlier row, so that side is searched too, and a NaN worst distance ## is beaten by anything the far side holds. search (further, depth + 1); endif endif endfunction endfunction function d = chebychev_dists (leaf_X, query) a = abs (leaf_X - query); d = max (a, [], 2); d(any (isnan (a), 2)) = NaN; endfunction statistics-release-2.1.2/inst/Nearest_Neighbors/rangesearch.m000066400000000000000000000571011527311104300243430ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## Copyright (C) 2025 Swayam Shah ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{idx} =} rangesearch (@var{X}, @var{Y}, @var{r}) ## @deftypefnx {statistics} {[@var{idx}, @var{D}] =} rangesearch (@var{X}, @var{Y}, @var{r}) ## @deftypefnx {statistics} {[@dots{}] =} rangesearch (@dots{}, @var{name}, @var{value}) ## ## Find all neighbors within specified distance from input data. ## ## @code{@var{idx} = rangesearch (@var{X}, @var{Y}, @var{r})} returns all the ## points in @var{X} that are within distance @var{r} from the points in ## @var{Y}. ## @var{X} must be an @math{N*P} numeric matrix of input data, where rows ## correspond to observations and columns correspond to features or variables. ## @var{Y} is an @math{M*P} numeric matrix with query points, which must have ## the same numbers of column as @var{X}. @var{r} must be a nonnegative scalar ## value. @var{idx} is an @math{M*1} cell array, where @math{M} is the number ## of observations in @var{Y}. The vector @qcode{@var{Idx}@{j@}} contains the ## indices of observations (rows) in @var{X} whose distances to ## @qcode{@var{Y}(j,:)} are not greater than @var{r}. ## ## @code{[@var{idx}, @var{D}] = rangesearch (@var{X}, @var{Y}, @var{r})} also ## returns the distances, @var{D}, which correspond to the points in @var{X} ## that are within distance @var{r} from the points in @var{Y}. @var{D} is an ## @math{M*1} cell array, where @math{M} is the number of observations in ## @var{Y}. The vector @qcode{@var{D}@{j@}} contains the distances of ## observations (rows) in @var{X} whose distances to @qcode{@var{Y}(j,:)} are ## not greater than @var{r}. ## ## Additional parameters can be specified by @qcode{Name-Value} pair arguments. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'P'} @tab is the Minkowski distance exponent and it must be ## a positive scalar. This argument is only valid when the selected distance ## metric is @qcode{'minkowski'}. By default it is 2. An exponent of ## @code{Inf} gives the Chebychev distance. ## ## @item @qcode{'Scale'} @tab is the scale parameter for the standardized ## Euclidean distance and it must be a nonnegative numeric vector of equal ## length to the number of columns in @var{X}. This argument is only valid when ## the selected distance metric is @qcode{'seuclidean'}, in which case each ## coordinate of @var{X} is scaled by the corresponding element of ## @qcode{'scale'}, as is each query point in @var{Y}. By default, the scale ## parameter is the standard deviation of each coordinate in @var{X}. ## ## @item @qcode{'Cov'} @tab is the covariance matrix for computing the ## mahalanobis distance and it must be a positive definite matrix matching the ## the number of columns in @var{X}. This argument is only valid when the ## selected distance metric is @qcode{'mahalanobis'}. ## ## @item @qcode{'BucketSize'} @tab is the maximum number of data points in ## the leaf node of the Kd-tree and it must be a positive integer. This ## argument is only valid when the selected search method is @qcode{'kdtree'}. ## ## @item @qcode{'SortIndices'} @tab is a boolean flag to sort the returned ## indices in ascending order by distance and it is @qcode{true} by default. ## When the selected search method is @qcode{'exhaustive'} or the ## @qcode{'IncludeTies'} flag is true, @code{rangesearch} always sorts the ## returned indices. ## ## @item @qcode{'Distance'} @tab is the distance metric used by ## @code{rangesearch} as specified below: ## @end multitable ## ## @multitable @columnfractions 0.2 0.75 ## @item @qcode{'euclidean'} @tab Euclidean distance. ## @item @qcode{'seuclidean'} @tab standardized Euclidean distance. Each ## coordinate difference between the rows in @var{X} and the query matrix ## @var{Y} is scaled by dividing by the corresponding element of the standard ## deviation computed from @var{X}. To specify a different scaling, use the ## @qcode{'Scale'} name-value argument. ## @item @qcode{'cityblock'} @tab City block distance. ## @item @qcode{'chebychev'} @tab Chebychev distance (maximum coordinate ## difference). A difference of @code{Inf - Inf} is @code{NaN}, and so is the ## distance, as for every other metric; MATLAB passes over it. ## @item @qcode{'minkowski'} @tab Minkowski distance. The default exponent ## is 2. To specify a different exponent, use the @qcode{'P'} name-value ## argument. ## @item @qcode{'mahalanobis'} @tab Mahalanobis distance, computed using a ## positive definite covariance matrix. To change the value of the covariance ## matrix, use the @qcode{'Cov'} name-value argument. ## @item @qcode{'cosine'} @tab Cosine distance. ## @item @qcode{'correlation'} @tab One minus the sample linear correlation ## between observations (treated as sequences of values). ## @item @qcode{'spearman'} @tab One minus the sample Spearman's rank ## correlation between observations (treated as sequences of values). ## @item @qcode{'hamming'} @tab Hamming distance, which is the percentage ## of coordinates that differ. ## @item @qcode{'jaccard'} @tab One minus the Jaccard coefficient, which is ## the percentage of nonzero coordinates that differ. ## @item @var{@@distfun} @tab Custom distance function handle. A distance ## function of the form @code{function @var{D2} = distfun (@var{XI}, @var{YI})}, ## where @var{XI} is a @math{1*P} vector containing a single observation in ## @math{P}-dimensional space, @var{YI} is an @math{N*P} matrix containing an ## arbitrary number of observations in the same @math{P}-dimensional space, and ## @var{D2} is an @math{N*P} vector of distances, where @qcode{(@var{D2}k)} is ## the distance between observations @var{XI} and @qcode{(@var{YI}k,:)}. ## @end multitable ## ## @multitable @columnfractions 0.18 0.8 ## @item @qcode{'NSMethod'} @tab is the nearest neighbor search method used ## by @code{rangesearch} as specified below. ## @end multitable ## ## @multitable @columnfractions 0.2 0.75 ## @item @qcode{'kdtree'} @tab Creates and uses a Kd-tree to find nearest ## neighbors. @qcode{'kdtree'} is the default value when the number of columns ## in @var{X} is less than or equal to 10, @var{X} is not sparse, and the ## distance metric is @qcode{'euclidean'}, @qcode{'cityblock'}, ## @qcode{'manhattan'}, @qcode{'chebychev'}, or @qcode{'minkowski'}. Otherwise, ## the default value is @qcode{'exhaustive'}. This argument is only valid when ## the distance metric is one of the four aforementioned metrics. ## @item @qcode{'exhaustive'} @tab Uses the exhaustive search algorithm by ## computing the distance values from all the points in @var{X} to each point in ## @var{Y}. ## @end multitable ## ## @seealso{knnsearch, pdist2} ## @end deftypefn function [idx, dist] = rangesearch (X, Y, r, varargin) ## Check input data if (nargin < 3) error ("rangesearch: too few input arguments."); endif if (size (X, 2) != size (Y, 2)) error ("rangesearch: number of columns in X and Y must match."); endif if (! isscalar (r) || ! isnumeric (r) || r < 0) error ("rangesearch: radius r must be a nonnegative scalar."); endif ## Add default values P = 2; # Exponent for Minkowski distance S = []; # Scale for the standardized Euclidean distance C = []; # Covariance matrix for Mahalanobis distance BS = 50; # Maximum number of points per leaf node for Kd-tree SI = true; # Sort returned indices according to distance Distance = 'euclidean'; # Distance metric to be used NSMethod = []; # Nearest neighbor search method DistParameter = []; # Distance parameter for pdist2 ## Parse additional parameters in Name/Value pairs PSC = 0; PG = false; while (numel (varargin) > 0) switch (tolower (varargin{1})) case 'p' P = varargin{2}; PSC += 1; PG = true; case 'scale' S = varargin{2}; PSC += 1; case 'cov' C = varargin{2}; PSC += 1; case 'bucketsize' BS = varargin{2}; case 'sortindices' SI = varargin{2}; case 'distance' Distance = varargin{2}; ## 'manhattan' is this package's documented alias of 'cityblock' and ## is resolved here, so every search path takes it. MATLAB has no ## such name and refuses it. if (ischar (Distance) && strcmpi (Distance, 'manhattan')) Distance = 'cityblock'; endif case 'nsmethod' NSMethod = varargin{2}; otherwise error ("rangesearch: invalid NAME in optional pairs of arguments."); endswitch varargin(1:2) = []; endwhile ## Check input parameters if (PSC > 1) error ("rangesearch: only a single distance parameter can be defined."); endif if (! (isscalar (P) && isnumeric (P) && P > 0)) error ("rangesearch: invalid value of Minkowski Exponent."); endif if (PG && ! (ischar (Distance) && strcmpi (Distance, 'minkowski'))) error ("rangesearch: 'P' applies only to the minkowski metric."); endif if (! isempty (S)) if (any (S) < 0 || numel (S) != columns (X) || ! strcmpi (Distance, 'seuclidean')) error ("rangesearch: invalid value in Scale or the size of Scale."); endif endif if (! isempty (C)) if (! strcmp (Distance, 'mahalanobis') || ! ismatrix (C) || ! isnumeric (C)) error (strcat ("rangesearch: invalid value in Cov, Cov can only", ... " be given for mahalanobis distance.")); endif endif if (! isscalar (BS) || ! isnumeric (BS) || BS <= 0 || fix (BS) != BS) error ("rangesearch: invalid value of bucketsize."); endif ## Select the appropriate distance parameter if (strcmpi (Distance, 'minkowski')) DistParameter = P; ## A Minkowski exponent of Inf is the Chebychev distance, its limit, which ## every search below computes directly if (P == Inf) Distance = 'chebychev'; DistParameter = []; endif elseif (strcmpi (Distance, 'seuclidean')) DistParameter = S; elseif (strcmpi (Distance, 'mahalanobis')) DistParameter = C; endif ## Check NSMethod and set kdtree as default if the conditions match if (isempty (NSMethod)) ## Set default method 'kdtree' if conditions are satisfied; if (! issparse (X) && (columns (X) <= 10) && (strcmpi (Distance, 'euclidean') || strcmpi (Distance, 'cityblock') || strcmpi (Distance, 'minkowski') || strcmpi (Distance, 'chebychev'))) NSMethod = 'kdtree'; else NSMethod = 'exhaustive'; endif else ## Disallow kdtree with custom distance functions if (strcmpi (NSMethod, 'kdtree') && isa (Distance, 'function_handle')) error (strcat ("rangesearch: 'kdtree' cannot be used", ... " with custom distance functions.")); endif ## Check if kdtree can be used if (strcmpi (NSMethod, 'kdtree') && ! (strcmpi (Distance, 'euclidean') || strcmpi (Distance, 'cityblock') || strcmpi (Distance, 'minkowski') || strcmpi (Distance, 'chebychev'))) error (strcat ("rangesearch: 'kdtree' cannot be used", ... " with the given distance metric.")); endif endif ## Check for NSMethod if (strcmpi (NSMethod, 'kdtree')) ## Build kdtree and search the query point kdtree = __build_kdtree__ (1:size (X,1), 0, X, BS); ## Return all neighbors as cell dist = cell (rows (Y), 1); idx = cell (rows (Y), 1); for i = 1:rows (Y) [temp_idx, temp_D] = __search_kdtree__ (kdtree, Y(i,:), Inf, X, ... Distance, DistParameter, true, r); if (SI) [sorted_D, sort_idx] = sort (temp_D); idx{i} = temp_idx(sort_idx)(:).'; dist{i} = sorted_D(:).'; else idx{i} = temp_idx(:).'; dist{i} = temp_D(:).'; endif endfor else ## Calculate all distances dist = cell (rows (Y), 1); idx = cell (rows (Y), 1); for i = 1:rows (Y) D = pdist2 (X, Y(i,:), Distance, DistParameter); Didx_row = find (D <= r)'; Dist_row = D(Didx_row)'; if (SI) [S, I] = sort (Dist_row); Dist_row = Dist_row(I); Didx_row = Didx_row(I); endif dist{i} = Dist_row; idx{i} = Didx_row; endfor endif endfunction %!demo %! ## Generate 100 random 2D points from each of five distinct multivariate %! ## normal distributions that form five separate classes %! rng (42); %! N = 100; %! d = 10; %! X1 = mvnrnd (d * [0, 0], eye (2), N); %! X2 = mvnrnd (d * [1, 1], eye (2), N); %! X3 = mvnrnd (d * [-1, -1], eye (2), N); %! X4 = mvnrnd (d * [1, -1], eye (2), N); %! X5 = mvnrnd (d * [-1, 1], eye (2), N); %! X = [X1; X2; X3; X4; X5]; %! %! ## For each point in X, find the points in X that are within a radius d %! ## away from the points in X. %! Idx = rangesearch (X, X, d, 'NSMethod', 'exhaustive'); %! %! ## Select the first point in X (corresponding to the first class) and find %! ## its nearest neighbors within the radius d. Display these points in %! ## one color and the remaining points in a different color. %! x = X(1,:); %! nearestPoints = X(Idx{1},:); %! nonNearestIdx = true (size (X, 1), 1); %! nonNearestIdx(Idx{1}) = false; %! %! scatter (X(nonNearestIdx,1), X(nonNearestIdx,2)) %! hold on %! scatter (nearestPoints(:,1),nearestPoints(:,2)) %! scatter (x(1), x(2), 'black', 'filled') %! hold off %! %! ## Select the last point in X (corresponding to the fifth class) and find %! ## its nearest neighbors within the radius d. Display these points in %! ## one color and the remaining points in a different color. %! x = X(end,:); %! nearestPoints = X(Idx{end},:); %! nonNearestIdx = true (size (X, 1), 1); %! nonNearestIdx(Idx{end}) = false; %! %! figure %! scatter (X(nonNearestIdx,1), X(nonNearestIdx,2)) %! hold on %! scatter (nearestPoints(:,1),nearestPoints(:,2)) %! scatter (x(1), x(2), 'black', 'filled') %! hold off ## Test output %!shared x, y, X, Y %! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1]; %! y = [2, 3, 4; 1, 4, 3]; %! X = [1, 2, 3, 4; 2, 3, 4, 5; 3, 4, 5, 6]; %! Y = [1, 2, 2, 3; 2, 3, 3, 4]; %!test %! [idx, D] = rangesearch (x, y, 4); %! assert_equal (idx, {[1, 4, 2]; [1, 4]}); %! assert_equal (D, {[1.7321, 3.3166, 3.4641]; [2, 3.4641]}, 1e-4); %!test %! [idx, D] = rangesearch (x, y, 4, 'NSMethod', 'exhaustive'); %! assert_equal (idx, {[1, 4, 2]; [1, 4]}); %! assert_equal (D, {[1.7321, 3.3166, 3.4641]; [2, 3.4641]}, 1e-4); %!test %! [idx, D] = rangesearch (x, y, 4, 'NSMethod', 'kdtree'); %! assert_equal (idx, {[1, 4, 2]; [1, 4]}); %! assert_equal (D, {[1.7321, 3.3166, 3.4641]; [2, 3.4641]}, 1e-4); %!test %! [idx, D] = rangesearch (x, y, 4, 'SortIndices', true); %! assert_equal (idx, {[1, 4, 2]; [1, 4]}); %! assert_equal (D, {[1.7321, 3.3166, 3.4641]; [2, 3.4641]}, 1e-4); %!test %! [idx, D] = rangesearch (x, y, 4, 'SortIndices', false); %! assert_equal (idx, {[1, 2, 4]; [1, 4]}); %! assert_equal (D, {[1.7321, 3.4641, 3.3166]; [2, 3.4641]}, 1e-4); %!test %! [idx, D] = rangesearch (x, y, 4, 'NSMethod', 'exhaustive', ... %! 'SortIndices', false); %! assert_equal (idx, {[1, 2, 4]; [1, 4]}); %! assert_equal (D, {[1.7321, 3.4641, 3.3166]; [2, 3.4641]}, 1e-4); %!test %! eucldist = @(v,m) sqrt (sumsq (repmat (v,rows (m),1)-m,2)); %! [idx, D] = rangesearch (x, y, 4, 'Distance', eucldist); %! assert_equal (idx, {[1, 4, 2]; [1, 4]}); %! assert_equal (D, {[1.7321, 3.3166, 3.4641]; [2, 3.4641]}, 1e-4); %!test %! eucldist = @(v,m) sqrt (sumsq (repmat (v,rows (m),1)-m,2)); %! [idx, D] = rangesearch (x, y, 4, 'Distance', eucldist, ... %! 'NSMethod', 'exhaustive'); %! assert_equal (idx, {[1, 4, 2]; [1, 4]}); %! assert_equal (D, {[1.7321, 3.3166, 3.4641]; [2, 3.4641]}, 1e-4); %!test %! [idx, D] = rangesearch (x, y, 1.5, 'Distance', 'seuclidean', ... %! 'NSMethod', 'exhaustive'); %! assert_equal (idx, {[1, 4, 2]; [1, 4]}); %! assert_equal (D, {[0.6024, 1.0079, 1.2047]; [0.6963, 1.2047]}, 1e-4); %!test %! [idx, D] = rangesearch (x, y, 1.5, 'Distance', 'seuclidean', ... %! 'NSMethod', 'exhaustive', 'SortIndices', false); %! assert_equal (idx, {[1, 2, 4]; [1, 4]}); %! assert_equal (D, {[0.6024, 1.2047, 1.0079]; [0.6963, 1.2047]}, 1e-4); %!test %! [idx, D] = rangesearch (X, Y, 4); %! assert_equal (idx, {[1, 2]; [1, 2, 3]}); %! assert_equal (D, {[1.4142, 3.1623]; [1.4142, 1.4142, 3.1623]}, 1e-4); %!test %! [idx, D] = rangesearch (X, Y, 2); %! assert_equal (idx, {[1]; [1, 2]}); %! assert_equal (D, {[1.4142]; [1.4142, 1.4142]}, 1e-4); %!test %! eucldist = @(v,m) sqrt (sumsq (repmat (v,rows (m),1)-m,2)); %! [idx, D] = rangesearch (X, Y, 4, 'Distance', eucldist); %! assert_equal (idx, {[1, 2]; [1, 2, 3]}); %! assert_equal (D, {[1.4142, 3.1623]; [1.4142, 1.4142, 3.1623]}, 1e-4); %!test %! [idx, D] = rangesearch (X, Y, 4, 'SortIndices', false); %! assert_equal (idx, {[1, 2]; [1, 2, 3]}); %! assert_equal (D, {[1.4142, 3.1623]; [1.4142, 1.4142, 3.1623]}, 1e-4); %!test %! [idx, D] = rangesearch (X, Y, 4, 'Distance', 'seuclidean', ... %! 'NSMethod', 'exhaustive'); %! assert_equal (idx, {[1, 2]; [1, 2, 3]}); %! assert_equal (D, {[1.4142, 3.1623]; [1.4142, 1.4142, 3.1623]}, 1e-4); %!test %! X = ones (10, 2); %! [idx, D] = rangesearch (X, X, 0.1, 'NSMethod', 'kdtree'); %! assert_equal (numel (idx), 10); %!test %! X = ones (3, 2); %! [idx, D] = rangesearch (X, X, 0.1, 'NSMethod', 'kdtree', 'BucketSize', 1); %! assert_equal (numel (idx), 3); %! assert_equal (cellfun (@numel, idx) == 3, [true; true; true]); %! assert_equal (idx{1}, [1, 2, 3]); %! assert_equal (idx{2}, [1, 2, 3]); %! assert_equal (idx{3}, [1, 2, 3]); %! assert_equal (D{1}, [0, 0, 0]); %! assert_equal (D{2}, [0, 0, 0]); %! assert_equal (D{3}, [0, 0, 0]); %!test %! [idx, D] = rangesearch (x, y, 4, 'NSMethod', 'kdtree', 'SortIndices', true); %! assert_equal (idx, {[1, 4, 2]; [1, 4]}); %! assert_equal (D, {[1.7321, 3.3166, 3.4641]; [2, 3.4641]}, 1e-4); %!test %! [idx, D] = rangesearch (x, y, 4, 'NSMethod', 'kdtree', 'SortIndices', false); %! assert_equal (idx, {[1, 2, 4]; [1, 4]}); %! assert_equal (D, {[1.7321, 3.4641, 3.3166]; [2, 3.4641]}, 1e-4); %!test %! ## A Minkowski exponent of Inf is the Chebyshev distance, as in MATLAB R2026b %! X = [1, 2; 4, 6; 0, 0]; %! [idx, D] = rangesearch (X(2:3,:), X(1,:), 4.5, 'Distance', 'minkowski', ... %! 'P', Inf, 'NSMethod', 'exhaustive'); %! assert_equal (idx, {[2, 1]}); %! assert_equal (D, {[2, 4]}); %!test %! ## and with the default kd-tree search, sorted or not, with ties %! X = [1, 2; 4, 6; 0, 0; 3, 1; 5, 5; 2, 3]; %! Y = [1, 1; 4, 4]; %! [idx, D] = rangesearch (X, Y, 2, 'Distance', 'minkowski', 'P', Inf); %! assert_equal (idx, {[1, 3, 4, 6]; [5, 2, 6]}); %! assert_equal (D, {[1, 1, 2, 2]; [1, 2, 2]}); %! [idx, D] = rangesearch (X, Y, 2, 'Distance', 'minkowski', 'P', Inf, ... %! 'SortIndices', false); %! assert_equal (idx, {[1, 3, 4, 6]; [2, 5, 6]}); %! assert_equal (D, {[1, 1, 2, 2]; [2, 1, 2]}); %!test %! ## A row holding a NaN is never within range, as in MATLAB R2024a %! Xn = [1, Inf; 4, 6; 0, Inf; NaN, 1; 2, 2]; %! [idx, D] = rangesearch (Xn, [1, 1], 5, 'Distance', 'chebychev'); %! assert_equal (idx, {[5, 2]}); %! assert_equal (D, {[1, 5]}); %!test %! ## Inf - Inf is NaN, so neither row sharing the query's Inf is in range, %! ## on both methods; MATLAB R2024a's exhaustive search returns rows 3 and 1 %! Xn = [1, Inf; 4, 6; 0, Inf; NaN, 1; 2, 2]; %! [idx, D] = rangesearch (Xn, [0, Inf], 5, 'Distance', 'chebychev'); %! assert_equal (idx, {zeros(1, 0)}); %! assert_equal (D, {zeros(1, 0)}); %!test %! Xn = [1, Inf; 4, 6; 0, Inf; NaN, 1; 2, 2]; %! [idx, D] = rangesearch (Xn, [0, Inf], 5, 'Distance', 'chebychev', ... %! 'NSMethod', 'exhaustive'); %! assert_equal (idx, {zeros(1, 0)}); %! assert_equal (D, {zeros(1, 0)}); ## Test input validation %!error rangesearch (1) %!error rangesearch (ones (4, 5)) %!error ... %! rangesearch (ones (4, 5), ones (4)) %!error ... %! rangesearch (ones (4, 5), ones (4), 1) %!error ... %! rangesearch (ones (4, 2), ones (3, 2), 1, 'Distance', 'euclidean', 'some', 'some') %!error ... %! rangesearch (ones (4, 5), ones (1, 5), 1, 'scale', ones (1, 5), 'P', 3) %!error ... %! rangesearch (ones (4, 5), ones (1, 5), 1, 'P', -2) %!error ... %! rangesearch (ones (4, 2), ones (1, 2), 1, 'Distance', 'minkowski', 'P', NaN) %!error ... %! rangesearch (ones (4, 2), ones (1, 2), 1, 'P', 3) %!error ... %! rangesearch (ones (4, 2), ones (1, 2), 1, 'P', 3, 'NSMethod', 'exhaustive') %!error ... %! rangesearch (ones (4, 5), ones (1, 5), 1, 'scale', ones (4,5), 'distance', 'euclidean') %!error ... %! rangesearch (ones (4, 5), ones (1, 5), 1, 'cov', ['some' 'some']) %!error ... %! rangesearch (ones (4, 5), ones (1, 5), 1, 'cov', ones (4,5), 'distance', 'euclidean') %!error ... %! rangesearch (ones (4, 5), ones (1, 5), 1, 'bucketsize', -1) %!error ... %! rangesearch (ones (4,2), ones (1,2), 1, 'BucketSize', 2.5) %!error ... %! rangesearch (ones (4, 5), ones (1, 5), 1, 'NSmethod', 'kdtree', 'distance', 'cosine') %!error ... %! rangesearch (ones (4, 5), ones (1, 5), 1, 'NSmethod', 'kdtree', 'distance', 'mahalanobis') %!error ... %! rangesearch (ones (4, 5), ones (1, 5), 1, 'NSmethod', 'kdtree', 'distance', 'correlation') %!error ... %! rangesearch (ones (4, 5), ones (1, 5), 1, 'NSmethod', 'kdtree', 'distance', 'seuclidean') %!error ... %! rangesearch (ones (4, 5), ones (1, 5), 1, 'NSmethod', 'kdtree', 'distance', 'spearman') %!error ... %! rangesearch (ones (4, 5), ones (1, 5), 1, 'NSmethod', 'kdtree', 'distance', 'hamming') %!error ... %! rangesearch (ones (4, 5), ones (1, 5), 1, 'NSmethod', 'kdtree', 'distance', 'jaccard') %!error ... %! rangesearch (ones (4,2), ones (1,2), 1, 'Distance', @(x,y) sqrt (sum ((x-y).^2)), 'NSMethod', 'kdtree') statistics-release-2.1.2/inst/Nearest_Neighbors/squareform.m000066400000000000000000000167631527311104300242560ustar00rootroot00000000000000## Copyright (C) 2015 Carnë Draug ## Copyright (C) 2026 Andreas Bertsatos ## Copyright (C) 2026 Avanish Salunke ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{zOut} =} squareform (@var{yIn}) ## @deftypefnx {statistics} {@var{yOut} =} squareform (@var{zIn}) ## @deftypefnx {statistics} {@var{zOut} =} squareform (@var{yIn}, @qcode{'tovector'}) ## @deftypefnx {statistics} {@var{yOut} =} squareform (@var{zIn}, @qcode{'tomatrix'}) ## ## Interchange between distance matrix and distance vector formats. ## ## Converts between a hollow (diagonal filled with zeros), square, and ## symmetric matrix and a vector of the lower triangular part. ## ## Its target application is the conversion of the vector returned by ## @code{pdist} into a distance matrix. It performs the opposite operation ## if input is a matrix. ## ## If @var{x} is a numeric or logical vector, its number of elements must fit ## into the triangular part of a matrix (main diagonal excluded). In other ## words, @code{numel (@var{x}) = @var{n} * (@var{n} - 1) / 2} for some integer ## @var{n}. The resulting matrix will be @var{n} by @var{n}. An empty vector ## is the distance vector of a single observation, so it returns the 1-by-1 ## distance matrix @code{0} and @code{squareform (pdist (@var{x}))} holds for a ## single row @var{x}. MATLAB returns an empty matrix instead. ## ## If @var{x} is a numeric or logical distance matrix, it must be square and the ## diagonal entries of @var{x} must all be zeros. If @var{x} is not symmetric, ## only the lower triangular part is used. ## ## The second argument is used to specify the output type in case the distance ## input is a scalar. Accepted values are @qcode{'tomatrix'} (or @qcode{'tom'}) ## and @qcode{'tovector'} (or @qcode{'tov'}). If not specified, it defaults to ## @qcode{'tomatrix'} otherwise. When @qcode{'tomatrix'} is specified, the ## distance input must be a vector. ## ## @seealso{pdist} ## @end deftypefn function y = squareform (x, method) if (nargin < 1 || nargin > 2) print_usage (); elseif (! isnumeric (x) && ! islogical (x)) error ("squareform: distance input must be either numeric or logical."); elseif (! ismatrix (x)) error ("squareform: distance input must be either a vector or a matrix."); endif if (nargin == 1) ## This is ambiguous when numel (x) == 1, but that's the whole reason ## why the "method" option exists. if (isvector (x)) method = 'tomatrix'; else method = 'tovector'; endif elseif (isstring (method)) if (! isscalar (method)) error (strcat ("squareform: second argument must be either", ... " a character vector or a string scalar.")); endif method = char (method); elseif (! ischar (method)) error (strcat ("squareform: second argument must be either", ... " a character vector or a string scalar.")); endif switch (tolower (method)) case {'tovector', 'tov'} if (! issquare (x)) error ("squareform: distance input is not a square matrix."); elseif (any (diag (x) != 0)) error ("squareform: distance input is not a hollow matrix."); endif y = vec (tril (x, -1, 'pack'), 2); case {'tomatrix', 'tom'} if (! isvector (x)) error (strcat ("squareform: distance input must be a vector to", ... " convert it to a distance matrix.")); endif ## the dimensions of y are the solution to the quadratic formula for: ## length (x) = (sy - 1) * (sy / 2) sy = (1 + sqrt (1 + 8 * numel (x))) / 2; if (fix (sy) != sy) error ("squareform: distance input vector cannot form a square matrix."); endif y = zeros (sy, class (x)); y(tril (true (sy), -1)) = x; # fill lower triangular part y += y.'; # and then the upper triangular part otherwise error ("squareform: invalid METHOD '%s'.", method); endswitch ## Force to logical (if applicable) if (isa (x, 'logical')) y = logical (y); endif endfunction %!shared v, m %! v = 1:6; %! m = [0, 1, 2, 3; 1, 0, 4, 5; 2, 4, 0, 6; 3, 5, 6, 0]; ## make sure that it can go both directions automatically %!test %!assert_equal (squareform (v), m) %!assert_equal (squareform (squareform (v)), v) %!assert_equal (squareform (m), v) ## treat row and column vectors equally %!test %!assert_equal (squareform (v'), m) ## handle 1 element input properly %!test %!assert_equal (squareform (1), [0 1;1 0]) %!assert_equal (squareform (1, 'tomatrix'), [0 1; 1 0]) %!assert_equal (squareform (0, 'tovector'), zeros (1, 0)) %!assert_equal (squareform (zeros (1, 0)), 0) %!assert_equal (squareform (zeros (0, 1), 'tomatrix'), 0) %!assert_equal (squareform (pdist ([1, 2])), 0) ## confirm that it respects input class %!test %! for c = {@single, @double, @uint8, @uint16, @uint32, @uint64, @logical} %! f = c{1}; %! assert_equal (squareform (f(v)), f(m)) %! assert_equal (squareform (f(m)), f(v)) %! endfor ## test logical inputs. %!test %! v_log = [true, false, true]; %! m_log = [false, true, false; true, false, true; false, true, false]; %! assert_equal (squareform (v_log), m_log); %! assert_equal (squareform (m_log), v_log); ## test partial string matching and case insensitivity %!assert_equal (squareform (v, 'tom'), m); %!assert_equal (squareform (m, 'tov'), v); %!assert_equal (squareform (v, 'TOMATRIX'), m); %!assert_equal (squareform (v, string ('tomatrix')), m); %!assert_equal (squareform (m, string ('tovector')), v); ## input validations %!error ... %! squareform ('string') %!error ... %! squareform ({1, 2, 3}) %!error ... %! squareform ([1, 2, 3; 4, 5, 6], 'tovector') %!error ... %! squareform (eye (3), 'tovector') %!error ... %! squareform ([1, 2, 3; 4, 5, 6], string ({'tomatrix', 'tomatrix'})) %!error ... %! squareform ([1, 2, 3; 4, 5, 6], true) %!error ... %! squareform ([1, 2, 3, 4], 'tomatrix') %!error ... %! squareform ([1, 2, 3; 4, 5, 6], 'tomatrix') %!error ... %! squareform (zeros (2, 2), 'tomatrix') %!error ... %! squareform ([], 'tomatrix') %!error squareform ([1, 2, 3], 'invalid') statistics-release-2.1.2/inst/PKG_ADD000066400000000000000000000017661527311104300173240ustar00rootroot00000000000000a1_e324kporit985_itogj3_dirlist = ... {'Anomaly_Detection', 'Clustering', 'Data_Manipulation', 'datasets', ... 'demos', 'Descriptive_Statistics', 'Dimensionality_Reduction', ... 'Distribution_Classes', 'Distribution_Fitting', ... 'Distribution_Functions', 'Distribution_Statistics', ... 'Distribution_Wrappers', 'Experimental_Design', ... 'Hypothesis_Testing', 'Markov_Models', 'Model_Evaluation', ... 'Nearest_Neighbors', 'Plotting', 'Random_Sampling', ... 'Regression', 'Machine_Learning'}; d_2seRTE546_oyi_795jg09_dirname = fileparts (canonicalize_file_name ... (mfilename ("fullpath"))); for iiII123DRT_idx = 1:length (a1_e324kporit985_itogj3_dirlist) addpath (fullfile (d_2seRTE546_oyi_795jg09_dirname, ... a1_e324kporit985_itogj3_dirlist{iiII123DRT_idx})); endfor clear a1_e324kporit985_itogj3_dirlist clear d_2seRTE546_oyi_795jg09_dirname iiII123DRT_idx warning ("off", "Octave:data-file-in-path") statistics-release-2.1.2/inst/PKG_DEL000066400000000000000000000020111527311104300173200ustar00rootroot00000000000000clear -f editDistance libsvmread libsvmwrite svmpredict svmtrain a1_e324kporit985_itogj3_dirlist = ... {'Anomaly_Detection', 'Clustering', 'Data_Manipulation', 'datasets', ... 'demos', 'Descriptive_Statistics', 'Dimensionality_Reduction', ... 'Distribution_Classes', 'Distribution_Fitting', ... 'Distribution_Functions', 'Distribution_Statistics', ... 'Distribution_Wrappers', 'Experimental_Design', ... 'Hypothesis_Testing', 'Markov_Models', 'Model_Evaluation', ... 'Nearest_Neighbors', 'Plotting', 'Random_Sampling', ... 'Regression', 'Machine_Learning'}; d_2seRTE546_oyi_795jg09_dirname = fileparts (canonicalize_file_name ... (mfilename ("fullpath"))); for iiII123DRT_idx = 1:length (a1_e324kporit985_itogj3_dirlist) rmpath (fullfile (d_2seRTE546_oyi_795jg09_dirname, ... a1_e324kporit985_itogj3_dirlist{iiII123DRT_idx})); endfor clear a1_e324kporit985_itogj3_dirlist clear d_2seRTE546_oyi_795jg09_dirname iiII123DRT_idx statistics-release-2.1.2/inst/Plotting/000077500000000000000000000000001527311104300200765ustar00rootroot00000000000000statistics-release-2.1.2/inst/Plotting/+stats/000077500000000000000000000000001527311104300213075ustar00rootroot00000000000000statistics-release-2.1.2/inst/Plotting/+stats/+chart/000077500000000000000000000000001527311104300224635ustar00rootroot00000000000000statistics-release-2.1.2/inst/Plotting/+stats/+chart/BoxChart.m000066400000000000000000001211011527311104300243470ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftp {statistics} stats.chart.BoxChart ## ## A box chart, as @code{boxchart} draws it. ## ## A @code{stats.chart.BoxChart} holds the data a box chart is drawn from and ## the choices it is drawn with, and redraws itself whenever one of them is ## set. It is what @code{boxchart} returns, and the documented way to reach ## it; one is returned per colour group where @qcode{'GroupByColor'} names ## one. ## ## The data comes either from vectors or from a table, never from both, and ## the object says which through @code{XDataMode} and @code{YDataMode}. ## Where the data came from vectors these read @qcode{'manual'} and ## @code{XVariable} and @code{YVariable} are empty; where it came from a ## table they read @qcode{'auto'} and naming a variable is what chooses the ## column. Setting @code{YData} on an object holding a table is refused, and ## so is naming a variable on one holding vectors, as MATLAB R2026a refuses ## them. Assigning @code{SourceTable} again re-reads the columns, so a table ## changed after the chart was drawn is picked up. ## ## The box spans the lower and upper quartiles with the median across it, the ## whiskers reach the furthest values within one and a half interquartile ## ranges of the box, and whatever lies beyond is drawn as a marker. These ## are the quartiles @code{quantile} computes and the rule ## @code{isoutlier} applies with @qcode{'quartiles'}. ## ## MATLAB's object carries the properties every graphics object carries, ## @code{Annotation} and @code{DataTipTemplate} among them. This one is a ## handle class drawing with ordinary primitives rather than a graphics ## object of its own, so it carries what belongs to a box chart and leaves ## the rest to the primitives it draws. ## ## @seealso{boxchart, swarmchart, boxplot} ## @end deftp classdef BoxChart < handle properties (Access = public) ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} BoxWidth ## ## Width of each box, in the units of the horizontal axis. The default ## is 0.5. ## ## @end deftp BoxWidth = 0.5; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} BoxFaceColor ## ## Colour the boxes are filled with, as an RGB triplet. Setting it puts ## @code{BoxFaceColorMode} at @qcode{'manual'}. ## ## @end deftp BoxFaceColor = [0, 0.447, 0.741]; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} BoxFaceAlpha ## ## How opaque the box fill is, from 0 to 1. The default is 0.2. ## ## @end deftp BoxFaceAlpha = 0.2; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} BoxEdgeColor ## ## Colour of the box outline. Setting it puts ## @code{BoxEdgeColorMode} at @qcode{'manual'}. ## ## @end deftp BoxEdgeColor = [0, 0.447, 0.741]; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} BoxMedianLineColor ## ## Colour of the line marking the median. Setting it puts ## @code{BoxMedianLineColorMode} at @qcode{'manual'}. ## ## @end deftp BoxMedianLineColor = [0, 0.447, 0.741]; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} WhiskerLineColor ## ## Colour of the whiskers and their caps. ## ## @end deftp WhiskerLineColor = [0, 0.447, 0.741]; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} WhiskerLineStyle ## ## Line style of the whiskers: @qcode{'-'} by default, or @qcode{'--'}, ## @qcode{':'}, @qcode{'-.'} or @qcode{'none'}. ## ## @end deftp WhiskerLineStyle = '-'; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} CapWidth ## ## Width of the cap at the end of each whisker, in the units of the ## horizontal axis. The default is 0.25. ## ## @end deftp CapWidth = 0.25; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} LineWidth ## ## Width of the box and whisker lines. The default is 1. ## ## @end deftp LineWidth = 1; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} MarkerStyle ## ## Marker an outlier is drawn with, @qcode{'o'} by default, or ## @qcode{'none'} to draw none. ## ## @end deftp MarkerStyle = 'o'; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} MarkerSize ## ## Size of an outlier marker. The default is 6. ## ## @end deftp MarkerSize = 6; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} MarkerColor ## ## Colour of an outlier marker. Setting it puts ## @code{MarkerColorMode} at @qcode{'manual'}. ## ## @end deftp MarkerColor = [0, 0.447, 0.741]; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} Notch ## ## Whether the box is notched about its median, @qcode{'off'} by default. ## A notch spans the median plus and minus ## @math{1.57 IQR / sqrt (n)}, so two boxes whose notches do not overlap ## have medians that differ at roughly the five per cent level. ## ## @end deftp Notch = 'off'; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} JitterOutliers ## ## Whether outlier markers are spread across the width of the box rather ## than drawn in a line, @qcode{'off'} by default. ## ## @end deftp JitterOutliers = 'off'; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} Orientation ## ## Whether the boxes stand @qcode{'vertical'}, the default, or lie ## @qcode{'horizontal'}. ## ## @end deftp Orientation = 'vertical'; endproperties properties (Access = public) ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} XData ## ## Where each observation's box stands, one value per observation: a ## grouping variable, numeric or categorical. Where no grouping was ## given every observation shares one position and this is a ## @code{categorical} of one level. Setting it is refused while ## @code{XVariable} names a column, the data coming from the table ## instead. ## ## @end deftp XData = []; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} YData ## ## The observations themselves, one value per observation. Setting it is ## refused while @code{YVariable} names a column. ## ## @end deftp YData = []; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} SourceTable ## ## The table the data is read from, empty where it came from vectors. ## Assigning it again re-reads the columns @code{XVariable} and ## @code{YVariable} name, so a table changed after the chart was drawn is ## picked up. ## ## @end deftp SourceTable = []; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} XVariable ## ## The column of @code{SourceTable} the boxes are grouped by, empty where ## none was named. Naming one is refused while the data came from ## vectors. ## ## @end deftp XVariable = []; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} YVariable ## ## The column of @code{SourceTable} holding the observations. Naming one ## is refused while the data came from vectors. ## ## @end deftp YVariable = []; endproperties properties (GetAccess = public, SetAccess = private) ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} XDataMode ## ## Where @code{XData} came from: @qcode{'manual'} where it was given as a ## grouping vector, and @qcode{'auto'} where the chart derived it, from a ## table or from there being no grouping at all. This property is ## read-only, where MATLAB allows it to be assigned. ## ## @end deftp XDataMode = 'auto'; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} YDataMode ## ## Where @code{YData} came from: @qcode{'manual'} where it was given as a ## vector and @qcode{'auto'} where it was read from a table. This ## property is read-only, where MATLAB allows it to be assigned. ## ## @end deftp YDataMode = 'manual'; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} BoxFaceColorMode ## ## Whether @code{BoxFaceColor} was chosen, @qcode{'manual'}, or left to ## the chart, @qcode{'auto'}. This property is read-only. ## ## @end deftp BoxFaceColorMode = 'auto'; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} BoxEdgeColorMode ## ## Whether @code{BoxEdgeColor} was chosen. This property is read-only. ## ## @end deftp BoxEdgeColorMode = 'auto'; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} BoxMedianLineColorMode ## ## Whether @code{BoxMedianLineColor} was chosen. This property is ## read-only. ## ## @end deftp BoxMedianLineColorMode = 'auto'; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} MarkerColorMode ## ## Whether @code{MarkerColor} was chosen. This property is read-only. ## ## @end deftp MarkerColorMode = 'auto'; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} CapWidthMode ## ## Whether @code{CapWidth} was chosen. This property is read-only. ## ## @end deftp CapWidthMode = 'auto'; ## -*- texinfo -*- ## @deftp {stats.chart.BoxChart} {property} Parent ## ## The axes the chart is drawn into. This property is read-only. ## ## @end deftp Parent = []; endproperties properties (Access = private, Hidden) Handles_ = []; # every primitive the last drawing made Drawn_ = false; # false while the constructor is still filling fields endproperties methods (Hidden) function disp (this) printf ('\n BoxChart with properties:\n\n'); printf ('%22s: %d\n', 'observations', numel (this.YData)); printf ('%22s: %d\n', 'boxes', numel (bcPositions (this))); printf ('%22s: %s\n', 'Orientation', this.Orientation); printf ('%22s: %s\n', 'Notch', this.Notch); if (! isempty (this.YVariable)) printf ('%22s: %s\n', 'YVariable', this.YVariable); endif printf ('\n'); endfunction function display (this) disp (this); endfunction endmethods methods (Hidden) ## The data comes from vectors or from a table, never from both, and ## R2026a refuses a mixture in both directions. Measured 2026-09-17. ## The rules below bite only once the chart is drawn: the constructor ## fills both sides through these same setters and must not trip them. function set.YData (this, val) if (this.Drawn_ && ! isempty (this.YVariable)) error (strcat ("stats.chart.BoxChart: setting 'YData' while", ... " 'YVariable' names a column is not supported.")); endif this.YData = bcCheckData (val, 'YData'); this.YDataMode = 'manual'; redraw (this); endfunction function set.XData (this, val) if (this.Drawn_ && ! isempty (this.XVariable)) error (strcat ("stats.chart.BoxChart: setting 'XData' while", ... " 'XVariable' names a column is not supported.")); endif this.XData = val; if (! isempty (val)) this.XDataMode = 'manual'; endif redraw (this); endfunction function set.YVariable (this, val) ## Clearing a name is always allowed; it is naming a column on data ## that came from vectors that R2026a refuses if (isempty (val)) this.YVariable = []; return; endif if (this.Drawn_ && strcmp (this.YDataMode, 'manual') && ! isempty (this.YData)) error (strcat ("stats.chart.BoxChart: setting 'YVariable' while", ... " 'YDataMode' is 'manual' is not supported.")); endif this.YVariable = bcCheckName (val, 'YVariable'); reread (this); endfunction function set.XVariable (this, val) if (isempty (val)) this.XVariable = []; return; endif if (this.Drawn_ && strcmp (this.XDataMode, 'manual') && ! isempty (this.XData)) error (strcat ("stats.chart.BoxChart: setting 'XVariable' while", ... " 'XDataMode' is 'manual' is not supported.")); endif this.XVariable = bcCheckName (val, 'XVariable'); reread (this); endfunction ## Assigning the table again re-reads the columns, so a table changed ## after the chart was drawn is picked up, as R2026a picks it up function set.SourceTable (this, val) if (! (isempty (val) || istable (val))) error ("stats.chart.BoxChart: 'SourceTable' must be a table."); endif this.SourceTable = val; reread (this); endfunction function set.BoxWidth (this, val) this.BoxWidth = bcCheckPositive (val, 'BoxWidth'); redraw (this); endfunction function set.CapWidth (this, val) this.CapWidth = bcCheckPositive (val, 'CapWidth'); this.CapWidthMode = 'manual'; redraw (this); endfunction function set.LineWidth (this, val) this.LineWidth = bcCheckPositive (val, 'LineWidth'); redraw (this); endfunction function set.MarkerSize (this, val) this.MarkerSize = bcCheckPositive (val, 'MarkerSize'); redraw (this); endfunction function set.BoxFaceAlpha (this, val) this.BoxFaceAlpha = bcCheckUnit (val, 'BoxFaceAlpha'); redraw (this); endfunction function set.BoxFaceColor (this, val) this.BoxFaceColor = bcCheckColor (val, 'BoxFaceColor'); this.BoxFaceColorMode = 'manual'; redraw (this); endfunction function set.BoxEdgeColor (this, val) this.BoxEdgeColor = bcCheckColor (val, 'BoxEdgeColor'); this.BoxEdgeColorMode = 'manual'; redraw (this); endfunction function set.BoxMedianLineColor (this, val) this.BoxMedianLineColor = bcCheckColor (val, 'BoxMedianLineColor'); this.BoxMedianLineColorMode = 'manual'; redraw (this); endfunction function set.MarkerColor (this, val) this.MarkerColor = bcCheckColor (val, 'MarkerColor'); this.MarkerColorMode = 'manual'; redraw (this); endfunction function set.WhiskerLineColor (this, val) this.WhiskerLineColor = bcCheckColor (val, 'WhiskerLineColor'); redraw (this); endfunction function set.WhiskerLineStyle (this, val) this.WhiskerLineStyle = bcCheckOneOf (val, ... {'-', '--', ':', '-.', 'none'}, ... 'WhiskerLineStyle'); redraw (this); endfunction function set.MarkerStyle (this, val) this.MarkerStyle = bcCheckOneOf (val, ... {'o', '+', '*', '.', 'x', 's', 'd', '^', 'v', ... '>', '<', 'p', 'h', 'none'}, 'MarkerStyle'); redraw (this); endfunction function set.Notch (this, val) this.Notch = bcCheckOneOf (val, {'on', 'off'}, 'Notch'); redraw (this); endfunction function set.JitterOutliers (this, val) this.JitterOutliers = bcCheckOneOf (val, {'on', 'off'}, ... 'JitterOutliers'); redraw (this); endfunction function set.Orientation (this, val) this.Orientation = bcCheckOneOf (val, {'vertical', 'horizontal'}, ... 'Orientation'); redraw (this); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {stats.chart.BoxChart} {@var{obj} =} stats.chart.BoxChart (@var{hax}, @var{spec}, @var{args}) ## ## Create a @code{stats.chart.BoxChart} object. ## ## @var{hax} is the axes to draw into, or empty for the current axes, ## which is resolved only once every value has been accepted. @var{spec} ## is a structure carrying ## the data as @code{boxchart} resolved it, with the fields ## @qcode{XData}, @qcode{YData}, @qcode{SourceTable}, @qcode{XVariable}, ## @qcode{YVariable} and @qcode{XDataMode}, and @var{args} the ## name-value pairs left to set. The documented way to reach this ## constructor is @code{boxchart}. ## ## @seealso{boxchart} ## @end deftypefn function this = BoxChart (hax, spec, args) if (nargin < 2) error ("stats.chart.BoxChart: too few input arguments."); endif if (nargin < 3) args = {}; endif this.XData = spec.XData; this.YData = spec.YData; this.SourceTable = spec.SourceTable; this.XVariable = spec.XVariable; this.YVariable = spec.YVariable; this.XDataMode = spec.XDataMode; this.YDataMode = spec.YDataMode; ## Whatever was named is set now, so that one drawing covers them all for k = 1:2:numel (args) name = args{k}; value = args{k+1}; this.(name) = value; endfor ## The axes comes last, so a rejected value leaves no figure behind if (isempty (hax)) hax = gca (); endif this.Parent = hax; this.Drawn_ = true; redraw (this); endfunction endmethods methods (Access = private) ## Draw the chart from scratch, discarding whatever was drawn before. function redraw (this) if (! this.Drawn_) return; endif h = this.Handles_; for k = 1:numel (h) if (ishghandle (h(k))) delete (h(k)); endif endfor this.Handles_ = []; if (isempty (this.YData)) return; endif ax = this.Parent; held = ishold (ax); hold (ax, 'on'); unwind_protect pos = bcPositions (this); for k = 1:numel (pos) drawOneBox (this, ax, pos(k), k); endfor unwind_protect_cleanup if (! held) hold (ax, 'off'); endif end_unwind_protect endfunction ## Draw the box, whiskers, caps, median and outliers of one group. function drawOneBox (this, ax, at, k) y = bcGroupData (this, k); y = y(! isnan (y)); if (isempty (y)) return; endif q1 = quantile (y, 0.25); q3 = quantile (y, 0.75); md = median (y); sp = q3 - q1; out = isoutlier (y, 'quartiles'); inside = y(! out); if (isempty (inside)) lo = md; hi = md; else lo = min (inside); hi = max (inside); endif hw = this.BoxWidth / 2; cw = this.CapWidth / 2; ## The box, notched about the median where asked for if (strcmp (this.Notch, 'on')) n = numel (y); d = 1.57 * sp / sqrt (n); nlo = max (md - d, q1); nhi = min (md + d, q3); bx = [at-hw, at+hw, at+hw, at+hw*0.5, at+hw, at+hw, ... at-hw, at-hw, at-hw*0.5, at-hw]; by = [q1, q1, nlo, md, nhi, q3, q3, nhi, md, nlo]; else bx = [at-hw, at+hw, at+hw, at-hw]; by = [q1, q1, q3, q3]; endif this.add (bcPatch (this, ax, bx, by)); ## The median, the whiskers and their caps this.add (bcLine (this, ax, [at-hw, at+hw], [md, md], ... this.BoxMedianLineColor, '-')); this.add (bcLine (this, ax, [at, at], [q3, hi], ... this.WhiskerLineColor, this.WhiskerLineStyle)); this.add (bcLine (this, ax, [at, at], [q1, lo], ... this.WhiskerLineColor, this.WhiskerLineStyle)); this.add (bcLine (this, ax, [at-cw, at+cw], [hi, hi], ... this.WhiskerLineColor, '-')); this.add (bcLine (this, ax, [at-cw, at+cw], [lo, lo], ... this.WhiskerLineColor, '-')); ## Whatever the whiskers do not reach if (any (out) && ! strcmp (this.MarkerStyle, 'none')) ox = at * ones (sum (out), 1); if (strcmp (this.JitterOutliers, 'on')) ox += (rand (sum (out), 1) - 0.5) * this.BoxWidth; endif this.add (bcMarks (this, ax, ox, y(out))); endif endfunction ## Keep a handle so the next drawing can take it away again. function add (this, h) this.Handles_(end+1) = h; endfunction ## Take the data from the table again. The assignments below each ask ## for a drawing of their own, so the flag holds them off until the last ## one is in and the chart is drawn once. function reread (this) if (isempty (this.SourceTable) || isempty (this.YVariable)) return; endif t = this.SourceTable; names = t.Properties.VariableNames; if (! any (strcmp (names, this.YVariable))) error (strcat ("stats.chart.BoxChart: the table holds no", ... " variable '%s'."), this.YVariable); endif drawn = this.Drawn_; this.Drawn_ = false; unwind_protect this.YData = bcCheckData (t.(this.YVariable), 'YVariable'); this.YDataMode = 'auto'; if (! isempty (this.XVariable)) if (! any (strcmp (names, this.XVariable))) error (strcat ("stats.chart.BoxChart: the table holds no", ... " variable '%s'."), this.XVariable); endif this.XData = t.(this.XVariable); this.XDataMode = 'auto'; endif unwind_protect_cleanup this.Drawn_ = drawn; end_unwind_protect redraw (this); endfunction endmethods endclassdef ## The observations, as a column of real numbers. function v = bcCheckData (val, name) if (! (isnumeric (val) && isreal (val) && isvector (val)) && ! isempty (val)) error ("stats.chart.BoxChart: '%s' must be a real numeric vector.", name); endif v = double (val(:)); endfunction ## The name of a table variable. function v = bcCheckName (val, name) if (isempty (val)) v = []; return; endif if (ischar (val) && isrow (val)) v = val; elseif (iscellstr (val) && isscalar (val)) v = val{1}; elseif (isa (val, 'string') && isscalar (val)) v = char (val); else error ("stats.chart.BoxChart: '%s' must name one table variable.", name); endif endfunction ## A width, a size or any other positive scalar. function v = bcCheckPositive (val, name) if (! (isnumeric (val) && isscalar (val) && isreal (val) && val > 0)) error ("stats.chart.BoxChart: '%s' must be a positive scalar.", name); endif v = double (val); endfunction ## A share, from none to all of it. function v = bcCheckUnit (val, name) if (! (isnumeric (val) && isscalar (val) && isreal (val) && val >= 0 && val <= 1)) error (strcat ("stats.chart.BoxChart: '%s' must be a scalar between", ... " 0 and 1."), name); endif v = double (val); endfunction ## A colour, as a triplet or as one of the names every toolkit knows. function v = bcCheckColor (val, name) if (isnumeric (val) && isreal (val) && isequal (size (val), [1, 3]) && all (val >= 0) && all (val <= 1)) v = double (val); return; endif if (ischar (val) && isrow (val)) known = {'r', 'g', 'b', 'c', 'm', 'y', 'k', 'w'}; rgb = [1, 0, 0; 0, 1, 0; 0, 0, 1; 0, 1, 1; ... 1, 0, 1; 1, 1, 0; 0, 0, 0; 1, 1, 1]; long = {'red', 'green', 'blue', 'cyan', 'magenta', 'yellow', ... 'black', 'white'}; j = find (strcmp (known, val) | strcmp (long, val), 1); if (! isempty (j)) v = rgb(j,:); return; endif endif error (strcat ("stats.chart.BoxChart: '%s' must be an RGB triplet", ... " or a colour name."), name); endfunction ## One of a short list of words. function v = bcCheckOneOf (val, allowed, name) if (! (ischar (val) && isrow (val) && any (strcmpi (allowed, val)))) error ("stats.chart.BoxChart: '%s' must be one of %s.", name, ... strjoin (strcat ("'", allowed, "'"), ', ')); endif v = lower (val); j = find (strcmpi (allowed, val), 1); v = allowed{j}; endfunction ## Where each box stands along the grouping axis, one position per group. function pos = bcPositions (this) x = this.XData; if (isempty (x)) pos = 1; return; endif if (isa (x, 'categorical')) pos = 1:numel (categories (x)); else pos = unique (x(! isnan (x)))'; endif if (isempty (pos)) pos = 1; endif endfunction ## The observations belonging to the kth box. function y = bcGroupData (this, k) x = this.XData; y = this.YData(:); if (isempty (x)) return; endif if (isa (x, 'categorical')) c = categories (x); y = y(x == c{k}); else pos = bcPositions (this); y = y(x(:) == pos(k)); endif endfunction ## The box itself, filled and outlined as the properties ask. function h = bcPatch (this, ax, bx, by) [bx, by] = bcOrient (this, bx, by); h = patch (ax, bx, by, this.BoxFaceColor, ... 'FaceAlpha', this.BoxFaceAlpha, ... 'EdgeColor', this.BoxEdgeColor, ... 'LineWidth', this.LineWidth); endfunction ## One straight line of the chart, in the colour and style asked for. function h = bcLine (this, ax, bx, by, col, style) [bx, by] = bcOrient (this, bx, by); h = line (ax, bx, by, 'Color', col, 'LineStyle', style, ... 'LineWidth', this.LineWidth); endfunction ## The markers standing for whatever the whiskers do not reach. function h = bcMarks (this, ax, bx, by) [bx, by] = bcOrient (this, bx, by); h = line (ax, bx, by, 'LineStyle', 'none', 'Marker', this.MarkerStyle, ... 'MarkerSize', this.MarkerSize, ... 'MarkerEdgeColor', this.MarkerColor, ... 'MarkerFaceColor', 'none'); endfunction ## A horizontal chart is the same drawing with the axes exchanged, which is ## done in one place so that nothing above has to know which way it lies. function [ox, oy] = bcOrient (this, bx, by) if (strcmp (this.Orientation, 'horizontal')) ox = by; oy = bx; else ox = bx; oy = by; endif endfunction %!shared bxY, bxG, bxS, bxR %! bxY = [1; 2; 3; 4; 5; 6; 7; 8; 9; 100]; %! bxG = [2; 2; 2; 2; 2; 5; 5; 5; 5; 5]; %! bxS = struct ('XData', [], 'YData', bxY, 'SourceTable', [], ... %! 'XVariable', [], 'YVariable', [], ... %! 'XDataMode', 'auto', 'YDataMode', 'manual'); %! bxR = struct ('XData', [], 'YData', [], ... %! 'SourceTable', table (bxG, bxY, ... %! 'VariableNames', {'grp', 'val'}), ... %! 'XVariable', 'grp', 'YVariable', 'val', ... %! 'XDataMode', 'auto', 'YDataMode', 'auto'); %!test # the class is built from an axes and a data specification %! hf = figure ('visible', 'off'); %! unwind_protect %! b = stats.chart.BoxChart (gca (), bxS); %! assert_equal (class (b), 'stats.chart.BoxChart'); %! assert_equal (b.Parent, gca ()); %! assert_equal (b.YData, bxY); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # an empty axes is the current one, and the pairs are set before drawing %! hf = figure ('visible', 'off'); %! unwind_protect %! b = stats.chart.BoxChart ([], bxS, {'Notch', 'on', 'BoxWidth', 0.8}); %! assert_equal (b.Parent, gca ()); %! assert_equal (b.Notch, 'on'); %! assert_equal (b.BoxWidth, 0.8); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a rejected value leaves no figure, the axes being resolved last %! n = numel (findall (0, 'type', 'figure')); %! msg = ''; %! try %! stats.chart.BoxChart ([], bxS, {'BoxWidth', -1}); %! catch err %! msg = err.message; %! end_try_catch %! assert_equal (isempty (msg), false); %! assert_equal (numel (findall (0, 'type', 'figure')), n); %!test # the colours every part is drawn in default to the same triplet %! hf = figure ('visible', 'off'); %! unwind_protect %! b = stats.chart.BoxChart (gca (), bxS); %! assert_equal (b.BoxFaceColor, [0, 0.447, 0.741]); %! assert_equal (b.BoxEdgeColor, [0, 0.447, 0.741]); %! assert_equal (b.BoxMedianLineColor, [0, 0.447, 0.741]); %! assert_equal (b.WhiskerLineColor, [0, 0.447, 0.741]); %! assert_equal (b.MarkerColor, [0, 0.447, 0.741]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # nothing was chosen, so every mode reads 'auto' %! hf = figure ('visible', 'off'); %! unwind_protect %! b = stats.chart.BoxChart (gca (), bxS); %! assert_equal (b.BoxFaceColorMode, 'auto'); %! assert_equal (b.BoxEdgeColorMode, 'auto'); %! assert_equal (b.BoxMedianLineColorMode, 'auto'); %! assert_equal (b.MarkerColorMode, 'auto'); %! assert_equal (b.CapWidthMode, 'auto'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # setting one of them records that it was chosen %! hf = figure ('visible', 'off'); %! unwind_protect %! b = stats.chart.BoxChart (gca (), bxS); %! b.BoxEdgeColor = [1, 0, 0]; %! assert_equal (b.BoxEdgeColorMode, 'manual'); %! b.BoxMedianLineColor = [1, 0, 0]; %! assert_equal (b.BoxMedianLineColorMode, 'manual'); %! b.MarkerColor = [1, 0, 0]; %! assert_equal (b.MarkerColorMode, 'manual'); %! b.CapWidth = 0.4; %! assert_equal (b.CapWidthMode, 'manual'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a colour name is kept as the triplet it stands for %! hf = figure ('visible', 'off'); %! unwind_protect %! b = stats.chart.BoxChart (gca (), bxS); %! b.BoxFaceColor = 'r'; %! assert_equal (b.BoxFaceColor, [1, 0, 0]); %! b.WhiskerLineColor = 'black'; %! assert_equal (b.WhiskerLineColor, [0, 0, 0]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a word is taken whatever its case and kept in the documented spelling %! hf = figure ('visible', 'off'); %! unwind_protect %! b = stats.chart.BoxChart (gca (), bxS); %! b.Notch = 'ON'; %! assert_equal (b.Notch, 'on'); %! b.Orientation = 'HORIZONTAL'; %! assert_equal (b.Orientation, 'horizontal'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## The modes and the parent are read-only here, where MATLAB assigns them %!test %! m = meta.class.fromName ('stats.chart.BoxChart'); %! p = m.PropertyList; %! if (iscell (p)) %! p = [p{:}]; %! endif %! nm = {p.Name}; %! i = find (strcmp (nm, 'XDataMode'), 1); %! assert_equal (p(i).SetAccess, 'private'); %! assert_equal (p(i).GetAccess, 'public'); %! i = find (strcmp (nm, 'Parent'), 1); %! assert_equal (p(i).SetAccess, 'private'); %!test # the box spans the quartiles and the median lies across it %! hf = figure ('visible', 'off'); %! unwind_protect %! stats.chart.BoxChart (gca (), bxS); %! k = flipud (get (gca (), 'children')); %! assert_equal (get (k(1), 'xdata'), [0.75; 1.25; 1.25; 0.75]); %! assert_equal (get (k(1), 'ydata'), [3; 3; 8; 8]); %! assert_equal (get (k(2), 'ydata'), [5.5, 5.5]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## The whiskers reach the furthest values within one and a half IQR of the box %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! stats.chart.BoxChart (gca (), bxS); %! k = flipud (get (gca (), 'children')); %! assert_equal (get (k(3), 'ydata'), [8, 9]); %! assert_equal (get (k(4), 'ydata'), [3, 1]); %! assert_equal (get (k(5), 'xdata'), [0.875, 1.125]); %! assert_equal (get (k(6), 'xdata'), [0.875, 1.125]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # whatever the whiskers do not reach is drawn as a marker %! hf = figure ('visible', 'off'); %! unwind_protect %! b = stats.chart.BoxChart (gca (), bxS); %! k = flipud (get (gca (), 'children')); %! assert_equal (numel (k), 7); %! assert_equal (get (k(7), 'ydata'), 100); %! assert_equal (get (k(7), 'marker'), 'o'); %! b.MarkerStyle = 'none'; %! assert_equal (numel (get (gca (), 'children')), 6); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a jittered marker is spread across the width of the box %! hf = figure ('visible', 'off'); %! unwind_protect %! b = stats.chart.BoxChart (gca (), bxS); %! k = flipud (get (gca (), 'children')); %! assert_equal (get (k(7), 'xdata'), 1); %! b.JitterOutliers = 'on'; %! k = flipud (get (gca (), 'children')); %! assert_equal (abs (get (k(7), 'xdata') - 1) <= 0.25, true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a horizontal chart is the same drawing with the axes exchanged %! hf = figure ('visible', 'off'); %! unwind_protect %! b = stats.chart.BoxChart (gca (), bxS, {'Orientation', 'horizontal'}); %! k = flipud (get (gca (), 'children')); %! assert_equal (get (k(1), 'xdata'), [3; 3; 8; 8]); %! assert_equal (get (k(1), 'ydata'), [0.75; 1.25; 1.25; 0.75]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## A notch spans the median plus and minus 1.57 IQR / sqrt (n) %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! stats.chart.BoxChart (gca (), bxS, {'Notch', 'on'}); %! k = flipud (get (gca (), 'children')); %! by = get (k(1), 'ydata'); %! d = 1.57 * (8 - 3) / sqrt (10); %! assert_equal (numel (by), 10); %! assert_equal (by(3), 5.5 - d, eps); %! assert_equal (by(5), 5.5 + d, eps); %! assert_equal (by(4), 5.5); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a numeric grouping puts each box at the value it was given %! hf = figure ('visible', 'off'); %! unwind_protect %! s = bxS; %! s.XData = bxG; %! s.XDataMode = 'manual'; %! stats.chart.BoxChart (gca (), s); %! p = findobj (gca (), 'type', 'patch'); %! x1 = get (p(1), 'xdata'); %! x2 = get (p(2), 'xdata'); %! assert_equal (sort ([x1(1), x2(1)]), [1.75, 4.75]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a categorical grouping draws one box per level %! hf = figure ('visible', 'off'); %! unwind_protect %! s = bxS; %! s.XData = categorical ({'a'; 'a'; 'a'; 'b'; 'b'; 'b'; 'c'; 'c'; 'c'; 'c'}); %! s.XDataMode = 'manual'; %! stats.chart.BoxChart (gca (), s); %! assert_equal (numel (findobj (gca (), 'type', 'patch')), 3); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # no observations, nothing drawn, and the data may be given afterwards %! hf = figure ('visible', 'off'); %! unwind_protect %! s = bxS; %! s.YData = []; %! b = stats.chart.BoxChart (gca (), s); %! assert_equal (isempty (get (gca (), 'children')), true); %! b.YData = bxY; %! assert_equal (numel (get (gca (), 'children')), 7); %! assert_equal (b.YDataMode, 'manual'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a row of observations is kept as a column %! hf = figure ('visible', 'off'); %! unwind_protect %! s = bxS; %! s.YData = bxY'; %! b = stats.chart.BoxChart (gca (), s); %! assert_equal (b.YData, bxY); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # the chart is a handle, so two names are one chart %! hf = figure ('visible', 'off'); %! unwind_protect %! b = stats.chart.BoxChart (gca (), bxS); %! c = b; %! c.BoxWidth = 0.9; %! assert_equal (b.BoxWidth, 0.9); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a table fills both sides and puts both modes at 'auto' %! hf = figure ('visible', 'off'); %! unwind_protect %! b = stats.chart.BoxChart (gca (), bxR); %! assert_equal (b.YData, bxY); %! assert_equal (b.XDataMode, 'auto'); %! assert_equal (b.YDataMode, 'auto'); %! assert_equal (numel (findobj (gca (), 'type', 'patch')), 2); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # clearing a variable name is allowed where naming one would not be %! hf = figure ('visible', 'off'); %! unwind_protect %! b = stats.chart.BoxChart (gca (), bxR); %! b.XVariable = []; %! assert_equal (isempty (b.XVariable), true); %! assert_equal (b.YData, bxY); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # disp heads with the short name and says what was drawn %! hf = figure ('visible', 'off'); %! unwind_protect %! b = stats.chart.BoxChart (gca (), bxS); %! s = evalc ('disp (b)'); %! assert_equal (isempty (strfind (s, 'BoxChart with properties:')), false); %! assert_equal (isempty (strfind (s, 'observations: 10')), false); %! assert_equal (isempty (strfind (s, 'boxes: 1')), false); %! assert_equal (isempty (strfind (s, 'Orientation: vertical')), false); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a chart drawn from a table also names the column it was drawn from %! hf = figure ('visible', 'off'); %! unwind_protect %! b = stats.chart.BoxChart (gca (), bxR); %! s = evalc ('disp (b)'); %! assert_equal (isempty (strfind (s, 'boxes: 2')), false); %! assert_equal (isempty (strfind (s, 'YVariable: val')), false); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Input validation %!error ... %! stats.chart.BoxChart () %!error ... %! stats.chart.BoxChart ([], setfield (bxS, 'YData', {1, 2})) %!error ... %! stats.chart.BoxChart ([], setfield (bxS, 'SourceTable', 5)) %!error ... %! stats.chart.BoxChart ([], setfield (bxR, 'XVariable', 5)) %!error ... %! stats.chart.BoxChart ([], setfield (bxR, 'YVariable', 'nope')) %!error ... %! stats.chart.BoxChart ([], setfield (bxR, 'XVariable', 'nope')) %!error ... %! stats.chart.BoxChart ([], bxS, {'BoxWidth', 0}) %!error ... %! stats.chart.BoxChart ([], bxS, {'CapWidth', [1, 2]}) %!error ... %! stats.chart.BoxChart ([], bxS, {'LineWidth', -1}) %!error ... %! stats.chart.BoxChart ([], bxS, {'MarkerSize', 'big'}) %!error ... %! stats.chart.BoxChart ([], bxS, {'BoxFaceAlpha', 1.5}) %!error ... %! stats.chart.BoxChart ([], bxS, {'BoxFaceColor', [2, 0, 0]}) %!error ... %! stats.chart.BoxChart ([], bxS, {'WhiskerLineColor', 'puce'}) %!error ... %! stats.chart.BoxChart ([], bxS, {'WhiskerLineStyle', 'wavy'}) ## The pattern of the next block stops at 'v': 'test.m' ends it at the first ## '>' of the line, with 'index (str, ">")', so the '>' among the markers this ## message lists cannot be matched. Measured on Octave 11.3.0. %!error ... %! stats.chart.BoxChart ([], bxS, {'MarkerStyle', 'q'}) %!error ... %! stats.chart.BoxChart ([], bxS, {'Notch', 'maybe'}) %!error ... %! stats.chart.BoxChart ([], bxS, {'JitterOutliers', 'yes'}) %!error ... %! stats.chart.BoxChart ([], bxS, {'Orientation', 'sideways'}) ## The data comes from vectors or from a table, never from both %!error %! hf = figure ('visible', 'off'); %! b = stats.chart.BoxChart (gca (), bxR); %! close (hf); %! b.YData = bxY; %!error %! hf = figure ('visible', 'off'); %! b = stats.chart.BoxChart (gca (), bxR); %! close (hf); %! b.XData = bxG; %!error %! hf = figure ('visible', 'off'); %! b = stats.chart.BoxChart (gca (), bxS); %! close (hf); %! b.YVariable = 'val'; %!error %! hf = figure ('visible', 'off'); %! s = bxS; %! s.XData = bxG; %! s.XDataMode = 'manual'; %! b = stats.chart.BoxChart (gca (), s); %! close (hf); %! b.XVariable = 'grp'; statistics-release-2.1.2/inst/Plotting/+stats/+chart/HeatmapChart.m000066400000000000000000002235301527311104300252070ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftp {statistics} stats.chart.HeatmapChart ## ## A heatmap, as @code{heatmap} draws it. ## ## A @code{stats.chart.HeatmapChart} holds a matrix of values, the labels of ## its columns and rows, and the choices it is drawn with, and redraws itself ## whenever one of them is set. It is what @code{heatmap} returns, and the ## documented way to reach it. ## ## The values come either from a matrix or from a table, never from both. ## From a table, each distinct value of @code{XVariable} is a column and each ## distinct value of @code{YVariable} a row, and each cell holds the number of ## rows of the table falling in it, or the mean, median, sum, minimum or ## maximum of @code{ColorVariable} over them, as @code{ColorMethod} says. ## Missing values of @code{ColorVariable} are left out of the mean, median, ## sum, minimum and maximum, and counted by @qcode{'count'}. ## ## @code{XData} and @code{YData} name the columns and rows of @code{ColorData}. ## @code{XDisplayData} and @code{YDisplayData} say which of them are shown and ## in what order, and @code{XDisplayLabels} and @code{YDisplayLabels} what they ## are called on the chart. @code{ColorDisplayData} is the matrix as shown. ## ## A heatmap takes the place of the current axes of its parent, as MATLAB's ## does, keeping their outer position, and refuses axes on which ## @code{hold} is on. A later plot into the figure takes the place of the ## heatmap in turn. ## ## MATLAB's chart is a graphics object of its own. This one is a handle class ## that draws into axes of its own, which become the current axes and are ## deleted with it; when a later plot clears those axes the chart lets go of ## them, and @code{Parent} and the drawing are gone. @code{GridVisible}, ## @code{ColorbarVisible} and @code{Visible} hold @qcode{'on'} or ## @qcode{'off'} where MATLAB holds an on-off state. ## ## @seealso{heatmap, imagesc} ## @end deftp classdef HeatmapChart < handle properties (Access = public) ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} ColorData ## ## The matrix of values, one row for each element of @code{YData} and one ## column for each element of @code{XData}. Where the chart was drawn ## from a table it is computed from the table and setting it is refused. ## ## @end deftp ColorData = []; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} XData ## ## The names of the columns of @code{ColorData}, a column cell array of ## distinct character vectors. By default they are @qcode{'1'}, ## @qcode{'2'}, @dots{}, and they follow the size of @code{ColorData} until ## they are set. ## ## @end deftp XData = cell (0, 1); ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} YData ## ## The names of the rows of @code{ColorData}, as @code{XData} names its ## columns. ## ## @end deftp YData = cell (0, 1); ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} XDisplayData ## ## The columns shown and their order, as elements of @code{XData}; a value ## not in @code{XData} is shown as a column of missing values. All of ## @code{XData} by default. ## ## @end deftp XDisplayData = cell (0, 1); ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} YDisplayData ## ## The rows shown and their order, as @code{XDisplayData} gives the ## columns. ## ## @end deftp YDisplayData = cell (0, 1); ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} XDisplayLabels ## ## The labels of the columns shown, one for each element of ## @code{XDisplayData}, which they are by default. A label set stays with ## its column when @code{XDisplayData} is reordered. ## ## @end deftp XDisplayLabels = cell (0, 1); ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} YDisplayLabels ## ## The labels of the rows shown, as @code{XDisplayLabels} labels the ## columns. ## ## @end deftp YDisplayLabels = cell (0, 1); ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} XLimits ## ## The first and the last column shown, as a 1-by-2 cell array of elements ## of @code{XDisplayData}; the columns between them in ## @code{XDisplayData} are shown. The whole of @code{XDisplayData} by ## default. ## ## @end deftp XLimits = cell (1, 0); ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} YLimits ## ## The first and the last row shown, as @code{XLimits} gives the columns. ## ## @end deftp YLimits = cell (1, 0); ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} SourceTable ## ## The table the chart was drawn from, or an empty table. ## ## @end deftp SourceTable = []; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} XVariable ## ## The variable of @code{SourceTable} whose values are the columns. ## ## @end deftp XVariable = ''; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} YVariable ## ## The variable of @code{SourceTable} whose values are the rows. ## ## @end deftp YVariable = ''; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} ColorVariable ## ## The numeric variable of @code{SourceTable} aggregated into each cell, ## or empty to count the rows. ## ## @end deftp ColorVariable = ''; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} ColorMethod ## ## How the rows of @code{SourceTable} falling in a cell make its value: ## @qcode{'count'}, @qcode{'mean'}, @qcode{'median'}, @qcode{'sum'}, ## @qcode{'min'}, @qcode{'max'} or @qcode{'none'}, which takes the one ## value there is and is refused where a cell holds more than one. It ## is @qcode{'count'} where no @code{ColorVariable} is named, ## @qcode{'mean'} where one is, and @qcode{'none'} for a matrix. ## ## @end deftp ColorMethod = 'none'; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} ColorScaling ## ## How the values are turned into colours: @qcode{'scaled'}, the default, ## maps them to the colormap as they are; @qcode{'scaledcolumns'} and ## @qcode{'scaledrows'} rescale each column or row to run from 0 to 1; ## @qcode{'log'} maps their natural logarithm, a negative value then ## being left uncoloured with a warning. The cell labels show the values ## themselves whatever the scaling. ## ## @end deftp ColorScaling = 'scaled'; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} ColorLimits ## ## The values mapped to the first and the last colour of @code{Colormap}, ## as a 1-by-2 increasing vector. Until it is set it follows the values ## shown: their smallest and largest, @math{v - 1} and @math{v + 1} where ## they are all @math{v}, @math{[0, 1]} where none is finite or where the ## columns or rows are rescaled. ## ## @end deftp ColorLimits = [0, 1]; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} Colormap ## ## The colours, as an M-by-3 matrix of values between 0 and 1. By default ## 256 colours running from a pale blue to @code{[0, 0.447, 0.741]}. ## ## @end deftp Colormap = []; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} ColorbarVisible ## ## Whether the colour bar is drawn, @qcode{'on'} by default or ## @qcode{'off'}. ## ## @end deftp ColorbarVisible = 'on'; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} MissingDataColor ## ## The colour of a cell holding a missing value, as an RGB triplet or a ## colour name. @code{[0.15, 0.15, 0.15]} by default. ## ## @end deftp MissingDataColor = [0.15, 0.15, 0.15]; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} MissingDataLabel ## ## The label of a cell holding a missing value, @qcode{'NaN'} by default. ## ## @end deftp MissingDataLabel = 'NaN'; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} CellLabelFormat ## ## The format each value is written into its cell with, as ## @code{sprintf} takes it. @qcode{'%0.4g'} by default. ## ## @end deftp CellLabelFormat = '%0.4g'; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} CellLabelColor ## ## The colour of the cell labels, as an RGB triplet or a colour name; ## @qcode{'auto'}, the default, writes black on a light cell and white on ## a dark one, and @qcode{'none'} writes no labels. ## ## @end deftp CellLabelColor = 'auto'; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} GridVisible ## ## Whether lines are drawn between the cells, @qcode{'on'} by default or ## @qcode{'off'}. ## ## @end deftp GridVisible = 'on'; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} Title ## ## The title, a character vector or a cell array of them, one for each ## line. From a table it says what the cells hold, such as ## @qcode{'Count of Y vs. X'} or @qcode{'Mean of V'}, until it is set. ## ## @end deftp Title = ''; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} XLabel ## ## The label of the horizontal axis; from a table the name of ## @code{XVariable}, until it is set. ## ## @end deftp XLabel = ''; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} YLabel ## ## The label of the vertical axis; from a table the name of ## @code{YVariable}, until it is set. ## ## @end deftp YLabel = ''; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} FontName ## ## The font of every text on the chart, @qcode{'Helvetica'} by default. ## ## @end deftp FontName = 'Helvetica'; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} FontSize ## ## The size of every text on the chart, in points, 10 by default. ## ## @end deftp FontSize = 10; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} FontColor ## ## The colour of the title, the axis labels and the tick labels. ## @code{[0.15, 0.15, 0.15]} by default. ## ## @end deftp FontColor = [0.15, 0.15, 0.15]; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} Interpreter ## ## How the texts are interpreted: @qcode{'tex'}, the default, ## @qcode{'latex'} or @qcode{'none'}. ## ## @end deftp Interpreter = 'tex'; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} Position ## ## The position of the grid of cells in its parent, as ## @code{[left, bottom, width, height]} in @code{Units}. While ## @code{PositionConstraint} is @qcode{'outerposition'} the chart places ## the cells within @code{OuterPosition} itself, leaving room for the ## labels and the colour bar, @code{[0.13, 0.11, 0.732143, 0.815]} of it ## at least; setting @code{Position} fixes the cells there and turns ## @code{PositionConstraint} to @qcode{'innerposition'}. ## @code{InnerPosition} is the same. ## ## @end deftp Position = [0.13, 0.11, 0.732143, 0.815]; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} OuterPosition ## ## The position of the whole chart in its parent, labels included, ## @code{[0, 0, 1, 1]} by default, or the outer position of the axes it ## took the place of. Setting it turns @code{PositionConstraint} to ## @qcode{'outerposition'}. ## ## @end deftp OuterPosition = [0, 0, 1, 1]; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} PositionConstraint ## ## Which position the chart keeps when its labels change: ## @qcode{'outerposition'}, the default, or @qcode{'innerposition'}. ## ## @end deftp PositionConstraint = 'outerposition'; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} Units ## ## The units of the positions, @qcode{'normalized'} by default. ## ## @end deftp Units = 'normalized'; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} Visible ## ## Whether the chart is shown, @qcode{'on'} by default or @qcode{'off'}. ## ## @end deftp Visible = 'on'; endproperties properties (Dependent) ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} ColorDisplayData ## ## @code{ColorData} as it is shown: its rows and columns in the order of ## @code{YDisplayData} and @code{XDisplayData}, missing where a displayed ## value is not in the data. This property is read-only. ## ## @end deftp ColorDisplayData; ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} InnerPosition ## ## The same as @code{Position}. ## ## @end deftp InnerPosition; endproperties properties (GetAccess = public, SetAccess = private) ## -*- texinfo -*- ## @deftp {stats.chart.HeatmapChart} {property} Parent ## ## The figure or panel the chart is placed in. This property is ## read-only. ## ## @end deftp Parent = []; endproperties properties (Access = private, Hidden) Axes_ = []; # the axes the chart draws into Colorbar_ = []; # its colour bar Drawn_ = false; # false while the constructor fills the fields Redrawing_ = false; # true while the chart clears its own drawing Aggregating_ = false; # true while the table is being read Internal_ = false; # true while the chart sets a value itself XDataAuto_ = true; # XData follows the size of ColorData YDataAuto_ = true; XDisplayAuto_ = true; # XDisplayData follows XData YDisplayAuto_ = true; XLabelMap_ = cell (0, 2); # labels set by hand, value against label YLabelMap_ = cell (0, 2); XLimitsAuto_ = true; YLimitsAuto_ = true; ColorLimitsAuto_ = true; TitleAuto_ = true; XLabelAuto_ = true; YLabelAuto_ = true; MethodAuto_ = true; # ColorMethod follows ColorVariable endproperties methods (Hidden) function disp (this) if (isempty (this.Title)) printf (' HeatmapChart with properties:\n\n'); else t = this.Title; if (iscell (t)) t = strjoin (t, ' '); endif printf (' HeatmapChart (%s) with properties:\n\n', t); endif if (isempty (this.SourceTable)) printf ('%13s: {%dx1 cell}\n', 'XData', numel (this.XData)); printf ('%13s: {%dx1 cell}\n', 'YData', numel (this.YData)); printf ('%13s: [%dx%d double]\n', 'ColorData', ... rows (this.ColorData), columns (this.ColorData)); else printf ('%13s: [%dx%d table]\n', 'SourceTable', ... size (this.SourceTable)); printf ('%13s: ''%s''\n', 'XVariable', this.XVariable); printf ('%13s: ''%s''\n', 'YVariable', this.YVariable); printf ('%13s: ''%s''\n', 'ColorVariable', this.ColorVariable); printf ('%13s: ''%s''\n', 'ColorMethod', this.ColorMethod); endif printf ('\n'); endfunction function display (this) disp (this); endfunction endmethods methods (Hidden) function set.ColorData (this, val) if (this.Drawn_ && ! isempty (this.SourceTable)) error (strcat ("stats.chart.HeatmapChart: setting 'ColorData'", ... " while 'SourceTable' holds a table is not", ... " supported.")); endif this.ColorData = hmCheckMatrix (val); fitData (this); redraw (this); endfunction function set.XData (this, val) this.XData = hmCheckNames (val, 'XData'); this.XDataAuto_ = false; fitData (this); redraw (this); endfunction function set.YData (this, val) this.YData = hmCheckNames (val, 'YData'); this.YDataAuto_ = false; fitData (this); redraw (this); endfunction function set.XDisplayData (this, val) this.XDisplayData = hmCheckNames (val, 'XDisplayData'); this.XDisplayAuto_ = false; this.Internal_ = true; unwind_protect this.XLimits = hmFitLimits (this.XLimits, this.XDisplayData, ... this.XLimitsAuto_, 'XLimits'); unwind_protect_cleanup this.Internal_ = false; end_unwind_protect redraw (this); endfunction function set.YDisplayData (this, val) this.YDisplayData = hmCheckNames (val, 'YDisplayData'); this.YDisplayAuto_ = false; this.Internal_ = true; unwind_protect this.YLimits = hmFitLimits (this.YLimits, this.YDisplayData, ... this.YLimitsAuto_, 'YLimits'); unwind_protect_cleanup this.Internal_ = false; end_unwind_protect redraw (this); endfunction function val = get.XDisplayLabels (this) val = hmLabels (this.XDisplayData, this.XLabelMap_); endfunction function set.XDisplayLabels (this, val) val = hmCheckLabels (val, numel (this.XDisplayData), 'XDisplayLabels'); this.XLabelMap_ = hmSetLabels (this.XLabelMap_, this.XDisplayData, val); redraw (this); endfunction function val = get.YDisplayLabels (this) val = hmLabels (this.YDisplayData, this.YLabelMap_); endfunction function set.YDisplayLabels (this, val) val = hmCheckLabels (val, numel (this.YDisplayData), 'YDisplayLabels'); this.YLabelMap_ = hmSetLabels (this.YLabelMap_, this.YDisplayData, val); redraw (this); endfunction function set.XLimits (this, val) this.XLimits = hmCheckLimits (val, this.XDisplayData, 'XLimits'); if (! this.Internal_) if (this.Drawn_) this.XLimitsAuto_ = false; endif redraw (this); endif endfunction function set.YLimits (this, val) this.YLimits = hmCheckLimits (val, this.YDisplayData, 'YLimits'); if (! this.Internal_) if (this.Drawn_) this.YLimitsAuto_ = false; endif redraw (this); endif endfunction function set.SourceTable (this, val) if (! (isempty (val) || istable (val))) error ("stats.chart.HeatmapChart: 'SourceTable' must be a table."); endif this.SourceTable = val; aggregate (this); endfunction function set.XVariable (this, val) this.XVariable = hmCheckVarName (val, 'XVariable'); aggregate (this); endfunction function set.YVariable (this, val) this.YVariable = hmCheckVarName (val, 'YVariable'); aggregate (this); endfunction function set.ColorVariable (this, val) this.ColorVariable = hmCheckVarName (val, 'ColorVariable'); aggregate (this); endfunction function set.ColorMethod (this, val) this.ColorMethod = hmCheckOneOf (val, {'count', 'mean', 'median', ... 'sum', 'min', 'max', 'none'}, ... 'ColorMethod'); if (! this.Aggregating_) this.MethodAuto_ = false; aggregate (this); endif endfunction function set.ColorScaling (this, val) this.ColorScaling = hmCheckOneOf (val, {'scaled', 'scaledcolumns', ... 'scaledrows', 'log'}, ... 'ColorScaling'); redraw (this); endfunction function set.ColorLimits (this, val) if (! (isnumeric (val) && isreal (val) && numel (val) == 2 && all (isfinite (val)) && val(2) > val(1))) error (strcat ("stats.chart.HeatmapChart: 'ColorLimits' must be", ... " a 1-by-2 increasing numeric vector.")); endif this.ColorLimits = double (val(:)'); if (! this.Internal_) if (this.Drawn_) this.ColorLimitsAuto_ = false; endif redraw (this); endif endfunction function set.Colormap (this, val) if (! (isnumeric (val) && isreal (val) && ndims (val) == 2 && columns (val) == 3 && rows (val) > 0 && all (val(:) >= 0) && all (val(:) <= 1))) error (strcat ("stats.chart.HeatmapChart: 'Colormap' must be an", ... " M-by-3 matrix of values between 0 and 1.")); endif this.Colormap = double (val); redraw (this); endfunction function set.ColorbarVisible (this, val) this.ColorbarVisible = hmCheckOneOf (val, {'on', 'off'}, ... 'ColorbarVisible'); redraw (this); endfunction function set.MissingDataColor (this, val) this.MissingDataColor = hmCheckColor (val, 'MissingDataColor', false); redraw (this); endfunction function set.MissingDataLabel (this, val) this.MissingDataLabel = hmCheckText (val, 'MissingDataLabel'); redraw (this); endfunction function set.CellLabelFormat (this, val) if (! (ischar (val) && (isrow (val) || isempty (val))) && ! (isa (val, 'string') && isscalar (val))) error (strcat ("stats.chart.HeatmapChart: 'CellLabelFormat' must", ... " be a character vector.")); endif this.CellLabelFormat = char (val); redraw (this); endfunction function set.CellLabelColor (this, val) this.CellLabelColor = hmCheckColor (val, 'CellLabelColor', true); redraw (this); endfunction function set.GridVisible (this, val) this.GridVisible = hmCheckOneOf (val, {'on', 'off'}, 'GridVisible'); redraw (this); endfunction function set.Title (this, val) this.Title = hmCheckText (val, 'Title'); if (this.Drawn_) this.TitleAuto_ = false; endif redraw (this); endfunction function set.XLabel (this, val) this.XLabel = hmCheckText (val, 'XLabel'); if (this.Drawn_) this.XLabelAuto_ = false; endif redraw (this); endfunction function set.YLabel (this, val) this.YLabel = hmCheckText (val, 'YLabel'); if (this.Drawn_) this.YLabelAuto_ = false; endif redraw (this); endfunction function set.FontName (this, val) this.FontName = hmCheckText (val, 'FontName'); redraw (this); endfunction function set.FontSize (this, val) if (! (isnumeric (val) && isreal (val) && isscalar (val) && isfinite (val) && val > 0)) error (strcat ("stats.chart.HeatmapChart: 'FontSize' must be a", ... " positive number.")); endif this.FontSize = double (val); redraw (this); endfunction function set.FontColor (this, val) this.FontColor = hmCheckColor (val, 'FontColor', false); redraw (this); endfunction function set.Interpreter (this, val) this.Interpreter = hmCheckOneOf (val, {'tex', 'latex', 'none'}, ... 'Interpreter'); redraw (this); endfunction function set.Position (this, val) this.Position = hmCheckPosition (val, 'Position'); if (! this.Internal_) this.PositionConstraint = 'innerposition'; endif endfunction function val = get.InnerPosition (this) val = this.Position; endfunction function set.InnerPosition (this, val) this.Position = val; endfunction function set.OuterPosition (this, val) this.OuterPosition = hmCheckPosition (val, 'OuterPosition'); if (! this.Internal_) this.PositionConstraint = 'outerposition'; endif endfunction function set.PositionConstraint (this, val) this.PositionConstraint = hmCheckOneOf (val, {'outerposition', ... 'innerposition'}, ... 'PositionConstraint'); redraw (this); endfunction function set.Units (this, val) this.Units = hmCheckOneOf (val, {'normalized', 'inches', ... 'centimeters', 'points', 'pixels', ... 'characters'}, 'Units'); redraw (this); endfunction function set.Visible (this, val) this.Visible = hmCheckOneOf (val, {'on', 'off'}, 'Visible'); redraw (this); endfunction function val = get.ColorDisplayData (this) val = hmDisplayMatrix (this.ColorData, this.XData, this.YData, ... this.XDisplayData, this.YDisplayData); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {stats.chart.HeatmapChart} {@var{obj} =} stats.chart.HeatmapChart (@var{parent}, @var{spec}, @var{args}) ## ## Create a @code{stats.chart.HeatmapChart} object. ## ## @var{parent} is the figure or panel to place the chart in, or empty for ## the current figure, which is resolved only once every value has been ## accepted. @var{spec} is a structure carrying the data as ## @code{heatmap} resolved it, with the fields @qcode{ColorData}, ## @qcode{XData}, @qcode{YData}, @qcode{SourceTable}, @qcode{XVariable}, ## @qcode{YVariable} and @qcode{ColorVariable}, and @var{args} the ## name-value pairs left to set. The documented way to reach this ## constructor is @code{heatmap}. ## ## @seealso{heatmap} ## @end deftypefn function this = HeatmapChart (parent, spec, args) if (nargin < 2) error ("stats.chart.HeatmapChart: too few input arguments."); endif if (nargin < 3) args = {}; endif this.Colormap = hmDefaultColormap (); if (isempty (spec.SourceTable)) this.ColorData = spec.ColorData; if (! isempty (spec.XData)) this.XData = spec.XData; endif if (! isempty (spec.YData)) this.YData = spec.YData; endif else this.SourceTable = spec.SourceTable; this.XVariable = spec.XVariable; this.YVariable = spec.YVariable; this.ColorVariable = spec.ColorVariable; endif ## Whatever was named is set now, so that one drawing covers them all; ## a value set here counts as chosen, as it would once drawn placed = false; for k = 1:2:numel (args) name = args{k}; this.(name) = args{k+1}; switch (name) case {'XLimits', 'YLimits', 'ColorLimits', 'Title', 'XLabel', ... 'YLabel', 'ColorMethod'} chosen (this, name); case {'Position', 'InnerPosition', 'OuterPosition'} placed = true; endswitch endfor aggregate (this); ## The parent comes last, so a rejected value leaves no figure behind. ## The chart takes the place of the current axes of its parent, as ## MATLAB's does, and refuses axes that are held if (isempty (parent)) parent = gcf (); endif fig = ancestor (parent, 'figure'); ca = get (fig, 'currentaxes'); if (! isempty (ca) && get (ca, 'parent') == parent) if (ishold (ca)) error (strcat ("stats.chart.HeatmapChart: a heatmap cannot be", ... " added to axes on which hold is on.")); endif if (! placed) units = get (ca, 'units'); set (ca, 'units', this.Units); this.OuterPosition = get (ca, 'outerposition'); set (ca, 'units', units); endif delete (ca); endif this.Parent = parent; this.Axes_ = axes ('parent', parent, 'tag', 'stats.chart.HeatmapChart'); set (fig, 'currentaxes', this.Axes_); ## The chart goes with its axes, and its axes with the chart set (this.Axes_, 'deletefcn', @(~, ~) delete (this)); this.Drawn_ = true; redraw (this); endfunction ## -*- texinfo -*- ## @deftypefn {stats.chart.HeatmapChart} {} sortx (@var{h}) ## @deftypefnx {stats.chart.HeatmapChart} {} sortx (@var{h}, @var{row}) ## @deftypefnx {stats.chart.HeatmapChart} {} sortx (@var{h}, @var{row}, @var{direction}) ## @deftypefnx {stats.chart.HeatmapChart} {} sortx (@dots{}, @qcode{'MissingPlacement'}, @var{mp}) ## ## Reorder the columns of a heatmap. ## ## @code{sortx (@var{h})} puts the columns in the order of their names. ## @code{sortx (@var{h}, @var{row})} puts them in the order of their values ## in the row named @var{row}, an element of @code{YData}; a cell array of ## rows sorts by the first, then by the next where the first ties. ## @var{direction} is @qcode{'ascend'}, the default, or @qcode{'descend'}. ## Missing values go last when ascending and first when descending, ## unless @var{mp}, @qcode{'first'}, @qcode{'last'} or @qcode{'auto'}, says ## otherwise. The new order is set as @code{XDisplayData}. ## ## @end deftypefn function sortx (this, varargin) this.XDisplayData = hmSortOrder (this, varargin, true); endfunction ## -*- texinfo -*- ## @deftypefn {stats.chart.HeatmapChart} {} sorty (@var{h}) ## @deftypefnx {stats.chart.HeatmapChart} {} sorty (@var{h}, @var{column}) ## @deftypefnx {stats.chart.HeatmapChart} {} sorty (@var{h}, @var{column}, @var{direction}) ## @deftypefnx {stats.chart.HeatmapChart} {} sorty (@dots{}, @qcode{'MissingPlacement'}, @var{mp}) ## ## Reorder the rows of a heatmap, as @code{sortx} reorders its columns, by ## the values in the column named @var{column}. ## ## @end deftypefn function sorty (this, varargin) this.YDisplayData = hmSortOrder (this, varargin, false); endfunction ## -*- texinfo -*- ## @deftypefn {stats.chart.HeatmapChart} {@var{limits} =} xlim (@var{h}) ## @deftypefnx {stats.chart.HeatmapChart} {} xlim (@var{h}, @var{limits}) ## ## The first and last column shown, as @code{XLimits} holds them; given ## @var{limits}, set them. ## ## @end deftypefn function out = xlim (this, lims) if (nargin > 1) this.XLimits = lims; else out = this.XLimits; endif endfunction ## -*- texinfo -*- ## @deftypefn {stats.chart.HeatmapChart} {@var{limits} =} ylim (@var{h}) ## @deftypefnx {stats.chart.HeatmapChart} {} ylim (@var{h}, @var{limits}) ## ## The first and last row shown, as @code{YLimits} holds them; given ## @var{limits}, set them. ## ## @end deftypefn function out = ylim (this, lims) if (nargin > 1) this.YLimits = lims; else out = this.YLimits; endif endfunction ## -*- texinfo -*- ## @deftypefn {stats.chart.HeatmapChart} {} delete (@var{h}) ## ## Delete the heatmap and the axes it is drawn in. ## ## @end deftypefn function delete (this) ax = this.Axes_; cb = this.Colorbar_; this.Axes_ = []; this.Colorbar_ = []; this.Parent = []; if (! isempty (cb) && ishghandle (cb)) delete (cb); endif if (! isempty (ax) && ishghandle (ax) && strcmp (get (ax, 'beingdeleted'), 'off')) set (ax, 'deletefcn', ''); delete (ax); endif endfunction endmethods methods (Hidden) ## Let go of the axes when something other than the chart clears them, ## as a later plot does, leaving them to that plot. function release (this) if (this.Redrawing_ || isempty (this.Axes_)) return; endif ax = this.Axes_; this.Axes_ = []; this.Parent = []; if (ishghandle (ax)) set (ax, 'deletefcn', '', 'tag', ''); endif if (! isempty (this.Colorbar_) && ishghandle (this.Colorbar_)) delete (this.Colorbar_); endif this.Colorbar_ = []; endfunction endmethods methods (Access = private) ## Note a value the caller chose, so that later changes leave it alone. function chosen (this, name) switch (name) case 'XLimits' this.XLimitsAuto_ = false; case 'YLimits' this.YLimitsAuto_ = false; case 'ColorLimits' this.ColorLimitsAuto_ = false; case 'Title' this.TitleAuto_ = false; case 'XLabel' this.XLabelAuto_ = false; case 'YLabel' this.YLabelAuto_ = false; case 'ColorMethod' this.MethodAuto_ = false; endswitch endfunction ## Bring XData, YData and what follows them in line with ColorData. function fitData (this) [r, c] = size (this.ColorData); if (this.XDataAuto_) this.XData = hmNumbered (c); this.XDataAuto_ = true; endif if (this.YDataAuto_) this.YData = hmNumbered (r); this.YDataAuto_ = true; endif if (this.XDisplayAuto_) this.XDisplayData = this.XData; this.XDisplayAuto_ = true; endif if (this.YDisplayAuto_) this.YDisplayData = this.YData; this.YDisplayAuto_ = true; endif endfunction ## Compute ColorData, its names and the automatic labels from the table. function aggregate (this) t = this.SourceTable; if (this.Aggregating_ || isempty (t) || isempty (this.XVariable) || isempty (this.YVariable)) return; endif this.Aggregating_ = true; unwind_protect aggregateTable (this, t); unwind_protect_cleanup this.Aggregating_ = false; end_unwind_protect redraw (this); endfunction ## Read the table into ColorData, its names and the automatic labels. function aggregateTable (this, t) names = t.Properties.VariableNames; for v = {'XVariable', 'YVariable', 'ColorVariable'} n = this.(v{1}); if (! isempty (n) && ! any (strcmp (names, n))) error (strcat ("stats.chart.HeatmapChart: '%s' does not name a", ... " variable of the source table."), v{1}); endif endfor if (this.MethodAuto_) if (isempty (this.ColorVariable)) this.ColorMethod = 'count'; else this.ColorMethod = 'mean'; endif this.MethodAuto_ = true; endif method = this.ColorMethod; [gx, xn] = hmGroups (t.(this.XVariable), 'XVariable'); [gy, yn] = hmGroups (t.(this.YVariable), 'YVariable'); keep = gx > 0 & gy > 0; if (strcmp (method, 'count') || isempty (this.ColorVariable)) v = ones (numel (gx), 1); method = 'count'; else v = t.(this.ColorVariable); if (! (isnumeric (v) || islogical (v)) || ! isreal (v)) error (strcat ("stats.chart.HeatmapChart: 'ColorVariable' must", ... " name a real numeric variable of the source", ... " table.")); endif v = double (v(:)); endif C = hmAggregate (gy(keep), gx(keep), v(keep), numel (yn), ... numel (xn), method); ## Written straight to the fields: the setters would refuse ColorData ## on a table, and each would redraw drawn = this.Drawn_; this.Drawn_ = false; unwind_protect this.ColorData = C; this.XData = xn; this.YData = yn; this.XDataAuto_ = false; this.YDataAuto_ = false; this.XDisplayAuto_ = true; this.YDisplayAuto_ = true; fitData (this); if (this.TitleAuto_) this.Title = hmTableTitle (method, this.XVariable, ... this.YVariable, this.ColorVariable); endif if (this.XLabelAuto_) this.XLabel = this.XVariable; endif if (this.YLabelAuto_) this.YLabel = this.YVariable; endif unwind_protect_cleanup this.Drawn_ = drawn; end_unwind_protect endfunction ## Draw the chart from scratch into its axes. function redraw (this) if (! this.Drawn_ || isempty (this.Axes_) || ! ishghandle (this.Axes_)) return; endif ax = this.Axes_; this.Redrawing_ = true; unwind_protect if (! isempty (this.Colorbar_) && ishghandle (this.Colorbar_)) delete (this.Colorbar_); endif this.Colorbar_ = []; delete (get (ax, 'children')); unwind_protect_cleanup this.Redrawing_ = false; end_unwind_protect ## A child whose deletion by anyone else releases the axes text (ax, NaN, NaN, '', 'deletefcn', @(~, ~) release (this)); ## The columns and rows shown, within the limits xd = this.XDisplayData; yd = this.YDisplayData; xi = hmWithin (xd, this.XLimits); yi = hmWithin (yd, this.YLimits); C = this.ColorDisplayData; C = C(yi, xi); xl = this.XDisplayLabels; yl = this.YDisplayLabels; xl = xl(xi); yl = yl(yi); [nr, nc] = size (C); ## The values as colours, and the limits they follow until set V = hmScale (C, this.ColorScaling); if (this.ColorLimitsAuto_) this.Internal_ = true; unwind_protect this.ColorLimits = hmAutoLimits (V, this.ColorScaling); unwind_protect_cleanup this.Internal_ = false; end_unwind_protect endif set (ax, 'units', this.Units, 'position', this.Position, ... 'visible', this.Visible, 'color', this.MissingDataColor, ... 'ydir', 'reverse', 'xlim', [0.5, nc + 0.5], ... 'ylim', [0.5, nr + 0.5], 'xtick', 1:nc, 'ytick', 1:nr, ... 'xticklabel', xl, 'yticklabel', yl, 'ticklength', [0, 0], ... 'box', 'on', 'layer', 'top', 'clim', this.ColorLimits, ... 'fontname', this.FontName, 'fontsize', this.FontSize, ... 'xcolor', this.FontColor, 'ycolor', this.FontColor, ... 'ticklabelinterpreter', this.Interpreter); colormap (ax, this.Colormap); if (nr > 0 && nc > 0) image (ax, 'cdata', V, 'cdatamapping', 'scaled', ... 'alphadata', double (isfinite (V)), 'xdata', [1, nc], ... 'ydata', [1, nr], 'visible', this.Visible); endif ## Lines between the cells if (strcmp (this.GridVisible, 'on')) gx = [(1.5:nc)', (1.5:nc)'; zeros(0, 2)]; gy = [(1.5:nr)', (1.5:nr)'; zeros(0, 2)]; for k = 1:rows (gx) line (ax, gx(k,:), [0.5, nr + 0.5], 'color', [1, 1, 1], ... 'visible', this.Visible); endfor for k = 1:rows (gy) line (ax, [0.5, nc + 0.5], gy(k,:), 'color', [1, 1, 1], ... 'visible', this.Visible); endfor endif ## The value of each cell written into it if (! (ischar (this.CellLabelColor) && strcmp (this.CellLabelColor, 'none'))) cmap = this.Colormap; cl = this.ColorLimits; for i = 1:nr for j = 1:nc if (isnan (C(i,j))) s = this.MissingDataLabel; bg = this.MissingDataColor; else s = sprintf (this.CellLabelFormat, C(i,j)); bg = hmCellColor (V(i,j), cl, cmap, this.MissingDataColor); endif if (ischar (this.CellLabelColor)) fg = [0, 0, 0]; if (bg * [0.299; 0.587; 0.114] < 0.5) fg = [1, 1, 1]; endif else fg = this.CellLabelColor; endif text (ax, j, i, s, 'horizontalalignment', 'center', ... 'verticalalignment', 'middle', 'color', fg, ... 'fontname', this.FontName, 'fontsize', this.FontSize, ... 'interpreter', this.Interpreter, 'visible', this.Visible); endfor endfor endif title (ax, this.Title, 'color', this.FontColor, 'interpreter', ... this.Interpreter, 'fontname', this.FontName); xlabel (ax, this.XLabel, 'color', this.FontColor, 'interpreter', ... this.Interpreter, 'fontname', this.FontName); ylabel (ax, this.YLabel, 'color', this.FontColor, 'interpreter', ... this.Interpreter, 'fontname', this.FontName); ## The cells placed within the outer position, leaving the labels the ## room they take and the colour bar its own, or where they were put showBar = strcmp (this.ColorbarVisible, 'on'); if (strcmp (this.PositionConstraint, 'outerposition')) this.Internal_ = true; unwind_protect this.Position = hmFitInner (this.OuterPosition, ... get (ax, 'tightinset'), showBar); unwind_protect_cleanup this.Internal_ = false; end_unwind_protect set (ax, 'position', this.Position); else this.Internal_ = true; unwind_protect this.OuterPosition = hmOuterFromInner (this.Position); unwind_protect_cleanup this.Internal_ = false; end_unwind_protect endif ## The colour bar, placed beside the cells rather than taking room from ## them; it stays visible to handles, which printing needs if (showBar) p = this.Position; this.Colorbar_ = colorbar (ax, 'units', this.Units, 'position', ... [p(1) + p(3) + 0.02 * p(3), p(2), ... 0.04 * p(3), p(4)]); set (this.Colorbar_, 'visible', this.Visible, 'fontname', ... this.FontName, 'fontsize', this.FontSize, 'ycolor', ... this.FontColor); endif endfunction endmethods endclassdef ## The position of the cells within the outer position O: MATLAB's default ## margins, widened where the labels, whose extent TI gives, need more function p = hmFitInner (o, ti, bar) pad = 0.02; L = max (0.13 * o(3), ti(1) + pad * o(3)); B = max (0.11 * o(4), ti(2) + pad * o(4)); T = max (0.075 * o(4), ti(4) + pad * o(4)); if (bar) R = 0.137857 * o(3); else R = max (0.095 * o(3), ti(3) + pad * o(3)); endif w = max (o(3) - L - R, 0.01 * o(3)); h = max (o(4) - B - T, 0.01 * o(4)); p = [o(1) + L, o(2) + B, w, h]; endfunction ## The outer position that holds the cells at P function o = hmOuterFromInner (p) o = [0, 0, p(3) / 0.732143, p(4) / 0.815]; o(1) = p(1) - 0.13 * o(3); o(2) = p(2) - 0.11 * o(4); endfunction ## Default colormap: 256 colours from a pale blue to the first line colour function map = hmDefaultColormap () a = [0.9, 0.9447, 0.9741]; b = [0, 0.447, 0.741]; map = a + (0:255)' / 255 .* (b - a); endfunction ## The names '1', '2', ... up to N, as a column cell array function out = hmNumbered (n) out = arrayfun (@(k) sprintf ('%d', k), (1:n)', 'UniformOutput', false); endfunction ## A 2-D real numeric matrix function v = hmCheckMatrix (val) if (! ((isnumeric (val) || islogical (val)) && isreal (val) && ndims (val) == 2)) error (strcat ("stats.chart.HeatmapChart: 'ColorData' must be a", ... " 2-dimensional real numeric matrix.")); endif v = double (val); endfunction ## Distinct names as a column cell array of character vectors, from a cell ## array of text, a string array, a categorical array or numbers function out = hmCheckNames (val, name) out = hmText (val); if (isempty (out) && ! isempty (val)) error (strcat ("stats.chart.HeatmapChart: '%s' must be a vector of", ... " text, numbers or categories."), name); endif if (numel (unique (out)) != numel (out)) error ("stats.chart.HeatmapChart: '%s' holds duplicate values.", name); endif endfunction ## Values of any supported kind as a column cell array of text; numbers are ## written with %g, as MATLAB writes them function out = hmText (val) out = cell (0, 1); if (isempty (val)) return; elseif (iscellstr (val)) out = val(:); elseif (isa (val, 'string')) out = cellstr (val(:)); elseif (isa (val, 'categorical')) out = cellstr (val(:)); elseif ((isnumeric (val) || islogical (val)) && isvector (val)) out = arrayfun (@(v) sprintf ('%g', v), double (val(:)), ... 'UniformOutput', false); elseif (ischar (val) && isrow (val)) out = {val}; endif endfunction ## Labels: text, one for each of N values function out = hmCheckLabels (val, n, name) out = hmText (val); if (numel (out) != n) error (strcat ("stats.chart.HeatmapChart: '%s' must hold one label", ... " for each displayed value."), name); endif endfunction ## The labels of the displayed values: those set by hand, else the values function out = hmLabels (shown, map) out = shown; for k = 1:numel (shown) i = find (strcmp (map(:,1), shown{k}), 1); if (! isempty (i)) out{k} = map{i,2}; endif endfor endfunction ## Record LABELS against the displayed values DISP function map = hmSetLabels (map, shown, labels) for k = 1:numel (shown) i = find (strcmp (map(:,1), shown{k}), 1); if (isempty (i)) map(end+1,:) = {shown{k}, labels{k}}; else map{i,2} = labels{k}; endif endfor endfunction ## Limits: a pair of displayed values, the first not after the second function out = hmCheckLimits (val, shown, name) if (isempty (val) || isempty (shown)) out = hmFullLimits (shown); return; endif val = hmText (val); if (numel (val) != 2) error ("stats.chart.HeatmapChart: '%s' must hold two values.", name); endif i = find (strcmp (shown, val{1}), 1); j = find (strcmp (shown, val{2}), 1); if (isempty (i) || isempty (j) || i > j) error (strcat ("stats.chart.HeatmapChart: '%s' must name two", ... " displayed values, the first not after the second."), ... name); endif out = val(:)'; endfunction ## The first and last of the displayed values function out = hmFullLimits (shown) if (isempty (shown)) out = cell (1, 0); else out = {shown{1}, shown{end}}; endif endfunction ## Limits kept where they still fit the displayed values, reset otherwise, ## with a warning where they had been chosen function out = hmFitLimits (lims, shown, auto, name) full = hmFullLimits (shown); if (auto || isempty (lims)) out = full; return; endif i = find (strcmp (shown, lims{1}), 1); j = find (strcmp (shown, lims{2}), 1); if (isempty (i) || isempty (j) || i > j) warning (strcat ("stats.chart.HeatmapChart: the '%s' value does not", ... " fit the new display order and is reset to its", ... " full range."), name); out = full; else out = lims; endif endfunction ## The indices of the displayed values from the first limit to the second function idx = hmWithin (shown, lims) idx = 1:numel (shown); if (numel (lims) == 2) i = find (strcmp (shown, lims{1}), 1); j = find (strcmp (shown, lims{2}), 1); if (! isempty (i) && ! isempty (j)) idx = i:j; endif endif endfunction ## ColorData rearranged to the displayed columns and rows function M = hmDisplayMatrix (C, xd, yd, xs, ys) M = NaN (numel (ys), numel (xs)); [okx, jx] = ismember (xs, xd); [oky, jy] = ismember (ys, yd); if (! isempty (C)) M(oky, okx) = C(jy(oky), jx(okx)); endif endfunction ## The values as colours function V = hmScale (C, scaling) switch (scaling) case 'scaledcolumns' lo = min (C, [], 1); span = max (C, [], 1) - lo; span(span == 0) = 1; V = (C - lo) ./ span; case 'scaledrows' lo = min (C, [], 2); span = max (C, [], 2) - lo; span(span == 0) = 1; V = (C - lo) ./ span; case 'log' if (any (C(:) < 0)) warning ("stats.chart.HeatmapChart: negative color data ignored."); endif V = C; V(V < 0) = NaN; V = log (V); otherwise V = C; endswitch endfunction ## The colour limits the values shown call for function cl = hmAutoLimits (V, scaling) if (any (strcmp (scaling, {'scaledcolumns', 'scaledrows'}))) cl = [0, 1]; return; endif v = V(isfinite (V)); if (isempty (v)) cl = [0, 1]; elseif (min (v) == max (v)) cl = min (v) + [-1, 1]; else cl = [min(v), max(v)]; endif endfunction ## The colour a value takes, for the colour of its label function c = hmCellColor (v, cl, cmap, missing) if (! isfinite (v)) c = missing; return; endif k = round ((v - cl(1)) / (cl(2) - cl(1)) * (rows (cmap) - 1)) + 1; c = cmap(min (max (k, 1), rows (cmap)),:); endfunction ## Variable names: a character vector or a string scalar function out = hmCheckVarName (val, name) if (isa (val, 'string') && isscalar (val)) val = char (val); endif if (! (ischar (val) && (isrow (val) || isempty (val)))) error (strcat ("stats.chart.HeatmapChart: '%s' must be a character", ... " vector."), name); endif out = val; endfunction ## Text: a character vector, a string, or a cell array of them for lines function out = hmCheckText (val, name) if (isa (val, 'string')) val = cellstr (val); if (numel (val) == 1) val = val{1}; endif endif if (! ((ischar (val) && (isrow (val) || isempty (val))) || iscellstr (val))) error ("stats.chart.HeatmapChart: '%s' must be text.", name); endif out = val; if (iscell (out)) out = out(:); endif endfunction ## One of a list of names, case free function out = hmCheckOneOf (val, list, name) if (isa (val, 'string') && isscalar (val)) val = char (val); endif if (! (ischar (val) && any (strcmpi (val, list)))) error ("stats.chart.HeatmapChart: '%s' must be one of %s.", name, ... strjoin (strcat ("'", list, "'"), ', ')); endif out = lower (val); endfunction ## A colour, as an RGB triplet or a name; 'auto' and 'none' too if AUTO function out = hmCheckColor (val, name, auto) if (isa (val, 'string') && isscalar (val)) val = char (val); endif if (auto && ischar (val) && any (strcmpi (val, {'auto', 'none'}))) out = lower (val); return; endif if (ischar (val)) names = {'red', 'green', 'blue', 'cyan', 'magenta', 'yellow', 'black', ... 'white', 'r', 'g', 'b', 'c', 'm', 'y', 'k', 'w'}; rgb = [1 0 0; 0 1 0; 0 0 1; 0 1 1; 1 0 1; 1 1 0; 0 0 0; 1 1 1]; i = find (strcmpi (val, names), 1); if (! isempty (i)) out = rgb(mod (i - 1, 8) + 1,:); return; endif elseif (isnumeric (val) && isreal (val) && numel (val) == 3 && all (val(:) >= 0) && all (val(:) <= 1)) out = double (val(:)'); return; endif error (strcat ("stats.chart.HeatmapChart: '%s' must be an RGB", ... " triplet or a colour name."), name); endfunction ## A position, as a 1-by-4 vector with positive width and height function out = hmCheckPosition (val, name) if (! (isnumeric (val) && isreal (val) && numel (val) == 4 && all (isfinite (val)) && val(3) > 0 && val(4) > 0)) error (strcat ("stats.chart.HeatmapChart: '%s' must be a 1-by-4", ... " vector with positive width and height."), name); endif out = double (val(:)'); endfunction ## The group of each row of a table variable and the names of the groups, ## 0 for a missing value: categories in their order, unused ones included; ## numbers, text and logical values sorted function [g, names] = hmGroups (v, name) v = v(:); if (isa (v, 'categorical')) names = categories (v); [~, g] = ismember (cellstr (v), names); g(isundefined (v)) = 0; elseif (isnumeric (v) || islogical (v)) if (islogical (v)) u = unique (v); names = cell (numel (u), 1); names(u == 0) = {'false'}; names(u == 1) = {'true'}; [~, g] = ismember (v, u); else ok = ! isnan (v); u = unique (v(ok)); names = arrayfun (@(x) sprintf ('%g', x), u, 'UniformOutput', false); g = zeros (numel (v), 1); [~, g(ok)] = ismember (v(ok), u); endif elseif (iscellstr (v) || isa (v, 'string')) t = cellstr (v); ok = ! cellfun (@isempty, t); if (isa (v, 'string')) ok &= ! ismissing (v); endif names = unique (t(ok)); g = zeros (numel (t), 1); [~, g(ok)] = ismember (t(ok), names); else error (strcat ("stats.chart.HeatmapChart: '%s' must name a variable", ... " of categories, numbers, text or logical values."), name); endif names = names(:); endfunction ## The values V of the rows falling in each cell, combined by METHOD function C = hmAggregate (gy, gx, v, nr, nc, method) if (strcmp (method, 'count')) C = accumarray ([gy, gx], 1, [nr, nc]); return; endif if (strcmp (method, 'none')) n = accumarray ([gy, gx], 1, [nr, nc]); if (any (n(:) > 1)) error (strcat ("stats.chart.HeatmapChart: 'ColorMethod' 'none'", ... " cannot combine the several rows of the table that", ... " fall in one cell.")); endif C = NaN (nr, nc); C(sub2ind ([nr, nc], gy, gx)) = v; return; endif ## Missing values left out; a cell with none left is missing, but sums to ## zero, as MATLAB sums it ok = ! isnan (v); fill = NaN; switch (method) case 'mean' f = @(x) mean (x); case 'median' f = @(x) median (x); case 'sum' f = @(x) sum (x); fill = 0; case 'min' f = @(x) min (x); case 'max' f = @(x) max (x); endswitch C = accumarray ([gy(ok), gx(ok)], v(ok), [nr, nc], f, fill); endfunction ## The title a table chart takes until one is set function t = hmTableTitle (method, xv, yv, cv) switch (method) case 'count' t = sprintf ('Count of %s vs. %s', yv, xv); case 'mean' t = sprintf ('Mean of %s', cv); case 'median' t = sprintf ('Median of %s', cv); case 'sum' t = sprintf ('Sum of %s', cv); case 'min' t = sprintf ('Minimum of %s', cv); case 'max' t = sprintf ('Maximum of %s', cv); otherwise t = cv; endswitch endfunction ## The new display order sortx and sorty ask for function order = hmSortOrder (this, args, byColumns) if (byColumns) shown = this.XDisplayData; other = this.YData; otherDisp = this.YDisplayData; fname = 'sortx'; else shown = this.YDisplayData; other = this.XData; otherDisp = this.XDisplayData; fname = 'sorty'; endif mp = 'auto'; isMP = @(a) (ischar (a) || isa (a, 'string')) ... && strcmpi (a, 'MissingPlacement'); k = find (cellfun (isMP, args), 1); if (! isempty (k)) if (k == numel (args)) error ("%s: 'MissingPlacement' needs a value.", fname); endif mp = hmCheckOneOf (args{k+1}, {'auto', 'first', 'last'}, ... 'MissingPlacement'); args(k:k+1) = []; endif if (numel (args) > 2) error ("%s: too many input arguments.", fname); endif direction = 'ascend'; if (numel (args) > 1) direction = hmCheckOneOf (args{2}, {'ascend', 'descend'}, 'direction'); endif if (isempty (args) || isempty (args{1})) [~, i] = sort (shown); if (strcmp (direction, 'descend')) i = flipud (i(:)); endif order = shown(i); return; endif ## The values of the named rows (or columns), one column of keys each keys = hmText (args{1}); if (isempty (keys) || ! all (ismember (keys, [other; otherDisp]))) error (strcat ("%s: every element to sort by must be in 'XData' or", ... " 'XDisplayData'."), fname); endif M = hmDisplayMatrix (this.ColorData, this.XData, this.YData, ... this.XDisplayData, this.YDisplayData); if (byColumns) [~, r] = ismember (keys, this.YDisplayData); K = M(r,:)'; else [~, c] = ismember (keys, this.XDisplayData); K = M(:,c); endif if (strcmp (mp, 'auto')) mp = 'last'; if (strcmp (direction, 'descend')) mp = 'first'; endif endif ## Missing values placed as asked, then sorted by each key in turn if (strcmp (direction, 'descend')) K = -K; endif if (strcmp (mp, 'first')) K(isnan (K)) = -Inf; else K(isnan (K)) = Inf; endif [~, i] = sortrows ([K, (1:rows (K))']); order = shown(i); endfunction %!shared tbl %! tbl = table (categorical ({'a';'b';'a';'c';'b';'a';'c';'c'}), ... %! categorical ({'u';'u';'v';'v';'u';'u';'v';'u'}), ... %! [1;2;3;4;5;6;7;8], 'VariableNames', {'X', 'Y', 'V'}); %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap (magic (4)); %! assert_equal (class (h), 'stats.chart.HeatmapChart'); %! assert_equal (h.XData, {'1'; '2'; '3'; '4'}); %! assert_equal (h.YData, {'1'; '2'; '3'; '4'}); %! assert_equal (h.ColorData, magic (4)); %! assert_equal (h.ColorDisplayData, magic (4)); %! assert_equal (h.ColorLimits, [1, 16]); %! assert_equal (h.ColorMethod, 'none'); %! assert_equal (h.ColorScaling, 'scaled'); %! assert_equal (h.CellLabelFormat, '%0.4g'); %! assert_equal (h.CellLabelColor, 'auto'); %! assert_equal (h.MissingDataColor, [0.15, 0.15, 0.15]); %! assert_equal (h.MissingDataLabel, 'NaN'); %! assert_equal (h.FontSize, 10); %! assert_equal (h.Position, [0.13, 0.11, 0.732143, 0.815], -1e-12); %! assert_equal (h.OuterPosition, [0, 0, 1, 1]); %! assert_equal (h.Parent, hf); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap (magic (3)); %! assert_equal (size (h.Colormap), [256, 3]); %! assert_equal (h.Colormap(1,:), [0.9, 0.9447, 0.9741]); %! assert_equal (h.Colormap(2,:), ... %! [0.896470588235294, 0.942748235294118, ... %! 0.973185882352941], -1e-14); %! assert_equal (h.Colormap(end,:), [0, 0.447, 0.741], -1e-15); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap ({'a', 'b', 'c'}, {'r1', 'r2'}, [1, 2, 3; 4, 5, 6]); %! assert_equal (h.XData, {'a'; 'b'; 'c'}); %! assert_equal (h.YData, {'r1'; 'r2'}); %! assert_equal (h.XDisplayLabels, {'a'; 'b'; 'c'}); %! assert_equal (h.XLimits, {'a', 'c'}); %! assert_equal (h.ColorLimits, [1, 6]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap ([1, 2, 3], [10, 20], [1, 2, 3; 4, 5, 6]); %! assert_equal (h.XData, {'1'; '2'; '3'}); %! assert_equal (h.YData, {'10'; '20'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap (tbl, 'X', 'Y'); %! assert_equal (h.ColorData, [2, 2, 1; 1, 0, 2]); %! assert_equal (h.XData, {'a'; 'b'; 'c'}); %! assert_equal (h.YData, {'u'; 'v'}); %! assert_equal (h.Title, 'Count of Y vs. X'); %! assert_equal (h.XLabel, 'X'); %! assert_equal (h.YLabel, 'Y'); %! assert_equal (h.ColorMethod, 'count'); %! assert_equal (h.ColorVariable, ''); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap (tbl, 'X', 'Y', 'ColorVariable', 'V'); %! assert_equal (h.ColorData, [3.5, 3.5, 8; 3, NaN, 5.5]); %! assert_equal (h.Title, 'Mean of V'); %! assert_equal (h.ColorMethod, 'mean'); %! h.ColorMethod = 'sum'; %! assert_equal (h.ColorData, [7, 7, 8; 3, 0, 11]); %! assert_equal (h.Title, 'Sum of V'); %! h.ColorMethod = 'median'; %! assert_equal (h.ColorData, [3.5, 3.5, 8; 3, NaN, 5.5]); %! assert_equal (h.Title, 'Median of V'); %! h.ColorMethod = 'max'; %! assert_equal (h.ColorData, [6, 5, 8; 3, NaN, 7]); %! assert_equal (h.Title, 'Maximum of V'); %! h.ColorMethod = 'min'; %! assert_equal (h.Title, 'Minimum of V'); %! h.ColorMethod = 'count'; %! assert_equal (h.Title, 'Count of Y vs. X'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! t = table (categorical ({'a';'b';'c';'a'}), ... %! categorical ({'u';'u';'v';'v'}), [1;2;3;4], ... %! 'VariableNames', {'X', 'Y', 'V'}); %! h = heatmap (t, 'X', 'Y', 'ColorVariable', 'V', 'ColorMethod', 'none'); %! assert_equal (h.ColorData, [1, 2, NaN; 4, NaN, 3]); %! assert_equal (h.Title, 'V'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! t = table ({'a';'a';'a';'b';'b'}, {'u';'u';'u';'u';'u'}, ... %! [1;NaN;5;2;4], 'VariableNames', {'X', 'Y', 'V'}); %! h = heatmap (t, 'X', 'Y', 'ColorVariable', 'V'); %! assert_equal (h.ColorData, [3, 3]); %! h.ColorMethod = 'count'; %! assert_equal (h.ColorData, [3, 2]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! t = table (categorical ({'lo';'hi';'lo'}, {'lo', 'mid', 'hi'}), ... %! categorical ({'u';'u';'v'}), [1;2;3], ... %! 'VariableNames', {'X', 'Y', 'V'}); %! h = heatmap (t, 'X', 'Y'); %! assert_equal (h.XData, {'lo'; 'mid'; 'hi'}); %! assert_equal (h.ColorData, [1, 0, 1; 1, 0, 0]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! t = table ([0.5; -2; 1e6; NaN; 3], {'u';'v';'u';'u';''}, ... %! [1;2;3;4;5], 'VariableNames', {'X', 'Y', 'V'}); %! h = heatmap (t, 'X', 'Y'); %! assert_equal (h.XData, {'-2'; '0.5'; '3'; '1e+06'}); %! assert_equal (h.YData, {'u'; 'v'}); %! assert_equal (h.ColorData, [0, 1, 0, 1; 1, 0, 0, 0]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! t = table ([3;1;2;1;3], {'p';'q';'p';'p';'q'}, [true; false; true; ... %! true; false], 'VariableNames', {'N', 'S', 'L'}); %! h = heatmap (t, 'N', 'S'); %! assert_equal (h.XData, {'1'; '2'; '3'}); %! assert_equal (h.ColorData, [1, 1, 1; 1, 0, 1]); %! h.XVariable = 'L'; %! assert_equal (h.XData, {'false'; 'true'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap ([1, NaN, 3; 4, 5, NaN]); %! assert_equal (h.ColorLimits, [1, 5]); %! assert_equal (h.ColorDisplayData, [1, NaN, 3; 4, 5, NaN]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap ([1, 2, 3; 4, 5, 16]); %! h.ColorScaling = 'scaledcolumns'; %! assert_equal (h.ColorDisplayData, [1, 2, 3; 4, 5, 16]); %! assert_equal (h.ColorLimits, [0, 1]); %! h.ColorScaling = 'scaledrows'; %! assert_equal (h.ColorLimits, [0, 1]); %! h.ColorScaling = 'log'; %! assert_equal (h.ColorLimits, [0, log(16)], -1e-15); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap ([2, 2; 2, 2]); %! assert_equal (h.ColorLimits, [1, 3]); %! h.ColorData = [NaN, NaN; NaN, NaN]; %! assert_equal (h.ColorLimits, [0, 1]); %! h.ColorData = [5, 6; 7, 50]; %! assert_equal (h.ColorLimits, [5, 50]); %! h.ColorLimits = [0, 20]; %! h.ColorData = [1, 100; 2, 3]; %! assert_equal (h.ColorLimits, [0, 20]); %! h.ColorScaling = 'scaledcolumns'; %! assert_equal (h.ColorLimits, [0, 20]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!warning ... %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap ([1, -2, 3; 4, 5, 6]); %! h.ColorScaling = 'log'; %! assert_equal (h.ColorLimits, [0, log(6)], -1e-15); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap ({'c', 'a', 'b'}, {'y', 'x'}, [3, 1, 2; 6, 4, 5]); %! sortx (h); %! assert_equal (h.XDisplayData, {'a'; 'b'; 'c'}); %! assert_equal (h.ColorDisplayData, [1, 2, 3; 4, 5, 6]); %! sortx (h, 'x', 'descend'); %! assert_equal (h.XDisplayData, {'c'; 'b'; 'a'}); %! assert_equal (h.ColorDisplayData, [3, 2, 1; 6, 5, 4]); %! sorty (h, 'a'); %! assert_equal (h.YDisplayData, {'y'; 'x'}); %! sorty (h); %! assert_equal (h.YDisplayData, {'x'; 'y'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap ({'c', 'a', 'b'}, {'y', 'x'}, [3, NaN, 2; 6, 4, 5]); %! sortx (h, 'y'); %! assert_equal (h.XDisplayData, {'b'; 'c'; 'a'}); %! sortx (h, 'y', 'descend'); %! assert_equal (h.XDisplayData, {'a'; 'c'; 'b'}); %! sortx (h, 'y', 'ascend', 'MissingPlacement', 'first'); %! assert_equal (h.XDisplayData, {'a'; 'b'; 'c'}); %! sortx (h, {'y', 'x'}); %! assert_equal (h.XDisplayData, {'b'; 'c'; 'a'}); %! sorty (h, 'b', 'descend'); %! assert_equal (h.YDisplayData, {'x'; 'y'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap ({'a', 'b', 'c'}, {'r1', 'r2'}, [1, 2, 3; 4, 5, 6]); %! h.XDisplayData = {'c'; 'a'}; %! assert_equal (h.ColorDisplayData, [3, 1; 6, 4]); %! h.XDisplayLabels = {'C'; 'A'}; %! assert_equal (h.XDisplayLabels, {'C'; 'A'}); %! assert_equal (h.XDisplayData, {'c'; 'a'}); %! h.XDisplayData = {'a'; 'c'; 'b'}; %! assert_equal (h.XDisplayLabels, {'A'; 'C'; 'b'}); %! h.XDisplayData = {'9'}; %! assert_equal (h.ColorDisplayData, [NaN; NaN]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap ({'c', 'a', 'b'}, {'y', 'x'}, [3, 1, 2; 6, 4, 5]); %! assert_equal (xlim (h), {'c', 'b'}); %! xlim (h, {'a', 'b'}); %! assert_equal (h.XLimits, {'a', 'b'}); %! assert_equal (h.XDisplayData, {'c'; 'a'; 'b'}); %! assert_equal (ylim (h), {'y', 'x'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!warning ... %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap ({'c', 'a', 'b'}, {'y', 'x'}, [3, 1, 2; 6, 4, 5]); %! xlim (h, {'a', 'b'}); %! h.XDisplayData = {'b'; 'c'; 'a'}; %! assert_equal (h.XLimits, {'b', 'a'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap ({'c', 'a', 'b'}, {'y', 'x'}, [3, 1, 2; 6, 4, 5]); %! sortx (h); %! sorty (h); %! lastwarn (''); %! sortx (h, 'x', 'descend'); %! sorty (h, 'a'); %! assert_equal (isempty (strfind (lastwarn (), 'HeatmapChart')), true); %! assert_equal (h.XLimits, {'c', 'a'}); %! assert_equal (h.YLimits, {'y', 'x'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap ([1, 2; 3, 4]); %! h.ColorData = [1, 2, 3; 4, 5, 6]; %! assert_equal (h.XData, {'1'; '2'; '3'}); %! h.XData = {'p'; 'q'; 'r'}; %! assert_equal (h.XDisplayData, {'p'; 'q'; 'r'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap (tbl, 'X', 'Y', 'ColorVariable', 'V'); %! h.Title = 'Mine'; %! h.ColorMethod = 'sum'; %! assert_equal (h.Title, 'Mine'); %! h.XLabel = 'XX'; %! h.XVariable = 'V'; %! assert_equal (h.XLabel, 'XX'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap (magic (3), 'Colormap', gray (5), 'ColorbarVisible', 'off', ... %! 'CellLabelColor', 'none', 'GridVisible', 'off', ... %! 'Title', 'T', 'XLabel', 'XL', 'YLabel', 'YL', ... %! 'FontSize', 14, 'CellLabelFormat', '%.2f'); %! assert_equal (h.Colormap, gray (5)); %! assert_equal (h.ColorbarVisible, 'off'); %! assert_equal (h.CellLabelColor, 'none'); %! assert_equal (h.GridVisible, 'off'); %! assert_equal (h.Title, 'T'); %! assert_equal (h.FontSize, 14); %! assert_equal (h.CellLabelFormat, '%.2f'); %! h.Title = {'one', 'two'}; %! assert_equal (h.Title, {'one'; 'two'}); %! h.MissingDataColor = 'r'; %! assert_equal (h.MissingDataColor, [1, 0, 0]); %! h.FontColor = 'g'; %! assert_equal (h.FontColor, [0, 1, 0]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap (magic (3)); %! ax = findall (hf, 'type', 'axes', 'tag', 'stats.chart.HeatmapChart'); %! assert_equal (numel (ax), 1); %! assert_equal (gca (), ax); %! im = findall (ax, 'type', 'image'); %! assert_equal (get (im, 'cdata'), magic (3)); %! assert_equal (get (ax, 'clim'), [1, 9]); %! assert_equal (numel (findall (ax, 'type', 'text', 'string', '5')), 1); %! delete (h); %! tag = 'stats.chart.HeatmapChart'; %! assert_equal (isempty (findall (hf, 'tag', tag)), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! hp = uipanel (hf); %! h = heatmap (hp, magic (3)); %! assert_equal (h.Parent, hp); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!shared hmS, hmT %! hmS = struct ('ColorData', magic (3), 'XData', {{}}, 'YData', {{}}, ... %! 'SourceTable', [], 'XVariable', '', 'YVariable', '', ... %! 'ColorVariable', ''); %! hmT = struct ('ColorData', [], 'XData', {{}}, 'YData', {{}}, ... %! 'SourceTable', table ({'a'; 'a'}, {'u'; 'u'}, [1; 2], ... %! 'VariableNames', {'X', 'Y', 'V'}), ... %! 'XVariable', 'X', 'YVariable', 'Y', 'ColorVariable', 'V'); %!error ... %! stats.chart.HeatmapChart ([]) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'ColorData', ones(2, 2, 2)}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'XData', {'a', 'a', 'b'}}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'XData', {1, 2}}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'XDisplayLabels', {'a'}}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'XLimits', {'1'}}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'XLimits', {'3', '1'}}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'SourceTable', 5}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'ColorVariable', 5}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'ColorMethod', 'mode'}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'ColorScaling', 'foo'}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'ColorLimits', [5, 1]}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'Colormap', 'jet'}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'ColorbarVisible', 'yes'}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'MissingDataColor', 'auto'}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'MissingDataLabel', 5}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'CellLabelFormat', 5}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'CellLabelColor', [2, 0, 0]}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'GridVisible', 1}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'Title', 5}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'FontSize', 0}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'FontColor', 'none'}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'Interpreter', 'html'}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'Position', [0, 0, 0, 1]}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'Units', 'miles'}) %!error ... %! stats.chart.HeatmapChart ([], hmS, {'Visible', 'no'}) %!error ... %! stats.chart.HeatmapChart ([], hmT, {'XVariable', 'Q'}) %!error ... %! stats.chart.HeatmapChart ([], hmT, {'ColorMethod', 'none'}) %!error ... %! stats.chart.HeatmapChart ([], hmT, {'ColorVariable', 'X'}) statistics-release-2.1.2/inst/Plotting/+stats/+chart/ParallelCoordinatesPlot.m000066400000000000000000001537431527311104300274440ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftp {statistics} stats.chart.ParallelCoordinatesPlot ## ## A parallel coordinates plot, as @code{parallelplot} draws it. ## ## A @code{stats.chart.ParallelCoordinatesPlot} holds observations of several ## variables, optionally split into groups, and the choices they are drawn ## with, and redraws itself whenever one of them is set. It is what ## @code{parallelplot} returns, and the documented way to reach it. Each ## variable is a vertical ruler, a coordinate, and each observation a line ## joining its values across the rulers. ## ## The observations come either from a numeric matrix, @code{Data}, whose ## columns @code{CoordinateData} chooses, or from a table, ## @code{SourceTable}, whose variables @code{CoordinateVariables} chooses, ## never from both. A table variable of categories, text or logical values ## places its distinct values evenly along its ruler, whatever ## @code{DataNormalization} says, and @code{Jitter} spreads the lines passing ## through each value. A missing value breaks its line. The groups take ## the colours of the colour order, in the order in which each first ## appears in @code{GroupData}. ## ## A chart takes the place of the current axes of its parent, keeping their ## outer position, and refuses axes on which @code{hold} is on; a later plot ## into the figure takes the place of the chart in turn. MATLAB's chart is ## a graphics object of its own; this one is a handle class drawing into ## axes of its own, made current, and it lets go of them when a later plot ## clears them. Octave's lines carry no transparency, so @code{LineAlpha} ## blends each line's colour toward the background of the axes instead, ## which matches MATLAB where lines do not cross. @code{LegendVisible} and ## @code{Visible} hold @qcode{'on'} or @qcode{'off'}, and @code{LineStyle} ## and @code{MarkerStyle} a character vector or a cell array of them, where ## MATLAB holds an on-off state and a string array. ## ## @seealso{parallelplot, parallelcoords} ## @end deftp classdef ParallelCoordinatesPlot < handle properties (Access = public) ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} Data ## ## The observations, a numeric matrix with one row for each observation ## and one column for each variable; empty where the chart was drawn from ## a table. ## ## @end deftp Data = []; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} CoordinateData ## ## The columns of @code{Data} drawn as coordinates, in the order drawn, ## as indices or a logical vector; all of them by default. Setting it on ## a chart drawn from a table is refused. ## ## @end deftp CoordinateData = []; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} SourceTable ## ## The table the chart was drawn from, or empty. ## ## @end deftp SourceTable = []; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} CoordinateVariables ## ## The variables of @code{SourceTable} drawn as coordinates, in the order ## drawn, as names, indices or a logical vector, kept in the form given; ## all of them by default. Setting it on a chart drawn from a matrix is ## refused. ## ## @end deftp CoordinateVariables = {}; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} GroupData ## ## The group of each observation: a vector with one element for each row ## of @code{Data} of categories, text, numbers or logical values, or ## empty for no groups. ## ## @end deftp GroupData = []; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} GroupVariable ## ## The variable of @code{SourceTable} holding the groups, or empty. ## ## @end deftp GroupVariable = ''; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} DataNormalization ## ## How the values are placed along the rulers: @qcode{'range'}, the ## default, as they are on rulers with limits of their own; ## @qcode{'none'} as they are on rulers sharing one scale; ## @qcode{'zscore'} as z-scores; @qcode{'scale'} divided by the standard ## deviation; @qcode{'center'} less the mean; @qcode{'norm'} divided by ## the 2-norm of the variable. All but @qcode{'range'} share one scale. ## ## @end deftp DataNormalization = 'range'; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} CoordinateTickLabels ## ## The label of each coordinate, a column cell array; by default the ## numbers of the columns or the names of the variables. ## ## @end deftp CoordinateTickLabels = cell (0, 1); ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} Jitter ## ## How far the lines through one value of a variable of categories are ## spread, from 0 to 1, where 1 fills the room up to the next value. 0.1 ## by default. ## ## @end deftp Jitter = 0.1; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} Color ## ## The colour of each group, one row of an RGB matrix for each; by ## default the colours of the colour order. ## ## @end deftp Color = [0, 0.447, 0.741]; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} LineStyle ## ## The line style of each group, a character vector or a cell array of ## them, @qcode{'-'} by default. ## ## @end deftp LineStyle = '-'; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} LineWidth ## ## The width of the lines, a scalar or one for each group, 1 by default. ## ## @end deftp LineWidth = 1; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} LineAlpha ## ## How opaque the lines are, from 0 to 1, a scalar or one for each group, ## 0.7 by default. ## ## @end deftp LineAlpha = 0.7; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} MarkerStyle ## ## The marker at each value, for each group, @qcode{'none'} by default. ## ## @end deftp MarkerStyle = 'none'; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} MarkerSize ## ## The size of the markers in points, a scalar or one for each group, 6 ## by default. ## ## @end deftp MarkerSize = 6; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} LegendVisible ## ## Whether the legend of the groups is shown, @qcode{'on'} or ## @qcode{'off'}; on where there are groups, until set. ## ## @end deftp LegendVisible = 'off'; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} LegendTitle ## ## The title of the legend; from a table the name of ## @code{GroupVariable}, until set. ## ## @end deftp LegendTitle = ''; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} CoordinateLabel ## ## The label under the coordinates. ## ## @end deftp CoordinateLabel = ''; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} DataLabel ## ## The label beside the rulers. ## ## @end deftp DataLabel = ''; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} Title ## ## The title of the chart. ## ## @end deftp Title = ''; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} FontName ## ## The font of every text on the chart, @qcode{'Helvetica'} by default. ## ## @end deftp FontName = 'Helvetica'; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} FontSize ## ## The size of every text on the chart, in points, 10 by default. ## ## @end deftp FontSize = 10; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} Position ## ## The position of the rulers in their parent, as ## @code{[left, bottom, width, height]} in @code{Units}. While ## @code{PositionConstraint} is @qcode{'outerposition'} the chart places ## them within @code{OuterPosition} itself; setting it fixes them there ## and turns @code{PositionConstraint} to @qcode{'innerposition'}. ## @code{InnerPosition} is the same. ## ## @end deftp Position = [0.13, 0.11, 0.775, 0.815]; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} OuterPosition ## ## The position of the whole chart in its parent, labels included, ## @code{[0, 0, 1, 1]} by default, or the outer position of the axes it ## took the place of. ## ## @end deftp OuterPosition = [0, 0, 1, 1]; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} PositionConstraint ## ## Which position the chart keeps: @qcode{'outerposition'}, the default, ## or @qcode{'innerposition'}. ## ## @end deftp PositionConstraint = 'outerposition'; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} Units ## ## The units of the positions, @qcode{'normalized'} by default. ## ## @end deftp Units = 'normalized'; ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} Visible ## ## Whether the chart is shown, @qcode{'on'} by default or @qcode{'off'}. ## ## @end deftp Visible = 'on'; endproperties properties (Dependent) ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} InnerPosition ## ## The same as @code{Position}. ## ## @end deftp InnerPosition; endproperties properties (GetAccess = public, SetAccess = private) ## -*- texinfo -*- ## @deftp {stats.chart.ParallelCoordinatesPlot} {property} Parent ## ## The figure or panel the chart is placed in. This property is ## read-only. ## ## @end deftp Parent = []; endproperties properties (Access = private, Hidden) Axes_ = []; Legend_ = []; Drawn_ = false; Redrawing_ = false; Internal_ = false; # true while the chart sets a value itself Filling_ = false; # true while the constructor sets the data StyleAuto_ = true; # colours and styles follow the groups LegendAuto_ = true; LabelsAuto_ = true; # CoordinateTickLabels follow the coordinates TitleAuto_ = true; # LegendTitle follows GroupVariable Offsets_ = []; # the jitter of each observation, drawn once endproperties methods (Hidden) function disp (this) printf (' ParallelCoordinatesPlot with properties:\n\n'); if (isempty (this.SourceTable)) printf ('%14s: [%dx%d double]\n', 'Data', size (this.Data)); printf ('%14s: %s\n', 'CoordinateData', mat2str (this.CoordinateData)); if (isempty (this.GroupData)) printf ('%14s: []\n', 'GroupData'); else printf ('%14s: [%dx1 %s]\n', 'GroupData', numel (this.GroupData), ... class (this.GroupData)); endif else printf ('%19s: [%dx%d table]\n', 'SourceTable', ... size (this.SourceTable)); cv = this.CoordinateVariables; if (iscellstr (cv)) cv = ['{', strjoin(strcat ("'", cv, "'"), ' '), '}']; else cv = mat2str (cv); endif printf ('%19s: %s\n', 'CoordinateVariables', cv); printf ('%19s: ''%s''\n', 'GroupVariable', this.GroupVariable); endif printf ('\n'); endfunction function display (this) disp (this); endfunction endmethods methods (Hidden) function set.Data (this, val) if (! ((isnumeric (val) || islogical (val)) && isreal (val) && ndims (val) == 2)) error (strcat ("stats.chart.ParallelCoordinatesPlot: 'Data' must", ... " be a real numeric matrix.")); endif if (this.Drawn_ && ! isempty (this.SourceTable)) error (strcat ("stats.chart.ParallelCoordinatesPlot: setting", ... " 'Data' while 'SourceTable' holds a table is not", ... " supported.")); endif this.Data = double (val); if (! this.Filling_) this.CoordinateData = 1:columns (this.Data); endif endfunction function set.CoordinateData (this, val) if (! isempty (this.SourceTable)) error (strcat ("stats.chart.ParallelCoordinatesPlot: setting", ... " 'CoordinateData' while 'SourceTable' holds a", ... " table is not supported.")); endif this.CoordinateData = pcCheckIndex (val, columns (this.Data), ... 'CoordinateData'); refit (this); endfunction function set.SourceTable (this, val) if (! (isempty (val) || istable (val))) error (strcat ("stats.chart.ParallelCoordinatesPlot:", ... " 'SourceTable' must be a table.")); endif this.SourceTable = val; if (! this.Filling_ && ! isempty (val)) this.CoordinateVariables = val.Properties.VariableNames; endif endfunction function set.CoordinateVariables (this, val) t = this.SourceTable; if (isempty (t)) error (strcat ("stats.chart.ParallelCoordinatesPlot: setting", ... " 'CoordinateVariables' after setting 'Data' is", ... " not supported.")); endif names = t.Properties.VariableNames; if (isa (val, 'string')) val = cellstr (val); elseif (ischar (val)) val = {val}; endif if (iscellstr (val)) if (! all (ismember (val, names))) error (strcat ("stats.chart.ParallelCoordinatesPlot:", ... " 'CoordinateVariables' does not name variables", ... " of the source table.")); endif val = val(:)'; else pcCheckIndex (val, numel (names), 'CoordinateVariables'); endif this.CoordinateVariables = val; refit (this); endfunction function set.GroupData (this, val) if (! isempty (val) && ! isvector (val)) error (strcat ("stats.chart.ParallelCoordinatesPlot: 'GroupData'", ... " must be a vector.")); endif if (isempty (val)) this.GroupData = []; else this.GroupData = val(:); endif refit (this); endfunction function set.GroupVariable (this, val) if (isa (val, 'string') && isscalar (val)) val = char (val); endif if (! (ischar (val) && (isrow (val) || isempty (val)))) error (strcat ("stats.chart.ParallelCoordinatesPlot:", ... " 'GroupVariable' must be a character vector.")); endif t = this.SourceTable; if (! isempty (val) && (isempty (t) || ! any (strcmp (t.Properties.VariableNames, val)))) error (strcat ("stats.chart.ParallelCoordinatesPlot:", ... " 'GroupVariable' does not name a variable of the", ... " source table.")); endif this.GroupVariable = val; if (this.TitleAuto_) this.Internal_ = true; this.LegendTitle = val; this.Internal_ = false; endif refit (this); endfunction function set.DataNormalization (this, val) this.DataNormalization = pcCheckOneOf (val, {'range', 'none', ... 'zscore', 'scale', ... 'center', 'norm'}, ... 'DataNormalization'); redraw (this); endfunction function set.CoordinateTickLabels (this, val) if (isa (val, 'string')) val = cellstr (val); endif if (! iscellstr (val)) error (strcat ("stats.chart.ParallelCoordinatesPlot:", ... " 'CoordinateTickLabels' must be a cell array of", ... " character vectors.")); endif this.CoordinateTickLabels = val(:); if (! this.Internal_) this.LabelsAuto_ = false; redraw (this); endif endfunction function set.Jitter (this, val) if (! (isnumeric (val) && isreal (val) && isscalar (val) && val >= 0 && val <= 1)) error (strcat ("stats.chart.ParallelCoordinatesPlot: 'Jitter' must", ... " be a number from 0 to 1.")); endif this.Jitter = double (val); redraw (this); endfunction function set.Color (this, val) if (! (isnumeric (val) && isreal (val) && columns (val) == 3 && rows (val) > 0 && all (val(:) >= 0) && all (val(:) <= 1))) error (strcat ("stats.chart.ParallelCoordinatesPlot: 'Color' must", ... " be an RGB triplet or a matrix of them.")); endif this.Color = double (val); if (! this.Internal_) this.StyleAuto_ = false; redraw (this); endif endfunction function set.LineStyle (this, val) this.LineStyle = pcCheckStyles (val, {'-', '--', ':', '-.', 'none'}, ... 'LineStyle'); if (! this.Internal_) redraw (this); endif endfunction function set.LineWidth (this, val) this.LineWidth = pcCheckPositive (val, 'LineWidth'); if (! this.Internal_) redraw (this); endif endfunction function set.LineAlpha (this, val) if (! (isnumeric (val) && isreal (val) && isvector (val) && all (val >= 0) && all (val <= 1))) error (strcat ("stats.chart.ParallelCoordinatesPlot: 'LineAlpha'", ... " must be from 0 to 1.")); endif this.LineAlpha = double (val(:)'); if (! this.Internal_) redraw (this); endif endfunction function set.MarkerStyle (this, val) this.MarkerStyle = pcCheckStyles (val, {'o', '+', '*', '.', 'x', 's', ... 'd', '^', 'v', '>', '<', 'p', ... 'h', 'none'}, 'MarkerStyle'); if (! this.Internal_) redraw (this); endif endfunction function set.MarkerSize (this, val) this.MarkerSize = pcCheckPositive (val, 'MarkerSize'); if (! this.Internal_) redraw (this); endif endfunction function set.LegendVisible (this, val) this.LegendVisible = pcCheckOneOf (val, {'on', 'off'}, 'LegendVisible'); if (! this.Internal_) this.LegendAuto_ = false; redraw (this); endif endfunction function set.LegendTitle (this, val) this.LegendTitle = pcCheckText (val, 'LegendTitle'); if (! this.Internal_) this.TitleAuto_ = false; redraw (this); endif endfunction function set.CoordinateLabel (this, val) this.CoordinateLabel = pcCheckText (val, 'CoordinateLabel'); redraw (this); endfunction function set.DataLabel (this, val) this.DataLabel = pcCheckText (val, 'DataLabel'); redraw (this); endfunction function set.Title (this, val) this.Title = pcCheckText (val, 'Title'); redraw (this); endfunction function set.FontName (this, val) this.FontName = pcCheckText (val, 'FontName'); redraw (this); endfunction function set.FontSize (this, val) if (! (isnumeric (val) && isreal (val) && isscalar (val) && isfinite (val) && val > 0)) error (strcat ("stats.chart.ParallelCoordinatesPlot: 'FontSize'", ... " must be a positive number.")); endif this.FontSize = double (val); redraw (this); endfunction function set.Position (this, val) this.Position = pcCheckPosition (val, 'Position'); if (! this.Internal_) this.PositionConstraint = 'innerposition'; endif endfunction function val = get.InnerPosition (this) val = this.Position; endfunction function set.InnerPosition (this, val) this.Position = val; endfunction function set.OuterPosition (this, val) this.OuterPosition = pcCheckPosition (val, 'OuterPosition'); if (! this.Internal_) this.PositionConstraint = 'outerposition'; endif endfunction function set.PositionConstraint (this, val) this.PositionConstraint = pcCheckOneOf (val, {'outerposition', ... 'innerposition'}, ... 'PositionConstraint'); redraw (this); endfunction function set.Units (this, val) this.Units = pcCheckOneOf (val, {'normalized', 'inches', ... 'centimeters', 'points', 'pixels', ... 'characters'}, 'Units'); redraw (this); endfunction function set.Visible (this, val) this.Visible = pcCheckOneOf (val, {'on', 'off'}, 'Visible'); redraw (this); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {stats.chart.ParallelCoordinatesPlot} {@var{obj} =} stats.chart.ParallelCoordinatesPlot (@var{parent}, @var{spec}, @var{args}) ## ## Create a @code{stats.chart.ParallelCoordinatesPlot} object. ## ## @var{parent} is the figure or panel to place the chart in, or empty for ## the current figure, which is resolved only once every value has been ## accepted. @var{spec} is a structure carrying the data as ## @code{parallelplot} resolved it, with the fields @qcode{Data}, ## @qcode{SourceTable}, @qcode{CoordinateData}, ## @qcode{CoordinateVariables}, @qcode{GroupData} and ## @qcode{GroupVariable}, and @var{args} the name-value pairs left to set. ## The documented way to reach this constructor is @code{parallelplot}. ## ## @seealso{parallelplot} ## @end deftypefn function this = ParallelCoordinatesPlot (parent, spec, args) if (nargin < 2) error (strcat ("stats.chart.ParallelCoordinatesPlot: too few input", ... " arguments.")); endif if (nargin < 3) args = {}; endif this.Filling_ = true; if (isempty (spec.SourceTable)) this.Data = spec.Data; this.Filling_ = false; if (isempty (spec.CoordinateData)) this.CoordinateData = 1:columns (this.Data); else this.CoordinateData = spec.CoordinateData; endif this.GroupData = spec.GroupData; else this.SourceTable = spec.SourceTable; this.Filling_ = false; if (isempty (spec.CoordinateVariables)) this.CoordinateVariables = spec.SourceTable.Properties.VariableNames; else this.CoordinateVariables = spec.CoordinateVariables; endif this.GroupVariable = spec.GroupVariable; endif ## Whatever was named is set now, so that one drawing covers them all placed = false; for k = 1:2:numel (args) name = args{k}; this.(name) = args{k+1}; if (any (strcmp (name, {'Position', 'InnerPosition', ... 'OuterPosition'}))) placed = true; endif endfor [~, ~, n] = observations (this); if (! isempty (this.GroupData) && numel (this.GroupData) != n) error (strcat ("stats.chart.ParallelCoordinatesPlot: 'GroupData'", ... " must hold one value for each observation.")); endif if (! this.LabelsAuto_ && numel (this.CoordinateTickLabels) != numel (coordinates (this))) error (strcat ("stats.chart.ParallelCoordinatesPlot:", ... " 'CoordinateTickLabels' must hold one label for", ... " each coordinate.")); endif refit (this); ## The parent comes last, so a rejected value leaves no figure behind. ## The chart takes the place of the current axes of its parent, as ## MATLAB's does, and refuses axes that are held if (isempty (parent)) parent = gcf (); endif fig = ancestor (parent, 'figure'); ca = get (fig, 'currentaxes'); if (! isempty (ca) && get (ca, 'parent') == parent) if (ishold (ca)) error (strcat ("stats.chart.ParallelCoordinatesPlot: a parallel", ... " coordinates plot cannot be added to axes on", ... " which hold is on.")); endif if (! placed) units = get (ca, 'units'); set (ca, 'units', this.Units); this.OuterPosition = get (ca, 'outerposition'); set (ca, 'units', units); endif delete (ca); endif this.Parent = parent; this.Axes_ = axes ('parent', parent, ... 'tag', 'stats.chart.ParallelCoordinatesPlot'); set (fig, 'currentaxes', this.Axes_); ## The chart goes with its axes, and its axes with the chart set (this.Axes_, 'deletefcn', @(~, ~) delete (this)); this.Drawn_ = true; redraw (this); endfunction ## -*- texinfo -*- ## @deftypefn {stats.chart.ParallelCoordinatesPlot} {} delete (@var{h}) ## ## Delete the chart and the axes it is drawn in. ## ## @end deftypefn function delete (this) ax = this.Axes_; lg = this.Legend_; this.Axes_ = []; this.Legend_ = []; this.Parent = []; if (! isempty (lg) && ishghandle (lg)) delete (lg); endif if (! isempty (ax) && ishghandle (ax) && strcmp (get (ax, 'beingdeleted'), 'off')) set (ax, 'deletefcn', ''); delete (ax); endif endfunction endmethods methods (Hidden) ## Let go of the axes when something other than the chart clears them, ## as a later plot does, leaving them to that plot. function release (this) if (this.Redrawing_ || isempty (this.Axes_)) return; endif ax = this.Axes_; this.Axes_ = []; this.Parent = []; if (ishghandle (ax)) set (ax, 'deletefcn', '', 'tag', ''); endif if (! isempty (this.Legend_) && ishghandle (this.Legend_)) delete (this.Legend_); endif this.Legend_ = []; endfunction endmethods methods (Access = private) ## The columns or variables drawn, as indices into the matrix or table. function idx = coordinates (this) if (isempty (this.SourceTable)) idx = this.CoordinateData; n = columns (this.Data); else idx = this.CoordinateVariables; names = this.SourceTable.Properties.VariableNames; n = numel (names); if (iscellstr (idx)) [~, idx] = ismember (idx, names); endif endif if (islogical (idx)) idx = find (idx); endif idx = idx(idx >= 1 & idx <= n); idx = idx(:)'; endfunction ## The values of each coordinate: a matrix of positions, one column for ## each, the levels of the columns holding categories, and the number of ## observations. function [V, levels, n] = observations (this) idx = coordinates (this); levels = cell (1, numel (idx)); if (isempty (this.SourceTable)) V = this.Data(:,idx); n = rows (this.Data); return; endif t = this.SourceTable; n = height (t); V = NaN (n, numel (idx)); for j = 1:numel (idx) v = t{:,idx(j)}; if ((isnumeric (v) || islogical (v)) && ! islogical (v)) V(:,j) = double (v(:,1)); else [code, levels{j}] = pcLevels (v); V(:,j) = code; endif endfor endfunction ## Bring the labels, styles and legend that follow the data in line with ## it, then draw. function refit (this) if (this.Filling_) return; endif [names, ~] = pcGroups (currentGroups (this)); ng = max (numel (names), 1); this.Internal_ = true; unwind_protect if (this.LabelsAuto_) if (isempty (this.SourceTable)) this.CoordinateTickLabels = arrayfun (@(k) sprintf ('%d', k), ... coordinates (this)', ... 'UniformOutput', false); else names_t = this.SourceTable.Properties.VariableNames; this.CoordinateTickLabels = names_t(coordinates (this))'; endif endif if (this.StyleAuto_) co = get (groot, 'defaultaxescolororder'); this.Color = co(mod ((1:ng) - 1, rows (co)) + 1,:); if (ng > 1) this.LineStyle = repmat ({'-'}, 1, ng); this.MarkerStyle = repmat ({'none'}, 1, ng); this.LineWidth = ones (1, ng); this.LineAlpha = 0.7 * ones (1, ng); this.MarkerSize = 6 * ones (1, ng); endif endif if (this.LegendAuto_) this.LegendVisible = pcOnOff (! isempty (names)); endif unwind_protect_cleanup this.Internal_ = false; end_unwind_protect this.Offsets_ = []; redraw (this); endfunction ## The groups of the observations, from the table or GroupData. function g = currentGroups (this) if (! isempty (this.SourceTable) && ! isempty (this.GroupVariable)) g = this.SourceTable.(this.GroupVariable); else g = this.GroupData; endif endfunction ## Draw the chart from scratch into its axes. function redraw (this) if (! this.Drawn_ || isempty (this.Axes_) || ! ishghandle (this.Axes_)) return; endif ax = this.Axes_; this.Redrawing_ = true; unwind_protect if (! isempty (this.Legend_) && ishghandle (this.Legend_)) delete (this.Legend_); endif this.Legend_ = []; delete (get (ax, 'children')); unwind_protect_cleanup this.Redrawing_ = false; end_unwind_protect ## A child whose deletion by anyone else releases the axes text (ax, NaN, NaN, '', 'deletefcn', @(~, ~) release (this)); [V, levels, n] = observations (this); nc = columns (V); iscat = ! cellfun (@isempty, levels); [names, g] = pcGroups (currentGroups (this)); ng = max (numel (names), 1); if (isempty (names)) g = ones (n, 1); endif ## Each value as a position along its ruler: on rulers of their own, ## from 0 at the smallest to 1 at the largest, or on one shared scale how = this.DataNormalization; P = NaN (n, nc); ticks = cell (1, nc); for j = 1:nc v = V(:,j); if (iscat(j)) P(:,j) = v; else P(:,j) = pcNormalize (v, how); endif endfor if (strcmp (how, 'range')) for j = 1:nc [P(:,j), ticks{j}] = pcRuler (P(:,j), levels{j}); endfor yl = [0, 1]; else num = P(:,! iscat); lo = min (num(:)); hi = max (num(:)); if (isempty (lo) || ! isfinite (lo)) lo = 0; hi = 1; elseif (lo == hi) lo -= 0.5; hi += 0.5; endif yl = [lo, hi]; for j = find (iscat) k = numel (levels{j}); P(:,j) = lo + (P(:,j) - 1) / max (k - 1, 1) * (hi - lo); tv = lo + ((1:k) - 1) / max (k - 1, 1) * (hi - lo); ticks{j} = {tv, levels{j}}; endfor endif ## Lines through the values of a variable of categories, spread over a ## share of the room up to the next value, drawn once for each set of ## observations if (rows (this.Offsets_) != n || columns (this.Offsets_) != nc) this.Offsets_ = rand (n, nc) - 0.5; endif for j = find (iscat) k = numel (levels{j}); step = (yl(2) - yl(1)) / max (k - 1, 1); P(:,j) += this.Jitter * step * this.Offsets_(:,j); endfor ## The lines of each group, observations joined across the rulers and ## parted by a missing value bg = get (ax, 'color'); if (ischar (bg)) bg = [1, 1, 1]; endif hl = zeros (1, ng); xs = [1:nc, NaN]; for k = 1:ng take = find (g == k); px = repmat (xs, numel (take), 1)'; py = [P(take,:), NaN(numel (take), 1)]'; c = pcPick (this.Color, k); a = pcPick (this.LineAlpha(:), k); hl(k) = line (ax, px(:), py(:), 'color', a * c + (1 - a) * bg, ... 'linestyle', pcPickStyle (this.LineStyle, k), ... 'linewidth', pcPick (this.LineWidth(:), k), ... 'marker', pcPickStyle (this.MarkerStyle, k), ... 'markersize', pcPick (this.MarkerSize(:), k), ... 'visible', this.Visible); if (! isempty (names)) set (hl(k), 'displayname', names{k}); endif endfor ## The rulers, one for each coordinate, with their own ticks where they ## have their own scale or hold categories for j = 1:nc line (ax, [j, j], yl, 'color', [0.15, 0.15, 0.15], ... 'visible', this.Visible); if (! isempty (ticks{j})) tv = ticks{j}{1}; tl = ticks{j}{2}; for i = 1:numel (tv) text (ax, j, tv(i), tl{i}, 'horizontalalignment', 'right', ... 'fontsize', 0.9 * this.FontSize, 'fontname', ... this.FontName, 'backgroundcolor', bg, 'margin', 1, ... 'visible', this.Visible); endfor endif endfor set (ax, 'xlim', [0.5, nc + 0.5], 'ylim', yl, 'xtick', 1:nc, ... 'xticklabel', this.CoordinateTickLabels, 'box', 'off', ... 'xgrid', 'off', 'units', this.Units, 'visible', this.Visible, ... 'fontname', this.FontName, 'fontsize', this.FontSize); if (strcmp (how, 'range')) set (ax, 'ytick', [], 'ycolor', 'none'); else set (ax, 'ytickmode', 'auto', 'ycolor', [0.15, 0.15, 0.15]); endif title (ax, this.Title, 'fontname', this.FontName); xlabel (ax, this.CoordinateLabel, 'fontname', this.FontName); ylabel (ax, this.DataLabel, 'fontname', this.FontName); ## Where the axes goes: within the outer position, leaving the labels ## the room they take and the legend its own showLegend = strcmp (this.LegendVisible, 'on') && ! isempty (names); if (strcmp (this.PositionConstraint, 'outerposition')) o = this.OuterPosition; ti = get (ax, 'tightinset'); pad = 0.02; L = max (0.13 * o(3), ti(1) + pad * o(3)); B = max (0.11 * o(4), ti(2) + pad * o(4)); T = max (0.075 * o(4), ti(4) + pad * o(4)); R = 0.095 * o(3); if (showLegend) R = 0.2 * o(3); endif w = max (o(3) - L - R, 0.01 * o(3)); h = max (o(4) - B - T, 0.01 * o(4)); this.Internal_ = true; unwind_protect this.Position = [o(1) + L, o(2) + B, w, h]; unwind_protect_cleanup this.Internal_ = false; end_unwind_protect endif set (ax, 'position', this.Position); ## The legend of the groups, beside the rulers if (showLegend) this.Legend_ = legend (ax, hl, names(:)', 'location', ... 'northeastoutside'); set (this.Legend_, 'fontname', this.FontName, 'visible', this.Visible); if (! isempty (this.LegendTitle)) title (this.Legend_, this.LegendTitle); endif set (ax, 'position', this.Position); endif endfunction endmethods endclassdef ## Values of a variable normalized as asked function v = pcNormalize (v, how) ok = isfinite (v); switch (how) case 'zscore' s = std (v(ok)); v = (v - mean (v(ok))) / (s + (s == 0)); case 'scale' s = std (v(ok)); v = v / (s + (s == 0)); case 'center' v = v - mean (v(ok)); case 'norm' s = norm (v(ok)); v = v / (s + (s == 0)); endswitch endfunction ## Positions from 0 to 1 along a ruler of its own, with the ticks it carries: ## the levels of a variable of categories, evenly spaced, or its smallest, ## largest and a few values between function [p, ticks] = pcRuler (v, levels) if (! isempty (levels)) k = numel (levels); p = (v - 1) / max (k - 1, 1); tv = ((1:k) - 1) / max (k - 1, 1); ticks = {tv, levels}; return; endif ok = isfinite (v); if (! any (ok)) p = v; ticks = {[], {}}; return; endif lo = min (v(ok)); hi = max (v(ok)); if (lo == hi) p = 0.5 * ones (size (v)); p(! ok) = NaN; ticks = {0.5, {sprintf('%.4g', lo)}}; return; endif p = (v - lo) / (hi - lo); tv = pcNiceTicks (lo, hi); tl = arrayfun (@(t) sprintf ('%.4g', t), tv, 'UniformOutput', false); ticks = {(tv - lo) / (hi - lo), tl}; endfunction ## Ticks between LO and HI at a round step of 1, 2 or 5 times a power of ## ten, three to seven of them function tv = pcNiceTicks (lo, hi) raw = (hi - lo) / 5; e = 10 ^ floor (log10 (raw)); m = [1, 2, 5, 10]; step = e * m(find (m * e >= raw, 1)); tv = (ceil (lo / step - 1e-9):floor (hi / step + 1e-9)) * step; tv(abs (tv) < step * 1e-9) = 0; endfunction ## The codes and names of the levels of a variable of categories, text or ## logical values: categories in their order, the rest sorted; NaN where ## missing function [code, names] = pcLevels (v) v = v(:,1); if (isa (v, 'categorical')) names = categories (v)'; [~, code] = ismember (cellstr (v), names); code = double (code); code(isundefined (v)) = NaN; elseif (islogical (v)) names = {'false', 'true'}; code = double (v) + 1; else t = cellstr (v); miss = cellfun (@isempty, t); if (isa (v, 'string')) miss |= ismissing (v); endif names = unique (t(! miss))'; code = NaN (numel (t), 1); [~, code(! miss)] = ismember (t(! miss), names); endif endfunction ## The names of the groups in order of first appearance, and the group of ## each observation, 0 where it is missing function [names, g] = pcGroups (gd) names = cell (0, 1); g = zeros (numel (gd), 1); if (isempty (gd)) return; endif if (isa (gd, 'categorical')) miss = isundefined (gd); t = cellstr (gd); elseif (iscellstr (gd) || isa (gd, 'string')) t = cellstr (gd); miss = cellfun (@isempty, t); if (isa (gd, 'string')) miss |= ismissing (gd); endif elseif (isnumeric (gd) || islogical (gd)) miss = isnan (double (gd)); t = arrayfun (@(v) sprintf ('%g', v), double (gd), 'UniformOutput', false); if (islogical (gd)) t(gd) = {'true'}; t(! gd) = {'false'}; endif else error (strcat ("stats.chart.ParallelCoordinatesPlot: 'GroupData'", ... " must hold categories, text, numbers or logical values.")); endif t = t(:); miss = miss(:); [names, i] = unique (t(! miss), 'first'); [~, o] = sort (i); names = names(o); [~, g(! miss)] = ismember (t(! miss), names); endfunction ## Indices into N columns: positive integers not above N, or a logical ## vector of length N function v = pcCheckIndex (val, n, name) if (islogical (val) && isvector (val) && numel (val) == n) v = val(:)'; elseif (isnumeric (val) && isreal (val) && (isvector (val) || isempty (val)) && all (val == fix (val)) && all (val >= 1) && all (val <= n)) v = double (val(:)'); else error (strcat ("stats.chart.ParallelCoordinatesPlot: '%s' must be", ... " indices of columns or a logical vector over them."), name); endif endfunction ## Row K of a matrix of values for the groups, the last one repeated function v = pcPick (vals, k) v = vals(min (k, rows (vals)),:); endfunction ## Style K of a character vector or a cell array of styles function s = pcPickStyle (styles, k) if (iscell (styles)) s = styles{min (k, numel (styles))}; else s = styles; endif endfunction ## 'on' or 'off' function s = pcOnOff (tf) if (tf) s = 'on'; else s = 'off'; endif endfunction ## Styles: one of LIST, or a cell array or string array of them function v = pcCheckStyles (val, list, name) if (isa (val, 'string')) val = cellstr (val); if (numel (val) == 1) val = val{1}; endif endif if (ischar (val) && any (strcmp (val, list))) v = val; elseif (iscellstr (val) && ! isempty (val) && all (ismember (val, list))) v = val(:)'; else error ("stats.chart.ParallelCoordinatesPlot: '%s' must be one of %s.", ... name, strjoin (strcat ("'", list, "'"), ', ')); endif endfunction ## Positive values, a scalar or one for each group function v = pcCheckPositive (val, name) if (! (isnumeric (val) && isreal (val) && isvector (val) && all (isfinite (val)) && all (val > 0))) error (strcat ("stats.chart.ParallelCoordinatesPlot: '%s' must be", ... " positive."), name); endif v = double (val(:)'); endfunction ## Text: a character vector, a string, or a cell array of them for lines function out = pcCheckText (val, name) if (isa (val, 'string')) val = cellstr (val); if (numel (val) == 1) val = val{1}; endif endif if (! ((ischar (val) && (isrow (val) || isempty (val))) || iscellstr (val))) error ("stats.chart.ParallelCoordinatesPlot: '%s' must be text.", name); endif out = val; endfunction ## One of a list of names, case free function out = pcCheckOneOf (val, list, name) if (isa (val, 'string') && isscalar (val)) val = char (val); endif if (! (ischar (val) && any (strcmpi (val, list)))) error ("stats.chart.ParallelCoordinatesPlot: '%s' must be one of %s.", ... name, strjoin (strcat ("'", list, "'"), ', ')); endif out = lower (val); endfunction ## A position, as a 1-by-4 vector with positive width and height function out = pcCheckPosition (val, name) if (! (isnumeric (val) && isreal (val) && numel (val) == 4 && all (isfinite (val)) && val(3) > 0 && val(4) > 0)) error (strcat ("stats.chart.ParallelCoordinatesPlot: '%s' must be a", ... " 1-by-4 vector with positive width and height."), name); endif out = double (val(:)'); endfunction %!shared X, g, T %! X = [2.1, 1.2, 10; 3.4, 2.8, 12; 1.9, 0.9, 11; 5.6, 2.2, 15; ... %! 4.4, 3.1, 9; 3.8, 1.7, 13]; %! g = categorical ({'a'; 'b'; 'a'; 'b'; 'c'; 'a'}); %! T = table (X(:,1), X(:,2), X(:,3), g, 'VariableNames', {'A', 'B', 'C', 'G'}); %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = parallelplot (X); %! assert_equal (class (h), 'stats.chart.ParallelCoordinatesPlot'); %! assert_equal (h.Data, X); %! assert_equal (h.CoordinateData, [1, 2, 3]); %! assert_equal (h.GroupData, []); %! assert_equal (h.CoordinateTickLabels, {'1'; '2'; '3'}); %! assert_equal (h.DataNormalization, 'range'); %! assert_equal (h.Color, [0, 0.447, 0.741]); %! assert_equal (h.LineWidth, 1); %! assert_equal (h.LineStyle, '-'); %! assert_equal (h.LineAlpha, 0.7); %! assert_equal (h.MarkerStyle, 'none'); %! assert_equal (h.MarkerSize, 6); %! assert_equal (h.Jitter, 0.1); %! assert_equal (h.LegendVisible, 'off'); %! assert_equal (h.FontSize, 10); %! assert_equal (h.OuterPosition, [0, 0, 1, 1]); %! assert_equal (h.Parent, hf); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = parallelplot (X, 'GroupData', g); %! assert_equal (h.Color, [0, 0.447, 0.741; 0.85, 0.325, 0.098; ... %! 0.929, 0.694, 0.125]); %! assert_equal (h.LineStyle, {'-', '-', '-'}); %! assert_equal (h.LineWidth, [1, 1, 1]); %! assert_equal (h.LineAlpha, [0.7, 0.7, 0.7]); %! assert_equal (h.MarkerStyle, {'none', 'none', 'none'}); %! assert_equal (h.MarkerSize, [6, 6, 6]); %! assert_equal (h.LegendVisible, 'on'); %! assert_equal (h.LegendTitle, ''); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = parallelplot (X, 'GroupData', {'p'; 'q'; 'p'; 'q'; 'p'; 'q'}); %! assert_equal (rows (h.Color), 2); %! assert_equal (h.LegendVisible, 'on'); %! lg = findall (hf, 'type', 'axes', 'tag', 'legend'); %! s = get (lg, 'string'); # a row under gnuplot, a column otherwise %! assert_equal (s(:), {'p'; 'q'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = parallelplot (T); %! assert_equal (h.CoordinateVariables, {'A', 'B', 'C', 'G'}); %! assert_equal (h.CoordinateTickLabels, {'A'; 'B'; 'C'; 'G'}); %! assert_equal (h.GroupVariable, ''); %! assert_equal (h.LegendTitle, ''); %! assert_equal (h.LegendVisible, 'off'); %! assert_equal (h.Data, []); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = parallelplot (T, 'GroupVariable', 'G'); %! assert_equal (h.LegendTitle, 'G'); %! assert_equal (h.LegendVisible, 'on'); %! assert_equal (rows (h.Color), 3); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = parallelplot (T, 'CoordinateVariables', {'C', 'A'}); %! assert_equal (h.CoordinateVariables, {'C', 'A'}); %! assert_equal (h.CoordinateTickLabels, {'C'; 'A'}); %! h.CoordinateVariables = [3, 1]; %! assert_equal (h.CoordinateVariables, [3, 1]); %! h.CoordinateVariables = [true, false, true, false]; %! assert_equal (h.CoordinateVariables, [true, false, true, false]); %! assert_equal (h.CoordinateTickLabels, {'A'; 'C'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = parallelplot (X, 'CoordinateTickLabels', {'x', 'y', 'z'}, ... %! 'Jitter', 0.3, 'DataNormalization', 'zscore'); %! assert_equal (h.CoordinateTickLabels, {'x'; 'y'; 'z'}); %! assert_equal (h.Jitter, 0.3); %! assert_equal (h.DataNormalization, 'zscore'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = parallelplot (T, 'GroupVariable', 'G', 'Color', ... %! [1, 0, 0; 0, 1, 0; 0, 0, 1], 'LineStyle', ... %! {'-', '--', ':'}, 'LineWidth', [1, 2, 3], ... %! 'MarkerStyle', {'o', 'x', 's'}, 'MarkerSize', ... %! [4, 6, 8], 'LineAlpha', 0.3); %! assert_equal (h.Color, [1, 0, 0; 0, 1, 0; 0, 0, 1]); %! assert_equal (h.LineStyle, {'-', '--', ':'}); %! assert_equal (h.LineWidth, [1, 2, 3]); %! assert_equal (h.MarkerStyle, {'o', 'x', 's'}); %! assert_equal (h.MarkerSize, [4, 6, 8]); %! assert_equal (h.LineAlpha, 0.3); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = parallelplot ([X; NaN, 1, 1]); %! assert_equal (size (h.Data), [7, 3]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## Values on rulers of their own run from 0 at the smallest to 1 at the %! ## largest, and a missing value breaks its line %! hf = figure ('visible', 'off'); %! unwind_protect %! h = parallelplot ([1, 10; 3, 20; 2, NaN]); %! tag = 'stats.chart.ParallelCoordinatesPlot'; %! ax = findall (hf, 'type', 'axes', 'tag', tag); %! ln = findobj (ax, 'type', 'line', '-and', '-not', 'linestyle', 'none'); %! yd = get (ln(end), 'ydata'); %! assert_equal (yd(:)', [0, 0, NaN, 1, 1, NaN, 0.5, NaN, NaN], -1e-15); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = parallelplot ([1, 10; 3, 20; 2, 30], 'DataNormalization', 'zscore'); %! tag = 'stats.chart.ParallelCoordinatesPlot'; %! ax = findall (hf, 'type', 'axes', 'tag', tag); %! ln = findobj (ax, 'type', 'line'); %! yd = get (ln(end), 'ydata'); %! assert_equal (yd(:)', [-1, -1, NaN, 1, 0, NaN, 0, 1, NaN], -1e-15); %! h.DataNormalization = 'center'; %! ln = findobj (ax, 'type', 'line'); %! yd = get (ln(end), 'ydata'); %! assert_equal (yd(:)', [-1, -10, NaN, 1, 0, NaN, 0, 10, NaN], -1e-14); %! h.DataNormalization = 'none'; %! assert_equal (get (ax, 'ylim'), [1, 30]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## A variable of categories places its levels evenly along its ruler %! hf = figure ('visible', 'off'); %! unwind_protect %! t = table ([1; 2; 3], categorical ({'lo'; 'hi'; 'lo'}, {'lo', 'mid', ... %! 'hi'}), 'VariableNames', {'N', 'C'}); %! h = parallelplot (t, 'Jitter', 0); %! tag = 'stats.chart.ParallelCoordinatesPlot'; %! ax = findall (hf, 'type', 'axes', 'tag', tag); %! ln = findobj (ax, 'type', 'line'); %! yd = get (ln(end), 'ydata'); %! assert_equal (yd(:)', [0, 0, NaN, 0.5, 1, NaN, 1, 0, NaN], -1e-15); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!shared pcS %! pcS = struct ('Data', magic (3), 'SourceTable', [], ... %! 'CoordinateData', [], 'CoordinateVariables', {{}}, ... %! 'GroupData', [], 'GroupVariable', ''); %!error ... %! stats.chart.ParallelCoordinatesPlot ([]) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, {'Data', {1}}) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, {'CoordinateData', 4}) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, {'CoordinateVariables', 1}) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, {'GroupData', [1; 2]}) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, {'GroupData', ones(3, 3)}) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, {'DataNormalization', 'norm1'}) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, {'CoordinateTickLabels', 5}) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, ... %! {'CoordinateTickLabels', {'x'}}) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, {'Jitter', 2}) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, {'Color', [2, 0, 0]}) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, {'LineStyle', 'o'}) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, {'LineWidth', 0}) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, {'LineAlpha', 2}) ## test.m ends a pattern at the first '>' of its line, and the list of ## markers holds one, so the message is matched only as far as 'v' %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, {'MarkerStyle', 'q'}) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, {'MarkerSize', -1}) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, {'LegendVisible', 1}) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, {'Title', 5}) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, {'FontSize', 0}) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, {'Position', [0, 0, 1, 0]}) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], pcS, {'Visible', 'maybe'}) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], ... %! struct ('Data', [], 'SourceTable', ... %! table ([1; 2], 'VariableNames', {'A'}), ... %! 'CoordinateData', [], 'CoordinateVariables', {{'Q'}}, ... %! 'GroupData', [], 'GroupVariable', '')) %!error ... %! stats.chart.ParallelCoordinatesPlot ([], ... %! struct ('Data', [], 'SourceTable', ... %! table ([1; 2], 'VariableNames', {'A'}), ... %! 'CoordinateData', [], 'CoordinateVariables', {{}}, ... %! 'GroupData', [], 'GroupVariable', 'Q')) statistics-release-2.1.2/inst/Plotting/+stats/+chart/ScatterHistogramChart.m000066400000000000000000001645411527311104300271210ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftp {statistics} stats.chart.ScatterHistogramChart ## ## A scatter plot with a histogram of each variable along its sides, as ## @code{scatterhistogram} draws it. ## ## A @code{stats.chart.ScatterHistogramChart} holds two samples, optionally ## split into groups, and the choices they are drawn with, and redraws itself ## whenever one of them is set. It is what @code{scatterhistogram} returns, ## and the documented way to reach it. ## ## The samples come either from vectors or from a table, never from both. ## The histograms are normalized as probability densities, one for each group, ## with the bins @code{histcounts} chooses for each group unless ## @code{NumBins} or @code{BinWidths} say otherwise. The groups take the ## colours of the colour order and the line styles @qcode{'-'}, @qcode{':'}, ## @qcode{'-.'} and @qcode{'--'} in turn, in the order in which each first ## appears in @code{GroupData}. ## ## A chart takes the place of the current axes of its parent, keeping their ## outer position, and refuses axes on which @code{hold} is on; a later plot ## into the figure takes the place of the chart in turn. MATLAB's chart is ## a graphics object of its own; this one is a handle class drawing into ## three axes of its own, the scatter plot's being made current, and it lets ## go of them when a later plot clears them. @code{LegendVisible}, ## @code{MarkerFilled} and @code{Visible} hold @qcode{'on'} or @qcode{'off'}, ## and @code{LineStyle} and @code{MarkerStyle} a character vector or a cell ## array of them, where MATLAB holds an on-off state and a string array. ## ## @seealso{scatterhistogram, scatterhist, histcounts, ksdensity} ## @end deftp classdef ScatterHistogramChart < handle properties (Access = public) ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} XData ## ## The values along the horizontal axis, a numeric column vector. Where ## the chart was drawn from a table it is read from the table and setting ## it is refused. ## ## @end deftp XData = []; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} YData ## ## The values along the vertical axis, a numeric column vector as long as ## @code{XData}. ## ## @end deftp YData = []; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} GroupData ## ## The group of each observation: a vector as long as @code{XData} of ## categories, text, numbers or logical values, or empty for no groups. ## ## @end deftp GroupData = []; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} SourceTable ## ## The table the chart was drawn from, or empty. ## ## @end deftp SourceTable = []; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} XVariable ## ## The variable of @code{SourceTable} holding @code{XData}. ## ## @end deftp XVariable = ''; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} YVariable ## ## The variable of @code{SourceTable} holding @code{YData}. ## ## @end deftp YVariable = ''; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} GroupVariable ## ## The variable of @code{SourceTable} holding @code{GroupData}, or empty. ## ## @end deftp GroupVariable = ''; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} HistogramDisplayStyle ## ## How the histograms are drawn: @qcode{'stairs'}, the default, as their ## outline; @qcode{'bar'} as bars; @qcode{'smooth'} as a kernel density ## estimate, as @code{ksdensity} computes it. ## ## @end deftp HistogramDisplayStyle = 'stairs'; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} NumBins ## ## The number of bins of each histogram, a 2-by-1 vector for the ## horizontal and the vertical variable, or a 2-by-N matrix with a column ## for each of N groups. Setting a scalar sets both. Empty where ## @code{HistogramDisplayStyle} is @qcode{'smooth'}. ## ## @end deftp NumBins = []; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} BinWidths ## ## The width of the bins, shaped as @code{NumBins}. Setting it takes ## precedence over @code{NumBins}, which then follows from it. ## ## @end deftp BinWidths = []; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} ScatterPlotLocation ## ## Which corner the scatter plot takes, the histograms lining the two ## sides away from it: @qcode{'SouthWest'}, the default, ## @qcode{'SouthEast'}, @qcode{'NorthEast'} or @qcode{'NorthWest'}. ## ## @end deftp ScatterPlotLocation = 'SouthWest'; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} ScatterPlotProportion ## ## The share of the width and of the height the scatter plot takes, a ## number between 0 and 1, 0.75 by default. ## ## @end deftp ScatterPlotProportion = 0.75; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} XHistogramDirection ## ## Which way the bars of the histogram of @code{XData} rise: ## @qcode{'up'}, the default, or @qcode{'down'}. ## ## @end deftp XHistogramDirection = 'up'; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} YHistogramDirection ## ## Which way the bars of the histogram of @code{YData} rise: ## @qcode{'right'}, the default, or @qcode{'left'}. ## ## @end deftp YHistogramDirection = 'right'; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} XLimits ## ## The limits of the horizontal axis, the range of @code{XData} until ## set. ## ## @end deftp XLimits = [0, 1]; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} YLimits ## ## The limits of the vertical axis, the range of @code{YData} until set. ## ## @end deftp YLimits = [0, 1]; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} Color ## ## The colour of each group, one row of an RGB matrix for each; by ## default the colours of the colour order. ## ## @end deftp Color = [0, 0.447, 0.741]; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} LineStyle ## ## The line style of the histograms of each group, a character vector or ## a cell array of them. ## ## @end deftp LineStyle = '-'; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} LineWidth ## ## The width of the histogram lines, a scalar or one for each group, 0.5 ## by default. ## ## @end deftp LineWidth = 0.5; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} MarkerStyle ## ## The marker of each group, a character vector or a cell array of them, ## @qcode{'o'} by default. ## ## @end deftp MarkerStyle = 'o'; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} MarkerSize ## ## The area of the markers in square points, a scalar or one for each ## group, 36 by default. ## ## @end deftp MarkerSize = 36; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} MarkerFilled ## ## Whether the markers are filled, @qcode{'on'} by default or ## @qcode{'off'}. ## ## @end deftp MarkerFilled = 'on'; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} MarkerAlpha ## ## How opaque the markers are, from 0 to 1, 1 by default. ## ## @end deftp MarkerAlpha = 1; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} LegendVisible ## ## Whether the legend of the groups is shown, @qcode{'on'} or ## @qcode{'off'}; on where there are groups, until set. ## ## @end deftp LegendVisible = 'off'; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} LegendTitle ## ## The title of the legend; from a table the name of ## @code{GroupVariable}, until set. ## ## @end deftp LegendTitle = ''; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} Title ## ## The title of the chart. ## ## @end deftp Title = ''; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} XLabel ## ## The label of the horizontal axis; from a table the name of ## @code{XVariable}, until set. ## ## @end deftp XLabel = ''; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} YLabel ## ## The label of the vertical axis; from a table the name of ## @code{YVariable}, until set. ## ## @end deftp YLabel = ''; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} FontName ## ## The font of every text on the chart, @qcode{'Helvetica'} by default. ## ## @end deftp FontName = 'Helvetica'; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} FontSize ## ## The size of every text on the chart, in points, 10 by default. ## ## @end deftp FontSize = 10; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} Position ## ## The position of the scatter plot in its parent, as ## @code{[left, bottom, width, height]} in @code{Units}. While ## @code{PositionConstraint} is @qcode{'outerposition'} the chart places ## it within @code{OuterPosition} itself; setting it fixes the scatter ## plot there and turns @code{PositionConstraint} to ## @qcode{'innerposition'}. @code{InnerPosition} is the same. ## ## @end deftp Position = [0.13, 0.11, 0.6, 0.6]; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} OuterPosition ## ## The position of the whole chart in its parent, labels included, ## @code{[0, 0, 1, 1]} by default, or the outer position of the axes it ## took the place of. ## ## @end deftp OuterPosition = [0, 0, 1, 1]; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} PositionConstraint ## ## Which position the chart keeps: @qcode{'outerposition'}, the default, ## or @qcode{'innerposition'}. ## ## @end deftp PositionConstraint = 'outerposition'; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} Units ## ## The units of the positions, @qcode{'normalized'} by default. ## ## @end deftp Units = 'normalized'; ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} Visible ## ## Whether the chart is shown, @qcode{'on'} by default or @qcode{'off'}. ## ## @end deftp Visible = 'on'; endproperties properties (Dependent) ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} InnerPosition ## ## The same as @code{Position}. ## ## @end deftp InnerPosition; endproperties properties (GetAccess = public, SetAccess = private) ## -*- texinfo -*- ## @deftp {stats.chart.ScatterHistogramChart} {property} Parent ## ## The figure or panel the chart is placed in. This property is ## read-only. ## ## @end deftp Parent = []; endproperties properties (Access = private, Hidden) Axes_ = []; # scatter plot, histogram of X, histogram of Y Legend_ = []; Drawn_ = false; Redrawing_ = false; Internal_ = false; # true while the chart sets a value itself Reading_ = false; # true while the table is being read BinsAuto_ = true; # NumBins and BinWidths chosen by histcounts WidthsSet_ = false; # BinWidths set, NumBins following from it LimitsAuto_ = [true, true]; StyleAuto_ = true; # colours and styles follow the groups LegendAuto_ = true; LabelsAuto_ = [true, true, true]; # XLabel, YLabel, LegendTitle endproperties methods (Hidden) function disp (this) printf (' ScatterHistogramChart with properties:\n\n'); if (isempty (this.SourceTable)) printf ('%13s: [%dx1 double]\n', 'XData', numel (this.XData)); printf ('%13s: [%dx1 double]\n', 'YData', numel (this.YData)); if (! isempty (this.GroupData)) printf ('%13s: [%dx1 %s]\n', 'GroupData', numel (this.GroupData), ... class (this.GroupData)); endif else printf ('%13s: [%dx%d table]\n', 'SourceTable', ... size (this.SourceTable)); printf ('%13s: ''%s''\n', 'XVariable', this.XVariable); printf ('%13s: ''%s''\n', 'YVariable', this.YVariable); printf ('%13s: ''%s''\n', 'GroupVariable', this.GroupVariable); endif printf ('\n'); endfunction function display (this) disp (this); endfunction endmethods methods (Hidden) function set.XData (this, val) if (this.Drawn_ && ! isempty (this.SourceTable)) error (strcat ("stats.chart.ScatterHistogramChart: setting", ... " 'XData' while 'SourceTable' holds a table is not", ... " supported.")); endif this.XData = shCheckData (val, 'XData'); refit (this); endfunction function set.YData (this, val) if (this.Drawn_ && ! isempty (this.SourceTable)) error (strcat ("stats.chart.ScatterHistogramChart: setting", ... " 'YData' while 'SourceTable' holds a table is not", ... " supported.")); endif this.YData = shCheckData (val, 'YData'); refit (this); endfunction function set.GroupData (this, val) if (! isempty (val) && ! isvector (val)) error (strcat ("stats.chart.ScatterHistogramChart: 'GroupData'", ... " must be a vector.")); endif if (isempty (val)) this.GroupData = []; else this.GroupData = val(:); endif refit (this); endfunction function set.SourceTable (this, val) if (! (isempty (val) || istable (val))) error (strcat ("stats.chart.ScatterHistogramChart: 'SourceTable'", ... " must be a table.")); endif this.SourceTable = val; readTable (this); endfunction function set.XVariable (this, val) this.XVariable = shCheckName (val, 'XVariable'); readTable (this); endfunction function set.YVariable (this, val) this.YVariable = shCheckName (val, 'YVariable'); readTable (this); endfunction function set.GroupVariable (this, val) this.GroupVariable = shCheckName (val, 'GroupVariable'); readTable (this); endfunction function set.HistogramDisplayStyle (this, val) this.HistogramDisplayStyle = shCheckOneOf (val, {'stairs', 'bar', ... 'smooth'}, ... 'HistogramDisplayStyle'); refit (this); endfunction function set.NumBins (this, val) if (this.Internal_) this.NumBins = val; return; endif this.NumBins = shCheckBins (val, numel (shGroups (this.GroupData)), ... 'NumBins', true); this.BinsAuto_ = false; this.WidthsSet_ = false; refit (this); endfunction function set.BinWidths (this, val) if (this.Internal_) this.BinWidths = val; return; endif this.BinWidths = shCheckBins (val, numel (shGroups (this.GroupData)), ... 'BinWidths', false); this.BinsAuto_ = false; this.WidthsSet_ = true; refit (this); endfunction function set.ScatterPlotLocation (this, val) list = {'SouthWest', 'SouthEast', 'NorthEast', 'NorthWest'}; v = shCheckOneOf (val, list, 'ScatterPlotLocation'); this.ScatterPlotLocation = list{strcmpi (v, list)}; redraw (this); endfunction function set.ScatterPlotProportion (this, val) if (! (isnumeric (val) && isreal (val) && isscalar (val) && val > 0 && val < 1)) error (strcat ("stats.chart.ScatterHistogramChart:", ... " 'ScatterPlotProportion' must be a number between", ... " 0 and 1.")); endif this.ScatterPlotProportion = double (val); redraw (this); endfunction function set.XHistogramDirection (this, val) this.XHistogramDirection = shCheckOneOf (val, {'up', 'down'}, ... 'XHistogramDirection'); redraw (this); endfunction function set.YHistogramDirection (this, val) this.YHistogramDirection = shCheckOneOf (val, {'right', 'left'}, ... 'YHistogramDirection'); redraw (this); endfunction function set.XLimits (this, val) this.XLimits = shCheckLimits (val, 'XLimits'); if (! this.Internal_) this.LimitsAuto_(1) = false; redraw (this); endif endfunction function set.YLimits (this, val) this.YLimits = shCheckLimits (val, 'YLimits'); if (! this.Internal_) this.LimitsAuto_(2) = false; redraw (this); endif endfunction function set.Color (this, val) if (! (isnumeric (val) && isreal (val) && columns (val) == 3 && rows (val) > 0 && all (val(:) >= 0) && all (val(:) <= 1))) error (strcat ("stats.chart.ScatterHistogramChart: 'Color' must be", ... " an RGB triplet or a matrix of them.")); endif this.Color = double (val); if (! this.Internal_) this.StyleAuto_ = false; redraw (this); endif endfunction function set.LineStyle (this, val) this.LineStyle = shCheckStyles (val, {'-', '--', ':', '-.', 'none'}, ... 'LineStyle'); if (! this.Internal_) this.StyleAuto_ = false; redraw (this); endif endfunction function set.LineWidth (this, val) this.LineWidth = shCheckPositive (val, 'LineWidth'); redraw (this); endfunction function set.MarkerStyle (this, val) this.MarkerStyle = shCheckStyles (val, {'o', '+', '*', '.', 'x', ... 's', 'd', '^', 'v', '>', '<', ... 'p', 'h', 'none'}, ... 'MarkerStyle'); redraw (this); endfunction function set.MarkerSize (this, val) this.MarkerSize = shCheckPositive (val, 'MarkerSize'); redraw (this); endfunction function set.MarkerFilled (this, val) this.MarkerFilled = shCheckOneOf (val, {'on', 'off'}, 'MarkerFilled'); redraw (this); endfunction function set.MarkerAlpha (this, val) if (! (isnumeric (val) && isreal (val) && isscalar (val) && val >= 0 && val <= 1)) error (strcat ("stats.chart.ScatterHistogramChart: 'MarkerAlpha'", ... " must be a number from 0 to 1.")); endif this.MarkerAlpha = double (val); redraw (this); endfunction function set.LegendVisible (this, val) this.LegendVisible = shCheckOneOf (val, {'on', 'off'}, 'LegendVisible'); if (! this.Internal_) this.LegendAuto_ = false; redraw (this); endif endfunction function set.LegendTitle (this, val) this.LegendTitle = shCheckText (val, 'LegendTitle'); if (! this.Internal_) this.LabelsAuto_(3) = false; redraw (this); endif endfunction function set.Title (this, val) this.Title = shCheckText (val, 'Title'); redraw (this); endfunction function set.XLabel (this, val) this.XLabel = shCheckText (val, 'XLabel'); if (! this.Internal_) this.LabelsAuto_(1) = false; redraw (this); endif endfunction function set.YLabel (this, val) this.YLabel = shCheckText (val, 'YLabel'); if (! this.Internal_) this.LabelsAuto_(2) = false; redraw (this); endif endfunction function set.FontName (this, val) this.FontName = shCheckText (val, 'FontName'); redraw (this); endfunction function set.FontSize (this, val) if (! (isnumeric (val) && isreal (val) && isscalar (val) && isfinite (val) && val > 0)) error (strcat ("stats.chart.ScatterHistogramChart: 'FontSize'", ... " must be a positive number.")); endif this.FontSize = double (val); redraw (this); endfunction function set.Position (this, val) this.Position = shCheckPosition (val, 'Position'); if (! this.Internal_) this.PositionConstraint = 'innerposition'; endif endfunction function val = get.InnerPosition (this) val = this.Position; endfunction function set.InnerPosition (this, val) this.Position = val; endfunction function set.OuterPosition (this, val) this.OuterPosition = shCheckPosition (val, 'OuterPosition'); if (! this.Internal_) this.PositionConstraint = 'outerposition'; endif endfunction function set.PositionConstraint (this, val) this.PositionConstraint = shCheckOneOf (val, {'outerposition', ... 'innerposition'}, ... 'PositionConstraint'); redraw (this); endfunction function set.Units (this, val) this.Units = shCheckOneOf (val, {'normalized', 'inches', ... 'centimeters', 'points', 'pixels', ... 'characters'}, 'Units'); redraw (this); endfunction function set.Visible (this, val) this.Visible = shCheckOneOf (val, {'on', 'off'}, 'Visible'); redraw (this); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {stats.chart.ScatterHistogramChart} {@var{obj} =} stats.chart.ScatterHistogramChart (@var{parent}, @var{spec}, @var{args}) ## ## Create a @code{stats.chart.ScatterHistogramChart} object. ## ## @var{parent} is the figure or panel to place the chart in, or empty for ## the current figure, which is resolved only once every value has been ## accepted. @var{spec} is a structure carrying the data as ## @code{scatterhistogram} resolved it, with the fields @qcode{XData}, ## @qcode{YData}, @qcode{GroupData}, @qcode{SourceTable}, ## @qcode{XVariable}, @qcode{YVariable} and @qcode{GroupVariable}, and ## @var{args} the name-value pairs left to set. The documented way to ## reach this constructor is @code{scatterhistogram}. ## ## @seealso{scatterhistogram} ## @end deftypefn function this = ScatterHistogramChart (parent, spec, args) if (nargin < 2) error ("stats.chart.ScatterHistogramChart: too few input arguments."); endif if (nargin < 3) args = {}; endif if (isempty (spec.SourceTable)) this.XData = spec.XData; this.YData = spec.YData; this.GroupData = spec.GroupData; else this.SourceTable = spec.SourceTable; this.XVariable = spec.XVariable; this.YVariable = spec.YVariable; this.GroupVariable = spec.GroupVariable; endif ## Whatever was named is set now, so that one drawing covers them all; ## a value set here counts as chosen, as it would once drawn placed = false; for k = 1:2:numel (args) name = args{k}; this.(name) = args{k+1}; if (any (strcmp (name, {'Position', 'InnerPosition', ... 'OuterPosition'}))) placed = true; endif endfor if (numel (this.XData) != numel (this.YData)) error (strcat ("stats.chart.ScatterHistogramChart: 'XData' and", ... " 'YData' must hold the same number of values.")); endif if (! isempty (this.GroupData) && numel (this.GroupData) != numel (this.XData)) error (strcat ("stats.chart.ScatterHistogramChart: 'GroupData'", ... " must hold one value for each observation.")); endif refit (this); ## The parent comes last, so a rejected value leaves no figure behind. ## The chart takes the place of the current axes of its parent, as ## MATLAB's does, and refuses axes that are held if (isempty (parent)) parent = gcf (); endif fig = ancestor (parent, 'figure'); ca = get (fig, 'currentaxes'); if (! isempty (ca) && get (ca, 'parent') == parent) if (ishold (ca)) error (strcat ("stats.chart.ScatterHistogramChart: a scatter", ... " histogram cannot be added to axes on which hold", ... " is on.")); endif if (! placed) units = get (ca, 'units'); set (ca, 'units', this.Units); this.OuterPosition = get (ca, 'outerposition'); set (ca, 'units', units); endif delete (ca); endif this.Parent = parent; tag = 'stats.chart.ScatterHistogramChart'; this.Axes_ = [axes('parent', parent, 'tag', tag), ... axes('parent', parent, 'tag', tag), ... axes('parent', parent, 'tag', tag)]; set (fig, 'currentaxes', this.Axes_(1)); ## The chart goes with its axes, and its axes with the chart set (this.Axes_, 'deletefcn', @(~, ~) delete (this)); this.Drawn_ = true; redraw (this); endfunction ## -*- texinfo -*- ## @deftypefn {stats.chart.ScatterHistogramChart} {@var{limits} =} xlim (@var{h}) ## @deftypefnx {stats.chart.ScatterHistogramChart} {} xlim (@var{h}, @var{limits}) ## ## The limits of the horizontal axis, as @code{XLimits} holds them; given ## @var{limits}, set them. ## ## @end deftypefn function out = xlim (this, lims) if (nargin > 1) this.XLimits = lims; else out = this.XLimits; endif endfunction ## -*- texinfo -*- ## @deftypefn {stats.chart.ScatterHistogramChart} {@var{limits} =} ylim (@var{h}) ## @deftypefnx {stats.chart.ScatterHistogramChart} {} ylim (@var{h}, @var{limits}) ## ## The limits of the vertical axis, as @code{YLimits} holds them; given ## @var{limits}, set them. ## ## @end deftypefn function out = ylim (this, lims) if (nargin > 1) this.YLimits = lims; else out = this.YLimits; endif endfunction ## -*- texinfo -*- ## @deftypefn {stats.chart.ScatterHistogramChart} {} delete (@var{h}) ## ## Delete the chart and the axes it is drawn in. ## ## @end deftypefn function delete (this) ax = this.Axes_; lg = this.Legend_; this.Axes_ = []; this.Legend_ = []; this.Parent = []; if (! isempty (lg) && ishghandle (lg)) delete (lg); endif for k = 1:numel (ax) if (ishghandle (ax(k)) && strcmp (get (ax(k), 'beingdeleted'), 'off')) set (ax(k), 'deletefcn', ''); delete (ax(k)); endif endfor endfunction endmethods methods (Hidden) ## Let go of the axes when something other than the chart clears the ## scatter plot, as a later plot does, leaving it to that plot and ## deleting the histograms. function release (this) if (this.Redrawing_ || isempty (this.Axes_)) return; endif ax = this.Axes_; this.Axes_ = []; this.Parent = []; if (ishghandle (ax(1))) set (ax(1), 'deletefcn', '', 'tag', ''); endif for k = 2:numel (ax) if (ishghandle (ax(k))) set (ax(k), 'deletefcn', ''); delete (ax(k)); endif endfor if (! isempty (this.Legend_) && ishghandle (this.Legend_)) delete (this.Legend_); endif this.Legend_ = []; endfunction endmethods methods (Access = private) ## Read the samples from the table, with the labels they bring. function readTable (this) t = this.SourceTable; if (this.Reading_ || isempty (t) || isempty (this.XVariable) || isempty (this.YVariable)) return; endif names = t.Properties.VariableNames; for v = {'XVariable', 'YVariable', 'GroupVariable'} n = this.(v{1}); if (! isempty (n) && ! any (strcmp (names, n))) error (strcat ("stats.chart.ScatterHistogramChart: '%s' does not", ... " name a variable of the source table."), v{1}); endif endfor this.Reading_ = true; drawn = this.Drawn_; this.Drawn_ = false; unwind_protect this.XData = shCheckData (t.(this.XVariable), 'XVariable'); this.YData = shCheckData (t.(this.YVariable), 'YVariable'); if (isempty (this.GroupVariable)) this.GroupData = []; else this.GroupData = t.(this.GroupVariable); endif this.Internal_ = true; if (this.LabelsAuto_(1)) this.XLabel = this.XVariable; endif if (this.LabelsAuto_(2)) this.YLabel = this.YVariable; endif if (this.LabelsAuto_(3)) this.LegendTitle = this.GroupVariable; endif unwind_protect_cleanup this.Internal_ = false; this.Drawn_ = drawn; this.Reading_ = false; end_unwind_protect refit (this); endfunction ## Bring the bins, limits, styles and legend that follow the data in line ## with it, then draw. function refit (this) if (this.Reading_) return; endif x = this.XData; y = this.YData; [names, g] = shGroups (this.GroupData); ng = max (numel (names), 1); this.Internal_ = true; unwind_protect ## Limits: the range of each sample if (this.LimitsAuto_(1)) this.XLimits = shRange (x); endif if (this.LimitsAuto_(2)) this.YLimits = shRange (y); endif ## Colours and line styles, one for each group if (this.StyleAuto_) co = get (groot, 'defaultaxescolororder'); this.Color = co(mod ((1:ng) - 1, rows (co)) + 1,:); ls = {'-', ':', '-.', '--'}; styles = ls(mod ((1:ng) - 1, 4) + 1); if (ng == 1) this.LineStyle = styles{1}; else this.LineStyle = styles; endif endif if (this.LegendAuto_) this.LegendVisible = shOnOff (! isempty (names)); endif ## Bins, for each variable and each group, as histcounts chooses them ## or from the numbers or widths set if (strcmp (this.HistogramDisplayStyle, 'smooth')) this.NumBins = []; this.BinWidths = []; elseif (numel (x) == numel (y)) [nb, bw] = shBins (x, y, g, ng, this.NumBins, this.BinWidths, ... this.BinsAuto_, this.WidthsSet_); this.NumBins = nb; this.BinWidths = bw; endif unwind_protect_cleanup this.Internal_ = false; end_unwind_protect redraw (this); endfunction ## Draw the chart from scratch into its axes. function redraw (this) if (! this.Drawn_ || isempty (this.Axes_) || ! all (ishghandle (this.Axes_))) return; endif ax = this.Axes_; this.Redrawing_ = true; unwind_protect if (! isempty (this.Legend_) && ishghandle (this.Legend_)) delete (this.Legend_); endif this.Legend_ = []; for k = 1:3 delete (get (ax(k), 'children')); endfor unwind_protect_cleanup this.Redrawing_ = false; end_unwind_protect ## A child whose deletion by anyone else releases the axes text (ax(1), NaN, NaN, '', 'deletefcn', @(~, ~) release (this)); x = this.XData; y = this.YData; [names, g] = shGroups (this.GroupData); ng = max (numel (names), 1); if (isempty (names)) g = ones (numel (x), 1); endif vis = this.Visible; set (ax, 'units', this.Units, 'visible', vis, 'fontname', ... this.FontName, 'fontsize', this.FontSize, 'nextplot', 'add'); ## The scatter plot, one set of markers for each group hs = zeros (1, ng); for k = 1:ng take = (g == k); c = shPick (this.Color, k); m = shPickStyle (this.MarkerStyle, k); sz = shPick (this.MarkerSize(:), k); if (strcmp (this.MarkerFilled, 'on')) hs(k) = scatter (ax(1), x(take), y(take), sz, c, m, 'filled'); else hs(k) = scatter (ax(1), x(take), y(take), sz, c, m); endif ## Under gnuplot a scatter is an hggroup with no marker alpha if (isprop (hs(k), 'markerfacealpha')) set (hs(k), 'markerfacealpha', this.MarkerAlpha, ... 'markeredgealpha', this.MarkerAlpha); endif set (hs(k), 'visible', vis); if (! isempty (names)) set (hs(k), 'displayname', names{k}); endif endfor set (ax(1), 'xlim', this.XLimits, 'ylim', this.YLimits, 'box', 'on'); xlabel (ax(1), this.XLabel, 'fontname', this.FontName); ylabel (ax(1), this.YLabel, 'fontname', this.FontName); ## The histograms, as densities, one outline, set of bars or curve for ## each group style = this.HistogramDisplayStyle; for k = 1:ng take = (g == k); c = shPick (this.Color, k); ls = shPickStyle (this.LineStyle, k); lw = shPick (this.LineWidth(:), k); for d = 1:2 if (d == 1) v = x(take); lims = this.XLimits; else v = y(take); lims = this.YLimits; endif v = v(isfinite (v)); if (isempty (v)) continue; endif if (strcmp (style, 'smooth')) [f, xi] = ksdensity (v); px = xi(:)'; py = f(:)'; else w = this.BinWidths(d, min (k, columns (this.BinWidths))); [n, e] = histcounts (v, 'BinWidth', w); f = n / (numel (v) * w); [px, py] = shStairs (e, f); endif hax = ax(d + 1); if (d == 2) [px, py] = deal (py, px); endif if (strcmp (style, 'bar')) if (d == 1) [bx, by] = shBars (e, f); else [by, bx] = shBars (e, f); endif patch (hax, bx, by, c, 'facealpha', 0.5, 'edgecolor', c, ... 'linestyle', ls, 'linewidth', lw, 'visible', vis); else line (hax, px, py, 'color', c, 'linestyle', ls, 'linewidth', lw, ... 'visible', vis); endif endfor endfor ## Where each axes goes, the histograms beside the scatter plot and ## sharing its limits [ps, px, py, pl] = layout (this); set (ax(1), 'position', ps); set (ax(2), 'position', px, 'xlim', this.XLimits, 'xticklabel', {}, ... 'box', 'off'); set (ax(3), 'position', py, 'ylim', this.YLimits, 'yticklabel', {}, ... 'box', 'off'); top = any (strcmp (this.ScatterPlotLocation, {'SouthWest', 'SouthEast'})); right = any (strcmp (this.ScatterPlotLocation, {'SouthWest', ... 'NorthWest'})); if (top) set (ax(2), 'xaxislocation', 'bottom'); else set (ax(2), 'xaxislocation', 'top'); endif if (right) set (ax(3), 'yaxislocation', 'left'); else set (ax(3), 'yaxislocation', 'right'); endif if (strcmp (this.XHistogramDirection, 'down')) set (ax(2), 'ydir', 'reverse'); else set (ax(2), 'ydir', 'normal'); endif if (strcmp (this.YHistogramDirection, 'left')) set (ax(3), 'xdir', 'reverse'); else set (ax(3), 'xdir', 'normal'); endif set (ax(2:3), 'color', 'none'); set (ax(2), 'ytick', [], 'ycolor', 'none'); set (ax(3), 'xtick', [], 'xcolor', 'none'); ## The title over the whole chart title (ax(1), ''); title (ax(2), ''); if (top) title (ax(2), this.Title, 'fontname', this.FontName); else title (ax(1), this.Title, 'fontname', this.FontName); endif ## Each group was added to the one before; the axes are left as plain ## axes are, so a later plot replaces them rather than adding to them set (ax, 'nextplot', 'replace'); ## The legend of the groups, in the corner the histograms leave empty if (strcmp (this.LegendVisible, 'on') && ! isempty (names)) this.Legend_ = legend (ax(1), hs, names(:)'); set (this.Legend_, 'units', this.Units, 'position', pl, ... 'visible', vis, 'fontname', this.FontName); if (! isempty (this.LegendTitle)) title (this.Legend_, this.LegendTitle); endif endif endfunction ## The positions of the scatter plot, the two histograms and the legend. function [ps, px, py, pl] = layout (this) p = this.ScatterPlotProportion; if (strcmp (this.PositionConstraint, 'outerposition')) o = this.OuterPosition; ti = get (this.Axes_(1), 'tightinset'); pad = 0.02; L = max (0.1 * o(3), ti(1) + pad * o(3)); B = max (0.1 * o(4), ti(2) + pad * o(4)); R = 0.05 * o(3); T = 0.08 * o(4); box = [o(1) + L, o(2) + B, o(3) - L - R, o(4) - B - T]; box(3:4) = max (box(3:4), 0.01); ps = [0, 0, p * box(3), p * box(4)]; else ps = this.Position; box = [0, 0, ps(3) / p, ps(4) / p]; endif gap = 0.01; w = box(3) * (1 - p) - gap; h = box(4) * (1 - p) - gap; loc = this.ScatterPlotLocation; west = any (strcmp (loc, {'SouthWest', 'NorthWest'})); south = any (strcmp (loc, {'SouthWest', 'SouthEast'})); if (strcmp (this.PositionConstraint, 'outerposition')) ps(1) = box(1) + ! west * (box(3) - ps(3)); ps(2) = box(2) + ! south * (box(4) - ps(4)); else box(1) = ps(1) - ! west * (box(3) - ps(3)); box(2) = ps(2) - ! south * (box(4) - ps(4)); endif if (south) px = [ps(1), ps(2) + ps(4) + gap, ps(3), h]; else px = [ps(1), ps(2) - gap - h, ps(3), h]; endif if (west) py = [ps(1) + ps(3) + gap, ps(2), w, ps(4)]; else py = [ps(1) - gap - w, ps(2), w, ps(4)]; endif pl = [py(1), px(2), w, h]; this.Internal_ = true; unwind_protect this.Position = ps; unwind_protect_cleanup this.Internal_ = false; end_unwind_protect endfunction endmethods endclassdef ## The observations, as a numeric column function v = shCheckData (val, name) if (! ((isnumeric (val) || islogical (val)) && isreal (val) && (isvector (val) || isempty (val)))) error (strcat ("stats.chart.ScatterHistogramChart: '%s' must be a", ... " real numeric vector."), name); endif v = double (val(:)); endfunction ## The names of the groups in order of first appearance, and the group of ## each observation, 0 where it is missing function [names, g] = shGroups (gd) names = cell (0, 1); g = zeros (numel (gd), 1); if (isempty (gd)) return; endif if (isa (gd, 'categorical')) miss = isundefined (gd); t = cellstr (gd); elseif (iscellstr (gd) || isa (gd, 'string')) t = cellstr (gd); miss = cellfun (@isempty, t); if (isa (gd, 'string')) miss |= ismissing (gd); endif elseif (isnumeric (gd) || islogical (gd)) miss = isnan (double (gd)); t = arrayfun (@(v) sprintf ('%g', v), double (gd), 'UniformOutput', false); if (islogical (gd)) t(gd) = {'true'}; t(! gd) = {'false'}; endif else error (strcat ("stats.chart.ScatterHistogramChart: 'GroupData' must", ... " hold categories, text, numbers or logical values.")); endif t = t(:); [names, i] = unique (t(! miss), 'first'); [~, o] = sort (i); names = names(o); [~, g(! miss)] = ismember (t(! miss), names); endfunction ## Bins for each variable and group: what histcounts chooses, or from the ## numbers or widths set, one column for each group function [nb, bw] = shBins (x, y, g, ng, nbSet, bwSet, auto, widths) nb = zeros (2, ng); bw = zeros (2, ng); if (all (g == 0)) g = ones (numel (x), 1); endif for k = 1:ng for d = 1:2 if (d == 1) v = x(g == k); else v = y(g == k); endif v = v(isfinite (v)); if (isempty (v)) continue; endif if (auto) [~, e] = histcounts (v); elseif (widths) [~, e] = histcounts (v, 'BinWidth', bwSet(d, min (k, columns (bwSet)))); else [~, e] = histcounts (v, nbSet(d, min (k, columns (nbSet)))); endif nb(d,k) = numel (e) - 1; bw(d,k) = e(2) - e(1); endfor endfor endfunction ## The outline of a histogram with edges E and heights F function [px, py] = shStairs (e, f) n = numel (f); px = reshape ([e(1:n); e(2:n+1)], 1, []); py = reshape ([f; f], 1, []); px = [e(1), px, e(end)]; py = [0, py, 0]; endfunction ## The patches of the bars of a histogram with edges E and heights F function [bx, by] = shBars (e, f) n = numel (f); bx = [e(1:n); e(2:n+1); e(2:n+1); e(1:n)]; by = [zeros(1, n); zeros(1, n); f(:)'; f(:)']; endfunction ## The range of the finite values, or [0, 1] function lims = shRange (v) v = v(isfinite (v)); if (isempty (v)) lims = [0, 1]; elseif (min (v) == max (v)) lims = min (v) + [-0.5, 0.5]; else lims = [min(v), max(v)]; endif endfunction ## Row K of a matrix of values for the groups, the last one repeated function v = shPick (vals, k) v = vals(min (k, rows (vals)),:); endfunction ## Style K of a character vector or a cell array of styles function s = shPickStyle (styles, k) if (iscell (styles)) s = styles{min (k, numel (styles))}; else s = styles; endif endfunction ## 'on' or 'off' function s = shOnOff (tf) if (tf) s = 'on'; else s = 'off'; endif endfunction ## Numbers or widths of bins: a positive scalar, a 2-by-1 vector or a ## 2-by-N matrix for N groups function v = shCheckBins (val, ng, name, integer) ok = isnumeric (val) && isreal (val) && ! isempty (val) ... && all (isfinite (val(:))) && all (val(:) > 0); if (ok && integer) ok = all (val(:) == fix (val(:))); endif if (ok && isscalar (val)) v = [val; val]; elseif (ok && isequal (size (val), [2, 1])) v = val; elseif (ok && rows (val) == 2 && columns (val) == max (ng, 1)) v = val; else error (strcat ("stats.chart.ScatterHistogramChart: '%s' must be a", ... " positive scalar, a 2-by-1 vector or a 2-by-N matrix", ... " for N groups."), name); endif v = double (v); endfunction ## Limits: a 1-by-2 increasing vector function v = shCheckLimits (val, name) if (! (isnumeric (val) && isreal (val) && numel (val) == 2 && all (isfinite (val)) && val(2) > val(1))) error (strcat ("stats.chart.ScatterHistogramChart: '%s' must be a", ... " 1-by-2 increasing numeric vector."), name); endif v = double (val(:)'); endfunction ## Styles: one of LIST, or a cell array or string array of them function v = shCheckStyles (val, list, name) if (isa (val, 'string')) val = cellstr (val); if (numel (val) == 1) val = val{1}; endif endif if (ischar (val) && any (strcmp (val, list))) v = val; elseif (iscellstr (val) && ! isempty (val) && all (ismember (val, list))) v = val(:)'; else error ("stats.chart.ScatterHistogramChart: '%s' must be one of %s.", ... name, strjoin (strcat ("'", list, "'"), ', ')); endif endfunction ## Positive values, a scalar or one for each group function v = shCheckPositive (val, name) if (! (isnumeric (val) && isreal (val) && isvector (val) && all (isfinite (val)) && all (val > 0))) error (strcat ("stats.chart.ScatterHistogramChart: '%s' must be", ... " positive."), name); endif v = double (val(:)'); endfunction ## Variable names: a character vector or a string scalar function out = shCheckName (val, name) if (isa (val, 'string') && isscalar (val)) val = char (val); endif if (! (ischar (val) && (isrow (val) || isempty (val)))) error (strcat ("stats.chart.ScatterHistogramChart: '%s' must be a", ... " character vector."), name); endif out = val; endfunction ## Text: a character vector, a string, or a cell array of them for lines function out = shCheckText (val, name) if (isa (val, 'string')) val = cellstr (val); if (numel (val) == 1) val = val{1}; endif endif if (! ((ischar (val) && (isrow (val) || isempty (val))) || iscellstr (val))) error ("stats.chart.ScatterHistogramChart: '%s' must be text.", name); endif out = val; endfunction ## One of a list of names, case free function out = shCheckOneOf (val, list, name) if (isa (val, 'string') && isscalar (val)) val = char (val); endif if (! (ischar (val) && any (strcmpi (val, list)))) error ("stats.chart.ScatterHistogramChart: '%s' must be one of %s.", ... name, strjoin (strcat ("'", list, "'"), ', ')); endif out = lower (val); endfunction ## A position, as a 1-by-4 vector with positive width and height function out = shCheckPosition (val, name) if (! (isnumeric (val) && isreal (val) && numel (val) == 4 && all (isfinite (val)) && val(3) > 0 && val(4) > 0)) error (strcat ("stats.chart.ScatterHistogramChart: '%s' must be a", ... " 1-by-4 vector with positive width and height."), name); endif out = double (val(:)'); endfunction %!shared x, y, g %! x = [2.1; 3.4; 1.9; 5.6; 4.4; 3.8; 2.7; 6.1; 3.3; 4.9; 1.2; 2.8]; %! y = [1.2; 2.8; 0.9; 2.2; 3.1; 1.7; 2.5; 0.4; 1.9; 3.6; 2.0; 1.1]; %! g = categorical ({'a';'b';'a';'b';'a';'b';'a';'b';'a';'c';'c';'c'}); %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = scatterhistogram (x, y); %! assert_equal (class (h), 'stats.chart.ScatterHistogramChart'); %! assert_equal (h.XData, x); %! assert_equal (h.YData, y); %! assert_equal (h.GroupData, []); %! assert_equal (h.NumBins, [4; 4]); %! assert_equal (h.BinWidths, [2; 1]); %! assert_equal (h.XLimits, [1.2, 6.1]); %! assert_equal (h.YLimits, [0.4, 3.6]); %! assert_equal (h.Color, [0, 0.447, 0.741]); %! assert_equal (h.HistogramDisplayStyle, 'stairs'); %! assert_equal (h.ScatterPlotLocation, 'SouthWest'); %! assert_equal (h.ScatterPlotProportion, 0.75); %! assert_equal (h.XHistogramDirection, 'up'); %! assert_equal (h.YHistogramDirection, 'right'); %! assert_equal (h.LineStyle, '-'); %! assert_equal (h.LineWidth, 0.5); %! assert_equal (h.MarkerStyle, 'o'); %! assert_equal (h.MarkerSize, 36); %! assert_equal (h.MarkerFilled, 'on'); %! assert_equal (h.MarkerAlpha, 1); %! assert_equal (h.LegendVisible, 'off'); %! assert_equal (h.OuterPosition, [0, 0, 1, 1]); %! assert_equal (h.Parent, hf); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = scatterhistogram (x, y, 'GroupData', g); %! assert_equal (h.LegendVisible, 'on'); %! assert_equal (h.Color, [0, 0.447, 0.741; 0.85, 0.325, 0.098; ... %! 0.929, 0.694, 0.125]); %! assert_equal (h.LineStyle, {'-', ':', '-.'}); %! assert_equal (h.NumBins, [3, 2, 1; 2, 2, 2]); %! assert_equal (h.BinWidths, [2, 3, 5; 2, 2, 3]); %! tag = 'stats.chart.ScatterHistogramChart'; %! lg = findall (hf, 'type', 'axes', 'tag', 'legend'); %! s = get (lg, 'string'); # a row under gnuplot, a column otherwise %! assert_equal (s(:), {'a'; 'b'; 'c'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = scatterhistogram (x, y, 'GroupData', {'q';'p';'q';'p';'q';'p'; ... %! 'q';'p';'q';'p';'q';'p'}); %! assert_equal (rows (h.Color), 2); %! lg = findall (hf, 'type', 'axes', 'tag', 'legend'); %! s = get (lg, 'string'); # a row under gnuplot, a column otherwise %! assert_equal (s(:), {'q'; 'p'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = scatterhistogram (x, y, 'NumBins', 4); %! assert_equal (h.NumBins, [4; 4]); %! assert_equal (h.BinWidths, [1.3; 0.9], -1e-12); %! h.NumBins = [3; 5]; %! assert_equal (h.NumBins, [3; 5]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = scatterhistogram (x, y, 'NumBins', 4, 'BinWidths', 1); %! assert_equal (h.NumBins, [6; 4]); %! assert_equal (h.BinWidths, [1; 1]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = scatterhistogram (x, y, 'HistogramDisplayStyle', 'smooth'); %! assert_equal (h.NumBins, []); %! assert_equal (h.BinWidths, []); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## Each histogram is a density: the area under it is one %! hf = figure ('visible', 'off'); %! unwind_protect %! h = scatterhistogram (x, y); %! tag = 'stats.chart.ScatterHistogramChart'; %! ax = findall (hf, 'type', 'axes', 'tag', tag); %! ln = [findall(ax, 'type', 'line'); findall(ax, 'type', 'patch')]; %! for k = 1:numel (ln) %! px = get (ln(k), 'xdata'); %! py = get (ln(k), 'ydata'); %! if (numel (px) > 2) %! area = max (abs (trapz (px(:), py(:))), abs (trapz (py(:), px(:)))); %! assert_equal (area, 1, -1e-12); %! endif %! endfor %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = scatterhistogram (x, y); %! h.XLimits = [0, 10]; %! assert_equal (h.XLimits, [0, 10]); %! assert_equal (xlim (h), [0, 10]); %! ylim (h, [-1, 5]); %! assert_equal (h.YLimits, [-1, 5]); %! h.XData = x + 100; %! assert_equal (h.XLimits, [0, 10]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = scatterhistogram ([x; NaN], [y; 2]); %! assert_equal (numel (h.XData), 13); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## The histograms line the sides away from the scatter plot's corner %! hf = figure ('visible', 'off'); %! unwind_protect %! h = scatterhistogram (x, y); %! tag = 'stats.chart.ScatterHistogramChart'; %! ax = findall (hf, 'type', 'axes', 'tag', tag); %! ps = h.Position; %! p = cell2mat (get (ax, 'position')); %! top = p(:,2) > ps(2) + ps(4) - 1e-9; %! right = p(:,1) > ps(1) + ps(3) - 1e-9; %! assert_equal ([sum(top), sum(right)], [1, 1]); %! h.ScatterPlotLocation = 'northeast'; %! assert_equal (h.ScatterPlotLocation, 'NorthEast'); %! ps = h.Position; %! p = cell2mat (get (ax, 'position')); %! below = p(:,2) + p(:,4) < ps(2) + 1e-9; %! left = p(:,1) + p(:,3) < ps(1) + 1e-9; %! assert_equal ([sum(below), sum(left)], [1, 1]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = scatterhistogram (x, y, 'XHistogramDirection', 'down', ... %! 'YHistogramDirection', 'left', ... %! 'MarkerFilled', 'off', 'MarkerAlpha', 0.5, ... %! 'Title', 'T', 'XLabel', 'XL', 'YLabel', 'YL'); %! assert_equal (h.XHistogramDirection, 'down'); %! assert_equal (h.YHistogramDirection, 'left'); %! assert_equal (h.MarkerFilled, 'off'); %! assert_equal (h.MarkerAlpha, 0.5); %! assert_equal (h.Title, 'T'); %! assert_equal (get (get (gca (), 'xlabel'), 'string'), 'XL'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = scatterhistogram (x, y, 'GroupData', g, 'Color', ... %! [1, 0, 0; 0, 1, 0; 0, 0, 1], 'LineStyle', ... %! {'-', '--', ':'}, 'MarkerStyle', {'o', 'x', 's'}, ... %! 'MarkerSize', [4, 6, 8], 'LineWidth', [1, 2, 3]); %! assert_equal (h.Color, [1, 0, 0; 0, 1, 0; 0, 0, 1]); %! assert_equal (h.LineStyle, {'-', '--', ':'}); %! assert_equal (h.MarkerStyle, {'o', 'x', 's'}); %! assert_equal (h.MarkerSize, [4, 6, 8]); %! assert_equal (h.LineWidth, [1, 2, 3]); %! h.GroupData = g(end:-1:1); %! assert_equal (h.Color, [1, 0, 0; 0, 1, 0; 0, 0, 1]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!shared shS %! shS = struct ('XData', [1; 2; 3], 'YData', [4; 5; 6], 'GroupData', [], ... %! 'SourceTable', [], 'XVariable', '', 'YVariable', '', ... %! 'GroupVariable', ''); %!error ... %! stats.chart.ScatterHistogramChart ([]) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'XData', {1, 2}}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'YData', [1; 2]}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'GroupData', [1; 2]}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'GroupData', ones(2, 2)}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'SourceTable', 5}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, ... %! {'HistogramDisplayStyle', 'area'}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'NumBins', 0}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'NumBins', [3, 5]}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'BinWidths', [1, 0.5]}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, ... %! {'ScatterPlotLocation', 'Middle'}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'ScatterPlotProportion', 1.2}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'XHistogramDirection', 'left'}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'YHistogramDirection', 'up'}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'XLimits', [2, 1]}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'Color', 'r'}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'LineStyle', '*'}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'LineWidth', 0}) ## test.m ends a pattern at the first '>' of its line, and the list of ## markers holds one, so the message is matched only as far as 'v' %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'MarkerStyle', 'q'}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'MarkerFilled', 1}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'MarkerAlpha', 2}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'LegendVisible', 'yes'}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'Title', 5}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'FontSize', -1}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'Position', [0, 0, 1]}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, {'Visible', 'maybe'}) %!error ... %! stats.chart.ScatterHistogramChart ([], shS, ... %! {'SourceTable', table([1; 2], 'VariableNames', {'X'}), ... %! 'XVariable', 'X', 'YVariable', 'Q'}) statistics-release-2.1.2/inst/Plotting/andrewsplot.m000066400000000000000000000221541527311104300226220ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} andrewsplot (@var{x}) ## @deftypefnx {statistics} {} andrewsplot (@var{x}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {statistics} {} andrewsplot (@var{ax}, @dots{}) ## @deftypefnx {statistics} {@var{h} =} andrewsplot (@dots{}) ## ## Create an Andrews plot of the multivariate data in @var{x}. ## ## @code{andrewsplot (@var{x})} plots each observation (row) of the ## @code{n}-by-@code{p} matrix @var{x} as a smooth curve defined by the finite ## Fourier series ## ## @example ## f_i(t) = x_i1/sqrt(2) + x_i2 sin(2*pi*t) + x_i3 cos(2*pi*t) ## + x_i4 sin(4*pi*t) + x_i5 cos(4*pi*t) + @dots{} ## @end example ## ## @noindent ## evaluated over @math{t} in the interval @math{[0,1]}, where @var{x_ij} is the ## @math{j}-th variable of the @math{i}-th observation. ## ## The following name/value pairs are accepted: ## ## @table @asis ## @item @qcode{"Group"} ## A grouping variable (numeric, logical, character, string, or cell array of ## strings) with one entry per row of @var{x}. Curves are colored by group. ## ## @item @qcode{"Standardize"} ## Controls how the columns of @var{x} are transformed before the curves are ## computed: @qcode{"off"} (default) uses the raw data, @qcode{"on"} centers and ## scales each column to zero mean and unit standard deviation, @qcode{"PCA"} ## uses the principal component scores, and @qcode{"PCAStd"} uses the principal ## component scores of the standardized data. ## ## @item @qcode{"Quantile"} ## A scalar @var{alpha} in the interval @math{(0,1)}. Instead of one curve per ## observation, only three curves per group are drawn: the pointwise median and ## the @var{alpha} and @math{1-}@var{alpha} quantiles of the group's curves. ## @end table ## ## @code{andrewsplot (@var{ax}, @dots{})} plots into the axes @var{ax}. ## ## The optional output @var{h} is a vector of handles to the plotted lines: one ## per observation, or three per group when @qcode{"Quantile"} is used. ## ## @seealso{parallelcoords, glyphplot, pca} ## @end deftypefn function h = andrewsplot (varargin) ## Optional leading axes handle hax = []; if (numel (varargin) > 0 && isaxes (varargin{1})) hax = varargin{1}; varargin(1) = []; endif if (numel (varargin) < 1) print_usage (); endif X = varargin{1}; varargin(1) = []; if (! isnumeric (X) || ! isreal (X) || ndims (X) > 2) error ("andrewsplot: X must be a real numeric matrix."); endif n = rows (X); ## Parse name/value options group = []; standardize = "off"; alpha = []; if (mod (numel (varargin), 2) != 0) error ("andrewsplot: name/value arguments must come in pairs."); endif for i = 1:2:numel (varargin) name = varargin{i}; value = varargin{i+1}; if (! ischar (name)) error ("andrewsplot: property names must be strings."); endif switch (lower (name)) case "group" group = value; case "standardize" standardize = value; case "quantile" alpha = value; otherwise error ("andrewsplot: unknown property '%s'.", name); endswitch endfor ## Validate before plotting, so a bad option never leaves a stray figure if (! isempty (alpha)) if (! (isscalar (alpha) && isreal (alpha) && alpha > 0 && alpha < 1)) error ("andrewsplot: Quantile ALPHA must be a scalar in (0,1)."); endif endif ## Standardize the data switch (lower (standardize)) case "off" Z = X; case "on" Z = zscore (X); case "pca" [~, Z] = pca (X); case "pcastd" [~, Z] = pca (zscore (X)); otherwise error ("andrewsplot: invalid Standardize option '%s'.", standardize); endswitch ## Grouping if (isempty (group)) gidx = ones (n, 1); gnames = {"1"}; else [gidx, gnames] = grp2idx (group); if (numel (gidx) != n) error ("andrewsplot: GROUP must have one entry per row of X."); endif endif k = numel (gnames); gcol = lines (k); ## Evaluate the Andrews curves f_i(t) over t in [0,1] t = linspace (0, 1, 1001); p = columns (Z); F = (Z(:,1) / sqrt (2)) * ones (1, numel (t)); for j = 2:p kf = floor (j / 2); if (mod (j, 2) == 0) F += Z(:,j) * sin (2 * pi * kf * t); else F += Z(:,j) * cos (2 * pi * kf * t); endif endfor if (isempty (hax)) hax = newplot (); else newplot (hax); endif old_hold = ishold (hax); hold (hax, "on"); h = []; if (isempty (alpha)) ## One curve per observation, colored by group for i = 1:n h(end+1) = line (hax, t, F(i,:), "color", gcol(gidx(i),:)); endfor else ## Median and alpha / 1-alpha quantile curves per group for g = 1:k Fg = F(gidx == g, :); med = median (Fg, 1); lo = quantile (Fg, alpha, 1); hi = quantile (Fg, 1 - alpha, 1); h(end+1) = line (hax, t, med, "color", gcol(g,:), "linewidth", 2); h(end+1) = line (hax, t, lo, "color", gcol(g,:), "linestyle", "--"); h(end+1) = line (hax, t, hi, "color", gcol(g,:), "linestyle", "--"); endfor endif xlabel (hax, "t"); ylabel (hax, "f(t)"); if (k > 1 && ! isempty (group)) warning ("off", "Octave:legend:unimplemented-location", "local"); ## One representative line handle per group for the legend if (isempty (alpha)) [~, first] = unique (gidx, "first"); legend (hax, h(sort (first)), gnames, "location", "best"); else legend (hax, h(1:3:end), gnames, "location", "best"); endif endif if (! old_hold) hold (hax, "off"); endif if (nargout == 0) clear h; endif endfunction %!demo %! ## Andrews plot of Fisher's iris data, grouped by species. %! %! load fisheriris; %! andrewsplot (meas, "Group", species); %!demo %! ## The same data with median and quartile curves per species. %! %! load fisheriris; %! andrewsplot (meas, "Group", species, "Quantile", 0.25); ## Test output %!test %! hf = figure ("visible", "off"); %! unwind_protect %! X = [1 2 3 4; 5 6 7 8; 2 3 1 4]; %! h = andrewsplot (X); %! assert_equal (numel (h), 3); %! t = get (h(1), "xdata"); %! assert_equal (numel (t), 1001); %! assert_equal (t([1, 501, 1001]), [0, 0.5, 1], 1e-12); %! y1 = get (h(1), "ydata"); %! assert_equal (y1([1, 501, 1001]), [3.70711, -2.29289, 3.70711], 1e-4); %! y3 = get (h(3), "ydata"); %! assert_equal (y3([1, 501, 1001]), [2.41421, 0.41421, 2.41421], 1e-4); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # Standardize "on" (z-score each column) %! hf = figure ("visible", "off"); %! unwind_protect %! X = [1 2 3 4; 5 6 7 8; 2 3 1 4]; %! h = andrewsplot (X, "Standardize", "on"); %! y1 = get (h(1), "ydata"); %! assert_equal (y1([1, 501, 1001]), [-0.78436, -0.34792, -0.78436], 1e-4); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # Standardize "pca" %! hf = figure ("visible", "off"); %! unwind_protect %! X = [1 2 3 4; 5 6 7 8; 2 3 1 4]; %! h = andrewsplot (X, "Standardize", "pca"); %! y1 = get (h(1), "ydata"); %! assert_equal (y1([1, 501, 1001]), [-1.76701, -1.76701, -1.76701], 1e-4); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # grouping and quantile mode give three curves per group %! hf = figure ("visible", "off"); %! unwind_protect %! X = [1 2 3 4; 5 6 7 8; 2 3 1 4; 3 1 4 1; 5 9 2 6; 4 2 1 3]; %! g = [1 1 1 2 2 2]'; %! h = andrewsplot (X, "Group", g, "Quantile", 0.25); %! assert_equal (numel (h), 6); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error andrewsplot () %!error andrewsplot ({1}) %!error ... %! andrewsplot (ones (3, 2), "Group") %!error ... %! andrewsplot (ones (3, 2), "bogus", 1) %!error ... %! andrewsplot (ones (3, 2), "Standardize", "xxx") %!error ... %! andrewsplot (ones (3, 2), "Group", [1 2]) %!error ... %! andrewsplot (ones (3, 2), "Quantile", 1.5) ## A bad Quantile ALPHA must error before any figure is created (no stray figure) %!test %! nfig = numel (get (0, "children")); %! fail ('andrewsplot (ones (3, 2), "Quantile", 1.5)', ... %! 'andrewsplot: Quantile ALPHA must be a scalar in .0,1..'); %! assert_equal (numel (get (0, "children")), nfig); statistics-release-2.1.2/inst/Plotting/bar3.m000066400000000000000000000432431527311104300211110ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} bar3 (@var{z}) ## @deftypefnx {statistics} {} bar3 (@var{y}, @var{z}) ## @deftypefnx {statistics} {} bar3 (@dots{}, @var{width}) ## @deftypefnx {statistics} {} bar3 (@dots{}, @var{style}) ## @deftypefnx {statistics} {} bar3 (@dots{}, @var{color}) ## @deftypefnx {statistics} {} bar3 (@dots{}, @var{name}, @var{value}) ## @deftypefnx {statistics} {} bar3 (@var{ax}, @dots{}) ## @deftypefnx {statistics} {@var{p} =} bar3 (@dots{}) ## ## Plot a 3D bar graph. ## ## @code{bar3 (@var{z})} plots 3D bar graph for the elements of @var{z}. Each ## bar corresponds to an element in @var{z}, which can be a scalar, vector, or ## 2D matrix. By default, each column in @var{z} is considered as a series and ## it is handled as a distinct series of bars. When @var{z} is a vector, unlike ## MATLAB, which plots it as a single series of bars, Octave discriminates ## between a row and column vector of @var{z}. Hence, when @var{z} is column ## vector, it is plotted as a single series of bars (same color), whereas when ## @var{z} is row vector, each bar is plotted as a different group (different ## colors). For an @math{M*N} matrix, the function plots the bars corresponding ## to each row on the @qcode{y-axis} ranging from @math{1} to @math{M} and each ## column on the @qcode{x-axis} ranging from @math{1} to @math{N}. ## ## @code{bar3 (@var{y}, @var{z})} plots a 3D bar graph of the elements in ## @var{z} at the @qcode{y-values} specified in @var{y}. It should be noted ## that @var{y} only affects the tick names along the @qcode{y-axis} rather the ## actual values. If you want to specify non-numerical values for @var{y}, you ## can specify it with the paired @var{name}/@var{value} syntax shown below. ## ## @code{bar3 (@dots{}, @var{width})} sets the width of the bars along the ## @qcode{x-} and @qcode{y-axes} and controls the separation of bars among each ## other. @var{width} can take any value in the range @math{(0,1]}. By default, ## @var{width} is 0.8 and the bars have a small separation. If width is 1, the ## bars touch one another. Alternatively, you can define @var{width} as a two- ## element vector using the paired @var{name}/@var{value} syntax shown below, in ## which case you can control the bar separation along each axis independently. ## ## @code{bar3 (@dots{}, @var{style})} specifies the style of the bars, where ## @var{style} can be @qcode{'detached'}, @qcode{'grouped'}, or ## @qcode{'stacked'}. The default style is @qcode{'detached'}. ## ## @code{bar3 (@dots{}, @var{color})} displays all bars using the color ## specified ## by color. For example, use @qcode{'red'} or @qcode{'r'} to specify all red ## bars. When you want to specify colors for several groups, @var{color} can be ## a cellstr vector with each element specifying the color of each group. ## @var{color} can also be specified as a numerical @math{M*3} matrix, where ## each row corresponds to a RGB value with its elements in the range ## @math{[0,1]}. If only one color is specified, then it applies to all bars. ## If the number of colors equals the number of groups, then each color is ## applied to each group. If the number of colors equals the number of elements ## in @var{z}, then each individual bar is assigned the particular color. You ## can also define @var{color} using the paired @var{name}/@var{value} syntax ## shown below. ## ## @code{bar3 (@dots{}, @var{name}, @var{value})} specifies one or more of the ## following name/value pairs: ## ## @multitable @columnfractions 0.2 0.75 ## @headitem Name @tab Value ## @item @qcode{'width'} @tab A two-element vector specifying the width ## of the bars along the @qcode{x-} and @qcode{y-axes}, respectively. Each ## element must be in the range @math{(0,1]}. ## ## @item @qcode{'color'} @tab A character or a cellstr vector, or a ## numerical @math{M*3} matrix following the same conventions as the @var{color} ## input argument. ## ## @item @qcode{'xlabel'} @tab A cellstr vector specifying the group names ## along the @qcode{x-axis}. ## ## @item @qcode{'ylabel'} @tab A cellstr vector specifying the names of the ## bars in the same series along the @qcode{y-axis}. ## @end multitable ## ## @code{bar3 (@var{ax}, @dots{})} can also take an axes handle @var{ax} as a ## first argument in which case it plots into the axes specified by @var{ax} ## instead of into the current axes specified by @code{gca ()}. The optional ## argument @var{ax} can precede any of the input argument combinations in the ## previous syntaxes. ## ## @code{@var{p} = bar3 (@dots{})} returns a patch handle @var{p}, which can be ## used to set properties of the bars after displaying the 3D bar graph. ## ## @seealso{boxplot, hist3} ## @end deftypefn function [varargout] = bar3 (varargin) if (nargin < 1) print_usage (); endif ## Check if first input is an axes handle if (isaxes (varargin{1})) ax = varargin{1}; varargin(1) = []; new_axes = false; else new_axes = true; endif ## Parse input argument Z if (numel (varargin) < 1) print_usage (); endif z = varargin{1}; varargin(1) = []; if (! isnumeric (z)) error ("bar3: Z must be numeric."); endif ## Add defaults y = []; width = 0.8; depth = 0.8; style = 'detached'; color = []; xlabel = []; ylabel = []; ## Valid Colors for input validation vc = {'red', 'r', 'green', 'g', 'blue', 'b', 'cyan', 'c', ... 'magenta', 'm', 'yellow', 'y', 'black', 'k', 'white', 'w'}; ## Parse extra input arguments while (numel (varargin) > 0) tmp = varargin{1}; if (isnumeric (tmp) && isempty (y)) if (isvector (tmp) && isvector (z) && numel (tmp) == numel (z)) y = z; z = tmp; elseif (all (size (tmp) > 1) && size (tmp, 1) == numel (z) && isvector (z)) y = z; z = tmp; elseif (isscalar (tmp)) y = NaN; if (tmp > 0 && tmp <= 1) width = tmp; depth = tmp; else error ("bar3: WIDTH must be a scalar in the range (0,1]."); endif elseif (size (tmp, 2) == 3 && all (tmp(:) >= 0) && all (tmp(:) <= 1)) y = NaN; color = tmp; else error ("bar3: inconsistent size in Y and Z input arguments."); endif varargin(1) = []; elseif (isnumeric (tmp) && isscalar (tmp)) if (tmp > 0 && tmp <= 1) width = tmp; depth = tmp; else error ("bar3: WIDTH must be a scalar in the range (0,1]."); endif varargin(1) = []; elseif (isnumeric (tmp)) if (size (tmp, 2) == 3 && all (tmp(:) >= 0) && all (tmp(:) <= 1)) color = tmp; else error (strcat ("bar3: numeric COLOR must be a 1x3 vector of an Nx3 matrix", " where each value is between 0 and 1 inclusive.")); endif color = tmp; varargin(1) = []; elseif (ischar (tmp)) if (any (strcmpi (tmp, {'detached', 'grouped', 'stacked'}))) style = tmp; varargin(1) = []; elseif (any (strcmpi (tmp, vc))) color = tmp; varargin(1) = []; elseif (strcmpi (tmp, 'width')) if (numel (varargin) < 2) error ("bar3: missing value for optional argument 'width'."); endif w = varargin{2}; if (isscalar (w) && isnumeric (w) && isfinite (w) && w > 0 && w <= 1) width = w; depth = w; elseif (numel (w) == 2 && isnumeric (w) && isfinite (w) && all (w > 0) && all (w <= 1)) width = w(1); depth = w(2); else error ("bar3: invalid value for optional argument 'width'."); endif varargin([1:2]) = []; elseif (strcmpi (tmp, 'color')) if (numel (varargin) < 2) error ("bar3: missing value for optional argument 'color'."); endif c = varargin{2}; if (iscellstr (c)) is_vc = all (cell2mat (cellfun (@(x) any (strcmpi (vc, x)), ... c, 'UniformOutput', false))); if (is_vc) color = c; else error ("bar3: invalid value for optional argument 'color'."); endif elseif (ischar (c) && isvector (c)) if (any (strcmpi (c, vc))) color = c; else error ("bar3: invalid value for optional argument 'color'."); endif elseif (isnumeric (c)) if (size (c, 2) == 3 && all (c(:) >= 0) && all (c(:) <= 1)) color = c; else error (strcat ("bar3: numeric COLOR must be a 1x3 vector of an Nx3", " matrix where each value is between 0 and 1 inclusive.")); endif else error ("bar3: invalid value for optional argument 'color'."); endif varargin([1:2]) = []; elseif (strcmpi (tmp, 'xlabel')) if (numel (varargin) < 2) error ("bar3: missing value for optional argument 'xlabel'."); endif xlabel = varargin{2}; if (! iscellstr (xlabel)) error ("bar3: invalid value for optional argument 'xlabel'."); endif varargin([1:2]) = []; elseif (strcmpi (tmp, 'ylabel')) if (numel (varargin) < 2) error ("bar3: missing value for optional argument 'ylabel'."); endif ylabel = varargin{2}; if (! iscellstr (ylabel)) error ("bar3: invalid value for optional argument 'ylabel'."); endif varargin([1:2]) = []; else error ("bar3: invalid optional argument."); endif elseif (iscellstr (tmp)) is_vc = all (cell2mat (cellfun (@(x) any (strcmpi (vc, x)), ... tmp, 'UniformOutput', false))); if (is_vc) color = tmp; else error ("bar3: invalid value for optional COLOR argument."); endif varargin(1) = []; else error ("bar3: invalid optional argument."); endif endwhile ## Get number of column bars from z input [ny, nx] = size (z); ## Check xlabel and ylabel if (! isempty (xlabel) && numel (xlabel) != nx) error ("bar3: the elements in 'xlabel' must equal the columns in Z."); endif if (! isempty (ylabel) && numel (ylabel) != ny) error ("bar3: the elements in 'ylabel' must equal the rows in Z."); endif ## Check COLOR for valid dimensions if (isempty (color)) if (nx == 1) cargs = {'FaceColor', 'b'}; elseif (nx == 0) defc = [0,0,1;1,0,1;0,1,0;1,1,0]; fvcd = kron (defc([1:nx],:), ones (6 * ny, 1)); cargs = {'FaceVertexCData', fvcd, 'FaceColor', 'flat'}; else fvcd = kron ((1:nx)', ones (6 * ny, 1)); cargs = {'FaceVertexCData', fvcd, 'FaceColor', 'flat', ... 'CDataMapping', 'scaled'}; endif elseif (isnumeric (color)) if (size (color, 1) == numel (z)) fvcd = kron (color, ones (6, 1)); cargs = {'FaceVertexCData', fvcd, 'FaceColor', 'flat'}; elseif (size (color, 1) == nx) fvcd = kron (color, ones (6 * ny, 1)); cargs = {'FaceVertexCData', fvcd, 'FaceColor', 'flat'}; elseif (size (color, 1) == ny) fvcd = repmat (kron (color, ones (6, 1)), nx, 1); cargs = {'FaceVertexCData', fvcd, 'FaceColor', 'flat'}; elseif (size (color, 1) == 1) cargs = {'FaceVertexCData', color, 'FaceColor', 'flat'}; endif elseif (ischar (color)) cargs = {'FaceColor', color}; elseif (iscellstr (color)) endif ## Construct a "template" column-bar (8 vertices and 6 faces) centered at ## origin, with height = 1, and width along x-axis and depth along y-axis hw = width / 2; hd = depth / 2; ## Scale the bar's base when grouping together if (strcmpi (style, 'grouped')) sc = nx + 1; hw = hw / sc; hd = hd / sc; endif [X, Y, Z] = ndgrid ([-hw, hw], [-hd, hd], [0, 1]); V = [X(:), Y(:), Z(:)]; F = [1, 2, 4, 3; 5, 6, 8, 7; 1, 2, 6, 5; 3, 4, 8, 7; 1, 5, 7, 3; 2, 6, 8, 4]; ## Replicate faces to the required number of bars increments = 0:8:8 * (nx * ny - 1); F = bsxfun (@plus, F, permute (increments, [1, 3, 2])); F = reshape (permute (F, [2, 1, 3]), 4, []).'; ## Replicate vertices to the required number of bars [offsetX, offsetY] = meshgrid (1:nx, 1:ny); offset = [offsetX(:), offsetY(:)]; offset(:,3) = 0; V = bsxfun (@plus, V, permute (offset, [3, 2, 1])); V = reshape (permute (V, [2, 1, 3]), 3, []).'; if (strcmpi (style, 'detached')) ## Adjust bar heights according to values in z input V(:,3) = V(:,3) .* kron (z(:), ones (8,1)); elseif (strcmpi (style, 'grouped')) ## Adjust bar heights according to values in z input V(:,3) = V(:,3) .* kron (z(:), ones (8,1)); ## Move groups along x axis V(:,1) = V(:,1) - kron (kron ([0:nx-1], ones (ny, 1))(:), ones (8,1)); ## Move groups along y axis offset = [-nx+1:2:nx-1] * (hd / width); V(:,2) = V(:,2) + kron (kron (ones (1, ny), offset)(:), ones (8,1)); nx = 1; elseif (strcmpi (style, 'stacked')) ## Move groups along x axis V(:,1) = V(:,1) - kron (kron ([0:nx-1], ones (ny, 1))(:), ones (8,1)); ## Adjust bar heights according to values in z input ZC = cumsum (z,2); Q1 = kron (ZC(:), ones (8,1)); ZC(:,end) = []; ZC = [zeros(ny,1), ZC]; Q2 = kron (ZC(:), ones (8,1)); V(:,3) = V(:,3) .* Q1; idx = V(:,3) == 0; V(idx,3) = Q2(idx); ## Collapse x axis nx = 1; endif ## Draw column bars as patches specified by faces/vertices if (new_axes) ax = gca (); endif p = patch ('Faces', F, 'Vertices', V, 'EdgeColor', 'k', 'Parent', ax, cargs{:}); ## Set view port and axes view (ax, 3); grid (ax, 'on'); axis tight; xlim ([0.5, nx+0.5]); ylim ([0.5, ny+0.5]); set (ax, 'XTick', 1:nx, 'YTick', 1:ny, 'Box', 'off', 'YDir', 'reverse'); ## Fix aspect ratio so that bars appear square when rotating the bar plot if (nx > ny) set (ax, 'PlotBoxAspectRatio', [1, ny/nx, (sqrt(5)-1)/2]); elseif (nx < ny) set (ax, 'PlotBoxAspectRatio', [nx/ny, 1, (sqrt(5)-1)/2]); else set (ax, 'PlotBoxAspectRatio', [1, 1, (sqrt(5)-1)/2]); endif ## Add tick labels in axes (if requested) if (! isempty (xlabel)) set (ax, 'XTickLabel', xlabel); endif if (! isempty (ylabel)) set (ax, 'YTickLabel', ylabel); elseif (! isempty (y) && ! isnan (y)) ylabel = arrayfun (@(x) sprintf ('%d', x), y, 'UniformOutput', false); set (ax, 'YTickLabel', ylabel); endif ## Return handle to patch object if requested if nargout > 0 varargout{1} = p; endif endfunction %!demo %! ## Plotting 5 bars in the same series. %! %! z = [50; 40; 30; 20; 10]; %! bar3 (z); %!demo %! ## Plotting 5 bars in different groups. %! %! z = [50, 40, 30, 20, 10]; %! bar3 (z); %!demo %! ## A 3D bar graph with each series corresponding to a column in z. %! %! z = [1, 4, 7; 2, 5, 8; 3, 6, 9; 4, 7, 10]; %! bar3 (z); %!demo %! ## Specify y-axis locations as tick names. y must be a column vector! %! %! y = [1950, 1960, 1970, 1980, 1990]'; %! z = [16, 8, 4, 2, 1]'; %! bar3 (y, z); %!demo %! ## Plot 3 series as a grouped plot without any space between the grouped bars %! %! z = [70 50 33 10; 75 55 35 15; 80 60 40 20]; %! bar3 (z, 1, 'grouped'); %!demo %! ## Plot a stacked style 3D bar graph %! %! z = [19, 30, 21, 30; 40, 16, 32, 12]; %! b = bar3 (z, 0.5, 'stacked'); ## Test input validation %!error bar3 ('A') %!error bar3 ({2,3,4,5}) %!error ... %! bar3 ([1,2,3]', ones (2)) %!error ... %! bar3 ([1:5], 1.2) %!error ... %! bar3 ([1:5]', ones (5), 1.2) %!error ... %! bar3 ([1:5]', ones (5), [0.8, 0.7]) %!error ... %! bar3 (ones (5), 'width') %!error ... %! bar3 (ones (5), 'width', 1.2) %!error ... %! bar3 (ones (5), 'width', [0.8, 0.8, 0.8]) %!error ... %! bar3 (ones (5), 'color') %!error ... %! bar3 (ones (5), 'color', [0.8, 0.8]) %!error ... %! bar3 (ones (5), 'color', 'brown') %!error ... %! bar3 (ones (5), 'color', {'r', 'k', 'c', 'm', 'brown'}) %!error ... %! bar3 (ones (5), 'xlabel') %!error ... %! bar3 (ones (5), 'xlabel', 4) %!error ... %! bar3 (ones (5), 'ylabel') %!error ... %! bar3 (ones (5), 'ylabel', 4) %!error bar3 (ones (5), 'this', 4) %!error ... %! bar3 (ones (5), 'xlabel', {'A', 'B', 'C'}) %!error ... %! bar3 (ones (5), 'ylabel', {'A', 'B', 'C'}) statistics-release-2.1.2/inst/Plotting/bar3h.m000066400000000000000000000434431527311104300212630ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} bar3h (@var{y}) ## @deftypefnx {statistics} {} bar3h (@var{z}, @var{y}) ## @deftypefnx {statistics} {} bar3h (@dots{}, @var{width}) ## @deftypefnx {statistics} {} bar3h (@dots{}, @var{style}) ## @deftypefnx {statistics} {} bar3h (@dots{}, @var{color}) ## @deftypefnx {statistics} {} bar3h (@dots{}, @var{name}, @var{value}) ## @deftypefnx {statistics} {} bar3h (@var{ax}, @dots{}) ## @deftypefnx {statistics} {@var{p} =} bar3h (@dots{}) ## ## Plot a horizontal 3D bar graph. ## ## @code{bar3h (@var{y})} plots 3D bar graph for the elements of @var{y}. Each ## bar corresponds to an element in @var{y}, which can be a scalar, vector, or ## 2D matrix. By default, each column in @var{y} is considered as a series and ## it is handled as a distinct series of bars. When @var{y} is a vector, unlike ## MATLAB, which plots it as a single series of bars, Octave distinguishes ## between a row and column vector of @var{y}. Hence, when @var{y} is column ## vector, it is plotted as a single series of bars (same color), whereas when ## @var{y} is row vector, each bar is plotted as a different group (different ## colors). For an @math{M*N} matrix, the function plots the bars corresponding ## to each row on the @qcode{z-axis} ranging from @math{1} to @math{M} and each ## column on the @qcode{x-axis} ranging from @math{1} to @math{N}. ## ## @code{bar3h (@var{z}, @var{y})} plots a 3D bar graph of the elements in ## @var{y} at the @qcode{z-values} specified in @var{z}. It should be noted ## that @var{z} only affects the tick names along the @qcode{z-axis} rather the ## actual values. If you want to specify non-numerical values for @var{z}, you ## can specify it with the paired @var{name}/@var{value} syntax shown below. ## ## @code{bar3h (@dots{}, @var{width})} sets the width of the bars along the ## @qcode{x-} and @qcode{z-axes} and controls the separation of bars among each ## other. @var{width} can take any value in the range @math{(0,1]}. By default, ## @var{width} is 0.8 and the bars have a small separation. If width is 1, the ## bars touch one another. Alternatively, you can define @var{width} as a two- ## element vector using the paired @var{name}/@var{value} syntax shown below, in ## which case you can control the bar separation along each axis independently. ## ## @code{bar3h (@dots{}, @var{style})} specifies the style of the bars, where ## @var{style} can be @qcode{'detached'}, @qcode{'grouped'}, or ## @qcode{'stacked'}. The default style is @qcode{'detached'}. ## ## @code{bar3h (@dots{}, @var{color})} displays all bars using the color ## specified by color. For example, use @qcode{'red'} or @qcode{'r'} to ## specify all red bars. When you want to specify colors for several groups, ## @var{color} can be a cellstr vector with each element specifying the color of ## each group. @var{color} can also be specified as a numerical @math{M*3} ## matrix, where each row corresponds to a RGB value with its elements in the ## range @math{[0,1]}. If only one color is specified, then it applies to all ## bars. If the number of colors equals the number of groups, then each color ## is applied to each group. If the number of colors equals the number of ## elements in @var{y}, then each individual bar is assigned the particular ## color. You can also define @var{color} using the paired ## @var{name}/@var{value} syntax shown below. ## ## @code{bar3h (@dots{}, @var{name}, @var{value})} specifies one or more of the ## following name/value pairs: ## ## @multitable @columnfractions 0.2 0.75 ## @headitem Name @tab Value ## @item @qcode{'width'} @tab A two-element vector specifying the width ## of the bars along the @qcode{x-} and @qcode{z-axes}, respectively. Each ## element must be in the range @math{(0,1]}. ## ## @item @qcode{'color'} @tab A character or a cellstr vector, or a ## numerical @math{M*3} matrix following the same conventions as the @var{color} ## input argument. ## ## @item @qcode{'xlabel'} @tab A cellstr vector specifying the group names ## along the @qcode{x-axis}. ## ## @item @qcode{'zlabel'} @tab A cellstr vector specifying the names of the ## bars in the same series along the @qcode{z-axis}. ## @end multitable ## ## @code{bar3h (@var{ax}, @dots{})} can also take an axes handle @var{ax} as a ## first argument in which case it plots into the axes specified by @var{ax} ## instead of into the current axes specified by @code{gca ()}. The optional ## argument @var{ax} can precede any of the input argument combinations in the ## previous syntaxes. ## ## @code{@var{p} = bar3h (@dots{})} returns a patch handle @var{p}, which can be ## used to set properties of the bars after displaying the 3D bar graph. ## ## @seealso{boxplot, hist3} ## @end deftypefn function [varargout] = bar3h (varargin) if (nargin < 1) print_usage (); endif ## Check if first input is an axes handle if (isaxes (varargin{1})) ax = varargin{1}; varargin(1) = []; new_axes = false; else new_axes = true; endif ## Parse input argument Z if (numel (varargin) < 1) print_usage (); endif y = varargin{1}; varargin(1) = []; if (! isnumeric (y)) error ("bar3h: Z must be numeric."); endif ## Add defaults z = []; width = 0.8; depth = 0.8; style = 'detached'; color = []; xlabel = []; zlabel = []; ## Valid Colors for input validation vc = {'red', 'r', 'green', 'g', 'blue', 'b', 'cyan', 'c', ... 'magenta', 'm', 'yellow', 'y', 'black', 'k', 'white', 'w'}; ## Parse extra input arguments while (numel (varargin) > 0) tmp = varargin{1}; if (isnumeric (tmp) && isempty (z)) if (isvector (tmp) && isvector (y) && numel (tmp) == numel (y)) z = y; y = tmp; elseif (all (size (tmp) > 1) && size (tmp, 1) == numel (y) && isvector (y)) z = y; y = tmp; elseif (isscalar (tmp)) z = NaN; if (tmp > 0 && tmp <= 1) width = tmp; depth = tmp; else error ("bar3h: WIDTH must be a scalar in the range (0,1]."); endif elseif (size (tmp, 2) == 3 && all (tmp(:) >= 0) && all (tmp(:) <= 1)) z = NaN; color = tmp; else error ("bar3h: inconsistent size in Y and Z input arguments."); endif varargin(1) = []; elseif (isnumeric (tmp) && isscalar (tmp)) if (tmp > 0 && tmp <= 1) width = tmp; depth = tmp; else error ("bar3h: WIDTH must be a scalar in the range (0,1]."); endif varargin(1) = []; elseif (isnumeric (tmp)) if (size (tmp, 2) == 3 && all (tmp(:) >= 0) && all (tmp(:) <= 1)) color = tmp; else error (strcat ("bar3h: numeric COLOR must be a 1x3 vector of an Nx3 matrix", " where each value is between 0 and 1 inclusive.")); endif color = tmp; varargin(1) = []; elseif (ischar (tmp)) if (any (strcmpi (tmp, {'detached', 'grouped', 'stacked'}))) style = tmp; varargin(1) = []; elseif (any (strcmpi (tmp, vc))) color = tmp; varargin(1) = []; elseif (strcmpi (tmp, 'width')) if (numel (varargin) < 2) error ("bar3h: missing value for optional argument 'width'."); endif w = varargin{2}; if (isscalar (w) && isnumeric (w) && isfinite (w) && w > 0 && w <= 1) width = w; depth = w; elseif (numel (w) == 2 && isnumeric (w) && isfinite (w) && all (w > 0) && all (w <= 1)) width = w(1); depth = w(2); else error ("bar3h: invalid value for optional argument 'width'."); endif varargin([1:2]) = []; elseif (strcmpi (tmp, 'color')) if (numel (varargin) < 2) error ("bar3h: missing value for optional argument 'color'."); endif c = varargin{2}; if (iscellstr (c)) is_vc = all (cell2mat (cellfun (@(x) any (strcmpi (vc, x)), ... c, 'UniformOutput', false))); if (is_vc) color = c; else error ("bar3h: invalid value for optional argument 'color'."); endif elseif (ischar (c) && isvector (c)) if (any (strcmpi (c, vc))) color = c; else error ("bar3h: invalid value for optional argument 'color'."); endif elseif (isnumeric (c)) if (size (c, 2) == 3 && all (c(:) >= 0) && all (c(:) <= 1)) color = c; else error (strcat ("bar3h: numeric COLOR must be a 1x3 vector of an Nx3", " matrix where each value is between 0 and 1 inclusive.")); endif else error ("bar3h: invalid value for optional argument 'color'."); endif varargin([1:2]) = []; elseif (strcmpi (tmp, 'xlabel')) if (numel (varargin) < 2) error ("bar3h: missing value for optional argument 'xlabel'."); endif xlabel = varargin{2}; if (! iscellstr (xlabel)) error ("bar3h: invalid value for optional argument 'xlabel'."); endif varargin([1:2]) = []; elseif (strcmpi (tmp, 'zlabel')) if (numel (varargin) < 2) error ("bar3h: missing value for optional argument 'zlabel'."); endif zlabel = varargin{2}; if (! iscellstr (zlabel)) error ("bar3h: invalid value for optional argument 'zlabel'."); endif varargin([1:2]) = []; else error ("bar3h: invalid optional argument."); endif elseif (iscellstr (tmp)) is_vc = all (cell2mat (cellfun (@(x) any (strcmpi (vc, x)), ... tmp, 'UniformOutput', false))); if (is_vc) color = tmp; else error ("bar3h: invalid value for optional COLOR argument."); endif varargin(1) = []; else error ("bar3h: invalid optional argument."); endif endwhile ## Get number of column bars from y input [nz, nx] = size (y); ## Check xlabel and zlabel if (! isempty (xlabel) && numel (xlabel) != nx) error ("bar3h: the elements in 'xlabel' must equal the columns in Z."); endif if (! isempty (zlabel) && numel (zlabel) != nz) error ("bar3h: the elements in 'zlabel' must equal the rows in Z."); endif ## Check COLOR for valid dimensions if (isempty (color)) if (nx == 1) cargs = {'FaceColor', 'b'}; elseif (nx == 0) defc = [0,0,1;1,0,1;0,1,0;1,1,0]; fvcd = kron (defc([1:nx],:), ones (6 * nz, 1)); cargs = {'FaceVertexCData', fvcd, 'FaceColor', 'flat'}; else fvcd = kron ((1:nx)', ones (6 * nz, 1)); cargs = {'FaceVertexCData', fvcd, 'FaceColor', 'flat', ... 'CDataMapping', 'scaled'}; endif elseif (isnumeric (color)) if (size (color, 1) == numel (y)) fvcd = kron (color, ones (6, 1)); cargs = {'FaceVertexCData', fvcd, 'FaceColor', 'flat'}; elseif (size (color, 1) == nx) fvcd = kron (color, ones (6 * nz, 1)); cargs = {'FaceVertexCData', fvcd, 'FaceColor', 'flat'}; elseif (size (color, 1) == nz) fvcd = repmat (kron (color, ones (6, 1)), nx, 1); cargs = {'FaceVertexCData', fvcd, 'FaceColor', 'flat'}; elseif (size (color, 1) == 1) cargs = {'FaceVertexCData', color, 'FaceColor', 'flat'}; endif elseif (ischar (color)) cargs = {'FaceColor', color}; elseif (iscellstr (color)) endif ## Construct a "template" column-bar (8 vertices and 6 faces) centered at ## origin, with height = 1, and width along x-axis and depth along z-axis hw = width / 2; hd = depth / 2; ## Scale the bar's base when grouping together if (strcmpi (style, 'grouped')) sc = nx + 1; hw = hw / sc; hd = hd / sc; endif [X, Y, Z] = ndgrid ([-hw, hw], [0, 1], [-hd, hd]); V = [X(:), Y(:), Z(:)]; F = [1, 2, 4, 3; 5, 6, 8, 7; 1, 2, 6, 5; 3, 4, 8, 7; 1, 5, 7, 3; 2, 6, 8, 4]; ## Replicate faces to the required number of bars increments = 0:8:8 * (nx * nz - 1); F = bsxfun (@plus, F, permute (increments, [1, 3, 2])); F = reshape (permute (F, [2, 1, 3]), 4, []).'; ## Replicate vertices to the required number of bars [offsetX, offsetZ] = meshgrid (1:nx, 1:nz); offset = [offsetX(:), zeros(numel (y), 1), offsetZ(:)]; #offset(:,3) = 0; V = bsxfun (@plus, V, permute (offset, [3, 2, 1])); V = reshape (permute (V, [2, 1, 3]), 3, []).'; A = NaN; if (strcmpi (style, 'detached')) ## Adjust bar heights according to values in y input V(:,2) = V(:,2) .* kron (y(:), ones (8,1)); elseif (strcmpi (style, 'grouped')) ## Adjust bar heights according to values in y input V(:,2) = V(:,2) .* kron (y(:), ones (8,1)); ## Move groups along x axis V(:,1) = V(:,1) - kron (kron ([0:nx-1], ones (nz, 1))(:), ones (8,1)); ## Move groups along z axis offset = [-nx+1:2:nx-1] * (hd / width); V(:,3) = V(:,3) + kron (kron (ones (1, nz), offset)(:), ones (8,1)); nx = 1; elseif (strcmpi (style, 'stacked')) ## Move groups along x axis V(:,1) = V(:,1) - kron (kron ([0:nx-1], ones (nz, 1))(:), ones (8,1)); ## Adjust bar heights according to values in y input ZC = cumsum (y,2); Q1 = kron (ZC(:), ones (8,1)); ZC(:,end) = []; ZC = [zeros(nz,1), ZC]; Q2 = kron (ZC(:), ones (8,1)); V(:,2) = V(:,2) .* Q1; idx = V(:,2) == 0; V(idx,2) = Q2(idx); ## Collapse x axis nx = 1; endif ## Draw column bars as patches specified by faces/vertices if (new_axes) ax = gca (); endif p = patch ('Faces', F, 'Vertices', V, 'EdgeColor', 'k', 'Parent', ax, cargs{:}); ## Set view port and axes view (ax, 3); grid (ax, 'on'); axis tight; xlim ([0.5, nx+0.5]); zlim ([0.5, nz+0.5]); set (ax, 'XTick', 1:nx, 'ZTick', 1:nz, 'Box', 'off', 'YDir', 'reverse'); ## Fix aspect ratio so that bars appear square when rotating the bar plot if (nx > nz) set (ax, 'PlotBoxAspectRatio', [1, (sqrt(5)-1)/2, nz/nx]); elseif (nx < nz) set (ax, 'PlotBoxAspectRatio', [nx/nz, (sqrt(5)-1)/2, 1]); else set (ax, 'PlotBoxAspectRatio', [1, (sqrt(5)-1)/2, 1]); endif ## Add tick labels in axes (if requested) if (! isempty (xlabel)) set (ax, 'XTickLabel', xlabel); endif if (! isempty (zlabel)) set (ax, 'ZTickLabel', zlabel); elseif (! isempty (z) && ! isnan (z)) zlabel = arrayfun (@(x) sprintf ("%d", x), z, 'UniformOutput', false); set (ax, 'ZTickLabel', zlabel); endif ## Return handle to patch object if requested if (nargout > 0) varargout{1} = p; endif endfunction %!demo %! ## Plotting 5 bars in the same series. %! %! y = [50; 40; 30; 20; 10]; %! bar3h (y); %!demo %! ## Plotting 5 bars in different groups. %! %! y = [50, 40, 30, 20, 10]; %! bar3h (y); %!demo %! ## A 3D bar graph with each series corresponding to a column in y. %! %! y = [1, 4, 7; 2, 5, 8; 3, 6, 9; 4, 7, 10]; %! bar3h (y); %!demo %! ## Specify z-axis locations as tick names. z must be a column vector! %! %! z = [1950, 1960, 1970, 1980, 1990]'; %! y = [16, 8, 4, 2, 1]'; %! bar3h (z, y); %!demo %! ## Plot 3 series as a grouped plot without any space between the grouped bars %! %! y = [70 50 33 10; 75 55 35 15; 80 60 40 20]; %! bar3h (y, 1, 'grouped'); %!demo %! ## Plot a stacked style 3D bar graph %! %! y = [19, 30, 21, 30; 40, 16, 32, 12]; %! b = bar3h (y, 0.5, 'stacked'); ## Test input validation %!error bar3h ('A') %!error bar3h ({2,3,4,5}) %!error ... %! bar3h ([1,2,3]', ones (2)) %!error ... %! bar3h ([1:5], 1.2) %!error ... %! bar3h ([1:5]', ones (5), 1.2) %!error ... %! bar3h ([1:5]', ones (5), [0.8, 0.7]) %!error ... %! bar3h (ones (5), 'width') %!error ... %! bar3h (ones (5), 'width', 1.2) %!error ... %! bar3h (ones (5), 'width', [0.8, 0.8, 0.8]) %!error ... %! bar3h (ones (5), 'color') %!error ... %! bar3h (ones (5), 'color', [0.8, 0.8]) %!error ... %! bar3h (ones (5), 'color', 'brown') %!error ... %! bar3h (ones (5), 'color', {'r', 'k', 'c', 'm', 'brown'}) %!error ... %! bar3h (ones (5), 'xlabel') %!error ... %! bar3h (ones (5), 'xlabel', 4) %!error ... %! bar3h (ones (5), 'zlabel') %!error ... %! bar3h (ones (5), 'zlabel', 4) %!error bar3h (ones (5), 'this', 4) %!error ... %! bar3h (ones (5), 'xlabel', {'A', 'B', 'C'}) %!error ... %! bar3h (ones (5), 'zlabel', {'A', 'B', 'C'}) statistics-release-2.1.2/inst/Plotting/biplot.m000066400000000000000000000304651527311104300215550ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} biplot (@var{coefs}) ## @deftypefnx {statistics} {} biplot (@var{coefs}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {statistics} {} biplot (@var{ax}, @dots{}) ## @deftypefnx {statistics} {@var{h} =} biplot (@dots{}) ## ## Create a biplot of the coefficients in @var{coefs}. ## ## @code{biplot (@var{coefs})} plots the rows of @var{coefs}, typically the ## principal component coefficients returned by @code{pca} or @code{pcacov} or ## the factor loadings returned by @code{factoran}, as vectors from the origin. ## @var{coefs} has one row per observed variable and either two columns (for a ## 2-D biplot) or three columns (for a 3-D biplot). ## ## The following name/value pairs are accepted: ## ## @table @asis ## @item @qcode{"Scores"} ## A matrix of scores with the same number of columns as @var{coefs} (one row ## per observation). The scores are plotted as points, scaled to fit within the ## unit circle relative to the maximum coefficient length: each score is divided ## by the maximum absolute score value and multiplied by the length of the ## longest coefficient vector. ## ## @item @qcode{"VarLabels"} ## A character array or cell array of strings labeling each variable vector. ## ## @item @qcode{"ObsLabels"} ## A character array or cell array of strings labeling each observation. ## ## @item @qcode{"Positive"} ## If @qcode{true}, the reference axes are drawn only over the positive quadrant ## (2-D) or octant (3-D). The default is @qcode{false}. ## @end table ## ## Any additional name/value pairs are treated as line properties and applied to ## the variable vectors. ## ## For readability the sign of each column of @var{coefs} is chosen so that its ## largest-magnitude element is positive; the same sign change is applied to the ## corresponding column of the scores. ## ## @code{biplot (@var{ax}, @dots{})} plots into the axes @var{ax}. ## ## The optional output @var{h} is a column vector of handles to the plotted ## graphics objects, ordered as the variable vector lines, the variable markers, ## the variable text labels (if any), the observation markers, the observation ## text labels (if any), and finally the reference axis lines. ## ## @seealso{pca, pcacov, factoran, rotatefactors} ## @end deftypefn function h = biplot (varargin) ## Optional leading axes handle hax = []; if (numel (varargin) > 0 && isaxes (varargin{1})) hax = varargin{1}; varargin(1) = []; endif if (numel (varargin) < 1) print_usage (); endif coefs = varargin{1}; varargin(1) = []; if (! isnumeric (coefs) || ! isreal (coefs) || ndims (coefs) > 2 ... || (columns (coefs) != 2 && columns (coefs) != 3)) error ("biplot: COEFS must be a real matrix with 2 or 3 columns."); endif p = rows (coefs); d = columns (coefs); ## Parse name/value options scores = []; varlabels = {}; obslabels = {}; positive = false; lineprops = {}; if (mod (numel (varargin), 2) != 0) error ("biplot: name/value arguments must come in pairs."); endif for i = 1:2:numel (varargin) name = varargin{i}; value = varargin{i+1}; if (! ischar (name)) error ("biplot: property names must be strings."); endif switch (lower (name)) case "scores" scores = value; case "varlabels" varlabels = value; case "obslabels" obslabels = value; case "positive" positive = value; otherwise lineprops(end+1:end+2) = {name, value}; endswitch endfor have_scores = ! isempty (scores); if (have_scores) if (! isnumeric (scores) || ! isreal (scores) || columns (scores) != d) error ("biplot: SCORES must be a real matrix with the same number of columns as COEFS."); endif endif if (ischar (varlabels)) varlabels = cellstr (varlabels); endif if (ischar (obslabels)) obslabels = cellstr (obslabels); endif ## Sign convention: force the largest-magnitude element of each column positive for j = 1:d [~, idx] = max (abs (coefs(:,j))); if (coefs(idx,j) < 0) coefs(:,j) = -coefs(:,j); if (have_scores) scores(:,j) = -scores(:,j); endif endif endfor ## Scale the scores to the coefficient space if (have_scores) maxlen = max (sqrt (sum (coefs .^ 2, 2))); maxscore = max (abs (scores(:))); if (maxscore > 0) scores = scores * (maxlen / maxscore); endif endif ## Reference axis extent axlim = 1.1 * max (abs (coefs(:))); if (axlim == 0 || isnan (axlim)) axlim = 1; endif if (isempty (hax)) hax = newplot (); else newplot (hax); endif old_hold = ishold (hax); hold (hax, "on"); vcol = [0 0 1]; # variable vectors in blue ocol = [1 0 0]; # observations in red acol = [0.5 0.5 0.5]; # reference axes in gray varlines = zeros (p, 1); varmarks = zeros (p, 1); vartext = zeros (numel (varlabels), 1); obsmarks = zeros (0, 1); obstext = zeros (numel (obslabels), 1); ## Variable vector lines (origin to each coefficient) for i = 1:p if (d == 2) varlines(i) = line (hax, [0, coefs(i,1)], [0, coefs(i,2)], ... "color", vcol, "marker", "none", lineprops{:}); else varlines(i) = line (hax, [0, coefs(i,1)], [0, coefs(i,2)], ... [0, coefs(i,3)], "color", vcol, "marker", "none", ... lineprops{:}); endif endfor ## Variable markers at the vector tips. Each marker's data carries the ## trailing NaN MATLAB puts there; it draws nothing, but code reading XData ## off the returned handle sees the same array in both. for i = 1:p if (d == 2) varmarks(i) = line (hax, [coefs(i,1), NaN], [coefs(i,2), NaN], ... "linestyle", "none", "marker", "o", "color", vcol); else varmarks(i) = line (hax, [coefs(i,1), NaN], [coefs(i,2), NaN], ... [coefs(i,3), NaN], "linestyle", "none", ... "marker", "o", "color", vcol); endif endfor ## Variable text labels for i = 1:numel (varlabels) if (d == 2) vartext(i) = text (hax, coefs(i,1), coefs(i,2), varlabels{i}, ... "color", vcol); else vartext(i) = text (hax, coefs(i,1), coefs(i,2), coefs(i,3), ... varlabels{i}, "color", vcol); endif endfor ## Observation markers if (have_scores) m = rows (scores); obsmarks = zeros (m, 1); for j = 1:m if (d == 2) obsmarks(j) = line (hax, [scores(j,1), NaN], [scores(j,2), NaN], ... "linestyle", "none", "marker", ".", "color", ocol); else obsmarks(j) = line (hax, [scores(j,1), NaN], [scores(j,2), NaN], ... [scores(j,3), NaN], "linestyle", "none", ... "marker", ".", "color", ocol); endif endfor ## Observation text labels for j = 1:numel (obslabels) if (d == 2) obstext(j) = text (hax, scores(j,1), scores(j,2), obslabels{j}, ... "color", ocol); else obstext(j) = text (hax, scores(j,1), scores(j,2), scores(j,3), ... obslabels{j}, "color", ocol); endif endfor endif ## Reference axis lines through the origin lo = ifelse (positive, 0, -axlim); if (d == 2) axline = line (hax, [lo, axlim, NaN, 0, 0], [0, 0, NaN, lo, axlim], ... "color", acol); else axline = line (hax, [lo, axlim, NaN, 0, 0, NaN, 0, 0], ... [0, 0, NaN, lo, axlim, NaN, 0, 0], ... [0, 0, NaN, 0, 0, NaN, lo, axlim], "color", acol); endif if (d == 3) view (hax, 3); zlabel (hax, "Component 3"); endif xlabel (hax, "Component 1"); ylabel (hax, "Component 2"); if (! old_hold) hold (hax, "off"); endif if (nargout > 0) h = [varlines; varmarks; vartext; obsmarks; obstext; axline]; endif endfunction ## Return a if cond is true, else b. function r = ifelse (cond, a, b) if (cond) r = a; else r = b; endif endfunction %!demo %! ## Biplot of the first two principal components of Fisher's iris data. %! %! load fisheriris; %! [coefs, score] = pca (zscore (meas)); %! biplot (coefs(:,1:2), "Scores", score(:,1:2), ... %! "VarLabels", {"SL", "SW", "PL", "PW"}); %!demo %! ## Three-component biplot of the same data. %! %! load fisheriris; %! coefs = pca (zscore (meas)); %! biplot (coefs(:,1:3), "VarLabels", {"SL", "SW", "PL", "PW"}); ## Test output %!test %! hf = figure ("visible", "off"); %! unwind_protect %! coefs = [0.6 -0.3; -0.2 0.7; 0.5 0.5]; %! score = [1 2; -3 1; 0.5 -2; 4 0]; %! h = biplot (coefs, "Scores", score); %! assert_equal (numel (h), 11); %! ## variable vector lines (origin to coefficient) %! assert_equal (get (h(1), "xdata"), [0 0.6], 1e-12); %! assert_equal (get (h(1), "ydata"), [0 -0.3], 1e-12); %! assert_equal (get (h(2), "xdata"), [0 -0.2], 1e-12); %! ## variable tip markers %! ## the trailing NaN is MATLAB's own, and draws nothing %! assert_equal (get (h(4), "xdata"), [0.6 NaN], 1e-12); %! assert_equal (get (h(4), "ydata"), [-0.3 NaN], 1e-12); %! ## observation markers (scaled scores) %! assert_equal (get (h(7), "xdata"), [0.182 NaN], 1e-4); %! assert_equal (get (h(7), "ydata"), [0.364 NaN], 1e-4); %! assert_equal (get (h(10), "xdata"), [0.728 NaN], 1e-4); %! ## reference axis extent %! assert_equal (get (h(11), "xdata"), [-0.77 0.77 NaN 0 0], 1e-12); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # 3-D biplot without scores %! hf = figure ("visible", "off"); %! unwind_protect %! coefs = [0.6 -0.3 0.2; -0.2 0.7 0.1; 0.5 0.5 -0.6]; %! h = biplot (coefs); %! assert_equal (numel (h), 7); %! ## column 3 has its largest-magnitude element (-0.6) forced positive, %! ## so the whole column is negated: [0.2 0.1 -0.6] -> [-0.2 -0.1 0.6] %! assert_equal (get (h(1), "zdata"), [0 -0.2], 1e-12); %! assert_equal (get (h(6), "zdata"), [0.6 NaN], 1e-12); %! assert_equal (get (h(7), "zdata"), [0 0 NaN 0 0 NaN -0.77 0.77], 1e-12); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # sign convention flips a column with a negative largest element %! hf = figure ("visible", "off"); %! unwind_protect %! coefs = [-0.8 0.1; 0.2 0.9; -0.5 0.5]; %! h = biplot (coefs); %! assert_equal (get (h(1), "xdata"), [0 0.8], 1e-12); %! assert_equal (get (h(2), "xdata"), [0 -0.2], 1e-12); %! assert_equal (get (h(3), "xdata"), [0 0.5], 1e-12); %! assert_equal (get (h(1), "ydata"), [0 0.1], 1e-12); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # labels add text handles in the documented order %! hf = figure ("visible", "off"); %! unwind_protect %! coefs = [0.6 -0.3; -0.2 0.7]; %! score = [1 2; -3 1]; %! h = biplot (coefs, "Scores", score, "VarLabels", {"a", "b"}, ... %! "ObsLabels", {"x", "y"}); %! ## 2 varlines + 2 varmarks + 2 vartext + 2 obsmarks + 2 obstext + 1 axis %! assert_equal (numel (h), 11); %! assert_equal (strcmp (get (h(5), "string"), "a"), true); %! assert_equal (strcmp (get (h(9), "string"), "x"), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error biplot () %!error ... %! biplot (ones (3, 4)) %!error biplot ({1}) %!error ... %! biplot ([0.6 0.3; 0.2 0.7], "Scores", [1 2 3]) %!error ... %! biplot ([0.6 0.3; 0.2 0.7], "Scores") statistics-release-2.1.2/inst/Plotting/blandAltmanPlot.m000066400000000000000000000153441527311104300233370ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} blandAltmanPlot (@var{X}, @var{Y}) ## @deftypefnx {statistics} {} blandAltmanPlot (@var{ax}, @var{X}, @var{Y}) ## @deftypefnx {statistics} {} blandAltmanPlot (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {@var{H} =} blandAltmanPlot (@dots{}) ## ## Bland-Altman plot of the agreement between two measurement methods. ## ## @code{blandAltmanPlot (@var{X}, @var{Y})} draws, for each subject measured ## by both methods, the difference @code{@var{X} - @var{Y}} against the mean ## of the two measurements, with a solid line at the bias, the mean ## difference, dashed lines at the limits of agreement, and dotted lines at ## the bounds of the confidence interval of each. @code{limitsOfAgreement} ## computes them, and the options it takes, @qcode{'Multiplier'} and ## @qcode{'Alpha'}, are passed on to it, as are @var{X} and @var{Y}, whose ## requirements it states. A pattern in the points, the differences growing ## with the mean for instance, is a sign that the limits, which assume one ## spread of differences throughout, do not describe the methods well. ## ## @code{blandAltmanPlot (@var{ax}, @dots{})} draws into the axes @var{ax} ## rather than the current axes. ## ## @code{@var{H} = blandAltmanPlot (@dots{})} returns the graphics objects ## drawn, in a row vector: the points, the lines at the bias, the lower limit ## and the upper limit, and the dotted pairs of lines of the intervals of the ## bias, the lower limit and the upper limit, each pair one line object. Each ## carries a @qcode{DisplayName}, so @code{legend} labels them. ## ## @seealso{limitsOfAgreement, gardnerAltmanPlot} ## @end deftypefn function varargout = blandAltmanPlot (varargin) ## Input validation ax = []; if (numel (varargin) > 0 && isscalar (varargin{1}) && ishghandle (varargin{1}) && strcmp (get (varargin{1}, 'type'), 'axes')) ax = varargin{1}; varargin(1) = []; endif if (numel (varargin) < 2 || ischar (varargin{2}) || isstring (varargin{2})) error ("blandAltmanPlot: two measurements X and Y are required."); endif ## The bias, the limits and their intervals, the data and options checked ## by limitsOfAgreement [LoA, stats] = limitsOfAgreement (varargin{:}); E = double (LoA.Estimate); CI = double (LoA.ConfidenceIntervals); m = double (stats.means); d = double (stats.differences); if (isempty (ax)) ax = gca (); endif ax = newplot (ax); held = ishold (ax); hold (ax, 'on'); co = get (ax, 'colororder'); xr = [min(m), max(m)]; if (xr(1) == xr(2)) xr += [-0.5, 0.5]; endif xs = [xr, NaN, xr]; names = {'Bias', 'Lower limit', 'Upper limit'}; h = line (ax, m, d, 'linestyle', 'none', 'marker', 'o', ... 'color', co(1,:), 'displayname', 'Subjects'); styles = {'-', '--', '--'}; for i = 1:3 h(end+1) = line (ax, xr, [E(i), E(i)], 'color', [0, 0, 0], ... 'linestyle', styles{i}, 'displayname', names{i}); endfor for i = 1:3 h(end+1) = line (ax, xs, [CI(i,[1, 1]), NaN, CI(i,[2, 2])], ... 'color', [0, 0, 0], 'linestyle', ':', ... 'displayname', [names{i}, ' interval']); endfor if (! held) hold (ax, 'off'); endif xlabel (ax, 'Mean of X and Y'); ylabel (ax, 'X - Y'); ## The handles are handed back only where they were asked for, so a call ## made for the drawing alone prints nothing if (nargout > 0) varargout{1} = h; endif endfunction %!demo %! ## Two meters measured the peak expiratory flow of 17 subjects %! x = [494, 395, 516, 434, 476, 557, 413, 442, 650, 433, 417, 656, 267, ... %! 478, 178, 423, 427]; %! y = [512, 430, 520, 428, 500, 600, 364, 380, 658, 445, 432, 626, 260, ... %! 477, 259, 350, 451]; %! H = blandAltmanPlot (x, y); %! legend (H(1:4), 'location', 'northeastoutside'); ## Test output %!shared x, y %! x = [494, 395, 516, 434, 476, 557, 413, 442, 650, 433, 417, 656, 267, ... %! 478, 178, 423, 427]; %! y = [512, 430, 520, 428, 500, 600, 364, 380, 658, 445, 432, 626, 260, ... %! 477, 259, 350, 451]; %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! H = blandAltmanPlot (x, y); %! assert_equal (size (H), [1, 7]); %! assert_equal (get (H(1), 'xdata'), (x + y) / 2); %! assert_equal (get (H(1), 'ydata'), x - y); %! L = limitsOfAgreement (x, y); %! assert_equal (get (H(2), 'ydata'), L.Estimate([1, 1])'); %! assert_equal (get (H(3), 'ydata'), L.Estimate([2, 2])'); %! assert_equal (get (H(4), 'ydata'), L.Estimate([3, 3])'); %! assert_equal (get (H(5), 'ydata'), [L.ConfidenceIntervals(1,[1, 1]), ... %! NaN, L.ConfidenceIntervals(1,[2, 2])]); %! assert_equal (get (H(2:4), 'linestyle'), {'-'; '--'; '--'}); %! assert_equal (get (H(1), 'displayname'), 'Subjects'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! ax = axes (); %! plot (ax, 1:3, 1:3); %! hold (ax, 'on'); %! H = blandAltmanPlot (ax, x, y, 'Multiplier', 2); %! assert_equal (ishold (ax), true); %! assert_equal (numel (get (ax, 'children')), 8); %! L = limitsOfAgreement (x, y, 'Multiplier', 2); %! assert_equal (get (H(3), 'ydata'), L.Estimate([2, 2])'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! blandAltmanPlot (x, y); %! assert_equal (ishold (gca ()), false); %! assert_equal (get (get (gca (), 'xlabel'), 'string'), 'Mean of X and Y'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error ... %! blandAltmanPlot ([1, 2, 3]) %!error ... %! blandAltmanPlot ([1, 2, 3], 'Alpha', 0.1) %!error ... %! hf = figure ('visible', 'off'); %! unwind_protect %! blandAltmanPlot ([1, 2, 3], [1, 2]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect statistics-release-2.1.2/inst/Plotting/boxchart.m000066400000000000000000000337041527311104300220750ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} boxchart (@var{ydata}) ## @deftypefnx {statistics} {} boxchart (@var{xgroupdata}, @var{ydata}) ## @deftypefnx {statistics} {} boxchart (@dots{}, @qcode{'GroupByColor'}, @var{cgroupdata}) ## @deftypefnx {statistics} {} boxchart (@var{tbl}, @var{yvar}) ## @deftypefnx {statistics} {} boxchart (@var{tbl}, @var{xvar}, @var{yvar}) ## @deftypefnx {statistics} {} boxchart (@dots{}, @var{name}, @var{value}) ## @deftypefnx {statistics} {} boxchart (@var{ax}, @dots{}) ## @deftypefnx {statistics} {@var{b} =} boxchart (@dots{}) ## ## Draw a box chart. ## ## @code{boxchart (@var{ydata})} draws one box over the observations in ## @var{ydata}. The box spans the lower and upper quartiles with the median ## across it, the whiskers reach the furthest observations within one and a ## half interquartile ranges of the box, and whatever lies beyond is drawn as ## a marker. ## ## @code{boxchart (@var{xgroupdata}, @var{ydata})} draws one box per distinct ## value of @var{xgroupdata}, which holds one value per observation. ## ## @code{boxchart (@var{tbl}, @var{yvar})} and ## @code{boxchart (@var{tbl}, @var{xvar}, @var{yvar})} take the observations, ## and where asked the grouping, from the named columns of a table. The ## chart keeps the table, so assigning @code{SourceTable} again re-reads it. ## ## @code{@var{b} = boxchart (@dots{})} returns the ## @code{stats.chart.BoxChart} object drawn, whose properties may be set ## afterwards to redraw it; see @code{stats.chart.BoxChart}. Where ## @qcode{'GroupByColor'} names a grouping, one object is returned per ## colour group, in the order of its distinct values. ## ## @qcode{'GroupByColor'} cannot be given with a table, which MATLAB R2026a ## refuses as well. ## ## Every other name-value pair sets the property of that name on the object; ## see @code{stats.chart.BoxChart} for what each takes. ## ## @seealso{stats.chart.BoxChart, boxplot, swarmchart} ## @end deftypefn function varargout = boxchart (varargin) if (nargin < 1) print_usage (); endif ## An axes may come first, as it may for every plotting function ax = []; if (isscalar (varargin{1}) && ishghandle (varargin{1}) && strcmp (get (varargin{1}, 'type'), 'axes')) ax = varargin{1}; varargin(1) = []; if (isempty (varargin)) print_usage (); endif endif [spec, cgroup, args] = bcParse (varargin); if (isempty (cgroup)) b = stats.chart.BoxChart (ax, spec, args); else ## One chart per colour group, over its own share of the observations [lev, ~, idx] = unique (cgroup(:)); n = numel (lev); co = bcColorOrder (ax); for k = 1:n take = (idx == k); sub = spec; sub.YData = spec.YData(take); if (! isempty (spec.XData)) sub.XData = spec.XData(take); endif col = co(mod (k - 1, rows (co)) + 1, :); kargs = [{'BoxFaceColor', col, 'BoxEdgeColor', col, ... 'BoxMedianLineColor', col, 'WhiskerLineColor', col, ... 'MarkerColor', col}, args]; ## Octave's classdef has no 'empty', so the array is grown by assigning ## its first element rather than preallocated if (k == 1) b = stats.chart.BoxChart (ax, sub, kargs); ## Every group draws into the axes the first of them settled on ax = b.Parent; else b(k) = stats.chart.BoxChart (ax, sub, kargs); endif endfor endif ## The chart is handed back only where it was asked for, so a call made ## for the drawing alone prints nothing if (nargout > 0) varargout{1} = b; endif endfunction ## Work out which syntax was used and turn it into what the chart takes. function [spec, cgroup, args] = bcParse (in) spec = struct ('XData', [], 'YData', [], 'SourceTable', [], ... 'XVariable', [], 'YVariable', [], ... 'XDataMode', 'auto', 'YDataMode', 'manual'); cgroup = []; if (istable (in{1})) [spec, args] = bcParseTable (in, spec); else [spec, args] = bcParseVectors (in, spec); endif ## 'GroupByColor' is a grouping rather than a property of the chart keep = true (1, numel (args)); for k = 1:2:numel (args) if (ischar (args{k}) && strcmpi (args{k}, 'GroupByColor')) if (! isempty (spec.SourceTable)) error (strcat ("boxchart: 'GroupByColor' is not supported with", ... " table input.")); endif cgroup = args{k+1}; keep(k:k+1) = false; endif endfor args = args(keep); if (! isempty (cgroup) && numel (cgroup) != numel (spec.YData)) error (strcat ("boxchart: 'GroupByColor' must hold one value per", ... " observation.")); endif endfunction ## boxchart (tbl, yvar) and boxchart (tbl, xvar, yvar). function [spec, args] = bcParseTable (in, spec) t = in{1}; rest = in(2:end); if (isempty (rest) || ! bcIsName (rest{1})) error ("boxchart: a table must be followed by the variable to draw."); endif if (numel (rest) > 1 && bcIsName (rest{2})) xvar = rest{1}; yvar = rest{2}; rest = rest(3:end); else xvar = []; yvar = rest{1}; rest = rest(2:end); endif spec.SourceTable = t; spec.XVariable = xvar; spec.YVariable = yvar; spec.YDataMode = 'auto'; spec.XDataMode = 'auto'; names = t.Properties.VariableNames; if (! any (strcmp (names, yvar))) error ("boxchart: the table holds no variable '%s'.", yvar); endif spec.YData = double (t.(yvar)(:)); if (! isempty (xvar)) if (! any (strcmp (names, xvar))) error ("boxchart: the table holds no variable '%s'.", xvar); endif spec.XData = t.(xvar); endif args = rest; endfunction ## boxchart (ydata) and boxchart (xgroupdata, ydata). function [spec, args] = bcParseVectors (in, spec) if (numel (in) > 1 && isnumeric (in{2}) && ! bcIsName (in{1})) spec.XData = in{1}; spec.YData = double (in{2}(:)); spec.XDataMode = 'manual'; args = in(3:end); else spec.YData = double (in{1}(:)); args = in(2:end); endif if (! (isnumeric (spec.YData) && isreal (spec.YData))) error ("boxchart: YDATA must be a real numeric vector."); endif if (! isempty (spec.XData) && numel (spec.XData) != numel (spec.YData)) error (strcat ("boxchart: XGROUPDATA must hold one value per", ... " observation.")); endif if (mod (numel (args), 2) != 0) error ("boxchart: name-value arguments must be in pairs."); endif endfunction ## The colour order the chart will draw with, without making an axes for it. function co = bcColorOrder (ax) if (isempty (ax)) hf = get (0, 'currentfigure'); if (! isempty (hf)) ax = get (hf, 'currentaxes'); endif endif if (isempty (ax)) co = get (0, 'defaultaxescolororder'); else co = get (ax, 'ColorOrder'); endif endfunction ## Whether an argument is the name of a table variable. function tf = bcIsName (x) tf = ((ischar (x) && isrow (x)) || (isa (x, 'string') && isscalar (x))); endfunction %!demo %! ## One box over a sample: the box spans the quartiles with the median %! ## across it, the whiskers reach the furthest values within one and a half %! ## interquartile ranges, and the rest are drawn as markers. %! load fisheriris %! boxchart (meas(:,1)); %! ylabel ('sepal length'); %!demo %! ## One box per group, and a notch about each median. Two notches that do %! ## not overlap mark medians that differ at roughly the five per cent level. %! load fisheriris %! g = grp2idx (species); %! boxchart (g, meas(:,1), 'Notch', 'on'); %! xlabel ('species'); %! ylabel ('sepal length'); %!demo %! ## The chart keeps what it was drawn from, so its properties may be set %! ## afterwards and it redraws itself. %! load fisheriris %! b = boxchart (meas(:,1)); %! b.Orientation = 'horizontal'; %! b.BoxFaceColor = [0.85, 0.325, 0.098]; %! b.JitterOutliers = 'on'; %!shared bcY, bcG, bcC, bcT %! bcY = [1; 2; 3; 4; 5; 6; 7; 8; 9; 100]; %! bcG = [1; 1; 1; 1; 1; 2; 2; 2; 2; 2]; %! bcC = {'a'; 'b'; 'a'; 'b'; 'a'; 'b'; 'a'; 'b'; 'a'; 'b'}; %! bcT = table (bcG, bcY, 'VariableNames', {'grp', 'val'}); ## MATLAB parity: one box is drawn from the quartiles and the median %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! b = boxchart (bcY); %! assert_equal (class (b), 'stats.chart.BoxChart'); %! assert_equal (quantile (bcY, 0.25), 3); %! assert_equal (quantile (bcY, 0.75), 8); %! assert_equal (median (bcY), 5.5); %! assert_equal (sum (isoutlier (bcY, 'quartiles')), 1); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # MATLAB parity: the defaults are the ones R2026a reports %! hf = figure ('visible', 'off'); %! unwind_protect %! b = boxchart (bcY); %! assert_equal (b.BoxWidth, 0.5); %! assert_equal (b.BoxFaceAlpha, 0.2); %! assert_equal (b.CapWidth, 0.25); %! assert_equal (b.LineWidth, 1); %! assert_equal (b.MarkerSize, 6); %! assert_equal (b.MarkerStyle, 'o'); %! assert_equal (b.Notch, 'off'); %! assert_equal (b.JitterOutliers, 'off'); %! assert_equal (b.Orientation, 'vertical'); %! assert_equal (b.WhiskerLineStyle, '-'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a group per distinct value, each with its own box %! hf = figure ('visible', 'off'); %! unwind_protect %! one = boxchart (bcY); %! n1 = numel (get (gca (), 'children')); %! clf (hf); %! two = boxchart (bcG, bcY); %! n2 = numel (get (gca (), 'children')); %! assert_equal (n2 > n1, true); %! assert_equal (two.XDataMode, 'manual'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## MATLAB parity: a table sets both modes to auto and records the columns %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! b = boxchart (bcT, 'grp', 'val'); %! assert_equal (b.YVariable, 'val'); %! assert_equal (b.XVariable, 'grp'); %! assert_equal (b.XDataMode, 'auto'); %! assert_equal (b.YDataMode, 'auto'); %! assert_equal (b.YData, bcY); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a table may name the observations alone %! hf = figure ('visible', 'off'); %! unwind_protect %! b = boxchart (bcT, 'val'); %! assert_equal (b.YVariable, 'val'); %! assert_equal (isempty (b.XVariable), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## MATLAB parity: assigning the table again re-reads it %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! b = boxchart (bcT, 'grp', 'val'); %! t = bcT; %! t.val(1) = 999; %! b.SourceTable = t; %! assert_equal (b.YData(1), 999); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## MATLAB parity: one chart per colour group %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! b = boxchart (bcG, bcY, 'GroupByColor', bcC); %! assert_equal (numel (b), 2); %! assert_equal (class (b), 'stats.chart.BoxChart'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # setting a property redraws rather than draws again over the old %! hf = figure ('visible', 'off'); %! unwind_protect %! b = boxchart (bcY); %! before = numel (get (gca (), 'children')); %! b.Notch = 'on'; %! assert_equal (numel (get (gca (), 'children')), before); %! b.BoxFaceColor = 'r'; %! assert_equal (numel (get (gca (), 'children')), before); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # setting a colour records that it was chosen %! hf = figure ('visible', 'off'); %! unwind_protect %! b = boxchart (bcY); %! assert_equal (b.BoxFaceColorMode, 'auto'); %! b.BoxFaceColor = [1, 0, 0]; %! assert_equal (b.BoxFaceColorMode, 'manual'); %! assert_equal (b.BoxFaceColor, [1, 0, 0]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a horizontal chart draws the same pieces the other way about %! hf = figure ('visible', 'off'); %! unwind_protect %! b = boxchart (bcY); %! before = numel (get (gca (), 'children')); %! b.Orientation = 'horizontal'; %! assert_equal (b.Orientation, 'horizontal'); %! assert_equal (numel (get (gca (), 'children')), before); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # the axes to draw into may be given first, as for any plot %! hf = figure ('visible', 'off'); %! unwind_protect %! a1 = subplot (1, 2, 1); %! a2 = subplot (1, 2, 2); %! b = boxchart (a2, bcY); %! assert_equal (b.Parent, a2); %! assert_equal (isempty (get (a1, 'children')), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Input validation %!error boxchart () %!error ... %! boxchart (bcT, 'grp', 'val', 'GroupByColor', bcC) %!error ... %! boxchart (bcT, 'nope') %!error ... %! boxchart ([1; 2], bcY) %!error ... %! boxchart (bcG, bcY, 'GroupByColor', {'a'; 'b'}) %!error ... %! boxchart (bcY, 'Notch') %!error ... %! boxchart (bcY, 'BoxWidth', -1) %!error ... %! boxchart (bcY, 'Notch', 'maybe') %!error ... %! boxchart (bcY, 'BoxFaceAlpha', 2) statistics-release-2.1.2/inst/Plotting/boxplot.m000066400000000000000000001344161527311104300217540ustar00rootroot00000000000000## Copyright (C) 2002 Alberto Terruzzi ## Copyright (C) 2006 Alberto Pose ## Copyright (C) 2011 Pascal Dupuis ## Copyright (C) 2012 Juan Pablo Carbajal ## Copyright (C) 2016 Pascal Dupuis ## Copyright (C) 2020 Andreas Bertsatos ## Copyright (C) 2020 Philip Nienhuis (prnienhuis@users.sf.net) ## Copyright (C) 2026 Avanish Salunke ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{s} =} boxplot (@var{data}) ## @deftypefnx {statistics} {@var{s} =} boxplot (@var{data}, @var{group}) ## @deftypefnx {statistics} {@var{s} =} boxplot (@var{data}, @var{notched}, @var{symbol}, @var{orientation}, @var{whisker}, @dots{}) ## @deftypefnx {statistics} {@var{s} =} boxplot (@var{data}, @var{group}, @var{notched}, @var{symbol}, @var{orientation}, @var{whisker}, @dots{}) ## @deftypefnx {statistics} {@var{s} =} boxplot (@var{data}, @var{options}) ## @deftypefnx {statistics} {@var{s} =} boxplot (@var{data}, @var{group}, @var{options}, @dots{}) ## @deftypefnx {statistics} {[@dots{}, @var{h}] =} boxplot (@var{data}, @dots{}) ## ## Produce a box plot. ## ## A box plot is a graphical display that simultaneously describes several ## important features of a data set, such as center, spread, departure from ## symmetry, and identification of observations that lie unusually far from ## the bulk of the data. ## ## Input arguments (case-insensitive) recognized by boxplot are: ## ## @itemize ## @item ## @var{data} is a matrix with one column for each data set, or a cell vector ## with one cell for each data set. Each cell must contain a numerical row or ## column vector (NaN and NA are ignored) and not a nested vector of cells. ## ## @item ## @var{notched} = 1 produces a notched-box plot. Notches represent a robust ## estimate of the uncertainty about the median. ## ## @var{notched} = 0 (default) produces a rectangular box plot. ## ## @var{notched} within the interval (0,1) produces a notch of the specified ## depth. Notched values outside (0,1) are amusing if not exactly impractical. ## ## @item ## @var{symbol} sets the symbol for the outlier values. The default symbol ## for points that lie outside 3 times the interquartile range is 'o'; ## the default symbol for points between 1.5 and 3 times the interquartile ## range is '+'. @* ## Alternative @var{symbol} settings: ## ## @var{symbol} = '.': points between 1.5 and 3 times the IQR are marked with ## '.' and points outside 3 times IQR with 'o'. ## ## @var{symbol} = ['x','*']: points between 1.5 and 3 times the IQR are marked ## with 'x' and points outside 3 times IQR with '*'. ## ## @item ## @var{orientation} = 0 makes the boxes horizontally. @* ## @var{orientation} = 1 plots the boxes vertically (default). Alternatively, ## orientation can be passed as a string, e.g., 'vertical' or 'horizontal'. ## ## @item ## @var{whisker} defines the length of the whiskers as a function of the IQR ## (default = 1.5). If @var{whisker} = 0 then @code{boxplot} displays all data ## values outside the box using the plotting symbol for points that lie ## outside 3 times the IQR. ## ## @item ## @var{group} may be passed as an optional argument only in the second ## position after @var{data}. @var{group} can be a numeric, character, ## string, or categorical vector defining separate categories. To group by ## multiple variables simultaneously, pass a cell array of grouping vectors ## (e.g., @code{@{group1, group2@}}). A separate box is plotted for each ## unique combination of group values. All grouping variables must have the ## same length as @var{data}. ## ## @item ## @var{options} are additional paired arguments passed with the formalism ## (Name, Value) that provide extra functionality as listed below. ## @var{options} can be passed at any order after the initial arguments and ## are case-insensitive. ## ## @multitable {Name} {Value} {description} @columnfractions .2 .2 .6 ## @item 'Notch' @tab 'on' @tab Notched by 0.25 of the boxes width. ## @item @tab 'off' @tab Produces a straight box. ## @item @tab scalar @tab Proportional width of the notch. ## ## @item 'Symbol' @tab '.' @tab Defines only outliers between 1.5 and 3 IQR. ## @item @tab ['x','*'] @tab 2nd character defines outliers > 3 IQR ## ## @item 'Orientation' @tab 'vertical' @tab Default value, can also be defined ## with numerical 1. ## @item @tab 'horizontal' @tab Can also be defined with numerical 0. ## ## @item 'Whisker' @tab scalar @tab Multiplier of IQR (default is 1.5). ## ## @item 'OutlierTags' @tab 'on' or 1 @tab Plot the vector index of the outlier ## value next to its point. ## @item @tab 'off' or 0 @tab No tags are plotted (default value). ## ## @item 'Sample_IDs' @tab 'cell' @tab A cell vector with one cell for each ## data set containing a nested cell vector with each sample's ID (should be ## a string). If this option is passed, then all outliers are tagged with ## their respective sample's ID string instead of their vector's index. ## ## @item 'BoxWidth' @tab 'proportional' @tab Create boxes with their width ## proportional to the number of samples in their respective dataset (default ## value). ## @item @tab 'fixed' @tab Make all boxes with equal width. ## ## @item 'Widths' @tab scalar @tab Scaling factor for box widths (default ## value is 0.4). ## ## @item 'CapWidths' @tab scalar @tab Scaling factor for whisker cap widths ## (default value is 1, which results to 'Widths'/8 halflength) ## ## @item 'BoxStyle' @tab 'outline' @tab Draw boxes as outlines (default value). ## @item @tab 'filled' @tab Fill boxes with a color (outlines are still ## plotted). ## ## @item 'Positions' @tab vector @tab Numerical vector that defines the ## position of each data set. It must have the same length as the number of ## groups in a desired manner. This vector merely defines the points along ## the group axis, which by default is [1:number of groups]. ## ## @item 'Labels' @tab cell @tab A cell vector of strings containing the names ## of each group. By default each group is labeled numerically. If multiple ## grouping variables are provided, default labels are automatically generated ## by joining the category names and stacked hierarchically. ## ## @item 'Colors' @tab character string or Nx3 numerical matrix @tab If just ## one character or 1x3 vector of RGB values, specify the fill color of all ## boxes when BoxStyle = 'filled'. If a character string or Nx3 matrix is ## entered, box #1's fill color corresponds to the first character or first ## matrix row, and the next boxes' fill colors corresponds to the next ## characters or rows. If the char string or Nx3 array is exhausted the color ## selection wraps around. ## @end multitable ## @end itemize ## ## Supplemental arguments not described above (@dots{}) are concatenated and ## passed to the plot() function. ## ## The returned matrix @var{s} has one column for each data set as follows: ## ## @multitable @columnfractions .1 .8 ## @item 1 @tab Minimum ## @item 2 @tab 1st quartile ## @item 3 @tab 2nd quartile (median) ## @item 4 @tab 3rd quartile ## @item 5 @tab Maximum ## @item 6 @tab Lower confidence limit for median ## @item 7 @tab Upper confidence limit for median ## @end multitable ## ## The quartiles are those of @code{quantile} at its default method, which is ## also what @code{prctile} returns, so the box edges of a data set always ## agree with @code{prctile (@var{data}, [25, 75])}. They set the ## inter-quartile range, and so the whisker fences and which observations are ## reported as outliers. ## ## The returned structure @var{h} contains handles to the plot elements, ## allowing customization of the visualization using set/get functions. ## ## Example ## ## @example ## title ("Grade 3 heights"); ## axis ([0,3]); ## set(gca (), "xtick", [1 2], "xticklabel", @{"girls", "boys"@}); ## boxplot (@{randn(10,1)*5+140, randn(13,1)*8+135@}); ## @end example ## ## @end deftypefn function [s_o, hs_o] = boxplot (data, varargin) ## Assign parameter defaults if (nargin < 1) print_usage; endif ## Check data if (! (isnumeric (data) || iscell (data))) error ("boxplot: numerical array or cell array containing data expected."); elseif (iscell (data)) ## Check if cell contain numerical data if (! all (cellfun ('isnumeric', data))) error ("boxplot: data cells must contain numerical data."); endif endif ## Integer observations are perfectly good data, and MATLAB accepts them, but ## the quartiles are taken through statistics, whose call to var insists on ## floating point. Convert rather than refuse. Unlike MATLAB the quartiles ## are then computed in double, so they are not rounded back to the integer ## grid: the quartiles of int32 (1:7) are 2.25 and 5.75, not 3 and 6. if (isinteger (data)) data = double (data); elseif (iscell (data) && any (cellfun (@isinteger, data))) data = cellfun (@double, data, 'UniformOutput', false); endif ## Default values maxwhisker = 1.5; orientation = 1; symbol = ['+', 'o']; notched = 0; plot_opts = {}; groups = []; sample_IDs = {}; outlier_tags = 0; box_width = 'proportional'; widths = 0.4; capwid = 1; box_style = 0; positions = []; labels = {}; nug = 0; bcolor = 'y'; ## Optional arguments analysis numarg = nargin - 1; indopt = 1; group_exists = 0; while (numarg) dummy = varargin{indopt++}; if ((! ischar (dummy) || iscellstr (dummy)) && indopt < 6) ## MATLAB allows passing the second argument as a grouping vector if (length (dummy) > 1) if (2 != indopt) error ("boxplot: grouping vector may only be passed as second arg."); endif if (isnumeric (dummy) || ischar (dummy) || iscell (dummy) || ... iscategorical (dummy) || isa (dummy, 'string')) groups = dummy; group_exists = 1; else error ("boxplot: grouping variable must be numeric, character, string, categorical, or cell array."); endif elseif (length (dummy) == 1) ## Old way: positional argument switch indopt - group_exists case 2 notched = dummy; case 4 orientation = dummy; case 5 maxwhisker = dummy; otherwise error ("boxplot: no positional argument allowed at position %d", ... --indopt); endswitch endif numarg--; continue; else if (3 == (indopt - group_exists) && length (dummy) <= 2) symbol = dummy; numarg--; continue; else ## Check for additional paired arguments switch lower (dummy) case 'notch' notched = varargin{indopt}; ## Check for string input: "on" or "off" if (ischar (notched)) if (strcmpi (notched, 'on')) notched = 1; elseif (strcmpi (notched, 'off')) notched = 0; else error (strcat ("boxplot: 'Notch' input argument accepts", ... " only 'on', 'off' or a numeric scalar value.")); endif elseif (! (isnumeric (notched) && isreal (notched))) error ("boxplot: invalid 'Notch' value."); endif case 'symbol' symbol = varargin{indopt}; if (! ischar (symbol)) error ("boxplot; Symbol(s) must be character(s)"); endif case 'orientation' orientation = varargin{indopt}; if (ischar (orientation)) ## Check for string input: "vertical" or "horizontal" if (strcmpi (orientation, 'vertical')) orientation = 1; elseif (strcmpi (orientation, 'horizontal')) orientation = 0; else error (strcat ("boxplot: 'Orientation' input argument", ... " accepts only 'vertical' (or 1) or", ... " 'horizontal' (or 0) as value.")); endif elseif (! (isnumeric (orientation) && isreal (orientation))) error ("boxplot: invalid 'Orientation' value."); endif case 'whisker' maxwhisker = varargin{indopt}; if (! isscalar (maxwhisker) || ! (isnumeric (maxwhisker) && isreal (maxwhisker))) error (strcat ("boxplot: 'Whisker' input argument accepts", ... " only a real scalar value as input parameter.")); endif case 'outliertags' outlier_tags = varargin{indopt}; ## Check for string input: "on" or "off" if (ischar (outlier_tags)) if (strcmpi (outlier_tags, 'on')) outlier_tags = 1; elseif (strcmpi (outlier_tags, 'off')) outlier_tags = 0; else error (strcat ("boxplot: 'OutlierTags' input argument accepts", ... " only 'on' (or 1) or 'off' (or 0) as value.")); endif elseif (! (isnumeric (outlier_tags) && isreal (outlier_tags))) error ("boxplot: invalid 'OutlierTags' value."); endif case 'sample_ids' sample_IDs = varargin{indopt}; if (! iscell (sample_IDs)) error (strcat ("boxplot: 'Sample_IDs' input argument", ... " accepts only a cell array as value.")); endif outlier_tags = 1; case 'boxwidth' box_width = varargin{indopt}; ## Check for string input: "fixed" or "proportional" if (! ischar (box_width) || ! ismember (lower (box_width), {'fixed', 'proportional'})) error (strcat ("boxplot: 'BoxWidth' input argument accepts", ... " only 'fixed' or 'proportional' as value.")); endif box_width = lower (box_width); case 'widths' widths = varargin{indopt}; if (! isscalar (widths) || ! (isnumeric (widths) && isreal (widths))) error (strcat ("boxplot: 'Widths' input argument accepts", ... " only a real scalar value as value.")); endif case 'capwidths' capwid = varargin{indopt}; if (! isscalar (capwid) || ! (isnumeric (capwid) && isreal (capwid))) error (strcat ("boxplot: 'CapWidths' input argument accepts", ... " only a real scalar value as value.")); endif case 'boxstyle' box_style = varargin{indopt}; ## Check for string input: "outline" or "filled" if (! ischar (box_style) || ! ismember (lower (box_style), {'outline', 'filled'})) error (strcat ("boxplot: 'BoxStyle' input argument accepts", ... " only 'outline' or 'filled' as value.")); endif box_style = lower (box_style); case 'positions' positions = varargin{indopt}; if (! isvector (positions) || ! isnumeric (positions)) error (strcat ("boxplot: 'Positions' input argument accepts", ... " only a numeric vector as value.")); endif case 'labels' labels = varargin{indopt}; if (! iscellstr (labels)) error (strcat ("boxplot: 'Labels' input argument accepts", ... " only a cellstr array as value.")); endif case 'colors' bcolor = varargin{indopt}; if (! (ischar (bcolor) || (isnumeric (bcolor) && size (bcolor, 2) == 3))) error (strcat ("boxplot: 'Colors' input argument accepts", ... " only a character vector or Nx3 numeric", ... " array as value.")); endif otherwise ## Take two args and append them to plot_opts plot_opts(1, end+1:end+2) = {dummy, varargin{indopt}}; endswitch endif numarg -= 2; indopt++; endif endwhile if (1 == length (symbol)) symbol(2) = symbol(1); endif if (1 == notched) notched = 0.25; endif a = 1-notched; ## Figure out how many data sets we have if (isempty (groups)) if (iscell (data)) nc = nug = length (data); for ind_c = (1:nc) lc(ind_c) = length (data{ind_c}); endfor else if (isvector (data)) data = data(:); endif nc = nug = columns (data); lc = ones (1, nc) * rows (data); endif groups = (1:nc); ## In case sample_IDs exists. check that it has same size as data if (! isempty (sample_IDs) && length (sample_IDs) == 1) for ind_c = (1:nc) if (lc(ind_c) != length (sample_IDs)) error ("boxplot: Sample_IDs must match the data."); endif endfor elseif (! isempty (sample_IDs) && length (sample_IDs) == nc) for ind_c = (1:nc) if (lc(ind_c) != length (sample_IDs{ind_c})) error ("boxplot: Sample_IDs must match the data."); endif endfor elseif (! isempty (sample_IDs) && length (sample_IDs) != nc) error ("boxplot: Sample_IDs must match the data."); endif ## Create labels according to number of datasets as ordered in data ## in case they are not provided by the user as optional argument if (isempty (labels)) for i = 1:nc column_label = num2str (groups(i)); labels(i) = {column_label}; endfor endif else if (! isvector (data)) error ("boxplot: with the formalism (data, group), both must be vectors."); endif ## Normalize groups into a cell array of grouping variables if (! iscell (groups) || iscellstr (groups)) groups_cell = {groups}; else groups_cell = groups; endif ## Validate dimensions against data n_obs = numel (data); for i = 1:numel (groups_cell) curr_group = groups_cell{i}; if (size (curr_group, 1) != n_obs && size (curr_group, 2) != n_obs) error ("boxplot: all grouping variables must have the same length as the data."); endif ## Ensure column vector format for consistent processing later if (isrow (curr_group) && ! ischar (curr_group)) groups_cell{i} = curr_group(:); endif endfor ## If sample IDs given, check that their size matches the data if (! isempty (sample_IDs)) if (length (sample_IDs) != 1 || length (sample_IDs{1}) != length (data)) error ("boxplot: Sample_IDs must match the data"); endif endif ## map native groups to integers n_groups = numel (groups_cell); idx_matrix = zeros (n_obs, n_groups); raw_uniques = cell (1, n_groups); for i = 1:n_groups [unq_vals, ~, col_idx] = unique (groups_cell{i}); idx_matrix(:, i) = col_idx; raw_uniques{i} = unq_vals; endfor ## find valid group combinations. [valid_combinations, ~, final_numeric_groups] = unique (idx_matrix, 'rows'); nc = size (valid_combinations, 1); nug = 1:nc; dummy_data = cell (1, nc); dummy_sIDs = cell (1, nc); ## apply mask and populate dummy arrays for i = 1:nc mask = (final_numeric_groups == i); dummy_data{i} = data(mask); if (! isempty (sample_IDs)) dummy_sIDs{i} = sample_IDs{1}(mask); endif endfor ## generate labels based on native datatypes if (isempty (labels)) labels = cell (1, nc); for i = 1:nc comb = valid_combinations(i, :); lbl_parts = cell (1, n_groups); for j = 1:n_groups if (iscell (raw_uniques{j})) val = raw_uniques{j}{comb(j)}; else val = raw_uniques{j}(comb(j)); endif if (isnumeric (val) || islogical (val)) lbl_parts{j} = num2str (val); elseif (ischar (val)) lbl_parts{j} = val; elseif (isa (val, 'string') || iscategorical (val)) lbl_parts{j} = char (val); else lbl_parts{j} = ''; endif endfor ## Join multiple group labels with a newline for hierarchical stacking labels{i} = strjoin (lbl_parts, char (10)); endfor endif data = dummy_data; groups = nug(:).'; if (! isempty (sample_IDs)) sample_IDs = dummy_sIDs; endif endif ## Compute statistics. ## s will contain ## 1,5 min and max ## 2,3,4 1st, 2nd and 3rd quartile ## 6,7 lower and upper confidence intervals for median s = zeros (7, nc); box = zeros (1, nc); ## Arrange the boxes into desired positions (if requested, otherwise leave ## default 1:nc) if (! isempty (positions)) groups = positions; endif ## Initialize whisker matrices to correct size and all necessary outlier ## variables whisker_x = ones (2, 1) * [groups, groups]; whisker_y = zeros (2, 2 * nc); outliers_x = []; outliers_y = []; outliers_idx = []; outliers_IDs = {}; outliers2_x = []; outliers2_y = []; outliers2_idx = []; outliers2_IDs = {}; for indi = (1:nc) ## Get the next data set from the array or cell array if (iscell (data)) col = data{indi}(:); if (! isempty (sample_IDs)) sIDs = sample_IDs{indi}; else sIDs = num2cell ([1:length(col)]); endif else col = data(:, indi); sIDs = num2cell ([1:length(col)]); endif ## Skip missing data (NaN, NA) and remove respective sample IDs. ## Do this only on nonempty data if (length (col) > 0) remove_samples = find (isnan (col) | isna (col)); if (length (remove_samples) > 0) col(remove_samples) = []; sIDs(remove_samples) = []; endif endif ## Remember data length nd = length (col); box(indi) = nd; if (nd > 1) ## Min, max and quartiles. These come from quantile at its own default ## method, which is the one prctile and MATLAB both use. Taking them ## from the core statistics function instead put the quartiles on a ## different definition (it asks quantile for method 7), so a box drawn ## here disagreed with prctile called on the same data, and the shifted ## inter-quartile range moved the whisker fences with it. s(1:5, indi) = quantile (col, [0, 0.25, 0.5, 0.75, 1])(:); ## Confidence interval for the median est = 1.57 * (s(4, indi) - s(2, indi)) / sqrt (nd); s(6, indi) = max ([s(3, indi) - est, s(2, indi)]); s(7, indi) = min ([s(3, indi) + est, s(4, indi)]); ## Whiskers out to the last point within the desired inter-quartile range IQR = maxwhisker * (s(4, indi) - s(2, indi)); lo_adj = min (col(col >= s(2, indi) - IQR)); hi_adj = max (col(col <= s(4, indi) + IQR)); ## A whisker never reaches back across its own quartile. When no ## observation lies between a quartile and its fence, the whisker ## collapses onto the quartile rather than being drawn into the box. lo_adj = min (lo_adj, s(2, indi)); hi_adj = max (hi_adj, s(4, indi)); whisker_y(:, indi) = [lo_adj; s(2, indi)]; whisker_y(:, nc+indi) = [hi_adj; s(4, indi)]; ## Outliers beyond 1 and 2 inter-quartile ranges outliers = col((col < s(2, indi) - IQR & col >= s(2, indi) - 2 * IQR) | ... (col > s(4, indi) + IQR & col <= s(4, indi) + 2 * IQR)); outliers2 = col(col < s(2, indi) - 2 * IQR | col > s(4, indi) + 2 * IQR); ## Get outliers indices from this dataset if (length (outliers) > 0) for out_i = 1:length (outliers) outliers_idx = [outliers_idx; (find (col == outliers(out_i)))]; outliers_IDs = {outliers_IDs{:}, sIDs{(find (col == outliers(out_i)))}}; endfor endif if (length (outliers2) > 0) for out_i = 1:length (outliers2) outliers2_idx = [outliers2_idx; find(col == outliers2(out_i))]; outliers2_IDs = {outliers2_IDs{:}, sIDs{find(col == outliers2(out_i))}}; endfor endif outliers_x = [outliers_x; (groups(indi) * ones (size (outliers)))]; outliers_y = [outliers_y; outliers]; outliers2_x = [outliers2_x; (groups(indi) * ones (size (outliers2)))]; outliers2_y = [outliers2_y; outliers2]; elseif (1 == nd) ## All statistics collapse to the value of the point s(:, indi) = col; ## Single point data sets are plotted as outliers. outliers_x = [outliers_x; groups(indi)]; outliers_y = [outliers_y; col]; ## Append the single point's index to keep the outliers' vector aligned outliers_idx = [outliers_idx; 1]; outliers_IDs = {outliers_IDs{:}, sIDs{:}}; else ## No statistics if no points s(:, indi) = NaN; endif endfor ## Note which boxes don't have enough stats chop = find (box <= 1); ## Replicate widths (if scalar or shorter vector) to match the number of boxes widths = widths(repmat (1:length (widths), 1, nc)); ## Truncate just in case :) widths([nc+1:end]) = []; ## Draw a box around the quartiles, with box width being fixed or proportional ## to the number of items in the box. if (strcmpi (box_width, 'proportional')) box = box .* (widths ./ max (box)); else box = box .* (widths ./ box); endif ## Draw notches if desired. quartile_x = ones (11, 1) * groups + ... [-a; -1; -1; 1 ; 1; a; 1; 1; -1; -1; -a] * box; quartile_y = s([3, 7, 4, 4, 7, 3, 6, 2, 2, 6, 3], :); ## Draw a line through the median median_x = ones (2, 1) * groups + [-a; +a] * box; median_y = s([3, 3], :); ## Chop all boxes which don't have enough stats quartile_x(:, chop) = []; quartile_y(:, chop) = []; whisker_x(:, [chop, chop + nc]) = []; whisker_y(:, [chop, chop + nc]) = []; median_x(:, chop) = []; median_y(:, chop) = []; box(chop) = []; ## The cap widths below are built from BOX and WIDTHS together, so WIDTHS has ## to lose the same entries. Left at its full length it made every grouped ## plot with a one-point group fail on a nonconformant subtraction. widths(chop) = []; ## Add caps to the remaining whiskers cap_x = whisker_x; if (strcmpi (box_width, 'proportional')) cap_x(1, :) -= repmat (((capwid * box .* (widths ./ max (box))) / 8), 1, 2); cap_x(2, :) += repmat (((capwid * box .* (widths ./ max (box))) / 8), 1, 2); else cap_x(1, :) -= repmat ((capwid * widths / 8), 1, 2); cap_x(2, :) += repmat ((capwid * widths / 8), 1, 2); endif cap_y = whisker_y([1, 1], :); ## Calculate coordinates for outlier tags outliers_tags_x = outliers_x + 0.08; outliers_tags_y = outliers_y; outliers2_tags_x = outliers2_x + 0.08; outliers2_tags_y = outliers2_y; ## Do the plot hold_status = ishold (); if (orientation) ## Define outlier_tags' vertical alignment outlier_tags_alignment = {'horizontalalignment', 'left'}; if (box_style) f = fillbox (quartile_x, quartile_y, bcolor); endif h = plot (quartile_x, quartile_y, 'b;;', whisker_x, whisker_y, 'b;;', cap_x, cap_y, 'b;;', median_x, median_y, 'r;;', outliers_x, outliers_y, [symbol(1), 'r;;'], outliers2_x, outliers2_y, [symbol(2), 'r;;'], plot_opts{:}); ## Print outlier tags if (outlier_tags == 1 && outliers_x > 0) t1 = plot_tags (outliers_tags_x, outliers_tags_y, outliers_idx, outliers_IDs, sample_IDs, outlier_tags_alignment); endif if (outlier_tags == 1 && outliers2_x > 0) t2 = plot_tags (outliers2_tags_x, outliers2_tags_y, outliers2_idx, outliers2_IDs, sample_IDs, outlier_tags_alignment); endif else ## Define outlier_tags' horizontal alignment outlier_tags_alignment = {'horizontalalignment', 'left', 'rotation', 90}; if (box_style) f = fillbox (quartile_y, quartile_x, bcolor); endif h = plot (quartile_y, quartile_x, 'b;;', whisker_y, whisker_x, 'b;;', cap_y, cap_x, 'b;;', median_y, median_x, 'r;;', outliers_y, outliers_x, [symbol(1), 'r;;'], outliers2_y, outliers2_x, [symbol(2), 'r;;'], plot_opts{:}); ## Print outlier tags if (outlier_tags == 1 && outliers_x > 0) t1 = plot_tags (outliers_tags_y, outliers_tags_x, outliers_idx, outliers_IDs, sample_IDs, outlier_tags_alignment); endif if (outlier_tags == 1 && outliers2_x > 0) t2 = plot_tags (outliers2_tags_y, outliers2_tags_x, outliers2_idx, outliers2_IDs, sample_IDs, outlier_tags_alignment); endif endif ## Distribute the handles that plot returned. They come back in the order ## the segments were passed and a segment with no columns contributes none, ## so walk them with a running offset. Chaining each block off the last index ## of the one before it broke as soon as a block was empty: a single-point, ## an all-NaN, or an empty data set leaves no box at all, and the chain then ## indexed the previous block at zero. n_box = columns (quartile_x); n_whis = 2 * columns (whisker_x); # the caps repeat the whisker columns n_med = columns (median_x); ## The outliers of every group are plotted as one series, so they account for ## a single handle when there are any. Counting their columns would say one ## for the 0x1 empty they collapse to when there are none. n_out = double (! isempty (outliers_y)); n_out2 = double (! isempty (outliers2_y)); used = 0; ## Box outlines and box fill (if any). The fill follows the boxes that were ## actually drawn, which is not the number of groups once any were chopped. hs.box = h(used + [1 : n_box]); used += n_box; if (box_style) hs.box_fill = f(1 : n_box); else hs.box_fill = []; endif ## Whiskers (including caps) and median lines hs.whisker = h(used + [1 : n_whis]); used += n_whis; hs.median = h(used + [1 : n_med]); used += n_med; ## Outliers (if any) and their respective tags (if applicable) if (n_out > 0) hs.outliers = h(used + [1 : n_out]); used += n_out; if (outlier_tags == 1) hs.out_tags = t1(1 : length (outliers_tags_y)); else hs.out_tags = []; endif else hs.outliers = []; hs.out_tags = []; endif ## Extreme outliers (if any) and their respective tags (if applicable) if (n_out2 > 0) hs.outliers2 = h(used + [1 : n_out2]); used += n_out2; if (outlier_tags == 1) hs.out_tags2 = t2(1 : length (outliers2_tags_y)); else hs.out_tags2 = []; endif else hs.outliers2 = []; hs.out_tags2 = []; endif ## Redraw the median lines to avoid colour overlapping in case of 'filled' ## BoxStyle if (box_style) set (hs.median, 'color', 'r'); endif ## Print labels according to orientation and return handle if (orientation) set (gca (), 'xtick', groups, 'xticklabel', labels); hs.labels = get (gcf, 'currentaxes'); else set (gca (), 'ytick', groups, 'yticklabel', labels); hs.labels = get (gcf, 'currentaxes'); endif ## retain original ishold status. if (! hold_status) hold off; endif ## return output arguments if desired. if (nargout >= 1) s_o = s; endif if (nargout == 2) hs_o = hs; endif endfunction function htags = plot_tags (out_tags_x, out_tags_y, out_idx, out_IDs, ... sample_IDs, opt) for i=1 : length (out_tags_x) if (! isempty (sample_IDs)) htags(i) = text (out_tags_x(i), out_tags_y(i), out_IDs{i}, opt{:}); else htags(i) = text (out_tags_x(i), out_tags_y(i), num2str (out_idx(i)), ... opt{:}); endif endfor endfunction function f = fillbox (quartile_y, quartile_x, bcolor) f = []; for icol = 1 : columns (quartile_x) if (ischar (bcolor)) f = [ f; fill(quartile_y(:, icol), quartile_x(:, icol), ... bcolor(mod (icol - 1, numel (bcolor)) + 1)) ]; else f = [ f; fill(quartile_y(:, icol), quartile_x(:, icol), ... bcolor(mod (icol - 1, size (bcolor, 1)) + 1, :)) ]; endif hold on; endfor endfunction %!demo %! rng (42); %! axis ([0, 3]); %! girls = randn (10, 1) * 5 + 140; %! boys = randn (13, 1) * 8 + 135; %! boxplot ({girls, boys}); %! set (gca (), 'xtick', [1 2], 'xticklabel', {'girls', 'boys'}) %! title ('Grade 3 heights'); %!demo %! rng (42); %! A = randn (10, 1) * 5 + 140; %! B = randn (25, 1) * 8 + 135; %! C = randn (20, 1) * 6 + 165; %! data = [A; B; C]; %! groups = [(ones (10, 1)); (ones (25, 1) * 2); (ones (20, 1) * 3)]; %! labels = {'Team A', 'Team B', 'Team C'}; %! pos = [2, 1, 3]; %! boxplot (data, groups, 'Notch', 'on', 'Labels', labels, 'Positions', pos, ... %! 'OutlierTags', 'on', 'BoxStyle', 'filled'); %! title ('Example of Group splitting with paired vectors'); %!demo %! rng (42); %! data = randn (100, 9); %! boxplot (data, 'notch', 'on', 'boxstyle', 'filled', ... %! 'colors', 'ygcwkmb', 'whisker', 1.2); %! title ('Example of different colors specified with characters'); %!demo %! rng (42); %! data = randn (100, 13); %! colors = [0.7 0.7 0.7; ... %! 0.0 0.4 0.9; ... %! 0.7 0.4 0.3; ... %! 0.7 0.1 0.7; ... %! 0.8 0.7 0.4; ... %! 0.1 0.8 0.5; ... %! 0.9 0.9 0.2]; %! boxplot (data, 'notch', 'on', 'boxstyle', 'filled', ... %! 'colors', colors, 'whisker', 1.3, 'boxwidth', 'proportional'); %! title ('Example of different colors specified as RGB values'); %!demo %! rng (42); %! data = randn (30, 1); %! ## Using modern string arrays %! str_groups = string (repmat (['Control'; 'TreatmentA'; 'TreatmentB'], 10, 1)); %! boxplot (data, str_groups, 'colors', 'rgb'); %! title ('Example using modern string arrays for grouping'); %!demo %! rng (42); %! data = randn (40, 1) * 5 + 50; %! ## Create two different grouping variables %! group1 = repmat ({'Alpha'; 'Beta'}, 20, 1); %! group2 = repmat ([2022; 2022; 2023; 2023], 10, 1); %! ## Pass them together as a cell array %! boxplot (data, {group1, group2}); %! title ('Example of Multiple Grouping Variables (Model & Year)'); ## Input data validation %!error boxplot ('a') %!error boxplot ({[1 2 3], 'a'}) %!error boxplot ([1 2 3], 1, {2, 3}) %!error boxplot ([1 2 3], {'a', 'b'}) %!error <'Notch' input argument accepts> boxplot ([1:10], 'notch', 'any') %!error boxplot ([1:10], 'notch', i) %!error boxplot ([1:10], 'notch', {}) %!error boxplot (1, 'symbol', 1) %!error <'Orientation' input argument accepts only> boxplot (1, 'orientation', 'diagonal') %!error boxplot (1, 'orientation', {}) %!error <'Whisker' input argument accepts only> boxplot (1, 'whisker', 'a') %!error <'Whisker' input argument accepts only> boxplot (1, 'whisker', [1 3]) %!error <'OutlierTags' input argument accepts only> boxplot (3, 'OutlierTags', 'maybe') %!error boxplot (3, 'OutlierTags', {}) %!error <'Sample_IDs' input argument accepts only> boxplot (1, 'sample_IDs', 1) %!error <'BoxWidth' input argument accepts only> boxplot (1, 'boxwidth', 2) %!error <'BoxWidth' input argument accepts only> boxplot (1, 'boxwidth', 'anything') %!error <'Widths' input argument accepts only> boxplot (5, 'widths', 'a') %!error <'Widths' input argument accepts only> boxplot (5, 'widths', [1:4]) %!error <'Widths' input argument accepts only> boxplot (5, 'widths', []) %!error <'CapWidths' input argument accepts only> boxplot (5, 'capwidths', 'a') %!error <'CapWidths' input argument accepts only> boxplot (5, 'capwidths', [1:4]) %!error <'CapWidths' input argument accepts only> boxplot (5, 'capwidths', []) %!error <'BoxStyle' input argument accepts only> boxplot (1, 'Boxstyle', 1) %!error <'BoxStyle' input argument accepts only> boxplot (1, 'Boxstyle', 'garbage') %!error <'Positions' input argument accepts only> boxplot (1, 'positions', 'aa') %!error <'Labels' input argument accepts only> boxplot (3, 'labels', [1 5]) %!error <'Colors' input argument accepts only> boxplot (1, 'colors', {}) %!error <'Colors' input argument accepts only> boxplot (2, 'colors', [1 2 3 4]) %!error boxplot (randn (10, 3), 'Sample_IDs', {'a', 'b'}) %!error boxplot (rand (3, 3), [1 2]) ## Test plotting %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! [a, b] = boxplot (rand (10, 3)); %! assert_equal (size (a), [7, 3]); %! assert_equal (numel (b.box), 3); %! assert_equal (numel (b.whisker), 12); %! assert_equal (numel (b.median), 3); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! [~, b] = boxplot (rand (10, 3), 'BoxStyle', 'filled', 'colors', 'ybc'); %! assert_equal (numel (b.box_fill), 3); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! hold on %! [a, b] = boxplot (rand (10, 3)); %! assert_equal (ishold, true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## Test multi-variable grouping. %! hf = figure ('visible', 'off'); %! unwind_protect %! data = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11; 12]; %! g1 = [1; 1; 2; 2; 3; 3; 1; 1; 2; 2; 3; 3]; %! g2 = string ({'A'; 'B'; 'A'; 'B'; 'A'; 'B'; 'A'; 'B'; 'A'; 'B'; 'A'; 'B'}); %! g3 = categorical ({'X'; 'X'; 'Y'; 'Y'; 'Z'; 'Z'; 'X'; 'X'; 'Y'; 'Y'; 'Z'; 'Z'}); %! [a, b] = boxplot (data, {g1, g2, g3}); %! assert_equal (size (a, 2), 6); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## Test multi-variable grouping with empty intersections dropping correctly. %! hf = figure ('visible', 'off'); %! unwind_protect %! data = [1; 2; 3; 4]; %! g1 = [1; 1; 2; 2]; %! g2 = string ({'A'; 'A'; 'B'; 'B'}); %! [a, b] = boxplot (data, {g1, g2}); %! assert_equal (size (a, 2), 2); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Inputs that used to die inside boxplot rather than be plotted or refused. %!test %! ## A single observation is plotted, not an internal subscript error. Every %! ## statistic collapses to the value, as it does in MATLAB. %! hf = figure ('visible', 'off'); %! unwind_protect %! [s, hs] = boxplot (5); %! assert_equal (s, 5 * ones (7, 1)); %! assert_equal (isempty (hs.box), true); %! assert_equal (numel (hs.outliers), 1); %! assert_equal (get (hs.outliers, 'YData'), 5); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## An empty input draws nothing and returns no statistics, as in MATLAB, %! ## rather than indexing an empty handle list at zero. %! hf = figure ('visible', 'off'); %! unwind_protect %! [s, hs] = boxplot ([]); %! assert_equal (size (s, 2), 0); %! assert_equal (isempty (hs.box), true); %! assert_equal (isempty (hs.whisker), true); %! assert_equal (isempty (hs.median), true); %! assert_equal (isempty (hs.outliers), true); %! clf; %! [s, hs] = boxplot ({}); %! assert_equal (size (s, 2), 0); %! assert_equal (isempty (hs.box), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## A variable that is entirely missing leaves no box, which used to take the %! ## handle bookkeeping with it. %! hf = figure ('visible', 'off'); %! unwind_protect %! [s, hs] = boxplot ([NaN; NaN; NaN]); %! assert_equal (all (isnan (s)), true); %! assert_equal (isempty (hs.box), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## Integer observations are accepted and agree with the same numbers in %! ## double; the internal call to var used to refuse them outright. %! hf = figure ('visible', 'off'); %! unwind_protect %! sd = boxplot ([1; 2; 3; 4; 5; 6; 7]); %! clf; %! si = boxplot (int32 ([1; 2; 3; 4; 5; 6; 7])); %! clf; %! su = boxplot (uint8 ([1; 2; 3; 4; 5; 6; 7])); %! assert_equal (si, sd); %! assert_equal (su, sd); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## The same holds for an integer variable inside a cell. %! hf = figure ('visible', 'off'); %! unwind_protect %! sd = boxplot ({[1; 2; 3; 4; 5], [2; 3; 4; 5; 6]}); %! clf; %! si = boxplot ({int32([1; 2; 3; 4; 5]), int32([2; 3; 4; 5; 6])}); %! assert_equal (si, sd); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## A group holding a single observation leaves fewer boxes than groups. The %! ## cap widths were built from the full-length widths vector against the %! ## chopped box vector, so the subtraction did not conform. %! hf = figure ('visible', 'off'); %! unwind_protect %! data = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 42]; %! grp = [1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 2]; %! [s, hs] = boxplot (data, grp); %! assert_equal (size (s, 2), 2); %! assert_equal (s(:, 2), 42 * ones (7, 1)); %! assert_equal (numel (hs.box), 1); %! assert_equal (numel (hs.median), 1); %! assert_equal (numel (hs.whisker), 4); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## The box fill follows the boxes actually drawn, not the number of groups. %! hf = figure ('visible', 'off'); %! unwind_protect %! data = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 42]; %! grp = [1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 2]; %! [s, hs] = boxplot (data, grp, 'BoxStyle', 'filled'); %! assert_equal (numel (hs.box_fill), numel (hs.box)); %! assert_equal (get (hs.box_fill(1), 'Type'), 'patch'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## Both kinds of outlier keep their own handles, and the ones after them %! ## stay in step. %! hf = figure ('visible', 'off'); %! unwind_protect %! data = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 42]; %! grp = [1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 2]; %! [s, hs] = boxplot ([data; 60], [grp; 1]); %! assert_equal (numel (hs.outliers), 1); %! assert_equal (numel (hs.outliers2), 1); %! assert_equal (get (hs.median, 'Type'), 'line'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Logical and character data stay refused, as they are in MATLAB. %!error ... %! boxplot ([true; false; true; true]) %!error ... %! boxplot ('abcde') ## Quartiles follow quantile's own default method, which is prctile's and ## MATLAB's. Values below are MATLAB R2024a's, read off the box, whisker and ## outlier objects it draws. %!test %! ## The box edges are the 25th and 75th percentiles, for odd and even counts %! ## alike, and agree with prctile called on the same data. %! hf = figure ('visible', 'off'); %! unwind_protect %! s = boxplot ((1:7)'); %! assert_equal (s(1:5)', [1, 2.25, 4, 5.75, 7]); %! clf; %! s = boxplot ((1:8)'); %! assert_equal (s(1:5)', [1, 2.5, 4.5, 6.5, 8]); %! clf; %! x = [2; 4; 4; 4; 5; 5; 7; 9]; %! s = boxplot (x); %! assert_equal (s(1:5)', [2, 4, 4.5, 6, 9]); %! assert_equal (s(2:4)', [prctile(x, 25), median(x), prctile(x, 75)]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## Small samples, where the two quantile definitions differ most. %! hf = figure ('visible', 'off'); %! unwind_protect %! s = boxplot ([1; 2]); %! assert_equal (s(1:5)', [1, 1, 1.5, 2, 2]); %! clf; %! s = boxplot ([1; 2; 3]); %! assert_equal (s(1:5)', [1, 1.25, 2, 2.75, 3]); %! clf; %! s = boxplot ([1; 2; 3; 4]); %! assert_equal (s(1:5)', [1, 1.5, 2.5, 3.5, 4]); %! clf; %! s = boxplot ([1; 2; 3; 4; 5]); %! assert_equal (s(1:5)', [1, 1.75, 3, 4.25, 5]); %! clf; %! s = boxplot ([1; 2; 3; 4; 5; 6]); %! assert_equal (s(1:5)', [1, 2, 3.5, 5, 6]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## Missing values are dropped before the quartiles are taken. %! hf = figure ('visible', 'off'); %! unwind_protect %! s = boxplot ([1; 2; NaN; 4; 5; 6; 7]); %! assert_equal (s(1:5)', [1, 2, 4.5, 6, 7]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## The inter-quartile range sets the fences, so the quartile definition %! ## decides which points are outliers. 33 lies beyond the fence and 31.5 %! ## does not; the old quartiles put the fence between them and called both %! ## outliers. %! hf = figure ('visible', 'off'); %! unwind_protect %! [s, hs] = boxplot ([(1:20)'; 31.5]); %! assert_equal (s(1:5)', [1, 5.75, 11, 16.25, 31.5]); %! assert_equal (isempty (hs.outliers), true); %! assert_equal (isempty (hs.outliers2), true); %! clf; %! [s, hs] = boxplot ([(1:20)'; 33]); %! assert_equal (s(1:5)', [1, 5.75, 11, 16.25, 33]); %! assert_equal (isempty (hs.outliers) && isempty (hs.outliers2), false); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## When no observation lies between a quartile and its fence the whisker %! ## collapses onto the quartile instead of being drawn back into the box. %! hf = figure ('visible', 'off'); %! unwind_protect %! [s, hs] = boxplot ([1; 1; 2; 2; 3; 3; 4; 40; 50]); %! assert_equal (s(1:5)', [1, 1.75, 3, 13, 50]); %! yd = get (hs.whisker, 'YData'); %! assert_equal (max ([yd{3}, yd{4}]), 13); %! clf; %! [s, hs] = boxplot ([-50; -40; -4; -3; -3; -2; -2; -1; -1]); %! assert_equal (s(1:5)', [-50, -13, -3, -1.75, -1]); %! yd = get (hs.whisker, 'YData'); %! assert_equal (min ([yd{1}, yd{2}]), -13); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## A whisker that does reach real data is not clamped. %! hf = figure ('visible', 'off'); %! unwind_protect %! [s, hs] = boxplot ([-50; -40; -4; -3; -3; -2; -2; 40; 50]); %! assert_equal (s(1:5)', [-50, -13, -3, 8.5, 50]); %! yd = get (hs.whisker, 'YData'); %! assert_equal (min ([yd{1}, yd{2}]), -40); %! assert_equal (max ([yd{3}, yd{4}]), 40); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect statistics-release-2.1.2/inst/Plotting/cdfplot.m000066400000000000000000000074221527311104300217140ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{hCDF} =} cdfplot (@var{x}) ## @deftypefnx {statistics} {[@var{hCDF}, @var{stats}] =} cdfplot (@var{x}) ## ## Display an empirical cumulative distribution function. ## ## @code{@var{hCDF} = cdfplot (@var{x})} plots an empirical cumulative ## distribution function (CDF) of the observations in the data sample vector ## @var{x}. @var{x} may be a row or column vector, and represents a random ## sample of observations from some underlying distribution. ## ## @code{cdfplot} plots F(x), the empirical (or sample) CDF versus the ## observations in @var{x}. The empirical CDF, F(x), is defined as follows: ## ## F(x) = (Number of observations <= x) / (Total number of observations) ## ## for all values in the sample vector @var{x}. NaNs are ignored. @var{hCDF} ## is the handle of the empirical CDF curve (a handle graphics 'line' object). ## ## @code{[@var{hCDF}, @var{stats}] = cdfplot (@var{x})} also returns a structure ## with the following fields as a statistical summary. ## ## @multitable @columnfractions 0.3 0.65 ## @item STATS.min @tab minimum value of @var{x} ## @item STATS.max @tab maximum value of @var{x} ## @item STATS.mean @tab sample mean of @var{x} ## @item STATS.median @tab sample median (50th percentile) of @var{x} ## @item STATS.std @tab sample standard deviation of @var{x} ## @end multitable ## ## @seealso{qqplot, cdfcalc} ## @end deftypefn function [hCDF, stats] = cdfplot (x) ## Check number of input arguments narginchk (1,1); ## Calculate sample cdf [yy, xx, ~, ~, eid] = cdfcalc (x); ## Check for errors returned from cdfcalc if (strcmpi (eid, 'VectorRequired')) error ("cdfplot: vector required as input."); elseif (strcmpi (eid, 'NotEnoughData')) error ("cdfplot: not enough data."); endif ## Create vectors for plotting k = length (xx); n = reshape (repmat (1:k, 2, 1), 2*k, 1); xCDF = [-Inf; xx(n); Inf]; yCDF = [0; 0; yy(1+n)]; ## Plot cdf h = plot (xCDF, yCDF); grid ('on') xlabel ('x') ylabel ('F(x)') title ('CDF plot of x'); ## Return requested output arguments if (nargout > 0) hCDF = h; endif if (nargout > 1) stats.min = nanmin (x); stats.max = nanmax (x); stats.mean = mean (x, 'omitnan'); stats.median = median (x, 'omitnan'); stats.std = std (x, 'omitnan'); endif endfunction %!demo %! rng (42); %! x = randn (100,1); %! cdfplot (x); ## Test results %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! x = [2, 4, 3, 2, 4, 3, 2, 5, 6, 4]; %! [hCDF, stats] = cdfplot (x); %! assert_equal (stats.min, 2); %! assert_equal (stats.max, 6); %! assert_equal (stats.median, 3.5); %! assert_equal (stats.std, 1.35400640077266, 1e-14); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! x = randn (100,1); %! cdfplot (x); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error cdfplot (); %!error cdfplot ([x',x']); %!error cdfplot ([NaN, NaN, NaN, NaN]); statistics-release-2.1.2/inst/Plotting/dendrogram.m000066400000000000000000000337761527311104300224160ustar00rootroot00000000000000## Copyright (c) 2012 Juan Pablo Carbajal ## Copyright (C) 2021 Stefano Guidoni ## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} dendrogram (@var{tree}) ## @deftypefnx {statistics} {} dendrogram (@var{tree}, @var{p}) ## @deftypefnx {statistics} {} dendrogram (@var{tree}, @var{prop}, @var{val}) ## @deftypefnx {statistics} {} dendrogram (@var{tree}, @var{p}, @var{prop}, @var{val} ) ## @deftypefnx {statistics} {@var{h} =} dendrogram (@dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{t}, @var{perm}] =} dendrogram (@dots{}) ## ## Plot a dendrogram of a hierarchical binary cluster tree. ## ## Given @var{tree}, a hierarchical binary cluster tree as the output of ## @code{linkage}, plot a dendrogram of the tree. The number of leaves shown by ## the dendrogram plot is limited to @var{p}. The default value for @var{p} is ## 30. Set @var{p} to 0 to plot all leaves. ## ## The optional outputs are @var{h}, @var{t} and @var{perm}: ## @itemize @bullet ## @item @var{h} is a handle to the lines of the plot. ## ## @item @var{t} is the vector with the numbers assigned to each leaf. ## Each element of @var{t} is a leaf of @var{tree} and its value is the number ## shown in the plot. ## When the dendrogram plot is collapsed, that is when the number of shown ## leaves @var{p} is inferior to the total number of leaves, a single leaf of ## the plot can represent more than one leaf of @var{tree}: in that case ## multiple elements of @var{t} share the same value, that is the same leaf of ## the plot. ## When the dendrogram plot is not collapsed, each leaf of the plot is the leaf ## of @var{tree} with the same number. ## ## @item @var{perm} is the vector list of the leaves as ordered as in the plot. ## @end itemize ## ## Additional input properties can be specified by pairs of properties and ## values. Known properties are: ## @itemize @bullet ## @item @qcode{'Reorder'} ## Reorder the leaves of the dendrogram plot using a numerical vector of size n, ## the number of leaves. When @var{p} is smaller than @var{n}, the reordering ## cannot break the @var{p} groups of leaves. ## ## @item @qcode{'Orientation'} ## Change the orientation of the plot. Available values: @qcode{top} (default), ## @qcode{bottom}, @qcode{left}, @qcode{right}. ## ## @item @qcode{'CheckCrossing'} ## Check if the lines of a reordered dendrogram cross each other. Available ## values: @qcode{true} (default), @qcode{false}. ## ## @item @qcode{'ColorThreshold'} ## Not implemented. ## ## @item @qcode{'Labels'} ## Use a char, string or cellstr array of size @var{n} to set the label for each ## leaf; the label is displayed only for nodes with just one leaf. ## @end itemize ## ## @seealso{cluster, clusterdata, cophenet, inconsistent, linkage, pdist} ## @end deftypefn function [H, T, perm] = dendrogram (tree, varargin) [m, d] = size (tree); if ((d != 3) || (! isnumeric (tree)) || (! (max (tree(end, 1:2)) == m * 2))) error (strcat ("dendrogram: tree must be a matrix as generated", ... " by the linkage function.")); endif pair_index = 1; ## Node count n = m + 1; ## Add default values P = 30; vReorder = []; csLabels = {}; checkCrossing = 1; orientation = 'top'; if (nargin > 1) if (isnumeric (varargin{1}) && isscalar (varargin{1})) ## dendrogram (tree, P) P = varargin{1}; pair_index++; endif ## dendrogram (..., Name, Value) while (pair_index < (nargin - 1)) switch (lower (varargin{pair_index})) case 'reorder' if (isvector (varargin{pair_index + 1}) && isnumeric (varargin{pair_index + 1}) && length (varargin{pair_index + 1}) == n ) vReorder = varargin{pair_index + 1}; else error (strcat ("dendrogram: 'reorder' must be a numeric", ... " vector of size n, the number of leaves.")); endif case 'checkcrossing' if (ischar (varargin{pair_index + 1})) switch (lower (varargin{pair_index + 1})) case 'true' checkCrossing = 1; case 'false' checkCrossing = 0; otherwise error ("dendrogram: unknown value '%s' for CheckCrossing.", ... varargin{pair_index + 1}); endswitch else error (strcat ("dendrogram: 'CheckCrossing' must be", ... " either 'true' or 'false'.")); endif case 'colorthreshold' warning ("dendrogram: property '%s' not implemented.",... varargin{pair_index}); case 'orientation' orientation = varargin{pair_index + 1}; # validity check below case 'labels' if (ischar (varargin{pair_index + 1}) && (isvector (varargin{pair_index + 1}) && length (varargin{pair_index + 1}) == n) || (ismatrix (varargin{pair_index + 1}) && rows (varargin{pair_index + 1}) == n)) csLabels = cellstr (varargin{pair_index + 1}); elseif (iscellstr (varargin{pair_index + 1}) && length (varargin{pair_index + 1}) == n) csLabels = varargin{pair_index + 1}; else error (strcat ("dendrogram: labels must be a char or", ... " string or cellstr array of size n.")); endif otherwise error ("dendrogram: unknown property '%s'.", varargin{pair_index}); endswitch pair_index += 2; endwhile endif ## MATLAB compatibility: ## P <= 0 to plot all leaves if (P < 1) P = n; endif if (n > P) level_0 = tree((n - P), 3); else P = n; level_0 = 0; endif vLeafPosition = zeros ((n + m), 1); T = (1:n)'; nodecnt = 1; ## main dendrogram_recursive (m, 0); ## T reordering ## MATLAB compatibility: when n > P, each node group is renamed with a number ## between 1 and P, according to the smallest node index of each group; ## the group with the node 1 is always group 1, while group 2 is the group ## with the smallest node index outside of group 1, and group 3 is the group ## with the smallest node index outside of groups 1 and 2... newT = 1 : (length (T)); if (n > P) uniqueT = unique (T); minT = zeros (size (uniqueT)); counter = 1; for i = 1:length (uniqueT) # it should be exactly equal to P idcs = find (T == uniqueT(i)); minT(i) = min (idcs); endfor minT = minT(find (minT > 0)); # to prevent a strange bug [minT, minTidcs] = sort (minT); uniqueT = uniqueT(minTidcs); for i = 1:length (uniqueT) idcs = find (T == uniqueT(i)); newT(idcs) = counter++; endfor endif ## leaf reordering if (! isempty (vReorder)) if (P < n) checkT = newT(vReorder(:)); for i = 1 : P idcs = find (checkT == i); if (length (idcs) > 1) if (max (idcs) - min (idcs) >= length (idcs)) error (strcat ("dendrogram: invalid reordering that", ... " redefines the 'P' groups of leaves")); endif endif endfor checkT = unique (checkT, 'stable'); vNewLeafPosition = zeros (n, 1); uT = unique (T, 'stable'); for i = 1:P vNewLeafPosition(uT(checkT(i))) = i; endfor vLeafPosition = vNewLeafPosition; else for i = 1:length (vReorder) vLeafPosition(vReorder(i)) = i; endfor endif endif ## figure x = []; ## ticks and tricks xticks = 1:P; perm = zeros (P, 1); for i = 1 : length (vLeafPosition) if (vLeafPosition(i) != 0) idcs = find (T == i); perm(vLeafPosition(i)) = newT(idcs(1)); endif endfor T = newT; # this should be unnecessary for n <= P ## lines for i = (n - P + 1):m vLeafPosition(n + i) = mean (vLeafPosition(tree(i, 1:2), 1)); x(end + 1,1:4) = [vLeafPosition(tree(i, 1:2))' tree(i, [3 3])]; for j = 1 : 2 x0 = 0; if (tree(i,j) > (2 * n - P)) x0 = tree(tree(i, j) - n, 3); endif x(end + 1, 1:4) = [vLeafPosition(tree(i, [j j]))' x0 tree(i, 3)]; endfor endfor ## plot stuff if (strcmp (orientation, 'top')) H = line (x(:, 1:2)', x(:, 3:4)', 'color', 'blue'); set (gca, 'xticklabel', perm, 'xtick', xticks); elseif (strcmp (orientation, 'bottom')) H = line (x(:, 1:2)', x(:, 3:4)', 'color', 'blue'); set (gca, 'xticklabel', perm, 'xtick', xticks, 'xaxislocation', 'top'); axis ('ij'); elseif (strcmp (orientation, 'left')) H = line (x(:, 3:4)', x(:, 1:2)', 'color', 'blue'); set (gca, 'yticklabel', perm, 'ytick', xticks, 'xdir', 'reverse',... 'yaxislocation', 'right'); elseif (strcmp (orientation, 'right')) H = line (x(:, 3:4)', x(:, 1:2)', 'color', 'blue'); set (gca, 'yticklabel', perm, 'ytick', xticks); else close (H); error ("dendrogram: invalid orientation '%s'", orientation); endif ## labels if (! isempty (csLabels)) csCurrent = cellstr (num2str (perm)); for i = 1:n ## when there is just one leaf, use the named label for that leaf if (1 == length (find (T == i))) csCurrent(find (perm == i)) = csLabels(find (T == i)); endif endfor switch (orientation) case {'top', 'bottom'} xticklabels (csCurrent); case {'left', 'right'} yticklabels (csCurrent); endswitch endif ## check crossings if (checkCrossing && ! isempty (vReorder)) for j = 1:rows (x) if (x(j, 3) == x(j, 4)) # an horizontal line for i = 1:rows (x) if (x(i, 1) == x(i, 2) && ... # orthogonal lines (x(i, 1) > x(j, 1) && x(i, 1) < x(j, 2)) && ... (x(j, 3) > x(i, 3) && x(j, 3) < x(i, 4))) warning ("dendrogram: line intersection detected"); endif endfor endif endfor endif ## dendrogram_recursive function dendrogram_recursive (k, cn) if (tree(k, 3) > level_0) for j = 1:2 if (tree(k, j) > n) dendrogram_recursive (tree(k, j) - n, 0) else vLeafPosition(tree(k, j)) = nodecnt++; T(tree(k, j)) = tree(k, j); endif endfor else for j = 1:2 if (cn == 0) cn = n + k; vLeafPosition(cn) = nodecnt++; endif if (tree(k, j) > n) dendrogram_recursive (tree(k, j) - n, cn) else T(tree(k, j)) = cn; endif endfor endif endfunction endfunction %!demo %! ## simple dendrogram %! y = [4, 5; 2, 6; 3, 7; 8, 9; 1, 10]; %! y(:,3) = 1:5; %! dendrogram (y); %! title ('simple dendrogram'); %!demo %! ## another simple dendrogram %! rng (42); %! v = 2 * rand (30, 1) - 1; %! d = abs (bsxfun (@minus, v(:, 1), v(:, 1)')); %! y = linkage (squareform (d, 'tovector')); %! dendrogram (y); %! title ('another simple dendrogram'); %!demo %! ## collapsed tree, find all the leaves of node 5 %! rng (42); %! X = randn (60, 2); %! D = pdist (X); %! y = linkage (D, 'average'); %! subplot (2, 1, 1); %! title ('original tree'); %! dendrogram (y, 0); %! subplot (2, 1, 2); %! title ('collapsed tree'); %! [~, t] = dendrogram (y, 20); %! find (t == 5) %!demo %! ## optimal leaf order %! rng (42); %! X = randn (30, 2); %! D = pdist (X); %! y = linkage (D, 'average'); %! order = optimalleaforder (y, D); %! subplot (2, 1, 1); %! title ('original leaf order'); %! dendrogram (y); %! subplot (2, 1, 2); %! title ('optimal leaf order'); %! dendrogram (y, 'Reorder', order); %!demo %! ## horizontal orientation and labels %! rng (42); %! X = randn (8, 2); %! D = pdist (X); %! L = ['Snow White'; 'Doc'; 'Grumpy'; 'Happy'; 'Sleepy'; 'Bashful'; ... %! 'Sneezy'; 'Dopey']; %! y = linkage (D, 'average'); %! dendrogram (y, 'Orientation', 'left', 'Labels', L); %! title ('horizontal orientation and labels'); ## Test plotting %!shared visibility_setting %! visibility_setting = get (0, 'DefaultFigureVisible'); %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! y = [4, 5; 2, 6; 3, 7; 8, 9; 1, 10]; %! y(:,3) = 1:5; %! dendrogram (y); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! y = [4, 5; 2, 6; 3, 7; 8, 9; 1, 10]; %! y(:,3) = 1:5; %! dendrogram (y); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! v = 2 * rand (30, 1) - 1; %! d = abs (bsxfun (@minus, v(:, 1), v(:, 1)')); %! y = linkage (squareform (d, 'tovector')); %! dendrogram (y); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! X = randn (30, 2); %! D = pdist (X); %! y = linkage (D, 'average'); %! order = optimalleaforder (y, D); %! subplot (2, 1, 1); %! title ('original leaf order'); %! dendrogram (y); %! subplot (2, 1, 2); %! title ('optimal leaf order'); %! dendrogram (y, 'Reorder', order); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error dendrogram (); %!error dendrogram (ones (2, 2), 1); %!error dendrogram ([1 2 1], 1, 'xxx', 'xxx'); %!error dendrogram ([1 2 1], 'Reorder', 'xxx'); %!error dendrogram ([1 2 1], 'Reorder', [1 2 3 4]); %! fail ('dendrogram ([1 2 1], "Orientation", "north")', 'invalid orientation .*') statistics-release-2.1.2/inst/Plotting/doc-cache000066400000000000000000004407331527311104300216420ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 180 # name: # type: sq_string # elements: 1 # length: 11 andrewsplot # name: # type: sq_string # elements: 1 # length: 1470 statistics: andrewsplot (x) statistics: andrewsplot (x, name, value, ...) statistics: andrewsplot (ax, ...) statistics: h = andrewsplot (...) Create an Andrews plot of the multivariate data in x. andrewsplot (x) plots each observation (row) of the n-by-p matrix x as a smooth curve defined by the finite Fourier series f_i(t) = x_i1/sqrt(2) + x_i2 sin(2*pi*t) + x_i3 cos(2*pi*t) + x_i4 sin(4*pi*t) + x_i5 cos(4*pi*t) + ... evaluated over t in the interval [0,1], where x_ij is the j-th variable of the i-th observation. The following name/value pairs are accepted: "Group" A grouping variable (numeric, logical, character, string, or cell array of strings) with one entry per row of x. Curves are colored by group. "Standardize" Controls how the columns of x are transformed before the curves are computed: "off" (default) uses the raw data, "on" centers and scales each column to zero mean and unit standard deviation, "PCA" uses the principal component scores, and "PCAStd" uses the principal component scores of the standardized data. "Quantile" A scalar alpha in the interval (0,1). Instead of one curve per observation, only three curves per group are drawn: the pointwise median and the alpha and 1-alpha quantiles of the group's curves. andrewsplot (ax, ...) plots into the axes ax. The optional output h is a vector of handles to the plotted lines: one per observation, or three per group when "Quantile" is used. See also: parallelcoords, glyphplot, pca # name: # type: sq_string # elements: 1 # length: 53 Create an Andrews plot of the multivariate data in x. # name: # type: sq_string # elements: 1 # length: 4 bar3 # name: # type: sq_string # elements: 1 # length: 3571 statistics: bar3 (z) statistics: bar3 (y, z) statistics: bar3 (..., width) statistics: bar3 (..., style) statistics: bar3 (..., color) statistics: bar3 (..., name, value) statistics: bar3 (ax, ...) statistics: p = bar3 (...) Plot a 3D bar graph. bar3 (z) plots 3D bar graph for the elements of z. Each bar corresponds to an element in z, which can be a scalar, vector, or 2D matrix. By default, each column in z is considered as a series and it is handled as a distinct series of bars. When z is a vector, unlike MATLAB, which plots it as a single series of bars, Octave discriminates between a row and column vector of z. Hence, when z is column vector, it is plotted as a single series of bars (same color), whereas when z is row vector, each bar is plotted as a different group (different colors). For an MxN matrix, the function plots the bars corresponding to each row on the y-axis ranging from 1 to M and each column on the x-axis ranging from 1 to N. bar3 (y, z) plots a 3D bar graph of the elements in z at the y-values specified in y. It should be noted that y only affects the tick names along the y-axis rather the actual values. If you want to specify non-numerical values for y, you can specify it with the paired name/value syntax shown below. bar3 (..., width) sets the width of the bars along the x- and y-axes and controls the separation of bars among each other. width can take any value in the range (0,1]. By default, width is 0.8 and the bars have a small separation. If width is 1, the bars touch one another. Alternatively, you can define width as a two- element vector using the paired name/value syntax shown below, in which case you can control the bar separation along each axis independently. bar3 (..., style) specifies the style of the bars, where style can be 'detached', 'grouped', or 'stacked'. The default style is 'detached'. bar3 (..., color) displays all bars using the color specified by color. For example, use 'red' or 'r' to specify all red bars. When you want to specify colors for several groups, color can be a cellstr vector with each element specifying the color of each group. color can also be specified as a numerical Mx3 matrix, where each row corresponds to a RGB value with its elements in the range [0,1]. If only one color is specified, then it applies to all bars. If the number of colors equals the number of groups, then each color is applied to each group. If the number of colors equals the number of elements in z, then each individual bar is assigned the particular color. You can also define color using the paired name/value syntax shown below. bar3 (..., name, value) specifies one or more of the following name/value pairs: Name Value 'width' A two-element vector specifying the width of the bars along the x- and y-axes, respectively. Each element must be in the range (0,1]. 'color' A character or a cellstr vector, or a numerical Mx3 matrix following the same conventions as the color input argument. 'xlabel' A cellstr vector specifying the group names along the x-axis. 'ylabel' A cellstr vector specifying the names of the bars in the same series along the y-axis. bar3 (ax, ...) can also take an axes handle ax as a first argument in which case it plots into the axes specified by ax instead of into the current axes specified by gca (). The optional argument ax can precede any of the input argument combinations in the previous syntaxes. p = bar3 (...) returns a patch handle p, which can be used to set properties of the bars after displaying the 3D bar graph. See also: boxplot, hist3 # name: # type: sq_string # elements: 1 # length: 20 Plot a 3D bar graph. # name: # type: sq_string # elements: 1 # length: 5 bar3h # name: # type: sq_string # elements: 1 # length: 3598 statistics: bar3h (y) statistics: bar3h (z, y) statistics: bar3h (..., width) statistics: bar3h (..., style) statistics: bar3h (..., color) statistics: bar3h (..., name, value) statistics: bar3h (ax, ...) statistics: p = bar3h (...) Plot a horizontal 3D bar graph. bar3h (y) plots 3D bar graph for the elements of y. Each bar corresponds to an element in y, which can be a scalar, vector, or 2D matrix. By default, each column in y is considered as a series and it is handled as a distinct series of bars. When y is a vector, unlike MATLAB, which plots it as a single series of bars, Octave distinguishes between a row and column vector of y. Hence, when y is column vector, it is plotted as a single series of bars (same color), whereas when y is row vector, each bar is plotted as a different group (different colors). For an MxN matrix, the function plots the bars corresponding to each row on the z-axis ranging from 1 to M and each column on the x-axis ranging from 1 to N. bar3h (z, y) plots a 3D bar graph of the elements in y at the z-values specified in z. It should be noted that z only affects the tick names along the z-axis rather the actual values. If you want to specify non-numerical values for z, you can specify it with the paired name/value syntax shown below. bar3h (..., width) sets the width of the bars along the x- and z-axes and controls the separation of bars among each other. width can take any value in the range (0,1]. By default, width is 0.8 and the bars have a small separation. If width is 1, the bars touch one another. Alternatively, you can define width as a two- element vector using the paired name/value syntax shown below, in which case you can control the bar separation along each axis independently. bar3h (..., style) specifies the style of the bars, where style can be 'detached', 'grouped', or 'stacked'. The default style is 'detached'. bar3h (..., color) displays all bars using the color specified by color. For example, use 'red' or 'r' to specify all red bars. When you want to specify colors for several groups, color can be a cellstr vector with each element specifying the color of each group. color can also be specified as a numerical Mx3 matrix, where each row corresponds to a RGB value with its elements in the range [0,1]. If only one color is specified, then it applies to all bars. If the number of colors equals the number of groups, then each color is applied to each group. If the number of colors equals the number of elements in y, then each individual bar is assigned the particular color. You can also define color using the paired name/value syntax shown below. bar3h (..., name, value) specifies one or more of the following name/value pairs: Name Value 'width' A two-element vector specifying the width of the bars along the x- and z-axes, respectively. Each element must be in the range (0,1]. 'color' A character or a cellstr vector, or a numerical Mx3 matrix following the same conventions as the color input argument. 'xlabel' A cellstr vector specifying the group names along the x-axis. 'zlabel' A cellstr vector specifying the names of the bars in the same series along the z-axis. bar3h (ax, ...) can also take an axes handle ax as a first argument in which case it plots into the axes specified by ax instead of into the current axes specified by gca (). The optional argument ax can precede any of the input argument combinations in the previous syntaxes. p = bar3h (...) returns a patch handle p, which can be used to set properties of the bars after displaying the 3D bar graph. See also: boxplot, hist3 # name: # type: sq_string # elements: 1 # length: 31 Plot a horizontal 3D bar graph. # name: # type: sq_string # elements: 1 # length: 6 biplot # name: # type: sq_string # elements: 1 # length: 1814 statistics: biplot (coefs) statistics: biplot (coefs, name, value, ...) statistics: biplot (ax, ...) statistics: h = biplot (...) Create a biplot of the coefficients in coefs. biplot (coefs) plots the rows of coefs, typically the principal component coefficients returned by pca or pcacov or the factor loadings returned by factoran, as vectors from the origin. coefs has one row per observed variable and either two columns (for a 2-D biplot) or three columns (for a 3-D biplot). The following name/value pairs are accepted: "Scores" A matrix of scores with the same number of columns as coefs (one row per observation). The scores are plotted as points, scaled to fit within the unit circle relative to the maximum coefficient length: each score is divided by the maximum absolute score value and multiplied by the length of the longest coefficient vector. "VarLabels" A character array or cell array of strings labeling each variable vector. "ObsLabels" A character array or cell array of strings labeling each observation. "Positive" If true, the reference axes are drawn only over the positive quadrant (2-D) or octant (3-D). The default is false. Any additional name/value pairs are treated as line properties and applied to the variable vectors. For readability the sign of each column of coefs is chosen so that its largest-magnitude element is positive; the same sign change is applied to the corresponding column of the scores. biplot (ax, ...) plots into the axes ax. The optional output h is a column vector of handles to the plotted graphics objects, ordered as the variable vector lines, the variable markers, the variable text labels (if any), the observation markers, the observation text labels (if any), and finally the reference axis lines. See also: pca, pcacov, factoran, rotatefactors # name: # type: sq_string # elements: 1 # length: 45 Create a biplot of the coefficients in coefs. # name: # type: sq_string # elements: 1 # length: 15 blandAltmanPlot # name: # type: sq_string # elements: 1 # length: 1319 statistics: blandAltmanPlot (X, Y) statistics: blandAltmanPlot (ax, X, Y) statistics: blandAltmanPlot (..., Name, Value) statistics: H = blandAltmanPlot (...) Bland-Altman plot of the agreement between two measurement methods. blandAltmanPlot (X, Y) draws, for each subject measured by both methods, the difference X - Y against the mean of the two measurements, with a solid line at the bias, the mean difference, dashed lines at the limits of agreement, and dotted lines at the bounds of the confidence interval of each. limitsOfAgreement computes them, and the options it takes, 'Multiplier' and 'Alpha', are passed on to it, as are X and Y, whose requirements it states. A pattern in the points, the differences growing with the mean for instance, is a sign that the limits, which assume one spread of differences throughout, do not describe the methods well. blandAltmanPlot (ax, ...) draws into the axes ax rather than the current axes. H = blandAltmanPlot (...) returns the graphics objects drawn, in a row vector: the points, the lines at the bias, the lower limit and the upper limit, and the dotted pairs of lines of the intervals of the bias, the lower limit and the upper limit, each pair one line object. Each carries a DisplayName, so legend labels them. See also: limitsOfAgreement, gardnerAltmanPlot # name: # type: sq_string # elements: 1 # length: 67 Bland-Altman plot of the agreement between two measurement methods. # name: # type: sq_string # elements: 1 # length: 8 boxchart # name: # type: sq_string # elements: 1 # length: 1447 statistics: boxchart (ydata) statistics: boxchart (xgroupdata, ydata) statistics: boxchart (..., 'GroupByColor', cgroupdata) statistics: boxchart (tbl, yvar) statistics: boxchart (tbl, xvar, yvar) statistics: boxchart (..., name, value) statistics: boxchart (ax, ...) statistics: b = boxchart (...) Draw a box chart. boxchart (ydata) draws one box over the observations in ydata. The box spans the lower and upper quartiles with the median across it, the whiskers reach the furthest observations within one and a half interquartile ranges of the box, and whatever lies beyond is drawn as a marker. boxchart (xgroupdata, ydata) draws one box per distinct value of xgroupdata, which holds one value per observation. boxchart (tbl, yvar) and boxchart (tbl, xvar, yvar) take the observations, and where asked the grouping, from the named columns of a table. The chart keeps the table, so assigning SourceTable again re-reads it. b = boxchart (...) returns the stats.chart.BoxChart object drawn, whose properties may be set afterwards to redraw it; see stats.chart.BoxChart. Where 'GroupByColor' names a grouping, one object is returned per colour group, in the order of its distinct values. 'GroupByColor' cannot be given with a table, which MATLAB R2026a refuses as well. Every other name-value pair sets the property of that name on the object; see stats.chart.BoxChart for what each takes. See also: stats.chart.BoxChart, boxplot, swarmchart # name: # type: sq_string # elements: 1 # length: 17 Draw a box chart. # name: # type: sq_string # elements: 1 # length: 7 boxplot # name: # type: sq_string # elements: 1 # length: 5924 statistics: s = boxplot (data) statistics: s = boxplot (data, group) statistics: s = boxplot (data, notched, symbol, orientation, whisker, ...) statistics: s = boxplot (data, group, notched, symbol, orientation, whisker, ...) statistics: s = boxplot (data, options) statistics: s = boxplot (data, group, options, ...) statistics: [..., h] = boxplot (data, ...) Produce a box plot. A box plot is a graphical display that simultaneously describes several important features of a data set, such as center, spread, departure from symmetry, and identification of observations that lie unusually far from the bulk of the data. Input arguments (case-insensitive) recognized by boxplot are: data is a matrix with one column for each data set, or a cell vector with one cell for each data set. Each cell must contain a numerical row or column vector (NaN and NA are ignored) and not a nested vector of cells. notched = 1 produces a notched-box plot. Notches represent a robust estimate of the uncertainty about the median. notched = 0 (default) produces a rectangular box plot. notched within the interval (0,1) produces a notch of the specified depth. Notched values outside (0,1) are amusing if not exactly impractical. symbol sets the symbol for the outlier values. The default symbol for points that lie outside 3 times the interquartile range is 'o'; the default symbol for points between 1.5 and 3 times the interquartile range is '+'. Alternative symbol settings: symbol = '.': points between 1.5 and 3 times the IQR are marked with '.' and points outside 3 times IQR with 'o'. symbol = ['x','*']: points between 1.5 and 3 times the IQR are marked with 'x' and points outside 3 times IQR with '*'. orientation = 0 makes the boxes horizontally. orientation = 1 plots the boxes vertically (default). Alternatively, orientation can be passed as a string, e.g., 'vertical' or 'horizontal'. whisker defines the length of the whiskers as a function of the IQR (default = 1.5). If whisker = 0 then boxplot displays all data values outside the box using the plotting symbol for points that lie outside 3 times the IQR. group may be passed as an optional argument only in the second position after data. group can be a numeric, character, string, or categorical vector defining separate categories. To group by multiple variables simultaneously, pass a cell array of grouping vectors (e.g., {group1, group2}). A separate box is plotted for each unique combination of group values. All grouping variables must have the same length as data. options are additional paired arguments passed with the formalism (Name, Value) that provide extra functionality as listed below. options can be passed at any order after the initial arguments and are case-insensitive. 'Notch' 'on' Notched by 0.25 of the boxes width. 'off' Produces a straight box. scalar Proportional width of the notch. 'Symbol' '.' Defines only outliers between 1.5 and 3 IQR. ['x','*'] 2nd character defines outliers > 3 IQR 'Orientation' 'vertical' Default value, can also be defined with numerical 1. 'horizontal' Can also be defined with numerical 0. 'Whisker' scalar Multiplier of IQR (default is 1.5). 'OutlierTags' 'on' or 1 Plot the vector index of the outlier value next to its point. 'off' or 0 No tags are plotted (default value). 'Sample_IDs' 'cell' A cell vector with one cell for each data set containing a nested cell vector with each sample's ID (should be a string). If this option is passed, then all outliers are tagged with their respective sample's ID string instead of their vector's index. 'BoxWidth' 'proportional' Create boxes with their width proportional to the number of samples in their respective dataset (default value). 'fixed' Make all boxes with equal width. 'Widths' scalar Scaling factor for box widths (default value is 0.4). 'CapWidths' scalar Scaling factor for whisker cap widths (default value is 1, which results to 'Widths'/8 halflength) 'BoxStyle' 'outline' Draw boxes as outlines (default value). 'filled' Fill boxes with a color (outlines are still plotted). 'Positions' vector Numerical vector that defines the position of each data set. It must have the same length as the number of groups in a desired manner. This vector merely defines the points along the group axis, which by default is [1:number of groups]. 'Labels' cell A cell vector of strings containing the names of each group. By default each group is labeled numerically. If multiple grouping variables are provided, default labels are automatically generated by joining the category names and stacked hierarchically. 'Colors' character string or Nx3 numerical matrix If just one character or 1x3 vector of RGB values, specify the fill color of all boxes when BoxStyle = 'filled'. If a character string or Nx3 matrix is entered, box #1's fill color corresponds to the first character or first matrix row, and the next boxes' fill colors corresponds to the next characters or rows. If the char string or Nx3 array is exhausted the color selection wraps around. Supplemental arguments not described above (...) are concatenated and passed to the plot() function. The returned matrix s has one column for each data set as follows: 1 Minimum 2 1st quartile 3 2nd quartile (median) 4 3rd quartile 5 Maximum 6 Lower confidence limit for median 7 Upper confidence limit for median The quartiles are those of quantile at its default method, which is also what prctile returns, so the box edges of a data set always agree with prctile (data, [25, 75]). They set the inter-quartile range, and so the whisker fences and which observations are reported as outliers. The returned structure h contains handles to the plot elements, allowing customization of the visualization using set/get functions. Example title ("Grade 3 heights"); axis ([0,3]); set(gca (), "xtick", [1 2], "xticklabel", {"girls", "boys"}); boxplot ({randn(10,1)*5+140, randn(13,1)*8+135}); # name: # type: sq_string # elements: 1 # length: 19 Produce a box plot. # name: # type: sq_string # elements: 1 # length: 7 cdfplot # name: # type: sq_string # elements: 1 # length: 1017 statistics: hCDF = cdfplot (x) statistics: [hCDF, stats] = cdfplot (x) Display an empirical cumulative distribution function. hCDF = cdfplot (x) plots an empirical cumulative distribution function (CDF) of the observations in the data sample vector x. x may be a row or column vector, and represents a random sample of observations from some underlying distribution. cdfplot plots F(x), the empirical (or sample) CDF versus the observations in x. The empirical CDF, F(x), is defined as follows: F(x) = (Number of observations <= x) / (Total number of observations) for all values in the sample vector x. NaNs are ignored. hCDF is the handle of the empirical CDF curve (a handle graphics 'line' object). [hCDF, stats] = cdfplot (x) also returns a structure with the following fields as a statistical summary. STATS.min minimum value of x STATS.max maximum value of x STATS.mean sample mean of x STATS.median sample median (50th percentile) of x STATS.std sample standard deviation of x See also: qqplot, cdfcalc # name: # type: sq_string # elements: 1 # length: 54 Display an empirical cumulative distribution function. # name: # type: sq_string # elements: 1 # length: 10 dendrogram # name: # type: sq_string # elements: 1 # length: 1961 statistics: dendrogram (tree) statistics: dendrogram (tree, p) statistics: dendrogram (tree, prop, val) statistics: dendrogram (tree, p, prop, val ) statistics: h = dendrogram (...) statistics: [h, t, perm] = dendrogram (...) Plot a dendrogram of a hierarchical binary cluster tree. Given tree, a hierarchical binary cluster tree as the output of linkage, plot a dendrogram of the tree. The number of leaves shown by the dendrogram plot is limited to p. The default value for p is 30. Set p to 0 to plot all leaves. The optional outputs are h, t and perm: h is a handle to the lines of the plot. t is the vector with the numbers assigned to each leaf. Each element of t is a leaf of tree and its value is the number shown in the plot. When the dendrogram plot is collapsed, that is when the number of shown leaves p is inferior to the total number of leaves, a single leaf of the plot can represent more than one leaf of tree: in that case multiple elements of t share the same value, that is the same leaf of the plot. When the dendrogram plot is not collapsed, each leaf of the plot is the leaf of tree with the same number. perm is the vector list of the leaves as ordered as in the plot. Additional input properties can be specified by pairs of properties and values. Known properties are: 'Reorder' Reorder the leaves of the dendrogram plot using a numerical vector of size n, the number of leaves. When p is smaller than n, the reordering cannot break the p groups of leaves. 'Orientation' Change the orientation of the plot. Available values: top (default), bottom, left, right. 'CheckCrossing' Check if the lines of a reordered dendrogram cross each other. Available values: true (default), false. 'ColorThreshold' Not implemented. 'Labels' Use a char, string or cellstr array of size n to set the label for each leaf; the label is displayed only for nodes with just one leaf. See also: cluster, clusterdata, cophenet, inconsistent, linkage, pdist # name: # type: sq_string # elements: 1 # length: 56 Plot a dendrogram of a hierarchical binary cluster tree. # name: # type: sq_string # elements: 1 # length: 8 ecdfhist # name: # type: sq_string # elements: 1 # length: 1233 statistics: [n, c] = ecdfhist (f, x) statistics: [n, c] = ecdfhist (f, x, m) statistics: [n, c] = ecdfhist (f, x, centers) statistics: ecdfhist (...) statistics: ecdfhist (ax, ...) Create a histogram from the output of ecdf. [n, c] = ecdfhist (f, x) takes the empirical cumulative distribution function f evaluated at the points x, as computed by ecdf, and returns the heights n of histogram bars for 10 equally spaced bins together with their centers c. Unlike a count histogram, the bar heights are normalized so that the area of the histogram is equal to 1, giving an estimate of the probability density function. [n, c] = ecdfhist (f, x, m) uses m equally spaced bins. [n, c] = ecdfhist (f, x, centers) uses bins with the specified centers, given as a vector of monotonically increasing values. ecdfhist (...) without output arguments plots the histogram. ecdfhist (ax, ...) plots into the axes ax instead of the current axes. The probability mass assigned to each bin is the sum of the increments of the empirical cdf, diff (f), over the points x that fall closest to the corresponding bin center; ties are assigned to the lower center. Each bar height is that mass divided by the bin width. See also: ecdf, cdfplot, hist # name: # type: sq_string # elements: 1 # length: 43 Create a histogram from the output of ecdf. # name: # type: sq_string # elements: 1 # length: 8 einstein # name: # type: sq_string # elements: 1 # length: 1212 statistics: einstein () statistics: tiles = einstein (a, b) statistics: [tiles, rhat] = einstein (a, b) statistics: [tiles, rhat, that] = einstein (a, b) statistics: [tiles, rhat, that, shat] = einstein (a, b) statistics: [tiles, rhat, that, shat, phat] = einstein (a, b) statistics: [tiles, rhat, that, shat, phat, fhat] = einstein (a, b) Plots the tiling of the basic clusters of einstein tiles. Scalars a and b define the shape of the einstein tile. See Smith et al (2023) for details: https://arxiv.org/abs/2303.10798 tiles is a structure containing the coordinates of the einstein tiles that are tiled on the plot. Each field contains the tile coordinates of the corresponding clusters. tiles.rhat contains the reflected einstein tiles tiles.that contains the three-hat shells tiles.shat contains the single-hat clusters tiles.phat contains the paired-hat clusters tiles.fhat contains the fylfot clusters rhat contains the coordinates of the first reflected tile that contains the coordinates of the first three-hat shell shat contains the coordinates of the first single-hat cluster phat contains the coordinates of the first paired-hat cluster fhat contains the coordinates of the first fylfot cluster # name: # type: sq_string # elements: 1 # length: 57 Plots the tiling of the basic clusters of einstein tiles. # name: # type: sq_string # elements: 1 # length: 17 gardnerAltmanPlot # name: # type: sq_string # elements: 1 # length: 2789 statistics: gardnerAltmanPlot (X, Y) statistics: gardnerAltmanPlot (ax, X, Y) statistics: gardnerAltmanPlot (..., Name, Value) statistics: H = gardnerAltmanPlot (...) Gardner-Altman plot of two samples and the effect size between them. gardnerAltmanPlot (X, Y) draws the observations of the samples X and Y at the horizontal positions 1 and 2, and the difference between their means, with its confidence interval, at position 3, measured against a second vertical axis on the right. That axis is placed so that its zero is level with the centre of Y and the effect size level with the centre of X, so the effect reads off the data it comes from. X and Y are vectors of type double or single, and a missing value (NaN) is left out. Two unpaired samples are drawn as swarms, the points of each spread sideways where they crowd, with a black line from the centre of each sample to the effect size: solid for X, dashed for Y. The centre is the median for 'mediandiff' and the mean otherwise. Paired samples are drawn as a line from each observation of X to its partner in Y, the pairs that increase, decrease and stay equal each in a colour of their own. For 'cliff' and 'kstest', whose values do not share the scale of the data, the right axis spans the values the effect can take, [-1.1, 1.1] and [-0.1, 1.1], and two unpaired samples get a reference line at zero instead of the lines from their centres. gardnerAltmanPlot (ax, ...) draws into the axes ax rather than the current axes. gardnerAltmanPlot (..., Name, Value) takes the options 'Effect', 'Paired', 'VarianceType', 'Alpha', 'ConfidenceIntervalType', 'NumBootstraps', 'BootstrapOptions' and 'Resampling' of meanEffectSize, which computes the effect size and its interval; 'Effect' names a single effect. 'Mean' does not apply, there always being two samples. H = gardnerAltmanPlot (...) returns the graphics objects drawn, in a row vector. For two unpaired samples they are the two swarms, the error bar, and the lines from the centres of X and Y, or the reference line. For paired samples they are the lines of the pairs that increase, decrease and stay equal, each where there is any, and the error bar. Where 'ConfidenceIntervalType' is 'none' the error bar is a line holding a single marker. MATLAB draws the effect size on a second vertical axis of the same axes; Octave has no such axis, so here it is a second axes laid over the first, which follows the first's position and vertical limits. The error bar is Octave's errorbar object, whose bounds are its 'ldata' and 'udata' properties. Reference: M. J. Gardner and D. G. Altman (1986). Confidence intervals rather than P values: estimation rather than hypothesis testing. British Medical Journal, 292(6522), 746-750. See also: meanEffectSize, swarmchart, errorbar # name: # type: sq_string # elements: 1 # length: 68 Gardner-Altman plot of two samples and the effect size between them. # name: # type: sq_string # elements: 1 # length: 9 glyphplot # name: # type: sq_string # elements: 1 # length: 1534 statistics: glyphplot (x) statistics: glyphplot (x, name, value, ...) statistics: g = glyphplot (...) Create a star (glyph) plot of the multivariate data in x. glyphplot (x) draws each observation (row) of the n-by-p matrix x as a star glyph, arranged on a grid. The p spokes of each star radiate from its center at equally spaced angles, with lengths proportional to the values of the p variables; the tips of the spokes are joined to form the star perimeter. The following name/value pairs are accepted: "Glyph" "star" (default) draws star glyphs. "face" (Chernoff faces) is not currently supported. "Standardize" How the columns of x are scaled to spoke lengths: "column" (default) scales each column to the range [0,1], "matrix" scales the whole matrix to [0,1], "PCA" uses principal component scores scaled to [0,1], and "off" uses the values as given. A spoke of relative length 0 is still drawn at 10% of the maximum radius so that it remains visible. "Grid" A two-element vector [rows cols] specifying the layout of the glyphs. The default is chosen automatically. "Centers" An n-by-2 matrix giving the center coordinates of the glyphs explicitly, overriding "Grid". "Radius" The maximum glyph radius (default 0.4). "ObsLabels" A character array or cell array of strings labeling the observations. The default is the observation numbers. The optional output g is an n-by-3 matrix of handles whose columns hold, respectively, the star perimeters, the star spokes, and the text labels. See also: andrewsplot, parallelcoords # name: # type: sq_string # elements: 1 # length: 57 Create a star (glyph) plot of the multivariate data in x. # name: # type: sq_string # elements: 1 # length: 11 gplotmatrix # name: # type: sq_string # elements: 1 # length: 2559 statistics: gplotmatrix (x, y, group) statistics: gplotmatrix (x, [], group) statistics: gplotmatrix (x, y, group, clr, sym, siz) statistics: gplotmatrix (..., doleg, dispopt) statistics: gplotmatrix (..., doleg, dispopt, xnam, ynam) statistics: gplotmatrix (parent, ...) statistics: [h, ax, bigax] = gplotmatrix (...) Create a matrix of scatter plots grouped by a categorical variable. gplotmatrix (x, y, group) creates a matrix of scatter plots. Each subplot in the resulting figure is a scatter plot of a column of x against a column of y. If x is n-by-p and y is n-by-q, the resulting figure holds a q-by-p grid of subplots; the subplot in row i and column j plots x(:,j) on the horizontal axis against y(:,i) on the vertical axis. Points are grouped and colored according to group, which is a grouping variable (numeric, logical, character, string, or cell array of strings) with one entry per row of x. gplotmatrix (x, [], group) is equivalent to gplotmatrix (x, x, group) except that the diagonal of the p-by-p grid is replaced by grouped histograms of the columns of x. The appearance of the plot is controlled by further positional arguments: clr Marker colors, given as a character vector of color specifiers (e.g. "rgb") or as a matrix of RGB triplets, one row per group. Colors cycle if fewer are supplied than there are groups. sym Marker symbols, given as a character vector (e.g. "o+x"); defaults to ".". Symbols cycle if fewer are supplied than there are groups. siz Marker sizes, given as a numeric vector. Sizes cycle if fewer are supplied than there are groups. doleg Either "on" (default) to display a legend of the groups or "off" to suppress it. dispopt Controls the diagonal of the grid when y is empty: "stairs" (default) for grouped stairstep histograms, "hist" or "grpbars" for grouped bar histograms, "none" to leave the diagonal empty, or "variable" to write the variable names on the diagonal. xnam, ynam Character vectors or cell arrays of strings giving the names of the columns of x and y, used to label the outer axes. An optional leading parent argument (a figure or uipanel handle) selects the container for the plot. The optional outputs are h, an array of handles to the plotted objects with size ny-by-p-by-k (where ny is the number of rows of the grid and k the number of groups); ax, the matrix of handles to the subplot axes (with an extra row of hidden axes for the diagonal histograms); and bigax, the handle to the invisible enclosing axes used for titles and labels. See also: gscatter, plotmatrix, grpstats # name: # type: sq_string # elements: 1 # length: 67 Create a matrix of scatter plots grouped by a categorical variable. # name: # type: sq_string # elements: 1 # length: 8 gscatter # name: # type: sq_string # elements: 1 # length: 1532 statistics: gscatter (x, y, g) statistics: gscatter (x, y, g, clr, sym, siz) statistics: gscatter (..., doleg, xnam, ynam) statistics: h = gscatter (...) Draw a scatter plot with grouped data. gscatter is a utility function to draw a scatter plot of x and y, according to the groups defined by g. Input x and y are numeric vectors of the same size, while g is either a vector of the same size as x or a character matrix with the same number of rows as the size of x. As a vector g can be numeric, logical, a character array, a string array (not implemented), a cell string or cell array. A number of optional inputs change the appearance of the plot: "clr" defines the color for each group; if not enough colors are defined by "clr", gscatter cycles through the specified colors. Colors can be defined as named colors, as rgb triplets or as indices for the current colormap. The default value is a different color for each group, according to the current colormap. "sym" is a char array of symbols for each group; if not enough symbols are defined by "sym", gscatter cycles through the specified symbols. "siz" is a numeric array of sizes for each group; if not enough sizes are defined by "siz", gscatter cycles through the specified sizes. "doleg" is a boolean value to show the legend; it can be either on (default) or off. "xnam" is a character array, the name for the x axis. "ynam" is a character array, the name for the y axis. Output h is an array of graphics handles to the line object of each group. See also: scatter # name: # type: sq_string # elements: 1 # length: 38 Draw a scatter plot with grouped data. # name: # type: sq_string # elements: 1 # length: 7 heatmap # name: # type: sq_string # elements: 1 # length: 1851 statistics: heatmap (cdata) statistics: heatmap (xvalues, yvalues, cdata) statistics: heatmap (tbl, xvar, yvar) statistics: heatmap (parent, ...) statistics: heatmap (..., Name, Value) statistics: h = heatmap (...) Heatmap of a matrix, or of the rows of a table counted or aggregated over two of its variables. heatmap (cdata) draws the matrix cdata as a grid of coloured cells, each carrying its value, with a colour bar beside them. The columns are named '1', '2', ..., and so are the rows. heatmap (xvalues, yvalues, cdata) names the columns with xvalues and the rows with yvalues, vectors of distinct numbers, text or categories with one element for each column and each row of cdata. heatmap (tbl, xvar, yvar) makes a column of each distinct value of the table variable xvar and a row of each distinct value of yvar, and colours each cell by the number of rows of tbl falling in it. With 'ColorVariable' naming a numeric variable, each cell holds the mean of that variable over its rows instead, or what 'ColorMethod' asks for: 'count', 'mean', 'median', 'sum', 'min', 'max', or 'none' where each cell holds at most one row. The categories of a categorical variable are taken in their order, those no row uses included; numbers, text and logical values are sorted. A row whose xvar or yvar is missing is left out. heatmap (parent, ...) places the heatmap in the figure or panel parent rather than the current figure. The heatmap takes the place of the current axes of its parent, and is refused where hold is on for them. heatmap (..., Name, Value) sets properties of the heatmap; see stats.chart.HeatmapChart for all of them. h = heatmap (...) returns the heatmap, a stats.chart.HeatmapChart object, whose methods sortx, sorty, xlim and ylim reorder and restrict its columns and rows. See also: stats.chart.HeatmapChart, imagesc, crosstab # name: # type: sq_string # elements: 1 # length: 95 Heatmap of a matrix, or of the rows of a table counted or aggregated over two of its variables. # name: # type: sq_string # elements: 1 # length: 5 hist3 # name: # type: sq_string # elements: 1 # length: 1817 statistics: hist3 (X) statistics: hist3 (X, nbins) statistics: hist3 (X, "Nbins", nbins) statistics: hist3 (X, centers) statistics: hist3 (X, "Ctrs", centers) statistics: hist3 (X, "Edges", edges) statistics: [N, C] = hist3 (...) statistics: hist3 (..., prop, val, ...) statistics: hist3 (hax, ...) Produce bivariate (2D) histogram counts or plots. The elements to produce the histogram are taken from the Nx2 matrix X. Any row with NaN values are ignored. The actual bins can be configured in 3 different: number, centers, or edges of the bins: Number of bins (default) Produces equally spaced bins between the minimum and maximum values of X. Defined as a 2 element vector, nbins, one for each dimension. Defaults to [10 10]. Center of bins Defined as a cell array of 2 monotonically increasing vectors, centers. The width of each bin is determined from the adjacent values in the vector with the initial and final bin, extending to Infinity. Edge of bins Defined as a cell array of 2 monotonically increasing vectors, edges. N(i,j) contains the number of elements in X for which: edges{1}(i) <= X(:,1) < edges{1}(i+1) edges{2}(j) <= X(:,2) < edges{2}(j+1) The consequence of this definition is that values outside the initial and final edge values are ignored, and that the final bin only contains the number of elements exactly equal to the final edge. The return values, N and C, are the bin counts and centers respectively. These are specially useful to produce intensity maps: [counts, centers] = hist3 (data); imagesc (centers{1}, centers{2}, counts) If there is no output argument, or if the axes graphics handle hax is defined, the function will plot a 3 dimensional bar graph. Any extra property/value pairs are passed directly to the underlying surface object. See also: hist, histc, lookup, mesh # name: # type: sq_string # elements: 1 # length: 49 Produce bivariate (2D) histogram counts or plots. # name: # type: sq_string # elements: 1 # length: 7 histfit # name: # type: sq_string # elements: 1 # length: 1284 statistics: histfit (x) statistics: histfit (x, nbins) statistics: histfit (x, nbins, distname) statistics: histfit (ax, ...) statistics: h = histfit (...) Plot histogram with superimposed distribution fit. histfit (x) plots a histogram of the values in the vector x using the number of bins equal to the square root of the number of non-missing elements in x and superimposes a fitted normal density function. histfit (x, nbins) plots a histogram of the values in the vector x using nbins number of bins in the histogram and superimposes a fitted normal density function. histfit (x, nbins, distname) plots a histogram of the values in the vector x using nbins number of bins in the histogram and superimposes a fitted density function from the distribution specified by distname. histfit (ax, ...) uses the axes handle ax to plot the histogram and the fitted density function onto followed by any of the input argument combinations specified in the previous syntaxes. h = histfit (...) returns a vector of handles h, where h(1) is the handle to the histogram and h(2) is the handle to the density curve. Note: calling histfit without any input arguments will return a cell array of character vectors listing all supported distributions. See also: bar, hist, normplot, fitdist # name: # type: sq_string # elements: 1 # length: 50 Plot histogram with superimposed distribution fit. # name: # type: sq_string # elements: 1 # length: 13 manovacluster # name: # type: sq_string # elements: 1 # length: 807 statistics: manovacluster (stats) statistics: manovacluster (stats, method) statistics: h = manovacluster (stats) statistics: h = manovacluster (stats, method) Cluster group means using manova1 output. manovacluster (stats) draws a dendrogram showing the clustering of group means, calculated using the output STATS structure from manova1 and applying the single linkage algorithm. See the dendrogram function for more information about the figure. manovacluster (stats, method) uses the method algorithm in place of single linkage. The available methods are: "single" --- nearest distance "complete" --- furthest distance "average" --- average distance "centroid" --- center of mass distance "ward" --- inner squared distance h = manovacluster (...) returns a vector of line handles. See also: manova1 # name: # type: sq_string # elements: 1 # length: 41 Cluster group means using manova1 output. # name: # type: sq_string # elements: 1 # length: 8 normplot # name: # type: sq_string # elements: 1 # length: 674 statistics: normplot (x) statistics: normplot (ax, x) statistics: h = normplot (...) Produce normal probability plot of the data in x. If x is a matrix, normplot plots the data for each column. NaN values are ignored. h = normplot (ax, x) takes a handle ax in addition to the data in x and it uses that axes for plotting. You may get this handle of an existing plot with gca. The line joining the 1st and 3rd quantile is drawn solid whereas its extensions to both ends are dotted. If the underlying distribution is normal, the points will cluster around the solid part of the line. Other distribution types will introduce curvature in the plot. See also: cdfplot, wblplot # name: # type: sq_string # elements: 1 # length: 49 Produce normal probability plot of the data in x. # name: # type: sq_string # elements: 1 # length: 14 parallelcoords # name: # type: sq_string # elements: 1 # length: 1446 statistics: parallelcoords (x) statistics: parallelcoords (x, name, value, ...) statistics: parallelcoords (ax, ...) statistics: h = parallelcoords (...) Create a parallel coordinates plot of the multivariate data in x. parallelcoords (x) plots each observation (row) of the n-by-p matrix x as a line connecting the values of its p coordinates, which are placed at the equally spaced horizontal positions 1, 2, ..., p. The following name/value pairs are accepted: "Group" A grouping variable (numeric, logical, character, string, or cell array of strings) with one entry per row of x. Lines are colored by group. "Standardize" Controls how the columns of x are transformed before plotting: "off" (default) uses the raw data, "on" centers and scales each column to zero mean and unit standard deviation, "PCA" uses the principal component scores, and "PCAStd" uses the principal component scores of the standardized data. "Quantile" A scalar alpha in the interval (0,1). Instead of one line per observation, only three lines per group are drawn: the coordinate-wise median and the alpha and 1-alpha quantiles of the group. "Labels" A character array or cell array of strings giving the tick labels for the coordinate axis. parallelcoords (ax, ...) plots into the axes ax. The optional output h is a vector of handles to the plotted lines: one per observation, or three per group when "Quantile" is used. See also: andrewsplot, glyphplot, pca # name: # type: sq_string # elements: 1 # length: 65 Create a parallel coordinates plot of the multivariate data in x. # name: # type: sq_string # elements: 1 # length: 12 parallelplot # name: # type: sq_string # elements: 1 # length: 1794 statistics: parallelplot (data) statistics: parallelplot (tbl) statistics: parallelplot (parent, ...) statistics: parallelplot (..., Name, Value) statistics: h = parallelplot (...) Parallel coordinates plot of observations of several variables. parallelplot (data) draws each column of the numeric matrix data as a vertical ruler, a coordinate, and each row as a line joining its values across the rulers. By default each ruler has limits of its own, from the smallest to the largest value of its column. parallelplot (tbl) draws the variables of the table tbl as coordinates, named after them. A variable of categories, text or logical values places its distinct values evenly along its ruler. 'CoordinateData' chooses the columns of data to draw, and 'CoordinateVariables' the variables of tbl, by names, indices or a logical vector. With 'GroupData', a vector with one element for each observation, or 'GroupVariable', a variable of tbl, each group is drawn in a colour of its own, with a legend naming the groups in the order in which each first appears. 'DataNormalization' places the values as they are on rulers of their own, 'range', or on one shared scale: as they are, 'none'; as z-scores, 'zscore'; divided by the standard deviation, 'scale'; less the mean, 'center'; divided by the 2-norm, 'norm'. parallelplot (parent, ...) places the chart in the figure or panel parent rather than the current figure. The chart takes the place of the current axes of its parent, and is refused where hold is on for them. parallelplot (..., Name, Value) sets properties of the chart; see stats.chart.ParallelCoordinatesPlot for all of them. h = parallelplot (...) returns the chart, a stats.chart.ParallelCoordinatesPlot object. See also: stats.chart.ParallelCoordinatesPlot, parallelcoords # name: # type: sq_string # elements: 1 # length: 63 Parallel coordinates plot of observations of several variables. # name: # type: sq_string # elements: 1 # length: 6 ppplot # name: # type: sq_string # elements: 1 # length: 860 statistics: ppplot (x, dist) statistics: ppplot (x, dist, params) statistics: [p, y] = ppplot (x, dist, params) Perform a PP-plot (probability plot). If F is the CDF of the distribution dist with parameters params and x a sample vector of length n, the PP-plot graphs ordinate y(i) = F (i-th largest element of x) versus abscissa p(i) = (i - 0.5)/n. If the sample comes from F, the pairs will approximately follow a straight line. The default for dist is the standard normal distribution. The optional argument params contains a list of parameters of dist. For example, for a probability plot of the uniform distribution on [2,4] and x, use ppplot (x, "unif", 2, 4) dist can be any string for which a function distcdf that calculates the CDF of distribution dist exists. If no output is requested then the data are plotted immediately. See also: qqplot # name: # type: sq_string # elements: 1 # length: 37 Perform a PP-plot (probability plot). # name: # type: sq_string # elements: 1 # length: 8 probplot # name: # type: sq_string # elements: 1 # length: 1941 statistics: probplot (y) statistics: probplot (dist, y) statistics: probplot (dist, y, cens) statistics: probplot (dist, y, cens, freq) statistics: probplot (ax, ...) statistics: probplot (..., "noref") statistics: h = probplot (...) Produce a probability plot of the data in y against the distribution dist. On a probability plot the ordered data are drawn against a nonlinear probability axis chosen so that a sample from the reference distribution dist falls approximately along a straight line. Systematic departures from the reference line indicate departures from the distribution. dist is one of "normal" (the default when dist is omitted), "lognormal", "exponential", "extreme value", "weibull", "rayleigh", "logistic", or "loglogistic". For "lognormal", "weibull", and "loglogistic" the data axis is logarithmic. y is a numeric vector, or a matrix in which case each column is plotted as a separate sample. NaN values are ignored. cens is a logical vector the same size as y that is true for right-censored observations; censored points are not plotted and the plotting positions of the remaining points follow the Kaplan-Meier estimate. freq is a vector of nonnegative integer frequencies (counts) the same size as y. Pass [] to omit either one. probplot (ax, ...) plots into the axes ax instead of the current axes. The trailing option "noref" suppresses the reference line. h = probplot (...) returns a column vector of handles to the plotted line objects (the data markers, followed by the reference line unless "noref" was given). The reference line is a robust fit through the first and third quartiles of the data on the transformed scale. For censored data the quartiles are taken from the Kaplan-Meier plotting positions; when heavy censoring prevents the data from reaching a quartile the position is linearly extrapolated, which may deviate slightly from MATLAB. See also: normplot, wblplot, qqplot, cdfplot, ecdf # name: # type: sq_string # elements: 1 # length: 74 Produce a probability plot of the data in y against the distribution dist. # name: # type: sq_string # elements: 1 # length: 6 qqplot # name: # type: sq_string # elements: 1 # length: 1082 statistics: [q, s] = qqplot (x) statistics: [q, s] = qqplot (x, y) statistics: [q, s] = qqplot (x, dist) statistics: [q, s] = qqplot (x, y, params) statistics: qqplot (...) Perform a QQ-plot (quantile plot). If F is the CDF of the distribution dist with parameters params and G its inverse, and x a sample vector of length n, the QQ-plot graphs ordinate s(i) = i-th largest element of x versus abscissa q(if) = G((i - 0.5)/n). If the sample comes from F, except for a transformation of location and scale, the pairs will approximately follow a straight line. If the second argument is a vector y the empirical CDF of y is used as dist. The default for dist is the standard normal distribution. The optional argument params contains a list of parameters of dist. For example, for a quantile plot of the uniform distribution on [2,4] and x, use qqplot (x, "unif", 2, 4) dist can be any string for which a function distinv or dist_inv exists that calculates the inverse CDF of distribution dist. If no output arguments are given, the data are plotted directly. See also: ppplot # name: # type: sq_string # elements: 1 # length: 34 Perform a QQ-plot (quantile plot). # name: # type: sq_string # elements: 1 # length: 11 scatterhist # name: # type: sq_string # elements: 1 # length: 3371 statistics: scatterhist (x, y) statistics: scatterhist (x, y, name, value, ...) statistics: h = scatterhist (...) Create a scatter plot of x and y with marginal histograms. scatterhist (x, y) draws a scatter plot of the vectors x and y in a central set of axes, with a histogram of x above it and a histogram of y to its right. x and y must be vectors of the same length; NaN values are removed pairwise from the scatter plot and individually from each marginal histogram. The following name/value pairs are accepted: "Group" A grouping variable (numeric, logical, character, string, or cell array of strings) with one entry per point. The scatter points and the marginal histograms are separated and colored by group. "NBins" The number of bins for the marginal histograms, either a scalar applied to both or a two-element vector [nx ny]. The default is chosen by Scott's rule. "Kernel" "off" (default) draws histograms for the marginals; "on" or "overlay" draws kernel density estimates instead. "Location" Corner occupied by the marginal plots, the scatter taking the opposite one: "SouthWest" (default), "SouthEast", "NorthEast", or "NorthWest". "Legend" "on" or "off" to show or hide the group legend. The default is "on" when a grouping variable is supplied. "Marker", "MarkerSize" The marker symbol(s) and size(s) for the scatter points, cycled over the groups. The optional output h is a three-element vector of axes handles: the central scatter axes, the axes of the x (horizontal) histogram, and the axes of the y (vertical) histogram. The y histogram's axes is built differently from MATLAB's and its properties read accordingly. We plot that marginal transposed, so its "XLim" is the density and "XDir" carries the bar direction. MATLAB plots it like the x one -- data along "XLim", density along "YLim" -- and rotates the whole axes by setting "View" to [270, 90], so there the direction is carried by "YDir" and "XDir" is always "normal". The picture is the same; code reading those properties off h(3) is not portable between the two. 'Parent' draws into a supplied figure or uipanel instead of the current figure. The container is used as it stands and is never cleared, so a scatterhist can be placed alongside other axes. 'Location' names the corner the marginal histograms occupy, 'SouthWest' by default, or 'SouthEast', 'NorthEast' or 'NorthWest'; the scatter takes the opposite corner. With the default the histograms are drawn below and to the left of the scatter. 'Direction' points the marginal bars toward the scatter plot, 'in' by default, or away from it with 'out', whichever side 'Location' has placed them on. 'PlotGroup' draws one marginal per group with 'on', or a single pooled marginal with 'off'. It defaults to 'on' when 'Group' is given and to 'off' otherwise. 'Style' outlines the marginals as 'bar' or 'stairs', defaulting to stairs when the data are grouped and bars when they are not. 'Color' sets the group colours, either as a character vector of colour names or as an N-by-3 matrix of RGB values, cycled over the groups. 'LineStyle' and 'LineWidth' style the marginal outlines, not the scatter markers, and are cycled over the groups. 'Bandwidth' sets the kernel bandwidth used when 'Kernel' is on: a scalar for all marginals, a pair for x and y, or one row per group. See also: gscatter, scatter, hist, ksdensity # name: # type: sq_string # elements: 1 # length: 58 Create a scatter plot of x and y with marginal histograms. # name: # type: sq_string # elements: 1 # length: 16 scatterhistogram # name: # type: sq_string # elements: 1 # length: 1694 statistics: scatterhistogram (x, y) statistics: scatterhistogram (tbl, xvar, yvar) statistics: scatterhistogram (parent, ...) statistics: scatterhistogram (..., Name, Value) statistics: h = scatterhistogram (...) Scatter plot of two samples with the histogram of each along its sides. scatterhistogram (x, y) draws the points (x(i), y(i)) with a histogram of x above the scatter plot and a histogram of y to its right, each normalized as a probability density, with the bins histcounts chooses. x and y are numeric vectors of the same length; a missing value is left out of the drawing. scatterhistogram (tbl, xvar, yvar) takes the samples from the variables xvar and yvar of the table tbl, and labels the axes with their names. With 'GroupData', a vector of categories, text, numbers or logical values with one element for each observation, or 'GroupVariable', a variable of tbl holding them, each group is drawn in a colour of its own, with histograms of its own and a legend naming the groups in the order in which each first appears. scatterhistogram (parent, ...) places the chart in the figure or panel parent rather than the current figure. The chart takes the place of the current axes of its parent, and is refused where hold is on for them. scatterhistogram (..., Name, Value) sets properties of the chart; see stats.chart.ScatterHistogramChart for all of them, among which 'HistogramDisplayStyle', 'stairs', 'bar' or 'smooth' for a kernel density estimate, 'NumBins', 'BinWidths' and 'ScatterPlotLocation'. h = scatterhistogram (...) returns the chart, a stats.chart.ScatterHistogramChart object. See also: stats.chart.ScatterHistogramChart, scatterhist, histcounts, ksdensity # name: # type: sq_string # elements: 1 # length: 71 Scatter plot of two samples with the histogram of each along its sides. # name: # type: sq_string # elements: 1 # length: 10 silhouette # name: # type: sq_string # elements: 1 # length: 1748 statistics: silhouette (X, clust) statistics: [si, h] = silhouette (X, clust) statistics: [si, h] = silhouette (..., Metric, MetricArg) Compute the silhouette values of clustered data and show them on a plot. X is a n-by-p matrix of n data points in a p-dimensional space. Each datapoint is assigned to a cluster using clust, a vector of n elements, one cluster assignment for each data point. Each silhouette value of si, a vector of size n, is a measure of the likelihood that a data point is accurately classified to the right cluster. Defining "a" as the mean distance between a point and the other points from its cluster, and "b" as the mean distance between that point and the points from other clusters, the silhouette value of the i-th point is: $$ S_i = \frac{b_i - a_i}{max(a_1,b_i)} $$ Each element of si ranges from -1, minimum likelihood of a correct classification, to 1, maximum likelihood. Optional input value Metric is the metric used to compute the distances between data points. Since silhouette uses pdist to compute these distances, Metric is similar to the Distance input argument of pdist and it can be: A known distance metric defined as a string: euclidean, squaredeuclidean (default), seuclidean, mahalanobis, cityblock, minkowski, chebychev, cosine, correlation, hamming, jaccard, or spearman. A vector as those created by pdist. In this case X does nothing. A function handle that is passed to pdist with MetricArg as optional inputs. Optional return value h is a handle to the silhouette plot. Reference Peter J. Rousseeuw, Silhouettes: a Graphical Aid to the Interpretation and Validation of Cluster Analysis. 1987. doi:10.1016/0377-0427(87)90125-7 See also: dendrogram, evalclusters, kmeans, linkage, pdist # name: # type: sq_string # elements: 1 # length: 72 Compute the silhouette values of clustered data and show them on a plot. # name: # type: sq_string # elements: 1 # length: 20 stats.chart.BoxChart # name: # type: sq_string # elements: 1 # length: 1557 statistics: stats.chart.BoxChart A box chart, as boxchart draws it. A stats.chart.BoxChart holds the data a box chart is drawn from and the choices it is drawn with, and redraws itself whenever one of them is set. It is what boxchart returns, and the documented way to reach it; one is returned per colour group where 'GroupByColor' names one. The data comes either from vectors or from a table, never from both, and the object says which through XDataMode and YDataMode. Where the data came from vectors these read 'manual' and XVariable and YVariable are empty; where it came from a table they read 'auto' and naming a variable is what chooses the column. Setting YData on an object holding a table is refused, and so is naming a variable on one holding vectors, as MATLAB R2026a refuses them. Assigning SourceTable again re-reads the columns, so a table changed after the chart was drawn is picked up. The box spans the lower and upper quartiles with the median across it, the whiskers reach the furthest values within one and a half interquartile ranges of the box, and whatever lies beyond is drawn as a marker. These are the quartiles quantile computes and the rule isoutlier applies with 'quartiles'. MATLAB's object carries the properties every graphics object carries, Annotation and DataTipTemplate among them. This one is a handle class drawing with ordinary primitives rather than a graphics object of its own, so it carries what belongs to a box chart and leaves the rest to the primitives it draws. See also: boxchart, swarmchart, boxplot # name: # type: sq_string # elements: 1 # length: 34 A box chart, as boxchart draws it. # name: # type: sq_string # elements: 1 # length: 29 stats.chart.BoxChart.BoxChart # name: # type: sq_string # elements: 1 # length: 488 stats.chart.BoxChart: obj = stats.chart.BoxChart (hax, spec, args) Create a stats.chart.BoxChart object. hax is the axes to draw into, or empty for the current axes, which is resolved only once every value has been accepted. spec is a structure carrying the data as boxchart resolved it, with the fields XData, YData, SourceTable, XVariable, YVariable and XDataMode, and args the name-value pairs left to set. The documented way to reach this constructor is boxchart. See also: boxchart # name: # type: sq_string # elements: 1 # length: 37 Create a stats.chart.BoxChart object. # name: # type: sq_string # elements: 1 # length: 33 stats.chart.BoxChart.BoxEdgeColor # name: # type: sq_string # elements: 1 # length: 116 stats.chart.BoxChart: property BoxEdgeColor Colour of the box outline. Setting it puts BoxEdgeColorMode at 'manual'. # name: # type: sq_string # elements: 1 # length: 26 Colour of the box outline. # name: # type: sq_string # elements: 1 # length: 37 stats.chart.BoxChart.BoxEdgeColorMode # name: # type: sq_string # elements: 1 # length: 108 stats.chart.BoxChart: property BoxEdgeColorMode Whether BoxEdgeColor was chosen. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Whether BoxEdgeColor was chosen. # name: # type: sq_string # elements: 1 # length: 33 stats.chart.BoxChart.BoxFaceAlpha # name: # type: sq_string # elements: 1 # length: 104 stats.chart.BoxChart: property BoxFaceAlpha How opaque the box fill is, from 0 to 1. The default is 0.2. # name: # type: sq_string # elements: 1 # length: 40 How opaque the box fill is, from 0 to 1. # name: # type: sq_string # elements: 1 # length: 33 stats.chart.BoxChart.BoxFaceColor # name: # type: sq_string # elements: 1 # length: 142 stats.chart.BoxChart: property BoxFaceColor Colour the boxes are filled with, as an RGB triplet. Setting it puts BoxFaceColorMode at 'manual'. # name: # type: sq_string # elements: 1 # length: 52 Colour the boxes are filled with, as an RGB triplet. # name: # type: sq_string # elements: 1 # length: 37 stats.chart.BoxChart.BoxFaceColorMode # name: # type: sq_string # elements: 1 # length: 148 stats.chart.BoxChart: property BoxFaceColorMode Whether BoxFaceColor was chosen, 'manual', or left to the chart, 'auto'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 72 Whether BoxFaceColor was chosen, 'manual', or left to the chart, 'auto'. # name: # type: sq_string # elements: 1 # length: 39 stats.chart.BoxChart.BoxMedianLineColor # name: # type: sq_string # elements: 1 # length: 140 stats.chart.BoxChart: property BoxMedianLineColor Colour of the line marking the median. Setting it puts BoxMedianLineColorMode at 'manual'. # name: # type: sq_string # elements: 1 # length: 38 Colour of the line marking the median. # name: # type: sq_string # elements: 1 # length: 43 stats.chart.BoxChart.BoxMedianLineColorMode # name: # type: sq_string # elements: 1 # length: 120 stats.chart.BoxChart: property BoxMedianLineColorMode Whether BoxMedianLineColor was chosen. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Whether BoxMedianLineColor was chosen. # name: # type: sq_string # elements: 1 # length: 29 stats.chart.BoxChart.BoxWidth # name: # type: sq_string # elements: 1 # length: 115 stats.chart.BoxChart: property BoxWidth Width of each box, in the units of the horizontal axis. The default is 0.5. # name: # type: sq_string # elements: 1 # length: 55 Width of each box, in the units of the horizontal axis. # name: # type: sq_string # elements: 1 # length: 29 stats.chart.BoxChart.CapWidth # name: # type: sq_string # elements: 1 # length: 142 stats.chart.BoxChart: property CapWidth Width of the cap at the end of each whisker, in the units of the horizontal axis. The default is 0.25. # name: # type: sq_string # elements: 1 # length: 81 Width of the cap at the end of each whisker, in the units of the horizontal axis. # name: # type: sq_string # elements: 1 # length: 33 stats.chart.BoxChart.CapWidthMode # name: # type: sq_string # elements: 1 # length: 100 stats.chart.BoxChart: property CapWidthMode Whether CapWidth was chosen. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Whether CapWidth was chosen. # name: # type: sq_string # elements: 1 # length: 35 stats.chart.BoxChart.JitterOutliers # name: # type: sq_string # elements: 1 # length: 155 stats.chart.BoxChart: property JitterOutliers Whether outlier markers are spread across the width of the box rather than drawn in a line, 'off' by default. # name: # type: sq_string # elements: 1 # length: 109 Whether outlier markers are spread across the width of the box rather than drawn in a line, 'off' by default. # name: # type: sq_string # elements: 1 # length: 30 stats.chart.BoxChart.LineWidth # name: # type: sq_string # elements: 1 # length: 94 stats.chart.BoxChart: property LineWidth Width of the box and whisker lines. The default is 1. # name: # type: sq_string # elements: 1 # length: 35 Width of the box and whisker lines. # name: # type: sq_string # elements: 1 # length: 32 stats.chart.BoxChart.MarkerColor # name: # type: sq_string # elements: 1 # length: 116 stats.chart.BoxChart: property MarkerColor Colour of an outlier marker. Setting it puts MarkerColorMode at 'manual'. # name: # type: sq_string # elements: 1 # length: 28 Colour of an outlier marker. # name: # type: sq_string # elements: 1 # length: 36 stats.chart.BoxChart.MarkerColorMode # name: # type: sq_string # elements: 1 # length: 106 stats.chart.BoxChart: property MarkerColorMode Whether MarkerColor was chosen. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Whether MarkerColor was chosen. # name: # type: sq_string # elements: 1 # length: 31 stats.chart.BoxChart.MarkerSize # name: # type: sq_string # elements: 1 # length: 86 stats.chart.BoxChart: property MarkerSize Size of an outlier marker. The default is 6. # name: # type: sq_string # elements: 1 # length: 26 Size of an outlier marker. # name: # type: sq_string # elements: 1 # length: 32 stats.chart.BoxChart.MarkerStyle # name: # type: sq_string # elements: 1 # length: 115 stats.chart.BoxChart: property MarkerStyle Marker an outlier is drawn with, 'o' by default, or 'none' to draw none. # name: # type: sq_string # elements: 1 # length: 72 Marker an outlier is drawn with, 'o' by default, or 'none' to draw none. # name: # type: sq_string # elements: 1 # length: 26 stats.chart.BoxChart.Notch # name: # type: sq_string # elements: 1 # length: 263 stats.chart.BoxChart: property Notch Whether the box is notched about its median, 'off' by default. A notch spans the median plus and minus 1.57 IQR / sqrt (n), so two boxes whose notches do not overlap have medians that differ at roughly the five per cent level. # name: # type: sq_string # elements: 1 # length: 62 Whether the box is notched about its median, 'off' by default. # name: # type: sq_string # elements: 1 # length: 32 stats.chart.BoxChart.Orientation # name: # type: sq_string # elements: 1 # length: 112 stats.chart.BoxChart: property Orientation Whether the boxes stand 'vertical', the default, or lie 'horizontal'. # name: # type: sq_string # elements: 1 # length: 69 Whether the boxes stand 'vertical', the default, or lie 'horizontal'. # name: # type: sq_string # elements: 1 # length: 27 stats.chart.BoxChart.Parent # name: # type: sq_string # elements: 1 # length: 99 stats.chart.BoxChart: property Parent The axes the chart is drawn into. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 The axes the chart is drawn into. # name: # type: sq_string # elements: 1 # length: 32 stats.chart.BoxChart.SourceTable # name: # type: sq_string # elements: 1 # length: 238 stats.chart.BoxChart: property SourceTable The table the data is read from, empty where it came from vectors. Assigning it again re-reads the columns XVariable and YVariable name, so a table changed after the chart was drawn is picked up. # name: # type: sq_string # elements: 1 # length: 66 The table the data is read from, empty where it came from vectors. # name: # type: sq_string # elements: 1 # length: 37 stats.chart.BoxChart.WhiskerLineColor # name: # type: sq_string # elements: 1 # length: 86 stats.chart.BoxChart: property WhiskerLineColor Colour of the whiskers and their caps. # name: # type: sq_string # elements: 1 # length: 38 Colour of the whiskers and their caps. # name: # type: sq_string # elements: 1 # length: 37 stats.chart.BoxChart.WhiskerLineStyle # name: # type: sq_string # elements: 1 # length: 121 stats.chart.BoxChart: property WhiskerLineStyle Line style of the whiskers: '-' by default, or '--', ':', '-.' or 'none'. # name: # type: sq_string # elements: 1 # length: 73 Line style of the whiskers: '-' by default, or '--', ':', '-.' or 'none'. # name: # type: sq_string # elements: 1 # length: 26 stats.chart.BoxChart.XData # name: # type: sq_string # elements: 1 # length: 345 stats.chart.BoxChart: property XData Where each observation's box stands, one value per observation: a grouping variable, numeric or categorical. Where no grouping was given every observation shares one position and this is a categorical of one level. Setting it is refused while XVariable names a column, the data coming from the table instead. # name: # type: sq_string # elements: 1 # length: 108 Where each observation's box stands, one value per observation: a grouping variable, numeric or categorical. # name: # type: sq_string # elements: 1 # length: 30 stats.chart.BoxChart.XDataMode # name: # type: sq_string # elements: 1 # length: 272 stats.chart.BoxChart: property XDataMode Where XData came from: 'manual' where it was given as a grouping vector, and 'auto' where the chart derived it, from a table or from there being no grouping at all. This property is read-only, where MATLAB allows it to be assigned. # name: # type: sq_string # elements: 1 # length: 164 Where XData came from: 'manual' where it was given as a grouping vector, and 'auto' where the chart derived it, from a table or from there being no grouping at all. # name: # type: sq_string # elements: 1 # length: 30 stats.chart.BoxChart.XVariable # name: # type: sq_string # elements: 1 # length: 176 stats.chart.BoxChart: property XVariable The column of SourceTable the boxes are grouped by, empty where none was named. Naming one is refused while the data came from vectors. # name: # type: sq_string # elements: 1 # length: 79 The column of SourceTable the boxes are grouped by, empty where none was named. # name: # type: sq_string # elements: 1 # length: 26 stats.chart.BoxChart.YData # name: # type: sq_string # elements: 1 # length: 146 stats.chart.BoxChart: property YData The observations themselves, one value per observation. Setting it is refused while YVariable names a column. # name: # type: sq_string # elements: 1 # length: 55 The observations themselves, one value per observation. # name: # type: sq_string # elements: 1 # length: 30 stats.chart.BoxChart.YDataMode # name: # type: sq_string # elements: 1 # length: 213 stats.chart.BoxChart: property YDataMode Where YData came from: 'manual' where it was given as a vector and 'auto' where it was read from a table. This property is read-only, where MATLAB allows it to be assigned. # name: # type: sq_string # elements: 1 # length: 105 Where YData came from: 'manual' where it was given as a vector and 'auto' where it was read from a table. # name: # type: sq_string # elements: 1 # length: 30 stats.chart.BoxChart.YVariable # name: # type: sq_string # elements: 1 # length: 148 stats.chart.BoxChart: property YVariable The column of SourceTable holding the observations. Naming one is refused while the data came from vectors. # name: # type: sq_string # elements: 1 # length: 51 The column of SourceTable holding the observations. # name: # type: sq_string # elements: 1 # length: 24 stats.chart.HeatmapChart # name: # type: sq_string # elements: 1 # length: 1609 statistics: stats.chart.HeatmapChart A heatmap, as heatmap draws it. A stats.chart.HeatmapChart holds a matrix of values, the labels of its columns and rows, and the choices it is drawn with, and redraws itself whenever one of them is set. It is what heatmap returns, and the documented way to reach it. The values come either from a matrix or from a table, never from both. From a table, each distinct value of XVariable is a column and each distinct value of YVariable a row, and each cell holds the number of rows of the table falling in it, or the mean, median, sum, minimum or maximum of ColorVariable over them, as ColorMethod says. Missing values of ColorVariable are left out of the mean, median, sum, minimum and maximum, and counted by 'count'. XData and YData name the columns and rows of ColorData. XDisplayData and YDisplayData say which of them are shown and in what order, and XDisplayLabels and YDisplayLabels what they are called on the chart. ColorDisplayData is the matrix as shown. A heatmap takes the place of the current axes of its parent, as MATLAB's does, keeping their outer position, and refuses axes on which hold is on. A later plot into the figure takes the place of the heatmap in turn. MATLAB's chart is a graphics object of its own. This one is a handle class that draws into axes of its own, which become the current axes and are deleted with it; when a later plot clears those axes the chart lets go of them, and Parent and the drawing are gone. GridVisible, ColorbarVisible and Visible hold 'on' or 'off' where MATLAB holds an on-off state. See also: heatmap, imagesc # name: # type: sq_string # elements: 1 # length: 31 A heatmap, as heatmap draws it. # name: # type: sq_string # elements: 1 # length: 39 stats.chart.HeatmapChart.CellLabelColor # name: # type: sq_string # elements: 1 # length: 220 stats.chart.HeatmapChart: property CellLabelColor The colour of the cell labels, as an RGB triplet or a colour name; 'auto', the default, writes black on a light cell and white on a dark one, and 'none' writes no labels. # name: # type: sq_string # elements: 1 # length: 170 The colour of the cell labels, as an RGB triplet or a colour name; 'auto', the default, writes black on a light cell and white on a dark one, and 'none' writes no labels. # name: # type: sq_string # elements: 1 # length: 40 stats.chart.HeatmapChart.CellLabelFormat # name: # type: sq_string # elements: 1 # length: 144 stats.chart.HeatmapChart: property CellLabelFormat The format each value is written into its cell with, as sprintf takes it. '%0.4g' by default. # name: # type: sq_string # elements: 1 # length: 73 The format each value is written into its cell with, as sprintf takes it. # name: # type: sq_string # elements: 1 # length: 34 stats.chart.HeatmapChart.ColorData # name: # type: sq_string # elements: 1 # length: 238 stats.chart.HeatmapChart: property ColorData The matrix of values, one row for each element of YData and one column for each element of XData. Where the chart was drawn from a table it is computed from the table and setting it is refused. # name: # type: sq_string # elements: 1 # length: 97 The matrix of values, one row for each element of YData and one column for each element of XData. # name: # type: sq_string # elements: 1 # length: 41 stats.chart.HeatmapChart.ColorDisplayData # name: # type: sq_string # elements: 1 # length: 225 stats.chart.HeatmapChart: property ColorDisplayData ColorData as it is shown: its rows and columns in the order of YDisplayData and XDisplayData, missing where a displayed value is not in the data. This property is read-only. # name: # type: sq_string # elements: 1 # length: 145 ColorData as it is shown: its rows and columns in the order of YDisplayData and XDisplayData, missing where a displayed value is not in the data. # name: # type: sq_string # elements: 1 # length: 36 stats.chart.HeatmapChart.ColorLimits # name: # type: sq_string # elements: 1 # length: 323 stats.chart.HeatmapChart: property ColorLimits The values mapped to the first and the last colour of Colormap, as a 1-by-2 increasing vector. Until it is set it follows the values shown: their smallest and largest, v - 1 and v + 1 where they are all v, [0, 1] where none is finite or where the columns or rows are rescaled. # name: # type: sq_string # elements: 1 # length: 94 The values mapped to the first and the last colour of Colormap, as a 1-by-2 increasing vector. # name: # type: sq_string # elements: 1 # length: 36 stats.chart.HeatmapChart.ColorMethod # name: # type: sq_string # elements: 1 # length: 343 stats.chart.HeatmapChart: property ColorMethod How the rows of SourceTable falling in a cell make its value: 'count', 'mean', 'median', 'sum', 'min', 'max' or 'none', which takes the one value there is and is refused where a cell holds more than one. It is 'count' where no ColorVariable is named, 'mean' where one is, and 'none' for a matrix. # name: # type: sq_string # elements: 1 # length: 203 How the rows of SourceTable falling in a cell make its value: 'count', 'mean', 'median', 'sum', 'min', 'max' or 'none', which takes the one value there is and is refused where a cell holds more than one. # name: # type: sq_string # elements: 1 # length: 37 stats.chart.HeatmapChart.ColorScaling # name: # type: sq_string # elements: 1 # length: 390 stats.chart.HeatmapChart: property ColorScaling How the values are turned into colours: 'scaled', the default, maps them to the colormap as they are; 'scaledcolumns' and 'scaledrows' rescale each column or row to run from 0 to 1; 'log' maps their natural logarithm, a negative value then being left uncoloured with a warning. The cell labels show the values themselves whatever the scaling. # name: # type: sq_string # elements: 1 # length: 277 How the values are turned into colours: 'scaled', the default, maps them to the colormap as they are; 'scaledcolumns' and 'scaledrows' rescale each column or row to run from 0 to 1; 'log' maps their natural logarithm, a negative value then being left uncoloured with a warning. # name: # type: sq_string # elements: 1 # length: 38 stats.chart.HeatmapChart.ColorVariable # name: # type: sq_string # elements: 1 # length: 139 stats.chart.HeatmapChart: property ColorVariable The numeric variable of SourceTable aggregated into each cell, or empty to count the rows. # name: # type: sq_string # elements: 1 # length: 90 The numeric variable of SourceTable aggregated into each cell, or empty to count the rows. # name: # type: sq_string # elements: 1 # length: 40 stats.chart.HeatmapChart.ColorbarVisible # name: # type: sq_string # elements: 1 # length: 109 stats.chart.HeatmapChart: property ColorbarVisible Whether the colour bar is drawn, 'on' by default or 'off'. # name: # type: sq_string # elements: 1 # length: 58 Whether the colour bar is drawn, 'on' by default or 'off'. # name: # type: sq_string # elements: 1 # length: 33 stats.chart.HeatmapChart.Colormap # name: # type: sq_string # elements: 1 # length: 173 stats.chart.HeatmapChart: property Colormap The colours, as an M-by-3 matrix of values between 0 and 1. By default 256 colours running from a pale blue to [0, 0.447, 0.741]. # name: # type: sq_string # elements: 1 # length: 59 The colours, as an M-by-3 matrix of values between 0 and 1. # name: # type: sq_string # elements: 1 # length: 34 stats.chart.HeatmapChart.FontColor # name: # type: sq_string # elements: 1 # length: 137 stats.chart.HeatmapChart: property FontColor The colour of the title, the axis labels and the tick labels. [0.15, 0.15, 0.15] by default. # name: # type: sq_string # elements: 1 # length: 61 The colour of the title, the axis labels and the tick labels. # name: # type: sq_string # elements: 1 # length: 33 stats.chart.HeatmapChart.FontName # name: # type: sq_string # elements: 1 # length: 104 stats.chart.HeatmapChart: property FontName The font of every text on the chart, 'Helvetica' by default. # name: # type: sq_string # elements: 1 # length: 60 The font of every text on the chart, 'Helvetica' by default. # name: # type: sq_string # elements: 1 # length: 33 stats.chart.HeatmapChart.FontSize # name: # type: sq_string # elements: 1 # length: 106 stats.chart.HeatmapChart: property FontSize The size of every text on the chart, in points, 10 by default. # name: # type: sq_string # elements: 1 # length: 62 The size of every text on the chart, in points, 10 by default. # name: # type: sq_string # elements: 1 # length: 36 stats.chart.HeatmapChart.GridVisible # name: # type: sq_string # elements: 1 # length: 115 stats.chart.HeatmapChart: property GridVisible Whether lines are drawn between the cells, 'on' by default or 'off'. # name: # type: sq_string # elements: 1 # length: 68 Whether lines are drawn between the cells, 'on' by default or 'off'. # name: # type: sq_string # elements: 1 # length: 37 stats.chart.HeatmapChart.HeatmapChart # name: # type: sq_string # elements: 1 # length: 540 stats.chart.HeatmapChart: obj = stats.chart.HeatmapChart (parent, spec, args) Create a stats.chart.HeatmapChart object. parent is the figure or panel to place the chart in, or empty for the current figure, which is resolved only once every value has been accepted. spec is a structure carrying the data as heatmap resolved it, with the fields ColorData, XData, YData, SourceTable, XVariable, YVariable and ColorVariable, and args the name-value pairs left to set. The documented way to reach this constructor is heatmap. See also: heatmap # name: # type: sq_string # elements: 1 # length: 41 Create a stats.chart.HeatmapChart object. # name: # type: sq_string # elements: 1 # length: 38 stats.chart.HeatmapChart.InnerPosition # name: # type: sq_string # elements: 1 # length: 70 stats.chart.HeatmapChart: property InnerPosition The same as Position. # name: # type: sq_string # elements: 1 # length: 21 The same as Position. # name: # type: sq_string # elements: 1 # length: 36 stats.chart.HeatmapChart.Interpreter # name: # type: sq_string # elements: 1 # length: 116 stats.chart.HeatmapChart: property Interpreter How the texts are interpreted: 'tex', the default, 'latex' or 'none'. # name: # type: sq_string # elements: 1 # length: 69 How the texts are interpreted: 'tex', the default, 'latex' or 'none'. # name: # type: sq_string # elements: 1 # length: 41 stats.chart.HeatmapChart.MissingDataColor # name: # type: sq_string # elements: 1 # length: 164 stats.chart.HeatmapChart: property MissingDataColor The colour of a cell holding a missing value, as an RGB triplet or a colour name. [0.15, 0.15, 0.15] by default. # name: # type: sq_string # elements: 1 # length: 81 The colour of a cell holding a missing value, as an RGB triplet or a colour name. # name: # type: sq_string # elements: 1 # length: 41 stats.chart.HeatmapChart.MissingDataLabel # name: # type: sq_string # elements: 1 # length: 114 stats.chart.HeatmapChart: property MissingDataLabel The label of a cell holding a missing value, 'NaN' by default. # name: # type: sq_string # elements: 1 # length: 62 The label of a cell holding a missing value, 'NaN' by default. # name: # type: sq_string # elements: 1 # length: 38 stats.chart.HeatmapChart.OuterPosition # name: # type: sq_string # elements: 1 # length: 249 stats.chart.HeatmapChart: property OuterPosition The position of the whole chart in its parent, labels included, [0, 0, 1, 1] by default, or the outer position of the axes it took the place of. Setting it turns PositionConstraint to 'outerposition'. # name: # type: sq_string # elements: 1 # length: 144 The position of the whole chart in its parent, labels included, [0, 0, 1, 1] by default, or the outer position of the axes it took the place of. # name: # type: sq_string # elements: 1 # length: 31 stats.chart.HeatmapChart.Parent # name: # type: sq_string # elements: 1 # length: 113 stats.chart.HeatmapChart: property Parent The figure or panel the chart is placed in. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 The figure or panel the chart is placed in. # name: # type: sq_string # elements: 1 # length: 33 stats.chart.HeatmapChart.Position # name: # type: sq_string # elements: 1 # length: 444 stats.chart.HeatmapChart: property Position The position of the grid of cells in its parent, as [left, bottom, width, height] in Units. While PositionConstraint is 'outerposition' the chart places the cells within OuterPosition itself, leaving room for the labels and the colour bar, [0.13, 0.11, 0.732143, 0.815] of it at least; setting Position fixes the cells there and turns PositionConstraint to 'innerposition'. InnerPosition is the same. # name: # type: sq_string # elements: 1 # length: 91 The position of the grid of cells in its parent, as [left, bottom, width, height] in Units. # name: # type: sq_string # elements: 1 # length: 43 stats.chart.HeatmapChart.PositionConstraint # name: # type: sq_string # elements: 1 # length: 158 stats.chart.HeatmapChart: property PositionConstraint Which position the chart keeps when its labels change: 'outerposition', the default, or 'innerposition'. # name: # type: sq_string # elements: 1 # length: 104 Which position the chart keeps when its labels change: 'outerposition', the default, or 'innerposition'. # name: # type: sq_string # elements: 1 # length: 36 stats.chart.HeatmapChart.SourceTable # name: # type: sq_string # elements: 1 # length: 101 stats.chart.HeatmapChart: property SourceTable The table the chart was drawn from, or an empty table. # name: # type: sq_string # elements: 1 # length: 54 The table the chart was drawn from, or an empty table. # name: # type: sq_string # elements: 1 # length: 30 stats.chart.HeatmapChart.Title # name: # type: sq_string # elements: 1 # length: 216 stats.chart.HeatmapChart: property Title The title, a character vector or a cell array of them, one for each line. From a table it says what the cells hold, such as 'Count of Y vs. X' or 'Mean of V', until it is set. # name: # type: sq_string # elements: 1 # length: 73 The title, a character vector or a cell array of them, one for each line. # name: # type: sq_string # elements: 1 # length: 30 stats.chart.HeatmapChart.Units # name: # type: sq_string # elements: 1 # length: 93 stats.chart.HeatmapChart: property Units The units of the positions, 'normalized' by default. # name: # type: sq_string # elements: 1 # length: 52 The units of the positions, 'normalized' by default. # name: # type: sq_string # elements: 1 # length: 32 stats.chart.HeatmapChart.Visible # name: # type: sq_string # elements: 1 # length: 96 stats.chart.HeatmapChart: property Visible Whether the chart is shown, 'on' by default or 'off'. # name: # type: sq_string # elements: 1 # length: 53 Whether the chart is shown, 'on' by default or 'off'. # name: # type: sq_string # elements: 1 # length: 30 stats.chart.HeatmapChart.XData # name: # type: sq_string # elements: 1 # length: 223 stats.chart.HeatmapChart: property XData The names of the columns of ColorData, a column cell array of distinct character vectors. By default they are '1', '2', ..., and they follow the size of ColorData until they are set. # name: # type: sq_string # elements: 1 # length: 89 The names of the columns of ColorData, a column cell array of distinct character vectors. # name: # type: sq_string # elements: 1 # length: 37 stats.chart.HeatmapChart.XDisplayData # name: # type: sq_string # elements: 1 # length: 190 stats.chart.HeatmapChart: property XDisplayData The columns shown and their order, as elements of XData; a value not in XData is shown as a column of missing values. All of XData by default. # name: # type: sq_string # elements: 1 # length: 117 The columns shown and their order, as elements of XData; a value not in XData is shown as a column of missing values. # name: # type: sq_string # elements: 1 # length: 39 stats.chart.HeatmapChart.XDisplayLabels # name: # type: sq_string # elements: 1 # length: 213 stats.chart.HeatmapChart: property XDisplayLabels The labels of the columns shown, one for each element of XDisplayData, which they are by default. A label set stays with its column when XDisplayData is reordered. # name: # type: sq_string # elements: 1 # length: 97 The labels of the columns shown, one for each element of XDisplayData, which they are by default. # name: # type: sq_string # elements: 1 # length: 31 stats.chart.HeatmapChart.XLabel # name: # type: sq_string # elements: 1 # length: 128 stats.chart.HeatmapChart: property XLabel The label of the horizontal axis; from a table the name of XVariable, until it is set. # name: # type: sq_string # elements: 1 # length: 86 The label of the horizontal axis; from a table the name of XVariable, until it is set. # name: # type: sq_string # elements: 1 # length: 32 stats.chart.HeatmapChart.XLimits # name: # type: sq_string # elements: 1 # length: 221 stats.chart.HeatmapChart: property XLimits The first and the last column shown, as a 1-by-2 cell array of elements of XDisplayData; the columns between them in XDisplayData are shown. The whole of XDisplayData by default. # name: # type: sq_string # elements: 1 # length: 140 The first and the last column shown, as a 1-by-2 cell array of elements of XDisplayData; the columns between them in XDisplayData are shown. # name: # type: sq_string # elements: 1 # length: 34 stats.chart.HeatmapChart.XVariable # name: # type: sq_string # elements: 1 # length: 102 stats.chart.HeatmapChart: property XVariable The variable of SourceTable whose values are the columns. # name: # type: sq_string # elements: 1 # length: 57 The variable of SourceTable whose values are the columns. # name: # type: sq_string # elements: 1 # length: 30 stats.chart.HeatmapChart.YData # name: # type: sq_string # elements: 1 # length: 104 stats.chart.HeatmapChart: property YData The names of the rows of ColorData, as XData names its columns. # name: # type: sq_string # elements: 1 # length: 63 The names of the rows of ColorData, as XData names its columns. # name: # type: sq_string # elements: 1 # length: 37 stats.chart.HeatmapChart.YDisplayData # name: # type: sq_string # elements: 1 # length: 114 stats.chart.HeatmapChart: property YDisplayData The rows shown and their order, as XDisplayData gives the columns. # name: # type: sq_string # elements: 1 # length: 66 The rows shown and their order, as XDisplayData gives the columns. # name: # type: sq_string # elements: 1 # length: 39 stats.chart.HeatmapChart.YDisplayLabels # name: # type: sq_string # elements: 1 # length: 117 stats.chart.HeatmapChart: property YDisplayLabels The labels of the rows shown, as XDisplayLabels labels the columns. # name: # type: sq_string # elements: 1 # length: 67 The labels of the rows shown, as XDisplayLabels labels the columns. # name: # type: sq_string # elements: 1 # length: 31 stats.chart.HeatmapChart.YLabel # name: # type: sq_string # elements: 1 # length: 126 stats.chart.HeatmapChart: property YLabel The label of the vertical axis; from a table the name of YVariable, until it is set. # name: # type: sq_string # elements: 1 # length: 84 The label of the vertical axis; from a table the name of YVariable, until it is set. # name: # type: sq_string # elements: 1 # length: 32 stats.chart.HeatmapChart.YLimits # name: # type: sq_string # elements: 1 # length: 106 stats.chart.HeatmapChart: property YLimits The first and the last row shown, as XLimits gives the columns. # name: # type: sq_string # elements: 1 # length: 63 The first and the last row shown, as XLimits gives the columns. # name: # type: sq_string # elements: 1 # length: 34 stats.chart.HeatmapChart.YVariable # name: # type: sq_string # elements: 1 # length: 99 stats.chart.HeatmapChart: property YVariable The variable of SourceTable whose values are the rows. # name: # type: sq_string # elements: 1 # length: 54 The variable of SourceTable whose values are the rows. # name: # type: sq_string # elements: 1 # length: 31 stats.chart.HeatmapChart.delete # name: # type: sq_string # elements: 1 # length: 84 stats.chart.HeatmapChart: delete (h) Delete the heatmap and the axes it is drawn in. # name: # type: sq_string # elements: 1 # length: 47 Delete the heatmap and the axes it is drawn in. # name: # type: sq_string # elements: 1 # length: 30 stats.chart.HeatmapChart.sortx # name: # type: sq_string # elements: 1 # length: 665 stats.chart.HeatmapChart: sortx (h) stats.chart.HeatmapChart: sortx (h, row) stats.chart.HeatmapChart: sortx (h, row, direction) stats.chart.HeatmapChart: sortx (..., 'MissingPlacement', mp) Reorder the columns of a heatmap. sortx (h) puts the columns in the order of their names. sortx (h, row) puts them in the order of their values in the row named row, an element of YData; a cell array of rows sorts by the first, then by the next where the first ties. direction is 'ascend', the default, or 'descend'. Missing values go last when ascending and first when descending, unless mp, 'first', 'last' or 'auto', says otherwise. The new order is set as XDisplayData. # name: # type: sq_string # elements: 1 # length: 33 Reorder the columns of a heatmap. # name: # type: sq_string # elements: 1 # length: 30 stats.chart.HeatmapChart.sorty # name: # type: sq_string # elements: 1 # length: 300 stats.chart.HeatmapChart: sorty (h) stats.chart.HeatmapChart: sorty (h, column) stats.chart.HeatmapChart: sorty (h, column, direction) stats.chart.HeatmapChart: sorty (..., 'MissingPlacement', mp) Reorder the rows of a heatmap, as sortx reorders its columns, by the values in the column named column. # name: # type: sq_string # elements: 1 # length: 103 Reorder the rows of a heatmap, as sortx reorders its columns, by the values in the column named column. # name: # type: sq_string # elements: 1 # length: 29 stats.chart.HeatmapChart.xlim # name: # type: sq_string # elements: 1 # length: 166 stats.chart.HeatmapChart: limits = xlim (h) stats.chart.HeatmapChart: xlim (h, limits) The first and last column shown, as XLimits holds them; given limits, set them. # name: # type: sq_string # elements: 1 # length: 79 The first and last column shown, as XLimits holds them; given limits, set them. # name: # type: sq_string # elements: 1 # length: 29 stats.chart.HeatmapChart.ylim # name: # type: sq_string # elements: 1 # length: 163 stats.chart.HeatmapChart: limits = ylim (h) stats.chart.HeatmapChart: ylim (h, limits) The first and last row shown, as YLimits holds them; given limits, set them. # name: # type: sq_string # elements: 1 # length: 76 The first and last row shown, as YLimits holds them; given limits, set them. # name: # type: sq_string # elements: 1 # length: 35 stats.chart.ParallelCoordinatesPlot # name: # type: sq_string # elements: 1 # length: 1753 statistics: stats.chart.ParallelCoordinatesPlot A parallel coordinates plot, as parallelplot draws it. A stats.chart.ParallelCoordinatesPlot holds observations of several variables, optionally split into groups, and the choices they are drawn with, and redraws itself whenever one of them is set. It is what parallelplot returns, and the documented way to reach it. Each variable is a vertical ruler, a coordinate, and each observation a line joining its values across the rulers. The observations come either from a numeric matrix, Data, whose columns CoordinateData chooses, or from a table, SourceTable, whose variables CoordinateVariables chooses, never from both. A table variable of categories, text or logical values places its distinct values evenly along its ruler, whatever DataNormalization says, and Jitter spreads the lines passing through each value. A missing value breaks its line. The groups take the colours of the colour order, in the order in which each first appears in GroupData. A chart takes the place of the current axes of its parent, keeping their outer position, and refuses axes on which hold is on; a later plot into the figure takes the place of the chart in turn. MATLAB's chart is a graphics object of its own; this one is a handle class drawing into axes of its own, made current, and it lets go of them when a later plot clears them. Octave's lines carry no transparency, so LineAlpha blends each line's colour toward the background of the axes instead, which matches MATLAB where lines do not cross. LegendVisible and Visible hold 'on' or 'off', and LineStyle and MarkerStyle a character vector or a cell array of them, where MATLAB holds an on-off state and a string array. See also: parallelplot, parallelcoords # name: # type: sq_string # elements: 1 # length: 54 A parallel coordinates plot, as parallelplot draws it. # name: # type: sq_string # elements: 1 # length: 41 stats.chart.ParallelCoordinatesPlot.Color # name: # type: sq_string # elements: 1 # length: 156 stats.chart.ParallelCoordinatesPlot: property Color The colour of each group, one row of an RGB matrix for each; by default the colours of the colour order. # name: # type: sq_string # elements: 1 # length: 104 The colour of each group, one row of an RGB matrix for each; by default the colours of the colour order. # name: # type: sq_string # elements: 1 # length: 50 stats.chart.ParallelCoordinatesPlot.CoordinateData # name: # type: sq_string # elements: 1 # length: 231 stats.chart.ParallelCoordinatesPlot: property CoordinateData The columns of Data drawn as coordinates, in the order drawn, as indices or a logical vector; all of them by default. Setting it on a chart drawn from a table is refused. # name: # type: sq_string # elements: 1 # length: 117 The columns of Data drawn as coordinates, in the order drawn, as indices or a logical vector; all of them by default. # name: # type: sq_string # elements: 1 # length: 51 stats.chart.ParallelCoordinatesPlot.CoordinateLabel # name: # type: sq_string # elements: 1 # length: 94 stats.chart.ParallelCoordinatesPlot: property CoordinateLabel The label under the coordinates. # name: # type: sq_string # elements: 1 # length: 32 The label under the coordinates. # name: # type: sq_string # elements: 1 # length: 56 stats.chart.ParallelCoordinatesPlot.CoordinateTickLabels # name: # type: sq_string # elements: 1 # length: 186 stats.chart.ParallelCoordinatesPlot: property CoordinateTickLabels The label of each coordinate, a column cell array; by default the numbers of the columns or the names of the variables. # name: # type: sq_string # elements: 1 # length: 119 The label of each coordinate, a column cell array; by default the numbers of the columns or the names of the variables. # name: # type: sq_string # elements: 1 # length: 55 stats.chart.ParallelCoordinatesPlot.CoordinateVariables # name: # type: sq_string # elements: 1 # length: 277 stats.chart.ParallelCoordinatesPlot: property CoordinateVariables The variables of SourceTable drawn as coordinates, in the order drawn, as names, indices or a logical vector, kept in the form given; all of them by default. Setting it on a chart drawn from a matrix is refused. # name: # type: sq_string # elements: 1 # length: 157 The variables of SourceTable drawn as coordinates, in the order drawn, as names, indices or a logical vector, kept in the form given; all of them by default. # name: # type: sq_string # elements: 1 # length: 40 stats.chart.ParallelCoordinatesPlot.Data # name: # type: sq_string # elements: 1 # length: 199 stats.chart.ParallelCoordinatesPlot: property Data The observations, a numeric matrix with one row for each observation and one column for each variable; empty where the chart was drawn from a table. # name: # type: sq_string # elements: 1 # length: 148 The observations, a numeric matrix with one row for each observation and one column for each variable; empty where the chart was drawn from a table. # name: # type: sq_string # elements: 1 # length: 45 stats.chart.ParallelCoordinatesPlot.DataLabel # name: # type: sq_string # elements: 1 # length: 84 stats.chart.ParallelCoordinatesPlot: property DataLabel The label beside the rulers. # name: # type: sq_string # elements: 1 # length: 28 The label beside the rulers. # name: # type: sq_string # elements: 1 # length: 53 stats.chart.ParallelCoordinatesPlot.DataNormalization # name: # type: sq_string # elements: 1 # length: 393 stats.chart.ParallelCoordinatesPlot: property DataNormalization How the values are placed along the rulers: 'range', the default, as they are on rulers with limits of their own; 'none' as they are on rulers sharing one scale; 'zscore' as z-scores; 'scale' divided by the standard deviation; 'center' less the mean; 'norm' divided by the 2-norm of the variable. All but 'range' share one scale. # name: # type: sq_string # elements: 1 # length: 296 How the values are placed along the rulers: 'range', the default, as they are on rulers with limits of their own; 'none' as they are on rulers sharing one scale; 'zscore' as z-scores; 'scale' divided by the standard deviation; 'center' less the mean; 'norm' divided by the 2-norm of the variable. # name: # type: sq_string # elements: 1 # length: 44 stats.chart.ParallelCoordinatesPlot.FontName # name: # type: sq_string # elements: 1 # length: 115 stats.chart.ParallelCoordinatesPlot: property FontName The font of every text on the chart, 'Helvetica' by default. # name: # type: sq_string # elements: 1 # length: 60 The font of every text on the chart, 'Helvetica' by default. # name: # type: sq_string # elements: 1 # length: 44 stats.chart.ParallelCoordinatesPlot.FontSize # name: # type: sq_string # elements: 1 # length: 117 stats.chart.ParallelCoordinatesPlot: property FontSize The size of every text on the chart, in points, 10 by default. # name: # type: sq_string # elements: 1 # length: 62 The size of every text on the chart, in points, 10 by default. # name: # type: sq_string # elements: 1 # length: 45 stats.chart.ParallelCoordinatesPlot.GroupData # name: # type: sq_string # elements: 1 # length: 205 stats.chart.ParallelCoordinatesPlot: property GroupData The group of each observation: a vector with one element for each row of Data of categories, text, numbers or logical values, or empty for no groups. # name: # type: sq_string # elements: 1 # length: 149 The group of each observation: a vector with one element for each row of Data of categories, text, numbers or logical values, or empty for no groups. # name: # type: sq_string # elements: 1 # length: 49 stats.chart.ParallelCoordinatesPlot.GroupVariable # name: # type: sq_string # elements: 1 # length: 117 stats.chart.ParallelCoordinatesPlot: property GroupVariable The variable of SourceTable holding the groups, or empty. # name: # type: sq_string # elements: 1 # length: 57 The variable of SourceTable holding the groups, or empty. # name: # type: sq_string # elements: 1 # length: 49 stats.chart.ParallelCoordinatesPlot.InnerPosition # name: # type: sq_string # elements: 1 # length: 81 stats.chart.ParallelCoordinatesPlot: property InnerPosition The same as Position. # name: # type: sq_string # elements: 1 # length: 21 The same as Position. # name: # type: sq_string # elements: 1 # length: 42 stats.chart.ParallelCoordinatesPlot.Jitter # name: # type: sq_string # elements: 1 # length: 202 stats.chart.ParallelCoordinatesPlot: property Jitter How far the lines through one value of a variable of categories are spread, from 0 to 1, where 1 fills the room up to the next value. 0.1 by default. # name: # type: sq_string # elements: 1 # length: 133 How far the lines through one value of a variable of categories are spread, from 0 to 1, where 1 fills the room up to the next value. # name: # type: sq_string # elements: 1 # length: 47 stats.chart.ParallelCoordinatesPlot.LegendTitle # name: # type: sq_string # elements: 1 # length: 133 stats.chart.ParallelCoordinatesPlot: property LegendTitle The title of the legend; from a table the name of GroupVariable, until set. # name: # type: sq_string # elements: 1 # length: 75 The title of the legend; from a table the name of GroupVariable, until set. # name: # type: sq_string # elements: 1 # length: 49 stats.chart.ParallelCoordinatesPlot.LegendVisible # name: # type: sq_string # elements: 1 # length: 155 stats.chart.ParallelCoordinatesPlot: property LegendVisible Whether the legend of the groups is shown, 'on' or 'off'; on where there are groups, until set. # name: # type: sq_string # elements: 1 # length: 95 Whether the legend of the groups is shown, 'on' or 'off'; on where there are groups, until set. # name: # type: sq_string # elements: 1 # length: 45 stats.chart.ParallelCoordinatesPlot.LineAlpha # name: # type: sq_string # elements: 1 # length: 142 stats.chart.ParallelCoordinatesPlot: property LineAlpha How opaque the lines are, from 0 to 1, a scalar or one for each group, 0.7 by default. # name: # type: sq_string # elements: 1 # length: 86 How opaque the lines are, from 0 to 1, a scalar or one for each group, 0.7 by default. # name: # type: sq_string # elements: 1 # length: 45 stats.chart.ParallelCoordinatesPlot.LineStyle # name: # type: sq_string # elements: 1 # length: 145 stats.chart.ParallelCoordinatesPlot: property LineStyle The line style of each group, a character vector or a cell array of them, '-' by default. # name: # type: sq_string # elements: 1 # length: 89 The line style of each group, a character vector or a cell array of them, '-' by default. # name: # type: sq_string # elements: 1 # length: 45 stats.chart.ParallelCoordinatesPlot.LineWidth # name: # type: sq_string # elements: 1 # length: 125 stats.chart.ParallelCoordinatesPlot: property LineWidth The width of the lines, a scalar or one for each group, 1 by default. # name: # type: sq_string # elements: 1 # length: 69 The width of the lines, a scalar or one for each group, 1 by default. # name: # type: sq_string # elements: 1 # length: 46 stats.chart.ParallelCoordinatesPlot.MarkerSize # name: # type: sq_string # elements: 1 # length: 137 stats.chart.ParallelCoordinatesPlot: property MarkerSize The size of the markers in points, a scalar or one for each group, 6 by default. # name: # type: sq_string # elements: 1 # length: 80 The size of the markers in points, a scalar or one for each group, 6 by default. # name: # type: sq_string # elements: 1 # length: 47 stats.chart.ParallelCoordinatesPlot.MarkerStyle # name: # type: sq_string # elements: 1 # length: 118 stats.chart.ParallelCoordinatesPlot: property MarkerStyle The marker at each value, for each group, 'none' by default. # name: # type: sq_string # elements: 1 # length: 60 The marker at each value, for each group, 'none' by default. # name: # type: sq_string # elements: 1 # length: 49 stats.chart.ParallelCoordinatesPlot.OuterPosition # name: # type: sq_string # elements: 1 # length: 204 stats.chart.ParallelCoordinatesPlot: property OuterPosition The position of the whole chart in its parent, labels included, [0, 0, 1, 1] by default, or the outer position of the axes it took the place of. # name: # type: sq_string # elements: 1 # length: 144 The position of the whole chart in its parent, labels included, [0, 0, 1, 1] by default, or the outer position of the axes it took the place of. # name: # type: sq_string # elements: 1 # length: 59 stats.chart.ParallelCoordinatesPlot.ParallelCoordinatesPlot # name: # type: sq_string # elements: 1 # length: 595 stats.chart.ParallelCoordinatesPlot: obj = stats.chart.ParallelCoordinatesPlot (parent, spec, args) Create a stats.chart.ParallelCoordinatesPlot object. parent is the figure or panel to place the chart in, or empty for the current figure, which is resolved only once every value has been accepted. spec is a structure carrying the data as parallelplot resolved it, with the fields Data, SourceTable, CoordinateData, CoordinateVariables, GroupData and GroupVariable, and args the name-value pairs left to set. The documented way to reach this constructor is parallelplot. See also: parallelplot # name: # type: sq_string # elements: 1 # length: 52 Create a stats.chart.ParallelCoordinatesPlot object. # name: # type: sq_string # elements: 1 # length: 42 stats.chart.ParallelCoordinatesPlot.Parent # name: # type: sq_string # elements: 1 # length: 124 stats.chart.ParallelCoordinatesPlot: property Parent The figure or panel the chart is placed in. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 The figure or panel the chart is placed in. # name: # type: sq_string # elements: 1 # length: 44 stats.chart.ParallelCoordinatesPlot.Position # name: # type: sq_string # elements: 1 # length: 340 stats.chart.ParallelCoordinatesPlot: property Position The position of the rulers in their parent, as [left, bottom, width, height] in Units. While PositionConstraint is 'outerposition' the chart places them within OuterPosition itself; setting it fixes them there and turns PositionConstraint to 'innerposition'. InnerPosition is the same. # name: # type: sq_string # elements: 1 # length: 86 The position of the rulers in their parent, as [left, bottom, width, height] in Units. # name: # type: sq_string # elements: 1 # length: 54 stats.chart.ParallelCoordinatesPlot.PositionConstraint # name: # type: sq_string # elements: 1 # length: 146 stats.chart.ParallelCoordinatesPlot: property PositionConstraint Which position the chart keeps: 'outerposition', the default, or 'innerposition'. # name: # type: sq_string # elements: 1 # length: 81 Which position the chart keeps: 'outerposition', the default, or 'innerposition'. # name: # type: sq_string # elements: 1 # length: 47 stats.chart.ParallelCoordinatesPlot.SourceTable # name: # type: sq_string # elements: 1 # length: 103 stats.chart.ParallelCoordinatesPlot: property SourceTable The table the chart was drawn from, or empty. # name: # type: sq_string # elements: 1 # length: 45 The table the chart was drawn from, or empty. # name: # type: sq_string # elements: 1 # length: 41 stats.chart.ParallelCoordinatesPlot.Title # name: # type: sq_string # elements: 1 # length: 75 stats.chart.ParallelCoordinatesPlot: property Title The title of the chart. # name: # type: sq_string # elements: 1 # length: 23 The title of the chart. # name: # type: sq_string # elements: 1 # length: 41 stats.chart.ParallelCoordinatesPlot.Units # name: # type: sq_string # elements: 1 # length: 104 stats.chart.ParallelCoordinatesPlot: property Units The units of the positions, 'normalized' by default. # name: # type: sq_string # elements: 1 # length: 52 The units of the positions, 'normalized' by default. # name: # type: sq_string # elements: 1 # length: 43 stats.chart.ParallelCoordinatesPlot.Visible # name: # type: sq_string # elements: 1 # length: 107 stats.chart.ParallelCoordinatesPlot: property Visible Whether the chart is shown, 'on' by default or 'off'. # name: # type: sq_string # elements: 1 # length: 53 Whether the chart is shown, 'on' by default or 'off'. # name: # type: sq_string # elements: 1 # length: 42 stats.chart.ParallelCoordinatesPlot.delete # name: # type: sq_string # elements: 1 # length: 93 stats.chart.ParallelCoordinatesPlot: delete (h) Delete the chart and the axes it is drawn in. # name: # type: sq_string # elements: 1 # length: 45 Delete the chart and the axes it is drawn in. # name: # type: sq_string # elements: 1 # length: 33 stats.chart.ScatterHistogramChart # name: # type: sq_string # elements: 1 # length: 1432 statistics: stats.chart.ScatterHistogramChart A scatter plot with a histogram of each variable along its sides, as scatterhistogram draws it. A stats.chart.ScatterHistogramChart holds two samples, optionally split into groups, and the choices they are drawn with, and redraws itself whenever one of them is set. It is what scatterhistogram returns, and the documented way to reach it. The samples come either from vectors or from a table, never from both. The histograms are normalized as probability densities, one for each group, with the bins histcounts chooses for each group unless NumBins or BinWidths say otherwise. The groups take the colours of the colour order and the line styles '-', ':', '-.' and '--' in turn, in the order in which each first appears in GroupData. A chart takes the place of the current axes of its parent, keeping their outer position, and refuses axes on which hold is on; a later plot into the figure takes the place of the chart in turn. MATLAB's chart is a graphics object of its own; this one is a handle class drawing into three axes of its own, the scatter plot's being made current, and it lets go of them when a later plot clears them. LegendVisible, MarkerFilled and Visible hold 'on' or 'off', and LineStyle and MarkerStyle a character vector or a cell array of them, where MATLAB holds an on-off state and a string array. See also: scatterhistogram, scatterhist, histcounts, ksdensity # name: # type: sq_string # elements: 1 # length: 95 A scatter plot with a histogram of each variable along its sides, as scatterhistogram draws it. # name: # type: sq_string # elements: 1 # length: 43 stats.chart.ScatterHistogramChart.BinWidths # name: # type: sq_string # elements: 1 # length: 165 stats.chart.ScatterHistogramChart: property BinWidths The width of the bins, shaped as NumBins. Setting it takes precedence over NumBins, which then follows from it. # name: # type: sq_string # elements: 1 # length: 41 The width of the bins, shaped as NumBins. # name: # type: sq_string # elements: 1 # length: 39 stats.chart.ScatterHistogramChart.Color # name: # type: sq_string # elements: 1 # length: 154 stats.chart.ScatterHistogramChart: property Color The colour of each group, one row of an RGB matrix for each; by default the colours of the colour order. # name: # type: sq_string # elements: 1 # length: 104 The colour of each group, one row of an RGB matrix for each; by default the colours of the colour order. # name: # type: sq_string # elements: 1 # length: 42 stats.chart.ScatterHistogramChart.FontName # name: # type: sq_string # elements: 1 # length: 113 stats.chart.ScatterHistogramChart: property FontName The font of every text on the chart, 'Helvetica' by default. # name: # type: sq_string # elements: 1 # length: 60 The font of every text on the chart, 'Helvetica' by default. # name: # type: sq_string # elements: 1 # length: 42 stats.chart.ScatterHistogramChart.FontSize # name: # type: sq_string # elements: 1 # length: 115 stats.chart.ScatterHistogramChart: property FontSize The size of every text on the chart, in points, 10 by default. # name: # type: sq_string # elements: 1 # length: 62 The size of every text on the chart, in points, 10 by default. # name: # type: sq_string # elements: 1 # length: 43 stats.chart.ScatterHistogramChart.GroupData # name: # type: sq_string # elements: 1 # length: 182 stats.chart.ScatterHistogramChart: property GroupData The group of each observation: a vector as long as XData of categories, text, numbers or logical values, or empty for no groups. # name: # type: sq_string # elements: 1 # length: 128 The group of each observation: a vector as long as XData of categories, text, numbers or logical values, or empty for no groups. # name: # type: sq_string # elements: 1 # length: 47 stats.chart.ScatterHistogramChart.GroupVariable # name: # type: sq_string # elements: 1 # length: 114 stats.chart.ScatterHistogramChart: property GroupVariable The variable of SourceTable holding GroupData, or empty. # name: # type: sq_string # elements: 1 # length: 56 The variable of SourceTable holding GroupData, or empty. # name: # type: sq_string # elements: 1 # length: 55 stats.chart.ScatterHistogramChart.HistogramDisplayStyle # name: # type: sq_string # elements: 1 # length: 216 stats.chart.ScatterHistogramChart: property HistogramDisplayStyle How the histograms are drawn: 'stairs', the default, as their outline; 'bar' as bars; 'smooth' as a kernel density estimate, as ksdensity computes it. # name: # type: sq_string # elements: 1 # length: 150 How the histograms are drawn: 'stairs', the default, as their outline; 'bar' as bars; 'smooth' as a kernel density estimate, as ksdensity computes it. # name: # type: sq_string # elements: 1 # length: 47 stats.chart.ScatterHistogramChart.InnerPosition # name: # type: sq_string # elements: 1 # length: 79 stats.chart.ScatterHistogramChart: property InnerPosition The same as Position. # name: # type: sq_string # elements: 1 # length: 21 The same as Position. # name: # type: sq_string # elements: 1 # length: 45 stats.chart.ScatterHistogramChart.LegendTitle # name: # type: sq_string # elements: 1 # length: 131 stats.chart.ScatterHistogramChart: property LegendTitle The title of the legend; from a table the name of GroupVariable, until set. # name: # type: sq_string # elements: 1 # length: 75 The title of the legend; from a table the name of GroupVariable, until set. # name: # type: sq_string # elements: 1 # length: 47 stats.chart.ScatterHistogramChart.LegendVisible # name: # type: sq_string # elements: 1 # length: 153 stats.chart.ScatterHistogramChart: property LegendVisible Whether the legend of the groups is shown, 'on' or 'off'; on where there are groups, until set. # name: # type: sq_string # elements: 1 # length: 95 Whether the legend of the groups is shown, 'on' or 'off'; on where there are groups, until set. # name: # type: sq_string # elements: 1 # length: 43 stats.chart.ScatterHistogramChart.LineStyle # name: # type: sq_string # elements: 1 # length: 145 stats.chart.ScatterHistogramChart: property LineStyle The line style of the histograms of each group, a character vector or a cell array of them. # name: # type: sq_string # elements: 1 # length: 91 The line style of the histograms of each group, a character vector or a cell array of them. # name: # type: sq_string # elements: 1 # length: 43 stats.chart.ScatterHistogramChart.LineWidth # name: # type: sq_string # elements: 1 # length: 135 stats.chart.ScatterHistogramChart: property LineWidth The width of the histogram lines, a scalar or one for each group, 0.5 by default. # name: # type: sq_string # elements: 1 # length: 81 The width of the histogram lines, a scalar or one for each group, 0.5 by default. # name: # type: sq_string # elements: 1 # length: 45 stats.chart.ScatterHistogramChart.MarkerAlpha # name: # type: sq_string # elements: 1 # length: 110 stats.chart.ScatterHistogramChart: property MarkerAlpha How opaque the markers are, from 0 to 1, 1 by default. # name: # type: sq_string # elements: 1 # length: 54 How opaque the markers are, from 0 to 1, 1 by default. # name: # type: sq_string # elements: 1 # length: 46 stats.chart.ScatterHistogramChart.MarkerFilled # name: # type: sq_string # elements: 1 # length: 114 stats.chart.ScatterHistogramChart: property MarkerFilled Whether the markers are filled, 'on' by default or 'off'. # name: # type: sq_string # elements: 1 # length: 57 Whether the markers are filled, 'on' by default or 'off'. # name: # type: sq_string # elements: 1 # length: 44 stats.chart.ScatterHistogramChart.MarkerSize # name: # type: sq_string # elements: 1 # length: 143 stats.chart.ScatterHistogramChart: property MarkerSize The area of the markers in square points, a scalar or one for each group, 36 by default. # name: # type: sq_string # elements: 1 # length: 88 The area of the markers in square points, a scalar or one for each group, 36 by default. # name: # type: sq_string # elements: 1 # length: 45 stats.chart.ScatterHistogramChart.MarkerStyle # name: # type: sq_string # elements: 1 # length: 141 stats.chart.ScatterHistogramChart: property MarkerStyle The marker of each group, a character vector or a cell array of them, 'o' by default. # name: # type: sq_string # elements: 1 # length: 85 The marker of each group, a character vector or a cell array of them, 'o' by default. # name: # type: sq_string # elements: 1 # length: 41 stats.chart.ScatterHistogramChart.NumBins # name: # type: sq_string # elements: 1 # length: 281 stats.chart.ScatterHistogramChart: property NumBins The number of bins of each histogram, a 2-by-1 vector for the horizontal and the vertical variable, or a 2-by-N matrix with a column for each of N groups. Setting a scalar sets both. Empty where HistogramDisplayStyle is 'smooth'. # name: # type: sq_string # elements: 1 # length: 154 The number of bins of each histogram, a 2-by-1 vector for the horizontal and the vertical variable, or a 2-by-N matrix with a column for each of N groups. # name: # type: sq_string # elements: 1 # length: 47 stats.chart.ScatterHistogramChart.OuterPosition # name: # type: sq_string # elements: 1 # length: 202 stats.chart.ScatterHistogramChart: property OuterPosition The position of the whole chart in its parent, labels included, [0, 0, 1, 1] by default, or the outer position of the axes it took the place of. # name: # type: sq_string # elements: 1 # length: 144 The position of the whole chart in its parent, labels included, [0, 0, 1, 1] by default, or the outer position of the axes it took the place of. # name: # type: sq_string # elements: 1 # length: 40 stats.chart.ScatterHistogramChart.Parent # name: # type: sq_string # elements: 1 # length: 122 stats.chart.ScatterHistogramChart: property Parent The figure or panel the chart is placed in. This property is read-only. # name: # type: sq_string # elements: 1 # length: 43 The figure or panel the chart is placed in. # name: # type: sq_string # elements: 1 # length: 42 stats.chart.ScatterHistogramChart.Position # name: # type: sq_string # elements: 1 # length: 352 stats.chart.ScatterHistogramChart: property Position The position of the scatter plot in its parent, as [left, bottom, width, height] in Units. While PositionConstraint is 'outerposition' the chart places it within OuterPosition itself; setting it fixes the scatter plot there and turns PositionConstraint to 'innerposition'. InnerPosition is the same. # name: # type: sq_string # elements: 1 # length: 90 The position of the scatter plot in its parent, as [left, bottom, width, height] in Units. # name: # type: sq_string # elements: 1 # length: 52 stats.chart.ScatterHistogramChart.PositionConstraint # name: # type: sq_string # elements: 1 # length: 144 stats.chart.ScatterHistogramChart: property PositionConstraint Which position the chart keeps: 'outerposition', the default, or 'innerposition'. # name: # type: sq_string # elements: 1 # length: 81 Which position the chart keeps: 'outerposition', the default, or 'innerposition'. # name: # type: sq_string # elements: 1 # length: 55 stats.chart.ScatterHistogramChart.ScatterHistogramChart # name: # type: sq_string # elements: 1 # length: 594 stats.chart.ScatterHistogramChart: obj = stats.chart.ScatterHistogramChart (parent, spec, args) Create a stats.chart.ScatterHistogramChart object. parent is the figure or panel to place the chart in, or empty for the current figure, which is resolved only once every value has been accepted. spec is a structure carrying the data as scatterhistogram resolved it, with the fields XData, YData, GroupData, SourceTable, XVariable, YVariable and GroupVariable, and args the name-value pairs left to set. The documented way to reach this constructor is scatterhistogram. See also: scatterhistogram # name: # type: sq_string # elements: 1 # length: 50 Create a stats.chart.ScatterHistogramChart object. # name: # type: sq_string # elements: 1 # length: 53 stats.chart.ScatterHistogramChart.ScatterPlotLocation # name: # type: sq_string # elements: 1 # length: 217 stats.chart.ScatterHistogramChart: property ScatterPlotLocation Which corner the scatter plot takes, the histograms lining the two sides away from it: 'SouthWest', the default, 'SouthEast', 'NorthEast' or 'NorthWest'. # name: # type: sq_string # elements: 1 # length: 153 Which corner the scatter plot takes, the histograms lining the two sides away from it: 'SouthWest', the default, 'SouthEast', 'NorthEast' or 'NorthWest'. # name: # type: sq_string # elements: 1 # length: 55 stats.chart.ScatterHistogramChart.ScatterPlotProportion # name: # type: sq_string # elements: 1 # length: 173 stats.chart.ScatterHistogramChart: property ScatterPlotProportion The share of the width and of the height the scatter plot takes, a number between 0 and 1, 0.75 by default. # name: # type: sq_string # elements: 1 # length: 107 The share of the width and of the height the scatter plot takes, a number between 0 and 1, 0.75 by default. # name: # type: sq_string # elements: 1 # length: 45 stats.chart.ScatterHistogramChart.SourceTable # name: # type: sq_string # elements: 1 # length: 101 stats.chart.ScatterHistogramChart: property SourceTable The table the chart was drawn from, or empty. # name: # type: sq_string # elements: 1 # length: 45 The table the chart was drawn from, or empty. # name: # type: sq_string # elements: 1 # length: 39 stats.chart.ScatterHistogramChart.Title # name: # type: sq_string # elements: 1 # length: 73 stats.chart.ScatterHistogramChart: property Title The title of the chart. # name: # type: sq_string # elements: 1 # length: 23 The title of the chart. # name: # type: sq_string # elements: 1 # length: 39 stats.chart.ScatterHistogramChart.Units # name: # type: sq_string # elements: 1 # length: 102 stats.chart.ScatterHistogramChart: property Units The units of the positions, 'normalized' by default. # name: # type: sq_string # elements: 1 # length: 52 The units of the positions, 'normalized' by default. # name: # type: sq_string # elements: 1 # length: 41 stats.chart.ScatterHistogramChart.Visible # name: # type: sq_string # elements: 1 # length: 105 stats.chart.ScatterHistogramChart: property Visible Whether the chart is shown, 'on' by default or 'off'. # name: # type: sq_string # elements: 1 # length: 53 Whether the chart is shown, 'on' by default or 'off'. # name: # type: sq_string # elements: 1 # length: 39 stats.chart.ScatterHistogramChart.XData # name: # type: sq_string # elements: 1 # length: 204 stats.chart.ScatterHistogramChart: property XData The values along the horizontal axis, a numeric column vector. Where the chart was drawn from a table it is read from the table and setting it is refused. # name: # type: sq_string # elements: 1 # length: 62 The values along the horizontal axis, a numeric column vector. # name: # type: sq_string # elements: 1 # length: 53 stats.chart.ScatterHistogramChart.XHistogramDirection # name: # type: sq_string # elements: 1 # length: 144 stats.chart.ScatterHistogramChart: property XHistogramDirection Which way the bars of the histogram of XData rise: 'up', the default, or 'down'. # name: # type: sq_string # elements: 1 # length: 80 Which way the bars of the histogram of XData rise: 'up', the default, or 'down'. # name: # type: sq_string # elements: 1 # length: 40 stats.chart.ScatterHistogramChart.XLabel # name: # type: sq_string # elements: 1 # length: 131 stats.chart.ScatterHistogramChart: property XLabel The label of the horizontal axis; from a table the name of XVariable, until set. # name: # type: sq_string # elements: 1 # length: 80 The label of the horizontal axis; from a table the name of XVariable, until set. # name: # type: sq_string # elements: 1 # length: 41 stats.chart.ScatterHistogramChart.XLimits # name: # type: sq_string # elements: 1 # length: 116 stats.chart.ScatterHistogramChart: property XLimits The limits of the horizontal axis, the range of XData until set. # name: # type: sq_string # elements: 1 # length: 64 The limits of the horizontal axis, the range of XData until set. # name: # type: sq_string # elements: 1 # length: 43 stats.chart.ScatterHistogramChart.XVariable # name: # type: sq_string # elements: 1 # length: 96 stats.chart.ScatterHistogramChart: property XVariable The variable of SourceTable holding XData. # name: # type: sq_string # elements: 1 # length: 42 The variable of SourceTable holding XData. # name: # type: sq_string # elements: 1 # length: 39 stats.chart.ScatterHistogramChart.YData # name: # type: sq_string # elements: 1 # length: 127 stats.chart.ScatterHistogramChart: property YData The values along the vertical axis, a numeric column vector as long as XData. # name: # type: sq_string # elements: 1 # length: 77 The values along the vertical axis, a numeric column vector as long as XData. # name: # type: sq_string # elements: 1 # length: 53 stats.chart.ScatterHistogramChart.YHistogramDirection # name: # type: sq_string # elements: 1 # length: 147 stats.chart.ScatterHistogramChart: property YHistogramDirection Which way the bars of the histogram of YData rise: 'right', the default, or 'left'. # name: # type: sq_string # elements: 1 # length: 83 Which way the bars of the histogram of YData rise: 'right', the default, or 'left'. # name: # type: sq_string # elements: 1 # length: 40 stats.chart.ScatterHistogramChart.YLabel # name: # type: sq_string # elements: 1 # length: 129 stats.chart.ScatterHistogramChart: property YLabel The label of the vertical axis; from a table the name of YVariable, until set. # name: # type: sq_string # elements: 1 # length: 78 The label of the vertical axis; from a table the name of YVariable, until set. # name: # type: sq_string # elements: 1 # length: 41 stats.chart.ScatterHistogramChart.YLimits # name: # type: sq_string # elements: 1 # length: 114 stats.chart.ScatterHistogramChart: property YLimits The limits of the vertical axis, the range of YData until set. # name: # type: sq_string # elements: 1 # length: 62 The limits of the vertical axis, the range of YData until set. # name: # type: sq_string # elements: 1 # length: 43 stats.chart.ScatterHistogramChart.YVariable # name: # type: sq_string # elements: 1 # length: 96 stats.chart.ScatterHistogramChart: property YVariable The variable of SourceTable holding YData. # name: # type: sq_string # elements: 1 # length: 42 The variable of SourceTable holding YData. # name: # type: sq_string # elements: 1 # length: 40 stats.chart.ScatterHistogramChart.delete # name: # type: sq_string # elements: 1 # length: 91 stats.chart.ScatterHistogramChart: delete (h) Delete the chart and the axes it is drawn in. # name: # type: sq_string # elements: 1 # length: 45 Delete the chart and the axes it is drawn in. # name: # type: sq_string # elements: 1 # length: 38 stats.chart.ScatterHistogramChart.xlim # name: # type: sq_string # elements: 1 # length: 186 stats.chart.ScatterHistogramChart: limits = xlim (h) stats.chart.ScatterHistogramChart: xlim (h, limits) The limits of the horizontal axis, as XLimits holds them; given limits, set them. # name: # type: sq_string # elements: 1 # length: 81 The limits of the horizontal axis, as XLimits holds them; given limits, set them. # name: # type: sq_string # elements: 1 # length: 38 stats.chart.ScatterHistogramChart.ylim # name: # type: sq_string # elements: 1 # length: 184 stats.chart.ScatterHistogramChart: limits = ylim (h) stats.chart.ScatterHistogramChart: ylim (h, limits) The limits of the vertical axis, as YLimits holds them; given limits, set them. # name: # type: sq_string # elements: 1 # length: 79 The limits of the vertical axis, as YLimits holds them; given limits, set them. # name: # type: sq_string # elements: 1 # length: 10 swarmchart # name: # type: sq_string # elements: 1 # length: 1702 statistics: swarmchart (x, y) statistics: swarmchart (x, y, sz) statistics: swarmchart (x, y, sz, c) statistics: swarmchart (..., 'filled') statistics: swarmchart (..., name, value) statistics: swarmchart (ax, ...) statistics: s = swarmchart (...) Draw a swarm chart. swarmchart (x, y) draws the points of y against x, spread sideways where they would otherwise sit on top of one another, so that the shape of each column shows where its values gather. sz sets the marker area and c its colour, as they do for scatter, and 'filled' fills the markers. s is the scatter object drawn, and carries XJitter, YJitter, XJitterWidth and YJitterWidth besides everything a scatter object carries. Setting any of them spreads the points again and redraws. Name Value 'XJitter' How the points are spread sideways: 'density', the default here, spreads each point by a uniform draw weighted by how many of its neighbours share its value, so a crowded part of a column spreads wide and a lone point hardly moves; 'rand' spreads every point alike; 'randn' draws the offsets from a normal distribution; 'none' leaves them in a line. 'YJitter' The same, upwards. The default is 'none'. 'XJitterWidth' How far a point may be moved sideways, in the units of the axis. The default is nine tenths of the smallest gap between distinct values of x. 'YJitterWidth' The same, upwards. MATLAB leaves XData as it was given and spreads the points as it draws them. Octave's scatter object has no such step, so this moves the values themselves: XData holds where each point was drawn rather than where it came from. The difference shows in XData and nowhere else, the chart being the same. See also: scatter, boxchart, gscatter # name: # type: sq_string # elements: 1 # length: 19 Draw a swarm chart. # name: # type: sq_string # elements: 1 # length: 6 violin # name: # type: sq_string # elements: 1 # length: 2289 statistics: violin (x) statistics: h = violin (x) statistics: h = violin (..., property, value, ...) statistics: h = violin (hax, ...) statistics: h = violin (..., "horizontal") Produce a Violin plot of the data x. The input data x can be a N-by-m array containing N observations of m variables. It can also be a cell with m elements, for the case in which the variables are not uniformly sampled. The following property can be set using property/value pairs (default values in parenthesis). The value of the property can be a scalar indicating that it applies to all the variables in the data. It can also be a cell/array, indicating the property for each variable. In this case it should have m columns (as many as variables). Color ("y") Indicates the filling color of the violins. Nbins (50) Internally, the function calls hist to compute the histogram of the data. This property indicates how many bins to use. See help hist for more details. SmoothFactor (4) The function performs simple kernel density estimation and automatically finds the bandwidth of the kernel function that best approximates the histogram using optimization (sqp). The result is in general very noisy. To smooth the result the bandwidth is multiplied by the value of this property. The higher the value the smoother the violins, but values too high might remove features from the data distribution. Bandwidth (NA) If this property is given a value other than NA, it sets the bandwidth of the kernel function. No optimization is performed and the property SmoothFactor is ignored. Width (0.5) Sets the maximum width of the violins. Violins are centered at integer axis values. The distance between two violin middle axis is 1. Setting a value higher than 1 in this property will cause the violins to overlap. If the string "Horizontal" is among the input arguments, the violin plot is rendered along the x axis with the variables in the y axis. The returned structure h has handles to the plot elements, allowing customization of the visualization using set/get functions. Example: title ("Grade 3 heights"); axis ([0,3]); set (gca, "xtick", 1:2, "xticklabel", {"girls"; "boys"}); h = violin ({randn(100,1)*5+140, randn(130,1)*8+135}, "Nbins", 10); set (h.violin, "linewidth", 2) See also: boxplot, hist # name: # type: sq_string # elements: 1 # length: 36 Produce a Violin plot of the data x. # name: # type: sq_string # elements: 1 # length: 7 wblplot # name: # type: sq_string # elements: 1 # length: 1582 statistics: wblplot (data, ...) statistics: handle = wblplot (data, ...) statistics: [handle, param] = wblplot (data) statistics: [handle, param] = wblplot (data, censor) statistics: [handle, param] = wblplot (data, censor, freq) statistics: [handle, param] = wblplot (data, censor, freq, confint) statistics: [handle, param] = wblplot (data, censor, freq, confint, fancygrid) statistics: [handle, param] = wblplot (data, censor, freq, confint, fancygrid, showlegend) Plot a column vector data on a Weibull probability plot using rank regression. censor: optional parameter is a column vector of same size as data with 1 for right censored data and 0 for exact observation. Pass [] when no censor data are available. freq: optional vector same size as data with the number of occurrences for corresponding data. Pass [] when no frequency data are available. confint: optional confidence limits for plotting upper and lower confidence bands using beta binomial confidence bounds. If a single value is given this will be used such as LOW = a and HIGH = 1 - a. Pass [] if confidence bounds is not requested. fancygrid: optional parameter which if set to anything but 1 will turn off the fancy gridlines. showlegend: optional parameter that when set to zero(0) turns off the legend. If one output argument is given, a handle for the data marker and plotlines is returned, which can be used for further modification of line and marker style. If a second output argument is specified, a param vector with scale, shape and correlation factor is returned. See also: normplot, wblpdf # name: # type: sq_string # elements: 1 # length: 78 Plot a column vector data on a Weibull probability plot using rank regression. statistics-release-2.1.2/inst/Plotting/ecdfhist.m000066400000000000000000000157031527311104300220530ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{n}, @var{c}] =} ecdfhist (@var{f}, @var{x}) ## @deftypefnx {statistics} {[@var{n}, @var{c}] =} ecdfhist (@var{f}, @var{x}, @var{m}) ## @deftypefnx {statistics} {[@var{n}, @var{c}] =} ecdfhist (@var{f}, @var{x}, @var{centers}) ## @deftypefnx {statistics} {} ecdfhist (@dots{}) ## @deftypefnx {statistics} {} ecdfhist (@var{ax}, @dots{}) ## ## Create a histogram from the output of @code{ecdf}. ## ## @code{[@var{n}, @var{c}] = ecdfhist (@var{f}, @var{x})} takes the empirical ## cumulative distribution function @var{f} evaluated at the points @var{x}, as ## computed by @code{ecdf}, and returns the heights @var{n} of histogram bars ## for 10 equally spaced bins together with their centers @var{c}. Unlike a ## count histogram, the bar heights are normalized so that the area of the ## histogram is equal to 1, giving an estimate of the probability density ## function. ## ## @code{[@var{n}, @var{c}] = ecdfhist (@var{f}, @var{x}, @var{m})} uses @var{m} ## equally spaced bins. ## ## @code{[@var{n}, @var{c}] = ecdfhist (@var{f}, @var{x}, @var{centers})} uses ## bins with the specified centers, given as a vector of monotonically ## increasing values. ## ## @code{ecdfhist (@dots{})} without output arguments plots the histogram. ## ## @code{ecdfhist (@var{ax}, @dots{})} plots into the axes @var{ax} instead of ## the current axes. ## ## The probability mass assigned to each bin is the sum of the increments of the ## empirical cdf, @code{diff (@var{f})}, over the points @var{x} that fall ## closest to the corresponding bin center; ties are assigned to the lower ## center. Each bar height is that mass divided by the bin width. ## ## @seealso{ecdf, cdfplot, hist} ## @end deftypefn function [nout, cout] = ecdfhist (varargin) ## Detect a leading axes handle ax = []; if (numel (varargin) > 0 && isaxes (varargin{1})) ax = varargin{1}; varargin(1) = []; endif if (numel (varargin) < 2 || numel (varargin) > 3) print_usage (); endif f = varargin{1}; x = varargin{2}; if (! isnumeric (f) || ! isreal (f) || ! isnumeric (x) || ! isreal (x)) error ("ecdfhist: F and X must be real numeric vectors."); endif f = f(:); x = x(:); if (numel (f) != numel (x)) error ("ecdfhist: F and X must have the same length."); endif if (numel (f) < 2) error ("ecdfhist: F and X must have at least two elements."); endif ## Probability masses are the cdf increments, located at x(2:end) xm = x(2:end); masses = diff (f); ## Determine the bin centers and widths if (numel (varargin) < 3 || isempty (varargin{3})) m = 10; centers = []; elseif (isscalar (varargin{3})) m = varargin{3}; if (! isnumeric (m) || ! isreal (m) || m < 1 || fix (m) != m) error ("ecdfhist: M must be a positive integer."); endif centers = []; else centers = varargin{3}; if (! isnumeric (centers) || ! isreal (centers) || ! isvector (centers)) error ("ecdfhist: CENTERS must be a real numeric vector."); endif centers = centers(:).'; endif if (isempty (centers)) xlo = min (xm); xhi = max (xm); if (xlo == xhi) xlo -= 0.5; xhi += 0.5; endif edges = linspace (xlo, xhi, m + 1); centers = edges(1:end-1) + diff (edges) / 2; binwidth = diff (edges); elseif (numel (centers) == 1) binwidth = 1; else ec = (centers(1:end-1) + centers(2:end)) / 2; binwidth = diff ([2 * centers(1) - ec(1), ec, 2 * centers(end) - ec(end)]); endif ncnt = numel (centers); ## Assign each mass to the nearest center (ties go to the lower center) [~, bin] = min (abs (xm - centers), [], 2); binmass = accumarray (bin, masses, [ncnt, 1]); heights = binmass.' ./ binwidth; if (nargout == 0) if (isempty (ax)) ax = newplot (); endif bar (ax, centers, heights, 'hist'); else nout = heights; cout = centers; endif endfunction %!demo %! ## Histogram (density estimate) from the empirical cdf of a random sample. %! %! rng (42); %! x = randn (100, 1); %! [f, xx] = ecdf (x); %! ecdfhist (f, xx); %! title ("ecdfhist of a standard normal sample"); %!demo %! ## Compare the empirical density with a finer set of bins. %! %! rande ('state', 42); %! x = exprnd (2, 200, 1); %! [f, xx] = ecdf (x); %! ecdfhist (f, xx, 20); %! title ("ecdfhist of an exponential sample (20 bins)"); ## Test output %!test %! f = [0 0.1 0.3 0.6 0.8 0.9 1.0]'; %! x = [1 1 2 3 4 5 8]'; %! [n, c] = ecdfhist (f, x); %! assert_equal (c, 1.35:0.7:7.65, 1e-12); %! assert_equal (n, [0.1 0.2 0.3 0 0.2 0.1 0 0 0 0.1] / 0.7, 1e-12); %! assert_equal (sum (n .* 0.7), 1, 1e-12); %!test %! f = [0 0.1 0.3 0.6 0.8 0.9 1.0]'; %! x = [1 1 2 3 4 5 8]'; %! [n, c] = ecdfhist (f, x, 5); %! assert_equal (c, [1.7 3.1 4.5 5.9 7.3], 1e-12); %! assert_equal (n, [0.3 0.3 0.3 0 0.1] / 1.4, 1e-12); %!test %! f = [0 0.1 0.3 0.6 0.8 0.9 1.0]'; %! x = [1 1 2 3 4 5 8]'; %! [n, c] = ecdfhist (f, x, [1.5 2.5 3.5 4.5 5.5 6.5 7.5]); %! assert_equal (c, [1.5 2.5 3.5 4.5 5.5 6.5 7.5], 1e-12); %! assert_equal (n, [0.3 0.3 0.2 0.1 0 0 0.1], 1e-12); %!test # returns only the heights when a single output is requested %! f = [0 0.1 0.3 0.6 0.8 0.9 1.0]'; %! x = [1 1 2 3 4 5 8]'; %! n = ecdfhist (f, x, 5); %! assert_equal (n, [0.3 0.3 0.3 0 0.1] / 1.4, 1e-12); ## Test plotting %!test %! hf = figure ("visible", "off"); %! unwind_protect %! f = [0 0.1 0.3 0.6 0.8 0.9 1.0]'; %! x = [1 1 2 3 4 5 8]'; %! ecdfhist (f, x); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ("visible", "off"); %! unwind_protect %! ax = axes ("parent", hf); %! f = [0 0.1 0.3 0.6 0.8 0.9 1.0]'; %! x = [1 1 2 3 4 5 8]'; %! ecdfhist (ax, f, x); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error ecdfhist () %!error ecdfhist ([0 1]) %!error ecdfhist ([0 1], {1 2}) %!error ecdfhist ([0 1], [1 2 3]) %!error ecdfhist (1, 1) %!error ecdfhist ([0 1], [1 2], 0) %!error ecdfhist ([0 1], [1 2], 2.5) statistics-release-2.1.2/inst/Plotting/einstein.m000066400000000000000000000225641527311104300221030ustar00rootroot00000000000000## Copyright (C) 2023 Arun Giridhar ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} einstein () ## @deftypefnx {statistics} {@var{tiles} =} einstein (@var{a}, @var{b}) ## @deftypefnx {statistics} {[@var{tiles}, @var{rhat}] =} einstein (@var{a}, @var{b}) ## @deftypefnx {statistics} {[@var{tiles}, @var{rhat}, @var{that}] =} einstein (@var{a}, @var{b}) ## @deftypefnx {statistics} {[@var{tiles}, @var{rhat}, @var{that}, @var{shat}] =} einstein (@var{a}, @var{b}) ## @deftypefnx {statistics} {[@var{tiles}, @var{rhat}, @var{that}, @var{shat}, @var{phat}] =} einstein (@var{a}, @var{b}) ## @deftypefnx {statistics} {[@var{tiles}, @var{rhat}, @var{that}, @var{shat}, @var{phat}, @var{fhat}] =} einstein (@var{a}, @var{b}) ## ## Plots the tiling of the basic clusters of einstein tiles. ## ## Scalars @var{a} and @var{b} define the shape of the einstein tile. ## See Smith et al (2023) for details: @url{https://arxiv.org/abs/2303.10798} ## ## @itemize ## @item @var{tiles} is a structure containing the coordinates of the einstein ## tiles that are tiled on the plot. Each field contains the tile coordinates ## of the corresponding clusters. ## @itemize ## @item @var{tiles}@qcode{.rhat} contains the reflected einstein tiles ## @item @var{tiles}@qcode{.that} contains the three-hat shells ## @item @var{tiles}@qcode{.shat} contains the single-hat clusters ## @item @var{tiles}@qcode{.phat} contains the paired-hat clusters ## @item @var{tiles}@qcode{.fhat} contains the fylfot clusters ## @end itemize ## ## @item @var{rhat} contains the coordinates of the first reflected tile ## @item @var{that} contains the coordinates of the first three-hat shell ## @item @var{shat} contains the coordinates of the first single-hat cluster ## @item @var{phat} contains the coordinates of the first paired-hat cluster ## @item @var{fhat} contains the coordinates of the first fylfot cluster ## @end itemize ## ## @end deftypefn function [varargout] = einstein (a, b, varargin) ## Check for valid number of input arguments if (nargin < 2) print_usage; endif ## Check A and B for valid type and range if (! (isscalar (a) && isscalar (b) && isnumeric (a) && isnumeric (b)... && isreal (a) && isreal (b))) error ("einstein: A and B must real scalars."); endif if (a <= 0 || a >= 1 || b <= 0 || b >= 1) error ("einstein: A and B must be within the open interval (0,1)."); endif ## Get initial hat points single_hat = getpoly (a, b) * rotz (-150)([1,2],[1,2]); ## Make a reflected-hat reflecthat = [-1, 1] .* single_hat; ## Make a three-hat shell cluster three_hat1 = rotatehat (single_hat, -60); three_hat1 = three_hat1 + (reflecthat(1,:) - three_hat1(9,:)); three_hat2 = three_hat1 + (reflecthat(6,:) - three_hat1(11,:)); three_hat3 = rotatehat (three_hat1, 120); three_hat3 = three_hat3 + (reflecthat(5,:) - three_hat1(9,:)); three_hat = [three_hat1, three_hat2, three_hat3]; ## Translate another four-tile cluster translate = three_hat(5,[3,4]) - three_hat(12,[1,2]); all.rhat = translatehat (reflecthat, translate); all.that = translatehat (three_hat, translate); all.rhat = [reflecthat, all.rhat]; all.that = [three_hat, all.that]; ## Rotate and translate another four-tile cluster tmp_3hat = rotatehat (three_hat, 120); tmp_rhat = rotatehat (reflecthat, 120); translate = three_hat(12,[5,6]) - tmp_3hat(5,[3,4]); tmp_3hat = translatehat (tmp_3hat, translate); tmp_rhat = translatehat (tmp_rhat, translate); all.that = [all.that, tmp_3hat]; all.rhat = [all.rhat, tmp_rhat]; ## Plot four-tile clusters patch (all.that(:,[1:2:end]), all.that(:,[2:2:end]), 'LineWidth', 2, ... 'FaceColor', 'c', 'EdgeColor','k'); patch (all.rhat(:,[1:2:end]), all.rhat(:,[2:2:end]), 'LineWidth', 2, ... 'FaceColor', 'b', 'EdgeColor','k'); title (sprintf ("a = %4.2f b = %4.2f", a, b), 'FontSize',30) ## Make a single-hat cluster translate = three_hat(10,[3,4]) - single_hat(2,:); singlehat = translatehat (single_hat, translate); all.shat = singlehat; ## Plot single-tile cluster patch (all.shat(:,1), all.shat(:,2), 'LineWidth', 2, ... 'FaceColor', [0.95, 0.95, 0.95], 'EdgeColor','k'); axis ('equal') ## Make a paired-hat cluster paired_hat1 = all.shat + (all.rhat(9,[5,6]) - all.shat(5,:)); paired_hat2 = three_hat(:,[3,4]) + (three_hat(1,[5,6]) - three_hat(3,[3,4])); paired_hat = [paired_hat1, paired_hat2]; ## Rotate and translate another two paired-hat clusters tmp_phat = rotatehat (paired_hat, -120); translate = three_hat(4,[3,4]) - tmp_phat(4,[3,4]); tmp_phat = translatehat (tmp_phat, translate); all.phat = [paired_hat, tmp_phat]; tmp_phat = rotatehat (paired_hat, -60); translate = all.that(10,[11,12]) - tmp_phat(11,[3,4]); tmp_phat = translatehat (tmp_phat, translate); all.phat = [all.phat, tmp_phat]; ## Plot paired-tiles clusters patch (all.phat(:,[1:2:end]), all.phat(:,[2:2:end]), 'LineWidth', 2, ... 'FaceColor', [0.9, 0.9, 0.9], 'EdgeColor','k'); ## Make a fylfot cluster fylfot_hat = paired_hat; tmp_fhat = rotatehat (paired_hat, -120); translate = fylfot_hat(1,[3,4]) - tmp_fhat(3,[3,4]); tmp_fhat = translatehat (tmp_fhat, translate); fylfot_hat = [fylfot_hat, tmp_fhat]; tmp_fhat = rotatehat (paired_hat, 120); translate = fylfot_hat(3,[3,4]) - tmp_fhat(1,[3,4]); tmp_fhat = translatehat (tmp_fhat, translate); fylfot_hat = [fylfot_hat, tmp_fhat]; translate = all.that(13,[13,14]) - fylfot_hat(13,[7,8]); fylfot_hat = translatehat (fylfot_hat, translate); ## Translate another two fylfot clusters translate = all.that(4,[9,10]) - fylfot_hat(13,[3,4]); tmp_fhat = translatehat (fylfot_hat, translate); all.fhat = [fylfot_hat, tmp_fhat]; translate = all.that(13,[1,2]) - fylfot_hat(4,[3,4]); tmp_fhat = translatehat (fylfot_hat, translate); all.fhat = [all.fhat, tmp_fhat]; ## Plot fylfot clusters patch (all.fhat(:,[1:2:end]), all.fhat(:,[2:2:end]), 'LineWidth', 2, ... 'FaceColor', 'r', 'EdgeColor','k'); if (nargout > 0) varargout{1} = all; endif if (nargout > 1) varargout{2} = reflecthat; endif if (nargout > 2) varargout{3} = three_hat; endif if (nargout > 3) varargout{4} = singlehat; endif if (nargout > 4) varargout{5} = paired_hat; endif if (nargout > 5) varargout{6} = fylfot_hat; endif endfunction ## Rotates a cluster of hats. function newhat = rotatehat (hat, degrees) rotM = rotz (degrees)([1,2],[1,2]); newhat = zeros (size (hat)); for i=1:2:columns (hat) newhat(:,[i,i+1]) = hat(:,[i,i+1]) * rotM; endfor endfunction ## Translates a cluster of hats. function newhat = translatehat (hat, dist) nhats = columns (hat) / 2; newhat = hat + repmat (dist, 1, nhats); endfunction ## Returns a unit vector given a direction angle in degrees function ret = u (t) persistent angles = (0:30:330); persistent tbl = [cosd(angles); sind(angles)]'; ret = tbl(angles == mod (t, 360), :); endfunction ## Returns the hat polygon function single_hat = getpoly (a, b) single_hat = zeros (14, 2); pos = 0; single_hat(++pos, :) = [0 0]; t = 270; single_hat(pos+1, :) = single_hat(pos++, :) + a * u (t); t += 60; single_hat(pos+1, :) = single_hat(pos++, :) + a * u (t); t -= 90; single_hat(pos+1, :) = single_hat(pos++, :) + b * u (t); t += 60; single_hat(pos+1, :) = single_hat(pos++, :) + b * u (t); t += 90; single_hat(pos+1, :) = single_hat(pos++, :) + a * u (t); t -= 60; single_hat(pos+1, :) = single_hat(pos++, :) + a * u (t); t += 90; single_hat(pos+1, :) = single_hat(pos++, :) + b * u (t); t -= 60; single_hat(pos+1, :) = single_hat(pos++, :) + b * u (t); t += 90; single_hat(pos+1, :) = single_hat(pos++, :) + a * u (t); t += 60; single_hat(pos+1, :) = single_hat(pos++, :) + a * u (t) * 2; t += 60; single_hat(pos+1, :) = single_hat(pos++, :) + a * u (t); t -= 90; single_hat(pos+1, :) = single_hat(pos++, :) + b * u (t); t += 60; single_hat(pos+1, :) = single_hat(pos++, :) + b * u (t); ## t(14, :) == t(1, :) to within numerical roundoff endfunction %!demo %! einstein (0.4, 0.6) %!demo %! einstein (0.2, 0.5) %!demo %! einstein (0.6, 0.1) ## Test plotting %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! tiles = einstein (0.4, 0.6); %! assert_equal (isstruct (tiles), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error einstein %!error einstein (0.5) %!error einstein (0, 0.9) %!error einstein (0.4, 1) %!error einstein (-0.4, 1) statistics-release-2.1.2/inst/Plotting/gardnerAltmanPlot.m000066400000000000000000000470401527311104300236770ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} gardnerAltmanPlot (@var{X}, @var{Y}) ## @deftypefnx {statistics} {} gardnerAltmanPlot (@var{ax}, @var{X}, @var{Y}) ## @deftypefnx {statistics} {} gardnerAltmanPlot (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {@var{H} =} gardnerAltmanPlot (@dots{}) ## ## Gardner-Altman plot of two samples and the effect size between them. ## ## @code{gardnerAltmanPlot (@var{X}, @var{Y})} draws the observations of the ## samples @var{X} and @var{Y} at the horizontal positions 1 and 2, and the ## difference between their means, with its confidence interval, at position ## 3, measured against a second vertical axis on the right. That axis is ## placed so that its zero is level with the centre of @var{Y} and the effect ## size level with the centre of @var{X}, so the effect reads off the data it ## comes from. @var{X} and @var{Y} are vectors of type double or single, and ## a missing value (@qcode{NaN}) is left out. ## ## Two unpaired samples are drawn as swarms, the points of each spread ## sideways where they crowd, with a black line from the centre of each ## sample to the effect size: solid for @var{X}, dashed for @var{Y}. The ## centre is the median for @qcode{'mediandiff'} and the mean otherwise. ## Paired samples are drawn as a line from each observation of @var{X} to its ## partner in @var{Y}, the pairs that increase, decrease and stay equal each ## in a colour of their own. For @qcode{'cliff'} and @qcode{'kstest'}, whose ## values do not share the scale of the data, the right axis spans the values ## the effect can take, @math{[-1.1, 1.1]} and @math{[-0.1, 1.1]}, and two ## unpaired samples get a reference line at zero instead of the lines from ## their centres. ## ## @code{gardnerAltmanPlot (@var{ax}, @dots{})} draws into the axes @var{ax} ## rather than the current axes. ## ## @code{gardnerAltmanPlot (@dots{}, @var{Name}, @var{Value})} takes the ## options @qcode{'Effect'}, @qcode{'Paired'}, @qcode{'VarianceType'}, ## @qcode{'Alpha'}, @qcode{'ConfidenceIntervalType'}, @qcode{'NumBootstraps'}, ## @qcode{'BootstrapOptions'} and @qcode{'Resampling'} of ## @code{meanEffectSize}, which computes the effect size and its interval; ## @qcode{'Effect'} names a single effect. @qcode{'Mean'} does not apply, ## there always being two samples. ## ## @code{@var{H} = gardnerAltmanPlot (@dots{})} returns the graphics objects ## drawn, in a row vector. For two unpaired samples they are the two swarms, ## the error bar, and the lines from the centres of @var{X} and @var{Y}, or ## the reference line. For paired samples they are the lines of the pairs ## that increase, decrease and stay equal, each where there is any, and the ## error bar. Where @qcode{'ConfidenceIntervalType'} is @qcode{'none'} the ## error bar is a line holding a single marker. ## ## MATLAB draws the effect size on a second vertical axis of the same axes; ## Octave has no such axis, so here it is a second axes laid over the first, ## which follows the first's position and vertical limits. The error bar is ## Octave's @code{errorbar} object, whose bounds are its @qcode{'ldata'} and ## @qcode{'udata'} properties. ## ## Reference: M. J. Gardner and D. G. Altman (1986). Confidence intervals ## rather than P values: estimation rather than hypothesis testing. British ## Medical Journal, 292(6522), 746-750. ## ## @seealso{meanEffectSize, swarmchart, errorbar} ## @end deftypefn function varargout = gardnerAltmanPlot (varargin) ## Input validation ax = []; if (numel (varargin) > 0 && isscalar (varargin{1}) && ishghandle (varargin{1}) && strcmp (get (varargin{1}, 'type'), 'axes')) ax = varargin{1}; varargin(1) = []; endif if (numel (varargin) < 2 || ischar (varargin{2}) || isa (varargin{2}, 'string')) error ("gardnerAltmanPlot: two samples X and Y are required."); endif X = varargin{1}; Y = varargin{2}; args = varargin(3:end); optNames = {'Effect', 'Paired', 'VarianceType', 'Alpha', ... 'ConfidenceIntervalType', 'NumBootstraps', ... 'BootstrapOptions', 'Resampling'}; dfValues = {'meandiff', false, [], [], [], [], [], []}; [effect, paired, ~, ~, ~, ~, ~, ~, rest] = ... parsePairedArguments (optNames, dfValues, args(:)); if (! isempty (rest)) error ("gardnerAltmanPlot: invalid optional paired argument."); endif if ((iscell (effect) || isa (effect, 'string')) && numel (effect) != 1) error ("gardnerAltmanPlot: 'Effect' must name a single effect."); endif ## The effect size and its interval, the options checked by meanEffectSize ## and its messages raised under this name try T = meanEffectSize (X, Y, args{:}); catch err error ("gardnerAltmanPlot: %s", ... regexprep (err.message, '^meanEffectSize: ', '')); end_try_catch name = T.Properties.RowNames{1}; e = double (T.Effect); noCI = ! any (strcmp (T.Properties.VariableNames, 'ConfidenceIntervals')); if (! noCI) ci = double (T.ConfidenceIntervals); endif ## A switch meanEffectSize accepted: true, false, 0, 1, 'on' or 'off' if (ischar (paired) || isa (paired, 'string')) paired = strcmpi (char (paired), 'on'); else paired = logical (paired); endif ## Missing values left out, of both samples where they are paired x = double (X(:)); y = double (Y(:)); if (paired) keep = ! (isnan (x) | isnan (y)); x = x(keep); y = y(keep); else x = x(! isnan (x)); y = y(! isnan (y)); endif if (strcmp (name, 'MedianDifference')) cx = median (x); cy = median (y); centre = 'Median'; else cx = mean (x); cy = mean (y); centre = 'Mean'; endif bounded = any (strcmp (name, {'CliffsDelta', ... 'KolmogorovSmirnovStatistic'})); ## The data on the left axes if (isempty (ax)) ax = gca (); endif ## The right axes and listeners of an earlier plot into these axes go removeOverlay (ax); ax = newplot (ax); held = ishold (ax); hold (ax, 'on'); co = get (ax, 'colororder'); nc = 0; if (paired) kinds = {y > x, y < x, y == x}; h = []; for k = 1:3 take = kinds{k}; if (any (take)) nc += 1; xd = repmat ([1; 2; NaN], 1, sum (take)); yd = [x(take)'; y(take)'; NaN(1, sum (take))]; h(end+1) = line (ax, xd(:)', yd(:)', 'color', ... co(mod (nc - 1, rows (co)) + 1,:)); endif endfor else h = zeros (1, 2); h(1) = swarmchart (ax, ones (numel (x), 1), x, 36, co(1,:), ... 'XJitterWidth', 0.5, 'DisplayName', 'X'); h(2) = swarmchart (ax, 2 * ones (numel (y), 1), y, 36, co(2,:), ... 'XJitterWidth', 0.5, 'DisplayName', 'Y'); nc = 2; endif ebColor = co(mod (nc, rows (co)) + 1,:); set (ax, 'xlim', [0.4, 3.3], 'xtick', 1:3, ... 'xticklabel', {'X', 'Y', effectLabel(name)}, 'box', 'on'); title (ax, 'Gardner-Altman Plot'); ## The effect on a second axes laid over the first, with the axis on the ## right; MATLAB puts it on a second axis of the same axes ax2 = axes ('parent', get (ax, 'parent'), 'position', ... get (ax, 'position'), 'color', 'none', 'yaxislocation', ... 'right', 'xlim', [0.4, 3.3], 'xtick', [], 'box', 'off', ... 'ycolor', ebColor); hold (ax2, 'on'); if (noCI) eb = line (ax2, 3, e, 'marker', 'square', 'linestyle', 'none', ... 'color', ebColor, 'displayname', effectLabel (name)); else eb = errorbar (ax2, 3, e, e - ci(1), ci(2) - e); set (eb, 'marker', 'square', 'color', ebColor, 'displayname', ... effectLabel (name)); endif hold (ax2, 'off'); h(end+1) = eb; ## The lines from each centre to the effect, or the reference line if (! paired) if (bounded) h(end+1) = line (ax2, [2.5, 3.5], [0, 0], 'color', [0, 0, 0]); else h(end+1) = line (ax, [1, 3], [cx, cx], 'color', [0, 0, 0], ... 'marker', '*', 'displayname', ['X ', centre]); h(end+1) = line (ax, [2, 3], [cy, cy], 'color', [0, 0, 0], ... 'marker', '*', 'linestyle', '--', ... 'displayname', ['Y ', centre]); endif endif if (! held) hold (ax, 'off'); endif ## The right axis follows the left one: its limits, unless they are ## fixed, and its position if (bounded) k = []; if (strcmp (name, 'CliffsDelta')) set (ax2, 'ylim', [-1.1, 1.1]); else set (ax2, 'ylim', [-0.1, 1.1]); endif else k = e / (cx - cy); if (! isfinite (k) || k == 0) k = 1; endif endif onYlim = @(~, ~) followLimits (ax, ax2, cy, k); onPosition = @(~, ~) followPosition (ax, ax2); onYlim (); addlistener (ax, 'ylim', onYlim); addlistener (ax, 'position', onPosition); setappdata (ax, 'gardnerAltmanPlot', struct ('axes', ax2, 'ylim', ... onYlim, 'position', onPosition)); ## The right axes lives as long as the data on the left: clearing or ## deleting the left axes deletes that data, and with it the right axes set (h(1), 'deletefcn', @(~, ~) removeOverlay (ax)); set (ancestor (ax, 'figure'), 'currentaxes', ax); ## The handles are handed back only where they were asked for, so a call ## made for the drawing alone prints nothing if (nargout > 0) varargout{1} = h; endif endfunction ## Delete the right axes laid over the axes AX, and the listeners that keep ## it in step function removeOverlay (ax) if (! ishghandle (ax) || ! isappdata (ax, 'gardnerAltmanPlot')) return; endif old = getappdata (ax, 'gardnerAltmanPlot'); rmappdata (ax, 'gardnerAltmanPlot'); dellistener (ax, 'ylim', old.ylim); dellistener (ax, 'position', old.position); if (ishghandle (old.axes)) delete (old.axes); endif endfunction ## The limits of the right axes AX2 from those of the left axes AX, so that ## zero is level with CY and the scale is K times the data's; none if K is ## empty function followLimits (ax, ax2, cy, k) if (! isempty (k) && ishghandle (ax) && ishghandle (ax2)) set (ax2, 'ylim', (get (ax, 'ylim') - cy) * k); endif endfunction ## The position of the right axes AX2 kept on that of the left axes AX function followPosition (ax, ax2) if (ishghandle (ax) && ishghandle (ax2)) set (ax2, 'position', get (ax, 'position')); endif endfunction ## The label of each effect on the horizontal axis function out = effectLabel (name) switch (name) case 'MeanDifference' out = 'Mean Difference'; case 'CohensD' out = 'Cohen''s d'; case 'GlasssDelta' out = 'Glass''s delta'; case 'CliffsDelta' out = 'Cliff''s Delta'; case 'MedianDifference' out = 'Median Difference'; case 'RobustCohensD' out = 'Robust Cohen''s d'; case 'AKPCohensD' out = 'AKP Cohen''s d'; case 'KolmogorovSmirnovStatistic' out = 'Kolmogorov-Smirnov Statistic'; endswitch endfunction %!demo %! ## Petal length of two iris species. The swarms show every flower, and %! ## the error bar on the right the difference between the mean lengths, %! ## read on the right axis, whose zero is level with the mean of the %! ## second species. %! load fisheriris %! x = meas(51:100,3); %! y = meas(101:150,3); %! rng (42); %! gardnerAltmanPlot (x, y); %!demo %! ## The same comparison as Cohen's d, the difference in units of the %! ## pooled standard deviation. The right axis is scaled so that the %! ## effect still sits level with the mean of the first species. %! load fisheriris %! x = meas(51:100,3); %! y = meas(101:150,3); %! rng (42); %! gardnerAltmanPlot (x, y, 'Effect', 'cohen'); %!demo %! ## Paired samples: two exam grades of the same students. Each line joins %! ## one student's two grades, coloured by whether the second is higher, %! ## lower or equal. %! load examgrades %! gardnerAltmanPlot (grades(:,1), grades(:,2), 'Paired', true); %!shared x, y, yp, isScatter %! x = [2.1; 3.4; 1.9; 5.6; 4.4; 3.8; 2.7; 6.1; 3.3; 4.9]; %! y = [1.2; 2.8; 0.9; 2.2; 3.1; 1.7; 2.5; 0.4; 1.9; 3.6; 2.0; 1.1]; %! yp = [1.8; 2.9; 2.2; 4.1; 4.9; 2.6; 3.0; 4.8; 2.1; 4.0]; %! ## Octave's scatter builds an hggroup under the gnuplot toolkit %! isScatter = ! strcmp (graphics_toolkit (), 'gnuplot'); %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! H = gardnerAltmanPlot (x, y); %! assert_equal (size (H), [1, 5]); %! if (isScatter) %! assert_equal (get (H(1:2), 'type'), {'scatter'; 'scatter'}); %! endif %! assert_equal (get (H(3:5), 'type'), {'hggroup'; 'line'; 'line'}); %! assert_equal (get (H(1), 'ydata'), x); %! assert_equal (get (H(2), 'ydata'), y); %! assert_equal (get (H(3), 'xdata'), 3); %! assert_equal (get (H(3), 'ydata'), 1.87, -1e-14); %! assert_equal (get (H(3), 'ldata'), 1.0606770309586725, -1e-13); %! assert_equal (get (H(3), 'udata'), 1.0606770309586709, -1e-13); %! assert_equal (get (H(4), 'xdata'), [1, 3]); %! assert_equal (get (H(4), 'ydata'), [3.82, 3.82], -1e-14); %! assert_equal (get (H(5), 'xdata'), [2, 3]); %! assert_equal (get (H(5), 'ydata'), [1.95, 1.95], -1e-14); %! assert_equal (get (H(5), 'linestyle'), '--'); %! ax = gca (); %! assert_equal (get (ax, 'xticklabel'), {'X'; 'Y'; 'Mean Difference'}); %! assert_equal (get (get (ax, 'title'), 'string'), 'Gardner-Altman Plot'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! H = gardnerAltmanPlot (x, y); %! ax = gca (); %! ax2 = get (H(3), 'parent'); %! assert_equal (get (ax2, 'ylim'), get (ax, 'ylim') - 1.95, -1e-14); %! set (ax, 'ylim', [-2, 9]); %! assert_equal (get (ax2, 'ylim'), [-3.95, 7.05], -1e-14); %! assert_equal (get (ax2, 'position'), get (ax, 'position')); %! assert_equal (get (ax2, 'yaxislocation'), 'right'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! H = gardnerAltmanPlot (x, y, 'Effect', 'cohen'); %! ax = gca (); %! ax2 = get (H(3), 'parent'); %! assert_equal (get (H(3), 'ydata'), 1.51473229411906, -1e-13); %! assert_equal (get (ax2, 'ylim'), (get (ax, 'ylim') - 1.95) ... %! * 1.51473229411906 / 1.87, -1e-13); %! assert_equal (get (ax, 'xticklabel'), {'X'; 'Y'; 'Cohen''s d'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! H = gardnerAltmanPlot (x, y, 'Effect', 'cliff'); %! assert_equal (size (H), [1, 4]); %! assert_equal (get (H(3), 'ydata'), 0.725, -1e-14); %! assert_equal (get (H(4), 'xdata'), [2.5, 3.5]); %! assert_equal (get (H(4), 'ydata'), [0, 0]); %! assert_equal (get (get (H(3), 'parent'), 'ylim'), [-1.1, 1.1]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! rand ('state', 1); %! H = gardnerAltmanPlot (x, y, 'Effect', 'kstest', 'NumBootstraps', 100); %! assert_equal (size (H), [1, 4]); %! assert_equal (get (get (H(3), 'parent'), 'ylim'), [-0.1, 1.1]); %! assert_equal (get (gca (), 'xticklabel'), ... %! {'X'; 'Y'; 'Kolmogorov-Smirnov Statistic'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! rand ('state', 1); %! H = gardnerAltmanPlot (x, y, 'Effect', 'mediandiff', 'NumBootstraps', 100); %! assert_equal (get (H(4), 'ydata'), [3.6, 3.6], -1e-14); %! assert_equal (get (H(5), 'ydata'), [1.95, 1.95], -1e-14); %! assert_equal (get (H(3), 'ydata'), 1.65, -1e-14); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! H = gardnerAltmanPlot (x, yp, 'Paired', true); %! assert_equal (get (H, 'type'), {'line'; 'line'; 'hggroup'}); %! assert_equal (get (H(1), 'xdata'), [1, 2, NaN, 1, 2, NaN, 1, 2, NaN]); %! assert_equal (get (H(1), 'ydata'), ... %! [1.9, 2.2, NaN, 4.4, 4.9, NaN, 2.7, 3, NaN]); %! assert_equal (numel (get (H(2), 'ydata')), 21); %! assert_equal (get (H(3), 'ydata'), 0.58, -1e-13); %! ax2 = get (H(3), 'parent'); %! assert_equal (get (ax2, 'ylim'), get (gca (), 'ylim') - 3.24, -1e-14); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! H = gardnerAltmanPlot ([1; 2; 3; 4], [1; 3; 2; 4], 'Paired', true); %! assert_equal (get (H, 'type'), {'line'; 'line'; 'line'; 'hggroup'}); %! assert_equal (get (H(3), 'ydata'), [1, 1, NaN, 4, 4, NaN]); %! co = get (gca (), 'colororder'); %! assert_equal (get (H(4), 'color'), co(4,:)); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! H = gardnerAltmanPlot ([1; 2; 3; 4], [2; 3; 4; 5], 'Paired', true); %! assert_equal (get (H, 'type'), {'line'; 'hggroup'}); %! co = get (gca (), 'colororder'); %! assert_equal (get (H(2), 'color'), co(2,:)); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! H = gardnerAltmanPlot (x, y, 'ConfidenceIntervalType', 'none'); %! assert_equal (get (H(3), 'type'), 'line'); %! assert_equal (get (H(3), 'marker'), 'square'); %! assert_equal (get (H(3), 'ydata'), 1.87, -1e-14); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! H = gardnerAltmanPlot (x, [y; NaN]); %! assert_equal (get (H(2), 'ydata'), y); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! ax = subplot (1, 2, 2); %! H = gardnerAltmanPlot (ax, x, y); %! assert_equal (get (H(1), 'parent'), ax); %! assert_equal (get (get (H(3), 'parent'), 'position'), ... %! get (ax, 'position')); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! gardnerAltmanPlot (x, y); %! gardnerAltmanPlot (x, yp, 'Paired', true); %! assert_equal (numel (findobj (hf, 'type', 'axes')), 2); %! plot (1:3); %! assert_equal (numel (findobj (hf, 'type', 'axes')), 1); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!error ... %! gardnerAltmanPlot ([1; 2; 3]) %!error ... %! gardnerAltmanPlot ([1; 2; 3], 'Effect', 'cohen') %!error ... %! gardnerAltmanPlot ([1; 2; 3], [2; 3; 4], 'Mean', 3) %!error ... %! gardnerAltmanPlot ([1; 2; 3], [2; 3; 4], 'Effect', {'cohen', 'glass'}) %!error ... %! gardnerAltmanPlot ([1, 2; 3, 4], [2; 3; 4]) %!error ... %! gardnerAltmanPlot ([1; 2; 3], [2; 3; 4], 'Paired', true, 'Effect', 'glass') statistics-release-2.1.2/inst/Plotting/glyphplot.m000066400000000000000000000210421527311104300222750ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} glyphplot (@var{x}) ## @deftypefnx {statistics} {} glyphplot (@var{x}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {statistics} {@var{g} =} glyphplot (@dots{}) ## ## Create a star (glyph) plot of the multivariate data in @var{x}. ## ## @code{glyphplot (@var{x})} draws each observation (row) of the ## @code{n}-by-@code{p} matrix @var{x} as a star glyph, arranged on a grid. The ## @var{p} spokes of each star radiate from its center at equally spaced angles, ## with lengths proportional to the values of the @var{p} variables; the tips of ## the spokes are joined to form the star perimeter. ## ## The following name/value pairs are accepted: ## ## @table @asis ## @item @qcode{"Glyph"} ## @qcode{"star"} (default) draws star glyphs. @qcode{"face"} (Chernoff faces) ## is not currently supported. ## ## @item @qcode{"Standardize"} ## How the columns of @var{x} are scaled to spoke lengths: @qcode{"column"} ## (default) scales each column to the range @math{[0,1]}, @qcode{"matrix"} ## scales the whole matrix to @math{[0,1]}, @qcode{"PCA"} uses principal ## component scores scaled to @math{[0,1]}, and @qcode{"off"} uses the values as ## given. A spoke of relative length 0 is still drawn at 10% of the maximum ## radius so that it remains visible. ## ## @item @qcode{"Grid"} ## A two-element vector @code{[rows cols]} specifying the layout of the glyphs. ## The default is chosen automatically. ## ## @item @qcode{"Centers"} ## An @code{n}-by-2 matrix giving the center coordinates of the glyphs ## explicitly, overriding @qcode{"Grid"}. ## ## @item @qcode{"Radius"} ## The maximum glyph radius (default 0.4). ## ## @item @qcode{"ObsLabels"} ## A character array or cell array of strings labeling the observations. The ## default is the observation numbers. ## @end table ## ## The optional output @var{g} is an @code{n}-by-3 matrix of handles whose ## columns hold, respectively, the star perimeters, the star spokes, and the ## text labels. ## ## @seealso{andrewsplot, parallelcoords} ## @end deftypefn function g = glyphplot (varargin) ## Optional leading figure handle parent = []; if (numel (varargin) > 0 && isscalar (varargin{1}) && ishghandle (varargin{1}) && strcmp (get (varargin{1}, "type"), "figure")) parent = varargin{1}; varargin(1) = []; endif if (numel (varargin) < 1) print_usage (); endif X = varargin{1}; varargin(1) = []; if (! isnumeric (X) || ! isreal (X) || ndims (X) > 2) error ("glyphplot: X must be a real numeric matrix."); endif n = rows (X); p = columns (X); ## Parse name/value options glyph = "star"; standardize = "column"; grid = []; centers = []; maxr = 0.4; obslabels = {}; if (mod (numel (varargin), 2) != 0) error ("glyphplot: name/value arguments must come in pairs."); endif for i = 1:2:numel (varargin) name = varargin{i}; value = varargin{i+1}; if (! ischar (name)) error ("glyphplot: property names must be strings."); endif switch (lower (name)) case "glyph" glyph = value; case "standardize" standardize = value; case "grid" grid = value; case "centers" centers = value; case "radius" maxr = value; case "obslabels" obslabels = value; otherwise error ("glyphplot: unknown property '%s'.", name); endswitch endfor if (strcmpi (glyph, "face")) error ("glyphplot: 'face' (Chernoff) glyphs are not yet supported."); elseif (! strcmpi (glyph, "star")) error ("glyphplot: Glyph must be 'star' or 'face'."); endif ## Scale the columns to spoke lengths in [0,1] switch (lower (standardize)) case "column" S = normalize01 (X, 1); case "matrix" S = normalize01 (X(:), 1); S = reshape (S, size (X)); case "pca" [~, sc] = pca (X); S = normalize01 (sc, 1); p = columns (S); case "off" S = X; otherwise error ("glyphplot: invalid Standardize option '%s'.", standardize); endswitch if (ischar (obslabels)) obslabels = cellstr (obslabels); endif if (isempty (obslabels)) obslabels = arrayfun (@num2str, (1:n)', "UniformOutput", false); endif ## Glyph centers if (! isempty (centers)) if (! isnumeric (centers) || rows (centers) != n || columns (centers) != 2) error ("glyphplot: Centers must be an n-by-2 numeric matrix."); endif cxy = centers; nrows = max (centers(:,2)); else if (isempty (grid)) ncols = ceil (sqrt (n)); nrows = ceil (n / ncols); else nrows = grid(1); ncols = grid(2); endif cxy = zeros (n, 2); for i = 1:n cxy(i,1) = mod (i - 1, ncols) + 1; cxy(i,2) = nrows - floor ((i - 1) / ncols); endfor endif hax = newplot (); old_hold = ishold (hax); hold (hax, "on"); ang = 2 * pi * (0:p-1) / p; g = zeros (n, 3); for i = 1:n cx = cxy(i,1); cy = cxy(i,2); r = maxr * (0.1 + 0.9 * S(i,:)); tx = cx + r .* cos (ang); ty = cy + r .* sin (ang); ## Perimeter (closed) g(i,1) = line (hax, [tx, tx(1)], [ty, ty(1)], "color", [0 0 1]); set (g(i,1), "userdata", [cx, cy, 1]); ## Spokes from the center to each tip sx = reshape ([cx * ones(1, p); tx; nan(1, p)], 1, []); sy = reshape ([cy * ones(1, p); ty; nan(1, p)], 1, []); g(i,2) = line (hax, sx, sy, "color", [0 0 1]); ## Text label below the glyph g(i,3) = text (hax, cx, cy - maxr * 1.2, obslabels{i}, ... "horizontalalignment", "center", "verticalalignment", "top"); endfor axis (hax, "equal"); axis (hax, "off"); if (! old_hold) hold (hax, "off"); endif if (nargout == 0) clear g; endif endfunction ## Scale to [0,1] along dimension dim (columns). Constant columns map to 0. function S = normalize01 (X, dim) lo = min (X, [], dim); hi = max (X, [], dim); rng = hi - lo; rng(rng == 0) = 1; S = (X - lo) ./ rng; endfunction %!demo %! ## Star plot of the first few cars in the carsmall data set. %! %! load carsmall; %! X = [Acceleration, Cylinders, Displacement, Horsepower, Weight]; %! glyphplot (X(1:9,:), "ObsLabels", cellstr (num2str ((1:9)'))); ## Test output %!test %! hf = figure ("visible", "off"); %! unwind_protect %! X = [1 4 2; 3 2 5; 5 5 1; 2 1 4]; %! g = glyphplot (X); %! assert_equal (size (g), [4, 3]); %! ## star perimeter of observation 1 (column standardize, radius 0.4) %! assert_equal (get (g(1,1), "xdata"), [1.04 0.845 0.935 1.04], 1e-4); %! assert_equal (get (g(1,1), "ydata"), [2 2.2685 1.8874 2], 1e-4); %! assert_equal (get (g(1,1), "userdata"), [1 2 1], 1e-12); %! ## observation 2 %! assert_equal (get (g(2,1), "xdata"), [2.22 1.935 1.8 2.22], 1e-4); %! assert_equal (get (g(2,1), "ydata"), [2 2.1126 1.6536 2], 1e-4); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # Radius option scales the glyph %! hf = figure ("visible", "off"); %! unwind_protect %! X = [1 4 2; 3 2 5; 5 5 1; 2 1 4]; %! g = glyphplot (X, "Radius", 0.8); %! ## perimeter x of obs 1 spoke 1: cx + 0.8*(0.1+0.9*0) = 1 + 0.08 %! assert_equal (get (g(1,1), "xdata")(1), 1.08, 1e-12); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error glyphplot () %!error glyphplot ({1}) %!error ... %! glyphplot (ones (3, 3), "Glyph", "face") %!error ... %! glyphplot (ones (3, 3), "Radius") %!error ... %! glyphplot (ones (3, 3), "bogus", 1) %!error ... %! glyphplot (ones (3, 3), "Standardize", "xxx") %!error ... %! glyphplot (ones (3, 3), "Centers", [1 2]) statistics-release-2.1.2/inst/Plotting/gplotmatrix.m000066400000000000000000000332551527311104300226360ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} gplotmatrix (@var{x}, @var{y}, @var{group}) ## @deftypefnx {statistics} {} gplotmatrix (@var{x}, [], @var{group}) ## @deftypefnx {statistics} {} gplotmatrix (@var{x}, @var{y}, @var{group}, @var{clr}, @var{sym}, @var{siz}) ## @deftypefnx {statistics} {} gplotmatrix (@dots{}, @var{doleg}, @var{dispopt}) ## @deftypefnx {statistics} {} gplotmatrix (@dots{}, @var{doleg}, @var{dispopt}, @var{xnam}, @var{ynam}) ## @deftypefnx {statistics} {} gplotmatrix (@var{parent}, @dots{}) ## @deftypefnx {statistics} {[@var{h}, @var{ax}, @var{bigax}] =} gplotmatrix (@dots{}) ## ## Create a matrix of scatter plots grouped by a categorical variable. ## ## @code{gplotmatrix (@var{x}, @var{y}, @var{group})} creates a matrix of ## scatter plots. Each subplot in the resulting figure is a scatter plot of a ## column of @var{x} against a column of @var{y}. If @var{x} is ## @code{n}-by-@var{p} and @var{y} is @code{n}-by-@var{q}, the resulting figure ## holds a @var{q}-by-@var{p} grid of subplots; the subplot in row @var{i} and ## column @var{j} plots @code{@var{x}(:,j)} on the horizontal axis against ## @code{@var{y}(:,i)} on the vertical axis. Points are grouped and colored ## according to @var{group}, which is a grouping variable (numeric, logical, ## character, string, or cell array of strings) with one entry per row of ## @var{x}. ## ## @code{gplotmatrix (@var{x}, [], @var{group})} is equivalent to ## @code{gplotmatrix (@var{x}, @var{x}, @var{group})} except that the diagonal ## of the @var{p}-by-@var{p} grid is replaced by grouped histograms of the ## columns of @var{x}. ## ## The appearance of the plot is controlled by further positional arguments: ## ## @table @asis ## @item @var{clr} ## Marker colors, given as a character vector of color specifiers (e.g. ## @qcode{"rgb"}) or as a matrix of RGB triplets, one row per group. Colors ## cycle if fewer are supplied than there are groups. ## ## @item @var{sym} ## Marker symbols, given as a character vector (e.g. @qcode{"o+x"}); defaults to ## @qcode{"."}. Symbols cycle if fewer are supplied than there are groups. ## ## @item @var{siz} ## Marker sizes, given as a numeric vector. Sizes cycle if fewer are supplied ## than there are groups. ## ## @item @var{doleg} ## Either @qcode{"on"} (default) to display a legend of the groups or ## @qcode{"off"} to suppress it. ## ## @item @var{dispopt} ## Controls the diagonal of the grid when @var{y} is empty: @qcode{"stairs"} ## (default) for grouped stairstep histograms, @qcode{"hist"} or ## @qcode{"grpbars"} for grouped bar histograms, @qcode{"none"} to leave the ## diagonal empty, or @qcode{"variable"} to write the variable names on the ## diagonal. ## ## @item @var{xnam}, @var{ynam} ## Character vectors or cell arrays of strings giving the names of the columns ## of @var{x} and @var{y}, used to label the outer axes. ## @end table ## ## An optional leading @var{parent} argument (a figure or uipanel handle) ## selects the container for the plot. ## ## The optional outputs are @var{h}, an array of handles to the plotted objects ## with size @var{ny}-by-@var{p}-by-@var{k} (where @var{ny} is the number of ## rows of the grid and @var{k} the number of groups); @var{ax}, the matrix of ## handles to the subplot axes (with an extra row of hidden axes for the ## diagonal histograms); and @var{bigax}, the handle to the invisible enclosing ## axes used for titles and labels. ## ## @seealso{gscatter, plotmatrix, grpstats} ## @end deftypefn function [h, ax, bigax] = gplotmatrix (varargin) ## Optional leading parent (figure or uipanel) handle parent = []; if (numel (varargin) > 0 && isscalar (varargin{1}) && ishghandle (varargin{1}) && any (strcmp (get (varargin{1}, "type"), {"figure", "uipanel"}))) parent = varargin{1}; varargin(1) = []; endif if (numel (varargin) < 1) print_usage (); endif ## Positional arguments X = varargin{1}; Y = []; group = []; clr = []; sym = []; siz = []; doleg = "on"; dispopt = "stairs"; xnam = []; ynam = []; nv = numel (varargin); if (nv >= 2), Y = varargin{2}; endif if (nv >= 3), group = varargin{3}; endif if (nv >= 4), clr = varargin{4}; endif if (nv >= 5), sym = varargin{5}; endif if (nv >= 6), siz = varargin{6}; endif if (nv >= 7 && ! isempty (varargin{7})), doleg = varargin{7}; endif if (nv >= 8 && ! isempty (varargin{8})), dispopt = varargin{8}; endif if (nv >= 9), xnam = varargin{9}; endif if (nv >= 10), ynam = varargin{10}; endif if (nv > 10) error ("gplotmatrix: too many input arguments."); endif if (! isnumeric (X) || ! isreal (X) || ! ismatrix (X) || ndims (X) > 2) error ("gplotmatrix: X must be a real numeric matrix."); endif n = rows (X); p = columns (X); ## Y empty selects the self plot with histograms on the diagonal do_hist = isempty (Y); if (do_hist) Y = X; elseif (! isnumeric (Y) || ! isreal (Y) || ndims (Y) > 2) error ("gplotmatrix: Y must be a real numeric matrix."); elseif (rows (Y) != n) error ("gplotmatrix: X and Y must have the same number of rows."); endif ny = columns (Y); ## Grouping variable if (isempty (group)) gidx = ones (n, 1); gnames = {"1"}; else [gidx, gnames] = grp2idx (group); if (numel (gidx) != n) error ("gplotmatrix: GROUP must have one entry per row of X."); endif endif k = numel (gnames); ## Per-group color, symbol, and size gcol = expand_color (clr, k); gsym = expand_sym (sym, k); gsiz = expand_siz (siz, k); ## Names for the outer axes xnam = name_list (xnam, p); ynam = name_list (ynam, ny); ## Container to draw into if (isempty (parent)) parent = gcf (); endif clf (parent); ## Grid geometry (row 1 at the top, column 1 at the left) Lm = 0.10; Bm = 0.10; Wt = 0.86; Ht = 0.86; gap = 0.015; cw = Wt / p; chh = Ht / ny; ax = zeros (ny, p); histax = zeros (1, p); h = []; glines = []; legax = []; for r = 1:ny for c = 1:p x0 = Lm + (c - 1) * cw; y0 = Bm + (ny - r) * chh; pos = [x0 + gap/2, y0 + gap/2, cw - gap, chh - gap]; a = axes ("parent", parent, "position", pos, "box", "on", ... "nextplot", "add"); ax(r,c) = a; if (do_hist && r == c) ## Diagonal: grouped histogram drawn in an overlay axes ah = axes ("parent", parent, "position", pos, "color", "none", ... "nextplot", "add", "xtick", [], "ytick", []); histax(c) = ah; for l = 1:k h(r,c,l) = draw_diag (ah, X(gidx == l, c), dispopt, ... gcol(l,:), xnam{c}); endfor set (a, "xtick", [], "ytick", []); else cell_lines = zeros (1, k); for l = 1:k idx = (gidx == l); hl = line (a, X(idx, c), Y(idx, r), "linestyle", "none", ... "marker", gsym(l), "markersize", gsiz(l), ... "color", gcol(l,:)); h(r,c,l) = hl; cell_lines(l) = hl; endfor if (isempty (glines)) glines = cell_lines; legax = a; endif endif ## Only the outer edges carry tick labels if (r != ny) set (a, "xticklabel", []); endif if (c != 1) set (a, "yticklabel", []); endif if (r == ny && ! isempty (xnam{c})) xlabel (a, xnam{c}); endif if (c == 1 && ! isempty (ynam{r})) ylabel (a, ynam{r}); endif endfor endfor ## Enclosing invisible axes for titles and overall labels bigax = axes ("parent", parent, "position", [Lm, Bm, Wt, Ht], ... "visible", "off", "xtick", [], "ytick", []); ## Legend of the groups if (strcmpi (doleg, "on") && k > 1 && ! isempty (glines)) warning ("off", "Octave:legend:unimplemented-location", "local"); legend (legax, glines, gnames, "location", "best"); endif if (nargout == 0) clear h ax bigax; elseif (do_hist) ax = [ax; histax]; endif endfunction ## Expand a color specification to a k-by-3 matrix of RGB triplets. function gcol = expand_color (clr, k) if (isempty (clr)) base = lines (k); elseif (ischar (clr)) base = zeros (numel (clr), 3); for i = 1:numel (clr) base(i,:) = char2rgb (clr(i)); endfor elseif (isnumeric (clr) && columns (clr) == 3) base = clr; else error ("gplotmatrix: CLR must be a color string or an n-by-3 RGB matrix."); endif idx = mod (0:k-1, rows (base)) + 1; gcol = base(idx, :); endfunction function rgb = char2rgb (ch) switch (ch) case "r", rgb = [1 0 0]; case "g", rgb = [0 1 0]; case "b", rgb = [0 0 1]; case "c", rgb = [0 1 1]; case "m", rgb = [1 0 1]; case "y", rgb = [1 1 0]; case "k", rgb = [0 0 0]; case "w", rgb = [1 1 1]; otherwise error ("gplotmatrix: unknown color '%s'.", ch); endswitch endfunction ## Expand a marker specification to a k-element character vector. function gsym = expand_sym (sym, k) if (isempty (sym)) sym = "."; elseif (! ischar (sym)) error ("gplotmatrix: SYM must be a character vector of markers."); endif idx = mod (0:k-1, numel (sym)) + 1; gsym = sym(idx); endfunction ## Expand a size specification to a k-element numeric vector. function gsiz = expand_siz (siz, k) if (isempty (siz)) siz = 6; elseif (! isnumeric (siz) || ! isreal (siz)) error ("gplotmatrix: SIZ must be a numeric vector of marker sizes."); endif idx = mod (0:k-1, numel (siz)) + 1; gsiz = siz(idx); endfunction ## Normalize a name specification to a cell array of p strings. function nm = name_list (names, p) if (isempty (names)) nm = repmat ({""}, 1, p); elseif (ischar (names)) nm = cellstr (names).'; elseif (iscellstr (names)) nm = names(:).'; else error ("gplotmatrix: variable names must be strings."); endif if (numel (nm) < p) nm(end+1:p) = {""}; endif endfunction ## Draw one group's contribution to a diagonal histogram cell. function ho = draw_diag (ah, data, dispopt, col, vname) switch (lower (dispopt)) case "none" ho = line (ah, NaN, NaN, "linestyle", "none"); case "variable" ho = text (ah, 0.5, 0.5, vname, "parent", ah, ... "units", "normalized", "horizontalalignment", "center"); otherwise if (isempty (data) || all (isnan (data))) ho = line (ah, NaN, NaN, "linestyle", "none"); return; endif [nn, xx] = hist (data, 10); switch (lower (dispopt)) case {"hist", "grpbars"} ho = bar (ah, xx, nn, 1.0, "facecolor", col, "edgecolor", col); otherwise # "stairs" e = xx(1:end-1) + diff (xx) / 2; xe = [xx(1)-(xx(2)-xx(1))/2, e, xx(end)+(xx(end)-xx(end-1))/2]; ho = stairs (ah, xe, [nn, nn(end)], "color", col); endswitch endswitch endfunction %!demo %! ## Grouped scatter-plot matrix of Fisher's iris measurements. %! %! load fisheriris; %! gplotmatrix (meas, [], species); %!demo %! ## Two sets of variables plotted against each other by group. %! %! load fisheriris; %! gplotmatrix (meas(:,1:2), meas(:,3:4), species); ## Test output shapes and scatter orientation %!test %! hf = figure ("visible", "off"); %! unwind_protect %! X = [10 20; 11 25; 12 21; 13 28; 14 23; 15 29]; %! g = [1 1 1 2 2 2]'; %! [h, ax, bigax] = gplotmatrix (X, [], g); %! assert_equal (size (h), [2, 2, 2]); %! assert_equal (size (ax), [3, 2]); %! assert_equal (isscalar (bigax) && isaxes (bigax), true); %! assert_equal (get (h(1,2,1), "xdata"), [20 25 21]); %! assert_equal (get (h(1,2,1), "ydata"), [10 11 12]); %! assert_equal (get (h(2,1,1), "xdata"), [10 11 12]); %! assert_equal (get (h(2,1,1), "ydata"), [20 25 21]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ("visible", "off"); %! unwind_protect %! X = [10 20; 11 25; 12 21; 13 28; 14 23; 15 29]; %! Y = [100 200 300; 110 250 280; 120 210 260; ... %! 130 280 240; 140 230 220; 150 290 210]; %! g = [1 1 1 2 2 2]'; %! [h, ax] = gplotmatrix (X, Y, g); %! assert_equal (size (h), [3, 2, 2]); %! assert_equal (size (ax), [3, 2]); %! assert_equal (get (h(1,2,1), "xdata"), [20 25 21]); %! assert_equal (get (h(1,2,1), "ydata"), [100 110 120]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # runs without a grouping variable %! hf = figure ("visible", "off"); %! unwind_protect %! [h, ax] = gplotmatrix (randn (20, 3), [], []); %! assert_equal (size (h), [3, 3]); %! assert_equal (size (ax), [4, 3]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error gplotmatrix () %!error gplotmatrix ({1}) %!error ... %! gplotmatrix (ones (5, 2), ones (4, 2), ones (5, 1)) %!error ... %! gplotmatrix (ones (5, 2), [], ones (4, 1)) %!error ... %! gplotmatrix (ones (5, 2), [], ones (5, 1), "r", ".", 6, "on", "hist", ... %! "a", "b", "c") statistics-release-2.1.2/inst/Plotting/gscatter.m000066400000000000000000000203031527311104300220660ustar00rootroot00000000000000## Copyright (C) 2021 Stefano Guidoni ## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} gscatter (@var{x}, @var{y}, @var{g}) ## @deftypefnx {statistics} {} gscatter (@var{x}, @var{y}, @var{g}, @var{clr}, @var{sym}, @var{siz}) ## @deftypefnx {statistics} {} gscatter (@dots{}, @var{doleg}, @var{xnam}, @var{ynam}) ## @deftypefnx {statistics} {@var{h} =} gscatter (@dots{}) ## ## Draw a scatter plot with grouped data. ## ## @code{gscatter} is a utility function to draw a scatter plot of @var{x} and ## @var{y}, according to the groups defined by @var{g}. Input @var{x} and ## @var{y} are numeric vectors of the same size, while @var{g} is either a ## vector of the same size as @var{x} or a character matrix with the same number ## of rows as the size of @var{x}. As a vector @var{g} can be numeric, logical, ## a character array, a string array (not implemented), a cell string or cell ## array. ## ## A number of optional inputs change the appearance of the plot: ## @itemize @bullet ## @item @var{"clr"} ## defines the color for each group; if not enough colors are defined by ## @var{"clr"}, @code{gscatter} cycles through the specified colors. Colors can ## be defined as named colors, as rgb triplets or as indices for the current ## @code{colormap}. The default value is a different color for each group, ## according to the current @code{colormap}. ## ## @item @var{"sym"} ## is a char array of symbols for each group; if not enough symbols are defined ## by @var{"sym"}, @code{gscatter} cycles through the specified symbols. ## ## @item @var{"siz"} ## is a numeric array of sizes for each group; if not enough sizes are defined ## by @var{"siz"}, @code{gscatter} cycles through the specified sizes. ## ## @item @var{"doleg"} ## is a boolean value to show the legend; it can be either @qcode{on} (default) ## or @qcode{off}. ## ## @item @var{"xnam"} ## is a character array, the name for the x axis. ## ## @item @var{"ynam"} ## is a character array, the name for the y axis. ## @end itemize ## ## Output @var{h} is an array of graphics handles to the @code{line} object of ## each group. ## ## @seealso{scatter} ## ## @end deftypefn function h = gscatter (varargin) ## optional axes handle if (isaxes (varargin{1})) ## parameter is an axes handle hax = varargin{1}; varargin = varargin(2:end); nargin--; endif ## check the input parameters if (nargin < 3) print_usage (); endif ## ## necessary parameters ## ## x coordinates if (isvector (varargin{1}) && isnumeric (varargin{1})) x = varargin{1}; n = numel (x); else error ("gscatter: x must be a numeric vector"); endif ## y coordinates if (isvector (varargin{2}) && isnumeric (varargin{2})) if (numel (varargin{2}) == n) y = varargin{2}; else error ("gscatter: x and y must have the same size"); endif else error ("gscatter: y must be a numeric vector"); endif ## groups if (isrow (varargin{3})) varargin{3} = transpose (varargin{3}); endif if (ismatrix (varargin{3}) && ischar (varargin{3})) varargin{3} = cellstr (varargin{3}); # char matrix to cellstr elseif (iscell (varargin{3}) && ! iscellstr (varargin{3})) varargin{3} = cell2mat (varargin{3}); # numeric cell to vector endif if (isvector (varargin{3})) # only numeric vectors or cellstr if (rows (varargin{3}) == n) gv = varargin{3}; if (iscellstr (gv)) g_names = unique (gv); # avoid warning else g_names = unique (gv, 'rows'); endif g_len = numel (g_names); if (iscellstr (g_names)) for i = 1 : g_len g(find (strcmp (gv, g_names{i}))) = i; endfor else for i = 1 : g_len g(find (gv == g_names(i))) = i; endfor endif else error ("gscatter: g must have the same size as x and y"); endif else error (strcat ("gscatter: g must be a numeric or logical or char vector,", " or a cell or cellstr array, or a char matrix")); endif ## ## optional parameters ## ## Note: this parameters are passed as they are to 'line', ## the validity check is delegated to 'line' g_col = lines (g_len); g_size = 6 * ones (g_len, 1); g_sym = repmat ('o', 1, g_len); ## optional parameters for legend and axes labels do_legend = 1; # legend shown by default ## MATLAB compatibility: by default MATLAB uses the variable name as ## label for either axis mygetname = @(x) inputname (1); # to retrieve the name of a variable x_nam = mygetname(varargin{1}); # this should retrieve the name of the var, y_nam = mygetname(varargin{2}); # but it does not work ## parameters are all in fixed positions for i = 4 : nargin switch (i) case 4 ## colours c_list = varargin{4}; if (isrow (c_list)) c_list = transpose (c_list); endif c_list_len = rows (c_list); g_col = repmat (c_list, ceil (g_len / c_list_len)); case {5, 6} ## size and symbols s_list = varargin{i}; s_list_len = length (s_list); g_tmp = repmat (s_list, ceil (g_len / s_list_len)); if (i == 6) g_size = g_tmp; else g_sym = g_tmp; endif case 7 ## legend switch (lower (varargin{7})) case 'on' do_legend = 1; case 'off' do_legend = 0; otherwise error ("gscatter: invalid dolegend parameter '%s'", varargin{7}); endswitch case {8, 9} ## x and y label if (! ischar (varargin{i}) && ! isvector (varargin{i})) error ("gscatter: xnam and ynam must be strings"); endif if (i == 8) x_nam = varargin{8}; else y_nam = varargin{9}; endif endswitch endfor ## scatter plot with grouping if (! exist ('hax', 'var')) hax = gca (); endif ## return value h = []; hold on; for i = 1 : g_len idcs = find (g == i); h(i) = line (hax, x(idcs), y(idcs), 'linestyle', 'none', ... 'markersize', g_size(i), 'color', g_col(i,:), 'marker', g_sym(i)); endfor if (do_legend) if (isnumeric (g_names)) g_names = num2str (g_names); endif warning ("off", 'Octave:legend:unimplemented-location', 'local'); legend (hax, g_names, 'location', 'best'); endif xlabel (hax, x_nam); ylabel (hax, y_nam); hold off; endfunction %!demo %! load fisheriris; %! X = meas(:,3:4); %! cidcs = kmeans (X, 3, 'Replicates', 5); %! gscatter (X(:,1), X(:,2), cidcs, [.75 .75 0; 0 .75 .75; .75 0 .75], 'os^'); %! title ('Fisher''s iris data'); ## Test plotting %!shared visibility_setting %! visibility_setting = get (0, 'DefaultFigureVisible'); %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! load fisheriris; %! X = meas(:,3:4); %! cidcs = kmeans (X, 3, 'Replicates', 5); %! gscatter (X(:,1), X(:,2), cidcs, [.75 .75 0; 0 .75 .75; .75 0 .75], 'os^'); %! title ('Fisher''s iris data'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error gscatter (); %!error gscatter ([1]); %!error gscatter ([1], [2]); %!error gscatter ('abc', [1 2 3], [1]); %!error gscatter ([1 2 3], [1 2], [1]); %!error gscatter ([1 2 3], 'abc', [1]); %!error gscatter ([1 2], [1 2], [1]); %!error gscatter ([1 2], [1 2], [1 2], 'rb', 'so', 12, 'xxx'); statistics-release-2.1.2/inst/Plotting/heatmap.m000066400000000000000000000270721527311104300217030ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} heatmap (@var{cdata}) ## @deftypefnx {statistics} {} heatmap (@var{xvalues}, @var{yvalues}, @var{cdata}) ## @deftypefnx {statistics} {} heatmap (@var{tbl}, @var{xvar}, @var{yvar}) ## @deftypefnx {statistics} {} heatmap (@var{parent}, @dots{}) ## @deftypefnx {statistics} {} heatmap (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {@var{h} =} heatmap (@dots{}) ## ## Heatmap of a matrix, or of the rows of a table counted or aggregated over ## two of its variables. ## ## @code{heatmap (@var{cdata})} draws the matrix @var{cdata} as a grid of ## coloured cells, each carrying its value, with a colour bar beside them. ## The columns are named @qcode{'1'}, @qcode{'2'}, @dots{}, and so are the ## rows. ## ## @code{heatmap (@var{xvalues}, @var{yvalues}, @var{cdata})} names the columns ## with @var{xvalues} and the rows with @var{yvalues}, vectors of distinct ## numbers, text or categories with one element for each column and each row ## of @var{cdata}. ## ## @code{heatmap (@var{tbl}, @var{xvar}, @var{yvar})} makes a column of each ## distinct value of the table variable @var{xvar} and a row of each distinct ## value of @var{yvar}, and colours each cell by the number of rows of ## @var{tbl} falling in it. With @qcode{'ColorVariable'} naming a numeric ## variable, each cell holds the mean of that variable over its rows instead, ## or what @qcode{'ColorMethod'} asks for: @qcode{'count'}, @qcode{'mean'}, ## @qcode{'median'}, @qcode{'sum'}, @qcode{'min'}, @qcode{'max'}, or ## @qcode{'none'} where each cell holds at most one row. The categories of a ## @code{categorical} variable are taken in their order, those no row uses ## included; numbers, text and logical values are sorted. A row whose ## @var{xvar} or @var{yvar} is missing is left out. ## ## @code{heatmap (@var{parent}, @dots{})} places the heatmap in the figure or ## panel @var{parent} rather than the current figure. The heatmap takes the ## place of the current axes of its parent, and is refused where ## @code{hold} is on for them. ## ## @code{heatmap (@dots{}, @var{Name}, @var{Value})} sets properties of the ## heatmap; see @code{stats.chart.HeatmapChart} for all of them. ## ## @code{@var{h} = heatmap (@dots{})} returns the heatmap, a ## @code{stats.chart.HeatmapChart} object, whose methods @code{sortx}, ## @code{sorty}, @code{xlim} and @code{ylim} reorder and restrict its columns ## and rows. ## ## @seealso{stats.chart.HeatmapChart, imagesc, crosstab} ## @end deftypefn function varargout = heatmap (varargin) if (nargin < 1) print_usage (); endif ## Input validation parent = []; args = varargin; if (isscalar (args{1}) && ! istable (args{1}) && isnumeric (args{1}) ... && ishghandle (args{1})) type = get (args{1}, 'type'); if (strcmp (type, 'axes')) error ("heatmap: a heatmap cannot be placed in axes."); elseif (! any (strcmp (type, {'figure', 'uipanel'}))) error ("heatmap: PARENT must be a figure or a panel."); endif parent = args{1}; args(1) = []; endif if (isempty (args)) print_usage (); endif spec = struct ('ColorData', [], 'XData', {{}}, 'YData', {{}}, ... 'SourceTable', [], 'XVariable', '', 'YVariable', '', ... 'ColorVariable', ''); if (istable (args{1})) if (numel (args) < 3 || ! isName (args{2}) || ! isName (args{3})) error ("heatmap: a table must be followed by two variable names."); endif spec.SourceTable = args{1}; spec.XVariable = char (args{2}); spec.YVariable = char (args{3}); rest = args(4:end); else npos = find (cellfun (@isName, args), 1) - 1; if (isempty (npos)) npos = numel (args); endif if (npos == 1) cdata = args{1}; elseif (npos == 3) cdata = args{3}; else error (strcat ("heatmap: the data must be a matrix, two vectors and", ... " a matrix, or a table and two variable names.")); endif if (! ((isnumeric (cdata) || islogical (cdata)) && isreal (cdata) && ndims (cdata) == 2)) error ("heatmap: CDATA must be a 2-dimensional real numeric matrix."); endif spec.ColorData = cdata; if (npos == 3) if (! isvector (args{1}) || numel (args{1}) != columns (cdata)) error (strcat ("heatmap: XVALUES must hold one value for each", ... " column of CDATA.")); endif if (! isvector (args{2}) || numel (args{2}) != rows (cdata)) error (strcat ("heatmap: YVALUES must hold one value for each", ... " row of CDATA.")); endif spec.XData = args{1}; spec.YData = args{2}; endif rest = args(npos+1:end); endif ## Name-value pairs, the names resolved to the properties they set if (mod (numel (rest), 2) != 0) error ("heatmap: optional arguments must be in Name, Value pairs."); endif props = {'ColorData', 'XData', 'YData', 'XDisplayData', 'YDisplayData', ... 'XDisplayLabels', 'YDisplayLabels', 'XLimits', 'YLimits', ... 'SourceTable', 'XVariable', 'YVariable', 'ColorVariable', ... 'ColorMethod', 'ColorScaling', 'ColorLimits', 'Colormap', ... 'ColorbarVisible', 'MissingDataColor', 'MissingDataLabel', ... 'CellLabelFormat', 'CellLabelColor', 'GridVisible', 'Title', ... 'XLabel', 'YLabel', 'FontName', 'FontSize', 'FontColor', ... 'Interpreter', 'Position', 'InnerPosition', 'OuterPosition', ... 'PositionConstraint', ... 'Units', 'Visible'}; for k = 1:2:numel (rest) i = []; if (isName (rest{k})) i = find (strcmpi (char (rest{k}), props), 1); endif if (isempty (i)) error ("heatmap: invalid optional paired argument."); endif rest{k} = props{i}; endfor ## The colour variable belongs with the table, so that the chart reads ## the table once with everything it needs k = find (strcmp (rest(1:2:end), 'ColorVariable'), 1, 'last'); if (! isempty (k)) if (isempty (spec.SourceTable)) error ("heatmap: 'ColorVariable' needs a table."); endif spec.ColorVariable = rest{2*k}; rest(2*k-1:2*k) = []; endif h = stats.chart.HeatmapChart (parent, spec, rest); ## The chart is handed back only where it was asked for if (nargout > 0) varargout{1} = h; endif endfunction ## A name: a character vector or a string scalar function tf = isName (v) tf = (ischar (v) && isrow (v)) || (isa (v, 'string') && isscalar (v)); endfunction %!demo %! ## A matrix as a heatmap: each cell carries its value and its colour. %! heatmap (magic (5)); %!demo %! ## Columns and rows may be named, and the colours changed. %! h = heatmap ({'Mon', 'Tue', 'Wed', 'Thu', 'Fri'}, {'am', 'pm'}, ... %! [12, 15, 9, 20, 18; 7, 11, 14, 6, 9]); %! h.Title = 'Calls answered'; %! h.Colormap = gray (64); %!demo %! ## From a table: the cars of each origin and number of cylinders, and %! ## then their mean mileage. %! load carsmall %! t = table (categorical (cellstr (Origin)), Cylinders, MPG, ... %! 'VariableNames', {'Origin', 'Cylinders', 'MPG'}); %! subplot (1, 2, 1); %! heatmap (t, 'Cylinders', 'Origin'); %! subplot (1, 2, 2); %! heatmap (t, 'Cylinders', 'Origin', 'ColorVariable', 'MPG'); %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! heatmap (magic (3)); %! ax = findall (hf, 'tag', 'stats.chart.HeatmapChart'); %! assert_equal (numel (ax), 1); %! assert_equal (gca (), ax); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = heatmap ({'a', 'b'}, [1, 2, 3], [1, 2; 3, 4; 5, 6], 'Title', 'T', ... %! 'colormap', gray (4)); %! assert_equal (h.XData, {'a'; 'b'}); %! assert_equal (h.YData, {'1'; '2'; '3'}); %! assert_equal (h.Title, 'T'); %! assert_equal (h.Colormap, gray (4)); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! t = table (categorical ({'a';'b';'a'}), categorical ({'u';'u';'v'}), ... %! [1;2;3], 'VariableNames', {'X', 'Y', 'V'}); %! h = heatmap (t, "X", "Y", 'ColorVariable', 'V', 'ColorMethod', 'sum'); %! assert_equal (h.ColorData, [1, 2; 3, 0]); %! assert_equal (h.Title, 'Sum of V'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! plot (1:3); %! pax = gca (); %! h = heatmap (magic (3)); %! assert_equal (ishghandle (pax), false); %! h2 = heatmap (magic (4)); %! assert_equal (numel (findall (hf, 'tag', 'stats.chart.HeatmapChart')), 1); %! assert_equal (isempty (h.Parent), true); %! plot (1:3); %! assert_equal (isempty (h2.Parent), true); %! tag = 'stats.chart.HeatmapChart'; %! assert_equal (isempty (findall (hf, 'tag', tag)), true); %! assert_equal (numel (findall (gca (), 'type', 'line')), 1); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! left = subplot (1, 2, 1); %! plot (1:3); %! subplot (1, 2, 2); %! outer = get (gca (), 'outerposition'); %! h = heatmap (magic (3)); %! assert_equal (h.OuterPosition, outer, -1e-12); %! assert_equal (ishghandle (left), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!error heatmap () %!error ... %! hf = figure ('visible', 'off'); %! unwind_protect %! heatmap (axes (hf), magic (3)); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!error ... %! heatmap (table ([1; 2]), 'Var1') %!error ... %! heatmap ([1, 2], [1, 2]) %!error ... %! heatmap ('abc') %!error ... %! heatmap ({1, 2}) %!error ... %! heatmap (ones (2, 2, 2)) %!error ... %! heatmap ({'a', 'b'}, {'r1'}, [1, 2, 3]) %!error ... %! heatmap ({'a', 'b', 'c'}, {'r1', 'r2'}, [1, 2, 3]) %!error ... %! heatmap (magic (3), 'Title') %!error ... %! heatmap (magic (3), 'Foo', 1) %!error ... %! heatmap (magic (3), 'ColorVariable', 'V') %!error ... %! hf = figure ('visible', 'off'); %! unwind_protect %! plot (1:3); %! hold on; %! heatmap (magic (3)); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect statistics-release-2.1.2/inst/Plotting/hist3.m000066400000000000000000000314671527311104300213210ustar00rootroot00000000000000## Copyright (C) 2015 Carnë Draug ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} hist3 (@var{X}) ## @deftypefnx {statistics} {} hist3 (@var{X}, @var{nbins}) ## @deftypefnx {statistics} {} hist3 (@var{X}, @code{"Nbins"}, @var{nbins}) ## @deftypefnx {statistics} {} hist3 (@var{X}, @var{centers}) ## @deftypefnx {statistics} {} hist3 (@var{X}, @code{"Ctrs"}, @var{centers}) ## @deftypefnx {statistics} {} hist3 (@var{X}, @code{"Edges"}, @var{edges}) ## @deftypefnx {statistics} {[@var{N}, @var{C}] =} hist3 (@dots{}) ## @deftypefnx {statistics} {} hist3 (@dots{}, @var{prop}, @var{val}, @dots{}) ## @deftypefnx {statistics} {} hist3 (@var{hax}, @dots{}) ## ## Produce bivariate (2D) histogram counts or plots. ## ## The elements to produce the histogram are taken from the Nx2 matrix ## @var{X}. Any row with NaN values are ignored. The actual bins can ## be configured in 3 different: number, centers, or edges of the bins: ## ## @table @asis ## @item Number of bins (default) ## Produces equally spaced bins between the minimum and maximum values ## of @var{X}. Defined as a 2 element vector, @var{nbins}, one for each ## dimension. Defaults to @code{[10 10]}. ## ## @item Center of bins ## Defined as a cell array of 2 monotonically increasing vectors, ## @var{centers}. The width of each bin is determined from the adjacent ## values in the vector with the initial and final bin, extending to Infinity. ## ## @item Edge of bins ## Defined as a cell array of 2 monotonically increasing vectors, ## @var{edges}. @code{@var{N}(i,j)} contains the number of elements ## in @var{X} for which: ## ## @itemize @w{} ## @item ## @var{edges}@{1@}(i) <= @var{X}(:,1) < @var{edges}@{1@}(i+1) ## @item ## @var{edges}@{2@}(j) <= @var{X}(:,2) < @var{edges}@{2@}(j+1) ## @end itemize ## ## The consequence of this definition is that values outside the initial ## and final edge values are ignored, and that the final bin only contains ## the number of elements exactly equal to the final edge. ## ## @end table ## ## The return values, @var{N} and @var{C}, are the bin counts and centers ## respectively. These are specially useful to produce intensity maps: ## ## @example ## [counts, centers] = hist3 (data); ## imagesc (centers@{1@}, centers@{2@}, counts) ## @end example ## ## If there is no output argument, or if the axes graphics handle ## @var{hax} is defined, the function will plot a 3 dimensional bar ## graph. Any extra property/value pairs are passed directly to the ## underlying surface object. ## ## @seealso{hist, histc, lookup, mesh} ## @end deftypefn function [N, C] = hist3 (X, varargin) if (nargin < 1) print_usage (); endif next_argin = 1; should_draw = true; if (isaxes (X)) hax = X; X = varargin{next_argin++}; elseif (nargout == 0) hax = gca (); else should_draw = false; endif if (! ismatrix (X) || columns (X) != 2) error ("hist3: X must be a 2 columns matrix"); endif method = 'nbins'; val = [10 10]; if (numel (varargin) >= next_argin) this_arg = varargin{next_argin++}; if (isnumeric (this_arg)) method = 'nbins'; val = this_arg; elseif (iscell (this_arg)) method = 'ctrs'; val = this_arg; elseif (numel (varargin) >= next_argin && any (strcmpi ({'nbins', 'ctrs', 'edges'}, this_arg))) method = tolower (this_arg); val = varargin{next_argin++}; else next_argin--; endif endif have_centers = false; switch (tolower (method)) case 'nbins' [r_edges, c_edges] = edges_from_nbins (X, val); case 'ctrs' have_centers = true; centers = val; [r_edges, c_edges] = edges_from_centers (val); case 'centers' ## This was supported until 1.2.4 when the Matlab compatible option ## 'Ctrs' was added. persistent warned = false; if (! warned) warning ("hist3: option `centers' is deprecated. Use `ctrs'"); endif have_centers = true; centers = val; [r_edges, c_edges] = edges_from_centers (val); case 'edges' if (! iscell (val) || numel (val) != 2 || ! all (cellfun (@isvector, val))) error ("hist3: EDGES must be a cell array with 2 vectors"); endif [r_edges] = vec (val{1}, 2); [c_edges] = vec (val{2}, 2); out_rows = any (X < [r_edges(1) c_edges(1)] | X > [r_edges(end) c_edges(end)], 2); X(out_rows,:) = []; otherwise ## we should never get here... error ("hist3: invalid binning method `%s'", method); endswitch ## We only remove the NaN now, after having computed the bin edges, ## because the extremes from each column that define the edges may ## be paired with a NaN. While such values do not appear on the ## histogram, they must still be used to compute the histogram ## edges. X(any (isnan (X), 2), :) = []; r_idx = lookup (r_edges, X(:,1), 'l'); c_idx = lookup (c_edges, X(:,2), 'l'); counts_size = [numel(r_edges) numel(c_edges)]; counts = accumarray ([r_idx, c_idx], 1, counts_size); if (should_draw) counts = counts.'; z = zeros ((size (counts) +1) *2); z(2:end-1,2:end-1) = kron (counts, ones (2, 2)); ## Setting the values for the end of the histogram bin like this ## seems straight wrong but that's how Matlab plots look. y = [kron(c_edges, ones (1, 2)) (c_edges(end)*2-c_edges(end-1))([1 1])]; x = [kron(r_edges, ones (1, 2)) (r_edges(end)*2-r_edges(end-1))([1 1])]; mesh (hax, x, y, z, 'facecolor', [.75 .85 .95], varargin{next_argin:end}); else N = counts; if (nargout > 1) if (! have_centers) C = {(r_edges + [diff(r_edges)([1:end end])]/ 2) ... (c_edges + [diff(c_edges)([1:end end])]/ 2)}; else C = centers(:)'; C{1} = vec (C{1}, 2); C{2} = vec (C{2}, 2); endif endif endif endfunction function [r_edges, c_edges] = edges_from_nbins (X, nbins) if (! isnumeric (nbins) || numel (nbins) != 2) error ("hist3: NBINS must be a 2 element vector"); endif inits = min (X, [], 1); ends = max (X, [], 1); ends -= (ends - inits) ./ vec (nbins, 2); ## If any histogram side has an empty range, then still make NBINS ## but then place that value at the centre of the centre bin so that ## they appear in the centre in the plot. single_bins = inits == ends; if (any (single_bins)) inits(single_bins) -= (floor (nbins(single_bins) ./2)) + 0.5; ends(single_bins) = inits(single_bins) + nbins(single_bins) -1; endif r_edges = linspace (inits(1), ends(1), nbins(1)); c_edges = linspace (inits(2), ends(2), nbins(2)); endfunction function [r_edges, c_edges] = edges_from_centers (ctrs) if (! iscell (ctrs) || numel (ctrs) != 2 || ! all (cellfun (@isvector, ctrs))) error ("hist3: CTRS must be a cell array with 2 vectors"); endif r_edges = vec (ctrs{1}, 2); c_edges = vec (ctrs{2}, 2); r_edges(2:end) -= diff (r_edges) / 2; c_edges(2:end) -= diff (c_edges) / 2; endfunction %!demo %! X = [ %! 1 1 %! 1 1 %! 1 10 %! 1 10 %! 5 5 %! 5 5 %! 5 5 %! 5 5 %! 5 5 %! 7 3 %! 7 3 %! 7 3 %! 10 10 %! 10 10]; %! hist3 (X) %!test %! N_exp = [ 0 0 0 5 20 %! 0 0 10 15 0 %! 0 15 10 0 0 %! 20 5 0 0 0]; %! %! n = 100; %! x = [1:n]'; %! y = [n:-1:1]'; %! D = [x y]; %! N = hist3 (D, [4 5]); %! assert_equal (N, N_exp); %!test %! N_exp = [0 0 0 0 1 %! 0 0 0 0 1 %! 0 0 0 0 1 %! 1 1 1 1 93]; %! %! n = 100; %! x = [1:n]'; %! y = [n:-1:1]'; %! D = [x y]; %! C{1} = [1 1.7 3 4]; %! C{2} = [1:5]; %! N = hist3 (D, C); %! assert_equal (N, N_exp); ## bug 44987 %!test %! D = [1 1; 3 1; 3 3; 3 1]; %! [c, nn] = hist3 (D, {0:4, 0:4}); %! exp_c = zeros (5); %! exp_c([7 9 19]) = [1 2 1]; %! assert_equal (c, exp_c); %! assert_equal (nn, {0:4, 0:4}); %!test %! for i = 10 %! assert_equal (size (hist3 (rand (9, 2), 'Edges', {[0:.2:1]; [0:.2:1]})), [6 6]) %! endfor %!test %! edge_1 = linspace (0, 10, 10); %! edge_2 = linspace (0, 50, 10); %! [c, nn] = hist3 ([1:10; 1:5:50]', 'Edges', {edge_1, edge_2}); %! exp_c = zeros (10, 10); %! exp_c([1 12 13 24 35 46 57 68 79 90]) = 1; %! assert_equal (c, exp_c); %! %! assert_equal (nn{1}, edge_1 + edge_1(2)/2, eps*10^4) %! assert_equal (nn{2}, edge_2 + edge_2(2)/2, eps*10^4) %!shared X %! X = [ %! 5 2 %! 5 3 %! 1 4 %! 5 3 %! 4 4 %! 1 2 %! 2 3 %! 3 3 %! 5 4 %! 5 3]; %!test %! N = zeros (10); %! N([1 10 53 56 60 91 98 100]) = [1 1 1 1 3 1 1 1]; %! C = {(1.2:0.4:4.8), (2.1:0.2:3.9)}; %! assert_equal (nthargout ([1 2], @hist3, X), {N C}, eps*10^3) %!test %! N = zeros (5, 7); %! N([1 5 17 18 20 31 34 35]) = [1 1 1 1 3 1 1 1]; %! C = {(1.4:0.8:4.6), ((2+(1/7)):(2/7):(4-(1/7)))}; %! assert_equal (nthargout ([1 2], @hist3, X, [5 7]), {N C}, eps*10^3) %! assert_equal (nthargout ([1 2], @hist3, X, 'Nbins', [5 7]), {N C}, eps*10^3) %!test %! N = [0 1 0; 0 1 0; 0 0 1; 0 0 0]; %! C = {(2:5), (2.5:1:4.5)}; %! assert_equal (nthargout ([1 2], @hist3, X, 'Edges', {(1.5:4.5), (2:4)}), {N C}) %!test %! N = [0 0 1 0 1 0; 0 0 0 1 0 0; 0 0 1 4 2 0]; %! C = {(1.2:3.2), (0:5)}; %! assert_equal (nthargout ([1 2], @hist3, X, 'Ctrs', C), {N C}) %! assert_equal (nthargout ([1 2], @hist3, X, C), {N C}) %!test %! [~, C] = hist3 (rand (10, 2), 'Edges', {[0 .05 .15 .35 .55 .95], %! [-1 .05 .07 .2 .3 .5 .89 1.2]}); %! C_exp = {[ 0.025 0.1 0.25 0.45 0.75 1.15], ... %! [-0.475 0.06 0.135 0.25 0.4 0.695 1.045 1.355]}; %! assert_equal (C, C_exp, eps*10^2) ## Test how handling of out of borders is different whether we are ## defining Centers or Edges. %!test %! Xv = repmat ([1:10]', [1 2]); %! %! ## Test Centers %! assert_equal (hist3 (Xv, 'Ctrs', {1:10, 1:10}), eye (10)) %! %! N_exp = eye (6); %! N_exp([1 end]) = 3; %! assert_equal (hist3 (Xv, 'Ctrs', {3:8, 3:8}), N_exp) %! %! N_exp = zeros (8, 6); %! N_exp([1 2 11 20 29 38 47 48]) = [2 1 1 1 1 1 1 2]; %! assert_equal (hist3 (Xv, 'Ctrs', {2:9, 3:8}), N_exp) %! %! ## Test Edges %! assert_equal (hist3 (Xv, 'Edges', {1:10, 1:10}), eye (10)) %! assert_equal (hist3 (Xv, 'Edges', {3:8, 3:8}), eye (6)) %! assert_equal (hist3 (Xv, 'Edges', {2:9, 3:8}), [zeros(1, 6); eye(6); zeros(1, 6)]) %! %! N_exp = zeros (14); %! N_exp(3:12, 3:12) = eye (10); %! assert_equal (hist3 (Xv, 'Edges', {-1:12, -1:12}), N_exp) %! %! ## Test for Nbins %! assert_equal (hist3 (Xv), eye (10)) %! assert_equal (hist3 (Xv, [10 10]), eye (10)) %! assert_equal (hist3 (Xv, 'nbins', [10 10]), eye (10)) %! assert_equal (hist3 (Xv, [5 5]), eye (5) * 2) %! %! N_exp = zeros (7, 5); %! N_exp([1 9 10 18 26 27 35]) = [2 1 1 2 1 1 2]; %! assert_equal (hist3 (Xv, [7 5]), N_exp) %!test # bug #51059 %! D = [1 1; NaN 2; 3 1; 3 3; 1 NaN; 3 1]; %! [c, nn] = hist3 (D, {0:4, 0:4}); %! exp_c = zeros (5); %! exp_c([7 9 19]) = [1 2 1]; %! assert_equal (c, exp_c) %! assert_equal (nn, {0:4, 0:4}) ## Single row of data or cases where all elements have the same value ## on one side of the histogram. %!test %! [c, nn] = hist3 ([1 8]); %! exp_c = zeros (10, 10); %! exp_c(6, 6) = 1; %! exp_nn = {-4:5, 3:12}; %! assert_equal (c, exp_c) %! assert_equal (nn, exp_nn, eps) %! %! [c, nn] = hist3 ([1 8], [10 11]); %! exp_c = zeros (10, 11); %! exp_c(6, 6) = 1; %! exp_nn = {-4:5, 3:13}; %! assert_equal (c, exp_c) %! assert_equal (nn, exp_nn, eps) ## NaNs paired with values defining the histogram edges. %!test %! [c, nn] = hist3 ([1 NaN; 2 3; 6 9; 8 NaN]); %! exp_c = zeros (10, 10); %! exp_c(2, 1) = 1; %! exp_c(8, 10) = 1; %! exp_nn = {linspace(1.35, 7.65, 10) linspace(3.3, 8.7, 10)}; %! assert_equal (c, exp_c) %! assert_equal (nn, exp_nn, eps*100) ## Columns full of NaNs (recent Matlab versions seem to throw an error ## but this did work like this on R2010b at least). %!test %! [c, nn] = hist3 ([1 NaN; 2 NaN; 6 NaN; 8 NaN]); %! exp_c = zeros (10, 10); %! exp_nn = {linspace(1.35, 7.65, 10) NaN(1, 10)}; %! assert_equal (c, exp_c) %! assert_equal (nn, exp_nn, eps*100) ## Behaviour of an empty X after removal of rows with NaN. %!test %! [c, nn] = hist3 ([1 NaN; NaN 3; NaN 9; 8 NaN]); %! exp_c = zeros (10, 10); %! exp_nn = {linspace(1.35, 7.65, 10) linspace(3.3, 8.7, 10)}; %! assert_equal (c, exp_c) %! assert_equal (nn, exp_nn, eps*100) statistics-release-2.1.2/inst/Plotting/histfit.m000066400000000000000000000207721527311104300217360ustar00rootroot00000000000000## Copyright (C) 2003 Alberto Terruzzi ## Copyright (C) 2022-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} histfit (@var{x}) ## @deftypefnx {statistics} {} histfit (@var{x}, @var{nbins}) ## @deftypefnx {statistics} {} histfit (@var{x}, @var{nbins}, @var{distname}) ## @deftypefnx {statistics} {} histfit (@var{ax}, @dots{}) ## @deftypefnx {statistics} {@var{h} =} histfit (@dots{}) ## ## Plot histogram with superimposed distribution fit. ## ## @code{histfit (@var{x})} plots a histogram of the values in the vector ## @var{x} using the number of bins equal to the square root of the number of ## non-missing elements in @var{x} and superimposes a fitted normal density ## function. ## ## @code{histfit (@var{x}, @var{nbins})} plots a histogram of the values in the ## vector @var{x} using @var{nbins} number of bins in the histogram and ## superimposes a fitted normal density function. ## ## @code{histfit (@var{x}, @var{nbins}, @var{distname})} plots a histogram of ## the values in the vector @var{x} using @var{nbins} number of bins in the ## histogram and superimposes a fitted density function from the distribution ## specified by @var{distname}. ## ## @code{histfit (@var{ax}, @dots{})} uses the axes handle @var{ax} to plot the ## histogram and the fitted density function onto followed by any of the input ## argument combinations specified in the previous syntaxes. ## ## @code{@var{h} = histfit (@dots{})} returns a vector of handles @var{h}, where ## @qcode{@var{h}(1)} is the handle to the histogram and @qcode{@var{h}(2)} is ## the handle to the density curve. ## ## Note: calling @code{histfit} without any input arguments will return a cell ## array of character vectors listing all supported distributions. ## ## @seealso{bar, hist, normplot, fitdist} ## @end deftypefn function [varargout] = histfit (varargin) ## Add list of supported probability distribution objects PDO = {'Beta'; 'BirnbaumSaunders'; 'Burr'; 'Exponential'; 'ExtremeValue'; ... 'Gamma'; 'GeneralizedExtremeValue'; 'GeneralizedPareto'; ... 'InverseGaussian'; 'Logistic'; 'Loglogistic'; 'Lognormal'; ... 'Nakagami'; 'NegativeBinomial'; 'Normal'; 'Poisson'; 'Rayleigh'; ... 'Rician'; 'tLocationScale'; 'Weibull'}; ABBR = {'bisa'; 'ev'; 'gev'; 'gp'; 'invg'; 'nbin'; 'tls'; 'wbl'}; ## Check for zero input arguments if (numel (varargin) < 1) varargout{1} = PDO; return endif ## Check for axes handle if (isaxes (varargin{1})) ax = varargin{1}; varargin(1) = []; get_current_axes = false; else get_current_axes = true; endif ## Get data if (numel (varargin) < 1) error ("histfit: too few input arguments."); else x = varargin{1}; if (! isnumeric (x) || ! isreal (x) || ! isvector (x) || isscalar (x)) error ("histfit: X must be a numeric vector of real numbers."); endif ## Remove missing values x(isnan (x)) = []; xsize = numel (x); ## Check for valid data if (xsize < 1) error ("histfit: no data in X."); endif endif ## Get nbins if (numel (varargin) > 1) nbins = varargin{2}; if (! (isreal (nbins) && isscalar (nbins) && fix (nbins) == nbins)) error ("histfit: NBINS must be a real scalar integer value."); endif else nbins = ceil (sqrt (xsize)); endif ## Get distribution if (numel (varargin) > 2) distname = varargin{3}; ## Check distribution name if (! (ischar (distname) && size (distname, 1) == 1)) error ("histfit: DISTNAME must be a character vector."); elseif (strcmpi (distname, 'kernel')) error ("histfit: 'Kernel' distribution is not supported yet."); elseif (! (any (strcmpi (distname, PDO)) || any (strcmpi (distname, ABBR)))) error ("histfit: unrecognized distribution name."); endif else distname = 'normal'; endif ## Create axes handle (if necessary) if (get_current_axes) ax = gca (); endif ## Plot the histogram if (any (strcmpi (distname, {'poisson', 'NegativeBinomial', 'nbin'}))) binwidth = 1; xmin = min (x) - 1; xmax = max (x) + 1; [binsize, bincenter] = hist (x, [xmin:xmax]); else [binsize, bincenter] = hist (x, nbins); binwidth = max (diff (bincenter)); xmin = min (x) - binwidth / 2; xmax = max (x) + binwidth / 2; endif h = bar (ax, bincenter, binsize, 1, 'facecolor', 'b'); ## Fit distribution to data pd = fitdist (x, distname); ## Compute density function if (any (strcmpi (distname, {'poisson', 'NegativeBinomial', 'nbin'}))) x = [min(x):max(x)]'; y = pdf (pd, x); else x = [xmin:(xmax-xmin)/100:xmax]'; y = pdf (pd, x); endif ## Normalize density line and overplot the histogram y = xsize * y * binwidth; hold on; if (any (strcmpi (distname, {'poisson', 'NegativeBinomial', 'nbin'}))) h(2) = plot (ax, x, y, ';;r-o'); else h(2) = plot (ax, x, y, ';;r-'); endif xlim ([xmin, xmax]); hold off; ## Return the plot's handle if requested if (nargout == 1) varargout{1} = h; endif endfunction %!demo %! rng (42); %! histfit (randn (100, 1)) %!demo %! rng (42); %! randp ('state', 42); %! histfit (poissrnd (2, 1000, 1), 10, 'Poisson') %!demo %! randg ('state', 42); %! histfit (betarnd (3, 10, 1000, 1), 10, 'beta') ## Test plotting %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! x = [2, 4, 3, 2, 4, 3, 2, 5, 6, 4, 7, 5, 9, 8, 10, 4, 11]; %! histfit (x); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! x = [2, 4, 3, 2, NaN, 3, 2, 5, 6, 4, 7, 5, 9, 8, 10, 4, 11]; %! histfit (x); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! x = [2, 4, 3, 2, NaN, 3, 2, 5, 6, 4, 7, 5, 9, 8, 10, 4, 11]; %! histfit (x, 3); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! histfit (randn (100, 1)); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! histfit (poissrnd (2, 1000, 1), 10, 'Poisson'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! histfit (betarnd (3, 10, 1000, 1), 10, 'beta'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! ax = gca (); %! histfit (ax, randn (100, 1)); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! ax = gca (); %! histfit (ax, poissrnd (2, 1000, 1), 10, 'Poisson'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! ax = gca (); %! histfit (ax, betarnd (3, 10, 1000, 1), 10, 'beta'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! ax = axes ('parent', hf); %! fail ('histfit (ax)', 'histfit: too few input arguments.'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!error ... %! histfit ('wer') %!error histfit ([NaN, NaN, NaN]); %!error ... %! histfit (randn (100, 1), 5.6) %!error ... %! histfit (randn (100, 1), 8, 5) %!error ... %! histfit (randn (100, 1), 8, {'normal'}) %!error ... %! histfit (randn (100, 1), 8, 'Kernel') %!error ... %! histfit (randn (100, 1), 8, 'ASDASDASD') statistics-release-2.1.2/inst/Plotting/manovacluster.m000066400000000000000000000066501527311104300231460ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} manovacluster (@var{stats}) ## @deftypefnx {statistics} {} manovacluster (@var{stats}, @var{method}) ## @deftypefnx {statistics} {@var{h} =} manovacluster (@var{stats}) ## @deftypefnx {statistics} {@var{h} =} manovacluster (@var{stats}, @var{method}) ## ## Cluster group means using manova1 output. ## ## @code{manovacluster (@var{stats})} draws a dendrogram showing the clustering ## of group means, calculated using the output STATS structure from ## @code{manova1} and applying the single linkage algorithm. See the ## @code{dendrogram} function for more information about the figure. ## ## @code{manovacluster (@var{stats}, @var{method})} uses the @var{method} ## algorithm in place of single linkage. The available methods are: ## ## @multitable @columnfractions 0.2 0.75 ## @item "single" @tab --- nearest distance ## @item "complete" @tab --- furthest distance ## @item "average" @tab --- average distance ## @item "centroid" @tab --- center of mass distance ## @item "ward" @tab --- inner squared distance ## @end multitable ## ## @code{@var{h} = manovacluster (@dots{})} returns a vector of line handles. ## ## @seealso{manova1} ## @end deftypefn function h = manovacluster (stats, method) ## Check for valid input arguments narginchk (1, 2); if nargin > 1 valid_methods = {'single', 'complete', 'average', 'centroid', 'ward'}; if ! any (strcmpi (method, valid_methods)) error ("manovacluster: invalid method."); endif else method = 'single'; endif ## Get stats fields and create dendrogram dist = stats.gmdist; group_names = stats.gnames; [a, b] = meshgrid (1:length (dist)); hh = dendrogram (linkage (dist(a < b)', method), 0); ## Fix tick labels on x-axis oldlab = get (gca, 'XTickLabel'); maxlen = max (cellfun ('length', group_names)); newlab = repmat (' ', size (oldlab, 1), maxlen); ng = size (group_names, 1); for j = 1:size (oldlab, 1) k = str2num (oldlab(j,:)); if (! isempty (k) & k > 0 & k <= ng) x = group_names{k,:}; newlab(j,1:length (x)) = x; endif endfor set (gca, 'XtickLabel', newlab); ## Return plot handles if requested if nargout > 0 h = hh; endif endfunction %!demo %! load carbig %! X = [MPG Acceleration Weight Displacement]; %! [d, p, stats] = manova1 (X, Origin); %! manovacluster (stats) ## Test plotting %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! load carbig %! X = [MPG Acceleration Weight Displacement]; %! [d, p, stats] = manova1 (X, Origin); %! manovacluster (stats); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error manovacluster (stats, 'some'); statistics-release-2.1.2/inst/Plotting/normplot.m000066400000000000000000000140741527311104300221340ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## Based on previous work by Paul Kienzle originally ## granted to the public domain. ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} normplot (@var{x}) ## @deftypefnx {statistics} {} normplot (@var{ax}, @var{x}) ## @deftypefnx {statistics} {@var{h} =} normplot (@dots{}) ## ## Produce normal probability plot of the data in @var{x}. If @var{x} is a ## matrix, @code{normplot} plots the data for each column. NaN values are ## ignored. ## ## @code{@var{h} = normplot (@var{ax}, @var{x})} takes a handle @var{ax} in ## addition to the data in @var{x} and it uses that axes for plotting. You may ## get this handle of an existing plot with @code{gca}. ## ## The line joining the 1st and 3rd quantile is drawn solid whereas its ## extensions to both ends are dotted. If the underlying distribution is normal, ## the points will cluster around the solid part of the line. Other distribution ## types will introduce curvature in the plot. ## ## @seealso{cdfplot, wblplot} ## @end deftypefn function h = normplot (varargin) ## Check for valid input arguments narginchk (1, 2); ## Parse input arguments if (nargin == 1) ax = []; x = varargin{1}; else ax = varargin{1}; ## Check that ax is a valid axis handle try isstruct (get (ax)); catch error ("normplot: invalid handle %f.", ax); end_try_catch x = varargin{2}; endif ## Check that x is a vector or a 2-D matrix if (isscalar (x) || ndims (x) > 2) error ("normplot: x must be a vector or a 2-D matrix handle."); endif ## If x is a vector, make it a column vector if (rows (x) == 1) x = x(:); endif ## If ax is empty, create a new axes if (isempty (ax)) ax = newplot (); endif ## Get number of column vectors in x col = size (x, 2); ## Process each column and plot data and fit lines color = {'blue', 'black', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow'}; hold on; for i = 1:col xc = x(:,i); ## Remove NaNs, get min, max, and range xc(isnan (xc)) = []; if (isempty (xc)) break; endif ## Transform data row_xc = rows (xc); yc = norminv (([1:row_xc]' - 0.5) / row_xc); xc = sort (xc); ## Find quartiles q1x = prctile (xc, 25); q3x = prctile (xc, 75); q1y = prctile (yc, 25); q3y = prctile (yc, 75); qx = [q1x; q3x]; qy = [q1y; q3y]; ## Calculate coordinates and limits for fitting lines dx = q3x - q1x; dy = q3y - q1y; slope = dy ./ dx; centerx = (q1x + q3x)/2; centery = (q1y + q3y)/2; maxx = max (xc); minx = min (xc); maxy = centery + slope.*(maxx - centerx); miny = centery - slope.*(centerx - minx); yinter = centery - slope.*(centerx); mx = [minx; maxx]; my = [miny; maxy]; ## Plot data and corresponding reference lines in the same color, ## following the default color order. Plot reference line first, ## followed by the data, so that data will be on top of reference line. h_end(i) = line (ax, mx, my, 'LineStyle', '-.', 'Marker', 'none', ... 'color', color{mod(i,8)}); h_mid(i) = line (ax, qx, qy, 'LineStyle', '-', 'Marker', 'none', ... 'color', color{mod(i,8)}); h_dat(i) = line (ax, xc, yc, 'LineStyle', 'none', 'Marker', '+', ... 'color', color{mod(i,8)}); endfor hold off; ## Change colors for single column vector if (i == 1) set (h_dat, 'Color', 'b'); set (h_mid, 'Color', 'r'); set (h_end, 'Color', 'r'); endif ## Bundle handles together if output requested if (nargout > 0) h = [h_dat, h_mid, h_end]'; endif ## Plot labels title 'Normal Probability Plot' ylabel 'Probability' xlabel 'Data' ## Plot grid p = [0.001, 0.003, 0.01, 0.02, 0.05, 0.10, 0.25, 0.5, ... 0.75, 0.90, 0.95, 0.98, 0.99, 0.997, 0.999]; label = {'0.001', '0.003', '0.01', '0.02', '0.05', '0.10', '0.25', '0.50', ... '0.75', '0.90', '0.95', '0.98', '0.99', '0.997', '0.999'}; tick = norminv (p, 0, 1); set (ax, 'ytick', tick, 'yticklabel', label); ## Set view range with a bit of space around data range = nanmax (x(:)) - nanmin (x(:)); if (range > 0) minxaxis = nanmin (x(:)) - 0.025 * range; maxxaxis = nanmax (x(:)) + 0.025 * range; else minxaxis = nanmin (x(:)) - 1; maxxaxis = nanmax (x(:)) + 1; endif minyaxis = norminv (0.25 ./ row_xc, 0, 1); maxyaxis = norminv ((row_xc - 0.25) ./ row_xc, 0, 1); set (ax, 'ylim', [minyaxis, maxyaxis], 'xlim', [minxaxis, maxxaxis]); grid (ax, 'on'); box (ax, 'off'); endfunction %!demo %! h = normplot ([1:20]); %!demo %! h = normplot ([1:20;5:2:44]'); %!demo %! ax = newplot (); %! h = normplot (ax, [1:20]); %! ax = gca; %! h = normplot (ax, [-10:10]); %! set (ax, 'xlim', [-11, 21]); ## Test input validation %!error normplot (); %!error normplot (23); %!error normplot (23, [1:20]); %!error normplot (ones (3,4,5)); ## Test plotting %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! ax = newplot (hf); %! h = normplot (ax, [1:20]); %! ax = gca; %! h = normplot (ax, [-10:10]); %! set (ax, 'xlim', [-11, 21]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = normplot ([1:20;5:2:44]'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect statistics-release-2.1.2/inst/Plotting/parallelcoords.m000066400000000000000000000226101527311104300232630ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} parallelcoords (@var{x}) ## @deftypefnx {statistics} {} parallelcoords (@var{x}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {statistics} {} parallelcoords (@var{ax}, @dots{}) ## @deftypefnx {statistics} {@var{h} =} parallelcoords (@dots{}) ## ## Create a parallel coordinates plot of the multivariate data in @var{x}. ## ## @code{parallelcoords (@var{x})} plots each observation (row) of the ## @code{n}-by-@code{p} matrix @var{x} as a line connecting the values of its ## @var{p} coordinates, which are placed at the equally spaced horizontal ## positions @code{1, 2, @dots{}, p}. ## ## The following name/value pairs are accepted: ## ## @table @asis ## @item @qcode{"Group"} ## A grouping variable (numeric, logical, character, string, or cell array of ## strings) with one entry per row of @var{x}. Lines are colored by group. ## ## @item @qcode{"Standardize"} ## Controls how the columns of @var{x} are transformed before plotting: ## @qcode{"off"} (default) uses the raw data, @qcode{"on"} centers and scales ## each column to zero mean and unit standard deviation, @qcode{"PCA"} uses the ## principal component scores, and @qcode{"PCAStd"} uses the principal component ## scores of the standardized data. ## ## @item @qcode{"Quantile"} ## A scalar @var{alpha} in the interval @math{(0,1)}. Instead of one line per ## observation, only three lines per group are drawn: the coordinate-wise median ## and the @var{alpha} and @math{1-}@var{alpha} quantiles of the group. ## ## @item @qcode{"Labels"} ## A character array or cell array of strings giving the tick labels for the ## coordinate axis. ## @end table ## ## @code{parallelcoords (@var{ax}, @dots{})} plots into the axes @var{ax}. ## ## The optional output @var{h} is a vector of handles to the plotted lines: one ## per observation, or three per group when @qcode{"Quantile"} is used. ## ## @seealso{andrewsplot, glyphplot, pca} ## @end deftypefn function h = parallelcoords (varargin) ## Optional leading axes handle hax = []; if (numel (varargin) > 0 && isaxes (varargin{1})) hax = varargin{1}; varargin(1) = []; endif if (numel (varargin) < 1) print_usage (); endif X = varargin{1}; varargin(1) = []; if (! isnumeric (X) || ! isreal (X) || ndims (X) > 2) error ("parallelcoords: X must be a real numeric matrix."); endif n = rows (X); ## Parse name/value options group = []; standardize = "off"; alpha = []; labels = {}; if (mod (numel (varargin), 2) != 0) error ("parallelcoords: name/value arguments must come in pairs."); endif for i = 1:2:numel (varargin) name = varargin{i}; value = varargin{i+1}; if (! ischar (name)) error ("parallelcoords: property names must be strings."); endif switch (lower (name)) case "group" group = value; case "standardize" standardize = value; case "quantile" alpha = value; case "labels" labels = value; otherwise error ("parallelcoords: unknown property '%s'.", name); endswitch endfor ## Validate before plotting, so a bad option never leaves a stray figure if (! isempty (alpha)) if (! (isscalar (alpha) && isreal (alpha) && alpha > 0 && alpha < 1)) error ("parallelcoords: Quantile ALPHA must be a scalar in (0,1)."); endif endif ## Standardize the data switch (lower (standardize)) case "off" Z = X; case "on" Z = zscore (X); case "pca" [~, Z] = pca (X); case "pcastd" [~, Z] = pca (zscore (X)); otherwise error ("parallelcoords: invalid Standardize option '%s'.", standardize); endswitch p = columns (Z); cx = 1:p; ## Grouping if (isempty (group)) gidx = ones (n, 1); gnames = {"1"}; else [gidx, gnames] = grp2idx (group); if (numel (gidx) != n) error ("parallelcoords: GROUP must have one entry per row of X."); endif endif k = numel (gnames); gcol = lines (k); if (isempty (hax)) hax = newplot (); else newplot (hax); endif old_hold = ishold (hax); hold (hax, "on"); h = []; if (isempty (alpha)) ## One line per observation, colored by group for i = 1:n h(end+1) = line (hax, cx, Z(i,:), "color", gcol(gidx(i),:)); endfor else ## Coordinate-wise median and alpha / 1-alpha quantile lines per group for g = 1:k Zg = Z(gidx == g, :); med = median (Zg, 1); lo = quantile (Zg, alpha, 1); hi = quantile (Zg, 1 - alpha, 1); h(end+1) = line (hax, cx, med, "color", gcol(g,:), "linewidth", 2); h(end+1) = line (hax, cx, lo, "color", gcol(g,:), "linestyle", "--"); h(end+1) = line (hax, cx, hi, "color", gcol(g,:), "linestyle", "--"); endfor endif ## Coordinate axis ticks and labels set (hax, "xtick", cx); set (hax, "xlim", [1, max(p, 2)]); if (! isempty (labels)) if (ischar (labels)) labels = cellstr (labels); endif set (hax, "xticklabel", labels); endif xlabel (hax, "Coordinate"); ylabel (hax, "Coordinate Value"); if (k > 1 && ! isempty (group)) warning ("off", "Octave:legend:unimplemented-location", "local"); if (isempty (alpha)) [~, first] = unique (gidx, "first"); legend (hax, h(sort (first)), gnames, "location", "best"); else legend (hax, h(1:3:end), gnames, "location", "best"); endif endif if (! old_hold) hold (hax, "off"); endif if (nargout == 0) clear h; endif endfunction %!demo %! ## Parallel coordinates plot of Fisher's iris data, grouped by species. %! %! load fisheriris; %! parallelcoords (meas, "Group", species, "Labels", ... %! {"SL", "SW", "PL", "PW"}); %!demo %! ## The same data with median and quartile lines per species. %! %! load fisheriris; %! parallelcoords (meas, "Group", species, "Quantile", 0.25, ... %! "Standardize", "on"); ## Test output %!test %! hf = figure ("visible", "off"); %! unwind_protect %! X = [1 2 3; 4 5 6; 7 8 9; 2 1 5]; %! h = parallelcoords (X); %! assert_equal (numel (h), 4); %! assert_equal (get (h(1), "xdata"), [1 2 3]); %! assert_equal (get (h(1), "ydata"), [1 2 3]); %! assert_equal (get (h(2), "ydata"), [4 5 6]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # Standardize "on" (z-score each column) %! hf = figure ("visible", "off"); %! unwind_protect %! X = [1 2 3; 4 5 6; 7 8 9; 2 1 5]; %! h = parallelcoords (X, "Standardize", "on"); %! assert_equal (get (h(1), "ydata"), [-0.94491, -0.63246, -1.1], 1e-4); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # Standardize "pca" %! hf = figure ("visible", "off"); %! unwind_protect %! X = [1 2 3; 4 5 6; 7 8 9; 2 1 5]; %! h = parallelcoords (X, "Standardize", "pca"); %! assert_equal (get (h(1), "ydata")(1:2), [-4.1082, 0.9670], 1e-4); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # Quantile mode: median, alpha, 1-alpha lines %! hf = figure ("visible", "off"); %! unwind_protect %! X = [1 2 3; 4 5 6; 7 8 9; 2 1 5]; %! h = parallelcoords (X, "Quantile", 0.25); %! assert_equal (numel (h), 3); %! assert_equal (get (h(1), "ydata"), [3, 3.5, 5.5], 1e-12); %! assert_equal (get (h(2), "ydata"), [1.5, 1.5, 4], 1e-12); %! assert_equal (get (h(3), "ydata"), [5.5, 6.5, 7.5], 1e-12); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # coordinate tick labels %! hf = figure ("visible", "off"); %! unwind_protect %! h = parallelcoords ([1 2 3; 4 5 6], "Labels", {"a", "b", "c"}); %! assert_equal (get (gca, "xtick"), [1 2 3]); %! assert_equal (get (gca, "xticklabel"), {"a"; "b"; "c"}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error parallelcoords () %!error parallelcoords ({1}) %!error ... %! parallelcoords (ones (3, 2), "Group") %!error ... %! parallelcoords (ones (3, 2), "bogus", 1) %!error ... %! parallelcoords (ones (3, 2), "Standardize", "xxx") %!error ... %! parallelcoords (ones (3, 2), "Group", [1 2]) %!error ... %! parallelcoords (ones (3, 2), "Quantile", 0) ## A bad Quantile ALPHA must error before any figure is created (no stray figure) %!test %! nfig = numel (get (0, "children")); %! fail ('parallelcoords (ones (3, 2), "Quantile", 0)', ... %! 'parallelcoords: Quantile ALPHA must be a scalar in .0,1..'); %! assert_equal (numel (get (0, "children")), nfig); statistics-release-2.1.2/inst/Plotting/parallelplot.m000066400000000000000000000224071527311104300227540ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} parallelplot (@var{data}) ## @deftypefnx {statistics} {} parallelplot (@var{tbl}) ## @deftypefnx {statistics} {} parallelplot (@var{parent}, @dots{}) ## @deftypefnx {statistics} {} parallelplot (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {@var{h} =} parallelplot (@dots{}) ## ## Parallel coordinates plot of observations of several variables. ## ## @code{parallelplot (@var{data})} draws each column of the numeric matrix ## @var{data} as a vertical ruler, a coordinate, and each row as a line ## joining its values across the rulers. By default each ruler has limits of ## its own, from the smallest to the largest value of its column. ## ## @code{parallelplot (@var{tbl})} draws the variables of the table ## @var{tbl} as coordinates, named after them. A variable of categories, ## text or logical values places its distinct values evenly along its ruler. ## ## @qcode{'CoordinateData'} chooses the columns of @var{data} to draw, and ## @qcode{'CoordinateVariables'} the variables of @var{tbl}, by names, ## indices or a logical vector. With @qcode{'GroupData'}, a vector with one ## element for each observation, or @qcode{'GroupVariable'}, a variable of ## @var{tbl}, each group is drawn in a colour of its own, with a legend ## naming the groups in the order in which each first appears. ## @qcode{'DataNormalization'} places the values as they are on rulers of ## their own, @qcode{'range'}, or on one shared scale: as they are, ## @qcode{'none'}; as z-scores, @qcode{'zscore'}; divided by the standard ## deviation, @qcode{'scale'}; less the mean, @qcode{'center'}; divided by ## the 2-norm, @qcode{'norm'}. ## ## @code{parallelplot (@var{parent}, @dots{})} places the chart in the ## figure or panel @var{parent} rather than the current figure. The chart ## takes the place of the current axes of its parent, and is refused where ## @code{hold} is on for them. ## ## @code{parallelplot (@dots{}, @var{Name}, @var{Value})} sets properties of ## the chart; see @code{stats.chart.ParallelCoordinatesPlot} for all of them. ## ## @code{@var{h} = parallelplot (@dots{})} returns the chart, a ## @code{stats.chart.ParallelCoordinatesPlot} object. ## ## @seealso{stats.chart.ParallelCoordinatesPlot, parallelcoords} ## @end deftypefn function varargout = parallelplot (varargin) if (nargin < 1) print_usage (); endif ## Input validation parent = []; args = varargin; if (isscalar (args{1}) && ! istable (args{1}) && isnumeric (args{1}) && ishghandle (args{1}) && numel (args) > 1 && (istable (args{2}) || (isnumeric (args{2}) && ! isscalar (args{2})))) type = get (args{1}, 'type'); if (strcmp (type, 'axes')) error (strcat ("parallelplot: a parallel coordinates plot cannot be", ... " placed in axes.")); elseif (! any (strcmp (type, {'figure', 'uipanel'}))) error ("parallelplot: PARENT must be a figure or a panel."); endif parent = args{1}; args(1) = []; endif spec = struct ('Data', [], 'SourceTable', [], 'CoordinateData', [], ... 'CoordinateVariables', {{}}, 'GroupData', [], ... 'GroupVariable', ''); if (istable (args{1})) spec.SourceTable = args{1}; elseif ((isnumeric (args{1}) || islogical (args{1})) && isreal (args{1}) && ndims (args{1}) == 2) spec.Data = args{1}; elseif (isnumeric (args{1})) error ("parallelplot: DATA must be a 2-dimensional real numeric matrix."); else error ("parallelplot: the data must be a table or a numeric matrix."); endif rest = args(2:end); ## Name-value pairs, the names resolved to the properties they set if (mod (numel (rest), 2) != 0) error ("parallelplot: optional arguments must be in Name, Value pairs."); endif props = {'Data', 'CoordinateData', 'SourceTable', ... 'CoordinateVariables', 'GroupData', 'GroupVariable', ... 'DataNormalization', 'CoordinateTickLabels', 'Jitter', 'Color', ... 'LineStyle', 'LineWidth', 'LineAlpha', 'MarkerStyle', ... 'MarkerSize', 'LegendVisible', 'LegendTitle', ... 'CoordinateLabel', 'DataLabel', 'Title', 'FontName', 'FontSize', ... 'Position', 'InnerPosition', 'OuterPosition', ... 'PositionConstraint', 'Units', 'Visible'}; for k = 1:2:numel (rest) i = []; if (isName (rest{k})) i = find (strcmpi (char (rest{k}), props), 1); endif if (isempty (i)) error ("parallelplot: invalid optional paired argument."); endif rest{k} = props{i}; endfor ## What chooses and groups the observations belongs with the data for f = {'CoordinateData', 'CoordinateVariables', 'GroupData', ... 'GroupVariable'} k = find (strcmp (rest(1:2:end), f{1}), 1, 'last'); if (! isempty (k)) fromTable = any (strcmp (f{1}, {'CoordinateVariables', ... 'GroupVariable'})); if (fromTable && isempty (spec.SourceTable)) error ("parallelplot: '%s' needs a table.", f{1}); elseif (! fromTable && ! isempty (spec.SourceTable) && strcmp (f{1}, 'CoordinateData')) error ("parallelplot: 'CoordinateData' needs a matrix."); endif spec.(f{1}) = rest{2*k}; rest(2*k-1:2*k) = []; endif endfor if (! isempty (spec.SourceTable) && ! isempty (spec.GroupData)) ## GroupData with a table groups its rows as with a matrix rest(end+1:end+2) = {'GroupData', spec.GroupData}; spec.GroupData = []; endif h = stats.chart.ParallelCoordinatesPlot (parent, spec, rest); ## The chart is handed back only where it was asked for if (nargout > 0) varargout{1} = h; endif endfunction ## A name: a character vector or a string scalar function tf = isName (v) tf = (ischar (v) && isrow (v)) || (isa (v, 'string') && isscalar (v)); endfunction %!demo %! ## The four measurements of each iris flower, joined across four rulers. %! load fisheriris %! parallelplot (meas); %!demo %! ## Grouped by species, with the columns named, as z-scores on one scale. %! load fisheriris %! parallelplot (meas, 'GroupData', species, 'CoordinateTickLabels', ... %! {'Sepal length', 'Sepal width', 'Petal length', ... %! 'Petal width'}, 'DataNormalization', 'zscore'); %!demo %! ## From a table, with a variable of categories as a coordinate. %! load carsmall %! t = table (MPG, Horsepower, Weight, categorical (cellstr (Origin)), ... %! 'VariableNames', {'MPG', 'Horsepower', 'Weight', 'Origin'}); %! parallelplot (t, 'GroupVariable', 'Origin', 'CoordinateVariables', ... %! {'MPG', 'Horsepower', 'Weight'}); %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! parallelplot (magic (4)); %! tag = 'stats.chart.ParallelCoordinatesPlot'; %! ax = findall (hf, 'type', 'axes', 'tag', tag); %! assert_equal (numel (ax), 1); %! assert_equal (gca (), ax); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! plot (1:3); %! pax = gca (); %! h = parallelplot (magic (4)); %! assert_equal (ishghandle (pax), false); %! plot (1:3); %! assert_equal (isempty (h.Parent), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!error parallelplot () %!error ... %! parallelplot ('abc') %!error ... %! parallelplot (ones (2, 2, 2)) %!error ... %! parallelplot (magic (3), 'Title') %!error ... %! parallelplot (magic (3), 'Foo', 1) %!error ... %! parallelplot (magic (3), 'CoordinateVariables', {'A'}) %!error ... %! parallelplot (magic (3), 'GroupVariable', 'G') %!error ... %! parallelplot (table ([1; 2]), 'CoordinateData', 1) %!error ... %! hf = figure ('visible', 'off'); %! unwind_protect %! parallelplot (axes (hf), magic (3)); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!error ... %! hf = figure ('visible', 'off'); %! unwind_protect %! plot (1:3); %! hold on; %! parallelplot (magic (3)); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect statistics-release-2.1.2/inst/Plotting/ppplot.m000066400000000000000000000062371527311104300216020ustar00rootroot00000000000000## Copyright (C) 1995-2017 Kurt Hornik ## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} ppplot (@var{x}, @var{dist}) ## @deftypefnx {statistics} {} ppplot (@var{x}, @var{dist}, @var{params}) ## @deftypefnx {statistics} {[@var{p}, @var{y}] =} ppplot (@var{x}, @var{dist}, @var{params}) ## ## Perform a PP-plot (probability plot). ## ## If F is the CDF of the distribution @var{dist} with parameters ## @var{params} and @var{x} a sample vector of length @var{n}, the PP-plot ## graphs ordinate @var{y}(@var{i}) = F (@var{i}-th largest element of ## @var{x}) versus abscissa @var{p}(@var{i}) = (@var{i} - 0.5)/@var{n}. If ## the sample comes from F, the pairs will approximately follow a straight ## line. ## ## The default for @var{dist} is the standard normal distribution. ## ## The optional argument @var{params} contains a list of parameters of ## @var{dist}. ## ## For example, for a probability plot of the uniform distribution on [2,4] ## and @var{x}, use ## ## @example ## ppplot (x, "unif", 2, 4) ## @end example ## ## @noindent ## @var{dist} can be any string for which a function @var{distcdf} that ## calculates the CDF of distribution @var{dist} exists. ## ## If no output is requested then the data are plotted immediately. ## @seealso{qqplot} ## @end deftypefn function [p, y] = ppplot (x, dist, varargin) if (nargin < 1) print_usage (); endif if (! isnumeric (x) || ! isreal (x) || ! isvector (x) || isscalar (x)) error ("ppplot: X must be a numeric vector of real numbers"); endif s = sort (x); n = length (x); p = ((1 : n)' - 0.5) / n; if (nargin == 1) F = @stdnormal_cdf; elseif (! ischar (dist)) error ("ppplot: DIST must be a string"); else F = str2func ([dist 'cdf']); endif if (nargin <= 2) y = feval (F, s); else y = feval (F, s, varargin{:}); endif if (nargout == 0) plot (p, y); axis ([0, 1, 0, 1]); endif endfunction function p = stdnormal_cdf (x) p = 0.5 * erfc (x ./ sqrt (2)); endfunction ## Test plotting %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! ppplot ([2 3 3 4 4 5 6 5 6 7 8 9 8 7 8 9 0 8 7 6 5 4 6 13 8 15 9 9]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error ppplot () %!error ppplot (ones (2,2)) %!error ppplot (1, 2) %!error ppplot ([1 2 3 4], 2) statistics-release-2.1.2/inst/Plotting/probplot.m000066400000000000000000000333611527311104300221230ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} probplot (@var{y}) ## @deftypefnx {statistics} {} probplot (@var{dist}, @var{y}) ## @deftypefnx {statistics} {} probplot (@var{dist}, @var{y}, @var{cens}) ## @deftypefnx {statistics} {} probplot (@var{dist}, @var{y}, @var{cens}, @var{freq}) ## @deftypefnx {statistics} {} probplot (@var{ax}, @dots{}) ## @deftypefnx {statistics} {} probplot (@dots{}, @qcode{"noref"}) ## @deftypefnx {statistics} {@var{h} =} probplot (@dots{}) ## ## Produce a probability plot of the data in @var{y} against the distribution ## @var{dist}. ## ## On a probability plot the ordered data are drawn against a nonlinear ## probability axis chosen so that a sample from the reference distribution ## @var{dist} falls approximately along a straight line. Systematic departures ## from the reference line indicate departures from the distribution. ## ## @var{dist} is one of @qcode{"normal"} (the default when @var{dist} is ## omitted), @qcode{"lognormal"}, @qcode{"exponential"}, ## @qcode{"extreme value"}, @qcode{"weibull"}, @qcode{"rayleigh"}, ## @qcode{"logistic"}, or @qcode{"loglogistic"}. For @qcode{"lognormal"}, ## @qcode{"weibull"}, and @qcode{"loglogistic"} the data axis is logarithmic. ## ## @var{y} is a numeric vector, or a matrix in which case each column is plotted ## as a separate sample. @code{NaN} values are ignored. ## ## @var{cens} is a logical vector the same size as @var{y} that is true for ## right-censored observations; censored points are not plotted and the plotting ## positions of the remaining points follow the Kaplan-Meier estimate. ## @var{freq} is a vector of nonnegative integer frequencies (counts) the same ## size as @var{y}. Pass @code{[]} to omit either one. ## ## @code{probplot (@var{ax}, @dots{})} plots into the axes @var{ax} instead of ## the current axes. The trailing option @qcode{"noref"} suppresses the ## reference line. ## ## @code{@var{h} = probplot (@dots{})} returns a column vector of handles to the ## plotted line objects (the data markers, followed by the reference line unless ## @qcode{"noref"} was given). ## ## The reference line is a robust fit through the first and third quartiles of ## the data on the transformed scale. For censored data the quartiles are taken ## from the Kaplan-Meier plotting positions; when heavy censoring prevents the ## data from reaching a quartile the position is linearly extrapolated, which ## may deviate slightly from @sc{matlab}. ## ## @seealso{normplot, wblplot, qqplot, cdfplot, ecdf} ## @end deftypefn function h = probplot (varargin) if (nargin < 1) print_usage (); endif args = varargin; ## Optional leading axes handle. ax = []; if (isscalar (args{1}) && ishghandle (args{1}) ... && strcmp (get (args{1}, "type"), "axes")) ax = args{1}; args(1) = []; endif if (isempty (args)) print_usage (); endif ## Distribution name (default "normal") followed by the data. if (ischar (args{1})) dist = lower (args{1}); args(1) = []; else dist = "normal"; endif if (isempty (args)) error ("probplot: missing data vector Y."); endif y = args{1}; args(1) = []; ## Remaining arguments: numeric CENS / FREQ and the trailing "noref" flag. noref = false; numargs = {}; for k = 1:numel (args) a = args{k}; if (ischar (a)) if (strcmpi (a, "noref")) noref = true; else error ("probplot: unknown option '%s'.", a); endif else numargs{end+1} = a; endif endfor if (numel (numargs) > 2) error ("probplot: too many input arguments."); endif cens = []; freq = []; if (numel (numargs) >= 1) cens = numargs{1}; endif if (numel (numargs) >= 2) freq = numargs{2}; endif ## Look up the distribution transform and axis scale. [invfun, logx, prettyname] = dist_transform (dist); ## Validate the data. if (! (isnumeric (y) && isreal (y))) error ("probplot: Y must be real numeric."); endif if (isrow (y)) y = y(:); endif if (ndims (y) > 2) error ("probplot: Y must be a vector or a 2-D matrix."); endif ncol = columns (y); if (ncol > 1 && (! isempty (cens) || ! isempty (freq))) error ("probplot: CENS and FREQ are only supported for a vector Y."); endif if (isempty (ax)) ax = newplot (); endif ## Probability grid for the y-axis ticks (as in a normal probability plot). pgrid = [0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.25, 0.5, ... 0.75, 0.9, 0.95, 0.99, 0.995, 0.999, 0.9995, 0.9999]; hmark = []; href = []; hold (ax, "on"); for j = 1:ncol yj = y(:,j); cj = cens; fj = freq; [xu, pp] = plot_positions (yj, cj, fj); if (isempty (xu)) continue; endif zz = invfun (pp); hmark(end+1) = line (ax, xu, zz, "linestyle", "none", "marker", "x", ... "color", "b"); if (! noref) href(end+1) = ref_line (ax, xu, pp, invfun, logx); endif endfor hold (ax, "off"); ## Data (x) axis scale. if (logx) set (ax, "xscale", "log"); else set (ax, "xscale", "linear"); endif ## Probability (y) axis: tick at invfun(pgrid), labelled with the probability. ok = isfinite (invfun (pgrid)); yt = invfun (pgrid(ok)); ylabels = arrayfun (@(p) num2str (p), pgrid(ok), "UniformOutput", false); set (ax, "ytick", yt, "yticklabel", ylabels); title (ax, sprintf ("Probability plot for %s distribution", prettyname)); xlabel (ax, "Data"); ylabel (ax, "Probability"); grid (ax, "on"); box (ax, "off"); if (nargout > 0) h = [hmark(:); href(:)]; endif endfunction ## Standardized inverse cdf (transform), log-x flag, and pretty name per dist. function [invfun, logx, prettyname] = dist_transform (dist) switch (dist) case "normal" invfun = @(p) norminv (p); logx = false; prettyname = "normal"; case "lognormal" invfun = @(p) norminv (p); logx = true; prettyname = "lognormal"; case "exponential" invfun = @(p) -log (1 - p); logx = false; prettyname = "exponential"; case {"extreme value", "ev"} invfun = @(p) log (-log (1 - p)); logx = false; prettyname = "extreme value"; case {"weibull", "wbl"} invfun = @(p) log (-log (1 - p)); logx = true; prettyname = "weibull"; case {"rayleigh", "rayl"} invfun = @(p) sqrt (-2 .* log (1 - p)); logx = false; prettyname = "rayleigh"; case "logistic" invfun = @(p) log (p ./ (1 - p)); logx = false; prettyname = "logistic"; case "loglogistic" invfun = @(p) log (p ./ (1 - p)); logx = true; prettyname = "loglogistic"; otherwise error ("probplot: unrecognized distribution '%s'.", dist); endswitch endfunction ## Sorted uncensored data and Kaplan-Meier survival-midpoint plotting positions. function [xu, pp] = plot_positions (y, cens, freq) keep = ! isnan (y); y = y(keep); if (isempty (cens)) cens = false (size (y)); else cens = logical (cens(:)); cens = cens(keep); endif if (isempty (freq)) freq = ones (size (y)); else freq = freq(:); freq = freq(keep); endif ## Expand by integer frequency. y = repelem (y(:), freq); cens = repelem (cens(:), freq); [ys, ord] = sort (y); cs = cens(ord); ## Kaplan-Meier survival, sampled to the midpoint of each uncensored jump. n = numel (ys); S = 1; nrisk = n; xu = []; pp = []; for i = 1:n if (! cs(i)) Safter = S * (nrisk - 1) / nrisk; xu(end+1) = ys(i); pp(end+1) = 1 - (S + Safter) / 2; S = Safter; endif nrisk -= 1; endfor xu = xu(:); pp = pp(:); endfunction ## Robust quartile reference line drawn across the data range. function hl = ref_line (ax, xu, pp, invfun, logx) ## Quartiles of the data from the plotting positions (transformed scale). if (logx) u = log (xu); else u = xu; endif ## interp1 needs strictly increasing, unique sample sites. [ppu, iu] = unique (pp); q = interp1 (ppu, u(iu), [0.25, 0.75], "linear", "extrap"); z = invfun ([0.25, 0.75]); slope = (z(2) - z(1)) / (q(2) - q(1)); intercept = z(1) - slope * q(1); ## Sample the straight line across the data range on the transformed scale. uu = linspace (min (u), max (u), 100)'; zz = intercept + slope * uu; if (logx) xx = exp (uu); else xx = uu; endif hl = line (ax, xx, zz, "linestyle", "--", "marker", "none", "color", "r"); endfunction %!demo %! probplot ([1:20]); %!demo %! probplot ("weibull", [1 2 3 4 5 6 7 8 9 10 15 20]); ## shared data %!shared y, c, f %! y = [2.1 3.4 1.8 5.2 2.9 4.1 3.3 2.7 6.0 3.8 4.5 2.2]; %! c = logical ([0 0 0 1 0 0 0 0 1 0 1 0]); %! f = [1 2 1 1 3 1 1 2 1 1 1 2]; ## normal: marker positions and reference line, verified against MATLAB %!test %! hf = figure ("visible", "off"); %! unwind_protect %! h = probplot (y); %! assert_equal (numel (h), 2); %! xd = get (h(1), "xdata"); %! yd = get (h(1), "ydata"); %! assert_equal (xd(:)', sort (y), 1e-12); %! ymatlab = [-1.7317 -1.1503 -0.8122 -0.5485 -0.3186 -0.1046 ... %! 0.1046 0.3186 0.5485 0.8122 1.1503 1.7317]; %! assert_equal (yd(:)', ymatlab, 1e-4); %! rl = get (h(2), "ydata"); %! rx = get (h(2), "xdata"); %! ## reference line slope matches MATLAB robust quartile fit %! slope = (rl(end) - rl(1)) / (rx(end) - rx(1)); %! assert_equal (slope, 0.72923, 1e-4); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## weibull uses a logarithmic data axis and the SEV transform %!test %! hf = figure ("visible", "off"); %! unwind_protect %! ax = axes (hf); %! h = probplot (ax, "weibull", y); %! assert_equal (get (ax, "xscale"), "log"); %! yd = get (h(1), "ydata"); %! ymatlab = [-3.1568 -2.0134 -1.4541 -1.0647 -0.7550 -0.4892 ... %! -0.2483 -0.0194 0.2088 0.4502 0.7321 1.1563]; %! assert_equal (yd(:)', ymatlab, 1e-4); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## remaining distributions: transform and data-axis scale vs MATLAB %!test %! hf = figure ("visible", "off"); %! unwind_protect %! dists = {"exponential", "lognormal", "rayleigh", "logistic", ... %! "loglogistic", "extreme value"}; %! gold = {[0.0426 0.1335 0.2336 0.3448 0.4700 0.6131 0.7802 0.9808 ... %! 1.2321 1.5686 2.0794 3.1781], ... %! [-1.7317 -1.1503 -0.8122 -0.5485 -0.3186 -0.1046 0.1046 ... %! 0.3186 0.5485 0.8122 1.1503 1.7317], ... %! [0.2918 0.5168 0.6835 0.8305 0.9695 1.1073 1.2491 1.4006 ... %! 1.5698 1.7712 2.0393 2.5211], ... %! [-3.1355 -1.9459 -1.3350 -0.8873 -0.5108 -0.1671 0.1671 ... %! 0.5108 0.8873 1.3350 1.9459 3.1355], ... %! [-3.1355 -1.9459 -1.3350 -0.8873 -0.5108 -0.1671 0.1671 ... %! 0.5108 0.8873 1.3350 1.9459 3.1355], ... %! [-3.1568 -2.0134 -1.4541 -1.0647 -0.7550 -0.4892 -0.2483 ... %! -0.0194 0.2088 0.4502 0.7321 1.1563]}; %! logx = {false, true, false, false, true, false}; %! for k = 1:numel (dists) %! ax = axes (hf); %! h = probplot (ax, dists{k}, y); %! assert_equal (get (h(1), "ydata")(:)', gold{k}, 1e-4); %! if (logx{k}) %! assert_equal (get (ax, "xscale"), "log"); %! else %! assert_equal (get (ax, "xscale"), "linear"); %! endif %! delete (ax); %! endfor %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## noref suppresses the reference line %!test %! hf = figure ("visible", "off"); %! unwind_protect %! h = probplot (y, "noref"); %! assert_equal (numel (h), 1); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## censoring: only uncensored points plotted, at Kaplan-Meier positions %!test %! hf = figure ("visible", "off"); %! unwind_protect %! h = probplot ("normal", y, c); %! xd = get (h(1), "xdata"); %! yd = get (h(1), "ydata"); %! assert_equal (xd(:)', [1.8 2.1 2.2 2.7 2.9 3.3 3.4 3.8 4.1], 1e-12); %! ymatlab = [-1.7317 -1.1503 -0.8122 -0.5485 -0.3186 -0.1046 ... %! 0.1046 0.3186 0.5485]; %! assert_equal (yd(:)', ymatlab, 1e-4); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## interspersed censoring: KM redistributes the plotting positions %!test %! hf = figure ("visible", "off"); %! unwind_protect %! cm = logical ([0 0 0 0 0 1 1 1 0 0 0 0]); %! h = probplot ("normal", y, cm); %! yd = get (h(1), "ydata"); %! ymatlab = [-1.7317 -1.1503 -0.8122 -0.5334 -0.2574 0.0196 ... %! 0.3462 0.7764 1.4544]; %! assert_equal (yd(:)', ymatlab, 1e-4); %! rl = get (h(2), "ydata"); %! rx = get (h(2), "xdata"); %! slope = (rl(end) - rl(1)) / (rx(end) - rx(1)); %! assert_equal (slope, 0.53519, 1e-4); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## frequency: each observation expanded into repeated markers %!test %! hf = figure ("visible", "off"); %! unwind_protect %! h = probplot ("normal", y, [], f); %! xd = get (h(1), "xdata"); %! assert_equal (numel (xd), sum (f)); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error probplot () %!error probplot ("foo", [1 2 3 4]) %!error probplot ([1 2 3 4], "bar") statistics-release-2.1.2/inst/Plotting/qqplot.m000066400000000000000000000102561527311104300216000ustar00rootroot00000000000000## Copyright (C) 1995-2017 Kurt Hornik ## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{q}, @var{s}] =} qqplot (@var{x}) ## @deftypefnx {statistics} {[@var{q}, @var{s}] =} qqplot (@var{x}, @var{y}) ## @deftypefnx {statistics} {[@var{q}, @var{s}] =} qqplot (@var{x}, @var{dist}) ## @deftypefnx {statistics} {[@var{q}, @var{s}] =} qqplot (@var{x}, @var{y}, @var{params}) ## @deftypefnx {statistics} {} qqplot (@dots{}) ## ## Perform a QQ-plot (quantile plot). ## ## If F is the CDF of the distribution @var{dist} with parameters ## @var{params} and G its inverse, and @var{x} a sample vector of length ## @var{n}, the QQ-plot graphs ordinate @var{s}(@var{i}) = @var{i}-th ## largest element of x versus abscissa @var{q}(@var{i}f) = G((@var{i} - ## 0.5)/@var{n}). ## ## If the sample comes from F, except for a transformation of location ## and scale, the pairs will approximately follow a straight line. ## ## If the second argument is a vector @var{y} the empirical CDF of @var{y} ## is used as @var{dist}. ## ## The default for @var{dist} is the standard normal distribution. The ## optional argument @var{params} contains a list of parameters of ## @var{dist}. For example, for a quantile plot of the uniform ## distribution on [2,4] and @var{x}, use ## ## @example ## qqplot (x, "unif", 2, 4) ## @end example ## ## @noindent ## @var{dist} can be any string for which a function @var{distinv} or ## @var{dist_inv} exists that calculates the inverse CDF of distribution ## @var{dist}. ## ## If no output arguments are given, the data are plotted directly. ## @seealso{ppplot} ## @end deftypefn function [qout, sout] = qqplot (x, dist, varargin) if (nargin < 1) print_usage (); endif if (! isnumeric (x) || ! isreal (x) || ! isvector (x) || isscalar (x)) error ("qqplot: X must be a numeric vector of real numbers"); endif if (nargin == 1) f = @probit; else if (isnumeric (dist)) f = @(y) empirical_inv (y, dist); elseif (ischar (dist) && (exist(invname = [dist 'inv']) || exist(invname = [dist '_inv']))) f = str2func (invname); else error ("qqplot: no inverse CDF found for distribution DIST"); endif endif; s = sort (x); n = length (x); t = ((1 : n)' - .5) / n; if (nargin <= 2) q = f(t); q_label = func2str (f); else q = f(t, varargin{:}); if (nargin == 3) q_label = sprintf ("%s with parameter %g", func2str (f), varargin{1}); else q_label = sprintf ("%s with parameters %g", func2str (f), varargin{1}); param_str = sprintf (", %g", varargin{2:end}); q_label = [q_label param_str]; endif endif if (nargout == 0) plot (q, s, '-x'); q_label = strrep (q_label, '_inv', '\_inv'); if (q_label(1) == '@') q_label = q_label(6:end); # Strip "@(y) " from anon. function endif xlabel (q_label); ylabel ('sample points'); else qout = q; sout = s; endif endfunction ## Test plotting %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! qqplot ([2 3 3 4 4 5 6 5 6 7 8 9 8 7 8 9 0 8 7 6 5 4 6 13 8 15 9 9]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error qqplot () %!error qqplot ({1}) %!error qqplot (ones (2,2)) %!error qqplot (1, 'foobar') %!error qqplot ([1 2 3], 'foobar') statistics-release-2.1.2/inst/Plotting/scatterhist.m000066400000000000000000000704441527311104300226220ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} scatterhist (@var{x}, @var{y}) ## @deftypefnx {statistics} {} scatterhist (@var{x}, @var{y}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {statistics} {@var{h} =} scatterhist (@dots{}) ## ## Create a scatter plot of @var{x} and @var{y} with marginal histograms. ## ## @code{scatterhist (@var{x}, @var{y})} draws a scatter plot of the vectors ## @var{x} and @var{y} in a central set of axes, with a histogram of @var{x} ## above it and a histogram of @var{y} to its right. @var{x} and @var{y} must ## be vectors of the same length; @qcode{NaN} values are removed pairwise from ## the scatter plot and individually from each marginal histogram. ## ## The following name/value pairs are accepted: ## ## @table @asis ## @item @qcode{"Group"} ## A grouping variable (numeric, logical, character, string, or cell array of ## strings) with one entry per point. The scatter points and the marginal ## histograms are separated and colored by group. ## ## @item @qcode{"NBins"} ## The number of bins for the marginal histograms, either a scalar applied to ## both or a two-element vector @code{[nx ny]}. The default is chosen by ## Scott's rule. ## ## @item @qcode{"Kernel"} ## @qcode{"off"} (default) draws histograms for the marginals; @qcode{"on"} or ## @qcode{"overlay"} draws kernel density estimates instead. ## ## @item @qcode{"Location"} ## Corner occupied by the marginal plots, the scatter taking the opposite one: ## @qcode{"SouthWest"} (default), @qcode{"SouthEast"}, @qcode{"NorthEast"}, or ## @qcode{"NorthWest"}. ## ## @item @qcode{"Legend"} ## @qcode{"on"} or @qcode{"off"} to show or hide the group legend. The default ## is @qcode{"on"} when a grouping variable is supplied. ## ## @item @qcode{"Marker"}, @qcode{"MarkerSize"} ## The marker symbol(s) and size(s) for the scatter points, cycled over the ## groups. ## @end table ## ## The optional output @var{h} is a three-element vector of axes handles: the ## central scatter axes, the axes of the @var{x} (horizontal) histogram, and the ## axes of the @var{y} (vertical) histogram. ## ## The @var{y} histogram's axes is built differently from MATLAB's and its ## properties read accordingly. We plot that marginal transposed, so its ## @qcode{"XLim"} is the density and @qcode{"XDir"} carries the bar direction. ## MATLAB plots it like the @var{x} one -- data along @qcode{"XLim"}, density ## along @qcode{"YLim"} -- and rotates the whole axes by setting ## @qcode{"View"} to @qcode{[270, 90]}, so there the direction is carried by ## @qcode{"YDir"} and @qcode{"XDir"} is always @qcode{"normal"}. The picture ## is the same; code reading those properties off @code{@var{h}(3)} is not ## portable between the two. ## ## @seealso{gscatter, scatter, hist, ksdensity} ## ## @qcode{'Parent'} draws into a supplied figure or uipanel instead of the ## current figure. The container is used as it stands and is never ## cleared, so a scatterhist can be placed alongside other axes. ## ## @qcode{'Location'} names the corner the marginal histograms occupy, ## @qcode{'SouthWest'} by default, or @qcode{'SouthEast'}, @qcode{'NorthEast'} ## or @qcode{'NorthWest'}; the scatter takes the opposite corner. With the ## default the histograms are drawn below and to the left of the scatter. ## ## @qcode{'Direction'} points the marginal bars toward the scatter plot, ## @qcode{'in'} by default, or away from it with @qcode{'out'}, whichever side ## @qcode{'Location'} has placed them on. ## ## @qcode{'PlotGroup'} draws one marginal per group with @qcode{'on'}, or a ## single pooled marginal with @qcode{'off'}. It defaults to @qcode{'on'} when ## @qcode{'Group'} is given and to @qcode{'off'} otherwise. ## ## @qcode{'Style'} outlines the marginals as @qcode{'bar'} or @qcode{'stairs'}, ## defaulting to stairs when the data are grouped and bars when they are not. ## ## @qcode{'Color'} sets the group colours, either as a character vector of ## colour names or as an @math{N}-by-3 matrix of RGB values, cycled over the ## groups. ## ## @qcode{'LineStyle'} and @qcode{'LineWidth'} style the marginal outlines, not ## the scatter markers, and are cycled over the groups. ## ## @qcode{'Bandwidth'} sets the kernel bandwidth used when @qcode{'Kernel'} is ## on: a scalar for all marginals, a pair for @var{x} and @var{y}, or one row ## per group. ## ## @end deftypefn function h = scatterhist (varargin) if (numel (varargin) < 2) print_usage (); endif x = varargin{1}(:); y = varargin{2}(:); varargin(1:2) = []; if (! isnumeric (x) || ! isreal (x) || ! isnumeric (y) || ! isreal (y)) error ("scatterhist: X and Y must be real numeric vectors."); endif if (numel (x) != numel (y)) error ("scatterhist: X and Y must have the same length."); endif ## Parse name/value options through the datatypes parser. It consumes the ## names it knows and returns whatever is left, so anything remaining is an ## unrecognised property. optNames = {"Group", "NBins", "Kernel", "Location", "Legend", "Marker", ... "MarkerSize", "Direction", "PlotGroup", "Style", "Color", ... "Bandwidth", "LineStyle", "LineWidth", "Parent"}; dfValues = {[], [], "off", "southwest", "", "o", 6, "in", "", "", [], ... [], "-", 0.5, []}; if (mod (numel (varargin), 2) != 0) error ("scatterhist: name/value arguments must come in pairs."); endif for i = 1:2:numel (varargin) if (! ischar (varargin{i})) error ("scatterhist: property names must be strings."); endif endfor [group, nbins, kernel, location, legend_opt, marker, markersize, ... direction, plotgroup, style, colour, bandwidth, linestyle, linewidth, ... parent, rest] = parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (rest)) error ("scatterhist: unknown property '%s'.", rest{1}); endif ## The parser only splits the pairs; every value is validated here. if (! isempty (nbins)) if (! (isnumeric (nbins) && all (nbins(:) > 0) && all (nbins(:) == fix (nbins(:))) && numel (nbins) <= 2)) error (strcat ("scatterhist: NBINS must be one or two positive", ... " integers.")); endif endif if (! (ischar (kernel) && any (strcmpi (kernel, {"off", "on", "overlay"})))) error ("scatterhist: KERNEL must be 'off', 'on' or 'overlay'."); endif if (! (ischar (location) && any (strcmpi (location, {"southwest", ... "southeast", "northeast", "northwest"})))) error (strcat ("scatterhist: LOCATION must be 'SouthWest', 'SouthEast',", ... " 'NorthEast' or 'NorthWest'.")); endif if (! (ischar (legend_opt) && any (strcmpi (legend_opt, {"", "on", "off"})))) error ("scatterhist: LEGEND must be 'on' or 'off'."); endif if (! ischar (marker)) error ("scatterhist: MARKER must be a character vector of markers."); endif if (! (isnumeric (markersize) && all (markersize(:) > 0))) error ("scatterhist: MARKERSIZE must be positive numeric values."); endif if (! (ischar (direction) && any (strcmpi (direction, {"in", "out"})))) error ("scatterhist: DIRECTION must be 'in' or 'out'."); endif if (! isempty (style) && ! (ischar (style) && any (strcmpi (style, {"bar", "stairs"})))) error ("scatterhist: STYLE must be 'bar' or 'stairs'."); endif if (! isempty (plotgroup) && ! (ischar (plotgroup) && any (strcmpi (plotgroup, {"on", "off"})))) error ("scatterhist: PLOTGROUP must be 'on' or 'off'."); endif if (! isempty (parent)) if (! (isscalar (parent) && ishghandle (parent) && any (strcmp (get (parent, "type"), {"figure", "uipanel"})))) error ("scatterhist: PARENT must be a figure or uipanel handle."); endif endif ## Marginal line style. 'Bandwidth' is a scalar, a pair for x and y, or one ## row per group; 'LineStyle' and 'LineWidth' cycle over the groups and apply ## to the marginals, not to the scatter markers. if (! isempty (bandwidth)) if (! (isnumeric (bandwidth) && all (bandwidth(:) > 0))) error ("scatterhist: BANDWIDTH must be positive numeric values."); endif if (isvector (bandwidth)) bandwidth = bandwidth(:)'; endif endif if (! (ischar (linestyle) || iscellstr (linestyle))) error ("scatterhist: LINESTYLE must be a string or a cell array of them."); endif if (ischar (linestyle)) linestyle = {linestyle}; endif if (! (isnumeric (linewidth) && all (linewidth(:) > 0))) error ("scatterhist: LINEWIDTH must be positive numeric values."); endif n = numel (x); if (isempty (group)) gidx = ones (n, 1); gnames = {"1"}; else [gidx, gnames] = grp2idx (group); if (numel (gidx) != n) error ("scatterhist: GROUP must have one entry per point."); endif endif k = numel (gnames); if (isempty (colour)) gcol = lines (k); else if (ischar (colour)) colour = colour(:); gcol = zeros (numel (colour), 3); for i = 1:numel (colour) gcol(i,:) = colour_rgb (colour(i)); endfor elseif (isnumeric (colour) && columns (colour) == 3) gcol = colour; else error (strcat ("scatterhist: COLOR must be a character vector of", ... " colour names or an N-by-3 matrix of RGB values.")); endif ## cycle the supplied colours over the groups gcol = gcol(mod ((0:k-1), rows (gcol)) + 1, :); endif do_kernel = any (strcmpi (kernel, {"on", "overlay"})); ## Number of bins for the marginals (Scott's rule by default) if (isempty (nbins)) nbx = scott_nbins (x); nby = scott_nbins (y); elseif (isscalar (nbins)) nbx = nby = nbins; else nbx = nbins(1); nby = nbins(2); endif ## Axes layout. 'Location' names the corner the marginals sit in, as MATLAB ## documents it, so the scatter takes the opposite one: with the default ## "southwest" the x marginal is below the scatter and the y marginal to its ## left, and the scatter itself is upper right. marg_west = any (strcmpi (location, {"southwest", "northwest"})); marg_south = any (strcmpi (location, {"southwest", "southeast"})); west = ! marg_west; south = ! marg_south; ## A container supplied through 'Parent' is drawn into as it stands: it ## belongs to the caller, so it is never cleared. Only a figure of our own ## choosing is. if (isempty (parent)) cf = gcf (); clf (cf); else cf = parent; endif ## The scatter is 0.55 square and offset by 0.35 on whichever sides carry a ## marginal, by 0.10 on the others -- R2024a's own geometry, measured. Each ## marginal then takes a 0.22 band in the strip its side frees. xs = ternary (marg_west, 0.35, 0.10); ys = ternary (marg_south, 0.35, 0.10); ys_marg = ternary (marg_west, 0.05, 0.73); xs_marg = ternary (marg_south, 0.05, 0.73); pos_s = [xs, ys, 0.55, 0.55]; pos_x = [xs, xs_marg, 0.55, 0.22]; pos_y = [ys_marg, ys, 0.22, 0.55]; ax_s = axes ("parent", cf, "position", pos_s, "box", "on", "nextplot", "add"); ax_x = axes ("parent", cf, "position", pos_x, "nextplot", "add"); ax_y = axes ("parent", cf, "position", pos_y, "nextplot", "add"); ## Central scatter plot, grouped scat = zeros (1, k); for g = 1:k idx = (gidx == g) & ! isnan (x) & ! isnan (y); scat(g) = line (ax_s, x(idx), y(idx), "linestyle", "none", ... "marker", marker(mod (g - 1, numel (marker)) + 1), ... "markersize", markersize(mod (g - 1, numel (markersize)) + 1), ... "color", gcol(g,:)); endfor xlabel (ax_s, inputname (1)); ylabel (ax_s, inputname (2)); ## Marginal for x along the top, for y along the right. 'Style' picks the ## outline, defaulting to stairs when grouped and bars when not, and ## 'PlotGroup' says whether the groups are drawn apart or pooled. if (isempty (style)) multi = (k > 1); else multi = strcmpi (style, "stairs"); endif if (isempty (plotgroup)) bygroup = ! isempty (group); else bygroup = strcmpi (plotgroup, "on"); endif if (bygroup) for g = 1:k xg = x(gidx == g & ! isnan (x)); yg = y(gidx == g & ! isnan (y)); marginal (ax_x, xg, nbx, gcol(g,:), do_kernel, multi, false, ... sty (bandwidth, linestyle, linewidth, g, 1)); marginal (ax_y, yg, nby, gcol(g,:), do_kernel, multi, true, ... sty (bandwidth, linestyle, linewidth, g, 2)); endfor else marginal (ax_x, x(! isnan (x)), nbx, gcol(1,:), do_kernel, multi, false, ... sty (bandwidth, linestyle, linewidth, 1, 1)); marginal (ax_y, y(! isnan (y)), nby, gcol(1,:), do_kernel, multi, true, ... sty (bandwidth, linestyle, linewidth, 1, 2)); endif ## 'Direction' points the marginal bars toward the scatter ("in", as MATLAB ## defaults) or away from it ("out"). Which way that is depends on where ## 'Location' put the marginals: bars grow along the positive axis, so the x ## marginal must be reversed only when it sits above the scatter, and the y ## marginal only when it sits to the scatter's right. toward = strcmpi (direction, "in"); set (ax_x, "ydir", ternary (xor (toward, ! south), "reverse", "normal")); set (ax_y, "xdir", ternary (xor (toward, ! west), "reverse", "normal")); ## Share the data axes with the scatter and tidy the marginal axes set (ax_x, "xlim", get (ax_s, "xlim"), "xtick", [], "ytick", []); set (ax_y, "ylim", get (ax_s, "ylim"), "xtick", [], "ytick", []); axis (ax_x, "off"); axis (ax_y, "off"); ## Legend show_legend = (! isempty (group)) && ! strcmpi (legend_opt, "off"); if (show_legend && k > 1) warning ("off", "Octave:legend:unimplemented-location", "local"); legend (ax_s, scat, gnames, "location", "best"); endif if (nargout > 0) h = [ax_s, ax_x, ax_y]; endif endfunction ## Choose between two values without an if block. function v = ternary (cond, a, b) if (cond) v = a; else v = b; endif endfunction ## Pick the bandwidth, line style and line width for group G and variable V ## (1 for x, 2 for y), cycling whatever the user supplied. function sy = sty (bandwidth, linestyle, linewidth, g, v) if (isempty (bandwidth)) sy.bw = []; elseif (isscalar (bandwidth)) sy.bw = bandwidth; elseif (isrow (bandwidth)) sy.bw = bandwidth(mod (v - 1, numel (bandwidth)) + 1); else row = bandwidth(mod (g - 1, rows (bandwidth)) + 1, :); sy.bw = row(mod (v - 1, numel (row)) + 1); endif sy.ls = linestyle{mod (g - 1, numel (linestyle)) + 1}; sy.lw = linewidth(mod (g - 1, numel (linewidth)) + 1); endfunction ## Map a colour name to its RGB triplet. function rgb = colour_rgb (c) switch (lower (c)) case "r", rgb = [1, 0, 0]; case "g", rgb = [0, 1, 0]; case "b", rgb = [0, 0, 1]; case "c", rgb = [0, 1, 1]; case "m", rgb = [1, 0, 1]; case "y", rgb = [1, 1, 0]; case "k", rgb = [0, 0, 0]; case "w", rgb = [1, 1, 1]; otherwise error ("scatterhist: unknown colour '%s'.", c); endswitch endfunction ## Scott's rule for the number of histogram bins. function nb = scott_nbins (v) v = v(! isnan (v)); n = numel (v); if (n < 2) nb = 1; return; endif bw = 3.5 * std (v) * n ^ (-1/3); if (bw <= 0) nb = 1; else nb = max (1, ceil ((max (v) - min (v)) / bw)); endif endfunction ## Draw one group's marginal, either as a histogram or a kernel density. ## When horiz is true the marginal runs along the vertical (y) axis. function marginal (ax, v, nb, col, do_kernel, multi, horiz, sy) if (isempty (v)) return; endif if (do_kernel) if (isempty (sy.bw)) [f, u] = ksdensity (v); else [f, u] = ksdensity (v, "Bandwidth", sy.bw); endif if (horiz) line (ax, f, u, "color", col, "linestyle", sy.ls, "linewidth", sy.lw); else line (ax, u, f, "color", col, "linestyle", sy.ls, "linewidth", sy.lw); endif else [nn, cc] = hist (v, nb); if (multi) ## stairstep outline e = cc(1:end-1) + diff (cc) / 2; ce = [cc(1)-(cc(2)-cc(1))/2, e, cc(end)+(cc(end)-cc(end-1))/2]; ne = [nn, nn(end)]; if (horiz) stairs (ax, ne, ce, "color", col, "linestyle", sy.ls, ... "linewidth", sy.lw); else stairs (ax, ce, ne, "color", col, "linestyle", sy.ls, ... "linewidth", sy.lw); endif else if (horiz) barh (ax, cc, nn, 1.0, "facecolor", col, "edgecolor", col, ... "linestyle", sy.ls, "linewidth", sy.lw); else bar (ax, cc, nn, 1.0, "facecolor", col, "edgecolor", col, ... "linestyle", sy.ls, "linewidth", sy.lw); endif endif endif endfunction %!demo %! ## Scatter plot of two iris measurements with marginal histograms by species. %! %! load fisheriris; %! scatterhist (meas(:,1), meas(:,2), "Group", species); %!demo %! ## Marginal kernel density estimates instead of histograms. %! %! load fisheriris; %! scatterhist (meas(:,3), meas(:,4), "Group", species, "Kernel", "on"); ## Test output %!test # LineWidth and LineStyle apply to the marginals and cycle over groups %! hf = figure ("visible", "off"); %! unwind_protect %! x = [1 2 3 4 5 6 7 8]'; y = [2 1 4 3 6 5 8 7]'; %! g = [1 1 1 1 2 2 2 2]'; %! h = scatterhist (x, y, "Group", g, "LineWidth", 3); %! ch = get (h(2), "children"); %! assert_equal (get (ch(1), "linewidth"), 3); %! assert_equal (get (ch(2), "linewidth"), 3); %! h = scatterhist (x, y, "Group", g, "LineStyle", {"--", ":"}); %! ch = get (h(2), "children"); %! assert_equal (get (ch(2), "linestyle"), "--"); %! assert_equal (get (ch(1), "linestyle"), ":"); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # Bandwidth is passed to the kernel density %! hf = figure ("visible", "off"); %! unwind_protect %! x = [1 2 3 4 5 6 7 8]'; y = [2 1 4 3 6 5 8 7]'; %! h = scatterhist (x, y, "Kernel", "on", "Bandwidth", 2); %! wide = get (get (h(2), "children")(1), "ydata"); %! h = scatterhist (x, y, "Kernel", "on"); %! auto = get (get (h(2), "children")(1), "ydata"); %! assert_equal (! isequal (wide, auto), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!error ... %! scatterhist ([1 2 3]', [1 2 3]', "LineWidth", -1) %!error ... %! scatterhist ([1 2 3]', [1 2 3]', "LineStyle", 5) %!error ... %! scatterhist ([1 2 3]', [1 2 3]', "Bandwidth", 0) %!test # Parent draws into a supplied container without clearing it %! hf = figure ("visible", "off"); %! unwind_protect %! x = [1 2 3 4 5 6 7 8]'; y = [2 1 4 3 6 5 8 7]'; %! pre = axes ("parent", hf, "position", [0.01, 0.01, 0.05, 0.05]); %! h = scatterhist (x, y, "Parent", hf); %! assert_equal (all (arrayfun (@(a) get (a, "parent"), h) == hf, 'all'), ... %! true); %! ## the caller's own axes are left alone %! assert_equal (ishghandle (pre), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a uipanel is a container too %! hf = figure ("visible", "off"); %! unwind_protect %! x = [1 2 3 4 5 6 7 8]'; y = [2 1 4 3 6 5 8 7]'; %! hp = uipanel ("parent", hf, "position", [0, 0, 1, 1]); %! h = scatterhist (x, y, "Parent", hp); %! assert_equal (all (arrayfun (@(a) get (a, "parent"), h) == hp, 'all'), ... %! true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # without Parent the current figure is still claimed and cleared %! hf = figure ("visible", "off"); %! unwind_protect %! x = [1 2 3 4 5 6 7 8]'; y = [2 1 4 3 6 5 8 7]'; %! old = axes ("parent", hf); %! h = scatterhist (x, y); %! assert_equal (ishghandle (old), false); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!error ... %! scatterhist ([1 2 3]', [1 2 3]', "Parent", 0) %!error ... %! scatterhist ([1 2 3]', [1 2 3]', "Parent", -5) %!test # Location names the marginals' corner, so the scatter takes the other %! hf = figure ("visible", "off"); %! unwind_protect %! x = [1 2 3 4 5 6 7 8]'; y = [2 1 4 3 6 5 8 7]'; %! ## Offsets and size are R2024a's own, measured. %! h = scatterhist (x, y, "Location", "SouthWest"); %! ps = get (h(1), "position"); px = get (h(2), "position"); %! assert_equal (ps(1:2), [0.35, 0.35], 1e-12); %! assert_equal (ps(3:4), [0.55, 0.55], 1e-12); %! assert_equal (px(2) < ps(2), true); # x marginal below the scatter %! h = scatterhist (x, y, "Location", "NorthEast"); %! ps = get (h(1), "position"); px = get (h(2), "position"); %! assert_equal (ps(1:2), [0.10, 0.10], 1e-12); %! assert_equal (px(2) > ps(2), true); # and above it here %! ## The y marginal follows the named corner in the same way. %! h = scatterhist (x, y, "Location", "SouthEast"); %! ps = get (h(1), "position"); py = get (h(3), "position"); %! assert_equal (ps(1:2), [0.10, 0.35], 1e-12); %! assert_equal (py(1) > ps(1), true); # y marginal right of scatter %! h = scatterhist (x, y, "Location", "NorthWest"); %! ps = get (h(1), "position"); py = get (h(3), "position"); %! assert_equal (ps(1:2), [0.35, 0.10], 1e-12); %! assert_equal (py(1) < ps(1), true); # and left of it here %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # Direction is relative to where Location put each marginal %! hf = figure ("visible", "off"); %! unwind_protect %! x = [1 2 3 4 5 6 7 8]'; y = [2 1 4 3 6 5 8 7]'; %! ## "in" points the bars at the scatter from whichever side they sit on. %! ## The x marginal's ydir is R2024a's, measured in all four corners; the y %! ## marginal's xdir is ours, MATLAB rotating that axes instead (see the %! ## note in the file header). %! h = scatterhist (x, y, "Location", "SouthWest"); %! assert_equal (get (h(2), "ydir"), "normal"); %! assert_equal (get (h(3), "xdir"), "normal"); %! h = scatterhist (x, y, "Location", "NorthEast"); %! assert_equal (get (h(2), "ydir"), "reverse"); %! assert_equal (get (h(3), "xdir"), "reverse"); %! h = scatterhist (x, y, "Location", "SouthEast"); %! assert_equal (get (h(2), "ydir"), "normal"); %! assert_equal (get (h(3), "xdir"), "reverse"); %! h = scatterhist (x, y, "Location", "NorthWest"); %! assert_equal (get (h(2), "ydir"), "reverse"); %! assert_equal (get (h(3), "xdir"), "normal"); %! ## and "out" inverts each of them %! h = scatterhist (x, y, "Location", "NorthEast", "Direction", "out"); %! assert_equal (get (h(2), "ydir"), "normal"); %! assert_equal (get (h(3), "xdir"), "normal"); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # Direction points the marginals toward the scatter by default %! hf = figure ("visible", "off"); %! unwind_protect %! x = [1 2 3 4 5 6 7 8]'; y = [2 1 4 3 6 5 8 7]'; %! h = scatterhist (x, y); %! assert_equal (get (h(2), "ydir"), "normal"); %! assert_equal (get (h(3), "xdir"), "normal"); %! h = scatterhist (x, y, "Direction", "out"); %! assert_equal (get (h(2), "ydir"), "reverse"); %! assert_equal (get (h(3), "xdir"), "reverse"); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # Color sets the group colours, cycling if fewer than the groups %! hf = figure ("visible", "off"); %! unwind_protect %! x = [1 2 3 4 5 6 7 8]'; y = [2 1 4 3 6 5 8 7]'; %! g = [1 1 1 1 2 2 2 2]'; %! h = scatterhist (x, y, "Group", g, "Color", "rb"); %! c = get (get (h(1), "children"), "color"); %! assert_equal (c{2}, [1, 0, 0]); %! assert_equal (c{1}, [0, 0, 1]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # PlotGroup pools the marginals when off %! hf = figure ("visible", "off"); %! unwind_protect %! x = [1 2 3 4 5 6 7 8]'; y = [2 1 4 3 6 5 8 7]'; %! g = [1 1 1 1 2 2 2 2]'; %! h = scatterhist (x, y, "Group", g, "PlotGroup", "off"); %! assert_equal (numel (get (h(2), "children")), 1); %! h = scatterhist (x, y, "Group", g, "PlotGroup", "on"); %! assert_equal (numel (get (h(2), "children")), 2); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!error ... %! scatterhist ([1 2 3]', [1 2 3]', "NBins", 0) %!error ... %! scatterhist ([1 2 3]', [1 2 3]', "NBins", 2.5) %!error ... %! scatterhist ([1 2 3]', [1 2 3]', "NBins", [2, 3, 4]) %!error ... %! scatterhist ([1 2 3]', [1 2 3]', "Kernel", "sometimes") %!error ... %! scatterhist ([1 2 3]', [1 2 3]', "Kernel", 1) %!error ... %! scatterhist ([1 2 3]', [1 2 3]', "Location", "middle") %!error ... %! scatterhist ([1 2 3]', [1 2 3]', "Legend", "maybe") %!error ... %! scatterhist ([1 2 3]', [1 2 3]', "Marker", 7) %!error ... %! scatterhist ([1 2 3]', [1 2 3]', "MarkerSize", 0) %!error ... %! scatterhist ([1 2 3]', [1 2 3]', "Color", [1, 2]) %!error ... %! scatterhist ([1 2 3]', [1 2 3]', "Color", "q") %!error ... %! scatterhist ([1 2 3]', [1 2 3]', "Direction", "sideways") %!error ... %! scatterhist ([1 2 3]', [1 2 3]', "Style", "curvy") %!error ... %! scatterhist ([1 2 3]', [1 2 3]', "PlotGroup", "maybe") %!test %! hf = figure ("visible", "off"); %! unwind_protect %! x = [2.1 3.4 1.9 5.6 4.2 3.3 2.8 6.1 4.9 3.7 2.2 5.1 4.4 3.9 2.6]'; %! y = [1.2 2.4 3.1 2.6 4.5 3.3 5.1 2.8 4.0 3.6 1.9 4.4 3.2 2.1 5.0]'; %! h = scatterhist (x, y); %! assert_equal (numel (h), 3); %! assert_equal (all (isaxes (h), 'all'), true); %! ## the scatter axes hold the data %! sc = get (h(1), "children"); %! assert_equal (get (sc(1), "xdata")(:), x, 1e-12); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # grouped scatterhist runs and returns three axes %! hf = figure ("visible", "off"); %! unwind_protect %! x = [1 2 3 4 5 6]'; %! y = [2 1 4 3 6 5]'; %! g = [1 1 1 2 2 2]'; %! h = scatterhist (x, y, "Group", g); %! assert_equal (numel (h), 3); %! assert_equal (all (isaxes (h), 'all'), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # kernel option runs %! hf = figure ("visible", "off"); %! unwind_protect %! x = randn (50, 1); %! y = randn (50, 1); %! h = scatterhist (x, y, "Kernel", "on"); %! assert_equal (numel (h), 3); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # NBins accepts a two-element specification %! hf = figure ("visible", "off"); %! unwind_protect %! x = randn (40, 1); %! y = randn (40, 1); %! h = scatterhist (x, y, "NBins", [5 8]); %! assert_equal (numel (h), 3); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error scatterhist (1) %!error ... %! scatterhist ({1}, {2}) %!error ... %! scatterhist ([1 2 3], [1 2]) %!error ... %! scatterhist ([1 2 3], [1 2 3], "Group") %!error ... %! scatterhist ([1 2 3], [1 2 3], "bogus", 1) %!error ... %! scatterhist ([1 2 3], [1 2 3], "Group", [1 2]) statistics-release-2.1.2/inst/Plotting/scatterhistogram.m000066400000000000000000000241121527311104300236370ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} scatterhistogram (@var{x}, @var{y}) ## @deftypefnx {statistics} {} scatterhistogram (@var{tbl}, @var{xvar}, @var{yvar}) ## @deftypefnx {statistics} {} scatterhistogram (@var{parent}, @dots{}) ## @deftypefnx {statistics} {} scatterhistogram (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {@var{h} =} scatterhistogram (@dots{}) ## ## Scatter plot of two samples with the histogram of each along its sides. ## ## @code{scatterhistogram (@var{x}, @var{y})} draws the points ## @code{(@var{x}(i), @var{y}(i))} with a histogram of @var{x} above the ## scatter plot and a histogram of @var{y} to its right, each normalized as a ## probability density, with the bins @code{histcounts} chooses. @var{x} and ## @var{y} are numeric vectors of the same length; a missing value is left out ## of the drawing. ## ## @code{scatterhistogram (@var{tbl}, @var{xvar}, @var{yvar})} takes the ## samples from the variables @var{xvar} and @var{yvar} of the table ## @var{tbl}, and labels the axes with their names. ## ## With @qcode{'GroupData'}, a vector of categories, text, numbers or logical ## values with one element for each observation, or @qcode{'GroupVariable'}, ## a variable of @var{tbl} holding them, each group is drawn in a colour of ## its own, with histograms of its own and a legend naming the groups in the ## order in which each first appears. ## ## @code{scatterhistogram (@var{parent}, @dots{})} places the chart in the ## figure or panel @var{parent} rather than the current figure. The chart ## takes the place of the current axes of its parent, and is refused where ## @code{hold} is on for them. ## ## @code{scatterhistogram (@dots{}, @var{Name}, @var{Value})} sets ## properties of the chart; see @code{stats.chart.ScatterHistogramChart} for ## all of them, among which @qcode{'HistogramDisplayStyle'}, ## @qcode{'stairs'}, @qcode{'bar'} or @qcode{'smooth'} for a kernel density ## estimate, @qcode{'NumBins'}, @qcode{'BinWidths'} and ## @qcode{'ScatterPlotLocation'}. ## ## @code{@var{h} = scatterhistogram (@dots{})} returns the chart, a ## @code{stats.chart.ScatterHistogramChart} object. ## ## @seealso{stats.chart.ScatterHistogramChart, scatterhist, histcounts, ## ksdensity} ## @end deftypefn function varargout = scatterhistogram (varargin) if (nargin < 1) print_usage (); endif ## Input validation parent = []; args = varargin; if (isscalar (args{1}) && ! istable (args{1}) && isnumeric (args{1}) && ishghandle (args{1}) && numel (args) > 1 && ! isName (args{2}) && ! isscalar (args{2})) type = get (args{1}, 'type'); if (strcmp (type, 'axes')) error ("scatterhistogram: a scatter histogram cannot be placed in axes."); elseif (! any (strcmp (type, {'figure', 'uipanel'}))) error ("scatterhistogram: PARENT must be a figure or a panel."); endif parent = args{1}; args(1) = []; endif spec = struct ('XData', [], 'YData', [], 'GroupData', [], ... 'SourceTable', [], 'XVariable', '', 'YVariable', '', ... 'GroupVariable', ''); if (numel (args) > 0 && istable (args{1})) if (numel (args) < 3 || ! isName (args{2}) || ! isName (args{3})) error (strcat ("scatterhistogram: a table must be followed by two", ... " variable names.")); endif spec.SourceTable = args{1}; spec.XVariable = char (args{2}); spec.YVariable = char (args{3}); rest = args(4:end); else if (numel (args) < 2 || ! isSample (args{1}) || ! isSample (args{2})) error (strcat ("scatterhistogram: the data must be two numeric", ... " vectors or a table and two variable names.")); endif if (numel (args{1}) != numel (args{2})) error ("scatterhistogram: X and Y must be vectors of the same length."); endif spec.XData = args{1}; spec.YData = args{2}; rest = args(3:end); endif ## Name-value pairs, the names resolved to the properties they set if (mod (numel (rest), 2) != 0) error (strcat ("scatterhistogram: optional arguments must be in Name,", ... " Value pairs.")); endif props = {'XData', 'YData', 'GroupData', 'SourceTable', 'XVariable', ... 'YVariable', 'GroupVariable', 'HistogramDisplayStyle', ... 'NumBins', 'BinWidths', 'ScatterPlotLocation', ... 'ScatterPlotProportion', 'XHistogramDirection', ... 'YHistogramDirection', 'XLimits', 'YLimits', 'Color', ... 'LineStyle', 'LineWidth', 'MarkerStyle', 'MarkerSize', ... 'MarkerFilled', 'MarkerAlpha', 'LegendVisible', 'LegendTitle', ... 'Title', 'XLabel', 'YLabel', 'FontName', 'FontSize', 'Position', ... 'InnerPosition', 'OuterPosition', 'PositionConstraint', 'Units', ... 'Visible'}; for k = 1:2:numel (rest) i = []; if (isName (rest{k})) i = find (strcmpi (char (rest{k}), props), 1); endif if (isempty (i)) error ("scatterhistogram: invalid optional paired argument."); endif rest{k} = props{i}; endfor ## The groups belong with the data, so that the chart reads them at once for f = {'GroupData', 'GroupVariable'} k = find (strcmp (rest(1:2:end), f{1}), 1, 'last'); if (! isempty (k)) if (strcmp (f{1}, 'GroupVariable') && isempty (spec.SourceTable)) error ("scatterhistogram: 'GroupVariable' needs a table."); endif spec.(f{1}) = rest{2*k}; rest(2*k-1:2*k) = []; endif endfor h = stats.chart.ScatterHistogramChart (parent, spec, rest); ## The chart is handed back only where it was asked for if (nargout > 0) varargout{1} = h; endif endfunction ## A name: a character vector or a string scalar function tf = isName (v) tf = (ischar (v) && isrow (v)) || (isa (v, 'string') && isscalar (v)); endfunction ## A sample: a real numeric or logical vector function tf = isSample (v) tf = (isnumeric (v) || islogical (v)) && isreal (v) ... && (isvector (v) || isempty (v)); endfunction %!demo %! ## Two measurements of the same flowers, and how each is spread. %! load fisheriris %! scatterhistogram (meas(:,1), meas(:,2)); %! xlabel (''); %!demo %! ## Grouped by species, each with its own histograms and colour. %! load fisheriris %! scatterhistogram (meas(:,3), meas(:,4), 'GroupData', species, ... %! 'XLabel', 'Petal length', 'YLabel', 'Petal width'); %!demo %! ## The same as kernel density estimates, the histograms below and to the %! ## left of the scatter plot. %! load fisheriris %! scatterhistogram (meas(:,3), meas(:,4), 'GroupData', species, ... %! 'HistogramDisplayStyle', 'smooth', ... %! 'ScatterPlotLocation', 'NorthEast'); %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! scatterhistogram ([1, 2, 3], [4, 5, 7]); %! tag = 'stats.chart.ScatterHistogramChart'; %! ax = findall (hf, 'type', 'axes', 'tag', tag); %! assert_equal (numel (ax), 3); %! assert_equal (ismember (gca (), ax), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! t = table ([1; 2; 3; 4], [2; 4; 1; 3], {'a'; 'b'; 'a'; 'b'}, ... %! 'VariableNames', {'X', 'Y', 'G'}); %! h = scatterhistogram (t, 'X', 'Y', 'GroupVariable', 'G'); %! assert_equal (h.XLabel, 'X'); %! assert_equal (h.YLabel, 'Y'); %! assert_equal (h.LegendTitle, 'G'); %! assert_equal (h.GroupData, {'a'; 'b'; 'a'; 'b'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! plot (1:3); %! pax = gca (); %! h = scatterhistogram ([1, 2, 3], [4, 5, 7]); %! assert_equal (ishghandle (pax), false); %! plot (1:3); %! assert_equal (isempty (h.Parent), true); %! tag = 'stats.chart.ScatterHistogramChart'; %! assert_equal (isempty (findall (hf, 'tag', tag)), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!error scatterhistogram () %!error ... %! scatterhistogram ([1, 2, 3]) %!error ... %! scatterhistogram ('a', 'b') %!error ... %! scatterhistogram ([1, 2, 3], [1, 2]) %!error ... %! scatterhistogram (table ([1; 2]), 'Var1') %!error ... %! scatterhistogram ([1, 2, 3], [4, 5, 6], 'Title') %!error ... %! scatterhistogram ([1, 2, 3], [4, 5, 6], 'Foo', 1) %!error ... %! scatterhistogram ([1, 2, 3], [4, 5, 6], 'GroupVariable', 'G') %!error ... %! hf = figure ('visible', 'off'); %! unwind_protect %! scatterhistogram (axes (hf), [1, 2, 3], [4, 5, 6]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!error ... %! hf = figure ('visible', 'off'); %! unwind_protect %! plot (1:3); %! hold on; %! scatterhistogram ([1, 2, 3], [4, 5, 6]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect statistics-release-2.1.2/inst/Plotting/silhouette.m000066400000000000000000000207451527311104300224510ustar00rootroot00000000000000## Copyright (C) 2016 Nan Zhou ## Copyright (C) 2021 Stefano Guidoni ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} silhouette (@var{X}, @var{clust}) ## @deftypefnx {statistics} {[@var{si}, @var{h}] =} silhouette (@var{X}, @var{clust}) ## @deftypefnx {statistics} {[@var{si}, @var{h}] =} silhouette (@dots{}, @var{Metric}, @var{MetricArg}) ## ## Compute the silhouette values of clustered data and show them on a plot. ## ## @var{X} is a n-by-p matrix of n data points in a p-dimensional space. Each ## datapoint is assigned to a cluster using @var{clust}, a vector of n elements, ## one cluster assignment for each data point. ## ## Each silhouette value of @var{si}, a vector of size n, is a measure of the ## likelihood that a data point is accurately classified to the right cluster. ## Defining "a" as the mean distance between a point and the other points from ## its cluster, and "b" as the mean distance between that point and the points ## from other clusters, the silhouette value of the i-th point is: ## ## @tex ## \def\frac#1#2{{\begingroup#1\endgroup\over#2}} ## $$ S_i = \frac{b_i - a_i}{max(a_1,b_i)} $$ ## @end tex ## @ifnottex ## @verbatim ## bi - ai ## Si = ------------ ## max(ai,bi) ## @end verbatim ## @end ifnottex ## ## Each element of @var{si} ranges from -1, minimum likelihood of a correct ## classification, to 1, maximum likelihood. ## ## Optional input value @var{Metric} is the metric used to compute the distances ## between data points. Since @code{silhouette} uses @code{pdist} to compute ## these distances, @var{Metric} is similar to the @var{Distance} input argument ## of @code{pdist} and it can be: ## @itemize @bullet ## @item A known distance metric defined as a string: @qcode{euclidean}, ## @qcode{squaredeuclidean} (default), @qcode{seuclidean}, @qcode{mahalanobis}, ## @qcode{cityblock}, @qcode{minkowski}, @qcode{chebychev}, @qcode{cosine}, ## @qcode{correlation}, @qcode{hamming}, @qcode{jaccard}, or @qcode{spearman}. ## ## @item A vector as those created by @code{pdist}. In this case @var{X} does ## nothing. ## ## @item A function handle that is passed to @code{pdist} with @var{MetricArg} ## as optional inputs. ## @end itemize ## ## Optional return value @var{h} is a handle to the silhouette plot. ## ## @strong{Reference} ## Peter J. Rousseeuw, Silhouettes: a Graphical Aid to the Interpretation and ## Validation of Cluster Analysis. 1987. doi:10.1016/0377-0427(87)90125-7 ## ## @seealso{dendrogram, evalclusters, kmeans, linkage, pdist} ## @end deftypefn function [si, h] = silhouette (X, clust, metric = 'squaredeuclidean', varargin) ## check the input parameters if (nargin < 2) print_usage (); endif ## Check for last argument being 'DoNotPlot' to prevent from opening a figure ## Undocumented feature to avoid issues with failing tests. It is only used by ## SilhouetteEvaluation class. DisplayPlot = true; if (numel (varargin) > 0) if (ischar (varargin{end})) if (strcmp (varargin{end}, 'DoNotPlot')) DisplayPlot = false; varargin{end} = []; endif endif endif n = size (clust, 1); ## check size if (! isempty (X)) if (size (X, 1) != n) error ("First dimension of X <%d> doesn't match that of clust <%d>",... size (X, 1), n); endif endif ## check metric if (ischar (metric)) metric = lower (metric); switch (metric) case 'sqeuclidean' metric = 'squaredeuclidean'; case {'euclidean', 'squaredeuclidean', 'seuclidean', 'mahalanobis', ... 'cityblock', 'minkowski', 'chebychev', 'cosine', 'correlation', ... 'hamming', 'jaccard', 'spearman'} ; otherwise error ("silhouette: invalid metric '%s'", metric); endswitch elseif (isnumeric (metric) && isvector (metric)) ## X can be omitted when using this distMatrix = squareform (metric); if (size (distMatrix, 1) != n) error ("First dimension of X <%d> doesn't match that of clust <%d>",... size (distMatrix, 1), n); endif endif ## main si = zeros (n, 1); clusterIDs = unique (clust); # eg [1; 2; 3; 4] m = length (clusterIDs); ## if only one cluster is defined, the silhouette value is not defined if (m == 1) si = NaN * ones (n, 1); return; endif ## Precompute cluster membership masks and counts clusterMask = false (n, m); clusterCount = zeros (m, 1); for jjj = 1:m clusterMask(:, jjj) = (clust == clusterIDs(jjj)); clusterCount(jjj) = sum (clusterMask(:, jjj)); endfor ## Map each point to its cluster index (1..m) pointClusterIdx = zeros (n, 1); for jjj = 1:m pointClusterIdx(clusterMask(:, jjj)) = jjj; endfor ## Use precomputed distance matrix if provided, otherwise compute on-the-fly if (exist ('distMatrix', 'var')) ## Use full precomputed distance matrix for iii = 1:n myCluster = pointClusterIdx(iii); myMask = clusterMask(:, myCluster); myCount = clusterCount(myCluster); ## Singleton cluster if (myCount == 1) si(iii) = 1; continue; endif ## a(i): mean distance to own cluster (excluding self) a_i = sum (distMatrix(iii, myMask)) / (myCount - 1); ## b(i): minimum mean distance to other clusters b_i = Inf; for jjj = 1:m if (jjj != myCluster) b_i = min (b_i, mean (distMatrix(iii, clusterMask(:, jjj)))); endif endfor ## s(i) si(iii) = (b_i - a_i) / max (a_i, b_i); endfor else ## Compute distances row-by-row to avoid O(n^2) memory for iii = 1:n ## Compute distances from point iii to all other points diffs = X - X(iii, :); switch (metric) case 'squaredeuclidean' dists = sum (diffs .^ 2, 2); case 'euclidean' dists = sqrt (sum (diffs .^ 2, 2)); case 'cityblock' dists = sum (abs (diffs), 2); case 'chebychev' dists = max (abs (diffs), [], 2); otherwise ## Fall back to pdist2-style computation for other metrics dists = pdist2 (X(iii, :), X, metric, varargin{:})'; endswitch myCluster = pointClusterIdx(iii); myMask = clusterMask(:, myCluster); myCount = clusterCount(myCluster); ## Singleton cluster if (myCount == 1) si(iii) = 1; continue; endif ## a(i): mean distance to own cluster (excluding self, dist=0) a_i = sum (dists(myMask)) / (myCount - 1); ## b(i): minimum mean distance to other clusters b_i = Inf; for jjj = 1:m if (jjj != myCluster) b_i = min (b_i, mean (dists(clusterMask(:, jjj)))); endif endfor ## s(i) si(iii) = (b_i - a_i) / max (a_i, b_i); endfor endif ## plot ## a poor man silhouette graph if (DisplayPlot) vBarsc = zeros (m, 1); vPadding = [0; 0; 0; 0]; Bars = vPadding; for i = 1 : m vBar = si(find (clust == clusterIDs(i))); vBarsc(i) = length (Bars) + (length (vBar) / 2); Bars = [Bars; (sort (vBar, 'descend')); vPadding]; endfor figure (); h = barh (Bars, 'hist', 'facecolor', [0 0.4471 0.7412]); xlabel ('Silhouette Value'); ylabel ('Cluster'); set (gca, 'ytick', vBarsc, 'yticklabel', clusterIDs); ylim ([0 (length (Bars))]); axis ('ij'); endif endfunction %!demo %! load fisheriris; %! X = meas(:,3:4); %! cidcs = kmeans (X, 3, 'Replicates', 5); %! silhouette (X, cidcs); %! y_labels(cidcs([1 51 101])) = unique (species); %! set (gca, 'yticklabel', y_labels); %! title ('Fisher''s iris data'); ## Test input validation %!error silhouette (); %!error silhouette ([1 2; 1 1]); %!error silhouette ([1 2; 1 1], [1 2 3]'); %!error silhouette ([1 2; 1 1], [1 2]', 'xxx'); statistics-release-2.1.2/inst/Plotting/swarmchart.m000066400000000000000000000336121527311104300224340ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} swarmchart (@var{x}, @var{y}) ## @deftypefnx {statistics} {} swarmchart (@var{x}, @var{y}, @var{sz}) ## @deftypefnx {statistics} {} swarmchart (@var{x}, @var{y}, @var{sz}, @var{c}) ## @deftypefnx {statistics} {} swarmchart (@dots{}, @qcode{'filled'}) ## @deftypefnx {statistics} {} swarmchart (@dots{}, @var{name}, @var{value}) ## @deftypefnx {statistics} {} swarmchart (@var{ax}, @dots{}) ## @deftypefnx {statistics} {@var{s} =} swarmchart (@dots{}) ## ## Draw a swarm chart. ## ## @code{swarmchart (@var{x}, @var{y})} draws the points of @var{y} against ## @var{x}, spread sideways where they would otherwise sit on top of one ## another, so that the shape of each column shows where its values gather. ## @var{sz} sets the marker area and @var{c} its colour, as they do for ## @code{scatter}, and @qcode{'filled'} fills the markers. ## ## @var{s} is the scatter object drawn, and carries @code{XJitter}, ## @code{YJitter}, @code{XJitterWidth} and @code{YJitterWidth} besides ## everything a scatter object carries. Setting any of them spreads the ## points again and redraws. ## ## @multitable @columnfractions 0.24 0.02 0.74 ## @headitem @var{Name} @tab @tab @var{Value} ## ## @item @qcode{'XJitter'} @tab @tab How the points are spread sideways: ## @qcode{'density'}, the default here, spreads each point by a uniform draw ## weighted by how many of its neighbours share its value, so a crowded part ## of a column spreads wide and a lone point hardly moves; ## @qcode{'rand'} spreads every point alike; @qcode{'randn'} draws the ## offsets from a normal distribution; @qcode{'none'} leaves them in a line. ## ## @item @qcode{'YJitter'} @tab @tab The same, upwards. The default is ## @qcode{'none'}. ## ## @item @qcode{'XJitterWidth'} @tab @tab How far a point may be moved ## sideways, in the units of the axis. The default is nine tenths of the ## smallest gap between distinct values of @var{x}. ## ## @item @qcode{'YJitterWidth'} @tab @tab The same, upwards. ## @end multitable ## ## MATLAB leaves @code{XData} as it was given and spreads the points as it ## draws them. Octave's scatter object has no such step, so this moves the ## values themselves: @code{XData} holds where each point was drawn rather ## than where it came from. The difference shows in @code{XData} and ## nowhere else, the chart being the same. ## ## @seealso{scatter, boxchart, gscatter} ## @end deftypefn function varargout = swarmchart (varargin) if (nargin < 2) print_usage (); endif ax = []; if (isscalar (varargin{1}) && ishghandle (varargin{1}) && strcmp (get (varargin{1}, 'type'), 'axes')) ax = varargin{1}; varargin(1) = []; endif if (numel (varargin) < 2) print_usage (); endif x = varargin{1}; y = varargin{2}; rest = varargin(3:end); if (! (isnumeric (x) && isreal (x) && isnumeric (y) && isreal (y))) error ("swarmchart: X and Y must be real numeric."); endif x = double (x(:)); y = double (y(:)); if (numel (x) != numel (y)) error ("swarmchart: X and Y must hold the same number of points."); endif ## Whatever scatter itself takes comes first, then the pairs of our own [opts, rest] = scJitterArgs (rest, x, y); if (isempty (ax)) ax = gca (); endif xd = scSpread (x, y, opts.XJitter, opts.XJitterWidth); yd = scSpread (y, x, opts.YJitter, opts.YJitterWidth); s = scatter (ax, xd, yd, rest{:}); ## The jitter is ours to carry: Octave's scatter has no such properties, ## and a listener on each lets the points be spread again after the fact addproperty ('XJitter', s, 'string', opts.XJitter); addproperty ('YJitter', s, 'string', opts.YJitter); addproperty ('XJitterWidth', s, 'double', opts.XJitterWidth); addproperty ('YJitterWidth', s, 'double', opts.YJitterWidth); for name = {'XJitter', 'YJitter', 'XJitterWidth', 'YJitterWidth'} addlistener (s, name{1}, @(h, ~) scRespread (h, x, y)); endfor ## The handle is handed back only where it was asked for, so a call made ## for the drawing alone prints nothing if (nargout > 0) varargout{1} = s; endif endfunction ## Spread the points again from where they started. function scRespread (h, x, y) set (h, 'XData', scSpread (x, y, get (h, 'XJitter'), ... get (h, 'XJitterWidth'))); set (h, 'YData', scSpread (y, x, get (h, 'YJitter'), ... get (h, 'YJitterWidth'))); endfunction ## Move each value off its column by as much as the kind of jitter allows. function out = scSpread (v, other, kind, width) out = v; if (strcmp (kind, 'none') || width <= 0) return; endif ## Every point sharing a column is spread against the others in it col = unique (v(! isnan (v))); for k = 1:numel (col) take = (v == col(k)); n = sum (take); if (n < 2) continue; endif switch (kind) case 'rand' off = (rand (n, 1) - 0.5) * width; case 'randn' off = randn (n, 1) * width / 6; otherwise w = scDensity (other(take)); off = (rand (n, 1) - 0.5) * width .* w; endswitch out(take) = v(take) + off; endfor endfunction ## How crowded each value is among its neighbours, from nothing to one. ## A Gaussian kernel with Silverman's bandwidth, which is what makes a dense ## part of a column spread wide and a lone point stay where it is. function w = scDensity (v) v = v(:); n = numel (v); s = std (v); if (! isfinite (s) || s == 0) w = ones (n, 1); return; endif h = 1.06 * s * n^(-1/5); if (h <= 0) w = ones (n, 1); return; endif d = (v - v') / h; w = sum (exp (-0.5 * d.^2), 2); w = w - min (w); if (max (w) > 0) w = w / max (w); else w = ones (n, 1); endif endfunction ## The pairs that belong to the jitter, taken out of what scatter is given. function [opts, rest] = scJitterArgs (in, x, y) opts = struct ('XJitter', 'density', 'YJitter', 'none', ... 'XJitterWidth', scDefaultWidth (x), ... 'YJitterWidth', scDefaultWidth (y)); keep = true (1, numel (in)); k = 1; while (k <= numel (in)) name = in{k}; if (ischar (name) && any (strcmpi (name, {'XJitter', 'YJitter', ... 'XJitterWidth', ... 'YJitterWidth'}))) if (k == numel (in)) error ("swarmchart: '%s' needs a value.", name); endif opts = scSetOne (opts, name, in{k+1}); keep(k:k+1) = false; k += 2; else k++; endif endwhile rest = in(keep); endfunction ## One jitter option, checked. function opts = scSetOne (opts, name, value) if (! isempty (strfind (lower (name), 'width'))) if (! (isnumeric (value) && isscalar (value) && isreal (value) && value >= 0)) error ("swarmchart: '%s' must be a nonnegative scalar.", name); endif value = double (value); else allowed = {'none', 'density', 'rand', 'randn'}; if (! (ischar (value) && isrow (value) && any (strcmpi (allowed, value)))) error (strcat ("swarmchart: '%s' must be 'none', 'density',", ... " 'rand' or 'randn'."), name); endif value = lower (value); endif fields = {'XJitter', 'YJitter', 'XJitterWidth', 'YJitterWidth'}; j = find (strcmpi (fields, name), 1); opts.(fields{j}) = value; endfunction ## Nine tenths of the smallest gap between distinct values, as MATLAB sets it. function w = scDefaultWidth (v) u = unique (v(! isnan (v))); if (numel (u) < 2) w = 0.9; else w = 0.9 * min (diff (u)); endif endfunction %!demo %! ## A swarm chart spreads the points of each column sideways, so the shape %! ## of a column shows where its values gather. A crowded part spreads %! ## wide and a lone point hardly moves. %! load fisheriris %! g = grp2idx (species); %! swarmchart (g, meas(:,1)); %! xlabel ('species'); %! ylabel ('sepal length'); %!demo %! ## A swarm sits over a box chart readily, the one showing the summary and %! ## the other every observation behind it. %! load fisheriris %! g = grp2idx (species); %! boxchart (g, meas(:,1)); %! hold on %! swarmchart (g, meas(:,1), 10, [0.3, 0.3, 0.3]); %! hold off %! xlabel ('species'); %! ylabel ('sepal length'); %!demo %! ## How far the points are spread, and by what rule, may be set afterwards. %! load fisheriris %! g = grp2idx (species); %! s = swarmchart (g, meas(:,1)); %! set (s, 'XJitterWidth', 0.3); %!shared scX, scY %! scX = [ones(8, 1); 2 * ones(8, 1)]; %! scY = [1; 1; 1; 2; 2; 3; 4; 9; 5; 5; 5; 6; 6; 7; 8; 20]; ## MATLAB parity: a scatter object carrying the jitter properties %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! s = swarmchart (scX, scY); %! assert_equal (size (get (s, 'cdata')), [1, 3]); %! ## Octave's scatter builds an hggroup under the gnuplot toolkit %! if (! strcmp (graphics_toolkit (), 'gnuplot')) %! assert_equal (get (s, 'type'), 'scatter'); %! endif %! assert_equal (get (s, 'XJitter'), 'density'); %! assert_equal (get (s, 'YJitter'), 'none'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # MATLAB parity: the default width is nine tenths of the smallest gap %! hf = figure ('visible', 'off'); %! unwind_protect %! s = swarmchart (scX, scY); %! assert_equal (get (s, 'XJitterWidth'), 0.9, 1e-12); %! s2 = swarmchart ([1; 1; 3; 3], [1; 2; 3; 4]); %! assert_equal (get (s2, 'XJitterWidth'), 1.8, 1e-12); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # a column with one value alone falls back to the fixed width %! hf = figure ('visible', 'off'); %! unwind_protect %! s = swarmchart (ones (5, 1), (1:5)'); %! assert_equal (get (s, 'XJitterWidth'), 0.9, 1e-12); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Every point stays in its own band, whatever the draw %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! s = swarmchart (scX, scY); %! xd = get (s, 'XData')(:); %! w = get (s, 'XJitterWidth'); %! assert_equal (numel (xd), numel (scX)); %! assert_equal (all (abs (xd - scX) <= w / 2 + 1e-12), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # the other axis is left alone where it is not jittered %! hf = figure ('visible', 'off'); %! unwind_protect %! s = swarmchart (scX, scY); %! assert_equal (get (s, 'YData')(:), scY); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # the points really are spread rather than left in two lines %! hf = figure ('visible', 'off'); %! unwind_protect %! s = swarmchart (scX, scY); %! xd = get (s, 'XData')(:); %! assert_equal (numel (unique (xd)) > 2, true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # asking for no jitter leaves the values exactly as they came %! hf = figure ('visible', 'off'); %! unwind_protect %! s = swarmchart (scX, scY, 'XJitter', 'none'); %! assert_equal (get (s, 'XData')(:), scX); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # setting the width spreads them again, into the narrower band %! hf = figure ('visible', 'off'); %! unwind_protect %! s = swarmchart (scX, scY); %! set (s, 'XJitterWidth', 0.2); %! xd = get (s, 'XData')(:); %! assert_equal (all (abs (xd - scX) <= 0.1 + 1e-12), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # and setting the kind to none puts them back where they started %! hf = figure ('visible', 'off'); %! unwind_protect %! s = swarmchart (scX, scY); %! set (s, 'XJitter', 'none'); %! assert_equal (get (s, 'XData')(:), scX); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # the y values may be spread instead, and then they move %! hf = figure ('visible', 'off'); %! unwind_protect %! s = swarmchart (scX, scY, 'XJitter', 'none', 'YJitter', 'rand', ... %! 'YJitterWidth', 0.5); %! assert_equal (get (s, 'XData')(:), scX); %! assert_equal (isequal (get (s, 'YData')(:), scY), false); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # the marker size and colour reach scatter as they would on their own %! hf = figure ('visible', 'off'); %! unwind_protect %! s = swarmchart (scX, scY, 25, [1, 0, 0]); %! assert_equal (get (s, 'SizeData'), 25); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test # the axes to draw into may be given first, as for any plot %! hf = figure ('visible', 'off'); %! unwind_protect %! a1 = subplot (1, 2, 1); %! a2 = subplot (1, 2, 2); %! s = swarmchart (a2, scX, scY); %! assert_equal (get (s, 'parent'), a2); %! assert_equal (isempty (get (a1, 'children')), true); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Input validation %!error swarmchart (1) %!error ... %! swarmchart ([1; 2], [1; 2; 3]) %!error ... %! swarmchart ({1, 2}, [1; 2]) %!error ... %! swarmchart ([1; 2], [1; 2], 'XJitter', 'sideways') %!error ... %! swarmchart ([1; 2], [1; 2], 'XJitterWidth', -1) %!error ... %! swarmchart ([1; 2], [1; 2], 'XJitter') statistics-release-2.1.2/inst/Plotting/violin.m000066400000000000000000000364601527311104300215650ustar00rootroot00000000000000## Copyright (C) 2016 Juan Pablo Carbajal ## Copyright (C) 2022-2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} violin (@var{x}) ## @deftypefnx {statistics} {@var{h} =} violin (@var{x}) ## @deftypefnx {statistics} {@var{h} =} violin (@dots{}, @var{property}, @var{value}, @dots{}) ## @deftypefnx {statistics} {@var{h} =} violin (@var{hax}, @dots{}) ## @deftypefnx {statistics} {@var{h} =} violin (@dots{}, @code{"horizontal"}) ## ## Produce a Violin plot of the data @var{x}. ## ## The input data @var{x} can be a N-by-m array containing N observations of m ## variables. It can also be a cell with m elements, for the case in which the ## variables are not uniformly sampled. ## ## The following @var{property} can be set using @var{property}/@var{value} ## pairs (default values in parenthesis). The value of the property can be a ## scalar indicating that it applies to all the variables in the data. It can ## also be a cell/array, indicating the property for each variable. In this case ## it should have m columns (as many as variables). ## ## @table @asis ## ## @item Color ## (@asis{"y"}) Indicates the filling color of the violins. ## ## @item Nbins ## (50) Internally, the function calls @command{hist} to compute the histogram ## of the data. This property indicates how many bins to use. ## See @command{help hist} for more details. ## ## @item SmoothFactor ## (4) The function performs simple kernel density estimation and automatically ## finds the bandwidth of the kernel function that best approximates the ## histogram using optimization (@command{sqp}). ## The result is in general very noisy. To smooth the result the bandwidth is ## multiplied by the value of this property. The higher the value the smoother ## the violins, but values too high might remove features from the data ## distribution. ## ## @item Bandwidth ## (NA) If this property is given a value other than NA, it sets the bandwidth ## of the kernel function. No optimization is performed and the property ## @asis{SmoothFactor} is ignored. ## ## @item Width ## (0.5) Sets the maximum width of the violins. Violins are centered at integer ## axis values. The distance between two violin middle axis is 1. Setting a ## value higher than 1 in this property will cause the violins to overlap. ## @end table ## ## If the string @asis{"Horizontal"} is among the input arguments, the violin ## plot is rendered along the x axis with the variables in the y axis. ## ## The returned structure @var{h} has handles to the plot elements, allowing ## customization of the visualization using set/get functions. ## ## Example: ## ## @example ## title ("Grade 3 heights"); ## axis ([0,3]); ## set (gca, "xtick", 1:2, "xticklabel", @{"girls"; "boys"@}); ## h = violin (@{randn(100,1)*5+140, randn(130,1)*8+135@}, "Nbins", 10); ## set (h.violin, "linewidth", 2) ## @end example ## ## @seealso{boxplot, hist} ## @end deftypefn function h = violin (ax, varargin) if (nargin < 1) print_usage (); endif # First argument is not an axis if (! ishandle (ax) || ! isscalar (ax)) x = ax; ax_given = false; else if (isempty (varargin)) print_usage (); endif x = varargin{1}; varargin(1) = []; ax_given = true; endif ###################### ## Parse parameters ## parser = inputParser (); parser.CaseSensitive = false; parser.FunctionName = 'violin'; parser.addParamValue ('Nbins', 50); parser.addParamValue ('SmoothFactor', 4); parser.addParamValue ('Bandwidth', NA); parser.addParamValue ('Width', 0.5); parser.addParamValue ('Color', 'y'); parser.addSwitch ('Horizontal'); parser.parse (varargin{:}); res = parser.Results; c = res.Color; # Color of violins if (ischar (c)) c = c(:); endif nb = res.Nbins; # Number of bins in histogram sf = res.SmoothFactor; # Smoothing factor for kernel estimation r0 = res.Bandwidth; # User value for KDE bandwidth to prevent optimization is_horiz = res.Horizontal; # Whether the plot must be rotated width = res.Width; # Width of the violins clear parser res ###################### ## Make everything a cell for code simplicity if (! iscell (x)) ## A vector is one variable however it is oriented, as in boxplot. Taking ## a row vector as many variables of a single observation each made every ## row vector fail inside the kernel estimate. if (isvector (x)) x = x(:); endif [N, Nc] = size (x); if (N == 0 || Nc == 0) error ("violin: X must not be empty."); endif x = mat2cell (x, N, ones (1, Nc)); else Nc = numel (x); if (Nc == 0) error ("violin: X must not be empty."); endif x = cellfun (@(v) v(:), x, 'UniformOutput', false); endif ## Integer and logical observations are perfectly good data; the kernel ## estimate only needs them in floating point, which is what var insists on. ## And a variable needs a spread for there to be a density at all. Both ## used to surface as an internal subscript or nonconformance error. if (! all (cellfun (@(v) isnumeric (v) || islogical (v), x))) error ("violin: X must be numeric or logical."); endif if (any (cellfun (@numel, x) < 2)) error ("violin: each variable in X needs at least two observations."); endif x = cellfun (@double, x, 'UniformOutput', false); ## Only now claim an axis. gca creates a figure when there is none, so ## doing it before the checks above left a rejected call -- violin ([]), ## violin (5), violin ('abc') -- with an empty figure open behind the error. if (! ax_given) ax = gca (); endif try [nb, c, sf, r0, width] = to_cell (nb, c, sf, r0, width, Nc); catch err if strcmp (err.identifier, 'to_cell:element_idx') n = str2num (err.message); txt = {'Nbins', 'Color', 'SmoothFactor', 'Bandwidth', 'Width'}; error ("Octave:invalid-input-arg", ... ['options should be scalars or cell/array with as many values as' ... ' number of variables in the data (wrong size of %s).'], txt{n}); else rethrow (lasterror ()) endif end_try_catch ## Build violins [px py mx] = cellfun (@(y,n,s,r)build_polygon (y, n, s, r), ... x, nb, sf, r0, 'unif', 0); Nc = 1:numel (px); Ncc = mat2cell (Nc, 1, ones (1, Nc(end))); ## get hold state old_hold = ishold (); ## Draw plain violins tmp = cellfun (@(x,y,n,u, w)patch (ax, (w * x + n)(:), y(:) ,u'), ... px, py, Ncc, c, width); h.violin = tmp; hold on ## Overlay mean value tmp = cellfun (@(z,y)plot (ax, z, y,'.k', 'markersize', 6), Ncc, mx); h.mean = tmp; ## Overlay median Mx = cellfun (@median, x, 'unif', 0); tmp = cellfun (@(z,y)plot (ax, z, y, 'ok'), Ncc, Mx); h.median = tmp; ## Overlay 1st and 3rd quartiles LUBU = cellfun (@(x,y)abs (quantile (x,[0.25 0.75])-y), x, Mx, 'unif', 0); tmp = cellfun (@(x,y,z)errorbar (ax, x, y, z(1),z(2)), Ncc, Mx, LUBU)(:); ## Flatten errorbar output handles tmp2 = allchild (tmp); if (! iscell (tmp2)) tmp2 = mat2cell (tmp2, ones (length (tmp2), 1), 1); endif tmp = mat2cell (tmp, ones (length (tmp), 1), 1); tmp = cellfun (@vertcat, tmp, tmp2, 'unif', 0); h.quartile = cell2mat (tmp); hold off ## Rotate the plot if it is horizontal if (is_horiz) structfun (@swap_axes, h); set (ax, 'ytick', Nc); else set (ax, 'xtick', Nc); endif if (nargout < 1); clear h; endif ## restore hold state if (old_hold) hold on endif endfunction function y = stdnormal_pdf (x) y = (2 * pi)^(- 1/2) * exp (- x .^ 2 / 2); endfunction function k = kde (x,r) k = mean (stdnormal_pdf (x / r)) / r; k /= max (k); endfunction function [px py mx] = build_polygon (x, nb, sf, r) N = size (x, 1); mx = mean (x); sx = std (x); X = (x - mx ) / sx; [count bin] = hist (X, nb); count /= max (count); Y = X - bin; if isna (r) r0 = 1.06 * N^(1/5); r = sqp (r0, @(r)sumsq (kde (Y,r) - count), [], [], 1e-3, 1e2); else sf = 1; endif sig = sf * r; ## Create violin polygon ## smooth tails: extend to 1.83 sigmas, i.e. ~99% of data. xx = linspace (0, 1.83 * sig, 5); bin = [bin(1)-fliplr(xx) bin bin(end)+xx]; py = [bin; fliplr(bin)].' * sx + mx; v = kde (X-bin, sig).'; px = [v -flipud(v)]; endfunction function tf = swap_axes (h) tmp = mat2cell (h(:), ones (length (h),1), 1); tmpy = cellfun (@(x)get (x, 'ydata'), tmp, 'unif', 0); tmpx = cellfun (@(x)get (x, 'xdata'), tmp, 'unif', 0); cellfun (@(h,x,y)set (h, 'xdata', y, 'ydata', x), tmp, tmpx, tmpy); tf = true; endfunction function varargout = to_cell (varargin) m = varargin{end}; varargin(end) = []; for i = 1:numel (varargin) x = varargin{i}; if (isscalar (x)) x = repmat (x, m, 1); endif if (iscell (x)) if (numel (x) != m) # no dimension equals m error ("to_cell:element_idx", "%d\n",i); endif varargout{i} = x; continue endif sz = size (x); d = find (sz == m); if (isempty (d)) # no dimension equals m error ("to_cell:element_idx", "%d\n",i); elseif (length (d) == 2) ## both dims are m, choose 1st elseif (d == 1) # 2nd dimension is m --> transpose x = x.'; sz = fliplr (sz); endif varargout{i} = mat2cell (x, sz(1), ones (m,1)); endfor endfunction %!demo %! rng (42); %! clf %! x = zeros (9e2, 10); %! for i=1:10 %! x(:,i) = (0.1 * randn (3e2, 3) * (randn (3,1) + 1) + 2 * randn (1,3))(:); %! endfor %! h = violin (x, 'color', 'c'); %! axis tight %! set (h.violin, 'linewidth', 2); %! set (gca, 'xgrid', 'on'); %! xlabel ('Variables') %! ylabel ('Values') %!demo %! rng (42); %! clf %! data = {randn(100,1)*5+140, randn(130,1)*8+135}; %! subplot (1,2,1) %! title ('Grade 3 heights - vertical'); %! set (gca, 'xtick', 1:2, 'xticklabel', {'girls'; 'boys'}); %! violin (data, 'Nbins', 10); %! axis tight %! %! subplot (1,2,2) %! title ('Grade 3 heights - horizontal'); %! set (gca, 'ytick', 1:2, 'yticklabel', {'girls'; 'boys'}); %! violin (data, 'horizontal', 'Nbins', 10); %! axis tight %!demo %! rng (42); %! rande ('state', 42); %! clf %! data = exprnd (0.1, 500,4); %! violin (data, 'nbins', {5,10,50,100}); %! axis ([0 5 0 max(data(:))]) %!demo %! rng (42); %! rande ('state', 42); %! clf %! data = exprnd (0.1, 500,4); %! violin (data, 'color', jet (4)); %! axis ([0 5 0 max(data(:))]) %!demo %! rng (42); %! rande ('state', 42); %! clf %! data = repmat (exprnd (0.1, 500,1), 1, 4); %! violin (data, 'width', linspace (0.1,0.5,4)); %! axis ([0 5 0 max(data(:))]) %!demo %! rng (42); %! rande ('state', 42); %! clf %! data = repmat (exprnd (0.1, 500,1), 1, 4); %! violin (data, 'nbins', [5,10,50,100], 'smoothfactor', [4 4 8 10]); %! axis ([0 5 0 max(data(:))]) ## Test plotting %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! data = exprnd (0.1, 500,4); %! violin (data, 'color', jet (4)); %! axis ([0 5 0 max(data(:))]) %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! data = {randn(100,1)*5+140, randn(130,1)*8+135}; %! subplot (1,2,1) %! title ('Grade 3 heights - vertical'); %! set (gca, 'xtick', 1:2, 'xticklabel', {'girls'; 'boys'}); %! violin (data, 'Nbins', 10); %! axis tight %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! data = {randn(100,1)*5+140, randn(130,1)*8+135}; %! subplot (1,2,1) %! title ('Grade 3 heights - vertical'); %! set (gca, 'xtick', 1:2, 'xticklabel', {'girls'; 'boys'}); %! violin (data, 'Nbins', 10); %! axis tight %! subplot (1,2,2) %! title ('Grade 3 heights - horizontal'); %! set (gca, 'ytick', 1:2, 'yticklabel', {'girls'; 'boys'}); %! violin (data, 'horizontal', 'Nbins', 10); %! axis tight %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! data = repmat (exprnd (0.1, 500,1), 1, 4); %! violin (data, 'nbins', [5,10,50,100], 'smoothfactor', [4 4 8 10]); %! axis ([0 5 0 max(data(:))]) %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! data = repmat (exprnd (0.1, 500,1), 1, 4); %! violin (data, 'width', linspace (0.1,0.5,4)); %! axis ([0 5 0 max(data(:))]) %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## A vector is one variable however it is oriented, as in boxplot. A row ## vector used to be read as many variables of a single observation each, and ## died inside the kernel estimate. %!test %! hf = figure ("visible", "off"); %! unwind_protect %! r = violin (randn (1, 60)); %! nrow = numel (findobj (gca (), "Type", "patch")); %! clf; %! c = violin (randn (60, 1)); %! ncol = numel (findobj (gca (), "Type", "patch")); %! assert_equal (nrow, 1); %! assert_equal (ncol, 1); %! assert_equal (numel (r.violin), numel (c.violin)); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Columns of a matrix remain one variable each. %!test %! hf = figure ("visible", "off"); %! unwind_protect %! h = violin (randn (40, 3)); %! assert_equal (numel (findobj (gca (), "Type", "patch")), 3); %! assert_equal (numel (h.violin), 3); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## A cell of row vectors is as good as a cell of columns. %!test %! hf = figure ("visible", "off"); %! unwind_protect %! violin ({[1, 2, 3, 4], [3, 4, 5, 6]}); %! assert_equal (numel (findobj (gca (), "Type", "patch")), 2); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Integer and logical observations are data too; var wanted them in floating ## point, so they were refused outright. %!test %! hf = figure ("visible", "off"); %! unwind_protect %! v = [1 2 3 4 5 4 3 2 1 2 3]; %! violin (int32 (v)); %! ni = numel (findobj (gca (), "Type", "patch")); %! clf; %! violin (double (v)); %! nd = numel (findobj (gca (), "Type", "patch")); %! assert_equal (ni, nd); %! clf; %! violin (logical ([1 0 1 1 0 0 1 0 1 1])); %! assert_equal (numel (findobj (gca (), "Type", "patch")), 1); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error violin () %!error violin ([]) %!error violin ({}) %!error ... %! violin (5) %!error ... %! violin ({[1, 2, 3], 4}) %!error violin ('abcdef') statistics-release-2.1.2/inst/Plotting/wblplot.m000066400000000000000000000314051527311104300217420ustar00rootroot00000000000000## Copyright (C) 2014 Bj{\"o}rn Vennberg ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} wblplot (@var{data}, @dots{}) ## @deftypefnx {statistics} {@var{handle} =} wblplot (@var{data}, @dots{}) ## @deftypefnx {statistics} {[@var{handle}, @var{param}] =} wblplot (@var{data}) ## @deftypefnx {statistics} {[@var{handle}, @var{param}] =} wblplot (@var{data}, @var{censor}) ## @deftypefnx {statistics} {[@var{handle}, @var{param}] =} wblplot (@var{data}, @var{censor}, @var{freq}) ## @deftypefnx {statistics} {[@var{handle}, @var{param}] =} wblplot (@var{data}, @var{censor}, @var{freq}, @var{confint}) ## @deftypefnx {statistics} {[@var{handle}, @var{param}] =} wblplot (@var{data}, @var{censor}, @var{freq}, @var{confint}, @var{fancygrid}) ## @deftypefnx {statistics} {[@var{handle}, @var{param}] =} wblplot (@var{data}, @var{censor}, @var{freq}, @var{confint}, @var{fancygrid}, @var{showlegend}) ## ## Plot a column vector @var{data} on a Weibull probability plot using rank ## regression. ## ## @var{censor}: optional parameter is a column vector of same size as ## @var{data} with 1 for right censored data and 0 for exact observation. ## Pass [] when no censor data are available. ## ## @var{freq}: optional vector same size as @var{data} with the number of ## occurrences for corresponding data. ## Pass [] when no frequency data are available. ## ## @var{confint}: optional confidence limits for plotting upper and lower ## confidence bands using beta binomial confidence bounds. If a single ## value is given this will be used such as LOW = a and HIGH = 1 - a. ## Pass [] if confidence bounds is not requested. ## ## @var{fancygrid}: optional parameter which if set to anything but 1 will turn ## off the fancy gridlines. ## ## @var{showlegend}: optional parameter that when set to zero(0) turns off the ## legend. ## ## If one output argument is given, a @var{handle} for the data marker and ## plotlines is returned, which can be used for further modification of line and ## marker style. ## ## If a second output argument is specified, a @var{param} vector with scale, ## shape and correlation factor is returned. ## ## @seealso{normplot, wblpdf} ## @end deftypefn function [handle, param] = wblplot (data, censor = [], freq = [], ... confint = [], fancygrid = 1, showlegend = 1) [mm, nn] = size (data); if (mm > 1 && nn > 1) error ("wblplot: can only handle a single data vector") elseif (mm == 1 && nn > 1) data = data(:); mm = nn; endif if (any (data <= 0)) error ("wblplot: data vector must be positive and non zero") endif ## A non-finite observation is not merely useless here, it is unrecoverable: ## Inf drives the view port to 10 ^ ceil (log10 (Inf)), and the grid loop ## below then runs over log10 (xmin) : Inf, never returning, while NaN makes ## the rank regression singular and plots a meaningless line. if (! all (isfinite (data))) error ("wblplot: data vector must contain finite values") endif if (isempty (freq)) freq = ones (mm, 1); N = mm; else [mmf nnf] = size (freq); if ((mmf == mm && nnf == 1) || (mmf == 1 && nnf == mm)) freq = freq(:); N = sum (freq); ## Total number of samples if (any (freq <= 0)) error ("wblplot: frequency vector must be positive non zero integers") endif else error ("wblplot: frequency must be vector of same length as data") endif endif if (isempty (censor)) censor = zeros (mm,1); else [mmc, nnc] = size (censor); if ((mmc == mm && nnc == 1) || (mmc == 1 && nnc == mm)) censor = censor(:); else error ("wblplot: censor must be a vector of same length as data") endif ## Make sure censored data is sorted correctly so that no censored samples ## are processed before failures if they have the same time. if (any (censor > 0)) ind = find (censor > 0); ind2 = find (data(1:end-1) == data(2:end)); if ((! isempty (ind)) && (! isempty (ind2))) if (any (ind == ind2)) tmp = censor(ind2); censor(ind2) = censor(ind2 + 1); censor(ind2+1) = tmp; tmp = freq(ind2); freq(ind2) = freq(ind2 + 1); freq(ind2 + 1) = tmp; endif endif endif endif ## Determine the order number wbdat = zeros (length (find (censor == 0)), 3); Op = 0; Oi = 0; c = N; nf = 0; for k = 1 : mm if (censor(k, 1) == 0) nf = nf + 1; wbdat(nf, 1) = data(k, 1); for s = 1 : freq(k, 1); Oi = Op + ((N + 1) - Op) / (1 + c); Op = Oi; c = c - 1; endfor wbdat(nf, 3) = Oi; else c = c - freq(k, 1); endif endfor ## Compute median rank a = wbdat(:, 3) ./ (N - wbdat(:, 3) + 1); f = finv (0.5, 2 * (N - wbdat(:, 3) + 1), 2 * wbdat(:, 3)); wbdat(:, 2) = a ./ (f+a); datx = log (wbdat(:,1)); daty = log (log (1 ./ (1 - wbdat(:,2)))); ## Rank regression poly = polyfit (datx, daty, 1); ## Shape factor beta_rry = poly(1); ## Scale factor eta_rry = exp (-(poly(2) / beta_rry)); ## Determine min-max values of view port aa = ceil (log10 (max (wbdat(:,1)))); bb = log10 (max (wbdat(:,1))); if ((aa - bb) < 0.2) aa = ceil (log10 (max (wbdat(:,1)))) + 1; endif xmax = 10 ^ aa; if ((log10 (min (wbdat(:,1))) - floor (log10 (min (wbdat(:,1))))) < 0.2) xmin = 10 ^ (floor (log10 (min (wbdat(:,1)))) - 1); else xmin = 10 ^ floor (log10 (min (wbdat(:,1)))); endif if (min (wbdat(:,2)) > 0.20) ymin = log (log (1 / (1 - 0.1))); elseif (min (wbdat(:,2)) > 0.02) ymin = log (log (1 / (1 - 0.01))); elseif (min (wbdat(:,2)) > 0.002) ymin = log (log (1 / (1 - 0.001))); else ymin = log (log (1 / (1 - 0.0001))); endif ymax= log (log (1 / (1 - 0.999))); x = [0;0]; y = [0;0]; label = char ('0.10', '1.00', '10.00', '99.00'); prob = [0.001 0.01 0.1 0.99]; tick = log (log (1 ./ (1 - prob))); xbf = [xmin; xmax]; ybf = polyval (poly, log (xbf)); newplot (); x(1, 1) = xmin; x(2, 1) = xmax; if (fancygrid == 1) for k = 1 : 4 ## Y major grids x(1, 1) = xmin; x(2, 1) = xmax*10; y(1, 1) = log (log (1 / (1 - 10 ^ (-k)))); y(2, 1) = y(1, 1); ymajorgrid(k) = line (x, y, 'LineStyle', '-', 'Marker', 'none', ... 'Color', [1 0.75 0.75], 'LineWidth', 0.1); endfor ## Y Minor grids 2 - 9 x(1, 1) = xmin; x(2, 1) = xmax * 10; for m = 1 : 4 for k = 1 : 8 y(1, 1) = log (log (1 / (1 - ((k + 1) / (10 ^ m))))); y(2, 1) = y(1, 1); yminorgrid(k) = line (x, y, 'LineStyle', '-', 'Marker', 'none', ... 'Color', [0.75 1 0.75], 'LineWidth', 0.1); endfor endfor ## X-axis grid y(1, 1) = ymin; y(2, 1) = ymax; for m = log10 (xmin) : log10 (xmax) x(1, 1) = 10 ^ m; x(2, 1) = x(1, 1); y(1, 1) = ymin; y(2, 1) = ymax; xmajorgrid(k) = line (x, y, 'LineStyle', '-', 'Marker', 'none', ... 'Color', [1 0.75 0.75]); for k = 1 : 8 ## X Minor grids - 2 - 9 x(1, 1) = (k + 1) * (10 ^ m); x(2, 1) = (k + 1) * (10 ^ m); xminorgrid(k) = line (x, y, 'LineStyle', '-', 'Marker', 'none', ... 'Color', [0.75 1 0.75], 'LineWidth', 0.1); endfor endfor endif set (gca, 'XScale', 'log'); set (gca, 'YTick', tick, 'YTickLabel', label); xlabel ('Data', 'FontSize', 12); ylabel ('Unreliability, F(t)=1-R(t)', 'FontSize', 12); title ('Weibull Probability Plot', 'FontSize', 12); set (gcf, 'Color', [0.9, 0.9, 0.9]); set (gcf, 'name', 'WblPlot'); hold on h = plot (wbdat(:,1), daty, 'o'); set (h, 'markerfacecolor', [0, 0, 1]); set (h, 'markersize', 8); h2 = line (xbf, ybf, 'LineStyle', '-', 'Marker', 'none', ... 'Color', [0.25 0.25 1], 'LineWidth', 1); ## If requested plot beta binomial confidence bounds if (! isempty (confint)) cb_high = []; cb_low = []; if (length (confint) == 1) if (confint > 0.5) cb_high = confint; cb_low = 1 - confint; else cb_high = 1 - confint; cb_low = confint; endif else cb_high = confint(2); cb_low = confint(1); endif conf = zeros (N + 4, 3); betainv = 1 / beta_rry; N2 = [1:N]'; N2 = [0.3; 0.7; N2; N2(end) + 0.5; N2(end) + 0.8]; ## Extend the ends a bit ypos = medianranks (0.5, N, N2); conf(:, 1) = eta_rry * log (1 ./ (1 - ypos)) .^ betainv; conf(:, 2) = medianranks (cb_low, N, N2); conf(:, 3) = medianranks (cb_high, N, N2); confy = log (log (1 ./ (1 - conf(:,2:3)))); confu = [conf(:,1) confy]; if (conf(1,1) > xmin) ## It looks better to extend the lines. p1 = polyfit (log (conf(1:2,1)), confy(1:2,1), 1); y1 = polyval (p1, log (xmin)); p2 = polyfit (log (conf(1:2,1)), confy(1:2,2), 1); y2 = polyval (p2, log (xmin)); confu = [xmin y1 y2; confu]; endif if (conf(end,1) < xmax) p3 = polyfit (log (conf(end-1:end,1)), confy(end-1:end,1), 1); y3 = polyval (p3, log (xmax)); p4 = polyfit (log (conf(end-1:end,1)), confy(end-1:end,2), 1); y4 = polyval (p4, log (xmax)); confu = [confu; xmax, y3, y4]; endif h3 = plot (confu(:,1), confu(:,2:3), 'LineStyle', '-' ,'Marker', 'none', ... 'Color', [1 0.25 0.25], 'LineWidth', 1); endif ## Correlation coefficient rsq = corr (datx, daty); if (showlegend == 1) s1 = sprintf (' RRY\n \\beta=%.3f \n \\eta=%.2f \n \\rho=%.4f', ... beta_rry, eta_rry, rsq); if (! isempty (confint)) s2 = sprintf ('CB_H=%.2f', cb_high); s3 = sprintf ('CB_L=%.2f', cb_low); legend ([h; h2; h3], 'Data', s1, s2, s3, 'location', 'northeastoutside'); else legend ([h; h2], 'Data', s1, 'location', 'northeastoutside'); endif legend ('boxoff'); endif axis ([xmin, xmax, ymin, (log (log (1 / (1 - 0.99))))]); hold off if (nargout >= 2) param = [eta_rry, beta_rry, rsq]; if (! isempty (confint)) handle = [h; h2; h3]; else handle = [h; h2]; endif endif if (nargout == 1) if (! isempty (confint)) handle = [h; h2; h3]; else handle = [h; h2]; endif endif endfunction function ret = medianranks (alpha, n, ii) a = ii ./ (n - ii + 1); f = finv (alpha, 2 * (n - ii + 1), 2 * ii); ret = a ./ (f + a); endfunction %!demo %! x = [16 34 53 75 93 120]; %! wblplot (x); %!demo %! x = [2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67]'; %! c = [0 1 0 1 0 1 1 1 0 0 1 0 1 0 1 1 0 1 1]'; %! [h, p] = wblplot (x, c); %! p %!demo %! x = [16, 34, 53, 75, 93, 120, 150, 191, 240 ,339]; %! [h, p] = wblplot (x, [], [], 0.05); %! p %! ## Benchmark Reliasoft eta = 146.2545 beta 1.1973 rho = 0.9999 %!demo %! x = [46 64 83 105 123 150 150]; %! c = [0 0 0 0 0 0 1]; %! f = [1 1 1 1 1 1 4]; %! wblplot (x, c, f, 0.05); %!demo %! x = [46 64 83 105 123 150 150]; %! c = [0 0 0 0 0 0 1]; %! f = [1 1 1 1 1 1 4]; %! ## Subtract 30.92 from x to simulate a 3 parameter wbl with gamma = 30.92 %! wblplot (x - 30.92, c, f, 0.05); ## Test plotting %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! x = [16, 34, 53, 75, 93, 120, 150, 191, 240 ,339]; %! [h, p] = wblplot (x, [], [], 0.05); %! assert_equal (numel (h), 4) %! assert_equal (p(1), 146.2545, 1E-4) %! assert_equal (p(2), 1.1973, 1E-4) %! assert_equal (p(3), 0.9999, 5E-5) %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation ## Inf used to hang: the view port became 10 ^ ceil (log10 (Inf)) and the grid ## loop ran over log10 (xmin) : Inf. NaN returned, but only after polyfit went ## singular on it. %!error ... %! wblplot ([1, Inf, 2, 3]) %!error ... %! wblplot ([1, NaN, 2, 3]) %!error ... %! wblplot ([1, 0, 2, 3]) ## -Inf is negative, so the positivity check catches it before the finite one. %!error ... %! wblplot ([1, 2, 3, -Inf]) %!error ... %! wblplot (ones (3, 3)) statistics-release-2.1.2/inst/Random_Sampling/000077500000000000000000000000001527311104300213505ustar00rootroot00000000000000statistics-release-2.1.2/inst/Random_Sampling/doc-cache000066400000000000000000000225201527311104300231020ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 5 # name: # type: sq_string # elements: 1 # length: 8 johnsrnd # name: # type: sq_string # elements: 1 # length: 1596 statistics: r = johnsrnd (quantiles) statistics: r = johnsrnd (quantiles, m) statistics: r = johnsrnd (quantiles, m, n, ...) statistics: r = johnsrnd (quantiles, [m, n, ...]) statistics: [r, type, coefs] = johnsrnd (...) Random arrays from the Johnson system of distributions. r = johnsrnd (quantiles) returns a random value drawn from the distribution in the Johnson system that matches the four values in quantiles. quantiles is a four-element vector of the desired quantiles at the standard normal quantiles [-1.5, -0.5, 0.5, 1.5], and its elements must be strictly increasing. johnsrnd fits the Johnson curve passing through these four points using the quantile method of Slifker and Shapiro. quantiles may also be a 2-by-4 numeric matrix, whose first row holds four strictly increasing, evenly spaced standard normal quantiles and whose second row holds the corresponding strictly increasing data quantiles. johnsrnd (quantiles, m, n, ...) or johnsrnd (quantiles, [m, n, ...]) returns an m-by-n-by-... array of random values, following the size conventions of randn. [r, type, coefs] = johnsrnd (...) also returns the selected member of the Johnson system in type, one of "SN" (the normal distribution), "SL" (lognormal), "SU" (unbounded), or "SB" (bounded), and the coefficients coefs = [gamma, delta, xi, lambda] of the transform. A value r is generated by transforming a standard normal deviate z as r = xi + lambda * g ((z - gamma) / delta), where g is the identity, exp, sinh, or the logistic function for "SN", "SL", "SU", and "SB", respectively. See also: pearsrnd, random, randn # name: # type: sq_string # elements: 1 # length: 55 Random arrays from the Johnson system of distributions. # name: # type: sq_string # elements: 1 # length: 8 mhsample # name: # type: sq_string # elements: 1 # length: 2812 statistics: [smpl, accept] = mhsample (start, nsamples, property, value, ...) Draws nsamples samples from a target stationary distribution pdf using Metropolis-Hastings algorithm. Inputs: start is a nchain by dim matrix of starting points for each Markov chain. Each row is the starting point of a different chain and each column corresponds to a different dimension. nsamples is the number of samples, the length of each Markov chain. Some property-value pairs can or must be specified, they are: (Required) One of: "pdf" pdf: a function handle of the target stationary distribution to be sampled. The function should accept different locations in each row and each column corresponds to a different dimension. or "logpdf" logpdf: a function handle of the log of the target stationary distribution to be sampled. The function should accept different locations in each row and each column corresponds to a different dimension. In case optional argument symmetric is set to false (the default), one of: "proppdf" proppdf: a function handle of the proposal distribution that is sampled from with proprnd to give the next point in the chain. The function should accept two inputs, the random variable and the current location each input should accept different locations in each row and each column corresponds to a different dimension. or "logproppdf" logproppdf: the log of "proppdf". The following input property/pair values may be needed depending on the desired output: "proprnd" proprnd: (Required) a function handle which generates random numbers from proppdf. The function should accept different locations in each row and each column corresponds to a different dimension corresponding with the current location. "symmetric" symmetric: true or false based on whether proppdf is a symmetric distribution. If true, proppdf (or logproppdf) need not be specified. The default is false. "burnin" burnin the number of points to discard at the beginning, the default is 0. "thin" thin: omits thin-1 of every thin points in the generated Markov chain. The default is 1. "nchain" nchain: the number of Markov chains to generate. The default is 1. Outputs: smpl: a nsamples x dim x nchain tensor of random values drawn from pdf, where the rows are different random values, the columns correspond to the dimensions of pdf, and the third dimension corresponds to different Markov chains. accept is a vector of the acceptance rate for each chain. Example : Sampling from a normal distribution start = 1; nsamples = 1e3; pdf = @(x) exp (-.5 * x .^ 2) / (pi ^ .5 * 2 ^ .5); proppdf = @(x,y) 1 / 6; proprnd = @(x) 6 * (rand (size (x)) - .5) + x; [smpl, accept] = mhsample (start, nsamples, "pdf", pdf, "proppdf", ... proppdf, "proprnd", proprnd, "thin", 4); histfit (smpl); See also: rand, slicesample # name: # type: sq_string # elements: 1 # length: 101 Draws nsamples samples from a target stationary distribution pdf using Metropolis-Hastings algorithm. # name: # type: sq_string # elements: 1 # length: 8 pearsrnd # name: # type: sq_string # elements: 1 # length: 1377 statistics: r = pearsrnd (mu, sigma, skew, kurt) statistics: r = pearsrnd (mu, sigma, skew, kurt, m) statistics: r = pearsrnd (mu, sigma, skew, kurt, m, n, ...) statistics: r = pearsrnd (mu, sigma, skew, kurt, [m, n, ...]) statistics: [r, type, coefs] = pearsrnd (...) Random arrays from the Pearson system of distributions. r = pearsrnd (mu, sigma, skew, kurt) returns a random value drawn from the distribution in the Pearson system with mean mu, standard deviation sigma, skewness skew, and kurtosis kurt. kurt is the (non-excess) kurtosis, and the moments must satisfy kurt > skew^2 + 1. pearsrnd (mu, sigma, skew, kurt, m, n, ...) or pearsrnd (..., [m, n, ...]) returns an m-by-n-by-... array of random values, following the size conventions of randn. [r, type, coefs] = pearsrnd (...) also returns the type of the Pearson distribution (an integer 0 to 7) in type, and the three coefficients coefs = [c0, c1, c2] of the denominator quadratic of the Pearson differential equation for the standardized distribution, so that f'(x) / f (x) = -(x + c1) / (c0 + c1 x + c2 x^2). The Pearson types are: 0 normal, 1 four-parameter beta, 2 symmetric four-parameter beta, 3 gamma, 4 (not a named distribution), 5 inverse gamma, 6 beta prime, and 7 Student's t. Type 4 is generated by numerical inversion of its cumulative distribution function. See also: johnsrnd, random, randn # name: # type: sq_string # elements: 1 # length: 55 Random arrays from the Pearson system of distributions. # name: # type: sq_string # elements: 1 # length: 6 qrandn # name: # type: sq_string # elements: 1 # length: 452 statistics: z = qrandn (q, r, c) statistics: z = qrandn (q, [r, c]) Returns random deviates drawn from a q-Gaussian distribution. Parameter q characterizes the q-Gaussian distribution. The result has the size indicated by s. Reference: W. Thistleton, J. A. Marsh, K. Nelson, C. Tsallis (2006) "Generalized Box-Muller method for generating q-Gaussian random deviates" arXiv:cond-mat/0605570 http://arxiv.org/abs/cond-mat/0605570 See also: rand, randn # name: # type: sq_string # elements: 1 # length: 61 Returns random deviates drawn from a q-Gaussian distribution. # name: # type: sq_string # elements: 1 # length: 11 slicesample # name: # type: sq_string # elements: 1 # length: 1738 statistics: [smpl, neval] = slicesample (start, nsamples, property, value, ...) Draws nsamples samples from a target stationary distribution pdf using slice sampling of Radford M. Neal. Input: start is a 1 by dim vector of the starting point of the Markov chain. Each column corresponds to a different dimension. nsamples is the number of samples, the length of the Markov chain. Next, several property-value pairs can or must be specified, they are: (Required properties) One of: "pdf": the value is a function handle of the target stationary distribution to be sampled. The function should accept different locations in each row and each column corresponds to a different dimension. or logpdf: the value is a function handle of the log of the target stationary distribution to be sampled. The function should accept different locations in each row and each column corresponds to a different dimension. The following input property/pair values may be needed depending on the desired output: "burnin" burnin the number of points to discard at the beginning, the default is 0. "thin" thin omits m-1 of every m points in the generated Markov chain. The default is 1. "width" width the maximum Manhattan distance between two samples. The default is 10. Outputs: smpl is a nsamples by dim matrix of random values drawn from pdf where the rows are different random values, the columns correspond to the dimensions of pdf. neval is the number of function evaluations per sample. Example : Sampling from a normal distribution start = 1; nsamples = 1e3; pdf = @(x) exp (-.5 * x .^ 2) / (pi ^ .5 * 2 ^ .5); [smpl, accept] = slicesample (start, nsamples, "pdf", pdf, "thin", 4); histfit (smpl); See also: rand, mhsample, randsample # name: # type: sq_string # elements: 1 # length: 99 Draws nsamples samples from a target stationary distribution pdf using slice sampling of Radford M. statistics-release-2.1.2/inst/Random_Sampling/johnsrnd.m000066400000000000000000000245601527311104300233620ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} johnsrnd (@var{quantiles}) ## @deftypefnx {statistics} {@var{r} =} johnsrnd (@var{quantiles}, @var{m}) ## @deftypefnx {statistics} {@var{r} =} johnsrnd (@var{quantiles}, @var{m}, @var{n}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} johnsrnd (@var{quantiles}, [@var{m}, @var{n}, @dots{}]) ## @deftypefnx {statistics} {[@var{r}, @var{type}, @var{coefs}] =} johnsrnd (@dots{}) ## ## Random arrays from the Johnson system of distributions. ## ## @code{@var{r} = johnsrnd (@var{quantiles})} returns a random value drawn from ## the distribution in the Johnson system that matches the four values in ## @var{quantiles}. @var{quantiles} is a four-element vector of the desired ## quantiles at the standard normal quantiles @code{[-1.5, -0.5, 0.5, 1.5]}, and ## its elements must be strictly increasing. @code{johnsrnd} fits the Johnson ## curve passing through these four points using the quantile method of Slifker ## and Shapiro. ## ## @var{quantiles} may also be a 2-by-4 numeric matrix, whose first row holds ## four strictly increasing, evenly spaced standard normal quantiles and whose ## second row holds the corresponding strictly increasing data quantiles. ## ## @code{johnsrnd (@var{quantiles}, @var{m}, @var{n}, @dots{})} or ## @code{johnsrnd (@var{quantiles}, [@var{m}, @var{n}, @dots{}])} returns an ## @var{m}-by-@var{n}-by-@dots{} array of random values, following the size ## conventions of @code{randn}. ## ## @code{[@var{r}, @var{type}, @var{coefs}] = johnsrnd (@dots{})} also returns ## the selected member of the Johnson system in @var{type}, one of @qcode{"SN"} ## (the normal distribution), @qcode{"SL"} (lognormal), @qcode{"SU"} ## (unbounded), or @qcode{"SB"} (bounded), and the coefficients @var{coefs} = ## @code{[@var{gamma}, @var{delta}, @var{xi}, @var{lambda}]} of the transform. ## A value @var{r} is generated by transforming a standard normal deviate ## @var{z} as @code{@var{r} = @var{xi} + @var{lambda} * g ((@var{z} - ## @var{gamma}) / @var{delta})}, where @code{g} is the identity, @code{exp}, ## @code{sinh}, or the logistic function for @qcode{"SN"}, @qcode{"SL"}, ## @qcode{"SU"}, and @qcode{"SB"}, respectively. ## ## @seealso{pearsrnd, random, randn} ## @end deftypefn function [r, type, coefs] = johnsrnd (quantiles, varargin) if (nargin < 1) print_usage (); endif if (isnumeric (quantiles) && isreal (quantiles) && isvector (quantiles) ... && numel (quantiles) == 4) zrow = [-1.5, -0.5, 0.5, 1.5]; q = quantiles(:)'; elseif (isnumeric (quantiles) && isreal (quantiles) ... && isequal (size (quantiles), [2, 4])) zrow = quantiles(1, :); q = quantiles(2, :); zdiff = diff (zrow); if (any (zdiff <= 0) || any (abs (zdiff - zdiff(1)) > 1e-9 .* abs (zdiff(1)))) error (["johnsrnd: standard normal quantiles must be strictly " ... "increasing and evenly spaced."]); endif else error (["johnsrnd: QUANTILES must be a four-element numeric vector " ... "or a 2-by-4 numeric matrix."]); endif if (any (isnan (q)) || any (diff (q) <= 0)) error ("johnsrnd: QUANTILES must be strictly increasing."); endif ## Fit the Johnson curve in normalised z-space, then rescale gamma and delta ## to the standard normal quantiles actually supplied in ZROW [type, coefs] = johnson_fit (q); h = zrow(2) - zrow(1); center = (zrow(2) + zrow(3)) ./ 2; coefs(1) = center + h .* coefs(1); coefs(2) = h .* coefs(2); ## Draw standard normal deviates and apply the fitted transform. Negative ## dimensions are treated as zero, as in core Octave and MATLAB. szargs = cellfun (@(x) max (x, 0), varargin, 'UniformOutput', false); z = randn (szargs{:}); u = (z - coefs(1)) ./ coefs(2); xi = coefs(3); lambda = coefs(4); switch (type) case "SN" r = xi + lambda .* u; case "SL" r = xi + lambda .* exp (u); case "SU" r = xi + lambda .* sinh (u); case "SB" r = xi + lambda ./ (1 + exp (-u)); endswitch endfunction ## Quantile-method fit (Slifker & Shapiro 1980) at the fixed standard normal ## anchors z = [-1.5, -0.5, 0.5, 1.5], i.e. z0 = 0.5 and 3*z0 = 1.5. Returns ## the Johnson family and the transform coefficients [gamma, delta, xi, lambda]. function [type, coefs] = johnson_fit (q) x1 = q(1); x2 = q(2); x3 = q(3); x4 = q(4); m = x4 - x3; # top gap (x at 1.5 minus x at 0.5) n = x2 - x1; # bottom gap (x at -0.5 minus x at -1.5) p = x3 - x2; # middle gap (x at 0.5 minus x at -0.5) d = m .* n ./ p .^ 2; # Slifker-Shapiro discriminant t1 = m ./ p; t2 = n ./ p; if (abs (d - 1) < 1e-9) if (abs (m - n) < 1e-9) ## Symmetric and linear in z: the normal distribution type = "SN"; coefs = [0, 1 ./ p, (x2 + x3) ./ 2, 1]; else ## Lognormal type = "SL"; delta = 1 ./ log (t1); lambda = sign (delta); scale = p ./ (2 .* sinh (0.5 ./ delta)); gamma = -delta .* log (scale ./ lambda); xi = x2 - lambda .* exp ((-0.5 - gamma) ./ delta); coefs = [gamma, delta, xi, lambda]; endif elseif (d > 1) ## Unbounded type = "SU"; delta = 1 ./ acosh (0.5 .* (t1 + t2)); gamma = delta .* asinh ((t2 - t1) ./ (2 .* sqrt (d - 1))); lambda = 2 .* p .* sqrt (d - 1) ./ ((t1 + t2 - 2) .* sqrt (t1 + t2 + 2)); xi = 0.5 .* (x2 + x3) + p .* (t2 - t1) ./ (2 .* (t1 + t2 - 2)); coefs = [gamma, delta, xi, lambda]; else ## Bounded type = "SB"; a = (1 + p ./ m) .* (1 + p ./ n); r = p .^ 2 ./ (m .* n) - 1; delta = 0.5 ./ acosh (0.5 .* sqrt (a)); gamma = delta .* asinh ((p ./ n - p ./ m) .* sqrt (a - 4) ./ (2 .* r)); lambda = p .* sqrt ((a - 2) .^ 2 - 4) ./ r; xi = 0.5 .* (x2 + x3) - 0.5 .* lambda + p .* (p ./ n - p ./ m) ./ (2 .* r); coefs = [gamma, delta, xi, lambda]; endif endfunction %!demo %! ## Fit a Johnson distribution to four quantiles and identify its type %! rng (42); %! [r, type, coefs] = johnsrnd ([-1, -0.25, 0.75, 3]) %!demo %! ## Draw a sample and check its shape %! rng (42); %! r = johnsrnd ([-1, -0.25, 0.75, 3], 1, 1000); %! hist (r, 50); ## Type and coefficients against MATLAB %!test %! [~, type, coefs] = johnsrnd ([-1.5, -0.5, 0.5, 1.5]); %! assert_equal (type, "SN"); %! assert_equal (coefs, [0, 1, 0, 1], 1e-12); %!test %! [~, type, coefs] = johnsrnd ([-1, -0.25, 0.75, 3]); %! assert_equal (type, "SU"); %! assert_equal (coefs, [-0.843945656908448, 1.03904346061751, ... %! -0.5, 0.741619848709566], 1e-10); %!test %! [~, type, coefs] = johnsrnd ([0.2, 0.9, 1.4, 1.8]); %! assert_equal (type, "SU"); %! assert_equal (coefs, ... %! [1.76613094093528, 2.25444447413448, 1.9, 0.845154254728516], 1e-10); %!test %! [~, type, coefs] = johnsrnd ([1, 2, 4, 9]); %! assert_equal (type, "SU"); %! assert_equal (coefs, [-1.5, 1.03904346061751, 1, 0.894427190999916], 1e-10); %!test %! [~, type, coefs] = johnsrnd ([-3, -0.75, 0.25, 1]); %! assert_equal (type, "SU"); %! assert_equal (coefs, [0.843945656908448, 1.03904346061751, ... %! 0.5, 0.741619848709566], 1e-10); ## Bounded and lognormal fits reproduce their input quantiles (unique fit) %!test %! q = [0.1, 0.3, 0.8, 0.95]; %! [~, type, c] = johnsrnd (q); %! assert_equal (type, "SB"); %! z = [-1.5, -0.5, 0.5, 1.5]; %! xz = c(3) + c(4) ./ (1 + exp (-(z - c(1)) ./ c(2))); %! assert_equal (xz, q, 1e-12); %!test %! q = [0, 1, 3, 7]; %! [~, type, c] = johnsrnd (q); %! assert_equal (type, "SL"); %! z = [-1.5, -0.5, 0.5, 1.5]; %! xz = c(3) + c(4) .* exp ((z - c(1)) ./ c(2)); %! assert_equal (xz, q, 1e-10); ## Size handling follows randn; bounded draws stay within the support %!test %! r = johnsrnd ([-1, -0.25, 0.75, 3], 3, 4); %! assert_equal (size (r), [3, 4]); %! assert_equal (all (isfinite (r), 'all'), true); %! assert_equal (size (johnsrnd ([-1, -0.25, 0.75, 3], -1)), [0, 0]); %! assert_equal (size (johnsrnd ([-1, -0.25, 0.75, 3], 2, -1, 5)), [2, 0, 5]); %!test %! [r, ~, c] = johnsrnd ([0.1, 0.3, 0.8, 0.95], 1, 500); %! assert_equal (all (r > c(3) & r < c(3) + c(4), 'all'), true); %!test %! [~, type, coefs] = johnsrnd ([-7, -3, -1, 0]); %! assert_equal (type, "SL"); %! assert_equal (coefs, [1.5, -1.44269504088896, 1, -1], 1e-10); %! assert_equal (isreal (coefs), true); %!test %! qnorm = [.5, 1, 1.5, 2]; %! q = [16.7000, 18.2086, 19.5376, 21.7263]; %! [r, type, coefs] = johnsrnd ([qnorm; q], 0); %! assert_equal (r, []); %! assert_equal (type, "SU"); %! assert_equal (coefs, [1.0920, 0.5829, 18.4382, 1.4493], 1e-4); %!test %! [~, type, coefs] = johnsrnd ([-1.5, -0.5, 0.5, 1.5; -1, -0.25, 0.75, 3], 0); %! [~, type2, coefs2] = johnsrnd ([-1, -0.25, 0.75, 3]); %! assert_equal (type, type2); %! assert_equal (coefs, coefs2, 1e-12); ## Test input validation %!error johnsrnd () %!error ... %! johnsrnd ([1, 2, 3]) %!error ... %! johnsrnd ("abcd") %!error ... %! johnsrnd (ones (3, 4)) %!error ... %! johnsrnd ([1, 2, 2, 3]) %!error ... %! johnsrnd ([4, 3, 2, 1]) %!error ... %! johnsrnd ([1, NaN, 3, 4]) %!error ... %! johnsrnd ([1.5, 0.5, -0.5, -1.5; -1, -0.25, 0.75, 3]) %!error ... %! johnsrnd ([-1.5, -0.5, 0.5, 1; -1, -0.25, 0.75, 3]) statistics-release-2.1.2/inst/Random_Sampling/mhsample.m000066400000000000000000000266551527311104300233520ustar00rootroot00000000000000## Copyright (C) 1995-2022 The Octave Project Developers ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{smpl}, @var{accept}] =} mhsample (@var{start}, @var{nsamples}, @var{property}, @var{value}, @dots{}) ## ## Draws @var{nsamples} samples from a target stationary distribution @var{pdf} ## using Metropolis-Hastings algorithm. ## ## Inputs: ## ## @itemize ## @item ## @var{start} is a @var{nchain} by @var{dim} matrix of starting points for each ## Markov chain. Each row is the starting point of a different chain and each ## column corresponds to a different dimension. ## ## @item ## @var{nsamples} is the number of samples, the length of each Markov chain. ## @end itemize ## ## Some property-value pairs can or must be specified, they are: ## ## (Required) One of: ## ## @itemize ## @item ## "pdf" @var{pdf}: a function handle of the target stationary distribution to ## be sampled. The function should accept different locations in each row and ## each column corresponds to a different dimension. ## ## or ## ## @item ## "logpdf" @var{logpdf}: a function handle of the log of the target stationary ## distribution to be sampled. The function should accept different locations ## in each row and each column corresponds to a different dimension. ## @end itemize ## ## In case optional argument @var{symmetric} is set to false (the default), one ## of: ## ## @itemize ## @item ## "proppdf" @var{proppdf}: a function handle of the proposal distribution that ## is sampled from with @var{proprnd} to give the next point in the chain. The ## function should accept two inputs, the random variable and the current ## location each input should accept different locations in each row and each ## column corresponds to a different dimension. ## ## or ## ## @item ## "logproppdf" @var{logproppdf}: the log of "proppdf". ## @end itemize ## ## The following input property/pair values may be needed depending on the ## desired output: ## ## @itemize ## @item ## "proprnd" @var{proprnd}: (Required) a function handle which generates random ## numbers from @var{proppdf}. The function should accept different locations ## in each row and each column corresponds to a different dimension ## corresponding with the current location. ## ## @item ## "symmetric" @var{symmetric}: true or false based on whether @var{proppdf} is ## a symmetric distribution. If true, @var{proppdf} (or @var{logproppdf}) need ## not be specified. The default is false. ## ## @item ## "burnin" @var{burnin} the number of points to discard at the beginning, the ## default is 0. ## ## @item ## "thin" @var{thin}: omits @var{thin}-1 of every @var{thin} points in the ## generated Markov chain. The default is 1. ## ## @item ## "nchain" @var{nchain}: the number of Markov chains to generate. The default ## is 1. ## @end itemize ## ## Outputs: ## ## @itemize ## @item ## @var{smpl}: a @var{nsamples} x @var{dim} x @var{nchain} tensor of random ## values drawn from @var{pdf}, where the rows are different random values, the ## columns correspond to the dimensions of @var{pdf}, and the third dimension ## corresponds to different Markov chains. ## ## @item ## @var{accept} is a vector of the acceptance rate for each chain. ## @end itemize ## ## Example : Sampling from a normal distribution ## ## @example ## @group ## start = 1; ## nsamples = 1e3; ## pdf = @@(x) exp (-.5 * x .^ 2) / (pi ^ .5 * 2 ^ .5); ## proppdf = @@(x,y) 1 / 6; ## proprnd = @@(x) 6 * (rand (size (x)) - .5) + x; ## [smpl, accept] = mhsample (start, nsamples, "pdf", pdf, "proppdf", ... ## proppdf, "proprnd", proprnd, "thin", 4); ## histfit (smpl); ## @end group ## @end example ## ## @seealso{rand, slicesample} ## @end deftypefn function [smpl, accept] = mhsample (start, nsamples, varargin) if (nargin < 6) print_usage (); endif sizestart = size (start); pdf = []; proppdf = []; logpdf = []; logproppdf = []; proprnd = []; sym = false; K = 0; # burnin m = 1; # thin nchain = 1; for k = 1:2:length (varargin) if (ischar (varargin{k})) switch lower (varargin{k}) case 'pdf' if (isa (varargin{k+1}, 'function_handle')) pdf = varargin{k+1}; else error ("mhsample: pdf must be a function handle"); endif case 'proppdf' if (isa (varargin{k+1}, 'function_handle')) proppdf = varargin{k+1}; else error ("mhsample: proppdf must be a function handle"); endif case 'logpdf' if (isa (varargin{k+1}, 'function_handle')) pdf = varargin{k+1}; else error ("mhsample: logpdf must be a function handle"); endif case 'logproppdf' if (isa (varargin{k+1}, 'function_handle')) proppdf = varargin{k+1}; else error ("mhsample: logproppdf must be a function handle"); endif case 'proprnd' if (isa (varargin{k+1}, 'function_handle')) proprnd = varargin{k+1}; else error ("mhsample: proprnd must be a function handle"); endif case 'symmetric' if (isa (varargin{k+1}, 'logical')) sym = varargin{k+1}; else error ("mhsample: sym must be true or false"); endif case 'burnin' if (varargin{k+1}>=0) K = varargin{k+1}; else error ("mhsample: K must be greater than or equal to 0"); endif case 'thin' if (varargin{k+1} >= 1) m = varargin{k+1}; else error ("mhsample: m must be greater than or equal to 1"); endif case 'nchain' if (varargin{k+1} >= 1) nchain = varargin{k+1}; else error ("mhsample: nchain must be greater than or equal to 1"); endif otherwise warning ("mhsample: Ignoring unknown option %s", varargin{k}); endswitch else error ("mhsample: %s is not a valid property.", varargin{k}); endif endfor if (! isempty (pdf) && isempty (logpdf)) logpdf=@(x) rloge (pdf (x)); elseif (isempty (pdf) && isempty (logpdf)) error ("mhsample: pdf or logpdf must be input."); endif if (! isempty (proppdf) && isempty (logproppdf)) logproppdf = @(x, y) rloge (proppdf(x, y)); elseif (isempty (proppdf) && isempty (logproppdf) && ! sym) error ("mhsample: proppdf or logproppdf must be input unless 'symmetrical' is true."); endif if (! isa (proprnd, 'function_handle')) error ("mhsample: proprnd must be a function handle."); endif if (length (sizestart) == 2) sizestart = [sizestart 0]; endif smpl = zeros (nsamples, sizestart(2), nchain); if (all (sizestart([1 3]) == [1 nchain])) ## Could remove, not Matlab compatible but allows continuing chains smpl(1, :, :) = start; elseif (all (sizestart([1 3]) == [nchain 0])) smpl(1, :, :) = permute (start, [3, 2, 1]); elseif (all (sizestart([1 3]) == [1 0])) ## Could remove, not Matlab compatible but allows all chains to start ## at the same location smpl(1, :, :) = repmat (start,[1, 1, nchain]); else error ("mhsample: start must be a nchain by dim matrix."); endif cx = permute (smpl(1, :, :),[3, 2, 1]); accept = zeros (nchain, 1); i = 1; rnd = log (rand (nchain, nsamples*m+K)); for k = 1:nsamples*m+K canacc = rem (k-K, m) == 0; px = proprnd(cx); if (sym) A = logpdf(px) - logpdf(cx); else A = (logpdf(px) + logproppdf(cx, px)) - (logpdf(cx) + logproppdf(px, cx)); endif ac = rnd(:, k) < min (A, 0); cx(ac, :) = px(ac, :); accept(ac)++; if (canacc) smpl(i, :, :) = permute (cx, [3, 2, 1]); endif if (k > K && canacc) i++; endif endfor accept ./= (nsamples * m + K); endfunction function y = rloge (x) y = -inf (size (x)); xg0 = x > 0; y(xg0) = log (x(xg0)); endfunction %!demo %! ## Define function to sample %! rng (42); %! d = 2; %! mu = [-1; 2]; %! Sigma = rand (d); %! Sigma = (Sigma + Sigma'); %! Sigma += eye (d) * abs (eigs (Sigma, 1, 'sa')) * 1.1; %! pdf = @(x)(2*pi)^(-d/2)*det (Sigma)^-.5*exp (-.5*sum ((x.'-mu).*(Sigma\(x.'-mu)),1)); %! ## Inputs %! start = ones (1, 2); %! nsamples = 500; %! sym = true; %! K = 500; %! m = 10; %! proprnd = @(x) (rand (size (x)) - .5) * 3 + x; %! [smpl, accept] = mhsample (start, nsamples, 'pdf', pdf, 'proprnd', proprnd, ... %! 'symmetric', sym, 'burnin', K, 'thin', m); %! figure; %! hold on; %! plot (smpl(:, 1), smpl(:, 2), 'x'); %! [x, y] = meshgrid (linspace (-6, 4), linspace (-3, 7)); %! z = reshape (pdf ([x(:), y(:)]), size (x)); %! mesh (x, y, z, 'facecolor', 'None'); %! ## Using sample points to find the volume of half a sphere with radius of .5 %! f = @(x) ((.25-(x(:,1)+1).^2-(x(:,2)-2).^2).^.5.*(((x(:,1)+1).^2+(x(:,2)-2).^2)<.25)).'; %! int = mean (f(smpl) ./ pdf (smpl)); %! errest = std (f(smpl) ./ pdf (smpl)) / nsamples ^ .5; %! trueerr = abs (2 / 3 * pi * .25 ^ (3 / 2) - int); %! printf ("Monte Carlo integral estimate int f(x) dx = %f\n", int); %! printf ("Monte Carlo integral error estimate %f\n", errest); %! printf ("The actual error %f\n", trueerr); %! mesh (x, y, reshape (f([x(:), y(:)]), size (x)), 'facecolor', 'None'); %!demo %! ## Integrate truncated normal distribution to find normalization constant %! rng (42); %! pdf = @(x) exp (-.5*x.^2)/(pi^.5*2^.5); %! nsamples = 1e3; %! proprnd = @(x) (rand (size (x)) - .5) * 3 + x; %! [smpl, accept] = mhsample (1, nsamples, 'pdf', pdf, 'proprnd', proprnd, ... %! 'symmetric', true, 'thin', 4); %! f = @(x) exp (-.5 * x .^ 2) .* (x >= -2 & x <= 2); %! x = linspace (-3, 3, 1000); %! area (x, f(x)); %! xlabel ('x'); %! ylabel ('f(x)'); %! int = mean (f(smpl) ./ pdf (smpl)); %! errest = std (f(smpl) ./ pdf (smpl)) / nsamples^ .5; %! trueerr = abs (erf (2 ^ .5) * 2 ^ .5 * pi ^ .5 - int); %! printf ("Monte Carlo integral estimate int f(x) dx = %f\n", int); %! printf ("Monte Carlo integral error estimate %f\n", errest); %! printf ("The actual error %f\n", trueerr); ## Test output %!test %! nchain = 1e4; %! start = rand (nchain, 1); %! nsamples = 1e3; %! pdf = @(x) exp (-.5*(x-1).^2)/(2*pi)^.5; %! proppdf = @(x, y) 1/3; %! proprnd = @(x) 3 * (rand (size (x)) - .5) + x; %! [smpl, accept] = mhsample (start, nsamples, 'pdf', pdf, 'proppdf', proppdf, ... %! 'proprnd', proprnd, 'thin', 2, 'nchain', nchain, ... %! 'burnin', 0); %! assert_equal (mean (mean (smpl, 1), 3), 1, .01); %! assert_equal (mean (var (smpl, 1), 3), 1, .01) ## Test input validation %!error mhsample (); %!error mhsample (1); %!error mhsample (1, 1); %!error mhsample (1, 1, 'pdf', @(x)x); %!error mhsample (1, 1, 'pdf', @(x)x, 'proprnd', @(x)x+rand (size (x))); statistics-release-2.1.2/inst/Random_Sampling/pearsrnd.m000066400000000000000000000267051527311104300233560ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{r} =} pearsrnd (@var{mu}, @var{sigma}, @var{skew}, @var{kurt}) ## @deftypefnx {statistics} {@var{r} =} pearsrnd (@var{mu}, @var{sigma}, @var{skew}, @var{kurt}, @var{m}) ## @deftypefnx {statistics} {@var{r} =} pearsrnd (@var{mu}, @var{sigma}, @var{skew}, @var{kurt}, @var{m}, @var{n}, @dots{}) ## @deftypefnx {statistics} {@var{r} =} pearsrnd (@var{mu}, @var{sigma}, @var{skew}, @var{kurt}, [@var{m}, @var{n}, @dots{}]) ## @deftypefnx {statistics} {[@var{r}, @var{type}, @var{coefs}] =} pearsrnd (@dots{}) ## ## Random arrays from the Pearson system of distributions. ## ## @code{@var{r} = pearsrnd (@var{mu}, @var{sigma}, @var{skew}, @var{kurt})} ## returns a random value drawn from the distribution in the Pearson system with ## mean @var{mu}, standard deviation @var{sigma}, skewness @var{skew}, and ## kurtosis @var{kurt}. @var{kurt} is the (non-excess) kurtosis, and the ## moments must satisfy @code{@var{kurt} > @var{skew}^2 + 1}. ## ## @code{pearsrnd (@var{mu}, @var{sigma}, @var{skew}, @var{kurt}, @var{m}, ## @var{n}, @dots{})} or @code{pearsrnd (@dots{}, [@var{m}, @var{n}, @dots{}])} ## returns an @var{m}-by-@var{n}-by-@dots{} array of random values, following ## the size conventions of @code{randn}. ## ## @code{[@var{r}, @var{type}, @var{coefs}] = pearsrnd (@dots{})} also returns ## the type of the Pearson distribution (an integer @code{0} to @code{7}) in ## @var{type}, and the three coefficients @var{coefs} = ## @code{[@var{c0}, @var{c1}, @var{c2}]} of the denominator quadratic of the ## Pearson differential equation for the standardized distribution, so that ## @code{@var{f}'(@var{x}) / @var{f} (@var{x}) = -(@var{x} + @var{c1}) / ## (@var{c0} + @var{c1} @var{x} + @var{c2} @var{x}^2)}. ## ## The Pearson types are: @code{0} normal, @code{1} four-parameter beta, ## @code{2} symmetric four-parameter beta, @code{3} gamma, @code{4} (not a named ## distribution), @code{5} inverse gamma, @code{6} beta prime, and @code{7} ## Student's t. Type @code{4} is generated by numerical inversion of its ## cumulative distribution function. ## ## @seealso{johnsrnd, random, randn} ## @end deftypefn function [r, type, coefs] = pearsrnd (mu, sigma, skew, kurt, varargin) if (nargin < 4) print_usage (); endif if (! all (cellfun (@(v) isscalar (v) && isnumeric (v) && isreal (v), ... {mu, sigma, skew, kurt}))) error ("pearsrnd: MU, SIGMA, SKEW, and KURT must be real scalars."); endif if (sigma < 0) error ("pearsrnd: SIGMA must be non-negative."); endif if (kurt <= skew .^ 2 + 1) error ("pearsrnd: KURT must be greater than SKEW^2 + 1."); endif ## Output size, following randn's conventions sz = size_from_args (varargin); n = prod (sz); ## Standardized Pearson coefficients and the selected type [type, coefs] = pearson_type (skew, kurt); ## Generate standardized deviates (mean 0, variance 1), then scale and shift. ## Asymmetric shapes are generated for a non-negative skewness and reflected, ## so each generator only needs the positive-skew case. if (n == 0) z = zeros (0, 1); else sgn = sign (skew); if (sgn == 0) sgn = 1; endif z = sgn .* pearson_standard (type, abs (skew), kurt, n); endif r = reshape (mu + sigma .* z, sz); endfunction ## Standardized Pearson coefficients [c0, c1, c2] and the type (0-7) function [type, coefs] = pearson_type (skew, kurt) b1 = skew .^ 2; b2 = kurt; den = 10 .* b2 - 12 .* b1 - 18; c0 = (4 .* b2 - 3 .* b1) ./ den; c1 = skew .* (b2 + 3) ./ den; c2 = (2 .* b2 - 3 .* b1 - 6) ./ den; coefs = [c0, c1, c2]; if (b1 == 0) if (b2 == 3) type = 0; elseif (b2 < 3) type = 2; else type = 7; endif elseif (abs (c2) < 1e-12) type = 3; else kappa = c1 .^ 2 ./ (4 .* c0 .* c2); if (kappa < 0) type = 1; elseif (abs (kappa - 1) < 1e-9) type = 5; elseif (kappa < 1) type = 4; else type = 6; endif endif endfunction ## Draw n standardized (mean 0, variance 1) deviates for a non-negative skew function z = pearson_standard (type, skew, kurt, n) b1 = skew .^ 2; b2 = kurt; den = 10 .* b2 - 12 .* b1 - 18; c0 = (4 .* b2 - 3 .* b1) ./ den; c1 = skew .* (b2 + 3) ./ den; c2 = (2 .* b2 - 3 .* b1 - 6) ./ den; switch (type) case 0 z = randn (n, 1); case 7 nu = 4 + 6 ./ (b2 - 3); z = trnd (nu, n, 1) ./ sqrt (nu ./ (nu - 2)); case 2 a = (6 ./ (3 - b2) - 3) ./ 2; z = (betarnd (a, a, n, 1) - 0.5) .* 2 .* sqrt (2 .* a + 1); case 3 k = 4 ./ b1; z = (gamrnd (k, 1, n, 1) - k) ./ sqrt (k); case {1, 6} [r1, r2, p1, p2] = pearson_roots (c0, c1, c2); if (type == 1) ## Bounded: a beta distribution on [lo, hi] lo = min (r1, r2); hi = max (r1, r2); if (lo == r1) shapes = [p1, p2]; else shapes = [p2, p1]; endif z = lo + (hi - lo) .* betarnd (shapes(1) + 1, shapes(2) + 1, n, 1); else ## Beta prime, supported on (b, Inf) with b the root nearest the mass b = max (r1, r2); o = min (r1, r2); if (b == r1) pb = p1; po = p2; else pb = p2; po = p1; endif y = betarnd (pb + 1, -pb - po - 1, n, 1); y = y ./ (1 - y); z = b + (b - o) .* y; endif case 5 ## Inverse gamma: x = r0 + 1 / w, with w gamma-distributed r0 = -c1 ./ (2 .* c2); k = 1 ./ c2 - 1; rate = -(r0 + c1) ./ c2; z = r0 + 1 ./ gamrnd (k, 1 ./ rate, n, 1); case 4 ## Pearson IV: numerical inversion of the cdf z = pearson4_invcdf (c0, c1, c2, kurt, n); endswitch endfunction ## Real roots and partial-fraction exponents of the Pearson denominator, using ## the numerator (x + c1) (i.e. the mode-centring constant a = -c1). function [r1, r2, p1, p2] = pearson_roots (c0, c1, c2) sq = sqrt (c1 .^ 2 - 4 .* c0 .* c2); r1 = (-c1 - sq) ./ (2 .* c2); r2 = (-c1 + sq) ./ (2 .* c2); a = -c1; p1 = -(r1 - a) ./ (c2 .* (r1 - r2)); p2 = -(r2 - a) ./ (c2 .* (r2 - r1)); endfunction ## Pearson type IV standardized deviates by numerical inversion of the cdf built ## from the closed-form density (complex denominator roots). function z = pearson4_invcdf (c0, c1, c2, kurt, n) L = max (40, 6 .* kurt); x = linspace (-L, L, 400001)'; D = c2 .* x .^ 2 + c1 .* x + c0; s = sqrt (4 .* c0 .* c2 - c1 .^ 2); logf = -(1 ./ (2 .* c2)) .* log (D) ... - c1 .* (1 - 1 ./ (2 .* c2)) .* (2 ./ s) ... .* atan ((2 .* c2 .* x + c1) ./ s); f = exp (logf - max (logf)); F = cumtrapz (x, f); F = F ./ F(end); [F, ia] = unique (F); z = interp1 (F, x(ia), rand (n, 1), "linear"); endfunction ## Output size vector from the trailing size arguments, as randn accepts them function sz = size_from_args (args) if (numel (args) == 0) sz = [1, 1]; elseif (numel (args) == 1) a = args{1}; if (isscalar (a)) sz = [a, a]; else sz = a(:)'; endif else sz = [args{:}]; endif if (! all (sz == fix (sz))) error ("pearsrnd: dimensions must be integers."); endif ## Negative dimensions are treated as zero, as in core Octave and MATLAB sz = max (sz, 0); endfunction %!demo %! ## Identify the Pearson type matching a set of moments %! rng (42); %! randg ('state', 42); %! [r, type, coefs] = pearsrnd (0, 1, 0.75, 4) %!demo %! ## Draw a sample with a target mean, sd, skewness, and kurtosis %! rng (42); %! randg ('state', 42); %! r = pearsrnd (10, 2, 1, 5, 1, 1000); %! [mean(r), std(r)] ## Type and coefficients against MATLAB %!test %! [~, type, coefs] = pearsrnd (0, 1, 0, 3); %! assert_equal (type, 0); %! assert_equal (coefs, [1, 0, 0], 1e-12); %!test %! [~, type, coefs] = pearsrnd (0, 1, 0, 4.5); %! assert_equal (type, 7); %! assert_equal (coefs, [0.666666666666667, 0, 0.111111111111111], 1e-12); %!test %! [~, type, coefs] = pearsrnd (0, 1, 0, 2.5); %! assert_equal (type, 2); %! assert_equal (coefs, [1.42857142857143, 0, -0.142857142857143], 1e-12); %!test %! [~, type, coefs] = pearsrnd (0, 1, 0.5, 3.5); %! assert_equal (type, 4); %! assert_equal (coefs, ... %! [0.946428571428571, 0.232142857142857, 0.0178571428571429], 1e-12); %!test %! [~, type, coefs] = pearsrnd (0, 1, 0.75, 4); %! assert_equal (type, 6); %! assert_equal (coefs, ... %! [0.938524590163934, 0.344262295081967, 0.0204918032786885], 1e-12); %!test %! [~, type, coefs] = pearsrnd (2, 3, 1, 5); %! assert_equal (type, 4); %! assert_equal (coefs, [0.85, 0.4, 0.05], 1e-12); %!test %! [~, type, coefs] = pearsrnd (0, 1, 1, 4.5); %! assert_equal (type, 3); %! assert_equal (coefs, [1, 0.5, 0], 1e-12); %!test %! [~, type, coefs] = pearsrnd (0, 1, 2, 9); %! assert_equal (type, 3); %! assert_equal (coefs, [1, 1, 0], 1e-12); ## Generated samples reproduce the target moments (each type is exercised) %!test %! cases = [0 1 0 3; 0 1 0 4.5; 0 1 0 2.5; 0 1 0.5 3.5; 0 1 0.75 4; ... %! 2 3 1 5; 0 1 1 4.5; 0 1 2 9]; %! ## Seed per case, not once for the run: a single seed at the top leaves each %! ## case starting wherever the ones before it left the stream, so the sample %! ## moments below are only as reproducible as the number of draws every %! ## earlier case happens to take. randg needs seeding too: the Pearson %! ## types are drawn through betarnd, gamrnd and trnd, and seeding rand and %! ## randn alone leaves that stream running on from wherever it was. %! for i = 1:rows (cases) %! rand ("state", 42); randn ("state", 42); randg ("state", 42); %! m = cases(i,1); s = cases(i,2); sk = cases(i,3); ku = cases(i,4); %! r = pearsrnd (m, s, sk, ku, 200000, 1); %! mr = mean (r); sr = std (r); %! g1 = mean (((r - mr) ./ sr) .^ 3); g2 = mean (((r - mr) ./ sr) .^ 4); %! assert_equal (mr, m, 0.05 .* s + 0.02); %! assert_equal (sr, s, 0.05 .* s + 0.02); %! assert_equal (g1, sk, 0.1); %! assert_equal (g2, ku, 0.4); %! endfor ## Negative skewness reflects the positive-skew shape %!test %! rand ("state", 7); randn ("state", 7); %! r = pearsrnd (0, 1, -1, 5, 100000, 1); %! assert_equal (mean (((r - mean (r)) ./ std (r)) .^ 3), -1, 0.1); ## Size handling follows randn %!test %! assert_equal (size (pearsrnd (0, 1, 0, 3, 3, 4)), [3, 4]); %! assert_equal (size (pearsrnd (0, 1, 0, 3, [2, 5])), [2, 5]); %! assert_equal (isscalar (pearsrnd (0, 1, 0, 3)), true); %! assert_equal (size (pearsrnd (0, 1, 0, 3, -1)), [0, 0]); %! assert_equal (size (pearsrnd (0, 1, 0, 3, 2, -1, 5)), [2, 0, 5]); ## Test input validation %!error pearsrnd (0, 1, 0) %!error ... %! pearsrnd (0, 1, 0, [3, 4]) %!error pearsrnd (0, -1, 0, 3) %!error ... %! pearsrnd (0, 1, 1, 1.5) statistics-release-2.1.2/inst/Random_Sampling/qrandn.m000066400000000000000000000062701527311104300230160ustar00rootroot00000000000000## Copyright (C) 2014 - Juan Pablo Carbajal ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{z} =} qrandn (@var{q}, @var{r}, @var{c}) ## @deftypefnx {statistics} {@var{z} =} qrandn (@var{q}, [@var{r}, @var{c}]) ## ## Returns random deviates drawn from a q-Gaussian distribution. ## ## Parameter @var{q} characterizes the q-Gaussian distribution. ## The result has the size indicated by @var{s}. ## ## Reference: ## W. Thistleton, J. A. Marsh, K. Nelson, C. Tsallis (2006) ## "Generalized Box-Muller method for generating q-Gaussian random deviates" ## arXiv:cond-mat/0605570 http://arxiv.org/abs/cond-mat/0605570 ## ## @seealso{rand, randn} ## @end deftypefn function z = qrandn (q, R, C=[]) if (nargin < 2) print_usage; endif if (! isscalar (q)) error ("qrandn: the parameter q must be a scalar."); endif ## Check that q < 3 if (q >= 3) error ("qrandn: the parameter q must be lower than 3."); endif if (numel (R) > 1) S = R; elseif (numel (R) == 1 && isempty (C)) S = [R, 1]; elseif (numel (R) == 1 && ! isempty (C)) S = [R, C]; endif ## Calculate the q to be used on the q-log qGen = (1 + q) / (3 - q); ## Initialize the output vector z = sqrt (-2 * log_q (rand (S), qGen)) .* sin (2 * pi * rand (S)); endfunction ## Returns the q-log of x, using q function a = log_q (x, q) dq = 1 - q; ## Check to see if q = 1 (to double precision) if (abs (dq) < 10 * eps) ## If q is 1, use the usual natural logarithm a = log (x); else ## If q differs from 1, use the definition of the q-log a = (x .^ dq - 1) ./ dq; endif endfunction %!demo %! z = qrandn (-5, 5e6); %! [c x] = hist (z,linspace (-1.5,1.5,200),1); %! figure (1) %! plot (x,c,'r.'); axis tight; axis ([-1.5,1.5]); %! %! z = qrandn (-0.14286, 5e6); %! [c x] = hist (z,linspace (-2,2,200),1); %! figure (2) %! plot (x,c,'r.'); axis tight; axis ([-2,2]); %! %! z = qrandn (2.75, 5e6); %! [c x] = hist (z,linspace (-1e3,1e3,1e3),1); %! figure (3) %! semilogy (x,c,'r.'); axis tight; axis ([-100,100]); %! %! # --------- %! # Figures from the reference paper. ## Tests for input validation %!error qrandn ([1 2], 1) %!error qrandn (4, 1) %!error qrandn (3, 1) %!error qrandn (2.5, 1, 2, 3) %!error qrandn (2.5) ## Tests for output validation %!test %! q = 1.5; %! s = [2, 3]; %! z = qrandn (q, s); %! assert_equal (isnumeric (z) && isequal (size (z), s), true); statistics-release-2.1.2/inst/Random_Sampling/slicesample.m000066400000000000000000000266671527311104300240500ustar00rootroot00000000000000## Copyright (C) 1995-2022 The Octave Project Developers ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{smpl}, @var{neval}] =} slicesample (@var{start}, @var{nsamples}, @var{property}, @var{value}, @dots{}) ## ## Draws @var{nsamples} samples from a target stationary distribution @var{pdf} ## using slice sampling of Radford M. Neal. ## ## Input: ## @itemize ## @item ## @var{start} is a 1 by @var{dim} vector of the starting point of the ## Markov chain. Each column corresponds to a different dimension. ## ## @item ## @var{nsamples} is the number of samples, the length of the Markov chain. ## @end itemize ## ## Next, several property-value pairs can or must be specified, they are: ## ## (Required properties) One of: ## ## @itemize ## @item ## @var{"pdf"}: the value is a function handle of the target stationary ## distribution to be sampled. The function should accept different locations ## in each row and each column corresponds to a different dimension. ## ## or ## ## @item ## @var{logpdf}: the value is a function handle of the log of the target ## stationary distribution to be sampled. The function should accept different ## locations in each row and each column corresponds to a different dimension. ## @end itemize ## ## The following input property/pair values may be needed depending on the ## desired output: ## ## @itemize ## @item ## "burnin" @var{burnin} the number of points to discard at the beginning, the ## default is 0. ## ## @item ## "thin" @var{thin} omits @var{m}-1 of every @var{m} points in the generated ## Markov chain. The default is 1. ## ## @item ## "width" @var{width} the maximum Manhattan distance between two samples. ## The default is 10. ## @end itemize ## ## Outputs: ## @itemize ## ## @item ## @var{smpl} is a @var{nsamples} by @var{dim} matrix of random ## values drawn from @var{pdf} where the rows are different random values, the ## columns correspond to the dimensions of @var{pdf}. ## ## @item ## @var{neval} is the number of function evaluations per sample. ## @end itemize ## Example : Sampling from a normal distribution ## ## @example ## @group ## start = 1; ## nsamples = 1e3; ## pdf = @@(x) exp (-.5 * x .^ 2) / (pi ^ .5 * 2 ^ .5); ## [smpl, accept] = slicesample (start, nsamples, "pdf", pdf, "thin", 4); ## histfit (smpl); ## @end group ## @end example ## ## @seealso{rand, mhsample, randsample} ## @end deftypefn function [smpl, neval] = slicesample (start, nsamples, varargin) if (nargin < 4) error ("slicesample: function called with too few input arguments."); endif sizestart = size (start); pdf = []; logpdf = []; width = 10; burnin = 0; thin = 1; for k = 1:2:length (varargin) if (ischar (varargin{k})) switch lower (varargin{k}) case 'pdf' if (isa (varargin{k+1}, 'function_handle')) pdf = varargin{k+1}; else error ("slicesample: pdf must be a function handle."); endif case 'logpdf' if (isa (varargin{k+1}, 'function_handle')) ## This assigned to PDF, so the supplied log density was then ## wrapped as log (pdf (x)) and taken the log of a second time. ## The chain sampled from something else entirely, quietly. logpdf = varargin{k+1}; else error ("slicesample: logpdf must be a function handle."); endif case 'width' if (numel (varargin{k+1}) == 1 || numel (varargin{k+1}) == sizestart(2)) width = varargin{k+1}(:).'; else error ("slicesample: width must be a scalar or 1 by dim vector."); endif if (! all (width > 0)) error (strcat ("slicesample: width must contain positive", ... " values that can be added to START.")); endif case 'burnin' if (varargin{k+1}>=0) burnin = varargin{k+1}; else error ("slicesample: burnin must be greater than or equal to 0."); endif case 'thin' if (varargin{k+1}>=1) thin = varargin{k+1}; else error ("slicesample: thin must be greater than or equal to 1."); endif otherwise error ("slicesample: invalid parameter name: %s.", varargin{k}); endswitch else error ("slicesample: %s is not a valid property.", varargin{k}); endif endfor if (! isempty (pdf) && isempty (logpdf)) logpdf = @(x) rloge (pdf (x)); elseif (isempty (pdf) && isempty (logpdf)) error ("slicesample: pdf or logpdf must be input."); endif dim = sizestart(2); ## Asking for no samples yields none; zeros () read a negative count as 1. if (nsamples < 1) nsamples = 0; endif smpl = zeros (nsamples, dim); if (! all (sizestart == [1 dim])) error ("slicesample: start must be a 1 by dim vector."); endif if (nsamples == 0) ## Nothing to draw. Assigning the start point below would grow SMPL back ## to one row and hand back a sample that was never taken. neval = 0; return; endif smpl(1, :) = start; maxit = 100; neval = 0; fgreaterthan = @(x, fxc) logpdf(x) >= fxc; ti = burnin + nsamples * thin; rndexp = rande (ti, 1); crand = rand (ti, dim); prand = rand (ti, dim); xc = smpl(1, :); for i = 1:ti neval++; sliceheight = logpdf(xc) - rndexp(i); c = width .* crand(i, :); lb = xc - c; ub = xc + width - c; #Only for single variable as bounds can not be found with point when dim > 1 if (dim == 1) for k=1:maxit neval++; if (! fgreaterthan(lb, sliceheight)) break endif lb -= width; endfor if (k == maxit) warning ("slicesample: Step out exceeded maximum iterations"); endif for k = 1:maxit neval++; if (! fgreaterthan(ub, sliceheight)) break endif ub += width; endfor if (k == maxit) warning ("slicesample: Step out exceeded maximum iterations"); endif endif xp = (ub - lb) .* prand(i, :) + lb; for k=1:maxit neval++; isgt = fgreaterthan(xp,sliceheight); if (all (isgt)) break endif lc = ! isgt & xp < xc; uc = ! isgt & xp > xc; lb(lc) = xp(lc); ub(uc) = xp(uc); xp = (ub - lb) .* rand (1, dim) + lb; endfor if (k == maxit) warning ("slicesample: Step in exceeded maximum iterations"); endif xc = xp; if (i > burnin) indx = (i - burnin) / thin; if rem (indx, 1) == 0 smpl(indx, :) = xc; endif endif endfor neval = neval / (nsamples * thin + burnin); endfunction function y = rloge (x) y = -inf (size (x)); xg0 = x > 0; y(xg0) = log (x(xg0)); endfunction %!demo %! ## Define function to sample %! rng (42); %! rande ('state', 42); %! d = 2; %! mu = [-1; 2]; %! Sigma = rand (d); %! Sigma = (Sigma + Sigma'); %! Sigma += eye (d)*abs (eigs (Sigma, 1, 'sa')) * 1.1; %! pdf = @(x)(2*pi)^(-d/2)*det (Sigma)^-.5*exp (-.5*sum ((x.'-mu).*(Sigma\(x.'-mu)),1)); %! %! ## Inputs %! start = ones (1,2); %! nsamples = 500; %! K = 500; %! m = 10; %! [smpl, accept] = slicesample (start, nsamples, 'pdf', pdf, 'burnin', K, 'thin', m, 'width', [20, 30]); %! figure; %! hold on; %! plot (smpl(:,1), smpl(:,2), 'x'); %! [x, y] = meshgrid (linspace (-6,4), linspace (-3,7)); %! z = reshape (pdf ([x(:), y(:)]), size (x)); %! mesh (x, y, z, 'facecolor', 'None'); %! %! ## Using sample points to find the volume of half a sphere with radius of .5 %! f = @(x) ((.25-(x(:,1)+1).^2-(x(:,2)-2).^2).^.5.*(((x(:,1)+1).^2+(x(:,2)-2).^2)<.25)).'; %! int = mean (f(smpl) ./ pdf (smpl)); %! errest = std (f(smpl) ./ pdf (smpl)) / nsamples^.5; %! trueerr = abs (2/3*pi*.25^(3/2)-int); %! fprintf ("Monte Carlo integral estimate int f(x) dx = %f\n", int); %! fprintf ("Monte Carlo integral error estimate %f\n", errest); %! fprintf ("The actual error %f\n", trueerr); %! mesh (x,y,reshape (f([x(:), y(:)]), size (x)), 'facecolor', 'None'); %!demo %! ## Integrate truncated normal distribution to find normalization constant %! rng (42); %! rande ('state', 42); %! pdf = @(x) exp (-.5*x.^2)/(pi^.5*2^.5); %! nsamples = 1e3; %! [smpl, accept] = slicesample (1, nsamples, 'pdf', pdf, 'thin', 4); %! f = @(x) exp (-.5 * x .^ 2) .* (x >= -2 & x <= 2); %! x = linspace (-3, 3, 1000); %! area (x, f(x)); %! xlabel ('x'); %! ylabel ('f(x)'); %! int = mean (f(smpl) ./ pdf (smpl)); %! errest = std (f(smpl) ./ pdf (smpl)) / nsamples ^ 0.5; %! trueerr = abs (erf (2 ^ 0.5) * 2 ^ 0.5 * pi ^ 0.5 - int); %! fprintf ("Monte Carlo integral estimate int f(x) dx = %f\n", int); %! fprintf ("Monte Carlo integral error estimate %f\n", errest); %! fprintf ("The actual error %f\n", trueerr); ## Test output %!test %! start = 0.5; %! nsamples = 1e3; %! pdf = @(x) exp (-.5*(x-1).^2)/(2*pi)^.5; %! [smpl, accept] = slicesample (start, nsamples, 'pdf', pdf, 'thin', 2, 'burnin', 0, 'width', 5); %! assert_equal (mean (smpl, 1), 1, .15); %! assert_equal (var (smpl, 1), 1, .25); ## 'logpdf' assigned its handle to PDF, which was then wrapped as ## log (pdf (x)). A supplied log density was therefore logged a second time ## and the chain sampled from something else entirely, without a word. %!test %! rand ('twister', 42); %! s1 = slicesample (0, 2000, 'logpdf', @(z) -z .^ 2 / 2, 'width', 5); %! assert_equal (mean (s1), 0, 0.15); %! assert_equal (std (s1), 1, 0.15); %!test %! ## the same target given as a density must agree to within sampling error %! rand ('twister', 42); %! sp = slicesample (0, 2000, 'pdf', @(z) exp (-z .^ 2 / 2), 'width', 5); %! rand ('twister', 42); %! sl = slicesample (0, 2000, 'logpdf', @(z) -z .^ 2 / 2, 'width', 5); %! assert_equal (mean (sp), mean (sl), 0.15); %! assert_equal (std (sp), std (sl), 0.15); %!test %! ## MATLAB accepts both, so this must produce a usable chain rather than %! ## the log of a log %! rand ('twister', 7); %! sb = slicesample (1, 500, 'pdf', @(z) exp (-z .^ 2 / 2), ... %! 'logpdf', @(z) -z .^ 2 / 2, 'width', 5); %! assert_equal (size (sb), [500, 1]); %! assert_equal (mean (sb), 0, 0.25); ## A sample count of zero or less draws nothing; zeros () read a negative ## count as 1 and the start point was then handed back as a sample. %!test %! assert_equal (size (slicesample (1, 0, 'pdf', @(z) exp (-z .^ 2 / 2))), [0, 1]); %! assert_equal (size (slicesample (1, -5, 'pdf', @(z) exp (-z .^ 2 / 2))), [0, 1]); ## Test input validation %!error ... %! slicesample (1, 50, 'pdf', @(z) exp (-z .^ 2 / 2), 'nosuch', 1) %!error ... %! slicesample (1, 50, 'pdf', @(z) exp (-z .^ 2 / 2), 'width', -1) %!error ... %! slicesample (1, 50, 'pdf', @(z) exp (-z .^ 2 / 2), 'width', 0) %!error slicesample (); %!error slicesample (1); %!error slicesample (1, 1); statistics-release-2.1.2/inst/Regression/000077500000000000000000000000001527311104300204165ustar00rootroot00000000000000statistics-release-2.1.2/inst/Regression/CompactLinearModel.m000066400000000000000000004053411527311104300243050ustar00rootroot00000000000000## Copyright (C) 2026 Avanish Salunke ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef CompactLinearModel < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} CompactLinearModel ## ## Compact linear regression model ## ## The @code{CompactLinearModel} class stores a fitted linear regression ## model without the training data. A @code{CompactLinearModel} object is ## returned by the @code{compact} method of a @code{LinearModel} object, and ## retains everything needed to inspect, predict from, and run inference on ## the fit, while discarding the observations and per-observation ## diagnostics that a @code{LinearModel} object carries. This makes a ## @code{CompactLinearModel} object smaller to store than the ## @code{LinearModel} it was compacted from. ## ## The properties of a @code{CompactLinearModel} object fall into four ## groups: ## ## @multitable @columnfractions 0.22 0.76 ## @headitem Group @tab Properties ## ## @item Coefficient estimates @tab @code{Coefficients} (a table of ## estimates, standard errors, t-statistics, and p-values for each term), ## @code{CoefficientCovariance}, @code{CoefficientNames}, and the ## coefficient counts @code{NumCoefficients} and ## @code{NumEstimatedCoefficients}. ## ## @item Summary statistics of the fit @tab @code{DFE}, @code{MSE}, ## @code{RMSE}, @code{Rsquared} (ordinary and adjusted), @code{SSE}, ## @code{SSR}, @code{SST}, @code{LogLikelihood}, and @code{ModelCriterion} ## (AIC, BIC, etc.). ## ## @item Fitting method information @tab @code{Robust}, which records ## the weighting function and tuning constant used when the model is fit by ## robust regression, and is empty for an ordinary least squares fit. ## ## @item Input data properties @tab @code{Formula}, @code{NumObservations}, ## @code{NumPredictors}, @code{NumVariables}, @code{PredictorNames}, ## @code{ResponseName}, @code{VariableInfo}, and @code{VariableNames}. ## @end multitable ## ## Because the training data is discarded, a @code{CompactLinearModel} ## object has no @code{Fitted}, @code{Residuals}, @code{Diagnostics}, or ## @code{ObservationInfo} properties, and none of its methods refit the ## model. Once created, the following methods are available on a ## @code{CompactLinearModel} object: ## ## @multitable @columnfractions 0.2 0.78 ## @headitem Method @tab Description ## ## @item @code{predict} @tab Predict responses at new predictor values ## given in a matrix or table. Can also return pointwise or simultaneous ## confidence intervals alongside the point predictions. ## ## @item @code{feval} @tab Predict responses given predictors as ## separate scalar or vector arguments (one per predictor variable) instead ## of a single matrix, so a @code{CompactLinearModel} object can be ## evaluated the same way as a plain function handle. Returns point ## predictions only. ## ## @item @code{random} @tab Simulate new response values at new ## predictor locations by adding independent Gaussian noise, drawn from the ## estimated error variance @code{MSE}, to the fitted response. ## ## @item @code{coefCI} @tab Return Wald confidence intervals for every ## fitted coefficient at a chosen significance level (default @math{0.05}). ## ## @item @code{coefTest} @tab Test a linear hypothesis on the fitted ## coefficients. With no arguments, tests the overall model F-test that ## all non-intercept coefficients are zero; a custom hypothesis can be ## given as a contrast matrix and, if needed, right-hand-side values. ## Returns the p-value, and optionally the F-statistic and its numerator ## degrees of freedom. ## ## @item @code{plotEffects} @tab Plot the estimated main effect and ## 95% confidence interval of each predictor, evaluated between its ## observed minimum and maximum with all other predictors held at their ## observed means. ## ## @item @code{plotInteraction} @tab Plot the main and conditional effects ## of two predictors, or the adjusted response as a function of one ## predictor for several fixed values of the other, to visualize whether ## the two predictors interact. ## ## @item @code{anova} @tab Analysis of variance for the fitted model. ## Type 3 raises an error on a model missing a lower-order relative of one ## of its terms, since a @code{CompactLinearModel} object has no data to ## refit with. ## @end multitable ## ## Create a @code{CompactLinearModel} object by using the @code{compact} ## method of a fitted @code{LinearModel} object. ## ## @seealso{LinearModel, LinearModel.compact} ## @end deftp properties(GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} CoefficientCovariance ## ## Covariance matrix of coefficient estimates ## ## A @math{p}-by-@math{p} numeric matrix of covariance values for the ## coefficient estimates, where @math{p} is the number of coefficients in ## the fitted model as given by @code{NumCoefficients}. This property is ## read-only. ## ## @end deftp CoefficientCovariance = []; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} CoefficientNames ## ## Coefficient names ## ## A cell array of character vectors, each containing the name of the ## corresponding model term (e.g., @qcode{'(Intercept)'}, @qcode{'x1'}, ## @qcode{'x1:x2'}). This property is read-only. ## ## @end deftp CoefficientNames = {}; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} Coefficients ## ## Coefficient values ## ## A table with one row for each coefficient and four columns: ## @itemize ## @item @code{Estimate} - estimated coefficient value ## @item @code{SE} - standard error of the estimate ## @item @code{tStat} - t-statistic for a two-sided test ## @item @code{pValue} - p-value for the t-statistic ## @end itemize ## Coefficients that are dropped due to rank deficiency have ## @code{Estimate = 0}, @code{SE = 0}, @code{tStat = NaN}, ## @code{pValue = NaN}. This property is read-only. ## ## @end deftp Coefficients = []; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} NumCoefficients ## ## Number of model coefficients ## ## A positive integer giving the total number of coefficients in the fitted ## model, including any coefficients set to zero because the model terms are ## rank deficient. This property is read-only. ## ## @end deftp NumCoefficients = []; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} NumEstimatedCoefficients ## ## Number of estimated coefficients ## ## A positive integer giving the number of coefficients actually estimated, ## i.e., not set to zero due to rank deficiency. ## @code{NumEstimatedCoefficients} equals the degrees of freedom for ## regression. This property is read-only. ## ## @end deftp NumEstimatedCoefficients = []; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} DFE ## ## Degrees of freedom for error ## ## A positive integer equal to the number of observations minus the number ## of estimated coefficients: @code{DFE = NumObservations - ## NumEstimatedCoefficients}. This property is read-only. ## ## @end deftp DFE = []; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} LogLikelihood ## ## Log-likelihood of the fitted model ## ## A scalar numeric value equal to the log-likelihood of the response ## values, assuming each response is normally distributed with mean equal ## to the fitted value and variance equal to @math{SSE/n} (the MLE ## variance estimate). This property is read-only. ## ## @end deftp LogLikelihood = []; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} ModelCriterion ## ## Model comparison criteria ## ## A structure with four fields: ## @itemize ## @item @code{AIC} - Akaike information criterion: ## @math{-2 * logL + 2 * m} ## @item @code{AICc} - AIC corrected for sample size: ## @math{AIC + (2*m*(m+1))/(n-m-1)} ## @item @code{BIC} - Bayesian information criterion: ## @math{-2 * logL + m * log(n)} ## @item @code{CAIC} - Consistent AIC: ## @math{-2 * logL + m * (log(n) + 1)} ## @end itemize ## Here @math{logL} is @code{LogLikelihood}, @math{m} is ## @code{NumEstimatedCoefficients}, and @math{n} is ## @code{NumObservations}. This property is read-only. ## ## @end deftp ModelCriterion = []; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} MSE ## ## Mean squared error ## ## A scalar numeric value equal to @math{SSE / DFE}, where @code{SSE} is ## the sum of squared errors and @code{DFE} is the degrees of freedom for ## error. This property is read-only. ## ## @end deftp MSE = []; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} RMSE ## ## Root mean squared error ## ## A scalar numeric value equal to @math{sqrt(MSE)}. This property is ## read-only. ## ## @end deftp RMSE = []; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} Rsquared ## ## R-squared goodness-of-fit statistics ## ## A structure with two fields: ## @itemize ## @item @code{Ordinary} - coefficient of determination: ## @math{R^2 = SSR / SST} ## @item @code{Adjusted} - adjusted @math{R^2} that accounts for the ## number of coefficients in the model ## @end itemize ## This property is read-only. ## ## @end deftp Rsquared = []; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} SSE ## ## Sum of squared errors ## ## A scalar numeric value equal to the sum of squared residuals. For a ## model with an intercept, @math{SST = SSE + SSR}. For weighted fits, ## this is the weighted sum of squares. This property is read-only. ## ## @end deftp SSE = []; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} SSR ## ## Regression sum of squares ## ## A scalar numeric value equal to the sum of squared deviations of the ## fitted values from the mean of the response. For a model with an ## intercept, @math{SST = SSE + SSR}. For weighted fits, this is the ## weighted sum of squares. This property is read-only. ## ## @end deftp SSR = []; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} SST ## ## Total sum of squares ## ## A scalar numeric value equal to the sum of squared deviations of the ## response from its mean. For a model with an intercept, ## @math{SST = SSE + SSR}. For a robust fit, @math{SST = SSE + SSR} ## rather than the deviation from the mean. For weighted fits, this is ## the weighted sum of squares. This property is read-only. ## ## @end deftp SST = []; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} Robust ## ## Robust fit information ## ## A structure with three fields: ## @itemize ## @item @code{WgtFun} - robust weighting function name, e.g. ## @qcode{'bisquare'} ## @item @code{Tune} - tuning constant; empty if @code{WgtFun} is ## @qcode{'ols'} or a function handle with the default tuning constant ## @item @code{Weights} - vector of final iteration weights; always ## empty for a @code{CompactLinearModel} object ## @end itemize ## This structure is empty unless the model was fit using robust ## regression. This property is read-only. ## ## @end deftp Robust = []; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} Formula ## ## Model formula information ## ## A structure representing the model formula with fields including ## @code{ResponseName}, @code{LinearPredictor}, @code{PredictorNames}, ## @code{TermNames}, @code{HasIntercept}, @code{Terms} (the terms ## matrix), and @code{InModel}. This property is read-only. ## ## @end deftp Formula = []; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} NumObservations ## ## Number of observations used in the fit ## ## A positive integer giving the number of observations actually used in ## fitting the original model. Rows with missing values and rows ## excluded via the @code{'Exclude'} name-value argument are not counted. ## This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} NumPredictors ## ## Number of predictor variables ## ## A positive integer giving the number of predictor variables used to ## fit the model. This property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} NumVariables ## ## Number of variables in the input data ## ## A positive integer giving the total number of variables in the input ## data used to fit the original model, counting predictors, the ## response, and any unused columns. This property is read-only. ## ## @end deftp NumVariables = []; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} PredictorNames ## ## Names of predictor variables ## ## A cell array of character vectors containing the names of the ## predictor variables used to fit the model. This property is ## read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} ResponseName ## ## Response variable name ## ## A character vector containing the name of the response variable. ## This property is read-only. ## ## @end deftp ResponseName = ''; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} VariableInfo ## ## Information about input variables ## ## A table with one row per variable including any unused variables, and ## four columns: ## @itemize ## @item @code{Class} - variable class as a character vector, e.g. ## @qcode{'double'} or @qcode{'categorical'} ## @item @code{Range} - for continuous variables, a two-element vector ## @code{[min, max]}; for categorical variables, a vector of the ## distinct values ## @item @code{InModel} - logical; true if the variable is in the ## fitted model ## @item @code{IsCategorical} - logical; true if the variable is ## categorical ## @end itemize ## This property is read-only. ## ## @end deftp VariableInfo = []; ## -*- texinfo -*- ## @deftp {CompactLinearModel} {property} VariableNames ## ## Names of all variables in the input data ## ## A cell array of character vectors containing the names of all ## variables used to fit the original model, including predictors, the ## response, and unused variables. This property is read-only. ## ## @end deftp VariableNames = {}; endproperties properties(Access = private, Hidden) ## Terms matrix from modelspec or parse_modelspec TermsMatrix = []; ## Categorical level info for re-encoding in predict CatLevelInfo = []; ## Predictor names after categorical dummy expansion EncPredictorNames = {}; ## Cached per-predictor design contrasts used by plotEffects EffectContrasts = []; ## Cached per-predictor-pair design contrasts used by plotInteraction InteractionContrasts = []; ## Whether the model includes an intercept term HasIntercept = true; ## F-statistic of the fitted model vs. the intercept-only model ModelFitVsNullModel = []; endproperties methods(Hidden) ## Custom display function disp (this) if (isempty (this.Robust)) fprintf ("\n Compact linear regression model:\n"); else fprintf ("\n Compact linear regression model (robust fit):\n"); endif if (! isempty (this.Formula) && isa (this.Formula, 'LinearFormula')) fprintf (" %s\n", char (this.Formula)); endif if (! isempty (this.Coefficients)) fprintf ("\n Estimated Coefficients:\n\n"); disp (this.Coefficients); endif fprintf ("\n"); if (! isempty (this.NumObservations) && ! isempty (this.DFE)) fprintf ("Number of observations: %d, Error degrees of freedom: %d\n", ... this.NumObservations, this.DFE); endif if (! isempty (this.RMSE)) fprintf ("Root Mean Squared Error: %g\n", this.RMSE); endif if (! isempty (this.Rsquared) && isstruct (this.Rsquared)) fprintf ("R-squared: %g, Adjusted R-Squared: %g\n", ... this.Rsquared.Ordinary, this.Rsquared.Adjusted); endif if (! isempty (this.ModelFitVsNullModel) ... && isstruct (this.ModelFitVsNullModel) ... && isfield (this.ModelFitVsNullModel, 'Fstat')) fprintf ("F-statistic vs. constant model: %g, p-value = %g\n", ... this.ModelFitVsNullModel.Fstat, ... this.ModelFitVsNullModel.Pvalue); endif endfunction ## Class specific subscripted reference function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); switch (s.type) case '()' error (strcat ("CompactLinearModel: () indexing is not", ... " supported. Use dot notation to access", ... " properties.")); case '{}' error (strcat ("CompactLinearModel: {} indexing is not", ... " supported. Use dot notation to access", ... " properties.")); case '.' if (! ischar (s.subs)) error ("CompactLinearModel.subsref: property name must be a character vector."); endif if (ismethod (this, s.subs)) [varargout{1:nargout}] = builtin ('subsref', this, [s, chain_s]); return; endif try out = this.(s.subs); catch error ("CompactLinearModel.subsref: unknown property '%s'.", s.subs); end_try_catch endswitch if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction ## -*- texinfo -*- ## @deftypefn {CompactLinearModel} {@var{cmdl} =} CompactLinearModel () ## @deftypefnx {CompactLinearModel} {@var{cmdl} =} CompactLinearModel (@var{mdl}) ## ## Create a compact linear regression model. ## ## @code{@var{cmdl} = CompactLinearModel ()} returns a ## @code{CompactLinearModel} object with all properties empty. ## ## @code{@var{cmdl} = CompactLinearModel (@var{mdl})} copies the ## coefficient estimates, fit statistics, and input data description ## from the fitted @code{LinearModel} object @var{mdl} into a new ## @code{CompactLinearModel} object @var{cmdl}, discarding the training ## data, per-observation diagnostics, and stepwise fitting history. If ## @var{mdl} was fit using robust regression, the @code{Weights} field of ## @code{@var{cmdl}.Robust} is emptied, although the rest of the ## @code{Robust} structure is retained. ## ## The usual way to obtain a @code{CompactLinearModel} object is to call ## the @code{compact} method on an already-fitted @code{LinearModel} ## object, rather than calling this constructor directly. ## ## @seealso{LinearModel, LinearModel.compact} ## @end deftypefn function this = CompactLinearModel (mdl = []) if (isempty (mdl)) return; elseif (! isa (mdl, 'LinearModel')) error ("CompactLinearModel: invalid model object."); endif this.CoefficientCovariance = mdl.CoefficientCovariance; this.CoefficientNames = mdl.CoefficientNames; this.Coefficients = mdl.Coefficients; this.NumCoefficients = mdl.NumCoefficients; this.NumEstimatedCoefficients = mdl.NumEstimatedCoefficients; this.DFE = mdl.DFE; this.LogLikelihood = mdl.LogLikelihood; this.ModelCriterion = mdl.ModelCriterion; this.MSE = mdl.MSE; this.RMSE = mdl.RMSE; this.Rsquared = mdl.Rsquared; this.SSE = mdl.SSE; this.SSR = mdl.SSR; this.SST = mdl.SST; this.Robust = mdl.Robust; if (isstruct (this.Robust) && isfield (this.Robust, 'Weights')) this.Robust.Weights = []; endif this.Formula = mdl.Formula; this.NumObservations = mdl.NumObservations; this.NumPredictors = mdl.NumPredictors; this.NumVariables = mdl.NumVariables; this.PredictorNames = mdl.PredictorNames; this.ResponseName = mdl.ResponseName; this.VariableInfo = mdl.VariableInfo; this.VariableNames = mdl.VariableNames; this.TermsMatrix = mdl.TermsMatrix; this.CatLevelInfo = mdl.CatLevelInfo; this.EncPredictorNames = mdl.EncPredictorNames; this.EffectContrasts = mdl.EffectContrasts; this.InteractionContrasts = mdl.InteractionContrasts; this.HasIntercept = mdl.HasIntercept; this.ModelFitVsNullModel = mdl.ModelFitVsNullModel; endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {CompactLinearModel} {@var{ci} =} coefCI (@var{mdl}) ## @deftypefnx {CompactLinearModel} {@var{ci} =} coefCI (@var{mdl}, @var{alpha}) ## ## Confidence intervals for the coefficient estimates of a fitted linear ## regression model. ## ## @code{@var{ci} = coefCI (@var{mdl})} returns 95% confidence intervals ## for every coefficient in @var{mdl} using a default significance level of ## @code{0.05}. ## ## @code{@var{ci} = coefCI (@var{mdl}, @var{alpha})} uses the significance ## level @var{alpha}, a scalar in @math{[0, 1]}. The resulting intervals ## have coverage @math{100(1-\alpha)\%}. Setting @var{alpha} to @code{0} ## produces intervals of infinite width; setting it to @code{1} collapses ## each interval to the corresponding point estimate. ## ## The output @var{ci} is a @math{k}-by-2 numeric matrix where ## @math{k = } @code{@var{mdl}.NumCoefficients}. Row @math{j} contains ## the interval for the @math{j}-th coefficient, whose name is stored in ## @code{@var{mdl}.CoefficientNames@{j@}}. Column 1 is the lower bound and ## column 2 is the upper bound. The midpoint of each interval equals the ## corresponding point estimate in @code{@var{mdl}.Coefficients.Estimate}. ## ## Intervals use the Wald method: ## @math{b_j \pm t_{(1-\alpha/2,\,\mathrm{DFE})}\,\mathrm{SE}(b_j)}, ## where @math{b_j} is the coefficient estimate, @math{\mathrm{SE}(b_j)} is ## its standard error from @code{@var{mdl}.Coefficients.SE}, and the ## critical value is the @math{1-\alpha/2} quantile of the ## @math{t}-distribution with @code{@var{mdl}.DFE} degrees of freedom. ## In rank-deficient models, aliased coefficients have ## @math{\mathrm{SE} = 0} and their row in @var{ci} is @code{[0, 0]}. ## ## @end deftypefn function ci = coefCI (mdl, alpha) if (nargin > 2) error ("coefCI: Too many input arguments."); endif if (nargin < 2) alpha = 0.05; endif if (! isscalar (alpha)) error (strcat ("coefCI: Invalid argument at position 2.", ... " Value must be a scalar.")); endif if (! (alpha >= 0)) error (strcat ("coefCI: Invalid argument at position 2.", ... " Value must be greater than or equal to 0.")); endif if (alpha > 1) error (strcat ("coefCI: Invalid argument at position 2.", ... " Value must be less than or equal to 1.")); endif t = tinv (1 - alpha / 2, mdl.DFE); b = mdl.Coefficients.Estimate; se = mdl.Coefficients.SE; ci = [b - t .* se, b + t .* se]; endfunction ## -*- texinfo -*- ## @deftypefn {CompactLinearModel} {@var{p} =} coefTest (@var{mdl}) ## @deftypefnx {CompactLinearModel} {@var{p} =} coefTest (@var{mdl}, @var{H}) ## @deftypefnx {CompactLinearModel} {@var{p} =} coefTest (@var{mdl}, @var{H}, @var{C}) ## @deftypefnx {CompactLinearModel} {[@var{p}, @var{F}] =} coefTest (@dots{}) ## @deftypefnx {CompactLinearModel} {[@var{p}, @var{F}, @var{r}] =} coefTest (@dots{}) ## ## Linear hypothesis test on the coefficients of a fitted linear regression ## model. ## ## @code{coefTest} tests whether one or more linear combinations of the ## fitted coefficients equal specified constants. Each linear combination ## is encoded as a row of the contrast matrix @var{H}, and the right-hand ## side is given by @var{C}. ## ## @code{@var{p} = coefTest (@var{mdl})} performs the overall model F-test: ## it tests the joint null hypothesis that every coefficient except the ## intercept is zero. The returned p-value matches the F-statistic line ## printed at the bottom of the model display. ## ## @code{@var{p} = coefTest (@var{mdl}, @var{H})} tests the null hypothesis ## @math{H \beta = 0}, where @math{\beta} is the full coefficient vector ## of length @math{k = } @code{@var{mdl}.NumCoefficients}. @var{H} must be ## a full-rank numeric matrix with @math{k} columns; each row specifies one ## linear constraint. To test a single coefficient, use a row vector with a ## @code{1} in that coefficient's position and zeros elsewhere; the ## resulting F-statistic equals the square of the corresponding t-statistic ## in @code{@var{mdl}.Coefficients}. To test a categorical predictor that ## expands to multiple indicator columns, include one row per indicator in ## @var{H}. ## ## @code{@var{p} = coefTest (@var{mdl}, @var{H}, @var{C})} tests ## @math{H \beta = C} instead of zero. @var{C} must be a numeric vector ## with the same number of elements as rows of @var{H}; both row and column ## vectors are accepted. ## ## The second output @var{F} is the value of the F-statistic: ## @math{F = (H\hat{\beta} - C)^\prime (H V H^\prime)^{-1} ## (H\hat{\beta} - C) / r}, where @math{V} is ## @code{@var{mdl}.CoefficientCovariance} and @math{r} is the number of ## rows of @var{H}. The third output @var{r} is that numerator degrees of ## freedom; the denominator degrees of freedom is @code{@var{mdl}.DFE}. ## Under the null hypothesis @math{F} follows an @math{F(r, \mathrm{DFE})} ## distribution and the p-value is the upper-tail probability. When ## @var{H} is rank-deficient but contains no @code{NaN}, both @var{p} and ## @var{F} are returned as @code{NaN} without an error. ## ## @end deftypefn function [p, F, r] = coefTest (mdl, varargin) if (nargout > 3) error ("coefTest: Too many output arguments."); endif if (numel (varargin) > 2) error ("coefTest: Too many input arguments."); endif k = mdl.NumCoefficients; if (numel (varargin) >= 1 && ! isempty (varargin{1})) H = varargin{1}; if (! isnumeric (H)) error ("coefTest: H must be a %d-by-%d numeric matrix.", size (H, 1), k); endif if (size (H, 2) != k) error ("coefTest: H must be a %d-by-%d numeric matrix.", size (H, 1), k); endif if (any (any (isnan (H)))) error (strcat ("coefTest: H is not full rank and hypotheses", ... " are not consistent.")); endif r = size (H, 1); if (numel (varargin) == 2) C = varargin{2}; if (! isnumeric (C)) error ("coefTest: C must be a numeric vector."); endif C = C(:); if (numel (C) != r) error ("coefTest: H must be a %d-by-%d numeric matrix.", numel (C), k); endif else C = zeros (r, 1); endif else if (mdl.HasIntercept && k > 1) H = [zeros(k-1, 1), eye(k-1)]; r = k - 1; else H = eye (k); r = k; endif C = zeros (r, 1); endif b = mdl.Coefficients.Estimate; V = mdl.CoefficientCovariance; HVH = H * V * H'; Hb_c = H * b - C; if (rcond (HVH) < eps (class (HVH))) F = NaN; p = NaN; else F = (Hb_c' * (HVH \ Hb_c)) / r; p = betainc (mdl.DFE / (mdl.DFE + r * F), mdl.DFE / 2, r / 2); endif endfunction ## -*- texinfo -*- ## @deftypefn {CompactLinearModel} {@var{ypred} =} predict (@var{mdl}, @var{Xnew}) ## @deftypefnx {CompactLinearModel} {[@var{ypred}, @var{yci}] =} predict (@var{mdl}, @var{Xnew}) ## @deftypefnx {CompactLinearModel} {[@var{ypred}, @var{yci}] =} predict (@var{mdl}, @var{Xnew}, @var{Name}, @var{Value}) ## ## Predict responses from a fitted linear regression model. ## ## @code{@var{ypred} = predict (@var{mdl}, @var{Xnew})} returns the fitted ## response values at the new predictor locations in @var{Xnew}. @var{Xnew} ## can be a numeric matrix with one column per predictor in the same order ## as the training data, or a table whose column names match ## @code{@var{mdl}.PredictorNames}. Rows containing @code{NaN} are returned ## as @code{NaN} without error. Unlike @code{LinearModel}, @var{Xnew} is ## required: a @code{CompactLinearModel} object does not store the ## training data, so there is no default to fall back on when it is ## omitted. ## ## @code{[@var{ypred}, @var{yci}] = predict (@dots{})} also returns ## @var{yci}, an @math{n}-by-2 matrix of confidence bounds where column 1 is ## the lower bound and column 2 is the upper bound. By default these are ## 95% pointwise confidence intervals on the mean response. ## ## Name-Value pair arguments: ## ## @multitable @columnfractions 0.2 0.78 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab Significance level for the confidence ## interval, specified as a scalar in @math{[0,1]}. The interval has ## coverage @math{100(1-\alpha)\%}. Default is @code{0.05}, giving a 95% ## interval. ## ## @item @qcode{'Prediction'} @tab Type of interval to compute. ## @code{"curve"} (default) gives a confidence interval on the mean response ## @math{f(x)}. @code{"observation"} gives a wider prediction interval for ## a single future observation @math{y = f(x) + \varepsilon}, which accounts ## for both estimation uncertainty and irreducible noise; it adds ## @code{@var{mdl}.MSE} to the variance before computing the half-width. ## ## @item @qcode{'Simultaneous'} @tab Logical flag controlling whether ## the bounds are simultaneous or pointwise. When @code{true}, ## Scheff@'{e}'s method is used so the entire predicted curve lies within ## the band with @math{100(1-\alpha)\%} confidence; these bands are always ## wider than pointwise ones. Default is @code{false}. ## @end multitable ## ## @end deftypefn function [ypred, yci] = predict (mdl, Xnew, varargin) if (nargin < 2) error ("predict: Not enough input arguments."); endif alpha = 0.05; pred_obs = false; simultan = false; i = 1; while (i <= numel (varargin)) if (strcmpi (varargin{i}, 'Alpha')) alpha = varargin{i+1}; if (! isscalar (alpha) || ! isnumeric (alpha) || alpha < 0 || alpha > 1) error ("predict: Alpha must be a scalar in [0,1]."); endif i += 2; elseif (strcmpi (varargin{i}, 'Prediction')) pred_str = lower (char (varargin{i+1})); if (! any (strcmp (pred_str, {'curve', 'observation'}))) error ("predict: Prediction must be 'curve' or 'observation'."); endif pred_obs = strcmp (pred_str, 'observation'); i += 2; elseif (strcmpi (varargin{i}, 'Simultaneous')) simultan = logical (varargin{i+1}); i += 2; else error ("predict: unknown option '%s'.", varargin{i}); endif endwhile pred_names = mdl.PredictorNames; p_raw = mdl.NumPredictors; if (istable (Xnew)) n_new = height (Xnew); X_raw = zeros (n_new, p_raw); for j = 1:p_raw if (! ismember (pred_names{j}, Xnew.Properties.VariableNames)) error ("predict: Xnew table is missing predictor '%s'.", pred_names{j}); endif col = Xnew.(pred_names{j}); if (iscell (col)) cat_idx = []; if (! isempty (mdl.CatLevelInfo.names)) cat_idx = find (strcmp (mdl.CatLevelInfo.names, pred_names{j})); endif if (! isempty (cat_idx)) levels_j = mdl.CatLevelInfo.levels{cat_idx}; codes = zeros (n_new, 1); for k = 1:numel (levels_j) codes(strcmp (col, levels_j{k})) = k; endfor X_raw(:, j) = codes; endif else X_raw(:, j) = double (col); endif endfor else X_raw = double (Xnew); if (columns (X_raw) != p_raw) error ("predict: Xnew must have %d columns.", p_raw); endif n_new = rows (X_raw); endif nan_rows = any (isnan (X_raw), 2); X_enc_new = reencode_predictors (X_raw, pred_names, mdl.CatLevelInfo, mdl.EncPredictorNames); X_design_new = build_design (mdl.TermsMatrix, X_enc_new); beta = mdl.Coefficients.Estimate; ypred = X_design_new * beta; ypred(nan_rows) = NaN; if (nargout > 1) CovB = mdl.CoefficientCovariance; var_cv = sum ((X_design_new * CovB) .* X_design_new, 2); if (pred_obs) var_ci = var_cv + mdl.MSE; else var_ci = var_cv; endif p_est = mdl.NumEstimatedCoefficients; if (simultan) mult = sqrt (p_est * finv (1 - alpha, p_est, mdl.DFE)); else mult = tinv (1 - alpha / 2, mdl.DFE); endif hw = mult * sqrt (max (var_ci, 0)); yci = [ypred - hw, ypred + hw]; yci(nan_rows,:) = NaN; endif endfunction ## -*- texinfo -*- ## @deftypefn {CompactLinearModel} {@var{ysim} =} random (@var{mdl}, @var{Xnew}) ## ## Simulate responses with random noise from a fitted linear regression ## model. ## ## @code{@var{ysim} = random (@var{mdl}, @var{Xnew})} computes the fitted ## response at each row of @var{Xnew} and then adds independent Gaussian ## noise to each value. The noise is drawn from @math{N(0, \sigma^2)} where ## @math{\sigma^2} is the estimated error variance @code{@var{mdl}.MSE} ## (mean squared error of the fit). The result is a column vector of the ## same length as the number of rows in @var{Xnew}. ## ## @var{Xnew} is required and must be non-empty. It can be a numeric ## matrix with one column per predictor in the same order as the training ## data, or a table whose column names match ## @code{@var{mdl}.PredictorNames}. ## ## Because the added noise is drawn freshly on every call, two calls with ## the same @var{Xnew} will generally produce different output. To get ## reproducible results, set the random seed with @code{rand ('state', s)} ## before calling @code{random}. ## ## For deterministic predictions without noise, use @code{predict} or ## @code{feval}. @code{predict} also provides confidence intervals on the ## mean response. ## ## @end deftypefn function ysim = random (mdl, Xnew, varargin) if (nargin < 2) error ("random: Not enough input arguments."); endif if (nargin > 2) error ("random: Too many input arguments."); endif if (isempty (Xnew)) error ("random: Xnew must have %d columns.", mdl.NumPredictors); endif ypred = predict (mdl, Xnew); ysim = ypred + sqrt (mdl.MSE) .* randn (numel (ypred), 1); endfunction ## -*- texinfo -*- ## @deftypefn {CompactLinearModel} {@var{ypred} =} feval (@var{mdl}, @var{X}) ## @deftypefnx {CompactLinearModel} {@var{ypred} =} feval (@var{mdl}, @var{x1}, @var{x2}, @dots{}, @var{xp}) ## ## Predict responses of a fitted linear regression model using separate ## predictor inputs. ## ## @code{@var{ypred} = feval (@var{mdl}, @var{X})} accepts a single ## numeric matrix @var{X} with one column per predictor in the same order ## as the training data, or a table whose column names match ## @code{@var{mdl}.PredictorNames}. The output is an @math{n}-by-1 column ## vector. Rows that contain @code{NaN} in any predictor column are ## returned as @code{NaN}. ## ## @code{@var{ypred} = feval (@var{mdl}, @var{x1}, @var{x2}, @dots{}, ## @var{xp})} accepts exactly @code{@var{mdl}.NumPredictors} separate ## arguments, one per predictor variable. All non-scalar arguments must ## have the same size; a scalar argument is broadcast to that size ## automatically. The output shape follows the shape of the non-scalar ## inputs: column vector inputs give a column vector output, row vector ## inputs give a row vector output, and all-scalar inputs give a scalar. ## This form is convenient when predictor data is already stored in separate ## vectors rather than a combined matrix. ## ## @code{feval} gives the same numerical predictions as @code{predict} but ## does not support confidence intervals. Use @code{predict} when you also ## need bounds on the response. Because a @code{CompactLinearModel} object ## behaves like a function through @code{feval}, it can be passed directly ## to routines that accept a function handle, such as @code{fminsearch} or ## @code{integral}. ## ## @end deftypefn function ypred = feval (mdl, varargin) p_raw = mdl.NumPredictors; n_extra = nargin - 1; if (n_extra < 1) error ("feval: Not enough input arguments."); endif if (n_extra == 1) Xnew = varargin{1}; if (istable (Xnew)) for j = 1:p_raw if (! ismember (mdl.PredictorNames{j}, Xnew.Properties.VariableNames)) error (strcat ("feval: X does not contain one or more", ... " predictor variables needed for this model.")); endif endfor else if (columns (double (Xnew)) != p_raw) error ("feval: Predictor data matrix must have %d columns.", p_raw); endif endif ypred = predict (mdl, Xnew); elseif (n_extra == p_raw) for i = 1:n_extra if (ischar (varargin{i}) || iscategorical (varargin{i})) if (iscategorical (varargin{i})) lvl_str = char (varargin{i}); else lvl_str = varargin{i}; endif ci = []; if (! isempty (mdl.CatLevelInfo.names)) ci = find (strcmp (mdl.CatLevelInfo.names, mdl.PredictorNames{i})); endif if (isempty (ci)) error ("feval: predictor '%s' is not categorical.", mdl.PredictorNames{i}); endif levels_i = mdl.CatLevelInfo.levels{ci}; code = find (strcmp (levels_i, lvl_str), 1); if (isempty (code)) code = NaN; endif varargin{i} = code; endif endfor ref_size = []; for i = 1:n_extra if (! isscalar (varargin{i})) s_i = size (varargin{i}); if (isempty (ref_size)) ref_size = s_i; elseif (! isequal (s_i, ref_size)) error ("feval: All input arguments must be the same size."); endif endif endfor if (isempty (ref_size)) ref_size = [1, 1]; endif n_pts = prod (ref_size); Xmat = zeros (n_pts, p_raw); for i = 1:n_extra ai = varargin{i}; if (isscalar (ai)) Xmat(:, i) = ai; else Xmat(:, i) = ai(:); endif endfor ypred = reshape (predict (mdl, Xmat), ref_size); else error (strcat ("feval: Incorrect number of input arguments. You", ... " must provide either %d separate predictor", ... " variable arguments, or one predictor matrix with", ... " %d columns."), p_raw, p_raw); endif endfunction ## -*- texinfo -*- ## @deftypefn {CompactLinearModel} {} plotEffects (@var{mdl}) ## @deftypefnx {CompactLinearModel} {} plotEffects (@var{ax}, @var{mdl}) ## @deftypefnx {CompactLinearModel} {@var{h} =} plotEffects (@dots{}) ## ## Plot the main effects of each predictor in a compact linear regression ## model. ## ## @code{plotEffects (@var{mdl})} creates a horizontal dot-and-line plot ## with one row per predictor. Each dot shows the estimated main effect on ## the response from changing that predictor from its minimum observed value ## to its maximum observed value, while holding all other predictors fixed ## at their observed means. A horizontal line through each dot shows the ## 95% confidence interval for that effect. ## ## The main effect for predictor @var{xs} is defined as ## @math{g(x_{s,\max}) - g(x_{s,\min})}, where the adjusted response ## function @math{g} evaluates the model at the specified value of ## @var{xs} with all other predictors set to their observed means. ## For numeric predictors the sign of the effect can be positive or ## negative depending on the direction of the relationship. Because a ## @code{CompactLinearModel} does not retain the training data, these ## values come from a summary computed once when the model was fitted, ## rather than recomputed from the original observations. ## ## @code{plotEffects (@var{ax}, @var{mdl})} creates the plot in the axes ## object @var{ax} instead of the current axes returned by @code{gca}. ## ## @code{@var{h} = plotEffects (@dots{})} returns a vector of ## @math{p+1} graphics handles where @math{p} is the number of predictors. ## @code{h(1)} is the line object containing the effect estimate markers ## (one circle per predictor, plotted as a single line object with ## @code{XData} of length @math{p} and @code{YData = 1:p}). ## @code{h(j+1)} is the confidence interval line for predictor @math{j}, ## with @code{XData = [ci_lo, ci_hi]} and @code{YData = [j, j]}. ## ## The y-axis tick labels follow the format ## @qcode{'varname: min to max'}, showing the predictor name and the ## minimum and maximum observed values used to compute the effect. ## ## @end deftypefn function h = plotEffects (this, varargin) [ax, mdl, args] = cm_plot_axes (this, varargin); if (! isempty (args)) error ("plotEffects: Wrong number of arguments."); endif p = mdl.NumPredictors; if (! any (any (mdl.TermsMatrix(:, 1:end-1) != 0))) error ("plotEffects: Model has no predictors."); endif if (isempty (ax)) ax = gca (); endif DEF_COLOR = [0.1490, 0.5490, 0.8660]; pred = mdl.PredictorNames; V = mdl.CoefficientCovariance; beta = mdl.Coefficients.Estimate; t_crit = tinv (0.975, mdl.DFE); cinfo = mdl.CatLevelInfo; C = mdl.EffectContrasts; effects = zeros (1, p); ci_lo = zeros (1, p); ci_hi = zeros (1, p); for j = 1:p effects(j) = C(j,:) * beta; SE = sqrt (max (0, C(j,:) * V * C(j,:)')); ci_lo(j) = effects(j) - t_crit * SE; ci_hi(j) = effects(j) + t_crit * SE; endfor hold (ax, 'on'); h(1) = plot (ax, effects, 1:p, ... 'LineStyle', 'none', ... 'Marker', 'o', ... 'MarkerSize', 6, ... 'Color', DEF_COLOR); for j = 1:p h(j+1) = line ([ci_lo(j), ci_hi(j)], [j, j], ... 'LineStyle', '-', ... 'Marker', 'none', ... 'Color', DEF_COLOR, ... 'Parent', ax); endfor hold (ax, 'off'); rn = mdl.VariableInfo.Properties.RowNames; ytl = cell (p, 1); for j = 1:p ci = []; if (! isempty (cinfo) && isfield (cinfo, 'names') && ! isempty (cinfo.names)) ci = find (strcmp (cinfo.names, pred{j})); endif vidx = find (strcmp (rn, pred{j})); rng = mdl.VariableInfo.Range{vidx}; if (! isempty (ci)) levels_j = cinfo.levels{ci}; lo_str = char (levels_j{1}); hi_str = char (levels_j{end}); else lo_str = num2str (rng(1), '%g'); hi_str = num2str (rng(2), '%g'); endif ytl{j} = [pred{j}, ': ', lo_str, ' to ', hi_str]; endfor set (ax, 'YTick', 1:p, 'YTickLabel', ytl, 'YDir', 'reverse'); ylim (ax, [0.5, p + 0.5]); xlabel (ax, 'Main Effect'); ylabel (ax, ''); title (ax, 'Main Effects Plot'); if (nargout == 0) clear h; endif endfunction ## -*- texinfo -*- ## @deftypefn {CompactLinearModel} {} plotInteraction (@var{mdl}, @var{var1}, @var{var2}) ## @deftypefnx {CompactLinearModel} {} plotInteraction (@var{mdl}, @var{var1}, @var{var2}, @var{ptype}) ## @deftypefnx {CompactLinearModel} {} plotInteraction (@var{ax}, @dots{}) ## @deftypefnx {CompactLinearModel} {@var{h} =} plotInteraction (@dots{}) ## ## Plot the interaction effects of two predictors in a compact linear ## regression model. ## ## @code{plotInteraction (@var{mdl}, @var{var1}, @var{var2})} creates a ## plot of the main effects of @var{var1} and @var{var2} together with ## their conditional effects, with horizontal lines through each effect ## value indicating its 95% confidence interval. @var{var1} and ## @var{var2} are each a character vector or string naming a variable in ## @code{mdl.VariableNames}, or a positive integer indexing into ## @code{mdl.VariableNames}; neither may name the response variable, and ## they must be different variables. ## ## The main effect of a predictor is the change in the adjusted response ## between the two predictor values that produce the minimum and maximum ## adjusted response, with the other predictor averaged over its own ## observed values row by row. For a numeric predictor these two values ## are its observed minimum and maximum; for a categorical predictor ## every level is evaluated and the levels producing the minimum and ## maximum adjusted response are used, so the effect is always ## nonnegative. ## ## The conditional effect of @var{var1} is its effect recomputed with ## @var{var2} additionally held fixed at each of a small set of ## conditioning values, and likewise the conditional effect of ## @var{var2} holds @var{var1} fixed. The conditioning values are the ## observed minimum, mean of the minimum and maximum, and maximum for a ## numeric predictor, or every level for a categorical predictor. When ## the main effect and conditional effect points for a predictor do not ## align vertically, the model exhibits an interaction between ## @var{var1} and @var{var2}. Because a @code{CompactLinearModel} does ## not retain the training data, these values come from a summary ## computed once when the model was fitted, rather than recomputed from ## the original observations. ## ## @code{plotInteraction (@var{mdl}, @var{var1}, @var{var2}, @var{ptype})} ## selects the plot type. @var{ptype} is @qcode{'effects'} (default), as ## described above, or @qcode{'predictions'}, which instead plots the ## adjusted response as a function of @var{var2} for each conditioning ## value of @var{var1} held fixed, evaluated over 101 equally spaced ## points spanning the observed range of @var{var2} when @var{var2} is ## numeric, or at each level of @var{var2} when it is categorical. ## ## @code{plotInteraction (@var{ax}, @dots{})} plots into the axes object ## @var{ax} instead of the current axes returned by @code{gca}. ## ## @code{@var{h} = plotInteraction (@dots{})} returns a vector of line ## handles. When @var{ptype} is @qcode{'effects'}, @code{h(1)} is the ## marker line through the two main effect points, @code{h(2)} and ## @code{h(3)} are the confidence interval lines for the main effects of ## @var{var1} and @var{var2}, and the remaining entries are the ## conditional effect points and their confidence intervals, tagged ## @qcode{'conditional1'} for @var{var1} and @qcode{'conditional2'} for ## @var{var2}. The main effect line objects are tagged @qcode{'main'}. ## When @var{ptype} is @qcode{'predictions'}, each entry in @var{h} ## corresponds to one adjusted response curve, one per conditioning ## value of @var{var1}. ## ## @end deftypefn function h = plotInteraction (this, varargin) [ax, mdl, args] = cm_plot_axes (this, varargin); if (numel (args) < 2) error ("plotInteraction: Not enough input arguments."); endif var1 = args{1}; var2 = args{2}; args = args(3:end); ptype = 'effects'; if (! isempty (args) && (ischar (args{1}) || isstring (args{1}))) ptype = lower (char (args{1})); args = args(2:end); if (! any (strcmp (ptype, {'effects', 'predictions'}))) error ("plotInteraction: PTYPE must be 'effects' or 'predictions'."); endif endif if (! isempty (args)) error ("plotInteraction: Too many input arguments."); endif vnames = mdl.VariableNames; if (ischar (var1) || isstring (var1)) v1name = char (var1); if (isempty (find (strcmp (vnames, v1name)))) error ("plotInteraction: '%s' is not a variable for this fit.", v1name); endif elseif (isnumeric (var1) && isscalar (var1)) if (var1 != fix (var1) || var1 < 1) error (strcat ("plotInteraction: Variable must be specified as a", ... " name or a positive integer.")); endif if (var1 > numel (vnames)) error ("plotInteraction: This model only contains %d variables.", numel (vnames)); endif v1name = vnames{var1}; else error (strcat ("plotInteraction: Variable must be specified as a", ... " name or a positive integer.")); endif if (strcmp (v1name, mdl.ResponseName)) error ("plotInteraction: The variable '%s' is the response in this model.", v1name); endif if (ischar (var2) || isstring (var2)) v2name = char (var2); if (isempty (find (strcmp (vnames, v2name)))) error ("plotInteraction: '%s' is not a variable for this fit.", v2name); endif elseif (isnumeric (var2) && isscalar (var2)) if (var2 != fix (var2) || var2 < 1) error (strcat ("plotInteraction: Variable must be specified as a", ... " name or a positive integer.")); endif if (var2 > numel (vnames)) error ("plotInteraction: This model only contains %d variables.", numel (vnames)); endif v2name = vnames{var2}; else error (strcat ("plotInteraction: Variable must be specified as a", ... " name or a positive integer.")); endif if (strcmp (v2name, mdl.ResponseName)) error ("plotInteraction: The variable '%s' is the response in this model.", v2name); endif if (strcmp (v1name, v2name)) error ("plotInteraction: VAR1 and VAR2 must be different variables."); endif pred = mdl.PredictorNames; cinfo = mdl.CatLevelInfo; beta = mdl.Coefficients.Estimate; V = mdl.CoefficientCovariance; t_crit = tinv (0.975, mdl.DFE); C = mdl.EffectContrasts; IC = mdl.InteractionContrasts; rn = mdl.VariableInfo.Properties.RowNames; j1 = find (strcmp (pred, v1name)); j2 = find (strcmp (pred, v2name)); is_cat1 = ! isempty (cinfo) && isfield (cinfo, 'names') ... && any (strcmp (cinfo.names, v1name)); is_cat2 = ! isempty (cinfo) && isfield (cinfo, 'names') ... && any (strcmp (cinfo.names, v2name)); if (is_cat1) ci1 = find (strcmp (cinfo.names, v1name)); levels_1 = cinfo.levels{ci1}; n_lv1 = numel (levels_1); g_lv1 = IC.OwnGridRows{j1} * beta; [~, i_lo1] = min (g_lv1); [~, i_hi1] = max (g_lv1); lbl1 = [v1name, ': ', char(levels_1{i_lo1}), ' to ', char(levels_1{i_hi1})]; grid1 = (1:n_lv1)'; grid1_lbls = cellfun (@(s) char (s), levels_1, 'UniformOutput', false); eff1 = g_lv1(i_hi1) - g_lv1(i_lo1); c_diff1 = IC.OwnGridRows{j1}(i_hi1,:) - IC.OwnGridRows{j1}(i_lo1,:); se1 = sqrt (max (0, c_diff1 * V * c_diff1')); hi1v = i_hi1; lo1v = i_lo1; else vidx1 = find (strcmp (rn, v1name)); rng1 = mdl.VariableInfo.Range{vidx1}; lo1 = rng1(1); hi1 = rng1(2); lbl1 = [v1name, ': ', num2str(lo1), ' to ', num2str(hi1)]; grid1 = [lo1; (lo1+hi1)/2; hi1]; grid1_lbls = arrayfun (@(v) num2str(v,'%g'), grid1, 'UniformOutput', false); eff1 = C(j1,:) * beta; se1 = sqrt (max (0, C(j1,:) * V * C(j1,:)')); hi1v = hi1; lo1v = lo1; endif if (is_cat2) ci2 = find (strcmp (cinfo.names, v2name)); levels_2 = cinfo.levels{ci2}; n_lv2 = numel (levels_2); g_lv2 = IC.OwnGridRows{j2} * beta; [~, i_lo2] = min (g_lv2); [~, i_hi2] = max (g_lv2); lbl2 = [v2name, ': ', char(levels_2{i_lo2}), ' to ', char(levels_2{i_hi2})]; grid2 = (1:n_lv2)'; grid2_lbls = cellfun (@(s) char (s), levels_2, 'UniformOutput', false); eff2 = g_lv2(i_hi2) - g_lv2(i_lo2); c_diff2 = IC.OwnGridRows{j2}(i_hi2,:) - IC.OwnGridRows{j2}(i_lo2,:); se2 = sqrt (max (0, c_diff2 * V * c_diff2')); hi2v = i_hi2; lo2v = i_lo2; else vidx2 = find (strcmp (rn, v2name)); rng2 = mdl.VariableInfo.Range{vidx2}; lo2 = rng2(1); hi2 = rng2(2); lbl2 = [v2name, ': ', num2str(lo2), ' to ', num2str(hi2)]; grid2 = [lo2; (lo2+hi2)/2; hi2]; grid2_lbls = arrayfun (@(v) num2str(v,'%g'), grid2, 'UniformOutput', false); eff2 = C(j2,:) * beta; se2 = sqrt (max (0, C(j2,:) * V * C(j2,:)')); hi2v = hi2; lo2v = lo2; endif P12 = IC.Pairs{j1,j2}; n2 = numel (grid2); if (isempty (P12)) eff_c1 = repmat (eff1, n2, 1); se_c1 = repmat (se1, n2, 1); else n2c = numel (P12.grid2); eff_c1 = zeros (n2, 1); se_c1 = zeros (n2, 1); for k = 1:n2 idx1_hi = find (P12.grid1 == hi1v, 1); idx1_lo = find (P12.grid1 == lo1v, 1); idx2_k = find (P12.grid2 == grid2(k), 1); c_hi = P12.rows((idx1_hi-1)*n2c + idx2_k, :); c_lo = P12.rows((idx1_lo-1)*n2c + idx2_k, :); eff_c1(k) = (c_hi - c_lo) * beta; se_c1(k) = sqrt (max (0, (c_hi - c_lo) * V * (c_hi - c_lo)')); endfor endif P21 = IC.Pairs{j2,j1}; n1 = numel (grid1); if (isempty (P21)) eff_c2 = repmat (eff2, n1, 1); se_c2 = repmat (se2, n1, 1); else n2c = numel (P21.grid2); eff_c2 = zeros (n1, 1); se_c2 = zeros (n1, 1); for k = 1:n1 idx1_hi = find (P21.grid1 == hi2v, 1); idx1_lo = find (P21.grid1 == lo2v, 1); idx2_k = find (P21.grid2 == grid1(k), 1); c_hi = P21.rows((idx1_hi-1)*n2c + idx2_k, :); c_lo = P21.rows((idx1_lo-1)*n2c + idx2_k, :); eff_c2(k) = (c_hi - c_lo) * beta; se_c2(k) = sqrt (max (0, (c_hi - c_lo) * V * (c_hi - c_lo)')); endfor endif if (isempty (ax)) ax = gca (); endif cla (ax); DEF_COLOR = [0.1490, 0.5490, 0.8660]; FIT_COLOR = [0.9600, 0.4660, 0.1600]; if (strcmp (ptype, 'effects')) y_main1 = 1; y_cond1 = (2:(1+n2))'; y_main2 = n2 + 4; y_cond2 = ((n2+5):(n2+4+n1))'; hold (ax, 'on'); line ([0, 0], [0.5, n2 + n1 + 4.5], 'LineStyle', ':', 'Marker', 'none', ... 'Color', [0, 0, 0], 'Parent', ax); h(1) = plot (ax, [eff1, eff2], [y_main1, y_main2], ... 'LineStyle', 'none', 'Marker', 'o', 'Color', DEF_COLOR, ... 'Tag', 'main'); h(2) = line ([eff1 - t_crit*se1, eff1 + t_crit*se1], [y_main1, y_main1], ... 'LineStyle', '-', 'Marker', 'none', 'Color', DEF_COLOR, ... 'Parent', ax, 'Tag', 'main'); h(3) = line ([eff2 - t_crit*se2, eff2 + t_crit*se2], [y_main2, y_main2], ... 'LineStyle', '-', 'Marker', 'none', 'Color', DEF_COLOR, ... 'Parent', ax, 'Tag', 'main'); h(4) = plot (ax, eff_c1, y_cond1, ... 'LineStyle', 'none', 'Marker', 'o', 'Color', FIT_COLOR, ... 'Tag', 'conditional1'); for k = 1:n2 h(4+k) = line ([eff_c1(k) - t_crit*se_c1(k), eff_c1(k) + t_crit*se_c1(k)], ... [y_cond1(k), y_cond1(k)], ... 'LineStyle', '-', 'Marker', 'none', 'Color', FIT_COLOR, ... 'Parent', ax, 'Tag', 'conditional1'); endfor h(5+n2) = plot (ax, eff_c2, y_cond2, ... 'LineStyle', 'none', 'Marker', 'o', 'Color', FIT_COLOR, ... 'Tag', 'conditional2'); for k = 1:n1 h(5+n2+k) = line ([eff_c2(k) - t_crit*se_c2(k), eff_c2(k) + t_crit*se_c2(k)], ... [y_cond2(k), y_cond2(k)], ... 'LineStyle', '-', 'Marker', 'none', 'Color', FIT_COLOR, ... 'Parent', ax, 'Tag', 'conditional2'); endfor hold (ax, 'off'); ytl = cell (2 + n1 + n2, 1); ytl{1} = lbl1; for k = 1:n2 ytl{1+k} = [v2name, '=', grid2_lbls{k}]; endfor ytl{2+n2} = lbl2; for k = 1:n1 ytl{2+n2+k} = [v1name, '=', grid1_lbls{k}]; endfor set (ax, 'YTick', [y_main1; y_cond1; y_main2; y_cond2], ... 'YTickLabel', ytl, 'YDir', 'reverse'); ylim (ax, [0.5, n2 + n1 + 4.5]); xlabel (ax, 'Effect'); ylabel (ax, ''); title (ax, ['Interaction of ', v1name, ' and ', v2name]); else ## 'predictions' if (is_cat2) x_grid2 = (1:n_lv2)'; else x_grid2 = linspace (lo2, hi2, 101)'; endif hold (ax, 'on'); line (NaN, NaN, 'Color', 'none', 'Parent', ax, 'DisplayName', v1name); colors = get (ax, 'ColorOrder'); n_colors = rows (colors); n2c = numel (P12.grid2); for k = 1:n1 idx1_k = find (P12.grid1 == grid1(k), 1); row_lo = (idx1_k - 1) * n2c + 1; row_hi = idx1_k * n2c; rows_k = P12.rows(row_lo:row_hi, :); if (is_cat2) y_curve = rows_k * beta; else deg = n2c - 1; Vm = (P12.grid2(:)) .^ (0:deg); coefs = Vm \ rows_k; Vq = (x_grid2(:)) .^ (0:deg); y_curve = Vq * coefs * beta; endif h(k) = line (x_grid2, y_curve, ... 'Color', colors(mod(k-1, n_colors)+1, :), ... 'LineStyle', '-', 'Marker', 'none', 'Parent', ax, ... 'DisplayName', grid1_lbls{k}); endfor hold (ax, 'off'); if (is_cat2) set (ax, 'XTick', 1:n_lv2, 'XTickLabel', grid2_lbls); endif xlabel (ax, v2name); ylabel (ax, ['Adjusted ', mdl.ResponseName]); title (ax, ['Interaction of ', v1name, ' and ', v2name]); legend (ax, 'show'); endif if (nargout == 0) clear h; endif endfunction ## -*- texinfo -*- ## @deftypefn {CompactLinearModel} {@var{tbl} =} anova (@var{mdl}) ## @deftypefnx {CompactLinearModel} {@var{tbl} =} anova (@var{mdl}, @var{anovatype}) ## @deftypefnx {CompactLinearModel} {@var{tbl} =} anova (@var{mdl}, @qcode{"components"}, @var{sstype}) ## ## Analysis of variance for a compact linear regression model. ## ## @code{anova (@var{mdl})} returns a table @var{tbl} with component ## ANOVA statistics for every term in @var{mdl} except the constant ## term, computed with hierarchical (@qcode{"h"}) sums of squares. Each ## row gives @code{SumSq}, @code{DF}, @code{MeanSq}, @code{F}, and ## @code{pValue} for the corresponding term; the trailing @qcode{Error} ## row gives @code{SumSq = @var{mdl}.SSE}, @code{DF = @var{mdl}.DFE}, ## @code{MeanSq = @var{mdl}.MSE}, and @code{NaN} for @code{F} and ## @code{pValue}. ## ## MATLAB reports @code{F = 1} and @code{pValue = 0.5} on that ## @qcode{Error} row instead. Those are not results: the row's @code{F} ## is its own @code{MeanSq} divided by itself, so it is @code{1} for every ## data set, and the @code{pValue} follows. MATLAB does not use them ## consistently either, reporting @code{NaN} for the same quantity on the ## @qcode{Residual} row of its summary table. This implementation reports ## @code{NaN} in both places. ## ## Every statistic is computed from ## @code{@var{mdl}.Coefficients} and ## @code{@var{mdl}.CoefficientCovariance} alone; a @code{CompactLinearModel} ## never refits, because it does not retain the training data. ## ## @code{anova (@var{mdl}, @var{anovatype})} selects ## @qcode{"components"} (default) or @qcode{"summary"}. For ## @qcode{"summary"}, @var{tbl} always contains rows @qcode{Total}, ## @qcode{Model}, and @qcode{Residual}, and additionally @qcode{. Linear} ## and @qcode{. Nonlinear} whenever @var{mdl} contains an interaction ## term or a continuous term of degree greater than 1. @qcode{Total} ## reports @code{@var{mdl}.SST} with @code{DF = NumObservations - 1}; ## @qcode{Model} reports @code{@var{mdl}.SSR} with @code{DF = ## NumCoefficients - HasIntercept}; @qcode{Residual} reports ## @code{@var{mdl}.SSE} with @code{DF = @var{mdl}.DFE}. Unlike ## @code{anova} on a @code{LinearModel}, @var{tbl} never contains ## @qcode{. Lack of fit} or @qcode{. Pure error} rows, since identifying ## observations with identical predictor values requires the training ## data that a @code{CompactLinearModel} does not retain. ## ## @code{anova (@var{mdl}, @qcode{"components"}, @var{sstype})} selects ## the sum of squares used for the component table: @code{1} (sequential, ## reduction from adding each term in formula order), @code{2} (reduction ## from adding the term to a model containing every term that does not ## contain it), @qcode{"h"} (default; as Type 2, but a higher-degree ## term in the same continuous variable, such as a squared term, is also ## treated as containing the lower-degree term), or @code{3} (reduction ## from adding the term to a model containing every other term, with ## categorical predictors recoded using sum-to-zero deviation contrasts ## instead of @var{mdl}'s reference-level coding). @var{sstype} is ## ignored when @var{anovatype} is @qcode{"summary"}. If @var{mdl} is ## missing a lower-order relative of one of its terms (e.g. an ## interaction without one of its main effects, or a categorical ## predictor fit without an intercept), type @code{3} raises an error, ## since a @code{CompactLinearModel} has no data to refit with. ## ## @seealso{LinearModel, CompactLinearModel.coefTest} ## @end deftypefn function tbl = anova (mdl, varargin) if (numel (varargin) > 2) error ("anova: too many input arguments."); endif anovatype = 'components'; if (numel (varargin) >= 1) anovatype = varargin{1}; valid_types = {'summary', 'components', 'oldcomponents', 'newcomponents'}; if (! ischar (anovatype) || ! any (strcmpi (anovatype, valid_types))) error ("anova: ANOVATYPE must be 'summary' or 'components'."); endif endif sstype = 'h'; if (numel (varargin) == 2 && ! strcmpi (anovatype, 'summary')) sstype = varargin{2}; valid_n = isnumeric (sstype) && isscalar (sstype) && any (sstype == [1, 2, 3]); valid_h = ischar (sstype) && strcmpi (sstype, 'h'); if (! valid_n && ! valid_h) error ("anova: SSTYPE must be 1, 2, or 3."); endif endif [term_name, term_cols] = cm_anova_term_groups ( ... mdl.CoefficientNames, mdl.CatLevelInfo, mdl.HasIntercept); nterm = numel (term_name); if (mdl.HasIntercept) icol = find (strcmp (mdl.CoefficientNames, '(Intercept)')); else icol = []; endif b = mdl.Coefficients.Estimate; V = mdl.CoefficientCovariance; se = mdl.Coefficients.SE; MSE = mdl.MSE; DFE = mdl.DFE; all_cols = 1:mdl.NumCoefficients; if (strcmpi (anovatype, 'summary')) is_nonlinear = false (nterm, 1); for k = 1:nterm parts = strsplit (term_name{k}, ':'); is_nonlinear(k) = (numel (parts) > 1) || ! isempty (strfind (parts{1}, '^')); endfor SumSq = [mdl.SST; mdl.SSR]; DF = [mdl.NumObservations - 1; mdl.NumCoefficients - mdl.HasIntercept]; RowNm = {'Total', 'Model'}; if (any (is_nonlinear)) nl_cols = cell2mat (term_cols(is_nonlinear)); SS_nl = MSE * cm_anova_qform (nl_cols, b, V); DF_nl = numel (nl_cols); SumSq = [SumSq; SumSq(2) - SS_nl; SS_nl]; DF = [DF; DF(2) - DF_nl; DF_nl]; RowNm = [RowNm, {'. Linear', '. Nonlinear'}]; endif SumSq = [SumSq; mdl.SSE]; DF = [DF; DFE]; RowNm = [RowNm, {'Residual'}]; MeanSq = SumSq ./ DF; F = NaN (numel (SumSq), 1); pValue = NaN (numel (SumSq), 1); for r = 2:(numel (SumSq) - 1) F(r) = MeanSq(r) / MSE; pValue(r) = betainc (DFE / (DFE + DF(r) * F(r)), DFE / 2, DF(r) / 2); endfor else use_seq = isnumeric (sstype) && sstype == 1; use_type3 = isnumeric (sstype) && sstype == 3; extended = ischar (sstype) && strcmpi (sstype, 'h'); if (use_type3) ## Deviation coding rewrites a categorical predictor's indicator ## block and leaves everything else alone, so with no categorical ## predictor the two designs are identical and the change of basis ## is the identity. Saying so avoids a synthetic design that ## cannot represent them: it carries one row per term degree, which ## is fewer rows than coefficients for an ordinary additive model, ## and gives every numeric predictor the same values scaled by a ## constant, which makes its numeric columns collinear whatever its ## height. ci_all = mdl.CatLevelInfo; has_cat = ! isempty (ci_all) && isfield (ci_all, 'names') ... && any (ismember (ci_all.names, mdl.PredictorNames)); if (! has_cat) Mm = eye (numel (mdl.CoefficientNames)); is_hier = true; else [Xsyn, Dsyn] = cm_anova_synthetic_design (mdl); Mm = Xsyn \ Dsyn; is_hier = (max (max (abs (Dsyn - Xsyn * Mm))) < 1e-8 * max (max (abs (Dsyn)))) ... && (rcond (Mm) > eps); endif if (! is_hier) error (strcat ("Cannot perform anova with type 3 sums of", ... " squares for a compacted model with missing", ... " lower-order terms.")); endif Hinv = inv (Mm); SumSq = zeros (nterm, 1); DF = zeros (nterm, 1); F = zeros (nterm, 1); for k = 1:nterm Hk = Hinv(term_cols{k}, :); HVH = Hk * V * Hk'; if (rcond (HVH) < eps (class (HVH))) SumSq(k) = 0; DF(k) = 0; F(k) = NaN; continue; endif Hb = Hk * b; DF(k) = numel (term_cols{k}); F(k) = (Hb' * (HVH \ Hb)) / DF(k); SumSq(k) = F(k) * DF(k) * MSE; endfor MeanSq = SumSq ./ DF; ok = DF > 0; pValue = NaN (nterm, 1); pValue(ok) = betainc (DFE ./ (DFE + DF(ok) .* F(ok)), DFE / 2, DF(ok) / 2); else contain_mx = cm_anova_containment (term_name, extended); SumSq = zeros (nterm, 1); DF = zeros (nterm, 1); for k = 1:nterm if (any (se(term_cols{k}) == 0)) SumSq(k) = 0; DF(k) = 0; continue; endif if (use_seq) cmp_cols = union (icol, cell2mat (term_cols(1:k-1))); else keep = true (1, nterm); keep(k) = false; for j = 1:nterm if (j != k && contain_mx(k, j)) keep(j) = false; endif endfor cmp_cols = union (icol, cell2mat (term_cols(keep))); endif full_cols = union (cmp_cols, term_cols{k}); excl_cols = setdiff (all_cols, full_cols); SumSq(k) = cm_anova_ss (term_cols{k}, excl_cols, b, V, MSE); DF(k) = numel (term_cols{k}); endfor MeanSq = SumSq ./ DF; F = MeanSq / MSE; ok = DF > 0; pValue = NaN (nterm, 1); pValue(ok) = betainc (DFE ./ (DFE + DF(ok) .* F(ok)), DFE / 2, DF(ok) / 2); endif SumSq = [SumSq; mdl.SSE]; DF = [DF; DFE]; MeanSq = [MeanSq; MSE]; F = [F; NaN]; pValue = [pValue; NaN]; RowNm = [term_name, {'Error'}]; endif tbl = table (SumSq, DF, MeanSq, F, pValue, ... 'VariableNames', {'SumSq', 'DF', 'MeanSq', 'F', 'pValue'}, ... 'RowNames', RowNm(:)); endfunction endmethods endclassdef ## Duplicated from LinearModel, since we don't have superclass support ## for Octave yet; kept as a separate, self-contained copy here instead ## of being shared across both class files. function [ax, mdl, args] = cm_plot_axes (this, rest) if (isscalar (this) && isgraphics (this, 'axes')) ax = this; mdl = rest{1}; args = rest(2:end); else ax = []; mdl = this; args = rest; endif endfunction ## Duplicated from LinearModel, since we don't have superclass support ## for Octave yet; kept as a separate, self-contained copy here instead ## of being shared across both class files. function [term_name, term_cols] = cm_anova_term_groups (coef_names, cat_info, has_intercept) dummy_names = {}; dummy_bases = {}; for ci = 1:numel (cat_info.names) base_nm = cat_info.names{ci}; levels_c = cat_info.levels{ci}; for L = 1:numel (levels_c) dummy_names{end+1} = [base_nm, '_', char(levels_c{L})]; dummy_bases{end+1} = base_nm; endfor endfor if (has_intercept) orig_idx = find (! strcmp (coef_names, '(Intercept)')); else orig_idx = 1:numel (coef_names); endif non_int = coef_names(orig_idx); term_name = {}; term_cols = {}; for t = 1:numel (non_int) factors_t = strsplit (non_int{t}, ':'); for f = 1:numel (factors_t) idx = find (strcmp (dummy_names, factors_t{f}), 1); if (! isempty (idx)) factors_t{f} = dummy_bases{idx}; endif endfor nm = strjoin (factors_t, ':'); k = find (strcmp (term_name, nm), 1); if (isempty (k)) term_name{end+1} = nm; term_cols{end+1} = orig_idx(t); else term_cols{k}(end+1) = orig_idx(t); endif endfor endfunction ## Duplicated from LinearModel, since we don't have superclass support ## for Octave yet; kept as a separate, self-contained copy here instead ## of being shared across both class files. function contain_mx = cm_anova_containment (term_name, extended) nterm = numel (term_name); factor_list = cell (nterm, 1); for k = 1:nterm parts = strsplit (term_name{k}, ':'); fl = struct ('var', {}, 'exp', {}); for f = 1:numel (parts) p = strsplit (parts{f}, '^'); if (numel (p) == 2) fl(end+1) = struct ('var', p{1}, 'exp', str2double (p{2})); else fl(end+1) = struct ('var', p{1}, 'exp', 1); endif endfor factor_list{k} = fl; endfor contain_mx = false (nterm, nterm); for i = 1:nterm for j = 1:nterm if (i == j) continue; endif fi = factor_list{i}; fj = factor_list{j}; ok = true; for f = 1:numel (fi) match = false; for g = 1:numel (fj) if (strcmp (fi(f).var, fj(g).var)) if (extended) match = (fj(g).exp >= fi(f).exp); else match = (fj(g).exp == fi(f).exp); endif if (match) break; endif endif endfor if (! match) ok = false; break; endif endfor contain_mx(i, j) = ok; endfor endfor endfunction ## Duplicated from LinearModel, since we don't have superclass support ## for Octave yet; kept as a separate, self-contained copy here instead ## of being shared across both class files. function q = cm_anova_qform (cols, b, V) if (isempty (cols)) q = 0; else bc = b(cols); Vc = V(cols, cols); q = bc' * (Vc \ bc); endif endfunction ## Duplicated from LinearModel, since we don't have superclass support ## for Octave yet; kept as a separate, self-contained copy here instead ## of being shared across both class files. function SumSq = cm_anova_ss (term_cols_k, excl_cols, b, V, MSE) full_cols = [excl_cols, term_cols_k]; SumSq = MSE * (cm_anova_qform (full_cols, b, V) - cm_anova_qform (excl_cols, b, V)); endfunction ## Duplicated from LinearModel, since we don't have superclass support ## for Octave yet; kept as a separate, self-contained copy here instead ## of being shared across both class files. function X_dev = cm_deviation_encode (X_enc, pred_names, cat_info) X_dev = X_enc; enc_pos = 0; for j = 1:numel (pred_names) ci = find (strcmp (cat_info.names, pred_names{j})); if (isempty (ci)) enc_pos = enc_pos + 1; else n_lev = numel (cat_info.levels{ci}); block = enc_pos + (1:(n_lev - 1)); is_ref = 1 - sum (X_dev(:, block), 2); X_dev(:, block) = X_dev(:, block) - is_ref; enc_pos = enc_pos + n_lev - 1; endif endfor endfunction ## Duplicated from LinearModel, since we don't have superclass support ## for Octave yet; kept as a separate, self-contained copy here instead ## of being shared across both class files. function [Xsyn, Dsyn] = cm_anova_synthetic_design (mdl) pred_names = mdl.PredictorNames; cat_info = mdl.CatLevelInfo; p_raw = mdl.NumPredictors; cat_logical = false (1, p_raw); cat_levels = cell (1, p_raw); for j = 1:p_raw ci = find (strcmp (cat_info.names, pred_names{j})); if (! isempty (ci)) cat_logical(j) = true; cat_levels{j} = cat_info.levels{ci}; else cat_levels{j} = {}; endif endfor cat_idx = find (cat_logical); n_cat = numel (cat_idx); if (n_cat == 0) cat_rows = 1; cat_combo = zeros (1, 0); else lev_counts = zeros (1, n_cat); for c = 1:n_cat lev_counts(c) = numel (cat_levels{cat_idx(c)}); endfor cat_rows = prod (lev_counts); cat_combo = zeros (cat_rows, n_cat); rep_inner = 1; for c = 1:n_cat pattern = repelem ((1:lev_counts(c))', rep_inner); cat_combo(:, c) = repmat (pattern, cat_rows / numel (pattern), 1); rep_inner = rep_inner * lev_counts(c); endfor endif num_idx = find (! cat_logical); n_num = numel (num_idx); R = max (2, max (mdl.TermsMatrix(:)) + 1); ## Cross the numeric predictors as a full factorial of R levels each, R ## being one more than the highest degree any term raises them to. Giving ## them all the same values scaled by a constant instead would leave their ## columns exact multiples of one another, and the design rank deficient ## however many rows it had. if (n_num == 0) num_grid = zeros (1, 0); else num_grid = zeros (R ^ n_num, n_num); rep_inner = 1; for k = 1:n_num pattern = repelem ((1:R)', rep_inner); num_grid(:, k) = repmat (pattern, rows (num_grid) / numel (pattern), 1); rep_inner = rep_inner * R; endfor endif num_rows = rows (num_grid); n_syn = cat_rows * num_rows; X_num_syn = zeros (n_syn, p_raw); for a = 1:num_rows rows_a = (a - 1) * cat_rows + (1:cat_rows); X_num_syn(rows_a, cat_idx) = cat_combo; if (n_num > 0) X_num_syn(rows_a, num_idx) = repmat (num_grid(a,:), cat_rows, 1); endif endfor X_enc_syn = encode_categorical (X_num_syn, cat_logical, pred_names, cat_levels); X_dev_syn = cm_deviation_encode (X_enc_syn, pred_names, cat_info); Xsyn = build_design (mdl.TermsMatrix, X_enc_syn); Dsyn = build_design (mdl.TermsMatrix, X_dev_syn); endfunction %!shared mdl, cmdl, X, y, n %! n = 20; %! X = [(1:n); (1:n).^2]' / n; %! y = X * [3; -1] + 0.2 * sin ((1:n)'); %! mdl = fitlm (X, y); %! cmdl = compact (mdl); %!test %! assert_equal (cmdl.NumObservations, 20); %! assert_equal (cmdl.NumCoefficients, 3); %! assert_equal (cmdl.NumVariables, 3); %! assert_equal (cmdl.NumPredictors, 2); %! assert_equal (cmdl.NumEstimatedCoefficients, 3); %! assert_equal (cmdl.DFE, 17); %! assert_equal (cmdl.SSE, 0.386545331386823, 1e-9); %! assert_equal (cmdl.SSR, 583.523874670959, 1e-6); %! assert_equal (cmdl.SST, 583.910420002346, 1e-6); %! assert_equal (cmdl.MSE, 0.0227379606698351, 1e-10); %! assert_equal (cmdl.RMSE, 0.150791116017606, 1e-10); %! assert_equal (cmdl.Rsquared.Ordinary, 0.999338005765704, 1e-10); %! assert_equal (cmdl.Rsquared.Adjusted, 0.999260124091081, 1e-10); %! assert_equal (cmdl.LogLikelihood, 11.0836133807695, 1e-6); %! assert_equal (cmdl.ModelCriterion.AIC, -16.1672267615389, 1e-6); %! assert_equal (cmdl.ModelCriterion.AICc, -14.6672267615389, 1e-6); %! assert_equal (cmdl.ModelCriterion.BIC, -13.180029940877, 1e-6); %! assert_equal (cmdl.ModelCriterion.CAIC, -10.180029940877, 1e-6); %!test %! assert_equal (cmdl.Coefficients.Estimate, [0.1161886778; 2.508451491; -0.9788353298], 1e-7); %! assert_equal (cmdl.Coefficients.SE, [0.112185831; 0.4920818186; 0.02276108523], 1e-8); %! assert_equal (cmdl.Coefficients.tStat, [1.035680502; 5.097630913; -43.00477415], 1e-6); %! assert_equal (all (cmdl.Coefficients.pValue >= 0 & cmdl.Coefficients.pValue <= 1), true); %! assert_equal (isequal (cmdl.CoefficientNames, {'(Intercept)', 'x1', 'x2'}), true); %! assert_equal (isequal (cmdl.CoefficientNames, cmdl.Coefficients.Properties.RowNames(:)'), true); %! assert_equal (size (cmdl.CoefficientCovariance), [3, 3]); %! assert_equal (diag (cmdl.CoefficientCovariance), [0.0125857; 0.242145; 0.000518067], 1e-6); %! assert_equal (width (cmdl.Coefficients), 4); %! assert_equal (isequal (cmdl.Coefficients.Properties.VariableNames, ... %! {'Estimate','SE','tStat','pValue'}), true); %!test %! assert_equal (cmdl.Formula.LinearPredictor, '1 + x1 + x2'); %! assert_equal (cmdl.Formula.HasIntercept, true); %! assert_equal (cmdl.PredictorNames, {'x1'; 'x2'}); %! assert_equal (cmdl.ResponseName, 'y'); %! assert_equal (cmdl.VariableNames, {'x1'; 'x2'; 'y'}); %! assert_equal (cmdl.VariableInfo.Range{1}, [0.05, 1], 1e-10); %! assert_equal (cmdl.VariableInfo.Range{2}, [0.05, 20], 1e-10); %! assert_equal (cmdl.VariableInfo.InModel, [true; true; false]); %! assert_equal (cmdl.Robust, []); %!test %! ci = coefCI (cmdl); %! assert_equal (size (ci), [3, 2]); %! assert_equal (class (ci), 'double'); %! assert_equal (all (ci(:,1) < ci(:,2)), true); %! assert_equal (ci(1,1), -0.120502736154050, 1e-10); %! assert_equal (ci(1,2), 0.352880091734465, 1e-10); %! assert_equal (ci(2,1), 1.470249604061007, 1e-10); %! assert_equal (ci(2,2), 3.546653377080718, 1e-10); %! assert_equal (ci(3,1), -1.026857022014626, 1e-10); %! assert_equal (ci(3,2), -0.930813637635746, 1e-10); %!test %! ci = coefCI (cmdl); %! t = tinv (0.975, cmdl.DFE); %! assert_equal ((ci(:,1) + ci(:,2)) / 2, cmdl.Coefficients.Estimate, 1e-10); %! assert_equal (ci(:,2) - ci(:,1), 2 * t * cmdl.Coefficients.SE, 1e-10); %! assert_equal (coefCI (cmdl, 0.05), ci); %!test %! ci = coefCI (cmdl, 0.01); %! assert_equal (size (ci), [3, 2]); %! assert_equal (ci(1,1), -0.208951721610638, 1e-10); %! assert_equal (ci(1,2), 0.441329077191052, 1e-10); %! assert_equal (ci(2,1), 1.082284945644892, 1e-10); %! assert_equal (ci(2,2), 3.934618035496833, 1e-10); %! assert_equal (ci(3,1), -1.044802201703589, 1e-10); %! assert_equal (ci(3,2), -0.912868457946783, 1e-10); %!test %! ## a zero alpha gives an infinite interval and a full alpha collapses it to the estimate %! ci = coefCI (cmdl, 0); %! assert_equal (all (ci(:,1) == -Inf), true); %! assert_equal (all (ci(:,2) == +Inf), true); %! ci = coefCI (cmdl, 1); %! assert_equal (ci(:,1), cmdl.Coefficients.Estimate, 1e-10); %! assert_equal (ci(:,2), cmdl.Coefficients.Estimate, 1e-10); %!test %! m = fitlm (X, y, 'Intercept', false); %! cm = compact (m); %! ci = coefCI (cm); %! assert_equal (size (ci), [2, 2]); %! assert_equal (ci(1,1), 2.486679110991696, 1e-10); %! assert_equal (ci(1,2), 3.436164115360526, 1e-10); %! assert_equal (ci(2,1), -1.027166590567854, 1e-10); %! assert_equal (ci(2,2), -0.967330908318718, 1e-10); %!test %! m = fitlm (X, y, 'Weights', (1:n)' / sum (1:n)); %! cm = compact (m); %! ci = coefCI (cm); %! assert_equal (size (ci), [3, 2]); %! assert_equal (ci(1,1), -0.355978167660141, 1e-10); %! assert_equal (ci(1,2), 0.516619434992026, 1e-10); %! assert_equal (ci(2,1), 1.142016390035618, 1e-10); %! assert_equal (ci(2,2), 4.154555017558383, 1e-10); %! assert_equal (ci(3,1), -1.044530853341675, 1e-10); %! assert_equal (ci(3,2), -0.924508441530335, 1e-10); %!test %! m = fitlm ([ones(n,1), X, X(:,1)+X(:,2)], y); %! cm = compact (m); %! ci = coefCI (cm); %! drop = find (cm.Coefficients.SE == 0); %! keep = setdiff (1:5, drop'); %! assert_equal (size (ci), [5, 2]); %! assert_equal (numel (drop), 2); %! assert_equal (all (all (ci(drop, :) == 0)), true); %! assert_equal (all (all (isfinite (ci(keep, :)))), true); %! assert_equal ((ci(keep,1) + ci(keep,2)) / 2, cm.Coefficients.Estimate(keep), 1e-10); %! assert_equal (cm.DFE, 17); %! assert_equal (m.SSE, 0.386545331386824, 1e-10); %! assert_equal (m.Rsquared.Ordinary, 0.999338005765704, 1e-10); %! assert_equal (m.Rsquared.Adjusted, 0.999260124091081, 1e-10); %!test %! m = fitlm ([1;1;1;2;2;2;3;3;3], [2.1;2.3;1.9;4.1;3.9;4.2;6.3;5.8;6.1], ... %! 'linear', 'CategoricalVars', 1); %! cm = compact (m); %! ci = coefCI (cm); %! assert_equal (size (ci), [3, 2]); %! assert_equal (ci(1,1), 1.809712563216694, 1e-10); %! assert_equal (ci(1,2), 2.390287436783304, 1e-10); %! assert_equal (ci(2,1), 1.556138236581195, 1e-10); %! assert_equal (ci(2,2), 2.377195096752140, 1e-10); %! assert_equal (ci(3,1), 3.556138236581195, 1e-10); %! assert_equal (ci(3,2), 4.377195096752140, 1e-10); %!test %! m = fitlm (X, y, 'RobustOpts', 'bisquare'); %! cm = compact (m); %! ci = coefCI (cm); %! assert_equal (cm.DFE, 17); %! assert_equal (ci(1,1), -0.136385388374896, 1e-10); %! assert_equal (ci(1,2), 0.378422288262478, 1e-10); %! assert_equal (ci(2,1), 1.359092508160098, 1e-10); %! assert_equal (ci(2,2), 3.617198504352038, 1e-10); %! assert_equal (ci(3,1), -1.030210688187146, 1e-10); %! assert_equal (ci(3,2), -0.925762726293341, 1e-10); %! ci = coefCI (cm, 0.1); %! assert_equal (ci(1,1), -0.091218796364050, 1e-10); %! assert_equal (ci(1,2), 0.333255696251631, 1e-10); %! assert_equal (ci(2,1), 1.557207176755238, 1e-10); %! assert_equal (ci(2,2), 3.419083835756898, 1e-10); %! assert_equal (ci(3,1), -1.021046958321974, 1e-10); %! assert_equal (ci(3,2), -0.934926456158514, 1e-10); %!test %! m = fitlm (X, y, 'constant'); %! cm = compact (m); %! ci = coefCI (cm); %! t = tinv (0.975, cm.DFE); %! assert_equal (size (ci), [1, 2]); %! assert_equal (ci(1,1), -8.184528886493887, 1e-10); %! assert_equal (ci(1,2), -2.995506675817716, 1e-10); %! assert_equal ((ci(1,1) + ci(1,2)) / 2, cm.Coefficients.Estimate, 1e-10); %! assert_equal (ci(1,2) - ci(1,1), 2 * t * cm.Coefficients.SE, 1e-10); %!test %! [p, F, r] = coefTest (cmdl); %! assert_equal (size (p), [1, 1]); %! assert_equal (class (p), 'double'); %! assert_equal (p >= 0 && p <= 1, true); %! assert_equal (F >= 0, true); %! assert_equal (p, 9.489880832170599e-28, -1e-8); %! assert_equal (F, 1.283149098426142e+04, -1e-8); %! assert_equal (r, 2); %!test %! k = cmdl.NumCoefficients; %! H = [zeros(k-1, 1), eye(k-1)]; %! [p, F, r] = coefTest (cmdl, H); %! assert_equal (p, 9.489880832170599e-28, -1e-8); %! assert_equal (F, 1.283149098426142e+04, -1e-8); %! assert_equal (r, 2); %!test %! [p, F, r] = coefTest (cmdl, [1 0 0]); %! assert_equal (size (r), [1, 1]); %! assert_equal (p, 0.314859866747774, -1e-8); %! assert_equal (F, 1.072634101844537, -1e-8); %! assert_equal (r, 1); %! [p, F, r] = coefTest (cmdl, [0 1 0]); %! assert_equal (p, 8.937794169018252e-05, -1e-8); %! assert_equal (F, 25.985840929474932, -1e-8); %! assert_equal (r, 1); %! [p, F, r] = coefTest (cmdl, [0 0 1]); %! assert_equal (p, 8.656938305821102e-19, -1e-8); %! assert_equal (F, 1.849410599855684e+03, -1e-8); %! assert_equal (r, 1); %! [p, F, r] = coefTest (cmdl, [0 1 0; 0 0 1]); %! assert_equal (p, 9.489880832170599e-28, -1e-8); %! assert_equal (F, 1.283149098426142e+04, -1e-8); %! assert_equal (r, 2); %!test %! b = cmdl.Coefficients.Estimate; %! [p, F] = coefTest (cmdl, [0 1 0], b(2)); %! assert_equal (p, 1, 1e-10); %! assert_equal (F, 0, 1e-10); %! [p, F] = coefTest (cmdl, [0 1 0], 0); %! assert_equal (p, 8.937794169018252e-05, -1e-8); %! assert_equal (F, 25.985840929474932, -1e-8); %!test %! [p, F, r] = coefTest (cmdl, [0 1 0; 0 0 1], [1.5; -1.0]); %! assert_equal (p, 2.833788304242915e-09, -1e-8); %! assert_equal (F, 77.603887650386312, -1e-8); %! assert_equal (r, 2); %! [p, F] = coefTest (cmdl, [0 1 0], 1.5); %! assert_equal (p, 0.056184159363707, -1e-8); %! assert_equal (F, 4.199865537706047, -1e-8); %!test %! m = fitlm (X, y, 'Intercept', false); %! cm = compact (m); %! [p, F, r] = coefTest (cm); %! assert_equal (r, cm.NumCoefficients); %! assert_equal (p, 6.060655830723051e-32, -1e-8); %! assert_equal (F, 2.646694317541346e+04, -1e-8); %!test %! m = fitlm (X, y, 'interactions'); %! cm = compact (m); %! [p, F, r] = coefTest (cm); %! assert_equal (r, cm.NumCoefficients - 1); %! assert_equal (r != cm.NumPredictors, true); %! assert_equal (p, 1.164196605688161e-25, -1e-8); %! assert_equal (F, 8.107508574885546e+03, -1e-8); %!test %! m = fitlm (X, y, 'Weights', (1:n)' / sum (1:n)); %! cm = compact (m); %! [p, F, r] = coefTest (cm); %! assert_equal (p, 1.481920976389473e-27, -1e-8); %! assert_equal (F, 1.217557180481257e+04, -1e-8); %! assert_equal (r, 2); %!test %! m = fitlm ([1;1;1;2;2;2;3;3;3], [2.1;2.3;1.9;4.1;3.9;4.2;6.3;5.8;6.1], ... %! 'linear', 'CategoricalVars', 1); %! cm = compact (m); %! [p, F, r] = coefTest (cm); %! assert_equal (p, 1.197590680415813e-06, -1e-8); %! assert_equal (F, 2.795000000000035e+02, -1e-8); %! assert_equal (r, 2); %! [p, F] = coefTest (cm, [1 0 0]); %! assert_equal (p, 2.087464608380450e-06, -1e-8); %! assert_equal (F, 3.133421052631613e+02, -1e-8); %! [p, F] = coefTest (cm, [0 1 0]); %! assert_equal (p, 2.325514143662469e-05, -1e-8); %! assert_equal (F, 1.374078947368438e+02, -1e-8); %! [p, F] = coefTest (cm, [0 0 1]); %! assert_equal (p, 3.757733067786492e-07, -1e-8); %! assert_equal (F, 5.589868421052698e+02, -1e-8); %!test %! m = fitlm (X, y, 'constant'); %! cm = compact (m); %! [p, F, r] = coefTest (cm); %! assert_equal (p, 2.399364086950727e-04, -1e-8); %! assert_equal (F, 20.335916494750592, -1e-8); %! assert_equal (r, 1); %!test %! m = fitlm ([ones(n,1), X, X(:,1)+X(:,2)], y); %! cm = compact (m); %! [p, F] = coefTest (cm); %! assert_equal (size (p), [1, 1]); %! assert_equal (class (p), 'double'); %! assert_equal (isnan (p), true); %! assert_equal (isnan (F), true); %! drop = find (cm.Coefficients.SE == 0); %! keep = setdiff (2:cm.NumCoefficients, drop'); %! H = zeros (numel (keep), cm.NumCoefficients); %! for i = 1:numel (keep) %! H(i, keep(i)) = 1; %! endfor %! [p, F, r] = coefTest (cm, H); %! assert_equal (r, numel (keep)); %! assert_equal (p, 6.706570586430847e-30, -1e-8); %! assert_equal (F, 1.771618642634559e+04, -1e-8); %!test %! m = fitlm (X, y, 'RobustOpts', 'bisquare'); %! cm = compact (m); %! [p, F, r] = coefTest (cm); %! assert_equal (p, 3.941715170923545e-27, -1e-8); %! assert_equal (F, 1.085097669445008e+04, -1e-8); %! assert_equal (r, 2); %! [p, F, r] = coefTest (cm, [0 1 -1]); %! assert_equal (p, 9.729154060050210e-06, -1e-8); %! assert_equal (F, 38.417457909307693, -1e-8); %! assert_equal (r, 1); %!test %! yp = predict (cmdl, [0.5 0.25; 1.0 1.0; 0.2 0.04]); %! assert_equal (class (yp), 'double'); %! assert_equal (size (yp), [3, 1]); %! assert_equal (yp(1), 1.125705590619342, 1e-10); %! assert_equal (yp(2), 1.645804838535884, 1e-10); %! assert_equal (yp(3), 0.578725562711373, 1e-10); %!test %! [yp, yci] = predict (cmdl, [0.5 0.25; 1.0 1.0; 0.2 0.04]); %! assert_equal (size (yci), [3, 2]); %! assert_equal (all (yci(:,1) < yci(:,2)), true); %! assert_equal (yci(1,1), 0.810180780547058, 1e-9); %! assert_equal (yci(1,2), 1.441230400691626, 1e-9); %! assert_equal (yci(2,1), 0.858229321851332, 1e-9); %! assert_equal (yci(2,2), 2.433380355220436, 1e-9); %! assert_equal (yci(3,1), 0.470499753577336, 1e-9); %! assert_equal (yci(3,2), 0.686951371845409, 1e-9); %!test %! [~, yci] = predict (cmdl, [0.5 0.25; 1.0 1.0; 0.2 0.04], 'Alpha', 0.01); %! assert_equal (yci(1,1), 0.692272619569794, 1e-9); %! assert_equal (yci(1,2), 1.559138561668890, 1e-9); %! assert_equal (yci(2,1), 0.563920989071667, 1e-9); %! assert_equal (yci(2,2), 2.727688688000101, 1e-9); %! assert_equal (yci(3,1), 0.430056955680247, 1e-9); %! assert_equal (yci(3,2), 0.727394169742498, 1e-9); %!test %! [~, yci] = predict (cmdl, [0.5 0.25; 1.0 1.0; 0.2 0.04], 'Simultaneous', true); %! assert_equal (yci(1,1), 0.662572505689110, 1e-9); %! assert_equal (yci(1,2), 1.588838675549574, 1e-9); %! assert_equal (yci(2,1), 0.489787095987915, 1e-9); %! assert_equal (yci(2,2), 2.801822581083853, 1e-9); %! assert_equal (yci(3,1), 0.419869741383617, 1e-9); %! assert_equal (yci(3,2), 0.737581384039129, 1e-9); %!test %! [~, yci] = predict (cmdl, [0.5 0.25; 1.0 1.0; 0.2 0.04], 'Prediction', 'observation'); %! assert_equal (yci(1,1), 0.677632064105876, 1e-9); %! assert_equal (yci(1,2), 1.573779117132808, 1e-9); %! assert_equal (yci(2,1), 0.796399650258815, 1e-9); %! assert_equal (yci(2,2), 2.495210026812952, 1e-9); %! assert_equal (yci(3,1), 0.242679724835377, 1e-9); %! assert_equal (yci(3,2), 0.914771400587368, 1e-9); %!test %! [~, yci] = predict (cmdl, [0.5 0.25; 1.0 1.0; 0.2 0.04], ... %! 'Alpha', 0.1, 'Simultaneous', true, 'Prediction', 'observation'); %! assert_equal (yci(1,1), 0.551414812037842, 1e-9); %! assert_equal (yci(1,2), 1.699996369200842, 1e-9); %! assert_equal (yci(2,1), 0.557131801540151, 1e-9); %! assert_equal (yci(2,2), 2.734477875531617, 1e-9); %! assert_equal (yci(3,1), 0.148019407463707, 1e-9); %! assert_equal (yci(3,2), 1.009431717959039, 1e-9); %!test %! [yp, yci] = predict (cmdl, [0.5 0.25; NaN 1.0; 1.0 1.0]); %! assert_equal (isnan (yp(2)), true); %! assert_equal (all (isnan (yci(2,:))), true); %! assert_equal (yp(1), 1.125705590619342, 1e-10); %! assert_equal (yp(3), 1.645804838535884, 1e-10); %! assert_equal (yci(1,1), 0.810180780547058, 1e-9); %! assert_equal (yci(3,2), 2.433380355220436, 1e-9); %!test %! Xt = table (0.5, 0.25, 'VariableNames', {'x1', 'x2'}); %! yp = predict (cmdl, Xt); %! assert_equal (yp, 1.125705590619342, 1e-10); %!test %! m = fitlm ([1;1;1;2;2;2;3;3;3], [2.1;2.3;1.9;4.1;3.9;4.2;6.3;5.8;6.1], ... %! 'linear', 'CategoricalVars', 1); %! cm = compact (m); %! yp = predict (cm, table ([1;2;3], 'VariableNames', {'x1'})); %! assert_equal (yp(1), 2.099999999999999, 1e-10); %! assert_equal (yp(2), 4.066666666666666, 1e-10); %! assert_equal (yp(3), 6.066666666666666, 1e-10); %!test %! m = fitlm (X, y, 'Weights', (1:n)' / sum (1:n)); %! cm = compact (m); %! [yp, yci] = predict (cm, [0.5 0.25; 1.0 1.0; 0.2 0.04], 'Alpha', 0.05); %! assert_equal (yp(1), 1.158333573705442, 1e-10); %! assert_equal (yp(2), 1.744086690026939, 1e-10); %! assert_equal (yp(3), 0.570596988527903, 1e-10); %! assert_equal (yci(1,1), 0.802165170771357, 1e-9); %! assert_equal (yci(2,2), 2.788483587522537, 1e-9); %!test %! m = fitlm (X, y, 'RobustOpts', 'bisquare'); %! cm = compact (m); %! [yp, yci] = predict (cm, [0.5 0.25; 1.0 1.0; 0.2 0.04], 'Simultaneous', true, 'Alpha', 0.1); %! assert_equal (yp(1), 1.120594526261764, 1e-10); %! assert_equal (yp(2), 1.631177248959615, 1e-10); %! assert_equal (yp(3), 0.579528082905394, 1e-10); %! assert_equal (yci(1,1), 0.680801249227337, 1e-9); %! assert_equal (yci(2,2), 2.728936938016809, 1e-9); %!test %! m = fitlm (X, y, 'quadratic'); %! cm = compact (m); %! [yp, yci] = predict (cm, [0.5 0.25; 1.0 1.0; 0.2 0.04]); %! assert_equal (yp(1), -0.948865258803113, 1e-9); %! assert_equal (yp(2), -3.349087980348939, 1e-9); %! assert_equal (yp(3), -0.053659932832480, 1e-9); %! assert_equal (yci(1,1), -3.431959757763334, 1e-9); %! assert_equal (yci(1,2), 1.534229240157108, 1e-9); %!test %! ysim = random (cmdl, [0.5 0.25; 1.0 1.0]); %! assert_equal (size (ysim), [2, 1]); %! assert_equal (class (ysim), 'double'); %! assert_equal (iscolumn (ysim), true); %! ypred = predict (cmdl, [0.5 0.25; 1.0 1.0]); %! assert_equal (ypred(1), 1.125705590619342, 1e-10); %! assert_equal (ypred(2), 1.645804838535884, 1e-10); %! assert_equal (all (isfinite (ysim - ypred)), true); %!test %! assert_equal (size (random (cmdl, [0.5 0.25])), [1, 1]); %!test %! ysim = random (cmdl, [0.5 0.25; NaN 1.0; 1.0 1.0]); %! assert_equal (size (ysim), [3, 1]); %! assert_equal (isfinite (ysim(1)), true); %! assert_equal (isnan (ysim(2)), true); %! assert_equal (isfinite (ysim(3)), true); %!test %! ya = random (cmdl, [0.5 0.25]); %! yb = random (cmdl, [0.5 0.25]); %! assert_equal (isequal (ya, yb), false); %!test %! Xt = table (0.5, 0.25, 'VariableNames', {'x1', 'x2'}); %! assert_equal (size (random (cmdl, Xt)), [1, 1]); %! assert_equal (all (isfinite (random (cmdl, Xt))), true); %! ysim = random (cmdl, X); %! assert_equal (size (ysim), [20, 1]); %! assert_equal (sum (isnan (ysim)), 0); %!test %! mw = compact (fitlm (X, y, 'Weights', (1:n)' / sum (1:n))); %! mni = compact (fitlm (X, y, 'Intercept', false)); %! assert_equal (all (isfinite (random (mw, [0.5 0.25; 1.0 1.0]))), true); %! assert_equal (all (isfinite (random (mni, [0.5 0.25; 1.0 1.0]))), true); %!test %! yf = feval (cmdl, [0.5 0.25; 1.0 1.0; 0.2 0.04]); %! assert_equal (yf(1), 1.125705590619342, 1e-10); %! assert_equal (yf(2), 1.645804838535884, 1e-10); %! assert_equal (yf(3), 0.578725562711373, 1e-10); %! assert_equal (feval (cmdl, [0.5; 1.0; 0.2], [0.25; 1.0; 0.04]), yf, 1e-10); %!test %! yf3 = feval (cmdl, [0.5, 1.0, 0.2], [0.25, 1.0, 0.04]); %! assert_equal (size (yf3), [1, 3]); %! assert_equal (yf3(1), 1.125705590619342, 1e-10); %! assert_equal (yf3(2), 1.645804838535884, 1e-10); %! assert_equal (yf3(3), 0.578725562711373, 1e-10); %!test %! assert_equal (feval (cmdl, 0.5, 0.25), 1.125705590619342, 1e-10); %! yf5 = feval (cmdl, 0.5, [0.1; 0.2; 0.3]); %! assert_equal (yf5(1), 1.272530890093120, 1e-10); %! assert_equal (yf5(2), 1.174647357110602, 1e-10); %! assert_equal (yf5(3), 1.076763824128083, 1e-10); %! yf6 = feval (cmdl, [0.1; 0.5; 0.9], 0.25); %! assert_equal (yf6(1), 0.122324994390997, 1e-10); %! assert_equal (yf6(2), 1.125705590619342, 1e-10); %! assert_equal (yf6(3), 2.129086186847688, 1e-10); %!test %! ms = compact (fitlm (X(:,1), y)); %! assert_equal (size (feval (ms, 0.5)), [1, 1]); %! assert_equal (size (feval (ms, [0.3; 0.5; 0.9])), [3, 1]); %! assert_equal (feval (ms, 0.5), predict (ms, 0.5), 1e-10); %! assert_equal (feval (ms, [0.3; 0.5; 0.9]), predict (ms, [0.3; 0.5; 0.9]), 1e-10); %! yf14 = feval (ms, [1; 2; 3]); %! assert_equal (yf14(1), -14.162385738140875, 1e-9); %! assert_equal (yf14(2), -32.209476173898921, 1e-9); %! assert_equal (yf14(3), -50.256566609656964, 1e-9); %!test %! Xt = table (0.5, 0.25, 'VariableNames', {'x1', 'x2'}); %! assert_equal (feval (cmdl, Xt), 1.125705590619342, 1e-10); %!test %! yf9 = feval (cmdl, [0.5 0.25; NaN 1.0; 1.0 1.0]); %! assert_equal (isnan (yf9(2)), true); %! assert_equal (yf9(1), 1.125705590619342, 1e-10); %! assert_equal (yf9(3), 1.645804838535884, 1e-10); %! yf10 = feval (cmdl, [0.5; NaN; 1.0], [0.25; 1.0; 1.0]); %! assert_equal (isnan (yf10(2)), true); %! yf11 = feval (cmdl, [0.5; 1.0; 1.0], [0.25; NaN; 1.0]); %! assert_equal (isnan (yf11(2)), true); %!test %! yf12 = feval (cmdl, X); %! assert_equal (size (yf12), [20, 1]); %! assert_equal (sum (isnan (yf12)), 0); %!test %! mw = compact (fitlm (X, y, 'Weights', (1:n)' / sum (1:n))); %! yfw = feval (mw, [0.5 0.25; 1.0 1.0]); %! assert_equal (yfw(1), 1.158333573705442, 1e-10); %! assert_equal (yfw(2), 1.744086690026939, 1e-10); %! assert_equal (feval (mw, [0.5; 1.0], [0.25; 1.0]), yfw, 1e-10); %!test %! Weight = [2000;2100;2200;2300;2400;2500;2600;2700;2800;2900;3000; ... %! 3100;3200;3300;3400;3500;3600;3700;3800;3900]; %! Year = categorical ([70;70;70;70;70;76;76;76;76;76;76;76;82;82; ... %! 82;82;82;82;82;82]); %! MPG = [30;29;28;27;26;25;24;23;22;21;20;19;18;17;16;15;14;13;12;11]; %! m = fitlm (table (MPG, Weight, Year), 'MPG ~ Weight + Year'); %! cm = compact (m); %! yf = feval (cm, [2500;3000], '76'); %! assert_equal (yf(1), 25.000000000000000, 1e-9); %! assert_equal (yf(2), 20.000000000000004, 1e-9); %! assert_equal (yf, feval (m, [2500;3000], '76'), 1e-10); %! yf2 = feval (cm, [2500;3000], categorical (70)); %! assert_equal (yf2(1), 24.999999999999996, 1e-9); %! assert_equal (yf2(2), 20.000000000000000, 1e-9); %! assert_equal (feval (cm, 2800, '82'), 21.999999999999996, 1e-9); %! assert_equal (isnan (feval (cm, 2500, '99')), true); %!test %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotEffects (ax, cmdl); %! xd1 = get (h(1), 'XData'); %! yd1 = get (h(1), 'YData'); %! xd2 = get (h(2), 'XData'); %! yd2 = get (h(2), 'YData'); %! xd3 = get (h(3), 'XData'); %! yd3 = get (h(3), 'YData'); %! ytl = get (ax, 'YTickLabel'); %! assert_equal (numel (h), 3); %! assert_equal (xd1(1), 2.38302891604232, -1e-10); %! assert_equal (xd1(2), -19.5277648300125, -1e-10); %! assert_equal (yd1, [1 2]); %! assert_equal (xd2(1), 1.39673712385796, -1e-10); %! assert_equal (xd2(2), 3.36932070822668, -1e-10); %! assert_equal (yd2, [1 1]); %! assert_equal (xd3(1), -20.4857975891918, -1e-10); %! assert_equal (xd3(2), -18.5697320708331, -1e-10); %! assert_equal (yd3, [2 2]); %! assert_equal (get (h(1), 'Color'), [0.1490 0.5490 0.8660], 1e-4); %! assert_equal (get (h(2), 'Color'), [0.1490 0.5490 0.8660], 1e-4); %! assert_equal (get (h(3), 'Color'), [0.1490 0.5490 0.8660], 1e-4); %! assert_equal (get (h(1), 'Marker'), 'o'); %! assert_equal (get (h(1), 'LineStyle'), 'none'); %! assert_equal (get (h(2), 'LineStyle'), '-'); %! assert_equal (get (h(2), 'Marker'), 'none'); %! assert_equal (get (h(3), 'LineStyle'), '-'); %! assert_equal (get (h(3), 'Marker'), 'none'); %! assert_equal (mean (xd2), xd1(1), 1e-10); %! assert_equal (mean (xd3), xd1(2), 1e-10); %! assert_equal (get (get (ax, 'xlabel'), 'string'), 'Main Effect'); %! assert_equal (get (get (ax, 'ylabel'), 'string'), ''); %! assert_equal (get (get (ax, 'title'), 'string'), 'Main Effects Plot'); %! assert_equal (get (ax, 'YTick'), [1 2]); %! assert_equal (ytl{1}, 'x1: 0.05 to 1'); %! assert_equal (ytl{2}, 'x2: 0.05 to 20'); %! close (fig); %!test %! ## 3-predictor model %! X3 = [X, sin((1:n)' * pi / n)]; %! y3 = X3 * [3; -1; 2] + 0.1 * cos ((1:n)' * pi / 7); %! cm3 = compact (fitlm (X3, y3)); %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotEffects (ax, cm3); %! xd1 = get (h(1), 'XData'); %! yd1 = get (h(1), 'YData'); %! xd2 = get (h(2), 'XData'); %! yd2 = get (h(2), 'YData'); %! xd3 = get (h(3), 'XData'); %! yd3 = get (h(3), 'YData'); %! xd4 = get (h(4), 'XData'); %! yd4 = get (h(4), 'YData'); %! ytl = get (ax, 'YTickLabel'); %! assert_equal (numel (h), 4); %! assert_equal (xd1(1), 8.10687671732127, -1e-10); %! assert_equal (xd1(2), -25.4487243632125, -1e-10); %! assert_equal (xd1(3), 0.661302203942261, -1e-10); %! assert_equal (yd1, [1 2 3]); %! assert_equal (xd2(1), 0.565266595687836, -1e-10); %! assert_equal (xd2(2), 15.6484868389547, -1e-10); %! assert_equal (yd2, [1 1]); %! assert_equal (xd3(1), -33.3368582824351, -1e-10); %! assert_equal (xd3(2), -17.5605904439899, -1e-10); %! assert_equal (yd3, [2 2]); %! assert_equal (xd4(1), -1.25582490831999, -1e-10); %! assert_equal (xd4(2), 2.57842931620451, -1e-10); %! assert_equal (yd4, [3 3]); %! assert_equal (get (ax, 'YTick'), [1 2 3]); %! assert_equal (ytl{1}, 'x1: 0.05 to 1'); %! assert_equal (ytl{2}, 'x2: 0.05 to 20'); %! assert_equal (ytl{3}, 'x3: 1.22465e-16 to 1'); %! assert_equal (mean (xd2), xd1(1), 1e-10); %! assert_equal (mean (xd3), xd1(2), 1e-10); %! assert_equal (mean (xd4), xd1(3), 1e-10); %! close (fig); %!test %! cme = compact (fitlm (X, y, 'Exclude', [2, 7])); %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotEffects (ax, cme); %! xd1 = get (h(1), 'XData'); %! yd1 = get (h(1), 'YData'); %! xd2 = get (h(2), 'XData'); %! yd2 = get (h(2), 'YData'); %! xd3 = get (h(3), 'XData'); %! yd3 = get (h(3), 'YData'); %! ytl = get (ax, 'YTickLabel'); %! assert_equal (numel (h), 3); %! assert_equal (xd1(1), 2.50035744908398, -1e-10); %! assert_equal (xd1(2), -19.5912988214488, -1e-10); %! assert_equal (yd1, [1 2]); %! assert_equal (xd2(1), 1.40421088339552, -1e-10); %! assert_equal (xd2(2), 3.59650401477245, -1e-10); %! assert_equal (yd2, [1 1]); %! assert_equal (xd3(1), -20.6333076647782, -1e-10); %! assert_equal (xd3(2), -18.5492899781194, -1e-10); %! assert_equal (yd3, [2 2]); %! assert_equal (ytl{1}, 'x1: 0.05 to 1'); %! assert_equal (ytl{2}, 'x2: 0.05 to 20'); %! assert_equal (mean (xd2), xd1(1), 1e-10); %! assert_equal (mean (xd3), xd1(2), 1e-10); %! close (fig); %!test %! cmw = compact (fitlm (X, y, 'Weights', (1:n)' / sum (1:n))); %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotEffects (ax, cmw); %! xd1 = get (h(1), 'XData'); %! yd1 = get (h(1), 'YData'); %! xd2 = get (h(2), 'XData'); %! yd2 = get (h(2), 'YData'); %! xd3 = get (h(3), 'XData'); %! yd3 = get (h(3), 'YData'); %! ytl = get (ax, 'YTickLabel'); %! assert_equal (numel (h), 3); %! assert_equal (xd1(1), 2.51587141860715, -1e-10); %! assert_equal (xd1(2), -19.6411669663483, -1e-10); %! assert_equal (yd1, [1 2]); %! assert_equal (xd2(1), 1.08491557053384, -1e-10); %! assert_equal (xd2(2), 3.94682726668046, -1e-10); %! assert_equal (yd2, [1 1]); %! assert_equal (xd3(1), -20.8383905241664, -1e-10); %! assert_equal (xd3(2), -18.4439434085302, -1e-10); %! assert_equal (yd3, [2 2]); %! assert_equal (ytl{1}, 'x1: 0.05 to 1'); %! assert_equal (ytl{2}, 'x2: 0.05 to 20'); %! assert_equal (mean (xd2), xd1(1), 1e-10); %! assert_equal (mean (xd3), xd1(2), 1e-10); %! close (fig); %!test %! cmni = compact (fitlm (X, y, 'Intercept', false)); %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotEffects (ax, cmni); %! xd1 = get (h(1), 'XData'); %! yd1 = get (h(1), 'YData'); %! xd2 = get (h(2), 'XData'); %! yd2 = get (h(2), 'YData'); %! xd3 = get (h(3), 'XData'); %! yd3 = get (h(3), 'YData'); %! ytl = get (ax, 'YTickLabel'); %! assert_equal (numel (h), 3); %! assert_equal (xd1(1), 2.81335053251731, -1e-10); %! assert_equal (xd1(2), -19.8951125513936, -1e-10); %! assert_equal (yd1, [1 2]); %! assert_equal (xd2(1), 2.36234515544211, -1e-10); %! assert_equal (xd2(2), 3.26435590959250, -1e-10); %! assert_equal (yd2, [1 1]); %! assert_equal (xd3(1), -20.4919734818287, -1e-10); %! assert_equal (xd3(2), -19.2982516209584, -1e-10); %! assert_equal (yd3, [2 2]); %! assert_equal (ytl{1}, 'x1: 0.05 to 1'); %! assert_equal (ytl{2}, 'x2: 0.05 to 20'); %! assert_equal (mean (xd2), xd1(1), 1e-10); %! assert_equal (mean (xd3), xd1(2), 1e-10); %! close (fig); %!test %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotEffects (ax, cmdl); %! assert_equal (isequal (get (h(1), 'Parent'), ax), true); %! assert_equal (get (h(1), 'XData'), [2.38302891604232, -19.5277648300125], -1e-10); %! close (fig); %!test %! fig = figure ('visible', 'off'); %! h = plotEffects (cmdl); %! assert_equal (isequal (get (h(1), 'Parent'), gca ()), true); %! assert_equal (get (h(1), 'XData'), [2.38302891604232, -19.5277648300125], -1e-10); %! close (fig); %!test %! fig = figure ('visible', 'off'); %! h1 = plotEffects (mdl); %! h2 = plotEffects (cmdl); %! assert_equal (get (h1(1), 'XData'), get (h2(1), 'XData'), 1e-10); %! assert_equal (get (h1(2), 'XData'), get (h2(2), 'XData'), 1e-10); %! assert_equal (get (h1(3), 'XData'), get (h2(3), 'XData'), 1e-10); %! close (fig); %!test %! ## continuous by continuous, effects mode %! cmi = compact (fitlm (X, y, 'y ~ x1*x2')); %! fig = figure ('visible', 'off'); %! h = plotInteraction (cmi, 'x1', 'x2'); %! assert_equal (numel (h), 11); %! assert_equal (get (h(1), 'XData'), [1.76843380852813, -18.8740348676059], 1e-9); %! assert_equal (get (h(1), 'YData'), [1, 7]); %! assert_equal (get (h(2), 'XData'), [-2.25617028020123, 5.79303789725749], 1e-9); %! assert_equal (get (h(3), 'XData'), [-23.1321080641968, -14.615961671015], 1e-9); %! assert_equal (get (h(4), 'XData'), ... %! [1.97967308209488, 1.68393809910143, 1.38820311610799], 1e-9); %! assert_equal (get (h(4), 'YData'), [2, 3, 4]); %! assert_equal (get (h(5), 'XData'), [-0.771057026434185, 4.73040319062394], 1e-9); %! assert_equal (get (h(6), 'XData'), [-2.86059582662229, 6.22847202482516], 1e-9); %! assert_equal (get (h(7), 'XData'), [-4.99614754441031, 7.77255377662629], 1e-9); %! assert_equal (get (h(8), 'XData'), ... %! [-18.5782998846125, -18.8740348676059, -19.1697698505994], 1e-9); %! assert_equal (get (h(8), 'YData'), [8, 9, 10]); %! assert_equal (get (h(9), 'XData'), [-24.674318784563, -12.4822809846619], 1e-9); %! assert_equal (get (h(10), 'XData'), [-23.1321080641968, -14.615961671015], 1e-9); %! assert_equal (get (h(11), 'XData'), [-21.6439971135684, -16.6955425876303], 1e-9); %! assert_equal (get (h(1), 'Tag'), 'main'); %! assert_equal (get (h(4), 'Tag'), 'conditional1'); %! assert_equal (get (h(8), 'Tag'), 'conditional2'); %! ax = gca (); %! assert_equal (get (get (ax, 'Title'), 'String'), 'Interaction of x1 and x2'); %! assert_equal (get (get (ax, 'XLabel'), 'String'), 'Effect'); %! assert_equal (get (ax, 'YTick'), [1, 2, 3, 4, 7, 8, 9, 10]); %! assert_equal (get (ax, 'YTickLabel'), ... %! {'x1: 0.05 to 1'; 'x2=0.05'; 'x2=10.025'; 'x2=20'; ... %! 'x2: 0.05 to 20'; 'x1=0.05'; 'x1=0.525'; 'x1=1'}); %! assert_equal (get (ax, 'YLim'), [0.5, 10.5]); %! close (fig); %!test %! ## continuous by continuous, predictions mode %! cmi = compact (fitlm (X, y, 'y ~ x1*x2')); %! fig = figure ('visible', 'off'); %! h = plotInteraction (cmi, 'x1', 'x2', 'predictions'); %! assert_equal (numel (h), 3); %! xd = get (h(1), 'XData'); %! assert_equal (numel (xd), 101); %! assert_equal (xd(1:3), [0.05, 0.2495, 0.449], 1e-9); %! assert_equal (xd(end-2:end), [19.601, 19.8005, 20], 1e-9); %! yd1 = get (h(1), 'YData'); %! assert_equal (yd1(1:3), ... %! [0.215349913094656, 0.0295669142485309, -0.156216084597594], 1e-9); %! assert_equal (yd1(end-2:end), ... %! [-17.9913839738256, -18.1771669726717, -18.3629499715178], 1e-9); %! yd2 = get (h(2), 'YData'); %! assert_equal (yd2(1:3), [1.20518645414209, 1.01644610546604, 0.827705756789976], 1e-9); %! assert_equal (yd2(end-2:end), ... %! [-17.2913677161117, -17.4801080647878, -17.6688484134638], 1e-9); %! yd3 = get (h(3), 'YData'); %! assert_equal (yd3(1:3), [2.19502299518953, 2.00332529668354, 1.81162759817755], 1e-9); %! assert_equal (yd3(end-2:end), ... %! [-16.5913514583978, -16.7830491569038, -16.9747468554098], 1e-9); %! assert_equal (get (h(1), 'DisplayName'), '0.05'); %! assert_equal (get (h(2), 'DisplayName'), '0.525'); %! assert_equal (get (h(3), 'DisplayName'), '1'); %! ax = gca (); %! assert_equal (get (get (ax, 'Title'), 'String'), 'Interaction of x1 and x2'); %! assert_equal (get (get (ax, 'XLabel'), 'String'), 'x2'); %! assert_equal (get (get (ax, 'YLabel'), 'String'), 'Adjusted y'); %! close (fig); %!test %! ## swapping var1/var2 order swaps roles and title %! cmi = compact (fitlm (X, y, 'y ~ x1*x2')); %! fig = figure ('visible', 'off'); %! h = plotInteraction (cmi, 'x2', 'x1'); %! assert_equal (numel (h), 11); %! assert_equal (get (h(1), 'XData'), [-18.8740348676059, 1.76843380852813], 1e-9); %! assert_equal (get (h(4), 'XData'), ... %! [-18.5782998846125, -18.8740348676059, -19.1697698505994], 1e-9); %! assert_equal (get (h(8), 'XData'), ... %! [1.97967308209488, 1.68393809910143, 1.38820311610799], 1e-9); %! ax = gca (); %! assert_equal (get (get (ax, 'Title'), 'String'), 'Interaction of x2 and x1'); %! assert_equal (get (ax, 'YTickLabel'), ... %! {'x2: 0.05 to 20'; 'x1=0.05'; 'x1=0.525'; 'x1=1'; ... %! 'x1: 0.05 to 1'; 'x2=0.05'; 'x2=10.025'; 'x2=20'}); %! close (fig); %!test %! ## interaction effects: variables given as indices into VariableNames %! cmi = compact (fitlm (X, y, 'y ~ x1*x2')); %! fig = figure ('visible', 'off'); %! h = plotInteraction (cmi, 1, 2); %! assert_equal (numel (h), 11); %! assert_equal (get (h(1), 'XData'), [1.76843380852813, -18.8740348676059], 1e-9); %! assert_equal (get (h(1), 'YData'), [1, 7]); %! ax = gca (); %! assert_equal (get (get (ax, 'Title'), 'String'), 'Interaction of x1 and x2'); %! close (fig); %!test %! ## interaction effects: explicit axes argument is honored %! cmi = compact (fitlm (X, y, 'y ~ x1*x2')); %! fig = figure ('visible', 'off'); %! axtarget = axes (fig); %! h = plotInteraction (axtarget, cmi, 'x1', 'x2'); %! assert_equal (numel (h), 11); %! assert_equal (isequal (get (h(1), 'Parent'), axtarget), true); %! assert_equal (isequal (gca (), axtarget), true); %! close (fig); %!test %! ## no interaction term: conditional effects collapse to the main effect %! cmn = compact (fitlm (X, y, 'y ~ x1 + x2')); %! fig = figure ('visible', 'off'); %! h = plotInteraction (cmn, 'x1', 'x2'); %! xd1 = get (h(1), 'XData'); %! eff1 = xd1(1); %! eff2 = xd1(2); %! assert_equal (eff1, 2.38302891604232, 1e-9); %! assert_equal (eff2, -19.5277648300125, 1e-9); %! assert_equal (get (h(4), 'XData'), [eff1, eff1, eff1], 1e-9); %! assert_equal (get (h(8), 'XData'), [eff2, eff2, eff2], 1e-9); %! close (fig); %!test %! ## categorical by continuous, effects mode %! xc = (1:30)' / 30; %! grp = categorical (repmat ({'A';'B';'C'}, 10, 1)); %! yv = 2*xc + 3*double (grp == 'B') - 1*double (grp == 'C') + ... %! 1.5*xc.*double (grp == 'B') + 0.3*sin ((1:30)'); %! tblc = table (yv, xc, grp, 'VariableNames', {'Response','Xc','Group'}); %! cmdlc = compact (fitlm (tblc, 'Response ~ Xc*Group')); %! fig = figure ('visible', 'off'); %! h = plotInteraction (cmdlc, 'Group', 'Xc'); %! assert_equal (numel (h), 11); %! assert_equal (get (h(1), 'XData'), [4.7896970899464, 2.32528157787528], 1e-9); %! assert_equal (get (h(2), 'XData'), [4.56862113373247, 5.01077304616034], 1e-9); %! assert_equal (get (h(3), 'XData'), [2.02254960835685, 2.62801354739371], 1e-9); %! assert_equal (get (h(4), 'XData'), ... %! [4.08328517685389, 4.7896970899464, 5.49610900303892], 1e-9); %! assert_equal (get (h(5), 'XData'), [3.64076372661607, 4.52580662709171], 1e-9); %! assert_equal (get (h(6), 'XData'), [4.56862113373247, 5.01077304616034], 1e-9); %! assert_equal (get (h(7), 'XData'), [5.07555715247022, 5.91666085360761], 1e-9); %! assert_equal (get (h(8), 'XData'), ... %! [1.88553612240401, 3.25156621870343, 1.8387423925184], 1e-9); %! assert_equal (get (h(9), 'XData'), [1.36118897012269, 2.40988327468532], 1e-9); %! assert_equal (get (h(10), 'XData'), [2.72721906642211, 3.77591337098474], 1e-9); %! assert_equal (get (h(11), 'XData'), [1.31439524023708, 2.36308954479972], 1e-9); %! ax = gca (); %! assert_equal (get (get (ax, 'Title'), 'String'), 'Interaction of Group and Xc'); %! assert_equal (get (ax, 'YTick'), [1, 2, 3, 4, 7, 8, 9, 10]); %! assert_equal (get (ax, 'YTickLabel'), ... %! {'Group: C to B'; 'Xc=0.0333333'; 'Xc=0.516667'; 'Xc=1'; ... %! 'Xc: 0.033333 to 1'; 'Group=A'; 'Group=B'; 'Group=C'}); %! close (fig); %!test %! ## categorical by continuous, predictions mode %! xc = (1:30)' / 30; %! grp = categorical (repmat ({'A';'B';'C'}, 10, 1)); %! yv = 2*xc + 3*double (grp == 'B') - 1*double (grp == 'C') + ... %! 1.5*xc.*double (grp == 'B') + 0.3*sin ((1:30)'); %! tblc = table (yv, xc, grp, 'VariableNames', {'Response','Xc','Group'}); %! cmdlc = compact (fitlm (tblc, 'Response ~ Xc*Group')); %! fig = figure ('visible', 'off'); %! h = plotInteraction (cmdlc, 'Group', 'Xc', 'predictions'); %! assert_equal (numel (h), 3); %! xd = get (h(1), 'XData'); %! assert_equal (numel (xd), 101); %! assert_equal (xd(1:3), [0.0333333333333333, 0.043, 0.0526666666666667], 1e-9); %! assert_equal (xd(end-2:end), [0.980666666666667, 0.990333333333333, 1], 1e-9); %! yd1 = get (h(1), 'YData'); %! assert_equal (yd1(1:3), [0.107201421526318, 0.126056782750359, 0.144912143974399], 1e-9); %! assert_equal (yd1(end-2:end), [1.95502682148225, 1.97388218270629, 1.99273754393033], 1e-9); %! yd2 = get (h(2), 'YData'); %! assert_equal (yd2(1:3), [3.18658823804771, 3.21910390023474, 3.25161956242178], 1e-9); %! assert_equal (yd2(end-2:end), [6.37312313237707, 6.4056387945641, 6.43815445675114], 1e-9); %! yd3 = get (h(3), 'YData'); %! assert_equal (yd3(1:3), [-0.896696938806178, -0.878309514880994, -0.85992209095581], 1e-9); %! assert_equal (yd3(end-2:end), [0.905270605861853, 0.923658029787037, 0.942045453712221], 1e-9); %! assert_equal (get (h(1), 'DisplayName'), 'A'); %! close (fig); %!test %! mi = fitlm (X, y, 'y ~ x1*x2'); %! cmi = compact (mi); %! fig1 = figure ('visible', 'off'); %! ax1 = axes (fig1); %! h1 = plotInteraction (ax1, mi, 'x1', 'x2'); %! fig2 = figure ('visible', 'off'); %! ax2 = axes (fig2); %! h2 = plotInteraction (ax2, cmi, 'x1', 'x2'); %! for k = 1:numel (h1) %! assert_equal (get (h1(k), 'XData'), get (h2(k), 'XData'), 1e-10); %! endfor %! close (fig1); %! close (fig2); %!test %! t = anova (cmdl); %! assert_equal (t.Properties.RowNames, {'x1'; 'x2'; 'Error'}); %! assert_equal (t.SumSq(1), 0.590865029026421, -1e-9); %! assert_equal (t.DF(1), 1); %! assert_equal (t.F(1), 25.9858409294749, -1e-8); %! assert_equal (t.pValue(1), 8.93779416901828e-05, -1e-8); %! assert_equal (t.SumSq(2), 42.051825481854, -1e-8); %! assert_equal (t.F(2), 1849.41059985568, -1e-7); %! assert_equal (t.pValue(2), 8.6569383058211e-19, -1e-8); %! assert_equal (t.SumSq(3), 0.386545331386823, -1e-9); %! assert_equal (t.DF(3), 17); %! assert_equal (isnan (t.F(3)), true); %! assert_equal (isnan (t.pValue(3)), true); %!test %! ## type 3 on a continuous-only model: no categorical, so deviation coding %! ## is a no-op and no synthetic design is needed (values from R2024a) %! xa = [1;2;3;4;5;6;7;8;9;10;1;2;3;4;5;6;7;8;9;10]; %! xb = [2;1;4;3;6;5;8;7;10;9;2;1;4;3;6;5;8;7;10;9]; %! yv = 3 + 2*xa - 0.5*xb + 0.4*sin ((1:20)'); %! c = compact (fitlm (table (xa, xb, yv), 'yv ~ xa + xb')); %! t = anova (c, 'components', 3); %! assert_equal (t.Properties.RowNames, {'xa'; 'xb'; 'Error'}); %! assert_equal (t.SumSq', ... %! [78.1451599777569, 4.93546634617531, 1.6375633770915], -1e-9); %!test %! ## a model missing a lower-order relative is computed, not refused %! xa = [1;2;3;4;5;6;7;8;9;10;1;2;3;4;5;6;7;8;9;10]; %! xb = [2;1;4;3;6;5;8;7;10;9;2;1;4;3;6;5;8;7;10;9]; %! yv = 3 + 2*xa - 0.5*xb + 0.4*sin ((1:20)'); %! c = compact (fitlm (table (xa, xb, yv), 'yv ~ xa + xa:xb')); %! t = anova (c, 'components', 3); %! assert_equal (t.SumSq', ... %! [42.7962732581896, 1.58706426534189, 4.98596545792161], -1e-9); %!test %! ## a categorical beside two numeric predictors: the synthetic design must %! ## cross them, or its numeric columns come out collinear %! xa = [1;2;3;4;5;6;7;8;9;10;1;2;3;4;5;6;7;8;9;10]; %! xb = [2;1;4;3;6;5;8;7;10;9;2;1;4;3;6;5;8;7;10;9]; %! gc = categorical ([1;1;1;1;1;2;2;2;2;2;3;3;3;3;3;1;2;3;1;2]); %! yv = 3 + 2*xa - 0.5*xb + 0.4*sin ((1:20)'); %! c = compact (fitlm (table (xa, xb, gc, yv), 'yv ~ xa + xb + gc')); %! t = anova (c, 'components', 3); %! assert_equal (t.Properties.RowNames, {'xa'; 'xb'; 'gc'; 'Error'}); %! assert_equal (t.SumSq', [70.9172463138446, 4.93563224639584, ... %! 0.0327536552228964, 1.60480972186862], -1e-9); %!test %! ## a categorical interacting with a numeric predictor, beside another %! xa = [1;2;3;4;5;6;7;8;9;10;1;2;3;4;5;6;7;8;9;10]; %! xb = [2;1;4;3;6;5;8;7;10;9;2;1;4;3;6;5;8;7;10;9]; %! gc = categorical ([1;1;1;1;1;2;2;2;2;2;3;3;3;3;3;1;2;3;1;2]); %! yv = 3 + 2*xa - 0.5*xb + 0.4*sin ((1:20)'); %! c = compact (fitlm (table (xa, xb, gc, yv), 'yv ~ xa*gc + xb')); %! t = anova (c, 'components', 3); %! assert_equal (t.SumSq', [69.1671592639563, 4.32040128576299, ... %! 0.106963060253346, 0.141990871527191, 1.46281885034151], -1e-9); %!test %! ## the compact table matches the full model's, term for term %! xa = [1;2;3;4;5;6;7;8;9;10;1;2;3;4;5;6;7;8;9;10]; %! xb = [2;1;4;3;6;5;8;7;10;9;2;1;4;3;6;5;8;7;10;9]; %! gc = categorical ([1;1;1;1;1;2;2;2;2;2;3;3;3;3;3;1;2;3;1;2]); %! yv = 3 + 2*xa - 0.5*xb + 0.4*sin ((1:20)'); %! m = fitlm (table (xa, xb, gc, yv), 'yv ~ xa + xb + gc'); %! assert_equal (anova (compact (m), 'components', 3).SumSq, ... %! anova (m, 'components', 3).SumSq, -1e-9); %!test %! t = anova (cmdl, 'components', 2); %! assert_equal (t.SumSq(1), 0.590865029026421, -1e-9); %! assert_equal (t.SumSq(2), 42.051825481854, -1e-8); %!test %! t = anova (cmdl, 'components', 1); %! assert_equal (t.SumSq(1), 541.472049188541, -1e-6); %! assert_equal (t.F(1), 23813.5713686671, -1e-6); %! assert_equal (t.pValue(1), 3.41699381541991e-28, -1e-8); %! assert_equal (t.SumSq(2), 42.051825481854, -1e-8); %!test %! t = anova (cmdl, 'summary'); %! assert_equal (t.Properties.RowNames, {'Total'; 'Model'; 'Residual'}); %! assert_equal (t.SumSq(1), 583.910420002346, -1e-8); %! assert_equal (t.DF(1), 19); %! assert_equal (isnan (t.F(1)), true); %! assert_equal (t.SumSq(2), 583.523874670959, -1e-8); %! assert_equal (t.DF(2), 2); %! assert_equal (t.F(2), 12831.4909842738, -1e-6); %! assert_equal (t.pValue(2), 9.48988083209278e-28, -1e-8); %! assert_equal (t.SumSq(3), 0.386545331386823, -1e-9); %! assert_equal (t.DF(3), 17); %!test %! t1 = anova (cmdl, 'oldcomponents'); %! t2 = anova (cmdl, 'newcomponents'); %! assert_equal (t1.SumSq(1), 0.590865029026421, -1e-9); %! assert_equal (t2.SumSq(2), 42.051825481854, -1e-8); %!test %! t = anova (cmdl, 'summary', 2); %! assert_equal (t.Properties.RowNames, {'Total'; 'Model'; 'Residual'}); %! assert_equal (t.SumSq(2), 583.523874670959, -1e-8); %!test %! a = categorical ([1;1;2;1;2;1;1;2;1;1;2;1;2;2;1;1;2;1;1;2]); %! b = categorical ([1;2;1;1;2;2;1;1;2;1;1;2;2;1;2;1;1;2;2;1]); %! z = (1:20)' / 10; %! w = 2*(a=='2') + 1.5*(b=='2') + 0.7*z + 0.9*(a=='2').*(b=='2') + ... %! [0.1;-0.2;0.05;0.15;-0.1;0.2;-0.05;0.1;0.0;-0.15; ... %! 0.1;0.05;-0.2;0.15;0.0;-0.1;0.05;0.2;-0.05;0.1]; %! t2 = table (a, b, z, w); %! c = compact (fitlm (t2, 'w ~ a*b')); %! t = anova (c); %! assert_equal (t.Properties.RowNames, {'a'; 'b'; 'a:b'; 'Error'}); %! assert_equal (t.SumSq(1), 26.0224323327611, -1e-8); %! assert_equal (t.F(1), 138.504723473417, -1e-7); %! assert_equal (t.pValue(1), 2.72592668860933e-09, -1e-8); %! assert_equal (t.SumSq(2), 15.2365308176098, -1e-8); %! assert_equal (t.SumSq(3), 0.0142868014375561, -1e-6); %! assert_equal (t.SumSq(4), 3.00609904761905, -1e-8); %! assert_equal (t.DF(4), 16); %!test %! a = categorical ([1;1;2;1;2;1;1;2;1;1;2;1;2;2;1;1;2;1;1;2]); %! b = categorical ([1;2;1;1;2;2;1;1;2;1;1;2;2;1;2;1;1;2;2;1]); %! z = (1:20)' / 10; %! w = 2*(a=='2') + 1.5*(b=='2') + 0.7*z + 0.9*(a=='2').*(b=='2') + ... %! [0.1;-0.2;0.05;0.15;-0.1;0.2;-0.05;0.1;0.0;-0.15; ... %! 0.1;0.05;-0.2;0.15;0.0;-0.1;0.05;0.2;-0.05;0.1]; %! t2 = table (a, b, z, w); %! c = compact (fitlm (t2, 'w ~ a*b')); %! t = anova (c, 'components', 1); %! assert_equal (t.SumSq(1), 16.354083333333, -1e-7); %! assert_equal (t.F(1), 87.0448142886636, -1e-7); %! assert_equal (t.pValue(1), 7.14746180184192e-08, -1e-8); %!test %! a = categorical ([1;1;2;1;2;1;1;2;1;1;2;1;2;2;1;1;2;1;1;2]); %! b = categorical ([1;2;1;1;2;2;1;1;2;1;1;2;2;1;2;1;1;2;2;1]); %! z = (1:20)' / 10; %! w = 2*(a=='2') + 1.5*(b=='2') + 0.7*z + 0.9*(a=='2').*(b=='2') + ... %! [0.1;-0.2;0.05;0.15;-0.1;0.2;-0.05;0.1;0.0;-0.15; ... %! 0.1;0.05;-0.2;0.15;0.0;-0.1;0.05;0.2;-0.05;0.1]; %! t2 = table (a, b, z, w); %! c = compact (fitlm (t2, 'w ~ a + a:b')); %! t = anova (c); %! assert_equal (t.Properties.RowNames, {'a'; 'a:b'; 'Error'}); %! assert_equal (t.SumSq(1), 16.3540833333333, -1e-8); %! assert_equal (t.F(1), 22.011053580241, -1e-7); %! assert_equal (t.SumSq(2), 5.62601666666666, -1e-8); %! assert_equal (t.F(2), 7.57208776360618, -1e-7); %! assert_equal (t.SumSq(3), 12.6309, -1e-6); %! assert_equal (t.DF(3), 17); %!test %! a = categorical ([1;1;2;1;2;1;1;2;1;1;2;1;2;2;1;1;2;1;1;2]); %! b = categorical ([1;2;1;1;2;2;1;1;2;1;1;2;2;1;2;1;1;2;2;1]); %! z = (1:20)' / 10; %! w = 2*(a=='2') + 1.5*(b=='2') + 0.7*z + 0.9*(a=='2').*(b=='2') + ... %! [0.1;-0.2;0.05;0.15;-0.1;0.2;-0.05;0.1;0.0;-0.15; ... %! 0.1;0.05;-0.2;0.15;0.0;-0.1;0.05;0.2;-0.05;0.1]; %! t2 = table (a, b, z, w); %! c = compact (fitlm (t2, 'w ~ a + b - 1')); %! t = anova (c); %! assert_equal (t.SumSq(1), 63.3745141509434, -1e-8); %! assert_equal (t.F(1), 178.349190204051, -1e-7); %! assert_equal (t.SumSq(2), 15.2365308176101, -1e-8); %! assert_equal (t.SumSq(3), 3.02038584905660, -1e-8); %! assert_equal (t.DF(3), 17); %!test %! a = categorical ([1;1;2;1;2;1;1;2;1;1;2;1;2;2;1;1;2;1;1;2]); %! b = categorical ([1;2;1;1;2;2;1;1;2;1;1;2;2;1;2;1;1;2;2;1]); %! z = (1:20)' / 10; %! w = 2*(a=='2') + 1.5*(b=='2') + 0.7*z + 0.9*(a=='2').*(b=='2') + ... %! [0.1;-0.2;0.05;0.15;-0.1;0.2;-0.05;0.1;0.0;-0.15; ... %! 0.1;0.05;-0.2;0.15;0.0;-0.1;0.05;0.2;-0.05;0.1]; %! wt = [1.2;0.8;1.5;1.0;0.9;1.1;1.3;0.7;1.0;1.4; ... %! 0.8;1.2;1.0;1.1;0.9;1.3;0.7;1.5;1.0;1.2]; %! t2 = table (a, b, z, w); %! c = compact (fitlm (t2, 'w ~ a + b', 'Weights', wt)); %! t = anova (c); %! assert_equal (t.SumSq(1), 26.6364861423312, -1e-8); %! assert_equal (t.F(1), 134.770391630163, -1e-7); %! assert_equal (t.SumSq(2), 17.4880599694592, -1e-8); %! assert_equal (t.SumSq(3), 3.35993877395756, -1e-8); %! assert_equal (t.DF(3), 17); %!test %! g = categorical ([1;1;1;1;1;2;2;2;2;2;2;2;2;2;3;3;3;3;3;3]); %! w = [2.1;1.9;2.3;2.0;1.8;4.1;4.3;3.9;4.0;4.2; ... %! 4.4;3.8;4.1;4.0;6.1;5.9;6.3;6.0;5.8;6.2]; %! t2 = table (g, w); %! c = compact (fitlm (t2, 'w ~ g')); %! t = anova (c); %! assert_equal (t.Properties.RowNames, {'g'; 'Error'}); %! assert_equal (t.DF(1), 2); %! assert_equal (t.SumSq(1), 44.3761111111084, -1e-6); %! assert_equal (t.F(1), 616.446794988158, -1e-6); %!test %! g3 = categorical ([1;1;1;1;2;2;2;3;3;3;3;1;2;3]); %! g2 = categorical ([1;1;2;2;1;2;2;1;1;2;2;2;1;1]); %! w = [10;12;15;14;9;11;13;16;18;20;22;17;10;19]; %! t2 = table (g3, g2, w); %! c = compact (fitlm (t2, 'w ~ g3*g2')); %! t = anova (c, 'components', 3); %! assert_equal (t.Properties.RowNames, {'g3'; 'g2'; 'g3:g2'; 'Error'}); %! assert_equal (t.SumSq(1), 176.678431372552, -1e-6); %! assert_equal (t.F(1), 44.6345510835922, -1e-6); %! assert_equal (t.SumSq(2), 38.7604166666673, -1e-6); %! assert_equal (t.SumSq(3), 1.85490196078435, -1e-6); %! assert_equal (t.SumSq(4), 15.8333333333333, -1e-8); %! assert_equal (t.DF(4), 8); %!test %! g = categorical ([1;1;1;1;2;2;2;2;3;3;3;3]); %! z = (1:12)' / 2; %! w = 5 + 2*(g=='2') + 4*(g=='3') + 0.3*z; %! w(10) = w(10) + 20; %! t2 = table (g, z, w); %! c = compact (fitlm (t2, 'w ~ g + z', 'RobustOpts', 'on')); %! t = anova (c, 'components', 3); %! assert_equal (t.SumSq(1), 3.35664335664336, -1e-8); %! assert_equal (t.F(1), 0.0801017164653529, -1e-8); %! assert_equal (t.SumSq(2), 0.337499999999999, -1e-8); %! assert_equal (t.SumSq(3), 167.619047619048, -1e-6); %! assert_equal (t.DF(3), 8); %!test %! z = [25;31;42;29;55;38;46;33;27;50;41;36;48;30;44]; %! g = categorical ({'M';'F';'F';'M';'M';'F';'M';'F';'F';'M';'F';'M';'F';'M';'F'}); %! w = [118;122;135;120;150;128;140;124;119;145;130;126;138;121;136]; %! t2 = table (z, g, w); %! c = compact (fitlm (t2, 'w ~ g*z')); %! t = anova (c, 'components', 3); %! assert_equal (t.Properties.RowNames, {'z'; 'g'; 'z:g'; 'Error'}); %! assert_equal (t.SumSq(1), 1086.93049009449, -1e-6); %! assert_equal (t.F(1), 525.990068941166, -1e-6); %! assert_equal (t.SumSq(2), 3.73180668223453, -1e-8); %! assert_equal (t.SumSq(3), 7.05971182791548, -1e-8); %! assert_equal (t.SumSq(4), 22.7309147016933, -1e-8); %! assert_equal (t.DF(4), 11); %!test %! z = [25;31;42;29;55;38;46;33;27;50;41;36;48;30;44]; %! g = categorical ({'M';'F';'F';'M';'M';'F';'M';'F';'F';'M';'F';'M';'F';'M';'F'}); %! w = [118;122;135;120;150;128;140;124;119;145;130;126;138;121;136]; %! t2 = table (z, g, w); %! c = compact (fitlm (t2, 'w ~ g + z^2')); %! t = anova (c, 'summary'); %! assert_equal (t.Properties.RowNames, ... %! {'Total'; 'Model'; '. Linear'; '. Nonlinear'; 'Residual'}); %! assert_equal (t.SumSq(2), 1394.52505797828, -1e-6); %! assert_equal (t.F(2), 241.097329241452, -1e-7); %! assert_equal (t.SumSq(3), 1385.94270680373, -1e-6); %! assert_equal (t.SumSq(4), 8.58235117455061, -1e-8); %! assert_equal (t.SumSq(5), 21.2082753550521, -1e-8); %! assert_equal (t.DF(5), 11); %!error CompactLinearModel (123) %!error cmdl(1) %!error cmdl{1} %!error cmdl.NotAProperty %!error cmdl.Fitted %!error cmdl.ObservationInfo %!error cmdl.Steps %!error coefCI (cmdl, 0.05, 'extra') %!error coefCI (cmdl, 1.5) %!error coefCI (cmdl, -0.1) %!error coefCI (cmdl, NaN) %!error coefCI (cmdl, [0.01 0.05]) %!error coefCI (cmdl, 'abc') %!error coefTest (cmdl, [1 0]) %!error coefTest (cmdl, 'abc') %!error coefTest (cmdl, [0 1 0], 'abc') %!error coefTest (cmdl, [0 1 0; 0 0 1], [1]) %!error coefTest (cmdl, [0 NaN 0]) %!error coefTest (cmdl, [0 1 0], 0, 'extra') %!error [a, b, c, d] = coefTest (cmdl) %!error predict (cmdl) %!error predict (cmdl, [0.5 0.25], 'BadOption', 1) %!error predict (cmdl, [0.5 0.25], 'Prediction', 'bad') %!error predict (cmdl, ones (3, 5)) %!error predict (cmdl, ones (3, 1)) %!error predict (cmdl, table ([1;2], 'VariableNames', {'z'})) %!error random (cmdl) %!error random (cmdl, [0.5 0.25], 'extra') %!error random (cmdl, ones (3, 5)) %!error random (cmdl, []) %!error feval (cmdl) %!error feval (cmdl, [0.5;1.0], [0.25;1.0], [0.1;0.2]) %!error feval (cmdl, ones (3, 1)) %!error feval (cmdl, [0.5;1.0;0.2], [0.25;1.0]) %!error feval (cmdl, table ([1;2], 'VariableNames', {'z'})) %!error feval (cmdl, []) %!error feval (cmdl, '2500', 0.25) %!error plotEffects (cmdl, 'extra') %!error plotEffects (cmdl, 'a', 'b') %!error plotEffects (compact (fitlm (X(:,1), y, 'constant'))) %!error plotInteraction (cmdl) %!error plotInteraction (cmdl, 'x1') %!error plotInteraction (cmdl, 'x1', 'x2', 'badtype') %!error plotInteraction (cmdl, 'x1', 'x2', 'effects', 'extra') %!error plotInteraction (cmdl, 'z', 'x2') %!error plotInteraction (cmdl, 'x1', 'z') %!error plotInteraction (cmdl, 99, 'x2') %!error plotInteraction (cmdl, 1.5, 'x2') %!error plotInteraction (cmdl, 'y', 'x2') %!error plotInteraction (cmdl, 'x1', 'y') %!error plotInteraction (cmdl, 'x1', 'x1') %!error anova (cmdl, 'components', 'h', 'extra') %!error anova (cmdl, 'bogus') %!error anova (cmdl, 'components', 4) statistics-release-2.1.2/inst/Regression/CoxModel.m000066400000000000000000001651241527311104300223170ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftp {statistics} {} CoxModel ## ## Cox proportional hazards regression model class. ## ## A @code{CoxModel} object encapsulates a Cox proportional hazards model of a ## survival time on one or more predictors, fitted by maximizing the Cox partial ## likelihood. It is the object counterpart of @code{coxphfit} and is normally ## created with the @code{fitcox} function. ## ## The model states that an observation with predictor values @var{x} has hazard ## ## @tex ## $$ h(x, t) = h_0(t)\exp\left(\sum_{j=1}^{p} x_{j} b_j\right) $$ ## @end tex ## @ifnottex ## @math{h(x, t) = h_0(t) exp (x' b)} ## @end ifnottex ## ## where @math{h_0(t)} is an unspecified baseline hazard. The model carries no ## constant term: any constant is absorbed into that baseline. ## ## The most useful properties are @code{Coefficients} (a table of estimates, ## standard errors, @math{z}-statistics and p-values), @code{Hazard} (the ## estimated baseline cumulative hazard), @code{LogLikelihood}, ## @code{Residuals}, and the three p-values @code{LikelihoodRatioTestPValue}, ## @code{ProportionalHazardsPValue} and ## @code{ProportionalHazardsPValueGlobal}. Fitted models support the ## @code{survival}, @code{hazardratio}, @code{coefci}, @code{linhyptest}, ## @code{plotSurvival} and @code{discardResiduals} methods. ## ## A categorical predictor expands to indicator columns, one per level bar the ## first, which the baseline hazard carries; the indicator columns are named ## @qcode{@var{name}_@var{level}} and enter the default baseline as zero, while ## a numeric predictor enters it as its mean. ## ## @code{ProportionalHazardsPValue} is a Grambsch-Therneau test of each ## coefficient against the mid-ranks of the event times, and ## @code{ProportionalHazardsPValueGlobal} the same test taken over the whole ## model. A small p-value is evidence that the hazard ratio moves with time, ## which is what proportionality denies. ## ## @strong{Deviations from MATLAB, all in naming.} MATLAB derives the names ## reported by a fitted model from three different places and they need not ## agree with one another: with default predictor names its @code{Formula} ## reads @qcode{'y ~ x1 + x2'} in lower case while @code{PredictorNames} holds ## @qcode{'X1'} and @qcode{'X2'}, and supplying @qcode{'PredictorNames'} ## changes @code{ResponseName} from @qcode{'y'} to the name of the variable ## passed as the response. Here the names are consistent by construction: ## @code{ResponseName} is @qcode{'y'} unless the data came from a table, the ## @code{Formula} is built from @code{PredictorNames} and ## @code{ResponseName}, and neither depends on which optional arguments were ## given. Every fitted quantity agrees with MATLAB. ## ## @seealso{fitcox, coxphfit, GeneralizedLinearModel, LinearModel} ## @end deftp classdef CoxModel properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {CoxModel} {property} Coefficients ## ## Coefficient estimates and their statistics ## ## A table with one row per encoded predictor column, its row names the ## encoded column names, and the variables @qcode{Beta}, @qcode{SE}, ## @qcode{zStat} and @qcode{pValue}. This property is read-only. ## ## @end deftp Coefficients = []; ## -*- texinfo -*- ## @deftp {CoxModel} {property} NumPredictors ## ## Number of predictors ## ## A positive integer counting the predictor variables of the model, ## before any categorical predictor is encoded. This property is ## read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {CoxModel} {property} LogLikelihood ## ## Log-likelihood of the fitted model ## ## A scalar, the maximised Cox partial log-likelihood. This property is ## read-only. ## ## @end deftp LogLikelihood = []; ## -*- texinfo -*- ## @deftp {CoxModel} {property} Hazard ## ## Estimated baseline cumulative hazard ## ## A numeric matrix of event times in its first column and the cumulative ## hazard at them in its second. A stratified model adds a third column ## holding the stratum each row belongs to. This property is read-only. ## ## @end deftp Hazard = []; ## -*- texinfo -*- ## @deftp {CoxModel} {property} PredictorNames ## ## Names of the predictor variables ## ## A cell array of character vectors with one name per predictor. This ## property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {CoxModel} {property} ResponseName ## ## Name of the response variable ## ## A character vector naming the response, which is the survival time. ## This property is read-only. ## ## @end deftp ResponseName = 'y'; ## -*- texinfo -*- ## @deftp {CoxModel} {property} Formula ## ## Model formula ## ## A @code{LinearFormula} object describing the terms of the model. This ## property is read-only. ## ## @end deftp Formula = []; ## -*- texinfo -*- ## @deftp {CoxModel} {property} Baseline ## ## Predictor values the baseline hazard is evaluated at ## ## The baseline the fit used, one row per stratum. It is reported as it ## was given when @code{fitcox} was given one, a scalar staying a scalar, ## and otherwise holds the rows the fit was centred on. This property is ## read-only. ## ## @end deftp Baseline = []; ## -*- texinfo -*- ## @deftp {CoxModel} {property} Stratification ## ## Stratification levels used in the fit ## ## The distinct levels of the stratification variable. It is empty when ## the model is not stratified. This property is read-only. ## ## @end deftp Stratification = []; ## -*- texinfo -*- ## @deftp {CoxModel} {property} CoefficientCovariance ## ## Estimated covariance of the coefficients ## ## A square numeric matrix, one row and column per encoded predictor ## column, holding the estimated covariance of the estimates in ## @qcode{Coefficients}. This property is read-only. ## ## @end deftp CoefficientCovariance = []; ## -*- texinfo -*- ## @deftp {CoxModel} {property} StandardError ## ## Standard errors of the coefficients ## ## A numeric column vector, the square roots of the diagonal of ## @qcode{CoefficientCovariance}, which is column @qcode{SE} of ## @qcode{Coefficients}. This property is read-only. ## ## @end deftp StandardError = []; ## -*- texinfo -*- ## @deftp {CoxModel} {property} Residuals ## ## Residuals of the fitted model ## ## A table with one row per observation and the variables ## @qcode{CoxSnell}, @qcode{Deviance}, @qcode{Martingale}, ## @qcode{Schoenfeld}, @qcode{ScaledSchoenfeld}, @qcode{Score} and ## @qcode{ScaledScore}. This property is read-only. ## ## @end deftp Residuals = []; ## -*- texinfo -*- ## @deftp {CoxModel} {property} ProportionalHazardsPValue ## ## Proportional hazards test, one predictor at a time ## ## A numeric vector with one @math{p} value per predictor, testing whether ## that predictor's effect is constant over time. A small value is ## evidence against the proportional hazards assumption. This property is ## read-only. ## ## @end deftp ProportionalHazardsPValue = []; ## -*- texinfo -*- ## @deftp {CoxModel} {property} ProportionalHazardsPValueGlobal ## ## Proportional hazards test over the whole model ## ## A scalar @math{p} value testing the proportional hazards assumption for ## all predictors at once. This property is read-only. ## ## @end deftp ProportionalHazardsPValueGlobal = []; ## -*- texinfo -*- ## @deftp {CoxModel} {property} LikelihoodRatioTestPValue ## ## Likelihood ratio test against the null model ## ## A scalar @math{p} value comparing the fitted model with the model that ## carries no predictors. This property is read-only. ## ## @end deftp LikelihoodRatioTestPValue = []; ## -*- texinfo -*- ## @deftp {CoxModel} {property} VariableInfo ## ## Information about the variables ## ## A table with one row per variable, its row names the variable names, ## and the variables @qcode{Class}, @qcode{Range}, @qcode{InModel} and ## @qcode{IsCategorical}. This property is read-only. ## ## @end deftp VariableInfo = []; endproperties properties (Access = private, Hidden) b_ = []; # coefficient vector aligned with the encoded columns X_ = []; # encoded predictor matrix used in the fit T_ = []; # response as passed to coxphfit cens_ = []; # censoring indicator freq_ = []; # frequency weights strata_ = []; # stratum label per observation ([] when unstratified) baserow_ = []; # baseline actually used, one row per stratum ties_ = 'breslow'; catinfo_ = []; # categorical encoding, for new data catcols_ = []; # logical mask of categorical predictors catbase_ = []; # logical mask of encoded columns that are indicators formulastr_ = ''; endproperties methods (Hidden) ## Custom display of the object name. function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ("%s =\n", in_name); endif disp (this); endfunction ## Custom display of the model summary. function disp (this) fprintf ("\n Cox proportional hazards regression model:\n"); if (! isempty (this.formulastr_)) fprintf (" %s\n", this.formulastr_); endif if (! isempty (this.Coefficients)) fprintf ("\n Coefficients:\n\n"); disp (this.Coefficients); endif fprintf ("\n"); if (! isempty (this.LogLikelihood)) fprintf ("Log-likelihood: %g\n", this.LogLikelihood); endif if (! isempty (this.LikelihoodRatioTestPValue)) fprintf ("Likelihood ratio test vs. constant model: p-value = %g\n", ... this.LikelihoodRatioTestPValue); endif endfunction ## Class specific subscripted reference. function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); switch (s.type) case '()' error (strcat ("CoxModel: () indexing is not supported.", ... " Use dot notation for properties.")); case '{}' error (strcat ("CoxModel: {} indexing is not supported.", ... " Use dot notation for properties.")); case '.' if (! ischar (s.subs)) error (strcat ("CoxModel.subsref: property name must be a", ... " character vector.")); endif if (ismethod (this, s.subs)) [varargout{1:nargout}] = builtin ('subsref', this, [s, chain_s]); return; endif try out = this.(s.subs); catch error ("CoxModel.subsref: unknown property '%s'.", s.subs); end_try_catch endswitch if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {CoxModel} {@var{mdl} =} CoxModel (@var{X}, @var{T}) ## @deftypefnx {CoxModel} {@var{mdl} =} CoxModel (@var{tbl}, @var{respvar}) ## @deftypefnx {CoxModel} {@var{mdl} =} CoxModel (@dots{}, @var{Name}, @var{Value}) ## ## Fit a Cox proportional hazards regression model. ## ## @code{@var{mdl} = CoxModel (@var{X}, @var{T})} fits the model to the ## @math{n}-by-@math{p} numeric predictor matrix @var{X} and the ## @math{n}-by-1 vector of event times @var{T}. @var{T} may instead be an ## @math{n}-by-2 matrix giving a @math{(start, stop]} interval of exposure, ## the counting process form. ## ## @code{@var{mdl} = CoxModel (@var{tbl}, @var{respvar})} takes the data ## from the table @var{tbl}, using the variable named @var{respvar} as the ## response and every other variable as a predictor. A @code{categorical} ## variable is encoded as indicator columns. ## ## @var{X} must not contain a constant column: the model has no constant ## term, since any constant is absorbed into the baseline hazard. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @multitable @columnfractions 0.25 0.75 ## @headitem Name @tab Value ## @item @qcode{"Baseline"} @tab The @var{X} values at which the baseline ## hazard is computed, either a scalar or a 1-by-@math{p} vector. The ## default is the mean of each numeric predictor and zero for each indicator ## column of a categorical predictor, taken within each stratum. ## @item @qcode{"Beta"} @tab The starting value of the iteration, a vector ## of length @math{p}. The default is @code{0.01 ./ std (@var{X})}. ## @item @qcode{"CategoricalPredictors"} @tab The predictors to treat as ## categorical, given as column indices, a logical vector, or a cell array ## of predictor names. Table variables of class @code{categorical} are ## detected without this argument. ## @item @qcode{"Censoring"} @tab A logical or 0/1 vector of length ## @math{n}, where 1 marks an observation right-censored at its recorded ## time. The default is a vector of zeros. ## @item @qcode{"Frequency"} @tab A vector of length @math{n} of ## non-negative values giving the number of observations each row ## represents, or a weight. The default is a vector of ones. ## @item @qcode{"OptimizationOptions"} @tab A structure of iteration ## settings, as built by @code{statset ("fitcox")}. The fields used are ## @qcode{"MaxIter"}, @qcode{"TolX"} and @qcode{"Display"}. ## @item @qcode{"PredictorNames"} @tab A cell array of @math{p} predictor ## names. The default is @qcode{"X1"}, @qcode{"X2"}, and so on, or the ## table variable names. ## @item @qcode{"Stratification"} @tab A vector of length @math{n} of ## stratum labels. Each stratum carries its own baseline hazard and its own ## risk sets, while the coefficients are shared across all of them. ## @item @qcode{"TieBreakMethod"} @tab The method of handling tied event ## times, either @qcode{"breslow"} (default) or @qcode{"efron"}. ## @end multitable ## ## @end deftypefn function this = CoxModel (X, T, varargin) if (nargin < 2) error ("CoxModel: too few input arguments."); endif ## --- the data ---------------------------------------------------- istbl = isa (X, 'table'); if (istbl) if (! ((ischar (T) && isrow (T)) || (isa (T, 'string') && isscalar (T)))) error (strcat ("CoxModel: RESPVAR must be a character vector", ... " naming a variable of the data table.")); endif resp_name = char (T); all_names = X.Properties.VariableNames; r_idx = find (strcmp (all_names, resp_name), 1); if (isempty (r_idx)) error ("CoxModel: '%s' is not a variable of the data table.", ... resp_name); endif T_val = X.(resp_name); pred_names = all_names; pred_names(r_idx) = []; tbl = X; else if (! (isnumeric (X) && isreal (X) && ismatrix (X))) error ("CoxModel: X must be a real numeric matrix."); endif T_val = T; resp_name = 'y'; pred_names = arrayfun (@(k) sprintf ("X%d", k), 1:columns (X), ... 'UniformOutput', false); tbl = []; endif p_raw = numel (pred_names); if (p_raw < 1) error ("CoxModel: the model needs at least one predictor."); endif ## --- optional arguments ------------------------------------------ Baseline = []; Beta0 = []; CatPreds = []; Censoring = []; Frequency = []; Options = []; PredNames = {}; Strata = []; Ties = 'breslow'; if (mod (numel (varargin), 2) != 0) error ("CoxModel: optional arguments must be name-value pairs."); endif for i = 1:2:numel (varargin) name = varargin{i}; if (! (ischar (name) && isrow (name))) error ("CoxModel: parameter name must be a character vector."); endif switch (lower (name)) case 'baseline' Baseline = varargin{i+1}; case 'beta' Beta0 = varargin{i+1}; case 'categoricalpredictors' CatPreds = varargin{i+1}; case 'censoring' Censoring = varargin{i+1}; case 'frequency' Frequency = varargin{i+1}; case 'optimizationoptions' Options = varargin{i+1}; case 'predictornames' PredNames = varargin{i+1}; case 'stratification' Strata = varargin{i+1}; case 'tiebreakmethod' Ties = varargin{i+1}; otherwise error ("CoxModel: unknown parameter name '%s'.", name); endswitch endfor if (! isempty (PredNames)) if (! (iscellstr (PredNames) && numel (PredNames) == p_raw)) error (strcat ("CoxModel: 'PredictorNames' must be a cell array", ... " of one character vector per predictor.")); endif pred_names = PredNames(:)'; endif if (! (ischar (Ties) && any (strcmpi (Ties, {'breslow', 'efron'})))) error (strcat ("CoxModel: 'TieBreakMethod' must be either", ... " 'breslow' or 'efron'.")); endif Ties = lower (Ties); ## --- the predictor data, under the names the model will report ---- ## A renamed table variable is renamed once, here, so that every later ## lookup goes through PredictorNames alone. if (istbl) X_raw = []; tbl_p = tbl(:, setdiff (1:numel (all_names), r_idx)); tbl_p.Properties.VariableNames = pred_names; n_total = rows (tbl); data_in = tbl_p; else X_raw = X; tbl_p = []; n_total = rows (X); data_in = X; endif ## --- which predictors are categorical ---------------------------- cat_cols = false (1, p_raw); if (istbl) for j = 1:p_raw col = tbl_p.(pred_names{j}); cat_cols(j) = isa (col, 'categorical') || iscell (col); endfor endif if (! isempty (CatPreds)) cat_cols = cat_cols | categorical_mask (CatPreds, pred_names); endif ## --- numeric codes, then indicator columns ----------------------- [X_num, cat_levels] = raw_to_codes (data_in, X_raw, tbl_p, pred_names, ... cat_cols, n_total); ## The baseline hazard plays the part of an intercept -- it is what the ## reference level of a categorical predictor is folded into -- so the ## encoding is reference coded even though the model has no constant ## term, which is what MATLAB does here too. [X_enc, enc_names, cat_info] = encode_categorical (X_num, cat_cols, ... pred_names, ... cat_levels, true); p = columns (X_enc); ## Mark the encoded columns that came from a categorical predictor: they ## enter the default baseline as zero rather than as their mean. ## encode_categorical gives a categorical predictor one column per level ## bar the reference and a numeric one a single column, so the encoded ## columns are walked in the same order to find which came from which. cat_enc = false (1, p); k = 0; for j = 1:p_raw if (cat_cols(j)) nlev = numel (cat_levels{j}) - 1; cat_enc(k+1:k+nlev) = true; k += nlev; else k += 1; endif endfor ## --- the fit ------------------------------------------------------ args = {}; if (! isempty (Censoring)) args = [args, {'Censoring', Censoring}]; endif if (! isempty (Frequency)) args = [args, {'Frequency', Frequency}]; endif if (! isempty (Beta0)) args = [args, {'B0', Beta0}]; endif if (! isempty (Options)) args = [args, {'Options', Options}]; endif if (! isempty (Strata)) args = [args, {'Strata', Strata}]; endif args = [args, {'Ties', Ties}]; ## The baseline each stratum is centred on. A numeric predictor enters ## it as its frequency-weighted mean and a categorical one as zero, the ## reference level being what the baseline hazard already carries. w = ones (n_total, 1); if (! isempty (Frequency)) w = Frequency(:); endif base_given = ! isempty (Baseline); if (isempty (Strata)) slev = []; nS = 1; smask = {true(n_total, 1)}; else slev = unique (Strata(:)); nS = numel (slev); smask = arrayfun (@(v) Strata(:) == v, slev, 'UniformOutput', false); endif brow = zeros (nS, p); # baseline reported and predicted against bdef = zeros (nS, p); # baseline coxphfit centres on by default for s = 1:nS m = smask{s}; bdef(s,:) = sum (w(m) .* X_enc(m,:), 1) / sum (w(m)); if (base_given) bl = Baseline; if (isscalar (bl)) bl = repmat (bl, 1, p); endif brow(s,:) = bl(:)'; else brow(s,:) = bdef(s,:); brow(s,cat_enc) = 0; endif endfor ## coxphfit takes one baseline for every stratum, so a per-stratum ## baseline is reached by fitting on its own default and rescaling the ## hazard afterwards -- exact, the hazard being scaled by exp (B b) and ## nothing else. Only a stratified model with a categorical predictor ## needs it; anything else states its baseline outright. rescale = false; if (base_given) args = [args, {'Baseline', Baseline}]; elseif (nS == 1) args = [args, {'Baseline', brow(1,:)}]; elseif (any (cat_enc)) rescale = true; endif [b, logl, H, stats] = coxphfit (X_enc, T_val, args{:}); if (rescale) for s = 1:nS m = H(:,3) == slev(s); H(m,2) = H(m,2) * exp ((brow(s,:) - bdef(s,:)) * b); endfor endif ## --- properties --------------------------------------------------- this.b_ = b; this.X_ = X_enc; this.T_ = T_val; this.cens_ = Censoring; this.freq_ = Frequency; this.strata_ = Strata; this.ties_ = Ties; this.catinfo_ = cat_info; this.catcols_ = cat_cols; this.catbase_ = cat_enc; this.NumPredictors = p_raw; this.LogLikelihood = logl; this.Hazard = H; this.PredictorNames = pred_names; this.ResponseName = resp_name; this.CoefficientCovariance = stats.covb; this.StandardError = stats.se(:); this.LikelihoodRatioTestPValue = stats.LikelihoodRatioTestP; this.Coefficients = table (b(:), stats.se(:), stats.z(:), stats.p(:), ... 'VariableNames', {'Beta', 'SE', 'zStat', 'pValue'}, ... 'RowNames', enc_names(:)); this.Residuals = table (stats.csres, stats.devres, stats.martres, ... stats.schres, stats.sschres, stats.scores, stats.sscores, ... 'VariableNames', {'CoxSnell', 'Deviance', 'Martingale', ... 'Schoenfeld', 'ScaledSchoenfeld', 'Score', ... 'ScaledScore'}); ## The baseline actually used, one row per stratum, kept for prediction. ## Reported as given when it was given -- a scalar stays a scalar -- and ## otherwise as the rows the fit was centred on. this.Stratification = slev; this.baserow_ = brow; if (base_given) this.Baseline = Baseline; else this.Baseline = brow; endif ## --- formula and per-variable information ------------------------- var_names_all = [pred_names, {resp_name}]; terms = [eye(p_raw), zeros(p_raw, 1)]; this.Formula = LinearFormula (terms, var_names_all, ... 'ResponseName', resp_name); this.formulastr_ = char (this.Formula); vi_class = cell (p_raw + 1, 1); vi_range = cell (p_raw + 1, 1); vi_inmodel = [true(p_raw, 1); false]; vi_iscat = [cat_cols(:); false]; for j = 1:p_raw if (istbl) col = tbl_p.(pred_names{j}); else col = X_raw(:, j); endif [vi_class{j}, vi_range{j}] = variable_class_and_range (col); endfor [vi_class{end}, vi_range{end}] = variable_class_and_range (T_val(:,end)); this.VariableInfo = table (vi_class, vi_range, vi_inmodel, vi_iscat, ... 'VariableNames', {'Class', 'Range', 'InModel', 'IsCategorical'}, ... 'RowNames', var_names_all(:)); ## --- the proportional hazards assumption -------------------------- [phz, phg] = ph_assumption_test (stats, T_val); this.ProportionalHazardsPValue = phz; this.ProportionalHazardsPValueGlobal = phg; endfunction ## -*- texinfo -*- ## @deftypefn {CoxModel} {@var{ci} =} coefci (@var{obj}) ## @deftypefnx {CoxModel} {@var{ci} =} coefci (@var{obj}, @var{level}) ## ## Confidence intervals for the coefficients of a Cox model. ## ## @code{@var{ci} = coefci (@var{obj})} returns a two-column matrix with one ## row per coefficient, holding the 95% confidence interval of each. ## ## @code{@var{ci} = coefci (@var{obj}, @var{level})} uses a ## @code{100 (1 - @var{level})}% interval. @var{level} must be a positive ## scalar smaller than 1; it is a significance level, not a coverage. ## ## @end deftypefn function ci = coefci (this, level) if (nargin < 2) level = 0.05; endif if (! (isnumeric (level) && isreal (level) && isscalar (level) && level > 0 && level < 1)) error (strcat ("CoxModel.coefci: LEVEL must be a real scalar", ... " greater than 0 and smaller than 1.")); endif z = norminv (1 - level / 2); se = this.StandardError(:); b = this.b_(:); ci = [b - z * se, b + z * se]; endfunction ## -*- texinfo -*- ## @deftypefn {CoxModel} {@var{obj} =} discardResiduals (@var{obj}) ## ## Drop the stored residuals of a Cox model. ## ## @code{@var{obj} = discardResiduals (@var{obj})} returns the model with an ## empty @code{Residuals} property. The residual table holds one row per ## observation and is the largest thing a fitted model carries, so ## discarding it makes a model that is only going to be used for prediction ## considerably smaller. Nothing else about the model changes. ## ## @end deftypefn function this = discardResiduals (this) this.Residuals = table (); endfunction ## -*- texinfo -*- ## @deftypefn {CoxModel} {@var{hr} =} hazardratio (@var{obj}, @var{X}) ## @deftypefnx {CoxModel} {@var{hr} =} hazardratio (@var{obj}, @var{X}, @var{S}) ## @deftypefnx {CoxModel} {@var{hr} =} hazardratio (@dots{}, @qcode{"Baseline"}, @var{B}) ## ## Hazard of a Cox model relative to its baseline. ## ## @code{@var{hr} = hazardratio (@var{obj}, @var{X})} returns the hazard at ## the predictor values @var{X} relative to the baseline the model was ## fitted with, @code{exp ((@var{X} - @var{B}) * @var{b})}. @var{X} has one ## row per evaluation point and is a numeric matrix, or a table when the ## model was fitted from one. ## ## @code{@var{hr} = hazardratio (@var{obj}, @var{X}, @var{S})} gives the ## stratum of each row of @var{X}, and is required when the model is ## stratified, each stratum having its own baseline. ## ## @code{@var{hr} = hazardratio (@dots{}, "Baseline", @var{B})} evaluates ## the ratio against the baseline @var{B} instead, either a scalar or a row ## vector with one element per encoded predictor column. ## ## @end deftypefn function hr = hazardratio (this, varargin) if (numel (varargin) < 1) error ("CoxModel.hazardratio: X is required."); endif [Xq, Sq, opts] = split_prediction_args (this, 'hazardratio', varargin); base = prediction_baseline (this, Sq, opts.Baseline, 'hazardratio'); hr = exp (sum ((Xq - base) .* this.b_(:)', 2)); endfunction ## -*- texinfo -*- ## @deftypefn {CoxModel} {@var{tbl} =} linhyptest (@var{obj}) ## ## Sequential tests on the coefficients of a Cox model. ## ## @code{@var{tbl} = linhyptest (@var{obj})} returns a table with one row ## per predictor, whose @math{k}-th row tests the hypothesis that the ## coefficients of the @math{k}-th and every later predictor are jointly ## zero. The @code{Predictor} column names the predictors the hypothesis ## leaves in the model, so its first row reads @qcode{"Empty Model"} and ## tests every coefficient at once, and its last row tests the last ## coefficient alone, reproducing that coefficient's own p-value. ## ## Each test is a Wald test on the fitted model, not a refit. ## ## @end deftypefn function tbl = linhyptest (this) p = numel (this.b_); pvals = zeros (p, 1); names = cell (p, 1); for k = 1:p idx = k:p; bk = this.b_(idx); Vk = this.CoefficientCovariance(idx, idx); stat = bk(:)' * (Vk \ bk(:)); pvals(k) = chi2cdf (stat, numel (idx), 'upper'); if (k == 1) names{k} = 'Empty Model'; else names{k} = strjoin (this.Coefficients.Properties.RowNames(1:k-1)', ... ', '); endif endfor tbl = table (names, pvals, ... 'VariableNames', {'Predictor', 'pValue'}); endfunction ## -*- texinfo -*- ## @deftypefn {CoxModel} {} plotSurvival (@var{obj}) ## @deftypefnx {CoxModel} {} plotSurvival (@var{obj}, @var{X}) ## @deftypefnx {CoxModel} {} plotSurvival (@var{obj}, @var{X}, @var{S}) ## @deftypefnx {CoxModel} {@var{h} =} plotSurvival (@dots{}) ## ## Plot the survival function of a Cox model. ## ## @code{plotSurvival (@var{obj})} draws the survival function at the ## model's baseline as a stairstep plot. @code{plotSurvival (@var{obj}, ## @var{X})} draws it at the predictor values @var{X}, one curve per row, ## and @var{S} gives the stratum of each row when the model is stratified. ## A stratified model with no @var{X} draws one curve per stratum. ## ## @code{@var{h} = plotSurvival (@dots{})} returns the handles of the ## stairstep lines. ## ## @end deftypefn function varargout = plotSurvival (this, varargin) [s, tout] = survival (this, varargin{:}); ## Unstratified curves come back as the columns of a matrix over one ## grid; stratified ones as a cell array, each on its own grid. if (iscell (s)) curves = s; times = tout; else curves = num2cell (s, 1); times = repmat ({tout}, 1, columns (s)); endif h = zeros (numel (curves), 1); hold_state = ishold (); for k = 1:numel (curves) h(k) = stairs (times{k}, curves{k}); if (k == 1) hold on; endif endfor if (! hold_state) hold off; endif xlabel (this.ResponseName); ylabel ("Survival probability"); title ("Cox proportional hazards model"); if (nargout > 0) varargout{1} = h; endif endfunction ## -*- texinfo -*- ## @deftypefn {CoxModel} {@var{s} =} survival (@var{obj}) ## @deftypefnx {CoxModel} {@var{s} =} survival (@var{obj}, @var{X}) ## @deftypefnx {CoxModel} {@var{s} =} survival (@var{obj}, @var{X}, @var{S}) ## @deftypefnx {CoxModel} {@var{s} =} survival (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {CoxModel} {[@var{s}, @var{T}] =} survival (@dots{}) ## ## Survival function of a Cox model. ## ## @code{@var{s} = survival (@var{obj})} returns the survival probability at ## the model's baseline, evaluated at each row of the @code{Hazard} ## property. @code{@var{s} = survival (@var{obj}, @var{X})} evaluates it at ## the predictor values @var{X}, and @var{S} gives the stratum of each row ## when the model is stratified. For a stratified model @var{s} is a cell ## array holding one column vector per curve. ## ## @code{[@var{s}, @var{T}] = survival (@dots{})} also returns the times the ## probabilities refer to. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @multitable @columnfractions 0.3 0.7 ## @headitem Name @tab Value ## @item @qcode{"Time"} @tab The times at which to evaluate the survival ## function. The default is the model's own event times. The baseline ## survival is interpolated linearly between them and raised to the hazard ## ratio of @var{X}. ## @item @qcode{"ExtrapolationMethod"} @tab How to evaluate a time outside ## the model's event times: @qcode{"nearest"} (default), @qcode{"linear"}, ## @qcode{"next"}, @qcode{"previous"}, or @qcode{"none"}. @qcode{"none"} ## returns @qcode{NaN} outside the range, as do @qcode{"next"} above it and ## @qcode{"previous"} below it. ## @end multitable ## ## @end deftypefn function [s, Tout] = survival (this, varargin) [Xq, Sq, opts] = split_prediction_args (this, 'survival', varargin); base = prediction_baseline (this, Sq, [], 'survival'); if (isempty (Xq)) hr = ones (max (1, numel (Sq)), 1); else hr = exp (sum ((Xq - base) .* this.b_(:)', 2)); endif strat = this.Stratification; if (isempty (strat)) grid_t = {this.Hazard(:,1)}; grid_H = {this.Hazard(:,2)}; which_grid = ones (numel (hr), 1); else nS = numel (strat); grid_t = cell (nS, 1); grid_H = cell (nS, 1); for k = 1:nS m = this.Hazard(:,3) == strat(k); grid_t{k} = this.Hazard(m,1); grid_H{k} = this.Hazard(m,2); endfor if (isempty (Sq)) which_grid = (1:nS)'; hr = ones (nS, 1); else [~, which_grid] = ismember (Sq(:), strat); endif endif n_out = numel (which_grid); s = cell (n_out, 1); Tout = cell (n_out, 1); for k = 1:n_out g = which_grid(k); tg = grid_t{g}; S0 = exp (-grid_H{g}); if (isempty (opts.Time)) s{k} = S0 .^ hr(k); Tout{k} = tg; else tq = opts.Time(:); s{k} = interp_survival (tg, S0, tq, opts.Extrap) .^ hr(k); Tout{k} = tq; endif endfor ## Unstratified curves all share one time grid, so they come back as the ## columns of a matrix, one per row of X. A stratified model gives each ## curve its own stratum's grid, so those stay a cell array. if (isempty (strat)) s = cell2mat (s(:)'); Tout = Tout{1}; else s = s(:)'; Tout = Tout(:)'; endif endfunction endmethods endclassdef ## Resolve a 'CategoricalPredictors' value to a logical mask over the ## predictors. function mask = categorical_mask (spec, pred_names) p = numel (pred_names); mask = false (1, p); if (islogical (spec)) if (numel (spec) != p) error (strcat ("CoxModel: a logical 'CategoricalPredictors' must", ... " have one element per predictor.")); endif mask = spec(:)'; elseif (isnumeric (spec)) if (any (spec != fix (spec)) || any (spec < 1) || any (spec > p)) error (strcat ("CoxModel: 'CategoricalPredictors' indices must be", ... " integers between 1 and the number of predictors.")); endif mask(spec) = true; elseif (iscellstr (spec) || ischar (spec)) if (ischar (spec)) spec = {spec}; endif for k = 1:numel (spec) j = find (strcmp (pred_names, spec{k}), 1); if (isempty (j)) error ("CoxModel: '%s' is not a predictor name.", spec{k}); endif mask(j) = true; endfor else error (strcat ("CoxModel: 'CategoricalPredictors' must be indices, a", ... " logical vector, or predictor names.")); endif endfunction ## Split the arguments of a prediction method into the predictor values, the ## stratum labels, and the name-value options. function [Xq, Sq, opts] = split_prediction_args (this, caller, args) opts = struct ('Time', [], 'Extrap', 'nearest', 'Baseline', []); ## Each method takes only its own options: 'Baseline' belongs to ## hazardratio, the times to survival. Accepting an option and ignoring it ## would claim a capability that is not there. if (strcmp (caller, 'hazardratio')) keys = {'baseline'}; else keys = {'time', 'extrapolationmethod'}; endif ## Every name is known to the scan that separates the positional arguments ## from the pairs, so that a name meant for the other method is reported as ## the wrong name rather than as a malformed argument list. scan_keys = {'time', 'extrapolationmethod', 'baseline'}; ## Positional arguments run until the first name that starts a pair. npos = 0; while (npos < numel (args) && npos < 2) a = args{npos+1}; if ((ischar (a) && isrow (a)) && any (strcmpi (a, scan_keys)) && numel (args) > npos + 1) break; endif npos++; endwhile Xq = []; Sq = []; if (npos >= 1) Xq = args{1}; endif if (npos >= 2) Sq = args{2}; endif rest = args(npos+1:end); if (mod (numel (rest), 2) != 0) error ("CoxModel.%s: optional arguments must be name-value pairs.", caller); endif for i = 1:2:numel (rest) name = rest{i}; if (! (ischar (name) && isrow (name))) error ("CoxModel.%s: parameter name must be a character vector.", caller); endif if (! any (strcmpi (name, keys))) error ("CoxModel.%s: unknown parameter name '%s'.", caller, name); endif switch (lower (name)) case 'time' t = rest{i+1}; if (! (isnumeric (t) && isreal (t) && isvector (t))) error ("CoxModel.%s: 'Time' must be a real vector.", caller); endif opts.Time = t; case 'extrapolationmethod' m = rest{i+1}; valid = {'nearest', 'linear', 'next', 'previous', 'none'}; if (! (ischar (m) && any (strcmpi (m, valid)))) error (strcat ("CoxModel.%s: 'ExtrapolationMethod' must be one", ... " of 'nearest', 'linear', 'next', 'previous', or", ... " 'none'."), caller); endif opts.Extrap = lower (m); case 'baseline' opts.Baseline = rest{i+1}; otherwise error ("CoxModel.%s: unknown parameter name '%s'.", caller, name); endswitch endfor ## Encode a table or raw predictor matrix the same way the fit did. if (! isempty (Xq)) Xq = encode_new_data (this, Xq, caller); endif if (! isempty (this.Stratification) && ! isempty (Xq) && isempty (Sq)) error (strcat ("CoxModel.%s: the model is stratified, so the stratum", ... " of each row of X is required."), caller); endif if (! isempty (Sq) && ! isempty (Xq) && numel (Sq) != rows (Xq)) error (strcat ("CoxModel.%s: S must have one element for each row", ... " of X."), caller); endif endfunction ## Encode new predictor values with the fitted model's own encoding. function Xq = encode_new_data (this, Xq, caller) if (isa (Xq, 'table')) names = Xq.Properties.VariableNames; X_num = zeros (rows (Xq), numel (this.PredictorNames)); for j = 1:numel (this.PredictorNames) k = find (strcmp (names, this.PredictorNames{j}), 1); if (isempty (k)) error ("CoxModel.%s: X has no variable named '%s'.", caller, ... this.PredictorNames{j}); endif col = Xq.(this.PredictorNames{j}); if (isa (col, 'categorical') || iscell (col)) levels = this.catinfo_.levels{j}; [tf, ic] = ismember (cellstr (col), levels); if (! all (tf)) error (strcat ("CoxModel.%s: X holds a level of '%s' that the", ... " model was not fitted with."), caller, ... this.PredictorNames{j}); endif X_num(:,j) = ic; else X_num(:,j) = double (col(:)); endif endfor Xq = encode_categorical (X_num, this.catcols_, this.PredictorNames, ... this.catinfo_.levels, true); else if (! (isnumeric (Xq) && isreal (Xq))) error ("CoxModel.%s: X must be a real numeric matrix.", caller); endif if (columns (Xq) == numel (this.PredictorNames) && any (this.catcols_)) Xq = encode_categorical (Xq, this.catcols_, this.PredictorNames, ... this.catinfo_.levels, true); endif endif if (columns (Xq) != numel (this.b_)) error (strcat ("CoxModel.%s: X must have one column for each", ... " predictor."), caller); endif endfunction ## The baseline each row of a prediction is measured against. function base = prediction_baseline (this, Sq, given, caller) p = numel (this.b_); if (! isempty (given)) if (! (isnumeric (given) && isreal (given) && (isscalar (given) || numel (given) == p))) error (strcat ("CoxModel.%s: 'Baseline' must be a scalar or a row", ... " vector with one element per predictor column."), caller); endif if (isscalar (given)) base = repmat (given, 1, p); else base = given(:)'; endif return; endif if (isempty (this.Stratification) || isempty (Sq)) base = this.baserow_(1,:); else [tf, idx] = ismember (Sq(:), this.Stratification); if (! all (tf)) error (strcat ("CoxModel.%s: S holds a stratum the model was not", ... " fitted with."), caller); endif base = this.baserow_(idx,:); endif endfunction ## Interpolate a baseline survival curve, honouring the extrapolation rule. function sq = interp_survival (tg, S0, tq, method) ## A repeated time carries two values -- the curve steps there -- and the ## lower one is what the interval to its right starts from. [tu, iu] = unique (tg(:), 'last'); su = S0(iu); below = tq < tu(1); above = tq > tu(end); if (numel (tu) < 2) ## One event time leaves nothing to interpolate between. sq = NaN (size (tq)); sq(tq == tu(1)) = su(1); else sq = interp1 (tu, su, tq, 'linear'); endif switch (method) case 'nearest' sq(below) = S0(1); sq(above) = su(end); case 'linear' sq(below) = S0(1); if (any (above) && numel (tu) > 1) slope = (su(end) - su(end-1)) / (tu(end) - tu(end-1)); sq(above) = min (1, max (0, su(end) + slope * (tq(above) - tu(end)))); endif case 'next' sq(below) = S0(1); sq(above) = NaN; case 'previous' sq(below) = NaN; sq(above) = su(end); case 'none' sq(below) = NaN; sq(above) = NaN; endswitch endfunction ## Grambsch-Therneau test of the proportional hazards assumption, on event ## times replaced by their ranks. The scaled Schoenfeld residual of a ## covariate is regressed on time; a slope that is not zero is a hazard ratio ## that moves with time, which is what proportionality denies. ## ## Tied event times take their mid-rank, the average of the ranks they span. ## Only ties tell the rankings apart -- dense ranks, ordinal ranks and ## mid-ranks agree on distinct times -- and the mid-rank is the one that ## reproduces MATLAB, measured under both tie-breaking methods. function [pz, pg] = ph_assumption_test (stats, T) ev = ! any (isnan (stats.schres), 2); if (! any (ev)) pz = NaN (1, columns (stats.schres)); pg = NaN; return; endif s = stats.schres(ev,:); t = T(ev,end); d = sum (ev); V = stats.covb; [ut, ~, ic] = unique (t); [~, ord] = sort (t); r = zeros (numel (t), 1); r(ord) = 1:numel (t); g = zeros (numel (t), 1); for j = 1:numel (ut) m = ic == j; g(m) = mean (r(m)); endfor w = g - mean (g); varx = sum (w .^ 2); if (varx == 0) pz = NaN (1, columns (s)); pg = NaN; return; endif ## The scaled Schoenfeld residual is beta + d V s; the constant drops out of ## the centred regression, so d V s is what is correlated with time. u = (w' * (s * V * d))'; z = (u .^ 2) ./ (varx * d * diag (V)); pz = chi2cdf (z, 1, 'upper')'; pg = chi2cdf (u' * ((V * d * varx) \ u), columns (s), 'upper'); endfunction %!demo %! ## Fit a Cox proportional hazards model and read its coefficients %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = fitcox (X, T) %!demo %! ## The survival function at the model's baseline %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = fitcox (X, T); %! [s, t] = survival (mdl); %! [t, s] %!shared X, T, C, Tt, S, F, T2 %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! C = [0; 0; 1; 0; 0; 1; 0; 0; 1; 0]; %! Tt = [4; 4; 6; 6; 8; 8; 11; 11; 13; 13]; %! S = [1; 1; 1; 1; 1; 2; 2; 2; 2; 2]; %! F = [1; 2; 1; 1; 3; 1; 1; 2; 1; 1]; %! T2 = [0 4; 0 6; 2 8; 0 11; 3 13; 0 16; 5 18; 0 21; 7 25; 0 30]; ## The fitted coefficients are those of coxphfit %!test %! mdl = CoxModel (X, T); %! assert_equal (mdl.Coefficients.Beta, ... %! [-1.3886093196382836; 4.3814437183613322], 1e-8); %! assert_equal (mdl.Coefficients.SE, ... %! [0.52737766743917369; 1.8537400210498443], 1e-8); %! assert_equal (mdl.Coefficients.zStat, ... %! [-2.6330453589001133; 2.3635696853973904], 1e-8); %! assert_equal (mdl.Coefficients.pValue, ... %! [0.0084623045168834322; 0.018099822319697333], 1e-10); %!test %! mdl = CoxModel (X, T); %! assert_equal (mdl.LogLikelihood, -8.8069639381632356, 1e-10); %! assert_equal (mdl.LikelihoodRatioTestPValue, 0.0018409958426933715, 1e-10); %! assert_equal (mdl.NumPredictors, 2); ## Coefficient table shape and names %!test %! mdl = CoxModel (X, T); %! assert_equal (mdl.Coefficients.Properties.VariableNames, ... %! {'Beta', 'SE', 'zStat', 'pValue'}); %! assert_equal (mdl.Coefficients.Properties.RowNames, {'X1'; 'X2'}); %! assert_equal (size (mdl.Coefficients), [2, 4]); ## The covariance, standard errors, and baseline %!test %! mdl = CoxModel (X, T); %! assert_equal (mdl.CoefficientCovariance, ... %! [0.27812720411358371, -0.88932589928247008; ... %! -0.88932589928247008, 3.4363520656418767], 1e-8); %! assert_equal (mdl.StandardError, ... %! [0.52737766743917369; 1.8537400210498443], 1e-8); %! assert_equal (mdl.Baseline, [5.9, 0.5], 1e-12); ## The baseline cumulative hazard, as coxphfit returns it %!test %! mdl = CoxModel (X, T); %! assert_equal (size (mdl.Hazard), [11, 2]); %! assert_equal (mdl.Hazard(1,:), [4, 0]); %! assert_equal (mdl.Hazard(end,2), 39.969396549779539, 1e-6); ## Names and formula are consistent with one another %!test %! mdl = CoxModel (X, T); %! assert_equal (mdl.PredictorNames, {'X1', 'X2'}); %! assert_equal (mdl.ResponseName, 'y'); %! assert_equal (char (mdl.Formula), 'y ~ X1 + X2'); %!test %! mdl = CoxModel (X, T, 'PredictorNames', {'age', 'trt'}); %! assert_equal (mdl.PredictorNames, {'age', 'trt'}); %! assert_equal (mdl.ResponseName, 'y'); %! assert_equal (char (mdl.Formula), 'y ~ age + trt'); %! assert_equal (mdl.Coefficients.Properties.RowNames, {'age'; 'trt'}); ## Per-variable information %!test %! mdl = CoxModel (X, T); %! assert_equal (mdl.VariableInfo.Properties.VariableNames, ... %! {'Class', 'Range', 'InModel', 'IsCategorical'}); %! assert_equal (mdl.VariableInfo.Properties.RowNames, {'X1'; 'X2'; 'y'}); %! assert_equal (mdl.VariableInfo.InModel, [true; true; false]); %! assert_equal (mdl.VariableInfo.IsCategorical, [false; false; false]); ## The residual table carries every type coxphfit computes %!test %! mdl = CoxModel (X, T); %! assert_equal (mdl.Residuals.Properties.VariableNames, ... %! {'CoxSnell', 'Deviance', 'Martingale', 'Schoenfeld', ... %! 'ScaledSchoenfeld', 'Score', 'ScaledScore'}); %! assert_equal (size (mdl.Residuals), [10, 7]); %! assert_equal (mdl.Residuals.Martingale(1), 0.6260391348376092, 1e-8); %!test %! mdl = CoxModel (X, T); %! mdl = discardResiduals (mdl); %! assert_equal (isempty (mdl.Residuals), true); %! assert_equal (mdl.LogLikelihood, -8.8069639381632356, 1e-10); ## The proportional hazards assumption test %!test %! mdl = CoxModel (X, T); %! assert_equal (mdl.ProportionalHazardsPValue, ... %! [0.43865495271858179, 0.71413497767000234], 1e-8); %! assert_equal (mdl.ProportionalHazardsPValueGlobal, ... %! 0.53179255135825187, 1e-8); %!test %! mdl = CoxModel (X, T, 'Censoring', C); %! assert_equal (mdl.ProportionalHazardsPValue, ... %! [0.38377818764906124, 0.62418297490071406], 1e-8); %! assert_equal (mdl.ProportionalHazardsPValueGlobal, ... %! 0.56302705208766479, 1e-8); ## Tied event times take their mid-rank; only a tie tells the rankings apart %!test %! mdl = CoxModel (X, Tt, 'Censoring', C); %! assert_equal (mdl.ProportionalHazardsPValue, ... %! [0.21978449883707185, 0.47185588628217012], 1e-8); %! assert_equal (mdl.ProportionalHazardsPValueGlobal, ... %! 0.33816199611424813, 1e-8); %!test %! mdl = CoxModel (X, Tt, 'Censoring', C, 'TieBreakMethod', 'efron'); %! assert_equal (mdl.Coefficients.SE, ... %! [0.40807722610877606; 1.7349017938498392], 1e-8); %! assert_equal (mdl.ProportionalHazardsPValue, ... %! [0.1946475346918406, 0.44447162515215921], 1e-8); %! assert_equal (mdl.ProportionalHazardsPValueGlobal, ... %! 0.29473648771539396, 1e-8); ## Confidence intervals %!test %! mdl = CoxModel (X, T); %! assert_equal (coefci (mdl), ... %! [-2.422250554069806, -0.35496808520676093; ... %! 0.74818004040311514, 8.0147073963195492], 1e-8); %! assert_equal (coefci (mdl, 0.01), ... %! [-2.747044169465374, -0.030174469811192983; ... %! -0.39347414902021249, 9.1563615857428768], 1e-8); ## Sequential Wald tests %!test %! mdl = CoxModel ([X, [1;3;2;5;4;6;8;7;9;10]], T); %! tbl = linhyptest (mdl); %! assert_equal (size (tbl), [3, 2]); %! assert_equal (tbl.Predictor, {'Empty Model'; 'X1'; 'X1, X2'}); %! assert_equal (tbl.pValue, [0.11426498364159436; 0.066700738069398954; ... %! 0.040978941270458896], 1e-8); ## The last sequential test is the last coefficient's own p-value %!test %! mdl = CoxModel ([X, [1;3;2;5;4;6;8;7;9;10]], T); %! tbl = linhyptest (mdl); %! assert_equal (tbl.pValue(end), mdl.Coefficients.pValue(end), 1e-10); ## Hazard ratios %!test %! mdl = CoxModel (X, T); %! assert_equal (hazardratio (mdl, X(1,:)), 25.149914260347884, 1e-6); %! assert_equal (hazardratio (mdl, X(1,:), 'Baseline', 0), ... %! 0.062211299031685895, 1e-8); %!test %! mdl = CoxModel (X, T); %! hr = hazardratio (mdl, X); %! assert_equal (numel (hr), 10); %! assert_equal (hr(end), 0.030119684486042686, 1e-8); ## The survival function %!test %! mdl = CoxModel (X, T); %! [s, t] = survival (mdl); %! assert_equal (numel (s), 11); %! assert_equal (s(1), 1); %! assert_equal (s(2), 0.98524073172292947, 1e-8); %! assert_equal (t, mdl.Hazard(:,1)); %!test %! mdl = CoxModel (X, T); %! s = survival (mdl, X(1,:)); %! assert_equal (s(2), 0.6880038362205394, 1e-8); %! assert_equal (s(3), 0.37858862123606984, 1e-8); ## Interpolation is linear on the survival scale, then raised to the ratio %!test %! mdl = CoxModel (X, T); %! s = survival (mdl, 'Time', [4; 5; 6; 7]); %! assert_equal (s, [0.98524073172292947; 0.97367819309820003; ... %! 0.96211565447347058; 0.91995051472240608], 1e-8); %!test %! mdl = CoxModel (X, T); %! s = survival (mdl, X(1,:), 'Time', [5; 12; 20]); %! assert_equal (s, [0.51126892454645156; 9.5014819976515837e-06; ... %! 1.7030500478184053e-37], 1e-8); ## Outside the event times, the extrapolation rule decides %!test %! mdl = CoxModel (X, T); %! tq = [1; 2; 3; 31; 40]; %! assert_equal (survival (mdl, 'Time', tq), ... %! [1; 1; 1; 4.3803784471438163e-18; ... %! 4.3803784471438163e-18], 1e-8); %! assert_equal (survival (mdl, 'Time', tq, 'ExtrapolationMethod', 'none'), ... %! [NaN; NaN; NaN; NaN; NaN]); %!test %! mdl = CoxModel (X, T); %! tq = [1; 2; 3; 31; 40]; %! s = survival (mdl, 'Time', tq, 'ExtrapolationMethod', 'previous'); %! assert_equal (isnan (s(1:3)), [true; true; true]); %! assert_equal (s(4), 4.3803784471438163e-18, 1e-8); %! s = survival (mdl, 'Time', tq, 'ExtrapolationMethod', 'next'); %! assert_equal (s(1:3), [1; 1; 1]); %! assert_equal (isnan (s(4:5)), [true; true]); ## Stratification %!test %! mdl = CoxModel (X, T, 'Censoring', C, 'Stratification', S); %! assert_equal (mdl.Stratification, [1; 2]); %! assert_equal (size (mdl.Hazard), [9, 3]); %! assert_equal (mdl.Coefficients.Beta, ... %! [-0.7705046389; 3.1181961544], 1e-6); %! assert_equal (mdl.ProportionalHazardsPValue, ... %! [0.59926200661233664, 0.7801291832899343], 1e-6); %!test %! mdl = CoxModel (X, T, 'Censoring', C, 'Stratification', S); %! s = survival (mdl); %! assert_equal (iscell (s), true); %! assert_equal (numel (s), 2); %! assert_equal (numel (s{1}), 5); %! assert_equal (numel (s{2}), 4); %!test %! mdl = CoxModel (X, T, 'Censoring', C, 'Stratification', S); %! assert_equal (hazardratio (mdl, X(1,:), 1), 1.825643762907144, 1e-6); ## Unstratified curves are the columns of a matrix, one per row of X %!test %! mdl = CoxModel (X, T); %! [s, t] = survival (mdl, X(1:3,:)); %! assert_equal (size (s), [11, 3]); %! assert_equal (size (t), [11, 1]); %! assert_equal (s(2,1), 0.6880038362205394, 1e-8); %! assert_equal (s(2,2), 0.62879787425614297, 1e-8); %! assert_equal (s(3,3), 0.78484832904143043, 1e-8); ## A stratified curve follows the stratum of its own row of X %!test %! mdl = CoxModel (X, T, 'Censoring', C, 'Stratification', S); %! s = survival (mdl, X(1:2,:), [2; 1]); %! assert_equal (iscell (s), true); %! assert_equal (numel (s{1}), 4); %! assert_equal (numel (s{2}), 5); ## A baseline given to the method overrides the fitted one %!test %! mdl = CoxModel (X, T); %! assert_equal (hazardratio (mdl, X(1,:), 'Baseline', [1 1]), ... %! 0.0031195920528550632, 1e-8); ## Discarding the residuals leaves prediction intact %!test %! mdl = discardResiduals (CoxModel (X, T)); %! s = survival (mdl, X(1,:)); %! assert_equal (s(2), 0.6880038362205394, 1e-8); ## Frequency weights may be given as a row %!test %! mdl = CoxModel (X, T, 'Frequency', F'); %! assert_equal (mdl.Coefficients.Beta, ... %! [-1.4447610177189139; 4.6300103378728581], 1e-6); ## plotSurvival draws one stairstep line per curve %!test %! f = figure ('visible', 'off'); %! unwind_protect %! mdl = CoxModel (X, T, 'Censoring', C, 'Stratification', S); %! h = plotSurvival (mdl); %! assert_equal (numel (h), 2); %! assert_equal (all (ishghandle (h)), true); %! unwind_protect_cleanup %! close (f); %! end_unwind_protect ## One line per curve, a curve per row of X when the model is not stratified %!test %! f = figure ('visible', 'off'); %! unwind_protect %! mdl = CoxModel (X, T); %! assert_equal (numel (plotSurvival (mdl)), 1); %! assert_equal (numel (plotSurvival (mdl, X(1:3,:))), 3); %! unwind_protect_cleanup %! close (f); %! end_unwind_protect ## The counting process form %!test %! mdl = CoxModel (X, T2, 'Censoring', C); %! assert_equal (mdl.Coefficients.Beta, [-1.0104; 3.2523], 1e-4); ## Frequency weights %!test %! mdl = CoxModel (X, T, 'Frequency', F); %! assert_equal (mdl.Coefficients.Beta, ... %! [-1.4447610177189139; 4.6300103378728581], 1e-6); ## A starting value changes nothing but the path taken to the answer %!test %! mdl = CoxModel (X, T, 'Beta', [0.1; -0.1]); %! assert_equal (mdl.Coefficients.Beta, ... %! [-1.3886093196382836; 4.3814437183613322], 1e-6); ## An options structure is accepted %!test %! mdl = CoxModel (X, T, 'OptimizationOptions', statset ('fitcox')); %! assert_equal (mdl.LogLikelihood, -8.8069639381632356, 1e-10); ## Errors %!error CoxModel (1) %!error CoxModel ({1}, [1; 2]) %!error ... %! CoxModel (X, T, 'Censoring') %!error ... %! CoxModel (X, T, 'Ties', 'efron') %!error ... %! CoxModel (X, T, 'TieBreakMethod', 'exact') %!error ... %! CoxModel (X, T, 'PredictorNames', {'only_one'}) %!error ... %! coefci (CoxModel (X, T), 1.5) %!error hazardratio (CoxModel (X, T)) %!error ... %! survival (CoxModel (X, T), X(1,:), 'Baseline', 0) %!error ... %! hazardratio (CoxModel (X, T), X(1,:), 'Time', 5) %!error ... %! survival (CoxModel (X, T), 'Time', 5, 'ExtrapolationMethod', 'cubic') %!error ... %! survival (CoxModel (X, T, 'Stratification', S), X(1,:)) %!error ... %! CoxModel (X, T)(1) %!error ... %! CoxModel (X, T).nosuch statistics-release-2.1.2/inst/Regression/GeneralizedLinearMixedModel.m000066400000000000000000000544121527311104300261360ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftp {statistics} GeneralizedLinearMixedModel ## ## Generalized linear mixed-effects model fitted to data. ## ## A @code{GeneralizedLinearMixedModel} object represents a fitted generalized ## linear mixed-effects model: a generalized linear model whose linear predictor ## @code{X*beta + Z*b} includes normally distributed random effects ## @code{b ~ N(0, Psi)}. Objects are created with @code{fitglme}. ## ## The model is fitted by penalized quasi-likelihood. The fixed-effects ## estimates and their statistics are available through the @code{Coefficients} ## table, the covariance parameters through @code{covarianceParameters}, and ## predictions, residuals, and hypothesis tests through the @code{predict}, ## @code{residuals}, @code{anova}, @code{coefTest}, and @code{coefCI} methods. ## ## @seealso{fitglme, fitlme, GeneralizedLinearModel} ## @end deftp classdef GeneralizedLinearMixedModel < PredictiveModel properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {GeneralizedLinearMixedModel} {property} Distribution ## ## Response distribution ## ## A character vector naming the distribution of the response, one of ## @qcode{'binomial'}, @qcode{'poisson'} and @qcode{'normal'}. This ## property is read-only. ## ## @end deftp Distribution = ""; ## -*- texinfo -*- ## @deftp {GeneralizedLinearMixedModel} {property} Link ## ## Link function ## ## A character vector naming the link, one of @qcode{'logit'}, ## @qcode{'log'} and @qcode{'identity'}. This property is read-only. ## ## @end deftp Link = ""; ## -*- texinfo -*- ## @deftp {GeneralizedLinearMixedModel} {property} FitMethod ## ## Estimation method ## ## A character vector naming the method that fitted the model, one of ## @qcode{'MPL'}, @qcode{'REMPL'}, @qcode{'Laplace'} and ## @qcode{'ApproximateLaplace'}. This property is read-only. ## ## @end deftp FitMethod = ""; ## -*- texinfo -*- ## @deftp {GeneralizedLinearMixedModel} {property} Dispersion ## ## Dispersion parameter ## ## A positive scalar. It is estimated for a normal response and fixed at ## 1 for a binomial or Poisson one. This property is read-only. ## ## @end deftp Dispersion = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearMixedModel} {property} NumObservations ## ## Number of observations ## ## A positive integer counting the observations used for the fit. This ## property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearMixedModel} {property} NumCoefficients ## ## Number of fixed-effects coefficients ## ## A positive integer counting the fixed-effects coefficients of the ## model. This property is read-only. ## ## @end deftp NumCoefficients = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearMixedModel} {property} Coefficients ## ## Fixed-effects estimates and their statistics ## ## A table with one row per fixed-effects coefficient, its row names taken ## from @qcode{CoefficientNames}, and the variables @qcode{Estimate}, ## @qcode{SE}, @qcode{tStat}, @qcode{DF}, @qcode{pValue}, @qcode{Lower} ## and @qcode{Upper}. @qcode{Lower} and @qcode{Upper} bound a 95% ## confidence interval. This property is read-only. ## ## @end deftp Coefficients = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearMixedModel} {property} CoefficientCovariance ## ## Covariance of the fixed-effects estimates ## ## A square numeric matrix, one row and column per fixed-effects ## coefficient, holding the estimated covariance of the estimates in ## @qcode{Coefficients}. This property is read-only. ## ## @end deftp CoefficientCovariance = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearMixedModel} {property} CoefficientNames ## ## Names of the fixed-effects coefficients ## ## A cell array of character vectors with one name per fixed-effects ## coefficient. This property is read-only. ## ## @end deftp CoefficientNames = {}; ## -*- texinfo -*- ## @deftp {GeneralizedLinearMixedModel} {property} LogLikelihood ## ## Log-likelihood of the fitted model ## ## A scalar. It is a pseudo log-likelihood when @qcode{FitMethod} is ## @qcode{'MPL'} or @qcode{'REMPL'}, and a Laplace approximation to the ## log-likelihood otherwise. This property is read-only. ## ## @end deftp LogLikelihood = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearMixedModel} {property} ModelCriterion ## ## Information criteria ## ## A scalar structure with the fields @qcode{AIC}, @qcode{BIC}, ## @qcode{LogLikelihood} and @qcode{Deviance}. The parameter count behind ## @qcode{AIC} and @qcode{BIC} holds the fixed-effects coefficients, the ## covariance parameters and the dispersion. This property is read-only. ## ## @end deftp ModelCriterion = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearMixedModel} {property} DFE ## ## Residual degrees of freedom ## ## A nonnegative integer, @qcode{NumObservations} less ## @qcode{NumCoefficients}. This property is read-only. ## ## @end deftp DFE = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearMixedModel} {property} Formula ## ## Model formula ## ## A character vector describing the model. It is empty for a fit built ## from design matrices, which carries no formula. This property is ## read-only. ## ## @end deftp Formula = ""; ## -*- texinfo -*- ## @deftp {GeneralizedLinearMixedModel} {property} ResponseName ## ## Name of the response variable ## ## A character vector naming the response. It is empty for a fit built ## from design matrices. This property is read-only. ## ## @end deftp ResponseName = ""; endproperties properties (Access = private, Hidden) X_ = []; y_ = []; beta_ = []; covbeta_ = []; Psi_ = {}; b_ = []; mu_ = []; resraw_ = []; respear_ = []; Zx_ = []; qk_ = []; nlev_ = []; levels_ = {}; gidx_ = {}; GroupNames_ = {}; REPred_ = {}; distr_ = ""; link_ = ""; endproperties methods (Hidden) function disp (this) fprintf ("\n Generalized linear mixed-effects model fit by %s\n", ... this.FitMethod); fprintf (" Distribution: %s, Link: %s\n", ... this.Distribution, this.Link); if (! isempty (this.Formula)) fprintf ("\n Formula:\n %s\n", this.Formula); endif if (! isempty (this.Coefficients)) fprintf ("\n Fixed effects coefficients:\n\n"); disp (this.Coefficients); endif if (! isempty (this.Psi_)) fprintf ("\n Random effects covariance parameters:\n"); for k = 1:numel (this.Psi_) fprintf (" Group: %s (%d levels)\n", this.GroupNames_{k}, ... this.nlev_(k)); disp (this.Psi_{k}); endfor endif fprintf ("\n"); if (! isempty (this.NumObservations)) fprintf ("Number of observations: %d, Error DF: %d\n", ... this.NumObservations, this.DFE); endif if (! isempty (this.LogLikelihood)) fprintf ("Log-likelihood: %g\n", this.LogLikelihood); endif endfunction function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); switch (s.type) case "()" error (strcat ("GeneralizedLinearMixedModel: () indexing is not", ... " supported; use dot notation.")); case "{}" error (strcat ("GeneralizedLinearMixedModel: {} indexing is not", ... " supported; use dot notation.")); case "." if (! ischar (s.subs)) error ("GeneralizedLinearMixedModel.subsref: invalid index."); endif if (ismethod (this, s.subs)) [varargout{1:nargout}] = builtin ("subsref", this, [s, chain_s]); return; endif try out = this.(s.subs); catch error (strcat ("GeneralizedLinearMixedModel.subsref: ", ... "unknown property '%s'."), s.subs); end_try_catch endswitch if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearMixedModel} {@var{glme} =} GeneralizedLinearMixedModel (@var{info}) ## ## Construct from a fitted-model info struct. Used internally by ## @code{fitglme}; call that function rather than the constructor directly. ## ## @end deftypefn function this = GeneralizedLinearMixedModel (info) if (nargin == 0) return; endif n = info.n; p = info.p; this.X_ = info.X; this.y_ = info.y; this.beta_ = info.beta; this.covbeta_ = info.covbeta; this.Psi_ = info.Psi; this.b_ = info.b; this.mu_ = info.mu; this.resraw_ = info.resid_raw; this.respear_ = info.resid_pearson; this.Zx_ = info.Zx; this.qk_ = info.qk; this.nlev_ = info.nlev; this.levels_ = info.levels; this.gidx_ = info.gidx; this.GroupNames_ = info.GroupNames; this.REPred_ = info.REPred; this.distr_ = info.distr; this.link_ = info.link; this.Distribution = info.distr; this.Link = info.link; this.FitMethod = info.FitMethod; this.Dispersion = info.dispersion; this.NumObservations = n; this.NumCoefficients = p; this.CoefficientNames = info.CoefficientNames; this.CoefficientCovariance = info.covbeta; this.LogLikelihood = info.loglik; this.DFE = n - p; if (isfield (info, "Formula")), this.Formula = info.Formula; endif if (isfield (info, "ResponseName")) this.ResponseName = info.ResponseName; endif se = sqrt (diag (info.covbeta)); tstat = info.beta ./ se; dfe = n - p; pval = 2 * tcdf (-abs (tstat), dfe); tcrit = tinv (0.975, dfe); this.Coefficients = table (info.beta(:), se(:), tstat(:), ... repmat (dfe, p, 1), pval(:), info.beta(:) - tcrit*se(:), ... info.beta(:) + tcrit*se(:), ... "VariableNames", {"Estimate", "SE", "tStat", "DF", "pValue", ... "Lower", "Upper"}, ... "RowNames", info.CoefficientNames(:)); ncov = sum (arrayfun (@(q) q*(q+1)/2, info.qk)) + 1; kpar = p + ncov; dev = -2 * info.loglik; mc.AIC = dev + 2 * kpar; mc.BIC = dev + kpar * log (n); mc.LogLikelihood = info.loglik; mc.Deviance = dev; this.ModelCriterion = mc; endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearMixedModel} {[@var{beta}, @var{names}] =} fixedEffects (@var{glme}) ## Return the fixed-effects coefficients and, optionally, their names. ## @end deftypefn function [beta, names] = fixedEffects (this) beta = this.beta_; names = this.CoefficientNames; endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearMixedModel} {@var{b} =} randomEffects (@var{glme}) ## Return the estimated random-effects (the conditional modes). ## @end deftypefn function b = randomEffects (this) b = this.b_; endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearMixedModel} {[@var{psi}, @var{disp}] =} covarianceParameters (@var{glme}) ## Return the random-effects covariance matrices and the dispersion. ## @end deftypefn function [psi, dispn] = covarianceParameters (this) psi = this.Psi_; dispn = this.Dispersion; endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearMixedModel} {@var{yf} =} fitted (@var{glme}) ## Return the fitted mean response (conditional on the random effects). ## @end deftypefn function yf = fitted (this) yf = this.mu_; endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearMixedModel} {@var{r} =} residuals (@var{glme}) ## @deftypefnx {GeneralizedLinearMixedModel} {@var{r} =} residuals (@var{glme}, @qcode{"ResidualType"}, @var{type}) ## Return @qcode{"Raw"} (default) or @qcode{"Pearson"} residuals. ## @end deftypefn function r = residuals (this, varargin) type = "raw"; if (numel (varargin) >= 2 && strcmpi (varargin{1}, "ResidualType")) type = lower (varargin{2}); endif switch (type) case "raw" r = this.resraw_; case "pearson" r = this.respear_; otherwise error (strcat ("GeneralizedLinearMixedModel: unknown", ... " ResidualType '%s'."), type); endswitch endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearMixedModel} {@var{ypred} =} predict (@var{glme}, @var{Xnew}, @var{Znew}, @var{Gnew}) ## Predict the mean response at new data. With @qcode{"Conditional"} true ## (default) the random effects of known grouping levels are added; ## unknown levels fall back to the marginal (fixed-effects) prediction. ## @end deftypefn function ypred = predict (this, Xnew, Znew, Gnew, varargin) if (nargin < 4), Znew = []; Gnew = []; endif cond = true; for i = 1:2:numel (varargin) if (strcmpi (varargin{i}, "Conditional")) cond = logical (varargin{i+1}); endif endfor eta = Xnew * this.beta_; if (cond && ! isempty (Znew) && ! isempty (Gnew)) Gnew = Gnew(:); q = this.qk_(1); lev = this.levels_{1}; for i = 1:rows (Xnew) li = find (lev == Gnew(i), 1); if (! isempty (li)) eta(i) += Znew(i, :) * this.b_((li-1)*q + (1:q)); endif endfor endif ypred = link_inv (eta, this.link_); endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearMixedModel} {@var{tbl} =} anova (@var{glme}) ## Analysis-of-deviance table of F-tests for the fixed-effects terms, using ## residual denominator degrees of freedom. ## @end deftypefn function tbl = anova (this) p = this.NumCoefficients; se = sqrt (diag (this.covbeta_)); Fstat = (this.beta_ ./ se) .^ 2; DF1 = ones (p, 1); DF2 = repmat (this.DFE, p, 1); pValue = fcdf (Fstat, DF1, DF2, 'upper'); tbl = table (Fstat(:), DF1(:), DF2(:), pValue(:), ... "VariableNames", {"FStat", "DF1", "DF2", "pValue"}, ... "RowNames", this.CoefficientNames(:)); endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearMixedModel} {[@var{p}, @var{F}, @var{df1}, @var{df2}] =} coefTest (@var{glme}, @var{H}) ## F-test of the linear hypothesis @code{H*beta = 0}. ## @end deftypefn function [pval, F, df1, df2] = coefTest (this, H) p = this.NumCoefficients; if (nargin < 2) H = [zeros(p-1, 1), eye(p-1)]; endif Hb = H * this.beta_; df1 = rank (H); df2 = this.DFE; F = (Hb' * ((H * this.covbeta_ * H') \ Hb)) / df1; pval = fcdf (F, df1, df2, 'upper'); endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearMixedModel} {@var{ci} =} coefCI (@var{glme}, @var{alpha}) ## Confidence intervals for the fixed-effects coefficients. ## @end deftypefn function ci = coefCI (this, alpha) if (nargin < 2), alpha = 0.05; endif se = sqrt (diag (this.covbeta_)); tcrit = tinv (1 - alpha/2, this.DFE); ci = [this.beta_ - tcrit * se, this.beta_ + tcrit * se]; endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearMixedModel} {@var{D} =} designMatrix (@var{glme}, @var{type}) ## Return the fixed (@qcode{"Fixed"}, default) design matrix as a full ## matrix, or the random (@qcode{"Random"}) design matrix as a sparse one. ## @end deftypefn function D = designMatrix (this, type) if (nargin < 2), type = "Fixed"; endif switch (lower (type)) case "fixed" D = this.X_; case "random" D = this.Zx_; otherwise error (strcat ("GeneralizedLinearMixedModel: type must be", ... " 'Fixed' or 'Random'.")); endswitch endfunction endmethods endclassdef ## Inverse link (canonical), shared with the fitting engine's convention. function mu = link_inv (eta, link) switch (lower (link)) case "logit" mu = 1 ./ (1 + exp (-eta)); case "log" mu = exp (eta); case "identity" mu = eta; otherwise error ("GeneralizedLinearMixedModel: unsupported link '%s'.", link); endswitch endfunction ## Shared MATLAB-verified fixture (fitglme R2026a): poisson random intercept. %!shared glme, tbl %! xL = [0.032760004 0.70410822 -0.8646718 -0.28869454 0.51276678 -1.4975462 ... %! -1.4527871 -0.80013541 -1.644209 1.5137701 0.72905543 0.20880758 1.0856145 ... %! 0.62862577 -0.87409978 1.9178276 0.09748204 0.50697633 1.0247569 ... %! -0.92789896 -0.88921018 -0.98322849 -0.031378913 0.86875961 -0.91481141 ... %! 0.034324163 -0.25025257 -1.0575644 -0.86131607 -0.35355444 0.82950729 ... %! -0.36874363 0.061580868 0.55803564 -0.1763803 1.0482413 1.0137831 ... %! -0.94876976 -0.010703972 -0.35149845 -1.6828735 -1.0493301]'; %! yPois = [3 3 1 1 1 1 1 2 1 5 2 0 5 0 1 5 0 2 0 0 1 0 0 5 3 0 1 0 0 1 1 1 2 2 ... %! 1 1 4 0 1 0 0 1]'; %! g = [1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 ... %! 6 1 2 3 4 5 6]'; %! tbl = table (yPois, xL, g); %! glme = fitglme (tbl, "yPois ~ xL + (1 | g)", "Distribution", "poisson", ... %! "FitMethod", "REMPL"); %!test # object type and basic properties %! assert_equal (isa (glme, "GeneralizedLinearMixedModel"), true); %! assert_equal (glme.NumObservations, 42); %! assert_equal (glme.NumCoefficients, 2); %! assert_equal (glme.DFE, 40); %! assert_equal (glme.Dispersion, 1); %!test # Coefficients table %! C = glme.Coefficients; %! assert_equal (C.Estimate, [0.23092; 0.67809], 1e-3); %! assert_equal (C.SE, [0.15395; 0.15220], 1e-3); %! assert_equal (C.DF, [40; 40]); %! assert_equal (C.tStat, C.Estimate ./ C.SE, 1e-10); %!test # effect extraction and covariance parameters %! [beta, names] = fixedEffects (glme); %! assert_equal (beta, glme.Coefficients.Estimate, 1e-12); %! assert_equal (names(:), {"(Intercept)"; "xL"}); %! b = randomEffects (glme); %! assert_equal (numel (b), 6); %! [psi, dispn] = covarianceParameters (glme); %! assert_equal (psi{1}, 0.015918, 1e-3); %! assert_equal (dispn, 1); %!test # anova F-tests: F = tStat^2 with residual DF %! a = anova (glme); %! assert_equal (a.FStat, (glme.Coefficients.tStat) .^ 2, 1e-8); %! assert_equal (a.DF2, [40; 40]); %!test # coefTest and coefCI %! [p, F, df1, df2] = coefTest (glme, [0 1]); %! assert_equal (F, glme.Coefficients.tStat(2) ^ 2, 1e-6); %! assert_equal (df1, 1); assert_equal (df2, 40); %! ci = coefCI (glme); %! assert_equal (ci(:,1), glme.Coefficients.Lower, 1e-12); %!test # fitted values are positive counts (log link) and residuals sum sensibly %! mu = fitted (glme); %! assert_equal (all (mu > 0, 'all'), true); %! assert_equal (residuals (glme), tbl.yPois - mu, 1e-12); %! assert_equal (residuals (glme, "ResidualType", "Pearson"), ... %! (tbl.yPois - mu) ./ sqrt (mu), 1e-10); %!test # predict: conditional (known group) and marginal (unseen group) %! ym = predict (glme, [1 0.5], [], [], "Conditional", false); %! yc = predict (glme, [1 0.5], 1, 1); %! yu = predict (glme, [1 0.5], 1, 99); # unseen group -> marginal %! assert_equal (ym, exp (glme.Coefficients.Estimate' * [1; 0.5]), 1e-10); %! assert_equal (yu, ym, 1e-12); %!test # designMatrix and ModelCriterion %! assert_equal (size (designMatrix (glme, "Fixed")), [42, 2]); %! assert_equal (size (designMatrix (glme, "Random")), [42, 6]); %! assert_equal (issparse (designMatrix (glme, "Fixed")), false); %! assert_equal (issparse (designMatrix (glme, "Random")), true); %! assert_equal (glme.ModelCriterion.Deviance, -2 * glme.LogLikelihood, 1e-10); %!test %! ## A normal response with a dispersion far from one, and p-values below %! ## the resolution of 1 - tcdf; values from MATLAB R2024a %! x = (1:30)'; %! g = repmat ((1:5)', 6, 1); %! T = table (x, g, 2 * x + g + 0.01 * sin (x), ... %! 'VariableNames', {'x', 'g', 'y'}); %! m = fitglme (T, 'y ~ x + (1|g)'); %! assert_equal (m.Coefficients.pValue, ... %! [5.4976892695175e-05; 1.12635133546576e-95], -1e-3); %!test %! ## The Laplace approximation is exact for a normal response %! x = (1:30)'; %! g = repmat ((1:5)', 6, 1); %! T = table (x, g, 2 * x + g + 0.01 * sin (x), ... %! 'VariableNames', {'x', 'g', 'y'}); %! m = fitglme (T, 'y ~ x + (1|g)', 'FitMethod', 'Laplace'); %! assert_equal (m.LogLikelihood, fitlme (T, 'y ~ x + (1|g)').LogLikelihood, ... %! -1e-8); %!test %! ## Below the resolution of 1 - fcdf, values from MATLAB R2024a %! u = (1:30)'; %! g = repmat ((1:5)', 6, 1); %! T = table (u, g, 2 * u + g + 0.01 * sin (u), ... %! 'VariableNames', {'x', 'g', 'y'}); %! m = fitglme (T, 'y ~ x + (1|g)'); %! assert_equal (coefTest (m), 1.12635133546576e-95, -1e-3); %!test %! u = (1:30)'; %! g = repmat ((1:5)', 6, 1); %! T = table (u, g, 2 * u + g + 0.01 * sin (u), ... %! 'VariableNames', {'x', 'g', 'y'}); %! A = anova (fitglme (T, 'y ~ x + (1|g)')); %! assert_equal (A.pValue(2), 1.12635133546576e-95, -1e-3); ## Error handling %!error residuals (glme, "ResidualType", "xxx") %!error designMatrix (glme, "bogus") %!error glme(1) statistics-release-2.1.2/inst/Regression/GeneralizedLinearModel.m000066400000000000000000003250671527311104300251560ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftp {statistics} {} GeneralizedLinearModel ## ## Generalized linear regression model class. ## ## A @code{GeneralizedLinearModel} object encapsulates a generalized linear ## model (GLM) of a response on one or more predictors, fitted by iteratively ## reweighted least squares. It is the GLM counterpart of @code{LinearModel} ## and is normally created with the @code{fitglm} function. ## ## The response is modelled through a distribution from the exponential family ## (@qcode{'normal'}, @qcode{'binomial'}, @qcode{'poisson'}, @qcode{'gamma'}, or ## @qcode{'inverse gaussian'}) and a link function @math{g} relating the mean ## @math{mu} to the linear predictor @math{eta = g (mu)}. ## ## The most useful properties are @code{Coefficients} (a table of estimates, ## standard errors, @math{t}-statistics and p-values), @code{Deviance}, ## @code{Dispersion}, @code{Residuals}, @code{Fitted}, @code{Diagnostics}, ## @code{Distribution}, and @code{Link}. @code{ObservationInfo} records which ## rows were weighted, excluded, or missing, and @code{Variables} holds the data ## the model was built from. For a binomial response given as an ## @math{n}-by-@math{2} matrix of successes and trials, @code{Variables} holds ## the @strong{success count} alone, that being the response the model fits; ## MATLAB stores both columns there. Fitted models support the ## @code{predict} and @code{feval} methods for prediction. ## ## @code{Fitted}, @code{Residuals}, @code{Diagnostics}, and ## @code{ObservationInfo} have one row per @emph{input} observation, not per ## fitted observation. Rows that were excluded with the @qcode{'Exclude'} pair ## still carry a fitted value and a residual, since the model can be evaluated ## there; rows dropped because a variable was missing carry @code{NaN}. ## ## For a binomial response carrying a number of trials @math{N} -- given ## either by the @qcode{'BinomialSize'} pair or as the second column of a ## two-column response -- the response is the @emph{number of successes}, as ## @code{fitglm} documents. @code{Fitted.Response} is then the fitted count ## @math{N p} and @code{Residuals.Raw} is on that same count scale, while ## @code{Fitted.Probability} carries @math{p} itself. @code{predict} returns ## the probability, never a count: a trial count belongs to an observation, ## and new predictor values do not carry one. ## ## A categorical predictor expands to indicator columns, one per level bar the ## reference level, which the intercept carries. When the model has no ## intercept, the @emph{first} categorical predictor is given an indicator for ## every one of its levels instead, so that its coefficients are the group ## means; any further categorical predictor stays reference coded, which keeps ## the design full rank. This differs from MATLAB, which omits the reference ## level whether or not an intercept is present and so cannot fit the reference ## group at all -- for a three-level grouping variable @code{g}, MATLAB fits ## @code{y ~ g - 1} with two coefficients, predicts exactly 0 for every ## observation in the omitted group, and reports a negative @math{R^2}. This ## implementation returns three coefficients, one per group. ## ## @seealso{fitglm, LinearModel, glmfit, glmval} ## @end deftp classdef GeneralizedLinearModel < PredictiveModel properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} Coefficients ## ## Coefficient values ## ## A table with one row per coefficient and four columns: ## @itemize ## @item @code{Estimate} - estimated coefficient value ## @item @code{SE} - standard error of the estimate ## @item @code{tStat} - the estimate divided by its standard error ## @item @code{pValue} - p-value of that statistic ## @end itemize ## ## The statistic is referred to the normal distribution where the dispersion ## is fixed, as it is for the binomial and Poisson families, and to a ## @math{t}-distribution on @code{DFE} degrees of freedom where it is ## estimated. Coefficients dropped as rank deficient have ## @code{Estimate = 0}, @code{SE = 0}, and @code{NaN} for both statistics. ## Row names are the coefficient names. ## ## This property is read-only. ## ## @end deftp Coefficients = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} CoefficientNames ## ## Names of the coefficients ## ## A cell array of character vectors, one per coefficient, in the order the ## coefficients appear. The intercept is @qcode{'(Intercept)'}, an ## interaction joins its factors with a colon, and a categorical predictor ## contributes one name per indicator, spelled ## @qcode{@var{name}_@var{level}}, so these are not the term names. ## ## This property is read-only. ## ## @end deftp CoefficientNames = {}; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} CoefficientCovariance ## ## Covariance matrix of the coefficient estimates ## ## A square matrix with one row and column per coefficient, whose diagonal ## is the square of @code{Coefficients.SE}. It is scaled by ## @code{Dispersion}, so it is the covariance under the estimated dispersion ## wherever one was estimated. ## ## This property is read-only. ## ## @end deftp CoefficientCovariance = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} NumCoefficients ## ## Number of coefficients ## ## A positive integer counting every coefficient the model carries, those ## dropped as rank deficient included. A categorical predictor with ## @math{L} levels contributes @math{L - 1} of them. ## ## This property is read-only. ## ## @end deftp NumCoefficients = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} NumEstimatedCoefficients ## ## Number of coefficients actually estimated ## ## A positive integer counting the coefficients that carry a degree of ## freedom, which is @code{NumCoefficients} less however many were dropped ## as rank deficient. It is the number the degrees of freedom and the ## information criteria are computed from. ## ## This property is read-only. ## ## @end deftp NumEstimatedCoefficients = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} NumPredictors ## ## Number of predictor variables ## ## A nonnegative integer counting the predictors the model was given, ## whether or not each appears in a term. It counts variables, so a ## categorical predictor counts once however many indicators it expands to. ## ## This property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} NumObservations ## ## Number of observations used in the fit ## ## A positive integer giving the number of observations the fit actually ## used. Rows holding a missing value and rows named by the ## @qcode{'Exclude'} name-value argument are not counted. ## ## This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} Deviance ## ## Deviance of the fitted model ## ## A nonnegative scalar, twice the difference between the log-likelihood of ## the saturated model and that of this one. It is the generalized linear ## model's counterpart of the residual sum of squares, and it is what a ## nested-model test compares. ## ## This property is read-only. ## ## @end deftp Deviance = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} DFE ## ## Error degrees of freedom ## ## A nonnegative integer, @code{NumObservations} less ## @code{NumEstimatedCoefficients}. ## ## This property is read-only. ## ## @end deftp DFE = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} Dispersion ## ## Dispersion parameter ## ## A positive scalar. It is estimated from the Pearson statistic for the ## normal, gamma, and inverse Gaussian families, and fixed at @math{1} for ## the binomial and Poisson families unless @qcode{'DispersionFlag'} asked ## otherwise. @code{CoefficientCovariance} and the standard errors are ## scaled by it. ## ## This property is read-only. ## ## @end deftp Dispersion = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} DispersionEstimated ## ## Whether the dispersion was estimated ## ## A logical scalar, true where @code{Dispersion} was estimated from the ## data and false where it was held at @math{1}. It decides whether a ## coefficient's statistic is referred to the normal or the @math{t} ## distribution. ## ## This property is read-only. ## ## @end deftp DispersionEstimated = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} Distribution ## ## The response distribution ## ## A structure with three fields: @code{Name}, the distribution's name; ## @code{DevianceFunction}, a function handle giving the deviance ## contribution of an observation from its response and mean; and ## @code{VarianceFunction}, a function handle giving the variance of an ## observation as a function of its mean. ## ## This property is read-only. ## ## @end deftp Distribution = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} Link ## ## The link function ## ## A structure with four fields: @code{Name}, the link's name; @code{Link}, ## a function handle mapping the mean to the linear predictor; ## @code{Derivative}, a handle giving that map's derivative; and ## @code{Inverse}, a handle mapping the linear predictor back to the mean. ## ## This property is read-only. ## ## @end deftp Link = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} Fitted ## ## Fitted values ## ## A table with one row per input observation and two columns, ## @code{Response} on the scale of the response and @code{LinearPredictor} ## on the scale of the link. A binomial fit gains a third, ## @code{Probability}, since its ## @code{Response} is a count of successes while the fit works in the ## proportion. Rows kept out of the fit by @qcode{'Exclude'} still carry a ## prediction; rows dropped as missing carry @code{NaN}. ## ## This property is read-only. ## ## @end deftp Fitted = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} Residuals ## ## Residuals for the fitted model ## ## A table with one row per input observation and five columns: ## @itemize ## @item @code{Raw} - observed minus fitted, on the response scale ## @item @code{LinearPredictor} - the working residual, on the link scale ## @item @code{Pearson} - raw residuals divided by the estimated standard ## deviation of the observation ## @item @code{Anscombe} - the transform that makes the residuals as nearly ## normal as the family allows ## @item @code{Deviance} - the signed square root of each observation's ## contribution to @code{Deviance} ## @end itemize ## ## Rows not used in the fit contain @code{NaN}. ## ## This property is read-only. ## ## @end deftp Residuals = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} Diagnostics ## ## Per-observation diagnostics ## ## A table with one row per input observation and three columns: ## @code{Leverage}, the diagonal of the weighted hat matrix; ## @code{CooksDistance}, the influence of the observation on every fitted ## value at once; and @code{HatMatrix}, that observation's row of the hat ## matrix. Rows not used in the fit contain @code{NaN}. ## ## This property is read-only. ## ## @end deftp Diagnostics = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} LogLikelihood ## ## Log-likelihood of the fitted model ## ## A scalar, the log-likelihood of the observations under the fitted ## coefficients and the family's own density. It is what the information ## criteria and the likelihood-ratio @math{R^2} are computed from. ## ## This property is read-only. ## ## @end deftp LogLikelihood = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} ModelCriterion ## ## Information criteria ## ## A structure with four fields, @code{AIC}, @code{AICc}, @code{BIC}, and ## @code{CAIC}, each penalising @code{LogLikelihood} by a different function ## of the coefficient count and the sample size. @code{AICc} is @code{Inf} ## where the correction's denominator is not positive. ## ## This property is read-only. ## ## @end deftp ModelCriterion = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} Rsquared ## ## Measures of fit ## ## A structure with five fields: @code{Ordinary} and @code{Adjusted}, ## computed from the sums of squares on the response scale; @code{Deviance}, ## one less the ratio of the model's deviance to the null model's; ## @code{LLR}, the same ratio taken over log-likelihoods; and ## @code{AdjGeneralized}, the Nagelkerke measure, which rescales the ## generalized @math{R^2} by its own attainable maximum so that it can reach ## one. ## ## This property is read-only. ## ## @end deftp Rsquared = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} SSE ## ## Error sum of squares ## ## A nonnegative scalar, the weighted sum of squared raw residuals on the ## response scale. For a generalized linear model this is a descriptive ## quantity rather than the fitted criterion, which is @code{Deviance}. ## ## This property is read-only. ## ## @end deftp SSE = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} SSR ## ## Regression sum of squares ## ## A nonnegative scalar, the weighted sum of squared differences between the ## fitted values and the weighted mean of the response, on the response ## scale. ## ## This property is read-only. ## ## @end deftp SSR = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} SST ## ## Total sum of squares ## ## A nonnegative scalar, the weighted sum of squared differences between the ## response and its weighted mean. Unlike a linear model, a generalized ## linear model does not in general satisfy @code{SST = SSE + SSR}. ## ## This property is read-only. ## ## @end deftp SST = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} Offset ## ## Offset added to the linear predictor ## ## A column vector with one element per input observation, added to the ## linear predictor with a coefficient fixed at one, so that it shifts the ## fit without being estimated. It is all zeros where no @qcode{'Offset'} ## was given. ## ## This property is read-only. ## ## @end deftp Offset = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} LikelihoodPenalty ## ## Penalty applied to the likelihood ## ## A character vector, always @qcode{'none'}: no penalized-likelihood ## fitting is offered, so the coefficients are always the plain ## maximum-likelihood ones. ## ## This property is read-only. ## ## @end deftp LikelihoodPenalty = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} ResponseName ## ## Name of the response variable ## ## A character vector. It is taken from the table column, the ## @qcode{'ResponseVar'} or @qcode{'VarNames'} argument, or the formula, and ## defaults to @qcode{'y'} for a predictor matrix. ## ## This property is read-only. ## ## @end deftp ResponseName = 'y'; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} PredictorNames ## ## Names of the predictor variables ## ## A cell array of character vectors naming the predictors in the order the ## data lists them. A predictor matrix gives them the names @qcode{'x1'}, ## @qcode{'x2'}, and so on. ## ## This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} VariableNames ## ## Names of all the variables ## ## A cell array of character vectors naming every variable the model was ## given, the response included, in the order the data lists them. ## ## This property is read-only. ## ## @end deftp VariableNames = {}; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} NumVariables ## ## Number of variables ## ## A positive integer, the number of elements of @code{VariableNames}: the ## predictors and the response together, whether or not each appears in a ## term. ## ## This property is read-only. ## ## @end deftp NumVariables = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} VariableInfo ## ## Per-variable information ## ## A table with one row per variable, named by it, and four columns: ## @code{Class}, the class of the data column; @code{Range}, its two-element ## range or, for a categorical, the list of its levels; @code{InModel}, true ## where the variable appears in a term; and @code{IsCategorical}, true ## where it was coded as indicators. ## ## This property is read-only. ## ## @end deftp VariableInfo = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} Variables ## ## The data the model was built from ## ## A table holding every variable, the response included, with one row per ## input observation. A model fitted from a predictor matrix gets a table ## assembled from it, so this property is a table either way. ## ## This property is read-only. ## ## @end deftp Variables = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} ObservationInfo ## ## Per-observation status ## ## A table with one row per input observation and four columns: ## @code{Weights}, the weight it was given; @code{Excluded}, true where ## @qcode{'Exclude'} named it; @code{Missing}, true where its data are ## incomplete; and @code{Subset}, true where it was used in the fit, which ## is neither excluded nor missing. ## ## This property is read-only. ## ## @end deftp ObservationInfo = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} ObservationNames ## ## Names of the observations ## ## A cell array of character vectors, one per input observation, and empty ## unless the data carried row names. ## ## This property is read-only. ## ## @end deftp ObservationNames = {}; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} Formula ## ## The model formula ## ## A @code{LinearFormula} object describing the fitted model, with ## properties including @code{ResponseName}, @code{LinearPredictor}, ## @code{PredictorNames}, ## @code{TermNames}, @code{Terms}, @code{HasIntercept}, and @code{Link}. ## Its terms are expressed over the model's variables, so a categorical ## predictor contributes one term however many indicators it expands to. ## ## This property is read-only. ## ## @end deftp Formula = []; ## -*- texinfo -*- ## @deftp {GeneralizedLinearModel} {property} Steps ## ## Stepwise fitting information ## ## A structure recording the term-selection trace, populated whenever the ## model was fit by @code{stepwiseglm} and @code{[]} otherwise. It has ## seven fields: ## ## @multitable @columnfractions 0.15 0.8 ## @headitem Field @tab Contents ## @item @code{Start} @tab a @code{LinearFormula} for the model the search ## started from. ## @item @code{Lower} @tab a @code{LinearFormula} for the smallest model ## considered; its terms are never removed. ## @item @code{Upper} @tab a @code{LinearFormula} for the largest model ## considered. ## @item @code{Criterion} @tab the selection criterion, such as ## @qcode{'deviance_chi2'}. ## @item @code{PEnter} @tab the threshold a term must beat to enter, empty ## unless one was given. ## @item @code{PRemove} @tab the threshold above which a term leaves, empty ## unless one was given. ## @item @code{History} @tab a table with one row per step. ## @end multitable ## ## @code{History} always carries @code{Action} (@qcode{'Start'}, ## @qcode{'Add'}, or @qcode{'Remove'}), @code{TermName}, @code{Terms} (the ## terms matrix after the step, over the model's variables), @code{DF} (the ## coefficient count after the step), and @code{delDF} (the change in it, ## negative for a removal). The remaining columns follow the criterion: ## @code{Deviance}, then ## @code{Chi2Stat} or @code{FStat}, then @code{PValue} under ## @qcode{'Deviance'}; ## @code{FStat} and @code{pValue} under @qcode{'sse'}; and a single column ## named ## @code{AIC} or @code{BIC} holding the criterion's value after the step ## otherwise. The first row is the starting model, named by its right-hand ## side. ## ## This property is read-only. ## ## @end deftp Steps = []; endproperties properties (Access = private, Hidden) b_ = []; # coefficient vector aligned with the design columns stats_ = []; # glmfit stats struct (for prediction CIs) distr_ = ''; # distribution name linkarg_ = []; # link specification for glmfit/glmval binomsize_ = []; # BinomialSize (trials) for the binomial family terms_ = []; # terms matrix (for rebuilding the design in predict) catinfo_ = []; # categorical level info (names + levels) encnames_ = {}; # encoded predictor column names prednames_ = {}; # raw predictor names nulldev_ = []; # deviance of the intercept-only model llnull_ = []; # log-likelihood of the intercept-only model design_ = []; # fitted design matrix (for diagnostics/plots) subset_ = []; # logical mask of the rows used in the fit formulastr_ = ''; # rendered formula, e.g. 'log(y) ~ 1 + x1 + x2' xmeans_ = []; # mean of each raw predictor (for slice/effect plots) endproperties methods (Hidden) ## Custom display of the model summary. function disp (this) fprintf ("\n Generalized linear regression model:\n"); if (! isempty (this.formulastr_)) fprintf (" %s\n", this.formulastr_); endif if (! isempty (this.Distribution) && isstruct (this.Distribution)) fprintf (" Distribution = %s, Link = %s\n", ... this.Distribution.Name, this.Link.Name); endif if (! isempty (this.Coefficients)) fprintf ("\n Coefficients:\n\n"); disp (this.Coefficients); endif fprintf ("\n"); if (! isempty (this.NumObservations) && ! isempty (this.DFE)) fprintf (strcat ("Number of observations: %d,", ... " Error degrees of freedom: %d\n"), ... this.NumObservations, this.DFE); endif if (! isempty (this.Dispersion)) fprintf ("Dispersion: %g\n", this.Dispersion); endif if (! isempty (this.Deviance)) fprintf ("Deviance: %g\n", this.Deviance); endif if (! isempty (this.NumCoefficients) && this.NumCoefficients > 1) df1 = this.NumCoefficients - 1; drop = this.nulldev_ - this.Deviance; if (this.DispersionEstimated) Fstat = (drop / df1) / this.Dispersion; pval = fcdf (Fstat, df1, this.DFE, 'upper'); fprintf ("F-statistic vs. constant model: %g, p-value = %g\n", ... Fstat, pval); else pval = chi2cdf (drop, df1, 'upper'); fprintf (strcat ("Chi^2-statistic vs. constant model: %g,", ... " p-value = %g\n"), drop, pval); endif endif endfunction ## Class specific subscripted reference. function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); switch (s.type) case '()' error (strcat ("GeneralizedLinearModel: () indexing is not", ... " supported. Use dot notation for properties.")); case '{}' error (strcat ("GeneralizedLinearModel: {} indexing is not", ... " supported. Use dot notation for properties.")); case '.' if (! ischar (s.subs)) error (strcat ("GeneralizedLinearModel.subsref: property name", ... " must be a character vector.")); endif if (ismethod (this, s.subs)) [varargout{1:nargout}] = builtin ('subsref', this, [s, chain_s]); return; endif try out = this.(s.subs); catch error (strcat ("GeneralizedLinearModel.subsref: unknown", ... " property '%s'."), s.subs); end_try_catch endswitch if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction ## Attach a stepwise-selection history structure. Used by @code{stepwiseglm} ## to record the term-selection trace on the returned object; not intended ## for direct use. function this = setSteps (this, steps) this.Steps = steps; endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearModel} {@var{mdl} =} GeneralizedLinearModel (@var{data}, @var{resp}, @var{modelspec}) ## @deftypefnx {GeneralizedLinearModel} {@var{mdl} =} GeneralizedLinearModel (@dots{}, @var{Name}, @var{Value}) ## ## Fit a generalized linear model. Prefer the @code{fitglm} function, which ## documents the accepted inputs and @var{Name}/@var{Value} pairs. ## ## @end deftypefn function this = GeneralizedLinearModel (data, resp, modelspec, varargin) if (nargin == 0) return; # empty object endif if (nargin < 3) error ("GeneralizedLinearModel: DATA, RESP, and MODELSPEC are required."); endif opts = glm_parse_nv (varargin); is_formula = ischar (modelspec) && any (modelspec == '~'); tbl = []; # defined only for table input; passed through helpers ## ------------------------------------------------------------------ ## ## Intake: resolve predictors, response, names, and the response vector. ## ------------------------------------------------------------------ ## ## Trial counts taken from a two-column binomial response, empty for ## every other input form. Set here so the table branch below and the ## fitting code further down can both test it unconditionally. binom_2col = []; ## The response on the scale the caller gave it, which is the number of ## successes for a binomial fit carrying trial counts and the response ## itself everywhere else. Y_FULL is always the proportion the fit ## works in; this is what the Variables table reports. y_input = []; if (! istable (data)) if (! (isnumeric (data) && isreal (data) && ismatrix (data))) error ("GeneralizedLinearModel: X must be a real matrix."); endif ## A binomial response may be given as an N-by-2 matrix holding the ## successes and the trials, as MATLAB accepts. A row vector stays a ## response vector: [3, 10] is two observations, never one pair. is_2col = strcmp (opts.Distribution, 'binomial') && isnumeric (resp) ... && isreal (resp) && ismatrix (resp) && ! isvector (resp) ... && columns (resp) == 2; if (! (is_2col || (isnumeric (resp) && isreal (resp) ... && isvector (resp)))) error ("GeneralizedLinearModel: Y must be a real vector."); endif X_raw = double (data); n_total = rows (X_raw); p_raw = columns (X_raw); if (is_2col) binom_2col = double (resp(:,2)); emsg = check_binomial_counts (double (resp(:,1)), binom_2col); if (! isempty (emsg)) error ("GeneralizedLinearModel: %s", emsg); endif ## The fit works in proportions; the success counts are kept for ## the Variables table. Only the successes are kept, not the two ## columns as handed in: Y is the number of successes by both routes ## since cd9c81d0, so that is the response the model has. MATLAB ## stores the matrix exactly as given. y_input = double (resp(:,1)); y_full = y_input ./ binom_2col; else y_full = double (resp(:)); endif if (rows (X_raw) != numel (y_full)) error (strcat ("GeneralizedLinearModel: X and Y must have the", ... " same number of observations.")); endif if (! isempty (opts.VarNames)) if (numel (opts.VarNames) != p_raw + 1) error ("GeneralizedLinearModel: VarNames must have %d elements.", ... p_raw + 1); endif pred_names = opts.VarNames(1:p_raw)(:)'; resp_name = opts.VarNames{end}; else pred_names = arrayfun (@(k) sprintf ("x%d", k), 1:p_raw, ... 'UniformOutput', false); resp_name = 'y'; endif if (! isempty (opts.ResponseVar)) resp_name = opts.ResponseVar; endif var_names_all = [pred_names, {resp_name}]; else tbl = data; X_raw = []; # numeric matrix built below via raw_to_codes col_names = tbl.Properties.VariableNames; n_total = height (tbl); var_names_all = col_names; if (ischar (resp) && ! isempty (resp)) resp_name = resp; elseif (isnumeric (resp) && ! isempty (resp)) resp_name = 'y'; if (! isempty (opts.ResponseVar)) resp_name = opts.ResponseVar; endif y_ext = double (resp(:)); elseif (is_formula) tparts = strsplit (modelspec, '~'); resp_name = strtrim (tparts{1}); else resp_name = col_names{end}; endif if (! isempty (opts.PredictorVars)) pred_names = opts.PredictorVars; elseif (is_formula) ## Only the variables the formula names take part in the model; any ## other column of the table is carried but is not a predictor. used = formula_var_names (modelspec); in_formula = ismember (col_names, used) ... & ! strcmp (col_names, resp_name); pred_names = col_names(in_formula); else pred_names = col_names(! strcmp (col_names, resp_name)); endif p_raw = numel (pred_names); if (exist ('y_ext', 'var')) y_full = y_ext; else y_full = double (tbl.(resp_name)(:)); endif endif ## Categorical predictor flags. cat_logical = false (1, p_raw); if (! isempty (opts.CategoricalVars)) cv = opts.CategoricalVars; if (islogical (cv)) n_cv = min (numel (cv), p_raw); cat_logical(1:n_cv) = cv(1:n_cv); elseif (isnumeric (cv)) cat_logical(cv(cv > 0 & cv <= p_raw)) = true; elseif (iscell (cv)) for i = 1:numel (cv) cat_logical(strcmp (pred_names, cv{i})) = true; endfor endif endif if (istable (data)) for j = 1:p_raw col = tbl.(pred_names{j}); ## A logical or string column groups its observations just as a cell ## or categorical one does, and is coded the same way. if (iscell (col) || isa (col, 'categorical') ... || islogical (col) || isa (col, 'string')) cat_logical(j) = true; endif endfor endif ## ------------------------------------------------------------------ ## ## Binomial trial counts. Y is the number of successes in both accepted ## forms -- the two-column response and 'BinomialSize' -- while the fit ## works in the proportion. Both are resolved before the missing mask ## is taken, so that a missing count makes its proportion missing too. ## ------------------------------------------------------------------ ## N_full = []; if (strcmp (opts.Distribution, 'binomial')) if (! isempty (binom_2col)) ## The trials came with the response. A 'BinomialSize' passed as ## well is ignored rather than refused, which is what MATLAB does. N_full = binom_2col; elseif (! isempty (opts.BinomialSize)) Nb = double (opts.BinomialSize(:)); if (! isscalar (Nb) && numel (Nb) != n_total) error (strcat ("GeneralizedLinearModel: BinomialSize must be a", ... " scalar or have one element per observation.")); endif N_full = expand_to_rows (Nb, n_total); emsg = check_binomial_counts (y_full, N_full); if (! isempty (emsg)) error ("GeneralizedLinearModel: %s", emsg); endif ## Up to v1.8.4 this response was read as the proportion, so one ## lying wholly within [0, 1] against more than one trial is almost ## certainly written for the old meaning. Warn rather than raise: ## an all-0/1 count vector is legitimate rare-event data. obs = y_full(! isnan (y_full)); if (! isempty (obs) && all (obs <= 1) && any (N_full > 1)) warning (strcat ("GeneralizedLinearModel: with 'BinomialSize'", ... " the response is the number of successes,", ... " not the proportion, from statistics 1.9.0;", ... " multiply by the trials for the old meaning.")); endif y_input = y_full; y_full = y_full ./ N_full; endif endif if (isempty (y_input)) y_input = y_full; endif ## Missing/excluded masks and the fitting subset. Only the variables the ## model actually uses can make a row missing; an unused table column with ## a gap in it does not cost an observation. if (! istable (data)) missing_mask = any (isnan (X_raw), 2) | isnan (y_full); else used_cols = [pred_names(:)', {resp_name}]; used_cols = used_cols(ismember (used_cols, col_names)); missing_mask = any (ismissing (tbl(:, used_cols)), 2); endif missing_mask = missing_mask(:); excluded_mask = false (n_total, 1); if (! isempty (opts.Exclude)) ex = opts.Exclude(:); if (islogical (ex)) excluded_mask(1:numel (ex)) = ex; else excluded_mask(ex) = true; endif endif avail_mask = ! missing_mask; subset_mask = avail_mask & ! excluded_mask; n_obs = sum (subset_mask); if (n_obs < 1) error (strcat ("GeneralizedLinearModel: no observations remain after", ... " removing missing/excluded rows.")); endif ## Weights, offset, and binomial trial counts. Each is kept at full ## length so that the per-observation tables can span the input rows. has_weights = ! isempty (opts.Weights); w_full = ones (n_total, 1); if (has_weights) w_full = expand_to_rows (double (opts.Weights(:)), n_total); endif w_sub = w_full(subset_mask); has_offset = ! isempty (opts.Offset); off_full = zeros (n_total, 1); if (has_offset) off_full = expand_to_rows (double (opts.Offset(:)), n_total); endif off_sub = off_full(subset_mask); distr = opts.Distribution; N_sub = []; if (! isempty (N_full)) N_sub = N_full(subset_mask); endif y_sub = y_full(subset_mask); ## Numeric predictor matrix (categorical columns as 1-based codes), used ## for encoding and for the slice/effect plots. [X_num_full, cat_levels] = raw_to_codes (data, X_raw, tbl, ... pred_names, cat_logical, n_total); ## ------------------------------------------------------------------ ## ## Design matrix (intercept included as a column when present). It is ## built over every input row so that the fitted values, residuals, and ## diagnostics can be reported for rows kept out of the fit; the fit ## itself sees only the subset. ## ------------------------------------------------------------------ ## if (is_formula) if (! istable (data)) tbl_all = array2table ([X_raw, y_full], ... 'VariableNames', var_names_all); else tbl_all = tbl; endif ## Hand the parser only the rows it would keep anyway, so that the ## returned design lines up row for row with AVAIL_MASK. tbl_avail = tbl_all(avail_mask, :); [X_avail, ~, coef_names] = parseWilkinsonFormula ( ... modelspec, 'model_matrix', tbl_avail, pred_names(cat_logical)); coef_names = coef_names(:)'; has_intercept = any (strcmp (coef_names, '(Intercept)')); enc_names = coef_names(! strcmp (coef_names, '(Intercept)')); X_design_all = NaN (n_total, columns (X_avail)); X_design_all(avail_mask, :) = X_avail; [terms, cat_info, term_cols] = terms_from_coefnames (coef_names, ... pred_names, cat_logical, data, tbl_avail); else ## Whether a categorical is given all its indicator columns depends ## on the intercept, which has to be settled before encoding. [X_enc_all, enc_names, cat_info] = encode_categorical ( ... X_num_full, cat_logical, pred_names, cat_levels, ... modelspec_has_intercept (modelspec, opts.Intercept)); [terms, has_intercept, coef_names, emsg] = parse_modelspec ( ... modelspec, enc_names, columns (X_enc_all), opts.Intercept); if (! isempty (emsg)) error ("GeneralizedLinearModel: %s", emsg); endif X_design_all = build_design (terms, X_enc_all); term_cols = [enc_names, {''}]; ## A missing categorical code encodes as an all-zero dummy row rather ## than NaN, so mark the missing rows explicitly. X_design_all(missing_mask, :) = NaN; endif X_design = X_design_all(subset_mask, :); ## ------------------------------------------------------------------ ## ## Fit the design via glmfit (the intercept is a design column already). ## ------------------------------------------------------------------ ## if (! isempty (opts.Link)) linkspec = opts.Link; else linkspec = default_link_spec (distr); endif linkname = link_name (linkspec); gargs = {'link', linkspec, 'constant', 'off'}; if (has_weights) gargs = [gargs, {'weights', w_sub}]; endif if (has_offset) gargs = [gargs, {'offset', off_sub}]; endif if (! isempty (opts.DispersionFlag)) gargs = [gargs, {'estdisp', ternary(opts.DispersionFlag, 'on', 'off')}]; endif yfit = y_sub; if (strcmp (distr, 'binomial') && ! isempty (N_sub)) yfit = [y_sub .* N_sub, N_sub]; endif [b, dev, stats] = glmfit (X_design, yfit, distr, gargs{:}); ## ------------------------------------------------------------------ ## ## Assemble the object. ## ------------------------------------------------------------------ ## [flink, dlink, ilink] = getlinkfunctions (linkspec); this.Link = struct ('Name', linkname, 'Link', flink, ... 'Derivative', dlink, 'Inverse', ilink); [devfun, varfun] = glm_family_functions (distr); this.Distribution = struct ('Name', distribution_name (distr), ... 'DevianceFunction', devfun, ... 'VarianceFunction', varfun); this.Coefficients = table (b(:), stats.se(:), stats.t(:), stats.p(:), ... 'VariableNames', {'Estimate', 'SE', 'tStat', 'pValue'}, ... 'RowNames', coef_names(:)); ## Fitted values over every input row. Rows kept out of the fit by ## 'Exclude' still get a prediction; rows dropped as missing give NaN. eta_all = X_design_all * b + off_full; mu_prob_all = ilink (eta_all); # probability (binomial) or mean (others) if (isempty (N_full)) N_all = ones (n_total, 1); else N_all = N_full; endif if (strcmp (distr, 'binomial')) mu_resp_all = N_all .* mu_prob_all; this.Fitted = table (mu_resp_all, eta_all, mu_prob_all, ... 'VariableNames', {'Response', 'LinearPredictor', 'Probability'}); else mu_resp_all = mu_prob_all; this.Fitted = table (mu_resp_all, eta_all, ... 'VariableNames', {'Response', 'LinearPredictor'}); endif [r_raw, r_pear, r_ansc, r_dev] = glm_residuals (distr, y_full, ... mu_prob_all, N_full); r_lin = (y_full - mu_prob_all) .* dlink (mu_prob_all); this.Residuals = table (r_raw, r_lin, r_pear, r_ansc, r_dev, ... 'VariableNames', {'Raw', 'LinearPredictor', 'Pearson', 'Anscombe', ... 'Deviance'}); mu_prob = mu_prob_all(subset_mask); mu_resp = mu_resp_all(subset_mask); ## Log-likelihood, information criteria, and R-squared measures. These ## use the intercept-only ("null") model as the baseline. w_ll = w_sub; [bn, nulldev, sn] = glmfit (ones (n_obs, 1), yfit, distr, gargs{:}); eta0 = bn * ones (n_obs, 1); if (! isempty (off_sub)) eta0 = eta0 + off_sub; endif mu0 = ilink (eta0); LL = glm_loglik (distr, y_sub, mu_prob, N_sub, w_ll, stats.s); LL_null = glm_loglik (distr, y_sub, mu0, N_sub, w_ll, sn.s); this.LogLikelihood = LL; this.nulldev_ = nulldev; this.llnull_ = LL_null; k = numel (b); # MATLAB counts the coefficients only, not the dispersion aic = -2 * LL + 2 * k; if (n_obs - k - 1 > 0) aicc = aic + 2 * k * (k + 1) / (n_obs - k - 1); else aicc = Inf; endif this.ModelCriterion = struct ('AIC', aic, ... 'AICc', aicc, 'BIC', -2 * LL + k * log (n_obs), ... 'CAIC', -2 * LL + k * (log (n_obs) + 1)); ybar_w = sum (w_ll .* y_sub) / sum (w_ll); sse = sum (w_ll .* (y_sub - mu_resp) .^ 2); ssr = sum (w_ll .* (mu_resp - ybar_w) .^ 2); sst = sum (w_ll .* (y_sub - ybar_w) .^ 2); r2_ord = 1 - sse / max (sst, eps); dfe = stats.dfe; if (dfe > 0) r2_adj = 1 - (1 - r2_ord) * (n_obs - 1) / dfe; else r2_adj = NaN; endif r2_dev = 1 - dev / max (nulldev, eps); r2_llr = 1 - LL / LL_null; r2_gen = 1 - exp (2 * (LL_null - LL) / n_obs); r2_gen_max = 1 - exp (2 * LL_null / n_obs); this.Rsquared = struct ('Ordinary', r2_ord, 'Adjusted', r2_adj, ... 'Deviance', r2_dev, 'AdjGeneralized', r2_gen / r2_gen_max, ... 'LLR', r2_llr); ## Model formula. TERMS is expressed over the encoded design columns; the ## formula is expressed over the model's variables, so a categorical's ## indicator columns have to be folded back onto the variable they came ## from before the term names can be built. n_vars = numel (var_names_all); var_idx = zeros (1, p_raw); for j = 1:p_raw k = find (strcmp (var_names_all, pred_names{j}), 1); if (! isempty (k)) var_idx(j) = k; endif endfor ## Indexed by the columns of TERMS, which are not the coefficient ## names: a factor appearing only inside an interaction or a power has a ## column without ever being a coefficient. [enc2raw, col_pow] = encodednames_to_row (term_cols(1:end-1), pred_names, ... cat_info); terms_var = variable_level_terms (terms(:, 1:end-1), enc2raw, var_idx, ... n_vars, col_pow); this.Formula = LinearFormula (terms_var, var_names_all(:)', ... 'ResponseName', resp_name, ... 'Link', linkspec); this.formulastr_ = char (this.Formula); in_model = this.Formula.InModel; ## Per-variable information, over the fitting subset rather than the full ## data, as MATLAB reports it: VariableInfo sits on a fitted model, so a ## range describes the data the fit actually used. Measured against ## R2024a, which also drops a category appearing only in excluded rows. vi_class = cell (n_vars, 1); vi_range = cell (n_vars, 1); vi_inmodel = in_model(:); vi_iscat = false (n_vars, 1); vi_iscat(var_idx(var_idx > 0)) = cat_logical(var_idx > 0); for j = 1:n_vars if (istable (data)) col = tbl.(var_names_all{j}); elseif (j <= p_raw) col = X_raw(:, j); else col = y_input; endif col = col(subset_mask); if (isa (col, 'categorical')) col = removecats (col); endif [vi_class{j}, vi_range{j}] = variable_class_and_range (col); endfor this.VariableInfo = table (vi_class, vi_range, vi_inmodel, vi_iscat, ... 'VariableNames', {'Class', 'Range', 'InModel', 'IsCategorical'}, ... 'RowNames', var_names_all(:)); if (istable (data)) this.Variables = tbl; else this.Variables = array2table ([X_raw, y_input], ... 'VariableNames', var_names_all); endif this.ObservationInfo = table (w_full, excluded_mask, missing_mask, ... subset_mask, ... 'VariableNames', {'Weights', 'Excluded', 'Missing', 'Subset'}); ## Leverage, hat matrix, and Cook's distance. The hat matrix is the ## asymmetric IRLS form H = X inv(X'WX) X' W, whose diagonal is the ## leverage; rows outside the fit contribute nothing to it. dmu_deta = 1 ./ dlink (mu_prob); vwt = glm_varfun (distr, mu_prob, N_sub); w_irls = (dmu_deta .^ 2) ./ max (vwt, realmin) .* w_ll; XtW = (X_design .* w_irls)'; Hs = X_design * ((XtW * X_design) \ XtW); lev = diag (Hs); pear = r_pear(subset_mask); phi_c = ternary (logical (stats.estdisp), stats.s, 1); cooksd = w_ll .* pear .^ 2 .* lev ... ./ (max (1 - lev, eps) .^ 2 * numel (b) * phi_c); lev_all = zeros (n_total, 1); lev_all(subset_mask) = lev; cd_all = NaN (n_total, 1); cd_all(subset_mask) = cooksd; hat_all = zeros (n_total, n_total); hat_all(subset_mask, subset_mask) = Hs; this.Diagnostics = table (lev_all, cd_all, hat_all, ... 'VariableNames', {'Leverage', 'CooksDistance', 'HatMatrix'}); this.b_ = b; this.stats_ = stats; this.distr_ = distr; this.linkarg_ = linkspec; ## Trials from a two-column response stand in for 'BinomialSize', which ## is ignored when both are given. if (! isempty (binom_2col)) this.binomsize_ = binom_2col; else this.binomsize_ = opts.BinomialSize; endif this.terms_ = terms; this.catinfo_ = cat_info; this.encnames_ = enc_names; this.prednames_ = pred_names; this.design_ = X_design; this.subset_ = subset_mask; this.xmeans_ = mean (X_num_full(subset_mask,:), 1); this.CoefficientNames = coef_names; this.CoefficientCovariance = stats.covb; this.NumCoefficients = numel (b); this.NumEstimatedCoefficients = numel (b); this.NumPredictors = p_raw; this.NumObservations = n_obs; this.NumVariables = n_vars; this.Deviance = dev; this.DFE = stats.dfe; this.Dispersion = stats.s; this.DispersionEstimated = logical (stats.estdisp); this.SSE = sse; this.SSR = ssr; this.SST = sst; this.Offset = off_full; this.LikelihoodPenalty = string ("none"); this.ResponseName = resp_name; this.PredictorNames = pred_names(:); this.VariableNames = var_names_all(:); endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearModel} {@var{yhat} =} predict (@var{mdl}, @var{Xnew}) ## @deftypefnx {GeneralizedLinearModel} {[@var{yhat}, @var{yci}] =} predict (@var{mdl}, @var{Xnew}) ## @deftypefnx {GeneralizedLinearModel} {[@dots{}] =} predict (@dots{}, @var{Name}, @var{Value}) ## ## Predict the response of the model @var{mdl} at the new predictor data ## @var{Xnew} (a numeric matrix or a table). Predictions are on the mean ## (response) scale. With two outputs, @var{yci} is an @math{m}-by-2 matrix ## of confidence intervals. The @qcode{'Alpha'} pair sets the confidence ## level to @math{100 (1 - @var{Alpha})%} (default 0.05). ## ## @end deftypefn function [yhat, yci] = predict (mdl, Xnew, varargin) if (nargin < 2) error ("GeneralizedLinearModel.predict: Xnew is required."); endif alpha = 0.05; offnew = []; for k = 1:2:numel (varargin) switch (lower (varargin{k})) case 'alpha'; alpha = varargin{k+1}; case 'offset'; offnew = varargin{k+1}; otherwise error (strcat ("GeneralizedLinearModel.predict: unknown", ... " parameter '%s'."), varargin{k}); endswitch endfor ## Marshal Xnew into a raw numeric predictor matrix (encode categoricals). p_raw = mdl.NumPredictors; if (istable (Xnew)) n_new = height (Xnew); X_raw = zeros (n_new, p_raw); for j = 1:p_raw col = Xnew.(mdl.prednames_{j}); cidx = []; if (! isempty (mdl.catinfo_.names)) cidx = find (strcmp (mdl.catinfo_.names, mdl.prednames_{j})); endif if (! isempty (cidx) && iscell (col)) levels_j = mdl.catinfo_.levels{cidx}; codes = zeros (n_new, 1); for L = 1:numel (levels_j) codes(strcmp (col, levels_j{L})) = L; endfor X_raw(:, j) = codes; else X_raw(:, j) = double (col); endif endfor else X_raw = double (Xnew); if (columns (X_raw) != p_raw) error ("GeneralizedLinearModel.predict: Xnew must have %d columns.", ... p_raw); endif endif X_enc = reencode_predictors (X_raw, mdl.prednames_, mdl.catinfo_, ... mdl.encnames_); X_design = build_design (mdl.terms_, X_enc); valargs = {'constant', 'off', 'confidence', 1 - alpha}; if (! isempty (offnew)) valargs = [valargs, {'offset', offnew(:)}]; endif ## A binomial prediction is the probability of success, never a count: ## the number of trials belongs to an observation, and new predictor ## values do not carry one. Measured against R2024a, whose PREDICT ## returns the probability for the two-column response and for ## 'BinomialSize' alike. Passing the fitted trial counts through to ## GLMVAL as 'size' also could not work for new data of a different ## height, which is what exposed this. if (nargout > 1) [yhat, ylo, yhi] = glmval (mdl.b_, X_design, mdl.linkarg_, ... mdl.stats_, valargs{:}); yci = [yhat - ylo, yhat + yhi]; else yhat = glmval (mdl.b_, X_design, mdl.linkarg_, valargs{:}); endif endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearModel} {@var{yhat} =} feval (@var{mdl}, @var{x1}, @var{x2}, @dots{}) ## ## Predict the response by passing each predictor as a separate argument (a ## scalar or column vector), returning point predictions on the mean scale. ## Equivalent to @code{predict (@var{mdl}, [@var{x1}, @var{x2}, @dots{}])}. ## ## @end deftypefn function yhat = feval (mdl, varargin) if (numel (varargin) == 1 && size (varargin{1}, 2) == mdl.NumPredictors) Xnew = varargin{1}; else cols = cellfun (@(c) c(:), varargin, 'UniformOutput', false); Xnew = [cols{:}]; endif yhat = predict (mdl, Xnew); endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearModel} {@var{ci} =} coefCI (@var{mdl}) ## @deftypefnx {GeneralizedLinearModel} {@var{ci} =} coefCI (@var{mdl}, @var{alpha}) ## ## Confidence intervals for the coefficient estimates. @var{ci} is a ## @math{k}-by-2 matrix of @math{100 (1 - @var{alpha})%} intervals (default ## @var{alpha} = 0.05). The @math{t} distribution is used when the ## dispersion was estimated, the normal distribution otherwise. ## ## @end deftypefn function ci = coefCI (mdl, alpha) if (nargin < 2) alpha = 0.05; endif if (! (isscalar (alpha) && isnumeric (alpha) && alpha >= 0 && alpha <= 1)) error ("GeneralizedLinearModel.coefCI: ALPHA must be in [0, 1]."); endif b = mdl.Coefficients.Estimate; se = mdl.Coefficients.SE; crit = tinv (1 - alpha / 2, mdl.DFE); ci = [b - crit .* se, b + crit .* se]; endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearModel} {@var{p} =} coefTest (@var{mdl}) ## @deftypefnx {GeneralizedLinearModel} {[@var{p}, @var{stat}, @var{df}] =} coefTest (@var{mdl}, @var{H}) ## ## Wald test of the linear hypothesis @math{H b = 0} on the coefficients. ## @var{H} is an @math{m}-by-@math{k} contrast matrix; when omitted it tests ## that all coefficients except the intercept are zero (the model versus the ## constant model). Returns the p-value @var{p}, and optionally the test ## statistic @var{stat} and its numerator degrees of freedom @var{df}. ## ## @end deftypefn function [p, stat, df] = coefTest (mdl, H) k = mdl.NumCoefficients; if (nargin < 2) ipos = find (strcmp (mdl.CoefficientNames, '(Intercept)')); rows_h = setdiff (1:k, ipos); H = zeros (numel (rows_h), k); for i = 1:numel (rows_h) H(i, rows_h(i)) = 1; endfor endif b = mdl.Coefficients.Estimate; df = rows (H); Hb = H * b; HVH = H * mdl.CoefficientCovariance * H'; ## Wald F statistic (MATLAB uses the F distribution for coefTest). stat = (Hb' * (HVH \ Hb)) / df; p = fcdf (stat, df, mdl.DFE, 'upper'); endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearModel} {@var{tbl} =} devianceTest (@var{mdl}) ## ## Likelihood-ratio (deviance) test of the fitted model against the ## intercept-only model. Returns a table with the deviance, degrees of ## freedom, and p-value of each model, the last row giving the test ## statistic and its p-value. Where the dispersion is fixed, the statistic ## is the drop in deviance, @qcode{chi2Stat}, referred to the chi-square ## distribution; where it is estimated, it is the drop per degree of ## freedom over the dispersion, @qcode{FStat}, referred to the ## @math{F}-distribution. Each row is named by the formula of the model it ## describes. ## ## @end deftypefn function tbl = devianceTest (mdl) dev_full = mdl.Deviance; dev_null = mdl.nulldev_; df_diff = mdl.NumCoefficients - 1; drop = dev_null - dev_full; if (mdl.DispersionEstimated) stat = (drop / df_diff) / mdl.Dispersion; pval = fcdf (stat, df_diff, mdl.DFE, 'upper'); statname = 'FStat'; else stat = drop; pval = chi2cdf (stat, df_diff, 'upper'); statname = 'chi2Stat'; endif Deviance = [dev_null; dev_full]; DFE = [mdl.DFE + df_diff; mdl.DFE]; Stat = [NaN; stat]; pValue = [NaN; pval]; ## Both rows are named by their formula, as MATLAB renders them: the null ## model is the same linked response against an intercept alone. nullstr = sprintf ("%s ~ 1", strtrim (strtok (mdl.formulastr_, "~"))); tbl = table (Deviance, DFE, Stat, pValue, ... 'VariableNames', {'Deviance', 'DFE', statname, 'pValue'}, ... 'RowNames', {nullstr, mdl.formulastr_}); endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearModel} {@var{ysim} =} random (@var{mdl}) ## @deftypefnx {GeneralizedLinearModel} {@var{ysim} =} random (@var{mdl}, @var{Xnew}) ## ## Simulate responses from the fitted model. With one argument the fitted ## values are used; otherwise the mean is predicted at the new predictor ## data @var{Xnew}. A random draw from the response distribution about that ## mean is returned. ## ## @end deftypefn function ysim = random (mdl, Xnew) if (nargin < 2) mu = mdl.Fitted.Response; else mu = predict (mdl, Xnew); endif switch (mdl.distr_) case 'normal' ysim = normrnd (mu, sqrt (mdl.Dispersion)); case 'poisson' ysim = poissrnd (mu); case 'binomial' if (isempty (mdl.binomsize_)) ysim = binornd (1, mu); else N = mdl.binomsize_(:); if (isscalar (N)) N = N * ones (size (mu)); endif ysim = binornd (N, mu ./ N); endif case 'gamma' ysim = gamrnd (1 / mdl.Dispersion, mu * mdl.Dispersion); case 'inverse gaussian' error (strcat ("GeneralizedLinearModel.random: simulation for the", ... " 'inverse gaussian' distribution is not supported.")); endswitch endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearModel} {@var{h} =} plotResiduals (@var{mdl}) ## @deftypefnx {GeneralizedLinearModel} {@var{h} =} plotResiduals (@var{mdl}, @var{plottype}) ## @deftypefnx {GeneralizedLinearModel} {@var{h} =} plotResiduals (@dots{}, @qcode{'ResidualType'}, @var{rt}) ## ## Plot the model residuals. @var{plottype} is one of @qcode{'histogram'} ## (default), @qcode{'caseorder'}, @qcode{'fitted'}, @qcode{'lagged'}, or ## @qcode{'probability'}. @qcode{'ResidualType'} picks the residual column ## (@qcode{'Raw'} default, @qcode{'Pearson'}, @qcode{'Deviance'}, ## @qcode{'Anscombe'}). Returns the graphics handle. ## ## @end deftypefn function h = plotResiduals (mdl, varargin) ptype = 'histogram'; rtype = 'Raw'; k = 1; if (numel (varargin) >= 1 && ischar (varargin{1}) ... && ! strcmpi (varargin{1}, 'ResidualType')) ptype = varargin{1}; k = 2; endif for i = k:2:numel (varargin) if (strcmpi (varargin{i}, 'ResidualType')) rtype = varargin{i+1}; endif endfor r = mdl.Residuals.(rtype); switch (lower (ptype)) case 'histogram' h = hist (r); xlabel ("Residuals"); ylabel ("Frequency"); case 'caseorder' h = plot (1:numel (r), r, 'x'); xlabel ("Row number"); ylabel (sprintf ("%s residuals", rtype)); case 'fitted' h = plot (mdl.Fitted.Response, r, 'x'); xlabel ("Fitted values"); ylabel (sprintf ("%s residuals", rtype)); case 'lagged' h = plot (r(1:end-1), r(2:end), 'x'); xlabel ("Residual (t-1)"); ylabel ("Residual (t)"); case 'probability' [rs, idx] = sort (r); n = numel (rs); pp = ((1:n)' - 0.5) / n; h = plot (rs, norminv (pp), 'x'); xlabel (sprintf ("%s residuals", rtype)); ylabel ("Standard normal quantiles"); otherwise error ("GeneralizedLinearModel.plotResiduals: bad plot type '%s'.", ... ptype); endswitch title (sprintf ("Residuals: %s", ptype)); endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearModel} {@var{h} =} plotDiagnostics (@var{mdl}) ## @deftypefnx {GeneralizedLinearModel} {@var{h} =} plotDiagnostics (@var{mdl}, @var{plottype}) ## ## Plot observation diagnostics. @var{plottype} is @qcode{'leverage'} ## (default) or @qcode{'cookd'} (Cook's distance). A reference line marks ## the usual threshold. Returns the graphics handle. ## ## @end deftypefn function h = plotDiagnostics (mdl, plottype) if (nargin < 2) plottype = 'leverage'; endif n = mdl.NumObservations; idx = find (mdl.subset_); switch (lower (plottype)) case 'leverage' lev = mdl.Diagnostics.Leverage(idx); h = stem (idx, lev, 'Marker', 'x'); ref = 2 * mdl.NumCoefficients / n; ylabel ("Leverage"); case 'cookd' cd = mdl.Diagnostics.CooksDistance(idx); h = stem (idx, cd, 'Marker', 'x'); ref = 3 * mean (cd); ylabel ("Cook's distance"); otherwise error (strcat ("GeneralizedLinearModel.plotDiagnostics: bad plot", ... " type '%s'."), plottype); endswitch hold on; plot ([idx(1), idx(end)], [ref, ref], 'r--'); hold off; xlabel ("Row number"); title (sprintf ("Diagnostics: %s", plottype)); endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearModel} {@var{h} =} plotEffects (@var{mdl}) ## ## Main-effects plot: for each predictor, the change in the fitted mean ## response as that predictor sweeps its observed range while the others are ## held at their means. Returns the graphics handle. ## ## @end deftypefn function h = plotEffects (mdl) p = mdl.NumPredictors; base = mdl.xmeans_; eff = zeros (p, 1); for j = 1:p a = base; b = base; a(j) = base(j) - 1; b(j) = base(j) + 1; eff(j) = predict (mdl, b) - predict (mdl, a); endfor h = plot (eff, 1:p, 'o', 'MarkerFaceColor', 'b'); hold on; for j = 1:p plot ([0, eff(j)], [j, j], 'b-'); endfor plot ([0, 0], [0.5, p + 0.5], 'k:'); hold off; ylim ([0.5, p + 0.5]); set (gca, 'YTick', 1:p, 'YTickLabel', mdl.PredictorNames); xlabel ("Effect on fitted response (two-unit change)"); title ("Main effects"); endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearModel} {@var{h} =} plotAdjustedResponse (@var{mdl}, @var{var}) ## ## Adjusted-response plot for the predictor @var{var} (a name or index): the ## fitted mean response as @var{var} sweeps its observed range with other ## predictors held at their means, overlaid on the partial residuals. ## Returns the graphics handle. ## ## @end deftypefn function h = plotAdjustedResponse (mdl, var) j = resolve_predictor (mdl, var); xj = linspace (mdl.xmeans_(j) - 2, mdl.xmeans_(j) + 2, 50)'; Xg = repmat (mdl.xmeans_, numel (xj), 1); Xg(:,j) = xj; yg = predict (mdl, Xg); h = plot (xj, yg, 'b-', 'LineWidth', 1.5); xlabel (mdl.PredictorNames{j}); ylabel (sprintf ("Adjusted %s", mdl.ResponseName)); title (sprintf ("Adjusted response for %s", mdl.PredictorNames{j})); endfunction ## -*- texinfo -*- ## @deftypefn {GeneralizedLinearModel} {@var{h} =} plotAdded (@var{mdl}, @var{var}) ## ## Added-variable (partial-regression) plot for the predictor @var{var} (a ## name or index): the response residuals from the model without @var{var} ## against the residuals of @var{var} regressed on the remaining predictors. ## Returns the graphics handle. ## ## @end deftypefn function h = plotAdded (mdl, var) j = resolve_predictor (mdl, var); X = mdl.design_; ## Locate the design column of the requested predictor. cidx = find (strcmp (mdl.CoefficientNames, mdl.PredictorNames{j}), 1); if (isempty (cidx)) error (strcat ("GeneralizedLinearModel.plotAdded: predictor is not a", ... " single design column.")); endif others = setdiff (1:columns (X), cidx); Xo = X(:,others); ## The design holds the fitted rows only, so take the residuals of the ## same rows. ry = mdl.Residuals.Raw(mdl.subset_); xj = X(:,cidx); bx = Xo \ xj; rx = xj - Xo * bx; h = plot (rx, ry, 'x'); hold on; bb = rx \ ry; xr = [min(rx), max(rx)]; plot (xr, bb * xr, 'r-'); hold off; xlabel (sprintf ("%s (adjusted)", mdl.PredictorNames{j})); ylabel (sprintf ("%s (adjusted)", mdl.ResponseName)); title (sprintf ("Added variable plot for %s", mdl.PredictorNames{j})); endfunction endmethods endclassdef ## Parse GeneralizedLinearModel name/value options into a structure. function opts = glm_parse_nv (nv) opts = struct ('Distribution', 'normal', 'Link', [], 'Weights', [], ... 'Offset', [], 'BinomialSize', [], 'Intercept', true, ... 'DispersionFlag', [], 'VarNames', {{}}, 'ResponseVar', '', ... 'PredictorVars', {{}}, 'CategoricalVars', [], 'Exclude', []); if (mod (numel (nv), 2) != 0) error (strcat ("GeneralizedLinearModel: optional arguments must be", ... " Name-Value pairs.")); endif for k = 1:2:numel (nv) switch (lower (nv{k})) case 'distribution'; opts.Distribution = lower (nv{k+1}); case 'link'; opts.Link = nv{k+1}; case 'weights'; opts.Weights = nv{k+1}; case 'offset'; opts.Offset = nv{k+1}; case 'binomialsize'; opts.BinomialSize = nv{k+1}; case 'intercept'; opts.Intercept = logical (nv{k+1}); case 'dispersionflag'; opts.DispersionFlag = logical (nv{k+1}); case 'varnames'; opts.VarNames = nv{k+1}; case 'responsevar'; opts.ResponseVar = nv{k+1}; case 'predictorvars'; opts.PredictorVars = nv{k+1}; case 'categoricalvars'; opts.CategoricalVars = nv{k+1}; case 'exclude'; opts.Exclude = nv{k+1}; otherwise error (strcat ("GeneralizedLinearModel: unknown parameter", ... " name '%s'."), nv{k}); endswitch endfor if (! any (strcmp (opts.Distribution, {'normal', 'binomial', 'poisson', ... 'gamma', 'inverse gaussian'}))) error ("GeneralizedLinearModel: unknown distribution '%s'.", ... opts.Distribution); endif endfunction ## Broadcast a scalar to N rows; otherwise pass the vector through. function v = expand_to_rows (v, n) if (isscalar (v)) v = v * ones (n, 1); endif endfunction ## Check a binomial response given as counts S against its trial counts N. ## Returns a message body rather than raising, so that the caller names ## itself; empty when the pair is good. Shared by the two-column response and ## the 'BinomialSize' form, which mean the same thing. ## ## The rules are MATLAB's, measured against R2024a: the trials must be ## non-negative integers, while the successes need not be integral, only ## within [0, trials]. A NaN success count is left alone, so that the ## proportion becomes NaN and the row is dropped as missing exactly as a NaN ## in a plain response vector is. A non-finite trial count is refused here; ## MATLAB does not check it and leaks 'X must be in the interval [0,1]' out of ## the fitter instead. function emsg = check_binomial_counts (S, N) emsg = ''; ## Deliberately stricter than MATLAB on one point: a trial count of zero is ## refused rather than accepted. MATLAB rejects it for any row with a ## success (the [0,N] rule), leaving only the wholly empty observation, ## which carries no information and would divide zero by zero here. if (! all (isfinite (N)) || any (N < 1) || any (N != fix (N))) emsg = strcat ("the number of binomial trials must be finite", ... " positive integers."); return; endif if (! all (isnan (S) | (S >= 0 & S <= N))) emsg = strcat ("a binomial response holds the number of successes and", ... " must be between zero and the corresponding number of", ... " trials."); endif endfunction ## Base names of the variables a Wilkinson formula refers to (response ## included), with any '^k' power suffix stripped. function vnames = formula_var_names (modelspec) schema = parseWilkinsonFormula (modelspec, 'matrix'); vnames = regexprep (schema.VariableNames, '\^\d+$', ''); vnames = unique (vnames, 'stable'); endfunction ## Display name of a response distribution. function name = distribution_name (distr) switch (distr) case 'normal'; name = 'Normal'; case 'binomial'; name = 'Binomial'; case 'poisson'; name = 'Poisson'; case 'gamma'; name = 'Gamma'; case 'inverse gaussian'; name = 'Inverse Gaussian'; endswitch endfunction ## Deviance and variance functions of a response distribution, as the handles ## reported by the Distribution property. function [devfun, varfun] = glm_family_functions (distr) switch (distr) case 'normal' devfun = @(mu, y) (y - mu) .^ 2; varfun = @(mu) ones (size (mu)); case 'binomial' devfun = @(mu, y, N) 2 * N .* (y .* log ((y + (y == 0)) ./ mu) ... + (1 - y) .* log ((1 - y + (y == 1)) ./ (1 - mu))); varfun = @(mu, N) mu .* (1 - mu) ./ N; case 'poisson' devfun = @(mu, y) 2 * (y .* (log ((y + (y == 0)) ./ mu)) - (y - mu)); varfun = @(mu) mu; case 'gamma' devfun = @(mu, y) 2 * (-log (y ./ mu) + (y - mu) ./ mu); varfun = @(mu) mu .^ 2; case 'inverse gaussian' devfun = @(mu, y) (((y - mu) ./ mu) .^ 2) ./ y; varfun = @(mu) mu .^ 3; endswitch endfunction ## Raw, Pearson, Anscombe, and deviance residuals of a GLM fit, evaluated for ## every input row. Y is the response (a proportion for the binomial family), ## MU the fitted probability/mean, and N the binomial trial counts (empty ## otherwise). Matches the residuals @code{glmfit} returns for the fitted rows. function [raw, pear, ansc, devr] = glm_residuals (distr, y, mu, N) [devfun, varfun] = glm_family_functions (distr); if (strcmp (distr, 'binomial')) if (isempty (N)) N = ones (size (y)); endif raw = (y - mu) .* N; sd = sqrt (varfun (mu, N)); devn = devfun (mu, y, N); else raw = y - mu; sd = sqrt (varfun (mu)); devn = devfun (mu, y); endif pear = (y - mu) ./ (sd + (y == mu)); devr = sign (y - mu) .* sqrt (max (0, devn)); switch (distr) case 'normal' ansc = y - mu; case 'binomial' ab = 2 / 3; ansc = beta (ab, ab) * (betainc (y, ab, ab) - betainc (mu, ab, ab)) ... ./ ((mu .* (1 - mu)) .^ (1 / 6) ./ sqrt (N)); case 'poisson' ansc = 1.5 * ((y .^ (2 / 3) - mu .^ (2 / 3)) ./ mu .^ (1 / 6)); case 'gamma' pwr = 1 / 3; ansc = 3 * (y .^ pwr - mu .^ pwr) ./ mu .^ pwr; case 'inverse gaussian' ansc = (log (y) - log (mu)) ./ mu; endswitch endfunction ## Log-likelihood of a GLM fit. Y is the response (proportion for binomial), ## MU the fitted probability/mean, N the binomial trials (empty otherwise), W ## the prior weights, and PHI the dispersion parameter. function ll = glm_loglik (distr, y, mu, N, w, phi) rmin = realmin; switch (distr) case 'normal' ne = sum (w); s2 = sum (w .* (y - mu) .^ 2) / ne; ll = -0.5 * ne * (log (2 * pi * s2) + 1); case 'poisson' ll = sum (w .* (y .* log (max (mu, rmin)) - mu - gammaln (y + 1))); case 'binomial' if (isempty (N)) N = ones (size (y)); endif yc = y .* N; ll = sum (w .* (gammaln (N + 1) - gammaln (yc + 1) ... - gammaln (N - yc + 1) + yc .* log (max (mu, rmin)) ... + (N - yc) .* log (max (1 - mu, rmin)))); case 'gamma' a = 1 ./ phi; ll = sum (w .* (a .* log (a) - a .* log (mu) + (a - 1) .* log (y) ... - a .* y ./ mu - gammaln (a))); case 'inverse gaussian' ll = sum (w .* (-0.5 * (log (2 * pi * phi .* y .^ 3) ... + (y - mu) .^ 2 ./ (phi .* mu .^ 2 .* y)))); endswitch endfunction ## Resolve a predictor reference (name or index) to a column index. function j = resolve_predictor (mdl, var) if (ischar (var)) j = find (strcmp (mdl.PredictorNames, var), 1); if (isempty (j)) error ("GeneralizedLinearModel: unknown predictor '%s'.", var); endif else j = var; if (! (isscalar (j) && j >= 1 && j <= mdl.NumPredictors)) error ("GeneralizedLinearModel: predictor index out of range."); endif endif endfunction ## Variance function V(mu) of the response distribution (for the IRLS weights ## used in leverage/Cook's-distance diagnostics). function v = glm_varfun (distr, mu, N) [~, varfun] = glm_family_functions (distr); if (strcmp (distr, 'binomial')) if (isempty (N)) N = ones (size (mu)); endif v = varfun (mu, N); else v = varfun (mu); endif endfunction ## Response distribution's canonical link specification. function spec = default_link_spec (distr) switch (distr) case 'normal'; spec = 'identity'; case 'binomial'; spec = 'logit'; case 'poisson'; spec = 'log'; case 'gamma'; spec = 'reciprocal'; case 'inverse gaussian'; spec = -2; endswitch endfunction ## Human-readable name of a link specification. function name = link_name (linkarg) if (ischar (linkarg)) name = linkarg; elseif (isnumeric (linkarg) && isscalar (linkarg)) name = sprintf ('%g', linkarg); else name = 'custom'; endif endfunction ## Small inline conditional helper. function out = ternary (cond, a, b) if (cond) out = a; else out = b; endif endfunction %!demo %! ## Fit a Poisson regression and inspect the model object. %! X = [0.1, 1.2; 0.4, 0.7; 1.1, 0.2; 1.5, 1.9; 0.3, 0.5; 1.8, 1.1; 0.9, 0.3]; %! y = [1; 0; 2; 3; 1; 4; 2]; %! mdl = fitglm (X, y, 'Distribution', 'poisson'); %! disp (mdl.Coefficients) %! printf ("Deviance = %g, AIC = %g\n", mdl.Deviance, mdl.ModelCriterion.AIC); ## Test direct construction and input validation %!test %! X = [1 2; 2 1; 3 4; 4 3; 5 6; 6 5; 1 3; 4 2]; %! y = [1; 0; 2; 3; 2; 4; 1; 3]; %! mdl = GeneralizedLinearModel (X, y, "linear", "Distribution", "poisson"); %! assert_equal (class (mdl), "GeneralizedLinearModel"); %! assert_equal (mdl.Distribution.Name, "Poisson"); %! assert_equal (mdl.Link.Name, "log"); %!test %! ## Below the resolution of 1 - fcdf, values from MATLAB R2024a %! u = (1:30)'; %! m = fitglm (u, 2 * u + 0.01 * sin (u)); %! assert_equal (coefTest (m), 2.48569519842047e-96, -1e-8); %!test %! ## Below the resolution of 1 - chi2cdf %! u = (1:20)'; %! N = 30 * ones (20, 1); %! m = fitglm (u, [round(N ./ (1 + exp (5 - 0.5 * u))), N], ... %! 'Distribution', 'binomial'); %! D = devianceTest (m); %! assert_equal (D.pValue(2), gammainc (D.chi2Stat(2) / 2, 1/2, 'upper'), ... %! -1e-12); %!error GeneralizedLinearModel (1) %!error ... %! GeneralizedLinearModel ("a", [1;2], "linear") %!error ... %! GeneralizedLinearModel ([1 2; 3 4], "a", "linear") %!error ... %! GeneralizedLinearModel ([1 2; 3 4], [1;0], "linear", ... %! "Distribution", "wibble") %!error ... %! mdl = GeneralizedLinearModel ([1 2; 2 1; 3 4; 4 3; 5 6; 6 5], ... %! [1;0;2;3;2;4], "linear", ... %! "Distribution", "poisson"); mdl(1); ## Comprehensive property and method coverage %!shared X, yp, yb, yn %! X = [ 0.37, 0.06, 1.76; -0.76, -1.52, 0.84; 0.76, -0.19, -0.47; ... %! -0.80, -2.74, -0.90; 0.08, 0.39, 1.05; -0.41, -0.03, 0.74; ... %! 0.23, 1.21, 0.35; 0.66, 0.94, 0.13; 0.66, -0.12, -0.06; ... %! 2.09, 1.33, -0.71; 1.50, 0.08, -0.52; 0.59, 0.07, -1.13; ... %! -1.17, -0.35, -1.28; 0.68, 0.63, -0.80; -0.69, 0.08, 0.41; ... %! 2.04, 0.96, -0.56]; %! yp = [5 2 0 3 1 1 0 1 2 1 3 0 0 1 1 3]'; %! yb = [1 1 1 0 0 1 1 1 1 1 1 0 0 0 0 1]'; %! yn = [2.1 -0.3 1.2 -1.1 0.8 0.4 1.5 1.1 0.6 2.9 2.0 0.7 -1.3 0.9 -0.2 2.5]'; %!test # a normal-distribution GLM with identity link reproduces OLS exactly %! mdl = fitglm (X, yn, "Distribution", "normal"); %! b_ols = [ones(16, 1), X] \ yn; %! assert_equal (mdl.Coefficients.Estimate, b_ols, 1e-10); %! assert_equal (mdl.Fitted.Response, [ones(16, 1), X] * b_ols, 1e-10); %! assert_equal (mdl.Residuals.Raw, yn - [ones(16, 1), X] * b_ols, 1e-10); %! assert_equal (mdl.Deviance, sum ((yn - [ones(16, 1), X] * b_ols) .^ 2), 1e-10); %! assert_equal (mdl.Link.Name, "identity"); %!test # the normal-GLM standard errors match those from regress %! mdl = fitglm (X, yn, "Distribution", "normal"); %! [~, bint] = regress (yn, [ones(16, 1), X], 0.05); %! se_reg = (bint(:,1) - bint(:,2)) / 2 / tinv (0.025, 12); %! assert_equal (mdl.Coefficients.SE, se_reg, 1e-9); %!test # normal-GLM dispersion is estimated as SSE/DFE; log-likelihood closed form %! mdl = fitglm (X, yn, "Distribution", "normal"); %! rss = mdl.Deviance; %! assert_equal (mdl.DispersionEstimated, true); %! assert_equal (mdl.Dispersion, rss / mdl.DFE, 1e-12); %! assert_equal (mdl.LogLikelihood, -8 * (log (2 * pi * rss / 16) + 1), 1e-6); %!test # Poisson coefficients and fit statistics (verified against MATLAB) %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! assert_equal (mdl.Coefficients.Estimate, ... %! [-0.3420955; 1.2804868; -1.0743272; 0.8395779], 1e-6); %! assert_equal (mdl.Deviance, 7.403008, 1e-5); %! assert_equal (mdl.LogLikelihood, -18.543280, 1e-5); %! assert_equal (mdl.ModelCriterion.AIC, 45.086559, 1e-5); %! assert_equal (mdl.Rsquared.Deviance, 0.6627677, 1e-6); %!test # scalar count/size properties of the Poisson fit %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! assert_equal (mdl.NumCoefficients, 4); %! assert_equal (mdl.NumEstimatedCoefficients, 4); %! assert_equal (mdl.NumPredictors, 3); %! assert_equal (mdl.NumObservations, 16); %! assert_equal (mdl.DFE, 12); %! assert_equal (mdl.ResponseName, "y"); %! assert_equal (mdl.CoefficientNames, {'(Intercept)', 'x1', 'x2', 'x3'}); %!test # Poisson has a fixed unit dispersion (not estimated) %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! assert_equal (mdl.Dispersion, 1); %! assert_equal (mdl.DispersionEstimated, false); %! assert_equal (mdl.Distribution.Name, "Poisson"); %! assert_equal (mdl.Link.Name, "log"); %!test # the coefficient covariance is symmetric with SE^2 on its diagonal %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! C = mdl.CoefficientCovariance; %! assert_equal (size (C), [4, 4]); %! assert_equal (C, C', 1e-14); %! assert_equal (diag (C), mdl.Coefficients.SE .^ 2, 1e-12); %!test # predict at the training data reproduces the fitted response %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! [yhat, yci] = predict (mdl, X); %! assert_equal (yhat, mdl.Fitted.Response, 1e-10); %! assert_equal (size (yci), [16, 2]); %! assert_equal (all (yci(:,1) <= yhat & yhat <= yci(:,2)), true); %!test # feval evaluates the model and agrees with predict %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! assert_equal (feval (mdl, X(:,1), X(:,2), X(:,3)), predict (mdl, X), 1e-12); %!test # coefCI matches the t-interval and honours a custom alpha %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! b = mdl.Coefficients.Estimate; se = mdl.Coefficients.SE; %! t95 = tinv (0.975, mdl.DFE); %! assert_equal (coefCI (mdl), [b - t95 * se, b + t95 * se], 1e-12); %! t90 = tinv (0.95, mdl.DFE); %! assert_equal (coefCI (mdl, 0.10), [b - t90 * se, b + t90 * se], 1e-12); %!test # coefTest gives the Wald F statistic against the constant model %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! [p, F, df] = coefTest (mdl); %! assert_equal (F, 3.685312, 1e-5); %! assert_equal (p, 0.04331745, 1e-7); %! assert_equal (df, 3); %!test %! ## devianceTest, gamma, the dispersion estimated; values from MATLAB R2024a %! u = (1:30)'; %! yg = exp (0.1 * u) .* (1 + 0.2 * sin (u)); %! D = devianceTest (fitglm (u, yg, 'Distribution', 'gamma')); %! assert_equal ([D.FStat(2), D.pValue(2)], ... %! [165.431140860884, 2.85011734507204e-13], -1e-12); %!test %! ## devianceTest, inverse Gaussian with a log link %! u = (1:30)'; %! yg = exp (0.1 * u) .* (1 + 0.2 * sin (u)); %! D = devianceTest (fitglm (u, yg, 'Distribution', 'inverse gaussian', ... %! 'Link', 'log')); %! assert_equal ([D.FStat(2), D.pValue(2)], ... %! [704.235449350226, 2.17509777893856e-21], -1e-12); %!test %! ## devianceTest, Poisson with an estimated dispersion %! u = (1:30)'; %! yc = round (5 * exp (0.05 * u) .* (1 + 0.3 * sin (u))); %! D = devianceTest (fitglm (u, yc, 'Distribution', 'poisson', ... %! 'DispersionFlag', true)); %! assert_equal ([D.FStat(2), D.pValue(2)], ... %! [91.127051941481, 2.65696346522055e-10], -1e-12); %!test %! ## devianceTest, binomial with an estimated dispersion %! u = (1:30)'; %! N = 40 * ones (30, 1); %! ybn = round (N ./ (1 + exp (3 - 0.15 * u + 0.8 * sin (u)))); %! D = devianceTest (fitglm (u, [ybn, N], 'Distribution', 'binomial', ... %! 'DispersionFlag', true)); %! assert_equal ([D.FStat(2), D.pValue(2)], ... %! [144.333237422448, 1.44965237553984e-12], -1e-12); %!test %! ## devianceTest, Poisson, the dispersion fixed %! u = (1:30)'; %! yc = round (5 * exp (0.05 * u) .* (1 + 0.3 * sin (u))); %! D = devianceTest (fitglm (u, yc, 'Distribution', 'poisson')); %! assert_equal ([D.chi2Stat(2), D.pValue(2)], ... %! [54.3584716312125, 1.67056245094831e-13], -1e-12); %!test %! ## devianceTest, normal with the dispersion fixed %! u = (1:30)'; %! yg = exp (0.1 * u) .* (1 + 0.2 * sin (u)); %! D = devianceTest (fitglm (u, yg, 'Distribution', 'normal', ... %! 'DispersionFlag', false)); %! assert_equal ([D.chi2Stat(2), D.pValue(2)], ... %! [713.497289579326, 3.47271763963398e-157], -1e-12); %!test # devianceTest chi-square equals the drop from the null deviance %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! dt = devianceTest (mdl); %! assert_equal (class (dt), "table"); %! assert_equal (dt.chi2Stat(2), dt.Deviance(1) - dt.Deviance(2), 1e-10); %!test # information criteria satisfy their defining identities %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! k = mdl.NumEstimatedCoefficients; ll = mdl.LogLikelihood; %! assert_equal (mdl.ModelCriterion.AIC, -2 * ll + 2 * k, 1e-9); %! assert_equal (mdl.ModelCriterion.BIC, -2 * ll + k * log (16), 1e-9); %!test # raw residuals are response minus fit; random draws match the response size %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! assert_equal (mdl.Residuals.Raw, yp - mdl.Fitted.Response, 1e-12); %! ysim = random (mdl); %! assert_equal (size (ysim), [16, 1]); %! assert_equal (all (ysim == round (ysim) & ysim >= 0), true); %!test # binomial/logistic fit: coefficients agree with the glmfit engine %! mdl = fitglm (X, yb, "Distribution", "binomial"); %! assert_equal (mdl.Coefficients.Estimate, glmfit (X, yb, "binomial"), 1e-8); %! assert_equal (mdl.Link.Name, "logit"); %! assert_equal (all (mdl.Fitted.Response >= 0 & mdl.Fitted.Response <= 1), true); %! assert_equal (mdl.Deviance, 10.997099, 1e-5); %!test # an interaction model adds the cross term and one coefficient %! mdl = fitglm (X, yp, "interactions", "Distribution", "poisson"); %! assert_equal (any (strcmp (mdl.CoefficientNames, "x1:x2")), true); %! assert_equal (mdl.NumCoefficients, 7); %!test # an offset is stored and applied %! mdl = fitglm (X, yp, "Distribution", "poisson", "Offset", log (2 * ones (16, 1))); %! assert_equal (numel (mdl.Offset), 16); %!test # disp prints the model header and the coefficient table %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! s = evalc ("disp (mdl)"); %! assert_equal (isempty (strfind (s, "Generalized linear regression model")), false); %! assert_equal (isempty (strfind (s, "Estimate")), false); %!test # chained subsref reaches property -> table column -> element %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! assert_equal (numel (mdl.Coefficients.Estimate), 4); %! assert_equal (mdl.Coefficients.Estimate(1), -0.3420955, 1e-6); %! assert_equal (mdl.Coefficients.Estimate(2), mdl.Coefficients{2, "Estimate"}, 1e-12); %!test # the diagnostic and effect plots run without error %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! hf = figure ("visible", "off"); %! unwind_protect %! plotResiduals (mdl); %! plotResiduals (mdl, "fitted", "ResidualType", "Pearson"); %! plotDiagnostics (mdl); %! plotDiagnostics (mdl, "cookd"); %! plotEffects (mdl); %! plotAdjustedResponse (mdl, 1); %! plotAdded (mdl, "x2"); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## The property surface below is checked against MATLAB R2024a on the same data. %!test # each family reports its MATLAB display name and its own functions %! names = {'Normal', 'Binomial', 'Poisson', 'Gamma', 'Inverse Gaussian'}; %! dists = {'normal', 'binomial', 'poisson', 'gamma', 'inverse gaussian'}; %! resp = {yn, yb, yp, abs(yn) + 1, abs(yn) + 1}; %! for k = 1:numel (dists) %! mdl = fitglm (X, resp{k}, "Distribution", dists{k}); %! assert_equal (mdl.Distribution.Name, names{k}); %! dev = mdl.Distribution.DevianceFunction; %! var = mdl.Distribution.VarianceFunction; %! assert_equal (is_function_handle (dev), true); %! assert_equal (is_function_handle (var), true); %! endfor %!test # the variance function is the family's variance, not its scale %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! v = mdl.Distribution.VarianceFunction; %! assert_equal (v ([1; 4; 9]), [1; 4; 9]); %! mdl = fitglm (X, abs (yn) + 1, "Distribution", "gamma"); %! v = mdl.Distribution.VarianceFunction; %! assert_equal (v ([1; 2; 3]), [1; 4; 9]); %!test # the deviance function reproduces the reported deviance %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! d = mdl.Distribution.DevianceFunction; %! assert_equal (sum (d (mdl.Fitted.Response, yp)), mdl.Deviance, 1e-10); %!test # sums of squares match MATLAB and reproduce Rsquared.Ordinary %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! assert_equal (mdl.SSE, 5.878748587925331, 1e-12); %! assert_equal (mdl.SSR, 22.693546014008586, 1e-11); %! assert_equal (mdl.SST, 30, 1e-12); %! assert_equal (mdl.Rsquared.Ordinary, 1 - mdl.SSE / mdl.SST, 1e-14); %!test # SSE + SSR closes to SST only for the identity link %! mdl = fitglm (X, yn); %! assert_equal (mdl.SSE, 1.007522513007451, 1e-12); %! assert_equal (mdl.SSR, 20.549977486992557, 1e-11); %! assert_equal (mdl.SST, 21.557500000000005, 1e-12); %! assert_equal (mdl.SSE + mdl.SSR, mdl.SST, 1e-12); %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! assert_equal (mdl.SSE + mdl.SSR < mdl.SST, true); %!test # binomial and gamma sums of squares %! mdl = fitglm (X, yb, "Distribution", "binomial"); %! assert_equal ([mdl.SSE, mdl.SSR, mdl.SST], ... %! [1.529775270428043, 2.016466154627350, 3.75], 1e-11); %! mdl = fitglm (X, abs (yn) + 1, "Distribution", "gamma"); %! assert_equal ([mdl.SSE, mdl.SSR, mdl.SST], ... %! [3.537122914639216, 5.173700107311616, 9.45], 1e-9); %!test # counts, penalty, and observation names %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! assert_equal (mdl.NumVariables, 4); %! assert_equal (char (mdl.LikelihoodPenalty), "none"); %! assert_equal (mdl.ObservationNames, {}); %! assert_equal (size (mdl.Steps), [0, 0]); %!test # Variables holds the data the model was built from %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! assert_equal (class (mdl.Variables), "table"); %! assert_equal (size (mdl.Variables), [16, 4]); %! assert_equal (mdl.Variables.Properties.VariableNames, ... %! {'x1', 'x2', 'x3', 'y'}); %! assert_equal (mdl.Variables{:, 'x2'}, X(:,2)); %! assert_equal (mdl.Variables{:, 'y'}, yp); %!test # ObservationInfo spans the input rows and records why each was used %! mdl = fitglm (X, yn); %! assert_equal (size (mdl.ObservationInfo), [16, 4]); %! assert_equal (mdl.ObservationInfo.Properties.VariableNames, ... %! {'Weights', 'Excluded', 'Missing', 'Subset'}); %! assert_equal (mdl.ObservationInfo.Weights, ones (16, 1)); %! assert_equal (any (mdl.ObservationInfo.Excluded), false); %! assert_equal (all (mdl.ObservationInfo.Subset), true); %!test # excluded rows keep their weight and drop out of the fit %! mdl = fitglm (X, yn, "Exclude", [2 5], "Weights", (1:16)' / 16); %! assert_equal (mdl.NumObservations, 14); %! assert_equal (mdl.DFE, 10); %! assert_equal (mdl.ObservationInfo.Weights, (1:16)' / 16); %! assert_equal (find (mdl.ObservationInfo.Excluded), [2; 5]); %! assert_equal (any (mdl.ObservationInfo.Missing), false); %! assert_equal (find (! mdl.ObservationInfo.Subset), [2; 5]); %! assert_equal (mdl.SSE, 0.376329223083291, 1e-11); %! assert_equal (mdl.SST, 12.408120155038763, 1e-10); %! assert_equal (mdl.Dispersion, 0.037632922308329, 1e-12); %!test # an excluded row still gets a fitted value; a missing row does not %! mdl = fitglm (X, yn, "Exclude", [2 5], "Weights", (1:16)' / 16); %! assert_equal (size (mdl.Fitted), [16, 2]); %! assert_equal (mdl.Fitted.Response(1:3), ... %! [1.452778236704902; -0.409857651323262; 1.021905387197724], 1e-11); %! assert_equal (mdl.Residuals.Raw(2), 0.109857651323262, 1e-11); %! Xm = X; Xm(3,2) = NaN; %! mdl = fitglm (Xm, yn); %! assert_equal (mdl.NumObservations, 15); %! assert_equal (find (mdl.ObservationInfo.Missing), 3); %! assert_equal (isnan (mdl.Fitted.Response(3)), true); %! assert_equal (isnan (mdl.Residuals.Raw(3)), true); %! assert_equal (mdl.Fitted.Response(1), 1.675087141815260, 1e-11); %! assert_equal (mdl.SSE, 0.990693655817047, 1e-11); %!test # the binomial family adds a Probability column to Fitted %! mdl = fitglm (X, yb, "Distribution", "binomial"); %! assert_equal (mdl.Fitted.Properties.VariableNames, ... %! {'Response', 'LinearPredictor', 'Probability'}); %! assert_equal (mdl.Fitted.Probability(1), 0.998111791212415, 1e-9); %! assert_equal (mdl.Fitted.Response, mdl.Fitted.Probability, 1e-14); %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! assert_equal (mdl.Fitted.Properties.VariableNames, ... %! {'Response', 'LinearPredictor'}); %!test # with BinomialSize the response is a count and so is the fit %! N = 5 * ones (16, 1); %! y = [3 4 5 1 0 4 3 5 4 5 5 1 0 2 1 5]'; %! mdl = fitglm (X, y, "Distribution", "binomial", "BinomialSize", N); %! assert_equal (mdl.Fitted.Response, N .* mdl.Fitted.Probability, 1e-12); %! assert_equal (mdl.Residuals.Raw, y - mdl.Fitted.Response, 1e-12); %!test # a two-column response and BinomialSize describe the same model %! N = 5 * ones (16, 1); %! y = [3 4 5 1 0 4 3 5 4 5 5 1 0 2 1 5]'; %! m1 = fitglm (X, [y, N], "Distribution", "binomial"); %! m2 = fitglm (X, y, "Distribution", "binomial", "BinomialSize", N); %! assert_equal (m1.Coefficients.Estimate, m2.Coefficients.Estimate, 1e-12); %! assert_equal (m1.Deviance, m2.Deviance, 1e-12); %!test # the two-column fit matches MATLAB R2024a %! x = (1:10)'; %! S = [0 1 1 2 3 4 6 7 9 9]'; %! mdl = fitglm (x, [S, 10 * ones(10, 1)], "Distribution", "binomial"); %! assert_equal (mdl.Coefficients.Estimate, ... %! [-4.07619632416318; 0.639874139429377], 1e-10); %! assert_equal (mdl.Deviance, 1.37328920133713, 1e-10); %! assert_equal (mdl.LogLikelihood, -11.0853057442458, 1e-10); %! assert_equal (mdl.Fitted.Probability(1), 0.0311793894382727, 1e-10); %! assert_equal (mdl.Fitted.Response(1), 0.311793894382727, 1e-10); %! assert_equal (mdl.Residuals.Raw(1), -0.311793894382727, 1e-10); %!test # a two-column fit over the shared predictors matches MATLAB R2024a %! N = 5 * ones (16, 1); %! y = [3 4 5 1 0 4 3 5 4 5 5 1 0 2 1 5]'; %! mdl = fitglm (X, [y, N], "Distribution", "binomial"); %! assert_equal (mdl.Coefficients.Estimate, ... %! [-0.130914595525068; 2.08707009838652; ... %! -0.640296111095014; 0.746404482784674], 1e-10); %! assert_equal (mdl.Deviance, 30.7821361135612, 1e-10); %! assert_equal (mdl.LogLikelihood, -23.9339330144783, 1e-10); %! assert_equal (mdl.Fitted.Response(1), 4.35876915277425, 1e-10); %! assert_equal (mdl.Residuals.Raw(1), -1.35876915277425, 1e-10); %!test # the trials given with the response win over BinomialSize %! x = (1:10)'; %! S = [0 1 1 2 3 4 6 7 9 9]'; %! N = 10 * ones (10, 1); %! mdl = fitglm (x, [S, N], "Distribution", "binomial", "BinomialSize", (2:2:20)'); %! assert_equal (mdl.Coefficients.Estimate, ... %! [-4.07619632416318; 0.639874139429377], 1e-10); %!test # a binomial prediction is a probability, not a count %! x = (1:10)'; %! S = [0 1 1 2 3 4 6 7 9 9]'; %! mdl = fitglm (x, [S, 10 * ones(10, 1)], "Distribution", "binomial"); %! assert_equal (predict (mdl, [2; 5; 8]), ... %! [0.0575164189082781; 0.293836018581318; ... %! 0.739389288247498], 1e-10); %!test # Variables reports the successes, not the proportion fitted %! x = (1:10)'; %! S = [0 1 1 2 3 4 6 7 9 9]'; %! mdl = fitglm (x, [S, 10 * ones(10, 1)], "Distribution", "binomial"); %! assert_equal (mdl.Variables{:, 'y'}, S); %!test # a missing success count drops its observation %! x = (1:10)'; %! S = [0 1 NaN 2 3 4 6 7 9 9]'; %! mdl = fitglm (x, [S, 10 * ones(10, 1)], "Distribution", "binomial"); %! assert_equal (mdl.NumObservations, 9); %!test # the successes need not be whole numbers, as in MATLAB %! x = (1:10)'; %! S = [0 1 1 2 3 4 6 7 9 9]' + 0.5; %! mdl = fitglm (x, [S, 10 * ones(10, 1)], "Distribution", "binomial"); %! assert_equal (mdl.Coefficients.Estimate, ... %! [-3.55186660705826; 0.609271573628808], 1e-10); %!warning ... %! N = 5 * ones (16, 1); %! fitglm (X, [3 4 5 1 0 4 3 5 4 5 5 1 0 2 1 5]' ./ N, ... %! "Distribution", "binomial", "BinomialSize", N); %!error ... %! fitglm ((1:4)', [1 1 1 1]', "Distribution", "binomial", ... %! "BinomialSize", [2 2 2 2.5]') %!error ... %! fitglm ((1:4)', [1 1 1 1; 2 2 2 0]', "Distribution", "binomial") %!error ... %! fitglm ((1:4)', [1 1 1 5; 2 2 2 2]', "Distribution", "binomial") %!error ... %! fitglm ((1:4)', [1 1 1 1]', "Distribution", "binomial", ... %! "BinomialSize", [2 2]') %!error ... %! fitglm ((1:4)', [1 1 1 1; 2 2 2 2; 3 3 3 3]', "Distribution", "binomial") %!error ... %! fitglm ((1:4)', [1 1 1 1; 2 2 2 2]', "Distribution", "poisson") %!test # residuals gain the linear-predictor column, in MATLAB's order %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! assert_equal (mdl.Residuals.Properties.VariableNames, ... %! {'Raw', 'LinearPredictor', 'Pearson', 'Anscombe', 'Deviance'}); %! assert_equal (mdl.Residuals.LinearPredictor(1), 0.066685156329760, 1e-12); %! ## For the log link the working residual is the raw one over the mean. %! assert_equal (mdl.Residuals.LinearPredictor, ... %! mdl.Residuals.Raw ./ mdl.Fitted.Response, 1e-12); %!test # the identity link leaves the linear-predictor residual raw %! mdl = fitglm (X, yn); %! assert_equal (mdl.Residuals.LinearPredictor, mdl.Residuals.Raw, 1e-14); %! mdl = fitglm (X, yb, "Distribution", "binomial"); %! assert_equal (mdl.Residuals.LinearPredictor(1), 1.001891780864838, 1e-7); %!test # leverage, Cook's distance, and the hat matrix %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! assert_equal (mdl.Diagnostics.Properties.VariableNames, ... %! {'Leverage', 'CooksDistance', 'HatMatrix'}); %! assert_equal (mdl.Diagnostics.Leverage(1), 0.768034312650850, 1e-7); %! assert_equal (mdl.Diagnostics.CooksDistance(1), 0.074381550609974, 1e-7); %! assert_equal (size (mdl.Diagnostics.HatMatrix), [16, 16]); %! assert_equal (diag (mdl.Diagnostics.HatMatrix), mdl.Diagnostics.Leverage, ... %! 1e-14); %! assert_equal (sum (mdl.Diagnostics.Leverage), mdl.NumCoefficients, 1e-8); %!test # the GLM hat matrix is the asymmetric IRLS form, so H(i,j) != H(j,i) %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! H = mdl.Diagnostics.HatMatrix; %! assert_equal (H(1,2), 0.198729950284820, 1e-7); %! assert_equal (H(2,1), 0.334913252272241, 1e-7); %! ## The identity link with unit weights makes it symmetric again. %! H = fitglm (X, yn).Diagnostics.HatMatrix; %! assert_equal (H, H', 1e-12); %!test # normal-family diagnostics match MATLAB %! mdl = fitglm (X, yn); %! assert_equal (mdl.Diagnostics.Leverage(1:3), ... %! [0.392783227704423; 0.308688028422869; 0.103894702431155], 1e-12); %! assert_equal (mdl.Diagnostics.CooksDistance(1), 0.562165139840681, 1e-11); %!test # rows outside the fit have no leverage and no Cook's distance %! mdl = fitglm (X, yn, "Exclude", [2 5], "Weights", (1:16)' / 16); %! assert_equal (mdl.Diagnostics.Leverage([2, 5]), [0; 0]); %! assert_equal (isnan (mdl.Diagnostics.CooksDistance([2, 5])), [true; true]); %! assert_equal (mdl.Diagnostics.Leverage(1), 0.116937877466335, 1e-11); %! assert_equal (mdl.Diagnostics.CooksDistance(1), 0.026081406006346, 1e-10); %! assert_equal (mdl.Diagnostics.HatMatrix([2, 5], :), zeros (2, 16)); %!test # VariableInfo carries the class and range of every variable %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! assert_equal (mdl.VariableInfo.Properties.VariableNames, ... %! {'Class', 'Range', 'InModel', 'IsCategorical'}); %! assert_equal (size (mdl.VariableInfo), [4, 4]); %! assert_equal (mdl.VariableInfo.Range{1}, [-1.17, 2.09], 1e-14); %! assert_equal (mdl.VariableInfo.Range{2}, [-2.74, 1.33], 1e-14); %! assert_equal (mdl.VariableInfo.Range{4}, [0, 5]); %! assert_equal (mdl.VariableInfo.InModel, [true; true; true; false]); %! assert_equal (mdl.VariableInfo.IsCategorical, false (4, 1)); %! assert_equal (mdl.VariableInfo.Class, repmat ({'double'}, 4, 1)); %!test # a range spans the fitted rows, not the whole variable, as in MATLAB %! mdl = fitglm (X, yn, "Exclude", [1, 10]); %! assert_equal (mdl.VariableInfo.Range{1}, [-1.17, 2.04], 1e-14); %! assert_equal (mdl.VariableInfo.Range{4}, [-1.3, 2.5], 1e-14); %! ## the excluded rows carry the extremes that no longer appear %! assert_equal ([min(X(:,1)), max(X(:,1))], [-1.17, 2.09], 1e-14); %! assert_equal ([min(yn), max(yn)], [-1.3, 2.9], 1e-14); %!test # a category appearing only in excluded rows drops out of the range %! u = (1:20)'; %! g = categorical ([repmat({'a'}, 5, 1); repmat({'b'}, 5, 1); ... %! repmat({'c'}, 5, 1); repmat({'d'}, 5, 1)]); %! resp = u + 0.5; %! mdl = fitglm (table (u, g, resp), 'resp ~ u + g', 'Exclude', (16:20)'); %! assert_equal (cellstr (mdl.VariableInfo.Range{2}), {'a', 'b', 'c'}); %! assert_equal (mdl.VariableInfo.Range{1}, [1, 15], 1e-14); %!test # the offset spans the input rows and is zero when none was given %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! assert_equal (mdl.Offset, zeros (16, 1)); %! mdl = fitglm (X, yp, "Distribution", "poisson", ... %! "Offset", log (2 * ones (16, 1))); %! assert_equal (mdl.Offset, log (2) * ones (16, 1), 1e-14); %!test # the formula is a structure describing the model over its variables %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! f = mdl.Formula; %! assert_equal (f.ResponseName, "y"); %! assert_equal (f.LinearPredictor, "1 + x1 + x2 + x3"); %! assert_equal (f.PredictorNames, {'x1', 'x2', 'x3'}); %! assert_equal (f.VariableNames, {'x1', 'x2', 'x3', 'y'}); %! assert_equal (f.TermNames, {'(Intercept)'; 'x1'; 'x2'; 'x3'}); %! assert_equal (f.Terms, [0 0 0 0; 1 0 0 0; 0 1 0 0; 0 0 1 0]); %! assert_equal (f.Link, "log"); %! assert_equal (f.InModel, [true, true, true, false]); %! assert_equal (f.HasIntercept, true); %! assert_equal ([f.NTerms, f.NVars, f.NPredictors], [4, 4, 3]); %! assert_equal (f.FunctionCalls, cell (1, 0)); %! assert_equal (f.ModelFun ([1; 2], [1, 3]), 7); %!test # a term whose parts are all present is written as a product %! f = fitglm (X, yn, "interactions").Formula; %! assert_equal (f.LinearPredictor, "1 + x1*x2 + x1*x3 + x2*x3"); %! assert_equal (f.NTerms, 7); %! assert_equal (f.TermNames, ... %! {'(Intercept)'; 'x1'; 'x2'; 'x3'; 'x1:x2'; 'x1:x3'; 'x2:x3'}); %! f = fitglm (X(:,1:2), yn, "quadratic").Formula; %! assert_equal (f.LinearPredictor, "1 + x1*x2 + x1^2 + x2^2"); %! assert_equal (f.TermNames, ... %! {'(Intercept)'; 'x1'; 'x2'; 'x1:x2'; 'x1^2'; 'x2^2'}); %! assert_equal (f.Terms, [0 0 0; 1 0 0; 0 1 0; 1 1 0; 2 0 0; 0 2 0]); %!test # only pairs collapse into a product, never a three-way interaction %! f = fitglm (X, yn, "full").Formula; %! assert_equal (f.LinearPredictor, "1 + x1*x2 + x1*x3 + x2*x3 + x1:x2:x3"); %!test # dropping the intercept drops the leading 1 %! f = fitglm (X, yn, "Intercept", false).Formula; %! assert_equal (f.LinearPredictor, "x1 + x2 + x3"); %! assert_equal (f.HasIntercept, false); %! assert_equal (f.Terms, [1 0 0 0; 0 1 0 0; 0 0 1 0]); %!test # disp names the link alongside the response %! s = evalc ("disp (fitglm (X, yp, 'Distribution', 'poisson'))"); %! assert_equal (isempty (strfind (s, "log(y) ~ 1 + x1 + x2 + x3")), false); %! s = evalc ("disp (fitglm (X, yb, 'Distribution', 'binomial'))"); %! assert_equal (isempty (strfind (s, "logit(y) ~ 1 + x1 + x2 + x3")), false); %! s = evalc ("disp (fitglm (X, yn))"); %! assert_equal (isempty (strfind (s, "y ~ 1 + x1 + x2 + x3")), false); %!test # a numeric link is named by its exponent and shown as a power %! mdl = fitglm (X, abs (yn) + 1, "Distribution", "inverse gaussian"); %! assert_equal (mdl.Link.Name, "-2"); %! s = evalc ("disp (mdl)"); %! assert_equal (isempty (strfind (s, "power(y,-2) ~ 1 + x1")), false); %!test # a table model keeps every column but only models what it names %! tbl = array2table ([X, yn], "VariableNames", {'a', 'b', 'c', 'resp'}); %! mdl = fitglm (tbl, "resp ~ 1 + a + b"); %! assert_equal (mdl.NumPredictors, 2); %! assert_equal (mdl.NumVariables, 4); %! assert_equal (mdl.PredictorNames, {'a'; 'b'}); %! assert_equal (mdl.VariableNames, {'a'; 'b'; 'c'; 'resp'}); %! assert_equal (mdl.Formula.LinearPredictor, "1 + a + b"); %! assert_equal (mdl.Formula.InModel, [true, true, false, false]); %! assert_equal (mdl.VariableInfo.InModel, [true; true; false; false]); %! assert_equal (size (mdl.Variables), [16, 4]); %!test # a column the model does not use cannot cost an observation %! tbl = array2table ([X, yn], "VariableNames", {'a', 'b', 'c', 'resp'}); %! tbl.c(4) = NaN; %! mdl = fitglm (tbl, "resp ~ 1 + a + b"); %! assert_equal (mdl.NumObservations, 16); %! assert_equal (any (mdl.ObservationInfo.Missing), false); %!test # a grouping column is reported by its own class and its levels %! g = {'lo'; 'hi'; 'lo'; 'hi'; 'lo'; 'hi'; 'lo'; 'hi'; ... %! 'lo'; 'hi'; 'lo'; 'hi'; 'lo'; 'hi'; 'lo'; 'hi'}; %! tbl = table (X(:,1), g, yn, "VariableNames", {'u', 'grp', 'resp'}); %! mdl = fitglm (tbl, "resp ~ 1 + u + grp"); %! assert_equal (mdl.VariableInfo.Class, {'double'; 'cell'; 'double'}); %! assert_equal (mdl.VariableInfo.IsCategorical, [false; true; false]); %! ## The levels are listed as the design codes them: first seen first. %! assert_equal (mdl.VariableInfo.Range{2}, {'lo', 'hi'}); %! assert_equal (mdl.NumPredictors, 2); %! assert_equal (mdl.NumVariables, 3); %! ## The indicator columns fold back onto the variable they came from. %! assert_equal (mdl.Formula.NTerms, 3); %! assert_equal (sort (mdl.Formula.TermNames), {'(Intercept)'; 'grp'; 'u'}); %!test # devianceTest labels both rows with their rendered formula %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! dt = devianceTest (mdl); %! assert_equal (dt.Properties.RowNames, ... %! {'log(y) ~ 1'; 'log(y) ~ 1 + x1 + x2 + x3'}); %! mdl = fitglm (X, yn); %! dt = devianceTest (mdl); %! assert_equal (dt.Properties.RowNames, ... %! {'y ~ 1'; 'y ~ 1 + x1 + x2 + x3'}); %!test # a power keeps its exponent in the variable's own column %! u = [1;2;3;4;5;6;7;8;9;10;11;12]; %! v = [2;1;4;3;6;5;8;7;10;9;12;11]; %! cnt = [2;5;3;7;4;6;8;3;5;9;4;6]; %! tbl = table (u, v, cnt); %! f = fitglm (tbl, 'cnt ~ 1 + u*v + u^2', 'Distribution', 'poisson').Formula; %! assert_equal (char (f), 'log(cnt) ~ 1 + u*v + u^2'); %! assert_equal (f.Terms, [0 0 0; 1 0 0; 0 1 0; 1 1 0; 2 0 0]); %! assert_equal (f.TermNames, {'(Intercept)'; 'u'; 'v'; 'u:v'; 'u^2'}); %!test # an interaction survives when one factor is not a main effect %! u = [1;2;3;4;5;6;7;8;9;10;11;12]; %! v = [2;1;4;3;6;5;8;7;10;9;12;11]; %! cnt = [2;5;3;7;4;6;8;3;5;9;4;6]; %! tbl = table (u, v, cnt); %! f = fitglm (tbl, 'cnt ~ 1 + u + u:v', 'Distribution', 'poisson').Formula; %! assert_equal (char (f), 'log(cnt) ~ 1 + u + u:v'); %! assert_equal (f.Terms, [0 0 0; 1 0 0; 1 1 0]); %!test # dropping the intercept codes the first categorical in full, both paths %! u = [1;2;3;4;5;6;7;8;9;10;11;12]; %! h2 = {'b';'c';'a';'b';'c';'a';'b';'c';'a';'b';'c';'a'}; %! cnt = [2;5;3;7;4;6;8;3;5;9;4;6]; %! tbl = table (u, h2, cnt); %! m = fitglm (tbl, 'cnt ~ h2 - 1', 'Distribution', 'poisson'); %! assert_equal (m.CoefficientNames, {'h2_b', 'h2_c', 'h2_a'}); %! m = fitglm (tbl, 'linear', 'Distribution', 'poisson', 'Intercept', false); %! assert_equal (m.CoefficientNames, {'u', 'h2_b', 'h2_c', 'h2_a'}); statistics-release-2.1.2/inst/Regression/LinearFormula.m000066400000000000000000000557431527311104300233520ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftp {statistics} LinearFormula ## ## Model formula of a linear or generalized linear regression. ## ## A @qcode{LinearFormula} object describes the terms of a fitted model: which ## variables the model draws on, how they combine into terms, and how the whole ## thing reads back as a formula. It is the class of the @code{Formula} ## property of a @code{LinearModel} and of a @code{GeneralizedLinearModel}, and ## is normally obtained from a fitted model rather than built directly. ## ## The object is defined by its terms matrix and the names of the variables that ## matrix is written over; every other property is derived from those two. Each ## row of @code{Terms} is one term of the model and each column is one variable, ## the entry giving the power that variable carries in that term. An all-zero ## row is the intercept. The response variable occupies a column of its own, ## which is always zero. ## ## Converting the object with @code{char} renders the whole formula, response ## included, as @qcode{"y ~ 1 + x1 + x2"}; the @code{LinearPredictor} property ## holds the right-hand side on its own. For a generalized linear model the ## response carries its link function, as in @qcode{"logit(y) ~ 1 + x1"}. ## ## @end deftp classdef LinearFormula properties (SetAccess = private) ## -*- texinfo -*- ## @deftp {LinearFormula} {property} ResponseName ## ## Name of the response variable ## ## A character vector naming the variable on the left-hand side of the ## formula. This property is read-only. ## ## @end deftp ResponseName = ''; ## -*- texinfo -*- ## @deftp {LinearFormula} {property} VariableNames ## ## Names of all variables available to the model ## ## A cell array of character vectors naming every variable the model was ## given, whether or not it is used, in the order the data lists them. The ## response is included. This property is read-only. ## ## @end deftp VariableNames = {}; ## -*- texinfo -*- ## @deftp {LinearFormula} {property} PredictorNames ## ## Names of the variables the model actually uses ## ## A cell array of character vectors holding those elements of ## @code{VariableNames} that appear in at least one term. This property is ## read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {LinearFormula} {property} TermNames ## ## Name of each term of the model ## ## A column cell array of character vectors, one per row of @code{Terms}, ## naming the term over the model's @emph{variables}: the intercept is ## @qcode{"(Intercept)"}, an interaction joins its factors with a colon, and ## a power is written with a caret. A categorical variable contributes one ## term under its own name however many indicator columns it expands to, so ## these are not the coefficient names. This property is read-only. ## ## @end deftp TermNames = {}; ## -*- texinfo -*- ## @deftp {LinearFormula} {property} Terms ## ## Terms matrix of the model ## ## A numeric matrix with one row per term and one column per variable of ## @code{VariableNames}, each entry giving the power that variable carries ## in that term. An all-zero row is the intercept. This property is ## read-only. ## ## @end deftp Terms = []; ## -*- texinfo -*- ## @deftp {LinearFormula} {property} InModel ## ## Which variables take part in the model ## ## A logical row vector with one element per variable of ## @code{VariableNames}, true where that variable appears in at least one ## term. The response is false. This property is read-only. ## ## @end deftp InModel = logical ([]); ## -*- texinfo -*- ## @deftp {LinearFormula} {property} HasIntercept ## ## Whether the model carries an intercept ## ## A logical scalar, true when @code{Terms} holds an all-zero row. This ## property is read-only. ## ## @end deftp HasIntercept = false; ## -*- texinfo -*- ## @deftp {LinearFormula} {property} LinearPredictor ## ## Right-hand side of the formula ## ## A character vector rendering the model's terms, such as ## @qcode{"1 + x1 + x2"}. A pair of variables appearing both on their own ## and as an interaction is written as a product, so that ## @qcode{"x1 + x2 + x1:x2"} reads @qcode{"x1*x2"}. This property is ## read-only. ## ## @end deftp LinearPredictor = ''; ## -*- texinfo -*- ## @deftp {LinearFormula} {property} Link ## ## Link function applied to the response ## ## The link of a generalized linear model, given as its name, its numeric ## exponent, or a structure of function handles. It is @qcode{"identity"} ## for a linear model. This property is read-only. ## ## @end deftp Link = 'identity'; ## -*- texinfo -*- ## @deftp {LinearFormula} {property} ModelFun ## ## Function computing the linear predictor ## ## A function handle taking the coefficient vector and the design matrix. ## This property is read-only. ## ## @end deftp ModelFun = @(b, X) X * b; ## -*- texinfo -*- ## @deftp {LinearFormula} {property} FunctionCalls ## ## Functions called from within the formula ## ## A cell array of character vectors, empty unless the formula applies a ## function to a variable. This property is read-only. ## ## @end deftp FunctionCalls = cell (1, 0); ## -*- texinfo -*- ## @deftp {LinearFormula} {property} NTerms ## ## Number of terms in the model ## ## A non-negative integer, the number of rows of @code{Terms}. This ## property is read-only. ## ## @end deftp NTerms = 0; ## -*- texinfo -*- ## @deftp {LinearFormula} {property} NVars ## ## Number of variables available to the model ## ## A non-negative integer, the number of elements of ## @code{VariableNames}. This property is read-only. ## ## @end deftp NVars = 0; ## -*- texinfo -*- ## @deftp {LinearFormula} {property} NPredictors ## ## Number of variables the model uses ## ## A non-negative integer, the number of true elements of @code{InModel}. ## This property is read-only. ## ## @end deftp NPredictors = 0; endproperties methods (Hidden) function disp (this) fprintf ("%s\n", char (this)); endfunction function display (this) name = inputname (1); if (isempty (name)) name = 'ans'; endif fprintf ("%s = %s\n", name, char (this)); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {LinearFormula} {@var{obj} =} LinearFormula () ## @deftypefnx {LinearFormula} {@var{obj} =} LinearFormula (@var{terms}, @var{varnames}) ## @deftypefnx {LinearFormula} {@var{obj} =} LinearFormula (@var{terms}, @var{varnames}, @var{name}, @var{value}, @dots{}) ## ## Create a model formula from a terms matrix. ## ## @code{@var{obj} = LinearFormula ()} returns an empty formula. ## ## @code{@var{obj} = LinearFormula (@var{terms}, @var{varnames})} builds a ## formula whose terms matrix is @var{terms} and whose variables are named ## by the cell array of character vectors @var{varnames}. @var{terms} must ## have one column per element of @var{varnames}; each row is one term and ## each entry the power its variable carries in that term. An all-zero row ## is the intercept. ## ## The remaining properties are derived from these two arguments, except ## those given as @var{name}-@var{value} pairs: ## ## @multitable @columnfractions 0.25 0.75 ## @headitem @var{name} @tab @var{value} ## ## @item @qcode{"ResponseName"} @tab A character vector naming the response ## variable. It must be one of @var{varnames}. ## ## @item @qcode{"Link"} @tab The link function applied to the response, as a ## name, a numeric exponent, or a structure of function handles. It ## defaults to @qcode{"identity"}. ## ## @item @qcode{"ModelFun"} @tab A function handle computing the linear ## predictor from the coefficients and the design matrix. ## ## @item @qcode{"FunctionCalls"} @tab A cell array of character vectors ## naming functions the formula applies to its variables. ## @end multitable ## ## @end deftypefn function this = LinearFormula (varargin) if (nargin == 0) return; endif if (nargin < 2) error ("LinearFormula: TERMS and VARNAMES are both required."); endif terms = varargin{1}; varnames = varargin{2}; if (! isnumeric (terms) || ! isreal (terms) || ndims (terms) > 2) error ("LinearFormula: TERMS must be a real matrix."); endif if (! iscellstr (varnames)) error (strcat ("LinearFormula: VARNAMES must be a cell array of", ... " character vectors.")); endif if (! isempty (terms) && columns (terms) != numel (varnames)) error (strcat ("LinearFormula: TERMS must have one column per", ... " element of VARNAMES.")); endif if (mod (nargin - 2, 2) != 0) error ("LinearFormula: optional arguments must be name-value pairs."); endif respname = ''; for i = 3:2:nargin name = varargin{i}; if (! (ischar (name) && isrow (name))) error ("LinearFormula: option names must be character vectors."); endif switch (lower (name)) case 'responsename' respname = varargin{i+1}; if (! (ischar (respname) && (isrow (respname) ... || isempty (respname)))) error (strcat ("LinearFormula: 'ResponseName' must be a", ... " character vector.")); endif case 'link' this.Link = varargin{i+1}; case 'modelfun' this.ModelFun = varargin{i+1}; case 'functioncalls' this.FunctionCalls = varargin{i+1}; otherwise error ("LinearFormula: unknown option '%s'.", name); endswitch endfor varnames = varnames(:)'; if (! isempty (respname) && ! any (strcmp (varnames, respname))) error ("LinearFormula: '%s' is not one of VARNAMES.", respname); endif this.ResponseName = respname; this.VariableNames = varnames; this.Terms = terms; this.NVars = numel (varnames); this.NTerms = rows (terms); if (isempty (terms)) this.InModel = false (1, this.NVars); else this.InModel = any (terms != 0, 1); endif this.PredictorNames = varnames(this.InModel); this.NPredictors = sum (this.InModel); this.HasIntercept = ! isempty (terms) && any (all (terms == 0, 2)); this.TermNames = term_names (terms, varnames); this.LinearPredictor = predictor_string (terms, varnames, ... this.TermNames); endfunction ## -*- texinfo -*- ## @deftypefn {LinearFormula} {@var{str} =} char (@var{obj}) ## ## Render a model formula as a character vector. ## ## @code{@var{str} = char (@var{obj})} returns the whole formula, response ## included, as in @qcode{"y ~ 1 + x1 + x2"}. The response carries the ## link function of a generalized linear model, as in ## @qcode{"logit(y) ~ 1 + x1"}. ## ## @end deftypefn function str = char (this) if (isempty (this.LinearPredictor) && isempty (this.ResponseName)) str = ''; return; endif str = sprintf ("%s ~ %s", linked_response (this.ResponseName, ... this.Link), ... this.LinearPredictor); endfunction ## -*- texinfo -*- ## @deftypefn {LinearFormula} {@var{str} =} string (@var{obj}) ## ## Render a model formula as a string scalar. ## ## @code{@var{str} = string (@var{obj})} is the @code{string} counterpart of ## @code{char}. ## ## @end deftypefn function str = string (this) str = string (char (this)); endfunction endmethods endclassdef ## Name each row of a terms matrix over the model's variables. function names = term_names (terms, varnames) n_terms = rows (terms); names = cell (n_terms, 1); for t = 1:n_terms idx = find (terms(t, :) != 0); if (isempty (idx)) names{t} = '(Intercept)'; else parts = cell (1, numel (idx)); for k = 1:numel (idx) parts{k} = factor_string (varnames{idx(k)}, terms(t, idx(k))); endfor names{t} = strjoin (parts, ':'); endif endfor endfunction ## One factor of a term, carrying its power when that is not 1. function str = factor_string (name, power) if (power == 1) str = name; else str = sprintf ("%s^%d", name, power); endif endfunction ## Render the right-hand side of a formula. Two variables that appear both on ## their own and as their interaction are written as a product -- 'x1 + x2 + ## x1:x2' becomes 'x1*x2' -- and the main effects it absorbs are then left out. ## Only pairs collapse: a three-way interaction is written out as it stands even ## when every one of its sub-terms is present, and a power never collapses. function str = predictor_string (terms, varnames, names) n_terms = rows (terms); absorbed = false (n_terms, 1); rendered = names; for t = 1:n_terms idx = find (terms(t, :) != 0); if (numel (idx) != 2 || any (terms(t, idx) != 1)) continue; endif main = zeros (1, 2); for k = 1:2 row = zeros (1, columns (terms)); row(idx(k)) = 1; hit = find (all (terms == row, 2), 1); if (isempty (hit)) main = []; break; endif main(k) = hit; endfor if (isempty (main)) continue; endif rendered{t} = strjoin (varnames(idx), '*'); absorbed(main) = true; endfor parts = {}; for t = 1:n_terms if (absorbed(t)) continue; elseif (all (terms(t, :) == 0)) parts{end+1} = '1'; else parts{end+1} = rendered{t}; endif endfor if (isempty (parts)) str = ''; else str = strjoin (parts, ' + '); endif endfunction ## The response as the formula shows it, wrapped in its link function. function str = linked_response (respname, link) if (isempty (link) || (ischar (link) && strcmpi (link, 'identity'))) str = respname; elseif (ischar (link)) str = sprintf ("%s(%s)", link, respname); elseif (isnumeric (link) && isscalar (link)) ## The identity link written as an exponent leaves the response alone. if (link == 1) str = respname; else str = sprintf ("power(%s,%g)", respname, link); endif else str = sprintf ("link(%s)", respname); endif endfunction %!demo %! ## The Formula property of a fitted model is a LinearFormula object. %! x1 = [1 2 3 4 5 6 7 8]'; %! x2 = [2 1 4 3 6 5 8 7]'; %! y = [3.1 4.2 5.3 6.4 7.5 8.6 9.7 10.8]'; %! mdl = fitlm ([x1, x2], y, 'interactions'); %! f = mdl.Formula %! ## Rendering it as text gives back the whole formula, response included, %! ## while the LinearPredictor property holds the right-hand side alone. %! char (f) %! f.LinearPredictor %! ## A pair of variables present both on their own and as their interaction %! ## is written as a product. %! f.TermNames %! f.Terms %!demo %! ## A formula can also be built directly from a terms matrix. Each column is %! ## a variable and each entry the power it carries in that term; the all-zero %! ## row is the intercept and the response keeps a column of its own. %! terms = [0 0 0; 1 0 0; 0 1 0; 1 1 0]; %! f = LinearFormula (terms, {'dose', 'age', 'score'}, ... %! 'ResponseName', 'score') %! f.PredictorNames %! f.InModel ## Properties derived from the terms matrix %!test %! f = LinearFormula ([0 0 0; 1 0 0; 0 1 0], {'u', 'g', 'resp'}, ... %! 'ResponseName', 'resp'); %! assert_equal (f.ResponseName, 'resp'); %! assert_equal (f.VariableNames, {'u', 'g', 'resp'}); %! assert_equal (f.PredictorNames, {'u', 'g'}); %! assert_equal (f.TermNames, {'(Intercept)'; 'u'; 'g'}); %! assert_equal (f.Terms, [0 0 0; 1 0 0; 0 1 0]); %! assert_equal (f.InModel, [true, true, false]); %! assert_equal (f.HasIntercept, true); %! assert_equal (f.LinearPredictor, '1 + u + g'); %! assert_equal (f.NTerms, 3); %! assert_equal (f.NVars, 3); %! assert_equal (f.NPredictors, 2); %! assert_equal (f.Link, 'identity'); %! assert_equal (f.FunctionCalls, cell (1, 0)); %!test # the response takes no part in the model %! f = LinearFormula ([0 0 0 0; 1 0 0 0; 0 0 1 0], {'u', 'v', 'w', 'resp'}, ... %! 'ResponseName', 'resp'); %! assert_equal (f.InModel, [true, false, true, false]); %! assert_equal (f.PredictorNames, {'u', 'w'}); %! assert_equal (f.NPredictors, 2); %! assert_equal (f.NVars, 4); %!test # an empty formula is renderable %! f = LinearFormula (); %! assert_equal (char (f), ''); %! assert_equal (f.NTerms, 0); %! assert_equal (f.HasIntercept, false); ## Rendering %!test # char gives the whole formula, LinearPredictor the right-hand side %! f = LinearFormula ([0 0 0; 1 0 0; 0 1 0], {'u', 'g', 'resp'}, ... %! 'ResponseName', 'resp'); %! assert_equal (char (f), 'resp ~ 1 + u + g'); %! assert_equal (f.LinearPredictor, '1 + u + g'); %!test # a pair present on its own and as an interaction reads as a product %! f = LinearFormula ([0 0 0; 1 0 0; 0 1 0; 1 1 0], {'u', 'v', 'resp'}, ... %! 'ResponseName', 'resp'); %! assert_equal (char (f), 'resp ~ 1 + u*v'); %!test # a product needs both main effects; without one the term stands alone %! f = LinearFormula ([0 0 0; 1 0 0; 1 1 0], {'u', 'v', 'resp'}, ... %! 'ResponseName', 'resp'); %! assert_equal (char (f), 'resp ~ 1 + u + u:v'); %!test # only pairs collapse, never a three-way interaction %! terms = [0 0 0 0; 1 0 0 0; 0 1 0 0; 0 0 1 0; ... %! 1 1 0 0; 1 0 1 0; 0 1 1 0; 1 1 1 0]; %! f = LinearFormula (terms, {'u', 'v', 'w', 'resp'}, ... %! 'ResponseName', 'resp'); %! assert_equal (char (f), 'resp ~ 1 + u*v + u*w + v*w + u:v:w'); %!test # a power is never written as a product %! f = LinearFormula ([0 0 0; 1 0 0; 0 1 0; 1 1 0; 2 0 0; 0 2 0], ... %! {'u', 'v', 'resp'}, 'ResponseName', 'resp'); %! assert_equal (char (f), 'resp ~ 1 + u*v + u^2 + v^2'); %! assert_equal (f.TermNames, ... %! {'(Intercept)'; 'u'; 'v'; 'u:v'; 'u^2'; 'v^2'}); %!test # dropping the intercept drops the leading 1 %! f = LinearFormula ([1 0 0; 0 1 0], {'u', 'g', 'resp'}, ... %! 'ResponseName', 'resp'); %! assert_equal (char (f), 'resp ~ u + g'); %! assert_equal (f.HasIntercept, false); %!test # an intercept-only model %! f = LinearFormula ([0 0 0], {'u', 'g', 'resp'}, 'ResponseName', 'resp'); %! assert_equal (char (f), 'resp ~ 1'); %! assert_equal (f.TermNames, {'(Intercept)'}); %! assert_equal (f.NPredictors, 0); %! assert_equal (f.PredictorNames, cell (1, 0)); %!test # string renders the same text as char %! f = LinearFormula ([0 0; 1 0], {'u', 'resp'}, 'ResponseName', 'resp'); %! s = string (f); %! assert_equal (class (s), 'string'); %! assert_equal (char (s), char (f)); %!test # disp writes the rendered formula %! f = LinearFormula ([0 0; 1 0], {'u', 'resp'}, 'ResponseName', 'resp'); %! s = evalc ('disp (f)'); %! assert_equal (strtrim (s), 'resp ~ 1 + u'); ## The link wraps the response %!test # a named link %! f = LinearFormula ([0 0 0; 1 0 0; 0 1 0], {'u', 'g', 'yb'}, ... %! 'ResponseName', 'yb', 'Link', 'logit'); %! assert_equal (char (f), 'logit(yb) ~ 1 + u + g'); %! assert_equal (f.Link, 'logit'); %!test # the identity link leaves the response alone, named or as an exponent %! f = LinearFormula ([0 0; 1 0], {'u', 'y'}, 'ResponseName', 'y', ... %! 'Link', 'identity'); %! assert_equal (char (f), 'y ~ 1 + u'); %! f = LinearFormula ([0 0; 1 0], {'u', 'y'}, 'ResponseName', 'y', 'Link', 1); %! assert_equal (char (f), 'y ~ 1 + u'); %!test # a numeric link is shown as a power %! f = LinearFormula ([0 0; 1 0], {'u', 'y'}, 'ResponseName', 'y', 'Link', -2); %! assert_equal (char (f), 'power(y,-2) ~ 1 + u'); %!test # a link given as a structure of handles is named generically %! lk = struct ('Link', @(x) x, 'Derivative', @(x) 1, 'Inverse', @(x) x); %! f = LinearFormula ([0 0; 1 0], {'u', 'y'}, 'ResponseName', 'y', ... %! 'Link', lk); %! assert_equal (char (f), 'link(y) ~ 1 + u'); ## Remaining options %!test # ModelFun and FunctionCalls are carried through %! fun = @(b, X) X * b; %! f = LinearFormula ([0 0; 1 0], {'u', 'y'}, 'ResponseName', 'y', ... %! 'ModelFun', fun, 'FunctionCalls', {'log'}); %! assert_equal (func2str (f.ModelFun), func2str (fun)); %! assert_equal (f.FunctionCalls, {'log'}); %!test # option names are matched without regard to case %! f = LinearFormula ([0 0; 1 0], {'u', 'y'}, 'responsename', 'y', ... %! 'LINK', 'log'); %! assert_equal (char (f), 'log(y) ~ 1 + u'); ## Input validation %!error ... %! LinearFormula ([0 0]) %!error ... %! LinearFormula ('abc', {'u', 'y'}) %!error ... %! LinearFormula ([1+2i, 0], {'u', 'y'}) %!error ... %! LinearFormula ([0 0], 'uy') %!error ... %! LinearFormula ([0 0], {1, 2}) %!error ... %! LinearFormula ([0 0 0], {'u', 'y'}) %!error ... %! LinearFormula ([0 0], {'u', 'y'}, 'Link') %!error ... %! LinearFormula ([0 0], {'u', 'y'}, 5, 'log') %!error ... %! LinearFormula ([0 0], {'u', 'y'}, 'bogus', 1) %!error ... %! LinearFormula ([0 0], {'u', 'y'}, 'ResponseName', 5) %!error ... %! LinearFormula ([0 0], {'u', 'y'}, 'ResponseName', 'z') statistics-release-2.1.2/inst/Regression/LinearMixedModel.m000066400000000000000000001002121527311104300237520ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftp {statistics} LinearMixedModel ## ## Linear mixed-effects model fitted to data. ## ## A @code{LinearMixedModel} object represents a fitted linear mixed-effects ## model ## @tex ## $$ y = X\beta + Zb + \varepsilon, $$ ## ## @end tex ## @ifnottex ## @code{y = X*beta + Z*b + e}, ## @end ifnottex ## with fixed effects @var{beta}, random effects @var{b} distributed as ## @code{N(0, Psi)}, and independent errors @code{N(0, sigma2)}. Objects are ## created with @code{fitlmematrix} (from design matrices). ## ## The estimated fixed effects and their statistics are available through the ## @code{Coefficients} table; the covariance parameters through ## @code{covarianceParameters}; the random-effect BLUPs through ## @code{randomEffects}; and predictions, residuals, and hypothesis tests ## through the @code{predict}, @code{residuals}, @code{anova}, @code{coefTest}, ## and @code{coefCI} methods. ## ## @seealso{fitlmematrix, fitlm} ## @end deftp classdef LinearMixedModel < PredictiveModel properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {LinearMixedModel} {property} FitMethod ## ## Estimation method ## ## A character vector, either @qcode{'ML'} for maximum likelihood or ## @qcode{'REML'} for restricted maximum likelihood, naming the method ## that fitted the model. This property is read-only. ## ## @end deftp FitMethod = ""; ## -*- texinfo -*- ## @deftp {LinearMixedModel} {property} NumObservations ## ## Number of observations ## ## A positive integer counting the observations used for the fit. This ## property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {LinearMixedModel} {property} NumCoefficients ## ## Number of fixed-effects coefficients ## ## A positive integer counting the fixed-effects coefficients of the ## model. This property is read-only. ## ## @end deftp NumCoefficients = []; ## -*- texinfo -*- ## @deftp {LinearMixedModel} {property} NumEstimatedCoefficients ## ## Number of estimated fixed-effects coefficients ## ## A positive integer counting the fixed-effects coefficients estimated ## from the data. Every coefficient the model carries is estimated, so ## this equals @qcode{NumCoefficients}. This property is read-only. ## ## @end deftp NumEstimatedCoefficients = []; ## -*- texinfo -*- ## @deftp {LinearMixedModel} {property} Coefficients ## ## Fixed-effects estimates and their statistics ## ## A table with one row per fixed-effects coefficient, its row names ## taken from @qcode{CoefficientNames}, and the variables ## @qcode{Estimate}, @qcode{SE}, @qcode{tStat}, @qcode{DF}, ## @qcode{pValue}, @qcode{Lower} and @qcode{Upper}. @qcode{Lower} and ## @qcode{Upper} bound a 95% confidence interval. This property is ## read-only. ## ## @end deftp Coefficients = []; ## -*- texinfo -*- ## @deftp {LinearMixedModel} {property} CoefficientCovariance ## ## Covariance of the fixed-effects estimates ## ## A square numeric matrix, one row and column per fixed-effects ## coefficient, holding the estimated covariance of the estimates in ## @qcode{Coefficients}. This property is read-only. ## ## @end deftp CoefficientCovariance = []; ## -*- texinfo -*- ## @deftp {LinearMixedModel} {property} CoefficientNames ## ## Names of the fixed-effects coefficients ## ## A cell array of character vectors with one name per fixed-effects ## coefficient. This property is read-only. ## ## @end deftp CoefficientNames = {}; ## -*- texinfo -*- ## @deftp {LinearMixedModel} {property} LogLikelihood ## ## Log-likelihood of the fitted model ## ## A scalar, the maximised log-likelihood, or the maximised restricted ## log-likelihood when @qcode{FitMethod} is @qcode{'REML'}. This ## property is read-only. ## ## @end deftp LogLikelihood = []; ## -*- texinfo -*- ## @deftp {LinearMixedModel} {property} ModelCriterion ## ## Information criteria ## ## A scalar structure with the fields @qcode{AIC}, @qcode{BIC}, ## @qcode{LogLikelihood} and @qcode{Deviance}. The parameter count ## behind @qcode{AIC} and @qcode{BIC} holds the fixed-effects ## coefficients, the covariance parameters and the residual variance. ## This property is read-only. ## ## @end deftp ModelCriterion = []; ## -*- texinfo -*- ## @deftp {LinearMixedModel} {property} Rsquared ## ## Coefficient of determination ## ## A scalar structure with the fields @qcode{Ordinary} and ## @qcode{Adjusted}. @qcode{Ordinary} is one less the ratio of ## @qcode{SSE} to @qcode{SST}, and @qcode{Adjusted} corrects that ratio ## for the error degrees of freedom. This property is read-only. ## ## @end deftp Rsquared = []; ## -*- texinfo -*- ## @deftp {LinearMixedModel} {property} SSE ## ## Error sum of squares ## ## A nonnegative scalar, the sum of the squared differences between the ## response and the conditional fit. This property is read-only. ## ## @end deftp SSE = []; ## -*- texinfo -*- ## @deftp {LinearMixedModel} {property} SSR ## ## Regression sum of squares ## ## A nonnegative scalar, the sum of the squared deviations of the ## conditional fit about the mean of the response. This property is ## read-only. ## ## @end deftp SSR = []; ## -*- texinfo -*- ## @deftp {LinearMixedModel} {property} SST ## ## Total sum of squares ## ## A nonnegative scalar, @qcode{SSE} plus @qcode{SSR}. This property is ## read-only. ## ## @end deftp SST = []; ## -*- texinfo -*- ## @deftp {LinearMixedModel} {property} MSE ## ## Residual variance estimate ## ## A positive scalar, the estimate of the error variance. This property ## is read-only. ## ## @end deftp MSE = []; ## -*- texinfo -*- ## @deftp {LinearMixedModel} {property} DFE ## ## Residual degrees of freedom ## ## A nonnegative integer, @qcode{NumObservations} less ## @qcode{NumCoefficients}. This property is read-only. ## ## @end deftp DFE = []; ## -*- texinfo -*- ## @deftp {LinearMixedModel} {property} Formula ## ## Model formula ## ## A character vector describing the model. It is empty for a fit built ## from design matrices, which carries no formula. This property is ## read-only. ## ## @end deftp Formula = ""; ## -*- texinfo -*- ## @deftp {LinearMixedModel} {property} ResponseName ## ## Name of the response variable ## ## A character vector naming the response. It is empty for a fit built ## from design matrices. This property is read-only. ## ## @end deftp ResponseName = ""; endproperties properties (Access = private, Hidden) X_ = []; y_ = []; Zcell_ = {}; Gcell_ = {}; Zx_ = []; qk_ = []; nlev_ = []; levels_ = {}; gidx_ = {}; beta_ = []; covbeta_ = []; Psi_ = {}; sigma2_ = []; b_ = []; theta_ = []; GroupNames_ = {}; REPred_ = {}; fitted_ = []; fitted_marg_ = []; resid_ = []; endproperties methods (Hidden) function disp (this) fprintf ("\n Linear mixed-effects model fit by %s\n", this.FitMethod); if (! isempty (this.Formula)) fprintf ("\n Formula:\n %s\n", this.Formula); endif if (! isempty (this.Coefficients)) fprintf ("\n Fixed effects coefficients:\n\n"); disp (this.Coefficients); endif if (! isempty (this.Psi_)) fprintf ("\n Random effects covariance parameters:\n"); for k = 1:numel (this.Psi_) fprintf (" Group: %s (%d levels)\n", this.GroupNames_{k}, ... this.nlev_(k)); disp (this.Psi_{k}); endfor fprintf (" Residual variance (sigma2): %g\n", this.sigma2_); endif fprintf ("\n"); if (! isempty (this.NumObservations)) fprintf ("Number of observations: %d, Error DF: %d\n", ... this.NumObservations, this.DFE); endif if (! isempty (this.LogLikelihood)) fprintf ("Log-likelihood: %g\n", this.LogLikelihood); endif if (! isempty (this.Rsquared) && isstruct (this.Rsquared)) fprintf ("R-squared: %g, Adjusted R-Squared: %g\n", ... this.Rsquared.Ordinary, this.Rsquared.Adjusted); endif endfunction function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); switch (s.type) case "()" error (strcat ("LinearMixedModel: () indexing is not supported.", ... " Use dot notation to access properties.")); case "{}" error (strcat ("LinearMixedModel: {} indexing is not supported.", ... " Use dot notation to access properties.")); case "." if (! ischar (s.subs)) error ("LinearMixedModel.subsref: property name must be a string."); endif ## Method calls (with or without arguments) go straight to builtin. if (ismethod (this, s.subs)) [varargout{1:nargout}] = builtin ("subsref", this, [s, chain_s]); return; endif try out = this.(s.subs); catch error ("LinearMixedModel.subsref: unknown property '%s'.", s.subs); end_try_catch endswitch ## Delegate any further indexing to the property's own subsref ## (e.g. lme.Coefficients.Estimate on the returned table). if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {LinearMixedModel} {@var{lme} =} LinearMixedModel (@var{info}) ## ## Construct a @code{LinearMixedModel} from a fitted-model info struct. ## This constructor is used internally by @code{fitlmematrix}; call that ## function rather than the constructor directly. ## ## @end deftypefn function this = LinearMixedModel (info) if (nargin == 0) return; endif n = info.n; p = numel (info.beta); this.X_ = info.X; this.y_ = info.y; this.Zcell_ = info.Zcell; this.Gcell_ = info.Gcell; this.Zx_ = info.Zx; this.qk_ = info.qk; this.nlev_ = info.nlev; this.levels_ = info.levels; this.gidx_ = info.gidx; this.beta_ = info.beta; this.covbeta_ = info.covbeta; this.Psi_ = info.Psi; this.sigma2_ = info.sigma2; this.b_ = info.b; this.theta_ = info.theta; this.GroupNames_ = info.GroupNames; this.REPred_ = info.REPred; this.fitted_ = info.fitted; this.fitted_marg_ = info.fitted_marg; this.resid_ = info.resid; if (isfield (info, "Formula")), this.Formula = info.Formula; endif if (isfield (info, "ResponseName")) this.ResponseName = info.ResponseName; endif this.FitMethod = info.method; this.NumObservations = n; this.NumCoefficients = p; this.NumEstimatedCoefficients = p; this.CoefficientNames = info.CoefficientNames; this.CoefficientCovariance = info.covbeta; this.LogLikelihood = info.loglik; this.MSE = info.sigma2; this.DFE = n - p; ## Fixed-effects coefficient statistics (Residual df = n - p). se = sqrt (diag (info.covbeta)); tstat = info.beta ./ se; dfe = n - p; pval = 2 * tcdf (-abs (tstat), dfe); tcrit = tinv (0.975, dfe); lower = info.beta - tcrit * se; upper = info.beta + tcrit * se; this.Coefficients = table (info.beta(:), se(:), tstat(:), ... repmat (dfe, p, 1), pval(:), lower(:), upper(:), ... "VariableNames", {"Estimate", "SE", "tStat", "DF", "pValue", ... "Lower", "Upper"}, ... "RowNames", info.CoefficientNames(:)); ## Sums of squares and R-squared (MATLAB's mixed-model convention: ## SST = SSE + SSR, both from the conditional fit). ybar = mean (info.y); this.SSE = sum ((info.y - info.fitted) .^ 2); this.SSR = sum ((info.fitted - ybar) .^ 2); this.SST = this.SSE + this.SSR; rsq.Ordinary = 1 - this.SSE / this.SST; rsq.Adjusted = 1 - (this.SSE / dfe) / (this.SST / (n - 1)); this.Rsquared = rsq; ## Information criteria (total parameter count = fixed + covariance). ncov = sum (arrayfun (@(q) q*(q+1)/2, info.qk)) + 1; kpar = p + ncov; dev = -2 * info.loglik; mc.AIC = dev + 2 * kpar; mc.BIC = dev + kpar * log (n); mc.LogLikelihood = info.loglik; mc.Deviance = dev; this.ModelCriterion = mc; endfunction ## -*- texinfo -*- ## @deftypefn {LinearMixedModel} {@var{beta} =} fixedEffects (@var{lme}) ## @deftypefnx {LinearMixedModel} {[@var{beta}, @var{names}] =} fixedEffects (@var{lme}) ## ## Return the estimated fixed-effects coefficients @var{beta} and, ## optionally, their names. ## ## @end deftypefn function [beta, names] = fixedEffects (this) beta = this.beta_; names = this.CoefficientNames; endfunction ## -*- texinfo -*- ## @deftypefn {LinearMixedModel} {@var{b} =} randomEffects (@var{lme}) ## @deftypefnx {LinearMixedModel} {[@var{b}, @var{names}] =} randomEffects (@var{lme}) ## ## Return the best linear unbiased predictors (BLUPs) of the random effects ## @var{b} and, optionally, a cell array of @code{group:level:predictor} ## labels. ## ## @end deftypefn function [b, names] = randomEffects (this) b = this.b_; if (nargout > 1) names = {}; for k = 1:numel (this.qk_) pn = this.REPred_{k}; for l = 1:this.nlev_(k) for j = 1:this.qk_(k) names{end+1, 1} = sprintf ("%s:%s:%s", this.GroupNames_{k}, ... num2str (this.levels_{k}(l)), pn{j}); endfor endfor endfor endif endfunction ## -*- texinfo -*- ## @deftypefn {LinearMixedModel} {[@var{psi}, @var{mse}] =} covarianceParameters (@var{lme}) ## ## Return the estimated random-effects covariance matrices @var{psi} (a cell ## array, one per grouping term) and the residual variance @var{mse}. ## ## @end deftypefn function [psi, mse] = covarianceParameters (this) psi = this.Psi_; mse = this.sigma2_; endfunction ## -*- texinfo -*- ## @deftypefn {LinearMixedModel} {@var{yf} =} fitted (@var{lme}) ## @deftypefnx {LinearMixedModel} {@var{yf} =} fitted (@var{lme}, @qcode{"Conditional"}, @var{tf}) ## ## Return the fitted values. With @qcode{"Conditional"} true (the default) ## the fit includes the random effects (@code{X*beta + Z*b}); with false it ## is the marginal fit (@code{X*beta}). ## ## @end deftypefn function yf = fitted (this, varargin) cond = true; if (numel (varargin) >= 2 && strcmpi (varargin{1}, "Conditional")) cond = logical (varargin{2}); endif if (cond) yf = this.fitted_; else yf = this.fitted_marg_; endif endfunction ## -*- texinfo -*- ## @deftypefn {LinearMixedModel} {@var{r} =} residuals (@var{lme}) ## @deftypefnx {LinearMixedModel} {@var{r} =} residuals (@var{lme}, @qcode{"ResidualType"}, @var{type}) ## ## Return the conditional residuals @code{y - (X*beta + Z*b)}. @var{type} ## is @qcode{"Raw"} (default), @qcode{"Pearson"} (raw divided by ## @code{sqrt (sigma2)}), or @qcode{"Standardized"} (raw divided by the ## square root of its estimated variance). ## ## @end deftypefn function r = residuals (this, varargin) type = "raw"; if (numel (varargin) >= 2 && strcmpi (varargin{1}, "ResidualType")) type = lower (varargin{2}); endif raw = this.resid_; switch (type) case "raw" r = raw; case "pearson" r = raw / sqrt (this.sigma2_); case "standardized" r = raw ./ sqrt (cond_resid_var (this)); otherwise error ("LinearMixedModel: unknown ResidualType '%s'.", type); endswitch endfunction ## -*- texinfo -*- ## @deftypefn {LinearMixedModel} {@var{ypred} =} predict (@var{lme}, @var{Xnew}, @var{Znew}, @var{Gnew}) ## @deftypefnx {LinearMixedModel} {[@var{ypred}, @var{yci}] =} predict (@dots{}) ## @deftypefnx {LinearMixedModel} {[@dots{}] =} predict (@dots{}, @var{name}, @var{value}) ## ## Predict the response at new fixed-effects design @var{Xnew}, ## random-effects design @var{Znew}, and grouping @var{Gnew}. By default ## the prediction is conditional on the estimated random effects (levels of ## @var{Gnew} not seen in the fit fall back to the marginal prediction). ## With ## @qcode{"Conditional"} false the marginal prediction @code{Xnew*beta} is ## returned. The second output @var{yci} gives 95% (or @qcode{"Alpha"}) ## confidence intervals for the marginal mean. ## ## @end deftypefn function [ypred, yci] = predict (this, Xnew, Znew, Gnew, varargin) if (nargin < 2) error ("LinearMixedModel: predict requires Xnew."); endif if (nargin < 3), Znew = []; endif if (nargin < 4), Gnew = []; endif cond = true; alpha = 0.05; for i = 1:2:numel (varargin) switch (lower (varargin{i})) case "conditional" cond = logical (varargin{i+1}); case "alpha" alpha = varargin{i+1}; endswitch endfor ypred = Xnew * this.beta_; if (cond && ! isempty (Znew) && ! isempty (Gnew)) Gnew = Gnew(:); for k = 1:numel (this.qk_) q = this.qk_(k); lev = this.levels_{k}; ## offset of term k inside the stacked BLUP vector off = 0; for kk = 1:k-1 off += this.qk_(kk) * this.nlev_(kk); endfor for i = 1:rows (Xnew) li = find (lev == Gnew(i), 1); if (! isempty (li)) bk = this.b_(off + (li-1)*q + (1:q)); ypred(i) += Znew(i, :) * bk; endif endfor endfor endif if (nargout > 1) v = sum ((Xnew * this.covbeta_) .* Xnew, 2); ## marginal mean variance tcrit = tinv (1 - alpha/2, this.DFE); half = tcrit * sqrt (v); ymarg = Xnew * this.beta_; yci = [ymarg - half, ymarg + half]; endif endfunction ## -*- texinfo -*- ## @deftypefn {LinearMixedModel} {@var{tbl} =} anova (@var{lme}) ## @deftypefnx {LinearMixedModel} {@var{tbl} =} anova (@var{lme}, @qcode{"DFMethod"}, @var{method}) ## ## Analysis-of-variance table of F-tests for the fixed-effects terms. Each ## row tests one coefficient. @var{method} selects the denominator degrees ## of freedom: @qcode{"Residual"} (default, @code{n - p}) or ## @qcode{"Satterthwaite"}. ## ## @end deftypefn function tbl = anova (this, varargin) dfmethod = "residual"; if (numel (varargin) >= 2 && strcmpi (varargin{1}, "DFMethod")) dfmethod = lower (varargin{2}); endif p = this.NumCoefficients; beta = this.beta_; se = sqrt (diag (this.covbeta_)); Fstat = (beta ./ se) .^ 2; DF1 = ones (p, 1); if (strcmp (dfmethod, "satterthwaite")) DF2 = __lme_dfsatt__ (this.X_, this.y_, this.Zx_, this.qk_, ... this.nlev_, this.Psi_, this.sigma2_, this.FitMethod, eye (p)); elseif (strcmp (dfmethod, "residual")) DF2 = repmat (this.DFE, p, 1); else error ("LinearMixedModel: unknown DFMethod '%s'.", dfmethod); endif pValue = fcdf (Fstat, DF1, DF2, 'upper'); tbl = table (Fstat(:), DF1(:), DF2(:), pValue(:), ... "VariableNames", {"FStat", "DF1", "DF2", "pValue"}, ... "RowNames", this.CoefficientNames(:)); endfunction ## -*- texinfo -*- ## @deftypefn {LinearMixedModel} {@var{p} =} coefTest (@var{lme}) ## @deftypefnx {LinearMixedModel} {@var{p} =} coefTest (@var{lme}, @var{H}) ## @deftypefnx {LinearMixedModel} {[@var{p}, @var{F}, @var{df1}, @var{df2}] =} coefTest (@dots{}) ## ## Test the linear hypothesis @code{H*beta = 0} with an F-test. With no ## @var{H}, tests that all non-intercept coefficients are zero (intercept ## is taken to be the first coefficient). Returns the p-value and, ## optionally, the F-statistic and its numerator and denominator degrees of ## freedom (denominator = @code{n - p}). ## ## @end deftypefn function [pval, F, df1, df2] = coefTest (this, H) p = this.NumCoefficients; if (nargin < 2) H = [zeros(p-1, 1), eye(p-1)]; ## all but the (first) intercept endif if (columns (H) != p) error ("LinearMixedModel: H must have one column per coefficient."); endif Hb = H * this.beta_; df1 = rank (H); df2 = this.DFE; F = (Hb' * ((H * this.covbeta_ * H') \ Hb)) / df1; pval = fcdf (F, df1, df2, 'upper'); endfunction ## -*- texinfo -*- ## @deftypefn {LinearMixedModel} {@var{ci} =} coefCI (@var{lme}) ## @deftypefnx {LinearMixedModel} {@var{ci} =} coefCI (@var{lme}, @var{alpha}) ## ## Confidence intervals for the fixed-effects coefficients at level ## @code{1 - @var{alpha}} (default @var{alpha} = 0.05). Row @math{j} holds ## the lower and upper bounds for coefficient @math{j}. ## ## @end deftypefn function ci = coefCI (this, alpha) if (nargin < 2), alpha = 0.05; endif se = sqrt (diag (this.covbeta_)); tcrit = tinv (1 - alpha/2, this.DFE); ci = [this.beta_ - tcrit * se, this.beta_ + tcrit * se]; endfunction ## -*- texinfo -*- ## @deftypefn {LinearMixedModel} {@var{D} =} designMatrix (@var{lme}, @var{type}) ## ## Return the fixed-effects design matrix (@var{type} = @qcode{"Fixed"}, ## default) as a full matrix, or the expanded random-effects design matrix ## (@var{type} = @qcode{"Random"}) as a sparse matrix. ## ## @end deftypefn function D = designMatrix (this, type) if (nargin < 2), type = "Fixed"; endif switch (lower (type)) case "fixed" D = this.X_; case "random" D = this.Zx_; otherwise error ("LinearMixedModel: type must be 'Fixed' or 'Random'."); endswitch endfunction endmethods methods (Access = private) ## Diagonal of the covariance of the conditional residuals, for standardized ## residuals. With V = sigma2*I + Zx*D*Zx' and ## P = inv (V) - inv (V)*X*inv (X'*inv (V)*X)*X'*inv (V), the conditional ## residuals are sigma2*P*y and P*V*P = P, so their covariance is ## sigma2^2*P. With D = sigma2*L*L' and A = Zx*L, sigma2*inv (V) is ## I - A*inv (K)*A' for K = I + A'*A, which gives diag (P) without an ## n-by-n matrix. function v = cond_resid_var (this) s = this.sigma2_; X = this.X_; n = rows (X); blocks = cell (1, numel (this.qk_)); for k = 1:numel (this.qk_) [U, E] = eig ((this.Psi_{k} + this.Psi_{k}') / (2 * s)); Lk = sparse (U * diag (sqrt (max (diag (E), 0)))); blocks{k} = kron (speye (this.nlev_(k)), Lk); endfor A = this.Zx_ * blkdiag (blocks{:}); q = columns (A); K = speye (q) + A' * A; [Rk, ~, pk] = chol ((K + K') / 2, "vector"); ## sigma2*diag (inv (V)), in row chunks that hold about 1e7 entries of ## the triangular solve at a time. dVi = zeros (n, 1); step = max (1, floor (1e7 / q)); for r0 = 1:step:n r = r0:min (n, r0 + step - 1); W = Rk' \ A(r,pk)'; dVi(r) = 1 - full (sum (W .^ 2, 1))'; endfor ## sigma2*inv (V)*X, then the fixed-effects term of diag (P). AtX = A' * X; w = zeros (size (AtX)); w(pk,:) = Rk \ (Rk' \ AtX(pk,:)); ViX = X - A * w; XtViX = X' * ViX; T = ViX / chol ((XtViX + XtViX') / 2); v = s * (dVi - sum (T .^ 2, 2)); endfunction endmethods endclassdef ## Shared MATLAB-verified fixture (fitlmematrix R2026a): random-intercept REML ## model y ~ x + x2 + (1|g) on 42 observations, 6 groups. %!shared X, yL, grp, xL, x2, lme %! xL = [0.032760004 0.70410822 -0.8646718 -0.28869454 0.51276678 -1.4975462 ... %! -1.4527871 -0.80013541 -1.644209 1.5137701 0.72905543 0.20880758 1.0856145 ... %! 0.62862577 -0.87409978 1.9178276 0.09748204 0.50697633 1.0247569 ... %! -0.92789896 -0.88921018 -0.98322849 -0.031378913 0.86875961 -0.91481141 ... %! 0.034324163 -0.25025257 -1.0575644 -0.86131607 -0.35355444 0.82950729 ... %! -0.36874363 0.061580868 0.55803564 -0.1763803 1.0482413 1.0137831 ... %! -0.94876976 -0.010703972 -0.35149845 -1.6828735 -1.0493301]'; %! x2 = [0.68979276 0.0074354814 -0.45697437 -0.5636481 1.4567202 -0.97829955 ... %! -1.12922 -0.030542479 1.5847779 -0.87837755 0.24121762 0.68747601 ... %! -0.56728765 0.98895053 -0.39350661 0.85326015 0.36524343 0.15824977 ... %! -1.7665212 0.59808246 -0.55763708 -1.1982294 -2.1473319 0.22521416 ... %! 0.37034398 -1.880586 0.052941033 -0.70016994 0.2174853 -1.7797082 ... %! 0.51971317 -0.35551286 1.9845963 -1.3498848 -0.63514097 -0.78794714 ... %! 1.3681179 1.4423152 -0.51233905 0.30238864 2.0458136 0.17326323]'; %! yL = [3.5635971 -0.36763498 1.4192715 3.4471073 2.2760668 3.6420287 ... %! 4.4481522 1.5292777 5.437158 0.44016873 0.7259756 2.9771749 1.2941347 ... %! 0.26407508 1.9673466 0.77589984 1.2176078 0.8528384 -0.86522876 2.8651388 ... %! 2.0127931 3.1242841 -0.49164361 1.3192295 5.3782287 -0.40680701 2.3989882 ... %! 3.606108 1.9193197 1.6713765 2.4383124 1.9314844 3.5112706 0.91644553 ... %! 0.034923706 -0.15510033 2.1765206 2.5327412 3.1421219 3.3472574 5.343425 ... %! 3.8775899]'; %! grp = [1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 ... %! 5 6 1 2 3 4 5 6]'; %! X = [ones(42,1), xL, x2]; %! lme = fitlmematrix (X, yL, ones (42, 1), grp, "FitMethod", "REML", ... %! "FixedEffectPredictors", {"(Intercept)", "x", "x2"}); %!test # returns a LinearMixedModel and basic properties %! assert_equal (isa (lme, "LinearMixedModel"), true); %! assert_equal (lme.NumObservations, 42); %! assert_equal (lme.NumCoefficients, 3); %! assert_equal (lme.DFE, 39); %! assert_equal (lme.FitMethod, "REML"); %!test # Coefficients table -- SE / tStat / DF / pValue / CI vs MATLAB %! C = lme.Coefficients; %! assert_equal (C.Estimate, [1.96839; -1.37926; 0.811747], 1e-4); %! assert_equal (C.SE, [0.376509; 0.113264; 0.0966571], 1e-5); %! assert_equal (C.tStat, [5.22801; -12.1773; 8.39822], 1e-4); %! assert_equal (C.DF, [39; 39; 39]); %! assert_equal (C.pValue, [6.08512e-06; 7.30179e-15; 2.8079e-10], 1e-8); %! assert_equal (C.Lower, [1.20683; -1.60835; 0.61624], 1e-4); %! assert_equal (C.Upper, [2.72996; -1.15016; 1.00725], 1e-4); %!test # anova with Residual DF (default): F = t^2, DF2 = n - p %! a = anova (lme); %! assert_equal (a.FStat, [27.3321; 148.287; 70.5301], 1e-3); %! assert_equal (a.DF1, [1; 1; 1]); %! assert_equal (a.DF2, [39; 39; 39]); %!test # anova with Satterthwaite DF vs MATLAB %! a = anova (lme, "DFMethod", "Satterthwaite"); %! assert_equal (a.DF2, [4.95056; 34.5037; 34.3302], 1e-3); %! assert_equal (a.pValue, [0.00348739; 4.79374e-14; 7.71731e-10], 1e-6); %!test # coefTest: joint test of the non-intercept coefficients %! [p, F, df1, df2] = coefTest (lme); %! assert_equal (F, 106.847, 1e-2); %! assert_equal (df1, 2); assert_equal (df2, 39); %! assert_equal (p < 1e-12, true); # ~1e-16, below the precision of 1 - fcdf %!test # coefTest with an explicit single-row hypothesis (the x coefficient) %! [p, F, df1] = coefTest (lme, [0 1 0]); %! assert_equal (F, 148.287, 1e-2); %! assert_equal (df1, 1); %!test # coefCI matches the Coefficients table bounds %! ci = coefCI (lme); %! assert_equal (ci(:,1), lme.Coefficients.Lower, 1e-12); %! assert_equal (ci(:,2), lme.Coefficients.Upper, 1e-12); %!test # residuals: raw, Pearson (= raw/sqrt(mse)), Standardized vs MATLAB %! [~, mse] = covarianceParameters (lme); %! raw = residuals (lme); %! assert_equal (raw, yL - fitted (lme), 1e-12); %! assert_equal (residuals (lme, "ResidualType", "Pearson"), ... %! raw / sqrt (mse), 1e-12); %! rs = residuals (lme, "ResidualType", "Standardized"); %! assert_equal (rs(1:3), [0.1824776; -0.4452186; -2.057201], 1e-5); %!test # fitted: conditional includes random effects, marginal does not %! fc = fitted (lme); %! fm = fitted (lme, "Conditional", false); %! assert_equal (fm, X * fixedEffects (lme), 1e-12); %! assert_equal (any (abs (fc - fm) > 1e-3, 'all'), true); %!test # predict: conditional (unseen group falls back to marginal) + marginal %! Xn = [1 0.5 0; 1 -0.5 1; 1 1 -1]; %! yc = predict (lme, Xn, ones (3, 1), [1; 2; 7]); %! ym = predict (lme, Xn, ones (3, 1), [1; 2; 7], "Conditional", false); %! assert_equal (yc, [2.254803; 2.351467; -0.2226094], 1e-4); %! assert_equal (ym, [1.278766; 3.469768; -0.2226094], 1e-4); %! # group 7 unseen -> conditional == marginal %! assert_equal (yc(3), ym(3), 1e-10); %!test # predict confidence intervals bracket the marginal mean %! Xn = [1 0.5 0; 1 -0.5 1]; %! [yp, ci] = predict (lme, Xn, ones (2, 1), [1; 2], "Conditional", false); %! assert_equal (all (ci(:,1) < yp & yp < ci(:,2), 'all'), true); %!test # effect extraction and design matrices %! [beta, names] = fixedEffects (lme); %! assert_equal (beta, lme.Coefficients.Estimate, 1e-12); %! assert_equal (names, {"(Intercept)", "x", "x2"}); %! b = randomEffects (lme); %! assert_equal (numel (b), 6); # 6 intercept BLUPs %! assert_equal (b, [0.9760378; -1.118301; -0.1922078; 0.8754252; ... %! -0.7955462; 0.2545924], 1e-4); # BLUPs vs MATLAB %! assert_equal (size (designMatrix (lme, "Fixed")), [42, 3]); %! assert_equal (size (designMatrix (lme, "Random")), [42, 6]); %! assert_equal (issparse (designMatrix (lme, "Fixed")), false); %! assert_equal (issparse (designMatrix (lme, "Random")), true); %!test # R-squared and sums of squares (MATLAB SST = SSE + SSR convention) %! assert_equal (lme.Rsquared.Ordinary, 0.8758549, 1e-6); %! assert_equal (lme.Rsquared.Adjusted, 0.8694885, 1e-6); %! assert_equal (lme.SST, lme.SSE + lme.SSR, 1e-10); %! assert_equal (lme.ModelCriterion.Deviance, -2 * lme.LogLikelihood, 1e-10); %!test %! ## Below the resolution of 1 - tcdf, value from MATLAB R2024a %! x = (1:30)'; %! g = repmat ((1:5)', 6, 1); %! T = table (x, g, 2 * x + g + 0.01 * sin (x), ... %! 'VariableNames', {'x', 'g', 'y'}); %! m = fitlme (T, 'y ~ x + (1|g)'); %! assert_equal (m.Coefficients.pValue(2), 1.12646802753448e-95, -1e-3); %!test %! ## Below the resolution of 1 - fcdf, values from MATLAB R2024a %! u = (1:30)'; %! g = repmat ((1:5)', 6, 1); %! T = table (u, g, 2 * u + g + 0.01 * sin (u), ... %! 'VariableNames', {'x', 'g', 'y'}); %! m = fitlme (T, 'y ~ x + (1|g)'); %! assert_equal (coefTest (m), 1.12646802753448e-95, -1e-3); %!test %! u = (1:30)'; %! g = repmat ((1:5)', 6, 1); %! T = table (u, g, 2 * u + g + 0.01 * sin (u), ... %! 'VariableNames', {'x', 'g', 'y'}); %! A = anova (fitlme (T, 'y ~ x + (1|g)')); %! assert_equal (A.pValue(2), 1.12646802753448e-95, -1e-3); ## Error handling %!error residuals (lme, "ResidualType", "xxx") %!error anova (lme, "DFMethod", "xxx") %!error designMatrix (lme, "bogus") %!error coefTest (lme, [1 0]) %!error lme(1) statistics-release-2.1.2/inst/Regression/LinearModel.m000066400000000000000000014357651527311104300230140ustar00rootroot00000000000000## Copyright (C) 2026 Avanish Salunke ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef LinearModel < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} LinearModel ## ## Linear regression model ## ## The @code{LinearModel} class represents a least-squares (or, optionally, ## robust) linear regression fit of a response variable to one or more ## predictor variables. A @code{LinearModel} object is returned by the ## @code{fitlm} function and holds everything about the fit in one place: ## the fitted coefficients, the data and specification used to produce ## them, and the diagnostics needed to assess the quality of the fit. ## ## The properties of a @code{LinearModel} object fall into four groups: ## ## @multitable @columnfractions 0.22 0.76 ## @headitem Group @tab Properties ## ## @item Coefficient estimates @tab @code{Coefficients} (a table of ## estimates, standard errors, t-statistics, and p-values for each term), ## @code{CoefficientCovariance}, @code{CoefficientNames}, and the ## coefficient counts @code{NumCoefficients} and ## @code{NumEstimatedCoefficients}. ## ## @item Summary statistics of the fit @tab @code{DFE}, ## @code{Fitted}, @code{Residuals} (raw, Pearson, Studentized, and ## standardized), @code{Diagnostics} (leverage, Cook's distance, and other ## per-observation influence measures), @code{MSE}, @code{RMSE}, ## @code{Rsquared} (ordinary and adjusted), @code{SSE}, @code{SSR}, ## @code{SST}, @code{LogLikelihood}, @code{ModelCriterion} (AIC, BIC, etc.), ## and @code{ModelFitVsNullModel} (the F-test of the fitted model against an ## intercept-only model). ## ## @item Fitting method information @tab @code{Robust}, which records ## the weighting function and tuning constant used when the model is fit by ## robust regression, and is empty for an ordinary least squares fit, and ## @code{Steps}, which records the stepwise fitting information whenever ## the model was fit using stepwise regression, and is currently always ## empty. ## ## @item Input data properties @tab @code{Formula}, ## @code{NumObservations}, @code{NumPredictors}, @code{NumVariables}, ## @code{ObservationInfo} (which observations were used, excluded, missing, ## or weighted), @code{ObservationNames}, @code{PredictorNames}, ## @code{ResponseName}, @code{VariableInfo}, @code{VariableNames}, and ## @code{Variables}. ## @end multitable ## ## A categorical predictor expands to indicator columns, one per level bar ## the reference level, which the intercept carries. When the model has no ## intercept, the @emph{first} categorical predictor is given an indicator ## for every one of its levels instead, so that its coefficients are the ## group means; any further categorical predictor stays reference coded, ## which keeps the design full rank. This differs from MATLAB, which omits ## the reference level whether or not an intercept is present and so cannot ## fit the reference group at all -- for a three-level grouping variable ## @code{g}, MATLAB fits @code{y ~ g - 1} with two coefficients, predicts ## exactly 0 for every observation in the omitted group, and reports a ## negative @math{R^2}. This implementation returns three coefficients, one ## per group. ## ## A @code{LinearModel} object supports categorical predictors, which are ## automatically encoded internally as indicator (dummy) variables, ## observation weights for a weighted least squares fit, excluding specific ## observations from the fit, and robust regression using iteratively ## reweighted least squares. Once fitted, the following methods are ## available on a @code{LinearModel} object: ## ## @multitable @columnfractions 0.2 0.78 ## @headitem Method @tab Description ## ## @item @code{predict} @tab Predict responses at new predictor values ## given in a matrix or table, or reproduce the training fitted values when ## called with no new data. Can also return pointwise or simultaneous ## confidence or prediction intervals alongside the point predictions. ## ## @item @code{feval} @tab Predict responses given predictors as ## separate scalar or vector arguments (one per predictor variable) instead ## of a single matrix, so a @code{LinearModel} object can be evaluated the ## same way as a plain function handle. Returns point predictions only. ## ## @item @code{random} @tab Simulate new response values at new ## predictor locations by adding independent Gaussian noise, drawn from the ## estimated error variance @code{MSE}, to the fitted response. ## ## @item @code{coefCI} @tab Return Wald confidence intervals for every ## fitted coefficient at a chosen significance level (default @math{0.05}). ## ## @item @code{coefTest} @tab Test a linear hypothesis on the fitted ## coefficients. With no arguments, tests the overall model F-test that ## all non-intercept coefficients are zero; a custom hypothesis can be ## given as a contrast matrix and, if needed, right-hand-side values. ## Returns the p-value, and optionally the F-statistic and its numerator ## degrees of freedom. ## ## @item @code{dwtest} @tab Durbin-Watson test for first-order ## autocorrelation among the model residuals, with a choice of exact or ## approximate p-value computation and a one- or two-sided alternative. ## ## @item @code{addTerms} @tab Return a new, refitted @code{LinearModel} ## with terms added to the current model specification, given as a ## Wilkinson formula fragment or a terms matrix. Weights, excluded rows, ## and categorical encodings carry over automatically; the original model ## object is left unmodified. ## ## @item @code{removeTerms} @tab Return a new, refitted ## @code{LinearModel} with terms removed from the current model ## specification, given as a Wilkinson formula fragment or a terms matrix. ## Weights, excluded rows, and categorical encodings carry over ## automatically; the original model object is left unmodified. ## ## @item @code{plotResiduals} @tab Plot the model residuals. Default ## is a probability density histogram; other supported plot types are ## @qcode{'fitted'}, @qcode{'caseorder'}, @qcode{'lagged'}, ## @qcode{'probability'}, and @qcode{'observed'}. ## ## @item @code{plotDiagnostics} @tab Plot per-observation influence ## diagnostics. Default is leverage by observation row number; other ## supported plot types are @qcode{'cookd'}, @qcode{'covratio'}, ## @qcode{'dfbetas'}, @qcode{'dffits'}, @qcode{'s2_i'}, and ## @qcode{'contour'} (standardized residuals against leverage with Cook's ## distance contours). ## ## @item @code{plotEffects} @tab Plot the estimated main effect and ## 95% confidence interval of each predictor, evaluated between its ## observed minimum and maximum with all other predictors held at their ## observed means. ## ## @item @code{plotAdjustedResponse} @tab Plot the fitted response ## against a single predictor, with the other predictors averaged out by ## averaging the fitted values over the observations used in the fit. ## ## @item @code{plotAdded} @tab Plot the incremental effect of one or ## more terms on the response, after removing the effects of all other ## terms, along with the fitted line and its 95% confidence bounds. ## ## @item @code{plot} @tab Plot a default view of the model. Creates an ## added variable plot for the whole model when more than one predictor ## is included, a scatter plot of the data with a fitted curve and 95% ## confidence bounds when exactly one predictor is included, or a ## histogram of the residuals when no predictors are included. ## ## @item @code{plotInteraction} @tab Plot the main and conditional effects ## of two predictors, or the adjusted response as a function of one ## predictor for several fixed values of the other, to visualize whether ## the two predictors interact. ## ## @item @code{compact} @tab Return a @code{CompactLinearModel} that ## discards the training data and per-observation diagnostics while ## retaining the coefficient estimates and fit statistics needed for ## prediction and inference. ## ## @item @code{anova} @tab Analysis of variance for the fitted model, ## reporting either the per-term breakdown of sums of squares or a ## summary table of the model against the total and residual variation. ## ## @item @code{step} @tab Improve the fitted model by one or more ## steps of stepwise term selection, returning a new, refitted ## @code{LinearModel} without modifying the original. ## @end multitable ## ## Create a @code{LinearModel} object by using the @code{fitlm} function or ## the class constructor directly. ## ## @seealso{fitlm} ## @end deftp properties(GetAccess = public, SetAccess = protected) ## Coefficient estimate properties ## -*- texinfo -*- ## @deftp {LinearModel} {property} CoefficientCovariance ## ## Covariance matrix of coefficient estimates ## ## A @math{p}-by-@math{p} numeric matrix of covariance values for the ## coefficient estimates, where @math{p} is the number of coefficients in ## the fitted model as given by @code{NumCoefficients}. This property is ## read-only. ## ## @end deftp CoefficientCovariance = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} CoefficientNames ## ## Coefficient names ## ## A cell array of character vectors, each containing the name of the ## corresponding model term (e.g., @qcode{'(Intercept)'}, @qcode{'x1'}, ## @qcode{'x1:x2'}). This property is read-only. ## ## @end deftp CoefficientNames = {}; ## -*- texinfo -*- ## @deftp {LinearModel} {property} Coefficients ## ## Coefficient values ## ## A table with one row for each coefficient and four columns: ## @itemize ## @item @code{Estimate} - estimated coefficient value ## @item @code{SE} - standard error of the estimate ## @item @code{tStat} - t-statistic for a two-sided test ## @item @code{pValue} - p-value for the t-statistic ## @end itemize ## Coefficients that are dropped due to rank deficiency have ## @code{Estimate = 0}, @code{SE = 0}, @code{tStat = NaN}, ## @code{pValue = NaN}. This property is read-only. ## ## @end deftp Coefficients = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} NumCoefficients ## ## Number of model coefficients ## ## A positive integer giving the total number of coefficients in the fitted ## model, including any coefficients set to zero because the model terms are ## rank deficient. This property is read-only. ## ## @end deftp NumCoefficients = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} NumEstimatedCoefficients ## ## Number of estimated coefficients ## ## A positive integer giving the number of coefficients actually estimated, ## i.e., not set to zero due to rank deficiency. ## @code{NumEstimatedCoefficients} equals the degrees of freedom for ## regression. This property is read-only. ## ## @end deftp NumEstimatedCoefficients = []; ## Summary statistic properties ## -*- texinfo -*- ## @deftp {LinearModel} {property} DFE ## ## Degrees of freedom for error ## ## A positive integer equal to the number of observations minus the number ## of estimated coefficients: @code{DFE = NumObservations - ## NumEstimatedCoefficients}. This property is read-only. ## ## @end deftp DFE = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} Diagnostics ## ## Observation diagnostics ## ## A table with one row per observation and seven columns: ## @itemize ## @item @code{Leverage} - diagonal of the hat matrix @math{H} ## @item @code{CooksDistance} - Cook's distance, a measure of scaled ## change in fitted values ## @item @code{Dffits} - delete-1 scaled differences in fitted values ## @item @code{S2_i} - delete-1 residual variance estimate ## @item @code{CovRatio} - ratio of the determinant of the coefficient ## covariance matrix with and without each observation ## @item @code{Dfbetas} - @math{n}-by-@math{p} matrix of scaled changes ## in coefficient estimates when each observation is deleted in turn ## @item @code{HatMatrix} - @math{n}-by-@math{n} projection matrix such ## that @code{Fitted = HatMatrix * y} ## @end itemize ## Rows not used in fitting have @code{NaN} in @code{CooksDistance}, ## @code{Dffits}, @code{S2_i}, and @code{CovRatio}, and zeros in ## @code{Leverage}, @code{Dfbetas}, and @code{HatMatrix}. This property ## is read-only. ## ## @end deftp Diagnostics = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} Fitted ## ## Fitted response values ## ## An @math{n}-by-1 numeric vector of predicted response values based on ## the training data, where @math{n} is the total number of observations, ## excluded and missing rows included. Every observation whose predictors ## are available carries a fitted value, whether or not it was used in the ## fit, so an excluded row and a row missing only its response are both ## fitted; only a row whose predictors are missing is @code{NaN}. The ## corresponding @code{Residuals} are @code{NaN} for any row not used in ## the fit, so @code{Fitted} and @code{Residuals.Raw} do not add back to ## the response there. Use @code{predict} to obtain predictions for new ## data or to compute confidence bounds. This property is read-only. ## ## @end deftp Fitted = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} LogLikelihood ## ## Log-likelihood of the fitted model ## ## A scalar numeric value equal to the log-likelihood of the response ## values, assuming each response is normally distributed with mean equal ## to the fitted value and variance equal to @math{SSE/n} (the MLE ## variance estimate). This property is read-only. ## ## For a weighted fit, observation @math{i} is taken to have variance ## @math{s^2/w_i}, so the log-likelihood carries the term ## @math{0.5 * sum (log (w))} and @math{n} counts only the observations ## with nonzero weight: ## ## @math{logL = -n/2 * (1 + log (2*pi*SSE/n)) + 0.5 * sum (log (w))} ## ## This makes the value invariant to the scale of the weights, as it must ## be: multiplying every weight by a constant rescales the estimated ## variance by the same constant and leaves the fit unchanged. ## ## MATLAB omits the @math{0.5 * sum (log (w))} term and counts every ## observation in @math{n}, so its @code{LogLikelihood} moves by ## @math{n/2 * log (c)} when the weights are multiplied by @math{c}, and ## the @code{ModelCriterion} values built on it move with it. This ## implementation follows R's @code{logLik.lm} instead. Unweighted fits ## are unaffected, and agree with MATLAB. ## ## A robust fit carries no weight term. Its @code{SSE} is a robust scale ## estimate rather than a weighted residual sum, so the two enter ## separately and the general form is used: ## ## @math{logL = -n/2 * log (2*pi*SSE/n) - sum (w .* r.^2) / (2*SSE/n)} ## ## which is what MATLAB computes, and which reduces to the expression ## above whenever @math{sum (w .* r.^2)} equals @code{SSE}, as it does for ## any least-squares fit. Robust fits therefore agree with MATLAB ## exactly, weighted or not. ## ## @end deftp LogLikelihood = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} ModelCriterion ## ## Model comparison criteria ## ## A structure with four fields: ## @itemize ## @item @code{AIC} - Akaike information criterion: ## @math{-2 * logL + 2 * m} ## @item @code{AICc} - AIC corrected for sample size: ## @math{AIC + (2*m*(m+1))/(n-m-1)} ## @item @code{BIC} - Bayesian information criterion: ## @math{-2 * logL + m * log(n)} ## @item @code{CAIC} - Consistent AIC: ## @math{-2 * logL + m * (log(n) + 1)} ## @end itemize ## Here @math{logL} is @code{LogLikelihood}, @math{m} is ## @code{NumEstimatedCoefficients}, and @math{n} is the number of ## observations with nonzero weight, which is @code{NumObservations} ## unless some weight is zero. This property is read-only. ## ## Because these are built on @code{LogLikelihood}, they inherit its ## treatment of weights; see that property for how it differs from ## MATLAB's. ## ## @end deftp ModelCriterion = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} ModelFitVsNullModel ## ## F-test of the fitted model versus the null model ## ## A structure with three fields: ## @itemize ## @item @code{Fstat} - F-statistic of the fitted model versus a null ## model containing only a constant term ## @item @code{Pvalue} - p-value for the F-statistic ## @item @code{NullModel} - character vector describing the null model ## @end itemize ## This property is read-only. ## ## @end deftp ModelFitVsNullModel = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} MSE ## ## Mean squared error ## ## A scalar numeric value equal to @math{SSE / DFE}, where @code{SSE} is ## the sum of squared errors and @code{DFE} is the degrees of freedom for ## error. This property is read-only. ## ## @end deftp MSE = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} Residuals ## ## Residuals for the fitted model ## ## A table with one row per observation and four columns: ## @itemize ## @item @code{Raw} - observed minus fitted values ## @item @code{Pearson} - raw residuals divided by @code{RMSE} ## @item @code{Standardized} - internally studentized residuals; raw ## residuals divided by their estimated standard deviation using the ## full-model @code{MSE} ## @item @code{Studentized} - externally studentized residuals; each raw ## residual divided by an estimate of the standard deviation based on ## all observations except that one, using the delete-1 @code{S2_i} ## @end itemize ## Rows not used in the fit contain @code{NaN}. This property is ## read-only. ## ## @end deftp Residuals = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} RMSE ## ## Root mean squared error ## ## A scalar numeric value equal to @math{sqrt(MSE)}. This property is ## read-only. ## ## @end deftp RMSE = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} Rsquared ## ## R-squared goodness-of-fit statistics ## ## A structure with two fields: ## @itemize ## @item @code{Ordinary} - coefficient of determination: ## @math{R^2 = SSR / SST} ## @item @code{Adjusted} - adjusted @math{R^2} that accounts for the ## number of coefficients in the model ## @end itemize ## This property is read-only. ## ## @end deftp Rsquared = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} SSE ## ## Sum of squared errors ## ## A scalar numeric value equal to the sum of squared residuals. For a ## model with an intercept, @math{SST = SSE + SSR}. For weighted fits, ## this is the weighted sum of squares. This property is read-only. ## ## @end deftp SSE = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} SSR ## ## Regression sum of squares ## ## A scalar numeric value equal to the sum of squared deviations of the ## fitted values from the mean of the response. For a model with an ## intercept, @math{SST = SSE + SSR}. For weighted fits, this is the ## weighted sum of squares. This property is read-only. ## ## @end deftp SSR = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} SST ## ## Total sum of squares ## ## A scalar numeric value equal to the sum of squared deviations of the ## response from its mean. For a model with an intercept, ## @math{SST = SSE + SSR}. For a robust fit, @math{SST = SSE + SSR} ## rather than the deviation from the mean. For weighted fits, this is ## the weighted sum of squares. This property is read-only. ## ## @end deftp SST = []; ## Fitting method properties ## -*- texinfo -*- ## @deftp {LinearModel} {property} Robust ## ## Robust fit information ## ## A structure with three fields: ## @itemize ## @item @code{WgtFun} - robust weighting function name, e.g. ## @qcode{'bisquare'} ## @item @code{Tune} - tuning constant; empty if @code{WgtFun} is ## @qcode{'ols'} or a function handle with the default tuning constant ## @item @code{Weights} - vector of final iteration weights; empty for ## a @code{CompactLinearModel} object ## @end itemize ## This structure is empty unless the model was fit using robust ## regression. This property is read-only. ## ## @end deftp Robust = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} Steps ## ## Stepwise fitting information ## ## A structure recording the term-selection trace, populated whenever the ## model was fit by @code{stepwiselm} or improved by @code{step}, and ## @code{[]} otherwise. It has seven fields: ## ## @multitable @columnfractions 0.15 0.8 ## @headitem Field @tab Contents ## @item @code{Start} @tab a @code{LinearFormula} for the model the search ## started from. ## @item @code{Lower} @tab a @code{LinearFormula} for the smallest model ## considered; its terms are never removed. ## @item @code{Upper} @tab a @code{LinearFormula} for the largest model ## considered. ## @item @code{Criterion} @tab the selection criterion, such as ## @qcode{'SSE'}. ## @item @code{PEnter} @tab the threshold a term must beat to enter. ## @item @code{PRemove} @tab the threshold above which a term leaves. ## @item @code{History} @tab a table with one row per step. ## @end multitable ## ## @code{History} carries the columns @code{Action} (@qcode{'Start'}, ## @qcode{'Add'}, or @qcode{'Remove'}), @code{TermName}, @code{Terms} (the ## terms matrix after the step, over the model's variables), @code{DF} (the ## coefficient count after the step), and @code{delDF} (the change in it, ## negative for a removal). The remaining columns follow the criterion: ## @code{FStat} and @code{pValue} under @qcode{'SSE'}, and otherwise a ## single column named for the criterion (@code{AIC}, @code{BIC}, ## @code{Rsquared}, or @code{AdjRsquared}) holding its value after the ## step. ## ## The first row is the starting model, named by its right-hand side, and ## @code{step} appends to the history it inherits rather than starting a ## new one. This property is read-only. ## ## @end deftp Steps = []; ## Input data properties ## -*- texinfo -*- ## @deftp {LinearModel} {property} Formula ## ## Model formula information ## ## A @code{LinearFormula} object representing the model formula, with ## properties including @code{ResponseName}, @code{LinearPredictor}, ## @code{PredictorNames}, @code{TermNames}, @code{HasIntercept}, ## @code{Terms} (the terms matrix), and @code{InModel}. Converting it with ## @code{char} renders the whole formula. This property is read-only. ## ## @end deftp Formula = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} NumObservations ## ## Number of observations used in the fit ## ## A positive integer giving the number of observations actually used in ## fitting. Rows with missing values and rows excluded via the ## @code{'Exclude'} name-value argument are not counted. This property ## is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} NumPredictors ## ## Number of predictor variables ## ## A positive integer giving the number of predictor variables used to ## fit the model. This property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} NumVariables ## ## Number of variables in the input data ## ## A positive integer giving the total number of variables in the input ## data, counting predictors, the response, and any unused columns. ## This property is read-only. ## ## @end deftp NumVariables = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} ObservationInfo ## ## Per-observation metadata ## ## An @math{n}-by-4 table where @math{n} is the total number of rows in ## the input data. The four columns are: ## @itemize ## @item @code{Weights} - observation weight, default is 1 ## @item @code{Excluded} - logical; true if excluded via the ## @code{'Exclude'} argument ## @item @code{Missing} - logical; true if the row contains any ## @code{NaN} value ## @item @code{Subset} - logical; true if the observation was used in ## the fit, i.e. not excluded and not missing ## @end itemize ## This property is read-only. ## ## @end deftp ObservationInfo = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} ObservationNames ## ## Observation names ## ## A cell array of character vectors containing the names of the ## observations. If the fit was based on a table that has row names, ## this property holds those names. Otherwise it is an empty cell array. ## This property is read-only. ## ## @end deftp ObservationNames = {}; ## -*- texinfo -*- ## @deftp {LinearModel} {property} PredictorNames ## ## Names of predictor variables ## ## A cell array of character vectors containing the names of the ## predictor variables used to fit the model. This property is ## read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {LinearModel} {property} ResponseName ## ## Response variable name ## ## A character vector containing the name of the response variable. ## This property is read-only. ## ## @end deftp ResponseName = ''; ## -*- texinfo -*- ## @deftp {LinearModel} {property} VariableInfo ## ## Information about input variables ## ## A table with one row per variable including any unused variables, and ## four columns: ## @itemize ## @item @code{Class} - variable class as a character vector, e.g. ## @qcode{'double'} or @qcode{'categorical'} ## @item @code{Range} - for continuous variables, a two-element vector ## @code{[min, max]}; for categorical variables, a vector of the ## distinct values ## @item @code{InModel} - logical; true if the variable is in the ## fitted model ## @item @code{IsCategorical} - logical; true if the variable is ## categorical ## @end itemize ## This property is read-only. ## ## @end deftp VariableInfo = []; ## -*- texinfo -*- ## @deftp {LinearModel} {property} VariableNames ## ## Names of all variables in the input data ## ## A cell array of character vectors containing the names of all ## variables, including predictors, the response, and unused variables. ## For table input these are the table column names. For matrix input ## these are the values given by @code{'VarNames'}, defaulting to ## @qcode{@{'x1','x2',...,'xp','y'@}}. This property is read-only. ## ## @end deftp VariableNames = {}; ## -*- texinfo -*- ## @deftp {LinearModel} {property} Variables ## ## Input data as a table ## ## A table containing predictor and response values for all observations, ## including unused variables. For table input this is the full input ## table. For matrix input this is a table constructed from the ## predictor matrix and response vector. This property is read-only. ## ## @end deftp Variables = []; endproperties properties(Access = private, Hidden) ## Full design matrix, n by p_design, used for predictions DesignMatrix = []; ## Column indices of active coefficients in the design matrix ActiveCols = []; ## Whether the model includes an intercept term HasIntercept = true; ## Response vector, full n by 1 with NaN for non-subset rows ResponseVector = []; ## Full n by 1 observation weights WeightVector = []; ## n by 1 logical mask: true for rows used in the fit SubsetMask = []; ## Terms matrix from modelspec or parse_modelspec TermsMatrix = []; ## Categorical level info for re-encoding in predict CatLevelInfo = []; ## Predictor names after categorical dummy expansion EncPredictorNames = {}; ## Predictors available to the fit, before those the model does not use ## are dropped from the public PredictorNames. Stepwise selection needs ## the candidates, not the chosen ones. PredictorNamesRaw = {}; ## Cached per-predictor design contrasts used by plotEffects EffectContrasts = []; ## Cached per-predictor-pair design contrasts used by plotInteraction InteractionContrasts = []; ## Cached conceptual-term name and design-column grouping used by anova TermGroups = {}; ## Encoded predictor matrix (Path B only), cached for refit EncodedPredMatrix = []; ## Parsed NV options stored for refit operations OrigOpts = []; endproperties methods(Hidden) ## Custom display function disp (this) if (isempty (this.Robust)) fprintf ("\n Linear regression model:\n"); else fprintf ("\n Linear regression model (robust fit):\n"); endif if (! isempty (this.Formula) && isa (this.Formula, 'LinearFormula')) fprintf (" %s\n", char (this.Formula)); endif if (! isempty (this.Coefficients)) fprintf ("\n Estimated Coefficients:\n\n"); disp (this.Coefficients); endif fprintf ("\n"); if (! isempty (this.NumObservations) && ! isempty (this.DFE)) fprintf ("Number of observations: %d, Error degrees of freedom: %d\n", ... this.NumObservations, this.DFE); endif if (! isempty (this.RMSE)) fprintf ("Root Mean Squared Error: %g\n", this.RMSE); endif if (! isempty (this.Rsquared) && isstruct (this.Rsquared)) fprintf ("R-squared: %g, Adjusted R-Squared: %g\n", ... this.Rsquared.Ordinary, this.Rsquared.Adjusted); endif if (! isempty (this.ModelFitVsNullModel) ... && isstruct (this.ModelFitVsNullModel) ... && isfield (this.ModelFitVsNullModel, 'Fstat')) fprintf ("F-statistic vs. constant model: %g, p-value = %g\n", ... this.ModelFitVsNullModel.Fstat, ... this.ModelFitVsNullModel.Pvalue); endif endfunction ## Class specific subscripted reference function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); switch (s.type) case '()' error (strcat ("LinearModel: () indexing is not supported.", ... " Use dot notation to access properties.")); case '{}' error (strcat ("LinearModel: {} indexing is not supported.", ... " Use dot notation to access properties.")); case '.' if (! ischar (s.subs)) error ("LinearModel.subsref: property name must be a character vector."); endif ## Allow normal execution if the user is calling a class method if (ismethod (this, s.subs)) [varargout{1:nargout}] = builtin ('subsref', this, [s, chain_s]); return; endif try out = this.(s.subs); catch error ("LinearModel.subsref: unknown property '%s'.", s.subs); end_try_catch endswitch if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction ## Attach a stepwise-selection history structure. Used by ## @code{stepwiselm} and by the @code{step} method to record the ## term-selection trace on the returned object; not intended for direct use. function this = setSteps (this, steps) this.Steps = steps; endfunction endmethods methods(Access = public) ## -*- texinfo -*- ## @deftypefn {LinearModel} {@var{mdl} =} LinearModel (@var{X}, @var{y}) ## @deftypefnx {LinearModel} {@var{mdl} =} LinearModel (@var{tbl}, @var{resp_input}) ## @deftypefnx {LinearModel} {@var{mdl} =} LinearModel (@dots{}, @var{modelspec}) ## @deftypefnx {LinearModel} {@var{mdl} =} LinearModel (@dots{}, @var{Name}, @var{Value}, @dots{}) ## ## Create a @qcode{LinearModel} class object representing a linear ## regression model. ## ## @code{@var{mdl} = LinearModel (@var{X}, @var{y})} returns a ## @code{LinearModel} object fit to the response @var{y} and the ## predictor data @var{X}. Unless removed via the @qcode{'Intercept'} ## option, the fitted model contains a constant (intercept) term and one ## linear term for every column of @var{X}. ## ## @itemize ## @item ## @var{X} is an @math{N*P} numeric or logical matrix of predictor data, ## where rows correspond to observations and columns correspond to ## variables. By default, the predictors are named @qcode{'x1'}, ## @qcode{'x2'}, @dots{}, @qcode{'xP'}. ## @item ## @var{y} is an @math{N*1} numeric or logical vector of response values, ## and must have the same number of observations (rows) as @var{X}. By ## default, the response is named @qcode{'y'}. ## @end itemize ## ## @code{@var{mdl} = LinearModel (@var{tbl}, @var{resp_input})} fits a ## model using the variables in the table (or dataset) @var{tbl} as ## predictors. @var{resp_input} selects the response and can be a ## character vector naming a variable in @var{tbl}, or a numeric vector ## the same height as @var{tbl} to use as an external response. If ## @var{resp_input} is left empty, the last variable in @var{tbl} is used ## as the response. Variables that are @code{categorical} arrays, cell ## arrays of character vectors, or logical arrays are automatically ## treated as categorical predictors. ## ## @code{@var{mdl} = LinearModel (@dots{}, @var{modelspec})} additionally ## specifies the terms of the model to fit. @var{modelspec} can be any of ## the following. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Value} @tab @var{Description} ## ## @item @qcode{'constant'} @tab Model contains only an intercept ## term. ## ## @item @qcode{'linear'} @tab Model contains an intercept and one ## term for each predictor variable. This is the default when ## @var{modelspec} is not specified. ## ## @item @qcode{'interactions'} @tab Model contains an intercept, all ## linear terms, and all pairwise products of distinct predictor ## variables (no squared terms). ## ## @item @qcode{'purequadratic'} @tab Model contains an intercept, ## all linear terms, and all squared terms. ## ## @item @qcode{'quadratic'} @tab Model contains an intercept, all ## linear terms, all pairwise products of distinct predictor variables, ## and all squared terms. ## ## @item @qcode{'full'} @tab Model contains an intercept and all ## terms up to and including the full @math{P}-way interaction of the ## predictor variables. ## ## @item terms matrix @tab A @math{T*P} or @math{T*(P+1)} numeric ## matrix, where @math{T} is the number of terms and @math{P} is the ## number of predictor variables. Each row represents one term, and the ## value in column @math{j} is the exponent to which predictor @math{j} ## is raised in that term; a row of all zeros represents the intercept. ## If a @math{T*(P+1)} matrix is supplied, its last column (representing ## the response variable) must be all zeros. ## ## @item Wilkinson formula @tab A character vector of the form ## @qcode{'y ~ terms'} describing the response and predictor terms using ## Wilkinson notation. For table input, the variable to the left of ## @qcode{'~'} is used as the response, overriding @var{resp_input}. ## @end multitable ## ## @code{@var{mdl} = LinearModel (@dots{}, @var{Name}, @var{Value}, ## @dots{})} specifies additional options using one or more ## @qcode{Name-Value} pair arguments as described below. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Intercept'} @tab A logical scalar indicating ## whether to include a constant (intercept) term in the model. Default ## is @qcode{true}. Ignored when @var{modelspec} is a Wilkinson formula. ## ## @item @qcode{'Weights'} @tab A numeric vector of nonnegative ## observation weights, with one element per observation, used to fit a ## weighted least squares model. Default is a vector of ones. ## ## @item @qcode{'Exclude'} @tab A numeric or logical vector ## specifying observations to exclude from the fit, given as row indices ## or a logical mask. Excluded observations, together with any ## observation containing a missing value, are recorded in ## @code{ObservationInfo} but do not contribute to the fit. ## ## @item @qcode{'CategoricalVars'} @tab Specifies which predictor ## variables are treated as categorical, given as a vector of column ## indices, a logical vector, or a cell array of variable names. Each ## categorical predictor with @math{L} categories is expanded into ## @math{L-1} indicator (dummy) variables, using the first category as ## the reference level. ## ## @item @qcode{'VarNames'} @tab A cell array of character vectors ## naming the predictor and response variables, in order, with the ## response variable name last. Only applies to matrix input, since ## table variables already carry their own names. ## ## @item @qcode{'ResponseVar'} @tab A character vector naming the ## response variable, used to override the response variable name that ## would otherwise be used. ## ## @item @qcode{'PredictorVars'} @tab A cell array of character ## vectors naming which variables in @var{tbl} to use as predictors. By ## default, all variables other than the response variable are used. ## ## @item @qcode{'RobustOpts'} @tab Selects ordinary least squares or ## robust regression fitting. This value can be @qcode{'off'} (default, ## ordinary least squares), @qcode{'on'} (robust fitting using the ## @qcode{'bisquare'} weighting function), the name of one of the ## weighting functions below, a function handle for a custom weighting ## function, or a scalar structure with fields @qcode{RobustWgtFun} and ## @qcode{Tune} specifying the weighting function and its tuning ## constant. Robust fitting uses Iteratively Reweighted Least Squares ## (IRLS), refitting the model with updated observation weights until the ## coefficients converge. Supported weighting function names: ## @qcode{'andrews'}, @qcode{'bisquare'}, @qcode{'cauchy'}, ## @qcode{'fair'}, @qcode{'huber'}, @qcode{'logistic'}, @qcode{'ols'}, ## @qcode{'talwar'}, @qcode{'welsch'}, each with its own default tuning ## constant. ## @end multitable ## ## @var{mdl} is returned as a @code{LinearModel} object. If ## @qcode{'RobustOpts'} is anything other than @qcode{'off'}, the returned ## model is a robust fit rather than an ordinary least squares fit. ## ## @end deftypefn function this = LinearModel (varargin) ## LinearModel (X, y, modelspec, NV...) ## LinearModel (tbl, resp_input, modelspec, NV...) if (nargin == 0) return; endif data = varargin{1}; resp_input = varargin{2}; modelspec = varargin{3}; nv_args = varargin(4:end); opts = lm_parse_nv (nv_args); is_formula = ischar (modelspec) && any (modelspec == '~'); if (! istable (data)) X_raw = double (data); n_total = size (X_raw, 1); p_raw = size (X_raw, 2); y_full = double (resp_input(:)); if (! isempty (opts.VarNames)) if (numel (opts.VarNames) != p_raw + 1) error ("LinearModel: VarNames must have %d elements.", p_raw + 1); endif pred_names_raw = opts.VarNames(1:p_raw); resp_name = opts.VarNames{end}; else pred_names_raw = arrayfun (@(k) sprintf ("x%d", k), 1:p_raw, ... 'UniformOutput', false); resp_name = 'y'; endif if (! isempty (opts.ResponseVar)) resp_name = opts.ResponseVar; endif var_names_all = [pred_names_raw, {resp_name}]; n_vars = p_raw + 1; else ## 'table' tbl = data; col_names = tbl.Properties.VariableNames; n_total = height (tbl); n_vars = width (tbl); var_names_all = col_names; if (! isempty (opts.ResponseVar)) resp_name = opts.ResponseVar; if (isnumeric (resp_input) && ! isempty (resp_input)) y_ext = double (resp_input(:)); endif elseif (ischar (resp_input) && ! isempty (resp_input)) resp_name = resp_input; elseif (isstring (resp_input) && ! isempty (resp_input)) resp_name = char (resp_input); elseif (isnumeric (resp_input) && ! isempty (resp_input)) resp_name = 'y'; y_ext = double (resp_input(:)); elseif (is_formula) tparts = strsplit (modelspec, '~'); resp_name = strtrim (tparts{1}); else resp_name = col_names{end}; endif if (opts.PredictorVarsGiven) pv = opts.PredictorVars; if (isnumeric (pv) || islogical (pv)) pred_names_raw = col_names(pv); else pred_names_raw = pv; endif else pred_names_raw = col_names(! strcmp (col_names, resp_name)); endif p_raw = numel (pred_names_raw); if (exist ('y_ext', 'var')) y_full = y_ext; else y_full = double (tbl.(resp_name)(:)); endif endif ## categorical column flags cat_logical = false (1, p_raw); if (! isempty (opts.CategoricalVars)) cv = opts.CategoricalVars; if (islogical (cv)) n_cv = min (numel (cv), p_raw); cat_logical(1:n_cv) = cv(1:n_cv); elseif (isnumeric (cv)) valid_cv = cv(cv > 0 & cv <= p_raw); cat_logical(valid_cv) = true; elseif (iscell (cv)) for i = 1:numel (cv) cat_logical(strcmp (pred_names_raw, cv{i})) = true; endfor endif endif if (istable (data)) for j = 1:p_raw col = tbl.(pred_names_raw{j}); ## A logical or string column groups its observations just as a cell ## or categorical one does, and is coded the same way. if (iscell (col) || isa (col, 'categorical') ... || islogical (col) || isa (col, 'string')) cat_logical(j) = true; endif endfor endif ## missing and excluded masks if (! istable (data)) missing_mask = any (isnan (X_raw), 2) | isnan (y_full); else missing_mask = any (ismissing (tbl), 2); endif excluded_mask = false (n_total, 1); if (! isempty (opts.Exclude)) ex = opts.Exclude(:); if (islogical (ex)) excluded_mask(1:numel (ex)) = ex; else excluded_mask(ex) = true; endif endif subset_mask = ! missing_mask & ! excluded_mask; n_obs = sum (subset_mask); if (n_obs < 1) error ("LinearModel: No observations remain after removing missing/excluded rows."); endif ## weights if (isempty (opts.Weights)) w_full = ones (n_total, 1); else w_full = double (opts.Weights(:)); endif w_sub = w_full(subset_mask); if (is_formula) ## PATH A: Wilkinson formula string if (! istable (data)) tbl_temp = array2table ([X_raw, y_full], 'VariableNames', var_names_all); tbl_sub = tbl_temp(subset_mask, :); else tbl_sub = tbl(subset_mask, :); endif [X_design_sub, ~, coef_names_raw] = parseWilkinsonFormula ( ... modelspec, 'model_matrix', tbl_sub, pred_names_raw(cat_logical)); coef_names = coef_names_raw(:)'; y_sub = y_full(subset_mask); n_coef = size (X_design_sub, 2); has_intercept = any (strcmp (coef_names, '(Intercept)')); [terms, cat_info, term_cols] = terms_from_coefnames (coef_names, ... pred_names_raw, cat_logical, data, tbl_sub); enc_names = term_cols(1:end-1); else ## PATH B: Keyword / numeric terms matrix X_num_full = zeros (n_total, p_raw); cat_str_levels = cell (1, p_raw); for j = 1:p_raw if (istable (data)) col = tbl.(pred_names_raw{j}); if (iscell (col)) ## Appearance order, so that the omitted reference level is the ## one the data shows first; see parseWilkinsonFormula for the ## formula path. [cat_str_levels{j}, ~, ic] = unique (col, 'stable'); X_num_full(:, j) = ic; elseif (isa (col, 'categorical')) cat_str_levels{j} = categories (col); [~, ic] = ismember (cellstr (col), cat_str_levels{j}); X_num_full(:, j) = ic; else X_num_full(:, j) = double (col(:)); cat_str_levels{j} = {}; endif else if (cat_logical(j)) uvals = sort (unique (X_raw(isfinite (X_raw(:,j)), j))); cat_str_levels{j} = strtrim (cellstr (num2str (uvals(:)))); [~, ic] = ismember (X_raw(:,j), uvals); X_num_full(:, j) = ic; else X_num_full(:, j) = X_raw(:, j); cat_str_levels{j} = {}; endif endif endfor X_num_sub = X_num_full(subset_mask, :); y_sub = y_full(subset_mask); ## Whether a categorical is given all its indicator columns depends ## on the intercept, which has to be settled before encoding. [X_enc_sub, enc_names, cat_info] = encode_categorical ( ... X_num_sub, cat_logical, pred_names_raw, cat_str_levels, ... modelspec_has_intercept (modelspec, opts.Intercept)); p_enc = size (X_enc_sub, 2); [terms, has_intercept, coef_names, emsg] = parse_modelspec ( ... modelspec, enc_names, p_enc, opts.Intercept); if (! isempty (emsg)) error ("LinearModel: %s", emsg); endif n_coef = rows (terms); X_design_sub = build_design (terms, X_enc_sub); term_cols = [enc_names, {''}]; endif if (isempty (opts.RobustOpts)) fit = LinearModel.lm_fit (X_design_sub, y_sub, w_sub); RobustS = []; else fit = lm_robust_fit (X_design_sub, y_sub, w_sub, ... opts.RobustOpts.WgtFun, opts.RobustOpts.Tune); RobustS.RobustWgtFun = opts.RobustOpts.WgtFun; RobustS.Tune = opts.RobustOpts.Tune; RobustS.Weights = fit.RobustWeights; endif D = lm_diagnostics (X_design_sub, y_sub, fit, w_sub); p = fit.rank_X; SSE = fit.SSE; SSR = fit.SSR; SST = fit.SST; DFE = fit.DFE; MSE = fit.MSE; RMSE = fit.RMSE; crit = LinearModel.lm_criteria (fit, has_intercept); LogLikelihood = crit.LogLikelihood; AIC = crit.AIC; AICc = crit.AICc; BIC = crit.BIC; CAIC = crit.CAIC; R2_ord = crit.Rsquared; R2_adj = crit.AdjRsquared; Fstat = crit.Fstat; Fpval = crit.Fpval; h = fit.leverage; S2_i_sub = D.S2_i; Raw_full = NaN (n_total, 1); Raw_full(subset_mask) = fit.Raw; Pearson_sub = fit.Raw / sqrt (max (MSE, eps)); Std_sub = fit.Raw ./ (RMSE .* sqrt (max (1 - h, eps))); Stu_sub = fit.Raw ./ (sqrt (max (S2_i_sub, eps)) .* sqrt (max (1 - h, eps))); Pearson_full = NaN (n_total, 1); Std_full = NaN (n_total, 1); Stu_full = NaN (n_total, 1); Pearson_full(subset_mask) = Pearson_sub; Std_full(subset_mask) = Std_sub; Stu_full(subset_mask) = Stu_sub; beta_full = fit.beta; se_full = zeros (n_coef, 1); tstat_full = NaN (n_coef, 1); pval_full = NaN (n_coef, 1); active = fit.active_cols; cov_diag = diag (fit.CovBeta); se_full(active) = sqrt (cov_diag(active)); tstat_full(active) = beta_full(active) ./ se_full(active); pval_full(active) = 2 * tcdf (-abs (tstat_full(active)), DFE); CoeffTable = table (beta_full, se_full, tstat_full, pval_full, ... 'VariableNames', {'Estimate', 'SE', 'tStat', 'pValue'}, ... 'RowNames', coef_names(:)); ResidTable = table (Raw_full, Pearson_full, Stu_full, Std_full, ... 'VariableNames', {'Raw', 'Pearson', 'Studentized', 'Standardized'}); Lev_full = zeros (n_total, 1); CD_full = NaN (n_total, 1); Dff_full = NaN (n_total, 1); S2i_full = NaN (n_total, 1); CR_full = NaN (n_total, 1); Lev_full(subset_mask) = D.Leverage; CD_full(subset_mask) = D.CooksDistance; Dff_full(subset_mask) = D.Dffits; S2i_full(subset_mask) = D.S2_i; CR_full(subset_mask) = D.CovRatio; Dfb_full = NaN (n_total, n_coef); Dfb_full(subset_mask, :) = D.Dfbetas; HatMat_pad = zeros (n_total, n_total); HatMat_pad(subset_mask, subset_mask) = D.HatMatrix; DiagTable = table (Lev_full, CD_full, Dff_full, S2i_full, CR_full, ... Dfb_full, HatMat_pad, ... 'VariableNames', {'Leverage', 'CooksDistance', 'Dffits', 'S2_i', ... 'CovRatio', 'Dfbetas', 'HatMatrix'}); dummy_names = {}; dummy_bases = {}; for ci = 1:numel (cat_info.names) base_nm = cat_info.names{ci}; levels_c = cat_info.levels{ci}; for L = 1:numel (levels_c) dummy_names{end+1} = [base_nm, '_', char(levels_c{L})]; dummy_bases{end+1} = base_nm; endfor endfor if (has_intercept) orig_idx = find (! strcmp (coef_names, '(Intercept)')); else orig_idx = 1:numel (coef_names); endif non_int = coef_names(orig_idx); disp_terms = {}; grp_cols = {}; for t = 1:numel (non_int) factors_t = strsplit (non_int{t}, ':'); for f = 1:numel (factors_t) idx = find (strcmp (dummy_names, factors_t{f}), 1); if (! isempty (idx)) factors_t{f} = dummy_bases{idx}; endif endfor nm = strjoin (factors_t, ':'); k = find (strcmp (disp_terms, nm), 1); if (isempty (k)) disp_terms{end+1} = nm; grp_cols{end+1} = orig_idx(t); else grp_cols{k}(end+1) = orig_idx(t); endif endfor ## Model formula. TERMS is expressed over the encoded design columns; the ## formula is expressed over the model's variables, so a categorical's ## indicator columns have to be folded back onto the variable they came ## from before the term names can be built. var_idx = zeros (1, p_raw); for j = 1:p_raw k = find (strcmp (var_names_all, pred_names_raw{j}), 1); if (! isempty (k)) var_idx(j) = k; endif endfor ## Indexed by the columns of TERMS, which are not the coefficient ## names: a factor appearing only inside an interaction or a power has a ## column without ever being a coefficient. [enc2raw, col_pow] = encodednames_to_row (term_cols(1:end-1), ... pred_names_raw, cat_info); terms_var = variable_level_terms (terms(:, 1:end-1), enc2raw, var_idx, ... n_vars, col_pow); ## A response passed apart from the table is still a variable of the ## model, and the formula is written over every one of them, so it takes ## a column of the terms matrix like any other. form_vars = var_names_all(:)'; form_terms = terms_var; if (! any (strcmp (form_vars, resp_name))) form_vars{end+1} = resp_name; form_terms(:, end+1) = 0; endif FormulaObj = LinearFormula (form_terms, form_vars, ... 'ResponseName', resp_name); ObsInfo = table (w_full, excluded_mask, missing_mask, subset_mask, ... 'VariableNames', {'Weights', 'Excluded', 'Missing', 'Subset'}); if (! istable (data)) VarsTable = array2table ([X_raw, y_full], 'VariableNames', var_names_all); else VarsTable = tbl; endif ## A variable the model does not actually use is not a predictor of it: ## a table may carry columns the formula never mentions, and a terms ## matrix may zero a predictor out. MATLAB drops these from ## PredictorNames and NumPredictors, and marks VariableInfo.InModel ## false, keeping them in VariableNames. pred_in_model = false (1, p_raw); for j = 1:p_raw ci = []; if (! isempty (cat_info) && isfield (cat_info, 'names') ... && ! isempty (cat_info.names)) ci = find (strcmp (cat_info.names, pred_names_raw{j}), 1); endif if (isempty (ci)) ecols = find (strcmp (enc_names, pred_names_raw{j})); else levels_j = cat_info.levels{ci}; ecols = []; for L = 2:numel (levels_j) lvl_name = sprintf ("%s_%s", pred_names_raw{j}, char (levels_j{L})); ecols = [ecols, find(strcmp (enc_names, lvl_name))]; endfor endif pred_in_model(j) = ! isempty (ecols) ... && any (any (terms(:, ecols) != 0)); endfor pred_names_used = pred_names_raw(pred_in_model); nv_total = numel (var_names_all); vi_class = cell (nv_total, 1); vi_range = cell (nv_total, 1); vi_inmodel = false (nv_total, 1); vi_iscat = false (nv_total, 1); for j = 1:nv_total vname = var_names_all{j}; is_resp_var = strcmp (vname, resp_name); j_pred = find (strcmp (pred_names_raw, vname), 1); if (! istable (data)) if (! is_resp_var && ! isempty (j_pred)) col_d = X_raw(:, j_pred); vi_iscat(j) = cat_logical(j_pred); else col_d = y_full; endif vi_class{j} = 'double'; fv = col_d(subset_mask & isfinite (col_d)); vi_range{j} = ifelse (isempty (fv), [NaN, NaN], [min(fv), max(fv)]); else col_d = tbl.(vname); col_d = col_d(subset_mask); [vi_class{j}, vi_range{j}] = variable_class_and_range (col_d); if (! is_resp_var && ! isempty (j_pred)) vi_iscat(j) = cat_logical(j_pred); endif endif if (! is_resp_var && ! isempty (j_pred) && pred_in_model(j_pred)) vi_inmodel(j) = true; endif endfor VarInfo = table (vi_class, vi_range, vi_inmodel, vi_iscat, ... 'VariableNames', {'Class', 'Range', 'InModel', 'IsCategorical'}, ... 'RowNames', var_names_all(:)); this.Coefficients = CoeffTable; this.CoefficientCovariance = fit.CovBeta; this.CoefficientNames = coef_names; this.NumCoefficients = n_coef; this.NumEstimatedCoefficients = p; this.DFE = DFE; this.Diagnostics = DiagTable; this.LogLikelihood = LogLikelihood; this.ModelCriterion = struct ('AIC', AIC, 'AICc', AICc, ... 'BIC', BIC, 'CAIC', CAIC); if (isnan (Fstat)) NullModelName = NaN; else NullModelName = 'constant'; endif this.ModelFitVsNullModel = struct ('Fstat', Fstat, ... 'Pvalue', Fpval, ... 'NullModel', NullModelName); this.MSE = MSE; this.Residuals = ResidTable; this.RMSE = RMSE; this.Rsquared = struct ('Ordinary', R2_ord, 'Adjusted', R2_adj); this.SSE = SSE; this.SSR = SSR; this.SST = SST; this.Robust = RobustS; this.Steps = []; this.Formula = FormulaObj; this.NumObservations = n_obs; this.NumPredictors = numel (pred_names_used); this.NumVariables = n_vars; this.ObservationInfo = ObsInfo; this.ObservationNames = {}; ## MATLAB returns these two as columns, and CoefficientNames as a row. this.PredictorNames = pred_names_used(:); this.ResponseName = resp_name; this.VariableInfo = VarInfo; this.VariableNames = var_names_all(:); this.Variables = VarsTable; this.DesignMatrix = X_design_sub; this.ActiveCols = fit.active_cols; this.HasIntercept = has_intercept; this.ResponseVector = y_full; this.WeightVector = w_full; this.SubsetMask = subset_mask; this.TermsMatrix = terms; this.CatLevelInfo = cat_info; this.EncPredictorNames = enc_names; this.PredictorNamesRaw = pred_names_raw(:); this.EffectContrasts = lm_effects_contrasts (this); this.InteractionContrasts = lm_interaction_contrasts (this); this.TermGroups = struct ('Name', disp_terms, 'Cols', grp_cols); this.OrigOpts = opts; if (! is_formula) this.EncodedPredMatrix = X_enc_sub; endif ## The fitted value of an observation is the model's prediction at its ## predictors, whether or not the observation was used in the fit, so ## excluded rows and rows missing only the response carry a value. It ## is computed through PREDICT, which cannot then disagree with it, and ## which leaves NaN wherever a predictor itself is missing. this.Fitted = predict (this, this.Variables); endfunction ## -*- texinfo -*- ## @deftypefn {LinearModel} {@var{ypred} =} predict (@var{mdl}, @var{Xnew}) ## @deftypefnx {LinearModel} {@var{ypred} =} predict (@var{mdl}) ## @deftypefnx {LinearModel} {[@var{ypred}, @var{yci}] =} predict (@var{mdl}, @var{Xnew}) ## @deftypefnx {LinearModel} {[@var{ypred}, @var{yci}] =} predict (@var{mdl}, @var{Xnew}, @var{Name}, @var{Value}) ## ## Predict responses from a fitted linear regression model. ## ## @code{@var{ypred} = predict (@var{mdl}, @var{Xnew})} returns the fitted ## response values at the new predictor locations in @var{Xnew}. @var{Xnew} ## can be a numeric matrix with one column per predictor in the same order ## as the training data, or a table whose column names match ## @code{@var{mdl}.PredictorNames}. Rows containing @code{NaN} are returned ## as @code{NaN} without error. ## ## @code{@var{ypred} = predict (@var{mdl})} omits @var{Xnew} and returns ## fitted values for the original training observations in their original ## row order. Rows that were excluded or contained missing values are ## returned as @code{NaN}. The result is identical to ## @code{@var{mdl}.Fitted}. ## ## @code{[@var{ypred}, @var{yci}] = predict (@dots{})} also returns ## @var{yci}, an @math{n}-by-2 matrix of confidence bounds where column 1 is ## the lower bound and column 2 is the upper bound. By default these are ## 95% pointwise confidence intervals on the mean response. ## ## Name-Value pair arguments: ## ## @multitable @columnfractions 0.2 0.78 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab Significance level for the confidence ## interval, specified as a scalar in @math{[0,1]}. The interval has ## coverage @math{100(1-\alpha)\%}. Default is @code{0.05}, giving a 95% ## interval. ## ## @item @qcode{'Prediction'} @tab Type of interval to compute. ## @code{"curve"} (default) gives a confidence interval on the mean response ## @math{f(x)}. @code{"observation"} gives a wider prediction interval for ## a single future observation @math{y = f(x) + \varepsilon}, which accounts ## for both estimation uncertainty and irreducible noise; it adds ## @code{@var{mdl}.MSE} to the variance before computing the half-width. ## ## @item @qcode{'Simultaneous'} @tab Logical flag controlling whether ## the bounds are simultaneous or pointwise. When @code{true}, ## Scheff@'{e}'s method is used so the entire predicted curve lies within ## the band with @math{100(1-\alpha)\%} confidence; these bands are always ## wider than pointwise ones. Default is @code{false}. ## @end multitable ## ## @end deftypefn function [ypred, yci] = predict (mdl, Xnew, varargin) alpha = 0.05; pred_obs = false; simultan = false; i = 1; while (i <= numel (varargin)) if (strcmpi (varargin{i}, 'Alpha')) alpha = varargin{i+1}; if (! isscalar (alpha) || ! isnumeric (alpha) || alpha < 0 || alpha > 1) error ("predict: Alpha must be a scalar in [0,1]."); endif i += 2; elseif (strcmpi (varargin{i}, 'Prediction')) pred_str = lower (char (varargin{i+1})); if (! any (strcmp (pred_str, {'curve', 'observation'}))) error ("predict: Prediction must be 'curve' or 'observation'."); endif pred_obs = strcmp (pred_str, 'observation'); i += 2; elseif (strcmpi (varargin{i}, 'Simultaneous')) simultan = logical (varargin{i+1}); i += 2; else error ("predict: unknown option '%s'.", varargin{i}); endif endwhile if (nargin < 2 || isempty (Xnew)) Xnew = mdl.Variables; endif pred_names = mdl.PredictorNames; p_raw = mdl.NumPredictors; if (istable (Xnew)) n_new = height (Xnew); X_raw = zeros (n_new, p_raw); for j = 1:p_raw if (! ismember (pred_names{j}, Xnew.Properties.VariableNames)) error ("predict: Xnew table is missing predictor '%s'.", pred_names{j}); endif col = Xnew.(pred_names{j}); if (iscell (col)) cat_idx = []; if (! isempty (mdl.CatLevelInfo.names)) cat_idx = find (strcmp (mdl.CatLevelInfo.names, pred_names{j})); endif if (! isempty (cat_idx)) levels_j = mdl.CatLevelInfo.levels{cat_idx}; codes = zeros (n_new, 1); for k = 1:numel (levels_j) codes(strcmp (col, levels_j{k})) = k; endfor X_raw(:, j) = codes; endif else X_raw(:, j) = double (col); endif endfor else X_raw = double (Xnew); if (columns (X_raw) != p_raw) error ("predict: Xnew must have %d columns.", p_raw); endif n_new = rows (X_raw); endif nan_rows = any (isnan (X_raw), 2); X_enc_new = reencode_predictors (X_raw, pred_names, mdl.CatLevelInfo, mdl.EncPredictorNames); X_design_new = build_design (mdl.TermsMatrix, X_enc_new); beta = mdl.Coefficients.Estimate; ypred = X_design_new * beta; ypred(nan_rows) = NaN; if (nargout > 1) CovB = mdl.CoefficientCovariance; var_cv = sum ((X_design_new * CovB) .* X_design_new, 2); if (pred_obs) var_ci = var_cv + mdl.MSE; else var_ci = var_cv; endif p_est = mdl.NumEstimatedCoefficients; if (simultan) mult = sqrt (p_est * finv (1 - alpha, p_est, mdl.DFE)); else mult = tinv (1 - alpha / 2, mdl.DFE); endif hw = mult * sqrt (max (var_ci, 0)); yci = [ypred - hw, ypred + hw]; yci(nan_rows,:) = NaN; endif endfunction ## -*- texinfo -*- ## @deftypefn {LinearModel} {@var{ysim} =} random (@var{mdl}, @var{Xnew}) ## ## Simulate responses with random noise from a fitted linear regression ## model. ## ## @code{@var{ysim} = random (@var{mdl}, @var{Xnew})} computes the fitted ## response at each row of @var{Xnew} and then adds independent Gaussian ## noise to each value. The noise is drawn from @math{N(0, \sigma^2)} where ## @math{\sigma^2} is the estimated error variance @code{@var{mdl}.MSE} ## (mean squared error of the fit). The result is a column vector of the ## same length as the number of rows in @var{Xnew}. ## ## @var{Xnew} is required and must be non-empty. It can be a numeric ## matrix with one column per predictor in the same order as the training ## data, or a table whose column names match ## @code{@var{mdl}.PredictorNames}. Unlike @code{predict}, there is no ## no-argument form; the predictor locations must always be supplied ## explicitly. ## ## Because the added noise is drawn freshly on every call, two calls with ## the same @var{Xnew} will generally produce different output. To get ## reproducible results, set the random seed with @code{rand ('state', s)} ## before calling @code{random}. ## ## For deterministic predictions without noise, use @code{predict} or ## @code{feval}. @code{predict} also provides confidence intervals on the ## mean response. ## ## @end deftypefn function ysim = random (mdl, Xnew, varargin) if (nargin < 2) error ("random: Not enough input arguments."); endif if (nargin > 2) error ("random: Too many input arguments."); endif if (isempty (Xnew)) error ("random: Xnew must have %d columns.", mdl.NumPredictors); endif ypred = predict (mdl, Xnew); ysim = ypred + sqrt (mdl.MSE) .* randn (numel (ypred), 1); endfunction ## -*- texinfo -*- ## @deftypefn {LinearModel} {@var{ypred} =} feval (@var{mdl}, @var{X}) ## @deftypefnx {LinearModel} {@var{ypred} =} feval (@var{mdl}, @var{x1}, @var{x2}, @dots{}, @var{xp}) ## ## Predict responses of a fitted linear regression model using separate ## predictor inputs. ## ## @code{@var{ypred} = feval (@var{mdl}, @var{X})} accepts a single ## numeric matrix @var{X} with one column per predictor in the same order ## as the training data, or a table whose column names match ## @code{@var{mdl}.PredictorNames}. The output is an @math{n}-by-1 column ## vector. Rows that contain @code{NaN} in any predictor column are ## returned as @code{NaN}. ## ## @code{@var{ypred} = feval (@var{mdl}, @var{x1}, @var{x2}, @dots{}, ## @var{xp})} accepts exactly @code{@var{mdl}.NumPredictors} separate ## arguments, one per predictor variable. All non-scalar arguments must ## have the same size; a scalar argument is broadcast to that size ## automatically. The output shape follows the shape of the non-scalar ## inputs: column vector inputs give a column vector output, row vector ## inputs give a row vector output, and all-scalar inputs give a scalar. ## This form is convenient when predictor data is already stored in separate ## vectors rather than a combined matrix. ## ## @code{feval} gives the same numerical predictions as @code{predict} but ## does not support confidence intervals. Use @code{predict} when you also ## need bounds on the response. Because a @code{LinearModel} object behaves ## like a function through @code{feval}, it can be passed directly to ## routines that accept a function handle, such as @code{fminsearch} or ## @code{integral}. ## ## @end deftypefn function ypred = feval (mdl, varargin) p_raw = mdl.NumPredictors; n_extra = nargin - 1; if (n_extra < 1) error ("feval: Not enough input arguments."); endif if (n_extra == 1) Xnew = varargin{1}; if (istable (Xnew)) for j = 1:p_raw if (! ismember (mdl.PredictorNames{j}, Xnew.Properties.VariableNames)) error (strcat ("feval: X does not contain one or more", ... " predictor variables needed for this model.")); endif endfor else if (columns (double (Xnew)) != p_raw) error ("feval: Predictor data matrix must have %d columns.", p_raw); endif endif ypred = predict (mdl, Xnew); elseif (n_extra == p_raw) for i = 1:n_extra if (ischar (varargin{i}) || iscategorical (varargin{i})) if (iscategorical (varargin{i})) lvl_str = char (varargin{i}); else lvl_str = varargin{i}; endif ci = []; if (! isempty (mdl.CatLevelInfo.names)) ci = find (strcmp (mdl.CatLevelInfo.names, mdl.PredictorNames{i})); endif if (isempty (ci)) error ("feval: predictor '%s' is not categorical.", mdl.PredictorNames{i}); endif levels_i = mdl.CatLevelInfo.levels{ci}; code = find (strcmp (levels_i, lvl_str), 1); if (isempty (code)) code = NaN; endif varargin{i} = code; endif endfor ref_size = []; for i = 1:n_extra if (! isscalar (varargin{i})) s_i = size (varargin{i}); if (isempty (ref_size)) ref_size = s_i; elseif (! isequal (s_i, ref_size)) error ("feval: All input arguments must be the same size."); endif endif endfor if (isempty (ref_size)) ref_size = [1, 1]; endif n_pts = prod (ref_size); Xmat = zeros (n_pts, p_raw); for i = 1:n_extra ai = varargin{i}; if (isscalar (ai)) Xmat(:, i) = ai; else Xmat(:, i) = ai(:); endif endfor ypred = reshape (predict (mdl, Xmat), ref_size); else error (strcat ("feval: Incorrect number of input arguments. You", ... " must provide either %d separate predictor", ... " variable arguments, or one predictor matrix with", ... " %d columns."), p_raw, p_raw); endif endfunction ## -*- texinfo -*- ## @deftypefn {LinearModel} {@var{ci} =} coefCI (@var{mdl}) ## @deftypefnx {LinearModel} {@var{ci} =} coefCI (@var{mdl}, @var{alpha}) ## ## Confidence intervals for the coefficient estimates of a fitted linear ## regression model. ## ## @code{@var{ci} = coefCI (@var{mdl})} returns 95% confidence intervals ## for every coefficient in @var{mdl} using a default significance level of ## @code{0.05}. ## ## @code{@var{ci} = coefCI (@var{mdl}, @var{alpha})} uses the significance ## level @var{alpha}, a scalar in @math{[0, 1]}. The resulting intervals ## have coverage @math{100(1-\alpha)\%}. Setting @var{alpha} to @code{0} ## produces intervals of infinite width; setting it to @code{1} collapses ## each interval to the corresponding point estimate. ## ## The output @var{ci} is a @math{k}-by-2 numeric matrix where ## @math{k = } @code{@var{mdl}.NumCoefficients}. Row @math{j} contains ## the interval for the @math{j}-th coefficient, whose name is stored in ## @code{@var{mdl}.CoefficientNames@{j@}}. Column 1 is the lower bound and ## column 2 is the upper bound. The midpoint of each interval equals the ## corresponding point estimate in @code{@var{mdl}.Coefficients.Estimate}. ## ## Intervals use the Wald method: ## @math{b_j \pm t_{(1-\alpha/2,\,\mathrm{DFE})}\,\mathrm{SE}(b_j)}, ## where @math{b_j} is the coefficient estimate, @math{\mathrm{SE}(b_j)} is ## its standard error from @code{@var{mdl}.Coefficients.SE}, and the ## critical value is the @math{1-\alpha/2} quantile of the ## @math{t}-distribution with @code{@var{mdl}.DFE} degrees of freedom. ## In rank-deficient models, aliased coefficients have ## @math{\mathrm{SE} = 0} and their row in @var{ci} is @code{[0, 0]}. ## ## @end deftypefn function ci = coefCI (mdl, alpha) if (nargin > 2) error ("coefCI: Too many input arguments."); endif if (nargin < 2) alpha = 0.05; endif if (! isscalar (alpha)) error (strcat ("coefCI: Invalid argument at position 2.", ... " Value must be a scalar.")); endif if (! (alpha >= 0)) error (strcat ("coefCI: Invalid argument at position 2.", ... " Value must be greater than or equal to 0.")); endif if (alpha > 1) error (strcat ("coefCI: Invalid argument at position 2.", ... " Value must be less than or equal to 1.")); endif t = tinv (1 - alpha / 2, mdl.DFE); b = mdl.Coefficients.Estimate; se = mdl.Coefficients.SE; ci = [b - t .* se, b + t .* se]; endfunction ## -*- texinfo -*- ## @deftypefn {LinearModel} {@var{p} =} coefTest (@var{mdl}) ## @deftypefnx {LinearModel} {@var{p} =} coefTest (@var{mdl}, @var{H}) ## @deftypefnx {LinearModel} {@var{p} =} coefTest (@var{mdl}, @var{H}, @var{C}) ## @deftypefnx {LinearModel} {[@var{p}, @var{F}] =} coefTest (@dots{}) ## @deftypefnx {LinearModel} {[@var{p}, @var{F}, @var{r}] =} coefTest (@dots{}) ## ## Linear hypothesis test on the coefficients of a fitted linear regression ## model. ## ## @code{coefTest} tests whether one or more linear combinations of the ## fitted coefficients equal specified constants. Each linear combination ## is encoded as a row of the contrast matrix @var{H}, and the right-hand ## side is given by @var{C}. ## ## @code{@var{p} = coefTest (@var{mdl})} performs the overall model F-test: ## it tests the joint null hypothesis that every coefficient except the ## intercept is zero. The returned p-value matches the F-statistic line ## printed at the bottom of the model display. ## ## @code{@var{p} = coefTest (@var{mdl}, @var{H})} tests the null hypothesis ## @math{H \beta = 0}, where @math{\beta} is the full coefficient vector ## of length @math{k = } @code{@var{mdl}.NumCoefficients}. @var{H} must be ## a full-rank numeric matrix with @math{k} columns; each row specifies one ## linear constraint. To test a single coefficient, use a row vector with a ## @code{1} in that coefficient's position and zeros elsewhere; the ## resulting F-statistic equals the square of the corresponding t-statistic ## in @code{@var{mdl}.Coefficients}. To test a categorical predictor that ## expands to multiple indicator columns, include one row per indicator in ## @var{H}. ## ## @code{@var{p} = coefTest (@var{mdl}, @var{H}, @var{C})} tests ## @math{H \beta = C} instead of zero. @var{C} must be a numeric vector ## with the same number of elements as rows of @var{H}; both row and column ## vectors are accepted. ## ## The second output @var{F} is the value of the F-statistic: ## @math{F = (H\hat{\beta} - C)^\prime (H V H^\prime)^{-1} ## (H\hat{\beta} - C) / r}, where @math{V} is ## @code{@var{mdl}.CoefficientCovariance} and @math{r} is the number of ## rows of @var{H}. The third output @var{r} is that numerator degrees of ## freedom; the denominator degrees of freedom is @code{@var{mdl}.DFE}. ## Under the null hypothesis @math{F} follows an @math{F(r, \mathrm{DFE})} ## distribution and the p-value is the upper-tail probability. When ## @var{H} is rank-deficient but contains no @code{NaN}, both @var{p} and ## @var{F} are returned as @code{NaN} without an error. ## ## @end deftypefn function [p, F, r] = coefTest (mdl, varargin) if (nargout > 3) error ("coefTest: Too many output arguments."); endif if (numel (varargin) > 2) error ("coefTest: Too many input arguments."); endif k = mdl.NumCoefficients; if (numel (varargin) >= 1 && ! isempty (varargin{1})) H = varargin{1}; if (! isnumeric (H)) error ("coefTest: H must be a %d-by-%d numeric matrix.", size (H, 1), k); endif if (size (H, 2) != k) error ("coefTest: H must be a %d-by-%d numeric matrix.", size (H, 1), k); endif if (any (any (isnan (H)))) error (strcat ("coefTest: H is not full rank and hypotheses", ... " are not consistent.")); endif r = size (H, 1); if (numel (varargin) == 2) C = varargin{2}; if (! isnumeric (C)) error ("coefTest: C must be a numeric vector."); endif C = C(:); if (numel (C) != r) error ("coefTest: H must be a %d-by-%d numeric matrix.", numel (C), k); endif else C = zeros (r, 1); endif else if (mdl.HasIntercept && k > 1) H = [zeros(k-1, 1), eye(k-1)]; r = k - 1; else H = eye (k); r = k; endif C = zeros (r, 1); endif b = mdl.Coefficients.Estimate; V = mdl.CoefficientCovariance; HVH = H * V * H'; Hb_c = H * b - C; if (rcond (HVH) < eps (class (HVH))) F = NaN; p = NaN; else F = (Hb_c' * (HVH \ Hb_c)) / r; p = betainc (mdl.DFE / (mdl.DFE + r * F), mdl.DFE / 2, r / 2); endif endfunction ## -*- texinfo -*- ## @deftypefn {LinearModel} {@var{p} =} dwtest (@var{mdl}) ## @deftypefnx {LinearModel} {@var{p} =} dwtest (@var{mdl}, @var{method}) ## @deftypefnx {LinearModel} {@var{p} =} dwtest (@var{mdl}, @var{method}, @var{tail}) ## @deftypefnx {LinearModel} {[@var{p}, @var{DW}] =} dwtest (@dots{}) ## ## Durbin-Watson test for serial autocorrelation of linear regression ## residuals. ## ## @code{dwtest} checks whether the raw residuals of @var{mdl} are ## correlated with their immediate neighbours in observation order, which ## would violate the independence assumption of ordinary least squares. ## The null hypothesis is that there is no autocorrelation. A small ## p-value gives evidence against this and suggests that the residuals are ## not independent. This test is most meaningful when the observations ## have a natural ordering, such as a time series. ## ## The test is based on the Durbin-Watson statistic ## @math{DW = \sum_{i=1}^{n-1}(e_{i+1}-e_i)^2 / \sum_{i=1}^{n}e_i^2}, ## where @math{e_i} are the raw residuals of the active (non-excluded) ## observations. The statistic always lies in @math{[0, 4]}: values near ## @math{2} indicate no autocorrelation, values well below @math{2} ## indicate positive autocorrelation (adjacent residuals tend to have the ## same sign), and values well above @math{2} indicate negative ## autocorrelation (adjacent residuals tend to alternate in sign). ## ## @var{method} controls how the p-value is computed and defaults to ## @qcode{'exact'}. @qcode{'exact'} uses the eigenvalues of the ## projected differencing matrix together with Imhof's numerical ## integration to obtain a precise p-value; this is slower but accurate ## for any sample size. @qcode{'approximate'} uses a normal approximation ## based on the first two moments of the DW distribution under the null; ## this is faster and adequate for large samples but less reliable for ## small ones. The argument is case-insensitive. ## ## @var{tail} selects the alternative hypothesis and defaults to ## @qcode{'both'}. @qcode{'right'} tests for positive autocorrelation ## (@math{DW < 2}), @qcode{'left'} tests for negative autocorrelation ## (@math{DW > 2}), and @qcode{'both'} tests for autocorrelation in ## either direction. The one-sided p-values always satisfy ## @math{p_{\mathrm{right}} + p_{\mathrm{left}} = 1}, and the two-sided ## p-value equals @math{2\min(p_{\mathrm{right}}, p_{\mathrm{left}})}. ## ## The second output @var{DW} is the value of the Durbin-Watson statistic ## itself; it does not depend on @var{method} or @var{tail}. ## ## @end deftypefn function [p, DW] = dwtest (mdl, varargin) if (nargout > 2) error ("dwtest: Too many output arguments."); endif if (numel (varargin) > 2) error ("dwtest: Too many input arguments."); endif method = 'exact'; tail = 'both'; if (numel (varargin) >= 1) method = varargin{1}; endif if (numel (varargin) == 2) tail = varargin{2}; endif if (! ischar (method) || ! ismember (lower (method), {'exact', 'approximate'})) error ("dwtest: The METHOD argument must be 'approximate' or 'exact'."); endif method = lower (method); tail = lower (tail); subset = logical (mdl.ObservationInfo.Subset); r = mdl.Residuals.Raw(subset); r = r(:); [p, DW] = dwtest (r, mdl.DesignMatrix, 'Method', method, 'Tail', tail); endfunction ## -*- texinfo -*- ## @deftypefn {LinearModel} {@var{NewMdl} =} addTerms (@var{mdl}, @var{terms}) ## ## Add terms to a fitted linear regression model. ## ## @code{addTerms} returns a new @code{LinearModel} refitted on the same ## data and settings as @var{mdl} with the specified @var{terms} appended ## to the model formula. The original model @var{mdl} is never modified; ## all settings including observation weights, excluded rows, and ## categorical variable encodings are carried over automatically. To ## update a model in place, reassign the result: ## @code{@var{mdl} = addTerms (@var{mdl}, @var{terms})}. ## ## @var{terms} may be a character vector in Wilkinson notation. Use ## @code{'x1'} for a main effect, @code{'x1:x2'} for a two-way ## interaction, @code{'x1*x2'} to add both main effects and their ## interaction in one step, @code{'x1 + x2^2'} to add several terms at ## once, or @code{'1'} to add an intercept to a no-intercept model. A ## bare power term @code{'x1^2'} adds @code{x1} together with ## @code{x1^2} (and any intermediate powers), matching the Wilkinson ## hierarchy convention; power notation used inside an interaction, e.g. ## @code{'x1:x2^2'}, adds only that exact interaction term. All ## variable names must match entries in @code{@var{mdl}.PredictorNames}. ## ## @var{terms} may also be a numeric matrix of size @var{t}-by-@var{v}, ## where @var{t} is the number of terms to add and @var{v} equals ## @code{@var{mdl}.NumVariables}. Entry @code{T(i,j)} is the exponent of ## variable @var{j} in term @var{i}. For example, in a model with ## variables @code{x1}, @code{x2}, @code{y}: @code{[0 0 0]} is the ## intercept, @code{[0 1 0]} is @code{x2}, @code{[1 1 0]} is ## @code{x1:x2}, and @code{[2 0 0]} is @code{x1^2}. The last column ## (response) is always zero. A matrix with @code{@var{mdl}.NumPredictors} ## columns is also accepted and is automatically padded with a trailing ## zero column for the response. ## ## Terms that are already present in @var{mdl} are silently skipped. If ## every specified term already exists, a warning is issued and @var{mdl} ## is returned unchanged. For a categorical predictor, @code{addTerms} ## adds the full group of indicator variables for that predictor in one ## step rather than adding individual indicator columns. ## ## @end deftypefn function NewMdl = addTerms (mdl, terms) if (nargin < 2) error ("addTerms: Not enough input arguments."); endif if (nargin > 2) error ("addTerms: Too many input arguments."); endif nv = mdl.NumVariables; ## The candidates, not the chosen: a term may name a predictor the ## current model does not use, and nv - 1 counts them all. pred = mdl.PredictorNamesRaw; if (isnumeric (terms) || islogical (terms)) T = double (terms); if (isempty (T)) error ("addTerms: Terms matrix must have %d columns.", nv); endif if (columns (T) == nv - 1) T = [T, zeros(rows (T), 1)]; endif if (columns (T) != nv) error ("addTerms: Terms matrix must have %d columns.", nv); endif elseif (ischar (terms) || isstring (terms)) terms_str = strtrim (char (terms)); T = zeros (0, nv); plus_tokens = strsplit (terms_str, '+'); for ti = 1:numel (plus_tokens) tok = strtrim (plus_tokens{ti}); if (isempty (tok)); continue; endif if (! isempty (strfind (tok, '*'))) star_parts = cellfun (@strtrim, strsplit (tok, '*'), ... 'UniformOutput', false); n_sp = numel (star_parts); colon_toks = {}; for mask = 1:(2^n_sp - 1) sub = {}; for bit = 1:n_sp if (bitand (mask, 2^(bit-1))) sub{end+1} = star_parts{bit}; endif endfor colon_toks{end+1} = strjoin (sub, ':'); endfor else colon_toks = {tok}; endif for ci = 1:numel (colon_toks) ctok = strtrim (colon_toks{ci}); if (strcmp (ctok, '1')) T = [T; zeros(1, nv)]; else parts = cellfun (@strtrim, strsplit (ctok, ':'), ... 'UniformOutput', false); if (numel (parts) == 1) part = parts{1}; hat = strfind (part, '^'); if (isempty (hat)) vname = part; exp = 1; else vname = strtrim (part(1:hat(1)-1)); exp = str2double (strtrim (part(hat(1)+1:end))); endif idx = find (strcmp (pred, vname)); if (isempty (idx)) error ("addTerms: Unrecognized variable: '%s'.", vname); endif for k = 1:exp row = zeros (1, nv); row(idx(1)) = k; T = [T; row]; endfor else row = zeros (1, nv); for pi = 1:numel (parts) part = parts{pi}; hat = strfind (part, '^'); if (isempty (hat)) vname = part; exp = 1; else vname = strtrim (part(1:hat(1)-1)); exp = str2double (strtrim (part(hat(1)+1:end))); endif idx = find (strcmp (pred, vname)); if (isempty (idx)) error ("addTerms: Unrecognized variable: '%s'.", vname); endif row(idx(1)) = row(idx(1)) + exp; endfor T = [T; row]; endif endif endfor endfor else error (strcat ("addTerms: Model update specification must be a", ... " model formula character vector or string scalar,", ... " or a terms matrix")); endif cat_info = mdl.CatLevelInfo; ename = mdl.EncPredictorNames; n_pred = nv - 1; ## Every candidate predictor's encoded column name(s), whether or not ## it is part of the model yet: a plain predictor occupies one column, ## a categorical one column per non-reference level, named exactly as ## reencode_predictors expects to find them. target_names = cell (n_pred, 1); for j = 1:n_pred ci = []; if (! isempty (cat_info) && isfield (cat_info, 'names') ... && ! isempty (cat_info.names)) ci = find (strcmp (cat_info.names, pred{j})); endif if (isempty (ci)) target_names{j} = pred(j); else levels_j = cat_info.levels{ci}; lvl_names = cell (1, numel (levels_j) - 1); for L = 2:numel (levels_j) lvl_names{L-1} = sprintf ("%s_%s", pred{j}, char (levels_j{L})); endfor target_names{j} = lvl_names; endif endfor target_enc = [target_names{:}]; nc_full = numel (target_enc) + 1; ## Re-slot the model's current encoded terms into that full space, so ## a predictor with no columns yet simply stays all zero. existing = zeros (rows (mdl.TermsMatrix), nc_full); existing(:, end) = mdl.TermsMatrix(:, end); for c = 1:numel (ename) col = find (strcmp (target_enc, ename{c}), 1); existing(:, col) = mdl.TermsMatrix(:, c); endfor ## Expand each requested raw-predictor row into that same full space. new_rows = zeros (0, nc_full); for i = 1:rows (T) orig_row = T(i, 1:n_pred); any_cat = false; cat_rows = zeros (0, nc_full); cont_row = zeros (1, nc_full); col_off = 0; for j = 1:n_pred n_cols = numel (target_names{j}); if (orig_row(j) != 0) if (n_cols > 1) any_cat = true; for k = 1:n_cols r = zeros (1, nc_full); r(col_off + k) = 1; cat_rows = [cat_rows; r]; endfor else cont_row(col_off + 1) = orig_row(j); endif endif col_off = col_off + n_cols; endfor if (any_cat) for k = 1:rows (cat_rows) new_rows = [new_rows; cat_rows(k,:) + cont_row]; endfor else new_rows = [new_rows; cont_row]; endif endfor is_new = false (rows (new_rows), 1); for i = 1:rows (new_rows) is_new(i) = ! any (all (existing == new_rows(i,:), 2)); endfor new_rows = new_rows(is_new, :); if (isempty (new_rows)) warning ("addTerms: There are no new terms among the terms you specified."); NewMdl = mdl; return; endif combined = [existing; new_rows]; int_mask = all (combined(:, 1:end-1) == 0, 2); body = combined(! int_mask, :); n_nonzero = sum (body(:, 1:end-1) != 0, 2); degree = sum (body(:, 1:end-1), 2); tier = zeros (rows (body), 1); tier(n_nonzero == 1 & degree == 1) = 1; tier(n_nonzero == 2) = 2; tier(n_nonzero == 1 & degree > 1) = 3; bitmask = zeros (rows (body), 1); for i = 1:rows (body) bitmask(i) = sum (2 .^ (find (body(i, 1:end-1)) - 1)); endfor [~, order] = sortrows ([tier, bitmask]); combined = [combined(int_mask, :); body(order, :)]; NewMdl = lm_refit (mdl, combined); endfunction ## -*- texinfo -*- ## @deftypefn {LinearModel} {@var{NewMdl} =} removeTerms (@var{mdl}, @var{terms}) ## ## Remove terms from a fitted linear regression model. ## ## @code{removeTerms} returns a new @code{LinearModel} refitted on the same ## data and settings as @var{mdl}, but with the specified @var{terms} ## dropped from the model formula. The original model @var{mdl} is never ## modified; all settings including observation weights, excluded rows, and ## categorical variable encodings are carried over automatically. To ## update a model in place, reassign the result: ## @code{@var{mdl} = removeTerms (@var{mdl}, @var{terms})}. ## ## @var{terms} may be a character vector in Wilkinson notation. Use ## @code{'x2'} to remove a main effect, @code{'x1:x2'} to remove an ## interaction, @code{'1'} to remove the intercept, or @code{'x1 + x2^2'} ## to remove several terms at once. A bare power term @code{'x1^2'} ## removes @code{x1} together with @code{x1^2} (and any intermediate ## powers), matching the Wilkinson hierarchy convention; power notation ## used inside an interaction, e.g. @code{'x1:x2^2'}, removes only that ## exact interaction term. The star operator @code{'x1*x2'} removes the ## main effects @code{x1} and @code{x2} together with their interaction ## @code{x1:x2} in a single call, following the same expansion rule as ## @code{addTerms}. All variable names must match entries in ## @code{@var{mdl}.PredictorNames}. ## ## @var{terms} may also be a numeric matrix of size @var{t}-by-@var{v}, ## where @var{t} is the number of terms to remove and @var{v} equals ## @code{@var{mdl}.NumVariables}. Entry @code{T(i,j)} is the exponent of ## variable @var{j} in term @var{i}. For example, in a model with ## variables @code{x1}, @code{x2}, @code{y}: @code{[0 0 0]} is the ## intercept, @code{[0 1 0]} is @code{x2}, @code{[1 1 0]} is ## @code{x1:x2}, and @code{[2 0 0]} is @code{x1^2}. A matrix with ## @code{@var{mdl}.NumPredictors} columns is also accepted and is ## automatically padded with a trailing zero column for the response. ## ## Terms specified but absent from @var{mdl} are silently skipped. A ## warning is issued and @var{mdl} is returned unchanged only when every ## single specified term is absent from the model. For a categorical ## predictor, @code{removeTerms} removes the full group of indicator ## variables for that predictor in one step. ## ## @end deftypefn function NewMdl = removeTerms (mdl, terms) if (nargin < 2) error ("removeTerms: Not enough input arguments."); endif if (nargin > 2) error ("removeTerms: Too many input arguments."); endif nv = mdl.NumVariables; ## The candidates, not the chosen: a term may name a predictor the ## current model does not use, and nv - 1 counts them all. pred = mdl.PredictorNamesRaw; if (isnumeric (terms) || islogical (terms)) T = double (terms); if (isempty (T)) error ("removeTerms: Terms matrix must have %d columns.", nv); endif if (columns (T) == nv - 1) T = [T, zeros(rows (T), 1)]; endif if (columns (T) != nv) error ("removeTerms: Terms matrix must have %d columns.", nv); endif elseif (ischar (terms) || isstring (terms)) terms_str = strtrim (char (terms)); T = zeros (0, nv); plus_tokens = strsplit (terms_str, '+'); for ti = 1:numel (plus_tokens) tok = strtrim (plus_tokens{ti}); if (isempty (tok)); continue; endif if (! isempty (strfind (tok, '*'))) star_parts = cellfun (@strtrim, strsplit (tok, '*'), ... 'UniformOutput', false); n_sp = numel (star_parts); colon_toks = {}; for mask = 1:(2^n_sp - 1) sub = {}; for bit = 1:n_sp if (bitand (mask, 2^(bit-1))) sub{end+1} = star_parts{bit}; endif endfor colon_toks{end+1} = strjoin (sub, ':'); endfor else colon_toks = {tok}; endif for ci = 1:numel (colon_toks) ctok = strtrim (colon_toks{ci}); if (strcmp (ctok, '1')) T = [T; zeros(1, nv)]; else parts = cellfun (@strtrim, strsplit (ctok, ':'), ... 'UniformOutput', false); if (numel (parts) == 1) part = parts{1}; hat = strfind (part, '^'); if (isempty (hat)) vname = part; exp = 1; else vname = strtrim (part(1:hat(1)-1)); exp = str2double (strtrim (part(hat(1)+1:end))); endif idx = find (strcmp (pred, vname)); if (isempty (idx)) error ("removeTerms: Unrecognized variable: '%s'.", vname); endif for k = 1:exp row = zeros (1, nv); row(idx(1)) = k; T = [T; row]; endfor else row = zeros (1, nv); for pi = 1:numel (parts) part = parts{pi}; hat = strfind (part, '^'); if (isempty (hat)) vname = part; exp = 1; else vname = strtrim (part(1:hat(1)-1)); exp = str2double (strtrim (part(hat(1)+1:end))); endif idx = find (strcmp (pred, vname)); if (isempty (idx)) error ("removeTerms: Unrecognized variable: '%s'.", vname); endif row(idx(1)) = row(idx(1)) + exp; endfor T = [T; row]; endif endif endfor endfor else error (strcat ("removeTerms: Model update specification must be a", ... " model formula character vector or string scalar,", ... " or a terms matrix")); endif nc = columns (mdl.TermsMatrix); if (nc != nv) cat_info = mdl.CatLevelInfo; ename = mdl.EncPredictorNames; n_pred = nv - 1; orig_to_enc = cell (n_pred, 1); for j = 1:n_pred ci = []; if (! isempty (cat_info) && isfield (cat_info, 'names') ... && ! isempty (cat_info.names)) ci = find (strcmp (cat_info.names, pred{j})); endif if (isempty (ci)) orig_to_enc{j} = find (strcmp (ename, pred{j})); else levels_j = cat_info.levels{ci}; ecols = []; for L = 2:numel (levels_j) lvl_name = sprintf ("%s_%s", pred{j}, char (levels_j{L})); k = find (strcmp (ename, lvl_name)); if (! isempty (k)) ecols(end+1) = k; endif endfor orig_to_enc{j} = ecols; endif endfor T_enc = zeros (0, nc); for i = 1:rows (T) orig_row = T(i, 1:n_pred); any_cat = false; cat_rows = zeros (0, nc); cont_row = zeros (1, nc); for j = 1:n_pred if (orig_row(j) != 0) ecols = orig_to_enc{j}; if (numel (ecols) > 1) any_cat = true; for k = 1:numel (ecols) r = zeros (1, nc); r(ecols(k)) = 1; cat_rows = [cat_rows; r]; endfor else cont_row(ecols) = orig_row(j); endif endif endfor if (any_cat) for k = 1:rows (cat_rows) T_enc = [T_enc; cat_rows(k,:) + cont_row]; endfor else T_enc = [T_enc; cont_row]; endif endfor T = T_enc; endif existing = mdl.TermsMatrix; n_exist = rows (existing); n_req = rows (T); found = false (n_req, 1); for i = 1:n_req found(i) = any (all (existing == T(i,:), 2)); endfor if (! any (found)) warning ("removeTerms: No specified terms appear in the model."); NewMdl = mdl; return; endif keep = true (n_exist, 1); for i = 1:n_req if (found(i)) for j = 1:n_exist if (keep(j) && all (existing(j,:) == T(i,:))) keep(j) = false; break; endif endfor endif endfor remaining = existing(keep, :); int_mask = all (remaining(:, 1:end-1) == 0, 2); body = remaining(! int_mask, :); n_nonzero = sum (body(:, 1:end-1) != 0, 2); degree = sum (body(:, 1:end-1), 2); tier = zeros (rows (body), 1); tier(n_nonzero == 1 & degree == 1) = 1; tier(n_nonzero == 2) = 2; tier(n_nonzero == 1 & degree > 1) = 3; bitmask = zeros (rows (body), 1); for i = 1:rows (body) bitmask(i) = sum (2 .^ (find (body(i, 1:end-1)) - 1)); endfor [~, order] = sortrows ([tier, bitmask]); remaining = [remaining(int_mask, :); body(order, :)]; ## Back into the variable space the refit works in. The request was ## mapped down to the model's own columns to be compared with its terms, ## and those are only as wide as the predictors the formula uses, while ## lm_refit refits against the whole variable table. A model fitted from ## a matrix has no gap between the two, which is why this only ever ## showed on a table model whose formula names a subset. if (nc != nv) R = zeros (rows (remaining), nv); for j = 1:n_pred ecols = orig_to_enc{j}; for k = 1:numel (ecols) R(:,j) = max (R(:,j), remaining(:,ecols(k))); endfor endfor R(:,end) = remaining(:,end); remaining = R; endif NewMdl = lm_refit (mdl, remaining); endfunction ## -*- texinfo -*- ## @deftypefn {LinearModel} {} plotResiduals (@var{mdl}) ## @deftypefnx {LinearModel} {} plotResiduals (@var{mdl}, @var{plottype}) ## @deftypefnx {LinearModel} {} plotResiduals (@var{mdl}, @var{plottype}, @var{Name}, @var{Value}) ## @deftypefnx {LinearModel} {} plotResiduals (@var{ax}, @dots{}) ## @deftypefnx {LinearModel} {@var{h} =} plotResiduals (@dots{}) ## ## Plot residuals of a fitted linear regression model. ## ## @code{plotResiduals (@var{mdl})} creates a probability density histogram ## of the raw residuals. Bin width follows Scott's rule ## @math{h = 3.5 \hat\sigma n^{-1/3}} and is rounded to a visually clean ## value. The bar areas sum to 1. ## ## @code{plotResiduals (@var{mdl}, @var{plottype})} creates the type of ## residual plot given by @var{plottype}. For all types except ## @code{"histogram"} and @code{"probability"}, the full observation vector ## including excluded rows is passed to the plot. Excluded or missing rows ## appear as @code{NaN} in the plotted data and produce visible gaps. ## @var{plottype} must be one of: ## ## @table @asis ## @item @qcode{'histogram'} (default) ## Probability density histogram. Only active observations are used. ## Returns one @code{patch} handle. Accepts @code{FaceColor}, ## @code{EdgeColor}, @code{FaceAlpha}, and @code{LineWidth} Name-Value ## arguments. ## ## @item @qcode{'fitted'} ## Residuals on the y-axis against fitted values on the x-axis. A dotted ## horizontal reference line marks @math{y = 0}. Returns two line handles: ## @code{h(1)} is the data scatter and @code{h(2)} is the reference line. ## ## @item @qcode{'caseorder'} ## Residuals on the y-axis against observation row number on the x-axis, ## covering all rows from 1 to @code{n_total}. A dotted horizontal ## reference line marks @math{y = 0}. Returns two line handles: ## @code{h(1)} is the data and @code{h(2)} is the reference line. ## ## @item @qcode{'lagged'} ## Each residual @math{r(t)} on the y-axis against the preceding residual ## @math{r(t-1)} on the x-axis. Two dotted reference lines mark ## @math{y = 0} and @math{x = 0}. Returns three line handles: @code{h(1)} ## is the scatter, @code{h(2)} is the horizontal reference, and @code{h(3)} ## is the vertical reference. ## ## @item @qcode{'probability'} ## Normal probability plot of the sorted active residuals produced by ## @code{normplot}. Returns two handles: @code{h(1)} is the data line and ## @code{h(2)} is the fitted reference line produced by @code{normplot}. ## Name-Value arguments are not applied for this plot type. ## ## @item @qcode{'observed'} ## Observed response values on the y-axis against fitted values on the ## x-axis. A dotted @math{y = x} reference line is drawn through the ## origin. Vertical segments connect each observed point down to the ## reference line. ## Returns three handles: @code{h(1)} is the scatter, @code{h(2)} is the ## @math{y = x} reference, and @code{h(3)} is the vertical segment line ## (stored as a single @code{NaN}-separated line object). ## ## @item @qcode{'symmetry'} ## Upper-tail distances from the median plotted against lower-tail distances ## from the median. Each point @code{(x, y)} satisfies ## @math{x = \mathrm{med} - r_{(i)}} and ## @math{y = r_{(n+1-i)} - \mathrm{med}}, using the ## @math{\lfloor n/2 \rfloor} most extreme observations on each side. A ## perfectly symmetric distribution falls on the dotted @math{y = x} ## reference line. Returns two handles: @code{h(1)} is the scatter and ## @code{h(2)} is the reference line. ## @end table ## ## @code{plotResiduals (@var{ax}, @dots{})} targets the axes object @var{ax} ## instead of the current axes returned by @code{gca}. ## ## @code{@var{h} = plotResiduals (@dots{})} returns a vector of graphics ## handles. The number of handles depends on @var{plottype} as described ## above. Name-Value arguments are applied to the data handle @code{h(1)} ## only. Reference lines are always drawn with the default style and are ## not affected by Name-Value arguments. ## ## The following Name-Value arguments are accepted. Arguments marked ## @emph{histogram only} are passed directly to the @code{patch} object and ## have no effect on other plot types. Arguments marked ## @emph{non-histogram} are applied to the scatter marker and have no ## effect on the histogram. ## ## @multitable @columnfractions 0.28 0.70 ## @headitem Name @tab Description and default ## ## @item @qcode{'ResidualType'} @tab ## Type of residual to plot. One of @qcode{'raw'} (default), ## @qcode{'pearson'}, @qcode{'standardized'}, or @qcode{'studentized'}. ## Case-insensitive. Selects the corresponding column of ## @code{mdl.Residuals}. ## ## @item @qcode{'Color'} @tab ## (@emph{non-histogram}) Marker color. ## Default: @code{[0.1490 0.5490 0.8660]}. ## ## @item @qcode{'Marker'} @tab ## (@emph{non-histogram}) Marker symbol. Any symbol accepted by ## @code{plot} is valid. Default: @qcode{'x'}. ## ## @item @qcode{'MarkerSize'} @tab ## (@emph{non-histogram}) Marker size in points. Default: @code{6}. ## ## @item @qcode{'MarkerEdgeColor'} @tab ## (@emph{non-histogram}) Marker edge color. Default: @qcode{'auto'}. ## ## @item @qcode{'MarkerFaceColor'} @tab ## (@emph{non-histogram}) Marker fill color. Default: @qcode{'none'}. ## ## @item @qcode{'LineWidth'} @tab ## (@emph{non-histogram}) Width of the marker edge in points. ## Default: @code{0.5}. ## ## @item @qcode{'FaceColor'} @tab ## (@emph{histogram only}) Fill color of the histogram bars. ## Default: @code{[0.1490 0.5490 0.8660]}. ## ## @item @qcode{'EdgeColor'} @tab ## (@emph{histogram only}) Edge color of the histogram bars. ## ## @item @qcode{'FaceAlpha'} @tab ## (@emph{histogram only}) Transparency of the histogram bars, ## specified as a scalar in @math{[0, 1]}. ## @end multitable ## ## @end deftypefn function h = plotResiduals (this, varargin) [ax, mdl, args] = lm_plot_axes (this, varargin); DEF_COLOR = [0.1490, 0.5490, 0.8660]; REF_COLOR = [0.8510, 0.8510, 0.8510]; valid_pt = {'histogram', 'fitted', 'lagged', 'caseorder', ... 'probability', 'observed', 'symmetry'}; known_nv = {'residualtype', 'color', 'marker', 'markersize', ... 'markeredgecolor', 'markerfacecolor', 'linewidth', ... 'facecolor', 'edgecolor', 'facealpha'}; if (! isempty (args) && ischar (args{1}) ... && ! any (strcmpi (args{1}, known_nv))) pt_str = args{1}; args = args(2:end); idx = find (strcmpi (pt_str, valid_pt)); if (isempty (idx)) error ("plotResiduals: Bad residuals plot type."); endif plottype = valid_pt{idx(1)}; else plottype = 'histogram'; endif residtype = 'raw'; nv_remaining = {}; i = 1; while (i <= numel (args)) if (ischar (args{i}) && strcmpi (args{i}, 'ResidualType')) if (i + 1 > numel (args)) error ("plotResiduals: ResidualType requires a value."); endif rt_val = lower (char (args{i+1})); valid_rt = {'raw', 'pearson', 'standardized', 'studentized'}; if (! any (strcmp (rt_val, valid_rt))) error (strcat ("plotResiduals: invalid ResidualType '%s'.", ... " Valid values are: 'Raw', 'Pearson',", ... " 'Standardized', 'Studentized'."), args{i+1}); endif residtype = rt_val; i = i + 2; else nv_remaining{end+1} = args{i}; i = i + 1; endif endwhile ## Parse the plot properties before taking an axes: gca creates a ## figure when none is current, and an unrecognised property would ## otherwise leave that figure behind when the error is raised. A ## histogram is the exception, forwarding its properties to PATCH, ## which accepts and validates its own. if (! strcmp (plottype, 'histogram')) props = lm_plot_props (nv_remaining); endif if (isempty (ax)) ax = gca (); endif switch (residtype) case 'raw'; rf = 'Raw'; case 'pearson'; rf = 'Pearson'; case 'standardized'; rf = 'Standardized'; case 'studentized'; rf = 'Studentized'; endswitch r = mdl.Residuals.(rf); switch (plottype) case 'histogram' r_act = r(! isnan (r)); n_act = numel (r_act); s = std (r_act); if (n_act <= 1 || s == 0) bw = 1; lo = floor (min (r_act)) - 0.5; hi = lo + 1; else bw_raw = 3.5 * s / (n_act ^ (1/3)); mag = 10 ^ floor (log10 (bw_raw)); frac = bw_raw / mag; if (frac < 1.5); nice = 1; elseif (frac < 2.5); nice = 2; elseif (frac < 4); nice = 3; elseif (frac < 7.5); nice = 5; else; nice = 10; endif bw = nice * mag; lo = floor (min (r_act) / bw) * bw; hi = ceil (max (r_act) / bw) * bw; endif n_bins = max (1, round ((hi - lo) / bw)); centers = lo + bw/2 : bw : lo + bw * (n_bins - 0.5); [counts, ~] = hist (r_act, centers); dens = counts / (n_act * bw); left = lo + (0:n_bins-1) * bw; right = left + bw; Xp = [left; left; right; right]; Yp = [zeros(1, n_bins); dens; dens; zeros(1, n_bins)]; h = patch (Xp, Yp, DEF_COLOR, 'FaceColor', DEF_COLOR, ... nv_remaining{:}, 'Parent', ax); xlabel (ax, 'Residuals'); ylabel (ax, 'Probability density'); title (ax, 'Histogram of residuals'); case 'fitted' fit = mdl.Fitted; fin = fit(! isnan (fit)); hold (ax, 'on'); h(1) = lm_plot_data (ax, fit, r, props); h(2) = line ([min(fin), max(fin)], [0, 0], ... 'LineStyle', ':', 'Color', REF_COLOR, 'Parent', ax); hold (ax, 'off'); xlabel (ax, 'Fitted values'); ylabel (ax, 'Residuals'); title (ax, 'Plot of residuals vs. fitted values'); case 'caseorder' n_tot = numel (r); hold (ax, 'on'); h(1) = lm_plot_data (ax, 1:n_tot, r, props); h(2) = line ([1, n_tot], [0, 0], ... 'LineStyle', ':', 'Color', REF_COLOR, 'Parent', ax); hold (ax, 'off'); xlabel (ax, 'Row number'); ylabel (ax, 'Residuals'); title (ax, 'Case order plot of residuals'); case 'lagged' r_x = r(1:end-1); r_y = r(2:end); rx_fin = r_x(! isnan (r_x)); ry_fin = r_y(! isnan (r_y)); hold (ax, 'on'); h(1) = lm_plot_data (ax, r_x, r_y, props); h(2) = line ([min(rx_fin), max(rx_fin)], [0, 0], ... 'LineStyle', ':', 'Color', REF_COLOR, 'Parent', ax); h(3) = line ([0, 0], [min(ry_fin), max(ry_fin)], ... 'LineStyle', ':', 'Color', REF_COLOR, 'Parent', ax); hold (ax, 'off'); xlabel (ax, 'Residual(t-1)'); ylabel (ax, 'Residual(t)'); title (ax, 'Plot of residuals vs. lagged residuals'); case 'probability' r_act = r(! isnan (r)); r_s = sort (r_act); h = normplot (ax, r_s); title (ax, 'Normal probability plot of residuals'); xlabel (ax, 'Residuals'); ylabel (ax, 'Probability'); case 'observed' fit = mdl.Fitted; obs = mdl.Variables{:, mdl.ResponseName}; fin = fit(! isnan (fit)); av = [fin(:); obs(! isnan (fit))]; xl = [min(av), max(av)]; n_tot = numel (fit); xv = reshape ([fit(:)'; fit(:)'; NaN(1, n_tot)], 1, []); yv = reshape ([fit(:)'; obs(:)'; NaN(1, n_tot)], 1, []); hold (ax, 'on'); h(1) = lm_plot_data (ax, fit, obs, props); h(2) = line (xl, xl, ... 'LineStyle', ':', 'Color', REF_COLOR, 'Parent', ax); h(3) = line (xv, yv, ... 'LineStyle', '-', 'Color', REF_COLOR, 'Parent', ax); hold (ax, 'off'); xlabel (ax, 'Fitted values'); ylabel (ax, 'Observed response values'); title (ax, 'Plot of observed vs. fitted values'); case 'symmetry' r_act = r(! isnan (r)); r_s = sort (r_act); med = median (r_s); m = floor (numel (r_s) / 2); x_sym = sort (med - r_s(1:m)); y_sym = sort (r_s(end-m+1:end) - med); mx = max ([x_sym(:); y_sym(:)]); hold (ax, 'on'); h(1) = lm_plot_data (ax, x_sym, y_sym, props); h(2) = line ([0, mx], [0, mx], ... 'LineStyle', ':', 'Color', REF_COLOR, 'Parent', ax); hold (ax, 'off'); xlabel (ax, 'Lower tail'); ylabel (ax, 'Upper tail'); title (ax, 'Symmetry plot of residuals around their median'); endswitch if (nargout == 0) clear h; endif endfunction ## -*- texinfo -*- ## @deftypefn {LinearModel} {} plotDiagnostics (@var{mdl}) ## @deftypefnx {LinearModel} {} plotDiagnostics (@var{mdl}, @var{plottype}) ## @deftypefnx {LinearModel} {} plotDiagnostics (@var{mdl}, @var{plottype}, @var{Name}, @var{Value}) ## @deftypefnx {LinearModel} {} plotDiagnostics (@var{ax}, @dots{}) ## @deftypefnx {LinearModel} {@var{h} =} plotDiagnostics (@dots{}) ## ## Plot observation diagnostics of a fitted linear regression model. ## ## @code{plotDiagnostics (@var{mdl})} creates a case-order plot of the ## leverage of each observation. The x-axis is the observation row number ## running from 1 to the total number of rows including any excluded rows. ## A dotted horizontal reference line marks the recommended threshold ## @math{2p/n}, where @math{p} is @code{mdl.NumCoefficients} and @math{n} ## is @code{mdl.NumObservations}. ## ## @code{plotDiagnostics (@var{mdl}, @var{plottype})} creates the diagnostic ## plot specified by @var{plottype}. For all types except @code{"contour"}, ## the x-axis is the row number and covers all rows including excluded ones. ## Excluded rows produce @code{NaN} values in the diagnostic vectors, which ## appear as natural gaps in the plot with no special handling required. ## @var{plottype} must be one of: ## ## @table @asis ## @item @qcode{'leverage'} (default) ## Leverage of each observation (@code{mdl.Diagnostics.Leverage}). ## One dotted horizontal reference line at @math{2p/n}. ## Returns two handles: @code{h(1)} is the data scatter and @code{h(2)} ## is the reference line. ## ## @item @qcode{'cookd'} ## Cook's distance for each observation ## (@code{mdl.Diagnostics.CooksDistance}). One dotted reference line at ## @math{3 \times \mathrm{mean(CooksDistance)}}, where the mean ignores ## @code{NaN} values. Returns two handles: @code{h(1)} data, @code{h(2)} ## reference. ## ## @item @qcode{'covratio'} ## Delete-1 ratio of the determinant of the coefficient covariance matrix ## (@code{mdl.Diagnostics.CovRatio}). Two dotted reference lines at ## @math{1 - 3p/n} (lower bound) and @math{1 + 3p/n} (upper bound). ## Both bounds are stored as a single @code{NaN}-separated line object. ## Returns two handles: @code{h(1)} data, @code{h(2)} combined reference. ## ## @item @qcode{'dfbetas'} ## Delete-1 scaled change in each coefficient estimate ## (@code{mdl.Diagnostics.Dfbetas}, one column per coefficient). ## One line object is drawn per coefficient. Two dotted reference lines ## at @math{\pm 3/\sqrt{n}} are stored as a single @code{NaN}-separated ## line object. Returns @math{p+1} handles: @code{h(1)} through ## @code{h(p)} are the per-coefficient data lines and @code{h(p+1)} is ## the combined reference. Name-Value arguments are applied to all ## @math{p} data handles. ## ## @item @qcode{'dffits'} ## Delete-1 scaled change in the fitted value ## (@code{mdl.Diagnostics.Dffits}). Two dotted reference lines at ## @math{\pm 2\sqrt{p/n}} stored as a single @code{NaN}-separated line. ## Returns two handles: @code{h(1)} data, @code{h(2)} combined reference. ## ## @item @qcode{'s2_i'} ## Delete-1 variance estimate (@code{mdl.Diagnostics.S2_i}). One dotted ## reference line at @code{mdl.MSE}. Returns two handles: @code{h(1)} ## data, @code{h(2)} reference. ## ## @item @qcode{'contour'} ## Standardized residuals on the y-axis against leverage on the x-axis, ## with Cook's distance contours overlaid at levels ## @math{[0.05, 0.10, 0.15, 0.20, 0.25]}. The contour surface is ## computed on a 31-by-30 grid over the range of the active leverage and ## residual values. Returns two handles: @code{h(1)} is the data scatter ## (a @code{line} object) and @code{h(2)} is the contour object. ## @end table ## ## @code{plotDiagnostics (@var{ax}, @dots{})} targets the axes object ## @var{ax} instead of the current axes returned by @code{gca}. ## ## @code{@var{h} = plotDiagnostics (@dots{})} returns a vector of graphics ## handles. The number of handles depends on @var{plottype} as described ## above. Name-Value arguments are applied to the data handle @code{h(1)}, ## except for @code{"dfbetas"} where they are applied to all @math{p} ## coefficient handles. Reference line handles are never affected by ## Name-Value arguments. ## ## @multitable @columnfractions 0.28 0.70 ## @headitem Name @tab Description and default ## ## @item @qcode{'Color'} @tab ## Marker color for data points. For @code{"dfbetas"} this color is ## applied to all @math{p} coefficient line objects. ## Default: @code{[0.1490 0.5490 0.8660]}. ## ## @item @qcode{'Marker'} @tab ## Marker symbol. Any symbol accepted by @code{plot} is valid. ## Default: @qcode{'x'}. ## ## @item @qcode{'MarkerSize'} @tab ## Marker size in points. Default: @code{6}. ## ## @item @qcode{'MarkerEdgeColor'} @tab ## Marker edge color. Default: @qcode{'auto'}. ## ## @item @qcode{'MarkerFaceColor'} @tab ## Marker fill color. Default: @qcode{'none'}. ## ## @item @qcode{'LineWidth'} @tab ## Width of the marker edge in points. Default: @code{0.5}. ## @end multitable ## ## @end deftypefn function h = plotDiagnostics (this, varargin) [ax, mdl, args] = lm_plot_axes (this, varargin); REF_COLOR = [0.8510, 0.8510, 0.8510]; valid_pt = {'leverage', 'cookd', 'covratio', 'dfbetas', ... 'dffits', 's2_i', 'contour'}; if (! isempty (args) && ischar (args{1}) ... && ! any (strcmpi (args{1}, ... {'color','marker','markersize','markeredgecolor', ... 'markerfacecolor','linewidth'}))) pt_str = args{1}; args = args(2:end); idx = find (strcmpi (pt_str, valid_pt)); if (isempty (idx)) error ("plotDiagnostics: Bad diagnostics plot type."); endif plottype = valid_pt{idx(1)}; else plottype = 'leverage'; endif props = lm_plot_props (args); if (isempty (ax)) ax = gca (); endif diag_t = mdl.Diagnostics; p = mdl.NumCoefficients; n_obs = mdl.NumObservations; mse = mdl.MSE; n = numel (diag_t.Leverage); switch (plottype) case 'leverage' lev = diag_t.Leverage; ref = 2 * p / n_obs; hold (ax, 'on'); h(1) = lm_plot_data (ax, 1:n, lev, props); h(2) = line ([0, n], [ref, ref], ... 'LineStyle', ':', 'Color', REF_COLOR, 'Parent', ax); hold (ax, 'off'); xlabel (ax, 'Row number'); ylabel (ax, 'Leverage'); title (ax, 'Case order plot of leverage'); case 'cookd' cd_ = diag_t.CooksDistance; ref = 3 * mean (cd_, 'omitnan'); hold (ax, 'on'); h(1) = lm_plot_data (ax, 1:n, cd_, props); h(2) = line ([0, n], [ref, ref], ... 'LineStyle', ':', 'Color', REF_COLOR, 'Parent', ax); hold (ax, 'off'); xlabel (ax, 'Row number'); ylabel (ax, 'Cook''s distance'); title (ax, 'Case order plot of Cook''s distance'); case 'covratio' cv = diag_t.CovRatio; lo = 1 - 3*p/n_obs; hi = 1 + 3*p/n_obs; xv = [0, n, NaN, 0, n]; yv = [lo, lo, NaN, hi, hi]; hold (ax, 'on'); h(1) = lm_plot_data (ax, 1:n, cv, props); h(2) = line (xv, yv, ... 'LineStyle', ':', 'Color', REF_COLOR, 'Parent', ax); hold (ax, 'off'); xlabel (ax, 'Row number'); ylabel (ax, 'Covariance ratio'); title (ax, 'Case order plot of covariance ratio'); case 'dfbetas' db = diag_t.Dfbetas; thr = 3 / sqrt (n_obs); xv = [0, n, NaN, 0, n]; yv = [-thr, -thr, NaN, thr, thr]; hold (ax, 'on'); for k = 1:p h(k) = lm_plot_data (ax, (1:n)', db(:,k), props); endfor h(p+1) = line (xv, yv, ... 'LineStyle', ':', 'Color', REF_COLOR, 'Parent', ax); hold (ax, 'off'); xlabel (ax, 'Row number'); ylabel (ax, 'Scaled change in coefficients'); title (ax, 'Case order plot of scaled change in coefficients'); case 'dffits' df = diag_t.Dffits; thr = 2 * sqrt (p / n_obs); xv = [0, n, NaN, 0, n]; yv = [-thr, -thr, NaN, thr, thr]; hold (ax, 'on'); h(1) = lm_plot_data (ax, 1:n, df, props); h(2) = line (xv, yv, ... 'LineStyle', ':', 'Color', REF_COLOR, 'Parent', ax); hold (ax, 'off'); xlabel (ax, 'Row number'); ylabel (ax, 'Scaled change in fit'); title (ax, 'Case order plot of scaled change in fit'); case 's2_i' s2 = diag_t.S2_i; hold (ax, 'on'); h(1) = lm_plot_data (ax, 1:n, s2, props); h(2) = line ([0, n], [mse, mse], ... 'LineStyle', ':', 'Color', REF_COLOR, 'Parent', ax); hold (ax, 'off'); xlabel (ax, 'Row number'); ylabel (ax, 'Leave-one-out variance'); title (ax, 'Case order plot of leave-one-out variance'); case 'contour' lev = diag_t.Leverage; r_raw = mdl.Residuals.Raw; act = ! isnan (lev); lev_a = lev(act); r_a = r_raw(act); x_grid = linspace (min (lev_a), max (lev_a), 31); y_grid = linspace (min (r_a), max (r_a), 30); [Hg, Rg] = meshgrid (x_grid, y_grid); Hg = min (Hg, 1 - 1e-10); Zg = Rg.^2 .* Hg ./ (p .* mse .* (1 - Hg).^2); levels = [0.05, 0.10, 0.15, 0.20, 0.25]; hold (ax, 'on'); h(1) = lm_plot_data (ax, lev, r_raw, props); [~, h_ct] = contour (ax, x_grid, y_grid, Zg, levels); h(2) = h_ct; hold (ax, 'off'); xlabel (ax, 'Leverage'); ylabel (ax, 'Residual'); title (ax, 'Cook''s distance factorization'); endswitch if (nargout == 0) clear h; endif endfunction ## -*- texinfo -*- ## @deftypefn {LinearModel} {} plotEffects (@var{mdl}) ## @deftypefnx {LinearModel} {} plotEffects (@var{ax}, @var{mdl}) ## @deftypefnx {LinearModel} {@var{h} =} plotEffects (@dots{}) ## ## Plot the main effects of each predictor in a fitted linear regression ## model. ## ## @code{plotEffects (@var{mdl})} creates a horizontal dot-and-line plot ## with one row per predictor. Each dot shows the estimated main effect on ## the response from changing that predictor from its minimum observed value ## to its maximum observed value, while holding all other predictors fixed ## at their observed means. A horizontal line through each dot shows the ## 95% confidence interval for that effect. ## ## The main effect for predictor @var{xs} is defined as ## @math{g(x_{s,\max}) - g(x_{s,\min})}, where the adjusted response ## function @math{g} evaluates the model at the specified value of ## @var{xs} with all other predictors set to their observed means. ## For numeric predictors the sign of the effect can be positive or ## negative depending on the direction of the relationship. ## ## @code{plotEffects (@var{ax}, @var{mdl})} creates the plot in the axes ## object @var{ax} instead of the current axes returned by @code{gca}. ## ## @code{@var{h} = plotEffects (@dots{})} returns a vector of ## @math{p+1} graphics handles where @math{p} is the number of predictors. ## @code{h(1)} is the line object containing the effect estimate markers ## (one circle per predictor, plotted as a single line object with ## @code{XData} of length @math{p} and @code{YData = 1:p}). ## @code{h(j+1)} is the confidence interval line for predictor @math{j}, ## with @code{XData = [ci_lo, ci_hi]} and @code{YData = [j, j]}. ## ## The y-axis tick labels follow the format ## @qcode{'varname: min to max'}, showing the predictor name and the ## minimum and maximum observed values used to compute the effect. ## ## @end deftypefn function h = plotEffects (this, varargin) [ax, mdl, args] = lm_plot_axes (this, varargin); if (! isempty (args)) error ("plotEffects: Wrong number of arguments."); endif p = mdl.NumPredictors; if (! any (any (mdl.TermsMatrix(:, 1:end-1) != 0))) error ("plotEffects: Model has no predictors."); endif if (isempty (ax)) ax = gca (); endif DEF_COLOR = [0.1490, 0.5490, 0.8660]; pred = mdl.PredictorNames; V = mdl.CoefficientCovariance; beta = mdl.Coefficients.Estimate; t_crit = tinv (0.975, mdl.DFE); cinfo = mdl.CatLevelInfo; C = mdl.EffectContrasts; effects = zeros (1, p); ci_lo = zeros (1, p); ci_hi = zeros (1, p); for j = 1:p effects(j) = C(j,:) * beta; SE = sqrt (max (0, C(j,:) * V * C(j,:)')); ci_lo(j) = effects(j) - t_crit * SE; ci_hi(j) = effects(j) + t_crit * SE; endfor hold (ax, 'on'); h(1) = plot (ax, effects, 1:p, ... 'LineStyle', 'none', ... 'Marker', 'o', ... 'MarkerSize', 6, ... 'Color', DEF_COLOR); for j = 1:p h(j+1) = line ([ci_lo(j), ci_hi(j)], [j, j], ... 'LineStyle', '-', ... 'Marker', 'none', ... 'Color', DEF_COLOR, ... 'Parent', ax); endfor hold (ax, 'off'); rn = mdl.VariableInfo.Properties.RowNames; ytl = cell (p, 1); for j = 1:p ci = []; if (! isempty (cinfo) && isfield (cinfo, 'names') && ! isempty (cinfo.names)) ci = find (strcmp (cinfo.names, pred{j})); endif vidx = find (strcmp (rn, pred{j})); rng = mdl.VariableInfo.Range{vidx}; if (! isempty (ci)) levels_j = cinfo.levels{ci}; lo_str = char (levels_j{1}); hi_str = char (levels_j{end}); else lo_str = num2str (rng(1), '%g'); hi_str = num2str (rng(2), '%g'); endif ytl{j} = [pred{j}, ': ', lo_str, ' to ', hi_str]; endfor set (ax, 'YTick', 1:p, 'YTickLabel', ytl, 'YDir', 'reverse'); ylim (ax, [0.5, p + 0.5]); xlabel (ax, 'Main Effect'); ylabel (ax, ''); title (ax, 'Main Effects Plot'); if (nargout == 0) clear h; endif endfunction ## -*- texinfo -*- ## @deftypefn {LinearModel} {} plotAdjustedResponse (@var{mdl}, @var{var}) ## @deftypefnx {LinearModel} {} plotAdjustedResponse (@var{mdl}, @var{var}, @var{Name}, @var{Value}) ## @deftypefnx {LinearModel} {} plotAdjustedResponse (@var{ax}, @dots{}) ## @deftypefnx {LinearModel} {@var{h} =} plotAdjustedResponse (@dots{}) ## ## Plot the adjusted response of a fitted linear regression model against ## a single predictor. ## ## @code{plotAdjustedResponse (@var{mdl}, @var{var})} creates an adjusted ## response plot for the predictor @var{var} in the linear regression ## model @var{mdl}. @var{var} is a character vector or string naming a ## predictor in @code{mdl.PredictorNames}, or a positive integer indexing ## into @code{mdl.VariableNames}. ## ## An adjusted response function describes the fitted response as a ## function of a single predictor, with the other predictors averaged out ## by averaging the fitted values over the observations used in the fit. ## For a model @math{y_i = f (x_{1i}, x_{2i}, @dots{}, x_{pi}) + r_i}, the ## adjusted response function for @math{x_1} is ## @math{g (x_1) = (1/n) \sum_{i=1}^n f (x_1, x_{2i}, x_{3i}, @dots{}, ## x_{pi})}, where @math{n} is the number of observations used to fit the ## model. The adjusted response data value for observation @math{i} is ## @math{\tilde y_i = g (x_{1i}) + r_i}. ## ## For a numeric predictor, the adjusted response function is evaluated ## on an evenly spaced grid of 100 points spanning the minimum to the ## maximum observed value of @var{var}. For a categorical predictor, the ## adjusted response function is evaluated at each category level. ## ## Excluded or missing observations appear as @code{NaN} in the adjusted ## data and produce gaps in the plotted data points. ## ## @code{plotAdjustedResponse (@var{mdl}, @var{var}, @var{Name}, ## @var{Value})} specifies additional Name-Value arguments applied to the ## adjusted data points (@code{h(1)}). The following are accepted: ## ## @multitable @columnfractions 0.28 0.70 ## @headitem Name @tab Description and default ## ## @item @qcode{'Color'} @tab ## Marker color. Default: @code{[0.1490 0.5490 0.8660]}. ## ## @item @qcode{'Marker'} @tab ## Marker symbol. Default: @qcode{'x'}. ## ## @item @qcode{'MarkerSize'} @tab ## Marker size in points. Default: @code{6}. ## ## @item @qcode{'MarkerEdgeColor'} @tab ## Marker edge color. Default: @qcode{'auto'}. ## ## @item @qcode{'MarkerFaceColor'} @tab ## Marker fill color. Default: @qcode{'none'}. ## ## @item @qcode{'LineWidth'} @tab ## Width of the marker edge in points. Default: @code{0.5}. ## @end multitable ## ## @code{plotAdjustedResponse (@var{ax}, @dots{})} plots into the axes ## object @var{ax} instead of the current axes returned by @code{gca}. ## ## @code{@var{h} = plotAdjustedResponse (@dots{})} returns a 2-by-1 ## vector of line handles. @code{h(1)} corresponds to the adjusted ## response data points and @code{h(2)} corresponds to the adjusted ## response function. Name-Value arguments only affect @code{h(1)}. ## ## @end deftypefn function h = plotAdjustedResponse (this, varargin) [ax, mdl, args] = lm_plot_axes (this, varargin); if (isempty (args)) error ("plotAdjustedResponse: Not enough input arguments."); endif var = args{1}; args = args(2:end); vnames = mdl.VariableNames; pred = mdl.PredictorNames; if (ischar (var) || isstring (var)) vname = char (var); if (isempty (find (strcmp (vnames, vname)))) error ("plotAdjustedResponse: '%s' is not a variable for this fit.", vname); endif elseif (isnumeric (var) && isscalar (var)) if (var != fix (var) || var < 1) error (strcat ("plotAdjustedResponse: Variable must be specified", ... " as a name or a positive integer.")); endif if (var > numel (vnames)) error ("plotAdjustedResponse: This model only contains %d variables.", ... numel (vnames)); endif vname = vnames{var}; else error (strcat ("plotAdjustedResponse: Variable must be specified", ... " as a name or a positive integer.")); endif if (strcmp (vname, mdl.ResponseName)) error ("plotAdjustedResponse: The variable '%s' is the response in this model.", ... vname); endif j = find (strcmp (pred, vname)); pname = pred{j}; act = mdl.ObservationInfo.Subset; cinfo = mdl.CatLevelInfo; n_act = sum (act); p = numel (pred); [X_act, is_cat, cat_lvls] = lm_encode_active_predictors (mdl, act, pred, cinfo); props = lm_plot_props (args); if (isempty (ax)) ax = gca (); endif FIT_COLOR = [0.9600, 0.4660, 0.1600]; n_total = numel (act); beta = mdl.Coefficients.Estimate; resid = mdl.Residuals.Raw; xdata = NaN (n_total, 1); ydata = NaN (n_total, 1); xdata(act) = X_act(:, j); if (is_cat(j)) levels = cat_lvls{j}; n_lvl = numel (levels); fit_y = zeros (n_lvl, 1); for L = 1:n_lvl X_rows = X_act; X_rows(:,j) = L; X_enc = reencode_predictors (X_rows, pred, mdl.CatLevelInfo, mdl.EncPredictorNames); D = build_design (mdl.TermsMatrix, X_enc); fit_y(L) = mean (D * beta); endfor fit_x = (1:n_lvl)'; ydata(act) = fit_y(X_act(:, j)) + resid(act); else x_active = X_act(:, j); g_active = zeros (n_act, 1); for i = 1:n_act X_rows = X_act; X_rows(:,j) = x_active(i); X_enc = reencode_predictors (X_rows, pred, mdl.CatLevelInfo, mdl.EncPredictorNames); D = build_design (mdl.TermsMatrix, X_enc); g_active(i) = mean (D * beta); endfor ydata(act) = g_active + resid(act); fit_x = linspace (min (x_active), max (x_active), 100)'; fit_y = zeros (100, 1); for k = 1:100 X_rows = X_act; X_rows(:,j) = fit_x(k); X_enc = reencode_predictors (X_rows, pred, mdl.CatLevelInfo, mdl.EncPredictorNames); D = build_design (mdl.TermsMatrix, X_enc); fit_y(k) = mean (D * beta); endfor endif hold (ax, 'on'); h(1) = lm_plot_data (ax, xdata, ydata, props); set (h(1), 'DisplayName', 'Adjusted data'); h(2) = line (fit_x, fit_y, 'Color', FIT_COLOR, 'LineStyle', '-', ... 'Marker', 'none', 'Parent', ax, ... 'DisplayName', 'Adjusted fit'); hold (ax, 'off'); if (is_cat(j)) set (ax, 'XTick', 1:n_lvl, 'XTickLabel', levels); endif xlabel (ax, pname); ylabel (ax, ['Adjusted ', mdl.ResponseName]); title (ax, 'Adjusted response plot'); hleg = legend (ax, 'show'); set (hleg, 'Location', lm_legend_corner (xdata, ydata)); if (nargout == 0) clear h; endif endfunction ## -*- texinfo -*- ## @deftypefn {LinearModel} {} plotAdded (@var{mdl}) ## @deftypefnx {LinearModel} {} plotAdded (@var{mdl}, @var{coef}) ## @deftypefnx {LinearModel} {} plotAdded (@var{mdl}, @var{coef}, @var{Name}, @var{Value}) ## @deftypefnx {LinearModel} {} plotAdded (@var{ax}, @dots{}) ## @deftypefnx {LinearModel} {@var{h} =} plotAdded (@dots{}) ## ## Create an added variable plot for a fitted linear regression model. ## ## @code{plotAdded (@var{mdl})} creates an added variable plot for the ## whole model @var{mdl} except the constant (intercept) term. ## ## @code{plotAdded (@var{mdl}, @var{coef})} creates an added variable ## plot for the coefficients specified by @var{coef}. @var{coef} is a ## character vector or string naming a single coefficient in ## @code{mdl.CoefficientNames}, the name of a categorical predictor in ## @code{mdl.PredictorNames} (which selects that predictor's whole group ## of indicator coefficients), or a vector of positive integers indexing ## into @code{mdl.CoefficientNames}. ## ## An added variable plot, also known as a partial regression leverage ## plot, illustrates the incremental effect on the response of the ## selected terms after removing the effects of all other terms. For a ## single selected predictor @math{x_1}, the response @math{y} and ## @math{x_1} are each fit to all other terms: ## @math{y_i = g_y (x_{2i}, @dots{}, x_{pi}) + r_{yi}}, ## @math{x_{1i} = g_x (x_{2i}, @dots{}, x_{pi}) + r_{xi}}. The adjusted ## values are @math{\tilde y_i = \bar y + r_{yi}} and ## @math{\tilde x_{1i} = \bar x_1 + r_{xi}}. When @var{coef} selects ## more than one coefficient, the selected columns of the design matrix ## are combined into a single direction using the unit vector ## @math{u = \beta / \lVert \beta \rVert}, and the added variable plot is ## created for that combined direction. ## ## Excluded or missing observations appear as @code{NaN} in the adjusted ## data and produce gaps in the plotted data points. ## ## @code{plotAdded (@var{mdl}, @var{coef}, @var{Name}, @var{Value})} ## specifies additional Name-Value arguments applied to the adjusted data ## points (@code{h(1)}). The following are accepted: ## ## @multitable @columnfractions 0.28 0.70 ## @headitem Name @tab Description and default ## ## @item @qcode{'Color'} @tab ## Marker color. Default: @code{[0.1490 0.5490 0.8660]}. ## ## @item @qcode{'Marker'} @tab ## Marker symbol. Default: @qcode{'x'}. ## ## @item @qcode{'MarkerSize'} @tab ## Marker size in points. Default: @code{6}. ## ## @item @qcode{'MarkerEdgeColor'} @tab ## Marker edge color. Default: @qcode{'auto'}. ## ## @item @qcode{'MarkerFaceColor'} @tab ## Marker fill color. Default: @qcode{'none'}. ## ## @item @qcode{'LineWidth'} @tab ## Width of the marker edge in points. Default: @code{0.5}. ## @end multitable ## ## @code{plotAdded (@var{ax}, @dots{})} plots into the axes object ## @var{ax} instead of the current axes returned by @code{gca}. ## ## @code{@var{h} = plotAdded (@dots{})} returns a 3-by-1 vector of line ## handles. @code{h(1)}, @code{h(2)}, and @code{h(3)} correspond to the ## adjusted data points, the fitted line, and the 95% confidence bounds ## of the fitted line, respectively. Name-Value arguments only affect ## @code{h(1)}. ## ## @end deftypefn function h = plotAdded (this, varargin) [ax, mdl, args] = lm_plot_axes (this, varargin); cnames = mdl.CoefficientNames; pred = mdl.PredictorNames; ncoef = numel (cnames); if (! isempty (args) && ! (ischar (args{1}) || isstring (args{1}) ... || isnumeric (args{1}))) error (strcat ("plotAdded: COEF must be a coefficient name or a", ... " vector of coefficient numbers.")); endif if (isempty (args)) J = 2:ncoef; if (numel (J) == 1) label = cnames{J}; else label = 'whole model'; endif else coefarg = args{1}; args = args(2:end); if (ischar (coefarg) || isstring (coefarg)) cname = char (coefarg); k = find (strcmp (cnames, cname)); if (! isempty (k)) J = k; else cinfo = mdl.CatLevelInfo; ci = []; if (! isempty (cinfo) && isfield (cinfo, 'names') ... && ! isempty (cinfo.names)) ci = find (strcmp (cinfo.names, cname)); endif if (isempty (ci)) error ("plotAdded: Bad coefficient name."); endif levels_ci = cinfo.levels{ci}; J = zeros (1, numel (levels_ci) - 1); for L = 2:numel (levels_ci) cn = sprintf ("%s_%s", cname, char (levels_ci{L})); J(L-1) = find (strcmp (cnames, cn)); endfor endif else J = coefarg(:)'; if (any (J != fix (J)) || any (J < 1) || any (J > ncoef)) error ("plotAdded: Bad coefficient number."); endif endif if (numel (J) == 1) label = cnames{J}; elseif (isequal (sort (J(:)'), 2:ncoef)) ## Every non-intercept coefficient: MATLAB names this the whole ## model, and reserves 'specified terms' for any other selection. label = 'whole model'; else label = 'specified terms'; cinfo = mdl.CatLevelInfo; if (! isempty (cinfo) && isfield (cinfo, 'names') ... && ! isempty (cinfo.names)) for ci = 1:numel (cinfo.names) levels_ci = cinfo.levels{ci}; Jc = zeros (1, numel (levels_ci) - 1); for L = 2:numel (levels_ci) cn = sprintf ("%s_%s", cinfo.names{ci}, char (levels_ci{L})); Jc(L-1) = find (strcmp (cnames, cn)); endfor if (isequal (sort (J), sort (Jc))) label = cinfo.names{ci}; break; endif endfor endif endif endif if (isempty (J)) error ("plotAdded: Bad coefficient number."); endif act = mdl.ObservationInfo.Subset; cinfo = mdl.CatLevelInfo; n_act = sum (act); p = numel (pred); [X_act, ~, ~] = lm_encode_active_predictors (mdl, act, pred, cinfo); D = reencode_predictors (X_act, pred, cinfo, mdl.EncPredictorNames); D = build_design (mdl.TermsMatrix, D); beta = mdl.Coefficients.Estimate; y_act = mdl.Variables{act, mdl.ResponseName}; Jc = setdiff (1:ncoef, J); if (numel (J) == 1) x1_act = D(:, J); slope = beta(J); else u = beta(J) / norm (beta(J)); x1_act = D(:, J) * u; slope = norm (beta(J)); endif bx = D(:,Jc) \ x1_act; rx = x1_act - D(:,Jc) * bx; if (! isempty (mdl.Robust)) w_act = mdl.Robust.Weights(act); else w_act = mdl.ObservationInfo.Weights(act); endif x1bar_w = sum (w_act .* x1_act) / sum (w_act); ybar_w = sum (w_act .* y_act) / sum (w_act); ry = mdl.Residuals.Raw(act) + slope * rx; xtilde = x1bar_w + rx; ytilde = ybar_w + ry; intercept_avp = mean (ytilde) - slope * mean (xtilde); props = lm_plot_props (args); if (isempty (ax)) ax = gca (); endif FIT_COLOR = [0.9600, 0.4660, 0.1600]; n_total = numel (act); xdata = NaN (n_total, 1); ydata = NaN (n_total, 1); xdata(act) = xtilde; ydata(act) = ytilde; fit_x = linspace (min (xtilde), max (xtilde), 100)'; fit_y = intercept_avp + slope * fit_x; Sxx = sum ((xtilde - mean (xtilde)) .^ 2); tcrit = tinv (0.975, mdl.DFE); se_pred = sqrt (mdl.MSE * (1/n_act + (fit_x - mean (xtilde)).^2 / Sxx)); halfw = tcrit * se_pred; bound_x = [fit_x; NaN; fit_x]; bound_y = [fit_y + halfw; NaN; fit_y - halfw]; hold (ax, 'on'); h(1) = lm_plot_data (ax, xdata, ydata, props); set (h(1), 'DisplayName', 'Adjusted data'); h(2) = line (fit_x, fit_y, 'Color', FIT_COLOR, 'LineStyle', '-', ... 'Marker', 'none', 'Parent', ax, ... 'DisplayName', sprintf ("Fit: y = %g*x", slope)); h(3) = line (bound_x, bound_y, 'Color', FIT_COLOR, 'LineStyle', ':', ... 'Marker', 'none', 'Parent', ax, ... 'DisplayName', '95% conf. bounds'); hold (ax, 'off'); xlabel (ax, ['Adjusted ', label]); ylabel (ax, ['Adjusted ', mdl.ResponseName]); title (ax, ['Added variable plot for ', label]); hleg = legend (ax, 'show'); set (hleg, 'Location', lm_legend_corner (xdata, ydata)); if (nargout == 0) clear h; endif endfunction ## -*- texinfo -*- ## @deftypefn {LinearModel} {} plot (@var{mdl}) ## @deftypefnx {LinearModel} {} plot (@var{ax}, @var{mdl}) ## @deftypefnx {LinearModel} {@var{h} =} plot (@dots{}) ## ## Create a default diagnostic plot for a fitted linear regression model. ## ## @code{plot (@var{mdl})} creates a plot whose type depends on the ## number of predictors in @var{mdl}. If @var{mdl} has two or more ## predictors, @code{plot} creates an added variable plot for the whole ## model except the constant (intercept) term, equivalent to ## @code{plotAdded (@var{mdl})}. If @var{mdl} has exactly one ## predictor, @code{plot} creates a scatter plot of the data together ## with the fitted curve and its 95% confidence bounds. If @var{mdl} ## has no predictors, @code{plot} creates a histogram of the residuals, ## equivalent to @code{plotResiduals (@var{mdl})}. ## ## For the single-predictor case, the fitted curve and confidence ## bounds are computed with @code{predict}, evaluated at 100 equally ## spaced points spanning the observed range of the predictor when the ## predictor is numeric, or at each level of the predictor when it is ## categorical. Excluded or missing observations appear as @code{NaN} ## in the data and produce gaps in the plotted points. ## ## @code{plot (@var{ax}, @var{mdl})} plots into the axes object ## @var{ax} instead of the current axes returned by @code{gca}. ## ## @code{@var{h} = plot (@dots{})} returns a vector of graphics object ## handles. For the two-or-more-predictor and no-predictor cases, see ## @code{plotAdded} and @code{plotResiduals}, respectively, for the ## meaning of @var{h}. For the single-predictor case, @var{h}(1), ## @var{h}(2), and @var{h}(3) correspond to the data points, the fitted ## curve, and the 95% confidence bounds of the fitted curve, ## respectively. ## ## @end deftypefn function h = plot (this, varargin) [ax, mdl, args] = lm_plot_axes (this, varargin); if (! isempty (args)) error ("plot: Too many input arguments."); endif pred = mdl.PredictorNames; cinfo = mdl.CatLevelInfo; enc_names = mdl.EncPredictorNames; enc_active = any (mdl.TermsMatrix(:, 1:end-1) != 0, 1); active_pred = false (1, numel (pred)); for c = find (enc_active) j = find (strcmp (pred, enc_names{c}), 1); if (isempty (j) && ! isempty (cinfo) && isfield (cinfo, 'names')) for k = 1:numel (cinfo.names) prefix = [cinfo.names{k}, '_']; if (strncmp (enc_names{c}, prefix, numel (prefix))) j = find (strcmp (pred, cinfo.names{k}), 1); break; endif endfor endif if (! isempty (j)) active_pred(j) = true; endif endfor n_active = sum (active_pred); if (n_active == 0) if (isempty (ax)) h = plotResiduals (mdl); else h = plotResiduals (ax, mdl); endif elseif (n_active >= 2) if (isempty (ax)) h = plotAdded (mdl); else h = plotAdded (ax, mdl); endif else j1 = find (active_pred, 1); act = mdl.ObservationInfo.Subset; n_act = sum (act); ci = []; if (! isempty (cinfo) && isfield (cinfo, 'names') ... && ! isempty (cinfo.names)) ci = find (strcmp (cinfo.names, pred{j1})); endif col = mdl.Variables{act, pred{j1}}; if (! isempty (ci)) levels_1 = cinfo.levels{ci}; col_str = lm_col_to_str (col); x_act = zeros (n_act, 1); for L = 1:numel (levels_1) x_act(strcmp (col_str, char (levels_1{L}))) = L; endfor x_grid = (1:numel (levels_1))'; else x_act = double (col(:)); x_grid = linspace (min (x_act), max (x_act), 100)'; endif y_act = mdl.Variables{act, mdl.ResponseName}; [y_fit, yci] = mdl.predict (x_grid); if (isempty (ax)) ax = gca (); endif FIT_COLOR = [0.9600, 0.4660, 0.1600]; n_total = numel (act); xdata = NaN (n_total, 1); ydata = NaN (n_total, 1); xdata(act) = x_act; ydata(act) = y_act; bound_x = [x_grid; NaN; x_grid]; bound_y = [yci(:,1); NaN; yci(:,2)]; props = lm_plot_props ({}); hold (ax, 'on'); h(1) = lm_plot_data (ax, xdata, ydata, props); set (h(1), 'DisplayName', 'Data'); h(2) = line (x_grid, y_fit, 'Color', FIT_COLOR, 'LineStyle', '-', ... 'Marker', 'none', 'Parent', ax, 'DisplayName', 'Fit'); h(3) = line (bound_x, bound_y, 'Color', FIT_COLOR, 'LineStyle', ':', ... 'Marker', 'none', 'Parent', ax, ... 'DisplayName', '95% conf. bounds'); hold (ax, 'off'); if (! isempty (ci)) set (ax, 'XTick', 1:numel (levels_1), 'XTickLabel', levels_1); endif xlabel (ax, pred{j1}); ylabel (ax, mdl.ResponseName); title (ax, [mdl.ResponseName, ' vs. ', pred{j1}]); hleg = legend (ax, 'show'); set (hleg, 'Location', lm_legend_corner (xdata, ydata)); endif if (nargout == 0) clear h; endif endfunction ## -*- texinfo -*- ## @deftypefn {LinearModel} {} plotInteraction (@var{mdl}, @var{var1}, @var{var2}) ## @deftypefnx {LinearModel} {} plotInteraction (@var{mdl}, @var{var1}, @var{var2}, @var{ptype}) ## @deftypefnx {LinearModel} {} plotInteraction (@var{ax}, @dots{}) ## @deftypefnx {LinearModel} {@var{h} =} plotInteraction (@dots{}) ## ## Plot the interaction effects of two predictors in a fitted linear ## regression model. ## ## @code{plotInteraction (@var{mdl}, @var{var1}, @var{var2})} creates a ## plot of the main effects of @var{var1} and @var{var2} together with ## their conditional effects, with horizontal lines through each effect ## value indicating its 95% confidence interval. @var{var1} and ## @var{var2} are each a character vector or string naming a variable in ## @code{mdl.VariableNames}, or a positive integer indexing into ## @code{mdl.VariableNames}; neither may name the response variable, and ## they must be different variables. ## ## The main effect of a predictor is the change in the adjusted response ## between the two predictor values that produce the minimum and maximum ## adjusted response, with the other predictor averaged over its own ## observed values row by row. For a numeric predictor these two values ## are its observed minimum and maximum; for a categorical predictor ## every level is evaluated and the levels producing the minimum and ## maximum adjusted response are used, so the effect is always ## nonnegative. ## ## The conditional effect of @var{var1} is its effect recomputed with ## @var{var2} additionally held fixed at each of a small set of ## conditioning values, and likewise the conditional effect of ## @var{var2} holds @var{var1} fixed. The conditioning values are the ## observed minimum, mean of the minimum and maximum, and maximum for a ## numeric predictor, or every level for a categorical predictor. When ## the main effect and conditional effect points for a predictor do not ## align vertically, the model exhibits an interaction between ## @var{var1} and @var{var2}. ## ## @code{plotInteraction (@var{mdl}, @var{var1}, @var{var2}, @var{ptype})} ## selects the plot type. @var{ptype} is @qcode{'effects'} (default), as ## described above, or @qcode{'predictions'}, which instead plots the ## adjusted response as a function of @var{var2} for each conditioning ## value of @var{var1} held fixed, evaluated over 101 equally spaced ## points spanning the observed range of @var{var2} when @var{var2} is ## numeric, or at each level of @var{var2} when it is categorical. ## ## @code{plotInteraction (@var{ax}, @dots{})} plots into the axes object ## @var{ax} instead of the current axes returned by @code{gca}. ## ## @code{@var{h} = plotInteraction (@dots{})} returns a vector of line ## handles. When @var{ptype} is @qcode{'effects'}, @code{h(1)} is the ## marker line through the two main effect points, @code{h(2)} and ## @code{h(3)} are the confidence interval lines for the main effects of ## @var{var1} and @var{var2}, and the remaining entries are the ## conditional effect points and their confidence intervals, tagged ## @qcode{'conditional1'} for @var{var1} and @qcode{'conditional2'} for ## @var{var2}. The main effect line objects are tagged @qcode{'main'}. ## When @var{ptype} is @qcode{'predictions'}, each entry in @var{h} ## corresponds to one adjusted response curve, one per conditioning ## value of @var{var1}. ## ## @end deftypefn function h = plotInteraction (this, varargin) [ax, mdl, args] = lm_plot_axes (this, varargin); if (numel (args) < 2) error ("plotInteraction: Not enough input arguments."); endif var1 = args{1}; var2 = args{2}; args = args(3:end); ptype = 'effects'; if (! isempty (args) && (ischar (args{1}) || isstring (args{1}))) ptype = lower (char (args{1})); args = args(2:end); if (! any (strcmp (ptype, {'effects', 'predictions'}))) error ("plotInteraction: PTYPE must be 'effects' or 'predictions'."); endif endif if (! isempty (args)) error ("plotInteraction: Too many input arguments."); endif vnames = mdl.VariableNames; if (ischar (var1) || isstring (var1)) v1name = char (var1); if (isempty (find (strcmp (vnames, v1name)))) error ("plotInteraction: '%s' is not a variable for this fit.", v1name); endif elseif (isnumeric (var1) && isscalar (var1)) if (var1 != fix (var1) || var1 < 1) error (strcat ("plotInteraction: Variable must be specified as a", ... " name or a positive integer.")); endif if (var1 > numel (vnames)) error ("plotInteraction: This model only contains %d variables.", numel (vnames)); endif v1name = vnames{var1}; else error (strcat ("plotInteraction: Variable must be specified as a", ... " name or a positive integer.")); endif if (strcmp (v1name, mdl.ResponseName)) error ("plotInteraction: The variable '%s' is the response in this model.", v1name); endif if (ischar (var2) || isstring (var2)) v2name = char (var2); if (isempty (find (strcmp (vnames, v2name)))) error ("plotInteraction: '%s' is not a variable for this fit.", v2name); endif elseif (isnumeric (var2) && isscalar (var2)) if (var2 != fix (var2) || var2 < 1) error (strcat ("plotInteraction: Variable must be specified as a", ... " name or a positive integer.")); endif if (var2 > numel (vnames)) error ("plotInteraction: This model only contains %d variables.", numel (vnames)); endif v2name = vnames{var2}; else error (strcat ("plotInteraction: Variable must be specified as a", ... " name or a positive integer.")); endif if (strcmp (v2name, mdl.ResponseName)) error ("plotInteraction: The variable '%s' is the response in this model.", v2name); endif if (strcmp (v1name, v2name)) error ("plotInteraction: VAR1 and VAR2 must be different variables."); endif pred = mdl.PredictorNames; cinfo = mdl.CatLevelInfo; ename = mdl.EncPredictorNames; terms = mdl.TermsMatrix; act = mdl.ObservationInfo.Subset; n_act = sum (act); p = numel (pred); beta = mdl.Coefficients.Estimate; V = mdl.CoefficientCovariance; t_crit = tinv (0.975, mdl.DFE); [X_act, is_cat, cat_lvls] = lm_encode_active_predictors (mdl, act, pred, cinfo); j1 = find (strcmp (pred, v1name)); j2 = find (strcmp (pred, v2name)); if (is_cat(j1)) levels_1 = cat_lvls{j1}; n_lv1 = numel (levels_1); g_lv1 = zeros (n_lv1, 1); for L = 1:n_lv1 c_row = lm_interaction_row (X_act, j1, L, pred, cinfo, ename, terms); g_lv1(L) = c_row * beta; endfor [~, i_lo1] = min (g_lv1); [~, i_hi1] = max (g_lv1); lo1 = i_lo1; hi1 = i_hi1; lbl1 = [v1name, ': ', char(levels_1{i_lo1}), ' to ', char(levels_1{i_hi1})]; grid1 = (1:n_lv1)'; grid1_lbls = cellfun (@(s) char (s), levels_1, 'UniformOutput', false); else lo1 = min (X_act(:,j1)); hi1 = max (X_act(:,j1)); lbl1 = [v1name, ': ', num2str(lo1), ' to ', num2str(hi1)]; grid1 = [lo1; (lo1+hi1)/2; hi1]; grid1_lbls = arrayfun (@(v) num2str(v,'%g'), grid1, 'UniformOutput', false); endif if (is_cat(j2)) levels_2 = cat_lvls{j2}; n_lv2 = numel (levels_2); g_lv2 = zeros (n_lv2, 1); for L = 1:n_lv2 c_row = lm_interaction_row (X_act, j2, L, pred, cinfo, ename, terms); g_lv2(L) = c_row * beta; endfor [~, i_lo2] = min (g_lv2); [~, i_hi2] = max (g_lv2); lo2 = i_lo2; hi2 = i_hi2; lbl2 = [v2name, ': ', char(levels_2{i_lo2}), ' to ', char(levels_2{i_hi2})]; grid2 = (1:n_lv2)'; grid2_lbls = cellfun (@(s) char (s), levels_2, 'UniformOutput', false); else lo2 = min (X_act(:,j2)); hi2 = max (X_act(:,j2)); lbl2 = [v2name, ': ', num2str(lo2), ' to ', num2str(hi2)]; grid2 = [lo2; (lo2+hi2)/2; hi2]; grid2_lbls = arrayfun (@(v) num2str(v,'%g'), grid2, 'UniformOutput', false); endif c_hi1 = lm_interaction_row (X_act, j1, hi1, pred, cinfo, ename, terms); c_lo1 = lm_interaction_row (X_act, j1, lo1, pred, cinfo, ename, terms); eff1 = (c_hi1 - c_lo1) * beta; se1 = sqrt (max (0, (c_hi1 - c_lo1) * V * (c_hi1 - c_lo1)')); c_hi2 = lm_interaction_row (X_act, j2, hi2, pred, cinfo, ename, terms); c_lo2 = lm_interaction_row (X_act, j2, lo2, pred, cinfo, ename, terms); eff2 = (c_hi2 - c_lo2) * beta; se2 = sqrt (max (0, (c_hi2 - c_lo2) * V * (c_hi2 - c_lo2)')); n2 = numel (grid2); eff_c1 = zeros (n2, 1); se_c1 = zeros (n2, 1); for k = 1:n2 c_hi = lm_interaction_row (X_act, [j1, j2], [hi1, grid2(k)], pred, cinfo, ename, terms); c_lo = lm_interaction_row (X_act, [j1, j2], [lo1, grid2(k)], pred, cinfo, ename, terms); eff_c1(k) = (c_hi - c_lo) * beta; se_c1(k) = sqrt (max (0, (c_hi - c_lo) * V * (c_hi - c_lo)')); endfor n1 = numel (grid1); eff_c2 = zeros (n1, 1); se_c2 = zeros (n1, 1); for k = 1:n1 c_hi = lm_interaction_row (X_act, [j2, j1], [hi2, grid1(k)], pred, cinfo, ename, terms); c_lo = lm_interaction_row (X_act, [j2, j1], [lo2, grid1(k)], pred, cinfo, ename, terms); eff_c2(k) = (c_hi - c_lo) * beta; se_c2(k) = sqrt (max (0, (c_hi - c_lo) * V * (c_hi - c_lo)')); endfor if (isempty (ax)) ax = gca (); endif cla (ax); DEF_COLOR = [0.1490, 0.5490, 0.8660]; FIT_COLOR = [0.9600, 0.4660, 0.1600]; if (strcmp (ptype, 'effects')) y_main1 = 1; y_cond1 = (2:(1+n2))'; y_main2 = n2 + 4; y_cond2 = ((n2+5):(n2+4+n1))'; hold (ax, 'on'); line ([0, 0], [0.5, n2 + n1 + 4.5], 'LineStyle', ':', 'Marker', 'none', ... 'Color', [0, 0, 0], 'Parent', ax); h(1) = plot (ax, [eff1, eff2], [y_main1, y_main2], ... 'LineStyle', 'none', 'Marker', 'o', 'Color', DEF_COLOR, ... 'Tag', 'main'); h(2) = line ([eff1 - t_crit*se1, eff1 + t_crit*se1], [y_main1, y_main1], ... 'LineStyle', '-', 'Marker', 'none', 'Color', DEF_COLOR, ... 'Parent', ax, 'Tag', 'main'); h(3) = line ([eff2 - t_crit*se2, eff2 + t_crit*se2], [y_main2, y_main2], ... 'LineStyle', '-', 'Marker', 'none', 'Color', DEF_COLOR, ... 'Parent', ax, 'Tag', 'main'); h(4) = plot (ax, eff_c1, y_cond1, ... 'LineStyle', 'none', 'Marker', 'o', 'Color', FIT_COLOR, ... 'Tag', 'conditional1'); for k = 1:n2 h(4+k) = line ([eff_c1(k) - t_crit*se_c1(k), eff_c1(k) + t_crit*se_c1(k)], ... [y_cond1(k), y_cond1(k)], ... 'LineStyle', '-', 'Marker', 'none', 'Color', FIT_COLOR, ... 'Parent', ax, 'Tag', 'conditional1'); endfor h(5+n2) = plot (ax, eff_c2, y_cond2, ... 'LineStyle', 'none', 'Marker', 'o', 'Color', FIT_COLOR, ... 'Tag', 'conditional2'); for k = 1:n1 h(5+n2+k) = line ([eff_c2(k) - t_crit*se_c2(k), eff_c2(k) + t_crit*se_c2(k)], ... [y_cond2(k), y_cond2(k)], ... 'LineStyle', '-', 'Marker', 'none', 'Color', FIT_COLOR, ... 'Parent', ax, 'Tag', 'conditional2'); endfor hold (ax, 'off'); ytl = cell (2 + n1 + n2, 1); ytl{1} = lbl1; for k = 1:n2 ytl{1+k} = [v2name, '=', grid2_lbls{k}]; endfor ytl{2+n2} = lbl2; for k = 1:n1 ytl{2+n2+k} = [v1name, '=', grid1_lbls{k}]; endfor set (ax, 'YTick', [y_main1; y_cond1; y_main2; y_cond2], ... 'YTickLabel', ytl, 'YDir', 'reverse'); ylim (ax, [0.5, n2 + n1 + 4.5]); xlabel (ax, 'Effect'); ylabel (ax, ''); title (ax, ['Interaction of ', v1name, ' and ', v2name]); else ## 'predictions' if (is_cat(j2)) x_grid2 = (1:n_lv2)'; else x_grid2 = linspace (lo2, hi2, 101)'; endif hold (ax, 'on'); line (NaN, NaN, 'Color', 'none', 'Parent', ax, 'DisplayName', v1name); colors = get (ax, 'ColorOrder'); n_colors = rows (colors); for k = 1:n1 y_curve = zeros (numel (x_grid2), 1); for m = 1:numel (x_grid2) c_row = lm_interaction_row (X_act, [j1, j2], [grid1(k), x_grid2(m)], ... pred, cinfo, ename, terms); y_curve(m) = c_row * beta; endfor h(k) = line (x_grid2, y_curve, ... 'Color', colors(mod(k-1, n_colors)+1, :), ... 'LineStyle', '-', 'Marker', 'none', 'Parent', ax, ... 'DisplayName', grid1_lbls{k}); endfor hold (ax, 'off'); if (is_cat(j2)) set (ax, 'XTick', 1:n_lv2, 'XTickLabel', grid2_lbls); endif xlabel (ax, v2name); ylabel (ax, ['Adjusted ', mdl.ResponseName]); title (ax, ['Interaction of ', v1name, ' and ', v2name]); legend (ax, 'show'); endif if (nargout == 0) clear h; endif endfunction ## -*- texinfo -*- ## @deftypefn {LinearModel} {@var{cmdl} =} compact (@var{mdl}) ## ## Create a compact version of a fitted linear regression model. ## ## @code{@var{cmdl} = compact (@var{mdl})} returns a ## @code{CompactLinearModel} object that retains the coefficient ## estimates, coefficient covariance, fit statistics, model formula, and ## fitting method information of @var{mdl}, but discards the training ## data and everything derived from it. Specifically, the following ## properties of @var{mdl} are not carried over and are unavailable on ## @var{cmdl}: @code{Fitted}, @code{Residuals}, @code{Diagnostics}, ## @code{ObservationInfo}, @code{ObservationNames}, @code{Variables}, ## @code{Steps}, and @code{ModelFitVsNullModel}. ## ## If @var{mdl} was fit using robust regression, the @code{Robust} ## structure is retained on @var{cmdl} except for its @code{Weights} ## field, which is always emptied; @code{RobustWgtFun} and @code{Tune} ## are preserved unchanged. ## ## A @code{CompactLinearModel} object consumes less memory than a ## @code{LinearModel} object and can still be used with @code{predict}, ## @code{feval}, @code{random}, @code{coefCI}, and @code{coefTest}, but ## does not support methods that require the original training data or ## refitting, such as @code{addTerms}, @code{removeTerms}, @code{step}, ## and @code{dwtest}. ## ## @seealso{LinearModel, CompactLinearModel} ## @end deftypefn function CVMdl = compact (this) CVMdl = CompactLinearModel (this); endfunction ## -*- texinfo -*- ## @deftypefn {LinearModel} {@var{tbl} =} anova (@var{mdl}) ## @deftypefnx {LinearModel} {@var{tbl} =} anova (@var{mdl}, @var{anovatype}) ## @deftypefnx {LinearModel} {@var{tbl} =} anova (@var{mdl}, @qcode{"components"}, @var{sstype}) ## ## Analysis of variance for a linear regression model. ## ## @code{anova (@var{mdl})} returns a table @var{tbl} with component ## ANOVA statistics for every term in @var{mdl} except the constant ## term, computed with hierarchical (@qcode{"h"}) sums of squares. Each ## row gives @code{SumSq}, @code{DF}, @code{MeanSq}, @code{F}, and ## @code{pValue} for the corresponding term; the trailing @qcode{Error} ## row gives @code{SumSq = @var{mdl}.SSE}, @code{DF = @var{mdl}.DFE}, ## @code{MeanSq = @var{mdl}.MSE}, and @code{NaN} for @code{F} and ## @code{pValue}. ## ## MATLAB reports @code{F = 1} and @code{pValue = 0.5} on that ## @qcode{Error} row instead. Those are not results: the row's @code{F} ## is its own @code{MeanSq} divided by itself, so it is @code{1} for every ## data set, and the @code{pValue} follows. MATLAB does not use them ## consistently either, reporting @code{NaN} for the same quantity on the ## @qcode{Residual} row of its summary table. This implementation reports ## @code{NaN} in both places. Every other value in both tables agrees ## with MATLAB. ## ## @code{anova (@var{mdl}, @var{anovatype})} selects ## @qcode{"components"} (default) or @qcode{"summary"}. For ## @qcode{"summary"}, @var{tbl} always contains rows @qcode{Total}, ## @qcode{Model}, and @qcode{Residual}, and additionally @qcode{. Linear} ## and @qcode{. Nonlinear} whenever @var{mdl} contains an interaction ## term or a continuous term of degree greater than 1. @qcode{Total} ## reports @code{@var{mdl}.SST} with @code{DF = NumObservations - 1}; ## @qcode{Model} reports @code{@var{mdl}.SSR} with @code{DF = ## NumCoefficients - HasIntercept}; @qcode{Residual} reports ## @code{@var{mdl}.SSE} with @code{DF = @var{mdl}.DFE}. Whenever the ## data contains two or more observations sharing identical predictor ## values, @var{tbl} additionally contains @qcode{. Lack of fit} and ## @qcode{. Pure error}, splitting @qcode{Residual} into the part ## explained by replicated observations and the remainder. ## ## @code{anova (@var{mdl}, @qcode{"components"}, @var{sstype})} selects ## the sum of squares used for the component table: @code{1} (sequential, ## reduction from adding each term in formula order), @code{2} (reduction ## from adding the term to a model containing every term that does not ## contain it), @qcode{"h"} (default; as Type 2, but a higher-degree ## term in the same continuous variable, such as a squared term, is also ## treated as containing the lower-degree term), or @code{3} (reduction ## from adding the term to a model containing every other term, with ## categorical predictors recoded using sum-to-zero deviation contrasts ## instead of @var{mdl}'s reference-level coding). Because Type 3 uses a ## different coding, its @qcode{Error} row can differ from @var{mdl}.SSE ## and @var{mdl}.DFE when @var{mdl} is missing a lower-order relative of ## one of its terms (e.g. an interaction without one of its main ## effects, or a categorical predictor fit without an intercept). ## ## @end deftypefn function tbl = anova (mdl, varargin) if (numel (varargin) > 2) error ("anova: too many input arguments."); endif anovatype = 'components'; if (numel (varargin) >= 1) anovatype = varargin{1}; if (! ischar (anovatype) ... || ! any (strcmpi (anovatype, {'summary', 'components'}))) error ("anova: ANOVATYPE must be 'summary' or 'components'."); endif endif sstype = 'h'; if (numel (varargin) == 2) if (! strcmpi (anovatype, 'components')) error ("anova: SSTYPE can only be specified with ANOVATYPE 'components'."); endif sstype = varargin{2}; valid_n = isnumeric (sstype) && isscalar (sstype) && any (sstype == [1, 2, 3]); valid_h = ischar (sstype) && strcmpi (sstype, 'h'); if (! valid_n && ! valid_h) error ("anova: SSTYPE must be 1, 2, or 3."); endif endif groups = mdl.TermGroups; nterm = numel (groups); term_cols = {groups.Cols}; term_name = {groups.Name}; if (mdl.HasIntercept) icol = find (strcmp (mdl.CoefficientNames, '(Intercept)')); else icol = []; endif X = mdl.DesignMatrix; y = mdl.ResponseVector (mdl.SubsetMask); w = mdl.WeightVector (mdl.SubsetMask); all_cols = 1:columns (X); if (strcmpi (anovatype, 'summary')) is_nonlinear = false (nterm, 1); for k = 1:nterm parts = strsplit (term_name{k}, ':'); is_nonlinear(k) = (numel (parts) > 1) || ! isempty (strfind (parts{1}, '^')); endfor SumSq = [mdl.SST; mdl.SSR]; DF = [mdl.NumObservations - 1; mdl.NumCoefficients - mdl.HasIntercept]; RowNm = {'Total', 'Model'}; if (any (is_nonlinear)) lin_cols = union (icol, cell2mat (term_cols(! is_nonlinear))); SS_nl = anova_delta_sse (X, y, w, lin_cols, all_cols); DF_nl = numel (all_cols) - numel (lin_cols); SumSq = [SumSq; SumSq(2) - SS_nl; SS_nl]; DF = [DF; DF(2) - DF_nl; DF_nl]; RowNm = [RowNm, {'. Linear', '. Nonlinear'}]; endif SumSq = [SumSq; mdl.SSE]; DF = [DF; mdl.DFE]; RowNm = [RowNm, {'Residual'}]; MeanSq = SumSq ./ DF; F = NaN (numel (SumSq), 1); pValue = NaN (numel (SumSq), 1); for r = 2:(numel (SumSq) - 1) F(r) = MeanSq(r) / mdl.MSE; pValue(r) = f_pvalue (mdl.DFE, DF(r), F(r)); endfor pred_names = mdl.PredictorNames; cat_info = mdl.CatLevelInfo; p_raw = mdl.NumPredictors; tbl_sub = mdl.Variables (mdl.SubsetMask, :); X_raw = anova_decode_raw (tbl_sub, pred_names, cat_info); [~, ~, gidx] = unique (X_raw, 'rows'); pe_ss = 0; pe_df = 0; for g = 1:max (gidx) rows_g = (gidx == g); w_g = w(rows_g); y_g = y(rows_g); wmean = sum (w_g .* y_g) / sum (w_g); pe_ss = pe_ss + sum (w_g .* (y_g - wmean).^2); pe_df = pe_df + sum (rows_g) - 1; endfor if (pe_df > 0) lof_ss = mdl.SSE - pe_ss; lof_df = mdl.DFE - pe_df; lof_ms = lof_ss / lof_df; pe_ms = pe_ss / pe_df; lof_F = lof_ms / pe_ms; lof_p = f_pvalue (pe_df, lof_df, lof_F); SumSq = [SumSq; lof_ss; pe_ss]; DF = [DF; lof_df; pe_df]; MeanSq = [MeanSq; lof_ms; pe_ms]; F = [F; lof_F; NaN]; pValue = [pValue; lof_p; NaN]; RowNm = [RowNm, {'. Lack of fit', '. Pure error'}]; endif else use_seq = isnumeric (sstype) && sstype == 1; use_type3 = isnumeric (sstype) && sstype == 3; extended = ischar (sstype) && strcmpi (sstype, 'h'); if (use_type3) pred_names = mdl.PredictorNames; cat_info = mdl.CatLevelInfo; enc_names = mdl.EncPredictorNames; p_raw = mdl.NumPredictors; if (! isempty (mdl.EncodedPredMatrix)) X_enc = mdl.EncodedPredMatrix; else tbl_sub = mdl.Variables (mdl.SubsetMask, :); X_raw = anova_decode_raw (tbl_sub, pred_names, cat_info); X_enc = reencode_predictors (X_raw, pred_names, cat_info, enc_names); endif ## deviation (effects) coding: reference level becomes -1 enc_pos = 0; for j = 1:p_raw ci = find (strcmp (cat_info.names, pred_names{j})); if (isempty (ci)) enc_pos = enc_pos + 1; else n_lev = numel (cat_info.levels{ci}); block = enc_pos + (1:(n_lev - 1)); is_ref = 1 - sum (X_enc(:, block), 2); X_enc(:, block) = X_enc(:, block) - is_ref; enc_pos = enc_pos + n_lev - 1; endif endfor D_eff = build_design (mdl.TermsMatrix, X_enc); Mm = X \ D_eff; is_hier = (max (max (abs (D_eff - X * Mm))) < 1e-8 * max (max (abs (D_eff)))) ... && (rcond (Mm) > eps); if (is_hier) Hinv = inv (Mm); b = mdl.Coefficients.Estimate; V = mdl.CoefficientCovariance; SumSq = zeros (nterm, 1); DF = zeros (nterm, 1); F = zeros (nterm, 1); pValue = zeros (nterm, 1); for k = 1:nterm Hk = Hinv(term_cols{k}, :); Hb = Hk * b; HVH = Hk * V * Hk'; DF(k) = numel (term_cols{k}); F(k) = (Hb' * (HVH \ Hb)) / DF(k); SumSq(k) = F(k) * DF(k) * mdl.MSE; pValue(k) = f_pvalue (mdl.DFE, DF(k), F(k)); endfor MeanSq = SumSq ./ DF; errSumSq = mdl.SSE; errDF = mdl.DFE; errMeanSq = mdl.MSE; else fit_eff = LinearModel.lm_fit (D_eff, y, w, false); all_cols = 1:columns (D_eff); SumSq = zeros (nterm, 1); DF = zeros (nterm, 1); for k = 1:nterm cmp_cols = setdiff (all_cols, term_cols{k}); [SumSq(k), ~, fit_r] = anova_delta_sse ( ... D_eff, y, w, cmp_cols, all_cols, fit_eff); DF(k) = fit_eff.rank_X - fit_r.rank_X; endfor ## A term that adds no rank explains nothing, so its sum of ## squares is zero however the arithmetic went. Taken as the ## difference of two residual sums of squares it is the ## cancellation of two nearly equal numbers, which lands a few ## bits either side of zero on some builds and turns the mean ## square into a signed infinity rather than NaN. SumSq(DF == 0) = 0; errSumSq = fit_eff.SSE; errDF = fit_eff.DFE; errMeanSq = fit_eff.MSE; MeanSq = SumSq ./ DF; F = MeanSq / errMeanSq; pValue = NaN (nterm, 1); ok = DF > 0; pValue(ok) = f_pvalue (errDF, DF(ok), F(ok)); endif SumSq = [SumSq; errSumSq]; DF = [DF; errDF]; MeanSq = [MeanSq; errMeanSq]; else if (! use_seq) contain_mx = anova_containment (groups, extended); endif SumSq = zeros (nterm, 1); DF = zeros (nterm, 1); for k = 1:nterm if (use_seq) cmp_cols = union (icol, cell2mat (term_cols(1:k-1))); else keep = true (1, nterm); keep(k) = false; for j = 1:nterm if (j != k && contain_mx(k, j)) keep(j) = false; endif endfor cmp_cols = union (icol, cell2mat (term_cols(keep))); endif full_cols = union (cmp_cols, term_cols{k}); SumSq(k) = anova_delta_sse (X, y, w, cmp_cols, full_cols); DF(k) = numel (term_cols{k}); endfor MeanSq = SumSq ./ DF; F = MeanSq / mdl.MSE; pValue = f_pvalue (mdl.DFE, DF, F); SumSq = [SumSq; mdl.SSE]; DF = [DF; mdl.DFE]; MeanSq = [MeanSq; mdl.MSE]; endif F = [F; NaN]; pValue = [pValue; NaN]; RowNm = [term_name, {'Error'}]; endif tbl = table (SumSq, DF, MeanSq, F, pValue, ... 'VariableNames', {'SumSq', 'DF', 'MeanSq', 'F', 'pValue'}, ... 'RowNames', RowNm(:)); endfunction ## -*- texinfo -*- ## @deftypefn {LinearModel} {@var{NewMdl} =} step (@var{mdl}) ## @deftypefnx {LinearModel} {@var{NewMdl} =} step (@var{mdl}, @var{Name}, @var{Value}) ## ## Improve a fitted linear regression model by one or more steps of ## stepwise term selection. ## ## @code{step} examines whether adding or removing a single term from ## @var{mdl} improves the fit, and returns the resulting model as ## @var{NewMdl}. The original model @var{mdl} is never modified. Unlike ## @code{stepwiselm}, @code{step} performs only one such improvement step ## by default; pass @code{'NSteps'} to allow more. ## ## @code{step} accepts the same @code{'Criterion'}, @code{'PEnter'}, ## @code{'PRemove'}, @code{'NSteps'}, @code{'Verbose'}, @code{'Lower'}, ## and @code{'Upper'} Name-Value options as @code{stepwiselm}, with the ## same defaults, except @code{'NSteps'} defaults to @code{1} rather than ## unlimited. @var{mdl}'s own predictors, weights, excluded observations, ## and categorical variable settings are carried over automatically as ## the starting point for the search. ## ## @code{step} is not available for a model fitted with robust ## regression. ## ## @seealso{stepwiselm, LinearModel.addTerms, LinearModel.removeTerms} ## @end deftypefn function NewMdl = step (mdl, varargin) if (nargin < 1) error ("step: Not enough input arguments."); endif if (! isempty (mdl.Robust)) error ("step: The STEP method is not available with a robust fit."); endif if (mod (numel (varargin), 2) != 0) error ("step: Name-Value arguments must be in pairs."); endif has_nsteps = false; for i = 1:2:numel (varargin) - 1 if ((ischar (varargin{i}) || isstring (varargin{i})) ... && strcmpi (char (varargin{i}), 'NSteps')) has_nsteps = true; endif endfor extra = varargin; if (! has_nsteps) extra = [extra, {'NSteps', 1}]; endif ## A model already fitted stepwise carries the settings the earlier ## search ran under, and 'step' continues that search rather than ## starting a fresh one, so anything not named again is inherited. ## 'Upper' is not: it bounds this call alone. if (! isempty (mdl.Steps)) for nm = {'Criterion', 'Lower', 'PEnter', 'PRemove'} if (! sw_named (extra, nm{1}) && ! isempty (mdl.Steps.(nm{1}))) v = mdl.Steps.(nm{1}); if (isa (v, 'LinearFormula')) v = char (v); endif extra = [extra, {nm{1}, v}]; endif endfor endif cat_vars = {}; if (! isempty (mdl.CatLevelInfo) && isfield (mdl.CatLevelInfo, 'names')) cat_vars = mdl.CatLevelInfo.names; endif nv_list = {'PredictorVars', mdl.PredictorNamesRaw}; if (! isempty (mdl.OrigOpts.Weights)) nv_list = [nv_list, {'Weights', mdl.OrigOpts.Weights}]; endif if (! isempty (mdl.OrigOpts.Exclude)) nv_list = [nv_list, {'Exclude', mdl.OrigOpts.Exclude}]; endif if (! isempty (cat_vars)) nv_list = [nv_list, {'CategoricalVars', cat_vars}]; endif if (mdl.HasIntercept) formula_str = [mdl.ResponseName, ' ~ ', mdl.Formula.LinearPredictor]; elseif (isempty (mdl.Formula.LinearPredictor)) formula_str = [mdl.ResponseName, ' ~ -1']; else formula_str = [mdl.ResponseName, ' ~ ', mdl.Formula.LinearPredictor, ' - 1']; endif NewMdl = stepwiselm (mdl.Variables, formula_str, nv_list{:}, extra{:}); ## Steps.Start is the model stepped from, and the history is appended to ## whatever the model already carried rather than begun again; where it ## carried none, the thresholds are left unreported, as MATLAB leaves ## them. steps = NewMdl.Steps; steps.Start = mdl.Formula; if (isempty (mdl.Steps)) if (! sw_named (extra, 'PEnter')) steps.PEnter = []; endif if (! sw_named (extra, 'PRemove')) steps.PRemove = []; endif else steps.History = [mdl.Steps.History; steps.History(2:end, :)]; endif NewMdl = setSteps (NewMdl, steps); endfunction endmethods methods(Access = public, Static, Hidden) ## weighted least-squares via pivoted QR; returns fit struct function fit = lm_fit (X, y, w, compute_H) if (nargin < 4) compute_H = true; endif n = rows (X); p = columns (X); w = w(:); W_sqrt = sqrt (w); Xw = X .* W_sqrt; yw = y .* W_sqrt; [Q, R, Pperm] = qr (Xw, 0); if (isvector (Pperm)) P_vec = double (Pperm(:)'); else [~, P_vec] = max (Pperm, [], 1); endif dr = abs (diag (R)); if (isempty (dr) || dr(1) == 0) rank_X = 0; else tol = max (size (Xw)) * eps (dr(1)); rank_X = sum (dr > tol); endif beta = zeros (p, 1); active_cols = P_vec(1:rank_X); if (rank_X > 0) R11 = R(1:rank_X, 1:rank_X); Q1 = Q(:, 1:rank_X); gamma = R11 \ (Q1' * yw); beta(active_cols) = gamma; endif Fitted = X * beta; Raw = y - Fitted; n_eff = sum (w > 0); SumLogW = sum (log (w(w > 0))); SSE = sum (w .* Raw.^2); wmean = sum (w .* y) / max (sum (w), eps); SST = sum (w .* (y - wmean).^2); SSR = SST - SSE; DFE = n_eff - rank_X; if (DFE > 0) MSE = SSE / DFE; RMSE = sqrt (MSE); else MSE = NaN; RMSE = NaN; endif CovBeta = zeros (p, p); if (rank_X > 0) R11_inv = R11 \ eye (rank_X); CovBeta(active_cols, active_cols) = MSE * (R11_inv * R11_inv'); endif ## Compute hat matrix and leverage if (rank_X > 0) leverage = sum (Q1.^2, 2); if (compute_H) Q1t = Q1 * Q1'; H = (Q1t ./ W_sqrt) .* W_sqrt'; else H = []; endif else leverage = zeros (n, 1); if (compute_H) H = zeros (n, n); else H = []; endif endif fit.beta = beta; fit.H = H; fit.leverage = leverage; fit.SSE = SSE; fit.SSR = SSR; fit.SST = SST; fit.DFE = DFE; fit.MSE = MSE; fit.RMSE = RMSE; fit.CovBeta = CovBeta; fit.rank_X = rank_X; fit.active_cols = active_cols; fit.Fitted = Fitted; fit.Raw = Raw; fit.SumLogW = SumLogW; fit.n_eff = n_eff; fit.SSW = SSE; endfunction function crit = lm_criteria (fit, has_intercept) p = fit.rank_X; SSE = fit.SSE; SSR = fit.SSR; SST = fit.SST; DFE = fit.DFE; MSE = fit.MSE; ## Observations with zero weight contribute to neither SSE nor SumLogW, ## so the likelihood, and every criterion built on it, is over the n_eff ## observations that carry weight. MATLAB counts all of them and drops ## the 0.5 * sum (log (w)) term; see the LogLikelihood property. n_obs = fit.n_eff; ## The Gaussian log-likelihood at variance SSE/n_obs. SSW is the ## weighted residual sum, which equals SSE for a least-squares fit and ## collapses the middle term to n_obs/2; a robust fit's SSE is a scale ## estimate instead, so the two differ and both are needed. s2 = SSE / n_obs; LogLikelihood = -(n_obs / 2) * log (2 * pi * s2) - fit.SSW / (2 * s2) ... + 0.5 * fit.SumLogW; AIC = -2 * LogLikelihood + 2 * p; dAIC = n_obs - p - 1; if (dAIC > 0) AICc = AIC + (2 * p * (p + 1)) / dAIC; else AICc = Inf; endif BIC = -2 * LogLikelihood + p * log (n_obs); CAIC = BIC + p; R2_ord = SSR / max (SST, eps); if (n_obs > 1 && DFE > 0) R2_adj = 1 - (SSE / DFE) / (SST / (n_obs - 1)); else R2_adj = NaN; endif if (has_intercept && p > 1) df1 = p - 1; Fstat = (SSR / df1) / max (MSE, eps); elseif (! has_intercept && p > 0) df1 = p; Fstat = (SSR / df1) / max (MSE, eps); else df1 = 0; Fstat = NaN; endif if (df1 > 0 && DFE > 0 && Fstat >= 0) Fpval = betainc (DFE / (DFE + df1 * Fstat), DFE / 2, df1 / 2); else Fpval = NaN; endif crit.LogLikelihood = LogLikelihood; crit.AIC = AIC; crit.AICc = AICc; crit.BIC = BIC; crit.CAIC = CAIC; crit.Rsquared = R2_ord; crit.AdjRsquared = R2_adj; crit.Fstat = Fstat; crit.Fpval = Fpval; endfunction function info = sw_extract (mdl0) pred_names = mdl0.PredictorNamesRaw; p_raw = numel (pred_names); cat_info = mdl0.CatLevelInfo; enc_names = mdl0.EncPredictorNames; y_sub = mdl0.ResponseVector (mdl0.SubsetMask); w_sub = mdl0.WeightVector (mdl0.SubsetMask); if (! isempty (mdl0.EncodedPredMatrix)) X_enc_sub = mdl0.EncodedPredMatrix; else tbl_sub = mdl0.Variables (mdl0.SubsetMask, :); X_raw = zeros (rows (tbl_sub), p_raw); for j = 1:p_raw col = tbl_sub.(pred_names{j}); if (iscell (col)) ci = find (strcmp (cat_info.names, pred_names{j})); levels_j = cat_info.levels{ci}; codes = zeros (rows (tbl_sub), 1); for k = 1:numel (levels_j) codes(strcmp (col, levels_j{k})) = k; endfor X_raw(:, j) = codes; elseif (isa (col, 'categorical')) ci = find (strcmp (cat_info.names, pred_names{j})); levels_j = cat_info.levels{ci}; [~, X_raw(:, j)] = ismember (cellstr (col), levels_j); else X_raw(:, j) = double (col); endif endfor X_enc_sub = reencode_predictors (X_raw, pred_names, cat_info, enc_names); endif info.terms_enc = mdl0.TermsMatrix; info.cat_info = cat_info; info.enc_names = enc_names; info.pred_names = pred_names; info.p_raw = p_raw; info.X_enc = X_enc_sub; info.y = y_sub; info.w = w_sub; info.has_intercept = mdl0.HasIntercept; info.n_obs = mdl0.NumObservations; info.orig_opts = mdl0.OrigOpts; info.variables = mdl0.Variables; info.response_name = mdl0.ResponseName; endfunction endmethods endclassdef ## True if NAME appears as a parameter name in the Name-Value list ARGS. function tf = sw_named (args, name) tf = false; for k = 1:2:numel (args) - 1 if ((ischar (args{k}) || isstring (args{k})) ... && strcmpi (char (args{k}), name)) tf = true; return; endif endfor endfunction function opts = lm_parse_nv (nv_args) opt_names = {'Intercept', 'Weights', 'Exclude', 'RobustOpts', ... 'VarNames', 'CategoricalVars', 'ResponseVar', 'PredictorVars'}; def_vals = {true, [], [], [], {}, [], '', {}}; [intercept, weights, exclude, robustopts, varnames, catvars, ... respvar, predvars, rem_args] = parsePairedArguments (opt_names, def_vals, nv_args); opts.PredictorVarsGiven = false; for i = 1:2:numel (nv_args) if ((ischar (nv_args{i}) || isstring (nv_args{i})) ... && strcmpi (char (nv_args{i}), 'PredictorVars')) opts.PredictorVarsGiven = true; endif endfor if (! isempty (rem_args)) error ("LinearModel: Unknown option '%s'.", rem_args{1}); endif opts.Intercept = logical (intercept); opts.Exclude = exclude; opts.CategoricalVars = catvars; opts.ResponseVar = char (respvar); if (isempty (robustopts) || (ischar (robustopts) && strcmpi (robustopts, 'off')) ... || (islogical (robustopts) && ! robustopts)) opts.RobustOpts = []; else rname = 'bisquare'; rtune = []; if (isstruct (robustopts)) if (isfield (robustopts, 'RobustWgtFun') && ! isempty (robustopts.RobustWgtFun)) rname = robustopts.RobustWgtFun; endif if (isfield (robustopts, 'Tune')) rtune = robustopts.Tune; endif elseif (is_function_handle (robustopts)) rname = robustopts; elseif (ischar (robustopts) && ! strcmpi (robustopts, 'on')) rname = robustopts; elseif (! (ischar (robustopts) && strcmpi (robustopts, 'on'))) error ("LinearModel: invalid RobustOpts value."); endif if (is_function_handle (rname)) wfun = rname; if (isempty (rtune)) rtune = 1; endif else switch (lower (char (rname))) case 'andrews' wfun = @(r) (abs (r) < pi) .* sin (max (sqrt (eps), abs (r))) ... ./ max (sqrt (eps), abs (r)); def_tune = 1.339; case 'bisquare' wfun = @(r) (abs (r) < 1) .* (1 - r.^2).^2; def_tune = 4.685; case 'cauchy' wfun = @(r) 1 ./ (1 + r.^2); def_tune = 2.385; case 'fair' wfun = @(r) 1 ./ (1 + abs (r)); def_tune = 1.400; case 'huber' wfun = @(r) 1 ./ max (1, abs (r)); def_tune = 1.345; case 'logistic' wfun = @(r) tanh (max (sqrt (eps), abs (r))) ./ max (sqrt (eps), abs (r)); def_tune = 1.205; case 'ols' wfun = @(r) ones (size (r)); def_tune = 1; case 'talwar' wfun = @(r) 1 * (abs (r) < 1); def_tune = 2.795; case 'welsch' wfun = @(r) exp (-(r.^2)); def_tune = 2.985; otherwise error ("LinearModel: unrecognised RobustWgtFun '%s'.", char (rname)); endswitch if (isempty (rtune)) rtune = def_tune; endif endif opts.RobustOpts = struct ('WgtFun', wfun, 'Tune', rtune); endif if (isempty (weights)) opts.Weights = []; else opts.Weights = double (weights(:)); endif if (isempty (varnames)) opts.VarNames = {}; else opts.VarNames = cellstr (varnames); endif if (isempty (predvars)) opts.PredictorVars = {}; elseif (ischar (predvars) || iscellstr (predvars) || isstring (predvars)) opts.PredictorVars = cellstr (predvars); else opts.PredictorVars = predvars; endif endfunction ## observation-level influence statistics; returns D struct function D = lm_diagnostics (X, y, fit, w) n = rows (X); p = fit.rank_X; h = fit.leverage; Raw = fit.Raw; DFE = fit.DFE; MSE = fit.MSE; RMSE = fit.RMSE; S2_i = (DFE * MSE - w .* Raw.^2 ./ max (1 - h, eps)) / max (DFE - 1, 1); r_std = Raw ./ max (RMSE .* sqrt (max (1 - h, eps)), eps); r_stu = Raw ./ max (sqrt (max (S2_i, eps)) .* sqrt (max (1 - h, eps)), eps); CooksDistance = (w / max (p, 1)) .* r_std.^2 .* h ./ max (1 - h, eps); Dffits = r_stu .* sqrt (h ./ max (1 - h, eps)) .* sqrt (w); CovRatio = (S2_i ./ max (MSE, eps)).^p ./ max (1 - h, eps); p_full = columns (X); active = fit.active_cols; CovB_act = fit.CovBeta(active, active); XtXinv_d = diag (CovB_act) / max (MSE, eps); Dfbetas = NaN (n, p_full); if (p > 0) for i = 1:n xi_act = X(i, active)'; infl = (CovB_act / max (MSE, eps)) * xi_act; denom_base = (1 - h(i)) * sqrt (max (S2_i(i), eps)); for jj = 1:p se_jj = sqrt (max (XtXinv_d(jj), eps)); Dfbetas(i, active(jj)) = infl(jj) * Raw(i) / max (denom_base * se_jj, eps); endfor endfor endif D.Leverage = h; D.CooksDistance = CooksDistance; D.Dffits = Dffits; D.S2_i = S2_i; D.CovRatio = CovRatio; D.Dfbetas = Dfbetas; D.HatMatrix = fit.H; endfunction function mdl2 = lm_refit (mdl, new_terms) opts = mdl.OrigOpts; has_int = any (all (new_terms(:, 1:end-1) == 0, 2)); if (! isempty (mdl.CatLevelInfo) && isfield (mdl.CatLevelInfo, 'names') ... && ! isempty (mdl.CatLevelInfo.names)) cat_vars = mdl.CatLevelInfo.names; else cat_vars = opts.CategoricalVars; endif nv_list = {'Intercept', has_int}; if (! isempty (opts.Weights)) nv_list = [nv_list, {'Weights', opts.Weights}]; endif if (! isempty (opts.Exclude)) nv_list = [nv_list, {'Exclude', opts.Exclude}]; endif if (! isempty (cat_vars)) nv_list = [nv_list, {'CategoricalVars', cat_vars}]; endif mdl2 = fitlm (mdl.Variables, mdl.ResponseName, new_terms, nv_list{:}); endfunction function [ax, mdl, args] = lm_plot_axes (this, rest) if (isscalar (this) && isgraphics (this, 'axes')) ax = this; mdl = rest{1}; args = rest(2:end); else ax = []; mdl = this; args = rest; endif endfunction function props = lm_plot_props (nv_args) opt_names = {'Color', 'Marker', 'MarkerSize', 'MarkerEdgeColor', ... 'MarkerFaceColor', 'LineWidth'}; def_vals = {[0.1490, 0.5490, 0.8660], 'x', 6, 'auto', 'none', 0.5}; [color, marker, markersize, mec, mfc, lw, rem_args] = ... parsePairedArguments (opt_names, def_vals, nv_args); if (! isempty (rem_args)) if (ischar (rem_args{1}) || isstring (rem_args{1})) error ("lm_plot_props: unrecognized property '%s'.", ... char (rem_args{1})); else error (strcat ("lm_plot_props: property name must be a character", ... " vector or string scalar, not a %s."), ... class (rem_args{1})); endif endif props.Color = color; props.Marker = marker; props.MarkerSize = markersize; props.MarkerEdgeColor = mec; props.MarkerFaceColor = mfc; props.LineWidth = lw; endfunction function h = lm_plot_data (ax, xdata, ydata, props) h = plot (ax, xdata, ydata, ... 'LineStyle', 'none', ... 'Color', props.Color, ... 'Marker', props.Marker, ... 'MarkerSize', props.MarkerSize, ... 'MarkerEdgeColor', props.MarkerEdgeColor, ... 'MarkerFaceColor', props.MarkerFaceColor, ... 'LineWidth', props.LineWidth); endfunction function loc = lm_legend_corner (xdata, ydata) xr = xdata(! isnan (xdata)); yr = ydata(! isnan (ydata)); xmid = (min (xr) + max (xr)) / 2; ymid = (min (yr) + max (yr)) / 2; counts = [sum(xr >= xmid & yr >= ymid), sum(xr < xmid & yr >= ymid), ... sum(xr >= xmid & yr < ymid), sum(xr < xmid & yr < ymid)]; locs = {'northeast', 'northwest', 'southeast', 'southwest'}; [~, best_idx] = min (counts); loc = locs{best_idx}; endfunction function c_row = lm_interaction_row (X_act, fix_cols, fix_vals, pred, cinfo, ename, terms) X_rows = X_act; for f = 1:numel (fix_cols) X_rows(:, fix_cols(f)) = fix_vals(f); endfor X_enc = reencode_predictors (X_rows, pred, cinfo, ename); D = build_design (terms, X_enc); c_row = mean (D, 1); endfunction function col_str = lm_col_to_str (col) if (iscell (col)) col_str = col; elseif (isa (col, 'categorical')) col_str = cellstr (col); elseif (isa (col, 'string')) col_str = cellstr (col); else col_str = cellstr (num2str (col(:))); endif endfunction function [X_act, is_cat, cat_lvls] = lm_encode_active_predictors (mdl, act, pred, cinfo) n_act = sum (act); p = numel (pred); X_act = zeros (n_act, p); is_cat = false (1, p); cat_lvls = cell (1, p); for k = 1:p ci = []; if (! isempty (cinfo) && isfield (cinfo, 'names') ... && ! isempty (cinfo.names)) ci = find (strcmp (cinfo.names, pred{k})); endif col = mdl.Variables{act, pred{k}}; if (! isempty (ci)) is_cat(k) = true; levels_k = cinfo.levels{ci}; cat_lvls{k} = levels_k; col_str = lm_col_to_str (col); codes = zeros (n_act, 1); for L = 1:numel (levels_k) codes(strcmp (col_str, char (levels_k{L}))) = L; endfor X_act(:,k) = codes; else X_act(:,k) = double (col(:)); endif endfor endfunction function used = lm_predictors_in_model (pred_all, cinfo, ename) n = numel (pred_all); used = false (1, n); for k = 1:n ci = []; if (! isempty (cinfo) && ! isempty (cinfo.names)) ci = find (strcmp (cinfo.names, pred_all{k})); endif if (isempty (ci)) used(k) = any (strcmp (ename, pred_all{k})); else levels_k = cinfo.levels{ci}; for L = 2:numel (levels_k) nm = sprintf ("%s_%s", pred_all{k}, char (levels_k{L})); if (any (strcmp (ename, nm))) used(k) = true; endif endfor endif endfor endfunction function C = lm_effects_contrasts (mdl) if (! any (any (mdl.TermsMatrix(:, 1:end-1) != 0))) C = []; return; endif pred_all = mdl.PredictorNames; cinfo = mdl.CatLevelInfo; ename = mdl.EncPredictorNames; pred = pred_all (lm_predictors_in_model (pred_all, cinfo, ename)); p = numel (pred); act = mdl.ObservationInfo.Subset; [X_act, is_cat, cat_lvls] = lm_encode_active_predictors (mdl, act, pred, cinfo); C = zeros (p, mdl.NumCoefficients); for j = 1:p if (is_cat(j)) x_lo = 1; x_hi = numel (cat_lvls{j}); else x_lo = min (X_act(:,j)); x_hi = max (X_act(:,j)); endif X_hi_rows = X_act; X_hi_rows(:,j) = x_hi; X_lo_rows = X_act; X_lo_rows(:,j) = x_lo; X_hi_enc = reencode_predictors (X_hi_rows, pred, cinfo, ename); X_lo_enc = reencode_predictors (X_lo_rows, pred, cinfo, ename); D_hi = build_design (mdl.TermsMatrix, X_hi_enc); D_lo = build_design (mdl.TermsMatrix, X_lo_enc); C(j,:) = mean (D_hi - D_lo, 1); endfor endfunction function IC = lm_interaction_contrasts (mdl) pred_all = mdl.PredictorNames; cinfo = mdl.CatLevelInfo; ename = mdl.EncPredictorNames; pred = pred_all (lm_predictors_in_model (pred_all, cinfo, ename)); p = numel (pred); IC = struct ('OwnGridRows', {cell(1, p)}, 'Pairs', {cell(p, p)}); if (p < 2) return; endif act = mdl.ObservationInfo.Subset; terms = mdl.TermsMatrix; tpred = terms(:, 1:p); [X_act, is_cat, cat_lvls] = lm_encode_active_predictors (mdl, act, pred, cinfo); grids = cell (1, p); for j = 1:p if (is_cat(j)) n_lv = numel (cat_lvls{j}); grids{j} = (1:n_lv)'; rows_j = zeros (n_lv, mdl.NumCoefficients); for L = 1:n_lv rows_j(L,:) = lm_interaction_row (X_act, j, L, pred, cinfo, ename, terms); endfor IC.OwnGridRows{j} = rows_j; else lo = min (X_act(:,j)); hi = max (X_act(:,j)); grids{j} = [lo; (lo + hi) / 2; hi]; endif endfor for j1 = 1:p for j2 = 1:p if (j1 == j2) continue; endif shared = tpred(:,j1) > 0 & tpred(:,j2) > 0; if (! any (shared)) continue; endif grid1 = grids{j1}; if (is_cat(j2)) grid2 = grids{j2}; else lo2 = min (X_act(:,j2)); hi2 = max (X_act(:,j2)); mid2 = (lo2 + hi2) / 2; deg2 = max (tpred(shared, j2)); extra_n = max (0, deg2 + 1 - 3); if (extra_n > 0) q = linspace (lo2, hi2, extra_n + 2); grid2 = sort ([lo2; mid2; hi2; q(2:end-1)']); else grid2 = [lo2; mid2; hi2]; endif endif n1 = numel (grid1); n2 = numel (grid2); rows = zeros (n1 * n2, mdl.NumCoefficients); idx = 0; for a = 1:n1 for b = 1:n2 idx = idx + 1; rows(idx,:) = lm_interaction_row (X_act, [j1, j2], [grid1(a), grid2(b)], ... pred, cinfo, ename, terms); endfor endfor IC.Pairs{j1,j2} = struct ('grid1', grid1, 'grid2', grid2, 'rows', rows); endfor endfor endfunction function fit = lm_robust_fit (X, y, w, wgtfun, tune) n = rows (X); p = columns (X); w = w(:); sw = sqrt (w); ## A robust fit's SSE is sigma^2 * DFE, a robust scale estimate rather than ## the weighted residual sum, so the two enter the log-likelihood ## separately; lm_criteria takes the residual sum from SSW. The ## 0.5 * sum (log (w)) correction of the OLS path does not apply here, sigma ## being formed from residuals already divided by sqrt (w) and so carrying ## no dependence on the scale of the weights. SumLogW = 0; n_eff = n; Xw = X .* sw; yw = y .* sw; beta = Xw \ yw; [~, R] = qr (Xw, 0); E = Xw / R; h = min (0.9999, sum (E.^2, 2)); adjfactor = 1 ./ sqrt (max (1 - h, eps)); DFE = n - p; if (DFE <= 0) fit.beta = beta; fit.H = zeros (n, n); fit.leverage = zeros (n, 1); fit.SSE = NaN; fit.SSR = NaN; fit.SST = NaN; fit.DFE = DFE; fit.MSE = NaN; fit.RMSE = NaN; fit.CovBeta = NaN (p, p); fit.rank_X = p; fit.active_cols = 1:p; fit.Fitted = X * beta; fit.Raw = y - fit.Fitted; fit.RobustWeights = ones (n, 1); fit.SumLogW = SumLogW; fit.n_eff = n_eff; fit.SSW = sum (w .* fit.Raw.^2); return; endif ols_s = norm (y - X * beta) / sqrt (DFE); tiny_s = 1e-6 * std (y); tolD = sqrt (eps); iterlim = 50; iter = 0; beta0 = zeros (size (beta)); wts = ones (n, 1); while (iter == 0 || any (abs (beta - beta0) > tolD * max (abs (beta), abs (beta0)))) iter = iter + 1; if (iter > iterlim) break; endif ## The prior weights enter through the fit and the leverage; the weight ## function scores the raw residual, as MATLAB's does. Dividing by ## sqrt (w) here would make a precisely measured observation less likely ## to be flagged as an outlier, not more. r = (y - X * beta); radj = r .* adjfactor; rs = sort (abs (radj)); s = median (rs(max (1, p):end)) / 0.6745; wts = wgtfun(radj / (max (s, tiny_s) * tune)); beta0 = beta; ww = sqrt (w .* wts); beta = (X .* ww) \ (y .* ww); endwhile r = (y - X * beta); radj = r .* adjfactor; rs = sort (abs (radj)); mad_s = median (rs(max (1, p):end)) / 0.6745; if (all (wts < tolD | wts > 1 - tolD)) included = wts > 1 - tolD; robust_s = norm (r(included)) / sqrt (max (sum (included) - p, eps)); else st = max (mad_s, tiny_s) * tune; u = radj / st; phi = u .* wgtfun(u); delta = 0.0001; u1 = u - delta; phi0 = u1 .* wgtfun(u1); u1 = u + delta; phi1 = u1 .* wgtfun(u1); dphi = (phi1 - phi0) / (2 * delta); m1 = mean (dphi); m2 = sum ((1 - h) .* phi.^2) / (n - p); K = 1 + (p / n) * (1 - m1) / m1; robust_s = K * sqrt (m2) * st / m1; endif sigma = max (robust_s, sqrt ((ols_s^2 * p^2 + robust_s^2 * n) / (p^2 + n))); RI = R \ eye (p); CovBeta = (RI * RI') * sigma^2; ww = sqrt (w .* wts); Xwf = X .* ww; [Qf, ~] = qr (Xwf, 0); Q1t = Qf * Qf'; H = (Q1t ./ ww) .* ww'; Fitted = X * beta; Raw = y - Fitted; ybar = mean (y); SSR = sum ((Fitted - ybar).^2); SSE = sigma^2 * DFE; SST = SSE + SSR; fit.beta = beta; fit.H = H; fit.leverage = h; fit.SSE = SSE; fit.SSR = SSR; fit.SST = SST; fit.DFE = DFE; fit.MSE = sigma^2; fit.RMSE = sigma; fit.CovBeta = CovBeta; fit.rank_X = p; fit.active_cols = 1:p; fit.Fitted = Fitted; fit.Raw = Raw; fit.RobustWeights = wts; fit.SumLogW = SumLogW; fit.n_eff = n_eff; fit.SSW = sum (w .* Raw.^2); endfunction function [delta, fit_f, fit_r] = anova_delta_sse (X, y, w, cols_reduced, cols_full, fit_f) if (nargin < 6 || isempty (fit_f)) fit_f = LinearModel.lm_fit (X(:, cols_full), y, w, false); endif fit_r = LinearModel.lm_fit (X(:, cols_reduced), y, w, false); delta = fit_r.SSE - fit_f.SSE; endfunction function contain_mx = anova_containment (groups, extended) nterm = numel (groups); factor_list = cell (nterm, 1); for k = 1:nterm parts = strsplit (groups(k).Name, ':'); fl = struct ('var', {}, 'exp', {}); for f = 1:numel (parts) p = strsplit (parts{f}, '^'); if (numel (p) == 2) fl(end+1) = struct ('var', p{1}, 'exp', str2double (p{2})); else fl(end+1) = struct ('var', p{1}, 'exp', 1); endif endfor factor_list{k} = fl; endfor contain_mx = false (nterm, nterm); for i = 1:nterm for j = 1:nterm if (i == j) continue; endif fi = factor_list{i}; fj = factor_list{j}; ok = true; for f = 1:numel (fi) match = false; for g = 1:numel (fj) if (strcmp (fi(f).var, fj(g).var)) if (extended) match = (fj(g).exp >= fi(f).exp); else match = (fj(g).exp == fi(f).exp); endif if (match) break; endif endif endfor if (! match) ok = false; break; endif endfor contain_mx(i, j) = ok; endfor endfor endfunction function X_raw = anova_decode_raw (tbl_sub, pred_names, cat_info) p_raw = numel (pred_names); X_raw = zeros (rows (tbl_sub), p_raw); for j = 1:p_raw col = tbl_sub.(pred_names{j}); if (iscell (col)) ci = find (strcmp (cat_info.names, pred_names{j})); levels_j = cat_info.levels{ci}; codes = zeros (rows (tbl_sub), 1); for k = 1:numel (levels_j) codes(strcmp (col, levels_j{k})) = k; endfor X_raw(:, j) = codes; elseif (isa (col, 'categorical')) ci = find (strcmp (cat_info.names, pred_names{j})); levels_j = cat_info.levels{ci}; [~, X_raw(:, j)] = ismember (cellstr (col), levels_j); else X_raw(:, j) = double (col); endif endfor endfunction ## The upper tail of an F distribution. BETAINC wants both of its shape ## parameters strictly positive, and with no degrees of freedom on either ## side there is no test to report, so those entries answer NaN rather than ## reaching it. The shapes are spread over the statistic so that a scalar ## count against a vector of them answers elementwise. function p = f_pvalue (dfe, df, Fstat) x = dfe ./ (dfe + df .* Fstat); a = (dfe / 2) .* ones (size (x)); b = (df / 2) .* ones (size (x)); p = NaN (size (x)); ok = (a > 0) & (b > 0); if (any (ok(:))) p(ok) = betainc (x(ok), a(ok), b(ok)); endif endfunction %!demo %! %! ## Simple linear regression with a single predictor. %! ## Ten runners record their weekly training distance and their finish %! ## time in a 10k race. We fit a straight line through this data and %! ## look at the fitted coefficients, then use predict to estimate the %! ## finish time for a runner who trains a distance not in the sample. %! Distance = [10; 15; 20; 25; 30; 35; 40; 45; 50; 55]; %! Time = [58; 55; 52; 50; 47; 45; 43; 41; 40; 38]; %! X = Distance; %! y = Time; %! %! ## Fit the model and inspect the estimated slope and intercept. %! mdl = fitlm (X, y) %! %! ## Predict the finish time for a runner training 32 km per week. %! ypred = predict (mdl, 32) %!demo %! %! ## Multiple linear regression with two predictors, followed by a %! ## confidence interval on the coefficients. %! ## Thirteen coffee shops report their weekly foot traffic and the %! ## number of items on their menu, along with weekly revenue. We fit a %! ## model with both predictors, then use coefCI to see how precisely %! ## each coefficient is estimated. %! Traffic = [120; 150; 90; 200; 175; 60; 220; 140; 100; 190; 80; 210; 130]; %! MenuSize = [8; 12; 6; 15; 10; 5; 18; 9; 7; 14; 6; 16; 11]; %! Revenue = [1450; 1820; 1010; 2400; 2050; 700; 2650; 1700; 1150; ... %! 2300; 900; 2500; 1600]; %! X = [Traffic, MenuSize]; %! y = Revenue; %! %! ## Fit the model with both predictors together. %! mdl = fitlm (X, y) %! %! ## Check how tight the 95% confidence interval is on each coefficient. %! ci = coefCI (mdl) %!demo %! %! ## Growing a model with addTerms and predicting with the richer model. %! ## We model fuel economy from the carsmall data set using weight and %! ## horsepower as main effects only. addTerms then brings in the %! ## weight-horsepower interaction without needing to refit by hand, and %! ## predict shows how the estimate for a new car changes once that %! ## interaction is included. %! load carsmall %! X = [Weight, Horsepower]; %! y = MPG; %! %! ## Fit the additive model first. %! mdl = fitlm (X, y); %! %! ## Add the interaction between weight and horsepower. %! mdl2 = addTerms (mdl, 'x1:x2'); %! %! ## Compare predictions from both models for the same new car. %! Xnew = [3200, 120]; %! ypred1 = predict (mdl, Xnew) %! ypred2 = predict (mdl2, Xnew) %!demo %! %! ## Simplifying a model with removeTerms and comparing fit quality. %! ## We fit the full Hald cement model with all four ingredients, then %! ## use removeTerms to drop the weakest predictor and refit %! ## automatically. Comparing SSE before and after shows how little %! ## explanatory power that ingredient was actually contributing. %! load hald %! X = ingredients; %! y = heat; %! %! ## Fit the model with all four ingredients. %! mdl = fitlm (X, y); %! %! ## Drop the third ingredient and refit on the same data. %! mdl2 = removeTerms (mdl, 'x3'); %! %! ## Compare how much the error sum of squares changed. %! sse_full = mdl.SSE %! sse_reduced = mdl2.SSE %!demo %! %! ## Testing a linear hypothesis and checking residual autocorrelation. %! ## Twelve patients are given a drug at different doses over different %! ## treatment durations, and a recovery score is recorded. coefTest %! ## checks whether the dose and duration coefficients are actually %! ## equal, and dwtest separately checks whether the residuals still %! ## carry a leftover pattern the model failed to capture. %! Dose = [10; 15; 20; 25; 30; 35; 12; 18; 22; 28; 32; 38]; %! Duration = [5; 7; 9; 11; 13; 15; 6; 8; 10; 12; 14; 16]; %! Recovery = [42; 48; 55; 60; 68; 74; 45; 52; 58; 65; 71; 78]; %! X = [Dose, Duration]; %! y = Recovery; %! mdl = fitlm (X, y); %! %! ## Test H0: the Dose and Duration coefficients are equal. %! H = [0 1 -1]; %! [p, F, r] = coefTest (mdl, H) %! %! ## Check for autocorrelation left over in the residuals. %! [pdw, dw] = dwtest (mdl) %!demo %! %! ## Checking residuals against fitted values. %! ## We fit a mileage model on the carsmall data set using weight and %! ## horsepower, then plot the raw residuals against the fitted values. %! ## A pattern in this plot, rather than a random scatter, would suggest %! ## the linear model is missing some curvature in the relationship. %! load carsmall %! X = [Weight, Horsepower]; %! y = MPG; %! mdl = fitlm (X, y); %! plotResiduals (mdl, 'fitted') %!demo %! %! ## Spotting influential observations with Cook's distance. %! ## Sixteen houses are matched by size and age to a sale price, but one %! ## house was sold far above what its size and age would predict. After %! ## fitting the model, plotDiagnostics with the cookd option highlights %! ## that single observation as having outsized influence on the fit. %! Size = [80; 95; 110; 120; 65; 140; 100; 130; 90; 150; 75; 105; ... %! 115; 85; 135; 125]; %! Age = [5; 10; 3; 8; 20; 2; 15; 6; 12; 1; 18; 9; 4; 14; 7; 11]; %! Price = [200; 230; 260; 280; 150; 320; 240; 300; 210; 340; 170; ... %! 250; 270; 190; 500; 290]; %! X = [Size, Age]; %! y = Price; %! mdl = fitlm (X, y); %! plotDiagnostics (mdl, 'cookd') %!demo %! %! ## Comparing the size of each predictor's effect, alongside a %! ## hypothesis test on the model as a whole. %! ## We fit a mileage model on the carsmall data set using weight and %! ## horsepower. plotEffects draws each coefficient's estimate with its %! ## confidence interval side by side, and coefTest checks whether %! ## weight's effect is significantly different from horsepower's. %! load carsmall %! X = [Weight, Horsepower]; %! y = MPG; %! mdl = fitlm (X, y); %! %! ## Visualize the relative size of each predictor's effect. %! plotEffects (mdl) %! %! ## Test whether the two coefficients differ significantly. %! H = [0 1 -1]; %! [p, F, r] = coefTest (mdl, H) %!shared mdl, X, y, n %! n = 20; %! X = [1:n; (1:n).^2]' / n; %! y = X * [3; -1] + 0.2 * sin ((1:n)'); %! mdl = fitlm (X, y); %!test %! ## scalar fit-quality %! assert_equal (mdl.NumObservations, 20); %! assert_equal (mdl.NumCoefficients, 3); %! assert_equal (mdl.NumVariables, 3); %! assert_equal (mdl.NumPredictors, 2); %! assert_equal (mdl.NumEstimatedCoefficients, 3); %! assert_equal (mdl.DFE, 17); %! assert_equal (mdl.SSE, 0.386545331386823, 1e-9); %! assert_equal (mdl.SSR, 583.523874670959, 1e-6); %! assert_equal (mdl.SST, 583.910420002346, 1e-6); %! assert_equal (mdl.MSE, 0.0227379606698351, 1e-10); %! assert_equal (mdl.RMSE, 0.150791116017606, 1e-10); %! assert_equal (mdl.Rsquared.Ordinary, 0.999338005765704, 1e-10); %! assert_equal (mdl.Rsquared.Adjusted, 0.999260124091081, 1e-10); %! assert_equal (mdl.LogLikelihood, 11.0836133807695, 1e-6); %! assert_equal (mdl.ModelCriterion.AIC, -16.1672267615389, 1e-6); %! assert_equal (mdl.ModelCriterion.AICc, -14.6672267615389, 1e-6); %! assert_equal (mdl.ModelCriterion.BIC, -13.180029940877, 1e-6); %! assert_equal (mdl.ModelCriterion.CAIC, -10.180029940877, 1e-6); %! assert_equal (mdl.ModelFitVsNullModel.Fstat, 12831.4909842738, 1e-4); %! assert_equal (strcmp (mdl.ModelFitVsNullModel.NullModel, 'constant'), true); %!test %! ## Steps is empty for a non-stepwise fit, as in GeneralizedLinearModel %! assert_equal (size (mdl.Steps), [0, 0]); %!test %! ## a table column the formula never mentions is not a predictor of the fit %! xa = (1:n)' / n; %! xb = cos ((1:n)'); %! gc = categorical (mod ((1:n)', 3)); %! Tu = table (xa, xb, gc, y, 'VariableNames', {'xa', 'xb', 'gc', 'y'}); %! m = fitlm (Tu, 'y ~ xa + xb'); %! assert_equal (m.PredictorNames, {'xa'; 'xb'}); %! assert_equal (m.NumPredictors, 2); %! assert_equal (m.VariableNames, {'xa'; 'xb'; 'gc'; 'y'}); %! assert_equal (m.NumVariables, 4); %! assert_equal (m.VariableInfo.InModel', [true, true, false, false]); %!test %! ## the unused variable being categorical no longer breaks type 3 sums of %! ## squares, which indexed past the end of the encoded matrix %! xa = (1:n)' / n; %! xb = cos ((1:n)'); %! gc = categorical (mod ((1:n)', 3)); %! Tu = table (xa, xb, gc, y, 'VariableNames', {'xa', 'xb', 'gc', 'y'}); %! m = fitlm (Tu, 'y ~ xa + xb'); %! t = anova (m, 'components', 3); %! assert_equal (t.Properties.RowNames, {'xa'; 'xb'; 'Error'}); %! assert_equal (numel (t.SumSq), 3); %!test %! ## a model fit with no predictors at all reports none %! mc = fitlm (X, y, 'constant'); %! assert_equal (isempty (mc.PredictorNames), true); %! assert_equal (mc.NumPredictors, 0); %! assert_equal (any (mc.VariableInfo.InModel), false); %!test %! ## a terms matrix that zeroes a predictor out drops it, as a formula does %! mt = fitlm (X, y, [0 0 0; 1 0 0]); %! mf = fitlm (X, y, 'y ~ x1'); %! assert_equal (mt.PredictorNames, {'x1'}); %! assert_equal (mt.NumPredictors, 1); %! assert_equal (mt.VariableInfo.InModel', [true, false, false]); %! assert_equal (mf.PredictorNames, {'x1'}); %! assert_equal (mf.NumPredictors, 1); %!test %! ## a predictor reached only through an interaction is used, and stays %! mi = fitlm (X, y, 'y ~ x1 + x1:x2'); %! assert_equal (mi.PredictorNames, {'x1'; 'x2'}); %! assert_equal (mi.NumPredictors, 2); %! assert_equal (mi.VariableInfo.InModel', [true, true, false]); %!test %! ## addTerms and stepwise selection search the candidates, not the chosen %! mc = fitlm (X, y, 'constant'); %! m1 = addTerms (mc, 'x1'); %! assert_equal (m1.PredictorNames, {'x1'}); %! assert_equal (m1.NumCoefficients, 2); %!test %! ## constant-only model: SSR is exactly zero, SSE equals SST %! mc = fitlm (X, y, 'constant'); %! assert_equal (mc.SSE, 583.910420002346, 1e-6); %! assert_equal (mc.SSR, 0, 1e-12); %! assert_equal (mc.SSE, mc.SST, 1e-12); %!test %! ## coefficient estimates, SE, tStat, names, covariance, schema %! assert_equal (mdl.Coefficients.Estimate, [0.1161886778; 2.508451491; -0.9788353298], 1e-7); %! assert_equal (mdl.Coefficients.SE, [0.112185831; 0.4920818186; 0.02276108523], 1e-8); %! assert_equal (mdl.Coefficients.tStat, [1.035680502; 5.097630913; -43.00477415], 1e-6); %! assert_equal (all (mdl.Coefficients.pValue >= 0 & mdl.Coefficients.pValue <= 1), true); %! assert_equal (isequal (mdl.CoefficientNames, {'(Intercept)', 'x1', 'x2'}), true); %! assert_equal (isequal (mdl.CoefficientNames, mdl.Coefficients.Properties.RowNames(:)'), true); %! assert_equal (size (mdl.CoefficientCovariance), [3, 3]); %! assert_equal (diag (mdl.CoefficientCovariance), [0.0125857; 0.242145; 0.000518067], 1e-6); %! assert_equal (width (mdl.Coefficients), 4); %! assert_equal (isequal (mdl.Coefficients.Properties.VariableNames, ... %! {'Estimate','SE','tStat','pValue'}), true); %!test %! ## fitted values, predict(), residual columns (obs 1-3), schema %! assert_equal (mdl.Fitted, y - mdl.Residuals.Raw, 1e-10); %! yp = predict (mdl, X); %! assert_equal (size (yp), [20, 1]); %! assert_equal (yp(1), 0.192669485827491, 1e-10); %! assert_equal (yp(2), 0.171266760882256, 1e-10); %! assert_equal (mdl.Residuals.Raw(1:3), [0.075624711134088; 0.110592724482880; -0.023756501342530], 1e-10); %! assert_equal (mdl.Residuals.Pearson(1:3), [0.501519672586403; 0.733416711830473; -0.157545762442370], 1e-9); %! assert_equal (mdl.Residuals.Standardized(1:3), [0.632246516521578; 0.844226394951239; -0.172381368754725], 1e-8); %! assert_equal (mdl.Residuals.Studentized(1:3), [0.620710275056923; 0.836747864205268; -0.167380843634378], 1e-6); %! assert_equal (width (mdl.Residuals), 4); %! assert_equal (isequal (mdl.Residuals.Properties.VariableNames, ... %! {'Raw','Pearson','Studentized','Standardized'}), true); %!test %! ## diagnostics %! H = mdl.Diagnostics.HatMatrix; %! assert_equal (size (H), [20, 20]); %! assert_equal (H, H', 1e-10); %! assert_equal (H * H, H, 1e-8); %! assert_equal (H(1,1), 0.370779220779221, 1e-10); %! assert_equal (H(1,2), 0.298051948051948, 1e-10); %! assert_equal (mdl.Diagnostics.Leverage(1:3), [0.370779220779221; 0.245283663704716; 0.164718614718615], 1e-10); %! assert_equal (mdl.Diagnostics.CooksDistance(1:3), [0.078517048682575; 0.077211407930332; 0.001953301452841], 1e-8); %! assert_equal (mdl.Diagnostics.S2_i(1:3), [0.023591009857798; 0.023146223303229; 0.024116854077430], 1e-8); %! assert_equal (mdl.Diagnostics.CovRatio(1:3), [1.774933176573401; 1.397661919176034; 1.428481535363283], 1e-6); %! assert_equal (mdl.Diagnostics.Dffits(1:3), [0.476480465355394; 0.477020506700835; -0.074329411030064], 1e-6); %! assert_equal (size (mdl.Diagnostics.Dfbetas), [20, 3]); %! assert_equal (width (mdl.Diagnostics), 7); %! assert_equal (isequal (mdl.Diagnostics.Properties.VariableNames, ... %! {'Leverage','CooksDistance','Dffits','S2_i', ... %! 'CovRatio','Dfbetas','HatMatrix'}), true); %!test %! ## ObservationInfo, VariableInfo, names, Formula, Variables %! assert_equal (width (mdl.ObservationInfo), 4); %! assert_equal (height (mdl.ObservationInfo), 20); %! assert_equal (isequal (mdl.ObservationInfo.Properties.VariableNames, ... %! {'Weights','Excluded','Missing','Subset'}), true); %! assert_equal (all (mdl.ObservationInfo.Weights == 1), true); %! assert_equal (all (mdl.ObservationInfo.Subset == ... %! (! mdl.ObservationInfo.Missing & ! mdl.ObservationInfo.Excluded)), true); %! assert_equal (width (mdl.VariableInfo), 4); %! assert_equal (height (mdl.VariableInfo), 3); %! assert_equal (isequal (mdl.VariableInfo.Properties.VariableNames, ... %! {'Class','Range','InModel','IsCategorical'}), true); %! assert_equal (mdl.VariableInfo.InModel(strcmp (mdl.VariableNames, 'y')), false); %! assert_equal (all (mdl.VariableInfo.InModel(! strcmp (mdl.VariableNames, 'y'))), true); %! assert_equal (mdl.ResponseName, 'y'); %! assert_equal (isequal (mdl.PredictorNames, {'x1';'x2'}), true); %! assert_equal (isequal (mdl.VariableNames, {'x1';'x2';'y'}), true); %! assert_equal (mdl.Formula.HasIntercept, true); %! assert_equal (mdl.Formula.LinearPredictor, '1 + x1 + x2'); %! assert_equal (mdl.Formula.NTerms, 3); %! assert_equal (strcmp (mdl.Variables.Properties.VariableNames{end}, 'y'), true); %!test %! ## NaN in predictor drops the row from the fit %! X2 = X; X2(2,1) = NaN; %! m = fitlm (X2, y); %! assert_equal (m.NumObservations, 19); %! assert_equal (m.ObservationInfo.Missing(2), true); %! assert_equal (m.ObservationInfo.Subset(2), false); %! assert_equal (isnan (m.Fitted(2)), true); %! assert_equal (m.SSE, 0.370339572851658, 1e-9); %! assert_equal (m.SST, 547.616796178045, 1e-6); %! assert_equal (m.Coefficients.Estimate, ... %! [0.0641300185953764; 2.68263140079657; -0.985345792254554], 1e-7); %! yp = predict (m, X2); %! assert_equal (isnan (yp(2)), true); %! assert_equal (! isnan (yp(1)), true); %! assert_equal (size (m.Diagnostics.HatMatrix), [20, 20]); %! assert_equal (m.Diagnostics.Leverage(1), 0.488485648300892, 1e-8); %! assert_equal (m.Diagnostics.CooksDistance(1), 0.38266162627176, 1e-6); %!test %! ## NaN in response drops the row but predict still works normally since X has no NaN %! y3 = y; y3(5) = NaN; %! m = fitlm (X, y3); %! assert_equal (m.NumObservations, 19); %! assert_equal (m.ObservationInfo.Missing(5), true); %! assert_equal (m.Fitted(5), -0.457777594993880, 1e-10); %! assert_equal (isnan (m.Residuals.Raw(5)), true); %! assert_equal (m.SSE, 0.337042910721425, 1e-9); %! assert_equal (m.SST, 558.654961265991, 1e-6); %! assert_equal (m.Coefficients.Estimate, ... %! [0.145131680993155; 2.4865383021829; -0.979635081226208], 1e-7); %! yp = predict (m, X); %! assert_equal (yp(5), -0.45777759499388, 1e-8); %! assert_equal (yp(1), 0.22047684204099, 1e-8); %! assert_equal (m.Fitted, yp, 1e-12); %! assert_equal (size (m.Diagnostics.HatMatrix), [20, 20]); %! assert_equal (m.Diagnostics.Leverage(1), 0.386399650026734, 1e-8); %!test %! ## multiple NaN rows drop all affected observations from the fit %! X4 = X; X4([2,8,14],2) = NaN; %! m = fitlm (X4, y); %! assert_equal (sum (m.ObservationInfo.Missing), 3); %! assert_equal (m.NumObservations, 17); %! assert_equal (m.SSE, 0.261285495635633, 1e-9); %! assert_equal (m.SSR, 527.635694805749, 1e-6); %! assert_equal (m.SST, 527.896980301385, 1e-6); %! assert_equal (m.Coefficients.Estimate, ... %! [0.0986395043600395; 2.3735792982821; -0.97106191310122], 1e-7); %! assert_equal (size (m.Diagnostics.HatMatrix), [20, 20]); %! assert_equal (sum (m.Diagnostics.Leverage), 3, 1e-8); %!test %! ## exclude by index and exclude by logical vector give identical results %! m = fitlm (X, y, 'Exclude', [3, 7]); %! excl = false (n, 1); excl([3, 7]) = true; %! m2 = fitlm (X, y, 'Exclude', excl); %! assert_equal (m.NumObservations, 18); %! assert_equal (sum (m.ObservationInfo.Excluded), 2); %! assert_equal (m.Fitted(3), 0.045673486217021, 1e-10); %! assert_equal (m.Fitted(7), -1.416779188276219, 1e-10); %! assert_equal (isnan (m.Residuals.Raw(3)), true); %! assert_equal (isnan (m.Residuals.Raw(7)), true); %! assert_equal (m.Coefficients.Estimate, m2.Coefficients.Estimate, 1e-12); %! assert_equal (m.Coefficients.Estimate, ... %! [0.118938102486219; 2.43606890944554; -0.974833228191174], 1e-7); %! ype = predict (m); %! assert_equal (size (ype), [20, 1]); %! assert_equal (! isnan (ype(3)) && ! isnan (ype(7)), true); %! [~, yci] = predict (m); %! assert_equal (yci(1,1), -0.0283122762458446, 1e-10); %! assert_equal (yci(1,2), 0.412312049343719, 1e-10); %! assert_equal (size (m.Diagnostics.HatMatrix), [20, 20]); %! assert_equal (m.Diagnostics.Leverage(1), 0.437780279893411, 1e-8); %! assert_equal (m.Diagnostics.CooksDistance(1), 0.110112457355807, 1e-7); %!test %! ## an excluded row is fitted, but keeps neither residuals nor diagnostics %! m = fitlm (X, y, 'Exclude', [3, 7]); %! assert_equal (m.Fitted(3), predict (m, X(3,:)), 1e-12); %! assert_equal (isnan (m.Residuals.Pearson(3)), true); %! assert_equal (isnan (m.Residuals.Studentized(3)), true); %! assert_equal (isnan (m.Residuals.Standardized(3)), true); %! assert_equal (m.Diagnostics.Leverage(3), 0); %! assert_equal (isnan (m.Diagnostics.CooksDistance(3)), true); %!test %! ## weighting does not change which rows carry a fitted value %! m = fitlm (X, y, 'Weights', ones (n, 1) / n, 'Exclude', [1, 3]); %! assert_equal (m.Fitted(1), 0.146089374212009, 1e-10); %! assert_equal (m.Fitted(2), 0.133765115244874, 1e-10); %! assert_equal (isnan (m.Residuals.Raw(1)), true); %!test %! ## a missing predictor is the one case that leaves the fitted value NaN %! X2 = X; X2(2,1) = NaN; %! m = fitlm (X2, y); %! assert_equal (isnan (m.Fitted(2)), true); %! assert_equal (m.Fitted(1), 0.148994299022478, 1e-10); %! assert_equal (isnan (m.Residuals.Raw(2)), true); %!test %! ## NaN and exclude together remove both the missing and the excluded row %! X6 = X; X6(1,1) = NaN; %! m = fitlm (X6, y, 'Exclude', [2]); %! assert_equal (m.NumObservations, 18); %! assert_equal (m.ObservationInfo.Missing(1), true); %! assert_equal (m.ObservationInfo.Excluded(2), true); %! assert_equal (m.SSE, 0.342515396265007, 1e-9); %! assert_equal (m.Coefficients.Estimate, ... %! [-0.0735450184226009; 3.17679029176988; -1.0045827469016], 1e-7); %!test %! ## weighted least squares produces different SSE and stores the weights %! w = abs (sin ((1:n)')) + 0.1; %! m = fitlm (X, y, 'Weights', w); %! assert_equal (m.SSE, 0.363519720897775, 1e-10); %! assert_equal (m.ObservationInfo.Weights, w, 1e-15); %! assert_equal (m.SST, 4.419834786423099e+02, 1e-8); %! [yp, yci] = predict (m, [0.5 0.25; 1.0 1.0]); %! assert_equal (yp(1), 1.106748776307639, 1e-10); %! assert_equal (yp(2), 1.593185531572655, 1e-10); %! assert_equal (yci(1,1), 0.763985050242272, 1e-10); %! assert_equal (yci(1,2), 1.449512502373006, 1e-10); %! assert_equal (m.Diagnostics.Leverage(1), 0.421642939812731, 1e-8); %! assert_equal (m.Diagnostics.Leverage(2), 0.301314342928707, 1e-8); %! assert_equal (m.Diagnostics.HatMatrix(1,1), 0.421642939812731, 1e-8); %! assert_equal (m.Diagnostics.CooksDistance(1), 0.0728569335883748, 1e-7); %! assert_equal (m.Diagnostics.CovRatio(1), 1.96611264276187, 1e-6); %!test %! ## uniform weights scale internals but leave point estimates unchanged %! m = fitlm (X, y, 'Weights', 2 * ones (n, 1)); %! assert_equal (m.Coefficients.Estimate, mdl.Coefficients.Estimate, 1e-10); %!test %! ## the weighted log-likelihood does not depend on the scale of the weights %! ## (MATLAB's does, moving by n/2 * log (c); see the LogLikelihood property) %! xw = [1;2;3;4;5;6;7;8]; %! yw = [2.1;3.9;6.2;7.8;10.1;12.2;13.8;16.1]; %! w = [0.5;1;2;1;3;1;0.25;4]; %! m1 = fitlm (xw, yw, 'Weights', w); %! m2 = fitlm (xw, yw, 'Weights', 100 * w); %! assert_equal (m2.LogLikelihood, m1.LogLikelihood, 1e-12); %! assert_equal (m2.ModelCriterion.AIC, m1.ModelCriterion.AIC, 1e-12); %!test %! ## the weighted log-likelihood is R's logLik.lm, term for term %! xw = [1;2;3;4;5;6;7;8]; %! yw = [2.1;3.9;6.2;7.8;10.1;12.2;13.8;16.1]; %! w = [0.5;1;2;1;3;1;0.25;4]; %! m = fitlm (xw, yw, 'Weights', w); %! nw = 8; %! rL = 0.5 * (sum (log (w)) - nw * (log (2*pi) + 1 - log (nw) + log (m.SSE))); %! assert_equal (m.LogLikelihood, rL, 1e-12); %! assert_equal (m.LogLikelihood, 4.55223335786031, 1e-12); %!test %! ## a zero weight drops the observation from n as well as from DFE, as in R %! xw = [1;2;3;4;5;6;7;8]; %! yw = [2.1;3.9;6.2;7.8;10.1;12.2;13.8;16.1]; %! w = [0.5;1;0;1;3;1;0.25;4]; %! m = fitlm (xw, yw, 'Weights', w); %! wp = w(w > 0); %! nw = numel (wp); %! rL = 0.5 * (sum (log (wp)) - nw * (log (2*pi) + 1 - log (nw) + log (m.SSE))); %! assert_equal (m.LogLikelihood, rL, 1e-12); %! assert_equal (m.LogLikelihood, 4.68069254977738, 1e-12); %! assert_equal (m.DFE, 5); %! assert_equal (m.NumObservations, 8); %!test %! ## an unweighted fit carries no weight term and agrees with R2024a %! xw = [1;2;3;4;5;6;7;8]; %! yw = [2.1;3.9;6.2;7.8;10.1;12.2;13.8;16.1]; %! m = fitlm (xw, yw); %! assert_equal (m.LogLikelihood, 3.51016777175681, 1e-12); %! assert_equal (m.ModelCriterion.AIC, -3.02033554351362, 1e-12); %!test %! ## an unweighted robust fit's log-likelihood matches R2024a exactly %! xr = [1;2;3;4;5;6;7;8;9;10]; %! yr = [2.1;3.9;6.2;7.8;10.1;12.2;13.8;16.1;18.0;30.0]; %! m = fitlm (xr, yr, 'RobustOpts', 'bisquare'); %! assert_equal (m.LogLikelihood, -38.7915287023322, 1e-10); %! assert_equal (m.SSE, 18.9400079498975, 1e-10); %!test %! ## a weighted robust fit matches R2024a in coefficients, SSE and likelihood %! xr = [1;2;3;4;5;6;7;8;9;10]; %! yr = [2.1;3.9;6.2;7.8;10.1;12.2;13.8;16.1;18.0;30.0]; %! w = [0.5;1;2;1;3;1;0.25;4;1;2]; %! m = fitlm (xr, yr, 'RobustOpts', 'bisquare', 'Weights', w); %! assert_equal (m.Coefficients.Estimate, ... %! [0.0606142999263569; 2.00273390115041], 1e-10); %! assert_equal (m.SSE, 19.6032720154597, 1e-10); %! assert_equal (m.MSE, 2.45040900193246, 1e-10); %! assert_equal (m.LogLikelihood, -62.7197604655733, 1e-9); %!test %! ## the robust log-likelihood takes SSE and the residual sum separately %! xr = [1;2;3;4;5;6;7;8;9;10]; %! yr = [2.1;3.9;6.2;7.8;10.1;12.2;13.8;16.1;18.0;30.0]; %! m = fitlm (xr, yr, 'RobustOpts', 'bisquare'); %! r = m.Residuals.Raw; %! nr = 10; %! s2 = m.SSE / nr; %! assert_equal (m.LogLikelihood, ... %! -(nr/2) * log (2*pi*s2) - sum (r.^2) / (2*s2), 1e-12); %!test %! ## constant linear and default modelspecs behave as expected %! m = fitlm (X, y, 'constant'); %! assert_equal (m.NumCoefficients, 1); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! m2 = fitlm (X, y, 'linear'); %! m3 = fitlm (X, y, []); %! assert_equal (m2.NumCoefficients, 3); %! assert_equal (m2.Coefficients.Estimate, mdl.Coefficients.Estimate, 1e-12); %! assert_equal (m3.Coefficients.Estimate, mdl.Coefficients.Estimate, 1e-12); %!test %! ## purequadratic modelspec produces the expected term count %! m = fitlm (X, y, 'purequadratic'); %! assert_equal (m.NumCoefficients, 5); %!test %! ## interactions modelspec term count and coefficients are verified %! m = fitlm (X, y, 'interactions'); %! assert_equal (m.NumCoefficients, 4); %! assert_equal (m.SSE, 0.383859187927621, 1e-9); %! assert_equal (m.Coefficients.Estimate, ... %! [0.157640728038039; 2.08542680311791; -0.929682701072813; -0.031208018255475], 1e-7); %!test %! ## quadratic modelspec is rank deficient for this design and drops one coefficient %! m = fitlm (X, y, 'quadratic'); %! assert_equal (m.NumCoefficients, 6); %! assert_equal (m.SSE, 0.315784637443501, 1e-9); %! assert_equal (m.Coefficients.Estimate, ... %! [0.447436249544699; -2.44859403731902; -0.0121968798776254; ... %! -1.36755100280532; 0; 0.0318176901083297], 1e-7); %! drop = find (m.Coefficients.SE == 0); %! assert_equal (numel (drop), 1); %! assert_equal (isnan (m.Coefficients.tStat(drop)), true); %!test %! ## full modelspec with two predictors matches interactions exactly %! m = fitlm (X, y, 'full'); %! m2 = fitlm (X, y, 'interactions'); %! assert_equal (m.NumCoefficients, 4); %! assert_equal (m.Coefficients.Estimate, m2.Coefficients.Estimate, 1e-10); %! assert_equal (m.Coefficients.Estimate, ... %! [0.157640728038039; 2.08542680311791; -0.929682701072813; -0.031208018255475], 1e-7); %!test %! ## full modelspec with three predictors includes the three way interaction term %! X3 = [X, cos((1:n)' * pi / n)]; %! m = fitlm (X3, y, 'full'); %! assert_equal (m.NumCoefficients, 8); %! assert_equal (any (strcmp (m.CoefficientNames, 'x1:x2:x3')), true); %! assert_equal (m.SSE, 0.231331066631196, 1e-8); %! idx3 = find (strcmp (m.CoefficientNames, 'x1:x2:x3')); %! assert_equal (m.Coefficients.Estimate(idx3), 0.514890561912964, 1e-6); %!test %! ## full modelspec without an intercept drops the intercept coefficient %! m = fitlm (X, y, 'full', 'Intercept', false); %! assert_equal (m.NumCoefficients, 3); %! assert_equal (! any (strcmp (m.CoefficientNames, '(Intercept)')), true); %! assert_equal (m.Coefficients.Estimate, ... %! [3.232987312533958; -1.041484635851565; 0.0324190990982863], 1e-7); %!test %! ## a p column terms matrix produces a model with no intercept %! m = fitlm (X, y, [1 0; 0 1]); %! assert_equal (m.NumCoefficients, 2); %! assert_equal (! any (strcmp (m.CoefficientNames, '(Intercept)')), true); %! assert_equal (m.Coefficients.Estimate, [2.96142161317611; -0.997248749443286], 1e-7); %!test %! ## a p plus one column terms matrix produces a model with an intercept %! m = fitlm (X, y, [0 0 0; 1 0 0; 0 1 0]); %! assert_equal (m.NumCoefficients, 3); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (m.Coefficients.Estimate, ... %! [0.116188677790207; 2.50845149057086; -0.978835329825186], 1e-7); %!test %! ## a table with a Wilkinson formula fits the same model and predicts on a table %! T = table (X(:,1), X(:,2), y, 'VariableNames', {'a','b','resp'}); %! m = fitlm (T, 'resp ~ a + b'); %! assert_equal (m.NumCoefficients, 3); %! assert_equal (m.ResponseName, 'resp'); %! assert_equal (m.Coefficients.Estimate, mdl.Coefficients.Estimate, 1e-8); %! Xt = table ([0.5;1.0], [0.25;1.0], 'VariableNames', {'a','b'}); %! yp = predict (m, Xt); %! assert_equal (yp(1), 1.125705590619342, 1e-10); %! assert_equal (yp(2), 1.645804838535884, 1e-10); %!test %! ## a matrix with a Wilkinson formula string fits the same model as the matrix alone %! m = fitlm (X, y, 'y ~ x1 + x2'); %! assert_equal (m.NumCoefficients, 3); %! assert_equal (m.Coefficients.Estimate, mdl.Coefficients.Estimate, 1e-8); %!test %! ## a pure interaction formula keeps EncPredictorNames aligned with TermsMatrix %! mi = fitlm (X, y, 'y ~ x1:x2'); %! assert_equal (numel (mi.EncPredictorNames), columns (mi.TermsMatrix) - 1); %! assert_equal (mi.NumCoefficients, 2); %! assert_equal (mi.CoefficientNames, {'(Intercept)', 'x1:x2'}); %! assert_equal (mi.Coefficients.Estimate, ... %! [-0.755813941484483; -0.876953077491396], 1e-9); %! assert_equal (predict (mi), mi.Fitted, 1e-10); %! fig = figure ('visible', 'off'); %! h = plot (mi); %! assert_equal (numel (h), 3); %! assert_equal (get (get (gca, 'Title'), 'String'), 'Added variable plot for x1:x2'); %! assert_equal (get (get (gca, 'XLabel'), 'String'), 'Adjusted x1:x2'); %! assert_equal (get (h(2), 'DisplayName'), 'Fit: y = -0.876953*x'); %! close (fig); %!test %! ## a table input with the default formula fits the same model as the matrix %! T3 = table (X(:,1), X(:,2), y, 'VariableNames', {'x1','x2','y'}); %! m = fitlm (T3); %! assert_equal (m.ResponseName, 'y'); %! assert_equal (m.Coefficients.Estimate, mdl.Coefficients.Estimate, 1e-8); %!test %! ## VarNames sets custom names and ResponseVar overrides the response name %! m = fitlm (X, y, 'VarNames', {'alpha','beta','resp'}); %! assert_equal (m.ResponseName, 'resp'); %! assert_equal (isequal (m.PredictorNames, {'alpha';'beta'}), true); %! assert_equal (any (strcmp (m.CoefficientNames, 'alpha')), true); %! assert_equal (any (strcmp (m.CoefficientNames, 'beta')), true); %! m2 = fitlm (X, y, 'VarNames', {'a','b','r'}, 'ResponseVar', 'r'); %! assert_equal (m2.ResponseName, 'r'); %!test %! ## a rank deficient design matrix leaves the dropped coefficients as NaN across the board %! X_rd = [ones(n,1), X, X(:,1)+X(:,2)]; %! m = fitlm (X_rd, y); %! assert_equal (m.NumCoefficients, 5); %! assert_equal (m.NumEstimatedCoefficients, 3); %! drop = find (m.Coefficients.SE == 0); %! assert_equal (numel (drop), 2); %! assert_equal (all (isnan (m.Coefficients.tStat(drop))), true); %! assert_equal (all (isnan (m.Coefficients.pValue(drop))), true); %! assert_equal (m.SST, 5.839104200023459e+02, 1e-8); %! assert_equal (all (all (m.CoefficientCovariance(drop,:) == 0)), true); %! yp = predict (m, X_rd); %! assert_equal (size (yp), [n, 1]); %! assert_equal (! any (isnan (yp)), true); %! assert_equal (size (m.Diagnostics.Dfbetas), [20, 5]); %! assert_equal (all (isnan (m.Diagnostics.Dfbetas(:, drop)(:))), true); %! assert_equal (m.Diagnostics.Leverage(1), 0.370779220779221, 1e-8); %!test %! ## Intercept=false %! mni = fitlm (X, y, 'Intercept', false); %! assert_equal (mni.NumCoefficients, 2); %! assert_equal (mni.Formula.HasIntercept, false); %! assert_equal (! any (strcmp (mni.CoefficientNames, '(Intercept)')), true); %! [yp, yci] = predict (mni, [0.5 0.25; 1.0 1.0]); %! assert_equal (yp(1), 1.231398619227234, 1e-10); %! assert_equal (yp(2), 1.964172863732825, 1e-10); %! assert_equal (yci(1,1), 1.001262470857215, 1e-10); %!test %! ## p-column terms matrix %! m_p = fitlm (X, y, [1 0; 0 1]); %! assert_equal (m_p.NumCoefficients, 2); %! assert_equal (! any (strcmp (m_p.CoefficientNames, '(Intercept)')), true); %!test %! ## p+1 column terms matrix %! m_p1 = fitlm (X, y, [0 0 0; 1 0 0; 0 1 0]); %! assert_equal (m_p1.NumCoefficients, 3); %! assert_equal (m_p1.CoefficientNames{1}, '(Intercept)'); %!test %! ## table Wilkinson formula %! T = table (X(:,1), X(:,2), y, 'VariableNames', {'a','b','resp'}); %! mf = fitlm (T, 'resp ~ a + b'); %! assert_equal (mf.NumCoefficients, 3); %! assert_equal (mf.ResponseName, 'resp'); %! assert_equal (mf.Coefficients.Estimate, mdl.Coefficients.Estimate, 1e-8); %! Xt = table ([0.5;1.0], [0.25;1.0], 'VariableNames', {'a','b'}); %! yp = predict (mf, Xt); %! assert_equal (yp(1), 1.125705590619342, 1e-10); %! assert_equal (yp(2), 1.645804838535884, 1e-10); %!test %! ## matrix Wilkinson formula %! mfm = fitlm (X, y, 'y ~ x1 + x2'); %! assert_equal (mfm.NumCoefficients, 3); %! assert_equal (mfm.Coefficients.Estimate, mdl.Coefficients.Estimate, 1e-8); %!test %! ## table default %! T3 = table (X(:,1), X(:,2), y, 'VariableNames', {'x1','x2','y'}); %! mt = fitlm (T3); %! assert_equal (mt.ResponseName, 'y'); %! assert_equal (mt.Coefficients.Estimate, mdl.Coefficients.Estimate, 1e-8); %!test %! ## VarNames sets custom names %! vn = fitlm (X, y, 'VarNames', {'alpha','beta','resp'}); %! assert_equal (vn.ResponseName, 'resp'); %! assert_equal (isequal (vn.PredictorNames, {'alpha';'beta'}), true); %! assert_equal (any (strcmp (vn.CoefficientNames, 'alpha')), true); %! assert_equal (any (strcmp (vn.CoefficientNames, 'beta')), true); %!test %! ## ResponseVar overrides VarNames %! rv = fitlm (X, y, 'VarNames', {'a','b','r'}, 'ResponseVar', 'r'); %! assert_equal (rv.ResponseName, 'r'); %!test %! ## rank-deficient matrix %! X_rd = [ones(n,1), X, X(:,1)+X(:,2)]; %! m_rd = fitlm (X_rd, y); %! assert_equal (m_rd.NumCoefficients, 5); %! assert_equal (m_rd.NumEstimatedCoefficients, 3); %! drop = find (m_rd.Coefficients.SE == 0); %! assert_equal (numel (drop), 2); %! assert_equal (all (isnan (m_rd.Coefficients.tStat(drop))), true); %! assert_equal (all (isnan (m_rd.Coefficients.pValue(drop))), true); %! assert_equal (m_rd.SST, 5.839104200023459e+02, 1e-8); %! assert_equal (all (all (m_rd.CoefficientCovariance(drop,:) == 0)), true); %! yp = predict (m_rd, X_rd); %! assert_equal (size (yp), [n, 1]); %! assert_equal (! any (isnan (yp)), true); %! assert_equal (yp(1:5), [0.192669485827486; 0.171266760882252; ... %! 0.0519805029545; -0.165189287955771; ... %! -0.480242611848561], 1e-10); %!test %! ## predict: ypred and default CI at new points %! [yp, yci] = predict (mdl, [0.5 0.25; 1.0 1.0]); %! assert_equal (yp(1), 1.125705590619347, 1e-10); %! assert_equal (yp(2), 1.645804838535894, 1e-10); %! assert_equal (yci(1,1), 0.810180780547215, 1e-10); %! assert_equal (yci(1,2), 1.441230400691478, 1e-10); %! assert_equal (yci(2,1), 0.858229321851723, 1e-10); %! assert_equal (yci(2,2), 2.433380355220066, 1e-10); %!test %! ## predict: observation interval %! [~, yci] = predict (mdl, [0.5 0.25; 1.0 1.0], 'Prediction', 'observation'); %! assert_equal (yci(1,1), 0.677632064105988, 1e-10); %! assert_equal (yci(1,2), 1.573779117132706, 1e-10); %!test %! ## predict: alpha 0.01 %! [~, yci] = predict (mdl, [0.5 0.25; 1.0 1.0], 'Alpha', 0.01); %! assert_equal (yci(1,1), 0.692272619570008, 1e-10); %! assert_equal (yci(1,2), 1.559138561668685, 1e-10); %!test %! ## predict: simultaneous CI %! [~, yci] = predict (mdl, [0.5 0.25; 1.0 1.0], 'Simultaneous', true); %! assert_equal (yci(1,1), 0.662572505689338, 1e-10); %! assert_equal (yci(1,2), 1.588838675549355, 1e-10); %!test %! ## predict: no Xnew returns all rows including training %! [yp, yci] = predict (mdl); %! assert_equal (size (yp), [20, 1]); %! assert_equal (size (yci), [20, 2]); %! assert_equal (yp(1), 0.192669485827490, 1e-10); %! assert_equal (yp(2), 0.171266760882255, 1e-10); %! assert_equal (yci(1,1), -0.001052067982566, 1e-10); %! assert_equal (yci(1,2), 0.386391039637546, 1e-10); %!test %! ## predict: NaN predictor propagates to NaN output and CI %! [yp, yci] = predict (mdl, [0.5 0.25; NaN 1.0; 1.0 1.0]); %! assert_equal (yp(1), 1.125705590619347, 1e-10); %! assert_equal (isnan (yp(2)), true); %! assert_equal (yp(3), 1.645804838535894, 1e-10); %! assert_equal (isnan (yci(2,1)), true); %! assert_equal (isnan (yci(2,2)), true); %!test %! ## predict: categorical model predictions at group centres %! Xc = [1;1;1;2;2;2;3;3;3]; %! yc = [2.1;2.3;1.9; 4.1;3.9;4.2; 6.3;5.8;6.1]; %! m_cat = fitlm (Xc, yc, 'linear', 'CategoricalVars', 1); %! [yp, yci] = predict (m_cat, [1;2;3]); %! assert_equal (yp(1), 2.099999999999998, 1e-10); %! assert_equal (yp(2), 4.066666666666667, 1e-10); %! assert_equal (yp(3), 6.066666666666666, 1e-10); %! assert_equal (yci(1,1), 1.80971256321669, 1e-10); %! assert_equal (yci(1,2), 2.3902874367833, 1e-10); %! assert_equal (yci(2,1), 3.77637922988336, 1e-10); %! assert_equal (yci(2,2), 4.35695410344997, 1e-10); %! assert_equal (yci(3,1), 5.77637922988336, 1e-10); %! assert_equal (yci(3,2), 6.35695410344997, 1e-10); %!test %! ## predict: interaction model %! [yp, yci] = predict (fitlm (X, y, 'interactions'), [0.5 0.25; 1.0 1.0]); %! assert_equal (yp(1), 0.964032452046850, 1e-10); %! assert_equal (yp(2), 1.282176811827644, 1e-10); %! assert_equal (yci(1,1), -0.110763003580605, 1e-10); %! assert_equal (yci(1,2), 2.038827907674306, 1e-10); %!test %! ## predict: weighted model, ypred and CI %! w = (1:n)' / sum (1:n); %! mw = fitlm (X, y, 'Weights', w); %! [yp, yci] = predict (mw, [0.5 0.25; 1.0 1.0]); %! assert_equal (yp(1), 1.15833357370544, 1e-10); %! assert_equal (yp(2), 1.74408669002694, 1e-10); %! assert_equal (yci(1,1), 0.802165170771357, 1e-10); %! assert_equal (yci(1,2), 1.51450197663953, 1e-10); %! assert_equal (yci(2,1), 0.69968979253134, 1e-10); %! assert_equal (yci(2,2), 2.78848358752254, 1e-10); %!test %! ## predict: no-intercept model, ypred and CI %! mni = fitlm (X, y, 'Intercept', false); %! [yp, yci] = predict (mni, [0.5 0.25; 1.0 1.0]); %! assert_equal (yp(1), 1.23139861922723, 1e-10); %! assert_equal (yp(2), 1.96417286373283, 1e-10); %! assert_equal (yci(1,1), 1.00126247085704, 1e-10); %! assert_equal (yci(1,2), 1.46153476759743, 1e-10); %! assert_equal (yci(2,1), 1.51833851162232, 1e-10); %! assert_equal (yci(2,2), 2.41000721584333, 1e-10); %!test %! ## predict: observation interval combined with simultaneous bound %! [~, yci] = predict (mdl, [0.5 0.25; 1.0 1.0], ... %! 'Prediction', 'observation', 'Simultaneous', true); %! assert_equal (yci(1,1), 0.46801507632267, 1e-10); %! assert_equal (yci(1,2), 1.78339610491601, 1e-10); %! assert_equal (yci(2,1), 0.399032373599106, 1e-10); %! assert_equal (yci(2,2), 2.89257730347266, 1e-10); %!test %! ## output is 2x1 double column vector %! ysim = random (mdl, [0.5, 0.25; 1.0, 1.0]); %! assert_equal (size (ysim), [2, 1]); %! assert_equal (class (ysim), 'double'); %! assert_equal (iscolumn (ysim), true); %!test %! ## single row input gives 1x1 output %! assert_equal (size (random (mdl, [0.5, 0.25])), [1, 1]); %!test %! ## predict values are exact and noise added is finite %! ypred = predict (mdl, [0.5, 0.25; 1.0, 1.0]); %! ysim = random (mdl, [0.5, 0.25; 1.0, 1.0]); %! assert_equal (ypred(1), 1.125705590619342, 1e-10); %! assert_equal (ypred(2), 1.645804838535884, 1e-10); %! assert_equal (all (isfinite (ysim - ypred)), true); %!test %! ## NaN predictor row gives NaN output, other rows stay finite %! ysim = random (mdl, [0.5, 0.25; NaN, 1.0; 1.0, 1.0]); %! assert_equal (size (ysim), [3, 1]); %! assert_equal (isfinite (ysim(1)), true); %! assert_equal (isnan (ysim(2)), true); %! assert_equal (isfinite (ysim(3)), true); %!test %! ## two sequential calls produce different output %! ya = random (mdl, [0.5, 0.25]); %! yb = random (mdl, [0.5, 0.25]); %! assert_equal (! isequal (ya, yb), true); %!test %! ## random: table input, full training data, weighted and no-intercept %! ## models all give finite output of the expected size %! Xt = table ([0.5;1.0], [0.25;1.0], 'VariableNames', {'x1','x2'}); %! mw = fitlm (X, y, 'Weights', (1:n)' / sum (1:n)); %! mni = fitlm (X, y, 'Intercept', false); %! assert_equal (size (random (mdl, Xt)), [2, 1]); %! assert_equal (all (isfinite (random (mdl, Xt))), true); %! assert_equal (size (random (mdl, X)), [20, 1]); %! assert_equal (sum (isnan (random (mdl, X))), 0); %! assert_equal (all (isfinite (random (mw, [0.5, 0.25; 1.0, 1.0]))), true); %! assert_equal (all (isfinite (random (mni, [0.5, 0.25; 1.0, 1.0]))), true); %!test %! yf = feval (mdl, [0.5 0.25; 1.0 1.0; 0.2 0.04]); %! assert_equal (size (yf), [3, 1]); %! assert_equal (class (yf), 'double'); %! assert_equal (yf(1), 1.125705590619342, 1e-10); %! assert_equal (yf(2), 1.645804838535884, 1e-10); %! assert_equal (yf(3), 0.578725562711373, 1e-10); %! assert_equal (yf, predict (mdl, [0.5 0.25; 1.0 1.0; 0.2 0.04]), 1e-10); %!test %! yf = feval (mdl, [0.5; 1.0; 0.2], [0.25; 1.0; 0.04]); %! assert_equal (size (yf), [3, 1]); %! assert_equal (iscolumn (yf), true); %! assert_equal (yf, predict (mdl, [0.5 0.25; 1.0 1.0; 0.2 0.04]), 1e-10); %!test %! yf = feval (mdl, [0.5, 1.0, 0.2], [0.25, 1.0, 0.04]); %! assert_equal (size (yf), [1, 3]); %! assert_equal (isrow (yf), true); %! assert_equal (yf(:), predict (mdl, [0.5 0.25; 1.0 1.0; 0.2 0.04]), 1e-10); %!test %! yf = feval (mdl, 0.5, 0.25); %! assert_equal (size (yf), [1, 1]); %! assert_equal (yf, 1.125705590619342, 1e-10); %! assert_equal (yf, predict (mdl, [0.5 0.25]), 1e-10); %!test %! yf = feval (mdl, 0.5, [0.1; 0.2; 0.3]); %! assert_equal (size (yf), [3, 1]); %! assert_equal (yf(1), 1.272530890093120, 1e-10); %! assert_equal (yf(2), 1.174647357110602, 1e-10); %! assert_equal (yf(3), 1.076763824128083, 1e-10); %! assert_equal (yf, predict (mdl, [0.5 0.1; 0.5 0.2; 0.5 0.3]), 1e-10); %!test %! yf = feval (mdl, [0.1; 0.5; 0.9], 0.25); %! assert_equal (size (yf), [3, 1]); %! assert_equal (yf(1), 0.122324994390997, 1e-10); %! assert_equal (yf(2), 1.125705590619342, 1e-10); %! assert_equal (yf(3), 2.129086186847688, 1e-10); %! assert_equal (yf, predict (mdl, [0.1 0.25; 0.5 0.25; 0.9 0.25]), 1e-10); %!test %! Weight = [2000;2100;2200;2300;2400;2500;2600;2700;2800;2900;3000; ... %! 3100;3200;3300;3400;3500;3600;3700;3800;3900]; %! Year = categorical ([70;70;70;70;70;76;76;76;76;76;76;76;82;82; ... %! 82;82;82;82;82;82]); %! MPG = [30;29;28;27;26;25;24;23;22;21;20;19;18;17;16;15;14;13;12;11]; %! m = fitlm (table (MPG, Weight, Year), 'MPG ~ Weight + Year'); %! yf = feval (m, [2500;3000], '76'); %! assert_equal (yf(1), 25.000000000000000, 1e-9); %! assert_equal (yf(2), 20.000000000000004, 1e-9); %! yf2 = feval (m, [2500;3000], categorical (70)); %! assert_equal (yf2(1), 24.999999999999996, 1e-9); %! assert_equal (yf2(2), 20.000000000000000, 1e-9); %! assert_equal (feval (m, 2800, '82'), 21.999999999999996, 1e-9); %! assert_equal (isnan (feval (m, 2500, '99')), true); %!test %! m = fitlm ((1:n)' / n, 2 * (1:n)' / n + 0.1 * sin ((1:n)')); %! assert_equal (size (feval (m, 0.5)), [1, 1]); %! assert_equal (size (feval (m, [0.3; 0.5; 0.9])), [3, 1]); %! assert_equal (feval (m, 0.5), predict (m, 0.5), 1e-10); %! assert_equal (feval (m, [0.3; 0.5; 0.9]), predict (m, [0.3; 0.5; 0.9]), 1e-10); %!test %! T = table ([0.5; 1.0; 0.2], [0.25; 1.0; 0.04], 'VariableNames', {'x1', 'x2'}); %! yf = feval (mdl, T); %! assert_equal (size (yf), [3, 1]); %! assert_equal (yf, predict (mdl, [0.5 0.25; 1.0 1.0; 0.2 0.04]), 1e-10); %!test %! yf = feval (mdl, [0.5 0.25; NaN 1.0; 1.0 1.0]); %! assert_equal (isfinite (yf(1)), true); %! assert_equal (isnan (yf(2)), true); %! assert_equal (isfinite (yf(3)), true); %!test %! yf = feval (mdl, [0.5; NaN; 1.0], [0.25; 1.0; 1.0]); %! assert_equal (isnan (yf(2)), true); %! yf = feval (mdl, [0.5; 1.0; 1.0], [0.25; NaN; 1.0]); %! assert_equal (isnan (yf(2)), true); %!test %! yf = feval (mdl, X); %! assert_equal (size (yf), [20, 1]); %! assert_equal (yf, mdl.Fitted, 1e-10); %!test %! m = fitlm (X, y, 'Intercept', false); %! yf = feval (m, [0.5 0.25; 1.0 1.0]); %! assert_equal (yf, predict (m, [0.5 0.25; 1.0 1.0]), 1e-10); %! assert_equal (feval (m, [0.5; 1.0], [0.25; 1.0]), yf, 1e-10); %!test %! m = fitlm (X, y, 'interactions'); %! yf = feval (m, [0.5 0.25; 1.0 1.0]); %! assert_equal (yf, predict (m, [0.5 0.25; 1.0 1.0]), 1e-10); %! assert_equal (feval (m, [0.5; 1.0], [0.25; 1.0]), yf, 1e-10); %!test %! m = fitlm ([1;1;1;2;2;2;3;3;3], [2.1;2.3;1.9;4.1;3.9;4.2;6.3;5.8;6.1], ... %! 'linear', 'CategoricalVars', 1); %! yf = feval (m, [1; 2; 3]); %! assert_equal (yf(1), 2.099999999999998, 1e-10); %! assert_equal (yf(2), 4.066666666666667, 1e-10); %! assert_equal (yf(3), 6.066666666666666, 1e-10); %!test %! ci = coefCI (mdl); %! assert_equal (size (ci), [3, 2]); %! assert_equal (class (ci), 'double'); %! assert_equal (all (ci(:,1) < ci(:,2)), true); %! assert_equal (ci(1,1), -0.120502736154050, 1e-10); %! assert_equal (ci(1,2), 0.352880091734465, 1e-10); %! assert_equal (ci(2,1), 1.470249604061007, 1e-10); %! assert_equal (ci(2,2), 3.546653377080718, 1e-10); %! assert_equal (ci(3,1), -1.026857022014626, 1e-10); %! assert_equal (ci(3,2), -0.930813637635746, 1e-10); %!test %! ## midpoints equal estimates %! ci = coefCI (mdl); %! t = tinv (0.975, mdl.DFE); %! assert_equal ((ci(:,1) + ci(:,2)) / 2, mdl.Coefficients.Estimate, 1e-10); %! assert_equal (ci(:,2) - ci(:,1), 2 * t * mdl.Coefficients.SE, 1e-10); %!test %! assert_equal (coefCI (mdl, 0.05), coefCI (mdl)); %!test %! ci = coefCI (mdl); %! ci01 = coefCI (mdl, 0.01); %! t01 = tinv (0.995, mdl.DFE); %! assert_equal (size (ci01), [3, 2]); %! assert_equal (ci01(1,1), -0.208951721610638, 1e-10); %! assert_equal (ci01(1,2), 0.441329077191052, 1e-10); %! assert_equal (ci01(2,1), 1.08228494564489, 1e-10); %! assert_equal (ci01(2,2), 3.934618035496833, 1e-10); %! assert_equal (ci01(3,1), -1.044802201703589, 1e-10); %! assert_equal (ci01(3,2), -0.912868457946783, 1e-10); %! assert_equal (all ((ci01(:,2) - ci01(:,1)) > (ci(:,2) - ci(:,1))), true); %! assert_equal (ci01(:,2) - ci01(:,1), 2 * t01 * mdl.Coefficients.SE, 1e-10); %!test %! ci0 = coefCI (mdl, 0); %! assert_equal (all (ci0(:,1) == -Inf), true); %! assert_equal (all (ci0(:,2) == +Inf), true); %!test %! ## alpha=1 collapses to point estimates %! ci1 = coefCI (mdl, 1); %! assert_equal (ci1(:,1), mdl.Coefficients.Estimate, 1e-10); %! assert_equal (ci1(:,2), mdl.Coefficients.Estimate, 1e-10); %!test %! m = fitlm (X, y, 'Intercept', false); %! ci = coefCI (m); %! t = tinv (0.975, m.DFE); %! assert_equal (size (ci), [2, 2]); %! assert_equal (ci(1,1), 2.486679110991696, 1e-10); %! assert_equal (ci(1,2), 3.436164115360526, 1e-10); %! assert_equal (ci(2,1), -1.027166590567854, 1e-10); %! assert_equal (ci(2,2), -0.967330908318718, 1e-10); %! assert_equal ((ci(:,1) + ci(:,2)) / 2, m.Coefficients.Estimate, 1e-10); %! assert_equal (ci(:,2) - ci(:,1), 2 * t * m.Coefficients.SE, 1e-10); %!test %! m = fitlm (X, y, 'interactions'); %! ci = coefCI (m); %! t = tinv (0.975, m.DFE); %! assert_equal (size (ci), [4, 2]); %! assert_equal (ci(1,1), -0.201030907566802, 1e-10); %! assert_equal (ci(1,2), 0.516312363642881, 1e-10); %! assert_equal ((ci(:,1) + ci(:,2)) / 2, m.Coefficients.Estimate, 1e-10); %! assert_equal (ci(:,2) - ci(:,1), 2 * t * m.Coefficients.SE, 1e-10); %!test %! ## constant model (1 coefficient) %! m = fitlm (X, y, 'constant'); %! ci = coefCI (m); %! t = tinv (0.975, m.DFE); %! assert_equal (size (ci), [1, 2]); %! assert_equal ((ci(1,1) + ci(1,2)) / 2, m.Coefficients.Estimate, 1e-10); %! assert_equal (ci(1,2) - ci(1,1), 2 * t * m.Coefficients.SE, 1e-10); %!test %! m = fitlm (X, y, 'Weights', (1:n)' / sum (1:n)); %! ci = coefCI (m); %! t = tinv (0.975, m.DFE); %! assert_equal (size (ci), [3, 2]); %! assert_equal (ci(1,1), -0.355978167660141, 1e-10); %! assert_equal (ci(1,2), 0.516619434992026, 1e-10); %! assert_equal ((ci(:,1) + ci(:,2)) / 2, m.Coefficients.Estimate, 1e-10); %! assert_equal (ci(:,2) - ci(:,1), 2 * t * m.Coefficients.SE, 1e-10); %!test %! ## rank-deficient: dropped rows give [0,0], active rows are finite %! m = fitlm ([ones(n,1), X, X(:,1)+X(:,2)], y); %! ci = coefCI (m); %! drop = find (m.Coefficients.SE == 0); %! assert_equal (size (ci), [5, 2]); %! assert_equal (all (all (ci(drop, :) == 0)), true); %! assert_equal (all (all (isfinite (ci(setdiff (1:5, drop'), :)))), true); %!test %! m = fitlm ([1;1;1;2;2;2;3;3;3], [2.1;2.3;1.9;4.1;3.9;4.2;6.3;5.8;6.1], ... %! 'linear', 'CategoricalVars', 1); %! ci = coefCI (m); %! assert_equal (size (ci), [3, 2]); %! assert_equal (ci(1,1), 1.80971256321669, 1e-10); %! assert_equal (ci(1,2), 2.3902874367833, 1e-10); %! assert_equal (ci(2,1), 1.55613823658119, 1e-10); %! assert_equal (ci(2,2), 2.37719509675214, 1e-10); %! assert_equal (ci(3,1), 3.55613823658119, 1e-10); %! assert_equal (ci(3,2), 4.37719509675214, 1e-10); %!test %! [p, F, r] = coefTest (mdl); %! assert_equal (size (p), [1, 1]); %! assert_equal (class (p), 'double'); %! assert_equal (p >= 0 && p <= 1, true); %! assert_equal (F >= 0, true); %! assert_equal (p, 9.489880832170599e-28, -1e-8); %! assert_equal (F, 1.283149098426142e+04, -1e-8); %! assert_equal (r, 2); %!test %! ## formula identity %! [p, F] = coefTest (mdl); %! k = mdl.NumCoefficients; %! H0 = [zeros(k-1, 1), eye(k-1)]; %! b = mdl.Coefficients.Estimate; %! V = mdl.CoefficientCovariance; %! Hb = H0 * b; %! Fm = (Hb' * ((H0 * V * H0') \ Hb)) / (k - 1); %! pm = betainc (mdl.DFE / (mdl.DFE + (k-1) * Fm), mdl.DFE/2, (k-1)/2); %! assert_equal (F, Fm, -1e-10); %! assert_equal (p, pm, -1e-10); %!test %! ## explicit H matches default %! k = mdl.NumCoefficients; %! H_exp = [zeros(k-1, 1), eye(k-1)]; %! [p1, F1, r1] = coefTest (mdl); %! [p2, F2, r2] = coefTest (mdl, H_exp); %! assert_equal (p2, p1, -1e-10); %! assert_equal (F2, F1, -1e-10); %! assert_equal (r2, size (H_exp, 1)); %!test %! ## pinned single and joint H %! [p1, F1, r1] = coefTest (mdl, [1 0 0]); %! assert_equal (p1, 0.314859866747774, -1e-8); %! assert_equal (F1, 1.072634101844537, -1e-8); %! assert_equal (r1, 1); %! [p2, F2, r2] = coefTest (mdl, [0 1 0]); %! assert_equal (p2, 8.937794169018252e-05, -1e-8); %! assert_equal (F2, 25.985840929474932, -1e-8); %! assert_equal (r2, 1); %! [p3, F3, r3] = coefTest (mdl, [0 0 1]); %! assert_equal (p3, 8.656938305821102e-19, -1e-8); %! assert_equal (F3, 1.849410599855684e+03, -1e-8); %! assert_equal (r3, 1); %! [pm, Fm, rm] = coefTest (mdl, [0 1 0; 0 0 1]); %! assert_equal (pm, 9.489880832170599e-28, -1e-8); %! assert_equal (Fm, 1.283149098426142e+04, -1e-8); %! assert_equal (rm, 2); %!test %! ## trivial hypothesis and C=0 %! b = mdl.Coefficients.Estimate; %! [p0, F0] = coefTest (mdl, [0 1 0], b(2)); %! assert_equal (F0 < 1e-12, true); %! assert_equal (p0, 1, 1e-10); %! [pa, Fa] = coefTest (mdl, [0 1 0], 0); %! [pb, Fb] = coefTest (mdl, [0 1 0]); %! assert_equal (pa, pb, -1e-10); %! assert_equal (Fa, Fb, -1e-10); %!test %! ## H with C %! [pc, Fc, rc] = coefTest (mdl, [0 1 0; 0 0 1], [1.5; -1.0]); %! assert_equal (pc, 2.833788304242915e-09, -1e-8); %! assert_equal (Fc, 77.603887650386312, -1e-8); %! assert_equal (rc, 2); %! [pr, Fr] = coefTest (mdl, [0 1 0; 0 0 1], [1.5, -1.0]); %! assert_equal (pr, pc, -1e-10); %! assert_equal (Fr, Fc, -1e-10); %! [ps, Fs] = coefTest (mdl, [0 1 0], 1.5); %! assert_equal (ps, 0.056184159363707, -1e-8); %! assert_equal (Fs, 4.199865537706047, -1e-8); %!test %! ## no-intercept model %! m = fitlm (X, y, 'Intercept', false); %! [p, F, r] = coefTest (m); %! assert_equal (p, 6.060655830723051e-32, -1e-8); %! assert_equal (F, 2.646694317541346e+04, -1e-8); %! assert_equal (r, m.NumCoefficients); %! [p2, F2] = coefTest (m, eye (m.NumCoefficients)); %! assert_equal (p2, p, -1e-10); %! assert_equal (F2, F, -1e-10); %!test %! ## interaction model %! m = fitlm (X, y, 'interactions'); %! [p, F, r] = coefTest (m); %! assert_equal (p, 1.164196605688161e-25, -1e-8); %! assert_equal (F, 8.107508574885546e+03, -1e-8); %! assert_equal (r, m.NumCoefficients - 1); %! assert_equal (r != m.NumPredictors, true); %!test %! ## weighted model %! m = fitlm (X, y, 'Weights', (1:n)' / sum (1:n)); %! [p, F, r] = coefTest (m); %! assert_equal (p, 1.481920976389473e-27, -1e-8); %! assert_equal (F, 1.217557180481257e+04, -1e-8); %! assert_equal (r, 2); %! assert_equal (p, m.ModelFitVsNullModel.Pvalue, -1e-8); %!test %! ## categorical model %! m = fitlm ([1;1;1;2;2;2;3;3;3], [2.1;2.3;1.9;4.1;3.9;4.2;6.3;5.8;6.1], ... %! 'linear', 'CategoricalVars', 1); %! [p, F, r] = coefTest (m); %! assert_equal (p, 1.197590680415813e-06, -1e-8); %! assert_equal (F, 2.795000000000035e+02, -1e-8); %! assert_equal (r, 2); %! [p1, F1] = coefTest (m, [1 0 0]); %! assert_equal (F1, 3.133421052631613e+02, -1e-8); %! assert_equal (p1, 2.087464608380450e-06, -1e-8); %! [p2, F2] = coefTest (m, [0 1 0]); %! assert_equal (F2, 1.374078947368438e+02, -1e-8); %! assert_equal (p2, 2.325514143662469e-05, -1e-8); %! [p3, F3] = coefTest (m, [0 0 1]); %! assert_equal (F3, 5.589868421052698e+02, -1e-8); %! assert_equal (p3, 3.757733067786492e-07, -1e-8); %!test %! ## constant model %! m = fitlm (X, y, 'constant'); %! [p, F, r] = coefTest (m); %! assert_equal (p, 0.000239936408695073, -1e-8); %! assert_equal (F, 20.3359164947506, -1e-8); %! assert_equal (r, 1); %!test %! ## rank-deficient model %! m = fitlm ([ones(n,1), X, X(:,1)+X(:,2)], y); %! [p, F] = coefTest (m); %! assert_equal (isnan (p), true); %! assert_equal (isnan (F), true); %! drop = find (m.Coefficients.SE == 0); %! keep = setdiff (2:m.NumCoefficients, drop'); %! H = zeros (numel (keep), m.NumCoefficients); %! for i = 1:numel (keep) %! H(i, keep(i)) = 1; %! endfor %! [p2, F2, r2] = coefTest (m, H); %! assert_equal (p2, 6.70657058643085e-30, -1e-8); %! assert_equal (F2, 17716.1864263456, -1e-8); %! assert_equal (r2, numel (keep)); %!test %! p = dwtest (mdl); %! assert_equal (size (p), [1, 1]); %! assert_equal (class (p), 'double'); %! [p, DW] = dwtest (mdl); %! assert_equal (size (DW), [1, 1]); %! assert_equal (p >= 0 && p <= 1, true); %! assert_equal (DW >= 0 && DW <= 4, true); %! assert_equal (p, 4.702593821571290e-04, -1e-6); %! assert_equal (DW, 0.870000704251173, 1e-12); %!test %! [p1, DW1] = dwtest (mdl); %! [p2, DW2] = dwtest (mdl, 'exact', 'both'); %! assert_equal (p1, p2, 1e-14); %! assert_equal (DW1, DW2, 1e-14); %!test %! ## DW is the same for all method and tail options %! [~, d1] = dwtest (mdl, 'exact', 'both'); %! [~, d2] = dwtest (mdl, 'exact', 'right'); %! [~, d3] = dwtest (mdl, 'exact', 'left'); %! [~, d4] = dwtest (mdl, 'approximate', 'both'); %! [~, d5] = dwtest (mdl, 'approximate', 'right'); %! [~, d6] = dwtest (mdl, 'approximate', 'left'); %! assert_equal (d1, 0.870000704251173, 1e-12); %! assert_equal (d2, 0.870000704251173, 1e-12); %! assert_equal (d3, 0.870000704251173, 1e-12); %! assert_equal (d4, 0.870000704251173, 1e-12); %! assert_equal (d5, 0.870000704251173, 1e-12); %! assert_equal (d6, 0.870000704251173, 1e-12); %!test %! ## one-sided p-values sum to 1 and two-sided equals twice the smaller %! pb = dwtest (mdl, 'exact', 'both'); %! pr = dwtest (mdl, 'exact', 'right'); %! pl = dwtest (mdl, 'exact', 'left'); %! assert_equal (pr + pl, 1, 1e-12); %! assert_equal (pb, 4.702593821571290e-04, 1e-12); %!test %! ## all six method and tail combinations pinned %! assert_equal (dwtest (mdl, 'exact', 'both'), 4.702593821571290e-04, -1e-6); %! assert_equal (dwtest (mdl, 'exact', 'right'), 2.351296910785645e-04, -1e-6); %! assert_equal (dwtest (mdl, 'exact', 'left'), 0.999764870308921, -1e-6); %! assert_equal (dwtest (mdl, 'approximate', 'both'), 0.001058795514879, -1e-6); %! assert_equal (dwtest (mdl, 'approximate', 'right'), 5.293977574395035e-04, -1e-6); %! assert_equal (dwtest (mdl, 'approximate', 'left'), 0.999470602242560, -1e-6); %!test %! ## no-intercept model %! m = fitlm (X, y, 'Intercept', false); %! [p, DW] = dwtest (m, 'exact', 'both'); %! assert_equal (DW, 0.841468411374128, 1e-12); %! assert_equal (p, 0.001402191159200, -1e-6); %! assert_equal (dwtest (m, 'exact', 'right'), 7.010955795999754e-04, -1e-6); %! assert_equal (dwtest (m, 'approximate', 'right'), 0.001350534002321, -1e-6); %!test %! ## weighted model %! m = fitlm (X, y, 'Weights', (1:n)' / sum (1:n)); %! [p, DW] = dwtest (m, 'exact', 'both'); %! assert_equal (DW, 0.871162354803032, 1e-12); %! assert_equal (p, 4.771641146603785e-04, -1e-6); %! assert_equal (dwtest (m, 'exact', 'right'), 2.385820573301892e-04, -1e-6); %! assert_equal (dwtest (m, 'approximate', 'right'), 5.346779629058873e-04, -1e-6); %!test %! ## positive autocorrelation model %! m = fitlm ((1:n)'/n, sin (pi * (1:n)'/n)); %! [~, DW] = dwtest (m, 'exact', 'both'); %! pr = dwtest (m, 'exact', 'right'); %! pl = dwtest (m, 'exact', 'left'); %! assert_equal (DW, 0.118112272685229, 1e-10); %! assert_equal (DW < 1, true); %! assert_equal (pr < pl, true); %! assert_equal (pr < 1e-10, true); %!test %! ## negative autocorrelation model %! m = fitlm ((1:n)'/n, 2*(1:n)'/n + repmat ([1; -1], n/2, 1)); %! [pb, DW] = dwtest (m, 'exact', 'both'); %! pl = dwtest (m, 'exact', 'left'); %! pr = dwtest (m, 'exact', 'right'); %! assert_equal (pb, 4.205713999283489e-09, 1e-10); %! assert_equal (DW, 3.825974025974026, 1e-10); %! assert_equal (DW > 2, true); %! assert_equal (pl < pr, true); %! assert_equal (pb, 2 * pl, 1e-10); %! assert_equal (pb < 1e-7, true); %!test %! m = addTerms (mdl, 'x1:x2'); %! assert_equal (isa (m, 'LinearModel'), true); %! assert_equal (mdl.NumCoefficients, 3); %! assert_equal (m.NumCoefficients, 4); %! assert_equal (m.NumPredictors, 2); %! assert_equal (m.NumObservations, 20); %! assert_equal (m.DFE, 16); %! assert_equal (m.Coefficients.Estimate(1), 0.157640728038039, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), 2.085426803117909, -1e-8); %! assert_equal (m.Coefficients.Estimate(3), -0.929682701072813, -1e-8); %! assert_equal (m.Coefficients.Estimate(4), -0.031208018255475, -1e-8); %! assert_equal (m.Coefficients.SE(1), 0.169192291625763, -1e-8); %! assert_equal (m.Coefficients.SE(2), 1.361534257888685, -1e-8); %! assert_equal (m.Coefficients.SE(3), 0.148744319911833, -1e-8); %! assert_equal (m.Coefficients.SE(4), 0.0932669056882381, -1e-8); %! assert_equal (m.Coefficients.tStat(1), 0.931725237144526, -1e-8); %! assert_equal (m.Coefficients.tStat(2), 1.531674132351069, -1e-8); %! assert_equal (m.Coefficients.tStat(3), -6.250206405353000, -1e-8); %! assert_equal (m.Coefficients.tStat(4), -0.334609774230031, -1e-8); %! assert_equal (m.Coefficients.pValue(1), 0.365325503492671, -1e-8); %! assert_equal (m.Coefficients.pValue(2), 0.145134783727025, -1e-8); %! assert_equal (m.Coefficients.pValue(3), 1.159217784590233e-05, -1e-8); %! assert_equal (m.Coefficients.pValue(4), 0.742265736761240, -1e-8); %! assert_equal (m.SSE, 0.383859187927621, -1e-8); %! assert_equal (m.MSE, 0.023991199245515, -1e-8); %! assert_equal (m.RMSE, 0.154890926930905, -1e-8); %! assert_equal (m.Rsquared.Ordinary, 0.999342606032059, -1e-8); %! assert_equal (m.Rsquared.Adjusted, 0.999219344663070, -1e-8); %! assert_equal (m.LogLikelihood, 11.153346988927943, -1e-8); %! assert_equal (m.ModelFitVsNullModel.Fstat, 8.107508574898859e+03, -1e-6); %! assert_equal (m.ModelFitVsNullModel.Pvalue, 1.164196605672873e-25, -1e-6); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (m.CoefficientNames{2}, 'x1'); %! assert_equal (m.CoefficientNames{3}, 'x2'); %! assert_equal (m.CoefficientNames{4}, 'x1:x2'); %!test %! ## x1*x2 crossing gives same result as x1:x2 when main effects exist %! m = addTerms (mdl, 'x1*x2'); %! assert_equal (m.NumCoefficients, 4); %! assert_equal (m.DFE, 16); %! assert_equal (m.SSE, 0.383859187927621, -1e-8); %! assert_equal (m.Coefficients.Estimate(1), 0.157640728038039, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), 2.085426803117909, -1e-8); %! assert_equal (m.Coefficients.Estimate(3), -0.929682701072813, -1e-8); %! assert_equal (m.Coefficients.Estimate(4), -0.031208018255475, -1e-8); %! assert_equal (m.CoefficientNames{4}, 'x1:x2'); %!test %! m = addTerms (mdl, 'x1 + x1:x2'); %! assert_equal (m.NumCoefficients, 4); %! assert_equal (m.DFE, 16); %! assert_equal (m.SSE, 0.383859187927621, -1e-8); %! assert_equal (m.Coefficients.Estimate(1), 0.157640728038039, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), 2.085426803117909, -1e-8); %! assert_equal (m.Coefficients.Estimate(3), -0.929682701072813, -1e-8); %! assert_equal (m.Coefficients.Estimate(4), -0.031208018255475, -1e-8); %!test %! ## adding existing term returns equivalent model %! ws = warning ('off', 'all'); %! m = addTerms (mdl, 'x1'); %! warning (ws); %! assert_equal (m.NumCoefficients, 3); %! assert_equal (m.DFE, 17); %! assert_equal (m.Coefficients.Estimate(1), 0.116188677790207, 1e-7); %! assert_equal (m.Coefficients.Estimate(2), 2.508451490570863, 1e-7); %! assert_equal (m.Coefficients.Estimate(3), -0.978835329825186, 1e-7); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (m.CoefficientNames{2}, 'x1'); %! assert_equal (m.CoefficientNames{3}, 'x2'); %!test %! m = addTerms (mdl, 'x2^2'); %! assert_equal (m.NumCoefficients, 4); %! assert_equal (m.DFE, 16); %! assert_equal (m.SSE, 0.386103933724971, -1e-8); %! assert_equal (m.Coefficients.Estimate(1), 0.130152473216993, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), 2.380771990884563, -1e-8); %! assert_equal (m.Coefficients.Estimate(3), -0.967672823484773, -1e-8); %! assert_equal (m.Coefficients.Estimate(4), -2.991483322469043e-04, -1e-8); %! assert_equal (m.Coefficients.SE(1), 0.154974488176692, -1e-8); %! assert_equal (m.Coefficients.SE(2), 1.071554310049276, -1e-8); %! assert_equal (m.Coefficients.SE(3), 0.085801310569364, -1e-8); %! assert_equal (m.Coefficients.SE(4), 0.002211890858232, -1e-8); %! assert_equal (m.CoefficientNames{4}, 'x2^2'); %!test %! ## x1^2 rank-deficient: DFE unchanged SE zero for dropped term %! m = addTerms (mdl, 'x1^2'); %! assert_equal (m.NumCoefficients, 4); %! assert_equal (m.DFE, 17); %! assert_equal (m.SSE, 0.386545331386823, -1e-8); %! assert_equal (m.Coefficients.Estimate(4), 0); %! assert_equal (m.Coefficients.SE(4), 0); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (m.CoefficientNames{2}, 'x1'); %! assert_equal (m.CoefficientNames{3}, 'x2'); %! assert_equal (m.CoefficientNames{4}, 'x1^2'); %!test %! ## numeric matrix [1,1,0] same as string x1:x2 %! m = addTerms (mdl, [1, 1, 0]); %! assert_equal (m.NumCoefficients, 4); %! assert_equal (m.DFE, 16); %! assert_equal (m.SSE, 0.383859187927621, -1e-8); %! assert_equal (m.Coefficients.Estimate(1), 0.157640728038039, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), 2.085426803117909, -1e-8); %! assert_equal (m.Coefficients.Estimate(3), -0.929682701072813, -1e-8); %! assert_equal (m.Coefficients.Estimate(4), -0.031208018255475, -1e-8); %!test %! ## numeric matrix [1,1] auto-padded to [1,1,0] %! m = addTerms (mdl, [1, 1]); %! assert_equal (m.NumCoefficients, 4); %! assert_equal (m.DFE, 16); %! assert_equal (m.SSE, 0.383859187927621, -1e-8); %! assert_equal (m.Coefficients.Estimate(1), 0.157640728038039, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), 2.085426803117909, -1e-8); %! assert_equal (m.Coefficients.Estimate(3), -0.929682701072813, -1e-8); %! assert_equal (m.Coefficients.Estimate(4), -0.031208018255475, -1e-8); %!test %! m = addTerms (mdl, [1, 1, 0; 0, 2, 0]); %! assert_equal (m.NumCoefficients, 5); %! assert_equal (m.DFE, 15); %! assert_equal (m.SSE, 0.315784637443501, -1e-8); %! assert_equal (m.CoefficientNames{4}, 'x1:x2'); %! assert_equal (m.CoefficientNames{5}, 'x2^2'); %!test %! mc = fitlm (X, y, 'constant'); %! m = addTerms (mc, 'x1'); %! assert_equal (m.NumCoefficients, 2); %! assert_equal (m.DFE, 18); %! assert_equal (m.Coefficients.Estimate(1), 3.884704697617172, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), -18.047090435758047, -1e-8); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (m.CoefficientNames{2}, 'x1'); %!test %! ## step from constant to full linear model %! mc = fitlm (X, y, 'constant'); %! mc1 = addTerms (mc, 'x1'); %! mc2 = addTerms (mc1, 'x2'); %! assert_equal (mc2.NumCoefficients, 3); %! assert_equal (mc2.DFE, 17); %! assert_equal (mc2.Coefficients.Estimate(1), 0.116188677790207, 1e-7); %! assert_equal (mc2.Coefficients.Estimate(2), 2.508451490570863, 1e-7); %! assert_equal (mc2.Coefficients.Estimate(3), -0.978835329825186, 1e-7); %! assert_equal (mc2.CoefficientNames{1}, '(Intercept)'); %! assert_equal (mc2.CoefficientNames{2}, 'x1'); %! assert_equal (mc2.CoefficientNames{3}, 'x2'); %!test %! ## adding intercept to no-intercept model %! mni = fitlm (X, y, 'Intercept', false); %! m = addTerms (mni, '1'); %! assert_equal (m.NumCoefficients, 3); %! assert_equal (m.DFE, 17); %! assert_equal (m.Coefficients.Estimate(1), 0.116188677790207, 1e-7); %! assert_equal (m.Coefficients.Estimate(2), 2.508451490570863, 1e-7); %! assert_equal (m.Coefficients.Estimate(3), -0.978835329825186, 1e-7); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (m.CoefficientNames{2}, 'x1'); %! assert_equal (m.CoefficientNames{3}, 'x2'); %!test %! ## weighted model weights preserved %! mw = fitlm (X, y, 'Weights', (1:n)' / sum (1:n)); %! m = addTerms (mw, 'x1:x2'); %! assert_equal (m.NumCoefficients, 4); %! assert_equal (m.DFE, 16); %! assert_equal (m.SSE, 0.019230053719402, -1e-8); %! assert_equal (m.Coefficients.Estimate(1), -0.122645849510537, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), 4.125799311652051, -1e-8); %! assert_equal (m.Coefficients.Estimate(3), -1.128467507844852, -1e-8); %! assert_equal (m.Coefficients.Estimate(4), 0.081921546574140, -1e-8); %! assert_equal (m.Coefficients.SE(1), 0.354926338845420, -1e-8); %! assert_equal (m.Coefficients.SE(2), 2.205951720932299, -1e-8); %! assert_equal (m.Coefficients.SE(3), 0.205033663966776, -1e-8); %! assert_equal (m.Coefficients.SE(4), 0.115523382309399, -1e-8); %! assert_equal (m.CoefficientNames{4}, 'x1:x2'); %!test %! ## excluded observations preserved %! me = fitlm (X, y, 'Exclude', [1, 2]); %! m = addTerms (me, 'x1:x2'); %! assert_equal (m.NumObservations, 18); %! assert_equal (m.DFE, 14); %! assert_equal (m.NumCoefficients, 4); %! assert_equal (m.Coefficients.Estimate(1), -0.345521184099998, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), 5.139185607268283, -1e-8); %! assert_equal (m.Coefficients.Estimate(3), -1.198851436671170, -1e-8); %! assert_equal (m.Coefficients.Estimate(4), 0.112619530301200, -1e-8); %! assert_equal (m.CoefficientNames{4}, 'x1:x2'); %!test %! ## remove two predictors by string from a 4-predictor model %! Xh = [7 26 6 60; 1 29 15 52; 11 56 8 20; 11 31 8 47; 7 52 6 33; ... %! 11 55 9 22; 3 71 17 6; 1 31 22 44; 2 54 18 22; 21 47 4 26; ... %! 1 40 23 34; 11 66 9 12; 10 68 8 12]; %! yh = [78.5;74.3;104.3;87.6;95.9;109.2;102.7;72.5;93.1;115.9;83.8;113.3;109.4]; %! m = removeTerms (fitlm (Xh, yh), 'x3 + x4'); %! assert_equal (m.NumCoefficients, 3); %! assert_equal (m.NumEstimatedCoefficients, 3); %! assert_equal (m.DFE, 10); %! assert_equal (m.NumObservations, 13); %! assert_equal (m.NumVariables, 5); %! assert_equal (m.Coefficients.Estimate(1), 52.577348882089481, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), 1.468305742215555, -1e-8); %! assert_equal (m.Coefficients.Estimate(3), 0.662250491274645, -1e-8); %! assert_equal (m.Coefficients.SE(1), 2.286174334503340, -1e-8); %! assert_equal (m.Coefficients.SE(2), 0.121300923606266, -1e-8); %! assert_equal (m.Coefficients.SE(3), 0.045854721468522, -1e-8); %! assert_equal (m.Coefficients.tStat, m.Coefficients.Estimate ./ m.Coefficients.SE, 1e-10); %! assert_equal (m.Coefficients.tStat(1), 22.997961305305111, -1e-8); %! assert_equal (m.Coefficients.tStat(2), 12.104654264476748, -1e-8); %! assert_equal (m.Coefficients.tStat(3), 14.442362096327519, -1e-8); %! assert_equal (m.Coefficients.pValue(1), 5.456570901490983e-10, -1e-7); %! assert_equal (m.Coefficients.pValue(2), 2.692212179685427e-07, -1e-8); %! assert_equal (m.Coefficients.pValue(3), 5.028960315638413e-08, -1e-8); %! assert_equal (m.SSE, 57.904483176113658, -1e-8); %! assert_equal (m.RMSE, 2.40633503852047, -1e-8); %! assert_equal (m.MSE, 5.790448317611299, 1e-12); %! assert_equal (m.SST, 2.715763076923078e+03, 1e-8); %! assert_equal (m.Rsquared.Ordinary, 0.978678374535632, -1e-8); %! assert_equal (m.Rsquared.Adjusted, 0.974414049442758, -1e-8); %! assert_equal (size (m.CoefficientCovariance), [3, 3]); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (m.CoefficientNames{2}, 'x1'); %! assert_equal (m.CoefficientNames{3}, 'x2'); %! assert_equal (m.Formula.HasIntercept, true); %! assert_equal (m.Formula.LinearPredictor, '1 + x1 + x2'); %! assert_equal (height (m.Diagnostics), 13); %! assert_equal (sum (m.Diagnostics.Leverage), 3, 1e-10); %! assert_equal (m.Residuals.Raw, yh - m.Fitted, 1e-10); %!test %! m = removeTerms (mdl, 'x2'); %! assert_equal (m.NumCoefficients, 2); %! assert_equal (m.NumEstimatedCoefficients, 2); %! assert_equal (m.DFE, 18); %! assert_equal (m.NumObservations, 20); %! assert_equal (m.Coefficients.Estimate(1), 3.88470469761717, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), -18.047090435758, -1e-8); %! assert_equal (m.Coefficients.SE(2), 1.19086428900602, -1e-8); %! assert_equal (m.Coefficients.tStat, m.Coefficients.Estimate ./ m.Coefficients.SE, 1e-10); %! assert_equal (m.Coefficients.tStat(2), -15.1546155194741, -1e-8); %! assert_equal (m.SSE, 42.4383708132815, -1e-8); %! assert_equal (m.SST, 583.910420002346, -1e-8); %! assert_equal (m.Rsquared.Ordinary, 0.927320408474452, -1e-8); %! assert_equal (size (m.CoefficientCovariance), [2, 2]); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (m.CoefficientNames{2}, 'x1'); %! assert_equal (m.Formula.HasIntercept, true); %! assert_equal (m.Formula.LinearPredictor, '1 + x1'); %! assert_equal (height (m.Diagnostics), 20); %! assert_equal (m.Residuals.Raw, y - m.Fitted, 1e-10); %!test %! ## removing the intercept via string '1' %! m = removeTerms (mdl, '1'); %! assert_equal (m.NumCoefficients, 2); %! assert_equal (m.NumEstimatedCoefficients, 2); %! assert_equal (m.DFE, 18); %! assert_equal (m.NumObservations, 20); %! assert_equal (m.Formula.HasIntercept, false); %! assert_equal (m.Formula.LinearPredictor, 'x1 + x2'); %! assert_equal (m.Coefficients.Estimate(1), 2.96142161317611, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), -0.997248749443286, -1e-8); %! assert_equal (m.Coefficients.tStat, m.Coefficients.Estimate ./ m.Coefficients.SE, 1e-10); %! assert_equal (m.SSE, 0.410934843407688, -1e-8); %! assert_equal (size (m.CoefficientCovariance), [2, 2]); %! assert_equal (m.CoefficientNames{1}, 'x1'); %! assert_equal (m.CoefficientNames{2}, 'x2'); %! assert_equal (! any (strcmp (m.CoefficientNames, '(Intercept)')), true); %! assert_equal (height (m.Diagnostics), 20); %!test %! ## removing both predictors leaves only the intercept %! m = removeTerms (mdl, 'x1 + x2'); %! assert_equal (m.NumCoefficients, 1); %! assert_equal (m.NumEstimatedCoefficients, 1); %! assert_equal (m.DFE, 19); %! assert_equal (m.NumObservations, 20); %! assert_equal (m.Formula.HasIntercept, true); %! assert_equal (m.Formula.LinearPredictor, '1'); %! assert_equal (m.Coefficients.Estimate(1), -5.5900177811558, -1e-8); %! assert_equal (m.Coefficients.tStat, m.Coefficients.Estimate ./ m.Coefficients.SE, 1e-10); %! assert_equal (m.SSE, 583.910420002346, -1e-8); %! assert_equal (m.SST, 583.910420002346, -1e-8); %! assert_equal (m.SSR, 0, 1e-20); %! assert_equal (size (m.CoefficientCovariance), [1, 1]); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (height (m.Diagnostics), 20); %! assert_equal (sum (m.Diagnostics.Leverage), 1, 1e-10); %! assert_equal (m.Residuals.Raw, y - m.Fitted, 1e-10); %!test %! ws = warning ('off', 'all'); %! m = removeTerms (mdl, 'x1:x2'); %! warning (ws); %! assert_equal (m.NumCoefficients, mdl.NumCoefficients); %! assert_equal (m.NumEstimatedCoefficients, mdl.NumEstimatedCoefficients); %! assert_equal (m.DFE, mdl.DFE); %! assert_equal (m.SSE, mdl.SSE, 1e-15); %! assert_equal (m.SSR, mdl.SSR, 1e-15); %! assert_equal (m.SST, mdl.SST, 1e-15); %! assert_equal (m.RMSE, mdl.RMSE, 1e-15); %! assert_equal (m.Coefficients.Estimate, mdl.Coefficients.Estimate, 1e-15); %! assert_equal (m.Coefficients.SE, mdl.Coefficients.SE, 1e-15); %! assert_equal (m.CoefficientCovariance, mdl.CoefficientCovariance, 1e-15); %! assert_equal (isequal (m.CoefficientNames, mdl.CoefficientNames), true); %! assert_equal (m.Formula.LinearPredictor, mdl.Formula.LinearPredictor); %! assert_equal (m.Formula.HasIntercept, mdl.Formula.HasIntercept); %!test %! m = removeTerms (mdl, [0 1 0]); %! assert_equal (m.NumCoefficients, 2); %! assert_equal (m.NumEstimatedCoefficients, 2); %! assert_equal (m.DFE, 18); %! assert_equal (m.NumObservations, 20); %! assert_equal (m.Coefficients.Estimate(1), 3.88470469761717, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), -18.047090435758, -1e-8); %! assert_equal (m.Coefficients.SE(2), 1.19086428900602, -1e-8); %! assert_equal (m.Coefficients.tStat, m.Coefficients.Estimate ./ m.Coefficients.SE, 1e-10); %! assert_equal (m.Coefficients.tStat(2), -15.1546155194741, -1e-8); %! assert_equal (m.SSE, 42.4383708132815, -1e-8); %! assert_equal (m.SST, 583.910420002346, -1e-8); %! assert_equal (m.Rsquared.Ordinary, 0.927320408474452, -1e-8); %! assert_equal (size (m.CoefficientCovariance), [2, 2]); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (m.CoefficientNames{2}, 'x1'); %! assert_equal (m.Formula.HasIntercept, true); %! assert_equal (m.Formula.LinearPredictor, '1 + x1'); %! assert_equal (height (m.Diagnostics), 20); %! assert_equal (sum (m.Diagnostics.Leverage), 2, 1e-10); %!test %! ## auto-padded matrix [0 1] gives identical result to [0 1 0] %! m = removeTerms (mdl, [0 1]); %! assert_equal (m.NumCoefficients, 2); %! assert_equal (m.NumEstimatedCoefficients, 2); %! assert_equal (m.DFE, 18); %! assert_equal (m.Coefficients.Estimate(1), 3.88470469761717, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), -18.047090435758, -1e-8); %! assert_equal (m.Coefficients.SE(2), 1.19086428900602, -1e-8); %! assert_equal (m.Coefficients.tStat, m.Coefficients.Estimate ./ m.Coefficients.SE, 1e-10); %! assert_equal (m.SSE, 42.4383708132815, -1e-8); %! assert_equal (m.SST, 583.910420002346, -1e-8); %! assert_equal (m.Rsquared.Ordinary, 0.927320408474452, -1e-8); %! assert_equal (size (m.CoefficientCovariance), [2, 2]); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (m.CoefficientNames{2}, 'x1'); %! assert_equal (m.Formula.LinearPredictor, '1 + x1'); %! assert_equal (height (m.Diagnostics), 20); %! assert_equal (sum (m.Diagnostics.Leverage), 2, 1e-10); %!test %! ## multi-row matrix removes two terms same as the string form %! Xh = [7 26 6 60; 1 29 15 52; 11 56 8 20; 11 31 8 47; 7 52 6 33; ... %! 11 55 9 22; 3 71 17 6; 1 31 22 44; 2 54 18 22; 21 47 4 26; ... %! 1 40 23 34; 11 66 9 12; 10 68 8 12]; %! yh = [78.5;74.3;104.3;87.6;95.9;109.2;102.7;72.5;93.1;115.9;83.8;113.3;109.4]; %! m = removeTerms (fitlm (Xh, yh), [0 0 1 0 0; 0 0 0 1 0]); %! assert_equal (m.NumCoefficients, 3); %! assert_equal (m.NumEstimatedCoefficients, 3); %! assert_equal (m.DFE, 10); %! assert_equal (m.NumObservations, 13); %! assert_equal (m.Coefficients.Estimate(1), 52.577348882089481, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), 1.468305742215555, -1e-8); %! assert_equal (m.Coefficients.Estimate(3), 0.662250491274645, -1e-8); %! assert_equal (m.Coefficients.SE(1), 2.286174334503340, -1e-8); %! assert_equal (m.Coefficients.SE(2), 0.121300923606266, -1e-8); %! assert_equal (m.Coefficients.SE(3), 0.045854721468522, -1e-8); %! assert_equal (m.Coefficients.tStat, m.Coefficients.Estimate ./ m.Coefficients.SE, 1e-10); %! assert_equal (m.Coefficients.tStat(2), 12.104654264476748, -1e-8); %! assert_equal (m.Coefficients.tStat(3), 14.442362096327519, -1e-8); %! assert_equal (m.Coefficients.pValue(2), 2.692212179685427e-07, -1e-8); %! assert_equal (m.SSE, 57.904483176113658, -1e-8); %! assert_equal (m.Rsquared.Ordinary, 0.978678374535632, -1e-8); %! assert_equal (m.Rsquared.Adjusted, 0.974414049442758, -1e-8); %! assert_equal (size (m.CoefficientCovariance), [3, 3]); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (m.CoefficientNames{2}, 'x1'); %! assert_equal (m.CoefficientNames{3}, 'x2'); %! assert_equal (m.Formula.HasIntercept, true); %! assert_equal (m.Formula.LinearPredictor, '1 + x1 + x2'); %! assert_equal (height (m.Diagnostics), 13); %! assert_equal (sum (m.Diagnostics.Leverage), 3, 1e-10); %! assert_equal (m.Residuals.Raw, yh - m.Fitted, 1e-10); %!test %! ## observation weights carry through to the refitted model %! w = (1:n)' / sum (1:n); %! mw = fitlm (X, y, 'Weights', w); %! m = removeTerms (mw, 'x2'); %! assert_equal (m.NumCoefficients, 2); %! assert_equal (m.NumEstimatedCoefficients, 2); %! assert_equal (m.DFE, 18); %! assert_equal (m.NumObservations, 20); %! assert_equal (m.Coefficients.Estimate(1), 6.29263960898714, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), -21.5708976231287, -1e-8); %! assert_equal (m.Coefficients.tStat, m.Coefficients.Estimate ./ m.Coefficients.SE, 1e-10); %! assert_equal (m.SSE, 1.41763159723151, -1e-8); %! assert_equal (size (m.CoefficientCovariance), [2, 2]); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (m.CoefficientNames{2}, 'x1'); %! assert_equal (m.Formula.HasIntercept, true); %! assert_equal (m.Formula.LinearPredictor, '1 + x1'); %! assert_equal (m.ObservationInfo.Weights, w, 1e-15); %! assert_equal (sum (m.ObservationInfo.Weights), 1, 1e-12); %! assert_equal (height (m.Diagnostics), 20); %! assert_equal (sum (m.Diagnostics.Leverage), 2, 1e-10); %! assert_equal (m.SSE != removeTerms (mdl, 'x2').SSE, true); %!test %! ## excluded rows are preserved and reduce effective sample size %! me = fitlm (X, y, 'Exclude', [1, 3]); %! m = removeTerms (me, 'x2'); %! assert_equal (m.NumObservations, 18); %! assert_equal (m.DFE, 16); %! assert_equal (m.NumCoefficients, 2); %! assert_equal (m.NumEstimatedCoefficients, 2); %! assert_equal (m.Coefficients.Estimate(1), 4.96609542902066, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), -19.5618050042778, -1e-8); %! assert_equal (m.Coefficients.tStat, m.Coefficients.Estimate ./ m.Coefficients.SE, 1e-10); %! assert_equal (size (m.CoefficientCovariance), [2, 2]); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (m.CoefficientNames{2}, 'x1'); %! assert_equal (m.Formula.LinearPredictor, '1 + x1'); %! assert_equal (m.ObservationInfo.Excluded(1), true); %! assert_equal (m.ObservationInfo.Excluded(3), true); %! assert_equal (m.ObservationInfo.Excluded(2), false); %! assert_equal (m.ObservationInfo.Missing(1), false); %! assert_equal (height (m.Diagnostics), 20); %! assert_equal (m.Fitted(1), 3.988005178806767, 1e-10); %! assert_equal (m.Fitted(3), 2.031824678378986, 1e-10); %! assert_equal (m.Fitted(2), 3.009914928592877, 1e-10); %! assert_equal (isnan (m.Residuals.Raw(1)), true); %!test %! ## removing x2 from a no-intercept model gives one slope term %! mni = fitlm (X, y, 'Intercept', false); %! m = removeTerms (mni, 'x2'); %! assert_equal (m.NumCoefficients, 1); %! assert_equal (m.NumEstimatedCoefficients, 1); %! assert_equal (m.DFE, 19); %! assert_equal (m.NumObservations, 20); %! assert_equal (m.Formula.HasIntercept, false); %! assert_equal (m.Formula.LinearPredictor, 'x1'); %! assert_equal (m.Coefficients.Estimate(1), -12.362156731928, -1e-8); %! assert_equal (m.Coefficients.tStat, m.Coefficients.Estimate ./ m.Coefficients.SE, 1e-10); %! assert_equal (m.SSE, 112.371951585499, -1e-8); %! assert_equal (size (m.CoefficientCovariance), [1, 1]); %! assert_equal (m.CoefficientCovariance(1,1) > 0, true); %! assert_equal (m.CoefficientNames{1}, 'x1'); %! assert_equal (! any (strcmp (m.CoefficientNames, '(Intercept)')), true); %! assert_equal (height (m.Diagnostics), 20); %! assert_equal (all (isfinite (m.Fitted)), true); %! assert_equal (sum (m.Diagnostics.Leverage), 1, 1e-10); %!test %! ## removing the interaction term recovers the plain linear model %! mi = fitlm (X, y, 'interactions'); %! m = removeTerms (mi, 'x1:x2'); %! assert_equal (m.NumCoefficients, 3); %! assert_equal (m.NumEstimatedCoefficients, 3); %! assert_equal (m.DFE, 17); %! assert_equal (m.NumObservations, 20); %! assert_equal (m.Coefficients.Estimate(1), 0.116188677790207, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), 2.508451490570863, -1e-8); %! assert_equal (m.Coefficients.Estimate(3), -0.978835329825186, -1e-8); %! assert_equal (m.Coefficients.SE(1), 0.112185831, -1e-7); %! assert_equal (m.Coefficients.SE(2), 0.4920818186, -1e-7); %! assert_equal (m.Coefficients.SE(3), 0.02276108523, -1e-7); %! assert_equal (m.Coefficients.tStat, m.Coefficients.Estimate ./ m.Coefficients.SE, 1e-10); %! assert_equal (m.Coefficients.tStat(1), 1.035680502, -1e-6); %! assert_equal (m.Coefficients.tStat(2), 5.097630913, -1e-6); %! assert_equal (m.Coefficients.tStat(3), -43.00477415, -1e-6); %! assert_equal (m.SSE, 0.386545331386823, -1e-8); %! assert_equal (size (m.CoefficientCovariance), [3, 3]); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (m.CoefficientNames{2}, 'x1'); %! assert_equal (m.CoefficientNames{3}, 'x2'); %! assert_equal (m.Formula.HasIntercept, true); %! assert_equal (m.Formula.LinearPredictor, '1 + x1 + x2'); %! assert_equal (height (m.Diagnostics), 20); %! assert_equal (sum (m.Diagnostics.Leverage), 3, 1e-10); %!test %! ## removing a quadratic term refits on the remaining terms %! mq = fitlm (X, y, 'quadratic'); %! m = removeTerms (mq, 'x2^2'); %! assert_equal (m.NumEstimatedCoefficients, 4); %! assert_equal (m.DFE, 16); %! assert_equal (m.SSE, 0.383859187927621, -1e-8); %! assert_equal (size (m.CoefficientCovariance, 1), m.NumCoefficients); %! assert_equal (size (m.CoefficientCovariance, 2), m.NumCoefficients); %! assert_equal (m.Formula.HasIntercept, true); %! assert_equal (height (m.Diagnostics), 20); %! assert_equal (sum (m.Diagnostics.Leverage), 4, 1e-10); %! assert_equal (m.SSE >= mq.SSE, true); %! assert_equal (! any (strcmp (m.CoefficientNames, 'x2^2')), true); %!test %! ## star notation removes main effects and interaction in one call %! mi = fitlm (X, y, 'interactions'); %! m = removeTerms (mi, 'x1*x2'); %! assert_equal (m.NumCoefficients, 1); %! assert_equal (m.NumEstimatedCoefficients, 1); %! assert_equal (m.DFE, 19); %! assert_equal (m.NumObservations, 20); %! assert_equal (m.Formula.HasIntercept, true); %! assert_equal (m.Formula.LinearPredictor, '1'); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (m.Coefficients.Estimate(1), -5.5900177811558, -1e-8); %! assert_equal (m.Coefficients.tStat, m.Coefficients.Estimate ./ m.Coefficients.SE, 1e-10); %! assert_equal (m.SSE, 583.910420002346, -1e-8); %! assert_equal (m.SST, 583.910420002346, -1e-8); %! assert_equal (m.SSR, 0, 1e-20); %! assert_equal (size (m.CoefficientCovariance), [1, 1]); %! assert_equal (height (m.Diagnostics), 20); %! assert_equal (sum (m.Diagnostics.Leverage), 1, 1e-10); %!test %! ## 3-predictor model: removing one term matches a direct two-predictor fit %! X3 = [X, sin((1:n)' * pi / n)]; %! y3 = X3 * [3; -1; 2] + 0.1 * cos ((1:n)' * pi / 7); %! m = removeTerms (fitlm (X3, y3), 'x3'); %! r = fitlm (X, y3); %! assert_equal (m.NumCoefficients, 3); %! assert_equal (m.NumEstimatedCoefficients, 3); %! assert_equal (m.DFE, 17); %! assert_equal (m.NumObservations, 20); %! assert_equal (m.Coefficients.Estimate, r.Coefficients.Estimate, 1e-10); %! assert_equal (m.Coefficients.SE, r.Coefficients.SE, 1e-10); %! assert_equal (m.Coefficients.tStat, r.Coefficients.tStat, 1e-10); %! assert_equal (m.Coefficients.pValue, r.Coefficients.pValue, 1e-10); %! assert_equal (m.SSE, r.SSE, 1e-12); %! assert_equal (m.SSR, r.SSR, 1e-12); %! assert_equal (m.SST, r.SST, 1e-12); %! assert_equal (m.MSE, r.MSE, 1e-12); %! assert_equal (m.RMSE, r.RMSE, 1e-12); %! assert_equal (m.Rsquared.Ordinary, r.Rsquared.Ordinary, 1e-12); %! assert_equal (m.Rsquared.Adjusted, r.Rsquared.Adjusted, 1e-12); %! assert_equal (m.CoefficientCovariance, r.CoefficientCovariance, 1e-12); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (m.CoefficientNames{2}, 'x1'); %! assert_equal (m.CoefficientNames{3}, 'x2'); %! assert_equal (m.Formula.HasIntercept, true); %! assert_equal (m.Formula.LinearPredictor, '1 + x1 + x2'); %! assert_equal (height (m.Diagnostics), 20); %! assert_equal (sum (m.Diagnostics.Leverage), 3, 1e-10); %!test %! ## 3-predictor model: removing two terms matches a direct one-predictor fit %! X3 = [X, sin((1:n)' * pi / n)]; %! y3 = X3 * [3; -1; 2] + 0.1 * cos ((1:n)' * pi / 7); %! m = removeTerms (fitlm (X3, y3), 'x2 + x3'); %! r = fitlm (X(:,1), y3); %! assert_equal (m.NumCoefficients, 2); %! assert_equal (m.NumEstimatedCoefficients, 2); %! assert_equal (m.DFE, 18); %! assert_equal (m.NumObservations, 20); %! assert_equal (m.Coefficients.Estimate, r.Coefficients.Estimate, 1e-10); %! assert_equal (m.Coefficients.SE, r.Coefficients.SE, 1e-10); %! assert_equal (m.Coefficients.tStat, r.Coefficients.tStat, 1e-10); %! assert_equal (m.Coefficients.pValue, r.Coefficients.pValue, 1e-10); %! assert_equal (m.SSE, r.SSE, 1e-12); %! assert_equal (m.SST, r.SST, 1e-12); %! assert_equal (m.MSE, r.MSE, 1e-12); %! assert_equal (m.RMSE, r.RMSE, 1e-12); %! assert_equal (m.Rsquared.Ordinary, r.Rsquared.Ordinary, 1e-12); %! assert_equal (m.Rsquared.Adjusted, r.Rsquared.Adjusted, 1e-12); %! assert_equal (m.CoefficientCovariance, r.CoefficientCovariance, 1e-12); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (m.CoefficientNames{2}, 'x1'); %! assert_equal (m.Formula.HasIntercept, true); %! assert_equal (m.Formula.LinearPredictor, '1 + x1'); %! assert_equal (height (m.Diagnostics), 20); %! assert_equal (sum (m.Diagnostics.Leverage), 2, 1e-10); %!test %! Xh = [7 26 6 60; 1 29 15 52; 11 56 8 20; 11 31 8 47; 7 52 6 33; ... %! 11 55 9 22; 3 71 17 6; 1 31 22 44; 2 54 18 22; 21 47 4 26; ... %! 1 40 23 34; 11 66 9 12; 10 68 8 12]; %! yh = [78.5;74.3;104.3;87.6;95.9;109.2;102.7;72.5;93.1;115.9;83.8;113.3;109.4]; %! m = removeTerms (fitlm (Xh, yh), 'x3 + x4'); %! r = removeTerms (removeTerms (fitlm (Xh, yh), 'x4'), 'x3'); %! assert_equal (r.NumCoefficients, 3); %! assert_equal (r.DFE, 10); %! assert_equal (r.SSE, m.SSE, 1e-12); %! assert_equal (r.SSR, m.SSR, 1e-12); %! assert_equal (r.SST, m.SST, 1e-12); %! assert_equal (r.RMSE, m.RMSE, 1e-12); %! assert_equal (r.Coefficients.Estimate, m.Coefficients.Estimate, 1e-10); %! assert_equal (r.Coefficients.SE, m.Coefficients.SE, 1e-10); %! assert_equal (r.Coefficients.tStat, m.Coefficients.tStat, 1e-10); %! assert_equal (r.Coefficients.pValue, m.Coefficients.pValue, 1e-10); %! assert_equal (r.CoefficientCovariance, m.CoefficientCovariance, 1e-12); %! assert_equal (isequal (r.CoefficientNames, m.CoefficientNames), true); %! assert_equal (r.Rsquared.Ordinary, m.Rsquared.Ordinary, 1e-12); %! assert_equal (r.Rsquared.Adjusted, m.Rsquared.Adjusted, 1e-12); %! assert_equal (r.Formula.LinearPredictor, m.Formula.LinearPredictor); %!test %! m = removeTerms (mdl, [0 0 0]); %! r = removeTerms (mdl, '1'); %! assert_equal (m.NumCoefficients, 2); %! assert_equal (m.Formula.HasIntercept, false); %! assert_equal (m.Formula.LinearPredictor, 'x1 + x2'); %! assert_equal (m.Coefficients.Estimate(1), 2.96142161317611, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), -0.997248749443286, -1e-8); %! assert_equal (m.Coefficients.tStat, m.Coefficients.Estimate ./ m.Coefficients.SE, 1e-10); %! assert_equal (m.SSE, r.SSE, 1e-15); %! assert_equal (m.SSR, r.SSR, 1e-15); %! assert_equal (m.SST, r.SST, 1e-15); %! assert_equal (m.Coefficients.Estimate, r.Coefficients.Estimate, 1e-15); %! assert_equal (m.Coefficients.SE, r.Coefficients.SE, 1e-15); %! assert_equal (m.CoefficientCovariance, r.CoefficientCovariance, 1e-15); %! assert_equal (isequal (m.CoefficientNames, r.CoefficientNames), true); %! assert_equal (height (m.Diagnostics), 20); %! assert_equal (sum (m.Diagnostics.Leverage), 2, 1e-10); %!test %! ## removing a categorical predictor drops all its indicator variables at once %! Xc = [1;1;1;2;2;2;3;3;3]; %! yc = [2.1;2.3;1.9; 4.1;3.9;4.2; 6.3;5.8;6.1]; %! mc = fitlm (Xc, yc, 'linear', 'CategoricalVars', 1); %! m = removeTerms (mc, 'x1'); %! assert_equal (m.NumCoefficients, 1); %! assert_equal (m.NumEstimatedCoefficients, 1); %! assert_equal (m.DFE, 8); %! assert_equal (m.NumObservations, 9); %! assert_equal (m.Formula.HasIntercept, true); %! assert_equal (m.Formula.LinearPredictor, '1'); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (m.Coefficients.Estimate(1), 4.07777777777778, -1e-8); %! assert_equal (m.Coefficients.tStat, m.Coefficients.Estimate ./ m.Coefficients.SE, 1e-10); %! assert_equal (m.SSR, 0, 1e-20); %! assert_equal (size (m.CoefficientCovariance), [1, 1]); %! assert_equal (height (m.Diagnostics), 9); %! assert_equal (all (isfinite (m.Fitted)), true); %! assert_equal (sum (m.Diagnostics.Leverage), 1, 1e-10); %! assert_equal (m.Residuals.Raw, yc - m.Fitted, 1e-10); %!test %! ## matrix row removes x4 from hald leaving intercept plus x1 x2 x3 %! Xh = [7 26 6 60; 1 29 15 52; 11 56 8 20; 11 31 8 47; 7 52 6 33; ... %! 11 55 9 22; 3 71 17 6; 1 31 22 44; 2 54 18 22; 21 47 4 26; ... %! 1 40 23 34; 11 66 9 12; 10 68 8 12]; %! yh = [78.5;74.3;104.3;87.6;95.9;109.2;102.7;72.5;93.1;115.9;83.8;113.3;109.4]; %! m = removeTerms (fitlm (Xh, yh), [0 0 0 1 0]); %! assert_equal (m.NumCoefficients, 4); %! assert_equal (m.NumEstimatedCoefficients, 4); %! assert_equal (m.DFE, 9); %! assert_equal (m.NumObservations, 13); %! assert_equal (m.Coefficients.Estimate(1), 48.1936343180437, -1e-8); %! assert_equal (m.Coefficients.Estimate(2), 1.69589016748479, -1e-8); %! assert_equal (m.Coefficients.Estimate(3), 0.656914878270554, -1e-8); %! assert_equal (m.Coefficients.Estimate(4), 0.250017606680009, -1e-8); %! assert_equal (m.Coefficients.tStat, m.Coefficients.Estimate ./ m.Coefficients.SE, 1e-10); %! assert_equal (m.SSE, 48.1106140726532, -1e-8); %! assert_equal (size (m.CoefficientCovariance), [4, 4]); %! assert_equal (m.CoefficientNames{1}, '(Intercept)'); %! assert_equal (m.CoefficientNames{2}, 'x1'); %! assert_equal (m.CoefficientNames{3}, 'x2'); %! assert_equal (m.CoefficientNames{4}, 'x3'); %! assert_equal (m.Formula.HasIntercept, true); %! assert_equal (m.Formula.LinearPredictor, '1 + x1 + x2 + x3'); %! assert_equal (height (m.Diagnostics), 13); %! assert_equal (all (isfinite (m.Fitted)), true); %! assert_equal (sum (m.Diagnostics.Leverage), 4, 1e-10); %! assert_equal (m.Residuals.Raw, yh - m.Fitted, 1e-10); %!test %! ## default call creates a histogram with correct bin count and density %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotResiduals (ax, mdl); %! xd = get (h(1), 'XData'); %! yd = get (h(1), 'YData'); %! r = mdl.Residuals.Raw(! isnan (mdl.Residuals.Raw)); %! bw = xd(3,1) - xd(1,1); %! assert_equal (numel (h), 1); %! assert_equal (get (h(1), 'type'), 'patch'); %! assert_equal (size (xd, 2) > 0, true); %! assert_equal (sum (yd(2,:)) * bw, 1, 1e-10); %! assert_equal (all (yd(1,:) == 0) && all (yd(4,:) == 0), true); %! assert_equal (get (get (ax, 'xlabel'), 'string'), 'Residuals'); %! assert_equal (get (get (ax, 'ylabel'), 'string'), 'Probability density'); %! assert_equal (get (get (ax, 'title'), 'string'), 'Histogram of residuals'); %! close (fig); %!test %! ## histogram bar color changes when FaceColor is passed %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotResiduals (ax, mdl, 'histogram', 'FaceColor', [0 1 0]); %! assert_equal (get (h(1), 'FaceColor'), [0 1 0], 1e-10); %! close (fig); %!test %! ## fitted plot shows residuals against fitted values with a zero reference line %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotResiduals (ax, mdl, 'fitted'); %! assert_equal (numel (h), 2); %! assert_equal (get (h(1), 'XData'), mdl.Fitted', 1e-15); %! assert_equal (get (h(1), 'YData'), mdl.Residuals.Raw', 1e-15); %! assert_equal (get (h(1), 'LineStyle'), 'none'); %! assert_equal (get (h(1), 'Marker'), 'x'); %! assert_equal (get (h(2), 'YData'), [0 0]); %! assert_equal (get (h(2), 'LineStyle'), ':'); %! assert_equal (get (get (ax, 'xlabel'), 'string'), 'Fitted values'); %! assert_equal (get (get (ax, 'ylabel'), 'string'), 'Residuals'); %! assert_equal (get (get (ax, 'title'), 'string'), 'Plot of residuals vs. fitted values'); %! close (fig); %!test %! ## custom color applies to data points but leaves the reference line unchanged %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotResiduals (ax, mdl, 'fitted', 'Color', [1 0 0]); %! assert_equal (get (h(1), 'Color'), [1 0 0], 1e-10); %! assert_equal (get (h(2), 'Color'), [0.8510 0.8510 0.8510], 1e-4); %! assert_equal (get (h(2), 'LineStyle'), ':'); %! close (fig); %!test %! ## excluded rows appear as gaps in the fitted plot %! me = fitlm (X, y, 'Exclude', [3, 8]); %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotResiduals (ax, me, 'fitted'); %! yd = get (h(1), 'YData'); %! assert_equal (numel (yd), 20); %! assert_equal (isnan (yd(3)), true); %! assert_equal (isnan (yd(8)), true); %! assert_equal (! isnan (yd(1)), true); %! close (fig); %!test %! ## case order plot covers all rows and shows gaps where rows were excluded %! me = fitlm (X, y, 'Exclude', [2, 5]); %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotResiduals (ax, me, 'caseorder'); %! xd = get (h(1), 'XData'); %! yd = get (h(1), 'YData'); %! assert_equal (xd, 1:20); %! assert_equal (isnan (yd(2)), true); %! assert_equal (isnan (yd(5)), true); %! assert_equal (! isnan (yd(1)), true); %! assert_equal (get (h(2), 'YData'), [0 0]); %! assert_equal (get (get (ax, 'xlabel'), 'string'), 'Row number'); %! assert_equal (get (get (ax, 'ylabel'), 'string'), 'Residuals'); %! assert_equal (get (get (ax, 'title'), 'string'), 'Case order plot of residuals'); %! close (fig); %!test %! ## lagged plot shows each residual against the previous one with two reference lines %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotResiduals (ax, mdl, 'lagged'); %! r = mdl.Residuals.Raw; %! assert_equal (numel (h), 3); %! assert_equal (get (h(1), 'XData'), r(1:end-1)', 1e-15); %! assert_equal (get (h(1), 'YData'), r(2:end)', 1e-15); %! assert_equal (get (h(2), 'YData'), [0 0]); %! assert_equal (get (h(3), 'XData'), [0 0]); %! assert_equal (get (h(2), 'LineStyle'), ':'); %! assert_equal (get (get (ax, 'xlabel'), 'string'), 'Residual(t-1)'); %! assert_equal (get (get (ax, 'ylabel'), 'string'), 'Residual(t)'); %! assert_equal (get (get (ax, 'title'), 'string'), 'Plot of residuals vs. lagged residuals'); %! close (fig); %!test %! ## probability plot uses sorted active residuals as its data %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotResiduals (ax, mdl, 'probability'); %! r_s = sort (mdl.Residuals.Raw(! isnan (mdl.Residuals.Raw))); %! assert_equal (get (h(1), 'XData'), r_s', 1e-15); %! assert_equal (get (get (ax, 'xlabel'), 'string'), 'Residuals'); %! assert_equal (get (get (ax, 'ylabel'), 'string'), 'Probability'); %! assert_equal (get (get (ax, 'title'), 'string'), 'Normal probability plot of residuals'); %! close (fig); %!test %! ## observed plot connects each point to the reference line with a vertical segment %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotResiduals (ax, mdl, 'observed'); %! obs = mdl.Variables{:, mdl.ResponseName}; %! assert_equal (numel (h), 3); %! assert_equal (get (h(1), 'XData'), mdl.Fitted', 1e-15); %! assert_equal (get (h(1), 'YData'), obs', 1e-15); %! assert_equal (isequal (get (h(2), 'XData'), get (h(2), 'YData')), true); %! xd3 = get (h(3), 'XData'); %! assert_equal (numel (xd3), 3 * mdl.NumObservations); %! assert_equal (sum (isnan (xd3)), mdl.NumObservations); %! assert_equal (get (get (ax, 'xlabel'), 'string'), 'Fitted values'); %! assert_equal (get (get (ax, 'ylabel'), 'string'), 'Observed response values'); %! assert_equal (get (get (ax, 'title'), 'string'), 'Plot of observed vs. fitted values'); %! close (fig); %!test %! ## symmetry plot measures distance from median in both tails %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotResiduals (ax, mdl, 'symmetry'); %! r_s = sort (mdl.Residuals.Raw(! isnan (mdl.Residuals.Raw))); %! med = median (r_s); %! m = floor (numel (r_s) / 2); %! x_sym = sort (med - r_s(1:m)); %! y_sym = sort (r_s(end-m+1:end) - med); %! assert_equal (numel (h), 2); %! assert_equal (get (h(1), 'XData'), x_sym', 1e-15); %! assert_equal (get (h(1), 'YData'), y_sym', 1e-15); %! assert_equal (isequal (get (h(2), 'XData'), get (h(2), 'YData')), true); %! assert_equal (get (h(2), 'LineStyle'), ':'); %! assert_equal (get (get (ax, 'xlabel'), 'string'), 'Lower tail'); %! assert_equal (get (get (ax, 'ylabel'), 'string'), 'Upper tail'); %! close (fig); %!test %! ## switching to pearson residuals changes plotted values but not x positions %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotResiduals (ax, mdl, 'fitted', 'ResidualType', 'pearson'); %! assert_equal (get (h(1), 'YData'), mdl.Residuals.Pearson', 1e-15); %! assert_equal (get (h(1), 'XData'), mdl.Fitted', 1e-15); %! assert_equal (! isequal (get (h(1), 'YData'), mdl.Residuals.Raw'), true); %! close (fig); %!test %! ## standardized and studentized residuals produce different values %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h1 = plotResiduals (ax, mdl, 'caseorder', 'ResidualType', 'standardized'); %! h2 = plotResiduals (ax, mdl, 'caseorder', 'ResidualType', 'studentized'); %! assert_equal (get (h1(1), 'YData'), mdl.Residuals.Standardized', 1e-15); %! assert_equal (get (h2(1), 'YData'), mdl.Residuals.Studentized', 1e-15); %! assert_equal (! isequal (get (h1(1), 'YData'), get (h2(1), 'YData')), true); %! close (fig); %!test %! ## marker style and size apply to data points but not to the reference line %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotResiduals (ax, mdl, 'fitted', 'Marker', 's', 'MarkerSize', 10); %! assert_equal (get (h(1), 'Marker'), 's'); %! assert_equal (get (h(1), 'MarkerSize'), 10); %! assert_equal (get (h(2), 'Marker'), 'none'); %! close (fig); %!test %! ## weighted model residuals differ from unweighted residuals in the fitted plot %! mw = fitlm (X, y, 'Weights', (1:n)' / sum (1:n)); %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotResiduals (ax, mw, 'fitted'); %! assert_equal (get (h(1), 'YData'), mw.Residuals.Raw', 1e-15); %! assert_equal (! isequal (get (h(1), 'YData'), mdl.Residuals.Raw'), true); %! close (fig); %!test %! ## calling without an axes handle plots into the current axes %! fig = figure ('visible', 'off'); %! h = plotResiduals (mdl, 'fitted'); %! assert_equal (isgraphics (get (h(1), 'Parent'), 'axes'), true); %! assert_equal (isequal (get (h(1), 'Parent'), gca ()), true); %! close (fig); %!test %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotDiagnostics (ax, mdl); %! yd = get (h(1), 'YData'); %! assert_equal (numel (h), 2); %! assert_equal (get (h(1), 'XData'), 1:n); %! assert_equal (yd(1), 0.370779220779221, -1e-10); %! assert_equal (yd(2), 0.245283663704716, -1e-10); %! assert_equal (yd(3), 0.164718614718615, -1e-10); %! assert_equal (yd(4), 0.118147641831852, -1e-10); %! assert_equal (yd(5), 0.0960013670539986, -1e-10); %! assert_equal (yd(6), 0.0900774663932558, -1e-10); %! assert_equal (yd(7), 0.0935406698564593, -1e-10); %! assert_equal (yd(8), 0.100922761449077, -1e-10); %! assert_equal (yd(9), 0.108122579175211, -1e-10); %! assert_equal (yd(10), 0.112406015037594, -1e-10); %! assert_equal (yd(11), 0.112406015037594, -1e-10); %! assert_equal (yd(12), 0.108122579175211, -1e-10); %! assert_equal (yd(13), 0.100922761449077, -1e-10); %! assert_equal (yd(14), 0.0935406698564592, -1e-10); %! assert_equal (yd(15), 0.0900774663932559, -1e-10); %! assert_equal (yd(16), 0.0960013670539986, -1e-10); %! assert_equal (yd(17), 0.118147641831852, -1e-10); %! assert_equal (yd(18), 0.164718614718615, -1e-10); %! assert_equal (yd(19), 0.245283663704716, -1e-10); %! assert_equal (yd(20), 0.370779220779221, -1e-10); %! assert_equal (get (h(2), 'YData'), [0.3, 0.3], 1e-12); %! assert_equal (get (h(2), 'XData'), [0, n]); %! assert_equal (get (h(2), 'LineStyle'), ':'); %! assert_equal (get (get (ax, 'xlabel'), 'string'), 'Row number'); %! assert_equal (get (get (ax, 'ylabel'), 'string'), 'Leverage'); %! assert_equal (get (get (ax, 'title'), 'string'), 'Case order plot of leverage'); %! close (fig); %!test %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotDiagnostics (ax, mdl, 'leverage', 'Color', [1 0 0]); %! assert_equal (get (h(1), 'Color'), [1 0 0], 1e-10); %! assert_equal (get (h(2), 'Color'), [0.8510 0.8510 0.8510], 1e-4); %! assert_equal (get (h(2), 'LineStyle'), ':'); %! close (fig); %!test %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotDiagnostics (ax, mdl, 'cookd'); %! yd = get (h(1), 'YData'); %! assert_equal (numel (h), 2); %! assert_equal (yd(1), 0.078517048682575, -1e-8); %! assert_equal (yd(2), 0.077211407930332, -1e-8); %! assert_equal (yd(3), 0.001953301452841, -1e-7); %! assert_equal (get (h(2), 'YData'), [0.1668641787, 0.1668641787], -1e-8); %! assert_equal (get (h(2), 'XData'), [0, n]); %! assert_equal (get (get (ax, 'ylabel'), 'string'), 'Cook''s distance'); %! assert_equal (get (get (ax, 'title'), 'string'), 'Case order plot of Cook''s distance'); %! close (fig); %!test %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotDiagnostics (ax, mdl, 'covratio'); %! yd = get (h(1), 'YData'); %! yv = get (h(2), 'YData'); %! xv = get (h(2), 'XData'); %! assert_equal (numel (h), 2); %! assert_equal (yd(1), 1.774933177, -1e-8); %! assert_equal (yd(2), 1.397661919, -1e-8); %! assert_equal (yd(3), 1.428481535, -1e-8); %! assert_equal (numel (xv), 5); %! assert_equal (sum (isnan (xv)), 1); %! assert_equal (yv(1), 0.55, 1e-12); %! assert_equal (yv(2), 0.55, 1e-12); %! assert_equal (yv(4), 1.45, 1e-12); %! assert_equal (yv(5), 1.45, 1e-12); %! assert_equal (get (get (ax, 'ylabel'), 'string'), 'Covariance ratio'); %! assert_equal (get (get (ax, 'title'), 'string'), 'Case order plot of covariance ratio'); %! close (fig); %!test %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotDiagnostics (ax, mdl, 'dfbetas'); %! p = mdl.NumCoefficients; %! yv = get (h(p+1), 'YData'); %! xv = get (h(p+1), 'XData'); %! assert_equal (numel (h), p + 1); %! assert_equal (numel (get (h(1), 'YData')), n); %! assert_equal (numel (get (h(2), 'YData')), n); %! assert_equal (numel (get (h(3), 'YData')), n); %! assert_equal (numel (xv), 5); %! assert_equal (sum (isnan (xv)), 1); %! assert_equal (yv(1), -0.6708203932, -1e-8); %! assert_equal (yv(end), 0.6708203932, -1e-8); %! assert_equal (get (get (ax, 'ylabel'), 'string'), 'Scaled change in coefficients'); %! assert_equal (get (get (ax, 'title'), 'string'), 'Case order plot of scaled change in coefficients'); %! close (fig); %!test %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotDiagnostics (ax, mdl, 'dfbetas', 'Color', [1 0 0]); %! p = mdl.NumCoefficients; %! for k = 1:p %! assert_equal (get (h(k), 'Color'), [1 0 0], 1e-10); %! endfor %! assert_equal (get (h(p+1), 'Color'), [0.8510 0.8510 0.8510], 1e-4); %! close (fig); %!test %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotDiagnostics (ax, mdl, 'dffits'); %! yd = get (h(1), 'YData'); %! yv = get (h(2), 'YData'); %! xv = get (h(2), 'XData'); %! assert_equal (numel (h), 2); %! assert_equal (yd(1), 0.476480465355394, -1e-8); %! assert_equal (yd(2), 0.477020506700835, -1e-8); %! assert_equal (yd(3), -0.074329411030064, -1e-7); %! assert_equal (sum (isnan (xv)), 1); %! assert_equal (yv(1), -0.7745966692, -1e-8); %! assert_equal (yv(end), 0.7745966692, -1e-8); %! assert_equal (get (get (ax, 'ylabel'), 'string'), 'Scaled change in fit'); %! assert_equal (get (get (ax, 'title'), 'string'), 'Case order plot of scaled change in fit'); %! close (fig); %!test %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotDiagnostics (ax, mdl, 's2_i'); %! yd = get (h(1), 'YData'); %! assert_equal (numel (h), 2); %! assert_equal (yd(1), 0.02359100986, -1e-8); %! assert_equal (yd(2), 0.02314622330, -1e-8); %! assert_equal (yd(3), 0.02411685408, -1e-8); %! assert_equal (get (h(2), 'YData'), [0.02273796067, 0.02273796067], -1e-8); %! assert_equal (get (h(2), 'XData'), [0, n]); %! assert_equal (get (get (ax, 'ylabel'), 'string'), 'Leave-one-out variance'); %! assert_equal (get (get (ax, 'title'), 'string'), 'Case order plot of leave-one-out variance'); %! close (fig); %!test %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotDiagnostics (ax, mdl, 'contour'); %! yd = get (h(1), 'YData'); %! xd = get (h(1), 'XData'); %! assert_equal (numel (h), 2); %! assert_equal (xd(1), 0.3707792208, -1e-8); %! assert_equal (xd(2), 0.2452836637, -1e-8); %! assert_equal (yd(1), 0.07562471113, -1e-8); %! assert_equal (yd(2), 0.11059272450, -1e-8); %! assert_equal (get (h(1), 'LineStyle'), 'none'); %! assert_equal (get (h(1), 'Marker'), 'x'); %! assert_equal (isgraphics (h(2)), true); %! assert_equal (get (get (ax, 'xlabel'), 'string'), 'Leverage'); %! assert_equal (get (get (ax, 'ylabel'), 'string'), 'Residual'); %! assert_equal (get (get (ax, 'title'), 'string'), 'Cook''s distance factorization'); %! close (fig); %!test %! me = fitlm (X, y, 'Exclude', [2, 7]); %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h_ex = plotDiagnostics (ax, me, 'cookd'); %! h_un = plotDiagnostics (ax, mdl, 'cookd'); %! ref_ex = get (h_ex(2), 'YData'); %! ref_un = get (h_un(2), 'YData'); %! assert_equal (! isequal (ref_ex, ref_un), true); %! assert_equal (ref_ex(1), 3 * mean (me.Diagnostics.CooksDistance, 'omitnan'), 1e-12); %! close (fig); %!test %! mw = fitlm (X, y, 'Weights', (1:n)' / sum (1:n)); %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! hw = plotDiagnostics (ax, mw, 'leverage'); %! hu = plotDiagnostics (ax, mdl, 'leverage'); %! ydw = get (hw(1), 'YData'); %! ydu = get (hu(1), 'YData'); %! assert_equal (ydw(1) != ydu(1), true); %! assert_equal (! isequal (ydw, ydu), true); %! close (fig); %!test %! fig = figure ('visible', 'off'); %! h = plotDiagnostics (mdl); %! assert_equal (isequal (get (h(1), 'Parent'), gca ()), true); %! close (fig); %!test %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotEffects (ax, mdl); %! xd1 = get (h(1), 'XData'); %! yd1 = get (h(1), 'YData'); %! xd2 = get (h(2), 'XData'); %! yd2 = get (h(2), 'YData'); %! xd3 = get (h(3), 'XData'); %! yd3 = get (h(3), 'YData'); %! ytl = get (ax, 'YTickLabel'); %! assert_equal (numel (h), 3); %! assert_equal (xd1(1), 2.38302891604232, -1e-10); %! assert_equal (xd1(2), -19.5277648300125, -1e-10); %! assert_equal (yd1, [1 2]); %! assert_equal (xd2(1), 1.39673712385796, -1e-10); %! assert_equal (xd2(2), 3.36932070822668, -1e-10); %! assert_equal (yd2, [1 1]); %! assert_equal (xd3(1), -20.4857975891918, -1e-10); %! assert_equal (xd3(2), -18.5697320708331, -1e-10); %! assert_equal (yd3, [2 2]); %! assert_equal (get (h(1), 'Color'), [0.1490 0.5490 0.8660], 1e-4); %! assert_equal (get (h(2), 'Color'), [0.1490 0.5490 0.8660], 1e-4); %! assert_equal (get (h(3), 'Color'), [0.1490 0.5490 0.8660], 1e-4); %! assert_equal (get (h(1), 'Marker'), 'o'); %! assert_equal (get (h(1), 'LineStyle'), 'none'); %! assert_equal (get (h(2), 'LineStyle'), '-'); %! assert_equal (get (h(2), 'Marker'), 'none'); %! assert_equal (get (h(3), 'LineStyle'), '-'); %! assert_equal (get (h(3), 'Marker'), 'none'); %! assert_equal (mean (xd2), xd1(1), 1e-10); %! assert_equal (mean (xd3), xd1(2), 1e-10); %! assert_equal (get (get (ax, 'xlabel'), 'string'), 'Main Effect'); %! assert_equal (get (get (ax, 'ylabel'), 'string'), ''); %! assert_equal (get (get (ax, 'title'), 'string'), 'Main Effects Plot'); %! assert_equal (get (ax, 'YTick'), [1 2]); %! assert_equal (ytl{1}, 'x1: 0.05 to 1'); %! assert_equal (ytl{2}, 'x2: 0.05 to 20'); %! close (fig); %!test %! ## 3-predictor model %! X3 = [X, sin((1:n)' * pi / n)]; %! y3 = X3 * [3; -1; 2] + 0.1 * cos ((1:n)' * pi / 7); %! m3 = fitlm (X3, y3); %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotEffects (ax, m3); %! xd1 = get (h(1), 'XData'); %! yd1 = get (h(1), 'YData'); %! xd2 = get (h(2), 'XData'); %! yd2 = get (h(2), 'YData'); %! xd3 = get (h(3), 'XData'); %! yd3 = get (h(3), 'YData'); %! xd4 = get (h(4), 'XData'); %! yd4 = get (h(4), 'YData'); %! ytl = get (ax, 'YTickLabel'); %! assert_equal (numel (h), 4); %! assert_equal (xd1(1), 8.10687671732127, -1e-10); %! assert_equal (xd1(2), -25.4487243632125, -1e-10); %! assert_equal (xd1(3), 0.661302203942261, -1e-10); %! assert_equal (yd1, [1 2 3]); %! assert_equal (xd2(1), 0.565266595687836, -1e-10); %! assert_equal (xd2(2), 15.6484868389547, -1e-10); %! assert_equal (yd2, [1 1]); %! assert_equal (xd3(1), -33.3368582824351, -1e-10); %! assert_equal (xd3(2), -17.5605904439899, -1e-10); %! assert_equal (yd3, [2 2]); %! assert_equal (xd4(1), -1.25582490831999, -1e-10); %! assert_equal (xd4(2), 2.57842931620451, -1e-10); %! assert_equal (yd4, [3 3]); %! assert_equal (get (ax, 'YTick'), [1 2 3]); %! assert_equal (ytl{1}, 'x1: 0.05 to 1'); %! assert_equal (ytl{2}, 'x2: 0.05 to 20'); %! assert_equal (ytl{3}, 'x3: 1.22465e-16 to 1'); %! assert_equal (mean (xd2), xd1(1), 1e-10); %! assert_equal (mean (xd3), xd1(2), 1e-10); %! assert_equal (mean (xd4), xd1(3), 1e-10); %! close (fig); %!test %! me = fitlm (X, y, 'Exclude', [2, 7]); %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotEffects (ax, me); %! xd1 = get (h(1), 'XData'); %! yd1 = get (h(1), 'YData'); %! xd2 = get (h(2), 'XData'); %! yd2 = get (h(2), 'YData'); %! xd3 = get (h(3), 'XData'); %! yd3 = get (h(3), 'YData'); %! ytl = get (ax, 'YTickLabel'); %! assert_equal (numel (h), 3); %! assert_equal (xd1(1), 2.50035744908398, -1e-10); %! assert_equal (xd1(2), -19.5912988214488, -1e-10); %! assert_equal (yd1, [1 2]); %! assert_equal (xd2(1), 1.40421088339552, -1e-10); %! assert_equal (xd2(2), 3.59650401477245, -1e-10); %! assert_equal (yd2, [1 1]); %! assert_equal (xd3(1), -20.6333076647782, -1e-10); %! assert_equal (xd3(2), -18.5492899781194, -1e-10); %! assert_equal (yd3, [2 2]); %! assert_equal (ytl{1}, 'x1: 0.05 to 1'); %! assert_equal (ytl{2}, 'x2: 0.05 to 20'); %! assert_equal (mean (xd2), xd1(1), 1e-10); %! assert_equal (mean (xd3), xd1(2), 1e-10); %! close (fig); %!test %! mw = fitlm (X, y, 'Weights', (1:n)' / sum (1:n)); %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotEffects (ax, mw); %! xd1 = get (h(1), 'XData'); %! yd1 = get (h(1), 'YData'); %! xd2 = get (h(2), 'XData'); %! yd2 = get (h(2), 'YData'); %! xd3 = get (h(3), 'XData'); %! yd3 = get (h(3), 'YData'); %! ytl = get (ax, 'YTickLabel'); %! assert_equal (numel (h), 3); %! assert_equal (xd1(1), 2.51587141860715, -1e-10); %! assert_equal (xd1(2), -19.6411669663483, -1e-10); %! assert_equal (yd1, [1 2]); %! assert_equal (xd2(1), 1.08491557053384, -1e-10); %! assert_equal (xd2(2), 3.94682726668046, -1e-10); %! assert_equal (yd2, [1 1]); %! assert_equal (xd3(1), -20.8383905241664, -1e-10); %! assert_equal (xd3(2), -18.4439434085302, -1e-10); %! assert_equal (yd3, [2 2]); %! assert_equal (ytl{1}, 'x1: 0.05 to 1'); %! assert_equal (ytl{2}, 'x2: 0.05 to 20'); %! assert_equal (mean (xd2), xd1(1), 1e-10); %! assert_equal (mean (xd3), xd1(2), 1e-10); %! close (fig); %!test %! mni = fitlm (X, y, 'Intercept', false); %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotEffects (ax, mni); %! xd1 = get (h(1), 'XData'); %! yd1 = get (h(1), 'YData'); %! xd2 = get (h(2), 'XData'); %! yd2 = get (h(2), 'YData'); %! xd3 = get (h(3), 'XData'); %! yd3 = get (h(3), 'YData'); %! ytl = get (ax, 'YTickLabel'); %! assert_equal (numel (h), 3); %! assert_equal (xd1(1), 2.81335053251731, -1e-10); %! assert_equal (xd1(2), -19.8951125513936, -1e-10); %! assert_equal (yd1, [1 2]); %! assert_equal (xd2(1), 2.36234515544211, -1e-10); %! assert_equal (xd2(2), 3.26435590959250, -1e-10); %! assert_equal (yd2, [1 1]); %! assert_equal (xd3(1), -20.4919734818287, -1e-10); %! assert_equal (xd3(2), -19.2982516209584, -1e-10); %! assert_equal (yd3, [2 2]); %! assert_equal (ytl{1}, 'x1: 0.05 to 1'); %! assert_equal (ytl{2}, 'x2: 0.05 to 20'); %! assert_equal (mean (xd2), xd1(1), 1e-10); %! assert_equal (mean (xd3), xd1(2), 1e-10); %! close (fig); %!test %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotEffects (ax, mdl); %! assert_equal (isequal (get (h(1), 'Parent'), ax), true); %! assert_equal (get (h(1), 'XData'), [2.38302891604232, -19.5277648300125], -1e-10); %! close (fig); %!test %! fig = figure ('visible', 'off'); %! h = plotEffects (mdl); %! assert_equal (isequal (get (h(1), 'Parent'), gca ()), true); %! assert_equal (get (h(1), 'XData'), [2.38302891604232, -19.5277648300125], -1e-10); %! close (fig); %!test %! ## numeric predictor adjusted data %! fig = figure ('visible', 'off'); %! h = plotAdjustedResponse (mdl, 'x1'); %! assert_equal (get (h(1), 'XData'), X(:,1)', 1e-10); %! assert_equal (get (h(1), 'YData'), ... %! [-6.70590752804287, -6.54551694016554, -6.5544435914624, -6.59143572669715, ... %! -6.49138418414686, -6.21712299745016, -5.89359961368025, -5.69299878673044, ... %! -5.67643670865536, -5.73777106454765, -5.70118778736348, -5.48284370035446, ... %! -5.16795154710756, -4.93243578806991, -4.88118846293094, -4.95163193306634, ... %! -4.97125247389768, -4.81620859768203, -4.52519034621851, -4.26384784484646], 1e-8); %! xf = get (h(2), 'XData'); %! yf = get (h(2), 'YData'); %! assert_equal (numel (xf), 100); %! assert_equal (xf(1:5), ... %! [0.05, 0.0595959595959596, 0.0691919191919192, 0.0787878787878788, 0.0883838383838384], 1e-10); %! assert_equal (yf(1:5), ... %! [-6.78153223917696, -6.75746124002502, -6.73339024087308, -6.70931924172113, -6.68524824256919], 1e-8); %! assert_equal (xf(end-4:end), ... %! [0.961616161616162, 0.971212121212121, 0.980808080808081, 0.99040404040404, 1], 1e-10); %! assert_equal (yf(end-4:end), ... %! [-4.49478731974241, -4.47071632059047, -4.44664532143853, -4.42257432228658, -4.39850332313464], 1e-8); %! close (fig); %!test %! ## title and axis labels follow the standard convention %! fig = figure ('visible', 'off'); %! plotAdjustedResponse (mdl, 'x1'); %! assert_equal (get (get (gca, 'Title'), 'String'), 'Adjusted response plot'); %! assert_equal (get (get (gca, 'XLabel'), 'String'), 'x1'); %! assert_equal (get (get (gca, 'YLabel'), 'String'), 'Adjusted y'); %! close (fig); %!test %! ## ax routing and a second predictor %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotAdjustedResponse (ax, mdl, 'x2'); %! assert_equal (isequal (get (h(1), 'Parent'), ax), true); %! assert_equal (get (h(1), 'XData'), X(:,2)', 1e-8); %! assert_equal (get (h(1), 'YData'), ... %! [1.45980865498274, 1.34795136885775, 0.968893310576047, 0.463886235373945, ... %! -0.00196069502564064, -0.391481514261341, -0.829623669406342, -1.48857191435397, ... %! -2.42944244115883, -3.5460929349136, -4.66270932857441, -5.6954484453929, ... %! -6.72952302895603, -7.94085753971093, -9.43434401734702, -11.14740482324, ... %! -12.9075262328114, -14.5908667583184, -16.23611644156, -18.0089254078756], 1e-7); %! close (fig); %!test %! ## name-value arguments style the data points only %! fig = figure ('visible', 'off'); %! h = plotAdjustedResponse (mdl, 'x1', 'Marker', 's', 'MarkerSize', 10, 'Color', 'r'); %! assert_equal (get (h(1), 'Marker'), 's'); %! assert_equal (get (h(1), 'MarkerSize'), 10); %! assert_equal (get (h(1), 'Color'), [1 0 0]); %! assert_equal (get (h(2), 'Marker'), 'none'); %! close (fig); %!test %! yn = y; %! yn(3) = NaN; %! mn = fitlm (X, yn); %! fig = figure ('visible', 'off'); %! h = plotAdjustedResponse (mn, 'x1'); %! xd = get (h(1), 'XData'); %! yd = get (h(1), 'YData'); %! assert_equal (isnan (xd(3)), true); %! assert_equal (isnan (yd(3)), true); %! assert_equal (yd([1 2 4]), [-7.04679028889336, -6.88651148335619, -6.93287739924846], 1e-8); %! close (fig); %!test %! w = mod ((1:n)', 3) + 1; %! mw = fitlm (X, y, 'Weights', w); %! fig = figure ('visible', 'off'); %! h = plotAdjustedResponse (mw, 'x1'); %! assert_equal (get (h(1), 'YData'), ... %! [-6.66184168801736, -6.5023788020353, -6.51285162315763, -6.55200839614801, ... %! -6.45473995928354, -6.18388034620285, -5.86437700397912, -5.66841468650568, ... %! -5.65710958583715, -5.72431938706618, -5.69423002314892, -5.482998317337, ... %! -5.17583701321739, -4.9486705712372, -4.90639103108588, -4.98642075413911, ... %! -5.01624601581846, -4.87202532838101, -4.59244873362587, -4.34316635689238], 1e-7); %! close (fig); %!test %! ## robust regression %! mr = fitlm (X, y, 'RobustOpts', 'on'); %! fig = figure ('visible', 'off'); %! h = plotAdjustedResponse (mr, 'x1'); %! assert_equal (get (h(1), 'YData'), ... %! [-6.69986109212516, -6.53959779763556, -6.54873660457867, -6.58602575771814, ... %! -6.48635609533107, -6.2125616510561, -5.88958987196639, -5.68962551195529, ... %! -5.67378476307741, -5.7359253104254, -5.70023308695542, -5.48286491591908, ... %! -5.16903354090336, -4.93466342235539, -4.88464659996458, -4.95640543510664, ... %! -4.97742620320314, -4.82386741651113, -4.53441911682976, -4.27473142949835], 1e-7); %! close (fig); %!test %! ## numeric predictor averaged over a categorical predictor %! wt = [3504;3693;3436;3433;3449;3672;3705;3288;3092;2500;2700;3100]; %! yr = categorical ([70;70;70;70;70;76;76;76;82;82;82;82]); %! mg = [18;15;18;16;17;20;22;24;30;32;28;26]; %! tc = table (mg, wt, yr, 'VariableNames', {'MPG','Weight','Year'}); %! mc = fitlm (tc, 'MPG ~ Year + Weight'); %! fig = figure ('visible', 'off'); %! h = plotAdjustedResponse (mc, 'Weight'); %! assert_equal (get (h(1), 'XData'), wt', 1e-10); %! assert_equal (get (h(1), 'YData'), ... %! [22.1247351073949, 19.1247351073949, 22.1247351073949, 20.1247351073949, ... %! 21.1247351073949, 18.6102199722546, 20.6102199722546, 22.6102199722546, ... %! 25.8864161365654, 27.8864161365654, 23.8864161365654, 21.8864161365654], 1e-8); %! xf = get (h(2), 'XData'); %! yf = get (h(2), 'YData'); %! assert_equal (xf(1:5), ... %! [2500, 2512.17171717172, 2524.34343434343, 2536.51515151515, 2548.68686868687], 1e-8); %! assert_equal (yf(1:5), ... %! [26.9912481948118, 26.9176291703666, 26.8440101459214, 26.7703911214761, 26.6967720970309], 1e-8); %! close (fig); %!test %! ## categorical predictor evaluated per level %! wt = [3504;3693;3436;3433;3449;3672;3705;3288;3092;2500;2700;3100]; %! yr = categorical ([70;70;70;70;70;76;76;76;82;82;82;82]); %! mg = [18;15;18;16;17;20;22;24;30;32;28;26]; %! tc = table (mg, wt, yr, 'VariableNames', {'MPG','Weight','Year'}); %! mc = fitlm (tc, 'MPG ~ Year + Weight'); %! fig = figure ('visible', 'off'); %! h = plotAdjustedResponse (mc, 'Year'); %! assert_equal (get (h(1), 'XData'), [1 1 1 1 1 2 2 2 3 3 3 3]); %! assert_equal (get (h(1), 'YData'), ... %! [19.2479799272553, 17.3911214761304, 18.8366909043795, 16.8185458004291, ... %! 17.9153196881646, 22.2641057484776, 24.463701891932, 23.9415324428265, ... %! 28.7560523180671, 27.1754184718549, 24.3850920685482, 24.8044392619348], 1e-7); %! assert_equal (get (h(2), 'XData'), [1 2 3]); %! assert_equal (get (h(2), 'YData'), [18.0419315592718, 23.5564466944121, 26.2802505301012], 1e-8); %! assert_equal (get (gca, 'XTickLabel'), {'70'; '76'; '82'}); %! close (fig); %!test %! ## added variable plot for the whole model %! fig = figure ('visible', 'off'); %! h = plotAdded (mdl); %! assert_equal (get (get (gca, 'Title'), 'String'), 'Added variable plot for whole model'); %! assert_equal (get (get (gca, 'XLabel'), 'String'), 'Adjusted whole model'); %! assert_equal (get (h(1), 'XData'), ... %! [0.0284033824838481, 0.0204548552857604, -0.0238455815942649, -0.104497928156226, ... %! -0.221502184400124, -0.374858350325959, -0.564566425933731, -0.79062641122344, ... %! -1.05303830619508, -1.35180211084867, -1.68691782518418, -2.05838544920164, ... %! -2.46620498290103, -2.91037642628236, -3.39089977934563, -3.90777504209083, ... %! -4.46100221451797, -5.05058129662704, -5.67651228841805, -6.338795189891], 1e-7); %! assert_equal (get (h(1), 'YData'), ... %! [0.268294196961578, 0.281859485365135, 0.0282240016119717, -0.351360499061586, ... %! -0.691784854932629, -0.955883099639786, -1.26860268025624, -1.80212835067532, ... %! -2.61757630295165, -3.60880422217788, -4.59999804131014, -5.50731458360009, ... %! -6.41596659263467, -7.50187852886103, -8.86994243196858, -10.457580663333, ... %! -12.0922794983759, -13.6501974493543, -15.1700245580674, -16.8174109498545], 1e-7); %! assert_equal (get (h(2), 'DisplayName'), 'Fit: y = 2.69267*x'); %! close (fig); %!test %! ## added variable plot for just the intercept term %! fig = figure ('visible', 'off'); %! h = plotAdded (mdl, 1); %! assert_equal (get (get (gca, 'Title'), 'String'), 'Added variable plot for (Intercept)'); %! assert_equal (get (h(1), 'YData'), ... %! [-5.41993222738086, -5.40485070721962, -5.55724508427077, -5.73586360329798, ... %! -5.77559710257835, -5.63927961575051, -5.45185858988763, -5.38551877888305, ... %! -5.50137637479139, -5.6932890627053, -5.78544277558092, -5.6939943366699, ... %! -5.50415648955918, -5.3918536946959, -5.4619779917695, -5.65195174215564, ... %! -5.78926122127593, -5.75006494138741, -5.57305294428921, -5.42387535532067], 1e-7); %! assert_equal (get (h(2), 'DisplayName'), 'Fit: y = 0.116189*x'); %! close (fig); %!test %! ## added variable plot for one predictor picked by index %! fig = figure ('visible', 'off'); %! h = plotAdded (mdl, 2); %! assert_equal (get (get (gca, 'Title'), 'String'), 'Added variable plot for x1'); %! assert_equal (get (h(1), 'YData'), ... %! [-5.90289714234183, -5.75941203626873, -5.79651449057265, -5.87295275001727, ... %! -5.82361765287968, -5.6113432327985, -5.36107693684693, -5.24500351891828, ... %! -5.32423917106718, -5.49264157838629, -5.57439667383173, -5.48566128065516, ... %! -5.31164814244354, -5.22828171964398, -5.34045405194592, -5.58558750072505, ... %! -5.79116834140295, -5.83335508623668, -5.75083777702536, -5.70926653910833], 1e-7); %! assert_equal (get (h(2), 'DisplayName'), 'Fit: y = 2.50845*x'); %! xf = get (h(2), 'XData'); %! yf = get (h(2), 'YData'); %! assert_equal (xf(1:3), [0.370121951219512, 0.372449462532903, 0.374776973846294], 1e-9); %! assert_equal (yf(1:3), [-5.97852185347592, -5.97268340425253, -5.96684495502913], 1e-7); %! close (fig); %!test %! ## added variable plot for the other predictor, a negative slope this time %! fig = figure ('visible', 'off'); %! h = plotAdded (mdl, 3); %! assert_equal (get (get (gca, 'Title'), 'String'), 'Added variable plot for x2'); %! assert_equal (get (h(1), 'YData'), ... %! [-8.3040737600235, -7.38815394983204, -6.7394349117973, -6.21666489068295, ... %! -5.65473472476609, -5.01647844768535, -4.4268435065139, -4.05801465514508, ... %! -3.9711080856335, -4.05998148307182, -4.14882078041619, -4.15378280091823, ... %! -4.16008028816491, -4.34363770260337, -4.80934708392301, -5.49463079349955, ... %! -6.22697510675454, -6.88253853594507, -7.50001112287024, -8.2450429928694], 1e-7); %! assert_equal (get (h(2), 'DisplayName'), 'Fit: y = -0.978835*x'); %! close (fig); %!test %! ## ax argument sends the plot to that axes %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotAdded (ax, mdl, 2); %! assert_equal (isequal (get (h(1), 'Parent'), ax), true); %! close (fig); %!test %! ## name-value styling only changes the data points, not the fit line %! fig = figure ('visible', 'off'); %! h = plotAdded (mdl, 2, 'Marker', 's', 'MarkerSize', 10, 'Color', 'r'); %! assert_equal (get (h(1), 'Marker'), 's'); %! assert_equal (get (h(1), 'MarkerSize'), 10); %! assert_equal (get (h(1), 'Color'), [1 0 0]); %! assert_equal (get (h(2), 'Marker'), 'none'); %! close (fig); %!test %! ## a missing observation leaves a gap in the adjusted data %! yn = y; %! yn(3) = NaN; %! mn = fitlm (X, yn); %! fig = figure ('visible', 'off'); %! h = plotAdded (mn, 2); %! xd = get (h(1), 'XData'); %! yd = get (h(1), 'YData'); %! assert_equal (isnan (xd(3)), true); %! assert_equal (isnan (yd(3)), true); %! close (fig); %!test %! ## weighted fit still gives a proper added variable plot %! w = mod ((1:n)', 3) + 1; %! mw = fitlm (X, y, 'Weights', w); %! fig = figure ('visible', 'off'); %! h = plotAdded (mw, 2); %! assert_equal (get (h(1), 'YData'), ... %! [-6.04899282212585, -5.90562605001686, -5.9429257275943, -6.01964009962184, ... %! -5.97066000437658, -5.75881947549715, -5.50906596005672, -5.39358421194863, ... %! -5.4734904232275, -5.64264227898597, -5.7252257121802, -5.63739754606181, ... %! -5.46437052421778, -5.38206910709522, -5.49538533438357, -5.74174156745852, ... %! -5.94862408174164, -5.99219138949, -5.91113353250271, -5.87110063611913], 1e-7); %! assert_equal (get (h(2), 'DisplayName'), 'Fit: y = 2.38836*x'); %! close (fig); %!test %! ## robust fit still gives a proper added variable plot %! mr = fitlm (X, y, 'RobustOpts', 'on'); %! fig = figure ('visible', 'off'); %! h = plotAdded (mr, 2); %! assert_equal (get (h(1), 'YData'), ... %! [-5.89409568732029, -5.75060102429134, -5.78768755033552, -5.86410351021651, ... %! -5.81473974221138, -5.60243027995878, -5.35212257053188, -5.23600136782402, ... %! -5.31518286388981, -5.4835247438219, -5.56521294057644, -5.47640427740507, ... %! -5.30231149789475, -5.2188590624926, -5.33093901088806, -5.5759737044568, ... %! -5.78144941862042, -5.82352466563597, -5.74088948730258, -5.69919400895959], 1e-7); %! assert_equal (get (h(2), 'DisplayName'), 'Fit: y = 2.48815*x'); %! close (fig); %!test %! ## added variable plot for the whole model %! load carsmall %! Year = categorical (Model_Year); %! tbl = table (MPG, Weight, Year); %! mdl1 = fitlm (tbl, 'MPG ~ Year + Weight^2'); %! fig = figure ('visible', 'off'); %! h = plotAdded (mdl1); %! assert_equal (get (get (gca, 'Title'), 'String'), 'Added variable plot for whole model'); %! assert_equal (get (get (gca, 'XLabel'), 'String'), 'Adjusted whole model'); %! xd = get (h(1), 'XData'); %! yd = get (h(1), 'YData'); %! assert_equal (xd(1:5), ... %! [-4.54006581304461, -4.65629283432076, -4.49502736854252, ... %! -4.49300111649177, -4.50376945388336], 1e-7); %! assert_equal (yd(1:5), [18, 15, 18, 16, 17], 1e-8); %! assert_equal (get (h(2), 'DisplayName'), 'Fit: y = 8.44866*x'); %! xf = get (h(2), 'XData'); %! yf = get (h(2), 'YData'); %! assert_equal (xf(1:3), [-5.06005142297709, -5.03050027197254, -5.000949120968], 1e-7); %! assert_equal (yf(1:3), [11.4556384009199, 11.7053059986795, 11.9549735964392], 1e-7); %! close (fig); %!test %! ## selecting every non-intercept coefficient is the whole model; any %! ## other multi-coefficient selection is a list of specified terms %! X3 = [X, cos((1:n)')]; %! m3 = fitlm (X3, y); %! fig = figure ('visible', 'off'); %! plotAdded (m3, [2 3 4]); %! assert_equal (get (get (gca, 'Title'), 'String'), ... %! 'Added variable plot for whole model'); %! assert_equal (get (get (gca, 'XLabel'), 'String'), 'Adjusted whole model'); %! clf; %! plotAdded (m3, [2 3]); %! assert_equal (get (get (gca, 'Title'), 'String'), ... %! 'Added variable plot for specified terms'); %! close (fig); %!test %! ## added variable plot for the weight terms picked as a pair %! load carsmall %! Year = categorical (Model_Year); %! tbl = table (MPG, Weight, Year); %! mdl1 = fitlm (tbl, 'MPG ~ Year + Weight^2'); %! fig = figure ('visible', 'off'); %! h = plotAdded (mdl1, [2 5]); %! assert_equal (get (get (gca, 'Title'), 'String'), 'Added variable plot for specified terms'); %! assert_equal (get (get (gca, 'XLabel'), 'String'), 'Adjusted specified terms'); %! xd = get (h(1), 'XData'); %! yd = get (h(1), 'YData'); %! assert_equal (xd(1:5), ... %! [-2181.48546848357, -2241.34798193195, -2158.28850051435, ... %! -2157.24488312395, -2162.79109548355], 1e-6); %! assert_equal (yd(1:5), ... %! [24.0284299339692, 21.0284299339692, 24.0284299339692, ... %! 22.0284299339692, 23.0284299339692], 1e-7); %! assert_equal (get (h(2), 'DisplayName'), 'Fit: y = 0.0164036*x'); %! close (fig); %!test %! ## multiple predictors delegate to plotAdded %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h1 = plot (ax, mdl); %! h2 = plotAdded (ax, mdl); %! assert_equal (get (h1(1), 'XData'), get (h2(1), 'XData')); %! assert_equal (get (h1(1), 'YData'), get (h2(1), 'YData')); %! assert_equal (get (h1(2), 'YData'), get (h2(2), 'YData')); %! assert_equal (get (h1(3), 'YData'), get (h2(3), 'YData')); %! assert_equal (get (get (ax, 'Title'), 'String'), 'Added variable plot for whole model'); %! close (fig); %!test %! ## no axes argument uses the current axes %! fig = figure ('visible', 'off'); %! h = plot (mdl); %! assert_equal (isequal (get (h(1), 'Parent'), gca ()), true); %! close (fig); %!test %! ## no predictors delegate to plotResiduals %! mc = fitlm (X, y, 'constant'); %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h1 = plot (ax, mc); %! h2 = plotResiduals (ax, mc); %! assert_equal (get (h1(1), 'type'), 'patch'); %! assert_equal (get (h1(1), 'YData'), get (h2(1), 'YData')); %! assert_equal (get (get (ax, 'Title'), 'String'), 'Histogram of residuals'); %! close (fig); %!test %! ## single predictor: data, fit line, and confidence bounds %! m1 = fitlm (X(:,1), y); %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plot (ax, m1); %! assert_equal (numel (h), 3); %! assert_equal (get (h(1), 'XData'), X(:,1)', 1e-15); %! assert_equal (get (h(1), 'YData'), y', 1e-15); %! xf = get (h(2), 'XData'); %! yf = get (h(2), 'YData'); %! assert_equal (numel (xf), 100); %! assert_equal (xf(1:3), [0.05, 0.0595959595959596, 0.0691919191919192], 1e-12); %! assert_equal (xf(end-2:end), [0.980808080808081, 0.99040404040404, 1], 1e-12); %! assert_equal (yf(1:3), [2.98235017582927, 2.80917102518311, 2.63599187453694], 1e-7); %! assert_equal (yf(end-2:end), [-13.8160274368485, -13.9892065874947, -14.1623857381409], 1e-7); %! yb = get (h(3), 'YData'); %! assert_equal (numel (yb), 201); %! assert_equal (isnan (yb(101)), true); %! assert_equal (yb(1:3), [1.59215527128182, 1.43944293975561, 1.28661387851973], 1e-7); %! assert_equal (yb(102:104), [4.37254508037672, 4.1788991106106, 3.98536987055415], 1e-7); %! assert_equal (yb(end-2:end), [-12.4666494408313, -12.6194785020672, -12.7721908335934], 1e-7); %! assert_equal (yb(1:100) + yb(102:201), 2 * yf, 1e-10); %! [yp, ~] = predict (m1, xf'); %! assert_equal (yf', yp, 1e-10); %! assert_equal (get (get (ax, 'Title'), 'String'), 'y vs. x1'); %! assert_equal (get (get (ax, 'XLabel'), 'String'), 'x1'); %! assert_equal (get (get (ax, 'YLabel'), 'String'), 'y'); %! close (fig); %!test %! ## real dataset with missing rows leaves gaps in the data %! load carsmall %! tbl2 = table (MPG, Weight); %! mdl2 = fitlm (tbl2, 'MPG ~ Weight'); %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plot (ax, mdl2); %! xd = get (h(1), 'XData'); %! yd = get (h(1), 'YData'); %! assert_equal (numel (xd), 100); %! assert_equal (any (isnan (xd)), true); %! assert_equal (any (isnan (yd)), true); %! assert_equal (xd(1:3), [3504, 3693, 3436], 1e-10); %! assert_equal (xd(end-2:end), [2295, 2625, 2720], 1e-10); %! assert_equal (yd(1:3), [18, 15, 18], 1e-10); %! assert_equal (yd(end-2:end), [32, 28, 31], 1e-10); %! xf = get (h(2), 'XData'); %! yf = get (h(2), 'YData'); %! assert_equal (numel (xf), 100); %! assert_equal (xf(1:3), [1795, 1824.666667, 1854.333333], 1e-4); %! assert_equal (xf(end-2:end), [4672.666667, 4702.333333, 4732], 1e-4); %! assert_equal (yf(1:3), [33.77920696, 33.52371956, 33.26823216], 1e-6); %! assert_equal (yf(end-2:end), [8.996929296, 8.741441898, 8.485954499], 1e-6); %! yb = get (h(3), 'YData'); %! assert_equal (numel (yb), 201); %! assert_equal (yb(1:3), [32.2768265, 32.0472587, 31.81746955], 1e-6); %! assert_equal (yb(end-2:end), [11.0004001, 10.77351303, 10.54671087], 1e-6); %! assert_equal (get (get (ax, 'Title'), 'String'), 'MPG vs. Weight'); %! assert_equal (get (get (ax, 'XLabel'), 'String'), 'Weight'); %! assert_equal (get (get (ax, 'YLabel'), 'String'), 'MPG'); %! close (fig); %!test %! ## categorical predictor: group codes with per-level fit and bounds %! yv = [4.73087805313537; 7.43361607881479; 4.48799323173627; 5.16869512618961; ... %! 6.50195295169252; 3.73839298415678; 4.3512762614163; 7.45869429457297; ... %! 4.08830081430877; 5.37758996783874; 6.70425002011403; 4.92219481850025; ... %! 5.90846112458998; 6.93808332486038; 3.44469932246968; 4.65954705986638; ... %! 7.00708466319882; 3.97022469477047; 4.66949449276983; 7.15297545753449; ... %! 3.79547107710474; 4.35907258855759; 6.85754858526846; 3.96760657205439; ... %! 5.50019159441948; 6.59933535084439; 4.04590000762589; 5.13690239596885; ... %! 5.93326622029102; 4.20198864027471]; %! grp = categorical (repmat ({'A';'B';'C'}, 10, 1)); %! tbl4 = table (yv, grp, 'VariableNames', {'Response','Group'}); %! mdl4 = fitlm (tbl4, 'Response ~ Group'); %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plot (ax, mdl4); %! assert_equal (numel (h), 3); %! assert_equal (get (h(1), 'XData'), repmat ([1 2 3], 1, 10)); %! assert_equal (get (h(1), 'YData'), yv', 1e-14); %! assert_equal (get (h(2), 'XData'), [1 2 3]); %! assert_equal (get (h(2), 'YData'), [4.98621086647521, 6.85868069471919, 4.0662772163002], 1e-10); %! assert_equal (get (h(3), 'XData'), [1 2 3 NaN 1 2 3]); %! assert_equal (get (h(3), 'YData'), ... %! [4.68577486025387, 6.55824468849784, 3.76584121007885, NaN, ... %! 5.28664687269656, 7.15911670094053, 4.36671322252154], 1e-10); %! assert_equal (get (ax, 'XTick'), [1 2 3]); %! assert_equal (get (ax, 'XTickLabel'), {'A'; 'B'; 'C'}); %! assert_equal (get (get (ax, 'Title'), 'String'), 'Response vs. Group'); %! assert_equal (get (get (ax, 'XLabel'), 'String'), 'Group'); %! assert_equal (get (get (ax, 'YLabel'), 'String'), 'Response'); %! close (fig); %!test %! ## continuous by continuous, effects mode %! mi = fitlm (X, y, 'y ~ x1*x2'); %! fig = figure ('visible', 'off'); %! h = plotInteraction (mi, 'x1', 'x2'); %! assert_equal (numel (h), 11); %! assert_equal (get (h(1), 'XData'), [1.76843380852813, -18.8740348676059], 1e-9); %! assert_equal (get (h(1), 'YData'), [1, 7]); %! assert_equal (get (h(2), 'XData'), [-2.25617028020123, 5.79303789725749], 1e-9); %! assert_equal (get (h(3), 'XData'), [-23.1321080641968, -14.615961671015], 1e-9); %! assert_equal (get (h(4), 'XData'), ... %! [1.97967308209488, 1.68393809910143, 1.38820311610799], 1e-9); %! assert_equal (get (h(4), 'YData'), [2, 3, 4]); %! assert_equal (get (h(5), 'XData'), [-0.771057026434185, 4.73040319062394], 1e-9); %! assert_equal (get (h(6), 'XData'), [-2.86059582662229, 6.22847202482516], 1e-9); %! assert_equal (get (h(7), 'XData'), [-4.99614754441031, 7.77255377662629], 1e-9); %! assert_equal (get (h(8), 'XData'), ... %! [-18.5782998846125, -18.8740348676059, -19.1697698505994], 1e-9); %! assert_equal (get (h(8), 'YData'), [8, 9, 10]); %! assert_equal (get (h(9), 'XData'), [-24.674318784563, -12.4822809846619], 1e-9); %! assert_equal (get (h(10), 'XData'), [-23.1321080641968, -14.615961671015], 1e-9); %! assert_equal (get (h(11), 'XData'), [-21.6439971135684, -16.6955425876303], 1e-9); %! assert_equal (get (h(1), 'Tag'), 'main'); %! assert_equal (get (h(4), 'Tag'), 'conditional1'); %! assert_equal (get (h(8), 'Tag'), 'conditional2'); %! ax = gca (); %! assert_equal (get (get (ax, 'Title'), 'String'), 'Interaction of x1 and x2'); %! assert_equal (get (get (ax, 'XLabel'), 'String'), 'Effect'); %! assert_equal (get (ax, 'YTick'), [1, 2, 3, 4, 7, 8, 9, 10]); %! assert_equal (get (ax, 'YTickLabel'), ... %! {'x1: 0.05 to 1'; 'x2=0.05'; 'x2=10.025'; 'x2=20'; ... %! 'x2: 0.05 to 20'; 'x1=0.05'; 'x1=0.525'; 'x1=1'}); %! assert_equal (get (ax, 'YLim'), [0.5, 10.5]); %! close (fig); %!test %! ## continuous by continuous, predictions mode %! mi = fitlm (X, y, 'y ~ x1*x2'); %! fig = figure ('visible', 'off'); %! h = plotInteraction (mi, 'x1', 'x2', 'predictions'); %! assert_equal (numel (h), 3); %! xd = get (h(1), 'XData'); %! assert_equal (numel (xd), 101); %! assert_equal (xd(1:3), [0.05, 0.2495, 0.449], 1e-9); %! assert_equal (xd(end-2:end), [19.601, 19.8005, 20], 1e-9); %! yd1 = get (h(1), 'YData'); %! assert_equal (yd1(1:3), ... %! [0.215349913094656, 0.0295669142485309, -0.156216084597594], 1e-9); %! assert_equal (yd1(end-2:end), ... %! [-17.9913839738256, -18.1771669726717, -18.3629499715178], 1e-9); %! yd2 = get (h(2), 'YData'); %! assert_equal (yd2(1:3), [1.20518645414209, 1.01644610546604, 0.827705756789976], 1e-9); %! assert_equal (yd2(end-2:end), ... %! [-17.2913677161117, -17.4801080647878, -17.6688484134638], 1e-9); %! yd3 = get (h(3), 'YData'); %! assert_equal (yd3(1:3), [2.19502299518953, 2.00332529668354, 1.81162759817755], 1e-9); %! assert_equal (yd3(end-2:end), ... %! [-16.5913514583978, -16.7830491569038, -16.9747468554098], 1e-9); %! assert_equal (get (h(1), 'DisplayName'), '0.05'); %! assert_equal (get (h(2), 'DisplayName'), '0.525'); %! assert_equal (get (h(3), 'DisplayName'), '1'); %! ax = gca (); %! assert_equal (get (get (ax, 'Title'), 'String'), 'Interaction of x1 and x2'); %! assert_equal (get (get (ax, 'XLabel'), 'String'), 'x2'); %! assert_equal (get (get (ax, 'YLabel'), 'String'), 'Adjusted y'); %! close (fig); %!test %! ## swapping var1/var2 order swaps roles and title %! mi = fitlm (X, y, 'y ~ x1*x2'); %! fig = figure ('visible', 'off'); %! h = plotInteraction (mi, 'x2', 'x1'); %! assert_equal (numel (h), 11); %! assert_equal (get (h(1), 'XData'), [-18.8740348676059, 1.76843380852813], 1e-9); %! assert_equal (get (h(4), 'XData'), ... %! [-18.5782998846125, -18.8740348676059, -19.1697698505994], 1e-9); %! assert_equal (get (h(8), 'XData'), ... %! [1.97967308209488, 1.68393809910143, 1.38820311610799], 1e-9); %! ax = gca (); %! assert_equal (get (get (ax, 'Title'), 'String'), 'Interaction of x2 and x1'); %! assert_equal (get (ax, 'YTickLabel'), ... %! {'x2: 0.05 to 20'; 'x1=0.05'; 'x1=0.525'; 'x1=1'; ... %! 'x1: 0.05 to 1'; 'x2=0.05'; 'x2=10.025'; 'x2=20'}); %! close (fig); %!test %! ## interaction effects: variables given as indices into VariableNames %! mi = fitlm (X, y, 'y ~ x1*x2'); %! fig = figure ('visible', 'off'); %! h = plotInteraction (mi, 1, 2); %! assert_equal (numel (h), 11); %! assert_equal (get (h(1), 'XData'), [1.76843380852813, -18.8740348676059], 1e-9); %! assert_equal (get (h(1), 'YData'), [1, 7]); %! ax = gca (); %! assert_equal (get (get (ax, 'Title'), 'String'), 'Interaction of x1 and x2'); %! close (fig); %!test %! ## interaction effects: explicit axes argument is honored %! mi = fitlm (X, y, 'y ~ x1*x2'); %! fig = figure ('visible', 'off'); %! axtarget = axes (fig); %! h = plotInteraction (axtarget, mi, 'x1', 'x2'); %! assert_equal (numel (h), 11); %! assert_equal (isequal (get (h(1), 'Parent'), axtarget), true); %! assert_equal (isequal (gca (), axtarget), true); %! close (fig); %!test %! ## no interaction term: conditional effects collapse to the main effect %! mn = fitlm (X, y, 'y ~ x1 + x2'); %! fig = figure ('visible', 'off'); %! h = plotInteraction (mn, 'x1', 'x2'); %! xd1 = get (h(1), 'XData'); %! eff1 = xd1(1); %! eff2 = xd1(2); %! assert_equal (eff1, 2.38302891604232, 1e-9); %! assert_equal (eff2, -19.5277648300125, 1e-9); %! assert_equal (get (h(4), 'XData'), [eff1, eff1, eff1], 1e-9); %! assert_equal (get (h(8), 'XData'), [eff2, eff2, eff2], 1e-9); %! close (fig); %!test %! ## categorical by continuous, effects mode %! xc = (1:30)' / 30; %! grp = categorical (repmat ({'A';'B';'C'}, 10, 1)); %! yv = 2*xc + 3*double (grp == 'B') - 1*double (grp == 'C') + ... %! 1.5*xc.*double (grp == 'B') + 0.3*sin ((1:30)'); %! tblc = table (yv, xc, grp, 'VariableNames', {'Response','Xc','Group'}); %! mdlc = fitlm (tblc, 'Response ~ Xc*Group'); %! fig = figure ('visible', 'off'); %! h = plotInteraction (mdlc, 'Group', 'Xc'); %! assert_equal (numel (h), 11); %! assert_equal (get (h(1), 'XData'), [4.7896970899464, 2.32528157787528], 1e-9); %! assert_equal (get (h(2), 'XData'), [4.56862113373247, 5.01077304616034], 1e-9); %! assert_equal (get (h(3), 'XData'), [2.02254960835685, 2.62801354739371], 1e-9); %! assert_equal (get (h(4), 'XData'), ... %! [4.08328517685389, 4.7896970899464, 5.49610900303892], 1e-9); %! assert_equal (get (h(5), 'XData'), [3.64076372661607, 4.52580662709171], 1e-9); %! assert_equal (get (h(6), 'XData'), [4.56862113373247, 5.01077304616034], 1e-9); %! assert_equal (get (h(7), 'XData'), [5.07555715247022, 5.91666085360761], 1e-9); %! assert_equal (get (h(8), 'XData'), ... %! [1.88553612240401, 3.25156621870343, 1.8387423925184], 1e-9); %! assert_equal (get (h(9), 'XData'), [1.36118897012269, 2.40988327468532], 1e-9); %! assert_equal (get (h(10), 'XData'), [2.72721906642211, 3.77591337098474], 1e-9); %! assert_equal (get (h(11), 'XData'), [1.31439524023708, 2.36308954479972], 1e-9); %! ax = gca (); %! assert_equal (get (get (ax, 'Title'), 'String'), 'Interaction of Group and Xc'); %! assert_equal (get (ax, 'YTick'), [1, 2, 3, 4, 7, 8, 9, 10]); %! assert_equal (get (ax, 'YTickLabel'), ... %! {'Group: C to B'; 'Xc=0.0333333'; 'Xc=0.516667'; 'Xc=1'; ... %! 'Xc: 0.033333 to 1'; 'Group=A'; 'Group=B'; 'Group=C'}); %! close (fig); %!test %! ## categorical by continuous, predictions mode %! xc = (1:30)' / 30; %! grp = categorical (repmat ({'A';'B';'C'}, 10, 1)); %! yv = 2*xc + 3*double (grp == 'B') - 1*double (grp == 'C') + ... %! 1.5*xc.*double (grp == 'B') + 0.3*sin ((1:30)'); %! tblc = table (yv, xc, grp, 'VariableNames', {'Response','Xc','Group'}); %! mdlc = fitlm (tblc, 'Response ~ Xc*Group'); %! fig = figure ('visible', 'off'); %! h = plotInteraction (mdlc, 'Group', 'Xc', 'predictions'); %! assert_equal (numel (h), 3); %! xd = get (h(1), 'XData'); %! assert_equal (numel (xd), 101); %! assert_equal (xd(1:3), [0.0333333333333333, 0.043, 0.0526666666666667], 1e-9); %! assert_equal (xd(end-2:end), [0.980666666666667, 0.990333333333333, 1], 1e-9); %! yd1 = get (h(1), 'YData'); %! assert_equal (yd1(1:3), [0.107201421526318, 0.126056782750359, 0.144912143974399], 1e-9); %! assert_equal (yd1(end-2:end), [1.95502682148225, 1.97388218270629, 1.99273754393033], 1e-9); %! yd2 = get (h(2), 'YData'); %! assert_equal (yd2(1:3), [3.18658823804771, 3.21910390023474, 3.25161956242178], 1e-9); %! assert_equal (yd2(end-2:end), [6.37312313237707, 6.4056387945641, 6.43815445675114], 1e-9); %! yd3 = get (h(3), 'YData'); %! assert_equal (yd3(1:3), [-0.896696938806178, -0.878309514880994, -0.85992209095581], 1e-9); %! assert_equal (yd3(end-2:end), [0.905270605861853, 0.923658029787037, 0.942045453712221], 1e-9); %! assert_equal (get (h(1), 'DisplayName'), 'A'); %! assert_equal (get (h(2), 'DisplayName'), 'B'); %! assert_equal (get (h(3), 'DisplayName'), 'C'); %! ax = gca (); %! assert_equal (get (get (ax, 'Title'), 'String'), 'Interaction of Group and Xc'); %! assert_equal (get (get (ax, 'XLabel'), 'String'), 'Xc'); %! assert_equal (get (get (ax, 'YLabel'), 'String'), 'Adjusted Response'); %! close (fig); %!test %! ## default h on shared fixture, no polynomial hierarchy present %! t = anova (mdl); %! assert_equal (t.Properties.RowNames, {'x1'; 'x2'; 'Error'}); %! assert_equal (t.SumSq(1), 0.590865029026992, -1e-9); %! assert_equal (t.DF(1), 1); %! assert_equal (t.MeanSq(1), 0.590865029026992, -1e-9); %! assert_equal (t.F(1), 25.9858409295, -1e-8); %! assert_equal (t.pValue(1), 8.93779416897245e-05, -1e-8); %! assert_equal (t.SumSq(2), 42.0518254818947, -1e-9); %! assert_equal (t.DF(2), 1); %! assert_equal (t.MeanSq(2), 42.0518254818947, -1e-9); %! assert_equal (t.F(2), 1849.41059985747, -1e-7); %! assert_equal (t.pValue(2), 8.65693830575066e-19, -1e-8); %! assert_equal (t.SumSq(3), 0.386545331386823, -1e-9); %! assert_equal (t.DF(3), 17); %! assert_equal (t.MeanSq(3), 0.0227379606698131, -1e-9); %! assert_equal (isnan (t.F(3)), true); %! assert_equal (isnan (t.pValue(3)), true); %!test %! ## explicit Type 2 on shared fixture reaches the identical result as h %! t = anova (mdl, 'components', 2); %! assert_equal (t.SumSq(1), 0.590865029026992, -1e-9); %! assert_equal (t.MeanSq(1), 0.590865029026992, -1e-9); %! assert_equal (t.F(1), 25.9858409295, -1e-8); %! assert_equal (t.pValue(1), 8.93779416897245e-05, -1e-8); %! assert_equal (t.SumSq(2), 42.0518254818947, -1e-9); %! assert_equal (t.MeanSq(2), 42.0518254818947, -1e-9); %! assert_equal (t.F(2), 1849.41059985747, -1e-7); %! assert_equal (t.pValue(2), 8.65693830575066e-19, -1e-8); %! assert_equal (t.SumSq(3), 0.386545331386823, -1e-9); %! assert_equal (t.MeanSq(3), 0.0227379606698131, -1e-9); %!test %! ## explicit Type 1 on shared fixture, order-dependent x1 row differs from h %! t = anova (mdl, 'components', 1); %! assert_equal (t.SumSq(1), 541.472049189064, -1e-9); %! assert_equal (t.MeanSq(1), 541.472049189064, -1e-9); %! assert_equal (t.F(1), 23813.5713686901, -1e-7); %! assert_equal (t.pValue(1), 3.41699381539186e-28, -1e-8); %! assert_equal (t.SumSq(2), 42.0518254818947, -1e-9); %! assert_equal (t.F(2), 1849.41059985747, -1e-7); %! assert_equal (t.SumSq(3), 0.386545331386823, -1e-9); %! assert_equal (t.DF(3), 17); %!test %! ## summary table on shared fixture, no Linear/Nonlinear split %! t = anova (mdl, 'summary'); %! assert_equal (t.Properties.RowNames, {'Total'; 'Model'; 'Residual'}); %! assert_equal (t.SumSq(1), 583.910420002346, -1e-9); %! assert_equal (t.DF(1), 19); %! assert_equal (t.MeanSq(1), 30.7321273685445, -1e-9); %! assert_equal (isnan (t.F(1)), true); %! assert_equal (t.SumSq(2), 583.523874670959, -1e-9); %! assert_equal (t.DF(2), 2); %! assert_equal (t.MeanSq(2), 291.761937335479, -1e-9); %! assert_equal (t.F(2), 12831.4909842738, -1e-7); %! assert_equal (t.pValue(2), 9.48988083209278e-28, -1e-8); %! assert_equal (t.SumSq(3), 0.386545331386823, -1e-9); %! assert_equal (t.DF(3), 17); %! assert_equal (t.MeanSq(3), 0.0227379606698131, -1e-9); %!test %! ## polynomial hierarchy, h and Type 2 diverge only on the Age row %! Age = [25;31;42;29;55;38;46;33;27;50;41;36;48;30;44]; %! Sex = categorical ({'M';'F';'F';'M';'M';'F';'M';'F';'F';'M';'F';'M';'F';'M';'F'}); %! BP = [118;122;135;120;150;128;140;124;119;145;130;126;138;121;136]; %! T = table (Age, Sex, BP); %! m = fitlm (T, 'BP ~ Sex + Age^2'); %! t = anova (m); %! t2 = anova (m, 'components', 2); %! assert_equal (t.Properties.RowNames, {'Age'; 'Sex'; 'Age^2'; 'Error'}); %! assert_equal (t.SumSq(1), 1363.92365918468, -1e-9); %! assert_equal (t.DF(1), 1); %! assert_equal (t.MeanSq(1), 1363.92365918468, -1e-9); %! assert_equal (t.F(1), 707.420098987802, -1e-7); %! assert_equal (t.pValue(1), 2.46488912775244e-11, -1e-8); %! assert_equal (t.SumSq(2), 1.90509548061236, -1e-9); %! assert_equal (t.F(2), 0.988107233422164, -1e-9); %! assert_equal (t.pValue(2), 0.341568882808456, -1e-9); %! assert_equal (t.SumSq(3), 8.58235117456131, -1e-9); %! assert_equal (t.F(3), 4.45136916320193, -1e-8); %! assert_equal (t.pValue(3), 0.0585944334523874, -1e-9); %! assert_equal (t.SumSq(4), 21.2082753550521, -1e-9); %! assert_equal (t.DF(4), 11); %! assert_equal (t.MeanSq(4), 1.92802503227746, -1e-9); %! assert_equal (t2.SumSq(1), 0.209727728295816, -1e-8); %! assert_equal (t2.F(1), 0.108778529731057, -1e-9); %! assert_equal (t2.pValue(1), 0.747734511582268, -1e-9); %! assert_equal (t2.SumSq(2), 1.90509548061236, -1e-9); %! assert_equal (t2.SumSq(3), 8.58235117456131, -1e-9); %! assert_equal (t2.SumSq(4), 21.2082753550521, -1e-9); %!test %! ## summary table with Linear/Nonlinear split, same polynomial model %! Age = [25;31;42;29;55;38;46;33;27;50;41;36;48;30;44]; %! Sex = categorical ({'M';'F';'F';'M';'M';'F';'M';'F';'F';'M';'F';'M';'F';'M';'F'}); %! BP = [118;122;135;120;150;128;140;124;119;145;130;126;138;121;136]; %! T = table (Age, Sex, BP); %! m = fitlm (T, 'BP ~ Sex + Age^2'); %! t = anova (m, 'summary'); %! assert_equal (t.Properties.RowNames, ... %! {'Total'; 'Model'; '. Linear'; '. Nonlinear'; 'Residual'}); %! assert_equal (t.SumSq(1), 1415.73333333333, -1e-9); %! assert_equal (t.DF(1), 14); %! assert_equal (t.MeanSq(1), 101.12380952381, -1e-9); %! assert_equal (t.SumSq(2), 1394.52505797828, -1e-9); %! assert_equal (t.DF(2), 3); %! assert_equal (t.F(2), 241.097329241452, -1e-7); %! assert_equal (t.pValue(2), 2.58928117898032e-10, -1e-8); %! assert_equal (t.SumSq(3), 1385.94270680372, -1e-9); %! assert_equal (t.DF(3), 2); %! assert_equal (t.F(3), 359.420309280577, -1e-7); %! assert_equal (t.pValue(3), 9.54785035758658e-11, -1e-8); %! assert_equal (t.SumSq(4), 8.58235117456131, -1e-9); %! assert_equal (t.DF(4), 1); %! assert_equal (t.F(4), 4.45136916320193, -1e-8); %! assert_equal (t.pValue(4), 0.0585944334523874, -1e-9); %! assert_equal (t.SumSq(5), 21.2082753550521, -1e-9); %! assert_equal (t.DF(5), 11); %! assert_equal (t.MeanSq(5), 1.92802503227746, -1e-9); %!test %! ## unbalanced categorical interaction, hierarchical model, default h %! grpA = categorical ([1;1;2;1;2;1;1;2;1;1;2;1;2;2;1;1;2;1;1;2]); %! grpB = categorical ([1;2;1;1;2;2;1;1;2;1;1;2;2;1;2;1;1;2;2;1]); %! xv = (1:20)' / 10; %! yv = 2*(grpA=='2') + 1.5*(grpB=='2') + 0.7*xv + ... %! 0.9*(grpA=='2').*(grpB=='2') + ... %! [0.1;-0.2;0.05;0.15;-0.1;0.2;-0.05;0.1;0.0;-0.15; ... %! 0.1;0.05;-0.2;0.15;0.0;-0.1;0.05;0.2;-0.05;0.1]; %! T = table (grpA, grpB, xv, yv); %! m = fitlm (T, 'yv ~ grpA*grpB'); %! t = anova (m); %! t2 = anova (m, 'components', 1); %! assert_equal (t.Properties.RowNames, {'grpA'; 'grpB'; 'grpA:grpB'; 'Error'}); %! assert_equal (t.SumSq(1), 26.0224323327616, -1e-9); %! assert_equal (t.F(1), 138.504723473419, -1e-8); %! assert_equal (t.pValue(1), 2.72592668860896e-09, -1e-8); %! assert_equal (t.SumSq(2), 15.2365308176101, -1e-9); %! assert_equal (t.F(2), 81.0966269640544, -1e-8); %! assert_equal (t.pValue(2), 1.15578182042777e-07, -1e-8); %! assert_equal (t.SumSq(3), 0.0142868014375564, -1e-8); %! assert_equal (t.F(3), 0.0760416803903897, -1e-8); %! assert_equal (t.pValue(3), 0.786264832724038, -1e-9); %! assert_equal (t.SumSq(4), 3.00609904761905, -1e-9); %! assert_equal (t.DF(4), 16); %! assert_equal (t.MeanSq(4), 0.18788119047619, -1e-9); %! assert_equal (t2.SumSq(1), 16.3540833333333, -1e-9); %! assert_equal (t2.F(1), 87.0448142886652, -1e-8); %! assert_equal (t2.pValue(1), 7.14746180184104e-08, -1e-8); %! assert_equal (t2.SumSq(2), 15.2365308176101, -1e-9); %! assert_equal (t2.SumSq(3), 0.0142868014375564, -1e-8); %!test %! ## non-hierarchical model, missing grpB main effect, h still succeeds %! grpA = categorical ([1;1;2;1;2;1;1;2;1;1;2;1;2;2;1;1;2;1;1;2]); %! grpB = categorical ([1;2;1;1;2;2;1;1;2;1;1;2;2;1;2;1;1;2;2;1]); %! xv = (1:20)' / 10; %! yv = 2*(grpA=='2') + 1.5*(grpB=='2') + 0.7*xv + ... %! 0.9*(grpA=='2').*(grpB=='2') + ... %! [0.1;-0.2;0.05;0.15;-0.1;0.2;-0.05;0.1;0.0;-0.15; ... %! 0.1;0.05;-0.2;0.15;0.0;-0.1;0.05;0.2;-0.05;0.1]; %! T = table (grpA, grpB, xv, yv); %! m = fitlm (T, 'yv ~ grpA + grpA:grpB'); %! t = anova (m); %! assert_equal (t.Properties.RowNames, {'grpA'; 'grpA:grpB'; 'Error'}); %! assert_equal (t.SumSq(1), 16.3540833333333, -1e-9); %! assert_equal (t.F(1), 22.0110535802411, -1e-8); %! assert_equal (t.pValue(1), 0.000209917579961884, -1e-8); %! assert_equal (t.SumSq(2), 5.62601666666667, -1e-9); %! assert_equal (t.F(2), 7.57208776360619, -1e-8); %! assert_equal (t.pValue(2), 0.0136181560209473, -1e-9); %! assert_equal (t.SumSq(3), 12.6309, -1e-9); %! assert_equal (t.DF(3), 17); %! assert_equal (t.MeanSq(3), 0.742994117647059, -1e-9); %!test %! ## no-intercept model %! grpA = categorical ([1;1;2;1;2;1;1;2;1;1;2;1;2;2;1;1;2;1;1;2]); %! grpB = categorical ([1;2;1;1;2;2;1;1;2;1;1;2;2;1;2;1;1;2;2;1]); %! xv = (1:20)' / 10; %! yv = 2*(grpA=='2') + 1.5*(grpB=='2') + 0.7*xv + ... %! 0.9*(grpA=='2').*(grpB=='2') + ... %! [0.1;-0.2;0.05;0.15;-0.1;0.2;-0.05;0.1;0.0;-0.15; ... %! 0.1;0.05;-0.2;0.15;0.0;-0.1;0.05;0.2;-0.05;0.1]; %! T = table (grpA, grpB, xv, yv); %! m = fitlm (T, 'yv ~ grpA + grpB - 1'); %! t = anova (m); %! assert_equal (t.SumSq(1), 63.3745141509434, -1e-8); %! assert_equal (t.F(1), 178.349190204051, -1e-7); %! assert_equal (t.pValue(1), 3.91187977275845e-12, -1e-8); %! assert_equal (t.SumSq(2), 15.2365308176101, -1e-8); %! assert_equal (t.F(2), 85.7575941763448, -1e-7); %! assert_equal (t.pValue(2), 4.71596005128041e-08, -1e-8); %! assert_equal (t.SumSq(3), 3.02038584905660, -1e-8); %! assert_equal (t.DF(3), 17); %! assert_equal (t.MeanSq(3), 0.177669755826859, -1e-9); %!test %! ## weighted fit %! grpA = categorical ([1;1;2;1;2;1;1;2;1;1;2;1;2;2;1;1;2;1;1;2]); %! grpB = categorical ([1;2;1;1;2;2;1;1;2;1;1;2;2;1;2;1;1;2;2;1]); %! xv = (1:20)' / 10; %! yv = 2*(grpA=='2') + 1.5*(grpB=='2') + 0.7*xv + ... %! 0.9*(grpA=='2').*(grpB=='2') + ... %! [0.1;-0.2;0.05;0.15;-0.1;0.2;-0.05;0.1;0.0;-0.15; ... %! 0.1;0.05;-0.2;0.15;0.0;-0.1;0.05;0.2;-0.05;0.1]; %! T = table (grpA, grpB, xv, yv); %! w = [1.2;0.8;1.5;1.0;0.9;1.1;1.3;0.7;1.0;1.4; ... %! 0.8;1.2;1.0;1.1;0.9;1.3;0.7;1.5;1.0;1.2]; %! m = fitlm (T, 'yv ~ grpA + grpB', 'Weights', w); %! t = anova (m); %! assert_equal (t.SumSq(1), 26.6364861423312, -1e-9); %! assert_equal (t.F(1), 134.770391630163, -1e-8); %! assert_equal (t.pValue(1), 1.66729622333192e-09, -1e-8); %! assert_equal (t.SumSq(2), 17.4880599694593, -1e-9); %! assert_equal (t.F(2), 88.4828681359069, -1e-8); %! assert_equal (t.pValue(2), 3.76674631526712e-08, -1e-8); %! assert_equal (t.SumSq(3), 3.35993877395756, -1e-9); %! assert_equal (t.DF(3), 17); %! assert_equal (t.MeanSq(3), 0.197643457291621, -1e-9); %!test %! grp3 = categorical ([1;1;1;1;1;2;2;2;2;2;2;2;2;2;3;3;3;3;3;3]); %! yy = [2.1;1.9;2.3;2.0;1.8;4.1;4.3;3.9;4.0;4.2; ... %! 4.4;3.8;4.1;4.0;6.1;5.9;6.3;6.0;5.8;6.2]; %! T = table (grp3, yy); %! m = fitlm (T, 'yy ~ grp3'); %! t = anova (m); %! assert_equal (t.Properties.RowNames, {'grp3'; 'Error'}); %! assert_equal (t.DF(1), 2); %! assert_equal (t.SumSq(1), 44.3761111111111, -1e-9); %! assert_equal (t.MeanSq(1), 22.1880555555556, -1e-9); %! assert_equal (t.F(1), 616.446794988197, -1e-7); %! assert_equal (t.pValue(1), 1.36582477075565e-16, -1e-8); %! assert_equal (t.SumSq(2), 0.611888888888889, -1e-9); %! assert_equal (t.DF(2), 17); %! assert_equal (t.MeanSq(2), 0.0359934640522876, -1e-9); %!test %! ## type 3 gives different numbers than type 2 here %! G3 = categorical ([1;1;1;1;2;2;2;3;3;3;3;1;2;3]); %! G2 = categorical ([1;1;2;2;1;2;2;1;1;2;2;2;1;1]); %! y1 = [10;12;15;14;9;11;13;16;18;20;22;17;10;19]; %! T1 = table (G3, G2, y1); %! mdl1 = fitlm (T1, 'y1 ~ G3*G2'); %! t1 = anova (mdl1, 'components', 3); %! assert_equal (t1.Properties.RowNames, {'G3'; 'G2'; 'G3:G2'; 'Error'}); %! assert_equal (t1.SumSq(1), 176.678431372549, -1e-9); %! assert_equal (t1.F(1), 44.6345510835913, -1e-8); %! assert_equal (t1.pValue(1), 4.57572925304662e-05, -1e-8); %! assert_equal (t1.SumSq(2), 38.7604166666666, -1e-9); %! assert_equal (t1.F(2), 19.5842105263158, -1e-8); %! assert_equal (t1.pValue(2), 0.00221050293676794, -1e-9); %! assert_equal (t1.SumSq(3), 1.85490196078431, -1e-9); %! assert_equal (t1.SumSq(4), 15.8333333333333, -1e-9); %! assert_equal (t1.DF(4), 8); %! assert_equal (t1.MeanSq(4), 1.97916666666667, -1e-9); %!test %! ## no intercept; every level is coded, so the error row equals mdl.SSE %! G = categorical ([1;1;1;2;2;2;3;3;3;3]); %! y1 = [10;12;11;20;22;19;15;17;16;14]; %! T = table (G, y1); %! mdl1 = fitlm (T, 'y1 ~ G - 1'); %! t = anova (mdl1, 'components', 3); %! assert_equal (mdl1.SSE, 11.6666666666667, -1e-9); %! assert_equal (t.SumSq(1), 2564.33333333333, -1e-8); %! assert_equal (t.DF(1), 3); %! assert_equal (t.F(1), 512.866666666667, -1e-7); %! assert_equal (t.pValue(1), 1.45297790506045e-08, -1e-8); %! assert_equal (t.SumSq(2), 11.6666666666667, -1e-9); %! assert_equal (t.DF(2), 7); %! assert_equal (t.MeanSq(2), 1.66666666666667, -1e-9); ## An intercept-only model has no degrees of freedom to test, so the p-value ## of its model row is NaN. It used to reach betainc, which wants both its ## shape parameters strictly positive and refused the zero. %!test %! y0 = [3; 4; 5; 4; 6; 5; 7; 6]; %! x0 = (1:8)'; %! t = anova (fitlm (table (x0, y0), 'y0 ~ 1'), 'summary'); %! assert_equal (t.DF(2), 0); %! assert_equal (isnan (t.F(2)), true); %! assert_equal (isnan (t.pValue(2)), true); %!test # and the components table answers for it too %! y0 = [3; 4; 5; 4; 6; 5; 7; 6]; %! x0 = (1:8)'; %! t = anova (fitlm (table (x0, y0), 'y0 ~ 1'), 'components', 3); %! assert_equal (t.Properties.RowNames, {'Error'}); %! assert_equal (isnan (t.pValue(1)), true); ## A term that adds no rank must show no sum of squares however the two ## residual sums of squares it is the difference of happened to round. The ## fixture below left 1.8e-15 there, so the mean square came back Inf and the ## F with it, and a neighbouring one left it negative and gave -Inf. %!test %! G1 = categorical ([1;1;1;2;2;2;3;3;3;3]); %! G2 = G1; %! y1 = [10; 12; 11; 22; 24; 21; 15; 17; 16; 14]; %! t = anova (fitlm (table (G1, G2, y1), 'y1 ~ G1 + G2'), 'components', 3); %! assert_equal (t.SumSq(1), 0); %! assert_equal (t.DF(1), 0); %! assert_equal (isnan (t.MeanSq(1)), true); %! assert_equal (isnan (t.F(1)), true); %! assert_equal (isnan (t.pValue(1)), true); %!test %! ## duplicate group column, so type 3 should show zero DF %! G1 = categorical ([1;1;1;2;2;2;3;3;3;3]); %! G2 = G1; %! y1 = [10;12;11;20;22;19;15;17;16;14]; %! T = table (G1, G2, y1); %! mdl1 = fitlm (T, 'y1 ~ G1 + G2'); %! t = anova (mdl1, 'components', 3); %! assert_equal (t.Properties.RowNames, {'G1'; 'G2'; 'Error'}); %! assert_equal (t.SumSq(1), 0, -1e-9); %! assert_equal (t.DF(1), 0); %! assert_equal (isnan (t.F(1)), true); %! assert_equal (isnan (t.pValue(1)), true); %! assert_equal (t.SumSq(2), 0, -1e-9); %! assert_equal (t.DF(2), 0); %! assert_equal (t.SumSq(3), 11.6666666666667, -1e-9); %! assert_equal (t.DF(3), 7); %! assert_equal (t.MeanSq(3), 1.66666666666667, -1e-9); %!test %! ## just an intercept, so only the error row shows up %! y1 = [5.1;4.9;5.2;4.8;5.1;4.9;5.2;4.8;5.1;4.9]; %! T = table (y1); %! mdl1 = fitlm (T, 'y1 ~ 1'); %! t = anova (mdl1, 'components', 3); %! assert_equal (t.Properties.RowNames, {'Error'}); %! assert_equal (t.SumSq(1), 0.22, -1e-9); %! assert_equal (t.DF(1), 9); %! assert_equal (t.MeanSq(1), 0.0244444444444444, -1e-9); %!test %! ## type 3 with a robust fit %! G = categorical ([1;1;1;1;2;2;2;2;3;3;3;3]); %! x = (1:12)' / 2; %! y1 = 5 + 2*(G=='2') + 4*(G=='3') + 0.3*x; %! y1(10) = y1(10) + 20; %! T = table (G, x, y1); %! mdl1 = fitlm (T, 'y1 ~ G + x', 'RobustOpts', 'on'); %! t = anova (mdl1, 'components', 3); %! assert_equal (t.SumSq(1), 3.35664335664336, -1e-8); %! assert_equal (t.F(1), 0.0801017164653529, -1e-8); %! assert_equal (t.pValue(1), 0.923753304031669, -1e-9); %! assert_equal (t.SumSq(2), 0.337499999999999, -1e-8); %! assert_equal (t.F(2), 0.0161079545454545, -1e-8); %! assert_equal (t.pValue(2), 0.902138027698746, -1e-9); %! assert_equal (t.SumSq(3), 167.619047619048, -1e-7); %! assert_equal (t.DF(3), 8); %! assert_equal (t.MeanSq(3), 20.9523809523809, -1e-8); %!test %! ## repeated x values, so lack of fit rows should show up %! x = [1;1;1;2;3;3;4;5;5;5;6;7]; %! y1 = [10.1;9.9;10.3;12.0;15.2;14.8;17.5;20.1;19.9;20.3;22.0;25.5]; %! mdl1 = fitlm (x, y1); %! t = anova (mdl1, 'summary'); %! assert_equal (t.Properties.RowNames, ... %! {'Total'; 'Model'; 'Residual'; '. Lack of fit'; '. Pure error'}); %! assert_equal (t.SumSq(3), 1.03026642984015, -1e-9); %! assert_equal (t.DF(3), 10); %! assert_equal (t.SumSq(4), 0.790266429840146, -1e-9); %! assert_equal (t.DF(4), 5); %! assert_equal (t.F(4), 3.2927767910006, -1e-9); %! assert_equal (t.pValue(4), 0.108434644423991, -1e-9); %! assert_equal (t.SumSq(5), 0.24, -1e-9); %! assert_equal (t.DF(5), 5); %! assert_equal (t.MeanSq(5), 0.0480000000000001, -1e-9); %!test %! ## same as above but weighted %! x = repmat ((1:4)', 6, 1); %! G = categorical (repmat ([1;1;2;2], 6, 1)); %! y1 = 3 + 0.7*x + 2*(G=='2') + ... %! [0.1;-0.1;0.2;-0.2;0.15;-0.15;0.1;-0.1;-0.05;0.05;0.2;-0.2; ... %! 0.1;-0.1;-0.15;0.15;0.2;-0.2;0.05;-0.05;-0.1;0.1;0.15;-0.15]; %! w = 1 + mod ((0:23)', 3); %! T = table (x, G, y1); %! mdl1 = fitlm (T, 'y1 ~ x + G', 'Weights', w); %! t = anova (mdl1, 'summary'); %! assert_equal (t.Properties.RowNames, ... %! {'Total'; 'Model'; 'Residual'; '. Lack of fit'; '. Pure error'}); %! assert_equal (t.SumSq(3), 0.626927083333335, -1e-9); %! assert_equal (t.DF(3), 21); %! assert_equal (t.SumSq(4), 0.00880208333333332, -1e-9); %! assert_equal (t.DF(4), 1); %! assert_equal (t.F(4), 0.284799460734748, -1e-9); %! assert_equal (t.pValue(4), 0.599454252702211, -1e-9); %! assert_equal (t.SumSq(5), 0.618125000000001, -1e-9); %! assert_equal (t.DF(5), 20); %! assert_equal (t.MeanSq(5), 0.0309062500000001, -1e-9); %!test %! w = (1:n)'; %! mdl0 = fitlm (X, y, 'Weights', w); %! m = step (mdl0, 'Upper', 'quadratic', 'Verbose', 0); %! assert_equal (m.NumObservations, 20); %! assert_equal (m.NumCoefficients, 3); %! assert_equal (m.DFE, 17); %! assert_equal (m.SSE, 4.16523310465, 1e-8); %! assert_equal (m.CoefficientNames, {'(Intercept)','x1','x2'}); %! assert_equal (m.Coefficients.Estimate, [0.080321; 2.6483; -0.98452], 1e-4); %! assert_equal (m.Formula.LinearPredictor, '1 + x1 + x2'); %!test %! mdl0 = fitlm (X, y, 'Exclude', [3 7 15]); %! m = step (mdl0, 'Upper', 'quadratic', 'Verbose', 0); %! assert_equal (m.NumObservations, 17); %! assert_equal (m.NumCoefficients, 3); %! assert_equal (m.DFE, 14); %! assert_equal (m.SSE, 0.340968585251, 1e-8); %! assert_equal (m.CoefficientNames, {'(Intercept)','x1','x2'}); %! assert_equal (m.Coefficients.Estimate, [0.13263; 2.3566; -0.97211], 1e-4); %! assert_equal (m.Formula.LinearPredictor, '1 + x1 + x2'); %!test %! ## 'step' continues the history it inherits rather than beginning a new %! ## one, and reports the model it stepped from as the start. %! s1 = ((1:48)' - 24.5)/12; %! s2 = sin ((1:48)'/5); %! s3 = cos ((1:48)'/7); %! sy = 4 + 2.5*s1 - 1.1*s2 + 0.6*s1.^2 + 0.2*sin ((1:48)'/3); %! m1 = stepwiselm ([s1, s2, s3], sy, 'constant', 'Upper', 'linear', ... %! 'Verbose', 0); %! m2 = step (m1, 'Upper', 'quadratic', 'NSteps', 1, 'Verbose', 0); %! assert_equal (size (m1.Steps.History, 1), 4); %! assert_equal (size (m2.Steps.History, 1), 5); %! assert_equal (m2.Steps.History.TermName, ... %! {'1'; 'x1'; 'x2'; 'x3'; 'x1:x3'}); %! assert_equal (m2.Steps.History.DF, (1:5)'); %! assert_equal (char (m2.Steps.Start), 'y ~ 1 + x1 + x2 + x3'); %!test %! ## Stepping a model that was not fitted stepwise starts the history at %! ## that model, and leaves the thresholds unreported. %! s1 = ((1:48)' - 24.5)/12; %! s2 = sin ((1:48)'/5); %! s3 = cos ((1:48)'/7); %! sy = 4 + 2.5*s1 - 1.1*s2 + 0.6*s1.^2 + 0.2*sin ((1:48)'/3); %! m0 = fitlm ([s1, s2, s3], sy); %! m1 = step (m0, 'Upper', 'linear', 'NSteps', 1, 'Verbose', 0); %! assert_equal (isempty (m0.Steps), true); %! assert_equal (size (m1.Steps.History, 1), 1); %! assert_equal (m1.Steps.History.TermName{1}, '1 + x1 + x2 + x3'); %! assert_equal (m1.Steps.History.DF, 4); %! assert_equal (isempty (m1.Steps.PEnter), true); %! assert_equal (isempty (m1.Steps.PRemove), true); %!test %! ## An unasked-for criterion is inherited from the search being continued, %! ## and the appended step is scored by it. %! s1 = ((1:48)' - 24.5)/12; %! s2 = sin ((1:48)'/5); %! s3 = cos ((1:48)'/7); %! sy = 4 + 2.5*s1 - 1.1*s2 + 0.6*s1.^2 + 0.2*sin ((1:48)'/3); %! m1 = stepwiselm ([s1, s2, s3], sy, 'constant', 'Upper', 'linear', ... %! 'Criterion', 'bic', 'Verbose', 0); %! m2 = step (m1, 'Upper', 'quadratic', 'NSteps', 1, 'Verbose', 0); %! assert_equal (m2.Steps.Criterion, 'bic'); %! assert_equal (m2.Steps.History.Properties.VariableNames, ... %! {'Action', 'TermName', 'Terms', 'DF', 'delDF', 'BIC'}); %! assert_equal (m2.Steps.History.TermName, ... %! {'1'; 'x1'; 'x2'; 'x3'; 'x1:x3'}); %! assert_equal (m2.Steps.History.BIC, ... %! [245.562553617099; 116.628368564602; 109.023785194728; ... %! -6.28640176660815; -83.7225948440224], 1e-9); %!test %! ## Stepping a model that holds a power term is not a 'PredictorVars' %! ## conflict: the factor 'x1^2' names the variable 'x1'. %! s1 = ((1:48)' - 24.5)/12; %! s2 = sin ((1:48)'/5); %! sy = 4 + 2.5*s1 - 1.1*s2 + 0.6*s1.^2 + 0.2*sin ((1:48)'/3); %! m1 = stepwiselm ([s1, s2], sy, 'y ~ x1 + x1^2', 'Upper', 'quadratic', ... %! 'NSteps', 0, 'Verbose', 0); %! m2 = step (m1, 'Upper', 'quadratic', 'NSteps', 1, 'Verbose', 0); %! assert_equal (any (strcmp (m2.CoefficientNames, 'x1^2')), true); %!test %! ## So is the lower bound, which would otherwise silently widen to the %! ## constant model and let a protected term be dropped. %! s1 = ((1:48)' - 24.5)/12; %! s2 = sin ((1:48)'/5); %! s3 = cos ((1:48)'/7); %! sy = 4 + 2.5*s1 - 1.1*s2 + 0.6*s1.^2 + 0.2*sin ((1:48)'/3); %! m1 = stepwiselm ([s1, s2, s3], sy, 'y ~ x1 + x2', 'Lower', 'y ~ x2', ... %! 'Upper', 'quadratic', 'NSteps', 1, 'Verbose', 0); %! m2 = step (m1, 'Upper', 'quadratic', 'NSteps', 1, 'Verbose', 0); %! assert_equal (char (m1.Steps.Lower), 'y ~ 1 + x2'); %! assert_equal (char (m2.Steps.Lower), 'y ~ 1 + x2'); %! assert_equal (m2.Steps.History.TermName, {'1 + x1 + x2'; 'x1^2'; 'x2^2'}); %!test %! mdl0 = fitlm (X, y, 'Intercept', false); %! m = step (mdl0, 'Upper', 'quadratic', 'Verbose', 0); %! assert_equal (m.NumObservations, 20); %! assert_equal (m.NumCoefficients, 2); %! assert_equal (m.DFE, 18); %! assert_equal (m.SSE, 0.410934843408, 1e-8); %! assert_equal (m.Formula.HasIntercept, false); %! assert_equal (m.CoefficientNames, {'x1','x2'}); %! assert_equal (m.Coefficients.Estimate, [2.9614; -0.99725], 1e-4); %! assert_equal (m.Formula.LinearPredictor, 'x1 + x2'); %!test %! load hald %! Xh = ingredients; %! yh = heat; %! mdlh = fitlm (Xh, yh); %! assert_equal (mdlh.Coefficients.Estimate, ... %! [62.405369299918; 1.55110264750845; 0.510167579684912; ... %! 0.101909403579662; -0.144061029071018], 1e-9); %! assert_equal (mdlh.Coefficients.SE, ... %! [70.0709592085362; 0.744769867130993; 0.72378800183518; ... %! 0.754709045051309; 0.70905206344651], 1e-9); %! assert_equal (mdlh.Coefficients.tStat, ... %! [0.890602469336764; 2.0826603169159; 0.704857746178952; ... %! 0.135031379639465; -0.203174120065001], 1e-8); %! assert_equal (mdlh.Coefficients.pValue, ... %! [0.399133563385561; 0.0708216874297252; 0.500901103474289; ... %! 0.895922690510107; 0.844071473291884], 1e-8); %! assert_equal (mdlh.CoefficientNames, {'(Intercept)', 'x1', 'x2', 'x3', 'x4'}); %! assert_equal (mdlh.NumCoefficients, 5); %! assert_equal (mdlh.NumEstimatedCoefficients, 5); %! assert_equal (mdlh.DFE, 8); %! assert_equal (mdlh.SSE, 47.863639350499, 1e-8); %! assert_equal (mdlh.SSR, 2667.89943757258, 1e-6); %! assert_equal (mdlh.SST, 2715.76307692308, 1e-6); %! assert_equal (mdlh.MSE, 5.98295491881254, 1e-9); %! assert_equal (mdlh.RMSE, 2.44600795559061, 1e-9); %! assert_equal (mdlh.Rsquared.Ordinary, 0.98237562040768, 1e-9); %! assert_equal (mdlh.Rsquared.Adjusted, 0.97356343061152, 1e-9); %! assert_equal (mdlh.LogLikelihood, -26.918344895826, 1e-8); %! assert_equal (mdlh.ModelCriterion.AIC, 63.8366897916521, 1e-7); %! assert_equal (mdlh.ModelCriterion.AICc, 72.4081183630806, 1e-7); %! assert_equal (mdlh.ModelCriterion.BIC, 66.6614365789598, 1e-7); %! assert_equal (mdlh.ModelCriterion.CAIC, 71.6614365789598, 1e-7); %! assert_equal (mdlh.Fitted(1:5), ... %! [78.4952395815018; 72.7887993002909; 105.970937532083; ... %! 89.3271002550427; 95.649244438227], 1e-8); %! assert_equal (mdlh.Residuals.Raw(1:5), ... %! [0.00476041849822195; 1.51120069970906; -1.67093753208295; ... %! -1.72710025504266; 0.250755561773033], 1e-8); %! assert_equal (mdlh.Residuals.Pearson(1:5), ... %! [0.00194619910672879; 0.617823297040002; -0.683128412670876; ... %! -0.706089385807266; 0.102516249466771], 1e-8); %! assert_equal (mdlh.Residuals.Studentized(1:5), ... %! [0.00271470565323249; 0.734526653667679; -1.05809320265782; ... %! -0.824036396702643; 0.119767490249399], 1e-8); %! assert_equal (mdlh.Residuals.Standardized(1:5), ... %! [0.00290214088954622; 0.756624558354514; -1.05027405557414; ... %! -0.841081414787206; 0.127905848829164], 1e-8); %! assert_equal (mdlh.Diagnostics.Leverage(1:5), ... %! [0.550284813713987; 0.333242829857405; 0.576942476415795; ... %! 0.29523667959374; 0.357601364034465], 1e-8); %! assert_equal (mdlh.Diagnostics.CooksDistance(1:5), ... %! [2.06118491039641e-06; 0.0572247602223712; 0.300862709270433; ... %! 0.0592697490074745; 0.00182140011900327], 1e-8); %! assert_equal (mdlh.Diagnostics.Dffits(1:5), ... %! [0.00300294746488125; 0.519283016757055; -1.23563576459509; ... %! -0.533347058289184; 0.0893585735072963], 1e-7); %! assert_equal (mdlh.Diagnostics.S2_i(1:5), ... %! [6.83765556564705; 6.34835899957971; 5.89485540180634; ... %! 6.23302709557492; 6.82367982420565], 1e-7); %! assert_equal (mdlh.Diagnostics.CovRatio(1:5), ... %! [4.33530738335252; 2.01725612858557; 2.19476339013102; ... %! 1.74129811023362; 3.00406926806094], 1e-6); %! assert_equal (coefCI (mdlh), ... %! [-99.178552392689 223.989290992525; -0.166339745871082 3.26854504088797; ... %! -1.15889054555817 2.179225704928; -1.63845277518465 1.84227158234397; ... %! -1.77913801945372 1.49101596131168], 1e-7); %! assert_equal (coefCI (mdlh, 0.1), ... %! [-67.8949453842232 192.705683984059; 0.166167302672858 2.93603799234403; ... %! -0.835750978716108 1.85608613808593; -1.30150832005232 1.50532712721164; ... %! -1.46257740216021 1.17445534401817], 1e-7); %! [p, F, r] = coefTest (mdlh); %! assert_equal (p, 4.75618174559791e-07, 1e-12); %! assert_equal (F, 111.479171821258, 1e-6); %! assert_equal (r, 4); %! [dw, pdw] = dwtest (mdlh); %! assert_equal (dw, 0.842123108585363, 1e-9); %! assert_equal (pdw, 2.05259693286049, 1e-8); %!test %! load hald %! Xq = ingredients; %! yq = heat; %! mdlq = fitlm (Xq, yq, 'purequadratic'); %! assert_equal (mdlq.Coefficients.Estimate, ... %! [-210.864812527187; 4.01775196981369; 5.27927179495849; 4.98703005469684; ... %! 1.18967556414545; -0.00475259718542981; -0.0278555063988026; ... %! -0.0885225308739459; 0.0125632231921875], 1e-8); %! assert_equal (mdlq.Coefficients.SE, ... %! [62.2492955088454; 0.709968572684776; 0.928573133899706; 1.15325562045609; ... %! 0.559753134550446; 0.0119258668009245; 0.00570193167405062; ... %! 0.0224063172978396; 0.0040139580330777], 1e-8); %! assert_equal (mdlq.Coefficients.tStat, ... %! [-3.38742488253901; 5.6590560827508; 5.68535918413584; 4.3243058748108; ... %! 2.12535757410437; -0.398511677579811; -4.88527537528597; ... %! -3.95078449069724; 3.12988404180068], 1e-7); %! assert_equal (mdlq.Coefficients.pValue, ... %! [0.0275955163014823; 0.00480588842249351; 0.00472567672152044; ... %! 0.0124052274432915; 0.100732674864514; 0.710609610173011; ... %! 0.00812965117162355; 0.0168069450605605; 0.0351891921636521], 1e-7); %! assert_equal (mdlq.CoefficientNames, ... %! {'(Intercept)', 'x1', 'x2', 'x3', 'x4', 'x1^2', 'x2^2', 'x3^2', 'x4^2'}); %! assert_equal (mdlq.NumCoefficients, 9); %! assert_equal (mdlq.DFE, 4); %! assert_equal (mdlq.Rsquared.Ordinary, 0.998060528051984, 1e-9); %! assert_equal (mdlq.Rsquared.Adjusted, 0.994181584155951, 1e-9); %! assert_equal (mdlq.SSE, 5.26714630515049, 1e-7); %! assert_equal (mdlq.SSR, 2710.49593061793, 1e-6); %! assert_equal (mdlq.SST, 2715.76307692308, 1e-6); %! Xnewq = mean (Xq, 1); %! [ypredq, yciq] = predict (mdlq, Xnewq); %! assert_equal (ypredq, 101.90428629036, 1e-7); %! assert_equal (yciq, [97.996264336243, 105.812308244477], 1e-6); %! [ypredq2, yciq2] = predict (mdlq, Xnewq, 'Alpha', 0.01); %! assert_equal (ypredq2, 101.90428629036, 1e-7); %! assert_equal (yciq2, [95.4237317847484, 108.384840795971], 1e-6); %! yfeq = feval (mdlq, Xnewq(1), Xnewq(2), Xnewq(3), Xnewq(4)); %! assert_equal (yfeq, 101.90428629036, 1e-7); %! ysimq = random (mdlq, Xnewq); %! assert_equal (isscalar (ysimq), true); %! assert_equal (isnumeric (ysimq), true); %! mdlq2 = removeTerms (mdlq, 'x1^2'); %! assert_equal (mdlq2.Coefficients.Estimate, ... %! [180.815670525319; 0.228012666917658; -2.0654020024496; -1.90840414992816; ... %! -0.0108001957000414; 0.0257318558607856; 0.00699693273966028], 1e-8); %! assert_equal (mdlq2.CoefficientNames, ... %! {'(Intercept)', 'x2', 'x3', 'x4', 'x2^2', 'x3^2', 'x4^2'}); %! assert_equal (mdlq2.NumCoefficients, 7); %! mdlq3 = addTerms (mdlq2, 'x1^2'); %! assert_equal (mdlq3.Coefficients.Estimate, mdlq.Coefficients.Estimate, 1e-8); %! assert_equal (mdlq3.CoefficientNames, mdlq.CoefficientNames); %! assert_equal (mdlq3.SSE, 5.26714630515049, 1e-7); %!test %! load hald %! Xr = ingredients; %! yr = heat; %! mdlr = fitlm (Xr, yr, 'RobustOpts', 'bisquare'); %! assert_equal (mdlr.Coefficients.Estimate, ... %! [60.0897358816096; 1.57529551556915; 0.532199192097796; ... %! 0.133455378556458; -0.120521170556001], 1e-8); %! assert_equal (mdlr.Coefficients.SE, ... %! [75.8175597390933; 0.805849306629754; 0.783146694256936; ... %! 0.816603608044244; 0.767202244491812], 1e-8); %! assert_equal (mdlr.Coefficients.tStat, ... %! [0.792556976093573; 1.95482642053437; 0.679565138945976; ... %! 0.163427368238162; -0.157091785668371], 1e-7); %! assert_equal (mdlr.Coefficients.pValue, ... %! [0.450897370203866; 0.0863457969332376; 0.515957116726031; ... %! 0.874235088124976; 0.879064839096153], 1e-7); %! assert_equal (mdlr.CoefficientNames, {'(Intercept)', 'x1', 'x2', 'x3', 'x4'}); %! assert_equal (is_function_handle (mdlr.Robust.RobustWgtFun), true); %! assert_equal (mdlr.Robust.Tune, 4.685, 1e-10); %! assert_equal (size (mdlr.Robust.Weights), [13, 1]); %! assert_equal (isnumeric (mdlr.Robust.Weights), true); %! assert_equal (mdlr.SSE, 56.0362670671825, 1e-6); %! assert_equal (mdlr.MSE, 7.00453338339782, 1e-8); %! assert_equal (mdlr.RMSE, 2.64660790133291, 1e-8); %! assert_equal (mdlr.Rsquared.Ordinary, 0.97929734395902, 1e-9); %! assert_equal (mdlr.Rsquared.Adjusted, 0.96894601593853, 1e-9); %! assert_equal (mdlr.DFE, 8); %! H = [0 1 -1 0 0]; %! [p1, F1, r1] = coefTest (mdlr, H); %! assert_equal (p1, 0.00308748318894346, 1e-11); %! assert_equal (F1, 17.4568343157849, 1e-7); %! assert_equal (r1, 1); %! [pd1, dw1] = dwtest (mdlr, 'exact', 'both'); %! assert_equal (pd1, 0.844119247360191, 1e-9); %! assert_equal (dw1, 2.05387711905232, 1e-8); %! [pd2, dw2] = dwtest (mdlr, 'approximate', 'right'); %! assert_equal (pd2, 0.425180546504485, 1e-9); %! assert_equal (dw2, 2.05387711905232, 1e-8); %! Xnewr = mean (Xr, 1); %! [ypredr, ycir] = predict (mdlr, Xnewr, 'Simultaneous', true); %! assert_equal (ypredr, 95.4263340097424, 1e-7); %! assert_equal (ycir, [92.2744598719279, 98.5782081475569], 1e-6); %! [ypredr2, ycir2] = predict (mdlr, Xnewr, 'Simultaneous', true, 'Alpha', 0.1); %! assert_equal (ypredr2, 95.4263340097424, 1e-7); %! assert_equal (ycir2, [92.7161333049321, 98.1365347145527], 1e-6); %!warning ... %! m = addTerms (mdl, 'x1'); %!warning ... %! m = removeTerms (mdl, 'x1:x2'); %!error fitlm (X, y, 'NotAKey', 1) %!error fitlm (X, y, 'VarNames', {'a','b','c','d'}) %!error fitlm (X, y, [1 2 3 4; 5 6 7 8]) %!error fitlm (X, y, [1 2 1; 0 1 1]) %!error fitlm (NaN (5, 2), NaN (5, 1)) %!error fitlm (NaN (3, 2), [1; 2; 3]) %!error fitlm ([1 2; 3 4; 5 6], NaN (3, 1)) %!error fitlm (X, y, 'Exclude', (1:n)') %!error fitlm () %!error fitlm ('hello', y) %!error fitlm ({'a';'b'}, [1; 2]) %!error fitlm (X) %!error fitlm (X, 'Weights', [1;1;1]) %!error fitlm (X, [1; 2]) %!error fitlm (X, [1 2]) %!error mdl(1) %!error mdl {1} %!error predict (mdl, [0.5 0.25], 'BadOption', 1) %!error predict (mdl, [0.5 0.25], 'Alpha', -0.1) %!error predict (mdl, [0.5 0.25], 'Alpha', 1.5) %!error predict (mdl, [0.5 0.25], 'Alpha', [0.01 0.05]) %!error predict (mdl, [0.5 0.25], 'Prediction', 'bad') %!error predict (mdl, ones (3, 5)) %!error predict (mdl, ones (3, 1)) %!error predict (mdl, table ([1;2], 'VariableNames', {'z'})) %!error random (mdl) %!error random (mdl, [0.5, 0.25], 'extra') %!error random (mdl, ones (3, 5)) %!error random (mdl, []) %!error feval (mdl) %!error feval (mdl, [0.5; 1.0], [0.25; 1.0], [0.1; 0.2]) %!error feval (mdl, ones (3, 1)) %!error feval (mdl, [0.5; 1.0; 0.2], [0.25; 1.0]) %!error feval (mdl, table ([1; 2], 'VariableNames', {'z'})) %!error feval (mdl, []) %!error feval (mdl, '0.5', 0.25) %!error coefCI (mdl, 0.05, 'extra') %!error coefCI (mdl, 1.5) %!error coefCI (mdl, -0.1) %!error coefCI (mdl, [0.01 0.05]) %!error coefCI (mdl, NaN) %!error coefCI (mdl, 'abc') %!error coefTest (mdl, [1 0]) %!error coefTest (mdl, 'abc') %!error coefTest (mdl, [0 1 0], 'abc') %!error coefTest (mdl, [0 1 0; 0 0 1], [1]) %!error coefTest (mdl, [0 NaN 0]) %!error coefTest (mdl, [0 1 0], 0, 'extra') %!error [a, b, c, d] = coefTest (mdl) %!error dwtest (mdl, 'badmethod', 'both') %!error dwtest (mdl, 123, 'both') %!error dwtest (mdl, 'exact', 'both', 'extra') %!error [a, b, c] = dwtest (mdl) %!error addTerms (mdl) %!error addTerms (mdl, 'x1:x2', 'extra') %!error addTerms (mdl, 'z') %!error addTerms (mdl, 'X1') %!error addTerms (mdl, 'x1:z') %!error addTerms (mdl, 'x1*z') %!error addTerms (mdl, [1, 1, 1, 0]) %!error addTerms (mdl, []) %!error addTerms (mdl, {}) %!error removeTerms (mdl) %!error removeTerms (mdl, 'x1', 'extra') %!error removeTerms (mdl, 'z1') %!error removeTerms (mdl, [0 1 0 0]) %!error removeTerms (mdl, []) %!error removeTerms (mdl, {'x1'}) %!error plotResiduals (mdl, 'badtype') %!error plotResiduals (mdl, 'fitted', 'ResidualType', 'bad') %!error plotDiagnostics (mdl, 'badtype') %!error plotDiagnostics (mdl, 'leverage', 'BadProp', 1) %!error plotEffects (mdl, 'extra') %!error plotEffects (mdl, 'a', 'b') %!error plotEffects (fitlm (X(:,1), y, 'constant')) %!error fitlm (X, y, 'RobustOpts', 'notarealfunction') %!error fitlm (X, y, 'RobustOpts', 42) %!error plotAdjustedResponse (mdl) %!error plotAdjustedResponse (mdl, 'z') %!error plotAdjustedResponse (mdl, 3) %!error plotAdjustedResponse (mdl, 99) %!error plotAdjustedResponse (mdl, 1.5) %!error plotAdjustedResponse (mdl, 'x1', 'BadOption', 5) %!error plotAdded (mdl, {'x1', 'x2'}) %!error plotResiduals (mdl, 'fitted', {1}, 5) %!test %! ## a rejected plot property must not leave a figure behind: the properties %! ## are parsed before the axes is taken, gca creating a figure when none is %! ## current %! b = findall (0, 'type', 'figure'); %! try %! plotResiduals (mdl, 'fitted', {1}, 5); %! catch %! end %! assert_equal (isempty (setdiff (findall (0, 'type', 'figure'), b)), true); %!test %! ## a histogram forwards its properties to patch, which takes its own %! fig = figure ('visible', 'off'); %! ax = axes (fig); %! h = plotResiduals (ax, mdl, 'histogram', 'FaceColor', [0 1 0]); %! assert_equal (get (h(1), 'FaceColor'), [0 1 0], 1e-10); %! close (fig); %!error plotAdded (mdl, 99) %!error plotAdded (mdl, 'NotACoef') %!error plotAdded (mdl, 2, 'BadOpt', 5) %!error mdl0 = fitlm (ones (n, 1), y, 'Intercept', false); plotAdded (mdl0) %!error plot (mdl, 'extra') %!error plotInteraction (mdl) %!error plotInteraction (mdl, 'x1') %!error plotInteraction (mdl, 'x1', 'x2', 'badtype') %!error plotInteraction (mdl, 'x1', 'x2', 'effects', 'extra') %!error plotInteraction (mdl, 'z', 'x2') %!error plotInteraction (mdl, 'x1', 'z') %!error plotInteraction (mdl, 99, 'x2') %!error plotInteraction (mdl, 1.5, 'x2') %!error plotInteraction (mdl, 'y', 'x2') %!error plotInteraction (mdl, 'x1', 'y') %!error plotInteraction (mdl, 'x1', 'x1') %!error compact (mdl, 'extra') %!error anova (mdl, 'components', 'h', 'extra') %!error anova (mdl, 'bogus') %!error anova (mdl, 'summary', 2) %!error anova (mdl, 'components', 4) %!error ... %! mdl0 = fitlm (X, y, 'RobustOpts', 'on'); step (mdl0) %!error step (mdl, 'Verbose') ## A factor reaching the model only through an interaction or a power has a ## column of the terms matrix without ever being a coefficient, so anything ## indexed by those columns must be built from their own names. %!shared tf, tv, tr, ttbl %! tf = [1;2;3;4;5;6;7;8;9;10;11;12]; %! tv = [2;1;4;3;6;5;8;7;10;9;12;11]; %! tr = [3.1;4.2;5.3;6.4;7.5;8.6;9.7;10.8;11.9;13.0;14.1;15.2]; %! ttbl = table (tf, tv, tr, 'VariableNames', {'u', 'v', 'resp'}); %!test # a power keeps its exponent in the variable's own column %! m = fitlm (ttbl, 'resp ~ 1 + u*v + u^2 + v^2'); %! assert_equal (char (m.Formula), 'resp ~ 1 + u*v + u^2 + v^2'); %! assert_equal (m.Formula.Terms, ... %! [0 0 0; 1 0 0; 0 1 0; 1 1 0; 2 0 0; 0 2 0]); %! assert_equal (m.Formula.TermNames, ... %! {'(Intercept)'; 'u'; 'v'; 'u:v'; 'u^2'; 'v^2'}); %!test # an interaction survives when one factor is not a main effect %! m = fitlm (ttbl, 'resp ~ 1 + u + u:v'); %! assert_equal (char (m.Formula), 'resp ~ 1 + u + u:v'); %! assert_equal (m.Formula.Terms, [0 0 0; 1 0 0; 1 1 0]); %! assert_equal (m.Formula.TermNames, {'(Intercept)'; 'u'; 'u:v'}); %! assert_equal (m.CoefficientNames, {'(Intercept)', 'u', 'u:v'}); %!test # a power alone still names the variable it belongs to %! m = fitlm (ttbl, 'resp ~ 1 + v^2 + u^2'); %! assert_equal (char (m.Formula), 'resp ~ 1 + u + v + u^2 + v^2'); %! assert_equal (m.Formula.NTerms, 5); %! assert_equal (m.Formula.PredictorNames, {'u', 'v'}); ## Dropping the intercept gives the first categorical predictor every one of ## its levels -- the cell-means parameterisation -- on both the formula and the ## keyword path. MATLAB drops the reference level either way and so cannot fit ## the reference group; see the note in the class documentation. %!shared cf, ch, cg, ct1, ct2 %! cf = [1;2;3;4;5;6;7;8;9;10;11;12]; %! ch = {'b';'c';'a';'b';'c';'a';'b';'c';'a';'b';'c';'a'}; %! cg = {'lo';'hi';'lo';'hi';'lo';'hi';'lo';'hi';'lo';'hi';'lo';'hi'}; %! ct1 = table (cf, ch, [3.1;4.2;5.3;6.4;7.5;8.6;9.7;10.8;11.9;13;14.1;15.2], ... %! 'VariableNames', {'u', 'h2', 'resp'}); %! ct2 = table (ch, cg, [3.1;4.2;5.3;6.4;7.5;8.6;9.7;10.8;11.9;13;14.1;15.2], ... %! 'VariableNames', {'h2', 'g', 'resp'}); %!test # every level is coded, and the estimates are the group means %! m = fitlm (ct1, 'resp ~ h2 - 1'); %! assert_equal (m.CoefficientNames, {'h2_b', 'h2_c', 'h2_a'}); %! assert_equal (m.Coefficients.Estimate', [8.05, 9.15, 10.25], 1e-12); %! assert_equal (m.Rsquared.Ordinary > 0, true); %!test # the keyword path codes it the same way %! m = fitlm (ct1, 'linear', 'Intercept', false); %! assert_equal (m.CoefficientNames, {'u', 'h2_b', 'h2_c', 'h2_a'}); %! assert_equal (m.NumCoefficients, m.NumEstimatedCoefficients); %!test # a second categorical stays reference coded, keeping the design full rank %! for spec = {'resp ~ h2 + g - 1', 'linear'} %! if (strcmp (spec{1}, 'linear')) %! m = fitlm (ct2, spec{1}, 'Intercept', false); %! else %! m = fitlm (ct2, spec{1}); %! endif %! assert_equal (m.CoefficientNames, {'h2_b', 'h2_c', 'h2_a', 'g_hi'}); %! assert_equal (m.NumCoefficients, m.NumEstimatedCoefficients); %! endfor %!test # an intercept still takes the reference level with it %! m = fitlm (ct1, 'resp ~ 1 + h2'); %! assert_equal (m.CoefficientNames, {'(Intercept)', 'h2_c', 'h2_a'}); %! m = fitlm (ct1, 'linear'); %! assert_equal (m.CoefficientNames, {'(Intercept)', 'u', 'h2_c', 'h2_a'}); ## A predictor that groups its observations is coded the same way whatever its ## column type, and VariableInfo reports it as categorical with its levels in ## the order the design codes them. %!shared vu, vg, vb, vk, vr %! vu = [1;2;3;4;5;6;7;8;9;10;11;12]; %! vg = {'lo';'hi';'lo';'hi';'lo';'hi';'lo';'hi';'lo';'hi';'lo';'hi'}; %! vb = logical ([0;1;0;1;0;1;0;1;0;1;0;1]); %! vk = [5;3;5;3;5;3;5;3;5;3;5;3]; %! vr = [3.1;4.2;5.3;6.4;7.5;8.6;9.7;10.8;11.9;13;14.1;15.2]; %!test # a logical column is coded by value, with false as the reference %! t = table (vu, vb, vr, 'VariableNames', {'u', 'b', 'resp'}); %! m = fitlm (t, 'resp ~ 1 + u + b'); %! assert_equal (m.CoefficientNames, {'(Intercept)', 'u', 'b_1'}); %! assert_equal (m.VariableInfo.IsCategorical, [false; true; false]); %! assert_equal (m.VariableInfo.Class, {'double'; 'logical'; 'double'}); %! assert_equal (m.VariableInfo.Range{2}, [false, true]); %! ## and the keyword path codes it identically %! assert_equal (fitlm (t, 'linear').CoefficientNames, ... %! {'(Intercept)', 'u', 'b_1'}); %!test # 'CategoricalVars' is honoured on the formula path too %! t = table (vu, vk, vr, 'VariableNames', {'u', 'k', 'resp'}); %! m = fitlm (t, 'resp ~ 1 + u + k', 'CategoricalVars', {'k'}); %! assert_equal (m.CoefficientNames, {'(Intercept)', 'u', 'k_5'}); %! assert_equal (m.VariableInfo.IsCategorical, [false; true; false]); %! assert_equal (m.VariableInfo.Range{2}, [3, 5]); %! ## without the declaration it stays numeric %! m = fitlm (t, 'resp ~ 1 + u + k'); %! assert_equal (m.CoefficientNames, {'(Intercept)', 'u', 'k'}); %!test # a string column groups like a cellstr one %! t = table (vu, string (vg), vr, 'VariableNames', {'u', 'g', 'resp'}); %! m = fitlm (t, 'resp ~ 1 + u + g'); %! assert_equal (m.CoefficientNames, {'(Intercept)', 'u', 'g_hi'}); %! assert_equal (m.VariableInfo.IsCategorical, [false; true; false]); %! assert_equal (class (m.VariableInfo.Range{2}), 'string'); %!test # Range keeps each variable's own type and the design's level order %! t = table (vu, vg, vr, 'VariableNames', {'u', 'g', 'resp'}); %! m = fitlm (t, 'resp ~ 1 + u + g'); %! assert_equal (m.VariableInfo.Range{2}, {'lo', 'hi'}); %! assert_equal (m.VariableInfo.Range{1}, [1, 12]); %! t = table (vu, categorical (vg), vr, 'VariableNames', {'u', 'g', 'resp'}); %! m = fitlm (t, 'resp ~ 1 + u + g'); %! assert_equal (class (m.VariableInfo.Range{2}), 'categorical'); %! assert_equal (cellstr (m.VariableInfo.Range{2}), {'hi', 'lo'}); %!test %! ## removeTerms names a term on a table model whose formula uses only some %! ## of the table's variables. The model's own terms are only as wide as the %! ## predictors it uses, while the refit runs against the whole table, and the %! ## two spaces have to be reconciled. Measured on R2024a: y ~ 1 + x1. %! x1 = [1; 2; 3; 4; 5; 6; 7; 8]; %! x2 = [2; 1; 4; 3; 6; 5; 8; 7]; %! x3 = [1; 1; 2; 2; 3; 3; 4; 4]; %! x4 = [8; 7; 6; 5; 4; 3; 2; 1]; %! y = 3 * x1 - 2 * x4; %! T = table (x1, x2, x3, x4, y); %! mdl = fitlm (T, "y ~ x1 + x4"); %! assert_equal (removeTerms (mdl, "x4").Formula.LinearPredictor, "1 + x1"); %! assert_equal (removeTerms (mdl, "x1").Formula.LinearPredictor, "1 + x4"); %!test %! ## The intercept goes by name on such a model too. %! x1 = [1; 2; 3; 4; 5; 6; 7; 8]; %! x2 = [2; 1; 4; 3; 6; 5; 8; 7]; %! x4 = [8; 7; 6; 5; 4; 3; 2; 1]; %! y = 3 * x1 - 2 * x4; %! T = table (x1, x2, x4, y); %! mdl = fitlm (T, "y ~ x1 + x4"); %! assert_equal (removeTerms (mdl, "1").Formula.LinearPredictor, "x1 + x4"); %!test %! ## Adding a variable the formula left out and removing it again returns %! ## the model to where it started. %! x1 = [1; 2; 3; 4; 5; 6; 7; 8]; %! x2 = [2; 1; 4; 3; 6; 5; 8; 7]; %! x4 = [8; 7; 6; 5; 4; 3; 2; 1]; %! y = 3 * x1 - 2 * x4; %! T = table (x1, x2, x4, y); %! mdl = fitlm (T, "y ~ x1"); %! grown = addTerms (mdl, "x4"); %! assert_equal (grown.Formula.LinearPredictor, "1 + x1 + x4"); %! assert_equal (removeTerms (grown, "x4").Formula.LinearPredictor, "1 + x1"); %!test %! ## step reaches a variable the current model does not use, which is what %! ## R2024a does: a model at y ~ 1 + x1 gains x4 with no Upper given. %! x1 = [1; 2; 3; 4; 5; 6; 7; 8]; %! x2 = [2; 1; 4; 3; 6; 5; 8; 7]; %! x4 = [8; 7; 6; 5; 4; 3; 2; 1] + [0; 0.3; -0.2; 0.1; 0.4; -0.1; 0.2; 0]; %! y = 3 * x1 - 2 * x4; %! T = table (x1, x2, x4, y); %! mdl = fitlm (T, "y ~ x1"); %! grown = step (mdl, "Verbose", 0); %! assert_equal (any (strcmp (grown.PredictorNames, "x4")), true); statistics-release-2.1.2/inst/Regression/MultinomialRegression.m000066400000000000000000003042271527311104300251370ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef MultinomialRegression < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} MultinomialRegression ## ## Multinomial regression model class. ## ## A @code{MultinomialRegression} object holds a multinomial regression of a ## categorical response on one or more predictors, fitted by @code{mnrfit}. ## It is normally created with the @code{fitmnr} function. The response has ## @math{K} categories, listed in @code{ClassNames}, and the model is one of ## three kinds, named by @code{ModelType}: a nominal model contrasts each ## category with the last, an ordinal model fits the cumulative probabilities ## of ordered categories, and a hierarchical model fits each category against ## the ones after it. ## ## The most useful properties are @code{Coefficients} (a table of estimates, ## standard errors, @math{t}-statistics and p-values), @code{Deviance}, ## @code{Fitted}, @code{Residuals}, @code{LogLikelihood}, ## @code{ModelCriterion} and @code{Rsquared}. @code{ObservationInfo} records ## which rows were weighted or missing, and @code{Variables} holds the data ## the model was built from. A fitted model supports @code{predict}, ## @code{feval} and @code{random}, inference on its coefficients through ## @code{coefCI}, @code{coefTest} and @code{testDeviance}, and partial ## dependence through @code{partialDependence} and ## @code{plotPartialDependence}. ## ## @code{Fitted}, @code{Residuals} and @code{ObservationInfo} have one row per ## @emph{input} row. A row left out of the fit because a value it needs is ## missing has @code{NaN} residuals and a missing label in @code{Fitted}, the ## empty character vector, @qcode{}, @qcode{} or ## @qcode{NaN} as the type of @code{ClassNames} has it; MATLAB labels such a ## row with the first class. ## ## @seealso{fitmnr, mnrfit, mnrval, GeneralizedLinearModel} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} Coefficients ## ## Coefficient values ## ## A table with one row per coefficient and four columns: ## @itemize ## @item @code{Value} - estimated coefficient value ## @item @code{SE} - standard error of the estimate ## @item @code{tStat} - the estimate divided by its standard error ## @item @code{pValue} - p-value of that statistic ## @end itemize ## ## The statistic is referred to the normal distribution, or to a ## @math{t}-distribution on @code{DFE} degrees of freedom where the ## dispersion is estimated. The row names are the coefficient names. ## ## This property is read-only. ## ## @end deftp Coefficients = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} CoefficientCovariance ## ## Covariance matrix of the coefficient estimates ## ## A square matrix with one row and column per coefficient, in the order of ## @code{CoefficientNames}. It is scaled by @code{Dispersion}, so it is the ## covariance under the estimated dispersion wherever one was estimated. ## ## This property is read-only. ## ## @end deftp CoefficientCovariance = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} CoefficientNames ## ## Names of the coefficients ## ## A string column, one name per coefficient. A coefficient of one category ## joins the term to the category, @qcode{(Intercept_@var{class})} and ## @qcode{@var{term}_@var{class}}; a slope shared by every category is ## named by its term alone. A categorical predictor contributes one term ## per level bar the first, spelled @qcode{@var{name}_@var{level}}, and an ## interaction joins its factors with a colon. ## ## This property is read-only. ## ## @end deftp CoefficientNames = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} NumCoefficients ## ## Number of coefficients ## ## A positive integer. ## ## This property is read-only. ## ## @end deftp NumCoefficients = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} ClassNames ## ## Names of the response categories ## ## A @math{K}-by-1 array in the type of the response: numeric, logical, a ## cell array of character vectors, a string or a categorical array. A ## character matrix response gives a cell array, and a matrix of counts the ## numbers of its columns. The categories of a numeric, logical or text ## response are sorted, and those of a categorical array are in the order of ## its categories. A category without observations is no class. The last ## class is the reference category of a nominal model. The model is fitted ## in this order, so @code{Fitted} and @code{predict} name the class they ## estimate; R2024a sorts the classes of text but fits them in the order the ## data first shows them, so its labels name the wrong classes unless the ## data show them sorted. ## ## This property is read-only. ## ## @end deftp ClassNames = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} IncludeClassInteractions ## ## Whether each category has its own slopes ## ## A logical scalar: @qcode{true} when each category has its own slopes, ## @qcode{false} when every category shares them. The intercepts always ## differ by category. ## ## This property is read-only. ## ## @end deftp IncludeClassInteractions = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} Link ## ## Link function ## ## A character vector, @qcode{'logit'}, @qcode{'probit'}, ## @qcode{'comploglog'} or @qcode{'loglog'}. A nominal model always uses ## the logit link. ## ## This property is read-only. ## ## @end deftp Link = ''; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} ModelType ## ## Type of the model ## ## A character vector, @qcode{'nominal'}, @qcode{'ordinal'} or ## @qcode{'hierarchical'}. ## ## This property is read-only. ## ## @end deftp ModelType = ''; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} Deviance ## ## Deviance of the fit ## ## A nonnegative scalar, twice the difference between the log-likelihood of ## the saturated model and that of the fitted one. The difference between ## the deviances of two nested models follows a chi-square distribution. ## ## This property is read-only. ## ## @end deftp Deviance = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} DFE ## ## Error degrees of freedom ## ## The number of rows used in the fit times @math{K-1}, less the number of ## coefficients; for a hierarchical model each stage counts only the rows ## with observations that reach it. ## ## This property is read-only. ## ## @end deftp DFE = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} Dispersion ## ## Dispersion parameter ## ## A positive scalar, 1 unless @code{EstimateDispersion} is true. The ## estimate is the square of the @qcode{'sfit'} that @code{mnrfit} ## returns, the Pearson statistic over the categories divided by ## @code{DFE}, so it differs from MATLAB's, as @code{mnrfit} documents. ## The standard errors are scaled by its square root. ## ## This property is read-only. ## ## @end deftp Dispersion = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} EstimateDispersion ## ## Whether the dispersion was estimated ## ## A logical scalar. ## ## This property is read-only. ## ## @end deftp EstimateDispersion = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} Fitted ## ## Fitted response values ## ## An @math{N}-by-1 array of the most probable class of each input row, in ## the type of @code{ClassNames}; a row left out of the fit is a missing ## label. ## ## This property is read-only. ## ## @end deftp Fitted = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} LogLikelihood ## ## Log-likelihood of the fit ## ## A scalar, @code{-@var{Deviance} / 2}: the log-likelihood measured from ## that of the saturated model, as MATLAB reports it. ## ## This property is read-only. ## ## @end deftp LogLikelihood = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} ModelCriterion ## ## Information criteria ## ## A structure with fields @code{AIC}, @code{AICc}, @code{BIC} and ## @code{CAIC}, computed from @code{LogLikelihood}, the number of ## coefficients @math{m} and the number of rows @math{n} used in the fit: ## @code{-2 * LogLikelihood + 2 * m}, its small-sample correction ## @code{+ 2 * m * (m + 1) / (n - m - 1)}, @code{-2 * LogLikelihood + m * ## log (n)} and @code{-2 * LogLikelihood + m * (log (n) + 1)}. ## ## This property is read-only. ## ## @end deftp ModelCriterion = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} Residuals ## ## Residuals of the fit ## ## A table with one row per input row and three variables, @code{Raw}, ## @code{Pearson} and @code{Deviance}, the @qcode{'resid'}, ## @qcode{'residp'} and @qcode{'residd'} that @code{mnrfit} returns and ## documents. @code{Raw} and @code{Pearson} have a column per category of a ## nominal model, and one fewer for an ordinal or hierarchical model; ## @code{Deviance} has one. A row left out of the fit is @code{NaN}. ## ## This property is read-only. ## ## @end deftp Residuals = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} Rsquared ## ## Pseudo R-squared values ## ## A structure with fields @code{Ordinary}, @code{1 - L / L0}, and ## @code{Adjusted}, @code{1 - (L - m) / L0}, where @math{L} is ## @code{LogLikelihood}, @math{L0} that of the model with intercepts alone ## and @math{m} the number of coefficients. ## ## This property is read-only. ## ## @end deftp Rsquared = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} Formula ## ## Model formula ## ## A @code{LinearFormula} object over the variables of the model, with the ## link on the response, such as @qcode{logit(y) ~ 1 + x1 + x2}. ## ## This property is read-only. ## ## @end deftp Formula = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} NumObservations ## ## Number of observations ## ## The number of input rows used in the fit, those left out because a value ## was missing not counted. A row of a matrix of counts counts once, ## whatever number of observations it holds. ## ## This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} NumPredictors ## ## Number of predictors ## ## The number of predictors used in the fit. ## ## This property is read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} NumVariables ## ## Number of variables ## ## The number of variables in the input data, the response and any variable ## the model does not use included. ## ## This property is read-only. ## ## @end deftp NumVariables = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} ObservationInfo ## ## Information about the observations ## ## A table with one row per input row and three variables: @code{Weights}, ## the observation weight, @code{Missing}, true where a value the fit needs ## is missing, and @code{Subset}, true where the row was used in the fit. ## ## This property is read-only. ## ## @end deftp ObservationInfo = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} ObservationNames ## ## Names of the observations ## ## The row names of an input table, as a cell array of character vectors, ## or an empty cell array where there were none. ## ## This property is read-only. ## ## @end deftp ObservationNames = {}; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} PredictorNames ## ## Names of the predictors ## ## A cell array of character vectors, one per predictor used in the fit. ## ## This property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} ResponseName ## ## Name of the response ## ## A character vector, @qcode{'y'} by default. ## ## This property is read-only. ## ## @end deftp ResponseName = ''; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} VariableInfo ## ## Information about the variables ## ## A table with one row per variable and four variables: @code{Class}, the ## class of the variable, @code{Range}, @code{[min, max]} of a continuous ## variable or the levels of a categorical one, @code{InModel}, true for a ## predictor used in the fit, and @code{IsCategorical}. The response is not ## in the model and is categorical. ## ## This property is read-only. ## ## @end deftp VariableInfo = []; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} VariableNames ## ## Names of the variables ## ## A cell array of character vectors naming every variable of the input ## data, the response included. ## ## This property is read-only. ## ## @end deftp VariableNames = {}; ## -*- texinfo -*- ## @deftp {MultinomialRegression} {property} Variables ## ## Input data ## ## A table of the predictors and the response, with any other variable of an ## input table. ## ## This property is read-only. ## ## @end deftp Variables = []; endproperties properties (Access = private, Hidden) ## Coefficients in the layout mnrfit returns them B_ = []; ## mnrfit's stats structure, for the confidence bounds of predict stats_ = []; ## Terms over the encoded predictor columns, the intercept row first terms_ = []; ## Categorical predictors and their levels, to code new data alike catinfo_ = []; ## Names of the encoded predictor columns that terms_ is written over encnames_ = {}; ## Which predictors are categorical iscat_ = []; ## Deviance of the model with intercepts alone nulldev_ = []; endproperties methods (Hidden) ## The model summary function disp (this) fprintf ("\n Multinomial regression with %s responses\n\n", ... this.ModelType); if (! isempty (this.Coefficients)) disp (this.Coefficients); endif fprintf ("\n%d observations, %d error degrees of freedom\n", ... this.NumObservations, this.DFE); fprintf ("Dispersion: %g\n", this.Dispersion); [p, stat] = testDeviance (this); if (! isnan (stat)) if (this.EstimateDispersion) fprintf ("F-statistic vs. constant model: %g, p-value = %g\n", ... stat, p); else fprintf (strcat ("Chi^2-statistic vs. constant model: %g,", ... " p-value = %g\n"), stat, p); endif endif endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {MultinomialRegression} {@var{mdl} =} MultinomialRegression (@var{X}, @var{Y}) ## @deftypefnx {MultinomialRegression} {@var{mdl} =} MultinomialRegression (@var{Tbl}, @var{Y}) ## @deftypefnx {MultinomialRegression} {@var{mdl} =} MultinomialRegression (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {MultinomialRegression} {@var{mdl} =} MultinomialRegression (@var{Tbl}, @var{Formula}) ## @deftypefnx {MultinomialRegression} {@var{mdl} =} MultinomialRegression (@dots{}, @var{name}, @var{value}) ## ## Fit a multinomial regression model. ## ## This is the constructor that @code{fitmnr} calls, and it takes exactly ## what @code{fitmnr} takes. See @code{fitmnr} for the arguments and the ## @var{name}, @var{value} pairs. ## ## @seealso{fitmnr} ## @end deftypefn function this = MultinomialRegression (data, resp, varargin) if (nargin == 0) return; endif if (nargin < 2) error ("MultinomialRegression: too few input arguments."); endif ## Parse the optional paired arguments if (mod (numel (varargin), 2) != 0) error ("MultinomialRegression: optional arguments must be in pairs."); endif optNames = {'CategoricalPredictors', 'EstimateDispersion', ... 'IncludeClassInteractions', 'IterationLimit', 'Link', ... 'ModelType', 'PredictorNames', 'ResponseName', ... 'Tolerance', 'Weights'}; ## IncludeClassInteractions resolves to a default per model type, and ## PredictorNames and ResponseName to the names the data gives dfValues = {[], false, [], 100, 'logit', 'nominal', [], [], 1e-6, []}; [CatPred, EstDisp, Interact, IterLim, LinkName, ModType, PredNames, ... RespName, Tol, Wts, args] = parsePairedArguments (optNames, dfValues, ... varargin); if (! isempty (args)) error ("MultinomialRegression: invalid optional paired argument."); endif if (! ((ischar (ModType) && isrow (ModType)) || (isa (ModType, 'string') && isscalar (ModType)))) error (strcat ("MultinomialRegression: 'ModelType' must be a", ... " character vector.")); endif ModType = lower (char (ModType)); if (! any (strcmp (ModType, {'nominal', 'ordinal', 'hierarchical'}))) error ("MultinomialRegression: unknown 'ModelType' '%s'.", ModType); endif if (! ((ischar (LinkName) && isrow (LinkName)) || (isa (LinkName, 'string') && isscalar (LinkName)))) error ("MultinomialRegression: 'Link' must be a character vector."); endif LinkName = lower (char (LinkName)); if (! ((islogical (EstDisp) || isnumeric (EstDisp)) && isscalar (EstDisp) && any (EstDisp == [0, 1]))) error (strcat ("MultinomialRegression: 'EstimateDispersion' must", ... " be true or false.")); endif EstDisp = logical (EstDisp); if (isempty (Interact)) Interact = ! strcmp (ModType, 'ordinal'); elseif (! ((islogical (Interact) || isnumeric (Interact)) && isscalar (Interact) && any (Interact == [0, 1]))) error (strcat ("MultinomialRegression: 'IncludeClassInteractions'", ... " must be true or false.")); endif Interact = logical (Interact); ## Resolve the predictors and the response [X_raw, tbl, Y, pred_names, resp_name, var_names, formula, ... Variables, obs_names] = MultinomialRegression.intake (data, resp, ... PredNames, ... RespName); n_total = rows (Y); p_raw = numel (pred_names); ## Which predictors are categorical: any but a numeric one in a table, ## and those 'CategoricalPredictors' names iscat = false (1, p_raw); if (! isempty (tbl)) for j = 1:p_raw col = tbl.(pred_names{j}); iscat(j) = ! (isnumeric (col) && isreal (col)); endfor endif if (! isempty (CatPred)) iscat |= MultinomialRegression.catflags (CatPred, pred_names); endif ## Rows missing a predictor, the response or a weight if (isempty (tbl)) missing = any (isnan (X_raw), 2); else missing = any (ismissing (tbl(:,pred_names)), 2); endif missing |= MultinomialRegression.missingresponse (Y); if (isempty (Wts)) w_full = ones (n_total, 1); else if (! (isnumeric (Wts) && isreal (Wts) && isvector (Wts) && numel (Wts) == n_total)) error (strcat ("MultinomialRegression: 'Weights' must be a", ... " numeric vector with one element per", ... " observation.")); endif w_full = double (Wts(:)); missing |= isnan (w_full); endif subset = ! missing; if (! any (subset)) error (strcat ("MultinomialRegression: no observations remain", ... " after removing missing values.")); endif ## The design matrix over every input row, without the intercept, which ## mnrfit adds itself if (isempty (formula)) [X_num, cat_levels] = raw_to_codes (data, X_raw, tbl, pred_names, ... iscat, n_total); [X_enc, enc_names, cat_info] = encode_categorical (X_num, iscat, ... pred_names, ... cat_levels, true); q = columns (X_enc); terms = [zeros(1, q); eye(q)]; coef_terms = enc_names(:)'; X_design = X_enc; X_design(missing,:) = NaN; else tbl_sub = tbl(subset,:); [X_sub, ~, coef_names] = parseWilkinsonFormula (formula, ... 'model_matrix', tbl_sub, pred_names(iscat)); coef_names = coef_names(:)'; [terms, cat_info, term_cols] = terms_from_coefnames (coef_names, ... pred_names, iscat, data, tbl_sub); enc_names = term_cols(1:end-1); terms = terms(:,1:end-1); icpt = strcmp (coef_names, '(Intercept)'); coef_terms = coef_names(! icpt); terms = [zeros(1, columns (terms)); terms(! all (terms == 0, 2),:)]; X_design = NaN (n_total, numel (coef_terms)); X_design(subset,:) = X_sub(:,! icpt); endif ## Fit through mnrfit if (Interact) onoff = 'on'; else onoff = 'off'; endif if (EstDisp) estdisp = 'on'; else estdisp = 'off'; endif ## The classes, and the response as the number of its class in each row, ## or as the matrix of counts it is, which is what mnrfit is given: its ## classes are then numbered as ClassNames lists them [classes, C, Yfit] = MultinomialRegression.classesof (Y(subset,:)); K = numel (classes); if (K < 2) error (strcat ("MultinomialRegression: the response must hold at", ... " least two categories.")); endif [B, dev, st] = mnrfit (X_design(subset,:), Yfit, 'model', ModType, ... 'link', LinkName, 'interactions', onoff, ... 'estdisp', estdisp, 'weights', w_full(subset), ... 'iterationlimit', IterLim, 'tolerance', Tol); ## Coefficient names, in the order of B(:) cnames = cellstr (MultinomialRegression.classlabels (classes)); if (strcmp (ModType, 'nominal') && ! Interact) names = strcat ('(Intercept_', cnames(1:K-1), ')'); elseif (Interact) names = cell (numel (coef_terms) + 1, K - 1); for j = 1:K-1 names(:,j) = [{['(Intercept_', cnames{j}, ')']}; ... strcat(coef_terms(:), '_', cnames{j})]; endfor else names = [strcat('(Intercept_', cnames(1:K-1), ')'); coef_terms(:)]; endif names = names(:); this.B_ = B; this.stats_ = st; this.terms_ = terms; this.catinfo_ = cat_info; this.encnames_ = enc_names; this.iscat_ = iscat; this.ModelType = ModType; this.Link = LinkName; this.IncludeClassInteractions = Interact; this.EstimateDispersion = EstDisp; this.ClassNames = classes; this.Coefficients = table (B(:), st.se(:), st.t(:), st.p(:), ... 'VariableNames', ... {'Value', 'SE', 'tStat', 'pValue'}, ... 'RowNames', names); this.CoefficientNames = string (names); this.CoefficientCovariance = st.covb; this.NumCoefficients = numel (B); this.Deviance = dev; this.DFE = st.dfe; this.Dispersion = st.s ^ 2; ## The fit of the model with intercepts alone: every row has the same ## probabilities, the weighted shares of the categories wC = w_full(subset) .* C; share = sum (wC, 1) / sum (wC(:)); d0 = wC .* log (wC ./ max (sum (wC, 2) .* share, realmin)); d0(wC == 0) = 0; this.nulldev_ = 2 * sum (d0(:)); n_obs = sum (subset); m = this.NumCoefficients; LL = - dev / 2; LL0 = - this.nulldev_ / 2; this.LogLikelihood = LL; aic = - 2 * LL + 2 * m; if (n_obs - m - 1 > 0) aicc = aic + 2 * m * (m + 1) / (n_obs - m - 1); else aicc = Inf; endif this.ModelCriterion = struct ('AIC', aic, 'AICc', aicc, ... 'BIC', - 2 * LL + m * log (n_obs), ... 'CAIC', - 2 * LL + m * (log (n_obs) + 1)); this.Rsquared = struct ('Ordinary', 1 - LL / LL0, ... 'Adjusted', 1 - (LL - m) / LL0); ## Fitted labels and residuals over every input row P = this.probabilities (X_design(subset,:), 'category'); [~, best] = max (P, [], 2); this.Fitted = MultinomialRegression.labelrows (classes, best, subset); R = cell (1, 3); fields = {'resid', 'residp', 'residd'}; for k = 1:3 R{k} = NaN (n_total, columns (st.(fields{k}))); R{k}(subset,:) = st.(fields{k}); endfor this.Residuals = table (R{:}, 'VariableNames', ... {'Raw', 'Pearson', 'Deviance'}); ## The model's variables if (isempty (formula)) in_model = true (1, p_raw); else in_model = any (terms(2:end,:) != 0, 1); enc2raw = encodednames_to_row (enc_names, pred_names, cat_info); used = false (1, p_raw); used(enc2raw(enc2raw > 0 & in_model)) = true; in_model = used; endif this.PredictorNames = pred_names(:); this.ResponseName = resp_name; this.NumPredictors = p_raw; this.NumObservations = n_obs; this.Variables = Variables; this.VariableNames = var_names(:); this.NumVariables = numel (var_names); this.ObservationNames = obs_names; this.ObservationInfo = table (w_full, missing, subset, ... 'VariableNames', ... {'Weights', 'Missing', 'Subset'}); n_vars = numel (var_names); vi_class = cell (n_vars, 1); vi_range = cell (n_vars, 1); vi_inmodel = false (n_vars, 1); vi_iscat = false (n_vars, 1); for j = 1:n_vars col = Variables.(var_names{j}); col = col(subset,:); if (isa (col, 'categorical')) col = removecats (col); endif k = find (strcmp (pred_names, var_names{j}), 1); if (strcmp (var_names{j}, resp_name)) vi_iscat(j) = true; [vi_class{j}, ~] = variable_class_and_range (col); vi_range{j} = MultinomialRegression.classlabels (classes)(:)'; if (isnumeric (classes) || islogical (classes)) vi_range{j} = classes(:)'; endif else [vi_class{j}, vi_range{j}] = variable_class_and_range (col); if (! isempty (k)) vi_inmodel(j) = in_model(k); vi_iscat(j) = iscat(k); else vi_iscat(j) = ! (isnumeric (col) && isreal (col)); endif endif endfor this.VariableInfo = table (vi_class, vi_range, vi_inmodel, vi_iscat, ... 'VariableNames', {'Class', 'Range', ... 'InModel', ... 'IsCategorical'}, ... 'RowNames', var_names(:)); this.Formula = MultinomialRegression.buildformula (terms, enc_names, ... pred_names, ... cat_info, ... var_names, ... resp_name, LinkName); endfunction ## -*- texinfo -*- ## @deftypefn {MultinomialRegression} {@var{label} =} predict (@var{mdl}, @var{Xnew}) ## @deftypefnx {MultinomialRegression} {[@var{label}, @var{probs}] =} predict (@var{mdl}, @var{Xnew}) ## @deftypefnx {MultinomialRegression} {[@var{label}, @var{probs}, @var{lower}, @var{upper}] =} predict (@var{mdl}, @var{Xnew}) ## @deftypefnx {MultinomialRegression} {[@dots{}] =} predict (@dots{}, @var{name}, @var{value}) ## ## Predict the response of a multinomial regression model. ## ## @code{@var{label} = predict (@var{mdl}, @var{Xnew})} returns the most ## probable class at each row of the new predictor data @var{Xnew}, in the ## type of @code{ClassNames}. @var{Xnew} is a table holding every predictor ## named in @code{PredictorNames}, or a numeric matrix with one column per ## predictor where they are all numeric. A row holding a missing value is a ## missing label. ## ## @var{probs} holds the probabilities, one row per row of @var{Xnew}, and ## @var{lower} and @var{upper} their confidence bounds, from the delta ## method. The following @var{name}, @var{value} pairs are accepted: ## ## @multitable @columnfractions 0.25 0.7 ## @headitem Name @tab Value ## @item @qcode{'Alpha'} @tab The significance level of the bounds, a ## scalar in @math{(0, 1)}; the default 0.05 gives 95% bounds. ## @item @qcode{'ProbabilityType'} @tab @qcode{'category'} (default), the ## probability of each of the @math{K} categories; @qcode{'cumulative'}, ## the probability of each of the first @math{K-1} categories or any before ## it; or @qcode{'conditional'}, the probability of each of the first ## @math{K-1} categories given that the response is not in an earlier one. ## @end multitable ## ## The bounds are at the confidence level @math{1 - @var{Alpha}}. R2024a ## takes @var{Alpha} as the confidence level itself, so its default bounds ## are at 5% and far too narrow. ## ## @seealso{MultinomialRegression.feval, MultinomialRegression.random, ## mnrval} ## @end deftypefn function [label, probs, lower, upper] = predict (this, Xnew, varargin) if (nargin < 2) error ("MultinomialRegression.predict: too few input arguments."); endif [Alpha, Type, args] = parsePairedArguments ({'Alpha', ... 'ProbabilityType'}, ... {0.05, 'category'}, varargin); if (! isempty (args)) error (strcat ("MultinomialRegression.predict: invalid optional", ... " paired argument.")); endif if (! (isnumeric (Alpha) && isscalar (Alpha) && Alpha > 0 && Alpha < 1)) error (strcat ("MultinomialRegression.predict: 'Alpha' must be a", ... " scalar in the range (0, 1).")); endif if (! ((ischar (Type) && isrow (Type)) || (isa (Type, 'string') && isscalar (Type)))) error (strcat ("MultinomialRegression.predict: 'ProbabilityType'", ... " must be a character vector.")); endif Type = lower (char (Type)); if (! any (strcmp (Type, {'category', 'cumulative', 'conditional'}))) error (strcat ("MultinomialRegression.predict: unknown", ... " 'ProbabilityType' '%s'."), Type); endif Xd = this.designof (Xnew, 'MultinomialRegression.predict'); ok = ! any (isnan (Xd), 2); Pc = NaN (rows (Xd), numel (this.ClassNames)); Pc(ok,:) = this.probabilities (Xd(ok,:), 'category'); [~, best] = max (Pc(ok,:), [], 2); label = MultinomialRegression.labelrows (this.ClassNames, best, ok); if (nargout > 1) if (strcmp (Type, 'category')) probs = Pc; else probs = NaN (rows (Xd), numel (this.ClassNames) - 1); probs(ok,:) = this.probabilities (Xd(ok,:), Type); endif endif if (nargout > 2) [~, dlo, dhi] = this.probabilities (Xd(ok,:), Type, 1 - Alpha); lower = NaN (size (probs)); upper = NaN (size (probs)); lower(ok,:) = probs(ok,:) - dlo; upper(ok,:) = probs(ok,:) + dhi; endif endfunction ## -*- texinfo -*- ## @deftypefn {MultinomialRegression} {@var{label} =} feval (@var{mdl}, @var{X1}, @dots{}, @var{Xp}) ## @deftypefnx {MultinomialRegression} {@var{label} =} feval (@var{mdl}, @var{Xnew}) ## ## Predict the response of a multinomial regression model. ## ## @code{@var{label} = feval (@var{mdl}, @var{X1}, @dots{}, @var{Xp})} ## takes one argument per predictor, each a column vector or a scalar, and a ## scalar stands for every row. @code{@var{label} = feval (@var{mdl}, ## @var{Xnew})} takes a table or a matrix, as @code{predict} does. Either ## returns what @code{predict} returns first, the most probable class. ## ## @seealso{MultinomialRegression.predict} ## @end deftypefn function label = feval (this, varargin) if (numel (varargin) == 1 && (istable (varargin{1}) || columns (varargin{1}) == this.NumPredictors)) label = predict (this, varargin{1}); return; endif if (numel (varargin) != this.NumPredictors) error ("MultinomialRegression.feval: give one argument per predictor."); endif n = max (cellfun (@numel, varargin)); X = zeros (n, this.NumPredictors); for j = 1:this.NumPredictors v = varargin{j}; if (! (isnumeric (v) && (isscalar (v) || numel (v) == n))) error (strcat ("MultinomialRegression.feval: each predictor", ... " must be a scalar or a vector of one common", ... " length.")); endif X(:,j) = v(:); endfor label = predict (this, X); endfunction ## -*- texinfo -*- ## @deftypefn {MultinomialRegression} {@var{ysim} =} random (@var{mdl}, @var{Xnew}) ## @deftypefnx {MultinomialRegression} {@var{ysim} =} random (@var{mdl}, @var{Xnew}, @var{NumTrials}) ## @deftypefnx {MultinomialRegression} {@var{ysim} =} random (@dots{}, @var{type}) ## ## Simulate responses of a multinomial regression model. ## ## @code{@var{ysim} = random (@var{mdl}, @var{Xnew})} draws one response at ## each row of @var{Xnew} from the probabilities the model gives there. ## @var{Xnew} is a table or a matrix, as @code{predict} takes it. ## @var{NumTrials}, a positive integer, draws that many at each row. ## ## @var{type} is @qcode{'samples'} (default), for an ## @math{r}-by-@var{NumTrials} array of class labels, in the type of ## @code{ClassNames}, or @qcode{'counts'}, for an @math{r}-by-@math{K} ## table counting how many draws fell in each class, one variable per ## class. ## ## @seealso{MultinomialRegression.predict} ## @end deftypefn function ysim = random (this, Xnew, varargin) if (nargin < 2) error ("MultinomialRegression.random: too few input arguments."); endif NumTrials = 1; type = 'samples'; for k = 1:numel (varargin) a = varargin{k}; if (isnumeric (a)) if (! (isscalar (a) && a >= 1 && fix (a) == a)) error (strcat ("MultinomialRegression.random: NUMTRIALS must", ... " be a positive integer.")); endif NumTrials = a; elseif ((ischar (a) && isrow (a)) || (isa (a, 'string') && isscalar (a))) type = lower (char (a)); if (! any (strcmp (type, {'samples', 'counts'}))) error ("MultinomialRegression.random: unknown TYPE '%s'.", type); endif else error ("MultinomialRegression.random: invalid input argument."); endif endfor [~, P] = predict (this, Xnew); [r, K] = size (P); ## Each draw is the first class whose cumulative probability passes a ## uniform number cumP = cumsum (P, 2); idx = zeros (r, NumTrials); for t = 1:NumTrials u = rand (r, 1); idx(:,t) = sum (u > cumP(:,1:K-1), 2) + 1; endfor if (strcmp (type, 'samples')) lab = MultinomialRegression.labelrows (this.ClassNames, idx(:), ... true (r * NumTrials, 1)); ysim = reshape (lab, r, NumTrials); else counts = zeros (r, K); for j = 1:K counts(:,j) = sum (idx == j, 2); endfor vn = cellstr (MultinomialRegression.classlabels (this.ClassNames)); ysim = array2table (counts, 'VariableNames', vn(:)'); endif endfunction ## -*- texinfo -*- ## @deftypefn {MultinomialRegression} {@var{ci} =} coefCI (@var{mdl}) ## @deftypefnx {MultinomialRegression} {@var{ci} =} coefCI (@var{mdl}, @var{alpha}) ## ## Confidence intervals for the coefficients. ## ## @code{@var{ci} = coefCI (@var{mdl})} returns a @math{p}-by-2 matrix, one ## row per coefficient in the order of @code{CoefficientNames}, of the lower ## and upper limits of its 95% confidence interval, ## @code{Value +/- tinv (1 - @var{alpha} / 2, DFE) * SE}. @var{alpha}, in ## @math{(0, 1)}, sets the level to @math{100 (1 - @var{alpha})%}. ## ## @seealso{MultinomialRegression.coefTest} ## @end deftypefn function ci = coefCI (this, alpha) if (nargin < 2) alpha = 0.05; endif if (! (isnumeric (alpha) && isscalar (alpha) && alpha > 0 && alpha < 1)) error (strcat ("MultinomialRegression.coefCI: ALPHA must be a", ... " scalar in the range (0, 1).")); endif b = this.Coefficients.Value; se = this.Coefficients.SE; t = tinv (1 - alpha / 2, this.DFE); ci = [b - t * se, b + t * se]; endfunction ## -*- texinfo -*- ## @deftypefn {MultinomialRegression} {@var{p} =} coefTest (@var{mdl}) ## @deftypefnx {MultinomialRegression} {@var{p} =} coefTest (@var{mdl}, @var{H}) ## @deftypefnx {MultinomialRegression} {@var{p} =} coefTest (@var{mdl}, @var{H}, @var{C}) ## @deftypefnx {MultinomialRegression} {[@var{p}, @var{F}, @var{r}] =} coefTest (@dots{}) ## ## Linear hypothesis test on the coefficients. ## ## @code{@var{p} = coefTest (@var{mdl}, @var{H}, @var{C})} tests the ## hypothesis that @code{@var{H} * @var{b} = @var{C}}, where @var{b} is the ## column of coefficients in the order of @code{CoefficientNames}. @var{H} ## is a full-rank @math{r}-by-@math{p} matrix and @var{C} a vector of ## @math{r} elements, zero by default; without @var{H} the hypothesis is ## that every coefficient is zero. @var{F} is the Wald statistic divided by ## @var{r}, and @var{p} its p-value from the @math{F} distribution on ## @var{r} and @code{DFE} degrees of freedom. ## ## @seealso{MultinomialRegression.coefCI, ## MultinomialRegression.testDeviance} ## @end deftypefn function [p, F, r] = coefTest (this, H, C) b = this.Coefficients.Value; nc = numel (b); if (nargin < 2) H = eye (nc); endif if (! ((isnumeric (H) || islogical (H)) && ismatrix (H) && columns (H) == nc)) error (strcat ("MultinomialRegression.coefTest: H must have one", ... " column per coefficient.")); endif H = double (H); r = rows (H); if (nargin < 3) C = zeros (r, 1); endif if (! (isnumeric (C) && isvector (C) && numel (C) == r)) error (strcat ("MultinomialRegression.coefTest: C must have one", ... " element per row of H.")); endif d = H * b - C(:); F = (d' * ((H * this.CoefficientCovariance * H') \ d)) / r; p = fcdf (F, r, this.DFE, 'upper'); endfunction ## -*- texinfo -*- ## @deftypefn {MultinomialRegression} {@var{p} =} testDeviance (@var{mdl}) ## @deftypefnx {MultinomialRegression} {[@var{p}, @var{stat}] =} testDeviance (@var{mdl}) ## ## Deviance test against the model with intercepts alone. ## ## @code{@var{p} = testDeviance (@var{mdl})} tests whether the model fits ## better than one with an intercept for each category and no predictors, ## comparing their deviances. Where the dispersion is fixed, @var{stat} is ## the drop in deviance and @var{p} its chi-square p-value on as many ## degrees of freedom as the model has coefficients beyond the intercepts; ## where it is estimated, @var{stat} is that drop per degree of freedom ## divided by @code{Dispersion}, and @var{p} its @math{F} p-value on those ## and @code{DFE} degrees of freedom. ## ## @seealso{MultinomialRegression.coefTest} ## @end deftypefn function [p, stat] = testDeviance (this) df1 = this.NumCoefficients - (numel (this.ClassNames) - 1); drop = this.nulldev_ - this.Deviance; if (df1 < 1) p = NaN; stat = NaN; elseif (this.EstimateDispersion) stat = (drop / df1) / this.Dispersion; p = fcdf (stat, df1, this.DFE, 'upper'); else stat = drop; p = chi2cdf (stat, df1, 'upper'); endif endfunction ## -*- texinfo -*- ## @deftypefn {MultinomialRegression} {} plotResiduals (@var{mdl}) ## @deftypefnx {MultinomialRegression} {} plotResiduals (@var{mdl}, @var{plotType}) ## @deftypefnx {MultinomialRegression} {} plotResiduals (@var{ax}, @dots{}) ## @deftypefnx {MultinomialRegression} {} plotResiduals (@dots{}, @var{name}, @var{value}) ## @deftypefnx {MultinomialRegression} {@var{h} =} plotResiduals (@dots{}) ## ## Plot the residuals of a multinomial regression model. ## ## @code{plotResiduals (@var{mdl})} draws a histogram of the deviance ## residuals, as a probability density. @var{plotType} chooses the plot: ## ## @multitable @columnfractions 0.2 0.75 ## @headitem Type @tab Plot ## @item @qcode{'histogram'} @tab a histogram of the residuals (default) ## @item @qcode{'caseorder'} @tab the residuals against the row number ## @item @qcode{'fitted'} @tab the residuals against the fitted ## probability of the class ## @item @qcode{'lagged'} @tab each residual against the one before it ## @item @qcode{'probability'} @tab a normal probability plot ## @item @qcode{'observed'} @tab the observed share of each row in the ## class against its fitted probability, with the line @math{y = x} and a ## segment from it to each row ## @item @qcode{'symmetry'} @tab the residuals above their median against ## those below it ## @end multitable ## ## The following @var{name}, @var{value} pairs are accepted: ## ## @multitable @columnfractions 0.2 0.75 ## @headitem Name @tab Value ## @item @qcode{'ResidualType'} @tab @qcode{'deviance'} (default), ## @qcode{'raw'} or @qcode{'Pearson'}, a variable of @code{Residuals}. ## @item @qcode{'ClassToPlot'} @tab For raw and Pearson residuals, the ## class whose column of residuals is plotted, and whose probability the ## @qcode{'fitted'} and @qcode{'observed'} plots show, a class named in ## @code{ClassNames}; the first by default. The residuals of an ordinal or ## hierarchical model have no column for the last class. With deviance ## residuals it is ignored, with a warning, and the first class shown. ## @item @qcode{'FaceColor'}, @qcode{'FaceAlpha'}, @qcode{'LineWidth'} @tab ## The fill color, transparency and edge width of the bars of a histogram. ## @item @qcode{'Color'}, @qcode{'LineStyle'}, @qcode{'Marker'} @tab The ## color, line style and marker of the residuals in the other plots. ## @end multitable ## ## @var{ax} is the axes to draw in, the current axes by default. @var{h} is ## the patch of a histogram, and for the other plots a column of the line ## of residuals and its reference lines. ## ## @seealso{MultinomialRegression.Residuals} ## @end deftypefn function h = plotResiduals (this, varargin) ## Called with an axes first, the model comes second if (isa (this, 'MultinomialRegression')) ax = []; mdl = this; args = varargin; else if (! (isscalar (this) && isgraphics (this, 'axes'))) error ("MultinomialRegression.plotResiduals: AX must be an axes."); endif ax = this; mdl = varargin{1}; args = varargin(2:end); endif ptypes = {'histogram', 'caseorder', 'fitted', 'lagged', ... 'probability', 'observed', 'symmetry'}; ptype = 'histogram'; if (! isempty (args) && ischar (args{1}) && mod (numel (args), 2) == 1) ptype = lower (args{1}); args(1) = []; if (! any (strcmp (ptype, ptypes))) error (strcat ("MultinomialRegression.plotResiduals: unknown", ... " plot type '%s'."), ptype); endif endif optNames = {'ResidualType', 'ClassToPlot', 'FaceColor', 'FaceAlpha', ... 'LineWidth', 'Color', 'LineStyle', 'Marker'}; dfValues = {'deviance', [], [0.066, 0.443, 0.745], 1, 0.5, ... [0.066, 0.443, 0.745], 'none', 'x'}; [rtype, cls, fc, fa, lw, col, ls, mk, rem] = ... parsePairedArguments (optNames, dfValues, args); if (! isempty (rem)) error (strcat ("MultinomialRegression.plotResiduals: invalid", ... " optional paired argument.")); endif if (! (ischar (rtype) || (isa (rtype, 'string') && isscalar (rtype)))) error (strcat ("MultinomialRegression.plotResiduals:", ... " 'ResidualType' must be a character vector.")); endif rtype = lower (char (rtype)); j = 1; switch (rtype) case 'deviance' if (! isempty (cls)) warning (strcat ("MultinomialRegression.plotResiduals:", ... " 'ClassToPlot' is ignored when", ... " 'ResidualType' is 'deviance'.")); endif r = mdl.Residuals.Deviance; case {'raw', 'pearson'} if (strcmp (rtype, 'raw')) R = mdl.Residuals.Raw; else R = mdl.Residuals.Pearson; endif if (! isempty (cls)) j = MultinomialRegression.classindex (mdl.ClassNames, cls); if (j == 0) error (strcat ("MultinomialRegression.plotResiduals:", ... " 'ClassToPlot' names no class of the", ... " model.")); endif if (j > columns (R)) error (strcat ("MultinomialRegression.plotResiduals: the", ... " residuals have no column for the last", ... " class.")); endif endif r = R(:,j); otherwise error (strcat ("MultinomialRegression.plotResiduals: unknown", ... " 'ResidualType' '%s'."), rtype); endswitch if (isempty (ax)) ax = gca (); endif plotargs = {'LineStyle', ls, 'Color', col, 'Marker', mk}; switch (ptype) case 'histogram' [left, right, dens] = MultinomialRegression.densitybins (r); nb = numel (left); Xp = [left; left; right; right]; Yp = [zeros(1, nb); dens; dens; zeros(1, nb)]; h = patch (ax, Xp, Yp, fc, 'FaceColor', fc, 'FaceAlpha', fa, ... 'EdgeColor', [0, 0, 0], 'LineWidth', lw); xlabel (ax, 'Residuals'); ylabel (ax, 'Probability density'); title (ax, 'Histogram of residuals'); case 'caseorder' hold (ax, 'on'); h = plot (ax, 1:numel (r), r, plotargs{:}); h(2,1) = line (ax, xlim (ax), [0, 0], 'LineStyle', ':', ... 'Color', [0, 0, 0]); hold (ax, 'off'); xlabel (ax, 'Row number'); ylabel (ax, 'Residuals'); title (ax, 'Case order plot of residuals'); case 'fitted' [f, ~] = mdl.fittedclass (j); hold (ax, 'on'); h = plot (ax, f, r, plotargs{:}); h(2,1) = line (ax, xlim (ax), [0, 0], 'LineStyle', ':', ... 'Color', [0, 0, 0]); hold (ax, 'off'); xlabel (ax, 'Fitted values'); ylabel (ax, 'Residuals'); title (ax, 'Plot of residuals vs. fitted values'); case 'observed' [f, obs] = mdl.fittedclass (j); n = numel (f); hold (ax, 'on'); h = plot (ax, f, obs, plotargs{:}); h(2,1) = line (ax, xlim (ax), xlim (ax), 'LineStyle', ':', ... 'Color', [0, 0, 0]); xv = reshape ([f(:)'; f(:)'; NaN(1, n)], 1, []); yv = reshape ([f(:)'; obs(:)'; NaN(1, n)], 1, []); h(3,1) = line (ax, xv, yv, 'LineStyle', '-', 'Color', col); hold (ax, 'off'); xlabel (ax, 'Fitted values'); ylabel (ax, 'Observed response values'); title (ax, 'Plot of observed vs. fitted values'); case 'lagged' hold (ax, 'on'); h = plot (ax, r(1:end-1), r(2:end), plotargs{:}); h(2,1) = line (ax, xlim (ax), [0, 0], 'LineStyle', ':', ... 'Color', [0, 0, 0]); h(3,1) = line (ax, [0, 0], ylim (ax), 'LineStyle', ':', ... 'Color', [0, 0, 0]); hold (ax, 'off'); xlabel (ax, 'Residual(t-1)'); ylabel (ax, 'Residual(t)'); title (ax, 'Plot of residuals vs. lagged residuals'); case 'probability' h = normplot (ax, sort (r(! isnan (r)))); set (h(1), plotargs{:}); xlabel (ax, 'Residuals'); ylabel (ax, 'Probability'); title (ax, 'Normal probability plot of residuals'); case 'symmetry' rs = sort (r(! isnan (r))); med = median (rs); k = floor (numel (rs) / 2); lower = sort (med - rs(1:k)); upper = sort (rs(end-k+1:end) - med); hold (ax, 'on'); h = plot (ax, lower, upper, plotargs{:}); top = max ([xlim(ax), ylim(ax)]); h(2,1) = line (ax, [0, top], [0, top], 'LineStyle', ':', ... 'Color', [0, 0, 0]); hold (ax, 'off'); xlabel (ax, 'Lower tail'); ylabel (ax, 'Upper tail'); title (ax, 'Symmetry plot of residuals around their median'); endswitch if (nargout == 0) clear h; endif endfunction ## -*- texinfo -*- ## @deftypefn {MultinomialRegression} {} plotSlice (@var{mdl}) ## @deftypefnx {MultinomialRegression} {} plotSlice (@var{mdl}, @var{Xnew}) ## @deftypefnx {MultinomialRegression} {} plotSlice (@var{mdl}, @var{plotType}) ## @deftypefnx {MultinomialRegression} {} plotSlice (@var{mdl}, @var{plotType}, @var{Xnew}) ## @deftypefnx {MultinomialRegression} {} plotSlice (@var{ax}, @dots{}) ## @deftypefnx {MultinomialRegression} {} plotSlice (@dots{}, @var{name}, @var{value}) ## @deftypefnx {MultinomialRegression} {@var{h} =} plotSlice (@dots{}) ## ## Plot slices through the fitted probabilities of a multinomial regression ## model. ## ## A slice varies one continuous predictor over its range and holds every ## other predictor fixed, a categorical one at its most common level. The ## ranges and the fixed values are taken from the data the model was ## fitted on, or from @var{Xnew}, a table or a matrix as @code{predict} ## takes it; a single row of @var{Xnew} gives each predictor the range from ## its value to 10% above it. @var{plotType} chooses the plot: ## ## @multitable @columnfractions 0.2 0.75 ## @headitem Type @tab Plot ## @item @qcode{'interactive'} @tab (default) a figure with one plot per ## continuous predictor, each the probability of one class along that ## predictor, the others fixed at the middle of their ranges, with its 95% ## confidence band shaded and dashed and dotted lines through the middle. ## @item @qcode{'histogram'} @tab overlaid bars of the probability of each ## class at 40 values along one predictor, the others fixed at their means. ## @item @qcode{'stackedhist'} @tab the same probabilities as stacked bars. ## @end multitable ## ## MATLAB's interactive plot is an app whose menus and boxes change the ## class, the predictors and the fixed values; here the figure has no such ## controls, and the plot is drawn again to change them. ## ## The following @var{name}, @var{value} pairs are accepted: ## ## @multitable @columnfractions 0.25 0.7 ## @headitem Name @tab Value ## @item @qcode{'ClassToPlot'} @tab For the interactive plot, the class ## whose probability is drawn, the first of @code{ClassNames} by default. ## @item @qcode{'PredictorToVary'} @tab For the histograms, the name of the ## continuous predictor to vary, the first by default. ## @item @qcode{'FaceColor'}, @qcode{'FaceAlpha'}, @qcode{'LineWidth'} @tab ## The fill color (@qcode{'auto'} by default, a color per class), the ## transparency (0.6) and the edge width (0.5) of the bars of a histogram. ## @item @qcode{'EdgeColor'}, @qcode{'LineStyle'} @tab The edge color and ## line style of the stacked bars, black up to 150 bars and none beyond, ## and a solid line. ## @end multitable ## ## @var{ax} is the axes to draw a histogram in, the current axes by default. ## @var{h} is the figure of the interactive plot, and for the histograms a ## column of one patch or bar object per class. ## ## @seealso{MultinomialRegression.predict, plotPartialDependence} ## @end deftypefn function h = plotSlice (this, varargin) caller = 'MultinomialRegression.plotSlice'; ## Called with an axes first, the model comes second if (isa (this, 'MultinomialRegression')) ax = []; mdl = this; args = varargin; else if (! (isscalar (this) && isgraphics (this, 'axes'))) error ("%s: AX must be an axes.", caller); endif ax = this; mdl = varargin{1}; args = varargin(2:end); endif ## The plot type and the new data come before the paired arguments ptypes = {'interactive', 'histogram', 'stackedhist'}; ptype = 'interactive'; Xnew = []; while (! isempty (args)) a = args{1}; if (ischar (a) && any (strcmpi (a, ptypes))) ptype = lower (a); elseif (isnumeric (a) || istable (a)) Xnew = a; else break; endif args(1) = []; endwhile if (! isempty (ax) && strcmp (ptype, 'interactive')) error ("%s: AX applies to the histograms only.", caller); endif optNames = {'ClassToPlot', 'PredictorToVary', 'FaceColor', ... 'FaceAlpha', 'LineWidth', 'EdgeColor', 'LineStyle'}; dfValues = {[], [], 'auto', 0.6, 0.5, [], '-'}; [cls, pvary, fc, fa, lw, ec, ls, rem] = ... parsePairedArguments (optNames, dfValues, args); if (! isempty (rem)) error ("%s: invalid optional paired argument.", caller); endif ## The data the ranges and the fixed values are taken from if (isempty (Xnew)) D = mdl.rawcodes (mdl.Variables(mdl.ObservationInfo.Subset,:), caller); else D = mdl.rawcodes (Xnew, caller); endif D = D(! any (isnan (D), 2),:); if (isempty (D)) error ("%s: XNEW holds no row without a missing value.", caller); endif cont = find (! mdl.iscat_); if (isempty (cont)) error ("%s: the model has no continuous predictor to vary.", caller); endif lo = min (D, [], 1); hi = max (D, [], 1); one = lo == hi; hi(one) = lo(one) + 0.1 * abs (lo(one)); hi(one & lo == 0) = 0.1; fixed = mean (D, 1); for j = find (mdl.iscat_) fixed(j) = mode (D(:,j)); endfor names = cellstr (MultinomialRegression.classlabels (mdl.ClassNames)); K = numel (names); if (strcmp (ptype, 'interactive')) k = 1; if (! isempty (cls)) k = MultinomialRegression.classindex (mdl.ClassNames, cls); if (k == 0) error ("%s: 'ClassToPlot' names no class of the model.", caller); endif endif mid = (lo + hi) / 2; mid(mdl.iscat_) = fixed(mdl.iscat_); p0 = mdl.probabilities (mdl.designcodes (mid), 'category'); h = figure (); q = numel (cont); axs = zeros (q, 1); top = 0; for i = 1:q j = cont(i); v = linspace (lo(j), hi(j), 41)'; G = repmat (mid, 41, 1); G(:,j) = v; [P, dlo, dhi] = mdl.probabilities (mdl.designcodes (G), ... 'category', 0.95); up = min (P(:,k) + dhi(:,k), 1); dn = max (P(:,k) - dlo(:,k), 0); axs(i) = subplot (1, q, i); hold (axs(i), 'on'); patch (axs(i), [v; flipud(v)], [up; flipud(dn)], ... [0, 0.447, 0.741], 'FaceAlpha', 0.3, 'EdgeColor', 'none'); plot (axs(i), v, P(:,k), '-', 'Color', [0, 0.447, 0.741]); hold (axs(i), 'off'); xlabel (axs(i), mdl.PredictorNames{j}); top = max ([top; up]); endfor ylabel (axs(1), sprintf ('Probability of %s', names{k})); for i = 1:q j = cont(i); ylim (axs(i), [0, top]); line (axs(i), [mid(j), mid(j)], [0, top], 'LineStyle', '--', ... 'Color', [0, 0, 0]); line (axs(i), [lo(j), hi(j)], [p0(k), p0(k)], 'LineStyle', ':', ... 'Color', [0, 0, 0]); endfor else if (isempty (pvary)) j = cont(1); else j = find (strcmp (mdl.PredictorNames, char (pvary)), 1); if (isempty (j)) error ("%s: 'PredictorToVary' names no predictor of the model.", ... caller); endif if (mdl.iscat_(j)) error (strcat ("%s: 'PredictorToVary' must name a continuous", ... " predictor."), caller); endif endif v = linspace (lo(j), hi(j), 40); step = v(2) - v(1); G = repmat (fixed, 40, 1); G(:,j) = v; P = mdl.probabilities (mdl.designcodes (G), 'category'); if (isempty (ax)) ax = gca (); endif if (strcmp (ptype, 'histogram')) colors = get (ax, 'ColorOrder'); h = zeros (K, 1); hold (ax, 'on'); for c = 1:K if (ischar (fc) && strcmp (fc, 'auto')) col = colors(mod (c - 1, rows (colors)) + 1,:); else col = fc; endif Xp = [v; v; v + step; v + step]; Yp = [zeros(1, 40); P(:,c)'; P(:,c)'; zeros(1, 40)]; h(c) = patch (ax, Xp, Yp, col, 'FaceAlpha', fa, ... 'EdgeColor', [0, 0, 0], 'LineWidth', lw, ... 'DisplayName', names{c}); endfor hold (ax, 'off'); else if (isempty (ec)) if (40 * K <= 150) ec = [0, 0, 0]; else ec = 'none'; endif endif h = bar (ax, v, P, 'stacked', 'BarWidth', 0.8); h = h(:); set (h, 'EdgeColor', ec, 'LineStyle', ls); for c = 1:K set (h(c), 'DisplayName', names{c}); endfor ylim (ax, [0, 1.01]); endif xlabel (ax, mdl.PredictorNames{j}); ylabel (ax, 'Probability'); endif if (nargout == 0) clear h; endif endfunction endmethods methods (Access = private) ## Probabilities of TYPE at the design rows XD, and with a confidence level ## their delta-method bounds function [P, dlo, dhi] = probabilities (this, Xd, type, level) if (this.IncludeClassInteractions) onoff = 'on'; else onoff = 'off'; endif B = this.B_; if (strcmp (this.ModelType, 'nominal') && ! this.IncludeClassInteractions) ## The intercepts alone B = B(:)'; Xd = zeros (rows (Xd), 0); onoff = 'on'; endif args = {'model', this.ModelType, 'link', this.Link, ... 'interactions', onoff, 'type', type}; if (nargout > 1) [P, dlo, dhi] = mnrval (B, Xd, this.stats_, args{:}, ... 'confidence', level); else P = mnrval (B, Xd, args{:}); endif endfunction ## The fitted probability of class J at each input row, and the share of ## the row's observations in that class; NaN at a row left out of the fit function [f, obs] = fittedclass (this, j) subset = this.ObservationInfo.Subset; n = numel (subset); f = NaN (n, 1); obs = NaN (n, 1); X = this.rawcodes (this.Variables(subset,:), 'MultinomialRegression'); P = this.probabilities (this.designcodes (X), 'category'); f(subset) = P(:,j); Y = this.Variables.(this.ResponseName); [~, C] = MultinomialRegression.classesof (Y(subset,:)); obs(subset) = C(:,j) ./ sum (C, 2); endfunction ## The design matrix, without the intercept, of new predictor data function Xd = designof (this, Xnew, caller) Xd = this.designcodes (this.rawcodes (Xnew, caller)); endfunction ## New predictor data as one column per predictor, a categorical one ## holding the number of its level, NaN for a level the model never saw function X_raw = rawcodes (this, Xnew, caller) p = this.NumPredictors; if (istable (Xnew)) X_raw = zeros (height (Xnew), p); for j = 1:p name = this.PredictorNames{j}; if (! any (strcmp (Xnew.Properties.VariableNames, name))) error ("%s: the table holds no predictor '%s'.", caller, name); endif col = Xnew.(name); k = []; if (! isempty (this.catinfo_.names)) k = find (strcmp (this.catinfo_.names, name)); endif if (! isempty (k)) levels = this.catinfo_.levels{k}; [tf, code] = ismember (MultinomialRegression.levelstrings (col), ... cellstr (levels)); code(! tf) = NaN; X_raw(:,j) = code; else X_raw(:,j) = double (col); endif endfor else if (! (isnumeric (Xnew) && isreal (Xnew) && ismatrix (Xnew) && columns (Xnew) == p)) error (strcat ("%s: XNEW must be a table or a numeric matrix", ... " with %d columns."), caller, p); endif if (any (this.iscat_)) ## A categorical predictor given as numbers is coded by its levels X_raw = double (Xnew); for j = find (this.iscat_) k = find (strcmp (this.catinfo_.names, this.PredictorNames{j})); levels = str2double (this.catinfo_.levels{k}); [tf, code] = ismember (X_raw(:,j), levels); code(! tf) = NaN; X_raw(:,j) = code; endfor else X_raw = double (Xnew); endif endif endfunction ## The design matrix, without the intercept, of predictor codes X_RAW function Xd = designcodes (this, X_raw) X_enc = reencode_predictors (X_raw, this.PredictorNames(:)', ... this.catinfo_, this.encnames_); ## A level the model never saw codes as NaN, and so does its row bad = any (isnan (X_raw), 2); Xd = build_design ([this.terms_, zeros(rows (this.terms_), 1)], X_enc); Xd = Xd(:,2:end); Xd(bad,:) = NaN; endfunction endmethods methods (Static, Access = private) ## Resolve the predictors and the response from the forms fitmnr takes function [X_raw, tbl, Y, pred_names, resp_name, var_names, formula, ... Variables, obs_names] = intake (data, resp, PredNames, RespName) formula = ''; obs_names = {}; if (! istable (data)) if (! (isnumeric (data) && isreal (data) && ismatrix (data))) error (strcat ("MultinomialRegression: X must be a real", ... " numeric matrix or a table.")); endif X_raw = double (data); tbl = []; p = columns (X_raw); Y = MultinomialRegression.responsecolumn (resp); if (rows (Y) != rows (X_raw)) error (strcat ("MultinomialRegression: X and Y must have the", ... " same number of rows.")); endif if (isempty (PredNames)) pred_names = arrayfun (@(k) sprintf ('x%d', k), 1:p, ... 'UniformOutput', false); else pred_names = cellstr (PredNames)(:)'; if (numel (pred_names) != p) error (strcat ("MultinomialRegression: 'PredictorNames' must", ... " have %d names."), p); endif endif if (isempty (RespName)) resp_name = 'y'; else resp_name = char (RespName); endif cols = num2cell (X_raw, 1); Variables = table (cols{:}, Y, ... 'VariableNames', [pred_names, {resp_name}]); var_names = [pred_names, {resp_name}]; return; endif tbl = data; X_raw = []; vn = tbl.Properties.VariableNames; if (! isempty (tbl.Properties.RowNames)) obs_names = tbl.Properties.RowNames(:); endif istext = (ischar (resp) && isrow (resp)) ... || (isa (resp, 'string') && isscalar (resp)); if (istext && any (char (resp) == '~')) ## A formula names the response and the predictors formula = char (resp); parts = strsplit (formula, '~'); resp_name = strtrim (parts{1}); if (! any (strcmp (vn, resp_name))) error ("MultinomialRegression: the table holds no variable '%s'.", ... resp_name); endif schema = parseWilkinsonFormula (formula, 'matrix'); used = regexprep (schema.VariableNames, '\^\d+$', ''); used = unique (used, 'stable'); pred_names = vn(ismember (vn, used) & ! strcmp (vn, resp_name)); Y = MultinomialRegression.responsecolumn (tbl.(resp_name)); Variables = tbl; elseif (istext) resp_name = char (resp); if (! any (strcmp (vn, resp_name))) error ("MultinomialRegression: the table holds no variable '%s'.", ... resp_name); endif Y = MultinomialRegression.responsecolumn (tbl.(resp_name)); if (isempty (PredNames)) pred_names = vn(! strcmp (vn, resp_name)); else pred_names = cellstr (PredNames)(:)'; endif Variables = tbl; else ## The response is given beside the table, all of whose variables are ## predictors unless 'PredictorNames' names some Y = MultinomialRegression.responsecolumn (resp); if (rows (Y) != height (tbl)) error (strcat ("MultinomialRegression: TBL and Y must have the", ... " same number of rows.")); endif if (isempty (RespName)) resp_name = 'y'; else resp_name = char (RespName); endif if (isempty (PredNames)) pred_names = vn; else pred_names = cellstr (PredNames)(:)'; endif Variables = tbl; Variables.(resp_name) = Y; endif missing = pred_names(! ismember (pred_names, vn)); if (! isempty (missing)) error ("MultinomialRegression: the table holds no variable '%s'.", ... missing{1}); endif ## Code a string or logical predictor by its text, as a cell array is for j = 1:numel (pred_names) col = tbl.(pred_names{j}); if (isa (col, 'string')) tbl.(pred_names{j}) = cellstr (col); elseif (islogical (col)) c = repmat ({'false'}, size (col)); c(col) = {'true'}; tbl.(pred_names{j}) = c; endif endfor var_names = Variables.Properties.VariableNames; endfunction ## The response as one row per observation: a character matrix becomes a ## cell array, and a row vector of labels a column function Y = responsecolumn (Y) if (ischar (Y)) Y = cellstr (Y); elseif ((iscell (Y) || isa (Y, 'string') || isa (Y, 'categorical') || isnumeric (Y) || islogical (Y)) && isrow (Y)) Y = Y(:); endif if (! (isnumeric (Y) || islogical (Y) || iscellstr (Y) || isa (Y, 'string') || isa (Y, 'categorical'))) error (strcat ("MultinomialRegression: Y must be a numeric,", ... " logical, string or categorical array, a cell", ... " array of character vectors, or a character", ... " matrix.")); endif endfunction ## Which rows of the response are missing function tf = missingresponse (Y) if (isnumeric (Y)) tf = any (isnan (Y), 2); elseif (islogical (Y)) tf = false (rows (Y), 1); else tf = ismissing (Y); if (iscellstr (Y)) tf = cellfun (@isempty, Y); endif endif endfunction ## The classes of the response, the counts of each class in each row, and ## the response as mnrfit is to be given it: the number of its class in ## each row, or the matrix of counts. Text is sorted, as MATLAB sorts it, ## and a categorical array keeps the order of the categories it uses. function [classes, C, Yfit] = classesof (Y) if ((isnumeric (Y) || islogical (Y)) && columns (Y) > 1) keep = any (Y > 0, 1); classes = find (keep)(:); C = double (Y(:,keep)); Yfit = Y; return; endif if (isa (Y, 'categorical')) [~, code] = ismember (cellstr (Y), categories (Y)); [present, first, num] = unique (code); classes = Y(first); elseif (iscellstr (Y) || isa (Y, 'string')) [~, first, num] = unique (cellstr (Y)); classes = Y(first); else [~, first, num] = unique (Y); classes = Y(first); endif classes = classes(:); num = num(:); C = double (num == (1:numel (classes))); Yfit = num; endfunction ## The labels of the classes as a string column function s = classlabels (classes) if (islogical (classes)) s = repmat (string ('false'), numel (classes), 1); s(classes(:)) = 'true'; elseif (isnumeric (classes)) s = string (arrayfun (@(v) num2str (v), double (classes(:)), ... 'UniformOutput', false)); else s = string (cellstr (classes)); s = s(:); endif endfunction ## The labels at the rows TF, the class numbered IDX each, and a missing ## label at every other row function lab = labelrows (classes, idx, tf) n = numel (tf); if (iscellstr (classes)) lab = repmat ({''}, n, 1); elseif (isa (classes, 'string')) lab = repmat (string (missing), n, 1); elseif (isa (classes, 'categorical')) lab = classes(ones (n, 1)); lab(:) = missing; elseif (islogical (classes)) lab = false (n, 1); else lab = NaN (n, 1); endif lab(tf) = classes(idx); endfunction ## The number of the class CLS among CLASSES, or 0 where it is none function j = classindex (classes, cls) j = 0; lab = cellstr (MultinomialRegression.classlabels (classes)); if (isnumeric (cls) || islogical (cls)) if (isscalar (cls) && (isnumeric (classes) || islogical (classes))) j = find (double (classes) == double (cls), 1); endif elseif ((ischar (cls) && isrow (cls)) || isscalar (cls)) j = find (strcmp (lab, char (cellstr (cls))), 1); endif if (isempty (j)) j = 0; endif endfunction ## Bins of a histogram of R as a probability density: their left and right ## edges and their heights. The width is Scott's rule rounded to 1, 2, 3, ## 5 or 10 times a power of ten, and the edges are its multiples. function [left, right, dens] = densitybins (r) r = r(! isnan (r)); n = numel (r); s = std (r); if (n <= 1 || s == 0) bw = 1; lo = floor (min (r)) - 0.5; hi = lo + 1; else raw = 3.5 * s / (n ^ (1/3)); mag = 10 ^ floor (log10 (raw)); f = raw / mag; nice = [1, 2, 3, 5, 10]; nice = nice(find (f < [1.5, 2.5, 4, 7.5, Inf], 1)); bw = nice * mag; lo = floor (min (r) / bw) * bw; hi = ceil (max (r) / bw) * bw; endif nb = max (1, round ((hi - lo) / bw)); left = lo + (0:nb-1) * bw; right = left + bw; counts = zeros (1, nb); for k = 1:nb if (k < nb) counts(k) = sum (r >= left(k) & r < right(k)); else counts(k) = sum (r >= left(k) & r <= right(k)); endif endfor dens = counts / (n * bw); endfunction ## The text of each element of a categorical predictor function s = levelstrings (col) if (isnumeric (col) || islogical (col)) s = arrayfun (@(v) num2str (v), double (col(:)), ... 'UniformOutput', false); else s = cellstr (col); s = s(:); endif endfunction ## Logical flags of the predictors 'CategoricalPredictors' names function tf = catflags (spec, pred_names) p = numel (pred_names); tf = false (1, p); if ((ischar (spec) && strcmpi (spec, 'all')) || (isa (spec, 'string') && isscalar (spec) && strcmpi (spec, 'all'))) tf(:) = true; elseif (islogical (spec)) if (numel (spec) != p) error (strcat ("MultinomialRegression: 'CategoricalPredictors'", ... " must have %d elements."), p); endif tf = spec(:)'; elseif (isnumeric (spec)) if (! all (spec >= 1 & spec <= p & fix (spec) == spec)) error (strcat ("MultinomialRegression: 'CategoricalPredictors'", ... " indices must be between 1 and %d."), p); endif tf(spec) = true; elseif (iscellstr (spec) || isa (spec, 'string') || ischar (spec)) names = cellstr (spec); [ok, k] = ismember (names, pred_names); if (! all (ok)) error (strcat ("MultinomialRegression: 'CategoricalPredictors'", ... " names no predictor '%s'."), names{find (! ok, 1)}); endif tf(k) = true; else error ("MultinomialRegression: invalid 'CategoricalPredictors'."); endif endfunction ## The model formula over the variables, with the link on the response function F = buildformula (terms, enc_names, pred_names, cat_info, ... var_names, resp_name, link) n_vars = numel (var_names); var_idx = zeros (1, numel (pred_names)); for j = 1:numel (pred_names) var_idx(j) = find (strcmp (var_names, pred_names{j}), 1); endfor [enc2raw, col_pow] = encodednames_to_row (enc_names, pred_names, ... cat_info); terms_var = variable_level_terms (terms, enc2raw, var_idx, n_vars, ... col_pow); F = LinearFormula (terms_var, var_names(:)', ... 'ResponseName', resp_name, 'Link', link); endfunction endmethods endclassdef ## Test the fit against MATLAB R2024a %!shared x, z, y, Xm, m %! x = [0.2, 1.1, 1.9, 3.2, 3.8, 5.1, 5.9, 7.2, 8.1, 8.8, 1.5, 4.4, 6.6, ... %! 2.7, 9.3]'; %! z = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]' / 3; %! y = [1, 1, 2, 1, 2, 2, 3, 2, 3, 3, 2, 3, 2, 1, 3]'; %! Xm = [x, z]; %! m = fitmnr (Xm, y); %!test %! assert_equal (m.Coefficients.Value, [5.9358487983056; -1.30171796766448; ... %! -0.474611351178345; 2.68369517887323; -0.725396483366165; ... %! 0.882324211575015], -1e-10); %!test %! assert_equal (m.Coefficients.SE, [3.20733455995645; 0.681543706775439; ... %! 1.36563950586161; 2.73281100769504; 0.404575904177872; ... %! 0.92708159102731], -1e-8); %!test %! assert_equal (m.Coefficients.pValue(1:2), [0.0642111655098015; ... %! 0.0561389778598589], -1e-8); %!test %! assert_equal (m.CoefficientNames, string ({'(Intercept_1)'; 'x1_1'; ... %! 'x2_1'; '(Intercept_2)'; 'x1_2'; 'x2_2'})); %!test %! assert_equal (m.ClassNames, [1; 2; 3]); %!test %! assert_equal ([m.NumCoefficients, m.DFE, m.NumObservations], [6, 24, 15]); %!test %! assert_equal (m.LogLikelihood, -9.30901781932902, 1e-10); %!test %! assert_equal ([m.Rsquared.Ordinary, m.Rsquared.Adjusted], ... %! [0.428116752664665, 0.0595172343688309], 1e-10); %!test %! mc = m.ModelCriterion; %! assert_equal ([mc.AIC, mc.AICc, mc.BIC, mc.CAIC], [30.618035638658, ... %! 41.118035638658, 34.8663368452713, 40.8663368452713], 1e-10); %!test %! assert_equal (char (m.Formula), 'logit(y) ~ 1 + x1 + x2'); %!test %! assert_equal (m.VariableInfo.Range{3}, [1, 2, 3]); %!test %! assert_equal (m.VariableInfo.InModel, [true; true; false]); %!test %! assert_equal (m.VariableInfo.IsCategorical, [false; false; true]); %!test %! assert_equal (m.Fitted(1:5), [1; 1; 1; 1; 2]); %!test %! assert_equal (m.Residuals.Properties.VariableNames, ... %! {'Raw', 'Pearson', 'Deviance'}); %!test %! [~, ~, s] = mnrfit (Xm, y); %! assert_equal (m.Residuals.Pearson, s.residp); %!test %! assert_equal (m.ObservationInfo.Properties.VariableNames, ... %! {'Weights', 'Missing', 'Subset'}); %!test %! assert_equal (m.Dispersion, 1); %!test %! [~, ~, s] = mnrfit (Xm, y, 'estdisp', 'on'); %! md = fitmnr (Xm, y, 'EstimateDispersion', true); %! assert_equal (md.Dispersion, s.s ^ 2, 1e-12); %!test %! md = fitmnr (Xm, y, 'EstimateDispersion', true); %! se = m.Coefficients.SE * sqrt (md.Dispersion); %! assert_equal (md.Coefficients.SE, se, -1e-12); ## Test the model types against MATLAB R2024a %!test %! mo = fitmnr (Xm, y, 'ModelType', 'ordinal'); %! assert_equal (mo.Coefficients.Value, [1.73183634703477; ... %! 5.07302821549392; -0.795693713914391; ... %! 0.00358605506018692], -1e-6); %!test %! mo = fitmnr (Xm, y, 'ModelType', 'ordinal'); %! assert_equal (mo.CoefficientNames, string ({'(Intercept_1)'; ... %! '(Intercept_2)'; 'x1'; 'x2'})); %!test %! mo = fitmnr (Xm, y, 'ModelType', 'ordinal'); %! assert_equal ([mo.Rsquared.Ordinary, mo.Rsquared.Adjusted], ... %! [0.354457649951417, 0.108724637754195], 1e-8); %!test %! mh = fitmnr (Xm, y, 'ModelType', 'hierarchical', 'Link', 'probit'); %! assert_equal (mh.Coefficients.Value, [1.94768532702581; ... %! -0.474517402244592; -0.637169549255952; ... %! 1.43714192930196; -0.419757944547173; ... %! 0.547210016922966], -1e-10); %!test %! mh = fitmnr (Xm, y, 'ModelType', 'hierarchical', 'Link', 'probit'); %! assert_equal (char (mh.Formula), 'probit(y) ~ 1 + x1 + x2'); %!test %! mo = fitmnr (Xm, y, 'ModelType', 'ordinal'); %! assert_equal (mo.IncludeClassInteractions, false); %!test %! mo = fitmnr (Xm, y, 'ModelType', 'ordinal', ... %! 'IncludeClassInteractions', true); %! assert_equal (mo.CoefficientNames(1:3), string ({'(Intercept_1)'; ... %! 'x1_1'; 'x2_1'})); ## Test the classes of each type of response %!test %! names = {'mid', 'lo', 'hi'}; %! mt = fitmnr (x, names(y)'); %! assert_equal (mt.ClassNames, {'hi'; 'lo'; 'mid'}); %!test %! names = {'mid', 'lo', 'hi'}; %! mt = fitmnr (x, names(y)'); %! assert_equal (mt.Fitted(1:3), {'mid'; 'mid'; 'mid'}); %!test %! names = {'mid', 'lo', 'hi'}; %! mt = fitmnr (x, names(y)'); %! assert_equal (mt.CoefficientNames, string ({'(Intercept_hi)'; 'x1_hi'; ... %! '(Intercept_lo)'; 'x1_lo'})); %!test %! names = {'mid', 'lo', 'hi'}; %! ms = fitmnr (x, string (names(y)')); %! assert_equal (ms.ClassNames, string ({'hi'; 'lo'; 'mid'})); %!test %! g = [2, 5, 9]; %! mg = fitmnr (x, g(y)'); %! assert_equal (mg.ClassNames, [2; 5; 9]); %!test %! names = {'mid', 'lo', 'hi'}; %! yk = categorical (names(y)', {'lo', 'mid', 'unused', 'hi'}); %! mk = fitmnr (x, yk); %! assert_equal (cellstr (mk.ClassNames), {'lo'; 'mid'; 'hi'}); %!test %! ml = fitmnr (x, y == 1); %! assert_equal (ml.CoefficientNames, string ({'(Intercept_false)'; ... %! 'x1_false'})); %!test %! Yc = [3, 1, 0; 2, 2, 1; 1, 2, 2; 0, 2, 3; 1, 0, 4; 0, 1, 1]; %! mc = fitmnr ((1:6)', Yc); %! assert_equal (mc.Fitted, [1; 1; 2; 3; 3; 3]); %!test %! Yc = [3, 1, 0; 2, 2, 1; 1, 2, 2; 0, 2, 3; 1, 0, 4; 0, 1, 1]; %! mc = fitmnr ((1:6)', [Yc(:,1), zeros(6,1), Yc(:,2:3)]); %! assert_equal (mc.ClassNames, [1; 3; 4]); ## Test tables and formulas against MATLAB R2024a %!test %! tbl = table (x, z, y); %! mtb = fitmnr (tbl, 'y'); %! [~, pr] = predict (mtb, tbl(1:3,:)); %! [~, pm] = predict (m, Xm(1:3,:)); %! assert_equal (pr, pm, 1e-12); %!test %! tbl = table (x, z, y); %! tbl.g = categorical (mod ((1:15)', 3)); %! S = warning ('off', 'all'); %! mtg = fitmnr (tbl, 'y ~ x + g'); %! warning (S); %! assert_equal (mtg.CoefficientNames, string ({'(Intercept_1)'; 'x_1'; ... %! 'g_1_1'; 'g_2_1'; '(Intercept_2)'; 'x_2'; 'g_1_2'; ... %! 'g_2_2'})); %!test %! tbl = table (x, z, y); %! tbl.g = categorical (mod ((1:15)', 3)); %! S = warning ('off', 'all'); %! mtg = fitmnr (tbl, 'y ~ x + g'); %! warning (S); %! [~, pr] = predict (mtg, tbl(1:3,:)); %! assert_equal (pr(:,2), [0.00316347560363747; 0.193496952442625; ... %! 0.913115541220257], 1e-7); %!test %! tbl = table (x, z, y); %! mt2 = fitmnr (tbl, 'y ~ x*z'); %! [~, pr] = predict (mt2, tbl(1:4,:)); %! B = mnrfit ([x, z, x .* z], y); %! assert_equal (pr, mnrval (B, [x(1:4), z(1:4), x(1:4) .* z(1:4)]), 1e-12); %!test %! tbl = table (x, z, y); %! tbl.g = categorical (mod ((1:15)', 3)); %! mt2 = fitmnr (tbl, 'y ~ x*z'); %! assert_equal (mt2.VariableInfo.InModel, [true; true; false; false]); ## Test missing values and weights %!test %! Xn = Xm; %! Xn(2,1) = NaN; %! mm = fitmnr (Xn, y); %! assert_equal (mm.Fitted(1:3), [1; NaN; 1]); %!test %! Xn = Xm; %! Xn(2,1) = NaN; %! mm = fitmnr (Xn, y); %! assert_equal (mm.ObservationInfo.Subset(1:3), [true; false; true]); %!test %! Xn = Xm; %! Xn(2,1) = NaN; %! mm = fitmnr (Xn, y); %! assert_equal (mm.Residuals.Deviance(2), NaN); %!test %! w = 1 + mod ((1:15)', 3); %! mw = fitmnr (Xm, y, 'Weights', w); %! [~, dev] = mnrfit (Xm, y, 'weights', w); %! assert_equal (mw.Deviance, dev); ## Test the methods against MATLAB R2024a %!test %! [lab, pr] = predict (m, Xm(1:3,:)); %! assert_equal (pr, [0.851674439820704, 0.143631440997612, ... %! 0.00469411918168434; 0.886831611467944, ... %! 0.101673529742501, 0.0114948587895541; ... %! 0.566508298983404, 0.400052723700518, ... %! 0.0334389773160776], 1e-10); %!test %! assert_equal (predict (m, Xm(1:3,:)), [1; 1; 1]); %!test %! [~, pr, lo, up] = predict (m, Xm(1:3,:)); %! [B, ~, s] = mnrfit (Xm, y); %! [~, dlo, dhi] = mnrval (B, Xm(1:3,:), s); %! assert_equal ([lo, up], [pr - dlo, pr + dhi], 1e-12); %!test %! [~, pr, lo] = predict (m, Xm(1:3,:), 'Alpha', 0.1); %! [B, ~, s] = mnrfit (Xm, y); %! [~, dlo] = mnrval (B, Xm(1:3,:), s, 'confidence', 0.9); %! assert_equal (lo, pr - dlo, 1e-12); %!test %! [~, pc] = predict (m, Xm(1:3,:), 'ProbabilityType', 'cumulative'); %! assert_equal (pc(:,2), [0.995305880818316; 0.988505141210446; ... %! 0.966561022683922], 1e-10); %!test %! [~, pk] = predict (m, Xm(1:3,:), 'ProbabilityType', 'conditional'); %! assert_equal (pk(:,2), [0.968352594279704; 0.898426946440983; ... %! 0.922861320671979], 1e-10); %!test %! assert_equal (feval (m, x(1:3), z(1:3)), predict (m, Xm(1:3,:))); %!test %! assert_equal (feval (m, x(1:3), 1), predict (m, [x(1:3), ones(3,1)])); %!test %! [p, F, r] = coefTest (m); %! assert_equal ([p, F, r], [0.407376657888557, 1.07023588159414, 6], 1e-8); %!test %! [p, F, r] = coefTest (m, [0, 1, 0, 0, 0, 0]); %! assert_equal ([p, F, r], [0.0681608433167721, 3.64792898304325, 1], 1e-8); %!test %! [p, F, r] = coefTest (m, [0, 1, 0, 0, 0, 0; 0, 0, 1, 0, 0, 0], [0.5; 0]); %! assert_equal ([p, F, r], [0.0377095939218408, 3.76922166716901, 2], 1e-8); %!test %! [p, stat] = testDeviance (m); %! assert_equal ([p, stat], [0.00749682238009594, 13.9376227503715], 1e-10); %!test %! mo = fitmnr (Xm, y, 'ModelType', 'ordinal'); %! [p, stat] = testDeviance (mo); %! assert_equal ([p, stat], [0.00312037814994931, 11.5396021651965], 1e-8); %!test %! md = fitmnr (Xm, y, 'EstimateDispersion', true); %! [p, stat] = testDeviance (md); %! assert_equal (stat, 13.9376227503715 / 4 / md.Dispersion, 1e-10); %!test %! assert_equal (coefCI (m), [-0.683764386648367, 12.5554619832596; ... %! -2.70835504376494, 0.104919108435981; ... %! -3.29315276302852, 2.34393006067183; ... %! -2.95654952910981, 8.32393988685627; ... %! -1.56040011006823, 0.109607143335903; ... %! -1.03107815065807, 2.7957265738081], 1e-8); %!test %! ci = coefCI (m, 0.1); %! t = tinv (0.95, 24); %! assert_equal (ci(:,2), m.Coefficients.Value + t * m.Coefficients.SE, 1e-12); %!test %! rs = random (m, Xm(1:3,:), 5); %! assert_equal (size (rs), [3, 5]); %!test %! rc = random (m, Xm(1:3,:), 4, 'counts'); %! assert_equal (sum (rc{:,:}, 2), [4; 4; 4]); %!test %! names = {'mid', 'lo', 'hi'}; %! mt = fitmnr (x, names(y)'); %! rc = random (mt, x(1:3), 2, 'counts'); %! assert_equal (rc.Properties.VariableNames, {'hi', 'lo', 'mid'}); %!test %! pd = partialDependence (m, 1, 1); %! assert_equal (size (pd), [1, 100]); ## Test the plots against MATLAB R2024a %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotResiduals (m); %! assert_equal (get (h, 'YData')(2,:), [0.4, 1/15, 1/30], 1e-12); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotResiduals (m); %! assert_equal (get (h, 'XData')(1,:), [0, 2, 4]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotResiduals (m, 'caseorder', 'ResidualType', 'raw', ... %! 'ClassToPlot', 2); %! assert_equal (get (h(1), 'YData')(:), m.Residuals.Raw(:,2)); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotResiduals (m, 'caseorder'); %! assert_equal (get (get (gca, 'Title'), 'String'), ... %! 'Case order plot of residuals'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotResiduals (m, 'lagged'); %! r = m.Residuals.Deviance; %! assert_equal (get (h(1), 'XData')(:), r(1:end-1)); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotResiduals (m, 'symmetry'); %! assert_equal (get (h(1), 'XData')(1:3), [0.0739143847548569, ... %! 0.439061974369248, 0.478480791323998], 1e-10); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotResiduals (m, 'histogram', 'ResidualType', 'raw', ... %! 'ClassToPlot', 3); %! assert_equal (get (h, 'YData')(2,:), [1, 9, 4, 1] / 7.5, 1e-12); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! plotResiduals (gca, m, 'probability'); %! assert_equal (get (get (gca, 'Title'), 'String'), ... %! 'Normal probability plot of residuals'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotSlice (m, 'histogram'); %! assert_equal (get (h(1), 'YData')(2,1:3), [0.689486884620728, ... %! 0.65928022754077, 0.627588553303704], 1e-10); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotSlice (m, 'histogram', 'PredictorToVary', 'x2'); %! assert_equal (get (h(1), 'YData')(2,1:3), [0.311392314971616, ... %! 0.299274789671695, 0.287250877321725], 1e-10); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotSlice (m, 'histogram', [4, 2]); %! assert_equal (get (h(1), 'XData')(1,[1, 40]), [4, 4.4], 1e-12); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotSlice (m, 'histogram', [4, 2; 6, 1]); %! assert_equal (get (h(1), 'YData')(2,1:3), [0.201897566085439, ... %! 0.195696318902365, 0.18959981331509], 1e-10); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotSlice (m, 'stackedhist'); %! assert_equal (get (h(2), 'YData')(1:3)(:), [0.305187356429808; ... %! 0.333819986299818; 0.363512252628566], 1e-10); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! dv = get (0, 'defaultfigurevisible'); %! set (0, 'defaultfigurevisible', 'off'); %! unwind_protect %! g = plotSlice (m); %! a = flipud (findobj (g, 'type', 'axes')); %! L = findobj (a(2), 'type', 'line', 'linestyle', '-'); %! assert_equal (get (L, 'YData')(1), 0.290726199748165, 1e-10); %! unwind_protect_cleanup %! close (g); %! set (0, 'defaultfigurevisible', dv); %! end_unwind_protect %!test %! dv = get (0, 'defaultfigurevisible'); %! set (0, 'defaultfigurevisible', 'off'); %! unwind_protect %! g = plotSlice (m, [4, 2]); %! a = flipud (findobj (g, 'type', 'axes')); %! L = findobj (a(1), 'type', 'line', 'linestyle', ':'); %! assert_equal (get (L, 'YData')(1), 0.0978608053565371, 1e-10); %! unwind_protect_cleanup %! close (g); %! set (0, 'defaultfigurevisible', dv); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotResiduals (m, 'fitted'); %! assert_equal (get (h(1), 'XData')(1:3), [0.851674439820703, ... %! 0.886831611467944, 0.566508298983404], 1e-10); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotResiduals (m, 'fitted', 'ResidualType', 'raw', 'ClassToPlot', 2); %! assert_equal (get (h(1), 'XData')(1:3), [0.143631440997612, ... %! 0.101673529742502, 0.400052723700518], 1e-10); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotResiduals (m, 'observed'); %! assert_equal (get (h(1), 'YData')(1:6), [1, 1, 0, 1, 0, 0]); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotResiduals (m, 'observed'); %! assert_equal (numel (h), 3); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! mo = fitmnr (Xm, y, 'ModelType', 'ordinal'); %! h = plotResiduals (mo, 'fitted'); %! assert_equal (get (h(1), 'XData')(1:3), [0.828677549406082, ... %! 0.702194243013224, 0.555964442786868], 1e-6); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!warning ... %! hf = figure ('visible', 'off'); %! unwind_protect %! plotResiduals (m, 'observed', 'ClassToPlot', 2); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error MultinomialRegression (1) %!error ... %! fitmnr (Xm, y, 'Foo', 1) %!error ... %! fitmnr (Xm, y, 'ModelType', 'linear') %!error ... %! fitmnr (Xm, y, 'ModelType', 1) %!error ... %! fitmnr (Xm, y, 'Link', 1) %!error ... %! fitmnr (Xm, y, 'EstimateDispersion', 2) %!error ... %! fitmnr (Xm, y, 'IncludeClassInteractions', 'on') %!error ... %! fitmnr (Xm, y, 'Weights', ones (3, 1)) %!error ... %! fitmnr ({1}, y) %!error ... %! fitmnr (Xm, y(1:5)) %!error ... %! fitmnr (Xm, y, 'PredictorNames', {'a'}) %!error ... %! fitmnr (table (x, y), 'w') %!error ... %! fitmnr (table (x, y), 'w ~ x') %!error ... %! fitmnr (table (x), y(1:5)) %!error ... %! fitmnr (x, ones (15, 1)) %!error ... %! fitmnr (NaN (15, 1), y) %!error ... %! fitmnr (Xm, y, 'CategoricalPredictors', 3) %!error ... %! fitmnr (Xm, y, 'CategoricalPredictors', {'w'}) %!error ... %! fitmnr (Xm, y, 'CategoricalPredictors', true) %!error predict (m) %!error ... %! predict (m, Xm, 'Foo', 1) %!error ... %! predict (m, Xm, 'Alpha', 2) %!error ... %! predict (m, Xm, 'ProbabilityType', 'other') %!error ... %! predict (m, Xm, 'ProbabilityType', 1) %!error ... %! predict (m, x) %!error ... %! predict (m, table (x, 'VariableNames', {'x1'})) %!error ... %! feval (m, 1, 2, 3) %!error ... %! feval (m, [1; 2], [1; 2; 3]) %!error random (m) %!error ... %! random (m, Xm, 0) %!error ... %! random (m, Xm, 'draws') %!error ... %! random (m, Xm, {1}) %!error ... %! coefCI (m, 1) %!error ... %! coefTest (m, [1, 0]) %!error ... %! coefTest (m, [1, 0, 0, 0, 0, 0], [1; 2]) %!error ... %! plotResiduals (m, 'boxes') %!error ... %! plotResiduals (m, 'caseorder', 'Foo', 1) %!error ... %! plotResiduals (m, 'ResidualType', 'scaled') %!error ... %! plotResiduals (m, 'ResidualType', 'raw', 'ClassToPlot', 7) %!error ... %! plotResiduals (fitmnr (Xm, y, 'ModelType', 'ordinal'), ... %! 'ResidualType', 'raw', 'ClassToPlot', 3) %!error ... %! plotResiduals (1, m) %!error ... %! plotSlice (gca, m) %!error ... %! plotSlice (m, 'histogram', 'Foo', 1) %!error ... %! plotSlice (m, 'ClassToPlot', 7) %!error ... %! plotSlice (m, 'histogram', 'PredictorToVary', 'w') %!error ... %! plotSlice (m, 'histogram', [NaN, 1]) %!error ... %! plotSlice (1, m) statistics-release-2.1.2/inst/Regression/NonLinearModel.m000066400000000000000000001245551527311104300234560ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef NonLinearModel < PredictiveModel ## -*- texinfo -*- ## @deftp {statistics} NonLinearModel ## ## Nonlinear regression model class. ## ## A @code{NonLinearModel} object holds a nonlinear regression fitted by ## @code{fitnlm}, together with its coefficients, fit statistics, and methods ## for inference, prediction, and diagnostics. Create a ## @code{NonLinearModel} object with @code{fitnlm} or the class constructor. ## ## The estimated coefficients and their statistics are in the ## @code{Coefficients} table; @code{Rsquared}, @code{ModelCriterion}, ## @code{LogLikelihood}, @code{RMSE}, @code{SSE}, @code{SST}, and @code{SSR} ## summarize the fit. The methods @code{predict}, @code{feval}, ## @code{random}, @code{coefCI}, @code{coefTest}, @code{plotResiduals}, ## @code{plotDiagnostics}, and @code{plotSlice} operate on the fitted model. ## ## @subheading Fit statistics ## ## The fit statistics follow MATLAB's conventions. @code{SSE} is the residual ## sum of squares, @code{SST} the total sum of squares of the response about ## its (weighted) mean, and @code{SSR} the regression sum of squares of the ## fitted values about that mean; because the model is nonlinear, @code{SST} ## does @emph{not} in general equal @code{SSR + SSE}. ## @code{Rsquared.Ordinary} is @code{1 - @var{SSE} / @var{SST}} and ## @code{Rsquared.Adjusted} corrects for the error degrees of freedom. ## @code{RMSE} is @code{sqrt (@var{MSE})}, and the Gaussian ## @code{LogLikelihood} uses the maximum-likelihood error variance ## @code{@var{SSE} / n}. The information criteria in @code{ModelCriterion} ## (@code{AIC}, @code{AICc}, @code{BIC}, @code{CAIC}) count the @math{p} ## coefficients as the only parameters; the error variance is @emph{not} ## counted. @code{coefTest} is a Wald test: for a contrast matrix @var{H} it ## forms ## @code{(@var{H}*b)' * inv (@var{H}*@var{V}*@var{H}') * (@var{H}*b) / r} with ## @var{V} the coefficient covariance and @math{r} the number of rows of ## @var{H}, referred to an @math{F} distribution on @math{r} and @var{DFE} ## degrees of freedom. The summary printed by @code{disp} instead reports an ## @math{F} statistic versus the zero model, formed from the uncorrected ## regression sum of squares (the sum of the squared fitted values). ## ## @seealso{fitnlm, nlinfit, nlparci, nlpredci, LinearModel, ## GeneralizedLinearModel} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} Coefficients ## ## Coefficient estimates and their statistics ## ## A table with one row per coefficient, its row names taken from ## @qcode{CoefficientNames}, and the variables @qcode{Estimate}, ## @qcode{SE}, @qcode{tStat} and @qcode{pValue}. @qcode{tStat} is the ## estimate divided by its standard error and @qcode{pValue} is the ## two-sided @math{t} test of a zero coefficient on @qcode{DFE} degrees ## of freedom. This property is read-only. ## ## @end deftp Coefficients = []; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} CoefficientNames ## ## Names of the coefficients ## ## A cell array of character vectors with one name per coefficient, in ## the order the model function expects them. The names default to ## @qcode{'b1'}, @qcode{'b2'} and so on, unless @code{fitnlm} was given a ## list of its own. This property is read-only. ## ## @end deftp CoefficientNames = {}; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} CoefficientCovariance ## ## Estimated covariance of the coefficients ## ## A square numeric matrix, one row and column per coefficient, holding ## the estimated covariance of the estimates in @qcode{Coefficients}. ## The square roots of its diagonal are the standard errors reported in ## column @qcode{SE} of that table. This property is read-only. ## ## @end deftp CoefficientCovariance = []; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} NumCoefficients ## ## Number of coefficients ## ## A positive integer counting the coefficients of the model, which is ## the number of elements of the starting vector handed to ## @code{fitnlm}. This property is read-only. ## ## @end deftp NumCoefficients = []; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} NumEstimatedCoefficients ## ## Number of estimated coefficients ## ## A positive integer counting the coefficients estimated from the data. ## A nonlinear fit estimates every coefficient it carries, so this equals ## @qcode{NumCoefficients}. This property is read-only. ## ## @end deftp NumEstimatedCoefficients = []; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} NumPredictors ## ## Number of predictors ## ## A positive integer counting the predictor variables, that is the ## columns of the predictor matrix used for the fit. This property is ## read-only. ## ## @end deftp NumPredictors = []; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} NumObservations ## ## Number of observations ## ## A positive integer counting the observations used for the fit, after ## the rows named by @qcode{'Exclude'} and the rows carrying missing ## values have been dropped. This property is read-only. ## ## @end deftp NumObservations = []; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} DFE ## ## Error degrees of freedom ## ## A nonnegative integer, @qcode{NumObservations} less ## @qcode{NumCoefficients}. This property is read-only. ## ## @end deftp DFE = []; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} MSE ## ## Mean squared error ## ## A positive scalar holding the estimated variance of the error term. ## This property is read-only. ## ## @end deftp MSE = []; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} RMSE ## ## Root mean squared error ## ## A positive scalar, the square root of @qcode{MSE}. This property is ## read-only. ## ## @end deftp RMSE = []; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} SSE ## ## Error sum of squares ## ## A nonnegative scalar, the sum of the squared residuals weighted by the ## observation weights. This property is read-only. ## ## @end deftp SSE = []; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} SST ## ## Total sum of squares ## ## A nonnegative scalar, the weighted sum of squared deviations of the ## response about its weighted mean. Because the model is nonlinear, ## @qcode{SST} does not in general equal @qcode{SSR} plus @qcode{SSE}. ## This property is read-only. ## ## @end deftp SST = []; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} SSR ## ## Regression sum of squares ## ## A nonnegative scalar, the weighted sum of squared deviations of the ## fitted values about the weighted mean of the response. This property ## is read-only. ## ## @end deftp SSR = []; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} LogLikelihood ## ## Log-likelihood of the fitted model ## ## A scalar, the Gaussian log-likelihood at the estimates, formed with ## the maximum-likelihood error variance @qcode{SSE} divided by ## @qcode{NumObservations}. This property is read-only. ## ## @end deftp LogLikelihood = []; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} ModelCriterion ## ## Information criteria ## ## A scalar structure with the fields @qcode{AIC}, @qcode{AICc}, ## @qcode{BIC} and @qcode{CAIC}. All four count the coefficients as the ## only parameters; the error variance is not counted. This property is ## read-only. ## ## @end deftp ModelCriterion = []; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} Rsquared ## ## Coefficient of determination ## ## A scalar structure with the fields @qcode{Ordinary} and ## @qcode{Adjusted}. @qcode{Ordinary} is one less the ratio of ## @qcode{SSE} to @qcode{SST}, and @qcode{Adjusted} corrects that ratio ## for the error degrees of freedom. This property is read-only. ## ## @end deftp Rsquared = []; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} Residuals ## ## Residuals of the fitted model ## ## A table with one row per observation used for the fit and the ## variables @qcode{Raw}, @qcode{Pearson}, @qcode{Standardized} and ## @qcode{Studentized}. This property is read-only. ## ## @end deftp Residuals = []; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} Fitted ## ## Fitted response values ## ## A numeric column vector with one fitted value per observation used for ## the fit. This property is read-only. ## ## @end deftp Fitted = []; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} Robust ## ## Robust fitting options ## ## Empty when the model was fitted by ordinary least squares. When ## @code{fitnlm} was given a robust weight function, a scalar structure ## whose field @qcode{RobustWgtFun} names it. This property is ## read-only. ## ## @end deftp Robust = []; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} Formula ## ## Model formula ## ## A character vector showing the fitted model, built from the model ## function together with the coefficient names and the response name. ## This property is read-only. ## ## @end deftp Formula = ''; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} ResponseName ## ## Name of the response variable ## ## A character vector naming the response. It defaults to @qcode{'y'} ## for a fit from matrices and is the name of the response column for a ## fit from a table. This property is read-only. ## ## @end deftp ResponseName = 'y'; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} PredictorNames ## ## Names of the predictor variables ## ## A cell array of character vectors with one name per predictor. The ## names default to @qcode{'x1'}, @qcode{'x2'} and so on for a fit from ## matrices, and are the column names for a fit from a table. This ## property is read-only. ## ## @end deftp PredictorNames = {}; ## -*- texinfo -*- ## @deftp {NonLinearModel} {property} VariableNames ## ## Names of all variables ## ## A cell array of character vectors holding @qcode{PredictorNames} ## followed by @qcode{ResponseName}. This property is read-only. ## ## @end deftp VariableNames = {}; endproperties ## Kept out of the documented surface. MATLAB carries the same information ## on its own class and does not expose it, so reading it here is an ## extension of ours. properties (GetAccess = public, SetAccess = protected, Hidden) ## The error model information that @code{nlinfit} returns for the fit. ErrorModelInfo = []; endproperties properties (Access = private, Hidden) modelfun_ = []; # model function handle @(b, X) beta_ = []; # fitted coefficient vector X_ = []; # numeric predictor matrix used for the fit y_ = []; # response vector used for the fit w_ = []; # observation weights (effective) J_ = []; # Jacobian at the solution R_ = []; # raw residuals at the solution leverage_ = []; # hat-matrix diagonal istable_ = false; # true when the model was fit from a table endproperties methods (Hidden) ## Custom display of the model summary. function disp (this) fprintf ("\n Nonlinear regression model:\n"); if (! isempty (this.Formula)) fprintf (" %s\n", this.Formula); endif if (! isempty (this.Coefficients)) fprintf ("\n Estimated Coefficients:\n\n"); disp (this.Coefficients); endif fprintf ("\n"); if (! isempty (this.NumObservations) && ! isempty (this.DFE)) fprintf (strcat ("Number of observations: %d,", ... " Error degrees of freedom: %d\n"), ... this.NumObservations, this.DFE); endif if (! isempty (this.RMSE)) fprintf ("Root Mean Squared Error: %.3g\n", this.RMSE); endif if (! isempty (this.Rsquared)) fprintf ("R-Squared: %.3g, Adjusted R-Squared %.3g\n", ... this.Rsquared.Ordinary, this.Rsquared.Adjusted); endif if (! isempty (this.beta_) && ! isempty (this.MSE)) ## F versus the zero model uses the uncorrected regression sum of ## squares (a nonlinear model has no guaranteed intercept term). yhat = this.Fitted; Fstat = (sum (yhat .^ 2) / this.NumCoefficients) / this.MSE; pval = fcdf (Fstat, this.NumCoefficients, this.DFE, 'upper'); fprintf ("F-statistic vs. zero model: %.3g, p-value = %.3g\n", ... Fstat, pval); endif endfunction ## Class specific subscripted reference. function varargout = subsref (this, s) chain_s = s(2:end); s = s(1); switch (s.type) case '()' error (strcat ("NonLinearModel: () indexing is not supported.", ... " Use dot notation for properties.")); case '{}' error (strcat ("NonLinearModel: {} indexing is not supported.", ... " Use dot notation for properties.")); case '.' if (! ischar (s.subs)) error (strcat ("NonLinearModel.subsref: property name must be", ... " a character vector.")); endif if (ismethod (this, s.subs)) [varargout{1:nargout}] = builtin ('subsref', this, [s, chain_s]); return; endif try out = this.(s.subs); catch error (strcat ("NonLinearModel.subsref: unknown property", ... " '%s'."), s.subs); end_try_catch endswitch if (! isempty (chain_s)) out = subsref (out, chain_s); endif varargout{1} = out; endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {NonLinearModel} {@var{mdl} =} NonLinearModel (@var{X}, @var{y}, @var{modelfun}, @var{beta0}) ## @deftypefnx {NonLinearModel} {@var{mdl} =} NonLinearModel (@var{tbl}, [], @var{modelfun}, @var{beta0}) ## @deftypefnx {NonLinearModel} {@var{mdl} =} NonLinearModel (@dots{}, @var{Name}, @var{Value}) ## ## Fit a nonlinear regression model. ## ## @code{@var{mdl} = NonLinearModel (@var{X}, @var{y}, @var{modelfun}, ## @var{beta0})} fits the nonlinear regression model ## @code{@var{y} = @var{modelfun} (@var{beta}, @var{X})} to the response ## vector @var{y} and the @math{n}-by-@math{p} predictor matrix @var{X}, ## starting the iterative fit from the coefficient vector @var{beta0}. ## @var{modelfun} is a function handle @code{@@(@var{b}, @var{X})} returning ## the fitted responses. ## ## @code{@var{mdl} = NonLinearModel (@var{tbl}, [], @var{modelfun}, ## @var{beta0})} takes the predictors and response from the table ## @var{tbl}; the last column is the response unless overridden by ## @qcode{'ResponseVar'}. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @multitable @columnfractions 0.2 0.75 ## @headitem Name @tab Value ## @item @qcode{'CoefficientNames'} @tab a cell array of names for the ## coefficients (default @qcode{'b1'}, @qcode{'b2'}, @dots{}). ## @item @qcode{'Weights'} @tab a vector of nonnegative observation weights. ## @item @qcode{'ErrorModel'} @tab the error-variance model: ## @qcode{'constant'} (default), @qcode{'proportional'}, or ## @qcode{'combined'}. ## @item @qcode{'RobustWgtFun'} @tab the name of a robust weight function, ## enabling robust fitting (see @code{nlinfit}). ## @item @qcode{'Options'} @tab a statset-style options structure ## controlling the iterative fit (@qcode{MaxIter}, @qcode{TolFun}, ## @qcode{TolX}). ## @item @qcode{'PredictorVars'}, @qcode{'ResponseVar'} @tab for table ## input, the predictor and response variable names. ## @item @qcode{'VarNames'} @tab a cell array of @math{p + 1} variable names ## (predictors followed by the response) for numeric @var{X}. ## @item @qcode{'Exclude'} @tab observations to exclude from the fit. ## @end multitable ## ## @code{fitnlm} makes the same fit and takes a table without the empty ## second argument. ## ## @seealso{fitnlm, nlinfit} ## @end deftypefn function this = NonLinearModel (data, resp, modelfun, beta0, varargin) if (nargin == 0) return; # empty object endif if (nargin < 4) error ("NonLinearModel: DATA, RESP, MODELFUN, and BETA0 are required."); endif if (! is_function_handle (modelfun)) error ("NonLinearModel: MODELFUN must be a function handle."); endif if (! (isnumeric (beta0) && isvector (beta0) && isreal (beta0))) error ("NonLinearModel: BETA0 must be a real numeric vector."); endif opts = nlm_parse_nv (varargin); beta0 = beta0(:); p = numel (beta0); ## ---------------------------------------------------------------- ## ## Intake: resolve the predictor matrix, response, and names. ## ---------------------------------------------------------------- ## if (istable (data)) this.istable_ = true; col_names = data.Properties.VariableNames; if (! isempty (opts.ResponseVar)) resp_name = opts.ResponseVar; else resp_name = col_names{end}; endif if (! isempty (opts.PredictorVars)) pred_names = opts.PredictorVars; else pred_names = col_names(! strcmp (col_names, resp_name)); endif y_full = double (data.(resp_name)(:)); X_full = zeros (numel (y_full), numel (pred_names)); for j = 1:numel (pred_names) X_full(:,j) = double (data.(pred_names{j})(:)); endfor else if (! (isnumeric (data) && isreal (data) && ismatrix (data))) error ("NonLinearModel: X must be a real matrix."); endif if (! (isnumeric (resp) && isreal (resp) && isvector (resp))) error ("NonLinearModel: Y must be a real vector."); endif X_full = double (data); y_full = double (resp(:)); if (rows (X_full) != numel (y_full)) error (strcat ("NonLinearModel: X and Y must have the same", ... " number of observations.")); endif p_raw = columns (X_full); if (! isempty (opts.VarNames)) if (numel (opts.VarNames) != p_raw + 1) error ("NonLinearModel: VarNames must have %d elements.", ... p_raw + 1); endif pred_names = opts.VarNames(1:p_raw)(:)'; resp_name = opts.VarNames{end}; else pred_names = arrayfun (@(k) sprintf ("x%d", k), 1:p_raw, ... 'UniformOutput', false); resp_name = 'y'; endif if (! isempty (opts.ResponseVar)) resp_name = opts.ResponseVar; endif endif ## Exclude and drop missing rows. n_total = numel (y_full); excluded = false (n_total, 1); if (! isempty (opts.Exclude)) ex = opts.Exclude(:); if (islogical (ex)) excluded(1:numel (ex)) = ex; else excluded(ex) = true; endif endif missing = any (isnan (X_full), 2) | isnan (y_full); keep = ! excluded & ! missing; X = X_full(keep,:); y = y_full(keep); n = numel (y); if (n <= p) error (strcat ("NonLinearModel: not enough observations to fit", ... " %d coefficients."), p); endif ## Coefficient names. if (! isempty (opts.CoefficientNames)) if (numel (opts.CoefficientNames) != p) error ("NonLinearModel: CoefficientNames must have %d elements.", p); endif coef_names = opts.CoefficientNames(:)'; else coef_names = arrayfun (@(k) sprintf ("b%d", k), 1:p, ... 'UniformOutput', false); endif ## ---------------------------------------------------------------- ## ## Fit via nlinfit, forwarding the fitting options. ## ---------------------------------------------------------------- ## nvfit = {}; if (! isempty (opts.Weights)) nvfit(end+1:end+2) = {"Weights", opts.Weights(keep)}; endif if (! isempty (opts.ErrorModel)) nvfit(end+1:end+2) = {"ErrorModel", opts.ErrorModel}; endif if (! isempty (opts.RobustWgtFun)) nvfit(end+1:end+2) = {"RobustWgtFun", opts.RobustWgtFun}; endif if (! isempty (opts.Tune)) nvfit(end+1:end+2) = {"Tune", opts.Tune}; endif if (! isempty (opts.Options)) nvfit(end+1:end+2) = {"Options", opts.Options}; endif [beta, R, J, CovB, MSE, EMI] = nlinfit (X, y, modelfun, beta0, nvfit{:}); ## ---------------------------------------------------------------- ## ## Populate the fit statistics. ## ---------------------------------------------------------------- ## dfe = n - p; yhat = y - R; if (isempty (opts.Weights)) w = ones (n, 1); else w = double (opts.Weights(keep)(:)); endif SSE = sum (w .* R .^ 2); ybar = sum (w .* y) / sum (w); SST = sum (w .* (y - ybar) .^ 2); SSR = sum (w .* (yhat - ybar) .^ 2); se = sqrt (diag (CovB)); tstat = beta ./ se; pval = 2 * tcdf (-abs (tstat), dfe); ## Hat-matrix diagonal from the weighted Jacobian. Jw = sqrt (w) .* J; H = Jw * pinv (Jw' * Jw) * Jw'; lev = diag (H); LL = -0.5 * n * (log (2 * pi) + log (SSE / n) + 1); AIC = -2 * LL + 2 * p; AICc = AIC + 2 * p * (p + 1) / max (n - p - 1, 1); BIC = -2 * LL + p * log (n); CAIC = -2 * LL + p * (log (n) + 1); ## Residuals table (raw, Pearson, standardized, studentized). rmse = sqrt (MSE); raw = R; pear = R .* sqrt (w); stnd = R ./ (rmse .* sqrt (max (1 - lev, eps))); s_i = sqrt (max ((dfe * MSE - (R .^ 2 .* w) ./ max (1 - lev, eps)) ... / max (dfe - 1, 1), 0)); stud = R ./ (s_i .* sqrt (max (1 - lev, eps)) + eps); ## Assemble the properties. this.modelfun_ = modelfun; this.beta_ = beta; this.X_ = X; this.y_ = y; this.w_ = w; this.J_ = J; this.R_ = R; this.leverage_ = lev; this.Coefficients = table (beta, se, tstat, pval, ... 'VariableNames', {'Estimate', 'SE', 'tStat', 'pValue'}, ... 'RowNames', coef_names); this.CoefficientNames = coef_names; this.CoefficientCovariance = CovB; this.NumCoefficients = p; this.NumEstimatedCoefficients = p; this.NumPredictors = columns (X); this.NumObservations = n; this.DFE = dfe; this.MSE = MSE; this.RMSE = rmse; this.SSE = SSE; this.SST = SST; this.SSR = SSR; this.LogLikelihood = LL; this.ModelCriterion = struct ('AIC', AIC, 'AICc', AICc, ... 'BIC', BIC, 'CAIC', CAIC); this.Rsquared = struct ('Ordinary', 1 - SSE / SST, ... 'Adjusted', 1 - (SSE / dfe) / (SST / (n - 1))); this.Residuals = table (raw, pear, stnd, stud, 'VariableNames', ... {'Raw', 'Pearson', 'Standardized', 'Studentized'}); this.Fitted = yhat; this.ErrorModelInfo = EMI; this.Robust = ternary (isempty (opts.RobustWgtFun), [], ... struct ('RobustWgtFun', opts.RobustWgtFun)); this.ResponseName = resp_name; this.PredictorNames = pred_names; this.VariableNames = [pred_names, {resp_name}]; this.Formula = build_formula_string (modelfun, coef_names, resp_name); endfunction ## -*- texinfo -*- ## @deftypefn {NonLinearModel} {@var{yhat} =} predict (@var{mdl}, @var{Xnew}) ## @deftypefnx {NonLinearModel} {[@var{yhat}, @var{yci}] =} predict (@var{mdl}, @var{Xnew}) ## @deftypefnx {NonLinearModel} {[@dots{}] =} predict (@dots{}, @var{Name}, @var{Value}) ## ## Predict responses of the nonlinear model @var{mdl} at the new predictor ## values @var{Xnew} (a numeric matrix or a table). With two outputs it ## also returns the confidence intervals @var{yci}. Accepts @qcode{'Alpha'} ## (default 0.05), @qcode{'Prediction'} (@qcode{'curve'} or ## @qcode{'observation'}), and @qcode{'Simultaneous'} (a logical). ## ## @end deftypefn function [yhat, yci] = predict (mdl, Xnew, varargin) alpha = 0.05; predtype = 'curve'; simul = false; for k = 1:2:numel (varargin) switch (lower (varargin{k})) case 'alpha' alpha = varargin{k+1}; case 'prediction' predtype = lower (varargin{k+1}); case 'simultaneous' simul = varargin{k+1}; otherwise error ("NonLinearModel.predict: unknown parameter '%s'.", ... varargin{k}); endswitch endfor Xq = mdl.resolve_predictors (Xnew); yhat = mdl.modelfun_ (mdl.beta_, Xq); yhat = yhat(:); if (nargout > 1) simopt = 'off'; if (simul) simopt = 'on'; endif [~, delta] = nlpredci (mdl.modelfun_, Xq, mdl.beta_, mdl.R_, ... 'Jacobian', mdl.J_, 'MSE', mdl.MSE, ... 'PredOpt', predtype, 'SimOpt', simopt, ... 'Alpha', alpha); yci = [yhat - delta, yhat + delta]; endif endfunction ## -*- texinfo -*- ## @deftypefn {NonLinearModel} {@var{yhat} =} feval (@var{mdl}, @var{X}) ## ## Evaluate the fitted model at the predictor values @var{X}, given either ## as a single matrix/table or as separate column arguments (one per ## predictor). ## ## @end deftypefn function yhat = feval (mdl, varargin) if (numel (varargin) == 1) Xq = mdl.resolve_predictors (varargin{1}); else Xq = cell2mat (cellfun (@(v) v(:), varargin, 'UniformOutput', false)); endif yhat = mdl.modelfun_ (mdl.beta_, Xq); yhat = yhat(:); endfunction ## -*- texinfo -*- ## @deftypefn {NonLinearModel} {@var{ysim} =} random (@var{mdl}, @var{Xnew}) ## ## Simulate responses from the fitted model at @var{Xnew} (default: the ## training predictors), adding Gaussian noise with the model's error ## standard deviation. ## ## @end deftypefn function ysim = random (mdl, Xnew) if (nargin < 2) Xq = mdl.X_; else Xq = mdl.resolve_predictors (Xnew); endif yhat = mdl.modelfun_ (mdl.beta_, Xq); yhat = yhat(:); ysim = yhat + sqrt (mdl.MSE) * randn (numel (yhat), 1); endfunction ## -*- texinfo -*- ## @deftypefn {NonLinearModel} {@var{ci} =} coefCI (@var{mdl}) ## @deftypefnx {NonLinearModel} {@var{ci} =} coefCI (@var{mdl}, @var{alpha}) ## ## Confidence intervals for the coefficients at level ## @math{100 (1 - @var{alpha})%} (default @var{alpha} = 0.05). ## ## @end deftypefn function ci = coefCI (mdl, alpha) if (nargin < 2) alpha = 0.05; endif se = mdl.Coefficients.SE; b = mdl.Coefficients.Estimate; t = tinv (1 - alpha / 2, mdl.DFE); ci = [b - t .* se, b + t .* se]; endfunction ## -*- texinfo -*- ## @deftypefn {NonLinearModel} {[@var{p}, @var{F}, @var{df}] =} coefTest (@var{mdl}) ## @deftypefnx {NonLinearModel} {[@dots{}] =} coefTest (@var{mdl}, @var{H}) ## ## Wald test of a linear hypothesis on the coefficients. With no @var{H} it ## tests that all coefficients are zero (the model versus the zero model) ## and returns the @math{p}-value @var{p}, the @math{F} statistic @var{F}, ## and its numerator degrees of freedom @var{df}. @var{H} is an ## @math{r}-by-@math{p} contrast matrix testing @code{@var{H} * @var{beta} ## = 0}. ## ## @end deftypefn function [p, F, df] = coefTest (mdl, H) b = mdl.beta_; if (nargin < 2) H = eye (numel (b)); endif df = rows (H); M = H * mdl.CoefficientCovariance * H'; F = ((H * b)' * pinv (M) * (H * b)) / df; p = fcdf (F, df, mdl.DFE, 'upper'); endfunction ## -*- texinfo -*- ## @deftypefn {NonLinearModel} {@var{h} =} plotResiduals (@var{mdl}) ## @deftypefnx {NonLinearModel} {@var{h} =} plotResiduals (@var{mdl}, @var{plottype}) ## ## Plot the model residuals. @var{plottype} is @qcode{'histogram'} ## (default), ## @qcode{'fitted'}, @qcode{'caseorder'}, or @qcode{'probability'}. ## ## @end deftypefn function h = plotResiduals (mdl, plottype, varargin) if (nargin < 2) plottype = 'histogram'; endif r = mdl.Residuals.Raw; switch (lower (plottype)) case 'histogram' h = hist (r); xlabel ("Residuals"); ylabel ("Frequency"); case 'fitted' h = plot (mdl.Fitted, r, 'o'); xlabel ("Fitted values"); ylabel ("Residuals"); hold on; plot (xlim (), [0, 0], 'k:'); hold off; case 'caseorder' h = plot (1:numel (r), r, 'o-'); xlabel ("Case order"); ylabel ("Residuals"); case 'probability' h = plot (sort (r), ((1:numel (r)) - 0.5) / numel (r), 'o'); xlabel ("Residuals"); ylabel ("Probability"); otherwise error ("NonLinearModel.plotResiduals: unknown plot type '%s'.", ... plottype); endswitch title ("Residuals"); endfunction ## -*- texinfo -*- ## @deftypefn {NonLinearModel} {@var{h} =} plotDiagnostics (@var{mdl}) ## @deftypefnx {NonLinearModel} {@var{h} =} plotDiagnostics (@var{mdl}, @var{plottype}) ## ## Plot fit diagnostics. @var{plottype} is @qcode{'leverage'} (default) or ## @qcode{'cookd'} (Cook's distance). ## ## @end deftypefn function h = plotDiagnostics (mdl, plottype) if (nargin < 2) plottype = 'leverage'; endif switch (lower (plottype)) case 'leverage' h = stem (mdl.leverage_); xlabel ("Observation"); ylabel ("Leverage"); case 'cookd' lev = mdl.leverage_; cd = (mdl.R_ .^ 2 ./ (mdl.NumCoefficients * mdl.MSE)) ... .* (lev ./ max (1 - lev, eps) .^ 2); h = stem (cd); xlabel ("Observation"); ylabel ("Cook's distance"); otherwise error ("NonLinearModel.plotDiagnostics: unknown plot type '%s'.", ... plottype); endswitch title ("Diagnostics"); endfunction ## -*- texinfo -*- ## @deftypefn {NonLinearModel} {@var{h} =} plotSlice (@var{mdl}) ## ## Plot the fitted response as each predictor is varied over its observed ## range with the others held at their means. ## ## @end deftypefn function h = plotSlice (mdl) pn = mdl.NumPredictors; xm = mean (mdl.X_, 1); h = zeros (pn, 1); for j = 1:pn subplot (1, pn, j); xj = linspace (min (mdl.X_(:,j)), max (mdl.X_(:,j)), 50)'; Xg = repmat (xm, numel (xj), 1); Xg(:,j) = xj; yg = mdl.modelfun_ (mdl.beta_, Xg); h(j) = plot (xj, yg(:)); xlabel (mdl.PredictorNames{j}); ylabel (mdl.ResponseName); endfor endfunction endmethods methods (Access = private) ## Resolve new predictor values from a matrix or a table to a numeric matrix ## matching the columns used at fit time. function Xq = resolve_predictors (mdl, Xnew) if (istable (Xnew)) Xq = zeros (height (Xnew), numel (mdl.PredictorNames)); for j = 1:numel (mdl.PredictorNames) Xq(:,j) = double (Xnew.(mdl.PredictorNames{j})(:)); endfor else Xq = double (Xnew); endif endfunction endmethods endclassdef ## --------------------------------------------------------------------------- ## Parse the fitnlm/NonLinearModel Name/Value options. function opts = nlm_parse_nv (nv) opts = struct ("CoefficientNames", [], "Weights", [], "ErrorModel", [], ... "RobustWgtFun", [], "Tune", [], "Options", [], ... "PredictorVars", [], "ResponseVar", [], "VarNames", [], ... "Exclude", []); if (mod (numel (nv), 2) != 0) error ("NonLinearModel: Name/Value arguments must come in pairs."); endif for k = 1:2:numel (nv) name = nv{k}; if (! ischar (name)) error ("NonLinearModel: parameter names must be character vectors."); endif switch (lower (name)) case 'coefficientnames' opts.CoefficientNames = nv{k+1}; case 'weights' opts.Weights = nv{k+1}; case 'errormodel' opts.ErrorModel = nv{k+1}; case 'robustwgtfun' opts.RobustWgtFun = nv{k+1}; case 'tune' opts.Tune = nv{k+1}; case 'options' opts.Options = nv{k+1}; case 'predictorvars' opts.PredictorVars = nv{k+1}; case 'responsevar' opts.ResponseVar = nv{k+1}; case 'varnames' opts.VarNames = nv{k+1}; case 'exclude' opts.Exclude = nv{k+1}; otherwise error ("NonLinearModel: unknown parameter name '%s'.", name); endswitch endfor endfunction ## --------------------------------------------------------------------------- ## Build the display formula "resp ~ body" from an anonymous model function, ## substituting b(k) -> coefname and stripping element-wise dots. function s = build_formula_string (modelfun, coef_names, resp_name) fstr = func2str (modelfun); ## Extract the argument names and the body of "@(b, x) body". tok = regexp (fstr, '^@\(([^)]*)\)(.*)$', 'tokens'); if (isempty (tok)) s = sprintf ("%s ~ f(b, X)", resp_name); return; endif args = strtrim (strsplit (tok{1}{1}, ',')); ## Strip whitespace first: func2str may render "b (1)" with a space. body = strrep (tok{1}{2}, " ", ""); bname = args{1}; ## Replace b(k) with the k-th coefficient name (longest index first). for k = numel (coef_names):-1:1 body = strrep (body, sprintf ("%s(%d)", bname, k), coef_names{k}); endfor ## Drop element-wise operator dots for a cleaner display. body = strrep (body, ".*", "*"); body = strrep (body, "./", "/"); body = strrep (body, ".^", "^"); s = sprintf ("%s ~ %s", resp_name, body); endfunction ## --------------------------------------------------------------------------- function out = ternary (cond, a, b) if (cond) out = a; else out = b; endif endfunction %!demo %! ## Fit an exponential growth model y = b1 * exp (b2 * x) and inspect it. %! x = (1:10)'; %! y = [2.1; 2.9; 4.2; 5.3; 7.1; 9.4; 12.8; 16.5; 22.1; 29.8]; %! modelfun = @(b, x) b(1) .* exp (b(2) .* x); %! mdl = fitnlm (x, y, modelfun, [1; 0.3]); %! disp (mdl.Coefficients) %! printf ("RMSE = %g, R^2 = %g\n", mdl.RMSE, mdl.Rsquared.Ordinary); ## Comprehensive property and method coverage %!shared X, y, modelfun, beta0 %! X = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]; %! y = [2.1; 2.9; 4.2; 5.3; 7.1; 9.4; 12.8; 16.5; 22.1; 29.8]; %! modelfun = @(b, x) b(1) .* exp (b(2) .* x); %! beta0 = [1; 0.3]; %!test # coefficient table (estimate, SE, tStat) verified against MATLAB %! mdl = fitnlm (X, y, modelfun, beta0); %! assert_equal (mdl.Coefficients.Estimate, [1.683747025; 0.286911087], 1e-6); %! assert_equal (mdl.Coefficients.SE, [0.035194899; 0.002350913], 1e-6); %! assert_equal (mdl.Coefficients.tStat, [47.8406555; 122.042406], -1e-4); %! assert_equal (mdl.Coefficients.tStat, ... %! mdl.Coefficients.Estimate ./ mdl.Coefficients.SE, 1e-8); %!test # sums of squares and their internal relationships %! mdl = fitnlm (X, y, modelfun, beta0); %! bhat = mdl.Coefficients.Estimate; fit = modelfun (bhat, X); %! assert_equal (mdl.Fitted, fit, 1e-8); %! assert_equal (mdl.SSE, sum ((y - fit) .^ 2), 1e-8); %! assert_equal (mdl.SST, sum ((y - mean (y)) .^ 2), 1e-6); %! assert_equal (mdl.SSR, sum ((fit - mean (y)) .^ 2), 1e-6); %! assert_equal (mdl.SSE, 0.233771954, 1e-7); %! assert_equal (mdl.SST, 750.976, 1e-3); %!test # MSE/RMSE/DFE and the coefficient of determination %! mdl = fitnlm (X, y, modelfun, beta0); %! assert_equal (mdl.DFE, 8); %! assert_equal (mdl.MSE, mdl.SSE / mdl.DFE, 1e-12); %! assert_equal (mdl.RMSE, sqrt (mdl.MSE), 1e-12); %! assert_equal (mdl.RMSE, 0.170942956, 1e-7); %! assert_equal (mdl.Rsquared.Ordinary, 1 - mdl.SSE / mdl.SST, 1e-12); %! assert_equal (mdl.Rsquared.Ordinary, 0.999688709, 1e-8); %! assert_equal (mdl.Rsquared.Adjusted, 0.999649798, 1e-8); %!test # log-likelihood and information criteria (values and identities) %! mdl = fitnlm (X, y, modelfun, beta0); %! ll = mdl.LogLikelihood; k = mdl.NumEstimatedCoefficients; n = 10; %! assert_equal (ll, 4.590586096, 1e-6); %! assert_equal (mdl.ModelCriterion.AIC, -5.181172193, 1e-6); %! assert_equal (mdl.ModelCriterion.BIC, -4.576002007, 1e-6); %! assert_equal (mdl.ModelCriterion.AIC, -2 * ll + 2 * k, 1e-9); %! assert_equal (mdl.ModelCriterion.BIC, -2 * ll + k * log (n), 1e-9); %! assert_equal (mdl.ModelCriterion.AICc, ... %! -2 * ll + 2 * k + 2 * k * (k + 1) / (n - k - 1), 1e-9); %!test # count/size properties and default names %! mdl = fitnlm (X, y, modelfun, beta0); %! assert_equal (mdl.NumCoefficients, 2); %! assert_equal (mdl.NumEstimatedCoefficients, 2); %! assert_equal (mdl.NumPredictors, 1); %! assert_equal (mdl.NumObservations, 10); %! assert_equal (mdl.CoefficientNames, {'b1', 'b2'}); %! assert_equal (mdl.ResponseName, "y"); %!test # the coefficient covariance is symmetric with SE^2 on the diagonal %! mdl = fitnlm (X, y, modelfun, beta0); %! C = mdl.CoefficientCovariance; %! assert_equal (size (C), [2, 2]); %! assert_equal (C, C', 1e-14); %! assert_equal (diag (C), mdl.Coefficients.SE .^ 2, 1e-12); %!test # raw residuals are response minus fit %! mdl = fitnlm (X, y, modelfun, beta0); %! assert_equal (class (mdl.Residuals), "table"); %! assert_equal (mdl.Residuals.Raw, y - mdl.Fitted, 1e-10); %!test # predict returns fitted values (verified against MATLAB) with CIs %! mdl = fitnlm (X, y, modelfun, beta0); %! [yhat, yci] = predict (mdl, [2.5; 5.5; 8.5]); %! assert_equal (yhat, [3.449741842; 8.158274281; 19.293455074], 1e-6); %! assert_equal (yci(:,1), [3.329126146; 7.997938483; 19.121921613], 1e-5); %! assert_equal (yci(:,2), [3.570357538; 8.318610079; 19.464988535], 1e-5); %! assert_equal (all (yci(:,1) <= yhat & yhat <= yci(:,2)), true); %!test # predict at the training data reproduces the fitted response %! mdl = fitnlm (X, y, modelfun, beta0); %! assert_equal (predict (mdl, X), mdl.Fitted, 1e-8); %!test # feval agrees with predict; random draws match the response size %! mdl = fitnlm (X, y, modelfun, beta0); %! assert_equal (feval (mdl, [2.5; 5.5]), predict (mdl, [2.5; 5.5]), 1e-12); %! ysim = random (mdl); %! assert_equal (size (ysim), [10, 1]); %!test # coefCI matches beta +/- t * SE and honours a custom alpha %! mdl = fitnlm (X, y, modelfun, beta0); %! b = mdl.Coefficients.Estimate; se = mdl.Coefficients.SE; %! t95 = tinv (0.975, mdl.DFE); %! assert_equal (coefCI (mdl), [b - t95 * se, b + t95 * se], 1e-12); %! t90 = tinv (0.95, mdl.DFE); %! assert_equal (coefCI (mdl, 0.10), [b - t90 * se, b + t90 * se], 1e-12); %!test # coefTest reports a Wald F statistic versus the zero model %! mdl = fitnlm (X, y, modelfun, beta0); %! [p, F, df] = coefTest (mdl); %! assert_equal (df, 2); %! assert_equal (F > 1e5, true); %! assert_equal (p < 1e-10, true); %!test # table input gives the same fit as matrix input %! tbl = table (X, y, "VariableNames", {'x', 'y'}); %! mdl = fitnlm (tbl, modelfun, beta0); %! assert_equal (mdl.Coefficients.Estimate, [1.683747025; 0.286911087], 1e-6); %! assert_equal (mdl.CoefficientNames, {'b1', 'b2'}); %!test # custom coefficient names are stored and used %! mdl = fitnlm (X, y, modelfun, beta0, "CoefficientNames", {'A', 'k'}); %! assert_equal (mdl.CoefficientNames, {'A', 'k'}); %!test # disp prints the model header and the coefficient table %! mdl = fitnlm (X, y, modelfun, beta0); %! s = evalc ("disp (mdl)"); %! assert_equal (isempty (strfind (s, "Nonlinear regression model")), false); %! assert_equal (isempty (strfind (s, "Estimate")), false); %!test # chained subsref reaches property -> table column -> element %! mdl = fitnlm (X, y, modelfun, beta0); %! assert_equal (numel (mdl.Coefficients.Estimate), 2); %! assert_equal (mdl.Coefficients.Estimate(1), 1.683747025, 1e-6); %!test # the residual and slice plots run without error %! mdl = fitnlm (X, y, modelfun, beta0); %! hf = figure ("visible", "off"); %! unwind_protect %! plotResiduals (mdl); %! plotResiduals (mdl, "fitted"); %! plotDiagnostics (mdl); %! plotSlice (mdl); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! ## Below the resolution of 1 - fcdf %! u = (1:30)'; %! m = fitnlm (u, 2 * u + 0.01 * sin (u), @(b, t) b(1) + b(2) * t, [1, 1]); %! [p, F, df] = coefTest (m); %! d2 = m.DFE; %! assert_equal (p, betainc (d2 / (d2 + df * F), d2 / 2, df / 2), -1e-12); ## Test input validation %!error NonLinearModel (1) %!error ... %! NonLinearModel ([1; 2], [1; 2], "bad", [1]) %!error ... %! mdl = fitnlm ([1;2;3;4], [1;2;3;4], @(b, x) b(1) * x, 1); mdl(1); statistics-release-2.1.2/inst/Regression/RepeatedMeasuresModel.m000066400000000000000000003343621527311104300250260ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . classdef RepeatedMeasuresModel ## -*- texinfo -*- ## @deftp {statistics} RepeatedMeasuresModel ## ## Repeated measures model. ## ## A @qcode{RepeatedMeasuresModel} object holds a multivariate linear model ## of several responses measured on the same subjects, the repeated ## measures, on between-subject predictors. Each row of the data is a ## subject and each response a measurement taken on it; the between-subject ## model describes how the mean of every response depends on the ## predictors, and the within-subject design describes how the responses ## relate to one another, as the levels of one or more within-subject ## factors. ## ## The between-subject coefficients are estimated by least squares, one ## column per response, with categorical predictors in effects coding: a ## predictor of @math{L} levels contributes @math{L - 1} columns named ## @qcode{@var{name}_@var{level}}, the last level coded -1 in all of them. ## @code{ranova} tests the within-subject effects, reporting the p-values ## corrected for departures from sphericity beside the uncorrected ones, ## @code{epsilon} gives those corrections and @code{mauchly} tests ## sphericity itself. ## ## Create a @qcode{RepeatedMeasuresModel} object with @code{fitrm}. ## ## @seealso{fitrm, anova2, manova1} ## @end deftp properties (GetAccess = public, SetAccess = protected) ## -*- texinfo -*- ## @deftp {RepeatedMeasuresModel} {property} BetweenDesign ## ## Design of the between-subject predictors ## ## The table the model was fitted to, one row per subject, responses ## included. A subject missing a response or a predictor is kept here, ## although the fit leaves it out. This property is read-only. ## ## @end deftp BetweenDesign = []; ## -*- texinfo -*- ## @deftp {RepeatedMeasuresModel} {property} BetweenModel ## ## Between-subject model ## ## A character vector holding the right side of the model formula, with ## its intercept, as in @qcode{'1 + species'}. This property is ## read-only. ## ## @end deftp BetweenModel = ''; ## -*- texinfo -*- ## @deftp {RepeatedMeasuresModel} {property} BetweenFactorNames ## ## Names of the between-subject predictors ## ## A cell row of character vectors naming the variables the ## between-subject model draws on, empty for a model with an intercept ## alone. This property is read-only. ## ## @end deftp BetweenFactorNames = {}; ## -*- texinfo -*- ## @deftp {RepeatedMeasuresModel} {property} ResponseNames ## ## Names of the responses ## ## A cell row of character vectors naming the repeated measures, in the ## order of the columns of @code{Coefficients}. This property is ## read-only. ## ## @end deftp ResponseNames = {}; ## -*- texinfo -*- ## @deftp {RepeatedMeasuresModel} {property} WithinDesign ## ## Design of the within-subject factors ## ## A table with one row per response, named by the responses, and one ## variable per within-subject factor. Without a design given at fitting ## it holds a single factor @qcode{Time} of the values 1 to @math{k} for ## @math{k} responses. This property is read-only. ## ## @end deftp WithinDesign = []; ## -*- texinfo -*- ## @deftp {RepeatedMeasuresModel} {property} WithinModel ## ## Within-subject model ## ## @qcode{'separatemeans'}, @qcode{'orthogonalcontrasts'}, a formula over ## the within-subject factors or a contrast matrix, as given at fitting. ## This property is read-only. ## ## @end deftp WithinModel = 'separatemeans'; ## -*- texinfo -*- ## @deftp {RepeatedMeasuresModel} {property} WithinFactorNames ## ## Names of the within-subject factors ## ## A cell row of character vectors, the variable names of ## @code{WithinDesign}. This property is read-only. ## ## @end deftp WithinFactorNames = {}; ## -*- texinfo -*- ## @deftp {RepeatedMeasuresModel} {property} Coefficients ## ## Estimated between-subject coefficients ## ## A table with one row per coefficient, named after the terms of the ## between-subject model, and one variable per response. This property is ## read-only. ## ## @end deftp Coefficients = []; ## -*- texinfo -*- ## @deftp {RepeatedMeasuresModel} {property} Covariance ## ## Estimated covariance of the responses ## ## A table holding the covariance of the residuals of the responses, ## their cross products over @code{DFE}, with rows and variables named by ## the responses. This property is read-only. ## ## @end deftp Covariance = []; ## -*- texinfo -*- ## @deftp {RepeatedMeasuresModel} {property} DFE ## ## Error degrees of freedom ## ## The number of subjects the fit used less the number of between-subject ## coefficients. This property is read-only. ## ## @end deftp DFE = []; endproperties properties (GetAccess = public, SetAccess = protected, Hidden) ## The between-subject design matrix, one row per subject of ## BetweenDesign, NaN on a row the fit left out. DesignMatrix = []; endproperties properties (Access = private, Hidden) ## Complete subjects' design matrix and residuals, the coefficients as a ## matrix, for each between term its name, its columns and its variables ## (the intercept's first, and none), and the coding of each variable. X_ = []; R_ = []; B_ = []; TermNames_ = {}; TermCols_ = {}; TermVars_ = {}; Coding_ = struct (); endproperties methods (Hidden) ## Custom display of the object name. function display (this) in_name = inputname (1); if (! isempty (in_name)) fprintf ("%s =\n", in_name); endif disp (this); endfunction ## Custom display of the model, grouped as MATLAB groups it. function disp (this) fprintf ("\n RepeatedMeasuresModel with properties:\n\n"); fprintf (" Between Subjects:\n"); fprintf ("%+22s: [%dx%d table]\n", 'BetweenDesign', ... size (this.BetweenDesign)); fprintf ("%+22s: %s\n", 'ResponseNames', cellrow (this.ResponseNames)); fprintf ("%+22s: %s\n", 'BetweenFactorNames', ... cellrow (this.BetweenFactorNames)); fprintf ("%+22s: '%s'\n\n", 'BetweenModel', this.BetweenModel); fprintf (" Within Subjects:\n"); fprintf ("%+22s: [%dx%d table]\n", 'WithinDesign', ... size (this.WithinDesign)); fprintf ("%+22s: %s\n", 'WithinFactorNames', ... cellrow (this.WithinFactorNames)); if (ischar (this.WithinModel)) fprintf ("%+22s: '%s'\n\n", 'WithinModel', this.WithinModel); else fprintf ("%+22s: [%dx%d double]\n\n", 'WithinModel', ... size (this.WithinModel)); endif fprintf (" Estimates:\n"); fprintf ("%+22s: [%dx%d table]\n", 'Coefficients', ... size (this.Coefficients)); fprintf ("%+22s: [%dx%d table]\n\n", 'Covariance', ... size (this.Covariance)); endfunction endmethods methods (Access = public) ## -*- texinfo -*- ## @deftypefn {RepeatedMeasuresModel} {@var{rm} =} RepeatedMeasuresModel (@var{t}, @var{modelspec}) ## @deftypefnx {RepeatedMeasuresModel} {@var{rm} =} RepeatedMeasuresModel (@var{t}, @var{modelspec}, @var{name}, @var{value}) ## ## Fit a repeated measures model. ## ## @code{@var{rm} = RepeatedMeasuresModel (@var{t}, @var{modelspec})} fits ## the repeated measures in the table @var{t} on the between-subject ## model given by the formula @var{modelspec}, a character vector or a ## string scalar of the form @qcode{'@var{responses} ~ @var{terms}'}. ## The responses are a range of the table's variables, as in ## @qcode{'y1-y6'}, which takes every variable from @qcode{y1} to ## @qcode{y6} in the table's order, a comma list, as in ## @qcode{'y1,y2,y3'}, or both. The terms are a Wilkinson formula over the ## other variables, as in @qcode{'species'} or @qcode{'g*x'}, or ## @qcode{'1'} for an intercept alone. A @qcode{categorical}, logical, ## text or string variable is a categorical predictor and any other a ## continuous one. ## ## A subject missing a response or a predictor is left out of the fit. ## ## The following name-value arguments are accepted. ## ## @multitable @columnfractions 0.25 0.75 ## @headitem Name @tab Value ## @item @qcode{'WithinDesign'} @tab The design of the within-subject ## factors: a table with one row per response and one variable per factor, ## or a numeric vector with one element per response, which becomes a ## single factor @qcode{Time}. The default is @qcode{Time} holding the ## values 1 to @math{k} for @math{k} responses. ## ## @item @qcode{'WithinModel'} @tab The within-subject model: ## @qcode{'separatemeans'}, the default, which compares the means of the ## responses; @qcode{'orthogonalcontrasts'}, which tests the orthogonal ## polynomial trends over a single numeric within-subject factor; a ## formula over the within-subject factors, as in @qcode{'A*B'}; or a ## contrast matrix with one row per response. ## @end multitable ## ## @qcode{'orthogonalcontrasts'} is refused at fitting unless the ## within-subject design holds a single numeric factor. MATLAB accepts ## the fit and refuses the model only when a test is asked of it. ## ## @seealso{fitrm} ## @end deftypefn function this = RepeatedMeasuresModel (t, modelspec, varargin) if (nargin < 2) error ("RepeatedMeasuresModel: too few input arguments."); endif if (! istable (t)) error ("RepeatedMeasuresModel: T must be a table."); endif if (isa (modelspec, 'string') && isscalar (modelspec)) modelspec = char (modelspec); endif if (! (ischar (modelspec) && isrow (modelspec))) error (strcat ("RepeatedMeasuresModel: MODELSPEC must be a", ... " character vector or a string scalar.")); endif ## Parse optional paired arguments. An empty WithinDesign is the ## default Time factor. optNames = {'WithinDesign', 'WithinModel'}; dfValues = {[], 'separatemeans'}; [WD, WM, args] = parsePairedArguments (optNames, dfValues, varargin(:)); if (! isempty (args)) error ("RepeatedMeasuresModel: invalid optional paired argument."); endif ## Responses and between-subject terms from the formula vnames = t.Properties.VariableNames; parts = strsplit (modelspec, '~'); if (numel (parts) != 2 || isempty (strtrim (parts{1})) ... || isempty (strtrim (parts{2}))) error (strcat ("RepeatedMeasuresModel: MODELSPEC must be of the", ... " form 'responses ~ terms'.")); endif [resp, errmsg] = responseNames (parts{1}, vnames); if (! isempty (errmsg)) error ("RepeatedMeasuresModel: %s", errmsg); endif rhs = strtrim (parts{2}); [terms, intercept, errmsg] = betweenTerms (rhs, vnames, resp); if (! isempty (errmsg)) error ("RepeatedMeasuresModel: %s", errmsg); endif Y = zeros (rows (t), numel (resp)); for j = 1:numel (resp) y = t.(resp{j}); if (! (isnumeric (y) && isreal (y) && isvector (y))) error ("RepeatedMeasuresModel: response '%s' must be numeric.", ... resp{j}); endif Y(:,j) = double (y(:)); endfor k = numel (resp); ## Effects-coded design over the complete subjects vars = unique ([terms{:}], 'stable'); [X, cnames, tcols, tnames, bad, coding] = effectsDesign (t, terms, ... intercept, vars); ok = ! bad & all (! isnan (Y), 2); ## A continuous predictor enters a marginal mean at its mean over the ## subjects the fit used, as in MATLAB for i = 1:numel (vars) if (! coding.(vars{i}).iscat) coding.(vars{i}).mean = mean (double (t.(vars{i})(ok))); endif endfor Xc = X(ok,:); Yc = Y(ok,:); if (rank (Xc) < columns (Xc) || rows (Xc) <= rank (Xc)) error (strcat ("RepeatedMeasuresModel: the between-subject", ... " design is rank deficient or leaves no error", ... " degrees of freedom.")); endif B = Xc \ Yc; R = Yc - Xc * B; dfe = rows (Xc) - rank (Xc); ## Within-subject design if (isempty (WD)) WD = table ((1:k)', 'VariableNames', {'Time'}, 'RowNames', resp); elseif (isnumeric (WD) && isvector (WD)) if (numel (WD) != k) error (strcat ("RepeatedMeasuresModel: 'WithinDesign' has %d", ... " points, where %d are required."), numel (WD), k); endif WD = table (double (WD(:)), 'VariableNames', {'Time'}, ... 'RowNames', resp); elseif (istable (WD)) if (rows (WD) != k) error (strcat ("RepeatedMeasuresModel: 'WithinDesign' has %d", ... " points, where %d are required."), rows (WD), k); endif WD.Properties.RowNames = resp; else error (strcat ("RepeatedMeasuresModel: 'WithinDesign' must be", ... " a table or a numeric vector.")); endif wnames = WD.Properties.VariableNames; [~, errmsg] = withinTerms (WM, WD, wnames, k); if (! isempty (errmsg)) error ("RepeatedMeasuresModel: %s", errmsg); endif if (isa (WM, 'string')) WM = char (WM); endif ## Populate the properties this.BetweenDesign = t; this.ResponseNames = resp; this.BetweenFactorNames = cell (1, 0); if (! isempty (vars)) this.BetweenFactorNames = vars; endif this.BetweenModel = modelText (rhs, intercept); this.WithinDesign = WD; this.WithinFactorNames = wnames; this.WithinModel = WM; this.Coefficients = array2table (B, 'VariableNames', resp, ... 'RowNames', cnames); this.Covariance = array2table (R' * R / dfe, 'VariableNames', resp, ... 'RowNames', resp); this.DFE = dfe; X(! ok,:) = NaN; this.DesignMatrix = X; this.X_ = Xc; this.R_ = R; this.B_ = B; this.TermNames_ = tnames; this.TermCols_ = tcols; this.TermVars_ = terms; this.Coding_ = coding; endfunction ## -*- texinfo -*- ## @deftypefn {RepeatedMeasuresModel} {@var{tbl} =} ranova (@var{rm}) ## @deftypefnx {RepeatedMeasuresModel} {@var{tbl} =} ranova (@var{rm}, @qcode{'WithinModel'}, @var{WM}) ## @deftypefnx {RepeatedMeasuresModel} {[@var{tbl}, @var{A}, @var{C}, @var{D}] =} ranova (@dots{}) ## ## Repeated measures analysis of variance. ## ## @code{@var{tbl} = ranova (@var{rm})} tests the within-subject effects of ## the repeated measures model @var{rm}: whether the means of the ## responses differ, and whether each between-subject term changes that ## difference. @var{tbl} holds one row per test and one per error term, ## with the variables @qcode{SumSq}, @qcode{DF}, @qcode{MeanSq}, @qcode{F} ## and @qcode{pValue}, and beside them the p-values under the ## Greenhouse-Geisser, Huynh-Feldt and lower bound corrections for ## departures from sphericity, @qcode{pValueGG}, @qcode{pValueHF} and ## @qcode{pValueLB}. The corrections are reported and never applied. ## ## @code{ranova (@var{rm}, @qcode{'WithinModel'}, @var{WM})} tests the ## within-subject model @var{WM} instead, which takes the forms the ## @qcode{'WithinModel'} argument of @code{fitrm} takes. The default is ## @qcode{'separatemeans'} whatever the model was fitted with, as in ## MATLAB. Under @qcode{'separatemeans'} or a contrast matrix the rows ## are named after the within-subject factor, or @qcode{Time} when there ## are several. A formula or @qcode{'orthogonalcontrasts'} gives one ## block of rows per within-subject term, the constant term first, whose ## tests are the between-subject tests of the mean of the responses. ## ## @code{[@var{tbl}, @var{A}, @var{C}, @var{D}] = ranova (@dots{})} also ## returns the hypothesis of each test as @math{A B C = D}: @var{A} a cell ## column holding the between-subject hypothesis matrix of each term, ## @var{C} the within-subject contrast, a cell row of one per term when ## there are several, and @var{D} zero. ## ## @code{'orthogonalcontrasts'} works here where MATLAB R2024a and R2026a ## both fail inside @code{ranova}; its results agree with those of ## MATLAB's @code{anova} for the same contrasts. ## ## @seealso{fitrm, RepeatedMeasuresModel.epsilon, ## RepeatedMeasuresModel.mauchly} ## @end deftypefn function [tbl, A, C, D] = ranova (this, varargin) [WM, args] = parsePairedArguments ({'WithinModel'}, {'separatemeans'}, ... varargin(:)); if (! isempty (args)) error ("RepeatedMeasuresModel.ranova: invalid optional paired argument."); endif k = numel (this.ResponseNames); [W, errmsg] = withinTerms (WM, this.WithinDesign, ... this.WithinFactorNames, k); if (! isempty (errmsg)) error ("RepeatedMeasuresModel.ranova: %s", errmsg); endif X = this.X_; E = this.R_' * this.R_; S = E / this.DFE; XtXi = inv (X' * X); nt = numel (this.TermNames_); A = cell (nt, 1); for i = 1:nt A{i} = eye (columns (X))(this.TermCols_{i},:); endfor names = {}; vals = zeros (0, 8); for w = 1:numel (W.C) Q = orth (W.C{w}); p = columns (Q); [gg, hf, lb] = sphericity (Q' * S * Q, p, this.DFE); SSE = trace (Q' * E * Q); dfE = p * this.DFE; for i = 1:nt AB = A{i} * this.B_; H = AB' * ((A{i} * XtXi * A{i}') \ AB); SSH = trace (Q' * H * Q); df = p * rows (A{i}); F = (SSH / df) / (SSE / dfE); vals(end+1,:) = [SSH, df, SSH / df, F, ... fcdf(F, df, dfE, 'upper'), ... fcdf(F, df * gg, dfE * gg, 'upper'), ... fcdf(F, df * hf, dfE * hf, 'upper'), ... fcdf(F, df * lb, dfE * lb, 'upper')]; if (isempty (W.names{w})) names{end+1} = this.TermNames_{i}; else names{end+1} = [this.TermNames_{i}, ':', W.names{w}]; endif endfor vals(end+1,:) = [SSE, dfE, SSE / dfE, NaN(1, 5)]; if (isempty (W.names{w})) names{end+1} = 'Error'; else names{end+1} = ['Error(', W.names{w}, ')']; endif endfor tbl = array2table (vals, 'VariableNames', {'SumSq', 'DF', 'MeanSq', ... 'F', 'pValue', 'pValueGG', 'pValueHF', 'pValueLB'}, ... 'RowNames', names); if (numel (W.C) == 1) C = W.C{1}; else C = W.C; endif D = 0; endfunction ## -*- texinfo -*- ## @deftypefn {RepeatedMeasuresModel} {@var{tbl} =} epsilon (@var{rm}) ## @deftypefnx {RepeatedMeasuresModel} {@var{tbl} =} epsilon (@var{rm}, @var{C}) ## ## Epsilon adjustments for repeated measures analysis of variance. ## ## @code{@var{tbl} = epsilon (@var{rm})} returns the corrections for ## departures from sphericity that @code{ranova} applies to its ## p-values, over the contrasts between successive responses: the ## variables @qcode{Uncorrected}, which is 1, @qcode{GreenhouseGeisser}, ## @qcode{HuynhFeldt} and @qcode{LowerBound}. The Huynh-Feldt value is ## Lecoutre's corrected form, capped at 1, and the lower bound is ## @math{1 / p} for @math{p} contrasts. ## ## @code{@var{tbl} = epsilon (@var{rm}, @var{C})} computes them over the ## contrast matrix @var{C}, which must have one row per response. ## ## @seealso{fitrm, RepeatedMeasuresModel.ranova, ## RepeatedMeasuresModel.mauchly} ## @end deftypefn function tbl = epsilon (this, C) if (nargin < 2) C = []; endif Q = contrastBasis (this, C, 'epsilon'); p = columns (Q); S = this.R_' * this.R_ / this.DFE; [gg, hf, lb] = sphericity (Q' * S * Q, p, this.DFE); tbl = array2table ([1, gg, hf, lb], 'VariableNames', {'Uncorrected', ... 'GreenhouseGeisser', 'HuynhFeldt', 'LowerBound'}); endfunction ## -*- texinfo -*- ## @deftypefn {RepeatedMeasuresModel} {@var{tbl} =} mauchly (@var{rm}) ## @deftypefnx {RepeatedMeasuresModel} {@var{tbl} =} mauchly (@var{rm}, @var{C}) ## ## Mauchly's test of sphericity. ## ## @code{@var{tbl} = mauchly (@var{rm})} tests whether the covariance of ## the contrasts between successive responses is a multiple of the ## identity, the sphericity that the uncorrected p-values of ## @code{ranova} assume. @var{tbl} holds Mauchly's @qcode{W}, the ## chi-square statistic @qcode{ChiStat} with Bartlett's factor, its ## degrees of freedom @qcode{DF} and the @qcode{pValue}. A singular ## covariance gives @var{W} 0, @var{ChiStat} @code{Inf} and a p-value of ## 0. ## ## @code{@var{tbl} = mauchly (@var{rm}, @var{C})} tests the contrasts of ## the matrix @var{C}, which must have one row per response. ## ## @seealso{fitrm, RepeatedMeasuresModel.ranova, ## RepeatedMeasuresModel.epsilon} ## @end deftypefn function tbl = mauchly (this, C) if (nargin < 2) C = []; endif Q = contrastBasis (this, C, 'mauchly'); p = columns (Q); S = Q' * (this.R_' * this.R_ / this.DFE) * Q; W = det (S) / (trace (S) / p) ^ p; df = p * (p + 1) / 2 - 1; ## Singularity is read from the conditioning, since rounding leaves the ## determinant of a singular matrix a small number of either sign if (rcond (S) < p * eps || ! (W > 0)) W = 0; chi = Inf; pval = 0; else chi = -(this.DFE - (2 * p ^ 2 + p + 2) / (6 * p)) * log (W); pval = chi2cdf (chi, df, 'upper'); if (df == 0) pval = 1; endif endif tbl = array2table ([W, chi, df, pval], 'VariableNames', ... {'W', 'ChiStat', 'DF', 'pValue'}); endfunction ## -*- texinfo -*- ## @deftypefn {RepeatedMeasuresModel} {@var{tbl} =} anova (@var{rm}) ## @deftypefnx {RepeatedMeasuresModel} {@var{tbl} =} anova (@var{rm}, @qcode{'WithinModel'}, @var{WM}) ## ## Analysis of variance for between-subject effects. ## ## @code{@var{tbl} = anova (@var{rm})} tests the between-subject terms of ## the repeated measures model @var{rm} on the average of the repeated ## measures, one univariate analysis of variance. @var{tbl} has the ## variables @qcode{Within}, the within-subject response analysed, ## @qcode{Between}, the between-subject term tested or @qcode{Error}, ## @qcode{SumSq}, @qcode{DF}, @qcode{MeanSq}, @qcode{F} and ## @qcode{pValue}. The intercept is named @qcode{constant}. ## ## @code{anova (@var{rm}, @qcode{'WithinModel'}, @var{WM})} analyses ## other responses built from the repeated measures, one block of rows ## each: ## ## @itemize ## @item @qcode{'separatemeans'}, the default: the average, named ## @qcode{Constant}. ## @item @qcode{'orthogonalcontrasts'}: the average and the orthogonal ## polynomial trends over a single numeric within-subject factor. ## @item A formula over the within-subject factors: each column of its ## effects-coded design, scaled to unit length and named after it. ## @item A contrast matrix with one row per response: each column as ## given, named @qcode{Contrast1}, @qcode{Contrast2}, @dots{} ## @end itemize ## ## MATLAB R2024a and R2026a fail on @qcode{'separatemeans'} given ## explicitly, though it is their documented default; here it is the ## default. ## ## @seealso{fitrm, RepeatedMeasuresModel.ranova, ## RepeatedMeasuresModel.manova} ## @end deftypefn function tbl = anova (this, varargin) [WM, args] = parsePairedArguments ({'WithinModel'}, {'separatemeans'}, ... varargin(:)); if (! isempty (args)) error ("RepeatedMeasuresModel.anova: invalid optional paired argument."); endif k = numel (this.ResponseNames); [W, errmsg] = withinTerms (WM, this.WithinDesign, ... this.WithinFactorNames, k); if (! isempty (errmsg)) error ("RepeatedMeasuresModel.anova: %s", errmsg); endif ## One response per column, and its name switch (W.kind) case 'separatemeans' Cs = {ones(k, 1) / sqrt(k)}; wn = {'Constant'}; case 'matrix' Cs = num2cell (W.C{1}, 1); wn = arrayfun (@(i) sprintf ('Contrast%d', i), 1:numel (Cs), ... 'UniformOutput', false); case 'orthogonalcontrasts' Cs = [{ones(k, 1) / sqrt(k)}, W.C(2:end)]; wn = W.cols; case 'formula' Cs = {}; wn = {}; for w = 1:numel (W.C) for j = 1:columns (W.C{w}) c = W.C{w}(:,j); Cs{end+1} = c / norm (c); wn{end+1} = W.cols{w}{j}; endfor endfor endswitch X = this.X_; E = this.R_' * this.R_; XtXi = inv (X' * X); bn = this.TermNames_; bn(strcmp (bn, '(Intercept)')) = {'constant'}; within = {}; between = {}; vals = zeros (0, 5); for w = 1:numel (Cs) c = Cs{w}; MSE = c' * E * c / this.DFE; for i = 1:numel (this.TermNames_) A = eye (columns (X))(this.TermCols_{i},:); Abc = A * this.B_ * c; SS = Abc' * ((A * XtXi * A') \ Abc); df = rows (A); F = (SS / df) / MSE; vals(end+1,:) = [SS, df, SS / df, F, fcdf(F, df, this.DFE, 'upper')]; within{end+1} = wn{w}; between{end+1} = bn{i}; endfor vals(end+1,:) = [MSE * this.DFE, this.DFE, MSE, NaN, NaN]; within{end+1} = wn{w}; between{end+1} = 'Error'; endfor tbl = table (categorical (within(:)), categorical (between(:)), ... vals(:,1), vals(:,2), vals(:,3), vals(:,4), vals(:,5), ... 'VariableNames', {'Within', 'Between', 'SumSq', 'DF', ... 'MeanSq', 'F', 'pValue'}); endfunction ## -*- texinfo -*- ## @deftypefn {RepeatedMeasuresModel} {@var{tbl} =} manova (@var{rm}) ## @deftypefnx {RepeatedMeasuresModel} {@var{tbl} =} manova (@var{rm}, @var{name}, @var{value}) ## @deftypefnx {RepeatedMeasuresModel} {[@var{tbl}, @var{A}, @var{C}, @var{D}] =} manova (@dots{}) ## ## Multivariate analysis of variance. ## ## @code{@var{tbl} = manova (@var{rm})} tests every term of the ## within-subject model of @var{rm}, as it was fitted, against every ## between-subject term, by the four multivariate statistics. @var{tbl} ## has the variables @qcode{Within}, @qcode{Between}, @qcode{Statistic}, ## one of @qcode{Pillai}, @qcode{Wilks}, @qcode{Hotelling} and ## @qcode{Roy}, its @qcode{Value}, the @qcode{F} statistic that ## approximates it, @qcode{RSquare}, the degrees of freedom @qcode{df1} ## and @qcode{df2}, and the @qcode{pValue}. Under ## @qcode{'separatemeans'} the within-subject hypothesis is that of equal ## means, named @qcode{Constant}, and a contrast matrix is named ## @qcode{Specified contrast}. ## ## The name-value arguments are @qcode{'WithinModel'}, which takes the ## forms @code{fitrm} takes but @qcode{'orthogonalcontrasts'}, and ## @qcode{'By'}, the name of a between-subject factor, which tests the ## within-subject hypotheses at each of its levels in place of the ## between-subject terms. ## ## @code{[@var{tbl}, @var{A}, @var{C}, @var{D}] = manova (@dots{})} also ## returns the hypotheses as @math{A B C = D}: @var{A} a cell column of ## the between-subject hypothesis matrices, @var{C} the within-subject ## contrast, a cell row of one per term when there are several, and ## @var{D} zero. ## ## Pillai's trace, Wilks' lambda and Roy's root are approximated by F as ## in MATLAB. The Hotelling-Lawley trace uses McKeon's F approximation ## with its own second degrees of freedom, as SAS does, and the ## Pillai-Samson one where McKeon's is undefined. MATLAB R2024a computes ## McKeon's F but refers it to the Pillai-Samson degrees of freedom, ## which makes its p-values too small. ## ## @seealso{fitrm, RepeatedMeasuresModel.coeftest, ## RepeatedMeasuresModel.ranova} ## @end deftypefn function [tbl, A, C, D] = manova (this, varargin) [WM, By, args] = parsePairedArguments ({'WithinModel', 'By'}, ... {this.WithinModel, ''}, ... varargin(:)); if (! isempty (args)) error ("RepeatedMeasuresModel.manova: invalid optional paired argument."); endif if ((ischar (WM) || isa (WM, 'string')) ... && strcmpi (char (WM), 'orthogonalcontrasts')) error (strcat ("RepeatedMeasuresModel.manova: the", ... " 'orthogonalcontrasts' model cannot be used with", ... " manova.")); endif k = numel (this.ResponseNames); [W, errmsg] = withinTerms (WM, this.WithinDesign, ... this.WithinFactorNames, k); if (! isempty (errmsg)) error ("RepeatedMeasuresModel.manova: %s", errmsg); endif switch (W.kind) case 'separatemeans' wn = {'Constant'}; case 'matrix' wn = {'Specified contrast'}; otherwise wn = W.names; wn(cellfun (@isempty, wn)) = {'(Intercept)'}; endswitch ## The between-subject hypotheses: each term, or each level of BY X = this.X_; nc = columns (X); if (isempty (By)) A = cell (numel (this.TermNames_), 1); for i = 1:numel (A) A{i} = eye (nc)(this.TermCols_{i},:); endfor bn = this.TermNames_; else [A, bn, errmsg] = byHypotheses (this, By); if (! isempty (errmsg)) error ("RepeatedMeasuresModel.manova: %s", errmsg); endif endif stat = {'Pillai'; 'Wilks'; 'Hotelling'; 'Roy'}; within = {}; between = {}; vals = zeros (0, 6); for w = 1:numel (W.C) for i = 1:numel (A) vals = [vals; mvtests(this, A{i}, W.C{w}, 0)]; within = [within; repmat(wn(w), 4, 1)]; between = [between; repmat(bn(i), 4, 1)]; endfor endfor tbl = table (categorical (within), categorical (between), ... categorical (repmat (stat, rows (vals) / 4, 1)), ... vals(:,1), vals(:,2), vals(:,3), vals(:,4), vals(:,5), ... vals(:,6), 'VariableNames', {'Within', 'Between', ... 'Statistic', 'Value', 'F', 'RSquare', 'df1', 'df2', ... 'pValue'}); if (numel (W.C) == 1) C = W.C{1}; else C = W.C; endif D = 0; endfunction ## -*- texinfo -*- ## @deftypefn {RepeatedMeasuresModel} {@var{tbl} =} coeftest (@var{rm}, @var{A}, @var{C}) ## @deftypefnx {RepeatedMeasuresModel} {@var{tbl} =} coeftest (@var{rm}, @var{A}, @var{C}, @var{D}) ## ## Linear hypothesis test on the coefficients of a repeated measures ## model. ## ## @code{@var{tbl} = coeftest (@var{rm}, @var{A}, @var{C})} tests the ## hypothesis @math{A B C = 0} on the coefficient matrix @math{B} of ## @var{rm}, @var{A} having one column per between-subject coefficient ## and @var{C} one row per response. @var{tbl} holds the four ## multivariate statistics as @code{manova} reports them: the variables ## @qcode{Statistic}, @qcode{Value}, @qcode{F}, @qcode{RSquare}, ## @qcode{df1}, @qcode{df2} and @qcode{pValue}. ## ## @code{@var{tbl} = coeftest (@var{rm}, @var{A}, @var{C}, @var{D})} tests ## @math{A B C = D} instead, @var{D} a scalar or a matrix with as many ## rows as @var{A} and as many columns as @var{C}. The default is 0. ## ## @seealso{fitrm, RepeatedMeasuresModel.manova} ## @end deftypefn function tbl = coeftest (this, A, C, D) if (nargin < 3) error ("RepeatedMeasuresModel.coeftest: too few input arguments."); endif nc = columns (this.X_); k = numel (this.ResponseNames); if (! (isnumeric (A) && isreal (A) && ismatrix (A) && columns (A) == nc ... && ! isempty (A))) error ("RepeatedMeasuresModel.coeftest: A must be a matrix with %d columns.", ... nc); endif if (! (isnumeric (C) && isreal (C) && ismatrix (C) && rows (C) == k ... && ! isempty (C))) error ("RepeatedMeasuresModel.coeftest: C must be a matrix with %d rows.", ... k); endif if (nargin < 4) D = 0; endif if (! (isnumeric (D) && isreal (D) && (isscalar (D) ... || isequal (size (D), [rows(A), columns(C)])))) error (strcat ("RepeatedMeasuresModel.coeftest: D must be a scalar", ... " or a %d-by-%d matrix."), rows (A), columns (C)); endif vals = mvtests (this, double (A), double (C), double (D)); tbl = table (categorical ({'Pillai'; 'Wilks'; 'Hotelling'; 'Roy'}), ... vals(:,1), vals(:,2), vals(:,3), vals(:,4), vals(:,5), ... vals(:,6), 'VariableNames', {'Statistic', 'Value', 'F', ... 'RSquare', 'df1', 'df2', 'pValue'}); endfunction ## -*- texinfo -*- ## @deftypefn {RepeatedMeasuresModel} {@var{tbl} =} margmean (@var{rm}, @var{vars}) ## @deftypefnx {RepeatedMeasuresModel} {@var{tbl} =} margmean (@var{rm}, @var{vars}, @qcode{'Alpha'}, @var{alpha}) ## ## Estimated marginal means. ## ## @code{@var{tbl} = margmean (@var{rm}, @var{vars})} estimates the mean ## of the repeated measures at each combination of the levels of the ## factors named by @var{vars}, a character vector or a cell array of ## them, each a categorical between-subject factor or a within-subject ## factor. The other between-subject factors are averaged over their ## levels with equal weights, a continuous predictor is held at its mean ## over the subjects the fit used, and the responses are averaged over ## the other within-subject factors. @var{tbl} holds one variable per ## factor, the first varying slowest, and @qcode{Mean}, @qcode{StdErr}, ## @qcode{Lower} and @qcode{Upper}, the limits of a @math{100 (1 - ## alpha)} per cent confidence interval on @code{DFE} degrees of freedom. ## The default @var{alpha} is 0.05. ## ## @seealso{fitrm, RepeatedMeasuresModel.multcompare, ## RepeatedMeasuresModel.grpstats} ## @end deftypefn function tbl = margmean (this, vars, varargin) if (nargin < 2) error ("RepeatedMeasuresModel.margmean: too few input arguments."); endif [alpha, args] = parsePairedArguments ({'Alpha'}, {0.05}, varargin(:)); if (! isempty (args)) error ("RepeatedMeasuresModel.margmean: invalid optional paired argument."); endif checkAlpha (alpha, 'margmean'); [F, errmsg] = factorInfo (this, vars); if (! isempty (errmsg)) error ("RepeatedMeasuresModel.margmean: %s", errmsg); endif L = combos (F); S = this.R_' * this.R_ / this.DFE; V = inv (this.X_' * this.X_); est = zeros (rows (L), 4); tq = tinv (1 - alpha / 2, this.DFE); for r = 1:rows (L) [a, w] = weights (this, F, L(r,:)); m = a * this.B_ * w; se = sqrt ((a * V * a') * (w' * S * w)); est(r,:) = [m, se, m - tq * se, m + tq * se]; endfor tbl = levelTable (F, L, {}); tbl = [tbl, array2table(est, 'VariableNames', ... {'Mean', 'StdErr', 'Lower', 'Upper'})]; endfunction ## -*- texinfo -*- ## @deftypefn {RepeatedMeasuresModel} {@var{tbl} =} grpstats (@var{rm}, @var{g}) ## @deftypefnx {RepeatedMeasuresModel} {@var{tbl} =} grpstats (@var{rm}, @var{g}, @var{stats}) ## ## Descriptive statistics of the repeated measures by group. ## ## @code{@var{tbl} = grpstats (@var{rm}, @var{g})} pools the repeated ## measures of every subject and summarizes them at each combination of ## the levels of the factors named by @var{g}, a character vector or a ## cell array of them, each a categorical between-subject factor or a ## within-subject factor. @var{tbl} holds one variable per factor, ## @qcode{GroupCount}, the number of values in the group, and their ## @qcode{mean} and @qcode{std}. ## ## @code{@var{tbl} = grpstats (@var{rm}, @var{g}, @var{stats})} computes ## the statistics named by @var{stats} instead, a character vector or a ## cell array of them, each one of @qcode{'mean'}, @qcode{'median'}, ## @qcode{'std'}, @qcode{'var'}, @qcode{'sem'}, @qcode{'min'}, ## @qcode{'max'}, @qcode{'range'} and @qcode{'numel'}. Missing values are ## left out. ## ## @seealso{fitrm, RepeatedMeasuresModel.margmean} ## @end deftypefn function tbl = grpstats (this, g, stats) if (nargin < 2) error ("RepeatedMeasuresModel.grpstats: too few input arguments."); endif if (nargin < 3) stats = {'mean', 'std'}; endif if (ischar (stats) || (isa (stats, 'string') && isscalar (stats))) stats = cellstr (stats); endif known = {'mean', 'median', 'std', 'var', 'sem', 'min', 'max', ... 'range', 'numel'}; if (! iscellstr (stats) || ! all (ismember (stats, known))) error (strcat ("RepeatedMeasuresModel.grpstats: STATS must name", ... " statistics among 'mean', 'median', 'std',", ... " 'var', 'sem', 'min', 'max', 'range' and", ... " 'numel'.")); endif [F, errmsg] = factorInfo (this, g); if (! isempty (errmsg)) error ("RepeatedMeasuresModel.grpstats: %s", errmsg); endif L = combos (F); Y = zeros (rows (this.BetweenDesign), numel (this.ResponseNames)); for j = 1:columns (Y) Y(:,j) = double (this.BetweenDesign.(this.ResponseNames{j})); endfor keep = true (rows (L), 1); vals = zeros (rows (L), numel (stats) + 1); for r = 1:rows (L) [rows_, cols_] = groupMembers (this, F, L(r,:)); y = Y(rows_, cols_)(:); y = y(! isnan (y)); keep(r) = ! isempty (y); vals(r,1) = numel (y); for i = 1:numel (stats) vals(r,i+1) = groupStatistic (y, stats{i}); endfor endfor tbl = levelTable (F, L(keep,:), {}); tbl = [tbl, array2table(vals(keep,:), 'VariableNames', ... [{'GroupCount'}, stats(:)'])]; endfunction ## -*- texinfo -*- ## @deftypefn {RepeatedMeasuresModel} {@var{tbl} =} multcompare (@var{rm}, @var{var}) ## @deftypefnx {RepeatedMeasuresModel} {@var{tbl} =} multcompare (@var{rm}, @var{var}, @var{name}, @var{value}) ## ## Multiple comparison of estimated marginal means. ## ## @code{@var{tbl} = multcompare (@var{rm}, @var{var})} compares the ## estimated marginal means of every pair of levels of the factor ## @var{var}, a categorical between-subject factor or a within-subject ## factor, as @code{margmean} estimates them. @var{tbl} holds one row per ## ordered pair, with the variables @qcode{@var{var}_1} and ## @qcode{@var{var}_2}, @qcode{Difference}, @qcode{StdErr}, the adjusted ## @qcode{pValue}, and @qcode{Lower} and @qcode{Upper}, the limits of the ## simultaneous confidence interval. ## ## The name-value arguments are: ## ## @multitable @columnfractions 0.25 0.75 ## @headitem Name @tab Value ## @item @qcode{'By'} @tab The name of another factor; the comparisons are ## made at each of its levels, which become the first variable of ## @var{tbl}. ## ## @item @qcode{'ComparisonType'} @tab @qcode{'tukey-kramer'}, the ## default, @qcode{'bonferroni'}, @qcode{'dunn-sidak'}, @qcode{'lsd'} or ## @qcode{'scheffe'}. ## ## @item @qcode{'Alpha'} @tab The significance level of the intervals. ## The default is 0.05. ## @end multitable ## ## The Tukey-Kramer p-values and limits come from the studentized range ## distribution, @code{stdrcdf} and @code{stdrinv}. MATLAB R2024a and ## R2026a floor their Tukey-Kramer p-values, 9.56e-10 over three groups ## whatever the difference, and R2024a's Dunn-Sidak p-values read 0 where ## the Bonferroni ones are near 1e-36; both are computed here. ## ## @seealso{fitrm, RepeatedMeasuresModel.margmean, stdrcdf} ## @end deftypefn function tbl = multcompare (this, var, varargin) if (nargin < 2) error ("RepeatedMeasuresModel.multcompare: too few input arguments."); endif [by, ctype, alpha, args] = parsePairedArguments ( ... {'By', 'ComparisonType', 'Alpha'}, ... {'', 'tukey-kramer', 0.05}, varargin(:)); if (! isempty (args)) error ("RepeatedMeasuresModel.multcompare: invalid optional paired argument."); endif checkAlpha (alpha, 'multcompare'); types = {'tukey-kramer', 'bonferroni', 'dunn-sidak', 'lsd', 'scheffe'}; if (! ((ischar (ctype) || isa (ctype, 'string')) ... && any (strcmpi (char (ctype), types)))) error (strcat ("RepeatedMeasuresModel.multcompare: 'ComparisonType'", ... " must be 'tukey-kramer', 'bonferroni',", ... " 'dunn-sidak', 'lsd' or 'scheffe'.")); endif ctype = lower (char (ctype)); if (isempty (by)) names = {var}; else names = {by, var}; if (strcmp (char (by), char (var))) error (strcat ("RepeatedMeasuresModel.multcompare: 'By' must", ... " differ from the factor compared.")); endif endif [F, errmsg] = factorInfo (this, names); if (! isempty (errmsg)) error ("RepeatedMeasuresModel.multcompare: %s", errmsg); endif nb = 1; if (numel (F) == 2) nb = numel (F(1).levels); endif m = numel (F(end).levels); npairs = m * (m - 1) / 2; S = this.R_' * this.R_ / this.DFE; V = inv (this.X_' * this.X_); crit = critical (m, npairs, this.DFE, ctype, alpha); L = zeros (0, numel (F) + 1); vals = zeros (0, 5); for b = 1:nb ## A pair and its reverse share their p-value pp = NaN (m); for i = 1:m for j = [1:i-1, i+1:m] if (numel (F) == 2) [ai, wi] = weights (this, F, [b, i]); [aj, wj] = weights (this, F, [b, j]); else [ai, wi] = weights (this, F, i); [aj, wj] = weights (this, F, j); endif d = ai * this.B_ * wi - aj * this.B_ * wj; if (F(end).between) se = sqrt (((ai - aj) * V * (ai - aj)') * (wi' * S * wi)); else se = sqrt ((ai * V * ai') * ((wi - wj)' * S * (wi - wj))); endif if (isnan (pp(j,i))) pp(i,j) = pairwise (abs (d) / se, m, npairs, this.DFE, ctype); else pp(i,j) = pp(j,i); endif pv = pp(i,j); vals(end+1,:) = [d, se, pv, d - crit * se, d + crit * se]; if (numel (F) == 2) L(end+1,:) = [b, i, j]; else L(end+1,:) = [i, j]; endif endfor endfor endfor if (numel (F) == 1) G = [F, F]; else G = [F, F(2)]; endif vn = {sprintf('%s_1', F(end).name), sprintf('%s_2', F(end).name)}; if (numel (F) == 2) vn = [{F(1).name}, vn]; endif tbl = levelTable (G, L, vn); tbl = [tbl, array2table(vals, 'VariableNames', ... {'Difference', 'StdErr', 'pValue', 'Lower', 'Upper'})]; endfunction ## -*- texinfo -*- ## @deftypefn {RepeatedMeasuresModel} {@var{ypred} =} predict (@var{rm}) ## @deftypefnx {RepeatedMeasuresModel} {@var{ypred} =} predict (@var{rm}, @var{tnew}) ## @deftypefnx {RepeatedMeasuresModel} {@var{ypred} =} predict (@dots{}, @var{name}, @var{value}) ## @deftypefnx {RepeatedMeasuresModel} {[@var{ypred}, @var{yci}] =} predict (@dots{}) ## ## Predict the repeated measures. ## ## @code{@var{ypred} = predict (@var{rm}, @var{tnew})} returns the ## predicted repeated measures of the subjects in the table @var{tnew}, ## one row each, from their between-subject predictors. @var{tnew} ## defaults to @code{BetweenDesign}. A subject missing a predictor, or ## holding a level the model was not fitted on, is predicted as ## @code{NaN}. ## ## Under @qcode{'separatemeans'} the prediction is one column per ## response. Under any other within-subject model the means are smoothed ## through that model: projected onto the terms of a formula, or onto the ## polynomial of @qcode{'orthogonalcontrasts'}, and evaluated at the ## within-subject design, which may then be a new one. ## ## The name-value arguments are @qcode{'WithinDesign'}, the within-subject ## design to predict at, as @code{fitrm} takes it; @qcode{'WithinModel'}, ## the within-subject model, by default the one fitted; and ## @qcode{'Alpha'}, the significance level of the intervals, 0.05 by ## default. Under @qcode{'separatemeans'} a new within-subject design is ## ignored with a warning, as in MATLAB. ## ## @code{[@var{ypred}, @var{yci}] = predict (@dots{})} also returns the ## confidence limits of the predicted means as an array of the size of ## @var{ypred} by 2, the lower limits first. ## ## @seealso{fitrm, RepeatedMeasuresModel.random} ## @end deftypefn function [ypred, yci] = predict (this, varargin) tnew = this.BetweenDesign; if (numel (varargin) > 0 && istable (varargin{1})) tnew = varargin{1}; varargin(1) = []; endif [WDn, WM, alpha, args] = parsePairedArguments ( ... {'WithinDesign', 'WithinModel', 'Alpha'}, ... {[], this.WithinModel, 0.05}, varargin(:)); if (! isempty (args)) error ("RepeatedMeasuresModel.predict: invalid optional paired argument."); endif checkAlpha (alpha, 'predict'); [Xn, errmsg] = newDesign (this, tnew); if (! isempty (errmsg)) error ("RepeatedMeasuresModel.predict: %s", errmsg); endif [P, errmsg] = smoother (this, WM, WDn); if (! isempty (errmsg)) error ("RepeatedMeasuresModel.predict: %s", errmsg); endif ypred = Xn * this.B_ * P; if (nargout > 1) S = this.R_' * this.R_ / this.DFE; vx = sum ((Xn / (this.X_' * this.X_)) .* Xn, 2); se = sqrt (vx .* sum (P .* (S * P), 1)); tq = tinv (1 - alpha / 2, this.DFE); yci = cat (3, ypred - tq * se, ypred + tq * se); endif endfunction ## -*- texinfo -*- ## @deftypefn {RepeatedMeasuresModel} {@var{ysim} =} random (@var{rm}) ## @deftypefnx {RepeatedMeasuresModel} {@var{ysim} =} random (@var{rm}, @var{tnew}) ## ## Generate new random repeated measures. ## ## @code{@var{ysim} = random (@var{rm}, @var{tnew})} draws one set of ## repeated measures for each subject of the table @var{tnew}, from a ## normal distribution with the subject's predicted means and the ## estimated covariance @code{Covariance}. @var{tnew} defaults to ## @code{BetweenDesign}. A subject missing a predictor gets a row of ## @code{NaN}. ## ## @seealso{fitrm, RepeatedMeasuresModel.predict, mvnrnd} ## @end deftypefn function ysim = random (this, tnew) if (nargin < 2) tnew = this.BetweenDesign; elseif (! istable (tnew)) error ("RepeatedMeasuresModel.random: TNEW must be a table."); endif [Xn, errmsg] = newDesign (this, tnew); if (! isempty (errmsg)) error ("RepeatedMeasuresModel.random: %s", errmsg); endif S = this.R_' * this.R_ / this.DFE; mu = Xn * this.B_; ysim = NaN (size (mu)); ok = all (isfinite (mu), 2); if (any (ok)) ysim(ok,:) = mvnrnd (mu(ok,:), S); endif endfunction ## -*- texinfo -*- ## @deftypefn {RepeatedMeasuresModel} {@var{h} =} plot (@var{rm}) ## @deftypefnx {RepeatedMeasuresModel} {@var{h} =} plot (@var{rm}, @var{name}, @var{value}) ## ## Plot the repeated measures of every subject. ## ## @code{plot (@var{rm})} draws one line per subject of ## @code{BetweenDesign} through its repeated measures, against the ## positions 1 to @math{k} of the @math{k} responses, and returns the ## column of line handles @var{h}. ## ## The name-value arguments are @qcode{'Group'}, the name of a ## categorical between-subject factor or a cell array of them, which ## colours the lines by group and adds a legend with one entry per group; ## @qcode{'Marker'}, the marker, @qcode{'s'} by default; and ## @qcode{'LineStyle'}, the line style, @qcode{'-'} by default. ## ## @seealso{fitrm, RepeatedMeasuresModel.plotprofile} ## @end deftypefn function h = plot (this, varargin) [grp, marker, ls, args] = parsePairedArguments ( ... {'Group', 'Marker', 'LineStyle'}, {'', 's', '-'}, varargin(:)); if (! isempty (args)) error ("RepeatedMeasuresModel.plot: invalid optional paired argument."); endif k = numel (this.ResponseNames); n = rows (this.BetweenDesign); Y = zeros (n, k); for j = 1:k Y(:,j) = double (this.BetweenDesign.(this.ResponseNames{j})); endfor gi = ones (n, 1); labels = {}; if (! isempty (grp)) [F, errmsg] = factorInfo (this, grp); if (isempty (errmsg) && ! all ([F.between])) errmsg = "'Group' must name between-subject factors."; endif if (! isempty (errmsg)) error ("RepeatedMeasuresModel.plot: %s", errmsg); endif L = combos (F); gi = zeros (n, 1); for r = 1:rows (L) gi(groupMembers (this, F, L(r,:))) = r; labels{r} = groupLabel (F, L(r,:)); endfor endif ax = newplot (); co = get (ax, 'ColorOrder'); h = zeros (n, 1); for i = 1:n c = co(mod (max (gi(i), 1) - 1, rows (co)) + 1,:); h(i) = line (1:k, Y(i,:), 'Marker', marker, 'LineStyle', ls, ... 'Color', c, 'Parent', ax); endfor if (! isempty (labels)) [u, first] = unique (gi, 'first'); keep = u > 0; legend (h(first(keep)), labels(u(keep))); endif endfunction ## -*- texinfo -*- ## @deftypefn {RepeatedMeasuresModel} {@var{h} =} plotprofile (@var{rm}, @var{X}) ## @deftypefnx {RepeatedMeasuresModel} {@var{h} =} plotprofile (@var{rm}, @var{X}, @var{name}, @var{value}) ## ## Plot the estimated marginal means. ## ## @code{plotprofile (@var{rm}, @var{X})} draws the estimated marginal ## means of @code{margmean} over the levels of the factor @var{X}, a ## categorical between-subject factor or a within-subject factor, and ## returns the line handles @var{h}. A numeric within-subject factor is ## drawn at its values; any other at the positions 1 to @math{L} of its ## @math{L} levels, labelled with them. ## ## The name-value arguments are @qcode{'Group'}, the name of another ## factor, which draws one line per level of it and adds a legend; ## @qcode{'Marker'}, @qcode{'o'} by default; and @qcode{'LineStyle'}, ## @qcode{'-'} by default. ## ## @seealso{fitrm, RepeatedMeasuresModel.margmean, ## RepeatedMeasuresModel.plot} ## @end deftypefn function h = plotprofile (this, X, varargin) if (nargin < 2) error ("RepeatedMeasuresModel.plotprofile: too few input arguments."); endif [grp, marker, ls, args] = parsePairedArguments ( ... {'Group', 'Marker', 'LineStyle'}, {'', 'o', '-'}, varargin(:)); if (! isempty (args)) error ("RepeatedMeasuresModel.plotprofile: invalid optional paired argument."); endif if (! (ischar (X) && isrow (X)) && ! (isa (X, 'string') && isscalar (X))) error ("RepeatedMeasuresModel.plotprofile: X must name one factor."); endif X = char (X); names = {X}; if (! isempty (grp)) names = [cellstr(grp), names]; endif [F, errmsg] = factorInfo (this, names); if (! isempty (errmsg)) error ("RepeatedMeasuresModel.plotprofile: %s", errmsg); endif tbl = margmean (this, names); nl = numel (F(end).levels); m = reshape (tbl.Mean, nl, []); if (! F(end).between && isnumeric (this.WithinDesign.(X))) x = [F(end).native{:}]; ticks = {}; else x = 1:nl; ticks = F(end).levels; endif ax = newplot (); co = get (ax, 'ColorOrder'); h = zeros (columns (m), 1); for g = 1:columns (m) h(g) = line (x, m(:,g)', 'Marker', marker, 'LineStyle', ls, ... 'Color', co(mod (g - 1, rows (co)) + 1,:), 'Parent', ax); endfor if (! isempty (ticks)) set (ax, 'XTick', x, 'XTickLabel', ticks); endif xlabel (ax, X); ylabel (ax, 'Estimated marginal means'); if (numel (F) > 1) G = combos (F(1:end-1)); labels = arrayfun (@(r) groupLabel (F(1:end-1), G(r,:)), ... 1:rows (G), 'UniformOutput', false); legend (h, labels); endif endfunction endmethods methods (Access = private) ## An orthonormal basis of the contrast C, or of the contrasts between ## successive responses when C is empty. function Q = contrastBasis (this, C, meth) k = numel (this.ResponseNames); if (isempty (C)) C = successive (k); elseif (! (isnumeric (C) && isreal (C) && ismatrix (C) && rows (C) == k)) error ("RepeatedMeasuresModel.%s: C must be a matrix with %d rows.", ... meth, k); endif Q = orth (double (C)); endfunction ## The factors named by NAMES, each categorical and either between or ## within: its name, side, level keys and native values. function [F, errmsg] = factorInfo (this, names) F = struct ('name', {}, 'between', {}, 'levels', {}, 'native', {}); errmsg = ''; if (ischar (names) || (isa (names, 'string') && isscalar (names))) names = cellstr (names); endif if (! iscellstr (names) || isempty (names)) errmsg = "factor names must be text."; return; endif for i = 1:numel (names) nm = names{i}; if (any (strcmp (nm, this.BetweenFactorNames)) ... && this.Coding_.(nm).iscat) c = this.Coding_.(nm); F(end+1) = struct ('name', nm, 'between', true, ... 'levels', {c.levels}, 'native', {c.native}); elseif (any (strcmp (nm, this.WithinFactorNames))) [lev, ~, native, iscat] = levelsOf (this.WithinDesign.(nm)); if (! iscat) v = this.WithinDesign.(nm); u = unique (v(:)); lev = arrayfun (@num2str, u, 'UniformOutput', false); native = num2cell (u); endif F(end+1) = struct ('name', nm, 'between', false, ... 'levels', {lev}, 'native', {native}); else errmsg = sprintf ("'%s' is not a categorical factor of the model.", nm); return; endif endfor endfunction ## The between-subject row A and the within-subject weights W at the ## levels IDX of the factors F, the other factors averaged. function [a, w] = weights (this, F, idx) fixed = struct (); k = numel (this.ResponseNames); w = ones (k, 1); for i = 1:numel (F) if (F(i).between) fixed.(F(i).name) = idx(i); else v = this.WithinDesign.(F(i).name); if (isnumeric (v)) w &= (double (v(:)) == F(i).native{idx(i)}); else w &= strcmp (cellstr (v(:)), F(i).levels{idx(i)}); endif endif endfor w = double (w) / sum (w); a = zeros (1, columns (this.X_)); off = numel (this.TermNames_) - numel (this.TermVars_); if (off) a(1) = 1; endif for t = 1:numel (this.TermVars_) M = 1; for v = this.TermVars_{t} c = this.Coding_.(v{1}); if (! c.iscat) e = c.mean; elseif (isfield (fixed, v{1})) L = numel (c.levels); e = zeros (1, L - 1); l = fixed.(v{1}); if (l < L) e(l) = 1; else e(:) = -1; endif else e = zeros (1, numel (c.levels) - 1); endif M = kron (M, e); endfor a(this.TermCols_{t+off}) = M; endfor endfunction ## The subjects and responses at the levels IDX of the factors F. function [r, c] = groupMembers (this, F, idx) r = true (rows (this.BetweenDesign), 1); c = true (numel (this.ResponseNames), 1); for i = 1:numel (F) if (F(i).between) [~, ic] = levelsOf (this.BetweenDesign.(F(i).name), F(i).levels); r &= (ic(:) == idx(i)); else v = this.WithinDesign.(F(i).name); if (isnumeric (v)) c &= (double (v(:)) == F(i).native{idx(i)}); else c &= strcmp (cellstr (v(:)), F(i).levels{idx(i)}); endif endif endfor endfunction ## The effects-coded design of the subjects of TNEW, NaN on a row missing ## a predictor or holding a level the model does not know. function [X, errmsg] = newDesign (this, tnew) X = []; errmsg = ''; if (! istable (tnew)) errmsg = "TNEW must be a table."; return; endif miss = setdiff (this.BetweenFactorNames, tnew.Properties.VariableNames); if (! isempty (miss)) errmsg = sprintf ("TNEW has no variable '%s'.", miss{1}); return; endif intercept = numel (this.TermNames_) > numel (this.TermVars_); [X, ~, ~, ~, bad] = effectsDesign (tnew, this.TermVars_, intercept, ... this.BetweenFactorNames, this.Coding_); X(bad,:) = NaN; endfunction ## The matrix that maps fitted means over the responses onto the ## predictions at the within-subject design WDN under the model WM. function [P, errmsg] = smoother (this, WM, WDn) errmsg = ''; k = numel (this.ResponseNames); P = []; if (isa (WM, 'string') && isscalar (WM)) WM = char (WM); endif if (isnumeric (WM) || strcmpi (WM, 'separatemeans')) if (! isempty (WDn)) warning (strcat ("RepeatedMeasuresModel.predict: the", ... " 'separatemeans' model does not use the", ... " 'WithinDesign' given.")); endif P = eye (k); return; endif WD = this.WithinDesign; wn = this.WithinFactorNames; if (isempty (WDn)) WDn = WD; elseif (isnumeric (WDn) && isvector (WDn) && numel (wn) == 1) WDn = table (double (WDn(:)), 'VariableNames', wn); elseif (! istable (WDn) || ! all (ismember (wn, WDn.Properties.VariableNames))) errmsg = "'WithinDesign' must hold the within-subject factors."; return; endif [~, errmsg] = withinTerms (WM, WD, wn, k); if (! isempty (errmsg)) return; endif if (strcmpi (WM, 'orthogonalcontrasts')) v = double (WD.(wn{1})); mu = mean (v); sc = max (1, std (v)); Wd = ((v(:) - mu) / sc) .^ (0:k-1); Wn = ((double (WDn.(wn{1}))(:) - mu) / sc) .^ (0:k-1); else s = parseWilkinsonFormula (['~', WM]); model = s.model; empty = cellfun (@isempty, model); terms = cellfun (@cellstr, model(! empty), 'UniformOutput', false); vars = unique ([terms{:}], 'stable'); intercept = any (empty) || isempty (model); [Wd, ~, ~, ~, ~, coding] = effectsDesign (WD, terms, intercept, vars); [Wn, ~, ~, ~, bad] = effectsDesign (WDn, terms, intercept, vars, coding); Wn(bad,:) = NaN; endif P = (Wn * pinv (Wd))'; endfunction ## The four multivariate statistics of A B C = D, one row each of Value, ## F, RSquare, df1, df2 and pValue. function vals = mvtests (this, A, C, D) X = this.X_; Q = orth (C); T = C \ Q; E = Q' * (this.R_' * this.R_) * Q; M = (A * this.B_ * C - D) * T; H = M' * ((A / (X' * X) * A') \ M); lam = sort (max (real (eig (E \ H)), 0), 'descend'); vals = multivariateF (lam, columns (Q), rank (A), this.DFE); endfunction ## The hypotheses at each level of the between-subject factor BY: the ## intercept and BY's own columns at that level's effects code. function [A, names, errmsg] = byHypotheses (this, by) A = {}; names = {}; errmsg = ''; by = char (by); if (! any (strcmp (by, this.BetweenFactorNames))) errmsg = sprintf ("'By' must name a between-subject factor, not '%s'.", ... by); return; endif v = this.BetweenDesign.(by); if (iscategorical (v)) lev = categories (v); lev = lev(ismember (lev, cellstr (v))); elseif (iscellstr (v) || isa (v, 'string') || ischar (v)) lev = unique (cellstr (v)); else errmsg = sprintf ("'By' must name a categorical factor, not '%s'.", by); return; endif col = find (strcmp (this.TermNames_, by)); L = numel (lev); for l = 1:L a = zeros (1, columns (this.X_)); a(1) = 1; e = zeros (1, L - 1); if (l < L) e(l) = 1; else e(:) = -1; endif a(this.TermCols_{col}) = e; A{end+1,1} = a; names{end+1} = sprintf ('%s=%s', by, lev{l}); endfor endfunction endmethods endclassdef ## The contrasts between successive responses, one column per pair. function C = successive (k) C = [eye(k - 1); zeros(1, k - 1)] - [zeros(1, k - 1); eye(k - 1)]; endfunction ## Greenhouse-Geisser, Huynh-Feldt and lower bound corrections from the ## covariance S of P orthonormal contrasts on DFE error degrees of freedom. ## The Huynh-Feldt value is Lecoutre's corrected form, capped at 1. function [gg, hf, lb] = sphericity (S, p, dfe) gg = trace (S) ^ 2 / (p * trace (S ^ 2)); hf = min (1, ((dfe + 1) * p * gg - 2) / (p * (dfe - p * gg))); lb = 1 / p; endfunction ## Every combination of the levels of the factors F, the first varying ## slowest, one row of level indices each. function L = combos (F) n = cellfun (@numel, {F.levels}); L = zeros (prod (n), numel (n)); for i = 1:numel (n) reps = prod (n(i+1:end)); L(:,i) = repmat (kron ((1:n(i))', ones (reps, 1)), prod (n(1:i-1)), 1); endfor endfunction ## The legend label of the group at the levels L of the factors F, as MATLAB ## writes it, 'species=setosa' or 'g=1,A=2'. function s = groupLabel (F, L) parts = arrayfun (@(i) sprintf ('%s=%s', F(i).name, F(i).levels{L(i)}), ... 1:numel (F), 'UniformOutput', false); s = strjoin (parts, ','); endfunction ## A table of the levels L of the factors F, one variable per column of L, ## named NAMES or after the factors, in the type each factor was given in. function tbl = levelTable (F, L, names) if (isempty (names)) names = {F.name}; endif cols = cell (1, columns (L)); for i = 1:columns (L) nat = F(i).native(L(:,i)); if (iscell (nat{1})) cols{i} = [nat{:}]'; else cols{i} = vertcat (nat{:}); endif endfor tbl = table (cols{:}, 'VariableNames', names); endfunction ## One statistic of the values Y. function v = groupStatistic (y, name) switch (name) case 'mean' v = mean (y); case 'median' v = median (y); case 'std' v = std (y); case 'var' v = var (y); case 'sem' v = std (y) / sqrt (numel (y)); case 'min' v = min (y); case 'max' v = max (y); case 'range' v = max (y) - min (y); case 'numel' v = numel (y); endswitch endfunction ## The adjusted p-value of a pairwise comparison with |t| = T among M means, ## NPAIRS pairs and DFE error degrees of freedom. The two-sided t tail is the ## incomplete beta function, which keeps its digits far out where tcdf ## rounds to zero. function p = pairwise (T, m, npairs, dfe, ctype) p2 = betainc (T ^ 2 / (dfe + T ^ 2), 1/2, dfe / 2, 'upper'); switch (ctype) case 'tukey-kramer' p = stdrcdf (sqrt (2) * T, m, dfe, 'upper'); case 'bonferroni' p = min (1, npairs * p2); case 'dunn-sidak' p = - expm1 (npairs * log1p (- p2)); case 'lsd' p = p2; case 'scheffe' p = fcdf (T ^ 2 / (m - 1), m - 1, dfe, 'upper'); endswitch endfunction ## The multiple of the standard error that gives simultaneous limits at ## level ALPHA for the comparison type CTYPE. function crit = critical (m, npairs, dfe, ctype, alpha) switch (ctype) case 'tukey-kramer' crit = stdrinv (1 - alpha, m, dfe) / sqrt (2); case 'bonferroni' crit = tinv (1 - alpha / (2 * npairs), dfe); case 'dunn-sidak' crit = tinv (1 - (1 - (1 - alpha) ^ (1 / npairs)) / 2, dfe); case 'lsd' crit = tinv (1 - alpha / 2, dfe); case 'scheffe' crit = sqrt ((m - 1) * finv (1 - alpha, m - 1, dfe)); endswitch endfunction ## Refuse an ALPHA outside (0, 1). function checkAlpha (alpha, meth) if (! (isnumeric (alpha) && isscalar (alpha) && alpha > 0 && alpha < 1)) error ("RepeatedMeasuresModel.%s: 'Alpha' must be a scalar in (0, 1).", ... meth); endif endfunction ## Pillai's trace, Wilks' lambda, the Hotelling-Lawley trace and Roy's root ## from the eigenvalues LAM of inv (E) * H, for P contrasts, a hypothesis of ## rank Q and DFE error degrees of freedom: one row each of Value, F, ## RSquare, df1, df2 and pValue. The Hotelling-Lawley trace uses McKeon's F ## approximation where it is defined and the Pillai-Samson one elsewhere, ## which is exact for a single nonzero eigenvalue. function vals = multivariateF (lam, p, q, dfe) s = min (p, q); m = (abs (p - q) - 1) / 2; n = (dfe - p - 1) / 2; vals = zeros (4, 6); V = sum (lam ./ (1 + lam)); r2 = V / s; d1 = s * (2 * m + s + 1); d2 = s * (2 * n + s + 1); vals(1,:) = [V, d2 / d1 * r2 / (1 - r2), r2, d1, d2, 0]; L = prod (1 ./ (1 + lam)); if (p ^ 2 + q ^ 2 - 5 > 0) t = sqrt ((p ^ 2 * q ^ 2 - 4) / (p ^ 2 + q ^ 2 - 5)); else t = 1; endif r2 = 1 - L ^ (1 / t); d1 = p * q; d2 = (dfe - (p - q + 1) / 2) * t - (p * q - 2) / 2; vals(2,:) = [L, d2 / d1 * r2 / (1 - r2), r2, d1, d2, 0]; T = sum (lam); r2 = (T / s) / (1 + T / s); if (s > 1 && n > 1) b = (p + 2 * n) * (q + 2 * n) / (2 * (2 * n + 1) * (n - 1)); d2 = 4 + (p * q + 2) / (b - 1); c = p * q * (d2 - 2) / (d2 * (dfe - p - 1)); d1 = p * q; F = T / c; else d1 = s * (2 * m + s + 1); d2 = 2 * (s * n + 1); F = d2 * T / (s * d1); endif vals(3,:) = [T, F, r2, d1, d2, 0]; th = max ([lam; 0]); rr = max (p, q); d2 = dfe - rr + q; vals(4,:) = [th, th * d2 / rr, th / (1 + th), rr, d2, 0]; vals(:,6) = fcdf (vals(:,2), vals(:,4), vals(:,5), 'upper'); endfunction ## Response names from the left side of the formula: ranges of the table's ## variables in the table's order, a comma list, or both. function [resp, errmsg] = responseNames (lhs, vnames) resp = {}; errmsg = ''; for part = strtrim (strsplit (lhs, ',')) ends = strtrim (strsplit (part{1}, '-')); idx = cellfun (@(n) find (strcmp (n, vnames), 1), ends, ... 'UniformOutput', false); miss = cellfun (@isempty, idx); if (any (miss) || numel (ends) > 2) bad = ends(miss); if (isempty (bad)) bad = part; endif errmsg = sprintf (strcat ("the model formula names '%s', which is", ... " not a variable of T."), bad{1}); return; endif if (numel (ends) == 2) if (idx{1} > idx{2}) errmsg = sprintf ("the response range '%s' runs backwards.", part{1}); return; endif resp = [resp, vnames(idx{1}:idx{2})]; else resp = [resp, vnames(idx{1})]; endif endfor if (numel (unique (resp)) < numel (resp)) errmsg = "a response is named more than once."; endif endfunction ## Between-subject terms of the right side, each a cell of variable names, ## and whether the model carries an intercept. function [terms, intercept, errmsg] = betweenTerms (rhs, vnames, resp) terms = {}; errmsg = ''; s = parseWilkinsonFormula (['~', rhs]); model = s.model; empty = cellfun (@isempty, model); intercept = any (empty) || isempty (model); terms = cellfun (@cellstr, model(! empty), 'UniformOutput', false); for i = 1:numel (terms) for v = terms{i} if (! any (strcmp (v{1}, vnames))) errmsg = sprintf (strcat ("the model formula names '%s', which is", ... " not a variable of T."), v{1}); return; elseif (any (strcmp (v{1}, resp))) errmsg = sprintf ("'%s' is a response and a predictor.", v{1}); return; endif endfor endfor endfunction ## The between-subject model as MATLAB renders it, '1 + g + x'. function txt = modelText (rhs, intercept) txt = regexprep (strtrim (rhs), '\s*([+-])\s*', ' $1 '); if (! strcmp (txt, '1') && intercept && isempty (regexp (txt, '^1\b'))) txt = ['1 + ', txt]; endif endfunction ## Effects-coded design matrix of the terms, the coefficient names, and for ## each term (the intercept first) its name and its columns. BAD marks the ## subjects missing a predictor or holding a level CODING does not know. ## CODING, one field per variable, is built from T unless given; for a ## categorical variable it holds the level keys and their native values, and ## for a continuous one its mean, set by the caller. function [X, cnames, tcols, tnames, bad, coding] = effectsDesign (t, terms, intercept, vars, coding) n = rows (t); bad = false (n, 1); given = (nargin > 4); if (! given) coding = struct (); endif code = struct (); for i = 1:numel (vars) v = t.(vars{i}); if (given) cd = coding.(vars{i}); if (cd.iscat) [~, ic, ~, ~, miss] = levelsOf (v, cd.levels); else [~, ~, ~, ~, miss] = levelsOf (v); endif else [lev, ic, native, iscat, miss] = levelsOf (v); cd = struct ('iscat', iscat, 'levels', {lev}, 'native', {native}, ... 'mean', NaN); coding.(vars{i}) = cd; endif if (cd.iscat) miss |= (ic(:) == 0); endif bad |= miss(:); if (cd.iscat) L = numel (cd.levels); M = zeros (n, L - 1); for l = 1:L-1 M(:,l) = (ic(:) == l) - (ic(:) == L); endfor code.(vars{i}) = struct ('M', M, 'names', ... {strcat(vars{i}, '_', cd.levels(1:L-1)(:)')}); else code.(vars{i}) = struct ('M', double (v(:)), 'names', {vars(i)}); endif endfor X = zeros (n, 0); cnames = {}; tcols = {}; tnames = {}; if (intercept) X = ones (n, 1); cnames = {'(Intercept)'}; tcols = {1}; tnames = {'(Intercept)'}; endif for i = 1:numel (terms) M = ones (n, 1); nm = {''}; for v = terms{i} c = code.(v{1}); Mn = zeros (n, 0); nn = {}; for a = 1:columns (M) for b = 1:columns (c.M) Mn(:,end+1) = M(:,a) .* c.M(:,b); if (isempty (nm{a})) nn{end+1} = c.names{b}; else nn{end+1} = [nm{a}, ':', c.names{b}]; endif endfor endfor M = Mn; nm = nn; endfor tcols{end+1} = columns (X) + (1:columns (M)); tnames{end+1} = strjoin (terms{i}, ':'); X = [X, M]; cnames = [cnames, nm]; endfor endfunction ## Levels of the variable V: their keys as text, the code of each element ## (0 where it is missing or not among LEV when LEV is given), one native ## value per level, whether V is categorical, and which elements are ## missing. A numeric V is continuous and has no levels. function [lev, ic, native, iscat, miss] = levelsOf (v, lev) native = {}; ic = []; if (iscategorical (v)) miss = isundefined (v(:)); keys = cellstr (v(:)); iscat = true; elseif (iscellstr (v) || isa (v, 'string') || ischar (v) || islogical (v)) if (islogical (v)) keys = cellstr (num2str (double (v(:)))); else keys = cellstr (v); keys = keys(:); endif miss = cellfun (@isempty, keys); iscat = true; else v = double (v(:)); miss = isnan (v); iscat = false; if (nargin < 2) lev = {}; endif return; endif if (nargin < 2) if (iscategorical (v)) lev = categories (v); lev = lev(ismember (lev, keys(! miss))); else lev = unique (keys(! miss)); endif endif [~, ic] = ismember (keys, lev); ic(miss) = 0; native = cell (numel (lev), 1); for l = 1:numel (lev) j = find (ic == l, 1); if (isempty (j)) native{l} = lev{l}; elseif (iscategorical (v) || islogical (v)) native{l} = v(j); else native{l} = keys(j); endif endfor endfunction ## The within-subject terms of the model WM over the design WD: their ## contrast matrices, W.C, and names, W.names, empty for the constant term, ## the names of the columns of each, W.cols, and the kind of model, W.kind. function [W, errmsg] = withinTerms (WM, WD, wnames, k) W = struct ('C', {{}}, 'names', {{}}, 'cols', {{}}, 'kind', ''); errmsg = ''; if (numel (wnames) == 1) label = wnames{1}; else label = 'Time'; endif if (isa (WM, 'string') && isscalar (WM)) WM = char (WM); endif if (isnumeric (WM)) if (! (isreal (WM) && ismatrix (WM) && rows (WM) == k && ! isempty (WM))) errmsg = sprintf ("'WithinModel' must be a matrix with %d rows.", k); return; endif W.C = {double(WM)}; W.names = {label}; W.kind = 'matrix'; elseif (! (ischar (WM) && isrow (WM))) errmsg = "invalid 'WithinModel'."; elseif (strcmpi (WM, 'separatemeans')) W.C = {successive(k)}; W.names = {label}; W.kind = 'separatemeans'; elseif (strcmpi (WM, 'orthogonalcontrasts')) if (numel (wnames) != 1 || ! isnumeric (WD.(wnames{1}))) errmsg = strcat ("the 'orthogonalcontrasts' model requires a", ... " within-subject design with a single numeric factor."); return; endif v = double (WD.(wnames{1})); v = (v(:) - mean (v)) / max (1, std (v)); [Qp, ~] = qr (v .^ (0:k-1), 0); W.C = [{ones(k, 1)}, num2cell(Qp(:,2:k), 1)]; W.names = [{''}, {wnames{1}}, ... arrayfun(@(d) sprintf ('%s^%d', wnames{1}, d), 2:k-1, ... 'UniformOutput', false)]; W.cols = W.names; W.cols{1} = 'Constant'; W.kind = 'orthogonalcontrasts'; else try s = parseWilkinsonFormula (['~', WM]); catch errmsg = "invalid 'WithinModel'."; return; end_try_catch model = s.model; empty = cellfun (@isempty, model); terms = cellfun (@cellstr, model(! empty), 'UniformOutput', false); if (any (! ismember ([terms{:}], wnames))) errmsg = "invalid 'WithinModel'."; return; endif [Xw, cn, tc, tn] = effectsDesign (WD, terms, true, unique ([terms{:}], ... 'stable')); if (any (empty) || isempty (model)) W.C = {ones(k, 1)}; W.names = {''}; W.cols = {{'(Intercept)'}}; endif for i = 1:numel (terms) W.C{end+1} = Xw(:,tc{i+1}); W.names{end+1} = tn{i+1}; W.cols{end+1} = cn(tc{i+1}); endfor W.kind = 'formula'; endif endfunction ## A cell row as MATLAB displays one, {'a' 'b'}. function s = cellrow (c) if (isempty (c)) s = sprintf ('{%dx%d cell}', size (c)); else s = ['{', strjoin(strcat ("'", c, "'"), ' '), '}']; endif endfunction ## Expected values are MATLAB R2024a's unless stated otherwise. %!shared rm, t2, W %! load fisheriris %! t = table (species, meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'species', 'meas1', 'meas2', 'meas3', 'meas4'}); %! Meas = table ([1, 2, 3, 4]', 'VariableNames', {'Measurements'}); %! rm = fitrm (t, 'meas1-meas4 ~ species', 'WithinDesign', Meas); %! g = categorical ([1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2]'); %! x = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8]'; %! [j, i] = meshgrid (1:6, 1:12); %! Y = mod (7 * i .* j + 3 * i + j .^ 2, 13) + 2 * j + 3 * (double (g) == 2) ... %! + 0.5 * x; %! t2 = array2table (Y, 'VariableNames', {'y1', 'y2', 'y3', 'y4', 'y5', 'y6'}); %! t2.g = g; %! t2.x = x; %! W = table (categorical ([1, 1, 1, 2, 2, 2]'), ... %! categorical ([1, 2, 3, 1, 2, 3]'), 'VariableNames', {'A', 'B'}); ## Properties %!assert_equal (rm.ResponseNames, {'meas1', 'meas2', 'meas3', 'meas4'}) %!assert_equal (rm.BetweenFactorNames, {'species'}) %!assert_equal (rm.BetweenModel, '1 + species') %!assert_equal (rm.WithinFactorNames, {'Measurements'}) %!assert_equal (rm.WithinModel, 'separatemeans') %!assert_equal (rm.DFE, 147) %!assert_equal (size (rm.BetweenDesign), [150, 5]) %!assert_equal (rm.WithinDesign.Properties.RowNames', rm.ResponseNames) %!assert_equal (rm.Coefficients.Properties.RowNames', ... %! {'(Intercept)', 'species_setosa', 'species_versicolor'}) %!assert_equal (table2array (rm.Coefficients), ... %! [5.84333333333334, 3.05733333333334, 3.758, 1.19933333333333; ... %! -0.837333333333333, 0.370666666666667, -2.296, ... %! -0.953333333333333; 0.0926666666666665, ... %! -0.287333333333334, 0.502, 0.126666666666667], -1e-12) %!assert_equal (table2array (rm.Covariance)(:,1), ... %! [0.265008163265306; 0.0927210884353741; ... %! 0.167514285714286; 0.0384013605442177], -1e-12) ## ranova under the default within model %!test %! tbl = ranova (rm); %! assert_equal (tbl.Properties.RowNames', {'(Intercept):Measurements', ... %! 'species:Measurements', 'Error(Measurements)'}); %!test %! tbl = ranova (rm); %! assert_equal (tbl.SumSq, [1656.26325; 282.4665; 35.42275], -1e-12); %! assert_equal (tbl.DF, [3; 6; 441]); %!test %! tbl = ranova (rm); %! assert_equal (tbl.F(1:2), [6873.28617202222; 586.100394520471], -1e-12); %! assert_equal (tbl.pValue(2), 1.42713829312908e-206, -1e-10); %!test %! tbl = ranova (rm); %! assert_equal (tbl.pValueGG(1:2), [9.44912100730417e-279; ... %! 4.93131396537195e-156], -1e-10); %! assert_equal (tbl.pValueHF(1:2), [2.92129935033006e-283; ... %! 1.54056390379818e-158], -1e-10); %! assert_equal (tbl.pValueLB(1:2), [2.58714529113126e-125; ... %! 9.01514627639756e-71], -1e-10); %!test %! [~, A, C, D] = ranova (rm); %! assert_equal (A, {[1, 0, 0]; [0, 1, 0; 0, 0, 1]}); %! assert_equal (C, [1, 0, 0; -1, 1, 0; 0, -1, 1; 0, 0, -1]); %! assert_equal (D, 0); %!test %! tbl = ranova (rm, 'WithinModel', [1, -1, 0, 0; 0, 1, -1, 0]'); %! assert_equal (tbl.SumSq, [630.067244444444; 282.402488888889; ... %! 24.5102666666667], -1e-12); %! assert_equal (tbl.pValueGG(1:2), [2.73416691642378e-192; ... %! 3.34270901331439e-146], -1e-10); ## orthogonalcontrasts, which R2024a and R2026a fail on inside ranova; the ## expected values are R2026a's anova for the same contrasts %!test %! tbl = ranova (rm, 'WithinModel', 'orthogonalcontrasts'); %! assert_equal (tbl.SumSq, [7201.65615000052; 309.6067; 53.87465; ... %! 1313.01136333336; 35.9780866666667; 13.75805; ... %! 1.93801666666664; 0.469633333333338; 4.53985; ... %! 341.313870000001; 246.01878; 17.12485], -1e-11); %!test %! tbl = ranova (rm, 'WithinModel', 'orthogonalcontrasts'); %! assert_equal (tbl.F([4, 5, 7, 8, 10, 11]), [14029.0717369107; ... %! 192.206698623715; 62.7528332433881; 7.60334592552624; ... %! 2929.84399221016; 1055.91466961754], -1e-11); %!test %! tbl = ranova (rm, 'WithinModel', 'orthogonalcontrasts'); %! assert_equal (tbl.Properties.RowNames([4, 7, 10])', ... %! {'(Intercept):Measurements', '(Intercept):Measurements^2', ... %! '(Intercept):Measurements^3'}); ## mauchly and epsilon %!test %! tbl = mauchly (rm); %! assert_equal (table2array (tbl), [0.558144130896497, 84.9761726331633, ... %! 5, 7.61488382466235e-17], -1e-12); %!test %! tbl = epsilon (rm); %! assert_equal (table2array (tbl), [1, 0.751790042430452, ... %! 0.764092317002038, 1/3], -1e-12); %!test %! C = [1, -1, 0, 0; 0, 1, -1, 0; 0, 0, 1, -1]'; %! assert_equal (table2array (epsilon (rm, C)), ... %! [1, 0.751790042430453, 0.764092317002038, 1/3], -1e-12); %!test %! C = [1, -1, 0, 0; 0, 1, -1, 0; 0, 0, 1, -1]'; %! assert_equal (mauchly (rm, C).W, 0.558144130896497, -1e-12); ## A continuous covariate beside a between factor, and a factorial within ## design %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W, 'WithinModel', 'A*B'); %! assert_equal (rm2.BetweenModel, '1 + g + x'); %! assert_equal (rm2.BetweenFactorNames, {'g', 'x'}); %! assert_equal (rm2.DFE, 9); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W); %! assert_equal (table2array (rm2.Coefficients)(:,1), ... %! [9.54310344827587; -0.0402298850574699; 0.586206896551724], ... %! -1e-12); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W, 'WithinModel', 'A*B'); %! tbl = ranova (rm2); %! assert_equal (tbl.Properties.RowNames', {'(Intercept):Time', 'g:Time', ... %! 'x:Time', 'Error(Time)'}); %! assert_equal (tbl.SumSq, [255.837635271832; 105.120215891605; ... %! 106.01539408867; 651.790161466886], -1e-12); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W); %! tbl = ranova (rm2); %! assert_equal (tbl.pValueGG(1:3), [0.028891532860066; 0.25048764667133; ... %! 0.247169589665584], -1e-12); %! assert_equal (tbl.pValueHF(1:3), [0.0115155408306671; 0.23038031489716; ... %! 0.226411224877626], -1e-12); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W); %! tbl = ranova (rm2, 'WithinModel', 'A*B'); %! assert_equal (tbl.SumSq, [3499.10306228529; 40.8161242267455; ... %! 97.0498768472906; 63.3112342638205; 173.443725936885; ... %! 6.54103670312192; 3.16262999452654; 229.976258894362; ... %! 60.1151512858879; 32.4673167683903; 78.5480295566504; ... %! 145.674192665572; 22.2787580490593; 66.1118624200928; ... %! 24.3047345374931; 276.139709906951], -1e-11); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W); %! tbl = ranova (rm2, 'WithinModel', 'A*B'); %! assert_equal (tbl.pValueGG(9:11), [0.0492037168776316; ... %! 0.167760677557413; 0.0238347461584444], -1e-11); %! assert_equal (tbl.pValueHF(13:15), [0.488916988695095; ... %! 0.149284419253376; 0.460679398895663], -1e-11); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W); %! assert_equal (table2array (mauchly (rm2)), [0.109478004315118, ... %! 15.7054245285396, 14, 0.331692719814282], -1e-11); %! assert_equal (table2array (epsilon (rm2)), [1, 0.589453986170189, ... %! 0.907778770724919, 0.2], -1e-12); %!test %! rm9 = fitrm (t2, 'y1-y6 ~ g', 'WithinDesign', W, 'WithinModel', 'A+B'); %! tbl = ranova (rm9, 'WithinModel', rm9.WithinModel); %! assert_equal (tbl.SumSq, [19900.125; 74.0138888888887; 160.361111111111; ... %! 583.680555555555; 5.01388888888888; 233.138888888889; ... %! 137.25; 58.5277777777778; 224.222222222222], -1e-12); %! assert_equal (tbl.pValueGG(7:8), [0.0102255822891222; ... %! 0.103329724488498], -1e-11); ## A numeric within design, Huynh-Feldt capped at 1 %!test %! rm3 = fitrm (t2, 'y1-y6 ~ g', 'WithinDesign', [1, 2, 4, 8, 16, 32]'); %! assert_equal (rm3.WithinDesign.Time, [1; 2; 4; 8; 16; 32]); %! assert_equal (rm3.WithinFactorNames, {'Time'}); %!test %! rm3 = fitrm (t2, 'y1-y6 ~ g', 'WithinDesign', [1, 2, 4, 8, 16, 32]'); %! tbl = ranova (rm3); %! assert_equal (tbl.SumSq, [722.125; 124.569444444444; 757.805555555556], ... %! -1e-12); %! assert_equal (tbl.pValueGG(1:2), [8.15757856520654e-05; ... %! 0.195494399896943], -1e-11); %! assert_equal (tbl.pValueHF(1:2), tbl.pValue(1:2)); %!test %! rm3 = fitrm (t2, 'y1-y6 ~ g', 'WithinDesign', [1, 2, 4, 8, 16, 32]'); %! assert_equal (table2array (epsilon (rm3)), [1, 0.650731538906031, 1, ... %! 0.2], -1e-12); %! assert_equal (table2array (mauchly (rm3)), [0.172269738544646, ... %! 14.2454196441778, 14, 0.431591774636887], -1e-11); ## An intercept alone %!test %! rm4 = fitrm (t2, 'y1-y6 ~ 1'); %! assert_equal (rm4.BetweenFactorNames, cell (1, 0)); %! assert_equal (rm4.BetweenModel, '1'); %!test %! rm4 = fitrm (t2, 'y1-y6 ~ 1'); %! tbl = ranova (rm4); %! assert_equal (tbl.SumSq, [722.125; 882.375], -1e-12); %! assert_equal (tbl.pValueGG(1), 6.37221717220761e-05, -1e-11); %! assert_equal (epsilon (rm4).GreenhouseGeisser, 0.693384390469508, -1e-12); ## An interaction between subjects, and a comma list of responses %!test %! rm7 = fitrm (t2, 'y1-y6 ~ g*x', 'WithinDesign', W); %! assert_equal (rm7.BetweenModel, '1 + g*x'); %! assert_equal (rm7.Coefficients.Properties.RowNames', ... %! {'(Intercept)', 'g_1', 'x', 'g_1:x'}); %! assert_equal (table2array (rm7.Coefficients)(:,1), [10.8028790057797; ... %! -3.00733997232247; 0.37163867256397; 0.659959840447182], ... %! -1e-12); %!test %! rm8 = fitrm (t2, 'y1,y2,y3 ~ g'); %! assert_equal (rm8.ResponseNames, {'y1', 'y2', 'y3'}); %! assert_equal (rm8.WithinDesign.Time, [1; 2; 3]); ## A subject missing a response is left out of the fit and kept in the design %!test %! t3 = t2; %! t3.y2(3) = NaN; %! rm5 = fitrm (t3, 'y1-y6 ~ g + x', 'WithinDesign', W); %! assert_equal (rm5.DFE, 8); %! assert_equal (rows (rm5.BetweenDesign), 12); %! assert_equal (table2array (rm5.Coefficients)(:,1), [9.79152291769344; ... %! 0.248891079349433; 0.594627895515033], -1e-12); %! assert_equal (ranova (rm5).SumSq, [242.715922479748; 90.7245227703455; ... %! 106.321888176989; 631.122556267455], -1e-12); %!test %! t4 = t2; %! t4.y2(12) = NaN; %! t4.y3(12) = NaN; %! assert_equal (fitrm (t4, 'y1-y6 ~ g', 'WithinDesign', W).DFE, 9); ## A singular covariance fails Mauchly's test outright %!test %! t5 = t2; %! t5.y2 = t5.y1 + 1; %! tbl = mauchly (fitrm (t5, 'y1-y6 ~ g')); %! assert_equal (table2array (tbl), [0, Inf, 14, 0]); ## anova %!test %! tbl = anova (rm); %! assert_equal (cellstr (tbl.Between)', {'constant', 'species', 'Error'}); %! assert_equal (cellstr (tbl.Within)', {'Constant', 'Constant', 'Constant'}); %!test %! tbl = anova (rm); %! assert_equal (tbl.SumSq, [7201.65615000065; 309.606699999999; ... %! 53.87465], -1e-12); %! assert_equal (tbl.DF, [1; 2; 147]); %! assert_equal (tbl.F(1:2), [19650.1221641365; 422.389610883782], -1e-12); %!test %! assert_equal (anova (rm, 'WithinModel', 'separatemeans').SumSq, ... %! anova (rm).SumSq); %!test %! tbl = anova (rm, 'WithinModel', [1, -1, 0, 0; 0, 1, -1, 0]'); %! assert_equal (cellstr (tbl.Within([1, 4]))', {'Contrast1', 'Contrast2'}); %! assert_equal (tbl.SumSq, [1164.2694; 114.4624; 28.6582; ... %! 73.6400666666671; 562.926933333334; 27.943], -1e-12); %! assert_equal (tbl.F([4, 5]), [387.39898364528; 1480.69747700676], -1e-12); %!test %! tbl = anova (rm, 'WithinModel', [1, 1, 1, 1]'); %! assert_equal (tbl.SumSq(1), 28806.6246000026, -1e-12); ## R2026a's values, as for ranova %!test %! tbl = anova (rm, 'WithinModel', 'orthogonalcontrasts'); %! assert_equal (cellstr (tbl.Within([1, 4, 7, 10]))', {'Constant', ... %! 'Measurements', 'Measurements^2', 'Measurements^3'}); %! assert_equal (tbl.SumSq([4, 7, 10]), [1313.01136333336; ... %! 1.93801666666664; 341.313870000001], -1e-11); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W); %! tbl = anova (rm2, 'WithinModel', 'A*B'); %! assert_equal (cellstr (tbl.Within(1:4:end))', {'(Intercept)', 'A_1', ... %! 'B_1', 'B_2', 'A_1:B_1', 'A_1:B_2'}); %! assert_equal (tbl.SumSq(9:12), [10.539317448175; 0.186084518387707; ... %! 7.3161945812808; 73.7254720853859], -1e-12); %! assert_equal (tbl.pValue(22:23), [0.153846787233791; ... %! 0.29518761419548], -1e-11); ## manova %!test %! tbl = manova (rm); %! assert_equal (cellstr (tbl.Statistic(1:4))', {'Pillai', 'Wilks', ... %! 'Hotelling', 'Roy'}); %! assert_equal (cellstr (tbl.Between([1, 5]))', {'(Intercept)', 'species'}); %! assert_equal (cellstr (tbl.Within(1)), {'Constant'}); %!test %! tbl = manova (rm); %! assert_equal (tbl.Value(5:8), [0.969092455350808; 0.0411531658067895; ... %! 23.0505039998432; 23.0396981666769], -1e-12); %! assert_equal (tbl.RSquare(5:8), [0.484546227675404; 0.797137569257416; ... %! 0.920161286974006; 0.958402139949237], -1e-12); %!test %! tbl = manova (rm); %! assert_equal (tbl.F([5, 6, 8]), [45.7485249172255; 189.92336681764; ... %! 1121.26531077827], -1e-11); %! assert_equal ([tbl.df1([5, 6, 8]), tbl.df2([5, 6, 8])], ... %! [6, 292; 6, 290; 3, 146]); %! assert_equal (tbl.pValue([5, 6, 8]), [2.47288601081631e-39; ... %! 2.39583203496623e-97; 1.47713689662351e-100], -1e-10); ## The Hotelling-Lawley F is R2024a's; its second degrees of freedom are ## McKeon's, and the p-value follows from them %!test %! tbl = manova (rm); %! assert_equal (tbl.F(7), 555.166436056617, -1e-11); %! n = (147 - 3 - 1) / 2; %! b = (3 + 2 * n) * (2 + 2 * n) / (2 * (2 * n + 1) * (n - 1)); %! d2 = 4 + 8 / (b - 1); %! assert_equal ([tbl.df1(7), tbl.df2(7)], [6, d2], -1e-14); %! assert_equal (tbl.pValue(7), fcdf (tbl.F(7), 6, d2, 'upper'), -1e-12); %!test %! tbl = manova (rm, 'By', 'species'); %! assert_equal (cellstr (tbl.Between(1:4:end))', {'species=setosa', ... %! 'species=versicolor', 'species=virginica'}); %! assert_equal (tbl.Value(1:4:end), [0.982302126154032; ... %! 0.97000070863222; 0.972606348435736], -1e-12); %! assert_equal (tbl.F(1), 2682.69152049929, -1e-11); %!test %! tbl = manova (rm, 'WithinModel', [1, -1, 0, 0; 0, 1, -1, 0]'); %! assert_equal (cellstr (tbl.Within(1)), {'Specified contrast'}); %! assert_equal (tbl.Value(5), 0.960761007306465, -1e-12); %! assert_equal (tbl.F(1), 4126.8008429193, -1e-11); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W, 'WithinModel', 'A*B'); %! [tbl, A, C, D] = manova (rm2); %! assert_equal (rows (tbl), 48); %! assert_equal (unique (cellstr (tbl.Within), 'stable')', ... %! {'(Intercept)', 'A', 'B', 'A:B'}); %! assert_equal (A, {[1, 0, 0]; [0, 1, 0]; [0, 0, 1]}); %! assert_equal (cellfun (@columns, C), [1, 1, 2, 2]); %! assert_equal (D, 0); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W, 'WithinModel', 'A*B'); %! tbl = manova (rm2); %! assert_equal (tbl.Value(41:44), [0.486347963843071; 0.513652036156928; ... %! 0.946843251088535; 0.946843251088535], -1e-12); %! assert_equal (tbl.pValue(41), 0.069610708832983, -1e-11); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W, 'WithinModel', 'A*B'); %! tbl = manova (rm2, 'WithinModel', 'separatemeans'); %! assert_equal (tbl.Value(1), 0.786551501991551, -1e-12); %! assert_equal ([tbl.F(3), tbl.df1(3), tbl.df2(3)], ... %! [3.68497089148136, 5, 5], -1e-12); %! assert_equal (tbl.pValue(3), 0.0893171383745427, -1e-11); ## coeftest %!test %! tbl = coeftest (rm, [0, 1, 0; 0, 0, 1], [1, -1, 0, 0; 0, 1, -1, 0]'); %! assert_equal (tbl.Value, [0.960761007306465; 0.0420065576612523; ... %! 22.739922777632; 22.7370251198758], -1e-12); %! assert_equal (tbl.F([1, 2, 4]), [67.9496579068885; 283.175722000404; ... %! 1671.17134631087], -1e-11); %! assert_equal (tbl.F(3), 828.147118575719, -1e-11); %!test %! tbl = coeftest (rm, [0, 1, 0; 0, 0, 1], [1, -1, 0, 0; 0, 1, -1, 0]', ... %! [0.5, 0.1; -0.2, 0.3]); %! assert_equal (tbl.Value, [1.07175176658522; 0.0463873977279332; ... %! 18.0107848010586; 17.8682529938836], -1e-12); %! assert_equal (tbl.pValue(4), 1.71285549134517e-94, -1e-10); %!test %! tbl = coeftest (rm, [1, 0, 0], [1, -1, 0, 0]', 1); %! assert_equal (tbl.F, 2454.27144063479 * ones (4, 1), -1e-12); %! assert_equal ([tbl.df1, tbl.df2], repmat ([1, 147], 4, 1)); %!test %! tbl = coeftest (rm, [0, 1, 0], [1, -1, 0, 0]'); %! assert_equal (tbl.Value(1), 0.792486767123089, -1e-12); %! assert_equal (tbl.F(1), 561.38855894648, -1e-12); ## margmean %!test %! tbl = margmean (rm, 'species'); %! assert_equal (tbl.species, {'setosa'; 'versicolor'; 'virginica'}); %! assert_equal (tbl.Mean, [2.5355; 3.573; 4.285], -1e-13); %! assert_equal (tbl.StdErr, 0.0428073718935813 * ones (3, 1), -1e-12); %! assert_equal (tbl.Lower(1), 2.45090264579764, -1e-12); %!test %! tbl = margmean (rm, 'Measurements'); %! assert_equal (tbl.Measurements, [1; 2; 3; 4]); %! assert_equal (tbl.StdErr, [0.0420323814271256; 0.0277353871557668; ... %! 0.0351366622491893; 0.0167096045540803], -1e-12); %!test %! tbl = margmean (rm, {'species', 'Measurements'}); %! assert_equal (rows (tbl), 12); %! assert_equal (tbl.Mean(1:4), [5.006; 3.428; 1.462; 0.246], -1e-12); %! assert_equal (tbl.StdErr(5), 0.072802220194896, -1e-12); %!test %! tbl = margmean (rm, 'species', 'Alpha', 0.01); %! assert_equal (tbl.Lower(1), 2.42378611949266, -1e-12); ## A continuous predictor enters at its mean %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W); %! tbl = margmean (rm2, 'g'); %! assert_equal (tbl.Mean, [15.8555692391899; 17.3944307608101], -1e-12); %! assert_equal (tbl.StdErr, 0.446919101103406 * ones (2, 1), -1e-12); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W); %! tbl = margmean (rm2, {'g', 'A'}); %! assert_equal (tbl.Mean, [13.3163656267105; 18.3947728516694; ... %! 14.2391899288451; 20.549671592775], -1e-12); %! assert_equal (tbl.StdErr(1:2), [1.12245927784633; 0.768527281387124], ... %! -1e-12); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W); %! tbl = margmean (rm2, {'B', 'g'}); %! assert_equal (tbl.Mean([1, 4, 6]), [13.8103448275862; 16.301724137931; ... %! 19.6919129720854], -1e-12); %! assert_equal (tbl.StdErr([1, 3, 5]), [0.863939313862951; ... %! 0.870431298727607; 0.688406615362842], -1e-12); ## grpstats %!test %! tbl = grpstats (rm, 'species'); %! assert_equal (tbl.GroupCount, [200; 200; 200]); %! assert_equal (tbl.mean, [2.5355; 3.573; 4.285], -1e-13); %! assert_equal (tbl.std, [1.84834293572383; 1.76238503313695; ... %! 1.91538993235446], -1e-12); %!test %! tbl = grpstats (rm, 'Measurements'); %! assert_equal (tbl.GroupCount, 150 * ones (4, 1)); %! assert_equal (tbl.std, [0.828066127977863; 0.435866284936698; ... %! 1.76529823325947; 0.762237668960347], -1e-12); %!test %! tbl = grpstats (rm, 'species', {'min', 'max'}); %! assert_equal (tbl.Properties.VariableNames, {'species', 'GroupCount', ... %! 'min', 'max'}); %! assert_equal ([tbl.min, tbl.max], [0.1, 5.8; 1, 7; 1.4, 7.9]); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W); %! tbl = grpstats (rm2, {'g', 'B'}); %! assert_equal (tbl.GroupCount, 12 * ones (6, 1)); %! assert_equal (tbl.std([1, 5]), [4.56767297030297; 3.29944898981082], ... %! -1e-12); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W); %! tbl = grpstats (rm2, 'g', {'mean', 'sem', 'var', 'range', 'numel', ... %! 'median'}); %! assert_equal ([tbl.sem, tbl.var], [0.797344578852233, ... %! 22.8873015873016; 0.874599933736397, 27.5373015873016], ... %! -1e-12); %! assert_equal ([tbl.range, tbl.numel, tbl.median], [19, 36, 16.25; ... %! 22, 36, 17.5]); ## multcompare %!test %! tbl = multcompare (rm, 'species', 'ComparisonType', 'bonferroni'); %! assert_equal (tbl.Difference(1:2), [-1.0375; -1.7495], -1e-12); %! assert_equal (tbl.StdErr(1), 0.0605387659014515, -1e-12); %! assert_equal (tbl.pValue(1:2), [2.15970302648855e-36; ... %! 5.03902053290415e-62], -1e-9); %! assert_equal (tbl.Lower(1), -1.18410589638625, -1e-12); %!test %! tbl = multcompare (rm, 'species', 'ComparisonType', 'lsd'); %! assert_equal (tbl.pValue(1), 7.19901008829516e-37, -1e-9); %! assert_equal (tbl.Lower(1), -1.15713872565386, -1e-12); %!test %! tbl = multcompare (rm, 'species', 'ComparisonType', 'scheffe'); %! assert_equal (tbl.pValue(1), 8.97549175066402e-36, -1e-9); %! assert_equal (tbl.Lower(1), -1.18720639857468, -1e-12); ## R2024a floors these Tukey-Kramer p-values, and takes its limits from the ## studentized range on infinite degrees of freedom; the quantile here is ## R 4.5.0's qtukey (0.95, 3, 147) %!test %! tbl = multcompare (rm, 'species'); %! assert_equal (tbl.Lower(1), -1.0375 - 3.34842406186643 / sqrt (2) ... %! * 0.0605387659014515, -1e-10); %! assert_equal (all (tbl.pValue < 1e-20), true); %!test %! tbl = multcompare (rm, 'Measurements', 'By', 'species'); %! assert_equal (rows (tbl), 36); %! assert_equal (tbl.Properties.VariableNames(1:3), {'species', ... %! 'Measurements_1', 'Measurements_2'}); %! assert_equal (tbl.StdErr(1:3), [0.0624425722558894; 0.047993196796791; ... %! 0.0678361370996215], -1e-12); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W); %! tbl = multcompare (rm2, 'B'); %! assert_equal (tbl.StdErr([1, 2, 4]), [0.914225240599476; ... %! 0.826222282469959; 0.710493930524169], -1e-12); %! ## R2024a's studentized range is good to about 2e-7 here %! assert_equal (tbl.pValue([1, 2, 4]), [0.278857228960382; ... %! 0.00693824145665634; 0.0634575438808659], -1e-6); %! assert_equal (tbl.Lower(1:2), [-4.05252199083511; -5.68181723897677], ... %! -1e-7); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W); %! tbl = multcompare (rm2, 'B', 'By', 'g'); %! assert_equal (tbl.Difference([1, 8]), [-2.88793103448276; ... %! -3.50225779967159], -1e-12); %! assert_equal (tbl.pValue([1, 10]), [0.122845632074643; ... %! 0.0214216738123368], -1e-7); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W); %! tbl = multcompare (rm2, 'g', 'By', 'A'); %! assert_equal (tbl.StdErr([1, 3]), [1.60451755027924; 1.09858373946539], ... %! -1e-12); %! assert_equal (tbl.pValue([1, 3]), [0.579288143050048; ... %! 0.0814446080647632], -1e-7); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W); %! tbl = multcompare (rm2, 'B', 'ComparisonType', 'dunn-sidak'); %! assert_equal (tbl.pValue([1, 2, 4]), [0.353395443395346; ... %! 0.00819161456139628; 0.0787120565287793], -1e-10); %! assert_equal (tbl.Lower(1), -4.17216326867877, -1e-10); ## predict and random %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W); %! assert_equal (predict (rm2, t2(1:2,:))(1,:), [11.2614942528736, ... %! 13.2783251231527, 13.1005747126437, 14.7040229885058, ... %! 20.3940886699507, 18.4835796387521], -1e-12); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W); %! [~, yci] = predict (rm2, t2(1:2,:)); %! assert_equal (size (yci), [2, 6, 2]); %! assert_equal ([yci(1,1,1), yci(1,1,2)], [7.64052800884495, ... %! 14.8824604969022], -1e-12); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W); %! t4 = t2(1:2,:); %! t4.x(1) = NaN; %! yp = predict (rm2, t4); %! assert_equal (isnan (yp(:,1)), [true; false]); %!test %! rm2 = fitrm (t2, 'y1-y6 ~ g + x', 'WithinDesign', W); %! assert_equal (size (predict (rm2)), [12, 6]); %!test %! rm3 = fitrm (t2, 'y1-y6 ~ g', 'WithinDesign', [1, 2, 4, 8, 16, 32]', ... %! 'WithinModel', 'orthogonalcontrasts'); %! assert_equal (predict (rm3, t2(1,:), 'WithinDesign', [3, 5]'), ... %! [13.8420137222029, 14.4827614967302], -1e-11); %!test %! rm9 = fitrm (t2, 'y1-y6 ~ g', 'WithinDesign', W, 'WithinModel', 'A+B'); %! assert_equal (predict (rm9, t2(1,:)), [10.9166666666667, ... %! 14.1666666666667, 14, 16.0833333333333, 19.3333333333333, ... %! 19.1666666666667], -1e-12); %!warning ... %! predict (rm, rm.BetweenDesign(1,:), 'WithinDesign', [1.5, 2.5]'); %!test %! assert_equal (size (random (rm)), [150, 4]); %! assert_equal (size (random (rm, rm.BetweenDesign([1, 51, 101],:))), [3, 4]); ## plot and plotprofile %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plot (rm); %! assert_equal (numel (h), 150); %! assert_equal (get (h(1), 'xdata'), [1, 2, 3, 4]); %! assert_equal (get (h(1), 'ydata'), [5.1, 3.5, 1.4, 0.2]); %! assert_equal (get (h(1), 'marker'), 's'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plot (rm, 'Group', 'species', 'Marker', 'o', 'LineStyle', '--'); %! assert_equal (get (legend (), 'string')(:)', {'species=setosa', ... %! 'species=versicolor', 'species=virginica'}); %! assert_equal (isequal (get (h(1), 'color'), get (h(51), 'color')), false); %! assert_equal (get (h(1), 'linestyle'), '--'); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotprofile (rm, 'species'); %! assert_equal (numel (h), 1); %! assert_equal (get (h, 'ydata'), [2.5355, 3.573, 4.285], -1e-13); %! assert_equal (get (get (gca, 'xlabel'), 'string'), 'species'); %! assert_equal (get (get (gca, 'ylabel'), 'string'), ... %! 'Estimated marginal means'); %! assert_equal (get (gca, 'xticklabel')', {'setosa', 'versicolor', ... %! 'virginica'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test %! hf = figure ('visible', 'off'); %! unwind_protect %! h = plotprofile (rm, 'Measurements', 'Group', 'species'); %! assert_equal (numel (h), 3); %! assert_equal (get (h(1), 'xdata'), [1, 2, 3, 4]); %! assert_equal (get (h(1), 'ydata'), [5.006, 3.428, 1.462, 0.246], -1e-12); %! assert_equal (get (legend (), 'string')(:)', {'species=setosa', ... %! 'species=versicolor', 'species=virginica'}); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error RepeatedMeasuresModel (1) %!error fitrm ([1, 2, 3], 'y1-y6 ~ g') %!error ... %! fitrm (t2, 5) %!error ... %! fitrm (t2, 'y1-y6') %!error ... %! fitrm (t2, 'y1-y6 ~ q') %!error ... %! fitrm (t2, 'y1-y9 ~ g') %!error ... %! fitrm (t2, 'y6-y1 ~ g') %!error ... %! fitrm (t2, 'y1,y1 ~ g') %!error ... %! fitrm (t2, 'y1-y6 ~ y1') %!error ... %! fitrm (t2, 'g,y1 ~ x') %!error ... %! fitrm (t2, 'y1-y6 ~ g', 'Nonsense', 1) %!error ... %! fitrm (t2, 'y1-y6 ~ g', 'WithinDesign', [1, 2, 3]') %!error ... %! fitrm (t2, 'y1-y6 ~ g', 'WithinDesign', {1}) %!error ... %! fitrm (t2, 'y1-y6 ~ g', 'WithinDesign', W, 'WithinModel', 'C*D') %!error ... %! fitrm (t2, 'y1-y6 ~ g', 'WithinDesign', W, 'WithinModel', 'orthogonalcontrasts') %!error ... %! fitrm ([t2, table(2 * t2.x, 'VariableNames', {'x2'})], 'y1-y6 ~ x + x2') %!error ... %! ranova (rm, 'WithinModel', ones (5, 1)) %!error ... %! ranova (rm, 'WithinModel', 'C*D') %!error ... %! ranova (rm, 'Nonsense', 1) %!error ... %! mauchly (rm, ones (5, 1)) %!error ... %! epsilon (rm, ones (5, 1)) %!error ... %! anova (rm, 'WithinModel', 'C*D') %!error ... %! anova (rm, 'Nonsense', 1) %!error ... %! manova (rm, 'WithinModel', 'orthogonalcontrasts') %!error ... %! manova (rm, 'By', 'g') %!error ... %! manova (fitrm (t2, 'y1-y6 ~ x'), 'By', 'x') %!error ... %! manova (rm, 'Nonsense', 1) %!error ... %! coeftest (rm, [0, 1, 0]) %!error ... %! coeftest (rm, [1, 0], [1, -1, 0, 0]') %!error ... %! coeftest (rm, [0, 1, 0], [1, -1, 0]') %!error ... %! coeftest (rm, [0, 1, 0], [1, -1, 0, 0]', [1, 2]) %!error margmean (rm) %!error ... %! margmean (rm, 'nosuch') %!error ... %! margmean (fitrm (t2, 'y1-y6 ~ g + x'), 'x') %!error ... %! margmean (rm, 'species', 'Alpha', 1) %!error ... %! margmean (rm, 'species', 'Nonsense', 1) %!error grpstats (rm) %!error ... %! grpstats (rm, 'nosuch') %!error ... %! grpstats (rm, 'species', 'mode') %!error multcompare (rm) %!error ... %! multcompare (rm, 'nosuch') %!error ... %! multcompare (rm, 'species', 'ComparisonType', 'hsd') %!error ... %! multcompare (rm, 'species', 'By', 'species') %!error ... %! multcompare (rm, 'species', 'Alpha', 0) %!error ... %! multcompare (rm, 'species', 'Nonsense', 1) %!error ... %! predict (rm, table ([1; 2], 'VariableNames', {'z'})) %!error ... %! predict (rm, 'Alpha', 2) %!error ... %! predict (rm, 'Nonsense', 1) %!error ... %! predict (fitrm (t2, 'y1-y6 ~ g', 'WithinDesign', W, 'WithinModel', 'A+B'), ... %! 'WithinDesign', {1}) %!error random (rm, 1) %!error ... %! random (rm, table ([1; 2], 'VariableNames', {'z'})) %!error ... %! plot (rm, 'Group', 'Measurements') %!error ... %! plot (rm, 'Group', 'nosuch') %!error ... %! plot (rm, 'Nonsense', 1) %!error ... %! plotprofile (rm) %!error ... %! plotprofile (rm, {'species', 'Measurements'}) %!error ... %! plotprofile (rm, 'nosuch') %!error ... %! plotprofile (rm, 'species', 'Nonsense', 1) statistics-release-2.1.2/inst/Regression/coxphfit.m000066400000000000000000001101311527311104300224150ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{b} =} coxphfit (@var{X}, @var{T}) ## @deftypefnx {statistics} {@var{b} =} coxphfit (@var{X}, @var{T}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {statistics} {[@var{b}, @var{logl}] =} coxphfit (@dots{}) ## @deftypefnx {statistics} {[@var{b}, @var{logl}, @var{H}] =} coxphfit (@dots{}) ## @deftypefnx {statistics} {[@var{b}, @var{logl}, @var{H}, @var{stats}] =} coxphfit (@dots{}) ## ## Fit a Cox proportional hazards regression model. ## ## @code{@var{b} = coxphfit (@var{X}, @var{T})} returns the @math{p}-by-1 vector ## of coefficients @var{b} of the Cox model ## ## @tex ## $$ h(x_i, t) = h_0(t)\exp\left(\sum_{j=1}^{p} x_{ij} b_j\right) $$ ## @end tex ## @ifnottex ## @math{h(x_i, t) = h_0(t) exp (x_i' b)} ## @end ifnottex ## ## fitted to the @math{n}-by-@math{p} matrix of predictors @var{X} and the ## @math{n}-by-1 vector of event times @var{T}. @var{T} may instead be an ## @math{n}-by-2 matrix whose rows give a @math{(start, stop]} interval of ## exposure, the counting process form, in which an observation joins the risk ## set only after its start time. @math{h_0(t)} is the baseline ## hazard, which is left unspecified: the coefficients are estimated by ## maximizing the Cox partial likelihood, which does not involve it. ## ## @strong{@var{X} must not contain a column of ones.} The model has no ## constant term, since any constant is absorbed into the baseline hazard. A ## constant column is detected, reported by a warning, and given a zero ## coefficient. ## ## Rows of @var{X}, @var{T} or @qcode{"Frequency"} holding @qcode{NaN} are ## removed before fitting. ## ## @code{[@var{b}, @var{logl}, @var{H}, @var{stats}] = coxphfit (@dots{})} ## additionally returns the maximized partial log-likelihood @var{logl}, the ## estimated baseline cumulative hazard @var{H}, and a structure @var{stats} of ## coefficient statistics and residuals. ## ## @var{H} is a two-column matrix whose first column holds the distinct event ## times and whose second holds the estimated cumulative hazard at those times, ## evaluated at the predictor values given by @qcode{"Baseline"}. Its first ## row is the first event time with a cumulative hazard of zero; an observation ## censored before any event contributes no row. In a stratified model ## @var{H} gains a third column carrying the stratum, the blocks appear in ## ascending stratum order, and each block leads with its own zero row at its ## own first event time. A stratum holding no event contributes a single row ## of @qcode{NaN} with its label. ## ## The following @var{name}/@var{value} pairs are accepted: ## ## @multitable @columnfractions 0.18 0.82 ## @headitem Name @tab Value ## @item @qcode{"Baseline"} @tab The @var{X} values at which the baseline ## hazard is computed, either a scalar or a 1-by-@math{p} vector. The default ## is the mean of @var{X} weighted by @qcode{"Frequency"} and taken within each ## stratum, so the hazard is that of an average observation of its stratum; ## pass @qcode{0} for a hazard relative to the origin. A value given ## explicitly is used for every stratum. The coefficients do not depend on ## this choice, only @var{H} does. ## @item @qcode{"Censoring"} @tab A logical or 0/1 vector of length @math{n}, ## where 1 marks an observation right-censored at its recorded time. The ## default is a vector of zeros, so every observation is a recorded event. ## @item @qcode{"Frequency"} @tab A vector of length @math{n} of non-negative ## values giving the number of observations each row represents, or a weight. ## The default is a vector of ones. ## @item @qcode{"Ties"} @tab The method of handling tied event times, either ## @qcode{"breslow"} (default) or @qcode{"efron"}. ## @item @qcode{"B0"} @tab The starting value of the iteration, a vector of ## length @math{p}. The default is @code{0.01 ./ std (@var{X})}. ## @item @qcode{"Options"} @tab A structure of iteration settings, as built by ## @code{statset ("coxphfit")}. The fields used are @qcode{"MaxIter"}, ## @qcode{"TolX"} and @qcode{"Display"}. ## @item @qcode{"Strata"} @tab A vector of length @math{n} of stratum labels. ## Each stratum carries its own baseline hazard and its own risk sets, while ## the coefficients are shared across all of them. A predictor that does not ## vary within any stratum cannot be estimated from a stratified fit; it is ## reported by a warning and held at zero. ## @end multitable ## ## The fields of @var{stats} are: ## ## @multitable @columnfractions 0.24 0.76 ## @headitem Field @tab Contents ## @item @qcode{"covb"} @tab The estimated covariance matrix of @var{b}. ## @item @qcode{"beta"} @tab The coefficients, as returned in @var{b}. ## @item @qcode{"se"} @tab The standard errors of the coefficients. ## @item @qcode{"z"} @tab The @math{z} statistics, @var{b} over its standard ## error. ## @item @qcode{"p"} @tab The two-sided @math{p}-values of the @math{z} ## statistics. ## @item @qcode{"csres"} @tab The Cox-Snell residuals. ## @item @qcode{"devres"} @tab The deviance residuals. ## @item @qcode{"martres"} @tab The martingale residuals. ## @item @qcode{"schres"} @tab The Schoenfeld residuals, @qcode{NaN} for a ## censored observation. The mean an event is measured against follows ## @qcode{"Ties"}: under @qcode{"efron"} a tied death is measured against the ## mean over the sub-risk sets that approximation splits the tie into, so that ## every tied death at one time shares one mean and the residual does not ## depend on the order the tie was recorded in. Without a tie the two methods ## agree. ## @item @qcode{"sschres"} @tab The scaled Schoenfeld residuals. ## @item @qcode{"scores"} @tab The score residuals. ## @item @qcode{"sscores"} @tab The scaled score residuals. ## @item @qcode{"LikelihoodRatioTestP"} @tab The @math{p}-value of the ## likelihood ratio test against the model with no predictors. ## @end multitable ## ## @strong{Two documented deviations, both where R2024a disagrees with ## itself.} The martingale residual is defined as the event indicator minus ## the cumulative hazard the observation actually experienced, so ## @qcode{"csres"} and @qcode{"martres"} must sum to that indicator. They do ## here, always. ## ## Under @qcode{"efron"} ties MATLAB's do not: its @qcode{"martres"} comes from ## a cumulative hazard agreeing neither with its own @qcode{"csres"} nor with ## the @var{H} it returns, and the two sum to 1.0437 and @math{-0.0414} where ## they must give 1 and 0. ## ## In the counting process form MATLAB's @qcode{"martres"} correctly subtracts ## the hazard accrued before the observation entered, but its @qcode{"csres"} ## does not, so the two disagree by exactly that amount for any row whose start ## time follows an event. Here both account for it, so @qcode{"csres"} ## differs from MATLAB's by @math{\Lambda(start) \exp (x'b)} and the identity ## is preserved. ## ## The score residuals inherit the first of those two deviations, being an ## integral against the martingale residual: under @qcode{"efron"} ties they ## differ from MATLAB's, whose own do not sum to the score at the maximum, ## while these sum to zero under both tie methods, weighted by ## @qcode{"Frequency"} where one is given. ## ## Every other output agrees with R2024a to machine precision, across ## censoring, weights, both tie methods, stratification, and the counting ## process form. ## ## @seealso{statset, ecdf, fitlm} ## @end deftypefn function [b, logl, H, stats] = coxphfit (X, T, varargin) if (nargin < 2) print_usage (); endif ## --- X and T ----------------------------------------------------------- if (! (isnumeric (X) && isreal (X) && ismatrix (X))) error ("coxphfit: X must be a real numeric matrix."); endif if (! (isnumeric (T) && isreal (T))) error ("coxphfit: T must be a real numeric vector."); endif ## T is either a vector of event times or, in the counting process form, an ## N-by-2 matrix whose rows give a (start, stop] interval of exposure. if (columns (T) == 2 && rows (T) == rows (X)) Tstart = T(:,1); T = T(:,2); if (any (Tstart >= T)) error (strcat ("coxphfit: each row of T must give a (start, stop]", ... " interval with start strictly less than stop.")); endif elseif (isvector (T) || isempty (T)) T = T(:); Tstart = -Inf (numel (T), 1); else error (strcat ("coxphfit: T must be a vector of event times or an", ... " N-by-2 matrix of (start, stop] intervals.")); endif n = numel (T); if (rows (X) != n) error ("coxphfit: T must have one element for each row of X."); endif if (n == 0) error ("coxphfit: X and T must contain at least one observation."); endif p = columns (X); ## --- name/value pairs -------------------------------------------------- Baseline = []; Censoring = zeros (n, 1); Frequency = ones (n, 1); Ties = 'breslow'; B0 = []; Options = []; Strata = []; if (mod (numel (varargin), 2) != 0) error ("coxphfit: optional arguments must occur in NAME/VALUE pairs."); endif for i = 1:2:numel (varargin) name = varargin{i}; if (! ischar (name)) error ("coxphfit: parameter name must be a character vector."); endif switch (lower (name)) case 'baseline' Baseline = varargin{i+1}; case 'censoring' Censoring = varargin{i+1}; case 'frequency' Frequency = varargin{i+1}; case 'ties' Ties = varargin{i+1}; case 'b0' B0 = varargin{i+1}; case 'options' Options = varargin{i+1}; case 'strata' Strata = varargin{i+1}; otherwise error ("coxphfit: unknown parameter name '%s'.", name); endswitch endfor if (! (ischar (Ties) && any (strcmpi (Ties, {'breslow', 'efron'})))) error (strcat ("coxphfit: 'Ties' must be either 'breslow' or", ... " 'efron'.")); endif Ties = lower (Ties); if (! (isnumeric (Censoring) || islogical (Censoring)) || numel (Censoring) != n) error (strcat ("coxphfit: 'Censoring' must have one element for each", ... " row of X.")); endif Censoring = logical (Censoring(:)); if (! (isnumeric (Frequency) && isreal (Frequency)) || numel (Frequency) != n) error (strcat ("coxphfit: 'Frequency' must have one element for each", ... " row of X.")); endif Frequency = Frequency(:); if (any (Frequency < 0)) error (strcat ("coxphfit: 'Frequency' must be a vector of", ... " non-negative values.")); endif if (isempty (Strata)) Strata = ones (n, 1); endif if (! (isnumeric (Strata) || islogical (Strata)) || numel (Strata) != n) error (strcat ("coxphfit: 'Strata' must have one element for each row", ... " of X.")); endif Strata = double (Strata(:)); if (! isempty (B0) && (! isnumeric (B0) || numel (B0) != p)) error (strcat ("coxphfit: 'B0' must be a real vector with %d", ... " elements."), p); endif ## Any option the caller leaves out falls back on this function's own ## defaults, which is what statset's merge already means by an empty field. if (isempty (Options)) Options = statset ('coxphfit'); elseif (isstruct (Options)) Options = statset (statset ('coxphfit'), Options); else error ("coxphfit: 'Options' must be a structure."); endif maxiter = statget (Options, 'MaxIter', 100); tolx = statget (Options, 'TolX', 1e-8); display = statget (Options, 'Display', 'off'); ## --- drop incomplete rows ---------------------------------------------- ok = all (isfinite (X), 2) & isfinite (T) & isfinite (Frequency) ... & ! isnan (Strata); if (! all (ok)) X = X(ok,:); T = T(ok); Tstart = Tstart(ok); Censoring = Censoring(ok); Frequency = Frequency(ok); Strata = Strata(ok); n = numel (T); endif if (n == 0) error ("coxphfit: no complete observations remain after removing NaNs."); endif ## A column with no variation carries no information. Globally, that is a ## constant term, which the Cox model has no place for; within a stratified ## model it is a column constant inside every stratum, which the partial ## likelihood never compares across strata and so cannot estimate either. ## Both are held at zero rather than estimated. usc = unique (Strata); const = false (1, p); gconst = false (1, p); for j = 1:p gconst(j) = all (X(:,j) == X(1,j)); cj = true; for si = 1:numel (usc) xs = X(Strata == usc(si), j); if (! all (xs == xs(1))) cj = false; break; endif endfor const(j) = cj; endfor if (any (gconst)) warning ("coxphfit: the Cox model cannot have a constant term in X."); elseif (any (const)) warning (strcat ("coxphfit: a column of X is constant within every", ... " stratum and cannot be estimated.")); endif ## The default baseline is the mean of X weighted by 'Frequency', taken ## within each stratum rather than over the whole sample -- each stratum ## carries its own baseline hazard, so each is centred on its own average ## observation. MathWorks documents it as mean (X) without qualification; ## the weighted, per-stratum mean is what R2024a computes. The two agree ## whenever every frequency is 1 and there is a single stratum, so the ## documentation is partial rather than wrong. baseline_default = isempty (Baseline); if (baseline_default) Baseline = sum (Frequency .* X, 1) / sum (Frequency); endif if (! (isnumeric (Baseline) && isreal (Baseline) && (isscalar (Baseline) || numel (Baseline) == p))) error (strcat ("coxphfit: 'Baseline' must be a scalar or a vector", ... " with %d elements."), p); endif if (isscalar (Baseline)) Baseline = repmat (Baseline, 1, p); endif Baseline = Baseline(:)'; if (isempty (B0)) s = std (X, 0, 1); s(s == 0) = 1; B0 = (0.01 ./ s)'; endif B0 = B0(:); B0(const) = 0; event = ! Censoring; w = Frequency; free = ! const; ## --- Newton-Raphson on the partial likelihood -------------------------- b = B0; for iter = 1:maxiter [logl, g, F] = partial_lik (b, X, T, Tstart, event, w, Strata, Ties); if (all (! free)) break; endif step = zeros (p, 1); step(free) = F(free,free) \ g(free); if (! all (isfinite (step))) warning (strcat ("coxphfit: estimation stopped, the maximum", ... " likelihood estimate may not be finite.")); break; endif b += step; if (strcmp (display, 'iter')) printf ("coxphfit: iteration %d, log-likelihood %g\n", iter, logl); endif if (max (abs (step)) < tolx) break; endif endfor [logl, g, F] = partial_lik (b, X, T, Tstart, event, w, Strata, Ties); b(const) = 0; if (nargout < 3) return; endif ## --- baseline cumulative hazard ---------------------------------------- ## H is computed at X == Baseline, so the linear predictor is centred there; ## this scales the hazard by exp (Baseline * b) and leaves b untouched. eta = (X - Baseline) * b; ex = w .* exp (eta); ## The hazard uses the Breslow estimator for both tie methods. 'Ties' enters ## only through the partial likelihood, and so through b; MATLAB does the ## same, which is why its Efron and Breslow hazards differ by the fit alone. ## Strata are emitted in ascending label order, each block leading with a ## zero row at its own first event time -- not at its earliest observation, ## since one censored before any event contributes no row. A stratum holding ## no event at all contributes a single row of NaN. us = unique (Strata); H = []; for si = 1:numel (us) ins = Strata == us(si); ut = unique (T(event & ins)); if (isempty (ut)) blk = [NaN, NaN]; else if (baseline_default) bs = sum (w(ins) .* X(ins,:), 1) / sum (w(ins)); else bs = Baseline; endif exs = w .* exp ((X - bs) * b); h = zeros (numel (ut), 1); for k = 1:numel (ut) D = event & ins & T == ut(k); R = ins & Tstart < ut(k) & T >= ut(k); h(k) = sum (w(D)) / sum (exs(R)); endfor blk = [ut(1), 0; ut, cumsum(h)]; endif if (numel (us) > 1) blk = [blk, repmat(us(si), rows (blk), 1)]; endif H = [H; blk]; endfor if (nargout < 4) return; endif ## --- coefficient statistics -------------------------------------------- covb = zeros (p); covb(free,free) = inv (F(free,free)); se = sqrt (diag (covb)); z = b ./ se; pval = 2 * normcdf (-abs (z)); ## Likelihood ratio against the model with no predictors. logl0 = partial_lik (zeros (p, 1), X, T, Tstart, event, w, Strata, Ties); df = sum (free); lrtp = chi2cdf (2 * (logl - logl0), df, 'upper'); ## --- residuals ---------------------------------------------------------- ## Each stratum has its own baseline hazard, so every risk set below is ## restricted to the stratum of the observation it belongs to. exu = w .* exp (X * b); us = unique (Strata); Hi = zeros (n, 1); xbar = NaN (n, p); # risk-set mean at each observation's time sh = cell (numel (us), 1); # per-stratum event times, hazards and means for si = 1:numel (us) ins = Strata == us(si); ut = unique (T(event & ins)); h0 = zeros (numel (ut), 1); xb = zeros (numel (ut), p); # Breslow mean, paired with h0 xbev = zeros (numel (ut), p); # mean an event at that time is measured against for k = 1:numel (ut) D = event & ins & T == ut(k); R = ins & Tstart < ut(k) & T >= ut(k); SR = sum (exu(R)); AR = exu(R)' * X(R,:); h0(k) = sum (w(D)) / SR; xb(k,:) = AR / SR; ## The mean an event is measured against follows the tie method the fit ## used. Efron's approximation splits a tie of d deaths into d terms, ## the l-th removing a fraction l/d of the tied deaths from the risk ## set, so the mean is averaged over those d sub-risk sets. Without a ## tie the two coincide, the sum having only its l = 0 term. ## ## Only the event term takes it. The hazard term below pairs with h0, ## which is the Breslow estimator under either method, and must use the ## Breslow mean to match: it is that pairing, and only that pairing, ## which leaves the score residuals summing to the score at the maximum, ## namely zero. dk = sum (D); if (strcmp (Ties, 'efron') && dk > 1) SD = sum (exu(D)); AD = exu(D)' * X(D,:); acc = zeros (1, p); for l = 0:dk-1 acc += (AR - (l / dk) * AD) / (SR - (l / dk) * SD); endfor xbev(k,:) = acc / dk; else xbev(k,:) = xb(k,:); endif endfor sh{si} = {ut, h0, cumsum(h0), xb}; for i = find (ins)' j = find (ut <= T(i), 1, 'last'); if (! isempty (j)) Hi(i) = sh{si}{3}(j); endif ## In the counting process form the observation is only exposed over ## (start, stop], so the hazard accrued before it entered is not its own. j0 = find (ut <= Tstart(i), 1, 'last'); if (! isempty (j0)) Hi(i) -= sh{si}{3}(j0); endif j = find (ut == T(i), 1); if (! isempty (j)) xbar(i,:) = xbev(j,:); endif endfor endfor martres = double (event) - Hi .* exp (X * b); csres = double (event) - martres; ## Deviance residuals: the martingale residual symmetrized about zero. devres = zeros (n, 1); for i = 1:n m = martres(i); e = double (event(i)); if (e - m <= 0) devres(i) = sign (m) * sqrt (-2 * m); else devres(i) = sign (m) * sqrt (-2 * (m + e * log (e - m))); endif endfor ## Schoenfeld residuals: the predictor minus the risk-set weighted mean at ## the event time. A censored observation contributes none. schres = NaN (n, p); schres(event,:) = X(event,:) - xbar(event,:); nev = sum (event); sschres = repmat (b', n, 1) + nev * (schres * covb); ## Score residuals: the per-observation contribution to the score. scores = zeros (n, p); for i = 1:n si = find (us == Strata(i), 1); ut = sh{si}{1}; h0 = sh{si}{2}; xb = sh{si}{4}; acc = zeros (1, p); if (event(i)) acc += X(i,:) - xbar(i,:); endif for k = find (ut <= T(i) & ut > Tstart(i))' acc -= (X(i,:) - xb(k,:)) * exp (X(i,:) * b) * h0(k); endfor scores(i,:) = acc; endfor sscores = scores * covb; stats = struct ('covb', covb, 'beta', b, 'se', se, 'z', z, 'p', pval, ... 'csres', csres, 'devres', devres, 'martres', martres, ... 'schres', schres, 'sschres', sschres, 'scores', scores, ... 'sscores', sscores, 'LikelihoodRatioTestP', lrtp); endfunction ## Cox partial log-likelihood, its gradient, and the observed information. function [l, g, F] = partial_lik (b, X, T, Tstart, event, w, Strata, ties) p = columns (X); eta = X * b; ex = w .* exp (eta); l = 0; g = zeros (p, 1); F = zeros (p); us = unique (Strata); ## Each stratum carries its own baseline hazard, so the partial likelihood is ## the sum of the within-stratum likelihoods and the risk sets never cross. for si = 1:numel (us) ins = Strata == us(si); ut = unique (T(event & ins)); for k = 1:numel (ut) tk = ut(k); D = find (event & ins & T == tk); R = find (ins & Tstart < tk & T >= tk); wD = w(D); WD = sum (wD); l += sum (wD .* eta(D)); g += X(D,:)' * wD; XR = X(R,:); exR = ex(R); if (strcmp (ties, 'breslow')) S0 = sum (exR); S1 = XR' * exR; S2 = XR' * (XR .* exR); l -= WD * log (S0); g -= WD * (S1 / S0); F += WD * (S2 / S0 - (S1 / S0) * (S1 / S0)'); else m = numel (D); XD = X(D,:); exD = ex(D); S0 = sum (exR); S0d = sum (exD); S1 = XR' * exR; S1d = XD' * exD; S2 = XR' * (XR .* exR); S2d = XD' * (XD .* exD); for r = 0:(m-1) f = r / m; A0 = S0 - f * S0d; A1 = S1 - f * S1d; A2 = S2 - f * S2d; l -= (WD / m) * log (A0); g -= (WD / m) * (A1 / A0); F += (WD / m) * (A2 / A0 - (A1 / A0) * (A1 / A0)'); endfor endif endfor endfor endfunction %!demo %! ## Fit a Cox model to right-censored survival data %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! censored = [0; 0; 1; 0; 0; 1; 0; 0; 1; 0]; %! [b, logl] = coxphfit (X, T, 'Censoring', censored) %!demo %! ## Compare the two methods of handling tied event times %! T = [4; 4; 6; 6; 8; 8; 11; 11; 13; 13]; %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! breslow = coxphfit (X, T, 'Ties', 'breslow'); %! efron = coxphfit (X, T, 'Ties', 'efron'); %! [breslow, efron] %!shared X, T, C, Tt, F %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! C = [0; 0; 1; 0; 0; 1; 0; 0; 1; 0]; %! Tt = [4; 4; 6; 6; 8; 8; 11; 11; 13; 13]; %! F = [1; 2; 1; 1; 3; 1; 1; 2; 1; 1]; ## Coefficients and log-likelihood, against R2024a %!test %! [b, logl] = coxphfit (X, T); %! assert_equal (b, [-1.3886093196382836; 4.3814437183613322], 1e-8); %! assert_equal (logl, -8.8069639381632356, 1e-10); %!test %! [b, logl] = coxphfit (X, T, 'Censoring', C); %! assert_equal (b, [-1.0422777707095543; 3.374484233216088], 1e-8); %! assert_equal (logl, -7.6973587461887778, 1e-10); %!test %! [b, logl] = coxphfit (X, Tt, 'Censoring', C, 'Ties', 'breslow'); %! assert_equal (b, [-0.71807146728479765; 3.0751600100470267], 1e-8); %! assert_equal (logl, -9.9446106821057487, 1e-10); %!test %! [b, logl] = coxphfit (X, Tt, 'Censoring', C, 'Ties', 'efron'); %! assert_equal (b, [-0.78425008234710136; 3.3784787697856826], 1e-8); %! assert_equal (logl, -9.2459071792218612, 1e-10); %!test %! [b, logl] = coxphfit (X, T, 'Censoring', C, 'Frequency', F); %! assert_equal (b, [-1.1439301196172516; 3.7057550554856507], 1e-8); %! assert_equal (logl, -15.957308443384392, 1e-10); %!test %! b = coxphfit (X(:,1), T, 'Censoring', C); %! assert_equal (b, -0.28958528273410233, 1e-8); ## Breslow is the default method for ties %!test %! assert_equal (coxphfit (X, Tt, 'Censoring', C), ... %! coxphfit (X, Tt, 'Censoring', C, 'Ties', 'breslow')); ## The coefficients do not depend on Baseline or on the starting value %!test %! b = coxphfit (X, T, 'Censoring', C, 'Baseline', 0); %! assert_equal (b, [-1.0422777707095543; 3.374484233216088], 1e-8); %!test %! b = coxphfit (X, T, 'Censoring', C, 'B0', [0.1; -0.1]); %! assert_equal (b, [-1.0422777707095543; 3.374484233216088], 1e-8); ## The baseline cumulative hazard %!test %! [b, logl, H] = coxphfit (X, T, 'Censoring', C); %! assert_equal (size (H), [8, 2]); %! assert_equal (H(1,:), [4, 0]); %! assert_equal (H(:,1)', [4, 4, 6, 11, 13, 18, 21, 30]); %! assert_equal (H(end,2), 16.21202553, 1e-6); ## The leading row is the first event time, not the earliest observation %!test %! Cc = [1; 0; 1; 0; 0; 1; 0; 0; 1; 0]; %! [b, logl, H] = coxphfit (X, T, 'Censoring', Cc); %! assert_equal (b, [-0.94457242311897405; 3.4887572101921012], 1e-8); %! assert_equal (logl, -6.4545533447841414, 1e-10); %! assert_equal (size (H), [7, 2]); %! assert_equal (H(1,:), [6, 0]); %! assert_equal (H(:,1)', [6, 6, 11, 13, 18, 21, 30]); ## Baseline scales the hazard by exp (Baseline * b) and nothing else %!test %! [~, ~, Hm] = coxphfit (X, T, 'Censoring', C); %! [b, ~, H0] = coxphfit (X, T, 'Censoring', C, 'Baseline', 0); %! assert_equal (Hm(2:end,2) ./ H0(2:end,2), ... %! repmat (exp (mean (X) * b), 7, 1), 1e-10); ## Coefficient statistics %!test %! [b, logl, H, stats] = coxphfit (X, T); %! assert_equal (stats.beta, b); %! assert_equal (stats.se, [0.52737766743917369; 1.8537400210498443], 1e-8); %! assert_equal (stats.z, [-2.6330453589001133; 2.3635696853973904], 1e-8); %! assert_equal (stats.p, [0.0084623045168834322; 0.018099822319697333], 1e-10); %!test %! [~, ~, ~, stats] = coxphfit (X, T); %! assert_equal (stats.covb, [0.27812720411358371, -0.88932589928247008; ... %! -0.88932589928247008, 3.4363520656418767], 1e-8); %!test %! [~, ~, ~, stats] = coxphfit (X, T); %! assert_equal (stats.LikelihoodRatioTestP, 0.0018409958426933715, 1e-10); ## The stats fields, in MATLAB's own order %!test %! [~, ~, ~, stats] = coxphfit (X, T); %! assert_equal (fieldnames (stats)', {'covb', 'beta', 'se', 'z', 'p', ... %! 'csres', 'devres', 'martres', 'schres', 'sschres', 'scores', 'sscores', ... %! 'LikelihoodRatioTestP'}); ## Residuals %!test %! [~, ~, ~, stats] = coxphfit (X, T); %! assert_equal (stats.martres(1), 0.6260391348376092, 1e-8); %! assert_equal (stats.csres(1), 0.3739608651623908, 1e-8); ## The Cox-Snell and martingale residuals partition the event indicator %!test %! [~, ~, ~, stats] = coxphfit (X, T, 'Censoring', C); %! assert_equal (stats.csres + stats.martres, double (! C), 1e-12); ## The defining identity holds under 'efron' too, where MATLAB's does not %!test %! [~, ~, ~, stats] = coxphfit (X, Tt, 'Censoring', C, 'Ties', 'efron'); %! assert_equal (stats.csres + stats.martres, double (! C), 1e-12); ## A censored observation has no Schoenfeld residual %!test %! [~, ~, ~, stats] = coxphfit (X, T, 'Censoring', C); %! assert_equal (all (isnan (stats.schres(logical (C),:))(:)), true); %! assert_equal (any (isnan (stats.schres(! logical (C),:))(:)), false); ## The Schoenfeld residual follows the tie method: the two tied deaths at ## t = 4 are measured against Efron's mean over its sub-risk sets, not the ## Breslow mean of the whole risk set, which would give -2.7871 and 0.2129 %!test %! [~, ~, ~, stats] = coxphfit (X, Tt, 'Censoring', C, 'Ties', 'efron'); %! assert_equal (stats.schres(1,:), ... %! [-2.8979126429870807, -0.66427498564818011], 1e-8); %! assert_equal (stats.schres(2,:), ... %! [0.10208735701291927, 0.33572501435181989], 1e-8); %! assert_equal (stats.schres(7,:), ... %! [-1.438004413666758, -0.52896263729197179], 1e-8); ## Both tied deaths are measured against the same mean, so the residual does ## not depend on the order the tie was recorded in %!test %! [~, ~, ~, stats] = coxphfit (X, Tt, 'Censoring', C, 'Ties', 'efron'); %! assert_equal (X(1,:) - stats.schres(1,:), X(2,:) - stats.schres(2,:), 1e-12); ## Without a tie the two methods measure against the same mean %!test %! [~, ~, ~, sb] = coxphfit (X, T, 'Censoring', C); %! [~, ~, ~, se] = coxphfit (X, T, 'Censoring', C, 'Ties', 'efron'); %! assert_equal (X - sb.schres, X - se.schres, 1e-8); ## The scaled Schoenfeld residuals follow from them %!test %! [~, ~, ~, stats] = coxphfit (X, Tt, 'Censoring', C, 'Ties', 'efron'); %! assert_equal (stats.sschres(1,:), ... %! [-1.2234488884655339, 2.2036060014505958], 1e-6); ## The score residuals sum to the score at the maximum, which is zero %!test %! for tie = {'breslow', 'efron'} %! [~, ~, ~, stats] = coxphfit (X, Tt, 'Censoring', C, 'Ties', tie{1}); %! assert_equal (sum (stats.scores, 1), [0, 0], 1e-8); %! endfor ## Weighted, they sum to zero against their own weights %!test %! [~, ~, ~, stats] = coxphfit (X, Tt, 'Censoring', C, 'Ties', 'efron', ... %! 'Frequency', F); %! assert_equal (sum (F .* stats.scores, 1), [0, 0], 1e-8); ## Stratified and counting-process fits keep the identity %!test %! S = [1; 1; 1; 1; 1; 2; 2; 2; 2; 2]; %! [~, ~, ~, stats] = coxphfit (X, Tt, 'Censoring', C, 'Ties', 'efron', ... %! 'Strata', S); %! assert_equal (sum (stats.scores, 1), [0, 0], 1e-8); %!test %! T2 = [0 4; 0 4; 2 6; 0 6; 3 8; 0 8; 5 11; 0 11; 7 13; 0 13]; %! [~, ~, ~, stats] = coxphfit (X, T2, 'Censoring', C, 'Ties', 'efron'); %! assert_equal (sum (stats.scores, 1), [0, 0], 1e-8); %! assert_equal (stats.schres(1,:), ... %! [-2.9010414762242362, -0.66966185365223341], 1e-6); ## Error conditions %!error coxphfit (X) %!error ... %! coxphfit (X, T(1:5)) %!error ... %! coxphfit (X, T, 'Ties', 'nosuch') %!error ... %! coxphfit (X, T, 'NoSuchOption', 1) %!error ... %! coxphfit (X, T, 'Censoring', C(1:5)) %!error ... %! coxphfit (X, T, 'Frequency', -ones (10, 1)) %!error ... %! coxphfit (X, T, 'B0', [1; 2; 3]) %!error ... %! coxphfit (X, T, 'Options', 5) %!error ... %! coxphfit (X, T, 'Ties') %!error ... %! coxphfit (X, T, 'Strata', ones (5, 1)) %!error ... %! coxphfit (X, [T, T]) %!error ... %! coxphfit (X, ones (10, 3)) ## Stratified fits, against R2024a %!test %! S = [1; 1; 1; 1; 1; 2; 2; 2; 2; 2]; %! [b, logl, H] = coxphfit (X, T, 'Censoring', C, 'Strata', S); %! assert_equal (b, [-0.77050463891752163; 3.118196154424218], 1e-8); %! assert_equal (logl, -5.1821575033369704, 1e-10); %! assert_equal (size (H), [9, 3]); ## Each stratum leads with a zero row at its own first event time %!test %! S = [1; 1; 1; 1; 1; 2; 2; 2; 2; 2]; %! [~, ~, H] = coxphfit (X, T, 'Censoring', C, 'Strata', S); %! assert_equal (H(1,:), [4, 0, 1]); %! assert_equal (H(6,:), [18, 0, 2]); %! assert_equal (H(:,3)', [1, 1, 1, 1, 1, 2, 2, 2, 2]); ## Strata are emitted in ascending label order, whatever the input order %!test %! Sa = [2; 1; 2; 1; 2; 1; 2; 1; 2; 1]; %! warning ('off', 'Octave:coxphfit-nostratvar', 'local'); %! [b, logl, H] = coxphfit (X, T, 'Censoring', C, 'Strata', Sa); %! assert_equal (H(:,3)', [1, 1, 1, 1, 1, 2, 2, 2, 2]); %! assert_equal (H(1,1), 6); %! assert_equal (H(6,1), 4); ## A column with no within-stratum variation is not estimable %!test %! Sa = [2; 1; 2; 1; 2; 1; 2; 1; 2; 1]; %! [b, logl] = coxphfit (X, T, 'Censoring', C, 'Strata', Sa); %! assert_equal (b(2), 0); %! assert_equal (isfinite (logl), true); %!warning ... %! coxphfit (X, T, 'Censoring', C, 'Strata', [2;1;2;1;2;1;2;1;2;1]); ## A stratum holding no event contributes a single NaN row %!test %! S = [1; 1; 1; 1; 1; 2; 2; 2; 2; 2]; %! Call = [0; 0; 1; 0; 0; 1; 1; 1; 1; 1]; %! [b, logl, H] = coxphfit (X, T, 'Censoring', Call, 'Strata', S); %! assert_equal (b, [-1.0429819735537043; 4.1236894294226589], 1e-6); %! assert_equal (size (H), [6, 3]); %! assert_equal (isnan (H(6,1:2)), [true, true]); %! assert_equal (H(6,3), 2); ## Stratified fit with efron ties %!test %! S = [1; 1; 1; 1; 1; 2; 2; 2; 2; 2]; %! [b, logl] = coxphfit (X, Tt, 'Censoring', C, 'Strata', S, 'Ties', 'efron'); %! assert_equal (b, [-0.55718778093112831; 3.2450469646639726], 1e-8); %! assert_equal (logl, -5.9561243977816423, 1e-10); ## The counting process form of T %!test %! T2 = [0 4; 0 6; 2 8; 0 11; 3 13; 0 16; 5 18; 0 21; 7 25; 0 30]; %! [b, logl, H] = coxphfit (X, T2, 'Censoring', C); %! assert_equal (b, [-1.0103738367681818; 3.2523078880768508], 1e-8); %! assert_equal (logl, -7.6542119934420016, 1e-10); %! assert_equal (size (H), [8, 2]); ## A row leaves the risk set before its start time %!test %! T3 = [0 4; 1 6; 2 8; 3 11; 4 13; 5 16; 6 18; 7 21; 8 25; 9 30]; %! [b, logl] = coxphfit (X, T3, 'Censoring', C); %! assert_equal (b, [-0.91180514501458609; 2.9397105827259509], 1e-6); %! assert_equal (logl, -7.4995427161575758, 1e-10); ## Strata and the counting process form together %!test %! T3 = [0 4; 1 6; 2 8; 3 11; 4 13; 5 16; 6 18; 7 21; 8 25; 9 30]; %! S = [1; 1; 1; 1; 1; 2; 2; 2; 2; 2]; %! [b, logl, H] = coxphfit (X, T3, 'Censoring', C, 'Strata', S); %! assert_equal (b, [-0.72488691909164049; 2.9281191508655646], 1e-8); %! assert_equal (logl, -5.1261267996693967, 1e-10); %! assert_equal (size (H), [9, 3]); ## A single stratum is the unstratified fit, and H keeps two columns %!test %! [b1, l1, H1] = coxphfit (X, T, 'Censoring', C); %! [b2, l2, H2] = coxphfit (X, T, 'Censoring', C, 'Strata', ones (10, 1)); %! assert_equal (b2, b1, 1e-12); %! assert_equal (l2, l1, 1e-12); %! assert_equal (H2, H1, 1e-12); ## The residual identity holds in the counting process form too %!test %! T2 = [0 4; 0 6; 2 8; 0 11; 3 13; 0 16; 5 18; 0 21; 7 25; 0 30]; %! [~, ~, ~, stats] = coxphfit (X, T2, 'Censoring', C); %! assert_equal (stats.csres + stats.martres, double (! C), 1e-12); ## With every start at zero the counting form reduces to the plain one %!test %! [b1, l1, H1, s1] = coxphfit (X, T, 'Censoring', C); %! [b2, l2, H2, s2] = coxphfit (X, [zeros(10,1), T], 'Censoring', C); %! assert_equal (b2, b1, 1e-12); %! assert_equal (l2, l1, 1e-12); %! assert_equal (H2, H1, 1e-12); %! assert_equal (s2.csres, s1.csres, 1e-12); ## Each stratum is centred on its own mean unless Baseline is given %!test %! S = [1; 1; 1; 1; 1; 2; 2; 2; 2; 2]; %! [~, ~, Ha] = coxphfit (X, T, 'Censoring', C, 'Strata', S); %! [~, ~, Hb] = coxphfit (X, T, 'Censoring', C, 'Strata', S, 'Baseline', 0); %! assert_equal (size (Ha), size (Hb)); %! assert_equal (any (abs (Ha(:,2) - Hb(:,2)) > 1e-12), true); ## A constant column is reported, not silently absorbed %!warning ... %! coxphfit ([X, ones(10,1)], T); ## Edge cases with empty arrays %!error ... %! coxphfit ([], []) %!error ... %! coxphfit (zeros (0, 3), zeros (0, 1)) statistics-release-2.1.2/inst/Regression/doc-cache000066400000000000000000013553011527311104300221570ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 356 # name: # type: sq_string # elements: 1 # length: 18 CompactLinearModel # name: # type: sq_string # elements: 1 # length: 3422 statistics: CompactLinearModel Compact linear regression model The CompactLinearModel class stores a fitted linear regression model without the training data. A CompactLinearModel object is returned by the compact method of a LinearModel object, and retains everything needed to inspect, predict from, and run inference on the fit, while discarding the observations and per-observation diagnostics that a LinearModel object carries. This makes a CompactLinearModel object smaller to store than the LinearModel it was compacted from. The properties of a CompactLinearModel object fall into four groups: Group Properties Coefficient estimates Coefficients (a table of estimates, standard errors, t-statistics, and p-values for each term), CoefficientCovariance, CoefficientNames, and the coefficient counts NumCoefficients and NumEstimatedCoefficients. Summary statistics of the fit DFE, MSE, RMSE, Rsquared (ordinary and adjusted), SSE, SSR, SST, LogLikelihood, and ModelCriterion (AIC, BIC, etc.). Fitting method information Robust, which records the weighting function and tuning constant used when the model is fit by robust regression, and is empty for an ordinary least squares fit. Input data properties Formula, NumObservations, NumPredictors, NumVariables, PredictorNames, ResponseName, VariableInfo, and VariableNames. Because the training data is discarded, a CompactLinearModel object has no Fitted, Residuals, Diagnostics, or ObservationInfo properties, and none of its methods refit the model. Once created, the following methods are available on a CompactLinearModel object: Method Description predict Predict responses at new predictor values given in a matrix or table. Can also return pointwise or simultaneous confidence intervals alongside the point predictions. feval Predict responses given predictors as separate scalar or vector arguments (one per predictor variable) instead of a single matrix, so a CompactLinearModel object can be evaluated the same way as a plain function handle. Returns point predictions only. random Simulate new response values at new predictor locations by adding independent Gaussian noise, drawn from the estimated error variance MSE, to the fitted response. coefCI Return Wald confidence intervals for every fitted coefficient at a chosen significance level (default 0.05). coefTest Test a linear hypothesis on the fitted coefficients. With no arguments, tests the overall model F-test that all non-intercept coefficients are zero; a custom hypothesis can be given as a contrast matrix and, if needed, right-hand-side values. Returns the p-value, and optionally the F-statistic and its numerator degrees of freedom. plotEffects Plot the estimated main effect and 95% confidence interval of each predictor, evaluated between its observed minimum and maximum with all other predictors held at their observed means. plotInteraction Plot the main and conditional effects of two predictors, or the adjusted response as a function of one predictor for several fixed values of the other, to visualize whether the two predictors interact. anova Analysis of variance for the fitted model. Type 3 raises an error on a model missing a lower-order relative of one of its terms, since a CompactLinearModel object has no data to refit with. Create a CompactLinearModel object by using the compact method of a fitted LinearModel object. See also: LinearModel, LinearModel.compact # name: # type: sq_string # elements: 1 # length: 31 Compact linear regression model # name: # type: sq_string # elements: 1 # length: 40 CompactLinearModel.CoefficientCovariance # name: # type: sq_string # elements: 1 # length: 285 CompactLinearModel: property CoefficientCovariance Covariance matrix of coefficient estimates A p-by-p numeric matrix of covariance values for the coefficient estimates, where p is the number of coefficients in the fitted model as given by NumCoefficients. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Covariance matrix of coefficient estimates # name: # type: sq_string # elements: 1 # length: 35 CompactLinearModel.CoefficientNames # name: # type: sq_string # elements: 1 # length: 222 CompactLinearModel: property CoefficientNames Coefficient names A cell array of character vectors, each containing the name of the corresponding model term (e.g., '(Intercept)', 'x1', 'x1:x2'). This property is read-only. # name: # type: sq_string # elements: 1 # length: 17 Coefficient names # name: # type: sq_string # elements: 1 # length: 31 CompactLinearModel.Coefficients # name: # type: sq_string # elements: 1 # length: 411 CompactLinearModel: property Coefficients Coefficient values A table with one row for each coefficient and four columns: Estimate - estimated coefficient value SE - standard error of the estimate tStat - t-statistic for a two-sided test pValue - p-value for the t-statistic Coefficients that are dropped due to rank deficiency have Estimate = 0, SE = 0, tStat = NaN, pValue = NaN. This property is read-only. # name: # type: sq_string # elements: 1 # length: 18 Coefficient values # name: # type: sq_string # elements: 1 # length: 37 CompactLinearModel.CompactLinearModel # name: # type: sq_string # elements: 1 # length: 871 CompactLinearModel: cmdl = CompactLinearModel () CompactLinearModel: cmdl = CompactLinearModel (mdl) Create a compact linear regression model. cmdl = CompactLinearModel () returns a CompactLinearModel object with all properties empty. cmdl = CompactLinearModel (mdl) copies the coefficient estimates, fit statistics, and input data description from the fitted LinearModel object mdl into a new CompactLinearModel object cmdl, discarding the training data, per-observation diagnostics, and stepwise fitting history. If mdl was fit using robust regression, the Weights field of cmdl.Robust is emptied, although the rest of the Robust structure is retained. The usual way to obtain a CompactLinearModel object is to call the compact method on an already-fitted LinearModel object, rather than calling this constructor directly. See also: LinearModel, LinearModel.compact # name: # type: sq_string # elements: 1 # length: 41 Create a compact linear regression model. # name: # type: sq_string # elements: 1 # length: 22 CompactLinearModel.DFE # name: # type: sq_string # elements: 1 # length: 239 CompactLinearModel: property DFE Degrees of freedom for error A positive integer equal to the number of observations minus the number of estimated coefficients: DFE = NumObservations - NumEstimatedCoefficients. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Degrees of freedom for error # name: # type: sq_string # elements: 1 # length: 26 CompactLinearModel.Formula # name: # type: sq_string # elements: 1 # length: 267 CompactLinearModel: property Formula Model formula information A structure representing the model formula with fields including ResponseName, LinearPredictor, PredictorNames, TermNames, HasIntercept, Terms (the terms matrix), and InModel. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Model formula information # name: # type: sq_string # elements: 1 # length: 32 CompactLinearModel.LogLikelihood # name: # type: sq_string # elements: 1 # length: 321 CompactLinearModel: property LogLikelihood Log-likelihood of the fitted model A scalar numeric value equal to the log-likelihood of the response values, assuming each response is normally distributed with mean equal to the fitted value and variance equal to SSE/n (the MLE variance estimate). This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Log-likelihood of the fitted model # name: # type: sq_string # elements: 1 # length: 22 CompactLinearModel.MSE # name: # type: sq_string # elements: 1 # length: 207 CompactLinearModel: property MSE Mean squared error A scalar numeric value equal to SSE / DFE, where SSE is the sum of squared errors and DFE is the degrees of freedom for error. This property is read-only. # name: # type: sq_string # elements: 1 # length: 18 Mean squared error # name: # type: sq_string # elements: 1 # length: 33 CompactLinearModel.ModelCriterion # name: # type: sq_string # elements: 1 # length: 446 CompactLinearModel: property ModelCriterion Model comparison criteria A structure with four fields: AIC - Akaike information criterion: -2 x logL + 2 x m AICc - AIC corrected for sample size: AIC + (2xmx(m+1))/(n-m-1) BIC - Bayesian information criterion: -2 x logL + m x log(n) CAIC - Consistent AIC: -2 x logL + m x (log(n) + 1) Here logL is LogLikelihood, m is NumEstimatedCoefficients, and n is NumObservations. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Model comparison criteria # name: # type: sq_string # elements: 1 # length: 34 CompactLinearModel.NumCoefficients # name: # type: sq_string # elements: 1 # length: 265 CompactLinearModel: property NumCoefficients Number of model coefficients A positive integer giving the total number of coefficients in the fitted model, including any coefficients set to zero because the model terms are rank deficient. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Number of model coefficients # name: # type: sq_string # elements: 1 # length: 43 CompactLinearModel.NumEstimatedCoefficients # name: # type: sq_string # elements: 1 # length: 305 CompactLinearModel: property NumEstimatedCoefficients Number of estimated coefficients A positive integer giving the number of coefficients actually estimated, i.e., not set to zero due to rank deficiency. NumEstimatedCoefficients equals the degrees of freedom for regression. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Number of estimated coefficients # name: # type: sq_string # elements: 1 # length: 34 CompactLinearModel.NumObservations # name: # type: sq_string # elements: 1 # length: 308 CompactLinearModel: property NumObservations Number of observations used in the fit A positive integer giving the number of observations actually used in fitting the original model. Rows with missing values and rows excluded via the 'Exclude' name-value argument are not counted. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Number of observations used in the fit # name: # type: sq_string # elements: 1 # length: 32 CompactLinearModel.NumPredictors # name: # type: sq_string # elements: 1 # length: 184 CompactLinearModel: property NumPredictors Number of predictor variables A positive integer giving the number of predictor variables used to fit the model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Number of predictor variables # name: # type: sq_string # elements: 1 # length: 31 CompactLinearModel.NumVariables # name: # type: sq_string # elements: 1 # length: 273 CompactLinearModel: property NumVariables Number of variables in the input data A positive integer giving the total number of variables in the input data used to fit the original model, counting predictors, the response, and any unused columns. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Number of variables in the input data # name: # type: sq_string # elements: 1 # length: 33 CompactLinearModel.PredictorNames # name: # type: sq_string # elements: 1 # length: 206 CompactLinearModel: property PredictorNames Names of predictor variables A cell array of character vectors containing the names of the predictor variables used to fit the model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Names of predictor variables # name: # type: sq_string # elements: 1 # length: 23 CompactLinearModel.RMSE # name: # type: sq_string # elements: 1 # length: 129 CompactLinearModel: property RMSE Root mean squared error A scalar numeric value equal to sqrt(MSE). This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Root mean squared error # name: # type: sq_string # elements: 1 # length: 31 CompactLinearModel.ResponseName # name: # type: sq_string # elements: 1 # length: 158 CompactLinearModel: property ResponseName Response variable name A character vector containing the name of the response variable. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Response variable name # name: # type: sq_string # elements: 1 # length: 25 CompactLinearModel.Robust # name: # type: sq_string # elements: 1 # length: 444 CompactLinearModel: property Robust Robust fit information A structure with three fields: WgtFun - robust weighting function name, e.g. 'bisquare' Tune - tuning constant; empty if WgtFun is 'ols' or a function handle with the default tuning constant Weights - vector of final iteration weights; always empty for a CompactLinearModel object This structure is empty unless the model was fit using robust regression. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Robust fit information # name: # type: sq_string # elements: 1 # length: 27 CompactLinearModel.Rsquared # name: # type: sq_string # elements: 1 # length: 273 CompactLinearModel: property Rsquared R-squared goodness-of-fit statistics A structure with two fields: Ordinary - coefficient of determination: R^2 = SSR / SST Adjusted - adjusted R^2 that accounts for the number of coefficients in the model This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 R-squared goodness-of-fit statistics # name: # type: sq_string # elements: 1 # length: 22 CompactLinearModel.SSE # name: # type: sq_string # elements: 1 # length: 249 CompactLinearModel: property SSE Sum of squared errors A scalar numeric value equal to the sum of squared residuals. For a model with an intercept, SST = SSE + SSR. For weighted fits, this is the weighted sum of squares. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Sum of squared errors # name: # type: sq_string # elements: 1 # length: 22 CompactLinearModel.SSR # name: # type: sq_string # elements: 1 # length: 305 CompactLinearModel: property SSR Regression sum of squares A scalar numeric value equal to the sum of squared deviations of the fitted values from the mean of the response. For a model with an intercept, SST = SSE + SSR. For weighted fits, this is the weighted sum of squares. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Regression sum of squares # name: # type: sq_string # elements: 1 # length: 22 CompactLinearModel.SST # name: # type: sq_string # elements: 1 # length: 354 CompactLinearModel: property SST Total sum of squares A scalar numeric value equal to the sum of squared deviations of the response from its mean. For a model with an intercept, SST = SSE + SSR. For a robust fit, SST = SSE + SSR rather than the deviation from the mean. For weighted fits, this is the weighted sum of squares. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Total sum of squares # name: # type: sq_string # elements: 1 # length: 31 CompactLinearModel.VariableInfo # name: # type: sq_string # elements: 1 # length: 517 CompactLinearModel: property VariableInfo Information about input variables A table with one row per variable including any unused variables, and four columns: Class - variable class as a character vector, e.g. 'double' or 'categorical' Range - for continuous variables, a two-element vector [min, max]; for categorical variables, a vector of the distinct values InModel - logical; true if the variable is in the fitted model IsCategorical - logical; true if the variable is categorical This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Information about input variables # name: # type: sq_string # elements: 1 # length: 32 CompactLinearModel.VariableNames # name: # type: sq_string # elements: 1 # length: 274 CompactLinearModel: property VariableNames Names of all variables in the input data A cell array of character vectors containing the names of all variables used to fit the original model, including predictors, the response, and unused variables. This property is read-only. # name: # type: sq_string # elements: 1 # length: 40 Names of all variables in the input data # name: # type: sq_string # elements: 1 # length: 24 CompactLinearModel.anova # name: # type: sq_string # elements: 1 # length: 2683 CompactLinearModel: tbl = anova (mdl) CompactLinearModel: tbl = anova (mdl, anovatype) CompactLinearModel: tbl = anova (mdl, "components", sstype) Analysis of variance for a compact linear regression model. anova (mdl) returns a table tbl with component ANOVA statistics for every term in mdl except the constant term, computed with hierarchical ("h") sums of squares. Each row gives SumSq, DF, MeanSq, F, and pValue for the corresponding term; the trailing Error row gives SumSq = mdl.SSE, DF = mdl.DFE, MeanSq = mdl.MSE, and NaN for F and pValue. MATLAB reports F = 1 and pValue = 0.5 on that Error row instead. Those are not results: the row's F is its own MeanSq divided by itself, so it is 1 for every data set, and the pValue follows. MATLAB does not use them consistently either, reporting NaN for the same quantity on the Residual row of its summary table. This implementation reports NaN in both places. Every statistic is computed from mdl.Coefficients and mdl.CoefficientCovariance alone; a CompactLinearModel never refits, because it does not retain the training data. anova (mdl, anovatype) selects "components" (default) or "summary". For "summary", tbl always contains rows Total, Model, and Residual, and additionally . Linear and . Nonlinear whenever mdl contains an interaction term or a continuous term of degree greater than 1. Total reports mdl.SST with DF = NumObservations - 1; Model reports mdl.SSR with DF = NumCoefficients - HasIntercept; Residual reports mdl.SSE with DF = mdl.DFE. Unlike anova on a LinearModel, tbl never contains . Lack of fit or . Pure error rows, since identifying observations with identical predictor values requires the training data that a CompactLinearModel does not retain. anova (mdl, "components", sstype) selects the sum of squares used for the component table: 1 (sequential, reduction from adding each term in formula order), 2 (reduction from adding the term to a model containing every term that does not contain it), "h" (default; as Type 2, but a higher-degree term in the same continuous variable, such as a squared term, is also treated as containing the lower-degree term), or 3 (reduction from adding the term to a model containing every other term, with categorical predictors recoded using sum-to-zero deviation contrasts instead of mdl's reference-level coding). sstype is ignored when anovatype is "summary". If mdl is missing a lower-order relative of one of its terms (e.g. an interaction without one of its main effects, or a categorical predictor fit without an intercept), type 3 raises an error, since a CompactLinearModel has no data to refit with. See also: LinearModel, CompactLinearModel.coefTest # name: # type: sq_string # elements: 1 # length: 59 Analysis of variance for a compact linear regression model. # name: # type: sq_string # elements: 1 # length: 25 CompactLinearModel.coefCI # name: # type: sq_string # elements: 1 # length: 1301 CompactLinearModel: ci = coefCI (mdl) CompactLinearModel: ci = coefCI (mdl, alpha) Confidence intervals for the coefficient estimates of a fitted linear regression model. ci = coefCI (mdl) returns 95% confidence intervals for every coefficient in mdl using a default significance level of 0.05. ci = coefCI (mdl, alpha) uses the significance level alpha, a scalar in [0, 1]. The resulting intervals have coverage 100(1-\alpha)\%. Setting alpha to 0 produces intervals of infinite width; setting it to 1 collapses each interval to the corresponding point estimate. The output ci is a k-by-2 numeric matrix where k = mdl.NumCoefficients. Row j contains the interval for the j-th coefficient, whose name is stored in mdl.CoefficientNames{j}. Column 1 is the lower bound and column 2 is the upper bound. The midpoint of each interval equals the corresponding point estimate in mdl.Coefficients.Estimate. Intervals use the Wald method: b_j \pm t_{(1-\alpha/2,\,\mathrm{DFE})}\,\mathrm{SE}(b_j), where b_j is the coefficient estimate, \mathrm{SE}(b_j) is its standard error from mdl.Coefficients.SE, and the critical value is the 1-\alpha/2 quantile of the t-distribution with mdl.DFE degrees of freedom. In rank-deficient models, aliased coefficients have \mathrm{SE} = 0 and their row in ci is [0, 0]. # name: # type: sq_string # elements: 1 # length: 87 Confidence intervals for the coefficient estimates of a fitted linear regression model. # name: # type: sq_string # elements: 1 # length: 27 CompactLinearModel.coefTest # name: # type: sq_string # elements: 1 # length: 2000 CompactLinearModel: p = coefTest (mdl) CompactLinearModel: p = coefTest (mdl, H) CompactLinearModel: p = coefTest (mdl, H, C) CompactLinearModel: [p, F] = coefTest (...) CompactLinearModel: [p, F, r] = coefTest (...) Linear hypothesis test on the coefficients of a fitted linear regression model. coefTest tests whether one or more linear combinations of the fitted coefficients equal specified constants. Each linear combination is encoded as a row of the contrast matrix H, and the right-hand side is given by C. p = coefTest (mdl) performs the overall model F-test: it tests the joint null hypothesis that every coefficient except the intercept is zero. The returned p-value matches the F-statistic line printed at the bottom of the model display. p = coefTest (mdl, H) tests the null hypothesis H \beta = 0, where \beta is the full coefficient vector of length k = mdl.NumCoefficients. H must be a full-rank numeric matrix with k columns; each row specifies one linear constraint. To test a single coefficient, use a row vector with a 1 in that coefficient's position and zeros elsewhere; the resulting F-statistic equals the square of the corresponding t-statistic in mdl.Coefficients. To test a categorical predictor that expands to multiple indicator columns, include one row per indicator in H. p = coefTest (mdl, H, C) tests H \beta = C instead of zero. C must be a numeric vector with the same number of elements as rows of H; both row and column vectors are accepted. The second output F is the value of the F-statistic: F = (H\hat{\beta} - C)^\prime (H V H^\prime)^{-1} (H\hat{\beta} - C) / r, where V is mdl.CoefficientCovariance and r is the number of rows of H. The third output r is that numerator degrees of freedom; the denominator degrees of freedom is mdl.DFE. Under the null hypothesis F follows an F(r, \mathrm{DFE}) distribution and the p-value is the upper-tail probability. When H is rank-deficient but contains no NaN, both p and F are returned as NaN without an error. # name: # type: sq_string # elements: 1 # length: 79 Linear hypothesis test on the coefficients of a fitted linear regression model. # name: # type: sq_string # elements: 1 # length: 24 CompactLinearModel.feval # name: # type: sq_string # elements: 1 # length: 1339 CompactLinearModel: ypred = feval (mdl, X) CompactLinearModel: ypred = feval (mdl, x1, x2, ..., xp) Predict responses of a fitted linear regression model using separate predictor inputs. ypred = feval (mdl, X) accepts a single numeric matrix X with one column per predictor in the same order as the training data, or a table whose column names match mdl.PredictorNames. The output is an n-by-1 column vector. Rows that contain NaN in any predictor column are returned as NaN. ypred = feval (mdl, x1, x2, ..., xp) accepts exactly mdl.NumPredictors separate arguments, one per predictor variable. All non-scalar arguments must have the same size; a scalar argument is broadcast to that size automatically. The output shape follows the shape of the non-scalar inputs: column vector inputs give a column vector output, row vector inputs give a row vector output, and all-scalar inputs give a scalar. This form is convenient when predictor data is already stored in separate vectors rather than a combined matrix. feval gives the same numerical predictions as predict but does not support confidence intervals. Use predict when you also need bounds on the response. Because a CompactLinearModel object behaves like a function through feval, it can be passed directly to routines that accept a function handle, such as fminsearch or integral. # name: # type: sq_string # elements: 1 # length: 86 Predict responses of a fitted linear regression model using separate predictor inputs. # name: # type: sq_string # elements: 1 # length: 30 CompactLinearModel.plotEffects # name: # type: sq_string # elements: 1 # length: 1762 CompactLinearModel: plotEffects (mdl) CompactLinearModel: plotEffects (ax, mdl) CompactLinearModel: h = plotEffects (...) Plot the main effects of each predictor in a compact linear regression model. plotEffects (mdl) creates a horizontal dot-and-line plot with one row per predictor. Each dot shows the estimated main effect on the response from changing that predictor from its minimum observed value to its maximum observed value, while holding all other predictors fixed at their observed means. A horizontal line through each dot shows the 95% confidence interval for that effect. The main effect for predictor xs is defined as g(x_{s,\max}) - g(x_{s,\min}), where the adjusted response function g evaluates the model at the specified value of xs with all other predictors set to their observed means. For numeric predictors the sign of the effect can be positive or negative depending on the direction of the relationship. Because a CompactLinearModel does not retain the training data, these values come from a summary computed once when the model was fitted, rather than recomputed from the original observations. plotEffects (ax, mdl) creates the plot in the axes object ax instead of the current axes returned by gca. h = plotEffects (...) returns a vector of p+1 graphics handles where p is the number of predictors. h(1) is the line object containing the effect estimate markers (one circle per predictor, plotted as a single line object with XData of length p and YData = 1:p). h(j+1) is the confidence interval line for predictor j, with XData = [ci_lo, ci_hi] and YData = [j, j]. The y-axis tick labels follow the format 'varname: min to max', showing the predictor name and the minimum and maximum observed values used to compute the effect. # name: # type: sq_string # elements: 1 # length: 77 Plot the main effects of each predictor in a compact linear regression model. # name: # type: sq_string # elements: 1 # length: 34 CompactLinearModel.plotInteraction # name: # type: sq_string # elements: 1 # length: 2972 CompactLinearModel: plotInteraction (mdl, var1, var2) CompactLinearModel: plotInteraction (mdl, var1, var2, ptype) CompactLinearModel: plotInteraction (ax, ...) CompactLinearModel: h = plotInteraction (...) Plot the interaction effects of two predictors in a compact linear regression model. plotInteraction (mdl, var1, var2) creates a plot of the main effects of var1 and var2 together with their conditional effects, with horizontal lines through each effect value indicating its 95% confidence interval. var1 and var2 are each a character vector or string naming a variable in mdl.VariableNames, or a positive integer indexing into mdl.VariableNames; neither may name the response variable, and they must be different variables. The main effect of a predictor is the change in the adjusted response between the two predictor values that produce the minimum and maximum adjusted response, with the other predictor averaged over its own observed values row by row. For a numeric predictor these two values are its observed minimum and maximum; for a categorical predictor every level is evaluated and the levels producing the minimum and maximum adjusted response are used, so the effect is always nonnegative. The conditional effect of var1 is its effect recomputed with var2 additionally held fixed at each of a small set of conditioning values, and likewise the conditional effect of var2 holds var1 fixed. The conditioning values are the observed minimum, mean of the minimum and maximum, and maximum for a numeric predictor, or every level for a categorical predictor. When the main effect and conditional effect points for a predictor do not align vertically, the model exhibits an interaction between var1 and var2. Because a CompactLinearModel does not retain the training data, these values come from a summary computed once when the model was fitted, rather than recomputed from the original observations. plotInteraction (mdl, var1, var2, ptype) selects the plot type. ptype is 'effects' (default), as described above, or 'predictions', which instead plots the adjusted response as a function of var2 for each conditioning value of var1 held fixed, evaluated over 101 equally spaced points spanning the observed range of var2 when var2 is numeric, or at each level of var2 when it is categorical. plotInteraction (ax, ...) plots into the axes object ax instead of the current axes returned by gca. h = plotInteraction (...) returns a vector of line handles. When ptype is 'effects', h(1) is the marker line through the two main effect points, h(2) and h(3) are the confidence interval lines for the main effects of var1 and var2, and the remaining entries are the conditional effect points and their confidence intervals, tagged 'conditional1' for var1 and 'conditional2' for var2. The main effect line objects are tagged 'main'. When ptype is 'predictions', each entry in h corresponds to one adjusted response curve, one per conditioning value of var1. # name: # type: sq_string # elements: 1 # length: 84 Plot the interaction effects of two predictors in a compact linear regression model. # name: # type: sq_string # elements: 1 # length: 26 CompactLinearModel.predict # name: # type: sq_string # elements: 1 # length: 1777 CompactLinearModel: ypred = predict (mdl, Xnew) CompactLinearModel: [ypred, yci] = predict (mdl, Xnew) CompactLinearModel: [ypred, yci] = predict (mdl, Xnew, Name, Value) Predict responses from a fitted linear regression model. ypred = predict (mdl, Xnew) returns the fitted response values at the new predictor locations in Xnew. Xnew can be a numeric matrix with one column per predictor in the same order as the training data, or a table whose column names match mdl.PredictorNames. Rows containing NaN are returned as NaN without error. Unlike LinearModel, Xnew is required: a CompactLinearModel object does not store the training data, so there is no default to fall back on when it is omitted. [ypred, yci] = predict (...) also returns yci, an n-by-2 matrix of confidence bounds where column 1 is the lower bound and column 2 is the upper bound. By default these are 95% pointwise confidence intervals on the mean response. Name-Value pair arguments: Name Value 'Alpha' Significance level for the confidence interval, specified as a scalar in [0,1]. The interval has coverage 100(1-\alpha)\%. Default is 0.05, giving a 95% interval. 'Prediction' Type of interval to compute. "curve" (default) gives a confidence interval on the mean response f(x). "observation" gives a wider prediction interval for a single future observation y = f(x) + \varepsilon, which accounts for both estimation uncertainty and irreducible noise; it adds mdl.MSE to the variance before computing the half-width. 'Simultaneous' Logical flag controlling whether the bounds are simultaneous or pointwise. When true, Scheff'{e}'s method is used so the entire predicted curve lies within the band with 100(1-\alpha)\% confidence; these bands are always wider than pointwise ones. Default is false. # name: # type: sq_string # elements: 1 # length: 56 Predict responses from a fitted linear regression model. # name: # type: sq_string # elements: 1 # length: 25 CompactLinearModel.random # name: # type: sq_string # elements: 1 # length: 1009 CompactLinearModel: ysim = random (mdl, Xnew) Simulate responses with random noise from a fitted linear regression model. ysim = random (mdl, Xnew) computes the fitted response at each row of Xnew and then adds independent Gaussian noise to each value. The noise is drawn from N(0, \sigma^2) where \sigma^2 is the estimated error variance mdl.MSE (mean squared error of the fit). The result is a column vector of the same length as the number of rows in Xnew. Xnew is required and must be non-empty. It can be a numeric matrix with one column per predictor in the same order as the training data, or a table whose column names match mdl.PredictorNames. Because the added noise is drawn freshly on every call, two calls with the same Xnew will generally produce different output. To get reproducible results, set the random seed with rand ('state', s) before calling random. For deterministic predictions without noise, use predict or feval. predict also provides confidence intervals on the mean response. # name: # type: sq_string # elements: 1 # length: 75 Simulate responses with random noise from a fitted linear regression model. # name: # type: sq_string # elements: 1 # length: 8 CoxModel # name: # type: sq_string # elements: 1 # length: 2274 statistics: CoxModel Cox proportional hazards regression model class. A CoxModel object encapsulates a Cox proportional hazards model of a survival time on one or more predictors, fitted by maximizing the Cox partial likelihood. It is the object counterpart of coxphfit and is normally created with the fitcox function. The model states that an observation with predictor values x has hazard $$ h(x, t) = h_0(t)\exp\left(\sum_{j=1}^{p} x_{j} b_j\right) $$ where h_0(t) is an unspecified baseline hazard. The model carries no constant term: any constant is absorbed into that baseline. The most useful properties are Coefficients (a table of estimates, standard errors, z-statistics and p-values), Hazard (the estimated baseline cumulative hazard), LogLikelihood, Residuals, and the three p-values LikelihoodRatioTestPValue, ProportionalHazardsPValue and ProportionalHazardsPValueGlobal. Fitted models support the survival, hazardratio, coefci, linhyptest, plotSurvival and discardResiduals methods. A categorical predictor expands to indicator columns, one per level bar the first, which the baseline hazard carries; the indicator columns are named name_level and enter the default baseline as zero, while a numeric predictor enters it as its mean. ProportionalHazardsPValue is a Grambsch-Therneau test of each coefficient against the mid-ranks of the event times, and ProportionalHazardsPValueGlobal the same test taken over the whole model. A small p-value is evidence that the hazard ratio moves with time, which is what proportionality denies. Deviations from MATLAB, all in naming. MATLAB derives the names reported by a fitted model from three different places and they need not agree with one another: with default predictor names its Formula reads 'y ~ x1 + x2' in lower case while PredictorNames holds 'X1' and 'X2', and supplying 'PredictorNames' changes ResponseName from 'y' to the name of the variable passed as the response. Here the names are consistent by construction: ResponseName is 'y' unless the data came from a table, the Formula is built from PredictorNames and ResponseName, and neither depends on which optional arguments were given. Every fitted quantity agrees with MATLAB. See also: fitcox, coxphfit, GeneralizedLinearModel, LinearModel # name: # type: sq_string # elements: 1 # length: 48 Cox proportional hazards regression model class. # name: # type: sq_string # elements: 1 # length: 17 CoxModel.Baseline # name: # type: sq_string # elements: 1 # length: 295 CoxModel: property Baseline Predictor values the baseline hazard is evaluated at The baseline the fit used, one row per stratum. It is reported as it was given when fitcox was given one, a scalar staying a scalar, and otherwise holds the rows the fit was centred on. This property is read-only. # name: # type: sq_string # elements: 1 # length: 52 Predictor values the baseline hazard is evaluated at # name: # type: sq_string # elements: 1 # length: 30 CoxModel.CoefficientCovariance # name: # type: sq_string # elements: 1 # length: 251 CoxModel: property CoefficientCovariance Estimated covariance of the coefficients A square numeric matrix, one row and column per encoded predictor column, holding the estimated covariance of the estimates in Coefficients. This property is read-only. # name: # type: sq_string # elements: 1 # length: 40 Estimated covariance of the coefficients # name: # type: sq_string # elements: 1 # length: 21 CoxModel.Coefficients # name: # type: sq_string # elements: 1 # length: 240 CoxModel: property Coefficients Coefficient estimates and their statistics A table with one row per encoded predictor column, its row names the encoded column names, and the variables Beta, SE, zStat and pValue. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Coefficient estimates and their statistics # name: # type: sq_string # elements: 1 # length: 17 CoxModel.CoxModel # name: # type: sq_string # elements: 1 # length: 2186 CoxModel: mdl = CoxModel (X, T) CoxModel: mdl = CoxModel (tbl, respvar) CoxModel: mdl = CoxModel (..., Name, Value) Fit a Cox proportional hazards regression model. mdl = CoxModel (X, T) fits the model to the n-by-p numeric predictor matrix X and the n-by-1 vector of event times T. T may instead be an n-by-2 matrix giving a (start, stop] interval of exposure, the counting process form. mdl = CoxModel (tbl, respvar) takes the data from the table tbl, using the variable named respvar as the response and every other variable as a predictor. A categorical variable is encoded as indicator columns. X must not contain a constant column: the model has no constant term, since any constant is absorbed into the baseline hazard. The following Name/Value pairs are accepted: Name Value "Baseline" The X values at which the baseline hazard is computed, either a scalar or a 1-by-p vector. The default is the mean of each numeric predictor and zero for each indicator column of a categorical predictor, taken within each stratum. "Beta" The starting value of the iteration, a vector of length p. The default is 0.01 ./ std (X). "CategoricalPredictors" The predictors to treat as categorical, given as column indices, a logical vector, or a cell array of predictor names. Table variables of class categorical are detected without this argument. "Censoring" A logical or 0/1 vector of length n, where 1 marks an observation right-censored at its recorded time. The default is a vector of zeros. "Frequency" A vector of length n of non-negative values giving the number of observations each row represents, or a weight. The default is a vector of ones. "OptimizationOptions" A structure of iteration settings, as built by statset ("fitcox"). The fields used are "MaxIter", "TolX" and "Display". "PredictorNames" A cell array of p predictor names. The default is "X1", "X2", and so on, or the table variable names. "Stratification" A vector of length n of stratum labels. Each stratum carries its own baseline hazard and its own risk sets, while the coefficients are shared across all of them. "TieBreakMethod" The method of handling tied event times, either "breslow" (default) or "efron". # name: # type: sq_string # elements: 1 # length: 48 Fit a Cox proportional hazards regression model. # name: # type: sq_string # elements: 1 # length: 16 CoxModel.Formula # name: # type: sq_string # elements: 1 # length: 127 CoxModel: property Formula Model formula A LinearFormula object describing the terms of the model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Model formula # name: # type: sq_string # elements: 1 # length: 15 CoxModel.Hazard # name: # type: sq_string # elements: 1 # length: 272 CoxModel: property Hazard Estimated baseline cumulative hazard A numeric matrix of event times in its first column and the cumulative hazard at them in its second. A stratified model adds a third column holding the stratum each row belongs to. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Estimated baseline cumulative hazard # name: # type: sq_string # elements: 1 # length: 34 CoxModel.LikelihoodRatioTestPValue # name: # type: sq_string # elements: 1 # length: 205 CoxModel: property LikelihoodRatioTestPValue Likelihood ratio test against the null model A scalar p value comparing the fitted model with the model that carries no predictors. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Likelihood ratio test against the null model # name: # type: sq_string # elements: 1 # length: 22 CoxModel.LogLikelihood # name: # type: sq_string # elements: 1 # length: 148 CoxModel: property LogLikelihood Log-likelihood of the fitted model A scalar, the maximised Cox partial log-likelihood. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Log-likelihood of the fitted model # name: # type: sq_string # elements: 1 # length: 22 CoxModel.NumPredictors # name: # type: sq_string # elements: 1 # length: 193 CoxModel: property NumPredictors Number of predictors A positive integer counting the predictor variables of the model, before any categorical predictor is encoded. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of predictors # name: # type: sq_string # elements: 1 # length: 23 CoxModel.PredictorNames # name: # type: sq_string # elements: 1 # length: 158 CoxModel: property PredictorNames Names of the predictor variables A cell array of character vectors with one name per predictor. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Names of the predictor variables # name: # type: sq_string # elements: 1 # length: 34 CoxModel.ProportionalHazardsPValue # name: # type: sq_string # elements: 1 # length: 307 CoxModel: property ProportionalHazardsPValue Proportional hazards test, one predictor at a time A numeric vector with one p value per predictor, testing whether that predictor's effect is constant over time. A small value is evidence against the proportional hazards assumption. This property is read-only. # name: # type: sq_string # elements: 1 # length: 50 Proportional hazards test, one predictor at a time # name: # type: sq_string # elements: 1 # length: 40 CoxModel.ProportionalHazardsPValueGlobal # name: # type: sq_string # elements: 1 # length: 215 CoxModel: property ProportionalHazardsPValueGlobal Proportional hazards test over the whole model A scalar p value testing the proportional hazards assumption for all predictors at once. This property is read-only. # name: # type: sq_string # elements: 1 # length: 46 Proportional hazards test over the whole model # name: # type: sq_string # elements: 1 # length: 18 CoxModel.Residuals # name: # type: sq_string # elements: 1 # length: 227 CoxModel: property Residuals Residuals of the fitted model A table with one row per observation and the variables CoxSnell, Deviance, Martingale, Schoenfeld, ScaledSchoenfeld, Score and ScaledScore. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Residuals of the fitted model # name: # type: sq_string # elements: 1 # length: 21 CoxModel.ResponseName # name: # type: sq_string # elements: 1 # length: 158 CoxModel: property ResponseName Name of the response variable A character vector naming the response, which is the survival time. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 22 CoxModel.StandardError # name: # type: sq_string # elements: 1 # length: 217 CoxModel: property StandardError Standard errors of the coefficients A numeric column vector, the square roots of the diagonal of CoefficientCovariance, which is column SE of Coefficients. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Standard errors of the coefficients # name: # type: sq_string # elements: 1 # length: 23 CoxModel.Stratification # name: # type: sq_string # elements: 1 # length: 198 CoxModel: property Stratification Stratification levels used in the fit The distinct levels of the stratification variable. It is empty when the model is not stratified. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Stratification levels used in the fit # name: # type: sq_string # elements: 1 # length: 21 CoxModel.VariableInfo # name: # type: sq_string # elements: 1 # length: 220 CoxModel: property VariableInfo Information about the variables A table with one row per variable, its row names the variable names, and the variables Class, Range, InModel and IsCategorical. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Information about the variables # name: # type: sq_string # elements: 1 # length: 15 CoxModel.coefci # name: # type: sq_string # elements: 1 # length: 394 CoxModel: ci = coefci (obj) CoxModel: ci = coefci (obj, level) Confidence intervals for the coefficients of a Cox model. ci = coefci (obj) returns a two-column matrix with one row per coefficient, holding the 95% confidence interval of each. ci = coefci (obj, level) uses a 100 (1 - level)% interval. level must be a positive scalar smaller than 1; it is a significance level, not a coverage. # name: # type: sq_string # elements: 1 # length: 57 Confidence intervals for the coefficients of a Cox model. # name: # type: sq_string # elements: 1 # length: 25 CoxModel.discardResiduals # name: # type: sq_string # elements: 1 # length: 396 CoxModel: obj = discardResiduals (obj) Drop the stored residuals of a Cox model. obj = discardResiduals (obj) returns the model with an empty Residuals property. The residual table holds one row per observation and is the largest thing a fitted model carries, so discarding it makes a model that is only going to be used for prediction considerably smaller. Nothing else about the model changes. # name: # type: sq_string # elements: 1 # length: 41 Drop the stored residuals of a Cox model. # name: # type: sq_string # elements: 1 # length: 20 CoxModel.hazardratio # name: # type: sq_string # elements: 1 # length: 740 CoxModel: hr = hazardratio (obj, X) CoxModel: hr = hazardratio (obj, X, S) CoxModel: hr = hazardratio (..., "Baseline", B) Hazard of a Cox model relative to its baseline. hr = hazardratio (obj, X) returns the hazard at the predictor values X relative to the baseline the model was fitted with, exp ((X - B) * b). X has one row per evaluation point and is a numeric matrix, or a table when the model was fitted from one. hr = hazardratio (obj, X, S) gives the stratum of each row of X, and is required when the model is stratified, each stratum having its own baseline. hr = hazardratio (..., "Baseline", B) evaluates the ratio against the baseline B instead, either a scalar or a row vector with one element per encoded predictor column. # name: # type: sq_string # elements: 1 # length: 47 Hazard of a Cox model relative to its baseline. # name: # type: sq_string # elements: 1 # length: 19 CoxModel.linhyptest # name: # type: sq_string # elements: 1 # length: 574 CoxModel: tbl = linhyptest (obj) Sequential tests on the coefficients of a Cox model. tbl = linhyptest (obj) returns a table with one row per predictor, whose k-th row tests the hypothesis that the coefficients of the k-th and every later predictor are jointly zero. The Predictor column names the predictors the hypothesis leaves in the model, so its first row reads "Empty Model" and tests every coefficient at once, and its last row tests the last coefficient alone, reproducing that coefficient's own p-value. Each test is a Wald test on the fitted model, not a refit. # name: # type: sq_string # elements: 1 # length: 52 Sequential tests on the coefficients of a Cox model. # name: # type: sq_string # elements: 1 # length: 21 CoxModel.plotSurvival # name: # type: sq_string # elements: 1 # length: 533 CoxModel: plotSurvival (obj) CoxModel: plotSurvival (obj, X) CoxModel: plotSurvival (obj, X, S) CoxModel: h = plotSurvival (...) Plot the survival function of a Cox model. plotSurvival (obj) draws the survival function at the model's baseline as a stairstep plot. plotSurvival (obj, X) draws it at the predictor values X, one curve per row, and S gives the stratum of each row when the model is stratified. A stratified model with no X draws one curve per stratum. h = plotSurvival (...) returns the handles of the stairstep lines. # name: # type: sq_string # elements: 1 # length: 42 Plot the survival function of a Cox model. # name: # type: sq_string # elements: 1 # length: 17 CoxModel.survival # name: # type: sq_string # elements: 1 # length: 1096 CoxModel: s = survival (obj) CoxModel: s = survival (obj, X) CoxModel: s = survival (obj, X, S) CoxModel: s = survival (..., Name, Value) CoxModel: [s, T] = survival (...) Survival function of a Cox model. s = survival (obj) returns the survival probability at the model's baseline, evaluated at each row of the Hazard property. s = survival (obj, X) evaluates it at the predictor values X, and S gives the stratum of each row when the model is stratified. For a stratified model s is a cell array holding one column vector per curve. [s, T] = survival (...) also returns the times the probabilities refer to. The following Name/Value pairs are accepted: Name Value "Time" The times at which to evaluate the survival function. The default is the model's own event times. The baseline survival is interpolated linearly between them and raised to the hazard ratio of X. "ExtrapolationMethod" How to evaluate a time outside the model's event times: "nearest" (default), "linear", "next", "previous", or "none". "none" returns NaN outside the range, as do "next" above it and "previous" below it. # name: # type: sq_string # elements: 1 # length: 33 Survival function of a Cox model. # name: # type: sq_string # elements: 1 # length: 27 GeneralizedLinearMixedModel # name: # type: sq_string # elements: 1 # length: 721 statistics: GeneralizedLinearMixedModel Generalized linear mixed-effects model fitted to data. A GeneralizedLinearMixedModel object represents a fitted generalized linear mixed-effects model: a generalized linear model whose linear predictor X*beta + Z*b includes normally distributed random effects b ~ N(0, Psi). Objects are created with fitglme. The model is fitted by penalized quasi-likelihood. The fixed-effects estimates and their statistics are available through the Coefficients table, the covariance parameters through covarianceParameters, and predictions, residuals, and hypothesis tests through the predict, residuals, anova, coefTest, and coefCI methods. See also: fitglme, fitlme, GeneralizedLinearModel # name: # type: sq_string # elements: 1 # length: 54 Generalized linear mixed-effects model fitted to data. # name: # type: sq_string # elements: 1 # length: 49 GeneralizedLinearMixedModel.CoefficientCovariance # name: # type: sq_string # elements: 1 # length: 272 GeneralizedLinearMixedModel: property CoefficientCovariance Covariance of the fixed-effects estimates A square numeric matrix, one row and column per fixed-effects coefficient, holding the estimated covariance of the estimates in Coefficients. This property is read-only. # name: # type: sq_string # elements: 1 # length: 41 Covariance of the fixed-effects estimates # name: # type: sq_string # elements: 1 # length: 44 GeneralizedLinearMixedModel.CoefficientNames # name: # type: sq_string # elements: 1 # length: 202 GeneralizedLinearMixedModel: property CoefficientNames Names of the fixed-effects coefficients A cell array of character vectors with one name per fixed-effects coefficient. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Names of the fixed-effects coefficients # name: # type: sq_string # elements: 1 # length: 40 GeneralizedLinearMixedModel.Coefficients # name: # type: sq_string # elements: 1 # length: 336 GeneralizedLinearMixedModel: property Coefficients Fixed-effects estimates and their statistics A table with one row per fixed-effects coefficient, its row names taken from CoefficientNames, and the variables Estimate, SE, tStat, DF, pValue, Lower and Upper. Lower and Upper bound a 95% confidence interval. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Fixed-effects estimates and their statistics # name: # type: sq_string # elements: 1 # length: 31 GeneralizedLinearMixedModel.DFE # name: # type: sq_string # elements: 1 # length: 159 GeneralizedLinearMixedModel: property DFE Residual degrees of freedom A nonnegative integer, NumObservations less NumCoefficients. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Residual degrees of freedom # name: # type: sq_string # elements: 1 # length: 38 GeneralizedLinearMixedModel.Dispersion # name: # type: sq_string # elements: 1 # length: 201 GeneralizedLinearMixedModel: property Dispersion Dispersion parameter A positive scalar. It is estimated for a normal response and fixed at 1 for a binomial or Poisson one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Dispersion parameter # name: # type: sq_string # elements: 1 # length: 40 GeneralizedLinearMixedModel.Distribution # name: # type: sq_string # elements: 1 # length: 204 GeneralizedLinearMixedModel: property Distribution Response distribution A character vector naming the distribution of the response, one of 'binomial', 'poisson' and 'normal'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Response distribution # name: # type: sq_string # elements: 1 # length: 37 GeneralizedLinearMixedModel.FitMethod # name: # type: sq_string # elements: 1 # length: 213 GeneralizedLinearMixedModel: property FitMethod Estimation method A character vector naming the method that fitted the model, one of 'MPL', 'REMPL', 'Laplace' and 'ApproximateLaplace'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 17 Estimation method # name: # type: sq_string # elements: 1 # length: 35 GeneralizedLinearMixedModel.Formula # name: # type: sq_string # elements: 1 # length: 205 GeneralizedLinearMixedModel: property Formula Model formula A character vector describing the model. It is empty for a fit built from design matrices, which carries no formula. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Model formula # name: # type: sq_string # elements: 1 # length: 55 GeneralizedLinearMixedModel.GeneralizedLinearMixedModel # name: # type: sq_string # elements: 1 # length: 198 GeneralizedLinearMixedModel: glme = GeneralizedLinearMixedModel (info) Construct from a fitted-model info struct. Used internally by fitglme; call that function rather than the constructor directly. # name: # type: sq_string # elements: 1 # length: 42 Construct from a fitted-model info struct. # name: # type: sq_string # elements: 1 # length: 32 GeneralizedLinearMixedModel.Link # name: # type: sq_string # elements: 1 # length: 159 GeneralizedLinearMixedModel: property Link Link function A character vector naming the link, one of 'logit', 'log' and 'identity'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Link function # name: # type: sq_string # elements: 1 # length: 41 GeneralizedLinearMixedModel.LogLikelihood # name: # type: sq_string # elements: 1 # length: 252 GeneralizedLinearMixedModel: property LogLikelihood Log-likelihood of the fitted model A scalar. It is a pseudo log-likelihood when FitMethod is 'MPL' or 'REMPL', and a Laplace approximation to the log-likelihood otherwise. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Log-likelihood of the fitted model # name: # type: sq_string # elements: 1 # length: 42 GeneralizedLinearMixedModel.ModelCriterion # name: # type: sq_string # elements: 1 # length: 298 GeneralizedLinearMixedModel: property ModelCriterion Information criteria A scalar structure with the fields AIC, BIC, LogLikelihood and Deviance. The parameter count behind AIC and BIC holds the fixed-effects coefficients, the covariance parameters and the dispersion. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Information criteria # name: # type: sq_string # elements: 1 # length: 43 GeneralizedLinearMixedModel.NumCoefficients # name: # type: sq_string # elements: 1 # length: 192 GeneralizedLinearMixedModel: property NumCoefficients Number of fixed-effects coefficients A positive integer counting the fixed-effects coefficients of the model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Number of fixed-effects coefficients # name: # type: sq_string # elements: 1 # length: 43 GeneralizedLinearMixedModel.NumObservations # name: # type: sq_string # elements: 1 # length: 168 GeneralizedLinearMixedModel: property NumObservations Number of observations A positive integer counting the observations used for the fit. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 40 GeneralizedLinearMixedModel.ResponseName # name: # type: sq_string # elements: 1 # length: 199 GeneralizedLinearMixedModel: property ResponseName Name of the response variable A character vector naming the response. It is empty for a fit built from design matrices. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 33 GeneralizedLinearMixedModel.anova # name: # type: sq_string # elements: 1 # length: 161 GeneralizedLinearMixedModel: tbl = anova (glme) Analysis-of-deviance table of F-tests for the fixed-effects terms, using residual denominator degrees of freedom. # name: # type: sq_string # elements: 1 # length: 113 Analysis-of-deviance table of F-tests for the fixed-effects terms, using residual denominator degrees of freedom. # name: # type: sq_string # elements: 1 # length: 34 GeneralizedLinearMixedModel.coefCI # name: # type: sq_string # elements: 1 # length: 111 GeneralizedLinearMixedModel: ci = coefCI (glme, alpha) Confidence intervals for the fixed-effects coefficients. # name: # type: sq_string # elements: 1 # length: 56 Confidence intervals for the fixed-effects coefficients. # name: # type: sq_string # elements: 1 # length: 36 GeneralizedLinearMixedModel.coefTest # name: # type: sq_string # elements: 1 # length: 110 GeneralizedLinearMixedModel: [p, F, df1, df2] = coefTest (glme, H) F-test of the linear hypothesis H*beta = 0. # name: # type: sq_string # elements: 1 # length: 43 F-test of the linear hypothesis H*beta = 0. # name: # type: sq_string # elements: 1 # length: 48 GeneralizedLinearMixedModel.covarianceParameters # name: # type: sq_string # elements: 1 # length: 136 GeneralizedLinearMixedModel: [psi, disp] = covarianceParameters (glme) Return the random-effects covariance matrices and the dispersion. # name: # type: sq_string # elements: 1 # length: 65 Return the random-effects covariance matrices and the dispersion. # name: # type: sq_string # elements: 1 # length: 40 GeneralizedLinearMixedModel.designMatrix # name: # type: sq_string # elements: 1 # length: 182 GeneralizedLinearMixedModel: D = designMatrix (glme, type) Return the fixed ("Fixed", default) design matrix as a full matrix, or the random ("Random") design matrix as a sparse one. # name: # type: sq_string # elements: 1 # length: 123 Return the fixed ("Fixed", default) design matrix as a full matrix, or the random ("Random") design matrix as a sparse one. # name: # type: sq_string # elements: 1 # length: 34 GeneralizedLinearMixedModel.fitted # name: # type: sq_string # elements: 1 # length: 116 GeneralizedLinearMixedModel: yf = fitted (glme) Return the fitted mean response (conditional on the random effects). # name: # type: sq_string # elements: 1 # length: 68 Return the fitted mean response (conditional on the random effects). # name: # type: sq_string # elements: 1 # length: 40 GeneralizedLinearMixedModel.fixedEffects # name: # type: sq_string # elements: 1 # length: 132 GeneralizedLinearMixedModel: [beta, names] = fixedEffects (glme) Return the fixed-effects coefficients and, optionally, their names. # name: # type: sq_string # elements: 1 # length: 67 Return the fixed-effects coefficients and, optionally, their names. # name: # type: sq_string # elements: 1 # length: 35 GeneralizedLinearMixedModel.predict # name: # type: sq_string # elements: 1 # length: 266 GeneralizedLinearMixedModel: ypred = predict (glme, Xnew, Znew, Gnew) Predict the mean response at new data. With "Conditional" true (default) the random effects of known grouping levels are added; unknown levels fall back to the marginal (fixed-effects) prediction. # name: # type: sq_string # elements: 1 # length: 38 Predict the mean response at new data. # name: # type: sq_string # elements: 1 # length: 41 GeneralizedLinearMixedModel.randomEffects # name: # type: sq_string # elements: 1 # length: 114 GeneralizedLinearMixedModel: b = randomEffects (glme) Return the estimated random-effects (the conditional modes). # name: # type: sq_string # elements: 1 # length: 60 Return the estimated random-effects (the conditional modes). # name: # type: sq_string # elements: 1 # length: 37 GeneralizedLinearMixedModel.residuals # name: # type: sq_string # elements: 1 # length: 168 GeneralizedLinearMixedModel: r = residuals (glme) GeneralizedLinearMixedModel: r = residuals (glme, "ResidualType", type) Return "Raw" (default) or "Pearson" residuals. # name: # type: sq_string # elements: 1 # length: 46 Return "Raw" (default) or "Pearson" residuals. # name: # type: sq_string # elements: 1 # length: 22 GeneralizedLinearModel # name: # type: sq_string # elements: 1 # length: 2732 statistics: GeneralizedLinearModel Generalized linear regression model class. A GeneralizedLinearModel object encapsulates a generalized linear model (GLM) of a response on one or more predictors, fitted by iteratively reweighted least squares. It is the GLM counterpart of LinearModel and is normally created with the fitglm function. The response is modelled through a distribution from the exponential family ('normal', 'binomial', 'poisson', 'gamma', or 'inverse gaussian') and a link function g relating the mean mu to the linear predictor eta = g (mu). The most useful properties are Coefficients (a table of estimates, standard errors, t-statistics and p-values), Deviance, Dispersion, Residuals, Fitted, Diagnostics, Distribution, and Link. ObservationInfo records which rows were weighted, excluded, or missing, and Variables holds the data the model was built from. For a binomial response given as an n-by-2 matrix of successes and trials, Variables holds the success count alone, that being the response the model fits; MATLAB stores both columns there. Fitted models support the predict and feval methods for prediction. Fitted, Residuals, Diagnostics, and ObservationInfo have one row per input observation, not per fitted observation. Rows that were excluded with the 'Exclude' pair still carry a fitted value and a residual, since the model can be evaluated there; rows dropped because a variable was missing carry NaN. For a binomial response carrying a number of trials N -- given either by the 'BinomialSize' pair or as the second column of a two-column response -- the response is the number of successes, as fitglm documents. Fitted.Response is then the fitted count N p and Residuals.Raw is on that same count scale, while Fitted.Probability carries p itself. predict returns the probability, never a count: a trial count belongs to an observation, and new predictor values do not carry one. A categorical predictor expands to indicator columns, one per level bar the reference level, which the intercept carries. When the model has no intercept, the first categorical predictor is given an indicator for every one of its levels instead, so that its coefficients are the group means; any further categorical predictor stays reference coded, which keeps the design full rank. This differs from MATLAB, which omits the reference level whether or not an intercept is present and so cannot fit the reference group at all -- for a three-level grouping variable g, MATLAB fits y ~ g - 1 with two coefficients, predicts exactly 0 for every observation in the omitted group, and reports a negative R^2. This implementation returns three coefficients, one per group. See also: fitglm, LinearModel, glmfit, glmval # name: # type: sq_string # elements: 1 # length: 42 Generalized linear regression model class. # name: # type: sq_string # elements: 1 # length: 44 GeneralizedLinearModel.CoefficientCovariance # name: # type: sq_string # elements: 1 # length: 348 GeneralizedLinearModel: property CoefficientCovariance Covariance matrix of the coefficient estimates A square matrix with one row and column per coefficient, whose diagonal is the square of Coefficients.SE. It is scaled by Dispersion, so it is the covariance under the estimated dispersion wherever one was estimated. This property is read-only. # name: # type: sq_string # elements: 1 # length: 46 Covariance matrix of the coefficient estimates # name: # type: sq_string # elements: 1 # length: 39 GeneralizedLinearModel.CoefficientNames # name: # type: sq_string # elements: 1 # length: 395 GeneralizedLinearModel: property CoefficientNames Names of the coefficients A cell array of character vectors, one per coefficient, in the order the coefficients appear. The intercept is '(Intercept)', an interaction joins its factors with a colon, and a categorical predictor contributes one name per indicator, spelled name_level, so these are not the term names. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Names of the coefficients # name: # type: sq_string # elements: 1 # length: 35 GeneralizedLinearModel.Coefficients # name: # type: sq_string # elements: 1 # length: 649 GeneralizedLinearModel: property Coefficients Coefficient values A table with one row per coefficient and four columns: Estimate - estimated coefficient value SE - standard error of the estimate tStat - the estimate divided by its standard error pValue - p-value of that statistic The statistic is referred to the normal distribution where the dispersion is fixed, as it is for the binomial and Poisson families, and to a t-distribution on DFE degrees of freedom where it is estimated. Coefficients dropped as rank deficient have Estimate = 0, SE = 0, and NaN for both statistics. Row names are the coefficient names. This property is read-only. # name: # type: sq_string # elements: 1 # length: 18 Coefficient values # name: # type: sq_string # elements: 1 # length: 26 GeneralizedLinearModel.DFE # name: # type: sq_string # elements: 1 # length: 161 GeneralizedLinearModel: property DFE Error degrees of freedom A nonnegative integer, NumObservations less NumEstimatedCoefficients. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Error degrees of freedom # name: # type: sq_string # elements: 1 # length: 31 GeneralizedLinearModel.Deviance # name: # type: sq_string # elements: 1 # length: 341 GeneralizedLinearModel: property Deviance Deviance of the fitted model A nonnegative scalar, twice the difference between the log-likelihood of the saturated model and that of this one. It is the generalized linear model's counterpart of the residual sum of squares, and it is what a nested-model test compares. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Deviance of the fitted model # name: # type: sq_string # elements: 1 # length: 34 GeneralizedLinearModel.Diagnostics # name: # type: sq_string # elements: 1 # length: 389 GeneralizedLinearModel: property Diagnostics Per-observation diagnostics A table with one row per input observation and three columns: Leverage, the diagonal of the weighted hat matrix; CooksDistance, the influence of the observation on every fitted value at once; and HatMatrix, that observation's row of the hat matrix. Rows not used in the fit contain NaN. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Per-observation diagnostics # name: # type: sq_string # elements: 1 # length: 33 GeneralizedLinearModel.Dispersion # name: # type: sq_string # elements: 1 # length: 368 GeneralizedLinearModel: property Dispersion Dispersion parameter A positive scalar. It is estimated from the Pearson statistic for the normal, gamma, and inverse Gaussian families, and fixed at 1 for the binomial and Poisson families unless 'DispersionFlag' asked otherwise. CoefficientCovariance and the standard errors are scaled by it. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Dispersion parameter # name: # type: sq_string # elements: 1 # length: 42 GeneralizedLinearModel.DispersionEstimated # name: # type: sq_string # elements: 1 # length: 315 GeneralizedLinearModel: property DispersionEstimated Whether the dispersion was estimated A logical scalar, true where Dispersion was estimated from the data and false where it was held at 1. It decides whether a coefficient's statistic is referred to the normal or the t distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Whether the dispersion was estimated # name: # type: sq_string # elements: 1 # length: 35 GeneralizedLinearModel.Distribution # name: # type: sq_string # elements: 1 # length: 383 GeneralizedLinearModel: property Distribution The response distribution A structure with three fields: Name, the distribution's name; DevianceFunction, a function handle giving the deviance contribution of an observation from its response and mean; and VarianceFunction, a function handle giving the variance of an observation as a function of its mean. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 The response distribution # name: # type: sq_string # elements: 1 # length: 29 GeneralizedLinearModel.Fitted # name: # type: sq_string # elements: 1 # length: 451 GeneralizedLinearModel: property Fitted Fitted values A table with one row per input observation and two columns, Response on the scale of the response and LinearPredictor on the scale of the link. A binomial fit gains a third, Probability, since its Response is a count of successes while the fit works in the proportion. Rows kept out of the fit by 'Exclude' still carry a prediction; rows dropped as missing carry NaN. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Fitted values # name: # type: sq_string # elements: 1 # length: 30 GeneralizedLinearModel.Formula # name: # type: sq_string # elements: 1 # length: 394 GeneralizedLinearModel: property Formula The model formula A LinearFormula object describing the fitted model, with properties including ResponseName, LinearPredictor, PredictorNames, TermNames, Terms, HasIntercept, and Link. Its terms are expressed over the model's variables, so a categorical predictor contributes one term however many indicators it expands to. This property is read-only. # name: # type: sq_string # elements: 1 # length: 17 The model formula # name: # type: sq_string # elements: 1 # length: 45 GeneralizedLinearModel.GeneralizedLinearModel # name: # type: sq_string # elements: 1 # length: 266 GeneralizedLinearModel: mdl = GeneralizedLinearModel (data, resp, modelspec) GeneralizedLinearModel: mdl = GeneralizedLinearModel (..., Name, Value) Fit a generalized linear model. Prefer the fitglm function, which documents the accepted inputs and Name/Value pairs. # name: # type: sq_string # elements: 1 # length: 31 Fit a generalized linear model. # name: # type: sq_string # elements: 1 # length: 40 GeneralizedLinearModel.LikelihoodPenalty # name: # type: sq_string # elements: 1 # length: 259 GeneralizedLinearModel: property LikelihoodPenalty Penalty applied to the likelihood A character vector, always 'none': no penalized-likelihood fitting is offered, so the coefficients are always the plain maximum-likelihood ones. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Penalty applied to the likelihood # name: # type: sq_string # elements: 1 # length: 27 GeneralizedLinearModel.Link # name: # type: sq_string # elements: 1 # length: 324 GeneralizedLinearModel: property Link The link function A structure with four fields: Name, the link's name; Link, a function handle mapping the mean to the linear predictor; Derivative, a handle giving that map's derivative; and Inverse, a handle mapping the linear predictor back to the mean. This property is read-only. # name: # type: sq_string # elements: 1 # length: 17 The link function # name: # type: sq_string # elements: 1 # length: 36 GeneralizedLinearModel.LogLikelihood # name: # type: sq_string # elements: 1 # length: 304 GeneralizedLinearModel: property LogLikelihood Log-likelihood of the fitted model A scalar, the log-likelihood of the observations under the fitted coefficients and the family's own density. It is what the information criteria and the likelihood-ratio R^2 are computed from. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Log-likelihood of the fitted model # name: # type: sq_string # elements: 1 # length: 37 GeneralizedLinearModel.ModelCriterion # name: # type: sq_string # elements: 1 # length: 318 GeneralizedLinearModel: property ModelCriterion Information criteria A structure with four fields, AIC, AICc, BIC, and CAIC, each penalising LogLikelihood by a different function of the coefficient count and the sample size. AICc is Inf where the correction's denominator is not positive. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Information criteria # name: # type: sq_string # elements: 1 # length: 38 GeneralizedLinearModel.NumCoefficients # name: # type: sq_string # elements: 1 # length: 273 GeneralizedLinearModel: property NumCoefficients Number of coefficients A positive integer counting every coefficient the model carries, those dropped as rank deficient included. A categorical predictor with L levels contributes L - 1 of them. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of coefficients # name: # type: sq_string # elements: 1 # length: 47 GeneralizedLinearModel.NumEstimatedCoefficients # name: # type: sq_string # elements: 1 # length: 369 GeneralizedLinearModel: property NumEstimatedCoefficients Number of coefficients actually estimated A positive integer counting the coefficients that carry a degree of freedom, which is NumCoefficients less however many were dropped as rank deficient. It is the number the degrees of freedom and the information criteria are computed from. This property is read-only. # name: # type: sq_string # elements: 1 # length: 41 Number of coefficients actually estimated # name: # type: sq_string # elements: 1 # length: 38 GeneralizedLinearModel.NumObservations # name: # type: sq_string # elements: 1 # length: 291 GeneralizedLinearModel: property NumObservations Number of observations used in the fit A positive integer giving the number of observations the fit actually used. Rows holding a missing value and rows named by the 'Exclude' name-value argument are not counted. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Number of observations used in the fit # name: # type: sq_string # elements: 1 # length: 36 GeneralizedLinearModel.NumPredictors # name: # type: sq_string # elements: 1 # length: 311 GeneralizedLinearModel: property NumPredictors Number of predictor variables A nonnegative integer counting the predictors the model was given, whether or not each appears in a term. It counts variables, so a categorical predictor counts once however many indicators it expands to. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Number of predictor variables # name: # type: sq_string # elements: 1 # length: 35 GeneralizedLinearModel.NumVariables # name: # type: sq_string # elements: 1 # length: 237 GeneralizedLinearModel: property NumVariables Number of variables A positive integer, the number of elements of VariableNames: the predictors and the response together, whether or not each appears in a term. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Number of variables # name: # type: sq_string # elements: 1 # length: 38 GeneralizedLinearModel.ObservationInfo # name: # type: sq_string # elements: 1 # length: 368 GeneralizedLinearModel: property ObservationInfo Per-observation status A table with one row per input observation and four columns: Weights, the weight it was given; Excluded, true where 'Exclude' named it; Missing, true where its data are incomplete; and Subset, true where it was used in the fit, which is neither excluded nor missing. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Per-observation status # name: # type: sq_string # elements: 1 # length: 39 GeneralizedLinearModel.ObservationNames # name: # type: sq_string # elements: 1 # length: 212 GeneralizedLinearModel: property ObservationNames Names of the observations A cell array of character vectors, one per input observation, and empty unless the data carried row names. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Names of the observations # name: # type: sq_string # elements: 1 # length: 29 GeneralizedLinearModel.Offset # name: # type: sq_string # elements: 1 # length: 321 GeneralizedLinearModel: property Offset Offset added to the linear predictor A column vector with one element per input observation, added to the linear predictor with a coefficient fixed at one, so that it shifts the fit without being estimated. It is all zeros where no 'Offset' was given. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Offset added to the linear predictor # name: # type: sq_string # elements: 1 # length: 37 GeneralizedLinearModel.PredictorNames # name: # type: sq_string # elements: 1 # length: 263 GeneralizedLinearModel: property PredictorNames Names of the predictor variables A cell array of character vectors naming the predictors in the order the data lists them. A predictor matrix gives them the names 'x1', 'x2', and so on. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Names of the predictor variables # name: # type: sq_string # elements: 1 # length: 32 GeneralizedLinearModel.Residuals # name: # type: sq_string # elements: 1 # length: 569 GeneralizedLinearModel: property Residuals Residuals for the fitted model A table with one row per input observation and five columns: Raw - observed minus fitted, on the response scale LinearPredictor - the working residual, on the link scale Pearson - raw residuals divided by the estimated standard deviation of the observation Anscombe - the transform that makes the residuals as nearly normal as the family allows Deviance - the signed square root of each observation's contribution to Deviance Rows not used in the fit contain NaN. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 Residuals for the fitted model # name: # type: sq_string # elements: 1 # length: 35 GeneralizedLinearModel.ResponseName # name: # type: sq_string # elements: 1 # length: 262 GeneralizedLinearModel: property ResponseName Name of the response variable A character vector. It is taken from the table column, the 'ResponseVar' or 'VarNames' argument, or the formula, and defaults to 'y' for a predictor matrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 31 GeneralizedLinearModel.Rsquared # name: # type: sq_string # elements: 1 # length: 454 GeneralizedLinearModel: property Rsquared Measures of fit A structure with five fields: Ordinary and Adjusted, computed from the sums of squares on the response scale; Deviance, one less the ratio of the model's deviance to the null model's; LLR, the same ratio taken over log-likelihoods; and AdjGeneralized, the Nagelkerke measure, which rescales the generalized R^2 by its own attainable maximum so that it can reach one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 15 Measures of fit # name: # type: sq_string # elements: 1 # length: 26 GeneralizedLinearModel.SSE # name: # type: sq_string # elements: 1 # length: 289 GeneralizedLinearModel: property SSE Error sum of squares A nonnegative scalar, the weighted sum of squared raw residuals on the response scale. For a generalized linear model this is a descriptive quantity rather than the fitted criterion, which is Deviance. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Error sum of squares # name: # type: sq_string # elements: 1 # length: 26 GeneralizedLinearModel.SSR # name: # type: sq_string # elements: 1 # length: 242 GeneralizedLinearModel: property SSR Regression sum of squares A nonnegative scalar, the weighted sum of squared differences between the fitted values and the weighted mean of the response, on the response scale. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Regression sum of squares # name: # type: sq_string # elements: 1 # length: 26 GeneralizedLinearModel.SST # name: # type: sq_string # elements: 1 # length: 288 GeneralizedLinearModel: property SST Total sum of squares A nonnegative scalar, the weighted sum of squared differences between the response and its weighted mean. Unlike a linear model, a generalized linear model does not in general satisfy SST = SSE + SSR. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Total sum of squares # name: # type: sq_string # elements: 1 # length: 28 GeneralizedLinearModel.Steps # name: # type: sq_string # elements: 1 # length: 1243 GeneralizedLinearModel: property Steps Stepwise fitting information A structure recording the term-selection trace, populated whenever the model was fit by stepwiseglm and [] otherwise. It has seven fields: Field Contents Start a LinearFormula for the model the search started from. Lower a LinearFormula for the smallest model considered; its terms are never removed. Upper a LinearFormula for the largest model considered. Criterion the selection criterion, such as 'deviance_chi2'. PEnter the threshold a term must beat to enter, empty unless one was given. PRemove the threshold above which a term leaves, empty unless one was given. History a table with one row per step. History always carries Action ('Start', 'Add', or 'Remove'), TermName, Terms (the terms matrix after the step, over the model's variables), DF (the coefficient count after the step), and delDF (the change in it, negative for a removal). The remaining columns follow the criterion: Deviance, then Chi2Stat or FStat, then PValue under 'Deviance'; FStat and pValue under 'sse'; and a single column named AIC or BIC holding the criterion's value after the step otherwise. The first row is the starting model, named by its right-hand side. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Stepwise fitting information # name: # type: sq_string # elements: 1 # length: 35 GeneralizedLinearModel.VariableInfo # name: # type: sq_string # elements: 1 # length: 389 GeneralizedLinearModel: property VariableInfo Per-variable information A table with one row per variable, named by it, and four columns: Class, the class of the data column; Range, its two-element range or, for a categorical, the list of its levels; InModel, true where the variable appears in a term; and IsCategorical, true where it was coded as indicators. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Per-variable information # name: # type: sq_string # elements: 1 # length: 36 GeneralizedLinearModel.VariableNames # name: # type: sq_string # elements: 1 # length: 237 GeneralizedLinearModel: property VariableNames Names of all the variables A cell array of character vectors naming every variable the model was given, the response included, in the order the data lists them. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Names of all the variables # name: # type: sq_string # elements: 1 # length: 32 GeneralizedLinearModel.Variables # name: # type: sq_string # elements: 1 # length: 308 GeneralizedLinearModel: property Variables The data the model was built from A table holding every variable, the response included, with one row per input observation. A model fitted from a predictor matrix gets a table assembled from it, so this property is a table either way. This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 The data the model was built from # name: # type: sq_string # elements: 1 # length: 29 GeneralizedLinearModel.coefCI # name: # type: sq_string # elements: 1 # length: 315 GeneralizedLinearModel: ci = coefCI (mdl) GeneralizedLinearModel: ci = coefCI (mdl, alpha) Confidence intervals for the coefficient estimates. ci is a k-by-2 matrix of 100 (1 - alpha)% intervals (default alpha = 0.05). The t distribution is used when the dispersion was estimated, the normal distribution otherwise. # name: # type: sq_string # elements: 1 # length: 51 Confidence intervals for the coefficient estimates. # name: # type: sq_string # elements: 1 # length: 31 GeneralizedLinearModel.coefTest # name: # type: sq_string # elements: 1 # length: 412 GeneralizedLinearModel: p = coefTest (mdl) GeneralizedLinearModel: [p, stat, df] = coefTest (mdl, H) Wald test of the linear hypothesis H b = 0 on the coefficients. H is an m-by-k contrast matrix; when omitted it tests that all coefficients except the intercept are zero (the model versus the constant model). Returns the p-value p, and optionally the test statistic stat and its numerator degrees of freedom df. # name: # type: sq_string # elements: 1 # length: 63 Wald test of the linear hypothesis H b = 0 on the coefficients. # name: # type: sq_string # elements: 1 # length: 35 GeneralizedLinearModel.devianceTest # name: # type: sq_string # elements: 1 # length: 574 GeneralizedLinearModel: tbl = devianceTest (mdl) Likelihood-ratio (deviance) test of the fitted model against the intercept-only model. Returns a table with the deviance, degrees of freedom, and p-value of each model, the last row giving the test statistic and its p-value. Where the dispersion is fixed, the statistic is the drop in deviance, chi2Stat, referred to the chi-square distribution; where it is estimated, it is the drop per degree of freedom over the dispersion, FStat, referred to the F-distribution. Each row is named by the formula of the model it describes. # name: # type: sq_string # elements: 1 # length: 86 Likelihood-ratio (deviance) test of the fitted model against the intercept-only model. # name: # type: sq_string # elements: 1 # length: 28 GeneralizedLinearModel.feval # name: # type: sq_string # elements: 1 # length: 245 GeneralizedLinearModel: yhat = feval (mdl, x1, x2, ...) Predict the response by passing each predictor as a separate argument (a scalar or column vector), returning point predictions on the mean scale. Equivalent to predict (mdl, [x1, x2, ...]). # name: # type: sq_string # elements: 1 # length: 145 Predict the response by passing each predictor as a separate argument (a scalar or column vector), returning point predictions on the mean scale. # name: # type: sq_string # elements: 1 # length: 32 GeneralizedLinearModel.plotAdded # name: # type: sq_string # elements: 1 # length: 277 GeneralizedLinearModel: h = plotAdded (mdl, var) Added-variable (partial-regression) plot for the predictor var (a name or index): the response residuals from the model without var against the residuals of var regressed on the remaining predictors. Returns the graphics handle. # name: # type: sq_string # elements: 1 # length: 199 Added-variable (partial-regression) plot for the predictor var (a name or index): the response residuals from the model without var against the residuals of var regressed on the remaining predictors. # name: # type: sq_string # elements: 1 # length: 43 GeneralizedLinearModel.plotAdjustedResponse # name: # type: sq_string # elements: 1 # length: 288 GeneralizedLinearModel: h = plotAdjustedResponse (mdl, var) Adjusted-response plot for the predictor var (a name or index): the fitted mean response as var sweeps its observed range with other predictors held at their means, overlaid on the partial residuals. Returns the graphics handle. # name: # type: sq_string # elements: 1 # length: 199 Adjusted-response plot for the predictor var (a name or index): the fitted mean response as var sweeps its observed range with other predictors held at their means, overlaid on the partial residuals. # name: # type: sq_string # elements: 1 # length: 38 GeneralizedLinearModel.plotDiagnostics # name: # type: sq_string # elements: 1 # length: 275 GeneralizedLinearModel: h = plotDiagnostics (mdl) GeneralizedLinearModel: h = plotDiagnostics (mdl, plottype) Plot observation diagnostics. plottype is 'leverage' (default) or 'cookd' (Cook's distance). A reference line marks the usual threshold. Returns the graphics handle. # name: # type: sq_string # elements: 1 # length: 29 Plot observation diagnostics. # name: # type: sq_string # elements: 1 # length: 34 GeneralizedLinearModel.plotEffects # name: # type: sq_string # elements: 1 # length: 238 GeneralizedLinearModel: h = plotEffects (mdl) Main-effects plot: for each predictor, the change in the fitted mean response as that predictor sweeps its observed range while the others are held at their means. Returns the graphics handle. # name: # type: sq_string # elements: 1 # length: 163 Main-effects plot: for each predictor, the change in the fitted mean response as that predictor sweeps its observed range while the others are held at their means. # name: # type: sq_string # elements: 1 # length: 36 GeneralizedLinearModel.plotResiduals # name: # type: sq_string # elements: 1 # length: 414 GeneralizedLinearModel: h = plotResiduals (mdl) GeneralizedLinearModel: h = plotResiduals (mdl, plottype) GeneralizedLinearModel: h = plotResiduals (..., 'ResidualType', rt) Plot the model residuals. plottype is one of 'histogram' (default), 'caseorder', 'fitted', 'lagged', or 'probability'. 'ResidualType' picks the residual column ('Raw' default, 'Pearson', 'Deviance', 'Anscombe'). Returns the graphics handle. # name: # type: sq_string # elements: 1 # length: 25 Plot the model residuals. # name: # type: sq_string # elements: 1 # length: 30 GeneralizedLinearModel.predict # name: # type: sq_string # elements: 1 # length: 459 GeneralizedLinearModel: yhat = predict (mdl, Xnew) GeneralizedLinearModel: [yhat, yci] = predict (mdl, Xnew) GeneralizedLinearModel: [...] = predict (..., Name, Value) Predict the response of the model mdl at the new predictor data Xnew (a numeric matrix or a table). Predictions are on the mean (response) scale. With two outputs, yci is an m-by-2 matrix of confidence intervals. The 'Alpha' pair sets the confidence level to 100 (1 - Alpha)% (default 0.05). # name: # type: sq_string # elements: 1 # length: 99 Predict the response of the model mdl at the new predictor data Xnew (a numeric matrix or a table). # name: # type: sq_string # elements: 1 # length: 29 GeneralizedLinearModel.random # name: # type: sq_string # elements: 1 # length: 319 GeneralizedLinearModel: ysim = random (mdl) GeneralizedLinearModel: ysim = random (mdl, Xnew) Simulate responses from the fitted model. With one argument the fitted values are used; otherwise the mean is predicted at the new predictor data Xnew. A random draw from the response distribution about that mean is returned. # name: # type: sq_string # elements: 1 # length: 41 Simulate responses from the fitted model. # name: # type: sq_string # elements: 1 # length: 13 LinearFormula # name: # type: sq_string # elements: 1 # length: 1089 statistics: LinearFormula Model formula of a linear or generalized linear regression. A LinearFormula object describes the terms of a fitted model: which variables the model draws on, how they combine into terms, and how the whole thing reads back as a formula. It is the class of the Formula property of a LinearModel and of a GeneralizedLinearModel, and is normally obtained from a fitted model rather than built directly. The object is defined by its terms matrix and the names of the variables that matrix is written over; every other property is derived from those two. Each row of Terms is one term of the model and each column is one variable, the entry giving the power that variable carries in that term. An all-zero row is the intercept. The response variable occupies a column of its own, which is always zero. Converting the object with char renders the whole formula, response included, as "y ~ 1 + x1 + x2"; the LinearPredictor property holds the right-hand side on its own. For a generalized linear model the response carries its link function, as in "logit(y) ~ 1 + x1". # name: # type: sq_string # elements: 1 # length: 59 Model formula of a linear or generalized linear regression. # name: # type: sq_string # elements: 1 # length: 27 LinearFormula.FunctionCalls # name: # type: sq_string # elements: 1 # length: 201 LinearFormula: property FunctionCalls Functions called from within the formula A cell array of character vectors, empty unless the formula applies a function to a variable. This property is read-only. # name: # type: sq_string # elements: 1 # length: 40 Functions called from within the formula # name: # type: sq_string # elements: 1 # length: 26 LinearFormula.HasIntercept # name: # type: sq_string # elements: 1 # length: 161 LinearFormula: property HasIntercept Whether the model carries an intercept A logical scalar, true when Terms holds an all-zero row. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Whether the model carries an intercept # name: # type: sq_string # elements: 1 # length: 21 LinearFormula.InModel # name: # type: sq_string # elements: 1 # length: 246 LinearFormula: property InModel Which variables take part in the model A logical row vector with one element per variable of VariableNames, true where that variable appears in at least one term. The response is false. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Which variables take part in the model # name: # type: sq_string # elements: 1 # length: 27 LinearFormula.LinearFormula # name: # type: sq_string # elements: 1 # length: 1147 LinearFormula: obj = LinearFormula () LinearFormula: obj = LinearFormula (terms, varnames) LinearFormula: obj = LinearFormula (terms, varnames, name, value, ...) Create a model formula from a terms matrix. obj = LinearFormula () returns an empty formula. obj = LinearFormula (terms, varnames) builds a formula whose terms matrix is terms and whose variables are named by the cell array of character vectors varnames. terms must have one column per element of varnames; each row is one term and each entry the power its variable carries in that term. An all-zero row is the intercept. The remaining properties are derived from these two arguments, except those given as name-value pairs: name value "ResponseName" A character vector naming the response variable. It must be one of varnames. "Link" The link function applied to the response, as a name, a numeric exponent, or a structure of function handles. It defaults to "identity". "ModelFun" A function handle computing the linear predictor from the coefficients and the design matrix. "FunctionCalls" A cell array of character vectors naming functions the formula applies to its variables. # name: # type: sq_string # elements: 1 # length: 43 Create a model formula from a terms matrix. # name: # type: sq_string # elements: 1 # length: 29 LinearFormula.LinearPredictor # name: # type: sq_string # elements: 1 # length: 306 LinearFormula: property LinearPredictor Right-hand side of the formula A character vector rendering the model's terms, such as "1 + x1 + x2". A pair of variables appearing both on their own and as an interaction is written as a product, so that "x1 + x2 + x1:x2" reads "x1*x2". This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 Right-hand side of the formula # name: # type: sq_string # elements: 1 # length: 18 LinearFormula.Link # name: # type: sq_string # elements: 1 # length: 249 LinearFormula: property Link Link function applied to the response The link of a generalized linear model, given as its name, its numeric exponent, or a structure of function handles. It is "identity" for a linear model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Link function applied to the response # name: # type: sq_string # elements: 1 # length: 22 LinearFormula.ModelFun # name: # type: sq_string # elements: 1 # length: 172 LinearFormula: property ModelFun Function computing the linear predictor A function handle taking the coefficient vector and the design matrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Function computing the linear predictor # name: # type: sq_string # elements: 1 # length: 25 LinearFormula.NPredictors # name: # type: sq_string # elements: 1 # length: 163 LinearFormula: property NPredictors Number of variables the model uses A non-negative integer, the number of true elements of InModel. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Number of variables the model uses # name: # type: sq_string # elements: 1 # length: 20 LinearFormula.NTerms # name: # type: sq_string # elements: 1 # length: 141 LinearFormula: property NTerms Number of terms in the model A non-negative integer, the number of rows of Terms. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Number of terms in the model # name: # type: sq_string # elements: 1 # length: 19 LinearFormula.NVars # name: # type: sq_string # elements: 1 # length: 166 LinearFormula: property NVars Number of variables available to the model A non-negative integer, the number of elements of VariableNames. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Number of variables available to the model # name: # type: sq_string # elements: 1 # length: 28 LinearFormula.PredictorNames # name: # type: sq_string # elements: 1 # length: 222 LinearFormula: property PredictorNames Names of the variables the model actually uses A cell array of character vectors holding those elements of VariableNames that appear in at least one term. This property is read-only. # name: # type: sq_string # elements: 1 # length: 46 Names of the variables the model actually uses # name: # type: sq_string # elements: 1 # length: 26 LinearFormula.ResponseName # name: # type: sq_string # elements: 1 # length: 172 LinearFormula: property ResponseName Name of the response variable A character vector naming the variable on the left-hand side of the formula. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 23 LinearFormula.TermNames # name: # type: sq_string # elements: 1 # length: 466 LinearFormula: property TermNames Name of each term of the model A column cell array of character vectors, one per row of Terms, naming the term over the model's variables: the intercept is "(Intercept)", an interaction joins its factors with a colon, and a power is written with a caret. A categorical variable contributes one term under its own name however many indicator columns it expands to, so these are not the coefficient names. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 Name of each term of the model # name: # type: sq_string # elements: 1 # length: 19 LinearFormula.Terms # name: # type: sq_string # elements: 1 # length: 267 LinearFormula: property Terms Terms matrix of the model A numeric matrix with one row per term and one column per variable of VariableNames, each entry giving the power that variable carries in that term. An all-zero row is the intercept. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Terms matrix of the model # name: # type: sq_string # elements: 1 # length: 27 LinearFormula.VariableNames # name: # type: sq_string # elements: 1 # length: 276 LinearFormula: property VariableNames Names of all variables available to the model A cell array of character vectors naming every variable the model was given, whether or not it is used, in the order the data lists them. The response is included. This property is read-only. # name: # type: sq_string # elements: 1 # length: 45 Names of all variables available to the model # name: # type: sq_string # elements: 1 # length: 18 LinearFormula.char # name: # type: sq_string # elements: 1 # length: 263 LinearFormula: str = char (obj) Render a model formula as a character vector. str = char (obj) returns the whole formula, response included, as in "y ~ 1 + x1 + x2". The response carries the link function of a generalized linear model, as in "logit(y) ~ 1 + x1". # name: # type: sq_string # elements: 1 # length: 45 Render a model formula as a character vector. # name: # type: sq_string # elements: 1 # length: 20 LinearFormula.string # name: # type: sq_string # elements: 1 # length: 131 LinearFormula: str = string (obj) Render a model formula as a string scalar. str = string (obj) is the string counterpart of char. # name: # type: sq_string # elements: 1 # length: 42 Render a model formula as a string scalar. # name: # type: sq_string # elements: 1 # length: 16 LinearMixedModel # name: # type: sq_string # elements: 1 # length: 697 statistics: LinearMixedModel Linear mixed-effects model fitted to data. A LinearMixedModel object represents a fitted linear mixed-effects model $$ y = X\beta + Zb + \varepsilon, $$ with fixed effects beta, random effects b distributed as N(0, Psi), and independent errors N(0, sigma2). Objects are created with fitlmematrix (from design matrices). The estimated fixed effects and their statistics are available through the Coefficients table; the covariance parameters through covarianceParameters; the random-effect BLUPs through randomEffects; and predictions, residuals, and hypothesis tests through the predict, residuals, anova, coefTest, and coefCI methods. See also: fitlmematrix, fitlm # name: # type: sq_string # elements: 1 # length: 42 Linear mixed-effects model fitted to data. # name: # type: sq_string # elements: 1 # length: 38 LinearMixedModel.CoefficientCovariance # name: # type: sq_string # elements: 1 # length: 261 LinearMixedModel: property CoefficientCovariance Covariance of the fixed-effects estimates A square numeric matrix, one row and column per fixed-effects coefficient, holding the estimated covariance of the estimates in Coefficients. This property is read-only. # name: # type: sq_string # elements: 1 # length: 41 Covariance of the fixed-effects estimates # name: # type: sq_string # elements: 1 # length: 33 LinearMixedModel.CoefficientNames # name: # type: sq_string # elements: 1 # length: 191 LinearMixedModel: property CoefficientNames Names of the fixed-effects coefficients A cell array of character vectors with one name per fixed-effects coefficient. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Names of the fixed-effects coefficients # name: # type: sq_string # elements: 1 # length: 29 LinearMixedModel.Coefficients # name: # type: sq_string # elements: 1 # length: 325 LinearMixedModel: property Coefficients Fixed-effects estimates and their statistics A table with one row per fixed-effects coefficient, its row names taken from CoefficientNames, and the variables Estimate, SE, tStat, DF, pValue, Lower and Upper. Lower and Upper bound a 95% confidence interval. This property is read-only. # name: # type: sq_string # elements: 1 # length: 44 Fixed-effects estimates and their statistics # name: # type: sq_string # elements: 1 # length: 20 LinearMixedModel.DFE # name: # type: sq_string # elements: 1 # length: 148 LinearMixedModel: property DFE Residual degrees of freedom A nonnegative integer, NumObservations less NumCoefficients. This property is read-only. # name: # type: sq_string # elements: 1 # length: 27 Residual degrees of freedom # name: # type: sq_string # elements: 1 # length: 26 LinearMixedModel.FitMethod # name: # type: sq_string # elements: 1 # length: 224 LinearMixedModel: property FitMethod Estimation method A character vector, either 'ML' for maximum likelihood or 'REML' for restricted maximum likelihood, naming the method that fitted the model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 17 Estimation method # name: # type: sq_string # elements: 1 # length: 24 LinearMixedModel.Formula # name: # type: sq_string # elements: 1 # length: 194 LinearMixedModel: property Formula Model formula A character vector describing the model. It is empty for a fit built from design matrices, which carries no formula. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Model formula # name: # type: sq_string # elements: 1 # length: 33 LinearMixedModel.LinearMixedModel # name: # type: sq_string # elements: 1 # length: 219 LinearMixedModel: lme = LinearMixedModel (info) Construct a LinearMixedModel from a fitted-model info struct. This constructor is used internally by fitlmematrix; call that function rather than the constructor directly. # name: # type: sq_string # elements: 1 # length: 61 Construct a LinearMixedModel from a fitted-model info struct. # name: # type: sq_string # elements: 1 # length: 30 LinearMixedModel.LogLikelihood # name: # type: sq_string # elements: 1 # length: 213 LinearMixedModel: property LogLikelihood Log-likelihood of the fitted model A scalar, the maximised log-likelihood, or the maximised restricted log-likelihood when FitMethod is 'REML'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Log-likelihood of the fitted model # name: # type: sq_string # elements: 1 # length: 20 LinearMixedModel.MSE # name: # type: sq_string # elements: 1 # length: 141 LinearMixedModel: property MSE Residual variance estimate A positive scalar, the estimate of the error variance. This property is read-only. # name: # type: sq_string # elements: 1 # length: 26 Residual variance estimate # name: # type: sq_string # elements: 1 # length: 31 LinearMixedModel.ModelCriterion # name: # type: sq_string # elements: 1 # length: 294 LinearMixedModel: property ModelCriterion Information criteria A scalar structure with the fields AIC, BIC, LogLikelihood and Deviance. The parameter count behind AIC and BIC holds the fixed-effects coefficients, the covariance parameters and the residual variance. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Information criteria # name: # type: sq_string # elements: 1 # length: 32 LinearMixedModel.NumCoefficients # name: # type: sq_string # elements: 1 # length: 181 LinearMixedModel: property NumCoefficients Number of fixed-effects coefficients A positive integer counting the fixed-effects coefficients of the model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Number of fixed-effects coefficients # name: # type: sq_string # elements: 1 # length: 41 LinearMixedModel.NumEstimatedCoefficients # name: # type: sq_string # elements: 1 # length: 293 LinearMixedModel: property NumEstimatedCoefficients Number of estimated fixed-effects coefficients A positive integer counting the fixed-effects coefficients estimated from the data. Every coefficient the model carries is estimated, so this equals NumCoefficients. This property is read-only. # name: # type: sq_string # elements: 1 # length: 46 Number of estimated fixed-effects coefficients # name: # type: sq_string # elements: 1 # length: 32 LinearMixedModel.NumObservations # name: # type: sq_string # elements: 1 # length: 157 LinearMixedModel: property NumObservations Number of observations A positive integer counting the observations used for the fit. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 29 LinearMixedModel.ResponseName # name: # type: sq_string # elements: 1 # length: 188 LinearMixedModel: property ResponseName Name of the response variable A character vector naming the response. It is empty for a fit built from design matrices. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 25 LinearMixedModel.Rsquared # name: # type: sq_string # elements: 1 # length: 264 LinearMixedModel: property Rsquared Coefficient of determination A scalar structure with the fields Ordinary and Adjusted. Ordinary is one less the ratio of SSE to SST, and Adjusted corrects that ratio for the error degrees of freedom. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Coefficient of determination # name: # type: sq_string # elements: 1 # length: 20 LinearMixedModel.SSE # name: # type: sq_string # elements: 1 # length: 183 LinearMixedModel: property SSE Error sum of squares A nonnegative scalar, the sum of the squared differences between the response and the conditional fit. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Error sum of squares # name: # type: sq_string # elements: 1 # length: 20 LinearMixedModel.SSR # name: # type: sq_string # elements: 1 # length: 196 LinearMixedModel: property SSR Regression sum of squares A nonnegative scalar, the sum of the squared deviations of the conditional fit about the mean of the response. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Regression sum of squares # name: # type: sq_string # elements: 1 # length: 20 LinearMixedModel.SST # name: # type: sq_string # elements: 1 # length: 116 LinearMixedModel: property SST Total sum of squares A nonnegative scalar, SSE plus SSR. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Total sum of squares # name: # type: sq_string # elements: 1 # length: 22 LinearMixedModel.anova # name: # type: sq_string # elements: 1 # length: 289 LinearMixedModel: tbl = anova (lme) LinearMixedModel: tbl = anova (lme, "DFMethod", method) Analysis-of-variance table of F-tests for the fixed-effects terms. Each row tests one coefficient. method selects the denominator degrees of freedom: "Residual" (default, n - p) or "Satterthwaite". # name: # type: sq_string # elements: 1 # length: 66 Analysis-of-variance table of F-tests for the fixed-effects terms. # name: # type: sq_string # elements: 1 # length: 23 LinearMixedModel.coefCI # name: # type: sq_string # elements: 1 # length: 235 LinearMixedModel: ci = coefCI (lme) LinearMixedModel: ci = coefCI (lme, alpha) Confidence intervals for the fixed-effects coefficients at level 1 - alpha (default alpha = 0.05). Row j holds the lower and upper bounds for coefficient j. # name: # type: sq_string # elements: 1 # length: 98 Confidence intervals for the fixed-effects coefficients at level 1 - alpha (default alpha = 0.05). # name: # type: sq_string # elements: 1 # length: 25 LinearMixedModel.coefTest # name: # type: sq_string # elements: 1 # length: 423 LinearMixedModel: p = coefTest (lme) LinearMixedModel: p = coefTest (lme, H) LinearMixedModel: [p, F, df1, df2] = coefTest (...) Test the linear hypothesis H*beta = 0 with an F-test. With no H, tests that all non-intercept coefficients are zero (intercept is taken to be the first coefficient). Returns the p-value and, optionally, the F-statistic and its numerator and denominator degrees of freedom (denominator = n - p). # name: # type: sq_string # elements: 1 # length: 53 Test the linear hypothesis H*beta = 0 with an F-test. # name: # type: sq_string # elements: 1 # length: 37 LinearMixedModel.covarianceParameters # name: # type: sq_string # elements: 1 # length: 186 LinearMixedModel: [psi, mse] = covarianceParameters (lme) Return the estimated random-effects covariance matrices psi (a cell array, one per grouping term) and the residual variance mse. # name: # type: sq_string # elements: 1 # length: 128 Return the estimated random-effects covariance matrices psi (a cell array, one per grouping term) and the residual variance mse. # name: # type: sq_string # elements: 1 # length: 29 LinearMixedModel.designMatrix # name: # type: sq_string # elements: 1 # length: 212 LinearMixedModel: D = designMatrix (lme, type) Return the fixed-effects design matrix (type = "Fixed", default) as a full matrix, or the expanded random-effects design matrix (type = "Random") as a sparse matrix. # name: # type: sq_string # elements: 1 # length: 165 Return the fixed-effects design matrix (type = "Fixed", default) as a full matrix, or the expanded random-effects design matrix (type = "Random") as a sparse matrix. # name: # type: sq_string # elements: 1 # length: 23 LinearMixedModel.fitted # name: # type: sq_string # elements: 1 # length: 250 LinearMixedModel: yf = fitted (lme) LinearMixedModel: yf = fitted (lme, "Conditional", tf) Return the fitted values. With "Conditional" true (the default) the fit includes the random effects (X*beta + Z*b); with false it is the marginal fit (X*beta). # name: # type: sq_string # elements: 1 # length: 25 Return the fitted values. # name: # type: sq_string # elements: 1 # length: 29 LinearMixedModel.fixedEffects # name: # type: sq_string # elements: 1 # length: 179 LinearMixedModel: beta = fixedEffects (lme) LinearMixedModel: [beta, names] = fixedEffects (lme) Return the estimated fixed-effects coefficients beta and, optionally, their names. # name: # type: sq_string # elements: 1 # length: 82 Return the estimated fixed-effects coefficients beta and, optionally, their names. # name: # type: sq_string # elements: 1 # length: 24 LinearMixedModel.predict # name: # type: sq_string # elements: 1 # length: 568 LinearMixedModel: ypred = predict (lme, Xnew, Znew, Gnew) LinearMixedModel: [ypred, yci] = predict (...) LinearMixedModel: [...] = predict (..., name, value) Predict the response at new fixed-effects design Xnew, random-effects design Znew, and grouping Gnew. By default the prediction is conditional on the estimated random effects (levels of Gnew not seen in the fit fall back to the marginal prediction). With "Conditional" false the marginal prediction Xnew*beta is returned. The second output yci gives 95% (or "Alpha") confidence intervals for the marginal mean. # name: # type: sq_string # elements: 1 # length: 101 Predict the response at new fixed-effects design Xnew, random-effects design Znew, and grouping Gnew. # name: # type: sq_string # elements: 1 # length: 30 LinearMixedModel.randomEffects # name: # type: sq_string # elements: 1 # length: 230 LinearMixedModel: b = randomEffects (lme) LinearMixedModel: [b, names] = randomEffects (lme) Return the best linear unbiased predictors (BLUPs) of the random effects b and, optionally, a cell array of group:level:predictor labels. # name: # type: sq_string # elements: 1 # length: 137 Return the best linear unbiased predictors (BLUPs) of the random effects b and, optionally, a cell array of group:level:predictor labels. # name: # type: sq_string # elements: 1 # length: 26 LinearMixedModel.residuals # name: # type: sq_string # elements: 1 # length: 295 LinearMixedModel: r = residuals (lme) LinearMixedModel: r = residuals (lme, "ResidualType", type) Return the conditional residuals y - (X*beta + Z*b). type is "Raw" (default), "Pearson" (raw divided by sqrt (sigma2)), or "Standardized" (raw divided by the square root of its estimated variance). # name: # type: sq_string # elements: 1 # length: 52 Return the conditional residuals y - (X*beta + Z*b). # name: # type: sq_string # elements: 1 # length: 11 LinearModel # name: # type: sq_string # elements: 1 # length: 6935 statistics: LinearModel Linear regression model The LinearModel class represents a least-squares (or, optionally, robust) linear regression fit of a response variable to one or more predictor variables. A LinearModel object is returned by the fitlm function and holds everything about the fit in one place: the fitted coefficients, the data and specification used to produce them, and the diagnostics needed to assess the quality of the fit. The properties of a LinearModel object fall into four groups: Group Properties Coefficient estimates Coefficients (a table of estimates, standard errors, t-statistics, and p-values for each term), CoefficientCovariance, CoefficientNames, and the coefficient counts NumCoefficients and NumEstimatedCoefficients. Summary statistics of the fit DFE, Fitted, Residuals (raw, Pearson, Studentized, and standardized), Diagnostics (leverage, Cook's distance, and other per-observation influence measures), MSE, RMSE, Rsquared (ordinary and adjusted), SSE, SSR, SST, LogLikelihood, ModelCriterion (AIC, BIC, etc.), and ModelFitVsNullModel (the F-test of the fitted model against an intercept-only model). Fitting method information Robust, which records the weighting function and tuning constant used when the model is fit by robust regression, and is empty for an ordinary least squares fit, and Steps, which records the stepwise fitting information whenever the model was fit using stepwise regression, and is currently always empty. Input data properties Formula, NumObservations, NumPredictors, NumVariables, ObservationInfo (which observations were used, excluded, missing, or weighted), ObservationNames, PredictorNames, ResponseName, VariableInfo, VariableNames, and Variables. A categorical predictor expands to indicator columns, one per level bar the reference level, which the intercept carries. When the model has no intercept, the first categorical predictor is given an indicator for every one of its levels instead, so that its coefficients are the group means; any further categorical predictor stays reference coded, which keeps the design full rank. This differs from MATLAB, which omits the reference level whether or not an intercept is present and so cannot fit the reference group at all -- for a three-level grouping variable g, MATLAB fits y ~ g - 1 with two coefficients, predicts exactly 0 for every observation in the omitted group, and reports a negative R^2. This implementation returns three coefficients, one per group. A LinearModel object supports categorical predictors, which are automatically encoded internally as indicator (dummy) variables, observation weights for a weighted least squares fit, excluding specific observations from the fit, and robust regression using iteratively reweighted least squares. Once fitted, the following methods are available on a LinearModel object: Method Description predict Predict responses at new predictor values given in a matrix or table, or reproduce the training fitted values when called with no new data. Can also return pointwise or simultaneous confidence or prediction intervals alongside the point predictions. feval Predict responses given predictors as separate scalar or vector arguments (one per predictor variable) instead of a single matrix, so a LinearModel object can be evaluated the same way as a plain function handle. Returns point predictions only. random Simulate new response values at new predictor locations by adding independent Gaussian noise, drawn from the estimated error variance MSE, to the fitted response. coefCI Return Wald confidence intervals for every fitted coefficient at a chosen significance level (default 0.05). coefTest Test a linear hypothesis on the fitted coefficients. With no arguments, tests the overall model F-test that all non-intercept coefficients are zero; a custom hypothesis can be given as a contrast matrix and, if needed, right-hand-side values. Returns the p-value, and optionally the F-statistic and its numerator degrees of freedom. dwtest Durbin-Watson test for first-order autocorrelation among the model residuals, with a choice of exact or approximate p-value computation and a one- or two-sided alternative. addTerms Return a new, refitted LinearModel with terms added to the current model specification, given as a Wilkinson formula fragment or a terms matrix. Weights, excluded rows, and categorical encodings carry over automatically; the original model object is left unmodified. removeTerms Return a new, refitted LinearModel with terms removed from the current model specification, given as a Wilkinson formula fragment or a terms matrix. Weights, excluded rows, and categorical encodings carry over automatically; the original model object is left unmodified. plotResiduals Plot the model residuals. Default is a probability density histogram; other supported plot types are 'fitted', 'caseorder', 'lagged', 'probability', and 'observed'. plotDiagnostics Plot per-observation influence diagnostics. Default is leverage by observation row number; other supported plot types are 'cookd', 'covratio', 'dfbetas', 'dffits', 's2_i', and 'contour' (standardized residuals against leverage with Cook's distance contours). plotEffects Plot the estimated main effect and 95% confidence interval of each predictor, evaluated between its observed minimum and maximum with all other predictors held at their observed means. plotAdjustedResponse Plot the fitted response against a single predictor, with the other predictors averaged out by averaging the fitted values over the observations used in the fit. plotAdded Plot the incremental effect of one or more terms on the response, after removing the effects of all other terms, along with the fitted line and its 95% confidence bounds. plot Plot a default view of the model. Creates an added variable plot for the whole model when more than one predictor is included, a scatter plot of the data with a fitted curve and 95% confidence bounds when exactly one predictor is included, or a histogram of the residuals when no predictors are included. plotInteraction Plot the main and conditional effects of two predictors, or the adjusted response as a function of one predictor for several fixed values of the other, to visualize whether the two predictors interact. compact Return a CompactLinearModel that discards the training data and per-observation diagnostics while retaining the coefficient estimates and fit statistics needed for prediction and inference. anova Analysis of variance for the fitted model, reporting either the per-term breakdown of sums of squares or a summary table of the model against the total and residual variation. step Improve the fitted model by one or more steps of stepwise term selection, returning a new, refitted LinearModel without modifying the original. Create a LinearModel object by using the fitlm function or the class constructor directly. See also: fitlm # name: # type: sq_string # elements: 1 # length: 23 Linear regression model # name: # type: sq_string # elements: 1 # length: 33 LinearModel.CoefficientCovariance # name: # type: sq_string # elements: 1 # length: 278 LinearModel: property CoefficientCovariance Covariance matrix of coefficient estimates A p-by-p numeric matrix of covariance values for the coefficient estimates, where p is the number of coefficients in the fitted model as given by NumCoefficients. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Covariance matrix of coefficient estimates # name: # type: sq_string # elements: 1 # length: 28 LinearModel.CoefficientNames # name: # type: sq_string # elements: 1 # length: 215 LinearModel: property CoefficientNames Coefficient names A cell array of character vectors, each containing the name of the corresponding model term (e.g., '(Intercept)', 'x1', 'x1:x2'). This property is read-only. # name: # type: sq_string # elements: 1 # length: 17 Coefficient names # name: # type: sq_string # elements: 1 # length: 24 LinearModel.Coefficients # name: # type: sq_string # elements: 1 # length: 404 LinearModel: property Coefficients Coefficient values A table with one row for each coefficient and four columns: Estimate - estimated coefficient value SE - standard error of the estimate tStat - t-statistic for a two-sided test pValue - p-value for the t-statistic Coefficients that are dropped due to rank deficiency have Estimate = 0, SE = 0, tStat = NaN, pValue = NaN. This property is read-only. # name: # type: sq_string # elements: 1 # length: 18 Coefficient values # name: # type: sq_string # elements: 1 # length: 15 LinearModel.DFE # name: # type: sq_string # elements: 1 # length: 232 LinearModel: property DFE Degrees of freedom for error A positive integer equal to the number of observations minus the number of estimated coefficients: DFE = NumObservations - NumEstimatedCoefficients. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Degrees of freedom for error # name: # type: sq_string # elements: 1 # length: 23 LinearModel.Diagnostics # name: # type: sq_string # elements: 1 # length: 770 LinearModel: property Diagnostics Observation diagnostics A table with one row per observation and seven columns: Leverage - diagonal of the hat matrix H CooksDistance - Cook's distance, a measure of scaled change in fitted values Dffits - delete-1 scaled differences in fitted values S2_i - delete-1 residual variance estimate CovRatio - ratio of the determinant of the coefficient covariance matrix with and without each observation Dfbetas - n-by-p matrix of scaled changes in coefficient estimates when each observation is deleted in turn HatMatrix - n-by-n projection matrix such that Fitted = HatMatrix * y Rows not used in fitting have NaN in CooksDistance, Dffits, S2_i, and CovRatio, and zeros in Leverage, Dfbetas, and HatMatrix. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Observation diagnostics # name: # type: sq_string # elements: 1 # length: 18 LinearModel.Fitted # name: # type: sq_string # elements: 1 # length: 691 LinearModel: property Fitted Fitted response values An n-by-1 numeric vector of predicted response values based on the training data, where n is the total number of observations, excluded and missing rows included. Every observation whose predictors are available carries a fitted value, whether or not it was used in the fit, so an excluded row and a row missing only its response are both fitted; only a row whose predictors are missing is NaN. The corresponding Residuals are NaN for any row not used in the fit, so Fitted and Residuals.Raw do not add back to the response there. Use predict to obtain predictions for new data or to compute confidence bounds. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Fitted response values # name: # type: sq_string # elements: 1 # length: 19 LinearModel.Formula # name: # type: sq_string # elements: 1 # length: 327 LinearModel: property Formula Model formula information A LinearFormula object representing the model formula, with properties including ResponseName, LinearPredictor, PredictorNames, TermNames, HasIntercept, Terms (the terms matrix), and InModel. Converting it with char renders the whole formula. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Model formula information # name: # type: sq_string # elements: 1 # length: 23 LinearModel.LinearModel # name: # type: sq_string # elements: 1 # length: 5289 LinearModel: mdl = LinearModel (X, y) LinearModel: mdl = LinearModel (tbl, resp_input) LinearModel: mdl = LinearModel (..., modelspec) LinearModel: mdl = LinearModel (..., Name, Value, ...) Create a LinearModel class object representing a linear regression model. mdl = LinearModel (X, y) returns a LinearModel object fit to the response y and the predictor data X. Unless removed via the 'Intercept' option, the fitted model contains a constant (intercept) term and one linear term for every column of X. X is an NxP numeric or logical matrix of predictor data, where rows correspond to observations and columns correspond to variables. By default, the predictors are named 'x1', 'x2', ..., 'xP'. y is an Nx1 numeric or logical vector of response values, and must have the same number of observations (rows) as X. By default, the response is named 'y'. mdl = LinearModel (tbl, resp_input) fits a model using the variables in the table (or dataset) tbl as predictors. resp_input selects the response and can be a character vector naming a variable in tbl, or a numeric vector the same height as tbl to use as an external response. If resp_input is left empty, the last variable in tbl is used as the response. Variables that are categorical arrays, cell arrays of character vectors, or logical arrays are automatically treated as categorical predictors. mdl = LinearModel (..., modelspec) additionally specifies the terms of the model to fit. modelspec can be any of the following. Value Description 'constant' Model contains only an intercept term. 'linear' Model contains an intercept and one term for each predictor variable. This is the default when modelspec is not specified. 'interactions' Model contains an intercept, all linear terms, and all pairwise products of distinct predictor variables (no squared terms). 'purequadratic' Model contains an intercept, all linear terms, and all squared terms. 'quadratic' Model contains an intercept, all linear terms, all pairwise products of distinct predictor variables, and all squared terms. 'full' Model contains an intercept and all terms up to and including the full P-way interaction of the predictor variables. terms matrix A TxP or Tx(P+1) numeric matrix, where T is the number of terms and P is the number of predictor variables. Each row represents one term, and the value in column j is the exponent to which predictor j is raised in that term; a row of all zeros represents the intercept. If a Tx(P+1) matrix is supplied, its last column (representing the response variable) must be all zeros. Wilkinson formula A character vector of the form 'y ~ terms' describing the response and predictor terms using Wilkinson notation. For table input, the variable to the left of '~' is used as the response, overriding resp_input. mdl = LinearModel (..., Name, Value, ...) specifies additional options using one or more Name-Value pair arguments as described below. Name Value 'Intercept' A logical scalar indicating whether to include a constant (intercept) term in the model. Default is true. Ignored when modelspec is a Wilkinson formula. 'Weights' A numeric vector of nonnegative observation weights, with one element per observation, used to fit a weighted least squares model. Default is a vector of ones. 'Exclude' A numeric or logical vector specifying observations to exclude from the fit, given as row indices or a logical mask. Excluded observations, together with any observation containing a missing value, are recorded in ObservationInfo but do not contribute to the fit. 'CategoricalVars' Specifies which predictor variables are treated as categorical, given as a vector of column indices, a logical vector, or a cell array of variable names. Each categorical predictor with L categories is expanded into L-1 indicator (dummy) variables, using the first category as the reference level. 'VarNames' A cell array of character vectors naming the predictor and response variables, in order, with the response variable name last. Only applies to matrix input, since table variables already carry their own names. 'ResponseVar' A character vector naming the response variable, used to override the response variable name that would otherwise be used. 'PredictorVars' A cell array of character vectors naming which variables in tbl to use as predictors. By default, all variables other than the response variable are used. 'RobustOpts' Selects ordinary least squares or robust regression fitting. This value can be 'off' (default, ordinary least squares), 'on' (robust fitting using the 'bisquare' weighting function), the name of one of the weighting functions below, a function handle for a custom weighting function, or a scalar structure with fields RobustWgtFun and Tune specifying the weighting function and its tuning constant. Robust fitting uses Iteratively Reweighted Least Squares (IRLS), refitting the model with updated observation weights until the coefficients converge. Supported weighting function names: 'andrews', 'bisquare', 'cauchy', 'fair', 'huber', 'logistic', 'ols', 'talwar', 'welsch', each with its own default tuning constant. mdl is returned as a LinearModel object. If 'RobustOpts' is anything other than 'off', the returned model is a robust fit rather than an ordinary least squares fit. # name: # type: sq_string # elements: 1 # length: 73 Create a LinearModel class object representing a linear regression model. # name: # type: sq_string # elements: 1 # length: 25 LinearModel.LogLikelihood # name: # type: sq_string # elements: 1 # length: 1524 LinearModel: property LogLikelihood Log-likelihood of the fitted model A scalar numeric value equal to the log-likelihood of the response values, assuming each response is normally distributed with mean equal to the fitted value and variance equal to SSE/n (the MLE variance estimate). This property is read-only. For a weighted fit, observation i is taken to have variance s^2/w_i, so the log-likelihood carries the term 0.5 x sum (log (w)) and n counts only the observations with nonzero weight: logL = -n/2 x (1 + log (2xpixSSE/n)) + 0.5 x sum (log (w)) This makes the value invariant to the scale of the weights, as it must be: multiplying every weight by a constant rescales the estimated variance by the same constant and leaves the fit unchanged. MATLAB omits the 0.5 x sum (log (w)) term and counts every observation in n, so its LogLikelihood moves by n/2 x log (c) when the weights are multiplied by c, and the ModelCriterion values built on it move with it. This implementation follows R's logLik.lm instead. Unweighted fits are unaffected, and agree with MATLAB. A robust fit carries no weight term. Its SSE is a robust scale estimate rather than a weighted residual sum, so the two enter separately and the general form is used: logL = -n/2 x log (2xpixSSE/n) - sum (w .x r.^2) / (2xSSE/n) which is what MATLAB computes, and which reduces to the expression above whenever sum (w .x r.^2) equals SSE, as it does for any least-squares fit. Robust fits therefore agree with MATLAB exactly, weighted or not. # name: # type: sq_string # elements: 1 # length: 34 Log-likelihood of the fitted model # name: # type: sq_string # elements: 1 # length: 15 LinearModel.MSE # name: # type: sq_string # elements: 1 # length: 200 LinearModel: property MSE Mean squared error A scalar numeric value equal to SSE / DFE, where SSE is the sum of squared errors and DFE is the degrees of freedom for error. This property is read-only. # name: # type: sq_string # elements: 1 # length: 18 Mean squared error # name: # type: sq_string # elements: 1 # length: 26 LinearModel.ModelCriterion # name: # type: sq_string # elements: 1 # length: 657 LinearModel: property ModelCriterion Model comparison criteria A structure with four fields: AIC - Akaike information criterion: -2 x logL + 2 x m AICc - AIC corrected for sample size: AIC + (2xmx(m+1))/(n-m-1) BIC - Bayesian information criterion: -2 x logL + m x log(n) CAIC - Consistent AIC: -2 x logL + m x (log(n) + 1) Here logL is LogLikelihood, m is NumEstimatedCoefficients, and n is the number of observations with nonzero weight, which is NumObservations unless some weight is zero. This property is read-only. Because these are built on LogLikelihood, they inherit its treatment of weights; see that property for how it differs from MATLAB's. # name: # type: sq_string # elements: 1 # length: 25 Model comparison criteria # name: # type: sq_string # elements: 1 # length: 31 LinearModel.ModelFitVsNullModel # name: # type: sq_string # elements: 1 # length: 336 LinearModel: property ModelFitVsNullModel F-test of the fitted model versus the null model A structure with three fields: Fstat - F-statistic of the fitted model versus a null model containing only a constant term Pvalue - p-value for the F-statistic NullModel - character vector describing the null model This property is read-only. # name: # type: sq_string # elements: 1 # length: 48 F-test of the fitted model versus the null model # name: # type: sq_string # elements: 1 # length: 27 LinearModel.NumCoefficients # name: # type: sq_string # elements: 1 # length: 258 LinearModel: property NumCoefficients Number of model coefficients A positive integer giving the total number of coefficients in the fitted model, including any coefficients set to zero because the model terms are rank deficient. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Number of model coefficients # name: # type: sq_string # elements: 1 # length: 36 LinearModel.NumEstimatedCoefficients # name: # type: sq_string # elements: 1 # length: 298 LinearModel: property NumEstimatedCoefficients Number of estimated coefficients A positive integer giving the number of coefficients actually estimated, i.e., not set to zero due to rank deficiency. NumEstimatedCoefficients equals the degrees of freedom for regression. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Number of estimated coefficients # name: # type: sq_string # elements: 1 # length: 27 LinearModel.NumObservations # name: # type: sq_string # elements: 1 # length: 282 LinearModel: property NumObservations Number of observations used in the fit A positive integer giving the number of observations actually used in fitting. Rows with missing values and rows excluded via the 'Exclude' name-value argument are not counted. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Number of observations used in the fit # name: # type: sq_string # elements: 1 # length: 25 LinearModel.NumPredictors # name: # type: sq_string # elements: 1 # length: 177 LinearModel: property NumPredictors Number of predictor variables A positive integer giving the number of predictor variables used to fit the model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Number of predictor variables # name: # type: sq_string # elements: 1 # length: 24 LinearModel.NumVariables # name: # type: sq_string # elements: 1 # length: 235 LinearModel: property NumVariables Number of variables in the input data A positive integer giving the total number of variables in the input data, counting predictors, the response, and any unused columns. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Number of variables in the input data # name: # type: sq_string # elements: 1 # length: 27 LinearModel.ObservationInfo # name: # type: sq_string # elements: 1 # length: 448 LinearModel: property ObservationInfo Per-observation metadata An n-by-4 table where n is the total number of rows in the input data. The four columns are: Weights - observation weight, default is 1 Excluded - logical; true if excluded via the 'Exclude' argument Missing - logical; true if the row contains any NaN value Subset - logical; true if the observation was used in the fit, i.e. not excluded and not missing This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Per-observation metadata # name: # type: sq_string # elements: 1 # length: 28 LinearModel.ObservationNames # name: # type: sq_string # elements: 1 # length: 283 LinearModel: property ObservationNames Observation names A cell array of character vectors containing the names of the observations. If the fit was based on a table that has row names, this property holds those names. Otherwise it is an empty cell array. This property is read-only. # name: # type: sq_string # elements: 1 # length: 17 Observation names # name: # type: sq_string # elements: 1 # length: 26 LinearModel.PredictorNames # name: # type: sq_string # elements: 1 # length: 199 LinearModel: property PredictorNames Names of predictor variables A cell array of character vectors containing the names of the predictor variables used to fit the model. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Names of predictor variables # name: # type: sq_string # elements: 1 # length: 16 LinearModel.RMSE # name: # type: sq_string # elements: 1 # length: 122 LinearModel: property RMSE Root mean squared error A scalar numeric value equal to sqrt(MSE). This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Root mean squared error # name: # type: sq_string # elements: 1 # length: 21 LinearModel.Residuals # name: # type: sq_string # elements: 1 # length: 577 LinearModel: property Residuals Residuals for the fitted model A table with one row per observation and four columns: Raw - observed minus fitted values Pearson - raw residuals divided by RMSE Standardized - internally studentized residuals; raw residuals divided by their estimated standard deviation using the full-model MSE Studentized - externally studentized residuals; each raw residual divided by an estimate of the standard deviation based on all observations except that one, using the delete-1 S2_i Rows not used in the fit contain NaN. This property is read-only. # name: # type: sq_string # elements: 1 # length: 30 Residuals for the fitted model # name: # type: sq_string # elements: 1 # length: 24 LinearModel.ResponseName # name: # type: sq_string # elements: 1 # length: 151 LinearModel: property ResponseName Response variable name A character vector containing the name of the response variable. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Response variable name # name: # type: sq_string # elements: 1 # length: 18 LinearModel.Robust # name: # type: sq_string # elements: 1 # length: 430 LinearModel: property Robust Robust fit information A structure with three fields: WgtFun - robust weighting function name, e.g. 'bisquare' Tune - tuning constant; empty if WgtFun is 'ols' or a function handle with the default tuning constant Weights - vector of final iteration weights; empty for a CompactLinearModel object This structure is empty unless the model was fit using robust regression. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Robust fit information # name: # type: sq_string # elements: 1 # length: 20 LinearModel.Rsquared # name: # type: sq_string # elements: 1 # length: 266 LinearModel: property Rsquared R-squared goodness-of-fit statistics A structure with two fields: Ordinary - coefficient of determination: R^2 = SSR / SST Adjusted - adjusted R^2 that accounts for the number of coefficients in the model This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 R-squared goodness-of-fit statistics # name: # type: sq_string # elements: 1 # length: 15 LinearModel.SSE # name: # type: sq_string # elements: 1 # length: 242 LinearModel: property SSE Sum of squared errors A scalar numeric value equal to the sum of squared residuals. For a model with an intercept, SST = SSE + SSR. For weighted fits, this is the weighted sum of squares. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Sum of squared errors # name: # type: sq_string # elements: 1 # length: 15 LinearModel.SSR # name: # type: sq_string # elements: 1 # length: 298 LinearModel: property SSR Regression sum of squares A scalar numeric value equal to the sum of squared deviations of the fitted values from the mean of the response. For a model with an intercept, SST = SSE + SSR. For weighted fits, this is the weighted sum of squares. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Regression sum of squares # name: # type: sq_string # elements: 1 # length: 15 LinearModel.SST # name: # type: sq_string # elements: 1 # length: 347 LinearModel: property SST Total sum of squares A scalar numeric value equal to the sum of squared deviations of the response from its mean. For a model with an intercept, SST = SSE + SSR. For a robust fit, SST = SSE + SSR rather than the deviation from the mean. For weighted fits, this is the weighted sum of squares. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Total sum of squares # name: # type: sq_string # elements: 1 # length: 17 LinearModel.Steps # name: # type: sq_string # elements: 1 # length: 1235 LinearModel: property Steps Stepwise fitting information A structure recording the term-selection trace, populated whenever the model was fit by stepwiselm or improved by step, and [] otherwise. It has seven fields: Field Contents Start a LinearFormula for the model the search started from. Lower a LinearFormula for the smallest model considered; its terms are never removed. Upper a LinearFormula for the largest model considered. Criterion the selection criterion, such as 'SSE'. PEnter the threshold a term must beat to enter. PRemove the threshold above which a term leaves. History a table with one row per step. History carries the columns Action ('Start', 'Add', or 'Remove'), TermName, Terms (the terms matrix after the step, over the model's variables), DF (the coefficient count after the step), and delDF (the change in it, negative for a removal). The remaining columns follow the criterion: FStat and pValue under 'SSE', and otherwise a single column named for the criterion (AIC, BIC, Rsquared, or AdjRsquared) holding its value after the step. The first row is the starting model, named by its right-hand side, and step appends to the history it inherits rather than starting a new one. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Stepwise fitting information # name: # type: sq_string # elements: 1 # length: 24 LinearModel.VariableInfo # name: # type: sq_string # elements: 1 # length: 510 LinearModel: property VariableInfo Information about input variables A table with one row per variable including any unused variables, and four columns: Class - variable class as a character vector, e.g. 'double' or 'categorical' Range - for continuous variables, a two-element vector [min, max]; for categorical variables, a vector of the distinct values InModel - logical; true if the variable is in the fitted model IsCategorical - logical; true if the variable is categorical This property is read-only. # name: # type: sq_string # elements: 1 # length: 33 Information about input variables # name: # type: sq_string # elements: 1 # length: 25 LinearModel.VariableNames # name: # type: sq_string # elements: 1 # length: 385 LinearModel: property VariableNames Names of all variables in the input data A cell array of character vectors containing the names of all variables, including predictors, the response, and unused variables. For table input these are the table column names. For matrix input these are the values given by 'VarNames', defaulting to {'x1','x2',...,'xp','y'}. This property is read-only. # name: # type: sq_string # elements: 1 # length: 40 Names of all variables in the input data # name: # type: sq_string # elements: 1 # length: 21 LinearModel.Variables # name: # type: sq_string # elements: 1 # length: 319 LinearModel: property Variables Input data as a table A table containing predictor and response values for all observations, including unused variables. For table input this is the full input table. For matrix input this is a table constructed from the predictor matrix and response vector. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Input data as a table # name: # type: sq_string # elements: 1 # length: 20 LinearModel.addTerms # name: # type: sq_string # elements: 1 # length: 1834 LinearModel: NewMdl = addTerms (mdl, terms) Add terms to a fitted linear regression model. addTerms returns a new LinearModel refitted on the same data and settings as mdl with the specified terms appended to the model formula. The original model mdl is never modified; all settings including observation weights, excluded rows, and categorical variable encodings are carried over automatically. To update a model in place, reassign the result: mdl = addTerms (mdl, terms). terms may be a character vector in Wilkinson notation. Use 'x1' for a main effect, 'x1:x2' for a two-way interaction, 'x1*x2' to add both main effects and their interaction in one step, 'x1 + x2^2' to add several terms at once, or '1' to add an intercept to a no-intercept model. A bare power term 'x1^2' adds x1 together with x1^2 (and any intermediate powers), matching the Wilkinson hierarchy convention; power notation used inside an interaction, e.g. 'x1:x2^2', adds only that exact interaction term. All variable names must match entries in mdl.PredictorNames. terms may also be a numeric matrix of size t-by-v, where t is the number of terms to add and v equals mdl.NumVariables. Entry T(i,j) is the exponent of variable j in term i. For example, in a model with variables x1, x2, y: [0 0 0] is the intercept, [0 1 0] is x2, [1 1 0] is x1:x2, and [2 0 0] is x1^2. The last column (response) is always zero. A matrix with mdl.NumPredictors columns is also accepted and is automatically padded with a trailing zero column for the response. Terms that are already present in mdl are silently skipped. If every specified term already exists, a warning is issued and mdl is returned unchanged. For a categorical predictor, addTerms adds the full group of indicator variables for that predictor in one step rather than adding individual indicator columns. # name: # type: sq_string # elements: 1 # length: 46 Add terms to a fitted linear regression model. # name: # type: sq_string # elements: 1 # length: 17 LinearModel.anova # name: # type: sq_string # elements: 1 # length: 2469 LinearModel: tbl = anova (mdl) LinearModel: tbl = anova (mdl, anovatype) LinearModel: tbl = anova (mdl, "components", sstype) Analysis of variance for a linear regression model. anova (mdl) returns a table tbl with component ANOVA statistics for every term in mdl except the constant term, computed with hierarchical ("h") sums of squares. Each row gives SumSq, DF, MeanSq, F, and pValue for the corresponding term; the trailing Error row gives SumSq = mdl.SSE, DF = mdl.DFE, MeanSq = mdl.MSE, and NaN for F and pValue. MATLAB reports F = 1 and pValue = 0.5 on that Error row instead. Those are not results: the row's F is its own MeanSq divided by itself, so it is 1 for every data set, and the pValue follows. MATLAB does not use them consistently either, reporting NaN for the same quantity on the Residual row of its summary table. This implementation reports NaN in both places. Every other value in both tables agrees with MATLAB. anova (mdl, anovatype) selects "components" (default) or "summary". For "summary", tbl always contains rows Total, Model, and Residual, and additionally . Linear and . Nonlinear whenever mdl contains an interaction term or a continuous term of degree greater than 1. Total reports mdl.SST with DF = NumObservations - 1; Model reports mdl.SSR with DF = NumCoefficients - HasIntercept; Residual reports mdl.SSE with DF = mdl.DFE. Whenever the data contains two or more observations sharing identical predictor values, tbl additionally contains . Lack of fit and . Pure error, splitting Residual into the part explained by replicated observations and the remainder. anova (mdl, "components", sstype) selects the sum of squares used for the component table: 1 (sequential, reduction from adding each term in formula order), 2 (reduction from adding the term to a model containing every term that does not contain it), "h" (default; as Type 2, but a higher-degree term in the same continuous variable, such as a squared term, is also treated as containing the lower-degree term), or 3 (reduction from adding the term to a model containing every other term, with categorical predictors recoded using sum-to-zero deviation contrasts instead of mdl's reference-level coding). Because Type 3 uses a different coding, its Error row can differ from mdl.SSE and mdl.DFE when mdl is missing a lower-order relative of one of its terms (e.g. an interaction without one of its main effects, or a categorical predictor fit without an intercept). # name: # type: sq_string # elements: 1 # length: 51 Analysis of variance for a linear regression model. # name: # type: sq_string # elements: 1 # length: 18 LinearModel.coefCI # name: # type: sq_string # elements: 1 # length: 1287 LinearModel: ci = coefCI (mdl) LinearModel: ci = coefCI (mdl, alpha) Confidence intervals for the coefficient estimates of a fitted linear regression model. ci = coefCI (mdl) returns 95% confidence intervals for every coefficient in mdl using a default significance level of 0.05. ci = coefCI (mdl, alpha) uses the significance level alpha, a scalar in [0, 1]. The resulting intervals have coverage 100(1-\alpha)\%. Setting alpha to 0 produces intervals of infinite width; setting it to 1 collapses each interval to the corresponding point estimate. The output ci is a k-by-2 numeric matrix where k = mdl.NumCoefficients. Row j contains the interval for the j-th coefficient, whose name is stored in mdl.CoefficientNames{j}. Column 1 is the lower bound and column 2 is the upper bound. The midpoint of each interval equals the corresponding point estimate in mdl.Coefficients.Estimate. Intervals use the Wald method: b_j \pm t_{(1-\alpha/2,\,\mathrm{DFE})}\,\mathrm{SE}(b_j), where b_j is the coefficient estimate, \mathrm{SE}(b_j) is its standard error from mdl.Coefficients.SE, and the critical value is the 1-\alpha/2 quantile of the t-distribution with mdl.DFE degrees of freedom. In rank-deficient models, aliased coefficients have \mathrm{SE} = 0 and their row in ci is [0, 0]. # name: # type: sq_string # elements: 1 # length: 87 Confidence intervals for the coefficient estimates of a fitted linear regression model. # name: # type: sq_string # elements: 1 # length: 20 LinearModel.coefTest # name: # type: sq_string # elements: 1 # length: 1965 LinearModel: p = coefTest (mdl) LinearModel: p = coefTest (mdl, H) LinearModel: p = coefTest (mdl, H, C) LinearModel: [p, F] = coefTest (...) LinearModel: [p, F, r] = coefTest (...) Linear hypothesis test on the coefficients of a fitted linear regression model. coefTest tests whether one or more linear combinations of the fitted coefficients equal specified constants. Each linear combination is encoded as a row of the contrast matrix H, and the right-hand side is given by C. p = coefTest (mdl) performs the overall model F-test: it tests the joint null hypothesis that every coefficient except the intercept is zero. The returned p-value matches the F-statistic line printed at the bottom of the model display. p = coefTest (mdl, H) tests the null hypothesis H \beta = 0, where \beta is the full coefficient vector of length k = mdl.NumCoefficients. H must be a full-rank numeric matrix with k columns; each row specifies one linear constraint. To test a single coefficient, use a row vector with a 1 in that coefficient's position and zeros elsewhere; the resulting F-statistic equals the square of the corresponding t-statistic in mdl.Coefficients. To test a categorical predictor that expands to multiple indicator columns, include one row per indicator in H. p = coefTest (mdl, H, C) tests H \beta = C instead of zero. C must be a numeric vector with the same number of elements as rows of H; both row and column vectors are accepted. The second output F is the value of the F-statistic: F = (H\hat{\beta} - C)^\prime (H V H^\prime)^{-1} (H\hat{\beta} - C) / r, where V is mdl.CoefficientCovariance and r is the number of rows of H. The third output r is that numerator degrees of freedom; the denominator degrees of freedom is mdl.DFE. Under the null hypothesis F follows an F(r, \mathrm{DFE}) distribution and the p-value is the upper-tail probability. When H is rank-deficient but contains no NaN, both p and F are returned as NaN without an error. # name: # type: sq_string # elements: 1 # length: 79 Linear hypothesis test on the coefficients of a fitted linear regression model. # name: # type: sq_string # elements: 1 # length: 19 LinearModel.compact # name: # type: sq_string # elements: 1 # length: 1064 LinearModel: cmdl = compact (mdl) Create a compact version of a fitted linear regression model. cmdl = compact (mdl) returns a CompactLinearModel object that retains the coefficient estimates, coefficient covariance, fit statistics, model formula, and fitting method information of mdl, but discards the training data and everything derived from it. Specifically, the following properties of mdl are not carried over and are unavailable on cmdl: Fitted, Residuals, Diagnostics, ObservationInfo, ObservationNames, Variables, Steps, and ModelFitVsNullModel. If mdl was fit using robust regression, the Robust structure is retained on cmdl except for its Weights field, which is always emptied; RobustWgtFun and Tune are preserved unchanged. A CompactLinearModel object consumes less memory than a LinearModel object and can still be used with predict, feval, random, coefCI, and coefTest, but does not support methods that require the original training data or refitting, such as addTerms, removeTerms, step, and dwtest. See also: LinearModel, CompactLinearModel # name: # type: sq_string # elements: 1 # length: 61 Create a compact version of a fitted linear regression model. # name: # type: sq_string # elements: 1 # length: 18 LinearModel.dwtest # name: # type: sq_string # elements: 1 # length: 2129 LinearModel: p = dwtest (mdl) LinearModel: p = dwtest (mdl, method) LinearModel: p = dwtest (mdl, method, tail) LinearModel: [p, DW] = dwtest (...) Durbin-Watson test for serial autocorrelation of linear regression residuals. dwtest checks whether the raw residuals of mdl are correlated with their immediate neighbours in observation order, which would violate the independence assumption of ordinary least squares. The null hypothesis is that there is no autocorrelation. A small p-value gives evidence against this and suggests that the residuals are not independent. This test is most meaningful when the observations have a natural ordering, such as a time series. The test is based on the Durbin-Watson statistic DW = \sum_{i=1}^{n-1}(e_{i+1}-e_i)^2 / \sum_{i=1}^{n}e_i^2, where e_i are the raw residuals of the active (non-excluded) observations. The statistic always lies in [0, 4]: values near 2 indicate no autocorrelation, values well below 2 indicate positive autocorrelation (adjacent residuals tend to have the same sign), and values well above 2 indicate negative autocorrelation (adjacent residuals tend to alternate in sign). method controls how the p-value is computed and defaults to 'exact'. 'exact' uses the eigenvalues of the projected differencing matrix together with Imhof's numerical integration to obtain a precise p-value; this is slower but accurate for any sample size. 'approximate' uses a normal approximation based on the first two moments of the DW distribution under the null; this is faster and adequate for large samples but less reliable for small ones. The argument is case-insensitive. tail selects the alternative hypothesis and defaults to 'both'. 'right' tests for positive autocorrelation (DW < 2), 'left' tests for negative autocorrelation (DW > 2), and 'both' tests for autocorrelation in either direction. The one-sided p-values always satisfy p_{\mathrm{right}} + p_{\mathrm{left}} = 1, and the two-sided p-value equals 2\min(p_{\mathrm{right}}, p_{\mathrm{left}}). The second output DW is the value of the Durbin-Watson statistic itself; it does not depend on method or tail. # name: # type: sq_string # elements: 1 # length: 77 Durbin-Watson test for serial autocorrelation of linear regression residuals. # name: # type: sq_string # elements: 1 # length: 17 LinearModel.feval # name: # type: sq_string # elements: 1 # length: 1318 LinearModel: ypred = feval (mdl, X) LinearModel: ypred = feval (mdl, x1, x2, ..., xp) Predict responses of a fitted linear regression model using separate predictor inputs. ypred = feval (mdl, X) accepts a single numeric matrix X with one column per predictor in the same order as the training data, or a table whose column names match mdl.PredictorNames. The output is an n-by-1 column vector. Rows that contain NaN in any predictor column are returned as NaN. ypred = feval (mdl, x1, x2, ..., xp) accepts exactly mdl.NumPredictors separate arguments, one per predictor variable. All non-scalar arguments must have the same size; a scalar argument is broadcast to that size automatically. The output shape follows the shape of the non-scalar inputs: column vector inputs give a column vector output, row vector inputs give a row vector output, and all-scalar inputs give a scalar. This form is convenient when predictor data is already stored in separate vectors rather than a combined matrix. feval gives the same numerical predictions as predict but does not support confidence intervals. Use predict when you also need bounds on the response. Because a LinearModel object behaves like a function through feval, it can be passed directly to routines that accept a function handle, such as fminsearch or integral. # name: # type: sq_string # elements: 1 # length: 86 Predict responses of a fitted linear regression model using separate predictor inputs. # name: # type: sq_string # elements: 1 # length: 16 LinearModel.plot # name: # type: sq_string # elements: 1 # length: 1443 LinearModel: plot (mdl) LinearModel: plot (ax, mdl) LinearModel: h = plot (...) Create a default diagnostic plot for a fitted linear regression model. plot (mdl) creates a plot whose type depends on the number of predictors in mdl. If mdl has two or more predictors, plot creates an added variable plot for the whole model except the constant (intercept) term, equivalent to plotAdded (mdl). If mdl has exactly one predictor, plot creates a scatter plot of the data together with the fitted curve and its 95% confidence bounds. If mdl has no predictors, plot creates a histogram of the residuals, equivalent to plotResiduals (mdl). For the single-predictor case, the fitted curve and confidence bounds are computed with predict, evaluated at 100 equally spaced points spanning the observed range of the predictor when the predictor is numeric, or at each level of the predictor when it is categorical. Excluded or missing observations appear as NaN in the data and produce gaps in the plotted points. plot (ax, mdl) plots into the axes object ax instead of the current axes returned by gca. h = plot (...) returns a vector of graphics object handles. For the two-or-more-predictor and no-predictor cases, see plotAdded and plotResiduals, respectively, for the meaning of h. For the single-predictor case, h(1), h(2), and h(3) correspond to the data points, the fitted curve, and the 95% confidence bounds of the fitted curve, respectively. # name: # type: sq_string # elements: 1 # length: 70 Create a default diagnostic plot for a fitted linear regression model. # name: # type: sq_string # elements: 1 # length: 21 LinearModel.plotAdded # name: # type: sq_string # elements: 1 # length: 2396 LinearModel: plotAdded (mdl) LinearModel: plotAdded (mdl, coef) LinearModel: plotAdded (mdl, coef, Name, Value) LinearModel: plotAdded (ax, ...) LinearModel: h = plotAdded (...) Create an added variable plot for a fitted linear regression model. plotAdded (mdl) creates an added variable plot for the whole model mdl except the constant (intercept) term. plotAdded (mdl, coef) creates an added variable plot for the coefficients specified by coef. coef is a character vector or string naming a single coefficient in mdl.CoefficientNames, the name of a categorical predictor in mdl.PredictorNames (which selects that predictor's whole group of indicator coefficients), or a vector of positive integers indexing into mdl.CoefficientNames. An added variable plot, also known as a partial regression leverage plot, illustrates the incremental effect on the response of the selected terms after removing the effects of all other terms. For a single selected predictor x_1, the response y and x_1 are each fit to all other terms: y_i = g_y (x_{2i}, ..., x_{pi}) + r_{yi}, x_{1i} = g_x (x_{2i}, ..., x_{pi}) + r_{xi}. The adjusted values are \tilde y_i = \bar y + r_{yi} and \tilde x_{1i} = \bar x_1 + r_{xi}. When coef selects more than one coefficient, the selected columns of the design matrix are combined into a single direction using the unit vector u = \beta / \lVert \beta \rVert, and the added variable plot is created for that combined direction. Excluded or missing observations appear as NaN in the adjusted data and produce gaps in the plotted data points. plotAdded (mdl, coef, Name, Value) specifies additional Name-Value arguments applied to the adjusted data points (h(1)). The following are accepted: Name Description and default 'Color' Marker color. Default: [0.1490 0.5490 0.8660]. 'Marker' Marker symbol. Default: 'x'. 'MarkerSize' Marker size in points. Default: 6. 'MarkerEdgeColor' Marker edge color. Default: 'auto'. 'MarkerFaceColor' Marker fill color. Default: 'none'. 'LineWidth' Width of the marker edge in points. Default: 0.5. plotAdded (ax, ...) plots into the axes object ax instead of the current axes returned by gca. h = plotAdded (...) returns a 3-by-1 vector of line handles. h(1), h(2), and h(3) correspond to the adjusted data points, the fitted line, and the 95% confidence bounds of the fitted line, respectively. Name-Value arguments only affect h(1). # name: # type: sq_string # elements: 1 # length: 67 Create an added variable plot for a fitted linear regression model. # name: # type: sq_string # elements: 1 # length: 32 LinearModel.plotAdjustedResponse # name: # type: sq_string # elements: 1 # length: 2267 LinearModel: plotAdjustedResponse (mdl, var) LinearModel: plotAdjustedResponse (mdl, var, Name, Value) LinearModel: plotAdjustedResponse (ax, ...) LinearModel: h = plotAdjustedResponse (...) Plot the adjusted response of a fitted linear regression model against a single predictor. plotAdjustedResponse (mdl, var) creates an adjusted response plot for the predictor var in the linear regression model mdl. var is a character vector or string naming a predictor in mdl.PredictorNames, or a positive integer indexing into mdl.VariableNames. An adjusted response function describes the fitted response as a function of a single predictor, with the other predictors averaged out by averaging the fitted values over the observations used in the fit. For a model y_i = f (x_{1i}, x_{2i}, ..., x_{pi}) + r_i, the adjusted response function for x_1 is g (x_1) = (1/n) \sum_{i=1}^n f (x_1, x_{2i}, x_{3i}, ..., x_{pi}), where n is the number of observations used to fit the model. The adjusted response data value for observation i is \tilde y_i = g (x_{1i}) + r_i. For a numeric predictor, the adjusted response function is evaluated on an evenly spaced grid of 100 points spanning the minimum to the maximum observed value of var. For a categorical predictor, the adjusted response function is evaluated at each category level. Excluded or missing observations appear as NaN in the adjusted data and produce gaps in the plotted data points. plotAdjustedResponse (mdl, var, Name, Value) specifies additional Name-Value arguments applied to the adjusted data points (h(1)). The following are accepted: Name Description and default 'Color' Marker color. Default: [0.1490 0.5490 0.8660]. 'Marker' Marker symbol. Default: 'x'. 'MarkerSize' Marker size in points. Default: 6. 'MarkerEdgeColor' Marker edge color. Default: 'auto'. 'MarkerFaceColor' Marker fill color. Default: 'none'. 'LineWidth' Width of the marker edge in points. Default: 0.5. plotAdjustedResponse (ax, ...) plots into the axes object ax instead of the current axes returned by gca. h = plotAdjustedResponse (...) returns a 2-by-1 vector of line handles. h(1) corresponds to the adjusted response data points and h(2) corresponds to the adjusted response function. Name-Value arguments only affect h(1). # name: # type: sq_string # elements: 1 # length: 90 Plot the adjusted response of a fitted linear regression model against a single predictor. # name: # type: sq_string # elements: 1 # length: 27 LinearModel.plotDiagnostics # name: # type: sq_string # elements: 1 # length: 3748 LinearModel: plotDiagnostics (mdl) LinearModel: plotDiagnostics (mdl, plottype) LinearModel: plotDiagnostics (mdl, plottype, Name, Value) LinearModel: plotDiagnostics (ax, ...) LinearModel: h = plotDiagnostics (...) Plot observation diagnostics of a fitted linear regression model. plotDiagnostics (mdl) creates a case-order plot of the leverage of each observation. The x-axis is the observation row number running from 1 to the total number of rows including any excluded rows. A dotted horizontal reference line marks the recommended threshold 2p/n, where p is mdl.NumCoefficients and n is mdl.NumObservations. plotDiagnostics (mdl, plottype) creates the diagnostic plot specified by plottype. For all types except "contour", the x-axis is the row number and covers all rows including excluded ones. Excluded rows produce NaN values in the diagnostic vectors, which appear as natural gaps in the plot with no special handling required. plottype must be one of: 'leverage' (default) Leverage of each observation (mdl.Diagnostics.Leverage). One dotted horizontal reference line at 2p/n. Returns two handles: h(1) is the data scatter and h(2) is the reference line. 'cookd' Cook's distance for each observation (mdl.Diagnostics.CooksDistance). One dotted reference line at 3 \times \mathrm{mean(CooksDistance)}, where the mean ignores NaN values. Returns two handles: h(1) data, h(2) reference. 'covratio' Delete-1 ratio of the determinant of the coefficient covariance matrix (mdl.Diagnostics.CovRatio). Two dotted reference lines at 1 - 3p/n (lower bound) and 1 + 3p/n (upper bound). Both bounds are stored as a single NaN-separated line object. Returns two handles: h(1) data, h(2) combined reference. 'dfbetas' Delete-1 scaled change in each coefficient estimate (mdl.Diagnostics.Dfbetas, one column per coefficient). One line object is drawn per coefficient. Two dotted reference lines at \pm 3/\sqrt{n} are stored as a single NaN-separated line object. Returns p+1 handles: h(1) through h(p) are the per-coefficient data lines and h(p+1) is the combined reference. Name-Value arguments are applied to all p data handles. 'dffits' Delete-1 scaled change in the fitted value (mdl.Diagnostics.Dffits). Two dotted reference lines at \pm 2\sqrt{p/n} stored as a single NaN-separated line. Returns two handles: h(1) data, h(2) combined reference. 's2_i' Delete-1 variance estimate (mdl.Diagnostics.S2_i). One dotted reference line at mdl.MSE. Returns two handles: h(1) data, h(2) reference. 'contour' Standardized residuals on the y-axis against leverage on the x-axis, with Cook's distance contours overlaid at levels [0.05, 0.10, 0.15, 0.20, 0.25]. The contour surface is computed on a 31-by-30 grid over the range of the active leverage and residual values. Returns two handles: h(1) is the data scatter (a line object) and h(2) is the contour object. plotDiagnostics (ax, ...) targets the axes object ax instead of the current axes returned by gca. h = plotDiagnostics (...) returns a vector of graphics handles. The number of handles depends on plottype as described above. Name-Value arguments are applied to the data handle h(1), except for "dfbetas" where they are applied to all p coefficient handles. Reference line handles are never affected by Name-Value arguments. Name Description and default 'Color' Marker color for data points. For "dfbetas" this color is applied to all p coefficient line objects. Default: [0.1490 0.5490 0.8660]. 'Marker' Marker symbol. Any symbol accepted by plot is valid. Default: 'x'. 'MarkerSize' Marker size in points. Default: 6. 'MarkerEdgeColor' Marker edge color. Default: 'auto'. 'MarkerFaceColor' Marker fill color. Default: 'none'. 'LineWidth' Width of the marker edge in points. Default: 0.5. # name: # type: sq_string # elements: 1 # length: 65 Plot observation diagnostics of a fitted linear regression model. # name: # type: sq_string # elements: 1 # length: 23 LinearModel.plotEffects # name: # type: sq_string # elements: 1 # length: 1547 LinearModel: plotEffects (mdl) LinearModel: plotEffects (ax, mdl) LinearModel: h = plotEffects (...) Plot the main effects of each predictor in a fitted linear regression model. plotEffects (mdl) creates a horizontal dot-and-line plot with one row per predictor. Each dot shows the estimated main effect on the response from changing that predictor from its minimum observed value to its maximum observed value, while holding all other predictors fixed at their observed means. A horizontal line through each dot shows the 95% confidence interval for that effect. The main effect for predictor xs is defined as g(x_{s,\max}) - g(x_{s,\min}), where the adjusted response function g evaluates the model at the specified value of xs with all other predictors set to their observed means. For numeric predictors the sign of the effect can be positive or negative depending on the direction of the relationship. plotEffects (ax, mdl) creates the plot in the axes object ax instead of the current axes returned by gca. h = plotEffects (...) returns a vector of p+1 graphics handles where p is the number of predictors. h(1) is the line object containing the effect estimate markers (one circle per predictor, plotted as a single line object with XData of length p and YData = 1:p). h(j+1) is the confidence interval line for predictor j, with XData = [ci_lo, ci_hi] and YData = [j, j]. The y-axis tick labels follow the format 'varname: min to max', showing the predictor name and the minimum and maximum observed values used to compute the effect. # name: # type: sq_string # elements: 1 # length: 76 Plot the main effects of each predictor in a fitted linear regression model. # name: # type: sq_string # elements: 1 # length: 27 LinearModel.plotInteraction # name: # type: sq_string # elements: 1 # length: 2750 LinearModel: plotInteraction (mdl, var1, var2) LinearModel: plotInteraction (mdl, var1, var2, ptype) LinearModel: plotInteraction (ax, ...) LinearModel: h = plotInteraction (...) Plot the interaction effects of two predictors in a fitted linear regression model. plotInteraction (mdl, var1, var2) creates a plot of the main effects of var1 and var2 together with their conditional effects, with horizontal lines through each effect value indicating its 95% confidence interval. var1 and var2 are each a character vector or string naming a variable in mdl.VariableNames, or a positive integer indexing into mdl.VariableNames; neither may name the response variable, and they must be different variables. The main effect of a predictor is the change in the adjusted response between the two predictor values that produce the minimum and maximum adjusted response, with the other predictor averaged over its own observed values row by row. For a numeric predictor these two values are its observed minimum and maximum; for a categorical predictor every level is evaluated and the levels producing the minimum and maximum adjusted response are used, so the effect is always nonnegative. The conditional effect of var1 is its effect recomputed with var2 additionally held fixed at each of a small set of conditioning values, and likewise the conditional effect of var2 holds var1 fixed. The conditioning values are the observed minimum, mean of the minimum and maximum, and maximum for a numeric predictor, or every level for a categorical predictor. When the main effect and conditional effect points for a predictor do not align vertically, the model exhibits an interaction between var1 and var2. plotInteraction (mdl, var1, var2, ptype) selects the plot type. ptype is 'effects' (default), as described above, or 'predictions', which instead plots the adjusted response as a function of var2 for each conditioning value of var1 held fixed, evaluated over 101 equally spaced points spanning the observed range of var2 when var2 is numeric, or at each level of var2 when it is categorical. plotInteraction (ax, ...) plots into the axes object ax instead of the current axes returned by gca. h = plotInteraction (...) returns a vector of line handles. When ptype is 'effects', h(1) is the marker line through the two main effect points, h(2) and h(3) are the confidence interval lines for the main effects of var1 and var2, and the remaining entries are the conditional effect points and their confidence intervals, tagged 'conditional1' for var1 and 'conditional2' for var2. The main effect line objects are tagged 'main'. When ptype is 'predictions', each entry in h corresponds to one adjusted response curve, one per conditioning value of var1. # name: # type: sq_string # elements: 1 # length: 83 Plot the interaction effects of two predictors in a fitted linear regression model. # name: # type: sq_string # elements: 1 # length: 25 LinearModel.plotResiduals # name: # type: sq_string # elements: 1 # length: 4289 LinearModel: plotResiduals (mdl) LinearModel: plotResiduals (mdl, plottype) LinearModel: plotResiduals (mdl, plottype, Name, Value) LinearModel: plotResiduals (ax, ...) LinearModel: h = plotResiduals (...) Plot residuals of a fitted linear regression model. plotResiduals (mdl) creates a probability density histogram of the raw residuals. Bin width follows Scott's rule h = 3.5 \hat\sigma n^{-1/3} and is rounded to a visually clean value. The bar areas sum to 1. plotResiduals (mdl, plottype) creates the type of residual plot given by plottype. For all types except "histogram" and "probability", the full observation vector including excluded rows is passed to the plot. Excluded or missing rows appear as NaN in the plotted data and produce visible gaps. plottype must be one of: 'histogram' (default) Probability density histogram. Only active observations are used. Returns one patch handle. Accepts FaceColor, EdgeColor, FaceAlpha, and LineWidth Name-Value arguments. 'fitted' Residuals on the y-axis against fitted values on the x-axis. A dotted horizontal reference line marks y = 0. Returns two line handles: h(1) is the data scatter and h(2) is the reference line. 'caseorder' Residuals on the y-axis against observation row number on the x-axis, covering all rows from 1 to n_total. A dotted horizontal reference line marks y = 0. Returns two line handles: h(1) is the data and h(2) is the reference line. 'lagged' Each residual r(t) on the y-axis against the preceding residual r(t-1) on the x-axis. Two dotted reference lines mark y = 0 and x = 0. Returns three line handles: h(1) is the scatter, h(2) is the horizontal reference, and h(3) is the vertical reference. 'probability' Normal probability plot of the sorted active residuals produced by normplot. Returns two handles: h(1) is the data line and h(2) is the fitted reference line produced by normplot. Name-Value arguments are not applied for this plot type. 'observed' Observed response values on the y-axis against fitted values on the x-axis. A dotted y = x reference line is drawn through the origin. Vertical segments connect each observed point down to the reference line. Returns three handles: h(1) is the scatter, h(2) is the y = x reference, and h(3) is the vertical segment line (stored as a single NaN-separated line object). 'symmetry' Upper-tail distances from the median plotted against lower-tail distances from the median. Each point (x, y) satisfies x = \mathrm{med} - r_{(i)} and y = r_{(n+1-i)} - \mathrm{med}, using the \lfloor n/2 \rfloor most extreme observations on each side. A perfectly symmetric distribution falls on the dotted y = x reference line. Returns two handles: h(1) is the scatter and h(2) is the reference line. plotResiduals (ax, ...) targets the axes object ax instead of the current axes returned by gca. h = plotResiduals (...) returns a vector of graphics handles. The number of handles depends on plottype as described above. Name-Value arguments are applied to the data handle h(1) only. Reference lines are always drawn with the default style and are not affected by Name-Value arguments. The following Name-Value arguments are accepted. Arguments marked histogram only are passed directly to the patch object and have no effect on other plot types. Arguments marked non-histogram are applied to the scatter marker and have no effect on the histogram. Name Description and default 'ResidualType' Type of residual to plot. One of 'raw' (default), 'pearson', 'standardized', or 'studentized'. Case-insensitive. Selects the corresponding column of mdl.Residuals. 'Color' (non-histogram) Marker color. Default: [0.1490 0.5490 0.8660]. 'Marker' (non-histogram) Marker symbol. Any symbol accepted by plot is valid. Default: 'x'. 'MarkerSize' (non-histogram) Marker size in points. Default: 6. 'MarkerEdgeColor' (non-histogram) Marker edge color. Default: 'auto'. 'MarkerFaceColor' (non-histogram) Marker fill color. Default: 'none'. 'LineWidth' (non-histogram) Width of the marker edge in points. Default: 0.5. 'FaceColor' (histogram only) Fill color of the histogram bars. Default: [0.1490 0.5490 0.8660]. 'EdgeColor' (histogram only) Edge color of the histogram bars. 'FaceAlpha' (histogram only) Transparency of the histogram bars, specified as a scalar in [0, 1]. # name: # type: sq_string # elements: 1 # length: 51 Plot residuals of a fitted linear regression model. # name: # type: sq_string # elements: 1 # length: 19 LinearModel.predict # name: # type: sq_string # elements: 1 # length: 1872 LinearModel: ypred = predict (mdl, Xnew) LinearModel: ypred = predict (mdl) LinearModel: [ypred, yci] = predict (mdl, Xnew) LinearModel: [ypred, yci] = predict (mdl, Xnew, Name, Value) Predict responses from a fitted linear regression model. ypred = predict (mdl, Xnew) returns the fitted response values at the new predictor locations in Xnew. Xnew can be a numeric matrix with one column per predictor in the same order as the training data, or a table whose column names match mdl.PredictorNames. Rows containing NaN are returned as NaN without error. ypred = predict (mdl) omits Xnew and returns fitted values for the original training observations in their original row order. Rows that were excluded or contained missing values are returned as NaN. The result is identical to mdl.Fitted. [ypred, yci] = predict (...) also returns yci, an n-by-2 matrix of confidence bounds where column 1 is the lower bound and column 2 is the upper bound. By default these are 95% pointwise confidence intervals on the mean response. Name-Value pair arguments: Name Value 'Alpha' Significance level for the confidence interval, specified as a scalar in [0,1]. The interval has coverage 100(1-\alpha)\%. Default is 0.05, giving a 95% interval. 'Prediction' Type of interval to compute. "curve" (default) gives a confidence interval on the mean response f(x). "observation" gives a wider prediction interval for a single future observation y = f(x) + \varepsilon, which accounts for both estimation uncertainty and irreducible noise; it adds mdl.MSE to the variance before computing the half-width. 'Simultaneous' Logical flag controlling whether the bounds are simultaneous or pointwise. When true, Scheff'{e}'s method is used so the entire predicted curve lies within the band with 100(1-\alpha)\% confidence; these bands are always wider than pointwise ones. Default is false. # name: # type: sq_string # elements: 1 # length: 56 Predict responses from a fitted linear regression model. # name: # type: sq_string # elements: 1 # length: 18 LinearModel.random # name: # type: sq_string # elements: 1 # length: 1108 LinearModel: ysim = random (mdl, Xnew) Simulate responses with random noise from a fitted linear regression model. ysim = random (mdl, Xnew) computes the fitted response at each row of Xnew and then adds independent Gaussian noise to each value. The noise is drawn from N(0, \sigma^2) where \sigma^2 is the estimated error variance mdl.MSE (mean squared error of the fit). The result is a column vector of the same length as the number of rows in Xnew. Xnew is required and must be non-empty. It can be a numeric matrix with one column per predictor in the same order as the training data, or a table whose column names match mdl.PredictorNames. Unlike predict, there is no no-argument form; the predictor locations must always be supplied explicitly. Because the added noise is drawn freshly on every call, two calls with the same Xnew will generally produce different output. To get reproducible results, set the random seed with rand ('state', s) before calling random. For deterministic predictions without noise, use predict or feval. predict also provides confidence intervals on the mean response. # name: # type: sq_string # elements: 1 # length: 75 Simulate responses with random noise from a fitted linear regression model. # name: # type: sq_string # elements: 1 # length: 23 LinearModel.removeTerms # name: # type: sq_string # elements: 1 # length: 1883 LinearModel: NewMdl = removeTerms (mdl, terms) Remove terms from a fitted linear regression model. removeTerms returns a new LinearModel refitted on the same data and settings as mdl, but with the specified terms dropped from the model formula. The original model mdl is never modified; all settings including observation weights, excluded rows, and categorical variable encodings are carried over automatically. To update a model in place, reassign the result: mdl = removeTerms (mdl, terms). terms may be a character vector in Wilkinson notation. Use 'x2' to remove a main effect, 'x1:x2' to remove an interaction, '1' to remove the intercept, or 'x1 + x2^2' to remove several terms at once. A bare power term 'x1^2' removes x1 together with x1^2 (and any intermediate powers), matching the Wilkinson hierarchy convention; power notation used inside an interaction, e.g. 'x1:x2^2', removes only that exact interaction term. The star operator 'x1*x2' removes the main effects x1 and x2 together with their interaction x1:x2 in a single call, following the same expansion rule as addTerms. All variable names must match entries in mdl.PredictorNames. terms may also be a numeric matrix of size t-by-v, where t is the number of terms to remove and v equals mdl.NumVariables. Entry T(i,j) is the exponent of variable j in term i. For example, in a model with variables x1, x2, y: [0 0 0] is the intercept, [0 1 0] is x2, [1 1 0] is x1:x2, and [2 0 0] is x1^2. A matrix with mdl.NumPredictors columns is also accepted and is automatically padded with a trailing zero column for the response. Terms specified but absent from mdl are silently skipped. A warning is issued and mdl is returned unchanged only when every single specified term is absent from the model. For a categorical predictor, removeTerms removes the full group of indicator variables for that predictor in one step. # name: # type: sq_string # elements: 1 # length: 51 Remove terms from a fitted linear regression model. # name: # type: sq_string # elements: 1 # length: 16 LinearModel.step # name: # type: sq_string # elements: 1 # length: 943 LinearModel: NewMdl = step (mdl) LinearModel: NewMdl = step (mdl, Name, Value) Improve a fitted linear regression model by one or more steps of stepwise term selection. step examines whether adding or removing a single term from mdl improves the fit, and returns the resulting model as NewMdl. The original model mdl is never modified. Unlike stepwiselm, step performs only one such improvement step by default; pass 'NSteps' to allow more. step accepts the same 'Criterion', 'PEnter', 'PRemove', 'NSteps', 'Verbose', 'Lower', and 'Upper' Name-Value options as stepwiselm, with the same defaults, except 'NSteps' defaults to 1 rather than unlimited. mdl's own predictors, weights, excluded observations, and categorical variable settings are carried over automatically as the starting point for the search. step is not available for a model fitted with robust regression. See also: stepwiselm, LinearModel.addTerms, LinearModel.removeTerms # name: # type: sq_string # elements: 1 # length: 89 Improve a fitted linear regression model by one or more steps of stepwise term selection. # name: # type: sq_string # elements: 1 # length: 21 MultinomialRegression # name: # type: sq_string # elements: 1 # length: 1439 statistics: MultinomialRegression Multinomial regression model class. A MultinomialRegression object holds a multinomial regression of a categorical response on one or more predictors, fitted by mnrfit. It is normally created with the fitmnr function. The response has K categories, listed in ClassNames, and the model is one of three kinds, named by ModelType: a nominal model contrasts each category with the last, an ordinal model fits the cumulative probabilities of ordered categories, and a hierarchical model fits each category against the ones after it. The most useful properties are Coefficients (a table of estimates, standard errors, t-statistics and p-values), Deviance, Fitted, Residuals, LogLikelihood, ModelCriterion and Rsquared. ObservationInfo records which rows were weighted or missing, and Variables holds the data the model was built from. A fitted model supports predict, feval and random, inference on its coefficients through coefCI, coefTest and testDeviance, and partial dependence through partialDependence and plotPartialDependence. Fitted, Residuals and ObservationInfo have one row per input row. A row left out of the fit because a value it needs is missing has NaN residuals and a missing label in Fitted, the empty character vector, , or NaN as the type of ClassNames has it; MATLAB labels such a row with the first class. See also: fitmnr, mnrfit, mnrval, GeneralizedLinearModel # name: # type: sq_string # elements: 1 # length: 35 Multinomial regression model class. # name: # type: sq_string # elements: 1 # length: 32 MultinomialRegression.ClassNames # name: # type: sq_string # elements: 1 # length: 820 MultinomialRegression: property ClassNames Names of the response categories A K-by-1 array in the type of the response: numeric, logical, a cell array of character vectors, a string or a categorical array. A character matrix response gives a cell array, and a matrix of counts the numbers of its columns. The categories of a numeric, logical or text response are sorted, and those of a categorical array are in the order of its categories. A category without observations is no class. The last class is the reference category of a nominal model. The model is fitted in this order, so Fitted and predict name the class they estimate; R2024a sorts the classes of text but fits them in the order the data first shows them, so its labels name the wrong classes unless the data show them sorted. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Names of the response categories # name: # type: sq_string # elements: 1 # length: 43 MultinomialRegression.CoefficientCovariance # name: # type: sq_string # elements: 1 # length: 332 MultinomialRegression: property CoefficientCovariance Covariance matrix of the coefficient estimates A square matrix with one row and column per coefficient, in the order of CoefficientNames. It is scaled by Dispersion, so it is the covariance under the estimated dispersion wherever one was estimated. This property is read-only. # name: # type: sq_string # elements: 1 # length: 46 Covariance matrix of the coefficient estimates # name: # type: sq_string # elements: 1 # length: 38 MultinomialRegression.CoefficientNames # name: # type: sq_string # elements: 1 # length: 445 MultinomialRegression: property CoefficientNames Names of the coefficients A string column, one name per coefficient. A coefficient of one category joins the term to the category, (Intercept_class) and term_class; a slope shared by every category is named by its term alone. A categorical predictor contributes one term per level bar the first, spelled name_level, and an interaction joins its factors with a colon. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Names of the coefficients # name: # type: sq_string # elements: 1 # length: 34 MultinomialRegression.Coefficients # name: # type: sq_string # elements: 1 # length: 487 MultinomialRegression: property Coefficients Coefficient values A table with one row per coefficient and four columns: Value - estimated coefficient value SE - standard error of the estimate tStat - the estimate divided by its standard error pValue - p-value of that statistic The statistic is referred to the normal distribution, or to a t-distribution on DFE degrees of freedom where the dispersion is estimated. The row names are the coefficient names. This property is read-only. # name: # type: sq_string # elements: 1 # length: 18 Coefficient values # name: # type: sq_string # elements: 1 # length: 25 MultinomialRegression.DFE # name: # type: sq_string # elements: 1 # length: 259 MultinomialRegression: property DFE Error degrees of freedom The number of rows used in the fit times K-1, less the number of coefficients; for a hierarchical model each stage counts only the rows with observations that reach it. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Error degrees of freedom # name: # type: sq_string # elements: 1 # length: 30 MultinomialRegression.Deviance # name: # type: sq_string # elements: 1 # length: 304 MultinomialRegression: property Deviance Deviance of the fit A nonnegative scalar, twice the difference between the log-likelihood of the saturated model and that of the fitted one. The difference between the deviances of two nested models follows a chi-square distribution. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Deviance of the fit # name: # type: sq_string # elements: 1 # length: 32 MultinomialRegression.Dispersion # name: # type: sq_string # elements: 1 # length: 370 MultinomialRegression: property Dispersion Dispersion parameter A positive scalar, 1 unless EstimateDispersion is true. The estimate is the square of the 'sfit' that mnrfit returns, the Pearson statistic over the categories divided by DFE, so it differs from MATLAB's, as mnrfit documents. The standard errors are scaled by its square root. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Dispersion parameter # name: # type: sq_string # elements: 1 # length: 40 MultinomialRegression.EstimateDispersion # name: # type: sq_string # elements: 1 # length: 135 MultinomialRegression: property EstimateDispersion Whether the dispersion was estimated A logical scalar. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Whether the dispersion was estimated # name: # type: sq_string # elements: 1 # length: 28 MultinomialRegression.Fitted # name: # type: sq_string # elements: 1 # length: 226 MultinomialRegression: property Fitted Fitted response values An N-by-1 array of the most probable class of each input row, in the type of ClassNames; a row left out of the fit is a missing label. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Fitted response values # name: # type: sq_string # elements: 1 # length: 29 MultinomialRegression.Formula # name: # type: sq_string # elements: 1 # length: 202 MultinomialRegression: property Formula Model formula A LinearFormula object over the variables of the model, with the link on the response, such as logit(y) ~ 1 + x1 + x2. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Model formula # name: # type: sq_string # elements: 1 # length: 46 MultinomialRegression.IncludeClassInteractions # name: # type: sq_string # elements: 1 # length: 270 MultinomialRegression: property IncludeClassInteractions Whether each category has its own slopes A logical scalar: true when each category has its own slopes, false when every category shares them. The intercepts always differ by category. This property is read-only. # name: # type: sq_string # elements: 1 # length: 40 Whether each category has its own slopes # name: # type: sq_string # elements: 1 # length: 26 MultinomialRegression.Link # name: # type: sq_string # elements: 1 # length: 189 MultinomialRegression: property Link Link function A character vector, 'logit', 'probit', 'comploglog' or 'loglog'. A nominal model always uses the logit link. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Link function # name: # type: sq_string # elements: 1 # length: 35 MultinomialRegression.LogLikelihood # name: # type: sq_string # elements: 1 # length: 210 MultinomialRegression: property LogLikelihood Log-likelihood of the fit A scalar, -Deviance / 2: the log-likelihood measured from that of the saturated model, as MATLAB reports it. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Log-likelihood of the fit # name: # type: sq_string # elements: 1 # length: 36 MultinomialRegression.ModelCriterion # name: # type: sq_string # elements: 1 # length: 412 MultinomialRegression: property ModelCriterion Information criteria A structure with fields AIC, AICc, BIC and CAIC, computed from LogLikelihood, the number of coefficients m and the number of rows n used in the fit: -2 * LogLikelihood + 2 * m, its small-sample correction + 2 * m * (m + 1) / (n - m - 1), -2 * LogLikelihood + m * log (n) and -2 * LogLikelihood + m * (log (n) + 1). This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Information criteria # name: # type: sq_string # elements: 1 # length: 31 MultinomialRegression.ModelType # name: # type: sq_string # elements: 1 # length: 149 MultinomialRegression: property ModelType Type of the model A character vector, 'nominal', 'ordinal' or 'hierarchical'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 17 Type of the model # name: # type: sq_string # elements: 1 # length: 43 MultinomialRegression.MultinomialRegression # name: # type: sq_string # elements: 1 # length: 521 MultinomialRegression: mdl = MultinomialRegression (X, Y) MultinomialRegression: mdl = MultinomialRegression (Tbl, Y) MultinomialRegression: mdl = MultinomialRegression (Tbl, ResponseVarName) MultinomialRegression: mdl = MultinomialRegression (Tbl, Formula) MultinomialRegression: mdl = MultinomialRegression (..., name, value) Fit a multinomial regression model. This is the constructor that fitmnr calls, and it takes exactly what fitmnr takes. See fitmnr for the arguments and the name, value pairs. See also: fitmnr # name: # type: sq_string # elements: 1 # length: 35 Fit a multinomial regression model. # name: # type: sq_string # elements: 1 # length: 37 MultinomialRegression.NumCoefficients # name: # type: sq_string # elements: 1 # length: 120 MultinomialRegression: property NumCoefficients Number of coefficients A positive integer. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of coefficients # name: # type: sq_string # elements: 1 # length: 37 MultinomialRegression.NumObservations # name: # type: sq_string # elements: 1 # length: 281 MultinomialRegression: property NumObservations Number of observations The number of input rows used in the fit, those left out because a value was missing not counted. A row of a matrix of counts counts once, whatever number of observations it holds. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 35 MultinomialRegression.NumPredictors # name: # type: sq_string # elements: 1 # length: 138 MultinomialRegression: property NumPredictors Number of predictors The number of predictors used in the fit. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of predictors # name: # type: sq_string # elements: 1 # length: 34 MultinomialRegression.NumVariables # name: # type: sq_string # elements: 1 # length: 200 MultinomialRegression: property NumVariables Number of variables The number of variables in the input data, the response and any variable the model does not use included. This property is read-only. # name: # type: sq_string # elements: 1 # length: 19 Number of variables # name: # type: sq_string # elements: 1 # length: 37 MultinomialRegression.ObservationInfo # name: # type: sq_string # elements: 1 # length: 307 MultinomialRegression: property ObservationInfo Information about the observations A table with one row per input row and three variables: Weights, the observation weight, Missing, true where a value the fit needs is missing, and Subset, true where the row was used in the fit. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Information about the observations # name: # type: sq_string # elements: 1 # length: 38 MultinomialRegression.ObservationNames # name: # type: sq_string # elements: 1 # length: 221 MultinomialRegression: property ObservationNames Names of the observations The row names of an input table, as a cell array of character vectors, or an empty cell array where there were none. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Names of the observations # name: # type: sq_string # elements: 1 # length: 36 MultinomialRegression.PredictorNames # name: # type: sq_string # elements: 1 # length: 170 MultinomialRegression: property PredictorNames Names of the predictors A cell array of character vectors, one per predictor used in the fit. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Names of the predictors # name: # type: sq_string # elements: 1 # length: 31 MultinomialRegression.Residuals # name: # type: sq_string # elements: 1 # length: 412 MultinomialRegression: property Residuals Residuals of the fit A table with one row per input row and three variables, Raw, Pearson and Deviance, the 'resid', 'residp' and 'residd' that mnrfit returns and documents. Raw and Pearson have a column per category of a nominal model, and one fewer for an ordinal or hierarchical model; Deviance has one. A row left out of the fit is NaN. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Residuals of the fit # name: # type: sq_string # elements: 1 # length: 34 MultinomialRegression.ResponseName # name: # type: sq_string # elements: 1 # length: 131 MultinomialRegression: property ResponseName Name of the response A character vector, 'y' by default. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Name of the response # name: # type: sq_string # elements: 1 # length: 30 MultinomialRegression.Rsquared # name: # type: sq_string # elements: 1 # length: 275 MultinomialRegression: property Rsquared Pseudo R-squared values A structure with fields Ordinary, 1 - L / L0, and Adjusted, 1 - (L - m) / L0, where L is LogLikelihood, L0 that of the model with intercepts alone and m the number of coefficients. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Pseudo R-squared values # name: # type: sq_string # elements: 1 # length: 34 MultinomialRegression.VariableInfo # name: # type: sq_string # elements: 1 # length: 392 MultinomialRegression: property VariableInfo Information about the variables A table with one row per variable and four variables: Class, the class of the variable, Range, [min, max] of a continuous variable or the levels of a categorical one, InModel, true for a predictor used in the fit, and IsCategorical. The response is not in the model and is categorical. This property is read-only. # name: # type: sq_string # elements: 1 # length: 31 Information about the variables # name: # type: sq_string # elements: 1 # length: 35 MultinomialRegression.VariableNames # name: # type: sq_string # elements: 1 # length: 196 MultinomialRegression: property VariableNames Names of the variables A cell array of character vectors naming every variable of the input data, the response included. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Names of the variables # name: # type: sq_string # elements: 1 # length: 31 MultinomialRegression.Variables # name: # type: sq_string # elements: 1 # length: 169 MultinomialRegression: property Variables Input data A table of the predictors and the response, with any other variable of an input table. This property is read-only. # name: # type: sq_string # elements: 1 # length: 10 Input data # name: # type: sq_string # elements: 1 # length: 28 MultinomialRegression.coefCI # name: # type: sq_string # elements: 1 # length: 433 MultinomialRegression: ci = coefCI (mdl) MultinomialRegression: ci = coefCI (mdl, alpha) Confidence intervals for the coefficients. ci = coefCI (mdl) returns a p-by-2 matrix, one row per coefficient in the order of CoefficientNames, of the lower and upper limits of its 95% confidence interval, Value +/- tinv (1 - alpha / 2, DFE) * SE. alpha, in (0, 1), sets the level to 100 (1 - alpha)%. See also: MultinomialRegression.coefTest # name: # type: sq_string # elements: 1 # length: 42 Confidence intervals for the coefficients. # name: # type: sq_string # elements: 1 # length: 30 MultinomialRegression.coefTest # name: # type: sq_string # elements: 1 # length: 690 MultinomialRegression: p = coefTest (mdl) MultinomialRegression: p = coefTest (mdl, H) MultinomialRegression: p = coefTest (mdl, H, C) MultinomialRegression: [p, F, r] = coefTest (...) Linear hypothesis test on the coefficients. p = coefTest (mdl, H, C) tests the hypothesis that H * b = C, where b is the column of coefficients in the order of CoefficientNames. H is a full-rank r-by-p matrix and C a vector of r elements, zero by default; without H the hypothesis is that every coefficient is zero. F is the Wald statistic divided by r, and p its p-value from the F distribution on r and DFE degrees of freedom. See also: MultinomialRegression.coefCI, MultinomialRegression.testDeviance # name: # type: sq_string # elements: 1 # length: 43 Linear hypothesis test on the coefficients. # name: # type: sq_string # elements: 1 # length: 27 MultinomialRegression.feval # name: # type: sq_string # elements: 1 # length: 476 MultinomialRegression: label = feval (mdl, X1, ..., Xp) MultinomialRegression: label = feval (mdl, Xnew) Predict the response of a multinomial regression model. label = feval (mdl, X1, ..., Xp) takes one argument per predictor, each a column vector or a scalar, and a scalar stands for every row. label = feval (mdl, Xnew) takes a table or a matrix, as predict does. Either returns what predict returns first, the most probable class. See also: MultinomialRegression.predict # name: # type: sq_string # elements: 1 # length: 55 Predict the response of a multinomial regression model. # name: # type: sq_string # elements: 1 # length: 35 MultinomialRegression.plotResiduals # name: # type: sq_string # elements: 1 # length: 1832 MultinomialRegression: plotResiduals (mdl) MultinomialRegression: plotResiduals (mdl, plotType) MultinomialRegression: plotResiduals (ax, ...) MultinomialRegression: plotResiduals (..., name, value) MultinomialRegression: h = plotResiduals (...) Plot the residuals of a multinomial regression model. plotResiduals (mdl) draws a histogram of the deviance residuals, as a probability density. plotType chooses the plot: Type Plot 'histogram' a histogram of the residuals (default) 'caseorder' the residuals against the row number 'fitted' the residuals against the fitted probability of the class 'lagged' each residual against the one before it 'probability' a normal probability plot 'observed' the observed share of each row in the class against its fitted probability, with the line y = x and a segment from it to each row 'symmetry' the residuals above their median against those below it The following name, value pairs are accepted: Name Value 'ResidualType' 'deviance' (default), 'raw' or 'Pearson', a variable of Residuals. 'ClassToPlot' For raw and Pearson residuals, the class whose column of residuals is plotted, and whose probability the 'fitted' and 'observed' plots show, a class named in ClassNames; the first by default. The residuals of an ordinal or hierarchical model have no column for the last class. With deviance residuals it is ignored, with a warning, and the first class shown. 'FaceColor', 'FaceAlpha', 'LineWidth' The fill color, transparency and edge width of the bars of a histogram. 'Color', 'LineStyle', 'Marker' The color, line style and marker of the residuals in the other plots. ax is the axes to draw in, the current axes by default. h is the patch of a histogram, and for the other plots a column of the line of residuals and its reference lines. See also: MultinomialRegression.Residuals # name: # type: sq_string # elements: 1 # length: 53 Plot the residuals of a multinomial regression model. # name: # type: sq_string # elements: 1 # length: 31 MultinomialRegression.plotSlice # name: # type: sq_string # elements: 1 # length: 2263 MultinomialRegression: plotSlice (mdl) MultinomialRegression: plotSlice (mdl, Xnew) MultinomialRegression: plotSlice (mdl, plotType) MultinomialRegression: plotSlice (mdl, plotType, Xnew) MultinomialRegression: plotSlice (ax, ...) MultinomialRegression: plotSlice (..., name, value) MultinomialRegression: h = plotSlice (...) Plot slices through the fitted probabilities of a multinomial regression model. A slice varies one continuous predictor over its range and holds every other predictor fixed, a categorical one at its most common level. The ranges and the fixed values are taken from the data the model was fitted on, or from Xnew, a table or a matrix as predict takes it; a single row of Xnew gives each predictor the range from its value to 10% above it. plotType chooses the plot: Type Plot 'interactive' (default) a figure with one plot per continuous predictor, each the probability of one class along that predictor, the others fixed at the middle of their ranges, with its 95% confidence band shaded and dashed and dotted lines through the middle. 'histogram' overlaid bars of the probability of each class at 40 values along one predictor, the others fixed at their means. 'stackedhist' the same probabilities as stacked bars. MATLAB's interactive plot is an app whose menus and boxes change the class, the predictors and the fixed values; here the figure has no such controls, and the plot is drawn again to change them. The following name, value pairs are accepted: Name Value 'ClassToPlot' For the interactive plot, the class whose probability is drawn, the first of ClassNames by default. 'PredictorToVary' For the histograms, the name of the continuous predictor to vary, the first by default. 'FaceColor', 'FaceAlpha', 'LineWidth' The fill color ('auto' by default, a color per class), the transparency (0.6) and the edge width (0.5) of the bars of a histogram. 'EdgeColor', 'LineStyle' The edge color and line style of the stacked bars, black up to 150 bars and none beyond, and a solid line. ax is the axes to draw a histogram in, the current axes by default. h is the figure of the interactive plot, and for the histograms a column of one patch or bar object per class. See also: MultinomialRegression.predict, plotPartialDependence # name: # type: sq_string # elements: 1 # length: 79 Plot slices through the fitted probabilities of a multinomial regression model. # name: # type: sq_string # elements: 1 # length: 29 MultinomialRegression.predict # name: # type: sq_string # elements: 1 # length: 1427 MultinomialRegression: label = predict (mdl, Xnew) MultinomialRegression: [label, probs] = predict (mdl, Xnew) MultinomialRegression: [label, probs, lower, upper] = predict (mdl, Xnew) MultinomialRegression: [...] = predict (..., name, value) Predict the response of a multinomial regression model. label = predict (mdl, Xnew) returns the most probable class at each row of the new predictor data Xnew, in the type of ClassNames. Xnew is a table holding every predictor named in PredictorNames, or a numeric matrix with one column per predictor where they are all numeric. A row holding a missing value is a missing label. probs holds the probabilities, one row per row of Xnew, and lower and upper their confidence bounds, from the delta method. The following name, value pairs are accepted: Name Value 'Alpha' The significance level of the bounds, a scalar in (0, 1); the default 0.05 gives 95% bounds. 'ProbabilityType' 'category' (default), the probability of each of the K categories; 'cumulative', the probability of each of the first K-1 categories or any before it; or 'conditional', the probability of each of the first K-1 categories given that the response is not in an earlier one. The bounds are at the confidence level 1 - Alpha. R2024a takes Alpha as the confidence level itself, so its default bounds are at 5% and far too narrow. See also: MultinomialRegression.feval, MultinomialRegression.random, mnrval # name: # type: sq_string # elements: 1 # length: 55 Predict the response of a multinomial regression model. # name: # type: sq_string # elements: 1 # length: 28 MultinomialRegression.random # name: # type: sq_string # elements: 1 # length: 677 MultinomialRegression: ysim = random (mdl, Xnew) MultinomialRegression: ysim = random (mdl, Xnew, NumTrials) MultinomialRegression: ysim = random (..., type) Simulate responses of a multinomial regression model. ysim = random (mdl, Xnew) draws one response at each row of Xnew from the probabilities the model gives there. Xnew is a table or a matrix, as predict takes it. NumTrials, a positive integer, draws that many at each row. type is 'samples' (default), for an r-by-NumTrials array of class labels, in the type of ClassNames, or 'counts', for an r-by-K table counting how many draws fell in each class, one variable per class. See also: MultinomialRegression.predict # name: # type: sq_string # elements: 1 # length: 53 Simulate responses of a multinomial regression model. # name: # type: sq_string # elements: 1 # length: 34 MultinomialRegression.testDeviance # name: # type: sq_string # elements: 1 # length: 662 MultinomialRegression: p = testDeviance (mdl) MultinomialRegression: [p, stat] = testDeviance (mdl) Deviance test against the model with intercepts alone. p = testDeviance (mdl) tests whether the model fits better than one with an intercept for each category and no predictors, comparing their deviances. Where the dispersion is fixed, stat is the drop in deviance and p its chi-square p-value on as many degrees of freedom as the model has coefficients beyond the intercepts; where it is estimated, stat is that drop per degree of freedom divided by Dispersion, and p its F p-value on those and DFE degrees of freedom. See also: MultinomialRegression.coefTest # name: # type: sq_string # elements: 1 # length: 54 Deviance test against the model with intercepts alone. # name: # type: sq_string # elements: 1 # length: 14 NonLinearModel # name: # type: sq_string # elements: 1 # length: 1752 statistics: NonLinearModel Nonlinear regression model class. A NonLinearModel object holds a nonlinear regression fitted by fitnlm, together with its coefficients, fit statistics, and methods for inference, prediction, and diagnostics. Create a NonLinearModel object with fitnlm or the class constructor. The estimated coefficients and their statistics are in the Coefficients table; Rsquared, ModelCriterion, LogLikelihood, RMSE, SSE, SST, and SSR summarize the fit. The methods predict, feval, random, coefCI, coefTest, plotResiduals, plotDiagnostics, and plotSlice operate on the fitted model. Fit statistics The fit statistics follow MATLAB's conventions. SSE is the residual sum of squares, SST the total sum of squares of the response about its (weighted) mean, and SSR the regression sum of squares of the fitted values about that mean; because the model is nonlinear, SST does not in general equal SSR + SSE. Rsquared.Ordinary is 1 - SSE / SST and Rsquared.Adjusted corrects for the error degrees of freedom. RMSE is sqrt (MSE), and the Gaussian LogLikelihood uses the maximum-likelihood error variance SSE / n. The information criteria in ModelCriterion (AIC, AICc, BIC, CAIC) count the p coefficients as the only parameters; the error variance is not counted. coefTest is a Wald test: for a contrast matrix H it forms (H*b)' * inv (H*V*H') * (H*b) / r with V the coefficient covariance and r the number of rows of H, referred to an F distribution on r and DFE degrees of freedom. The summary printed by disp instead reports an F statistic versus the zero model, formed from the uncorrected regression sum of squares (the sum of the squared fitted values). See also: fitnlm, nlinfit, nlparci, nlpredci, LinearModel, GeneralizedLinearModel # name: # type: sq_string # elements: 1 # length: 33 Nonlinear regression model class. # name: # type: sq_string # elements: 1 # length: 36 NonLinearModel.CoefficientCovariance # name: # type: sq_string # elements: 1 # length: 338 NonLinearModel: property CoefficientCovariance Estimated covariance of the coefficients A square numeric matrix, one row and column per coefficient, holding the estimated covariance of the estimates in Coefficients. The square roots of its diagonal are the standard errors reported in column SE of that table. This property is read-only. # name: # type: sq_string # elements: 1 # length: 40 Estimated covariance of the coefficients # name: # type: sq_string # elements: 1 # length: 31 NonLinearModel.CoefficientNames # name: # type: sq_string # elements: 1 # length: 293 NonLinearModel: property CoefficientNames Names of the coefficients A cell array of character vectors with one name per coefficient, in the order the model function expects them. The names default to 'b1', 'b2' and so on, unless fitnlm was given a list of its own. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Names of the coefficients # name: # type: sq_string # elements: 1 # length: 27 NonLinearModel.Coefficients # name: # type: sq_string # elements: 1 # length: 376 NonLinearModel: property Coefficients Coefficient estimates and their statistics A table with one row per coefficient, its row names taken from CoefficientNames, and the variables Estimate, SE, tStat and pValue. tStat is the estimate divided by its standard error and pValue is the two-sided t test of a zero coefficient on DFE degrees of freedom. This property is read-only. # name: # type: sq_string # elements: 1 # length: 42 Coefficient estimates and their statistics # name: # type: sq_string # elements: 1 # length: 18 NonLinearModel.DFE # name: # type: sq_string # elements: 1 # length: 143 NonLinearModel: property DFE Error degrees of freedom A nonnegative integer, NumObservations less NumCoefficients. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Error degrees of freedom # name: # type: sq_string # elements: 1 # length: 21 NonLinearModel.Fitted # name: # type: sq_string # elements: 1 # length: 163 NonLinearModel: property Fitted Fitted response values A numeric column vector with one fitted value per observation used for the fit. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Fitted response values # name: # type: sq_string # elements: 1 # length: 22 NonLinearModel.Formula # name: # type: sq_string # elements: 1 # length: 209 NonLinearModel: property Formula Model formula A character vector showing the fitted model, built from the model function together with the coefficient names and the response name. This property is read-only. # name: # type: sq_string # elements: 1 # length: 13 Model formula # name: # type: sq_string # elements: 1 # length: 28 NonLinearModel.LogLikelihood # name: # type: sq_string # elements: 1 # length: 240 NonLinearModel: property LogLikelihood Log-likelihood of the fitted model A scalar, the Gaussian log-likelihood at the estimates, formed with the maximum-likelihood error variance SSE divided by NumObservations. This property is read-only. # name: # type: sq_string # elements: 1 # length: 34 Log-likelihood of the fitted model # name: # type: sq_string # elements: 1 # length: 18 NonLinearModel.MSE # name: # type: sq_string # elements: 1 # length: 144 NonLinearModel: property MSE Mean squared error A positive scalar holding the estimated variance of the error term. This property is read-only. # name: # type: sq_string # elements: 1 # length: 18 Mean squared error # name: # type: sq_string # elements: 1 # length: 29 NonLinearModel.ModelCriterion # name: # type: sq_string # elements: 1 # length: 240 NonLinearModel: property ModelCriterion Information criteria A scalar structure with the fields AIC, AICc, BIC and CAIC. All four count the coefficients as the only parameters; the error variance is not counted. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Information criteria # name: # type: sq_string # elements: 1 # length: 29 NonLinearModel.NonLinearModel # name: # type: sq_string # elements: 1 # length: 1514 NonLinearModel: mdl = NonLinearModel (X, y, modelfun, beta0) NonLinearModel: mdl = NonLinearModel (tbl, [], modelfun, beta0) NonLinearModel: mdl = NonLinearModel (..., Name, Value) Fit a nonlinear regression model. mdl = NonLinearModel (X, y, modelfun, beta0) fits the nonlinear regression model y = modelfun (beta, X) to the response vector y and the n-by-p predictor matrix X, starting the iterative fit from the coefficient vector beta0. modelfun is a function handle @(b, X) returning the fitted responses. mdl = NonLinearModel (tbl, [], modelfun, beta0) takes the predictors and response from the table tbl; the last column is the response unless overridden by 'ResponseVar'. The following Name/Value pairs are accepted: Name Value 'CoefficientNames' a cell array of names for the coefficients (default 'b1', 'b2', ...). 'Weights' a vector of nonnegative observation weights. 'ErrorModel' the error-variance model: 'constant' (default), 'proportional', or 'combined'. 'RobustWgtFun' the name of a robust weight function, enabling robust fitting (see nlinfit). 'Options' a statset-style options structure controlling the iterative fit (MaxIter, TolFun, TolX). 'PredictorVars', 'ResponseVar' for table input, the predictor and response variable names. 'VarNames' a cell array of p + 1 variable names (predictors followed by the response) for numeric X. 'Exclude' observations to exclude from the fit. fitnlm makes the same fit and takes a table without the empty second argument. See also: fitnlm, nlinfit # name: # type: sq_string # elements: 1 # length: 33 Fit a nonlinear regression model. # name: # type: sq_string # elements: 1 # length: 30 NonLinearModel.NumCoefficients # name: # type: sq_string # elements: 1 # length: 224 NonLinearModel: property NumCoefficients Number of coefficients A positive integer counting the coefficients of the model, which is the number of elements of the starting vector handed to fitnlm. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of coefficients # name: # type: sq_string # elements: 1 # length: 39 NonLinearModel.NumEstimatedCoefficients # name: # type: sq_string # elements: 1 # length: 269 NonLinearModel: property NumEstimatedCoefficients Number of estimated coefficients A positive integer counting the coefficients estimated from the data. A nonlinear fit estimates every coefficient it carries, so this equals NumCoefficients. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Number of estimated coefficients # name: # type: sq_string # elements: 1 # length: 30 NonLinearModel.NumObservations # name: # type: sq_string # elements: 1 # length: 245 NonLinearModel: property NumObservations Number of observations A positive integer counting the observations used for the fit, after the rows named by 'Exclude' and the rows carrying missing values have been dropped. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Number of observations # name: # type: sq_string # elements: 1 # length: 28 NonLinearModel.NumPredictors # name: # type: sq_string # elements: 1 # length: 203 NonLinearModel: property NumPredictors Number of predictors A positive integer counting the predictor variables, that is the columns of the predictor matrix used for the fit. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Number of predictors # name: # type: sq_string # elements: 1 # length: 29 NonLinearModel.PredictorNames # name: # type: sq_string # elements: 1 # length: 280 NonLinearModel: property PredictorNames Names of the predictor variables A cell array of character vectors with one name per predictor. The names default to 'x1', 'x2' and so on for a fit from matrices, and are the column names for a fit from a table. This property is read-only. # name: # type: sq_string # elements: 1 # length: 32 Names of the predictor variables # name: # type: sq_string # elements: 1 # length: 19 NonLinearModel.RMSE # name: # type: sq_string # elements: 1 # length: 125 NonLinearModel: property RMSE Root mean squared error A positive scalar, the square root of MSE. This property is read-only. # name: # type: sq_string # elements: 1 # length: 23 Root mean squared error # name: # type: sq_string # elements: 1 # length: 24 NonLinearModel.Residuals # name: # type: sq_string # elements: 1 # length: 209 NonLinearModel: property Residuals Residuals of the fitted model A table with one row per observation used for the fit and the variables Raw, Pearson, Standardized and Studentized. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Residuals of the fitted model # name: # type: sq_string # elements: 1 # length: 27 NonLinearModel.ResponseName # name: # type: sq_string # elements: 1 # length: 242 NonLinearModel: property ResponseName Name of the response variable A character vector naming the response. It defaults to 'y' for a fit from matrices and is the name of the response column for a fit from a table. This property is read-only. # name: # type: sq_string # elements: 1 # length: 29 Name of the response variable # name: # type: sq_string # elements: 1 # length: 21 NonLinearModel.Robust # name: # type: sq_string # elements: 1 # length: 244 NonLinearModel: property Robust Robust fitting options Empty when the model was fitted by ordinary least squares. When fitnlm was given a robust weight function, a scalar structure whose field RobustWgtFun names it. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Robust fitting options # name: # type: sq_string # elements: 1 # length: 23 NonLinearModel.Rsquared # name: # type: sq_string # elements: 1 # length: 262 NonLinearModel: property Rsquared Coefficient of determination A scalar structure with the fields Ordinary and Adjusted. Ordinary is one less the ratio of SSE to SST, and Adjusted corrects that ratio for the error degrees of freedom. This property is read-only. # name: # type: sq_string # elements: 1 # length: 28 Coefficient of determination # name: # type: sq_string # elements: 1 # length: 18 NonLinearModel.SSE # name: # type: sq_string # elements: 1 # length: 170 NonLinearModel: property SSE Error sum of squares A nonnegative scalar, the sum of the squared residuals weighted by the observation weights. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Error sum of squares # name: # type: sq_string # elements: 1 # length: 18 NonLinearModel.SSR # name: # type: sq_string # elements: 1 # length: 206 NonLinearModel: property SSR Regression sum of squares A nonnegative scalar, the weighted sum of squared deviations of the fitted values about the weighted mean of the response. This property is read-only. # name: # type: sq_string # elements: 1 # length: 25 Regression sum of squares # name: # type: sq_string # elements: 1 # length: 18 NonLinearModel.SST # name: # type: sq_string # elements: 1 # length: 256 NonLinearModel: property SST Total sum of squares A nonnegative scalar, the weighted sum of squared deviations of the response about its weighted mean. Because the model is nonlinear, SST does not in general equal SSR plus SSE. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Total sum of squares # name: # type: sq_string # elements: 1 # length: 28 NonLinearModel.VariableNames # name: # type: sq_string # elements: 1 # length: 173 NonLinearModel: property VariableNames Names of all variables A cell array of character vectors holding PredictorNames followed by ResponseName. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Names of all variables # name: # type: sq_string # elements: 1 # length: 21 NonLinearModel.coefCI # name: # type: sq_string # elements: 1 # length: 166 NonLinearModel: ci = coefCI (mdl) NonLinearModel: ci = coefCI (mdl, alpha) Confidence intervals for the coefficients at level 100 (1 - alpha)% (default alpha = 0.05). # name: # type: sq_string # elements: 1 # length: 91 Confidence intervals for the coefficients at level 100 (1 - alpha)% (default alpha = 0.05). # name: # type: sq_string # elements: 1 # length: 23 NonLinearModel.coefTest # name: # type: sq_string # elements: 1 # length: 363 NonLinearModel: [p, F, df] = coefTest (mdl) NonLinearModel: [...] = coefTest (mdl, H) Wald test of a linear hypothesis on the coefficients. With no H it tests that all coefficients are zero (the model versus the zero model) and returns the p-value p, the F statistic F, and its numerator degrees of freedom df. H is an r-by-p contrast matrix testing H * beta = 0. # name: # type: sq_string # elements: 1 # length: 53 Wald test of a linear hypothesis on the coefficients. # name: # type: sq_string # elements: 1 # length: 20 NonLinearModel.feval # name: # type: sq_string # elements: 1 # length: 181 NonLinearModel: yhat = feval (mdl, X) Evaluate the fitted model at the predictor values X, given either as a single matrix/table or as separate column arguments (one per predictor). # name: # type: sq_string # elements: 1 # length: 143 Evaluate the fitted model at the predictor values X, given either as a single matrix/table or as separate column arguments (one per predictor). # name: # type: sq_string # elements: 1 # length: 30 NonLinearModel.plotDiagnostics # name: # type: sq_string # elements: 1 # length: 178 NonLinearModel: h = plotDiagnostics (mdl) NonLinearModel: h = plotDiagnostics (mdl, plottype) Plot fit diagnostics. plottype is 'leverage' (default) or 'cookd' (Cook's distance). # name: # type: sq_string # elements: 1 # length: 21 Plot fit diagnostics. # name: # type: sq_string # elements: 1 # length: 28 NonLinearModel.plotResiduals # name: # type: sq_string # elements: 1 # length: 191 NonLinearModel: h = plotResiduals (mdl) NonLinearModel: h = plotResiduals (mdl, plottype) Plot the model residuals. plottype is 'histogram' (default), 'fitted', 'caseorder', or 'probability'. # name: # type: sq_string # elements: 1 # length: 25 Plot the model residuals. # name: # type: sq_string # elements: 1 # length: 24 NonLinearModel.plotSlice # name: # type: sq_string # elements: 1 # length: 149 NonLinearModel: h = plotSlice (mdl) Plot the fitted response as each predictor is varied over its observed range with the others held at their means. # name: # type: sq_string # elements: 1 # length: 113 Plot the fitted response as each predictor is varied over its observed range with the others held at their means. # name: # type: sq_string # elements: 1 # length: 22 NonLinearModel.predict # name: # type: sq_string # elements: 1 # length: 420 NonLinearModel: yhat = predict (mdl, Xnew) NonLinearModel: [yhat, yci] = predict (mdl, Xnew) NonLinearModel: [...] = predict (..., Name, Value) Predict responses of the nonlinear model mdl at the new predictor values Xnew (a numeric matrix or a table). With two outputs it also returns the confidence intervals yci. Accepts 'Alpha' (default 0.05), 'Prediction' ('curve' or 'observation'), and 'Simultaneous' (a logical). # name: # type: sq_string # elements: 1 # length: 108 Predict responses of the nonlinear model mdl at the new predictor values Xnew (a numeric matrix or a table). # name: # type: sq_string # elements: 1 # length: 21 NonLinearModel.random # name: # type: sq_string # elements: 1 # length: 191 NonLinearModel: ysim = random (mdl, Xnew) Simulate responses from the fitted model at Xnew (default: the training predictors), adding Gaussian noise with the model's error standard deviation. # name: # type: sq_string # elements: 1 # length: 149 Simulate responses from the fitted model at Xnew (default: the training predictors), adding Gaussian noise with the model's error standard deviation. # name: # type: sq_string # elements: 1 # length: 21 RepeatedMeasuresModel # name: # type: sq_string # elements: 1 # length: 1071 statistics: RepeatedMeasuresModel Repeated measures model. A RepeatedMeasuresModel object holds a multivariate linear model of several responses measured on the same subjects, the repeated measures, on between-subject predictors. Each row of the data is a subject and each response a measurement taken on it; the between-subject model describes how the mean of every response depends on the predictors, and the within-subject design describes how the responses relate to one another, as the levels of one or more within-subject factors. The between-subject coefficients are estimated by least squares, one column per response, with categorical predictors in effects coding: a predictor of L levels contributes L - 1 columns named name_level, the last level coded -1 in all of them. ranova tests the within-subject effects, reporting the p-values corrected for departures from sphericity beside the uncorrected ones, epsilon gives those corrections and mauchly tests sphericity itself. Create a RepeatedMeasuresModel object with fitrm. See also: fitrm, anova2, manova1 # name: # type: sq_string # elements: 1 # length: 24 Repeated measures model. # name: # type: sq_string # elements: 1 # length: 35 RepeatedMeasuresModel.BetweenDesign # name: # type: sq_string # elements: 1 # length: 281 RepeatedMeasuresModel: property BetweenDesign Design of the between-subject predictors The table the model was fitted to, one row per subject, responses included. A subject missing a response or a predictor is kept here, although the fit leaves it out. This property is read-only. # name: # type: sq_string # elements: 1 # length: 40 Design of the between-subject predictors # name: # type: sq_string # elements: 1 # length: 40 RepeatedMeasuresModel.BetweenFactorNames # name: # type: sq_string # elements: 1 # length: 251 RepeatedMeasuresModel: property BetweenFactorNames Names of the between-subject predictors A cell row of character vectors naming the variables the between-subject model draws on, empty for a model with an intercept alone. This property is read-only. # name: # type: sq_string # elements: 1 # length: 39 Names of the between-subject predictors # name: # type: sq_string # elements: 1 # length: 34 RepeatedMeasuresModel.BetweenModel # name: # type: sq_string # elements: 1 # length: 200 RepeatedMeasuresModel: property BetweenModel Between-subject model A character vector holding the right side of the model formula, with its intercept, as in '1 + species'. This property is read-only. # name: # type: sq_string # elements: 1 # length: 21 Between-subject model # name: # type: sq_string # elements: 1 # length: 34 RepeatedMeasuresModel.Coefficients # name: # type: sq_string # elements: 1 # length: 233 RepeatedMeasuresModel: property Coefficients Estimated between-subject coefficients A table with one row per coefficient, named after the terms of the between-subject model, and one variable per response. This property is read-only. # name: # type: sq_string # elements: 1 # length: 38 Estimated between-subject coefficients # name: # type: sq_string # elements: 1 # length: 32 RepeatedMeasuresModel.Covariance # name: # type: sq_string # elements: 1 # length: 254 RepeatedMeasuresModel: property Covariance Estimated covariance of the responses A table holding the covariance of the residuals of the responses, their cross products over DFE, with rows and variables named by the responses. This property is read-only. # name: # type: sq_string # elements: 1 # length: 37 Estimated covariance of the responses # name: # type: sq_string # elements: 1 # length: 25 RepeatedMeasuresModel.DFE # name: # type: sq_string # elements: 1 # length: 174 RepeatedMeasuresModel: property DFE Error degrees of freedom The number of subjects the fit used less the number of between-subject coefficients. This property is read-only. # name: # type: sq_string # elements: 1 # length: 24 Error degrees of freedom # name: # type: sq_string # elements: 1 # length: 43 RepeatedMeasuresModel.RepeatedMeasuresModel # name: # type: sq_string # elements: 1 # length: 1720 RepeatedMeasuresModel: rm = RepeatedMeasuresModel (t, modelspec) RepeatedMeasuresModel: rm = RepeatedMeasuresModel (t, modelspec, name, value) Fit a repeated measures model. rm = RepeatedMeasuresModel (t, modelspec) fits the repeated measures in the table t on the between-subject model given by the formula modelspec, a character vector or a string scalar of the form 'responses ~ terms'. The responses are a range of the table's variables, as in 'y1-y6', which takes every variable from y1 to y6 in the table's order, a comma list, as in 'y1,y2,y3', or both. The terms are a Wilkinson formula over the other variables, as in 'species' or 'g*x', or '1' for an intercept alone. A categorical, logical, text or string variable is a categorical predictor and any other a continuous one. A subject missing a response or a predictor is left out of the fit. The following name-value arguments are accepted. Name Value 'WithinDesign' The design of the within-subject factors: a table with one row per response and one variable per factor, or a numeric vector with one element per response, which becomes a single factor Time. The default is Time holding the values 1 to k for k responses. 'WithinModel' The within-subject model: 'separatemeans', the default, which compares the means of the responses; 'orthogonalcontrasts', which tests the orthogonal polynomial trends over a single numeric within-subject factor; a formula over the within-subject factors, as in 'A*B'; or a contrast matrix with one row per response. 'orthogonalcontrasts' is refused at fitting unless the within-subject design holds a single numeric factor. MATLAB accepts the fit and refuses the model only when a test is asked of it. See also: fitrm # name: # type: sq_string # elements: 1 # length: 30 Fit a repeated measures model. # name: # type: sq_string # elements: 1 # length: 35 RepeatedMeasuresModel.ResponseNames # name: # type: sq_string # elements: 1 # length: 204 RepeatedMeasuresModel: property ResponseNames Names of the responses A cell row of character vectors naming the repeated measures, in the order of the columns of Coefficients. This property is read-only. # name: # type: sq_string # elements: 1 # length: 22 Names of the responses # name: # type: sq_string # elements: 1 # length: 34 RepeatedMeasuresModel.WithinDesign # name: # type: sq_string # elements: 1 # length: 315 RepeatedMeasuresModel: property WithinDesign Design of the within-subject factors A table with one row per response, named by the responses, and one variable per within-subject factor. Without a design given at fitting it holds a single factor Time of the values 1 to k for k responses. This property is read-only. # name: # type: sq_string # elements: 1 # length: 36 Design of the within-subject factors # name: # type: sq_string # elements: 1 # length: 39 RepeatedMeasuresModel.WithinFactorNames # name: # type: sq_string # elements: 1 # length: 183 RepeatedMeasuresModel: property WithinFactorNames Names of the within-subject factors A cell row of character vectors, the variable names of WithinDesign. This property is read-only. # name: # type: sq_string # elements: 1 # length: 35 Names of the within-subject factors # name: # type: sq_string # elements: 1 # length: 33 RepeatedMeasuresModel.WithinModel # name: # type: sq_string # elements: 1 # length: 218 RepeatedMeasuresModel: property WithinModel Within-subject model 'separatemeans', 'orthogonalcontrasts', a formula over the within-subject factors or a contrast matrix, as given at fitting. This property is read-only. # name: # type: sq_string # elements: 1 # length: 20 Within-subject model # name: # type: sq_string # elements: 1 # length: 27 RepeatedMeasuresModel.anova # name: # type: sq_string # elements: 1 # length: 1219 RepeatedMeasuresModel: tbl = anova (rm) RepeatedMeasuresModel: tbl = anova (rm, 'WithinModel', WM) Analysis of variance for between-subject effects. tbl = anova (rm) tests the between-subject terms of the repeated measures model rm on the average of the repeated measures, one univariate analysis of variance. tbl has the variables Within, the within-subject response analysed, Between, the between-subject term tested or Error, SumSq, DF, MeanSq, F and pValue. The intercept is named constant. anova (rm, 'WithinModel', WM) analyses other responses built from the repeated measures, one block of rows each: 'separatemeans', the default: the average, named Constant. 'orthogonalcontrasts': the average and the orthogonal polynomial trends over a single numeric within-subject factor. A formula over the within-subject factors: each column of its effects-coded design, scaled to unit length and named after it. A contrast matrix with one row per response: each column as given, named Contrast1, Contrast2, ... MATLAB R2024a and R2026a fail on 'separatemeans' given explicitly, though it is their documented default; here it is the default. See also: fitrm, RepeatedMeasuresModel.ranova, RepeatedMeasuresModel.manova # name: # type: sq_string # elements: 1 # length: 49 Analysis of variance for between-subject effects. # name: # type: sq_string # elements: 1 # length: 30 RepeatedMeasuresModel.coeftest # name: # type: sq_string # elements: 1 # length: 670 RepeatedMeasuresModel: tbl = coeftest (rm, A, C) RepeatedMeasuresModel: tbl = coeftest (rm, A, C, D) Linear hypothesis test on the coefficients of a repeated measures model. tbl = coeftest (rm, A, C) tests the hypothesis A B C = 0 on the coefficient matrix B of rm, A having one column per between-subject coefficient and C one row per response. tbl holds the four multivariate statistics as manova reports them: the variables Statistic, Value, F, RSquare, df1, df2 and pValue. tbl = coeftest (rm, A, C, D) tests A B C = D instead, D a scalar or a matrix with as many rows as A and as many columns as C. The default is 0. See also: fitrm, RepeatedMeasuresModel.manova # name: # type: sq_string # elements: 1 # length: 72 Linear hypothesis test on the coefficients of a repeated measures model. # name: # type: sq_string # elements: 1 # length: 29 RepeatedMeasuresModel.epsilon # name: # type: sq_string # elements: 1 # length: 688 RepeatedMeasuresModel: tbl = epsilon (rm) RepeatedMeasuresModel: tbl = epsilon (rm, C) Epsilon adjustments for repeated measures analysis of variance. tbl = epsilon (rm) returns the corrections for departures from sphericity that ranova applies to its p-values, over the contrasts between successive responses: the variables Uncorrected, which is 1, GreenhouseGeisser, HuynhFeldt and LowerBound. The Huynh-Feldt value is Lecoutre's corrected form, capped at 1, and the lower bound is 1 / p for p contrasts. tbl = epsilon (rm, C) computes them over the contrast matrix C, which must have one row per response. See also: fitrm, RepeatedMeasuresModel.ranova, RepeatedMeasuresModel.mauchly # name: # type: sq_string # elements: 1 # length: 63 Epsilon adjustments for repeated measures analysis of variance. # name: # type: sq_string # elements: 1 # length: 30 RepeatedMeasuresModel.grpstats # name: # type: sq_string # elements: 1 # length: 813 RepeatedMeasuresModel: tbl = grpstats (rm, g) RepeatedMeasuresModel: tbl = grpstats (rm, g, stats) Descriptive statistics of the repeated measures by group. tbl = grpstats (rm, g) pools the repeated measures of every subject and summarizes them at each combination of the levels of the factors named by g, a character vector or a cell array of them, each a categorical between-subject factor or a within-subject factor. tbl holds one variable per factor, GroupCount, the number of values in the group, and their mean and std. tbl = grpstats (rm, g, stats) computes the statistics named by stats instead, a character vector or a cell array of them, each one of 'mean', 'median', 'std', 'var', 'sem', 'min', 'max', 'range' and 'numel'. Missing values are left out. See also: fitrm, RepeatedMeasuresModel.margmean # name: # type: sq_string # elements: 1 # length: 57 Descriptive statistics of the repeated measures by group. # name: # type: sq_string # elements: 1 # length: 28 RepeatedMeasuresModel.manova # name: # type: sq_string # elements: 1 # length: 1623 RepeatedMeasuresModel: tbl = manova (rm) RepeatedMeasuresModel: tbl = manova (rm, name, value) RepeatedMeasuresModel: [tbl, A, C, D] = manova (...) Multivariate analysis of variance. tbl = manova (rm) tests every term of the within-subject model of rm, as it was fitted, against every between-subject term, by the four multivariate statistics. tbl has the variables Within, Between, Statistic, one of Pillai, Wilks, Hotelling and Roy, its Value, the F statistic that approximates it, RSquare, the degrees of freedom df1 and df2, and the pValue. Under 'separatemeans' the within-subject hypothesis is that of equal means, named Constant, and a contrast matrix is named Specified contrast. The name-value arguments are 'WithinModel', which takes the forms fitrm takes but 'orthogonalcontrasts', and 'By', the name of a between-subject factor, which tests the within-subject hypotheses at each of its levels in place of the between-subject terms. [tbl, A, C, D] = manova (...) also returns the hypotheses as A B C = D: A a cell column of the between-subject hypothesis matrices, C the within-subject contrast, a cell row of one per term when there are several, and D zero. Pillai's trace, Wilks' lambda and Roy's root are approximated by F as in MATLAB. The Hotelling-Lawley trace uses McKeon's F approximation with its own second degrees of freedom, as SAS does, and the Pillai-Samson one where McKeon's is undefined. MATLAB R2024a computes McKeon's F but refers it to the Pillai-Samson degrees of freedom, which makes its p-values too small. See also: fitrm, RepeatedMeasuresModel.coeftest, RepeatedMeasuresModel.ranova # name: # type: sq_string # elements: 1 # length: 34 Multivariate analysis of variance. # name: # type: sq_string # elements: 1 # length: 30 RepeatedMeasuresModel.margmean # name: # type: sq_string # elements: 1 # length: 914 RepeatedMeasuresModel: tbl = margmean (rm, vars) RepeatedMeasuresModel: tbl = margmean (rm, vars, 'Alpha', alpha) Estimated marginal means. tbl = margmean (rm, vars) estimates the mean of the repeated measures at each combination of the levels of the factors named by vars, a character vector or a cell array of them, each a categorical between-subject factor or a within-subject factor. The other between-subject factors are averaged over their levels with equal weights, a continuous predictor is held at its mean over the subjects the fit used, and the responses are averaged over the other within-subject factors. tbl holds one variable per factor, the first varying slowest, and Mean, StdErr, Lower and Upper, the limits of a 100 (1 - alpha) per cent confidence interval on DFE degrees of freedom. The default alpha is 0.05. See also: fitrm, RepeatedMeasuresModel.multcompare, RepeatedMeasuresModel.grpstats # name: # type: sq_string # elements: 1 # length: 25 Estimated marginal means. # name: # type: sq_string # elements: 1 # length: 29 RepeatedMeasuresModel.mauchly # name: # type: sq_string # elements: 1 # length: 668 RepeatedMeasuresModel: tbl = mauchly (rm) RepeatedMeasuresModel: tbl = mauchly (rm, C) Mauchly's test of sphericity. tbl = mauchly (rm) tests whether the covariance of the contrasts between successive responses is a multiple of the identity, the sphericity that the uncorrected p-values of ranova assume. tbl holds Mauchly's W, the chi-square statistic ChiStat with Bartlett's factor, its degrees of freedom DF and the pValue. A singular covariance gives W 0, ChiStat Inf and a p-value of 0. tbl = mauchly (rm, C) tests the contrasts of the matrix C, which must have one row per response. See also: fitrm, RepeatedMeasuresModel.ranova, RepeatedMeasuresModel.epsilon # name: # type: sq_string # elements: 1 # length: 29 Mauchly's test of sphericity. # name: # type: sq_string # elements: 1 # length: 33 RepeatedMeasuresModel.multcompare # name: # type: sq_string # elements: 1 # length: 1261 RepeatedMeasuresModel: tbl = multcompare (rm, var) RepeatedMeasuresModel: tbl = multcompare (rm, var, name, value) Multiple comparison of estimated marginal means. tbl = multcompare (rm, var) compares the estimated marginal means of every pair of levels of the factor var, a categorical between-subject factor or a within-subject factor, as margmean estimates them. tbl holds one row per ordered pair, with the variables var_1 and var_2, Difference, StdErr, the adjusted pValue, and Lower and Upper, the limits of the simultaneous confidence interval. The name-value arguments are: Name Value 'By' The name of another factor; the comparisons are made at each of its levels, which become the first variable of tbl. 'ComparisonType' 'tukey-kramer', the default, 'bonferroni', 'dunn-sidak', 'lsd' or 'scheffe'. 'Alpha' The significance level of the intervals. The default is 0.05. The Tukey-Kramer p-values and limits come from the studentized range distribution, stdrcdf and stdrinv. MATLAB R2024a and R2026a floor their Tukey-Kramer p-values, 9.56e-10 over three groups whatever the difference, and R2024a's Dunn-Sidak p-values read 0 where the Bonferroni ones are near 1e-36; both are computed here. See also: fitrm, RepeatedMeasuresModel.margmean, stdrcdf # name: # type: sq_string # elements: 1 # length: 48 Multiple comparison of estimated marginal means. # name: # type: sq_string # elements: 1 # length: 26 RepeatedMeasuresModel.plot # name: # type: sq_string # elements: 1 # length: 637 RepeatedMeasuresModel: h = plot (rm) RepeatedMeasuresModel: h = plot (rm, name, value) Plot the repeated measures of every subject. plot (rm) draws one line per subject of BetweenDesign through its repeated measures, against the positions 1 to k of the k responses, and returns the column of line handles h. The name-value arguments are 'Group', the name of a categorical between-subject factor or a cell array of them, which colours the lines by group and adds a legend with one entry per group; 'Marker', the marker, 's' by default; and 'LineStyle', the line style, '-' by default. See also: fitrm, RepeatedMeasuresModel.plotprofile # name: # type: sq_string # elements: 1 # length: 44 Plot the repeated measures of every subject. # name: # type: sq_string # elements: 1 # length: 33 RepeatedMeasuresModel.plotprofile # name: # type: sq_string # elements: 1 # length: 725 RepeatedMeasuresModel: h = plotprofile (rm, X) RepeatedMeasuresModel: h = plotprofile (rm, X, name, value) Plot the estimated marginal means. plotprofile (rm, X) draws the estimated marginal means of margmean over the levels of the factor X, a categorical between-subject factor or a within-subject factor, and returns the line handles h. A numeric within-subject factor is drawn at its values; any other at the positions 1 to L of its L levels, labelled with them. The name-value arguments are 'Group', the name of another factor, which draws one line per level of it and adds a legend; 'Marker', 'o' by default; and 'LineStyle', '-' by default. See also: fitrm, RepeatedMeasuresModel.margmean, RepeatedMeasuresModel.plot # name: # type: sq_string # elements: 1 # length: 34 Plot the estimated marginal means. # name: # type: sq_string # elements: 1 # length: 29 RepeatedMeasuresModel.predict # name: # type: sq_string # elements: 1 # length: 1363 RepeatedMeasuresModel: ypred = predict (rm) RepeatedMeasuresModel: ypred = predict (rm, tnew) RepeatedMeasuresModel: ypred = predict (..., name, value) RepeatedMeasuresModel: [ypred, yci] = predict (...) Predict the repeated measures. ypred = predict (rm, tnew) returns the predicted repeated measures of the subjects in the table tnew, one row each, from their between-subject predictors. tnew defaults to BetweenDesign. A subject missing a predictor, or holding a level the model was not fitted on, is predicted as NaN. Under 'separatemeans' the prediction is one column per response. Under any other within-subject model the means are smoothed through that model: projected onto the terms of a formula, or onto the polynomial of 'orthogonalcontrasts', and evaluated at the within-subject design, which may then be a new one. The name-value arguments are 'WithinDesign', the within-subject design to predict at, as fitrm takes it; 'WithinModel', the within-subject model, by default the one fitted; and 'Alpha', the significance level of the intervals, 0.05 by default. Under 'separatemeans' a new within-subject design is ignored with a warning, as in MATLAB. [ypred, yci] = predict (...) also returns the confidence limits of the predicted means as an array of the size of ypred by 2, the lower limits first. See also: fitrm, RepeatedMeasuresModel.random # name: # type: sq_string # elements: 1 # length: 30 Predict the repeated measures. # name: # type: sq_string # elements: 1 # length: 28 RepeatedMeasuresModel.random # name: # type: sq_string # elements: 1 # length: 465 RepeatedMeasuresModel: ysim = random (rm) RepeatedMeasuresModel: ysim = random (rm, tnew) Generate new random repeated measures. ysim = random (rm, tnew) draws one set of repeated measures for each subject of the table tnew, from a normal distribution with the subject's predicted means and the estimated covariance Covariance. tnew defaults to BetweenDesign. A subject missing a predictor gets a row of NaN. See also: fitrm, RepeatedMeasuresModel.predict, mvnrnd # name: # type: sq_string # elements: 1 # length: 38 Generate new random repeated measures. # name: # type: sq_string # elements: 1 # length: 28 RepeatedMeasuresModel.ranova # name: # type: sq_string # elements: 1 # length: 1726 RepeatedMeasuresModel: tbl = ranova (rm) RepeatedMeasuresModel: tbl = ranova (rm, 'WithinModel', WM) RepeatedMeasuresModel: [tbl, A, C, D] = ranova (...) Repeated measures analysis of variance. tbl = ranova (rm) tests the within-subject effects of the repeated measures model rm: whether the means of the responses differ, and whether each between-subject term changes that difference. tbl holds one row per test and one per error term, with the variables SumSq, DF, MeanSq, F and pValue, and beside them the p-values under the Greenhouse-Geisser, Huynh-Feldt and lower bound corrections for departures from sphericity, pValueGG, pValueHF and pValueLB. The corrections are reported and never applied. ranova (rm, 'WithinModel', WM) tests the within-subject model WM instead, which takes the forms the 'WithinModel' argument of fitrm takes. The default is 'separatemeans' whatever the model was fitted with, as in MATLAB. Under 'separatemeans' or a contrast matrix the rows are named after the within-subject factor, or Time when there are several. A formula or 'orthogonalcontrasts' gives one block of rows per within-subject term, the constant term first, whose tests are the between-subject tests of the mean of the responses. [tbl, A, C, D] = ranova (...) also returns the hypothesis of each test as A B C = D: A a cell column holding the between-subject hypothesis matrix of each term, C the within-subject contrast, a cell row of one per term when there are several, and D zero. 'orthogonalcontrasts' works here where MATLAB R2024a and R2026a both fail inside ranova; its results agree with those of MATLAB's anova for the same contrasts. See also: fitrm, RepeatedMeasuresModel.epsilon, RepeatedMeasuresModel.mauchly # name: # type: sq_string # elements: 1 # length: 39 Repeated measures analysis of variance. # name: # type: sq_string # elements: 1 # length: 8 coxphfit # name: # type: sq_string # elements: 1 # length: 5674 statistics: b = coxphfit (X, T) statistics: b = coxphfit (X, T, name, value, ...) statistics: [b, logl] = coxphfit (...) statistics: [b, logl, H] = coxphfit (...) statistics: [b, logl, H, stats] = coxphfit (...) Fit a Cox proportional hazards regression model. b = coxphfit (X, T) returns the p-by-1 vector of coefficients b of the Cox model $$ h(x_i, t) = h_0(t)\exp\left(\sum_{j=1}^{p} x_{ij} b_j\right) $$ fitted to the n-by-p matrix of predictors X and the n-by-1 vector of event times T. T may instead be an n-by-2 matrix whose rows give a (start, stop] interval of exposure, the counting process form, in which an observation joins the risk set only after its start time. h_0(t) is the baseline hazard, which is left unspecified: the coefficients are estimated by maximizing the Cox partial likelihood, which does not involve it. X must not contain a column of ones. The model has no constant term, since any constant is absorbed into the baseline hazard. A constant column is detected, reported by a warning, and given a zero coefficient. Rows of X, T or "Frequency" holding NaN are removed before fitting. [b, logl, H, stats] = coxphfit (...) additionally returns the maximized partial log-likelihood logl, the estimated baseline cumulative hazard H, and a structure stats of coefficient statistics and residuals. H is a two-column matrix whose first column holds the distinct event times and whose second holds the estimated cumulative hazard at those times, evaluated at the predictor values given by "Baseline". Its first row is the first event time with a cumulative hazard of zero; an observation censored before any event contributes no row. In a stratified model H gains a third column carrying the stratum, the blocks appear in ascending stratum order, and each block leads with its own zero row at its own first event time. A stratum holding no event contributes a single row of NaN with its label. The following name/value pairs are accepted: Name Value "Baseline" The X values at which the baseline hazard is computed, either a scalar or a 1-by-p vector. The default is the mean of X weighted by "Frequency" and taken within each stratum, so the hazard is that of an average observation of its stratum; pass 0 for a hazard relative to the origin. A value given explicitly is used for every stratum. The coefficients do not depend on this choice, only H does. "Censoring" A logical or 0/1 vector of length n, where 1 marks an observation right-censored at its recorded time. The default is a vector of zeros, so every observation is a recorded event. "Frequency" A vector of length n of non-negative values giving the number of observations each row represents, or a weight. The default is a vector of ones. "Ties" The method of handling tied event times, either "breslow" (default) or "efron". "B0" The starting value of the iteration, a vector of length p. The default is 0.01 ./ std (X). "Options" A structure of iteration settings, as built by statset ("coxphfit"). The fields used are "MaxIter", "TolX" and "Display". "Strata" A vector of length n of stratum labels. Each stratum carries its own baseline hazard and its own risk sets, while the coefficients are shared across all of them. A predictor that does not vary within any stratum cannot be estimated from a stratified fit; it is reported by a warning and held at zero. The fields of stats are: Field Contents "covb" The estimated covariance matrix of b. "beta" The coefficients, as returned in b. "se" The standard errors of the coefficients. "z" The z statistics, b over its standard error. "p" The two-sided p-values of the z statistics. "csres" The Cox-Snell residuals. "devres" The deviance residuals. "martres" The martingale residuals. "schres" The Schoenfeld residuals, NaN for a censored observation. The mean an event is measured against follows "Ties": under "efron" a tied death is measured against the mean over the sub-risk sets that approximation splits the tie into, so that every tied death at one time shares one mean and the residual does not depend on the order the tie was recorded in. Without a tie the two methods agree. "sschres" The scaled Schoenfeld residuals. "scores" The score residuals. "sscores" The scaled score residuals. "LikelihoodRatioTestP" The p-value of the likelihood ratio test against the model with no predictors. Two documented deviations, both where R2024a disagrees with itself. The martingale residual is defined as the event indicator minus the cumulative hazard the observation actually experienced, so "csres" and "martres" must sum to that indicator. They do here, always. Under "efron" ties MATLAB's do not: its "martres" comes from a cumulative hazard agreeing neither with its own "csres" nor with the H it returns, and the two sum to 1.0437 and -0.0414 where they must give 1 and 0. In the counting process form MATLAB's "martres" correctly subtracts the hazard accrued before the observation entered, but its "csres" does not, so the two disagree by exactly that amount for any row whose start time follows an event. Here both account for it, so "csres" differs from MATLAB's by \Lambda(start) \exp (x'b) and the identity is preserved. The score residuals inherit the first of those two deviations, being an integral against the martingale residual: under "efron" ties they differ from MATLAB's, whose own do not sum to the score at the maximum, while these sum to zero under both tie methods, weighted by "Frequency" where one is given. Every other output agrees with R2024a to machine precision, across censoring, weights, both tie methods, stratification, and the counting process form. See also: statset, ecdf, fitlm # name: # type: sq_string # elements: 1 # length: 48 Fit a Cox proportional hazards regression model. # name: # type: sq_string # elements: 1 # length: 6 fitcox # name: # type: sq_string # elements: 1 # length: 3081 statistics: mdl = fitcox (X, T) statistics: mdl = fitcox (tbl, respvar) statistics: mdl = fitcox (..., Name, Value) Fit a Cox proportional hazards regression model. mdl = fitcox (X, T) fits the Cox proportional hazards model $$ h(x_i, t) = h_0(t)\exp\left(\sum_{j=1}^{p} x_{ij} b_j\right) $$ to the n-by-p numeric matrix of predictors X and the n-by-1 vector of event times T, and returns a CoxModel object. T may instead be an n-by-2 matrix whose rows give a (start, stop] interval of exposure, the counting process form, in which an observation joins the risk set only after its start time. h_0(t) is the baseline hazard, which is left unspecified: the coefficients are estimated by maximizing the Cox partial likelihood, which does not involve it. X must not contain a column of ones, the model having no constant term, since any constant is absorbed into that baseline. mdl = fitcox (tbl, respvar) takes the data from the table tbl, using the variable named respvar as the response and every other variable as a predictor. A categorical variable is encoded as indicator columns, one per level bar the first, which the baseline hazard carries. The following Name/Value pairs are accepted: Name Value "Baseline" The X values at which the baseline hazard is computed, either a scalar or a 1-by-p vector. The default is the mean of each numeric predictor and zero for each indicator column of a categorical predictor, taken within each stratum. Pass 0 for a hazard relative to the origin. The coefficients do not depend on this choice. "Beta" The starting value of the iteration, a vector of length p. The default is 0.01 ./ std (X). "CategoricalPredictors" The predictors to treat as categorical, given as column indices, a logical vector, or a cell array of predictor names. Table variables of class categorical are detected without this argument. "Censoring" A logical or 0/1 vector of length n, where 1 marks an observation right-censored at its recorded time. The default is a vector of zeros, so every observation is a recorded event. "Frequency" A vector of length n of non-negative values giving the number of observations each row represents, or a weight. The default is a vector of ones. "OptimizationOptions" A structure of iteration settings, as built by statset ("fitcox"). The fields used are "MaxIter", "TolX" and "Display". "PredictorNames" A cell array of p predictor names. The default is "X1", "X2", and so on, or the table variable names. "Stratification" A vector of length n of stratum labels. Each stratum carries its own baseline hazard and its own risk sets, while the coefficients are shared across all of them. "TieBreakMethod" The method of handling tied event times, either "breslow" (default) or "efron". fitcox is the object interface to coxphfit, which fits the same model and returns the estimates as plain arrays. The two agree exactly; the object additionally reports the proportional hazards assumption tests and carries the survival, hazardratio, coefci, linhyptest and plotSurvival methods. See also: CoxModel, coxphfit, ecdf, statset # name: # type: sq_string # elements: 1 # length: 48 Fit a Cox proportional hazards regression model. # name: # type: sq_string # elements: 1 # length: 6 fitglm # name: # type: sq_string # elements: 1 # length: 3480 statistics: mdl = fitglm (X, y) statistics: mdl = fitglm (X, y, modelspec) statistics: mdl = fitglm (tbl) statistics: mdl = fitglm (tbl, modelspec) statistics: mdl = fitglm (..., Name, Value) Fit a generalized linear regression model. mdl = fitglm (X, y) fits a generalized linear model of the response vector y on the columns of the n-by-p numeric predictor matrix X, and returns a GeneralizedLinearModel object. mdl = fitglm (tbl) instead takes the predictors and response from the table tbl (the last column is the response unless overridden). By default the response is 'normal' with an identity link, an intercept is included, and the model is additive in the predictors. For the 'binomial' distribution y holds the number of successes, and the number of trials is given either by the 'BinomialSize' pair or by passing y as an n-by-2 matrix whose first column holds the successes and whose second holds the trials. The two forms describe the same model; when both are given, the trials supplied with the response are used. A trial count must be a positive integer, while a success count need not be whole. modelspec selects the model terms. It is either a Wilkinson formula string (e.g. 'y ~ x1 + x2*x3'), a keyword ('constant', 'linear', 'interactions', 'purequadratic', 'quadratic', or 'full'), or a terms matrix. The following Name/Value pairs are accepted: Name Value 'Distribution' the response distribution: 'normal' (default), 'binomial', 'poisson', 'gamma', or 'inverse gaussian'. 'Link' the link function. Defaults to the canonical link of the distribution; accepts any link name understood by glmfit or a numeric exponent for a power link. 'Weights' a vector of nonnegative observation weights. 'Offset' a vector added as a fixed term to the linear predictor. 'BinomialSize' for the 'binomial' distribution, the number of trials (a scalar or a per-observation vector); y holds the number of successes. Changed in 1.9.0: y was previously read as the proportion of successes. Multiply an existing proportion by the trials to keep its meaning. 'Intercept' a logical value (default true) whether to include an intercept term. 'DispersionFlag' a logical value forcing the dispersion parameter to be estimated (true) or held at 1 (false). 'CategoricalVars' predictors to treat as categorical (a logical vector, numeric indices, or a cell array of names). 'Exclude' observations to exclude from the fit (a logical vector or numeric indices). 'VarNames' a cell array of p + 1 variable names (predictors followed by the response) for numeric X. 'PredictorVars', 'ResponseVar' for table input, the predictor and response variable names. A categorical predictor expands to indicator columns, one per level bar the reference level, which the intercept carries. When the model has no intercept, the first categorical predictor is given an indicator for every one of its levels instead, so that its coefficients are the group means; any further categorical predictor stays reference coded, which keeps the design full rank. This differs from MATLAB, which omits the reference level whether or not an intercept is present and so cannot fit the reference group at all -- for a three-level grouping variable g, MATLAB fits y ~ g - 1 with two coefficients, predicts exactly 0 for every observation in the omitted group, and reports a negative R^2. This implementation returns three coefficients, one per group. See also: GeneralizedLinearModel, fitlm, glmfit, glmval, lassoglm # name: # type: sq_string # elements: 1 # length: 42 Fit a generalized linear regression model. # name: # type: sq_string # elements: 1 # length: 7 fitglme # name: # type: sq_string # elements: 1 # length: 1214 statistics: glme = fitglme (tbl, formula) statistics: glme = fitglme (..., name, value) Fit a generalized linear mixed-effects model specified by a formula. fitglme (tbl, formula) fits the generalized linear mixed-effects model described by formula to the table tbl and returns a GeneralizedLinearMixedModel object. formula uses the same syntax as fitlme: a response, a fixed-effects part, and one or more random-effects terms (expr | group), for example "y ~ x + (1 | g)". The model is fitted by penalized quasi-likelihood. The following name/value pairs are accepted: "Distribution" The response distribution: "normal" (default), "binomial", or "poisson". "Link" The link function: "identity", "logit", or "log". The default is the canonical link of the chosen distribution. "FitMethod" "MPL" (maximum pseudo-likelihood, the default), "REMPL" (restricted MPL), "Laplace", or "ApproximateLaplace". The first two differ in the pseudo-likelihood used for the covariance parameters; the last two report the Laplace-approximated marginal log-likelihood. Only the canonical links and the full (unstructured) random-effects covariance are currently supported. See also: GeneralizedLinearMixedModel, fitlme, fitglm # name: # type: sq_string # elements: 1 # length: 68 Fit a generalized linear mixed-effects model specified by a formula. # name: # type: sq_string # elements: 1 # length: 5 fitlm # name: # type: sq_string # elements: 1 # length: 9468 statistics: mdl = fitlm (X, y) statistics: mdl = fitlm (tbl) statistics: mdl = fitlm (tbl, ResponseVarName) statistics: mdl = fitlm (tbl, y) statistics: mdl = fitlm (..., modelspec) statistics: mdl = fitlm (..., Name, Value, ...) Fit a linear regression model to data and return a LinearModel object. The returned object stores the fitted coefficients, their standard errors, t-statistics, and p-values, summary statistics of the fit (R^2, RMSE, F-statistic, etc.), and the residuals and diagnostics of the fit, and exposes methods such as predict, plotResiduals, coefTest, addTerms, and removeTerms for further analysis of the fitted model. Basic Syntax mdl = fitlm (X, y) fits a linear regression model of the response y to the predictor data X. Unless removed via the 'Intercept' option, the fitted model contains a constant (intercept) term and one linear term for every column of X. X is an NxP numeric or logical matrix of predictor data, where rows correspond to observations and columns correspond to variables. By default, the predictors are named 'x1', 'x2', ..., 'xP'. X can also be a categorical vector of length N, representing a single categorical predictor. In this case y must be supplied as the next argument, and the predictor is named 'x1' by default. y is a numeric or logical vector of N response values, one for each row of X; a row vector is taken as a column. By default, the response is named 'y'. mdl = fitlm (tbl) fits a linear regression model using the variables contained in the table (or dataset) tbl. By default, the last variable in tbl is used as the response and all other variables are used as predictors. Variables that are categorical arrays, cell arrays of character vectors, or logical arrays are automatically treated as categorical predictors. mdl = fitlm (tbl, ResponseVarName) fits a model using the variable named ResponseVarName in tbl as the response, and all remaining variables in tbl as predictors. mdl = fitlm (tbl, y) fits a model using the variables in tbl as predictors and the external numeric vector y as the response. y must have height (tbl) elements. Model Specification mdl = fitlm (..., modelspec) additionally specifies the terms of the model to fit, using any of the input combinations shown above. modelspec can be any of the following. Value Description 'constant' Model contains only an intercept term. 'linear' Model contains an intercept and one term for each predictor variable. This is the default when modelspec is not specified. 'interactions' Model contains an intercept, all linear terms, and all pairwise products of distinct predictor variables (no squared terms). 'purequadratic' Model contains an intercept, all linear terms, and all squared terms. 'quadratic' Model contains an intercept, all linear terms, all pairwise products of distinct predictor variables, and all squared terms. 'full' Model contains an intercept and all terms up to and including the full P-way interaction of the predictor variables, i.e. every combination of one or more distinct predictors. terms matrix A TxP or Tx(P+1) numeric matrix, where T is the number of terms and P is the number of predictor variables. Each row represents one term, and the value in column j is the exponent to which predictor j is raised in that term; a row of all zeros represents the intercept. If a Tx(P+1) matrix is supplied, its last column (representing the response variable) must be all zeros. Wilkinson formula A character vector of the form 'y ~ terms' describing the response and predictor terms using Wilkinson notation. The variable name to the left of '~' is used as the response, overriding any response implied elsewhere in the call. When modelspec is given as a Wilkinson formula, the following operators may be used on its right-hand side to build up terms: Operator Meaning Example + add a term 'x1 + x2' adds x1 and x2 as separate terms - remove a term 'x1*x2 - x1:x2' removes the interaction, leaving only x1 and x2 * cross two terms 'x1*x2' expands to x1, x2, x1:x2 : interaction only 'x1:x2' adds only the interaction term between x1 and x2 ^ power / crossing limit 'x^2' adds x and x^2; '(x1+x2)^2' expands to x1, x2, x1:x2 -1 remove intercept 'x1 + x2 - 1' fits the model without a constant term A formula includes an intercept term by default; append '- 1' to the formula to omit it. For a categorical predictor, fitlm generates the necessary indicator (dummy) variables automatically from the formula, so a formula does not need to be changed when the underlying design matrix changes. Options mdl = fitlm (..., Name, Value, ...) specifies additional options using one or more Name-Value pair arguments, which may be combined with modelspec or used on their own. Name Value 'Intercept' A logical scalar indicating whether to include a constant (intercept) term in the model. Default is true. This option only applies when modelspec is a character vector model name (or omitted); it is ignored when modelspec is a terms matrix or a Wilkinson formula, where the intercept is instead controlled by the matrix/formula itself. 'Weights' A numeric vector of nonnegative observation weights, with one element per observation, used to fit a weighted least squares model. Default is a vector of ones, i.e. an unweighted ordinary least squares fit. 'Exclude' A numeric or logical vector specifying observations to exclude from the fit, given as row indices into the original data or as a logical mask the same length as the number of observations. Excluded observations, together with any observation that contains a missing (NaN) value in a predictor or the response, are recorded in the ObservationInfo property of the fitted model but do not contribute to the fitted coefficients or summary statistics. 'CategoricalVars' Specifies which predictor variables are treated as categorical, given as a vector of column indices, a logical vector, or a cell array of variable names (only valid for table input). Each categorical predictor with L distinct categories is expanded into L-1 indicator (dummy) variables, using the first category (in sorted or original order) as the reference level that is omitted from the design matrix. Variables that are already categorical arrays or cell arrays of character vectors are always treated as categorical, regardless of this option. 'VarNames' A cell array of character vectors naming the predictor and response variables, listed in order with the response variable name last, e.g. {"x1", "x2", "y"} for two predictors. Only applies when X and y (or a categorical vector and y) are supplied directly, since table variables already carry their own names. By default, predictors are named 'x1', 'x2', etc. and the response is named 'y'. 'ResponseVar' A character vector naming the response variable, used to override the response variable name that would otherwise be inferred (the last table variable, or 'y' for matrix input). 'PredictorVars' A cell array of character vectors naming which variables in tbl to use as predictors. By default, all variables in tbl other than the response variable are used as predictors. 'RobustOpts' Selects ordinary least squares or robust regression fitting. This value can be 'off' (default, ordinary least squares), 'on' (robust fitting using the 'bisquare' weighting function), the name of one of the weighting functions below, a function handle for a custom weighting function, or a scalar structure with fields RobustWgtFun and Tune specifying the weighting function and its tuning constant. Robust fitting uses Iteratively Reweighted Least Squares (IRLS), refitting the model with updated observation weights until the coefficients converge. Supported weighting function names: 'andrews', 'bisquare', 'cauchy', 'fair', 'huber', 'logistic', 'ols', 'talwar', 'welsch', each with its own default tuning constant. Algorithm fitlm solves the (weighted) least squares problem by applying a pivoted QR decomposition to the design matrix, which remains numerically stable even when predictors are collinear; coefficients corresponding to columns beyond the numerically detected rank of the design matrix are set to zero. Robust fits refine this ordinary least squares solution using IRLS as described above. Observations with missing values in any variable used by the model, or explicitly excluded via 'Exclude', are omitted from the fit entirely and flagged in ObservationInfo, but are otherwise not counted as errors. mdl is returned as a LinearModel object. If 'RobustOpts' is anything other than 'off', the returned model is a robust fit rather than an ordinary least squares fit, and its Robust property is populated accordingly. A categorical predictor expands to indicator columns, one per level bar the reference level, which the intercept carries. When the model has no intercept, the first categorical predictor is given an indicator for every one of its levels instead, so that its coefficients are the group means; any further categorical predictor stays reference coded, which keeps the design full rank. This differs from MATLAB, which omits the reference level whether or not an intercept is present and so cannot fit the reference group at all -- for a three-level grouping variable g, MATLAB fits y ~ g - 1 with two coefficients, predicts exactly 0 for every observation in the omitted group, and reports a negative R^2. This implementation returns three coefficients, one per group. See also: LinearModel # name: # type: sq_string # elements: 1 # length: 70 Fit a linear regression model to data and return a LinearModel object. # name: # type: sq_string # elements: 1 # length: 6 fitlme # name: # type: sq_string # elements: 1 # length: 1121 statistics: lme = fitlme (tbl, formula) statistics: lme = fitlme (..., name, value) Fit a linear mixed-effects model specified by a formula. fitlme (tbl, formula) fits the linear mixed-effects model described by formula to the variables in the table tbl, and returns a LinearMixedModel object. formula is a character vector in Wilkinson notation extended with random-effects terms, for example "y ~ x1 + x2 + (1 | g)". The part to the left of ~ names the response; the fixed-effects part uses the usual operators (+, *, :, ^, and -1 to drop the intercept); and each random-effects term (expr | group) adds random intercepts and slopes expr grouped by the factor group (or an interaction of factors, e.g. g1:g2). As with fixed effects, a random intercept is implicit unless suppressed with 0 or -1. Rows of tbl with missing values in any model variable are removed before fitting. The following name/value pairs are accepted: "FitMethod" The estimation criterion, "ML" (maximum likelihood, the default) or "REML" (restricted maximum likelihood). See also: LinearMixedModel, fitlmematrix, fitlm, parseWilkinsonFormula # name: # type: sq_string # elements: 1 # length: 56 Fit a linear mixed-effects model specified by a formula. # name: # type: sq_string # elements: 1 # length: 12 fitlmematrix # name: # type: sq_string # elements: 1 # length: 1827 statistics: lme = fitlmematrix (X, y, Z, G) statistics: lme = fitlmematrix (..., name, value) Fit a linear mixed-effects model from design matrices. fitlmematrix (X, y, Z, G) fits the linear mixed-effects model $$ y = X\beta + Zb + \varepsilon $$ with fixed-effects design X, response y, random-effects design Z, and grouping variable G. The random effects b are normally distributed with mean zero and an unstructured covariance Psi (shared across the levels of the grouping variable), and the observation errors are independent N(0, sigma2). X is an n-by-p numeric matrix and y an n-by-1 response vector. Z is an n-by-q random-effects design and G an n-by-1 grouping variable (numeric, logical, char, cell array of strings, or categorical). To specify several grouping terms, pass Z and G as cell arrays of the same length, one design and one grouping variable per term. The following name/value pairs are accepted: "FitMethod" The estimation criterion, either "ML" (maximum likelihood, the default) or "REML" (restricted maximum likelihood). "FixedEffectPredictors" A cell array of p names for the columns of X (default {"x1", ..., "xp"}). "RandomEffectPredictors" A cell array (one entry per grouping term) of cell arrays naming the columns of each Z (default z1, z2, ...). "RandomEffectGroups" A cell array of names for the grouping terms (default g1, g2, etc.). The returned lme is a LinearMixedModel object describing the fitted model: the estimated fixed effects and their statistics (lme.Coefficients), the covariance parameters (covarianceParameters), the random-effects BLUPs (randomEffects), the log-likelihood, and methods for prediction, residuals, and hypothesis tests. Only the full (unstructured) random-effects covariance is currently supported. See also: LinearMixedModel, fitlm, parseWilkinsonFormula # name: # type: sq_string # elements: 1 # length: 54 Fit a linear mixed-effects model from design matrices. # name: # type: sq_string # elements: 1 # length: 6 fitmnr # name: # type: sq_string # elements: 1 # length: 3104 statistics: mdl = fitmnr (X, Y) statistics: mdl = fitmnr (Tbl, Y) statistics: mdl = fitmnr (Tbl, ResponseVarName) statistics: mdl = fitmnr (Tbl, Formula) statistics: mdl = fitmnr (..., name, value) Fit a multinomial regression model. mdl = fitmnr (X, Y) fits a multinomial regression of the response Y on the columns of the N-by-P numeric predictor matrix X, and returns a MultinomialRegression object. An intercept is always fitted, so X holds no column of ones. Y is an N-by-1 numeric, logical, string or categorical vector, a cell array of character vectors, or a character matrix, each element or row the category of one observation; or an N-by-K matrix of counts, Y(i,j) observations of category j at the predictor values X(i,:), as mnrfit documents. mdl = fitmnr (Tbl, Y) takes the predictors from the variables of the table Tbl, and the response from Y. mdl = fitmnr (Tbl, ResponseVarName) takes the response from the variable of Tbl that ResponseVarName names, a character vector or a string scalar, and the predictors from the others. mdl = fitmnr (Tbl, Formula) takes both from the Wilkinson formula Formula, such as 'y ~ x1 + x2*x3'. A numeric variable of a table is a continuous predictor and any other is categorical, coded as one indicator per level bar the first. The following name, value pairs are accepted: Name Value 'ModelType' 'nominal' (default), a model contrasting each category with the last; 'ordinal', a model of the cumulative probabilities of ordered categories; or 'hierarchical', a model of each category against the ones after it. 'Link' The link function of an ordinal or hierarchical model: 'logit' (default), 'probit', 'comploglog' or 'loglog'. A nominal model always uses the logit link. 'IncludeClassInteractions' true to give each category its own slopes, false for slopes shared by every category. The default is true for nominal and hierarchical models and false for ordinal ones. A nominal model without interactions fits its intercepts alone. 'EstimateDispersion' true to estimate the dispersion and scale the standard errors by it, false (default) for the theoretical dispersion of 1. 'Weights' An N-by-1 vector of nonnegative observation weights, each multiplying the counts of its observation; ones by default. 'CategoricalPredictors' The predictors to treat as categorical: a vector of indices, a logical vector with one element per predictor, a cell array or string array of names, or 'all'. 'PredictorNames' The names of the predictors: for a matrix X, one per column, x1, x2, ... by default; for a table, the variables to use as predictors. A formula names its own. 'ResponseName' The name of the response, 'y' by default. A response variable name or a formula names its own. 'IterationLimit' The largest number of iterations of the fit, 100 by default. 'Tolerance' The convergence tolerance of the fit, the largest change of a coefficient relative to its value, 1e-6 by default. A row holding a missing value of a predictor it uses, of the response, or of the weights is left out of the fit. See also: MultinomialRegression, mnrfit, mnrval, fitglm # name: # type: sq_string # elements: 1 # length: 35 Fit a multinomial regression model. # name: # type: sq_string # elements: 1 # length: 6 fitnlm # name: # type: sq_string # elements: 1 # length: 1453 statistics: mdl = fitnlm (X, y, modelfun, beta0) statistics: mdl = fitnlm (tbl, modelfun, beta0) statistics: mdl = fitnlm (..., Name, Value) Fit a nonlinear regression model. mdl = fitnlm (X, y, modelfun, beta0) fits the nonlinear regression model y = modelfun (beta, X) to the response vector y and the n-by-p predictor matrix X, starting the iterative fit from the coefficient vector beta0, and returns a NonLinearModel object. modelfun is a function handle @(b, X) returning the fitted responses. mdl = fitnlm (tbl, modelfun, beta0) takes the predictors and response from the table tbl; the last column is the response unless overridden by 'ResponseVar'. The following Name/Value pairs are accepted: Name Value 'CoefficientNames' a cell array of names for the coefficients (default 'b1', 'b2', ...). 'Weights' a vector of nonnegative observation weights. 'ErrorModel' the error-variance model: 'constant' (default), 'proportional', or 'combined'. 'RobustWgtFun' the name of a robust weight function, enabling robust fitting (see nlinfit). 'Options' a statset-style options structure controlling the iterative fit (MaxIter, TolFun, TolX). 'PredictorVars', 'ResponseVar' for table input, the predictor and response variable names. 'VarNames' a cell array of p + 1 variable names (predictors followed by the response) for numeric X. 'Exclude' observations to exclude from the fit. See also: NonLinearModel, nlinfit, nlparci, nlpredci, fitlm, fitglm # name: # type: sq_string # elements: 1 # length: 33 Fit a nonlinear regression model. # name: # type: sq_string # elements: 1 # length: 5 fitrm # name: # type: sq_string # elements: 1 # length: 881 statistics: rm = fitrm (t, modelspec) statistics: rm = fitrm (t, modelspec, name, value) Fit a repeated measures model. rm = fitrm (t, modelspec) fits the repeated measures held in the table t, one row per subject, on the between-subject model modelspec, and returns a RepeatedMeasuresModel object. modelspec is a character vector or a string scalar of the form 'responses ~ terms': the responses a range of the table's variables such as 'y1-y6', a comma list such as 'y1,y2,y3', or both, and the terms a Wilkinson formula over the other variables, such as 'species' or 'g*x', or '1' for an intercept alone. rm = fitrm (..., name, value) takes the within-subject design and model as the name-value arguments 'WithinDesign' and 'WithinModel'; see RepeatedMeasuresModel for both. ranova, epsilon and mauchly test the fitted model. See also: RepeatedMeasuresModel, anova2, manova1 # name: # type: sq_string # elements: 1 # length: 30 Fit a repeated measures model. # name: # type: string # elements: 1 # length: 6 glmfit # name: # type: sq_string # elements: 1 # length: 4735 statistics: b = glmfit (X, y, distribution) statistics: b = glmfit (X, y, distribution, Name, Value) statistics: [b, dev] = glmfit (...) statistics: [b, dev, stats] = glmfit (...) Perform generalized linear model fitting. b = glmfit (X, y, distribution) returns a vector b of coefficient estimates for a generalized linear regression model of the responses in y on the predictors in X, using the distribution defined in distribution. X is an nxp numeric matrix of predictor variables with n observations and p predictors. y is an nx1 numeric vector of responses for all supported distributions, except for the 'binomial' distribution in which case y can be either a numeric or logical nx1 vector or an nx2 matrix, where the first column contains the number of successes and the second column contains the number of trials. distribution is a character vector specifying the distribution of the response variable. Supported distributions are 'normal', 'binomial', 'poisson', 'gamma', and 'inverse gaussian'. b = glmfit (..., Name, Value) specifies additional options using Name-Value pair arguments. Name Value 'B0' A numeric vector specifying initial values for the coefficient estimates. By default, the initial values are fitted values fitted from the data. 'Constant' A character vector specifying whether to include a constant term in the model. Valid options are "on" (default) and "off". 'EstDisp' A character vector specifying whether to compute dispersion parameter. Valid options are "on" and "off". For 'binomial' and 'poisson' distributions the default is "off", whereas for the 'normal', 'gamma', and 'inverse gaussian' distributions the default is "on". 'link' A character vector specifying the name of a canonical link function or a numeric scalar for specifying a 'power' link function. Supported canonical link functions include 'identity' (default for 'normal' distribution), 'log' (default for 'poisson' distribution), 'logit' (default for 'binomial' distribution), 'probit', 'loglog', 'comploglog', and 'reciprocal' (default for the 'gamma' distribution). The 'power' link function is the default for the 'inverse gaussian' distribution with p = -2. For custom link functions, the user can provide cell array with three function handles: the link function, its derivative, and its inverse, or alternatively a structure S with three fields: S.Link, S.Derivative, and S.Inverse. Each field can either contain a function handle or a character vector with the name of an existing function. All custom link functions must accept a vector of inputs and return a vector of the same size. 'Offset' A numeric vector of the same length as the response y specifying an offset variable in the fit. It is used as an additional predictor with a coefficient value fixed at 1. 'Options' A scalar structure containing the fields MaxIter and TolX. MaxIter must be a non-negative integer specifying the maximum number of iterations allowed for fitting the model, counted after the first weighted fit, as in MATLAB, so a value of 0 returns that first fit. TolX must be a positive scalar value specifying the termination tolerance. 'Weights' An nx1 numeric vector of nonnegative values, where n is the number of observations in X. By default, it is ones (n, 1). [b, dev] = glmfit (...) also returns the deviance of the fit as a numeric value in dev. Deviance is a generalization of the residual sum of squares. It measures the goodness of fit compared to a saturated model. The model is fitted by iteratively reweighted least squares. A step that takes the linear predictor outside the domain of the inverse link, as the power link of the 'inverse gaussian' distribution does at or below zero, is halved back until the fitted means are valid. MATLAB stops at such a step with a warning that the weights are ill-conditioned and can return coefficients whose fitted means are not valid: with u = (1:30)', for glmfit (u, exp (0.1 * u) .* (1 + 0.2 * sin (u)), 'inverse gaussian') it reports a deviance of 2.2064 where the fit reaches 1.9353. [b, dev, stats] = glmfit (...) also returns the structure stats, which contains the model statistics in the following fields: beta - Coefficient estimates b dfe - Degrees of freedom for error sfit - Estimated dispersion parameter s - Theoretical or estimated dispersion parameter estdisp - false when 'EstDisp' is 'off' and true when 'EstDisp' is 'on' covb - Estimated covariance matrix for b se - Vector of standard errors of the coefficient estimates b coeffcorr - Correlation matrix for b t - t statistics for b p - p-values for b resid - Vector of residuals residp - Vector of Pearson residuals residd - Vector of deviance residuals resida - Vector of Anscombe residuals See also: glmval # name: # type: sq_string # elements: 1 # length: 41 Perform generalized linear model fitting. # name: # type: sq_string # elements: 1 # length: 6 glmval # name: # type: sq_string # elements: 1 # length: 1680 statistics: yhat = glmval (b, X, link) statistics: [yhat, y_lo, y_hi] = glmval (b, X, link, stats) statistics: [...] = glmval (..., Name, Value) Predict values for a generalized linear model. yhat = glmval (b, X, link) returns the predicted values for the generalized linear model with a vector of coefficient estimates b, a matrix of predictors X, in which each column corresponds to a distinct predictor variable, and a link function link, which can be any of the character vectors, numeric scalar, or custom-defined link functions used as values for the 'link' name-value pair argument in the glmfit function. [yhat, y_lo, y_hi] = glmval (b, X, link, stats) also returns the 95% confidence intervals for the predicted values according to the model's statistics contained in the stats structure, which is the output of the glmfit function. By default, the confidence intervals are nonsimultaneous, and apply to the fitted curve instead of new observations. [...] = glmval (..., Name, Value) specifies additional options using Name-Value pair arguments. Name Value 'confidence' A scalar value between 0 and 1 specifying the confidence level for the confidence bounds. 'Constant' A character vector specifying whether to include a constant term in the model. Valid options are "on" (default) and "off". 'simultaneous' A logical or numeric (0 or 1) scalar specifying whether the confidence bounds are simultaneous. The default is false, which yields nonsimultaneous (pointwise) bounds. 'size' A numeric scalar or a vector with one value for each row of X specifying the size parameter N for a binomial model. 'BinomialSize' is accepted as an alias for 'size'. See also: glmfit # name: # type: sq_string # elements: 1 # length: 46 Predict values for a generalized linear model. # name: # type: sq_string # elements: 1 # length: 7 invpred # name: # type: sq_string # elements: 1 # length: 1714 statistics: x0 = invpred (x, y, y0) statistics: [x0, dxlo, dxup] = invpred (x, y, y0) statistics: [...] = invpred (..., name, value) Inverse prediction from a simple linear regression. x0 = invpred (x, y, y0) fits the simple linear regression of y on x and returns, for each element of y0, the value of the predictor at which the fitted line takes that response. x and y must be vectors of real values of the same length; y0 may be of any size and x0 is returned with the same size. Observations where either x or y is NaN are dropped in pairs before the fit. [x0, dxlo, dxup] = invpred (...) also returns the width of a confidence interval on either side of x0, so that the interval is [x0 - dxlo, x0 + dxup]. The bounds follow Fieller's theorem and are therefore not symmetric about x0. They are not simultaneous over the elements of y0, and they need not be finite: when the slope is not significantly different from zero at the requested level, the interval either covers the entire real line, with dxlo and dxup both Inf, or it excludes a finite range around the fitted line and extends to Inf on one side only, with the other side of dxlo/dxup finite. [...] = invpred (..., name, value) accepts the following name-value pairs: "alpha" is the significance level of the interval, a scalar strictly between 0 and 1, so that the interval has confidence 100 x (1 - alpha)%. The default is 0.05. "predopt" selects what the interval covers. With "observation", the default, it covers a new observation whose response is y0. With "curve", it covers the point at which the true regression line takes the value y0, and is narrower because it carries no new-observation variance. See also: regress, fitlm, polyfit # name: # type: sq_string # elements: 1 # length: 51 Inverse prediction from a simple linear regression. # name: # type: string # elements: 1 # length: 5 lasso # name: # type: sq_string # elements: 1 # length: 3076 statistics: B = lasso (X, y) statistics: [B, FitInfo] = lasso (X, y) statistics: [...] = lasso (..., Name, Value) Lasso and elastic-net regularized least-squares regression. B = lasso (X, y) fits a series of regularized linear models of the response y on the predictor matrix X by lasso, over a sequence of values of the regularization parameter Lambda. B is a PxL matrix whose column k holds the coefficient estimates for the k-th Lambda, in ascending order of Lambda. y is a vector with one element per row of X; a row vector is taken as a column. [B, FitInfo] = lasso (...) additionally returns a structure FitInfo with fields Intercept, Lambda, Alpha, DF (number of non-zero coefficients), and MSE (mean squared error), one entry per value of Lambda. The following Name-Value pairs are supported: Name Value 'Alpha' The elastic-net mixing parameter in (0, 1]. 1 (default) is the lasso penalty; smaller values add a ridge penalty. 'Lambda' A vector of non-negative regularization parameters. By default a geometric sequence of 'NumLambda' values is used, from the smallest value that drives all coefficients to zero down to 'LambdaRatio' times that value. 'NumLambda' The number of Lambda values in the default sequence (default 100). 'LambdaRatio' The ratio of the smallest to the largest Lambda in the default sequence (default 1e-4, or 1e-2 when the number of observations is below the number of predictors). 'Standardize' Whether to standardize X to zero mean and unit variance before fitting (default true). Coefficients are always returned on the original scale. 'Weights' A real, finite, non-negative vector of class single or double, with one element per row of X. The weights of rows removed because of NaN values are dropped with those rows, and the remaining weights are normalized to sum to one. 'RelTol' Convergence tolerance for the coordinate descent (default 1e-4). 'MaxIter' Maximum number of coordinate-descent iterations, a non-negative integer (default 1e5). A value of 0 returns the starting point, every coefficient 0, where MATLAB's lasso refuses 0 and its lassoglm accepts it. 'DFmax' The maximum number of non-zero coefficients; the default sequence stops once this is exceeded. 'Intercept' Whether to fit a constant term (default true). 'PredictorNames' A cell array of predictor names, kept in FitInfo. 'CV' The number of folds K for K-fold cross-validation of the mean squared error, or a cvpartition object. 'MCReps' The number of Monte-Carlo repetitions of the cross-validation (default 1). Observations with NaN values in X or y are removed before fitting. At least two observations must remain and, if 'Weights' is given, at least two of them must have a non-zero weight. When 'CV' is used, FitInfo.MSE is the cross-validated error, plus SE, LambdaMinMSE, IndexMinMSE, Lambda1SE, and Index1SE, which report the Lambda with the lowest error and the largest Lambda within one standard error of it. The fold assignment is random, so these selections are not reproducible without fixing the random seed. See also: ridge, regress, lassoglm # name: # type: sq_string # elements: 1 # length: 59 Lasso and elastic-net regularized least-squares regression. # name: # type: string # elements: 1 # length: 8 lassoglm # name: # type: sq_string # elements: 1 # length: 4700 statistics: B = lassoglm (X, y) statistics: B = lassoglm (X, y, distr) statistics: B = lassoglm (X, y, distr, Name, Value) statistics: [B, FitInfo] = lassoglm (...) Lasso and elastic-net regularized generalized linear model regression. B = lassoglm (X, y, distr) returns fitted least-squares regression coefficients for a generalized linear model of the response y on the predictor data X, penalized by the lasso (L1) or elastic-net penalty. X is an n-by-p numeric matrix of p predictors at each of n observations, and y is a numeric vector of n responses. For the 'normal', 'binomial' and 'poisson' distributions y may also be a logical vector, true read as 1, or a categorical vector with exactly two categories, the second read as 1 and undefined elements as missing; for 'binomial' the value 1 is the success. For the 'binomial' distribution y may also be an n-by-2 matrix whose first column holds the number of successes and whose second holds the number of trials; a row with NaN in either column or with no trials is removed. distr names the distribution of the response: 'normal' (default), 'binomial', 'poisson', 'gamma', or 'inverse gaussian'. The canonical link function of the chosen distribution is used unless overridden by the 'Link' option. B is a p-by-L matrix, where L is the number of regularization ('Lambda') values used; column k holds the coefficients for the k-th value of lambda, in order of ascending lambda. [B, FitInfo] = lassoglm (...) also returns a structure FitInfo with information about the fitted models: Intercept a 1-by-L vector of intercept terms Lambda the 1-by-L vector of lambda values, in ascending order Alpha the elastic-net mixing value used DF the number of nonzero coefficients in each column of B Deviance the deviance of the fitted model at each lambda; when cross-validation is requested this is instead the cross-validated mean deviance When cross-validation is requested (see 'CV' below), FitInfo additionally contains SE (standard error of the cross-validated deviance), LambdaMinDeviance and IndexMinDeviance (the lambda with minimum cross-validated deviance and its index), and Lambda1SE and Index1SE (the largest lambda within one standard error of that minimum). lassoglm accepts the following Name/Value pairs: Name Value 'Alpha' the elastic-net mixing parameter, a scalar in (0, 1]. 'Alpha' = 1 is the lasso penalty (default); values towards 0 approach ridge regression. 'Lambda' a vector of non-negative lambda values. 'Standardize' a logical value (default true) specifying whether the predictors are standardized before fitting. 'Weights' a real, non-negative vector of class single or double, with one element per row of X. A NaN weight marks its observation as missing. 'BinomialSize' for the 'binomial' distribution, the number of trials, a scalar or a vector with one element per row of X; y holds the number of successes. Default is 1 (Bernoulli responses). It describes the same model as the two-column y, whose trials are used when both are given. A row whose number of trials is NaN or 0 is removed. 'BinomialSize' is an Octave extension, and 'Size' is accepted as an alias for it. 'Link' the link function to use instead of the family's canonical link. Accepts any link name understood by glmfit (e.g. 'log', 'probit') or a numeric exponent for a power link. 'Offset' a numeric vector, one value per observation, added as a fixed term to the linear predictor (not penalized or fitted). 'RelTol' convergence tolerance for the coordinate descent. 'MaxIter' maximum number of iterations, a non-negative integer. A value of 0 returns the starting point, every coefficient 0, as MATLAB does. 'DFmax' maximum number of nonzero coefficients. 'Intercept' a logical value (default true) whether to fit an intercept term. 'PredictorNames' a cell array of predictor names. 'CV' the number of folds K for K-fold cross-validation, or a cvpartition object. The fold assignment is random, so the selected lambda values are not reproducible without a fixed random seed. 'MCReps' the number of Monte-Carlo repetitions of the cross-validation (default 1). Observations with NaN values in X, y or 'Weights' are removed before fitting. At least two observations must remain and, if 'Weights' is given, at least two of them must have a non-zero weight. A response at the edge of its range on every observation, all failures or all successes for 'binomial' and all zeros for 'poisson', has no finite fit. B is then zero, the intercept places the fitted mean at the limit of its range, and the default Lambda values are all zero, as in MATLAB. MATLAB's intercept there is about 102.5 in magnitude. See also: lasso, glmfit, glmval, cvpartition # name: # type: sq_string # elements: 1 # length: 70 Lasso and elastic-net regularized generalized linear model regression. # name: # type: sq_string # elements: 1 # length: 19 logistic_regression # name: # type: sq_string # elements: 1 # length: 1928 statistics: [intercept, slope, dev, dl, d2l, P, stats] = logistic_regression (y, x, print, intercept, slope) Perform ordinal logistic regression. Suppose y takes values in k ordered categories, and let P_i (x) be the cumulative probability that y falls in one of the first i categories given the covariate x. Then [intercept, slope] = logistic_regression (y, x) fits the model logit (P_i (x)) = x * slope + intercept_i, i = 1 ... k-1 The number of ordinal categories, k, is taken to be the number of distinct values of round (y). If k equals 2, y is binary and the model is ordinary logistic regression. The matrix x is assumed to have full column rank. Given y only, intercept = logistic_regression (y) fits the model with baseline logit odds only. The full form is [intercept, slope, dev, dl, d2l, P, ... stats] = logistic_regression (y, x, print, ... intercept, slope) in which all output arguments and all input arguments except y are optional. Setting print to 1 requests summary information about the fitted model to be displayed. Setting print to 2 requests information about convergence at each iteration. Other values request no information to be displayed. The input arguments intercept and slope give initial estimates for intercept and slope. The returned value dev holds minus twice the log-likelihood. The returned values dl and d2l are the vector of first and the matrix of second derivatives of the log-likelihood with respect to intercept and slope. P holds estimates for the conditional distribution of y given x. stats returns a structure that contains the following fields: "intercept": intercept coefficients "slope": slope coefficients "coeff": regression coefficients (intercepts and slops) "covb": estimated covariance matrix for coefficients (coeff) "coeffcorr": correlation matrix for coeff "se": standard errors of the coeff "z": z statistics for coeff "pval": p-values for coeff # name: # type: sq_string # elements: 1 # length: 36 Perform ordinal logistic regression. # name: # type: sq_string # elements: 1 # length: 6 mnrfit # name: # type: sq_string # elements: 1 # length: 6444 statistics: B = mnrfit (X, Y) statistics: B = mnrfit (X, Y, name, value) statistics: [B, dev] = mnrfit (...) statistics: [B, dev, stats] = mnrfit (...) Fit a multinomial logistic regression model. Nominal models are fitted with a baseline-category multinomial logit, using the last category of Y as the reference. Ordinal models are fitted with a cumulative link model and hierarchical models with a sequential (continuation-ratio) link model, both honouring the 'link' option below. Nominal models always use the logit link. B = mnrfit (X, Y) returns a matrix, B, of coefficient estimates for a multinomial logistic regression of the nominal responses in Y on the predictors in X. X is an NxP numeric matrix the observations on predictor variables, where N corresponds to the number of observations and P corresponds to predictor variables. Y contains the response category labels and it can be an Nx1 vector of positive integers, logical values, character vectors in a cell array, strings, or categorical values, each element the category of one observation. A cell array, string, or categorical Y can also be a row vector. Y can also be a character matrix with each row holding the label of an observation of X. Y can also be an NxK numeric or logical matrix of counts, with a column for each of the K categories: Y(i,j) is the number of observations in category j at the predictor values in X(i,:), so that a row of X stands for the sum (Y(i,:)) observations it was seen with. The counts must be nonnegative integers. A row of zeros stands for no observations and takes no part in the estimates, though it counts in 'dfe', as in MATLAB. A matrix of 1s and 0s with a single 1 in each row is the same as a vector of one category per observation. The categories of a numeric or logical Y are taken in sorted order, those of a cell array, string, or character matrix in order of first appearance, and those of a categorical Y in the order of its categories. A category without observations is ignored wherever it lies among the categories of a categorical Y; MATLAB ignores only a last one and fits coefficients for any other. Likewise the categories of a numeric Y are the values it holds, where MATLAB numbers them from 1 to the largest and fits coefficients for any value missing below it, and a column of zeros in a matrix Y is no category, where MATLAB fits coefficients for it. The last category is the reference category of a nominal model. An observation whose label is missing (an empty character vector, , or ), whose row of a matrix Y holds a NaN, or whose predictors hold a NaN, is left out of the fit, and its rows of the residuals in stats are NaN. A Y with a single category leaves nothing to fit: a nominal or hierarchical model returns a B with no columns, a deviance of 0 and residuals of 0, the raw and Pearson residuals of a hierarchical model having no columns, and an ordinal model is an error. B = mnrfit (X, Y, name, value) returns a matrix, B, of coefficient estimates for a multinomial model fit with additional parameters specified Name-Value pair arguments. Name Value 'model' The type of model to fit: 'nominal' (default) for a baseline-category model, 'ordinal' for a cumulative model, or 'hierarchical' for a sequential (continuation-ratio) model. 'link' The link function for ordinal and hierarchical models: 'logit' (default), 'probit', 'comploglog', or 'loglog'. Nominal models always use the logit link. 'interactions' 'on' to give each category its own slopes, or 'off' for slopes shared by every category, the intercepts always differing. The default is 'on' for nominal and hierarchical models and 'off' for ordinal ones. With 'on', B is a (P+1)x(K-1) matrix whose column j holds the intercept and slopes of category j; with 'off', B is a (K-1+P)x1 vector of the K-1 intercepts followed by the P shared slopes. A nominal model without interactions has no slopes to share, so it fits the K-1 intercepts alone, with a warning. 'weights' An Nx1 vector of nonnegative observation weights, each multiplying the counts of its row of Y. The default is a vector of ones. A row whose weight is NaN is left out of the fit. 'iterationlimit' The largest number of iterations of the fit, a positive integer. The default is 100. 'tolerance' The convergence tolerance of the fit, a positive scalar: the fit stops once no coefficient changes by more than this fraction of its value. The default is 1e-6. 'estdisp' 'on' to estimate a dispersion parameter, scaling the coefficient standard errors by it and testing the coefficients against the t distribution, or 'off' (default) for the theoretical dispersion of 1. [B, dev, stats] = mnrfit (...) also returns the deviance of the fit, dev, and a structure stats with the fitted coefficients 'beta' (same as B), their standard errors 'se', covariance matrix 'covb', correlation matrix 'coeffcorr', error degrees of freedom 'dfe', the coefficient t statistics 't' and p-values 'p', the dispersion parameters 's', 'sfit', and 'estdisp', and the raw, Pearson, and deviance residuals 'resid', 'residp', and 'residd'. The raw and Pearson residuals are taken on what the model fits: an NxK matrix over the categories of a nominal model, and an Nx(K-1) matrix over the cumulative probabilities of an ordinal model or over the stages of a hierarchical one, where a row is NaN at the stages that none of its observations reach. A raw residual is the count minus its fitted value, the count times the fitted probability, and a Pearson residual divides it by the binomial standard deviation of the count; a weight multiplies both counts. A row of zeros has a Pearson residual of NaN. The deviance residuals are an Nx1 vector holding the contribution of each row to the deviance, so they sum to dev; for an observation of one category it is -2 times the log of the fitted probability of that category. For a hierarchical model MATLAB sums the signed deviance residuals of the stages instead, which is NaN for every observation in the first category. 'sfit' is the square root of the Pearson statistic over the categories divided by 'dfe', and it differs from MATLAB's, which for ordinal and hierarchical models is taken over the residuals in 'residp'. 'dfe' is the number of rows of X times K-1, less the number of coefficients; for a hierarchical model each stage counts only the rows with observations that reach it. See also: mnrval, logistic_regression # name: # type: sq_string # elements: 1 # length: 44 Fit a multinomial logistic regression model. # name: # type: sq_string # elements: 1 # length: 6 mnrval # name: # type: sq_string # elements: 1 # length: 2615 statistics: pihat = mnrval (B, X) statistics: [pihat, dlo, dhi] = mnrval (B, X, stats) statistics: yhat = mnrval (B, X, ssize) statistics: [yhat, dlo, dhi] = mnrval (B, X, ssize, stats) statistics: [...] = mnrval (..., name, value) Predict values for a multinomial logistic regression model. pihat = mnrval (B, X) returns the predicted category probabilities pihat of a multinomial logistic regression with coefficients B, evaluated at the predictor values in X. X is an NxP numeric matrix of N observations on P predictors. pihat is an NxK matrix, where K is the number of response categories and each row sums to one. B is the coefficient matrix returned by mnrfit (see below for its shape under each model). mnrval is the prediction companion of mnrfit. Unlike the current mnrfit, which only fits ordinal and two-category nominal models, mnrval evaluates all three model types, so a coefficient matrix B obtained elsewhere (e.g. MATLAB) can be used for prediction. yhat = mnrval (B, X, ssize) returns predicted category counts instead of probabilities, for the sample sizes in ssize (a scalar or an Nx1 vector). [pihat, dlo, dhi] = mnrval (B, X, stats) also returns 95% confidence bounds on the predictions. stats is the structure returned by mnrfit; its 'covb' field (the coefficient covariance matrix) is required. The confidence interval for each prediction is [pihat - dlo, pihat + dhi]. The bounds are nonsimultaneous and apply to the fitted values, not to new observations. The following Name-Value pairs control the model: Name Value 'model' The model type: 'nominal' (default), 'ordinal', or 'hierarchical'. 'interactions' 'on' to include category-specific coefficients, or 'off' for a common set of coefficients with category-specific intercepts only. Default is 'on' for nominal and hierarchical models and 'off' for ordinal models. With 'interactions','on', B is a (P+1)x(K-1) matrix. With 'interactions','off', B is a (K-1+P)x1 vector holding the K-1 intercepts followed by the P common slopes. 'link' The link function for ordinal and hierarchical models: 'logit' (default), 'probit', 'comploglog', or 'loglog'. Nominal models always use the multinomial logit link. 'type' The kind of probability returned: 'category' (default, NxK category probabilities), 'cumulative' (Nx(K-1) cumulative probabilities of the first K-1 categories), or 'conditional' (Nx(K-1) conditional probabilities of each category given membership in that or a later category). 'confidence' The confidence level for dlo and dhi, a scalar in the range (0,1). Default is 0.95. See also: mnrfit, glmval, logistic_regression # name: # type: sq_string # elements: 1 # length: 59 Predict values for a multinomial logistic regression model. # name: # type: sq_string # elements: 1 # length: 15 monotone_smooth # name: # type: sq_string # elements: 1 # length: 1447 statistics: yy = monotone_smooth (x, y, h) Produce a smooth monotone increasing approximation to a sampled functional dependence. A kernel method is used (an Epanechnikov smoothing kernel is applied to y(x); this is integrated to yield the monotone increasing form. See Reference 1 for details.) Arguments x is a vector of values of the independent variable. y is a vector of values of the dependent variable, of the same size as x. For best performance, it is recommended that the y already be fairly smooth, e.g. by applying a kernel smoothing to the original values if they are noisy. h is the kernel bandwidth to use. If h is not given, a "reasonable" value is computed. Return values yy is the vector of smooth monotone increasing function values at x. Examples x = 0:0.1:10; y = (x .^ 2) + 3 * randn(size(x)); # typically non-monotonic from the added noise ys = ([y(1) y(1:(end-1))] + y + [y(2:end) y(end)])/3; # crudely smoothed via moving average, but still typically non-monotonic yy = monotone_smooth(x, ys); # yy is monotone increasing in x plot(x, y, '+', x, ys, x, yy) References Holger Dette, Natalie Neumeyer and Kay F. Pilz (2006), A simple nonparametric estimator of a strictly monotone regression function, Bernoulli, 12:469-490 Regine Scheder (2007), R Package 'monoProc', Version 1.0-6, http://cran.r-project.org/web/packages/monoProc/monoProc.pdf (The implementation here is based on the monoProc function mono.1d) # name: # type: sq_string # elements: 1 # length: 86 Produce a smooth monotone increasing approximation to a sampled functional dependence. # name: # type: sq_string # elements: 1 # length: 9 mvregress # name: # type: sq_string # elements: 1 # length: 1728 statistics: beta = mvregress (X, Y) statistics: beta = mvregress (..., name, value) statistics: [beta, Sigma, E, CovB, logL] = mvregress (...) Multivariate (multiple-response) linear regression by maximum likelihood. mvregress (X, Y) fits the multivariate normal regression of the n-by-d response matrix Y on the design X and returns the coefficient estimates beta. X is either a numeric n-by-p matrix, in which case the same p predictors apply to every response and beta is returned as a p-by-d matrix; or a cell array of n design matrices, each d-by-K, in which case beta is a K-by-1 vector. Missing responses (NaN entries of Y) are handled according to the estimation algorithm. The following name/value pairs are accepted: "algorithm" "mvn" (multivariate normal; observations with any missing response are discarded), "ecm" (expectation-conditional-maximization, using every observed response), or "cwls" (covariance-weighted least squares, with the weight given by "covar0"). The default is "mvn" when Y has no missing values and "ecm" otherwise. "covar0" The d-by-d covariance weight for "cwls" (default the identity), or the initial covariance for "ecm". "maxiter" Maximum number of iterations (default 100). "tolbeta", "tolobj" Convergence tolerances on the coefficients and the objective (defaults 1e-8 and 1e-8). The additional outputs are the estimated residual covariance Sigma (d-by-d), the residuals E (n-by-d), the covariance CovB of the coefficient estimates, and the log-likelihood logL. (With missing data and the "ecm" algorithm, CovB is the standard observed-information covariance and can differ from MATLAB's value at the 1e-3 level; all other outputs agree.) See also: mvregresslike, regress, fitlm # name: # type: sq_string # elements: 1 # length: 73 Multivariate (multiple-response) linear regression by maximum likelihood. # name: # type: sq_string # elements: 1 # length: 13 mvregresslike # name: # type: sq_string # elements: 1 # length: 1335 statistics: nlogL = mvregresslike (X, Y, beta, Sigma, alg) statistics: [nlogL, COVB] = mvregresslike (...) Negative log-likelihood for a multivariate regression model. mvregresslike (X, Y, beta, Sigma, alg) returns the negative log-likelihood nlogL of the multivariate normal regression model with responses Y (an n-by-d matrix, one row per observation), coefficients beta, and residual covariance Sigma (d-by-d). X specifies the design. It is either a numeric n-by-p matrix, in which case the same p predictors apply to every response and beta is p-by-d; or a cell array of n design matrices, each d-by-K, in which case beta is K-by-1. alg selects how missing responses (NaN entries of Y) are handled: "ecm" (the default) and "cwls" use every observed response through the marginal likelihood of the observed components, while "mvn" discards any observation that has a missing response. With no missing data all three agree. The optional second output COVB is the covariance matrix of the coefficient estimates, computed as the inverse of the observed Fisher information at beta and Sigma. With missing data and the "ecm"/"cwls" algorithms this is the standard observed-data covariance and can differ from MATLAB's value (which uses a different information convention) at the 1e-3 level; nlogL agrees exactly. See also: mvregress # name: # type: sq_string # elements: 1 # length: 60 Negative log-likelihood for a multivariate regression model. # name: # type: sq_string # elements: 1 # length: 7 nlinfit # name: # type: sq_string # elements: 1 # length: 4168 statistics: beta = nlinfit (X, y, modelfun, beta0) statistics: beta = nlinfit (..., options) statistics: beta = nlinfit (..., Name, Value) statistics: [beta, R, J, CovB, MSE, ErrorModelInfo] = nlinfit (...) Fit a nonlinear regression model. beta = nlinfit (X, y, modelfun, beta0) estimates the coefficients of the nonlinear regression model y = modelfun (beta, X) by iteratively minimizing the (possibly weighted) sum of squared residuals, starting from the initial coefficient vector beta0. The fit uses the Levenberg-Marquardt algorithm with a numerically computed Jacobian. X is a matrix of predictor values. nlinfit does not interpret the columns of X; the array is passed unchanged as the second argument of modelfun, so its shape is whatever modelfun expects. y is a numeric vector of responses, one element per observation. modelfun is a function handle @(b, X) returning a vector of fitted responses the same size as y. beta0 is a numeric vector of initial values for the coefficients. Additional options are given either as a statset-style options structure or as Name/Value pairs (or both). The supported options are: Name Value 'Weights' A vector of nonnegative observation weights, or a function handle @(yhat) returning such a vector. Weighted least squares is used. 'ErrorModel' The form of the error variance: 'constant' (default), 'proportional', or 'combined'. 'ErrorParameters' Initial values for the error-model parameters. 'RobustWgtFun' The name of a robust weight function ('andrews', 'bisquare', 'cauchy', 'fair', 'huber', 'logistic', 'talwar', or 'welsch'), enabling robust iteratively reweighted least squares. MATLAB accepts this name only inside an 'Options' structure; taking it as a Name/Value pair as well is an Octave extension. 'Tune' The tuning constant for the robust weight function. 'Options' A statset-style structure whose MaxIter, TolFun, TolX, and DerivStep fields override the corresponding defaults, and whose RobustWgtFun, Robust, WgtFun and Tune fields select a robust fit. RobustWgtFun names the weight function on its own and takes precedence over the other two; the older WgtFun is read only when Robust is 'on'. The structure statset ('nlinfit') returns carries WgtFun 'bisquare' beside Robust 'off', and so leaves the fit unweighted. The remaining outputs describe the converged fit: R is the vector of raw residuals y - modelfun (beta, X), J is the Jacobian of modelfun with respect to beta at the solution, CovB is the estimated covariance matrix of the coefficients, MSE is the mean squared error, and ErrorModelInfo is a structure describing the fitted error model. Algorithm The coefficients are estimated by the Levenberg-Marquardt algorithm using a numerically computed (forward-difference) Jacobian. For an ordinary or weighted fit the coefficient covariance is CovB = MSE * inv (J' * W * J), where W is the diagonal matrix of observation weights and MSE is the weighted residual sum of squares divided by the error degrees of freedom n - p (with p coefficients). A non-constant 'ErrorModel' is fitted by generalized least squares, re-deriving the observation weights from the fitted values each iteration; the 'proportional' model weights each observation by the inverse squared fitted value, and MSE then estimates the proportionality constant of the variance. For a robust fit ('RobustWgtFun') the coefficients are found by iteratively reweighted least squares applied to leverage-adjusted residuals (the leverage is taken from the ordinary fit and held fixed). The robust coefficient covariance follows the Street-Carroll-Ruppert convention, the same one used by robustfit: CovB = s^2 * inv (J' * J) and MSE = s^2, where the scale s blends the ordinary-fit scale ols_s with the robust scale robust_s at the solution as s^2 = (p^2 x ols_s^2 + n x robust_s^2) / (n + p^2), taken to be at least robust_s. The robust MSE and CovB differ from MATLAB's by up to a few tenths of a percent, because the shared robust scale does; robustfit documents that difference and why it is left in place. The coefficients themselves agree to about 1e-8. See also: fitnlm, nlparci, nlpredci, NonLinearModel, robustfit # name: # type: sq_string # elements: 1 # length: 33 Fit a nonlinear regression model. # name: # type: sq_string # elements: 1 # length: 7 nlparci # name: # type: sq_string # elements: 1 # length: 1023 statistics: ci = nlparci (beta, resid, 'covar', CovB) statistics: ci = nlparci (beta, resid, 'jacobian', J) statistics: ci = nlparci (..., 'alpha', alpha) Confidence intervals for the coefficients of a nonlinear regression. ci = nlparci (beta, resid, 'covar', CovB) returns the 100 (1 - alpha)% confidence intervals for the fitted coefficients beta of a nonlinear regression, given the residual vector resid and the estimated coefficient covariance matrix CovB (both produced by nlinfit). ci is a p-by-2 matrix whose rows are the lower and upper bounds for the corresponding coefficient. ci = nlparci (beta, resid, 'jacobian', J) instead derives the coefficient covariance from the Jacobian J and the residuals. A legacy positional form nlparci (beta, resid, J) is also accepted. The confidence level defaults to 95%; pass 'alpha', alpha for a 100 (1 - alpha)% interval. The intervals use Student's t distribution with numel (resid) - numel (beta) degrees of freedom. See also: nlinfit, nlpredci, fitnlm, NonLinearModel # name: # type: sq_string # elements: 1 # length: 68 Confidence intervals for the coefficients of a nonlinear regression. # name: # type: sq_string # elements: 1 # length: 8 nlpredci # name: # type: sq_string # elements: 1 # length: 1923 statistics: [ypred, delta] = nlpredci (modelfun, X, beta, resid, 'Jacobian', J) statistics: [ypred, delta] = nlpredci (modelfun, X, beta, resid, 'Covar', CovB) statistics: [ypred, delta] = nlpredci (..., Name, Value) Confidence intervals for predictions of a nonlinear regression. [ypred, delta] = nlpredci (modelfun, X, beta, resid, 'Jacobian', J) returns the predicted responses ypred of the model modelfun (beta, X) at the new predictor values X, together with the half-widths delta of the 100 (1 - alpha)% confidence intervals, so that ypred - delta and ypred + delta bound the response. beta, resid (the residuals) and J (the Jacobian) come from nlinfit. Instead of the Jacobian, an estimated coefficient covariance may be supplied with 'Covar', CovB. The following Name/ Value pairs are also accepted: Name Value 'MSE' The mean squared error from nlinfit, required with 'Covar' for observation (prediction) intervals. 'PredOpt' 'curve' (default) for confidence intervals on the fitted curve, or 'observation' for prediction intervals on a new observation. 'SimOpt' 'off' (default) for pointwise intervals, or 'on' for simultaneous (Scheffe) intervals. 'Alpha' The significance level; the interval has confidence 100 (1 - alpha)% (default alpha = 0.05). Algorithm Each half-width is delta = c * sqrt (v). The variance v of the fitted curve is diag (Jnew * V * Jnew'), where V is the coefficient covariance (either CovB, or MSE * inv (J' * J) when a Jacobian is supplied) and Jnew is the Jacobian of modelfun at X; an 'observation' interval adds the error variance MSE to v. The critical value c is the Student's t quantile at 1 - alpha/2 with the error degrees of freedom for a pointwise interval, or the Scheffe value sqrt (k * finv (1 - alpha, k, dfe)) for a simultaneous interval, where k is the number of coefficients (plus one for an observation interval). See also: nlinfit, nlparci, fitnlm, NonLinearModel # name: # type: sq_string # elements: 1 # length: 63 Confidence intervals for predictions of a nonlinear regression. # name: # type: sq_string # elements: 1 # length: 10 plsregress # name: # type: sq_string # elements: 1 # length: 4603 statistics: [xload, yload] = plsregress (X, Y) statistics: [xload, yload] = plsregress (X, Y, NCOMP) statistics: [xload, yload, xscore, yscore, coef, pctVar, mse, stats] = plsregress (X, Y, NCOMP) statistics: [xload, yload, xscore, yscore, coef, pctVar, mse, stats] = plsregress (..., Name, Value) Calculate partial least squares regression using SIMPLS algorithm. plsregress uses the SIMPLS algorithm, and first centers X and Y by subtracting off column means to get centered variables, unless 'Intercept' is false. However, it does not rescale the columns. To perform partial least squares regression with standardized variables, use zscore to normalize X and Y. [xload, yload] = plsregress (X, Y) computes a partial least squares regression of Y on X, using NCOMP PLS components, which by default are calculated as min (size (X, 1) - 1, size(X, 2)), and returns the the predictor and response loadings in xload and yload, respectively. X is an NxP matrix of predictor variables, with rows corresponding to observations, and columns corresponding to variables. Y is an NxM response matrix. xload is a PxNCOMP matrix of predictor loadings, where each row of xload contains coefficients that define a linear combination of PLS components that approximate the original predictor variables. yload is an MxNCOMP matrix of response loadings, where each row of yload contains coefficients that define a linear combination of PLS components that approximate the original response variables. [xload, yload] = plsregress (X, Y, NCOMP) defines the desired number of PLS components to use in the regression. NCOMP, a scalar positive integer, must not exceed the default calculated value. [xload, yload, xscore, yscore, coef, pctVar, mse, stats] = plsregress (X, Y, NCOMP) also returns the following arguments: xscore is an NxNCOMP orthonormal matrix with the predictor scores, i.e., the PLS components that are linear combinations of the variables in X, with rows corresponding to observations and columns corresponding to components. yscore is an NxNCOMP orthonormal matrix with the response scores, i.e., the linear combinations of the responses with which the PLS components xscore have maximum covariance, with rows corresponding to observations and columns corresponding to components. coef is a (P+1)xM matrix with the PLS regression coefficients, containing the intercepts in the first row. If 'Intercept' is false, coef is a PxM matrix without the intercepts. pctVar is a 2xNCOMP matrix containing the percentage of the variance explained by the model with the first row containing the percentage of explained varianced in X by each PLS component and the second row containing the percentage of explained variance in Y. mse is a 2x(NCOMP+1) matrix containing the estimated mean squared errors for PLS models with 0:NCOMP components with the first row containing the squared errors for the predictor variables in X and the second row containing the mean squared errors for the response variable(s) in Y. stats is a structure with the following fields: stats.W is a PxNCOMP matrix of PLS weights. stats.T2 is the T^2 statistics for each point in xscore. stats.Xresiduals is an NxP matrix with the predictor residuals. stats.Yresiduals is an NxM matrix with the response residuals. [...] = plsregress (..., Name, Value, ...) specifies one or more of the following Name/Value pairs: Name Value 'CV' The method used to compute mse. When Value is a positive integer K, plsregress uses K-fold cross-validation. Set Value to a cross-validation partition, created using cvpartition, to use other forms of cross-validation. Set Value to 'resubstitution' to use both X and Y to fit the model and to estimate the mean squared errors, without cross-validation. By default, Value = "resubstitution". 'MCReps' A positive integer indicating the number of Monte-Carlo repetitions for cross-validation. By default, Value = 1. A different 'MCReps' value is only meaningful when using the 'HoldOut' method for cross-validation, previously set by a cvpartition object. If no cross-validation method is used, then 'MCReps' must be 1. 'Intercept' A logical scalar, or 'on' or 'off', specifying whether the model includes a constant term. When the constant term is excluded, X and Y are not centered and coef contains no intercepts. By default, Value = true. Further information about the PLS regression can be found at https://en.wikipedia.org/wiki/Partial_least_squares_regression References SIMPLS: An alternative approach to partial least squares regression. Chemometrics and Intelligent Laboratory Systems (1993) # name: # type: sq_string # elements: 1 # length: 66 Calculate partial least squares regression using SIMPLS algorithm. # name: # type: sq_string # elements: 1 # length: 7 regress # name: # type: sq_string # elements: 1 # length: 1155 statistics: [b, bint, r, rint, stats] = regress (y, X, [alpha]) Multiple Linear Regression using Least Squares Fit of y on X with the model y = X * beta + e. Here, y is a column vector of observed values X is a matrix of regressors, with the first column filled with the constant value 1 beta is a column vector of regression parameters e is a column vector of random errors Arguments are y is the y in the model, a vector; a row vector is taken as a column X is the X in the model alpha is the significance level used to calculate the confidence intervals bint and rint (see `Return values' below). If not specified, ALPHA defaults to 0.05 Return values are b is the beta in the model bint is the confidence interval for b r is a column vector of residuals rint is the confidence interval for r stats is a row vector containing: The R^2 statistic The F statistic The p value for the full model The estimated error variance r and rint can be passed to rcoplot to visualize the residual intervals and identify outliers. NaN values in y and X are removed before calculation begins. See also: regress_gp, regression_ftest, regression_ttest # name: # type: sq_string # elements: 1 # length: 93 Multiple Linear Regression using Least Squares Fit of y on X with the model y = X * beta + e. # name: # type: sq_string # elements: 1 # length: 10 regress_gp # name: # type: sq_string # elements: 1 # length: 3298 statistics: [Yfit, Yint, m, K] = regress_gp (X, Y, Xfit) statistics: [Yfit, Yint, m, K] = regress_gp (X, Y, Xfit, 'linear') statistics: [Yfit, Yint, Ysd] = regress_gp (X, Y, Xfit, 'rbf') statistics: [...] = regress_gp (X, Y, Xfit, 'linear', Sp) statistics: [...] = regress_gp (X, Y, Xfit, Sp) statistics: [...] = regress_gp (X, Y, Xfit, 'rbf', theta) statistics: [...] = regress_gp (X, Y, Xfit, 'rbf', theta, g) statistics: [...] = regress_gp (X, Y, Xfit, 'rbf', theta, g, alpha) statistics: [...] = regress_gp (X, Y, Xfit, theta) statistics: [...] = regress_gp (X, Y, Xfit, theta, g) statistics: [...] = regress_gp (X, Y, Xfit, theta, g, alpha) Regression using Gaussian Processes. [Yfit, Yint, m, K] = regress_gp (X, Y, Xfit) will estimate a linear Gaussian Process model m in the form Y = X' * m, where X is an NxP matrix with N observations in P dimensional space and Y is an Nx1 column vector as the dependent variable. The information about errors of the predictions (interpolation/extrapolation) is given by the covariance matrix K. By default, the linear model defines the prior covariance of m as Sp = 100 * eye (size (X, 2) + 1). A custom prior covariance matrix can be passed as Sp, which must be a P+1xP+1 positive definite matrix. The model is evaluated for input Xfit, which must have the same columns as X, and the estimates are returned in Yfit along with the estimated variation in Yint. Yint(:,1) contains the lower boundary and Yint(:,2) the upper boundary of the interval about Yfit, at the confidence level 1 - alpha. [Yfit, Yint, Ysd] = regress_gp (X, Y, Xfit, 'rbf') will estimate a Gaussian Process model with a Radial Basis Function (RBF) kernel with default parameters theta = 5 and g = 0.01, which corresponds to the nugget effect, and alpha = 0.05 which defines the confidence level for the estimated intervals returned in Yint. The function also returns the predictive covariance matrix in Ysd. For multidimensional predictors X the function will automatically normalize each column to a zero mean and a standard deviation to one. Four things about the RBF kernel are worth stating, because they decide what the numbers mean. theta is not the characteristic lengthscale. The kernel is exp (-d^2 / theta) with d the distance between two points, so theta is twice the square of a lengthscale l, and theta = 2 * l^2. The intervals in Yint are prediction intervals for a new observation, not confidence intervals on the mean: the nugget is carried in the predictive variance, so the noise of an observation is included. The predictors are centred and scaled only when there is more than one of them, so theta is measured in the units of X for a single predictor and in standard deviations for several. A nugget of zero is accepted and gives an interpolating process, one that reproduces Y at the training points and reports almost no uncertainty there. It also leaves the kernel matrix rank deficient whenever two inputs are close, so in that case the covariance is applied through its pseudoinverse and the fit is the minimum-norm solution. This is a genuine answer rather than a refusal, but a small nugget is the better way to ask for a smooth fit. Run demo regress_gp to see examples. See also: fitrgp, RegressionGP, regress, regression_ftest, regression_ttest # name: # type: sq_string # elements: 1 # length: 36 Regression using Gaussian Processes. # name: # type: sq_string # elements: 1 # length: 5 ridge # name: # type: sq_string # elements: 1 # length: 1724 statistics: b = ridge (y, X, k) statistics: b = ridge (y, X, k, scaled) Ridge regression. b = ridge (y, X, k) returns the vector of coefficient estimates by applying ridge regression from the predictor matrix X to the response vector y. Each value of b is the coefficient for the respective ridge parameter given k. By default, b is calculated after centering and scaling the predictors to have a zero mean and standard deviation 1. A predictor whose standard deviation is below sqrt (eps), taken in the class of X, is treated as a constant term: it is replaced by a column of ones and its coefficient is never rescaled. b = ridge (y, X, k, scaled) performs the regression with the specified scaling of the coefficient estimates b. When scaled = 0, the function restores the coefficients to the scale of the original data thus is more useful for making predictions. When scaled = 1, the coefficient estimates correspond to the scaled centered data. y must be an Nx1 numeric vector with the response data. X must be an Nxp numeric matrix with the predictor data. k must be a numeric vector with the ridge parameters. scaled must be a numeric scalar indicating whether the coefficient estimates in b are restored to the scale of the original data. By default, scaled = 1. Rows of y and X holding a NaN are removed before the fit, and at least two rows must remain. MATLAB answers with fewer: with none it returns zeros, or NaN when scaled = 0, and with one it returns slopes that do not depend on X, and too many coefficients when X has more than one column and scaled = 0. Further information about Ridge regression can be found at https://en.wikipedia.org/wiki/Ridge_regression See also: lasso, stepwisefit, regress # name: # type: sq_string # elements: 1 # length: 17 Ridge regression. # name: # type: sq_string # elements: 1 # length: 9 robustfit # name: # type: sq_string # elements: 1 # length: 2088 statistics: b = robustfit (X, y) statistics: b = robustfit (X, y, wfun) statistics: b = robustfit (X, y, wfun, tune) statistics: b = robustfit (X, y, wfun, tune, const) statistics: [b, stats] = robustfit (...) Robust linear regression. b = robustfit (X, y) returns the coefficient vector b of a linear regression of the response y on the predictors X, fitted by robust M-estimation (iteratively reweighted least squares) so that outlying observations are downweighted. A column of ones is added to X by default, so b(1) is the intercept. y is a vector with one element per row of X; a row vector is taken as a column. b = robustfit (X, y, wfun, tune, const) selects the weight function wfun, its tuning constant tune, and whether a constant term is included. wfun is one of 'bisquare' (default), 'andrews', 'cauchy', 'fair', 'huber', 'logistic', 'ols', 'talwar', 'welsch', or a function handle @(r) giving the weights as a function of the scaled residual. tune defaults to the value that gives 95% efficiency for each weight function. const is 'on' (default) to include a constant term or 'off' to omit. [b, stats] = robustfit (...) also returns a structure stats with fields ols_s, robust_s, mad_s, s, se, covb, coeffcorr, t, p, w, R, dfe, h, and resid. The coefficients and the fields ols_s, mad_s, dfe, h, w, and resid match MATLAB. The standard errors and quantities derived from them (se, t, p, covb) agree with MATLAB to within a small fraction of a percent; robust_s is the Street-Carroll-Ruppert robust scale estimate and differs from MATLAB's by about 1.5%, measured, with a negligible effect on the standard errors. That difference is left in place deliberately. The squared influence is averaged here over n, where the estimator as it is usually published averages over n-p; taking that published form moves the result further from MATLAB rather than closer, so MATLAB implements neither, and matching it would mean reproducing an undocumented variant. The same scale serves nlinfit, which is why its robust MSE and CovB carry the same difference. See also: regress, fitlm # name: # type: sq_string # elements: 1 # length: 25 Robust linear regression. # name: # type: sq_string # elements: 1 # length: 11 stepwisefit # name: # type: sq_string # elements: 1 # length: 3096 statistics: stepwisefit (X, y) statistics: b = stepwisefit (X, y) statistics: b, se, pval, finalmodel, stats, nextstep, history = stepwisefit (X, y, varargin) Perform stepwise linear regression using conditional p-value criteria. stepwisefit fits a linear regression model to response vector y using predictor matrix X and performs stepwise variable selection based on hypothesis tests for individual regression coefficients. At each iteration, predictors not currently in the model are tested for inclusion using partial F- or t-tests. The predictor with the smallest p-value below the entry threshold is added. Predictors currently in the model (excluding forced predictors) are then tested for removal, and the predictor with the largest p-value exceeding the removal threshold is removed. The procedure repeats until the model stabilizes or the maximum number of iterations is reached. After variable selection, the final regression model is refit using regress to compute coefficient estimates and inferential statistics for both included and excluded predictors. Arguments X is an n-by-p numeric matrix of predictor variables. y is a numeric response vector of n elements. A row vector is taken as a column, where MATLAB refuses it. Optional Name-Value pairs may be supplied to control the stepwise selection procedure. Name-Value Arguments 'InModel' Logical row vector of length p specifying predictors that are initially included in the model. 'Keep' Logical row vector of length p specifying predictors that must remain in the model and are never removed during stepwise selection. 'PEnter' Scalar significance level in the open interval (0,1) specifying the maximum p-value required for a predictor to enter the model. Default is 0.05. 'PRemove' Scalar significance level in the open interval (0,1) specifying the minimum p-value required for a predictor to be removed from the model. If not specified, a default value greater than or equal to 'PEnter' is used. 'MaxIter' Positive integer specifying the maximum number of stepwise iterations. Default is Inf. 'Scale' Either 'on' or 'off'. When enabled, predictors are standardized prior to stepwise selection only. Final regression coefficients are always reported on the original data scale. 'Display' Either 'on' or 'off'. Accepted for compatibility but currently does not affect output. Return Values b is a p-by-1 vector of regression coefficients. Coefficients for excluded predictors are computed conditionally. se is a p-by-1 vector of standard errors. pval is a p-by-1 vector of two-sided p-values. finalmodel is a logical row vector indicating which predictors are included in the final model. stats is a structure containing regression diagnostics, including sums of squares, degrees of freedom, residuals, covariance estimates, F-statistic, and related quantities. nextstep is a scalar indicating whether an additional stepwise iteration is recommended. Currently always zero. history is a structure summarizing the final model state, including selected predictors and coefficient history. See also: regress # name: # type: sq_string # elements: 1 # length: 70 Perform stepwise linear regression using conditional p-value criteria. # name: # type: sq_string # elements: 1 # length: 11 stepwiseglm # name: # type: sq_string # elements: 1 # length: 4524 statistics: mdl = stepwiseglm (X, y) statistics: mdl = stepwiseglm (X, y, modelspec) statistics: mdl = stepwiseglm (tbl) statistics: mdl = stepwiseglm (tbl, modelspec) statistics: mdl = stepwiseglm (..., Name, Value) Fit a generalized linear regression model by stepwise term selection. mdl = stepwiseglm (X, y) starts from a model given by modelspec and repeatedly adds or removes terms, one at a time, until no further move improves the selection criterion. It returns the fitted GeneralizedLinearModel object mdl, whose Steps property records the term-selection trace. X is an n-by-p numeric predictor matrix and y the response; mdl = stepwiseglm (tbl) instead takes the predictors and response from the table tbl (the last column is the response unless overridden). modelspec is the starting model. It is a Wilkinson formula string (e.g. 'y ~ x1 + x2'), a keyword ('constant' (default), 'linear', 'interactions', 'purequadratic', 'quadratic', or 'full'), or a terms matrix. The candidate terms available to the search are bounded below by 'Lower' and above by 'Upper'. The following Name/Value pairs control the stepwise search: Name Value 'Lower' the smallest model considered (terms in it are never removed). Defaults to 'constant'. 'Upper' the largest model considered (the candidate term universe). Defaults to 'interactions'. 'Criterion' the selection criterion: 'Deviance' (default), 'sse', 'aic', or 'bic'. Under 'Deviance' and 'sse', terms enter or leave by a chi-squared or F test on the change in deviance; under 'aic'/'bic' the move that most reduces the information criterion is taken. 'PEnter' the p-value (or criterion margin) below which a term is added. Defaults to 0.05 for 'Deviance' and 'sse', and 0 for 'aic'/'bic'. 'PRemove' the p-value (or criterion margin) above which a term is removed. Defaults to 0.10 for 'Deviance' and 'sse', and 0 for 'aic'/'bic'. 'NSteps' the maximum number of steps. Defaults to Inf (run to convergence). 'Verbose' 0 to run silently, or 1 (default) to print each accepted step. As in fitglm, a 'binomial' response is the number of successes, and the trials come either from 'BinomialSize' or from passing y as an n-by-2 matrix of successes and trials. Changed in 1.9.0: y was previously read as the proportion. All Name/Value pairs accepted by fitglm (such as 'Distribution', 'Link', 'Weights', 'Offset', 'BinomialSize', 'Intercept', 'DispersionFlag', 'CategoricalVars', and 'Exclude') are also accepted and forwarded to the fit. The Steps property The returned model's Steps property records the trace, as a structure with seven fields. Start, Lower, and Upper are LinearFormula objects for the starting model and the two bounds; Criterion is the criterion as it was asked for; PEnter and PRemove are the thresholds it ran under; and History is a table with one row per step. History always carries Action ('Start', 'Add', or 'Remove'), TermName, Terms (the terms matrix after the step, over the model's variables), DF (the coefficient count after the step), and delDF (the change in it, negative for a removal). The remaining columns follow the criterion, which is why a history is read by name and not by position: Criterion Further columns 'Deviance' Deviance, then Chi2Stat or FStat as the dispersion is fixed or estimated, then PValue. 'sse' FStat and pValue. 'aic', 'bic' one column, AIC or BIC, holding the criterion's value after the step, the starting model included. The first row is the starting model, named by its right-hand side. Categorical predictors A categorical predictor with L levels contributes L - 1 indicator columns, the first level being the omitted reference, and the search treats that whole group as a single term: it is added or removed in one step, worth L - 1 degrees of freedom, and never one indicator at a time. An interaction naming a categorical predictor behaves the same way, contributing one column per indicator and entering as one term. Steps.History names such a term by the predictor ('g', or 'x1:g' for the interaction) rather than by its indicators, while CoefficientNames names the indicators ('g_B', 'x1:g_C'). In a table, every column that groups its observations is taken as categorical: a cell array of character vectors, a categorical array, a string array, or a logical column. 'CategoricalVars' adds to these, and is the only way to mark a column of a predictor matrix; it takes predictor names, column indices, or a logical vector. See also: GeneralizedLinearModel, fitglm, stepwisefit, glmfit, glmval # name: # type: sq_string # elements: 1 # length: 69 Fit a generalized linear regression model by stepwise term selection. # name: # type: sq_string # elements: 1 # length: 10 stepwiselm # name: # type: sq_string # elements: 1 # length: 9194 statistics: mdl = stepwiselm (tbl) statistics: mdl = stepwiselm (tbl, ResponseVarName) statistics: mdl = stepwiselm (tbl, y) statistics: mdl = stepwiselm (X, y) statistics: mdl = stepwiselm (..., InitialModel) statistics: mdl = stepwiselm (..., Name, Value, ...) Fit a linear regression model using stepwise regression and return a LinearModel object. stepwiselm starts from an initial model and repeatedly searches for a term to add to, or remove from, the current model, based on the value of the 'Criterion' option, until no single addition or removal improves the model any further. Basic Syntax mdl = stepwiselm (tbl) fits a stepwise model using the variables in the table (or dataset) tbl, starting from a constant model. By default, the last variable in tbl is used as the response and all other variables are candidate predictors. Variables that are categorical arrays, cell arrays of character vectors, or logical arrays are automatically treated as categorical predictors. mdl = stepwiselm (tbl, ResponseVarName) uses the variable named ResponseVarName in tbl as the response, and all remaining variables in tbl as candidate predictors. mdl = stepwiselm (tbl, y) uses the variables in tbl as candidate predictors and the external numeric vector y as the response. mdl = stepwiselm (X, y) fits a stepwise model of the response y to the predictor data X, an NxP numeric or logical matrix. By default, the predictors are named 'x1', 'x2', ..., 'xP' and the response is named 'y'. y is a vector with one element per row of X; a row vector is taken as a column. Initial Model, and Lower/Upper Bounds mdl = stepwiselm (..., InitialModel) additionally specifies the model to start the stepwise search from, using any of the input combinations shown above. InitialModel can be any of the following, and the same set of values can also be used for the 'Lower' and 'Upper' options below, which bound the smallest and largest set of terms stepwiselm is allowed to reach. Value Description 'constant' Model contains only an intercept term. This is the default InitialModel and default 'Lower' bound. 'linear' Model contains an intercept and one term for each predictor variable. 'interactions' Model contains an intercept, all linear terms, and all pairwise products of distinct predictor variables (no squared terms). This is the default 'Upper' bound. 'purequadratic' Model contains an intercept, all linear terms, and all squared terms. 'quadratic' Model contains an intercept, all linear terms, all pairwise products of distinct predictor variables, and all squared terms. 'polyijk' Model is a polynomial with maximum degree i in the first predictor, j in the second, and so on, given as a run of single-digit numerals, one per predictor (e.g. 'poly21' for two predictors). The model contains interaction terms, but the degree of each interaction term never exceeds the largest of the specified per-predictor degrees. terms matrix A TxP or Tx(P+1) numeric matrix, where T is the number of terms and P is the number of predictor variables, following the same convention as fitlm's terms matrix. When InitialModel is given as a terms matrix, the 'PredictorVars' option may not also be used. Wilkinson formula A character vector of the form 'y ~ terms'. When a formula is combined with 'ResponseVar' or 'PredictorVars', the formula's response and predictor terms must agree with those options, or stepwiselm errors. Options mdl = stepwiselm (..., Name, Value, ...) specifies additional options using one or more Name-Value pair arguments. Name Value 'Criterion' Criterion used to decide whether a term is added or removed at each step. One of 'sse' (default), 'aic', 'bic', 'rsquared', or 'adjrsquared'. For 'sse', the p-value of an F-test comparing the model with and without the candidate term is used; for the others, the raw change in the named quantity is used directly against 'PEnter'/'PRemove'. 'PEnter' Threshold to add a term. Defaults depend on 'Criterion': 0.05 for 'sse', 0 for 'aic'/'bic', 0.1 for 'rsquared', 0 for 'adjrsquared'. 'PRemove' Threshold to remove a term. Defaults depend on 'Criterion': 0.10 for 'sse', 0.01 for 'aic'/'bic', 0.05 for 'rsquared', -0.05 for 'adjrsquared'. 'NSteps' Maximum number of add/remove steps to take, given as a nonnegative integer. Default is unlimited. 'NSteps' set to 0 returns the initial model unchanged. 'Lower' Model specification (in the same form as InitialModel, above) describing terms that may never be removed from the model. Terms in 'Lower' are protected from removal, but are not automatically added if absent from InitialModel. Default is 'constant'. 'Upper' Model specification (in the same form as InitialModel, above) describing the largest set of terms stepwiselm may add. Default is 'interactions'. 'Verbose' Controls how much progress information is printed while stepping. 0 suppresses all output, 1 (default) prints the action taken at each step, 2 additionally prints the p-value or criterion value considered for every candidate term examined at each step. 'Intercept' A logical scalar indicating whether the initial model includes a constant (intercept) term. Only applies when InitialModel is a character vector model name (or omitted); ignored when InitialModel is a terms matrix or formula. Default is true. 'Weights' A numeric vector of nonnegative observation weights, one per observation. Default is a vector of ones. 'Exclude' A numeric or logical vector specifying observations to exclude from the fit. 'CategoricalVars' Specifies which predictor variables are treated as categorical, given as a vector of column indices, a logical vector, or a cell array of variable names (table input only). Each categorical predictor with L distinct categories is expanded into L-1 indicator variables, and stepwiselm always adds or removes that entire group of indicator variables together, in a single step, treating the categorical predictor as one term. 'VarNames' A cell array of character vectors naming the predictor and response variables, response last. Only applies when X and y are supplied directly, not table input. 'ResponseVar' A character vector naming the response variable, overriding the response that would otherwise be inferred (the last table variable, or 'y' for matrix input). 'PredictorVars' A cell array of character vectors naming which variables in tbl to consider as candidate predictors. By default, all variables in tbl other than the response are used. May not be combined with a terms-matrix InitialModel, and must agree with any formula-based InitialModel. The Steps property The returned model's Steps property records the trace, as a structure with seven fields. Start, Lower, and Upper are LinearFormula objects for the starting model and the two bounds; Criterion is the criterion as it was asked for; PEnter and PRemove are the thresholds it ran under; and History is a table with one row per step. History always carries Action ('Start', 'Add', or 'Remove'), TermName, Terms (the terms matrix after the step, over the model's variables), DF (the coefficient count after the step), and delDF (the change in it, negative for a removal). The remaining columns follow the criterion, which is why a history is read by name and not by position: Criterion Further columns 'SSE' FStat and pValue. 'AIC', 'BIC', 'Rsquared', 'AdjRsquared' one column named for the criterion, holding its value after the step, the starting model included. The first row is the starting model, named by its right-hand side. step appends to the history it inherits rather than beginning a new one, and inherits Criterion, Lower, PEnter, and PRemove from it; Steps.Start is then the model stepped from. Algorithm At each step, stepwiselm examines every term not currently in the model but within the 'Upper' bound, and every term currently in the model but not protected by the 'Lower' bound. If any term outside the model would improve it by at least 'PEnter', the best such term is added; otherwise, if any term inside the model falls short of 'PRemove', the worst such term is removed. The process repeats until neither an addition nor a removal improves the model, or until 'NSteps' steps have been taken. stepwiselm never adds a higher-order term unless all of its lower-order marginal terms are already in the model (e.g. it will not add x1:x2^2 unless both x1 and x2^2 are already present), and correspondingly never removes a lower-order term that a higher-order term still in the model depends on. At every step, if a term in the current model is found to be exactly redundant (linearly dependent on the other terms already in the model), it is removed immediately regardless of the 'Criterion' value. Because the final model depends on the initial model and the order in which terms are considered, stepwiselm finds a locally, but not necessarily globally, optimal model. Robust fitting cannot be combined with stepwise regression; do not pass 'RobustOpts' to stepwiselm. mdl is returned as a LinearModel object. See also the step method of LinearModel, which performs a single bounded round of stepwise search starting from an already-fitted model. See also: LinearModel, fitlm # name: # type: sq_string # elements: 1 # length: 88 Fit a linear regression model using stepwise regression and return a LinearModel object. statistics-release-2.1.2/inst/Regression/fitcox.m000066400000000000000000000200271527311104300220710ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{mdl} =} fitcox (@var{X}, @var{T}) ## @deftypefnx {statistics} {@var{mdl} =} fitcox (@var{tbl}, @var{respvar}) ## @deftypefnx {statistics} {@var{mdl} =} fitcox (@dots{}, @var{Name}, @var{Value}) ## ## Fit a Cox proportional hazards regression model. ## ## @code{@var{mdl} = fitcox (@var{X}, @var{T})} fits the Cox proportional ## hazards model ## ## @tex ## $$ h(x_i, t) = h_0(t)\exp\left(\sum_{j=1}^{p} x_{ij} b_j\right) $$ ## @end tex ## @ifnottex ## @math{h(x_i, t) = h_0(t) exp (x_i' b)} ## @end ifnottex ## ## to the @math{n}-by-@math{p} numeric matrix of predictors @var{X} and the ## @math{n}-by-1 vector of event times @var{T}, and returns a @code{CoxModel} ## object. @var{T} may instead be an @math{n}-by-2 matrix whose rows give a ## @math{(start, stop]} interval of exposure, the counting process form, in ## which an observation joins the risk set only after its start time. ## ## @math{h_0(t)} is the baseline hazard, which is left unspecified: the ## coefficients are estimated by maximizing the Cox partial likelihood, which ## does not involve it. @strong{@var{X} must not contain a column of ones}, ## the model having no constant term, since any constant is absorbed into that ## baseline. ## ## @code{@var{mdl} = fitcox (@var{tbl}, @var{respvar})} takes the data from the ## table @var{tbl}, using the variable named @var{respvar} as the response and ## every other variable as a predictor. A @code{categorical} variable is ## encoded as indicator columns, one per level bar the first, which the baseline ## hazard carries. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @multitable @columnfractions 0.25 0.75 ## @headitem Name @tab Value ## @item @qcode{"Baseline"} @tab The @var{X} values at which the baseline hazard ## is computed, either a scalar or a 1-by-@math{p} vector. The default is the ## mean of each numeric predictor and zero for each indicator column of a ## categorical predictor, taken within each stratum. Pass @qcode{0} for a ## hazard relative to the origin. The coefficients do not depend on this ## choice. ## @item @qcode{"Beta"} @tab The starting value of the iteration, a vector of ## length @math{p}. The default is @code{0.01 ./ std (@var{X})}. ## @item @qcode{"CategoricalPredictors"} @tab The predictors to treat as ## categorical, given as column indices, a logical vector, or a cell array of ## predictor names. Table variables of class @code{categorical} are detected ## without this argument. ## @item @qcode{"Censoring"} @tab A logical or 0/1 vector of length @math{n}, ## where 1 marks an observation right-censored at its recorded time. The ## default is a vector of zeros, so every observation is a recorded event. ## @item @qcode{"Frequency"} @tab A vector of length @math{n} of non-negative ## values giving the number of observations each row represents, or a weight. ## The default is a vector of ones. ## @item @qcode{"OptimizationOptions"} @tab A structure of iteration settings, ## as built by @code{statset ("fitcox")}. The fields used are ## @qcode{"MaxIter"}, @qcode{"TolX"} and @qcode{"Display"}. ## @item @qcode{"PredictorNames"} @tab A cell array of @math{p} predictor names. ## The default is @qcode{"X1"}, @qcode{"X2"}, and so on, or the table variable ## names. ## @item @qcode{"Stratification"} @tab A vector of length @math{n} of stratum ## labels. Each stratum carries its own baseline hazard and its own risk sets, ## while the coefficients are shared across all of them. ## @item @qcode{"TieBreakMethod"} @tab The method of handling tied event times, ## either @qcode{"breslow"} (default) or @qcode{"efron"}. ## @end multitable ## ## @code{fitcox} is the object interface to @code{coxphfit}, which fits the same ## model and returns the estimates as plain arrays. The two agree exactly; the ## object additionally reports the proportional hazards assumption tests and ## carries the @code{survival}, @code{hazardratio}, @code{coefci}, ## @code{linhyptest} and @code{plotSurvival} methods. ## ## @seealso{CoxModel, coxphfit, ecdf, statset} ## @end deftypefn function mdl = fitcox (X, T, varargin) if (nargin < 2) print_usage (); endif mdl = CoxModel (X, T, varargin{:}); endfunction %!demo %! ## Fit a Cox proportional hazards model to right-censored survival times %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! C = [0; 0; 1; 0; 0; 1; 0; 0; 1; 0]; %! mdl = fitcox (X, T, 'Censoring', C) %!demo %! ## The hazard of each observation relative to the average one %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = fitcox (X, T); %! hazardratio (mdl, X) %!demo %! ## Each stratum carries its own baseline hazard %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! S = [1; 1; 1; 1; 1; 2; 2; 2; 2; 2]; %! mdl = fitcox (X, T, 'Stratification', S); %! mdl.Baseline %!shared X, T, C, S %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! C = [0; 0; 1; 0; 0; 1; 0; 0; 1; 0]; %! S = [1; 1; 1; 1; 1; 2; 2; 2; 2; 2]; ## fitcox returns a CoxModel object %!test %! mdl = fitcox (X, T); %! assert_equal (class (mdl), 'CoxModel'); %! assert_equal (mdl.Coefficients.Beta, ... %! [-1.3886093196382836; 4.3814437183613322], 1e-8); ## It fits the same model as coxphfit %!test %! mdl = fitcox (X, T, 'Censoring', C); %! [b, logl] = coxphfit (X, T, 'Censoring', C); %! assert_equal (mdl.Coefficients.Beta, b, 1e-12); %! assert_equal (mdl.LogLikelihood, logl, 1e-12); ## The name-value pairs are fitcox's, not coxphfit's %!test %! mdl = fitcox (X, T, 'Censoring', C, 'TieBreakMethod', 'efron'); %! b = coxphfit (X, T, 'Censoring', C, 'Ties', 'efron'); %! assert_equal (mdl.Coefficients.Beta, b, 1e-12); %!test %! mdl = fitcox (X, T, 'Censoring', C, 'Stratification', S); %! b = coxphfit (X, T, 'Censoring', C, 'Strata', S); %! assert_equal (mdl.Coefficients.Beta, b, 1e-12); %!test %! mdl = fitcox (X, T, 'Beta', [0.1; -0.1]); %! b = coxphfit (X, T, 'B0', [0.1; -0.1]); %! assert_equal (mdl.Coefficients.Beta, b, 1e-12); ## A table names its own variables %!test %! tbl = table (X(:,1), X(:,2), T, 'VariableNames', {'age', 'trt', 'time'}); %! mdl = fitcox (tbl, 'time'); %! assert_equal (mdl.PredictorNames, {'age', 'trt'}); %! assert_equal (mdl.ResponseName, 'time'); %! assert_equal (char (mdl.Formula), 'time ~ age + trt'); %! assert_equal (mdl.Coefficients.Beta, ... %! [-1.3886093196382836; 4.3814437183613322], 1e-8); ## A categorical predictor is reference coded against its first level %!test %! g = categorical ({'a'; 'b'; 'a'; 'b'; 'a'; 'b'; 'a'; 'b'; 'a'; 'b'}); %! tbl = table (X(:,1), g, T, 'VariableNames', {'age', 'grp', 'time'}); %! mdl = fitcox (tbl, 'time'); %! assert_equal (mdl.PredictorNames, {'age', 'grp'}); %! assert_equal (mdl.Coefficients.Properties.RowNames, {'age'; 'grp_b'}); %! assert_equal (mdl.Coefficients.Beta, ... %! [-1.3886093196382836; 4.3814437183613322], 1e-8); %! assert_equal (mdl.Baseline, [5.9, 0], 1e-12); %! assert_equal (mdl.VariableInfo.IsCategorical, [false; true; false]); ## Errors %!error fitcox (1) %!error ... %! fitcox (table ([1; 2], [3; 4]), 'time') statistics-release-2.1.2/inst/Regression/fitglm.m000066400000000000000000000237161527311104300220670ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{mdl} =} fitglm (@var{X}, @var{y}) ## @deftypefnx {statistics} {@var{mdl} =} fitglm (@var{X}, @var{y}, @var{modelspec}) ## @deftypefnx {statistics} {@var{mdl} =} fitglm (@var{tbl}) ## @deftypefnx {statistics} {@var{mdl} =} fitglm (@var{tbl}, @var{modelspec}) ## @deftypefnx {statistics} {@var{mdl} =} fitglm (@dots{}, @var{Name}, @var{Value}) ## ## Fit a generalized linear regression model. ## ## @code{@var{mdl} = fitglm (@var{X}, @var{y})} fits a generalized linear model ## of the response vector @var{y} on the columns of the @math{n}-by-@math{p} ## numeric predictor matrix @var{X}, and returns a @code{GeneralizedLinearModel} ## object. @code{@var{mdl} = fitglm (@var{tbl})} instead takes the predictors ## and response from the table @var{tbl} (the last column is the response unless ## overridden). By default the response is @qcode{'normal'} with an identity ## link, an intercept is included, and the model is additive in the predictors. ## ## For the @qcode{'binomial'} distribution @var{y} holds the @emph{number of ## successes}, and the number of trials is given either by the ## @qcode{'BinomialSize'} pair or by passing @var{y} as an ## @math{n}-by-@math{2} matrix whose first column holds the successes and ## whose second holds the trials. The two forms describe the same model; when ## both are given, the trials supplied with the response are used. A trial ## count must be a positive integer, while a success count need not be whole. ## ## @var{modelspec} selects the model terms. It is either a Wilkinson formula ## string (e.g.@: @qcode{'y ~ x1 + x2*x3'}), a keyword (@qcode{'constant'}, ## @qcode{'linear'}, @qcode{'interactions'}, @qcode{'purequadratic'}, ## @qcode{'quadratic'}, or @qcode{'full'}), or a terms matrix. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @multitable @columnfractions 0.2 0.75 ## @headitem Name @tab Value ## @item @qcode{'Distribution'} @tab the response distribution: ## @qcode{'normal'} (default), @qcode{'binomial'}, @qcode{'poisson'}, ## @qcode{'gamma'}, or @qcode{'inverse gaussian'}. ## @item @qcode{'Link'} @tab the link function. Defaults to the canonical link ## of the distribution; accepts any link name understood by @code{glmfit} or a ## numeric exponent for a power link. ## @item @qcode{'Weights'} @tab a vector of nonnegative observation weights. ## @item @qcode{'Offset'} @tab a vector added as a fixed term to the linear ## predictor. ## @item @qcode{'BinomialSize'} @tab for the @qcode{'binomial'} distribution, ## the number of trials (a scalar or a per-observation vector); @var{y} holds ## the number of successes. @strong{Changed in 1.9.0}: @var{y} was previously ## read as the proportion of successes. Multiply an existing proportion by ## the trials to keep its meaning. ## @item @qcode{'Intercept'} @tab a logical value (default @qcode{true}) whether ## to include an intercept term. ## @item @qcode{'DispersionFlag'} @tab a logical value forcing the dispersion ## parameter to be estimated (@qcode{true}) or held at 1 (@qcode{false}). ## @item @qcode{'CategoricalVars'} @tab predictors to treat as categorical (a ## logical vector, numeric indices, or a cell array of names). ## @item @qcode{'Exclude'} @tab observations to exclude from the fit (a logical ## vector or numeric indices). ## @item @qcode{'VarNames'} @tab a cell array of @math{p + 1} variable names ## (predictors followed by the response) for numeric @var{X}. ## @item @qcode{'PredictorVars'}, @qcode{'ResponseVar'} @tab for table input, ## the predictor and response variable names. ## @end multitable ## ## A categorical predictor expands to indicator columns, one per level bar the ## reference level, which the intercept carries. When the model has no ## intercept, the @emph{first} categorical predictor is given an indicator for ## every one of its levels instead, so that its coefficients are the group ## means; any further categorical predictor stays reference coded, which keeps ## the design full rank. This differs from MATLAB, which omits the reference ## level whether or not an intercept is present and so cannot fit the reference ## group at all -- for a three-level grouping variable @code{g}, MATLAB fits ## @code{y ~ g - 1} with two coefficients, predicts exactly 0 for every ## observation in the omitted group, and reports a negative @math{R^2}. This ## implementation returns three coefficients, one per group. ## ## @seealso{GeneralizedLinearModel, fitlm, glmfit, glmval, lassoglm} ## @end deftypefn function mdl = fitglm (varargin) if (nargin < 1) print_usage (); endif arg1 = varargin{1}; if (istable (arg1)) [modelspec, nv] = split_modelspec (varargin(2:end)); mdl = GeneralizedLinearModel (arg1, [], modelspec, nv{:}); else if (nargin < 2) print_usage (); endif [modelspec, nv] = split_modelspec (varargin(3:end)); mdl = GeneralizedLinearModel (arg1, varargin{2}, modelspec, nv{:}); endif endfunction ## Split an optional leading model specification from the Name/Value pairs. function [modelspec, nv] = split_modelspec (rest) modelspec = 'linear'; nv = rest; if (! isempty (rest)) a = rest{1}; if ((ischar (a) && ! is_param_name (a)) || isnumeric (a)) modelspec = a; nv = rest(2:end); endif endif endfunction ## True if S names one of fitglm's Name/Value parameters. function tf = is_param_name (s) tf = ischar (s) && any (strcmpi (s, {'Distribution', 'Link', 'Weights', ... 'Offset', 'BinomialSize', 'Intercept', 'DispersionFlag', ... 'CategoricalVars', 'Exclude', 'VarNames', 'PredictorVars', ... 'ResponseVar'})); endfunction %!demo %! ## Poisson regression of counts on two predictors. %! X = [0.1, 1.2; 0.4, 0.7; 1.1, 0.2; 1.5, 1.9; 0.3, 0.5; 1.8, 1.1; 0.9, 0.3]; %! y = [1; 0; 2; 3; 1; 4; 2]; %! mdl = fitglm (X, y, 'Distribution', 'poisson') %!demo %! ## Logistic regression with an interaction, specified by a formula. %! X = [0.1, 1.2; 0.4, 0.7; 1.1, 0.2; 1.5, 1.9; 0.3, 0.5; 1.8, 1.1; 0.9, 0.3]; %! y = [0; 0; 1; 1; 0; 1; 1]; %! tbl = array2table ([X, y], 'VariableNames', {'x1', 'x2', 'y'}); %! mdl = fitglm (tbl, 'y ~ x1 + x2 + x1:x2', 'Distribution', 'binomial') %!shared X, yp, yb %! X = [ 0.37, 0.06, 1.76; -0.76, -1.52, 0.84; 0.76, -0.19, -0.47; ... %! -0.80, -2.74, -0.90; 0.08, 0.39, 1.05; -0.41, -0.03, 0.74; ... %! 0.23, 1.21, 0.35; 0.66, 0.94, 0.13; 0.66, -0.12, -0.06; ... %! 2.09, 1.33, -0.71; 1.50, 0.08, -0.52; 0.59, 0.07, -1.13; ... %! -1.17, -0.35, -1.28; 0.68, 0.63, -0.80; -0.69, 0.08, 0.41; ... %! 2.04, 0.96, -0.56]; %! yp = [5 2 0 3 1 1 0 1 2 1 3 0 0 1 1 3]'; %! yb = [1 1 1 0 0 1 1 1 1 1 1 0 0 0 0 1]'; ## Test results (values verified against MATLAB's fitglm) %!test %! ## Poisson fit: coefficients, deviance, log-likelihood, AIC. %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! assert_equal (mdl.Coefficients.Estimate, ... %! [-0.3420955; 1.2804868; -1.0743272; 0.8395779], 1e-6); %! assert_equal (mdl.Deviance, 7.403008, 1e-5); %! assert_equal (mdl.LogLikelihood, -18.543280, 1e-5); %! assert_equal (mdl.ModelCriterion.AIC, 45.086559, 1e-5); %! assert_equal (mdl.Rsquared.Deviance, 0.6627677, 1e-6); %!test %! ## coefTest reports a Wald F statistic versus the constant model. %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! [p, F, df] = coefTest (mdl); %! assert_equal (F, 3.685312, 1e-5); %! assert_equal (p, 0.04331745, 1e-7); %! assert_equal (df, 3); %!test %! ## coefCI uses the t distribution with the error degrees of freedom. %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! ci = coefCI (mdl); %! b = mdl.Coefficients.Estimate; se = mdl.Coefficients.SE; %! t = tinv (0.975, mdl.DFE); %! assert_equal (ci, [b - t .* se, b + t .* se], 1e-12); %!test %! ## devianceTest chi-square equals the drop from the null deviance. %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! dt = devianceTest (mdl); %! assert_equal (dt.chi2Stat(2), dt.Deviance(1) - dt.Deviance(2), 1e-10); %!test %! ## An interaction model names the cross term x1:x2. %! mdl = fitglm (X, yp, "interactions", "Distribution", "poisson"); %! assert_equal (any (strcmp (mdl.CoefficientNames, "x1:x2")), true); %! assert_equal (mdl.NumCoefficients, 7); %!test %! ## Leverage sums to the number of coefficients. %! mdl = fitglm (X, yb, "Distribution", "binomial"); %! assert_equal (sum (mdl.Diagnostics.Leverage), mdl.NumCoefficients, 1e-9); %!test # plotting methods and random run without error %! mdl = fitglm (X, yp, "Distribution", "poisson"); %! hf = figure ("visible", "off"); %! unwind_protect %! plotResiduals (mdl); %! plotResiduals (mdl, "fitted", "ResidualType", "Pearson"); %! plotDiagnostics (mdl); %! plotDiagnostics (mdl, "cookd"); %! plotEffects (mdl); %! plotAdjustedResponse (mdl, 1); %! plotAdded (mdl, "x2"); %! assert_equal (numel (random (mdl)), 16); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error fitglm () %!error ... %! fitglm ("a", [1;2]) %!error ... %! fitglm ([1, 2; 3, 4], [1; 0], 'Distribution', 'wibble') %!error ... %! fitglm ([1, 2; 3, 4], [1; 0], 'linear', 'foo', 1) statistics-release-2.1.2/inst/Regression/fitglme.m000066400000000000000000000276151527311104300222360ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{glme} =} fitglme (@var{tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{glme} =} fitglme (@dots{}, @var{name}, @var{value}) ## ## Fit a generalized linear mixed-effects model specified by a formula. ## ## @code{fitglme (@var{tbl}, @var{formula})} fits the generalized linear ## mixed-effects model described by @var{formula} to the table @var{tbl} and ## returns a @code{GeneralizedLinearMixedModel} object. ## ## @var{formula} uses the same syntax as @code{fitlme}: a response, a ## fixed-effects part, and one or more random-effects terms ## @code{(@var{expr} | @var{group})}, for example ## @qcode{"y ~ x + (1 | g)"}. The model is fitted by penalized ## quasi-likelihood. ## ## The following @var{name}/@var{value} pairs are accepted: ## ## @table @asis ## @item @qcode{"Distribution"} ## The response distribution: @qcode{"normal"} (default), @qcode{"binomial"}, or ## @qcode{"poisson"}. ## ## @item @qcode{"Link"} ## The link function: @qcode{"identity"}, @qcode{"logit"}, or @qcode{"log"}. ## The default is the canonical link of the chosen distribution. ## ## @item @qcode{"FitMethod"} ## @qcode{"MPL"} (maximum pseudo-likelihood, the default), @qcode{"REMPL"} ## (restricted MPL), @qcode{"Laplace"}, or @qcode{"ApproximateLaplace"}. The ## first two differ in the pseudo-likelihood used for the covariance parameters; ## the last two report the Laplace-approximated marginal log-likelihood. ## @end table ## ## Only the canonical links and the full (unstructured) random-effects ## covariance are currently supported. ## ## @seealso{GeneralizedLinearMixedModel, fitlme, fitglm} ## @end deftypefn function glme = fitglme (tbl, formula, varargin) if (nargin < 2) print_usage (); endif if (! isa (tbl, "table")) error ("fitglme: TBL must be a table."); endif if (! (ischar (formula) || (isstring (formula) && isscalar (formula)))) error ("fitglme: FORMULA must be a character vector."); endif formula = char (formula); ## --- options --- distr = "normal"; link = ""; method = "MPL"; if (mod (numel (varargin), 2) != 0) error ("fitglme: name/value arguments must come in pairs."); endif for i = 1:2:numel (varargin) switch (lower (char (varargin{i}))) case "distribution" distr = lower (char (varargin{i+1})); case "link" link = lower (char (varargin{i+1})); case "fitmethod" method = char (varargin{i+1}); otherwise error ("fitglme: unknown option '%s'.", char (varargin{i})); endswitch endfor if (! any (strcmp (distr, {"normal", "binomial", "poisson"}))) error ("fitglme: Distribution must be 'normal', 'binomial', or 'poisson'."); endif if (isempty (link)) link = canonical_link (distr); endif mkey = lower (method); if (! any (strcmp (mkey, {"mpl", "rempl", "laplace", "approximatelaplace"}))) error (strcat ("fitglme: FitMethod must be 'MPL', 'REMPL', 'Laplace',", ... " or 'ApproximateLaplace'.")); endif ## --- decompose the formula --- S = parseWilkinsonFormula (formula, "mixed"); if (! S.HasRandom) error (strcat ("fitglme: FORMULA must contain a random-effects term", ... " '(...|...)'; use fitglm for fixed-effects models.")); endif if (isempty (S.Response)) error ("fitglme: FORMULA must specify a response variable."); endif ## --- drop rows with missing values in any model variable --- vars = collect_vars (S); tvars = tbl.Properties.VariableNames; mask = true (height (tbl), 1); for i = 1:numel (vars) if (! ismember (vars{i}, tvars)) error ("fitglme: variable '%s' is not in the table.", vars{i}); endif col = tbl.(vars{i}); if (isnumeric (col)) mask = mask & all (! isnan (col), 2); endif endfor tbl = tbl(mask, :); ## --- fixed design (formula-term order) and random designs --- [X, y, fenames] = parseWilkinsonFormula (S.FixedFormula, "model_matrix", tbl); perm = formula_order (fenames, S.FixedTerms, S.FixedIntercept); X = X(:, perm); fenames = fenames(perm); nt = numel (S.Random); Z = cell (1, nt); G = cell (1, nt); renames = cell (1, nt); grnames = cell (1, nt); for k = 1:nt rhs = terms_to_rhs (S.Random(k).Terms, S.Random(k).Intercept); [Zk, ~, znames] = parseWilkinsonFormula (["~ ", rhs], "model_matrix", tbl); Z{k} = Zk; renames{k} = znames; G{k} = combine_groups (tbl, S.Random(k).GroupVars); grnames{k} = S.Random(k).Group; endfor ## --- fit --- fit = __glmefit__ (X, y, Z, G, distr, link, mkey); info = fit; info.X = X; info.y = y; info.CoefficientNames = fenames; info.GroupNames = grnames; info.REPred = renames; info.FitMethod = method; info.Formula = formula; info.ResponseName = S.Response; glme = GeneralizedLinearMixedModel (info); endfunction function lnk = canonical_link (distr) switch (distr) case "normal", lnk = "identity"; case "binomial", lnk = "logit"; case "poisson", lnk = "log"; endswitch endfunction ## --- formula helpers (shared conventions with fitlme) --- function vars = collect_vars (S) vars = {}; if (! isempty (S.Response)), vars{end+1} = strtrim (S.Response); endif vars = [vars, flatten_terms(S.FixedTerms)]; for k = 1:numel (S.Random) vars = [vars, flatten_terms(S.Random(k).Terms), S.Random(k).GroupVars]; endfor vars = unique (vars); endfunction function names = flatten_terms (terms) names = {}; for i = 1:numel (terms), names = [names, terms{i}]; endfor endfunction function perm = formula_order (names, terms, intercept) assigned = false (size (names)); perm = []; if (intercept) j = find (strcmp (names, "(Intercept)"), 1); if (! isempty (j)), perm(end+1) = j; assigned(j) = true; endif endif for t = 1:numel (terms) tvars = sort (terms{t}); for j = 1:numel (names) if (assigned(j)), continue; endif if (isequal (sort (strsplit (names{j}, ":")), tvars)) perm(end+1) = j; assigned(j) = true; endif endfor endfor perm = [perm, find(! assigned)']; endfunction function rhs = terms_to_rhs (terms, intercept) parts = cell (1, numel (terms)); for i = 1:numel (terms), parts{i} = strjoin (terms{i}, ":"); endfor if (intercept) if (isempty (parts)), rhs = "1"; else, rhs = strjoin (parts, " + "); endif else rhs = [strjoin(parts, " + "), " - 1"]; endif endfunction function g = combine_groups (tbl, gvars) if (numel (gvars) == 1) g = tbl.(gvars{1}); else keys = []; for i = 1:numel (gvars) [~, ~, idx] = unique (tbl.(gvars{i})); keys = [keys, idx(:)]; endfor [~, ~, g] = unique (keys, "rows"); endif endfunction %!demo %! ## Poisson mixed model with a random intercept per group. %! rng (42); %! randg ('state', 42); %! randp ('state', 42); %! g = reshape (repmat (1:6, 7, 1), [], 1); %! x = randn (42, 1); %! y = poissrnd (exp (0.3 + 0.5 * x + 0.2 * reshape (repmat (randn (1, 6), 7, 1), [], 1))); %! tbl = table (y, x, g); %! glme = fitglme (tbl, "y ~ x + (1 | g)", "Distribution", "poisson"); %! disp (glme.Coefficients); ## MATLAB-verified parity (fitglme R2026a) on the reference GLME data. %!shared tbl %! xL = [0.032760004 0.70410822 -0.8646718 -0.28869454 0.51276678 -1.4975462 ... %! -1.4527871 -0.80013541 -1.644209 1.5137701 0.72905543 0.20880758 1.0856145 ... %! 0.62862577 -0.87409978 1.9178276 0.09748204 0.50697633 1.0247569 ... %! -0.92789896 -0.88921018 -0.98322849 -0.031378913 0.86875961 -0.91481141 ... %! 0.034324163 -0.25025257 -1.0575644 -0.86131607 -0.35355444 0.82950729 ... %! -0.36874363 0.061580868 0.55803564 -0.1763803 1.0482413 1.0137831 ... %! -0.94876976 -0.010703972 -0.35149845 -1.6828735 -1.0493301]'; %! yBin = [0 0 0 0 1 0 0 1 0 1 0 0 0 1 1 0 0 0 0 0 0 0 0 1 1 0 0 0 0 1 1 0 1 1 ... %! 0 1 1 0 1 1 0 0]'; %! yPois = [3 3 1 1 1 1 1 2 1 5 2 0 5 0 1 5 0 2 0 0 1 0 0 5 3 0 1 0 0 1 1 1 2 2 ... %! 1 1 4 0 1 0 0 1]'; %! g = [1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 ... %! 6 1 2 3 4 5 6]'; %! tbl = table (yBin, yPois, xL, g); %!test # binomial logit, MPL -- matches MATLAB fitglme %! glme = fitglme (tbl, "yBin ~ xL + (1 | g)", "Distribution", "binomial"); %! assert_equal (isa (glme, "GeneralizedLinearMixedModel"), true); %! assert_equal (glme.Coefficients.Estimate, [-0.55912; 0.76062], 1e-3); %! assert_equal (glme.Coefficients.SE, [0.33856; 0.39971], 1e-3); %! assert_equal (glme.LogLikelihood, -92.58872, 1e-2); %!test # poisson log, REMPL -- non-degenerate random-effect variance %! glme = fitglme (tbl, "yPois ~ xL + (1 | g)", "Distribution", "poisson", ... %! "FitMethod", "REMPL"); %! assert_equal (glme.Coefficients.Estimate, [0.23092; 0.67809], 1e-3); %! [psi, ~] = covarianceParameters (glme); %! assert_equal (psi{1}, 0.015918, 1e-3); %! assert_equal (glme.LogLikelihood, -56.90298, 1e-2); %!test # Laplace reports the marginal log-likelihood %! glme = fitglme (tbl, "yBin ~ xL + (1 | g)", "Distribution", "binomial", ... %! "FitMethod", "Laplace"); %! assert_equal (glme.LogLikelihood, -25.36010, 1e-2); %!test # coefficient stats: DF = n - p, tStat = Estimate / SE %! glme = fitglme (tbl, "yPois ~ xL + (1 | g)", "Distribution", "poisson"); %! C = glme.Coefficients; %! assert_equal (C.DF, [40; 40]); %! assert_equal (C.tStat, C.Estimate ./ C.SE, 1e-10); %!test # metadata %! glme = fitglme (tbl, "yBin ~ xL + (1 | g)", "Distribution", "binomial"); %! assert_equal (glme.Distribution, "binomial"); %! assert_equal (glme.Link, "logit"); %! assert_equal (glme.FitMethod, "MPL"); %! assert_equal (glme.ResponseName, "yBin"); %!test # all 8 fits (binomial/poisson x MPL/REMPL/Laplace/ApproximateLaplace) %! fits = { "yBin", "MPL", [-0.5591172; 0.7606217], [0.3385596; 0.3997127], -92.58872; ... %! "yBin", "REMPL", [-0.5591172; 0.7606217], [0.3385596; 0.3997127], -92.75162; ... %! "yBin", "Laplace", [-0.5591172; 0.7606217], [0.3385596; 0.3997127], -25.3601; ... %! "yBin", "ApproximateLaplace", [-0.5591172; 0.7606217], [0.3385596; 0.3997127], -25.3601; ... %! "yPois", "MPL", [0.2317919; 0.6848705], [0.1450691; 0.1496462], -55.13664; ... %! "yPois", "REMPL", [0.2309183; 0.6780919], [0.1539524; 0.1521982], -56.90296; ... %! "yPois", "Laplace", [0.2317919; 0.6848705], [0.1450691; 0.1496462], -59.2123; ... %! "yPois", "ApproximateLaplace", [0.2317919; 0.6848705], [0.1450691; 0.1496462], -59.2123 }; %! for k = 1:rows (fits) %! dist = "binomial"; %! if (strcmp (fits{k,1}, "yPois")), dist = "poisson"; endif %! glme = fitglme (tbl, [fits{k,1} " ~ xL + (1 | g)"], "Distribution", dist, ... %! "FitMethod", fits{k,2}); %! assert_equal (glme.Coefficients.Estimate, fits{k,3}, 1e-3); %! assert_equal (glme.Coefficients.SE, fits{k,4}, 1e-3); %! assert_equal (glme.LogLikelihood, fits{k,5}, 1e-2); %! endfor ## Input validation %!error fitglme (table ()) %!error fitglme (magic (3), "y ~ x + (1|g)") %!error fitglme (table ((1:3)', "VariableNames", {"y"}), "y ~ (1|y)", "Distribution", "xxx") %!error fitglme (table ((1:3)', "VariableNames", {"y"}), "y ~ 1") %!error fitglme (table ((1:3)', "VariableNames", {"y"}), "y ~ (1|y)", "bogus", 1) statistics-release-2.1.2/inst/Regression/fitlm.m000066400000000000000000001012721527311104300217120ustar00rootroot00000000000000## Copyright (C) 2022 Andrew Penn ## Copyright (C) 2026 Avanish Salunke ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{mdl} =} fitlm (@var{X}, @var{y}) ## @deftypefnx {statistics} {@var{mdl} =} fitlm (@var{tbl}) ## @deftypefnx {statistics} {@var{mdl} =} fitlm (@var{tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{mdl} =} fitlm (@var{tbl}, @var{y}) ## @deftypefnx {statistics} {@var{mdl} =} fitlm (@dots{}, @var{modelspec}) ## @deftypefnx {statistics} {@var{mdl} =} fitlm (@dots{}, @var{Name}, @var{Value}, @dots{}) ## ## Fit a linear regression model to data and return a @code{LinearModel} ## object. ## ## The returned object stores the fitted coefficients, their standard errors, ## t-statistics, and p-values, summary statistics of the fit (@math{R^2}, ## RMSE, F-statistic, etc.), and the residuals and diagnostics of the fit, and ## exposes methods such as @code{predict}, @code{plotResiduals}, ## @code{coefTest}, @code{addTerms}, and @code{removeTerms} for further ## analysis of the fitted model. ## ## @subheading Basic Syntax ## ## @code{@var{mdl} = fitlm (@var{X}, @var{y})} fits a linear regression model ## of the response @var{y} to the predictor data @var{X}. Unless removed via ## the @qcode{'Intercept'} option, the fitted model contains a constant ## (intercept) term and one linear term for every column of @var{X}. ## ## @itemize ## @item ## @var{X} is an @math{N*P} numeric or logical matrix of predictor data, where ## rows correspond to observations and columns correspond to variables. By ## default, the predictors are named @qcode{'x1'}, @qcode{'x2'}, @dots{}, ## @qcode{'xP'}. ## @item ## @var{X} can also be a categorical vector of length @math{N}, representing a ## single categorical predictor. In this case @var{y} must be supplied as the ## next argument, and the predictor is named @qcode{'x1'} by default. ## @item ## @var{y} is a numeric or logical vector of @math{N} response values, one for ## each row of @var{X}; a row vector is taken as a column. By default, the ## response is named @qcode{'y'}. ## @end itemize ## ## @code{@var{mdl} = fitlm (@var{tbl})} fits a linear regression model using ## the variables contained in the table (or dataset) @var{tbl}. By default, ## the last variable in @var{tbl} is used as the response and all other ## variables are used as predictors. Variables that are @code{categorical} ## arrays, cell arrays of character vectors, or logical arrays are ## automatically treated as categorical predictors. ## ## @code{@var{mdl} = fitlm (@var{tbl}, @var{ResponseVarName})} fits a model ## using the variable named @var{ResponseVarName} in @var{tbl} as the ## response, and all remaining variables in @var{tbl} as predictors. ## ## @code{@var{mdl} = fitlm (@var{tbl}, @var{y})} fits a model using the ## variables in @var{tbl} as predictors and the external numeric vector ## @var{y} as the response. @var{y} must have @code{height (@var{tbl})} ## elements. ## ## @subheading Model Specification ## ## @code{@var{mdl} = fitlm (@dots{}, @var{modelspec})} additionally specifies ## the terms of the model to fit, using any of the input combinations shown ## above. @var{modelspec} can be any of the following. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Value} @tab @var{Description} ## ## @item @qcode{'constant'} @tab Model contains only an intercept term. ## ## @item @qcode{'linear'} @tab Model contains an intercept and one term ## for each predictor variable. This is the default when @var{modelspec} is ## not specified. ## ## @item @qcode{'interactions'} @tab Model contains an intercept, all ## linear terms, and all pairwise products of distinct predictor variables ## (no squared terms). ## ## @item @qcode{'purequadratic'} @tab Model contains an intercept, all ## linear terms, and all squared terms. ## ## @item @qcode{'quadratic'} @tab Model contains an intercept, all linear ## terms, all pairwise products of distinct predictor variables, and all ## squared terms. ## ## @item @qcode{'full'} @tab Model contains an intercept and all terms up ## to and including the full @math{P}-way interaction of the predictor ## variables, i.e. every combination of one or more distinct predictors. ## ## @item terms matrix @tab A @math{T*P} or @math{T*(P+1)} numeric matrix, ## where @math{T} is the number of terms and @math{P} is the number of ## predictor variables. Each row represents one term, and the value in ## column @math{j} is the exponent to which predictor @math{j} is raised in ## that term; a row of all zeros represents the intercept. If a ## @math{T*(P+1)} matrix is supplied, its last column (representing the ## response variable) must be all zeros. ## ## @item Wilkinson formula @tab A character vector of the form ## @qcode{'y ~ terms'} describing the response and predictor terms using ## Wilkinson notation. The variable name to the left of @qcode{'~'} is used ## as the response, overriding any response implied elsewhere in the call. ## @end multitable ## ## When @var{modelspec} is given as a Wilkinson formula, the following ## operators may be used on its right-hand side to build up @code{terms}: ## ## @multitable @columnfractions 0.12 0.38 0.5 ## @headitem Operator @tab Meaning @tab Example ## @item @code{+} @tab add a term @tab @qcode{'x1 + x2'} adds @code{x1} and ## @code{x2} as separate terms ## @item @code{-} @tab remove a term @tab @qcode{'x1*x2 - x1:x2'} removes the ## interaction, leaving only @code{x1} and @code{x2} ## @item @code{*} @tab cross two terms @tab @qcode{'x1*x2'} expands to ## @code{x1}, @code{x2}, @code{x1:x2} ## @item @code{:} @tab interaction only @tab @qcode{'x1:x2'} adds only the ## interaction term between @code{x1} and @code{x2} ## @item @code{^} @tab power / crossing limit @tab @qcode{'x^2'} adds ## @code{x} and @code{x^2}; @qcode{'(x1+x2)^2'} expands to @code{x1}, ## @code{x2}, @code{x1:x2} ## @item @code{-1} @tab remove intercept @tab @qcode{'x1 + x2 - 1'} fits the ## model without a constant term ## @end multitable ## ## A formula includes an intercept term by default; append @qcode{'- 1'} to ## the formula to omit it. For a categorical predictor, @code{fitlm} ## generates the necessary indicator (dummy) variables automatically from the ## formula, so a formula does not need to be changed when the underlying ## design matrix changes. ## ## @subheading Options ## ## @code{@var{mdl} = fitlm (@dots{}, @var{Name}, @var{Value}, @dots{})} ## specifies additional options using one or more @qcode{Name-Value} pair ## arguments, which may be combined with @var{modelspec} or used on their own. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Intercept'} @tab A logical scalar indicating whether to ## include a constant (intercept) term in the model. Default is @qcode{true}. ## This option only applies when @var{modelspec} is a character vector model ## name (or omitted); it is ignored when @var{modelspec} is a terms matrix or ## a Wilkinson formula, where the intercept is instead controlled by the ## matrix/formula itself. ## ## @item @qcode{'Weights'} @tab A numeric vector of nonnegative ## observation weights, with one element per observation, used to fit a ## weighted least squares model. Default is a vector of ones, i.e. an ## unweighted ordinary least squares fit. ## ## @item @qcode{'Exclude'} @tab A numeric or logical vector specifying ## observations to exclude from the fit, given as row indices into the ## original data or as a logical mask the same length as the number of ## observations. Excluded observations, together with any observation that ## contains a missing (@qcode{NaN}) value in a predictor or the response, are ## recorded in the @code{ObservationInfo} property of the fitted model but do ## not contribute to the fitted coefficients or summary statistics. ## ## @item @qcode{'CategoricalVars'} @tab Specifies which predictor ## variables are treated as categorical, given as a vector of column indices, ## a logical vector, or a cell array of variable names (only valid for table ## input). Each categorical predictor with @math{L} distinct categories is ## expanded into @math{L-1} indicator (dummy) variables, using the first ## category (in sorted or original order) as the reference level that is ## omitted from the design matrix. Variables that are already ## @code{categorical} arrays or cell arrays of character vectors are always ## treated as categorical, regardless of this option. ## ## @item @qcode{'VarNames'} @tab A cell array of character vectors ## naming the predictor and response variables, listed in order with the ## response variable name last, e.g. @code{@{"x1", "x2", "y"@}} for two ## predictors. Only applies when @var{X} and @var{y} (or a categorical ## vector and @var{y}) are supplied directly, since table variables already ## carry their own names. By default, predictors are named @qcode{'x1'}, ## @qcode{'x2'}, etc. and the response is named @qcode{'y'}. ## ## @item @qcode{'ResponseVar'} @tab A character vector naming the ## response variable, used to override the response variable name that would ## otherwise be inferred (the last table variable, or @qcode{'y'} for matrix ## input). ## ## @item @qcode{'PredictorVars'} @tab A cell array of character vectors ## naming which variables in @var{tbl} to use as predictors. By default, all ## variables in @var{tbl} other than the response variable are used as ## predictors. ## ## @item @qcode{'RobustOpts'} @tab Selects ordinary least squares or ## robust regression fitting. This value can be @qcode{'off'} (default, ## ordinary least squares), @qcode{'on'} (robust fitting using the ## @qcode{'bisquare'} weighting function), the name of one of the weighting ## functions below, a function handle for a custom weighting function, or a ## scalar structure with fields @qcode{RobustWgtFun} and @qcode{Tune} ## specifying the weighting function and its tuning constant. Robust fitting ## uses Iteratively Reweighted Least Squares (IRLS), refitting the model with ## updated observation weights until the coefficients converge. Supported ## weighting function names: @qcode{'andrews'}, @qcode{'bisquare'}, ## @qcode{'cauchy'}, @qcode{'fair'}, @qcode{'huber'}, @qcode{'logistic'}, ## @qcode{'ols'}, @qcode{'talwar'}, @qcode{'welsch'}, each with its own default ## tuning constant. ## @end multitable ## ## @subheading Algorithm ## ## @code{fitlm} solves the (weighted) least squares problem by applying a ## pivoted QR decomposition to the design matrix, which remains numerically ## stable even when predictors are collinear; coefficients corresponding to ## columns beyond the numerically detected rank of the design matrix are set ## to zero. Robust fits refine this ordinary least squares solution using ## IRLS as described above. Observations with missing values in any variable ## used by the model, or explicitly excluded via @qcode{'Exclude'}, are ## omitted from the fit entirely and flagged in @code{ObservationInfo}, but ## are otherwise not counted as errors. ## ## @var{mdl} is returned as a @code{LinearModel} object. If ## @qcode{'RobustOpts'} is anything other than @qcode{'off'}, the returned ## model is a robust fit rather than an ordinary least squares fit, and its ## @code{Robust} property is populated accordingly. ## ## A categorical predictor expands to indicator columns, one per level bar the ## reference level, which the intercept carries. When the model has no ## intercept, the @emph{first} categorical predictor is given an indicator for ## every one of its levels instead, so that its coefficients are the group ## means; any further categorical predictor stays reference coded, which keeps ## the design full rank. This differs from MATLAB, which omits the reference ## level whether or not an intercept is present and so cannot fit the reference ## group at all -- for a three-level grouping variable @code{g}, MATLAB fits ## @code{y ~ g - 1} with two coefficients, predicts exactly 0 for every ## observation in the omitted group, and reports a negative @math{R^2}. This ## implementation returns three coefficients, one per group. ## ## @seealso{LinearModel} ## @end deftypefn function mdl = fitlm (varargin) if (nargin < 1) error ("fitlm: Not enough input arguments."); endif ## List of Name-Value keys used to check if the response variable y is missing. nv_keys = {'varnames', 'intercept', 'responsevar', 'predictorvars', ... 'categoricalvars', 'exclude', 'weights', 'robustopts'}; is_nv = @(s) (ischar (s) || isstring (s)) && ... any (strcmpi (char (s), nv_keys)); arg1 = varargin{1}; rest = varargin(2:end); if (isa (arg1, 'categorical')) if (! isvector (arg1)) error (strcat ("fitlm: Predictor variables must be numeric vectors,", ... " numeric matrices, or categorical vectors.")); endif if (isempty (rest) || is_nv(rest{1})) error ("fitlm: Y argument is required unless X is a dataset or table."); endif y_arg = rest{1}; if (numel (y_arg) != size (arg1, 1)) error ("fitlm: Predictor and response variables must have the same length."); endif if (! isvector (y_arg) || (! isnumeric (y_arg) && ! islogical (y_arg))) error ("fitlm: Response variable must be a numeric vector."); endif pred_name = 'x1'; resp_name = 'y'; tail = rest(2:end); keep = true (1, numel (tail)); for k = 1:2:numel (tail)-1 if (ischar (tail{k}) && strcmpi (tail{k}, 'VarNames') && iscell (tail{k+1})) vn = tail{k+1}; if (numel (vn) >= 1); pred_name = vn{1}; endif if (numel (vn) >= 2); resp_name = vn{2}; endif keep(k:k+1) = false; endif endfor tail = tail(keep); tbl = table (arg1(:), double (y_arg(:)), 'VariableNames', {pred_name, resp_name}); mdl = fitlm (tbl, tail{:}); return; endif if (istable (arg1)) response = []; modelspec = []; nv_args = {}; if (! isempty (rest)) ## Even length starting with Name-Value key means all are Name-Value pairs if (mod (numel (rest), 2) == 0 && is_nv(rest{1})) nv_args = rest; else arg2 = rest{1}; after_arg2 = rest(2:end); n_rows = height (arg1); n_cols = width (arg1); col_names = arg1.Properties.VariableNames; if (ischar (arg2) || isstring (arg2)) s = char (arg2); if (any (s == '~')) ## Wilkinson formula string modelspec = s; if (mod (numel (after_arg2), 2) != 0) error ("fitlm: Name-Value arguments must be in pairs."); endif nv_args = after_arg2; elseif (any (strcmp (s, col_names))) ## Response variable name; ODD/EVEN for remainder response = s; [modelspec, nv_args] = lm_split_args (after_arg2); else ## Modelspec keyword or invalid string; LinearModel will validate modelspec = s; if (mod (numel (after_arg2), 2) != 0) error ("fitlm: Name-Value arguments must be in pairs."); endif nv_args = after_arg2; endif elseif (isnumeric (arg2) || islogical (arg2)) [nr2, nc2] = size (arg2); if (isempty (arg2)) error (strcat ("fitlm: The terms matrix must have one column", ... " for each variable in the dataset or table.")); elseif (nc2 == n_cols) ## Column count matches table, so it is a terms matrix if (! any (all (double (arg2) == 0, 1))) error (strcat ("fitlm: Cannot determine the response", ... " variable from the terms matrix.")); endif modelspec = double (arg2); if (mod (numel (after_arg2), 2) != 0) error ("fitlm: Name-Value arguments must be in pairs."); endif nv_args = after_arg2; elseif (nc2 == 1 && nr2 == n_rows) ## Single column matching table height is an external y vector response = double (arg2(:)); [modelspec, nv_args] = lm_split_args (after_arg2); else error ("fitlm: Predictor and response variables must have the same length."); endif else error ("fitlm: invalid second argument for table input."); endif endif endif mdl = LinearModel (arg1, response, modelspec, nv_args{:}); elseif ((isnumeric (arg1) || islogical (arg1)) && ismatrix (arg1)) n = size (arg1, 1); ## Ensure the response variable y is provided. if (isempty (rest) || is_nv(rest{1})) error ("fitlm: Y argument is required unless X is a dataset or table."); endif arg2 = rest{1}; ## Ensure the response variable has the correct length. if (max ([size(arg2), 0]) != n) error ("fitlm: Predictor and response variables must have the same length."); endif if (! isvector (arg2)) error ("fitlm: Response variable must be a numeric vector."); endif if (! isnumeric (arg2) && ! islogical (arg2)) error ("fitlm: Response variable must be a numeric vector."); endif y = double (arg2(:)); [modelspec, nv_args] = lm_split_args (rest(2:end)); mdl = LinearModel (arg1, y, modelspec, nv_args{:}); else error (strcat ("fitlm: Predictor variables must be numeric vectors,", ... " numeric matrices, or categorical vectors.")); endif endfunction ## If count is odd, the first element is the modelspec and the rest are Name-Value pairs. ## If count is even, there is no modelspec and all elements are Name-Value pairs. function [modelspec, nv_args] = lm_split_args (remaining) if (isempty (remaining)) modelspec = []; nv_args = {}; elseif (mod (numel (remaining), 2) == 1) modelspec = remaining{1}; nv_args = remaining(2:end); else modelspec = []; nv_args = remaining; endif endfunction %!demo %! %! ## The simplest call: a matrix of predictor data and a response vector. %! ## Ten students study for varying numbers of hours before an exam, with %! ## their resulting scores recorded. With no `modelspec`, `fitlm` fits an %! ## intercept plus one linear term per column of the predictor matrix. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Score = [52;55;61;64;70;73;77;81;85;90]; %! mdl = fitlm (Hours, Score) %!demo %! %! ## Table input with a Wilkinson formula, and a categorical predictor %! ## detected automatically from its cell-array type. %! ## Nine stores in three regions report ad spend and sales. `Region` is %! ## a cell array of strings, so it is expanded into indicator columns %! ## without needing `'CategoricalVars'` to say so explicitly. %! AdSpend = [10;20;30;15;25;35;12;22;32]; %! Region = {'North';'North';'North';'South';'South';'South'; ... %! 'East';'East';'East'}; %! Sales = [15;18;24;20;27;33;12;19;26]; %! T = table (AdSpend, Region, Sales, ... %! 'VariableNames', {'AdSpend','Region','Sales'}); %! mdl = fitlm (T, 'Sales ~ AdSpend + Region') %!demo %! %! ## A terms matrix used directly as modelspec, instead of a keyword or a %! ## formula string. %! ## Weekly sales depend on temperature and humidity. Each row of the %! ## terms matrix is one term, and each column is the exponent of one %! ## predictor in that term -- here, both main effects plus their %! ## interaction. %! Temp = [60;65;70;75;80;85;90]; %! Humidity = [30;35;40;45;50;55;60]; %! Sales = [200;230;260;300;340;370;410]; %! T_terms = [0 0; 1 0; 0 1; 1 1]; %! mdl = fitlm ([Temp, Humidity], Sales, T_terms) %!demo %! %! ## Observation weights and an excluded observation used together. %! ## Ten observations follow a roughly linear trend, except one clear %! ## outlier. `'Exclude'` leaves that observation out of the fit %! ## entirely, while `'Weights'` gives the remaining observations %! ## unequal influence on the fit. %! x = [1;2;3;4;5;6;7;8;9;10]; %! y = [10;13;15;40;22;25;29;33;35;39]; %! w = [1;1;2;1;2;1;2;1;2;1]; %! mdl = fitlm (x, y, 'Weights', w, 'Exclude', 4) %!demo %! %! ## Robust regression versus an ordinary fit, on data with a planted %! ## outlier. %! ## Twelve observations follow a linear trend with a small amount of %! ## noise, except one observation shifted far off the line. The %! ## ordinary fit is pulled toward the outlier; the robust fit, using %! ## iteratively reweighted least squares with the bisquare weighting %! ## function, downweights it instead. %! x = (1:12)'; %! y = 3 + 2*x + 0.5*sin (x); %! y(10) = y(10) + 30; %! mdl_ols = fitlm (x, y) %! mdl_robust = fitlm (x, y, 'RobustOpts', 'bisquare') %!demo %! %! ## Selecting specific predictors and a specific response by name from a %! ## larger table, using the `carsmall` data set. %! ## `'ResponseVar'` and `'PredictorVars'` pick out exactly which %! ## columns of the table to use, regardless of how many other variables %! ## the table also contains. %! load carsmall %! T = table (Weight, Acceleration, MPG); %! mdl = fitlm (T, 'ResponseVar', 'MPG', ... %! 'PredictorVars', {'Weight', 'Acceleration'}) %!demo %! y = [ 8.706 10.362 11.552 6.941 10.983 10.092 6.421 14.943 15.931 ... %! 22.968 18.590 16.567 15.944 21.637 14.492 17.965 18.851 22.891 ... %! 22.028 16.884 17.252 18.325 25.435 19.141 21.238 22.196 18.038 ... %! 22.628 31.163 26.053 24.419 32.145 28.966 30.207 29.142 33.212 ... %! 25.694 ]'; %! X = [1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5]'; %! %! mdl = fitlm (X, y, 'linear', 'CategoricalVars', 1) %!demo %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! brands = {'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'}; %! popper = {'oil', 'oil', 'oil'; 'oil', 'oil', 'oil'; 'oil', 'oil', 'oil'; ... %! 'air', 'air', 'air'; 'air', 'air', 'air'; 'air', 'air', 'air'}; %! %! T = table (brands(:), popper(:), 'VariableNames', {'brands', 'popper'}); %! mdl = fitlm (T, popcorn(:), 'interactions') %!test %! y = [ 8.706 10.362 11.552 6.941 10.983 10.092 6.421 14.943 15.931 ... %! 22.968 18.590 16.567 15.944 21.637 14.492 17.965 18.851 22.891 ... %! 22.028 16.884 17.252 18.325 25.435 19.141 21.238 22.196 18.038 ... %! 22.628 31.163 26.053 24.419 32.145 28.966 30.207 29.142 33.212 ... %! 25.694 ]'; %! X = [1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5]'; %! fitlm (X, y, 'CategoricalVars', 1); %! fitlm (X, y, 'constant', 'CategoricalVars', 1); %! fitlm (X, y, 'linear', 'CategoricalVars', 1); %! mdl = fitlm (X, y, 'linear', 'CategoricalVars', 1); %! assert_equal (mdl.Coefficients.Estimate(1), 10, 1e-04); %! assert_equal (mdl.Coefficients.Estimate(2), 7.99999999999999, 1e-09); %! assert_equal (mdl.Coefficients.Estimate(3), 8.99999999999999, 1e-09); %! assert_equal (mdl.Coefficients.Estimate(4), 11.0001428571429, 1e-09); %! assert_equal (mdl.Coefficients.Estimate(5), 19.0001111111111, 1e-09); %! assert_equal (mdl.Coefficients.SE(1), 1.01775379540949, 1e-09); %! assert_equal (mdl.Coefficients.SE(2), 1.64107868458008, 1e-09); %! assert_equal (mdl.Coefficients.SE(3), 1.43932122062479, 1e-09); %! assert_equal (mdl.Coefficients.SE(4), 1.48983900477565, 1e-09); %! assert_equal (mdl.Coefficients.SE(5), 1.3987687997822, 1e-09); %! assert_equal (mdl.Coefficients.tStat(1), 9.82555903510687, 1e-09); %! assert_equal (mdl.Coefficients.tStat(2), 4.87484242844031, 1e-09); %! assert_equal (mdl.Coefficients.tStat(3), 6.25294748040552, 1e-09); %! assert_equal (mdl.Coefficients.tStat(4), 7.38344399756088, 1e-09); %! assert_equal (mdl.Coefficients.tStat(5), 13.5834536158296, 1e-09); %! assert_equal (mdl.Coefficients.pValue(2), 2.85812420217862e-05, 1e-12); %! assert_equal (mdl.Coefficients.pValue(3), 5.22936741204002e-07, 1e-06); %! assert_equal (mdl.Coefficients.pValue(4), 2.12794763209106e-08, 1e-07); %! assert_equal (mdl.Coefficients.pValue(5), 7.82091664406755e-15, 1e-08); %!test %! popcorn = [5.5, 4.5, 3.5; 5.5, 4.5, 4.0; 6.0, 4.0, 3.0; ... %! 6.5, 5.0, 4.0; 7.0, 5.5, 5.0; 7.0, 5.0, 4.5]; %! brands = bsxfun (@times, ones (6, 1), [1, 2, 3]); %! popper = bsxfun (@times, [1; 1; 1; 2; 2; 2], ones (1, 3)); %! X = [brands(:), popper(:)]; %! mdl = fitlm (X, popcorn(:), 'interactions', 'CategoricalVars', [1, 2]); %! assert_equal (mdl.Coefficients.Estimate(1), 5.66666666666667, 1e-09); %! assert_equal (mdl.Coefficients.Estimate(2), -1.33333333333333, 1e-09); %! assert_equal (mdl.Coefficients.Estimate(3), -2.16666666666667, 1e-09); %! assert_equal (mdl.Coefficients.Estimate(4), 1.16666666666667, 1e-09); %! assert_equal (mdl.Coefficients.Estimate(6), -0.333333333333334, 1e-09); %! assert_equal (mdl.Coefficients.Estimate(7), -0.166666666666667, 1e-09); %! assert_equal (mdl.Coefficients.SE(1), 0.215165741455965, 1e-09); %! assert_equal (mdl.Coefficients.SE(2), 0.304290309725089, 1e-09); %! assert_equal (mdl.Coefficients.SE(3), 0.304290309725089, 1e-09); %! assert_equal (mdl.Coefficients.SE(4), 0.304290309725089, 1e-09); %! assert_equal (mdl.Coefficients.SE(6), 0.43033148291193, 1e-09); %! assert_equal (mdl.Coefficients.SE(7), 0.43033148291193, 1e-09); %! assert_equal (mdl.Coefficients.tStat(1), 26.3362867542108, 1e-09); %! assert_equal (mdl.Coefficients.tStat(2), -4.38178046004138, 1e-09); %! assert_equal (mdl.Coefficients.tStat(3), -7.12039324756724, 1e-09); %! assert_equal (mdl.Coefficients.tStat(4), 3.83405790253621, 1e-09); %! assert_equal (mdl.Coefficients.tStat(6), -0.774596669241495, 1e-09); %! assert_equal (mdl.Coefficients.tStat(7), -0.387298334620748, 1e-09); %! assert_equal (mdl.Coefficients.pValue(1), 5.49841502258254e-12, 1e-09); %! assert_equal (mdl.Coefficients.pValue(2), 0.000893505495903642, 1e-09); %! assert_equal (mdl.Coefficients.pValue(3), 1.21291454302428e-05, 1e-09); %! assert_equal (mdl.Coefficients.pValue(4), 0.00237798044119407, 1e-09); %! assert_equal (mdl.Coefficients.pValue(6), 0.453570536021938, 1e-09); %! assert_equal (mdl.Coefficients.pValue(7), 0.705316781644046, 1e-09); %! brands = {'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'; ... %! 'Gourmet', 'National', 'Generic'}; %! popper = {'oil', 'oil', 'oil'; 'oil', 'oil', 'oil'; 'oil', 'oil', 'oil'; ... %! 'air', 'air', 'air'; 'air', 'air', 'air'; 'air', 'air', 'air'}; %! T = table (brands(:), popper(:), 'VariableNames', {'brands', 'popper'}); %! mdl = fitlm (T, popcorn(:), 'interactions'); %!test %! load carsmall %! X = [Weight, Horsepower, Acceleration]; %! fitlm (X, MPG, 'constant'); %! mdl = fitlm (X, MPG, 'linear'); %! assert_equal (mdl.Coefficients.Estimate(1), 47.9767628118615, 1e-09); %! assert_equal (mdl.Coefficients.Estimate(2), -0.00654155878851796, 1e-09); %! assert_equal (mdl.Coefficients.Estimate(3), -0.0429433065881864, 1e-09); %! assert_equal (mdl.Coefficients.Estimate(4), -0.0115826516894871, 1e-09); %! assert_equal (mdl.Coefficients.SE(1), 3.87851641748551, 1e-09); %! assert_equal (mdl.Coefficients.SE(2), 0.00112741016370336, 1e-09); %! assert_equal (mdl.Coefficients.SE(3), 0.0243130608813806, 1e-09); %! assert_equal (mdl.Coefficients.SE(4), 0.193325043113178, 1e-09); %! assert_equal (mdl.Coefficients.tStat(1), 12.369874881944, 1e-09); %! assert_equal (mdl.Coefficients.tStat(2), -5.80228828790225, 1e-09); %! assert_equal (mdl.Coefficients.tStat(3), -1.76626492228599, 1e-09); %! assert_equal (mdl.Coefficients.tStat(4), -0.0599128364487485, 1e-09); %! assert_equal (mdl.Coefficients.pValue(1), 4.89570341688996e-21, 1e-09); %! assert_equal (mdl.Coefficients.pValue(2), 9.87424814144e-08, 1e-09); %! assert_equal (mdl.Coefficients.pValue(3), 0.0807803098213114, 1e-09); %! assert_equal (mdl.Coefficients.pValue(4), 0.952359384151778, 1e-09); %!shared X, y, yl, T1, T2, T3, C %! X = [1 2; 3 4; 5 6]; %! y = [2; 4; 5]; %! yl = logical ([1; 0; 1]); %! T1 = table ([1;2;3], [4;5;6], 'VariableNames', {'x1','x2'}); %! T2 = table ([1;2;3], [4;5;6], 'VariableNames', {'x1','y'}); %! T3 = table ([1;2;3], [4;5;6], [2;4;5], 'VariableNames', {'x1','x2','y'}); %! C = categorical ({'a';'b';'a'}); %!test %! assert_equal (class (fitlm (X, y)), 'LinearModel'); %!test %! assert_equal (class (fitlm (X, yl)), 'LinearModel'); %!test %! assert_equal (class (fitlm (X, y, 'linear')), 'LinearModel'); %!test %! assert_equal (class (fitlm (X, y, [1 0; 0 1])), 'LinearModel'); %!test %! assert_equal (class (fitlm (X, y, 'Intercept', false)), 'LinearModel'); %!test %! assert_equal (class (fitlm (X, y, 'linear', 'Weights', [1;2;1])), 'LinearModel'); %!test %! mdl = fitlm (C, y); %! assert_equal (class (mdl), 'LinearModel'); %! assert_equal (mdl.VariableNames, {'x1'; 'y'}); %!test %! mdl = fitlm (C, y, 'VarNames', {'grp', 'score'}); %! assert_equal (mdl.VariableNames, {'grp'; 'score'}); %!test %! assert_equal (class (fitlm (C, y, 'Intercept', false)), 'LinearModel'); %!test %! assert_equal (class (fitlm (T2)), 'LinearModel'); %!test %! assert_equal (class (fitlm (T3)), 'LinearModel'); %!test %! assert_equal (class (fitlm (T3, 'Exclude', [2])), 'LinearModel'); %!test %! assert_equal (class (fitlm (T2, 'y')), 'LinearModel'); %!test %! assert_equal (class (fitlm (T3, 'x1')), 'LinearModel'); %!test %! assert_equal (class (fitlm (T3, 'y ~ x1 + x2')), 'LinearModel'); %!test %! assert_equal (class (fitlm (T1, 'linear')), 'LinearModel'); %!test %! assert_equal (class (fitlm (T1, [2;4;5])), 'LinearModel'); %!test %! assert_equal (class (fitlm (T2, [0 0; 1 0])), 'LinearModel'); %!test %! assert_equal (class (fitlm (T2, 'y', 'linear', 'Intercept', false)), 'LinearModel'); %!error fitlm () %!error ... %! fitlm ('hello', y) %!error ... %! fitlm (struct ('a', 1), [1;2]) %!error ... %! fitlm (categorical ([1 2; 3 4]), y) %!error ... %! fitlm (C) %!error ... %! fitlm (C, 'Intercept', false) %!error ... %! fitlm (C, [1;2]) %!error fitlm (C, {'a';'b';'a'}) %!error fitlm (X) %!error ... %! fitlm (X, 'Weights', [1;1;1]) %!error ... %! fitlm (X, []) %!error ... %! fitlm (X, [1;2]) %!error fitlm (X, ones (3, 2)) %!error fitlm (X, {'1';'2';'3'}) %!error ... %! fitlm (T1, []) %!error ... %! fitlm (T1, ones (1, 2)) %!error ... %! fitlm (T1, ones (4, 1)) %!error ... %! fitlm (T1, ones (2, 3)) %!error fitlm (T1, {1, 2}) %!error fitlm (T2, 'y ~ x1', 'linear') %!error fitlm (T1, 'linear', 'Weights') %!error fitlm (T2, [0 0; 1 0], 'Weights') %!test %! Xr = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4]; %! yr = [1.1 1.9 3.2 3.9 5.1 6.2]; %! m1 = fitlm (Xr, yr); %! m2 = fitlm (Xr, yr'); %! assert_equal (m1.Coefficients.Estimate, m2.Coefficients.Estimate); statistics-release-2.1.2/inst/Regression/fitlme.m000066400000000000000000000316521527311104300220630ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{lme} =} fitlme (@var{tbl}, @var{formula}) ## @deftypefnx {statistics} {@var{lme} =} fitlme (@dots{}, @var{name}, @var{value}) ## ## Fit a linear mixed-effects model specified by a formula. ## ## @code{fitlme (@var{tbl}, @var{formula})} fits the linear mixed-effects model ## described by @var{formula} to the variables in the table @var{tbl}, and ## returns a @code{LinearMixedModel} object. ## ## @var{formula} is a character vector in Wilkinson notation extended with ## random-effects terms, for example @qcode{"y ~ x1 + x2 + (1 | g)"}. The part ## to the left of @code{~} names the response; the fixed-effects part uses the ## usual operators (@code{+}, @code{*}, @code{:}, @code{^}, and @code{-1} to ## drop the intercept); and each random-effects term ## @code{(@var{expr} | @var{group})} ## adds random intercepts and slopes @var{expr} grouped by the factor ## @var{group} (or an interaction of factors, e.g.@: @code{g1:g2}). As with ## fixed effects, a random intercept is implicit unless suppressed with ## @code{0} or @code{-1}. ## ## Rows of @var{tbl} with missing values in any model variable are removed ## before fitting. ## ## The following @var{name}/@var{value} pairs are accepted: ## ## @table @asis ## @item @qcode{"FitMethod"} ## The estimation criterion, @qcode{"ML"} (maximum likelihood, the default) or ## @qcode{"REML"} (restricted maximum likelihood). ## @end table ## ## @seealso{LinearMixedModel, fitlmematrix, fitlm, parseWilkinsonFormula} ## @end deftypefn function lme = fitlme (tbl, formula, varargin) if (nargin < 2) print_usage (); endif if (! isa (tbl, "table")) error ("fitlme: TBL must be a table."); endif if (! (ischar (formula) || (isstring (formula) && isscalar (formula)))) error ("fitlme: FORMULA must be a character vector."); endif formula = char (formula); ## --- options --- method = "ML"; if (mod (numel (varargin), 2) != 0) error ("fitlme: name/value arguments must come in pairs."); endif for i = 1:2:numel (varargin) switch (lower (char (varargin{i}))) case "fitmethod" method = upper (char (varargin{i+1})); if (! any (strcmp (method, {"ML", "REML"}))) error ("fitlme: FitMethod must be 'ML' or 'REML'."); endif otherwise error ("fitlme: unknown option '%s'.", char (varargin{i})); endswitch endfor ## --- decompose the formula --- S = parseWilkinsonFormula (formula, "mixed"); if (! S.HasRandom) error (strcat ("fitlme: FORMULA must contain a random-effects term", ... " '(...|...)'; use fitlm for fixed-effects models.")); endif if (isempty (S.Response)) error ("fitlme: FORMULA must specify a response variable."); endif if (! isempty (strfind (S.Response, ","))) error ("fitlme: multiple responses are not supported."); endif ## --- drop rows with missing values in any model variable --- vars = collect_vars (S); tvars = tbl.Properties.VariableNames; mask = true (height (tbl), 1); for i = 1:numel (vars) if (! ismember (vars{i}, tvars)) error ("fitlme: variable '%s' is not in the table.", vars{i}); endif col = tbl.(vars{i}); if (isnumeric (col)) mask = mask & all (! isnan (col), 2); endif endfor tbl = tbl(mask, :); ## --- fixed-effects design (reordered from the alphabetical 'model_matrix' ## column order to the formula's term order, to match MATLAB) --- [X, y, fenames] = parseWilkinsonFormula (S.FixedFormula, "model_matrix", tbl); perm = formula_order (fenames, S.FixedTerms, S.FixedIntercept); X = X(:, perm); fenames = fenames(perm); ## --- random-effects designs and grouping --- nt = numel (S.Random); Z = cell (1, nt); G = cell (1, nt); renames = cell (1, nt); grnames = cell (1, nt); for k = 1:nt rhs = terms_to_rhs (S.Random(k).Terms, S.Random(k).Intercept); [Zk, ~, znames] = parseWilkinsonFormula (["~ ", rhs], "model_matrix", tbl); Z{k} = Zk; renames{k} = znames; G{k} = combine_groups (tbl, S.Random(k).GroupVars); grnames{k} = S.Random(k).Group; endfor ## --- fit --- lme = fitlmematrix (X, y, Z, G, "FitMethod", method, ... "FixedEffectPredictors", fenames, "RandomEffectPredictors", renames, ... "RandomEffectGroups", grnames, "Formula", formula, ... "ResponseName", S.Response); endfunction ## All variable names referenced by the decomposed formula. function vars = collect_vars (S) vars = {}; if (! isempty (S.Response)) vars{end+1} = strtrim (S.Response); endif vars = [vars, flatten_terms(S.FixedTerms)]; for k = 1:numel (S.Random) vars = [vars, flatten_terms(S.Random(k).Terms), S.Random(k).GroupVars]; endfor vars = unique (vars); endfunction function names = flatten_terms (terms) names = {}; for i = 1:numel (terms) names = [names, terms{i}]; endfor endfunction ## Permutation putting the alphabetically-ordered model_matrix columns into the ## formula's term order: intercept first, then each fixed term (matched by its ## set of variables), with any unmatched columns (e.g. categorical dummies) ## appended in their original order. function perm = formula_order (names, terms, intercept) assigned = false (size (names)); perm = []; if (intercept) j = find (strcmp (names, "(Intercept)"), 1); if (! isempty (j)) perm(end+1) = j; assigned(j) = true; endif endif for t = 1:numel (terms) tvars = sort (terms{t}); for j = 1:numel (names) if (assigned(j)) continue; endif cvars = sort (strsplit (names{j}, ":")); if (isequal (cvars, tvars)) perm(end+1) = j; assigned(j) = true; endif endfor endfor perm = [perm, find(! assigned)']; endfunction ## Formula RHS string for a random-effects design expression. function rhs = terms_to_rhs (terms, intercept) parts = cell (1, numel (terms)); for i = 1:numel (terms) parts{i} = strjoin (terms{i}, ":"); endfor if (intercept) if (isempty (parts)) rhs = "1"; else rhs = strjoin (parts, " + "); endif else rhs = [strjoin(parts, " + "), " - 1"]; endif endfunction ## Grouping index for one random-effects term: a single grouping variable, or ## the observed combinations of an interaction of grouping variables. function g = combine_groups (tbl, gvars) if (numel (gvars) == 1) g = tbl.(gvars{1}); else keys = []; for i = 1:numel (gvars) [~, ~, idx] = unique (tbl.(gvars{i})); keys = [keys, idx(:)]; endfor [~, ~, g] = unique (keys, "rows"); endif endfunction %!demo %! ## Random-intercept model: sleep-study-like data with per-subject intercepts. %! subject = reshape (repmat (1:6, 5, 1), [], 1); %! days = repmat ((0:4)', 6, 1); %! b0 = reshape (repmat ([1 -1 0.5 -0.5 0.2 -0.2], 5, 1), [], 1); %! y = 250 + 10 * days + 15 * b0 + 3 * sin (1:30)'; %! tbl = table (y, days, subject); %! lme = fitlme (tbl, "y ~ days + (1 | subject)", "FitMethod", "REML"); %! disp (lme.Coefficients); ## --- MATLAB-verified parity: the formula path reproduces the design-matrix fit ## and MATLAB's fitlme (R2026a) on the reference 42-row data set. --- %!shared tbl, xL %! xL = [0.032760004 0.70410822 -0.8646718 -0.28869454 0.51276678 -1.4975462 ... %! -1.4527871 -0.80013541 -1.644209 1.5137701 0.72905543 0.20880758 1.0856145 ... %! 0.62862577 -0.87409978 1.9178276 0.09748204 0.50697633 1.0247569 ... %! -0.92789896 -0.88921018 -0.98322849 -0.031378913 0.86875961 -0.91481141 ... %! 0.034324163 -0.25025257 -1.0575644 -0.86131607 -0.35355444 0.82950729 ... %! -0.36874363 0.061580868 0.55803564 -0.1763803 1.0482413 1.0137831 ... %! -0.94876976 -0.010703972 -0.35149845 -1.6828735 -1.0493301]'; %! x2 = [0.68979276 0.0074354814 -0.45697437 -0.5636481 1.4567202 -0.97829955 ... %! -1.12922 -0.030542479 1.5847779 -0.87837755 0.24121762 0.68747601 ... %! -0.56728765 0.98895053 -0.39350661 0.85326015 0.36524343 0.15824977 ... %! -1.7665212 0.59808246 -0.55763708 -1.1982294 -2.1473319 0.22521416 ... %! 0.37034398 -1.880586 0.052941033 -0.70016994 0.2174853 -1.7797082 ... %! 0.51971317 -0.35551286 1.9845963 -1.3498848 -0.63514097 -0.78794714 ... %! 1.3681179 1.4423152 -0.51233905 0.30238864 2.0458136 0.17326323]'; %! yL = [3.5635971 -0.36763498 1.4192715 3.4471073 2.2760668 3.6420287 ... %! 4.4481522 1.5292777 5.437158 0.44016873 0.7259756 2.9771749 1.2941347 ... %! 0.26407508 1.9673466 0.77589984 1.2176078 0.8528384 -0.86522876 2.8651388 ... %! 2.0127931 3.1242841 -0.49164361 1.3192295 5.3782287 -0.40680701 2.3989882 ... %! 3.606108 1.9193197 1.6713765 2.4383124 1.9314844 3.5112706 0.91644553 ... %! 0.034923706 -0.15510033 2.1765206 2.5327412 3.1421219 3.3472574 5.343425 ... %! 3.8775899]'; %! g = [1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 ... %! 5 6 1 2 3 4 5 6]'; %! g2 = [1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 ... %! 2 3 1 2 3 1 2 3]'; %! tbl = table (yL, xL, x2, g, g2); %!test # every random-effects syntax form reproduces MATLAB's log-likelihood %! forms = { "yL ~ xL + x2 + (1|g)", -49.016009; ... %! "yL ~ xL + x2 + (1 + xL|g)", -48.280962; ... %! "yL ~ xL + x2 + (1|g) + (xL-1|g)", -48.947341; ... %! "yL ~ xL + x2 + (xL-1|g)", -61.635296; ... %! "yL ~ xL + x2 + (-1 + xL|g)", -61.635296; ... %! "yL ~ xL + x2 + (1|g) + (1|g2)", -45.924711; ... %! "yL ~ xL + x2 + (1|g) + (1|g:g2)", -49.016009; ... %! "yL ~ xL*x2 + (1|g)", -50.366415; ... %! "yL ~ xL + x2 + (1 + xL + x2|g)", -48.162461 }; %! for k = 1:rows (forms) %! lme = fitlme (tbl, forms{k,1}, "FitMethod", "REML"); %! assert_equal (lme.LogLikelihood, forms{k,2}, 1e-4); %! endfor %!test # random intercept via formula -- matches MATLAB fitlme %! lme = fitlme (tbl, "yL ~ xL + x2 + (1 | g)", "FitMethod", "REML"); %! assert_equal (isa (lme, "LinearMixedModel"), true); %! assert_equal (lme.Coefficients.Estimate, ... %! [1.96839; -1.37926; 0.811747], 1e-4); %! assert_equal (lme.Coefficients.SE, [0.376509; 0.113264; 0.0966571], 1e-5); %! assert_equal (lme.LogLikelihood, -49.016009, 1e-4); %! [psi, mse] = covarianceParameters (lme); %! assert_equal (psi{1}, 0.79429917, 1e-3); %! assert_equal (mse, 0.38539058, 1e-3); %!test # correlated random intercept + slope via formula -- matches MATLAB %! lme = fitlme (tbl, "yL ~ xL + x2 + (1 + xL | g)", "FitMethod", "REML"); %! assert_equal (lme.Coefficients.Estimate, ... %! [2.0034354; -1.3374715; 0.83788802], 1e-3); %! assert_equal (lme.LogLikelihood, -48.280962, 1e-3); %! psi = covarianceParameters (lme); %! assert_equal (psi{1}, ... %! [0.7846112, -0.14287556; -0.14287556, 0.026017248], 1e-3); %!test # formula fit equals the equivalent fitlmematrix fit %! lme_f = fitlme (tbl, "yL ~ xL + x2 + (1 | g)", "FitMethod", "REML"); %! X = [ones(42,1), xL, tbl.x2]; %! lme_m = fitlmematrix (X, tbl.yL, ones (42, 1), tbl.g, "FitMethod", "REML"); %! assert_equal (lme_f.Coefficients.Estimate, ... %! lme_m.Coefficients.Estimate, 1e-8); %! assert_equal (lme_f.LogLikelihood, lme_m.LogLikelihood, 1e-8); %!test # slope-only random term (no random intercept) %! lme = fitlme (tbl, "yL ~ xL + x2 + (xL - 1 | g)", "FitMethod", "REML"); %! [psi, ~] = covarianceParameters (lme); %! assert_equal (isscalar (psi{1}), true); # 1x1 covariance (slope only) %!test # metadata: Formula and ResponseName are stored %! lme = fitlme (tbl, "yL ~ xL + (1 | g)"); %! assert_equal (lme.Formula, "yL ~ xL + (1 | g)"); %! assert_equal (lme.ResponseName, "yL"); %! assert_equal (lme.FitMethod, "ML"); # default %!test # rows with missing values are dropped before fitting %! t2 = tbl; %! t2.yL(3) = NaN; t2.xL(10) = NaN; %! lme = fitlme (t2, "yL ~ xL + x2 + (1 | g)"); %! assert_equal (lme.NumObservations, 40); ## Input validation %!error fitlme (table ()) %!error fitlme (magic (3), "y ~ x + (1|g)") %!error fitlme (table (), 5) %!error fitlme (table ((1:3)', "VariableNames", {"y"}), "y ~ 1") %!error fitlme (table ((1:3)', "VariableNames", {"y"}), "y ~ (1|y)", "bogus", 1) %!error fitlme (table ((1:3)', "VariableNames", {"y"}), "y ~ (1|y)", "FitMethod", "xxx") statistics-release-2.1.2/inst/Regression/fitlmematrix.m000066400000000000000000000335411527311104300233070ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{lme} =} fitlmematrix (@var{X}, @var{y}, @var{Z}, @var{G}) ## @deftypefnx {statistics} {@var{lme} =} fitlmematrix (@dots{}, @var{name}, @var{value}) ## ## Fit a linear mixed-effects model from design matrices. ## ## @code{fitlmematrix (@var{X}, @var{y}, @var{Z}, @var{G})} fits the linear ## mixed-effects model ## @tex ## $$ y = X\beta + Zb + \varepsilon $$ ## ## @end tex ## @ifnottex ## @code{y = X*beta + Z*b + e} ## @end ifnottex ## with fixed-effects design @var{X}, response @var{y}, random-effects design ## @var{Z}, and grouping variable @var{G}. The random effects @var{b} are ## normally distributed with mean zero and an unstructured covariance @var{Psi} ## (shared across the levels of the grouping variable), and the observation ## errors are independent @code{N(0, sigma2)}. ## ## @var{X} is an @var{n}-by-@var{p} numeric matrix and @var{y} an @var{n}-by-1 ## response vector. @var{Z} is an @var{n}-by-@var{q} random-effects design and ## @var{G} an @var{n}-by-1 grouping variable (numeric, logical, char, cell array ## of strings, or categorical). To specify several grouping terms, pass @var{Z} ## and @var{G} as cell arrays of the same length, one design and one grouping ## variable per term. ## ## The following @var{name}/@var{value} pairs are accepted: ## ## @table @asis ## @item @qcode{"FitMethod"} ## The estimation criterion, either @qcode{"ML"} (maximum likelihood, the ## default) or @qcode{"REML"} (restricted maximum likelihood). ## ## @item @qcode{"FixedEffectPredictors"} ## A cell array of @var{p} names for the columns of @var{X} (default ## @code{@{"x1", @dots{}, "xp"@}}). ## ## @item @qcode{"RandomEffectPredictors"} ## A cell array (one entry per grouping term) of cell arrays naming the columns ## of each @var{Z} (default @code{z1, z2, @dots{}}). ## ## @item @qcode{"RandomEffectGroups"} ## A cell array of names for the grouping terms (default @code{g1, g2}, etc.). ## @end table ## ## The returned @var{lme} is a @code{LinearMixedModel} object describing the ## fitted model: the estimated fixed effects and their statistics ## (@code{lme.Coefficients}), the covariance parameters ## (@code{covarianceParameters}), the random-effects BLUPs ## (@code{randomEffects}), the log-likelihood, and methods for prediction, ## residuals, and hypothesis tests. ## ## Only the full (unstructured) random-effects covariance is currently ## supported. ## ## @seealso{LinearMixedModel, fitlm, parseWilkinsonFormula} ## @end deftypefn function lme = fitlmematrix (X, y, Z, G, varargin) if (nargin < 4) print_usage (); endif ## --- fixed-effects design and response --- if (! (isnumeric (X) && ismatrix (X) && isreal (X))) error ("fitlmematrix: X must be a real numeric matrix."); endif if (! (isnumeric (y) && isreal (y) && isvector (y))) error ("fitlmematrix: y must be a real numeric vector."); endif y = y(:); n = rows (X); if (numel (y) != n) error ("fitlmematrix: y must have as many elements as X has rows."); endif ## --- random-effects designs and grouping (normalise to cells) --- if (! iscell (Z)) Z = {Z}; endif if (! iscell (G)) G = {G}; endif if (numel (Z) != numel (G)) error ("fitlmematrix: Z and G must have the same number of terms."); endif nt = numel (Z); for k = 1:nt if (! (isnumeric (Z{k}) && ismatrix (Z{k}) && isreal (Z{k}))) error ("fitlmematrix: each Z must be a real numeric matrix."); endif if (rows (Z{k}) != n) error ("fitlmematrix: each Z must have as many rows as X."); endif G{k} = G{k}(:); if (numel (G{k}) != n) error ("fitlmematrix: each G must have as many elements as X has rows."); endif endfor ## --- name/value options --- method = "ML"; fenames = {}; renames = {}; grnames = {}; formula_str = ""; respname = ""; if (mod (numel (varargin), 2) != 0) error ("fitlmematrix: name/value arguments must come in pairs."); endif for i = 1:2:numel (varargin) name = varargin{i}; value = varargin{i+1}; if (! (ischar (name) || (isstring (name) && isscalar (name)))) error ("fitlmematrix: option names must be strings."); endif switch (lower (char (name))) case "fitmethod" method = upper (char (value)); if (! any (strcmp (method, {"ML", "REML"}))) error ("fitlmematrix: FitMethod must be 'ML' or 'REML'."); endif case "fixedeffectpredictors" fenames = cellstr (value); case "randomeffectpredictors" renames = value; case "randomeffectgroups" grnames = cellstr (value); case "formula" formula_str = char (value); case "responsename" respname = char (value); case "covariancepattern" pat = lower (char (value)); if (! any (strcmp (pat, {"fullcholesky", "full"}))) error (strcat ("fitlmematrix: only the 'FullCholesky' covariance", ... " pattern is currently supported.")); endif otherwise error ("fitlmematrix: unknown option '%s'.", char (name)); endswitch endfor p = columns (X); if (isempty (fenames)) fenames = arrayfun (@(j) sprintf ("x%d", j), 1:p, "UniformOutput", false); elseif (numel (fenames) != p) error ("fitlmematrix: FixedEffectPredictors must name every column of X."); endif if (isempty (grnames)) grnames = arrayfun (@(k) sprintf ("g%d", k), 1:nt, "UniformOutput", false); endif if (isempty (renames)) renames = cell (1, nt); for k = 1:nt renames{k} = arrayfun (@(j) sprintf ("z%d", j), 1:columns (Z{k}), ... "UniformOutput", false); endfor endif ## --- fit --- fit = __lmefit__ (X, y, Z, G, method); ## --- build the info struct and wrap it in a LinearMixedModel object --- info = fit; info.X = X; info.y = y; info.Zcell = Z; info.Gcell = G; info.CoefficientNames = fenames; info.GroupNames = grnames; info.REPred = renames; info.method = method; info.Formula = formula_str; info.ResponseName = respname; lme = LinearMixedModel (info); endfunction %!demo %! ## A random-intercept model on a small balanced data set: five subjects %! ## measured at four values of a predictor x. %! x = repmat ([1 2 3 4]', 5, 1); %! subject = reshape (repmat (1:5, 4, 1), [], 1); %! y = 2 + 0.8 * x + reshape (repmat ([1 -1 0.5 -0.5 0]', 1, 4)', [], 1) ... %! + 0.1 * sin (1:20)'; %! X = [ones(20,1), x]; %! lme = fitlmematrix (X, y, ones (20, 1), subject, "FitMethod", "REML"); %! beta = fixedEffects (lme); %! [psi, mse] = covarianceParameters (lme); %! printf ("intercept = %.4f, slope = %.4f\n", beta); %! printf ("between-subject var = %.4f, residual var = %.4f\n", psi{1}, mse); ## --- Balanced one-way random-intercept model: closed-form REML/ANOVA check --- ## For a balanced one-way design (a groups of r observations) the REML estimates ## coincide with the ANOVA estimates: sigma2 = MSE, tau2 = (MSB - MSE)/r, and ## the fixed intercept is the grand mean. %!test %! a = 5; r = 6; n = a*r; %! grp = reshape (repmat (1:a, r, 1), [], 1); %! yv = [ 4.1 4.5 3.8 4.3 4.0 4.2, 6.0 5.7 6.3 5.9 6.1 6.2, ... %! 2.2 2.5 2.0 2.4 2.1 2.3, 5.1 4.9 5.3 5.0 5.2 4.8, ... %! 3.3 3.6 3.1 3.4 3.2 3.5 ]'; %! X = ones (n, 1); %! lme = fitlmematrix (X, yv, ones (n, 1), grp, "FitMethod", "REML"); %! gm = mean (yv); %! gmeans = accumarray (grp, yv, [], @mean); %! SSB = r * sum ((gmeans - gm) .^ 2); MSB = SSB / (a - 1); %! SSE = sum ((yv - gmeans(grp)) .^ 2); MSE = SSE / (n - a); %! tau2 = (MSB - MSE) / r; %! [psi, mse] = covarianceParameters (lme); %! assert_equal (fixedEffects (lme), gm, 1e-8); %! assert_equal (mse, MSE, 1e-6); %! assert_equal (psi{1}, tau2, 1e-6); ## --- Internal consistency: sigma2 and covbeta satisfy their defining GLS ## relations at the returned fit. --- %!test %! a = 4; r = 5; n = a*r; %! grp = reshape (repmat (1:a, r, 1), [], 1); %! x = (1:n)' / n; %! yv = 1 + 2*x + reshape (repmat ([0.5 -0.5 0.2 -0.2], r, 1), [], 1) ... %! + 0.05 * cos (1:n)'; %! X = [ones(n,1), x]; %! lme = fitlmematrix (X, yv, ones (n, 1), grp, "FitMethod", "REML"); %! ## rebuild V = Psi over groups + sigma2 I and check the GLS identities %! [psi, mse] = covarianceParameters (lme); %! Zx = zeros (n, a); %! for l = 1:a, Zx(grp==l, l) = 1; end %! V = psi{1} * (Zx * Zx') + mse * eye (n); %! Vi = inv (V); %! beta = (X' * Vi * X) \ (X' * Vi * yv); %! assert_equal (fixedEffects (lme), beta, 1e-6); %! assert_equal (lme.CoefficientCovariance, inv (X' * Vi * X), 1e-5); ## --- MATLAB-verified parity: unbalanced random-intercept fit (ML). --- ## Reference values are MATLAB fitlmematrix (R2026a) on this exact data. %!shared X, yL, grp, xL, x2 %! xL = [0.032760004 0.70410822 -0.8646718 -0.28869454 0.51276678 -1.4975462 ... %! -1.4527871 -0.80013541 -1.644209 1.5137701 0.72905543 0.20880758 1.0856145 ... %! 0.62862577 -0.87409978 1.9178276 0.09748204 0.50697633 1.0247569 ... %! -0.92789896 -0.88921018 -0.98322849 -0.031378913 0.86875961 -0.91481141 ... %! 0.034324163 -0.25025257 -1.0575644 -0.86131607 -0.35355444 0.82950729 ... %! -0.36874363 0.061580868 0.55803564 -0.1763803 1.0482413 1.0137831 ... %! -0.94876976 -0.010703972 -0.35149845 -1.6828735 -1.0493301]'; %! x2 = [0.68979276 0.0074354814 -0.45697437 -0.5636481 1.4567202 -0.97829955 ... %! -1.12922 -0.030542479 1.5847779 -0.87837755 0.24121762 0.68747601 ... %! -0.56728765 0.98895053 -0.39350661 0.85326015 0.36524343 0.15824977 ... %! -1.7665212 0.59808246 -0.55763708 -1.1982294 -2.1473319 0.22521416 ... %! 0.37034398 -1.880586 0.052941033 -0.70016994 0.2174853 -1.7797082 ... %! 0.51971317 -0.35551286 1.9845963 -1.3498848 -0.63514097 -0.78794714 ... %! 1.3681179 1.4423152 -0.51233905 0.30238864 2.0458136 0.17326323]'; %! yL = [3.5635971 -0.36763498 1.4192715 3.4471073 2.2760668 3.6420287 ... %! 4.4481522 1.5292777 5.437158 0.44016873 0.7259756 2.9771749 1.2941347 ... %! 0.26407508 1.9673466 0.77589984 1.2176078 0.8528384 -0.86522876 2.8651388 ... %! 2.0127931 3.1242841 -0.49164361 1.3192295 5.3782287 -0.40680701 2.3989882 ... %! 3.606108 1.9193197 1.6713765 2.4383124 1.9314844 3.5112706 0.91644553 ... %! 0.034923706 -0.15510033 2.1765206 2.5327412 3.1421219 3.3472574 5.343425 ... %! 3.8775899]'; %! grp = [1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 ... %! 5 6 1 2 3 4 5 6]'; %! X = [ones(42,1), xL, x2]; %!test # returns a LinearMixedModel object %! lme = fitlmematrix (X, yL, ones (42, 1), grp, "FitMethod", "ML"); %! assert_equal (isa (lme, "LinearMixedModel"), true); %!test # random intercept, ML -- matches MATLAB fitlmematrix %! lme = fitlmematrix (X, yL, ones (42, 1), grp, "FitMethod", "ML"); %! [psi, mse] = covarianceParameters (lme); %! assert_equal (fixedEffects (lme), [1.9685543; -1.3771207; 0.81016147], 1e-4); %! assert_equal (mse, 0.36422151, 1e-4); %! assert_equal (psi{1}, 0.65265828, 1e-4); %! assert_equal (lme.LogLikelihood, -46.203282, 1e-4); %!test # correlated random intercept + slope, REML -- matches MATLAB %! Z = [ones(42,1), xL]; %! lme = fitlmematrix (X, yL, Z, grp, "FitMethod", "REML"); %! [psi, mse] = covarianceParameters (lme); %! assert_equal (fixedEffects (lme), [2.0034354; -1.3374715; 0.83788802], 1e-3); %! assert_equal (mse, 0.36649172, 1e-3); %! assert_equal (lme.LogLikelihood, -48.280962, 1e-3); %! assert_equal (psi{1}, ... %! [0.7846112, -0.14287556; -0.14287556, 0.026017248], 1e-3); %!test # ML and REML give different (both sensible) variance components %! lme_ml = fitlmematrix (X, yL, ones (42, 1), grp, "FitMethod", "ML"); %! lme_re = fitlmematrix (X, yL, ones (42, 1), grp, "FitMethod", "REML"); %! pml = covarianceParameters (lme_ml); %! pre = covarianceParameters (lme_re); %! assert_equal (pre{1} > pml{1}, true); %! # close, not equal %! assert_equal (fixedEffects (lme_ml), fixedEffects (lme_re), 5e-3); %!test # default FitMethod is ML %! l1 = fitlmematrix (X, yL, ones (42, 1), grp); %! l2 = fitlmematrix (X, yL, ones (42, 1), grp, "FitMethod", "ML"); %! assert_equal (l1.LogLikelihood, l2.LogLikelihood, 1e-10); %!test # names propagate to the fitted object %! lme = fitlmematrix (X, yL, ones (42, 1), grp, ... %! "FixedEffectPredictors", {"Int", "x", "x2"}); %! assert_equal (lme.CoefficientNames, {"Int", "x", "x2"}); ## Input validation %!error fitlmematrix (1, 2) %!error fitlmematrix ({1}, 1, 1, 1) %!error fitlmematrix (ones (3), {1}, 1, 1) %!error fitlmematrix (ones (3, 2), [1 2], ones (3, 1), [1 2 3]) %!error fitlmematrix (ones (3, 2), [1;2;3], {ones(3,1), ones(3,1)}, {[1;2;3]}) %!error fitlmematrix (ones (3, 2), [1;2;3], ones (2, 1), [1;2;3]) %!error fitlmematrix (ones (3, 2), [1;2;3], ones (3, 1), [1;2;3], "FitMethod", "xxx") %!error fitlmematrix (ones (3, 2), [1;2;3], ones (3, 1), [1;2;3], "FitMethod") %!error fitlmematrix (ones (3, 2), [1;2;3], ones (3, 1), [1;2;3], "bogus", 1) %!error fitlmematrix (ones (3, 2), [1;2;3], ones (3, 1), [1;2;3], "CovariancePattern", "Diagonal") statistics-release-2.1.2/inst/Regression/fitmnr.m000066400000000000000000000137611527311104300221030ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{mdl} =} fitmnr (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{mdl} =} fitmnr (@var{Tbl}, @var{Y}) ## @deftypefnx {statistics} {@var{mdl} =} fitmnr (@var{Tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{mdl} =} fitmnr (@var{Tbl}, @var{Formula}) ## @deftypefnx {statistics} {@var{mdl} =} fitmnr (@dots{}, @var{name}, @var{value}) ## ## Fit a multinomial regression model. ## ## @code{@var{mdl} = fitmnr (@var{X}, @var{Y})} fits a multinomial regression ## of the response @var{Y} on the columns of the @math{N}-by-@math{P} numeric ## predictor matrix @var{X}, and returns a @code{MultinomialRegression} object. ## An intercept is always fitted, so @var{X} holds no column of ones. ## ## @var{Y} is an @math{N}-by-1 numeric, logical, string or categorical vector, ## a cell array of character vectors, or a character matrix, each element or ## row the category of one observation; or an @math{N}-by-@math{K} matrix of ## counts, @code{@var{Y}(i,j)} observations of category @math{j} at the ## predictor values @code{@var{X}(i,:)}, as @code{mnrfit} documents. ## ## @code{@var{mdl} = fitmnr (@var{Tbl}, @var{Y})} takes the predictors from ## the variables of the table @var{Tbl}, and the response from @var{Y}. ## @code{@var{mdl} = fitmnr (@var{Tbl}, @var{ResponseVarName})} takes the ## response from the variable of @var{Tbl} that @var{ResponseVarName} names, a ## character vector or a string scalar, and the predictors from the others. ## @code{@var{mdl} = fitmnr (@var{Tbl}, @var{Formula})} takes both from the ## Wilkinson formula @var{Formula}, such as @qcode{'y ~ x1 + x2*x3'}. A ## numeric variable of a table is a continuous predictor and any other is ## categorical, coded as one indicator per level bar the first. ## ## The following @var{name}, @var{value} pairs are accepted: ## ## @multitable @columnfractions 0.3 0.65 ## @headitem Name @tab Value ## @item @qcode{'ModelType'} @tab @qcode{'nominal'} (default), a model ## contrasting each category with the last; @qcode{'ordinal'}, a model of the ## cumulative probabilities of ordered categories; or @qcode{'hierarchical'}, ## a model of each category against the ones after it. ## @item @qcode{'Link'} @tab The link function of an ordinal or hierarchical ## model: @qcode{'logit'} (default), @qcode{'probit'}, @qcode{'comploglog'} ## or @qcode{'loglog'}. A nominal model always uses the logit link. ## @item @qcode{'IncludeClassInteractions'} @tab @qcode{true} to give each ## category its own slopes, @qcode{false} for slopes shared by every ## category. The default is @qcode{true} for nominal and hierarchical models ## and @qcode{false} for ordinal ones. A nominal model without interactions ## fits its intercepts alone. ## @item @qcode{'EstimateDispersion'} @tab @qcode{true} to estimate the ## dispersion and scale the standard errors by it, @qcode{false} (default) for ## the theoretical dispersion of 1. ## @item @qcode{'Weights'} @tab An @math{N}-by-1 vector of nonnegative ## observation weights, each multiplying the counts of its observation; ones ## by default. ## @item @qcode{'CategoricalPredictors'} @tab The predictors to treat as ## categorical: a vector of indices, a logical vector with one element per ## predictor, a cell array or string array of names, or @qcode{'all'}. ## @item @qcode{'PredictorNames'} @tab The names of the predictors: for a ## matrix @var{X}, one per column, @qcode{x1}, @qcode{x2}, @dots{} by default; ## for a table, the variables to use as predictors. A formula names its own. ## @item @qcode{'ResponseName'} @tab The name of the response, @qcode{'y'} by ## default. A response variable name or a formula names its own. ## @item @qcode{'IterationLimit'} @tab The largest number of iterations of the ## fit, 100 by default. ## @item @qcode{'Tolerance'} @tab The convergence tolerance of the fit, the ## largest change of a coefficient relative to its value, @code{1e-6} by ## default. ## @end multitable ## ## A row holding a missing value of a predictor it uses, of the response, or ## of the weights is left out of the fit. ## ## @seealso{MultinomialRegression, mnrfit, mnrval, fitglm} ## @end deftypefn function mdl = fitmnr (varargin) if (nargin < 2) error ("fitmnr: too few input arguments."); endif mdl = MultinomialRegression (varargin{:}); endfunction %!demo %! ## A nominal model of the iris species on the length of the sepal, and %! ## the probabilities it gives three new flowers %! load fisheriris %! mdl = fitmnr (meas(:,1), species, 'PredictorNames', {'SepalLength'}) %! [label, probs] = predict (mdl, [5; 6; 7]) %!demo %! ## An ordinal model, the species ordered by size, and the cumulative %! ## probabilities it gives three new flowers %! load fisheriris %! mdl = fitmnr (meas(:,1), species, 'ModelType', 'ordinal') %! [~, cumprob] = predict (mdl, [5; 6; 7], 'ProbabilityType', 'cumulative') ## Test output %!test %! load fisheriris %! mdl = fitmnr (meas(:,1), species, 'ModelType', 'ordinal'); %! assert_equal (class (mdl), 'MultinomialRegression'); %!test %! load fisheriris %! mdl = fitmnr (meas(:,1), species, 'ModelType', 'ordinal'); %! B = mnrfit (meas(:,1), species, 'model', 'ordinal'); %! assert_equal (mdl.Coefficients.Value, B, 1e-12); ## Test input validation %!error fitmnr (1) statistics-release-2.1.2/inst/Regression/fitnlm.m000066400000000000000000000156771527311104300221050ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{mdl} =} fitnlm (@var{X}, @var{y}, @var{modelfun}, @var{beta0}) ## @deftypefnx {statistics} {@var{mdl} =} fitnlm (@var{tbl}, @var{modelfun}, @var{beta0}) ## @deftypefnx {statistics} {@var{mdl} =} fitnlm (@dots{}, @var{Name}, @var{Value}) ## ## Fit a nonlinear regression model. ## ## @code{@var{mdl} = fitnlm (@var{X}, @var{y}, @var{modelfun}, @var{beta0})} ## fits the nonlinear regression model @code{@var{y} = @var{modelfun} ## (@var{beta}, @var{X})} to the response vector @var{y} and the ## @math{n}-by-@math{p} predictor matrix @var{X}, starting the iterative fit ## from the coefficient vector ## @var{beta0}, and returns a @code{NonLinearModel} object. @var{modelfun} is a ## function handle @code{@@(@var{b}, @var{X})} returning the fitted responses. ## ## @code{@var{mdl} = fitnlm (@var{tbl}, @var{modelfun}, @var{beta0})} takes the ## predictors and response from the table @var{tbl}; the last column is the ## response unless overridden by @qcode{'ResponseVar'}. ## ## The following @var{Name}/@var{Value} pairs are accepted: ## ## @multitable @columnfractions 0.2 0.75 ## @headitem Name @tab Value ## @item @qcode{'CoefficientNames'} @tab a cell array of names for the ## coefficients (default @qcode{'b1'}, @qcode{'b2'}, @dots{}). ## @item @qcode{'Weights'} @tab a vector of nonnegative observation weights. ## @item @qcode{'ErrorModel'} @tab the error-variance model: @qcode{'constant'} ## (default), @qcode{'proportional'}, or @qcode{'combined'}. ## @item @qcode{'RobustWgtFun'} @tab the name of a robust weight function, ## enabling robust fitting (see @code{nlinfit}). ## @item @qcode{'Options'} @tab a statset-style options structure controlling ## the iterative fit (@qcode{MaxIter}, @qcode{TolFun}, @qcode{TolX}). ## @item @qcode{'PredictorVars'}, @qcode{'ResponseVar'} @tab for table input, ## the predictor and response variable names. ## @item @qcode{'VarNames'} @tab a cell array of @math{p + 1} variable names ## (predictors followed by the response) for numeric @var{X}. ## @item @qcode{'Exclude'} @tab observations to exclude from the fit. ## @end multitable ## ## @seealso{NonLinearModel, nlinfit, nlparci, nlpredci, fitlm, fitglm} ## @end deftypefn function mdl = fitnlm (varargin) if (nargin < 3) print_usage (); endif if (istable (varargin{1})) mdl = NonLinearModel (varargin{1}, [], varargin{2}, varargin{3}, ... varargin{4:end}); else if (nargin < 4) print_usage (); endif mdl = NonLinearModel (varargin{1}, varargin{2}, varargin{3}, ... varargin{4}, varargin{5:end}); endif endfunction %!demo %! ## Fit an exponential growth model and inspect the summary. %! x = [1:10]'; %! y = [2.1;2.9;4.2;5.3;7.1;9.4;12.8;16.5;22.1;29.8]; %! modelfun = @(b, x) b(1) .* exp (b(2) .* x); %! mdl = fitnlm (x, y, modelfun, [1; 0.3]) %!demo %! ## Predictions with 95% confidence intervals on the fitted curve. %! x = [1:10]'; %! y = [2.1;2.9;4.2;5.3;7.1;9.4;12.8;16.5;22.1;29.8]; %! modelfun = @(b, x) b(1) .* exp (b(2) .* x); %! mdl = fitnlm (x, y, modelfun, [1; 0.3]); %! [ypred, yci] = predict (mdl, [2.5; 5.5; 8.5]) %!shared X, y, modelfun, beta0 %! X = [1;2;3;4;5;6;7;8;9;10]; %! y = [2.1;2.9;4.2;5.3;7.1;9.4;12.8;16.5;22.1;29.8]; %! modelfun = @(b, x) b(1) .* exp (b(2) .* x); %! beta0 = [1; 0.3]; ## Values verified against MATLAB's fitnlm. %!test %! mdl = fitnlm (X, y, modelfun, beta0); %! assert_equal (mdl.Coefficients.Estimate, ... %! [1.683747025; 0.286911087], 1e-6); %! assert_equal (mdl.Coefficients.SE, [0.035194899; 0.002350913], 1e-6); %! assert_equal (mdl.Coefficients.tStat, [47.8406555; 122.042406], -1e-4); %! assert_equal (mdl.RMSE, 0.170942956, 1e-7); %! assert_equal (mdl.SSE, 0.233771954, 1e-7); %! assert_equal (mdl.SST, 750.976, 1e-3); %!test %! mdl = fitnlm (X, y, modelfun, beta0); %! assert_equal (mdl.Rsquared.Ordinary, 0.999688709, 1e-8); %! assert_equal (mdl.Rsquared.Adjusted, 0.999649798, 1e-8); %! assert_equal (mdl.LogLikelihood, 4.590586096, 1e-6); %! assert_equal (mdl.ModelCriterion.AIC, -5.181172193, 1e-6); %! assert_equal (mdl.ModelCriterion.BIC, -4.576002007, 1e-6); %!test %! ## coefCI matches beta +/- t * SE with the error degrees of freedom. %! mdl = fitnlm (X, y, modelfun, beta0); %! ci = coefCI (mdl); %! b = mdl.Coefficients.Estimate; se = mdl.Coefficients.SE; %! t = tinv (0.975, mdl.DFE); %! assert_equal (ci, [b - t .* se, b + t .* se], 1e-12); %!test %! ## Table input gives the same fit as matrix input. %! tbl = table (X, y, "VariableNames", {"x", "y"}); %! mdl = fitnlm (tbl, modelfun, beta0); %! assert_equal (mdl.Coefficients.Estimate, [1.683747025; 0.286911087], 1e-6); %! assert_equal (mdl.CoefficientNames, {"b1", "b2"}); %!test %! ## Custom coefficient names. %! mdl = fitnlm (X, y, modelfun, beta0, "CoefficientNames", {"A", "k"}); %! assert_equal (mdl.CoefficientNames, {"A", "k"}); %!test %! ## coefTest reports a Wald F statistic versus the zero model. %! mdl = fitnlm (X, y, modelfun, beta0); %! [p, F, df] = coefTest (mdl); %! assert_equal (df, 2); %! assert_equal (F > 1e5, true); %! assert_equal (p < 1e-10, true); %!test %! ## predict returns the fitted values and confidence intervals. %! mdl = fitnlm (X, y, modelfun, beta0); %! [yp, yci] = predict (mdl, [2.5; 5.5; 8.5]); %! assert_equal (yp, [3.449741842; 8.158274281; 19.293455074], 1e-6); %! assert_equal (yci(:,1), [3.329126146; 7.997938483; 19.121921613], 1e-5); %! assert_equal (yci(:,2), [3.570357538; 8.318610079; 19.464988535], 1e-5); %!test # plotting methods, feval, and random run without error %! mdl = fitnlm (X, y, modelfun, beta0); %! assert_equal (feval (mdl, [2.5; 5.5]), predict (mdl, [2.5; 5.5]), 1e-12); %! assert_equal (numel (random (mdl)), 10); %! hf = figure ("visible", "off"); %! unwind_protect %! plotResiduals (mdl); %! plotResiduals (mdl, "fitted"); %! plotDiagnostics (mdl); %! plotSlice (mdl); %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect ## Test input validation %!error fitnlm () %!error fitnlm ([1, 2], [1; 2]) %!error ... %! fitnlm ([1;2], [1;2], "bad", [1]) %!error ... %! fitnlm ([1;2;3], [1;2;3], @(b, x) b(1) * x, 1, "foo", 1) statistics-release-2.1.2/inst/Regression/fitrm.m000066400000000000000000000057221527311104300217230ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{rm} =} fitrm (@var{t}, @var{modelspec}) ## @deftypefnx {statistics} {@var{rm} =} fitrm (@var{t}, @var{modelspec}, @var{name}, @var{value}) ## ## Fit a repeated measures model. ## ## @code{@var{rm} = fitrm (@var{t}, @var{modelspec})} fits the repeated ## measures held in the table @var{t}, one row per subject, on the ## between-subject model @var{modelspec}, and returns a ## @code{RepeatedMeasuresModel} object. @var{modelspec} is a character vector ## or a string scalar of the form @qcode{'@var{responses} ~ @var{terms}'}: the ## responses a range of the table's variables such as @qcode{'y1-y6'}, a comma ## list such as @qcode{'y1,y2,y3'}, or both, and the terms a Wilkinson formula ## over the other variables, such as @qcode{'species'} or @qcode{'g*x'}, or ## @qcode{'1'} for an intercept alone. ## ## @code{@var{rm} = fitrm (@dots{}, @var{name}, @var{value})} takes the ## within-subject design and model as the name-value arguments ## @qcode{'WithinDesign'} and @qcode{'WithinModel'}; see ## @code{RepeatedMeasuresModel} for both. ## ## @code{ranova}, @code{epsilon} and @code{mauchly} test the fitted model. ## ## @seealso{RepeatedMeasuresModel, anova2, manova1} ## @end deftypefn function rm = fitrm (t, modelspec, varargin) if (nargin < 2) error ("fitrm: too few input arguments."); endif rm = RepeatedMeasuresModel (t, modelspec, varargin{:}); endfunction %!demo %! ## Four measurements on each iris, and whether they differ by species %! load fisheriris %! t = table (species, meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'species', 'meas1', 'meas2', 'meas3', 'meas4'}); %! Meas = table ([1 2 3 4]', 'VariableNames', {'Measurements'}); %! rm = fitrm (t, 'meas1-meas4 ~ species', 'WithinDesign', Meas) %! ranova (rm) %! mauchly (rm) %! epsilon (rm) %!test %! load fisheriris %! t = table (species, meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'species', 'meas1', 'meas2', 'meas3', 'meas4'}); %! rm = fitrm (t, 'meas1-meas4 ~ species'); %! assert_equal (class (rm), 'RepeatedMeasuresModel'); %!error fitrm () %!error fitrm (1) statistics-release-2.1.2/inst/Regression/glmfit.m000066400000000000000000001037541527311104300220700ustar00rootroot00000000000000## Copyright (C) 2024 Ruchika Sonagote ## Copyright (C) 2025 Swayam Shah ## Copyright (C) 2024-2025 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{b} =} glmfit (@var{X}, @var{y}, @var{distribution}) ## @deftypefnx {statistics} {@var{b} =} glmfit (@var{X}, @var{y}, @var{distribution}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{b}, @var{dev}] =} glmfit (@dots{}) ## @deftypefnx {statistics} {[@var{b}, @var{dev}, @var{stats}] =} glmfit (@dots{}) ## ## Perform generalized linear model fitting. ## ## @code{@var{b} = glmfit (@var{X}, @var{y}, @var{distribution})} returns a ## vector @var{b} of coefficient estimates for a generalized linear regression ## model of the responses in @var{y} on the predictors in @var{X}, using the ## distribution defined in @var{distribution}. ## ## @itemize ## @item @var{X} is an @math{n*p} numeric matrix of predictor variables with ## @math{n} observations and @math{p} predictors. ## @item @var{y} is an @math{n*1} numeric vector of responses for all supported ## distributions, except for the 'binomial' distribution in which case @var{y} ## can be either a numeric or logical @math{n*1} vector or an @math{n*2} ## matrix, where the first column contains the number of successes and the ## second column contains the number of trials. ## @item @var{distribution} is a character vector specifying the distribution of ## the response variable. Supported distributions are @qcode{'normal'}, ## @qcode{'binomial'}, @qcode{'poisson'}, @qcode{'gamma'}, and @qcode{'inverse ## gaussian'}. ## @end itemize ## ## @code{@var{b} = glmfit (@dots{}, @var{Name}, @var{Value})} specifies ## additional options using @qcode{Name-Value} pair arguments. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'B0'} @tab A numeric vector specifying initial values for ## the coefficient estimates. By default, the initial values are fitted values ## fitted from the data. ## ## @item @qcode{'Constant'} @tab A character vector specifying whether to ## include a constant term in the model. Valid options are @var{"on"} (default) ## and @var{"off"}. ## ## @item @qcode{'EstDisp'} @tab A character vector specifying whether to ## compute dispersion parameter. Valid options are @var{"on"} and @var{"off"}. ## For @qcode{'binomial'} and @qcode{'poisson'} distributions the default is ## @var{"off"}, whereas for the @qcode{'normal'}, @qcode{'gamma'}, and ## @qcode{'inverse gaussian'} distributions the default is @var{"on"}. ## ## @item @qcode{'link'} @tab A character vector specifying the name of a ## canonical link function or a numeric scalar for specifying a @qcode{'power'} ## link function. Supported canonical link functions include @qcode{'identity'} ## (default for @qcode{'normal'} distribution), @qcode{'log'} (default for ## @qcode{'poisson'} distribution), @qcode{'logit'} (default for ## @qcode{'binomial'} distribution), @qcode{'probit'}, @qcode{'loglog'}, ## @qcode{'comploglog'}, and @qcode{'reciprocal'} (default for the ## @qcode{'gamma'} distribution). The @qcode{'power'} link function is the ## default for the @qcode{'inverse gaussian'} distribution with @math{p = -2}. ## For custom link functions, the user can provide cell array with three ## function handles: the link function, its derivative, and its inverse, or ## alternatively a structure @var{S} with three fields: @qcode{S.Link}, ## @qcode{S.Derivative}, and @qcode{S.Inverse}. Each field can either contain a ## function handle or a character vector with the name of an existing function. ## All custom link functions must accept a vector of inputs and return a vector ## of the same size. ## ## @item @qcode{'Offset'} @tab A numeric vector of the same length as the ## response @var{y} specifying an offset variable in the fit. It is used as an ## additional predictor with a coefficient value fixed at 1. ## ## @item @qcode{'Options'} @tab A scalar structure containing the fields ## @qcode{MaxIter} and @qcode{TolX}. @qcode{MaxIter} must be a non-negative ## integer specifying the maximum number of iterations allowed for fitting the ## model, counted after the first weighted fit, as in MATLAB, so a value of 0 ## returns that first fit. @qcode{TolX} must be a positive scalar value ## specifying the termination tolerance. ## ## @item @qcode{'Weights'} @tab An @math{n*1} numeric vector of nonnegative ## values, where @math{n} is the number of observations in @var{X}. By default, ## it is @code{ones (n, 1)}. ## @end multitable ## ## @code{[@var{b}, @var{dev}] = glmfit (@dots{})} also returns the deviance of ## the fit as a numeric value in @var{dev}. Deviance is a generalization of the ## residual sum of squares. It measures the goodness of fit compared to a ## saturated model. ## ## The model is fitted by iteratively reweighted least squares. A step that ## takes the linear predictor outside the domain of the inverse link, as the ## power link of the @qcode{'inverse gaussian'} distribution does at or below ## zero, is halved back until the fitted means are valid. MATLAB stops at ## such a step with a warning that the weights are ill-conditioned and can ## return coefficients whose fitted means are not valid: with ## @code{u = (1:30)'}, for @code{glmfit (u, exp (0.1 * u) .* (1 + 0.2 * sin ## (u)), 'inverse gaussian')} it reports a deviance of 2.2064 where the fit ## reaches 1.9353. ## ## @code{[@var{b}, @var{dev}, @var{stats}] = glmfit (@dots{})} also returns the ## structure @var{stats}, which contains the model statistics in the following ## fields: ## ## @itemize ## @item @qcode{beta} - Coefficient estimates @var{b} ## @item @qcode{dfe} - Degrees of freedom for error ## @item @qcode{sfit} - Estimated dispersion parameter ## @item @qcode{s} - Theoretical or estimated dispersion parameter ## @item @qcode{estdisp} - @code{false} when @qcode{'EstDisp'} is @qcode{'off'} ## and @code{true} when @qcode{'EstDisp'} is @qcode{'on'} ## @item @qcode{covb} - Estimated covariance matrix for @var{b} ## @item @qcode{se} - Vector of standard errors of the coefficient estimates ## @var{b} ## @item @qcode{coeffcorr} - Correlation matrix for @var{b} ## @item @qcode{t} - @math{t} statistics for @var{b} ## @item @qcode{p} - @math{p}-values for @var{b} ## @item @qcode{resid} - Vector of residuals ## @item @qcode{residp} - Vector of Pearson residuals ## @item @qcode{residd} - Vector of deviance residuals ## @item @qcode{resida} - Vector of Anscombe residuals ## @end itemize ## ## @seealso{glmval} ## @end deftypefn function [b, dev, stats] = glmfit (X, y, distribution, varargin) ## Check input arguments if (nargin < 3) error ("glmfit: too few input arguments."); elseif (mod (nargin - 3, 2) != 0) error ("glmfit: Name-Value arguments must be in pairs."); elseif (! isnumeric (X) || isempty (X)) error ("glmfit: X must be a numeric matrix."); elseif (! (isnumeric (y) || islogical (y)) || isempty (y)) error ("glmfit: Y must be either a numeric matrix or a logical vector."); elseif (size (X, 1) != size (y, 1)) error ("glmfit: X and Y must have the same number of observations."); elseif (! ischar (distribution)) error ("glmfit: DISTRIBUTION must be a character vector."); endif ## Remove missing values xymissing = any (isnan (y), 2) | any (isnan (X), 2); y(xymissing) = []; X(xymissing,:) = []; [ny, cy] = size (y); [nx, cx] = size (X); ## Check y dimensions based on distribution if (strcmpi (distribution, 'binomial')) if (cy > 2) error (strcat ("glmfit: for a 'binomial' distribution,", " Y must be an n-by-1 or n-by-2 matrix.")); ## Get y and N for binomial distribution elseif (cy == 2) if (! isnumeric (y)) error (strcat ("glmfit: n-by-2 matrix Y for 'binomial' distribution", " must be numeric.")); endif N = y(:, 2); y = y(:, 1) ./ N; else if (islogical (y)) y = double (y); endif N = ones (size (y)); endif else if (cy != 1) error (strcat ("glmfit: for distributions other than 'binomial',", " Y must be an n-by-1 column vector.")); endif endif ## Set default link, variance, and deviance functions ## Set defaults for estimating dispersion parameter and limiting mu switch (tolower (distribution)) case 'normal' [flink, dlink, ilink] = getlinkfunctions ('identity'); varFun = @(mu) ones (size (mu)); devFun = @(mu, y) (y - mu) .^ 2; estDisp = true; case 'binomial' [flink, dlink, ilink] = getlinkfunctions ('logit'); varFun = @(mu, N) sqrt (mu) .* sqrt (1 - mu) ./ sqrt (N); devFun = @(mu, y, N) 2 * N .* (y .* log ((y + (y == 0)) ./ mu) + ... (1 - y) .* log ((1 - y + (y == 1)) ./ (1 - mu))); estDisp = false; muLimits = [eps, 1-eps]; case 'poisson' [flink, dlink, ilink] = getlinkfunctions ('log'); varFun = @(mu) sqrt (mu); devFun = @(mu, y) 2 * (y .* (log ((y + (y == 0)) ./ mu)) - (y - mu)); estDisp = false; muLimits = realmin; case 'gamma' [flink, dlink, ilink] = getlinkfunctions ('reciprocal'); varFun = @(mu) mu; devFun = @(mu, y) 2 * (-log (y ./ mu) + (y - mu) ./ mu); estDisp = true; muLimits = realmin; case 'inverse gaussian' [flink, dlink, ilink] = getlinkfunctions (-2); varFun = @(mu) mu .^ (3 / 2); devFun = @(mu, y) (((y - mu) ./ mu) .^ 2) ./ y; estDisp = true; muLimits = realmin; otherwise error ("glmfit: unsupported distribution."); endswitch ## Set defaults B0 = []; constant = true; offset = zeros (nx, 1); weight = ones (nx, 1); MaxIter = 100; TolX = 1e-6; ## Parse extra parameters while (numel (varargin) > 0) switch (tolower (varargin {1})) case 'b0' B0 = varargin {2}; if (! (isnumeric (B0) && isequal (size (B0), size (xymissing)))) error ("glmfit: 'B0' must be a numeric vector of the same size as Y."); endif B0(xymissing) = []; case 'constant' constant = tolower (varargin {2}); if (strcmpi (constant, 'on')) constant = true; elseif (strcmpi (constant, 'off')) constant = false; else error ("glmfit: 'Constant' should be either 'on' or 'off'."); endif case 'estdisp' estDisp = tolower (varargin {2}); if (strcmpi (estDisp, 'on')) estDisp = true; elseif (strcmpi (estDisp, 'off')) estDisp = false; else error ("glmfit: 'EstDisp' should be either 'on' or 'off'."); endif case 'link' linkArg = varargin {2}; ## Input validation is performed in private function [flink, dlink, ilink, errmsg] = getlinkfunctions (linkArg); if (! isempty (errmsg)) error ("glmfit: %s", errmsg); endif case 'options' options = varargin {2}; rf = {'MaxIter', 'TolX'}; if (! (isstruct (options) && all (ismember (rf, fieldnames (options))))) error (strcat ("glmfit: 'Options' must be a structure containing", " the fields 'MaxIter', and 'TolX'.")); endif MaxIter = options.MaxIter; TolX = options.TolX; if (! isscalar (MaxIter) || MaxIter < 0 || fix (MaxIter) != MaxIter) error (strcat ("glmfit: 'MaxIter' in 'Options' structure must", ... " be a non-negative integer.")); endif if (! isscalar (TolX) || TolX <= 0) error (strcat ("glmfit: 'TolX' in 'Options' structure must", " be a positive scalar.")); endif case 'offset' offset = varargin {2}; if (! (isnumeric (offset) && isequal (size (offset), size (xymissing)))) error (strcat ("glmfit: 'Offset' must be a numeric vector", " of the same size as Y.")); endif offset(xymissing) = []; case 'weights' weight = varargin {2}; if (! (isnumeric (weight) && isequal (size (weight), size (xymissing)))) error (strcat ("glmfit: 'Weights' must be a numeric vector", " of the same size as Y.")); endif weight(xymissing) = []; otherwise error ("glmfit: unknown parameter name."); endswitch varargin(1:2) = []; endwhile ## Adjust X based on constant if (constant) X = [ones(nx, 1), X]; cx += 1; endif ## Check X for rank deficiency [~, R, P] = qr (X .* weight(:, ones (1, cx)), 0); if (isempty (R)) rankX = 0; else rankX = sum (abs (diag (R)) > abs (R(1)) * max (nx, cx) * eps); endif if (rankX < cx) warning ("glmfit: X is ill-conditioned."); P = P(1:rankX); X = X(:,P); else P = [1:cx]; endif ## Adjust number of observations for zero weights if (any (weight == 0)) nx = nx - sum (weight == 0); endif ## Initialize mu and eta if (isempty (B0)) # from y switch (distribution) case 'binomial' mu = (N .* y + 0.5) ./ (N + 1); case 'poisson' mu = y + 0.25; case {'gamma', 'inverse gaussian'} mu = max (y, eps); otherwise mu = y; endswitch eta = flink(mu); else # from coefficient estimates eta = offset + X * B0(:); mu = ilink(eta); endif ## Initialize coefficient vector and iterations. The first weighted fit is ## not counted, as in MATLAB, so MaxIter allows MaxIter + 1 fits. numc = size (X, 2); Bnew = zeros (numc, 1); seps = sqrt (eps); iter = 0; converged = false; while (iter <= MaxIter) iter += 1; Bold = Bnew; ## Compute iteratively reweighted least squares weights d_eta = dlink(mu); if (strcmpi (distribution, 'binomial')) IRLS = abs (d_eta) .* varFun(mu, N); else IRLS = abs (d_eta) .* varFun(mu); endif squaredWeight = sqrt (weight) ./ IRLS; ## Estimate coefficients z_off = eta + (y - mu) .* d_eta - offset; yweighted = (z_off) .* squaredWeight; Xweighted = X .* squaredWeight(:, ones (1, numc)); [Q, R] = qr (Xweighted, 0); Bnew = R \ (Q' * yweighted); ## Compute predicted mean using current linear predictor. A step that ## leaves the domain of the inverse link, as a power link does at a ## linear predictor at or below zero, is halved back towards the last ## linear predictor until the mean is real and finite again. eta_new = offset + X * Bnew; mu = ilink(eta_new); halvings = 0; while ((! isreal (mu) || ! all (isfinite (mu))) && halvings < 60) eta_new = (eta_new + eta) / 2; mu = ilink(eta_new); halvings += 1; endwhile eta = eta_new; ## Force predicted mean within distribution support limits if (strcmpi (distribution, 'normal')) mu = mu; elseif (strcmpi (distribution, 'binomial')) mu = max (min (mu, muLimits(2)), muLimits(1)); else # for "poisson", "gamma", and "inverse gaussian" distributions mu = max (mu, muLimits(1)); endif ## Break if TolX is reached if (! any (abs (Bnew - Bold) > TolX * max (seps, abs (Bold)))) converged = true; break; endif endwhile ## Warn if iteration limit is reached if (! converged) warning ("glmfit: maximum number of iterations has been reached."); endif ## Return estimated coefficients if (rankX < cx) b = zeros (cx, 1); b(P) = Bnew; else b = Bnew; endif ## Compute deviance if (nargout > 1) if (strcmpi (distribution, 'binomial')) devn = devFun(mu, y, N); dev = sum (weight .* devn); else devn = devFun(mu, y); dev = sum (weight .* devn); endif endif ## Compute stats if (nargout > 2) ## Store coefficient estimates stats.beta = b; ## Compute degrees of freedom for error stats.dfe = max (nx - numc, 0); ## Compute estimated dispersion parameter if (stats.dfe > 0) switch (tolower (distribution)) case 'normal' stats.sfit = sum (weight .* (y - mu) .^ 2) / stats.dfe; case 'binomial' stats.sfit = sum (weight .* (y - mu) .^ 2 ./ ... (mu .* (1 - mu) ./ N)) / stats.dfe; case 'poisson' stats.sfit = sum (weight .* (y - mu) .^ 2 ./ mu) / stats.dfe; case 'gamma' stats.sfit = sum (weight .* ((y - mu) ./ mu) .^ 2) / stats.dfe; case 'inverse gaussian' stats.sfit = sum (weight .* ((y - mu) ./ mu .^ (3 / 2)) .^ 2) / ... stats.dfe; endswitch else stats.sfit = NaN; endif ## Store theoretical or estimated dispersion parameter if (estDisp) stats.s = stats.sfit; stats.estdisp = estDisp; else stats.s = 1; stats.estdisp = estDisp; endif ## Compute covariance matrix, standard errors, correlation matrix, ## t-statistic, and p-value for coefficient estimates if (isnan (stats.s)) stats.covb = NaN (numel (b)); stats.se = NaN (size (b)); stats.coeffcorr = NaN (numel (b)); stats.t = NaN (size (b)); stats.p = NaN (size (b)); else stats.covb = zeros (cx, cx); stats.se = zeros (cx, 1); stats.coeffcorr = zeros (cx, cx); stats.t = NaN (cx, 1); stats.p = NaN (cx, 1); RI = R \ eye (numc); C = RI * RI'; if (estDisp) C = C * stats.s; endif stats.covb(P,P) = C; se = sqrt (diag (C)); se = se(:); stats.se(P) = se; C = C ./ (se * se'); stats.coeffcorr(P,P) = C; stats.t(P) = b(P) ./ se; if (estDisp) stats.p = 2 * tcdf (-abs (stats.t), stats.dfe); else stats.p = 2 * normcdf (-abs (stats.t)); endif endif ## Compute residuals stats.resid = NaN (size (xymissing)); # Vector of residuals stats.residp = NaN (size (xymissing)); # Vector of Pearson residuals stats.residd = NaN (size (xymissing)); # Vector of deviance residuals stats.resida = NaN (size (xymissing)); # Vector of Anscombe residuals if (isequal (distribution, 'binomial')) stats.resid(! xymissing) = (y - mu) .* N; stats.residp(! xymissing) = (y - mu) ./ (varFun(mu, N) + (y == mu)); else stats.resid(! xymissing) = y - mu; stats.residp(! xymissing) = (y - mu) ./ (varFun(mu) + (y == mu)); endif stats.residd(! xymissing) = sign (y - mu) .* sqrt (max (0, devn)); switch (tolower (distribution)) case 'normal' stats.resida(! xymissing) = y - mu; case 'binomial' ab = 2 / 3; stats.resida(! xymissing) = beta (ab, ab) ... * (betainc (y, ab, ab) - betainc (mu, ab, ab)) ... ./ ((mu .* (1 - mu)) .^ (1 / 6) ./ sqrt (N)); case 'poisson' stats.resida(! xymissing) = 1.5 * ((y .^ (2 / 3) - mu .^ (2 / 3)) ... ./ mu .^ (1 / 6)); case 'gamma' pwr = 1 / 3; stats.resida(! xymissing) = 3 * (y .^ pwr - mu .^ pwr) ./ mu .^ pwr; case 'inverse gaussian' stats.resida(! xymissing) = (log (y) - log (mu)) ./ mu; endswitch endif endfunction %!demo %! x = [210, 230, 250, 270, 290, 310, 330, 350, 370, 390, 410, 430]'; %! n = [48, 42, 31, 34, 31, 21, 23, 23, 21, 16, 17, 21]'; %! y = [1, 2, 0, 3, 8, 8, 14, 17, 19, 15, 17, 21]'; %! b = glmfit (x, [y n], 'binomial', 'Link', 'probit'); %! yfit = glmval (b, x, 'probit', 'Size', n); %! plot (x, y./n, 'o', x, yfit ./ n, '-') %!demo %! load fisheriris %! X = meas (51:end, :); %! y = strcmp ('versicolor', species(51:end)); %! b = glmfit (X, y, 'binomial', 'link', 'logit') ## Test output %!test %! load fisheriris; %! X = meas(51:end,:); %! y = strcmp ('versicolor', species(51:end)); %! b = glmfit (X, y, 'binomial', 'link', 'logit'); %! assert_equal (b, [42.6379; 2.4652; 6.6809; -9.4294; -18.2861], 1e-4); ## Requesting the stats output must not clobber the coefficient vector b; the ## binomial Anscombe-residual computation previously reused the name "b". %!test %! load fisheriris; %! X = meas(51:end,:); %! y = strcmp ('versicolor', species(51:end)); %! [b, dev, stats] = glmfit (X, y, 'binomial', 'link', 'logit'); %! assert_equal (b, [42.6379; 2.4652; 6.6809; -9.4294; -18.2861], 1e-4); %! assert_equal (numel (stats.se), numel (b)); %!test %! ## For an estimated-dispersion family the coefficient covariance is scaled by %! ## the dispersion (not its square), so the standard errors match ordinary %! ## least squares for the identity-link normal case. %! X = [1, 2; 2, 1; 3, 4; 4, 3; 5, 6; 6, 5]; %! y = [2.1; 1.9; 4.2; 3.8; 6.1; 5.9]; %! [b, dev, stats] = glmfit (X, y, 'normal'); %! Xd = [ones(6, 1), X]; %! mse = sum ((y - Xd * b) .^ 2) / (6 - 3); %! se_ols = sqrt (mse * diag (inv (Xd' * Xd))); %! assert_equal (stats.se, se_ols, 1e-12); %!test %! X = [1.2, 2.3, 3.4, 4.5, 5.6, 6.7, 7.8, 8.9, 9.0, 10.1]'; %! y = [0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4]'; %! [Bnew, dev] = glmfit (X, y, 'gamma', 'link', 'log'); %! b_matlab = [-0.7631; 0.1113]; %! dev_matlab = 0.0111; %! assert_equal (Bnew, b_matlab, 0.001); %! assert_equal (dev, dev_matlab, 0.001); %!test %! X = [1.2, 2.3, 3.4, 4.5, 5.6, 6.7, 7.8, 8.9, 9.0, 10.1]'; %! y = [0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4]'; %! p_input = 1; %! [Bnew, dev] = glmfit (X, y, 'inverse gaussian', 'link', p_input); %! b_matlab = [0.3813; 0.0950]; %! dev_matlab = 0.0051; %! assert_equal (Bnew, b_matlab, 0.001); %! assert_equal (dev, dev_matlab, 0.001); %!test %! ## a rank deficient design drops the redundant column: zero coefficient and %! ## standard error, NaN test statistics, and the rest as the reduced fit %! x = [1.2; 2.3; 3.4; 4.5; 5.6; 6.7; 7.8; 8.9; 9.0; 10.1]; %! y = [0.5; 0.6; 0.7; 0.8; 0.9; 1.0; 1.1; 1.2; 1.3; 1.4]; %! status = warning; %! warning ('off'); %! [b, dev, stats] = glmfit ([x, x], y, 'normal'); %! warning (status); %! [br, devr, statsr] = glmfit (x, y, 'normal'); %! assert_equal (b, [br; 0], -1e-12); %! assert_equal (dev, devr, -1e-12); %! assert_equal (stats.se, [statsr.se; 0], -1e-12); %! assert_equal (stats.t, [statsr.t; NaN], -1e-12); %! assert_equal (stats.p, [statsr.p; NaN], -1e-12); %!test %! ## A step leaving the domain of the power link is halved back; MATLAB %! ## R2024a stops there at a deviance of 2.20639741059281 and computes %! ## 1.93529703471832 at the coefficients below %! u = (1:30)'; %! [~, dev] = glmfit (u, exp (0.1 * u) .* (1 + 0.2 * sin (u)), ... %! 'inverse gaussian'); %! assert_equal (dev, 1.93529703471832, -1e-10); %!test %! u = (1:30)'; %! b = glmfit (u, exp (0.1 * u) .* (1 + 0.2 * sin (u)), 'inverse gaussian'); %! assert_equal (b, [0.1324742958; -0.004405637293], -1e-8); ## The first weighted fit is not counted against MaxIter, as in MATLAB; ## values checked against MATLAB R2024a, on separated data that reach the limit %!test %! x = (1:10)'; %! y = 2 * exp (0.3 * x) + sin (x) / 10 > 20; %! S = warning ('off', 'all'); %! b = glmfit (x, y, 'binomial', ... %! 'Options', struct ('MaxIter', 0, 'TolX', 1e-6)); %! warning (S); %! assert_equal (b', [-4.3775021196025969, 0.61904070378218545], 1e-12); %!test %! x = (1:10)'; %! y = 2 * exp (0.3 * x) + sin (x) / 10 > 20; %! S = warning ('off', 'all'); %! b = glmfit (x, y, 'binomial', ... %! 'Options', struct ('MaxIter', 1, 'TolX', 1e-6)); %! warning (S); %! assert_equal (b', [-7.5991011550861405, 1.044776006603054], 1e-12); %!test %! x = (1:10)'; %! y = 2 * exp (0.3 * x) + sin (x) / 10 > 20; %! S = warning ('off', 'all'); %! b = glmfit (x, y, 'binomial'); %! warning (S); %! assert_equal (b', [-607.50428633551712, 81.561811975812105], -1e-12); %!warning ... %! glmfit ((1:10)', (1:10)' > 5, 'binomial', ... %! 'Options', struct ('MaxIter', 0, 'TolX', 1e-6)); ## Test input validation %!error glmfit () %!error glmfit (1) %!error glmfit (1, 2) %!error ... %! glmfit (rand (6, 1), rand (6, 1), 'poisson', 'link') %!error ... %! glmfit ('abc', rand (6, 1), 'poisson') %!error ... %! glmfit ([], rand (6, 1), 'poisson') %!error ... %! glmfit (rand (5, 2), 'abc', 'poisson') %!error ... %! glmfit (rand (5, 2), [], 'poisson') %!error ... %! glmfit (rand (5, 2), rand (6, 1), 'poisson') %!error ... %! glmfit (rand (6, 2), rand (6, 1), 3) %!error ... %! glmfit (rand (6, 2), rand (6, 1), {'poisson'}) %!error ... %! glmfit (rand (5, 2), rand (5, 3), 'binomial') %!error ... %! glmfit (rand (2, 2), [true, true; false, false], 'binomial') %!error ... %! glmfit (rand (5, 2), rand (5, 2), 'normal') %!error ... %! glmfit (rand (5, 2), rand (5, 1), 'chebychev') %!error ... %! glmfit (rand (5, 2), rand (5, 1), 'normal', 'B0', [1; 2; 3; 4]) %!error ... %! glmfit (rand (5, 2), rand (5, 1), 'normal', 'constant', 1) %!error ... %! glmfit (rand (5, 2), rand (5, 1), 'normal', 'constant', 'o') %!error ... %! glmfit (rand (5, 2), rand (5, 1), 'normal', 'constant', true) %!error ... %! glmfit (rand (5, 2), rand (5, 1), 'normal', 'estdisp', 1) %!error ... %! glmfit (rand (5, 2), rand (5, 1), 'normal', 'estdisp', 'o') %!error ... %! glmfit (rand (5, 2), rand (5, 1), 'normal', 'estdisp', true) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', struct ('Link', {1, 2})) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', struct ('Link', 'norminv')) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', struct ('Link', 'some', 'Derivative', @(x)x, 'Inverse', 'normcdf')) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', struct ('Link', 1, 'Derivative', @(x)x, 'Inverse', 'normcdf')) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', struct ('Link', @(x) [x, x], 'Derivative', @(x)x, 'Inverse', 'normcdf')) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', struct ('Link', 'what', 'Derivative', @(x)x, 'Inverse', 'normcdf')) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', struct ('Link', @(x)x, 'Derivative', 'some', 'Inverse', 'normcdf')) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', struct ('Link', @(x)x, 'Derivative', 1, 'Inverse', 'normcdf')) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', struct ('Link', @(x)x, 'Derivative', @(x) [x, x], 'Inverse', 'normcdf')) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', struct ('Link', @(x)x, 'Derivative', 'what', 'Inverse', 'normcdf')) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', struct ('Link', @(x)x, 'Derivative', 'normcdf', 'Inverse', 'some')) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', struct ('Link', @(x)x, 'Derivative', 'normcdf', 'Inverse', 1)) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', struct ('Link', @(x)x, 'Derivative', 'normcdf', 'Inverse', @(x) [x, x])) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', struct ('Link', @(x)x, 'Derivative', 'normcdf', 'Inverse', 'what')) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', {'log'}) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', {'log', 'hijy'}) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', {1, 2, 3, 4}) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', {'log', 'dfv', 'dfgvd'}) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', {@(x) [x, x], 'dfv', 'dfgvd'}) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', {@(x) what (x), 'dfv', 'dfgvd'}) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', {@(x) x, 'dfv', 'dfgvd'}) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', {@(x) x, @(x) [x, x], 'dfgvd'}) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', {@(x) x, @(x) what (x), 'dfgvd'}) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', {@(x) x, @(x) x, 'dfgvd'}) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', {@(x) x, @(x) x, @(x) [x, x]}) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', {@(x) x, @(x) x, @(x) what (x)}) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', NaN) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', [1, 2]) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', [1i]) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', ['log'; 'log1']) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', 'somelinkfunction') %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'link', true) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'options', true) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'options', struct ('MaxIter', 100)) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'options', struct ('MaxIter', 4.5, 'TolX', 1e-6)) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'options', struct ('MaxIter', -100, 'TolX', 1e-6)) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'options', struct ('MaxIter', [50 ,50], 'TolX', 1e-6)) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'options', struct ('MaxIter', 100, 'TolX', 0)) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'options', struct ('MaxIter', 100, 'TolX', -1e-6)) %!error ... %! glmfit (rand (5,2), rand (5,1), 'poisson', 'options', struct ('MaxIter', 100, 'TolX', [1e-6, 1e-6])) %!error ... %! glmfit (rand (5, 2), rand (5, 1), 'normal', 'offset', [1; 2; 3; 4]) %!error ... %! glmfit (rand (5, 2), rand (5, 1), 'normal', 'offset', 'asdfg') %!error ... %! glmfit (rand (5, 2), rand (5, 1), 'normal', 'weights', [1; 2; 3; 4]) %!error ... %! glmfit (rand (5, 2), rand (5, 1), 'normal', 'weights', 'asdfg') statistics-release-2.1.2/inst/Regression/glmval.m000066400000000000000000000407641527311104300220710ustar00rootroot00000000000000## Copyright (C) 2025 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{yhat} =} glmval (@var{b}, @var{X}, @var{link}) ## @deftypefnx {statistics} {[@var{yhat}, @var{y_lo}, @var{y_hi}] =} glmval (@var{b}, @var{X}, @var{link}, @var{stats}) ## @deftypefnx {statistics} {[@dots{}] =} glmval (@dots{}, @var{Name}, @var{Value}) ## ## Predict values for a generalized linear model. ## ## @code{@var{yhat} = glmval (@var{b}, @var{X}, @var{link})} returns the ## predicted values for the generalized linear model with a vector of ## coefficient estimates @var{b}, a matrix of predictors @var{X}, in which each ## column corresponds to a distinct predictor variable, and a link function ## @var{link}, which can be any of the character vectors, numeric scalar, or ## custom-defined link functions used as values for the @qcode{'link'} ## name-value pair argument in the @code{glmfit} function. ## ## @code{[@var{yhat}, @var{y_lo}, @var{y_hi}] = glmval (@var{b}, @var{X}, ## @var{link}, @var{stats})} also returns the 95% confidence intervals for the ## predicted values according to the model's statistics contained in the ## @var{stats} structure, which is the output of the @code{glmfit} function. ## By default, the confidence intervals are nonsimultaneous, and apply to the ## fitted curve instead of new observations. ## ## @code{[@dots{}] = glmval (@dots{}, @var{Name}, @var{Value})} specifies ## additional options using @qcode{Name-Value} pair arguments. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'confidence'} @tab A scalar value between 0 and 1 ## specifying the confidence level for the confidence bounds. ## ## @item @qcode{'Constant'} @tab A character vector specifying whether to ## include a constant term in the model. Valid options are @var{"on"} (default) ## and @var{"off"}. ## ## @item @qcode{'simultaneous'} @tab A logical or numeric (@code{0} or ## @code{1}) scalar specifying whether the confidence bounds are simultaneous. ## The default is @code{false}, which yields nonsimultaneous (pointwise) bounds. ## ## @item @qcode{'size'} @tab A numeric scalar or a vector with one value ## for each row of @var{X} specifying the size parameter @math{N} for a binomial ## model. @qcode{'BinomialSize'} is accepted as an alias for @qcode{'size'}. ## @end multitable ## ## @seealso{glmfit} ## @end deftypefn function [yhat, y_lo, y_hi] = glmval (b, X, link, varargin) ## Check input arguments if (nargin < 3) error ("glmval: too few input arguments."); elseif (! (isnumeric (b) && isvector (b)) || isempty (b)) error ("glmval: B must be a numeric vector of coefficient estimates."); elseif (! isnumeric (X) || isempty (X)) error ("glmval: X must be a numeric matrix."); endif ## Get inverse link (input validation is performed in private function) [~, ~, ilink, errmsg] = getlinkfunctions (link); if (! isempty (errmsg)) error ("glmval: %s", errmsg); endif ## Check if fourth input argument is a STATS structure stats = []; if (nargin > 3) if (isstruct (varargin{1})) stats = varargin{1}; rf = {'s', 'se', 'coeffcorr', 'estdisp', 'dfe'}; if (! all (ismember (rf, fieldnames (stats)))) error ("glmval: invalid 'stats' structure."); endif varargin(1) = []; endif endif ## Set defaults confidence = 0.95; constant = true; offset = zeros (size (X, 1), 1); simultaneous = false; N = 1; ## Parse extra parameters if (mod (numel (varargin), 2) != 0) error ("glmval: Name-Value arguments must be in pairs."); endif while (numel (varargin) > 0) switch (tolower (varargin {1})) case 'confidence' confidence = varargin {2}; if (! (isscalar (confidence) && isnumeric (confidence) && confidence > 0 && confidence < 1)) error ("glmval: 'Confidence' must be a scalar between 0 and 1."); endif case 'constant' constant = tolower (varargin {2}); if (strcmpi (constant, 'on')) constant = true; elseif (strcmpi (constant, 'off')) constant = false; else error ("glmval: 'Constant' should be either 'on' or 'off'."); endif case 'offset' offset = varargin {2}; if (! (isnumeric (offset) && isequal (numel (offset), size (X, 1)))) error (strcat ("glmval: 'Offset' must be a numeric vector", " of the same length as the rows in X.")); endif offset = offset(:); case 'simultaneous' simultaneous = varargin {2}; if (! isscalar (simultaneous) ... || ! (islogical (simultaneous) || isnumeric (simultaneous)) ... || ! any (simultaneous == [0, 1])) error (strcat ("glmval: 'simultaneous' must be a logical or", ... " numeric (0 or 1) scalar.")); endif simultaneous = logical (simultaneous); case {'size', 'binomialsize'} N = varargin {2}; if (! isnumeric (N) || ! (isscalar (N) || isvector (N) && isequal (numel (N), size (X, 1)))) error (strcat ("glmval: 'size' must be a scalar or a vector with", " one value for each row of X.")); endif N = N(:); otherwise error ("glmval: unknown parameter name."); endswitch varargin(1:2) = []; endwhile ## Adjust X based on constant if (constant) X = [ones(size (X, 1), 1), X]; endif ## Predict yhat eta = X * b + offset; yhat = N .* ilink(eta); ## Compute lower and upper bounds if (nargout > 1) if (isempty (stats)) error (strcat ("glmval: cannot compute confidence", " intervals without STATS structure.")); endif if (isnan (stats.s)) y_lo = NaN (size (yhat)); y_hi = NaN (size (yhat)); else V = (stats.se * stats.se') .* stats.coeffcorr; XVX = sum ((X * V) .* X, 2); if (simultaneous) dof = length (b); if (stats.estdisp) Xcv = sqrt (dof * finv (confidence, dof, stats.dfe)); else Xcv = sqrt (chi2inv (confidence, dof)); endif elseif (stats.estdisp) Xcv = tinv ((1 + confidence) / 2, stats.dfe); else Xcv = norminv ((1 + confidence) / 2); endif interval = Xcv * sqrt (XVX); int_hilo = [N.*ilink(eta-interval) N.*ilink(eta+interval)]; y_lo = yhat - min (int_hilo, [], 2); y_hi = max (int_hilo, [], 2) - yhat; endif endif endfunction %!demo %! x = [210, 230, 250, 270, 290, 310, 330, 350, 370, 390, 410, 430]'; %! n = [48, 42, 31, 34, 31, 21, 23, 23, 21, 16, 17, 21]'; %! y = [1, 2, 0, 3, 8, 8, 14, 17, 19, 15, 17, 21]'; %! b = glmfit (x, [y n], 'binomial', 'Link', 'probit'); %! yfit = glmval (b, x, 'probit', 'Size', n); %! plot (x, y./n, 'o', x, yfit ./ n, '-') ## Test input validation %!error glmval () %!error glmval (1) %!error glmval (1, 2) %!error ... %! glmval ('asd', [1; 1; 1], 'probit') %!error ... %! glmval ([], [1; 1; 1], 'probit') %!error ... %! glmval ([0.1; 0.3; 0.4], [], 'probit') %!error ... %! glmval ([0.1; 0.3; 0.4], 'asd', 'probit') %!error ... %! glmval (rand (3,1), rand (5,2), struct ('Link', {1, 2})) %!error ... %! glmval (rand (3,1), rand (5,2), struct ('Link', 'norminv')) %!error ... %! glmval (rand (3,1), rand (5,2), struct ('Link', 'some', 'Derivative', @(x)x, 'Inverse', 'normcdf')) %!error ... %! glmval (rand (3,1), rand (5,2), struct ('Link', 1, 'Derivative', @(x)x, 'Inverse', 'normcdf')) %!error ... %! glmval (rand (3,1), rand (5,2), struct ('Link', @(x) [x, x], 'Derivative', @(x)x, 'Inverse', 'normcdf')) %!error ... %! glmval (rand (3,1), rand (5,2), struct ('Link', 'what', 'Derivative', @(x)x, 'Inverse', 'normcdf')) %!error ... %! glmval (rand (3,1), rand (5,2), struct ('Link', @(x)x, 'Derivative', 'some', 'Inverse', 'normcdf')) %!error ... %! glmval (rand (3,1), rand (5,2), struct ('Link', @(x)x, 'Derivative', 1, 'Inverse', 'normcdf')) %!error ... %! glmval (rand (3,1), rand (5,2), struct ('Link', @(x)x, 'Derivative', @(x) [x, x], 'Inverse', 'normcdf')) %!error ... %! glmval (rand (3,1), rand (5,2), struct ('Link', @(x)x, 'Derivative', 'what', 'Inverse', 'normcdf')) %!error ... %! glmval (rand (3,1), rand (5,2), struct ('Link', @(x)x, 'Derivative', 'normcdf', 'Inverse', 'some')) %!error ... %! glmval (rand (3,1), rand (5,2), struct ('Link', @(x)x, 'Derivative', 'normcdf', 'Inverse', 1)) %!error ... %! glmval (rand (3,1), rand (5,2), struct ('Link', @(x)x, 'Derivative', 'normcdf', 'Inverse', @(x) [x, x])) %!error ... %! glmval (rand (3,1), rand (5,2), struct ('Link', @(x)x, 'Derivative', 'normcdf', 'Inverse', 'what')) %!error ... %! glmval (rand (3,1), rand (5,2), {'log'}) %!error ... %! glmval (rand (3,1), rand (5,2), {'log', 'hijy'}) %!error ... %! glmval (rand (3,1), rand (5,2), {1, 2, 3, 4}) %!error ... %! glmval (rand (3,1), rand (5,2), {'log', 'dfv', 'dfgvd'}) %!error ... %! glmval (rand (3,1), rand (5,2), {@(x) [x, x], 'dfv', 'dfgvd'}) %!error ... %! glmval (rand (3,1), rand (5,2), {@(x) what (x), 'dfv', 'dfgvd'}) %!error ... %! glmval (rand (3,1), rand (5,2), {@(x) x, 'dfv', 'dfgvd'}) %!error ... %! glmval (rand (3,1), rand (5,2), {@(x) x, @(x) [x, x], 'dfgvd'}) %!error ... %! glmval (rand (3,1), rand (5,2), {@(x) x, @(x) what (x), 'dfgvd'}) %!error ... %! glmval (rand (3,1), rand (5,2), {@(x) x, @(x) x, 'dfgvd'}) %!error ... %! glmval (rand (3,1), rand (5,2), {@(x) x, @(x) x, @(x) [x, x]}) %!error ... %! glmval (rand (3,1), rand (5,2), {@(x) x, @(x) x, @(x) what (x)}) %!error ... %! glmval (rand (3,1), rand (5,2), NaN) %!error ... %! glmval (rand (3,1), rand (5,2), [1, 2]) %!error ... %! glmval (rand (3,1), rand (5,2), [1i]) %!error ... %! glmval (rand (3,1), rand (5,2), ['log'; 'log1']) %!error ... %! glmval (rand (3,1), rand (5,2), 'somelinkfunction') %!error ... %! glmval (rand (3,1), rand (5,2), true) %!error ... %! glmval (rand (3,1), rand (5,2), 'probit', struct ('s', 1)) %!error ... %! glmval (rand (3,1), rand (5,2), 'probit', 'confidence') %!error ... %! glmval (rand (3,1), rand (5,2), 'probit', 'confidence', 0) %!error ... %! glmval (rand (3,1), rand (5,2), 'probit', 'confidence', 1.2) %!error ... %! glmval (rand (3,1), rand (5,2), 'probit', 'confidence', [0.9, 0.95]) %!error ... %! glmval (rand (3, 1), rand (5, 2), 'probit', 'constant', 1) %!error ... %! glmval (rand (3, 1), rand (5, 2), 'probit', 'constant', 'o') %!error ... %! glmval (rand (3, 1), rand (5, 2), 'probit', 'constant', true) %!error ... %! glmval (rand (3, 1), rand (5, 2), 'probit', 'offset', [1; 2; 3; 4]) %!error ... %! glmval (rand (3, 1), rand (5, 2), 'probit', 'offset', 'asdfg') %!test # numeric 0/1 are accepted for 'simultaneous' (MATLAB compatibility) %! b = [0.2; 0.5]; %! X = [1; 2; 3]; %! assert_equal (numel (glmval (b, X, 'logit', 'simultaneous', 0)), 3); %! assert_equal (numel (glmval (b, X, 'logit', 'simultaneous', 1)), 3); %!test # 'BinomialSize' is accepted as an alias for 'size' %! b = [0.2; 0.5; -0.3]; %! X = [0.1 0.2; 0.3 0.4; 0.5 0.6; 0.7 0.8]; %! assert_equal (glmval (b, X, 'logit', 'BinomialSize', 10), ... %! glmval (b, X, 'logit', 'size', 10)); %!test # CI: fixed-dispersion uses the normal quantile; rows are independent %! X = [1 2; 2 1; 3 3; 4 2; 5 4; 6 5]; %! y = [1; 0; 2; 3; 2; 4]; %! [b, dev, stats] = glmfit (X, y, 'poisson'); %! Xnew = [2 2; 4 3]; %! [yh, ylo, yhi] = glmval (b, Xnew, 'log', stats); %! Xd = [ones(2, 1), Xnew]; %! se_eta = sqrt (sum ((Xd * stats.covb) .* Xd, 2)); %! eta = Xd * b; %! z = norminv (0.975); %! lo = exp (eta) - exp (eta - z * se_eta); %! hi = exp (eta + z * se_eta) - exp (eta); %! assert_equal (ylo, lo, 1e-10); %! assert_equal (yhi, hi, 1e-10); %! [~, l1, h1] = glmval (b, Xnew(1,:), 'log', stats); %! assert_equal ([ylo(1); yhi(1)], [l1; h1], 1e-12); %!error ... %! glmval (rand (3, 1), rand (5, 2), 'probit', 'simultaneous', 'asdfg') %!error ... %! glmval (rand (3, 1), rand (5, 2), 'probit', 'simultaneous', [true, false]) %!error ... %! glmval (rand (3, 1), rand (5, 2), 'probit', 'size', 'asd') %!error ... %! glmval (rand (3, 1), rand (5, 2), 'probit', 'size', [2, 3, 4]) %!error ... %! glmval (rand (3, 1), rand (5, 2), 'probit', 'size', [2; 3; 4]) %!error ... %! glmval (rand (3, 1), rand (5, 2), 'probit', 'size', ones (3)) %!error ... %! glmval (rand (3, 1), rand (5, 2), 'probit', 'someparam', 4) %!error ... %! [y,lo,hi] = glmval (rand (3, 1), rand (5, 2), 'probit') statistics-release-2.1.2/inst/Regression/invpred.m000066400000000000000000000312161527311104300222460ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x0} =} invpred (@var{x}, @var{y}, @var{y0}) ## @deftypefnx {statistics} {[@var{x0}, @var{dxlo}, @var{dxup}] =} invpred (@var{x}, @var{y}, @var{y0}) ## @deftypefnx {statistics} {[@dots{}] =} invpred (@dots{}, @var{name}, @var{value}) ## ## Inverse prediction from a simple linear regression. ## ## @code{@var{x0} = invpred (@var{x}, @var{y}, @var{y0})} fits the simple ## linear regression of @var{y} on @var{x} and returns, for each element of ## @var{y0}, the value of the predictor at which the fitted line takes that ## response. @var{x} and @var{y} must be vectors of real values of the same ## length; @var{y0} may be of any size and @var{x0} is returned with the same ## size. Observations where either @var{x} or @var{y} is @code{NaN} are ## dropped in pairs before the fit. ## ## @code{[@var{x0}, @var{dxlo}, @var{dxup}] = invpred (@dots{})} also returns ## the width of a confidence interval on either side of @var{x0}, so that the ## interval is @code{[@var{x0} - @var{dxlo}, @var{x0} + @var{dxup}]}. The ## bounds follow Fieller's theorem and are therefore not symmetric about ## @var{x0}. They are not simultaneous over the elements of @var{y0}, and ## they need not be finite: when the slope is not significantly different ## from zero at the requested level, the interval either covers the entire ## real line, with @var{dxlo} and @var{dxup} both @code{Inf}, or it excludes ## a finite range around the fitted line and extends to @code{Inf} on one ## side only, with the other side of @var{dxlo}/@var{dxup} finite. ## ## @code{[@dots{}] = invpred (@dots{}, @var{name}, @var{value})} accepts the ## following name-value pairs: ## ## @itemize ## @item ## @qcode{"alpha"} is the significance level of the interval, a scalar ## strictly between 0 and 1, so that the interval has confidence ## @math{100 * (1 - @var{alpha})%}. The default is @qcode{0.05}. ## ## @item ## @qcode{"predopt"} selects what the interval covers. With ## @qcode{"observation"}, the default, it covers a new observation whose ## response is @var{y0}. With @qcode{"curve"}, it covers the point at which ## the true regression line takes the value @var{y0}, and is narrower because ## it carries no new-observation variance. ## @end itemize ## ## @seealso{regress, fitlm, polyfit} ## @end deftypefn function [x0, dxlo, dxup] = invpred (x, y, y0, varargin) ## Check for valid number of input arguments if (nargin < 3) print_usage (); endif if (! (isvector (x) && isnumeric (x) && isreal (x))) error ("invpred: X must be a vector of real values."); endif if (! (isvector (y) && isnumeric (y) && isreal (y))) error ("invpred: Y must be a vector of real values."); endif if (numel (x) != numel (y)) error ("invpred: X and Y must have the same length."); endif if (! (isnumeric (y0) && isreal (y0))) error ("invpred: Y0 must be numeric and real."); endif ## Parse optional name-value pairs alpha = 0.05; predopt = "observation"; if (mod (numel (varargin), 2) != 0) error ("invpred: optional arguments must be name-value pairs."); endif for i = 1:2:numel (varargin) name = varargin{i}; value = varargin{i+1}; if (! (ischar (name) && isrow (name))) error ("invpred: parameter name must be a character vector."); endif switch (lower (name)) case 'alpha' alpha = value; case 'predopt' predopt = value; otherwise error ("invpred: invalid parameter name: %s.", name); endswitch endfor if (! (isnumeric (alpha) && isreal (alpha) && isscalar (alpha) ... && alpha > 0 && alpha < 1)) error ("invpred: ALPHA must be a scalar strictly between 0 and 1."); endif if (! (ischar (predopt) && isrow (predopt)) ... || ! any (strcmpi (predopt, {"curve", "observation"}))) error ("invpred: PREDOPT value must be 'curve' or 'observation'."); endif ## Drop observations that are missing in either variable x = x(:); y = y(:); keep = ! (isnan (x) | isnan (y)); x = x(keep); y = y(keep); n = numel (x); xbar = mean (x); ybar = mean (y); sxx = sum ((x - xbar) .^ 2); if (sxx == 0) error ("invpred: cannot compute inverse predictions if X is constant."); endif ## Fit the line and take the residual variance about it slope = sum ((x - xbar) .* (y - ybar)) / sxx; s2 = sum ((y - (ybar + slope * (x - xbar))) .^ 2) / (n - 2); ## The point estimate simply inverts the fitted line offset = (y0 - ybar) / slope; x0 = xbar + offset; if (nargout < 2) return; endif ## Fieller's interval. The factor g compares the sampling variability of ## the slope with the slope itself; once it reaches 1 the line cannot be ## distinguished from a horizontal one and the interval is unbounded. t_crit = tinv (1 - alpha / 2, n - 2); g = t_crit ^ 2 * s2 / (slope ^ 2 * sxx); if (g == 1) dxlo = Inf (size (y0)); dxup = Inf (size (y0)); return; endif ## A new observation carries its own variance; a point on the curve does not extra = double (strcmpi (predopt, "observation")); centre = offset / (1 - g); term = offset .^ 2 / sxx + (1 - g) * (1 / n + extra); halfwidth = (t_crit * sqrt (s2) / (abs (slope) * abs (1 - g))) ... .* sqrt (max (term, 0)); if (g < 1) dxlo = offset - centre + halfwidth; dxup = centre + halfwidth - offset; else ## Outside the significance level for the slope: the confidence region ## is the exterior of the two roots, so one side of x0 is unbounded. dxlo = Inf (size (y0)); dxup = Inf (size (y0)); real_roots = term >= 0; lower_ray = real_roots & (offset < centre); upper_ray = real_roots & (offset >= centre); dxup(lower_ray) = centre(lower_ray) - halfwidth(lower_ray) ... - offset(lower_ray); dxlo(upper_ray) = offset(upper_ray) - centre(upper_ray) ... - halfwidth(upper_ray); endif endfunction %!demo %! ## Estimate the predictor value at which a fitted line reaches a response %! ## of 20, with a 95% confidence interval either side of it. %! %! x = (1:10)'; %! y = 2 + 3 * x + [0.5; -0.3; 0.2; 0.8; -0.6; 0.1; -0.4; 0.7; -0.2; 0.3]; %! [x0, dxlo, dxup] = invpred (x, y, 20) %! interval = [x0 - dxlo, x0 + dxup] ## The point estimate inverts the fitted line. %!test %! x = (1:10)'; %! y = 2 + 3 * x + [0.5; -0.3; 0.2; 0.8; -0.6; 0.1; -0.4; 0.7; -0.2; 0.3]; %! x0 = invpred (x, y, 20); %! assert_equal (x0, 5.964741641337386, 1e-12); ## A confidence interval is returned on either side of the estimate. %!test %! x = (1:10)'; %! y = 2 + 3 * x + [0.5; -0.3; 0.2; 0.8; -0.6; 0.1; -0.4; 0.7; -0.2; 0.3]; %! [x0, dxlo, dxup] = invpred (x, y, 20); %! assert_equal (x0, 5.964741641337386, 1e-12); %! assert_equal (dxlo, 0.408566704275405, 1e-12); %! assert_equal (dxup, 0.410279499114103, 1e-12); ## The interval is not symmetric about the estimate. %!test %! x = (1:10)'; %! y = 2 + 3 * x + [0.5; -0.3; 0.2; 0.8; -0.6; 0.1; -0.4; 0.7; -0.2; 0.3]; %! [~, dxlo, dxup] = invpred (x, y, 20); %! assert_equal (dxlo != dxup, true); ## Y0 may hold several values, and the output follows its shape. %!test %! x = (1:10)'; %! y = 2 + 3 * x + [0.5; -0.3; 0.2; 0.8; -0.6; 0.1; -0.4; 0.7; -0.2; 0.3]; %! [x0, dxlo, dxup] = invpred (x, y, [10; 20; 30]); %! assert_equal (x0, [2.621276595744681; 5.964741641337386; ... %! 9.308206686930092], 1e-12); %! assert_equal (dxlo, [0.432537159571881; 0.408566704275405; ... %! 0.433439039304474], 1e-12); %! assert_equal (dxup, [0.421927689383959; 0.410279499114103; ... %! 0.447474099169791], 1e-12); %! [x0r, dxlor, dxupr] = invpred (x, y, [10, 20, 30]); %! assert_equal (x0r, x0'); %! assert_equal (dxlor, dxlo'); %! assert_equal (dxupr, dxup'); ## A matrix of responses is returned as a matrix. %!test %! x = (1:10)'; %! y = 2 + 3 * x + [0.5; -0.3; 0.2; 0.8; -0.6; 0.1; -0.4; 0.7; -0.2; 0.3]; %! x0 = invpred (x, y, [10, 20; 30, 40]); %! assert_equal (size (x0), [2, 2]); %! assert_equal (x0, [2.621276595744681, 5.964741641337386; ... %! 9.308206686930092, 12.651671732522798], 1e-12); ## An empty Y0 gives an empty result. %!test %! x = (1:10)'; %! y = 2 + 3 * x + [0.5; -0.3; 0.2; 0.8; -0.6; 0.1; -0.4; 0.7; -0.2; 0.3]; %! x0 = invpred (x, y, []); %! assert_equal (size (x0), [0, 0]); ## ALPHA widens the interval without moving the estimate. %!test %! x = (1:10)'; %! y = 2 + 3 * x + [0.5; -0.3; 0.2; 0.8; -0.6; 0.1; -0.4; 0.7; -0.2; 0.3]; %! [x0, dxlo, dxup] = invpred (x, y, 20, 'alpha', 0.01); %! assert_equal (x0, 5.964741641337386, 1e-12); %! assert_equal (dxlo, 0.594536204654897, 1e-12); %! assert_equal (dxup, 0.598170042325828, 1e-12); ## An interval on the curve is narrower than one on a new observation. %!test %! x = (1:10)'; %! y = 2 + 3 * x + [0.5; -0.3; 0.2; 0.8; -0.6; 0.1; -0.4; 0.7; -0.2; 0.3]; %! [x0, dxlo, dxup] = invpred (x, y, 20, 'predopt', 'curve'); %! assert_equal (x0, 5.964741641337386, 1e-12); %! assert_equal (dxlo, 0.124048892442359, 1e-12); %! assert_equal (dxup, 0.125761687281057, 1e-12); %! [~, obslo, obsup] = invpred (x, y, 20, 'predopt', 'observation'); %! assert_equal (dxlo < obslo && dxup < obsup, true); ## A slope indistinguishable from zero leaves the interval unbounded. %!test %! [x0, dxlo, dxup] = invpred ((1:5)', [1; 5; 2; 8; 3], 4); %! assert_equal (x0, 3.285714285714286, 1e-12); %! assert_equal (dxlo, Inf); %! assert_equal (dxup, Inf); ## Rows and columns are accepted alike. %!test %! x = (1:10)'; %! y = 2 + 3 * x + [0.5; -0.3; 0.2; 0.8; -0.6; 0.1; -0.4; 0.7; -0.2; 0.3]; %! [x0, dxlo, dxup] = invpred (x', y', 20); %! assert_equal (x0, 5.964741641337386, 1e-12); %! assert_equal (dxlo, 0.408566704275405, 1e-12); %! assert_equal (dxup, 0.410279499114103, 1e-12); ## Observations missing in either variable are dropped in pairs. %!test %! x = (1:10)'; %! y = 2 + 3 * x + [0.5; -0.3; 0.2; 0.8; -0.6; 0.1; -0.4; 0.7; -0.2; 0.3]; %! xn = x; xn(3) = NaN; %! assert_equal (invpred (xn, y, 20), 5.967084254482929, 1e-12); %! yn = y; yn(4) = NaN; %! assert_equal (invpred (x, yn, 20), 5.988352745424295, 1e-12); ## When the slope is not significant, one side of the interval may be ## unbounded while the other stays finite. %!test %! [x0, dxlo, dxup] = invpred ((1:5)', [1; 5; 2; 8; 3], 100); %! assert_equal (x0, 140.42857142857144, 1e-12); %! assert_equal (dxlo, 111.30773209684287, 1e-12); %! assert_equal (dxup, Inf); ## Y0 may hold several values even when the interval is one-sided in ## different directions for different elements. %!test %! [x0, dxlo, dxup] = invpred ((1:5)', [1; 5; 2; 8; 3], [-100; 100]); %! assert_equal (x0, [-145.28571428571428; 140.42857142857144], 1e-12); %! assert_equal (dxlo, [Inf; 111.30773209684287], 1e-12); %! assert_equal (dxup, [120.07306008453997; Inf], 1e-12); %!test %! [x0, dxlo, dxup] = invpred ((1:5)', [1; 5; 2; 8; 3], [100; 200]); %! assert_equal (x0, [140.42857142857144; 283.2857142857143], 1e-12); %! assert_equal (dxlo, [111.30773209684287; 226.72556884927243], 1e-12); %! assert_equal (dxup, [Inf; Inf], 1e-12); %!error invpred ((1:10)', (1:10)') %!error ... %! invpred (ones (10, 2), (1:10)', 5) %!error ... %! invpred ((1:10)', ones (10, 2), 5) %!error ... %! invpred ((1:10)', (1:5)', 5) %!error ... %! invpred ((1:10)', (1:10)', 5, 'alpha', 5) %!error ... %! invpred ((1:10)', (1:10)', 5, 'alpha', [0.05, 0.01]) %!error ... %! invpred ((1:10)', (1:10)', 5, 'predopt', 'bogus') %!error ... %! invpred ((1:10)', (1:10)', 5, 'badname', 1) %!error ... %! invpred ((1:10)', (1:10)', 5, 'alpha') %!error ... %! invpred (ones (10, 1), (1:10)', 5) statistics-release-2.1.2/inst/Regression/lasso.m000066400000000000000000000517461527311104300217320ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{B} =} lasso (@var{X}, @var{y}) ## @deftypefnx {statistics} {[@var{B}, @var{FitInfo}] =} lasso (@var{X}, @var{y}) ## @deftypefnx {statistics} {[@dots{}] =} lasso (@dots{}, @var{Name}, @var{Value}) ## ## Lasso and elastic-net regularized least-squares regression. ## ## @code{@var{B} = lasso (@var{X}, @var{y})} fits a series of regularized linear ## models of the response @var{y} on the predictor matrix @var{X} by lasso, over ## a sequence of values of the regularization parameter @var{Lambda}. @var{B} ## is a @math{P*L} matrix whose column @math{k} holds the coefficient estimates ## for the @math{k}-th @var{Lambda}, in ascending order of @var{Lambda}. ## @var{y} is a vector with one element per row of @var{X}; a row vector is ## taken as a column. ## ## @code{[@var{B}, @var{FitInfo}] = lasso (@dots{})} additionally returns a ## structure @var{FitInfo} with fields @code{Intercept}, @code{Lambda}, ## @code{Alpha}, @code{DF} (number of non-zero coefficients), and @code{MSE} ## (mean squared error), one entry per value of @var{Lambda}. ## ## The following @qcode{Name-Value} pairs are supported: ## ## @multitable @columnfractions 0.18 0.82 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Alpha'} @tab The elastic-net mixing parameter in ## @math{(0, 1]}. @math{1} (default) is the lasso penalty; smaller values add a ## ridge penalty. ## ## @item @qcode{'Lambda'} @tab A vector of non-negative regularization ## parameters. By default a geometric sequence of @qcode{'NumLambda'} values is ## used, from the smallest value that drives all coefficients to zero down to ## @qcode{'LambdaRatio'} times that value. ## ## @item @qcode{'NumLambda'} @tab The number of @var{Lambda} values in the ## default sequence (default @math{100}). ## ## @item @qcode{'LambdaRatio'} @tab The ratio of the smallest to the largest ## @var{Lambda} in the default sequence (default @math{1e-4}, or @math{1e-2} ## when the number of observations is below the number of predictors). ## ## @item @qcode{'Standardize'} @tab Whether to standardize @var{X} to zero mean ## and unit variance before fitting (default @qcode{true}). Coefficients are ## always returned on the original scale. ## ## @item @qcode{'Weights'} @tab A real, finite, non-negative vector of class ## single or double, with one element per row of @var{X}. The weights of rows ## removed because of @qcode{NaN} values are dropped with those rows, and the ## remaining weights are normalized to sum to one. ## ## @item @qcode{'RelTol'} @tab Convergence tolerance for the coordinate descent ## (default @math{1e-4}). ## ## @item @qcode{'MaxIter'} @tab Maximum number of coordinate-descent ## iterations, a non-negative integer (default @math{1e5}). A value of 0 ## returns the starting point, every coefficient 0, where MATLAB's ## @code{lasso} refuses 0 and its @code{lassoglm} accepts it. ## ## @item @qcode{'DFmax'} @tab The maximum number of non-zero coefficients; the ## default sequence stops once this is exceeded. ## ## @item @qcode{'Intercept'} @tab Whether to fit a constant term (default ## @qcode{true}). ## ## @item @qcode{'PredictorNames'} @tab A cell array of predictor names, kept in ## @var{FitInfo}. ## ## @item @qcode{'CV'} @tab The number of folds @math{K} for @math{K}-fold ## cross-validation of the mean squared error, or a @code{cvpartition} object. ## ## @item @qcode{'MCReps'} @tab The number of Monte-Carlo repetitions of the ## cross-validation (default @math{1}). ## @end multitable ## ## Observations with @qcode{NaN} values in @var{X} or @var{y} are removed before ## fitting. At least two observations must remain and, if @qcode{'Weights'} is ## given, at least two of them must have a non-zero weight. ## ## When @qcode{'CV'} is used, @var{FitInfo}@code{.MSE} is the cross-validated ## error, plus @code{SE}, @code{LambdaMinMSE}, @code{IndexMinMSE}, ## @code{Lambda1SE}, and @code{Index1SE}, which report the @var{Lambda} with the ## lowest error and the largest @var{Lambda} within one standard error of it. ## The fold assignment is random, so these selections are not reproducible ## without fixing the random seed. ## ## @seealso{ridge, regress, lassoglm} ## @end deftypefn function [B, FitInfo] = lasso (X, y, varargin) if (nargin < 2) print_usage (); endif if (! (isnumeric (X) && isreal (X) && ismatrix (X))) error ("lasso: X must be a real matrix."); endif if (! (isnumeric (y) && isreal (y) && isvector (y))) error ("lasso: Y must be a real vector."); endif y = y(:); if (rows (X) != numel (y)) error ("lasso: X and Y must have the same number of observations."); endif ## Defaults and Name-Value parsing. alpha = 1; lambda = []; numlambda = 100; lambdaratio = []; standardize = true; weights = []; reltol = 1e-4; maxiter = 1e5; dfmax = columns (X); intercept = true; prednames = {}; cvarg = []; mcreps = 1; if (mod (numel (varargin), 2) != 0) error ("lasso: optional arguments must be Name-Value pairs."); endif for k = 1:2:numel (varargin) if (! ischar (varargin{k})) error ("lasso: parameter names must be character vectors."); endif switch (lower (varargin{k})) case "alpha"; alpha = varargin{k+1}; case "lambda"; lambda = varargin{k+1}; case "numlambda"; numlambda = varargin{k+1}; case "lambdaratio"; lambdaratio = varargin{k+1}; case "standardize"; standardize = logical (varargin{k+1}); case "weights"; weights = varargin{k+1}; case "reltol"; reltol = varargin{k+1}; case "maxiter"; maxiter = varargin{k+1}; case "dfmax"; dfmax = varargin{k+1}; case "intercept"; intercept = logical (varargin{k+1}); case "predictornames"; prednames = varargin{k+1}; case "cv"; cvarg = varargin{k+1}; case "mcreps"; mcreps = varargin{k+1}; otherwise error ("lasso: unknown parameter name '%s'.", varargin{k}); endswitch endfor if (! (isnumeric (alpha) && isscalar (alpha) && alpha > 0 && alpha <= 1)) error ("lasso: 'Alpha' must be a scalar in (0, 1]."); endif if (! isempty (lambda) && (! isnumeric (lambda) || any (lambda < 0))) error ("lasso: 'Lambda' must be a vector of non-negative values."); endif if (! (isnumeric (maxiter) && isreal (maxiter) && isscalar (maxiter) && isfinite (maxiter) && maxiter >= 0 && maxiter == fix (maxiter))) error ("lasso: 'MaxIter' must be a non-negative integer."); endif if (! intercept && standardize) warning (["lasso: when the 'Intercept' value is false, the " ... "'Standardize' value is set to false."]); standardize = false; endif ## Weights are checked as given, before any row is dropped. if (! isempty (weights)) if (! (isfloat (weights) && isreal (weights) && all (isfinite (weights(:))) && all (weights(:) >= 0))) error (strcat ("lasso: 'Weights' must be a real, finite,", ... " non-negative floating-point vector.")); endif if (! (isvector (weights) && numel (weights) == rows (X))) error (strcat ("lasso: 'Weights' must be a vector with one element", ... " per row of X.")); endif weights = double (weights(:)); endif ## Drop observations with missing values, together with their weights. ok = ! (any (isnan (X), 2) | isnan (y)); X = X(ok,:); y = y(ok); if (! isempty (weights)) weights = weights(ok); endif [n, p] = size (X); if (n < 2) error (strcat ("lasso: at least two observations must remain after", ... " removing NaN values.")); endif if (! isempty (weights) && nnz (weights) < 2) error (strcat ("lasso: at least two of the remaining observations", ... " must have non-zero weight.")); endif ## Observation weights, normalised to sum to one. if (isempty (weights)) w = ones (n, 1) / n; else w = weights / sum (weights); endif ## Centre and (optionally) standardise the predictors; centre the response. if (intercept) mux = w' * X; muy = w' * y; else mux = zeros (1, p); muy = 0; endif Xc = X - mux; if (standardize) sig = sqrt (w' * (Xc .^ 2)); sig(sig == 0) = 1; else sig = ones (1, p); endif Xs = Xc ./ sig; yc = y - muy; ## Column "norms" d_j and the response correlations for the path bounds. d = (w' * (Xs .^ 2))'; xy = Xs' * (w .* yc); lambda_max = max (abs (xy)) / alpha; ## Regularization path. userlambda = ! isempty (lambda); if (userlambda) lampath = sort (lambda(:)', "descend"); # fit large -> small (warm start) else if (isempty (lambdaratio)) lambdaratio = ifelse (n < p, 1e-2, 1e-4); endif lampath = lambda_max * lambdaratio .^ ((0:numlambda-1) / (numlambda - 1)); endif nullmse = w' * (yc .^ 2); soft = @(z, g) sign (z) .* max (abs (z) - g, 0); ## Fit along the path, warm-started from the previous solution. L = numel (lampath); Bstd = zeros (p, L); bs = zeros (p, 1); nkeep = L; for kk = 1:L lam = lampath(kk); for it = 1:maxiter bprev = bs; for j = 1:p rj = (w .* (yc - Xs * bs + Xs(:,j) * bs(j))); rho = Xs(:,j)' * rj; bs(j) = soft (rho, lam * alpha) / (d(j) + lam * (1 - alpha)); endfor if (max (abs (bs - bprev)) <= reltol * max (max (abs (bs)), 1)) break; endif endfor Bstd(:,kk) = bs; ## Trim the default path once the fit is essentially exact or too dense. if (! userlambda) bo = bs ./ sig'; mse = w' * ((yc - Xs * bs) .^ 2); if (sum (bs != 0) > dfmax) nkeep = kk - 1; break; elseif (mse < 1e-3 * nullmse) nkeep = kk; break; endif endif endfor Bstd = Bstd(:,1:nkeep); lampath = lampath(1:nkeep); ## Unwind to the original scale; assemble outputs in ascending Lambda order. B = flip (Bstd ./ sig', 2); lampath = flip (lampath); icept = zeros (1, columns (B)); mse = zeros (1, columns (B)); for kk = 1:columns (B) icept(kk) = muy - mux * B(:,kk); mse(kk) = w' * ((y - icept(kk) - X * B(:,kk)) .^ 2); endfor FitInfo.Intercept = icept; FitInfo.Lambda = lampath; FitInfo.Alpha = alpha; FitInfo.DF = sum (B != 0, 1); FitInfo.MSE = mse; if (! isempty (prednames)) FitInfo.PredictorNames = prednames; endif ## Cross-validation of the mean squared error along the fitted path. The ## fold assignment is random, so the selected lambdas are not reproducible ## across runs (or identical to MATLAB) without a fixed random seed. if (! isempty (cvarg)) lam = FitInfo.Lambda; foldmse = []; for rep = 1:mcreps if (isnumeric (cvarg)) cvp = cvpartition (n, "KFold", cvarg); elseif (rep == 1) cvp = cvarg; else cvp = repartition (cvp); endif for f = 1:cvp.NumTestSets tr = training (cvp, f); te = test (cvp, f); wtr = w(tr) / sum (w(tr)); yhat = lasso_foldpredict_ (X(tr,:), y(tr), wtr, X(te,:), lam, alpha, ... standardize, intercept, reltol, maxiter); foldmse(end+1,:) = mean ((y(te) - yhat) .^ 2, 1); endfor endfor cvmse = mean (foldmse, 1); cvse = std (foldmse, 0, 1) / sqrt (rows (foldmse)); FitInfo.MSE = cvmse; FitInfo.SE = cvse; [~, imin] = min (cvmse); FitInfo.LambdaMinMSE = lam(imin); FitInfo.IndexMinMSE = imin; i1se = max (find (cvmse <= cvmse(imin) + cvse(imin))); FitInfo.Lambda1SE = lam(i1se); FitInfo.Index1SE = i1se; endif endfunction ## Fit the lasso path on training data and predict at the test rows, for the ## fixed sequence LAM (returns an ntest-by-numel(LAM) matrix of predictions). function yhat = lasso_foldpredict_ (Xtr, ytr, w, Xte, lam, alpha, ... standardize, intercept, reltol, maxiter) [n, p] = size (Xtr); if (intercept) mux = w' * Xtr; muy = w' * ytr; else mux = zeros (1, p); muy = 0; endif Xc = Xtr - mux; if (standardize) sig = sqrt (w' * (Xc .^ 2)); sig(sig == 0) = 1; else sig = ones (1, p); endif Xs = Xc ./ sig; yc = ytr - muy; d = (w' * (Xs .^ 2))'; soft = @(z, g) sign (z) .* max (abs (z) - g, 0); [lamd, ord] = sort (lam(:)', "descend"); # warm-start from large lambda L = numel (lamd); Bstd = zeros (p, L); bs = zeros (p, 1); for kk = 1:L for it = 1:maxiter bprev = bs; for j = 1:p rj = w .* (yc - Xs * bs + Xs(:,j) * bs(j)); bs(j) = soft (Xs(:,j)' * rj, lamd(kk) * alpha) ... / (d(j) + lamd(kk) * (1 - alpha)); endfor if (max (abs (bs - bprev)) <= reltol * max (max (abs (bs)), 1)) break; endif endfor Bstd(:,kk) = bs; endfor Bo = Bstd ./ sig'; yhat = zeros (rows (Xte), L); yhat(:,ord) = (muy - mux * Bo) + Xte * Bo; endfunction function out = ifelse (cond, a, b) if (cond) out = a; else out = b; endif endfunction %!demo %! ## Lasso path drives coefficients to zero as the penalty grows %! rng (42); %! X = rand (50, 6); %! b = [3; 0; -2; 0; 1.5; 0]; %! y = X * b + 0.1 * randn (50, 1); %! [B, FitInfo] = lasso (X, y); %! plot (log (FitInfo.Lambda), B'); %! xlabel ("log (Lambda)"); ylabel ("coefficient"); %!shared X, y %! n = 20; %! X = zeros (n, 6); %! for j = 1:6 %! X(:,j) = mod ((1:n)' * j, 7) + cos ((1:n)' * j); %! endfor %! y = X * [3;0;-2;0;1.5;0] + 0.1 * sin ((1:n)' * 3); %!test # MATLAB parity: coefficients and intercept at explicit Lambda (lasso) %! lam = [2 1 0.5 0.2 0.1 0.05 0.01]; %! [B, I] = lasso (X, y, "Lambda", lam); %! assert_equal (B(:,1), [2.99553372017526; 0; -1.99263231698882; ... %! 0.00211715208971491; 1.49324410153201; 0], 1e-3); %! assert_equal (B(:,7), [2.23897682145602; 0; -0.482753193552206; ... %! 0.0267252048528861; 0.118128986514695; 0], 1e-3); %! assert_equal (I.Intercept(1), 0.00566677333597632, 1e-3); %! assert_equal (I.Intercept(7), 2.00069783707236, 1e-3); %! assert_equal (I.DF, [4 4 4 4 4 4 4]); %! assert_equal (I.MSE(1), 0.00590963740808093, 1e-3); %!test # MATLAB parity: Lambda is ascending and columns follow it %! [B, I] = lasso (X, y, "Lambda", [2 1 0.5 0.2 0.1 0.05 0.01]); %! assert_equal (I.Lambda, [0.01 0.05 0.1 0.2 0.5 1 2], 1e-12); %! assert_equal (issorted (I.Lambda), true); %!test # MATLAB parity: default path endpoints and length %! [B, I] = lasso (X, y); %! assert_equal (I.Lambda(end), 7.18535290566157, 1e-4); %! assert_equal (I.Lambda(1), 0.119858910419061, 1e-4); %! assert_equal (numel (I.Lambda), 45); %!test # MATLAB parity: elastic net (Alpha = 0.5) %! B = lasso (X, y, "Lambda", [2 1 0.5 0.2 0.1 0.05 0.01], "Alpha", 0.5); %! assert_equal (B(:,1), [2.945477; -0.009332; -1.940313; ... %! 0.053970; 1.485977; -0.049272], 2e-3); %!test # MATLAB parity: Standardize = false %! [B, I] = lasso (X, y, "Lambda", [2 1 0.5 0.2 0.1 0.05 0.01], ... %! "Standardize", false); %! assert_equal (B(:,1), [2.99774227822918; 0; -1.99668154187853; ... %! 0.00196231656035102; 1.49691209089871; 0], 2e-3); %!test # at Lambda -> 0 the lasso solution approaches least squares %! B = lasso (X, y, "Lambda", [1 0.1 0.001]); # columns ascend in Lambda %! bols = regress (y - mean (y), (X - mean (X))); %! assert_equal (B(:,1), bols, 1e-2); # smallest Lambda ~ OLS %!test # a large Lambda drives all coefficients to exactly zero %! B = lasso (X, y, "Lambda", 100); %! assert_equal (B, zeros (6, 1)); %!test # DFmax limits the number of non-zero coefficients on the path %! [B, I] = lasso (X, y, "DFmax", 2); %! assert_equal (all (I.DF <= 2), true); %!test # cross-validation adds the selection fields and they are consistent %! rand ("seed", 7); %! [B, I] = lasso (X, y, "CV", 5); %! assert_equal (numel (I.MSE), numel (I.Lambda)); %! assert_equal (numel (I.SE), numel (I.Lambda)); %! assert_equal (all (I.SE >= 0), true); %! assert_equal (I.LambdaMinMSE, I.Lambda(I.IndexMinMSE), 1e-12); %! assert_equal (I.Lambda1SE, I.Lambda(I.Index1SE), 1e-12); %! assert_equal (I.Index1SE >= I.IndexMinMSE, true); # 1-SE picks a larger Lambda %! assert_equal (I.MSE(I.IndexMinMSE), min (I.MSE), 1e-12); %!test # cross-validated MSE at the min is within one SE at the 1-SE lambda %! rand ("seed", 3); %! [~, I] = lasso (X, y, "CV", 4, "MCReps", 2); %! thr = I.MSE(I.IndexMinMSE) + I.SE(I.IndexMinMSE); %! assert_equal (I.MSE(I.Index1SE) <= thr + 1e-9, true); %!warning ... %! lasso (X, y, "Lambda", 0.1, "Intercept", false); %!test # a row vector Y is taken as a column %! Xr = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4]; %! yr = [1.1 1.9 3.2 3.9 5.1 6.2]; %! assert_equal (lasso (Xr, yr, "Lambda", 0.1), lasso (Xr, yr', "Lambda", 0.1)); %!test # MATLAB parity: a NaN row in X drops its weight %! Xw = [1 2; 2 1; 3 5; NaN 3; 5 6; 6 4; 7 8; 8 7]; %! yw = [1.1 1.9 3.2 3.9 5.1 6.2 6.8 8.1]'; %! ww = [1 2 1 2 1 2 1 2]'; %! [B, I] = lasso (Xw, yw, "Lambda", 0.1, "Weights", ww); %! assert_equal (B, [0.968150410240289; 0], 1e-9); %! assert_equal (I.Intercept, 0.212878030846611, 1e-9); %! assert_equal (I.MSE, 0.027961038961038, 1e-9); %!test # MATLAB parity: NaN rows in X and in Y drop their weights %! Xw = [1 2; 2 1; 3 5; NaN 3; 5 6; 6 4; 7 8; 8 7]; %! yw = [1.1 NaN 3.2 3.9 5.1 6.2 6.8 8.1]'; %! ww = [1 2 1 2 1 2 1 2]'; %! [B, I] = lasso (Xw, yw, "Lambda", 0.1, "Weights", ww); %! assert_equal (B, [0.942070707667087; 0], 1e-9); %! assert_equal (I.Intercept, 0.418611107831020, 1e-9); %! assert_equal (I.MSE, 0.023928571428572, 1e-9); %!test # MATLAB parity: two remaining observations are enough to fit %! Xw = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! yw = [1.1 1.9 3.2 3.9 5.1 6.2 6.8 8.1]'; %! ww = [1 2 1 2 1 2 1 2]'; %! Xw(3:8,1) = NaN; %! [B, I] = lasso (Xw, yw, "Lambda", 0.1, "Weights", ww); %! assert_equal (B, [0.587867965644036; 0], 1e-9); %! assert_equal (I.Intercept, 0.653553390593274, 1e-9); %! assert_equal (I.MSE, 0.01, 1e-9); %!test # MATLAB parity: single weights give a double-precision result %! Xw = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! yw = [1.1 1.9 3.2 3.9 5.1 6.2 6.8 8.1]'; %! ww = [1 2 1 2 1 2 1 2]'; %! [B, I] = lasso (Xw, yw, "Lambda", 0.1, "Weights", single (ww)); %! assert_equal (class (B), "double"); %! assert_equal (B, [0.967942630210206; 0], 1e-9); %! assert_equal (I.Intercept, 0.182934392352372, 1e-9); %! assert_equal (I.MSE, 0.028173758865249, 1e-9); ## No iteration returns the starting point %!test %! load fisheriris %! [B, FitInfo] = lasso (meas(:,2:4), meas(:,1), 'MaxIter', 0); %! assert_equal (all (B(:) == 0), true); %! assert_equal (FitInfo.Intercept, repmat (mean (meas(:,1)), 1, 100), 1e-12); ## Test input validation %!error ... %! lasso (X, y, 'MaxIter', -1) %!error ... %! lasso (X, y, 'MaxIter', 1.5) %!error lasso (1) %!error ... %! lasso ([1 2; 3 4], [1 2 3]') %!error lasso (X, y, "Alpha", 0) %!error ... %! lasso (X, y, "Lambda", [-1 2]) %!error ... %! Xn = X; Xn(3,1) = NaN; %! lasso (Xn, y, "Weights", ones (19, 1)) %!error ... %! lasso (X, y, "Weights", [Inf; ones(19,1)]) %!error ... %! lasso (X, y, "Weights", complex (ones (20, 1), 0)) %!error ... %! lasso (X, y, "Weights", int8 (ones (20, 1))) %!error ... %! lasso (X, y, "Weights", true (20, 1)) %!error ... %! Xn = X; Xn(2,1) = NaN; %! w = ones (20, 1); w(2) = NaN; %! lasso (Xn, y, "Weights", w) %!error ... %! Xn = X; Xn(2,1) = NaN; %! w = zeros (20, 1); w([2 5]) = 1; %! lasso (Xn, y, "Weights", w) %!error ... %! Xn = X; Xn(2:20,1) = NaN; %! lasso (Xn, y, "Lambda", 0.1) statistics-release-2.1.2/inst/Regression/lassoglm.m000066400000000000000000001320101527311104300224120ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{B} =} lassoglm (@var{X}, @var{y}) ## @deftypefnx {statistics} {@var{B} =} lassoglm (@var{X}, @var{y}, @var{distr}) ## @deftypefnx {statistics} {@var{B} =} lassoglm (@var{X}, @var{y}, @var{distr}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{B}, @var{FitInfo}] =} lassoglm (@dots{}) ## ## Lasso and elastic-net regularized generalized linear model regression. ## ## @code{@var{B} = lassoglm (@var{X}, @var{y}, @var{distr})} returns fitted ## least-squares regression coefficients for a generalized linear model of the ## response @var{y} on the predictor data @var{X}, penalized by the lasso (L1) ## or elastic-net penalty. @var{X} is an @math{n}-by-@math{p} numeric matrix of ## @math{p} predictors at each of @math{n} observations, and @var{y} is a ## numeric vector of @math{n} responses. For the @qcode{'normal'}, ## @qcode{'binomial'} and @qcode{'poisson'} distributions @var{y} may also be ## a logical vector, @qcode{true} read as 1, or a categorical vector with ## exactly two categories, the second read as 1 and undefined elements as ## missing; for @qcode{'binomial'} the value 1 is the success. For the ## @qcode{'binomial'} distribution @var{y} may also be an @math{n}-by-2 matrix ## whose first column holds the number of successes and whose second holds the ## number of trials; a row with @qcode{NaN} in either column or with no trials ## is removed. @var{distr} names the distribution of ## the response: @qcode{'normal'} (default), @qcode{'binomial'}, ## @qcode{'poisson'}, @qcode{'gamma'}, or @qcode{'inverse gaussian'}. The ## canonical link function of the chosen distribution is used unless overridden ## by the @qcode{'Link'} option. ## ## @var{B} is a @math{p}-by-@math{L} matrix, where @math{L} is the number of ## regularization (@qcode{'Lambda'}) values used; column @math{k} holds the ## coefficients for the @math{k}-th value of @math{lambda}, in order of ## ascending @math{lambda}. ## ## @code{[@var{B}, @var{FitInfo}] = lassoglm (@dots{})} also returns a structure ## @var{FitInfo} with information about the fitted models: ## ## @multitable @columnfractions 0.2 0.75 ## @item @qcode{Intercept} @tab a @math{1}-by-@math{L} vector of intercept terms ## @item @qcode{Lambda} @tab the @math{1}-by-@math{L} vector of @math{lambda} ## values, in ascending order ## @item @qcode{Alpha} @tab the elastic-net mixing value used ## @item @qcode{DF} @tab the number of nonzero coefficients in each column of ## @var{B} ## @item @qcode{Deviance} @tab the deviance of the fitted model at each ## @math{lambda}; when cross-validation is requested this is instead the ## cross-validated mean deviance ## @end multitable ## ## When cross-validation is requested (see @qcode{'CV'} below), @var{FitInfo} ## additionally contains @qcode{SE} (standard error of the cross-validated ## deviance), @qcode{LambdaMinDeviance} and @qcode{IndexMinDeviance} (the ## @math{lambda} with minimum cross-validated deviance and its index), and ## @qcode{Lambda1SE} and @qcode{Index1SE} (the largest @math{lambda} within one ## standard error of that minimum). ## ## @code{lassoglm} accepts the following @var{Name}/@var{Value} pairs: ## ## @multitable @columnfractions 0.2 0.75 ## @headitem Name @tab Value ## @item @qcode{'Alpha'} @tab the elastic-net mixing parameter, a scalar in ## @math{(0, 1]}. @qcode{'Alpha'} = 1 is the lasso penalty (default); ## values towards 0 approach ridge regression. ## @item @qcode{'Lambda'} @tab a vector of non-negative @math{lambda} values. ## @item @qcode{'Standardize'} @tab a logical value (default @qcode{true}) ## specifying whether the predictors are standardized before fitting. ## @item @qcode{'Weights'} @tab a real, non-negative vector of class single or ## double, with one element per row of @var{X}. A @qcode{NaN} weight marks its ## observation as missing. ## @item @qcode{'BinomialSize'} @tab for the @qcode{'binomial'} distribution, ## the number of trials, a scalar or a vector with one element per row of ## @var{X}; @var{y} holds the number of successes. Default is 1 (Bernoulli ## responses). It describes the same model as the two-column @var{y}, whose ## trials are used when both are given. A row whose number of trials is ## @qcode{NaN} or 0 is removed. @qcode{'BinomialSize'} is an Octave ## extension, and @qcode{'Size'} is accepted as an alias for it. ## @item @qcode{'Link'} @tab the link function to use instead of the family's ## canonical link. Accepts any link name understood by @code{glmfit} (e.g. ## @qcode{'log'}, @qcode{'probit'}) or a numeric exponent for a power link. ## @item @qcode{'Offset'} @tab a numeric vector, one value per observation, ## added as a fixed term to the linear predictor (not penalized or fitted). ## @item @qcode{'RelTol'} @tab convergence tolerance for the coordinate descent. ## @item @qcode{'MaxIter'} @tab maximum number of iterations, a non-negative ## integer. A value of 0 returns the starting point, every coefficient 0, as ## MATLAB does. ## @item @qcode{'DFmax'} @tab maximum number of nonzero coefficients. ## @item @qcode{'Intercept'} @tab a logical value (default @qcode{true}) whether ## to fit an intercept term. ## @item @qcode{'PredictorNames'} @tab a cell array of predictor names. ## @item @qcode{'CV'} @tab the number of folds @math{K} for @math{K}-fold ## cross-validation, or a @code{cvpartition} object. The fold assignment is ## random, so the selected @math{lambda} values are not reproducible without a ## fixed random seed. ## @item @qcode{'MCReps'} @tab the number of Monte-Carlo repetitions of the ## cross-validation (default 1). ## @end multitable ## ## Observations with @qcode{NaN} values in @var{X}, @var{y} or ## @qcode{'Weights'} are removed before fitting. At least two observations ## must remain and, if @qcode{'Weights'} is given, at least two of them must ## have a non-zero weight. ## ## A response at the edge of its range on every observation, all failures or ## all successes for @qcode{'binomial'} and all zeros for @qcode{'poisson'}, ## has no finite fit. @var{B} is then zero, the intercept places the fitted ## mean at the limit of its range, and the default @var{Lambda} values are all ## zero, as in MATLAB. MATLAB's intercept there is about 102.5 in magnitude. ## ## @seealso{lasso, glmfit, glmval, cvpartition} ## @end deftypefn function [B, FitInfo] = lassoglm (X, y, distr, varargin) if (nargin < 2) print_usage (); endif if (! (isnumeric (X) && isreal (X) && ismatrix (X))) error ("lassoglm: X must be a real matrix."); endif if (nargin < 3 || isempty (distr)) distr = 'normal'; endif if (! (ischar (distr) && isrow (distr))) error ("lassoglm: DISTR must be a character vector."); endif distr = lower (distr); if (! any (strcmp (distr, {'normal', 'binomial', 'poisson', 'gamma', ... 'inverse gaussian'}))) error ("lassoglm: unknown distribution '%s'.", distr); endif ## A normal, binomial or poisson response may be logical or categorical ## with two categories; a binomial one may also be a two-column matrix of ## successes and trials. A row vector stays a response vector. trials = []; zeroone = any (strcmp (distr, {'normal', 'binomial', 'poisson'})); if (zeroone && islogical (y)) y = double (y); elseif (zeroone && isa (y, 'categorical')) if (! (isvector (y) && numel (categories (y)) == 2)) error (strcat ("lassoglm: a categorical Y must be a vector with", ... " exactly two categories.")); endif y = double (y) - 1; endif if (strcmp (distr, 'binomial') && isnumeric (y) && isreal (y) && ismatrix (y) && ! isvector (y) && columns (y) == 2) trials = y(:,2); y = y(:,1); elseif (! (isnumeric (y) && isreal (y) && isvector (y))) if (strcmp (distr, 'binomial')) error (strcat ("lassoglm: for the 'binomial' distribution Y must be", ... " a real vector or a two-column matrix.")); endif error ("lassoglm: Y must be a real vector."); endif y = y(:); if (rows (X) != numel (y)) error ("lassoglm: X and Y must have the same number of observations."); endif ## Defaults and Name-Value parsing. alpha = 1; lambda = []; numlambda = 100; lambdaratio = []; standardize = true; weights = []; reltol = 1e-4; maxiter = 1e5; dfmax = columns (X); intercept = true; prednames = {}; binomsize = []; linkarg = []; offset = []; cvarg = []; mcreps = 1; usecov = false; if (mod (numel (varargin), 2) != 0) error ("lassoglm: optional arguments must be Name-Value pairs."); endif for k = 1:2:numel (varargin) if (! ischar (varargin{k})) error ("lassoglm: parameter names must be character vectors."); endif switch (lower (varargin{k})) case "alpha"; alpha = varargin{k+1}; case "lambda"; lambda = varargin{k+1}; case "numlambda"; numlambda = varargin{k+1}; case "lambdaratio"; lambdaratio = varargin{k+1}; case "standardize"; standardize = logical (varargin{k+1}); case "weights"; weights = varargin{k+1}; case {"binomialsize", "size"} binomsize = varargin{k+1}; case "link"; linkarg = varargin{k+1}; case "offset"; offset = varargin{k+1}; case "reltol"; reltol = varargin{k+1}; case "maxiter"; maxiter = varargin{k+1}; case "dfmax"; dfmax = varargin{k+1}; case "intercept"; intercept = logical (varargin{k+1}); case "predictornames"; prednames = varargin{k+1}; case "cv"; cvarg = varargin{k+1}; case "mcreps"; mcreps = varargin{k+1}; case "usecovariance"; usecov = logical (varargin{k+1}); otherwise error ("lassoglm: unknown parameter name '%s'.", varargin{k}); endswitch endfor if (! (isnumeric (alpha) && isscalar (alpha) && alpha > 0 && alpha <= 1)) error ("lassoglm: 'Alpha' must be a scalar in (0, 1]."); endif if (! isempty (lambda) && (! isnumeric (lambda) || any (lambda < 0))) error ("lassoglm: 'Lambda' must be a vector of non-negative values."); endif if (! (isnumeric (maxiter) && isreal (maxiter) && isscalar (maxiter) && isfinite (maxiter) && maxiter >= 0 && maxiter == fix (maxiter))) error ("lassoglm: 'MaxIter' must be a non-negative integer."); endif ## Weights are checked as given, before any row is dropped. if (! isempty (weights)) if (! (isfloat (weights) && isreal (weights))) error ("lassoglm: 'Weights' must be a real floating-point vector."); endif if (! (isvector (weights) && numel (weights) == rows (X))) error (strcat ("lassoglm: 'Weights' must be a vector with one", ... " element per row of X.")); endif if (any (weights(:) < 0 | isinf (weights(:)))) error ("lassoglm: 'Weights' must be non-negative and not infinite."); endif weights = double (weights(:)); endif ## Number of binomial trials per observation, default one. The trials in a ## two-column Y take precedence over 'BinomialSize'. N = ones (rows (X), 1); if (strcmp (distr, 'binomial')) if (! isempty (trials)) N = trials; elseif (isscalar (binomsize)) N(:) = binomsize; elseif (! isempty (binomsize)) if (! (isvector (binomsize) && numel (binomsize) == rows (X))) error (strcat ("lassoglm: 'BinomialSize' must be a scalar or a", ... " vector with one element per row of X.")); endif N = binomsize(:); endif if (! (isnumeric (N) && isreal (N))) error ("lassoglm: the number of binomial trials must be real."); endif N = double (N); endif ## Drop observations with missing values: a NaN weight, a NaN or zero ## number of trials among them. ok = ! (any (isnan (X), 2) | isnan (y) | isnan (N) | N == 0); if (! isempty (weights)) ok &= ! isnan (weights); weights = weights(ok); endif X = X(ok,:); y = y(ok); N = N(ok); [n, p] = size (X); if (n < 2) error (strcat ("lassoglm: at least two observations must remain after", ... " removing NaN values.")); endif if (! isempty (weights) && nnz (weights) < 2) error (strcat ("lassoglm: at least two of the remaining observations", ... " must have non-zero weight.")); endif if (any (N < 0)) error ("lassoglm: the number of binomial trials must be non-negative."); endif ## Prior observation weights. PW keeps the raw weights for the deviance sum; ## O, normalised to sum to one, centres and scales the predictors. if (isempty (weights)) pw = ones (n, 1); else pw = weights; endif o = pw / sum (pw); ## OL sets the scale of the loss: it is divided by the total number of ## trials, as MATLAB divides it. ol = pw / sum (pw .* N); ## Offset: a fixed per-observation term added to the linear predictor. if (isempty (offset)) off = zeros (n, 1); else if (! (isnumeric (offset) && isvector (offset) && numel (offset) == numel (ok))) error ("lassoglm: 'Offset' must be a numeric vector, one per observation."); endif off = offset(:); off = off(ok); endif ## Response-domain checks for the chosen family. switch (distr) case 'binomial' if (any (y < 0) || any (y > N)) error (strcat ("lassoglm: for the 'binomial' distribution Y must be", ... " between 0 and the number of trials.")); endif case 'poisson' if (any (y < 0)) error ("lassoglm: for the 'poisson' distribution Y must be non-negative."); endif case {'gamma', 'inverse gaussian'} if (any (y <= 0)) error ("lassoglm: for the '%s' distribution Y must be positive.", distr); endif endswitch ## Link, variance, and (unit) deviance functions for the chosen family. A ## user-supplied 'Link' overrides the family's canonical link. [flink, dlink, ilink, varfun, devfun, mulims] = glmfamily (distr); if (! isempty (linkarg)) [flink, dlink, ilink, errmsg] = getlinkfunctions (linkarg); if (! isempty (errmsg)) error ("lassoglm: %s", errmsg); endif endif ## Standardise the predictors using the prior weights (fixed for all fits). mux = o' * X; Xc = X - mux; if (standardize) sig = sqrt (o' * (Xc .^ 2)); sig(sig == 0) = 1; else sig = ones (1, p); endif Xs = Xc ./ sig; ## Intercept-only (null) fit -- offset-aware. Supplies the starting ## intercept B0, the null deviance (the path-trim scale), and the null-model ## fitted mean MU0 used for the LAMBDA_MAX gradient. A response at the edge ## of its range on every row has no finite fit: the intercept is set where ## the fitted mean reaches the limit of its range, and B stays zero. atlow = all (y ./ N <= mulims(1)); athigh = all (y ./ N >= mulims(2)); edge = intercept && (atlow || athigh); if (edge && atlow) b0 = flink (mulims(1)) - max (off); mu0 = max (min (ilink (off + b0), mulims(2)), mulims(1)); elseif (edge) b0 = flink (mulims(2)) - min (off); mu0 = max (min (ilink (off + b0), mulims(2)), mulims(1)); else [b0, mu0] = glm_null_fit (off, y, N, o, flink, dlink, ilink, varfun, ... mulims, intercept, maxiter, reltol); endif nulldev = sum (pw .* devfun (mu0, y ./ N, N)); ## Regularization path. soft = @(z, g) sign (z) .* max (abs (z) - g, 0); userlambda = ! isempty (lambda); if (userlambda) lampath = sort (lambda(:)', "descend"); elseif (edge) lampath = zeros (1, numlambda); else deta0 = dlink (mu0); grad = Xs' * (ol .* (y ./ N - mu0) ./ (deta0 .* varfun (mu0, N))); lambda_max = max (abs (grad)) / alpha; if (isempty (lambdaratio)) lambdaratio = ifelse (n < p, 1e-2, 1e-4); endif lampath = lambda_max * lambdaratio .^ ((0:numlambda-1) / (numlambda - 1)); endif ## Fit along the path, warm-started from the previous (larger-lambda) fit. L = numel (lampath); Bstd = zeros (p, L); icept_eta = zeros (1, L); bs = zeros (p, 1); nkeep = L; for kk = 1:L if (edge) icept_eta(kk) = b0; continue; endif lam = lampath(kk); for outer = 1:maxiter bouter = bs; ## IRLS: quadratic (working-response) approximation at the current fit. eta = off + b0 + Xs * bs; mu = ilink (eta); mu = max (min (mu, mulims(2)), mulims(1)); deta = dlink (mu); ## Working response with the offset removed (fitted by intercept + Xs). zc = eta - off + (y ./ N - mu) .* deta; W = ol ./ (deta .^ 2 .* varfun (mu, N)); sumW = sum (W); ## Inner coordinate descent on the penalized weighted least squares. for it = 1:maxiter bprev = bs; if (intercept) b0 = sum (W .* (zc - Xs * bs)) / sumW; endif for j = 1:p rj = zc - b0 - Xs * bs + Xs(:,j) * bs(j); rho = sum (W .* Xs(:,j) .* rj); dj = sum (W .* Xs(:,j) .^ 2); bs(j) = soft (rho, lam * alpha) / (dj + lam * (1 - alpha)); endfor if (max (abs (bs - bprev)) <= reltol * max (max (abs (bs)), 1)) break; endif endfor if (max (abs (bs - bouter)) <= reltol * max (max (abs (bs)), 1)) break; endif endfor ## Snap coordinate-descent residue at the KKT boundary to exact zero, so a ## coefficient that is numerically negligible (e.g. ~1e-16 at LAMBDA_MAX) ## does not count towards the degrees of freedom. bs(abs (bs) < 1e-9) = 0; Bstd(:,kk) = bs; icept_eta(kk) = b0; ## Trim the default path once the model saturates (DFmax) or the fit is ## essentially exact (deviance a negligible fraction of the null deviance). if (! userlambda) if (sum (bs != 0) > dfmax) nkeep = kk - 1; break; endif eta = off + b0 + Xs * bs; mu = max (min (ilink (eta), mulims(2)), mulims(1)); devk = sum (pw .* devfun (mu, y ./ N, N)); if (devk < 1e-3 * nulldev) nkeep = kk; break; endif endif endfor Bstd = Bstd(:,1:nkeep); icept_eta = icept_eta(1:nkeep); lampath = lampath(1:nkeep); ## Unwind to the original predictor scale; assemble ascending-lambda outputs. B = flip (Bstd ./ sig', 2); icept_eta = flip (icept_eta); lampath = flip (lampath); Lk = columns (B); icept = zeros (1, Lk); dev = zeros (1, Lk); for kk = 1:Lk icept(kk) = icept_eta(kk) - mux * B(:,kk); eta = off + icept(kk) + X * B(:,kk); mu = ilink (eta); mu = max (min (mu, mulims(2)), mulims(1)); dev(kk) = sum (pw .* devfun (mu, y ./ N, N)); endfor FitInfo.Intercept = icept; FitInfo.Lambda = lampath; FitInfo.Alpha = alpha; FitInfo.DF = sum (B != 0, 1); FitInfo.Deviance = dev; ## Cross-validation of the mean deviance along the fitted path. The fold ## assignment is random, so the selected lambdas are self-consistent but not ## reproducible across runs (or identical to MATLAB) without a fixed seed. if (! isempty (cvarg)) lam = FitInfo.Lambda; folddev = []; for rep = 1:mcreps if (isnumeric (cvarg)) cvp = cvpartition (n, "KFold", cvarg); elseif (rep == 1) cvp = cvarg; else cvp = repartition (cvp); endif for f = 1:cvp.NumTestSets tr = training (cvp, f); te = test (cvp, f); folddev(end+1,:) = lassoglm_folddev_ ( ... X(tr,:), y(tr), N(tr), off(tr), pw(tr), ... X(te,:), y(te), N(te), off(te), lam, distr, alpha, ... standardize, intercept, reltol, maxiter, linkarg); endfor endfor cvdev = mean (folddev, 1); cvse = std (folddev, 0, 1) / sqrt (rows (folddev)); FitInfo.Deviance = cvdev; FitInfo.SE = cvse; [~, imin] = min (cvdev); FitInfo.LambdaMinDeviance = lam(imin); FitInfo.IndexMinDeviance = imin; i1se = max (find (cvdev <= cvdev(imin) + cvse(imin))); FitInfo.Lambda1SE = lam(i1se); FitInfo.Index1SE = i1se; endif FitInfo.PredictorNames = prednames; FitInfo.UseCovariance = usecov; endfunction ## Family link, variance, deviance, mean-initialiser, and mean-support limits. ## The link triplet is taken from the shared private helper GETLINKFUNCTIONS; ## the variance and deviance formulas mirror those in GLMFIT. function [flink, dlink, ilink, varfun, devfun, mulims] = glmfamily (distr) switch (distr) case 'normal' [flink, dlink, ilink] = getlinkfunctions ('identity'); varfun = @(mu, N) ones (size (mu)); devfun = @(mu, y, N) (y - mu) .^ 2; mulims = [-Inf, Inf]; case 'binomial' [flink, dlink, ilink] = getlinkfunctions ('logit'); varfun = @(mu, N) mu .* (1 - mu) ./ N; devfun = @(mu, y, N) 2 * N .* (y .* log ((y + (y == 0)) ./ mu) + ... (1 - y) .* log ((1 - y + (y == 1)) ./ (1 - mu))); seps = sqrt (eps); mulims = [seps, 1 - seps]; case 'poisson' [flink, dlink, ilink] = getlinkfunctions ('log'); varfun = @(mu, N) mu; devfun = @(mu, y, N) 2 * (y .* log ((y + (y == 0)) ./ mu) - (y - mu)); mulims = [realmin, Inf]; case 'gamma' [flink, dlink, ilink] = getlinkfunctions ('reciprocal'); varfun = @(mu, N) mu .^ 2; devfun = @(mu, y, N) 2 * (- log (y ./ mu) + (y - mu) ./ mu); mulims = [realmin, Inf]; case 'inverse gaussian' [flink, dlink, ilink] = getlinkfunctions (-2); varfun = @(mu, N) mu .^ 3; devfun = @(mu, y, N) ((y - mu) ./ mu) .^ 2 ./ y; mulims = [realmin, Inf]; endswitch endfunction ## Intercept-only (null) fit by IRLS, honouring the offset. Returns the fitted ## intercept B0 (linear-predictor scale) and the fitted mean vector MU0. When ## there is no intercept the model is just the offset. function [b0, mu0] = glm_null_fit (off, y, N, o, flink, dlink, ilink, ... varfun, mulims, intercept, maxiter, reltol) if (! intercept) b0 = 0; mu0 = max (min (ilink (off), mulims(2)), mulims(1)); return; endif ## Initialise from the marginal mean of the response. b0 = flink (o' * (y ./ N)); for it = 1:maxiter eta = off + b0; mu = max (min (ilink (eta), mulims(2)), mulims(1)); deta = dlink (mu); zc = eta - off + (y ./ N - mu) .* deta; W = o ./ (deta .^ 2 .* varfun (mu, N)); b0new = sum (W .* zc) / sum (W); if (abs (b0new - b0) <= reltol * max (abs (b0), 1)) b0 = b0new; break; endif b0 = b0new; endfor mu0 = max (min (ilink (off + b0), mulims(2)), mulims(1)); endfunction ## Mean held-out deviance (per test observation, one value per LAMBDA) for a ## single cross-validation fold: fit on the training rows at the fixed LAMBDA ## path, then score the deviance on the test rows. function dev = lassoglm_folddev_ (Xtr, ytr, Ntr, offtr, wtr, Xte, yte, Nte, ... offte, lam, distr, alpha, standardize, ... intercept, reltol, maxiter, linkarg) args = {"Lambda", lam, "Alpha", alpha, "Standardize", standardize, ... "Intercept", intercept, "RelTol", reltol, "MaxIter", maxiter, ... "Weights", wtr, "Offset", offtr}; if (strcmp (distr, "binomial")) args = [args, {"BinomialSize", Ntr}]; endif if (! isempty (linkarg)) args = [args, {"Link", linkarg}]; endif [Btr, Ftr] = lassoglm (Xtr, ytr, distr, args{:}); [flink, dlink, ilink, varfun, devfun, mulims] = glmfamily (distr); if (! isempty (linkarg)) [flink, dlink, ilink] = getlinkfunctions (linkarg); endif dev = zeros (1, columns (Btr)); for k = 1:columns (Btr) eta = offte + Ftr.Intercept(k) + Xte * Btr(:,k); mu = max (min (ilink (eta), mulims(2)), mulims(1)); dev(k) = mean (devfun (mu, yte ./ Nte, Nte)); endfor endfunction function out = ifelse (cond, a, b) if (cond) out = a; else out = b; endif endfunction %!demo %! ## Logistic regression with a lasso penalty on a small binary dataset. %! X = [0.1, 1.2; 0.4, 0.7; 1.1, 0.2; 1.5, 1.9; 0.3, 0.5; 1.8, 1.1]; %! y = [0; 0; 1; 1; 0; 1]; %! [B, FitInfo] = lassoglm (X, y, 'binomial', 'Lambda', [0.01, 0.1]) %!demo %! ## Poisson (count) regression with an elastic-net penalty. %! X = [0.1, 1.2; 0.4, 0.7; 1.1, 0.2; 1.5, 1.9; 0.3, 0.5; 1.8, 1.1; 0.9, 0.3]; %! y = [1; 0; 2; 3; 1; 4; 2]; %! [B, FitInfo] = lassoglm (X, y, 'poisson', 'Lambda', [0.05, 0.2], ... %! 'Alpha', 0.6) %!demo %! ## Choosing the penalty by cross-validation. Passing a 'CV' fold count makes %! ## lassoglm cross-validate the deviance along the lambda path. It then %! ## reports the lambda with the lowest mean deviance (LambdaMinDeviance) and %! ## the largest lambda within one standard error of it (Lambda1SE) -- the %! ## sparser "one-standard-error" model that is often preferred. %! ## %! ## Here only the first two of eight predictors truly drive the response, so a %! ## good fit should keep few nonzero coefficients. The fold assignment is %! ## random; the seed below just makes the printed numbers reproducible. %! rng (42); %! X = randn (60, 8); %! beta = [1.5; -2; zeros(6, 1)]; %! y = double (rand (60, 1) < 1 ./ (1 + exp (- X * beta))); %! [B, FitInfo] = lassoglm (X, y, 'binomial', 'CV', 5); %! printf ('LambdaMinDeviance = %.4f (%d nonzero)\n', ... %! FitInfo.LambdaMinDeviance, FitInfo.DF(FitInfo.IndexMinDeviance)); %! printf ('Lambda1SE = %.4f (%d nonzero)\n', ... %! FitInfo.Lambda1SE, FitInfo.DF(FitInfo.Index1SE)); %! ## Coefficients of the one-standard-error model. %! coef_1SE = B(:, FitInfo.Index1SE) %!shared X, yb, yp, yn %! X = [ 0.12, -0.16, 1.35, 0.48; 0.39, 0.72, -0.91, -0.12; ... %! 0.55, -0.11, -0.28, -0.28; -1.32, 0.80, -1.25, 2.14; ... %! -0.24, 0.77, -2.62, 0.66; 0.05, 0.70, -1.55, 0.06; ... %! 1.05, 0.84, -0.16, 0.54; 0.55, -0.50, -0.25, 0.07; ... %! 0.31, -0.58, -0.77, -0.50; 1.09, 0.44, -0.47, -0.26; ... %! 1.08, -1.67, 2.08, 0.29; -1.52, 1.51, -2.46, -1.41; ... %! -0.55, 1.40, -0.59, -0.17; 1.35, -0.16, -0.38, -0.26]; %! yb = [1 1 1 0 0 1 1 1 1 1 1 0 0 1]'; %! yp = [1 0 1 1 0 1 1 2 1 1 1 0 0 0]'; %! yn = [0.7 -0.86 0.21 -1.38 -0.81 -1.16 0.12 1.33 0.87 0.65 4.18 -4.57 ... %! -2.76 1.51]'; ## Test results (values verified against MATLAB's lassoglm) %!test %! ## Gaussian lassoglm coincides with lasso and matches MATLAB. %! [Bg, Fg] = lassoglm (X, yn, "normal", "Lambda", [0.3, 0.1, 0.02]); %! [Bl, Fl] = lasso (X, yn, "Lambda", [0.3, 0.1, 0.02]); %! assert_equal (Bg, Bl, 1e-4); %! assert_equal (Fg.Intercept, Fl.Intercept, 1e-4); %!test %! ## Poisson regression against R2024a's own intercept and deviance. The %! ## coordinate descent converges to a slightly different point, so 1e-4. %! [B, F] = lassoglm (X, yp, "poisson", "Lambda", [0.2, 0.05, 0.01]); %! assert_equal (F.Intercept, ... %! [-0.408479371164708, -0.318644470147402, ... %! -0.297271780712625], 1e-4); %! assert_equal (F.Deviance, ... %! [7.068880870474705, 7.267211985220860, ... %! 8.458049628150873], 1e-4); %! assert_equal (F.DF, [3, 3, 1]); %!test %! ## Binomial (logistic) regression against R2024a's own values. The %! ## largest lambda is the loosest fit and agrees least closely, at 5e-4. %! [B, F] = lassoglm (X, yb, "binomial", "Lambda", [0.2, 0.1, 0.05, 0.01]); %! assert_equal (F.Intercept, ... %! [2.282419079745844, 1.388013352791727, ... %! 0.964941260795408, 0.834029178373540], 1e-3); %! assert_equal (F.Deviance, ... %! [1.146705233151688, 3.739184081182877, ... %! 6.057648047528616, 9.585820201838006], 1e-3); %! assert_equal (F.DF, [3, 3, 3, 1]); %!test %! ## Default path: DF is zero at lambda_max and lambda is ascending. %! [B, F] = lassoglm (X, yb, "binomial"); %! assert_equal (F.DF(end), 0); %! assert_equal (issorted (F.Lambda), true); %! assert_equal (all (F.Deviance(1:end-1) <= F.Deviance(2:end)), true); %!test %! ## Cross-validation adds the expected FitInfo fields and the 1-SE rule %! ## selects a lambda no smaller than the minimum-deviance lambda. %! cvp = cvpartition (14, "KFold", 5); %! [B, F] = lassoglm (X, yb, "binomial", "CV", cvp); %! assert_equal (isfield (F, "SE") && isfield (F, "LambdaMinDeviance"), true); %! assert_equal (numel (F.SE), numel (F.Lambda)); %! assert_equal (F.Lambda1SE >= F.LambdaMinDeviance, true); %! assert_equal (F.Index1SE >= F.IndexMinDeviance, true); %!test %! ## MATLAB parity: a NaN row in X drops its weight. R2024a stops nearer the %! ## optimum at its default 'RelTol' than ours, hence 1e-8 here. %! Xw = [1 2; 2 1; 3 5; NaN 3; 5 6; 6 4; 7 8; 8 7]; %! yw = [1 0 2 1 3 2 4 3]'; %! ww = [1 2 1 2 1 2 1 2]'; %! [B, F] = lassoglm (Xw, yw, "poisson", "Lambda", 0.1, "Weights", ww, ... %! "RelTol", 1e-8); %! assert_equal (B, [0; 0.270328055695712], 1e-6); %! assert_equal (F.Intercept, -0.720881899870004, 1e-6); %! assert_equal (F.Deviance, 3.139684900086630, 1e-6); %!test %! ## MATLAB parity: a NaN weight drops its row. %! Xw = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! yw = [1 0 2 1 3 2 4 3]'; %! ww = [1 NaN 1 2 1 2 1 2]'; %! [B, F] = lassoglm (Xw, yw, "poisson", "Lambda", 0.1, "Weights", ww, ... %! "RelTol", 1e-8); %! assert_equal (B, [0; 0.201726591245334], 1e-6); %! assert_equal (F.Intercept, -0.275203095895550, 1e-6); %! assert_equal (F.Deviance, 0.456977765877823, 1e-6); %!test %! ## MATLAB parity: two remaining observations are enough to fit. %! Xw = [1 2; 2 1; NaN 5; NaN 3; NaN 6; NaN 4; NaN 8; NaN 7]; %! yw = [1 0 2 1 3 2 4 3]'; %! ww = [1 2 1 2 1 2 1 2]'; %! [B, F] = lassoglm (Xw, yw, "poisson", "Lambda", 0.1, "Weights", ww, ... %! "RelTol", 1e-8); %! assert_equal (B, [-2.496681686051138; 0], 1e-6); %! assert_equal (F.Intercept, 2.344204689224574, 1e-6); %! assert_equal (F.Deviance, 0.304953993765223, 1e-6); %!test %! ## A NaN row drops its number of trials. %! Xs = [1 2; 2 1; 3 5; NaN 3; 5 6; 6 4; 7 8; 8 7]; %! ys = [1 0 2 1 3 2 2 3]'; %! Ns = [3 2 3 3 4 3 4 3]'; %! [B, F] = lassoglm (Xs, ys, "binomial", "Lambda", 0.1, "BinomialSize", Ns); %! [Br, Fr] = lassoglm (Xs([1:3, 5:8],:), ys([1:3, 5:8]), "binomial", ... %! "Lambda", 0.1, "BinomialSize", Ns([1:3, 5:8])); %! assert_equal (B, Br); %! assert_equal (F.Deviance, Fr.Deviance); %!test %! ## MATLAB parity: a two-column Y holds the successes and the trials. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! ys = [1 0 2 1 3 2 2 3]'; %! Ns = [3 2 3 3 4 3 4 3]'; %! [B, F] = lassoglm (Xs, [ys, Ns], "binomial", "Lambda", 0.1, ... %! "RelTol", 1e-10); %! assert_equal (B, [0.047756856379363; 0.108580852504688], 1e-8); %! assert_equal (F.Intercept, -0.503479881696499, 1e-8); %! assert_equal (F.Deviance, 6.500381919164349, 1e-8); %!test %! ## MATLAB parity: two-column Y without standardization. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! ys = [1 0 2 1 3 2 2 3]'; %! Ns = [3 2 3 3 4 3 4 3]'; %! [B, F] = lassoglm (Xs, [ys, Ns], "binomial", "Lambda", 0.1, ... %! "RelTol", 1e-10, "Standardize", false); %! assert_equal (B, [0.109801582646360; 0.172075761411567], 1e-8); %! assert_equal (F.Intercept, -1.091146717397907, 1e-8); %! assert_equal (F.Deviance, 5.474626800232128, 1e-8); %!test %! ## 'BinomialSize' describes the same model as a two-column Y. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! ys = [1 0 2 1 3 2 2 3]'; %! Ns = [3 2 3 3 4 3 4 3]'; %! [B, F] = lassoglm (Xs, [ys, Ns], "binomial", "Lambda", 0.1); %! [Bs, Fs] = lassoglm (Xs, ys, "binomial", "Lambda", 0.1, ... %! "BinomialSize", Ns); %! assert_equal (B, Bs); %! assert_equal (F.Deviance, Fs.Deviance); %!test %! ## 'Size' is an alias for 'BinomialSize'. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! ys = [1 0 2 1 3 2 2 3]'; %! Ns = [3 2 3 3 4 3 4 3]'; %! B = lassoglm (Xs, ys, "binomial", "Lambda", 0.1, "BinomialSize", Ns); %! assert_equal (lassoglm (Xs, ys, "binomial", "Lambda", 0.1, "Size", Ns), B); %!test %! ## The trials in a two-column Y take precedence over 'BinomialSize'. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! ys = [1 0 2 1 3 2 2 3]'; %! Ns = [3 2 3 3 4 3 4 3]'; %! B = lassoglm (Xs, [ys, Ns], "binomial", "Lambda", 0.1); %! assert_equal (lassoglm (Xs, [ys, Ns], "binomial", "Lambda", 0.1, ... %! "BinomialSize", 5), B); %!test %! ## MATLAB parity: a NaN number of trials drops its row. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! ys = [1 0 2 1 3 2 2 3]'; %! Ns = [3 NaN 3 3 4 3 4 3]'; %! B = lassoglm (Xs, [ys, Ns], "binomial", "Lambda", 0.1); %! r = [1, 3:8]; %! assert_equal (B, lassoglm (Xs(r,:), [ys(r), Ns(r)], "binomial", ... %! "Lambda", 0.1)); %!test %! ## MATLAB parity: a NaN number of successes drops its row. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! ys = [1 NaN 2 1 3 2 2 3]'; %! Ns = [3 2 3 3 4 3 4 3]'; %! B = lassoglm (Xs, [ys, Ns], "binomial", "Lambda", 0.1); %! r = [1, 3:8]; %! assert_equal (B, lassoglm (Xs(r,:), [ys(r), Ns(r)], "binomial", ... %! "Lambda", 0.1)); %!test %! ## MATLAB parity: a row with no trials is dropped. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! ys = [1 0 2 1 3 2 2 3]'; %! Ns = [3 0 3 3 4 3 4 3]'; %! B = lassoglm (Xs, [ys, Ns], "binomial", "Lambda", 0.1); %! r = [1, 3:8]; %! assert_equal (B, lassoglm (Xs(r,:), [ys(r), Ns(r)], "binomial", ... %! "Lambda", 0.1)); %!test %! ## A weight of 2 counts its row twice; R2024a returns one fit per row. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! ys = [1 0 2 1 3 2 2 3]'; %! Ns = [3 2 3 3 4 3 4 3]'; %! ws = [1 2 1 2 1 2 1 2]'; %! r = [1 2 2 3 4 4 5 6 6 7 8 8]; %! B = lassoglm (Xs, [ys, Ns], "binomial", "Lambda", 0.1, "Weights", ws, ... %! "RelTol", 1e-10); %! Br = lassoglm (Xs(r,:), [ys(r), Ns(r)], "binomial", "Lambda", 0.1, ... %! "RelTol", 1e-10); %! assert_equal (B, Br, 1e-12); %!test %! ## MATLAB parity: a logical Y takes true as the success. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! yl = logical ([1 0 1 0 1 1 0 1]'); %! [B, F] = lassoglm (Xs, yl, "binomial", "Lambda", 0.1); %! assert_equal (B, [0; 0]); %! assert_equal (F.Intercept, 0.510825623765984, 1e-12); %! assert_equal (F.Deviance, 10.585011810527714, 1e-12); %!test %! ## MATLAB parity: a categorical Y takes its second category as the success. %! ## R2024a stops short of the optimum at its default 'RelTol', hence 1e-3. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! yc = categorical ({"a"; "b"; "a"; "b"; "a"; "a"; "b"; "a"}); %! [B, F] = lassoglm (Xs, yc, "binomial", "Lambda", 0.01, "RelTol", 1e-10); %! assert_equal (B, [0.139722006326075; -0.252634000628786], 1e-3); %! assert_equal (F.Intercept, -0.018771754821193, 1e-3); %! assert_equal (F.Deviance, 10.206050689662469, 1e-3); %!test %! ## MATLAB parity: the success follows the order of the categories. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! c = {"a"; "b"; "a"; "b"; "a"; "a"; "b"; "a"}; %! B = lassoglm (Xs, categorical (c, {"b", "a"}), "binomial", "Lambda", 0.01); %! assert_equal (B, lassoglm (Xs, double (strcmp (c, "a")), "binomial", ... %! "Lambda", 0.01)); %!test %! ## MATLAB parity: an undefined categorical element is missing. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! c = {"a"; "b"; "a"; "b"; "a"; "a"; "b"; "a"}; %! yc = categorical (c); %! yc(2) = missing; %! yd = double (strcmp (c, "b")); %! yd(2) = NaN; %! B = lassoglm (Xs, yc, "binomial", "Lambda", 0.01); %! assert_equal (B, lassoglm (Xs, yd, "binomial", "Lambda", 0.01)); %!test %! ## MATLAB parity: a logical Y is read as 0 and 1 for 'normal'. R2024a %! ## stops short of the optimum at its default 'RelTol', hence 1e-4. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! yl = logical ([1 0 1 0 1 1 0 1]'); %! [B, F] = lassoglm (Xs, yl, "normal", "Lambda", 0.01, "RelTol", 1e-10); %! assert_equal (B, [-0.032242354036557; 0.058218556273363], 1e-4); %! assert_equal (F.Intercept, 0.508107089934371, 1e-4); %! assert_equal (F.Deviance, 1.787206180927742, 1e-4); %!test %! ## MATLAB parity: a logical Y is read as 0 and 1 for 'poisson'. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! yl = logical ([1 0 1 0 1 1 0 1]'); %! [B, F] = lassoglm (Xs, yl, "poisson", "Lambda", 0.01, "RelTol", 1e-10); %! assert_equal (B, [-0.051841404637406; 0.093560162977848], 1e-4); %! assert_equal (F.Intercept, -0.666532313337360, 1e-4); %! assert_equal (F.Deviance, 4.559139924308249, 1e-4); %!test %! ## MATLAB parity: a categorical Y reads its second category as 1 for %! ## 'poisson'. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! yc = categorical ({"a"; "b"; "a"; "b"; "a"; "a"; "b"; "a"}); %! [B, F] = lassoglm (Xs, yc, "poisson", "Lambda", 0.01, "RelTol", 1e-10); %! assert_equal (B, [0.087166887965021; -0.157171639558528], 1e-4); %! assert_equal (F.Intercept, -0.690514209555808, 1e-4); %! assert_equal (F.Deviance, 5.648865520394161, 1e-4); %!test %! ## MATLAB parity: an undefined categorical element is missing for 'normal'. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! yc = categorical ({"a"; "b"; "a"; "b"; "a"; "a"; "b"; "a"}); %! yc(2) = missing; %! [B, F] = lassoglm (Xs, yc, "normal", "Lambda", 0.01, "RelTol", 1e-10); %! assert_equal (B, [0.028851564044848; 0.004953960674243], 1e-4); %! assert_equal (F.Intercept, 0.120808314125238, 1e-4); %! assert_equal (F.Deviance, 1.381322945196538, 1e-4); %!test %! ## MATLAB parity: all failures have no finite fit; B is zero. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! [B, F] = lassoglm (Xs, zeros (8, 1), "binomial", "Lambda", 0.01); %! assert_equal (B, [0; 0]); %! assert_equal (F.DF, 0); %! assert_equal (F.Intercept, log (sqrt (eps) / (1 - sqrt (eps))), 1e-12); %! assert_equal (F.Deviance < 1e-6, true); %!test %! ## MATLAB parity: all successes have no finite fit; B is zero. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! [B, F] = lassoglm (Xs, ones (8, 1), "binomial", "Lambda", 0.01); %! assert_equal (B, [0; 0]); %! assert_equal (F.Intercept, log ((1 - sqrt (eps)) / sqrt (eps)), 1e-12); %! assert_equal (F.Deviance < 1e-6, true); %!test %! ## MATLAB parity: a two-column Y of all successes has no finite fit. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! Ns = [3 2 3 3 4 3 4 3]'; %! [B, F] = lassoglm (Xs, [Ns, Ns], "binomial", "Lambda", 0.01); %! assert_equal (B, [0; 0]); %! assert_equal (F.Intercept, log ((1 - sqrt (eps)) / sqrt (eps)), 1e-12); %!test %! ## MATLAB parity: the default Lambda values are zero for all failures. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! [B, F] = lassoglm (Xs, zeros (8, 1), "binomial"); %! assert_equal (B, zeros (2, 100)); %! assert_equal (F.Lambda, zeros (1, 100)); %!test %! ## MATLAB parity: a 'poisson' Y of all zeros has no finite fit. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! [B, F] = lassoglm (Xs, zeros (8, 1), "poisson", "Lambda", 0.01); %! assert_equal (B, [0; 0]); %! assert_equal (F.Intercept, log (realmin), 1e-12); %! assert_equal (F.Deviance < 1e-300, true); %!test %! ## MATLAB parity: an ordinal categorical Y is read as a nominal one. %! Xs = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! c = {"a"; "b"; "a"; "b"; "a"; "a"; "b"; "a"}; %! yo = categorical (c, {"a", "b"}, "Ordinal", true); %! B = lassoglm (Xs, yo, "binomial", "Lambda", 0.01); %! assert_equal (B, lassoglm (Xs, categorical (c), "binomial", ... %! "Lambda", 0.01)); %!test %! ## MATLAB parity: single weights give a double-precision result. %! Xw = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! yw = [1 0 2 1 3 2 4 3]'; %! ww = single ([1 2 1 2 1 2 1 2]'); %! [B, F] = lassoglm (Xw, yw, "poisson", "Lambda", 0.1, "Weights", ww, ... %! "RelTol", 1e-8); %! assert_equal (class (B), "double"); %! assert_equal (B, [0; 0.272130393791213], 1e-6); %! assert_equal (F.Intercept, -0.739508255321235, 1e-6); %! assert_equal (F.Deviance, 3.144646252896119, 1e-6); ## No iteration returns the starting point; values checked against ## MATLAB R2024a %!test %! load fisheriris %! [B, FitInfo] = lassoglm (meas(:,2:4), meas(:,1), 'normal', 'MaxIter', 0); %! assert_equal (all (B(:) == 0), true); %! assert_equal (FitInfo.Intercept(end), 5.843333333, 1e-9); %!test %! load fisheriris %! y = double (strcmp (species, 'virginica')); %! [B, FitInfo] = lassoglm (meas(:,2:4), y, 'binomial', 'MaxIter', 0); %! assert_equal (all (B(:) == 0), true); %! assert_equal (FitInfo.Intercept(end), -0.6931471806, 1e-9); %! assert_equal (FitInfo.Deviance(end), 190.9542505, 1e-7); ## Test input validation %!error ... %! lassoglm ([1, 2; 3, 4; 5, 6], [1; 2; 3], 'normal', 'MaxIter', -1) %!error ... %! lassoglm ([1, 2; 3, 4; 5, 6], [1; 2; 3], 'normal', 'MaxIter', 1.5) %!error lassoglm (1) %!error lassoglm ("a", [1;2]) %!error lassoglm ([1, 2; 3, 4], "a") %!error ... %! lassoglm ([1, 2; 3, 4], [1; 2; 3]) %!error ... %! lassoglm ([1, 2; 3, 4], [1; 0], 'wibble') %!error ... %! lassoglm ([1, 2; 3, 4], [1; -1], 'poisson') %!error ... %! lassoglm ([1, 2; 3, 4], [1; 0], 'gamma') %!error ... %! lassoglm ([1, 2; 3, 4], [1; 3], 'binomial') %!error ... %! lassoglm ([1, 2; 3, 4], [1; 0], 'binomial', 'Offset', [1, 2, 3]) %!error ... %! lassoglm (X, yp, 'poisson', 'Weights', int8 (ones (14, 1))) %!error ... %! lassoglm (X, yp, 'poisson', 'Weights', true (14, 1)) %!error ... %! lassoglm (X, yp, 'poisson', 'Weights', complex (ones (14, 1), 0)) %!error ... %! Xn = X; Xn(3,1) = NaN; %! lassoglm (Xn, yp, 'poisson', 'Weights', ones (13, 1)) %!error ... %! lassoglm (X, yp, 'poisson', 'Weights', [Inf; ones(13,1)]) %!error ... %! lassoglm (X, yp, 'poisson', 'Weights', [-1; ones(13,1)]) %!error ... %! Xn = X; Xn(2:14,1) = NaN; %! lassoglm (Xn, yp, 'poisson', 'Lambda', 0.1) %!error ... %! Xn = X; Xn(2,1) = NaN; %! w = zeros (14, 1); w([2 5]) = 1; %! lassoglm (Xn, yp, 'poisson', 'Weights', w) %!error ... %! lassoglm (X, yb, 'binomial', 'BinomialSize', ones (15, 1)) %!error ... %! Xn = X; Xn(3,1) = NaN; %! lassoglm (Xn, yb, 'binomial', 'BinomialSize', ones (13, 1)) %!error ... %! lassoglm (X, yb, 'binomial', 'BinomialSize', [-1; ones(13,1)]) %!error ... %! lassoglm (X, yb, 'binomial', 'BinomialSize', 1i) %!error ... %! lassoglm (X, [2 * yb, ones(14,1)], 'binomial') %!error ... %! lassoglm (X, [yb, ones(14,2)], 'binomial') %!error ... %! lassoglm (X, [yp, ones(14,1)], 'poisson') %!error ... %! lassoglm (X(1:3,:), categorical ({'a'; 'b'; 'a'}, {'a', 'b', 'c'}), ... %! 'binomial') %!error ... %! lassoglm (X(1:3,:), {'a'; 'b'; 'a'}, 'binomial') %!error ... %! lassoglm (X(1:3,:), categorical ({'a'; 'b'; 'a'}, {'a', 'b', 'c'}), ... %! 'normal') %!error ... %! lassoglm (X(1:3,:), logical ([1; 0; 1]), 'gamma') %!error ... %! lassoglm ([1, 2; 3, 4], [1; 0], 'binomial', 'Alpha', 0) %!error ... %! lassoglm ([1, 2; 3, 4], [1; 0], 'binomial', 'Lambda', -1) %!error ... %! lassoglm ([1, 2; 3, 4], [1; 0], 'binomial', 'Lambda') %!error ... %! lassoglm ([1, 2; 3, 4], [1; 0], 'binomial', 'foo', 1) statistics-release-2.1.2/inst/Regression/logistic_regression.m000066400000000000000000000260721527311104300246600ustar00rootroot00000000000000## Copyright (C) 1995-2017 Kurt Hornik ## Copyright (C) 2022 Andrew Penn ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{intercept}, @var{slope}, @var{dev}, @var{dl}, @var{d2l}, @var{P}, @var{stats}] =} logistic_regression (@var{y}, @var{x}, @var{print}, @var{intercept}, @var{slope}) ## ## Perform ordinal logistic regression. ## ## Suppose @var{y} takes values in k ordered categories, and let ## @code{P_i (@var{x})} be the cumulative probability that @var{y} ## falls in one of the first i categories given the covariate ## @var{x}. Then ## ## @example ## [@var{intercept}, @var{slope}] = logistic_regression (@var{y}, @var{x}) ## @end example ## ## @noindent ## fits the model ## ## @example ## logit (P_i (@var{x})) = @var{x} * @var{slope} + @var{intercept}_i, ## i = 1 @dots{} k-1 ## @end example ## ## The number of ordinal categories, k, is taken to be the number ## of distinct values of @code{round (@var{y})}. If k equals 2, ## @var{y} is binary and the model is ordinary logistic regression. The ## matrix @var{x} is assumed to have full column rank. ## ## Given @var{y} only, @code{@var{intercept} = logistic_regression (@var{y})} ## fits the model with baseline logit odds only. ## ## The full form is ## ## @example ## @group ## [@var{intercept}, @var{slope}, @var{dev}, @var{dl}, @var{d2l}, @var{P}, ... ## @var{stats}] = logistic_regression (@var{y}, @var{x}, @var{print}, ... ## @var{intercept}, @var{slope}) ## @end group ## @end example ## ## @noindent ## in which all output arguments and all input arguments except @var{y} ## are optional. ## ## Setting @var{print} to 1 requests summary information about the fitted ## model to be displayed. Setting @var{print} to 2 requests information ## about convergence at each iteration. Other values request no ## information to be displayed. The input arguments @var{intercept} and ## @var{slope} give initial estimates for @var{intercept} and @var{slope}. ## ## The returned value @var{dev} holds minus twice the log-likelihood. ## ## The returned values @var{dl} and @var{d2l} are the vector of first ## and the matrix of second derivatives of the log-likelihood with ## respect to @var{intercept} and @var{slope}. ## ## @var{P} holds estimates for the conditional distribution of @var{y} ## given @var{x}. ## ## @var{stats} returns a structure that contains the following fields: ## @itemize ## @item ## "intercept": intercept coefficients ## @item ## "slope": slope coefficients ## @item ## "coeff": regression coefficients (intercepts and slops) ## @item ## "covb": estimated covariance matrix for coefficients (coeff) ## @item ## "coeffcorr": correlation matrix for coeff ## @item ## "se": standard errors of the coeff ## @item ## "z": z statistics for coeff ## @item ## "pval": p-values for coeff ## @end itemize ## @end deftypefn function [intercept, slope, dev, dl, d2l, P, stats] = logistic_regression (y, x, print, intercept, slope) ## check input y = round (y(:)); if (nargin < 2) x = zeros (length (y), 0); endif; xymissing = (isnan (y) | any (isnan (x), 2)); y(xymissing) = []; x(xymissing,:) = []; [my, ny] = size (y); [mx, nx] = size (x); if (mx != my) error ("logistic_regression: X and Y must have the same number of observations"); endif ## initial calculations tol = 1e-12; incr = 10; decr = 2; ymin = min (y); ymax = max (y); yrange = ymax - ymin; z = (y * ones (1, yrange)) == ((y * 0 + 1) * (ymin : (ymax - 1))); z1 = (y * ones (1, yrange)) == ((y * 0 + 1) * ((ymin + 1) : ymax)); z = z(:, any (z)); z1 = z1(:, any (z1)); [mz, nz] = size (z); ## starting values if (nargin < 3) print = 0; endif; if (nargin < 4) g = cumsum (sum (z))' ./ my; intercept = log (g ./ (1 - g)); endif; if (nargin < 5) slope = zeros (nx, 1); endif; tb = [intercept; slope]; ## likelihood and derivatives at starting values [g, g1, p, dev] = logistic_regression_likelihood (y, x, tb, z, z1); [dl, d2l] = logistic_regression_derivatives (x, z, z1, g, g1, p); epsilon = std (vec (d2l)) / 1000; ## maximize likelihood using Levenberg modified Newton's method iter = 0; while (abs (dl' * (d2l \ dl) / length (dl)) > tol) iter += 1; tbold = tb; devold = dev; tb = tbold - d2l \ dl; [g, g1, p, dev] = logistic_regression_likelihood (y, x, tb, z, z1); if ((dev - devold) / (dl' * (tb - tbold)) < 0) epsilon /= decr; else while ((dev - devold) / (dl' * (tb - tbold)) > 0) epsilon *= incr; if (epsilon > 1e+15) error ("logistic_regression: epsilon too large"); endif tb = tbold - (d2l - epsilon * eye (size (d2l))) \ dl; [g, g1, p, dev] = logistic_regression_likelihood (y, x, tb, z, z1); disp ('epsilon'); disp (epsilon); endwhile endif [dl, d2l] = logistic_regression_derivatives (x, z, z1, g, g1, p); if (print == 2) disp ('Iteration'); disp (iter); disp ('Deviance'); disp (dev); disp ('First derivative'); disp (dl'); disp ('Eigenvalues of second derivative'); disp (eig (d2l)'); endif endwhile ## tidy up output intercept = tb(1 : nz, 1); slope = tb((nz + 1) : (nz + nx), 1); cov = inv (-d2l); se = sqrt (diag (cov)); if (nargout > 5) ## Compute predicted probabilities (P) if (nx > 0) e = ((x * slope) * ones (1, nz)) + ((y * 0 + 1) * intercept'); else e = (y * 0 + 1) * intercept'; endif P = diff ([(y * 0), (exp (e) ./ (1 + exp (e))), (y * 0 + 1)]')'; endif if (nargout > 6) ## Create stats structure dfe = mx - nx - 1; zstat = tb ./ se; coeffcorr = cov2corr (cov); resid = y - P(:,2); stats = struct ('intercept', intercept, ... 'slope', slope, ... 'coeff', tb, ... 'cov', cov, ... 'coeffcorr', coeffcorr, ... 'se', se, ... 'z', zstat, ... 'pval', 2 * normcdf (-abs (zstat))); endif if (print >= 1) printf ("\n"); printf ("Logistic Regression Results:\n"); printf ("\n"); printf ("Number of Iterations: %d\n", iter); printf ("Deviance: %f\n", dev); printf ("Parameter Estimates:\n"); printf (" Intercept S.E.\n"); for i = 1 : nz printf (" %8.4f %8.4f\n", tb(i), se(i)); endfor if (nx > 0) printf (" Slope S.E.\n"); for i = (nz + 1) : (nz + nx) printf (" %8.4f %8.4f\n", tb(i), se(i)); endfor endif endif endfunction function [g, g1, p, dev] = logistic_regression_likelihood (y, x, slope, z, z1) ## Calculate the likelihood for the ordinal logistic regression model. e = exp ([z, x] * slope); e1 = exp ([z1, x] * slope); g = e ./ (1 + e); g1 = e1 ./ (1 + e1); g = max (y == max (y), g); g1 = min (y > min (y), g1); p = g - g1; dev = -2 * sum (log (p)); endfunction function [dl, d2l] = logistic_regression_derivatives (x, z, z1, g, g1, p) ## Calculate derivatives of the log-likelihood for ordinal logistic regression ## first derivative v = g .* (1 - g) ./ p; v1 = g1 .* (1 - g1) ./ p; dlogp = [(diag (v) * z - diag (v1) * z1), (diag (v - v1) * x)]; dl = sum (dlogp)'; ## second derivative w = v .* (1 - 2 * g); w1 = v1 .* (1 - 2 * g1); d2l = [z, x]' * diag (w) * [z, x] - [z1, x]' * diag (w1) * [z1, x] ... - dlogp' * dlogp; endfunction function R = cov2corr (vcov) ## Convert covariance matrix to correlation matrix sed = sqrt (diag (vcov)); R = vcov ./ (sed * sed'); R = (R + R') / 2; # This step ensures that the matrix is positive definite endfunction %!test %! # Output compared to following MATLAB commands %! # [B, DEV, STATS] = mnrfit(X,Y+1,'model','ordinal'); %! # P = mnrval(B,X) %! X = [1.489381332449196, 1.1534152241851305; ... %! 1.8110085304863965, 0.9449666896938425; ... %! -0.04453299665130296, 0.34278203449678646; ... %! -0.36616019468850347, 1.130254275908322; ... %! 0.15339143291005095, -0.7921044310668951; ... %! -1.6031878794469698, -1.8343471035233376; ... %! -0.14349521143198166, -0.6762996896828459; ... %! -0.4403818557740143, -0.7921044310668951; ... %! -0.7372685001160434, -0.027793137932169563; ... %! -0.11875465773681024, 0.5512305689880763]; %! Y = [1,1,1,1,1,0,0,0,0,0]'; %! [INTERCEPT, SLOPE, DEV, DL, D2L, P] = logistic_regression (Y, X, false); #%! assert_equal (DEV, 5.680728861124, 1e-05); #%! assert_equal (INTERCEPT(1), -1.10999599948243, 1e-05); #%! assert_equal (SLOPE(1), -9.12480634225699, 1e-05); #%! assert_equal (SLOPE(2), -2.18746124517476, 1e-05); #%! assert_equal (corr(P(:,1),Y), -0.786673288976468, 1e-05); %!test %! # Output compared to following MATLAB commands %! # [B, DEV, STATS] = mnrfit(X,Y+1,'model','ordinal'); %! load carbig %! X = [Acceleration Displacement Horsepower Weight]; %! miles = [1,1,1,1,1,1,1,1,1,1,NaN,NaN,NaN,NaN,NaN,1,1,NaN,1,1,2,2,1,2,2,2, ... %! 2,2,2,2,2,1,1,1,1,2,2,2,2,NaN,2,1,1,2,1,1,1,1,1,1,1,1,1,2,2,1,2, ... %! 2,3,3,3,3,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,2,2,2,2,2, ... %! 2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,1,1,2,2,2,1,2,2, ... %! 2,1,1,3,2,2,2,1,2,2,1,2,2,2,1,3,2,3,2,1,1,1,1,1,1,1,1,3,2,2,3,3, ... %! 2,2,2,2,2,3,2,1,1,1,1,1,1,1,1,1,1,1,2,2,1,3,2,2,2,2,2,2,1,3,2,2, ... %! 2,2,2,3,2,2,2,2,2,1,1,1,1,2,2,2,2,3,2,3,3,2,1,1,1,3,3,2,2,2,1,2, ... %! 2,1,1,1,1,1,3,3,3,2,3,1,1,1,1,1,2,2,1,1,1,1,1,3,2,2,2,3,3,3,3,2, ... %! 2,2,4,3,3,4,3,2,2,2,2,2,2,2,2,2,2,2,1,1,2,1,1,1,3,2,2,3,2,2,2,2, ... %! 2,1,2,1,3,3,2,2,2,2,2,1,1,1,1,1,1,2,1,3,3,3,2,2,2,2,2,3,3,3,3,2, ... %! 2,2,3,4,3,3,3,2,2,2,2,3,3,3,3,3,4,2,4,4,4,3,3,4,4,3,3,3,2,3,2,3, ... %! 2,2,2,2,3,4,4,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,NaN,3,2,2,2,2,2,1,2, ... %! 2,3,3,3,2,2,2,3,3,3,3,3,3,3,3,3,3,3,2,3,2,2,3,3,2,2,4,3,2,3]'; %! [INTERCEPT, SLOPE, DEV, DL, D2L, P] = logistic_regression (miles, X, false); %! assert_equal (DEV, 433.197174495549, 1e-05); %! assert_equal (INTERCEPT(1), -16.6895155618903, 1e-05); %! assert_equal (INTERCEPT(2), -11.7207818178493, 1e-05); %! assert_equal (INTERCEPT(3), -8.0605768506075, 1e-05); %! assert_equal (SLOPE(1), 0.104762463756714, 1e-05); %! assert_equal (SLOPE(2), 0.0103357623191891, 1e-05); %! assert_equal (SLOPE(3), 0.0645199313242276, 1e-05); %! assert_equal (SLOPE(4), 0.00166377028388103, 1e-05); statistics-release-2.1.2/inst/Regression/mnrfit.m000066400000000000000000001410741527311104300221020ustar00rootroot00000000000000## Copyright (C) 2024 Andrew C Penn ## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{B} =} mnrfit (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{B} =} mnrfit (@var{X}, @var{Y}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{B}, @var{dev}] =} mnrfit (@dots{}) ## @deftypefnx {statistics} {[@var{B}, @var{dev}, @var{stats}] =} mnrfit (@dots{}) ## ## Fit a multinomial logistic regression model. ## ## Nominal models are fitted with a baseline-category multinomial logit, using ## the last category of @var{Y} as the reference. Ordinal models are fitted ## with a cumulative link model and hierarchical models with a sequential ## (continuation-ratio) link model, both honouring the @qcode{'link'} option ## below. Nominal models always use the logit link. ## ## @code{@var{B} = mnrfit (@var{X}, @var{Y})} returns a matrix, @var{B}, of ## coefficient estimates for a multinomial logistic regression of the nominal ## responses in @var{Y} on the predictors in @var{X}. @var{X} is an @math{N*P} ## numeric matrix the observations on predictor variables, where @math{N} ## corresponds to the number of observations and @math{P} corresponds to ## predictor variables. @var{Y} contains the response category labels and it ## can be an @math{N*1} vector of positive integers, logical values, character ## vectors in a cell array, strings, or categorical values, each element the ## category of one observation. A cell array, string, or categorical @var{Y} ## can also be a row vector. @var{Y} can also be a character matrix with each ## row holding the label of an observation of @var{X}. ## ## @var{Y} can also be an @math{N*K} numeric or logical matrix of counts, with ## a column for each of the @math{K} categories: @code{@var{Y}(i,j)} is the ## number of observations in category @math{j} at the predictor values in ## @code{@var{X}(i,:)}, so that a row of @var{X} stands for the ## @code{sum (@var{Y}(i,:))} observations it was seen with. The counts must be ## nonnegative integers. A row of zeros stands for no observations and takes ## no part in the estimates, though it counts in @qcode{'dfe'}, as in MATLAB. ## A matrix of 1s and 0s with a single 1 in each row is the same as a vector ## of one category per observation. ## ## The categories of a numeric or logical @var{Y} are taken in sorted order, ## those of a cell array, string, or character matrix in order of first ## appearance, and those of a categorical @var{Y} in the order of its ## categories. A category without observations is ignored wherever it lies ## among the categories of a categorical @var{Y}; MATLAB ignores only a last one ## and fits coefficients for any other. Likewise the categories of a numeric ## @var{Y} are the values it holds, where MATLAB numbers them from 1 to the ## largest and fits coefficients for any value missing below it, and a column ## of zeros in a matrix @var{Y} is no category, where MATLAB fits coefficients ## for it. The last ## category is the reference category of a nominal model. An observation whose ## label is missing (an empty character vector, @qcode{}, or ## @qcode{}), whose row of a matrix @var{Y} holds a @qcode{NaN}, or ## whose predictors hold a @qcode{NaN}, is left out of the fit, and its rows of ## the residuals in @var{stats} are @qcode{NaN}. ## ## A @var{Y} with a single category leaves nothing to fit: a nominal or ## hierarchical model returns a @var{B} with no columns, a deviance of 0 and ## residuals of 0, the raw and Pearson residuals of a hierarchical model having ## no columns, and an ordinal model is an error. ## ## @code{@var{B} = mnrfit (@var{X}, @var{Y}, @var{name}, @var{value})} returns a ## matrix, @var{B}, of coefficient estimates for a multinomial model fit with ## additional parameters specified @qcode{Name-Value} pair arguments. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'model'} @tab The type of model to fit: @qcode{'nominal'} ## (default) for a baseline-category model, @qcode{'ordinal'} for a cumulative ## model, or @qcode{'hierarchical'} for a sequential (continuation-ratio) model. ## ## @item @qcode{'link'} @tab The link function for ordinal and hierarchical ## models: @qcode{'logit'} (default), @qcode{'probit'}, @qcode{'comploglog'}, or ## @qcode{'loglog'}. Nominal models always use the logit link. ## ## @item @qcode{'interactions'} @tab @qcode{'on'} to give each category its ## own slopes, or @qcode{'off'} for slopes shared by every category, the ## intercepts always differing. The default is @qcode{'on'} for nominal and ## hierarchical models and @qcode{'off'} for ordinal ones. With ## @qcode{'on'}, @var{B} is a @math{(P+1)*(K-1)} matrix whose column @math{j} ## holds the intercept and slopes of category @math{j}; with @qcode{'off'}, ## @var{B} is a @math{(K-1+P)*1} vector of the @math{K-1} intercepts followed by ## the @math{P} shared slopes. A nominal model without interactions has no ## slopes to share, so it fits the @math{K-1} intercepts alone, with a warning. ## ## @item @qcode{'weights'} @tab An @math{N*1} vector of nonnegative ## observation weights, each multiplying the counts of its row of @var{Y}. ## The default is a vector of ones. A row whose weight is @qcode{NaN} is left ## out of the fit. ## ## @item @qcode{'iterationlimit'} @tab The largest number of iterations of the ## fit, a positive integer. The default is 100. ## ## @item @qcode{'tolerance'} @tab The convergence tolerance of the fit, a ## positive scalar: the fit stops once no coefficient changes by more than this ## fraction of its value. The default is @code{1e-6}. ## ## @item @qcode{'estdisp'} @tab @qcode{'on'} to estimate a dispersion ## parameter, scaling the coefficient standard errors by it and testing the ## coefficients against the @math{t} distribution, or @qcode{'off'} (default) ## for the theoretical dispersion of @math{1}. ## @end multitable ## ## @code{[@var{B}, @var{dev}, @var{stats}] = mnrfit (@dots{})} also returns the ## deviance of the fit, @var{dev}, and a structure @var{stats} with the fitted ## coefficients @qcode{'beta'} (same as @var{B}), their standard errors ## @qcode{'se'}, covariance matrix @qcode{'covb'}, correlation matrix ## @qcode{'coeffcorr'}, error degrees of freedom @qcode{'dfe'}, the coefficient ## @math{t} statistics @qcode{'t'} and @math{p}-values @qcode{'p'}, the ## dispersion parameters @qcode{'s'}, @qcode{'sfit'}, and @qcode{'estdisp'}, and ## the raw, Pearson, and deviance residuals @qcode{'resid'}, @qcode{'residp'}, ## and @qcode{'residd'}. ## ## The raw and Pearson residuals are taken on what the model fits: an ## @math{N*K} matrix over the categories of a nominal model, and an ## @math{N*(K-1)} matrix over the cumulative probabilities of an ordinal model ## or over the stages of a hierarchical one, where a row is @qcode{NaN} at the ## stages that none of its observations reach. A raw residual is the count ## minus its fitted value, the count times the fitted probability, and a ## Pearson residual divides it by the binomial standard deviation of the count; ## a weight multiplies both counts. A row of zeros has a Pearson residual of ## @qcode{NaN}. The deviance residuals are an @math{N*1} vector holding the ## contribution of each row to the deviance, so they sum to @var{dev}; for an ## observation of one category it is @math{-2} times the log of the fitted ## probability of that category. For a hierarchical model MATLAB sums the ## signed deviance residuals of the stages instead, which is @qcode{NaN} for ## every observation in the first category. @qcode{'sfit'} is the square root ## of the Pearson statistic over the categories divided by @qcode{'dfe'}, and it ## differs from MATLAB's, which for ordinal and hierarchical models is taken ## over the residuals in @qcode{'residp'}. ## ## @qcode{'dfe'} is the number of rows of @var{X} times @math{K-1}, less the ## number of coefficients; for a hierarchical model each stage counts only the ## rows with observations that reach it. ## ## @seealso{mnrval, logistic_regression} ## @end deftypefn function [B, DEV, STATS] = mnrfit (X, Y, varargin) ## Check input arguments X and Y if (nargin < 2) error ("mnrfit: too few input arguments."); endif if (! isnumeric (X)) error ("mnrfit: Predictors must be numeric.") endif if (isscalar (X) || (ndims (X) > 2)) error ("mnrfit: Predictors must be a vector or a 2D matrix.") endif if (isscalar (Y) || (ndims (Y) > 2)) error ("mnrfit: Response must be a vector or a 2D matrix.") endif ## A row vector of text or categories holds one label per observation if ((iscellstr (Y) || isstring (Y) || iscategorical (Y)) && isrow (Y)) Y = Y(:); endif [N, P] = size (X); [n, K] = size (Y); if (N == 1) ## if X is a row vector, make it a column vector X = X(:); N = P; P = 1; endif if (n != N) error ("mnrfit: Y must have the same number of rows as X.") endif if (! (isnumeric (Y) || islogical (Y) || ischar (Y) || iscellstr (Y) || isstring (Y) || iscategorical (Y))) error (strcat ("mnrfit: Response labels must be a character array,", ... " a cell vector of strings, a string or categorical", ... " vector, or a vector or matrix of doubles, singles or", ... " logical values.")); endif ## Check supplied parameters if (mod (numel (varargin), 2) != 0) error ("mnrfit: optional arguments must be in pairs.") endif MODELTYPE = 'nominal'; LINK = 'logit'; ESTDISP = 'off'; INTERACTIONS = []; ## resolved to a default per model below WEIGHTS = []; MAXITER = 100; TOL = 1e-6; while (numel (varargin) > 0) name = varargin{1}; value = varargin{2}; switch (lower (name)) case 'model' MODELTYPE = value; case 'link' LINK = value; case 'estdisp' ESTDISP = value; case 'interactions' INTERACTIONS = value; case 'weights' WEIGHTS = value; case 'iterationlimit' MAXITER = value; case 'tolerance' TOL = value; otherwise warning (sprintf ("mnrfit: parameter %s will be ignored", name)); endswitch varargin(1:2) = []; endwhile MODELTYPE = lower (MODELTYPE); if (! any (strcmp (MODELTYPE, {'nominal', 'ordinal', 'hierarchical'}))) error ("mnrfit: model type not recognised."); endif LINK = lower (LINK); if (! any (strcmp (LINK, {'logit', 'probit', 'comploglog', 'loglog'}))) error ("mnrfit: unrecognised 'link' value."); endif if (! (ischar (ESTDISP) && any (strcmpi (ESTDISP, {'on', 'off'})))) error ("mnrfit: 'estdisp' must be 'on' or 'off'."); endif estdisp = strcmpi (ESTDISP, 'on'); if (isempty (INTERACTIONS)) if (strcmp (MODELTYPE, 'ordinal')) INTERACTIONS = 'off'; else INTERACTIONS = 'on'; endif endif if (! (ischar (INTERACTIONS) && any (strcmpi (INTERACTIONS, {'on', 'off'})))) error ("mnrfit: 'interactions' must be 'on' or 'off'."); endif interact = strcmpi (INTERACTIONS, 'on'); if (! (isnumeric (MAXITER) && isscalar (MAXITER) && MAXITER >= 1 && fix (MAXITER) == MAXITER)) error ("mnrfit: 'iterationlimit' must be a positive integer."); endif if (! (isnumeric (TOL) && isscalar (TOL) && TOL > 0)) error ("mnrfit: 'tolerance' must be a positive scalar."); endif if (isempty (WEIGHTS)) W = ones (N, 1); else if (! (isnumeric (WEIGHTS) && isreal (WEIGHTS) && isvector (WEIGHTS) && numel (WEIGHTS) == N && all (WEIGHTS(! isnan (WEIGHTS)) >= 0))) error (strcat ("mnrfit: 'weights' must be a vector of nonnegative", ... " values with one element per observation.")); endif W = double (WEIGHTS(:)); endif ## Categorize Y if it holds text or categories. Text is numbered by first ## appearance and a categorical by its categories, as in MATLAB. Each row of ## a character array is one label, so its columns are not categories. istext = ischar (Y) || iscellstr (Y) || isstring (Y) || iscategorical (Y); iscount = K > 1 && ! istext; if (istext) if (ischar (Y)) Y = cellstr (Y); elseif (K > 1) if (iscellstr (Y)) ytype = 'cellstr'; else ytype = class (Y); endif error ("mnrfit: Y must be a column vector when given as %s.", ytype); endif ## Get groups in Y: "", and are NaN YN = grp2idx (Y); Ymiss = isnan (YN); elseif (iscount) ## A matrix Y holds the number of observations in each category for each ## row of X; a row holding a NaN is missing YN = double (Y); Yok = isnan (YN) | (YN >= 0 & fix (YN) == YN); if (! all (Yok(:))) error (strcat ("mnrfit: Y must contain nonnegative integer counts", ... " when given as a 2D matrix.")); endif Ymiss = any (isnan (YN), 2); else YN = double (Y); Ymiss = isnan (YN); endif ## Remove missing values from X, Y and the weights RowsUsed = ! (any (isnan (X), 2) | Ymiss | isnan (W)); X = X(RowsUsed,:); W = W(RowsUsed); YN = YN(RowsUsed,:); if (isempty (YN)) error ("mnrfit: X and Y must contain at least one observation."); endif if (isnumeric (Y) && ! iscount) if (! (all (YN > 0) && all (fix (YN) == YN))) error ("mnrfit: Y must contain positive integer category numbers.") endif endif ## Hold the response as counts, one column per category. A category without ## observations is ignored, as a categorical may hold unused ones and a ## category may have lost all its observations. if (iscount) C = YN(:, any (YN > 0, 1)); else [~, ~, YN] = unique (YN); C = double (YN(:) == (1:max (YN))); endif n = columns (C); if (n == 0) error ("mnrfit: X and Y must contain at least one observation."); endif ## Fit the requested model type switch (MODELTYPE) case 'nominal' if (! strcmp (LINK, 'logit')) error ("mnrfit: nominal models support only the logit link."); endif if (n == 1) STATS = mnrfit_empty_ (X, estdisp, 1); else if (! interact) warning (strcat ("mnrfit: a nominal model without interactions", ... " fits the intercepts alone.")); endif STATS = mnrfit_nominal_ (X, C, W, interact, MAXITER, TOL); endif case 'ordinal' if (n == 1) error ("mnrfit: an ordinal model needs at least two categories."); endif STATS = mnrfit_ordinal_ (X, C, W, LINK, interact, MAXITER, TOL); case 'hierarchical' if (n == 1) STATS = mnrfit_empty_ (X, estdisp, 0); else STATS = mnrfit_hierarchical_ (X, C, W, LINK, interact, MAXITER, TOL); endif endswitch B = STATS.beta; ## Residuals, deviance and dispersion, computed from the fitted category ## probabilities (common to every model type) DEV = 0; if (n > 1) pihat = mnrfit_pihat_ (B, X, MODELTYPE, interact, LINK); [STATS.resid, STATS.residp, STATS.residd] = ... mnrfit_residuals_ (pihat, C, W, MODELTYPE); DEV = sum (STATS.residd); STATS.estdisp = estdisp; endif if (! isempty (STATS.dfe) && STATS.dfe > 0) ## Standard Pearson dispersion estimate, over the categories e = sum (C, 2) .* pihat; X2 = W .* (C - e) .^ 2 ./ e; STATS.sfit = sqrt (sum (X2(e > 0)) / STATS.dfe); if (estdisp) ## Scale the coefficient covariance and standard errors by the estimated ## dispersion and test the coefficients against the t distribution. STATS.s = STATS.sfit; STATS.covb = STATS.covb * STATS.s ^ 2; STATS.se = STATS.se * STATS.s; STATS.t = STATS.beta ./ STATS.se; STATS.p = 2 * tcdf (- abs (STATS.t), STATS.dfe); endif endif ## Observations left out keep their rows in the residuals, as NaN if (! all (RowsUsed)) for name = {'resid', 'residp', 'residd'} R = NaN (numel (RowsUsed), columns (STATS.(name{1}))); R(RowsUsed,:) = STATS.(name{1}); STATS.(name{1}) = R; endfor endif endfunction ## Fit a baseline-category multinomial logit model by Newton-Raphson. The last ## category is the reference; B is a (P+1)-by-(K-1) matrix whose column j holds ## the intercept and slopes contrasting category j against the reference, or ## without interactions a (K-1)-by-1 vector of intercepts alone. C holds the ## counts of each category and W multiplies them. function stats = mnrfit_nominal_ (X, C, W, interact, maxiter, tol) nobs = rows (X); if (interact) Z = [ones(nobs, 1), X]; ## design with intercept, nobs-by-q else Z = ones (nobs, 1); endif q = columns (Z); ncat = columns (C) - 1; ## non-reference categories (1..k-1) wC = W .* C(:,1:ncat); ## weighted counts, nobs-by-ncat wn = W .* sum (C, 2); ## weighted trials, nobs-by-1 ## Newton-Raphson on the multinomial logit log-likelihood beta = zeros (q, ncat); converged = false; for iter = 1:maxiter [grad, H] = mnrfit_nominal_derivs_ (Z, beta, wC, wn); ## A vanishing Hessian condition signals (quasi-)separable data with no ## finite maximum likelihood estimate; stop and keep the current estimate. if (rcond (H) < eps) break; endif step = - (H \ grad(:)); ## Newton step (maximise the likelihood) if (! all (isfinite (step))) break; endif old = beta(:); beta(:) = old + step; if (all (abs (step) <= tol * max (sqrt (eps), abs (old)))) converged = true; break; endif endfor if (! converged) warning ("mnrfit: iteration limit reached; results may be unreliable."); endif [~, H] = mnrfit_nominal_derivs_ (Z, beta, wC, wn); if (interact) B = beta; else B = beta(:); endif ## Coefficient covariance from the inverse negative Hessian at the MLE covb = inv (-H); se_v = sqrt (diag (covb)); se = reshape (se_v, size (B)); tstat = B ./ se; stats = mnrfit_stats_ (B, nobs * ncat - numel (B), covb, se, tstat); endfunction ## Gradient and Hessian of the multinomial logit log-likelihood at BETA function [grad, H] = mnrfit_nominal_derivs_ (Z, beta, wC, wn) [nobs, q] = size (Z); ncat = columns (beta); P = mnrfit_softmax_ (Z, beta, nobs); grad = Z' * (wC - wn .* P); ## q-by-ncat ## Hessian of the log-likelihood (block form, negative definite) H = zeros (q * ncat); for a = 1:ncat for b = 1:ncat if (a == b) w = P(:,a) .* (1 - P(:,a)); else w = - P(:,a) .* P(:,b); endif H((a-1)*q+(1:q), (b-1)*q+(1:q)) = - (Z' * (Z .* (wn .* w))); endfor endfor endfunction ## Fit a sequential (continuation-ratio) model. Stage j is a binary model of ## category j against the later categories, over the trials that reach it. ## With interactions each stage has its own coefficients, and B is a ## (P+1)-by-(K-1) matrix; without, the stages share the slopes and B is a ## (K-1+P)-by-1 vector of K-1 intercepts followed by the P slopes. function stats = mnrfit_hierarchical_ (X, C, W, link, interact, maxiter, tol) [nobs, p] = size (X); ncat = columns (C) - 1; ## The trials reaching each stage, those not in an earlier category m = sum (C, 2) - [zeros(nobs, 1), cumsum(C(:,1:ncat-1), 2)]; opts = struct ('MaxIter', maxiter, 'TolX', tol); if (interact) q = p + 1; B = zeros (q, ncat); se = zeros (q, ncat); covb = zeros (q * ncat); for j = 1:ncat r = m(:,j) > 0; [Bj, ~, sj] = glmfit (X(r,:), [C(r,j), m(r,j)], 'binomial', ... 'link', link, 'weights', W(r), 'options', opts); B(:,j) = Bj; se(:,j) = sj.se; covb((j-1)*q+(1:q), (j-1)*q+(1:q)) = sj.covb; endfor else ## One binary model over every stage, with an intercept for each stage [ii, jj] = find (m > 0); idx = sub2ind (size (m), ii, jj); Z = [double(jj == (1:ncat)), X(ii,:)]; [B, ~, sj] = glmfit (Z, [C(idx), m(idx)], 'binomial', 'link', link, ... 'constant', 'off', 'weights', W(ii), 'options', opts); se = sj.se; covb = sj.covb; endif tstat = B ./ se; stats = mnrfit_stats_ (B, sum (m(:) > 0) - numel (B), covb, se, tstat); endfunction ## Fit a cumulative link model by Fisher scoring. Without interactions the ## categories share the slopes and B is a (K-1+P)-by-1 vector of K-1 ## thresholds followed by the P slopes; with interactions each cumulative ## probability has its own slopes and B is a (P+1)-by-(K-1) matrix. function stats = mnrfit_ordinal_ (X, C, W, link, interact, maxiter, tol) [nobs, p] = size (X); ncat = columns (C) - 1; [F, f] = mnrfit_links_ (link); ## The derivative of each cumulative linear predictor by the parameters if (interact) npar = ncat * (p + 1); else npar = ncat + p; endif D = cell (1, ncat); for j = 1:ncat D{j} = zeros (nobs, npar); if (interact) D{j}(:,(j-1)*(p+1)+(1:p+1)) = [ones(nobs, 1), X]; else D{j}(:,j) = 1; D{j}(:,ncat+1:end) = X; endif endfor ## Initial thresholds from the cumulative category frequencies, slopes at 0 tot = sum (W .* C, 1); cumfreq = cumsum (tot(1:ncat)) / sum (tot); cumfreq = min (max (cumfreq, 1e-3), 1 - 1e-3); theta = mnrfit_flink_ (link, cumfreq)(:); if (interact) psi = reshape ([theta'; zeros(p, ncat)], [], 1); else psi = [theta; zeros(p, 1)]; endif [ll, g, I] = mnrfit_ordinal_score_ (psi, D, C, W, F, f); converged = false; for iter = 1:maxiter if (rcond (I) < eps || ! all (isfinite (g))) break; endif step = I \ g; if (! all (isfinite (step))) break; endif ## Halve a step that leaves a probability outside (0, 1) or lowers the ## likelihood for halving = 1:30 [ll_new, g_new, I_new] = mnrfit_ordinal_score_ (psi + step, D, C, ... W, F, f); if (ll_new >= ll - 1e-10 * abs (ll)) break; endif step /= 2; endfor if (ll_new < ll - 1e-10 * abs (ll)) break; endif old = psi; psi = old + step; ll = ll_new; g = g_new; I = I_new; if (all (abs (step) <= tol * max (sqrt (eps), abs (old)))) converged = true; break; endif endfor if (! converged) warning ("mnrfit: iteration limit reached; results may be unreliable."); endif if (interact) B = reshape (psi, p + 1, ncat); else B = psi; endif covb = inv (I); se = reshape (sqrt (diag (covb)), size (B)); tstat = B ./ se; stats = mnrfit_stats_ (B, nobs * ncat - npar, covb, se, tstat); endfunction ## Log-likelihood, score and expected information of a cumulative link model ## at the parameters PSI. D{j} is the derivative of the j-th cumulative linear ## predictor by the parameters. A probability outside (0, 1) gives a ## log-likelihood of -Inf. function [ll, g, I] = mnrfit_ordinal_score_ (psi, D, C, W, F, f) [nobs, k] = size (C); ncat = k - 1; npar = numel (psi); eta = zeros (nobs, ncat); for j = 1:ncat eta(:,j) = D{j} * psi; endfor gamma = F (eta); ## cumulative probabilities P = diff ([zeros(nobs, 1), gamma, ones(nobs, 1)], 1, 2); g = zeros (npar, 1); I = zeros (npar); if (! all (P(:) > 0)) ll = -Inf; return; endif wC = W .* C; ll = sum (wC(wC > 0) .* log (P(wC > 0))); wn = W .* sum (C, 2); dens = f (eta); Aprev = zeros (nobs, npar); for c = 1:k if (c < k) A = dens(:,c) .* D{c}; else A = zeros (nobs, npar); endif dP = A - Aprev; ## derivative of category c g += dP' * (wC(:,c) ./ P(:,c)); I += dP' * (dP .* (wn ./ P(:,c))); Aprev = A; endfor endfunction ## The STATS structure of a fit, before its residuals and dispersion function stats = mnrfit_stats_ (B, dfe, covb, se, tstat) se_v = se(:); stats = struct ('beta', B, ... 'dfe', dfe, ... 's', 1, ... 'sfit', 1, ... 'estdisp', false, ... 'coeffcorr', covb ./ (se_v * se_v'), ... 'covb', covb, ... 'se', se, ... 't', tstat, ... 'p', 2 * normcdf (- abs (tstat)), ... 'resid', [], ... 'residp', [], ... 'residd', []); endfunction ## Softmax over the K-1 linear predictors and the reference category (fixed at ## 0). P is nobs-by-(K-1) for categories 1..K-1; Pall appends the reference. function [P, Pall] = mnrfit_softmax_ (Z, beta, nobs) eta = Z * beta; ## nobs-by-(k-1) m = max ([eta, zeros(nobs, 1)], [], 2); ## row max including baseline 0 ee = exp (eta - m); base = exp (-m); ## reference category, unnormalised den = base + sum (ee, 2); P = ee ./ den; if (nargout > 1) Pall = [P, base ./ den]; endif endfunction ## Fitted category probabilities of the model B on the predictors X function pihat = mnrfit_pihat_ (B, X, model, interact, link) if (strcmp (model, 'nominal') && ! interact) ## The intercepts alone: every observation has the same probabilities e = exp ([B(:)', 0] - max ([B(:)', 0])); pihat = repmat (e / sum (e), rows (X), 1); else if (interact) onoff = 'on'; else onoff = 'off'; endif pihat = mnrval (B, X, 'model', model, 'link', link, 'interactions', onoff); endif endfunction ## Raw, Pearson, and deviance residuals. PIHAT is the nobs-by-K matrix of ## fitted category probabilities, C the counts of each category, and W ## multiplies them. resid and residp are taken on what MODEL fits: ## nobs-by-K over the categories of a nominal model, nobs-by-(K-1) over the ## cumulative probabilities of an ordinal one and over the stages of a ## hierarchical one, NaN at a stage no trials reach. residd is nobs-by-1, the ## contribution of each observation to the deviance. function [resid, residp, residd] = mnrfit_residuals_ (pihat, C, W, model) [nobs, k] = size (C); wC = W .* C; wn = W .* sum (C, 2); switch (model) case 'nominal' obs = wC; fit = pihat; m = repmat (wn, 1, k); case 'ordinal' ## The last cumulative probability is always 1, so it has no residual obs = cumsum (wC(:,1:k-1), 2); fit = cumsum (pihat(:,1:k-1), 2); m = repmat (wn, 1, k - 1); case 'hierarchical' ## Stage j is category j against the later ones, over the trials that ## reach it obs = wC(:,1:k-1); fit = pihat(:,1:k-1) ./ ... (1 - [zeros(nobs, 1), cumsum(pihat(:,1:k-2), 2)]); m = wn - [zeros(nobs, 1), cumsum(wC(:,1:k-2), 2)]; endswitch resid = obs - m .* fit; residp = resid ./ sqrt (m .* fit .* (1 - fit)); residp(isnan (residp)) = 0; ## 0/0 at a saturated probability -> 0 residp(m == 0) = NaN; ## no trials, no residual if (strcmp (model, 'hierarchical')) resid(m == 0) = NaN; endif d = wC .* log (wC ./ max (wn .* pihat, realmin)); d(wC == 0) = 0; residd = 2 * sum (d, 2); endfunction ## The fit of a response with a single category, which leaves nothing to fit: ## no coefficients, a deviance of 0, and residuals of 0, as MATLAB returns it. ## NCOL is the number of columns of the raw and Pearson residuals: one for the ## single category, or none for the stages of a hierarchical model. function stats = mnrfit_empty_ (X, estdisp, ncol) nobs = rows (X); B = zeros (columns (X) + 1, 0); if (estdisp) s = NaN; else s = 1; endif stats = struct ('beta', B, ... 'dfe', 0, ... 's', s, ... 'sfit', NaN, ... 'estdisp', estdisp, ... 'coeffcorr', [], ... 'covb', [], ... 'se', B, ... 't', B, ... 'p', B, ... 'resid', zeros (nobs, ncol), ... 'residp', zeros (nobs, ncol), ... 'residd', zeros (nobs, 1)); endfunction ## Inverse link (cumulative probability) of the linear predictor and its ## derivative, matching the links used by mnrval. function [F, f] = mnrfit_links_ (link) switch (link) case 'logit' F = @(e) 1 ./ (1 + exp (-e)); f = @(e) 1 ./ (1 + exp (-e)) .* (1 - 1 ./ (1 + exp (-e))); case 'probit' F = @(e) normcdf (e); f = @(e) normpdf (e); case 'comploglog' F = @(e) 1 - exp (-exp (e)); f = @(e) exp (e - exp (e)); case 'loglog' F = @(e) exp (-exp (e)); f = @(e) - exp (e - exp (e)); endswitch endfunction ## Forward link, used only to seed the initial thresholds. function e = mnrfit_flink_ (link, g) switch (link) case 'logit' e = log (g ./ (1 - g)); case 'probit' e = norminv (g); case 'comploglog' e = log (- log (1 - g)); case 'loglog' e = log (- log (g)); endswitch endfunction ## Test nominal multinomial logit fitting %!test # nominal MLE reproduces the observed category totals %! X = [-2; -1; 0; 1; 2; -2; -1; 0; 1; 2; -1.5; 1.5]; %! Y = [1; 2; 3; 1; 2; 3; 1; 2; 3; 1; 2; 3]; %! [B, dev, stats] = mnrfit (X, Y, 'model', 'nominal'); %! assert_equal (size (B), [2, 2]); %! P = mnrval (B, X); %! assert_equal (sum (P, 2), ones (12, 1), 1e-10); %! assert_equal (sum (P, 1), [sum(Y == 1), sum(Y == 2), sum(Y == 3)], 1e-6); %!test # binary nominal agrees with the ordinal cumulative-logit fit %! X = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]; %! Y = [1; 1; 1; 1; 2; 1; 2; 2; 2; 2]; %! assert_equal (mnrfit (X, Y, 'model', 'nominal'), ... %! mnrfit (X, Y, 'model', 'ordinal'), 1e-5); ## Test hierarchical (sequential) fitting %!test # hierarchical fit yields valid probabilities through mnrval %! X = [-2; -1; 0; 1; 2; -2; -1; 0; 1; 2; -1.5; 1.5]; %! Y = [1; 2; 3; 1; 2; 3; 1; 2; 3; 1; 2; 3]; %! [B, dev, stats] = mnrfit (X, Y, 'model', 'hierarchical'); %! assert_equal (size (B), [2, 2]); %! P = mnrval (B, X, 'model', 'hierarchical'); %! assert_equal (sum (P, 2), ones (12, 1), 1e-10); %! assert_equal (all (P(:) >= 0 & P(:) <= 1), true); %!test # first hierarchical stage is the binary logit of category 1 vs. rest %! X = [-2; -1; 0; 1; 2; -2; -1; 0; 1; 2; -1.5; 1.5]; %! Y = [1; 2; 3; 1; 2; 3; 1; 2; 3; 1; 2; 3]; %! Bh = mnrfit (X, Y, 'model', 'hierarchical'); %! Bb = mnrfit (X, 2 - double (Y == 1), 'model', 'nominal'); %! assert_equal (Bh(:,1), Bb, 1e-8); ## Test link functions for ordinal and hierarchical models %!test # non-logit ordinal fits round-trip through mnrval with valid probs %! X = [-2; -1; 0; 1; 2; -1.5; 0.5; 1.2; -0.7; 0.3; -0.4; 0.8]; %! Y = [1; 1; 2; 2; 3; 1; 2; 3; 3; 2; 1; 3]; %! for lk = {'probit', 'comploglog', 'loglog'} %! B = mnrfit (X, Y, 'model', 'ordinal', 'link', lk{1}); %! P = mnrval (B, X, 'model', 'ordinal', 'link', lk{1}); %! assert_equal (sum (P, 2), ones (12, 1), 1e-9); %! assert_equal (all (P(:) >= -1e-12 & P(:) <= 1 + 1e-12), true); %! endfor %!test # non-logit hierarchical fit runs and round-trips through mnrval %! X = [-2; -1; 0; 1; 2; -1.5; 0.5; 1.2; -0.7; 0.3; -0.4; 0.8]; %! Y = [1; 1; 2; 2; 3; 1; 2; 3; 3; 2; 1; 3]; %! B = mnrfit (X, Y, 'model', 'hierarchical', 'link', 'probit'); %! P = mnrval (B, X, 'model', 'hierarchical', 'link', 'probit'); %! assert_equal (sum (P, 2), ones (12, 1), 1e-9); ## Test residual outputs %!test # residuals have the right shape and are internally consistent %! X = [-2; -1; 0; 1; 2; -2; -1; 0; 1; 2; -1.5; 1.5]; %! Y = [1; 2; 3; 1; 2; 3; 1; 2; 3; 1; 2; 3]; %! [B, dev, stats] = mnrfit (X, Y, 'model', 'nominal'); %! assert_equal (size (stats.resid), [12, 3]); %! assert_equal (size (stats.residp), [12, 3]); %! assert_equal (size (stats.residd), [12, 1]); %! assert_equal (sum (stats.resid, 2), zeros (12, 1), 1e-10); %! assert_equal (sum (stats.residd), dev, 1e-8); %!test # dispersion estimate sfit = sqrt (Pearson X2 / dfe) matches MATLAB %! X = [-2; -1; 0; 1; 2; -2; -1; 0; 1; 2; -1.5; 1.5]; %! Y = [1; 2; 3; 1; 2; 3; 1; 2; 3; 1; 2; 3]; %! [~, ~, sn] = mnrfit (X, Y, 'model', 'nominal'); %! assert_equal (sn.sfit, 1.0954, 1e-3); %! [~, ~, so] = mnrfit (X, Y, 'model', 'ordinal'); %! assert_equal (so.sfit, 1.0691, 1e-3); ## Test the EstDisp option %!test # EstDisp on scales se/covb by the dispersion and uses t-based p-values %! X = [-2; -1; 0; 1; 2; -2; -1; 0; 1; 2; -1.5; 1.5]; %! Y = [1; 2; 3; 1; 2; 3; 1; 2; 3; 1; 2; 3]; %! [~, ~, s0] = mnrfit (X, Y, 'model', 'nominal'); %! [~, ~, s1] = mnrfit (X, Y, 'model', 'nominal', 'estdisp', 'on'); %! assert_equal (s0.s, 1); %! assert_equal (s1.s, s1.sfit); %! assert_equal (s1.se, s0.se * s1.sfit, 1e-12); %! assert_equal (s1.covb, s0.covb * s1.sfit ^ 2, 1e-10); %! assert_equal (s1.p, 2 * tcdf (- abs (s1.t), s1.dfe), 1e-12); ## Test text and categorical responses against MATLAB R2026b. Text responses ## number their categories by first appearance and categorical ones by their ## categories, the last category being the reference; a missing label drops ## its observation, and a category without observations is ignored. %!shared X, sp, B1, Brev, Bmiss %! load fisheriris %! X = meas(:,1); %! sp = species; %! B1 = [38.7590, 12.6771; -6.8464, -2.0307]; %! Brev = [-38.7590, -26.0819; 6.8464, 4.8157]; %! Bmiss = [38.4986, 12.6788; -6.8013, -2.0310]; %!test %! assert_equal (mnrfit (X, sp), B1, 1e-4); %! assert_equal (mnrfit (flipud (X), flipud (sp)), Brev, 1e-4); %!test %! assert_equal (mnrfit (X, string (sp)), B1, 1e-4); %! assert_equal (mnrfit (flipud (X), flipud (string (sp))), Brev, 1e-4); %!test %! assert_equal (mnrfit (X, char (sp)), B1, 1e-4); %! ## labels of different lengths, padded with blanks in the char matrix %! y = {'a'; 'bcd'; 'ef'}([2 * ones(50,1); ones(50,1); 3 * ones(50,1)]); %! assert_equal (mnrfit (X, char (y)), mnrfit (X, y), 1e-12); %!test %! assert_equal (mnrfit (X, categorical (sp)), B1, 1e-4); %! c = categorical (sp, {'virginica', 'setosa', 'versicolor'}); %! assert_equal (mnrfit (X, c), [-12.6771, 26.0819; 2.0307, -4.8157], 1e-4); %! assert_equal (mnrfit (X, addcats (categorical (sp), 'extra')), B1, 1e-4); %!test %! c = categorical (sp); %! c(1) = missing; %! [B, dev] = mnrfit (X, c); %! assert_equal (B, Bmiss, 1e-4); %! assert_equal (dev, 181.6322, 1e-4); %!test %! s = string (sp); %! s(1) = missing; %! [B, dev] = mnrfit (X, s); %! assert_equal (B, Bmiss, 1e-4); %! assert_equal (dev, 181.6322, 1e-4); %!test %! c = categorical (sp, {'setosa', 'versicolor', 'virginica'}, 'Ordinal', true); %! B = mnrfit (X, c, 'model', 'ordinal'); %! assert_equal (B, [18.5691; 21.6898; -3.4524], 1e-4); %!test %! c = categorical (sp, {'extra', 'setosa', 'versicolor', 'virginica'}); %! assert_equal (mnrfit (X, c), B1, 1e-4); %!test %! assert_equal (mnrfit (X, sp'), mnrfit (X, sp)); %! assert_equal (mnrfit (X, string (sp)'), mnrfit (X, sp)); %! assert_equal (mnrfit (X, categorical (sp)'), mnrfit (X, sp)); ## Test the residuals of observations left out against MATLAB R2024a %!test %! c = categorical (sp); %! c(1) = missing; %! [~, ~, s] = mnrfit (X, c); %! assert_equal (size (s.resid), [150, 3]); %! assert_equal (s.resid(1:2,:), [NaN, NaN, NaN; 0.084568478433509675, ... %! -0.079375981712630134, -0.0051924967208795622], -1e-10); %! assert_equal (s.residp(1:2,:), [NaN, NaN, NaN; 0.3039424270334104, ... %! -0.29363201424385138, -0.072246795596986726], -1e-10); %! assert_equal (s.residd(1:2), [NaN; 0.17671943321891109], -1e-10); %!test %! Xn = X; %! Xn(5) = NaN; %! [~, ~, s] = mnrfit (Xn, grp2idx (sp)); %! assert_equal (size (s.resid), [150, 3]); %! assert_equal (s.resid(4:6,:), [0.020809847448850061, ... %! -0.020095179210191657, -0.00071466823865832662; ... %! NaN, NaN, NaN; 0.52496547239191615, ... %! -0.44466304411132351, -0.080302428280592628], -1e-10); ## Test ordinal and hierarchical residuals against MATLAB R2024a %!test %! [~, ~, s] = mnrfit ((1:9)', [1 1 2 1 2 3 2 3 3]', 'model', 'ordinal'); %! assert_equal (s.resid(3:6,:), [-0.640017262401357, 0.0119172052798397; ... %! 0.662144725149099, 0.0403306249191799; -0.12773091630328, ... %! 0.12773091630328; -0.04033062491918, -0.662144725149098], ... %! 1e-7); %!test %! [~, ~, s] = mnrfit ((1:9)', [1 1 2 1 2 3 2 3 3]', 'model', 'ordinal'); %! assert_equal (s.residp(3:6,:), [-1.33338328387092, 0.109822301711886; ... %! 1.39994543884798, 0.205001320144029; -0.382668552795833, ... %! 0.382668552795833; -0.205001320144029, -1.39994543884798], ... %! 1e-7); %!test %! [~, ~, s] = mnrfit ((1:9)', [1 1 2 1 2 3 2 3 3]', 'model', 'hierarchical'); %! assert_equal (s.resid(1:5,:), [0.041296779926102, NaN; ... %! 0.132552307459812, NaN; -0.648478668920583, ... %! 0.0180292890394411; 0.657879876493689, NaN; ... %! -0.127852690165849, 0.156132139470677], 1e-10); %!test %! [~, ~, s] = mnrfit ((1:9)', [1 1 2 1 2 3 2 3 3]', 'model', 'hierarchical'); %! assert_equal (s.residp(1:5,:), [0.207546780834513, NaN; ... %! 0.390905695445191, NaN; -1.35822589269996, ... %! 0.135500230327833; 1.38670471123006, NaN; ... %! -0.382877647171603, 0.430139109910011], 1e-10); %!test %! [~, dev, s] = mnrfit ((1:9)', [1 1 2 1 2 3 2 3 3]', 'model', 'hierarchical'); %! assert_equal (sum (s.residd), dev, 1e-10); %!test %! [~, ~, s] = mnrfit ((1:9)', [1 1 2 1 2 2 1 2 2]', 'model', 'ordinal'); %! assert_equal (size (s.residp), [9, 1]); %!test %! [~, ~, s] = mnrfit ((1:9)', [1 1 2 1 2 2 1 2 2]', 'model', 'hierarchical'); %! assert_equal (size (s.residp), [9, 1]); %!test %! [~, ~, s] = mnrfit ([1 NaN 3:9]', [1 1 2 1 2 3 2 3 3]', ... %! 'model', 'hierarchical'); %! assert_equal (s.resid(1:3,:), [0.0823310009353325, NaN; NaN, NaN; ... %! -0.566038550416822, 0.0180292890394411], 1e-10); ## Test an indicator matrix response with a NaN row against MATLAB R2024a %!test %! Yi = double ([1 1 2 1 2 3 2 3 3]' == 1:3); %! Bk = mnrfit ([1:2, 4:9]', Yi([1:2, 4:9],:)); %! Yi(3,:) = NaN; %! assert_equal (mnrfit ((1:9)', Yi), Bk); %!test %! Yi = double ([1 1 2 1 2 3 2 3 3]' == 1:3); %! Bk = mnrfit ([1:2, 4:9]', Yi([1:2, 4:9],:)); %! Yi(3,1) = NaN; %! assert_equal (mnrfit ((1:9)', Yi), Bk); %!test %! Yi = double ([1 1 2 1 2 3 2 3 3]' == 1:3); %! Yi(3,:) = NaN; %! [~, ~, s] = mnrfit ((1:9)', Yi); %! assert_equal (s.dfe, 12); %!test %! Yi = double ([1 1 2 1 2 3 2 3 3]' == 1:3); %! Yi(3,:) = NaN; %! [~, ~, s] = mnrfit ((1:9)', Yi); %! assert_equal (s.resid(3,:), [NaN, NaN, NaN]); ## Test a response with a single category against MATLAB R2024a %!test %! [B, dev, s] = mnrfit (X(1:50), sp(1:50)); %! assert_equal (B, zeros (2, 0)); %! assert_equal (dev, 0); %! assert_equal ([s.dfe, s.s, s.sfit], [0, 1, NaN]); %! assert_equal (s.resid, zeros (50, 1)); %! assert_equal (s.residd, zeros (50, 1)); %!test %! [B, dev] = mnrfit (X(1:50), sp(1:50), 'model', 'hierarchical'); %! assert_equal (B, zeros (2, 0)); %! assert_equal (dev, 0); %!test %! [~, ~, s] = mnrfit (X(1:50), sp(1:50), 'model', 'hierarchical'); %! assert_equal (s.resid, zeros (50, 0)); %!test %! [~, ~, s] = mnrfit (X(1:50), sp(1:50), 'model', 'hierarchical'); %! assert_equal (s.residd, zeros (50, 1)); %!test %! [~, ~, s] = mnrfit (X(1:50), sp(1:50), 'estdisp', 'on'); %! assert_equal ([s.s, s.sfit], [NaN, NaN]); %!test %! y = sp(1:50); %! y{2} = ''; %! [~, ~, s] = mnrfit (X(1:50), y); %! assert_equal (s.resid(1:3), [0; NaN; 0]); ## Test input validation %!error mnrfit (ones (50,1)) %!error ... %! mnrfit ({1 ;2 ;3 ;4 ;5}, ones (5,1)) %!error ... %! mnrfit (ones (50, 4, 2), ones (50, 1)) %!error ... %! mnrfit (ones (50, 4), ones (50, 1, 3)) %!error ... %! mnrfit (ones (50, 4), ones (45,1)) %!error ... %! mnrfit (ones (5, 4), {1 ;2 ;3 ;4 ;5}) %!error ... %! mnrfit (ones (5, 4), ones (5, 1), 'model') %!error ... %! mnrfit (ones (5, 4), {'q','q';'w','w';'q','q';'w','w';'q','q'}) %!error ... %! mnrfit (ones (5, 4), categorical ([1, 2; 1, 2; 1, 2; 1, 2; 1, 2])) %!error ... %! mnrfit (ones (5, 4), string ({'q','q';'w','w';'q','q';'w','w';'q','q'})) %!error ... %! mnrfit ((1:3)', [1, 2.5; 1, 2; 2, 1]) %!error ... %! mnrfit ((1:3)', [1, -2; 1, 2; 2, 1]) %!error ... %! mnrfit (ones (5, 4), [1; -1; 1; 2; 1]) %!error ... %! mnrfit (ones (5, 4), [1; 2; 3; 2; 1], 'model', 'whatever') %!error ... %! mnrfit (ones (5, 4), [1; 2; 1; 2; 1], 'link', 'cauchit') %!error ... %! mnrfit (ones (5, 4), [1; 2; 1; 2; 1], 'model', 'nominal', 'link', 'probit') %!error ... %! mnrfit (ones (5, 4), [1; 2; 1; 2; 1], 'estdisp', 'maybe') %!error ... %! mnrfit ((1:5)', {'a'; 'a'; 'a'; 'a'; 'a'}, 'model', 'ordinal') %!error ... %! mnrfit ((1:5)', [1, 2, 1, 2, 1]) ## Edge cases with empty arrays %!error ... %! mnrfit ([], []) %!error ... %! mnrfit (zeros (0, 3), zeros (0, 1)) ## Test count matrices, weights and interactions against MATLAB R2024a %!shared X6, Yc, x, y, wy %! X6 = (1:6)'; %! Yc = [3, 1, 0; 2, 2, 1; 1, 2, 2; 0, 2, 3; 1, 0, 4; 0, 1, 1]; %! x = [0.2, 1.1, 1.9, 3.2, 3.8, 5.1, 5.9, 7.2, 8.1, 8.8, 1.5, 4.4, 6.6, ... %! 2.7, 9.3]'; %! y = [1, 1, 2, 1, 2, 2, 3, 2, 3, 3, 2, 3, 2, 1, 3]'; %! wy = [1, 2, 1, 1, 3, 1, 2, 1, 1, 2, 1, 1, 3, 1, 2]'; %!test %! B = mnrfit (X6, Yc); %! assert_equal (B, [3.11521381121846, 1.72595553252332; ... %! -1.14291919345809, -0.557616323768001], -1e-10); %!test %! [~, dev, s] = mnrfit (X6, Yc); %! assert_equal ([dev, s.dfe], [7.29816119718767, 8], 1e-10); %!test %! [~, ~, s] = mnrfit (X6, Yc); %! assert_equal (s.resid(1:2,:), [0.479032510699473, -0.12827209523121, ... %! -0.350760415468263; -0.232219981795573, ... %! 0.206186037368912, 0.0260339444266604], 1e-10); %!test %! [~, ~, s] = mnrfit (X6, Yc); %! assert_equal (s.residp(1:2,:), [0.496160860213629, ... %! -0.142522560672393, -0.620060541159726; ... %! -0.208905708865958, 0.192247718709891, ... %! 0.0293977673685418], 1e-10); %!test %! [~, ~, s] = mnrfit (X6, Yc); %! assert_equal (s.residd, [0.802442532667939; 0.0485724721972971; ... %! 0.109354412004761; 1.36850192992456; 3.70158681524588; ... %! 1.26770303514723], 1e-10); %!test %! B = mnrfit (X6, Yc, 'model', 'ordinal'); %! assert_equal (B, [1.29939112375683; 3.04351150827255; ... %! -0.797767453558435], -1e-6); %!test %! [~, dev, s] = mnrfit (X6, Yc, 'model', 'ordinal'); %! assert_equal ([dev, s.dfe], [7.34011610110504, 9], 1e-10); %!test %! [~, ~, s] = mnrfit (X6, Yc, 'model', 'ordinal'); %! assert_equal (s.resid(1:2,:), [0.508636704845893, 0.382868827000934; ... %! -0.132502184597763, -0.0484341779702326], 1e-6); %!test %! B = mnrfit (X6, Yc, 'model', 'hierarchical'); %! assert_equal (B, [1.4485769644502, 1.98445711156869; ... %! -0.847495943829511, -0.629407372230075], -1e-10); %!test %! [~, dev, s] = mnrfit (X6, Yc, 'model', 'hierarchical'); %! assert_equal ([dev, s.dfe], [7.01155523089269, 8], 1e-10); %!test %! [~, ~, s] = mnrfit (X6, Yc, 'model', 'hierarchical'); %! assert_equal (s.resid(1:2,:), [0.416385649011944, 0.205046027351086; ... %! -0.193530522479006, -0.0215448777590356], 1e-10); %!test %! Xe = repelem (X6, sum (Yc, 2)); %! ye = repelem (repmat ((1:3)', 6, 1), reshape (Yc', [], 1)); %! assert_equal (mnrfit (X6, Yc), mnrfit (Xe, ye), 1e-10); %!test %! Yz = Yc; %! Yz(2,:) = 0; %! [~, dev, s] = mnrfit (X6, Yz); %! assert_equal ([dev, s.dfe], [7.22221543051311, 8], 1e-10); %!test %! Yz = Yc; %! Yz(2,:) = 0; %! [~, ~, s] = mnrfit (X6, Yz); %! assert_equal (s.residp(2,:), [NaN, NaN, NaN]); %!test %! Y2 = [1, 0, 0; 0, 1, 0; 1, 1, 0; 0, 0, 1; 0, 1, 0; 0, 0, 1]; %! assert_equal (mnrfit (X6, Y2), [7.55936380401886, 5.12548052211949; ... %! -2.20811162886661, -1.12377716339686], -1e-10); %!test %! Yu = [Yc(:,1), zeros(6,1), Yc(:,2:3)]; %! assert_equal (mnrfit (X6, Yu), mnrfit (X6, Yc)); %!test %! Yr = Yc; %! Yr(1,:) = [2, 0, 0]; %! [~, ~, s] = mnrfit (X6, Yr, 'model', 'hierarchical'); %! assert_equal (s.dfe, 7); %!test %! Yr = Yc; %! Yr(1,:) = [2, 0, 0]; %! [~, ~, s] = mnrfit (X6, Yr, 'model', 'hierarchical'); %! assert_equal (s.residp(1,:), [0.914729298681304, NaN], 1e-10); %!test %! B = mnrfit (X6, Yc, 'weights', [1, 2, 1, 3, 1, 2]'); %! assert_equal (B, [3.26764731943353, 1.37782734242591; ... %! -1.31700484949595, -0.429541892229879], -1e-10); %!test %! [~, ~, s] = mnrfit (X6, Yc, 'weights', [1, 2, 1, 3, 1, 2]'); %! assert_equal (s.residp(1,:), [0.369677091482785, 0.0317726223417427, ... %! -0.645010798142362], 1e-10); %!test %! [~, dev] = mnrfit (X6, Yc, 'weights', [1, 2, 1, 3, 1, 2]'); %! assert_equal (dev, 10.2534960048411, 1e-10); %!test %! B = mnrfit (X6, Yc, 'model', 'ordinal', 'interactions', 'on'); %! assert_equal (B, [1.29967633179764, 3.0431268128545; ... %! -0.797892338817538, -0.797669756913824], -1e-6); %!test %! B = mnrfit (X6, Yc, 'model', 'hierarchical', 'interactions', 'off'); %! assert_equal (B, [1.18480730583952; 2.39950317498638; ... %! -0.745002595688337], -1e-10); %!test %! [~, ~, s] = mnrfit (X6, Yc, 'model', 'hierarchical', 'interactions', 'off'); %! assert_equal (s.dfe, 9); %!test %! S = warning ('off', 'all'); %! B = mnrfit (x, y, 'interactions', 'off'); %! warning (S); %! assert_equal (B, [log(4/5); log(6/5)], 1e-10); %!warning ... %! mnrfit (x, y, 'interactions', 'off'); %!test %! B = mnrfit (x, y, 'model', 'ordinal', 'link', 'probit'); %! assert_equal (B, [1.07863009080549; 3.04642129916916; ... %! -0.485126029810669], -1e-6); %!test %! B = mnrfit (x, y, 'model', 'hierarchical', 'link', 'comploglog'); %! assert_equal (B, [1.11606258630227, 2.15097058822628; ... %! -0.720736258358282, -0.42477426118437], -1e-10); %!test %! B = mnrfit (x, y, 'model', 'ordinal', 'interactions', 'on', ... %! 'link', 'loglog'); %! assert_equal (B, [-1.64348124603758, -4.37698558082175; ... %! 0.536188854191686, 0.608581711015173], -1e-6); %!test %! B = mnrfit (x, y, 'weights', wy); %! assert_equal (B, [7.59751957764148, 5.03769507149626; ... %! -1.88801982284467, -0.77411323264876], -1e-10); %!test %! B = mnrfit (x, y, 'model', 'ordinal', 'weights', wy); %! assert_equal (B, [2.05249214200963; 6.21644116058936; ... %! -0.945758358953641], -1e-6); %!test %! B = mnrfit (x, y, 'model', 'hierarchical', 'weights', wy); %! assert_equal (B, [2.64758153523882, 5.0288032858106; ... %! -1.17838215436985, -0.771428066114368], -1e-10); %!test %! B = mnrfit (X6, Yc, 'tolerance', 1e-2); %! assert_equal (B, mnrfit (X6, Yc), 1e-4); %!warning ... %! mnrfit (X6, Yc, 'iterationlimit', 2); %!error ... %! mnrfit (X6, Yc, 'interactions', true) %!error ... %! mnrfit (X6, Yc, 'weights', ones (5, 1)) %!error ... %! mnrfit (X6, Yc, 'weights', [1; 1; -1; 1; 1; 1]) %!error ... %! mnrfit (X6, Yc, 'iterationlimit', 2.5) %!error ... %! mnrfit (X6, Yc, 'tolerance', 0) %!warning ... %! mnrfit (X6, Yc, 'display', 'on'); statistics-release-2.1.2/inst/Regression/mnrval.m000066400000000000000000000406551527311104300221050ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{pihat} =} mnrval (@var{B}, @var{X}) ## @deftypefnx {statistics} {[@var{pihat}, @var{dlo}, @var{dhi}] =} mnrval (@var{B}, @var{X}, @var{stats}) ## @deftypefnx {statistics} {@var{yhat} =} mnrval (@var{B}, @var{X}, @var{ssize}) ## @deftypefnx {statistics} {[@var{yhat}, @var{dlo}, @var{dhi}] =} mnrval (@var{B}, @var{X}, @var{ssize}, @var{stats}) ## @deftypefnx {statistics} {[@dots{}] =} mnrval (@dots{}, @var{name}, @var{value}) ## ## Predict values for a multinomial logistic regression model. ## ## @code{@var{pihat} = mnrval (@var{B}, @var{X})} returns the predicted category ## probabilities @var{pihat} of a multinomial logistic regression with ## coefficients @var{B}, evaluated at the predictor values in @var{X}. @var{X} ## is an @math{N*P} numeric matrix of @math{N} observations on @math{P} ## predictors. @var{pihat} is an @math{N*K} matrix, where @math{K} is the ## number of response categories and each row sums to one. @var{B} is the ## coefficient matrix returned by @code{mnrfit} (see below for its shape under ## each model). ## ## @code{mnrval} is the prediction companion of @code{mnrfit}. Unlike the ## current @code{mnrfit}, which only fits ordinal and two-category nominal ## models, @code{mnrval} evaluates all three model types, so a coefficient ## matrix @var{B} obtained elsewhere (e.g.@: MATLAB) can be used for prediction. ## ## @code{@var{yhat} = mnrval (@var{B}, @var{X}, @var{ssize})} returns predicted ## category counts instead of probabilities, for the sample sizes in @var{ssize} ## (a scalar or an @math{N*1} vector). ## ## @code{[@var{pihat}, @var{dlo}, @var{dhi}] = mnrval (@var{B}, @var{X}, ## @var{stats})} also returns @math{95%} confidence bounds on the predictions. ## @var{stats} is the structure returned by @code{mnrfit}; its @qcode{'covb'} ## field (the coefficient covariance matrix) is required. The confidence ## interval for each prediction is @code{[@var{pihat} - @var{dlo}, @var{pihat} + ## @var{dhi}]}. The bounds are nonsimultaneous and apply to the fitted values, ## not to new observations. ## ## The following @qcode{Name-Value} pairs control the model: ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'model'} @tab The model type: @qcode{'nominal'} (default), ## @qcode{'ordinal'}, or @qcode{'hierarchical'}. ## ## @item @qcode{'interactions'} @tab @qcode{'on'} to include category-specific ## coefficients, or @qcode{'off'} for a common set of coefficients with ## category-specific intercepts only. Default is @qcode{'on'} for nominal and ## hierarchical models and @qcode{'off'} for ordinal models. With ## @qcode{'interactions','on'}, @var{B} is a @math{(P+1)*(K-1)} matrix. With ## @qcode{'interactions','off'}, @var{B} is a @math{(K-1+P)*1} vector holding ## the @math{K-1} intercepts followed by the @math{P} common slopes. ## ## @item @qcode{'link'} @tab The link function for ordinal and hierarchical ## models: @qcode{'logit'} (default), @qcode{'probit'}, @qcode{'comploglog'}, or ## @qcode{'loglog'}. Nominal models always use the multinomial logit link. ## ## @item @qcode{'type'} @tab The kind of probability returned: ## @qcode{'category'} (default, @math{N*K} category probabilities), ## @qcode{'cumulative'} (@math{N*(K-1)} cumulative probabilities of the first ## @math{K-1} categories), or @qcode{'conditional'} (@math{N*(K-1)} conditional ## probabilities of each category given membership in that or a later category). ## ## @item @qcode{'confidence'} @tab The confidence level for @var{dlo} and ## @var{dhi}, a scalar in the range @math{(0,1)}. Default is @math{0.95}. ## @end multitable ## ## @seealso{mnrfit, glmval, logistic_regression} ## @end deftypefn function [pihat, dlo, dhi] = mnrval (B, X, varargin) if (nargin < 2) error ("mnrval: too few input arguments."); endif if (! isnumeric (B) || ! isreal (B)) error ("mnrval: B must be a real numeric matrix."); endif if (! isnumeric (X) || ! isreal (X) || ndims (X) > 2) error ("mnrval: X must be a real numeric 2D matrix."); endif ## Peel off the optional positional arguments (SSIZE and/or STATS), which ## precede any Name-Value pairs. SSIZE is numeric, STATS is a struct. ssize = []; stats = []; args = varargin; while (numel (args) > 0 && ! ischar (args{1})) a = args{1}; if (isstruct (a)) stats = a; elseif (isnumeric (a)) ssize = a; else error ("mnrval: invalid optional positional argument."); endif args(1) = []; endwhile ## Parse Name-Value pairs if (mod (numel (args), 2) != 0) error ("mnrval: optional arguments must be in Name-Value pairs."); endif model = 'nominal'; interactions = []; ## resolved to a default per model below link = 'logit'; type = 'category'; conf = 0.95; while (numel (args) > 0) name = args{1}; value = args{2}; switch (lower (name)) case 'model' model = value; case 'interactions' interactions = value; case 'link' link = value; case 'type' type = value; case 'confidence' conf = value; otherwise error ("mnrval: unknown parameter name '%s'.", name); endswitch args(1:2) = []; endwhile ## Validate and normalise the options model = lower (model); if (! any (strcmp (model, {'nominal', 'ordinal', 'hierarchical'}))) error ("mnrval: unrecognised 'model' value."); endif if (isempty (interactions)) if (strcmp (model, 'ordinal')) interactions = 'off'; else interactions = 'on'; endif endif interactions = lower (interactions); if (! any (strcmp (interactions, {'on', 'off'}))) error ("mnrval: 'interactions' must be 'on' or 'off'."); endif link = lower (link); if (! any (strcmp (link, {'logit', 'probit', 'comploglog', 'loglog'}))) error ("mnrval: unrecognised 'link' value."); endif if (strcmp (model, 'nominal') && ! strcmp (link, 'logit')) error ("mnrval: nominal models use the multinomial logit link only."); endif type = lower (type); if (! any (strcmp (type, {'category', 'cumulative', 'conditional'}))) error ("mnrval: unrecognised 'type' value."); endif if (! (isscalar (conf) && isreal (conf) && conf > 0 && conf < 1)) error ("mnrval: 'confidence' must be a scalar in the range (0,1)."); endif [n, P] = size (X); ## Determine K-1 and validate the shape of B against X and 'interactions' if (strcmp (interactions, 'on')) if (rows (B) != P + 1) error (strcat ("mnrval: with 'interactions','on', B must have one", ... " more row than the number of columns of X.")); endif Km1 = columns (B); else B = B(:); if (numel (B) <= P) error (strcat ("mnrval: with 'interactions','off', B must have more", ... " elements than the number of columns of X.")); endif Km1 = numel (B) - P; endif if (Km1 < 1) error ("mnrval: B implies fewer than two response categories."); endif ## Validate SSIZE and STATS if supplied if (! isempty (ssize)) if (! (isscalar (ssize) || (isvector (ssize) && numel (ssize) == n))) error ("mnrval: SSIZE must be a scalar or an N-element vector."); endif ssize = ssize(:); endif if (nargout > 1 && isempty (stats)) error ("mnrval: STATS is required to compute confidence bounds."); endif ## Point predictions pihat = mnr_predict (B, X, model, interactions, link, type, ssize, P); ## Confidence bounds by the delta method on the coefficient covariance if (nargout > 1) if (! isfield (stats, 'covb')) error ("mnrval: STATS must contain a 'covb' field."); endif covb = stats.covb; b0 = B(:); np = numel (b0); if (! isequal (size (covb), [np, np])) error ("mnrval: size of STATS.covb does not match the number of coefficients."); endif ncol = columns (pihat); ## Central-difference Jacobian of the returned quantity with respect to B J = zeros (n * ncol, np); h = 1e-6 * max (abs (b0), 1); for j = 1:np bp = b0; bp(j) += h(j); bm = b0; bm(j) -= h(j); fp = mnr_predict (reshape (bp, size (B)), X, model, interactions, ... link, type, ssize, P); fm = mnr_predict (reshape (bm, size (B)), X, model, interactions, ... link, type, ssize, P); J(:,j) = (fp(:) - fm(:)) / (2 * h(j)); endfor v = sum ((J * covb) .* J, 2); ## variance of each output element se = reshape (sqrt (max (v, 0)), n, ncol); z = norminv (1 - (1 - conf) / 2); dlo = z * se; dhi = z * se; endif endfunction ## Core prediction: returns the requested quantity (category/cumulative/ ## conditional probabilities, optionally scaled to counts by SSIZE). function out = mnr_predict (B, X, model, interactions, link, type, ssize, P) n = rows (X); ## Linear predictors ETA (n-by-(K-1)) if (strcmp (interactions, 'on')) Km1 = columns (B); eta = [ones(n, 1), X] * B; else Km1 = numel (B) - P; icept = B(1:Km1)(:).'; slope = B(Km1+1:end); if (P > 0) eta = X * slope + repmat (icept, n, 1); else eta = repmat (icept, n, 1); endif endif ## Inverse link (mean function) for ordinal and hierarchical models switch (link) case 'logit' G = 1 ./ (1 + exp (-eta)); case 'probit' G = normcdf (eta); case 'comploglog' G = 1 - exp (-exp (eta)); case 'loglog' G = exp (-exp (eta)); endswitch ## Category probabilities PCAT (n-by-K) switch (model) case 'nominal' ee = exp (eta); den = 1 + sum (ee, 2); pcat = [ee ./ den, 1 ./ den]; case 'ordinal' pcat = [G(:,1), diff(G, 1, 2), 1 - G(:,end)]; case 'hierarchical' ## G holds the conditional probabilities P(Y = j | Y >= j) surv = cumprod ([ones(n, 1), 1 - G], 2); ## surv(:,j) = P(Y >= j) pcat = [G .* surv(:,1:Km1), surv(:,end)]; endswitch ## Requested probability type switch (type) case 'category' out = pcat; case 'cumulative' c = cumsum (pcat, 2); out = c(:,1:end-1); case 'conditional' surv = fliplr (cumsum (fliplr (pcat), 2)); ## surv(:,j) = P(Y >= j) out = pcat(:,1:end-1) ./ surv(:,1:end-1); endswitch ## Scale to counts when sample sizes are supplied if (! isempty (ssize)) out = out .* ssize; endif endfunction %!demo %! ## Fit an ordinal model and predict the category probabilities %! X = [1; 2; 3; 4; 5; 6; 7; 8]; %! Y = [1; 1; 1; 2; 2; 2; 3; 3]; %! B = mnrfit (X, Y, 'model', 'ordinal'); %! pihat = mnrval (B, X, 'model', 'ordinal') ## Round-trip against logistic_regression: mnrval must reproduce the fitted ## probabilities of the ordinal model that mnrfit wraps. %!test %! X = [1.489381332449196, 1.1534152241851305; ... %! 1.8110085304863965, 0.9449666896938425; ... %! -0.04453299665130296, 0.34278203449678646; ... %! -0.36616019468850347, 1.130254275908322; ... %! 0.15339143291005095, -0.7921044310668951; ... %! -1.6031878794469698, -1.8343471035233376; ... %! -0.14349521143198166, -0.6762996896828459; ... %! -0.4403818557740143, -0.7921044310668951; ... %! -0.7372685001160434, -0.027793137932169563; ... %! -0.11875465773681024, 0.5512305689880763]; %! Y = [1;1;1;1;1;0;0;0;0;0]; %! B = mnrfit (X, Y + 1, 'model', 'ordinal'); %! [~, ~, ~, ~, ~, Pref] = logistic_regression (Y, X, false); %! assert_equal (mnrval (B, X, 'model', 'ordinal'), Pref, 1e-6); ## For a binary response the default nominal call equals the ordinal call %!test %! X = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]; %! Y = [1; 1; 1; 1; 1; 2; 2; 2; 2; 2]; %! B = mnrfit (X, Y, 'model', 'ordinal'); %! p_nom = mnrval (B, X); %! p_ord = mnrval (B, X, 'model', 'ordinal'); %! assert_equal (p_nom, p_ord, 1e-12); ## Every row of the category probabilities sums to one, for all three models %!test %! X = randn (20, 2); %! Bnom = [0.5, -0.2; 1.0, 0.3; -0.4, 0.1]; # (P+1)-by-(K-1), K = 3 %! assert_equal (sum (mnrval (Bnom, X), 2), ones (20, 1), 1e-12); %! assert_equal (sum (mnrval (Bnom, X, 'model', 'hierarchical'), 2), ... %! ones (20, 1), 1e-12); %! Bord = [-1; 1; 0.5; -0.3]; # (K-1+P)-by-1, K = 3, P = 2 %! assert_equal (sum (mnrval (Bord, X, 'model', 'ordinal'), 2), ... %! ones (20, 1), 1e-12); ## Probabilities lie in [0, 1] %!test %! X = randn (15, 2); %! Bord = [-1; 0.8; 0.5; -0.3]; %! p = mnrval (Bord, X, 'model', 'ordinal'); %! assert_equal (all (p(:) >= 0 & p(:) <= 1), true); ## Cumulative type equals the running sum of the category probabilities %!test %! X = randn (12, 2); %! Bord = [-0.5; 1.2; 0.4; -0.2]; %! pc = mnrval (Bord, X, 'model', 'ordinal', 'type', 'category'); %! cu = mnrval (Bord, X, 'model', 'ordinal', 'type', 'cumulative'); %! assert_equal (cu, cumsum (pc(:,1:end-1), 2), 1e-12); ## Predicted counts equal probabilities times the sample size %!test %! X = randn (10, 2); %! Bord = [-0.5; 1.0; 0.4; -0.2]; %! p = mnrval (Bord, X, 'model', 'ordinal'); %! y = mnrval (Bord, X, 100, 'model', 'ordinal'); %! assert_equal (y, p * 100, 1e-10); ## Confidence bounds have the same size as the prediction and are non-negative %!test %! X = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]; %! Y = [1; 1; 1; 1; 1; 2; 2; 2; 2; 2]; %! [B, ~, stats] = mnrfit (X, Y, 'model', 'ordinal'); %! [p, dlo, dhi] = mnrval (B, X, stats, 'model', 'ordinal'); %! assert_equal (size (dlo), size (p)); %! assert_equal (size (dhi), size (p)); %! assert_equal (all (dlo(:) >= 0) && all (dhi(:) >= 0), true); ## Every link produces category probabilities that sum to one %!test %! X = randn (10, 1); %! Bord = [-0.5; 0.7; 0.4]; %! for lnk = {'logit', 'probit', 'comploglog', 'loglog'} %! p = mnrval (Bord, X, 'model', 'ordinal', 'link', lnk{1}); %! assert_equal (sum (p, 2), ones (10, 1), 1e-10); %! endfor ## Increasing links with ordered intercepts keep probabilities in [0, 1] %!test %! X = randn (10, 1); %! Bord = [-0.5; 0.7; 0.4]; %! for lnk = {'logit', 'probit', 'comploglog'} %! p = mnrval (Bord, X, 'model', 'ordinal', 'link', lnk{1}); %! assert_equal (all (p(:) >= 0 & p(:) <= 1), true); %! endfor ## The loglog link is decreasing, so a valid ordinal model needs its ## intercepts in decreasing order %!test %! X = randn (10, 1); %! Bll = [1.0; -0.5; 0.4]; %! p = mnrval (Bll, X, 'model', 'ordinal', 'link', 'loglog'); %! assert_equal (sum (p, 2), ones (10, 1), 1e-10); %! assert_equal (all (p(:) >= 0 & p(:) <= 1), true); ## Test input validation %!error mnrval (1) %!error mnrval ({1}, ones (3, 1)) %!error ... %! mnrval ([1; 1], ones (3, 3, 3)) %!error ... %! mnrval ([1; 1], ones (3, 1), 'model') %!error ... %! mnrval ([1; 1], ones (3, 1), 'foo', 'bar') %!error ... %! mnrval ([1; 1], ones (3, 1), 'model', 'whatever') %!error ... %! mnrval ([1; 1], ones (3, 1), 'interactions', 'maybe') %!error ... %! mnrval ([1; 1], ones (3, 1), 'model', 'ordinal', 'link', 'foo') %!error ... %! mnrval ([1; 1], ones (3, 1), 'link', 'probit') %!error ... %! mnrval ([1; 1], ones (3, 1), 'type', 'foo') %!error ... %! mnrval ([1; 1], ones (3, 1), 'confidence', 2) %!error ... %! mnrval ([1; 1; 1], ones (3, 1)) %!error ... %! [p, dlo] = mnrval ([1; 1], ones (3, 1)); %!error ... %! [p, dlo] = mnrval ([1; 1], ones (3, 1), struct ('x', 1)); statistics-release-2.1.2/inst/Regression/monotone_smooth.m000066400000000000000000000146361527311104300240350ustar00rootroot00000000000000## Copyright (C) 2011 Nir Krakauer ## Copyright (C) 2011 Carnë Draug ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{yy} =} monotone_smooth (@var{x}, @var{y}, @var{h}) ## ## Produce a smooth monotone increasing approximation to a sampled functional ## dependence. ## ## A kernel method is used (an Epanechnikov smoothing kernel is applied to y(x); ## this is integrated to yield the monotone increasing form. See Reference 1 ## for details.) ## ## @subheading Arguments ## ## @itemize @bullet ## @item ## @var{x} is a vector of values of the independent variable. ## ## @item ## @var{y} is a vector of values of the dependent variable, of the same size as ## @var{x}. For best performance, it is recommended that the @var{y} already be ## fairly smooth, e.g. by applying a kernel smoothing to the original values if ## they are noisy. ## ## @item ## @var{h} is the kernel bandwidth to use. If @var{h} is not given, ## a "reasonable" value is computed. ## ## @end itemize ## ## @subheading Return values ## ## @itemize @bullet ## @item ## @var{yy} is the vector of smooth monotone increasing function values at ## @var{x}. ## ## @end itemize ## ## @subheading Examples ## ## @example ## @group ## x = 0:0.1:10; ## y = (x .^ 2) + 3 * randn(size(x)); # typically non-monotonic from the added ## noise ## ys = ([y(1) y(1:(end-1))] + y + [y(2:end) y(end)])/3; # crudely smoothed via ## moving average, but still typically non-monotonic ## yy = monotone_smooth(x, ys); # yy is monotone increasing in x ## plot(x, y, '+', x, ys, x, yy) ## @end group ## @end example ## ## @subheading References ## ## @enumerate ## @item ## Holger Dette, Natalie Neumeyer and Kay F. Pilz (2006), A simple nonparametric ## estimator of a strictly monotone regression function, @cite{Bernoulli}, ## 12:469-490 ## @item ## Regine Scheder (2007), R Package 'monoProc', Version 1.0-6, ## @url{http://cran.r-project.org/web/packages/monoProc/monoProc.pdf} (The ## implementation here is based on the monoProc function mono.1d) ## @end enumerate ## @end deftypefn function yy = monotone_smooth (x, y, h) if (nargin < 2 || nargin > 3) print_usage (); elseif (! isnumeric (x) || ! isvector (x)) error ("monotone_smooth: X must be a numeric vector."); elseif (! isnumeric (y) || ! isvector (y)) error ("monotone_smooth: Y must be a numeric vector."); elseif (numel (x) != numel (y)) error ("monotone_smooth: X and Y must have the same number of elements."); elseif (nargin == 3 && (! isscalar (h) || ! isnumeric (h))) error ("monotone_smooth: H (kernel bandwidth) must a numeric scalar."); endif n = numel (x); ## Set filter bandwidth at a reasonable default value, if not specified if (nargin != 3) s = std (x); h = s / (n ^ 0.2); endif x_min = min (x); x_max = max (x); y_min = min (y); y_max = max (y); ## Transform range of X to [0, 1] xl = (x - x_min) / (x_max - x_min); yy = ones (size (y)); ## Epanechnikov smoothing kernel (with finite support) ## K_epanech_kernel = @(z) (3/4) * ((1 - z).^2) .* (abs(z) < 1); K_epanech_int = @(z) mean (((abs (z) < 1)/2) - (3/4) * (z .* (abs (z) < 1) ... - (1/3) * (z.^3) .* (abs (z) < 1)) + (z < -1)); ## Integral of kernels up to t monotone_inverse = @(t) K_epanech_int((y - t) / h); ## Find the value of the monotone smooth function at each point in X niter_max = 150; # maxIter for estimating each value (adequate for most cases) for l = 1:n tmax = y_max; tmin = y_min; wmin = monotone_inverse(tmin); wmax = monotone_inverse(tmax); if (wmax == wmin) yy(l) = tmin; else wt = xl(l); iter_max_reached = 1; for i = 1:niter_max wt_scaled = (wt - wmin) / (wmax - wmin); tn = tmin + wt_scaled * (tmax - tmin) ; wn = monotone_inverse(tn); wn_scaled = (wn - wmin) / (wmax - wmin); ## if (abs(wt-wn) < 1E-4) || (tn < (y_min-0.1)) || (tn > (y_max+0.1)) ## criterion for break in the R code -- replaced by the following line ## to hopefully be less dependent on the scale of y if ((abs (wt_scaled-wn_scaled) < 1E-4) || (wt_scaled < -0.1) || (wt_scaled > 1.1)) iter_max_reached = 0; break endif if (wn > wt) tmax = tn; wmax = wn; else tmin = tn; wmin = wn; endif endfor if (iter_max_reached) msg = sprintf (strcat ("at x = %%g, maximum number of iterations", ... " %%d reached without convergence;", ... " approximation may not be optimal")); warning (msg, x(l), niter_max) endif yy(l) = tmin + (wt - wmin) * (tmax - tmin) / (wmax - wmin); endif endfor endfunction ## Test input validation %!error ... %! monotone_smooth (1) %!error ... %! monotone_smooth ('char', 1) %!error ... %! monotone_smooth ({1,2,3}, 1) %!error ... %! monotone_smooth (ones (20,3), 1) %!error ... %! monotone_smooth (1, 'char') %!error ... %! monotone_smooth (1, {1,2,3}) %!error ... %! monotone_smooth (1, ones (20,3)) %!error monotone_smooth (ones (10,1), ones (10,1), [1, 2]) %!error monotone_smooth (ones (10,1), ones (10,1), {2}) %!error monotone_smooth (ones (10,1), ones (10,1), 'char') statistics-release-2.1.2/inst/Regression/mvregress.m000066400000000000000000000401311527311104300226100ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{beta} =} mvregress (@var{X}, @var{Y}) ## @deftypefnx {statistics} {@var{beta} =} mvregress (@dots{}, @var{name}, @var{value}) ## @deftypefnx {statistics} {[@var{beta}, @var{Sigma}, @var{E}, @var{CovB}, @var{logL}] =} mvregress (@dots{}) ## ## Multivariate (multiple-response) linear regression by maximum likelihood. ## ## @code{mvregress (@var{X}, @var{Y})} fits the multivariate normal regression ## of the @var{n}-by-@var{d} response matrix @var{Y} on the design @var{X} and ## returns the coefficient estimates @var{beta}. ## ## @var{X} is either a numeric @var{n}-by-@var{p} matrix, in which case the same ## @var{p} predictors apply to every response and @var{beta} is returned as a ## @var{p}-by-@var{d} matrix; or a cell array of @var{n} design matrices, each ## @var{d}-by-@var{K}, in which case @var{beta} is a @var{K}-by-1 vector. ## ## Missing responses (@code{NaN} entries of @var{Y}) are handled according to ## the estimation algorithm. ## ## The following @var{name}/@var{value} pairs are accepted: ## ## @table @asis ## @item @qcode{"algorithm"} ## @qcode{"mvn"} (multivariate normal; observations with any missing response ## are discarded), @qcode{"ecm"} (expectation-conditional-maximization, using ## every observed response), or @qcode{"cwls"} (covariance-weighted least ## squares, with the weight given by @qcode{"covar0"}). The default is ## @qcode{"mvn"} when @var{Y} has no missing values and @qcode{"ecm"} otherwise. ## ## @item @qcode{"covar0"} ## The @var{d}-by-@var{d} covariance weight for @qcode{"cwls"} (default the ## identity), or the initial covariance for @qcode{"ecm"}. ## ## @item @qcode{"maxiter"} ## Maximum number of iterations (default 100). ## ## @item @qcode{"tolbeta"}, @qcode{"tolobj"} ## Convergence tolerances on the coefficients and the objective (defaults ## @code{1e-8} and @code{1e-8}). ## @end table ## ## The additional outputs are the estimated residual covariance @var{Sigma} ## (@var{d}-by-@var{d}), the residuals @var{E} (@var{n}-by-@var{d}), the ## covariance @var{CovB} of the coefficient estimates, and the log-likelihood ## @var{logL}. (With missing data and the @qcode{"ecm"} algorithm, @var{CovB} ## is the standard observed-information covariance and can differ from ## @sc{matlab}'s value at the @code{1e-3} level; all other outputs agree.) ## ## @seealso{mvregresslike, regress, fitlm} ## @end deftypefn function [beta, Sigma, E, CovB, logL] = mvregress (X, Y, varargin) if (nargin < 2) print_usage (); endif if (! (isnumeric (Y) && ismatrix (Y) && isreal (Y))) error ("mvregress: Y must be a real numeric matrix."); endif [n, d] = size (Y); if (isempty (Y)) error ("mvregress: Y cannot be an empty array."); endif ## --- normalise the design to per-observation d-by-K matrices --- is_numeric = ! iscell (X); if (is_numeric) if (rows (X) != n) error ("mvregress: X must have as many rows as Y."); endif p = columns (X); K = p * d; Xcell = cell (n, 1); for i = 1:n Xcell{i} = kron (eye (d), X(i, :)); endfor else if (numel (X) != n) error ("mvregress: X must have one design matrix per observation."); endif K = columns (X{1}); Xcell = X(:); endif ## --- options --- hasnan = any (isnan (Y(:))); alg = ""; covar0 = []; maxiter = 100; tolbeta = 1e-8; tolobj = 1e-8; if (mod (numel (varargin), 2) != 0) error ("mvregress: name/value arguments must come in pairs."); endif for i = 1:2:numel (varargin) switch (lower (char (varargin{i}))) case "algorithm" alg = lower (char (varargin{i+1})); case "covar0" covar0 = varargin{i+1}; case "maxiter" maxiter = varargin{i+1}; case {"tolbeta"} tolbeta = varargin{i+1}; case {"tolobj"} tolobj = varargin{i+1}; otherwise error ("mvregress: unknown option '%s'.", char (varargin{i})); endswitch endfor if (isempty (alg)) if (hasnan) alg = "ecm"; else alg = "mvn"; endif endif if (! any (strcmp (alg, {"ecm", "cwls", "mvn"}))) error ("mvregress: algorithm must be 'ecm', 'cwls', or 'mvn'."); endif ## 'mvn' discards observations with any missing response. keep = true (n, 1); if (strcmp (alg, "mvn")) keep = all (! isnan (Y), 2); endif ## --- fit --- ## All three algorithms produce the maximum-likelihood estimates ('mvn' on the ## kept rows, 'ecm'/'cwls' using every observed response). They differ only ## in how the reported log-likelihood and coefficient covariance are weighted: ## 'cwls' uses the fixed weight covar0 (default the identity), the others use ## the estimated residual covariance. if (isempty (covar0)), covar0 = eye (d); endif if (strcmp (alg, "mvn")) [b, Sigma] = ml_fit (Xcell, Y, eye (d), keep, maxiter, tolbeta, tolobj); Wsig = Sigma; else [b, Sigma] = ml_fit (Xcell, Y, covar0, keep, maxiter, tolbeta, tolobj); if (strcmp (alg, "cwls")) Wsig = covar0; ## logL / CovB use the fixed weight else Wsig = Sigma; endif endif E = residuals (Xcell, Y, b, n, d); ## --- outputs --- logL = -nll_obs (Xcell, Y, b, Wsig, keep); CovB = inv (info_beta (Xcell, Y, Wsig, keep)); if (is_numeric) beta = reshape (b, p, d); else beta = b; endif endfunction ## GLS coefficient estimate with a fixed covariance W (over the kept rows). function b = gls_beta (Xcell, Y, W, keep) K = columns (Xcell{1}); A = zeros (K); rhs = zeros (K, 1); for i = 1:numel (Xcell) if (! keep(i)), continue; endif o = ! isnan (Y(i, :)); if (! any (o)), continue; endif Xio = Xcell{i}(o, :); Wo = W(o, o); A += Xio' * (Wo \ Xio); rhs += Xio' * (Wo \ Y(i, o)'); endfor b = A \ rhs; endfunction ## Maximum-likelihood fit by expectation-conditional-maximization. Reduces to ## ordinary least squares for a complete common design. function [b, Sigma] = ml_fit (Xcell, Y, Sigma, keep, maxiter, tolbeta, tolobj) n = numel (Xcell); d = columns (Y); b = gls_beta (Xcell, Y, Sigma, keep); prev_obj = Inf; for iter = 1:maxiter Si = inv (Sigma); K = numel (b); A = zeros (K); rhs = zeros (K, 1); Scov = zeros (d); nu = 0; ## E-step: impute missing responses; M-step accumulation. for i = 1:n if (! keep(i)), continue; endif o = ! isnan (Y(i, :)); if (! any (o)), continue; endif m = ! o; Xi = Xcell{i}; yfull = zeros (d, 1); yfull(o) = Y(i, o)'; Ccond = zeros (d); if (any (m)) ro = Y(i, o)' - Xi(o, :) * b; yfull(m) = Xi(m, :) * b + Sigma(m, o) * (Sigma(o, o) \ ro); Ccond(m, m) = Sigma(m, m) - Sigma(m, o) * (Sigma(o, o) \ Sigma(o, m)); endif A += Xi' * Si * Xi; rhs += Xi' * Si * yfull; nu += 1; ## store per-observation info to rebuild the residual after beta update imp{i} = yfull; cc{i} = Ccond; endfor b = A \ rhs; for i = 1:n if (! keep(i) || ! any (! isnan (Y(i, :)))), continue; endif r = imp{i} - Xcell{i} * b; Scov += r * r' + cc{i}; endfor Sigma = Scov / nu; obj = nll_obs (Xcell, Y, b, Sigma, keep); if (max (abs (A \ rhs - b)) < tolbeta && abs (prev_obj - obj) < tolobj) break; endif prev_obj = obj; endfor endfunction ## Residuals Y - X*beta (NaN preserved where Y is missing). function E = residuals (Xcell, Y, b, n, d) E = nan (n, d); for i = 1:n o = ! isnan (Y(i, :)); E(i, o) = Y(i, o)' - Xcell{i}(o, :) * b; endfor endfunction ## Negative log-likelihood of the observed responses. function nll = nll_obs (Xcell, Y, b, Sigma, keep) nll = 0; for i = 1:numel (Xcell) if (! keep(i)), continue; endif o = ! isnan (Y(i, :)); if (! any (o)), continue; endif r = Y(i, o)' - Xcell{i}(o, :) * b; So = Sigma(o, o); nll += 0.5 * (sum (o) * log (2*pi) + 2*sum (log (diag (chol (So)))) ... + r' * (So \ r)); endfor endfunction ## Fisher information for the coefficients. function I = info_beta (Xcell, Y, Sigma, keep) K = columns (Xcell{1}); I = zeros (K); for i = 1:numel (Xcell) if (! keep(i)), continue; endif o = ! isnan (Y(i, :)); if (! any (o)), continue; endif Xio = Xcell{i}(o, :); I += Xio' * (Sigma(o, o) \ Xio); endfor endfunction %!demo %! ## Two correlated responses regressed on a common predictor. %! rng (42); %! X = [ones(30,1), (1:30)'/30]; %! B = [1 -1; 2 0.5]; %! E = [0.3 0.1; 0.1 0.2]; %! Y = X * B + randn (30, 2) * chol (E); %! [beta, Sigma] = mvregress (X, Y) %!shared X, Ycomp, Ymiss, Btrue %! X = [ones(12,1), linspace(-1, 1, 12)']; %! Btrue = [1 -0.5 2; 0.3 1.2 -0.8]; %! Ycomp = X * Btrue + 0.05 * cos ((1:12)' * [1 2 3]); %! Ymiss = Ycomp; Ymiss(3,2) = NaN; Ymiss(8,3) = NaN; %!test # numeric design returns a p-by-d beta equal to OLS (complete data) %! [beta, Sigma, E] = mvregress (X, Ycomp, "algorithm", "mvn"); %! assert_equal (size (beta), [2, 3]); %! assert_equal (beta, (X'*X)\(X'*Ycomp), 1e-8); %! assert_equal (Sigma, E'*E/12, 1e-8); %!test # cell design returns the vectorised (K-by-1) beta %! Xc = cell (12, 1); %! for i = 1:12, Xc{i} = kron (eye (3), X(i,:)); end %! bnum = mvregress (X, Ycomp); %! bcell = mvregress (Xc, Ycomp); %! assert_equal (bcell, bnum(:), 1e-8); %!test # logL equals -mvregresslike at the fit (mvn, complete) %! [beta, Sigma, E, CovB, logL] = mvregress (X, Ycomp, "algorithm", "mvn"); %! assert_equal (logL, -mvregresslike (X, Ycomp, beta, Sigma, "mvn"), 1e-8); %! assert_equal (CovB, kron (Sigma, inv (X'*X)), 1e-8); %!test # ecm uses all observed data; the log-likelihood improves on listwise %! b_ecm = mvregress (X, Ymiss, "algorithm", "ecm"); %! b_mvn = mvregress (X, Ymiss, "algorithm", "mvn"); %! assert_equal (! isequal (b_ecm, b_mvn), true); # different estimates %!test # default algorithm: mvn without missing data, ecm with %! [b1, ~, ~, ~, L1] = mvregress (X, Ycomp); %! [b2, ~, ~, ~, L2] = mvregress (X, Ycomp, "algorithm", "mvn"); %! assert_equal (L1, L2, 1e-10); %!test # cwls: logL and CovB use the identity weight %! [beta, Sigma, E, CovB, logL] = mvregress (X, Ycomp, "algorithm", "cwls"); %! assert_equal (logL, -mvregresslike (X, Ycomp, beta, eye (3), "cwls"), 1e-8); %! assert_equal (CovB, kron (eye (3), inv (X'*X)), 1e-8); ## MATLAB-verified parity (mvregress R2026a): 25 observations, 3 responses. %!shared Xc, Yc, Ym %! Xc = [1 -0.5382438937; ... %! 1 0.8672321576; ... %! 1 0.9759864635; ... %! 1 0.3373902524; ... %! 1 -0.9960940966; ... %! 1 -0.5232140317; ... %! 1 -1.297447477; ... %! 1 0.9173885891; ... %! 1 0.1766016286; ... %! 1 0.7551799357; ... %! 1 -0.5914999598; ... %! 1 1.844389637; ... %! 1 1.816922249; ... %! 1 -0.1238333503; ... %! 1 -1.110601355; ... %! 1 -0.6809058803; ... %! 1 0.0141693264; ... %! 1 -0.05955061046; ... %! 1 -0.6610110145; ... %! 1 0.3059509151; ... %! 1 -0.4090578458; ... %! 1 -1.281854823; ... %! 1 -0.2849028295; ... %! 1 -0.06478685589; ... %! 1 1.000383189]; %! Yc = [0.03719753357 -1.298719829 2.592641544; ... %! -0.8732979121 0.2860080857 0.07019509407; ... %! 0.9664790872 2.93781363 2.07467313; ... %! 1.928227972 0.000735060807 1.023278991; ... %! 1.099525737 -1.790260694 1.621821039; ... %! 0.8212389245 -1.973919957 3.698786362; ... %! 0.1466274959 -1.562219847 2.319552949; ... %! 1.316125907 0.7610511082 0.9228417259; ... %! 3.19879516 1.119205548 3.035118373; ... %! 3.324100363 1.40533639 2.521210797; ... %! -0.5492763312 -2.535610011 -0.009321693727; ... %! 0.7506317092 1.042220218 -0.1580843947; ... %! 2.65978063 1.200673839 0.1312316546; ... %! 1.060827996 -1.429695245 2.854978118; ... %! 0.3193218045 -0.6292560647 3.238543733; ... %! 0.08284787504 -0.7632940769 3.223147972; ... %! 1.943818586 -2.359810723 2.132345279; ... %! -1.007337725 0.4325819195 1.790333779; ... %! 0.5483627737 -2.463255725 2.103406495; ... %! 0.4463418718 0.4193706852 1.806900862; ... %! 2.151700093 -0.6796218054 0.2488978108; ... %! 1.601013282 -0.7661239912 3.004258316; ... %! 1.51658466 -0.6677671869 2.37476419; ... %! 0.8655905179 -1.911105684 1.609121594; ... %! -1.036725518 0.3884447934 0.2753353942]; %! Ym = [0.03719753357 -1.298719829 2.592641544; ... %! -0.8732979121 NaN 0.07019509407; ... %! 0.9664790872 2.93781363 2.07467313; ... %! 1.928227972 0.000735060807 1.023278991; ... %! 1.099525737 -1.790260694 NaN; ... %! 0.8212389245 -1.973919957 3.698786362; ... %! 0.1466274959 NaN 2.319552949; ... %! 1.316125907 0.7610511082 0.9228417259; ... %! 3.19879516 1.119205548 3.035118373; ... %! 3.324100363 1.40533639 2.521210797; ... %! -0.5492763312 -2.535610011 -0.009321693727; ... %! 0.7506317092 1.042220218 -0.1580843947; ... %! 2.65978063 1.200673839 0.1312316546; ... %! 1.060827996 NaN 2.854978118; ... %! 0.3193218045 -0.6292560647 3.238543733; ... %! 0.08284787504 -0.7632940769 3.223147972; ... %! 1.943818586 -2.359810723 2.132345279; ... %! -1.007337725 0.4325819195 1.790333779; ... %! 0.5483627737 -2.463255725 NaN; ... %! 0.4463418718 0.4193706852 1.806900862; ... %! 2.151700093 -0.6796218054 0.2488978108; ... %! 1.601013282 -0.7661239912 3.004258316; ... %! 1.51658466 -0.6677671869 2.37476419; ... %! 0.8655905179 -1.911105684 1.609121594; ... %! -1.036725518 0.3884447934 0.2753353942]; %!test # complete data: beta (=OLS), Sigma (ML), logL, CovB vs MATLAB %! [beta, Sigma, ~, CovB, logL] = mvregress (Xc, Yc, "algorithm", "mvn"); %! assert_equal (beta, [0.9290602475, -0.4513207572, 1.79229326; ... %! 0.2367436851, 1.147222011, -0.7755015141], 1e-6); %! assert_equal (Sigma, [1.332860184, 0.1860841493, 0.3727350368; ... %! 0.1860841493, 0.9491200208, 0.328607071; ... %! 0.3727350368, 0.328607071, 0.8648578221], 1e-6); %! assert_equal (logL, -104.1503454, 1e-5); %! assert_equal (CovB, kron (Sigma, inv (Xc'*Xc)), 1e-8); %!test # cwls reports the log-likelihood under the identity weight %! [~, ~, ~, ~, logL] = mvregress (Xc, Yc, "algorithm", "cwls"); %! assert_equal (logL, -108.2558653, 1e-5); %!test # missing data, ecm (uses every observed response) vs MATLAB %! [beta, Sigma, ~, ~, logL] = mvregress (Xc, Ym, "algorithm", "ecm"); %! assert_equal (beta, [0.9290602475, -0.4380875727, 1.82736137; ... %! 0.2367436851, 1.173891485, -0.825576035], 1e-5); %! assert_equal (Sigma, [1.332860184, 0.2265958964, 0.3918376495; ... %! 0.2265958964, 1.034602545, 0.4028401374; ... %! 0.3918376495, 0.4028401374, 0.8842430898], 1e-5); %! assert_equal (logL, -98.2574859, 1e-5); %!test # missing data, mvn (listwise deletion) vs MATLAB %! [beta, ~, ~, ~, logL] = mvregress (Xc, Ym, "algorithm", "mvn"); %! assert_equal (beta, [1.025969216, -0.3367657906, 1.885558095; ... %! 0.3142894292, 1.09915421, -0.8366732999], 1e-5); %! assert_equal (logL, -85.21502995, 1e-5); %!test # missing data, cwls logL under identity weight vs MATLAB %! [~, ~, ~, ~, logL] = mvregress (Xc, Ym, "algorithm", "cwls"); %! assert_equal (logL, -102.6628017, 1e-5); %!error mvregress (1) %!error mvregress (ones (3), {1}) %!error mvregress (ones (2, 2), ones (3, 2)) %!error ... %! mvregress ([], []) %!error ... %! mvregress (zeros (0, 3), zeros (0, 1)) %!error mvregress (ones (3, 2), ones (3, 2), "algorithm", "xxx") %!error mvregress (ones (3, 2), ones (3, 2), "bogus", 1) statistics-release-2.1.2/inst/Regression/mvregresslike.m000066400000000000000000000244011527311104300234570ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{nlogL} =} mvregresslike (@var{X}, @var{Y}, @var{beta}, @var{Sigma}, @var{alg}) ## @deftypefnx {statistics} {[@var{nlogL}, @var{COVB}] =} mvregresslike (@dots{}) ## ## Negative log-likelihood for a multivariate regression model. ## ## @code{mvregresslike (@var{X}, @var{Y}, @var{beta}, @var{Sigma}, @var{alg})} ## returns the negative log-likelihood @var{nlogL} of the multivariate normal ## regression model with responses @var{Y} (an @var{n}-by-@var{d} matrix, one ## row per observation), coefficients @var{beta}, and residual covariance ## @var{Sigma} (@var{d}-by-@var{d}). ## ## @var{X} specifies the design. It is either a numeric @var{n}-by-@var{p} ## matrix, in which case the same @var{p} predictors apply to every response and ## @var{beta} is @var{p}-by-@var{d}; or a cell array of @var{n} design matrices, ## each @var{d}-by-@var{K}, in which case @var{beta} is @var{K}-by-1. ## ## @var{alg} selects how missing responses (@code{NaN} entries of @var{Y}) are ## handled: @qcode{"ecm"} (the default) and @qcode{"cwls"} use every observed ## response through the marginal likelihood of the observed components, while ## @qcode{"mvn"} discards any observation that has a missing response. With no ## missing data all three agree. ## ## The optional second output @var{COVB} is the covariance matrix of the ## coefficient estimates, computed as the inverse of the observed Fisher ## information at @var{beta} and @var{Sigma}. With missing data and the ## @qcode{"ecm"}/@qcode{"cwls"} algorithms this is the standard observed-data ## covariance and can differ from @sc{matlab}'s value (which uses a different ## information convention) at the @code{1e-3} level; @var{nlogL} agrees exactly. ## ## @seealso{mvregress} ## @end deftypefn function [nlogL, COVB] = mvregresslike (X, Y, beta, Sigma, alg) if (nargin < 4) print_usage (); endif if (nargin < 5 || isempty (alg)) alg = "ecm"; endif alg = lower (char (alg)); if (! any (strcmp (alg, {"ecm", "cwls", "mvn"}))) error ("mvregresslike: ALG must be 'ecm', 'cwls', or 'mvn'."); endif [Xcell, bvec, n, d, K] = mvr_design (X, Y, beta); if (! isequal (size (Sigma), [d, d])) error ("mvregresslike: Sigma must be a %d-by-%d matrix.", d, d); endif ## 'mvn' discards observations with any missing response. listwise = strcmp (alg, "mvn"); nlogL = 0; Info = zeros (K, K); for i = 1:n o = ! isnan (Y(i, :)); if (! any (o) || (listwise && ! all (o))) continue; endif Xio = Xcell{i}(o, :); r = Y(i, o)' - Xio * bvec; So = Sigma(o, o); nlogL += 0.5 * (sum (o) * log (2*pi) + logdet (So) + r' * (So \ r)); Info += Xio' * (So \ Xio); endfor if (nargout > 1) COVB = inv (Info); endif endfunction ## Normalise the design to a cell array of per-observation d-by-K matrices and ## the coefficient vector to K-by-1. function [Xcell, bvec, n, d, K] = mvr_design (X, Y, beta) if (! (isnumeric (Y) && ismatrix (Y))) error ("mvregresslike: Y must be a numeric matrix."); endif [n, d] = size (Y); if (iscell (X)) if (numel (X) != n) error ("mvregresslike: X must have one design matrix per observation."); endif K = columns (X{1}); Xcell = X(:); bvec = beta(:); else if (rows (X) != n) error ("mvregresslike: X must have as many rows as Y."); endif p = columns (X); K = p * d; Xcell = cell (n, 1); for i = 1:n Xcell{i} = kron (eye (d), X(i, :)); endfor bvec = beta(:); endif if (numel (bvec) != K) error ("mvregresslike: beta has the wrong number of elements."); endif endfunction ## log(det(A)) via the Cholesky factor (A is a positive-definite covariance). function ld = logdet (A) ld = 2 * sum (log (diag (chol (A)))); endfunction %!demo %! ## Negative log-likelihood of a two-response regression at the true params. %! rng (42); %! X = [ones(20,1), (1:20)'/20]; %! B = [1 -2; 0.5 3]; %! Y = X * B + 0.3 * randn (20, 2); %! nll = mvregresslike (X, Y, B, cov (Y - X*B)) %!test # complete data: nll agrees across algorithms %! X = [ones(15,1), linspace(-1,1,15)']; %! B = [2 -1 0.5; 1 0.3 -0.4]; %! Y = X * B + 0.1 * cos ((1:15)' * [1 2 3]); %! S = cov (Y - X*B); %! n1 = mvregresslike (X, Y, B, S, "ecm"); %! n2 = mvregresslike (X, Y, B, S, "cwls"); %! n3 = mvregresslike (X, Y, B, S, "mvn"); %! assert_equal (n1, n2, 1e-12); %! assert_equal (n1, n3, 1e-12); %!test # COVB of complete common design equals kron (Sigma, inv (X'X)) %! X = [ones(15,1), linspace(-1,1,15)']; %! B = [2 -1 0.5; 1 0.3 -0.4]; %! Y = X * B + 0.1 * cos ((1:15)' * [1 2 3]); %! S = cov (Y - X*B); %! [~, COVB] = mvregresslike (X, Y, B, S, "ecm"); %! assert_equal (COVB, kron (S, inv (X'*X)), 1e-10); %!test # cell and numeric designs give the same nll %! X = [ones(10,1), (1:10)']; %! B = [1 2; -1 0.5]; %! Y = X * B + 0.2 * sin ((1:10)' * [1 2]); %! S = cov (Y - X*B); %! Xc = cell (10, 1); %! for i = 1:10, Xc{i} = kron (eye (2), X(i,:)); end %! assert_equal (mvregresslike (Xc, Y, B(:), S), ... %! mvregresslike (X, Y, B, S), 1e-12); ## MATLAB-verified parity (mvregresslike R2026a): 25 observations, 3 responses, ## a shared 2-column design, at four anchor points and complete/missing data. %!shared Xc, Yc, Ym, anch %! Xc = [1 -0.5382438937; ... %! 1 0.8672321576; ... %! 1 0.9759864635; ... %! 1 0.3373902524; ... %! 1 -0.9960940966; ... %! 1 -0.5232140317; ... %! 1 -1.297447477; ... %! 1 0.9173885891; ... %! 1 0.1766016286; ... %! 1 0.7551799357; ... %! 1 -0.5914999598; ... %! 1 1.844389637; ... %! 1 1.816922249; ... %! 1 -0.1238333503; ... %! 1 -1.110601355; ... %! 1 -0.6809058803; ... %! 1 0.0141693264; ... %! 1 -0.05955061046; ... %! 1 -0.6610110145; ... %! 1 0.3059509151; ... %! 1 -0.4090578458; ... %! 1 -1.281854823; ... %! 1 -0.2849028295; ... %! 1 -0.06478685589; ... %! 1 1.000383189]; %! Yc = [0.03719753357 -1.298719829 2.592641544; ... %! -0.8732979121 0.2860080857 0.07019509407; ... %! 0.9664790872 2.93781363 2.07467313; ... %! 1.928227972 0.000735060807 1.023278991; ... %! 1.099525737 -1.790260694 1.621821039; ... %! 0.8212389245 -1.973919957 3.698786362; ... %! 0.1466274959 -1.562219847 2.319552949; ... %! 1.316125907 0.7610511082 0.9228417259; ... %! 3.19879516 1.119205548 3.035118373; ... %! 3.324100363 1.40533639 2.521210797; ... %! -0.5492763312 -2.535610011 -0.009321693727; ... %! 0.7506317092 1.042220218 -0.1580843947; ... %! 2.65978063 1.200673839 0.1312316546; ... %! 1.060827996 -1.429695245 2.854978118; ... %! 0.3193218045 -0.6292560647 3.238543733; ... %! 0.08284787504 -0.7632940769 3.223147972; ... %! 1.943818586 -2.359810723 2.132345279; ... %! -1.007337725 0.4325819195 1.790333779; ... %! 0.5483627737 -2.463255725 2.103406495; ... %! 0.4463418718 0.4193706852 1.806900862; ... %! 2.151700093 -0.6796218054 0.2488978108; ... %! 1.601013282 -0.7661239912 3.004258316; ... %! 1.51658466 -0.6677671869 2.37476419; ... %! 0.8655905179 -1.911105684 1.609121594; ... %! -1.036725518 0.3884447934 0.2753353942]; %! Ym = [0.03719753357 -1.298719829 2.592641544; ... %! -0.8732979121 NaN 0.07019509407; ... %! 0.9664790872 2.93781363 2.07467313; ... %! 1.928227972 0.000735060807 1.023278991; ... %! 1.099525737 -1.790260694 NaN; ... %! 0.8212389245 -1.973919957 3.698786362; ... %! 0.1466274959 NaN 2.319552949; ... %! 1.316125907 0.7610511082 0.9228417259; ... %! 3.19879516 1.119205548 3.035118373; ... %! 3.324100363 1.40533639 2.521210797; ... %! -0.5492763312 -2.535610011 -0.009321693727; ... %! 0.7506317092 1.042220218 -0.1580843947; ... %! 2.65978063 1.200673839 0.1312316546; ... %! 1.060827996 NaN 2.854978118; ... %! 0.3193218045 -0.6292560647 3.238543733; ... %! 0.08284787504 -0.7632940769 3.223147972; ... %! 1.943818586 -2.359810723 2.132345279; ... %! -1.007337725 0.4325819195 1.790333779; ... %! 0.5483627737 -2.463255725 NaN; ... %! 0.4463418718 0.4193706852 1.806900862; ... %! 2.151700093 -0.6796218054 0.2488978108; ... %! 1.601013282 -0.7661239912 3.004258316; ... %! 1.51658466 -0.6677671869 2.37476419; ... %! 0.8655905179 -1.911105684 1.609121594; ... %! -1.036725518 0.3884447934 0.2753353942]; %! Bt = [1 -0.5 2; ... %! 0.3 1.2 -0.8]; %! S0 = cov (Yc); %! anch = {Bt(:), S0; Bt(:)*0, eye(3); ... %! Bt(:)+0.25, 1.5*S0; Bt(:)-0.4, S0+0.2*eye(3)}; %!test # nll vs MATLAB, complete data (all three algorithms agree) %! ref = [111.8681339; 179.5391646; 120.2543928; 116.9416069]; %! for k = 1:4 %! for alg = {"ecm", "cwls", "mvn"} %! assert_equal (mvregresslike (Xc, Yc, anch{k,1}, anch{k,2}, alg{1}), ... %! ref(k), 1e-6); %! endfor %! endfor %!test # nll vs MATLAB, missing data: ecm/cwls use all observed, mvn is listwise %! ref_ecm = [105.2323495; 169.1339808; 112.8053693; 110.1131617]; %! ref_mvn = [91.35955956; 146.830606; 97.1466466; 96.69358201]; %! for k = 1:4 %! assert_equal (mvregresslike (Xc, Ym, anch{k,1}, anch{k,2}, "ecm"), ... %! ref_ecm(k), 1e-6); %! assert_equal (mvregresslike (Xc, Ym, anch{k,1}, anch{k,2}, "cwls"), ... %! ref_ecm(k), 1e-6); %! assert_equal (mvregresslike (Xc, Ym, anch{k,1}, anch{k,2}, "mvn"), ... %! ref_mvn(k), 1e-6); %! endfor %!test # COVB vs MATLAB (complete data) = kron (Sigma, inv (X'X)) %! for k = 1:4 %! [~, COVB] = mvregresslike (Xc, Yc, anch{k,1}, anch{k,2}, "ecm"); %! assert_equal (COVB, kron (anch{k,2}, inv (Xc'*Xc)), 1e-8); %! endfor %!error mvregresslike (1, 2, 3) %!error mvregresslike ([1;2], [1;2], 1, 1, "xxx") %!error mvregresslike (ones(3,2), ones(3,2), ones(2,2), 1) statistics-release-2.1.2/inst/Regression/nlinfit.m000066400000000000000000000643701527311104300222510ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{beta} =} nlinfit (@var{X}, @var{y}, @var{modelfun}, @var{beta0}) ## @deftypefnx {statistics} {@var{beta} =} nlinfit (@dots{}, @var{options}) ## @deftypefnx {statistics} {@var{beta} =} nlinfit (@dots{}, @var{Name}, @var{Value}) ## @deftypefnx {statistics} {[@var{beta}, @var{R}, @var{J}, @var{CovB}, @var{MSE}, @var{ErrorModelInfo}] =} nlinfit (@dots{}) ## ## Fit a nonlinear regression model. ## ## @code{@var{beta} = nlinfit (@var{X}, @var{y}, @var{modelfun}, @var{beta0})} ## estimates the coefficients of the nonlinear regression model ## @code{@var{y} = @var{modelfun} (@var{beta}, @var{X})} by iteratively ## minimizing the (possibly weighted) sum of squared residuals, starting from ## the initial coefficient vector @var{beta0}. The fit uses the ## Levenberg-Marquardt algorithm with a numerically computed Jacobian. ## ## @itemize ## @item @var{X} is a matrix of predictor values. @code{nlinfit} does not ## interpret the columns of @var{X}; the array is passed unchanged as the second ## argument of @var{modelfun}, so its shape is whatever @var{modelfun} expects. ## @item @var{y} is a numeric vector of responses, one element per observation. ## @item @var{modelfun} is a function handle @code{@@(@var{b}, @var{X})} ## returning a vector of fitted responses the same size as @var{y}. ## @item @var{beta0} is a numeric vector of initial values for the coefficients. ## @end itemize ## ## Additional options are given either as a statset-style @var{options} ## structure or as @qcode{Name}/@qcode{Value} pairs (or both). The supported ## options are: ## ## @multitable @columnfractions 0.2 0.78 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'Weights'} @tab A vector of nonnegative observation weights, or ## a function handle @code{@@(@var{yhat})} returning such a vector. Weighted ## least squares is used. ## @item @qcode{'ErrorModel'} @tab The form of the error variance: ## @qcode{'constant'} (default), @qcode{'proportional'}, or @qcode{'combined'}. ## @item @qcode{'ErrorParameters'} @tab Initial values for the error-model ## parameters. ## @item @qcode{'RobustWgtFun'} @tab The name of a robust weight function ## (@qcode{'andrews'}, @qcode{'bisquare'}, @qcode{'cauchy'}, @qcode{'fair'}, ## @qcode{'huber'}, @qcode{'logistic'}, @qcode{'talwar'}, or @qcode{'welsch'}), ## enabling robust iteratively reweighted least squares. MATLAB accepts this ## name only inside an @qcode{'Options'} structure; taking it as a ## @qcode{Name}/@qcode{Value} pair as well is an Octave extension. ## @item @qcode{'Tune'} @tab The tuning constant for the robust weight function. ## @item @qcode{'Options'} @tab A statset-style structure whose @qcode{MaxIter}, ## @qcode{TolFun}, @qcode{TolX}, and @qcode{DerivStep} fields override the ## corresponding defaults, and whose @qcode{RobustWgtFun}, @qcode{Robust}, ## @qcode{WgtFun} and @qcode{Tune} fields select a robust fit. ## @qcode{RobustWgtFun} names the weight function on its own and takes ## precedence over the other two; the older @qcode{WgtFun} is read only when ## @qcode{Robust} is @qcode{'on'}. The structure @code{statset ('nlinfit')} ## returns carries @qcode{WgtFun} @qcode{'bisquare'} beside @qcode{Robust} ## @qcode{'off'}, and so leaves the fit unweighted. ## @end multitable ## ## The remaining outputs describe the converged fit: @var{R} is the vector of ## raw residuals @code{@var{y} - @var{modelfun} (@var{beta}, @var{X})}, @var{J} ## is the Jacobian of @var{modelfun} with respect to @var{beta} at the solution, ## @var{CovB} is the estimated covariance matrix of the coefficients, @var{MSE} ## is the mean squared error, and @var{ErrorModelInfo} is a structure describing ## the fitted error model. ## ## @subheading Algorithm ## ## The coefficients are estimated by the Levenberg-Marquardt algorithm using a ## numerically computed (forward-difference) Jacobian. For an ordinary or ## weighted fit the coefficient covariance is @code{@var{CovB} = @var{MSE} * ## inv (@var{J}' * @var{W} * @var{J})}, where @var{W} is the diagonal matrix of ## observation weights and @var{MSE} is the weighted residual sum of squares ## divided by the error degrees of freedom @math{n - p} (with @math{p} ## coefficients). A non-constant @qcode{'ErrorModel'} is fitted by generalized ## least squares, re-deriving the observation weights from the fitted values ## each iteration; the @qcode{'proportional'} model weights each observation by ## the inverse squared fitted value, and @var{MSE} then estimates the ## proportionality constant of the variance. ## ## For a robust fit (@qcode{'RobustWgtFun'}) the coefficients are found by ## iteratively reweighted least squares applied to leverage-adjusted residuals ## (the leverage is taken from the ordinary fit and held fixed). The robust ## coefficient covariance follows the Street-Carroll-Ruppert convention, the ## same one used by @code{robustfit}: @code{@var{CovB} = s^2 * inv (@var{J}' * ## @var{J})} and @math{@var{MSE} = s^2}, where the scale @code{s} blends the ## ordinary-fit scale @code{ols_s} with the robust scale @code{robust_s} at the ## solution as @math{s^2 = (p^2 * ols_s^2 + n * robust_s^2) / (n + p^2)}, taken ## to be at least @code{robust_s}. ## ## The robust @code{MSE} and @code{CovB} differ from MATLAB's by up to a few ## tenths of a percent, because the shared robust scale does; @code{robustfit} ## documents that difference and why it is left in place. The coefficients ## themselves agree to about 1e-8. ## ## @seealso{fitnlm, nlparci, nlpredci, NonLinearModel, robustfit} ## @end deftypefn function [beta, R, J, CovB, MSE, ErrorModelInfo] = nlinfit (X, y, modelfun, ... beta0, varargin) if (nargin < 4) print_usage (); endif ## Validate the core inputs. if (! is_function_handle (modelfun)) error ("nlinfit: MODELFUN must be a function handle."); endif if (! (isnumeric (beta0) && isvector (beta0) && isreal (beta0))) error ("nlinfit: BETA0 must be a real numeric vector."); endif if (! (isnumeric (y) && isreal (y))) error ("nlinfit: Y must be a real numeric vector."); endif if (! isvector (y)) error ("nlinfit: Y must be a vector."); endif if (! isequal (size (modelfun (beta0(:), X)), size (y))) error ("nlinfit: MODELFUN must return a vector of the same size as Y."); endif beta0 = beta0(:); y = y(:); n = numel (y); p = numel (beta0); ## Parse the options structure and the Name/Value pairs. opts = parse_options (varargin); ## Resolve observation weights (constant unless a vector/handle is given). if (isempty (opts.Weights)) w = ones (n, 1); elseif (is_function_handle (opts.Weights)) w = []; # deferred: depends on the fitted values else w = check_weights (opts.Weights, n); endif ## Fit. A robust weight function triggers iteratively reweighted least ## squares; a non-constant error model triggers generalized least squares ## with variance-dependent weights; both wrap the weighted LM solver. dfe = n - p; if (! isempty (opts.RobustWgtFun)) ## Robust fit: MSE and CovB use the Street-Carroll-Ruppert robust scale ## (as in robustfit), CovB = MSE * inv (J' * J) at the robust solution. [wname, tune] = robust_weight_function (opts.RobustWgtFun, opts.Tune); [beta, R, J, ols_s, adj] = robust_fit (X, y, modelfun, beta0, w, ... wname, tune, opts); [MSE, CovB] = robust_covariance (R, J, adj, ols_s, wname, tune, n, p); else if (! strcmp (opts.ErrorModel, "constant")) [beta, R, J, extraw] = errormodel_fit (X, y, modelfun, beta0, w, opts); else [beta, R, J] = lm_fit (X, y, modelfun, beta0, w, opts); extraw = ones (n, 1); endif ## Effective per-observation weights combine the user weights (which may be ## a function of the fitted values) with any error-model weights. if (is_function_handle (opts.Weights)) w = check_weights (opts.Weights (modelfun (beta, X)), n); endif weff = w .* extraw; ## Mean squared error and CovB = MSE * inv (J' * Weff * J). if (dfe > 0) MSE = sum (weff .* R .^ 2) / dfe; else MSE = NaN; endif Jw = sqrt (weff) .* J; CovB = MSE * pinv (Jw' * Jw); CovB = (CovB + CovB') / 2; # symmetrise against round-off endif ErrorModelInfo = error_model_info (opts, MSE, p); endfunction ## --------------------------------------------------------------------------- ## Generalized least squares for a non-constant error model. The error weights ## depend on the fitted values and are refreshed each outer iteration. ## proportional: var_i = phi * mu_i^2 -> weight 1 / mu_i^2 ## combined: var_i = phi * (a + |mu_i|)^2 -> weight 1 / (a + |mu_i|)^2 function [beta, R, J, ew] = errormodel_fit (X, y, modelfun, beta0, wuser, opts) if (isempty (wuser)) wuser = ones (numel (y), 1); endif beta = beta0; ew = ones (numel (y), 1); for outer = 1:opts.MaxIter ewprev = ew; [beta, R, J] = lm_fit (X, y, modelfun, beta, wuser .* ew, opts); mu = y - R; ew = errormodel_weights (opts.ErrorModel, mu); if (max (abs (ew - ewprev) ./ (ewprev + eps)) < opts.TolX) break; endif endfor endfunction ## --------------------------------------------------------------------------- ## Per-observation error-model weights from the current fitted values MU. function ew = errormodel_weights (errmodel, mu) am = abs (mu); am(am < eps) = eps; # guard against division by zero switch (errmodel) case "proportional" ew = 1 ./ am .^ 2; case "combined" ## a is a small offset relative to the scale of the fitted values. a = 0.5 * mean (am); ew = 1 ./ (a + am) .^ 2; otherwise ew = ones (size (mu)); endswitch endfunction ## --------------------------------------------------------------------------- ## Plain (optionally weighted) Levenberg-Marquardt nonlinear least squares. ## Returns the coefficients, raw residuals, and Jacobian at the solution. function [beta, R, J] = lm_fit (X, y, modelfun, beta0, w, opts) if (isempty (w)) w = ones (numel (y), 1); endif sw = sqrt (w); beta = beta0; yhat = modelfun (beta, X); yhat = yhat(:); R = y - yhat; sse = sum (w .* R .^ 2); lambda = 1e-2; # Marquardt damping J = nlfun_jacobian (modelfun, beta, X, opts.DerivStep); for iter = 1:opts.MaxIter Jw = sw .* J; Rw = sw .* R; JtJ = Jw' * Jw; Jtr = Jw' * Rw; diagJtJ = diag (diag (JtJ)); ## Try damped steps, increasing lambda until the objective decreases. stepok = false; for inner = 1:20 A = JtJ + lambda * diagJtJ; delta = pinv (A) * Jtr; bnew = beta + delta; ynew = modelfun (bnew, X); rnew = y - ynew(:); ssenew = sum (w .* rnew .^ 2); if (isfinite (ssenew) && ssenew < sse) stepok = true; break; endif lambda = lambda * 10; endfor if (! stepok) break; # cannot improve: converged/stalled endif ## Accept the step and relax the damping. relchg = abs (ssenew - sse) / (sse + eps); stepsz = max (abs (delta) ./ (abs (beta) + eps)); beta = bnew; R = rnew; sse = ssenew; lambda = max (lambda / 10, 1e-12); J = nlfun_jacobian (modelfun, beta, X, opts.DerivStep); if (relchg < opts.TolFun || stepsz < opts.TolX) break; endif endfor endfunction ## --------------------------------------------------------------------------- ## Robust iteratively reweighted least squares around LM_FIT, following the ## robustfit scheme: residuals are leverage-adjusted before scaling, and the ## leverage (from the ordinary fit) is held fixed. Returns the ordinary-fit ## scale OLS_S and the leverage adjustment ADJ for the covariance step. function [beta, R, J, ols_s, adj] = robust_fit (X, y, modelfun, beta0, w, ... wname, tune, opts) if (isempty (w)) w = ones (numel (y), 1); endif n = numel (y); p = numel (beta0); ## Ordinary weighted fit: OLS scale and the (fixed) leverage adjustment. [beta, R, J] = lm_fit (X, y, modelfun, beta0, w, opts); ols_s = norm (R) / sqrt (max (n - p, 1)); Jw = sqrt (w) .* J; h = min (0.9999, diag (Jw * pinv (Jw' * Jw) * Jw')); adj = 1 ./ sqrt (1 - h); for iter = 1:opts.MaxIter betaprev = beta; s = madsigma (R .* adj, p, y); rw = max (robustwfun (R .* adj ./ (s * tune), wname), 0); [beta, R, J] = lm_fit (X, y, modelfun, beta, w .* rw, opts); if (all (abs (beta - betaprev) <= sqrt (eps) * max (abs (beta), ... abs (betaprev)))) break; endif endfor endfunction ## --------------------------------------------------------------------------- ## Street-Carroll-Ruppert robust coefficient covariance (matching robustfit). ## The scale blends the ordinary-fit scale with the robust scale, and ## CovB = s^2 * inv (J' * J) at the robust solution. function [MSE, CovB] = robust_covariance (R, J, adj, ols_s, wname, tune, n, p) radj = R .* adj; mad_s = madsigma (radj, p); if (mad_s == 0) mad_s = 1; endif z = radj ./ (mad_s * tune); [w, psi, psip] = robustwfun (z, wname); if (all (w == 1)) robust_s = ols_s; else K = 1 + (p / n) * var (psip) / mean (psip) ^ 2; robust_s = tune * mad_s * sqrt (mean (psi .^ 2)) / mean (psip) * K; endif s = sqrt ((p ^ 2 * ols_s ^ 2 + n * robust_s ^ 2) / (n + p ^ 2)); s = max (s, robust_s); MSE = s ^ 2; CovB = MSE * pinv (J' * J); CovB = (CovB + CovB') / 2; endfunction ## --------------------------------------------------------------------------- ## Validate the robust weight function name and settle its tuning constant. ## The weight function itself and the tuning constants live in the shared ## private helpers robustwfun and robusttune, which robustfit also uses. function [wname, tune] = robust_weight_function (name, tune) names = {"andrews", "bisquare", "cauchy", "fair", "huber", "logistic", ... "talwar", "welsch"}; if (! ischar (name)) error ("nlinfit: RobustWgtFun must be a character vector."); endif if (! any (strcmp (lower (name), names))) error ("nlinfit: unknown RobustWgtFun '%s'.", name); endif wname = lower (name); if (isempty (tune)) tune = robusttune (wname); endif endfunction ## --------------------------------------------------------------------------- ## Assemble the ErrorModelInfo structure returned as the sixth output. The ## Scheffe dimension for simultaneous prediction is p for a non-constant error ## model and p + 1 for the constant model (the extra observation error term). function info = error_model_info (opts, MSE, p) info = struct (); info.ErrorModel = opts.ErrorModel; info.ErrorParameters = sqrt (MSE); if (strcmp (opts.ErrorModel, "proportional")) info.ErrorVariance = @(x) MSE * abs (x) .^ 2; else info.ErrorVariance = @(x) MSE * ones (size (x, 1), 1); endif info.MSE = MSE; info.ScheffeSimPred = p + strcmp (opts.ErrorModel, "constant"); info.WeightFunction = is_function_handle (opts.Weights); info.FixedWeights = (! isempty (opts.Weights) ... && ! is_function_handle (opts.Weights)); info.RobustWeightFunction = ! isempty (opts.RobustWgtFun); endfunction ## --------------------------------------------------------------------------- ## Validate a numeric weight vector. function w = check_weights (w, n) if (! (isnumeric (w) && isvector (w) && isreal (w))) error ("nlinfit: WEIGHTS must be a real numeric vector."); endif w = w(:); if (numel (w) != n) error ("nlinfit: WEIGHTS must have one element per observation."); endif if (any (w < 0) || any (isnan (w))) error ("nlinfit: WEIGHTS must be nonnegative."); endif endfunction ## --------------------------------------------------------------------------- ## Parse an optional leading statset structure followed by Name/Value pairs. function opts = parse_options (args) ## Defaults. The statset-backed options come from statset ("nlinfit"); the ## rest are nlinfit's own Name/Value arguments. sopts = statset ("nlinfit"); opts = struct ("Weights", [], "ErrorModel", "constant", ... "ErrorParameters", []); ## An options structure may lead the Name/Value pairs. if (! isempty (args) && isstruct (args{1})) sopts = merge_options (sopts, args{1}); args = args(2:end); endif if (mod (numel (args), 2) != 0) error ("nlinfit: Name/Value arguments must come in pairs."); endif for k = 1:2:numel (args) name = args{k}; val = args{k+1}; if (! ischar (name)) error ("nlinfit: parameter names must be character vectors."); endif switch (lower (name)) case 'weights' opts.Weights = val; case 'errormodel' opts.ErrorModel = lower (val); case 'errorparameters' opts.ErrorParameters = val; case 'robustwgtfun' sopts.RobustWgtFun = val; case 'tune' sopts.Tune = val; case 'options' sopts = merge_options (sopts, val); otherwise error ("nlinfit: unknown parameter name '%s'.", name); endswitch endfor if (! any (strcmp (opts.ErrorModel, ... {'constant', 'proportional', 'combined'}))) error ("nlinfit: unknown ErrorModel '%s'.", opts.ErrorModel); endif ## Resolve the robust weight function. 'RobustWgtFun' selects it on its own, ## whatever 'Robust' says, and wins when both are given. The legacy ## 'WgtFun' applies only when 'Robust' is "on", so statset ("nlinfit") -- ## which carries WgtFun "bisquare" beside Robust "off" -- leaves the fit ## unweighted. All three measured against R2024a. opts.RobustWgtFun = sopts.RobustWgtFun; if (isempty (opts.RobustWgtFun) && ischar (sopts.Robust) ... && strcmpi (sopts.Robust, "on")) opts.RobustWgtFun = sopts.WgtFun; if (isempty (opts.RobustWgtFun)) opts.RobustWgtFun = "bisquare"; endif endif opts.Tune = sopts.Tune; opts.MaxIter = sopts.MaxIter; opts.TolFun = sopts.TolFun; opts.TolX = sopts.TolX; opts.DerivStep = sopts.DerivStep; endfunction ## --------------------------------------------------------------------------- ## Merge a statset-style structure into the options, under nlinfit's own name. function sopts = merge_options (sopts, s) if (! (isstruct (s) && isscalar (s))) error ("nlinfit: OPTIONS must be a structure."); endif sopts = statset (sopts, s); endfunction %!demo %! ## Fit an exponential growth model y = b1 * exp (b2 * x). %! x = [1:10]'; %! y = [2.1;2.9;4.2;5.3;7.1;9.4;12.8;16.5;22.1;29.8]; %! modelfun = @(b, x) b(1) .* exp (b(2) .* x); %! beta = nlinfit (x, y, modelfun, [1; 0.3]) %!demo %! ## Robust fitting downweights a gross outlier (5th point corrupted). %! x = [1:10]'; %! y = [2.1;2.9;4.2;5.3;7.1;9.4;12.8;16.5;22.1;29.8]; %! y(5) = 30; %! modelfun = @(b, x) b(1) .* exp (b(2) .* x); %! beta_ols = nlinfit (x, y, modelfun, [1; 0.3]); %! beta_rob = nlinfit (x, y, modelfun, [1; 0.3], 'RobustWgtFun', 'bisquare'); %! [beta_ols, beta_rob] %!shared x, y, modelfun, beta0 %! x = [1;2;3;4;5;6;7;8;9;10]; %! y = [2.1;2.9;4.2;5.3;7.1;9.4;12.8;16.5;22.1;29.8]; %! modelfun = @(b, x) b(1) .* exp (b(2) .* x); %! beta0 = [1; 0.3]; ## The literals below are R2024a's own values, measured, and each tolerance ## states how closely ours agree with them. %!test %! [beta, R, J, CovB, MSE] = nlinfit (x, y, modelfun, beta0); %! assert_equal (beta, [1.683747024946374; 0.286911087203926], 1e-6); %! assert_equal (MSE, 0.029221494299720, 1e-9); %! assert_equal (sqrt (diag (CovB)), ... %! [0.035194898750254; 0.002350913062791], 1e-6); %!test %! ## The returned Jacobian equals the analytic model Jacobian. %! [beta, R, J] = nlinfit (x, y, modelfun, beta0); %! Ja = [exp(beta(2).*x), beta(1).*x.*exp(beta(2).*x)]; %! assert_equal (J, Ja, -1e-4); %!test %! ## Proportional error model reweights by 1 / mu^2 and changes the fit. %! [beta, R, J, CovB, MSE, EMI] = nlinfit (x, y, modelfun, beta0, ... %! "ErrorModel", "proportional"); %! assert_equal (beta, [1.650639591; 0.289917266], 1e-6); %! assert_equal (MSE, 9.988359e-4, 1e-9); %! assert_equal (EMI.ErrorModel, "proportional"); %! assert_equal (EMI.ScheffeSimPred, 2); %!test %! ## Constant error model: ErrorModelInfo fields. %! [beta, R, J, CovB, MSE, EMI] = nlinfit (x, y, modelfun, beta0); %! assert_equal (EMI.ErrorModel, "constant"); %! assert_equal (EMI.ScheffeSimPred, 3); %! assert_equal (EMI.ErrorParameters, 0.170942956, 1e-7); %!test %! ## Observation weights. %! w = (1:10)'; %! [beta, R, J, CovB, MSE] = nlinfit (x, y, modelfun, beta0, "Weights", w); %! assert_equal (beta, [1.677485713; 0.287323059], 1e-6); %! assert_equal (MSE, 0.178838470, 1e-6); %!test %! ## Robust fitting resists a gross outlier: the estimate stays close to the %! ## clean-data fit, far closer than ordinary least squares. Coefficients, %! ## MSE, and covariance match MATLAB's nlinfit (Street-Carroll-Ruppert scale). %! yo = y; yo(5) = 30; %! [br, Rr, Jr, Cr, Mr] = nlinfit (x, yo, modelfun, beta0, ... %! "RobustWgtFun", "bisquare"); %! bo = nlinfit (x, yo, modelfun, beta0); %! bc = nlinfit (x, y, modelfun, beta0); %! assert_equal (max (abs (br - bc)) < max (abs (bo - bc)), true); %! assert_equal (br, [1.679154218; 0.287198922], 1e-5); %! assert_equal (Mr, 15.892255, 1e-2); %! assert_equal (Cr, [0.671544, -0.044220; -0.044220, 0.003012], 1e-3); %!test %! ## The huber robust fit also matches MATLAB's coefficients. %! yo = y; yo(5) = 30; %! bh = nlinfit (x, yo, modelfun, beta0, "RobustWgtFun", "huber"); %! assert_equal (bh, [1.716586997; 0.284865506], 1e-5); ## Test input validation %!error nlinfit (1, 2, @(b, x) x) %!error nlinfit (1, 2, 3, 4) %!error ... %! nlinfit ([1;2], [1;2], @(b, x) b(1) * ones (2, 1), "a") %!error ... %! nlinfit ([1;2], [1;2], @(b, x) b(1) * ones (2, 1), 1, "foo", 1) %!error ... %! nlinfit ([1;2], [1;2], @(b, x) b(1) * ones (2, 1), 1, "ErrorModel", "bad") ## An options structure naming no robust weight function leaves the fit ## unweighted: the legacy WgtFun acts only when Robust is "on" (R2024a) %!test %! x = (1:10)'; %! y = [2.1; 3.9; 6.2; 7.8; 10.1; 12.2; 13.8; 16.1; 18.0; 100.0]; %! modelfun = @(b, xx) b(1) + b(2) * xx; %! bols = nlinfit (x, y, modelfun, [0; 1]); %! bdef = nlinfit (x, y, modelfun, [0; 1], statset ("nlinfit")); %! boff = nlinfit (x, y, modelfun, [0; 1], statset (statset ("nlinfit"), ... %! "Robust", "off", ... %! "WgtFun", "huber")); %! assert_equal (bols, [-15.959999998553135; 6.359999999774407], 1e-8); %! assert_equal (bdef, bols, 1e-12); %! assert_equal (boff, bols, 1e-12); ## Robust "on" activates WgtFun, and RobustWgtFun wins where both are given %!test %! x = (1:10)'; %! y = [2.1; 3.9; 6.2; 7.8; 10.1; 12.2; 13.8; 16.1; 18.0; 100.0]; %! modelfun = @(b, xx) b(1) + b(2) * xx; %! opts = statset ("nlinfit"); %! bon = nlinfit (x, y, modelfun, [0; 1], statset (opts, "Robust", "on")); %! bhub = nlinfit (x, y, modelfun, [0; 1], statset (opts, "Robust", "on", ... %! "WgtFun", "huber")); %! bboth = nlinfit (x, y, modelfun, [0; 1], statset (opts, "Robust", "on", ... %! "WgtFun", "bisquare", ... %! "RobustWgtFun", "huber")); %! assert_equal (bon, [0.040259809225918; 1.996768212418451], 1e-7); %! assert_equal (bhub, [-0.032803877294096; 2.016488145372473], 1e-7); %! assert_equal (bboth, bhub, 1e-12); ## Input validation of Y, the weights and the optional arguments %!error ... %! nlinfit ([1;2;3;4], {1, 2}, @(b, x) b(1) * x, 1) %!error ... %! nlinfit ([1;2;3;4], [2;4;6;8], @(b, x) b(1) * x, 1, "RobustWgtFun", 5) %!error ... %! nlinfit ([1;2;3;4], [2;4;6;8], @(b, x) b(1) * x, 1, "Weights", {1, 2, 3, 4}) %!error ... %! nlinfit ([1;2;3;4], [2;4;6;8], @(b, x) b(1) * x, 1, "Weights", [1; 2]) %!error ... %! nlinfit ([1;2;3;4], [2;4;6;8], @(b, x) b(1) * x, 1, "Weights", [1; -1; 1; 1]) %!error ... %! nlinfit ([1;2;3;4], [2;4;6;8], @(b, x) b(1) * x, 1, ... %! "Weights", [1; NaN; 1; 1]) %!error ... %! nlinfit ([1;2;3;4], [2;4;6;8], @(b, x) b(1) * x, 1, "Weights") %!error ... %! nlinfit ([1;2;3;4], [2;4;6;8], @(b, x) b(1) * x, 1, 42, 1) %!error ... %! nlinfit ([1;2;3;4], [2;4;6;8], @(b, x) b(1) * x, 1, ... %! struct ("MaxIter", {10, 20})) ## Edge cases with empty arrays %!error ... %! nlinfit (ones (4, 2), ones (2, 2), @(b, x) b(1) * x, 1) %!error ... %! nlinfit ([], zeros (0, 1), @(b, x) b(1) .* x, [1]) %!error ... %! nlinfit (zeros (0, 3), zeros (0, 1), @(b, x) b(1) .* x, [1]) %!error ... %! nlinfit ([1;2], [1;2], @(b, x) b(1) * ones (2, 1), 1, "RobustWgtFun", "bad") statistics-release-2.1.2/inst/Regression/nlparci.m000066400000000000000000000135541527311104300222340ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{ci} =} nlparci (@var{beta}, @var{resid}, @qcode{'covar'}, @var{CovB}) ## @deftypefnx {statistics} {@var{ci} =} nlparci (@var{beta}, @var{resid}, @qcode{'jacobian'}, @var{J}) ## @deftypefnx {statistics} {@var{ci} =} nlparci (@dots{}, @qcode{'alpha'}, @var{alpha}) ## ## Confidence intervals for the coefficients of a nonlinear regression. ## ## @code{@var{ci} = nlparci (@var{beta}, @var{resid}, @qcode{'covar'}, ## @var{CovB})} returns the @math{100 (1 - @var{alpha})%} confidence intervals ## for the fitted coefficients @var{beta} of a nonlinear regression, given the ## residual vector @var{resid} and the estimated coefficient covariance matrix ## @var{CovB} (both produced by @code{nlinfit}). @var{ci} is a ## @math{p}-by-@math{2} matrix whose rows are the lower and upper bounds for the ## corresponding coefficient. ## ## @code{@var{ci} = nlparci (@var{beta}, @var{resid}, @qcode{'jacobian'}, ## @var{J})} instead derives the coefficient covariance from the Jacobian ## @var{J} and the residuals. A legacy positional form @code{nlparci ## (@var{beta}, @var{resid}, @var{J})} is also accepted. ## ## The confidence level defaults to @math{95%}; pass @code{@qcode{'alpha'}, ## @var{alpha}} for a @math{100 (1 - @var{alpha})%} interval. The intervals use ## Student's @math{t} distribution with @code{numel (@var{resid}) - numel ## (@var{beta})} degrees of freedom. ## ## @seealso{nlinfit, nlpredci, fitnlm, NonLinearModel} ## @end deftypefn function ci = nlparci (beta, resid, varargin) if (nargin < 3) print_usage (); endif if (! (isnumeric (beta) && isvector (beta) && isreal (beta))) error ("nlparci: BETA must be a real numeric vector."); endif if (! (isnumeric (resid) && isreal (resid))) error ("nlparci: RESID must be a real numeric vector."); endif beta = beta(:); resid = resid(:); p = numel (beta); dfe = numel (resid) - p; if (dfe <= 0) error ("nlparci: not enough residuals to estimate the coefficients."); endif ## Parse the covariance source and the optional confidence level. The third ## argument may be a bare Jacobian (legacy) or a 'covar'/'jacobian' keyword. alpha = 0.05; CovB = []; J = []; args = varargin; if (! ischar (args{1})) J = args{1}; args = args(2:end); endif if (mod (numel (args), 2) != 0) error ("nlparci: Name/Value arguments must come in pairs."); endif for k = 1:2:numel (args) switch (lower (args{k})) case 'covar' CovB = args{k+1}; case 'jacobian' J = args{k+1}; case 'alpha' alpha = args{k+1}; otherwise error ("nlparci: unknown parameter name '%s'.", args{k}); endswitch endfor if (! (isscalar (alpha) && isreal (alpha) && alpha > 0 && alpha < 1)) error ("nlparci: ALPHA must be a scalar in the range (0, 1)."); endif ## Standard errors from either the supplied covariance or the Jacobian. if (! isempty (CovB)) se = sqrt (diag (CovB)); elseif (! isempty (J)) rmse = sqrt (sum (resid .^ 2) / dfe); se = rmse * sqrt (diag (pinv (J' * J))); else error ("nlparci: a covariance matrix or a Jacobian is required."); endif delta = se * tinv (1 - alpha / 2, dfe); ci = [beta - delta, beta + delta]; endfunction %!demo %! ## 95% confidence intervals for the coefficients of an exponential fit. %! x = [1:10]'; %! y = [2.1;2.9;4.2;5.3;7.1;9.4;12.8;16.5;22.1;29.8]; %! modelfun = @(b, x) b(1) .* exp (b(2) .* x); %! [beta, R, J, CovB] = nlinfit (x, y, modelfun, [1; 0.3]); %! ci = nlparci (beta, R, 'covar', CovB) %!shared beta, R, J, CovB %! x = [1;2;3;4;5;6;7;8;9;10]; %! y = [2.1;2.9;4.2;5.3;7.1;9.4;12.8;16.5;22.1;29.8]; %! modelfun = @(b, xx) b(1) .* exp (b(2) .* xx); %! [beta, R, J, CovB] = nlinfit (x, y, modelfun, [1; 0.3]); ## The interval below is R2024a's own, measured; ours agrees to 1e-5, the ## difference coming through nlinfit's covariance rather than from nlparci. %!test %! ci = nlparci (beta, R, "covar", CovB); %! assert_equal (ci, [1.602587442890197, 1.764906607002551; ... %! 0.281489871959626, 0.292332302448227], 1e-5); %!test %! ## The Jacobian and covariance forms agree. %! ci1 = nlparci (beta, R, "covar", CovB); %! ci2 = nlparci (beta, R, "jacobian", J); %! assert_equal (ci1, ci2, 1e-8); %!test %! ## The legacy positional Jacobian form matches the named form. %! ci = nlparci (beta, R, J); %! assert_equal (ci, nlparci (beta, R, "jacobian", J), 1e-12); %!test %! ## A 90% interval is narrower than the default 95% interval. %! ci95 = nlparci (beta, R, "covar", CovB); %! ci90 = nlparci (beta, R, "covar", CovB, "alpha", 0.10); %! assert_equal (all (diff (ci90, 1, 2) < diff (ci95, 1, 2), 'all'), true); ## Test input validation %!error nlparci (1, 2) %!error ... %! nlparci ([1;2], [1;2;3], "foo", 1) %!error ... %! nlparci ([1;2], [1;2;3], "covar", eye (2), "alpha", 2) %!error ... %! nlparci ([1;2], [1;2;3], "alpha", 0.05) %!error nlparci ([1;2], 1, "covar", eye (2)) statistics-release-2.1.2/inst/Regression/nlpredci.m000066400000000000000000000222001527311104300223700ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{ypred}, @var{delta}] =} nlpredci (@var{modelfun}, @var{X}, @var{beta}, @var{resid}, @qcode{'Jacobian'}, @var{J}) ## @deftypefnx {statistics} {[@var{ypred}, @var{delta}] =} nlpredci (@var{modelfun}, @var{X}, @var{beta}, @var{resid}, @qcode{'Covar'}, @var{CovB}) ## @deftypefnx {statistics} {[@var{ypred}, @var{delta}] =} nlpredci (@dots{}, @var{Name}, @var{Value}) ## ## Confidence intervals for predictions of a nonlinear regression. ## ## @code{[@var{ypred}, @var{delta}] = nlpredci (@var{modelfun}, @var{X}, ## @var{beta}, @var{resid}, @qcode{'Jacobian'}, @var{J})} returns the predicted ## responses @var{ypred} of the model @code{@var{modelfun} (@var{beta}, ## @var{X})} at the new predictor values @var{X}, together with the half-widths ## @var{delta} of the @math{100 (1 - @var{alpha})%} confidence intervals, so ## that @code{@var{ypred} - @var{delta}} and @code{@var{ypred} + @var{delta}} ## bound the response. @var{beta}, @var{resid} (the residuals) and @var{J} (the ## Jacobian) come from @code{nlinfit}. ## ## Instead of the Jacobian, an estimated coefficient covariance may be supplied ## with @code{@qcode{'Covar'}, @var{CovB}}. The following @qcode{Name}/ ## @qcode{Value} pairs are also accepted: ## ## @multitable @columnfractions 0.2 0.78 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'MSE'} @tab The mean squared error from @code{nlinfit}, required ## with @qcode{'Covar'} for observation (prediction) intervals. ## @item @qcode{'PredOpt'} @tab @qcode{'curve'} (default) for confidence ## intervals on the fitted curve, or @qcode{'observation'} for prediction ## intervals on a new observation. ## @item @qcode{'SimOpt'} @tab @qcode{'off'} (default) for pointwise intervals, ## or @qcode{'on'} for simultaneous (Scheffe) intervals. ## @item @qcode{'Alpha'} @tab The significance level; the interval has ## confidence @math{100 (1 - @var{alpha})%} (default @var{alpha} = 0.05). ## @end multitable ## ## @subheading Algorithm ## ## Each half-width is @code{@var{delta} = c * sqrt (v)}. The variance @code{v} ## of the fitted curve is @code{diag (@var{Jnew} * @var{V} * @var{Jnew}')}, ## where @var{V} is the coefficient covariance (either @var{CovB}, or ## @code{@var{MSE} * inv (@var{J}' * @var{J})} when a Jacobian is supplied) and ## @var{Jnew} is the Jacobian of @var{modelfun} at @var{X}; an ## @qcode{'observation'} interval adds the error variance @var{MSE} to @code{v}. ## The critical value @code{c} is the Student's @math{t} quantile at ## @math{1 - @var{alpha}/2} with the error degrees of freedom for a pointwise ## interval, or the Scheffe value @code{sqrt (k * finv (1 - @var{alpha}, k, ## dfe))} for a simultaneous interval, where @math{k} is the number of ## coefficients (plus one for an observation interval). ## ## @seealso{nlinfit, nlparci, fitnlm, NonLinearModel} ## @end deftypefn function [ypred, delta] = nlpredci (modelfun, X, beta, resid, varargin) if (nargin < 5) print_usage (); endif if (! is_function_handle (modelfun)) error ("nlpredci: MODELFUN must be a function handle."); endif if (! (isnumeric (beta) && isvector (beta) && isreal (beta))) error ("nlpredci: BETA must be a real numeric vector."); endif if (! (isnumeric (resid) && isreal (resid))) error ("nlpredci: RESID must be a real numeric vector."); endif beta = beta(:); resid = resid(:); p = numel (beta); dfe = numel (resid) - p; if (dfe <= 0) error ("nlpredci: not enough residuals to estimate the error variance."); endif ## Parse the covariance source and options. A bare Jacobian may follow the ## residuals (legacy), otherwise 'Jacobian'/'Covar' keywords select it. J = []; CovB = []; MSE = []; predopt = 'curve'; simopt = 'off'; alpha = 0.05; args = varargin; if (! ischar (args{1})) J = args{1}; args = args(2:end); endif if (mod (numel (args), 2) != 0) error ("nlpredci: Name/Value arguments must come in pairs."); endif for k = 1:2:numel (args) switch (lower (args{k})) case 'jacobian' J = args{k+1}; case 'covar' CovB = args{k+1}; case 'mse' MSE = args{k+1}; case 'predopt' predopt = lower (args{k+1}); case 'simopt' simopt = lower (args{k+1}); case 'alpha' alpha = args{k+1}; case {'weights', 'errormodelinfo'} ## accepted for compatibility; not needed for the covariance path otherwise error ("nlpredci: unknown parameter name '%s'.", args{k}); endswitch endfor if (! (isscalar (alpha) && isreal (alpha) && alpha > 0 && alpha < 1)) error ("nlpredci: ALPHA must be a scalar in the range (0, 1)."); endif if (! any (strcmp (predopt, {'curve', 'observation'}))) error ("nlpredci: PREDOPT must be 'curve' or 'observation'."); endif if (! any (strcmp (simopt, {'on', 'off'}))) error ("nlpredci: SIMOPT must be 'on' or 'off'."); endif ## Fitted responses and the Jacobian of the model at the new points. ypred = modelfun (beta, X); ypred = ypred(:); Jnew = nlfun_jacobian (modelfun, beta, X, eps ^ (1/3)); ## Coefficient covariance and the error variance for observation intervals. if (! isempty (CovB)) Vbeta = CovB; if (isempty (MSE)) errvar = sum (resid .^ 2) / dfe; else errvar = MSE; endif elseif (! isempty (J)) if (isempty (MSE)) errvar = sum (resid .^ 2) / dfe; else errvar = MSE; endif Vbeta = errvar * pinv (J' * J); else error ("nlpredci: a Jacobian or a covariance matrix is required."); endif ## Variance of the fitted curve, plus the error variance for observations. varpred = sum ((Jnew * Vbeta) .* Jnew, 2); varpred = max (varpred, 0); if (strcmp (predopt, 'observation')) varpred = varpred + errvar; endif ## Critical value: Student's t (pointwise) or Scheffe (simultaneous). The ## Scheffe dimension is the number of coefficients, plus one for a new ## observation's own error term. if (strcmp (simopt, 'on')) nsim = p + strcmp (predopt, 'observation'); crit = sqrt (nsim * finv (1 - alpha, nsim, dfe)); else crit = tinv (1 - alpha / 2, dfe); endif delta = crit * sqrt (varpred); endfunction %!demo %! ## Prediction intervals for an exponential fit at three new x-values. %! x = [1:10]'; %! y = [2.1;2.9;4.2;5.3;7.1;9.4;12.8;16.5;22.1;29.8]; %! modelfun = @(b, x) b(1) .* exp (b(2) .* x); %! [beta, R, J] = nlinfit (x, y, modelfun, [1; 0.3]); %! [ypred, delta] = nlpredci (modelfun, [2.5; 5.5; 8.5], beta, R, 'Jacobian', J) %!shared modelfun, beta, R, J, CovB, MSE, xp %! x = [1;2;3;4;5;6;7;8;9;10]; %! y = [2.1;2.9;4.2;5.3;7.1;9.4;12.8;16.5;22.1;29.8]; %! modelfun = @(b, xx) b(1) .* exp (b(2) .* xx); %! [beta, R, J, CovB, MSE] = nlinfit (x, y, modelfun, [1; 0.3]); %! xp = [2.5; 5.5; 8.5]; ## Values verified against MATLAB's nlpredci. %!test %! [yp, dc] = nlpredci (modelfun, xp, beta, R, "Jacobian", J); %! assert_equal (yp, [3.449741734; 8.158274148; 19.293455050], 1e-6); %! assert_equal (dc, [0.120614865; 0.160334882; 0.171532624], 1e-6); %!test %! ## Observation (prediction) intervals exceed the curve intervals by MSE. %! [yp, dc] = nlpredci (modelfun, xp, beta, R, "Jacobian", J); %! [yp, dp] = nlpredci (modelfun, xp, beta, R, "Jacobian", J, ... %! "PredOpt", "observation"); %! assert_equal (dp, [0.412235094; 0.425555051; 0.429899138], 1e-6); %! assert_equal (all (dp > dc, 'all'), true); %!test %! ## Simultaneous (Scheffe) intervals are wider than the pointwise ones. %! [yp, dc] = nlpredci (modelfun, xp, beta, R, "Jacobian", J); %! [yp, ds] = nlpredci (modelfun, xp, beta, R, "Jacobian", J, "SimOpt", "on"); %! assert_equal (ds, [0.156197123; 0.207634833; 0.222135990], 1e-6); %! assert_equal (all (ds > dc, 'all'), true); %!test %! ## The covariance form matches the Jacobian form. %! [yp, dj] = nlpredci (modelfun, xp, beta, R, "Jacobian", J); %! [yp, dv] = nlpredci (modelfun, xp, beta, R, "Covar", CovB, "MSE", MSE); %! assert_equal (dj, dv, 1e-8); ## Test input validation %!error nlpredci (@(b, x) x, [1;2], [1;2], [1;2]) %!error ... %! nlpredci (1, [1;2], [1;2], [1;2;3], "Jacobian", eye (2)) %!error ... %! nlpredci (@(b, x) x, [1;2], [1;2], [1;2;3], "Jacobian", eye (2), ... %! "PredOpt", "bad") %!error ... %! nlpredci (@(b, x) x, [1;2], [1;2], [1;2;3], "foo", 1) statistics-release-2.1.2/inst/Regression/plsregress.m000066400000000000000000000733661527311104300230040ustar00rootroot00000000000000## Copyright (C) 2012-2019 Fernando Damian Nieuwveldt ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{xload}, @var{yload}] =} plsregress (@var{X}, @var{Y}) ## @deftypefnx {statistics} {[@var{xload}, @var{yload}] =} plsregress (@var{X}, @var{Y}, @var{NCOMP}) ## @deftypefnx {statistics} {[@var{xload}, @var{yload}, @var{xscore}, @var{yscore}, @var{coef}, @var{pctVar}, @var{mse}, @var{stats}] =} plsregress (@var{X}, @var{Y}, @var{NCOMP}) ## @deftypefnx {statistics} {[@var{xload}, @var{yload}, @var{xscore}, @var{yscore}, @var{coef}, @var{pctVar}, @var{mse}, @var{stats}] =} plsregress (@dots{}, @var{Name}, @var{Value}) ## ## Calculate partial least squares regression using SIMPLS algorithm. ## ## @code{plsregress} uses the SIMPLS algorithm, and first centers @var{X} and ## @var{Y} by subtracting off column means to get centered variables, unless ## @qcode{'Intercept'} is @qcode{false}. However, it does not rescale the ## columns. To perform partial least squares regression with standardized ## variables, use @code{zscore} to normalize @var{X} and @var{Y}. ## ## @code{[@var{xload}, @var{yload}] = plsregress (@var{X}, @var{Y})} computes a ## partial least squares regression of @var{Y} on @var{X}, using @var{NCOMP} ## PLS components, which by default are calculated as ## @qcode{min (size (@var{X}, 1) - 1, size(@var{X}, 2))}, and returns the ## the predictor and response loadings in @var{xload} and @var{yload}, ## respectively. ## @itemize ## @item @var{X} is an @math{N*P} matrix of predictor variables, with rows ## corresponding to observations, and columns corresponding to variables. ## @item @var{Y} is an @math{N*M} response matrix. ## @item @var{xload} is a @math{P*NCOMP} matrix of predictor loadings, where ## each row of @var{xload} contains coefficients that define a linear ## combination of PLS components that approximate the original predictor ## variables. ## @item @var{yload} is an @math{M*NCOMP} matrix of response loadings, where ## each row of @var{yload} contains coefficients that define a linear ## combination of PLS components that approximate the original response ## variables. ## @end itemize ## ## @code{[@var{xload}, @var{yload}] = plsregress (@var{X}, @var{Y}, ## @var{NCOMP})} defines the desired number of PLS components to use in the ## regression. @var{NCOMP}, a scalar positive integer, must not exceed the ## default calculated value. ## ## @code{[@var{xload}, @var{yload}, @var{xscore}, @var{yscore}, @var{coef}, ## @var{pctVar}, @var{mse}, @var{stats}] = plsregress (@var{X}, @var{Y}, ## @var{NCOMP})} also returns the following arguments: ## @itemize ## @item @var{xscore} is an @math{N*NCOMP} orthonormal matrix with the predictor ## scores, i.e., the PLS components that are linear combinations of the ## variables in @var{X}, with rows corresponding to observations and columns ## corresponding to components. ## @item @var{yscore} is an @math{N*NCOMP} orthonormal matrix with the response ## scores, i.e., the linear combinations of the responses with which the PLS ## components @var{xscore} have maximum covariance, with rows corresponding to ## observations and columns corresponding to components. ## @item @var{coef} is a @math{(P+1)*M} matrix with the PLS regression ## coefficients, containing the intercepts in the first row. If ## @qcode{'Intercept'} is @qcode{false}, @var{coef} is a @math{P*M} matrix ## without the intercepts. ## @item @var{pctVar} is a @math{2*NCOMP} matrix containing the percentage of ## the variance explained by the model with the first row containing the ## percentage of explained varianced in @var{X} by each PLS component and the ## second row containing the percentage of explained variance in @var{Y}. ## @item @var{mse} is a @math{2*(NCOMP+1)} matrix containing the estimated mean ## squared errors for PLS models with @qcode{0:@var{NCOMP}} components with the ## first row containing the squared errors for the predictor variables in ## @var{X} and the second row containing the mean squared errors for the ## response variable(s) in @var{Y}. ## @item @var{stats} is a structure with the following fields: ## @itemize ## @item @var{stats}@qcode{.W} is a @math{P*NCOMP} matrix of PLS weights. ## @item @var{stats}@qcode{.T2} is the @math{T^2} statistics for each point in ## @var{xscore}. ## @item @var{stats}@qcode{.Xresiduals} is an @math{N*P} matrix with the ## predictor residuals. ## @item @var{stats}@qcode{.Yresiduals} is an @math{N*M} matrix with the ## response residuals. ## @end itemize ## @end itemize ## ## @code{[@dots{}] = plsregress (@dots{}, @var{Name}, @var{Value}, @dots{})} ## specifies one or more of the following @var{Name}/@var{Value} pairs: ## ## @multitable @columnfractions 0.2 0.75 ## @headitem @var{Name} @tab @var{Value} ## @item @qcode{'CV'} @tab The method used to compute @var{mse}. When ## @var{Value} is a positive integer @math{K}, @code{plsregress} uses ## @math{K}-fold cross-validation. Set @var{Value} to a cross-validation ## partition, created using @code{cvpartition}, to use other forms of ## cross-validation. Set @var{Value} to @qcode{'resubstitution'} to use both ## @var{X} and @var{Y} to fit the model and to estimate the mean squared errors, ## without cross-validation. By default, @qcode{@var{Value} = "resubstitution"}. ## @item @qcode{'MCReps'} @tab A positive integer indicating the number of ## Monte-Carlo repetitions for cross-validation. By default, ## @qcode{@var{Value} = 1}. A different @qcode{'MCReps'} value is only ## meaningful when using the @qcode{'HoldOut'} method for cross-validation, ## previously set by a @code{cvpartition} object. If no cross-validation method ## is used, then @qcode{'MCReps'} must be @qcode{1}. ## @item @qcode{'Intercept'} @tab A logical scalar, or @qcode{'on'} or ## @qcode{'off'}, specifying whether the model includes a constant term. When ## the constant term is excluded, @var{X} and @var{Y} are not centered and ## @var{coef} contains no intercepts. By default, @qcode{@var{Value} = true}. ## @end multitable ## ## Further information about the PLS regression can be found at ## @url{https://en.wikipedia.org/wiki/Partial_least_squares_regression} ## ## @subheading References ## @enumerate ## @item ## SIMPLS: An alternative approach to partial least squares regression. ## Chemometrics and Intelligent Laboratory Systems (1993) ## ## @end enumerate ## @end deftypefn function [xload, yload, xscore, yscore, coef, pctVar, mse, stats] = ... plsregress (X, Y, NCOMP, varargin) ## Check input arguments and add defaults if (nargin < 2) error ("plsregress: function called with too few input arguments."); endif if (! isnumeric (X) || ! isnumeric (Y)) error ("plsregress: X and Y must be real matrices."); endif ## Get size of predictor and response inputs [nobs, npred] = size (X); [Yobs, nresp] = size (Y); if (nobs != Yobs) error ("plsregress: X and Y observations mismatch."); endif ## Calculate max number of components NCOMPmax = min (nobs - 1, npred); if (nargin < 3) NCOMP = NCOMPmax; elseif (! isnumeric (NCOMP) || ! isscalar (NCOMP) || NCOMP != fix (NCOMP) || NCOMP <= 0) error ("plsregress: invalid value for NCOMP."); elseif (NCOMP > NCOMPmax) error ("plsregress: NCOMP exceeds maximum components for X."); endif ## Add default optional arguments CV = false; mcreps = 1; intercept = true; ## Parse additional Name-Value pairs while (numel (varargin) > 0) if (strcmpi (varargin{1}, 'cv')) cvarg = varargin{2}; if (isa (cvarg, 'cvpartition')) CV = true; elseif (isscalar (cvarg) && cvarg == fix (cvarg) && cvarg > 0) CV = true; elseif (! strcmpi (cvarg, 'resubstitution')) error ("plsregress: invalid VALUE for 'cv' optional argument."); endif elseif (strcmpi (varargin{1}, 'mcreps')) mcreps = varargin{2}; if (! (isscalar (mcreps) && mcreps == fix (mcreps) && mcreps > 0)) error ("plsregress: invalid VALUE for 'mcreps' optional argument."); endif elseif (strcmpi (varargin{1}, 'intercept')) intarg = varargin{2}; if (ischar (intarg) && isrow (intarg) && any (strcmpi (intarg, {'on', 'off'}))) intercept = strcmpi (intarg, 'on'); elseif ((isnumeric (intarg) || islogical (intarg)) && isscalar (intarg) && (intarg == 0 || intarg == 1)) intercept = logical (intarg); else error ("plsregress: invalid VALUE for 'intercept' optional argument."); endif else error ("plsregress: invalid NAME argument."); endif varargin(1:2) = []; endwhile ## Check MCREPS = 1 when "resubstitution" is set for cross validation if (! CV && mcreps != 1) error (strcat ("plsregress: 'mcreps' must be 1 when 'resubstitution'", ... " is specified for cross validation.")); endif ## Check number of output arguments if (nargout > 8) print_usage (); endif ## Mean centering Data matrix and responses, unless intercept is excluded if (intercept) Xmeans = mean (X); Ymeans = mean (Y); else Xmeans = zeros (1, npred); Ymeans = zeros (1, nresp); endif X0 = bsxfun (@minus, X, Xmeans); Y0 = bsxfun (@minus, Y, Ymeans); [P, Q, T, U, W] = simpls (X0, Y0, NCOMP, intercept); ## Store output arguments xload = P; yload = Q; xscore = T; yscore = U; ## Compute regression coefficients if (nargout > 4) coef = W * Q'; if (intercept) coef = [Ymeans - Xmeans * coef; coef]; endif endif ## Compute the percent of variance explained for X and Y if (nargout > 5) XVar = sum (abs (xload) .^ 2, 1) ./ sum (sum (abs (X0) .^ 2, 1)); YVar = sum (abs (yload) .^ 2, 1) ./ sum (sum (abs (Y0) .^ 2, 1)); pctVar = [XVar; YVar]; endif ## Estimate the mean squared errors if (nargout > 6) ## Compute MSE by cross-validation if (CV) mse = NaN (2, NCOMP + 1); ## Check crossval method and recalculate max number of components if isa (cvarg, 'cvpartition') type = 'Partition'; NCOMPmax = min (min (cvarg.TrainSize)-1,npred); ts = sum (cvarg.TestSize); else type = 'Kfold'; NCOMPmax = min (floor ((nobs * (cvarg - 1) / cvarg) -1), npred); ts = nobs; endif if (NCOMP > NCOMPmax) warning (strcat ("plsregress: NCOMP exceeds maximum components", ... " for cross validation.")); NCOMP = NCOMPmax; endif ## Create function handle with NCOMP extra argument F = @(xtr, ytr, xte, yte) sseCV (xtr, ytr, xte, yte, NCOMP, intercept); ## Apply cross validation sse = crossval (F, X, Y, type, cvarg, 'mcreps', mcreps); ## Compute MSE from the SSEs collected from each cross validation set mse(:,1:NCOMP+1) = reshape (sum (sse, 1) / (ts * mcreps), [2, NCOMP+1]); ## Computed fitted if residuals are requested if (nargout > 7) xfitted = xscore * xload'; yfitted = xscore * yload'; endif ## Compute MSE by resubstitution else mse = zeros (2, NCOMP + 1); ## Model with 0 components mse(1,1) = sum (sum (abs (X0) .^ 2, 2)); mse(2,1) = sum (sum (abs (Y0) .^ 2, 2)); ## Models with 1:NCOMP components for i = 1:NCOMP xfitted = xscore(:,1:i) * xload(:,1:i)'; yfitted = xscore(:,1:i) * yload(:,1:i)'; mse(1,i+1) = sum (sum (abs (X0 - xfitted) .^ 2, 2)); mse(2,i+1) = sum (sum (abs (Y0 - yfitted) .^ 2, 2)); endfor ## Compute the mean of the sum of squares above mse = mse / nobs; endif endif ## Compute stats if (nargout > 7) ## Save weights stats.W = W; ## Compute T-squared stats.T2 = sum (bsxfun (@rdivide, abs (xscore) .^ 2, ... var (xscore, [], 1)) , 2); ## Compute residuals for X and Y stats.Xresiduals = X0 - xfitted; stats.Yresiduals = Y0 - yfitted; endif endfunction ## SIMPLS algorithm function [P, Q, T, U, W] = simpls (X0, Y0, NCOMP, intercept) ## Get size of predictor and response inputs [nobs, npred] = size (X0); [Yobs, nresp] = size (Y0); ## Compute covariance S = X0' * Y0; ## Preallocate matrices W = P = V = zeros (npred, NCOMP); T = U = zeros (nobs, NCOMP); Q = zeros (nresp, NCOMP); ## Models with 1:NCOMP components for a = 1:NCOMP [eigvec, eigval] = eig (S' * S); # Y factor weights ## Get max eigenvector, the first one when several tie [~, domindex] = max (diag (eigval)); q = eigvec(:,domindex); w = S * q; # X block factor weights t = X0 * w; # X block factor scores if (intercept) t = t - mean (t); endif nt = sqrt (t' * t); # compute norm t = t ./ nt; w = w ./ nt; # normalize p = X0' * t; # X block factor loadings q = Y0' * t; # Y block factor loadings u = Y0 * q; # Y block factor scores v = p; ## Ensure orthogonality if (a > 1) v = v - V * (V' * p); u = u - T * (T' * u); endif v = v ./ sqrt (v' * v); # normalize orthogonal loadings S = S - v * (v' * S); # deflate S wrt loadings V(:,a) = v; ## Store data P(:,a) = p; # Xloads Q(:,a) = q; # Yloads T(:,a) = t; # xscore U(:,a) = u; # Yscores W(:,a) = w; # Weights endfor endfunction ## Helper function for SSE cross-validation function sse = sseCV (XTR, YTR, XTE, YTE, NCOMP, intercept) ## Center train data, unless intercept is excluded if (intercept) XTRmeans = mean (XTR); YTRmeans = mean (YTR); else XTRmeans = zeros (1, columns (XTR)); YTRmeans = zeros (1, columns (YTR)); endif X0TR = bsxfun (@minus, XTR, XTRmeans); Y0TR = bsxfun (@minus, YTR, YTRmeans); ## Center test data X0TE = bsxfun (@minus, XTE, XTRmeans); Y0TE = bsxfun (@minus, YTE, YTRmeans); ## Fit the full model [xload, yload, ~, ~, W] = simpls (X0TR, Y0TR, NCOMP, intercept); XTEscore = X0TE * W; ## Preallocate SSE matrix sse = zeros (2, NCOMP + 1); ## Model with 0 components sse(1,1) = sum (sum (abs (X0TE) .^ 2, 2)); sse(2,1) = sum (sum (abs (Y0TE) .^ 2, 2)); ## Models with 1:NCOMP components for i = 1:NCOMP X0fitted = XTEscore(:,1:i) * xload(:,1:i)'; sse(1,i+1) = sum (sum (abs (X0TE - X0fitted) .^ 2, 2)); Y0fitted = XTEscore(:,1:i) * yload(:,1:i)'; sse(2,i+1) = sum (sum (abs (Y0TE - Y0fitted) .^ 2, 2)); endfor ## crossval collects a row per test set, and the caller reshapes the summed ## rows back to [2, NCOMP+1] sse = sse(:)'; endfunction %!demo %! ## Perform Partial Least-Squares Regression %! %! ## Load the spectra data set and use the near infrared (NIR) spectral %! ## intensities (NIR) as the predictor and the corresponding octave %! ## ratings (octave) as the response. %! load spectra %! %! ## Perform PLS regression with 10 components %! [xload, yload, xscore, yscore, coef, ptcVar] = plsregress (NIR, octane, 10); %! %! ## Plot the percentage of explained variance in the response variable %! ## (PCTVAR) as a function of the number of components. %! plot (1:10, cumsum (100 * ptcVar(2,:)), '-ro'); %! xlim ([1, 10]); %! xlabel ('Number of PLS components'); %! ylabel ('Percentage of Explained Variance in octane'); %! title ('Explained Variance per PLS components'); %! %! ## Compute the fitted response and display the residuals. %! octane_fitted = [ones(size(NIR,1),1), NIR] * coef; %! residuals = octane - octane_fitted; %! figure %! stem (residuals, 'color', 'r', 'markersize', 4, 'markeredgecolor', 'r') %! xlabel ('Observations'); %! ylabel ('Residuals'); %! title ('Residuals in octane''s fitted response'); %!demo %! ## Calculate Variable Importance in Projection (VIP) for PLS Regression %! %! ## Load the spectra data set and use the near infrared (NIR) spectral %! ## intensities (NIR) as the predictor and the corresponding octave %! ## ratings (octave) as the response. Variables with a VIP score greater than %! ## 1 are considered important for the projection of the PLS regression model. %! load spectra %! %! ## Perform PLS regression with 10 components %! [xload, yload, xscore, yscore, coef, pctVar, mse, stats] = ... %! plsregress (NIR, octane, 10); %! %! ## Calculate the normalized PLS weights %! W0 = stats.W ./ sqrt (sum (stats.W.^2,1)); %! %! ## Calculate the VIP scores for 10 components %! nobs = size (xload, 1); %! SS = sum (xscore .^ 2, 1) .* sum (yload .^ 2, 1); %! VIPscore = sqrt (nobs * sum (SS .* (W0 .^ 2), 2) ./ sum (SS, 2)); %! %! ## Find variables with a VIP score greater than or equal to 1 %! VIPidx = find (VIPscore >= 1); %! %! ## Plot the VIP scores %! scatter (1:length (VIPscore), VIPscore, 'xb'); %! hold on %! scatter (VIPidx, VIPscore(VIPidx), 'xr'); %! plot ([1, length(VIPscore)], [1, 1], '--k'); %! hold off %! axis ('tight'); %! xlabel ('Predictor Variables'); %! ylabel ('VIP scores'); %! title ('VIP scores for each predictor variable with 10 components'); ## Test output %!test %! load spectra %! [xload, yload, xscore, yscore, coef, pctVar] = plsregress (NIR, octane, 10); %! xload1_out = [-0.0170, 0.0039, 0.0095, 0.0258, 0.0025, ... %! -0.0075, 0.0000, 0.0018, -0.0027, 0.0020]; %! yload_out = [6.6384, 9.3106, 2.0505, 0.6471, 0.9625, ... %! 0.5905, 0.4244, 0.2437, 0.3516, 0.2548]; %! xscore1_out = [-0.0401, -0.1764, -0.0340, 0.1669, 0.1041, ... %! -0.2067, 0.0457, 0.1565, 0.0706, -0.1471]; %! yscore1_out = [-12.4635, -15.0003, 0.0638, 0.0652, -0.0070, ... %! -0.0634, 0.0062, -0.0012, -0.0151, -0.0173]; %! assert_equal (xload(1,:), xload1_out, 1e-4); %! assert_equal (yload, yload_out, 1e-4); %! assert_equal (xscore(1,:), xscore1_out, 1e-4); %! assert_equal (yscore(1,:), yscore1_out, 1e-4); %!test %! load spectra %! [xload, yload, xscore, yscore, coef, pctVar] = plsregress (NIR, octane, 5); %! xload1_out = [-0.0170, 0.0039, 0.0095, 0.0258, 0.0025]; %! yload_out = [6.6384, 9.3106, 2.0505, 0.6471, 0.9625]; %! xscore1_out = [-0.0401, -0.1764, -0.0340, 0.1669, 0.1041]; %! yscore1_out = [-12.4635, -15.0003, 0.0638, 0.0652, -0.0070]; %! assert_equal (xload(1,:), xload1_out, 1e-4); %! assert_equal (yload, yload_out, 1e-4); %! assert_equal (xscore(1,:), xscore1_out, 1e-4); %! assert_equal (yscore(1,:), yscore1_out, 1e-4); ## Test Intercept option %!shared Xi, Yi %! Xi = [1 2 4; 2 1 3; 3 5 2; 4 3 6; 5 6 5; 6 4 9; 7 8 7; 8 7 10]; %! Yi = [3 1; 2 4; 6 2; 5 7; 9 5; 8 9; 12 8; 11 11]; %!test %! [xload, yload, xscore, yscore, coef] = plsregress (Xi, Yi, 2, ... %! 'Intercept', false); %! xload_out = [-14.2220866289987, -0.382908357308142; ... %! -13.7943470180713, -3.6736441811456; ... %! -17.5828395894702, 3.22646836023154]; %! yload_out = [-21.7155246617638, -3.37516200500198; ... %! -18.5549450196154, 2.92602803399922]; %! xscore_out = [-0.159463240281499, 0.176166759987928; ... %! -0.135201662297805, 0.219123900595871; ... %! -0.209658303362604, -0.554109815239913; ... %! -0.290146968405309, 0.275009629902996; ... %! -0.344859965660409, -0.334985914644042; ... %! -0.425348630703114, 0.494133530498867; ... %! -0.474906494400693, -0.403685011986161; ... %! -0.550240025885877, 0.137611435218759]; %! yscore_out = [-83.7015190049069, -4.16947710556566; ... %! -117.650829401989, 7.52277173856108; ... %! -167.403038009814, -10.4151724456727; ... %! -238.462238446127, 9.11950487145006; ... %! -288.214447053951, -9.19359074611281; ... %! -340.718702470649, 7.41505853701072; ... %! -409.025856098089, -8.06996238088916; ... %! -442.975166495171, 5.51470553389892]; %! coef_out = [0.42805469436003, 0.390758452559943; ... %! 0.979698857562371, -0.111297240033807; ... %! 0.120196040659335, 0.826534140491024]; %! assert_equal (xload, xload_out, 1e-10); %! assert_equal (yload, yload_out, 1e-10); %! assert_equal (xscore, xscore_out, 1e-10); %! assert_equal (yscore, yscore_out, 1e-10); %! assert_equal (coef, coef_out, 1e-10); %!test %! [~, ~, ~, ~, ~, pctVar, mse] = plsregress (Xi, Yi, 2, 'Intercept', false); %! pctVar_out = [0.963884623305986, 0.0330389814001876; ... %! 0.965502953865825, 0.0236134421488264]; %! mse_out = [91, 3.28649927915526, 0.27995197173819; ... %! 105.625, 3.64375049792233, 1.14958067095253]; %! assert_equal (pctVar, pctVar_out, 1e-10); %! assert_equal (mse, mse_out, 1e-10); %!test %! [~, ~, ~, ~, ~, ~, ~, stats] = plsregress (Xi, Yi, 2, 'Intercept', false); %! W_out = [-0.0203808431932239, 0.00430379596844149; ... %! -0.0197436438096987, -0.163238171288745; ... %! -0.0248987773672195, 0.124584826649244]; %! T2_out = [1.31872433173103; 1.12791712302996; 4.05334038679012; ... %! 4.17604263860742; 5.93709890387945; 9.54610820786475; ... %! 10.9102185426489; 13.247290390847]; %! Xres_out = [-1.20044429274505, 0.447482719671833, 0.627786948487707; ... %! 0.161054619240101, -0.0600354047820661, -0.0842254726551177; ... %! -0.193951832033687, 0.0722983097243972, 0.101429470340183; ... %! -0.021191834171586, 0.00789955822793997, 0.0110825275173898; ... %! -0.0328972127935856, 0.0122629049423306, 0.0172039976850655; ... %! 0.139862785068515, -0.0521358465541262, -0.0731429451377288; ... %! 0.0912643311496959, -0.0340200801975641, -0.0477277923719566; ... %! 0.227131253714974, -0.0846664120518552, -0.118781052596658]; %! Yres_out = [0.131763427877889, -2.47430053445642; ... %! -0.196396368260435, 0.85017791344085; ... %! -0.423050452210615, -0.268857438492841; ... %! -0.372491594083243, 0.811653066915507; ... %! 0.380553179528951, -0.41867952505199; ... %! 0.431112037656321, -0.338169019643642; ... %! 0.32465399436548, 0.369329768913692; ... %! -0.484289964410188, 0.387671654846869]; %! assert_equal (stats.W, W_out, 1e-10); %! assert_equal (stats.T2, T2_out, 1e-10); %! assert_equal (stats.Xresiduals, Xres_out, 1e-10); %! assert_equal (stats.Yresiduals, Yres_out, 1e-10); %!test %! [~, ~, ~, ~, coef] = plsregress (Xi, Yi, 2); %! coef_out = [-0.0955463955587028, -0.810241454727286; ... %! 0.502555885912014, 0.483878040247745; ... %! 0.950692448939054, -0.0936986428112594; ... %! 0.0966832849963297, 0.857292898480539]; %! assert_equal (coef, coef_out, 1e-10); %!test %! Xc = Xi - mean (Xi); %! Yc = Yi - mean (Yi); %! [xl1, yl1, xs1, ys1, c1] = plsregress (Xc, Yc, 2, 'Intercept', false); %! [xl2, yl2, xs2, ys2, c2] = plsregress (Xc, Yc, 2); %! assert_equal (xl1, xl2, 1e-10); %! assert_equal (yl1, yl2, 1e-10); %! assert_equal (xs1, xs2, 1e-10); %! assert_equal (ys1, ys2, 1e-10); %! assert_equal (c1, c2(2:end,:), 1e-10); %! assert_equal (c2(1,:), [0, 0], 1e-10); %!test %! [~, ~, ~, ~, coef] = plsregress (Xi, Yi, 2, 'Intercept', 'OFF'); %! assert_equal (size (coef), [3, 2]); %! [~, ~, ~, ~, coef] = plsregress (Xi, Yi, 2, 'Intercept', 'On'); %! assert_equal (size (coef), [4, 2]); %!test %! [~, ~, ~, ~, coef] = plsregress (Xi, Yi, 2, 'Intercept', int8 (0)); %! assert_equal (size (coef), [3, 2]); %! [~, ~, ~, ~, coef] = plsregress (Xi, Yi, 2, 'Intercept', uint8 (1)); %! assert_equal (size (coef), [4, 2]); %!test %! [~, ~, ~, ~, ~, ~, mse] = plsregress (Xi, Yi, 2, 'Intercept', false, ... %! 'CV', 8); %! mse_out = [91, 4.32192776866963, 0.340549626995997; ... %! 105.625, 4.71960207316224, 1.60619414427168]; %! assert_equal (mse, mse_out, 1e-10); %!test %! [~, ~, ~, ~, ~, ~, mse] = plsregress (Xi, Yi, 2, 'CV', 8); %! mse_out = [22.7755102040816, 5.53413798133651, 0.437067443370619; ... %! 28.8775510204082, 6.35600933981599, 2.45515345309489]; %! assert_equal (mse, mse_out, 1e-10); %!warning %! [~, ~, ~, ~, ~, ~, mse] = plsregress (Xi(1:4,:), Yi(1:4,:), 3, ... %! 'Intercept', false, 'CV', 4); %! mse_out = [33.5, 6.11816888105823, 4.40871978441352; ... %! 36, 8.82425775321653, 7.84649635299314]; %! assert_equal (mse(:,1:3), mse_out, 1e-10); %! assert_equal (isnan (mse(:,4)), [true; true]); %!test %! [~, yload, ~, ~, coef, ~, ~, stats] = plsregress (Xi, Yi(:,1), 2, ... %! 'Intercept', false); %! yload_out = [21.7303444547984, 3.27301621090877]; %! coef_out = [0.490175387381509; 0.948569023203085; 0.0943735846723177]; %! Yres_out = [0.235192227523052; -0.212040551983055; -0.402118447504584; ... %! -0.372650127169194; 0.385841000512361; 0.415309320847751; ... %! 0.319605009998538; -0.505122108196838]; %! assert_equal (yload, yload_out, 1e-10); %! assert_equal (coef, coef_out, 1e-10); %! assert_equal (stats.Yresiduals, Yres_out, 1e-10); %!test %! Y = [1:6; (1:6) .^ 2]'; %! [~, ~, ~, ~, b, pv, mse, st] = plsregress (ones (6, 2), Y, 1); %! assert_equal (b, NaN (3, 2)); %! assert_equal (pv, [NaN; NaN]); %! assert_equal (mse, [0, NaN; 152.055555555556, NaN], 1e-12); %! assert_equal (st.T2, NaN (6, 1)); %!test %! X = [ones(6, 1), (1:6)']; %! Y = [1:6; (1:6) .^ 2]'; %! [xl, yl, ~, ~, b, pv, mse, st] = plsregress (X, Y, 2); %! assert_equal (abs (xl), [0, NaN; 4.18330013267038, NaN], 1e-12); %! assert_equal (abs (yl), [4.18330013267038, NaN; 29.2831009286927, NaN], ... %! 1e-12); %! assert_equal (b, NaN (3, 2)); %! assert_equal (pv, [1, NaN; 0.959079283887468, NaN], 1e-12); %! assert_equal (mse, [2.91666666666667, 0, NaN; ... %! 152.055555555556, 6.22222222222222, NaN], 1e-12); %! assert_equal (st.T2, NaN (6, 1)); ## Test input validation %!error %! plsregress (1) %!error plsregress (1, 'asd') %!error plsregress (1, {1,2,3}) %!error plsregress ('asd', 1) %!error plsregress ({1,2,3}, 1) %!error ... %! plsregress (ones (20,3), ones (15,1)) %!error ... %! plsregress (ones (20,3), ones (20,1), 0) %!error ... %! plsregress (ones (20,3), ones (20,1), -5) %!error ... %! plsregress (ones (20,3), ones (20,1), 3.2) %!error ... %! plsregress (ones (20,3), ones (20,1), [2, 3]) %!error ... %! plsregress (ones (20,3), ones (20,1), 4) %!error ... %! plsregress (ones (20,3), ones (20,1), 3, 'cv', 4.5) %!error ... %! plsregress (ones (20,3), ones (20,1), 3, 'cv', -1) %!error ... %! plsregress (ones (20,3), ones (20,1), 3, 'cv', 'somestring') %!error ... %! plsregress (ones (20,3), ones (20,1), 3, 'cv', 3, 'mcreps', 2.2) %!error ... %! plsregress (ones (20,3), ones (20,1), 3, 'cv', 3, 'mcreps', -2) %!error ... %! plsregress (ones (20,3), ones (20,1), 3, 'cv', 3, 'mcreps', [1, 2]) %!error ... %! plsregress (ones (20,3), ones (20,1), 3, 'Name', 3, 'mcreps', 1) %!error ... %! plsregress (ones (20,3), ones (20,1), 3, 'cv', 3, 'Name', 1) %!error ... %! plsregress (ones (20,3), ones (20,1), 3, 'mcreps', 2) %!error ... %! plsregress (ones (20,3), ones (20,1), 3, 'cv', 'resubstitution', 'mcreps', 2) %!error ... %! plsregress (ones (20,3), ones (20,1), 3, 'Intercept', 2) %!error ... %! plsregress (ones (20,3), ones (20,1), 3, 'Intercept', 'true') %!error ... %! plsregress (ones (20,3), ones (20,1), 3, 'Intercept', [true, false]) %!error ... %! plsregress (ones (20,3), ones (20,1), 3, 'Intercept', {false}) %!error ... %! plsregress (ones (20,3), ones (20,1), 3, 'Intercept', ['off'; 'off']) %!test %! ## A single output returns the predictor loadings, as MATLAB allows; %! ## fewer than two outputs used to be refused outright. %! X = [1, 2, 3; 2, 3, 5; 3, 5, 8; 4, 7, 11; 5, 11, 16; 6, 13, 19]; %! Y = [1; 2; 3; 4; 5; 6]; %! xl = plsregress (X, Y, 2); %! [xl2, yl2] = plsregress (X, Y, 2); %! assert_equal (xl, xl2); %! assert_equal (size (xl), [3, 2]); statistics-release-2.1.2/inst/Regression/private/000077500000000000000000000000001527311104300220705ustar00rootroot00000000000000statistics-release-2.1.2/inst/Regression/private/__glmefit__.m000066400000000000000000000353561527311104300245050ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{M} =} __glmefit__ (@var{X}, @var{y}, @var{Z}, @var{G}, @var{distr}, @var{link}, @var{method}) ## ## Estimation core for generalized linear mixed-effects models. Undocumented ## internal function for @code{fitglme}/@code{GeneralizedLinearMixedModel}. ## ## Fits the model by penalized quasi-likelihood (PQL): it alternates between ## forming the generalized-linear working response and iterative weights, and ## fitting a weighted linear mixed model to that pseudo-response, until ## convergence. @var{method} @qcode{"mpl"} uses a maximum-likelihood inner fit, ## @qcode{"rempl"} a restricted-maximum-likelihood inner fit; @qcode{"laplace"} ## and @qcode{"approximatelaplace"} use the same point estimates but report the ## Laplace-approximated marginal log-likelihood. ## ## @end deftypefn function M = __glmefit__ (X, y, Z, G, distr, link, method) n = rows (X); p = columns (X); method = lower (method); distr = lower (distr); link = lower (link); ## Build the expanded random design + per-term bookkeeping (cf. __lmefit__). [Zx, qk, nlev, levels, gidx] = expand_random (Z, G, n); ## Fixed dispersion for binomial/poisson; estimated for normal/gamma/invgauss. fixed_disp = any (strcmp (distr, {"binomial", "poisson"})); isreml = strcmp (method, "rempl"); ## --- initialise beta from a plain GLM (no random effects) --- beta = glm_irls (X, y, distr, link); b = zeros (columns (Zx), 1); theta = init_theta (qk); disp0 = 1; ## --- PQL outer iterations --- for outer = 1:100 eta = X * beta + Zx * b; [mu, dmu] = inv_link (eta, link); v = var_fun (mu, distr); w = dmu .^ 2 ./ v; ## iterative weights (disp = 1) zt = eta + (y - mu) ./ dmu; ## working response ## inner weighted (RE)ML fit of the pseudo-response zt, Var(e)=disp/w; ## theta is relative to the dispersion, so scale it back to absolute [theta, disp0] = inner_fit (theta, X, zt, Zx, qk, nlev, w, isreml, ... fixed_disp); theta_abs = sqrt (disp0) * theta; [beta_n, b, Psi, covbeta] = inner_solve (theta_abs, X, zt, Zx, qk, ... nlev, w, disp0); if (max (abs (beta_n - beta)) < 1e-8) beta = beta_n; break; endif beta = beta_n; endfor ## --- final quantities --- eta = X * beta + Zx * b; [mu, dmu] = inv_link (eta, link); v = var_fun (mu, distr); w = dmu .^ 2 ./ v; zt = eta + (y - mu) ./ dmu; ## pseudo (RE)ML log-likelihood of the final weighted linear mixed model ## (REML uses n - p in place of n in the constant term). if (isreml), ndf = n - p; else, ndf = n; endif pll = -0.5 * (ndf * log (2*pi) ... + weighted_dev (theta_abs, weighted_cross (X, zt, Zx, w), ... qk, nlev, n, p, isreml, disp0, false)); if (any (strcmp (method, {"laplace", "approximatelaplace"}))) loglik = laplace_loglik (X, y, beta, Zx, qk, nlev, gidx, Psi, distr, ... link, disp0); else loglik = pll; endif M = struct (); M.beta = beta; M.b = b; M.Psi = Psi; M.dispersion = disp0; M.covbeta = covbeta; M.loglik = loglik; M.mu = mu; M.fitted = mu; M.resid_raw = y - mu; M.resid_pearson = (y - mu) ./ sqrt (var_fun (mu, distr)); M.Zx = Zx; M.qk = qk; M.nlev = nlev; M.levels = levels; M.gidx = gidx; M.n = n; M.p = p; M.dfe = n - p; M.distr = distr; M.link = link; M.method = method; endfunction ## ---- link functions (canonical) ---- function [mu, dmu] = inv_link (eta, link) switch (link) case "logit" mu = 1 ./ (1 + exp (-eta)); dmu = mu .* (1 - mu); case "log" mu = exp (eta); dmu = mu; case "identity" mu = eta; dmu = ones (size (eta)); otherwise error ("__glmefit__: unsupported link '%s'.", link); endswitch endfunction function v = var_fun (mu, distr) switch (distr) case "binomial" v = mu .* (1 - mu); case "poisson" v = mu; case "normal" v = ones (size (mu)); otherwise error ("__glmefit__: unsupported distribution '%s'.", distr); endswitch endfunction ## plain GLM by iteratively reweighted least squares (no random effects) function beta = glm_irls (X, y, distr, link) beta = zeros (columns (X), 1); for it = 1:100 eta = X * beta; [mu, dmu] = inv_link (eta, link); w = dmu .^ 2 ./ var_fun (mu, distr); z = eta + (y - mu) ./ dmu; bn = (X' * (w .* X)) \ (X' * (w .* z)); if (max (abs (bn - beta)) < 1e-10) beta = bn; break; endif beta = bn; endfor endfunction ## ---- weighted mixed-model inner solver / objective ---- function [theta, disp0] = inner_fit (theta0, X, z, Zx, qk, nlev, w, ... isreml, fixed_disp) ## The weights are fixed for the whole inner fit, so every cross product the ## objective needs is formed once here rather than n-by-n matrices being ## rebuilt on each of its evaluations. CP = weighted_cross (X, z, Zx, w); n = rows (X); p = columns (X); ## Where the dispersion is estimated, theta is relative to it and the ## dispersion is profiled out of the objective, as in __lmefit__; where it is ## fixed at one, relative and absolute coincide. obj = @(th) weighted_dev (th, CP, qk, nlev, n, p, isreml, 1, ! fixed_disp); ## The weighted deviance is differentiable in theta and its gradient is ## closed form, so hand it over rather than let the optimiser difference an ## objective whose curvature in the variance components is far below the ## step a finite difference has to take. See __lmefit__ for the same ## treatment of the linear case. opts = optimset ("TolX", 1e-10, "TolFun", 1e-10, "MaxFunEvals", 2000, ... "Display", "off", "GradObj", "on"); theta = fminunc (obj, theta0, opts); if (fixed_disp) disp0 = 1; else disp0 = profile_dispersion (theta, CP, qk, nlev, n, p, isreml); endif endfunction ## Weighted -2*log-likelihood at the covariance parameters theta, and its ## gradient. Only Zx*D*Zx' in V depends on theta, so dV/dtheta is ## Zx*(dD/dtheta)*Zx'; beta drops out because it is the GLS minimiser, and ## D = L*L' gives dD = E_ij*L' + L*E_ij', so every entry is 2*(S*L)(i,j) for ## the matching accumulated S. With PROF, V = disp*V0 with theta giving V0 ## and the dispersion replaced by its estimate r'*inv (V0)*r / ndf, so the ## residual term becomes ndf*log (r'*inv (V0)*r / ndf) + ndf and its ## gradient is scaled by ndf / (r'*inv (V0)*r). function [dev, grad] = weighted_dev (theta, CP, qk, nlev, n, p, isreml, ... disp0, prof) S = wquad (theta, CP, qk, nlev, n, disp0); if (! S.ok) dev = Inf; grad = zeros (size (theta)); return; endif beta = S.XtViX \ S.XtViz; rVir = S.ztViz - S.XtViz' * beta; if (prof) if (isreml), ndf = n - p; else, ndf = n; endif dev = S.logdetV + ndf * log (rVir / ndf) + ndf; sc = ndf / rVir; else dev = S.logdetV + rVir; sc = 1; endif if (isreml) Rx = chol (S.XtViX); dev += 2 * sum (log (diag (Rx))); endif if (nargout < 2) return; endif ## One further solve carries the gradient, all of it in the random effects ## dimension: c is Zx'*inv (V)*r and B is Zx'*inv (V)*X. Of ## ZtViZ = Zx'*inv (V)*Zx only the per-level diagonal blocks are needed, ## taken entry by entry, since the whole product Zt'*Zt fills in. LtZtZ = S.Lf' * CP.ZtZ / disp0; Zt = S.Rk' \ LtZtZ(S.pk,:); B = CP.ZtX / disp0 - Zt' * S.Xt; c = (CP.Ztz / disp0 - Zt' * S.zt) - B * beta; if (isreml) T = Rx' \ B'; endif grad = zeros (size (theta)); off = 0; col = 0; for k = 1:numel (qk) q = qk(k); m = q*(q+1)/2; Lk = tril_from_theta (theta(off+(1:m)), q); Sa = zeros (q); base = col + (0:nlev(k)-1)' * q; for j = 1:q for i = 1:q ci = base + i; cj = base + j; ZtZij = sum (CP.ZtZ(sub2ind (size (CP.ZtZ), ci, cj))) / disp0; Sa(i,j) = full (ZtZij - sum (sum (Zt(:,ci) .* Zt(:,cj)))); endfor endfor Sc = zeros (q); Sp = zeros (q); for l = 1:nlev(k) sel = col + (1:q); col += q; cs = c(sel); Sc += cs * cs'; if (isreml) Ts = T(:,sel); Sp += Ts' * Ts; endif endfor Ga = 2 * (Sa * Lk); Gc = 2 * (Sc * Lk); if (isreml) Gp = 2 * (Sp * Lk); else Gp = zeros (q); endif idx = 0; for j = 1:q for i = j:q idx += 1; grad(off+idx) = Ga(i,j) - sc * Gc(i,j) - Gp(i,j); endfor endfor off += m; endfor endfunction ## The dispersion at the fitted covariance parameters: the weighted residual ## sum of squares over its degrees of freedom. Whitened, as weighted_dev is, ## rather than through an explicit inverse of V. function d = profile_dispersion (theta, CP, qk, nlev, n, p, isreml) S = wquad (theta, CP, qk, nlev, n, 1); beta = S.XtViX \ S.XtViz; rVir = S.ztViz - S.XtViz' * beta; if (isreml) d = rVir / (n - p); else d = rVir / n; endif endfunction function [beta, b, Psi, covbeta] = inner_solve (theta, X, z, Zx, qk, ... nlev, w, disp0) n = rows (X); p = columns (X); CP = weighted_cross (X, z, Zx, w); S = wquad (theta, CP, qk, nlev, n, disp0); beta = S.XtViX \ S.XtViz; ## The BLUPs need Zx'*inv (V)*r rather than inv (V)*r itself, and that stays ## in the random effects dimension: with g = Zx'*inv (Dg)*r, it is ## g - Zt'*(Rk'\(L'*g)), so the n-vector is never formed. g = (CP.Ztz - CP.ZtX * beta) / disp0; Ltg = S.Lf' * g; u = zeros (size (Ltg)); u(S.pk) = S.Rk \ (S.Rk' \ Ltg(S.pk)); ZtVir = g - (CP.ZtZ / disp0) * (S.Lf * u); b = S.Lf * (S.Lf' * ZtVir); ## The covariance of the fixed effects is genuinely an inverse, but it is ## the inverse of a matrix already factored, so take it from the factor. Rx = chol (S.XtViX); covbeta = Rx \ (Rx' \ eye (p)); ## per-term covariance matrices Psi = cell (1, numel (qk)); off = 0; for k = 1:numel (qk) m = qk(k)*(qk(k)+1)/2; Lk = tril_from_theta (theta(off+(1:m)), qk(k)); Psi{k} = Lk * Lk'; off += m; endfor endfunction ## The weighted cross products the inner fit needs. All are free of theta and ## of the dispersion, so they survive every evaluation of the objective. function CP = weighted_cross (X, z, Zx, w) w = w(:); Zw = Zx .* w; CP.ZtZ = choose_storage (Zw' * Zx); CP.ZtX = Zw' * X; CP.Ztz = Zw' * z; CP.XtX = (X .* w)' * X; CP.Xtz = (X .* w)' * z; CP.ztz = (z .* w)' * z; CP.logw = sum (log (w)); endfunction ## X'*inv (V)*X, X'*inv (V)*z, z'*inv (V)*z and logdet (V) for ## V = Zx*L*L'*Zx' + disp*diag (1./w), none of them formed in the observation ## dimension. The leading term is diagonal rather than a multiple of the ## identity, so inv (Dg) is diag (w)/disp and the Woodbury capacitance is ## K = I_q + L'*(Zx'*diag (w)*Zx)*L/disp. function S = wquad (theta, CP, qk, nlev, n, disp0) S.Lf = build_Lfull (theta, qk, nlev); [S.Rk, flag, S.pk] = factor_K (S.Lf, CP.ZtZ / disp0); S.ok = (flag == 0); if (! S.ok) return; endif LtZtX = S.Lf' * CP.ZtX / disp0; LtZtz = S.Lf' * CP.Ztz / disp0; S.Xt = S.Rk' \ LtZtX(S.pk,:); S.zt = S.Rk' \ LtZtz(S.pk); S.XtViX = CP.XtX / disp0 - S.Xt' * S.Xt; S.XtViX = (S.XtViX + S.XtViX') / 2; S.XtViz = CP.Xtz / disp0 - S.Xt' * S.zt; S.ztViz = CP.ztz / disp0 - S.zt' * S.zt; S.logdetV = n * log (disp0) - CP.logw + 2 * sum (log (full (diag (S.Rk)))); endfunction function theta0 = init_theta (qk) theta0 = []; for k = 1:numel (qk) for j = 1:qk(k) for i = j:qk(k) theta0(end+1) = 0.1 * (i == j); endfor endfor endfor theta0 = theta0(:); endfunction ## ---- Laplace-approximated marginal log-likelihood (single grouping term) ---- ## Per group: logL_g = log p(y_g | b_hat) - 0.5*b_hat'*inv(P)*b_hat ## - 0.5*log|I + P*(Z_g'*W*Z_g)|, with W the GLM weights at ## the mode b_hat, divided by the dispersion. The last two terms are combined ## into a single log-det that stays finite as the random-effect variance goes ## to zero. function ll = laplace_loglik (X, y, beta, Zx, qk, nlev, gidx, Psi, distr, ... link, disp0) if (numel (qk) != 1) ll = NaN; ## only single-term supported return; endif q = qk(1); P = Psi{1}; ## A negligible random-effect variance pins the group modes at zero, so the ## marginal likelihood is the plain GLM likelihood (as MATLAB reports). degenerate = (max (abs (diag (P))) < 1e-8); if (! degenerate), Pi = inv (P); endif gi = gidx{1}; ll = 0; for g = 1:nlev(1) idx = (gi == g); Xg = X(idx, :); Zg = full (Zx(idx, (g-1)*q + (1:q))); yg = y(idx); bg = zeros (q, 1); if (! degenerate) for it = 1:100 ## Newton for the group mode eta = Xg * beta + Zg * bg; [mu, dmu] = inv_link (eta, link); W = dmu .^ 2 ./ (disp0 * var_fun (mu, distr)); gr = Zg' * (yg - mu) / disp0 - Pi * bg; ## canonical-link score H = -(Zg' * (W .* Zg)) - Pi; step = H \ gr; bg = bg - step; if (max (abs (step)) < 1e-10), break; endif endfor endif eta = Xg * beta + Zg * bg; [mu, dmu] = inv_link (eta, link); W = dmu .^ 2 ./ (disp0 * var_fun (mu, distr)); if (degenerate) pen = 0; else pen = 0.5 * (bg' * (P \ bg)); endif ll += log_pmf (yg, mu, distr, disp0) - pen ... - 0.5 * logdet_spd (eye (q) + P * (Zg' * (W .* Zg))); endfor endfunction function lp = log_pmf (y, mu, distr, disp0) switch (distr) case "binomial" lp = sum (y .* log (mu) + (1 - y) .* log (1 - mu)); case "poisson" lp = sum (y .* log (mu) - mu - gammaln (y + 1)); case "normal" lp = sum (-0.5 * (y - mu) .^ 2 / disp0 - 0.5 * log (2*pi*disp0)); otherwise lp = NaN; endswitch endfunction function ld = logdet_spd (A) ## A is built from products and is not bitwise symmetric; see build_V. A = (A + A') / 2; ld = 2 * sum (log (diag (chol (A)))); endfunction statistics-release-2.1.2/inst/Regression/private/__lme_dfsatt__.m000066400000000000000000000172671527311104300252010ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{df} =} __lme_dfsatt__ (@var{X}, @var{y}, @var{Zx}, @var{qk}, @var{nlev}, @var{Psi}, @var{sigma2}, @var{method}, @var{L}) ## ## Satterthwaite denominator degrees of freedom for the single-row contrasts in ## the rows of @var{L}. Undocumented internal helper for ## @code{LinearMixedModel}. ## ## Uses the delta method: for a contrast @var{l}, the denominator variance is ## @code{l*C*l'} with @code{C} the fixed-effect covariance; its sampling ## variance is @code{g'*Veta*g} where @code{g} is the gradient of @code{l*C*l'} ## with respect to the covariance parameters @var{eta} (the lower-triangle ## entries of each @var{Psi} block followed by @var{sigma2}) and @code{Veta} is ## the asymptotic covariance of @var{eta} (twice the inverse of the numerical ## Hessian of the -2 log-(RE)likelihood). The df is ## @code{2*(l*C*l')^2 / (g'*Veta*g)}. ## ## @end deftypefn function df = __lme_dfsatt__ (X, y, Zx, qk, nlev, Psi, sigma2, method, L) is_reml = strcmp (upper (method), "REML"); n = rows (X); p = columns (X); ## Pack the natural covariance parameters: vech(Psi_k) per term, then sigma2. eta = []; for k = 1:numel (qk) eta = [eta; vech_lower(Psi{k})]; endfor eta = [eta; sigma2]; ne = numel (eta); ## Every cross product below is free of eta, and the finite differences ask ## for O(ne^2) deviances, so forming them once is what keeps the whole ## Satterthwaite calculation out of the observation dimension. CP.ZtZ = choose_storage (Zx' * Zx); CP.ZtX = Zx' * X; CP.Zty = Zx' * y; CP.XtX = X' * X; CP.Xty = X' * y; CP.yty = y' * y; dev = @(e) reml_deviance (e, qk, nlev, CP, n, p, is_reml); cov = @(e) fixed_cov (e, qk, nlev, CP); ## Asymptotic covariance of eta: 2 * inv (Hessian of the -2 log-likelihood). hs = 1e-4 * max (abs (eta), 1); H = zeros (ne); for i = 1:ne for j = i:ne ei = zeros (ne, 1); ei(i) = hs(i); ej = zeros (ne, 1); ej(j) = hs(j); H(i,j) = (dev (eta+ei+ej) - dev (eta+ei-ej) ... - dev (eta-ei+ej) + dev (eta-ei-ej)) / (4 * hs(i) * hs(j)); H(j,i) = H(i,j); endfor endfor Veta = 2 * inv (H); C0 = cov (eta); df = zeros (rows (L), 1); for r = 1:rows (L) l = L(r,:); lcl = l * C0 * l'; g = zeros (ne, 1); for i = 1:ne ei = zeros (ne, 1); ei(i) = hs(i); g(i) = (l * cov (eta+ei) * l' - l * cov (eta-ei) * l') / (2 * hs(i)); endfor df(r) = 2 * lcl^2 / (g' * Veta * g); endfor endfunction ## Column-major lower-triangle entries of a symmetric matrix. function v = vech_lower (A) q = rows (A); v = []; for j = 1:q for i = j:q v(end+1, 1) = A(i, j); endfor endfor endfunction ## The block-diagonal random-effects covariance P and the residual variance, ## read out of eta. V is sigma2*I + Zx*P*Zx', which is never formed: with ## G = Zx'*Zx the identity ## ## inv (V) = I/s - Zx*P*inv (I + G*P/s)*Zx'/s^2 ## ## and its determinant counterpart det (V) = s^n * det (I + P*G/s) take every ## quantity into the random effects dimension. Note that this parameterises ## by the covariance rather than by a factor of it, and a finite difference ## drives a small variance component negative, so P is routinely indefinite ## here and no Cholesky of it exists. The eigenvalues of P*G are real, P and ## G being symmetric with G positive semidefinite, and they carry both the ## log-determinant and the test that replaces the old chol of V: V is positive ## definite exactly when s > 0 and every 1 + lambda/s is. function [P, s] = build_P (eta, qk, nlev) blocks = {}; off = 0; for k = 1:numel (qk) q = qk(k); m = q*(q+1)/2; Pk = tril_from_theta (eta(off+(1:m)), q); Pk = Pk + tril (Pk, -1)'; for l = 1:nlev(k) blocks{end+1} = Pk; endfor off += m; endfor P = blkdiag (blocks{:}); s = eta(end); endfunction ## X'*inv (V)*X, X'*inv (V)*y, y'*inv (V)*y and logdet (V), all in the random ## effects dimension, and ok false where V is not positive definite. When ## every Psi_k is positive semidefinite, P = L*L' and, with A = Zx*L, ## V = sigma2*I + A*A', so K = I + A'*A/sigma2 carries every quantity as it ## does in the fit, factorised sparse unless it fills in. A finite difference ## that drives a small variance component negative leaves P without such a ## factor, and vquad_eig takes over. function [XtViX, XtViy, ytViy, logdetV, ok] = vquad (eta, qk, nlev, CP, n) XtViX = XtViy = ytViy = logdetV = []; ok = false; s = eta(end); if (s <= 0) return; endif [L, psd] = build_L (eta, qk, nlev); if (! psd) [XtViX, XtViy, ytViy, logdetV, ok] = vquad_eig (eta, qk, nlev, CP, n); return; endif [Rk, flag, pk] = factor_K (L, CP.ZtZ / s); if (flag != 0) return; endif ok = true; logdetV = n * log (s) + 2 * sum (log (full (diag (Rk)))); LtZtX = L' * CP.ZtX / s; LtZty = L' * CP.Zty / s; Xt = Rk' \ LtZtX(pk,:); yt = Rk' \ LtZty(pk); XtViX = CP.XtX / s - Xt' * Xt; XtViX = (XtViX + XtViX') / 2; XtViy = CP.Xty / s - Xt' * yt; ytViy = CP.yty / s - yt' * yt; endfunction ## Sparse factor L of the block-diagonal P read out of eta, with P = L*L', and ## whether it exists: it does not when any Psi_k has a negative eigenvalue. function [L, psd] = build_L (eta, qk, nlev) blocks = cell (1, numel (qk)); off = 0; psd = true; L = []; for k = 1:numel (qk) q = qk(k); m = q*(q+1)/2; Pk = tril_from_theta (eta(off+(1:m)), q); Pk = Pk + tril (Pk, -1)'; [U, E] = eig (Pk); if (any (diag (E) < 0)) psd = false; return; endif blocks{k} = kron (speye (nlev(k)), sparse (U * sqrt (E))); off += m; endfor L = blkdiag (blocks{:}); endfunction ## The same through the eigenvalues of P*G, for a P with no real factor. ok ## is false exactly where V is not positive definite. function [XtViX, XtViy, ytViy, logdetV, ok] = vquad_eig (eta, qk, nlev, CP, n) [P, s] = build_P (eta, qk, nlev); XtViX = XtViy = ytViy = logdetV = []; ok = false; if (s <= 0) return; endif PG = P * full (CP.ZtZ); lam = real (eig (PG)); if (any (1 + lam / s <= 0)) return; endif ok = true; logdetV = n * log (s) + sum (log (1 + lam / s)); ## G*P is the transpose of P*G, both factors being symmetric. Wq = eye (columns (CP.ZtZ)) + PG' / s; Ax = P * (Wq \ CP.ZtX); Ay = P * (Wq \ CP.Zty); XtViX = CP.XtX / s - (CP.ZtX' * Ax) / s^2; XtViX = (XtViX + XtViX') / 2; XtViy = CP.Xty / s - (CP.ZtX' * Ay) / s^2; ytViy = CP.yty / s - (CP.Zty' * Ay) / s^2; endfunction function d = reml_deviance (eta, qk, nlev, CP, n, p, is_reml) [XtViX, XtViy, ytViy, logdetV, ok] = vquad (eta, qk, nlev, CP, n); if (! ok) d = Inf; return; endif beta = XtViX \ XtViy; rVir = ytViy - XtViy' * beta; if (is_reml) d = logdetV + rVir + 2*sum (log (diag (chol (XtViX)))) + (n-p)*log (2*pi); else d = logdetV + rVir + n*log (2*pi); endif endfunction function C = fixed_cov (eta, qk, nlev, CP) XtViX = vquad (eta, qk, nlev, CP, 1); C = inv (XtViX); endfunction statistics-release-2.1.2/inst/Regression/private/__lmefit__.m000066400000000000000000000244251527311104300243310ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{M} =} __lmefit__ (@var{X}, @var{y}, @var{Z}, @var{G}, @var{method}) ## ## Estimation core for linear mixed-effects models. Undocumented internal ## function for @code{fitlmematrix}/@code{fitlme}/@code{LinearMixedModel}. ## ## Fits @code{y = X*beta + Z*b + e} with @code{b ~ N(0, Psi)} and ## @code{e ~ N(0, sigma2*I)} by maximum likelihood (@var{method} = @qcode{"ML"}) ## or restricted maximum likelihood (@qcode{"REML"}). @var{Z} and @var{G} are ## cell arrays, one entry per grouping term: @var{Z}@{k@} is the @var{n}-by-q_k ## random-effects design and @var{G}@{k@} the @var{n}-by-1 grouping index of ## term k. Each grouping term has a full (unstructured) q_k-by-q_k covariance, ## shared across the levels of that term. ## ## Returns a struct with the fitted quantities: @code{beta}, @code{Psi} (cell, ## one covariance per term), @code{sigma2}, @code{b} (BLUPs), @code{loglik}, ## @code{covbeta}, the profiled covariance parameters @code{theta}, and the ## bookkeeping needed to rebuild the random design (@code{Zx}, @code{qk}, ## @code{nlev}, @code{levels}). ## ## @end deftypefn function M = __lmefit__ (X, y, Z, G, method) if (nargin < 5 || isempty (method)) method = "ML"; endif method = upper (method); is_reml = strcmp (method, "REML"); n = rows (X); p = columns (X); nt = numel (Z); ## Build the expanded random design Zx (n-by-N, N = sum_k q_k*nlev_k) and the ## per-term bookkeeping. Column block for (term k, level l) holds the rows of ## Z{k} that belong to level l and zeros elsewhere, so each row has only ## sum_k q_k non-zeros and Zx is held sparse. [Zx, qk, nlev, levels, gidx] = expand_random (Z, G, n); ## theta layout: lower-triangular Cholesky entries of the *relative* ## covariance D_k = L_k*L_k' (Psi_k = sigma2*D_k) per term, concatenated. theta0 = init_theta (qk); ## Every cross product the objective needs is free of theta, so it is formed ## once here rather than n-by-n matrices being rebuilt on every evaluation. CP = cross_products (X, y, Zx); obj = @(th) profiled_deviance (th, CP, qk, nlev, n, p, is_reml); ## The profiled deviance is differentiable in theta and the gradient is ## closed form, so give it to the optimiser rather than let it difference. ## A finite difference takes a step near 1e-8 on an objective whose ## curvature in the variance components is around 1e-13, which makes the ## search path, and the estimate it stops at, sensitive to the last bits of ## the linear algebra and so to the BLAS in use. opts = optimset ("TolX", 1e-10, "TolFun", 1e-10, "MaxFunEvals", 2000, ... "MaxIter", 1000, "Display", "off", "GradObj", "on"); [theta, dev] = fminunc (obj, theta0, opts); ## Recover everything at the optimum, through the same q-by-q factorisation ## the objective used. K is symmetric in exact arithmetic but not bitwise, ## the two products being separate calls, and 'chol' reads one triangle, so ## which one it reads decides the factor and a threaded or blocked BLAS can ## disagree with a reference one. Symmetrise before every factorisation. Lf = build_Lfull (theta, qk, nlev); [Rk, ~, pk] = factor_K (Lf, CP.ZtZ); LtZtX = Lf' * CP.ZtX; LtZty = Lf' * CP.Zty; Xt = Rk' \ LtZtX(pk,:); yt = Rk' \ LtZty(pk); XtMiX = CP.XtX - Xt' * Xt; XtMiX = (XtMiX + XtMiX') / 2; XtMiy = CP.Xty - Xt' * yt; beta = XtMiX \ XtMiy; rMir = (CP.yty - yt' * yt) - XtMiy' * beta; if (is_reml) sigma2 = rMir / (n - p); else sigma2 = rMir / n; endif ## BLUPs and covariance of the fixed effects. The BLUPs need Zx'*inv (M)*r ## rather than inv (M)*r itself, and Woodbury turns that into the random ## effects dimension too: with g = Zx'*r, inv (M)*r is r - Zx*L*inv (K)*L'*g, ## so Zx'*inv (M)*r is g - (Zx'*Zx)*L*inv (K)*L'*g and the n-vector is never ## formed. The covariance is genuinely an inverse, but XtMiX is symmetric ## positive definite, so take it from a factor rather than a general inverse. g = CP.Zty - CP.ZtX * beta; Ltg = Lf' * g; w = zeros (size (Ltg)); w(pk) = Rk \ (Rk' \ Ltg(pk)); b = Lf * (Lf' * (g - CP.ZtZ * (Lf * w))); Rx = chol (XtMiX); covbeta = sigma2 * (Rx \ (Rx' \ eye (p))); loglik = -0.5 * dev; ## Per-term covariance matrices Psi_k = sigma2 * D_k. Psi = cell (1, nt); off = 0; for k = 1:nt m = qk(k)*(qk(k)+1)/2; Lk = tril_from_theta (theta(off+(1:m)), qk(k)); Psi{k} = sigma2 * (Lk * Lk'); off += m; endfor M = struct (); M.beta = beta; M.Psi = Psi; M.sigma2 = sigma2; M.b = b; M.loglik = loglik; M.covbeta = covbeta; M.theta = theta; M.method = method; M.Zx = Zx; M.qk = qk; M.nlev = nlev; M.levels = levels; M.gidx = gidx; M.fitted = X * beta + Zx * b; ## conditional fitted values M.fitted_marg = X * beta; ## marginal (fixed-effects-only) fit M.resid = y - M.fitted; ## raw conditional residuals M.n = n; M.p = p; M.dfe = n - p; endfunction ## Profiled -2*log-likelihood at the covariance parameters theta, and its ## gradient. With M = I + Zx*D*Zx' and nu = n or n-p, the deviance is ## nu*log (rMir/nu) + logdet (M) [+ logdet (X'*inv (M)*X)] + const, and each ## term differentiates through dM/dtheta = Zx*(dD/dtheta)*Zx'. Because beta ## is the GLS minimiser, its own dependence on theta drops out. Every ## derivative then contracts to the random effects dimension: with ## D = L*L' gives dD = E_ij*L' + L*E_ij', each gradient entry is 2*(S*L)(i,j) ## for the matching accumulated S. ## ## Nothing here works in the observation dimension. With A = Zx*L and ## K = I_q + A'*A, the Woodbury identity gives inv (M) = I - A*inv (K)*A' and ## the matrix determinant lemma gives logdet (M) = logdet (K), so a q-by-q ## factorisation replaces the n-by-n one. Every quantity the deviance and its ## gradient need has the form P'*inv (M)*Q, which is P'*Q - (Rk'\(A'*P))' * ## (Rk'\(A'*Q)); each P'*Q and A'*P reduces to a cross product free of theta, ## so the caller forms those once and the evaluation costs q cubed rather than ## n cubed. function [dev, grad] = profiled_deviance (theta, CP, qk, nlev, n, p, is_reml) Lf = build_Lfull (theta, qk, nlev); ## Lf is sparse block diagonal, which keeps both products inside the block ## structure, and K is factorised sparse unless it fills in. [Rk, flag, pk] = factor_K (Lf, CP.ZtZ); if (flag != 0) dev = Inf; grad = zeros (size (theta)); return; endif LtZtX = Lf' * CP.ZtX; LtZty = Lf' * CP.Zty; Xt = Rk' \ LtZtX(pk,:); yt = Rk' \ LtZty(pk); XtMiX = CP.XtX - Xt' * Xt; XtMiX = (XtMiX + XtMiX') / 2; XtMiy = CP.Xty - Xt' * yt; ytMiy = CP.yty - yt' * yt; ## beta is the GLS minimiser, so r'*inv (M)*r collapses to one inner product ## once it is in hand. Solving p normal equations is what the whitened ## n-by-n form avoided; p is the fixed-effects count and small, and the ## deviance depends on beta only to second order because beta minimises it. beta = XtMiX \ XtMiy; rMir = ytMiy - XtMiy' * beta; logdetM = 2 * sum (log (full (diag (Rk)))); if (is_reml) nu = n - p; Rx = chol (XtMiX); ldXMiX = 2 * sum (log (diag (Rx))); dev = nu*log (rMir/nu) + logdetM + rMir/(rMir/nu) + ldXMiX + nu*log (2*pi); else nu = n; dev = nu*log (rMir/nu) + logdetM + rMir/(rMir/nu) + nu*log (2*pi); endif if (nargout < 2) return; endif ## One further triangular solve carries the whole gradient: c is ## Zx'*inv (M)*r and B is Zx'*inv (M)*X. Of ZtMiZ = Zx'*inv (M)*Zx only ## the per-level diagonal blocks are needed, and they are taken entry by ## entry, since the whole product Zt'*Zt fills in. LtZtZ = Lf' * CP.ZtZ; Zt = Rk' \ LtZtZ(pk,:); B = CP.ZtX - Zt' * Xt; c = (CP.Zty - Zt' * yt) - B * beta; if (is_reml) T = Rx' \ B'; endif grad = zeros (size (theta)); off = 0; col = 0; for k = 1:numel (qk) q = qk(k); m = q*(q+1)/2; Lk = tril_from_theta (theta(off+(1:m)), q); Sa = zeros (q); base = col + (0:nlev(k)-1)' * q; for j = 1:q for i = 1:q ci = base + i; cj = base + j; Sa(i,j) = full (sum (CP.ZtZ(sub2ind (size (CP.ZtZ), ci, cj))) ... - sum (sum (Zt(:,ci) .* Zt(:,cj)))); endfor endfor Sc = zeros (q); Sp = zeros (q); for l = 1:nlev(k) S = col + (1:q); col += q; cs = c(S); Sc += cs * cs'; if (is_reml) Ts = T(:,S); Sp += Ts' * Ts; endif endfor ## dD = E_ij*L' + L*E_ij', so tr (dD*S) and v'*dD*v are both 2*(S*L)(i,j) ## for symmetric S. logdet (M) rises with it, rMir and logdet (X'MiX) ## fall. Ga = 2 * (Sa * Lk); Gc = 2 * (Sc * Lk); if (is_reml) Gp = 2 * (Sp * Lk); else Gp = zeros (q); endif idx = 0; for j = 1:q for i = j:q idx += 1; grad(off+idx) = -nu * Gc(i,j) / rMir + Ga(i,j) - Gp(i,j); endfor endfor off += m; endfor endfunction ## The cross products the profiled deviance needs. All are free of theta, so ## they are formed once per fit and the optimiser never touches X, y or Zx. function CP = cross_products (X, y, Zx) CP.ZtZ = choose_storage (Zx' * Zx); CP.ZtX = Zx' * X; CP.Zty = Zx' * y; CP.XtX = X' * X; CP.Xty = X' * y; CP.yty = y' * y; endfunction ## Starting covariance parameters: identity relative factors (D_k = I). function theta0 = init_theta (qk) theta0 = []; for k = 1:numel (qk) L = eye (qk(k)); th = []; for j = 1:qk(k) for i = j:qk(k) th(end+1) = L(i, j); endfor endfor theta0 = [theta0, th]; endfor theta0 = theta0(:); endfunction statistics-release-2.1.2/inst/Regression/private/build_Lfull.m000066400000000000000000000032431527311104300245050ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{Lf} =} build_Lfull (@var{theta}, @var{qk}, @var{nlev}) ## ## Block-diagonal relative covariance factor of a mixed model. ## ## @var{theta} holds, term after term, the q_k*(q_k+1)/2 column-major ## lower-triangle entries of the factor L_k of each term's relative covariance. ## @var{Lf} is the sparse block-diagonal matrix with L_k repeated over the ## @var{nlev}(k) levels of term k, held sparse so that products against it stay ## inside the block structure. ## ## This helper is shared by @code{__lmefit__} and @code{__glmefit__}. ## ## @end deftypefn function Lf = build_Lfull (theta, qk, nlev) blocks = cell (1, numel (qk)); off = 0; for k = 1:numel (qk) m = qk(k)*(qk(k)+1)/2; Lk = sparse (tril_from_theta (theta(off+(1:m)), qk(k))); blocks{k} = kron (speye (nlev(k)), Lk); off += m; endfor Lf = blkdiag (blocks{:}); endfunction statistics-release-2.1.2/inst/Regression/private/build_design.m000066400000000000000000000041511527311104300246770ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## Copyright (C) 2026 Avanish Salunke ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{X_design} =} build_design (@var{terms}, @var{X_enc}) ## ## Build a model design matrix from a terms matrix and an encoded predictor ## matrix. ## ## @var{X_enc} is the @math{n}-by-@math{p} encoded predictor matrix (categorical ## columns already expanded to indicator variables) and @var{terms} is an ## @math{m}-by-(@math{p}+1) matrix whose @math{t}-th row gives the exponents of ## each encoded predictor in the @math{t}-th model term (the trailing column, ## reserved for the response, is ignored). A row of all zeros denotes the ## intercept. ## ## The returned @var{X_design} is @math{n}-by-@math{m}; column @math{t} is the ## element-wise product of the encoded predictors raised to their exponents in ## term @math{t}. ## ## This helper is shared by the @code{LinearModel} and ## @code{GeneralizedLinearModel} classes. ## ## @end deftypefn function X_design = build_design (terms, X_enc) n_obs = rows (X_enc); n_coef = rows (terms); p_enc = columns (X_enc); X_design = zeros (n_obs, n_coef); for t = 1:n_coef term_row = terms(t, 1:p_enc); col_t = ones (n_obs, 1); for j = find (term_row != 0) col_t = col_t .* (X_enc(:, j) .^ term_row(j)); endfor X_design(:, t) = col_t; endfor endfunction statistics-release-2.1.2/inst/Regression/private/choose_storage.m000066400000000000000000000033101527311104300252470ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{ZtZ} =} choose_storage (@var{ZtZ}) ## ## Choose sparse or full storage for the random-effects cross product ## @var{ZtZ}, which @code{factor_K} follows. ## ## @code{K = I + L'*ZtZ*L} has the non-zero pattern of @var{ZtZ} whatever the ## covariance parameters are, so the choice is made once, from the fill of its ## Cholesky factor estimated under an approximate minimum degree order. Sparse ## is faster up to about a fifth of the triangle filled, as with nested or ## few-level crossed terms; crossed terms with many levels each fill in, and ## @var{ZtZ} is returned full. ## ## This helper is shared by @code{__lmefit__}, @code{__glmefit__} and ## @code{__lme_dfsatt__}. ## ## @end deftypefn function ZtZ = choose_storage (ZtZ) q = columns (ZtZ); P = spones (ZtZ) + speye (q); o = amd (P); if (sum (symbfact (P(o,o))) > 0.2 * q * (q + 1) / 2) ZtZ = full (ZtZ); endif endfunction statistics-release-2.1.2/inst/Regression/private/encode_categorical.m000066400000000000000000000076371527311104300260550ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## Copyright (C) 2026 Avanish Salunke ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{X_enc}, @var{enc_names}, @var{cat_info}] =} encode_categorical (@var{X_num}, @var{cat_cols}, @var{pred_names}, @var{cat_levels}, @var{has_intercept}) ## ## Encode categorical predictors as reference-coded indicator (dummy) variables. ## ## @var{X_num} is the @math{n}-by-@math{p} numeric predictor matrix (categorical ## columns hold 1-based level codes), @var{cat_cols} is a logical vector marking ## the categorical columns, @var{pred_names} is the cell array of predictor ## names, and @var{cat_levels} is a cell array whose @math{j}-th entry lists the ## level labels of predictor @math{j} (empty to infer them from the data). ## ## @var{has_intercept} states whether the model carries an intercept and ## defaults to true. ## ## Numeric predictors are copied through unchanged. Each categorical predictor ## with @math{k} levels expands to @math{k - 1} indicator columns (the first ## level is the omitted reference), named @qcode{@var{name}_@var{level}}. ## ## Without an intercept the @emph{first} categorical predictor takes its place ## and is given all @math{k} indicator columns, which is the cell-means ## parameterisation that dropping the intercept asks for. Any further ## categorical predictor stays reference coded: giving two of them a full set of ## indicators would make the design rank deficient, since each set sums to the ## intercept column. ## ## @var{X_enc} is the encoded design matrix, @var{enc_names} the cell array of ## its column names, and @var{cat_info} a structure with fields @qcode{names} ## and @qcode{levels} recording the categorical predictors and the level labels ## used, so the same encoding can be reproduced for new data. ## ## This helper is shared by the @code{LinearModel} and ## @code{GeneralizedLinearModel} classes. ## ## @end deftypefn function [X_enc, enc_names, cat_info] = encode_categorical ( ... X_num, cat_cols, pred_names, cat_levels, has_intercept) if (nargin < 5) has_intercept = true; endif ## Set once the intercept's place has been taken, whether by an actual ## intercept or by the first categorical predictor standing in for it. reference_coded = has_intercept; X_enc = zeros (rows (X_num), 0); enc_names = {}; cat_info.names = {}; cat_info.levels = {}; for j = 1:numel (pred_names) if (! cat_cols(j)) X_enc = [X_enc, X_num(:, j)]; enc_names = [enc_names, pred_names{j}]; else levels_j = cat_levels{j}; if (isempty (levels_j)) uvals = sort (unique (X_num(isfinite (X_num(:,j)), j))); levels_j = strtrim (cellstr (num2str (uvals(:)))); endif n_lev = numel (levels_j); if (reference_coded) first_lev = 2; else first_lev = 1; reference_coded = true; endif for L = first_lev:n_lev dummy = double (X_num(:, j) == L); X_enc = [X_enc, dummy]; enc_names = [enc_names, [pred_names{j}, '_', char(levels_j{L})]]; endfor cat_info.names{end+1} = pred_names{j}; cat_info.levels{end+1} = levels_j; endif endfor endfunction statistics-release-2.1.2/inst/Regression/private/encodednames_to_row.m000066400000000000000000000053551527311104300262740ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{map}, @var{pow}] =} encodednames_to_row (@var{col_names}, @var{pred_names}, @var{cat_info}) ## ## Map each encoded design column back onto the predictor it came from. ## ## @var{col_names} lists the names of the columns to be mapped, @var{pred_names} ## the model's predictors, and @var{cat_info} the structure of categorical names ## and levels used to encode them. ## ## @var{map} is a row vector holding, for each encoded column, the index of its ## predictor in @var{pred_names}, or zero when no predictor claims it. A ## categorical predictor's indicator columns are named @qcode{@var{name}_@var{level}}, ## so a column is matched by the longest predictor name it starts with, which ## keeps @qcode{ab_x} with @qcode{ab} rather than with @qcode{a}. ## ## @var{pow} is a row vector holding the exponent each column name carries, so ## that a column named @qcode{u^2} maps onto predictor @qcode{u} with an ## exponent of 2. It is 1 for a name carrying no exponent. ## ## This helper is shared by the @code{LinearModel} and ## @code{GeneralizedLinearModel} classes. ## ## @end deftypefn function [m, pow] = encodednames_to_row (col_names, pred_names, cat_info) m = zeros (1, numel (col_names)); pow = ones (1, numel (col_names)); for k = 1:numel (col_names) nm = col_names{k}; ## A column named 'u^2' belongs to predictor 'u' and carries the exponent. tok = regexp (nm, '^(.*)\^(\d+)$', 'tokens'); if (! isempty (tok)) nm = tok{1}{1}; pow(k) = str2double (tok{1}{2}); endif idx = find (strcmp (pred_names, nm), 1); if (isempty (idx)) idx = 0; best_len = 0; for j = 1:numel (pred_names) pj = pred_names{j}; if (numel (nm) > numel (pj) + 1 ... && strncmp (nm, [pj, '_'], numel (pj) + 1) ... && numel (pj) > best_len) idx = j; best_len = numel (pj); endif endfor endif m(k) = idx; endfor endfunction statistics-release-2.1.2/inst/Regression/private/expand_random.m000066400000000000000000000043761527311104300250770ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{Zx}, @var{qk}, @var{nlev}, @var{levels}, @var{gidx}] =} expand_random (@var{Z}, @var{G}, @var{n}) ## ## Build the expanded random-effects design of a mixed model. ## ## @var{Z} and @var{G} are cell arrays with one entry per grouping term: ## @var{Z}@{k@} is the @var{n}-by-q_k random-effects design and @var{G}@{k@} the ## @var{n}-by-1 grouping variable of term k. @var{Zx} is the sparse ## @var{n}-by-N matrix, N = sum_k q_k*nlev_k, whose column block for term k and ## level l holds the rows of @var{Z}@{k@} that belong to level l and zeros ## elsewhere. @var{qk} and @var{nlev} are the per-term column and level counts, ## @var{levels}@{k@} the sorted levels of term k and @var{gidx}@{k@} the level ## index of each observation. ## ## This helper is shared by @code{__lmefit__} and @code{__glmefit__}. ## ## @end deftypefn function [Zx, qk, nlev, levels, gidx] = expand_random (Z, G, n) nt = numel (Z); N = 0; qk = zeros (1, nt); nlev = zeros (1, nt); levels = cell (1, nt); gidx = cell (1, nt); I = J = V = cell (1, nt); for k = 1:nt qk(k) = columns (Z{k}); [lev, ~, gi] = unique (G{k}(:)); nlev(k) = numel (lev); levels{k} = lev; gidx{k} = gi; I{k} = repmat ((1:n)', qk(k), 1); J{k} = N + repmat ((gi - 1) * qk(k), qk(k), 1) ... + kron ((1:qk(k))', ones (n, 1)); V{k} = Z{k}(:); N += qk(k) * nlev(k); endfor Zx = sparse (vertcat (I{:}), vertcat (J{:}), vertcat (V{:}), n, N); endfunction statistics-release-2.1.2/inst/Regression/private/factor_K.m000066400000000000000000000034311527311104300237770ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{Rk}, @var{flag}, @var{pk}] =} factor_K (@var{L}, @var{ZtZ}) ## ## Cholesky factor of the mixed-model matrix @code{K = I + L'*ZtZ*L}. ## ## @var{Rk} is taken in the row order @var{pk}, so that @code{Rk'*Rk} is ## @code{K(pk,pk)}, and @var{flag} is nonzero when @var{K} is not positive ## definite. @var{K} is factorised sparse, under a fill-reducing order, when ## @var{ZtZ} is sparse, and full otherwise, with @var{pk} the identity order. ## @var{K} is symmetric in exact arithmetic but not bitwise, the two products ## being separate calls, and @code{chol} reads one triangle, so it is ## symmetrised first. ## ## This helper is shared by @code{__lmefit__}, @code{__glmefit__} and ## @code{__lme_dfsatt__}. ## ## @end deftypefn function [Rk, flag, pk] = factor_K (L, ZtZ) K = speye (columns (ZtZ)) + L' * ZtZ * L; K = (K + K') / 2; if (issparse (K)) [Rk, flag, pk] = chol (K, "vector"); else [Rk, flag] = chol (K); pk = 1:columns (K); endif endfunction statistics-release-2.1.2/inst/Regression/private/getlinkfunctions.m000066400000000000000000000144231527311104300256400ustar00rootroot00000000000000## Copyright (C) 2025 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{flink}, @var{dlink}, @var{ilink}, @var{errmsg}] =} getlinkfunctions (@var{linkArg}) ## ## Return the link function and its derivative and inverse base. ## ## @end deftypefn function [flink, dlink, ilink, errmsg] = getlinkfunctions (linkArg) flink = dlink = ilink = []; errmsg = ''; ## linkArg is a scalar structure if (isstruct (linkArg)) if (! isscalar (linkArg)) errmsg = 'structure with custom link functions must be a scalar.'; return; endif rf = {'Link', 'Derivative', 'Inverse'}; if (! all (ismember (rf, fieldnames (linkArg)))) errmsg = ['structure with custom link functions requires', ... ' the fields ''Link'', ''Derivative'', and ''Inverse''.']; return; endif if (ischar (linkArg.Link) && ! isempty (which (linkArg.Link))) flink = @(mu) feval (linkArg.Link, mu); elseif (isa (linkArg.Link, 'function_handle')) flink = linkArg.Link; else errmsg = ['bad ''Link'' function in custom link function structure.']; return; endif errmsg = testLinkFunction (flink, 'Link'); if (! isempty (errmsg)) return; endif if (ischar (linkArg.Derivative) && ! isempty (which (linkArg.Derivative))) dlink = @(mu) feval (linkArg.Derivative, mu); elseif (isa (linkArg.Derivative, 'function_handle')) dlink = linkArg.Derivative; else errmsg = ['bad ''Derivative'' function in custom link function structure.']; return; endif errmsg = testLinkFunction (dlink, 'Derivative'); if (! isempty (errmsg)) return; endif if (ischar (linkArg.Inverse) && ! isempty (which (linkArg.Inverse))) ilink = @(mu) feval (linkArg.Inverse, mu); elseif (isa (linkArg.Inverse, 'function_handle')) ilink = linkArg.Inverse; else errmsg = ['bad ''Inverse'' function in custom link function structure.']; return; endif errmsg = testLinkFunction (ilink, 'Inverse'); if (! isempty (errmsg)) return; endif ## linkArg is a cell array elseif (iscell (linkArg)) if (numel (linkArg) != 3) errmsg = 'cell array with custom link functions must have three elements.'; return; endif if (isa (linkArg{1}, 'function_handle')) flink = linkArg{1}; else errmsg = ['bad ''Link'' function in custom link function cell array.']; return; endif errmsg = testLinkFunction (flink, 'Link'); if (! isempty (errmsg)) return; endif if (isa (linkArg{2}, 'function_handle')) dlink = linkArg{2}; else errmsg = ['bad ''Derivative'' function in custom link function cell array.']; return; endif errmsg = testLinkFunction (dlink, 'Derivative'); if (! isempty (errmsg)) return; endif if (isa (linkArg{3}, 'function_handle')) ilink = linkArg{3}; else errmsg = ['bad ''Inverse'' function in custom link function cell array.']; return; endif errmsg = testLinkFunction (ilink, 'Inverse'); if (! isempty (errmsg)) return; endif ## linkArg is a scalar value elseif (isnumeric (linkArg)) if (! (isscalar (linkArg) && isfinite (linkArg) && isreal (linkArg))) errmsg = ['numeric input for custom link function', ... ' must be a finite real scalar value.']; return; endif flink = @(mu) mu .^ linkArg; dlink = @(mu) linkArg .* mu .^ (linkArg - 1); ilink = @(eta) eta .^ (1 / linkArg); ## linkArg is character vector elseif (ischar (linkArg)) if (! isvector (linkArg)) errmsg = 'canonical link function name must be a character vector.'; return; endif supported_link_functions = {'identity', 'log', 'logit', 'probit', ... 'loglog', 'comploglog', 'reciprocal'}; if (! any (strcmpi (linkArg, supported_link_functions))) errmsg = sprintf ("canonical link function '%s' is not supported.", ... linkArg); return; endif ## Select a canonical link function switch (linkArg) case 'identity' flink = @(mu) mu; dlink = @(mu) 1; ilink = @(eta) eta; case 'log' flink = @(mu) log (mu); dlink = @(mu) 1 ./ (mu); ilink = @(eta) exp (eta); case 'logit' flink = @(mu) log (mu ./ (1 - mu)); dlink = @(mu) 1 ./ (mu .* (1 - mu)); ilink = @(eta) 1 ./ (1 + exp (- eta)); case 'probit' flink = @(mu) norminv (mu); dlink = @(mu) 1 ./ normpdf (norminv (mu)); ilink = @(eta) normcdf (eta); case 'loglog' flink = @(mu) log (- log (mu)); dlink = @(mu) 1 ./ (mu .* log (mu)); ilink = @(eta) exp (- exp (eta)); case 'comploglog' flink = @(mu) log (- log1p (- mu)); dlink = @(mu) 1 ./ - ((1 - mu) .* log1p (- mu)); ilink = @(eta) -expm1 (- exp (eta)); case 'reciprocal' flink = @(mu) 1 ./ mu; dlink = @(mu) -1 ./ (mu .^ 2); ilink = @(eta) 1 ./ (eta); endswitch else errmsg = 'invalid value for custom link function.'; endif endfunction function errmsg = testLinkFunction (flink, linkname); errmsg = ''; testInput = [1; 2; 3; 4; 5]; try testOutput = flink(testInput); if (! isequal (size (testInput), size (testOutput))) errmsg = sprintf (['custom ''%s'' function must return an output', ... ' of the same size as input.'], linkname); endif catch errmsg = sprintf ("invalid custom '%s' function.", linkname); end_try_catch endfunction statistics-release-2.1.2/inst/Regression/private/madsigma.m000066400000000000000000000043621527311104300240350ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## Copyright (C) 2026 Avanish Salunke ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{s} =} madsigma (@var{r}, @var{p}) ## @deftypefnx {Private Function} {@var{s} =} madsigma (@var{r}, @var{p}, @var{y}) ## ## Robust scale from the median absolute deviation of the residuals @var{r}, ## dropping the @math{@var{p}-1} smallest to account for the fitted parameters. ## ## Given the response @var{y}, the scale is floored at @code{1e-6 * std (y)}, ## which is what an iteration needs and what the reported scale must not have. ## ## @seealso{robustfit, nlinfit} ## @end deftypefn function s = madsigma (r, p, y) rs = sort (abs (r)); s = median (rs(max (1, p):end)) / 0.6745; ## The scale that weights an iteration is floored relative to the response, ## so a fit that is already exact does not grade its weights out of rounding ## noise: on y = 3x through the origin the residuals are 1e-15 and the ## unfloored scale is 1e-15 with them, leaving the ratios order one and the ## weights graded where MATLAB returns ones. Measured on R2024a, on a ## fixture where std (Y), norm (Y) and max (abs (Y)) differ by six orders of ## magnitude: the floor is 1e-6 * std (Y). The scale reported back to a ## caller is the unfloored one, so the covariance step asks for two ## arguments and only an iteration asks for three. if (nargin > 2) tiny = 1e-6 * std (y); if (tiny == 0) tiny = 1; endif s = max (s, tiny); endif endfunction statistics-release-2.1.2/inst/Regression/private/modelspec_has_intercept.m000066400000000000000000000040571527311104300271370ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{tf} =} modelspec_has_intercept (@var{modelspec}, @var{intercept_nv}) ## ## Report whether a model specification carries an intercept. ## ## @var{modelspec} is a keyword, a numeric terms matrix, or empty, and ## @var{intercept_nv} is the @qcode{'Intercept'} name-value flag. ## ## @var{tf} is true when the fitted model will hold an intercept term. Every ## keyword specification supplies one, so for those the flag decides alone; a ## numeric terms matrix supplies one only if it holds an all-zero row. ## ## The answer is needed @emph{before} the predictors are encoded, because ## whether a categorical predictor is given all its indicator columns or only ## @math{k - 1} of them depends on it. It is available that early: an all-zero ## row is all-zero whether or not the matrix carries its optional trailing ## response column, so the encoded width is never needed to decide. ## ## This helper is shared by the @code{LinearModel} and ## @code{GeneralizedLinearModel} classes. ## ## @end deftypefn function tf = modelspec_has_intercept (modelspec, intercept_nv) if (! intercept_nv) tf = false; elseif (isnumeric (modelspec) && ! isempty (modelspec)) tf = any (all (double (modelspec) == 0, 2)); else tf = true; endif endfunction statistics-release-2.1.2/inst/Regression/private/nlfun_jacobian.m000066400000000000000000000030511527311104300252150ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{J} =} nlfun_jacobian (@var{modelfun}, @var{beta}, @var{X}, @var{derivstep}) ## ## Numeric Jacobian of @var{modelfun} with respect to @var{beta} by forward ## differences, evaluated at @var{X}. Internal helper shared by @code{nlinfit} ## and @code{nlpredci}; not intended to be called directly. ## ## @end deftypefn function J = nlfun_jacobian (modelfun, beta, X, derivstep) beta = beta(:); f0 = modelfun (beta, X); f0 = f0(:); n = numel (f0); p = numel (beta); J = zeros (n, p); for j = 1:p h = derivstep * max (abs (beta(j)), 1); if (h == 0) h = derivstep; endif bj = beta; bj(j) = bj(j) + h; fj = modelfun (bj, X); J(:,j) = (fj(:) - f0) / h; endfor endfunction statistics-release-2.1.2/inst/Regression/private/parse_modelspec.m000066400000000000000000000117441527311104300254220ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## Copyright (C) 2026 Avanish Salunke ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{terms}, @var{has_intercept}, @var{coef_names}, @var{errmsg}] =} parse_modelspec (@var{modelspec}, @var{pred_names}, @var{n_preds}, @var{intercept_nv}) ## ## Turn a model specification into a terms matrix and coefficient names. ## ## @var{modelspec} is either a keyword (@qcode{'constant'}, @qcode{'linear'}, ## @qcode{'interactions'}, @qcode{'purequadratic'}, @qcode{'quadratic'}, or ## @qcode{'full'}) or a numeric terms matrix; @var{pred_names} lists the ## @var{n_preds} encoded predictor names, and @var{intercept_nv} is a logical ## flag requesting an intercept term. ## ## @var{terms} is the resulting @math{m}-by-(@var{n_preds}+1) terms matrix, ## @var{has_intercept} indicates whether an intercept row is present, and ## @var{coef_names} are the term (coefficient) names. On invalid input the ## outputs are empty and @var{errmsg} holds a diagnostic message body; the ## caller should emit it under its own function name. @var{errmsg} is empty on ## success. ## ## This helper is shared by the @code{LinearModel} and ## @code{GeneralizedLinearModel} classes. ## ## @end deftypefn function [terms, has_intercept, coef_names, errmsg] = parse_modelspec ( ... modelspec, pred_names, n_preds, intercept_nv) terms = []; has_intercept = []; coef_names = {}; errmsg = ''; p = n_preds; if (isempty (modelspec) ... || (ischar (modelspec) && strcmpi (modelspec, 'linear'))) terms = [zeros(1, p+1); [eye(p), zeros(p, 1)]]; elseif (ischar (modelspec) && strcmpi (modelspec, 'constant')) terms = zeros (1, p+1); elseif (ischar (modelspec) && strcmpi (modelspec, 'interactions')) linear_part = [zeros(1, p+1); [eye(p), zeros(p, 1)]]; inter_part = zeros (0, p+1); for i = 1:p for j = i+1:p row = zeros (1, p+1); row(i) = 1; row(j) = 1; inter_part = [inter_part; row]; endfor endfor terms = [linear_part; inter_part]; elseif (ischar (modelspec) && strcmpi (modelspec, 'purequadratic')) linear_part = [zeros(1, p+1); [eye(p), zeros(p, 1)]]; quad_part = zeros (p, p+1); for j = 1:p quad_part(j, j) = 2; endfor terms = [linear_part; quad_part]; elseif (ischar (modelspec) && strcmpi (modelspec, 'quadratic')) linear_part = [zeros(1, p+1); [eye(p), zeros(p, 1)]]; quad_part = zeros (p, p+1); for j = 1:p quad_part(j, j) = 2; endfor inter_part = zeros (0, p+1); for i = 1:p for j = i+1:p row = zeros (1, p+1); row(i) = 1; row(j) = 1; inter_part = [inter_part; row]; endfor endfor terms = [linear_part; inter_part; quad_part]; elseif (ischar (modelspec) && strcmpi (modelspec, 'full')) terms = zeros (1, p+1); for k = 1:p idx_mat = nchoosek (1:p, k); for j = 1:rows (idx_mat) row = zeros (1, p+1); row(idx_mat(j,:)) = 1; terms = [terms; row]; endfor endfor elseif (isnumeric (modelspec)) terms = double (modelspec); if (size (terms, 2) == p) terms = [terms, zeros(rows (terms), 1)]; elseif (size (terms, 2) == p + 1) if (! all (terms(:, end) == 0)) terms = []; errmsg = "Last column of terms matrix must be all zeros."; return; endif else terms = []; errmsg = sprintf ("Terms matrix must have %d or %d columns.", p, p+1); return; endif else errmsg = "Unknown model specification."; return; endif if (! intercept_nv) int_rows = all (terms(:, 1:end-1) == 0, 2); terms = terms(! int_rows, :); endif has_intercept = any (all (terms(:, 1:end-1) == 0, 2)); n_terms = rows (terms); coef_names = cell (1, n_terms); for t = 1:n_terms term_row = terms(t, 1:end-1); if (all (term_row == 0)) coef_names{t} = '(Intercept)'; else parts_t = {}; for j = 1:numel (term_row) if (term_row(j) != 0) if (term_row(j) == 1) parts_t{end+1} = pred_names{j}; else parts_t{end+1} = sprintf ("%s^%d", pred_names{j}, term_row(j)); endif endif endfor coef_names{t} = strjoin (parts_t, ':'); endif endfor endfunction statistics-release-2.1.2/inst/Regression/private/raw_to_codes.m000066400000000000000000000057311527311104300247240ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{X_num}, @var{cat_levels}] =} raw_to_codes (@var{data}, @var{X_raw}, @var{tbl}, @var{pred_names}, @var{cat_logical}, @var{n_total}) ## ## Convert raw predictor data to numeric level codes for categorical columns. ## ## @var{data} is the predictor data as the caller received it, and decides which ## branch is taken: a table is read variable by variable through @var{tbl}, ## anything else column by column through the numeric matrix @var{X_raw}. ## @var{pred_names} names the predictors, @var{cat_logical} marks the ones to ## treat as categorical, and @var{n_total} is the number of observations. ## ## @var{X_num} holds the numeric predictors unchanged and 1-based level codes ## for the categorical ones, and @var{cat_levels} lists the level labels of each ## predictor, empty for a numeric one. The output feeds ## @code{encode_categorical}, which turns the codes into indicator columns. ## ## This helper is shared by the @code{CoxModel} and ## @code{GeneralizedLinearModel} classes. ## ## @end deftypefn function [X_num, cat_levels] = raw_to_codes (data, X_raw, tbl, pred_names, ... cat_logical, n_total) p = numel (pred_names); X_num = zeros (n_total, p); cat_levels = cell (1, p); for j = 1:p if (istable (data)) col = tbl.(pred_names{j}); if (iscell (col)) ## Appearance order, so that the omitted reference level is the one the ## data shows first; see parseWilkinsonFormula for the formula path. [cat_levels{j}, ~, ic] = unique (col, 'stable'); X_num(:, j) = ic; elseif (isa (col, 'categorical')) cat_levels{j} = categories (col); [~, ic] = ismember (cellstr (col), cat_levels{j}); X_num(:, j) = ic; else X_num(:, j) = double (col(:)); cat_levels{j} = {}; endif else if (cat_logical(j)) uvals = sort (unique (X_raw(isfinite (X_raw(:,j)), j))); cat_levels{j} = strtrim (cellstr (num2str (uvals(:)))); [~, ic] = ismember (X_raw(:,j), uvals); X_num(:, j) = ic; else X_num(:, j) = X_raw(:, j); cat_levels{j} = {}; endif endif endfor endfunction statistics-release-2.1.2/inst/Regression/private/reencode_predictors.m000066400000000000000000000060241527311104300262720ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## Copyright (C) 2026 Avanish Salunke ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{X_enc} =} reencode_predictors (@var{X_raw}, @var{pred_names}, @var{cat_info}, @var{enc_names}) ## ## Re-encode raw predictor data to match a fitted model's encoded columns. ## ## Given new raw predictor data @var{X_raw} (numeric, one column per predictor ## in @var{pred_names}), the categorical level information @var{cat_info} ## recorded at fit time (fields @qcode{names} and @qcode{levels}), and the ## target encoded column names @var{enc_names}, return the ## @math{n}-by-@code{numel (enc_names)} matrix @var{X_enc} whose columns ## reproduce, in order, the encoding used when the model was fitted. ## ## Each encoded name is matched as a plain predictor, a power term ## @qcode{name^k}, or a categorical indicator @qcode{name_level}, so the result ## aligns with the fitted design. When @var{enc_names} is omitted it defaults ## to @var{pred_names} (plain pass-through). ## ## This helper is shared by the @code{LinearModel} and ## @code{GeneralizedLinearModel} classes. ## ## @end deftypefn function X_enc = reencode_predictors (X_raw, pred_names, cat_info, enc_names) if (nargin < 4) enc_names = pred_names; endif n = rows (X_raw); X_enc = zeros (n, numel (enc_names)); for c = 1:numel (enc_names) name = enc_names{c}; j = find (strcmp (pred_names, name), 1); if (! isempty (j)) X_enc(:, c) = X_raw(:, j); continue; endif tok = regexp (name, '^(.+)\^(\d+)$', 'tokens'); if (! isempty (tok)) j = find (strcmp (pred_names, tok{1}{1}), 1); k = str2double (tok{1}{2}); X_enc(:, c) = X_raw(:, j) .^ k; continue; endif found = false; for j = 1:numel (pred_names) ci = []; if (! isempty (cat_info.names)) ci = find (strcmp (cat_info.names, pred_names{j})); endif if (isempty (ci)) continue; endif levels_j = cat_info.levels{ci}; for L = 2:numel (levels_j) if (strcmp (name, sprintf ("%s_%s", pred_names{j}, char (levels_j{L})))) X_enc(:, c) = double (X_raw(:, j) == L); found = true; break; endif endfor if (found) break; endif endfor endfor endfunction statistics-release-2.1.2/inst/Regression/private/robusttune.m000066400000000000000000000032051527311104300244600ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## Copyright (C) 2026 Avanish Salunke ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{t} =} robusttune (@var{wfun}) ## ## Default tuning constant, giving 95% efficiency, for a robust weight function. ## ## @var{wfun} is one of the names @code{robustwfun} accepts, or a function ## handle, which carries no default and takes 1. ## ## @seealso{robustfit, nlinfit, robustwfun} ## @end deftypefn function t = robusttune (wfun) if (is_function_handle (wfun)) t = 1; return; endif switch (wfun) case "andrews"; t = 1.339; case "bisquare"; t = 4.685; case "cauchy"; t = 2.385; case "fair"; t = 1.400; case "huber"; t = 1.345; case "logistic"; t = 1.205; case "ols"; t = 1; case "talwar"; t = 2.795; case "welsch"; t = 2.985; endswitch endfunction statistics-release-2.1.2/inst/Regression/private/robustwfun.m000066400000000000000000000064521527311104300244730ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## Copyright (C) 2026 Avanish Salunke ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{w}, @var{psi}, @var{psip}] =} robustwfun (@var{z}, @var{wfun}) ## ## Weight, influence function and its derivative for a robust weight function. ## ## @var{z} is the vector of residuals already scaled by the robust scale and the ## tuning constant, and @var{wfun} is one of @qcode{"andrews"}, ## @qcode{"bisquare"}, @qcode{"cauchy"}, @qcode{"fair"}, @qcode{"huber"}, ## @qcode{"logistic"}, @qcode{"ols"}, @qcode{"talwar"} or @qcode{"welsch"}, or a ## function handle returning the weights. @var{w} is the weight, @var{psi} is ## @code{@var{z} .* @var{w}} and @var{psip} is its derivative, computed ## analytically for a named function and by central difference for a handle. ## ## @qcode{"andrews"} and @qcode{"logistic"} take their limiting value of 1 at ## @code{@var{z} == 0}, where the quotient defining them is @math{0/0}. ## ## @seealso{robustfit, nlinfit, robusttune} ## @end deftypefn function [w, psi, psip] = robustwfun (z, wfun) if (is_function_handle (wfun)) w = wfun (z); if (nargout > 1) d = 1e-6; psi = z .* w; psip = ((z + d) .* wfun (z + d) - (z - d) .* wfun (z - d)) / (2 * d); endif return; endif a = abs (z); switch (wfun) case "andrews" in = a < pi; w = (sin (z) ./ (z + (z == 0))) .* in; w(z == 0) = 1; if (nargout > 1); psi = sin (z) .* in; psip = cos (z) .* in; endif case "bisquare" in = a < 1; w = (1 - z .^ 2) .^ 2 .* in; if (nargout > 1) psi = z .* w; psip = (1 - z .^ 2) .* (1 - 5 * z .^ 2) .* in; endif case "cauchy" w = 1 ./ (1 + z .^ 2); if (nargout > 1) psi = z .* w; psip = (1 - z .^ 2) ./ (1 + z .^ 2) .^ 2; endif case "fair" w = 1 ./ (1 + a); if (nargout > 1); psi = z .* w; psip = 1 ./ (1 + a) .^ 2; endif case "huber" w = 1 ./ max (1, a); if (nargout > 1); psi = z .* w; psip = double (a < 1); endif case "logistic" th = tanh (z); w = th ./ (z + (z == 0)); w(z == 0) = 1; if (nargout > 1); psi = th; psip = 1 - th .^ 2; endif case "ols" w = ones (size (z)); if (nargout > 1); psi = z; psip = ones (size (z)); endif case "talwar" in = a < 1; w = double (in); if (nargout > 1); psi = z .* in; psip = double (in); endif case "welsch" e = exp (- z .^ 2); w = e; if (nargout > 1); psi = z .* e; psip = (1 - 2 * z .^ 2) .* e; endif endswitch endfunction statistics-release-2.1.2/inst/Regression/private/terms_from_coefnames.m000066400000000000000000000077461527311104300264610ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{terms}, @var{cat_info}, @var{col_names}] =} terms_from_coefnames (@var{coef_names}, @var{pred_names}, @var{cat_logical}, @var{data}, @var{tbl_sub}) ## ## Recover a terms matrix and the categorical level information from the ## coefficient names a model formula produced. ## ## @var{coef_names} lists the coefficient names, @var{pred_names} the model's ## predictors, @var{cat_logical} marks the categorical ones, @var{data} is the ## data as given, and @var{tbl_sub} the table of rows kept for the fit. ## ## @var{terms} is the terms matrix over the encoded design columns, ## @var{cat_info} a structure with fields @qcode{names} and @qcode{levels}, and ## @var{col_names} the name of each column of @var{terms}. @var{col_names} is ## @emph{not} @var{coef_names}: a factor appearing only inside an interaction or ## a power gets a column of its own without ever being a coefficient, so the two ## lists diverge and anything indexed by the columns of @var{terms} must be ## built from @var{col_names}. The ## levels of a character or string grouping column are taken in the order the ## data presents them, matching the order the design matrix was built in; a ## @code{categorical} column carries its own category order. ## ## This helper is shared by the @code{LinearModel} and ## @code{GeneralizedLinearModel} classes. ## ## @end deftypefn function [terms, cat_info, col_names] = terms_from_coefnames (coef_names, pred_names, ... cat_logical, data, tbl_sub) cat_info.names = {}; cat_info.levels = {}; if (istable (data)) for j = 1:numel (pred_names) if (cat_logical(j)) col = tbl_sub.(pred_names{j}); if (iscell (col) || isa (col, 'string')) ## In the order the data presents them, as the design matrix was ## built: these levels re-encode new data at prediction time, so a ## sorted list here would silently map it onto the wrong indicators. levels_j = cellstr (unique (col(:), 'stable')); elseif (isa (col, 'categorical')) levels_j = categories (col); elseif (islogical (col) || isnumeric (col)) ## Logical, or numeric and declared categorical: coded by ascending ## value, so the reference level is the smallest. vals = double (col(:)); vals = sort (unique (vals(! isnan (vals)))); levels_j = arrayfun (@(x) strtrim (num2str (x)), vals, ... 'UniformOutput', false); else levels_j = {}; endif cat_info.names{end+1} = pred_names{j}; cat_info.levels{end+1} = levels_j; endif endfor endif nc = numel (coef_names); atomic = {}; for t = 1:nc if (strcmp (coef_names{t}, '(Intercept)')) continue; endif for f = strsplit (coef_names{t}, ':') if (! any (strcmp (atomic, f{1}))) atomic{end+1} = f{1}; endif endfor endfor terms = zeros (nc, numel (atomic) + 1); for t = 1:nc if (strcmp (coef_names{t}, '(Intercept)')) continue; endif for f = strsplit (coef_names{t}, ':') terms(t, strcmp (atomic, f{1})) = 1; endfor endfor col_names = [atomic, {''}]; endfunction statistics-release-2.1.2/inst/Regression/private/tril_from_theta.m000066400000000000000000000024001527311104300254240ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{L} =} tril_from_theta (@var{th}, @var{q}) ## ## Lower-triangular @var{q}-by-@var{q} matrix from its @var{q}*(@var{q}+1)/2 ## entries @var{th}, taken in column-major order. ## ## This helper is shared by @code{build_Lfull} and @code{__lme_dfsatt__}. ## ## @end deftypefn function L = tril_from_theta (th, q) L = zeros (q, q); idx = 0; for j = 1:q for i = j:q idx += 1; L(i, j) = th(idx); endfor endfor endfunction statistics-release-2.1.2/inst/Regression/private/variable_class_and_range.m000066400000000000000000000046661527311104300272320ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {[@var{cname}, @var{range}] =} variable_class_and_range (@var{col}) ## ## Report the class of a model variable and the range of values it takes. ## ## @var{col} is one variable of the data as it was given. ## ## @var{cname} is its class. @var{range} is the two-element vector ## @code{[min, max]} for a numeric or logical variable, and otherwise the list ## of levels it takes, as a row and in the variable's own type: a ## @code{categorical} keeps its category order and comes back a ## @code{categorical}, while a @code{string} or a cell array of character ## vectors lists its levels in the order the data presents them and comes back ## as a @code{string} or a cell array respectively. ## ## The level order matters: it is the order the design matrix codes them in, so ## the reference level is the first entry. ## ## This helper is shared by the @code{LinearModel} and ## @code{GeneralizedLinearModel} classes. ## ## @end deftypefn function [cname, range] = variable_class_and_range (col) cname = class (col); if (isnumeric (col) || islogical (col)) fv = double (col(:)); fv = fv(isfinite (fv)); if (isempty (fv)) range = [NaN, NaN]; else range = [min(fv), max(fv)]; endif ## The range keeps the variable's own type, as the level list does. if (islogical (col)) range = logical (range); endif elseif (isa (col, 'categorical')) lv = categories (col); range = categorical (lv(:)', lv); elseif (isa (col, 'string')) range = unique (col(:)', 'stable'); elseif (iscell (col)) lv = unique (col, 'stable'); range = lv(:)'; else range = {}; endif endfunction statistics-release-2.1.2/inst/Regression/private/variable_level_terms.m000066400000000000000000000052401527311104300264350ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {Private Function} {@var{T} =} variable_level_terms (@var{terms_enc}, @var{enc2raw}, @var{var_idx}, @var{n_vars}, @var{col_pow}) ## ## Fold a terms matrix over encoded design columns onto one over the model's ## variables. ## ## @var{terms_enc} is the terms matrix expressed over the encoded design ## columns, @var{enc2raw} maps each of those columns onto a predictor, ## @var{var_idx} maps each predictor onto a variable, and @var{n_vars} is the ## number of variables. @var{col_pow} gives the exponent each column name ## carries and defaults to all ones; it is what distinguishes a column named ## @qcode{u^2} from one named @qcode{u}. ## ## @var{enc2raw} and @var{col_pow} are indexed by the @emph{columns of ## @var{terms_enc}}, so they must be built from the names of those columns and ## not from the coefficient names, which differ whenever a factor appears only ## inside an interaction or a power. ## ## @var{T} has one column per variable and one row per distinct term. The ## several indicator columns of a categorical predictor collapse onto the single ## variable they came from, so the duplicate rows this produces are dropped. ## ## This helper is shared by the @code{LinearModel} and ## @code{GeneralizedLinearModel} classes. ## ## @end deftypefn function T = variable_level_terms (terms_enc, enc2raw, var_idx, n_vars, col_pow) if (nargin < 5) col_pow = ones (1, columns (terms_enc)); endif n_terms = rows (terms_enc); T = zeros (n_terms, n_vars); for t = 1:n_terms for j = 1:columns (terms_enc) if (terms_enc(t, j) != 0 && enc2raw(j) > 0 && var_idx(enc2raw(j)) > 0) c = var_idx(enc2raw(j)); T(t, c) = max (T(t, c), terms_enc(t, j) * col_pow(j)); endif endfor endfor keep = true (n_terms, 1); for t = 2:n_terms if (any (all (T(1:t-1, :) == T(t, :), 2))) keep(t) = false; endif endfor T = T(keep, :); endfunction statistics-release-2.1.2/inst/Regression/regress.m000066400000000000000000000164741527311104300222620ustar00rootroot00000000000000## Copyright (C) 2005, 2006 William Poetra Yoga Hadisoeseno ## Copyright (C) 2011 Nir Krakauer ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{b}, @var{bint}, @var{r}, @var{rint}, @var{stats}] =} regress (@var{y}, @var{X}, [@var{alpha}]) ## ## Multiple Linear Regression using Least Squares Fit of @var{y} on @var{X} ## with the model @code{y = X * beta + e}. ## ## Here, ## ## @itemize ## @item ## @code{y} is a column vector of observed values ## @item ## @code{X} is a matrix of regressors, with the first column filled with ## the constant value 1 ## @item ## @code{beta} is a column vector of regression parameters ## @item ## @code{e} is a column vector of random errors ## @end itemize ## ## Arguments are ## ## @itemize ## @item ## @var{y} is the @code{y} in the model, a vector; a row vector is taken as a ## column ## @item ## @var{X} is the @code{X} in the model ## @item ## @var{alpha} is the significance level used to calculate the confidence ## intervals @var{bint} and @var{rint} (see `Return values' below). If not ## specified, ALPHA defaults to 0.05 ## @end itemize ## ## Return values are ## ## @itemize ## @item ## @var{b} is the @code{beta} in the model ## @item ## @var{bint} is the confidence interval for @var{b} ## @item ## @var{r} is a column vector of residuals ## @item ## @var{rint} is the confidence interval for @var{r} ## @item ## @var{stats} is a row vector containing: ## ## @itemize ## @item The R^2 statistic ## @item The F statistic ## @item The p value for the full model ## @item The estimated error variance ## @end itemize ## @end itemize ## ## @var{r} and @var{rint} can be passed to @code{rcoplot} to visualize ## the residual intervals and identify outliers. ## ## NaN values in @var{y} and @var{X} are removed before calculation begins. ## ## @seealso{regress_gp, regression_ftest, regression_ttest} ## @end deftypefn function [b, bint, r, rint, stats] = regress (y, X, alpha) if (nargin < 2 || nargin > 3) print_usage; endif if (! ismatrix (y)) error ("regress: y must be a numeric matrix"); endif if (! ismatrix (X)) error ("regress: X must be a numeric matrix"); endif if (! isvector (y)) error ("regress: y must be a vector."); endif y = y(:); if (rows (y) != rows (X)) error ("regress: y and X must contain the same number of rows"); endif if (nargin < 3) alpha = 0.05; elseif (! isscalar (alpha)) error ("regress: alpha must be a scalar value") endif notnans = ! logical (sum (isnan ([y X]), 2)); y = y(notnans); X = X(notnans,:); [Xq Xr] = qr (X, 0); pinv_X = Xr \ Xq'; b = pinv_X * y; if (nargout > 1) n = rows (X); p = columns (X); dof = n - p; t_alpha_2 = tinv (alpha / 2, dof); r = y - X * b; # added -- Nir SSE = sum (r .^ 2); v = SSE / dof; # c = diag (inv (X' * X)) using the (economy) QR factor Xr = R. Since # X' * X == R' * R, we have inv (X' * X) == inv (R) * inv (R)', whose # diagonal is the row-wise sum of squares of inv (R). Forming R' * R # would square the condition number (and warns "matrix singular to # machine precision" for ill-conditioned designs, e.g. the Longley data); # working from inv (R) directly avoids that. Xri = inv (Xr); c = sum (Xri .^ 2, 2); db = t_alpha_2 * sqrt (v * c); bint = [b + db, b - db]; endif if (nargout > 3) dof1 = n - p - 1; h = sum (X.*pinv_X', 2); #added -- Nir (same as diag(X*pinv_X), without doing the matrix multiply) # From Matlab's documentation on Multiple Linear Regression, # sigmaihat2 = norm (r) ^ 2 / dof1 - r .^ 2 / (dof1 * (1 - h)); # dr = -tinv (1 - alpha / 2, dof) * sqrt (sigmaihat2 .* (1 - h)); # Substitute # norm (r) ^ 2 == sum (r .^ 2) == SSE # -tinv (1 - alpha / 2, dof) == tinv (alpha / 2, dof) == t_alpha_2 # We get # sigmaihat2 = (SSE - r .^ 2 / (1 - h)) / dof1; # dr = t_alpha_2 * sqrt (sigmaihat2 .* (1 - h)); # Combine, we get # dr = t_alpha_2 * sqrt ((SSE * (1 - h) - (r .^ 2)) / dof1); dr = t_alpha_2 * sqrt ((SSE * (1 - h) - (r .^ 2)) / dof1); rint = [r + dr, r - dr]; endif if (nargout > 4) R2 = 1 - SSE / sum ((y - mean (y)) .^ 2); # F = (R2 / (p - 1)) / ((1 - R2) / dof); F = dof / (p - 1) / (1 / R2 - 1); pval = fcdf (F, p - 1, dof, 'upper'); stats = [R2 F pval v]; endif endfunction %!test %! % Longley data from the NIST Statistical Reference Dataset %! Z = [ 60323 83.0 234289 2356 1590 107608 1947 %! 61122 88.5 259426 2325 1456 108632 1948 %! 60171 88.2 258054 3682 1616 109773 1949 %! 61187 89.5 284599 3351 1650 110929 1950 %! 63221 96.2 328975 2099 3099 112075 1951 %! 63639 98.1 346999 1932 3594 113270 1952 %! 64989 99.0 365385 1870 3547 115094 1953 %! 63761 100.0 363112 3578 3350 116219 1954 %! 66019 101.2 397469 2904 3048 117388 1955 %! 67857 104.6 419180 2822 2857 118734 1956 %! 68169 108.4 442769 2936 2798 120445 1957 %! 66513 110.8 444546 4681 2637 121950 1958 %! 68655 112.6 482704 3813 2552 123366 1959 %! 69564 114.2 502601 3931 2514 125368 1960 %! 69331 115.7 518173 4806 2572 127852 1961 %! 70551 116.9 554894 4007 2827 130081 1962 ]; %! % Results certified by NIST using 500 digit arithmetic %! % b and standard error in b %! V = [ -3482258.63459582 890420.383607373 %! 15.0618722713733 84.9149257747669 %! -0.358191792925910E-01 0.334910077722432E-01 %! -2.02022980381683 0.488399681651699 %! -1.03322686717359 0.214274163161675 %! -0.511041056535807E-01 0.226073200069370 %! 1829.15146461355 455.478499142212 ]; %! Rsq = 0.995479004577296; %! F = 330.285339234588; %! y = Z(:,1); X = [ones(rows(Z),1), Z(:,2:end)]; %! alpha = 0.05; %! [b, bint, r, rint, stats] = regress (y, X, alpha); %! assert_equal (b,V(:,1),4e-6); %! assert_equal (stats(1),Rsq,1e-12); %! assert_equal (stats(2),F,3e-8); %! assert_equal (((bint(:,1)-bint(:,2))/2)/tinv (alpha/2,9),V(:,2),-1e-11); %!test %! X = [ones(6, 1), (1:6)']; %! y = [1.1 1.9 3.2 3.9 5.1 6.2]; %! assert_equal (regress (y, X), regress (y', X)); %!test %! ## Below the resolution of 1 - fcdf, values from MATLAB R2024a %! u = (1:30)'; %! [~, ~, ~, ~, st] = regress (2 * u + 0.01 * sin (u), [ones(30, 1), u]); %! assert_equal (st(3), 2.48569519842005e-96, -1e-8); %!error regress (ones (3, 2), ones (3, 2)) statistics-release-2.1.2/inst/Regression/regress_gp.m000066400000000000000000000731021527311104300227370ustar00rootroot00000000000000## Copyright (c) 2012 Juan Pablo Carbajal ## Copyright (C) 2023-2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {[@var{Yfit}, @var{Yint}, @var{m}, @var{K}] =} regress_gp (@var{X}, @var{Y}, @var{Xfit}) ## @deftypefnx {statistics} {[@var{Yfit}, @var{Yint}, @var{m}, @var{K}] =} regress_gp (@var{X}, @var{Y}, @var{Xfit}, @qcode{'linear'}) ## @deftypefnx {statistics} {[@var{Yfit}, @var{Yint}, @var{Ysd}] =} regress_gp (@var{X}, @var{Y}, @var{Xfit}, @qcode{'rbf'}) ## @deftypefnx {statistics} {[@dots{}] =} regress_gp (@var{X}, @var{Y}, @var{Xfit}, @qcode{'linear'}, @var{Sp}) ## @deftypefnx {statistics} {[@dots{}] =} regress_gp (@var{X}, @var{Y}, @var{Xfit}, @var{Sp}) ## @deftypefnx {statistics} {[@dots{}] =} regress_gp (@var{X}, @var{Y}, @var{Xfit}, @qcode{'rbf'}, @var{theta}) ## @deftypefnx {statistics} {[@dots{}] =} regress_gp (@var{X}, @var{Y}, @var{Xfit}, @qcode{'rbf'}, @var{theta}, @var{g}) ## @deftypefnx {statistics} {[@dots{}] =} regress_gp (@var{X}, @var{Y}, @var{Xfit}, @qcode{'rbf'}, @var{theta}, @var{g}, @var{alpha}) ## @deftypefnx {statistics} {[@dots{}] =} regress_gp (@var{X}, @var{Y}, @var{Xfit}, @var{theta}) ## @deftypefnx {statistics} {[@dots{}] =} regress_gp (@var{X}, @var{Y}, @var{Xfit}, @var{theta}, @var{g}) ## @deftypefnx {statistics} {[@dots{}] =} regress_gp (@var{X}, @var{Y}, @var{Xfit}, @var{theta}, @var{g}, @var{alpha}) ## ## Regression using Gaussian Processes. ## ## @code{[@var{Yfit}, @var{Yint}, @var{m}, @var{K}] = regress_gp (@var{X}, ## @var{Y}, @var{Xfit})} will estimate a linear Gaussian Process model @var{m} ## in the form @qcode{@var{Y} = @var{X}' * @var{m}}, where @var{X} is an ## @math{N*P} matrix with @math{N} observations in @math{P} dimensional space ## and @var{Y} is an @math{N*1} column vector as the dependent variable. The ## information about errors of the predictions (interpolation/extrapolation) is ## given by the covariance matrix @var{K}. ## By default, the linear model defines the prior covariance of @var{m} as ## @code{@var{Sp} = 100 * eye (size (@var{X}, 2) + 1)}. A custom prior ## covariance matrix can be passed as @var{Sp}, which must be a @math{P+1*P+1} ## positive definite matrix. The model is evaluated for input @var{Xfit}, which ## must have the same columns as @var{X}, and the estimates are returned in ## @var{Yfit} along with the estimated variation in @var{Yint}. ## @qcode{@var{Yint}(:,1)} contains the lower boundary and ## @qcode{@var{Yint}(:,2)} the upper boundary of the interval about ## @var{Yfit}, at the confidence level @qcode{1 - @var{alpha}}. ## ## @code{[@var{Yfit}, @var{Yint}, @var{Ysd}] = regress_gp (@var{X}, ## @var{Y}, @var{Xfit}, @qcode{'rbf'})} will estimate a Gaussian Process model ## with a Radial Basis Function (RBF) kernel with default parameters ## @qcode{@var{theta} = 5} and @qcode{@var{g} = 0.01}, which corresponds to the ## nugget effect, and ## @qcode{@var{alpha} = 0.05} which defines the confidence level for the ## estimated intervals returned in @var{Yint}. The function also returns the ## predictive covariance matrix in @var{Ysd}. For multidimensional predictors ## @var{X} the function will automatically normalize each column to a zero mean ## and a standard deviation to one. ## ## Four things about the RBF kernel are worth stating, because they decide ## what the numbers mean. ## ## @itemize ## @item ## @var{theta} is not the characteristic lengthscale. The kernel is ## @code{exp (-d^2 / @var{theta})} with @math{d} the distance between two ## points, so @var{theta} is twice the square of a lengthscale @math{l}, and ## @code{@var{theta} = 2 * l^2}. ## ## @item ## The intervals in @var{Yint} are prediction intervals for a @emph{new ## observation}, not confidence intervals on the mean: the nugget is carried ## in the predictive variance, so the noise of an observation is included. ## ## @item ## The predictors are centred and scaled only when there is more than one of ## them, so @var{theta} is measured in the units of @var{X} for a single ## predictor and in standard deviations for several. ## ## @item ## A nugget of zero is accepted and gives an interpolating process, one that ## reproduces @var{Y} at the training points and reports almost no uncertainty ## there. It also leaves the kernel matrix rank deficient whenever two inputs ## are close, so in that case the covariance is applied through its ## pseudoinverse and the fit is the minimum-norm solution. This is a genuine ## answer rather than a refusal, but a small nugget is the better way to ask ## for a smooth fit. ## @end itemize ## ## Run @code{demo regress_gp} to see examples. ## ## @seealso{fitrgp, RegressionGP, regress, regression_ftest, ## regression_ttest} ## @end deftypefn function [Yfit, Yint, varargout] = regress_gp (X, Y, Xfit, varargin) ## Check input arguments if (nargin < 3) print_usage; endif if (ndims (X) != 2) error ("regress_gp: X must be a 2-D matrix."); endif if (! isvector (Y) || size (Y, 2) != 1) error ("regress_gp: Y must be a column vector."); endif if (size (X, 1) != length (Y)) error ("regress_gp: rows in X must equal the length of Y."); endif if (size (X, 2) != size (Xfit, 2)) error ("regress_gp: X and XI must have the same number of columns."); endif ## Add defaults kernel = 'linear'; Sp = 100 * eye (size (X, 2) + 1); theta = 5; g = 0.01; alpha = 0.05; ## Parse extra arguments if (nargin > 3) tmp = varargin{1}; if (ischar (tmp) && strcmpi (tmp, 'linear')) kernel = 'linear'; sinput = true; elseif (ischar (tmp) && strcmpi (tmp, 'rbf')) kernel = 'rbf'; sinput = true; elseif (isnumeric (tmp) && ! isscalar (tmp)) kernel = 'linear'; sinput = false; Sp = checkSp (tmp, size (X, 2)); elseif (isnumeric (tmp) && isscalar (tmp)) kernel = 'rbf'; sinput = false; theta = tmp; else error ("regress_gp: invalid 4th argument."); endif endif if (nargin > 4) tmp = varargin{2}; if (sinput) if (isnumeric (tmp) && ! isscalar (tmp)) if (strcmpi (kernel, 'rbf')) error ("regress_gp: theta must be a scalar when using RBF kernel."); endif Sp = checkSp (tmp, size (X, 2)); elseif (isnumeric (tmp) && isscalar (tmp)) if (strcmpi (kernel, 'linear')) error ("regress_gp: wrong size for prior covariance matrix Sp."); endif theta = tmp; else error ("regress_gp: invalid 5th argument."); endif else if (strcmpi (kernel, 'linear')) error ("regress_gp: invalid 5th argument."); endif ## Every other argument in this function is validated before it is ## used. This one was not, so a non-numeric nugget reached the kernel ## and failed inside a matrix product with a message about ## nonconformant arguments, naming neither g nor this function. if (! (isnumeric (tmp) && isscalar (tmp))) error ("regress_gp: invalid 5th argument."); endif g = tmp; endif endif if (nargin > 5) tmp = varargin{3}; if (isnumeric (tmp) && isscalar (tmp) && sinput) g = tmp; elseif (isnumeric (tmp) && isscalar (tmp) && ! sinput) alpha = tmp; else error ("regress_gp: invalid 6th argument."); endif endif if (nargin > 6) tmp = varargin{4}; if (isnumeric (tmp) && isscalar (tmp) && sinput) alpha = tmp; else error ("regress_gp: invalid 7th argument."); endif endif ## User linear kernel if (strcmpi (kernel, 'linear')) ## Add constant vector X = [ones(1,size(X,1)); X']; ## Juan Pablo Carbajal ## Note that in the book the equation (below 2.11) for the A reads ## A = (1/sy^2)*X*X' + inv (Vp); ## where sy is the scalar variance of the of the residuals (i.e Y = X' * w + epsilon) ## and epsilon is drawn from N(0,sy^2). Vp is the variance of the parameters w. ## Note that ## (sy^2 * A)^{-1} = (1/sy^2)*A^{-1} = (X*X' + sy^2 * inv(Vp))^{-1}; ## and that the formula for the w mean is ## (1/sy^2)*A^{-1}*X*Y ## Then one obtains ## inv(X*X' + sy^2 * inv(Vp))*X*Y ## Looking at the formula below we see that Sp = (1/sy^2)*Vp ## making the regression depend on only one parameter, Sp, and not two. ## Xsq = sum (X' .^ 2); ## [n, d] = size (X); ## sigma = 1/sqrt(2); ## Ks = exp (-(Xsq' * ones (1, n) -ones (n, 1) * Xsq + 2 * X * X') / (2 * sigma ^ 2)); ## Sp and A are both positive definite, so their inverses are taken ## through a Cholesky factor rather than by inv. A is built from a Gram ## matrix and inherits the square of the predictors' conditioning, which is ## exactly where a general inverse loses digits and a triangular solve does ## not. K is still formed because it is the fourth output. Rp = chol (Sp); A = X * X' + Rp \ (Rp' \ eye (rows (Sp))); Ra = chol (A); K = Ra \ (Ra' \ eye (rows (A))); wm = Ra \ (Ra' \ (X * Y)); ## Add constant vector Xfit = [ones(size(Xfit,1),1), Xfit]; ## Compute predictions Yfit = Xfit*wm; ## Only the diagonal of Xfit * K * Xfit' is ever read here, so the full ## m-by-m matrix is never formed: with A = Ra' * Ra the product is U' * U ## for U = Ra' \ Xfit', whose diagonal is the squared column norms of U. ## Exact, not an approximation, and O(m) in place of O(m^2). U = Ra' \ Xfit'; Ysd = sum (U .^ 2, 1)'; ## The diagonal of a covariance is a variance, so it is the square root ## that scales the interval, and the interval is that many standard ## deviations wide for the confidence level asked for. This branch used ## the variance itself, applied no level at all, and returned its columns ## in the opposite order to the other branch. dy = norminv (1 - alpha / 2) * sqrt (Ysd); Yint = [Yfit-dy, Yfit+dy]; if (nargout > 2) varargout{1} = wm; endif if (nargout > 3) varargout{2} = K; endif endif ## User RBF kernel if (strcmpi (kernel, 'rbf')) ## Normalize predictors if (size (X, 2) > 1) [X, MU, SIGMA] = zscore (X); Xfit = (Xfit - MU) ./SIGMA; endif ## Get number of training samples n = size (X, 1); ## Calculate squared distance matrix of training input D = squareform (pdist (X) .^2); ## Compute kernel covariance for training quantities S = exp (-D / theta) + g * eye (n); ## Compute kernel covariance for prediction Dx = pdist2 (Xfit, X) .^ 2; Sx = exp (-Dx / theta); ## S is symmetric, and positive definite whenever the nugget is, so every ## quantity below goes through a solve rather than through inv (S). The ## inverse was not merely slower: at g = 0 it warns "matrix singular to ## machine precision" with an rcond of 1e-18 and returns what that implies, ## on an argument the function accepts. SinvY = Y; SinvSx = Sx'; if (rcond (S) > eps) SinvY = S \ Y; SinvSx = S \ Sx'; else ## A nugget of zero on repeated or near-repeated inputs leaves S rank ## deficient. The minimum-norm solution is the defensible answer here ## and pinv gives it without the warning inv emitted. Sinv = pinv (S); SinvY = Sinv * Y; SinvSx = Sinv * Sx'; endif ## Calculate response output Yfit = Sx * SinvY; ## Estimate scale parameter for predictive variance scale = (Y' * SinvY) / size (Y, 1); ## The interval needs only the diagonal of the predictive covariance, and ## the test-test prior contributes exactly 1 + g to it, since a point is at ## zero distance from itself. So neither the m-by-m distance matrix nor ## the full covariance is built unless the caller asks for the covariance ## itself as the third output. ysd1 = sqrt (scale * ((1 + g) - sum (Sx' .* SinvSx, 1)')); if (nargout > 2) Dxi = squareform (pdist (Xfit) .^ 2); Sxi = exp (-Dxi / theta) + g * eye (rows (Xfit)); Ysd = scale * (Sxi - Sx * SinvSx); endif ## Calculate prediction intervals. A two sided interval puts alpha/2 in ## each tail, so the quantile is of 1 - alpha/2; taking it at alpha ## returned a 100*(1-2*alpha) per cent interval under a 100*(1-alpha) per ## cent name, which at the default made every interval a 90 per cent one. dy = norminv (1 - alpha / 2) * ysd1; Yint = [Yfit-dy, Yfit+dy]; if (nargout > 2) varargout{1} = Ysd; endif endif endfunction ## Validate the prior covariance of the weights. Both argument positions that ## accept Sp check it here, because they did not check the same things: the ## fourth-argument form validated nothing at all, and the fifth validated the ## size but not the definiteness the documentation requires. A singular Sp is ## inverted to infinities and every prediction comes back NaN, warning about a ## matrix the caller never handed to inv. function Sp = checkSp (Sp, p) if (! isequal (size (Sp), (p + 1) * [1, 1])) error ("regress_gp: wrong size for prior covariance matrix Sp."); endif [~, spd] = chol (Sp); if (spd != 0) error (strcat ("regress_gp: prior covariance matrix Sp must be", ... " positive definite.")); endif endfunction %!demo %! ## Linear fitting of 1D Data %! rng (42); %! X = 2 * rand (5, 1) - 1; %! Y = 2 * X - 1 + 0.3 * randn (5, 1); %! %! ## Points for interpolation/extrapolation %! Xfit = linspace (-2, 2, 10)'; %! %! ## Fit regression model %! [Yfit, Yint, m] = regress_gp (X, Y, Xfit); %! %! ## Plot fitted data %! plot (X, Y, 'xk', Xfit, Yfit, 'r-', Xfit, Yint, 'b-'); %! title ('Gaussian process regression with linear kernel'); %!demo %! ## Linear fitting of 2D Data %! rng (42); %! X = 2 * rand (4, 2) - 1; %! Y = 2 * X(:,1) - 3 * X(:,2) - 1 + 1 * randn (4, 1); %! %! ## Mesh for interpolation/extrapolation %! [x1, x2] = meshgrid (linspace (-1, 1, 10)); %! Xfit = [x1(:), x2(:)]; %! %! ## Fit regression model %! [Ypred, Yint, m] = regress_gp (X, Y, Xfit); %! Ypred = reshape (Ypred, 10, 10); %! YintL = reshape (Yint(:,1), 10, 10); %! YintU = reshape (Yint(:,2), 10, 10); %! %! ## Plot fitted data %! plot3 (X(:,1), X(:,2), Y, '.k', 'markersize', 16); %! hold on; %! h = mesh (x1, x2, Ypred, zeros (10, 10)); %! set (h, 'facecolor', 'none', 'edgecolor', 'yellow'); %! h = mesh (x1, x2, YintU, ones (10, 10)); %! set (h, 'facecolor', 'none', 'edgecolor', 'cyan'); %! h = mesh (x1, x2, YintL, ones (10, 10)); %! set (h, 'facecolor', 'none', 'edgecolor', 'cyan'); %! hold off %! axis tight %! view (75, 25) %! title ('Gaussian process regression with linear kernel'); %!demo %! ## Projection over basis function with linear kernel %! rng (42); %! pp = [2, 2, 0.3, 1]; %! n = 10; %! X = 2 * rand (n, 1) - 1; %! Y = polyval (pp, X) + 0.3 * randn (n, 1); %! %! ## Powers %! px = [sqrt(abs(X)), X, X.^2, X.^3]; %! %! ## Points for interpolation/extrapolation %! Xfit = linspace (-1, 1, 100)'; %! pxi = [sqrt(abs(Xfit)), Xfit, Xfit.^2, Xfit.^3]; %! %! ## Define a prior covariance assuming that the sqrt component is not present %! Sp = 100 * eye (size (px, 2) + 1); %! Sp(2,2) = 1; # We don't believe the sqrt(abs(X)) is present %! %! ## Fit regression model %! [Yfit, Yint, m] = regress_gp (px, Y, pxi, Sp); %! %! ## Plot fitted data %! plot (X, Y, 'xk;Data;', Xfit, Yfit, 'r-;Estimation;', ... %! Xfit, polyval (pp, Xfit), 'g-;True;'); %! axis tight %! axis manual %! hold on %! plot (Xfit, Yint(:,1), 'b-;Lower bound;', ... %! Xfit, Yint(:,2), 'm-;Upper bound;'); %! hold off %! title ('Linear kernel over basis function with prior covariance'); %!demo %! ## Projection over basis function with linear kernel %! rng (42); %! pp = [2, 2, 0.3, 1]; %! n = 10; %! X = 2 * rand (n, 1) - 1; %! Y = polyval (pp, X) + 0.3 * randn (n, 1); %! %! ## Powers %! px = [sqrt(abs(X)), X, X.^2, X.^3]; %! %! ## Points for interpolation/extrapolation %! Xfit = linspace (-1, 1, 100)'; %! pxi = [sqrt(abs(Xfit)), Xfit, Xfit.^2, Xfit.^3]; %! %! ## Fit regression model without any assumption on prior covariance %! [Yfit, Yint, m] = regress_gp (px, Y, pxi); %! %! ## Plot fitted data %! plot (X, Y, 'xk;Data;', Xfit, Yfit, 'r-;Estimation;', ... %! Xfit, polyval (pp, Xfit), 'g-;True;'); %! axis tight %! axis manual %! hold on %! plot (Xfit, Yint(:,1), 'b-;Lower bound;', ... %! Xfit, Yint(:,2), 'm-;Upper bound;'); %! hold off %! title ('Linear kernel over basis function without prior covariance'); %!demo %! ## Projection over basis function with rbf kernel %! rng (42); %! pp = [2, 2, 0.3, 1]; %! n = 10; %! X = 2 * rand (n, 1) - 1; %! Y = polyval (pp, X) + 0.3 * randn (n, 1); %! %! ## Powers %! px = [sqrt(abs(X)), X, X.^2, X.^3]; %! %! ## Points for interpolation/extrapolation %! Xfit = linspace (-1, 1, 100)'; %! pxi = [sqrt(abs(Xfit)), Xfit, Xfit.^2, Xfit.^3]; %! %! ## Fit regression model with RBF kernel (standard parameters) %! [Yfit, Yint, Ysd] = regress_gp (px, Y, pxi, 'rbf'); %! %! ## Plot fitted data %! plot (X, Y, 'xk;Data;', Xfit, Yfit, 'r-;Estimation;', ... %! Xfit, polyval (pp, Xfit), 'g-;True;'); %! axis tight %! axis manual %! hold on %! plot (Xfit, Yint(:,1), 'b-;Lower bound;', ... %! Xfit, Yint(:,2), 'm-;Upper bound;'); %! hold off %! title ('RBF kernel over basis function with standard parameters'); %! text (-0.5, 4, "theta = 5\n g = 0.01"); %!demo %! ## Projection over basis function with rbf kernel %! rng (42); %! pp = [2, 2, 0.3, 1]; %! n = 10; %! X = 2 * rand (n, 1) - 1; %! Y = polyval (pp, X) + 0.3 * randn (n, 1); %! %! ## Powers %! px = [sqrt(abs(X)), X, X.^2, X.^3]; %! %! ## Points for interpolation/extrapolation %! Xfit = linspace (-1, 1, 100)'; %! pxi = [sqrt(abs(Xfit)), Xfit, Xfit.^2, Xfit.^3]; %! %! ## Fit regression model with RBF kernel with different parameters %! [Yfit, Yint, Ysd] = regress_gp (px, Y, pxi, 'rbf', 10, 0.01); %! %! ## Plot fitted data %! plot (X, Y, 'xk;Data;', Xfit, Yfit, 'r-;Estimation;', ... %! Xfit, polyval (pp, Xfit), 'g-;True;'); %! axis tight %! axis manual %! hold on %! plot (Xfit, Yint(:,1), 'b-;Lower bound;', ... %! Xfit, Yint(:,2), 'm-;Upper bound;'); %! hold off %! title ('GP regression with RBF kernel and non default parameters'); %! text (-0.5, 4, "theta = 10\n g = 0.01"); %! %! ## Fit regression model with RBF kernel with different parameters %! [Yfit, Yint, Ysd] = regress_gp (px, Y, pxi, 'rbf', 50, 0.01); %! %! ## Plot fitted data %! figure %! plot (X, Y, 'xk;Data;', Xfit, Yfit, 'r-;Estimation;', ... %! Xfit, polyval (pp, Xfit), 'g-;True;'); %! axis tight %! axis manual %! hold on %! plot (Xfit, Yint(:,1), 'b-;Lower bound;', ... %! Xfit, Yint(:,2), 'm-;Upper bound;'); %! hold off %! title ('GP regression with RBF kernel and non default parameters'); %! text (-0.5, 4, "theta = 50\n g = 0.01"); %! %! ## Fit regression model with RBF kernel with different parameters %! [Yfit, Yint, Ysd] = regress_gp (px, Y, pxi, 'rbf', 50, 0.001); %! %! ## Plot fitted data %! figure %! plot (X, Y, 'xk;Data;', Xfit, Yfit, 'r-;Estimation;', ... %! Xfit, polyval (pp, Xfit), 'g-;True;'); %! axis tight %! axis manual %! hold on %! plot (Xfit, Yint(:,1), 'b-;Lower bound;', ... %! Xfit, Yint(:,2), 'm-;Upper bound;'); %! hold off %! title ('GP regression with RBF kernel and non default parameters'); %! text (-0.5, 4, "theta = 50\n g = 0.001"); %! %! ## Fit regression model with RBF kernel with different parameters %! [Yfit, Yint, Ysd] = regress_gp (px, Y, pxi, 'rbf', 50, 0.05); %! %! ## Plot fitted data %! figure %! plot (X, Y, 'xk;Data;', Xfit, Yfit, 'r-;Estimation;', ... %! Xfit, polyval (pp, Xfit), 'g-;True;'); %! axis tight %! axis manual %! hold on %! plot (Xfit, Yint(:,1), 'b-;Lower bound;', ... %! Xfit, Yint(:,2), 'm-;Upper bound;'); %! hold off %! title ('GP regression with RBF kernel and non default parameters'); %! text (-0.5, 4, "theta = 50\n g = 0.05"); %!demo %! ## RBF fitting on noiseless 1D Data %! rng (42); %! x = [0:2*pi/7:2*pi]'; %! y = 5 * sin (x); %! %! ## Predictive grid of 500 equally spaced locations %! xi = [-0.5:(2*pi+1)/499:2*pi+0.5]'; %! %! ## Fit regression model with RBF kernel %! [Yfit, Yint, Ysd] = regress_gp (x, y, xi, 'rbf'); %! %! ## Plot fitted data %! r = mvnrnd (Yfit, diag (Ysd)', 50); %! plot (xi, r', 'c-'); %! hold on %! plot (xi, Yfit, 'r-;Estimation;', xi, Yint, 'b-;Confidence interval;'); %! plot (x, y, '.k;Predictor points;', 'markersize', 20) %! plot (xi, 5 * sin (xi), '-y;True Function;'); %! xlim ([-0.5,2*pi+0.5]); %! ylim ([-10,10]); %! hold off %! title ('GP regression with RBF kernel on noiseless 1D data'); %! text (0, -7, "theta = 5\n g = 0.01"); %!demo %! ## RBF fitting on noisy 1D Data %! rng (42); %! x = [0:2*pi/7:2*pi]'; %! x = [x; x]; %! y = 5 * sin (x) + randn (size (x)); %! %! ## Predictive grid of 500 equally spaced locations %! xi = [-0.5:(2*pi+1)/499:2*pi+0.5]'; %! %! ## Fit regression model with RBF kernel %! [Yfit, Yint, Ysd] = regress_gp (x, y, xi, 'rbf'); %! %! ## Plot fitted data %! r = mvnrnd (Yfit, diag (Ysd)', 50); %! plot (xi, r', 'c-'); %! hold on %! plot (xi, Yfit, 'r-;Estimation;', xi, Yint, 'b-;Confidence interval;'); %! plot (x, y, '.k;Predictor points;', 'markersize', 20) %! plot (xi, 5 * sin (xi), '-y;True Function;'); %! xlim ([-0.5,2*pi+0.5]); %! ylim ([-10,10]); %! hold off %! title ('GP regression with RBF kernel on noisy 1D data'); %! text (0, -7, "theta = 5\n g = 0.01"); ## Test input validation ## The value tests this function never had. Its two branches are checked ## against closed forms computed here, and the RBF branch additionally against ## RegressionGP, whose arithmetic is verified against MATLAB. %!test %! ## The RBF mean is the Gaussian process posterior mean, which RegressionGP %! ## computes too. Matching the parameterisations: this function writes the %! ## kernel as exp (-d2 / theta) with a noise variance g added, where %! ## RegressionGP writes it as SigmaF^2 * exp (-0.5 * d2 / SigmaL^2) with a %! ## noise standard deviation, so SigmaL = sqrt (theta/2), SigmaF = 1 and %! ## Sigma = sqrt (g). %! x = linspace (0, 1, 20)'; %! y = sin (2*pi*x); %! xq = [0.15; 0.45; 0.75]; %! theta = 0.5; %! g = 0.01; %! yf = regress_gp (x, y, xq, 'rbf', theta, g); %! Mdl = RegressionGP (x, y, 'KernelFunction', 'squaredexponential', ... %! 'KernelParameters', [sqrt(theta/2); 1], ... %! 'Sigma', sqrt (g), 'BasisFunction', 'none', ... %! 'FitMethod', 'none'); %! assert_equal (yf, predict (Mdl, xq), 1e-10); %!test %! ## The RBF interval is the normal quantile of the level times the standard %! ## deviation, on both sides of the fit %! x = linspace (0, 1, 20)'; %! y = sin (2*pi*x); %! xq = [0.2; 0.5; 0.8]; %! [yf, yi, ys] = regress_gp (x, y, xq, 'rbf'); %! sd = sqrt (diag (ys)); %! assert_equal (yi(:,1), yf - norminv (0.975) * sd, 1e-12); %! assert_equal (yi(:,2), yf + norminv (0.975) * sd, 1e-12); %!test %! ## A looser level gives a narrower interval, and the level reaches the %! ## interval at all, which it did not before %! x = linspace (0, 1, 20)'; %! y = sin (2*pi*x); %! xq = [0.2; 0.5; 0.8]; %! [~, yi95] = regress_gp (x, y, xq, 'rbf', 5, 0.01, 0.05); %! [~, yi80] = regress_gp (x, y, xq, 'rbf', 5, 0.01, 0.20); %! assert (all (yi80(:,2) - yi80(:,1) < yi95(:,2) - yi95(:,1))); %!test %! ## The linear mean is the Bayesian linear regression posterior mean %! x = linspace (0, 1, 15)'; %! y = 2 * x + 0.5; %! xq = [0.25; 0.75]; %! Sp = 100 * eye (2); %! yf = regress_gp (x, y, xq); %! Xd = [ones(1, 15); x']; %! wm = inv (Xd * Xd' + inv (Sp)) * Xd * y; %! assert_equal (yf, [ones(2, 1), xq] * wm, 1e-12); %!test %! ## The linear interval is built from a standard deviation and carries the %! ## confidence level, where it used to be a bare variance %! x = linspace (0, 1, 15)'; %! y = 2 * x + 0.5; %! xq = [0.25; 0.75]; %! [yf, yi, wm, K] = regress_gp (x, y, xq); %! sd = sqrt (diag ([ones(2, 1), xq] * K * [ones(2, 1), xq]')); %! assert_equal (yi(:,1), yf - norminv (0.975) * sd, 1e-12); %! assert_equal (yi(:,2), yf + norminv (0.975) * sd, 1e-12); %!test %! ## Both branches order the interval the same way, lower bound first, as %! ## every other prediction interval in the package does %! x = linspace (0, 1, 15)'; %! y = sin (3*x); %! xq = [0.3; 0.6]; %! [~, yiL] = regress_gp (x, y, xq, 'linear'); %! [~, yiR] = regress_gp (x, y, xq, 'rbf'); %! assert (all (yiL(:,1) < yiL(:,2))); %! assert (all (yiR(:,1) < yiR(:,2))); ## The Cholesky path and the predictive covariance must agree: the interval is ## built from the diagonal computed directly, the third output from the full ## matrix, and the two are formed by different routes. %!test %! rand ('seed', 91); %! x = 2 * rand (12, 1) - 1; %! y = sin (3 * x); %! xq = linspace (-1, 1, 6)'; %! [yf, yi, S] = regress_gp (x, y, xq, 'rbf'); %! assert_equal (yi(:,2) - yf, norminv (0.975) * sqrt (diag (S)), 1e-12); %! assert_equal (yf - yi(:,1), norminv (0.975) * sqrt (diag (S)), 1e-12); ## Asking for the covariance must not change the fit or the interval. %!test %! rand ('seed', 91); %! x = 2 * rand (12, 1) - 1; %! y = sin (3 * x); %! xq = linspace (-1, 1, 6)'; %! [yf2, yi2] = regress_gp (x, y, xq, 'rbf'); %! [yf3, yi3, S] = regress_gp (x, y, xq, 'rbf'); %! assert_equal (yf2, yf3); %! assert_equal (yi2, yi3); ## Without a nugget the process interpolates its training data. This used to ## be computed through inv () on a kernel matrix with an rcond of 1e-18, which ## warned and returned what that implies. %!test %! rand ('seed', 3); %! x = sort (2 * rand (10, 1) - 1); %! y = sin (3 * x) + 0.1; %! [yf, yi] = regress_gp (x, y, x, 'rbf', 1, 0); %! assert_equal (yf, y, 1e-4); %! assert (max (abs (yi(:,2) - yf)) < 1e-2); ## The linear branch reports the posterior covariance of the weights as its ## fourth output, and the interval must follow that same matrix. %!test %! rand ('seed', 17); %! x = 2 * rand (10, 2) - 1; %! y = x(:,1) - 2 * x(:,2) + 0.1; %! xq = 2 * rand (4, 2) - 1; %! [yf, yi, wm, K] = regress_gp (x, y, xq); %! xa = [ones(4, 1), xq]; %! assert_equal (yi(:,2) - yf, ... %! norminv (0.975) * sqrt (diag (xa * K * xa')), 1e-12); %!error regress_gp (ones (20, 2)) %!error regress_gp (ones (20, 2), ones (20, 1)) %!error ... %! regress_gp (ones (20, 2, 3), ones (20, 1), ones (20, 2)) %!error ... %! regress_gp (ones (20, 2), ones (20, 2), ones (20, 2)) %!error ... %! regress_gp (ones (20, 2), ones (15, 1), ones (20, 2)) %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (20, 3)) %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (10, 2), {[3]}) %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (10, 2), 'kernel') %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (10, 2), 'rbf', ones (4)) %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (20, 2), 5, 'junk') %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (20, 2), zeros (3, 3)) %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (20, 2), 'linear', zeros (3, 3)) %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (20, 2), eye (4)) %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (10, 2), 'linear', 1) %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (10, 2), 'rbf', 'value') %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (10, 2), 'rbf', {5}) %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (10, 2), eye (3), 5) %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (10, 2), 'linear', 5) %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (10, 2), 'rbf', 5, {5}) %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (10, 2), 'rbf', 5, ones (2)) %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (10, 2), 5, 0.01, [1, 1]) %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (10, 2), 5, 0.01, 'f') %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (10, 2), 5, 0.01, 'f') %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (10, 2), 'rbf', 5, 0.01, 'f') %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (10, 2), 'rbf', 5, 0.01, [1, 1]) %!error ... %! regress_gp (ones (20, 2), ones (20, 1), ones (10, 2), 'linear', 1) statistics-release-2.1.2/inst/Regression/ridge.m000066400000000000000000000227111527311104300216710ustar00rootroot00000000000000## Copyright (C) 2023 Mohammed Azmat Khan ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{b} =} ridge (@var{y}, @var{X}, @var{k}) ## @deftypefnx {statistics} {@var{b} =} ridge (@var{y}, @var{X}, @var{k}, @var{scaled}) ## ## Ridge regression. ## ## @code{@var{b} = ridge (@var{y}, @var{X}, @var{k})} returns the vector of ## coefficient estimates by applying ridge regression from the predictor matrix ## @var{X} to the response vector @var{y}. Each value of @var{b} is the ## coefficient for the respective ridge parameter given @var{k}. By default, ## @var{b} is calculated after centering and scaling the predictors to have a ## zero mean and standard deviation 1. A predictor whose standard deviation is ## below @code{sqrt (eps)}, taken in the class of @var{X}, is treated as a ## constant term: it is replaced by a column of ones and its coefficient is ## never rescaled. ## ## @code{@var{b} = ridge (@var{y}, @var{X}, @var{k}, @var{scaled})} performs the ## regression with the specified scaling of the coefficient estimates @var{b}. ## When @qcode{@var{scaled} = 0}, the function restores the coefficients to the ## scale of the original data thus is more useful for making predictions. When ## @qcode{@var{scaled} = 1}, the coefficient estimates correspond to the scaled ## centered data. ## ## @itemize ## @item ## @code{y} must be an @math{N*1} numeric vector with the response data. ## @item ## @code{X} must be an @math{N*p} numeric matrix with the predictor data. ## @item ## @code{k} must be a numeric vector with the ridge parameters. ## @item ## @code{scaled} must be a numeric scalar indicating whether the coefficient ## estimates in @var{b} are restored to the scale of the original data. By ## default, @qcode{@var{scaled} = 1}. ## @end itemize ## ## Rows of @var{y} and @var{X} holding a @code{NaN} are removed before the fit, ## and at least two rows must remain. MATLAB answers with fewer: with none it ## returns zeros, or @code{NaN} when @qcode{@var{scaled} = 0}, and with one it ## returns slopes that do not depend on @var{X}, and too many coefficients when ## @var{X} has more than one column and @qcode{@var{scaled} = 0}. ## ## Further information about Ridge regression can be found at ## @url{https://en.wikipedia.org/wiki/Ridge_regression} ## ## @seealso{lasso, stepwisefit, regress} ## @end deftypefn function b = ridge (y, X, k, scaled) ## Check input arguments if (nargin < 3) error ("ridge: function called with too few input arguments."); endif if (! isvector (y) || columns (y) != 1 || isempty (y)) error ("ridge: Y must be a numeric column vector."); endif if (! ismatrix (X) || isempty (X)) error ("ridge: X must be a numeric matrix."); endif if (rows (y) != rows (X)) error ("ridge: Y and X must contain the same number of rows."); endif ## Parse 4th input argument if (nargin < 4 || isempty (scaled)) unscale = false; elseif (scaled == 1) unscale = false; elseif (scaled == 0) unscale = true; else error ("ridge: wrong value for SCALED argument."); endif ## Force y to a column vector y = y(:); ## Remove any missing values notnans = ! logical (sum (isnan ([y, X]), 2)); y = y(notnans); X = X(notnans,:); if (rows (X) < 2) error ("ridge: Y and X must contain at least two rows without NaN."); endif ## Scale and center X to zero mean and StD = 1 m = mean (X); stdx = std (X, 0, 1); ## Columns without variation are treated as a constant term isconst = stdx < sqrt (eps (class (stdx))); stdx(isconst) = 1; z = (X - m) ./ stdx; z(:,isconst) = 1; ## Add pseudo observations p = columns (X); I = eye (p); Z_pseudo = [z; (sqrt(k(1)) .* I)]; Y_pseudo = [y; zeros(p, 1)]; ## Compute coefficients b = Z_pseudo \ Y_pseudo; nk = numel (k); ## Compute the coefficient estimates for additional ridge parameters. if (nk >= 2) ## Adding a multiple of the identity matrix to the last p rows. ## b is set to 0 for the current ridge parameter value b(end,nk) = 0; for i=2:nk Z_pseudo(end-p+1:end, :) = sqrt (k(i)) .* I; b(:,i) = Z_pseudo \ Y_pseudo; endfor endif ## Changing back to the scale if (unscale) b = b ./ repmat (stdx', 1, nk); b = [mean(y)-m*b; b]; endif endfunction %!demo %! ## Perform ridge regression for a range of ridge parameters and observe %! ## how the coefficient estimates change based on the acetylene dataset. %! %! load acetylene %! %! X = [x1, x2, x3]; %! %! x1x2 = x1 .* x2; %! x1x3 = x1 .* x3; %! x2x3 = x2 .* x3; %! %! D = [x1, x2, x3, x1x2, x1x3, x2x3]; %! %! k = 0:1e-5:5e-3; %! %! b = ridge (y, D, k); %! %! figure %! plot (k, b, 'LineWidth', 2) %! ylim ([-100, 100]) %! grid on %! xlabel ('Ridge Parameter') %! ylabel ('Standardized Coefficient') %! title ('Ridge Trace') %! legend ('x1', 'x2', 'x3', 'x1x2', 'x1x3', 'x2x3') %! %!demo %! %! rng (42); %! load carbig %! X = [Acceleration Weight Displacement Horsepower]; %! y = MPG; %! %! n = length (y); %! %! %! c = cvpartition (n,'HoldOut',0.3); %! idxTrain = training(c,1); %! idxTest = ! idxTrain; %! %! idxTrain = training(c,1); %! idxTest = ! idxTrain; %! %! k = 5; %! b = ridge (y(idxTrain),X(idxTrain,:),k,0); %! %! % Predict MPG values for the test data using the model. %! yhat = b(1) + X(idxTest,:)*b(2:end); %! scatter (y(idxTest),yhat) %! %! hold on %! plot (y(idxTest),y(idxTest),'r') %! xlabel ('Actual MPG') %! ylabel ('Predicted MPG') %! hold off %! ## Test output %!test %! b = ridge ([1 2 3 4]', [1 2 3 4; 2 3 4 5]', 1); %! assert_equal (b, [0.5533; 0.5533], 1e-4); %!test %! b = ridge ([1 2 3 4]', [1 2 3 4; 2 3 4 5]', 2); %! assert_equal (b, [0.4841; 0.4841], 1e-4); %!test %! load acetylene %! x = [x1, x2, x3]; %! b = ridge (y, x, 0); %! assert_equal (b,[10.2273;1.97128;-0.601818],1e-4); %!test %! load acetylene %! x = [x1, x2, x3]; %! b = ridge (y, x, 0.0005); %! assert_equal (b,[10.2233;1.9712;-0.6056],1e-4); %!test %! load acetylene %! x = [x1, x2, x3]; %! b = ridge (y, x, 0.001); %! assert_equal (b,[10.2194;1.9711;-0.6094],1e-4); %!test %! load acetylene %! x = [x1, x2, x3]; %! b = ridge (y, x, 0.002); %! assert_equal (b,[10.2116;1.9709;-0.6169],1e-4); %!test %! load acetylene %! x = [x1, x2, x3]; %! b = ridge (y, x, 0.005); %! assert_equal (b,[10.1882;1.9704;-0.6393],1e-4); %!test %! load acetylene %! x = [x1, x2, x3]; %! b = ridge (y, x, 0.01); %! assert_equal (b,[10.1497;1.9695;-0.6761],1e-4); %!test %! y = [1 3 2 5 4]'; %! X = [-2 * ones(5,1), (1:5)']; %! b = ridge (y, X, 0.1); %! assert_equal (b, [2.941176471; 1.234059575], 1e-8); %!test %! y = [1 3 2 5 4]'; %! X = [3 * ones(5,1), (1:5)']; %! b = ridge (y, X, 0.1, 0); %! assert_equal (b, [-8.164992826; 2.941176471; 0.7804878049], 1e-8); %!test %! y = [1 3 2 5 4]'; %! X = [ones(5,1), 2 * ones(5,1), (1:5)']; %! b = ridge (y, X, 0.1); %! assert_equal (b, [1.485148515; 1.485148515; 1.234059575], 1e-8); %!test %! y = [1 3 NaN 5 4]'; %! X = [1 1 7 1 1; 1 2 3 4 5]'; %! b = ridge (y, X, 0.1); %! assert_equal (b, [3.170731707; 1.413477568], 1e-8); %!test %! x = (1:10)'; %! y = x / 2 + sin (x); %! c = 1.45e-8 * (x - mean (x)) / std (x); %! b = ridge (y, [c, x], 0.1); %! assert_equal (b, ridge (y, [ones(10,1), x], 0.1), 1e-14); %!test %! x = (1:10)'; %! y = x / 2 + sin (x); %! c = 1.53e-8 * (x - mean (x)) / std (x); %! b = ridge (y, [c, x], 0.1); %! assert_equal (b, [0.690800497259392; 0.690800497259375], 1e-10); %!test %! x = (1:10)'; %! y = single (x / 2 + sin (x)); %! c = single (1e-4 * (x - mean (x)) / std (x)); %! b = ridge (y, [c, single(x)], 0.1); %! assert_equal (b, single ([2.86249389814040; 1.37400978026317]), 1e-5); %!test %! x = (1:10)'; %! y = single (x / 2 + sin (x)); %! c = single (1e-3 * (x - mean (x)) / std (x)); %! b = ridge (y, [c, single(x)], 0.1); %! assert_equal (b, single ([0.6907972; 0.6908036]), 1e-5); ## Test input validation %!error ridge (1) %!error ridge (1, 2) %!error ridge (ones (3), ones (3), 2) %!error ridge ([1, 2], ones (2), 2) %!error ridge ([], ones (3), 2) %!error ridge (ones (5,1), [], 2) %!error ... %! ridge ([1; 2; 3; 4; 5], ones (3), 3) %!error ... %! ridge (5, [1, 2], 0.1) %!error ... %! ridge ([NaN; 5], [1, 2; 3, 4], 0.1) %!error ... %! ridge ([NaN; 5], [1, 2; 3, NaN], 0.1) %!error ... %! ridge ([1; 2; 3], ones (3), 3, 2) %!error ... %! ridge ([1; 2; 3], ones (3), 3, 'some') statistics-release-2.1.2/inst/Regression/robustfit.m000066400000000000000000000324471527311104300226270ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{b} =} robustfit (@var{X}, @var{y}) ## @deftypefnx {statistics} {@var{b} =} robustfit (@var{X}, @var{y}, @var{wfun}) ## @deftypefnx {statistics} {@var{b} =} robustfit (@var{X}, @var{y}, @var{wfun}, @var{tune}) ## @deftypefnx {statistics} {@var{b} =} robustfit (@var{X}, @var{y}, @var{wfun}, @var{tune}, @var{const}) ## @deftypefnx {statistics} {[@var{b}, @var{stats}] =} robustfit (@dots{}) ## ## Robust linear regression. ## ## @code{@var{b} = robustfit (@var{X}, @var{y})} returns the coefficient vector ## @var{b} of a linear regression of the response @var{y} on the predictors ## @var{X}, fitted by robust M-estimation (iteratively reweighted least squares) ## so that outlying observations are downweighted. A column of ones is added to ## @var{X} by default, so @code{@var{b}(1)} is the intercept. @var{y} is a ## vector with one element per row of @var{X}; a row vector is taken as a ## column. ## ## @code{@var{b} = robustfit (@var{X}, @var{y}, @var{wfun}, @var{tune}, ## @var{const})} selects the weight function @var{wfun}, its tuning constant ## @var{tune}, and whether a constant term is included. @var{wfun} is one of ## @qcode{'bisquare'} (default), @qcode{'andrews'}, @qcode{'cauchy'}, ## @qcode{'fair'}, @qcode{'huber'}, @qcode{'logistic'}, @qcode{'ols'}, ## @qcode{'talwar'}, @qcode{'welsch'}, or a function handle @code{@@(r)} giving ## the weights as a function of the scaled residual. @var{tune} defaults to the ## value that gives 95% efficiency for each weight function. @var{const} is ## @qcode{'on'} (default) to include a constant term or @qcode{'off'} to omit. ## ## @code{[@var{b}, @var{stats}] = robustfit (@dots{})} also returns a structure ## @var{stats} with fields @code{ols_s}, @code{robust_s}, @code{mad_s}, ## @code{s}, @code{se}, @code{covb}, @code{coeffcorr}, @code{t}, @code{p}, ## @code{w}, @code{R}, @code{dfe}, @code{h}, and @code{resid}. The coefficients ## and the fields @code{ols_s}, @code{mad_s}, @code{dfe}, @code{h}, @code{w}, ## and @code{resid} match MATLAB. The standard errors and quantities derived ## from them (@code{se}, @code{t}, @code{p}, @code{covb}) agree with MATLAB to ## within a small fraction of a percent; @code{robust_s} is the ## Street-Carroll-Ruppert robust scale estimate and differs from MATLAB's by ## about 1.5%, measured, with a negligible effect on the standard errors. ## ## That difference is left in place deliberately. The squared influence is ## averaged here over @math{n}, where the estimator as it is usually published ## averages over @math{n-p}; taking that published form moves the result ## further from MATLAB rather than closer, so MATLAB implements neither, and ## matching it would mean reproducing an undocumented variant. The same scale ## serves @code{nlinfit}, which is why its robust @code{MSE} and @code{CovB} ## carry the same difference. ## ## @seealso{regress, fitlm} ## @end deftypefn function [b, stats] = robustfit (X, y, wfun, tune, const) if (nargin < 2) print_usage (); endif if (! (isnumeric (X) && isreal (X) && ismatrix (X))) error ("robustfit: X must be a real matrix."); endif if (! (isnumeric (y) && isreal (y) && (isvector (y) || isempty (y)))) error ("robustfit: Y must be a real vector."); endif y = y(:); if (isvector (X) && rows (X) == 1 && columns (X) == numel (y)) X = X(:); endif if (rows (X) != numel (y)) error ("robustfit: X and Y must have the same number of observations."); endif if (nargin < 3 || isempty (wfun)) wfun = "bisquare"; endif if (ischar (wfun)) wfun = lower (wfun); if (! any (strcmp (wfun, {"bisquare", "andrews", "cauchy", "fair", ... "huber", "logistic", "ols", "talwar", "welsch"}))) error ("robustfit: unrecognised weight function '%s'.", wfun); endif elseif (! is_function_handle (wfun)) error ("robustfit: WFUN must be a name or a function handle."); endif if (nargin < 4 || isempty (tune)) tune = robusttune (wfun); elseif (! (isnumeric (tune) && isscalar (tune) && isreal (tune) && tune > 0)) error ("robustfit: TUNE must be a positive scalar."); endif if (nargin < 5 || isempty (const)) const = "on"; endif if (! (ischar (const) && any (strcmpi (const, {"on", "off"})))) error ("robustfit: CONST must be 'on' or 'off'."); endif addconst = strcmpi (const, "on"); ## Drop observations with missing values. ok = ! (any (isnan (X), 2) | isnan (y)); n0 = numel (y); X = X(ok,:); y = y(ok); if (addconst) X = [ones(rows (X), 1), X]; endif [n, p] = size (X); if (n <= p) error ("robustfit: not enough observations for the number of parameters."); endif ## Ordinary least squares start, leverages, and the OLS scale. [Q, R, perm] = qr (X, 0); b = zeros (p, 1); b(perm) = R \ (Q' * y); hlev = min (0.9999, sum (Q .^ 2, 2)); adj = 1 ./ sqrt (1 - hlev); dfe = n - p; ols_s = norm (y - X * b) / sqrt (dfe); ## Iteratively reweighted least squares. bprev = b; for iter = 1:50 r = y - X * b; radj = r .* adj; s = madsigma (radj, p, y); w = robustwfun (radj / (s * tune), wfun); sw = sqrt (w); b(perm) = (X(:,perm) .* sw) \ (y .* sw); if (all (abs (b - bprev) <= sqrt (eps) * max (abs (b), abs (bprev)))) break; endif bprev = b; endfor if (nargout < 2) return; endif ## Robust scale and coefficient covariance. The reported weights stay the ## ones the last iteration actually used, which came from the floored ## scale; the scale below is computed from the unfloored one, which is what ## MATLAB reports as mad_s and what its robust_s is built from. Recomputing ## the weights here and reporting those instead graded them out of rounding ## noise on a fit that is already exact. r = y - X * b; radj = r .* adj; mad_s = madsigma (radj, p); [wc, psi, psip] = robustwfun (radj / (mad_s * tune), wfun); if (all (wc == 1)) robust_s = ols_s; else K = 1 + (p / n) * var (psip) / mean (psip) ^ 2; robust_s = tune * mad_s * sqrt (mean (psi .^ 2)) / mean (psip) * K; endif ## Combine the OLS and robust scales (larger of the two, blended by n and p). s = sqrt ((p ^ 2 * ols_s ^ 2 + n * robust_s ^ 2) / (n + p ^ 2)); s = max (s, robust_s); covb = s ^ 2 * inv (X' * X); se = sqrt (diag (covb)); se_outer = se * se'; coeffcorr = covb ./ se_outer; t = b ./ se; pval = 2 * tcdf (-abs (t), dfe); stats.ols_s = ols_s; stats.robust_s = robust_s; stats.mad_s = mad_s; stats.s = s; stats.se = se; stats.covb = covb; stats.coeffcorr = coeffcorr; stats.t = t; stats.p = pval; stats.w = unfilter (w, ok, n0); stats.R = R; stats.dfe = dfe; stats.h = unfilter (hlev, ok, n0); stats.resid = unfilter (r, ok, n0); endfunction ## Scatter a per-observation vector back to the original length, with NaN for ## observations that were dropped for missing values. function out = unfilter (v, ok, n0) if (all (ok)) out = v; else out = NaN (n0, 1); out(ok) = v; endif endfunction %!demo %! ## Robust fit is resistant to an outlier that pulls the OLS line %! x = (1:10)'; %! y = 2 * x + 1; %! y(10) = 0; # an outlier %! b_ols = regress (y, [ones(10,1), x]); %! b_rob = robustfit (x, y); %! plot (x, y, "o", x, [ones(10,1) x]*b_ols, "r-", ... %! x, [ones(10,1) x]*b_rob, "b-"); %! legend ("data", "OLS", "robust", "location", "northwest"); %!shared X, y %! X = [1;2;3;4;5;6;7;8;9;10]; %! y = [3.1;5.2;6.9;9.1;11.0;12.9;15.2;17.1;19.0;5.0]; %!test # MATLAB parity: bisquare coefficients and exact stats fields %! [b, st] = robustfit (X, y); %! assert_equal (b, [1.08223788958791; 1.9947584179332], 1e-8); %! assert_equal (st.ols_s, 4.58673317428878, 1e-8); %! assert_equal (st.mad_s, 0.219326216641263, 1e-8); %! assert_equal (st.dfe, 8); %! assert_equal (st.h(1), 0.345454545454545, 1e-9); %! assert_equal (st.w(10), 0, 1e-10); %! assert_equal (st.resid(10), -16.0298220689199, 1e-6); %!test # MATLAB parity: standard errors, t, p within a fraction of a percent %! [b, st] = robustfit (X, y); %! assert_equal (st.s, 2.45430591997485, 5e-3); %! assert_equal (st.se, [1.67661012843903; 0.270210188642742], 2e-3); %! assert_equal (st.t, [0.64549168064224; 7.38224723483898], 5e-3); %! assert_equal (st.p, [0.536678473587217; 7.75017584465372e-05], 5e-3); %! assert_equal (st.covb, [2.81102152278434, -0.401574503254905; ... %! -0.401574503254905, 0.0730135460463465], 1e-2); %!test # weight-function coefficients match MATLAB %! assert_equal (robustfit (X, y, "huber"), ... %! [1.13942072329047; 1.97894586334502], 1e-6); %! assert_equal (robustfit (X, y, "andrews"), ... %! [1.08226666865865; 1.99475428605778], 1e-6); %! assert_equal (robustfit (X, y, "cauchy"), ... %! [1.08721258646419; 1.99357974946387], 1e-6); %! assert_equal (robustfit (X, y, "fair"), ... %! [1.25156505474148; 1.94736889966414], 1e-6); %! assert_equal (robustfit (X, y, "logistic"), ... %! [1.14596301338789; 1.9773188568744], 1e-6); %! assert_equal (robustfit (X, y, "talwar"), [1.08055555555555; 1.995], 1e-6); %! assert_equal (robustfit (X, y, "welsch"), ... %! [1.08260376495793; 1.99470613677464], 1e-6); %!test # 'ols' weight reproduces ordinary least squares %! assert_equal (robustfit (X, y, "ols"), regress (y, [ones(10,1), X]), 1e-10); %!test # custom tuning constant and const='off' %! assert_equal (robustfit (X, y, "bisquare", 3), ... %! [1.08512259456964; 1.99435302345156], 1e-6); %! [b, st] = robustfit (X, y, "bisquare", 4.685, "off"); %! assert_equal (b, 2.16460032959659, 1e-6); %! assert_equal (st.dfe, 9); %!test # MATLAB parity: an exactly zero residual weighs 1, not 0 %! ## andrews and logistic are w = sin (z) / z and tanh (z) / z, both 0/0 at %! ## the origin. With no constant term the middle residual is exactly zero %! ## whatever the coefficient, so this reaches the limit every time. %! xz = [-1; 0; 1]; %! yz = [-2; 0; 3]; %! [bz, sz] = robustfit (xz, yz, "andrews", [], "off"); %! assert_equal (sz.resid(2), 0); %! assert_equal (sz.w, [0.958241991423609; 1; 0.958241991423609], 1e-12); %! [bz, sz] = robustfit (xz, yz, "logistic", [], "off"); %! assert_equal (sz.resid(2), 0); %! assert_equal (sz.w, [0.907175968590982; 1; 0.907175968590982], 1e-12); %!test # a planted outlier is downweighted relative to OLS %! x = (1:20)'; %! yy = 3 * x - 5; yy(7) = yy(7) + 100; %! b = robustfit (x, yy); %! assert_equal (b, [-5; 3], 0.1); %!test # missing observations are dropped, weights padded with NaN %! x = (1:10)'; yy = 2*x + 1; yy(4) = NaN; %! [b, st] = robustfit (x, yy); %! assert_equal (b, [1; 2], 1e-8); %! assert_equal (isnan (st.w(4)), true); %! assert_equal (st.dfe, 7); ## A fit that is already exact keeps every weight at 1. The scale that ## weights an iteration is floored at 1e-6 * std (Y), so residuals that are ## nothing but rounding noise cannot grade the weights out of it; R2024a ## returns ones here and used to differ from us by 0.24 on the outermost. %!test %! xf = [-2; -1; 0; 1; 2]; %! yf = 3 * xf; %! [bf, stats] = robustfit (xf, yf, 'andrews', [], 'off'); %! assert_equal (bf, 3, 1e-12); %! assert_equal (stats.w, ones (5, 1), 1e-8); ## Just below the floor the weights are graded, but only slightly, and these ## are R2024a's own numbers. %!test %! xf = [-2; -1; 0; 1; 2]; %! yf = 3 * xf + 1e-6 * [1; -1; 0; 1; -1]; %! [bf, stats] = robustfit (xf, yf, 'andrews', [], 'off'); %! assert_equal (stats.w', [0.997523, 0.993403, 1, 0.993403, 0.997523], 1e-6); ## Well above the floor nothing is floored and the weights are the ordinary ## ones, again R2024a's. %!test %! xf = [-2; -1; 0; 1; 2]; %! yf = 3 * xf + 1e-3 * [1; -1; 0; 1; -1]; %! [bf, stats] = robustfit (xf, yf, 'andrews', [], 'off'); %! assert_equal (stats.w', [0.958242, 0.868203, 1, 0.868203, 0.958242], 1e-6); ## A row vector Y is taken as a column. %!test %! xf = [1; 2; 3; 4; 5; 6]; %! yf = [1.1 1.9 3.2 3.9 5.1 6.2]; %! assert_equal (robustfit (xf, yf), robustfit (xf, yf')); ## Test input validation %!error robustfit (1) %!error ... %! robustfit ([1;2;3], [1;2]) %!error ... %! robustfit (X, y, "foo") %!error ... %! robustfit (X, y, "huber", -1) %!error ... %! robustfit (X, y, "huber", 1.345, "maybe") ## Edge cases with empty arrays %!error ... %! robustfit ([], []) %!error ... %! robustfit (zeros (0, 3), zeros (0, 1)) statistics-release-2.1.2/inst/Regression/stepwisefit.m000066400000000000000000000576351527311104300231620ustar00rootroot00000000000000## Copyright (C) 2013-2021 Nir Krakauer ## Copyright (C) 2014 Mikael Kurula ## Copyright (C) 2025 Jayant Chauhan <0001jayant@gmail.com> ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {} stepwisefit (@var{X}, @var{y}) ## @deftypefnx {statistics} {@var{b} =} stepwisefit (@var{X}, @var{y}) ## @deftypefnx {statistics} {@var{b}, @var{se}, @var{pval}, @var{finalmodel}, @var{stats}, @var{nextstep}, @var{history} =} stepwisefit (@var{X}, @var{y}, @var{varargin}) ## ## Perform stepwise linear regression using conditional p-value criteria. ## ## @code{stepwisefit} fits a linear regression model to response vector ## @var{y} using predictor matrix @var{X} and performs stepwise variable ## selection based on hypothesis tests for individual regression coefficients. ## ## At each iteration, predictors not currently in the model are tested for ## inclusion using partial F- or t-tests. The predictor with the smallest ## p-value below the entry threshold is added. Predictors currently in the ## model (excluding forced predictors) are then tested for removal, and the ## predictor with the largest p-value exceeding the removal threshold is ## removed. The procedure repeats until the model stabilizes or the maximum ## number of iterations is reached. ## ## After variable selection, the final regression model is refit using ## @code{regress} to compute coefficient estimates and inferential statistics ## for both included and excluded predictors. ## ## @subheading Arguments ## ## @itemize @bullet ## @item ## @var{X} is an @var{n}-by-@var{p} numeric matrix of predictor variables. ## ## @item ## @var{y} is a numeric response vector of @var{n} elements. A row vector is ## taken as a column, where MATLAB refuses it. ## ## @item ## Optional Name-Value pairs may be supplied to control the stepwise ## selection procedure. ## @end itemize ## ## @subheading Name-Value Arguments ## ## @table @asis ## @item @qcode{'InModel'} ## Logical row vector of length @var{p} specifying predictors that are initially ## included in the model. ## ## @item @qcode{'Keep'} ## Logical row vector of length @var{p} specifying predictors that must remain ## in the model and are never removed during stepwise selection. ## ## @item @qcode{'PEnter'} ## Scalar significance level in the open interval (0,1) specifying the maximum ## p-value required for a predictor to enter the model. Default is @code{0.05}. ## ## @item @qcode{'PRemove'} ## Scalar significance level in the open interval (0,1) specifying the minimum ## p-value required for a predictor to be removed from the model. If not ## specified, a default value greater than or equal to @qcode{'PEnter'} is used. ## ## @item @qcode{'MaxIter'} ## Positive integer specifying the maximum number of stepwise iterations. ## Default is @code{Inf}. ## ## @item @qcode{'Scale'} ## Either @qcode{'on'} or @qcode{'off'}. When enabled, predictors are ## standardized prior to stepwise selection only. Final regression ## coefficients are always reported on the original data scale. ## ## @item @qcode{'Display'} ## Either @qcode{'on'} or @qcode{'off'}. Accepted for compatibility but ## currently does not affect output. ## @end table ## ## @subheading Return Values ## ## @itemize @bullet ## @item ## @var{b} is a @var{p}-by-1 vector of regression coefficients. Coefficients ## for excluded predictors are computed conditionally. ## ## @item ## @var{se} is a @var{p}-by-1 vector of standard errors. ## ## @item ## @var{pval} is a @var{p}-by-1 vector of two-sided p-values. ## ## @item ## @var{finalmodel} is a logical row vector indicating which predictors are ## included in the final model. ## ## @item ## @var{stats} is a structure containing regression diagnostics, including ## sums of squares, degrees of freedom, residuals, covariance estimates, ## F-statistic, and related quantities. ## ## @item ## @var{nextstep} is a scalar indicating whether an additional stepwise ## iteration is recommended. Currently always zero. ## ## @item ## @var{history} is a structure summarizing the final model state, including ## selected predictors and coefficient history. ## @end itemize ## ## @seealso{regress} ## @end deftypefn function [b, se, pval, finalmodel, stats, nextstep, history] = ... stepwisefit (X, y, varargin) b = []; se = []; pval = []; finalmodel = []; stats = struct (); nextstep = 0; history = struct (); ## Input validation (positional) if (nargin < 2) error ("stepwisefit: at least two input arguments required"); endif if (! ismatrix (X)) error ("stepwisefit: X must be a matrix."); endif if (! isvector (y)) error ("stepwisefit: Y must be a vector."); endif y = y(:); ## Validate row compatibility BEFORE any concatenation if (rows (X) != rows (y)) error ("stepwisefit: X and Y must have the same number of rows."); endif ## Parse Name-Value pairs InModel = []; Display = 'on'; ## MATLAB-compatible defaults PEnter = 0.05; PRemove = []; Scale = 'off'; MaxIter = Inf; Keep = []; ## Parse Name-Value paired arguments optNames = {'InModel', 'Display', 'PEnter', 'PRemove', ... 'Scale', 'MaxIter', 'Keep'}; dfValues = {[], 'on', 0.05, [], 'off', Inf, []}; [InModel, Display, PEnter, PRemove, Scale, MaxIter, Keep, args] = ... parsePairedArguments (optNames, dfValues, varargin(:)); ## Semantic validation for Name-Value options ## Validate Display if (! any (strcmpi (Display, {'on', 'off'}))) error ("stepwisefit: Display must be 'on' or 'off'"); endif Display = lower (Display); ## Validate Scale if (! any (strcmpi (Scale, {'on', 'off'}))) error ("stepwisefit: Scale must be 'on' or 'off'"); endif Scale = lower (Scale); ## Validate PEnter if (! (isscalar (PEnter) && isnumeric (PEnter) && PEnter > 0 && PEnter < 1)) error ("stepwisefit: PEnter must be a scalar strictly between 0 and 1"); endif ## Validate PRemove (if provided) if (! isempty (PRemove)) if (! (isscalar (PRemove) && isnumeric (PRemove) && PRemove > 0 && PRemove < 1)) error ("stepwisefit: PRemove must be a scalar strictly between 0 and 1"); endif if (PRemove < PEnter) error ("stepwisefit: PRemove must be greater than or equal to PEnter"); endif endif ## Validate MaxIter if (! (isscalar (MaxIter) && isnumeric (MaxIter) && MaxIter > 0 && fix (MaxIter) == MaxIter)) error ("stepwisefit: MaxIter must be a positive integer"); endif ## Handle missing values wasnan = any (isnan ([X y]), 2); Xc = X(! wasnan, :); yc = y(! wasnan); n = rows (Xc); p = columns (Xc); ## Validate Keep and InModel type (if provided) if (! isempty (Keep) && ! islogical (Keep)) error ("stepwisefit: Keep must be a logical vector"); endif if (! isempty (InModel) && ! islogical (InModel)) error ("stepwisefit: InModel must be a logical vector"); endif ## Validate lengths (these already exist in your file, but keep them here for order) if (! isempty (Keep) && numel (Keep) != p) error ("stepwisefit: Keep length must match number of predictors"); endif if (! isempty (InModel) && numel (InModel) != p) error ("stepwisefit: InModel length must match number of predictors"); endif if (! isempty (args)) error ("stepwisefit: unrecognized input arguments"); endif ## No complete observation: return the empty result if (n == 0) b = NaN (p, 1); se = NaN (p, 1); pval = NaN (p, 1); finalmodel = false (1, p); stats = struct ('source', 'stepwisefit', 'df0', -1, 'dfe', 0, ... 'SStotal', 0, 'SSresid', 0, 'rmse', NaN, ... 'intercept', 0, 'wasnan', wasnan, 'yr', zeros (0, 1), ... 'B', NaN (p, 1), 'SE', NaN (p, 1), ... 'TSTAT', NaN (p, 1), 'PVAL', NaN (p, 1), ... 'xr', zeros (0, p), 'covb', NaN (p, p), ... 'fstat', NaN, 'pval', NaN); history = struct ('in', false (0, p), 'df0', zeros (0, 0), ... 'rmse', zeros (0, 0), 'B', zeros (p, 0)); return; endif if (isempty (Keep)) Keep = false (1, p); endif ## Default PRemove if unset if (isempty (PRemove)) PRemove = max (PEnter, 0.1); endif if (PRemove < PEnter) error ("stepwisefit: PRemove must be greater than or equal to PEnter"); endif if (strcmp (Scale, 'on')) muX = mean (Xc, 1); sigX = std (Xc, 0, 1); sigX(sigX == 0) = 1; ## prevent division by zero Xs = (Xc - muX) ./ sigX; else Xs = Xc; endif ## Validate InModel if (! isempty (InModel)) cur = logical (InModel(:).'); else cur = false (1, p); endif ## Ensure Keep predictors are always in model (already set) cur(Keep) = true; prev = cur; iter = 0; ## Iterative selection: each iteration attempts ADD then REMOVE. while (iter < MaxIter) iter = iter + 1; ## ADD phase: evaluate candidates by conditional p-value candidates = find (! cur); if (! isempty (candidates)) best_p = Inf; best_j = -1; cols = find (cur); ## current included predictors (may be empty) for idx = 1:numel (candidates) j = candidates(idx); ## Build trial design: intercept, current included (if any), candidate j if (isempty (cols)) Xtry = [ones(n,1), Xs(:, j)]; else Xtry = [ones(n,1), Xs(:, cols), Xs(:, j)]; endif ## Regress and compute candidate p-value; skip singular/failed fits try [btry, binttry] = regress (yc, Xtry); catch continue; end_try_catch df_try = n - columns (Xtry); if (df_try <= 0) continue; endif se_try = (binttry(end,2) - btry(end)) / tinv (0.975, df_try); if (se_try <= 0 || ! isfinite (se_try)) continue; endif tstat = btry(end) / se_try; p_candidate = 2 * tcdf (-abs (tstat), df_try); ## Deterministic tie-break: first encountered when nearly equal if (p_candidate < best_p - eps) best_p = p_candidate; best_j = j; endif endfor ## Add best candidate only if it meets the PEnter threshold if (best_j > 0 && best_p < PEnter) cur(best_j) = true; endif endif ## REMOVE phase: compute conditional p-values for included predictors, ## remove worst (largest p) among removable predictors included = find (cur); removable = setdiff (included, find (Keep)); ## never remove Keep if (! isempty (removable)) Xfull = [ones(n,1), Xs(:, included)]; ## Regress current full model; guard against singular / failed fits try [bfull, bintfull] = regress (yc, Xfull); catch bfull = NaN (columns (Xfull), 1); bintfull = NaN (columns (Xfull), 2); end_try_catch df_full = n - columns (Xfull); pvals_included = Inf (1, numel (included)); for ii = 1:numel (included) if (df_full <= 0) pvals_included(ii) = Inf; else se_i = (bintfull(ii+1,2) - bfull(ii+1)) / tinv (0.975, df_full); if (se_i > 0 && isfinite (se_i)) t_i = bfull(ii+1) / se_i; pvals_included(ii) = 2 * tcdf (-abs (t_i), df_full); else pvals_included(ii) = Inf; endif endif endfor ## Map removable predictors to positions within 'included' [~, removable_positions] = ismember (removable, included); [maxp, pos] = max (pvals_included(removable_positions)); if (maxp > PRemove) cur(removable(pos)) = false; endif endif ## Convergence: structural (model unchanged) if (isequal (cur, prev)) break; endif prev = cur; endwhile ## Final set of selected predictors X_use = find (cur); ## Final regression on selected predictors Xfinal = [ones(n,1), Xc(:, X_use)]; ## regstats intentionally unused; retained for MATLAB parity [B, BINT, R, RINT, regstats] = regress (yc, Xfinal); Rresid = R(:); ## freeze residual vector ## Allocate outputs b = zeros (p,1); se = zeros (p,1); pval = zeros (p,1); df = n - columns (Xfinal); if (df <= 0) ## Not enough residual degrees of freedom to estimate SE reliably. se(:) = NaN; pval(:) = NaN; endif ## Included predictors b(X_use) = B(2:end); se(X_use) = (BINT(2:end,2) - B(2:end)) ./ tinv (0.975, df); pval(X_use) = 2 * tcdf (-abs (B(2:end) ./ se(X_use)), df); ## Excluded predictors: conditional refit excluded = setdiff (1:p, X_use); for j = excluded Xj = [ones(n,1), Xc(:, [X_use j])]; [Bj, BjINT] = regress (yc, Xj); bj = Bj(end); sej = (BjINT(end,2) - bj) ./ tinv (0.975, n - columns (Xj)); b(j) = bj; se(j) = sej; pval(j) = 2 * tcdf (-abs (bj / sej), n - columns (Xj)); endfor ## Final model indicator finalmodel = false (1,p); finalmodel(X_use) = true; ## Stats structure stats = struct (); stats.source = 'stepwisefit'; stats.df0 = numel (X_use); stats.dfe = n - stats.df0 - 1; stats.SStotal = sum ((yc - mean (yc)).^2); stats.SSresid = sum (Rresid.^2); stats.rmse = sqrt (stats.SSresid / stats.dfe); stats.intercept = B(1); stats.wasnan = wasnan; stats.yr = Rresid; stats.B = b; stats.SE = se; stats.TSTAT = b ./ se; stats.PVAL = pval; stats.TSTAT (! isfinite (stats.TSTAT)) = NaN; excluded = setdiff (1:p, X_use); xr = zeros (n, numel (excluded)); if (! isempty (X_use)) Z = [ones(n,1), Xc(:, X_use)]; P = Z / (Z' * Z) * Z'; ## projection matrix for k = 1:numel (excluded) j = excluded(k); xr(:,k) = Xc(:,j) - P * Xc(:,j); endfor else ## intercept-only case for k = 1:numel (excluded) j = excluded(k); xr(:,k) = Xc(:,j) - mean (Xc(:,j)); endfor endif stats.xr = xr; covb = NaN (p+1, p+1); covB = (stats.rmse^2) * pinv (Xfinal' * Xfinal); idx = [1, X_use + 1]; covb(idx, idx) = covB; stats.covb = covb; if (stats.df0 > 0) stats.fstat = ((stats.SStotal - stats.SSresid) / stats.df0) ... / (stats.SSresid / stats.dfe); stats.pval = fcdf (stats.fstat, stats.df0, stats.dfe, 'upper'); else stats.fstat = NaN; stats.pval = NaN; endif history = struct (); history.in = finalmodel; history.df0 = stats.df0; history.rmse = stats.rmse; ## Coefficient history (excluding intercept) ## MATLAB stores this as p-by-k; here k = 1 Bhist = zeros (p, 1); Bhist(finalmodel) = b(finalmodel); history.B = Bhist; ## Placeholders for future phases nextstep = 0; endfunction %!test %! X = [7 26 6 60; %! 1 29 15 52; %! 11 56 8 20; %! 11 31 8 47; %! 7 52 6 33; %! 11 55 9 22; %! 3 71 17 6; %! 1 31 22 44; %! 2 54 18 22; %! 21 47 4 26; %! 1 40 23 34; %! 11 66 9 12; %! 10 68 8 12]; %! y = [78.5; 74.3; 104.3; 87.6; 95.9; 109.2; %! 102.7; 72.5; 93.1; 115.9; 83.8; 113.3; 109.4]; %! [b,se,pval,finalmodel,stats] = stepwisefit (X,y); %! assert_equal (finalmodel, [true false false true]); %! assert_equal (b, [1.4400; 0.4161; -0.4100; -0.6140], 1e-4); %! assert_equal (se, [0.1384; 0.1856; 0.1992; 0.0486], 1e-4); %! assert_equal (pval, [0; 0.0517; 0.0697; 0], 1e-4); %! assert_equal (stats.rmse, 2.7343, 1e-4); %! assert_equal (stats.SStotal, 2715.7631, 1e-3); %! assert_equal (stats.SSresid, 74.7621, 1e-4); %! assert_equal (stats.df0, 2); %! assert_equal (stats.dfe, 10); %! assert_equal (stats.intercept, 103.0974, 1e-4); %!test %! X = [ %! 12.0 4 120 95 2600; %! 11.5 6 200 110 3000; %! 10.5 8 300 150 3600; %! 13.0 4 140 100 2800; %! 12.5 6 180 120 3200; %! 11.0 8 250 140 3500; %! 14.0 4 130 98 2700; %! 13.5 6 210 115 3100; %! 12.2 8 320 160 3800; %! 11.8 4 150 105 2900 %! ]; %! y = [28; 22; 18; 27; 23; 19; 29; 21; 17; 26]; %! %! [b,se,pval,finalmodel,stats] = stepwisefit (X,y); %! %! assert_equal (islogical (finalmodel), true); %! assert_equal (numel (finalmodel) == 5, true); %! assert_equal (sum (finalmodel) >= 1, true); %! assert_equal (isnumeric (b), true); %! assert_equal (isnumeric (se), true); %! assert_equal (isnumeric (pval), true); %! assert_equal (stats.rmse > 0, true); %! assert_equal (isfinite (stats.intercept), true); %!test %! X = randn (30, 4); %! y = randn (30, 1); %! [~,~,~,~,stats] = stepwisefit (X, y); %! %! required_fields = { %! 'source', 'df0', 'dfe', 'SStotal', 'SSresid', 'fstat', 'pval', ... %! 'rmse', 'xr', 'yr', 'B', 'SE', 'TSTAT', 'PVAL', 'covb', ... %! 'intercept', 'wasnan' %! }; %! %! for k = 1:numel (required_fields) %! assert_equal (isfield (stats, required_fields{k}), true); %! endfor %!test %! X = randn (40, 5); %! y = randn (40, 1); %! [b,se,pval,finalmodel,stats] = stepwisefit (X, y); %! %! p = columns (X); %! n = rows (X(! stats.wasnan, :)); %! %! assert_equal (size (stats.yr), [n, 1]); %! assert_equal (rows (stats.B) == p, true); %! assert_equal (rows (stats.SE) == p, true); %! assert_equal (rows (stats.TSTAT) == p, true); %! assert_equal (rows (stats.PVAL) == p, true); %! assert_equal (size (stats.covb), [p+1, p+1]); %!test %! X = randn (25, 3); %! y = randn (25, 1); %! [~,~,~,~,stats] = stepwisefit (X, y); %! %! SSresid_calc = sum (stats.yr .^ 2); %! assert_equal (SSresid_calc, stats.SSresid, 1e-10); %! %! rmse_calc = sqrt (stats.SSresid / stats.dfe); %! assert_equal (rmse_calc, stats.rmse, 1e-10); %!test %! X = randn (50, 6); %! y = randn (50, 1); %! [~,~,~,~,stats] = stepwisefit (X, y); %! %! if (stats.df0 > 0) %! F_calc = ((stats.SStotal - stats.SSresid) / stats.df0) ... %! / (stats.SSresid / stats.dfe); %! %! assert_equal (F_calc, stats.fstat, 1e-10); %! assert_equal (stats.pval >= 0 && stats.pval <= 1, true); %! else %! assert_equal (isnan (stats.fstat), true); %! assert_equal (isnan (stats.pval), true); %! endif %!test %! X = randn (35, 4); %! y = randn (35, 1); %! [~,~,~,finalmodel,stats] = stepwisefit (X, y); %! p = columns (X); %! k = sum (finalmodel); %! assert_equal (size (stats.xr, 2) == p - k, true); %! assert_equal (all (isfinite (stats.xr(:))), true); %!test %! X = randn (35, 4); %! y = randn (35, 1); %! [~,~,~,finalmodel,stats] = stepwisefit (X, y); %! %! Xc = X(! stats.wasnan, :); %! Xfinal = [ones(rows (Xc),1), Xc(:, finalmodel)]; %! %! for j = 1:columns (stats.xr) %! ortho = Xfinal' * stats.xr(:,j); %! assert_equal (max (abs (ortho(:))) < 1e-6, true); %! endfor %!test %! X = randn (40, 5); %! y = randn (40, 1); %! [~,~,~,finalmodel,stats,nextstep,history] = stepwisefit (X, y); %! %! assert_equal (nextstep == 0, true); %! assert_equal (isstruct (history), true); %! assert_equal (isfield (history, 'in'), true); %! assert_equal (isfield (history, 'df0'), true); %! assert_equal (isfield (history, 'rmse'), true); %! assert_equal (isfield (history, 'B'), true); %! assert_equal (isequal (history.in, finalmodel), true); %! assert_equal (history.df0 == stats.df0, true); %! assert_equal (history.rmse == stats.rmse, true); %! assert_equal (rows (history.B) == columns (X), true); %!test %! X = randn (20,4); %! y = randn (20,1); %! stepwisefit (X,y,'Keep',[true false true false]); %!test %! X = randn (30, 4); %! y = randn (30, 1); %! keep = [true false false false]; %! [~,~,~,finalmodel] = stepwisefit (X, y, 'Keep', keep); %! assert_equal (finalmodel(1) == true, true); %!test %! X = randn (40, 6); %! y = randn (40, 1); %! [~,~,~,finalmodel] = stepwisefit (X, y, 'MaxIter', 1); %! assert_equal (islogical (finalmodel), true); %!test %! X = randn (50, 5); %! y = randn (50, 1); %! [b1] = stepwisefit (X, y); %! [b2] = stepwisefit (X, y, 'Scale', 'on'); %! assert_equal (rows (b1) == rows (b2), true); %!test %! [b,se,pval,finalmodel,stats,nextstep,history] = ... %! stepwisefit (zeros (0, 3), zeros (0, 1), 'Display', 'off'); %! assert_equal (b, NaN (3, 1)); %! assert_equal (se, NaN (3, 1)); %! assert_equal (pval, NaN (3, 1)); %! assert_equal (finalmodel, false (1, 3)); %! assert_equal (nextstep, 0); %! assert_equal (stats.df0, -1); %! assert_equal (stats.dfe, 0); %! assert_equal (stats.intercept, 0); %! assert_equal (stats.covb, NaN (3, 3)); %! assert_equal (stats.xr, zeros (0, 3)); %! assert_equal (stats.wasnan, false (0, 1)); %! assert_equal (history.in, false (0, 3)); %! assert_equal (history.df0, []); %! assert_equal (history.rmse, []); %! assert_equal (history.B, zeros (3, 0)); %!test %! X = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! y = [1.2 2.1 3.9 4.2 5.8 6.1 8.2 7.9]; %! assert_equal (stepwisefit (X, y, 'Display', 'off'), ... %! stepwisefit (X, y', 'Display', 'off')); %!test %! ## Below the resolution of 1 - tcdf, value from MATLAB R2024a %! x1 = (1:30)'; %! x2 = x1 + 0.1 * sin (1:30)'; %! [~, ~, pval] = stepwisefit ([x1, x2], x2 + 0.001 * cos (1:30)', ... %! 'Display', 'off'); %! assert_equal (pval(2), 3.00226579216343e-116, -1e-9); %!test %! ## Below the resolution of 1 - fcdf, values from MATLAB R2024a %! u = (1:30)'; %! u2 = u + 0.1 * sin (u); %! [~, ~, ~, ~, st] = stepwisefit ([u, u2], u2 + 0.001 * cos (u), ... %! 'Display', 'off'); %! assert_equal (st.pval, 3.0022657921636e-116, -1e-9); %!error ... %! stepwisefit (randn (20,4), randn (20,1), 'Keep', [true false]) ## Test input validation %!error ... %! stepwisefit () %!error ... %! stepwisefit (ones (2,2,2), [1;2]) %!error ... %! stepwisefit ([], [], 'Display', 'off') %!error ... %! stepwisefit (ones (4,2), ones (4,2), 'Display', 'off') %!error ... %! stepwisefit (ones (3,2), ones (2,1)) %!error ... %! stepwisefit (randn (10,2), randn (10,1), 'UnknownOpt', 5) %!error ... %! stepwisefit (randn (10,2), randn (10,1), 'Display', 'maybe') %!error ... %! stepwisefit (randn (10,2), randn (10,1), 'Scale', 123) %!error ... %! stepwisefit (randn (10,2), randn (10,1), 'PEnter', -0.1) %!error ... %! stepwisefit (randn (10,2), randn (10,1), 'PRemove', 1.5) %!error ... %! stepwisefit (randn (10,2), randn (10,1), ... %! 'PEnter', 0.05, 'PRemove', 0.01) %!error ... %! stepwisefit (randn (10,2), randn (10,1), 'MaxIter', -2) %!error ... %! stepwisefit (randn (10,2), randn (10,1), 'MaxIter', 2.5) %!error ... %! stepwisefit (randn (10,2), randn (10,1), 'Keep', [1 0]) %!error ... %! stepwisefit (randn (10,2), randn (10,1), 'InModel', [1 0]) %!error ... %! stepwisefit (randn (10,4), randn (10,1), 'Keep', [true false]) %!error ... %! stepwisefit (zeros (0, 3), zeros (0, 1), 'Keep', [true false]) %!error ... %! stepwisefit (randn (10,4), randn (10,1), 'InModel', true) statistics-release-2.1.2/inst/Regression/stepwiseglm.m000066400000000000000000001607451527311104300231540ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{mdl} =} stepwiseglm (@var{X}, @var{y}) ## @deftypefnx {statistics} {@var{mdl} =} stepwiseglm (@var{X}, @var{y}, @var{modelspec}) ## @deftypefnx {statistics} {@var{mdl} =} stepwiseglm (@var{tbl}) ## @deftypefnx {statistics} {@var{mdl} =} stepwiseglm (@var{tbl}, @var{modelspec}) ## @deftypefnx {statistics} {@var{mdl} =} stepwiseglm (@dots{}, @var{Name}, @var{Value}) ## ## Fit a generalized linear regression model by stepwise term selection. ## ## @code{@var{mdl} = stepwiseglm (@var{X}, @var{y})} starts from a model given ## by @var{modelspec} and repeatedly adds or removes terms, one at a time, until ## no further move improves the selection criterion. It returns the fitted ## @code{GeneralizedLinearModel} object @var{mdl}, whose @code{Steps} property ## records the term-selection trace. @var{X} is an @math{n}-by-@math{p} numeric ## predictor matrix and @var{y} the response; @code{@var{mdl} = stepwiseglm ## (@var{tbl})} instead takes the predictors and response from the table ## @var{tbl} (the last column is the response unless overridden). ## ## @var{modelspec} is the @emph{starting} model. It is a Wilkinson formula ## string (e.g.@: @qcode{'y ~ x1 + x2'}), a keyword (@qcode{'constant'} ## (default), @qcode{'linear'}, @qcode{'interactions'}, @qcode{'purequadratic'}, ## @qcode{'quadratic'}, or @qcode{'full'}), or a terms matrix. The candidate ## terms available to the search are bounded below by @qcode{'Lower'} and above ## by @qcode{'Upper'}. ## ## The following @var{Name}/@var{Value} pairs control the stepwise search: ## ## @multitable @columnfractions 0.2 0.75 ## @headitem Name @tab Value ## @item @qcode{'Lower'} @tab the smallest model considered (terms in it are ## never removed). Defaults to @qcode{'constant'}. ## @item @qcode{'Upper'} @tab the largest model considered (the candidate term ## universe). Defaults to @qcode{'interactions'}. ## @item @qcode{'Criterion'} @tab the selection criterion: @qcode{'Deviance'} ## (default), @qcode{'sse'}, @qcode{'aic'}, or @qcode{'bic'}. Under ## @qcode{'Deviance'} and @qcode{'sse'}, terms enter or leave by a chi-squared ## or @math{F} test on the change in deviance; under @qcode{'aic'}/@qcode{'bic'} ## the move that most reduces the information criterion is taken. ## @item @qcode{'PEnter'} @tab the @math{p}-value (or criterion margin) below ## which a term is added. Defaults to @math{0.05} for @qcode{'Deviance'} and ## @qcode{'sse'}, and @math{0} for @qcode{'aic'}/@qcode{'bic'}. ## @item @qcode{'PRemove'} @tab the @math{p}-value (or criterion margin) above ## which a term is removed. Defaults to @math{0.10} for @qcode{'Deviance'} and ## @qcode{'sse'}, and @math{0} for @qcode{'aic'}/@qcode{'bic'}. ## @item @qcode{'NSteps'} @tab the maximum number of steps. Defaults to ## @code{Inf} (run to convergence). ## @item @qcode{'Verbose'} @tab @math{0} to run silently, or @math{1} (default) ## to print each accepted step. ## @end multitable ## ## As in @code{fitglm}, a @qcode{'binomial'} response is the number of ## successes, and the trials come either from @qcode{'BinomialSize'} or from ## passing @var{y} as an @math{n}-by-@math{2} matrix of successes and trials. ## @strong{Changed in 1.9.0}: @var{y} was previously read as the proportion. ## ## All @var{Name}/@var{Value} pairs accepted by @code{fitglm} (such as ## @qcode{'Distribution'}, @qcode{'Link'}, @qcode{'Weights'}, @qcode{'Offset'}, ## @qcode{'BinomialSize'}, @qcode{'Intercept'}, @qcode{'DispersionFlag'}, ## @qcode{'CategoricalVars'}, and @qcode{'Exclude'}) are also accepted and ## forwarded to the fit. ## ## @subsubheading The @code{Steps} property ## ## The returned model's @code{Steps} property records the trace, as a ## structure with seven fields. @code{Start}, @code{Lower}, and @code{Upper} ## are @code{LinearFormula} objects for the starting model and the two bounds; ## @code{Criterion} is the criterion as it was asked for; @code{PEnter} and ## @code{PRemove} are the thresholds it ran under; and @code{History} is a ## table with one row per step. ## ## @code{History} always carries @code{Action} (@qcode{'Start'}, ## @qcode{'Add'}, or @qcode{'Remove'}), @code{TermName}, @code{Terms} (the ## terms matrix after the step, over the model's variables), @code{DF} (the ## coefficient count after the step), and @code{delDF} (the change in it, ## @emph{negative} for a removal). The remaining columns follow the ## criterion, which is why a history is read by name and not by position: ## ## @multitable @columnfractions 0.25 0.7 ## @headitem Criterion @tab Further columns ## @item @qcode{'Deviance'} @tab @code{Deviance}, then @code{Chi2Stat} or ## @code{FStat} as the dispersion is fixed or estimated, then @code{PValue}. ## @item @qcode{'sse'} @tab @code{FStat} and @code{pValue}. ## @item @qcode{'aic'}, @qcode{'bic'} @tab one column, @code{AIC} or ## @code{BIC}, holding the criterion's value after the step, the starting ## model included. ## @end multitable ## ## The first row is the starting model, named by its right-hand side. ## ## @subsubheading Categorical predictors ## ## A categorical predictor with @math{L} levels contributes @math{L - 1} ## indicator columns, the first level being the omitted reference, and the ## search treats that whole group as a @emph{single term}: it is added or ## removed in one step, worth @math{L - 1} degrees of freedom, and never one ## indicator at a time. An interaction naming a categorical predictor behaves ## the same way, contributing one column per indicator and entering as one ## term. @code{Steps.History} names such a term by the predictor ## (@qcode{'g'}, or @qcode{'x1:g'} for the interaction) rather than by its ## indicators, while @code{CoefficientNames} names the indicators ## (@qcode{'g_B'}, @qcode{'x1:g_C'}). ## ## In a table, every column that groups its observations is taken as ## categorical: a cell array of character vectors, a @code{categorical} array, ## a string array, or a logical column. @qcode{'CategoricalVars'} adds to ## these, and is the only way to mark a column of a predictor @emph{matrix}; ## it takes predictor names, column indices, or a logical vector. ## ## @seealso{GeneralizedLinearModel, fitglm, stepwisefit, glmfit, glmval} ## @end deftypefn function mdl = stepwiseglm (varargin) if (nargin < 1) print_usage (); endif ## ------------------------------------------------------------------------ ## ## Separate the data/response, the (optional) starting model, and the pairs. ## ------------------------------------------------------------------------ ## arg1 = varargin{1}; if (istable (arg1)) data = arg1; resp = []; rest = varargin(2:end); else if (nargin < 2) print_usage (); endif data = arg1; resp = varargin{2}; rest = varargin(3:end); endif start_spec = 'constant'; if (! isempty (rest)) a = rest{1}; if ((ischar (a) && ! is_param_name (a)) || isnumeric (a)) start_spec = a; rest = rest(2:end); endif endif ## Split the pairs into stepwise controls and forwarded fitglm options. [sw, opts, glmnv] = parse_pairs (rest); ## ------------------------------------------------------------------------ ## ## Intake: numeric predictor matrix, response, names, and fitting subset. ## ------------------------------------------------------------------------ ## ## Trial counts taken from a two-column binomial response, empty otherwise. binom_2col = []; start_is_formula = ischar (start_spec) && any (start_spec == '~'); if (istable (data)) col_names = data.Properties.VariableNames; if (ischar (resp) && ! isempty (resp)) resp_name = resp; elseif (! isempty (opts.ResponseVar)) resp_name = opts.ResponseVar; elseif (start_is_formula) resp_name = strtrim (strsplit (start_spec, '~'){1}); else resp_name = col_names{end}; endif if (! isempty (opts.PredictorVars)) pred_names = opts.PredictorVars; else pred_names = col_names(! strcmp (col_names, resp_name)); endif p = numel (pred_names); ## Filled in by RAW_TO_CODES below, which reads the table itself. X = zeros (height (data), p); y = double (data.(resp_name)(:)); else if (! (isnumeric (data) && isreal (data) && ismatrix (data))) error ("stepwiseglm: X must be a real matrix."); endif ## A binomial response may be given as an N-by-2 matrix of successes and ## trials, as MATLAB accepts and as GeneralizedLinearModel accepts; the ## trials are carried alongside and the final model is handed the original ## response, which validates it. is_2col = strcmp (opts.Distribution, 'binomial') && isnumeric (resp) ... && isreal (resp) && ismatrix (resp) && ! isvector (resp) ... && columns (resp) == 2; if (! (is_2col || (isnumeric (resp) && isreal (resp) && isvector (resp)))) error ("stepwiseglm: Y must be a real vector."); endif X = double (data); if (is_2col) binom_2col = double (resp(:,2)); y = double (resp(:,1)); else y = double (resp(:)); endif p = columns (X); if (rows (X) != numel (y)) error (strcat ("stepwiseglm: X and Y must have the same number of", ... " observations.")); endif if (! isempty (opts.VarNames)) pred_names = opts.VarNames(1:p)(:)'; resp_name = opts.VarNames{end}; else pred_names = arrayfun (@(k) sprintf ("x%d", k), 1:p, ... 'UniformOutput', false); resp_name = 'y'; endif if (! isempty (opts.ResponseVar)) resp_name = opts.ResponseVar; endif endif ## ------------------------------------------------------------------------ ## ## Categorical predictors. Each is coded 1..L and expanded to L-1 indicator ## columns, exactly as GeneralizedLinearModel encodes them, so that the final ## model refits the design the search scored. The search itself stays over ## the model's variables, which is what makes a categorical enter and leave ## as one term of L-1 degrees of freedom rather than one indicator at a time. ## ------------------------------------------------------------------------ ## cat_cols = categorical_flags (opts.CategoricalVars, pred_names, p, data); [X, cat_levels] = raw_to_codes (data, X, data, pred_names, cat_cols, ... rows (X)); [X_enc, enc_names, cat_info] = encode_categorical (X, cat_cols, ... pred_names, cat_levels, opts.Intercept); ## Distribution and criterion validation. known_distr = {'normal', 'binomial', 'poisson', 'gamma', ... 'inverse gaussian'}; distr = opts.Distribution; if (! any (strcmp (distr, known_distr))) error ("stepwiseglm: unknown distribution '%s'.", distr); endif crit = lower (sw.Criterion); if (! any (strcmp (crit, {'deviance', 'sse', 'aic', 'bic'}))) error ("stepwiseglm: unknown criterion '%s'.", sw.Criterion); endif ## Fitting subset (drop missing and excluded rows). missing_mask = any (isnan (X), 2) | isnan (y); excluded_mask = false (rows (X), 1); if (! isempty (opts.Exclude)) ex = opts.Exclude(:); if (islogical (ex)) excluded_mask(1:numel (ex)) = ex; else excluded_mask(ex) = true; endif endif subset = ! missing_mask & ! excluded_mask; if (! any (subset)) error (strcat ("stepwiseglm: no observations remain after removing", ... " missing/excluded rows.")); endif Xs = X_enc(subset, :); ys = y(subset); n = rows (Xs); ## Weights, offset, and binomial trial counts, subset to the fitting rows. w_sub = []; if (! isempty (opts.Weights)) w_sub = double (opts.Weights(:))(subset); endif off_sub = []; if (! isempty (opts.Offset)) off_sub = double (opts.Offset(:))(subset); endif N_sub = []; N_all = []; if (strcmp (distr, 'binomial')) if (! isempty (binom_2col)) ## The trials came with the response and win over 'BinomialSize'. N_all = binom_2col; elseif (! isempty (opts.BinomialSize)) N_all = opts.BinomialSize(:); if (isscalar (N_all)) N_all = N_all * ones (rows (X), 1); endif endif endif if (! isempty (N_all)) N_sub = N_all(subset); endif w_ll = w_sub; if (isempty (w_ll)) w_ll = ones (n, 1); endif ## Link function and glmfit argument list (shared by every candidate fit). if (! isempty (opts.Link)) linkspec = opts.Link; else linkspec = default_link (distr); endif [~, ~, ilink] = getlinkfunctions (linkspec); gargs = {'link', linkspec, 'constant', 'off'}; if (! isempty (w_sub)) gargs = [gargs, {'weights', w_sub}]; endif if (! isempty (off_sub)) gargs = [gargs, {'offset', off_sub}]; endif if (! isempty (opts.DispersionFlag)) if (opts.DispersionFlag) gargs = [gargs, {'estdisp', 'on'}]; else gargs = [gargs, {'estdisp', 'off'}]; endif endif yfit = ys; if (strcmp (distr, 'binomial') && ! isempty (N_sub)) ## YS is the number of successes: GLMFIT takes [successes, trials], while ## the log-likelihood below takes the proportion. yfit = [ys, N_sub]; ys = ys ./ N_sub; endif ## Whether dispersion is estimated (chooses the F- vs chi-squared test and ## the internal criterion label). if (! isempty (opts.DispersionFlag)) estdisp = logical (opts.DispersionFlag); else estdisp = any (strcmp (distr, {'normal', 'gamma', 'inverse gaussian'})); endif ## Package the fitting context passed to the inner fitter. ctx = struct ('X', Xs, 'yfit', yfit, 'y', ys, 'distr', distr, ... 'gargs', {gargs}, 'ilink', ilink, 'off', off_sub, ... 'N', N_sub, 'w', w_ll, 'n', n, 'p', p, ... 'pred_names', {pred_names}, 'cat_info', cat_info, ... 'enc_names', {enc_names}); ## ------------------------------------------------------------------------ ## ## Resolve the starting, lower, and upper models to terms matrices. ## ------------------------------------------------------------------------ ## T_start = resolve_terms (start_spec, pred_names, p, opts.Intercept); T_lower = resolve_terms (sw.Lower, pred_names, p, opts.Intercept); T_upper = resolve_terms (sw.Upper, pred_names, p, opts.Intercept); ## The search operates within [Lower, Upper]; the start must contain Lower. T = union_rows (T_start, T_lower); ## Entry/removal thresholds. if (any (strcmp (crit, {'deviance', 'sse'}))) penter = ternary (isempty (sw.PEnter), 0.05, sw.PEnter); premove = ternary (isempty (sw.PRemove), 0.10, sw.PRemove); else penter = ternary (isempty (sw.PEnter), 0, sw.PEnter); premove = ternary (isempty (sw.PRemove), 0, sw.PRemove); endif ## ------------------------------------------------------------------------ ## ## Stepwise search. ## ------------------------------------------------------------------------ ## ws = warning (); # silence per-candidate glmfit warnings warning ("off", "all"); unwind_protect f0 = fit_terms (T, ctx); if (any (strcmp (crit, {'aic', 'bic'}))) start_stat = crit_value (f0, crit, ctx); else start_stat = NaN; endif hist = start_history (T, f0.dev, f0.ncoef, ... start_name (T, pred_names, p), start_stat); stepnum = 0; do stepnum += 1; [T, hstep] = one_step (T, T_lower, T_upper, ctx, p, crit, estdisp, ... penter, premove, pred_names); if (isempty (hstep)) break; endif hist(end+1) = hstep; if (sw.Verbose) print_step (stepnum, hstep, crit, estdisp, pred_names, p); endif until (stepnum >= sw.NSteps) unwind_protect_cleanup warning (ws); end_unwind_protect ## ------------------------------------------------------------------------ ## ## Build the final object (canonical term order) and attach the trace. ## ------------------------------------------------------------------------ ## T_final = canonical_sort (T, p); ## With categorical predictors the constructor reads a terms matrix over the ## encoded columns, so the selected terms are expanded and the resolved names ## are handed over -- an index or a table column detected here would ## otherwise be re-derived, and need not agree. if (isempty (cat_info.names)) spec_final = T_final; glmnv_final = glmnv; else spec_final = expand_terms (T_final, ctx); glmnv_final = set_categorical (glmnv, cat_info.names); endif mdl = GeneralizedLinearModel (data, resp, spec_final, glmnv_final{:}); var_names = [pred_names, {resp_name}]; steps = struct (); steps.Start = LinearFormula (T_start, var_names, 'ResponseName', ... resp_name, 'Link', linkspec); steps.Lower = LinearFormula (T_lower, var_names, 'ResponseName', ... resp_name, 'Link', linkspec); steps.Upper = LinearFormula (T_upper, var_names, 'ResponseName', ... resp_name, 'Link', linkspec); steps.Criterion = criterion_label (crit, estdisp); steps.PEnter = sw.PEnter; steps.PRemove = sw.PRemove; steps.History = history_table (hist, crit, estdisp, pred_names, p); mdl = setSteps (mdl, steps); endfunction ## ========================================================================== ## ## Argument parsing ## ========================================================================== ## ## True if S names a stepwiseglm or forwarded fitglm parameter. function tf = is_param_name (s) tf = ischar (s) && any (strcmpi (s, {'Lower', 'Upper', 'Criterion', ... 'PEnter', 'PRemove', 'NSteps', 'Verbose', 'Distribution', 'Link', ... 'Weights', 'Offset', 'BinomialSize', 'Intercept', 'DispersionFlag', ... 'CategoricalVars', 'Exclude', 'VarNames', 'PredictorVars', ... 'ResponseVar'})); endfunction ## Split Name/Value pairs into stepwise controls (SW), the fit options this ## function needs (OPTS), and the raw pairs forwarded to GeneralizedLinearModel ## (GLMNV). function [sw, opts, glmnv] = parse_pairs (pairs) sw = struct ('Lower', 'constant', 'Upper', 'interactions', ... 'Criterion', 'Deviance', 'PEnter', [], 'PRemove', [], ... 'NSteps', Inf, 'Verbose', 1); opts = struct ('Distribution', 'normal', 'Link', [], 'Weights', [], ... 'Offset', [], 'BinomialSize', [], 'DispersionFlag', [], ... 'CategoricalVars', [], 'Exclude', [], 'Intercept', true, ... 'VarNames', {{}}, 'PredictorVars', [], 'ResponseVar', []); glmnv = {}; if (mod (numel (pairs), 2) != 0) error ("stepwiseglm: Name/Value arguments must come in pairs."); endif for k = 1:2:numel (pairs) name = pairs{k}; val = pairs{k+1}; if (! ischar (name)) error ("stepwiseglm: parameter names must be character vectors."); endif switch (lower (name)) case 'lower' sw.Lower = val; case 'upper' sw.Upper = val; case 'criterion' sw.Criterion = val; case 'penter' sw.PEnter = val; case 'premove' sw.PRemove = val; case 'nsteps' sw.NSteps = val; case 'verbose' sw.Verbose = val; otherwise ## Fit option: record what we need and forward the pair unchanged. switch (lower (name)) case 'distribution'; opts.Distribution = lower (val); case 'link'; opts.Link = val; case 'weights'; opts.Weights = val; case 'offset'; opts.Offset = val; case 'binomialsize'; opts.BinomialSize = val; case 'dispersionflag'; opts.DispersionFlag = val; case 'categoricalvars'; opts.CategoricalVars = val; case 'exclude'; opts.Exclude = val; case 'intercept'; opts.Intercept = val; case 'varnames'; opts.VarNames = val; case 'predictorvars'; opts.PredictorVars = val; case 'responsevar'; opts.ResponseVar = val; otherwise error ("stepwiseglm: unknown parameter name '%s'.", name); endswitch glmnv = [glmnv, {name, val}]; endswitch endfor endfunction ## ========================================================================== ## ## Term-selection engine ## ========================================================================== ## ## Perform one step: try to add, then to remove. Returns the updated terms T ## and a history record HSTEP ([] if no move was accepted). function [T, hstep] = one_step (T, T_lower, T_upper, ctx, p, crit, estdisp, ... penter, premove, pred_names) hstep = []; fC = fit_terms (T, ctx); if (any (strcmp (crit, {'deviance', 'sse'}))) ## --- Add phase: pick the addable term with the smallest p-value. ------- addable = candidates_add (T, T_upper, p); best_p = Inf; best = []; for i = 1:rows (addable) r = addable(i, :); fN = fit_terms ([T; r], ctx); [stat, pval, ddf] = nested_test (fC.dev, fC.dfe, fN.dev, fN.dfe, estdisp); if (pval < best_p) best_p = pval; best = struct ('row', r, 'stat', stat, 'pval', pval, ... 'ddf', ddf, 'dev', fN.dev, 'df', fN.ncoef); endif endfor if (! isempty (best) && best_p < penter) T = [T; best.row]; hstep = mkhist ('Add', best.row, T, best.df, best.ddf, best.dev, ... best.stat, best.pval, p); return; endif ## --- Remove phase: pick the removable term with the largest p-value. --- removable = candidates_remove (T, T_lower, p); worst_p = -Inf; worst = []; for i = 1:rows (removable) r = removable(i, :); Tn = remove_row (T, r); fN = fit_terms (Tn, ctx); [stat, pval, ddf] = nested_test (fN.dev, fN.dfe, fC.dev, fC.dfe, estdisp); if (pval > worst_p) worst_p = pval; worst = struct ('row', r, 'stat', stat, 'pval', pval, ... 'ddf', ddf, 'dev', fN.dev, 'df', fN.ncoef, 'Tn', Tn); endif endfor if (! isempty (worst) && worst_p > premove) T = worst.Tn; hstep = mkhist ('Remove', worst.row, T, worst.df, -worst.ddf, ... worst.dev, worst.stat, worst.pval, p); endif else ## --- Information criterion: take the single best neighbouring move. ---- curval = crit_value (fC, crit, ctx); best_val = curval; best = []; addable = candidates_add (T, T_upper, p); for i = 1:rows (addable) r = addable(i, :); fN = fit_terms ([T; r], ctx); v = crit_value (fN, crit, ctx); if (v < best_val - 1e-12) best_val = v; best = struct ('row', r, 'act', 'Add', 'T', [T; r], 'dev', fN.dev, ... 'df', fN.ncoef, 'ddf', fN.ncoef - fC.ncoef, 'val', v); endif endfor removable = candidates_remove (T, T_lower, p); for i = 1:rows (removable) r = removable(i, :); Tn = remove_row (T, r); fN = fit_terms (Tn, ctx); v = crit_value (fN, crit, ctx); if (v < best_val - 1e-12) best_val = v; best = struct ('row', r, 'act', 'Remove', 'T', Tn, 'dev', fN.dev, ... 'df', fN.ncoef, 'ddf', fN.ncoef - fC.ncoef, 'val', v); endif endfor if (! isempty (best)) T = best.T; hstep = mkhist (best.act, best.row, T, best.df, best.ddf, best.dev, ... best.val, NaN, p); endif endif endfunction ## Fit a candidate model given by terms matrix T. function f = fit_terms (T, ctx) T_enc = expand_terms (T, ctx); Xd = build_design (T_enc, ctx.X); [b, dev, stats] = glmfit (Xd, ctx.yfit, ctx.distr, ctx.gargs{:}); f = struct ('b', b, 'dev', dev, 'dfe', stats.dfe, 's', stats.s, ... 'ncoef', rows (T_enc), 'X', Xd); endfunction ## Nested-model test between the smaller (A) and larger (B) model. Returns the ## chi-squared or F statistic, its p-value, and the degrees of freedom change. function [stat, pval, ddf] = nested_test (devA, dfeA, devB, dfeB, estdisp) ddf = dfeA - dfeB; # number of coefficients that differ (>= 1) drop = devA - devB; # deviance reduction from A to B if (estdisp) stat = (drop / ddf) / (devB / dfeB); pval = fcdf (stat, ddf, dfeB, 'upper'); else stat = drop; pval = chi2cdf (stat, ddf, 'upper'); endif endfunction ## Information-criterion value (AIC or BIC) of a fitted candidate. function v = crit_value (f, crit, ctx) eta = f.X * f.b; if (! isempty (ctx.off)) eta = eta + ctx.off; endif mu = ctx.ilink (eta); LL = glm_loglik (ctx.distr, ctx.y, mu, ctx.N, ctx.w, f.s); k = f.ncoef; switch (crit) case 'aic' v = -2 * LL + 2 * k; case 'bic' v = -2 * LL + k * log (ctx.n); endswitch endfunction ## ========================================================================== ## ## Candidate generation and hierarchy ## ========================================================================== ## ## Rows of the upper model that may be added to T (not present and with every ## one-step-down parent present, so the model hierarchy is preserved). function C = candidates_add (T, T_upper, p) C = zeros (0, columns (T)); for i = 1:rows (T_upper) r = T_upper(i, :); if (row_member (r, T)) continue; endif if (parents_present (r, T, p)) C = [C; r]; endif endfor endfunction ## Rows of T that may be removed (not the intercept, not in the lower model, ## and not a required parent of any higher-order term present in T). function C = candidates_remove (T, T_lower, p) C = zeros (0, columns (T)); for i = 1:rows (T) r = T(i, :); if (all (r(1:p) == 0)) # intercept continue; endif if (row_member (r, T_lower)) continue; endif if (has_superset (r, T, p)) continue; endif C = [C; r]; endfor endfunction ## True if every one-step-down parent of term R is present in T. function tf = parents_present (r, T, p) tf = true; for j = find (r(1:p) != 0) par = r; par(j) -= 1; if (any (par(1:p) != 0) && ! row_member (par, T)) tf = false; return; endif endfor endfunction ## True if some other row of T is a strict superset of term R. function tf = has_superset (r, T, p) tf = false; for i = 1:rows (T) s = T(i, :); if (isequal (s, r)) continue; endif if (all (s(1:p) >= r(1:p)) && any (s(1:p) > r(1:p))) tf = true; return; endif endfor endfunction function tf = row_member (r, T) tf = ! isempty (T) && any (all (T == r, 2)); endfunction function T = remove_row (T, r) T(all (T == r, 2), :) = []; endfunction function U = union_rows (A, B) U = A; for i = 1:rows (B) if (! row_member (B(i, :), U)) U = [U; B(i, :)]; endif endfor endfunction ## ========================================================================== ## ## Categorical predictors ## ========================================================================== ## ## Which predictors are categorical. An explicit 'CategoricalVars' may name ## them, index them, or mark them with a logical vector; a table additionally ## contributes every column whose class groups its observations. function cat_cols = categorical_flags (cv, pred_names, p, data) cat_cols = false (1, p); if (! isempty (cv)) if (islogical (cv)) n = min (numel (cv), p); cat_cols(1:n) = cv(1:n); elseif (isnumeric (cv)) cat_cols(cv(cv > 0 & cv <= p)) = true; elseif (iscellstr (cv) || ischar (cv)) cv = cellstr (cv); for i = 1:numel (cv) j = find (strcmp (pred_names, cv{i})); if (isempty (j)) error ("stepwiseglm: unknown categorical predictor '%s'.", cv{i}); endif cat_cols(j) = true; endfor else error (strcat ("stepwiseglm: 'CategoricalVars' must be predictor", ... " names, indices, or a logical vector.")); endif endif if (istable (data)) for j = 1:p col = data.(pred_names{j}); if (iscell (col) || isa (col, 'categorical') || islogical (col) ... || isa (col, 'string')) cat_cols(j) = true; endif endfor endif endfunction ## Expand a terms matrix over the P predictors into one over the encoded ## indicator columns. A term naming a categorical predictor becomes one row ## per indicator, so the whole group is fitted, added, and dropped together. function T_enc = expand_terms (T, ctx) if (isempty (ctx.cat_info.names)) T_enc = T; return; endif T_enc = zeros (0, numel (ctx.enc_names) + 1); for r = 1:rows (T) G = expand_term_row (T(r, 1:ctx.p), ctx); T_enc = [T_enc; G, zeros(rows (G), 1)]; endfor endfunction ## The encoded rows of one term. Two categorical factors in an interaction ## multiply out, giving one row per pair of indicators. function G = expand_term_row (row, ctx) p_enc = numel (ctx.enc_names); G = zeros (1, p_enc); for j = find (row != 0) ci = find (strcmp (ctx.cat_info.names, ctx.pred_names{j}), 1); if (isempty (ci)) G(:, find (strcmp (ctx.enc_names, ctx.pred_names{j}), 1)) = row(j); else cols = indicator_cols (ctx.pred_names{j}, ctx.cat_info.levels{ci}, ... ctx.enc_names); Gn = zeros (rows (G) * numel (cols), p_enc); k = 0; for i = 1:rows (G) for c = cols k += 1; Gn(k, :) = G(i, :); Gn(k, c) = 1; endfor endfor G = Gn; endif endfor endfunction ## Encoded columns of a categorical predictor. The reference level has no ## column of its own unless the predictor stood in for a dropped intercept, so ## the levels are looked up by name rather than assumed to start at the second. function cols = indicator_cols (name, levels, enc_names) cols = []; for L = 1:numel (levels) c = find (strcmp (enc_names, [name, '_', char(levels{L})]), 1); if (! isempty (c)) cols(end+1) = c; endif endfor endfunction ## Replace any 'CategoricalVars' pair forwarded to the constructor with the ## resolved predictor names. function nv = set_categorical (nv, names) keep = true (1, numel (nv)); for k = 1:2:numel (nv) - 1 if (strcmpi (nv{k}, 'CategoricalVars')) keep(k:k+1) = false; endif endfor nv = [nv(keep), {'CategoricalVars', names}]; endfunction ## ========================================================================== ## ## Spec resolution and term naming ## ========================================================================== ## ## Resolve a model specification (keyword, terms matrix, or formula) to a terms ## matrix over the P predictors, with the trailing response column. function T = resolve_terms (spec, pred_names, p, intercept) if (ischar (spec) && any (spec == '~')) res = parseWilkinsonFormula (spec, 'expand'); model = res.model; T = zeros (0, p + 1); has_int = false; for i = 1:numel (model) if (isempty (model{i})) has_int = true; else row = term_row (model{i}, pred_names, p); T = [T; row]; endif endfor if (has_int && intercept) T = [zeros(1, p + 1); T]; endif else [T, ~, ~, emsg] = parse_modelspec (spec, pred_names, p, intercept); if (! isempty (emsg)) error ("stepwiseglm: %s", emsg); endif endif endfunction ## Build a terms-matrix row from a cell of factor strings (e.g. {'x1','x2^2'}). function row = term_row (factors, pred_names, p) row = zeros (1, p + 1); for k = 1:numel (factors) f = factors{k}; pw = 1; hat = strfind (f, '^'); if (! isempty (hat)) pw = str2double (f(hat(1)+1:end)); f = f(1:hat(1)-1); endif j = find (strcmp (pred_names, f)); if (isempty (j)) error ("stepwiseglm: unknown predictor '%s' in formula.", f); endif row(j) += pw; endfor endfunction ## Display name of a terms-matrix row (e.g. 'x1:x2', 'x3^2', '(Intercept)'). function nm = term_name (r, pred_names, p) if (all (r(1:p) == 0)) nm = '(Intercept)'; return; endif parts = {}; for j = find (r(1:p) != 0) if (r(j) == 1) parts{end+1} = pred_names{j}; else parts{end+1} = sprintf ("%s^%d", pred_names{j}, r(j)); endif endfor nm = strjoin (parts, ':'); endfunction ## Wilkinson-style formula string for a terms matrix (used for Steps.Start etc). function s = terms_formula (T, resp_name, pred_names, p) has_int = false; parts = {}; for i = 1:rows (T) if (all (T(i, 1:p) == 0)) has_int = true; else parts{end+1} = term_name (T(i, :), pred_names, p); endif endfor if (has_int) rhs = strjoin ([{'1'}, parts], ' + '); elseif (isempty (parts)) rhs = '1'; else rhs = strjoin (parts, ' + '); endif s = sprintf ("%s ~ %s", resp_name, rhs); endfunction ## Sort terms into canonical order: intercept first, then by degree, then by ## the sorted list of predictor indices (matching fitglm's coefficient order). function T = canonical_sort (T, p) m = rows (T); deg = zeros (m, 1); idx = zeros (m, p); # padded index lists for i = 1:m lst = []; for j = find (T(i, 1:p) != 0) lst = [lst, repmat(j, 1, T(i, j))]; endfor deg(i) = numel (lst); idx(i, 1:numel (lst)) = lst; endfor [~, ord] = sortrows ([deg, idx]); T = T(ord, :); endfunction ## ========================================================================== ## ## History ## ========================================================================== ## function h = start_history (T, dev, ncoef, name, stat) h = struct ('Action', 'Start', 'TermName', name, 'Terms', T, ... 'DF', ncoef, 'delDF', NaN, 'Deviance', dev, ... 'Stat', stat, 'PValue', NaN); endfunction ## Name of the Start row: the right-hand side of the starting model, which is ## '1' for a constant start and the whole term list for any other. function nm = start_name (T, pred_names, p) s = terms_formula (T, 'y', pred_names, p); nm = strtrim (s(index (s, '~') + 1:end)); endfunction function h = mkhist (action, r, T, df, ddf, dev, stat, pval, p) ## pred_names not needed here: the term name is resolved by the caller h = struct ('Action', action, 'TermName', r, 'Terms', T, 'DF', df, ... 'delDF', ddf, 'Deviance', dev, 'Stat', stat, 'PValue', pval); endfunction ## Assemble the History table, naming the statistic column after the criterion. function tbl = history_table (hist, crit, estdisp, pred_names, p) m = numel (hist); Action = cell (m, 1); TermName = cell (m, 1); Terms = cell (m, 1); DF = zeros (m, 1); delDF = zeros (m, 1); Deviance = zeros (m, 1); Stat = zeros (m, 1); PValue = zeros (m, 1); for i = 1:m Action{i} = hist(i).Action; if (ischar (hist(i).TermName)) TermName{i} = hist(i).TermName; else TermName{i} = term_name (hist(i).TermName, pred_names, p); endif ## The model's own term order, not the order the search added them in: ## MATLAB's last history row equals the fitted model's terms matrix. Terms{i} = canonical_sort (hist(i).Terms, p); DF(i) = hist(i).DF; delDF(i) = hist(i).delDF; Deviance(i) = hist(i).Deviance; Stat(i) = hist(i).Stat; PValue(i) = hist(i).PValue; endfor ## Which columns the table carries is decided by the criterion: a p-value ## criterion reports the test that drove the step, an information criterion ## reports its own value after the step and has no test to report. The ## deviance is reported only where it is the criterion. switch (crit) case 'deviance' statname = ternary (estdisp, 'FStat', 'Chi2Stat'); tbl = table (Action, TermName, Terms, DF, delDF, Deviance, Stat, ... PValue, 'VariableNames', {'Action', 'TermName', 'Terms', ... 'DF', 'delDF', 'Deviance', statname, 'PValue'}); case 'sse' tbl = table (Action, TermName, Terms, DF, delDF, Stat, PValue, ... 'VariableNames', {'Action', 'TermName', 'Terms', 'DF', ... 'delDF', 'FStat', 'pValue'}); otherwise tbl = table (Action, TermName, Terms, DF, delDF, Stat, ... 'VariableNames', {'Action', 'TermName', 'Terms', 'DF', ... 'delDF', upper(crit)}); endswitch endfunction ## Print an accepted step to the console (Verbose = 1). function print_step (stepnum, h, crit, estdisp, pred_names, p) nm = term_name (h.TermName, pred_names, p); if (strcmp (h.Action, 'Add')) verb = 'Adding'; else verb = 'Removing'; endif switch (crit) case 'deviance' if (estdisp) printf ("%d. %s %s, Deviance = %g, FStat = %g, PValue = %g\n", ... stepnum, verb, nm, h.Deviance, h.Stat, h.PValue); else printf ("%d. %s %s, Deviance = %g, Chi2Stat = %g, PValue = %g\n", ... stepnum, verb, nm, h.Deviance, h.Stat, h.PValue); endif case 'sse' printf ("%d. %s %s, FStat = %g, pValue = %g\n", ... stepnum, verb, nm, h.Stat, h.PValue); case 'aic' printf ("%d. %s %s, AIC = %g\n", stepnum, verb, nm, h.Stat); case 'bic' printf ("%d. %s %s, BIC = %g\n", stepnum, verb, nm, h.Stat); endswitch endfunction ## ========================================================================== ## ## Small helpers ported from GeneralizedLinearModel ## ========================================================================== ## function s = criterion_label (crit, estdisp) switch (crit) case 'deviance' s = ternary (estdisp, 'deviance_f', 'deviance_chi2'); otherwise s = crit; endswitch endfunction function spec = default_link (distr) switch (distr) case 'normal'; spec = 'identity'; case 'binomial'; spec = 'logit'; case 'poisson'; spec = 'log'; case 'gamma'; spec = 'reciprocal'; case 'inverse gaussian'; spec = -2; endswitch endfunction function ll = glm_loglik (distr, y, mu, N, w, phi) rmin = realmin; switch (distr) case 'normal' ne = sum (w); s2 = sum (w .* (y - mu) .^ 2) / ne; ll = -0.5 * ne * (log (2 * pi * s2) + 1); case 'poisson' ll = sum (w .* (y .* log (max (mu, rmin)) - mu - gammaln (y + 1))); case 'binomial' if (isempty (N)) N = ones (size (y)); endif yc = y .* N; ll = sum (w .* (gammaln (N + 1) - gammaln (yc + 1) ... - gammaln (N - yc + 1) + yc .* log (max (mu, rmin)) ... + (N - yc) .* log (max (1 - mu, rmin)))); case 'gamma' a = 1 ./ phi; ll = sum (w .* (a .* log (a) - a .* log (mu) + (a - 1) .* log (y) ... - a .* y ./ mu - gammaln (a))); case 'inverse gaussian' ll = sum (w .* (-0.5 * (log (2 * pi * phi .* y .^ 3) ... + (y - mu) .^ 2 ./ (phi .* mu .^ 2 .* y)))); endswitch endfunction function out = ternary (cond, a, b) if (cond) out = a; else out = b; endif endfunction %!demo %! ## Stepwise Poisson regression: start from a constant model and let the %! ## search add the predictors that matter. %! X = [0.83, -0.68; 0.22, 0.93; -0.12, 0.72; 0.55, -2.55; 1.89, 1.39; ... %! -1.46, -1.18; 1.06, -0.75; -0.89, 0.85; 0.19, -0.71; -0.43, -0.57]; %! y = [2; 2; 2; 1; 9; 0; 2; 1; 1; 1]; %! mdl = stepwiseglm (X, y, 'constant', 'Distribution', 'poisson', ... %! 'Upper', 'linear') %!demo %! ## Stepwise logistic regression selected by AIC, reported from a table. %! X = [0.83, 1.02; 0.22, 0.29; -0.12, 0.09; 0.55, 0.56; 1.89, 1.96; ... %! -1.46, -0.46; 1.06, -0.87; -0.89, 1.18; 0.19, -1.00; -0.43, -0.04]; %! y = [1; 1; 0; 1; 1; 0; 1; 0; 1; 0]; %! tbl = array2table ([X, y], 'VariableNames', {'x1', 'x2', 'y'}); %! mdl = stepwiseglm (tbl, 'constant', 'Distribution', 'binomial', ... %! 'Criterion', 'aic', 'Verbose', 0) ## Shared test data (identical to the MATLAB stepwiseglm verification probes). ## CT and CU carry a three-level categorical predictor; CU adds a second one, ## H, that the response does not depend on, so that it can be dropped. %!shared X, yb, yp, yn, CT, CU, CN %! X = [ 0.83, -0.68, 1.02, 0.02; 0.22, 0.93, 0.29, -0.57; ... %! -0.12, 0.72, 0.09, -1.03; 0.55, -2.55, 0.56, 0.97; ... %! 1.89, 1.39, 1.96, 0.16; -1.46, -1.18, -0.46, 0.33; ... %! 1.06, -0.75, -0.87, 0.08; -0.89, 0.85, 1.18, -1.04; ... %! 0.19, -0.71, -1.00, 1.85; -0.43, -0.57, -0.04, 0.75; ... %! -0.90, -1.75, -0.61, -0.68; 1.52, -0.10, 0.43, -1.16; ... %! 0.58, -1.63, 0.08, -0.73; 0.11, 0.87, -0.40, -0.15; ... %! 1.26, -0.42, -0.95, -1.07; -0.02, 1.09, 1.03, 1.54; ... %! 0.80, 0.97, 0.53, 0.62; -0.40, -1.18, 2.83, -1.36; ... %! -0.61, -0.44, -1.85, -1.57; 1.22, -0.94, -0.26, -0.75; ... %! -0.84, 0.11, -0.55, -0.42; 1.66, -1.27, -1.05, -0.98; ... %! 0.29, 0.59, -0.36, -0.74; -1.10, -1.29, 0.40, 0.35; ... %! 0.08, -0.90, -0.74, -0.51; 1.48, -0.04, -0.11, -1.47; ... %! 0.06, 0.05, -0.09, -1.55; 0.84, 0.13, -1.11, -1.24; ... %! -0.72, -0.02, 0.75, -0.39; 1.11, 0.34, -1.32, -0.18; ... %! 1.48, -1.55, -0.06, 1.28; -0.16, -0.24, 1.34, -1.92; ... %! -0.67, 0.80, 1.01, -0.07; -0.26, -1.10, -0.33, -0.88; ... %! -0.26, -2.42, 0.77, -1.44; -0.62, -0.38, -0.38, -1.77; ... %! 0.41, 0.90, 0.85, 0.72; -1.16, 0.99, 0.74, -2.00; ... %! -0.44, -1.22, 1.06, -1.14; 1.85, 0.21, 0.65, -0.11]; %! yb = [1 1 0 1 1 0 1 0 1 0 0 1 0 1 1 1 1 1 0 1 0 0 0 0 0 1 0 1 0 1 ... %! 0 1 0 0 1 0 1 0 0 1]'; %! yp = [2 2 2 1 9 0 2 1 1 1 0 3 1 2 2 2 3 1 1 2 1 2 2 0 1 3 1 2 1 3 ... %! 2 1 1 1 0 1 3 1 0 5]'; %! yn = [3.17 1.66 0.99 3.87 6.22 1.34 4.8 0.58 4.05 2.06 2.81 3.97 3.84 ... %! 1.11 4.26 2.57 3.07 -1.07 1.7 4.58 0.47 6.41 1.55 1.02 3.29 3.81 ... %! 1.58 2.65 0.81 2.94 5.93 1.09 1.06 2.55 1.44 0.97 2.8 -0.47 0.81 ... %! 4.7]'; %! lev = {'A', 'B', 'C'}'; %! a1 = ((1:48)' - 24.5)/12; %! a2 = sin ((1:48)'/5); %! ag = lev(mod ((0:47)', 3) + 1); %! ay = [0 1 0 1 2 0 1 2 0 1 2 1 1 2 1 1 3 1 1 3 1 1 3 1 1 4 1 2 5 1 3 8 ... %! 2 4 11 3 5 15 4 7 18 4 8 20 4 8 20 5]'; %! CT = table (a1, a2, ag, ay, 'VariableNames', {'x1', 'x2', 'g', 'y'}); %! b1 = ((1:60)' - 30.5)/15; %! bg = lev(mod ((0:59)', 3) + 1); %! bh = lev(mod (floor ((0:59)'/2), 3) + 1); %! by = [2 0 7 2 0 6 2 0 6 2 1 5 2 1 5 2 1 5 2 1 4 3 2 4 3 2 4 3 3 3 3 4 ... %! 3 4 5 3 4 6 3 4 8 2 5 11 2 5 14 2 5 18 2 6 23 2 6 30 2 7 39 2]'; %! CU = table (b1, bg, bh, by, 'VariableNames', {'x1', 'g', 'h', 'y'}); %! CN = table (b1, bg, bh, by / 4, 'VariableNames', {'x1', 'g', 'h', 'y'}); ## Test results (values verified against MATLAB's stepwiseglm) %!test %! ## Binomial: the Deviance criterion selects x1 and x3. %! mdl = stepwiseglm (X, yb, 'constant', 'Distribution', 'binomial', ... %! 'Upper', 'interactions', 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, {'(Intercept)', 'x1', 'x3'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [-0.567847008659897; 2.24005553968806; 1.10257647012], 1e-8); %! assert_equal (mdl.Deviance, 35.424118776089266, 1e-9); %! assert_equal (mdl.LogLikelihood, -17.712059388044633, 1e-9); %!test %! ## Poisson: selects x1 and x2 (interaction not significant). %! mdl = stepwiseglm (X, yp, 'constant', 'Distribution', 'poisson', ... %! 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, {'(Intercept)', 'x1', 'x2'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [0.242293607853268; 0.672082935896644; 0.459103523205052], 1e-8); %! assert_equal (mdl.Deviance, 5.62441316952502, 1e-9); %!test %! ## Normal (estimated dispersion, F-test): mains plus x1:x4 and x2:x3. %! mdl = stepwiseglm (X, yn, 'constant', 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, ... %! {'(Intercept)', 'x1', 'x2', 'x3', 'x4', 'x1:x4', 'x2:x3'}); %! assert_equal (mdl.NumCoefficients, 7); %! assert_equal (any (strcmp (mdl.CoefficientNames, 'x2:x3')), true); %!test %! ## The AIC criterion is greedier and also brings in x3:x4. %! mdl = stepwiseglm (X, yn, 'linear', 'Upper', 'interactions', ... %! 'Criterion', 'aic', 'Verbose', 0); %! assert_equal (mdl.NumCoefficients, 8); %! assert_equal (any (strcmp (mdl.CoefficientNames, 'x3:x4')), true); %!test %! ## BIC reaches the same eight-coefficient model here. %! mdl = stepwiseglm (X, yn, 'constant', 'Criterion', 'bic', 'Verbose', 0); %! assert_equal (mdl.NumCoefficients, 8); %! assert_equal (any (strcmp (mdl.CoefficientNames, 'x3:x4')), true); %!test %! ## A formula-valued Upper bounds the candidate universe. %! mdl = stepwiseglm (X, yp, 'y ~ x1', 'Distribution', 'poisson', ... %! 'Upper', 'y ~ x1 + x2 + x3', 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, {'(Intercept)', 'x1', 'x2'}); %!test %! ## Lower/Upper keywords: search within [linear, quadratic] adds interactions. %! mdl = stepwiseglm (X, yn, 'linear', 'Lower', 'linear', ... %! 'Upper', 'quadratic', 'Verbose', 0); %! assert_equal (mdl.NumCoefficients, 7); %! assert_equal (any (strcmp (mdl.CoefficientNames, 'x2:x3')), true); %! assert_equal (any (strcmp (mdl.CoefficientNames, 'x1:x4')), true); %!test %! ## The Steps property records the trace; History matches the deviance test. %! mdl = stepwiseglm (X, yb, 'constant', 'Distribution', 'binomial', ... %! 'Upper', 'interactions', 'Verbose', 0); %! assert_equal (mdl.Steps.Criterion, 'deviance_chi2'); %! assert_equal (size (mdl.Steps.History, 1), 3); %! assert_equal (mdl.Steps.History.Action{1}, 'Start'); %! assert_equal (mdl.Steps.History.Chi2Stat(2), 14.8398950335268, 1e-6); %!test %! ## Table input, response taken from the last column by default. %! tbl = array2table ([X(:,1:2), yp], 'VariableNames', {'a', 'b', 'y'}); %! mdl = stepwiseglm (tbl, 'constant', 'Distribution', 'poisson', ... %! 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, {'(Intercept)', 'a', 'b'}); %!test %! ## Collinear predictors are fitted, not refused; the redundant column is %! ## dropped rather than entering the model. %! mdl = stepwiseglm ([X(:,1), X(:,1)], yn, 'constant', 'Verbose', 0); %! assert_equal (class (mdl), 'GeneralizedLinearModel'); %! assert_equal (mdl.CoefficientNames, {'(Intercept)', 'x1'}); %!test %! ## A cell-array column of a table is taken as categorical without being %! ## named, and the fit matches MATLAB R2024a. %! mdl = stepwiseglm (CT, 'y ~ 1', 'Upper', 'y ~ x1 + x2 + g', ... %! 'Distribution', 'poisson', 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, ... %! {'(Intercept)', 'x1', 'x2', 'g_B', 'g_C'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [0.603329330791641; 0.796711055930047; 0.285851899335031; ... %! 0.915480956228751; -0.550531653341791], 1e-10); %! assert_equal (mdl.Deviance, 5.49511881200541, 1e-10); %!test %! ## The indicators of a categorical predictor enter as one term, so the %! ## step is worth L-1 degrees of freedom and is named for the predictor. %! mdl = stepwiseglm (CT, 'y ~ 1', 'Upper', 'y ~ x1 + x2 + g', ... %! 'Distribution', 'poisson', 'Verbose', 0); %! assert_equal (mdl.Steps.History.TermName, {'1'; 'x1'; 'g'; 'x2'}); %! assert_equal (mdl.Steps.History.DF, [1; 2; 4; 5]); %! assert_equal (mdl.Steps.History.delDF, [NaN; 1; 2; 1]); %!test %! ## The deviance test of a grouped term is the joint test of its indicators. %! mdl = stepwiseglm (CT, 'y ~ 1', 'Upper', 'y ~ x1 + x2 + g', ... %! 'Distribution', 'poisson', 'Verbose', 0); %! assert_equal (mdl.Steps.History.Deviance, ... %! [220.977093395079; 81.2408117834874; 10.3794318146782; ... %! 5.49511881200541], 1e-9); %! assert_equal (mdl.Steps.History.Chi2Stat, ... %! [NaN; 139.736281611591; 70.8613799688091; ... %! 4.88431300267280], 1e-9); %! assert_equal (mdl.Steps.History.PValue(4), 0.0271018179857880, 1e-12); %!test %! ## An interaction naming a categorical predictor is also one term, and its %! ## coefficients are named for the indicators. %! mdl = stepwiseglm (CU, 'y ~ 1', 'Upper', 'y ~ x1*g', ... %! 'Distribution', 'poisson', 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, {'(Intercept)', 'x1', 'g_B', 'g_C', ... %! 'x1:g_B', 'x1:g_C'}); %! assert_equal (mdl.Steps.History.TermName, {'1'; 'x1'; 'g'; 'x1:g'}); %! assert_equal (mdl.Steps.History.delDF, [NaN; 1; 2; 2]); %! assert_equal (mdl.Coefficients.Estimate, ... %! [1.21521326020005; 0.372340711075598; -0.0523666888775920; ... %! 0; 0.950243386703989; -0.744681422151197], 1e-9); %!test %! ## A grouped term leaves in one step too, and its delDF is negative. %! mdl = stepwiseglm (CU, 'y ~ x1 + g + h', 'Lower', 'y ~ 1', 'Upper', ... %! 'y ~ x1 + g + h', 'Distribution', 'poisson', ... %! 'Verbose', 0); %! assert_equal (mdl.Formula.LinearPredictor, '1 + x1 + g'); %! assert_equal (mdl.Steps.History.Action, {'Start'; 'Remove'}); %! assert_equal (mdl.Steps.History.TermName, {'1 + x1 + g + h'; 'h'}); %! assert_equal (mdl.Steps.History.DF, [6; 4]); %! assert_equal (mdl.Steps.History.delDF, [NaN; -2]); %! assert_equal (mdl.Steps.History.Chi2Stat, [NaN; 0.914420851291737], 1e-10); %!test %! ## A categorical array is coded by its categories, a cell array by the %! ## order its labels appear; on this fixture the two agree. %! ct = CT; %! ct.g = categorical (ct.g); %! m1 = stepwiseglm (ct, 'y ~ 1', 'Upper', 'y ~ x1 + x2 + g', ... %! 'Distribution', 'poisson', 'Verbose', 0); %! m2 = stepwiseglm (CT, 'y ~ 1', 'Upper', 'y ~ x1 + x2 + g', ... %! 'Distribution', 'poisson', 'Verbose', 0); %! assert_equal (m1.CoefficientNames, m2.CoefficientNames); %! assert_equal (m1.Coefficients.Estimate, m2.Coefficients.Estimate, 1e-12); %!test %! ## A column of a predictor matrix is categorical only when named, and the %! ## levels are then its sorted distinct values. %! Xm = [((1:60)' - 30.5)/15, mod((0:59)', 3) + 1, ... %! mod(floor((0:59)'/2), 3) + 1]; %! ym = CU.y; %! mdl = stepwiseglm (Xm, ym, 'constant', 'Upper', 'linear', ... %! 'Distribution', 'poisson', ... %! 'CategoricalVars', logical ([0, 1, 1]), 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, {'(Intercept)', 'x1', 'x2_2', 'x2_3'}); %! assert_equal (mdl.Steps.History.TermName, {'1'; 'x1'; 'x2'}); %! assert_equal (mdl.Steps.History.delDF, [NaN; 1; 2]); %!test %! ## Indices and a logical vector mark the same columns. %! Xm = [((1:60)' - 30.5)/15, mod((0:59)', 3) + 1, ... %! mod(floor((0:59)'/2), 3) + 1]; %! ym = CU.y; %! m1 = stepwiseglm (Xm, ym, 'constant', 'Upper', 'linear', ... %! 'Distribution', 'poisson', 'CategoricalVars', [2, 3], ... %! 'Verbose', 0); %! m2 = stepwiseglm (Xm, ym, 'constant', 'Upper', 'linear', ... %! 'Distribution', 'poisson', ... %! 'CategoricalVars', logical ([0, 1, 1]), 'Verbose', 0); %! assert_equal (m1.Coefficients.Estimate, m2.Coefficients.Estimate, 1e-12); %!test %! ## The Start row names the starting model, not the constant, and reports %! ## the coefficient count it was fitted with. %! mdl = stepwiseglm (CU, 'y ~ x1 + g', 'Lower', 'y ~ x1 + g', 'Upper', ... %! 'y ~ x1 + g', 'Distribution', 'poisson', 'Verbose', 0); %! assert_equal (mdl.Steps.History.TermName, {'1 + x1 + g'}); %! assert_equal (mdl.Steps.History.DF, 4); %!test %! ## An estimated dispersion turns the deviance test into an F test, and the %! ## criterion is labelled for it. %! mdl = stepwiseglm (CN, 'y ~ 1', 'Upper', 'y ~ x1 + g + h', ... %! 'Distribution', 'normal', 'Verbose', 0); %! assert_equal (mdl.Steps.Criterion, 'deviance_f'); %! assert_equal (mdl.Steps.History.Properties.VariableNames, ... %! {'Action', 'TermName', 'Terms', 'DF', 'delDF', 'Deviance', ... %! 'FStat', 'PValue'}); %! assert_equal (mdl.Steps.History.FStat, ... %! [NaN; 15.1516937210352; 4.75429941449063], 1e-10); %!test %! ## Under 'sse' the history reports the F test alone, with MATLAB's own %! ## lower-case column name. %! mdl = stepwiseglm (CN, 'y ~ 1', 'Upper', 'y ~ x1 + g + h', ... %! 'Distribution', 'normal', 'Criterion', 'sse', ... %! 'Verbose', 0); %! assert_equal (mdl.Steps.History.Properties.VariableNames, ... %! {'Action', 'TermName', 'Terms', 'DF', 'delDF', 'FStat', ... %! 'pValue'}); %! assert_equal (mdl.Steps.History.pValue, ... %! [NaN; 0.000258691112717; 0.012385457865093], 1e-12); %!test %! ## An information criterion has no test to report, so the history carries %! ## its value instead, the starting model included. %! mdl = stepwiseglm (CU, 'y ~ 1', 'Upper', 'y ~ x1 + g + h', ... %! 'Distribution', 'poisson', 'Criterion', 'aic', ... %! 'Verbose', 0); %! assert_equal (mdl.Steps.History.Properties.VariableNames, ... %! {'Action', 'TermName', 'Terms', 'DF', 'delDF', 'AIC'}); %! assert_equal (mdl.Steps.History.AIC, ... %! [504.960173118363; 394.245450753972; 341.770323510125], 1e-9); %!test %! ## A p-value far below eps is computed on the upper tail, not as one minus %! ## the lower, so it does not round to zero. %! mdl = stepwiseglm (CU, 'y ~ 1', 'Upper', 'y ~ x1*g', ... %! 'Distribution', 'poisson', 'Verbose', 0); %! assert_equal (mdl.Steps.History.PValue(2), 2.49165e-26, 1e-30); %! assert_equal (mdl.Steps.History.PValue(4), 1.19350e-32, 1e-36); %!test %! ## Each history row carries the terms in the model's own order, not in the %! ## order the search added them, so the last row is the fitted model's. %! mdl = stepwiseglm (CT, 'y ~ 1', 'Upper', 'y ~ x1 + x2 + g', ... %! 'Distribution', 'poisson', 'Verbose', 0); %! assert_equal (mdl.Steps.History.TermName, {'1'; 'x1'; 'g'; 'x2'}); %! assert_equal (mdl.Steps.History.Terms{end}, mdl.Formula.Terms); %! assert_equal (mdl.Steps.History.Terms{end}, ... %! [0, 0, 0, 0; 1, 0, 0, 0; 0, 1, 0, 0; 0, 0, 1, 0]); %!test %! ## Start, Lower, and Upper are formula objects carrying the link. %! mdl = stepwiseglm (CT, 'y ~ 1', 'Upper', 'y ~ x1 + x2 + g', ... %! 'Distribution', 'poisson', 'Verbose', 0); %! assert_equal (class (mdl.Steps.Start), 'LinearFormula'); %! assert_equal (char (mdl.Steps.Start), 'log(y) ~ 1'); %! assert_equal (char (mdl.Steps.Upper), 'log(y) ~ 1 + x1 + x2 + g'); %! assert_equal (mdl.Steps.Lower.LinearPredictor, '1'); ## Test input validation %!error stepwiseglm () %!error stepwiseglm ("a", [1;2]) %!error ... %! stepwiseglm ([1, 2; 3, 4], [1; 0], 'Distribution', 'wibble') %!error ... %! stepwiseglm ([1, 2; 3, 4], [1; 0], 'Criterion', 'nope') %!error ... %! stepwiseglm ([1, 2; 3, 4], [1; 0], 'linear', 'foo', 1) %!error ... %! stepwiseglm ([1, 2; 3, 4], [1; 0], 'CategoricalVars', {'nope'}) %!error ... %! stepwiseglm ([1, 2; 3, 4], [1; 0], 'CategoricalVars', {1}) %!test # a two-column binomial response matches MATLAB R2024a %! x = (1:10)'; %! S = [0 1 1 2 3 4 6 7 9 9]'; %! mdl = stepwiseglm (x, [S, 10 * ones(10, 1)], 'constant', 'upper', ... %! 'linear', 'Distribution', 'binomial', 'Verbose', 0); %! assert_equal (mdl.Coefficients.Estimate, ... %! [-4.07619632416318; 0.639874139429377], 1e-10); %! assert_equal (mdl.Deviance, 1.37328920133713, 1e-10); %!test # the two-column and BinomialSize forms select the same model %! x = (1:10)'; %! S = [0 1 1 2 3 4 6 7 9 9]'; %! N = 10 * ones (10, 1); %! m1 = stepwiseglm (x, [S, N], 'constant', 'upper', 'linear', ... %! 'Distribution', 'binomial', 'Verbose', 0); %! m2 = stepwiseglm (x, S, 'constant', 'upper', 'linear', ... %! 'Distribution', 'binomial', 'BinomialSize', N, ... %! 'Verbose', 0); %! assert_equal (m2.Coefficients.Estimate, m1.Coefficients.Estimate, 1e-12); statistics-release-2.1.2/inst/Regression/stepwiselm.m000066400000000000000000003071751527311104300230050ustar00rootroot00000000000000## Copyright (C) 2026 Avanish Salunke ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{mdl} =} stepwiselm (@var{tbl}) ## @deftypefnx {statistics} {@var{mdl} =} stepwiselm (@var{tbl}, @var{ResponseVarName}) ## @deftypefnx {statistics} {@var{mdl} =} stepwiselm (@var{tbl}, @var{y}) ## @deftypefnx {statistics} {@var{mdl} =} stepwiselm (@var{X}, @var{y}) ## @deftypefnx {statistics} {@var{mdl} =} stepwiselm (@dots{}, @var{InitialModel}) ## @deftypefnx {statistics} {@var{mdl} =} stepwiselm (@dots{}, @var{Name}, @var{Value}, @dots{}) ## ## Fit a linear regression model using stepwise regression and return a ## @code{LinearModel} object. ## ## @code{stepwiselm} starts from an initial model and repeatedly searches for ## a term to add to, or remove from, the current model, based on the value of ## the @qcode{'Criterion'} option, until no single addition or removal ## improves the model any further. ## ## @subheading Basic Syntax ## ## @itemize ## @item ## @code{@var{mdl} = stepwiselm (@var{tbl})} fits a stepwise model using the ## variables in the table (or dataset) @var{tbl}, starting from a constant ## model. By default, the last variable in @var{tbl} is used as the response ## and all other variables are candidate predictors. Variables that are ## @code{categorical} arrays, cell arrays of character vectors, or logical ## arrays are automatically treated as categorical predictors. ## @item ## @code{@var{mdl} = stepwiselm (@var{tbl}, @var{ResponseVarName})} uses the ## variable named @var{ResponseVarName} in @var{tbl} as the response, and all ## remaining variables in @var{tbl} as candidate predictors. ## @item ## @code{@var{mdl} = stepwiselm (@var{tbl}, @var{y})} uses the variables in ## @var{tbl} as candidate predictors and the external numeric vector @var{y} ## as the response. ## @item ## @code{@var{mdl} = stepwiselm (@var{X}, @var{y})} fits a stepwise model of ## the response @var{y} to the predictor data @var{X}, an @math{N*P} numeric ## or logical matrix. By default, the predictors are named @qcode{'x1'}, ## @qcode{'x2'}, @dots{}, @qcode{'xP'} and the response is named ## @qcode{'y'}. @var{y} is a vector with one element per row of @var{X}; a ## row vector is taken as a column. ## @end itemize ## ## @subheading Initial Model, and Lower/Upper Bounds ## ## @code{@var{mdl} = stepwiselm (@dots{}, @var{InitialModel})} additionally ## specifies the model to start the stepwise search from, using any of the ## input combinations shown above. @var{InitialModel} can be any of the ## following, and the same set of values can also be used for the ## @qcode{'Lower'} and @qcode{'Upper'} options below, which bound the ## smallest and largest set of terms @code{stepwiselm} is allowed to reach. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Value} @tab @var{Description} ## ## @item @qcode{'constant'} @tab Model contains only an intercept term. This ## is the default @var{InitialModel} and default @qcode{'Lower'} bound. ## ## @item @qcode{'linear'} @tab Model contains an intercept and one term ## for each predictor variable. ## ## @item @qcode{'interactions'} @tab Model contains an intercept, all ## linear terms, and all pairwise products of distinct predictor variables ## (no squared terms). This is the default @qcode{'Upper'} bound. ## ## @item @qcode{'purequadratic'} @tab Model contains an intercept, all ## linear terms, and all squared terms. ## ## @item @qcode{'quadratic'} @tab Model contains an intercept, all linear ## terms, all pairwise products of distinct predictor variables, and all ## squared terms. ## ## @item @qcode{'polyijk'} @tab Model is a polynomial with maximum degree ## @math{i} in the first predictor, @math{j} in the second, and so on, given ## as a run of single-digit numerals, one per predictor (e.g. ## @qcode{'poly21'} for two predictors). The model contains interaction ## terms, but the degree of each interaction term never exceeds the largest ## of the specified per-predictor degrees. ## ## @item terms matrix @tab A @math{T*P} or @math{T*(P+1)} numeric matrix, ## where @math{T} is the number of terms and @math{P} is the number of ## predictor variables, following the same convention as @code{fitlm}'s ## terms matrix. When @var{InitialModel} is given as a terms matrix, the ## @qcode{'PredictorVars'} option may not also be used. ## ## @item Wilkinson formula @tab A character vector of the form ## @qcode{'y ~ terms'}. When a formula is combined with @qcode{'ResponseVar'} ## or @qcode{'PredictorVars'}, the formula's response and predictor terms ## must agree with those options, or @code{stepwiselm} errors. ## @end multitable ## ## @subheading Options ## ## @code{@var{mdl} = stepwiselm (@dots{}, @var{Name}, @var{Value}, @dots{})} ## specifies additional options using one or more @qcode{Name-Value} pair ## arguments. ## ## @multitable @columnfractions 0.18 0.8 ## @headitem @var{Name} @tab @var{Value} ## ## @item @qcode{'Criterion'} @tab Criterion used to decide whether a term is ## added or removed at each step. One of @qcode{'sse'} (default), ## @qcode{'aic'}, @qcode{'bic'}, @qcode{'rsquared'}, or @qcode{'adjrsquared'}. ## For @qcode{'sse'}, the p-value of an F-test comparing the model with and ## without the candidate term is used; for the others, the raw change in the ## named quantity is used directly against @qcode{'PEnter'}/@qcode{'PRemove'}. ## ## @item @qcode{'PEnter'} @tab Threshold to add a term. Defaults depend on ## @qcode{'Criterion'}: @qcode{0.05} for @qcode{'sse'}, @qcode{0} for ## @qcode{'aic'}/@qcode{'bic'}, @qcode{0.1} for @qcode{'rsquared'}, @qcode{0} ## for @qcode{'adjrsquared'}. ## ## @item @qcode{'PRemove'} @tab Threshold to remove a term. Defaults depend ## on @qcode{'Criterion'}: @qcode{0.10} for @qcode{'sse'}, @qcode{0.01} for ## @qcode{'aic'}/@qcode{'bic'}, @qcode{0.05} for @qcode{'rsquared'}, ## @qcode{-0.05} for @qcode{'adjrsquared'}. ## ## @item @qcode{'NSteps'} @tab Maximum number of add/remove steps to take, ## given as a nonnegative integer. Default is unlimited. @qcode{'NSteps'} ## set to @qcode{0} returns the initial model unchanged. ## ## @item @qcode{'Lower'} @tab Model specification (in the same form as ## @var{InitialModel}, above) describing terms that may never be removed from ## the model. Terms in @qcode{'Lower'} are protected from removal, but are ## not automatically added if absent from @var{InitialModel}. Default is ## @qcode{'constant'}. ## ## @item @qcode{'Upper'} @tab Model specification (in the same form as ## @var{InitialModel}, above) describing the largest set of terms ## @code{stepwiselm} may add. Default is @qcode{'interactions'}. ## ## @item @qcode{'Verbose'} @tab Controls how much progress information is ## printed while stepping. @qcode{0} suppresses all output, @qcode{1} ## (default) prints the action taken at each step, @qcode{2} additionally ## prints the p-value or criterion value considered for every candidate term ## examined at each step. ## ## @item @qcode{'Intercept'} @tab A logical scalar indicating whether the ## initial model includes a constant (intercept) term. Only applies when ## @var{InitialModel} is a character vector model name (or omitted); ignored ## when @var{InitialModel} is a terms matrix or formula. Default is ## @qcode{true}. ## ## @item @qcode{'Weights'} @tab A numeric vector of nonnegative observation ## weights, one per observation. Default is a vector of ones. ## ## @item @qcode{'Exclude'} @tab A numeric or logical vector specifying ## observations to exclude from the fit. ## ## @item @qcode{'CategoricalVars'} @tab Specifies which predictor variables ## are treated as categorical, given as a vector of column indices, a logical ## vector, or a cell array of variable names (table input only). Each ## categorical predictor with @math{L} distinct categories is expanded into ## @math{L-1} indicator variables, and @code{stepwiselm} always adds or ## removes that entire group of indicator variables together, in a single ## step, treating the categorical predictor as one term. ## ## @item @qcode{'VarNames'} @tab A cell array of character vectors naming the ## predictor and response variables, response last. Only applies when ## @var{X} and @var{y} are supplied directly, not table input. ## ## @item @qcode{'ResponseVar'} @tab A character vector naming the response ## variable, overriding the response that would otherwise be inferred (the ## last table variable, or @qcode{'y'} for matrix input). ## ## @item @qcode{'PredictorVars'} @tab A cell array of character vectors ## naming which variables in @var{tbl} to consider as candidate predictors. ## By default, all variables in @var{tbl} other than the response are used. ## May not be combined with a terms-matrix @var{InitialModel}, and must ## agree with any formula-based @var{InitialModel}. ## @end multitable ## ## @subsubheading The @code{Steps} property ## ## The returned model's @code{Steps} property records the trace, as a ## structure with seven fields. @code{Start}, @code{Lower}, and @code{Upper} ## are @code{LinearFormula} objects for the starting model and the two bounds; ## @code{Criterion} is the criterion as it was asked for; @code{PEnter} and ## @code{PRemove} are the thresholds it ran under; and @code{History} is a ## table with one row per step. ## ## @code{History} always carries @code{Action} (@qcode{'Start'}, ## @qcode{'Add'}, or @qcode{'Remove'}), @code{TermName}, @code{Terms} (the ## terms matrix after the step, over the model's variables), @code{DF} (the ## coefficient count after the step), and @code{delDF} (the change in it, ## @emph{negative} for a removal). The remaining columns follow the ## criterion, which is why a history is read by name and not by position: ## ## @multitable @columnfractions 0.25 0.7 ## @headitem Criterion @tab Further columns ## @item @qcode{'SSE'} @tab @code{FStat} and @code{pValue}. ## @item @qcode{'AIC'}, @qcode{'BIC'}, @qcode{'Rsquared'}, ## @qcode{'AdjRsquared'} @tab one column named for the criterion, holding its ## value after the step, the starting model included. ## @end multitable ## ## The first row is the starting model, named by its right-hand side. ## @code{step} appends to the history it inherits rather than beginning a new ## one, and inherits @code{Criterion}, @code{Lower}, @code{PEnter}, and ## @code{PRemove} from it; @code{Steps.Start} is then the model stepped from. ## ## @subheading Algorithm ## ## At each step, @code{stepwiselm} examines every term not currently in the ## model but within the @qcode{'Upper'} bound, and every term currently in ## the model but not protected by the @qcode{'Lower'} bound. If any term ## outside the model would improve it by at least @qcode{'PEnter'}, the best ## such term is added; otherwise, if any term inside the model falls short of ## @qcode{'PRemove'}, the worst such term is removed. The process repeats ## until neither an addition nor a removal improves the model, or until ## @qcode{'NSteps'} steps have been taken. ## ## @code{stepwiselm} never adds a higher-order term unless all of its ## lower-order marginal terms are already in the model (e.g. it will not add ## @code{x1:x2^2} unless both @code{x1} and @code{x2^2} are already present), ## and correspondingly never removes a lower-order term that a higher-order ## term still in the model depends on. At every step, if a term in the ## current model is found to be exactly redundant (linearly dependent on the ## other terms already in the model), it is removed immediately regardless of ## the @qcode{'Criterion'} value. ## ## Because the final model depends on the initial model and the order in ## which terms are considered, @code{stepwiselm} finds a locally, but not ## necessarily globally, optimal model. ## ## Robust fitting cannot be combined with stepwise regression; do not pass ## @qcode{'RobustOpts'} to @code{stepwiselm}. ## ## @var{mdl} is returned as a @code{LinearModel} object. See also the ## @code{step} method of @code{LinearModel}, which performs a single bounded ## round of stepwise search starting from an already-fitted model. ## ## @seealso{LinearModel, fitlm} ## @end deftypefn function mdl = stepwiselm (varargin) if (nargin < 1) error ("stepwiselm: Not enough input arguments."); endif sw_keys = {'criterion','penter','premove','nsteps','verbose','lower','upper'}; is_sw = @(s) (ischar (s) || isstring (s)) && any (strcmpi (char (s), sw_keys)); criterion = 'sse'; ## Steps.Criterion reports the criterion as it was asked for; unasked, it ## reports MATLAB's own spelling of the default. crit_label = 'SSE'; penter = []; premove = []; nsteps = Inf; verbose = 1; lower_sp = 'constant'; upper_sp = 'interactions'; fit_args = {}; i = 1; n = numel (varargin); while (i <= n) a = varargin{i}; if (is_sw (a)) if (i == n) error ("stepwiselm: Name-Value arguments must be in pairs."); endif key = lower (char (a)); val = varargin{i+1}; if (strcmp (key, 'criterion')) if (! (ischar (val) || isstring (val)) || ... ! any (strcmpi (char (val), {'sse','aic','bic','rsquared','adjrsquared'}))) error (["stepwiselm: '" char(val) "' is not a valid value for the" ... " 'Criterion' argument. Valid values are: 'AIC', 'BIC'," ... " 'Rsquared', 'AdjRsquared', 'SSE'."]); endif criterion = lower (char (val)); crit_label = char (val); elseif (strcmp (key, 'penter')) penter = double (val); elseif (strcmp (key, 'premove')) premove = double (val); elseif (strcmp (key, 'nsteps')) nsteps = double (val); elseif (strcmp (key, 'verbose')) verbose = double (val); elseif (strcmp (key, 'lower')) lower_sp = val; elseif (strcmp (key, 'upper')) upper_sp = val; endif i += 2; else fit_args{end+1} = a; i += 1; endif endwhile if (isempty (penter)) pe_tbl = struct ('sse', 0.05, 'aic', 0, 'bic', 0, 'rsquared', 0.1, 'adjrsquared', 0); penter = pe_tbl.(criterion); endif if (isempty (premove)) pr_tbl = struct ('sse', 0.10, 'aic', 0.01, 'bic', 0.01, 'rsquared', 0.05, 'adjrsquared', -0.05); premove = pr_tbl.(criterion); endif ## A term must not qualify to enter and to leave at the same time. Where ## the thresholds are p-values PEnter sits below PRemove; where they are an ## improvement in the criterion the sense is reversed. Without this the ## search adds and drops one term forever, NSteps being unlimited by ## default, so the call never returns. if (any (strcmp (criterion, {'sse', 'aic', 'bic'}))) if (penter >= premove) error (strcat ("stepwiselm: PEnter (%g) must be less than PRemove", ... " (%g) for the '%s' criterion."), penter, premove, criterion); endif elseif (penter <= premove) error (strcat ("stepwiselm: PEnter (%g) must be greater than PRemove", ... " (%g) for the '%s' criterion."), penter, premove, criterion); endif fit_args = sw_ensure_modelspec (fit_args); sw_predictorvars_matrix_conflict (fit_args); [positional, first_nv] = sw_split_positional (fit_args); init_spec = positional{end}; nv_part = fit_args(first_nv:end); resp_name = sw_response_name (positional, nv_part); probe_positional = positional; probe_positional{end} = 'constant'; probe_nv = sw_set_responsevar (nv_part, resp_name); probe_args = [probe_positional, probe_nv]; mdl0 = fitlm (probe_args{:}); if (! isempty (mdl0.Robust)) error ("stepwiselm: Robust fitting cannot be combined with stepwise regression."); endif info = LinearModel.sw_extract (mdl0); cat_mask = false (1, info.p_raw); if (! isempty (info.cat_info.names)) for j = 1:info.p_raw cat_mask(j) = any (strcmp (info.cat_info.names, info.pred_names{j})); endfor endif T_init_raw = sw_resolve_bound (init_spec, info.pred_names, cat_mask, info); T_lower_raw = sw_resolve_bound (lower_sp, info.pred_names, cat_mask, info); T_upper_raw = sw_resolve_bound (upper_sp, info.pred_names, cat_mask, info); terms_cur = sw_raw_terms_to_encoded (T_init_raw, info.pred_names, info.cat_info, info.enc_names); T_start_enc = terms_cur; T_lower_enc = sw_raw_terms_to_encoded (T_lower_raw, info.pred_names, info.cat_info, info.enc_names); T_upper_enc = sw_raw_terms_to_encoded (T_upper_raw, info.pred_names, info.cat_info, info.enc_names); groups_upper = sw_group_encoded_terms (T_upper_enc, info.pred_names, info.cat_info, info.enc_names); groups_lower = sw_group_encoded_terms (T_lower_enc, info.pred_names, info.cat_info, info.enc_names); cur_hi = any (all (terms_cur(:,1:end-1) == 0, 2)); cur_fit = LinearModel.lm_fit (build_design (terms_cur, info.X_enc), info.y, info.w, false); ## The trace reported as Steps.History. Its first row is the starting ## model, named by its right-hand side and carrying no test. hist = sw_hist_row ('Start', sw_terms_rhs (terms_cur, info), terms_cur, ... rows (terms_cur), NaN, ... sw_start_stat (criterion, cur_fit, cur_hi), NaN, info); step_no = 0; any_step = false; while (step_no < nsteps) groups_cur = sw_group_encoded_terms (terms_cur, info.pred_names, info.cat_info, info.enc_names); cur_raws = sw_group_raws (groups_cur); add_cands = sw_add_candidates (groups_upper, cur_raws); best_add = []; best_add_score = []; for c = 1:numel (add_cands) g = add_cands{c}; trial_terms = [terms_cur; g.term_rows]; trial_hi = any (all (trial_terms(:,1:end-1) == 0, 2)); trial_fit = LinearModel.lm_fit (build_design (trial_terms, info.X_enc), info.y, info.w, false); score = sw_criterion_score (criterion, info.n_obs, cur_hi, trial_hi, cur_fit, trial_fit); if (verbose >= 2) sw_print_candidate (criterion, 'add', g.name, score); endif if (isempty (best_add) || sw_prefer (criterion, 'add', score, best_add_score)) best_add = g; best_add.fit = trial_fit; best_add.hi = trial_hi; best_add_score = score; endif endfor if (isempty (add_cands) && verbose >= 2) printf ("No candidate terms to add\n"); endif if (! isempty (best_add) && sw_threshold_ok (criterion, 'add', best_add_score, penter)) prev_df = rows (terms_cur); terms_cur = [terms_cur; best_add.term_rows]; cur_fit = best_add.fit; cur_hi = best_add.hi; [hs, hp] = sw_hist_stat (criterion, 'add', best_add_score); hist(end+1) = sw_hist_row ('Add', best_add.name, terms_cur, ... rows (terms_cur), ... rows (terms_cur) - prev_df, hs, hp, info); step_no += 1; any_step = true; if (verbose >= 1) sw_print_action (criterion, step_no, 'Adding', best_add.name, best_add_score); endif continue; endif groups_cur = sw_group_encoded_terms (terms_cur, info.pred_names, info.cat_info, info.enc_names); cur_raws = sw_group_raws (groups_cur); lower_raws = sw_group_raws (groups_lower); rem_cands = sw_remove_candidates (groups_cur, lower_raws, cur_raws); redundant_g = []; for c = 1:numel (rem_cands) g = rem_cands{c}; keep = true (rows (terms_cur), 1); keep (g.idx) = false; trial_terms = terms_cur (keep, :); trial_fit = LinearModel.lm_fit (build_design (trial_terms, info.X_enc), info.y, info.w, false); if (trial_fit.rank_X == cur_fit.rank_X) redundant_g = g; redundant_g.fit = trial_fit; redundant_g.hi = any (all (trial_terms(:,1:end-1) == 0, 2)); break; endif endfor if (! isempty (redundant_g)) keep = true (rows (terms_cur), 1); keep (redundant_g.idx) = false; prev_df = rows (terms_cur); terms_cur = terms_cur (keep, :); cur_fit = redundant_g.fit; cur_hi = redundant_g.hi; hist(end+1) = sw_hist_row ('Remove', redundant_g.name, terms_cur, ... rows (terms_cur), ... rows (terms_cur) - prev_df, Inf, NaN, info); step_no += 1; any_step = true; if (verbose >= 1) printf ("%d. Removing %s, FStat = Inf, pValue = NaN\n", step_no, redundant_g.name); endif continue; endif best_rem = []; best_rem_score = []; for c = 1:numel (rem_cands) g = rem_cands{c}; keep = true (rows (terms_cur), 1); keep (g.idx) = false; trial_terms = terms_cur (keep, :); trial_hi = any (all (trial_terms(:,1:end-1) == 0, 2)); trial_fit = LinearModel.lm_fit (build_design (trial_terms, info.X_enc), info.y, info.w, false); score = sw_criterion_score (criterion, info.n_obs, trial_hi, cur_hi, trial_fit, cur_fit); if (verbose >= 2) sw_print_candidate (criterion, 'remove', g.name, score); endif if (isempty (best_rem) || sw_prefer (criterion, 'remove', score, best_rem_score)) best_rem = g; best_rem.fit = trial_fit; best_rem.hi = trial_hi; best_rem_score = score; endif endfor if (isempty (rem_cands) && verbose >= 2) printf (" No candidate terms to remove\n"); endif if (! isempty (best_rem) && sw_threshold_ok (criterion, 'remove', best_rem_score, premove)) keep = true (rows (terms_cur), 1); keep (best_rem.idx) = false; prev_df = rows (terms_cur); terms_cur = terms_cur (keep, :); cur_fit = best_rem.fit; cur_hi = best_rem.hi; [hs, hp] = sw_hist_stat (criterion, 'remove', best_rem_score); hist(end+1) = sw_hist_row ('Remove', best_rem.name, terms_cur, ... rows (terms_cur), ... rows (terms_cur) - prev_df, hs, hp, info); step_no += 1; any_step = true; if (verbose >= 1) sw_print_action (criterion, step_no, 'Removing', best_rem.name, best_rem_score); endif continue; endif break; endwhile if (! any_step && verbose >= 1) printf ("No terms to add to or remove from initial model.\n"); endif int_mask = all (terms_cur(:, 1:end-1) == 0, 2); body = terms_cur(! int_mask, :); n_nonzero = sum (body(:, 1:end-1) != 0, 2); degree = sum (body(:, 1:end-1), 2); tier = zeros (rows (body), 1); tier(n_nonzero == 1 & degree == 1) = 1; tier(n_nonzero == 2) = 2; tier(n_nonzero == 1 & degree > 1) = 3; bitmask = zeros (rows (body), 1); for i = 1:rows (body) bitmask(i) = sum (2 .^ (find (body(i, 1:end-1)) - 1)); endfor [~, order] = sortrows ([tier, bitmask]); terms_cur = [terms_cur(int_mask, :); body(order, :)]; groups_final = sw_group_encoded_terms (terms_cur, info.pred_names, info.cat_info, info.enc_names); raws_final = sw_group_raws (groups_final); if (isempty (raws_final)) used_mask = false (1, numel (info.pred_names)); else used_mask = any (raws_final != 0, 1); endif used_pred_names = info.pred_names(used_mask); used_cols = any (terms_cur(:, 1:end-1) != 0, 1); terms_cur = terms_cur(:, [used_cols, true]); nv_list = {'Intercept', cur_hi, 'PredictorVars', used_pred_names}; if (! isempty (info.orig_opts.Weights)) nv_list = [nv_list, {'Weights', info.orig_opts.Weights}]; endif if (! isempty (info.orig_opts.Exclude)) nv_list = [nv_list, {'Exclude', info.orig_opts.Exclude}]; endif if (! isempty (info.cat_info.names)) nv_list = [nv_list, {'CategoricalVars', info.cat_info.names}]; endif mdl = fitlm (info.variables, info.response_name, terms_cur, nv_list{:}); mdl = setSteps (mdl, sw_steps_struct (hist, crit_label, penter, premove, ... T_start_enc, T_lower_enc, ... T_upper_enc, criterion, info)); endfunction ## ========================================================================== ## ## Stepwise history ## ========================================================================== ## ## One row of Steps.History. DF is the coefficient count after the step and ## TERMS the model's terms matrix over its variables, both as MATLAB reports ## them; a grouped categorical therefore counts once in TERMS and once per ## indicator in DF. function h = sw_hist_row (action, name, terms_enc, ncoef, ddf, stat, pval, info) h = struct ('Action', action, 'TermName', name, ... 'Terms', sw_history_terms (terms_enc, info), 'DF', ncoef, ... 'delDF', ddf, 'Stat', stat, 'PValue', pval); endfunction ## The statistic a step reports. Under 'sse' it is the F test that drove the ## move; under an information criterion it is the criterion's own value after ## the move, which is the model kept -- the trial model for an addition and ## the reduced model for a removal. function [stat, pval] = sw_hist_stat (criterion, mode, score) if (strcmp (criterion, 'sse')) stat = score.Fstat; pval = score.pvalue; elseif (strcmp (mode, 'add')) stat = score.abs_with; pval = NaN; else stat = score.abs_without; pval = NaN; endif endfunction ## The starting model has no test to report, but an information criterion has ## a value for it. function stat = sw_start_stat (criterion, fit, hi) if (strcmp (criterion, 'sse')) stat = NaN; return; endif c = LinearModel.lm_criteria (fit, hi); switch (criterion) case 'aic' stat = c.AIC; case 'bic' stat = c.BIC; case 'rsquared' stat = c.Rsquared; case 'adjrsquared' stat = c.AdjRsquared; endswitch endfunction ## Fold a terms matrix over the encoded columns onto one over the model's ## variables, in the order the model itself lists its terms. function T = sw_history_terms (terms_enc, info) groups = sw_group_encoded_terms (terms_enc, info.pred_names, ... info.cat_info, info.enc_names); raws = sw_group_raws (groups); T = sw_order_terms (sw_raws_to_variables (raws, info), ... sw_response_column (info)); endfunction ## Spread a matrix of per-predictor exponents over the model's variables, ## which include the response and any predictor the model never used. function T = sw_raws_to_variables (raws, info) var_names = info.variables.Properties.VariableNames; T = zeros (rows (raws), numel (var_names)); for j = 1:numel (info.pred_names) c = find (strcmp (var_names, info.pred_names{j}), 1); if (! isempty (c)) T(:, c) = raws(:, j); endif endfor endfunction function c = sw_response_column (info) c = find (strcmp (info.variables.Properties.VariableNames, ... info.response_name), 1); endfunction ## Order terms as the fitted model orders its coefficients: the intercept, ## then the linear terms, then the interactions, then the powers, each group ## by predictor index. function T = sw_order_terms (T, resp_col) cols = true (1, columns (T)); if (! isempty (resp_col)) cols(resp_col) = false; endif body_cols = T(:, cols); int_mask = all (body_cols == 0, 2); body = body_cols(! int_mask, :); n_nonzero = sum (body != 0, 2); degree = sum (body, 2); tier = zeros (rows (body), 1); tier(n_nonzero == 1 & degree == 1) = 1; tier(n_nonzero == 2) = 2; tier(n_nonzero == 1 & degree > 1) = 3; bitmask = zeros (rows (body), 1); for i = 1:rows (body) bitmask(i) = sum (2 .^ (find (body(i, :)) - 1)); endfor [~, order] = sortrows ([tier, bitmask]); rest = find (! int_mask); T = T([find(int_mask); rest(order)], :); endfunction ## Right-hand side of a model, used to name the Start row. function rhs = sw_terms_rhs (terms_enc, info) T = sw_history_terms (terms_enc, info); cols = true (1, columns (T)); rc = sw_response_column (info); if (! isempty (rc)) cols(rc) = false; endif names = info.variables.Properties.VariableNames(cols); body = T(:, cols); parts = {}; has_int = false; for i = 1:rows (body) if (all (body(i, :) == 0)) has_int = true; else parts{end+1} = sw_term_name (body(i, :), names); endif endfor if (has_int) parts = [{'1'}, parts]; elseif (isempty (parts)) parts = {'1'}; endif rhs = strjoin (parts, ' + '); endfunction ## Assemble the Steps structure attached to the returned model. function steps = sw_steps_struct (hist, crit_label, penter, premove, ... T_start, T_lower, T_upper, criterion, info) var_names = info.variables.Properties.VariableNames; resp_col = sw_response_column (info); steps = struct (); steps.Start = LinearFormula (sw_order_terms (sw_raws_to_variables ( ... sw_group_raws (sw_group_encoded_terms (T_start, ... info.pred_names, info.cat_info, info.enc_names)), info), ... resp_col), var_names, 'ResponseName', info.response_name); steps.Lower = LinearFormula (sw_order_terms (sw_raws_to_variables ( ... sw_group_raws (sw_group_encoded_terms (T_lower, ... info.pred_names, info.cat_info, info.enc_names)), info), ... resp_col), var_names, 'ResponseName', info.response_name); steps.Upper = LinearFormula (sw_order_terms (sw_raws_to_variables ( ... sw_group_raws (sw_group_encoded_terms (T_upper, ... info.pred_names, info.cat_info, info.enc_names)), info), ... resp_col), var_names, 'ResponseName', info.response_name); steps.Criterion = crit_label; steps.PEnter = penter; steps.PRemove = premove; steps.History = sw_history_table (hist, criterion); endfunction ## Build Steps.History. Which columns it carries is decided by the criterion: ## 'sse' reports the F test that drove each step, while an information ## criterion reports its own value after the step and has no test to report. function tbl = sw_history_table (hist, criterion) m = numel (hist); Action = cell (m, 1); TermName = cell (m, 1); Terms = cell (m, 1); DF = zeros (m, 1); delDF = zeros (m, 1); Stat = zeros (m, 1); pValue = zeros (m, 1); for i = 1:m Action{i} = hist(i).Action; TermName{i} = hist(i).TermName; Terms{i} = hist(i).Terms; DF(i) = hist(i).DF; delDF(i) = hist(i).delDF; Stat(i) = hist(i).Stat; pValue(i) = hist(i).PValue; endfor if (strcmp (criterion, 'sse')) tbl = table (Action, TermName, Terms, DF, delDF, Stat, pValue, ... 'VariableNames', {'Action', 'TermName', 'Terms', 'DF', ... 'delDF', 'FStat', 'pValue'}); else switch (criterion) case 'aic' cname = 'AIC'; case 'bic' cname = 'BIC'; case 'rsquared' cname = 'Rsquared'; case 'adjrsquared' cname = 'AdjRsquared'; endswitch tbl = table (Action, TermName, Terms, DF, delDF, Stat, ... 'VariableNames', {'Action', 'TermName', 'Terms', 'DF', ... 'delDF', cname}); endif endfunction function [positional, first_nv] = sw_split_positional (fit_args) nv_keys_fitlm = {'varnames','intercept','responsevar','predictorvars', ... 'categoricalvars','exclude','weights','robustopts'}; is_nv = @(s) (ischar (s) || isstring (s)) && any (strcmpi (char (s), nv_keys_fitlm)); n = numel (fit_args); first_nv = n + 1; for i = 1:n if (is_nv (fit_args{i})) first_nv = i; break; endif endfor positional = fit_args(1:first_nv-1); endfunction function fit_args = sw_ensure_modelspec (fit_args) [positional, first_nv] = sw_split_positional (fit_args); if (isempty (positional)) return; endif if (istable (positional{1})) tbl = positional{1}; col_names = tbl.Properties.VariableNames; if (numel (positional) >= 2) arg2 = positional{2}; if ((ischar (arg2) || isstring (arg2)) && ! any (char (arg2) == '~') ... && any (strcmp (char (arg2), col_names))) fit_args = [{tbl}, fit_args(3:first_nv-1), ... {'ResponseVar', char(arg2)}, fit_args(first_nv:end)]; [positional, first_nv] = sw_split_positional (fit_args); elseif (isnumeric (arg2) || islogical (arg2)) [nr2, nc2] = size (arg2); if (nc2 != width (tbl) && nc2 == 1 && nr2 == height (tbl)) ## arg2 is an external y vector, not a terms matrix n_cols = width (tbl); col_data = cell (1, n_cols); for k = 1:n_cols col_data{k} = tbl.(col_names{k}); endfor tbl_ext = table (col_data{:}, double (arg2(:)), ... 'VariableNames', [col_names, {'y'}]); fit_args = [{tbl_ext}, fit_args(3:first_nv-1), ... {'ResponseVar', 'y'}, fit_args(first_nv:end)]; [positional, first_nv] = sw_split_positional (fit_args); endif endif endif min_pos = 1; else min_pos = 2; endif if (numel (positional) <= min_pos) fit_args = [fit_args(1:first_nv-1), {'constant'}, fit_args(first_nv:end)]; endif endfunction function resp_name = sw_response_name (positional, nv_part) resp_name = ''; for i = 1:2:numel (nv_part)-1 if (strcmpi (char (nv_part{i}), 'ResponseVar')) resp_name = char (nv_part{i+1}); endif endfor if (! isempty (resp_name)) return; endif spec = positional{end}; if ((ischar (spec) || isstring (spec)) && any (char (spec) == '~')) tparts = strsplit (char (spec), '~'); resp_name = strtrim (tparts{1}); return; endif if (istable (positional{1})) resp_name = positional{1}.Properties.VariableNames{end}; endif endfunction function nv_out = sw_set_responsevar (nv_part, resp_name) nv_out = nv_part; if (isempty (resp_name)) return; endif found = false; for i = 1:2:numel (nv_out)-1 if (strcmpi (char (nv_out{i}), 'ResponseVar')) nv_out{i+1} = resp_name; found = true; endif endfor if (! found) nv_out = [nv_out, {'ResponseVar', resp_name}]; endif endfunction function T_raw = sw_reduce_to_raw_padded (mdl_bound, pred_names_full) groups_bound = sw_group_encoded_terms (mdl_bound.TermsMatrix, ... mdl_bound.PredictorNames, mdl_bound.CatLevelInfo, mdl_bound.EncPredictorNames); raws_bound = sw_group_raws (groups_bound); n_full = numel (pred_names_full); T_raw = zeros (rows (raws_bound), n_full + 1); for r = 1:rows (raws_bound) for k = 1:numel (mdl_bound.PredictorNames) e = raws_bound (r, k); if (e == 0) continue; endif j = find (strcmp (pred_names_full, mdl_bound.PredictorNames{k}), 1); T_raw (r, j) = e; endfor endfor endfunction function sw_predictorvars_matrix_conflict (fit_args) [positional, first_nv] = sw_split_positional (fit_args); n = numel (fit_args); pv_value = {}; rv_value = ''; for i = first_nv:2:n-1 if ((ischar (fit_args{i}) || isstring (fit_args{i})) ... && strcmpi (char (fit_args{i}), 'PredictorVars')) pv_value = fit_args{i+1}; endif if ((ischar (fit_args{i}) || isstring (fit_args{i})) ... && strcmpi (char (fit_args{i}), 'ResponseVar')) rv_value = char (fit_args{i+1}); endif endfor if (isempty (pv_value) || isempty (positional)) return; endif if (istable (positional{1})) min_pos = 1; else min_pos = 2; endif if (numel (positional) <= min_pos) return; endif last_extra = positional{end}; if (isnumeric (last_extra) || islogical (last_extra)) error ("stepwiselm: You may not specify PredictorVars with a terms matrix."); elseif ((ischar (last_extra) || isstring (last_extra)) ... && any (char (last_extra) == '~')) result = parseWilkinsonFormula (char (last_extra), 'expand'); conflict_msg = strcat ("stepwiselm: 'PredictorVars' or 'ResponseVar'", ... " values conflict with the formula character", ... " vector or string scalar."); formula_resp = ''; if (! isempty (result.response) && ! isempty (result.response{1})) formula_resp = result.response{1}{1}; endif if (! isempty (rv_value) && ! isempty (formula_resp) ... && ! strcmp (rv_value, formula_resp)) error (conflict_msg); endif vars_in_formula = {}; for t = 1:numel (result.model) vars_in_formula = [vars_in_formula, result.model{t}]; endfor ## A factor carries its exponent, as in 'x1^2', where 'PredictorVars' ## names variables, so compare the variable and not the power of it. ## Otherwise stepping any model holding a power term reads as a conflict. for k = 1:numel (vars_in_formula) hat = index (vars_in_formula{k}, '^'); if (hat > 0) vars_in_formula{k} = vars_in_formula{k}(1:hat-1); endif endfor vars_in_formula = unique (vars_in_formula); if (! all (ismember (vars_in_formula, pv_value))) error (conflict_msg); endif endif endfunction function T_raw = sw_resolve_bound_formula (raw_spec, info) nv_list = {}; if (! isempty (info.orig_opts.Weights)) nv_list = [nv_list, {'Weights', info.orig_opts.Weights}]; endif if (! isempty (info.orig_opts.Exclude)) nv_list = [nv_list, {'Exclude', info.orig_opts.Exclude}]; endif if (! isempty (info.cat_info.names)) nv_list = [nv_list, {'CategoricalVars', info.cat_info.names}]; endif mdl_bound = fitlm (info.variables, info.response_name, raw_spec, nv_list{:}); T_raw = sw_reduce_to_raw_padded (mdl_bound, info.pred_names); endfunction function T_raw = sw_resolve_bound (spec, pred_names, cat_mask, info) p = numel (pred_names); if (isempty (spec)) spec = 'constant'; endif if (isnumeric (spec) || islogical (spec)) T = double (spec); if (columns (T) == p) T = [T, zeros(rows (T), 1)]; elseif (columns (T) != p + 1) error ("stepwiselm: Lower/Upper terms matrix must have %d or %d columns.", p, p+1); endif T_raw = T; return; endif if (! (ischar (spec) || isstring (spec))) error ("stepwiselm: Lower/Upper must be a model name, terms matrix, or polynomial specification."); endif raw_spec = char (spec); if (any (raw_spec == '~')) T_raw = sw_resolve_bound_formula (raw_spec, info); return; endif s = lower (strtrim (raw_spec)); poly_tok = regexp (s, '^poly([0-9]+)$', 'tokens'); if (! isempty (poly_tok)) dstr = poly_tok{1}{1}; if (numel (dstr) != p) error (strcat ("stepwiselm: the number of digits in a 'polyijk'", ... " specification must match the number of predictors.")); endif digits = dstr - '0'; T_raw = sw_poly_terms (digits, p, cat_mask); return; endif switch (s) case 'constant' T_raw = zeros (1, p + 1); case 'linear' T_raw = [zeros(1,p+1); [eye(p), zeros(p,1)]]; case {'interactions', 'purequadratic', 'quadratic', 'full'} T_raw = sw_keyword_terms (s, p, cat_mask); otherwise error ("stepwiselm: '%s' is not a valid model specification.", s); endswitch endfunction function T = sw_keyword_terms (keyword, p, cat_mask) lin = [eye(p), zeros(p,1)]; T = [zeros(1,p+1); lin]; if (strcmp (keyword, 'linear')) return; endif if (any (strcmp (keyword, {'interactions','quadratic','full'}))) for i = 1:p for j = i+1:p row = zeros (1, p+1); row(i) = 1; row(j) = 1; T = [T; row]; endfor endfor endif if (any (strcmp (keyword, {'purequadratic','quadratic'}))) for j = 1:p if (! cat_mask(j)) row = zeros (1, p+1); row(j) = 2; T = [T; row]; endif endfor endif if (strcmp (keyword, 'full')) T = zeros (1, p+1); for k = 1:p idx_mat = nchoosek (1:p, k); for r = 1:rows (idx_mat) row = zeros (1, p+1); row(idx_mat(r,:)) = 1; T = [T; row]; endfor endfor endif endfunction function T = sw_poly_terms (digits, p, cat_mask) ranges = cell (1, p); for k = 1:p if (cat_mask(k)) ranges{k} = 0:min (digits(k), 1); else ranges{k} = 0:digits(k); endif endfor grids = cell (1, p); [grids{:}] = ndgrid (ranges{:}); n = numel (grids{1}); combos = zeros (n, p); for k = 1:p gk = grids{k}; combos(:,k) = gk(:); endfor keep = false (n, 1); for r = 1:n e = combos (r, :); nz = find (e != 0); if (isempty (nz)) continue; endif total_deg = sum (e(nz)); cap = max (digits(nz)); if (total_deg <= cap) keep(r) = true; endif endfor T = [zeros(1,p); combos(keep, :)]; T = [T, zeros(rows (T), 1)]; endfunction function T_enc = sw_raw_terms_to_encoded (T_raw, pred_names, cat_info, enc_names) p_enc = numel (enc_names); T_enc = zeros (0, p_enc + 1); for r = 1:rows (T_raw) G = sw_expand_conceptual_term (T_raw(r, 1:end-1), pred_names, cat_info, enc_names); G = [G, zeros(rows (G), 1)]; T_enc = [T_enc; G]; endfor endfunction function Genc = sw_expand_conceptual_term (term_row, pred_names, cat_info, enc_names) p_enc = numel (enc_names); Genc = zeros (1, p_enc); for j = find (term_row != 0) e = term_row(j); ci = []; if (! isempty (cat_info.names)) ci = find (strcmp (cat_info.names, pred_names{j})); endif if (isempty (ci)) col = find (strcmp (enc_names, pred_names{j}), 1); Genc (:, col) = e; else levels_j = cat_info.levels{ci}; cols_j = []; for L = 2:numel (levels_j) nm = sprintf ("%s_%s", pred_names{j}, char (levels_j{L})); col = find (strcmp (enc_names, nm), 1); cols_j(end+1) = col; endfor new_rows = zeros (numel (cols_j) * rows (Genc), p_enc); r_out = 0; for existing = 1:rows (Genc) for c = 1:numel (cols_j) r_out += 1; row = Genc(existing, :); row (cols_j(c)) = 1; new_rows (r_out, :) = row; endfor endfor Genc = new_rows; endif endfor endfunction function groups = sw_group_encoded_terms (terms_enc, pred_names, cat_info, enc_names) n_pred = numel (pred_names); n_rows = rows (terms_enc); raw_of = zeros (n_rows, n_pred); for r = 1:n_rows row_raw = zeros (1, n_pred); for j = 1:n_pred ci = []; if (! isempty (cat_info.names)) ci = find (strcmp (cat_info.names, pred_names{j})); endif if (isempty (ci)) col = find (strcmp (enc_names, pred_names{j}), 1); if (! isempty (col)) row_raw(j) = terms_enc (r, col); endif ## A design built from a formula names a power with its own column, ## 'x1^2' beside 'x1', rather than carrying the exponent in the 'x1' ## column. Read it off the name: otherwise the term folds onto the ## all-zero row and is silently lost as a duplicate intercept. for c = 1:numel (enc_names) nm = enc_names{c}; hat = index (nm, '^'); if (hat > 1 && strcmp (nm(1:hat-1), pred_names{j}) ... && terms_enc (r, c) != 0) e = str2double (nm(hat+1:end)); if (! isnan (e)) row_raw(j) = max (row_raw(j), terms_enc (r, c) * e); endif endif endfor else levels_j = cat_info.levels{ci}; present = false; for L = 2:numel (levels_j) nm = sprintf ("%s_%s", pred_names{j}, char (levels_j{L})); col = find (strcmp (enc_names, nm), 1); if (! isempty (col) && terms_enc (r, col) != 0) present = true; endif endfor if (present) row_raw(j) = 1; endif endif endfor raw_of (r, :) = row_raw; endfor groups = {}; used = false (n_rows, 1); for r = 1:n_rows if (used(r)) continue; endif match = false (n_rows, 1); for r2 = r:n_rows if (! used(r2) && isequal (raw_of(r2,:), raw_of(r,:))) match(r2) = true; endif endfor used = used | match; g.raw = raw_of(r, :); g.idx = find (match)'; g.term_rows = terms_enc (g.idx, :); g.name = sw_term_name (g.raw, pred_names); groups{end+1} = g; endfor endfunction function nm = sw_term_name (raw_row, pred_names) if (all (raw_row == 0)) nm = '(Intercept)'; return; endif parts = {}; for j = find (raw_row != 0) if (raw_row(j) == 1) parts{end+1} = pred_names{j}; else parts{end+1} = sprintf ("%s^%d", pred_names{j}, raw_row(j)); endif endfor nm = strjoin (parts, ':'); endfunction function raws = sw_group_raws (groups) if (isempty (groups)) raws = []; return; endif raws = zeros (numel (groups), numel (groups{1}.raw)); for k = 1:numel (groups) raws (k, :) = groups{k}.raw; endfor endfunction function tf = sw_row_present (row, mat) tf = false; for k = 1:rows (mat) if (isequal (mat(k,:), row)) tf = true; return; endif endfor endfunction function ok = sw_hierarchy_ok_add (term_raw, cur_raws) nz = find (term_raw != 0); if (numel (nz) == 0) ok = true; return; endif if (numel (nz) == 1) j = nz(1); e = term_raw(j); if (e <= 1) ok = true; return; endif pred_row = zeros (size (term_raw)); pred_row(j) = e - 1; ok = sw_row_present (pred_row, cur_raws); return; endif ok = true; for k = 1:numel (nz) j = nz(k); reduced = term_raw; reduced(j) = 0; if (! sw_row_present (reduced, cur_raws)) ok = false; return; endif endfor endfunction function ok = sw_hierarchy_ok_remove (term_raw, cur_raws) ok = true; for k = 1:rows (cur_raws) s = cur_raws(k, :); if (isequal (s, term_raw)) continue; endif if (sw_is_predecessor (term_raw, s)) ok = false; return; endif endfor endfunction function tf = sw_is_predecessor (pred_row, term_raw) nz = find (term_raw != 0); tf = false; if (numel (nz) == 0) return; endif if (numel (nz) == 1) j = nz(1); e = term_raw(j); if (e <= 1) return; endif want = zeros (size (term_raw)); want(j) = e - 1; tf = isequal (want, pred_row); return; endif for k = 1:numel (nz) j = nz(k); reduced = term_raw; reduced(j) = 0; if (isequal (reduced, pred_row)) tf = true; return; endif endfor endfunction function cand = sw_add_candidates (groups_upper, cur_raws) cand = {}; for u = 1:numel (groups_upper) ug = groups_upper{u}; if (sw_row_present (ug.raw, cur_raws)) continue; endif if (sw_hierarchy_ok_add (ug.raw, cur_raws)) cand{end+1} = ug; endif endfor endfunction function cand = sw_remove_candidates (groups_cur, lower_raws, cur_raws) cand = {}; for c = 1:numel (groups_cur) cg = groups_cur{c}; if (sw_row_present (cg.raw, lower_raws)) continue; endif if (sw_hierarchy_ok_remove (cg.raw, cur_raws)) cand{end+1} = cg; endif endfor endfunction function [Fstat, pval, df1] = sw_ftest (fit_small, fit_big) df1 = fit_big.rank_X - fit_small.rank_X; if (df1 <= 0 || fit_big.DFE <= 0) Fstat = NaN; pval = NaN; return; endif Fstat = ((fit_small.SSE - fit_big.SSE) / df1) / max (fit_big.MSE, eps); if (Fstat < 0 || isnan (Fstat)) pval = NaN; else pval = betainc (fit_big.DFE / (fit_big.DFE + df1 * Fstat), fit_big.DFE / 2, df1 / 2); endif endfunction function score = sw_criterion_score (criterion, n_obs, hi_without, hi_with, fit_without, fit_with) score = struct (); if (strcmp (criterion, 'sse')) [Fs, p, ~] = sw_ftest (fit_without, fit_with); score.pvalue = p; score.Fstat = Fs; return; endif crit_without = LinearModel.lm_criteria (fit_without, hi_without); crit_with = LinearModel.lm_criteria (fit_with, hi_with); switch (criterion) case 'aic' score.benefit = crit_with.AIC - crit_without.AIC; score.abs_with = crit_with.AIC; score.abs_without = crit_without.AIC; case 'bic' score.benefit = crit_with.BIC - crit_without.BIC; score.abs_with = crit_with.BIC; score.abs_without = crit_without.BIC; case 'rsquared' score.benefit = crit_with.Rsquared - crit_without.Rsquared; score.abs_with = crit_with.Rsquared; score.abs_without = crit_without.Rsquared; case 'adjrsquared' score.benefit = crit_with.AdjRsquared - crit_without.AdjRsquared; score.abs_with = crit_with.AdjRsquared; score.abs_without = crit_without.AdjRsquared; endswitch endfunction function tf = sw_threshold_ok (criterion, mode, score, thresh) if (strcmp (criterion, 'sse')) if (strcmp (mode, 'add')) tf = ! isnan (score.pvalue) && score.pvalue < thresh; else tf = ! isnan (score.pvalue) && score.pvalue > thresh; endif return; endif if (isnan (score.benefit)) tf = false; return; endif is_aic_bic = any (strcmp (criterion, {'aic','bic'})); if (strcmp (mode, 'add')) if (is_aic_bic) tf = score.benefit < thresh; else tf = score.benefit > thresh; endif else if (is_aic_bic) tf = score.benefit > thresh; else tf = score.benefit < thresh; endif endif endfunction function tf = sw_prefer (criterion, mode, score_a, score_b) if (strcmp (criterion, 'sse')) if (strcmp (mode, 'add')) tf = score_a.pvalue < score_b.pvalue; else tf = score_a.pvalue > score_b.pvalue; endif return; endif is_aic_bic = any (strcmp (criterion, {'aic','bic'})); if (strcmp (mode, 'add')) if (is_aic_bic) tf = score_a.benefit < score_b.benefit; else tf = score_a.benefit > score_b.benefit; endif else if (is_aic_bic) tf = score_a.benefit > score_b.benefit; else tf = score_a.benefit < score_b.benefit; endif endif endfunction function sw_print_candidate (criterion, mode, name, score) if (strcmp (mode, 'add')) verb = 'adding'; else verb = 'removing'; endif if (strcmp (criterion, 'sse')) printf (" pValue for %s %s is %.6g\n", verb, name, score.pvalue); return; endif labels = struct ('aic','AIC','bic','BIC','rsquared','Rsquared', 'adjrsquared','AdjRsquared'); lbl = labels.(criterion); if (strcmp (mode, 'add')) val = score.benefit; else val = -score.benefit; endif printf (" Change in %s for %s %s is %.6g\n", lbl, verb, name, val); endfunction function sw_print_action (criterion, step_no, verb, name, score) if (strcmp (criterion, 'sse')) printf ("%d. %s %s, FStat = %.6g, pValue = %.7g\n", step_no, verb, name, score.Fstat, score.pvalue); return; endif labels = struct ('aic','AIC','bic','BIC','rsquared','Rsquared', 'adjrsquared','AdjRsquared'); lbl = labels.(criterion); if (strcmp (verb, 'Adding')) val = score.abs_with; else val = score.abs_without; endif printf ("%d. %s %s, %s = %.6g\n", step_no, verb, name, lbl, val); endfunction %!demo %! %! ## Stepwise search from a constant model, with a custom entry threshold. %! ## Twenty apartments are described by their size, floor number, and %! ## distance from the city center, along with their monthly rent. %! ## Starting from an empty model, `stepwiselm` adds one predictor at a %! ## time as long as it improves the fit by at least `PEnter`, then checks %! ## whether anything already in the model should come back out. %! Size = [45 50 38 62 70 55 48 40 65 58 42 72 35 68 52 60 46 66 39 54]'; %! Floor = [3 5 2 8 10 4 1 6 9 7 3 12 2 11 5 6 4 10 1 5]'; %! Distance = [12 8 15 5 3 9 18 10 4 7 14 2 20 3 8 6 11 4 17 9]'; %! Rent = 200 + 12*Size - 15*Distance + 2*Floor + 5*sin ((1:20)'/2); %! X = [Size, Floor, Distance]; %! %! ## Fit with a looser entry threshold, printing every candidate examined. %! mdl = stepwiselm (X, Rent, 'PEnter', 0.06, 'Verbose', 2) %!demo %! %! ## Starting from a formula, with the response, predictors, and %! ## categorical variable all named explicitly. %! ## Eighteen coffee shops report their weekly ad spend, staff count, and %! ## sales season, along with weekly sales. The search starts already %! ## containing `AdSpend`, with `Employees` and the categorical `Season` %! ## left to consider adding. %! AdSpend = [200 350 500 220 370 520 240 390 540 260 410 560 280 430 580 300 450 600]'; %! Employees = [4 5 6 4 5 6 4 5 6 4 5 6 4 5 6 4 5 6]'; %! Season = {'Low';'Mid';'Peak';'Low';'Mid';'Peak';'Low';'Mid';'Peak'; ... %! 'Low';'Mid';'Peak';'Low';'Mid';'Peak';'Low';'Mid';'Peak'}; %! SeasonEffect = [0;150;400;0;150;400;0;150;400;0;150;400;0;150;400;0;150;400]; %! Sales = 1000 + 0.8*AdSpend + 5*Employees + SeasonEffect + 6*sin ((1:18)'); %! T = table (AdSpend, Employees, Season, Sales, ... %! 'VariableNames', {'AdSpend','Employees','Season','Sales'}); %! %! ## Fit, treating Season as categorical and naming the response explicitly. %! mdl = stepwiselm (T, 'Sales ~ 1 + AdSpend', 'ResponseVar', 'Sales', ... %! 'PredictorVars', {'AdSpend','Employees','Season'}, ... %! 'CategoricalVars', {'Season'}, 'Verbose', 1) %!demo %! %! ## Bounding the search with terms matrices instead of model-name %! ## keywords. %! ## Twelve potted plants are given varying hours of sunlight and amounts %! ## of water, and their growth is measured. `T_initial` is a constant %! ## model and `T_upper` allows the two main effects plus their %! ## interaction, using the same terms-matrix convention as `fitlm`. %! Sunlight = [2 4 6 8 3 5 7 9 2.5 4.5 6.5 8.5]'; %! Water = [100 150 200 250 120 170 220 270 110 160 210 260]'; %! Growth = 5 + 0.3*Sunlight + 0.06*Water + 0.4*sin ((1:12)'); %! X = [Sunlight, Water]; %! T_initial = [0 0 0]; %! T_upper = [0 0 0; 1 0 0; 0 1 0; 1 1 0]; %! %! ## Fit, printing the p-value considered for every candidate term. %! mdl = stepwiselm (X, Growth, T_initial, 'Upper', T_upper, 'Verbose', 2) %!demo %! %! ## A categorical predictor is added or removed as one indicator group, %! ## never one indicator column at a time. %! ## Eighteen plots receive varying fertilizer amounts across three %! ## regions, and crop yield is recorded. The upper bound `poly21` allows %! ## a constant, `Fertilizer`, `Fertilizer^2`, `Region`, and their %! ## interaction; `stepwiselm` treats the two indicator columns generated %! ## by the three-level `Region` as a single term throughout. %! Fertilizer = repmat ([10;20;30;40;50;60], 3, 1); %! Region = [repmat({'A'},6,1); repmat({'B'},6,1); repmat({'C'},6,1)]; %! RegionEffect = [zeros(6,1); 15*ones(6,1); 35*ones(6,1)]; %! Yield = 20 + 0.5*Fertilizer + RegionEffect + 1.5*sin ((1:18)'); %! T = table (Fertilizer, Region, Yield); %! %! ## Fit, printing every candidate considered at each step. %! mdl = stepwiselm (T, 'Yield ~ Fertilizer', 'Upper', 'poly21', 'Verbose', 2) %!demo %! %! ## Capping the search with `NSteps` before it converges on its own. %! ## Twenty observations depend on three predictors, all with genuine %! ## effects. Left alone, `stepwiselm` would add all three; limiting %! ## `NSteps` to 1 stops the search after only the single best addition. %! x1 = (1:20)'; %! x2 = mod ((0:19)', 5); %! x3 = mod ((0:19)', 3); %! y = 5 + 2*x1 + 1.5*x2 + 3*x3 + 2*sin ((1:20)'/1.7); %! X = [x1, x2, x3]; %! %! ## Fit, but stop after the very first step. %! mdl = stepwiselm (X, y, 'Upper', 'linear', 'NSteps', 1, 'Verbose', 1) %!demo %! %! ## Raw matrix input with default `x1, x2, ...` naming, and custom entry %! ## and removal thresholds. %! ## Fifteen observations depend mainly on `x1`. `PEnter` and `PRemove` %! ## are set explicitly rather than relying on the defaults for the SSE %! ## criterion. %! x1 = (1:15)'; %! x2 = mod ((0:14)', 4); %! y = 8 + 0.5*x1 + 0.3*x2 + 2.5*sin ((1:15)'/1.2); %! X = [x1, x2]; %! %! ## Fit with a wider entry threshold and a stricter removal threshold. %! mdl = stepwiselm (X, y, 'PEnter', 0.2, 'PRemove', 0.3, 'Verbose', 1) %!demo %! %! ## The `hald` cement data, mirroring MATLAB's own reference example for %! ## `stepwiselm`. %! ## Four chemical percentages in cement (`ingredients`) are used to %! ## predict heat given off while hardening (`heat`). Starting from a %! ## constant model, `stepwiselm` adds three terms and then removes one %! ## of them again once it becomes redundant. %! load hald %! %! ## Fit with a looser entry threshold than the default. %! mdl = stepwiselm (ingredients, heat, 'PEnter', 0.06, 'Verbose', 1) %!demo %! %! ## The `carsmall` data, mirroring MATLAB's own reference example for %! ## `stepwiselm` with a terms-matrix bound. %! ## Fuel economy (`MPG`) is predicted from acceleration and weight. %! ## `T_initial` is a constant model and `T_upper` allows both main %! ## effects plus their interaction. %! load carsmall %! X = [Acceleration, Weight]; %! T_initial = [0 0 0]; %! T_upper = [0 0 0; 1 0 0; 0 1 0; 1 1 0]; %! %! ## Fit, printing the p-value considered for every candidate term. %! mdl = stepwiselm (X, MPG, T_initial, 'Upper', T_upper, 'Verbose', 2) %!shared x1, x2, x3, y, X, tbl %! n = 24; %! x1 = (1:n)'/n; x2 = sin((1:n)'/4); x3 = cos((1:n)'/5); %! y = 4 + 2*x1 - x2 + 0.5*x3 + 0.15*sin((1:n)'*0.8); %! X = [x1, x2, x3]; %! tbl = table (x1, x2, x3, y, 'VariableNames', {'x1','x2','x3','y'}); %!test %! mdl = stepwiselm (tbl, 'Verbose', 0); %! assert_equal (mdl.NumObservations, 24); %! assert_equal (mdl.NumCoefficients, 4); %! assert_equal (mdl.NumVariables, 4); %! assert_equal (mdl.NumPredictors, 3); %! assert_equal (mdl.DFE, 20); %! assert_equal (mdl.SSE, 0.237882441508, 1e-9); %! assert_equal (mdl.SSR, 25.3955253054, 1e-7); %! assert_equal (mdl.SST, 25.6334077469, 1e-7); %! assert_equal (mdl.MSE, 0.0118941220754, 1e-10); %! assert_equal (mdl.RMSE, 0.109060176395, 1e-9); %! assert_equal (mdl.Rsquared.Ordinary, 0.990719827662, 1e-9); %! assert_equal (mdl.Rsquared.Adjusted, 0.989327801811, 1e-9); %! assert_equal (mdl.LogLikelihood, 21.3138652143, 1e-6); %! assert_equal (mdl.ModelCriterion.AIC, -34.6277304285, 1e-6); %! assert_equal (mdl.ModelCriterion.AICc, -32.5224672707, 1e-6); %! assert_equal (mdl.ModelCriterion.BIC, -29.9155151072, 1e-6); %! assert_equal (mdl.ModelCriterion.CAIC, -25.9155151072, 1e-6); %! assert_equal (mdl.ModelFitVsNullModel.Fstat, 711.710796991, 1e-5); %! assert_equal (mdl.ModelFitVsNullModel.NullModel, 'constant'); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x3'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [4.11199897343; 1.79245385284; -1.06961941012; 0.51029628797], 1e-9); %! assert_equal (mdl.Coefficients.SE, ... %! [0.0761375845531; 0.144191944451; 0.0575954799856; 0.0489829266327], 1e-9); %! assert_equal (mdl.Coefficients.tStat, ... %! [54.007478666; 12.4310262939; -18.5712387566; 10.4178399097], 1e-6); %! assert_equal (mdl.Coefficients.pValue, ... %! [3.79220676881e-23; 7.26932473407e-11; 4.42354835071e-14; 1.58256682968e-09], 1e-9); %! assert_equal (mdl.ResponseName, 'y'); %! assert_equal (mdl.PredictorNames, {'x1';'x2';'x3'}); %! assert_equal (mdl.VariableNames, {'x1';'x2';'x3';'y'}); %! assert_equal (mdl.Formula.HasIntercept, true); %! assert_equal (mdl.Formula.LinearPredictor, '1 + x1 + x2 + x3'); %! assert_equal (mdl.Formula.NTerms, 4); %!test %! mdl = stepwiselm (tbl, 'y', 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x3'}); %! assert_equal (mdl.SSE, 0.237882441508, 1e-9); %! assert_equal (mdl.ResponseName, 'y'); %! assert_equal (mdl.PredictorNames, {'x1';'x2';'x3'}); %!test %! tt = table (x1, x2, x3, 'VariableNames', {'x1','x2','x3'}); %! mdl = stepwiselm (tt, y, 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x3'}); %! assert_equal (mdl.SSE, 0.237882441508, 1e-9); %! assert_equal (mdl.ResponseName, 'y'); %! assert_equal (mdl.VariableNames, {'x1';'x2';'x3';'y'}); %!test %! mdl = stepwiselm (X, y, 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x3'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [4.11199897343; 1.79245385284; -1.06961941012; 0.51029628797], 1e-9); %! assert_equal (mdl.ResponseName, 'y'); %! assert_equal (mdl.VariableNames, {'x1';'x2';'x3';'y'}); %!test %! mdl = stepwiselm (tbl, 'y ~ x1', 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x3'}); %! assert_equal (mdl.SSE, 0.237882441508, 1e-9); %! assert_equal (mdl.Formula.LinearPredictor, '1 + x1 + x2 + x3'); %!test %! mdl = stepwiselm (X, y, 'constant', 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x3'}); %! assert_equal (mdl.SSE, 0.237882441508, 1e-9); %!test %! mdl = stepwiselm (X, y, 'linear', 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x3'}); %! assert_equal (mdl.SSE, 0.237882441508, 1e-9); %!test %! mdl = stepwiselm (X, y, 'constant', 'Upper', 'purequadratic', 'Verbose', 0); %! assert_equal (mdl.NumObservations, 24); %! assert_equal (mdl.NumCoefficients, 4); %! assert_equal (mdl.NumVariables, 4); %! assert_equal (mdl.NumPredictors, 2); %! assert_equal (mdl.DFE, 20); %! assert_equal (mdl.SSE, 0.22345826866, 1e-9); %! assert_equal (mdl.SSR, 25.4099494782, 1e-7); %! assert_equal (mdl.SST, 25.6334077469, 1e-7); %! assert_equal (mdl.MSE, 0.011172913433, 1e-10); %! assert_equal (mdl.RMSE, 0.105702002975, 1e-9); %! assert_equal (mdl.Rsquared.Ordinary, 0.991282537583, 1e-9); %! assert_equal (mdl.Rsquared.Adjusted, 0.98997491822, 1e-9); %! assert_equal (mdl.LogLikelihood, 22.0644883645, 1e-6); %! assert_equal (mdl.ModelCriterion.AIC, -36.1289767289, 1e-6); %! assert_equal (mdl.ModelCriterion.AICc, -34.023713571, 1e-6); %! assert_equal (mdl.ModelCriterion.BIC, -31.4167614075, 1e-6); %! assert_equal (mdl.ModelCriterion.CAIC, -27.4167614075, 1e-6); %! assert_equal (mdl.ModelFitVsNullModel.Fstat, 758.081874544, 1e-5); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x1^2'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [4.79002491626; -1.87038904608; -0.881529179216; 3.14370698956], 1e-9); %! assert_equal (mdl.Coefficients.SE, ... %! [0.0903999262734; 0.328540613357; 0.0534940080998; 0.290849609965], 1e-9); %! assert_equal (mdl.Coefficients.tStat, ... %! [52.9870445002; -5.6930223237; -16.4790265402; 10.8087027861], 1e-6); %! assert_equal (mdl.Coefficients.pValue, ... %! [5.53960001242e-23; 1.4288675776e-05; 4.19790256549e-13; 8.42439951369e-10], 1e-9); %! assert_equal (mdl.PredictorNames, {'x1';'x2'}); %! assert_equal (mdl.Formula.LinearPredictor, '1 + x1 + x2 + x1^2'); %!test %! mdl = stepwiselm (X, y, 'constant', 'Upper', 'quadratic', 'Verbose', 0); %! assert_equal (mdl.NumPredictors, 2); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x1^2'}); %! assert_equal (mdl.SSE, 0.22345826866, 1e-9); %! assert_equal (mdl.PredictorNames, {'x1';'x2'}); %!test %! T0 = [0 0 0]; %! T1 = [0 0 0; 1 0 0; 0 1 0; 1 1 0]; %! mdl = stepwiselm (X(:,1:2), y, T0, 'Upper', T1, 'Verbose', 0); %! assert_equal (mdl.NumObservations, 24); %! assert_equal (mdl.NumCoefficients, 4); %! assert_equal (mdl.NumVariables, 3); %! assert_equal (mdl.NumPredictors, 2); %! assert_equal (mdl.DFE, 20); %! assert_equal (mdl.SSE, 1.11546087527, 1e-8); %! assert_equal (mdl.SSR, 24.5179468716, 1e-7); %! assert_equal (mdl.SST, 25.6334077469, 1e-7); %! assert_equal (mdl.MSE, 0.0557730437634, 1e-10); %! assert_equal (mdl.RMSE, 0.236163171903, 1e-9); %! assert_equal (mdl.Rsquared.Ordinary, 0.956484097383, 1e-9); %! assert_equal (mdl.Rsquared.Adjusted, 0.94995671199, 1e-9); %! assert_equal (mdl.LogLikelihood, 2.77090924057, 1e-6); %! assert_equal (mdl.ModelCriterion.AIC, 2.45818151886, 1e-6); %! assert_equal (mdl.ModelCriterion.AICc, 4.56344467675, 1e-6); %! assert_equal (mdl.ModelCriterion.BIC, 7.17039684025, 1e-6); %! assert_equal (mdl.ModelCriterion.CAIC, 11.1703968402, 1e-6); %! assert_equal (mdl.ModelFitVsNullModel.Fstat, 146.534031599, 1e-5); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x1:x2'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [4.00100246651; 1.35895695997; -0.23206474908; -1.2780495686], 1e-9); %! assert_equal (mdl.Coefficients.SE, ... %! [0.181530230599; 0.299744817513; 0.272053305331; 0.469486828642], 1e-9); %! assert_equal (mdl.Coefficients.tStat, ... %! [22.0404196772; 4.53371294703; -0.853012055108; -2.72222667524], 1e-6); %! assert_equal (mdl.Coefficients.pValue, ... %! [1.67738549624e-15; 0.000202251230338; 0.403753045528; 0.0131231039551], 1e-9); %! assert_equal (mdl.PredictorNames, {'x1';'x2'}); %! assert_equal (mdl.VariableNames, {'x1';'x2';'y'}); %!test %! mdl = stepwiselm (X, y, 'Criterion', 'aic', 'Upper', 'interactions', 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x3'}); %! assert_equal (mdl.ModelCriterion.AIC, -34.6277304285, 1e-6); %!test %! mdl = stepwiselm (X, y, 'Criterion', 'bic', 'Upper', 'interactions', 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x3'}); %! assert_equal (mdl.ModelCriterion.BIC, -29.9155151072, 1e-6); %!test %! mdl = stepwiselm (X, y, 'Criterion', 'rsquared', 'Upper', 'interactions', 'Verbose', 0); %! assert_equal (mdl.NumCoefficients, 2); %! assert_equal (mdl.NumPredictors, 1); %! assert_equal (mdl.DFE, 22); %! assert_equal (mdl.SSE, 2.69619594281, 1e-8); %! assert_equal (mdl.Rsquared.Ordinary, 0.894817108617, 1e-9); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x2'}); %! assert_equal (mdl.Coefficients.Estimate, [4.93053729606; -1.35113880263], 1e-9); %! assert_equal (mdl.Coefficients.SE, [0.0714593428648; 0.0987629466731], 1e-9); %! assert_equal (mdl.Coefficients.tStat, [68.9977978861; -13.6806246486], 1e-6); %! assert_equal (mdl.Coefficients.pValue, [3.2882259984e-27; 3.08475274476e-12], 1e-9); %! assert_equal (mdl.PredictorNames, {'x2'}); %!test %! mdl = stepwiselm (X, y, 'Criterion', 'adjrsquared', 'Upper', 'interactions', 'Verbose', 0); %! assert_equal (mdl.NumCoefficients, 5); %! assert_equal (mdl.DFE, 19); %! assert_equal (mdl.SSE, 0.224234467489, 1e-9); %! assert_equal (mdl.Rsquared.Adjusted, 0.989410626691, 1e-9); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x3','x1:x3'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [4.58266504435; 1.22125846816; -1.38808827902; 0.143991325796; 1.04450806346], 1e-8); %! assert_equal (mdl.Coefficients.SE, ... %! [0.444198853188; 0.55023668447; 0.301652972275; 0.344106973415; 0.971297090441], 1e-8); %! assert_equal (mdl.Coefficients.tStat, ... %! [10.3166971537; 2.21951480632; -4.6016065035; 0.418449310593; 1.07537443872], 1e-6); %! assert_equal (mdl.Coefficients.pValue, ... %! [3.1776147586e-09; 0.0388206335538; 0.000194754077847; 0.680310511095; 0.295673895373], 1e-8); %! assert_equal (mdl.PredictorNames, {'x1';'x2';'x3'}); %!test %! p1 = (1:24)'/24; p2 = sin ((1:24)'/3.5); %! pr = 3 + 0.9*p1 - 0.2*p2 + 0.1*sin ((1:24)'*1.1); %! mdl = stepwiselm (p1, pr, 'Verbose', 0); %! assert_equal (mdl.NumObservations, 24); %! assert_equal (mdl.NumCoefficients, 2); %! assert_equal (mdl.DFE, 22); %! assert_equal (mdl.SSE, 0.422994127869, 1e-9); %! assert_equal (mdl.SSR, 2.71741844039, 1e-8); %! assert_equal (mdl.SST, 3.14041256826, 1e-8); %! assert_equal (mdl.Rsquared.Ordinary, 0.865306191886, 1e-9); %! assert_equal (mdl.Rsquared.Adjusted, 0.859183746062, 1e-9); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1'}); %! assert_equal (mdl.Coefficients.Estimate, [2.85858604186; 1.16664998725], 1e-9); %! assert_equal (mdl.Coefficients.SE, [0.0584250816201; 0.0981336947312], 1e-9); %! assert_equal (mdl.Coefficients.tStat, [48.9273778074; 11.8883732081], 1e-6); %! assert_equal (mdl.Coefficients.pValue, [6.03142199019e-24; 4.75617587395e-11], 1e-9); %!test %! p1 = (1:24)'/24; p2 = sin ((1:24)'/3.5); %! pr = 3 + 0.9*p1 - 0.2*p2 + 0.1*sin ((1:24)'*1.1); %! mdl = stepwiselm (p1, pr, 'PEnter', 0.5, 'PRemove', 0.6, 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1'}); %! assert_equal (mdl.SSE, 0.422994127869, 1e-9); %!test %! mdl = stepwiselm (X, y, 'Upper', 'interactions', 'NSteps', 1, 'Verbose', 0); %! assert_equal (mdl.NumCoefficients, 2); %! assert_equal (mdl.DFE, 22); %! assert_equal (mdl.SSE, 2.69619594281, 1e-8); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x2'}); %! assert_equal (mdl.Coefficients.Estimate, [4.93053729606; -1.35113880263], 1e-9); %! assert_equal (mdl.PredictorNames, {'x2'}); %!test %! mdl = stepwiselm (X, y, 'NSteps', 0, 'Verbose', 0); %! assert_equal (mdl.NumCoefficients, 1); %! assert_equal (mdl.NumPredictors, 0); %! assert_equal (mdl.DFE, 23); %! assert_equal (mdl.SSE, 25.6334077469, 1e-7); %! assert_equal (mdl.SSR, 0, 1e-12); %! assert_equal (mdl.Rsquared.Ordinary, 0, 1e-12); %! assert_equal (isnan (mdl.ModelFitVsNullModel.Fstat), true); %! assert_equal (isnan (mdl.ModelFitVsNullModel.NullModel), true); %! assert_equal (mdl.CoefficientNames, {'(Intercept)'}); %! assert_equal (mdl.Coefficients.Estimate, 4.92948000444, 1e-9); %! assert_equal (mdl.Coefficients.SE, 0.215493231622, 1e-9); %! assert_equal (mdl.Coefficients.tStat, 22.8753356537, 1e-6); %! assert_equal (mdl.Coefficients.pValue, 2.53817914154e-17, 1e-9); %! assert_equal (isempty (mdl.PredictorNames), true); %! assert_equal (mdl.Formula.LinearPredictor, '1'); %!test %! w = 1400 + (1:45)'*9; %! g = {'A','B','C'}(mod ((0:44), 3) + 1)'; %! m = 55 - 0.005*w + [0 5 -3](mod ((0:44), 3) + 1)' + 0.3*sin ((1:45)'/6); %! t = table (w, g, m, 'VariableNames', {'Weight','Group','MPG'}); %! mdl = stepwiselm (t, 'MPG ~ Weight', 'CategoricalVars', {'Group'}, 'Upper', 'interactions', 'Verbose', 0); %! assert_equal (mdl.NumObservations, 45); %! assert_equal (mdl.NumCoefficients, 4); %! assert_equal (mdl.NumVariables, 3); %! assert_equal (mdl.NumPredictors, 2); %! assert_equal (mdl.DFE, 41); %! assert_equal (mdl.SSE, 1.7056787721, 1e-8); %! assert_equal (mdl.SSR, 512.932602391, 1e-6); %! assert_equal (mdl.SST, 514.638281163, 1e-6); %! assert_equal (mdl.MSE, 0.0416019212713, 1e-10); %! assert_equal (mdl.RMSE, 0.203965490393, 1e-9); %! assert_equal (mdl.Rsquared.Ordinary, 0.996685674513, 1e-9); %! assert_equal (mdl.Rsquared.Adjusted, 0.996443162891, 1e-9); %! assert_equal (mdl.LogLikelihood, 9.78350141322, 1e-6); %! assert_equal (mdl.ModelCriterion.AIC, -11.5670028264, 1e-6); %! assert_equal (mdl.ModelCriterion.AICc, -10.5670028264, 1e-6); %! assert_equal (mdl.ModelCriterion.BIC, -4.34035286736, 1e-6); %! assert_equal (mdl.ModelCriterion.CAIC, -0.340352867356, 1e-6); %! assert_equal (mdl.ModelFitVsNullModel.Fstat, 4109.84706734, 1e-4); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','Weight','Group_B','Group_C'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [56.0078644136; -0.00561620967245; 5.01185752618; -2.97710174861], 1e-8); %! assert_equal (mdl.Coefficients.SE, ... %! [0.41983060818; 0.000260647333731; 0.074514600823; 0.0746252935319], 1e-8); %! assert_equal (mdl.Coefficients.tStat, ... %! [133.40586256; -21.5471594973; 67.2600734732; -39.8940038652], 1e-5); %! assert_equal (mdl.Coefficients.pValue, ... %! [1.00643907454e-55; 5.62403610622e-24; 1.37550401987e-43; 1.97956717929e-34], 1e-8); %! assert_equal (mdl.ResponseName, 'MPG'); %! assert_equal (mdl.PredictorNames, {'Weight';'Group'}); %! assert_equal (mdl.Formula.LinearPredictor, '1 + Weight + Group'); %!test %! w = 1400 + (1:45)'*9; %! code = mod ((0:44), 3)' + 1; %! m = 55 - 0.005*w + [0 5 -3](mod ((0:44), 3) + 1)' + 0.3*sin ((1:45)'/6); %! mdl = stepwiselm ([w, code], m, 'CategoricalVars', 2, 'Upper', 'interactions', 'Verbose', 0); %! assert_equal (mdl.NumPredictors, 2); %! assert_equal (mdl.DFE, 41); %! assert_equal (mdl.SSE, 1.7056787721, 1e-8); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2_2','x2_3'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [56.007864413564; -0.00561620967245463; 5.01185752617851; -2.97710174860745], 1e-8); %! assert_equal (mdl.PredictorNames, {'x1';'x2'}); %!test %! ex = false (24, 1); %! ex([2 9 15]) = true; %! mdl = stepwiselm (X, y, 'Upper', 'interactions', 'Exclude', ex, 'Verbose', 0); %! assert_equal (mdl.NumObservations, 21); %! assert_equal (mdl.DFE, 17); %! assert_equal (mdl.SSE, 0.198175777, 1e-9); %! assert_equal (mdl.SSR, 23.9528827198, 1e-7); %! assert_equal (mdl.SST, 24.1510584968, 1e-7); %! assert_equal (mdl.Rsquared.Ordinary, 0.991794323341, 1e-9); %! assert_equal (mdl.Rsquared.Adjusted, 0.990346262754, 1e-9); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x3'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [4.10558649487; 1.78751811424; -1.08307909721; 0.499781080674], 1e-9); %! assert_equal (mdl.Coefficients.SE, ... %! [0.0832423204478; 0.15196995208; 0.0630609308312; 0.0532045604032], 1e-9); %! assert_equal (mdl.Coefficients.tStat, ... %! [49.320903992; 11.762312811; -17.1751206798; 9.39357598083], 1e-6); %! assert_equal (mdl.Coefficients.pValue, ... %! [8.57561115348e-20; 1.36568780583e-09; 3.54709582656e-12; 3.84225548219e-08], 1e-9); %! assert_equal (mdl.PredictorNames, {'x1';'x2';'x3'}); %!test %! mdl = stepwiselm (X, y, 'Upper', 'interactions', 'Exclude', [2 9 15], 'Verbose', 0); %! assert_equal (mdl.NumObservations, 21); %! assert_equal (mdl.SSE, 0.198175777, 1e-9); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x3'}); %!test %! wt = 0.6 + mod ((1:24)', 4)*0.25; %! mdl = stepwiselm (X, y, 'Upper', 'interactions', 'Weights', wt, 'Verbose', 0); %! assert_equal (mdl.NumObservations, 24); %! assert_equal (mdl.DFE, 20); %! assert_equal (mdl.SSE, 0.238797389485, 1e-9); %! assert_equal (mdl.SSR, 24.6325434536, 1e-7); %! assert_equal (mdl.SST, 24.8713408431, 1e-7); %! assert_equal (mdl.Rsquared.Ordinary, 0.990398692576, 1e-9); %! assert_equal (mdl.Rsquared.Adjusted, 0.988958496462, 1e-9); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x3'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [4.13717568894; 1.74127146188; -1.08433885985; 0.50630139226], 1e-9); %! assert_equal (mdl.Coefficients.SE, ... %! [0.0789083883313; 0.150888905488; 0.0591696809619; 0.0500332545132], 1e-9); %! assert_equal (mdl.Coefficients.tStat, ... %! [52.4301126462; 11.5400894204; -18.3259203401; 10.1192975989], 1e-6); %! assert_equal (mdl.Coefficients.pValue, ... %! [6.83329714397e-23; 2.70346109195e-10; 5.69051367149e-14; 2.59082042824e-09], 1e-9); %!test %! mdl = stepwiselm (X, y, 'constant', 'Intercept', false, 'Upper', 'linear', 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x3'}); %! assert_equal (mdl.SSE, 0.237882441508, 1e-9); %! assert_equal (mdl.Formula.HasIntercept, true); %!test %! mdl = stepwiselm (tbl, 'ResponseVar', 'y', 'PredictorVars', {'x1','x2'}, 'Verbose', 0); %! assert_equal (mdl.NumPredictors, 2); %! assert_equal (mdl.SSE, 1.11546087527, 1e-8); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x1:x2'}); %! assert_equal (mdl.PredictorNames, {'x1';'x2'}); %!test %! mdl = stepwiselm (tbl, 'ResponseVar', 'y', 'PredictorVars', [1 2], 'Verbose', 0); %! assert_equal (mdl.NumPredictors, 2); %! assert_equal (mdl.SSE, 1.11546087527, 1e-8); %! assert_equal (mdl.PredictorNames, {'x1';'x2'}); %!test %! mdl = stepwiselm (tbl, 'ResponseVar', 'y', 'Upper', 'interactions', 'Verbose', 0); %! assert_equal (mdl.NumPredictors, 3); %! assert_equal (mdl.SSE, 0.237882441508, 1e-9); %! assert_equal (mdl.ResponseName, 'y'); %!test %! mdl = stepwiselm (X, y, 'VarNames', {'Alpha','Beta','Gamma','Score'}, 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','Alpha','Beta','Gamma'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [4.11199897343; 1.79245385284; -1.06961941012; 0.51029628797], 1e-9); %! assert_equal (mdl.ResponseName, 'Score'); %! assert_equal (mdl.PredictorNames, {'Alpha';'Beta';'Gamma'}); %! assert_equal (mdl.VariableNames, {'Alpha';'Beta';'Gamma';'Score'}); %! assert_equal (mdl.Formula.LinearPredictor, '1 + Alpha + Beta + Gamma'); %!test %! mdl = stepwiselm (tbl, 'y ~ x1 + x2', 'Lower', 'y ~ x2', 'Upper', 'y ~ x1+x2+x3', 'PRemove', 0.99, 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x3'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [4.11199897343; 1.79245385284; -1.06961941012; 0.51029628797], 1e-9); %!test %! xu = (1:50)'/50; %! idx = mod ((1:50), 2); %! gu = {'Hi','Lo'}(idx + 1)'; %! sl = [7 1](idx + 1)'; %! yu = 2 + sl.*xu + 0.15*sin ((1:50)'/3); %! t = table (xu, gu, yu, 'VariableNames', {'X','G','Y'}); %! mdl = stepwiselm (t, 'Y ~ X + G', 'Upper', 'quadratic', 'Verbose', 0); %! assert_equal (mdl.NumObservations, 50); %! assert_equal (mdl.NumCoefficients, 4); %! assert_equal (mdl.NumVariables, 3); %! assert_equal (mdl.NumPredictors, 2); %! assert_equal (mdl.DFE, 46); %! assert_equal (mdl.SSE, 0.540477312442, 1e-9); %! assert_equal (mdl.SSR, 225.563187153, 1e-6); %! assert_equal (mdl.SST, 226.103664466, 1e-6); %! assert_equal (mdl.Rsquared.Ordinary, 0.997609603923, 1e-9); %! assert_equal (mdl.Rsquared.Adjusted, 0.997453708527, 1e-9); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','X','G_Hi','X:G_Hi'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [2.02876253697; 0.971349141035; 0.0058812055616; 5.98354193726], 1e-8); %! assert_equal (mdl.Coefficients.SE, ... %! [0.0433841054684; 0.0751585081172; 0.0622864091272; 0.106290181507], 1e-8); %! assert_equal (mdl.Coefficients.tStat, ... %! [46.7628066792; 12.9240077453; 0.0944219717272; 56.2943994677], 1e-5); %! assert_equal (mdl.Coefficients.pValue, ... %! [1.96152617542e-40; 6.42479379426e-17; 0.92518406634; 4.45945899483e-44], 1e-8); %! assert_equal (mdl.PredictorNames, {'X';'G'}); %!test %! idx3 = mod ((1:48), 3); idx2 = mod ((1:48), 2); %! g1 = {'A','B','C'}(idx3 + 1)'; %! g2 = {'P','Q'}(idx2 + 1)'; %! base = [0 4 -3](idx3 + 1)'; qeff = [5 0](idx2 + 1)'; %! cross = 6*((idx3 == 1) & (idx2 == 0))'; %! y2 = 10 + base + qeff + cross + 0.2*sin ((1:48)'/4); %! t = table (g1, g2, y2, 'VariableNames', {'G1','G2','Y'}); %! mdl = stepwiselm (t, 'Y ~ G1 + G2', 'Upper', 'interactions', 'Verbose', 0); %! assert_equal (mdl.NumObservations, 48); %! assert_equal (mdl.NumCoefficients, 6); %! assert_equal (mdl.NumPredictors, 2); %! assert_equal (mdl.DFE, 42); %! assert_equal (mdl.SSE, 1.00031269326, 1e-8); %! assert_equal (mdl.SSR, 1526.57402706, 1e-6); %! assert_equal (mdl.SST, 1527.57433975, 1e-6); %! assert_equal (mdl.Rsquared.Ordinary, 0.999345162676, 1e-9); %! assert_equal (mdl.Rsquared.Adjusted, 0.999267205851, 1e-9); %! assert_equal (mdl.CoefficientNames, ... %! {'(Intercept)','G1_C','G1_A','G2_P','G1_C:G2_P','G1_A:G2_P'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [14.0072303291; -7.00943486401; -4.00436689813; 10.9931096893; -5.98569634506; -6.00058514115], 1e-8); %! assert_equal (mdl.Coefficients.SE, ... %! [0.0545630013001; 0.0771637364424; 0.0771637364424; 0.0771637364424; 0.1091260026; 0.1091260026], 1e-8); %! assert_equal (mdl.Coefficients.tStat, ... %! [256.716639395; -90.8384584155; -51.8944141736; 142.464714594; -54.8512380408; -54.9876747811], 1e-5); %! assert_equal (mdl.Coefficients.pValue, ... %! [9.40196273084e-69; 7.64513283056e-50; 1.00685653504e-39; 5.03307771166e-58; 1.01525097736e-40; 9.15939827666e-41], 1e-8); %! assert_equal (mdl.PredictorNames, {'G1';'G2'}); %!test %! idxb = mod ((1:48), 2); %! ga = {'M','F'}(idxb + 1)'; %! gb = ga; %! y3 = 50 - 10*strcmp (ga, 'F') + 0.3*sin ((1:48)'/5); %! t = table (ga, gb, y3, 'VariableNames', {'GA','GB','Y'}); %! mdl = stepwiselm (t, 'Y ~ 1 + GB', 'ResponseVar', 'Y', ... %! 'PredictorVars', {'GA','GB'}, 'CategoricalVars', {'GA','GB'}, 'Verbose', 0); %! assert_equal (mdl.NumObservations, 48); %! assert_equal (mdl.NumCoefficients, 2); %! assert_equal (mdl.NumPredictors, 1); %! assert_equal (mdl.DFE, 46); %! assert_equal (mdl.SSE, 1.94300386071, 1e-8); %! assert_equal (mdl.SSR, 1199.4398757, 1e-6); %! assert_equal (mdl.SST, 1201.38287956, 1e-6); %! assert_equal (mdl.Rsquared.Ordinary, 0.998382693899, 1e-9); %! assert_equal (mdl.Rsquared.Adjusted, 0.998347535071, 1e-9); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','GB_M'}); %! assert_equal (mdl.Coefficients.Estimate, [40.0624369099; 9.99766587633], 1e-8); %! assert_equal (mdl.Coefficients.SE, [0.041951963781; 0.0593290361473], 1e-8); %! assert_equal (mdl.Coefficients.tStat, [954.959751562; 168.512191088], 1e-5); %! assert_equal (mdl.Coefficients.pValue, [1.70539389023e-100; 7.42606982902e-66], 1e-8); %! assert_equal (mdl.PredictorNames, {'GB'}); %! assert_equal (mdl.Formula.LinearPredictor, '1 + GB'); %!test %! e1 = (1:30)'/30; e2 = sin ((1:30)'/5); e3 = e1 + e2; %! ye = 3 + 1.5*e1 - e2 + 0.3*sin ((1:30)'/4); %! mdl = stepwiselm ([e1,e2,e3], ye, 'linear', 'Upper', 'linear', 'Verbose', 0); %! assert_equal (mdl.NumObservations, 30); %! assert_equal (mdl.NumCoefficients, 3); %! assert_equal (mdl.NumPredictors, 2); %! assert_equal (mdl.DFE, 27); %! assert_equal (mdl.SSE, 0.736469147619, 1e-9); %! assert_equal (mdl.SSR, 30.2842015322, 1e-7); %! assert_equal (mdl.SST, 31.0206706798, 1e-7); %! assert_equal (mdl.Rsquared.Ordinary, 0.976258761288, 1e-9); %! assert_equal (mdl.Rsquared.Adjusted, 0.974500151013, 1e-9); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x2','x3'}); %! assert_equal (mdl.Coefficients.Estimate, [2.83853512391; -2.5770987124; 1.87079571697], 1e-8); %! assert_equal (mdl.Coefficients.SE, [0.101176877319; 0.131763778382; 0.186693438852], 1e-8); %! assert_equal (mdl.Coefficients.tStat, [28.0551762332; -19.5584761158; 10.0206827218], 1e-6); %! assert_equal (mdl.Coefficients.pValue, [1.65333811724e-21; 1.79264031168e-17; 1.35825364979e-10], 1e-9); %! assert_equal (mdl.PredictorNames, {'x2';'x3'}); %!test %! h1 = [7;1;11;11;7;11;3;1;2;21;1;11;10]; %! h2 = [26;29;56;31;52;55;71;31;54;47;40;66;68]; %! h3 = [6;15;8;8;6;9;17;22;18;4;23;9;8]; %! h4 = [60;52;20;47;33;22;6;44;22;26;34;12;12]; %! yh = [78.5;74.3;104.3;87.6;95.9;109.2;102.7;72.5;93.1;115.9;83.8;113.3;109.4]; %! mdl = stepwiselm ([h1,h2,h3,h4], yh, 'PEnter', 0.06, 'Verbose', 0); %! assert_equal (mdl.NumObservations, 13); %! assert_equal (mdl.NumCoefficients, 3); %! assert_equal (mdl.NumPredictors, 2); %! assert_equal (mdl.DFE, 10); %! assert_equal (mdl.SSE, 57.9044831761, 1e-8); %! assert_equal (mdl.SSR, 2657.85859375, 1e-5); %! assert_equal (mdl.SST, 2715.76307692, 1e-5); %! assert_equal (mdl.Rsquared.Ordinary, 0.978678374536, 1e-9); %! assert_equal (mdl.Rsquared.Adjusted, 0.974414049443, 1e-9); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2'}); %! assert_equal (mdl.Coefficients.Estimate, [52.5773488821; 1.46830574222; 0.662250491275], 1e-8); %! assert_equal (mdl.Coefficients.SE, [2.2861743345; 0.121300923606; 0.0458547214685], 1e-8); %! assert_equal (mdl.Coefficients.tStat, [22.9979613053; 12.1046542645; 14.4423620963], 1e-6); %! assert_equal (mdl.Coefficients.pValue, [5.45657090149e-10; 2.69221217969e-07; 5.02896031564e-08], 1e-9); %! assert_equal (mdl.PredictorNames, {'x1';'x2'}); %! assert_equal (mdl.VariableNames, {'x1';'x2';'x3';'x4';'y'}); %! assert_equal (mdl.Formula.LinearPredictor, '1 + x1 + x2'); %!test %! w = 1400 + (1:45)'*9; %! g = {'A','B','C'}(mod ((0:44), 3) + 1)'; %! m = 55 - 0.005*w + [0 5 -3](mod ((0:44), 3) + 1)' + 0.3*sin ((1:45)'/6); %! t = table (w, g, m, 'VariableNames', {'Weight','Group','MPG'}); %! mdl0 = fitlm (t, 'MPG ~ Weight'); %! mdl = step (mdl0, 'Verbose', 0); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','Weight','Group_B','Group_C'}); %! assert_equal (mdl.SSE, 1.7056787721, 1e-8); %! assert_equal (mdl.Coefficients.Estimate, ... %! [56.0078644136; -0.00561620967245; 5.01185752618; -2.97710174861], 1e-8); %!test %! w = 1400 + (1:45)'*9; %! g = {'A','B','C'}(mod ((0:44), 3) + 1)'; %! m = 55 - 0.005*w + [0 5 -3](mod ((0:44), 3) + 1)' + 0.3*sin ((1:45)'/6); %! t = table (w, g, m, 'VariableNames', {'Weight','Group','MPG'}); %! mdl0 = fitlm (t, 'MPG ~ Weight'); %! mdl = step (mdl0, 'Upper', 'quadratic', 'NSteps', 5, 'Verbose', 0); %! assert_equal (mdl.NumCoefficients, 5); %! assert_equal (mdl.DFE, 40); %! assert_equal (mdl.SSE, 0.987020309951, 1e-8); %! assert_equal (mdl.Rsquared.Ordinary, 0.998082108646, 1e-9); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','Weight','Group_B','Group_C','Weight^2'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [82.5932822956; -0.0388795480431; 5.01269583682; -2.97710174861; 1.03495141166e-05], 1e-6); %!test %! w = 1400 + (1:45)'*9; %! code = mod ((0:44), 3)' + 1; %! m = 55 - 0.005*w + [0 5 -3](mod ((0:44), 3) + 1)' + 0.3*sin ((1:45)'/6); %! mdl0 = fitlm ([w, code], m); %! mdl = step (mdl0, 'Criterion', 'bic', 'Upper', 'interactions', 'Verbose', 0); %! assert_equal (mdl.NumCoefficients, 2); %! assert_equal (mdl.DFE, 43); %! assert_equal (mdl.SSE, 443.573715939, 1e-6); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x2'}); %! assert_equal (mdl.Coefficients.Estimate, [50.739060918498; -1.53909676135581], 1e-6); %!test %! tbl_noy = table (x1, x2, x3, 'VariableNames', {'x1','x2','x3'}); %! mdl = stepwiselm (tbl_noy, y, 'Upper', 'interactions', 'Verbose', 0); %! assert_equal (mdl.NumObservations, 24); %! assert_equal (mdl.NumCoefficients, 4); %! assert_equal (mdl.NumPredictors, 3); %! assert_equal (mdl.DFE, 20); %! assert_equal (mdl.SSE, 0.237882441508, 1e-9); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x3'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [4.1119989734334; 1.79245385284216; -1.06961941011697; 0.510296287970133], 1e-9); %! assert_equal (mdl.ResponseName, 'y'); %! assert_equal (mdl.PredictorNames, {'x1';'x2';'x3'}); %!test %! T_initial = [0 0 0 0]; %! T_upper = [0 0 0 0; 1 0 0 0; 0 1 0 0; 1 1 0 0]; %! mdl = stepwiselm (X, y, T_initial, 'Upper', T_upper, 'Verbose', 0); %! assert_equal (mdl.NumCoefficients, 4); %! assert_equal (mdl.NumPredictors, 2); %! assert_equal (mdl.DFE, 20); %! assert_equal (mdl.SSE, 1.11546087527, 1e-8); %! assert_equal (mdl.SSR, 24.5179468716, 1e-6); %! assert_equal (mdl.Rsquared.Ordinary, 0.956484097383, 1e-9); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x1:x2'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [4.00100246650923; 1.35895695996517; -0.232064749079705; -1.27804956860349], 1e-8); %! assert_equal (mdl.Coefficients.SE, ... %! [0.181530230599361; 0.299744817513483; 0.272053305331473; 0.469486828642072], 1e-8); %! assert_equal (mdl.Coefficients.tStat, ... %! [22.0404196772024; 4.53371294702715; -0.853012055107928; -2.72222667524045], 1e-6); %! assert_equal (mdl.Coefficients.pValue, ... %! [1.67738549623508e-15; 0.000202251230338429; 0.403753045528205; 0.0131231039551409], 1e-8); %! assert_equal (mdl.PredictorNames, {'x1';'x2'}); %!test %! mdl = stepwiselm (X, y, 'Upper', 'poly110', 'Verbose', 0); %! assert_equal (mdl.NumCoefficients, 3); %! assert_equal (mdl.NumPredictors, 2); %! assert_equal (mdl.DFE, 21); %! assert_equal (mdl.SSE, 1.52876802397, 1e-8); %! assert_equal (mdl.SSR, 24.1046397229, 1e-6); %! assert_equal (mdl.Rsquared.Ordinary, 0.940360328246, 1e-9); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [4.21600780425081; 1.37121494151013; -0.897420541182461], 1e-8); %! assert_equal (mdl.Coefficients.SE, ... %! [0.186735917268724; 0.342414105749106; 0.136495702980697], 1e-8); %! assert_equal (mdl.Coefficients.tStat, ... %! [22.577380216489; 4.00455156048637; -6.57471643125185], 1e-6); %! assert_equal (mdl.Coefficients.pValue, ... %! [3.27879166781824e-16; 0.000642688076385025; 1.64115221424634e-06], 1e-8); %! assert_equal (mdl.PredictorNames, {'x1';'x2'}); %!test %! p1 = (1:15)'/15; %! code = repmat ([1;2;3], 5, 1); %! resp = [5.18127588719375; 9.35081376514746; 3.49974949866041; 5.6242630760159; ... %! 9.72651388107706; 3.81411200080599; 5.89825501056437; 9.99098641713587; ... %! 4.10224698823349; 6.23744090586702; 10.3961126341096; 4.57205845018011; ... %! 6.75484533214212; 10.9323653265385; 5.09379999767747]; %! mdl = stepwiselm ([p1, code], resp, 'CategoricalVars', logical ([0 1]), ... %! 'Upper', 'interactions', 'Verbose', 0); %! assert_equal (mdl.NumObservations, 15); %! assert_equal (mdl.NumCoefficients, 4); %! assert_equal (mdl.NumPredictors, 2); %! assert_equal (mdl.DFE, 11); %! assert_equal (mdl.SSE, 0.0670192081399, 1e-8); %! assert_equal (mdl.Rsquared.Ordinary, 0.999296834962, 1e-9); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2_2','x2_3'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [5.04173312790335; 1.92317767382845; 4.01193051752317; -1.97924634508893], 1e-8); %! assert_equal (mdl.Coefficients.SE, ... %! [0.0482103221773782; 0.0712545629266561; 0.0495946318075163; 0.050272494208676], 1e-8); %! assert_equal (mdl.Coefficients.tStat, ... %! [104.577876691085; 26.990238868043; 80.8944510989422; -39.3703629836483], 1e-5); %! assert_equal (mdl.Coefficients.pValue, ... %! [7.63821769167572e-18; 2.10244709730975e-11; 1.28303158624727e-16; 3.44025340004292e-13], 1e-8); %! assert_equal (mdl.PredictorNames, {'x1';'x2'}); %!test %! x1n = x1; yn = y; %! x1n(5) = NaN; %! yn(12) = NaN; %! mdl = stepwiselm ([x1n, x2, x3], yn, 'Upper', 'interactions', 'Verbose', 0); %! assert_equal (mdl.NumObservations, 22); %! assert_equal (mdl.NumCoefficients, 4); %! assert_equal (mdl.NumPredictors, 3); %! assert_equal (mdl.DFE, 18); %! assert_equal (mdl.SSE, 0.220518784715, 1e-8); %! assert_equal (mdl.SSR, 23.3502845138, 1e-6); %! assert_equal (mdl.Rsquared.Ordinary, 0.990644409445, 1e-9); %! assert_equal (mdl.CoefficientNames, {'(Intercept)','x1','x2','x3'}); %! assert_equal (mdl.Coefficients.Estimate, ... %! [4.11761577459912; 1.79579438452674; -1.05927400080053; 0.512812488395983], 1e-8); %! assert_equal (mdl.Coefficients.SE, ... %! [0.0775488744294778; 0.146527417121933; 0.0594402526920882; 0.0517068469441139], 1e-8); %! assert_equal (mdl.Coefficients.tStat, ... %! [53.0970411226747; 12.2556885243692; -17.8208192735615; 9.91769018424668], 1e-6); %! assert_equal (mdl.Coefficients.pValue, ... %! [3.09620365929824e-21; 3.59135935529609e-10; 6.98703068441474e-13; 1.01407825601795e-08], 1e-8); %! assert_equal (mdl.PredictorNames, {'x1';'x2';'x3'}); %!test %! ## Steps carries MATLAB's seven fields, in its order. %! s1 = ((1:48)' - 24.5)/12; %! s2 = sin ((1:48)'/5); %! s3 = cos ((1:48)'/7); %! sy = 4 + 2.5*s1 - 1.1*s2 + 0.6*s1.^2 + 0.2*sin ((1:48)'/3); %! mdl = stepwiselm ([s1, s2, s3], sy, 'constant', 'Upper', 'quadratic', ... %! 'Verbose', 0); %! assert_equal (fieldnames (mdl.Steps), {'Start'; 'Lower'; 'Upper'; ... %! 'Criterion'; 'PEnter'; ... %! 'PRemove'; 'History'}); %!test %! ## The history records every step, and matches MATLAB R2024a throughout. %! s1 = ((1:48)' - 24.5)/12; %! s2 = sin ((1:48)'/5); %! s3 = cos ((1:48)'/7); %! sy = 4 + 2.5*s1 - 1.1*s2 + 0.6*s1.^2 + 0.2*sin ((1:48)'/3); %! mdl = stepwiselm ([s1, s2, s3], sy, 'constant', 'Upper', 'quadratic', ... %! 'Verbose', 0); %! assert_equal (mdl.Steps.History.TermName, ... %! {'1'; 'x1'; 'x1^2'; 'x2'; 'x2^2'; 'x3'; 'x2:x3'; 'x1:x2'; ... %! 'x1:x3'}); %! assert_equal (mdl.Steps.History.DF, (1:9)'); %! assert_equal (mdl.Steps.History.FStat, ... %! [NaN; 685.736350968728; 12.6370657930220; ... %! 1231.00720701963; 124.872101651316; 13.4519287960430; ... %! 107.087846311142; 34.9838482569170; 420.274658550522], 1e-8); %!test %! ## The default criterion is reported as MATLAB spells it, with the %! ## thresholds it resolved. %! s1 = ((1:48)' - 24.5)/12; %! sy = 4 + 2.5*s1 + 0.2*sin ((1:48)'/3); %! mdl = stepwiselm (s1, sy, 'constant', 'Upper', 'linear', 'Verbose', 0); %! assert_equal (mdl.Steps.Criterion, 'SSE'); %! assert_equal (mdl.Steps.PEnter, 0.05); %! assert_equal (mdl.Steps.PRemove, 0.10); %!test %! ## A criterion asked for is reported as it was asked for, not canonicalised. %! s1 = ((1:48)' - 24.5)/12; %! sy = 4 + 2.5*s1 + 0.2*sin ((1:48)'/3); %! m1 = stepwiselm (s1, sy, 'constant', 'Upper', 'linear', 'Criterion', ... %! 'aic', 'Verbose', 0); %! m2 = stepwiselm (s1, sy, 'constant', 'Upper', 'linear', 'Criterion', ... %! 'AIC', 'Verbose', 0); %! assert_equal (m1.Steps.Criterion, 'aic'); %! assert_equal (m2.Steps.Criterion, 'AIC'); %!test %! ## An information criterion has no test to report, so the history carries %! ## its value after each step, the starting model included. %! s1 = ((1:48)' - 24.5)/12; %! s2 = sin ((1:48)'/5); %! s3 = cos ((1:48)'/7); %! sy = 4 + 2.5*s1 - 1.1*s2 + 0.6*s1.^2 + 0.2*sin ((1:48)'/3); %! mdl = stepwiselm ([s1, s2, s3], sy, 'constant', 'Upper', 'quadratic', ... %! 'Criterion', 'aic', 'Verbose', 0); %! assert_equal (mdl.Steps.History.Properties.VariableNames, ... %! {'Action', 'TermName', 'Terms', 'DF', 'delDF', 'AIC'}); %! assert_equal (mdl.Steps.History.AIC(1), 243.691352606191, 1e-9); %! assert_equal (mdl.Steps.History.AIC(end), -339.898392996306, 1e-9); %!test %! ## A removal takes degrees of freedom away, so its delDF is negative. %! s1 = ((1:48)' - 24.5)/12; %! s2 = sin ((1:48)'/5); %! s3 = cos ((1:48)'/7); %! sy = 4 + 2.5*s1 - 1.1*s2 + 0.6*s1.^2 + 0.2*sin ((1:48)'/3); %! mdl = stepwiselm ([s1, s2, s3], sy, 'constant', 'Upper', 'quadratic', ... %! 'Criterion', 'aic', 'Verbose', 0); %! assert_equal (mdl.Steps.History.Action{end}, 'Remove'); %! assert_equal (mdl.Steps.History.TermName{end}, 'x2^2'); %! assert_equal (mdl.Steps.History.delDF(end), -1); %! assert_equal (mdl.Steps.History.DF(end), 9); %!test %! ## Under 'rsquared' the column is named for the criterion too. %! s1 = ((1:48)' - 24.5)/12; %! s2 = sin ((1:48)'/5); %! s3 = cos ((1:48)'/7); %! sy = 4 + 2.5*s1 - 1.1*s2 + 0.6*s1.^2 + 0.2*sin ((1:48)'/3); %! mdl = stepwiselm ([s1, s2, s3], sy, 'constant', 'Upper', 'quadratic', ... %! 'Criterion', 'rsquared', 'Verbose', 0); %! assert_equal (mdl.Steps.History.Properties.VariableNames, ... %! {'Action', 'TermName', 'Terms', 'DF', 'delDF', 'Rsquared'}); %! assert_equal (mdl.Steps.History.Rsquared, [0; 0.937135827762142], 1e-12); %!test %! ## A categorical predictor is one step worth L-1 degrees of freedom, named %! ## for the predictor rather than for its indicators. %! w = 1400 + (1:45)'*9; %! g = {'A','B','C'}(mod ((0:44), 3) + 1)'; %! m = 55 - 0.005*w + [0 5 -3](mod ((0:44), 3) + 1)' + 0.3*sin ((1:45)'/6); %! t = table (w, g, m, 'VariableNames', {'Weight','Group','MPG'}); %! mdl = stepwiselm (t, 'MPG ~ 1', 'Upper', 'MPG ~ Weight + Group', ... %! 'Verbose', 0); %! assert_equal (mdl.Steps.History.TermName, {'1'; 'Group'; 'Weight'}); %! assert_equal (mdl.Steps.History.DF, [1; 3; 4]); %! assert_equal (mdl.Steps.History.delDF, [NaN; 2; 1]); %! assert_equal (mdl.Steps.History.FStat, ... %! [NaN; 493.133397072530; 464.280082409591], 1e-8); %!test %! ## The Terms of the last row are the terms of the model returned. %! w = 1400 + (1:45)'*9; %! g = {'A','B','C'}(mod ((0:44), 3) + 1)'; %! m = 55 - 0.005*w + [0 5 -3](mod ((0:44), 3) + 1)' + 0.3*sin ((1:45)'/6); %! t = table (w, g, m, 'VariableNames', {'Weight','Group','MPG'}); %! mdl = stepwiselm (t, 'MPG ~ 1', 'Upper', 'MPG ~ Weight + Group', ... %! 'Verbose', 0); %! assert_equal (mdl.Steps.History.Terms{end}, mdl.Formula.Terms); %! assert_equal (mdl.Steps.History.Terms{end}, [0, 0, 0; 1, 0, 0; 0, 1, 0]); %!test %! ## The Start row names the starting model, not the constant. %! w = 1400 + (1:45)'*9; %! g = {'A','B','C'}(mod ((0:44), 3) + 1)'; %! m = 55 - 0.005*w + [0 5 -3](mod ((0:44), 3) + 1)' + 0.3*sin ((1:45)'/6); %! t = table (w, g, m, 'VariableNames', {'Weight','Group','MPG'}); %! mdl = stepwiselm (t, 'MPG ~ Weight + Group', 'Lower', 'MPG ~ Weight', ... %! 'Upper', 'MPG ~ Weight + Group', 'Verbose', 0); %! assert_equal (mdl.Steps.History.TermName, {'1 + Weight + Group'}); %! assert_equal (mdl.Steps.History.DF, 4); %!test %! ## Start, Lower, and Upper are formula objects over the model's variables. %! s1 = ((1:48)' - 24.5)/12; %! s2 = sin ((1:48)'/5); %! sy = 4 + 2.5*s1 - 1.1*s2 + 0.2*sin ((1:48)'/3); %! mdl = stepwiselm ([s1, s2], sy, 'constant', 'Upper', 'linear', ... %! 'Verbose', 0); %! assert_equal (class (mdl.Steps.Start), 'LinearFormula'); %! assert_equal (char (mdl.Steps.Start), 'y ~ 1'); %! assert_equal (char (mdl.Steps.Upper), 'y ~ 1 + x1 + x2'); %! assert_equal (mdl.Steps.Start.VariableNames, {'x1', 'x2', 'y'}); %!test %! ## A power term in a starting formula survives. Folding it onto the %! ## all-zero row once discarded it as a duplicate intercept, so %! ## 'y ~ x1 + x1^2' fitted '1 + x1'. %! s1 = ((1:48)' - 24.5)/12; %! s2 = sin ((1:48)'/5); %! sy = 4 + 2.5*s1 - 1.1*s2 + 0.6*s1.^2 + 0.2*sin ((1:48)'/3); %! mdl = stepwiselm ([s1, s2], sy, 'y ~ x1 + x1^2', 'Upper', 'quadratic', ... %! 'NSteps', 0, 'Verbose', 0); %! assert_equal (mdl.Formula.LinearPredictor, '1 + x1 + x1^2'); %! assert_equal (mdl.Steps.History.TermName, {'1 + x1 + x1^2'}); %! assert_equal (mdl.Steps.History.DF, 3); %!test %! ## Capping the search at no steps leaves the history with its Start row. %! s1 = ((1:48)' - 24.5)/12; %! sy = 4 + 2.5*s1 + 0.2*sin ((1:48)'/3); %! mdl = stepwiselm (s1, sy, 'constant', 'Upper', 'linear', 'NSteps', 0, ... %! 'Verbose', 0); %! assert_equal (size (mdl.Steps.History, 1), 1); %! assert_equal (mdl.Steps.History.Action{1}, 'Start'); %!error stepwiselm () %!error stepwiselm (X, y, 'Verbose') %!error ... %! stepwiselm (X, y, 'Criterion', 'bogus') %!error ... %! stepwiselm (X, y, 'RobustOpts', 'on') %!error ... %! stepwiselm (X, y, [0 0], 'PredictorVars', {'x1', 'x2'}) %!error ... %! stepwiselm (tbl, 'y ~ x1', 'PredictorVars', {'x2'}) %!error ... %! stepwiselm (tbl, 'y ~ x1', 'ResponseVar', 'x2', 'PredictorVars', {'x1'}) %!error stepwiselm (X, y, 'Upper', [0 0 0 0 0]) %!error ... %! stepwiselm (X, y, 'Upper', {1, 2}) %!error stepwiselm (X, y, 'Upper', 'poly1') %!error stepwiselm (X, y, 'Upper', 'bogusmodel') %!error stepwiselm (X, y, 'PEnter', 0.5) %!error stepwiselm (X, y, 'PEnter', 0.1, 'PRemove', 0.1) %!error stepwiselm (X, y, 'Criterion', 'aic', 'PEnter', 0.5, 'PRemove', 0.1) %!error stepwiselm (X, y, 'Criterion', 'rsquared', 'PEnter', 0.001, 'PRemove', 0.5) %!test %! ## sse's default thresholds, 0.05 and 0.10, do not trip the guard %! assert_equal (isa (stepwiselm (X, y, 'Criterion', 'sse', 'Verbose', 0), ... %! 'LinearModel'), true); %!test %! ## aic's, 0 and 0.01 %! assert_equal (isa (stepwiselm (X, y, 'Criterion', 'aic', 'Verbose', 0), ... %! 'LinearModel'), true); %!test %! ## bic's, 0 and 0.01 %! assert_equal (isa (stepwiselm (X, y, 'Criterion', 'bic', 'Verbose', 0), ... %! 'LinearModel'), true); %!test %! ## rsquared's, 0.1 and 0.05, which run the other way round %! assert_equal (isa (stepwiselm (X, y, 'Criterion', 'rsquared', 'Verbose', 0), ... %! 'LinearModel'), true); %!test %! ## adjrsquared's, 0 and -0.05, the negative one %! assert_equal (isa (stepwiselm (X, y, 'Criterion', 'adjrsquared', 'Verbose', 0), ... %! 'LinearModel'), true); %!test %! ## a row vector Y is taken as a column %! Xr = [1 2; 2 1; 3 5; 4 3; 5 6; 6 4; 7 8; 8 7]; %! yr = [1.2 2.1 3.9 4.2 5.8 6.1 8.2 7.9]; %! m1 = stepwiselm (Xr, yr, 'Verbose', 0); %! m2 = stepwiselm (Xr, yr', 'Verbose', 0); %! assert_equal (m1.Coefficients.Estimate, m2.Coefficients.Estimate); statistics-release-2.1.2/inst/cholcov.m000066400000000000000000000101761527311104300201160ustar00rootroot00000000000000## Copyright (C) 2022 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{T} =} cholcov (@var{sigma}) ## @deftypefnx {statistics} {[@var{T}, @var{p}] =} cholcov (@var{sigma}) ## @deftypefnx {statistics} {[@dots{}] =} cholcov (@var{sigma}, @var{flag}) ## ## Cholesky-like decomposition for covariance matrix. ## ## @code{@var{T} = cholcov (@var{sigma})} computes matrix @var{T} such that ## @var{sigma} = @var{T}' @var{T}. @var{sigma} must be square, symmetric, and ## positive semi-definite. ## ## If @var{sigma} is positive definite, then @var{T} is the square, upper ## triangular Cholesky factor. If @var{sigma} is not positive definite, @var{T} ## is computed with an eigenvalue decomposition of @var{sigma}, but in this case ## @var{T} is not necessarily triangular or square. Any eigenvectors whose ## corresponding eigenvalue is close to zero (within a tolerance) are omitted. ## If any remaining eigenvalues are negative, @var{T} is empty. ## ## The tolerance is calculated as @code{10 * eps (max (abs (diag (sigma))))}. ## ## @code{[@var{T}, @var{p}] = cholcov (@var{sigma})} returns in @var{p} the ## number of negative eigenvalues of @var{sigma}. If @var{p} > 0, then @var{T} ## is empty, whereas if @var{p} = 0, @var{sigma}) is positive semi-definite. ## ## If @var{sigma} is not square and symmetric, P is NaN and T is empty. ## ## @code{[@var{T}, @var{p}] = cholcov (@var{sigma}, 0)} returns @var{p} = 0 if ## @var{sigma} is positive definite, in which case @var{T} is the Cholesky ## factor. If @var{sigma} is not positive definite, @var{p} is a positive ## integer and @var{T} is empty. ## ## @code{[@dots{}] = cholcov (@var{sigma}, 1)} is equivalent to ## @code{ [@dots{}] = cholcov (@var{sigma})}. ## ## @seealso{chol} ## @end deftypefn function [T, p] = cholcov (sigma, flag) ## Check number of input arguments narginchk (1,2) ## Add default flag if not given if (nargin < 2) flag = 1; endif ## Check if sigma is a sparse matrix is_sparse = issparse (sigma); ## Check if sigma is single or double class is_type = 'double'; if (isa (sigma, 'single')) is_type = 'single'; endif ## Check if sigma is square and symmetric [col, row] = size (sigma); ## Add tolerance Tol = 10 * eps (max (abs (diag (sigma)))); if ((row == col) && all (all (abs (sigma - sigma') < col * Tol))) ## Check if positive definite [T, p] = chol (sigma); if (p > 0) ## Check flag for factoring using eigenvalue decomposition if (flag) [V, LAMBDA] = eig (full ((sigma + sigma') / 2)); [~, EIGMAX] = max (abs (V), [], 1); neg_idx = (V(EIGMAX + (0:row:(col-1)*row)) < 0); V(:,neg_idx) = -V(:,neg_idx); LAMBDA = diag (LAMBDA); Tol = eps (max (LAMBDA)) * length (LAMBDA); t = (abs (LAMBDA) > Tol); LAMBDA = LAMBDA(t); p = sum (LAMBDA < 0); ## Check for negative eigenvalues if (p == 0) T = diag (sqrt (LAMBDA)) * V(:,t)'; else T = zeros (0, is_type); endif else T = zeros (0, is_type); endif endif else T = zeros (0, is_type); p = NaN (is_type); endif if (is_sparse) T = sparse (T); endif endfunction %!demo %! C1 = [2, 1, 1, 2; 1, 2, 1, 2; 1, 1, 2, 2; 2, 2, 2, 3] %! T = cholcov (C1) %! C2 = T'*T %!test %! C1 = [2, 1, 1, 2; 1, 2, 1, 2; 1, 1, 2, 2; 2, 2, 2, 3]; %! T = cholcov (C1); %! assert_equal (C1, T'*T, 1e-15 * ones (size (C1))); statistics-release-2.1.2/inst/datasets/000077500000000000000000000000001527311104300201065ustar00rootroot00000000000000statistics-release-2.1.2/inst/datasets/acetylene.mat000066400000000000000000000045241527311104300225670ustar00rootroot00000000000000Octave-1-L Descriptionÿ sq_stringþÿÿÿi= M xxxy SKC RMT= u 123: ouh eah l ::: une freA t C rum eq c i RRCo cgi ruAe p eaon eic eamt l atnv :,a nrey e cete Tl cdrl t ar . etie r oocs ,E :,cn e rfti n ae g o Tg Dn r tHtn .i . D e e2i n ,Sa s m mo Te tt s ptef ae Raa i eo mr .t o r (n ui s= n ans- rn St= t-eh ag ni d uhce , ec a reop P ei t epnt "r ,a a tda No n (asn eg ", w dn)e wr R i ee e iv t g t As d. h r( o cs g2 em e, e9 c eo a t o sl c yv Rn r e e l. eo r c t e5 g. e er y n7 r1 l na l e e a tt e ( s( t ii n P1 s1 e go e r9 i9 d r) o6 o7 a ( c1 n5 p d % e) ) r e ) s, i, e ) s n d p p i Up Pp c s. r. t e4 a3 o s3 c- r - t2 s H4 i0 y9 c. d. e r , o " g e n D i l u t i o n , " x1ÿmatrixþÿÿÿP”@P”@P”@P”@P”@P”@À’@À’@À’@À’@À’@À’@0‘@0‘@0‘@0‘@x2ÿmatrixþÿÿÿ@"@&@+@1@7@333333@@&@+@1@7@333333@@&@1@x3ÿmatrixþÿÿÿú~j¼t“ˆ?ú~j¼t“ˆ?Zd;ßO�‡?9´Èv¾ŸŠ?ÙÎ÷S㥋?ú~j¼t“ˆ?{®Gáz¤?Ûù~j¼t£?ü©ñÒMb ?9´Èv¾Ÿš?œÄ °rh¡?Ë¡E¶óý¤?/Ý$�µ?J +‡¹?Zd;ßO�·?j¼t“¶?yÿmatrixþÿÿÿ€H@š™™™™I@@I@@H@ÀG@@F@<@€?@@A@€A@C@@C@.@1@€4@€=@statistics-release-2.1.2/inst/datasets/arrhythmia.mat000066400000000000000000037416671527311104300230110ustar00rootroot00000000000000Octave-1-L Descriptionÿ sq_stringþÿÿÿEChSY Nate ortei mdp s121ii:V -6na/at=1 ac/rh 5=l aNen ara o=uarcmc ntrhelavcthisaralryv srraitefshisbh.o yosumiratuitic thsfeasntm is .arice dumialdaaceb a rtisu scea. t sa eoe escfdf seoru r sdo/2a o emm7n f d l9g t/ i a ahdin r seang r tp h nUauf y uCstr t mIe o h e tvm m rmsa i ia/r1 a ccAi hrat virbo anhl reye1 i ts6 alh.: bem lai era sn i wn ig t hr e vp ao ls ui et so r 0y : a n d 1VarNamesÿcellþÿÿÿÿ sq_stringþÿÿÿ Age, yearsÿ sq_stringþÿÿÿSex (0=male, 1=female)ÿ sq_stringþÿÿÿ Height, cmÿ sq_stringþÿÿÿ Weight, kgÿ sq_stringþÿÿÿ QRS durationÿ sq_stringþÿÿÿ P-R intervalÿ sq_stringþÿÿÿ Q-T intervalÿ sq_stringþÿÿÿ T intervalÿ sq_stringþÿÿÿ P intervalÿ sq_stringþÿÿÿ QRS angleÿ sq_stringþÿÿÿT angleÿ sq_stringþÿÿÿP angleÿ sq_stringþÿÿÿ QRST angleÿ sq_stringþÿÿÿJ angleÿ sq_stringþÿÿÿHeart rate per minuteÿ sq_stringþÿÿÿDI Q wave widthÿ sq_stringþÿÿÿDI R wave widthÿ sq_stringþÿÿÿDI S wave widthÿ sq_stringþÿÿÿDI R' wave widthÿ sq_stringþÿÿÿDI S' wave widthÿ sq_stringþÿÿÿDI N of intrinsic deflectionsÿ sq_stringþÿÿÿDI Existence of ragged R waveÿ sq_stringþÿÿÿ-DI Existence of diphasic derivation of R waveÿ sq_stringþÿÿÿDI Existence of ragged P waveÿ sq_stringþÿÿÿ-DI Existence of diphasic derivation of P waveÿ sq_stringþÿÿÿDI Existence of ragged T waveÿ sq_stringþÿÿÿ-DI Existence of diphasic derivation of T waveÿ sq_stringþÿÿÿDII Q wave widthÿ sq_stringþÿÿÿDII R wave widthÿ sq_stringþÿÿÿDII S wave widthÿ sq_stringþÿÿÿDII R' wave widthÿ sq_stringþÿÿÿDII S' wave widthÿ sq_stringþÿÿÿDII N of intrinsic deflectionsÿ sq_stringþÿÿÿDII Existence of ragged R waveÿ sq_stringþÿÿÿ.DII Existence of diphasic derivation of R waveÿ sq_stringþÿÿÿDII Existence of ragged P waveÿ sq_stringþÿÿÿ.DII Existence of diphasic derivation of P waveÿ sq_stringþÿÿÿDII Existence of ragged T waveÿ sq_stringþÿÿÿ.DII Existence of diphasic derivation of T waveÿ sq_stringþÿÿÿDIII Q wave widthÿ sq_stringþÿÿÿDIII R wave widthÿ sq_stringþÿÿÿDIII S wave widthÿ sq_stringþÿÿÿDIII R' wave widthÿ sq_stringþÿÿÿDIII S' wave widthÿ sq_stringþÿÿÿDIII N of intrinsic deflectionsÿ sq_stringþÿÿÿDIII Existence of ragged R waveÿ sq_stringþÿÿÿ/DIII Existence of diphasic derivation of R waveÿ sq_stringþÿÿÿDIII Existence of ragged P waveÿ sq_stringþÿÿÿ/DIII Existence of diphasic derivation of P waveÿ sq_stringþÿÿÿDIII Existence of ragged T waveÿ sq_stringþÿÿÿ/DIII Existence of diphasic derivation of T waveÿ sq_stringþÿÿÿAVR Q wave widthÿ sq_stringþÿÿÿAVR R wave widthÿ sq_stringþÿÿÿAVR S wave widthÿ sq_stringþÿÿÿAVR R' wave widthÿ sq_stringþÿÿÿAVR S' wave widthÿ sq_stringþÿÿÿAVR N of intrinsic deflectionsÿ sq_stringþÿÿÿAVR Existence of ragged R waveÿ sq_stringþÿÿÿ.AVR Existence of diphasic derivation of R waveÿ sq_stringþÿÿÿAVR Existence of ragged P waveÿ sq_stringþÿÿÿ.AVR Existence of diphasic derivation of P waveÿ sq_stringþÿÿÿAVR Existence of ragged T waveÿ sq_stringþÿÿÿ.AVR Existence of diphasic derivation of T waveÿ sq_stringþÿÿÿAVL Q wave widthÿ sq_stringþÿÿÿAVL R wave widthÿ sq_stringþÿÿÿAVL S wave widthÿ sq_stringþÿÿÿAVL R' wave widthÿ sq_stringþÿÿÿAVL S' wave widthÿ sq_stringþÿÿÿAVL N of intrinsic deflectionsÿ sq_stringþÿÿÿAVL Existence of ragged R waveÿ sq_stringþÿÿÿ.AVL Existence of diphasic derivation of R waveÿ sq_stringþÿÿÿAVL Existence of ragged P waveÿ sq_stringþÿÿÿ.AVL Existence of diphasic derivation of P waveÿ sq_stringþÿÿÿAVL Existence of ragged T waveÿ sq_stringþÿÿÿ.AVL Existence of diphasic derivation of T waveÿ sq_stringþÿÿÿAVF Q wave widthÿ sq_stringþÿÿÿAVF R wave widthÿ sq_stringþÿÿÿAVF S wave widthÿ sq_stringþÿÿÿAVF R' wave widthÿ sq_stringþÿÿÿAVF S' wave widthÿ sq_stringþÿÿÿAVF N of intrinsic deflectionsÿ sq_stringþÿÿÿAVF Existence of ragged R waveÿ sq_stringþÿÿÿ.AVF Existence of diphasic derivation of R waveÿ sq_stringþÿÿÿAVF Existence of ragged P waveÿ sq_stringþÿÿÿ.AVF Existence of diphasic derivation of P waveÿ sq_stringþÿÿÿAVF Existence of ragged T waveÿ sq_stringþÿÿÿ.AVF Existence of diphasic derivation of T waveÿ sq_stringþÿÿÿV1 Q wave widthÿ sq_stringþÿÿÿV1 R wave widthÿ sq_stringþÿÿÿV1 S wave widthÿ sq_stringþÿÿÿV1 R' wave widthÿ sq_stringþÿÿÿV1 S' wave widthÿ sq_stringþÿÿÿV1 N of intrinsic deflectionsÿ sq_stringþÿÿÿV1 Existence of ragged R waveÿ sq_stringþÿÿÿ-V1 Existence of diphasic derivation of R waveÿ sq_stringþÿÿÿV1 Existence of ragged P waveÿ sq_stringþÿÿÿ-V1 Existence of diphasic derivation of P waveÿ sq_stringþÿÿÿV1 Existence of ragged T waveÿ sq_stringþÿÿÿ-V1 Existence of diphasic derivation of T waveÿ sq_stringþÿÿÿV2 Q wave widthÿ sq_stringþÿÿÿV2 R wave widthÿ sq_stringþÿÿÿV2 S wave widthÿ sq_stringþÿÿÿV2 R' wave widthÿ sq_stringþÿÿÿV2 S' wave widthÿ sq_stringþÿÿÿV2 N of intrinsic deflectionsÿ sq_stringþÿÿÿV2 Existence of ragged R waveÿ sq_stringþÿÿÿ-V2 Existence of diphasic derivation of R waveÿ sq_stringþÿÿÿV2 Existence of ragged P waveÿ sq_stringþÿÿÿ-V2 Existence of diphasic derivation of P waveÿ sq_stringþÿÿÿV2 Existence of ragged T waveÿ sq_stringþÿÿÿ-V2 Existence of diphasic derivation of T waveÿ sq_stringþÿÿÿV3 Q wave widthÿ sq_stringþÿÿÿV3 R wave widthÿ sq_stringþÿÿÿV3 S wave widthÿ sq_stringþÿÿÿV3 R' wave widthÿ sq_stringþÿÿÿV3 S' wave widthÿ sq_stringþÿÿÿV3 N of intrinsic deflectionsÿ sq_stringþÿÿÿV3 Existence of ragged R waveÿ sq_stringþÿÿÿ-V3 Existence of diphasic derivation of R waveÿ sq_stringþÿÿÿV3 Existence of ragged P waveÿ sq_stringþÿÿÿ-V3 Existence of diphasic derivation of P waveÿ sq_stringþÿÿÿV3 Existence of ragged T waveÿ sq_stringþÿÿÿ-V3 Existence of diphasic derivation of T waveÿ sq_stringþÿÿÿV4 Q wave widthÿ sq_stringþÿÿÿV4 R wave widthÿ sq_stringþÿÿÿV4 S wave widthÿ sq_stringþÿÿÿV4 R' wave widthÿ sq_stringþÿÿÿV4 S' wave widthÿ sq_stringþÿÿÿV4 N of intrinsic deflectionsÿ sq_stringþÿÿÿV4 Existence of ragged R waveÿ sq_stringþÿÿÿ-V4 Existence of diphasic derivation of R waveÿ sq_stringþÿÿÿV4 Existence of ragged P waveÿ sq_stringþÿÿÿ-V4 Existence of diphasic derivation of P waveÿ sq_stringþÿÿÿV4 Existence of ragged T waveÿ sq_stringþÿÿÿ-V4 Existence of diphasic derivation of T waveÿ sq_stringþÿÿÿV5 Q wave widthÿ sq_stringþÿÿÿV5 R wave widthÿ sq_stringþÿÿÿV5 S wave widthÿ sq_stringþÿÿÿV5 R' wave widthÿ sq_stringþÿÿÿV5 S' wave widthÿ sq_stringþÿÿÿV5 N of intrinsic deflectionsÿ sq_stringþÿÿÿV5 Existence of ragged R waveÿ sq_stringþÿÿÿ-V5 Existence of diphasic derivation of R waveÿ sq_stringþÿÿÿV5 Existence of ragged P waveÿ sq_stringþÿÿÿ-V5 Existence of diphasic derivation of P waveÿ sq_stringþÿÿÿV5 Existence of ragged T waveÿ sq_stringþÿÿÿ-V5 Existence of diphasic derivation of T waveÿ sq_stringþÿÿÿV6 Q wave widthÿ sq_stringþÿÿÿV6 R wave widthÿ sq_stringþÿÿÿV6 S wave widthÿ sq_stringþÿÿÿV6 R' wave widthÿ sq_stringþÿÿÿV6 S' wave widthÿ sq_stringþÿÿÿV6 N of intrinsic deflectionsÿ sq_stringþÿÿÿV6 Existence of ragged R waveÿ sq_stringþÿÿÿ-V6 Existence of diphasic derivation of R waveÿ sq_stringþÿÿÿV6 Existence of ragged P waveÿ sq_stringþÿÿÿ-V6 Existence of diphasic derivation of P waveÿ sq_stringþÿÿÿV6 Existence of ragged T waveÿ sq_stringþÿÿÿ-V6 Existence of diphasic derivation of T waveÿ sq_stringþÿÿÿDI JJ wave amplitudeÿ sq_stringþÿÿÿDI Q wave amplitudeÿ sq_stringþÿÿÿDI R wave amplitudeÿ sq_stringþÿÿÿDI S wave amplitudeÿ sq_stringþÿÿÿDI R' wave amplitudeÿ sq_stringþÿÿÿDI S' wave amplitudeÿ sq_stringþÿÿÿDI P wave amplitudeÿ sq_stringþÿÿÿDI T wave amplitudeÿ sq_stringþÿÿÿDI QRSAÿ sq_stringþÿÿÿDI QRSTAÿ sq_stringþÿÿÿDII JJ wave amplitudeÿ sq_stringþÿÿÿDII Q wave amplitudeÿ sq_stringþÿÿÿDII R wave amplitudeÿ sq_stringþÿÿÿDII S wave amplitudeÿ sq_stringþÿÿÿDII R' wave amplitudeÿ sq_stringþÿÿÿDII S' wave amplitudeÿ sq_stringþÿÿÿDII P wave amplitudeÿ sq_stringþÿÿÿDII T wave amplitudeÿ sq_stringþÿÿÿDII QRSAÿ sq_stringþÿÿÿ DII QRSTAÿ sq_stringþÿÿÿDIII JJ wave amplitudeÿ sq_stringþÿÿÿDIII Q wave amplitudeÿ sq_stringþÿÿÿDIII R wave amplitudeÿ sq_stringþÿÿÿDIII S wave amplitudeÿ sq_stringþÿÿÿDIII R' wave amplitudeÿ sq_stringþÿÿÿDIII S' wave amplitudeÿ sq_stringþÿÿÿDIII P wave amplitudeÿ sq_stringþÿÿÿDIII T wave amplitudeÿ sq_stringþÿÿÿ DIII QRSAÿ sq_stringþÿÿÿ DIII QRSTAÿ sq_stringþÿÿÿAVR JJ wave amplitudeÿ sq_stringþÿÿÿAVR Q wave amplitudeÿ sq_stringþÿÿÿAVR R wave amplitudeÿ sq_stringþÿÿÿAVR S wave amplitudeÿ sq_stringþÿÿÿAVR R' wave amplitudeÿ sq_stringþÿÿÿAVR S' wave amplitudeÿ sq_stringþÿÿÿAVR P wave amplitudeÿ sq_stringþÿÿÿAVR T wave amplitudeÿ sq_stringþÿÿÿAVR QRSAÿ sq_stringþÿÿÿ AVR QRSTAÿ sq_stringþÿÿÿAVL JJ wave amplitudeÿ sq_stringþÿÿÿAVL Q wave amplitudeÿ sq_stringþÿÿÿAVL R wave amplitudeÿ sq_stringþÿÿÿAVL S wave amplitudeÿ sq_stringþÿÿÿAVL R' wave amplitudeÿ sq_stringþÿÿÿAVL S' wave amplitudeÿ sq_stringþÿÿÿAVL P wave amplitudeÿ sq_stringþÿÿÿAVL T wave amplitudeÿ sq_stringþÿÿÿAVL QRSAÿ sq_stringþÿÿÿ AVL QRSTAÿ sq_stringþÿÿÿAVF JJ wave amplitudeÿ sq_stringþÿÿÿAVF Q wave amplitudeÿ sq_stringþÿÿÿAVF R wave amplitudeÿ sq_stringþÿÿÿAVF S wave amplitudeÿ sq_stringþÿÿÿAVF R' wave amplitudeÿ sq_stringþÿÿÿAVF S' wave amplitudeÿ sq_stringþÿÿÿAVF P wave amplitudeÿ sq_stringþÿÿÿAVF T wave amplitudeÿ sq_stringþÿÿÿAVF QRSAÿ sq_stringþÿÿÿ AVF QRSTAÿ sq_stringþÿÿÿV1 JJ wave amplitudeÿ sq_stringþÿÿÿV1 Q wave amplitudeÿ sq_stringþÿÿÿV1 R wave amplitudeÿ sq_stringþÿÿÿV1 S wave amplitudeÿ sq_stringþÿÿÿV1 R' wave amplitudeÿ sq_stringþÿÿÿV1 S' wave amplitudeÿ sq_stringþÿÿÿV1 P wave amplitudeÿ sq_stringþÿÿÿV1 T wave amplitudeÿ sq_stringþÿÿÿV1 QRSAÿ sq_stringþÿÿÿV1 QRSTAÿ sq_stringþÿÿÿV2 JJ wave amplitudeÿ sq_stringþÿÿÿV2 Q wave amplitudeÿ sq_stringþÿÿÿV2 R wave amplitudeÿ sq_stringþÿÿÿV2 S wave amplitudeÿ sq_stringþÿÿÿV2 R' wave amplitudeÿ sq_stringþÿÿÿV2 S' wave amplitudeÿ sq_stringþÿÿÿV2 P wave amplitudeÿ sq_stringþÿÿÿV2 T wave amplitudeÿ sq_stringþÿÿÿV2 QRSAÿ sq_stringþÿÿÿV2 QRSTAÿ sq_stringþÿÿÿV3 JJ wave amplitudeÿ sq_stringþÿÿÿV3 Q wave amplitudeÿ sq_stringþÿÿÿV3 R wave amplitudeÿ sq_stringþÿÿÿV3 S wave amplitudeÿ sq_stringþÿÿÿV3 R' wave amplitudeÿ sq_stringþÿÿÿV3 S' wave amplitudeÿ sq_stringþÿÿÿV3 P wave amplitudeÿ sq_stringþÿÿÿV3 T wave amplitudeÿ sq_stringþÿÿÿV3 QRSAÿ sq_stringþÿÿÿV3 QRSTAÿ sq_stringþÿÿÿV4 JJ wave amplitudeÿ sq_stringþÿÿÿV4 Q wave amplitudeÿ sq_stringþÿÿÿV4 R wave amplitudeÿ sq_stringþÿÿÿV4 S wave amplitudeÿ sq_stringþÿÿÿV4 R' wave amplitudeÿ sq_stringþÿÿÿV4 S' wave amplitudeÿ sq_stringþÿÿÿV4 P wave amplitudeÿ sq_stringþÿÿÿV4 T wave amplitudeÿ sq_stringþÿÿÿV4 QRSAÿ sq_stringþÿÿÿV4 QRSTAÿ sq_stringþÿÿÿV5 JJ wave amplitudeÿ sq_stringþÿÿÿV5 Q wave amplitudeÿ sq_stringþÿÿÿV5 R wave amplitudeÿ sq_stringþÿÿÿV5 S wave amplitudeÿ sq_stringþÿÿÿV5 R' wave amplitudeÿ sq_stringþÿÿÿV5 S' wave amplitudeÿ sq_stringþÿÿÿV5 P wave amplitudeÿ sq_stringþÿÿÿV5 T wave amplitudeÿ sq_stringþÿÿÿV5 QRSAÿ sq_stringþÿÿÿV5 QRSTAÿ sq_stringþÿÿÿV6 JJ wave amplitudeÿ sq_stringþÿÿÿV6 Q wave amplitudeÿ sq_stringþÿÿÿV6 R wave amplitudeÿ sq_stringþÿÿÿV6 S wave amplitudeÿ sq_stringþÿÿÿV6 R' wave amplitudeÿ sq_stringþÿÿÿV6 S' wave amplitudeÿ sq_stringþÿÿÿV6 P wave amplitudeÿ sq_stringþÿÿÿV6 T wave amplitudeÿ sq_stringþÿÿÿV6 QRSAÿ sq_stringþÿÿÿV6 QRSTAXÿmatrixþÿÿÿÄÀR@L@K@€K@ÀR@*@D@€H@F@I@O@€F@K@>@F@€G@€G@G@@R@€L@<@€F@B@€L@D@F@A@?@L@€I@€J@M@I@J@@Q@F@I@€A@O@€F@€E@D@>@A@D@ÀR@@Q@>@€D@A@ÀR@<@€C@8@J@R@C@E@G@@@ð?€A@B@;@H@F@K@:@€F@P@€B@€B@?@G@€A@A@€F@€B@L@ÀQ@€B@€L@ÀS@€I@€O@ÀR@3@D@Q@@R@J@P@€O@O@9@€K@€@@€B@J@B@€I@€C@3@;@B@€O@G@€L@1@I@ÀQ@€E@€@@"@=@€C@€R@<@8@M@@Q@I@R@ÀQ@€B@9@M@€N@€J@G@L@H@€C@G@€D@D@€G@€G@€A@€I@€A@ð?€H@?@A@B@2@A@€C@1@H@@@B@A@;@€F@€F@N@€M@G@E@E@K@8@D@9@Q@3@@R@D@F@I@@@L@€K@K@P@;@€P@L@€O@2@F@€L@@P@€N@€G@O@6@€P@€H@N@@P@B@F@;@5@G@*@M@€Q@P@I@€A@€@@G@€J@O@E@€G@&@2@€O@€J@€H@>@1@€P@I@@S@€G@E@L@ÀT@E@ÀP@€B@€E@B@€H@R@T@6@€L@G@€F@€E@B@L@C@€E@O@G@€H@R@N@H@J@F@G@.@M@G@€M@€P@Q@€J@€F@H@F@€@@€A@€C@C@A@€A@M@I@€E@I@R@H@€N@€O@€N@P@@@M@H@?@€D@€H@€L@€A@@R@@P@€@@€A@€L@I@N@€F@D@€K@F@€@@€M@@(@€I@€M@€O@€R@ÀP@?@,@€B@€J@O@D@€D@T@@Q@€Q@P@€L@J@€K@€B@@:@€M@K@C@H@M@ÀP@P@€M@€M@@P@E@€B@3@€G@?@€@@€C@E@K@F@H@R@B@@@4@@T@A@O@P@€J@H@:@€F@€G@€G@L@D@K@O@D@€G@€L@C@G@€E@€M@P@>@Q@G@H@R@.@€Q@ÀP@K@ÀP@ @€P@€L@B@€G@€A@ÀR@€S@€J@F@€Q@€P@B@€I@0@€M@€N@€F@>@€G@ÀQ@€K@"@€P@@O@5@S@9@M@€P@€L@P@€D@€J@ÀP@>@;@@P@I@;@M@€O@=@€I@@B@€A@M@P@ @&@€G@&@€Q@4@€C@@@€A@€B@€H@€B@€B@@P@€D@=@€F@4@€J@€B@B@@@€S@ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?Àg@ d@€e@àe@Àg@ e@d@@d@e@àd@@e@ d@€e@@e@d@Àb@`e@Àc@ d@Àd@d@ e@ c@ d@ c@ e@@e@d@€d@d@àe@`d@d@`c@f@d@€e@€d@`d@àe@ c@d@d@ d@d@€c@d@Àc@àc@àc@€c@€c@€e@`d@ d@d@d@e@ d@d@€[@d@Àc@@d@`c@`c@d@d@Àb@€c@`c@Àf@ d@`d@Àc@ d@ f@`e@ d@€c@€c@ f@Àb@ d@d@àc@ d@@d@ d@Àc@`c@`c@`d@ d@e@ g@Àb@ c@`c@d@€c@`c@ c@@e@d@àe@ d@ c@@e@g@@d@f@`d@€`@€e@d@€e@àc@@d@@e@@c@€d@@e@@e@àc@@d@@g@ g@`c@`d@ d@ d@ c@ f@`c@€e@e@@d@@d@d@@e@`ˆ@Àd@Àb@àe@e@àe@`c@d@d@€c@ e@€e@àd@ d@ e@@d@d@ d@ c@@e@d@@e@f@ d@@d@e@€c@d@d@@f@e@`d@e@€c@d@d@`c@àd@@d@€c@àe@`f@àe@`c@@e@`d@@e@ d@€c@Àd@d@€f@€e@ e@`c@Àd@e@ `@`c@d@d@àc@d@e@@d@@f@f@ c@f@_@€f@Àe@ e@d@d@€c@d@@e@ d@ d@@f@ e@d@ d@`e@e@€e@@e@@e@`e@ e@`d@€d@ d@ e@f@ b@@e@ d@d@`c@ f@Àd@ d@`c@ c@ c@d@ d@@e@àe@@e@d@d@€d@ d@@e@@f@@e@ d@d@d@@e@Àb@`c@d@Àe@`d@d@d@Àc@`e@d@@c@€c@ c@Àb@d@e@àe@@e@àe@ d@ d@àe@d@`c@àe@`d@Àc@@e@@f@ e@d@d@d@@`@ d@@e@ d@€d@@e@f@d@àe@àe@`e@@e@f@€f@d@d@ d@àd@@e@ƒ@Àe@@e@@e@@Z@€f@ d@€d@@e@@e@Àd@e@€c@`c@ d@`e@€g@àe@ d@d@ d@ d@d@€e@@e@ c@@d@ d@`e@Àc@@g@ d@ d@@f@ e@€c@d@@e@ e@àe@àe@@e@Àb@d@ c@@e@`d@@e@€e@@f@ d@d@@e@ f@@b@ d@Àf@@c@`d@d@`d@d@d@^@e@ d@ g@ e@ d@@d@c@€c@@e@@f@d@f@ d@d@Àe@€c@Àc@d@àe@ d@d@^@`c@À_@ d@€c@Àb@d@€b@d@€e@d@@e@d@ d@àd@àd@€e@`d@d@d@àd@@d@@g@À]@`e@d@d@d@@`@@a@Àd@€a@àd@@f@€d@€d@`c@àe@e@f@d@d@@c@Àd@àe@ c@d@Àg@Àd@`c@d@T@P@ÀW@€W@T@€I@J@K@L@ÀP@R@€U@M@@R@V@H@€M@M@€O@R@M@ÀP@ÀR@€M@€K@T@@R@K@@P@ÀT@@U@Q@@R@€Q@ÀR@€F@T@€W@N@T@ÀQ@ÀR@O@€N@€I@€K@ÀQ@€L@€K@Q@€J@J@S@€J@T@€Q@€O@ÀS@€P@M@$@€I@M@€J@€K@@P@€O@@P@ÀQ@€T@N@€V@@R@Y@N@@P@R@@U@@R@L@J@Q@N@S@T@€M@I@@P@€Q@R@Z@V@O@€Q@€K@@Z@€K@O@Z@€Q@N@€M@D@€P@@P@T@€K@€N@€N@X@R@€V@ÀR@€@@@Q@€F@€R@L@ÀR@ÀR@€M@ÀR@€R@J@L@€S@2@ÀW@€O@€U@K@€Q@L@€V@L@N@P@€R@Q@T@T@@@R@P@N@ÀR@N@K@O@K@O@ÀR@€V@N@€I@@R@€N@ÀT@€Q@€Q@T@Q@€S@R@€J@H@€M@€G@T@€Q@@V@T@@P@S@€S@P@ÀP@€K@€Q@P@€Q@€P@@U@T@Q@€S@€K@@U@J@T@O@€N@ÀS@@T@ÀQ@€F@Q@N@>@N@€V@T@€Q@€I@@Z@@P@U@@R@@P@ÀW@9@€O@ÀS@f@T@@P@D@N@T@@S@ÀP@ÀR@U@€K@K@ÀP@€T@€V@@U@ÀS@€R@@U@I@P@ÀT@€Q@R@H@T@€X@K@€S@@U@ÀU@Q@@P@ÀW@€K@P@N@€J@€S@T@Q@_@€J@P@€R@T@ÀR@@U@ÀP@€R@ÀW@€F@€K@O@€V@@U@€Z@€Q@J@€Q@€O@€O@@R@M@€S@€N@T@@X@T@@T@ÀW@M@@P@@Q@M@€Q@M@ÀR@€R@S@ÀR@@P@P@R@>@€G@€T@@P@K@ÀP@T@€K@€M@ÀQ@ÀS@€[@€R@V@€Q@N@€Q@R@@P@$@€T@R@€V@(@@X@€W@ÀT@@Q@€R@€Q@@P@S@R@€R@@U@ÀV@V@@P@@P@L@M@€Q@ÀT@€Q@N@U@P@S@€O@€P@N@N@@V@€O@@P@H@€E@T@€T@€W@€T@H@€Q@@P@Q@T@€P@€N@€U@T@J@€Q@€S@O@@S@@S@€K@C@U@€S@ÀP@Q@<@€T@€R@ÀW@T@€M@€R@G@€Q@U@T@€J@@P@@U@N@€R@@S@@P@J@€W@€T@€Q@9@O@6@€Q@F@K@L@@P@€Q@€R@N@ÀP@N@€J@@S@H@€P@@P@E@€V@Q@€L@ÀW@5@@W@€J@@P@€O@8@=@L@E@N@@P@O@€L@€O@€T@€P@R@I@I@ÀR@€O@ÀR@€L@€Q@@U@Q@€K@€Q@ÀV@@T@@a@Y@V@Y@@S@€S@U@@V@€Y@@S@€S@ÀV@@S@ÀR@€T@€Q@ÀV@€T@ÀT@€V@ÀQ@ÀR@€T@@[@€W@ÀW@€V@X@@U@ÀQ@ÀR@€S@€T@@Q@ÀY@@U@T@€W@T@ÀS@W@U@€U@@R@ÀR@@R@€S@T@@T@ÀT@ÀY@W@V@@S@ÀS@€V@ÀV@ÀT@T@€S@S@€T@@T@T@€T@ÀQ@@R@ÀU@S@@Y@@W@€V@ÀU@V@€W@€W@€V@€S@ÀT@W@@W@Y@ÀT@`d@X@€\@@e@ a@U@€T@ÀS@R@@X@ÀU@€Y@@U@U@€S@ÀS@ÀV@€X@€U@R@@X@€T@€S@@X@€W@€T@€S@ÀS@ÀU@@W@ÀR@€Z@X@V@€V@€R@@U@€V@ÀQ@ÀR@@S@ÀU@ÀW@€R@ÀX@@V@ÀT@€S@Y@T@ÀU@ÀS@€Y@T@@U@@T@@U@ÀW@€T@@X@W@€Y@€U@T@@U@@U@À[@@V@€O@€Q@W@ÀR@@W@ÀT@U@ÀV@@T@@\@W@@T@€R@€S@T@€T@ÀW@€Z@ÀW@Y@€V@V@ÀQ@€S@@S@ÀU@ÀS@@U@ÀV@W@€X@Q@U@W@€[@[@ c@@R@@T@ÀU@ÀU@ÀW@@R@ÀW@Y@X@@X@S@O@€U@ÀQ@ÀX@Q@€V@@b@€U@T@€V@@W@ÀV@À[@@V@ÀS@@S@@R@W@€X@€S@ÀU@€X@U@€T@€V@@U@ÀQ@U@@U@@S@@W@€R@@V@@V@ÀY@@U@V@@T@U@X@€V@€X@€S@@W@T@@U@ÀP@€R@@Q@@Z@ÀW@ÀU@[@ÀX@ÀS@€U@€T@ÀV@@W@T@U@€V@@T@€V@€S@€X@@T@€V@@U@ÀS@€N@€V@ÀS@ÀT@U@€S@€U@R@ÀU@ÀV@€Y@T@€T@Z@@U@ÀT@@T@€W@U@ÀV@ÀX@@Y@€`@ÀS@ÀY@@X@``@ÀV@€V@@^@€S@U@@X@€W@X@ÀV@T@@X@W@€V@€V@À^@@Z@@U@@S@ÀT@W@@U@X@@Q@V@U@ÀZ@ÀV@€R@@V@@[@T@ÀT@W@Y@@\@@Z@ÀX@U@€R@S@ÀU@ÀU@X@V@T@@U@€V@€R@ÀW@€U@€R@ÀW@€R@@R@Z@W@@Z@@X@@Y@ÀT@U@€X@@T@@W@U@€Y@€W@ÀV@Y@W@€W@€X@@b@U@@S@€T@U@S@€Z@€Q@ÀU@€]@€U@€T@X@@V@@W@€T@@X@€U@€g@@W@R@ÀZ@@T@S@ÀT@`b@€T@€[@[@€T@@W@ÀW@T@ g@@R@€S@ÀQ@€U@@U@S@@W@ÀU@X@€U@€S@ÀU@T@€X@ÀR@€S@€T@€W@ÀT@€W@€a@ÀU@€K@ `@ÀT@@S@À^@ÀS@V@T@V@ÀS@@S@ÀU@V@€W@V@€R@@U@V@@T@ÀV@@T@T@Y@[@@W@ÀS@ h@Àe@`d@@i@ f@àd@ `@€]@@`@à`@àa@`c@€f@Àc@€`@ b@^@@c@ f@`o@€^@€`@ c@€a@`@@g@ d@€d@`b@ c@a@@_@ a@ b@@f@Àa@i@ g@`d@@d@@c@@c@c@ f@àc@€c@ a@€l@à`@€f@à`@`b@ c@@i@Àa@ b@f@€^@@^@`b@a@e@@]@Àc@Àb@@c@`e@Àd@`r@ e@`c@@a@àc@€d@€b@`b@`h@`b@àc@@f@ b@€f@`b@àb@àg@h@€d@ k@@h@d@ e@ `@@r@àa@ b@€g@€]@ b@`e@f@`h@Àa@`b@€`@d@`r@`i@@e@àc@ `@`d@ d@ c@Àb@ c@àc@Àa@€f@Àc@@d@ a@Àd@ h@ d@`d@@`@@b@`c@0p@À`@@b@c@€_@€c@àd@à`@ d@ c@`h@àd@@h@à`@À_@À^@ a@f@`e@€d@€d@Àa@_@€^@@^@@h@ c@`g@`b@k@ c@d@À\@àc@ a@a@`d@àf@àc@ b@ c@€b@@d@ i@ c@€e@ d@`a@n@ c@i@@f@`g@ c@g@€c@€`@àg@ a@ b@`a@€c@àg@ g@f@`@`g@d@e@_@a@àe@f@@a@@[@e@ d@d@àb@Àd@e@``@€Z@@f@@\@a@ e@@g@ c@g@ h@Àf@`a@`@€e@àf@ `@`c@€f@`c@Àd@@e@@d@a@ f@€e@ f@ f@b@ f@ c@ `@ c@ b@ b@Àb@€f@@c@@e@ c@`d@l@`d@ b@`c@€c@ e@à`@ c@Àc@@Z@ e@àb@c@t@Àe@ a@Àc@`d@Àe@`b@€i@€h@Àh@€b@`d@ `@ d@Àe@g@€b@ b@ a@€d@ d@g@@e@ b@ b@ d@`c@€a@àe@b@_@ a@ g@ e@k@ i@ h@@f@@c@d@€_@c@Àh@@f@`c@ h@c@Àf@€d@d@Àc@àe@ c@ j@Àc@`a@ j@c@€`@Àb@àh@@`@d@``@@c@@b@@c@e@€c@g@À^@àg@@f@ f@d@ÀY@Àh@€h@àe@àf@d@ e@Àe@àb@€_@ f@ d@€c@€g@`@@d@€\@i@ e@€h@€a@à`@ d@ e@Àf@Àc@€_@€d@ i@àc@Àa@g@f@@^@ a@@_@€]@€d@€b@Àc@`€@€d@`b@€^@@c@ e@Àf@`c@€]@€g@€i@ f@àb@@g@@d@€a@d@ c@``@àg@ `@€f@€d@c@àh@àb@ c@àe@€d@`i@ c@Àb@`d@€b@@_@ b@ b@À^@ b@`c@`c@b@Àa@@b@@e@ c@àa@àa@ c@àa@À`@àb@àh@ a@f@€Z@À_@0w@y@ x@Àw@€v@t@�w@€w@ v@ðw@y@Pw@àw@0v@ðx@àu@°u@v@€x@ðx@ðw@u@Àv@`y@@x@àw@Pw@py@@z@y@€y@0u@v@w@Pv@Pv@àv@x@ v@y@ðw@àu@ u@ðw@ðv@àu@ t@w@Ðz@°w@ x@pv@@v@ w@w@€x@€w@Àv@@w@ x@ðq@@t@Pw@`w@àw@°v@ y@àu@€y@Àw@x@@v@�v@ z@ðw@0w@ðz@�w@@x@°t@ t@Ðu@�v@pv@Ðw@ðz@Pw@ðw@0z@�x@@z@`u@0w@€t@ u@`y@Àv@Ðx@ |@ n@�w@0x@pu@ày@0w@ w@ðy@w@v@€v@àv@w@Àv@ðt@`x@ z@°y@@u@v@àw@Àw@0u@ x@Pt@0u@ w@@w@@u@ x@�x@°u@ z@àu@Àw@ v@v@àx@Pv@Àv@0v@u@ m@ðw@ v@0u@0x@°w@w@pw@0t@x@°u@Pv@Àx@Ðy@ w@Pv@ðw@�x@ðw@ w@u@ày@u@°u@�w@@v@`u@°u@°t@Àw@`v@�w@ w@Àw@�y@ðu@�u@ x@ p@ðy@Àv@p{@s@°u@@u@ðv@ z@`y@Pz@Ðu@ v@€x@°u@ x@Àt@�w@Ðy@@v@`x@Àx@€w@`s@ðv@Ðu@px@0v@`{@`w@w@Ðu@t@ y@0u@�t@€v@}@Àv@t@pv@`u@@v@@w@`v@€w@ðu@°w@@v@°v@�w@ t@`v@ w@y@Ày@àu@ðt@Px@v@@v@v@�r@py@ t@v@w@w@�s@ t@`t@ v@€w@ u@@x@�t@Àv@@s@Pw@°t@Àu@Àu@@t@Px@u@ v@àt@`v@ v@Pv@ðw@w@Ðx@Àw@pz@�v@pw@0w@ v@€v@ w@ y@àu@Àv@ðx@@w@Pw@Ðw@ x@àw@ w@0x@Ðw@0v@`x@àu@0w@°v@�w@pv@àw@�t@�x@À{@Pv@ {@@u@ t@°v@`r@ v@Pu@@y@€{@�t@v@pv@m@0v@ u@àu@n@@y@Àx@pv@Pv@Ðw@0v@`w@pw@€u@@x@@x@`v@ v@ðw@°w@x@Ðt@Pv@Ðu@€t@Àw@ v@ x@Àv@0x@ w@pv@àr@w@`w@pv@pv@°v@pt@Ðt@u@Ðt@ z@ r@@|@àw@ v@Ðz@y@0w@u@pv@Py@Pw@|@w@Pt@Ðw@@u@Ðx@Ðv@0u@ x@ðr@ x@Pr@ t@w@ðv@Pv@àw@Ðr@ |@ p@°w@py@Pv@�t@�w@Ð@u@ z@Pu@w@0x@°u@ z@Àq@Ðw@°v@@x@€w@ðz@w@€w@Àx@w@ u@Àu@ v@pu@0u@ v@`w@Àq@p{@pv@ðv@`{@ v@@u@z@Àv@`v@�v@Ðw@ v@ r@€v@ðv@@u@px@Pv@ðw@Px@`w@°v@x@Pt@€w@°v@àw@�v@Ðv@ x@Àv@Àe@ b@ g@`f@ f@Àe@ `@ c@d@€c@€c@Àb@`d@ c@`d@ e@ e@€^@àe@Àc@ g@ f@ e@àa@ b@`h@l@e@Ðw@Ðr@€e@c@àf@€b@ `@ a@ d@Àe@Àd@àc@ a@d@Àc@b@`b@@a@€e@àa@@`@ b@ a@ e@@d@Àa@@\@d@ d@àe@ d@Àe@€c@`c@c@àd@ j@Àa@ a@ d@àb@c@€e@€d@€d@@d@@f@àd@Àc@Àa@@f@€b@ `@f@€`@ b@ b@@n@`b@Àk@€p@€n@ k@@a@`e@à`@`m@h@e@f@ h@c@€e@@]@€`@Àf@ a@@e@[@h@ a@`i@€b@àd@àd@àd@ a@`c@ d@c@€c@e@@c@ c@Àe@à`@ a@Ài@Àb@`a@Àb@Àb@àe@À`@`b@`f@€c@d@€e@à`@À`@àb@c@Àb@`d@€c@`d@àe@àd@ c@ c@àa@€e@`m@€e@`a@Àc@€f@àa@€a@ e@ f@ e@``@ h@ d@@c@]@€d@`n@€d@ a@`b@Àd@c@b@@a@`a@c@b@Àd@Àh@€e@àg@àd@@`@€d@àl@€^@Àh@@k@ o@@c@À_@ a@ài@ b@Àg@`e@`b@`e@ e@`c@a@€k@e@ n@`b@Àb@°q@àd@g@ j@€e@Àh@i@`d@€a@`b@b@€h@ b@@_@@i@ d@Àd@Àb@`p@Àf@``@g@àb@€a@ b@`d@@^@Àh@ o@€c@b@@c@àc@@e@ j@ f@€^@`a@l@@o@ a@ d@ d@àa@@i@`o@Àh@€e@€e@Àj@ p@àc@ l@a@@f@Àd@@c@ c@@`@@c@ h@àb@`d@€c@àe@Àc@@a@Àa@@b@c@@]@`c@ d@Ào@e@ e@@d@àc@`b@@_@ c@`b@àa@€g@Àg@`i@àc@ p@€`@`o@k@c@k@d@àa@Àf@@e@À_@ l@`g@à`@g@ d@ i@ c@àf@ b@c@ a@`@€d@@a@ e@ `@€g@a@ n@€e@Àa@b@@k@€f@�r@`d@Àf@€k@ f@g@@r@Àa@d@@f@Àb@@e@Àc@`e@€b@@e@ e@ d@@b@À_@€c@@a@o@ c@àb@@k@`e@ l@ l@ d@`d@d@ p@ a@`d@ b@@d@c@ n@ m@€c@q@@b@`m@`e@àn@`d@€h@àa@c@€d@`n@`g@àc@`c@ e@Àa@l@ c@Pp@`c@d@Àf@€b@ a@À[@pt@Àe@@c@e@c@àe@€c@@f@À^@@e@@c@@e@g@`f@ c@ào@ e@@i@àc@€_@e@€d@ b@ a@ d@€h@`a@@c@`e@@l@ f@@d@@p@^@àc@ k@ e@€l@`@`d@ c@€b@ a@`f@c@€e@@b@@b@€`@@k@d@Àd@@c@ i@@h@@k@@a@@^@€C@€Y@àa@ÀY@ÀV@@S@€Q@€O@@R@ÀT@@P@@T@Z@€W@@P@€N@J@ÀT@ÀS@àf@€S@€T@W@€T@N@@_@ÀT@ÀX@€T@ÀV@S@€O@ÀT@@Y@T@€W@€R@ÀZ@€Z@U@€W@X@ÀQ@@\@ÀX@@Z@@S@€W@@X@€Z@T@T@Q@@X@V@ÀS@ÀS@K@ÀP@ÀR@T@€S@€O@R@ÀU@@T@@T@€Y@Y@àa@[@€]@Q@W@@T@@T@W@€Y@X@@]@T@ÀW@Q@@X@€Y@€X@]@@Z@@]@€_@Y@@U@X@àe@€T@V@@V@Q@ÀT@@W@b@T@ÀV@@W@@U@@V@@f@€Y@@X@@P@N@€V@X@€V@X@€\@X@@V@Y@ÀY@X@U@X@€`@@X@@\@€Q@ÀT@Z@ÀW@M@@V@ÀV@ÀT@]@À_@ÀS@€Z@W@€Y@€S@ÀT@@R@€S@ÀP@ÀU@Z@€W@ÀP@U@€I@O@R@€U@@X@€U@€R@X@@e@S@@T@ÀP@ÀT@W@€T@@W@€W@X@@Y@V@@W@@X@ b@ÀV@@W@€T@ÀU@À^@€S@À[@€X@€Y@€W@€\@Q@P@ÀX@ÀV@ÀS@@W@@X@@V@@W@ a@ÀP@@^@Z@U@Q@@T@€\@[@ÀU@€N@@W@€X@@Y@€U@Y@@Y@@S@€L@ÀX@M@ÀU@€U@@\@€T@X@@]@@Z@€T@S@Z@€R@ÀQ@Y@[@ÀR@@[@€Z@Y@@U@U@ÀY@^@€X@T@Y@K@ÀS@€V@€T@W@X@@U@ÀW@€Z@ÀU@ÀU@€^@Z@ÀX@Y@@Z@Y@€Q@[@€\@€Q@€Y@P@@X@`c@R@ÀS@X@Z@ÀT@ÀS@T@R@àa@À[@€]@@Y@@]@àa@ÀT@@V@Y@@R@@X@W@À\@ÀY@ÀV@€Y@€T@Y@V@€S@E@Y@€P@ÀU@ÀZ@ÀT@€U@À[@À[@R@@Q@Y@N@@T@@W@€\@P@ÀX@Y@À_@€S@ÀU@€Z@@Z@€Z@@Z@Z@ÀV@ÀZ@€]@T@@U@Y@ÀQ@@V@@S@€\@€S@@U@€U@€U@@Z@@T@@Z@@]@Z@À\@€B@@P@€i@À\@`a@€]@U@P@€U@]@Y@R@€U@@X@Q@€X@@Q@ÀW@@P@]@€S@Z@R@@T@[@€T@€[@À\@T@À[@À\@€Y@ÀV@€V@À[@€K@ÀS@ÀR@€T@ÀU@€U@@[@ i@@U@Q@€O@€W@ÀX@ÀZ@ÀY@€P@Z@R@@W@ÀU@Z@€Z@€W@ÀR@Y@S@[@€T@\@@V@X@ f@@W@@U@Y@@Q@€Z@@T@X@€Y@W@€V@€Q@T@@Y@@T@@W@ÀQ@ÀW@€T@V@R@À\@@V@ÀR@U@\@€R@ÀT@T@@]@@R@]@€O@€S@0À9@X@<@0ÀÀZ@@S@ÀP@€N@@U@R@(@8ÀQ@G@B@@S@€L@@R@(ÀI@@T@O@@J@AÀ€V@$@ À€BÀJÀ*@C@"@€H@O@K@H@À€LÀ€J@ÀP@K@€M@2À2À2@>@€O@,Àð?A@&@P@M@>@A@&ÀE@@T@€_@D@€R@€P@ÀR@L@9@€P@P@BÀA@€@@€O@8À€N@V@]@>@V@O@N@Q@F@€KÀ€T@L@Q@P@O@,@;@À"@*À aÀ3@€@@8@6@:@$À@R@ e@€E@L@Àð¿€B@€V@:@(À€D@M@€S@O@(@€K@0À5ÀJ@.@2@GÀ:@N@L@ð¿€C@=@ÀU@H@€P@€T@€M@@T@M@€K@N@B@ÀQ@V@2À€N@@ÀÀV@7@N@R@€S@8@IÀ€H@€Q@9@I@€W@D@,À@P@ð¿I@&@ÀP@L@€IÀÀ€I@&@ÀIÀ<@=@ÀS@ÀR@€P@&ÀÀT@4@4@€E@@ÀDÀS@O@8@4ÀMÀS@6À€G@A@@R@ÀV@(À€Y@€@@ð¿@ÀF@€W@@T@.À0@€P@@R@T@À[@0@€U@€A@@€J@;@K@ÀS@9@9ÀR@€A@S@O@N@,@@@P@€@@1@€P@O@€S@ð?S@€Q@7@D@€R@@4@€Q@€K@$À€M@;@3À5ÀCÀÀ€OÀBÀHÀ€G@ @ÀU@N@B@À$ÀA@ÀQ@€K@€G@@U@?À=À6@@S@@Q@L@€L@3@&@"@@P@D@€P@À€CÀ @3À:À€HÀ@Q@@R@G@0ÀI@F@FÀM@ÀP@ð¿`c@ @"@€P@ð¿;ÀHÀ€N@T@€G@ð?€C@ÀT@@@5@<@ÀdÀ*@Q@@_@E@R@.@@W@À]@@NÀD@@LÀ8@@Q@ð¿€N@ÀV@J@,@4@€[@P@€P@<@@U@I@€C@?@@Q@€R@M@&À\@@"@€@@H@@ÀOÀÀU@JÀ€J@ @€H@€J@H@"@O@$À*@AÀ€R@S@€K@ @€C@G@ð?@@$À@RÀ&@M@^@ `@W@J@@€T@F@ÀU@J@(À?ÀF@ÀS@P@@T@ÀU@?@C@€Y@€HÀ€@@€F@U@3@€eÀ€A@U@€J@9@ÀQ@€@@ÀO@€R@€R@ÀQ@@S@À€E@7@L@9@P@ÀDÀF@D@ÀcÀ=@ÀU@\@J@2ÀÀPÀ6ÀI@;@€P@ð?W@ÀP@Q@DÀ@P@8@ÀV@€E@€BÀ€U@@UÀK@<@*@€B@A@&@*@€P@€H@@@Q@A@ÀQ@€B@E@€I@4@€F@ÀR@€H@8À<@€C@€S@L@$@1@\@J@H@ c@€e@0@4@@@L@G@7@*@€A@@ À$@ÀH@J@@N@ @2ÀS@G@P@A@"@€F@ÀS@€@@,@>@8À€J@9@7@P@O@G@$@øÿ€F@€E@J@?@3@€F@3@I@Q@€dÀLÀQ@.@ÀJ@G@ÀU@ÀS@aÀàe@.@_ÀÀa@*Àøÿ$@"ÀJ@M@1@€G@À dÀ ÀN@@R@J@5@C@€Q@5@WÀàa@*@E@O@H@1@6@7@3@*@R@€L@€D@€L@@P@M@B@€O@N@€K@F@@S@F@8@@@G@€K@€J@@T@€@@B@€D@>@€I@4@J@(@C@$@E@=@@Q@2@6@$@D@9@ @$@€@@K@€E@ð¿P@€G@€L@€L@J@H@D@Z@@S@I@Q@€P@@R@,@3@3@9@àdÀ.ÀÀP@[@ÀRÀd@€\@6À^@À\À`b@C@7@L@€TÀ0@€H@L@,@€OÀ€A@øÿJ@€eÀA@€Y@€L@€B@`f@1@€U@ÀU@R@øÿJÀ€HÀB@Àe@;ÀÀW@À^ÀP@&@C@€N@@P@H@*@,@ @N@E@ a@N@€\@€O@€d@€H@Àa@øÿ@J@ fÀ@ÀYÀF@€ZÀ€G@ÀP@€P@<@€H@EÀ@]@À_@ÀR@B@dÀ€^@€M@€S@ @E@1@K@€@@>@€@@À(@9@€H@M@ÀQ@€J@0À,@2@€@@E@<@€U@5@(@:@€D@Y@8@I@ ÀÀR@ÀQ@H@5À@^À(@ÀeÀB@,@D@€IÀ"@4@@Z@]ÀB@À&@À]À€O@€@@*À@S@@9@€O@5@€F@ÀV@U@ÀW@0@ `@`dÀ€P@3@@R@ÀZ@€HÀ c@@E@€C@@ÀS@A@&@L@L@€P@@S@ÀC@€F@€D@B@2@J@.@O@P@€`À@\@€C@ b@ÀU@ÀX@@_@@P@bÀøÿX@E@@R@?@&@@d@€J@@T@1À`f@€C@øÿ@S@K@V@[@€D@€I@1@€_@SÀ7À.ÀJ@ÀÀU@"@ f@Àb@R@M@$@€D@ÀY@À\À€G@€J@ÀU@.@B@€D@€\@8@€D@ÀQ@ÀT@6@ `@À€I@ÀøÿB@ÀP@:ÀQÀa@€@@€N@€`ÀK@€L@€U@8@€A@*@Y@€K@1À€G@J@À[@2@B@€K@H@ b@ÀH@,@$ÀF@;@Q@E@@€P@3À=@ÀS@P@1À€Q@À€N@J@ÀR@ @€S@€Q@Q@€H@€D@N@€F@Q@@S@À€N@I@G@ÀP@€F@M@@Z@@c@N@€C@€D@ÀK@€N@@R@€J@ÀQ@€K@B@.@D@€Q@€C@@P@P@€N@N@€L@€Q@B@@S@N@€O@€K@€K@O@øÿB@€K@€N@€P@øÿN@@P@€Q@Q@,ÀO@O@.@€D@N@G@A@ÀP@?@€I@N@€B@H@ÀR@€R@€O@N@€P@L@€L@ÀR@R@N@^@2@€D@P@L@>@€S@2@G@€L@@€E@@Q@€J@M@P@@P@@@øÿI@øÿB@€F@€G@€K@À0@J@øÿ1@Q@O@N@€P@€J@ÀZ@@R@=@€C@P@J@J@N@D@@R@øÿÀQ@€R@6@€G@ÀT@€O@I@>@C@L@N@"À€N@L@@@€S@O@M@B@P@€L@ @L@€O@E@€E@M@G@€M@H@€N@N@À@Q@€G@I@€D@ÀS@@Q@;@øÿ&ÀÀP@øÿF@I@N@€O@"@€K@€E@I@@R@E@K@0@ÀS@E@øÿL@€N@@Q@€L@Q@@PÀ>@@1@€D@øÿ@P@N@€KÀB@€N@L@ÀR@@@S@O@*@ÀP@øÿ€E@øÿN@N@I@€O@M@ÀQ@€K@F@ÀN@€I@€M@€N@@R@€L@€G@@S@ÀQ@L@M@€Q@:@€I@øÿÀV@€R@I@R@H@ÀR@€Q@ÀR@Q@øÿ€P@€N@M@B@K@O@€C@ÀQ@€G@€P@@P@Q@;@€M@€A@A@@eÀ€F@ÀR@@@€I@€J@$À@P@øÿ:@O@€H@A@øÿI@€G@€L@S@€D@€H@Q@M@€E@€N@M@5@€K@øÿ€A@øÿ€F@N@€M@N@L@Q@€D@øÿ@S@øÿ€C@ ÀM@L@@P@IÀL@M@€C@K@€H@L@F@€R@€R@J@Q@J@€P@G@,ÀU@€R@Q@€Q@K@ÀP@€O@K@€Y@€G@€@@ÀQ@P@R@ÀP@€I@I@@øÿNÀ€R@>@€N@€P@€C@@Q@<@E@,@€A@ÀQ@H@>@&@€S@@S@<@Q@ ÀL@€K@J@€E@?@€M@€N@€B@L@R@M@F@€J@F@@WÀ?@9ÀÀP@H@@P@@R@€O@ÀN@>@J@K@E@=@f@€F@G@ÀS@C@€N@;ÀÀP@2@JÀ€J@€R@G@€R@A@€H@€M@øÿ€O@€R@€N@=@H@@Q@€O@N@0@,@N@ÀP@:@€U@R@S@€L@€I@@P@M@€H@K@€F@@@?@R@D@J@€NÀ6ÀJ@À?@€P@4@@V@@P@€I@€P@ÀQ@R@:@*À€O@D@D@ÀR@K@E@ð?€E@T@N@@E@@@S@>@ÀPÀ@À,@€A@5@€G@€J@>@€C@ð¿HÀB@€E@J@€L@(Àð¿,@4@Q@$À;@€@@"@€M@O@?@:@@€@@S@Z@>@Q@O@ÀP@?@€C@€L@M@@@@:@L@ ÀL@€R@`a@(@€T@K@€H@€M@F@:@€T@€O@€T@I@à`À€H@@ð¿"@*ÀI@€I@9@B@@>@ÀÀQ@_@G@€G@@(@<@W@€K@ÀD@O@O@€G@3@E@ð?À€L@;@;@$À€J@L@G@1@H@€C@@T@€G@K@€J@I@@R@L@S@€K@B@€O@J@0@H@F@ @@Q@.@I@I@€R@5@À?@M@9@C@€T@C@0@€K@ð¿M@€A@€O@L@;@F@€B@,@=ÀJ@€C@S@@O@€H@ÀÀN@€H@D@1@EÀ€R@€M@ÀR@&@@ÀÀP@$À3@;@€M@@T@$À€@@@.@5ÀC@X@€R@&@=@K@@T@ÀU@T@0@@Q@ À5@U@ð?ÀT@@U@€H@À€M@€H@€Q@€L@@@*@@@P@B@C@€N@@P@@R@€H@€P@@Y@4@:@@P@øÿ(@€P@€J@€EÀ€K@D@ð?*@<ÀPÀ4À @€C@CÀ€Y@€O@€H@ÀA@:@ÀP@G@F@€P@ ÀÀ9@Q@€O@€P@€K@@(@(@N@€D@€I@,@3À&@ÀÀ&À€M@€O@8@@P@€F@=ÀÀP@€M@2ÀN@$@4@À@À&ÀK@O@<@@6@R@@@(@Q@*À3@ÀP@€Y@E@ÀS@€Q@W@V@2@0À"@€@@€@@EÀ,À@]@€L@ÀS@€@@@P@€@@W@N@€M@?@€T@5@C@A@€J@€L@€E@@Z@4@:@8@@P@(À b@V@=À@P@C@€N@€J@€Q@1@ÀP@ @(@€PÀ@Q@€S@:@7@€C@F@E@G@ÀTÀ7@€K@Q@@_@€T@D@À@Q@@@ÀU@€D@€CÀ@ÀO@Q@€I@@R@ÀV@1À€E@S@3À7@D@P@?@Àd@C@€S@O@8@W@ð¿A@ÀO@N@@R@€K@€G@*@C@H@@T@4@M@@@@Q@B@€B@=ÀD@S@€I@€H@€@@@PÀÀG@€D@€M@@@S@M@€K@<À€K@9@T@E@;ÀÀS@€QÀ€E@€G@øÿøÿ7@øÿøÿøÿøÿøÿU@øÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿd@øÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿ@WÀøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿ@c@øÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿbÀ\Àøÿ@WÀ f@øÿøÿøÿ@VÀøÿøÿøÿøÿøÿàe@øÿøÿøÿøÿøÿøÿøÿøÿÀWÀøÿøÿøÿU@øÿøÿøÿøÿøÿøÿøÿ`dÀøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿ€@ÀøÿøÿøÿøÿB@øÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿ2Àøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿ€dÀøÿøÿøÿ[À@^Àøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿ@f@àdÀ d@øÿøÿøÿøÿøÿøÿøÿøÿ€CÀøÿøÿøÿ e@øÿøÿøÿøÿ cÀ€]À@]Àøÿøÿøÿ€LÀøÿøÿøÿ cÀøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿ€d@øÿ€`Àøÿ aÀøÿÀcÀøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿ€`@`a@ e@øÿ`fÀøÿøÿøÿÀbÀøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿ€U@øÿøÿøÿøÿøÿ€_Àøÿøÿøÿ@[Ààb@€@@øÿøÿÀUÀøÿøÿøÿøÿøÿøÿøÿ€^Àøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿ€\@øÿøÿ cÀeÀ€R@øÿøÿøÿÀ]ÀøÿøÿøÿøÿøÿÀV@øÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿ€dÀøÿÀ^@øÿb@@\Àøÿ@dÀøÿøÿøÿøÿøÿøÿ a@@ZÀÀdÀøÿ eÀøÿøÿøÿøÿøÿ]@øÿøÿøÿ c@øÿøÿøÿøÿøÿøÿøÿf@`@øÿøÿøÿøÿøÿ€bÀøÿøÿ@^@øÿøÿøÿøÿ*@O@øÿøÿøÿøÿøÿ_ÀøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿøÿÀY@øÿUÀøÿøÿøÿFÀøÿøÿøÿøÿøÿøÿøÿøÿøÿ€VÀøÿøÿøÿøÿøÿøÿøÿøÿU@ÀY@øÿ€O@€J@ÀR@ÀQ@øÿU@€Q@ÀP@P@€O@€Q@R@@R@L@R@S@ÀP@€Q@€P@€P@S@€P@@S@@Q@Q@€O@ÀT@ÀP@ÀS@ÀQ@€I@@T@@W@@Q@T@€O@€Q@O@ÀR@€J@€Q@€T@ÀV@P@ÀS@@V@€X@ÀP@€J@€S@@P@@R@@R@P@€M@€L@€Q@ÀQ@L@€P@Z@W@@P@€Q@ÀP@@R@K@€Q@L@@P@€J@@Q@T@N@€P@N@€O@€Q@@P@€X@€S@€U@€R@@T@I@R@ÀP@S@€O@T@Q@@U@ÀR@ÀY@@W@P@ÀS@@R@€P@R@@R@€O@@Y@@P@€L@@V@€O@R@€U@Q@Q@@U@@S@€S@€L@@P@€N@€U@@W@€Q@@T@T@ÀQ@W@@T@@R@€Q@T@R@S@€O@€N@@T@€O@€Q@M@O@Q@T@@U@ÀR@@a@N@S@@Q@N@O@O@@P@@W@€R@€R@€N@K@M@@P@@S@T@@Q@@R@P@@U@€N@Q@R@N@€U@W@ÀY@@V@€Q@@R@R@ÀR@ÀQ@L@€S@ÀP@€L@ÀR@P@R@ÀS@W@€T@U@@S@€P@@W@P@ÀQ@U@€O@U@@S@€W@€T@€J@X@€R@@V@Q@@T@@R@€Q@N@@Q@P@P@M@ÀV@W@€M@€R@€X@ÀS@@V@@T@€Q@€M@@T@ÀR@€I@U@€P@@T@L@@T@O@R@U@T@N@ÀP@€R@€U@€P@@S@ÀS@V@€S@@V@€O@À\@€N@€X@R@€X@€S@€U@T@S@@X@N@€W@@Q@€W@P@€Y@€U@ÀU@X@R@@W@@R@ÀU@R@ÀU@Q@@R@Q@O@€J@€O@@R@€S@Q@ÀQ@ÀQ@@Q@€T@€R@€S@€K@P@€S@@R@Q@R@S@S@€N@U@€J@€S@@R@R@ÀU@Q@@R@ÀR@@P@F@@W@€P@ÀT@ÀY@I@Y@€U@Q@ÀP@I@@W@€Q@@T@`d@€R@ÀS@@V@_@€M@€P@S@S@ÀP@@R@€O@O@ÀR@@P@€O@€S@U@R@S@ÀR@W@T@@T@@T@ÀQ@€S@M@R@R@€L@S@\@€Q@€O@@P@M@@R@@W@W@@W@R@€J@@Y@ÀU@Q@ÀP@I@L@R@T@ÀR@@T@€P@€N@@Q@@V@€O@X@V@@Q@€S@€N@À^@V@ÀZ@@U@@P@K@@S@ÀR@@]@@S@W@ÀQ@@U@ÀR@@Q@€O@L@ÀU@€P@€U@ÀT@ÀP@€T@€M@€^@@S@@Q@€O@€S@ÀQ@€P@@S@€N@€O@T@@T@Q@@V@€Y@Q@€S@Z@€M@ÀQ@ÀQ@€Q@T@@T@€Q@@P@T@@W@€H@€R@^@ÀQ@@T@@T@O@O@R@€K@@P@Q@O@€S@€Q@ÀR@€O@@R@U@T@ÀR@4@0@4@(@0@4@4@0@4@0@(@0@4@0@4@V@8@8@8@4@8@4@4@R@0@4@0@0@8@0@4@8@8@0@0@0@4@4@4@(@4@8@8@(@<@<@<@4@8@0@8@F@0@4@4@8@4@8@0@8@4@4@4@@@8@4@4@0@4@8@4@<@8@0@4@8@<@0@B@4@4@4@4@4@(@8@0@4@4@F@4@4@4@@@(@4@4@8@B@8@8@0@0@0@0@4@8@0@4@0@0@(@4@0@8@0@4@0@J@H@D@R@H@B@F@F@D@F@B@D@R@W@T@H@H@H@F@L@B@B@F@R@T@T@F@J@R@N@W@T@D@P@F@F@B@J@L@N@J@B@F@Q@R@P@B@R@D@L@R@B@H@N@F@F@S@R@F@B@<@F@D@N@F@N@H@H@N@D@H@H@N@F@Q@B@F@H@H@H@D@N@@@D@€a@L@J@_@^@J@S@F@H@@@B@F@N@L@R@H@F@L@D@J@R@F@F@L@L@B@F@F@D@B@D@B@B@H@J@F@J@S@B@D@F@N@P@Q@B@H@Q@B@D@B@J@F@U@P@H@<@U@H@B@F@D@L@D@D@D@J@D@J@P@J@J@D@a@Q@H@T@Q@H@J@H@B@N@H@N@J@L@L@F@F@N@H@D@H@J@D@V@B@F@J@B@B@B@S@^@P@H@R@H@H@D@F@F@4@Q@F@N@N@J@B@D@F@a@L@D@F@@@H@H@D@F@L@H@<@D@F@F@H@D@B@B@D@N@H@B@J@N@D@J@F@P@B@D@Q@H@F@L@D@F@H@H@Y@H@H@B@F@H@J@P@H@D@J@<@H@N@L@B@D@H@F@D@D@F@D@B@F@D@H@D@F@S@S@J@T@F@N@L@F@R@V@H@L@D@N@P@N@L@H@F@J@H@P@B@U@L@D@L@T@H@H@F@L@R@R@B@P@F@J@@@H@D@4@F@B@B@<@@@Q@L@F@F@J@J@N@B@Q@H@D@H@F@D@F@F@P@D@D@D@J@H@H@D@T@4@J@Q@Q@H@P@B@D@S@R@Q@F@N@Q@P@H@L@S@B@J@<@D@`@D@H@H@L@H@H@F@D@B@D@8@J@D@F@W@B@D@€c@R@F@D@P@H@D@^@F@H@Q@D@R@D@L@0@F@D@P@L@D@Q@L@L@Q@D@L@F@J@H@L@J@H@P@N@T@_@Q@P@B@N@B@B@F@D@J@T@J@F@F@P@B@D@H@B@D@H@@@H@J@F@D@L@F@F@T@4@D@H@B@D@H@<@L@0@<@8@(@B@B@@@0@F@@@0@B@H@B@B@D@D@F@D@L@@@8@@@8@B@H@F@F@B@@@B@0@D@<@N@B@<@0@F@D@H@B@@@@@V@B@H@8@0@@@@@@@(@(@D@H@@@F@@@(@B@4@B@8@H@(@R@4@F@<@0@H@D@H@(@H@H@<@D@8@L@D@D@B@B@H@@@F@B@D@H@L@@@<@F@4@8@B@H@F@D@B@B@8@@@B@D@8@B@0@8@N@B@<@@@F@F@B@B@@@0@D@<@B@D@F@@@<@@@4@@@8@J@D@@@D@B@4@B@@@B@0@B@H@D@D@B@D@4@D@4@D@D@D@0@B@<@B@D@@@P@J@S@4@H@J@@@@@D@N@P@B@8@H@@@H@8@L@4@<@4@<@B@H@F@<@B@B@@@H@F@8@B@<@F@J@F@J@H@F@F@8@4@@@@@(@B@T@P@D@4@B@H@D@@@B@B@F@L@D@<@8@J@4@H@4@D@<@J@@@D@0@Q@H@Q@<@0@0@F@N@@@4@B@D@0@@@8@B@D@<@8@(@(@0@@@8@8@H@<@4@8@8@4@<@B@4@8@@@<@8@4@8@4@<@@@4@8@8@D@F@4@<@@@D@<@<@@@8@8@4@B@8@<@D@8@4@D@<@4@8@0@<@4@@@B@@@4@F@B@@@<@8@<@B@0@8@4@@@<@8@8@@@B@B@B@8@B@8@<@8@H@@@<@8@F@B@B@8@Y@B@@@N@N@<@<@8@<@0@B@D@D@@@@@<@B@<@B@@@<@8@<@@@4@B@8@<@@@@@4@D@B@<@<@4@D@4@4@8@F@B@F@L@4@<@<@@@4@4@<@<@D@8@<@0@B@8@D@8@D@D@4@4@8@<@8@<@D@<@<@8@B@<@8@<@8@4@8@<@4@B@@@8@8@<@<@F@<@<@<@8@H@B@8@8@F@8@<@H@4@F@F@F@4@<@<@<@D@4@8@F@(@D@8@@@J@<@L@4@8@Q@@@@@<@4@8@8@8@8@<@8@@@8@8@4@8@D@4@B@8@@@8@B@<@H@4@<@<@B@0@4@4@B@<@<@F@8@<@<@<@<@<@4@B@<@B@D@D@8@F@F@<@B@<@4@D@<@8@4@4@8@D@B@8@4@8@4@8@@@@@@@<@8@@@D@8@8@B@H@B@8@4@D@D@H@F@B@8@<@@@4@D@F@D@B@<@L@@@<@4@F@H@4@B@8@F@4@<@4@(@8@D@D@0@4@8@F@8@8@<@<@@@4@F@D@8@8@D@D@8@8@8@8@4@8@<@<@8@D@<@ @<@<@<@B@D@H@8@@@8@B@B@@@F@F@<@8@F@J@<@B@4@F@8@<@D@B@<@0@8@B@@@<@<@@@D@8@D@4@F@Q@B@B@4@@@<@4@J@D@@@<@8@@@4@@@8@B@4@8@<@8@8@@@B@8@8@@@4@@@<@@@<@<@D@B@<@U@<@<@0@8@8@4@8@4@8@B@@@F@8@D@4@4@8@B@4@<@4@<@<@8@D@@@8@ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?4@4@<@4@0@S@<@4@4@<@4@(@4@8@8@8@8@8@8@B@B@8@4@4@8@8@8@F@B@8@0@4@4@0@4@8@4@4@0@4@B@0@8@<@D@@@S@0@4@4@<@4@4@4@8@@@4@4@D@4@<@8@4@B@4@0@4@8@<@8@4@B@4@8@8@8@8@8@4@8@4@4@4@4@8@8@@@0@8@D@0@4@4@8@4@(@<@(@4@4@(@<@8@4@0@F@P@L@P@D@F@D@J@F@L@J@D@F@H@R@F@J@H@U@P@F@J@R@H@J@J@H@H@N@D@H@P@D@N@R@J@J@H@<@F@P@J@H@H@<@Q@L@P@F@D@H@H@L@T@R@J@B@W@P@<@F@H@J@T@J@F@F@N@4@R@P@F@L@R@@@T@H@H@U@Q@F@Q@<@W@]@L@U@Z@]@H@P@D@B@D@B@D@H@F@L@@@Q@@@R@S@L@H@N@D@S@H@D@H@F@J@B@R@L@@@L@J@D@F@D@D@H@D@H@N@P@J@F@H@U@J@V@R@H@F@D@T@@@J@U@H@F@F@F@N@F@F@H@@@H@H@T@J@P@]@H@N@H@Q@F@L@H@F@F@B@N@H@B@H@H@N@D@U@F@D@H@V@H@D@N@8@H@S@R@L@W@T@H@N@F@D@L@L@N@U@D@Q@J@F@P@R@F@€`@J@N@H@@@H@T@N@D@N@T@H@V@L@H@T@W@H@S@H@J@F@R@Q@L@H@S@L@J@T@B@Y@L@D@H@L@Q@L@D@T@F@B@@@D@J@0@J@F@H@H@F@H@U@L@D@N@J@P@D@L@B@F@H@U@F@Y@F@F@J@L@R@F@S@J@D@N@B@H@F@L@S@D@J@H@H@N@H@Q@Q@V@H@J@N@R@R@H@F@H@F@R@R@T@J@J@R@D@<@H@D@D@Q@Q@D@<@F@H@R@H@H@Q@4@L@F@F@U@Y@Q@H@N@F@H@H@@@H@J@R@J@D@F@J@F@L@F@J@H@L@F@4@P@B@S@H@S@Q@P@R@[@H@J@8@H@B@H@V@H@J@F@H@F@B@B@J@D@F@L@N@J@L@R@D@L@[@0@F@L@L@N@D@Z@H@Q@D@H@Q@D@P@@@P@H@Q@V@R@D@J@H@N@U@S@D@F@F@D@J@J@P@J@D@P@Q@F@D@T@N@@@L@D@<@D@N@D@Q@F@W@L@N@F@H@D@D@L@F@L@D@P@L@4@J@B@8@B@@@@@(@8@4@F@4@<@B@@@<@B@B@<@Q@4@J@B@L@@@D@B@<@@@S@B@H@D@D@D@8@H@<@H@<@B@8@B@D@@@(@F@@@D@<@H@8@D@8@B@N@D@J@@@H@<@J@0@J@D@F@(@0@<@4@F@H@8@8@B@<@D@4@4@D@0@0@0@H@D@D@(@F@D@F@F@D@D@D@F@H@F@@@8@D@4@B@D@@@N@B@8@<@B@B@B@D@<@F@R@@@B@D@L@B@0@0@B@@@<@F@8@B@H@D@@@F@@@D@B@D@(@B@B@H@Q@J@L@(@@@4@D@<@B@F@H@H@0@0@F@@@(@4@L@F@L@B@H@D@U@H@H@8@J@L@F@8@(@(@L@8@4@<@@@8@@@F@D@0@J@@@@@B@B@@@@@(@B@D@D@<@(@B@8@P@@@J@B@P@D@J@<@8@@@0@P@<@B@@@N@4@F@U@J@<@B@B@B@W@(@8@H@B@L@<@L@8@8@L@0@B@D@<@J@S@Q@B@H@@@8@H@@@0@J@D@4@4@8@D@L@B@4@4@<@4@0@(@0@ @4@(@L@<@8@D@B@8@F@8@<@<@@@<@4@<@J@F@<@<@<@<@F@8@<@8@<@@@B@D@<@D@8@<@0@8@D@4@@@<@<@(@<@4@8@F@@@(@@@@@<@<@8@<@<@J@<@@@@@4@B@F@8@8@<@@@<@8@<@B@@@ @D@<@H@D@@@D@D@H@H@@@@@H@8@H@B@N@B@J@P@D@@@B@8@4@<@B@D@<@<@@@B@F@F@F@<@S@@@<@L@@@@@D@<@@@<@4@D@@@B@@@@@8@@@8@8@B@4@F@8@H@8@8@8@H@<@8@<@8@F@8@<@4@B@<@@@@@D@8@4@4@4@@@4@8@8@8@<@D@B@<@@@@@<@8@@@<@@@8@B@L@8@8@@@8@@@8@@@8@@@@@<@@@F@F@0@H@8@F@@@L@@@B@D@D@8@F@D@H@F@4@4@@@8@D@<@<@N@H@B@@@B@@@J@J@8@@@<@@@B@<@@@F@B@<@0@8@8@<@D@B@N@<@@@H@@@8@F@B@F@4@@@<@@@8@8@D@8@4@0@8@H@ @<@<@<@B@8@<@@@<@8@B@<@8@B@@@4@<@8@<@<@<@8@8@@@@@<@8@<@B@<@8@4@8@<@F@8@D@4@@@H@<@B@D@H@8@@@@@B@P@8@@@<@<@D@F@8@H@<@@@<@8@(@8@F@4@<@F@8@0@<@<@4@B@4@D@(@8@F@F@H@F@8@<@D@<@<@<@D@8@@@8@@@4@D@8@<@<@8@@@<@D@<@<@H@8@@@@@<@@@B@8@8@<@@@@@H@H@D@J@<@@@8@<@8@8@4@<@8@<@B@8@@@@@@@8@F@J@F@4@<@H@<@B@L@D@<@<@8@<@D@8@8@D@8@B@D@8@4@F@<@<@@@@@D@F@<@@@@@@@F@@@8@F@<@8@4@<@8@D@<@8@0@8@8@F@D@<@@@B@B@@@<@4@4@B@@@@@@@D@B@ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?J@@@<@4@J@8@@@T@B@8@0@0@4@8@U@V@H@B@T@8@<@8@R@F@N@U@8@V@4@8@<@8@<@8@4@(@@@4@4@F@J@Q@@@S@<@4@@@H@8@8@L@B@B@D@8@0@4@@@W@4@4@0@<@@@0@B@<@@@0@<@4@4@4@T@8@H@8@<@H@Q@<@R@F@H@D@8@T@0@4@4@@@D@F@J@0@8@@@H@B@@@<@8@L@S@8@F@H@<@N@B@B@8@B@4@4@F@@@J@R@L@4@J@<@D@(@@@Q@<@4@D@U@<@<@P@N@<@B@J@@@F@4@8@H@<@B@F@@@4@8@8@0@L@0@B@@@8@@@<@@@4@4@N@L@8@B@8@T@<@@@8@S@D@@@4@(@R@4@@@4@@@8@8@H@D@@@Q@B@N@4@<@4@@@H@@@8@0@4@4@0@D@8@]@J@B@P@F@L@B@R@Z@F@D@P@D@J@F@R@4@J@N@L@<@D@<@R@F@0@<@<@J@L@4@@@4@@@H@V@D@B@B@B@P@4@F@J@L@X@<@F@0@R@P@<@H@F@P@H@D@L@Q@4@<@B@D@8@J@F@R@H@J@S@U@H@P@8@W@W@J@Q@U@@@<@8@F@8@@@<@4@8@8@R@B@P@4@W@@@H@D@D@<@@@L@Q@N@@@P@@@D@@@0@8@B@<@F@D@D@@@J@H@D@Q@J@W@H@B@F@8@Q@8@@@N@D@8@U@F@H@F@R@F@F@L@8@L@P@T@8@R@0@H@0@N@D@4@@@H@F@4@F@D@F@S@N@8@@@S@F@4@H@4@B@R@H@D@D@Q@0@P@J@D@Q@D@H@L@(@4@B@N@R@4@@@L@R@D@@@4@S@F@8@J@J@B@X@<@8@R@<@H@S@N@P@D@H@H@R@Q@T@D@8@H@L@(@<@F@L@N@8@<@8@@@4@@@<@H@J@L@@@4@@@<@J@P@F@T@<@8@<@S@F@T@F@B@P@B@S@4@8@@@F@N@R@B@D@U@8@V@N@W@D@<@N@4@@@B@F@Q@J@F@P@B@0@<@S@D@P@B@P@8@4@Q@D@8@0@F@B@4@J@8@R@W@J@H@4@H@H@J@8@L@N@@@@@H@F@S@R@0@B@(@D@<@V@4@S@D@P@T@N@<@P@@@B@L@J@F@Q@F@H@@@F@<@N@D@F@N@H@8@L@D@B@N@8@J@X@J@N@N@T@Q@Q@8@F@J@D@4@<@F@P@H@D@L@H@D@S@S@L@H@B@H@0@T@H@J@L@<@@@B@N@J@R@@@4@<@J@4@P@D@T@N@R@@@H@0@R@L@8@S@<@N@4@H@<@<@ @D@B@(@R@<@@@J@@@S@Q@P@0@F@8@<@Q@<@8@N@D@H@8@<@B@@@B@4@D@@@L@B@F@0@@@F@R@4@@@R@F@L@<@J@Q@J@0@H@8@0@J@J@8@F@U@F@4@N@@@B@0@D@<@N@D@Q@<@4@R@J@D@J@L@P@N@8@N@4@<@N@L@Q@B@F@H@L@F@Q@F@Z@P@D@D@R@L@0@8@N@U@F@F@H@J@8@P@<@B@<@B@4@@@4@S@L@L@N@(@8@Q@P@4@P@<@(@J@0@J@S@S@F@F@F@P@L@F@P@J@B@4@B@D@0@B@4@J@H@S@H@F@S@F@D@@@<@B@4@D@8@R@D@4@L@L@R@B@D@J@L@H@Q@H@F@P@D@(@8@R@@@(@B@€`@P@B@Q@D@(@ @(@@@(@4@L@(@D@N@0@@@J@J@Q@8@N@@@H@D@<@B@H@N@B@B@J@<@H@<@J@8@0@0@4@<@B@8@0@(@0@D@0@N@@@P@F@4@H@ @4@J@L@(@H@0@(@(@<@F@8@<@8@8@N@D@J@L@N@H@8@8@4@<@B@B@J@J@8@<@8@F@ @@@D@@@(@F@0@D@@@ @J@0@H@4@J@0@(@B@D@Q@F@4@J@8@@@P@@@0@J@D@N@@@N@F@B@(@<@<@F@<@<@D@8@ @0@D@F@R@@@B@B@D@J@8@D@F@@@J@@@J@@@P@J@4@0@S@<@D@0@0@<@D@L@J@F@F@L@P@4@F@J@J@0@@@<@D@B@0@F@4@@@4@8@0@4@<@8@<@H@4@8@<@0@L@8@@@D@F@D@B@B@J@0@B@ @0@0@@@8@B@4@<@4@D@L@<@J@L@(@B@ @<@D@@@8@(@8@F@L@ @<@4@H@@@0@(@0@@@@@ @L@J@H@8@B@<@0@D@@N@4@8@F@(@H@L@H@(@4@D@<@ @ @F@F@8@D@ @L@N@0@8@H@8@J@D@(@H@P@8@8@@@F@J@L@@@4@J@<@H@(@H@B@D@Q@B@B@H@(@L@(@Q@ @0@0@@@@@F@4@ @ @0@4@B@0@F@F@(@P@@@8@H@<@(@H@D@4@S@(@4@0@J@<@F@H@B@W@J@L@F@<@0@N@ @0@4@F@D@B@B@<@8@N@U@H@J@8@<@F@Q@F@D@4@U@ @F@4@8@H@(@N@J@J@B@ @<@<@@@D@8@H@F@0@B@F@L@<@@8@D@4@(@N@ @@@8@F@B@D@ @D@0@4@H@D@F@8@4@ @4@@@0@<@8@F@B@F@(@<@4@@@8@ @0@B@H@8@F@L@0@R@(@B@D@F@L@N@J@8@8@<@F@F@4@N@<@D@F@4@B@@@B@<@D@0@0@0@(@D@F@<@4@(@(@H@8@D@0@8@D@B@4@<@8@4@D@(@<@(@@@N@ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?J@H@J@P@H@F@F@H@D@L@D@D@F@L@T@D@J@H@S@Q@H@J@R@S@P@S@H@R@H@@@S@H@Q@R@F@H@H@L@J@S@H@P@S@H@J@D@F@J@J@P@J@H@J@Q@@@F@H@J@S@H@F@H@L@B@L@J@L@R@J@J@J@N@J@N@`@N@[@]@H@R@D@F@D@F@L@B@P@J@U@N@J@N@H@F@D@H@B@R@F@J@J@D@B@D@D@H@N@W@H@H@H@H@H@U@S@U@H@S@D@H@U@D@D@D@D@F@J@B@H@H@L@L@J@U@H@J@H@Q@F@L@H@B@D@D@L@D@J@L@H@Q@F@L@U@H@L@H@P@]@B@S@J@J@D@J@F@V@F@L@Q@H@P@H@F@`@P@J@F@@@H@F@D@L@Q@<@L@H@F@U@F@V@F@L@H@T@R@F@R@L@N@T@D@R@D@J@Q@L@F@S@H@H@B@D@B@R@D@H@F@N@L@B@F@J@H@H@J@D@F@F@H@F@F@D@F@R@P@J@S@H@H@L@F@D@L@@@H@F@P@H@H@P@R@@@H@L@Q@H@H@F@Q@S@T@F@L@R@J@@@H@@@J@D@@@F@H@T@@@F@L@D@Q@L@J@F@F@D@H@H@H@L@D@J@F@H@H@J@D@J@N@H@F@J@F@S@J@P@T@T@H@H@^@F@J@H@F@<@D@H@B@B@F@N@J@P@F@a@P@D@J@N@J@D@[@L@H@D@R@P@8@Q@H@Q@T@F@Q@H@R@F@P@J@H@H@J@P@L@F@S@P@H@R@F@J@D@<@D@R@V@H@N@H@J@D@H@D@@@J@D@H@B@R@H@P@@@<@B@0@@@(@<@B@<@B@@@@@0@4@8@8@(@H@H@4@0@D@B@4@B@B@8@4@F@4@<@<@8@@@<@8@B@@@4@B@B@8@4@D@0@@@F@<@4@<@8@<@F@8@@@N@4@4@0@F@0@8@J@B@<@8@0@@@8@0@F@0@<@B@<@D@D@0@4@B@4@4@(@@@<@F@F@4@4@F@B@B@F@8@B@D@B@H@4@D@<@4@J@D@0@@@4@@@8@4@8@<@B@<@4@B@B@F@<@<@0@D@H@<@F@4@8@<@F@0@D@D@0@B@@@@@@@0@<@4@B@4@@@4@H@8@B@F@H@8@4@8@B@@@<@D@F@F@H@0@0@0@4@D@0@@@B@0@4@<@B@<@8@F@<@0@P@0@S@<@8@@@D@@@H@8@4@N@P@8@8@D@4@@@8@@@D@0@D@4@<@4@<@4@B@8@<@4@B@@@4@B@8@B@(@B@8@4@D@@@B@L@B@0@F@H@4@<@8@8@0@<@0@@@N@4@D@T@H@B@4@B@B@<@<@B@0@H@J@@@4@J@(@F@0@8@(@4@4@J@4@<@@@8@4@B@0@D@0@<@@@J@4@4@B@4@4@F@<@4@8@8@B@<@H@<@H@L@F@J@J@L@H@B@F@D@<@F@B@F@S@@@R@H@B@B@F@Q@H@D@D@Q@F@H@D@P@<@@@H@S@Q@H@N@N@F@P@J@B@D@H@F@Q@Q@D@D@B@B@L@T@P@J@<@Q@H@<@H@B@F@J@F@F@H@B@L@@@D@D@@@B@F@@@@@@@B@H@D@B@H@@@0@F@4@D@H@D@B@@@4@<@<@4@B@0@P@B@@@4@(@H@8@B@@@4@<@4@U@N@@@V@R@L@J@H@L@F@N@H@H@H@F@L@J@R@J@L@Q@J@L@B@ @J@J@J@Q@N@R@ @L@(@L@F@H@P@0@L@D@H@H@L@J@D@ @P@N@0@P@L@Q@P@P@U@N@F@J@J@P@R@ @N@L@0@F@Q@0@(@H@H@L@D@J@N@N@J@F@H@H@P@(@L@J@T@W@H@J@H@J@R@Q@H@J@N@F@L@J@N@L@J@J@F@F@ @J@N@P@H@ @L@Q@ @(@R@S@F@R@F@L@J@0@J@H@L@J@D@Q@(@H@D@L@@H@J@N@L@4@H@Q@ @F@R@J@H@J@D@H@(@F@U@H@L@(@(@J@F@N@L@P@H@J@J@H@H@H@J@J@L@P@H@H@P@D@H@(@N@W@R@T@ @U@L@N@0@L@T@N@J@H@R@P@L@Q@F@N@H@D@J@D@N@H@ @0@(@J@H@H@L@L@(@L@P@F@L@J@L@L@H@P@ @H@Q@L@R@N@@Q@Q@ @(@(@Q@P@L@ @J@L@H@J@N@H@J@(@N@H@0@N@H@ @S@R@F@P@D@H@H@ @J@H@P@L@L@ @J@L@N@H@W@R@J@N@L@H@D@F@ @J@J@N@H@L@F@D@L@H@L@J@J@ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?B@J@<@H@(@F@4@<@8@4@8@4@(@4@4@8@0@Q@B@B@4@<@4@4@0@F@T@L@V@P@@@4@4@(@4@4@8@8@4@4@<@<@8@J@F@U@F@8@8@8@8@0@4@4@8@8@<@J@4@4@B@4@8@(@8@B@<@4@N@P@4@J@8@@@4@8@J@8@H@4@4@8@4@8@4@D@(@8@4@8@J@4@0@8@4@4@4@8@8@0@B@@@N@0@0@<@<@<@4@F@@@4@N@4@R@0@@@@@4@@@<@B@<@8@<@8@<@Q@4@F@L@P@4@8@N@4@<@Q@4@@@4@0@0@0@4@8@8@J@4@P@8@4@L@F@B@N@H@<@<@8@D@U@D@B@N@@@8@B@L@S@<@L@<@S@8@Y@Q@P@X@R@F@J@D@B@@@@@L@N@@@<@D@B@R@P@F@L@8@P@R@N@P@B@H@D@P@T@B@<@D@8@D@@@F@B@L@F@D@8@F@8@F@B@J@@@@@<@P@4@J@D@^@N@Q@B@H@H@D@X@R@J@P@H@0@4@P@@@J@N@D@B@J@N@@@@@B@<@B@B@B@D@B@D@L@N@B@8@@@L@V@D@P@F@@@<@B@B@B@<@<@4@T@@@D@P@0@P@4@@@B@H@U@F@<@L@D@B@D@N@4@Q@@@W@4@<@N@L@B@F@L@P@J@N@B@P@B@J@N@@@L@B@H@L@F@8@@@P@_@S@0@Q@D@J@B@8@H@@@L@F@J@N@D@8@F@€`@(@4@8@H@(@L@S@R@D@<@@@B@R@@@@@<@8@B@L@L@D@F@J@D@D@Q@0@@@N@F@@@N@D@@@D@H@B@H@H@@@R@H@J@J@N@D@J@8@@@F@H@B@L@4@@@B@(@F@J@@@8@B@@@F@P@R@D@P@D@L@U@D@U@V@S@B@<@B@R@L@@@B@F@@@F@N@B@R@N@B@N@S@J@D@@@F@T@L@4@J@F@J@B@H@8@B@@@B@4@<@N@J@W@D@L@L@V@<@P@B@<@D@N@B@<@<@N@8@@@@@V@<@B@T@J@T@Q@<@P@H@8@Q@@@N@D@D@N@T@D@Q@R@H@<@<@B@]@D@F@H@L@H@Q@Q@8@8@<@F@N@8@F@@@@@€b@R@B@F@@@_@D@@@R@B@F@<@J@(@H@F@H@D@Q@F@J@4@@@@@4@W@8@J@D@L@D@S@P@F@P@@@L@@@@@B@T@J@T@H@L@D@S@8@0@H@J@D@J@J@P@B@B@4@W@(@B@P@L@D@<@B@B@D@@@Q@<@B@B@H@B@F@B@4@4@@@0@F@L@L@F@8@@@Q@D@L@@@D@0@D@N@H@8@D@<@4@B@B@N@D@<@J@B@B@<@4@F@F@<@4@(@ @D@8@<@B@8@<@N@4@8@N@8@D@R@B@B@H@H@N@4@D@4@@@@@H@J@B@J@L@0@@@F@H@L@<@N@H@D@<@D@@@@@B@U@B@F@4@N@8@Q@B@<@F@4@H@B@0@L@H@N@H@B@F@D@J@@@<@8@0@@@F@D@0@F@N@<@H@D@4@J@H@<@B@H@H@4@J@D@4@(@@@P@B@L@V@L@8@L@D@F@<@N@Q@@@@@<@J@F@D@P@H@@@D@H@R@J@D@D@N@B@@@H@<@N@N@B@N@L@F@@@@@0@S@L@D@J@@@D@F@J@L@B@F@0@J@4@J@H@8@J@D@D@D@W@H@P@<@8@F@Q@D@<@<@@@F@8@B@8@@@D@B@@@D@@@8@8@F@<@0@0@D@4@<@<@4@4@N@ @(@@@8@0@B@B@L@(@B@8@D@B@4@8@4@4@0@0@0@<@L@0@0@F@8@D@8@@@4@ @B@J@@@4@<@8@D@@@4@4@0@4@D@D@4@F@4@D@B@8@(@D@(@8@8@@@0@(@L@U@ @@@ @W@4@B@4@<@H@8@0@B@0@8@@@ @Q@D@<@@@4@8@4@<@F@B@0@8@(@B@4@D@4@4@B@0@F@D@(@4@F@H@4@N@4@4@(@4@4@H@0@0@@@L@4@F@4@@F@ @0@8@<@<@(@0@8@4@8@B@D@8@@@0@4@(@B@4@D@4@<@<@8@<@F@8@8@8@H@D@(@J@B@@@@@<@(@D@@@H@<@@D@8@B@D@(@H@8@@@8@<@H@0@P@D@W@<@L@(@4@@B@8@0@8@F@0@4@B@0@D@N@@@0@<@8@8@<@@@8@<@B@ @4@B@8@0@8@D@4@4@<@4@<@<@0@J@<@@@H@F@8@F@J@0@<@B@4@D@ @0@0@8@8@<@D@(@4@0@8@D@@@<@8@8@@@F@8@<@F@@@(@(@4@@@@@J@B@D@B@@@@@8@F@F@4@8@F@L@8@4@F@@@@@ @<@8@<@8@<@(@4@4@B@8@0@B@<@4@8@J@@@F@0@F@0@0@8@L@D@J@0@H@(@B@0@4@ @B@@@8@4@B@<@L@L@(@H@(@@@8@8@N@@@8@8@F@L@0@0@0@S@4@4@D@J@8@H@8@B@D@0@<@D@(@8@H@4@L@B@0@B@4@X@4@8@J@4@8@(@@@<@8@B@J@8@<@8@<@H@8@0@ @@@<@4@8@<@D@@@L@8@B@0@D@8@8@4@4@B@J@B@@@4@F@(@ @D@<@B@<@H@(@B@ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?H@F@8@(@R@@@4@V@T@<@0@4@B@8@<@0@8@4@8@8@4@8@8@@@F@L@4@4@@@8@4@F@B@8@4@8@0@8@4@4@<@4@0@4@0@D@4@@@B@H@F@8@Q@4@4@@@4@4@4@F@4@4@4@H@8@@@4@4@4@8@H@F@N@0@P@U@4@@@8@<@@@D@4@B@P@8@4@T@8@4@0@0@8@0@@@8@B@4@8@Q@0@8@8@8@@@8@8@<@<@0@(@(@@@N@`@N@F@F@J@B@N@U@8@F@T@D@J@H@Q@8@H@L@R@D@J@F@H@B@8@B@Q@B@H@Q@N@H@@@8@B@R@V@H@F@J@F@P@<@D@H@H@L@S@V@H@<@W@U@Q@D@H@J@P@H@F@N@R@4@T@P@D@B@R@B@S@J@L@S@R@D@Q@<@V@X@L@R@X@T@H@<@<@4@F@B@D@J@D@F@@@R@B@P@S@N@L@Q@F@Q@D@D@R@J@L@B@S@D@8@L@F@@@8@B@@@J@B@F@L@L@N@D@H@H@J@W@N@B@H@F@S@<@H@R@H@F@H@F@L@D@F@H@4@H@L@D@L@R@U@B@P@D@Q@L@L@F@@@D@F@L@<@@@H@L@J@4@U@N@D@T@H@H@L@0@B@S@S@J@<@S@0@N@H@D@P@H@J@T@8@<@8@H@P@R@@@U@R@P@D@<@J@U@H@B@L@N@B@W@H@D@V@F@F@U@U@F@D@R@N@H@H@U@L@H@T@B@S@N@L@8@P@P@P@@@N@B@8@H@L@F@@@H@8@S@J@S@<@B@P@L@P@F@P@S@T@F@T@D@D@J@H@R@H@U@B@D@@@<@B@8@N@R@B@B@F@L@D@S@P@0@F@H@N@Q@4@F@B@H@L@J@D@N@Q@H@R@<@D@F@U@P@T@H@@@D@F@4@L@F@ @H@P@F@S@Y@Q@J@X@F@J@H@<@H@P@Q@H@H@F@Q@D@S@8@F@D@N@F@0@X@<@S@F@P@Q@N@B@Y@B@J@@@J@@@H@S@H@J@D@H@8@@@@@N@D@H@N@R@J@L@T@F@L@J@F@P@L@S@P@W@P@R@<@P@P@D@J@B@L@P@J@J@Q@D@H@@@H@V@T@L@F@F@F@@@L@S@J@@@P@P@D@<@H@P@@@Q@B@8@D@L@B@S@F@W@L@L@B@H@@@D@L@8@S@@@P@U@F@D@0@<@(@0@B@@@P@8@<@D@8@N@N@N@B@D@@@B@R@@@J@@@@@F@8@F@@@F@D@8@B@<@L@P@8@D@@@D@P@4@L@F@H@L@@@J@@@F@H@4@F@J@0@D@Q@H@D@B@L@@@D@@@@@@@(@D@(@4@0@J@F@F@D@F@B@B@H@J@F@B@F@F@8@J@F@N@B@<@B@D@D@@@H@[@Q@B@B@N@8@H@4@H@H@@@F@@@<@J@(@D@F@@@@@8@8@H@0@B@J@N@L@0@B@J@D@D@@@(@F@F@@@(@@@L@H@S@J@0@H@Q@<@X@<@F@F@<@B@J@J@D@0@4@0@8@0@0@H@D@D@L@B@@@4@B@4@<@<@D@<@@@<@B@B@J@4@Q@F@@@D@N@@@(@8@R@H@D@4@P@8@B@T@<@@@B@^@J@B@P@B@T@4@D@ @4@L@@@B@F@L@T@0@ @P@F@H@J@0@H@H@D@4@4@L@N@<@8@4@B@0@B@B@@@F@4@(@ @0@4@8@F@@@L@4@8@@@F@8@<@4@@@<@4@J@@@8@<@<@@@ @<@@@@@8@@@4@D@4@(@4@8@4@D@4@D@<@0@(@@@8@D@F@8@@@<@<@0@<@<@<@J@8@@@@@0@J@@@@@4@<@@@F@<@<@B@@@ @F@<@F@8@@@D@B@8@F@<@@@F@4@D@B@J@@@N@N@B@@@Q@(@ @<@B@D@@@4@8@D@H@D@D@B@L@D@4@L@<@<@D@4@B@<@8@F@8@8@@@<@0@(@4@0@8@8@F@4@H@<@4@8@B@<@<@D@F@<@8@8@J@B@8@@@@@D@8@4@B@4@@@ @8@B@4@<@<@D@0@B@<@@@D@@@8@8@<@@@J@@@0@@@B@<@(@@@8@<@@@@@<@J@J@J@8@T@@@0@@@ @F@<@8@F@B@H@F@(@(@S@8@D@<@ @F@B@D@<@@@B@F@N@4@<@@@8@F@<@<@D@P@8@4@@@@@8@F@8@N@<@B@J@4@<@F@<@F@@@<@H@@@8@8@F@4@(@B@Q@<@0@@@8@B@<@8@(@8@F@<@<@8@B@0@@@8@4@4@8@B@@@<@<@8@<@(@0@0@4@(@F@<@H@<@H@8@(@F@J@J@F@@@H@N@ @<@0@<@D@<@H@8@<@@@J@0@4@H@8@<@D@B@0@4@<@R@0@0@@<@H@4@D@F@B@B@B@<@@@<@D@4@F@4@@@8@B@D@<@<@ @<@8@4@B@D@B@(@@@<@H@@@D@0@J@4@@@(@H@H@D@J@8@@@<@@@(@4@4@@@8@4@B@8@D@<@<@<@H@8@4@@@H@@@D@L@4@@@0@0@8@D@(@8@4@8@8@B@F@0@F@0@<@8@@@D@H@<@8@0@@@D@@@4@H@8@8@D@<@8@D@@@8@(@<@D@@@4@<@@@B@@@4@<@0@4@B@ @@@0@B@B@ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?T@L@V@N@R@N@S@S@L@L@L@R@H@N@P@H@N@R@H@P@P@€a@_@[@V@Q@S@U@N@J@H@H@J@H@B@H@N@P@L@L@P@S@\@L@Q@R@S@N@L@P@P@P@T@L@D@J@V@N@P@S@@@L@H@@@J@F@D@L@N@R@B@T@Q@P@J@S@Q@Q@^@B@R@N@Q@N@F@S@J@N@F@8@8@B@4@4@4@8@8@4@8@F@<@4@0@4@8@@@@@<@8@<@8@4@8@4@<@<@<@8@8@@@B@8@4@4@0@<@<@B@@@@@4@<@<@8@8@4@<@@@8@4@4@8@@@<@<@<@0@8@@@@@8@8@4@F@F@B@8@<@8@H@B@<@8@<@@@8@4@8@@@8@D@4@@@8@8@@@<@B@<@<@8@(@8@<@8@D@<@8@0@B@4@4@8@D@@@<@8@@@B@@@@@@@@@8@<@@@4@8@B@<@4@<@B@@@<@0@D@@@@@4@0@8@@@B@<@<@8@@@4@0@@@<@0@8@<@B@@@H@8@D@8@<@D@8@@@8@8@@@D@8@<@8@8@<@8@8@@@4@P@@@8@<@F@@@4@<@@@D@<@<@B@8@@@B@<@8@4@B@4@@@D@8@@@4@0@B@<@8@4@8@8@@@@@8@(@@@4@@@4@<@@@<@B@<@@@<@<@4@B@<@@@8@4@4@8@@@4@@@<@<@<@4@8@@@4@B@@@\@@@B@k@<@B@H@8@8@8@B@4@0@@@<@4@0@B@U@8@8@R@<@B@<@F@B@8@<@8@<@@@D@<@<@@@B@4@8@<@<@<@8@8@D@<@4@<@B@4@@@@@F@<@<@8@(@@@8@@@8@@@B@4@D@8@0@<@D@8@8@B@<@<@@@4@4@8@8@@@4@<@B@8@@@B@Q@8@8@B@H@<@(@4@<@4@8@H@4@B@<@B@<@H@<@8@0@8@8@4@@@8@<@4@4@@@<@4@<@8@<@4@<@8@<@@@8@B@4@@@4@B@4@<@<@J@B@N@L@F@L@B@P@F@B@F@H@F@H@Q@F@D@N@H@H@D@J@H@J@L@Q@@@H@N@L@D@J@L@N@L@D@J@B@L@N@J@L@N@N@N@(@H@F@H@L@N@F@J@J@S@N@H@L@8@N@L@Q@N@<@F@P@P@J@8@H@@@P@N@H@L@B@N@J@D@B@P@P@H@L@L@L@H@N@Q@N@L@H@L@L@H@J@H@H@N@R@F@Q@J@D@H@N@J@J@J@F@D@B@P@F@<@D@J@J@H@R@J@L@S@B@L@Q@P@F@N@J@H@H@L@N@N@N@F@J@L@@@L@J@N@H@F@N@H@P@Q@N@[@D@L@N@@@L@H@P@Q@J@L@N@L@F@P@J@H@B@P@H@Q@J@H@J@N@@@P@P@@@H@N@F@B@L@B@N@H@L@N@D@N@J@R@J@N@N@J@F@@@J@N@R@P@F@H@L@L@J@P@L@J@D@J@P@N@F@N@N@F@N@L@R@F@N@Q@F@L@P@F@L@P@P@N@F@Q@N@L@B@D@Q@N@J@L@N@Q@R@J@P@P@Q@@@F@D@8@L@J@D@F@N@B@F@L@H@H@H@R@T@N@<@L@D@D@L@D@H@<@L@D@F@H@L@L@F@N@J@H@Q@N@L@@@N@Q@L@N@L@P@[@N@D@P@V@H@8@P@H@J@P@P@]@N@L@F@J@F@J@F@P@L@F@0@L@P@H@J@D@L@H@L@H@Q@H@F@J@N@F@N@J@W@L@F@B@P@4@8@J@@@N@J@H@N@D@F@N@L@H@4@J@N@N@0@J@D@S@@@<@4@@@@@B@D@D@D@N@F@<@<@B@F@D@4@(@B@8@4@<@@@B@8@F@0@<@@@R@B@B@B@<@B@F@B@J@Q@@@(@@@T@J@L@F@F@(@(@<@0@Y@8@(@ @(@Q@(@(@R@<@4@(@R@P@0@4@4@0@(@(@R@ @0@(@0@0@R@0@0@4@4@(@(@(@@0@4@8@4@4@(@4@0@0@0@H@0@4@(@(@(@0@4@0@4@0@ @0@4@4@(@(@ @<@<@8@0@4@P@<@8@0@(@0@4@(@(@(@0@(@8@(@4@(@0@4@0@8@0@4@(@@(@4@ @<@0@(@ @8@ @(@0@8@0@(@0@H@8@4@4@U@4@0@0@4@(@(@8@0@ @0@8@8@S@ @8@4@0@ @@0@4@8@0@4@(@4@ @ @4@0@ @(@0@8@4@<@(@<@(@0@4@(@4@(@(@4@<@0@0@(@(@0@(@(@4@@J@4@T@0@4@4@ @0@4@8@0@4@4@(@4@<@0@(@(@8@(@4@8@0@4@(@ @4@4@0@(@Q@0@4@4@0@@4@(@8@ @0@4@0@8@0@4@0@0@(@4@0@4@(@(@(@R@4@ @4@0@0@(@(@0@0@ @8@4@W@4@8@]@R@8@<@0@ @ @8@(@ @4@0@(@ @V@Q@(@(@H@0@8@R@@@8@S@0@0@0@4@8@0@0@4@4@(@D@S@0@0@S@N@8@P@(@0@0@ @4@4@<@0@0@(@D@4@0@S@(@4@8@(@4@(@ @4@8@0@ @4@U@4@0@T@(@0@(@4@(@0@8@0@4@4@N@(@(@4@V@0@@(@4@N@0@@@(@8@(@8@0@J@0@(@Q@(@0@(@4@(@X@(@J@V@4@ @4@ @4@ @0@(@0@4@(@Q@(@4@(@J@L@0@0@ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?4@4@N@R@0@H@L@N@S@R@€`@^@N@H@N@4@N@U@P@V@V@P@R@Q@T@X@Q@U@U@U@J@N@N@R@<@U@P@T@\@R@B@R@F@D@@@D@D@D@@@8@8@8@B@0@B@B@H@B@<@4@8@F@B@D@D@@@<@4@@@B@B@(@8@@@8@B@0@F@D@B@4@@@<@B@D@B@B@<@8@<@4@D@F@F@D@8@<@@@D@@@J@B@B@8@B@<@<@B@B@@@D@@@<@@@B@F@@@4@8@F@<@H@F@H@8@0@@@4@D@@@J@D@D@(@H@D@B@<@<@F@D@@@D@J@B@@@@@<@@@B@F@D@@@F@B@<@4@@@F@J@8@H@@@B@<@F@@@<@B@F@B@D@B@@@F@D@B@@@D@B@B@D@<@@@@@4@@@<@@@D@@@<@B@@@F@B@F@4@<@<@D@F@B@B@@@J@<@@@<@B@D@8@<@B@H@B@4@J@<@F@@@8@8@N@<@F@@@<@B@B@D@D@B@B@@@@@D@4@B@<@D@B@B@@@L@D@8@D@D@D@F@F@B@8@@@F@D@J@<@F@J@D@B@D@D@H@@@<@H@B@@@8@@@4@@@B@D@F@B@B@D@8@8@B@F@D@B@0@B@D@F@F@H@B@F@D@D@@@F@B@@@B@D@H@D@D@F@@@<@D@<@J@F@B@a@8@H@N@k@D@L@J@@@8@D@B@@@@@@@8@<@F@B@<@8@N@F@H@@@0@B@L@@@B@H@4@F@F@D@F@D@D@N@8@8@@@B@<@<@4@F@@@D@8@D@H@D@8@D@8@F@@@B@<@D@F@8@H@F@N@F@8@H@@@8@<@<@D@B@F@@@D@@@<@D@J@<@8@8@@@L@F@@@B@H@<@D@F@R@@@F@B@B@@@H@H@8@<@D@<@@@J@<@F@F@4@B@@@D@@@B@D@@@B@<@B@8@D@8@D@@@@@8@F@F@B@8@B@F@@@B@D@F@<@<@F@0@@@U@B@F@J@J@<@F@J@N@L@H@H@F@L@H@@@8@B@F@H@P@8@H@B@<@F@F@P@H@F@H@D@F@D@F@H@F@P@H@J@<@@@J@J@B@F@H@J@P@L@B@D@J@D@J@H@J@L@J@L@8@F@B@H@B@L@L@B@F@H@N@L@J@J@H@L@J@J@N@H@N@H@F@L@W@R@J@J@F@@@D@<@F@R@F@D@H@H@P@B@H@L@F@J@B@B@F@J@L@F@L@L@J@F@<@F@H@F@B@4@F@D@D@F@F@L@@@N@D@F@D@D@H@@@F@D@L@J@N@F@B@J@J@L@L@D@H@L@H@H@L@F@F@H@N@B@D@P@D@F@J@H@P@J@L@8@P@J@H@J@8@H@L@J@@@L@F@F@L@P@N@[@N@<@P@H@B@P@L@H@J@H@J@J@F@D@J@Y@J@D@J@F@H@N@F@B@H@J@H@@@L@B@<@N@L@@@@@D@B@F@B@B@J@N@B@F@P@F@H@J@F@L@@@8@H@H@J@F@D@N@F@L@F@H@J@H@H@N@D@H@F@F@@@L@H@H@H@D@P@@@L@H@<@F@J@4@H@N@J@J@F@F@N@N@J@F@4@<@P@L@J@H@L@J@J@L@P@L@U@(@L@@@B@D@D@N@D@H@H@@@J@8@F@@@H@H@J@P@H@4@F@@@J@D@J@J@J@B@J@4@F@H@@@D@B@L@N@F@P@H@H@N@J@J@P@J@D@L@L@L@N@Y@L@P@D@J@Q@D@D@8@D@H@B@L@J@L@J@Z@N@F@N@F@D@F@D@F@H@J@L@F@F@@@L@D@F@L@F@F@J@@@H@J@H@D@B@J@L@H@J@J@T@F@H@N@P@D@<@J@4@H@@@D@N@D@P@H@F@4@@@F@P@H@L@F@<@F@D@N@4@8@4@4@4@F@F@4@B@4@4@ @B@8@<@4@<@D@@@0@0@(@<@F@H@4@8@R@H@<@@@0@@@<@4@X@4@<@D@0@0@(@4@<@8@8@@@4@4@S@0@4@8@8@8@4@0@(@4@8@4@4@(@4@(@4@ @<@<@8@L@4@0@8@8@8@8@0@(@0@(@8@<@8@<@(@4@8@8@4@@@8@8@0@4@0@0@4@8@0@<@4@0@4@4@8@4@(@0@8@0@@@<@@@(@ @4@(@8@T@B@8@8@@@@8@8@0@0@<@8@4@8@B@4@4@4@0@4@4@<@8@4@@@8@4@(@4@<@D@0@<@4@8@0@<@4@0@8@<@4@<@<@B@@@8@8@4@<@8@4@8@0@4@4@R@4@0@4@<@4@0@8@4@@@4@@@(@0@0@8@@@4@8@8@B@0@4@0@8@4@(@0@8@8@8@ @@@0@@@0@0@0@D@0@8@4@0@4@<@8@4@4@4@4@4@4@(@8@0@8@8@8@4@D@8@(@<@4@<@<@8@8@(@4@8@<@B@0@@@B@8@8@<@8@<@4@4@@@4@4@0@4@R@4@8@8@<@4@8@<@S@0@4@<@<@8@ @4@<@8@<@B@4@@@8@8@4@<@8@4@4@<@<@8@0@8@4@0@8@0@D@<@8@B@(@<@F@T@8@D@B@4@(@8@8@0@(@4@(@0@@@4@0@(@<@@@@@4@ @8@D@4@4@<@Q@8@@@<@@@8@8@<@(@P@4@8@0@4@(@<@0@8@0@8@<@8@(@<@(@@@4@8@4@J@@@0@<@@@H@@@(@@@4@(@0@0@<@4@@@4@8@T@4@8@B@4@(@(@0@<@<@4@4@@@4@<@@@L@U@@@4@8@4@R@<@0@4@<@0@4@B@0@@@@@(@8@4@<@4@4@0@4@8@0@8@0@8@(@8@4@4@L@B@@@8@0@4@@@4@8@8@<@4@0@<@ @4@N@4@8@ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?8@8@4@0@T@T@4@0@8@0@U@P@U@W@P@T@X@S@Q@U@T@V@4@T@V@V@S@@@N@H@F@H@J@H@@@B@B@<@D@<@D@F@J@J@H@8@@@D@J@H@D@F@D@D@@@B@P@H@F@B@B@B@F@D@B@H@J@N@J@H@D@N@J@F@B@B@F@D@@@@@D@J@S@H@F@B@D@F@D@D@F@H@J@F@B@D@J@H@D@L@H@D@H@D@J@J@B@D@J@J@B@L@@@4@N@N@4@@@B@F@@@D@B@J@H@H@<@H@H@V@@@B@L@F@H@B@J@D@B@D@B@B@D@F@N@D@H@H@@@@@@@F@N@@@J@@@N@B@H@F@J@B@D@D@F@D@F@J@D@J@J@H@F@F@J@F@J@H@F@H@4@J@F@F@H@H@B@N@D@F@H@L@H@@@@@D@F@H@F@D@L@D@D@F@H@D@H@F@L@D@D@8@N@B@<@<@@@0@D@N@B@J@F@B@H@D@J@F@J@F@H@L@F@<@D@L@D@B@F@D@J@H@@@F@J@F@H@F@D@@@H@J@F@L@D@J@N@H@F@F@F@J@D@D@L@D@F@D@4@D@<@D@D@N@F@D@D@D@@@H@D@J@F@D@D@F@D@H@J@L@H@F@F@H@D@Q@F@D@J@H@J@H@H@F@H@B@H@B@J@L@F@V@H@J@L@€`@L@L@L@D@D@F@F@F@L@B@@@B@F@B@B@@@@@H@L@@@8@F@F@L@H@H@J@J@D@D@F@J@H@N@D@B@H@L@<@J@@@F@J@J@D@J@H@4@F@B@F@F@L@B@B@J@<@F@H@@@J@H@Q@F@J@J@B@<@B@H@H@J@D@F@D@J@F@D@J@J@B@B@@@D@S@H@B@F@L@<@B@4@H@N@D@F@F@D@B@J@D@H@P@B@F@N@H@F@L@@@H@F@8@D@B@D@J@L@F@<@H@B@H@@@P@B@H@@@J@J@H@@@F@L@B@N@H@F@D@@@R@<@L@F@F@H@H@H@4@D@H@N@H@F@F@H@J@H@F@B@4@4@J@F@J@<@@@F@<@P@D@L@H@B@J@J@D@B@@@@@D@@@J@F@<@<@@@F@D@B@@@F@F@D@H@H@D@F@H@F@@@H@J@F@F@B@<@B@@@@@H@H@4@@@F@8@L@F@D@D@@@L@L@F@B@F@L@F@J@Z@F@H@\@U@N@B@D@D@L@@@F@B@Q@F@D@N@J@8@H@D@F@H@D@<@D@H@J@8@N@B@J@H@@@F@D@B@B@8@@@4@F@@@F@@@<@H@B@J@D@N@D@F@<@<@H@<@H@H@J@D@D@F@F@H@H@J@@@F@F@F@S@F@@@D@P@B@@@@@D@D@H@H@N@J@H@B@N@F@B@D@D@<@H@B@F@J@D@@@L@P@Q@[@L@F@<@N@B@@@J@D@F@F@F@@@L@<@4@L@X@J@D@F@F@F@L@F@@@D@L@B@D@H@<@D@H@F@4@8@@@@@D@<@J@F@L@@@B@H@D@J@J@F@H@L@8@F@4@J@<@L@B@D@F@H@D@F@F@H@F@D@L@F@F@D@F@0@J@F@H@J@4@L@@@D@B@B@@@H@F@B@J@J@H@H@D@D@<@@@L@H@ @<@B@N@H@D@F@H@D@D@H@H@H@W@T@H@8@L@D@@@N@B@D@F@N@B@B@F@B@F@D@H@J@D@<@@@B@D@B@F@F@L@F@B@D@@@D@@@H@H@4@@@L@@@J@L@F@J@N@F@H@N@J@B@L@8@J@L@V@D@F@F@<@H@P@<@B@ @H@D@D@J@F@H@D@Y@L@F@J@B@D@H@Z@@@J@J@J@F@B@N@0@B@4@4@J@F@L@F@D@D@F@B@B@N@D@H@J@H@@@H@U@@@D@U@N@0@X@D@D@F@<@0@L@B@N@H@8@<@H@@@L@4@J@8@Q@D@B@L@8@4@S@4@H@8@B@@@@@@@@@@@F@8@8@0@8@D@8@<@B@@@4@(@0@<@@@@@8@<@8@8@4@8@D@@@<@8@4@8@0@<@4@@@@@B@L@<@8@F@@@<@8@8@8@8@4@B@<@B@8@<@<@4@<@<@8@8@8@@@8@<@4@<@B@8@8@D@@@4@8@8@B@B@8@<@B@B@8@D@0@(@D@F@(@0@4@<@0@8@8@@@8@@@0@@@@@@@4@8@@@<@8@8@@@8@4@<@8@8@8@@@D@4@B@@@4@4@4@<@D@B@<@4@D@8@<@@@B@8@8@8@@@8@@@B@8@B@B@@@<@<@@@@@B@@@8@@@S@@@<@<@B@@@8@B@8@@@<@B@D@4@4@<@@@@@<@8@D@<@8@<@<@D@<@<@F@<@<@(@D@4@D@0@0@ @8@F@4@D@<@4@F@@@B@<@<@@@<@B@<@0@8@D@<@8@@@8@B@@@4@<@B@<@@@<@<@4@@@B@<@@@8@B@D@@@<@<@<@@@8@<@D@8@8@8@(@8@0@8@<@8@<@8@8@8@8@0@<@@@<@8@<@8@<@<@B@D@<@<@<@@@8@H@<@8@@@@@B@@@B@<@@@4@B@4@B@B@<@@@4@D@D@V@D@F@F@8@8@<@@@<@D@8@0@8@@@8@8@4@0@B@D@8@0@<@<@F@@@<@B@@@<@F@@@B@@@F@4@8@<@B@0@<@4@<@<@<@8@@@@@(@<@8@<@@@F@(@8@@@4@<@@@4@@@@@J@<@D@B@8@0@4@@@B@@@8@@@4@@@8@<@B@B@8@4@4@4@H@@@8@<@B@0@8@(@B@F@<@<@@@8@J@<@8@<@F@8@<@F@@@@@D@4@@@@@0@<@8@8@D@<@@@4@8@4@B@0@H@8@8@4@D@B@@@4@<@D@8@B@@@<@8@0@L@0@B@@@<@@@ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?8@4@8@4@U@4@0@8@4@0@4@8@0@<@4@8@4@4@4@8@4@T@L@T@B@8@(@0@0@S@4@4@8@8@8@(@V@0@0@(@F@(@8@0@0@0@4@V@4@<@8@J@H@F@L@H@J@D@F@F@L@@@B@H@N@H@H@L@H@F@N@H@F@H@F@H@B@L@H@F@F@H@H@D@F@B@F@J@H@J@N@J@D@F@N@F@@@B@H@F@H@D@F@F@N@H@H@H@F@H@H@@@H@F@L@J@F@D@F@N@B@J@F@H@J@L@B@L@F@F@J@D@H@H@J@L@H@F@<@U@J@H@D@D@F@J@H@F@F@H@H@W@@@N@L@H@B@H@F@J@F@B@D@H@H@B@D@J@D@J@H@B@F@F@F@H@@@H@D@L@F@J@F@F@F@H@H@F@H@D@H@D@F@J@H@F@B@J@D@F@H@N@F@N@J@F@J@F@P@D@F@F@B@L@J@J@B@H@B@H@J@F@H@D@F@D@J@F@D@J@H@H@@@H@@@L@F@B@P@F@B@F@N@J@F@H@N@H@H@F@H@H@D@J@J@H@D@N@L@F@D@F@F@J@D@F@H@F@F@L@H@F@<@H@P@F@F@F@P@D@J@L@H@F@F@H@J@H@F@J@D@L@F@U@B@8@F@F@J@0@F@F@D@F@D@F@J@D@N@J@H@F@L@F@F@J@J@H@F@D@H@F@L@H@J@L@H@J@H@F@F@B@H@H@B@F@J@H@F@J@N@H@H@U@H@H@H@D@H@F@J@<@F@F@B@N@F@D@B@H@D@B@F@F@D@@@H@F@L@F@J@H@D@L@B@B@F@H@F@D@F@H@H@N@B@H@F@D@H@P@D@F@H@B@H@S@H@H@J@0@F@D@H@D@H@J@B@H@F@L@J@F@H@F@F@F@H@D@F@F@F@F@J@S@F@8@H@H@H@H@F@D@V@H@B@H@L@N@B@@@F@R@D@D@H@B@B@F@H@H@H@N@D@F@J@H@L@H@L@F@H@D@D@L@H@H@N@D@@@F@H@F@H@P@J@H@D@<@F@J@F@D@H@F@L@Q@H@J@F@H@J@@@P@J@J@H@J@F@V@H@J@D@F@H@@@D@L@L@J@(@(@8@<@D@J@4@<@L@8@H@D@Q@8@J@L@Q@B@D@B@D@F@8@N@H@8@8@8@D@B@4@H@F@F@D@B@F@F@D@H@(@H@@@D@L@L@F@B@D@D@B@0@F@L@8@@@D@8@L@8@H@8@D@J@L@H@F@H@J@J@D@W@D@F@^@N@J@F@D@F@N@B@D@D@N@F@F@N@4@4@L@B@D@F@F@F@<@B@B@J@4@N@L@H@H@B@F@D@B@F@<@B@D@B@J@D@D@B@L@F@H@D@Q@D@F@8@@@N@8@F@F@H@F@H@F@B@D@H@0@4@J@B@N@H@D@H@Q@8@<@B@B@8@J@F@P@Q@D@D@D@H@@@B@F@D@D@B@J@F@B@D@J@Q@L@Z@D@B@D@@@B@D@B@D@L@L@F@8@J@4@4@N@S@B@D@L@D@H@L@J@@@D@N@D@D@F@F@H@D@B@J@0@<@(@D@8@N@8@J@@@@@L@L@H@J@H@L@D@F@8@J@D@L@J@L@B@H@D@F@B@F@D@H@F@F@F@L@L@H@D@(@J@F@J@H@4@F@4@D@<@H@B@H@Q@H@B@H@D@L@L@@@D@N@8@F@L@L@B@D@R@L@D@J@<@L@B@D@F@<@L@X@T@H@4@T@F@F@N@8@B@J@N@D@D@J@<@@@F@F@F@H@D@@@H@4@D@@@B@D@J@J@B@@@8@D@B@N@H@@@L@F@Q@H@J@D@D@B@L@4@P@H@J@D@N@H@Y@F@D@H@D@F@P@D@D@J@4@D@F@F@D@H@B@[@N@H@F@D@ @N@X@@@D@J@N@J@B@F@_@0@B@D@B@N@F@J@F@8@B@@@@@B@L@H@4@H@J@D@N@V@8@8@T@J@4@R@D@D@N@B@B@4@H@F@L@H@4@D@B@8@J@B@H@4@Q@D@D@4@ @0@ @8@0@(@4@4@0@ @(@8@0@0@D@0@B@@@<@B@@@B@<@<@@@D@D@8@@@B@8@<@D@@@@@B@@@@@@@<@<@8@B@<@<@<@@@@@8@<@8@<@B@@@@@B@B@<@F@B@<@4@8@@@8@@@8@<@<@N@<@<@B@@@@@@@4@<@<@B@@@<@<@8@B@8@@@<@@@@@D@B@B@@@<@B@F@@@@@B@D@F@8@4@4@B@@@8@8@8@@@@@<@<@@@@@J@4@B@@@@@4@@@@@@@<@D@8@@@@@B@8@@@8@@@@@8@@@@@<@@@B@H@8@D@<@B@<@F@<@@@@@<@@@<@@@8@<@B@@@<@H@@@8@<@@@F@<@B@<@<@@@<@<@<@F@<@F@B@@@B@4@@@8@@@B@<@@@D@<@<@@@<@F@B@<@@@D@@@4@B@<@F@H@<@0@@@F@@@<@@@D@H@@@@@@@8@<@@@@@@@0@D@B@<@<@@@<@@@<@<@@@<@<@B@@@@@H@@@F@<@B@<@F@B@@@B@@@8@<@@@@@@@8@@@8@D@<@H@8@(@<@@@B@ @@@@@8@<@F@<@<@<@D@@@<@<@B@<@<@B@@@<@<@<@@@<@B@<@<@D@<@B@<@<@<@D@<@@@F@<@B@H@H@8@D@D@@@8@@@B@<@8@<@<@B@0@D@<@8@F@<@<@8@<@B@@@<@<@8@4@B@@@D@<@@@@@N@B@@@F@<@@@<@D@<@<@@@D@B@B@<@<@D@H@8@F@@@8@@@@@@@@@B@ @<@@@<@8@<@@@8@<@<@B@D@H@@@<@8@<@@@8@<@<@<@<@@@<@<@D@@@@@@@<@@@F@F@<@4@B@@@B@8@0@8@H@@@<@@@4@8@8@<@@@<@B@8@<@B@<@D@@@@@8@@@<@8@D@@@B@D@<@4@@@@@@@@@F@B@@@8@4@@@@@<@<@<@<@D@H@@@@@<@@@B@4@F@D@B@@@ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?4@8@0@0@0@4@8@0@@@0@(@(@(@4@4@V@(@0@<@4@<@4@<@4@8@0@0@4@8@4@<@4@4@8@(@0@4@4@8@<@0@8@4@4@4@0@0@T@0@<@4@8@(@4@0@(@0@8@0@B@4@0@4@<@8@8@8@8@<@0@8@0@4@4@0@B@4@8@4@0@4@0@4@0@4@8@T@8@4@4@0@<@8@0@0@<@4@0@8@0@0@J@H@F@N@J@F@D@H@F@J@B@D@F@J@F@H@J@H@H@P@D@D@H@H@J@F@L@H@J@F@J@F@D@D@B@H@J@H@J@Q@H@B@F@H@H@<@B@H@H@H@D@D@D@J@F@N@H@F@H@F@8@F@H@J@H@D@F@D@L@D@L@H@D@J@L@B@J@F@H@J@F@H@B@J@W@H@F@X@^@J@D@F@D@D@J@D@H@H@H@F@R@@@J@L@H@P@F@F@L@H@@@D@D@D@B@D@H@D@H@J@D@F@H@H@H@@@H@H@L@D@J@H@D@F@F@H@J@F@D@J@F@F@H@F@F@D@F@D@D@D@J@F@J@J@F@H@H@S@F@H@H@F@N@H@J@B@D@B@F@F@F@H@D@H@F@L@F@F@J@J@H@B@F@B@F@F@@@Q@[@F@F@N@H@D@H@J@H@L@H@F@L@F@H@<@J@F@a@J@J@B@B@F@H@F@D@H@B@H@H@F@H@F@D@D@H@L@F@F@H@P@D@P@J@L@D@H@H@H@H@L@H@F@L@F@U@D@B@B@B@F@D@N@H@H@D@4@D@L@N@D@J@H@F@F@L@D@H@F@H@H@D@F@D@H@L@J@J@J@H@H@F@F@D@F@J@J@F@F@J@L@H@J@N@L@J@@@H@H@S@B@H@<@F@D@F@Q@F@F@L@F@@@B@H@D@8@D@D@H@@@H@H@J@D@F@H@D@J@@@F@D@F@F@D@F@H@H@J@B@F@H@D@H@H@F@F@H@F@F@P@J@B@H@F@H@J@J@R@J@F@D@F@H@J@F@F@F@B@]@H@H@D@B@F@F@F@J@B@F@H@J@F@H@L@H@F@^@J@D@F@L@J@B@D@D@R@F@F@H@@@F@@@F@F@J@J@D@B@D@H@D@D@N@B@H@J@B@D@H@J@J@F@D@H@J@B@J@N@@@H@D@@@D@J@D@F@H@F@D@Q@F@J@F@D@J@D@J@H@H@J@H@D@S@H@F@D@F@F@<@B@L@J@J@(@(@B@B@B@J@8@<@L@B@F@B@Q@D@L@H@Q@B@F@D@D@H@@@L@F@4@B@H@B@4@H@<@D@B@@@H@D@J@N@F@B@J@J@B@@@D@B@B@(@D@J@8@@@@@J@4@H@B@D@H@H@F@F@F@H@B@4@B@J@S@P@H@B@F@L@B@J@B@P@D@D@N@B@L@B@D@D@B@D@@@<@H@4@R@L@F@H@@@L@B@H@D@B@@@D@@@H@D@B@@@L@F@F@B@L@B@D@@@P@J@0@N@F@F@F@F@D@B@H@F@4@4@H@B@L@J@D@F@N@B@<@@@J@D@H@N@N@Q@D@D@@@D@<@@@D@D@B@F@F@H@@@@@H@Q@H@L@B@@@D@<@<@B@D@D@<@H@F@H@0@F@4@P@B@D@L@D@L@F@H@8@<@F@L@@@D@F@F@F@B@<@0@H@ @@@0@4@J@4@B@4@J@H@H@H@F@J@B@H@F@L@B@L@P@N@<@F@8@D@F@D@B@F@D@F@F@L@J@H@D@(@H@D@J@F@@@D@0@D@<@L@B@H@P@J@@@D@J@L@D@Q@8@S@<@D@L@D@H@L@<@L@D@B@B@J@X@R@F@B@F@B@D@J@4@H@H@N@D@D@J@H@@@F@B@L@D@H@8@F@0@B@@@F@J@L@@@B@<@0@D@<@H@F@<@J@D@L@N@B@D@H@H@Q@N@@@Q@B@L@H@F@D@F@D@F@N@D@D@S@L@D@F@D@J@@@L@@@S@J@F@D@D@L@X@8@B@N@L@H@@@F@L@(@0@B@L@F@H@F@B@R@<@F@@@N@H@^@F@H@D@L@T@4@R@V@F@4@R@B@F@L@H@B@0@D@D@J@H@4@L@D@4@J@B@D@@@N@D@D@N@0@0@4@0@@@<@<@B@<@@@8@<@<@@@D@<@@@J@B@<@@@<@<@D@D@8@<@<@@@8@@@8@@@<@@@<@8@8@B@<@@@<@@@D@@@4@D@H@@@0@4@<@<@@@4@8@B@L@D@@@@@<@<@B@8@8@<@@@<@4@<@@@B@8@@@<@D@B@B@B@@@B@@@<@D@<@F@@@€`@H@F@Q@R@B@<@<@8@8@@@D@<@@@<@8@H@4@<@<@@@D@8@F@@@<@D@8@8@B@B@8@<@8@<@@@8@@@@@<@<@B@J@@@D@<@@@<@F@<@8@<@<@8@8@@@<@8@<@<@8@F@8@4@D@4@@@<@<@@@8@<@<@@@<@F@@@H@D@B@@@4@8@B@<@<@<@@@D@4@8@@@8@F@B@<@<@D@8@8@<@<@F@H@U@@@8@F@D@F@<@@@F@D@<@<@<@@@<@0@<@<@Z@@@@@8@8@<@F@D@8@D@4@<@8@<@D@8@8@H@<@B@8@D@<@L@4@@@D@B@D@D@<@B@@@B@<@<@B@<@D@8@4@8@B@H@8@F@@@<@D@F@F@<@B@8@@@<@8@<@@@8@<@<@<@<@B@<@8@@@B@@@@@B@@@@@8@<@8@F@<@@@F@<@B@H@H@8@D@F@@@0@8@@@D@8@<@D@<@D@<@F@<@8@B@<@B@4@<@F@@@8@B@@@4@@@@@B@8@8@@@J@@@B@F@F@D@D@B@<@<@<@@@D@<@@@8@D@<@8@F@@@<@<@<@@@D@<@<@B@@@B@8@<@<@8@<@<@@@@@H@8@D@P@<@@@8@D@<@<@@@@@4@<@<@<@D@<@@@@@J@Q@@@8@<@B@@@8@4@4@<@@@<@<@D@<@4@D@8@<@@@8@B@8@<@H@<@B@D@<@@@4@D@<@@@@@<@8@@@@@4@@@8@D@8@<@4@4@@@B@8@<@8@D@F@@@@@8@<@@@8@<@@@<@B@ð?ð?ð?ð?ð?ð?ð?4@4@4@8@0@0@4@(@4@4@8@0@<@4@0@(@0@(@8@4@V@4@4@@@8@@@8@0@8@8@0@8@4@0@4@0@4@(@8@4@8@(@4@8@8@(@0@8@0@(@4@@@0@8@0@4@8@8@<@@@0@4@8@(@4@<@0@0@0@8@8@4@Q@4@8@4@@@4@4@8@4@0@0@<@8@0@8@8@<@<@8@8@4@@@0@4@4@4@8@4@4@<@0@<@8@4@8@4@4@0@0@8@B@D@0@8@4@4@0@4@8@4@0@8@0@0@<@<@(@4@4@0@0@0@8@0@0@(@4@0@0@L@H@F@P@J@B@F@H@D@F@D@D@F@H@P@H@H@J@P@Q@F@D@H@L@J@N@N@H@P@F@H@F@D@H@D@Q@H@J@J@Q@H@B@F@H@H@@@D@H@J@H@D@F@H@L@F@N@J@H@H@F@8@B@F@J@H@J@F@F@L@D@Q@P@S@L@J@B@J@F@H@Q@F@N@@@L@€b@H@F@[@^@J@D@F@D@F@H@F@J@J@H@B@S@B@N@R@F@Q@H@H@N@J@B@D@@@D@B@S@F@D@H@L@D@F@H@H@H@@@J@H@P@B@H@S@D@F@Q@F@J@D@F@T@D@F@S@F@D@D@F@D@B@B@H@B@J@Q@L@H@J@T@F@H@J@S@L@H@J@B@D@B@L@J@D@F@D@N@F@N@B@H@H@N@J@F@N@D@P@J@B@J@\@F@L@P@J@F@F@J@H@N@P@D@R@F@H@Q@H@F@€`@H@J@B@<@H@J@F@D@H@F@J@L@D@H@F@D@H@J@L@F@H@S@N@F@P@J@N@T@H@H@J@H@N@F@J@L@F@T@D@D@B@D@F@F@P@F@H@H@H@D@L@R@B@H@J@D@F@J@F@F@F@J@H@D@F@D@J@U@L@J@J@J@J@H@D@H@H@L@J@Q@L@L@L@J@H@P@N@Q@@@L@H@S@F@J@B@D@D@Q@S@H@J@V@H@B@B@H@D@8@J@D@D@B@H@H@Q@D@F@H@F@L@D@J@H@F@N@F@F@H@F@H@N@H@H@D@H@D@R@F@F@D@F@P@L@H@J@(@H@J@Q@J@R@N@N@F@F@H@L@@@J@F@F@^@H@H@H@D@H@D@F@J@B@F@H@N@H@J@L@D@H@b@H@F@D@L@J@B@^@H@N@F@D@H@B@H@<@F@S@J@L@D@F@H@H@J@B@N@D@F@J@B@D@H@J@J@D@T@H@J@D@R@B@@@H@B@<@H@J@D@H@H@L@D@R@D@F@D@D@J@D@J@F@F@H@F@R@D@H@L@D@8@B@J@B@H@@@B@4@B@H@@@(@B@L@P@<@D@B@@@L@D@0@@@F@B@H@4@B@F@B@F@J@D@F@H@@@D@B@@@H@8@F@D@F@F@D@D@@@H@N@J@0@<@D@N@@@H@N@B@H@L@<@B@4@<@F@0@L@D@F@J@B@D@8@8@B@<@D@J@J@D@H@B@@@@@H@H@J@F@B@D@D@F@J@H@H@H@D@D@@@H@B@F@H@Q@J@B@B@B@@@D@B@B@4@N@D@H@F@D@B@B@D@F@F@(@4@N@B@J@@@H@8@8@8@B@J@B@D@F@F@8@<@F@<@<@F@F@D@F@D@J@B@F@<@L@J@N@P@4@@@B@D@L@D@D@F@F@L@J@H@B@(@F@B@H@B@(@B@8@N@H@L@F@B@D@J@B@Q@P@4@F@F@H@J@8@H@V@N@D@@@N@4@0@J@F@F@B@B@H@H@B@F@@@4@D@0@B@B@H@J@<@B@<@B@4@F@J@D@J@R@B@D@P@L@F@P@J@F@B@B@B@D@P@B@B@R@L@D@H@L@H@4@J@B@J@4@B@R@L@F@@@F@L@@@J@B@D@S@D@<@L@D@[@D@F@B@L@@@0@Q@V@4@D@R@F@D@@@(@B@D@F@4@L@D@8@H@@@J@@@L@@@D@4@4@<@@@<@8@D@@@D@8@8@8@F@D@8@<@H@D@<@<@<@<@D@D@D@8@B@<@8@@@<@B@<@<@8@@@8@B@8@@@<@<@B@<@4@D@H@@@0@4@8@<@@@4@4@D@L@D@<@<@8@<@B@8@B@8@<@8@8@8@@@@@8@@@8@H@B@<@B@<@F@<@@@D@<@F@@@Z@J@H@P@R@B@8@<@4@<@<@F@8@B@<@@@F@D@<@<@<@B@B@H@@@@@B@4@@@D@@@D@8@4@<@@@8@@@8@@@B@D@H@<@F@4@<@8@F@8@8@B@8@F@8@<@8@4@8@8@8@D@4@4@D@0@<@4@<@<@B@8@<@<@8@D@@@F@B@D@<@4@@@B@H@D@8@<@D@8@8@@@@@D@@@8@8@F@D@<@D@8@F@F@N@<@<@D@F@H@8@D@F@H@D@8@<@<@8@@@8@<@X@D@<@4@@@<@F@D@8@F@8@<@<@4@F@8@8@F@<@@@8@D@8@N@8@<@H@B@D@F@8@F@8@B@B@@@@@<@B@8@8@8@B@H@@@F@<@8@F@D@4@<@B@4@8@<@4@<@<@<@8@<@<@<@F@<@8@<@@@@@<@@@@@@@<@8@8@F@<@<@H@@@B@J@H@4@D@H@8@@@8@<@F@8@<@4@D@D@B@F@<@8@@@<@D@4@8@F@@@8@D@8@4@<@<@@@4@4@<@J@<@B@H@H@D@F@D@8@<@8@<@D@<@<@4@D@B@4@F@<@8@<@@@<@J@<@F@F@B@<@@@<@@@@@8@8@<@@@4@H@8@D@N@<@<@8@D@<@8@<@<@@@8@<@<@D@@@<@8@H@R@8@4@D@@@<@4@N@B@8@<@8@<@D@<@0@D@8@<@<@8@D@D@<@H@@@@@D@D@@@@@D@<@@@<@8@D@<@<@B@<@4@D@<@4@0@B@<@B@<@B@<@D@H@<@D@8@8@<@8@<@@@B@@@ð?ð?ð?ð?ð?ð?ð?ð?ð?š™™™™™É¿333333ã¿ð?ÍÌÌÌÌÌì?à?à¿333333Ó¿š™™™™™¹?š™™™™™¹¿š™™™™™Ù¿š™™™™™Ù?š™™™™™É¿š™™™™™É?š™™™™™É?š™™™™™¹?à¿à¿š™™™™™¹?à?࿚™™™™™Ù?š™™™™™¹?š™™™™™¹¿ÍÌÌÌÌÌ쿚™™™™™¹?š™™™™™¹?ÍÌÌÌÌÌì¿333333û¿š™™™™™¹?ffffffæ¿333333㿚™™™™™É?š™™™™™¹¿š™™™™™¹?š™™™™™Ù?š™™™™™é¿š™™™™™É¿333333Ó¿š™™™™™¹¿š™™™™™¹¿333333㿚™™™™™É¿š™™™™™¹?࿚™™™™™é¿š™™™™™É?š™™™™™¹?š™™™™™¹¿š™™™™™É¿333333㿚™™™™™¹¿333333Ó¿š™™™™™Ù¿333333Ó¿š™™™™™¹?š™™™™™¹?333333Ó¿š™™™™™¹¿š™™™™™¹¿à¿333333Ó¿333333㿚™™™™™¹?š™™™™™Ù¿ð¿š™™™™™¹?ffffffæ¿333333ã?š™™™™™¹¿à¿š™™™™™É¿š™™™™™¹?š™™™™™Ù¿333333Ó¿š™™™™™¹¿ø¿à¿š™™™™™¹¿š™™™™™É¿š™™™™™Àš™™™™™¹¿š™™™™™É?š™™™™™¹?š™™™™™¹?š™™™™™Ù¿š™™™™™É¿š™™™™™É?𿚙™™™™¹?š™™™™™¹¿š™™™™™É¿333333Ó¿š™™™™™¹¿š™™™™™¹?š™™™™™É¿š™™™™™¹¿š™™™™™Ù?š™™™™™¹?š™™™™™É?࿚™™™™™¹?࿚™™™™™¹¿ð¿š™™™™™¹?š™™™™™¹¿š™™™™™¹¿š™™™™™Ù?š™™™™™é¿š™™™™™É?࿚™™™™™É¿š™™™™™Ù?š™™™™™¹?333333Ó¿š™™™™™Ù?ÍÌÌÌÌÌì?š™™™™™¹¿š™™™™™¹¿ffffffæ?š™™™™™É?š™™™™™Ù?š™™™™™¹?࿚™™™™™É?š™™™™™Ù?š™™™™™É?š™™™™™É¿š™™™™™É¿333333Ó?333333Ó¿333333Ó¿ð?š™™™™™Ù¿š™™™™™É¿š™™™™™¹?š™™™™™Ù¿š™™™™™Ù¿š™™™™™¹¿ÍÌÌÌÌÌì¿ð¿š™™™™™¹¿333333Ó¿š™™™™™¹¿š™™™™™¹¿à¿à¿ffffff濚™™™™™¹¿ÍÌÌÌÌÌì¿à¿š™™™™™Ù?333333ó¿ð¿333333Àš™™™™™¹¿333333Ó¿333333㿚™™™™™¹¿š™™™™™¹?š™™™™™¹¿š™™™™™Ù¿š™™™™™ñ?࿚™™™™™Ù¿š™™™™™É¿ffffffö¿333333Ó¿š™™™™™¹?š™™™™™Ù¿333333Ó¿ð¿333333ã¿333333Ó¿š™™™™™É?333333ã?à¿ÍÌÌÌÌÌì?à¿333333ã?š™™™™™¹¿ð¿š™™™™™é?š™™™™™É¿š™™™™™Ù¿š™™™™™¹¿š™™™™™¹?š™™™™™¹¿š™™™™™Ù¿š™™™™™¹?333333Ó?š™™™™™¹¿333333Ó¿š™™™™™é¿š™™™™™¹?ÍÌÌÌÌÌì¿333333Ó¿ffffffö¿š™™™™™¹?ffffffæ¿à¿ffffffæ¿333333Ó¿ffffffö¿š™™™™™¹?333333ã¿à¿š™™™™™É?š™™™™™É¿š™™™™™é¿š™™™™™¹?333333Ó¿ffffffæ¿ð¿ffffffæ¿ÍÌÌÌÌÌô¿š™™™™™¹?࿚™™™™™Ù¿š™™™™™é¿š™™™™™É?š™™™™™É¿š™™™™™¹¿š™™™™™¹?࿚™™™™™¹?333333Ó¿š™™™™™É¿š™™™™™É?š™™™™™Ù¿š™™™™™Ù¿š™™™™™¹¿š™™™™™Ù¿à¿š™™™™™É¿š™™™™™¹¿š™™™™™¹¿š™™™™™¹¿333333Ó¿š™™™™™¹?à¿à¿à¿ffffffæ?ffffffæ¿333333Ó¿ÍÌÌÌÌÌô¿ffffffö?333333Ó?š™™™™™É¿š™™™™™¹?š™™™™™É¿š™™™™™¹¿333333㿚™™™™™Ù¿š™™™™™É?š™™™™™Ù?š™™™™™¹?𿚙™™™™¹¿š™™™™™É¿333333㿚™™™™™¹?ÍÌÌÌÌÌì?š™™™™™Ù¿š™™™™™É¿à¿š™™™™™Ù?à¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌÀš™™™™™É?333333Ó¿š™™™™™Ù¿ffffffæ¿ffffff濚™™™™™¹¿š™™™™™Ù¿333333ã?š™™™™™¹?š™™™™™É?š™™™™™¹¿š™™™™™Ù?š™™™™™Ù¿š™™™™™É¿š™™™™™¹¿š™™™™™É?š™™™™™¹?à?š™™™™™¹¿š™™™™™¹¿š™™™™™É?333333Ó¿ffffff濚™™™™™é¿š™™™™™¹¿ð¿š™™™™™¹¿à¿š™™™™™é¿š™™™™™Ù¿333333ã¿ÍÌÌÌÌÌô¿š™™™™™é¿š™™™™™¹¿à¿š™™™™™¹?š™™™™™¹?𿚙™™™™¹¿ø¿ffffffö¿333333Ó¿ffffff濚™™™™™É¿š™™™™™Ù¿333333㿚™™™™™¹?š™™™™™É?333333ó¿š™™™™™¹?š™™™™™¹?š™™™™™¹?š™™™™™¹¿š™™™™™É¿"ÀÍÌÌÌÌÌô¿š™™™™™É¿š™™™™™Ù?š™™™™™Ù¿333333Ó¿ffffffö¿333333㿚™™™™™É?ffffff濚™™™™™¹¿š™™™™™É¿š™™™™™Ù?š™™™™™É¿333333ã?333333Ó?š™™™™™É¿š™™™™™¹¿š™™™™™É?333333㿚™™™™™¹¿ffffffæ¿333333Ó¿š™™™™™¹¿š™™™™™¹?š™™™™™¹¿333333Ó¿š™™™™™É¿š™™™™™Ù¿š™™™™™¹?š™™™™™É¿š™™™™™Ù¿à¿š™™™™™É¿333333㿚™™™™™Ù?š™™™™™¹?š™™™™™¹?š™™™™™¹¿š™™™™™É¿ð?333333Ó¿333333ã?333333Ó¿š™™™™™É?š™™™™™É?š™™™™™¹¿333333Ó¿š™™™™™¹¿š™™™™™¹?š™™™™™¹¿à¿š™™™™™¹¿š™™™™™¹?š™™™™™¹?š™™™™™¹?š™™™™™É¿š™™™™™¹?š™™™™™Ù¿š™™™™™Ù¿à¿š™™™™™Ù¿333333ã¿à¿à¿š™™™™™é¿333333㿚™™™™™Ù¿š™™™™™Ù¿š™™™™™Ù¿à¿333333ã¿ffffffæ¿à¿š™™™™™Ù¿ÍÌÌÌÌÌÀ333333㿚™™™™™Ù¿ÍÌÌÌÌÌì¿ffffffæ¿ffffff濚™™™™™é¿š™™™™™é¿š™™™™™Àà¿ð¿š™™™™™Ù¿š™™™™™Ù¿ffffffæ¿333333㿚™™™™™Ù¿ÍÌÌÌÌÌô¿ð¿š™™™™™Ù¿š™™™™™Ù¿333333ã¿333333ã¿ÍÌÌÌÌÌì¿333333㿚™™™™™Ù¿š™™™™™é¿š™™™™™ñ¿ø¿š™™™™™Ù¿ffffffö¿333333ã¿ð¿š™™™™™é¿š™™™™™é¿à¿à¿ÍÌÌÌÌÌÀ࿚™™™™™é¿š™™™™™Ù¿ffffff濚™™™™™Ù¿ffffffæ¿à¿ffffffæ¿333333㿚™™™™™é¿š™™™™™é¿ÍÌÌÌÌÌü¿ø¿š™™™™™Ù¿à¿š™™™™™Ù¿333333ã¿à¿333333ã¿ffffffæ¿ffffff濚™™™™™Ù¿š™™™™™é¿š™™™™™ñ¿ð¿333333ã¿ÍÌÌÌÌÌÀ࿚™™™™™Ù¿š™™™™™Ù¿ffffffæ¿333333㿚™™™™™Ù¿ð¿à¿333333㿚™™™™™Ù¿Àffffffæ¿ÍÌÌÌÌÌì¿333333ã¿ffffffÀš™™™™™Ù¿š™™™™™Ù¿333333ã¿ffffffæ¿ÍÌÌÌÌÌü¿ð¿š™™™™™ñ¿à¿š™™™™™Ù¿à¿à¿ffffffæ¿à¿à¿ÍÌÌÌÌÌì¿333333ã¿à¿š™™™™™Ù¿333333ã¿à¿ffffffæ¿333333ã¿333333㿚™™™™™Ù¿ffffff@ÍÌÌÌÌÌ@@333333@ÍÌÌÌÌÌ@š™™™™™@ÍÌÌÌÌÌü?ffffff@ffffff@ @333333@ffffff@333333@š™™™™™ @ffffff$@ffffff@š™™™™™@š™™™™™ @333333@#@333333@@ÍÌÌÌÌÌ@š™™™™™@ffffff@ @ffffffþ?333333@ÍÌÌÌÌÌ$@ffffff)@333333@ffffff@333333@ÍÌÌÌÌÌ"@ffffff@@ÍÌÌÌÌÌ @ffffff@'@333333!@ @@333333@š™™™™™@š™™™™™@ÍÌÌÌÌÌ@ @@ÍÌÌÌÌÌ @š™™™™™&@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ffffff!@ffffff@333333@š™™™™™#@ffffff@@ffffff@ @333333@@ffffff@ÍÌÌÌÌÌ @@333333@333333@š™™™™™@ÍÌÌÌÌÌ@333333@ÍÌÌÌÌÌ @ffffff@š™™™™™ @ffffff%@ÍÌÌÌÌÌ @333333 @ffffff@ @š™™™™™@333333@333333@š™™™™™@š™™™™™ @š™™™™™ @ÍÌÌÌÌÌ @333333@š™™™™™@333333û?@@ÍÌÌÌÌÌ@ffffff@ÍÌÌÌÌÌ!@@ffffff@š™™™™™@š™™™™™@ @333333$@ @š™™™™™@š™™™™™!@333333@ffffff&@š™™™™™ @ @333333 @ffffff @@š™™™™™@ffffff@š™™™™™@š™™™™™@@š™™™™™@333333!@@ @333333%@333333 @š™™™™™@@š™™™™™@ÍÌÌÌÌÌ@š™™™™™@@333333@ÍÌÌÌÌÌ)@ÍÌÌÌÌÌ@@@@ÍÌÌÌÌÌ @333333@ffffff@ÍÌÌÌÌÌ @ffffff @ÍÌÌÌÌÌ@@ffffff(@ffffff@ffffff@ffffff@ffffff@333333@ÍÌÌÌÌÌ@333333@333333@š™™™™™@@333333@š™™™™™ @333333@ÍÌÌÌÌÌ@š™™™™™@@@333333@ffffff@ÍÌÌÌÌÌ$@š™™™™™@@ffffff@š™™™™™@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ffffff@333333@ÍÌÌÌÌÌ @ffffff@ffffff@333333%@ffffff@@š™™™™™@š™™™™™@333333-@333333"@ffffff@@š™™™™™@333333@333333@333333@333333 @ffffff@ÍÌÌÌÌÌ@$@š™™™™™@ÍÌÌÌÌÌ@333333#@ @333333@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@@ÍÌÌÌÌÌô?,@ffffff @@ÍÌÌÌÌÌ/@@333333û?@š™™™™™@@š™™™™™@@š™™™™™@ÍÌÌÌÌÌô?š™™™™™@ffffff@ffffff @ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@š™™™™™@@@ÍÌÌÌÌÌ @ffffff@ÍÌÌÌÌÌ@333333@š™™™™™@333333 @@ffffff@ÍÌÌÌÌÌ@!@333333 @ffffff @ffffff@333333#@š™™™™™@ÍÌÌÌÌÌ @@ @333333 @š™™™™™&@ffffff@333333@ffffff"@ffffff@@333333 @@š™™™™™#@š™™™™™@š™™™™™ @š™™™™™@š™™™™™ @ffffff@š™™™™™$@š™™™™™@š™™™™™@ffffff&@š™™™™™@ÍÌÌÌÌÌ@333333@ÍÌÌÌÌÌ'@š™™™™™@333333%@@ffffff@ffffff@ffffff@@@333333@š™™™™™@š™™™™™@ffffff@ffffff@ffffff!@ffffff@š™™™™™,@ÍÌÌÌÌÌ@333333@333333@š™™™™™!@ÍÌÌÌÌÌ@ffffff@333333@š™™™™™@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@@333333 @š™™™™™@333333$@333333@@ÍÌÌÌÌÌ@š™™™™™@ffffff%@ffffff-@333333@ÍÌÌÌÌÌ!@š™™™™™'@ffffff @"@š™™™™™@333333!@333333@ÍÌÌÌÌÌ@333333@#@ffffff @ffffff@ffffff@ÍÌÌÌÌÌ(@@333333 @ffffff@333333@ÍÌÌÌÌÌô?š™™™™™@333333@333333#@š™™™™™ @@333333@š™™™™1@š™™™™™ @ffffff@333333@ÍÌÌÌÌÌ#@333333@@ffffff@@333333@@š™™™™™@ÍÌÌÌÌÌ@ffffff@ÍÌÌÌÌÌ@@ÍÌÌÌÌÌ@ffffff@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@@š™™™™™@ÍÌÌÌÌÌ@@333333ã?@@@!@š™™™™™@ffffff@333333@333333!@ÍÌÌÌÌÌ@ffffff@ÍÌÌÌÌÌ)@š™™™™™!@š™™™™™@š™™™™™!@ÍÌÌÌÌÌ@ffffff@ÍÌÌÌÌÌ&@333333@š™™™™™@@š™™™™™@333333@ÍÌÌÌÌÌ @ffffff@$@ÍÌÌÌÌÌ@ffffff"@š™™™™™@ffffff@333333@333333@333333 @ffffff@@333333%@š™™™™™@ÍÌÌÌÌÌ"@š™™™™™ù?ffffff$@fffffæ3@333333)@ffffff@š™™™™™@333333@333333@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@š™™™™™@š™™™™™ @ @@š™™™™™@š™™™™™@ÍÌÌÌÌÌ'@š™™™™™ @š™™™™™@333333û?ÍÌÌÌÌÌ@ÍÌÌÌÌL2@ffffff @333333#@ffffff!@ÍÌÌÌÌÌ$@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ @333333@ÍÌÌÌÌÌ@ffffff@ÍÌÌÌÌÌ@333333@333333@ffffff@333333@ffffff@š™™™™™ @333333@š™™™™™@@š™™™™™@ÍÌÌÌÌÌ@ffffff@333333@@@ffffff@ffffff@333333@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ @@@333333@š™™™™™@ÍÌÌÌÌÌ@ffffff@ffffff@ffffff@ÍÌÌÌÌÌ@ffffff @ÍÌÌÌÌÌ @ÍÌÌÌÌÌ@š™™™™™$@ð¿ffffffÀffffffæ¿ffffffö¿š™™™™™Àš™™™™™ñ¿Àffffffþ¿333333û¿š™™™™™é¿333333ã¿ÍÌÌÌÌÌì¿ffffffö¿333333ã¿ffffffö¿š™™™™™é¿š™™™™™Ù¿ffffffæ¿ffffff Àš™™™™™ñ¿ð¿ÍÌÌÌÌÌ쿚™™™™™ù¿ÍÌÌÌÌÌü¿333333ó¿ø¿ffffffö¿ÍÌÌÌÌÌÀÍÌÌÌÌÌì¿ffffffÀÍÌÌÌÌÌô¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌì¿ffffffæ¿ffffffÀÀÍÌÌÌÌÌü¿Àš™™™™™ù¿š™™™™™ñ¿š™™™™™ù¿333333㿚™™™™™ù¿333333ó¿ ÀÍÌÌÌÌÌì¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌì¿ÀÍÌÌÌÌÌô¿š™™™™™ñ¿ð¿333333û¿ð¿333333ÀÍÌÌÌÌÌô¿ÍÌÌÌÌÌ À333333ã¿ð¿š™™™™™ñ¿ffffffÀš™™™™™é¿à¿333333㿚™™™™™ù¿333333û¿ø¿ÍÌÌÌÌÌ쿚™™™™™ñ¿š™™™™™Ù¿š™™™™™ù¿š™™™™™ñ¿ À333333ã¿ffffffö¿à¿š™™™™™ñ¿ffffffæ¿ÍÌÌÌÌÌô¿ffffff'À333333ã¿ø¿š™™™™™ÀffffffÀš™™™™™Ù¿ÀÍÌÌÌÌÌÀ333333û¿ÍÌÌÌÌÌì¿à¿ÍÌÌÌÌÌÀ333333û¿š™™™™™é¿š™™™™™ñ¿ÍÌÌÌÌÌü¿ffffffæ¿à¿ÍÌÌÌÌÌÀð¿ÍÌÌÌÌÌü¿Àffffffö¿À333333ó¿š™™™™™Àš™™™™™ñ¿à¿333333ã¿ÍÌÌÌÌÌÀÍÌÌÌÌÌô¿ÍÌÌÌÌÌÀš™™™™™ù¿333333ó¿ÍÌÌÌÌÌì¿À333333Àø¿ø¿333333ó¿š™™™™™é¿ÍÌÌÌÌÌü¿Àà¿333333ó¿ð¿333333ã¿333333Àffffff濚™™™™™é¿ffffffæ¿333333ã¿ffffffÀš™™™™™é¿ffffffö¿333333û¿ffffffÀš™™™™™Àð¿333333û¿333333㿚™™™™™Àš™™™™™ñ¿333333ã¿ffffffö¿333333ó¿ÍÌÌÌÌÌÀffffffþ¿ÍÌÌÌÌÌì¿ffffffÀÀffffffþ¿š™™™™™Ù¿ø¿333333ÀÀÍÌÌÌÌÌô¿ø¿ÍÌÌÌÌÌü¿à¿š™™™™™Ù¿333333ó¿š™™™™™ù¿ÍÌÌÌÌÌü¿ÍÌÌÌÌÌü¿333333ã¿ð¿š™™™™™é¿333333û¿333333û¿333333ó¿333333À333333ó¿ffffff À333333ã¿à¿š™™™™™ÀÀÍÌÌÌÌÌô¿š™™™™™ñ¿ À ÀÍÌÌÌÌÌô¿š™™™™™Ù¿ÍÌÌÌÌÌÀ333333û¿ffffffÀffffffÀ333333À333333㿚™™™™™ñ¿š™™™™™Ù¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌü¿Àš™™™™™ù¿ð¿ÍÌÌÌÌÌô¿š™™™™™ñ¿ð¿ÍÌÌÌÌÌÀÍÌÌÌÌÌô¿ø¿ð¿š™™™™™ù¿š™™™™™Àš™™™™™ Àffffffþ¿À333333Àš™™™™™é¿333333 Àš™™™™™é¿ffffffæ¿ø¿ffffffö¿š™™™™™Ù¿ffffffæ¿333333ÀffffffÀ333333û¿333333㿚™™™™™é¿333333ÀÍÌÌÌÌÌô¿ffffffö¿ffffffþ¿ÍÌÌÌÌÌô¿À Àš™™™™™ù¿š™™™™™é¿333333㿚™™™™™+À333333ã¿ffffffÀÍÌÌÌÌÌ쿚™™™™™ ÀÍÌÌÌÌÌô¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌì¿Àš™™™™™Ù¿ffffff ÀÍÌÌÌÌÌÀš™™™™™ À333333ó¿š™™™™™é¿333333ã¿333333û¿ÍÌÌÌÌÌÀ333333㿚™™™™™é¿š™™™™™ñ¿333333û¿š™™™™™é¿ffffffö¿à¿š™™™™™ù¿333333À333333û¿ffffffþ?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?333333ã?š™™™™™Ù?333333Ó?š™™™™™ñ?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?š™™™™™É?š™™™™™é?š™™™™™Ù?š™™™™™Ù?š™™™™™é?š™™™™™é?š™™™™™é?š™™™™™Ù?333333ã?333333Ó?š™™™™™Ù?š™™™™™É?ffffffæ?ÍÌÌÌÌÌì?ffffffæ?à?š™™™™™é?333333ã?š™™™™™É¿333333Ó¿š™™™™™é?ffffffæ?ÍÌÌÌÌÌì?333333Ó?333333ã?à?š™™™™™¹¿ffffffæ?ffffffæ?š™™™™™Ù?ffffffö?š™™™™™ù?333333ó?š™™™™™Ù?333333ã?à?š™™™™™é?333333ã?š™™™™™Ù?š™™™™™ñ?ffffffæ?à?333333Ó?ð?ffffffæ?š™™™™™é?š™™™™™é?š™™™™™Ù?š™™™™™É¿ffffffæ?333333ã?š™™™™™é?333333ã?š™™™™™Ù¿ð?š™™™™™é?š™™™™™é?š™™™™™Ù?ÍÌÌÌÌÌì?333333ã?333333ã?š™™™™™é?š™™™™™é?š™™™™™é?š™™™™™é?š™™™™™é?š™™™™™é?š™™™™™ù?ffffffæ?333333ã?ffffffæ?ffffffæ?à?333333ã?333333ã?š™™™™™é?š™™™™™É?ffffffæ?š™™™™™É?333333Ó?à?š™™™™™é?š™™™™™¹¿ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?333333Ó?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?š™™™™™Ù?ÍÌÌÌÌÌì?š™™™™™é?š™™™™™é?ÍÌÌÌÌÌì?ð?333333ã?ffffffæ?š™™™™™é?333333ã?333333Ó?ð?ð?à?ffffffæ?333333ã?ÍÌÌÌÌÌì?ð?š™™™™™Ù?à?ÍÌÌÌÌÌì?333333ã?ffffffæ?333333Ó?š™™™™™Ù?333333ã?à?ÍÌÌÌÌÌì?࿚™™™™™Ù?š™™™™™é?333333ó?ffffffæ?ffffffæ?ð?333333Ó?š™™™™™ñ?š™™™™™Ù?à?ffffffæ?à?š™™™™™ñ?š™™™™™é?š™™™™™É?š™™™™™é?ffffffæ?ø?š™™™™™é?š™™™™™é?333333ã?à?333333ã?š™™™™™é?ffffffæ?333333Ó?333333ã?ffffffæ?333333ã?à?à?š™™™™™é?š™™™™™é?ÍÌÌÌÌÌì?ffffffæ?333333Ó?ffffffæ?ÍÌÌÌÌÌì?333333ó?š™™™™™é?à?š™™™™™Ù?à?ÍÌÌÌÌÌì?ð?š™™™™™ñ?š™™™™™é?à?š™™™™™Ù?à?333333Ó?ÍÌÌÌÌÌì?333333ã?ffffffæ?ÍÌÌÌÌÌì?à?ð?333333ã?ÍÌÌÌÌÌì?à?333333ó?š™™™™™é?š™™™™™Ù?à?š™™™™™é?à?à?š™™™™™Ù?ÍÌÌÌÌÌì?333333Ó?ÍÌÌÌÌÌì?š™™™™™é?š™™™™™Ù?à?333333ó?à?š™™™™™é?ð?ÍÌÌÌÌÌì?333333ã?š™™™™™É¿333333ã?à?à?ø?333333ã?ÍÌÌÌÌÌì?333333ã?š™™™™™ñ?333333ã?š™™™™™é?š™™™™™é?š™™™™™Ù?š™™™™™É¿ð?š™™™™™Ù?š™™™™™é?333333Ó?à?333333ã?ffffffæ?ffffffæ?ð?ð?333333Ó?ð?ð?333333ã?333333Ó?ffffffæ?333333ã?š™™™™™Ù?à?ÍÌÌÌÌÌì?ð?ffffffæ?333333ó?ÍÌÌÌÌÌì?š™™™™™É?š™™™™™¹¿š™™™™™Ù?ð?š™™™™™Ù?š™™™™™é?à?à?š™™™™™É¿š™™™™™Ù?ffffffæ?š™™™™™é?š™™™™™é?ð?ffffffæ?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?333333û?333333ã?ffffffæ?š™™™™™Ù?š™™™™™é?à?333333ã?ð?š™™™™™é?ffffffæ?š™™™™™é?ð¿333333ã?š™™™™™É?333333ã?ffffffæ?š™™™™™ñ?333333ã?à?333333Ó¿ð?ffffffæ?ffffffæ?ÍÌÌÌÌÌì?ffffffæ?333333ã?ffffffæ?à?š™™™™™Ù?š™™™™™é?ffffffæ?ÍÌÌÌÌÌì?333333ã?š™™™™™é?ffffffæ?à?š™™™™™Ù?ÍÌÌÌÌÌô?š™™™™™É¿š™™™™™é?333333ã?ÍÌÌÌÌÌô?ffffffæ?ÍÌÌÌÌÌì?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?333333ã?š™™™™™É?š™™™™™Ù?š™™™™™¹¿ÍÌÌÌÌÌô?š™™™™™ñ?ffffffæ?ÍÌÌÌÌÌì?ffffffæ?333333ã?ÍÌÌÌÌÌì?333333ã?š™™™™™ñ?ð?ð?333333ã?333333ã?à?333333Ó?à?š™™™™™Ù?ð?š™™™™™Ù?ð?ffffffæ?333333Ó?333333Ó?š™™™™™Ù?š™™™™™Ù?ffffffæ?333333ã?š™™™™™Ù?333333ã?333333Ó¿ð?š™™™™™é?à?ffffffæ?š™™™™™Ù?333333ã?ÍÌÌÌÌÌì?ffffffö?ð?à?333333Ó?š™™™™™É?333333ó?š™™™™™é?ffffffæ?333333Ó?à?ÍÌÌÌÌÌô?ffffffö?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?š™™™™™Ù?š™™™™™é?à?ffffffæ?š™™™™™É?š™™™™™É?ffffffæ?š™™™™™é?333333ã?ffffffæ?ffffffæ?333333ã?333333ã?à?š™™™™™é?ð?š™™™™™é?ð?š™™™™™é?à?333333ó?š™™™™™Ù?ð?š™™™™™¹¿333333ó?ø?ffffffæ?à?š™™™™™é?333333ã?ffffffæ?333333Ó?à?ÍÌÌÌÌÌì?333333ã?ð?š™™™™™é?ÍÌÌÌÌÌì?ffffffæ?š™™™™™é?ø¿à?š™™™™™Ù?š™™™™™Ù?ÍÌÌÌÌÌô?ffffffæ?333333ã?333333ã?š™™™™™É?à?ÍÌÌÌÌÌì?š™™™™™Ù?à?333333ã?à?š™™™™™é?š™™™™™é?š™™™™™é?333333Ó?ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?š™™™™™ñ?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?ð?š™™™™™ñ?à?à?ÍÌÌÌÌÌì?š™™™™™¹¿à?à?ÍÌÌÌÌÌì?š™™™™™é?à?š™™™™™Ù?ffffffæ?š™™™™™é?ffffffæ?ÍÌÌÌÌÌì?333333ã?š™™™™™Ù?š™™™™™é?ffffffæ?ÍÌÌÌÌÌô?ffffffæ?333333ã?ÍÌÌÌÌÌ@ø?@ffffffþ?ffffff@333333û?ð?ð?ð?ÍÌÌÌÌÌô?š™™™™™Ù?š™™™™™ù?ffffffæ?š™™™™™ñ?ffffff@333333û?ffffffæ?à?ð?333333û?@333333Ó?ð?ð?ffffffþ?ø¿ÍÌÌÌÌÌô?ÍÌÌÌÌÌü?ffffffö¿333333û¿333333ó?ð?333333@ð?ffffffæ?ð?ÍÌÌÌÌÌü?ÍÌÌÌÌÌ@š™™™™™ @š™™™™™ù?ø?ÍÌÌÌÌÌô?ÍÌÌÌÌÌô?š™™™™™ñ?ffffffö?š™™™™™é?š™™™™™ù?š™™™™™é?š™™™™™Ù?š™™™™™Ù?š™™™™™é?ffffffö?333333ó?ø?š™™™™™¹?@š™™™™™ù?ð?š™™™™™Ù?ffffffæ?ð?š™™™™™@333333ó?333333û?ÍÌÌÌÌÌü?ÍÌÌÌÌÌì?à¿ÍÌÌÌÌÌ@š™™™™™ñ?š™™™™™ñ?333333@333333@š™™™™™ñ?333333û?ÍÌÌÌÌÌ@š™™™™™ù?ÍÌÌÌÌÌô¿333333ã?ÍÌÌÌÌÌì?š™™™™™é?ÍÌÌÌÌÌì?ÍÌÌÌÌÌü?š™™™™™Ù?š™™™™™¹?333333Ó?š™™™™™ñ¿333333û¿333333ã?ÍÌÌÌÌÌô¿ÍÌÌÌÌÌü¿ø?333333ó?ffffff@ð?ø?333333ó?333333û?š™™™™™@ffffffþ?ð¿333333û?à?š™™™™™Ù?@ÍÌÌÌÌÌô?à?333333Ó?ÍÌÌÌÌÌ@333333Ó?ð¿ÍÌÌÌÌÌü?ffffffö?ð?333333 @@š™™™™™@@@ffffffþ?à?ÍÌÌÌÌÌì?š™™™™™é?ffffffö?ÍÌÌÌÌÌì?333333ã?š™™™™™@ffffffæ?ÍÌÌÌÌÌì?ffffffæ?š™™™™™é?š™™™™™é?ffffffæ?ÍÌÌÌÌÌô?@ffffffþ?333333ó?333333û?333333ã?ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?ø?š™™™™™ñ?ø?333333@@š™™™™™@@333333@333333ó?š™™™™™ù?š™™™™™é?ÍÌÌÌÌÌ@333333@@š™™™™™ù?š™™™™™@š™™™™™ù?ffffffæ?333333û?š™™™™™ñ?@š™™™™™ñ?ø?ffffffþ?ÍÌÌÌÌÌô?š™™™™™Ù?333333ó?ffffff@š™™™™™ñ?ffffff濚™™™™™Ù?ÍÌÌÌÌÌì?333333ó?333333ã?à?š™™™™™ù?ÍÌÌÌÌÌô?@ÍÌÌÌÌÌü?à¿ffffffö?ffffffö?ð¿333333Ó?ffffffö¿333333ã¿ÍÌÌÌÌÌì?ÍÌÌÌÌÌì¿333333㿚™™™™™À@š™™™™™é?à?š™™™™™é?š™™™™™ù?ÍÌÌÌÌÌô?333333ó?ø?ÍÌÌÌÌÌ@š™™™™™@š™™™™™Ù?ÍÌÌÌÌÌì?333333û¿ÍÌÌÌÌÌ@š™™™™™é¿ÍÌÌÌÌÌô?333333û?ffffffö¿333333@333333Ó?š™™™™™Ù?š™™™™™ñ?š™™™™™É¿333333û?š™™™™™é?@ffffffö¿š™™™™™ù?š™™™™™Ù¿š™™™™™é?ÍÌÌÌÌÌ@ÍÌÌÌÌÌü?ÍÌÌÌÌÌô?333333ã?333333ã?š™™™™™ù?ffffffö?@š™™™™™é?ffffffæ?ð¿ffffffþ?ffffff濚™™™™™ù?ø¿ø?ÍÌÌÌÌÌì¿à¿š™™™™™@š™™™™™é?333333û¿333333@333333Ó?ÍÌÌÌÌÌü?333333ã¿ÍÌÌÌÌÌô?ffffffæ?à?š™™™™™ù?ø?š™™™™™é?ffffff濚™™™™™ñ¿ÍÌÌÌÌÌì?š™™™™™@ÍÌÌÌÌÌÀ࿚™™™™™é?333333Ó?333333ó?333333@333333@333333ã?š™™™™™ù?š™™™™™É?ffffffö?@š™™™™™@ÍÌÌÌÌÌ@333333ó?ffffffþ?ÍÌÌÌÌÌì?ffffffæ?ffffffö?333333ã?@š™™™™™é?333333û?š™™™™™@š™™™™™¹?ÍÌÌÌÌÌ@ffffffö?ÍÌÌÌÌÌô?ø?333333Ó¿ffffffö?333333ó?ÍÌÌÌÌÌô?ÍÌÌÌÌÌü?333333Ó?ÍÌÌÌÌÌô?333333ó?š™™™™™ @š™™™™™é¿š™™™™™ù?š™™™™™Àš™™™™™@ffffffþ?@ÍÌÌÌÌÌô?ÍÌÌÌÌÌ@ð?ffffffæ¿333333ã¿ffffff@ffffffæ?333333û?ÍÌÌÌÌÌô¿ÍÌÌÌÌÌô?ffffff@š™™™™™ù?ffffffæ?ÍÌÌÌÌÌô?ffffffþ?à?ÍÌÌÌÌÌü?ø?š™™™™™É¿333333Ó?333333û?ffffffæ¿ffffffþ¿ð?333333ó?ffffffæ?333333ã¿ÍÌÌÌÌÌô?ffffffö¿@ÍÌÌÌÌÌì?ffffffþ?ffffffþ?333333ó?ø?ffffffæ?ð?ffffffþ?š™™™™™¹?à?ø?333333ó?ffffffæ?ffffff@@ffffffþ?ÍÌÌÌÌÌô?à?à?š™™™™™ñ?333333ã¿ð¿333333ã?ffffff Àš™™™™™É?š™™™™™é¿ffffff濚™™™™™ñ?š™™™™™Àš™™™™™É¿333333Ó¿ÍÌÌÌÌÌì?333333ã?š™™™™™ù?š™™™™™@ÍÌÌÌÌÌô¿š™™™™™Ù?š™™™™™É¿ø?ffffffþ¿333333ã?ÍÌÌÌÌÌì¿ÍÌÌÌÌÌì?ffffff@š™™™™™É?š™™™™™é¿š™™™™™ñ?333333û?š™™™™™@࿚™™™™™ñ?ð?ÍÌÌÌÌÌ@š™™™™™@333333û?š™™™™™É?ffffffþ?ffffff!ÀÍÌÌÌÌÌô¿ffffffæ?š™™™™™@ffffffö?š™™™™™ù?š™™™™™É¿ÍÌÌÌÌÌì¿333333û?333333ó?333333Ó?ÍÌÌÌÌÌô?š™™™™™@ÍÌÌÌÌÌ@333333Ó¿š™™™™™ @š™™™™™@333333ó?333333Ó?ÍÌÌÌÌÌ@ð¿ÍÌÌÌÌÌô?à?ÍÌÌÌÌÌü?333333Ó?ffffffþ?à?333333ã?333333ã?333333Ó¿ffffffö?ffffffþ?ø¿ÍÌÌÌÌÌì?š™™™™™ù?333333ó?ð?333333@ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?š™™™™™É¿š™™™™™ñ?@ÍÌÌÌÌÌì?@333333Ó¿ffffffþ?š™™™™™ñ?š™™™™™ù?ÍÌÌÌÌÌô?š™™™™™Ù¿ÍÌÌÌÌÌô?ø?ffffffö?š™™™™™ù?š™™™™™é?š™™™™™@ffffffæ?ffffffþ?š™™™™™é?š™™™™™ñ?ÍÌÌÌÌÌ@š™™™™™@š™™™™™Ù?333333+@3333331@š™™™™™ÀÍÌÌÌÌL;@333333#@%À333333@ffffff@ffffff@š™™™™™ @š™™™™™@ÍÌÌÌÌÌ#@š™™™™™6@ffffff-@ffffffD@ÍÌÌÌÌÌ*@ÍÌÌÌÌÌ@ffffff@@333333:@ffffff@ffffff@333333&@3333335@ffffff2@ffffff?@š™™™™™ù?ÍÌÌÌÌÌ2@33333³B@ÍÌÌÌÌ C@33333³3@ffffff2@š™™™™™,@>@@š™™™™™ @@ffffff$@ffffff?@ÍÌÌÌÌÌ9@@333333 @333333*@33333³5@3333335@7@š™™™™™(@2@ÍÌÌÌÌÌ@š™™™™™?@fffffæ3@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ1@fffffæ4@!@3333335@š™™™™7@2@333333(@ffffff@ffffff'Àffffff&@ffffff@š™™™™™%@333333%@ffffff!@š™™™™™$@ffffff)@333333,@333333$@33333³3@1@ffffff"@€2@ffffff(@š™™™™™¹?ffffff"ÀÍÌÌÌÌÌ#@ÍÌÌÌÌÌü?š™™™™™&@ÍÌÌÌÌÌ*@ÍÌÌÌÌÌ"@ffffff*@š™™™™™ @š™™™™™ @3333339@ffffff1@$@%@€F@š™™™™™!@š™™™™™5@@š™™™™™4@333333ó¿333333@ffffff'@ffffff5@ @fffff&B@€0@ffffff*@ffffff#Àš™™™™™8@333333%@š™™™™™2@š™™™™™*@š™™™™™.@333333ó¿3333336@š™™™™™3@š™™™™™@#@333333#@š™™™™™&@@ffffff%@ÍÌÌÌÌÌ&@333333"@ÍÌÌÌÌÌ0@€;@š™™™™™@ÍÌÌÌÌÌ2@ffffff@@ffffff'@ffffff-@.@ffffff(@ÍÌÌÌÌLE@ÍÌÌÌÌÌü?-@ffffff$@ø?@š™™™™™ @ÍÌÌÌÌÌ0@@š™™™™™)@ÍÌÌÌÌÌ*@ffffff"@ffffffæ?fffffæ2@ffffff,@@333333@ÍÌÌÌÌÌì?fffffæ4@ffffff@333333@333333@(@@š™™™™™ @ffffff"@ffffff,@fffffæ1@š™™™™™ ÀffffffI@ffffff5@ffffff&@ÍÌÌÌÌÌD@ÍÌÌÌÌL2@ @ÍÌÌÌÌÌ'@ffffff'@ffffff@ÍÌÌÌÌÌ1@ffffff$@ffffff4@ÍÌÌÌÌÌ5@ÍÌÌÌÌÌ0@ÍÌÌÌÌÌ*@6@š™™™™™@2@ÍÌÌÌÌÌ#@ffffff @š™™™™™@@ffffff7@š™™™™™@3333338@ÍÌÌÌÌÌ&@&@3333334@š™™™™™@@@ffffff9@N@333333!@ÍÌÌÌÌÌ@@A@ÍÌÌÌÌL2@š™™™™0@š™™™™™ @š™™™™™Ù¿#@333333&Àš™™™™™G@ffffff.@€6@ÍÌÌÌÌŒG@š™™™™™,@š™™™™™ù¿@ÍÌÌÌÌÌ'@ÍÌÌÌÌÌG@š™™™™6@ffffff@333333@š™™™™™ Àffffff!@ffffff@@*@333333*@ÍÌÌÌÌÌ)@š™™™™™@@š™™™™™@)@ÍÌÌÌÌÌ @333333%@ÍÌÌÌÌÌ@333333@333333@ÍÌÌÌÌÌ6@3333331@+@fffffæ3@ÍÌÌÌÌÌ7@333333@fffffæ8@%@ÍÌÌÌÌÌ:@333333@@€;@33333³:@š™™™™™@ÍÌÌÌÌÌ5@'@333333@š™™™™™'@33333³0@4@fffffæ6@ÍÌÌÌÌÌ.@š™™™™™@ÍÌÌÌÌÌ'@š™™™™1@š™™™™™&@33333³<@š™™™™2@ÍÌÌÌÌÌ"@ÍÌÌÌÌL<@ffffff(@ffffff7@fffff¦@@ÍÌÌÌÌÌ@ffffff3@ÍÌÌÌÌÌ@(@@ffffffþ?ffffff"@$@"@@š™™™™™ @"@š™™™™™@ÍÌÌÌÌÌ/@š™™™™7@fffff&K@)@3333337@ @ffffff:@3333335@š™™™™™%@š™™™™™6@š™™™™7@@ÍÌÌÌÌÌ*@@ffffff$@ffffff5@fffffæ:@3@ÍÌÌÌÌÌ(@333333@ffffff)@€6@€G@À@B@š™™™™?@@ÍÌÌÌÌÌ8@š™™™™™3@fffffæ0@ffffff@@ÍÌÌÌÌÌ%@š™™™™Ù@@€=@@ÍÌÌÌÌL0@9@ÍÌÌÌÌÌ@333333ÀÍÌÌÌÌÌ/@š™™™™™@š™™™™1Àš™™™™™'@ffffff@ÍÌÌÌÌÌ@333333ã?š™™™™™)À33333³3@š™™™™™G@@*@ÍÌÌÌÌÌ/@€8@33333³5@ÍÌÌÌÌÌ@fffffæ6@333333'@ð¿-@ffffff,@ffffff'@š™™™™™À'@ffffff!@ÍÌÌÌÌÌ,@ð?333333@333333%@*@š™™™™™@š™™™™™@ÍÌÌÌÌÌ!@>@"Àš™™™™™,@š™™™™6@333333+@€3@ÍÌÌÌÌÌ3@ffffff@ÍÌÌÌÌÌô?ÍÌÌÌÌL@@fffffæ0@33333³4@š™™™™<@ffffff:@-@€;@333333"@š™™™™™.@@E@"@ÍÌÌÌÌÌ&@333333ó?ÍÌÌÌÌÌ!@fffff&D@@š™™™™™/@33333³6@š™™™™5@ÍÌÌÌÌÌ3@š™™™™™@333333 @333333(@ffffffÀ333333Àš™™™™™ñ¿š™™™™™1@š™™™™3@333333@š™™™™™E@š™™™™™É?š™™™™™1@ffffffc@fffff¦F@š™™™™™@š™™™™™@ffffff+@ @š™™™™™é?333333?@(@š™™™™™À333333;@š™™™™™!@ffffff3@333333@ÍÌÌÌÌL@@fffff¦@Àš™™™™™/@ffffff@ÍÌÌÌÌÌ*@š™™™™™I@ÍÌÌÌÌÌ@ÍÌÌÌÌL@@š™™™™8@333333<@333333+@@$@ffffff@ @@333333&@ÍÌÌÌÌÌ-@š™™™™™(@33333³6@ffffff'@ffffff0@ÍÌÌÌÌÌ3@š™™™™7@,@ÍÌÌÌÌÌÀffffff*@ÍÌÌÌÌÌô¿ÀÍÌÌÌÌÌ!@ÍÌÌÌÌÌ"@ÍÌÌÌÌÌ$@:@ÍÌÌÌÌÌ.@ÍÌÌÌÌL0@ffffff@ffffff:@333333À333333"@!@ffffff'@ffffff@š™™™™™)@ffffffö?333333-@/@š™™™™™@š™™™™™@-@ÍÌÌÌÌL4@ÍÌÌÌÌÌ>@€:@ÍÌÌÌÌÌ3@ÍÌÌÌÌŒF@š™™™™™?@ffffff@%@ffffff+@333333'@+@ÍÌÌÌÌÌ @33333³6@fffffæ:@fffffæ5@š™™™™M@fffffæ9@ÍÌÌÌÌÌ%@%@333333'@33333³C@ÍÌÌÌÌŒB@333333@ÍÌÌÌÌL1@333333;@33333³?@š™™™™0@ffffff%@š™™™™™@@3333335@š™™™™™(@333333>@ÍÌÌÌÌÌ8@€@@fffffæA@"@333333.@ÍÌÌÌÌL;@ÍÌÌÌÌ C@€N@B@ÍÌÌÌÌÌ0@*@š™™™™™5@ÍÌÌÌÌÌ=@ÍÌÌÌÌÌ?@š™™™™;@fffffæ5@3333337@ÍÌÌÌÌÌ!@À@@9@4@333333:@€>@ffffff"@ffffffE@ÀA@ffffffA@ffffff.@ffffff!@ffffffÀ33333³?@ffffff*@ffffff8@fffffæ7@ÍÌÌÌÌÌ/@333333@ÍÌÌÌÌÌ;@6@fffffæ0@ffffffD@š™™™™B@š™™™™™0@33333³>@š™™™™<@ÍÌÌÌÌÌ)@2ÀÍÌÌÌÌÌ+@333333"@3333330@ÍÌÌÌÌL3@š™™™™8@ÍÌÌÌÌÌ/@333333@@š™™™™™.@ÍÌÌÌÌÌ@š™™™™™2@ffffff"Àš™™™™7@3333334@ÍÌÌÌÌL;@ÍÌÌÌÌL9@333333:@333333&@ÍÌÌÌÌÌ0@fffffæ8@š™™™™YG@3333337@fffffæ;@ÍÌÌÌÌÌ?@š™™™™™0@ffffffÀÍÌÌÌÌŒG@fffffæ2@3333335@ffffff,@ÀA@š™™™™™@(@33333³>@š™™™™3@ÍÌÌÌÌL1@33333³B@ÍÌÌÌÌL7@ÍÌÌÌÌL7@€7@ffffff>@€7@ÍÌÌÌÌL4@š™™™™YA@ffffff(@333333>@š™™™™™*@333333@€?@2@€3@ffffff0@ÀG@333333@š™™™™2@3@ÍÌÌÌÌÌ/@€4@š™™™™™*@ÍÌÌÌÌL=@ffffff@ÍÌÌÌÌL2@fffffæ4@fffffæ4@333333%@333333?@333333@@š™™™™™3@ffffff=@ffffff/@ÍÌÌÌÌ C@,@ffffff*@ @fffffæ;@3333339@€5@ffffff4@fffffæ>@33333³<@ÍÌÌÌÌÌô?ÍÌÌÌÌlP@fffffæ=@š™™™™™;@ffffffH@ÍÌÌÌÌL>@ÍÌÌÌÌL5@33333³5@,@ÍÌÌÌÌÌ,@333333E@š™™™™2@ffffff/@š™™™™™8@33333³6@6@ÍÌÌÌÌÌ9@ÍÌÌÌÌÌ@š™™™™™;@33333³2@ÍÌÌÌÌÌ3@fffff&F@ÍÌÌÌÌÌ2@)@33333³A@333333@ffffff(@š™™™™™@š™™™™™É¿333333%@ø?€5@š™™™™™:@š™™™™™8@333333'@fffff&B@<@fffffæ:@)@ÍÌÌÌÌÌ!@33333³3@ÍÌÌÌÌÌ-@33333óO@ÍÌÌÌÌL1@ÍÌÌÌÌL;@ÍÌÌÌÌL6@ÍÌÌÌÌ B@333333 Àš™™™™™-@€9@ÍÌÌÌÌÌ;@fffffæC@š™™™™™"@š™™™™™@333333@ @ÍÌÌÌÌL3@333333"@š™™™™™:@š™™™™™é?ÍÌÌÌÌÌ6@š™™™™™@ÍÌÌÌÌÌì?ffffff(@333333;@ÍÌÌÌÌL5@33333³4@ffffff@š™™™™™*@š™™™™™2@fffff&@@fffffæD@fffffæ1@€9@3333330@33333³7@š™™™™™6@3333339@ffffff @-@ffffffþ¿9@ÍÌÌÌÌÌF@ÍÌÌÌÌÌ%@ffffff@š™™™™E@333333@š™™™™9@ÍÌÌÌÌÌ$@ffffff>@fffffæ9@3333333@ÍÌÌÌÌÌ0@š™™™™6@š™™™™™;@333333@š™™™™3@ÍÌÌÌÌL7@;@ÍÌÌÌÌÌ@ffffffÀffffff-@€8@š™™™™YD@ffffff7@š™™™™ÙE@$@ÍÌÌÌÌÌ7@š™™™™™ @š™™™™™)@ffffff<@33333³:@ÍÌÌÌÌÌ?@š™™™™™-@33333³6@ffffff-@/@€8@333333:@33333“R@š™™™™™0@33333³A@š™™™™9@š™™™™;@fffff&B@33333³3@fffffæ?@ÍÌÌÌÌÌ@@ÍÌÌÌÌÌ@8@š™™™™™/@ÍÌÌÌÌÌ3@ÍÌÌÌÌ B@33333³=@fffffæ=@ÍÌÌÌÌÌ4@33333óA@333333û?€@@ÍÌÌÌÌÌ%@š™™™™:@fffffæH@33333óE@)@€E@333333=@š™™™™™%@333333ó?33333³9@ÍÌÌÌÌL1@@F@5@ÍÌÌÌÌÌ+@ÍÌÌÌÌŒA@ÀA@š™™™™™"@@<@333333%@À33333³5@333333@ÍÌÌÌÌÌ@333333ã? @ffffff-@š™™™™8@/@ffffff4@ffffff3@fffffæ2@fffff¦C@'À33333³D@33333³1@*@š™™™™<@33333³8@ÍÌÌÌÌÌ<@à¿fffffæ1@š™™™™™8@š™™™™™-@333333 @š™™™™™.@€2@2@333333:@ffffff4@fffffæ6@ÍÌÌÌÌLD@Àfffffæ0@š™™™™=@ÍÌÌÌÌÌ@š™™™™™(@š™™™™™;@ffffffCÀ@ffffff;@333333)@š™™™™™:@š™™™™™!@ÍÌÌÌÌL8@333333(@fffff&A@ÍÌÌÌÌÌ*@ÍÌÌÌÌÌ:@fffffP@333333Ó¿1@š™™™™™ñ¿š™™™™3@ffffff0@ffffff#@ÍÌÌÌÌÌ@>@ffffffD@š™™™™5@ffffffö¿š™™™™™/@8@š™™™™™@ffffff&ÀÍÌÌÌÌÌ#@7@š™™™™A@33333³3@ÍÌÌÌÌ L@ø?ffffff;@ÍÌÌÌÌ E@ÀA@š™™™™™)@33333³9@š™™™™™6@3333331@š™™™™™¹¿š™™™™™1@€8@š™™™™™@33333³=@š™™™™™1@fffffæB@š™™™™:@š™™™™>@ÍÌÌÌÌÌ%Àfffffæ@@š™™™™™)@š™™™™™/@R@š™™™™™ÀÍÌÌÌÌŒC@;@fffffæD@š™™™™™1@ÍÌÌÌÌÌ4@ffffff(@ffffff)@333333/@@š™™™™™4@33333³=@333333ã¿=@€6@ffffff;@333333?@fffff&E@ÍÌÌÌÌL7@à?ÍÌÌÌÌÌ(@š™™™™™@3333333@ffffff.@ffffff9@333333"@ffffffD@33333³7@€;@333333.@6@š™™™™™@ffffff5@š™™™™1@3333337@!@33333³A@333333@fffffæ;@š™™™™™4@š™™™™™'@ÍÌÌÌÌÌ4@33333³@@fffffæ5@š™™™™™é?š™™™™™¹?š™™™™™¹?š™™™™™¹?š™™™™™¹¿333333Ó¿333333ó?š™™™™™é¿š™™™™™¹?š™™™™™¹¿š™™™™™¹?š™™™™™É¿333333Ó?š™™™™™¹¿ffffffæ?࿚™™™™™É?š™™™™™¹¿š™™™™™Ù¿à?š™™™™™¹?š™™™™™É¿333333Ó¿š™™™™™Ù?š™™™™™Ù¿š™™™™™É¿333333Ó?š™™™™™¹?š™™™™™¹¿š™™™™™¹¿à¿š™™™™™Ù¿š™™™™™É¿š™™™™™É¿š™™™™™É?333333Ó¿ffffffæ?š™™™™™É¿š™™™™™É?333333Ó¿š™™™™™Ù¿š™™™™™¹¿ð¿à¿š™™™™™É?š™™™™™É?࿚™™™™™Ù¿š™™™™™É¿à¿333333Ó¿333333ã?š™™™™™¹¿š™™™™™¹¿š™™™™™¹?333333Ó¿š™™™™™É?333333Ó?š™™™™™É?333333ӿ࿚™™™™™Ù¿š™™™™™É?333333Ó¿333333û¿š™™™™™ù¿š™™™™™Ù?š™™™™™ù¿333333ó¿š™™™™™É¿333333Ó¿à?ffffff濚™™™™™É¿š™™™™™É¿š™™™™™É¿333333Ó¿š™™™™™Ù¿š™™™™™¹¿à¿š™™™™™¹?š™™™™™É¿333333Ó¿š™™™™™¹?ÍÌÌÌÌÌü¿š™™™™™É?š™™™™™¹¿š™™™™™¹¿ÍÌÌÌÌÌì?š™™™™™É?š™™™™™É¿š™™™™™¹?š™™™™™¹¿š™™™™™É?š™™™™™é¿à¿ð¿š™™™™™¹?ffffffæ?š™™™™™¹?333333Ó¿333333ã?š™™™™™¹¿à?š™™™™™Ù¿š™™™™™¹?š™™™™™¹¿ffffff濚™™™™™¹?š™™™™™¹?š™™™™™¹?š™™™™™é?š™™™™™É¿š™™™™™¹?à?ffffffæ?š™™™™™¹¿š™™™™™Ù¿š™™™™™Ù¿š™™™™™¹?š™™™™™É?࿚™™™™™É¿š™™™™™é¿333333㿚™™™™™¹?š™™™™™¹¿š™™™™™¹¿š™™™™™¹¿à?š™™™™™É?š™™™™™¹?š™™™™™¹¿š™™™™™¹?š™™™™™Ù¿š™™™™™¹¿š™™™™™É¿ÍÌÌÌÌÌ쿚™™™™™Ù?š™™™™™¹?š™™™™™¹?ÍÌÌÌÌÌô¿à¿š™™™™™É¿š™™™™™¹?š™™™™™Ù¿š™™™™™¹?š™™™™™¹?333333Ó¿š™™™™™Ù¿š™™™™™¹?333333Ó?š™™™™™Ù¿333333ã¿333333Ó¿333333Ó¿š™™™™™¹?333333ã¿à¿à¿333333Ó¿š™™™™™É¿š™™™™™¹?š™™™™™É¿333333㿚™™™™™¹?š™™™™™Ù¿333333Ó?š™™™™™é¿ÍÌÌÌÌÌô¿ð¿š™™™™™¹?à?333333Ó¿ffffff濚™™™™™É?à?š™™™™™Ù¿333333ã¿à?333333㿚™™™™™¹¿š™™™™™Ù?š™™™™™É?š™™™™™Ù¿š™™™™™¹¿š™™™™™¹?š™™™™™Ù¿333333Ó?š™™™™™¹¿š™™™™™¹¿š™™™™™Ù¿š™™™™™¹?š™™™™™É¿ÍÌÌÌÌÌô¿ð¿š™™™™™¹¿ø¿š™™™™™Ù¿š™™™™™Ù¿š™™™™™é¿š™™™™™É¿š™™™™™¹¿š™™™™™¹?333333㿚™™™™™Ù?š™™™™™¹?333333Ó¿ffffffæ?ffffffæ¿333333Ó?š™™™™™Ù?𿚙™™™™É?ffffff濚™™™™™Ù¿333333ã?š™™™™™é¿š™™™™™¹¿š™™™™™É¿š™™™™™¹¿š™™™™™É?š™™™™™É?333333Ó?333333Ó¿š™™™™™É¿ÍÌÌÌÌÌì¿à¿š™™™™™Ù?š™™™™™É¿š™™™™™Ù?š™™™™™¹?š™™™™™É?š™™™™™¹?š™™™™™é?š™™™™™¹¿š™™™™™¹¿š™™™™™É¿š™™™™™¹?ÍÌÌÌÌÌ쿚™™™™™¹¿š™™™™™Ù?ffffffæ¿ð¿ø?š™™™™™¹?š™™™™™ñ¿š™™™™™É?ffffff濚™™™™™¹?š™™™™™¹¿š™™™™™Ù?ffffffþ¿š™™™™™Ù?š™™™™™¹?à¿333333Ó¿š™™™™™Ù?333333Ó?à?š™™™™™¹?333333ã?ffffffæ?š™™™™™¹?š™™™™™¹?333333ã¿ffffffö¿ð?ffffffæ¿333333Ó?š™™™™™¹?ffffffö¿333333㿚™™™™™É¿333333Ó?à?š™™™™™Ù¿ø?333333Ó¿333333ӿ࿚™™™™™¹?š™™™™™É¿š™™™™™É¿à¿š™™™™™¹?š™™™™™¹¿à?š™™™™™É?š™™™™™É¿š™™™™™é?š™™™™™É¿à¿ffffff濚™™™™™Ù¿š™™™™™é¿ð?š™™™™™É?š™™™™™¹?ð¿ffffffæ¿ð¿ÍÌÌÌÌÌ쿚™™™™™Ù¿š™™™™™É¿à?š™™™™™¹?š™™™™™É?š™™™™™Ù?ð¿ð¿ÍÌÌÌÌÌì¿à¿à¿à¿333333Ó¿333333Ó¿333333ã?š™™™™™¹?š™™™™™¹¿š™™™™™Ù¿ffffff濚™™™™™¹?š™™™™™¹?333333ã¿ffffff濚™™™™™¹¿333333Àffffffæ?š™™™™™É¿š™™™™™Ù?š™™™™™Ù¿à¿ø¿à¿š™™™™™¹?ffffffæ?š™™™™™Ù¿à?š™™™™™É?333333㿚™™™™™É?š™™™™™é?333333ã?š™™™™™¹?š™™™™™É¿š™™™™™¹¿ð¿š™™™™™¹¿333333㿚™™™™™É?š™™™™™¹¿à¿ÍÌÌÌÌÌì¿à¿à¿š™™™™™Ù?ffffff濚™™™™™Ù?333333Ó¿š™™™™™¹?ffffffþ?š™™™™™é?š™™™™™é¿š™™™™™É?š™™™™™é¿333333Ó¿333333ã¿ÍÌÌÌÌÌì?š™™™™™¹?š™™™™™É?š™™™™™Ù¿š™™™™™é¿333333Ó¿š™™™™™¹¿333333Ó¿333333Ó¿š™™™™™É¿š™™™™™Ù¿333333Ó?à?š™™™™™¹?š™™™™™¹?š™™™™™é?ffffffö?š™™™™™é?š™™™™™Ù¿š™™™™™Ù¿333333ó¿ÍÌÌÌÌÌô¿à¿š™™™™™Ù¿333333 Àffffffö¿ffffff濚™™™™™é¿ø¿333333ó¿à¿š™™™™™é¿š™™™™™ñ¿333333ó¿ð¿š™™™™™é¿à¿ð¿ÍÌÌÌÌÌÀš™™™™™ÀÍÌÌÌÌÌ쿚™™™™™ñ¿š™™™™™é¿š™™™™™ÀÍÌÌÌÌÌô¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌü¿š™™™™™Àð¿ffffff濚™™™™™Ù¿333333ã¿à¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌô¿333333ã¿ð¿š™™™™™Ù¿à¿333333û¿š™™™™™Ù¿š™™™™™ñ¿š™™™™™ù¿š™™™™™Àš™™™™™ñ¿ffffffÀš™™™™™Ù¿ffffffæ¿à¿Àffffffæ¿ffffffæ¿à¿š™™™™™ñ¿š™™™™™ñ¿ffffffæ¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌÀ333333ã¿ÍÌÌÌÌÌô¿ð¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌÀà¿à¿ð¿ÍÌÌÌÌÌô¿š™™™™™ù¿333333ó¿š™™™™™é¿ÍÌÌÌÌÌü¿š™™™™™Ù¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌ쿚™™™™™é¿à¿333333ó¿333333ã¿ð¿à¿à¿ð¿à¿ð¿ø¿ÍÌÌÌÌÌô¿333333ã¿333333ã¿333333À333333㿚™™™™™Ù¿š™™™™™é¿š™™™™™ù¿ffffff濚™™™™™Ù¿333333Àš™™™™™Ù¿ÍÌÌÌÌÌ쿚™™™™™Ù¿š™™™™™Ù¿š™™™™™Àš™™™™™é¿à¿à¿333333û?@š™™™™™@333333"@š™™™™™ù?š™™™™3@ffffff@ÍÌÌÌÌÌ @š™™™™™@ÍÌÌÌÌÌ#@š™™™™™@@š™™™™™ @333333-@ÍÌÌÌÌÌ)@š™™™™™@š™™™™™#@@ffffff@333333 @333333@ @333333@ffffff@ffffff&@@ÍÌÌÌÌÌ"@š™™™™™#@333333@@ffffff@@š™™™™™"@@ÍÌÌÌÌÌ @š™™™™™@@ffffff@š™™™™™é?ffffff@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ @š™™™™™0@ffffff@333333 @ÍÌÌÌÌÌ@š™™™™™@333333@ffffff@š™™™™™@@ffffff @ffffff1@@ffffff@ÍÌÌÌÌÌ#@ÍÌÌÌÌÌ@333333@#@š™™™™™@š™™™™™!@š™™™™™@333333*@š™™™™™-@ffffff@ffffff@ffffff)@333333#@333333û?š™™™™™@ÍÌÌÌÌÌ@333333 @ffffff @!@ffffff&@š™™™™™ @333333@'@333333"@333333#@333333)@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@š™™™™™!@333333@3333333@333333@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333@ffffff@333333@š™™™™™@333333'@ffffff@333333@ÍÌÌÌÌÌ%@š™™™™™@333333/@!@ÍÌÌÌÌÌ&@333333@333333@ffffff @ÍÌÌÌÌÌ@š™™™™™2@333333@ffffff@ÍÌÌÌÌÌ@@ÍÌÌÌÌÌ-@š™™™™™$@333333@š™™™™™@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ#@333333!@ @ø?ffffff@333333@š™™™™™"@ÍÌÌÌÌÌ@š™™™™™ @@@#@ffffff$@"@!@ffffff@&@š™™™™™'@ÍÌÌÌÌÌ@'@333333@š™™™™™'@333333@ÍÌÌÌÌÌ @333333&@ffffff@ffffff@333333)@ÍÌÌÌÌÌ!@333333@!@@š™™™™™@@333333@ÍÌÌÌÌÌ!@333333@š™™™™™$@ffffff@ÍÌÌÌÌÌ @ @ÍÌÌÌÌÌ#@ffffff @ffffff @ffffff@333333&@333333"@ffffff@ffffff@333333@ @333333@ffffff@333333@ffffff%@@ffffff@ÍÌÌÌÌÌ#@š™™™™™#@@š™™™™™@ @š™™™™™#@ffffff@333333@ffffffæ?333333û?ffffff@333333@"@š™™™™™ @333333@@$@!@333333@ffffff+@@-@+@ÍÌÌÌÌÌ@@š™™™™™@š™™™™™@333333'@š™™™™™&@@š™™™™™@ÍÌÌÌÌL1@333333!@ffffff+@333333!@ffffff@ffffff0@@ffffff@ @ÍÌÌÌÌÌ@ffffffþ?333333@333333@ffffff@333333 @ffffff@ffffff@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333@@ÍÌÌÌÌÌ+@@š™™™™™ @ffffff"@ÍÌÌÌÌÌ*@ÍÌÌÌÌL0@333333@333333@ÍÌÌÌÌÌ@š™™™™™@333333(@333333@ffffff@š™™™™™@ÍÌÌÌÌÌ@ffffff @333333@ffffff'@ffffff%@@ÍÌÌÌÌÌ@š™™™™™@ffffff@š™™™™™Ù?š™™™™™@ÍÌÌÌÌÌ@š™™™™™@333333@333333@"@ffffff@ffffff@ÍÌÌÌÌÌ@333333"@ÍÌÌÌÌÌ @ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333@ffffffþ?ffffff @š™™™™™ @333333@ffffff$@333333@š™™™™™@ffffff@@333333'@333333!@333333"@ @ffffff@ÍÌÌÌÌÌ@@š™™™™™@š™™™™™ @333333@ffffff)@ÍÌÌÌÌÌ@ffffff@@@š™™™™™/@š™™™™™@333333 @@fffffæ0@ÍÌÌÌÌÌ@ffffff@š™™™™™"@š™™™™™@ffffff@ÍÌÌÌÌÌ @@ÍÌÌÌÌÌô?ffffff@ffffff*@ÍÌÌÌÌÌ@@š™™™™™%@ÍÌÌÌÌÌ@@ffffff@š™™™™™@ø?ÍÌÌÌÌÌ@š™™™™™#@333333@š™™™™™@š™™™™™@@š™™™™™ @š™™™™™"@333333@ÍÌÌÌÌÌ@333333@333333@š™™™™™ @ÍÌÌÌÌÌì?ÍÌÌÌÌÌ @š™™™™™@333333@!@(@š™™™™™@ÍÌÌÌÌÌ@ffffff@š™™™™™@333333&@ÍÌÌÌÌÌ$@š™™™™™"@333333%@@ffffff@ffffff@!@ÍÌÌÌÌÌ @"@@333333@ffffff @ffffff @ @ÍÌÌÌÌÌ"@@ÍÌÌÌÌÌ@@@š™™™™™@333333@š™™™™™#@333333)@@@š™™™™™@@#@333333@š™™™™™-@333333@333333@ffffff@@š™™™™™#@ÍÌÌÌÌÌ@333333'@@ÍÌÌÌÌÌü?ÍÌÌÌÌÌ@ffffff%@(@333333@%@%@@333333*@333333(@$@ÍÌÌÌÌÌ&@ÍÌÌÌÌÌ!@ffffffæ?ÍÌÌÌÌÌ@ffffff$@ffffff#@-@ffffff@@ÍÌÌÌÌÌ@333333@ffffff@@@š™™™™™1@ÍÌÌÌÌÌ!@333333 @ffffff @ffffff @š™™™™™@ffffff-@ffffff@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ$@333333@-@333333!@333333%@š™™™™™#@ÍÌÌÌÌÌ.@š™™™™™@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@š™™™™™!@š™™™™™@ffffff)@š™™™™™@ÍÌÌÌÌÌ@333333"@@ÍÌÌÌÌÌ@333333@ÍÌÌÌÌÌ$@ffffff/@333333@333333@ÍÌÌÌÌÌì?@#@ÍÌÌÌÌÌ"@#@!@ffffff@333333%@ffffff!@ÍÌÌÌÌÌ @ÍÌÌÌÌÌ@@ffffff@ffffff@ffffffþ?%@š™™™™™ù?š™™™™™"@ÍÌÌÌÌÌ@ð¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌÀà¿ffffffÀš™™™™™ñ¿ø¿ffffff濚™™™™™é¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌÀš™™™™™Ù¿ÍÌÌÌÌÌü¿333333û¿š™™™™™ñ¿333333û¿ÍÌÌÌÌÌô¿š™™™™™ñ¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌÀffffff濚™™™™™ Àffffffæ¿ÍÌÌÌÌÌÀš™™™™™é¿ø¿ÍÌÌÌÌÌô¿333333û¿ø¿333333ÀÍÌÌÌÌÌô¿š™™™™™ Àš™™™™™ù¿333333ÀÍÌÌÌÌÌÀ333333ó¿ÀÀš™™™™™ù¿ÍÌÌÌÌÌÀffffffÀffffff濚™™™™™ ÀÍÌÌÌÌÌÀÀš™™™™™Ù¿ÍÌÌÌÌÌÀÀš™™™™™Àffffffö¿ffffffÀÍÌÌÌÌÌÀš™™™™™Àð¿ffffffö¿ffffffÀÍÌÌÌÌÌü¿ffffff À333333ó¿ÍÌÌÌÌÌÀð¿333333Àà¿ffffffæ¿ffffffþ¿š™™™™™Àš™™™™™é¿š™™™™™Ù¿ffffffö¿š™™™™™ÀÍÌÌÌÌÌÀffffffÀš™™™™™ñ¿ffffff濚™™™™™Àš™™™™™ ÀÍÌÌÌÌÌü¿à¿š™™™™™ù¿ffffffÀ333333û¿ð¿333333ã¿333333Àš™™™™™ñ¿ÍÌÌÌÌÌô¿ffffffæ¿ÍÌÌÌÌÌÀÍÌÌÌÌÌÀ333333ÀÀÍÌÌÌÌÌÀÀø¿ÍÌÌÌÌÌÀffffff Àš™™™™™ù¿š™™™™™ù¿š™™™™™ Àš™™™™™ÀÍÌÌÌÌÌì¿333333Àš™™™™™Àš™™™™™ñ¿ffffffÀÀ333333û¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌ쿚™™™™™ù¿ÍÌÌÌÌÌÀÍÌÌÌÌÌô¿333333ÀffffffÀ333333ÀÍÌÌÌÌÌô¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌÀÍÌÌÌÌÌÀ333333û¿ffffffþ¿ð¿š™™™™™ù¿333333#ÀÍÌÌÌÌÌÀffffffö¿333333À333333ã¿ffffffÀ333333ó¿ð¿ffffffþ¿ffffffö¿333333û¿333333û¿š™™™™™ÀÍÌÌÌÌÌô¿ð¿š™™™™™ÀÀÀÀÍÌÌÌÌÌÀÍÌÌÌÌÌ쿚™™™™™Ù¿ffffffö¿ffffff ÀÍÌÌÌÌÌô¿Àš™™™™™ù¿333333û¿ÍÌÌÌÌÌÀš™™™™™ñ¿ð¿ø¿ffffff濚™™™™™é¿ÀÀ333333ÀÍÌÌÌÌÌÀ333333ÀffffffÀÍÌÌÌÌÌÀÍÌÌÌÌÌü¿ÍÌÌÌÌÌÀ333333ÀÍÌÌÌÌÌì¿ ÀÀÍÌÌÌÌÌÀð¿333333ã¿ffffffæ¿Àð¿ð¿ffffffö¿ÀÀš™™™™™ÀÀš™™™™™À333333ã¿À333333ã¿ffffffÀÍÌÌÌÌÌô¿333333ÀÀffffffÀ333333û¿ø¿ffffffþ¿Àø¿à¿333333û¿ÍÌÌÌÌÌü¿333333û¿ffffffþ¿š™™™™™ÀÍÌÌÌÌÌÀffffffÀffffff Àffffffö¿ø¿333333Àš™™™™™Àš™™™™™Ù¿ffffffö¿š™™™™™ñ¿À333333û¿ÍÌÌÌÌÌÀÍÌÌÌÌÌü¿333333ó¿ffffff ÀÍÌÌÌÌÌô¿ÍÌÌÌÌÌÀÍÌÌÌÌÌô¿333333û¿š™™™™™ÀÀ333333ã¿ÍÌÌÌÌÌ쿚™™™™™À333333À333333Àš™™™™™ù¿ffffff#À333333ó¿ø¿ÍÌÌÌÌÌÀ333333û¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌÀffffffÀffffff"À333333ÀffffffÀÍÌÌÌÌÌÀÀÀÍÌÌÌÌÌì¿ffffffÀø¿ffffffæ¿ÍÌÌÌÌÌÀš™™™™™ Àš™™™™™ù¿ffffffö¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌÀ€0À333333ã?ffffffæ?ð?š™™™™™Ù?š™™™™™ @š™™™™™Ù?š™™™™™Ù?ÍÌÌÌÌÌì?š™™™™™Ù?š™™™™™é?333333ó¿ø¿ÍÌÌÌÌÌô?š™™™™™¹?ffffffæ?š™™™™™Ù?ffffffþ?ffffffö?à?333333ã?ÍÌÌÌÌÌü?333333ó?333333û?333333ó?ÍÌÌÌÌÌì?š™™™™™é?ÍÌÌÌÌÌì?333333ó?ffffffö?š™™™™™¹?ÍÌÌÌÌÌì?333333ó?ð?ø?š™™™™™ñ?š™™™™™é?š™™™™™é?š™™™™™¹¿š™™™™™ù?š™™™™™é?š™™™™™ñ?š™™™™™¹?š™™™™™ñ?ffffffæ?ð?333333ó?ffffffþ?ÍÌÌÌÌÌì?š™™™™™ñ?ÍÌÌÌÌÌô?ffffffö?ÍÌÌÌÌÌì?š™™™™™é?ð?ffffffö?ÍÌÌÌÌÌô?š™™™™™é?ÍÌÌÌÌÌü?ÍÌÌÌÌÌô?à?š™™™™™é?ffffffþ?ÍÌÌÌÌÌì?ð?ÍÌÌÌÌÌô?š™™™™™é?š™™™™™É¿š™™™™™é?à?ÍÌÌÌÌÌô?ÍÌÌÌÌÌô?à¿@ø?š™™™™™ù?š™™™™™é?š™™™™™É?ffffffæ?333333ó?ffffffæ?š™™™™™ñ?ÍÌÌÌÌÌô?š™™™™™ñ?333333ã?ÍÌÌÌÌÌü?ffffffö?ð?š™™™™™ñ?š™™™™™ñ?333333ó?@333333ó?ÍÌÌÌÌÌô?ffffffö?333333ã?š™™™™™ñ?š™™™™™Ù?ð?ð?ÍÌÌÌÌÌô?š™™™™™É?ÍÌÌÌÌÌì?ð?š™™™™™é?ffffffö?ffffffæ?ffffffþ?š™™™™™é?š™™™™™ñ?333333ó?ÍÌÌÌÌÌì?ð?333333û?333333ó?ø?š™™™™™é?š™™™™™é?ÍÌÌÌÌÌì?š™™™™™ñ?ÍÌÌÌÌÌ쿚™™™™™é?ffffffæ?ð?ffffffö?š™™™™™¹?š™™™™™Ù?ø?š™™™™™é?š™™™™™é?333333ã?ÍÌÌÌÌÌì?ð?š™™™™™ñ?333333ó?ÍÌÌÌÌÌì?ð?333333ã?333333ó?ÍÌÌÌÌÌô?š™™™™™é?ÍÌÌÌÌÌô?à?ffffffö?ÍÌÌÌÌÌô?ffffffæ?ÍÌÌÌÌÌü?333333ó?ÍÌÌÌÌÌô?ÍÌÌÌÌÌô?š™™™™™ñ?ÍÌÌÌÌÌô?ð?333333û?à?ø?š™™™™™ñ?š™™™™™É?ÍÌÌÌÌÌô?ø?333333ã?ø?ø?ffffffæ?ffffffæ?ð?š™™™™™é?ffffffæ?333333ó?333333û?ffffffæ?333333ã?333333ó?ð?ffffffö?š™™™™™ñ?š™™™™™ñ?š™™™™™é?š™™™™™É?ÍÌÌÌÌÌ@š™™™™™ñ?š™™™™™ñ?ÍÌÌÌÌÌì?ffffffö?ð?š™™™™™Ù?333333ã¿333333ã?š™™™™™ñ?ffffffæ?333333ó?ffffffæ?ffffffö?š™™™™™ñ?333333ã?ÍÌÌÌÌÌì?ø?š™™™™™ñ?ø?333333ã?333333ó?š™™™™™é?à?š™™™™™ñ?ð?š™™™™™Ù¿š™™™™™ñ?š™™™™™é?š™™™™™ñ?ÍÌÌÌÌÌì?333333 @ffffffæ¿ffffffæ?333333ã?ÍÌÌÌÌÌì?333333ã?š™™™™™É¿ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?333333ã¿ø?š™™™™™ñ?š™™™™™ù?š™™™™™ù?333333ã?333333û?ø?333333ã?š™™™™™ñ?š™™™™™¹¿ffffffö?š™™™™™é?š™™™™™ñ?š™™™™™Ù?ð?ffffffæ?ffffffþ?333333ó?ð?à?333333ã?ffffffö?ø?ÍÌÌÌÌÌô?ð?ø?ð?ffffffö?ø?ffffffö?333333û?ø?š™™™™™é?333333ó?š™™™™™É?š™™™™™ñ?š™™™™™ù?š™™™™™ù?ÍÌÌÌÌÌô?ÍÌÌÌÌÌô?333333û?333333ó?š™™™™™é?ÍÌÌÌÌÌì?à?@š™™™™™ù?333333ó?š™™™™™é?ð?333333û?333333û?ð?333333ã?ÍÌÌÌÌÌì?à?333333ó?ffffffö?ÍÌÌÌÌÌì?š™™™™™ñ?333333ã?333333ã?ð¿ÍÌÌÌÌÌì?333333ã?333333ã?š™™™™™é?ÍÌÌÌÌÌü?š™™™™™É?š™™™™™ñ?š™™™™™¹¿š™™™™™é?ð?ð?ÍÌÌÌÌÌì?š™™™™™Ù?ð?š™™™™™é?ÍÌÌÌÌÌì?ð?ffffffæ?ÍÌÌÌÌÌì?ÍÌÌÌÌÌü?ffffffö?ÍÌÌÌÌÌì?š™™™™™ñ?à?à?š™™™™™@š™™™™™É?333333ã?ð?ÍÌÌÌÌÌô?333333ó?š™™™™™ù?333333Ó?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?ffffffæ¿ø?š™™™™™É¿ÍÌÌÌÌÌô?š™™™™™É?333333ó?ø?333333û?š™™™™™Ù¿333333û?š™™™™™é?ð?ÍÌÌÌÌÌü?ð?š™™™™™é?333333ã?š™™™™™ù?ð?333333ã?ÍÌÌÌÌÌô?333333û?š™™™™™é?333333ó?š™™™™™Ù?333333@š™™™™™é?ð?š™™™™™ñ?ffffffö?333333û?ffffffæ?š™™™™™ñ?333333ó?ÍÌÌÌÌÌô?ffffffæ?ffffffö?š™™™™™ù?ð?ffffffö?ð?@ÍÌÌÌÌÌì?333333ã?š™™™™™É¿à?ÍÌÌÌÌÌ@333333û?ÍÌÌÌÌÌì?ffffffæ?š™™™™™Ù?ÍÌÌÌÌÌì?ÍÌÌÌÌÌü?ÍÌÌÌÌÌì?ð?ð?š™™™™™é?à?333333ã?š™™™™™ñ?ø?š™™™™™é?š™™™™™ù?š™™™™™Ù?š™™™™™é?ffffffæ?ÍÌÌÌÌÌì?ffffffæ?333333ã?ø?333333û?ÍÌÌÌÌÌì?ffffffö?ffffffþ?š™™™™™ñ?ø?à?ÍÌÌÌÌÌô?ÍÌÌÌÌÌô¿333333ó?š™™™™™¹¿333333ã?333333ó?ð?š™™™™™ñ?333333ã?š™™™™™ñ?š™™™™™ñ?333333Ó?š™™™™™ñ?ffffffæ?333333ó?ð?ffffffæ?š™™™™™é?333333ã¿333333ã?š™™™™™Ù?333333û?ÍÌÌÌÌÌô?ÍÌÌÌÌÌô?š™™™™™É?ffffffö?š™™™™™¹?333333Ó¿ø?š™™™™™ñ?ð?ffffff@333333ã?333333ó?333333ó?ÍÌÌÌÌÌü?š™™™™™é?š™™™™™ù?ø?ffffffö?š™™™™™ñ?ffffffæ?333333ã?ð?ÍÌÌÌÌÌì?š™™™™™ñ?333333ó?à?ÍÌÌÌÌÌô?333333û?ffffffö?333333û?333333ó?333333ó?š™™™™™é?ð?ÍÌÌÌÌÌì?š™™™™™é?ð?š™™™™™Ù?ffffffö?ð?ÍÌÌÌÌÌì?ø¿à?ð?ø?333333û?š™™™™™@ø?333333û?333333@ffffffö?ffffffæ?ffffff@333333ó?ffffffö?ÍÌÌÌÌÌü?š™™™™™é?333333û?@ffffff@ÍÌÌÌÌÌ@ffffffæ?š™™™™™É?ÍÌÌÌÌÌü?š™™™™™ @ø?ÍÌÌÌÌÌü?ffffffæ?š™™™™™ù?ÍÌÌÌÌÌü?ÍÌÌÌÌÌ@@š™™™™™¹?š™™™™™é¿333333ó?š™™™™™é?@ÍÌÌÌÌÌü?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?ø?333333 @@à?ð?333333ã?ffffffþ?ffffffþ?š™™™™™é?ÍÌÌÌÌÌô?ffffffö?š™™™™™É?ffffffö?à?333333û?ffffffö?š™™™™™é?ÍÌÌÌÌÌ@333333ã?@ffffffö?333333ã?š™™™™™¹?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?@333333@ @333333@š™™™™™é?à?333333@ø?333333û?333333@ÍÌÌÌÌÌ@ø?ffffffö?ffffff @ÍÌÌÌÌÌ@š™™™™™ñ¿š™™™™™Ù¿š™™™™™@š™™™™™é?333333ã?@à?š™™™™™ñ?ffffffþ?ø¿ffffffö¿333333ã?š™™™™™Àffffffæ?à?š™™™™™¹?333333û?333333Ó?ÍÌÌÌÌÌ@333333@ø?š™™™™™ @š™™™™™ñ?ÍÌÌÌÌÌì¿ffffffæ?ð?ffffffö?@333333ó?š™™™™™Ù?à?š™™™™™@ø¿à?ÍÌÌÌÌÌô?š™™™™™ù?ffffff@š™™™™™@ÍÌÌÌÌÌü?š™™™™™@ÍÌÌÌÌÌü?@ÍÌÌÌÌÌô?ffffffö?333333û?ð?ÍÌÌÌÌÌ@ffffff@š™™™™™ñ?@333333ó?ÍÌÌÌÌÌü?333333ó?š™™™™™ñ?333333 @š™™™™™é?ÍÌÌÌÌÌô?333333@š™™™™™@š™™™™™@333333@333333@ð?ffffffö?ÍÌÌÌÌÌü?333333û?ffffff@ÍÌÌÌÌÌ@š™™™™™ @ffffffþ?333333@š™™™™™ù?333333û?ffffffö?333333@333333û?ÍÌÌÌÌÌ@ffffffö?ffffffþ?ÍÌÌÌÌÌ@333333ó?ffffffæ?ÍÌÌÌÌÌü?š™™™™™ù?ffffff@š™™™™™é?ffffff @š™™™™™@@š™™™™™é?@ÍÌÌÌÌÌ@ÍÌÌÌÌÌô?š™™™™™ñ?333333ó?ffffffö?š™™™™™ @ø?ø?333333ó?ffffffö?š™™™™™ù?333333û?š™™™™™Ù¿à?333333 @ffffffæ?ÍÌÌÌÌÌì¿333333Ó¿š™™™™™é?š™™™™™É?š™™™™™Ù?333333ã¿à?@š™™™™™é?ÍÌÌÌÌÌì?333333ó¿ffffffö?@ffffff@ffffffö?333333Àš™™™™™@333333ã?333333ó?ffffffö¿ffffff@333333@333333@@333333ó¿ffffff@@ÍÌÌÌÌÌ@š™™™™™ @ÍÌÌÌÌÌü¿à¿š™™™™™@ffffff濚™™™™™Ù?ð?333333ã¿ÍÌÌÌÌÌü?š™™™™™ù?ÍÌÌÌÌÌ@š™™™™™@š™™™™™ù?à?ÍÌÌÌÌÌô?333333ó?ffffffþ?ø?š™™™™™é?š™™™™™Ù?ffffff@333333ã? @333333ã¿ffffff@à?333333ó¿š™™™™™ù?ø?š™™™™™ñ¿ÍÌÌÌÌÌü?š™™™™™Ù¿@ð¿ffffffþ?333333û?š™™™™™ñ?š™™™™™ù?ffffff@à¿333333ã?š™™™™™ñ?ffffff@ÍÌÌÌÌÌ@333333û¿333333ã?ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?ffffffæ?ÍÌÌÌÌÌ@ffffff@ð?ÍÌÌÌÌÌü?333333Ó?ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?š™™™™™ù?@ÍÌÌÌÌÌ@ @@š™™™™™é?š™™™™™Ù?à?@ÍÌÌÌÌÌì?@ÍÌÌÌÌÌ@ffffffæ?ÍÌÌÌÌÌü?š™™™™™ñ?ÍÌÌÌÌÌô?ffffffö?ð?š™™™™™ù?ffffffþ?333333ã?ÍÌÌÌÌÌì?333333ã?333333@ÍÌÌÌÌÌü?333333ã?š™™™™™ñ¿š™™™™™ù?333333û¿333333@ffffffö?š™™™™™@š™™™™™é¿š™™™™™ù?š™™™™™ñ?333333û?ÍÌÌÌÌÌô¿@333333ã?333333ó?ÍÌÌÌÌÌü¿333333@333333@333333ã?333333@ÍÌÌÌÌÌì?ÍÌÌÌÌÌü?333333ó?ø?@ø?ffffffö?š™™™™™é?ø?š™™™™™Ù?333333û¿š™™™™™@š™™™™™ñ?333333û?ø?š™™™™™À333333Ó?ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?ffffff@ffffffö?333333@ÍÌÌÌÌÌì?ffffffæ?ÍÌÌÌÌÌü?333333 @333333Ó?ÍÌÌÌÌÌü?333333ã?ffffffö?ð?š™™™™™@š™™™™™@š™™™™™ù?ÍÌÌÌÌÌü?à?š™™™™™ñ?ffffff@ÍÌÌÌÌÌì¿333333ó?š™™™™™é?à?ø?ffffff@333333ã?ffffff@333333û¿š™™™™™¹¿ffffff@š™™™™™ñ?š™™™™™@333333û?@333333Ó¿š™™™™™é?à?à?š™™™™™Àffffffæ?ÍÌÌÌÌÌô¿333333 @@š™™™™™Ù?ð?ffffffö?š™™™™™@@333333Ó?š™™™™™ù¿333333Ó?333333ã?ÍÌÌÌÌÌ @ð?ffffffþ?ÍÌÌÌÌÌô?š™™™™™Àš™™™™™¹?ffffffþ?333333@ÍÌÌÌÌÌì?ÍÌÌÌÌÌ@333333ã?ÍÌÌÌÌÌü¿š™™™™™@ÍÌÌÌÌÌ@@š™™™™™ñ?333333@ffffff @à?333333@ffffff@ÍÌÌÌÌÌ @š™™™™™ù?333333@333333Ó?š™™™™™é?333333ã?ð?333333ã?ÍÌÌÌÌÌ@ÍÌÌÌÌÌô?333333Ó?333333û¿š™™™™™¹?ø?š™™™™™ @ÍÌÌÌÌÌü¿à?ffffff@š™™™™™@ÍÌÌÌÌÌ@š™™™™™@š™™™™™ù?ð?333333ã?@ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?333333 @à?ÍÌÌÌÌÌü?333333ó?ffffff@ÍÌÌÌÌÌü?à?ffffffæ?ffffff@š™™™™™ñ?ffffffæ?š™™™™™é?ÍÌÌÌÌÌ@ffffffþ?333333@à?š™™™™™@333333ã?@@š™™™™™ @š™™™™™1@ffffff,@€8@ÍÌÌÌÌÌ@š™™™™YB@+@ffffff3@ÍÌÌÌÌÌ&@ÍÌÌÌÌÌ:@3333333@ÍÌÌÌÌÌ@333333@ffffff@@33333óF@š™™™™™-@ffffff6@ÍÌÌÌÌÌ,@ÍÌÌÌÌÌ/@333333"@ffffff+@€2@<@333333"@fffffæ;@ÍÌÌÌÌÌÀ€5@ffffff.@ffffff'@ffffffÀÍÌÌÌÌÌ)Àš™™™™™#@ffffff6@333333/@333333'@333333>@š™™™™™,@ÍÌÌÌÌÌ/@333333-@fffffæ4ÀÍÌÌÌÌÌ,@š™™™™™0@ÍÌÌÌÌÌ5@ÍÌÌÌÌŒC@333333 @ø?333333.@fffffæ1@š™™™™4@@ffffff@ÍÌÌÌÌÌ"@333333+@ÍÌÌÌÌLG@<@€9@fffffæ6@ffffff@33333³3@333333=@@ffffff.@fffffæ0@ÍÌÌÌÌL<@š™™™™™M@ÍÌÌÌÌÌ'@ffffff @š™™™™;@ÍÌÌÌÌÌ<@ffffffÀffffff<@3333332@ÍÌÌÌÌL0@ @š™™™™™>@š™™™™™*@333333+@š™™™™™#@;@š™™™™C@ÍÌÌÌÌL@@š™™™™™8@ÍÌÌÌÌÌ/@ÍÌÌÌÌÌÀ333333D@@B@ÍÌÌÌÌLI@33333³7@:@ÍÌÌÌÌLF@ffffff"@333333@ÍÌÌÌÌÌ @ÍÌÌÌÌÌ@ÍÌÌÌÌÌÀ@ÍÌÌÌÌÌ+@ffffff)@ffffff@€>@ffffff@ÀI@š™™™™™ ÀÍÌÌÌÌLD@ÍÌÌÌÌÌ9@333333@ffffff@š™™™™™2@fffff¦@@fffffæ7@333333@ffffff$@ffffff0@fffffæ?@ÍÌÌÌÌÌ8@š™™™™™@3333331@@š™™™™™@ffffff:@ÍÌÌÌÌL6@ÍÌÌÌÌÌ@š™™™™™À333333@@ffffff5@š™™™™™!@ÍÌÌÌÌÌ,@+@ÍÌÌÌÌÌ0@33333³8@š™™™™™5@3333335@š™™™™A@š™™™™™/@333333H@333333E@ÍÌÌÌÌÌ@€6@ @š™™™™™G@š™™™™™ @ÍÌÌÌÌÌ @ÍÌÌÌÌLG@333333@@š™™™™4@š™™™™™.@ÍÌÌÌÌÌ)@ffffff+@ÍÌÌÌÌÌ(@ÍÌÌÌÌÌ%@333333À'@ÍÌÌÌÌL5@3333335@33333³:@ffffff6@š™™™™YH@š™™™™™@333333;@š™™™™™/@ÍÌÌÌÌÌ;@ÍÌÌÌÌÌ@?@fffffæ4@À333333@ÍÌÌÌÌÌ%@@š™™™™™$@#Àš™™™™™*@š™™™™™7@€3@333333@ÀD@fffffæ3@ffffff'@)@š™™™™™A@333333.@š™™™™™ @ffffff.@333333Àffffffö¿9@ffffff+@3333334@ffffff@333333%À333333;@@33333³=@ffffff/@ÍÌÌÌÌÌ"@fffffæB@333333@š™™™™YD@L@š™™™™™@333333+@š™™™™™0@ÍÌÌÌÌÌ.@ÍÌÌÌÌŒB@ÍÌÌÌÌLD@ffffff@š™™™™E@š™™™™F@ÍÌÌÌÌÌ9@€1@ÍÌÌÌÌÌ@333333@33333P@333333@š™™™™™!@8@ÍÌÌÌÌÌ(@ @€9@ÍÌÌÌÌÌ@ffffffþ?33333³?@ffffff1@333333-@ÍÌÌÌÌÌ/@š™™™™™%@ÍÌÌÌÌL0@ffffff$@ÍÌÌÌÌŒH@33333³2@2@ÍÌÌÌÌÌ3@33333sI@€F@š™™™™™$@333333=@(@€=@ÍÌÌÌÌL@@333333+@ffffff(@+@ÍÌÌÌÌÌ/@fffffæ6@ÍÌÌÌÌÌ@33333óF@ÍÌÌÌÌÌ6@ÍÌÌÌÌÌü?š™™™™™É?š™™™™™ù¿ÍÌÌÌÌÌ@.ÀÍÌÌÌÌÌÀ333333"Àš™™™™™-@3333332@š™™™™™@3333334@fffffæ?@ffffff,@ÍÌÌÌÌÌü?ÍÌÌÌÌL;@4@š™™™™™8@ÍÌÌÌÌÌ$@fffffæ0@333333㿚™™™™™@š™™™™™!@š™™™™Y@@š™™™™™5@8@&@ÍÌÌÌÌÌ*@š™™™™™,@ÍÌÌÌÌL?@fffffæ>@š™™™™™3@ffffff>@ffffff*@š™™™™™À"@š™™™™™@š™™™™™Ù?ÍÌÌÌÌÌü¿@A@ffffff9@ffffff%@ÍÌÌÌÌÌ$@ÍÌÌÌÌÌ@€>@3333333@333333ã?š™™™™6@L@333333;@ÍÌÌÌÌÌì?ffffff2@3333335@ÍÌÌÌÌÌ$@ÍÌÌÌÌÌ)@à?ÍÌÌÌÌÌ Àš™™™™™&@ffffff;@fffffæ0@3333330@š™™™™™E@š™™™™0@š™™™™™.@€9@333333@š™™™™™ÀÍÌÌÌÌÌ(@š™™™™™1@ffffff!@š™™™™™0@ffffff6@ÍÌÌÌÌÌ@ø?š™™™™0@ÍÌÌÌÌÌ#@33333³2@ffffffÀÍÌÌÌÌL0@333333(@š™™™™™2À€7@ffffff,@š™™™™™@fffff&A@33333³M@ÍÌÌÌÌÌ:@ÍÌÌÌÌÌ(@$@ffffff#@ffffff7@ÍÌÌÌÌL5@ÍÌÌÌÌÌ*@33333³6@ffffff@ÍÌÌÌÌL2@ÍÌÌÌÌÌ,@ÍÌÌÌÌÌ-@š™™™™™1@ffffff4@ÍÌÌÌÌÌ@š™™™™™1@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ/@3333330@ÍÌÌÌÌÌ9@ø¿ffffffÀÍÌÌÌÌL4@š™™™™3Àš™™™™<@ÍÌÌÌÌÌ%@š™™™™™B@ffffffE@8@ÍÌÌÌÌÌ3@š™™™™=@@š™™™™™6@333333㿚™™™™Ù@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ0@=@333333#@fffffæ7@333333+@ÍÌÌÌÌL7@ÍÌÌÌÌÌ@fffffæ0ÀÍÌÌÌÌÌ@3333339@ffffff$@ffffffþ?€:@€?@ÍÌÌÌÌÌ/@ÍÌÌÌÌÌ@@ÀE@333333(@333333?@ÍÌÌÌÌL;@ffffffÀš™™™™™$@€;@33333³:@ÀE@š™™™™™#@333333?@,@š™™™™™)@ÍÌÌÌÌÌ,Àš™™™™™%@€9@š™™™™?@ffffff<@33333³5À€9@š™™™™™3@š™™™™™:@fffff&P@š™™™™4@ÍÌÌÌÌÌ&@7@)@ÀE@33333óA@š™™™™D@33333³2@š™™™™™?@ffffff@ffffff"@ffffff1@€4@ffffff4@ÍÌÌÌÌÌ:@š™™™™™@ÍÌÌÌÌÌÀfffffæ>@ÍÌÌÌÌÌ.@ÍÌÌÌÌÌÀ3333331@333333?@333333+@3333330@ffffff @333333/À333333@€<@fffffæ0@ÍÌÌÌÌÌ?@(@33333sA@333333;@š™™™™™9@À333333#@ÍÌÌÌÌÌ&@š™™™™™'@3333334@š™™™™™ñ¿ffffff=@€EÀÍÌÌÌÌL=@€5@-@€=@33333óB@ffffffB@fffffæ2@333333U@33333³6@fffffæ6@?@š™™™™™A@333333=@33333³5@ÍÌÌÌÌÌ@F@ÍÌÌÌÌŒN@ÍÌÌÌÌÌ?@ÍÌÌÌÌLE@ÍÌÌÌÌL2@1@š™™™™7@š™™™™YI@š™™™™™:@ÍÌÌÌÌŒC@333333)@ÍÌÌÌÌŒC@ÍÌÌÌÌÌ*@š™™™™B@š™™™™™C@š™™™™™*@3333330Àffffffö¿ÍÌÌÌÌÌ,@33333óD@€<@š™™™™0@€A@š™™™™?@@G@=@š™™™™™1ÀÍÌÌÌÌÌ3@3333335@fffff&A@š™™™™™I@ @ffffff @ÍÌÌÌÌL:@33333³2@š™™™™™=@š™™™™™"@š™™™™3@3333336@3@ÍÌÌÌÌŒL@š™™™™™?@ÍÌÌÌÌŒG@33333³>@,@€4@A@š™™™™™&@š™™™™A@š™™™™ÙB@33333sK@š™™™™9S@€3@ffffff'@33333óG@ÍÌÌÌÌÌC@ffffff"@€H@fffff¦@@€:@+@ÍÌÌÌÌÌK@33333sG@ÍÌÌÌÌÌ@š™™™™™@ÀD@š™™™™E@@B@š™™™™™H@33333³2@ÍÌÌÌÌÌ@33333sJ@7@33333óE@ÍÌÌÌÌÌ?@.ÀÍÌÌÌÌŒJ@*@333333@€0@@.@fffffæ=@ÍÌÌÌÌL9@š™™™™YE@ÍÌÌÌÌÌ.@7@#@33333óL@ @fffff&S@33333³@@ffffff@@š™™™™ÙA@3333330@>@+@33333³5@ÍÌÌÌÌŒB@fffffS@ÍÌÌÌÌÌA@3333336@š™™™™>@4@ffffff%@33333óA@ffffff@@(@š™™™™™%@€4@ffffff&@33333óE@3333330@ÍÌÌÌÌÌ8@ffffff5@8@@I@fffffæ9@>@33333³I@š™™™™ÙA@€P@fffffæO@7@ÍÌÌÌÌL;@ffffff0@33333³M@>@ffffff6@@O@33333³>@fffffæ3@fffffæB@ÍÌÌÌÌÌ9@ÍÌÌÌÌÌ8@3333335@@@ÍÌÌÌÌÌ7@ÍÌÌÌÌÌ)@33333³4@ÍÌÌÌÌLA@ÍÌÌÌÌŒB@fffff&B@ffffff<@ P@€4@ÍÌÌÌÌ I@3333335@33333³M@š™™™™™8@I@€9@333333'@š™™™™ÙE@ffffff3@ffffff*@fffffæ2@333333Ó¿fffff&B@š™™™™™@@333333<@*@€H@š™™™™=@fffffæ5@ÍÌÌÌÌÌ!@š™™™™C@€F@š™™™™™(@#@333333Àš™™™™™@ÍÌÌÌÌL:@š™™™™™/@333333.@š™™™™™)@ÍÌÌÌÌÌ"@ÍÌÌÌÌL@@333333!@€0@ÍÌÌÌÌL8@š™™™™™6@@J@ÍÌÌÌÌÌ-@ @ÍÌÌÌÌ,R@š™™™™™#@5@š™™™™™ù¿33333sC@33333óL@33333³K@3@3333339@ÍÌÌÌÌ L@fffffæN@fffffÆU@š™™™™™?@333333@G@@33333³7@33333³2@š™™™™™.@ffffff#@fffffæ4@3333332@333333*@33333óF@333333C@ffffff9@š™™™™™6@3333335@fffffæ7@33333³9@fffff¦M@ffffff8@5@š™™™™ÙI@€J@ÍÌÌÌÌLS@333333@ffffffF@333333/@€7@G@3333337@š™™™™™¹¿@@@ffffff+@33333óE@š™™™™™ÀÍÌÌÌÌŒN@ÍÌÌÌÌL@@#@333333)@,@Àš™™™™™Àffffff@š™™™™™+@š™™™™™<@à¿ÍÌÌÌÌÌ%@ÍÌÌÌÌL8@@E@fffffæ1@€7@ÍÌÌÌÌLG@ffffff9@š™™™™ÙA@(@;@š™™™™™#@š™™™™™,@ÍÌÌÌÌÌ>@š™™™™H@š™™™™H@H@ffffff4@š™™™™0@33333³0@33333sG@ÀA@A@š™™™™YG@ÍÌÌÌÌÌ1@@€1@š™™™™™'@&@ÍÌÌÌÌÌ@fffff¦F@ÍÌÌÌÌLD@ffffff+@33333³1@ÍÌÌÌÌÌ*@fffffæL@33333³>@š™™™™™@@fffff¦O@@š™™™™@@333333<@fffff&C@@ffffff8@333333(@ffffff@ @33333sH@ffffff6@ÍÌÌÌÌÌ7@ÍÌÌÌÌÌ;@333333D@33333sA@š™™™™™<@fffffæ8@š™™™™™¹?fffffæ7@3333339@2@š™™™™™<@€>@ffffff1@ÍÌÌÌÌÌ@€>@(@À€0@š™™™™™6@8@Àš™™™™™ À€2@ffffff/@š™™™™D@š™™™™S@ÍÌÌÌÌLA@33333³O@3333334@333333)@š™™™™Y@@fffffæH@333333.@333333@@ffffff#@€;@3333334@€B@€@@š™™™™@@ffffff5@š™™™™™4@š™™™™™+@š™™™™™>@333333@š™™™™@@333333$@š™™™™™@€>@333333û¿33333³>@fffffæ=@š™™™™™3@fffffæD@ffffffE@ÍÌÌÌÌL>@33333sF@3333332@ÍÌÌÌÌLC@ffffffþ¿33333sF@333333)@3333334@ÍÌÌÌÌÌÀš™™™™™*@333333@33333³D@ÍÌÌÌÌ N@#@333333 À333333-@ÍÌÌÌÌ F@ÍÌÌÌÌÌ5@333333@%@33333³@@š™™™™™3@ÀM@33333óI@š™™™™™8@33333³B@ÍÌÌÌÌÌ6À333333ÀÍÌÌÌÌÌ9@33333SP@€?@333333K@š™™™™™)@333333@š™™™™™?@fffffæ;@ffffff@š™™™™1@š™™™™ÙE@ffffffM@ÍÌÌÌÌÌ?@ffffffö¿333333H@J@ÍÌÌÌÌŒB@33333óT@š™™™™™6@.@ffffff9@33333³5@ÍÌÌÌÌÌI@€I@š™™™™YG@33333³4@ÍÌÌÌÌÌ#@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ2@š™™™™G@ffffff@33333³7@fffffæF@š™™™™9@33333S@@H@8@@fffffæ4@333333F@8@ÍÌÌÌÌL9@€>@ÍÌÌÌÌÌ(ÀÍÌÌÌÌÌ2@333333B@@B@ÍÌÌÌÌÌE@ÍÌÌÌÌL1@š™™™™ÙB@G@š™™™™@@@š™™™™™-@š™™™™™@@ÍÌÌÌÌÌ9@€B@ÍÌÌÌÌÌ@š™™™™ÙG@@CÀÍÌÌÌÌ F@33333ó@@š™™™™™¹?333333Ó?š™™™™™É¿š™™™™™Ù¿š™™™™™É?š™™™™™Ù¿š™™™™™ñ?š™™™™™¹¿š™™™™™É¿š™™™™™¹¿333333ã?š™™™™™É¿š™™™™™É¿š™™™™™É?š™™™™™É¿š™™™™™É¿ÍÌÌÌÌÌì?š™™™™™É?ffffffæ?ÍÌÌÌÌÌô?š™™™™™¹?š™™™™™¹¿š™™™™™É?š™™™™™É?š™™™™™¹¿š™™™™™¹?š™™™™™¹?š™™™™™É¿š™™™™™é?š™™™™™¹?š™™™™™¹¿š™™™™™¹¿š™™™™™¹¿š™™™™™É?š™™™™™Ù?333333Ó¿ÍÌÌÌÌÌì?š™™™™™É?š™™™™™¹¿š™™™™™¹?š™™™™™¹¿š™™™™™É¿š™™™™™¹?ð¿333333Ó?à?š™™™™™É¿š™™™™™¹?š™™™™™É¿š™™™™™¹¿š™™™™™Ù?š™™™™™¹¿š™™™™™¹?à?š™™™™™¹¿š™™™™™é?š™™™™™Ù¿333333Ó?š™™™™™¹?à?š™™™™™Ù¿š™™™™™É¿š™™™™™¹¿à?š™™™™™¹?333333Ó?š™™™™™É?š™™™™™É¿š™™™™™ñ¿à?ÍÌÌÌÌÌô¿ø?š™™™™™É¿à?ð¿à¿š™™™™™¹?š™™™™™¹¿à¿333333ã?š™™™™™¹¿š™™™™™¹¿333333Ó¿š™™™™™¹?š™™™™™¹?š™™™™™É¿š™™™™™¹?ffffffö¿à?š™™™™™¹?š™™™™™¹¿š™™™™™é?333333Ó¿š™™™™™É?š™™™™™É¿š™™™™™Ù¿š™™™™™É?333333Ó?š™™™™™é?š™™™™™¹?š™™™™™É¿ÍÌÌÌÌÌì?333333ã?ffffffæ?333333ã?š™™™™™Ù¿š™™™™™Ù?š™™™™™é¿š™™™™™É¿à¿à?š™™™™™¹¿š™™™™™¹?š™™™™™¹¿š™™™™™¹?ffffff濚™™™™™¹¿à?š™™™™™¹?š™™™™™¹¿š™™™™™É¿à¿š™™™™™É¿š™™™™™¹?š™™™™™É¿š™™™™™¹?š™™™™™É¿333333㿚™™™™™É¿š™™™™™é¿š™™™™™É?š™™™™™É¿š™™™™™¹¿š™™™™™Ù?š™™™™™¹?š™™™™™¹¿š™™™™™¹?š™™™™™¹¿333333ã?š™™™™™Ù?ffffffæ?š™™™™™¹?š™™™™™Ù?ÍÌÌÌÌÌì¿333333Ó¿š™™™™™¹¿š™™™™™É¿š™™™™™É?š™™™™™¹?š™™™™™É?š™™™™™É?š™™™™™¹¿š™™™™™Ù?333333ã?ffffffæ?š™™™™™É?@333333ã?333333ã¿333333Ó¿š™™™™™Ù¿ø¿à?š™™™™™É?š™™™™™É?333333ó?š™™™™™É?333333㿚™™™™™¹?ffffffæ?š™™™™™¹?š™™™™™é¿à¿š™™™™™¹¿333333Ó?ffffffö¿ffffffæ?š™™™™™¹?࿚™™™™™¹¿š™™™™™Ù¿š™™™™™¹?š™™™™™É?333333Ó?333333Ó¿š™™™™™¹¿š™™™™™¹?333333ã¿333333ã?š™™™™™¹¿333333ã?š™™™™™¹?š™™™™™¹?š™™™™™Ù?š™™™™™ñ?š™™™™™É¿š™™™™™é?š™™™™™¹¿š™™™™™¹?š™™™™™¹?š™™™™™¹¿à¿š™™™™™É?š™™™™™Ù¿š™™™™™É¿ffffffæ?š™™™™™É¿š™™™™™¹?š™™™™™¹¿š™™™™™ñ?333333ó?š™™™™™Ù?333333ã?333333ã?š™™™™™É¿333333ã?ffffffæ?š™™™™™¹¿š™™™™™¹¿à¿š™™™™™¹¿333333ã?à¿à¿š™™™™™É¿š™™™™™¹?š™™™™™É?š™™™™™É?š™™™™™¹?š™™™™™¹?š™™™™™¹¿š™™™™™É¿š™™™™™¹?š™™™™™Ù?š™™™™™É?333333ã?š™™™™™¹?333333ã?š™™™™™¹¿š™™™™™¹?ð?š™™™™™¹¿333333Ó?š™™™™™É¿š™™™™™Ù¿š™™™™™é?333333Ó¿š™™™™™¹¿ffffffæ¿ø?š™™™™™¹?š™™™™™É¿š™™™™™Ù?š™™™™™ñ¿333333ã?š™™™™™É?333333ã?š™™™™™¹¿à¿š™™™™™é¿š™™™™™¹?ÍÌÌÌÌÌì¿à?š™™™™™É?333333Ó¿š™™™™™É?š™™™™™É?š™™™™™¹¿à¿333333Ó?ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?333333Ó¿333333ã?333333ã?š™™™™™é?š™™™™™Ù¿š™™™™™É?ÍÌÌÌÌÌì?ffffffæ¿333333Ó?š™™™™™¹¿š™™™™™É¿ø?š™™™™™Ù¿à¿š™™™™™Ù¿š™™™™™¹¿š™™™™™¹?š™™™™™¹¿à¿š™™™™™¹¿š™™™™™É¿š™™™™™¹¿š™™™™™É?š™™™™™É?š™™™™™¹?š™™™™™É?š™™™™™É¿š™™™™™¹?š™™™™™É?ø?š™™™™™¹?š™™™™™é?࿚™™™™™¹?333333Ó?š™™™™™É?š™™™™™É?š™™™™™¹?š™™™™™¹?333333ó?ffffff濚™™™™™Ù?š™™™™™¹¿š™™™™™Ù?š™™™™™É¿š™™™™™Ù?š™™™™™¹¿ð?š™™™™™¹¿à¿š™™™™™Ù¿333333ó?ffffff濚™™™™™¹¿ffffffæ¿333333㿚™™™™™¹?ffffff@@š™™™™™¹¿š™™™™™¹¿š™™™™™¹?š™™™™™¹¿ÍÌÌÌÌÌô?š™™™™™¹?ffffffæ?š™™™™™É¿š™™™™™É¿à¿š™™™™™Ù?š™™™™™é?š™™™™™¹?333333Ó?š™™™™™¹?࿚™™™™™É?š™™™™™¹¿š™™™™™É?š™™™™™¹?ffffffæ¿ffffff濚™™™™™Ù?š™™™™™É¿š™™™™™é?š™™™™™¹¿@š™™™™™Ù?333333ó¿333333ã¿à?333333Ó¿333333û¿š™™™™™¹¿š™™™™™Ù?333333Ó?333333㿚™™™™™¹?࿚™™™™™Ù¿333333Ó¿š™™™™™¹¿333333ã?333333Ó¿š™™™™™¹?š™™™™™¹?ffffffæ?333333ó?333333ó?333333Ó?ÍÌÌÌÌÌÀš™™™™™ù¿333333㿚™™™™™Ù¿ffffffÀffffffÀÍÌÌÌÌÌÀ ÀÍÌÌÌÌÌÀ333333ã¿à¿š™™™™™Ù¿à¿ffffffæ¿333333%ÀÀÍÌÌÌÌÌÀÍÌÌÌÌÌü¿ÀÍÌÌÌÌÌô¿ÍÌÌÌÌÌì¿ffffffæ¿Àš™™™™™ ÀffffffÀÍÌÌÌÌÌü¿ð¿š™™™™™ À333333ã¿333333û¿ÍÌÌÌÌÌô¿ø¿ø¿333333㿚™™™™™ñ¿š™™™™™Ù¿333333ó¿333333ã¿à¿ÍÌÌÌÌÌü¿š™™™™™ÀÍÌÌÌÌÌÀ333333û¿ÍÌÌÌÌÌÀš™™™™™é¿à¿ffffffþ¿š™™™™™Àà¿ð¿š™™™™™%Àffffffþ¿333333ÀÍÌÌÌÌÌÀ333333ã¿à¿ø¿333333ó¿š™™™™™Àffffffæ¿333333㿚™™™™™Ù¿333333ó¿à¿ÍÌÌÌÌÌì¿ffffffö¿ffffffþ¿ÍÌÌÌÌÌô¿š™™™™™Ù¿333333ó¿ÍÌÌÌÌÌì¿333333㿚™™™™™é¿š™™™™™ À࿚™™™™™ñ¿333333ã¿ÍÌÌÌÌÌì¿ffffffÀÍÌÌÌÌÌÀffffffö¿ÀffffffÀš™™™™™!Àš™™™™™ÀÍÌÌÌÌÌÀÀš™™™™™Ù¿à¿š™™™™™é¿ffffffÀ333333û¿ ÀffffffÀ333333ã¿ÍÌÌÌÌÌô¿š™™™™™ñ¿š™™™™™À333333À333333ó¿333333ã¿ø¿š™™™™™ÀÍÌÌÌÌÌÀà¿333333 ÀffffffÀš™™™™™é¿ÍÌÌÌÌÌÀš™™™™™ Àš™™™™™ñ¿333333û¿š™™™™™À333333ã¿à¿ffffffÀÀÀ333333ó¿ÍÌÌÌÌÌÀffffffæ¿ÍÌÌÌÌÌ Àš™™™™™é¿Àš™™™™™Ù¿ÍÌÌÌÌÌü¿ÍÌÌÌÌÌÀÍÌÌÌÌÌì¿à¿ffffffÀÍÌÌÌÌÌÀÍÌÌÌÌÌô¿ð¿333333Àš™™™™™Àš™™™™™é¿š™™™™™ù¿333333$Àš™™™™™ Àø¿333333ã¿333333ã¿Àš™™™™™ñ¿À333333Àffffffæ¿ÍÌÌÌÌÌ쿚™™™™™Ù¿ÍÌÌÌÌÌ쿚™™™™™Ù¿€0Àffffffæ¿à¿ð¿ð¿ÍÌÌÌÌÌü¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌü¿à¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌÀš™™™™™Àà¿333333À333333û¿š™™™™™Àffffffö¿333333 Àà¿ffffffÀš™™™™™ù¿ø¿333333㿚™™™™™Ù¿333333(Àð¿ð¿à¿ÍÌÌÌÌÌü¿333333ã¿ÍÌÌÌÌÌü¿š™™™™™ Àø¿š™™™™™ ÀÍÌÌÌÌÌÀ333333ÀffffffÀš™™™™™Ù¿š™™™™™ñ¿ffffffæ¿Àffffff À333333À࿚™™™™™Ù¿333333ã¿ffffffæ¿ÍÌÌÌÌÌì¿ffffffö?ÍÌÌÌÌÌì?š™™™™™@š™™™™™ù?333333ó?33333³5@š™™™™™@š™™™™™@@š™™™™™ @š™™™™™ù?š™™™™™ñ?333333*@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@@@ffffff @à?ÍÌÌÌÌÌ@š™™™™™@@ffffffö?@ÍÌÌÌÌÌü?ffffff @333333@š™™™™™Ù?333333ó?ffffff@ffffff@ffffff@à?š™™™™™ @ð?ÍÌÌÌÌÌ@333333@ÍÌÌÌÌÌô?333333$@@333333û?ffffff@ffffff@ffffffæ?ÍÌÌÌÌÌì?š™™™™™ñ?š™™™™™$@ÍÌÌÌÌÌ@ø?ffffff@à?@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@@#@š™™™™™(@š™™™™™@ffffff@ffffff@333333@à?333333ó?ffffffþ?š™™™™™@ffffffæ?š™™™™™@333333@š™™™™™@333333û?š™™™™™'@š™™™™™ @333333 @š™™™™™@š™™™™™ñ?ffffff @ffffff@š™™™™™@ffffff)@ @ffffff @ffffff@ÍÌÌÌÌÌü?š™™™™™é?333333 @ffffffæ?ffffff@ð?ÍÌÌÌÌÌü?š™™™™™@š™™™™™ù?ffffff$@ffffff#@@š™™™™™Ù?@333333ã?@š™™™™5@ffffffþ?ffffffö?333333û?ffffff@ffffff$@@ffffffö?ÍÌÌÌÌÌ @š™™™™™ù?ÍÌÌÌÌÌ@ffffff@š™™™™™ñ?ÍÌÌÌÌÌì?š™™™™™ @ÍÌÌÌÌÌ@š™™™™™@333333û?ffffff @ffffffþ?š™™™™™@ffffff@š™™™™™@ @@ÍÌÌÌÌÌ@ @š™™™™™@333333 @@333333@š™™™™™@ø?ÍÌÌÌÌÌ@ÍÌÌÌÌÌì?ð?ÍÌÌÌÌÌ@ÍÌÌÌÌÌü?333333û?š™™™™™@@@@@à?333333 @333333@ÍÌÌÌÌÌ@ffffffæ?333333@à? @š™™™™™Ù?333333@333333@ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?š™™™™™ù?ffffff@ÍÌÌÌÌÌì?š™™™™™@š™™™™™ù?ÍÌÌÌÌÌ@@ffffff@š™™™™™ñ?š™™™™™ù?@@à?@ÍÌÌÌÌÌô?š™™™™™@@333333@ÍÌÌÌÌÌ@ffffffæ?@à?333333@ø?ffffff@š™™™™™(@ÍÌÌÌÌÌô?ffffff4@333333@š™™™™™Ù?ffffffæ?ffffffþ?ÍÌÌÌÌÌ*@!@à?à?333333$@ffffff@š™™™™™%@ÍÌÌÌÌÌ"@à?ffffff,@333333 @š™™™™™Ù?ffffff @š™™™™™ù?ÍÌÌÌÌÌ@@ð?ø?ffffff@333333ó?š™™™™™@ffffff@@!@ffffff@333333@ffffff@333333 @ffffff+@333333@@ÍÌÌÌÌÌì?ffffff @333333@š™™™™™é?ÍÌÌÌÌÌì?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333!@ÍÌÌÌÌÌô?ffffffö?š™™™™™é?333333@333333ã?ffffffö?ø?ÍÌÌÌÌÌ@333333@š™™™™™ @ÍÌÌÌÌÌü?š™™™™™Ù?à?@š™™™™™@š™™™™™@@ffffff@š™™™™™Ù?ffffffæ?š™™™™™é?ÍÌÌÌÌÌ@š™™™™™@ÍÌÌÌÌÌô?ÍÌÌÌÌÌ@ffffffæ?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ @333333@ffffffæ?333333ã?333333@333333ã?š™™™™™@ffffff @ffffff@ÍÌÌÌÌÌ @š™™™™™é?ÍÌÌÌÌÌì?333333@ffffff*@š™™™™™@š™™™™™@š™™™™™ù?ffffff @333333ã?ð?š™™™™™ @333333@ÍÌÌÌÌÌ @333333@@ffffff@ÍÌÌÌÌÌô?š™™™™™Ù?à?ÍÌÌÌÌÌü?@ffffff$@š™™™™™@š™™™™™@ð?ÍÌÌÌÌÌô?333333&@@ffffffæ?à?333333ó?ÍÌÌÌÌÌô?ÍÌÌÌÌÌô?ffffff@à?ÍÌÌÌÌÌ@'@ffffff @ffffffþ?ð?š™™™™™@333333@@ÍÌÌÌÌÌ@ffffff@333333ó?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@@ÍÌÌÌÌÌ@š™™™™™@ffffff"@ffffffþ?@à?š™™™™™ñ?@ffffff@š™™™™™Ù?@ffffff@333333 @@ffffff @333333ã?š™™™™™ @ÍÌÌÌÌÌô?333333ó?%@@333333 @ÍÌÌÌÌÌ@à?333333@ÍÌÌÌÌÌ@333333@ø?ffffff@@ffffff@333333!@š™™™™™@ÍÌÌÌÌÌì?ffffff$@ffffff@!@ffffff@ð?ÍÌÌÌÌÌì?ffffff@@333333&@ffffff@@333333ã?š™™™™™@ffffffæ?ffffffö?@.@š™™™™™@š™™™™™ù?š™™™™™@@ffffff@333333@ffffff@ÍÌÌÌÌÌ@333333%@@@ÍÌÌÌÌÌ@ffffff)@ffffff@ÍÌÌÌÌÌô?š™™™™™Ù?ffffff@š™™™™™!@š™™™™™ @š™™™™™@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333ã?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ0@ÍÌÌÌÌÌô?ø?š™™™™™é?š™™™™™é?ÍÌÌÌÌÌ @ÍÌÌÌÌÌü?@ð?ffffff@@ffffff@333333@ÍÌÌÌÌÌ @ð?ÍÌÌÌÌÌ@ffffffö?à?333333@ø?ÍÌÌÌÌÌ@ÍÌÌÌÌÌì?š™™™™™À࿚™™™™™é¿š™™™™™Ù¿ÍÌÌÌÌÌü¿333333ó¿š™™™™™Ù¿ffffffÀš™™™™™ñ¿ø¿ffffffÀffffffþ¿ÍÌÌÌÌÌ%ÀÍÌÌÌÌÌ À333333Àš™™™™™é¿333333ÀÍÌÌÌÌÌÀÍÌÌÌÌÌì¿333333,À333333ã¿ffffffæ¿Àffffffþ¿Àš™™™™™ñ¿À333333 ÀÍÌÌÌÌÌ À333333ó¿ffffffæ¿ÍÌÌÌÌÌÀÍÌÌÌÌÌÀ333333Àð¿ffffff!Àà¿333333ã¿ À333333Àš™™™™™ù¿ÍÌÌÌÌÌü¿ffffffÀÍÌÌÌÌÌü¿š™™™™™Àffffffæ¿333333ÀÍÌÌÌÌÌô¿ÍÌÌÌÌÌü¿333333㿚™™™™™Àš™™™™™Ù¿š™™™™™Ù¿š™™™™™ À333333Àš™™™™™é¿333333ÀÀÍÌÌÌÌÌÀà¿!ÀÍÌÌÌÌÌ Àš™™™™™ñ¿š™™™™™Ù¿ÍÌÌÌÌÌÀ333333û¿š™™™™™'Àffffffþ¿333333û¿š™™™™™ù¿š™™™™™(ÀÀÍÌÌÌÌÌÀffffffÀÀ333333Àà¿ffffffÀð¿ÍÌÌÌÌÌÀ333333ã¿ð¿Àš™™™™™À333333 Àš™™™™™ Àffffffö¿š™™™™™$ÀffffffÀffffffÀÍÌÌÌÌÌô¿š™™™™™ ÀffffffÀš™™™™™Àš™™™™™ñ¿333333ÀÀffffff#À333333ã¿333333û¿ffffffÀffffff Àš™™™™™ÀÀš™™™™™ñ¿333333ÀÍÌÌÌÌÌÀÀÍÌÌÌÌÌÀ333333ó¿š™™™™™é¿333333Àš™™™™™é¿ffffff ÀÍÌÌÌÌÌÀffffffÀÍÌÌÌÌÌ!À333333(ÀÀš™™™™™Ù¿ffffffÀÀÍÌÌÌÌÌô¿ÍÌÌÌÌÌü¿ Àš™™™™™ Àà¿ÍÌÌÌÌÌÀffffffö¿À ÀÍÌÌÌÌÌÀš™™™™™À333333Àš™™™™™ÀffffffÀ333333ÀffffffÀÍÌÌÌÌÌÀ'Àš™™™™™ù¿ffffffö¿ÍÌÌÌÌÌÀffffff À333333ã¿ÍÌÌÌÌÌÀ333333û¿À(Àš™™™™™Àš™™™™™ Àø¿ffffffÀÀš™™™™™ ÀÀÍÌÌÌÌÌì¿ø¿ffffffÀÍÌÌÌÌÌì¿à¿š™™™™™ÀffffffÀÀ333333À333333Àffffff*ÀÍÌÌÌÌÌÀÍÌÌÌÌÌô¿333333Àš™™™™™ÀÍÌÌÌÌÌÀÀ333333ÀÍÌÌÌÌÌÀ333333À333333ó¿ø¿333333ó¿š™™™™™ Àà¿333333ã¿ÍÌÌÌÌÌÀÍÌÌÌÌÌ&À333333Àš™™™™™ù¿ÍÌÌÌÌÌ(Àð¿ffffffæ¿333333㿚™™™™™Ù¿š™™™™™ÀÍÌÌÌÌÌô¿333333ó¿333333Àà¿ffffffÀ)Àš™™™™™é¿ø¿333333À$ÀffffffÀffffffÀš™™™™™Àð¿333333!Àffffffö¿333333ó¿à¿ÍÌÌÌÌÌÀÍÌÌÌÌL0ÀffffffÀffffffö?ffffff@š™™™™™ù?ffffff @ð?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ-@š™™™™™Ù?ÍÌÌÌÌÌì?ffffffæ?ÍÌÌÌÌÌô?ffffff@š™™™™™@à?š™™™™™Ù?à?ÍÌÌÌÌÌô?ð?333333ó?ÍÌÌÌÌÌ@@ffffff@333333û? @š™™™™™Ù?333333ã?ffffff@333333@ffffffæ?ffffff@š™™™™™Ù?ffffffæ?à?ÍÌÌÌÌÌô?ÍÌÌÌÌÌô¿ffffff濚™™™™™ñ¿333333û¿333333ó¿š™™™™™é?333333Ó¿à?ð¿ð?ffffffæ?333333Ó?333333Ó¿ffffffö?š™™™™™é?ð?à?š™™™™™Ù?š™™™™™Ù?ffffffæ?š™™™™™é?333333ó?š™™™™™É¿333333ã?š™™™™™Ù?333333Ó?š™™™™™ñ?333333Ó?š™™™™™Ù?ffffffæ?š™™™™™Ù?ÍÌÌÌÌÌì?š™™™™™É?333333Ó?ffffffæ¿à?š™™™™™Ù?333333Ó?š™™™™™Ù?ÍÌÌÌÌÌô?à?š™™™™™Ù?333333Ó¿333333Ó?333333ã?š™™™™™É?333333ã?š™™™™™é?ffffffæ?š™™™™™Ù?ÍÌÌÌÌÌì?š™™™™™ñ?š™™™™™¹?ffffffæ?ÍÌÌÌÌÌì?333333ã?333333ã?ffffffæ?š™™™™™Ù?š™™™™™¹?š™™™™™É?333333Ó?333333ã?ÍÌÌÌÌÌì?š™™™™™É¿ð?ÍÌÌÌÌÌì?333333ó?333333ã?ffffffæ¿à?š™™™™™é?š™™™™™É¿š™™™™™É?ffffffæ?š™™™™™Ù?š™™™™™É?š™™™™™ñ?š™™™™™É?à?333333ã?š™™™™™¹¿à?ø?š™™™™™ñ?ffffffæ?ffffffæ?š™™™™™Ù?à?š™™™™™Ù?ffffffæ?š™™™™™é?ffffffæ?š™™™™™¹?š™™™™™é¿š™™™™™Ù?333333ã?ffffffæ?š™™™™™É?ø?333333Ó¿š™™™™™Ù?à?333333㿚™™™™™Ù?š™™™™™ñ?à?š™™™™™é?333333ã?š™™™™™Ù?š™™™™™É?š™™™™™Ù?ffffffö¿š™™™™™É?š™™™™™Ù?à?à?333333Ó¿333333Ó¿ffffffæ?š™™™™™Ù?333333Ó¿š™™™™™Ù?à?333333ã?š™™™™™é?ffffffæ?ð?ffffffæ?š™™™™™¹?š™™™™™Ù?š™™™™™é?333333Ó?ffffffæ?333333Ó?š™™™™™Ù?ÍÌÌÌÌÌì?š™™™™™É?ÍÌÌÌÌÌô?ð?à¿ffffffæ?ð?ffffffæ?333333Ó?š™™™™™¹¿333333Ó?333333ã?333333ã?š™™™™™Ù¿ffffffæ?ffffffæ?š™™™™™¹?š™™™™™ñ?š™™™™™é?š™™™™™Ù?š™™™™™¹?333333ã?333333Ó?à¿333333ã?š™™™™™é?333333ã?š™™™™™Ù?à?à?ÍÌÌÌÌÌì?à?333333ã?š™™™™™Ù?ffffffæ¿ffffffö?š™™™™™é?š™™™™™Ù?š™™™™™É?ÍÌÌÌÌÌô?ffffffæ?š™™™™™¹?ffffff濚™™™™™Ù¿ffffffæ?š™™™™™Ù¿š™™™™™Ù?333333Ó?ÍÌÌÌÌÌì?ffffffæ?𿚙™™™™Ù?à?š™™™™™Ù?ð?š™™™™™¹?à?š™™™™™¹¿à¿ð?š™™™™™Ù?à¿333333ã?à?ffffffæ?š™™™™™Ù?ÍÌÌÌÌÌ@𿚙™™™™É?à¿à¿š™™™™™É?š™™™™™É?ffffffæ?à?333333㿚™™™™™É?ffffffæ?ffffffæ?333333ó?333333ã¿ø?ÍÌÌÌÌÌì?333333Ó¿ffffffæ?š™™™™™É?à?š™™™™™Ù?à?š™™™™™¹?333333ã?à?ÍÌÌÌÌÌô?333333ã?š™™™™™Ù?࿚™™™™™É?š™™™™™é?š™™™™™é?ffffffæ?ffffffæ?ÍÌÌÌÌÌì?333333Ó?š™™™™™ñ?ð?333333ã?š™™™™™é?333333ó?š™™™™™É?š™™™™™Ù?š™™™™™É¿š™™™™™Ù?š™™™™™ñ?ffffffæ?ÍÌÌÌÌÌì?š™™™™™Ù?333333ó?š™™™™™é?ffffffæ?333333ã?333333ã¿333333ó?ÍÌÌÌÌÌì?š™™™™™é?š™™™™™¹?š™™™™™Ù?š™™™™™é?š™™™™™Ù?ÍÌÌÌÌÌì?333333Ó?333333ã?à¿ffffffæ?ÍÌÌÌÌÌì?à¿333333ã?š™™™™™¹?š™™™™™Ù?š™™™™™Ù?333333Ó?à?š™™™™™¹?ffffffæ?ffffffæ?š™™™™™é¿333333ã?à?š™™™™™É?333333ã?à?š™™™™™É?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?à?ÍÌÌÌÌÌì?333333Ó?à?ÍÌÌÌÌÌô?š™™™™™é?š™™™™™Ù?ffffffæ?š™™™™™É?š™™™™™Ù¿ÍÌÌÌÌÌì?333333Ó?š™™™™™É?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™é?š™™™™™É?à?à?š™™™™™É?ð¿333333ó?š™™™™™É¿à?š™™™™™ñ¿333333ã?ffffffæ?ð?ÍÌÌÌÌÌì¿ÍÌÌÌÌÌì?333333ã?333333Ó?ÍÌÌÌÌÌô?333333ã?à?š™™™™™Ù?333333ó?ffffffæ?333333Ó?š™™™™™é?ffffffæ?333333ã?à?ffffff濚™™™™™@à?à?ffffffæ?333333ã?š™™™™™ñ?š™™™™™Ù?à?ÍÌÌÌÌÌô?š™™™™™Ù?š™™™™™É?ÍÌÌÌÌÌì?ð?š™™™™™é?š™™™™™é?333333ã?333333ã?š™™™™™é¿333333Ó¿š™™™™™Ù¿š™™™™™Ù?ÍÌÌÌÌÌì?š™™™™™ñ?à?333333ã?ffffffæ¿333333Ó?à?š™™™™™É¿š™™™™™É?ffffffæ?š™™™™™Ù?š™™™™™¹¿š™™™™™Ù¿ÍÌÌÌÌÌì?ð?š™™™™™¹¿š™™™™™ñ?333333ã¿à?à?š™™™™™Ù?333333Ó?333333Ó?ffffffæ?ð?š™™™™™É?ÍÌÌÌÌÌì?š™™™™™ù?333333ã?à?333333Ó?à?333333ó¿š™™™™™¹?ð¿ð¿ffffffæ?à?ffffffæ?333333Ó¿ffffffæ?ffffffæ?à¿333333ã?š™™™™™É?ffffffæ?333333ã?333333Ó?š™™™™™É?ð?333333Ó?à?ffffffö?š™™™™™Ù?ffffffæ?ffffffæ¿ÍÌÌÌÌÌì?š™™™™™É¿š™™™™™é¿ffffffæ?ÍÌÌÌÌÌì?š™™™™™Ù?333333û?š™™™™™¹?333333ã?333333ã?š™™™™™ñ?333333ã?ÍÌÌÌÌÌì?š™™™™™É¿š™™™™™Ù?ffffffæ?š™™™™™Ù?š™™™™™É?333333Ó¿š™™™™™Ù¿à?š™™™™™é?š™™™™™¹?ð?333333ó?333333ó?š™™™™™é?š™™™™™Ù?ffffffæ?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™¹?š™™™™™¹?ð?333333Ó?333333Ó?ffffffÀš™™™™™ñ¿š™™™™™Ù?333333㿚™™™™™Ù?š™™™™™É?ÍÌÌÌÌÌì¿333333ã¿ÍÌÌÌÌÌ@333333ã?333333ã¿ÍÌÌÌÌÌü?333333Ó?š™™™™™ñ?š™™™™™É?š™™™™™¹?333333ã?ffffffæ¿333333ã?@š™™™™™É?𿚙™™™™¹¿ffffffæ?ÍÌÌÌÌÌô?š™™™™™é?š™™™™™Ù¿à¿333333@ÍÌÌÌÌÌì?š™™™™™ñ?š™™™™™Ù?š™™™™™ñ?ffffffæ¿333333Ó¿š™™™™™É?š™™™™™é?333333Ó?333333ã¿333333ã¿ffffffæ?š™™™™™ù¿ð¿à¿ÍÌÌÌÌÌì¿333333ã?ffffffæ?ffffffæ¿ffffffæ?333333ã¿ffffffæ¿ð?š™™™™™É?ð?š™™™™™É?à¿333333ã?333333ã?š™™™™™É?š™™™™™é¿š™™™™™É?š™™™™™Ù¿š™™™™™Ù?š™™™™™É¿à¿333333û?ffffffþ?ffffffæ?š™™™™™é¿333333ã?š™™™™™é?š™™™™™Ù?ffffffæ?š™™™™™¹?ffffffæ¿à?333333Ó¿333333ó?ÍÌÌÌÌÌ@š™™™™™Ù?ffffffæ¿ffffffö?333333㿚™™™™™é¿333333ó?š™™™™™É?333333ó?ø?š™™™™™Ù¿ÍÌÌÌÌÌô?š™™™™™É¿š™™™™™ù¿ffffffþ?š™™™™™ñ¿ÍÌÌÌÌÌô¿š™™™™™é¿ÍÌÌÌÌÌì¿ð?š™™™™™ñ?à¿333333ã?𿚙™™™™Ù?ø¿à?ð?ø?š™™™™™É¿š™™™™™¹?333333Ó?š™™™™™Ù¿ÍÌÌÌÌÌ쿚™™™™™é?333333ã¿à?ÍÌÌÌÌÌô?ffffffþ?à¿à¿333333Ó¿š™™™™™Ù¿à¿ð?š™™™™™é?š™™™™™É?ffffffö?ÍÌÌÌÌÌô?333333ã?333333Ó?ffffffæ?ÍÌÌÌÌÌì?333333ã?333333Ó?ÍÌÌÌÌÌ@š™™™™™Ù?š™™™™™É¿š™™™™™Ù?š™™™™™é?333333ó?333333ó?ÍÌÌÌÌÌü?š™™™™™¹?š™™™™™É?š™™™™™Ù?š™™™™™é¿ÍÌÌÌÌÌì?à¿ÍÌÌÌÌÌô?š™™™™™é¿š™™™™™Ù?š™™™™™é¿333333ã?š™™™™™É?š™™™™™ù?à¿333333Ó¿333333ã¿à?333333Ó¿š™™™™™é¿ffffffæ¿333333ã?š™™™™™é?š™™™™™Ù¿ffffffþ?š™™™™™é?š™™™™™ñ?333333Ó?š™™™™™é?ÍÌÌÌÌÌô?š™™™™™Ù?ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?à?@ÍÌÌÌÌÌì?š™™™™™ñ?š™™™™™Ù¿ffffff濚™™™™™Ù¿š™™™™™É¿š™™™™™É?š™™™™™ñ¿ÍÌÌÌÌÌ@ffffffæ?333333ó¿ÍÌÌÌÌÌô?š™™™™™ñ?ffffffæ¿333333ã?à¿ffffff@à?š™™™™™¹¿333333ã?ÍÌÌÌÌÌì¿333333ã¿333333ã?š™™™™™ñ?ffffffæ¿À333333Ó?š™™™™™É?333333ã?ffffffæ?š™™™™™É?š™™™™™ @š™™™™™ñ?333333Ó?ffffffæ?ð¿ÍÌÌÌÌÌ @š™™™™™@š™™™™™@ÍÌÌÌÌÌü¿333333ó¿333333Ó?ð?š™™™™™ù¿š™™™™™ñ?333333Ó¿ð?ffffff濚™™™™™Ù?ffffffö?ÍÌÌÌÌÌì?š™™™™™É?333333ã¿ÍÌÌÌÌÌ쿚™™™™™ñ¿ffffffæ?š™™™™™Ù?š™™™™™ñ?ffffffþ?333333ã?ffffffþ?ffffffö?š™™™™™é?ð?ffffffæ¿333333ó¿333333ã?ÍÌÌÌÌÌì?ð¿333333Ó¿ffffffæ?š™™™™™Ù¿333333ã?š™™™™™ñ?à?š™™™™™¹?š™™™™™é?𿚙™™™™é?@ÍÌÌÌÌÌü?à?š™™™™™Ù?ð?333333Ó?ÍÌÌÌÌÌô?à¿333333ã?ffffffæ¿à?š™™™™™¹?333333Ó¿333333Ó?ø¿333333㿚™™™™™É¿š™™™™™é?š™™™™™ù?ÍÌÌÌÌÌ@š™™™™™Ù?š™™™™™ñ¿333333ӿ࿚™™™™™É?à?š™™™™™É?ffffffæ?š™™™™™Ù¿ffffff濚™™™™™¹¿š™™™™™É?ð?š™™™™™Ù¿ffffffæ?ffffffæ¿ÍÌÌÌÌÌì¿333333ã?š™™™™™@ffffffæ?š™™™™™ À333333ã¿ÍÌÌÌÌÌô¿ð?à?à¿333333ã?ÀÍÌÌÌÌÌ쿚™™™™™Ù¿š™™™™™@ffffff濚™™™™™Ù?333333ó¿à¿333333ó¿š™™™™™ù?š™™™™™É?š™™™™™ñ¿ffffffþ?333333㿚™™™™™É¿ffffffæ?333333Ó¿333333ã?š™™™™™ù?ÍÌÌÌÌÌü?ÍÌÌÌÌÌì?333333Ó¿ð?ffffffæ?333333û?333333Ó¿ÍÌÌÌÌÌô?ÍÌÌÌÌÌ@ÍÌÌÌÌÌü¿ð?š™™™™™ñ¿333333Ó?à?ÍÌÌÌÌÌô¿@333333ã¿333333ã¿ÍÌÌÌÌÌì?š™™™™™ù?š™™™™™É?ffffffö?𿚙™™™™É?333333Ó?ffffffæ?š™™™™™Ù?š™™™™™Ù¿333333ã?ð¿à?ÍÌÌÌÌÌô?š™™™™™Ù¿š™™™™™ù?š™™™™™É? @š™™™™™ù?ffffff@333333ó?333333û?333333㿚™™™™™¹?ÍÌÌÌÌÌ@š™™™™™Ù?ø?š™™™™™¹?š™™™™™é¿ð?à?à?333333ó¿ø?š™™™™™É?333333ã¿ÍÌÌÌÌÌ@š™™™™™ù?š™™™™™É?š™™™™™ù?333333Ó?ð?š™™™™™é¿333333ã?š™™™™™ù¿ð¿š™™™™™ù¿ffffffö?š™™™™™ñ¿ffffffþ?š™™™™™é¿333333@333333ó?ÍÌÌÌÌÌô?@333333ã¿à?ffffffæ?š™™™™™ñ¿ffffffæ?ÍÌÌÌÌÌì?333333@࿚™™™™™Ù?š™™™™™é?ÍÌÌÌÌÌì?333333Ó¿ffffffæ?@ø?à¿ð?333333㿚™™™™™É?𿚙™™™™É?š™™™™™Ù?š™™™™™é?333333ã¿ø¿š™™™™™Ù?š™™™™™¹?ÍÌÌÌÌÌü?ffffff濚™™™™™Ù¿333333ó?š™™™™™ñ?ffffff@333333Ó¿š™™™™™É?à¿ffffffæ?š™™™™™é?ffffffþ¿š™™™™™Ù?ÍÌÌÌÌÌô?š™™™™™é?333333ã¿333333Ó?š™™™™™ñ?ffffffæ?š™™™™™É?333333ó¿š™™™™™é?࿚™™™™™ñ¿333333Ó?š™™™™™É¿ÍÌÌÌÌÌô?à?š™™™™™Ù¿š™™™™™ù?ÍÌÌÌÌÌü¿š™™™™™É?š™™™™™ù?ffffff%Àø¿33333³8@ø¿ÍÌÌÌÌÌÀÍÌÌÌ̬P@ffffff(@ffffff&@333333@ÍÌÌÌÌÌ=@š™™™™™ @333333û¿,Àš™™™™™5@ffffff)@š™™™™™ù?ffffff1@š™™™™™@ÍÌÌÌÌÌ&@3333337ÀÍÌÌÌÌÌ@ÍÌÌÌÌÌ0@ffffff+@ffffff!À#@33333sCÀ33333³<@ÀÍÌÌÌÌÌ3À@FÀ3333338ÀÀ @&Àš™™™™™@š™™™™™)@ffffff@333333@2À33333sGÀÍÌÌÌÌÌ@333333 @ÍÌÌÌÌÌ@š™™™™™2@+À3333330ÀÀ333333Ó?ÍÌÌÌÌÌ!@š™™™™™4ÀÍÌÌÌÌÌÀø?Àš™™™™™;@$@ÍÌÌÌÌÌô¿š™™™™™@,ÀÍÌÌÌÌÌ@š™™™™™8@3333330@š™™™™™@)@ffffff-@ÀB@ÍÌÌÌÌÌ@333333Ó¿2@ÍÌÌÌÌL0@'Àš™™™™™É¿ÍÌÌÌÌÌô?333333&@fffffæ0Àffffff)@333333/@3333333@ÍÌÌÌÌÌ@33333³<@333333(@ffffff,@ffffff-@@ÍÌÌÌÌÌÀ<@ÍÌÌÌÌÌ.@ffffff?@ÍÌÌÌÌÌ'@&@ffffff/Àffffffö¿333333#Àš™™™™™À33333³3Àš™™™™™¹¿333333ó¿ffffffþ?Àffffff Àš™™™™™é¿š™™™™™#ÀÍÌÌÌÌ B@ffffff @ffffff@333333 @ÍÌÌÌÌÌÀš™™™™™Àffffffö?@A@ffffffö¿333333*À@@ÍÌÌÌÌŒ@@333333)@À333333#@ffffff0À333333$Àffffff!@333333 À333333Ó¿€8ÀÍÌÌÌÌÌ@š™™™™™"@"Àš™™™™™@š™™™™™é¿333333@À€4@ÍÌÌÌÌÌ @š™™™™™'@33333³8@@ÍÌÌÌÌŒ@@š™™™™™0@@š™™™™™'@š™™™™™É¿š™™™™™9@ffffff!@š™™™™™+À2@ÍÌÌÌÌÌ À333333 Àš™™™™<@ÍÌÌÌÌÌô¿@š™™™™™!@ÍÌÌÌÌÌ,@ffffffÀš™™™™™)Àš™™™™™@3333330@À#@5@333333"@333333%Àš™™™™™)@ffffff.ÀÍÌÌÌÌÌ @333333Àffffff4@ffffff"@333333&Àffffff'ÀÍÌÌÌÌÌ@333333Àffffff,Àš™™™™™:Àffffff濚™™™™™É?.@"ÀÍÌÌÌÌÌ6@ÍÌÌÌÌÌ&@33333³6À-À33333³8@333333À333333Àš™™™™™ @ÍÌÌÌÌÌ3ÀÍÌÌÌÌÌ!ÀÍÌÌÌÌÌ,@ffffff@ffffffþ¿ÍÌÌÌÌ CÀ0Àffffff2@3333337Àffffff!@333333û?š™™™™™@€D@333333+Àfffff¦F@ÍÌÌÌÌÌ@Àš™™™™™#Àffffff:À@š™™™™C@š™™™™™>@ÍÌÌÌÌÌ%À*ÀÍÌÌÌÌÌ;@ÍÌÌÌÌL2@š™™™™™#@š™™™™™@ÍÌÌÌÌÌÀš™™™™J@š™™™™™@333333À!@š™™™™™É¿ffffffö?š™™™™™5@à¿ffffff,ÀÍÌÌÌÌÌ+@ffffffö?333333#@ffffff!@ÍÌÌÌÌÌ@Àš™™™™™#Àš™™™™™:@š™™™™™É?ÍÌÌÌÌÌÀ333333&@š™™™™=@33333³F@š™™™™™"À3333335@š™™™™™ @ffffff À@$@š™™™™™ Àš™™™™™Àffffff#@333333 @333333$À5@ffffffæ¿ffffff+À333333Àš™™™™8Àffffff)ÀÍÌÌÌÌL8Àš™™™™™?Àffffff4À@š™™™™™"ÀÍÌÌÌÌÌ@ffffff"@š™™™™™¹?š™™™™9Àffffff ÀÍÌÌÌÌÌ@ÍÌÌÌÌÌ&@333333!@š™™™™™@ffffff4@!ÀÍÌÌÌÌÌ$Àffffffæ¿ÍÌÌÌÌÌ1@333333)@ÍÌÌÌÌÌ@@ffffffÀ333333ÀÍÌÌÌÌÌ/ÀÍÌÌÌÌÌ.@333333@&@š™™™™™)À33333³=ÀffffffÀ*ÀÍÌÌÌÌÌ/ÀÍÌÌÌÌÌ"Àfffffæ5@š™™™™™'@ÍÌÌÌÌÌ @ffffff"À33333³4À%@ffffff @%Àffffff)@ffffffB@333333:À33333³3@ffffff!Àš™™™™™#Àffffff@ffffff'À.À3333338À@ÍÌÌÌÌÌ;@ÍÌÌÌÌÌ@(À@ÍÌÌÌÌL0@ÍÌÌÌÌÌì?š™™™™™Àà?š™™™™™ @ÍÌÌÌÌÌÀ'@ÍÌÌÌÌL@@@ffffff @ÀÍÌÌÌÌÌô?š™™™™YB@333333'Àš™™™™™3À3333332Àffffff@333333Àš™™™™GÀÀffffff#@$Àš™™™™™.@š™™™™ÙI@ÍÌÌÌÌÌ@333333À333333Àffffff/@ÍÌÌÌÌÌ+@ÍÌÌÌÌÌ)@333333ó?ÍÌÌÌÌÌ3@ÍÌÌÌÌÌ @à?ø?š™™™™™$@š™™™™™+@333333#@333333,ÀÍÌÌÌÌŒ@@ffffffÀš™™™™™,Àš™™™™™ù?ÍÌÌÌÌÌ!@š™™™™™3À(ÀÍÌÌÌÌÌ5@ÍÌÌÌÌŒGÀ#@ffffffÀ/@2@333333@š™™™™™"Àš™™™™™#@š™™™™™!ÀÀš™™™™™$À€;@ffffff@333333@.Àš™™™™™ñ?ÍÌÌÌÌÌ@*Àffffffæ¿ffffff1ÀÍÌÌÌÌÌ6Àš™™™™™ ÀÍÌÌÌÌÌ#@š™™™™™/@ÍÌÌÌÌÌ@ffffff8@ÍÌÌÌÌÌ#@š™™™™™(À33333³;@ÍÌÌÌÌÌ@333333#@š™™™™™(@€RÀÀEÀffffff@333333=@ÍÌÌÌÌÌ/@fffff¦@@ffffff%@š™™™™™Ù?@.@fffff¦DÀš™™™™™É?ffffff@fffffæ9@333333À*@ÍÌÌÌÌÌü?ÍÌÌÌÌÌ4@ÍÌÌÌÌÌ!@ÍÌÌÌÌÌÀÍÌÌÌÌÌ+@ÍÌÌÌÌÌ/À333333@š™™™™™.À€4@33333³0@š™™™™™:@š™™™™™/@€<@ÀÍÌÌÌÌÌ@š™™™™™ù¿ffffff&@š™™™™™ À/@š™™™™™Àfffffæ;Àffffff@ÍÌÌÌÌÌ @š™™™™™ù?ffffffö¿š™™™™1@š™™™™™A@ffffff@ffffff!À3333339À7Àffffff!@333333ó¿ÍÌÌÌÌÌ.@ffffff&Àffffff>@ÍÌÌÌÌÌ-@ÍÌÌÌÌL2@ÍÌÌÌÌÌ0À333333@ø¿fffffæ0@@ÍÌÌÌÌÌ.Àš™™™™™=@ffffffGÀÍÌÌÌÌÌ'@ffffffÀ333333/ÀÍÌÌÌÌÌô?333333:@ffffff"ÀÍÌÌÌÌÌ*À33333óW@.@š™™™™™@fffffæ1@@@š™™™™™0@333333㿚™™™™™*À€9@š™™™™™@@ÍÌÌÌÌL@@333333@@33333³7Àffffff(@ÍÌÌÌÌÌ7@š™™™™2@333333&Àffffff@-Àš™™™™Ù@@ÍÌÌÌÌÌü?ÍÌÌÌÌÌ+ÀÍÌÌÌÌÌ;ÀÍÌÌÌÌÌ<ÀffffffÀ@333333Àffffff@š™™™™™#@333333ã¿ffffff$@@À33333sJÀffffffþ?š™™™™™ñ?333333"@š™™™™™8@ÍÌÌÌÌÌ2À333333)Àš™™™™™!À333333Àffffff/@ÍÌÌÌÌL3ÀÍÌÌÌÌÌ쿚™™™™™@&À€?@-@333333Ó¿333333Àffffff%Àš™™™™™É?€;@333333-@333333ã¿33333³8@ffffff:@ÍÌÌÌÌLE@333333Àffffff@ÍÌÌÌÌL6@ffffff2@À333333ã?333333Àš™™™™™*@ÍÌÌÌÌL3ÀÍÌÌÌÌL5@333333B@š™™™™™6@š™™™™™ù¿ffffffB@333333"@"@ÍÌÌÌÌL6@333333@࿚™™™™C@š™™™™™'@ÍÌÌÌÌŒB@ÍÌÌÌÌÌ"@š™™™™™*Àš™™™™™@ffffff#Àš™™™™™.Àffffff"Àš™™™™™9Àffffff@ÍÌÌÌÌÌ$@ÀÍÌÌÌÌÌü?š™™™™™&Àø?ÍÌÌÌÌL4ÀffffffC@#@fffffæ3@@š™™™™™Àš™™™™™À333333û¿ÍÌÌÌÌÌ8@š™™™™™ @š™™™™1À333333@33333³1@ÍÌÌÌÌLH@%@ffffffÀÍÌÌÌÌÌ@ffffff3Àš™™™™™,À333333.@333333û¿š™™™™™Ù?333333*À@@333333'@ÀÍÌÌÌÌÌ @ @€>Àš™™™™YC@š™™™™™#@š™™™™™%@€;@333333&@fffff&D@ffffff9@ffffff.@)@ð?33333³<@ø¿Àffffff.@@333333$ÀÍÌÌÌÌL>@ÀÍÌÌÌÌÌ@333333$@ÍÌÌÌÌÌ;@ÍÌÌÌÌÌÀffffff.À333333ó?33333³3@ÍÌÌÌÌÌÀÍÌÌÌÌÌ@.@333333"@333333ÀÍÌÌÌÌL3@€1Àš™™™™™:@š™™™™™ @33333³<@ffffff%@À333333 ÀÍÌÌÌÌÌ@ø?333333Àš™™™™™7Àffffff+@š™™™™™@33333³4@ffffff&ÀÍÌÌÌÌÌ3@ffffff"@š™™™™™7Àš™™™™™)Àš™™™™™0@0@š™™™™™¹?š™™™™™ÀÍÌÌÌÌÌ Àš™™™™™@ffffff#@ÍÌÌÌÌÌ@š™™™™™À333333 À(Àfffffæ1@š™™™™™3Àš™™™™™Àð¿333333#@ÀH@3333331À+À333333@333333ÀÀ3333330ÀÍÌÌÌÌÌ@333333N@ÍÌÌÌÌ B@!ÀÍÌÌÌÌÌÀffffff4@š™™™™YI@33333S@8@ÍÌÌÌÌÌÀ33333ó@@ÍÌÌÌÌÌÀffffffÀ-@(Àffffff @ÍÌÌÌÌL3@š™™™™™@š™™™™3À33333³0@ffffff'@š™™™™™-@ffffff'@333333Ó¿333333+ÀÍÌÌÌÌÌ2À€?@333333 @333333@;@333333@@€M@ @š™™™™:@/@ÍÌÌÌÌÌÀÀš™™™™™-@333333û?š™™™™™)Àš™™™™™ @ÍÌÌÌÌÌ-@333333,ÀÍÌÌÌÌL8@333333@ÍÌÌÌÌÌ#ÀÍÌÌÌÌÌ À3À€8À333333,À,ÀÍÌÌÌÌÌÀ#@š™™™™™Àš™™™™™'@š™™™™™%@ffffff%@333333<Àffffffþ?Àš™™™™™.@š™™™™™"@333333 @33333³6@3ÀÍÌÌÌÌÌ-ÀffffffÀffffff7@33333³8@6@333333&@ffffff$À Àfffffæ3Àffffff0@ÍÌÌÌÌÌ @ÍÌÌÌÌÌ(@ÍÌÌÌÌÌÀÍÌÌÌÌ @Àš™™™™™!Àffffff+À.Àš™™™™™Ù¿fffffæ3@š™™™™2@ø¿ffffff0À.ÀfffffæA@ÍÌÌÌÌÌ @ÍÌÌÌÌ BÀ@š™™™™?@š™™™™™)Àš™™™™9@š™™™™™'ÀÍÌÌÌÌÌÀ333333!À3333331À3333333ÀffffffÀÍÌÌÌÌÌü?ÍÌÌÌÌL?@ffffffþ¿ffffff.À333333À33333³=@@&Àš™™™™™.@š™™™™™é¿À/@€>@š™™™™™@š™™™™1@.@ÍÌÌÌÌÌ@33333ó@@š™™™™™À333333&ÀffffffÀffffffæ?@€:Àfffffæ:Àš™™™™8@š™™™™2À€1@33333³K@ÍÌÌÌÌÌ Àfffff¦A@333333+Àš™™™™™+@š™™™™™1@33333³7@š™™™™™@€:@333333 Àffffffö?š™™™™™ @0@š™™™™™0@š™™™™™@&À€<@ÍÌÌÌÌÌÀÀš™™™™™é¿š™™™™4@ÍÌÌÌÌÌ0À33333³>@33333ó@@fffffæ:Àfffffæ0@ÍÌÌÌÌÌ@š™™™™™"@3333333@š™™™™™<@333333Àš™™™™™4@!ÀÍÌÌÌÌÌ'ÀÍÌÌÌÌÌÀ@A@+@ÍÌÌÌÌÌô¿ffffff@333333@333333û¿š™™™™™ @,@š™™™™0ÀÍÌÌÌÌÌÀffffffþ¿fffffæ0@ÍÌÌÌÌÌ$@ffffff*@ÍÌÌÌÌÌ @ @ÍÌÌÌÌÌ7ÀÀA@ø¿ÍÌÌÌÌL5@ÍÌÌÌÌÌ@333333'À333333AÀÍÌÌÌÌÌ)@ffffffG@333333(@€B@ÍÌÌÌÌÌ-@ÍÌÌÌÌL0Àffffff@š™™™™™3@33333³4Àš™™™™™ ÀÍÌÌÌÌÌ$@ÍÌÌÌÌL@@ffffffþ?ÍÌÌÌÌÌ%@ffffff@fffff¦E@ffffff3@ffffff%Àš™™™™6@fffffæ2À@€8À3333337@ÍÌÌÌÌÌ3@33333³>@333333'@š™™™™™"@ÍÌÌÌÌÌô¿š™™™™™ @ÍÌÌÌÌÌ(@ @ÍÌÌÌÌÌÀ3333336@š™™™™™@€J@š™™™™™ @@333333À@33333³5@ffffff*@ffffff@333333û?ÍÌÌÌÌÌ3Àš™™™™™;ÀÍÌÌÌÌÌ%@@ffffff2@!ÀÍÌÌÌÌÌ6@€3@ffffff.@33333³8Àš™™™™™@ÍÌÌÌÌÌÀ€:@333333@ÍÌÌÌÌÌ1À333333D@ÍÌÌÌÌLMÀffffff+@ffffff@š™™™™™Ù?š™™™™™¹?𿚙™™™™Ù¿š™™™™™¹¿š™™™™™É¿š™™™™™¹?š™™™™™¹?ffffff濚™™™™™¹?š™™™™™¹?š™™™™™¹¿š™™™™™É?š™™™™™¹¿š™™™™™¹¿š™™™™™É?š™™™™™É¿š™™™™™¹?333333㿚™™™™™¹¿š™™™™™¹¿š™™™™™¹¿š™™™™™¹?à?š™™™™™É¿š™™™™™¹¿ffffffæ?ÍÌÌÌÌÌì?š™™™™™¹¿š™™™™™Ù?š™™™™™É?š™™™™™¹¿š™™™™™É¿333333Ó?š™™™™™É?š™™™™™É¿š™™™™™Ù?š™™™™™¹?š™™™™™¹?š™™™™™Ù?š™™™™™¹?š™™™™™¹¿š™™™™™Ù?š™™™™™¹¿š™™™™™É¿š™™™™™¹¿š™™™™™¹?š™™™™™¹¿š™™™™™¹¿333333Ó?š™™™™™¹¿š™™™™™Ù?š™™™™™É?333333Ó¿š™™™™™¹¿š™™™™™¹¿š™™™™™Ù?š™™™™™¹?à?333333Ó?š™™™™™Ù?à?š™™™™™Ù¿š™™™™™É?à?š™™™™™É¿333333ã?š™™™™™¹¿š™™™™™É¿š™™™™™Ù¿š™™™™™¹?à?š™™™™™É?š™™™™™¹?š™™™™™É?š™™™™™¹?š™™™™™¹?š™™™™™É?ffffffþ?š™™™™™ñ?š™™™™™¹¿š™™™™™é?ffffffþ?š™™™™™¹¿š™™™™™Ù¿à?š™™™™™É?š™™™™™¹?š™™™™™é?š™™™™™É?š™™™™™¹?š™™™™™Ù?š™™™™™¹?š™™™™™¹?š™™™™™¹¿š™™™™™ñ?š™™™™™¹?š™™™™™Ù?š™™™™™É¿333333㿚™™™™™É¿333333Ó?š™™™™™¹¿š™™™™™¹?š™™™™™¹¿ffffffæ?š™™™™™Ù?ð?š™™™™™É?š™™™™™¹¿š™™™™™É?š™™™™™¹?à?š™™™™™¹¿š™™™™™É?š™™™™™¹¿š™™™™™¹¿333333ó¿333333ã?š™™™™™¹¿ffffffæ?ð¿à¿š™™™™™¹?š™™™™™Ù¿š™™™™™¹¿à¿333333Ó?š™™™™™É?š™™™™™É?š™™™™™¹¿š™™™™™¹¿š™™™™™É¿š™™™™™É?333333Ó?š™™™™™¹?š™™™™™É?š™™™™™¹?š™™™™™Ù¿š™™™™™Ù?š™™™™™É¿š™™™™™¹¿š™™™™™¹?š™™™™™¹?ffffffæ?š™™™™™¹?š™™™™™É?ÍÌÌÌÌÌì?š™™™™™É?ffffffö?ð?š™™™™™¹?š™™™™™¹?333333Ó?š™™™™™¹¿š™™™™™¹¿š™™™™™É?š™™™™™¹?š™™™™™¹?š™™™™™¹¿š™™™™™¹¿333333ã?ffffffæ?š™™™™™ù?š™™™™™É¿š™™™™™¹?à?š™™™™™¹¿333333Ó?š™™™™™É?š™™™™™Ù?š™™™™™Ù¿š™™™™™Ù?š™™™™™É?ÍÌÌÌÌÌì?š™™™™™¹?š™™™™™¹¿ð?š™™™™™Ù?ð?࿚™™™™™Ù?š™™™™™¹¿š™™™™™¹?࿚™™™™™¹?333333ã?š™™™™™Ù¿à?š™™™™™Ù?š™™™™™É¿š™™™™™¹¿š™™™™™Ù?š™™™™™¹¿š™™™™™É?š™™™™™É?š™™™™™Ù?à?š™™™™™¹?š™™™™™é?š™™™™™¹¿š™™™™™ñ?š™™™™™É¿ffffffæ?š™™™™™Ù?ð?à?333333ã?333333ã?š™™™™™Ù?333333Ó¿š™™™™™É?š™™™™™¹?š™™™™™¹¿š™™™™™¹¿š™™™™™¹?š™™™™™¹?ffffffæ?à¿ÍÌÌÌÌÌì?š™™™™™¹?š™™™™™¹?š™™™™™¹?ffffffæ?š™™™™™Ù¿à?š™™™™™¹?š™™™™™Ù¿333333Ó?š™™™™™¹?333333ã?š™™™™™¹¿š™™™™™É?š™™™™™Ù¿à?š™™™™™¹?ffffffæ?š™™™™™¹?ffffffæ?š™™™™™É¿š™™™™™¹?à?š™™™™™Ù¿š™™™™™¹?š™™™™™¹¿š™™™™™É¿š™™™™™¹?š™™™™™É?š™™™™™É?š™™™™™Ù?ÍÌÌÌÌÌô?š™™™™™Ù¿ffffffæ?333333ã?à?ÍÌÌÌÌÌ쿚™™™™™¹¿à?࿚™™™™™¹?333333Ó?333333ã?š™™™™™¹¿333333ã¿ø?š™™™™™É¿š™™™™™É?š™™™™™¹?š™™™™™¹?š™™™™™Ù¿š™™™™™¹¿ffffff濚™™™™™¹?š™™™™™¹¿à¿š™™™™™É¿ð?ffffffö?š™™™™™Ù¿333333ã?š™™™™™É¿š™™™™™¹?š™™™™™ñ?ffffffæ?š™™™™™É?333333㿚™™™™™Ù?š™™™™™Ù¿š™™™™™¹¿š™™™™™¹?š™™™™™Ù?š™™™™™Ù¿š™™™™™É?š™™™™™¹?à?š™™™™™¹¿333333ã¿333333Ó?333333Ó?ffffff濚™™™™™É?333333ã?333333ã?š™™™™™é?333333Ó?š™™™™™¹¿š™™™™™Ù?333333Ó?ffffffæ?ffffffö?ffffffæ?š™™™™™¹?š™™™™™¹¿š™™™™™¹?š™™™™™¹¿š™™™™™Ù?ffffffæ?333333ó?ð?333333ã?š™™™™™Ù?à?à?š™™™™™É?š™™™™™¹¿333333Ó¿333333Ó?333333Ó?š™™™™™¹?ffffffæ?š™™™™™¹¿333333ã?333333Ó?à?š™™™™™@š™™™™™¹?š™™™™™Ù?š™™™™™Ù¿333333Ó?à?ffffffö?333333Ó?š™™™™™É¿š™™™™™É?š™™™™™¹¿š™™™™™É¿š™™™™™¹?ffffffþ¿ð¿š™™™™™É¿š™™™™™¹¿š™™™™™¹¿š™™™™™¹?ÍÌÌÌÌÌì?333333ã?š™™™™™Ù?š™™™™™Ù¿š™™™™™¹?š™™™™™¹?š™™™™™Ù?à?š™™™™™É?ffffffæ¿333333ã?š™™™™™¹¿š™™™™™¹¿333333Ó¿ÍÌÌÌÌÌ쿚™™™™™¹¿š™™™™™Ù?š™™™™™¹?š™™™™™¹?333333Ó¿333333Ó?333333㿚™™™™™¹?š™™™™™Ù¿š™™™™™¹?š™™™™™É¿333333Ó?š™™™™™¹?š™™™™™Ù?š™™™™™É?333333Ó?š™™™™™Ù¿š™™™™™É¿333333Ó?š™™™™™¹?š™™™™™é¿š™™™™™Ù¿š™™™™™É?333333Àš™™™™™À333333Àffffff À333333 À"ÀffffffÀ333333À333333ÀffffffÀÍÌÌÌÌÌÀ333333ÀÍÌÌÌÌÌÀ À'ÀÍÌÌÌÌÌÀÀffffffÀ333333Àš™™™™™À333333ÀÍÌÌÌÌÌÀ333333À333333À333333ÀffffffÀ333333!ÀÀ Àš™™™™™ù¿333333Àš™™™™™ÀÍÌÌÌÌÌ!Àš™™™™™Àš™™™™™À333333Àš™™™™™"ÀffffffÀš™™™™™À333333ÀffffffÀ333333ÀffffffÀ333333À!ÀffffffÀ333333Àš™™™™™!ÀffffffÀš™™™™™ Àš™™™™™ÀÍÌÌÌÌÌÀš™™™™™ÀffffffÀÍÌÌÌÌÌÀÀ333333ÀÍÌÌÌÌÌ ÀÍÌÌÌÌÌ"ÀÍÌÌÌÌÌÀffffffÀš™™™™™"ÀÀ333333À333333 Àš™™™™™ÀÍÌÌÌÌÌÀffffffÀš™™™™™ÀffffffÀš™™™™™Àš™™™™™Àš™™™™™ÀffffffÀ333333Àš™™™™™)Àffffff ÀffffffÀÍÌÌÌÌÌÀÀÀÀffffff ÀffffffÀ333333$ÀÀ333333$Àš™™™™™À333333ÀffffffÀÍÌÌÌÌÌÀffffffÀÀffffffÀ"Àš™™™™™ ÀÀÍÌÌÌÌÌÀš™™™™™À!ÀÍÌÌÌÌÌ!Àffffff ÀffffffÀ333333 Àffffff ÀffffffÀffffffÀffffff ÀffffffÀÍÌÌÌÌÌ ÀÀš™™™™™À333333ÀÀ!ÀffffffÀš™™™™™À ÀÀffffffÀÍÌÌÌÌÌ Àš™™™™™ÀffffffÀffffff ÀÀffffffÀÀÍÌÌÌÌÌÀ333333ÀÍÌÌÌÌÌÀffffffÀš™™™™™!ÀÍÌÌÌÌÌÀš™™™™™Àš™™™™™ÀffffffÀÍÌÌÌÌÌ À333333ÀÀffffffÀš™™™™™À333333Àš™™™™™ÀÀÀš™™™™™Àš™™™™™ÀÀÀffffffÀ Àš™™™™™Àš™™™™™ÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀ333333À333333Àš™™™™™Àffffffö¿ÀffffffÀš™™™™™"ÀÍÌÌÌÌÌÀÀÀ333333,Àš™™™™™ÀffffffÀš™™™™™&ÀÍÌÌÌÌÌÀÀffffffÀffffffÀÀffffff)ÀffffffÀffffff!ÀÀš™™™™™ÀÍÌÌÌÌÌÀffffffÀ333333À333333Àø¿333333ÀffffffÀš™™™™™Àš™™™™™ÀÀ333333ÀÀÍÌÌÌÌÌÀffffffÀ333333%ÀffffffÀÍÌÌÌÌÌÀ&À333333%À333333ÀffffffÀffffffÀffffffÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀ333333 Àš™™™™™ÀffffffÀffffffÀ333333$À333333À333333À333333ÀÍÌÌÌÌÌô¿ÍÌÌÌÌÌÀÀ333333ÀffffffÀÍÌÌÌÌÌÀffffff ÀffffffÀš™™™™™#Àš™™™™™ÀffffffÀ333333Àš™™™™™ÀffffffÀÀš™™™™™À333333ÀÀ333333Àš™™™™™À ÀÀÍÌÌÌÌÌ)Àš™™™™™ÀffffffÀÀ333333À333333À333333Àš™™™™™ÀÀÍÌÌÌÌÌ ÀÍÌÌÌÌÌ!À333333ÀffffffÀffffffÀ333333ÀffffffÀ%Àš™™™™™ ÀÍÌÌÌÌÌ!À333333#À333333ÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀš™™™™™!ÀÀš™™™™™À#À333333ÀÀš™™™™™#ÀÀÀffffffÀ333333ÀÀffffffÀ ÀÀ333333À333333'Àš™™™™™À333333Àš™™™™™ÀffffffÀš™™™™™ÀÀ333333Àš™™™™™ÀffffffÀÍÌÌÌÌÌÀÍÌÌÌÌÌ!ÀÀ333333ÀÀÀ333333Àš™™™™™ÀÍÌÌÌÌÌÀffffff Àš™™™™™ÀÍÌÌÌÌÌÀš™™™™™Àš™™™™™ÀffffffÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀ333333ÀÍÌÌÌÌÌÀffffffÀffffff%À333333ÀÍÌÌÌÌÌÀš™™™™™ÀÀÀffffffÀ333333!Àffffff#ÀffffffÀš™™™™™ù¿ÍÌÌÌÌÌÀffffff!ÀÍÌÌÌÌÌ#ÀÍÌÌÌÌÌÀš™™™™™Àffffff!Àffffff ÀÍÌÌÌÌÌ#ÀffffffÀffffff,À333333ÀÍÌÌÌÌÌÀÀffffffÀš™™™™™"ÀÍÌÌÌÌÌ ÀffffffÀÀffffffÀÍÌÌÌÌÌÀš™™™™™Àš™™™™™$ÀÍÌÌÌÌÌ Àffffff Àš™™™™™ÀÀ0Àš™™™™™À333333À333333Àffffff"ÀÍÌÌÌÌÌÀffffffÀ333333#ÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀ333333Àš™™™™™ ÀÍÌÌÌÌÌÀffffffÀffffffÀÍÌÌÌÌÌÀ333333À!Àš™™™™™ÀffffffÀ333333ó¿333333À333333À333333 Àš™™™™™ À333333À333333À333333Àš™™™™™À333333ÀÀÀš™™™™™À ÀffffffÀffffffÀÀ"ÀÍÌÌÌÌÌü?ÍÌÌÌÌÌü?š™™™™™é?š™™™™™ @333333ã?ÍÌÌÌÌÌô?à?š™™™™™ñ?ÍÌÌÌÌÌô?š™™™™™ù?333333ã?333333ó?š™™™™™ñ?š™™™™™Ù?ÍÌÌÌÌÌì?ð?ð?š™™™™™Ù?ffffffþ?@š™™™™™é?š™™™™™Ù?ffffffþ?š™™™™™ù?ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?ð?ffffffæ?š™™™™™Ù?ÍÌÌÌÌÌì?ð?ÍÌÌÌÌÌô?ffffffæ?333333ã?ø?ÍÌÌÌÌÌì?ffffffæ?333333@333333û?š™™™™™Ù?š™™™™™ù?ffffffþ?ffffffö?333333ã?ÍÌÌÌÌÌ@à?ÍÌÌÌÌÌì?ffffffþ?ÍÌÌÌÌÌì?š™™™™™é?333333ó?ffffffþ?ffffffæ?ø?š™™™™™ñ?ÍÌÌÌÌÌô?ffffff @š™™™™™é?ffffffæ?š™™™™™Ù?ø?à?š™™™™™ñ? @š™™™™™é?à?š™™™™™ñ?333333ó?ffffffö?ffffffæ?ffffffö?@333333ã?333333ó?ÍÌÌÌÌÌì?ÍÌÌÌÌÌ@333333û?ÍÌÌÌÌÌô?š™™™™™Ù?ffffffæ?ø?ð?à?333333ã?ÍÌÌÌÌÌ@à?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ð?š™™™™™Ù?333333@š™™™™™@ffffff@@333333û?š™™™™™ñ?333333û?ÍÌÌÌÌÌô?ø?333333ã?š™™™™™@ÍÌÌÌÌÌô?š™™™™™Ù?333333@ffffffö?à?ÍÌÌÌÌÌô?š™™™™™é?ÍÌÌÌÌÌü?ffffffæ?š™™™™™Ù?ÍÌÌÌÌÌì?333333ó?ffffffö?š™™™™™ñ?ffffffæ?@š™™™™™é?š™™™™™ù?š™™™™™é?ffffffö?à?ffffffö?ffffff@333333 @ÍÌÌÌÌÌô?333333ã?ffffffþ?ð?ÍÌÌÌÌÌü?š™™™™™Ù?š™™™™™é?ÍÌÌÌÌÌü?à?š™™™™™ñ?ÍÌÌÌÌÌô?ð?ø?ffffffæ?š™™™™™ñ?333333ã?333333û?š™™™™™é?š™™™™™ù?333333û?ÍÌÌÌÌÌ@ð?ffffffö?333333@@ÍÌÌÌÌÌü?333333ã?ÍÌÌÌÌÌì?ÍÌÌÌÌÌü?@ÍÌÌÌÌÌô?333333û?ÍÌÌÌÌÌü?@š™™™™™ù?à?333333ó?š™™™™™Ù?333333ó?ø?š™™™™™Ù?333333ó?333333ó?š™™™™™Ù?ð?ð?ÍÌÌÌÌÌü?ffffffæ?š™™™™™é?ffffffþ?333333ó?333333ã?ffffff@à?333333@ð?à?ÍÌÌÌÌÌô?333333ó?333333ã?@š™™™™™ù?à?@@333333ó?à? @š™™™™™Ù?ffffff@š™™™™™ @333333@333333@ffffffæ?š™™™™™ù?à?š™™™™™é?ffffffæ?ffffff@ffffffæ?333333û?ø?ø?333333ã?ÍÌÌÌÌÌü?ffffffö?ÍÌÌÌÌÌì?ø?ø?ÍÌÌÌÌÌü?ffffffæ?ÍÌÌÌÌÌì?à?333333ã?š™™™™™é?ffffffþ?ð?š™™™™™ñ?333333ó?à?333333ó?ð?333333ã?ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?ð?š™™™™™Ù?ÍÌÌÌÌÌô?š™™™™™Ù?ÍÌÌÌÌÌô?333333@ÍÌÌÌÌÌì?š™™™™™é?ÍÌÌÌÌÌ@ÍÌÌÌÌÌü?š™™™™™ù?š™™™™™Ù?š™™™™™ù?ÍÌÌÌÌÌ@š™™™™™é?ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?à?333333@ffffffæ?š™™™™™ñ?ffffffæ?š™™™™™'@à?333333ó?333333ã?š™™™™™ù?ÍÌÌÌÌÌì?ffffffæ?ð?š™™™™™@à?ÍÌÌÌÌÌô?š™™™™™ù?š™™™™™ù?š™™™™™ñ?ffffff@š™™™™™Ù?š™™™™™@333333ã?ÍÌÌÌÌÌì?ÍÌÌÌÌÌ@ffffff@ð?à?333333ó?š™™™™™ñ?š™™™™™é?@ffffff@š™™™™™ñ?ø?š™™™™™é?š™™™™™ñ?š™™™™™ñ?333333%@ð?ÍÌÌÌÌÌÀš™™™™™ Àš™™™™™À'Àš™™™™™Àffffff(ÀÀÀð¿"À333333ÀÀÍÌÌÌÌÌÀ333333"Àš™™™™™ÀÀÍÌÌÌÌÌ%À333333 À333333À333333À ÀÍÌÌÌÌÌÀÀffffff"Àš™™™™™ ÀÍÌÌÌÌÌÀ333333%ÀÍÌÌÌÌÌ#ÀÍÌÌÌÌÌÀ333333À333333 Àš™™™™™Àš™™™™™Àš™™™™™Àffffff"Àš™™™™™ÀffffffÀ333333'À333333À333333Àš™™™™™"Àš™™™™™ ÀÀš™™™™™'ÀffffffÀš™™™™™)ÀÀ À333333À333333Àš™™™™™ÀÀÍÌÌÌÌÌÀš™™™™™À!À333333À%À333333%ÀÍÌÌÌÌÌÀ#À333333ÀffffffÀffffffÀš™™™™™!À333333%ÀÍÌÌÌÌÌÀ333333$À#Àš™™™™™é¿ffffffÀ333333À Àš™™™™™Àš™™™™™!À333333ó?ffffffö?@ÍÌÌÌÌÌü?ÍÌÌÌÌÌü?ÍÌÌÌÌÌì?ø?š™™™™™@333333ó?à?@333333ã?333333@ @ð?ð?š™™™™™é?333333ã?333333û?š™™™™™é?š™™™™™Ù?ffffff@333333ã?ÍÌÌÌÌÌü?š™™™™™é?š™™™™™é?ffffffæ?333333ã?š™™™™™@333333ã?ð?@à?š™™™™™ñ?š™™™™™é?333333@@š™™™™™Ù¿š™™™™™é¿333333Ó¿à¿ffffffæ¿ffffffö¿š™™™™™ñ¿š™™™™™Ù¿333333㿚™™™™™ñ¿333333ã¿ð¿š™™™™™ñ¿š™™™™™ñ¿333333ã¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌ쿚™™™™™Ù¿ffffff濚™™™™™ñ¿ffffff濚™™™™™ñ¿ð¿à¿š™™™™™¹?š™™™™™É?ffffffö¿333333ã¿ÍÌÌÌÌÌì¿à¿š™™™™™é¿ffffffæ¿à¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌô¿ffffffæ¿ð¿ffffffö¿ÍÌÌÌÌÌô¿333333ã¿ffffffæ¿ffffffæ¿333333ó¿ÍÌÌÌÌÌì¿333333ã¿ÍÌÌÌÌÌô¿333333ó¿à¿à¿ffffffö¿ffffffæ¿ÍÌÌÌÌÌì¿ð¿à¿333333ã¿ð¿333333Ó¿ffffffæ¿ð¿ÍÌÌÌÌÌì¿ø¿š™™™™™ñ¿ð¿333333ã¿333333Ó¿š™™™™™é¿ÍÌÌÌÌÌì¿333333ã¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌì¿à¿333333ó¿ÍÌÌÌÌÌô¿š™™™™™é¿š™™™™™é¿ÍÌÌÌÌÌ쿚™™™™™é¿ÍÌÌÌÌÌô¿ffffffæ¿ÍÌÌÌÌÌ쿚™™™™™ù¿š™™™™™Ù¿ÍÌÌÌÌÌ쿚™™™™™Ù¿333333ã¿333333ã¿ð¿333333ó¿š™™™™™é¿š™™™™™é¿š™™™™™ñ¿š™™™™™é¿š™™™™™ñ¿š™™™™™é¿ÍÌÌÌÌÌì¿ffffffæ¿à¿ð¿333333ó¿ÍÌÌÌÌÌ쿚™™™™™ñ¿333333ã¿à¿š™™™™™ñ¿333333Ó¿ÍÌÌÌÌÌì¿333333Ó?ffffffæ¿333333ã¿ð¿ð¿à¿333333㿚™™™™™ñ¿š™™™™™É?333333ã¿à¿ffffff濚™™™™™é¿ffffffæ¿ð¿333333Ó¿333333ã¿ffffffæ¿333333ó¿ÍÌÌÌÌÌì¿à¿ÍÌÌÌÌÌ쿚™™™™™ñ¿ffffff濚™™™™™Ù?333333ó¿ffffff濚™™™™™ñ¿333333ó¿333333㿚™™™™™ñ¿ffffff濚™™™™™ù¿à¿š™™™™™ñ¿š™™™™™é¿à¿ÍÌÌÌÌÌì¿333333ó¿à¿ð¿š™™™™™ñ¿à¿š™™™™™é¿333333ã¿à¿333333ã¿ð¿ÍÌÌÌÌÌ쿚™™™™™é¿š™™™™™é¿ÍÌÌÌÌÌ쿚™™™™™ñ¿ð¿š™™™™™é¿š™™™™™é¿333333㿚™™™™™Ù¿ffffffö¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌì¿333333ã¿ÍÌÌÌÌÌì¿ffffffæ¿à¿š™™™™™É?š™™™™™É¿ffffffæ¿333333Ó?ð¿à¿š™™™™™ñ¿š™™™™™é¿ÍÌÌÌÌÌì¿ffffffæ¿ffffffö¿ÍÌÌÌÌÌì¿ð¿š™™™™™é¿ÍÌÌÌÌÌì¿333333Ó¿š™™™™™¹?333333ã¿ð¿333333ã¿ÍÌÌÌÌÌì¿à¿ffffffæ¿333333ã¿ffffffÀš™™™™™¹?333333Ó¿š™™™™™é¿ÍÌÌÌÌÌì¿333333㿚™™™™™Ù¿ffffffæ¿ffffff濚™™™™™Ù¿š™™™™™é¿š™™™™™é¿ÍÌÌÌÌÌô¿š™™™™™ñ¿š™™™™™é¿š™™™™™ñ¿ð¿ffffff濚™™™™™é¿333333ó¿333333ã¿ÍÌÌÌÌÌì¿333333㿚™™™™™é¿š™™™™™Ù¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌì¿ð¿333333㿚™™™™™¹?ffffffö¿ð¿š™™™™™é¿333333㿚™™™™™ñ¿ffffffæ¿ÍÌÌÌÌÌ쿚™™™™™é¿333333ó¿ÍÌÌÌÌÌô¿š™™™™™ñ¿ð¿ð¿š™™™™™É?333333ӿ𿚙™™™™ñ¿š™™™™™é¿š™™™™™ñ¿ÍÌÌÌÌÌì¿ffffffæ¿ffffffæ¿333333㿚™™™™™Ù?ð¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌ쿚™™™™™é¿š™™™™™ñ¿ffffffö¿ÀÍÌÌÌÌÌì¿333333ã¿333333ã¿333333㿚™™™™™ñ¿ÍÌÌÌÌÌ쿚™™™™™ñ¿š™™™™™é¿333333Ó¿333333ã¿333333Ó?ffffffæ¿333333Ó¿333333ã¿ð¿ffffffö¿à¿š™™™™™é¿333333Ó¿ffffff濚™™™™™é¿à¿ÍÌÌÌÌÌ쿚™™™™™Ù?à¿ffffffæ¿ffffffæ¿à¿à¿ð¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌ쿚™™™™™é¿ffffffæ¿333333㿚™™™™™Ù¿333333ó¿à¿ffffff濚™™™™™Ù¿ð¿ÍÌÌÌÌÌì¿333333ó¿ÍÌÌÌÌÌì¿333333ã¿ffffff濚™™™™™É?ÍÌÌÌÌÌ쿚™™™™™¹?333333ó¿š™™™™™é¿ð¿š™™™™™ñ¿333333ó¿š™™™™™É¿333333ó¿š™™™™™é¿š™™™™™ñ¿ffffffö¿š™™™™™ñ¿ffffffæ¿ð¿ð¿333333ã¿333333㿚™™™™™é¿ø¿à¿š™™™™™ñ¿ffffffæ¿333333ó¿333333ã¿333333ã¿ð¿ð¿333333ó¿à¿š™™™™™é¿à¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌ쿚™™™™™ñ¿ffffffæ¿ffffffæ¿ffffffæ¿Àffffffæ¿à?333333Ó¿š™™™™™Ù¿š™™™™™ù¿333333ó¿š™™™™™é¿š™™™™™Ù¿š™™™™™ñ¿ø¿à¿ffffffæ¿333333㿚™™™™™é¿à¿333333ã¿ffffff濚™™™™™ñ¿š™™™™™Ù¿ð¿à¿ffffffæ¿333333㿚™™™™™é¿333333㿚™™™™™Ù¿š™™™™™ñ¿ffffffö¿ffffffæ¿ð¿ÍÌÌÌÌÌô¿š™™™™™é¿š™™™™™é¿à¿ÍÌÌÌÌÌì¿à?š™™™™™ñ¿ÍÌÌÌÌÌì¿à¿ð¿š™™™™™é¿š™™™™™é¿333333㿚™™™™™é¿333333㿚™™™™™é¿333333ó¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌì¿333333㿚™™™™™Ù¿ÍÌÌÌÌÌô?ÍÌÌÌÌÌ쿚™™™™™é¿ð¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌ쿚™™™™™¹¿ÍÌÌÌÌÌì¿ffffff濚™™™™™É¿333333ó¿ffffff濚™™™™™é¿ÍÌÌÌÌÌô¿à¿š™™™™™ñ¿ffffff濚™™™™™ñ¿à¿333333ó¿ÍÌÌÌÌÌô¿š™™™™™ñ¿ffffffæ¿333333ã¿ð¿š™™™™™é¿ffffff濚™™™™™é¿333333ã¿ÍÌÌÌÌÌ쿚™™™™™ñ¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌô¿333333㿚™™™™™é¿ffffff濚™™™™™é¿ffffffæ¿333333ã¿ÍÌÌÌÌÌì¿333333㿚™™™™™é¿ÍÌÌÌÌÌì¿à¿š™™™™™¹¿333333㿚™™™™™é¿333333û¿š™™™™™ù¿À333333û¿ø¿333333ÀÍÌÌÌÌÌ쿚™™™™™é¿ffffffþ¿333333ó¿ð¿ÍÌÌÌÌÌü¿333333ã¿ffffffö¿ÀÍÌÌÌÌÌü¿š™™™™™ù¿333333㿚™™™™™Ù¿333333û¿333333Àš™™™™™ñ¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌ쿚™™™™™ù¿ð¿333333û¿š™™™™™Àš™™™™™¹¿333333ó?š™™™™™ñ¿333333ó¿ffffffÀ333333ó¿ÍÌÌÌÌÌì¿333333ó¿ÍÌÌÌÌÌô¿333333 ÀffffffÀš™™™™™ñ¿š™™™™™ñ¿ÍÌÌÌÌÌì¿ffffffö¿ffffffö¿ð¿š™™™™™ñ¿333333ó¿š™™™™™Ù¿ÍÌÌÌÌÌì¿à¿š™™™™™ñ¿ÍÌÌÌÌÌô¿ð¿ÍÌÌÌÌÌü¿à¿ffffffÀø¿333333㿚™™™™™É¿ÍÌÌÌÌÌô¿ffffffæ¿ÍÌÌÌÌÌÀÍÌÌÌÌÌÀÀÍÌÌÌÌÌÀffffff濚™™™™™¹¿À333333ó¿ffffffö¿333333ÀffffffÀ333333ó¿ø¿š™™™™™À333333À333333ó?ffffffö¿š™™™™™é¿ffffffæ¿ÍÌÌÌÌÌÀš™™™™™Ù¿š™™™™™Ù¿š™™™™™ù¿333333ó?ffffffö?333333ã¿@ø?𿚙™™™™é?ffffffþ¿ð¿ÀÍÌÌÌÌÌü¿ffffffö¿Àš™™™™™ñ¿š™™™™™é?ð¿ffffff濚™™™™™é¿ÍÌÌÌÌÌÀš™™™™™ñ¿š™™™™™Ù¿š™™™™™Ù¿š™™™™™Àð?ffffffæ?ø¿ffffffö¿ø¿333333À333333û¿š™™™™™Àffffffþ¿š™™™™™Àš™™™™™ù¿š™™™™™é¿ÍÌÌÌÌÌô¿š™™™™™é¿ø¿š™™™™™ù¿š™™™™™é¿š™™™™™ÀÍÌÌÌÌÌì¿ÍÌÌÌÌÌô¿ð¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌÀš™™™™™é¿333333ó¿ Àš™™™™™Àš™™™™™ù¿ffffffÀš™™™™™ñ¿ÍÌÌÌÌÌì¿ø¿ffffffþ¿ð¿ÍÌÌÌÌÌü¿š™™™™™ÀÀš™™™™™Àš™™™™™Àffffffþ¿š™™™™™ñ¿ø¿ø¿ffffffþ¿ÍÌÌÌÌÌÀø¿ÍÌÌÌÌÌÀffffffÀÍÌÌÌÌÌô¿à¿333333û¿333333ó¿ffffffÀ333333ã¿333333ÀÍÌÌÌÌÌÀffffffþ¿à¿ffffffö¿333333 ÀÍÌÌÌÌÌì¿333333㿚™™™™™Ù¿š™™™™™ñ¿ÍÌÌÌÌÌÀð¿ð¿š™™™™™ñ¿ffffffö¿ÀÍÌÌÌÌÌü¿š™™™™™Ù?ÍÌÌÌÌÌì¿333333À333333Ó¿333333Ó?ÍÌÌÌÌÌì?333333Ó¿à¿333333ã¿333333ã?333333û?333333û¿ffffff濚™™™™™ñ¿333333ó?ffffffö¿333333ó¿š™™™™™ù¿ffffffö¿š™™™™™Ù?ffffffÀš™™™™™É?š™™™™™é¿ø?ÍÌÌÌÌÌÀ333333ó¿ÍÌÌÌÌÌÀÍÌÌÌÌÌü¿333333ó?š™™™™™ÀÍÌÌÌÌÌÀÍÌÌÌÌÌ ÀÍÌÌÌÌÌÀ333333Ó?333333ó¿333333Ó¿š™™™™™Àð?š™™™™™é¿à¿š™™™™™Ù?ÍÌÌÌÌÌô¿ø¿ffffffþ¿ffffffþ¿š™™™™™ñ¿à¿ø¿ø¿333333ÀÍÌÌÌÌÌô¿š™™™™™é¿à?333333À333333Ó¿Àš™™™™™é?ÍÌÌÌÌÌü¿ÍÌÌÌÌÌì?š™™™™™é¿š™™™™™À𿚙™™™™ù?ÍÌÌÌÌÌÀ333333ã?š™™™™™Àš™™™™™É?š™™™™™ù¿333333ó¿333333ã¿ÍÌÌÌÌÌô¿À࿚™™™™™Ù¿š™™™™™é?ÍÌÌÌÌÌô¿333333ÀÍÌÌÌÌÌü?š™™™™™É?š™™™™™Ù¿ffffffæ¿ÍÌÌÌÌÌì¿À333333Àffffffæ¿ø¿333333ã¿333333ó¿ÍÌÌÌÌÌü¿ÍÌÌÌÌÌü¿Àš™™™™™ù¿š™™™™™ÀÀffffff濚™™™™™é¿š™™™™™Ù¿š™™™™™Àš™™™™™é¿ÍÌÌÌÌÌü¿ffffffþ¿ffffffæ¿ffffffþ¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌì¿ø¿š™™™™™Ù¿ø¿ø¿ffffffæ¿ÍÌÌÌÌÌô¿à¿ÍÌÌÌÌÌÀÍÌÌÌÌÌì¿À333333ã?š™™™™™ù¿ffffffþ?ffffffÀø¿ÍÌÌÌÌÌÀ333333Ó¿ffffffÀð¿333333ã¿ÍÌÌÌÌÌì?333333À333333ã¿ffffffö¿š™™™™™ù?À333333Àš™™™™™ñ¿333333û¿š™™™™™ñ¿ffffffþ¿š™™™™™é¿333333û¿333333û¿š™™™™™ñ¿ð¿š™™™™™¹¿š™™™™™ñ¿333333Ó?š™™™™™ù?ffffffþ¿š™™™™™ñ¿ð¿š™™™™™Ù¿š™™™™™¹¿ÍÌÌÌÌÌô?Àà¿ffffffþ¿ffffffö¿ffffff ÀÍÌÌÌÌÌü¿ffffffæ¿333333ó¿Àš™™™™™É¿ð¿333333ó¿ð¿ð¿ÀÀ333333û¿333333ó¿š™™™™™é¿š™™™™™é¿ø¿š™™™™™Ù?š™™™™™Ù¿ffffff濚™™™™™ñ?333333ã¿ÍÌÌÌÌÌô¿333333Ó?ÍÌÌÌÌÌÀÍÌÌÌÌÌô?333333Ó¿ð¿ÍÌÌÌÌÌì¿ffffffö¿š™™™™™ù¿š™™™™™Àš™™™™™Ù?333333㿚™™™™™Ù¿ÍÌÌÌÌÌì¿@333333㿚™™™™™É¿ÍÌÌÌÌÌÀš™™™™™ Àš™™™™™É?ÍÌÌÌÌÌì¿333333ó¿š™™™™™ÀÀ333333Ó¿ÍÌÌÌÌÌô?ffffffæ¿ð¿ÍÌÌÌÌÌÀð¿ð¿š™™™™™ù¿š™™™™™@š™™™™™é?š™™™™™ñ¿š™™™™™ À333333ó¿ffffffþ¿š™™™™™É¿333333ó?ffffffþ¿ø¿š™™™™™ñ¿ÍÌÌÌÌÌô¿š™™™™™ÀffffffÀš™™™™™É¿À333333ÀffffffÀffffffæ¿333333Àš™™™™™É¿ÍÌÌÌÌÌ쿚™™™™™Ù¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌì?ffffffþ¿ÍÌÌÌÌÌ쿚™™™™™É¿ffffff濚™™™™™¹?ÍÌÌÌÌÌô¿š™™™™™Àffffffö?࿚™™™™™À333333û¿š™™™™™ ÀffffffÀø¿ÍÌÌÌÌÌì¿333333Ó¿ø¿333333ó¿š™™™™™ñ¿š™™™™™Àš™™™™™É¿ÍÌÌÌÌÌü¿ð¿š™™™™™À333333û¿à?ð¿ffffffþ¿333333ó¿ð¿à¿Àø¿Àffffffæ¿333333û¿š™™™™™ñ¿ÍÌÌÌÌÌÀ333333ó¿333333$Àš™™™™™.À Àffffff7ÀÀ333333,Àffffff#À333333*ÀÀ/À333333"ÀÍÌÌÌÌÌ ÀÍÌÌÌÌÌ$Àffffff6ÀGÀ'ÀÍÌÌÌÌÌ+Àffffff%À&ÀÍÌÌÌÌÌ4ÀÍÌÌÌÌÌ+Àš™™™™™(Àš™™™™™6À3333332Àfffffæ8À3333335Àš™™™™™$Àš™™™™0À;À(À333333û¿š™™™™™-À€0À333333>À$À7ÀÍÌÌÌÌÌ(À333333&À5À333333Àffffff(À3333332Àffffff*À€=Àš™™™™™$Àfffffæ0À3333332À3333335À'ÀÍÌÌÌÌL4Àš™™™™™&Àffffff!Àffffff4À33333s@À.À€:À€4ÀÍÌÌÌÌÌ(Àš™™™™™.Àffffff6À333333㿚™™™™™+À333333&À3Àš™™™™ÙAÀš™™™™™ÀÍÌÌÌÌÌ!ÀÍÌÌÌÌL6Àš™™™™™3Àš™™™™™Àš™™™™™6Àš™™™™™0À333333)À+Àfffffæ5ÀÍÌÌÌÌÌ#Àš™™™™™é¿333333.Àš™™™™1À2À33333³7À,ÀÍÌÌÌÌÌ&Àš™™™™™é¿ÍÌÌÌÌL2À€2Àš™™™™BÀ333333'ÀÍÌÌÌÌÌ1Àš™™™™CÀš™™™™™À3333330ÀÍÌÌÌÌÌÀffffff&Àffffff @333333À333333,ÀÍÌÌÌÌÌ;ÀÍÌÌÌÌÌÀ333333<À333333 À33333óBÀffffffþ?fffff&@Àš™™™™™0Àffffff:À$À33333³2À33333³2ÀÍÌÌÌÌL5Àffffff*À333333À)Àš™™™™™/Àš™™™™3ÀÀ333333.À333333#ÀÀ€4Àš™™™™7ÀÀš™™™™™À333333 ÀÀ33333³0Àfffffæ0À333333*ÀÍÌÌÌÌÌ&Àš™™™™ÙBÀ333333-Àš™™™™3ÀÍÌÌÌÌÌ,Àš™™™™™.À'Àffffff=Àfffff&@ÀÀš™™™™™?Àffffff'Àffffff>À333333À333333"Àš™™™™™AÀÍÌÌÌÌÌÀš™™™™™À"Àffffff,ÀffffffÀš™™™™™#À#Àš™™™™™)Àš™™™™™ñ¿š™™™™™"À333333+À333333/Àš™™™™™8Àš™™™™™)ÀÍÌÌÌÌŒ@Àš™™™™™#ÀÍÌÌÌÌL0ÀÍÌÌÌÌÌ1Àš™™™™7À333333À3333335ÀÍÌÌÌÌÌ/Àš™™™™™é¿š™™™™™%Àffffff&À/ÀÍÌÌÌÌÌ0ÀÍÌÌÌÌÌ Àš™™™™™+Àš™™™™5À333333)À(Àš™™™™8À333333/Àfffffæ6À33333³4À33333³8ÀÍÌÌÌÌÌ,ÀÍÌÌÌÌÌ(À€0Àffffff'ÀÍÌÌÌÌÌÀ'À333333Àffffff6À33333sEÀ𿚙™™™1Àš™™™™™/Àš™™™™8Àš™™™™1À333333Àš™™™™2ÀÀ333333/ÀOÀffffff*À333333/À333333CÀÍÌÌÌÌÌ,À0À1À333333ÀÍÌÌÌÌÌDÀÍÌÌÌÌLDÀš™™™™™/ÀÍÌÌÌÌÌÀ333333û¿ÍÌÌÌÌÌÀš™™™™™>ÀÍÌÌÌÌÌÀ$Àš™™™™™1ÀÍÌÌÌÌL0Àffffff濚™™™™™-ÀÍÌÌÌÌÌÀ333333ÀÍÌÌÌÌÌ*À€4Àš™™™™™ Àš™™™™1Àš™™™™™À/À333333.À333333EÀfffffæ5ÀÍÌÌÌÌL3À333333*ÀÍÌÌÌÌÌCÀš™™™™™=À2Àffffff4Àffffff"À€9Àfffff&AÀš™™™™™Àffffff1ÀÍÌÌÌÌÌ(À'ÀÍÌÌÌÌL3À'ÀÍÌÌÌÌÌ<Àffffff7Àffffff$À ÀÍÌÌÌÌÌÀffffff/Àš™™™™™É?+ÀffffffÀš™™™™™'À€7À333333ÀÍÌÌÌÌÌ(Àš™™™™™5Àfffffæ6Àffffff À€5Àš™™™™™-Àš™™™™™/Àffffff'À333333$À333333ÀÍÌÌÌÌÌÀš™™™™™'À+ÀÍÌÌÌÌÌ*À333333"ÀffffffÀ333333-Àš™™™™™5Àš™™™™™DÀfffffæ1ÀÍÌÌÌÌÌ<À€0Àffffff*Àš™™™™™&Àffffff Àš™™™™™#Àš™™™™™#ÀÀÍÌÌÌÌL5Àffffff%À333333 ÀÍÌÌÌÌL0À333333,À5À%ÀÍÌÌÌÌÌÀfffffæ1ÀÍÌÌÌÌŒEÀfffffæBÀÍÌÌÌÌÌ!@fffffæ3À333333:Àffffff Àffffff5ÀffffffÀš™™™™™Àš™™™™™!Àffffff1À333333+Àš™™™™<ÀCÀÍÌÌÌÌÌ!ÀÍÌÌÌÌÌ.Àš™™™™™AÀš™™™™™ Àffffff@ÍÌÌÌÌÌ,Àš™™™™™(À333333@*ÀÍÌÌÌÌÌ"Àš™™™™™ÀÍÌÌÌÌÌü¿333333ÀÍÌÌÌÌL0À333333GÀø?ÍÌÌÌÌÌ-Àffffff*À333333ÀÍÌÌÌÌÌ:À333333$Àš™™™™™,Àš™™™™:À5Àfffffæ2À333333,À333333'Àš™™™™™À€3ÀÍÌÌÌÌÌ/Àffffff5Àš™™™™™,ÀÍÌÌÌÌÌ$À333333$À333333+Àffffff(ÀÍÌÌÌÌÌ(Àffffff0À333333+ÀÍÌÌÌÌÌÀÍÌÌÌÌÌ&Àš™™™™™5ÀÍÌÌÌÌÌ*À€5Àš™™™™™#À@ffffff'Àš™™™™™À€7À.Àfffff¦AÀš™™™™AÀ3333337ÀÍÌÌÌÌÌ<À'À333333#Àš™™™™8À333333ÀÍÌÌÌÌÌ5ÀffffffÀ333333)ÀEÀš™™™™™ À€4Àffffff3À3333337À)À@À3333333Àð¿333333ó¿ffffff&Àš™™™™:Àfffffæ2À€2Àš™™™™™?Àš™™™™™!À3333336À XÀ33333³5ÀÍÌÌÌÌÌ À33333³1ÀÍÌÌÌÌÌ3Àš™™™™8ÀffffffÀ33333s@ÀÍÌÌÌÌÌ,ÀÀffffff"Àffffff!À7ÀÍÌÌÌÌÌ/À33333s@ÀÍÌÌÌÌL:@ÍÌÌÌÌÌ;ÀÍÌÌÌÌÌ&Àffffff4ÀPÀ)À€:À333333:ÀÍÌÌÌÌÌ0ÀÍÌÌÌÌŒ@Àš™™™™™1À33333³6Àffffff+Àfffffæ7Àš™™™™™À333333 Àš™™™™™'À€1À33333³5ÀÍÌÌÌÌL5À333333 À%À33333³6À333333+ÀÍÌÌÌÌÌ@3333331Àš™™™™™,Àš™™™™™Àš™™™™™)À333333À333333@333333'À<ÀÍÌÌÌÌÌ2ÀÍÌÌÌÌÌAÀÍÌÌÌÌÌ1ÀÍÌÌÌÌÌ,Àš™™™™™1À/Àš™™™™™Àš™™™™™ ÀÍÌÌÌÌÌ%ÀffffffÀ1ÀffffffÀš™™™™™,Àš™™™™™2@;À3333334À6À€9À€<ÀÍÌÌÌÌÌAÀÍÌÌÌÌÌ1À€CÀffffff-Àfffffæ1Àffffff4Àš™™™™8À/Àš™™™™7À333333-À?Àš™™™™™MÀÍÌÌÌÌÌ8ÀÍÌÌÌÌL:À333333,Àš™™™™™*Àfffff¦@ÀDÀš™™™™™2Àffffff>ÀÍÌÌÌÌÌ6À33333³AÀffffff?À3333336Àš™™™™™@À=ÀÍÌÌÌÌÌ@%À6Àfffff&CÀ33333³BÀffffff.Àfffffæ>À3333336Àš™™™™ÙDÀš™™™™ÙDÀ(Àš™™™™2À33333³7À5ÀÍÌÌÌÌ CÀ€0ÀÍÌÌÌÌÌ6ÀÍÌÌÌÌÌ9Àš™™™™7À3333331Àš™™™™7ÀÍÌÌÌÌL2ÀÍÌÌÌÌÌ3À333333;À33333sEÀfffffæ1Àš™™™™™GÀÍÌÌÌÌÌ=ÀÍÌÌÌÌL0À33333³0Àš™™™™@À333333À€@ÀÍÌÌÌÌÌ9ÀÀBÀÍÌÌÌÌÌIÀ&ÀÍÌÌÌÌÌ"ÀffffffDÀ€;Àš™™™™™,Àš™™™™YEÀÍÌÌÌÌÌ@À€4ÀÍÌÌÌÌL8À333333EÀ@@Àffffff@333333.ÀÍÌÌÌÌL:Àš™™™™™6À33333³>À333333<Àš™™™™™+ÀffffffÀffffff>À À;Àffffff3À333333!@3333334ÀÍÌÌÌÌÌ/ÀÍÌÌÌÌÌ"À3333334À3333332ÀÍÌÌÌÌÌ'Àš™™™™™5Àfffffæ7Àfffff¦GÀ333333+Àffffff5Àš™™™™8ÀfffffæDÀ333333û¿fffff&MÀ3333337À€;Àš™™™™™)À@AÀÍÌÌÌÌÌ!À-Àfffffæ5Àffffff2Àš™™™™6Àfffff&EÀš™™™™™>ÀÍÌÌÌÌÌ6À33333³<À9À2À33333³9ÀÍÌÌÌÌÌ=Àffffff%Àš™™™™™,À333333+À Àš™™™™YAÀfffffæ5ÀÍÌÌÌÌL4À2Àfffff¦GÀ€>À€7ÀÍÌÌÌÌL6ÀÍÌÌÌÌlPÀÍÌÌÌÌL;ÀÍÌÌÌÌLDÀš™™™™IÀš™™™™™'ÀÍÌÌÌÌLBÀ3333336À33333sEÀfffffæ1À€7Àfffff&JÀ33333³6À€5Àffffff8À€;Àffffff)À4ÀÍÌÌÌÌL4ÀÍÌÌÌÌÌ:Àfffffæ1À2ÀÍÌÌÌÌL<À333333AÀÍÌÌÌÌÌ?ÀÍÌÌÌÌÌ/ÀHÀš™™™™3ÀÍÌÌÌÌŒAÀfffffæ4ÀÍÌÌÌÌ GÀffffff5ÀÍÌÌÌÌ AÀ€2Àffffff#Àfffff¦CÀffffff0ÀÍÌÌÌÌL2ÀÍÌÌÌÌÌ2ÀÍÌÌÌÌÌ(Àš™™™™™;À€;À2Àš™™™™2À@@Àš™™™™™9ÀAÀš™™™™1ÀÍÌÌÌÌL?ÀÀAÀ333333.Àffffff-Àš™™™™™ÀffffffÀ333333+À"Àfffffæ0À3333336Àffffff(À€5À33333³7Àffffff#ÀÍÌÌÌÌL:Àffffff*Àš™™™™™<À33333³0Àš™™™™™(ÀffffffSÀÍÌÌÌÌÌ&Àš™™™™™3À3333330ÀÍÌÌÌÌ AÀÍÌÌÌÌÌ9Àfffff¦@À3333334Àš™™™™™8ÀÍÌÌÌÌLKÀš™™™™YAÀfffff¦BÀ3333331ÀÀÀEÀ333333Àfffffæ8Àffffff"Àfffffæ3ÀffffffÀš™™™™™'Àffffff-À1Àfffffæ9À@AÀ/À33333³5Àfffffæ4À33333³9Àfffff&AÀš™™™™YIÀÍÌÌÌÌL;À333333/Àfffff&BÀš™™™™™DÀÍÌÌÌÌÌIÀ333333 Àš™™™™@ÀffffffÀ33333³?ÀÀGÀš™™™™™,Àš™™™™™é?š™™™™™=Àš™™™™™À333333BÀ#À33333sDÀš™™™™™=ÀÍÌÌÌÌÌ)Àš™™™™™&ÀÍÌÌÌÌÌ2À3333332ÀÀffffffÀffffff*Àffffff?Àš™™™™™ Àš™™™™™ Àffffff0Àfffffæ9Àffffff;ÀÍÌÌÌÌÌ3Àš™™™™DÀ€2À€8ÀÍÌÌÌÌÌ+Àffffff3À33333³5À€4Àš™™™™™:À8À€AÀš™™™™7Àffffff0Àš™™™™™2Àffffff8À33333³LÀ3333336À€DÀfffffæ;Àš™™™™2Àfffffæ8ÀÍÌÌÌÌL2À333333-À3333335ÀÍÌÌÌÌÌÀ33333³=À3333335Àš™™™™™'Àš™™™™™9Àš™™™™™8ÀÍÌÌÌÌÌBÀ.Àffffff3ÀÍÌÌÌÌÌ#ÀJÀ333333*À333333.Àfffffæ<Àš™™™™ÙDÀÍÌÌÌÌÌ$À€BÀš™™™™™1Àš™™™™™#Àš™™™™™À€BÀš™™™™™3À@BÀ333333:À€8À€AÀ333333EÀ€4ÀÍÌÌÌÌÌü¿fffffæ:Àffffff1À333333"À€7Àffffff0À33333³0ÀÀ,ÀÍÌÌÌÌÌ-Àfffffæ:Àffffff+À3333335ÀÍÌÌÌÌÌ1ÀÍÌÌÌÌÌÀš™™™™™<À@€=Àfffffæ<ÀÍÌÌÌÌLAÀÍÌÌÌÌÌ;ÀfffffæEÀÍÌÌÌÌÌCÀš™™™™™À33333³9À33333³AÀffffff6Àfffffæ3ÀÍÌÌÌÌL2Àš™™™™0Àš™™™™™3À333333?À3333339Àš™™™™™<À33333³5ÀÀ0Àffffff=Àffffff"ÀÍÌÌÌÌL8Àffffff-ÀÍÌÌÌÌÌ"@333333.À0ÀÍÌÌÌÌL6À33333³=À€9Àš™™™™™AÀ?Àfffff&AÀÍÌÌÌÌL5ÀÍÌÌÌÌÌ3À33333óCÀÍÌÌÌÌÌô¿333333>ÀÍÌÌÌÌÌ%Àš™™™™2ÀÍÌÌÌÌÌ(À'Àš™™™™™5Àfffff¦AÀJÀ333333"Àš™™™™™¹?ÍÌÌÌÌÌ-ÀÍÌÌÌÌ AÀ333333)À333333À@=Àfffffæ7ÀffffffDÀBÀš™™™™™-À333333>À333333(À/Àš™™™™™/À33333óEÀ;Àš™™™™YAÀ333333Àffffff-Àš™™™™™=À0Àš™™™™™/À€1Àš™™™™™CÀfffff&CÀ333333AÀffffff%@@GÀÍÌÌÌÌÌ<À:ÀÍÌÌÌ̬TÀš™™™™™,ÀÍÌÌÌÌÌ>À333333<ÀÍÌÌÌÌL8À3333339À€>À;À333333/Àš™™™™Y@À333333ÀÍÌÌÌÌÌ/Àš™™™™™=À333333"ÀÍÌÌÌÌÌ8Àš™™™™YBÀ33333³7À333333IÀš™™™™ÙCÀÍÌÌÌÌL6À333333ó¿ÍÌÌÌÌL3Àfffffæ7À3À4ÀÍÌÌÌÌÌ:Àffffff @3333339À333333AÀš™™™™AÀfffffæFÀffffff$ÀÍÌÌÌÌÌ3À333333@À33333³6Àffffff(À%Àffffff?Àffffff/Àš™™™™™>ÀÍÌÌÌÌÌ$Àfffffæ;À%@fffffæEÀš™™™™;À333333Ó¿à?ÍÌÌÌÌÌì?š™™™™™¹¿à?š™™™™™É?š™™™™™¹?à¿à¿š™™™™™¹?š™™™™™¹?š™™™™™É¿š™™™™™¹¿à¿333333Ó¿š™™™™™¹¿š™™™™™É?à?š™™™™™¹?333333Ó?š™™™™™¹¿ÍÌÌÌÌÌì¿333333Ó?ÍÌÌÌÌÌì¿ffffffö¿š™™™™™É¿333333㿚™™™™™¹¿š™™™™™Ù¿š™™™™™Ù?š™™™™™é¿š™™™™™¹¿š™™™™™¹?š™™™™™É¿ffffff濚™™™™™Ù¿š™™™™™É?š™™™™™é¿ffffff濚™™™™™¹?š™™™™™¹¿š™™™™™¹?š™™™™™¹?š™™™™™¹?š™™™™™¹?333333Ó?333333Ó¿š™™™™™É¿š™™™™™É¿à?š™™™™™¹?š™™™™™Ù¿333333Ó¿333333㿚™™™™™É¿333333ó¿š™™™™™¹?333333㿚™™™™™¹?š™™™™™¹¿à?š™™™™™É?333333Ó¿š™™™™™¹¿ÍÌÌÌÌÌ쿚™™™™™¹¿š™™™™™É¿ffffff濚™™™™™¹?š™™™™™Ù¿š™™™™™ñ?ÍÌÌÌÌÌÀš™™™™™¹?࿚™™™™™Ù?š™™™™™¹?š™™™™™¹?š™™™™™¹¿ð¿š™™™™™¹?࿚™™™™™¹?š™™™™™É¿333333ã?࿚™™™™™¹¿š™™™™™É?š™™™™™É¿š™™™™™É?š™™™™™É?š™™™™™Ù¿š™™™™™É¿ffffffæ¿333333Ó¿š™™™™™Ù¿š™™™™™¹¿š™™™™™¹?š™™™™™É?à¿333333ã¿ffffffæ¿333333㿚™™™™™¹?333333㿚™™™™™É?ÍÌÌÌÌÌ@š™™™™™¹?š™™™™™Ù¿š™™™™™¹?333333㿚™™™™™¹?333333Ó¿ffffffæ?š™™™™™¹?š™™™™™É¿š™™™™™¹¿š™™™™™¹¿š™™™™™¹?š™™™™™¹?š™™™™™¹?š™™™™™¹¿à¿š™™™™™¹?333333ã?š™™™™™¹¿š™™™™™¹?š™™™™™É¿š™™™™™¹¿š™™™™™¹?š™™™™™¹¿š™™™™™¹¿š™™™™™É¿š™™™™™¹?š™™™™™Ù¿333333㿚™™™™™Ù¿š™™™™™¹¿š™™™™™É¿š™™™™™É¿š™™™™™Ù¿š™™™™™¹¿š™™™™™¹¿š™™™™™¹?š™™™™™Ù¿š™™™™™¹¿š™™™™™¹?à¿333333ã¿À333333Ó¿333333㿚™™™™™¹?š™™™™™¹¿š™™™™™¹?š™™™™™¹?333333Ó¿ÍÌÌÌÌÌô?š™™™™™Ù¿š™™™™™¹¿333333ó¿š™™™™™É¿š™™™™™Ù?333333ã¿ffffff濚™™™™™¹?š™™™™™¹?š™™™™™É¿ÍÌÌÌÌÌì?š™™™™™É¿š™™™™™¹?333333Ó?333333Ó¿š™™™™™Ù?š™™™™™¹?š™™™™™É¿š™™™™™¹?š™™™™™¹¿š™™™™™¹?š™™™™™Ù¿š™™™™™¹¿š™™™™™¹?333333ã¿333333㿚™™™™™Ù?š™™™™™Ù¿à¿333333ó¿š™™™™™¹¿à¿ffffff濚™™™™™¹¿š™™™™™¹¿333333ã¿333333Ó¿š™™™™™É¿à¿š™™™™™É?š™™™™™É¿333333Ó¿š™™™™™é¿š™™™™™é¿ð¿š™™™™™¹¿ÍÌÌÌÌÌ쿚™™™™™¹¿ÍÌÌÌÌÌì¿333333ã¿à¿š™™™™™¹?š™™™™™É¿š™™™™™¹¿š™™™™™¹?333333Ó¿š™™™™™É?333333ã¿333333Ó¿333333Ó¿333333Ó¿š™™™™™É¿š™™™™™¹¿š™™™™™É¿š™™™™™Ù¿š™™™™™¹¿š™™™™™Ù¿š™™™™™¹¿ffffff濚™™™™™¹?š™™™™™É?š™™™™™¹?š™™™™™É¿š™™™™™É?š™™™™™é¿š™™™™™É¿š™™™™™¹?š™™™™™Ù?333333Ó¿š™™™™™¹?𿚙™™™™Ù?š™™™™™¹¿š™™™™™Ù¿à?š™™™™™¹¿333333㿚™™™™™Ù¿š™™™™™É?333333ã?š™™™™™Ù?š™™™™™¹¿š™™™™™É¿š™™™™™¹¿š™™™™™É?š™™™™™¹?š™™™™™¹¿š™™™™™¹?ffffffæ?š™™™™™É¿333333㿚™™™™™Ù¿š™™™™™Ù¿ð¿š™™™™™¹¿š™™™™™¹¿ÍÌÌÌÌÌì¿à¿š™™™™™Ù¿š™™™™™¹?š™™™™™É?ÍÌÌÌÌÌì¿ffffffæ¿333333Ó?š™™™™™É?š™™™™™Ù¿š™™™™™¹¿š™™™™™É?š™™™™™¹?333333Ó¿š™™™™™¹?š™™™™™¹?š™™™™™¹¿à¿š™™™™™Ù?š™™™™™¹?ÍÌÌÌÌÌ쿚™™™™™¹¿š™™™™™é¿š™™™™™¹¿š™™™™™ñ¿š™™™™™É?š™™™™™É¿ÍÌÌÌÌÌì¿333333Ó¿š™™™™™¹?333333Ó¿š™™™™™¹¿š™™™™™ñ¿š™™™™™¹?ÍÌÌÌÌÌì¿ffffff濚™™™™™Ù¿à¿š™™™™™¹¿ffffff濚™™™™™Ù¿š™™™™™¹?333333ã?š™™™™™Ù¿333333ã?š™™™™™É¿š™™™™™É?š™™™™™¹?š™™™™™É¿š™™™™™É?333333Ó¿ffffffÀš™™™™™ù¿š™™™™™¹¿š™™™™™¹?333333Ó¿š™™™™™É¿š™™™™™¹¿333333ã¿à¿š™™™™™É?ÍÌÌÌÌÌì¿333333Ó¿333333㿚™™™™™É?š™™™™™Ù?š™™™™™ñ?à¿333333Ó¿š™™™™™Ù¿š™™™™™É¿š™™™™™¹¿š™™™™™é¿š™™™™™¹¿š™™™™™É¿š™™™™™É¿à?š™™™™™É¿š™™™™™Ù?š™™™™™¹?333333ӿ࿚™™™™™¹?ø¿š™™™™™Ù¿à?š™™™™™Ù¿333333ó?š™™™™™¹?š™™™™™É¿š™™™™™É?333333Ó?š™™™™™¹¿š™™™™™É?š™™™™™É?à?ffffffæ¿333333ã¿ffffffæ?š™™™™™Ù¿ffffff濚™™™™™É¿š™™™™™É¿ÍÌÌÌÌÌì¿à¿à¿333333Ó¿ÍÌÌÌÌÌÀš™™™™™é¿333333ã¿ÍÌÌÌÌÌÀš™™™™™Ù¿333333 Àà¿ð¿š™™™™™ñ¿333333ã¿ð¿ð¿š™™™™™Ù¿333333ã¿333333ã¿333333û¿à¿ÍÌÌÌÌÌ ÀÍÌÌÌÌÌÀ333333Àà¿à¿333333ã¿ffffffæ¿à¿333333ã¿ffffffÀš™™™™™ù¿ffffffþ¿ffffff Àffffffö¿333333㿚™™™™™Ù¿š™™™™™Ù¿ffffffæ¿à¿à¿š™™™™™ñ¿š™™™™™ñ¿333333ã¿ffffffæ¿333333ó¿333333ó¿ffffffÀ333333û¿ÀÍÌÌÌÌÌÀffffffö¿ð¿š™™™™™é¿à¿š™™™™™Ù¿ffffffæ¿à¿ÍÌÌÌÌÌì¿ð¿333333ó¿ÍÌÌÌÌÌÀš™™™™™ñ¿ffffffæ¿333333ã¿333333ã¿333333㿚™™™™™Ù¿333333ã¿Àð¿333333ã¿ffffffÀffffffÀš™™™™™Ù¿ÀÍÌÌÌÌÌì¿À333333ã¿ffffffæ¿ø¿333333ã¿ffffffæ¿à¿ÍÌÌÌÌÌ쿚™™™™™ñ¿333333ã¿ø¿à¿š™™™™™Àš™™™™™Ù¿Àà¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌü¿š™™™™™Ù¿š™™™™™Ù¿ÍÌÌÌÌÌô¿333333ã¿333333㿚™™™™™Ù¿ffffffö¿ÍÌÌÌÌÌô¿š™™™™™Ù¿š™™™™™ÀÍÌÌÌÌÌì¿ffffff!Àš™™™™™é¿š™™™™™Ù¿ffffff濚™™™™™ñ¿333333ó¿ð¿ÍÌÌÌÌÌÀš™™™™™ù¿š™™™™™Ù¿333333ó¿à¿333333À333333ã¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌÀ࿚™™™™™é¿š™™™™™ñ¿ffffffö¿ø¿ð¿ffffff濚™™™™™é¿à¿š™™™™™ Àffffffæ¿Àš™™™™™ÀÍÌÌÌÌÌÀ࿚™™™™™ñ¿333333Àš™™™™™Ù¿ffffffæ¿333333û¿à¿Àš™™™™™é¿š™™™™™Ù¿š™™™™™Ù¿à¿ffffffæ¿333333ã¿ffffffö¿ÍÌÌÌÌÌÀà¿ffffffÀð¿333333ã¿ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333û?š™™™™™ @š™™™™™@ÍÌÌÌÌÌü?333333ã?ø?ffffff@ffffff@ffffff@ffffff @333333û?š™™™™™Ù?š™™™™™é?ÍÌÌÌÌÌì?š™™™™™@333333ó?@@ø?333333!@ÍÌÌÌÌÌì?ffffff@333333"@ffffff&@š™™™™™@ffffffþ?ffffff@ÍÌÌÌÌÌ@š™™™™™ñ?ð?333333@333333û? @š™™™™™&@ÍÌÌÌÌÌü?š™™™™™@ffffff@ÍÌÌÌÌÌì?333333@333333@ÍÌÌÌÌÌ@š™™™™™@333333ã?333333!@ffffff@@@ffffffæ?š™™™™™@š™™™™™@ÍÌÌÌÌÌ@333333@@š™™™™™@ffffffþ?š™™™™™Ù?š™™™™™ù?@333333@ÍÌÌÌÌÌô?@ÍÌÌÌÌÌ@š™™™™™ @š™™™™™é?333333#@š™™™™™é?333333@ÍÌÌÌÌÌô?ø?ø?š™™™™™ù?ø? @š™™™™™é?ÍÌÌÌÌÌ@š™™™™™Ù?ÍÌÌÌÌÌ@ÍÌÌÌÌÌô?333333@ÍÌÌÌÌÌ@@333333ó?333333@ÍÌÌÌÌÌ@@ð?ffffff@ÍÌÌÌÌÌ@š™™™™™Ù?333333ã?ffffff @ÍÌÌÌÌÌ@š™™™™™$@333333@333333@ffffff @333333@@š™™™™™@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333ó?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@@š™™™™™@ÍÌÌÌÌÌ @ffffff@333333û?š™™™™™@ffffffþ?ÍÌÌÌÌÌì?@š™™™™™@š™™™™™é?ÍÌÌÌÌÌü?š™™™™™$@š™™™™™@ffffff@š™™™™™é?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌì?@ø?š™™™™™(@ @š™™™™™@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@š™™™™™Ù?ÍÌÌÌÌÌ @à?333333@š™™™™™@š™™™™™@@ð?333333ã?š™™™™™ @333333û?@ffffff @š™™™™™@333333ã?@@ÍÌÌÌÌÌ@333333ã?ø?@@ð?@ÍÌÌÌÌÌ@ffffff@@ffffff@333333@ffffff@š™™™™™ñ?š™™™™™(@333333@ÍÌÌÌÌÌì?ffffff@@ÍÌÌÌÌÌ@333333 @333333@š™™™™™é?@333333@#@@š™™™™™Ù?š™™™™™ @ @@ÍÌÌÌÌÌì?333333ã?ÍÌÌÌÌÌ@ffffffö? @333333@ffffff@333333)@š™™™™™ñ?ø?ffffff@š™™™™™@š™™™™™Ù?š™™™™™é?ð?333333@à?@333333 @š™™™™™é?@333333 @ffffff@ffffff@š™™™™™ @ffffffæ?ffffff@š™™™™™Ù?ffffffþ?š™™™™™ @333333 @š™™™™™@š™™™™™@333333@ÍÌÌÌÌÌ@333333@@ÍÌÌÌÌÌ@à?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@@ÍÌÌÌÌÌü?@333333@333333ó?ffffffþ?š™™™™™@@@ffffff@ÍÌÌÌÌÌ@ffffff@ÍÌÌÌÌÌ@ffffff@ÍÌÌÌÌÌ$@ffffff@ffffff @š™™™™™@š™™™™™ù?ÍÌÌÌÌÌ@@ÍÌÌÌÌÌ"@š™™™™™@@à?ffffffþ?333333û?à?ffffff@š™™™™™@ÍÌÌÌÌÌ@ð?ffffffþ?ø?ffffff@ffffff@š™™™™™!@333333@333333ó?ffffffþ?333333@333333@ @ÍÌÌÌÌÌ@š™™™™™@@š™™™™™é?ffffffæ?ffffffæ?@š™™™™™ @š™™™™™ñ?@ÍÌÌÌÌÌ@@@ffffff+@ffffff@ÍÌÌÌÌÌ@333333 @š™™™™™@333333@333333@ffffff#@333333ó?š™™™™™é?ffffffþ?ÍÌÌÌÌÌ@ffffff @š™™™™™Ù?ffffff@"@ffffff@š™™™™™@š™™™™™@ffffffæ?š™™™™™ @š™™™™™ñ?333333@ÍÌÌÌÌÌ@@@333333+@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333*@ÍÌÌÌÌÌ @@š™™™™™@@ð?š™™™™™ @333333@ÍÌÌÌÌÌ@š™™™™™é?š™™™™™ù?š™™™™™ @š™™™™™ñ?ffffffþ?333333 @333333@š™™™™™Ù?ffffff @@ffffff@333333 @š™™™™™@š™™™™™@@@ÍÌÌÌÌÌô?333333&@š™™™™™é?ÍÌÌÌÌÌ@#@ffffff@ø?š™™™™™@ffffff@333333@@333333@š™™™™™é?š™™™™™@ÍÌÌÌÌÌü?š™™™™™@ø?@š™™™™™@ÍÌÌÌÌÌ@ffffff@ffffff@ÍÌÌÌÌÌü?ø?ffffff@ffffff@@š™™™™™@š™™™™™é?š™™™™™ @ffffffæ?š™™™™™@333333/@ÍÌÌÌÌÌ(@333333ó?ÍÌÌÌÌÌì?ffffffö?ÍÌÌÌÌÌü?@š™™™™™Ù?ffffff%@š™™™™™ @@ÍÌÌÌÌÌì?ffffff@@ffffff@ffffffæ?333333(@ffffff@ffffff@ÍÌÌÌÌÌ@š™™™™™@ð?ffffffþ?333333û?š™™™™™Ù?ð?ffffffþ?ÍÌÌÌÌÌ@333333@ÍÌÌÌÌÌ@š™™™™™ñ?ffffff@333333@333333û?333333û?ffffffö?333333@ð?ffffff@ÍÌÌÌÌÌ@ffffff@333333@ÍÌÌÌÌÌ@ÍÌÌÌÌÌô?š™™™™™ @š™™™™™ñ?ÍÌÌÌÌÌ@š™™™™™ñ?à?š™™™™™@š™™™™™@ÍÌÌÌÌÌ@333333@ÍÌÌÌÌÌ%@333333ó?š™™™™™@ð¿ÍÌÌÌÌÌ쿚™™™™™Àš™™™™™Ù¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌ)ÀffffffÀffffffþ¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌÀÍÌÌÌÌÌì¿ÀffffffÀš™™™™™ À𿚙™™™™ñ¿333333ó¿ÍÌÌÌÌÌÀ333333ó¿š™™™™™Àš™™™™™ù¿ffffffþ¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌô¿š™™™™™À333333ÀÍÌÌÌÌÌÀffffffþ¿Àffffffþ¿š™™™™™Àð¿333333Àffffffö¿ÍÌÌÌÌÌü¿ð¿š™™™™™ ÀÀš™™™™™é¿š™™™™™Ù¿š™™™™™é¿ÍÌÌÌÌÌì¿333333ã¿333333ã¿333333û¿š™™™™™ ÀffffffÀ333333û¿ ÀÍÌÌÌÌÌü¿333333(À𿚙™™™™ñ¿ffffffö¿ffffffÀø¿ffffffþ¿333333ã¿ffffffæ¿Àð¿ffffffþ¿š™™™™™Àffffffæ¿ÍÌÌÌÌÌÀÀš™™™™™Ù¿ffffffþ¿333333Àš™™™™™*Àš™™™™™Àš™™™™™Àffffffö¿š™™™™™ÀÀffffffö¿š™™™™™Àš™™™™™Ù¿ÍÌÌÌÌÌü¿ÍÌÌÌÌÌ쿚™™™™™ù¿ø¿š™™™™™ÀffffffÀffffffö¿ÍÌÌÌÌÌÀÍÌÌÌÌÌ Àš™™™™™ñ¿ø¿ÍÌÌÌÌÌÀš™™™™™Àš™™™™™Àffffffö¿333333Àffffff*ÀÍÌÌÌÌÌ쿚™™™™™ñ¿ÍÌÌÌÌÌô¿333333ÀffffffÀÍÌÌÌÌÌì¿ÍÌÌÌÌÌÀffffffþ¿ÍÌÌÌÌÌô¿333333ó¿ÍÌÌÌÌÌÀffffffæ¿ÍÌÌÌÌÌô¿š™™™™™ù¿š™™™™™Ù¿À333333㿚™™™™™ ÀÀš™™™™™é¿ffffff ÀÍÌÌÌÌÌÀ333333û¿333333 ÀÍÌÌÌÌÌì¿ffffffÀ333333ÀÀffffffö¿š™™™™™ñ¿333333Àà¿ffffffþ¿ffffffÀš™™™™™ù¿š™™™™™é¿ffffffÀffffffþ¿ffffffæ¿À333333ó¿š™™™™™ñ¿ÍÌÌÌÌÌÀffffffö¿š™™™™™ñ¿š™™™™™é¿š™™™™™ ÀÀš™™™™™ñ¿333333ÀÍÌÌÌÌÌü¿š™™™™™Ù¿ffffff濚™™™™™é¿ÍÌÌÌÌÌÀÍÌÌÌÌÌì¿Àš™™™™™Àš™™™™™ù¿ÀÀð¿ÍÌÌÌÌÌÀ333333û¿À333333Àš™™™™™é¿ÍÌÌÌÌÌü¿ð¿ÍÌÌÌÌÌÀffffff À333333Àš™™™™™!ÀÍÌÌÌÌÌÀš™™™™™ñ¿À333333ÀÍÌÌÌÌÌÀš™™™™™Àffffffö¿333333û¿333333 Àà¿À333333Àø¿333333 À333333ÀÍÌÌÌÌÌì¿333333 ÀÍÌÌÌÌÌÀÍÌÌÌÌÌ Àš™™™™™ñ¿ÍÌÌÌÌÌì¿à¿ffffffÀffffffÀffffffö¿ffffffÀ333333ó¿ffffffÀÍÌÌÌÌÌÀÀffffffÀø¿Àffffffö¿333333"À333333㿚™™™™™Àš™™™™™ñ¿ffffffÀ333333À333333û¿ÍÌÌÌÌÌÀø¿ÍÌÌÌÌÌ쿚™™™™™À#À𿚙™™™™ñ¿š™™™™™ÀffffffÀÍÌÌÌÌÌô¿ø¿ÍÌÌÌÌÌì?333333û?ffffff@š™™™™™é?333333ã?š™™™™™é?š™™™™™é?ÍÌÌÌÌÌì?ð?ø?ffffffö?š™™™™™¹¿š™™™™™Ù?š™™™™™É¿ffffffæ?š™™™™™¹¿š™™™™™Ù?333333Ó¿333333ã?333333㿚™™™™™¹¿š™™™™™Ù¿333333Ó?333333ã?š™™™™™¹¿ffffffæ?š™™™™™¹?š™™™™™Ù¿333333Ó?š™™™™™É¿š™™™™™¹¿š™™™™™É?š™™™™™¹¿333333Ó?š™™™™™¹¿š™™™™™Ù¿š™™™™™É¿š™™™™™¹?š™™™™™Ù?à?š™™™™™Ù?333333Ó?š™™™™™É¿ffffffæ?š™™™™™¹?࿚™™™™™Ù?š™™™™™é?ÍÌÌÌÌÌì?à?333333Ó¿š™™™™™Ù?š™™™™™É¿333333Ó¿š™™™™™¹¿š™™™™™¹¿š™™™™™¹¿333333ã¿333333Ó?333333Ó¿š™™™™™É¿333333Ó¿š™™™™™¹¿333333Ó?š™™™™™¹¿š™™™™™É?à?š™™™™™¹¿š™™™™™É¿š™™™™™É¿333333Ó?š™™™™™¹?333333Ó¿š™™™™™Ù¿š™™™™™É¿š™™™™™Ù?š™™™™™É¿š™™™™™¹¿š™™™™™Ù?333333Ó?š™™™™™É¿š™™™™™Ù?à?š™™™™™Ù¿ÍÌÌÌÌÌì?333333Ó?š™™™™™¹¿333333Ó?š™™™™™Ù?à¿à¿š™™™™™¹¿š™™™™™É¿š™™™™™¹¿š™™™™™¹¿333333Ó?333333Ó¿333333Ó¿š™™™™™É¿333333ã?à?š™™™™™¹¿š™™™™™¹¿š™™™™™é?333333ã¿à?à?š™™™™™É¿à?à?333333Ó¿š™™™™™É?š™™™™™É?š™™™™™¹¿ÍÌÌÌÌÌì?š™™™™™É?š™™™™™¹¿333333ã?333333ã?à?à?š™™™™™É¿333333Ó?333333Ó?333333Ó?š™™™™™¹¿333333Ó?š™™™™™É¿š™™™™™¹¿š™™™™™É¿š™™™™™É¿š™™™™™¹¿333333ã¿333333Ó¿à?333333ã?š™™™™™É¿š™™™™™¹¿š™™™™™Ù?š™™™™™é?333333Ó¿333333ã¿à¿333333Ó¿ffffffæ?333333ã?à¿333333Ó¿š™™™™™¹?š™™™™™é?333333ã?š™™™™™¹¿š™™™™™¹¿333333ã?š™™™™™¹¿š™™™™™¹¿š™™™™™Ù?š™™™™™Ù¿š™™™™™¹¿š™™™™™É¿333333Ó?š™™™™™¹¿š™™™™™¹?333333ã?š™™™™™¹¿333333Ó¿333333ã?ffffffæ?š™™™™™É¿à?333333Ó¿333333Ó?š™™™™™¹¿š™™™™™¹¿à?࿚™™™™™Ù?š™™™™™¹¿333333Ó?ffffff濚™™™™™É¿à?š™™™™™É?š™™™™™Ù?333333Ó¿š™™™™™Ù¿š™™™™™Ù?333333Ó?š™™™™™É¿š™™™™™É¿333333ã?š™™™™™¹¿ffffffæ?š™™™™™¹¿333333Ó¿š™™™™™É?š™™™™™É?333333Ó?š™™™™™¹¿š™™™™™Ù¿333333ã?š™™™™™É?š™™™™™¹¿š™™™™™É¿š™™™™™É¿š™™™™™¹?š™™™™™Ù¿333333ã?à?ffffffæ?333333ã?333333Ó?š™™™™™É?š™™™™™É¿š™™™™™¹¿š™™™™™Ù?à?š™™™™™¹¿š™™™™™É?333333㿚™™™™™é?š™™™™™é¿š™™™™™¹¿à?š™™™™™É¿š™™™™™é?333333Ó¿š™™™™™¹?š™™™™™É¿š™™™™™¹¿š™™™™™Ù¿333333Ó?à?ffffffæ?š™™™™™¹?333333ã?š™™™™™¹¿š™™™™™É¿ffffffæ?333333Ó?š™™™™™Ù¿333333Ó¿š™™™™™É¿š™™™™™¹¿à¿ð?š™™™™™¹¿š™™™™™¹¿š™™™™™Ù?333333Ó¿333333ã?333333Ó¿š™™™™™Ù?š™™™™™Ù¿333333Ó¿ÍÌÌÌÌÌì?š™™™™™É¿š™™™™™É?š™™™™™É¿š™™™™™É¿š™™™™™¹¿333333Ó?333333Ó¿à¿ÍÌÌÌÌÌì?š™™™™™Ù¿š™™™™™¹¿š™™™™™¹¿à?š™™™™™¹¿š™™™™™É¿š™™™™™É¿š™™™™™É¿š™™™™™É?š™™™™™é?š™™™™™¹?š™™™™™Ù?š™™™™™É¿333333ã?à?š™™™™™É?ffffffæ?333333Ó¿333333Ó?ÍÌÌÌÌÌì?333333Ó¿š™™™™™Ù?333333ã?à¿à?333333Ó?š™™™™™¹¿333333Ó¿š™™™™™Ù?š™™™™™¹¿š™™™™™Ù¿š™™™™™Ù?š™™™™™Ù?š™™™™™¹¿š™™™™™É¿š™™™™™É?š™™™™™¹¿333333Ó?à?š™™™™™¹¿š™™™™™¹¿333333Ó¿š™™™™™¹¿š™™™™™Ù?š™™™™™É¿333333Ó?333333ã?š™™™™™Ù¿š™™™™™¹?š™™™™™é?ffffffæ?š™™™™™É¿š™™™™™¹¿333333Ó¿ð?š™™™™™Ù?š™™™™™¹¿333333ã?à?š™™™™™¹¿š™™™™™¹?333333ã?š™™™™™Ù¿333333Ó¿š™™™™™¹¿š™™™™™É¿333333Ó?š™™™™™¹¿š™™™™™Ù?ffffffæ?ð¿333333Ó¿š™™™™™É¿333333Ó¿333333Ó?š™™™™™É¿š™™™™™É¿333333Ó?š™™™™™é¿à?333333ã?333333Ó¿š™™™™™¹?333333Ó¿š™™™™™É¿š™™™™™Ù?š™™™™™É?š™™™™™é?࿚™™™™™É¿333333Ó?333333Ó¿š™™™™™Ù¿š™™™™™é?š™™™™™É¿ð?333333ã?333333Ó?à?š™™™™™É¿š™™™™™¹¿š™™™™™¹?à?333333Ó¿š™™™™™Ù¿š™™™™™¹?333333Ó¿ffffffæ?š™™™™™¹¿š™™™™™¹¿š™™™™™¹¿à?ffffffæ?š™™™™™É¿333333Ó¿š™™™™™Ù?333333Ó?à¿333333Ó?š™™™™™Ù?š™™™™™É?ffffffæ?à?ffffffæ?ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?š™™™™™É¿š™™™™™Ù?333333Ó¿š™™™™™É¿š™™™™™¹¿ffffffæ?š™™™™™¹?ð?333333ã?š™™™™™¹¿š™™™™™Ù?333333Ó?ffffffö¿333333Ó?333333Ó?333333ã¿ffffffæ?š™™™™™É¿à?š™™™™™É¿à?333333ã?333333Ó?333333Ó¿à?š™™™™™Ù¿à?š™™™™™Ù?š™™™™™¹¿š™™™™™¹¿333333Ó¿š™™™™™É¿à?š™™™™™¹¿š™™™™™É¿š™™™™™É¿š™™™™™Ù?ffffffæ?š™™™™™¹¿š™™™™™É¿333333ã?333333Ó¿333333ӿ࿚™™™™™É?š™™™™™¹¿š™™™™™É¿š™™™™™¹¿333333Ó?š™™™™™É¿š™™™™™Ù?333333ã?à?333333Ó¿š™™™™™Ù?š™™™™™¹?@ffffffæ?š™™™™™É?333333ó?ffffffæ?ð?333333ó?š™™™™™ñ?š™™™™™Ù¿š™™™™™Ù?ffffffæ?࿚™™™™™é?š™™™™™É¿ffffffæ?š™™™™™¹?š™™™™™É?ð?à?ffffff濚™™™™™¹?š™™™™™é?š™™™™™é?š™™™™™ñ?š™™™™™Ù¿š™™™™™É?333333ã?š™™™™™ñ?ÍÌÌÌÌÌü¿š™™™™™Ù?š™™™™™é?333333ó¿ffffffö¿ffffffæ?333333ã?ffffffö?š™™™™™¹?333333Ó?à?ð?ÍÌÌÌÌÌü?š™™™™™@ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?š™™™™™ñ?333333Ó?333333Ó?ÍÌÌÌÌÌì?š™™™™™É¿š™™™™™ñ?333333ã?š™™™™™Ù¿š™™™™™É?š™™™™™É¿ffffffæ?š™™™™™é?à?š™™™™™É¿ÍÌÌÌÌÌô?ð?à?š™™™™™Ù?š™™™™™É?š™™™™™Ù?ø?š™™™™™¹¿š™™™™™É¿š™™™™™é?š™™™™™Ù?š™™™™™É?ffffffæ?š™™™™™Ù?š™™™™™É?333333ó?ÍÌÌÌÌÌô?š™™™™™Ù?ð?à?ffffff濚™™™™™é¿à?333333Ó¿à?ffffffæ?à?š™™™™™É?à¿ffffffæ¿333333Ó¿ffffffö¿š™™™™™Ù?à¿333333û¿ÍÌÌÌÌÌô?333333ó¿š™™™™™ù?š™™™™™ñ?333333Ó?š™™™™™¹¿ÍÌÌÌÌÌì?š™™™™™¹¿ÍÌÌÌÌÌô?ffffff濚™™™™™ù?š™™™™™¹¿333333Ó¿à?ffffffæ?š™™™™™É?š™™™™™Ù¿ÍÌÌÌÌÌô?š™™™™™Ù?ÍÌÌÌÌÌì¿ð?333333ã?š™™™™™¹¿š™™™™™ñ?333333ó?333333ó?ð?ffffffö?333333ó?333333Ó¿š™™™™™É?š™™™™™Ù?333333ã?š™™™™™É¿š™™™™™É?ÍÌÌÌÌÌì?333333Ó¿š™™™™™¹¿š™™™™™É?à?ÍÌÌÌÌÌì¿à?333333ã?@333333ã?à?š™™™™™Ù?š™™™™™ñ¿š™™™™™Ù?333333ã?ffffffæ?ffffffæ¿333333Ó?ÍÌÌÌÌÌô?š™™™™™Ù?333333û?š™™™™™é?š™™™™™ù?333333ã?š™™™™™ñ?333333Ó¿333333ó?ÍÌÌÌÌÌô?333333ó?š™™™™™é?333333ó?ð?333333Ó?ÍÌÌÌÌÌì?š™™™™™¹¿š™™™™™é?ffffffæ?333333Ó¿à?š™™™™™É?š™™™™™Ù?ffffffæ?ffffffæ?࿚™™™™™Ù¿š™™™™™É?š™™™™™Ù¿š™™™™™¹¿š™™™™™Ù¿š™™™™™é?ffffffæ?ÍÌÌÌÌÌô?ð?333333Ó¿333333ó?š™™™™™Ù¿š™™™™™¹¿ffffffæ?ÍÌÌÌÌÌô¿ÍÌÌÌÌÌ쿚™™™™™é?š™™™™™é¿š™™™™™Ù?Àš™™™™™é?š™™™™™Ù?333333Ó?à?ÍÌÌÌÌÌì?333333Ó?à?ÍÌÌÌÌÌì?ffffff@ð?š™™™™™¹¿333333Ó?𿚙™™™™ñ?š™™™™™À333333Ó?ffffffæ?ð¿ÍÌÌÌÌÌô?333333û¿Àffffffæ¿ø?ð?ÍÌÌÌÌÌì?ð¿ffffffþ?š™™™™™é¿š™™™™™¹¿š™™™™™É¿333333ó?š™™™™™é?š™™™™™¹¿š™™™™™¹¿š™™™™™É¿š™™™™™ñ?š™™™™™é?ÍÌÌÌÌÌ@š™™™™™É¿à?𿚙™™™™¹?š™™™™™Ù¿à¿ÍÌÌÌÌÌô¿à?š™™™™™ñ¿333333Ó?ffffffþ?š™™™™™Ù?ffffffö¿ÍÌÌÌÌÌü?š™™™™™É¿ffffffæ?333333Ó?à?333333Ó¿333333Ó¿ÍÌÌÌÌÌì?à?ffffffæ?ffffffæ¿ffffffö¿ÍÌÌÌÌÌì¿333333ó?š™™™™™ñ¿ð¿333333ã¿à¿ffffffæ?ffffffæ?333333û?333333Ó?ffffffæ?š™™™™™É?š™™™™™é?ÍÌÌÌÌÌü?ø?š™™™™™ñ?à?š™™™™™¹¿à¿š™™™™™É¿333333ó?š™™™™™É?ø?333333ã?333333ã?ÍÌÌÌÌÌô?333333㿚™™™™™ñ?ÍÌÌÌÌÌì?à?333333Ó?š™™™™™Ù¿š™™™™™é?š™™™™™Ù?š™™™™™é?333333ó?333333Ó¿ÍÌÌÌÌÌì¿à?ffffff @š™™™™™É¿š™™™™™ñ?ÍÌÌÌÌÌü¿ÍÌÌÌÌÌô?š™™™™™ñ?ÍÌÌÌÌÌì?ø?ÍÌÌÌÌÌü?à?ÍÌÌÌÌÌô¿333333Ó?š™™™™™ñ?ÍÌÌÌÌÌì?š™™™™™ñ?333333ã?š™™™™™É¿š™™™™™ñ?ÍÌÌÌÌÌô?ð¿ÍÌÌÌÌÌì?ð?š™™™™™¹¿ð?333333ã?ffffff濚™™™™™é¿333333Ó¿š™™™™™ñ?š™™™™™é¿š™™™™™ñ¿š™™™™™É¿ffffffæ?ffffffæ¿ÍÌÌÌÌÌô¿ø?š™™™™™ñ¿ÍÌÌÌÌÌü?š™™™™™Ù?ð?ÍÌÌÌÌÌô?ÍÌÌÌÌÌü¿š™™™™™¹¿333333ã?333333Ó?à?333333ã¿333333ó?š™™™™™Ù?333333Ó?ð?ÍÌÌÌÌÌì?š™™™™™ñ?š™™™™™¹¿š™™™™™Ù?š™™™™™É¿333333Ó¿š™™™™™É¿š™™™™™ñ¿š™™™™™Ù?ÍÌÌÌÌÌÀffffffæ¿333333ó¿ÍÌÌÌÌÌì¿333333Ó¿ð¿333333Ó?ÍÌÌÌÌÌô¿š™™™™™Ù?࿚™™™™™é?ÍÌÌÌÌÌü?š™™™™™é¿333333ã?š™™™™™Ù?ffffffö?ÍÌÌÌÌÌô¿333333Ó?333333Ó¿š™™™™™ñ¿333333Ó?š™™™™™É¿š™™™™™ñ¿š™™™™™Ù?š™™™™™Ù?333333ó?à¿ð?ð?ø?333333ã?š™™™™™é?ð¿333333ó?ÍÌÌÌÌÌÀffffffö¿à¿š™™™™™¹?ÍÌÌÌÌÌì?ffffffæ?333333Ó¿š™™™™™Ù?š™™™™™Ù?à?ffffffö¿ÍÌÌÌÌÌì?š™™™™™ñ?š™™™™™ñ?333333ã¿333333û?š™™™™™é?ffffffæ¿ffffffæ¿ffffffö?š™™™™™é¿š™™™™™ñ?š™™™™™¹¿ÍÌÌÌÌÌô?333333Ó¿š™™™™™é?š™™™™™É¿ffffffæ?š™™™™™é?333333Ó¿333333ã?333333Ó¿333333ã¿à?à?333333Ó? À333333ó?ffffffæ?š™™™™™é?š™™™™™É¿š™™™™™É?ø?333333Ó?š™™™™™Ù?à¿333333ó?333333ã?š™™™™™É?š™™™™™Ù?š™™™™™Ù¿š™™™™™ñ?333333ã?ÍÌÌÌÌÌì?ffffffö?333333Ó?ÍÌÌÌÌÌô?333333Ó¿333333ã?333333ã?333333Ó¿ffffffþ?333333ó?ffffffæ¿333333,@š™™™™™ @ÍÌÌÌÌÌ"ÀÍÌÌÌÌÌ"@ffffff @š™™™™YCÀš™™™™™ ÀÀ333333%ÀffffffÀ @ffffff5@ffffffÀ333333@ffffff@ffffffÀÍÌÌÌÌÌì?333333Àš™™™™6@333333@š™™™™™Àš™™™™™é¿333333(@ffffffæ?ÍÌÌÌÌL?@'À,@fffffæ>@fffffæA@€7@333333@ffffff!@33333³2@333333Ó?333333Ó¿ÍÌÌÌÌÌü?š™™™™™@ffffff6@fffff¦@@ffffffæ?ÍÌÌÌÌÌü¿š™™™™™ù?ÍÌÌÌÌÌì?3333330@€2@ÍÌÌÌÌÌ&@@ffffffæ?€;@ÍÌÌÌÌÌ"@š™™™™™@0@š™™™™™é¿š™™™™™@ÍÌÌÌÌÌ(@š™™™™™ @333333'@@ffffff(À333333&Àffffff@ffffff ÀÀffffff)Àà¿ffffff@ÍÌÌÌÌÌü¿ÍÌÌÌÌÌ*@333333#@š™™™™™@ø¿33333³1@ÍÌÌÌÌÌô¿Àffffff.À!@33333³0À333333û?333333ó¿š™™™™™À@@š™™™™™ Àffffffæ?ffffffÀš™™™™™Ù¿š™™™™™ñ¿š™™™™A@ @2@@ffffff*@333333ã?ÍÌÌÌÌÌ@$@ÍÌÌÌÌÌ @š™™™™™ù?ÍÌÌÌÌL0@333333%@š™™™™™%ÀffffffÀ333333$@333333@š™™™™™1@ffffff!@š™™™™™@š™™™™™/Àš™™™™™(@š™™™™™/@š™™™™™@333333Ó?Àà¿@ÍÌÌÌÌÌ @333333&@%@333333@333333,@ÍÌÌÌÌÌ@š™™™™™5@@š™™™™™É¿ffffffö?ÍÌÌÌÌL0@ @ÍÌÌÌÌÌ @€?@š™™™™™#Àš™™™™™@š™™™™™ Àš™™™™™À333333Ó?333333)À333333ó?ffffffö¿ÀÍÌÌÌÌÌü?ffffffÀÍÌÌÌÌÌÀš™™™™™)@ÍÌÌÌÌÌì¿333333@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ"ÀÍÌÌÌÌÌ%@Àš™™™™™¹¿ffffff ÀÍÌÌÌÌÌ@333333)@š™™™™™@ÀÍÌÌÌÌÌ!@333333 @333333)Àš™™™™™@1@ À33333³2@ffffff@333333#@ffffffþ¿š™™™™™ñ?"@š™™™™™)@333333㿚™™™™™@ÍÌÌÌÌÌ/@š™™™™5@@ffffff.@ÍÌÌÌÌÌ!À333333(@ÍÌÌÌÌÌÀ333333ã¿ÍÌÌÌÌÌ>@3333335@#ÀÍÌÌÌÌÌ$@ffffff@ÍÌÌÌÌÌ @ÍÌÌÌÌÌ5@ÍÌÌÌÌÌ&@š™™™™™ Àš™™™™™¹?ÍÌÌÌÌÌ(@33333sM@ÍÌÌÌÌÌ&@ÍÌÌÌÌÌÀ<@ffffff@ffffff@À33333³0Àš™™™™™#@ffffff=Àš™™™™™4@ÍÌÌÌÌÌ*@-@š™™™™™B@š™™™™™@5ÀÍÌÌÌÌÌ!Àš™™™™™%@=@À333333 ÀÍÌÌÌÌÌÀš™™™™™À333333@ÍÌÌÌÌÌ;À@ÍÌÌÌÌÌ)@ffffff@ÍÌÌÌÌÌ@333333Ó?ÍÌÌÌÌÌÀffffff @ffffff)@ffffff Àš™™™™™ @333333 Àð?š™™™™™É?ÍÌÌÌÌÌ!@š™™™™™(@š™™™™™ À@ÍÌÌÌÌÌ0@š™™™™™ù¿ffffffö¿ffffff*ÀÍÌÌÌÌL3@ÍÌÌÌÌÌÀš™™™™™ À333333.@%@š™™™™™À5@š™™™™™@ffffffÀš™™™™™ù?333333)@@333333'@š™™™™™)@ffffff @š™™™™™(@@ffffff2@ffffff:@€3@333333 @33333³2@ÍÌÌÌÌÌÀÀš™™™™™!@ÍÌÌÌÌL6@333333@ÍÌÌÌÌÌ,@ÍÌÌÌÌÌÀÍÌÌÌÌÌ@@ÍÌÌÌÌÌÀ&@š™™™™™.@ÍÌÌÌÌÌ@333333ÀffffffÀš™™™™™¹?ÍÌÌÌÌÌì?š™™™™™#@ffffff)@š™™™™™?@Àffffff@š™™™™™À€0@333333<@@š™™™™™1@4@ffffff.@333333Àš™™™™™Àš™™™™™é?2@3333337@š™™™™™ @š™™™™™@š™™™™™@333333ó?š™™™™™ ÀÍÌÌÌÌŒD@)À33333³4@ÍÌÌÌÌÌ6@à¿fffffæ5@0@ffffff4@ÍÌÌÌÌÌÀš™™™™™@ÍÌÌÌÌÌ>@ÍÌÌÌÌÌ@333333Àš™™™™™@€1@ffffff@ffffff À%@Àš™™™™:À333333@š™™™™™ ÀÍÌÌÌÌÌ@ÀÍÌÌÌÌL9À(@š™™™™=@ffffff,@@.@ÍÌÌÌÌLB@š™™™™™/@ø¿333333,@ÍÌÌÌÌÌÀ€8À333333㿚™™™™™,@š™™™™™ @ffffff"À333333ó¿À333333#@ÍÌÌÌÌÌ!À333333û?š™™™™™@%@ÍÌÌÌÌÌ Àš™™™™™Àffffff @6@š™™™™™6Àš™™™™™#@333333+@š™™™™™@š™™™™™@ÍÌÌÌÌÌ1@ffffff!@ffffff!Àš™™™™™B@333333ó?333333,@ffffff@@ffffff @š™™™™™7@ÍÌÌÌÌÌÀ333333-@33333³7@ÍÌÌÌÌÌ&@Àš™™™™™ñ¿ø?š™™™™A@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ0@š™™™™™@333333-@š™™™™2@ffffff@à?š™™™™™'Àffffff À)Àš™™™™™@€5@&Àffffff@ÍÌÌÌÌÌÀ333333@š™™™™Ù\@ÍÌÌÌÌLF@ÍÌÌÌÌÌ@Àš™™™™™À.À333333À333333&@@ffffff(ÀC@333333@š™™™™™@333333*Àš™™™™™2@33333³7ÀÍÌÌÌÌÌ$@ÍÌÌÌÌÌÀš™™™™™ñ?š™™™™<@à?33333³4@&@3333333@à?ffffffÀÍÌÌÌÌÌÀffffffæ¿$Àffffff@333333û?ÍÌÌÌÌÌ@ÍÌÌÌÌÌü?ffffff'@ÍÌÌÌÌÌü¿ffffff!@š™™™™8@š™™™™™ @š™™™™™@ffffffþ¿š™™™™™@š™™™™™&Àfffffæ7Àffffff@ffffff"@ÍÌÌÌÌL1@ffffff4@ÍÌÌÌÌÌü?š™™™™™$@ÍÌÌÌÌÌÀ3@š™™™™™,Àffffff濚™™™™™Ù?ÍÌÌÌÌL1@ÍÌÌÌÌÌ@ÍÌÌÌÌÌÀš™™™™™ @ÍÌÌÌÌL0@333333(À€8@ffffff@š™™™™™!@€6@š™™™™™(@333333ó¿2@€0@ÍÌÌÌÌ EÀffffffö¿333333@333333 Àš™™™™™ÀffffffÀffffff+@ÍÌÌÌÌL6@ÀÍÌÌÌÌÌ#@š™™™™™!@$Àffffffö?333333@<@ffffff,@š™™™™™"Àš™™™™™Ù?ÍÌÌÌÌÌ.@š™™™™™@*@ffffff#À3333334@ÍÌÌÌÌÌ@,@€<@ÍÌÌÌÌÌ$@š™™™™™3@ÍÌÌÌÌL3@ffffffþ?@ÍÌÌÌÌÌ"@fffffæ2@ÍÌÌÌÌ G@ÍÌÌÌÌÌD@š™™™™™@š™™™™™@ffffff @@š™™™™6@3333331@fffffæ3@š™™™™™&@ÍÌÌÌÌÌü¿fffffæ<@333333 @ÍÌÌÌÌÌ&@ffffff5@@š™™™™™ @ffffff4@333333.@333333.@ÍÌÌÌÌÌ@ffffff%Àš™™™™™ ÀÍÌÌÌÌÌ-@ffffffÀ333333 ÀÍÌÌÌÌÌÀš™™™™™ù?ÍÌÌÌÌÌ@ffffff @ÍÌÌÌÌÌ@š™™™™™,@š™™™™™3@1@š™™™™™ñ?š™™™™9@@ÍÌÌÌÌÌ&ÀÍÌÌÌÌL4Àš™™™™™&@fffffæ2ÀÍÌÌÌÌÌ@333333@ð¿ffffff@š™™™™™ù?333333+Àffffffþ¿ÍÌÌÌÌÌ,ÀÍÌÌÌÌÌ@333333Àš™™™™™+@ÍÌÌÌÌÌ+@ÍÌÌÌÌÌ@š™™™™0@6@@ffffff@ÍÌÌÌÌL0@333333@ÍÌÌÌÌÌ%@š™™™™™$@š™™™™B@ffffff&ÀÀš™™™™™,@333333@š™™™™2@ÍÌÌÌÌÌ@ffffff-@š™™™™™'À333333@š™™™™™6@ÍÌÌÌÌÌ@š™™™™™¹¿š™™™™™¹¿ffffff@333333)@$@š™™™™5@ÍÌÌÌÌL4@š™™™™™.@@ffffff:@ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?ffffff!@.@333333@š™™™™™@š™™™™™A@ffffff.À!@š™™™™™É?8@ffffff@333333"À333333@333333"À333333û¿@Àffffff(ÀÍÌÌÌÌÌ-@ffffff"@333333@fffffæ3@ffffffÀš™™™™6@ÍÌÌÌÌÌô?333333@š™™™™™À,@7@333333"@ÍÌÌÌÌÌ@33333³2@š™™™™™%@%Àffffff(@€0@@7@š™™™™™é?ffffff*@333333㿚™™™™™ñ?š™™™™™'@š™™™™™2@@ffffff@333333*@ffffff6@333333@333333-@'À€0@Àš™™™™™@š™™™™™B@š™™™™™2@š™™™™™Ù¿š™™™™™@š™™™™™@ffffff@(@333333@ø?333333À0@ÍÌÌÌÌL6@š™™™™™0@ÍÌÌÌÌÌÀš™™™™>@333333&@*@333333ã¿,Àffffff/@ÍÌÌÌÌÌ*@ffffff;@š™™™™™)@/@ffffff6@%@š™™™™Ù@À333333À3333330@š™™™™™0@333333@33333³2ÀÍÌÌÌÌÌ@À%À333333@333333'Àffffff$@3@ffffffÀÍÌÌÌÌL2@š™™™™™ÀffffffÀÍÌÌÌÌÌ@ÍÌÌÌÌÌ4@ffffffþ?ÍÌÌÌÌÌ@ffffffÀø¿333333$@š™™™™™,@š™™™™=@š™™™™™À!@ÍÌÌÌÌÌ"@š™™™™™ñ¿ÍÌÌÌÌÌ Àš™™™™™/Àš™™™™™ @ffffffö¿333333&Àfffffæ0@:@333333ã?ffffff@š™™™™™6@ffffffÀ@ffffff-@333333@ffffff#@š™™™™™%@ffffff!@ffffff/@&@š™™™™™"@333333'@š™™™™™(@ffffff*@ffffff@ffffff!ÀÍÌÌÌÌÌÀ@333333:@!@ÍÌÌÌÌL;@À@@fffffæ;@ffffff:@š™™™™™,@ ÀÍÌÌÌÌÌÀ333333 ÀffffffÀffffff1@+@š™™™™YE@333333Ó?ffffff@ffffff@'@ÍÌÌÌÌ B@)@4@3333336@*@š™™™™™Ù¿š™™™™™¹?ÍÌÌÌÌÌ@š™™™™™:@fffffæ0@333333Àš™™™™™@333333@@ffffffö¿333333ã¿33333³1@ffffffö¿ÍÌÌÌÌL;@ffffff;@333333%@ÍÌÌÌÌ A@5@š™™™™™#@ÍÌÌÌÌÌô?ÍÌÌÌÌÌ@ffffff'@33333³B@š™™™™™(@ffffffÀÍÌÌÌÌÌ-@:@333333Àffffff@š™™™™1@333333Àš™™™™3À!@ÍÌÌÌÌÌÀš™™™™™ÀÀ1Àš™™™™™@ffffff/@š™™™™™)@333333"@ÍÌÌÌÌÌ&@3333338@fffff&F@ffffff0À33333³;@š™™™™™É?ÍÌÌÌÌL1ÀÍÌÌÌÌÌ@ffffffþ¿š™™™™™@š™™™™™Àš™™™™™É?à?333333#@333333)Àš™™™™™$@ÍÌÌÌÌÌ@š™™™™™(@@ffffffæ¿ffffff&@€5@ffffff4À"@š™™™™™'@š™™™™™@ÍÌÌÌÌÌô¿š™™™™™4@À333333,Àffffff;@ÍÌÌÌÌÌÀÍÌÌÌÌÌ'@ø¿š™™™™™@333333À3333339@ffffffÀ33333³3@ÍÌÌÌÌLB@333333@ÍÌÌÌÌÌì?š™™™™™ @ÍÌÌÌÌÌ$@.@ffffff@333333@š™™™™™ @333333$@(@š™™™™™"@š™™™™™!@333333@š™™™™™ À/ÀffffffÀš™™™™™"@€>@ÍÌÌÌÌÌÀffffff#@333333,À333333"@;@š™™™™Ù@@333333û¿ÍÌÌÌÌÌÀ@ffffff%Àš™™™™™ÀÍÌÌÌÌL0@ffffff"@ffffff!ÀÍÌÌÌÌÌ:@$@)@333333À333333,@ÍÌÌÌÌÌ-Àš™™™™0@)ÀffffffÀ€C@ffffffÀÍÌÌÌÌL;@ÍÌÌÌÌÌ$@333333;@ÍÌÌÌÌÌÀffffffö¿ÍÌÌÌÌÌÀ@ÍÌÌÌÌÌ@š™™™™™@ffffff@ffffffæ¿ÍÌÌÌÌÌ-@333333û?š™™™™™&@ÍÌÌÌÌÌ,À)@333333$@ @@333333$À333333ÀÍÌÌÌÌÌ@ffffff(@š™™™™™-@€=@ffffff@ffffff'@š™™™™™É¿333333)@À333333@333333@ÍÌÌÌÌÌ:@š™™™™™ù?ÍÌÌÌÌL0@ffffff ÀÍÌÌÌÌÌ@š™™™™5@-ÀÍÌÌÌÌ C@333333-@@š™™™™™É?š™™™™™Ù?š™™™™™¹¿333333ã?š™™™™™¹¿š™™™™™É¿š™™™™™É¿š™™™™™ñ?š™™™™™É¿š™™™™™Ù?š™™™™™¹?š™™™™™É¿š™™™™™¹¿š™™™™™Ù?š™™™™™¹¿š™™™™™Ù?à?š™™™™™É¿š™™™™™É?š™™™™™É¿š™™™™™É?š™™™™™É¿š™™™™™¹¿à?š™™™™™¹?š™™™™™É¿š™™™™™¹?š™™™™™Ù?š™™™™™É?š™™™™™¹¿š™™™™™Ù¿š™™™™™¹?š™™™™™é?š™™™™™¹?š™™™™™¹¿š™™™™™É?š™™™™™¹¿š™™™™™Ù?š™™™™™¹?š™™™™™¹¿à¿š™™™™™¹¿à?š™™™™™¹?š™™™™™É¿ffffffæ?š™™™™™É?š™™™™™¹?š™™™™™¹¿333333Ó¿š™™™™™¹¿š™™™™™Ù¿š™™™™™¹¿š™™™™™É?š™™™™™Ù?š™™™™™É?333333Ó¿š™™™™™¹¿333333㿚™™™™™¹?š™™™™™¹?ffffffæ?š™™™™™É¿š™™™™™Ù?š™™™™™¹?š™™™™™É?š™™™™™Ù?š™™™™™¹¿333333Ó¿š™™™™™¹¿š™™™™™É¿š™™™™™¹¿à?š™™™™™¹?š™™™™™ñ¿ÍÌÌÌÌÌô¿à?333333û¿š™™™™™¹?š™™™™™É¿333333ã?š™™™™™Ù?à¿333333Ó¿š™™™™™¹¿š™™™™™¹¿š™™™™™¹?š™™™™™¹¿š™™™™™É¿š™™™™™¹¿š™™™™™Ù?š™™™™™¹¿š™™™™™Ù?333333û¿š™™™™™Ù?š™™™™™¹¿š™™™™™¹¿ð?333333Ó¿š™™™™™Ù¿à¿333333㿚™™™™™É¿š™™™™™¹¿333333ã?š™™™™™¹?š™™™™™É¿š™™™™™Ù¿ffffffæ?š™™™™™¹?ÍÌÌÌÌÌì?333333ã?š™™™™™¹¿š™™™™™É?š™™™™™Ù¿ffffffö¿à¿š™™™™™Ù?š™™™™™¹¿ffffffæ?à?š™™™™™Ù¿š™™™™™¹?š™™™™™¹¿š™™™™™¹?à?333333Ó¿š™™™™™É¿à¿š™™™™™¹?333333Ó¿š™™™™™É?࿚™™™™™¹¿š™™™™™¹¿š™™™™™¹?š™™™™™Ù¿š™™™™™Ù?š™™™™™¹?333333Ó¿š™™™™™É¿š™™™™™¹¿š™™™™™¹?࿚™™™™™¹?š™™™™™¹?š™™™™™É?ÍÌÌÌÌÌô¿333333㿚™™™™™¹¿š™™™™™¹?333333Ó¿š™™™™™É?š™™™™™É?š™™™™™É¿333333Ó?333333Ó?š™™™™™¹?š™™™™™¹¿š™™™™™É¿333333ã?š™™™™™¹?š™™™™™Ù?š™™™™™É?š™™™™™Ù¿à¿š™™™™™Ù¿š™™™™™É¿ÍÌÌÌÌÌ쿚™™™™™É?š™™™™™É?š™™™™™É?࿚™™™™™¹?à?333333ã¿à¿ÍÌÌÌÌÌì¿333333Ó?š™™™™™¹¿ÍÌÌÌÌÌ쿚™™™™™¹?à¿333333Ó¿š™™™™™É?࿚™™™™™¹¿š™™™™™¹¿333333Ó?š™™™™™Ù¿š™™™™™É¿š™™™™™Ù?š™™™™™¹¿š™™™™™¹¿š™™™™™¹¿š™™™™™Ù?š™™™™™É?š™™™™™¹¿š™™™™™¹?š™™™™™¹?š™™™™™é¿š™™™™™Ù?333333㿚™™™™™É¿333333Ó¿š™™™™™Ù¿š™™™™™¹¿à?333333ã?š™™™™™¹¿333333ã?š™™™™™É?à?ffffffæ?333333Ó?ffffffæ?š™™™™™é?š™™™™™Ù?š™™™™™É¿š™™™™™¹?࿚™™™™™É¿333333ã?š™™™™™É¿333333Ó¿š™™™™™¹?š™™™™™É¿333333Ó?š™™™™™Ù?š™™™™™¹?š™™™™™¹?࿚™™™™™É¿š™™™™™Ù¿š™™™™™¹?š™™™™™É¿ffffffæ?333333ã?š™™™™™Ù?š™™™™™¹¿š™™™™™É?š™™™™™¹¿333333㿚™™™™™É?š™™™™™ñ¿š™™™™™Ù¿ÍÌÌÌÌÌì¿333333Ó?š™™™™™Ù?š™™™™™¹?š™™™™™¹?š™™™™™ñ¿333333ã?š™™™™™É?š™™™™™É¿ffffff濚™™™™™É?ffffffö¿à?š™™™™™¹?࿚™™™™™¹¿š™™™™™¹?š™™™™™¹?š™™™™™¹?š™™™™™¹?š™™™™™é?š™™™™™É?à?š™™™™™É¿333333Ó¿333333ã?š™™™™™Ù¿š™™™™™Ù?à?333333ó¿š™™™™™Ù¿š™™™™™Ù?š™™™™™É?š™™™™™Ù¿ffffffö?š™™™™™ñ?š™™™™™Ù¿333333Ó¿š™™™™™É?š™™™™™É¿š™™™™™Ù¿š™™™™™É¿333333ã?š™™™™™Ù?࿚™™™™™Ù?š™™™™™¹¿à¿333333㿚™™™™™É¿š™™™™™¹?à?333333Ó?ffffffæ?š™™™™™Ù¿š™™™™™Ù¿à¿š™™™™™¹¿š™™™™™¹¿à?š™™™™™¹¿š™™™™™É?à?š™™™™™é¿333333Ó¿š™™™™™É¿š™™™™™É¿š™™™™™¹¿š™™™™™Ù¿š™™™™™Ù?ffffffæ?š™™™™™¹?333333Ó¿š™™™™™Ù¿š™™™™™¹?à¿333333Ó?ÍÌÌÌÌÌ쿚™™™™™¹¿333333㿚™™™™™¹¿ffffffæ?ffffffö?š™™™™™É¿š™™™™™¹?š™™™™™É¿š™™™™™¹¿š™™™™™é¿š™™™™™¹?š™™™™™¹?ÍÌÌÌÌÌì?333333ã?333333㿚™™™™™é?ð?ffffffæ?š™™™™™É?š™™™™™Ù?š™™™™™É?š™™™™™¹¿š™™™™™é¿š™™™™™¹¿š™™™™™Ù¿333333ã?š™™™™™¹?ffffffæ¿333333Ó¿333333㿚™™™™™É?š™™™™™¹¿333333ã?š™™™™™¹¿@333333ã?ÍÌÌÌÌÌ쿚™™™™™É¿š™™™™™¹?š™™™™™é¿š™™™™™Ù¿š™™™™™Ù?š™™™™™¹?࿚™™™™™¹?333333Ó¿š™™™™™É¿š™™™™™¹¿ÍÌÌÌÌÌô¿š™™™™™Ù¿š™™™™™É?š™™™™™¹¿š™™™™™¹?š™™™™™É?š™™™™™¹?š™™™™™¹¿š™™™™™é?ffffffö?ð?š™™™™™¹?Àš™™™™™ù¿š™™™™™ù¿à¿333333Àffffffþ¿š™™™™™Ù¿À333333Àffffffö¿š™™™™™Ù¿ffffffæ¿333333Àš™™™™™é¿333333ó¿ð¿ffffffæ¿ð¿š™™™™™é¿333333ó¿ffffffæ¿333333ã¿ÍÌÌÌÌÌì¿ø¿333333Àš™™™™™ñ¿ÍÌÌÌÌÌ쿚™™™™™Ù¿ÍÌÌÌÌÌÀÍÌÌÌÌÌ쿚™™™™™é¿333333Àffffff Àffffff濚™™™™™ñ¿š™™™™™Ù¿333333ã¿ð¿š™™™™™ñ¿333333ã¿ffffffæ¿333333㿚™™™™™Ù¿š™™™™™Ù¿š™™™™™Ù¿333333Àš™™™™™Ù¿333333Àš™™™™™À333333ÀffffffÀffffffæ¿333333À333333ã¿333333ã¿ÍÌÌÌÌÌÀffffffæ¿ffffffæ¿ÍÌÌÌÌÌ쿚™™™™™ù¿333333ã¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌì¿333333 Àffffffæ¿Àffffffæ¿ÍÌÌÌÌÌì¿ffffffö¿ÍÌÌÌÌÌü¿ffffffö¿ffffff À333333Àš™™™™™Ù¿ÍÌÌÌÌÌü¿À333333ó¿š™™™™™ñ¿333333ó¿333333ó¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌô¿š™™™™™Ù¿à¿ffffffæ¿333333ã¿à¿ÍÌÌÌÌÌÀ333333ã¿333333ã¿333333㿚™™™™™Ù¿ÍÌÌÌÌÌì¿à¿333333ã¿ffffffö¿333333ó¿š™™™™™Ù¿333333ã¿333333Àffffffæ¿333333ã¿333333û¿š™™™™™Ù¿333333 Àð¿à¿333333ã¿333333Àš™™™™™Ù¿š™™™™™Ù¿à¿š™™™™™é?š™™™™™@š™™™™™@ffffff@fffffæ3@333333@333333@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ!@š™™™™™@ÍÌÌÌÌÌô?ÍÌÌÌÌÌ+@ÍÌÌÌÌÌ @333333@333333!@š™™™™™@š™™™™™ @333333ã?333333@ÍÌÌÌÌÌ@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ!@333333û?š™™™™™!@ffffff@ffffffæ?@ffffff@@333333 @ÍÌÌÌÌÌ@ffffff @š™™™™™@ffffffö?š™™™™™é?ÍÌÌÌÌÌ@š™™™™™@@š™™™™™*@š™™™™™ @333333û?333333@š™™™™™@š™™™™™Ù? @ffffff@333333@ÍÌÌÌÌÌ+@ÍÌÌÌÌÌ @ÍÌÌÌÌÌ@ffffff@333333@333333@333333!@š™™™™™@333333@š™™™™™@š™™™™™&@333333+@ffffff @333333@ÍÌÌÌÌÌ#@333333@š™™™™™Ù?ÍÌÌÌÌÌ @š™™™™™@@ffffffæ?ÍÌÌÌÌÌ@#@@@333333'@@@š™™™™™#@@š™™™™™@ffffff@ffffff@ÍÌÌÌÌÌ/@ÍÌÌÌÌÌ@ffffff@ÍÌÌÌÌÌ@ffffff@333333ó?š™™™™™é?ÍÌÌÌÌÌì?ffffff@333333@ffffff @333333@ÍÌÌÌÌÌ @š™™™™™@@ffffff)@ffffff"@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ffffff @ffffff @ÍÌÌÌÌÌ3@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@@ffffff$@ffffff@ffffff@ffffff@ÍÌÌÌÌÌô?ø?333333@333333@ÍÌÌÌÌÌü?à?ffffff@333333@š™™™™™@@333333@ffffffþ?ffffffþ?ÍÌÌÌÌÌ!@š™™™™™@ÍÌÌÌÌÌ@ffffff@333333@š™™™™™"@#@333333@ffffff$@ÍÌÌÌÌÌ@#@@ø?!@ffffff@ @ffffff#@š™™™™™@ffffff@@@ffffff@ffffffæ?š™™™™™ @ffffff@@@ÍÌÌÌÌÌ@@š™™™™™ñ?333333@333333@ffffff@333333@š™™™™™!@ÍÌÌÌÌÌ@š™™™™™Ù?@š™™™™™@ffffff@ð?ÍÌÌÌÌÌü?ÍÌÌÌÌÌ@š™™™™™@š™™™™™@š™™™™™ñ?333333@@š™™™™™@@š™™™™™@š™™™™™ @ffffff@š™™™™™é?333333ó?ffffff@ÍÌÌÌÌÌü?@ÍÌÌÌÌÌì?@š™™™™™Ù?š™™™™™@š™™™™™@ÍÌÌÌÌÌ@*@ffffff@€1@ffffff@ð?š™™™™™é?ffffffæ?š™™™™™@)@ffffff#@š™™™™™Ù?333333@š™™™™™,@@ÍÌÌÌÌÌ'@"@ÍÌÌÌÌÌü?/@@ÍÌÌÌÌÌ@ffffff@ø?ÍÌÌÌÌÌ@@š™™™™™ñ?333333û?ÍÌÌÌÌÌ@š™™™™™ñ?@333333 @š™™™™™@ÍÌÌÌÌÌ@š™™™™™@ffffff$@ffffff@š™™™™™@@$@333333-@ffffffö?333333@333333@@š™™™™™@š™™™™™@@ÍÌÌÌÌÌô?333333 @ÍÌÌÌÌÌ@ÍÌÌÌÌÌü?333333$@@333333ó?ffffff @ÍÌÌÌÌÌ @ÍÌÌÌÌÌü?ø?333333 @333333@š™™™™™@ÍÌÌÌÌÌ@ffffff@333333ó?ffffffþ?@ffffff@š™™™™™@ffffff@š™™™™™@ffffffæ?ffffff@ffffff @ÍÌÌÌÌÌ@š™™™™™ @ @š™™™™™@š™™™™™@š™™™™™@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?ffffffæ?333333ó?@ffffffþ?333333$@333333@š™™™™™@ø?(@ÍÌÌÌÌÌ@š™™™™™é?@š™™™™™,@à?ffffff @@š™™™™™ù? @š™™™™™Ù?ÍÌÌÌÌÌü?š™™™™™@š™™™™™@ffffff$@@ÍÌÌÌÌÌì?ffffff@š™™™™™@ÍÌÌÌÌÌ @@š™™™™™@@333333 @ÍÌÌÌÌÌ@333333@ffffff@333333û?ffffff@ÍÌÌÌÌÌ$@š™™™™™@à?ÍÌÌÌÌÌ @ð?š™™™™™Ù?333333@@ffffffæ?@š™™™™™&@ÍÌÌÌÌÌ@333333@ø?ffffff@ffffff"@š™™™™™ @@333333!@š™™™™™@333333@ffffff@333333@ffffff@@ÍÌÌÌÌÌô?ÍÌÌÌÌÌ@à?ffffff@š™™™™™@š™™™™™@š™™™™™@333333ã?@à?š™™™™™@ffffff@333333 @š™™™™™ @333333@ffffffþ?333333@š™™™™™é?333333@à?ffffff)@ÍÌÌÌÌÌ@ffffff@ffffff@š™™™™™@ffffff@š™™™™™ @š™™™™™@333333ã?š™™™™™ù?@333333@333333 @ÍÌÌÌÌÌ@š™™™™™#@@@ÍÌÌÌÌÌ&@ffffff@ffffff$@333333@ffffff @@š™™™™™@ffffff @*@š™™™™™@š™™™™™@š™™™™™ @š™™™™™@š™™™™™ñ?333333û?ffffff@ÍÌÌÌÌL0@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333@333333@333333@333333@333333@333333û?ÍÌÌÌÌÌ@ÍÌÌÌÌÌô?333333(@š™™™™™@š™™™™™!@ @333333-@333333@š™™™™™@š™™™™™ @ffffff@333333 @ÍÌÌÌÌÌ%@333333@š™™™™™@š™™™™™@333333@š™™™™™ @š™™™™™@š™™™™™@š™™™™0@š™™™™™ @š™™™™™ @ffffffæ?š™™™™™@š™™™™™@ffffff@333333@š™™™™™@ÍÌÌÌÌÌ@ @333333@333333@@@@333333@š™™™™™Ù?333333#@ffffffö?ffffff@ÍÌÌÌÌÌ@Àffffffæ¿ÍÌÌÌÌÌì¿333333ó¿à¿ffffffæ¿ffffffþ¿ø¿ffffffÀ333333ó¿333333ó¿333333ÀÍÌÌÌÌÌô¿333333ÀffffffÀÀ333333ó¿ÍÌÌÌÌÌ À333333ó¿ð¿š™™™™™#ÀÍÌÌÌÌÌì¿ÍÌÌÌÌÌÀÍÌÌÌÌÌì¿Àš™™™™™ Àš™™™™™ñ¿Àš™™™™™ÀffffffÀš™™™™™ù¿333333㿚™™™™™ À333333 À333333ÀÀš™™™™™ñ¿š™™™™™ñ¿ð¿333333 Àš™™™™™ÀÍÌÌÌÌÌÀš™™™™™Àffffffþ¿ffffffö¿ÍÌÌÌÌÌ À333333ó¿333333ÀÍÌÌÌÌÌô¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌÀ333333ã¿ÀffffffÀÍÌÌÌÌÌì¿ÍÌÌÌÌÌÀš™™™™™Àffffffþ¿ffffffÀffffffö¿333333À ÀÍÌÌÌÌÌô¿ÍÌÌÌÌÌ Àffffffþ¿ffffffÀš™™™™™Ù¿ffffffÀ𿚙™™™™é¿ÍÌÌÌÌÌÀ ÀÍÌÌÌÌÌÀffffff À333333ó¿ÍÌÌÌÌÌÀø¿à¿ÍÌÌÌÌÌÀÀ333333À333333ó¿š™™™™™ ÀÍÌÌÌÌÌü¿ÍÌÌÌÌÌì¿ffffffÀš™™™™™ ÀÀš™™™™™ Àø¿ffffffö¿ffffff Àš™™™™™ÀÍÌÌÌÌÌì¿333333ÀffffffÀffffff À333333ó¿ffffffÀš™™™™™À333333ã¿333333Àð¿333333ó¿ÍÌÌÌÌÌÀš™™™™™ Àð¿ÀffffffÀffffff Àš™™™™™Ù¿ÍÌÌÌÌÌô¿Àš™™™™™ñ¿ÍÌÌÌÌÌô¿ffffffæ¿ð¿š™™™™™Àø¿ffffffö¿ÀÀÀ333333ã¿333333ã¿ffffffþ¿š™™™™™ÀÍÌÌÌÌÌô¿š™™™™™é¿ð¿ð¿ffffffþ¿ffffffþ¿ffffff濚™™™™™ ÀffffffÀffffffÀ333333 ÀffffffÀffffffæ¿ÍÌÌÌÌÌ ÀffffffÀ333333ã¿333333ÀÍÌÌÌÌÌÀ333333À333333 Àø¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌÀš™™™™™ÀÀš™™™™™é¿š™™™™™ù¿333333㿚™™™™™ù¿ø¿š™™™™™é¿ÀÀÍÌÌÌÌÌü¿ffffff'ÀÀš™™™™™ÀÍÌÌÌÌÌô¿ÍÌÌÌÌÌü¿š™™™™™Àš™™™™™é¿ð¿333333û¿333333ó¿ÍÌÌÌÌÌÀ333333㿚™™™™™ÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀ333333ó¿333333$ÀÍÌÌÌÌÌÀš™™™™™é¿ffffffæ¿ffffffö¿ffffffö¿š™™™™™Ù¿ÍÌÌÌÌÌì¿333333ÀÍÌÌÌÌÌÀÀÍÌÌÌÌÌì¿333333 Àø¿333333ó¿ÍÌÌÌÌÌü¿ø¿ð¿ffffff Àš™™™™™Àš™™™™™ÀÍÌÌÌÌÌÀš™™™™™#À333333ó¿ffffffÀÍÌÌÌÌÌô¿ffffffþ¿à¿š™™™™™ù¿ Àš™™™™™ñ¿ffffffÀÀffffff%ÀÍÌÌÌÌÌÀš™™™™™Ù¿š™™™™™Ù¿ÍÌÌÌÌÌü¿333333À!À333333À333333ó¿ffffff ÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀš™™™™™ñ¿333333㿚™™™™™ÀÍÌÌÌÌÌ.Àà?333333"@š™™™™™é?ÍÌÌÌÌÌü?à?š™™™™™ù?š™™™™™Ù?ð?333333@ffffffæ?ÍÌÌÌÌÌô?à?ffffffæ?š™™™™™Ù?ÍÌÌÌÌÌì¿Àffffff Àð?š™™™™™É¿333333ã?333333Ó?ffffffö?ð?ffffffæ?š™™™™™É?š™™™™™ù?š™™™™™é?ÍÌÌÌÌÌô?š™™™™™é?ffffffæ?333333ã?à?š™™™™™é?ÍÌÌÌÌÌô?š™™™™™¹?ffffffæ?š™™™™™é?à?š™™™™™ñ?ffffffæ?333333ã?ffffffæ?š™™™™™Ù?333333ó?š™™™™™Ù?333333ã?333333Ó?š™™™™™é?ffffffæ?à?ffffffæ?š™™™™™ù?ffffffæ?ffffffæ?š™™™™™É¿š™™™™™é?ffffffæ?à?ÍÌÌÌÌÌì?ffffffö?ÍÌÌÌÌÌì?333333ã?š™™™™™ñ?ÍÌÌÌÌÌô?333333Ó?ffffffæ?ffffffö?ffffffæ?ffffffæ?ð?333333ã?333333ã?š™™™™™Ù?333333Ó?ÍÌÌÌÌÌì?š™™™™™ñ?333333ã?ø?333333ó?ffffffö?ffffffæ?š™™™™™¹¿ð?ÍÌÌÌÌÌì?š™™™™™É?333333ã?ÍÌÌÌÌÌì?ffffffæ?š™™™™™É?ffffffö?333333ã?ffffffæ?š™™™™™é?š™™™™™é?333333ã?ÍÌÌÌÌÌü?š™™™™™ñ?ð?š™™™™™ù?à?š™™™™™é?š™™™™™Ù?š™™™™™é?ÍÌÌÌÌÌì?ð?ffffffæ?à?ffffffæ?ð?333333ã?š™™™™™ù?333333Ó?ffffffæ?ffffffæ?333333Ó?ffffffæ?ffffffö?ffffffæ?š™™™™™ñ?š™™™™™é?333333ã?ffffffæ?š™™™™™É?ffffffæ?𿚙™™™™Ù?š™™™™™Ù?š™™™™™é?ð?š™™™™™¹?333333Ó?š™™™™™é?š™™™™™¹¿333333Ó?333333ã?ffffffæ?ffffffæ?ffffffæ?333333ã?ð?š™™™™™é?333333Ó?ffffffæ?ð?š™™™™™Ù?š™™™™™é?š™™™™™é?ð?ffffffæ?ø?š™™™™™ñ?š™™™™™é?ffffffæ?ð?333333ó?333333ã?ð?š™™™™™Ù?ð?š™™™™™é?š™™™™™¹?ÍÌÌÌÌÌì?ð?333333Ó?ø?š™™™™™ñ?š™™™™™Ù?à?ffffffæ?333333ã?333333Ó?š™™™™™é?š™™™™™ñ?à?š™™™™™Ù?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?š™™™™™é?333333ã?š™™™™™é?333333ã?333333Ó?333333û?à?š™™™™™é?š™™™™™Ù?ÍÌÌÌÌÌô?š™™™™™é?š™™™™™É?š™™™™™Ù¿š™™™™™Ù¿š™™™™™é?333333Ó?ffffffæ?š™™™™™Ù?š™™™™™ñ?ÍÌÌÌÌÌì?ffffffæ?333333ã?š™™™™™ñ?333333ã?ÍÌÌÌÌÌô?333333ã?ÍÌÌÌÌÌì?š™™™™™¹?š™™™™™¹¿š™™™™™ñ?ffffffæ?š™™™™™Ù?š™™™™™é?333333ã?ÍÌÌÌÌÌì?333333ã?š™™™™™@ffffffæ¿à¿š™™™™™É?333333ã?š™™™™™Ù?š™™™™™Ù?š™™™™™é?ffffffæ?333333㿚™™™™™Ù?ÍÌÌÌÌÌì?333333ó?š™™™™™ù?š™™™™™É?ø?š™™™™™ñ?š™™™™™É?ÍÌÌÌÌÌì?ð?333333ã?ð?à?š™™™™™é?à?š™™™™™ù?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?š™™™™™¹¿š™™™™™¹¿333333ó?š™™™™™ñ?š™™™™™é?š™™™™™é?ÍÌÌÌÌÌô?333333ã?333333ó?š™™™™™ñ?š™™™™™ñ?333333ó?ÍÌÌÌÌÌô?š™™™™™Ù?š™™™™™é?š™™™™™É¿š™™™™™É?ÍÌÌÌÌÌô?ð?ð?ð?ÍÌÌÌÌÌô?ð?333333ã?ffffffæ?š™™™™™Ù¿š™™™™™ñ?š™™™™™ñ?ÍÌÌÌÌÌì?š™™™™™Ù?ÍÌÌÌÌÌì?ffffffö?ø?ð?333333Ó?ffffffæ?š™™™™™¹?333333ó?š™™™™™ñ?à?š™™™™™é?š™™™™™¹¿333333Ó?š™™™™™É¿333333ã?š™™™™™Ù?š™™™™™Ù?š™™™™™é?333333ó?333333Ó?š™™™™™é?š™™™™™É¿š™™™™™É?š™™™™™é?333333Ó?š™™™™™Ù?333333Ó¿à?333333ã?333333ã?š™™™™™é?333333Ó?à?ø?ÍÌÌÌÌÌì?333333ã?š™™™™™é?ffffffæ?š™™™™™Ù?š™™™™™ñ?š™™™™™É?š™™™™™Ù?333333Ó?333333ã?š™™™™™ñ?ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?š™™™™™é?à?ÍÌÌÌÌÌì¿333333ó?š™™™™™¹¿š™™™™™é?à?ÍÌÌÌÌÌì?ð?ÍÌÌÌÌÌô?ffffffæ¿333333ó?ffffffæ?š™™™™™é?ÍÌÌÌÌÌì?333333ã?333333ã?š™™™™™é?ÍÌÌÌÌÌô?ffffffæ?š™™™™™Ù?ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?333333ã?š™™™™™é?š™™™™™Ù¿ffffff@ð?š™™™™™é?ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?à?š™™™™™é?š™™™™™ñ?š™™™™™é?š™™™™™Ù?š™™™™™ñ?ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?ð?ffffffæ?ÍÌÌÌÌÌü?333333ã?333333Ó¿š™™™™™Ù?š™™™™™É?@ÍÌÌÌÌÌô?à?à?š™™™™™Ù?ð?š™™™™™¹?333333Ó?š™™™™™é?à?333333Ó?333333Ó?ð?333333ó?333333Ó?ÍÌÌÌÌÌô?š™™™™™¹?ffffffæ?à?333333ã?à?š™™™™™¹?ð?333333ó?à?ð?ÍÌÌÌÌÌü?ÍÌÌÌÌÌì?š™™™™™Ù?š™™™™™Ù?ffffffæ?š™™™™™ñ¿333333ã?š™™™™™Ù¿à?ð?ffffffæ?š™™™™™é?ÍÌÌÌÌÌì?š™™™™™é?š™™™™™¹¿ÍÌÌÌÌÌì?š™™™™™é?ÍÌÌÌÌÌì?ffffffæ?š™™™™™Ù?š™™™™™Ù¿š™™™™™É¿333333ã?à?ø?š™™™™™é?ÍÌÌÌÌÌì?š™™™™™Ù¿ð?š™™™™™Ù?333333㿚™™™™™ñ?ð?à?333333û?333333Ó?š™™™™™é?š™™™™™é?š™™™™™ñ?333333ã?333333ó?š™™™™™é?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?333333ã?š™™™™™¹¿š™™™™™Ù?ffffffæ?ð?333333Ó?š™™™™™ñ?ffffffö?ÍÌÌÌÌÌô?ÍÌÌÌÌÌô?à?ÍÌÌÌÌÌì?333333ã?ffffffæ?333333ã?à?à?š™™™™™Ù?ð?à?à?ÍÌÌÌÌÌÀÍÌÌÌÌÌì¿ffffffæ?š™™™™™Ù?š™™™™™é?ffffffö?333333ã?333333Ó?ffffff @ffffffæ?333333Ó?ffffff@ffffffæ?ð?333333ó?š™™™™™Ù?š™™™™™ñ?ð?ffffffö?š™™™™™@š™™™™™Ù?š™™™™™Ù¿š™™™™™é?ÍÌÌÌÌÌü?ffffffö?333333ó?š™™™™™É?333333ã?@ø?ø?333333ã¿à?ffffffæ?à?ffffffö?š™™™™™ñ?ffffffæ?š™™™™™é?š™™™™™Ù?333333û?333333Ó?š™™™™™É¿š™™™™™É?š™™™™™É?š™™™™™ñ?333333ó?š™™™™™É?š™™™™™ñ?š™™™™™¹?333333Ó¿333333ó?333333Ó?333333ó?ffffffæ?333333Ó?ÍÌÌÌÌÌô?333333ã?ø?333333ã?š™™™™™¹?š™™™™™É¿333333ó?ffffffæ?333333ó?š™™™™™@ÍÌÌÌÌÌ@ffffffþ?à?š™™™™™¹?ÍÌÌÌÌÌü?š™™™™™é?š™™™™™ñ?333333ó?ð?ÍÌÌÌÌÌì?à?š™™™™™@ffffff @š™™™™™Ù¿à¿ÍÌÌÌÌÌü?š™™™™™Ù?š™™™™™¹?333333û?333333Ó?ð?ffffff@š™™™™™é¿ffffffæ?š™™™™™É?ø¿333333ó?š™™™™™Ù¿š™™™™™é?à?š™™™™™É?333333û?333333û?à?ffffff@333333㿚™™™™™Ù¿š™™™™™Ù?ffffffæ?š™™™™™ñ?š™™™™™@à?333333Ó?à?š™™™™™é?333333ó¿š™™™™™Ù?à?ð?ø?ffffff@333333ã?ð?š™™™™™é?ffffffæ?à?š™™™™™ñ?ÍÌÌÌÌÌô?ffffffæ?š™™™™™ù?ÍÌÌÌÌÌü?ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?š™™™™™é?ð?@333333ã?à?333333û?š™™™™™ù?ffffffö?ÍÌÌÌÌÌ@@à?ÍÌÌÌÌÌì?ffffffö?ð?ffffffö?ÍÌÌÌÌÌì?ÍÌÌÌÌÌ@š™™™™™Ù?ffffffö?š™™™™™Ù?à?ffffffæ?ÍÌÌÌÌÌü?ffffffæ?ÍÌÌÌÌÌì?š™™™™™É?ÍÌÌÌÌÌô?š™™™™™ñ?333333Ó?à?333333ó?ÍÌÌÌÌÌô?ø?š™™™™™Ù¿š™™™™™@ø?ÍÌÌÌÌÌü?à?333333ó?ÍÌÌÌÌÌ@333333ã?š™™™™™ñ?š™™™™™é?ÍÌÌÌÌÌì?@333333ó?ÍÌÌÌÌÌô?333333Ó?ffffffæ?ffffffæ?š™™™™™é?š™™™™™É¿š™™™™™Ù¿ffffff@333333ã?ð¿à?ÍÌÌÌÌÌì?à¿333333ã?à¿ø?š™™™™™é?333333Ó?ð?š™™™™™é¿333333ã?š™™™™™ñ?š™™™™™ù?333333ã?ÀÍÌÌÌÌÌô?š™™™™™¹¿š™™™™™é?333333Ó?ø?ffffff @@š™™™™™ñ?ÍÌÌÌÌÌì?ð?ffffff@@š™™™™™@š™™™™™ù¿ffffffæ¿ÍÌÌÌÌÌô?333333Ó?š™™™™™ñ¿š™™™™™ñ?š™™™™™Ù¿ffffffö?à?ÍÌÌÌÌÌô?@333333ó?š™™™™™É?š™™™™™Ù?ffffffæ?š™™™™™Ù?ÍÌÌÌÌÌô?š™™™™™Ù?ffffffæ?333333@333333ã?ÍÌÌÌÌÌ@333333ã?ffffffö?333333ã?à?333333Ó?ÍÌÌÌÌÌì?š™™™™™¹?333333Ó?š™™™™™é¿ø?š™™™™™é¿333333ó?ffffffö?ffffffæ?š™™™™™Ù?ø?š™™™™™É¿333333ã?333333ó?@ø?š™™™™™é¿ÍÌÌÌÌÌì?š™™™™™Ù?ÍÌÌÌÌÌì?333333Ó?ÍÌÌÌÌÌü?ÍÌÌÌÌÌì?ffffffæ?ÍÌÌÌÌÌì?š™™™™™Ù?333333ã?à?š™™™™™Ù?ÍÌÌÌÌÌì?š™™™™™ñ?š™™™™™@333333@š™™™™™Ù?࿚™™™™™É?ffffffæ?333333Ó?333333ó?333333ã?ÍÌÌÌÌÌô?š™™™™™é?š™™™™™Ù?š™™™™™Ù?333333ó?š™™™™™é?š™™™™™é?ÍÌÌÌÌÌô?š™™™™™Ù¿š™™™™™¹¿333333ã?ffffff @à?ÍÌÌÌÌÌô¿š™™™™™é¿š™™™™™é?š™™™™™É¿333333û?à?333333ó?ÍÌÌÌÌÌô¿à?à?ffffffþ?ÍÌÌÌÌÌì¿ffffffö?333333Ó?333333Ó?ffffffö¿š™™™™™@ÍÌÌÌÌÌô?ffffffæ¿ffffff@š™™™™™É?š™™™™™é?ÍÌÌÌÌÌì?ffffffæ?ÍÌÌÌÌÌô?ÍÌÌÌÌÌü?ÍÌÌÌÌÌô?333333Ó?š™™™™™Ù?333333ã?ÍÌÌÌÌÌì¿@à?ÍÌÌÌÌÌô?333333û?š™™™™™É?333333ã?š™™™™™É?333333Ó?ÍÌÌÌÌÌô?š™™™™™Ù?ÍÌÌÌÌÌ@ø?š™™™™™Ù?333333ó?333333@š™™™™™É?ø?333333Ó?ffffffæ?ffffffæ?š™™™™™ù?š™™™™™ñ?333333ã?333333ó?ffffffæ?š™™™™™é?š™™™™™ù?š™™™™™É¿ffffffö?š™™™™™Ù?ð?ffffffö?@š™™™™™é?ÍÌÌÌÌÌ@š™™™™™Ù¿à¿ffffff@à?ffffffþ?š™™™™™é?š™™™™™Ù?333333Ó?333333ã?à?333333ã¿ÍÌÌÌÌÌô?333333Ó?š™™™™™Ù?@ffffff@333333Ó?333333û?š™™™™™é?ffffffþ?š™™™™™é?š™™™™™É?ø¿ffffffæ¿à¿š™™™™™@ffffffæ¿@š™™™™™Ù?ÍÌÌÌÌÌô?ffffffæ?š™™™™™ù?ÍÌÌÌÌÌ@333333Ó?ÍÌÌÌÌÌô?333333ã?ÍÌÌÌÌÌô¿ø?ffffffö?ffffff@à?333333ó?ffffffþ?333333ã?ÍÌÌÌÌÌô?ø?@ÍÌÌÌÌÌô?ð?333333ã?š™™™™™Ù¿š™™™™™Ù?333333ã¿333333ã¿ð?ð?࿚™™™™™ù¿š™™™™™É?ffffffæ?333333@333333ó¿š™™™™™É¿@š™™™™™ù?@ð?ÍÌÌÌÌÌì?š™™™™™Ù?š™™™™™Ù?ffffffö?333333ó¿ÍÌÌÌÌÌì?ffffff@333333ã?š™™™™™é?à?@333333ó?š™™™™™¹¿š™™™™™É?ø?š™™™™™Ù?333333Ó¿333333Ó?333333ó?š™™™™™ù?ffffffö?š™™™™™¹?ffffffþ?ÍÌÌÌÌÌì¿ÍÌÌÌÌÌì?ÍÌÌÌÌÌü?333333Àffffff@ÍÌÌÌÌL?@š™™™™™#@ À333333D@ffffff,@ffffff-@ÍÌÌÌÌÌ@33333³:@ffffff2@ffffffæ?ÍÌÌÌÌÌ$À€;@ÍÌÌÌÌÌ@@ÍÌÌÌÌÌ@fffffæ3@%@š™™™™™%@ffffffÀš™™™™™&@€2@š™™™™™5@š™™™™™Ù?š™™™™™5@33333³3À33333³4@333333 @ÍÌÌÌÌÌÀ3333338À3333331Àø?#@ÍÌÌÌÌÌ @ÍÌÌÌÌÌ@ÍÌÌÌÌL3@ÍÌÌÌÌÌ#@&@š™™™™™Ù?fffff&AÀÍÌÌÌÌÌ"@š™™™™™/@ÍÌÌÌÌÌ3@333333?@ÍÌÌÌÌÌÀÍÌÌÌÌÌÀ @ @,@š™™™™™ÀÍÌÌÌÌÌ@š™™™™™@fffff¦B@333333+@333333+@š™™™™™(@333333û¿&@ÍÌÌÌÌ B@333333,@ÍÌÌÌÌÌ @333333-@š™™™™™7@š™™™™YE@š™™™™™@ÍÌÌÌÌÌ@33333³<@333333:@333333ÀÍÌÌÌÌÌ,@333333!@333333*@333333'Àš™™™™8@ÍÌÌÌÌÌ,@ffffff0@333333@33333³?@ÍÌÌÌÌÌ6@ffffff7@1@ffffff$@333333À333333?@š™™™™™4@333333E@333333/@š™™™™™3@š™™™™™*@@ffffffö¿š™™™™™ñ?333333Àð¿333333@!@ÍÌÌÌÌÌ$@333333@,@ÍÌÌÌÌÌ쿚™™™™YF@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ4@ÍÌÌÌÌÌ/@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ffffff&@ÍÌÌÌÌÌB@ffffff$@ffffffÀš™™™™™@2@ffffff9@€4@š™™™™™é?,@333333ÀÍÌÌÌÌÌÀ3@š™™™™™@333333Ó?ffffff2À333333û?ÍÌÌÌÌÌ @š™™™™™0@333333ó¿!@333333@ffffffÀ33333³:@333333+@ffffff2@š™™™™1@333333&@š™™™™YE@fffffæ;@ÍÌÌÌÌÌ@€8@333333%@ÍÌÌÌÌ B@ffffff"@Àš™™™™™>@ÍÌÌÌÌÌì?à?3333334@ÍÌÌÌÌÌ@333333@&@"@333333@ffffffÀffffff@ffffff3@@š™™™™™3@ÍÌÌÌÌL6@5@š™™™™™À3333335@ffffffÀ3@ÍÌÌÌÌÌ@š™™™™™8@š™™™™™-@ffffff"Àð¿333333)@ffffff@ð?33333³0ÀÍÌÌÌÌÌ@*@š™™™™™0@ffffffö¿š™™™™Y@@5@ÍÌÌÌÌÌÀ333333ó¿:@š™™™™™@ÍÌÌÌÌÌ@š™™™™™%@ÍÌÌÌÌÌ%ÀÍÌÌÌÌÌÀÍÌÌÌÌL3@@ÍÌÌÌÌÌ#@=À333333 À33333³8@ÍÌÌÌÌÌ%Àš™™™™1@333333#@ÍÌÌÌÌÌ @€D@ÍÌÌÌÌÌÀš™™™™E@33333³7@à?š™™™™™ñ?ÍÌÌÌÌÌÀÍÌÌÌÌÌ)@€C@33333sA@š™™™™™À333333(@33333³I@ÍÌÌÌÌÌ4@š™™™™™@@333333@ÍÌÌÌÌ P@@ø?333333/@@à?33333³4@ÍÌÌÌÌÌ@ffffffÀš™™™™™:@š™™™™™@333333%@ffffff,@š™™™™™&@333333@ð?fffffæA@š™™™™™(@@333333.@E@D@ÍÌÌÌÌÌô?3333339@ÍÌÌÌÌÌ#@ffffff@4@ffffff,@š™™™™™É?333333@š™™™™™%@3333332@ÍÌÌÌÌÌ À333333<@333333#@ffffff Àš™™™™™ñ¿ÍÌÌÌÌÌÀffffff@ffffff4Àš™™™™1À*ÀÍÌÌÌÌÌ@ø?ffffff$@š™™™™™-@ffffff1@ffffff À𿚙™™™™)@ffffff2@333333/@@š™™™™™1@ÍÌÌÌÌÌÀÍÌÌÌÌÌ ÀÍÌÌÌÌÌ@ffffff:@ffffff1@ÍÌÌÌÌÌ$@š™™™™™@333333@š™™™™™ù?@3333335@333333/@ÍÌÌÌÌÌ5@333333ÀÍÌÌÌÌÌ(Àš™™™™™ñ?ffffffÀÍÌÌÌÌÌ À&Àš™™™™=@3333331@ÍÌÌÌÌÌ@š™™™™™À3333331@333333@š™™™™1Àš™™™™1@ÍÌÌÌÌ F@ÍÌÌÌÌÌÀ333333@š™™™™™@š™™™™™"@333333ó¿Àš™™™™™.ÀÍÌÌÌÌÌ@š™™™™<@333333@ÍÌÌÌÌÌì?ÍÌÌÌÌÌ3@ÍÌÌÌÌL2@ @ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@š™™™™™À@33333³0@333333>@ffffff"@fffffæ1@ÍÌÌÌÌÌ@333333@3333334@ÍÌÌÌÌÌô¿@ÍÌÌÌÌÌ2À$@ffffffö¿ÍÌÌÌÌL@Àš™™™™™$@š™™™™™.@ø?ÍÌÌÌÌL4@ÍÌÌÌÌ L@3@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333(@š™™™™™6@33333³0@333333@€3@ÍÌÌÌÌÌ@333333 @ffffff@ÍÌÌÌÌL0@ÍÌÌÌÌL0@š™™™™1@ffffffÀ333333=@š™™™™™É¿š™™™™™@ÍÌÌÌÌÌ@3333333@$À$Àffffff:@ÍÌÌÌÌÌ@À€4@ÍÌÌÌÌÌì¿9@333333<@ffffff-@@(@ffffffÀš™™™™™@š™™™™™Àffffff?@ÍÌÌÌÌÌ@š™™™™™'@333333 @ÍÌÌÌÌÌ@š™™™™™/@ð¿ÍÌÌÌÌÌ)@š™™™™™É¿ffffff7Àš™™™™™@ÍÌÌÌÌÌ3@"@š™™™™™@ÍÌÌÌÌL;@ffffff7@ÍÌÌÌÌÌ@ÍÌÌÌÌL>@ffffff>@€0@fffffæ0@33333óAÀ33333³3Àffffff@3333339@ffffff6@33333³H@š™™™™™*@-@ffffff$@333333(@ÍÌÌÌÌÌ=Àš™™™™™@ÍÌÌÌÌL0@€<@ @À(@ÍÌÌÌÌL7@.@š™™™™™1@ffffff,@333333 @š™™™™™'@ÍÌÌÌÌÌü¿ÍÌÌÌÌL=@fffffæ9@š™™™™™A@3333335@fffffæ=@š™™™™™Ù¿š™™™™™@333333 @333333/@ÍÌÌÌÌÌ)@ÍÌÌÌÌL7@ffffffÀš™™™™™,À3333331@333333#@333333À@€7@333333-@)@333333Àš™™™™™3À333333#Àš™™™™™1@ÍÌÌÌÌÌ!@33333³;@333333û?ÍÌÌÌÌŒ@@ffffff6@3@š™™™™™"À333333@333333@(@š™™™™™)@ÍÌÌÌÌÌ'À333333B@FÀÍÌÌÌÌÌ4@ÍÌÌÌÌÌ%@š™™™™™Àffffff'@š™™™™YE@333333)@ffffffþ¿š™™™™YP@š™™™™1@ÍÌÌÌÌÌ0@ÍÌÌÌÌÌ6@333333>@3333339@%@Àfffff&A@33333³D@€1@ÍÌÌÌÌ B@333333)@ffffff @ð¿33333³:@š™™™™™:@ÍÌÌÌÌÌ<@333333ó?33333³8@ffffffæ?333333?@333333.@fffffæ2Àffffff0Àš™™™™™*Àš™™™™™@ffffff4@333333%@ÍÌÌÌÌÌ#@€8@š™™™™™(@ÍÌÌÌÌL:@@š™™™™™AÀ333333%@3333331@33333³9@33333óB@333333ÀÍÌÌÌÌÌì?333333@ffffff@š™™™™™5@Àš™™™™™@ffffff$@ÍÌÌÌÌÌ@š™™™™™E@ÍÌÌÌÌL1@33333³8@ÍÌÌÌÌÌ-@333333ó¿š™™™™™#@š™™™™™F@š™™™™3@0@>@š™™™™D@L@#@ÍÌÌÌÌÌ@fffffæC@ÍÌÌÌÌÌ>@ÍÌÌÌÌÌì?ÍÌÌÌÌÌ6@.@š™™™™2@333333 ÀffffffD@ÍÌÌÌÌ D@*@ø?@E@€8@ffffff8@333333;@ffffff'@ÍÌÌÌÌÌ@ÍÌÌÌÌLH@333333+@€G@33333³1@š™™™™™É¿33333³;@ÍÌÌÌÌÌü?š™™™™™@ÍÌÌÌÌÌ@š™™™™™é¿ffffff$@33333³1@ÍÌÌÌÌÌ%@ÍÌÌÌÌL<@ÍÌÌÌÌÌô?333333%@@ÍÌÌÌÌLH@333333@33333³E@fffffæ2@333333@@0@8@.@333333 ÀÍÌÌÌÌÌ&@š™™™™™;@333333F@ÍÌÌÌÌL7@ @33333³3@ÍÌÌÌÌÌì¿ð?ÍÌÌÌÌÌ9@ffffff,@333333@ffffffÀffffff*@ÍÌÌÌÌÌ@333333;@333333@ÍÌÌÌÌL0@ffffff$@š™™™™™@33333sH@1@ffffff5@ÀB@fffffæ5@33333óI@š™™™™F@š™™™™1@<@33333³0@fffff¦F@š™™™™™4@ÍÌÌÌÌÌ@@B@ffffff0@ÍÌÌÌÌÌ @>@333333"@ÍÌÌÌÌÌ#@ffffff/@ÍÌÌÌÌL6@"@š™™™™™¹?!@€<@š™™™™™+@ÍÌÌÌÌÌ5@fffffæ9@ÍÌÌÌÌL?@ÍÌÌÌÌÌ@33333s@@À33333sF@.@ffffffB@ÍÌÌÌÌL1@À3333332@š™™™™™.@ffffff'@š™™™™™@#À3333337@š™™™™™4@ÍÌÌÌÌÌ7@š™™™™™¹?33333óA@€8@š™™™™™ù¿Àš™™™™™7@€?@ÍÌÌÌÌÌ&@333333@ffffffÀffffffþ?0@333333&@ffffff@ffffff%Àš™™™™™ Àffffff:@š™™™™™ À@333333(@333333,@33333óH@333333ã?À@@@š™™™™™Ù¿ÍÌÌÌÌÌ@ffffff ÀÍÌÌÌÌL8@š™™™™YN@š™™™™ÙH@@ÍÌÌÌÌÌ8@33333³M@€K@fffffFQ@fffffæ9@333333@š™™™™™H@š™™™™™é¿š™™™™™$@š™™™™2@ÀÍÌÌÌÌÌ @33333³1@'@333333û¿333333A@3333334@ÍÌÌÌÌÌ1@3333330@ÍÌÌÌÌÌ-@š™™™™™!@333333 @ÍÌÌÌÌ F@-@ffffff#@fffff&C@@F@ffffffO@ffffff!@333333A@ffffff,@333333'@ffffff6@3333333@ÍÌÌÌÌÌô?ffffff@ffffff@š™™™™B@(À€B@3333332@ÍÌÌÌÌÌì?ffffffæ?ffffffþ?ÍÌÌÌÌÌü?ÍÌÌÌÌÌ*Àš™™™™™Àš™™™™™É¿ÍÌÌÌÌL1@333333Àffffff.@ÍÌÌÌÌÌ2@š™™™™™6@ffffffö¿š™™™™™%@4@€7@š™™™™4@ÍÌÌÌÌÌ!@€5@š™™™™™É?333333Àffffff"@š™™™™Y@@€C@333333;@š™™™™™'@š™™™™™ù?@333333(@€6@33333³7@3333339@@ÍÌÌÌÌÌÀffffff @333333Àffffffæ?š™™™™™À33333³@@ÍÌÌÌÌL:@ffffff@à¿ÍÌÌÌÌÌ@š™™™™E@š™™™™™#@fffffæ:Àš™™™™™@š™™™™ÙH@ÍÌÌÌÌÌÀ333333-@ÍÌÌÌÌÌ @'@333333Ó¿š™™™™™@À333333Àš™™™™™ù?š™™™™™D@š™™™™™%@333333@333333@33333³A@š™™™™™2@ÍÌÌÌÌÌ @3333337@ÍÌÌÌÌÌì¿ffffff"@š™™™™6@ÍÌÌÌÌ A@ÍÌÌÌÌÌ/@ffffff=@+@š™™™™™@ÍÌÌÌÌÌ6@333333@š™™™™™ÀffffffÀffffff(@š™™™™™@ÍÌÌÌÌL0À333333,@š™™™™8@@ÍÌÌÌÌL5@33333óO@€5@J@ffffff=@š™™™™™,@š™™™™<@33333³A@š™™™™™@33333³:@š™™™™™"@(@ÍÌÌÌÌÌ%@ffffff<@fffffæ6@5@333333@ÍÌÌÌÌ @@š™™™™™@.@ffffff@š™™™™™;@ ÀÀ33333óA@333333*ÀÍÌÌÌÌL8@.@6@33333³:@ÍÌÌÌÌL@@ffffff@ÍÌÌÌÌL9@ffffff@š™™™™™#@ÀfffffæC@ffffff&@ @š™™™™™6@ÍÌÌÌÌÌ@3333331@ffffff6@ÍÌÌÌÌLC@š™™™™™@š™™™™™#À@š™™™™Y@@ffffff.@ÍÌÌÌÌÌ$@333333,@ÍÌÌÌÌÌ2@š™™™™™@š™™™™ÙH@š™™™™<@ÍÌÌÌÌL=@€3@ÍÌÌÌÌÌ2Àš™™™™™,À33333³2@ÍÌÌÌÌLI@ÍÌÌÌÌL8@ÍÌÌÌÌÌK@€0@ÍÌÌÌÌÌÀ3333336@3333334@ÍÌÌÌÌÌ&ÀÍÌÌÌÌÌ@€7@fffff¦E@š™™™™™(@š™™™™™é¿ffffff7@ÍÌÌÌÌ G@ÍÌÌÌÌÌ6@š™™™™™8@2@ð?ÍÌÌÌÌÌ*@Àš™™™™8@ÍÌÌÌÌ @@š™™™™D@ffffff0@š™™™™™*@ffffffæ?ÍÌÌÌÌÌ#@€6@ÍÌÌÌÌÌ @ffffff'@š™™™™ÙA@ÍÌÌÌÌÌ'@š™™™™YO@š™™™™™8@333333,@ÍÌÌÌÌÌÀš™™™™™"@333333@@ffffff@3333332@+@3333330À333333ÀÍÌÌÌÌÌ3@fffffæ6@33333sA@333333Ó?fffff&A@fffff&@@33333³4@ffffff%Àš™™™™™@333333-@ÍÌÌÌÌÌ6@ÍÌÌÌÌL6@š™™™™™&Àfffff¦H@33333³HÀfffffæ:@6@š™™™™™¹?š™™™™™Ù?š™™™™™é¿š™™™™™ù?š™™™™™Ù?ÍÌÌÌÌÌô?ffffffæ?à?š™™™™™¹?ffffffæ?š™™™™™¹?333333ã?š™™™™™é?333333ã?333333ó?ÍÌÌÌÌÌô?333333ã?ffffffö?š™™™™™Ù?ffffffæ?š™™™™™Ù?à?š™™™™™Ù?333333@à?ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?š™™™™™ñ?ð?š™™™™™¹?333333ã?š™™™™™ñ?š™™™™™é?à?ffffffæ?ÍÌÌÌÌÌì?ffffffæ?ð?333333Ó?š™™™™™¹?333333ã?à?š™™™™™É?š™™™™™Ù?š™™™™™É?š™™™™™¹?š™™™™™É?333333ó?š™™™™™Ù?š™™™™™Ù?ÍÌÌÌÌÌì?š™™™™™É?ÍÌÌÌÌÌì?ffffffæ?š™™™™™ñ?ÍÌÌÌÌÌì?š™™™™™é?š™™™™™Ù?333333Ó?š™™™™™É?333333ã?š™™™™™Ù?à?š™™™™™¹?ffffffæ?333333ã?š™™™™™Ù?š™™™™™¹?š™™™™™Ù?333333ã?333333ã?333333ã?š™™™™™Ù?ffffffæ?š™™™™™¹?š™™™™™É?à?ÍÌÌÌÌÌì?ð?à?š™™™™™é?š™™™™™é?333333ó?@ÍÌÌÌÌÌü?333333ó?š™™™™™@@333333Ó?š™™™™™¹?š™™™™™¹?š™™™™™¹?333333Ó?š™™™™™¹?333333ã?à?š™™™™™Ù?333333ã?ffffffæ?š™™™™™Ù?š™™™™™ñ?à?š™™™™™É¿ffffffö?š™™™™™É?š™™™™™Ù?š™™™™™Ù?ffffffæ?ÍÌÌÌÌÌô?š™™™™™¹?ð?333333ã?ffffffæ?š™™™™™ñ?à?333333ã?š™™™™™É?ð?š™™™™™É?š™™™™™É?ø?š™™™™™¹?ÍÌÌÌÌÌì?à?333333ã?š™™™™™Ù?š™™™™™¹?ÍÌÌÌÌÌì?à?ffffffæ?š™™™™™@š™™™™™¹?à?ð?ð¿ffffffæ?š™™™™™Ù?š™™™™™¹?ffffffæ?ffffffæ?à?š™™™™™¹?š™™™™™¹?333333ã?š™™™™™¹¿à?333333ã?š™™™™™Ù?š™™™™™¹?à?š™™™™™Ù?ÍÌÌÌÌÌ쿚™™™™™¹?ffffffæ?ð?à?š™™™™™É¿š™™™™™Ù?à?š™™™™™É?š™™™™™ñ?š™™™™™¹?š™™™™™ù?ffffffæ?ÍÌÌÌÌÌì?333333ó?ffffffæ?333333ó?š™™™™™Ù?š™™™™™é?ÍÌÌÌÌÌô?š™™™™™Ù?š™™™™™Ù?ffffffö?ÍÌÌÌÌÌô?à?š™™™™™é?š™™™™™¹¿333333ã?š™™™™™é?ð?ffffff@ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?ffffffæ?333333ã?ð?333333ã?š™™™™™¹?ÍÌÌÌÌÌì?š™™™™™é?š™™™™™¹?š™™™™™¹?ø?š™™™™™¹?ffffffþ?à?š™™™™™¹?@ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?333333ó¿333333ó?ÍÌÌÌÌÌü?š™™™™™Ù¿š™™™™™¹?ÍÌÌÌÌÌì?à?š™™™™™Ù?ffffffæ?à?š™™™™™É?š™™™™™¹¿š™™™™™Ù?š™™™™™¹?š™™™™™é?š™™™™™ñ?à?333333ã?ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?à?ffffffö?š™™™™™ñ?ffffffö?333333ã?ÍÌÌÌÌÌô?š™™™™™É?š™™™™™¹?ÍÌÌÌÌÌü?š™™™™™É?š™™™™™É?333333ã?ÍÌÌÌÌÌì?ø?ffffffæ?š™™™™™Ù?š™™™™™Ù?š™™™™™É?š™™™™™é?ÍÌÌÌÌÌì?š™™™™™ñ?š™™™™™É?ffffffæ?333333ó?ð?333333ã?ffffffö?ð?š™™™™™É?ffffffæ?ÍÌÌÌÌÌì?ffffffæ?š™™™™™Ù?š™™™™™é?š™™™™™Ù?š™™™™™¹¿š™™™™™¹?š™™™™™¹?š™™™™™¹?š™™™™™¹¿à?š™™™™™Ù?ð?š™™™™™É¿ffffffæ?ÍÌÌÌÌÌì?š™™™™™é?ø?š™™™™™é?333333ã?š™™™™™ñ?š™™™™™É¿ÍÌÌÌÌÌì?333333ã?š™™™™™¹?š™™™™™Ù?333333ó?333333û?š™™™™™Ù?333333ã¿333333ã?ÍÌÌÌÌÌô?š™™™™™ñ?š™™™™™¹¿š™™™™™¹¿333333û?š™™™™™Ù¿š™™™™™Ù?ð?ÍÌÌÌÌÌì?à?š™™™™™é?š™™™™™É¿š™™™™™ù?š™™™™™é?à?š™™™™™é?š™™™™™¹?ÍÌÌÌÌÌÀš™™™™™¹?ÍÌÌÌÌÌì?333333û¿š™™™™™Ù?333333ã?à?333333ó?ø?ÍÌÌÌÌÌì?@à?š™™™™™Ù?à?ÍÌÌÌÌÌô?š™™™™™ñ?à?š™™™™™É?š™™™™™¹?š™™™™™¹?š™™™™™ñ?333333ã?š™™™™™¹?š™™™™™Ù¿š™™™™™Ù?ð?333333û?ÍÌÌÌÌÌì?à?š™™™™™¹?ÍÌÌÌÌÌì?š™™™™™É?333333Ó?333333ã?š™™™™™Ù?333333Ó?ø?š™™™™™ñ?ÍÌÌÌÌÌì?à?333333û?š™™™™™ñ?@ð?š™™™™™ñ?š™™™™™é?š™™™™™É?š™™™™™ñ?š™™™™™é?š™™™™™É?ð?ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?ÍÌÌÌÌÌ@š™™™™™É?š™™™™™é?333333@à?š™™™™™Ù?š™™™™™¹?à?š™™™™™¹?š™™™™™Ù?ffffffö¿š™™™™™Ù?à?ffffffæ?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?333333ã?ÍÌÌÌÌÌì?š™™™™™0@à?š™™™™™¹?ffffffæ?š™™™™™¹¿ð?ð?ffffff@š™™™™™é?š™™™™™¹?ÍÌÌÌÌÌü?š™™™™™¹?333333ã?š™™™™™¹?ffffffæ?Àš™™™™™É¿š™™™™™¹?à?š™™™™™@š™™™™™Ù?š™™™™™É?333333ó?ø?š™™™™™é?š™™™™™¹¿š™™™™™ñ?ð?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?à?š™™™™™é?333333Ó?333333ã?š™™™™™Ù?@ffffffö?š™™™™™Ù?š™™™™™¹?ffffffæ?333333ó?ffffffö¿ffffffæ?࿚™™™™™¹?ffffffþ?š™™™™™é?à?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?333333ã?333333ã?333333Ó?333333ã?333333Ó?ffffffæ?š™™™™™Ù?š™™™™™É?333333ã?ð?ÍÌÌÌÌÌü¿ð?š™™™™™Ù?ÀÀÍÌÌÌÌÌÀÍÌÌÌÌÌ$À333333"ÀÍÌÌÌÌÌÀÍÌÌÌÌL2À333333À333333Àš™™™™™ À333333ÀÍÌÌÌÌÌÀffffffÀÍÌÌÌÌÌÀ333333Àffffff%À"À333333À!Àš™™™™™%ÀÍÌÌÌÌÌÀ%À333333,ÀÍÌÌÌÌÌ'Àš™™™™™À333333ÀÀ ÀÍÌÌÌÌÌÀffffffÀš™™™™™ Àffffff ÀÍÌÌÌÌÌÀÀffffffÀ333333ÀÀÍÌÌÌÌÌÀ333333$Àffffff!Àffffff#Àš™™™™™(À1À333333À333333ÀÀ333333Àš™™™™™À333333 À333333+ÀÀ333333À333333$À3333331ÀffffffÀ333333ÀÍÌÌÌÌÌÀ!ÀÀ333333 Àš™™™™™ÀffffffÀš™™™™™Àffffffþ¿333333ÀÀ333333ó¿ffffffÀÍÌÌÌÌÌ)Àš™™™™™ Àffffffæ¿333333"ÀÀÍÌÌÌÌÌ&ÀÍÌÌÌÌÌÀffffffö¿š™™™™™(Àš™™™™™%Àš™™™™3ÀffffffÀš™™™™™Àš™™™™™!ÀÀš™™™™™%ÀÍÌÌÌÌÌÀffffffÀ333333ÀÍÌÌÌÌÌÀÀð?ffffffæ?ÍÌÌÌÌÌ@ÍÌÌÌÌÌô?à?ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?333333ã?333333ã?à?ffffff @333333 @ffffffö?à?ð?ÍÌÌÌÌÌ@@š™™™™™ù?@ð?š™™™™™ñ?333333ó?333333ã?ø?š™™™™™ñ?333333ó?ÍÌÌÌÌÌô?ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?š™™™™™ñ?ÍÌÌÌÌÌ @333333 @ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?à?à?ffffffæ?ffffff@333333@ÍÌÌÌÌÌü?@š™™™™™ñ?@333333ó?ÍÌÌÌÌÌì?ffffffö?ffffffæ?333333û?ÍÌÌÌÌÌô?333333û?à?ÍÌÌÌÌÌô?ffffff@ffffffö?ffffff@š™™™™™ñ?š™™™™™ù?à?333333ó?333333@@333333ã?š™™™™™ñ?ffffffæ?ffffff@š™™™™™@ffffffþ?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?333333ã?ffffff@333333@ÍÌÌÌÌÌü?š™™™™™ñ?ÍÌÌÌÌÌ@333333@ffffffæ?ffffffæ?ffffffö?š™™™™™é?ÍÌÌÌÌÌü?ffffffþ?333333ã?ffffffö?š™™™™™ñ?ÍÌÌÌÌÌ @ffffff@š™™™™™@333333@ffffff@ÍÌÌÌÌÌ@š™™™™™é?š™™™™™Ù?ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?š™™™™™Ù?@ffffffþ?ÍÌÌÌÌÌì?š™™™™™Ù?@333333ã?333333ó?ø?ffffff@ffffff@š™™™™™é?ð?@333333û?@š™™™™™ù?333333û?ffffff@ÍÌÌÌÌÌô?ð?ffffffþ?ffffffæ?333333ó?ffffffþ?333333û?à?ffffffþ?ffffffæ?ffffffö?ÍÌÌÌÌÌü?333333ã?@333333@ffffff@à?à?@š™™™™™ù?333333 @333333@š™™™™™ù?š™™™™™ñ? @à?333333ã?ffffff@š™™™™™ù?ffffffæ?ð?ffffff@@ffffffþ?333333@š™™™™™ù?š™™™™™@333333ó?ffffffæ?ffffffþ?ø?ffffff@š™™™™™ñ?š™™™™™é?@@ð?333333û?ÍÌÌÌÌÌô?š™™™™™ù?ø?ø?ffffffæ?@š™™™™™Ù?333333%@ @333333ã?š™™™™™@ÍÌÌÌÌÌô?ffffff@333333ã?š™™™™™ñ?ÍÌÌÌÌÌ@@š™™™™™@ffffffþ?ÍÌÌÌÌÌü?š™™™™™ù?ffffff@š™™™™™@ð?ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?ÍÌÌÌÌÌ@š™™™™™Ù?š™™™™™ù?š™™™™™ @ð?ffffff@š™™™™™é?ffffffæ?ffffff@ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?š™™™™™ñ?š™™™™™ñ?š™™™™™ñ?š™™™™™@š™™™™™ @ÍÌÌÌÌÌì?š™™™™™Ù?ffffff@à?š™™™™™@ffffffæ?ÍÌÌÌÌÌô?@ffffffæ?ffffff@333333ó?333333ó?ffffffö?š™™™™™ù?333333ó?š™™™™™ñ?ffffffö?ÍÌÌÌÌÌü?333333ã?š™™™™™Ù?333333ã?ÍÌÌÌÌÌì?š™™™™™@à?ø?ÍÌÌÌÌÌü?ø?š™™™™™ù?š™™™™™ñ?ÍÌÌÌÌÌô?ffffff@à? @ffffffþ?@š™™™™™ @ffffff@fffffæ8@ffffffþ?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌì?š™™™™™é?š™™™™™Ù?333333@333333û?š™™™™™Ù?333333û?ÍÌÌÌÌÌô?ffffffæ?š™™™™™Ù?ffffff@ffffff$@ð?à?&@ÍÌÌÌÌÌì?333333@ÍÌÌÌÌÌô?ffffffþ?š™™™™™@š™™™™™é?333333û?š™™™™™ñ?ÍÌÌÌÌÌô?ÍÌÌÌÌÌô?ÍÌÌÌÌÌ@ÍÌÌÌÌÌì?333333ó?ffffff@š™™™™™ @ÍÌÌÌÌÌì?ÍÌÌÌÌÌ@ffffffæ?š™™™™™ñ?ffffffö?ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?ffffff@ÍÌÌÌÌÌü?ffffffæ?ffffff @ffffffö?333333ã?@333333û?333333@ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?ð?333333û?ffffffö?333333ó?ÍÌÌÌÌÌ@š™™™™™é?333333 @ffffff@š™™™™™ñ?ÍÌÌÌÌÌ@333333û?à?333333@š™™™™™@ø?à?ÍÌÌÌÌÌü?ÍÌÌÌÌÌ@š™™™™™ñ?ÍÌÌÌÌÌ@ÍÌÌÌÌÌì?333333ã?š™™™™™é?š™™™™™@333333@ffffffæ?š™™™™™@@š™™™™™ù?333333ó?@ffffff@à?ÍÌÌÌÌÌì?ÍÌÌÌÌÌ@:@333333ó?š™™™™™Ù?š™™™™™ñ?333333@š™™™™™Ù?š™™™™™é?$@à?ffffff@333333ã?@š™™™™™@@@ffffffþ?š™™™™™Ù?ÍÌÌÌÌÌì?ð?ffffffæ?ÍÌÌÌÌÌü?ÍÌÌÌÌÌì?333333ó?ffffffæ?ð?@ffffffö?333333ã?@ffffffæ?@ffffffæ?333333û?š™™™™™ñ?@@ffffffæ?@š™™™™™Ù?ÍÌÌÌÌÌü?š™™™™™é?ÍÌÌÌÌÌ@à?š™™™™™@@š™™™™™!ÀffffffÀffffff8À'À333333ÀÀš™™™™™À!Àš™™™™™Àš™™™™™Àš™™™™™ÀÍÌÌÌÌÌ$Àffffff+Àffffff!ÀÀ333333ÀÀÍÌÌÌÌÌÀÍÌÌÌÌÌ$À"ÀÍÌÌÌÌÌ'Àffffff*À"À)À333333"ÀffffffÀ333333À!ÀffffffÀ$À333333-ÀffffffÀÍÌÌÌÌÌÀš™™™™™ ÀÍÌÌÌÌL0Àš™™™™™ÀÀÀ333333)À"ÀÍÌÌÌÌÌ$Àffffff/Àš™™™™™ ÀÀÀš™™™™™é¿ÍÌÌÌÌÌÀÍÌÌÌÌÌÀÍÌÌÌÌÌ$ÀffffffÀš™™™™™À333333%ÀÀ$Àš™™™™™ À ÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀš™™™™™ÀÀ"ÀÍÌÌÌÌÌ"ÀffffffÀÍÌÌÌÌÌ3ÀffffffÀffffffÀ333333À333333û¿333333Àš™™™™™Àffffff ÀÍÌÌÌÌÌÀš™™™™™À333333,ÀÀ333333'ÀÍÌÌÌÌÌÀffffffÀffffffÀ333333À&ÀffffffÀffffffÀš™™™™™.À$À333333À333333Àš™™™™™ À333333 À333333Àš™™™™™"À333333Àš™™™™™ÀÍÌÌÌÌÌÀÍÌÌÌÌÌ(ÀÍÌÌÌÌÌÀ333333ÀÀÍÌÌÌÌÌÀ333333ÀÍÌÌÌÌÌÀš™™™™™$À333333Àš™™™™™!ÀffffffÀ&Àš™™™™™À333333!ÀÀÍÌÌÌÌÌÀ333333%Àš™™™™™!ÀÀ333333ÀffffffÀÍÌÌÌÌÌÀffffffÀ"ÀffffffÀ333333"Àš™™™™™Àš™™™™™ ÀffffffÀÍÌÌÌÌÌ$À ÀffffffÀÍÌÌÌÌÌÀffffff"À333333ÀÍÌÌÌÌÌÀffffffÀ(ÀÍÌÌÌÌÌ#ÀffffffÀ333333&À333333 À333333ÀÀffffffÀ)Àš™™™™™À333333ÀÍÌÌÌÌL0ÀÍÌÌÌÌÌ!ÀÀffffffÀ!Àš™™™™™ÀÍÌÌÌÌÌ"À€0ÀffffffÀš™™™™™"Àffffff&À333333À333333 Àš™™™™™ÀÍÌÌÌÌÌ(ÀÍÌÌÌÌL2ÀÍÌÌÌÌÌÀ333333Àffffff3À333333ÀÍÌÌÌÌÌ Àš™™™™™ ÀÍÌÌÌÌÌ+À333333À!ÀffffffÀ333333Àffffff+À333333À333333 Àš™™™™™ Àš™™™™™ ÀÍÌÌÌÌÌÀš™™™™™Àffffff Àš™™™™™ À!À333333 ÀÍÌÌÌÌÌÀffffff(Àš™™™™™!Àš™™™™™ÀÍÌÌÌÌÌ0ÀÍÌÌÌÌÌ&À333333&Àš™™™™™ÀffffffÀš™™™™™ÀÍÌÌÌÌÌ%ÀÀÍÌÌÌÌÌ(Àš™™™™™À333333Àš™™™™™!Àffffff#ÀÀ!Àš™™™™™ÀÍÌÌÌÌÌÀš™™™™™À333333,Àffffff(Àš™™™™™&À&Àš™™™™™ù¿š™™™™™ÀÀffffff-ÀÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀš™™™™™ÀÍÌÌÌÌÌÀ333333Àš™™™™™ ÀÀffffffÀš™™™™™*ÀÍÌÌÌÌÌÀ333333"Àš™™™™™"À333333!ÀÀffffffÀÀffffffÀffffff#À"À333333À333333ÀÍÌÌÌÌÌ+ÀffffffÀš™™™™™(À&Àffffff À333333$À333333Àš™™™™™Àš™™™™™%ÀÍÌÌÌÌÌÀ Àffffff7Àš™™™™™Àffffff Àš™™™™™ À333333'Àš™™™™™%ÀffffffÀ333333À À333333ÀÀ333333ÀÍÌÌÌÌÌ#À333333À333333-ÀffffffÀ333333À,ÀffffffÀÍÌÌÌÌÌÀÀÀffffffÀ ÀÀÀ333333#ÀÍÌÌÌÌÌÀ333333)Àffffff!ÀffffffÀš™™™™™À333333"À!À333333ÀffffffÀ À'À333333Àš™™™™™ÀffffffÀ*Àš™™™™6À333333 Àffffff(ÀÍÌÌÌÌÌÀ333333ÀÍÌÌÌÌÌ&Àš™™™™™"ÀÍÌÌÌÌL1À333333Àffffff À33333³0ÀÍÌÌÌÌÌ#ÀÍÌÌÌÌÌ!ÀÀffffffþ¿ffffff À$ÀÍÌÌÌÌÌ À333333ÀÍÌÌÌÌÌ*Àš™™™™™À"ÀfffffæBÀ333333#Àffffff Àš™™™™™ÀffffffÀš™™™™0ÀffffffÀffffffÀffffffÀffffff!ÀÍÌÌÌÌÌÀ333333ÀÀš™™™™™À"À33333³6ÀffffffÀ333333ÀÍÌÌÌÌÌ*À333333)Àffffff$Àffffff Àffffff#Àš™™™™0Àš™™™™™ÀÍÌÌÌÌÌ"À333333ÀÍÌÌÌÌÌ&Àš™™™™™#À3333334Àffffff'ÀÍÌÌÌÌÌÀ333333À333333"Àš™™™™™ Àffffff!ÀÀffffffÀ333333/ÀÀš™™™™™-Àš™™™™™À333333%ÀÍÌÌÌÌÌÀ#Àffffff$À333333û¿ffffffö¿ffffff!À333333À333333Àffffffö¿ffffff(Àffffff À@ffffffþ?ÍÌÌÌÌÌô?ÍÌÌÌÌÌü?š™™™™™ñ?ffffff@ÍÌÌÌÌÌü?ÍÌÌÌÌÌü?(@š™™™™™ñ?@ffffff @à?š™™™™™ñ?ffffffþ?š™™™™™@ð?à?ffffffæ?ffffffþ?ÍÌÌÌÌÌô?ø?ÍÌÌÌÌÌü?ð?ÍÌÌÌÌÌô?ffffffæ?333333ó?š™™™™™ñ?ÍÌÌÌÌÌ@333333@333333"@333333@ð?ffffffö?ÍÌÌÌÌÌì?ø?š™™™™™@@ffffff @3333333@ÍÌÌÌÌÌü?à?š™™™™™ñ?333333@ @333333@333333@333333)@š™™™™™Ù¿ffffffæ¿333333Àš™™™™™é¿à?333333Ó?š™™™™™Ù¿š™™™™™Ù¿333333ó¿333333Ó¿š™™™™™É?ffffff濚™™™™™É?š™™™™™¹¿š™™™™™Ù¿š™™™™™é¿š™™™™™É?š™™™™™é¿ÍÌÌÌÌÌì¿333333㿚™™™™™É?š™™™™™É¿333333㿚™™™™™É?à¿333333ã¿333333Ó?š™™™™™é?ÍÌÌÌÌÌÀš™™™™™¹?333333ã¿à¿333333㿚™™™™™¹?š™™™™™Ù?࿚™™™™™É?š™™™™™É¿š™™™™™¹?š™™™™™ñ¿333333㿚™™™™™¹¿à¿333333ó¿š™™™™™Ù¿š™™™™™é¿333333㿚™™™™™Ù¿ÍÌÌÌÌÌ쿚™™™™™Ù¿š™™™™™ñ¿ffffffæ?à?à¿à¿š™™™™™É?š™™™™™é¿333333Ó?࿚™™™™™¹?š™™™™™É?š™™™™™Ù?333333Ó¿333333ã¿333333Ó?ffffffæ?š™™™™™é¿ÍÌÌÌÌÌì¿333333ã¿333333ã¿à¿š™™™™™é¿ffffff濚™™™™™Ù¿š™™™™™é¿333333㿚™™™™™é¿ffffffþ¿ffffff濚™™™™™é¿333333㿚™™™™™é¿š™™™™™Ù¿333333ã¿333333㿚™™™™™Ù¿š™™™™™ñ¿š™™™™™¹¿š™™™™™Ù?š™™™™™¹?333333Ó?š™™™™™Ù?ð¿à¿ÍÌÌÌÌÌì¿333333ó¿š™™™™™ñ¿ffffffæ¿333333ó¿333333㿚™™™™™¹¿š™™™™™ñ¿š™™™™™é¿š™™™™™¹?à?š™™™™™é¿š™™™™™É?š™™™™™¹?š™™™™™¹?333333Ó?࿚™™™™™é¿333333㿚™™™™™¹?š™™™™™É?ffffffæ¿333333ã¿à¿à¿à¿333333Ó¿š™™™™™é¿333333Ó¿333333Ó?333333Ó?š™™™™™é¿ð¿333333Ó¿ffffffþ¿š™™™™™¹?š™™™™™Ù¿333333ã¿333333㿚™™™™™é¿š™™™™™É?š™™™™™é¿ffffff濚™™™™™É?333333ã¿à?š™™™™™é¿š™™™™™Ù?333333Ó¿š™™™™™é¿ffffff濚™™™™™é¿š™™™™™Ù¿333333ã¿333333Ó?ffffff濚™™™™™ñ¿š™™™™™Ù¿š™™™™™Ù¿š™™™™™é¿333333Ó?࿚™™™™™é¿š™™™™™¹?ffffff濚™™™™™¹?š™™™™™É?333333ã¿ÍÌÌÌÌÌì¿333333Ó¿ffffffæ¿ð¿š™™™™™¹?à?ð¿à¿333333Ó?333333㿚™™™™™¹?š™™™™™é¿š™™™™™¹?š™™™™™É?333333ã¿ Àš™™™™™é¿333333Ó¿š™™™™™Ù?š™™™™™¹?š™™™™™Ù¿333333Ó?333333Ó¿ffffffæ¿à¿ð?š™™™™™¹?š™™™™™É?š™™™™™¹¿à¿š™™™™™É?333333㿚™™™™™¹?333333㿚™™™™™¹?š™™™™™¹¿ÍÌÌÌÌÌ쿚™™™™™¹?š™™™™™é¿š™™™™™¹?333333Ó?333333ã?333333Ó?ÍÌÌÌÌÌô¿š™™™™™é¿š™™™™™Ù¿333333ã¿333333ó¿š™™™™™é¿š™™™™™Ù¿ÍÌÌÌÌÌô¿ffffffæ¿333333Ó?š™™™™™ñ¿333333Ó?333333ó¿ffffff濚™™™™™É¿à¿ð¿333333Ó?š™™™™™É?ffffffæ¿à¿š™™™™™é¿š™™™™™¹?š™™™™™Ù?š™™™™™¹?333333ã¿333333Ó?š™™™™™É?𿚙™™™™É?333333㿚™™™™™Ù?333333Ó¿š™™™™™Ù?ÍÌÌÌÌÌì¿à?࿚™™™™™¹¿š™™™™™É¿333333Ó¿š™™™™™¹?333333Ó?š™™™™™É?333333㿚™™™™™Ù¿š™™™™™¹?š™™™™™Ù¿š™™™™™Ù¿à?à¿333333Ó¿š™™™™™Ù?š™™™™™¹¿333333ӿ࿚™™™™™É?333333Ó¿š™™™™™é¿ÍÌÌÌÌÌ쿚™™™™™Ù?ffffffæ¿333333Ó¿ÍÌÌÌÌÌì¿ffffffö¿ffffff濚™™™™™é?𿚙™™™™Ù?š™™™™™¹?ffffffæ¿333333ã¿ffffffæ?333333ã¿ð?ÍÌÌÌÌÌì¿333333Ó¿ffffffö¿333333㿚™™™™™ñ¿333333ó¿ffffffæ¿333333û¿š™™™™™¹¿à¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌì¿333333ó¿333333ã¿333333ó¿ffffff濚™™™™™É¿333333ã¿ÍÌÌÌÌÌì¿333333ã¿ffffff濚™™™™™É¿à¿ffffffæ¿333333Ó¿333333Ó?š™™™™™É?333333ó¿ffffffæ¿333333Ó¿333333ã¿333333㿚™™™™™¹?ÍÌÌÌÌÌì¿333333ó¿š™™™™™é¿ð¿š™™™™™Ù¿ffffffÀš™™™™™É¿à?à¿ð?ÍÌÌÌÌÌô¿ffffff濚™™™™™é?š™™™™™é¿ð¿333333Ó?š™™™™™Ù¿333333ã?š™™™™™Ù?ÍÌÌÌÌÌô¿š™™™™™ñ¿ð¿š™™™™™Ù¿š™™™™™é¿š™™™™™¹?š™™™™™¹?333333ó¿à?š™™™™™Ù¿ÍÌÌÌÌÌô¿ffffffö¿š™™™™™¹?š™™™™™¹¿š™™™™™É¿ð¿š™™™™™Ù¿š™™™™™¹?š™™™™™Ù¿š™™™™™é¿333333Ó?š™™™™™Ù¿à¿š™™™™™é¿š™™™™™é¿š™™™™™¹?à?š™™™™™ñ¿333333ã¿ffffff濚™™™™™¹?š™™™™™Ù¿ÍÌÌÌÌÌô?333333㿚™™™™™é¿333333ã?š™™™™™é¿333333Ó¿ø¿š™™™™™¹?š™™™™™É?š™™™™™é¿333333Ó?š™™™™™É¿š™™™™™É?333333ó¿š™™™™™Ù?š™™™™™¹?š™™™™™é¿à?ffffffæ¿333333ó¿ffffffæ¿333333ã¿333333ã?333333㿚™™™™™Ù¿š™™™™™ñ?š™™™™™Ù¿ð¿ÍÌÌÌÌÌô¿333333㿚™™™™™É?ÍÌÌÌÌÌì¿333333Ó¿š™™™™™É¿333333ã¿ffffff濚™™™™™Ù?š™™™™™Ù¿333333Ó?š™™™™™¹?333333Ó?š™™™™™¹¿š™™™™™Ù¿ÍÌÌÌÌÌü?ffffffæ?š™™™™™É?333333ã¿333333ã¿ffffff Àš™™™™™Ù¿à¿š™™™™™Ù?333333û?š™™™™™¹¿ð?š™™™™™é?ffffffö¿ÍÌÌÌÌÌü¿š™™™™™Ù¿š™™™™™Ù¿333333Ó¿à?š™™™™™Ù¿à?ffffff@ffffffæ¿Àffffffæ?ffffffæ?ÍÌÌÌÌÌ쿚™™™™™ù¿š™™™™™@333333ã?š™™™™™À@ÍÌÌÌÌÌü?ø?333333Ó¿š™™™™™Ù¿ÍÌÌÌÌÌü?ÍÌÌÌÌÌì?š™™™™™¹¿š™™™™™ñ¿ÍÌÌÌÌÌü¿š™™™™™ù¿ð¿š™™™™™é¿333333ã?š™™™™™é¿ÍÌÌÌÌÌ쿚™™™™™É¿à¿333333ã?š™™™™™É¿š™™™™™é¿š™™™™™é?ffffffö¿333333Ó¿š™™™™™é?ð¿ÍÌÌÌÌÌ@333333Ó¿à¿ffffffæ?ffffffþ?𿚙™™™™ñ?333333ó¿ÍÌÌÌÌÌ쿚™™™™™ù¿š™™™™™ñ¿à?ffffffö?ffffffö¿ffffff濚™™™™™É¿ð¿ÍÌÌÌÌÌì?333333Ó¿ÍÌÌÌÌÌì?š™™™™™ñ¿à¿š™™™™™ù?š™™™™™Ù?à¿ffffffæ?ffffffæ?ffffff濚™™™™™¹¿ø?ð?@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@š™™™™™@333333 @ffffffö¿š™™™™™É?ÍÌÌÌÌÌ쿚™™™™™É?š™™™™™ñ¿333333ó?333333û¿333333û¿ÍÌÌÌÌÌô¿š™™™™™É¿ð¿333333ã?ð¿333333û¿333333Ó¿š™™™™™ù?ffffffæ¿ÍÌÌÌÌÌü¿ffffff@ffffff@š™™™™™¹¿ð?š™™™™™É¿ffffffÀffffffþ?333333ó¿ÍÌÌÌÌÌì¿ffffffÀš™™™™™¹¿ø?š™™™™™ñ?š™™™™™Ù?333333û?ÍÌÌÌÌÌì¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌü¿ffffffæ?ffffffæ?333333ã?ÍÌÌÌÌÌì?ð¿à?333333㿚™™™™™É¿333333û¿ð?š™™™™™ñ¿333333ó?š™™™™™ñ¿š™™™™™Ù¿ÍÌÌÌÌÌì?ÍÌÌÌÌÌì¿333333ã¿333333ó¿ffffffæ¿ffffff濚™™™™™ù?ø¿ø¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌ쿚™™™™™¹?ø¿ffffffÀø¿ø¿ÍÌÌÌÌÌì?ffffffö¿ð¿Àffffffæ?333333ã¿ÍÌÌÌÌÌü?333333ó¿š™™™™™É¿à?ÍÌÌÌÌÌü¿333333ã?ffffff@ð?ffffff@š™™™™™Ù¿ÍÌÌÌÌÌì¿Àð¿ffffffæ?333333ó¿ÍÌÌÌÌÌü?š™™™™™¹?333333ã?š™™™™™Ù¿ffffff@ffffffæ?333333Ó¿ð?ÍÌÌÌÌÌì¿à¿ÍÌÌÌÌÌô?@333333ó¿š™™™™™É¿ffffffæ?à¿à?333333Ó¿š™™™™™ñ¿ÍÌÌÌÌÌì¿ffffffæ?333333û¿à¿à?ffffffö?ÍÌÌÌÌÌü¿@333333Ó¿333333㿚™™™™™@333333ó¿ÍÌÌÌÌÌì¿ffffff À333333Ó¿@š™™™™™ù?333333ó?333333ó¿ÍÌÌÌÌÌô?š™™™™™é¿š™™™™™ñ?à¿ffffff濚™™™™™É¿333333Ó¿š™™™™™é¿š™™™™™¹¿333333ã¿333333Ó?ð?333333ã¿ø?333333ó?@š™™™™™ù¿@ø?ffffffö?š™™™™™é?š™™™™™ñ?š™™™™™é?ffffffö¿ð¿ffffff@ÍÌÌÌÌÌÀ333333ã?ð¿ÍÌÌÌÌÌô?333333û?ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?à?š™™™™™ù¿333333ó?š™™™™™é¿ð?à?ffffffæ?ffffff@333333ó?ÍÌÌÌÌÌì?ø?š™™™™™é¿ÍÌÌÌÌÌü¿ÍÌÌÌÌÌü¿š™™™™™Ù¿ÍÌÌÌÌÌ@š™™™™™Ù¿ð¿333333Ó¿333333ã¿ÍÌÌÌÌÌ쿚™™™™™Ù?333333Ó?ffffffæ?š™™™™™É?š™™™™™é¿à?š™™™™™¹¿ÍÌÌÌÌÌô¿š™™™™™É¿š™™™™™ñ?ð?ÍÌÌÌÌÌ@à?333333Ó?333333ã?333333ã?ffffffæ?š™™™™™ñ¿š™™™™™Ù¿à¿à?š™™™™™ù?à¿ffffffö¿š™™™™™Ù?à¿ÍÌÌÌÌÌ@333333À333333û¿333333@ffffffæ?š™™™™™ù¿333333Ó¿ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?ffffffö¿@š™™™™™É¿@š™™™™™¹¿š™™™™™ñ¿333333Ó¿ffffff濚™™™™™Àš™™™™™Ù¿š™™™™™ñ?š™™™™™ À333333û?ÍÌÌÌÌÌü?š™™™™™ñ¿à?333333û?à?@333333ӿ࿚™™™™™é?š™™™™™ù?š™™™™™ù?ð?š™™™™™é¿ffffffæ?š™™™™™Ù¿š™™™™™¹¿ffffffö?ÍÌÌÌÌÌô¿333333ã¿333333ó?à¿ÍÌÌÌÌÌ@ÍÌÌÌÌÌô?š™™™™™é¿333333ã?à¿ÀÍÌÌÌÌÌü¿š™™™™™Ù¿š™™™™™ù?333333Ó¿ø?š™™™™™ù?333333ó?ÍÌÌÌÌÌô?ffffffþ? @ffffffþ?š™™™™™ù?ð?š™™™™™ñ?ffffff@333333ã?ð?ffffffæ?ffffffþ¿à¿ÍÌÌÌÌÌô?333333û?š™™™™™@𿚙™™™™É¿ÍÌÌÌÌÌ@š™™™™™Ù¿333333ã?š™™™™™ñ?š™™™™™É¿333333Ó¿š™™™™™¹¿à¿š™™™™™ñ¿š™™™™™Àà?ð?š™™™™™ñ?š™™™™™¹¿š™™™™™ñ¿à¿@š™™™™™Ù?33333³0@ffffffæ?ffffffæ?ÍÌÌÌÌÌô¿à¿ffffffþ?@333333@š™™™™™Ù?ffffffÀ333333Ó¿š™™™™™Ù¿ffffffæ?À333333û?ÍÌÌÌÌÌÀ333333ã¿ffffffþ¿333333û?ÍÌÌÌÌÌô?333333Ó¿š™™™™™É?333333@ø?333333û?333333û¿333333Ó¿ffffffö?ÍÌÌÌÌÌì?333333ã?ffffffæ¿à¿š™™™™™é¿à?333333ó¿š™™™™™É?𿚙™™™™¹¿š™™™™™é¿ÍÌÌÌÌÌü¿333333ó?ffffffö?ffffffÀš™™™™™¹¿Àš™™™™™¹¿ÍÌÌÌÌÌô?à¿ø¿ÍÌÌÌÌÌì¿333333ó?ø¿ffffffæ¿333333ã¿à¿š™™™™™É?š™™™™™É?ø?ÍÌÌÌÌÌô¿š™™™™™¹¿ÍÌÌÌÌÌ@ffffffÀð?š™™™™™ñ?8Àš™™™™™5À33333³2@3333332À9Àš™™™™™NÀfffffæ>À/À-ÀÍÌÌÌÌÌÀ€:À"À333333À"À3333334À33333³<Àš™™™™™2À333333?Àš™™™™Y@À333333'Àš™™™™™3ÀÍÌÌÌÌÌÀÀÍÌÌÌÌL4À33333³7À`QÀffffff2ÀÍÌÌÌÌÌ2À33333³;Àš™™™™Ù@ÀÍÌÌÌÌÌ%À3333337ÀÍÌÌÌÌÌ3Àffffff?ÀÍÌÌÌÌÌ7ÀÍÌÌÌÌÌ5À3333331ÀÍÌÌÌÌÌÀffffff3Àfffffæ9À333333"À1ÀÍÌÌÌÌL6Àš™™™™7Àš™™™™4ÀÍÌÌÌÌÌ(À333333/À3333333Àffffff8Àš™™™™™FÀÍÌÌÌÌÌ%À*À@333333@À7Àfffffæ9ÀÍÌÌÌÌŒDÀÍÌÌÌÌL7À4Àfffffæ1Àš™™™™8@ÍÌÌÌÌÌ!ÀÍÌÌÌÌÌ-Àš™™™™™9Àfffffæ6Àffffff+Àfffffæ1Àš™™™™™4À;Àffffff ÀÍÌÌÌÌÌ6À>ÀÍÌÌÌÌÌ5Àffffff*Àš™™™™4Àš™™™™™Ù?3333331À33333³0À€1Àffffff4À@AÀÍÌÌÌÌL=À€;Àffffff@š™™™™™8À`RÀffffff<Àfffffæ4Àš™™™™ÙUÀÍÌÌÌÌ PÀfffffæ9À€2À33333³3ÀÍÌÌÌÌÌ#ÀÍÌÌÌÌÌ@ð?š™™™™™ù?33333³7ÀÍÌÌÌÌÌ6ÀÍÌÌÌÌL1ÀÍÌÌÌÌÌ)ÀÍÌÌÌÌL@Àš™™™™™ÀÍÌÌÌÌ AÀÍÌÌÌÌÌ'ÀÍÌÌÌÌÌ@Àš™™™™™ÀÍÌÌÌÌÌ#ÀÍÌÌÌÌL0ÀÍÌÌÌÌLAÀš™™™™1À333333+Àš™™™™™,À@CÀ€5À333333À33333³0À9Àffffff5Àš™™™™™.Àš™™™™6Àš™™™™™#Àfffffæ3Àš™™™™™À+ÀÍÌÌÌÌL4Àš™™™™2À333333/ÀÍÌÌÌÌÌ0À333333;ÀÍÌÌÌÌÌ*Àš™™™™™.À3333333À0Àffffff*Àfffffæ8Àfffffæ8À$À33333³0Àffffff,Àffffff;À@&ÀÍÌÌÌÌL2Àš™™™™™À333333À333333Àš™™™™™:ÀffffffÀ333333À'À/ÀffffffÀ€1ÀÍÌÌÌÌL3ÀÍÌÌÌÌÌ1À5À333333ÀÍÌÌÌÌL=À+À333333À33333³<À3333337Àffffff/Àš™™™™™*Àš™™™™;Àš™™™™™*ÀÍÌÌÌÌÌ(À333333(À€;Àfffffæ8Àš™™™™™4À@Àffffff%ÀÍÌÌÌÌÌ*Àffffff2Àffffff0À333333Àffffff@À33333³2À3333330Àš™™™™™AÀ333333,À€3À333333<Àš™™™™™%@ÍÌÌÌÌL9ÀÍÌÌÌÌÌ+ÀÍÌÌÌÌÌ:ÀÍÌÌÌÌ VÀÍÌÌÌÌL8À?Àš™™™™™À3333338À333333=ÀffffffÀ33333³5Àffffff#Àš™™™™™>À€NÀ)À€1Àfffff&LÀfffffæ0Àš™™™™YGÀš™™™™™0ÀÍÌÌÌÌÌ"ÀÍÌÌÌÌÌWÀ33333s@À333333&Àš™™™™™2@ffffff%À#ÀÀEÀ333333 Àš™™™™™/Àfffffæ4À3333330Àffffff5À2À333333 ÀÍÌÌÌÌÌü¿ffffff(ÀÀ𿚙™™™4À33333³1À3333331Àffffff6Àš™™™™™#Àš™™™™1À3333337Àffffff)ÀÀHÀÍÌÌÌÌÌ5Àš™™™™™>À33333³0À333333À.ÀÍÌÌÌÌÌ5À3À33333³?À333333(À#Àš™™™™1ÀÍÌÌÌÌÌ5ÀÀEÀ3333334Àš™™™™5Àš™™™™™À1À333333,ÀÍÌÌÌÌL7À333333DÀ€3ÀÍÌÌÌÌÌ5À9À33333³<Àš™™™™<ÀÍÌÌÌÌ HÀš™™™™>ÀffffffÀ2À-Àš™™™™=Àffffff&À333333À333333&ÀffffffÀ À)Àffffff/À,ÀÍÌÌÌÌÌÀ'Àš™™™™3ÀffffffBÀÍÌÌÌÌL9À3333332Àffffff8À33333³<À333333!À333333*À33333³2À333333Àš™™™™™4À:ÀÍÌÌÌÌL;Àš™™™™2Àffffff5À€9ÀÍÌÌÌÌL5Àš™™™™™,ÀÍÌÌÌÌÌ@@ÍÌÌÌÌÌ,ÀÍÌÌÌÌÌ?À33333³:ÀfffffÎp@š™™™™™ À333333/À,À3333332ÀÍÌÌÌÌÌ)Àš™™™™;À333333,ÀÍÌÌÌÌÌ2À33333sSÀ€8Àš™™™™™2À33333³8À333333BÀ333333BÀffffff>@ffffffE@ffffff+ÀÍÌÌÌÌÌ.ÀffffffB@ffffff,ÀÍÌÌÌÌÌ"Àffffff+À333333Àš™™™™™Àffffff2À33333óCÀffffffÀÍÌÌÌÌÌ$ÀÍÌÌÌÌÌÀ33333óBÀ,À333333'Àffffff$À3333336ÀÍÌÌÌÌÌ:Àffffff4Àffffffþ?33333³2À࿚™™™™1ÀÍÌÌÌÌÌ%Àfffff¦@Àffffff+Àš™™™™™-À333333Àš™™™™™,Àš™™™™™ù?33333³8Àš™™™™™À333333"À333333ÀÍÌÌÌÌÌ$À3333335Àš™™™™YCÀš™™™™™1À€=ÀffffffÀ€6ÀÍÌÌÌÌÌ=Àš™™™™™.À>À33333ÓRÀš™™™™6À333333BÀš™™™™Y@Àš™™™™™@€0Àfffff¦@À3333333ÀffffffIÀÍÌÌÌÌÌÀ3333338ÀÍÌÌÌÌlVÀš™™™™™.À333333BÀffffff:ÀÀffffff1ÀÍÌÌÌÌÌÀ+ÀÍÌÌÌÌÌÀš™™™™ÙM@333333ÀfffffæDÀÍÌÌÌÌL9À&Àš™™™™?Àš™™™™YBÀš™™™™™8Àfffffæ;ÀkÀ9À3333336Àš™™™™™,ÀÀÀ@À€0Àfffff¦\ÀÍÌÌÌÌÌ!Àš™™™™™#@š™™™™™:Àš™™™™™ ÀÍÌÌÌÌL7À333333Àffffff:ÀfffffæE@ffffff.Àš™™™™™+À€4Àš™™™™ÙKÀ%Àfffffæ4À333333 Àš™™™™YAÀš™™™™™1ÀÍÌÌÌÌÌ$À333333@À0À333333@ÀffffffÀš™™™™™+À:Àffffff%Àš™™™™™@À3333338ÀÍÌÌÌÌÌ,ÀÍÌÌÌÌ WÀfffffæ=ÀÍÌÌÌÌÌ/Àš™™™™1@ffffff<Àš™™™™2@ffffff$@š™™™™™1ÀÀ333333$ÀFÀš™™™™0ÀÍÌÌÌÌL?À€5Àfffffæ2À333333$À33333³9ÀÍÌÌÌÌL9Àš™™™™™ Àš™™™™™@3333336À333333*Àš™™™™™6Àš™™™™™2À333333Àš™™™™7@33333³<ÀffffffÀ33333³=ÀÍÌÌÌÌÌ:À333333+Àffffff6À>À33333óMÀÍÌÌÌÌÌ+Àffffff0À333333ÀÍÌÌÌÌÌ@33333óCÀÍÌÌÌÌL;ÀÍÌÌÌÌÌ&Àš™™™™™(À3333337ÀÍÌÌÌÌÌ5À3333336Àffffff8ÀÀ1Àš™™™™™DÀà¿ÍÌÌÌÌÌü¿€<ÀfffffæEÀ ÀÍÌÌÌÌÌ(ÀÍÌÌÌÌLCÀš™™™™™ÀÍÌÌÌÌÌÀ @ffffff:Àffffff8Àffffff.Àš™™™™™,À€6À€=À2ÀÍÌÌÌÌŒ@ÀÍÌÌÌÌŒAÀÍÌÌÌÌÌ/ÀÍÌÌÌÌÌ&ÀÍÌÌÌÌL<Àfffffæ;ÀÍÌÌÌÌÌ5À3333330À#ÀÍÌÌÌÌÌ4À333333=ÀffffffAÀš™™™™™?ÀÍÌÌÌÌL0ÀÍÌÌÌÌÌ%@š™™™™DÀš™™™™™ù¿=À333333GÀ333333/ÀÍÌÌÌÌÌü¿€9ÀÍÌÌÌÌŒ@@3333338À7ÀCÀš™™™™YAÀffffff%À333333@33333s@À@À$À333333@À33333³4Àš™™™™9Àš™™™™™ÀÍÌÌÌÌL?Àš™™™™™À333333@ÍÌÌÌÌÌ'À€6Àffffff)À33333³9ÀffffffBÀÍÌÌÌÌÌ<À33333³4@2À33333³1Àš™™™™:@š™™™™™$@333333@4ÀfffffæDÀffffff1Àš™™™™™=Àffffff!ÀffffffÀffffff*@ffffff/ÀÍÌÌÌÌLCÀ@AÀš™™™™3Àfffffæ9ÀÍÌÌÌÌÌ7Àš™™™™™/ÀÍÌÌÌÌLGÀ333333-ÀÍÌÌÌÌÌ.À333333,À33333³9À33333³5@ffffffö?ÍÌÌÌÌÌ1Àš™™™™™ À3333330Àš™™™™YWÀš™™™™™ ÀÍÌÌÌÌL3ÀÍÌÌÌÌL>Àš™™™™YIÀffffff6À333333ã?ffffff)ÀffffffÀÍÌÌÌÌÌÀš™™™™™+À3333333ÀÍÌÌÌÌ DÀ333333*ÀÍÌÌÌÌÌ&À&Àš™™™™™4À9À333333'À33333³8À2Àš™™™™;Àffffff/À@BÀš™™™™™@3333339À3333332Àffffff1Àà¿ÍÌÌÌÌÌ0Àš™™™™™<Àš™™™™™(À333333,ÀÍÌÌÌÌÌ&@š™™™™YDÀš™™™™3ÀÍÌÌÌÌÌ%À3333339ÀÍÌÌÌÌÌ:ÀÍÌÌÌÌÌÀš™™™™@Àfffff&EÀÍÌÌÌÌŒ@À33333sBÀ@ÍÌÌÌÌ FÀfffffæ6Àffffff8Àffffff6Àfffffæ<À333333ó?ffffff;Àfffffæ<À"ÀÍÌÌÌÌ BÀ333333ÀÍÌÌÌÌÌì¿33333³0Àffffffþ?š™™™™ÙAÀ€1À;À:ÀÍÌÌÌÌÌ&Àffffff*Àš™™™™™-À€0À#ÀÀCÀ333333'@333333,Àfffffæ>À6@CÀš™™™™™6À(À3333332ÀÍÌÌÌÌLAÀÍÌÌÌÌÌ@Àš™™™™™é¿ÍÌÌÌÌL=Àš™™™™7Àffffff$À33333s@Àš™™™™™/ÀÍÌÌÌÌÌ8ÀÍÌÌÌ̬TÀ3333332À333333*ÀÀDÀ33333³@ÀÍÌÌÌÌÌ!À3333333Àffffff)Àffffff*ÀÍÌÌÌÌ FÀ33333³3Àfffffæ8Àffffff*À@A@9À#@fffffæ7À333333ã?33333³7À333333#À7À333333(ÀffffffÀš™™™™™-Àffffff-Àffffffþ¿<À333333-ÀffffffÀÍÌÌÌÌL=Àffffff@š™™™™™ÀÍÌÌÌÌÌÀš™™™™;À33333³:ÀÍÌÌÌÌÌÀÍÌÌÌÌÌ'ÀÍÌÌÌÌÌ!À333333$@š™™™™™ ÀÍÌÌÌÌL?Àffffff;ÀÍÌÌÌÌÌ쿚™™™™YAÀ333333Àš™™™™9Àš™™™™™!Àffffff5ÀÍÌÌÌÌÌÀ(ÀffffffÀÍÌÌÌÌÌ<ÀÍÌÌÌÌÌÀ€@À=À.À333333-À333333@ÍÌÌÌÌÌ*Àffffff2Àfffffæ<ÀffffffBÀÍÌÌÌÌL5Àš™™™™™>ÀÍÌÌÌÌL2À"ÀÍÌÌÌÌÌ,Àffffff*Àffffff,À333333$À/À333333!À333333)ÀÍÌÌÌÌÌ"Àš™™™™™Àš™™™™1Àš™™™™™.Àš™™™™ÙBÀ@Àfffffæ3ÀÍÌÌÌÌÌ*ÀÍÌÌÌÌÌ-À333333&@333333 Àš™™™™0ÀÀfffffæ1Àš™™™™™3Àš™™™™YEÀfffffæ5ÀÍÌÌÌÌÌ:Àš™™™™6ÀÍÌÌÌÌÌ!À3333332ÀÍÌÌÌÌÌ7@ÍÌÌÌÌÌ#À33333sBÀ333333"@33333ãn@3333338À2@ÀÍÌÌÌÌ BÀ333333/ÀÍÌÌÌÌL2À333333û?fffff¦AÀffffffMÀš™™™™:Àffffff@ÍÌÌÌÌÌ9Àš™™™™HÀÀCÀÍÌÌÌÌL7@ÍÌÌÌÌÌÀffffff1ÀÍÌÌÌÌÌÀÍÌÌÌÌÌ,@333333@ÍÌÌÌÌÌ$@š™™™™™7Àš™™™™™ Àffffff5@%Àš™™™™™ÀffffffÀffffff,Àš™™™™™%@3333330Àffffffæ?ÍÌÌÌÌÌ À3333331Àfffffæ0Àš™™™™™?Àffffff5À333333/@ÍÌÌÌÌLBÀÀffffffÀš™™™™™/Àš™™™™™ Àffffffæ¿4Àš™™™™™ù¿fffffæ0À333333/ÀÍÌÌÌÌŒCÀÍÌÌÌÌÌ'ÀÍÌÌÌÌÌ@333333Àffffff@333333Àffffff7ÀÍÌÌÌÌÌü¿ffffff*ÀÍÌÌÌÌÌ=@333333Àffffff,ÀffffffÀ3333335ÀÍÌÌÌÌŒKÀÍÌÌÌÌL2À9ÀÍÌÌÌÌÌ7Àš™™™™™ À3333334À3333337Àffffffþ¿33333s@@ÍÌÌÌÌÌ)Àš™™™™:ÀÍÌÌÌÌÌ@ÍÌÌÌÌÌ2ÀÍÌÌÌÌÌ8Àffffff/ÀÍÌÌÌÌÌÀÍÌÌÌÌL4À!Àffffff1ÀÍÌÌÌÌL2Àš™™™™™A@š™™™™™Ù?€AÀÍÌÌÌÌL0ÀÍÌÌÌÌÌ'ÀÍÌÌÌÌ DÀš™™™™YEÀ333333ã?9ÀÍÌÌÌÌÌ1@š™™™™4À€0ÀÍÌÌÌÌL<Àš™™™™™$Àš™™™™™!ÀÍÌÌÌÌL5@ffffff$Àš™™™™™À"ÀÍÌÌÌÌL=À333333(Àš™™™™™-ÀfffffæBÀ333333&Àø?3333333Àš™™™™™<ÀffffffÀffffffBÀš™™™™™/Àš™™™™3À33333³4@3333332ÀÍÌÌÌÌÌô¿ffffff?À33333óAÀ@8Àš™™™™™@fffffæ3ÀÍÌÌÌÌÌ>À€3Àffffff9À333333BÀffffff)ÀÍÌÌÌÌÌZÀ?À8ÀÀ,ÀÍÌÌÌÌÌ=@33333óFÀš™™™™™2ÀÀAÀ333333&Àffffff:À5À333333EÀ33333³?À333333 Àfffffæ7À333333@ÀÍÌÌÌÌÌ>À333333%ÀÍÌÌÌÌÌ!@ÍÌÌÌÌÌ3Àš™™™™™É¿ffffffAÀ33333³3À3333335@ÍÌÌÌÌÌì¿ÍÌÌÌÌL0ÀÍÌÌÌÌÌ@š™™™™™Ù?ÍÌÌÌÌÌì¿ÍÌÌÌÌÌ@š™™™™™É¿ÍÌÌÌÌÌì?333333û?ÍÌÌÌÌÌì?ffffffæ?š™™™™™é?š™™™™™¹?š™™™™™¹?ð?ffffff@333333ó?š™™™™™ù?333333@š™™™™™¹?ÍÌÌÌÌÌ@333333Ó?333333ã?š™™™™™ñ?333333ó?ð?ð?ffffffþ?333333û?333333ó?à?š™™™™™Ù?ffffffö?š™™™™™Ù?š™™™™™é?333333ó?333333ó?ÍÌÌÌÌÌì?ð?ÍÌÌÌÌÌì?š™™™™™¹?š™™™™™ñ?ffffffæ?ÍÌÌÌÌÌì?š™™™™™¹?ÍÌÌÌÌÌì?š™™™™™¹?@à?š™™™™™é?š™™™™™é?333333Ó?š™™™™™ñ?š™™™™™é?ÍÌÌÌÌÌü?ø?š™™™™™ñ?333333ã?š™™™™™Ù?ÍÌÌÌÌÌì?š™™™™™é?à?à?š™™™™™Ù?š™™™™™Ù?333333ã?š™™™™™¹?333333ã?ÍÌÌÌÌÌì?333333ó?333333ó?à?ð?333333û?š™™™™™¹?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?333333û?š™™™™™ñ?š™™™™™ñ?333333ã?š™™™™™ñ?ÍÌÌÌÌÌ @ÍÌÌÌÌÌì?š™™™™™ @ffffff@@333333Ó?š™™™™™Ù?333333ó?à¿333333ã?333333Ó¿ffffffæ?š™™™™™É?333333Ó?š™™™™™¹?333333ã?ð?ffffffæ?ffffffæ?š™™™™™É?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?š™™™™™¹?333333ó?š™™™™™¹?ffffffæ?š™™™™™é?š™™™™™ñ?@ÍÌÌÌÌÌì?333333û?ffffffæ?à?333333ã?ð?ffffffö?š™™™™™É?à?ÍÌÌÌÌÌì?ffffffæ?ÍÌÌÌÌÌü?ÍÌÌÌÌÌì?š™™™™™ñ?ÍÌÌÌÌÌô?ffffffæ?š™™™™™ñ?š™™™™™é?š™™™™™ñ?ÍÌÌÌÌÌì?š™™™™™ñ?333333ã?333333ã?ÍÌÌÌÌÌô?333333û?333333ó?ð?333333ã?333333ó?@ffffffö?š™™™™™¹?ÍÌÌÌÌÌì?ffffffæ?ø?ÍÌÌÌÌÌì?š™™™™™Ù?à?à?333333ã?333333ã?333333û?ÍÌÌÌÌÌô?ÍÌÌÌÌÌô?š™™™™™ñ?š™™™™™é?š™™™™™¹?š™™™™™Ù?333333ã?333333ã?@š™™™™™¹¿š™™™™™@333333ã?š™™™™™é?ffffff@š™™™™™Ù?ÍÌÌÌÌÌì?š™™™™™É?ffffffæ?333333Ó?333333ã?š™™™™™@ffffffæ?333333ó?ð?333333Ó¿@333333ã?333333ó?333333@ffffffö?ø?࿚™™™™™É?š™™™™™ù?š™™™™™¹?ð?š™™™™™Ù?333333@ÍÌÌÌÌÌô?š™™™™™É¿ÍÌÌÌÌÌì?333333ã?@ffffffæ?š™™™™™¹?ÍÌÌÌÌÌ@š™™™™™ñ?š™™™™™Ù?š™™™™™¹¿ð?@ffffff@ð¿ÍÌÌÌÌÌì?š™™™™™ñ?à?333333ó?à?à¿333333ó?ffffffæ?š™™™™™Ù?š™™™™™é?333333ã?š™™™™™ñ?š™™™™™@š™™™™™ù?š™™™™™é?š™™™™™ñ?ffffffæ?ffffff@333333û?333333û?ð?š™™™™™¹¿à?š™™™™™É¿š™™™™™¹¿š™™™™™ñ?š™™™™™¹?š™™™™™¹?333333ã?š™™™™™é?š™™™™™@à?ffffffæ?š™™™™™ñ?333333Ó?ffffffö?ð?@š™™™™™Ù?ffffffö?ÍÌÌÌÌÌô?ø?333333û?ffffffæ?š™™™™™¹?š™™™™™¹?ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?š™™™™™ñ?š™™™™™ù?333333ã?š™™™™™¹?š™™™™™¹?333333Ó?š™™™™™¹?š™™™™™É¿š™™™™™¹?333333ã?š™™™™™ù?š™™™™™ñ?333333ó?ffffffö?333333û?š™™™™™ñ?š™™™™™¹?š™™™™™ù?š™™™™™¹¿ð?š™™™™™é?à?333333ã?333333Ó?š™™™™™ñ?ffffffæ?333333Ó?š™™™™™É?ÍÌÌÌÌÌô?ffffffö¿š™™™™™Ù¿š™™™™™É?š™™™™™@333333Ó¿333333ã?š™™™™™¹¿ø?ffffffæ?ð?ÍÌÌÌÌÌ @ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?š™™™™™é?ÍÌÌÌÌÌì?š™™™™™¹?ø¿à?ð?ÍÌÌÌÌÌô¿ÍÌÌÌÌÌô?ffffffö?š™™™™™Ù?ffffff@ÍÌÌÌÌÌ@333333ã?ÍÌÌÌÌÌô?ÍÌÌÌÌÌü?333333Ó¿ÍÌÌÌÌÌô?š™™™™™é?š™™™™™¹¿š™™™™™Ù?ÍÌÌÌÌÌ@ffffff@ð?333333ã?š™™™™™¹?š™™™™™¹?š™™™™™ñ?ffffffþ?ÍÌÌÌÌÌ@ÍÌÌÌÌÌô?333333ã?š™™™™™¹?à?ø?ø?@š™™™™™É?š™™™™™Ù?333333ã?ø?333333ã?š™™™™™é?333333 @ffffffþ?ffffff@333333ó?333333û?À333333Ó¿@š™™™™™ù?à?š™™™™™Ù?ð?š™™™™™@ÍÌÌÌÌÌ@š™™™™™Ù¿ffffffö? @à?š™™™™™¹?š™™™™™é?š™™™™™¹¿š™™™™™¹?333333ã?ffffffæ?ÍÌÌÌÌÌÀ333333ã?ÍÌÌÌÌÌü?ÍÌÌÌÌÌô?ffffffæ?@ÍÌÌÌÌÌü?ffffffþ?$@š™™™™™É¿š™™™™™¹?š™™™™™@@ÍÌÌÌÌÌì?ÍÌÌÌÌÌ@@à?ÍÌÌÌÌÌ@š™™™™™É?ø?š™™™™™é?ÍÌÌÌÌÌì?ø¿š™™™™™É?š™™™™™Ù?ÍÌÌÌÌÌì?ffffff@333333ã?š™™™™™¹?ÍÌÌÌÌÌô?š™™™™™é?333333ó?à?ÍÌÌÌÌÌ@333333û?ð?ffffffö?ÍÌÌÌÌÌì?ffffff@š™™™™™é¿à?ÍÌÌÌÌÌì?à?ÍÌÌÌÌÌ@@š™™™™™ñ?ø?à?ffffffö?ffffffæ?š™™™™™É?š™™™™™¹?ffffff @ffffffö?ð?š™™™™™ù?š™™™™™é?š™™™™™é?š™™™™™é?ffffffæ?ð?š™™™™™¹?333333@à?š™™™™™ñ?ð?333333û?ffffffÀ@š™™™™™Ù¿à¿š™™™™™é¿ÍÌÌÌÌÌÀÍÌÌÌÌÌÀš™™™™™Ù¿ffffffþ¿š™™™™™À333333!Àš™™™™™(À*Àš™™™™™=Àš™™™™3ÀÀÍÌÌÌÌÌü¿ÍÌÌÌÌÌÀš™™™™™Ù¿ffffff-ÀÍÌÌÌÌL>ÀÍÌÌÌÌÌÀ333333*À333333ÀÍÌÌÌÌÌ ÀÀÍÌÌÌÌÌÀffffff!Àffffff(ÀÍÌÌÌÌÌ1Àffffff ÀÀš™™™™™:ÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀ.Àš™™™™™Àà¿ÍÌÌÌÌÌ2ÀffffffÀ3333335À33333³:À333333Àð¿ffffff+ÀÀ@ÍÌÌÌÌÌ@š™™™™™@333333ó?š™™™™™ù?ÍÌÌÌÌÌ'@ffffff@ffffffæ?333333ó?š™™™™™@@ffffff @ffffff@ÍÌÌÌÌÌ!@ffffff@@š™™™™™é?š™™™™™é?š™™™™™@š™™™™™@333333@š™™™™™@@@333333 @ÍÌÌÌÌÌü?ÍÌÌÌÌÌ@ÍÌÌÌÌÌì?š™™™™™Ù?ÍÌÌÌÌÌì?@ø?ÍÌÌÌÌÌ@ffffffæ?ffffff@@333333û?333333 @ÍÌÌÌÌÌ@333333û?@š™™™™™@š™™™™™@333333 @ÍÌÌÌÌÌ@ÍÌÌÌÌÌì?ffffffö?ø?333333@š™™™™™!@ffffffþ?333333@š™™™™™ù?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ @ @ffffff @ÍÌÌÌÌÌ@333333@ffffff@ÍÌÌÌÌÌü?š™™™™™@ø?@ÍÌÌÌÌÌ@333333@ffffff @ffffff@ffffff@š™™™™™@333333@ÍÌÌÌÌÌ @@ÍÌÌÌÌÌ@ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?š™™™™™ @š™™™™™ù?!@333333'@ÍÌÌÌÌÌ@ø?à?š™™™™™ù?333333ó?ffffff@@333333@333333@ffffff@š™™™™™Ù?š™™™™™@ÍÌÌÌÌÌ$@ÍÌÌÌÌÌ@@@333333@ffffff@š™™™™™ @333333@333333.@ffffff@ÍÌÌÌÌÌü?333333 @333333@ffffff@ffffff@@š™™™™™ @333333 @@š™™™™™ @š™™™™™ù?ÍÌÌÌÌÌì?ffffffö?ffffff@333333û?ð?@ffffff @@ÍÌÌÌÌÌ@ffffff@ffffff@ÍÌÌÌÌÌ@ffffff@ffffff#@@š™™™™™@@š™™™™™@ffffff @š™™™™™@333333@@#@ÍÌÌÌÌÌ @ÍÌÌÌÌÌ@š™™™™™@@333333@@ffffffæ?ÍÌÌÌÌÌ@333333û? @ffffff@ffffff@333333ó?@ÍÌÌÌÌÌ@š™™™™™"@ffffff@ÍÌÌÌÌÌ@ffffffæ?š™™™™™ù?š™™™™™@š™™™™™@333333!@ÍÌÌÌÌÌ @333333@ÍÌÌÌÌÌ @ffffff@ffffff@@š™™™™™@@ffffff@ÍÌÌÌÌÌì?@333333@333333@333333@š™™™™™ñ?333333'@ @ÍÌÌÌÌÌ @ÍÌÌÌÌÌ@ÍÌÌÌÌÌô?ð?ÍÌÌÌÌÌ%@ffffff@š™™™™™@ffffff@š™™™™™ @ÍÌÌÌÌÌ@ÍÌÌÌÌÌ @@ffffff@333333@š™™™™™ @ @ffffff@š™™™™™@333333ó?@ÍÌÌÌÌÌü?š™™™™2@ÍÌÌÌÌÌ@š™™™™™@ÍÌÌÌÌÌ@ffffff@333333@333333ó?š™™™™™ @ÍÌÌÌÌÌü?š™™™™™@@š™™™™™@ÍÌÌÌÌÌ@ffffffæ?@@333333!@š™™™™™,@333333@ffffff@š™™™™™(@333333@ÍÌÌÌÌÌ@333333%@š™™™™™ @š™™™™™#@ÍÌÌÌÌÌ@ÍÌÌÌÌÌü?@ÍÌÌÌÌÌ@ @333333û?š™™™™™@333333ó?š™™™™™@š™™™™™@@333333@ffffff@333333!@ @ð?ø?ÍÌÌÌÌÌ@333333@ø?!@ð?@š™™™™™@333333$@@@š™™™™™ @ @333333@ÍÌÌÌÌÌ@333333@333333 @@@ffffff@ÍÌÌÌÌÌü?ffffff@333333@ffffffþ?ffffff@@ÍÌÌÌÌÌ@ffffff @ffffffæ?333333@ffffff@ÍÌÌÌÌÌ@333333@333333ó?!@333333,@ÍÌÌÌÌÌ(@@š™™™™™!@333333@š™™™™™@ffffffæ?333333@š™™™™™@š™™™™™ñ?ÍÌÌÌÌÌì?ÍÌÌÌÌÌü?ÍÌÌÌÌÌì?@ÍÌÌÌÌÌ"@ffffff@@š™™™™™ù?ÍÌÌÌÌÌ@ffffff@ÍÌÌÌÌÌ@š™™™™™@ffffffæ?@333333 @@š™™™™™ @@š™™™™™é?š™™™™™@š™™™™™ @333333"@ffffff@ @333333@ffffff@ÍÌÌÌÌÌ@š™™™™™é?333333@@ÍÌÌÌÌÌü?ÍÌÌÌÌÌ@ÍÌÌÌÌÌì?$@ffffffþ?ÍÌÌÌÌÌ @@ffffff @š™™™™™"@@@!@333333ó?š™™™™™@ffffff@š™™™™™@ffffff@ÍÌÌÌÌL6@ffffff @ffffff@@š™™™™™@ffffff@ffffff@š™™™™™é?ffffff)@@333333û?333333û?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ)@ @š™™™™™ñ?š™™™™™ù?ÍÌÌÌÌÌ@ffffff#@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333@ÍÌÌÌÌÌ@333333@ø?333333@š™™™™™ @ffffff@š™™™™™ @@š™™™™™@ffffff@ÍÌÌÌÌÌ@@ffffff@ÍÌÌÌÌÌ@ffffff@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ%@ffffff@ÍÌÌÌÌÌ!@ÍÌÌÌÌÌ@ffffffþ?333333 @š™™™™™@ÍÌÌÌÌÌü?ÍÌÌÌÌÌ@€<@ð?ffffff$@333333@ffffffþ?ffffff@ @š™™™™™ @š™™™™™@ @333333ó?š™™™™™ @ffffff@ffffff@@ffffff@ffffff@ð?ffffff@'@ÍÌÌÌÌÌ@@ÍÌÌÌÌÌ@š™™™™™ @333333 @ÍÌÌÌÌÌ@ffffff@333333 @š™™™™™@ffffff @ÍÌÌÌÌÌ@333333@333333@ffffffþ?ffffffþ?à?ffffff @ffffff3@333333@333333'@š™™™™™Àš™™™™™ÀffffffÀš™™™™™ÀÀfffff¦EÀ€1ÀÍÌÌÌÌÌ!Àš™™™™™À!ÀÍÌÌÌÌÌ)ÀÍÌÌÌÌÌÀš™™™™™ À À333333ÀÍÌÌÌÌÌ'Àffffff#Àš™™™™™*ÀÀffffffÀffffff#ÀffffffÀ333333%Àffffff%À33333³0Àš™™™™™'À333333Àš™™™™™!ÀffffffÀffffffÀ333333&ÀffffffÀÍÌÌÌÌÌ&Àš™™™™™Àffffff"ÀÀÍÌÌÌÌÌÀš™™™™™ Àš™™™™™À333333!À333333(À333333'ÀÍÌÌÌÌÌ!À333333ÀffffffÀffffffÀffffff,ÀÍÌÌÌÌÌÀffffff!À333333ÀÍÌÌÌÌÌÀffffff)Àffffff!Àš™™™™™0Àš™™™™™.Àš™™™™™%Àffffff&ÀÍÌÌÌÌÌÀ333333ÀÍÌÌÌÌÌÀ333333$ÀÍÌÌÌÌÌ&À333333ÀffffffÀffffffÀš™™™™™ÀÀš™™™™™'À&ÀffffffÀffffff"ÀÍÌÌÌÌÌ%Àš™™™™™ À)Àffffff#ÀÍÌÌÌÌÌ'ÀÍÌÌÌÌÌ$À)ÀÍÌÌÌÌÌÀš™™™™7À0Àffffff,Àš™™™™™ ÀffffffÀffffff&ÀÍÌÌÌÌÌÀÀš™™™™™ Àš™™™™™ ÀffffffÀš™™™™™ Àš™™™™™!À333333 À333333-Àffffff*ÀÍÌÌÌÌÌ'ÀÀffffff+À333333À333333(À33333³0ÀÀ333333Àš™™™™™#À5ÀÍÌÌÌÌÌ0ÀÍÌÌÌÌÌÀffffff2ÀffffffÀ"À333333 ÀÍÌÌÌÌÌ À333333 Àš™™™™™À!ÀffffffÀ333333û¿ffffffÀÀffffff"À333333&À À(Àš™™™™™À'À333333+À-Àš™™™™™!À)ÀÍÌÌÌÌÌ0Àš™™™™™ù¿ffffffÀ333333"Àš™™™™™ÀÍÌÌÌÌÌ#À33333³1ÀÍÌÌÌÌÌ!ÀÍÌÌÌÌÌÀÀÍÌÌÌÌÌÀffffff"À333333!À333333À333333"ÀÀ333333 ÀÍÌÌÌÌÌ+À$ÀÀffffffÀ&Àffffff/ÀÍÌÌÌÌÌÀš™™™™™À!Àš™™™™™Àffffff'ÀÍÌÌÌÌÌ"À4Àffffff&ÀÀ33333³3ÀÀ333333%À333333!À333333À"ÀÀffffffÀffffff+ÀÍÌÌÌÌL1ÀÍÌÌÌÌÌÀÍÌÌÌÌÌ,À333333(Àš™™™™™À4ÀÍÌÌÌÌÌ&À333333(À,À333333+Àš™™™™™ÀÍÌÌÌÌÌÀÍÌÌÌÌÌ$ÀÍÌÌÌÌÌÀš™™™™™-ÀÍÌÌÌÌÌ À@AÀ/ÀÀ333333ÀÍÌÌÌÌL2À333333ÀÍÌÌÌÌÌÀš™™™™™Àfffffæ7Àš™™™™™)ÀÀ3333331Àš™™™™™)Àffffff)Àfffffæ5ÀffffffÀÍÌÌÌÌÌ!À$Àffffff À333333 Àš™™™™™Àš™™™™™ÀÍÌÌÌÌÌ À333333ÀÍÌÌÌÌÌ#À+ÀffffffÀ333333)ÀffffffÀ"ÀÍÌÌÌÌL=À333333)Àffffff$Àš™™™™™$Àš™™™™™-ÀffffffÀš™™™™™ À333333Àffffff%ÀÍÌÌÌÌÌ'ÀÍÌÌÌÌÌÀÍÌÌÌÌÌ'À€1ÀffffffÀš™™™™™À)ÀÀš™™™™™"Àš™™™™™/À€2À$À/ÀÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀš™™™™™À0À333333À333333ÀffffffÀš™™™™™ÀffffffÀffffff ÀÍÌÌÌÌÌÀ333333"ÀffffffÀffffff"Àš™™™™™ÀffffffÀÍÌÌÌÌÌ%À Àffffff'À#À333333Àš™™™™™ÀÍÌÌÌÌÌÀÀÍÌÌÌÌÌÀÍÌÌÌÌÌ%ÀÍÌÌÌÌÌ#ÀÍÌÌÌÌÌÀ!ÀÍÌÌÌÌÌ$ÀÍÌÌÌÌÌÀ333333ÀÍÌÌÌÌÌ1Àffffff!Àš™™™™™Ù¿š™™™™™*Àš™™™™™#Àš™™™™™$À333333À333333Àš™™™™™!ÀÀ À(Àš™™™™™ À333333,À"Àffffffþ¿ ÀÀÀ)Àš™™™™™%À(ÀÍÌÌÌÌÌ&ÀÍÌÌÌÌÌ2À2Àffffff À333333Àš™™™™™ Àffffff'ÀÀÍÌÌÌÌÌ"Àš™™™™™ÀÍÌÌÌÌÌ ÀÍÌÌÌÌÌ/À.ÀÍÌÌÌÌÌô¿š™™™™™ÀÀffffffÀ333333Àffffff&ÀÍÌÌÌÌÌ*ÀffffffÀ333333*À333333À À)Àš™™™™™+À333333&ÀffffffÀ333333Àš™™™™™'ÀÍÌÌÌÌÌ)Àffffff,Àš™™™™™/Àffffff-Àš™™™™™7À>Àš™™™™™!ÀÍÌÌÌÌÌ2ÀffffffÀffffffÀ333333Àffffff-ÀÍÌÌÌÌÌ*ÀÍÌÌÌÌÌ#ÀÍÌÌÌÌÌ-À;Àš™™™™™%ÀÍÌÌÌÌÌÀffffff(ÀÀÍÌÌÌÌÌÀffffffÀffffffÀ#ÀÍÌÌÌÌÌ À$À333333À€5Àš™™™™™+À'Àš™™™™™À€=À333333ÀÍÌÌÌÌÌ Àfffffæ0ÀÀš™™™™Y@Àffffff,ÀÍÌÌÌÌÌ$Àø¿ÍÌÌÌÌÌÀ333333'ÀÍÌÌÌÌL2Àffffff%Àš™™™™™+Àš™™™™™À Àffffff#À333333/ÀÀffffff#À€3ÀÍÌÌÌÌÌÀ+Àffffff+À3333335ÀÍÌÌÌÌÌ+À.À333333#ÀÍÌÌÌÌÌ0ÀÍÌÌÌÌÌÀ333333$Àš™™™™™(ÀffffffÀš™™™™™CÀ(Àffffff!À333333"Àš™™™™™À333333À333333,À333333%ÀffffffÀÍÌÌÌÌÌ.ÀÀ333333À+ÀÀffffff$Àš™™™™™"À)ÀÍÌÌÌÌÌü¿ffffffö¿*À333333 Àš™™™™™Àffffff ÀÍÌÌÌÌÌ!ÀffffffÀ,ÀÀš™™™™™@@@ø?ÍÌÌÌÌÌô?š™™™™™é?@333333 @ÍÌÌÌÌÌü?ÍÌÌÌÌÌ@ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?š™™™™™Ù?333333 @333333ó?@š™™™™™ù?š™™™™™@@ÍÌÌÌÌÌ@ffffffö?ffffffæ?ÍÌÌÌÌÌì?ÍÌÌÌÌÌ@š™™™™™ @ÍÌÌÌÌÌ-@ð?ÍÌÌÌÌÌ@š™™™™™ù?ÍÌÌÌÌÌ@ð?ÍÌÌÌÌÌÀš™™™™™ñ¿Àš™™™™™É?š™™™™™é?à?à¿ffffff濚™™™™™é?333333ã¿333333ã?࿚™™™™™é?š™™™™™É¿š™™™™™É?š™™™™™¹?333333Ó?࿚™™™™™é¿š™™™™™Ù¿333333Ó?š™™™™™¹¿š™™™™™É¿333333Ó¿š™™™™™Ù?333333Ó¿333333Ó?š™™™™™ñ?š™™™™™ù¿š™™™™™Ù?š™™™™™É¿š™™™™™É?š™™™™™¹?š™™™™™¹?à?š™™™™™Ù¿333333Ó?š™™™™™¹?š™™™™™¹?ffffffö¿333333Ó?š™™™™™Ù¿333333Ó¿š™™™™™É?333333Ó¿à?333333Ó¿š™™™™™¹?333333Ó?333333ӿ࿚™™™™™¹¿š™™™™™é?ffffff濚™™™™™Ù¿š™™™™™É?š™™™™™É?š™™™™™¹?š™™™™™É¿š™™™™™É?š™™™™™¹?à?333333Ó?š™™™™™É?333333Ó?š™™™™™¹¿š™™™™™É¿333333Ó?333333Ó?333333Ó?333333Ó?š™™™™™Ù¿à¿š™™™™™¹?š™™™™™¹?š™™™™™Ù¿333333ã¿ffffffÀš™™™™™¹¿š™™™™™É¿š™™™™™é¿š™™™™™Ù¿š™™™™™Ù¿333333Ó¿š™™™™™É¿333333Ó?š™™™™™É¿333333ã?333333Ó?333333Ó¿333333Ó?š™™™™™Ù¿333333Ó?à?ffffffæ¿333333Ó?š™™™™™¹¿š™™™™™ñ¿333333㿚™™™™™É¿š™™™™™É?333333Ó¿333333Ó?ffffffæ?333333Ó¿š™™™™™Ù?333333Ó?333333Ó¿š™™™™™¹?š™™™™™¹¿333333㿚™™™™™¹?ffffffæ?š™™™™™É¿š™™™™™Ù¿š™™™™™Ù?š™™™™™É?š™™™™™¹?š™™™™™Ù?ffffff濚™™™™™Ù¿333333ã¿ÍÌÌÌÌÌì?š™™™™™¹¿à¿š™™™™™¹?ffffff濚™™™™™é?š™™™™™É?š™™™™™Ù?š™™™™™é?333333Ó?š™™™™™¹?š™™™™™Ù?ffffffæ?š™™™™™é¿333333Ó?š™™™™™¹?š™™™™™Ù?š™™™™™Ù¿333333Ó?š™™™™™¹¿š™™™™™¹?à¿333333Ó¿333333Ó?š™™™™™É¿š™™™™™Ù?š™™™™™¹?š™™™™™É?333333Ó?š™™™™™É?333333ã¿333333Ó?š™™™™™É?333333ã?333333ã?࿚™™™™™¹?333333Ó?š™™™™™É¿à?š™™™™™Ù¿š™™™™™¹?š™™™™™Ù?333333Ó?333333Ó?ffffff濚™™™™™É¿à?333333Ó?š™™™™™É?࿚™™™™™¹?š™™™™™¹?š™™™™™É?ffffffö¿ÍÌÌÌÌÌ쿚™™™™™É?š™™™™™é?š™™™™™É?š™™™™™¹?š™™™™™é?š™™™™™É¿š™™™™™É?333333Ó?333333ó?š™™™™™É?ÍÌÌÌÌÌì?š™™™™™¹¿333333Ó¿à?š™™™™™Ù¿š™™™™™¹?ÍÌÌÌÌÌ쿚™™™™™¹?à¿333333Ó?333333Ó¿š™™™™™Ù?š™™™™™É?š™™™™™ñ?333333Ó?ÍÌÌÌÌÌ쿚™™™™™Ù¿333333Ó¿š™™™™™¹?333333ã¿ffffffæ¿ffffff濚™™™™™É¿à?𿚙™™™™É?࿚™™™™™Ù?š™™™™™É¿333333Ó¿à¿333333Ó?333333Ó?333333㿚™™™™™¹?ffffff濚™™™™™Ù¿ffffffæ?š™™™™™¹?š™™™™™¹¿š™™™™™Ù?š™™™™™¹?ÍÌÌÌÌÌ쿚™™™™™¹?333333ã¿à?š™™™™™¹?ffffffæ?333333ã¿ð?š™™™™™é?š™™™™™É?š™™™™™¹¿333333Ó¿š™™™™™¹?333333Ó?333333ã?š™™™™™¹?š™™™™™É¿š™™™™™¹?š™™™™™¹¿š™™™™™é¿à?š™™™™™¹¿š™™™™™¹?à?š™™™™™É¿š™™™™™¹?š™™™™™¹¿š™™™™™Ù¿š™™™™™¹?333333Ó¿ffffffæ¿333333Ó?࿚™™™™™¹?à?š™™™™™Ù?à?333333Ó?š™™™™™¹?š™™™™™¹?š™™™™™É?333333Ó?333333Ó¿š™™™™™Ù?š™™™™™É?333333Ó?š™™™™™É¿š™™™™™É¿333333㿚™™™™™¹?ffffffæ¿ffffff濚™™™™™Ù¿ð¿š™™™™™Ù¿š™™™™™¹?333333㿚™™™™™É?333333ã?࿚™™™™™É¿333333Ó¿š™™™™™É?333333Ó?333333Ó¿š™™™™™É?š™™™™™É¿š™™™™™Ù¿š™™™™™Ù?š™™™™™Ù¿333333Ó¿š™™™™™Ù?š™™™™™É?ffffff濚™™™™™É?š™™™™™¹¿333333Ó¿š™™™™™Ù¿š™™™™™É?333333Ó¿ffffffæ¿à¿š™™™™™Ù¿333333Ó?𿚙™™™™É¿333333ã?š™™™™™É?ÍÌÌÌÌÌì?ÍÌÌÌÌÌ쿚™™™™™É?ð?333333ã?š™™™™™É?ð?333333ã¿à?333333ã?333333Ó?š™™™™™ñ¿š™™™™™É?ÍÌÌÌÌÌ쿚™™™™™¹¿š™™™™™¹?333333Ó?š™™™™™¹?š™™™™™é?š™™™™™Ù¿ÍÌÌÌÌÌ쿚™™™™™ñ¿š™™™™™¹?š™™™™™¹?š™™™™™Ù¿à?š™™™™™é¿333333Ó?333333㿚™™™™™¹?š™™™™™É?š™™™™™É¿š™™™™™É?š™™™™™ù¿š™™™™™É¿333333Ó?ÍÌÌÌÌÌì?š™™™™™é¿š™™™™™É?š™™™™™Ù¿š™™™™™É?š™™™™™É?𿚙™™™™Ù?š™™™™™É?333333ã?ffffff濚™™™™™Ù?š™™™™™Ù?š™™™™™Ù?à?š™™™™™¹?š™™™™™Ù?š™™™™™Ù¿š™™™™™É?333333㿚™™™™™Ù?333333Ó?à?à?š™™™™™¹¿š™™™™™É?š™™™™™Ù?š™™™™™¹?࿚™™™™™Ù¿ffffffæ?ð?š™™™™™É¿š™™™™™é¿à¿333333ã¿333333Ó?š™™™™™É?333333Ó¿š™™™™™¹?š™™™™™É?š™™™™™É¿š™™™™™Ù?à?ffffffæ?š™™™™™¹?à?š™™™™™É?š™™™™™Ù¿ffffff @à?š™™™™™Ù?333333@@333333À333333@ÍÌÌÌÌÌ@333333 @@@ÍÌÌÌÌÌ@333333@š™™™™™ÀÍÌÌÌÌÌì¿333333ã?ÍÌÌÌÌÌô?333333 @ffffff@š™™™™™é?333333ã?ffffff@š™™™™™ù?ÍÌÌÌÌÌ@ @š™™™™™@š™™™™™é?ð?ffffff@š™™™™™@ffffffö?š™™™™™@ÍÌÌÌÌÌô?333333@333333û?ÍÌÌÌÌÌ @ffffff@ÍÌÌÌÌÌ@333333@ffffffö?@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ffffffþ?333333@ffffff@š™™™™™¹¿ÍÌÌÌÌÌ@333333û?333333 @š™™™™™ñ?š™™™™™¹¿š™™™™™ @š™™™™™¹¿@@ø?ÍÌÌÌÌÌ@š™™™™™@ffffff@ÍÌÌÌÌÌ@333333@ÍÌÌÌÌÌô?ÍÌÌÌÌÌ@ffffff@ÍÌÌÌÌÌ@š™™™™™@ÍÌÌÌÌÌ @333333ó?ffffffö?@ÍÌÌÌÌÌì?ÍÌÌÌÌÌü?š™™™™™@š™™™™™@@333333@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ffffffæ?ffffffþ?š™™™™™ù?ffffff @ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ffffff@ÍÌÌÌÌÌ@#@ÍÌÌÌÌÌ@@$@ÍÌÌÌÌÌ@333333Ó?š™™™™™@@ffffffþ?@ÍÌÌÌÌÌ@333333Ó?ÍÌÌÌÌÌô?š™™™™™Ù?ð?š™™™™™@ÍÌÌÌÌÌü?š™™™™™É¿ffffffö?ÍÌÌÌÌÌ@@ÍÌÌÌÌÌü?ø?ffffff@333333@ @@333333@ÍÌÌÌÌÌì¿@333333@@š™™™™™@ffffff@ÍÌÌÌÌÌ@333333 @333333û?š™™™™™@333333ã?@š™™™™™@333333@ffffff@333333@š™™™™™ @š™™™™™ù?ø?ÍÌÌÌÌÌ@@333333Ó?@ffffff @š™™™™™ñ?š™™™™™Ù?ÍÌÌÌÌÌ@@à¿333333@š™™™™™@@@333333@ @ffffffæ?š™™™™™É¿š™™™™™é¿š™™™™™ @š™™™™™ @š™™™™™é?ð?@333333ã?333333û?š™™™™™ @à?à?š™™™™™@@ÍÌÌÌÌÌ@333333Ó¿333333û?@š™™™™™@ÍÌÌÌÌÌü?ÍÌÌÌÌÌ@333333Ó?@ffffff@ffffffæ?š™™™™™ù¿š™™™™™ñ?333333 @ @@333333Ó?@@ÍÌÌÌÌÌü?@š™™™™™Ù?ffffffþ?ffffff@ð?333333@@333333ó?ffffffþ?ÍÌÌÌÌÌ@à?@ÍÌÌÌÌÌ@š™™™™™ù?ÍÌÌÌÌÌ@@333333@š™™™™™É¿ffffff@ÍÌÌÌÌÌ@@š™™™™™@ffffff @ffffff@š™™™™™ @š™™™™™ @š™™™™™é¿š™™™™™ À@ffffff!@ffffff@ffffff@ffffff@š™™™™™ù?š™™™™™ñ?ÍÌÌÌÌÌ@ø?š™™™™™É¿ÍÌÌÌÌÌ @@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌô?333333@@š™™™™™@@ffffffþ?ffffff @333333û?@ffffff@š™™™™™é?@š™™™™™ñ¿š™™™™™@ÍÌÌÌÌÌ @ffffffæ? @š™™™™™ù?ffffffö?š™™™™™ù?š™™™™™ù?333333 @333333@š™™™™™ù?@à¿ffffff@333333ã¿333333û?333333 @@333333 @ÍÌÌÌÌÌ@333333ó?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@š™™™™™ @ffffffæ?ÍÌÌÌÌÌì?333333@š™™™™™ñ?ffffffö? @ @š™™™™™@333333 @š™™™™™@ffffff@š™™™™™É?333333û?ÍÌÌÌÌÌô?ÍÌÌÌÌÌ@333333㿚™™™™™@ÍÌÌÌÌÌ@333333@ffffff@ffffff@š™™™™™ @ÍÌÌÌÌÌ@333333ã?333333@ÍÌÌÌÌÌì?š™™™™™@ð?ÍÌÌÌÌÌì?š™™™™™@š™™™™™@@š™™™™™Ù?ffffff@ffffffÀš™™™™™ÀÍÌÌÌÌÌü?ffffff!@@ÍÌÌÌÌÌì?š™™™™™ù?ffffffö?333333ã?ffffffö?@ÍÌÌÌÌÌ@333333@š™™™™™ @ffffff@ÍÌÌÌÌÌ@š™™™™™ @ÍÌÌÌÌÌÀ@ffffffþ?ÍÌÌÌÌÌÀ@š™™™™™@š™™™™™É¿ÍÌÌÌÌÌ@@š™™™™™é?ffffff @ffffff@ffffffö?ÍÌÌÌÌÌÀš™™™™™@š™™™™™@ÍÌÌÌÌÌì?š™™™™™ @ffffff@@ffffff@@ffffffæ?š™™™™™ñ?š™™™™™@@333333 @š™™™™™@š™™™™™é?333333@ffffffæ?š™™™™™Ù¿ffffffö?@@333333ã?@@333333û?333333û?ffffffö¿333333@ÍÌÌÌÌÌü?333333@ffffffþ?333333 @333333@ø?ffffff@š™™™™™ @š™™™™™ñ?333333@@ffffff@333333"@š™™™™™ñ¿ @ffffff*@ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?ffffff@ffffff@333333Ó?333333Ó?ffffffö?ffffffþ?333333!Àš™™™™™ñ?ÍÌÌÌÌÌ@@333333@333333@ÍÌÌÌÌÌ @ffffff@@ÍÌÌÌÌÌ'@ffffffö?ffffffö?š™™™™™@333333ã?ÍÌÌÌÌÌ&@ffffff@š™™™™™"@ÍÌÌÌÌÌ@š™™™™™é?333333û?š™™™™™é?333333@ffffffæ?š™™™™™@š™™™™™Àffffff @333333ã¿ÍÌÌÌÌÌ@ @š™™™™™Ù¿ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@@@š™™™™™ñ?š™™™™™ù?ÍÌÌÌÌÌ@333333 @333333ó?ø?ffffff@𿚙™™™™ñ?š™™™™™@š™™™™™@š™™™™™@š™™™™™@ÍÌÌÌÌÌ@ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?ffffff@ÍÌÌÌÌÌÀ333333 @ð¿333333ã¿@ffffffö?333333@ÍÌÌÌÌÌ@333333@š™™™™™ñ?ffffff @ÍÌÌÌÌÌ @ffffffæ?ø?333333@ÍÌÌÌÌÌ@ffffff@ø?š™™™™™!@À@ÍÌÌÌÌÌ@333333)Àffffff0Àfffffæ4@ffffff)ÀÍÌÌÌÌÌ(Àš™™™™Ù[Àfffff&GÀ€4ÀÍÌÌÌÌÌ+Àš™™™™™%Àfffff¦AÀ333333$ÀÍÌÌÌÌÌü¿ffffff'@à?š™™™™™6Àffffff3Àš™™™™4ÀÍÌÌÌÌÌDÀÍÌÌÌÌÌ@ffffff Àffffffö¿ÍÌÌÌÌÌì?ffffff+À33333³4ÀIÀ€9À)Àfffffæ0À€3ÀÍÌÌÌÌÌÀÍÌÌÌÌÌ(Àffffff1Àš™™™™™+Àš™™™™™7ÀÍÌÌÌÌÌ0Àš™™™™1À333333 Àffffff"ÀÍÌÌÌÌÌÀffffffö¿4ÀÍÌÌÌÌÌ2À&Àffffff+À333333À333333)ÀÍÌÌÌÌÌ%À33333³5ÀfffffæBÀš™™™™™ ÀffffffÀ333333û?!Àffffff8Àfffffæ2ÀffffffCÀÍÌÌÌÌŒBÀ5Àš™™™™:À333333"@ÍÌÌÌÌÌÀš™™™™™!Àš™™™™6À"À333333,À+Àffffffæ¿ÍÌÌÌÌÌ9ÀÀffffff"À333333:À333333;Àffffff$ÀÍÌÌÌÌL3À&Àš™™™™YGÀffffff(Àffffff<ÀÍÌÌÌÌL8Àš™™™™AÀfffffæ1Àfffff¦AÀffffff"@ffffffGÀš™™™™ihÀÍÌÌÌÌL9ÀÍÌÌÌÌÌ<Àfffff¦\ÀÀOÀffffff=À333333 Àfffffæ;À333333Ó¿ffffff@)@ÍÌÌÌÌÌ @333333 Àš™™™™™=Àš™™™™™ù¿ffffff@fffffæ7Àš™™™™<Àš™™™™™AÀÍÌÌÌÌÌô?š™™™™™2À€6À333333&À,ÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀffffffÀš™™™™0ÀÍÌÌÌÌŒFÀfffff¦BÀ!ÀÍÌÌÌÌ BÀš™™™™™À2À333333Àffffffæ?333333/ÀÍÌÌÌÌÌ0À333333À€0Àš™™™™™¹¿333333/Àš™™™™™@/Àffffff*À3Àffffff-Àš™™™™3ÀffffffÀfffffæ>Àš™™™™2À333333%ÀÍÌÌÌÌÌ$Àffffff*ÀÍÌÌÌÌL?À333333@#ÀÍÌÌÌÌÌ À333333+Àfffffæ4ÀÍÌÌÌÌ AÀ333333*ÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀ333333-Àfffffæ2ÀÍÌÌÌÌÌ0Àffffff$Àfffffæ0ÀÍÌÌÌÌL1Àš™™™™™+Àš™™™™5Àffffff8ÀÍÌÌÌÌÌ"À333333Àš™™™™7À33333³:ÀÀÀ€:À333333#À3333332À333333/Àš™™™™=Àffffff=À333333#À33333³HÀ333333%@33333³>À€1Àš™™™™™+Àffffff'Àffffff@$Àš™™™™Y@À33333³AÀffffffÀfffffæ=À€9ÀÍÌÌÌÌL2@ÍÌÌÌÌŒIÀ€<Àš™™™™™=Àfffff†RÀÍÌÌÌÌ FÀÍÌÌÌÌÌCÀ=@š™™™™™0À3À333333À33333sEÀš™™™™™ÀÍÌÌÌÌìPÀÍÌÌÌÌL?Àš™™™™™!Àš™™™™™À33333sDÀffffffÀÍÌÌÌÌÌ_ÀÀÍÌÌÌÌÌ"À33333“]Àš™™™™9ÀÀÍÌÌÌÌÌ Àš™™™™0À€:À€MÀffffff@š™™™™™Àš™™™™™6Àš™™™™™8ÀÍÌÌÌÌÌLÀÍÌÌÌÌÌ-ÀÀš™™™™™@$Àš™™™™™ñ?ÍÌÌÌÌÌ@€9ÀÍÌÌÌÌÌ:Àfffffæ<ÀÍÌÌÌÌÌ=Àš™™™™™@333333Àfffffæ7@ffffff(ÀffffffHÀš™™™™™"@%À3333335À3333337À333333û¿333333@š™™™™™1À5Àffffff'Àffffff7À)À€4À€GÀffffffÀÍÌÌÌÌÌ3Àffffff@š™™™™1Àffffffö?ffffffAÀ@MÀÍÌÌÌÌÌ1Àfffffæ;Àš™™™™™5À@NÀ7À33333³<Àffffff0À333333Ó¿š™™™™™¹¿ÍÌÌÌÌÌ-Àš™™™™7Àffffff%ÀÍÌÌÌÌÌ#Àffffffö¿š™™™™™@333333ÀÍÌÌÌÌÌ Àffffff @33333³1ÀÍÌÌÌÌÌÀ333333+ÀÍÌÌÌÌÌ+ÀÍÌÌÌÌÌü¿š™™™™™;À Àffffff9À3333333À333333@ÍÌÌÌÌÌÀ3333330À333333@š™™™™™+À333333+Àš™™™™=Àš™™™™3À€0ÀÀffffff)ÀÍÌÌÌÌÌ*ÀÍÌÌÌÌÌH@*ÀÍÌÌÌÌÌ9À3333337@ÍÌÌÌ̼`@333333#@@3333334À€8ÀÍÌÌÌÌÌÀfffffæ2Àffffff'Àš™™™™™/ÀÍÌÌÌÌì[À333333)Àffffff3ÀÍÌÌÌÌÌ>À€9À33333óAÀ3333331À333333%@š™™™™™3À333333À%@ÍÌÌÌÌÌ-ÀffffffÀ@@ÀÍÌÌÌÌL6Àfffff&AÀffffff1Àš™™™™™4À333333ó¿š™™™™™À333333@3333339À@ÍÌÌÌÌÌ,Àffffffæ¿ffffff/ÀfffffæEÀffffff6À,@ÍÌÌÌÌÌ%ÀÀš™™™™™+ÀÀš™™™™™:À333333?À€1Àø¿Àffffff%@š™™™™9À33333³0Àffffff@333333,ÀÍÌÌÌÌÌÀš™™™™™#À€FÀ0Àfffff¦BÀffffff3À33333sGÀš™™™™™1À€9À33333sIÀÍÌÌÌÌ GÀ333333Àš™™™™¹SÀÍÌÌÌÌ GÀ Àš™™™™™(À333333ó??ÀÍÌÌÌÌLBÀš™™™™™@33333óCÀ `À33333³6Àfffffæ;Àfffffæ0Àffffffö?333333%Àš™™™™™'ÀÍÌÌÌÌÌÀÀ33333³@@'À333333UÀÍÌÌÌÌÌ1Àš™™™™™@ÍÌÌÌÌLLÀ€@Àffffff>ÀÍÌÌÌÌÌ/ÀfffffF`ÀffffffÀ)ÀÀEÀÍÌÌÌÌÌ@€MÀÍÌÌÌÌÌ5À°bÀÀÍÌÌÌÌÌ&@ÍÌÌÌÌÌ-ÀÍÌÌÌÌÌÀš™™™™™8Àš™™™™™?À€1Àš™™™™™%ÀÍÌÌÌÌÌ!@3333334Àffffff-À€7À€2Àffffff0À333333?@ffffff-Àffffff @ÍÌÌÌÌÌ0Àš™™™™™JÀ€5Àffffff8Àfffff¦HÀš™™™™™%ÀÍÌÌÌÌLBÀš™™™™™+ÀÍÌÌÌÌÌ5ÀÍÌÌÌÌÌ:À333333/ÀðbÀ5Àš™™™™2À33333³5ÀÍÌÌÌÌÌ4Àffffffö¿š™™™™™@ÍÌÌÌÌÌ4Àffffff(@ÍÌÌÌÌÌ#À333333?ÀÍÌÌÌÌÌÀÀ33333³BÀš™™™™™À3333339Àš™™™™™.Àš™™™™™5Àffffff@ÍÌÌÌÌÌ@ÍÌÌÌÌL9ÀÍÌÌÌÌL7À333333+À€6Àš™™™™™,Àš™™™™9R@ÍÌÌÌÌÌ0À+@ffffff.@333333ó?ÍÌÌÌÌÌÀ2@333333!@fffffÆWÀÍÌÌÌÌÌô¿@$@9@š™™™™YKÀffffff1Àš™™™™™@š™™™™™6@ÍÌÌÌÌŒA@33333ó@@ffffff(À333333(À3333338@š™™™™4@ÍÌÌÌÌÌ3@ÍÌÌÌÌÌ@@ÍÌÌÌÌL7@ÍÌÌÌÌÌÀ333333 Àffffff À@š™™™™™¹?333333 @333333Ó¿fffff&C@š™™™™™@ÍÌÌÌÌL8@š™™™™™;@333333@333333@ø¿33333³@@ffffff,@33333³1@ÍÌÌÌÌÌ-@@@ffffff'À333333@ffffff@6@ÍÌÌÌÌÌÀ6Àš™™™™™$@š™™™™™#Àš™™™™™2@333333<@š™™™™™ @ffffff2@€@@ffffff@š™™™™4@333333@333333*À@A@š™™™™ÙF@333333-@ffffffÀÍÌÌÌÌÌ=@ffffffÀffffff#@33333³9@€2À%@€@@ÍÌÌÌÌŒA@ÍÌÌÌÌÌÀš™™™™2@š™™™™™ñ?ÍÌÌÌÌ A@ÍÌÌÌÌÌ1ÀÀÍÌÌÌÌÌ"ÀÍÌÌÌÌÌÀ333333@ffffff;@ ÀÍÌÌÌÌÌ>@333333=À`SÀC@š™™™™™K@ffffff7@ÍÌÌÌÌÌô?:ÀÍÌÌÌÌÌ#@š™™™™™Ù¿ffffff(@ÍÌÌÌÌL>@fffff¦F@ÍÌÌÌÌÌ&@ @:Àffffff@fffffæ;@š™™™™™@š™™™™=Àš™™™™6ÀÍÌÌÌÌÌ=@333333/@š™™™™™À@8@ÍÌÌÌÌL5@333333<@š™™™™™7@š™™™™™@ÍÌÌÌÌ KÀ3333337@ÍÌÌÌÌL3@#@ffffff2@7@33333³4@fffffæ=@333333Ó?š™™™™C@333333Ó?333333Ó?š™™™™™=@š™™™™™"@€5@ @š™™™™YA@ÍÌÌÌÌÌ@333333Àffffff"@ÍÌÌÌÌÌ:@š™™™™™ Àš™™™™™@ÍÌÌÌÌÌ0@ffffffö¿333333Àfffffæ@@ÀD@ÍÌÌÌÌÌ @,@ffffff0@ÍÌÌÌÌÌ"@ÍÌÌÌÌŒA@33333ó@@ÍÌÌÌÌÌ3@ @ffffff À7À33333³;@fffffæ1@333333Àš™™™™™ÀÍÌÌÌÌÌ@ffffffÀÍÌÌÌÌÌô?#@š™™™™™Àffffffö¿ffffff@ffffff'@33333³1@š™™™™™!À333333%ÀÍÌÌÌÌÌ'@ÍÌÌÌÌÌM@@€B@š™™™™™:À@A@š™™™™™@ÍÌÌÌÌÌ/@ÀDÀffffff"Àš™™™™2@ÍÌÌÌÌÌ3@ÍÌÌÌÌ O@ÍÌÌÌÌÌ ÀÍÌÌÌÌÌÀ333333@ÍÌÌÌÌÌ#@À5ÀfffffæD@€2ÀÍÌÌÌÌÌ-Àš™™™™™-@fffffæ>@ÍÌÌÌÌŒ@Àffffff,ÀÍÌÌÌÌÌD@ffffff'ÀÍÌÌÌÌÌ=@-@š™™™™™:Àš™™™™™'@ffffffö¿333333@š™™™™™&Àš™™™™™.@333333 Àš™™™™™&@š™™™™™MÀ7@"@333333@š™™™™™@ffffff-ÀÍÌÌÌÌLIÀš™™™™™@ÍÌÌÌÌLQ@€1@ÍÌÌÌÌŒF@1@333333@š™™™™™,À€AÀš™™™™™¹?333333À333333D@ffffff0@š™™™™<@š™™™™8@ÍÌÌÌÌÌ À-@33333³8@fffffæN@ÍÌÌÌÌÌG@3333331@33333sH@ÍÌÌÌÌÌ@š™™™™™3@š™™™™ùW@š™™™™™É?2@ÍÌÌÌÌ CÀ€2@ffffffA@333333%À€1@@ffffff(À333333ó?ÀffffffÀÍÌÌÌÌLA@333333Ó¿š™™™™™D@ÍÌÌÌÌÌ3À33333³C@ÍÌÌÌÌ EÀš™™™™™CÀ/@33333sF@ffffff0@š™™™™Ù@À333333$ÀÍÌÌÌÌÌ%@@€<@333333@333333À33333³;@ffffffþ¿š™™™™™Ù¿€A@€B@3333334@fffffæ8@€H@@š™™™™™ÀÍÌÌÌÌÌô?ffffffÀ@O@ÍÌÌÌÌÌ=Àš™™™™™&@š™™™™™:@ÍÌÌÌÌÌ4@33333³L@ÍÌÌÌÌÌ?@ffffff*@š™™™™™8@#ÀÍÌÌÌÌÌ @33333³0À@ÍÌÌÌÌÌÀÍÌÌÌÌÌ@:@ÍÌÌÌÌÌ"@ffffffP@!À0@!Àš™™™™9Y@9@š™™™™¹Z@333333@-ÀÍÌÌÌÌÌ!@ÍÌÌÌÌÌÀ333333ó¿ÍÌÌÌÌÌô¿33333sJÀÍÌÌÌÌÌ*@33333óC@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ!ÀÍÌÌÌÌÌ.@š™™™™3À333333!@333333$@ÍÌÌÌÌÌÀš™™™™™M@fffff&H@fffff&AÀffffffÀfffff¦H@333333Àš™™™™™5@3333336@ÍÌÌÌÌÌ@33333³DÀÍÌÌÌÌÌ@333333G@š™™™™™ ÀÍÌÌÌÌÌ?@$@3333331@š™™™™™?@33333³E@ÍÌÌÌÌÌô¿ÍÌÌÌÌÌ@ÍÌÌÌÌL=@€2@š™™™™™)@ÍÌÌÌÌÌ;@š™™™™™$À5@š™™™™™é?@š™™™™™(Àš™™™™8@33333sP@ffffff$À3333336@ffffff&@ÍÌÌÌÌL7ÀÍÌÌÌÌÌ@fffff¦HÀfffff&O@€;À333333'@333333Àfffff¦@@š™™™™7À333333@š™™™™™>ÀÍÌÌÌÌÌÀ333333@fffffæ9@ÍÌÌÌÌŒG@ÍÌÌÌÌÌ@fffffæM@333333À333333ÀÍÌÌÌÌŒQ@š™™™™™-À%À%@33333SR@ÍÌÌÌÌÌÀ#À333333@€2@€<Àð?33333³NÀÍÌÌÌÌÌ7@€H@š™™™™™.Àffffff$@3333335@333333&@333333B@š™™™™™@À?@ffffff @ÍÌÌÌÌLV@ÍÌÌÌÌÌ*@fffffæ1@š™™™™YJ@fffffæ1@ffffffæ?š™™™™™¹¿ÍÌÌÌÌ G@š™™™™9Àš™™™™™:@;ÀÍÌÌÌÌLC@š™™™™8ÀÍÌÌÌÌ A@33333SW@3333339À333333(@fffffæ^@fffff&D@fffffæ:@ffffffÀÍÌÌÌÌŒ@À@L@@š™™™™YAÀ333333@333333(@ÍÌÌÌÌÌ8ÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀ@`UÀÍÌÌÌÌLI@ffffff @ffffffÀ$Àfffffæ1@š™™™™YIÀffffff*@333333 @333333)ÀP@333333%@š™™™™™7@333333À3333332@333333.ÀÍÌÌÌÌL0@'@fffffæ0@fffffæ1@K@𿚙™™™6@333333Àš™™™™ÙR@ffffff)@fffffæO@š™™™™Ù@@š™™™™™¹¿ffffffæ?à¿@š™™™™™ñ?š™™™™™é?ø?š™™™™™Ù?ÍÌÌÌÌÌ쿚™™™™™¹¿ø?š™™™™™é?ø?š™™™™™ù?š™™™™™¹?š™™™™™ù?ffffffæ?ffffffþ?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?š™™™™™é?š™™™™™ù?š™™™™™ @š™™™™™É¿333333û¿333333ó?š™™™™™Ù?333333ã?š™™™™™É?ffffffæ?ÍÌÌÌÌÌì?š™™™™™¹?š™™™™™Ù?š™™™™™é?ffffffæ¿333333ã?333333ã?࿚™™™™™¹¿ÍÌÌÌÌÌì?š™™™™™É¿š™™™™™Ù¿ø?333333㿚™™™™™ñ?š™™™™™é?333333Ó?š™™™™™É?š™™™™™É?ÍÌÌÌÌÌì?333333ó?š™™™™™é?à?ð?š™™™™™¹?š™™™™™¹?š™™™™™É?333333Ó?š™™™™™¹?š™™™™™¹?š™™™™™Ù?š™™™™™¹?š™™™™™¹?ÍÌÌÌÌÌô?333333ã?š™™™™™é?ð?š™™™™™ @333333Ó¿à?ffffffæ?ÍÌÌÌÌÌì?333333ã?ffffffö?333333ã?š™™™™™¹?333333.@š™™™™™é¿š™™™™™ @333333 @@333333Ó?š™™™™™É?š™™™™™ù?ð¿333333û?࿚™™™™™ñ?š™™™™™¹?࿚™™™™™¹?ø?ø?ÍÌÌÌÌÌô?333333ã?ÍÌÌÌÌÌ@š™™™™™¹?š™™™™™@š™™™™™ñ¿š™™™™™Ù¿333333ã?ffffffæ?ffffffþ?ÍÌÌÌÌÌ@ffffffæ?333333û?ffffff濚™™™™™É¿333333Ó¿ð?š™™™™™ñ?à?š™™™™™Ù¿ffffffæ?š™™™™™É?333333Ó?333333ã?ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?š™™™™™É¿333333ó?à?à?ÍÌÌÌÌÌì?ð?333333ã¿ffffffæ?ÍÌÌÌÌÌô?333333ã?ffffffæ?333333Ó?ÍÌÌÌÌÌì?š™™™™™é?ÍÌÌÌÌÌô?ÍÌÌÌÌÌ@š™™™™™É?š™™™™™É?ffffffæ?333333@ffffffæ?ffffffæ?ffffffæ?š™™™™™¹?š™™™™™é?š™™™™™é?š™™™™™ñ?š™™™™™É?š™™™™™¹?š™™™™™É?š™™™™™é?š™™™™™¹¿š™™™™™É?ÍÌÌÌÌÌì?ø?š™™™™™é¿@š™™™™™¹?333333ó?š™™™™™@š™™™™™¹¿333333Ó¿š™™™™™Ù?š™™™™™É¿š™™™™™Ù¿š™™™™™¹?š™™™™™É¿333333ã?ÍÌÌÌÌÌô?ffffffæ?333333û¿š™™™™™é?š™™™™™¹?333333ó?@š™™™™™ñ?333333û?ø¿ffffffö?š™™™™™¹?ffffffö?333333Ó¿333333@š™™™™™é?࿚™™™™™¹¿à¿ffffffæ?333333@š™™™™™É?333333@ÍÌÌÌÌÌì?š™™™™™¹¿ÍÌÌÌÌÌ@š™™™™™@š™™™™™ñ?@Àà?š™™™™™ñ?š™™™™™¹?@333333ó¿ÍÌÌÌÌÌì¿333333ó?ffffffö?333333ã?333333Ó?ffffffæ?š™™™™™ù?@à?ÍÌÌÌÌÌì?š™™™™™Ù?333333û¿à?333333ó?ÍÌÌÌÌÌì?ffffffæ?š™™™™™ñ?ffffffþ¿333333ã?š™™™™™é¿š™™™™™ñ?ø?š™™™™™É¿à?333333Ó?š™™™™™@š™™™™™¹?š™™™™™¹?ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?ÍÌÌÌÌÌü?ÍÌÌÌÌÌì?ÍÌÌÌÌÌ@š™™™™™ñ?š™™™™™É?š™™™™™@š™™™™™é¿ffffffþ?š™™™™™¹?š™™™™™¹?333333ã?333333ó?š™™™™™¹¿ÍÌÌÌÌÌô?š™™™™™Ù?ffffff濚™™™™™¹?š™™™™™É¿333333ã?š™™™™™¹?à¿ffffffæ?š™™™™™Ù¿ð?ÍÌÌÌÌÌì?333333ó?š™™™™™é?ffffffæ?333333Ó¿333333Ó?333333Ó?ffffffæ?333333ã?333333ã?333333Ó¿333333Ó?ÍÌÌÌÌÌ@333333ã?š™™™™™Ù?š™™™™™¹?ffffffÀš™™™™™¹¿ffffffæ?ø?š™™™™™é¿š™™™™™Ù?333333Ó¿ÍÌÌÌÌÌ@ffffffö?ffffff @š™™™™™@š™™™™™¹¿š™™™™™¹?333333û?š™™™™™ù?à?ffffffæ¿à?333333ó?ÍÌÌÌÌÌô¿ð?ð?ÍÌÌÌÌÌì?ffffff @ÍÌÌÌÌÌ@š™™™™™É¿ffffffþ?333333㿚™™™™™Ù¿333333@š™™™™™¹¿ffffffæ¿ÍÌÌÌÌÌì¿333333ã?333333@333333@333333ó?š™™™™™¹?@ffffffæ¿ffffffö?š™™™™™@ÍÌÌÌÌÌ@333333ã?ÍÌÌÌÌÌ쿚™™™™™ñ?@š™™™™™¹?ffffffö?ð?à?š™™™™™Ù?à?ÍÌÌÌÌÌ@333333@ffffff@ÍÌÌÌÌÌü?333333Ó¿@š™™™™™À333333û¿@š™™™™™ñ?à?࿚™™™™™é?ø?333333@š™™™™™é¿ffffffþ?ÍÌÌÌÌÌ@š™™™™™ñ?ÍÌÌÌÌÌì¿à¿à?š™™™™™É¿š™™™™™É?š™™™™™É?ffffff À333333ã?ÍÌÌÌÌÌü?ÍÌÌÌÌÌô?š™™™™™¹?ÍÌÌÌÌÌ@à?333333 @ffffff@࿚™™™™™É¿ @ÍÌÌÌÌÌ@333333ã?ffffff@ÍÌÌÌÌÌô?à?333333@š™™™™™É?333333û?š™™™™™ñ?ð¿à?à?š™™™™™¹?š™™™™™ù?š™™™™™¹?333333Ó¿š™™™™™¹¿š™™™™™¹¿ÍÌÌÌÌÌ@à?333333ó?ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?333333@š™™™™™É?š™™™™™@333333ó¿š™™™™™É?š™™™™™¹?333333ã?%@ÍÌÌÌÌÌ@ÍÌÌÌÌÌì?ffffffþ?333333Ó?ÍÌÌÌÌÌô?ffffff濚™™™™™Ù?333333û?ffffff @š™™™™™É?š™™™™™ñ?ÍÌÌÌÌÌô?à?333333Ó?ffffffæ?333333ã?333333ã?š™™™™™é?333333@š™™™™™é?ffffffæ?ð?ð?ffffff濚™™™™™@ÍÌÌÌÌÌ쿚™™™™™é¿ÀÍÌÌÌÌÌì¿à¿ÍÌÌÌÌL6ÀÍÌÌÌÌL7Àffffffæ¿ð¿ð¿à¿33333s@À333333Àffffff3À333333ÀÍÌÌÌÌÌ Àffffff$À/À€=ÀÀÍÌÌÌÌÌ'ÀÍÌÌÌÌÌ*À2Àð¿%Àfffffæ1Àš™™™™1Àš™™™™™$ÀÍÌÌÌÌÌì¿ffffff%ÀÍÌÌÌÌÌ @333333@333333@"@!@ÍÌÌÌÌÌ3@š™™™™™ @ÍÌÌÌÌÌ@333333@333333@ÍÌÌÌÌÌ'@@ffffff@ÍÌÌÌÌÌ&@š™™™™™ @š™™™™™@ffffffö?333333û?@ÍÌÌÌÌÌ,@@333333,@!@ÍÌÌÌÌÌ%@š™™™™™@ffffff"@ffffff@@@ffffff@333333@ffffff@333333@š™™™™™ù?š™™™™™@ÍÌÌÌÌÌ@ffffff(@ÍÌÌÌÌÌ#@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ!@ÍÌÌÌÌÌ@ffffff@ffffff&@@ÍÌÌÌÌÌ@333333$@#@@333333@333333@3333332@ÍÌÌÌÌÌ#@ffffff0@ÍÌÌÌÌÌ @ffffff%@ffffff@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ffffff @@š™™™™™.@ffffff @š™™™™™@š™™™™™@ÍÌÌÌÌÌ!@333333@š™™™™™@ÍÌÌÌÌÌ/@@333333%@ffffff@+@ø?ÍÌÌÌÌÌ@@ÍÌÌÌÌL3@333333@ÍÌÌÌÌÌ @š™™™™™ @ffffff@ÍÌÌÌÌÌ.@333333@ÍÌÌÌÌÌ+@ÍÌÌÌÌÌ@@š™™™™™3@#@ÍÌÌÌÌÌô?š™™™™™ @@š™™™™™@ffffff @ÍÌÌÌÌÌ$@ffffff@š™™™™™,@ÍÌÌÌÌÌ@@ffffff@š™™™™™-@0@š™™™™™@333333@@ÍÌÌÌÌÌ(@,@@3333331@ffffff<@ffffff$@333333@š™™™™™@333333@ÍÌÌÌÌÌ@ffffff@ffffff)@ffffff @š™™™™™@(@333333(@@@š™™™™™ù?ffffff@š™™™™™%@333333@š™™™™™!@@ÍÌÌÌÌÌ @333333@ffffff@š™™™™™@333333#@ffffff@ÍÌÌÌÌÌ@ffffff@š™™™™™3@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ#@ffffff#@š™™™™3@ffffff @@š™™™™™@@333333#@ffffff"@š™™™™™@ @333333@333333"@@š™™™™™é?š™™™™™@ÍÌÌÌÌÌ@š™™™™™@-@333333)@ffffff @ffffff@ÍÌÌÌÌÌ@ÍÌÌÌÌL5@ÍÌÌÌÌÌ$@333333$@ÍÌÌÌÌÌü?ffffff@š™™™™™@ÍÌÌÌÌÌ%@š™™™™™&@ffffff@%@@33333³5@š™™™™™ @ÍÌÌÌÌÌ@ffffff@š™™™™™@š™™™™™/@ @š™™™™™"@š™™™™™(@333333@ffffff @@3333332@ÍÌÌÌÌÌ @ffffff"@ffffff@š™™™™™ù?à?ÍÌÌÌÌÌ @š™™™™5@ @ffffff@333333@ffffff@ÍÌÌÌÌÌ/@ÍÌÌÌÌL4@ÍÌÌÌÌÌ+@ffffff @333333@š™™™™™&@@ffffff!@ÍÌÌÌÌÌ%@š™™™™™@ffffff@@333333;@333333"@š™™™™™!@@333333#@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ'@ffffff@š™™™™™)@ÍÌÌÌÌÌ%@ÍÌÌÌÌÌ*@š™™™™™@333333@ @š™™™™™+@š™™™™™(@fffffæ9@333333@(@33333³2@333333%@333333@3333335@ÍÌÌÌÌÌ"@3333331@@š™™™™™@333333&@ÍÌÌÌÌÌ @š™™™™™@333333 @ÍÌÌÌÌÌô?ffffff)@ÍÌÌÌÌÌ @š™™™™™$@ÍÌÌÌÌÌ@ffffff@ @333333&@š™™™™™%@ÍÌÌÌÌÌ!@ffffff@ÍÌÌÌÌÌ@š™™™™™&@š™™™™™!@š™™™™™ @ffffff#@ffffff @ @"@ffffff(@333333&@333333&@ÍÌÌÌÌÌ%@@š™™™™™@š™™™™™ @333333@ffffff(@ffffff@333333@ffffff@333333@fffffæ1@"@ffffff@š™™™™™#@@333333 @š™™™™™@@š™™™™™(@š™™™™™'@@333333-@ffffff@ffffff1@fffffæ:@ÍÌÌÌÌÌ8@š™™™™™"@ffffff4@š™™™™™.@333333@ÍÌÌÌÌÌ@333333@š™™™™™#@ÍÌÌÌÌÌ%@ÍÌÌÌÌÌ@ffffff@333333@333333 @€7@$@š™™™™™ @"@0@333333$@€0@š™™™™™ @ffffff@333333"@ffffff$@1@š™™™™™@ffffff@ÍÌÌÌÌÌ*@ÍÌÌÌÌÌ*@š™™™™™@š™™™™™1@ffffff @!@ÍÌÌÌÌÌ&@#@ffffff@ffffff@ÍÌÌÌÌÌ@@333333@@š™™™™™@ÍÌÌÌÌÌ.@ÍÌÌÌÌÌ%@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ*@,@333333@ffffff$@š™™™™™@ffffff@333333.@'@ffffffö?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ+@ffffff@š™™™™5@2@š™™™™™@#@š™™™™™@ÍÌÌÌÌÌ0@ffffff@333333@ffffff6@@š™™™™™ù?š™™™™™@ÍÌÌÌÌÌ$@333333@ÍÌÌÌÌÌ/@@š™™™™™ @333333@@333333)@ffffff@333333!@-@@333333@ffffff@š™™™™™@ÍÌÌÌÌÌ)@@š™™™™™@ffffff@ffffff#@@#@ÍÌÌÌÌÌü?ÍÌÌÌÌÌ(@š™™™™™@@ÍÌÌÌÌÌ@ffffff/@@š™™™™™$@333333*@ @"@ÍÌÌÌÌÌ,@ÍÌÌÌÌÌ@š™™™™™#@33333³:@ÍÌÌÌÌÌ@ffffff1@333333.@ÍÌÌÌÌÌ @š™™™™™@333333@ffffff @333333@@š™™™™™@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333@333333@ÍÌÌÌÌÌ @ffffff@"@333333@!@ÍÌÌÌÌÌ,@ffffff@@ @%@!@š™™™™™@333333@333333@@ÍÌÌÌÌÌ@ffffff@333333@333333@ffffff @ffffff!@ÍÌÌÌÌÌô?ffffff(@ÍÌÌÌÌL2@š™™™™™!@33333³4@$ÀÍÌÌÌÌÌÀffffffÀš™™™™™Àffffff$À333333HÀ&À"Àš™™™™™$À333333Àš™™™™™2À333333"À333333Àš™™™™™ ÀÀffffff Àš™™™™™'ÀffffffÀÍÌÌÌÌL1Àš™™™™™À333333À33333³6ÀÀffffff$À333333 À33333³6À-ÀÍÌÌÌÌÌ ÀÍÌÌÌÌÌÀffffffÀffffffÀffffffÀ333333$ÀÍÌÌÌÌÌÀ333333$ÀffffffÀš™™™™™.Àš™™™™™ÀÍÌÌÌÌÌ ÀÀffffffÀÍÌÌÌÌÌ&À333333Àffffffö¿333333Àš™™™™™!À333333"À333333ÀffffffÀ333333*À%Àffffff*Àffffff$ÀÍÌÌÌÌÌ&ÀÍÌÌÌÌÌÀffffff!À+Àš™™™™™,À$À)ÀÀ333333ÀÍÌÌÌÌÌÀÍÌÌÌÌÌ ÀffffffÀš™™™™™%ÀÍÌÌÌÌÌÀffffffÀ333333&ÀÀÍÌÌÌÌÌ#ÀÍÌÌÌÌÌÀÀffffffÀÍÌÌÌÌÌ)Àš™™™™™ À333333,À333333)Àffffff"À333333%À'Àš™™™™™#À333333ÀÍÌÌÌÌŒGÀš™™™™3À€0Àš™™™™™>Àffffff%À333333#ÀffffffÀ333333)ÀÀÍÌÌÌÌÌ&À333333ÀÍÌÌÌÌÌÀ333333Àffffff#Àš™™™™™$À333333!Àš™™™™;Àffffff-ÀÀÍÌÌÌÌL3ÀÀ33333³DÀffffff+ÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀš™™™™™$Àš™™™™™6Àffffff2ÀffffffÀffffff=ÀffffffÀffffffÀ!À333333 Àš™™™™™(À333333'À333333À333333ÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀÍÌÌÌÌÌü¿333333ÀÍÌÌÌÌÌÀÍÌÌÌÌÌ+ÀffffffÀffffffÀš™™™™™,Àffffff%À-ÀÍÌÌÌÌÌ(ÀÍÌÌÌÌL2À333333À.ÀÍÌÌÌÌÌ À+À333333*ÀÀ333333&À*ÀÍÌÌÌÌÌ-À333333Àffffff À333333Àš™™™™™ÀÍÌÌÌÌÌ*Àš™™™™™$Àš™™™™™ÀÍÌÌÌÌÌÀffffff%ÀffffffÀfffffæ4ÀffffffÀÍÌÌÌÌÌ!À Àš™™™™™$ÀÍÌÌÌÌÌ4ÀffffffÀ333333Àš™™™™™Àffffff&Àš™™™™™!Àš™™™™™*ÀÍÌÌÌÌL5Àš™™™™™/À33333³0ÀÍÌÌÌÌL1À333333"Àš™™™™™/ÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀ333333Àffffff À333333Àš™™™™™"À333333&À$Àš™™™™™.Àffffff*ÀÀ#Àffffff/À333333(À$ÀÍÌÌÌÌÌ ÀÍÌÌÌÌÌ-ÀÀ333333Àš™™™™™ÀÀÀffffffÀš™™™™DÀ0ÀÍÌÌÌÌÌ À333333Àš™™™™™,ÀffffffÀffffffþ¿š™™™™™*À3333339À333333!À333333Àš™™™™™DÀš™™™™™5ÀÍÌÌÌÌÌ1Àffffff3À À333333À(ÀÍÌÌÌÌÌ.À333333À'À*À333333#Àffffff ÀÍÌÌÌÌÌ#Àš™™™™™!ÀÀ333333ÀffffffÀÍÌÌÌÌÌÀ/Àffffff!À.ÀÍÌÌÌÌÌ'À33333³4À333333À333333Àš™™™™™*À ÀÍÌÌÌÌÌ$ÀÍÌÌÌÌÌ$ÀÍÌÌÌÌÌ Àš™™™™™/ÀÍÌÌÌÌÌ'Àš™™™™™ ÀÍÌÌÌÌÌ#ÀffffffÀš™™™™™-Àš™™™™™Àffffff)ÀÍÌÌÌÌÌ/À333333,ÀÍÌÌÌÌÌÀ€1Àffffff À(Àš™™™™™ÀffffffÀÍÌÌÌÌÌ(Àš™™™™™Àš™™™™™Àš™™™™™"Àš™™™™™ÀÍÌÌÌÌÌ"ÀffffffÀš™™™™™ù¿333333&Àš™™™™™À$À333333!À333333 ÀÍÌÌÌÌÌ&ÀÀÍÌÌÌÌÌ#ÀÍÌÌÌÌÌ'À#ÀffffffÀš™™™™™À333333ÀÀ$Àš™™™™™ À Àffffff$ÀffffffÀ!ÀÍÌÌÌÌÌì¿ffffffÀ333333*Àffffff*Àš™™™™™Ù¿š™™™™™ÀÍÌÌÌÌÌ2À€4ÀÍÌÌÌÌL0Àš™™™™™ÀÍÌÌÌÌL1Àš™™™™™7Àš™™™™™ÀffffffÀ,Àš™™™™™À3333334ÀÍÌÌÌÌL3À333333ÀÍÌÌÌÌÌ!ÀÍÌÌÌÌÌÀ333333"Àffffff+À&Àš™™™™™1Àffffff5À33333³1Àš™™™™™&Àffffff6ÀÍÌÌÌÌÌ#Àš™™™™™ÀÍÌÌÌÌÌ"À333333 Àffffff$À333333À333333)À33333³3ÀÍÌÌÌÌL0Àš™™™™™À333333ÀÍÌÌÌÌL3Àš™™™™™ÀÀš™™™™™&ÀÍÌÌÌÌÌ%À333333(Àffffff(ÀÀ333333Àš™™™™™$ÀÍÌÌÌÌÌ#À,À333333"À333333$ÀffffffÀÀš™™™™5À(À33333³:À5Àš™™™™™"ÀÍÌÌÌÌÌ7ÀÍÌÌÌÌÌ-À333333 Àffffff,ÀffffffÀš™™™™™ÀffffffÀ/ÀÍÌÌÌÌÌ%À€1À33333³2À$ÀffffffÀ$ÀÀš™™™™™#À333333!Àffffff*ÀffffffÀÍÌÌÌÌÌÀffffffæ¿333333-ÀÍÌÌÌÌÌ$ÀÍÌÌÌÌÌ À7À333333À3À333333À333333/Àš™™™™™ÀÍÌÌÌÌÌ#ÀÍÌÌÌÌL7ÀffffffÀÍÌÌÌÌÌ.Àffffff4Àffffff@Àš™™™™™ À!Àš™™™™™ ÀÍÌÌÌÌÌ$À€7À$Àš™™™™™,Àffffffþ¿333333ÀÀÍÌÌÌÌÌÀffffff+Àffffff&Àš™™™™™0Àš™™™™™!ÀÍÌÌÌÌÌ4À€4ÀffffffÀš™™™™™!À(Àš™™™™™?À!À333333&À À333333%ÀffffffÀ&Àfffff&GÀffffffÀ333333À33333³2À333333 Àffffff Àš™™™™™!À333333"À#À*ÀÀÀ*ÀffffffÀš™™™™™#Àffffff$ÀffffffÀÍÌÌÌÌÌÀš™™™™™ÀÍÌÌÌÌÌÀš™™™™™'Àš™™™™™Àffffff"Àš™™™™™ÀÍÌÌÌÌ AÀÍÌÌÌÌÌÀ333333À@š™™™™™ñ?ÍÌÌÌÌÌì?à?ffffffö?@à¿ffffffÀ333333ã?ÍÌÌÌÌÌì?š™™™™™Ù?š™™™™™¹?𿚙™™™™ù?à¿333333ã?333333Ó?š™™™™™ñ?š™™™™™É¿à?à?à?š™™™™™Ù?333333ã?š™™™™™Ù?à?š™™™™™É?š™™™™™¹?࿚™™™™™ñ?333333ã?ffffffö?ffffffö?333333ã?š™™™™™Ù¿š™™™™™É?š™™™™™Ù?333333Ó?š™™™™™é?š™™™™™Ù¿š™™™™™é?š™™™™™É?333333Ó?š™™™™™é¿à?ffffff濚™™™™™¹?ÍÌÌÌÌÌì?š™™™™™Ù¿ffffffæ?š™™™™™É?333333Ó?š™™™™™Ù?333333Ó?š™™™™™¹¿ffffffæ¿ffffffæ?š™™™™™é¿š™™™™™É?333333Ó?333333ó?ffffffæ?š™™™™™¹?š™™™™™¹?š™™™™™é?ÍÌÌÌÌÌì?333333ã?ffffffæ?333333Ó¿š™™™™™¹?ffffffæ?ÍÌÌÌÌÌì?ffffffæ?à?à?ÍÌÌÌÌÌ쿚™™™™™Ù?ffffffæ?à¿333333Ó¿ÍÌÌÌÌÌÀ333333Ó¿š™™™™™¹?ø¿š™™™™™É?š™™™™™¹¿š™™™™™Ù¿š™™™™™É?š™™™™™Ù¿š™™™™™Ù?333333Ó¿š™™™™™é?333333Ó?à¿333333ã?à?333333ã?š™™™™™ñ?333333Ó?333333ã?à?š™™™™™é?333333Ó¿š™™™™™É¿ffffffæ?š™™™™™É?š™™™™™Ù?ffffffæ?ÍÌÌÌÌÌì?š™™™™™Ù?à?š™™™™™É?š™™™™™É?333333Ó?š™™™™™É?333333Ó?š™™™™™é?š™™™™™É?š™™™™™Ù¿ffffffæ?š™™™™™Ù?à?š™™™™™ñ?ffffff濚™™™™™¹?ffffffæ¿ÍÌÌÌÌÌô?š™™™™™É¿333333Ó?š™™™™™¹?à¿@333333ã?333333ã?ð?ð?à?ffffffæ?333333ã?ð¿à?333333Ó?333333Ó?333333ã?š™™™™™É¿š™™™™™Ù?š™™™™™Ù¿333333Ó?à?ffffffæ?ffffffæ?š™™™™™É?333333ã?š™™™™™Ù?à?š™™™™™Ù¿à?š™™™™™Ù?š™™™™™é?333333ã?333333ã¿333333Ó?ÍÌÌÌÌÌì?š™™™™™¹?š™™™™™é?࿚™™™™™Ù?ð?333333ã?š™™™™™Ù?š™™™™™é?333333Ó¿333333ã?333333Ó?333333Ó?š™™™™™É¿333333Ó?š™™™™™É?š™™™™™Ù?@ÍÌÌÌÌÌì¿333333Ó?ÍÌÌÌÌÌì?333333Ó?š™™™™™É?ÍÌÌÌÌÌì?333333Ó¿à?š™™™™™Ù?ffffffö?333333Ó?š™™™™™ñ?333333Ó¿333333ã¿à?333333Ó¿š™™™™™¹?à?ffffffæ?š™™™™™Ù?333333Ó¿š™™™™™é?š™™™™™É?š™™™™™é?333333ã?š™™™™™ñ?à?š™™™™™É?333333Ó¿š™™™™™É?š™™™™™¹?à?333333ã¿ffffffæ¿ÍÌÌÌÌÌ쿚™™™™™É?ð?š™™™™™Ù¿333333Ó?š™™™™™é¿š™™™™™Ù?š™™™™™É¿š™™™™™É¿333333Ó¿à?ffffffæ?š™™™™™Ù¿š™™™™™Ù?333333ã?333333Ó?š™™™™™é?333333Ó?š™™™™™É?ÍÌÌÌÌÌì?š™™™™™¹?ð¿333333Ó?š™™™™™Ù¿ffffffæ?š™™™™™Ù?ÍÌÌÌÌÌì?333333㿚™™™™™ñ?ffffffæ?333333Ó?š™™™™™¹¿š™™™™™É¿š™™™™™Ù?š™™™™™Ù?333333ã?à?š™™™™™¹¿š™™™™™É?š™™™™™Ù¿333333ã¿333333ã?333333Ó?š™™™™™¹?à?š™™™™™¹¿333333Ó?333333Ó?333333Ó?š™™™™™¹?333333Ó?333333㿚™™™™™é?š™™™™™É?š™™™™™Ù?ð?š™™™™™é?333333㿚™™™™™é?š™™™™™Ù?ffffffæ¿ffffffæ?š™™™™™Ù?š™™™™™Ù?š™™™™™¹?š™™™™™Ù?ffffffæ?à?š™™™™™Ù¿š™™™™™É?š™™™™™É?š™™™™™É?š™™™™™É¿333333Ó¿š™™™™™Ù?333333㿚™™™™™é?š™™™™™Ù?333333ã?š™™™™™é?š™™™™™¹?à¿ffffffæ?š™™™™™¹¿à?š™™™™™É?š™™™™™Ù?333333ӿ࿚™™™™™é?333333Ó¿à¿333333ã?333333Ó?à?š™™™™™é?333333Ó?š™™™™™¹?š™™™™™É?333333Ó¿š™™™™™É?š™™™™™É?333333Ó¿333333Ó?333333Ó?à?š™™™™™É¿333333ã?à?š™™™™™É¿š™™™™™é¿à?ð?333333ã?š™™™™™é?š™™™™™ñ?ffffffæ¿à?ffffffæ?333333Ó?333333㿚™™™™™Ù?š™™™™™ñ¿333333Ó¿š™™™™™Ù?š™™™™™Ù?333333ã?ffffffæ?š™™™™™É¿333333ã?š™™™™™Ù¿š™™™™™Ù?š™™™™™É?ffffffæ¿à?333333û¿š™™™™™Ù?à¿333333Ó?333333Ó?š™™™™™Ù¿à?𿚙™™™™É?š™™™™™¹?ÍÌÌÌÌÌì?ÍÌÌÌÌÌô¿à?š™™™™™É?à¿333333Ó?ð¿ffffffæ?à?ffffffæ?333333Ó?š™™™™™Ù?š™™™™™Ù?ÍÌÌÌÌÌì?333333ó?ÍÌÌÌÌÌì?š™™™™™Ù?š™™™™™Ù¿š™™™™™Ù?š™™™™™é?à?333333ã?š™™™™™é?333333ã?333333Ó¿333333ã?à?š™™™™™¹?ÍÌÌÌÌÌ쿚™™™™™É?333333ã?š™™™™™ñ?333333Ó¿š™™™™™Ù¿š™™™™™Ù¿ÍÌÌÌÌÌì¿à?à?333333Ó¿š™™™™™¹¿333333Ó?333333Ó?333333ã?333333ã?ð?š™™™™™Ù?š™™™™™é?š™™™™™¹¿333333Ó¿ffffffö?ÍÌÌÌÌÌì?š™™™™™Ù?š™™™™™@ffffff@333333Ó?ffffff@ÍÌÌÌÌÌ@ffffff!@ffffff@ffffff@333333@@ÍÌÌÌÌÌÀš™™™™™Ù?ð?333333@ffffff @@š™™™™™@ÍÌÌÌÌÌô?š™™™™™@@ÍÌÌÌÌÌ@ffffff@@ÍÌÌÌÌÌô?333333@333333@ÍÌÌÌÌÌ @ffffff@333333Ó?š™™™™™ù¿333333@333333@ÍÌÌÌÌÌ@333333@ÍÌÌÌÌÌ@333333 @ffffff@333333!@333333@@ÍÌÌÌÌÌ @333333 @ffffff@ÍÌÌÌÌÌü?š™™™™™@š™™™™™@ÍÌÌÌÌÌ@ffffffþ?333333Ó?333333@333333ã¿ffffff @333333@ffffff@š™™™™™@š™™™™™@ÍÌÌÌÌÌ@š™™™™™@ @ÍÌÌÌÌÌ@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ @ffffff@š™™™™™@@ffffffö?333333@333333@333333@333333@ffffff@š™™™™™@ÍÌÌÌÌÌ @š™™™™™@ÍÌÌÌÌÌ@š™™™™™@333333Ó?333333@š™™™™™ @@@š™™™™™@333333@333333@ÍÌÌÌÌÌ2@333333@@333333-@ffffff@š™™™™™é?@š™™™™™@š™™™™™@š™™™™™!@333333@ffffff@333333@ÍÌÌÌÌÌì?š™™™™™ñ?333333û?š™™™™™@ð?š™™™™™@@š™™™™™@ÍÌÌÌÌÌ@š™™™™™@ffffff&@ð?ÍÌÌÌÌÌ@333333@ffffff @ffffff@ffffff@ffffff@@ @333333 @ffffff@333333@@ffffff@333333@š™™™™™@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ @@š™™™™™ @ÍÌÌÌÌÌ@š™™™™™@333333@ÍÌÌÌÌÌ@333333@ffffff@@ÍÌÌÌÌÌ@š™™™™™é?ÍÌÌÌÌÌü?ÍÌÌÌÌÌ@333333@333333ó?@333333 @ffffff@333333@#@š™™™™™@333333û?333333ã?ø?ÍÌÌÌÌÌ#@š™™™™™@ffffff @333333 @ÍÌÌÌÌÌ@š™™™™™ñ?ÍÌÌÌÌÌô?ffffff@333333@@333333@ÍÌÌÌÌÌ@š™™™™™@ÍÌÌÌÌÌì?ffffff@@š™™™™™ @š™™™™™@š™™™™™@333333Ó?ÍÌÌÌÌÌ@333333$@ffffffþ?ÍÌÌÌÌÌ쿚™™™™™ñ?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@š™™™™™¹?ffffff@ffffff@ð?ÍÌÌÌÌÌ@š™™™™™Ù?ø¿š™™™™™ñ?š™™™™™é?@ÍÌÌÌÌÌ@š™™™™™@š™™™™™ @š™™™™™@ÍÌÌÌÌÌì?ÍÌÌÌÌÌ@333333 @@333333@@333333@333333Ó?ffffff@š™™™™™é?ÍÌÌÌÌÌ@333333@@333333@ffffff!@ffffff@333333ã?333333@333333@ÍÌÌÌÌÌ!@ÍÌÌÌÌÌ@333333@ÍÌÌÌÌÌ@š™™™™™ñ?š™™™™™ñ?@@š™™™™™é?š™™™™™@ÍÌÌÌÌÌ@ffffff@ÍÌÌÌÌÌ@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ @ffffff@@ð?š™™™™™é?@@š™™™™™@𿚙™™™™@À333333 @@333333 @ÍÌÌÌÌÌ@š™™™™™ñ?ÍÌÌÌÌÌ@ffffff @š™™™™™ñ?ffffff@333333@ÍÌÌÌÌÌü?ÍÌÌÌÌÌ@ffffffþ?ffffff@333333ã?ÍÌÌÌÌÌ@š™™™™™@ÍÌÌÌÌÌ'@ÍÌÌÌÌÌü?š™™™™™ @ffffffö?ÍÌÌÌÌÌ@333333@š™™™™™@@ÍÌÌÌÌÌô?š™™™™™@š™™™™™ñ?@ffffff@š™™™™™@ÍÌÌÌÌÌ @ffffff@@ffffff@ffffffæ?@ð?@š™™™™™é¿ÍÌÌÌÌÌ @333333 @@"@ffffff@š™™™™™@ @ÍÌÌÌÌÌì?š™™™™™ @ÍÌÌÌÌÌô?ffffff@@š™™™™™ñ?&@ffffff"@333333@ffffffæ?333333 @ffffffÀÍÌÌÌÌÌÀffffff@#@ffffff@ffffffþ?333333 @ffffffþ?ffffffö?ffffff@ffffff@š™™™™™@ffffff@333333@333333 @ÍÌÌÌÌÌ @333333@ffffffæ¿333333@ÍÌÌÌÌÌ@š™™™™™@ÍÌÌÌÌÌ@@@š™™™™™@š™™™™™@ffffffö¿ffffffþ?@@š™™™™™ù?ÍÌÌÌÌÌ@333333@š™™™™™É?ffffff@@ @ffffff@@ÍÌÌÌÌÌì?333333@333333@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ!@@ø?ø?ÍÌÌÌÌÌ@ffffff@@@š™™™™™ù?ÍÌÌÌÌÌ@ffffff@ffffffæ?ffffffö?ÍÌÌÌÌÌ@ffffff!@@@ÍÌÌÌÌÌì¿ÍÌÌÌÌÌ(@ð?ÍÌÌÌÌÌô?@ÍÌÌÌÌÌ @ÍÌÌÌÌÌ@@ffffff@ÍÌÌÌÌÌ@š™™™™™"@ÀÍÌÌÌÌÌ@333333@ø?333333ã?ffffff@"@š™™™™™É?ÍÌÌÌÌÌü?ÍÌÌÌÌÌü?@š™™™™™'Àð?š™™™™™ @333333@ffffff@ÍÌÌÌÌÌ!@š™™™™™ @ffffff!@@ffffff@ÍÌÌÌÌÌô?333333û?%@à?333333%@ÍÌÌÌÌÌ@333333'@333333@ffffff @ÍÌÌÌÌÌ@š™™™™™ñ?@ffffff @ÍÌÌÌÌÌ@à?ÍÌÌÌÌÌ@@333333@333333@š™™™™™¹?@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@@š™™™™™@ÍÌÌÌÌÌ@ffffffö?š™™™™™!@š™™™™™ù¿ð?333333@ÍÌÌÌÌÌ@ffffff%@ffffff@š™™™™™@333333@ÍÌÌÌÌÌì?ffffff@š™™™™™À@ÍÌÌÌÌÌ@š™™™™™é?š™™™™™@š™™™™™@ffffff@ÍÌÌÌÌÌ @ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@@š™™™™™@š™™™™™ñ?@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@š™™™™™@š™™™™™%@ffffff À333333@333333@333333ÀÍÌÌÌÌÌÀffffff4@ffffff@À \ÀÍÌÌÌÌÌ3Àš™™™™0ÀÍÌÌÌÌL3ÀÍÌÌÌÌÌ!Àš™™™™YAÀš™™™™™!Àø¿7@3@š™™™™™%@=À333333À33333sAÀš™™™™Ù@@@33333³5ÀÍÌÌÌÌÌ!@š™™™™™%Àš™™™™™ÀffffffHÀ33333³9Àffffff&@333333À333333 ÀÍÌÌÌÌÌ%À333333À333333ÀffffffÀš™™™™™À333333À%ÀÍÌÌÌÌÌ+@ffffff@š™™™™™0@ÍÌÌÌÌÌ@ffffff3À6@ÍÌÌÌÌÌ,@à¿333333ó¿333333Àš™™™™™ @ÍÌÌÌÌÌÀš™™™™™;ÀÍÌÌÌÌÌ@ffffff"À3333332@fffffæ?@333333ã?ø?3333330À33333³6ÀÍÌÌÌÌÌ*À*À333333 @ÍÌÌÌÌÌ!@š™™™™™ @@ffffff@ffffffÀ333333.Àš™™™™C@333333ó?š™™™™™ À333333%@ÍÌÌÌÌÌ@š™™™™™)ÀÀš™™™™™É¿€=@ÍÌÌÌÌLVÀ333333À€@À333333&Àffffffæ¿33333³0@fffffæ;Àffffff1@fffffæ0ÀÍÌÌÌÌLnÀÍÌÌÌÌÌ0@333333&À@eÀfffffæCÀÍÌÌÌÌL8ÀÍÌÌÌÌÌü?4À333333@5À€=@333333@ÍÌÌÌÌÌ@33333³=ÀÍÌÌÌÌÌ)@3333335@ÍÌÌÌÌL>@ÍÌÌÌÌlRÀ?Àfffff&@@/ÀÍÌÌÌÌLWÀffffffö¿ÍÌÌÌÌ NÀ€D@@š™™™™™@333333ÀÍÌÌÌÌŒDÀ33333sAÀ333333"@fffff&NÀÍÌÌÌÌL1@ffffff'ÀÍÌÌÌÌÌ @š™™™™™7@3Àffffff2Àffffff#À@ffffff;@ð?5@ffffff&Àffffff+@3333333Àš™™™™™@@ÍÌÌÌÌÌ"ÀÀÍÌÌÌÌL;À333333#Àš™™™™™'ÀÍÌÌÌÌÌÀš™™™™™&À+@ÍÌÌÌÌL3@333333$ÀÍÌÌÌÌÌ$@ÍÌÌÌÌÌ-Àš™™™™™/Àš™™™™™1Àš™™™™™$@ÍÌÌÌÌÌô¿š™™™™™ @ffffffÀ333333(Àš™™™™™Àffffff*À)@&Àš™™™™™À333333&Àffffff @333333+Àffffff&@ÍÌÌÌÌÌÀ33333³3Àš™™™™™/@š™™™™™7@333333ÀÍÌÌÌÌÌ/À"À333333$ÀÍÌÌÌÌL:ÀAÀÍÌÌÌÌL4À€>À333333F@ÍÌÌÌÌÌ>Àffffffö¿333333ó?ffffff@ÍÌÌÌÌÌ,@333333û?ffffffþ¿-@ÍÌÌÌÌÌ ÀÍÌÌÌÌL7À8À33333³G@3333334ÀÍÌÌÌÌLCÀš™™™™YAÀÀIÀš™™™™7À€9À33333sK@333333&ÀÍÌÌÌÌÌ@333333ã?-À€:@fffffæGÀÍÌÌÌÌÌì?š™™™™™Ù¿š™™™™™+@ffffff.À@33333CaÀffffff6@š™™™™™*ÀÍÌÌÌÌL]À333333 À@š™™™™BÀ333333?ÀÍÌÌÌÌÌ3Àfffff&FÀ333333@333333@fffffæ3À33333³5Àš™™™™YTÀ1À333333@ffffff@ffffffÀ0@š™™™™™Àš™™™™™:Àfffffæ3À'Àš™™™™™¹¿ÍÌÌÌÌL?@fffffæ2@€P@333333@š™™™™™É?F@333333+À33333³0Àš™™™™™&À333333%@33333³@@fffffæ2À333333%À@ÍÌÌÌÌÌ3Àffffff À33333³5À@À/@33333³0À0@7Àš™™™™™"@ffffffDÀš™™™™™RÀfffffæ1Àš™™™™™ÀffffffÀfffff\ÀÍÌÌÌÌÌì?33333ó@Àš™™™™™ù¿ffffffÀÍÌÌÌÌÌ@$À333333Àš™™™™™ ÀÍÌÌÌÌÌ,À333333ÀÍÌÌÌÌL0@$@š™™™™™,@fffffæ8@š™™™™™(Àš™™™™™ÀffffffÀffffff%ÀC@ffffff.Àffffffö¿š™™™™™¹¿Àffffff=@333333&@ffffffþ¿š™™™™™@š™™™™6ÀÍÌÌÌÌÌü?fffffæ3ÀÍÌÌÌÌÌÀ333333/À@ÍÌÌÌÌÌ1@š™™™™™ù¿€O@ð?333333!@š™™™™ÙE@ÍÌÌÌ̬d@€2@ÍÌÌÌÌL7@33333³2À€:Àffffffö¿33333óHÀ6ÀÍÌÌÌÌŒAÀÍÌÌÌÌÌJÀ333333,@š™™™™™@ÍÌÌÌÌÌ:Àš™™™™™$À33333ó@À€BÀffffff"Àffffff-Àffffff @š™™™™™É?š™™™™™ Àš™™™™™<@33333sGÀÍÌÌÌÌÌ?ÀÍÌÌÌÌÌ.À333333Àš™™™™™3Àš™™™™™ñ?š™™™™™@ffffff,@ÍÌÌÌÌÌSÀ33333³<@ffffffÀš™™™™™2@ffffff(Àš™™™™=À333333À€1@š™™™™™Ù?ÍÌÌÌÌÌ7Àš™™™™™@š™™™™™@€5À+À€=À@3333334@š™™™™™ @ À.@ffffff&@ffffff3ÀÍÌÌÌÌÌü¿š™™™™™!@š™™™™™ñ?ÍÌÌÌÌÌ9À*@š™™™™¹PÀfffff¦IÀEÀffffff/@KÀ333333û?ffffff9@ÍÌÌÌ̬SÀ€>Àš™™™™™@ffffff@fffff¦F@š™™™™@Àffffff @ffffff)@ÍÌÌÌÌŒFÀfffffæDÀÍÌÌÌÌÌ"À333333@ÍÌÌÌÌÌ@š™™™™™;@333333!ÀÍÌÌÌÌ AÀà¿ÍÌÌÌÌÌ@fffff&A@ÍÌÌÌÌÌ6ÀÍÌÌÌÌÌRÀø?fffffæ4@€HÀffffffÀÍÌÌÌÌŒCÀš™™™™™É?=À333333ÀÀ333333IÀÍÌÌÌÌÌ3@š™™™™7ÀÍÌÌÌÌÌ?ÀfffffeÀ€0@š™™™™™ÀÍÌÌÌÌŒCÀ!Àš™™™™™(À3333331ÀÀÍÌÌÌÌÌ9À@@@Àš™™™™™3@fffff&D@š™™™™<ÀÍÌÌÌÌÌÀš™™™™Ù@@ÍÌÌÌÌÌÀ333333 À333333ó?š™™™™™!À333333û¿$Àš™™™™¹VÀš™™™™™À3333335À ÀÍÌÌÌÌÌ'ÀÍÌÌÌÌÌ@š™™™™™-ÀÍÌÌÌÌgÀà?ffffffÀ33333sLÀ33333³3Àš™™™™™8@ÍÌÌÌÌL0À333333Àffffff$ÀÍÌÌÌÌ @À€3À€1@ÍÌÌÌÌÌ2@š™™™™™<Àš™™™™™Ù¿"ÀÍÌÌÌÌÌ%Àfffffæ2@%@š™™™™™ÀÍÌÌÌÌÌ@ÍÌÌÌÌÌ;Àffffff:@š™™™™6À33333³;@ÍÌÌÌÌìRÀ333333@fffff&C@š™™™™YJ@33333³;@ÍÌÌÌÌL7@€I@€E@333333RÀ3333335@š™™™™5@š™™™™™E@fffffæ;@33333SÀÀffffff@fffff¦O@ÍÌÌÌÌLJ@š™™™™ÙZ@333333%@š™™™™™&@333333I@33333ÓP@àP@fffff¦H@ÀC@333333 @š™™™™=@7@33333óM@33333³F@ÍÌÌÌÌÌì¿€@ÀÍÌÌÌÌŒM@€8@š™™™™ÙH@ÍÌÌÌÌÌB@š™™™™4@ÍÌÌÌÌL8@ÍÌÌÌÌÌ4@@W@ÀD@?@ÍÌÌÌÌÌA@)@33333óJ@fffffæ9@fffff¦B@/@33333sF@ffffff2@ffffffÀÍÌÌÌÌL<@ÍÌÌÌÌÌô¿33333³;@@Q@fffffæO@š™™™™<@ÍÌÌÌÌÌN@ÀH@333333?@3333334@2@33333³N@33333³O@ÍÌÌÌÌ B@33333óI@š™™™™P@ÍÌÌÌÌÌ)@333333 @ R@fffffæ8@ÍÌÌÌÌÌ2@fffffæI@fffffQ@ÍÌÌÌÌÌA@333333C@@P@š™™™™ÙY@33333³3Àffffffæ¿Àš™™™™™=@L@`T@ffffffæ?K@š™™™™™/@š™™™™™"À33333óB@š™™™™9R@ffffff?@š™™™™™7@ffffff/Àš™™™™YC@33333ó@@5@33333sV@ÀR@ffffff@@ÀC@33333³3Àfffffæ5@fffff¦E@ffffffO@š™™™™9PÀfffffæ@@fffff¦M@'@,Àš™™™™YK@ÍÌÌÌÌ W@fffffæH@33333óE@33333sG@€8@š™™™™™$Àš™™™™ÙB@ffffffO@ÍÌÌÌÌL6ÀÀH@fffffæ4@C@@N@@fffffæE@ÍÌÌÌÌÌ7@š™™™™=@fffffÆS@fffffæ?@33333óL@š™™™™4@@O@ÍÌÌÌÌÌ?@:@š™™™™YH@ffffffK@š™™™™;@ÍÌÌÌÌŒB@ÍÌÌÌÌ L@333333 Àš™™™™™"@š™™™™YD@333333R@ÍÌÌÌÌL=@A@ÍÌÌÌÌÌD@333333I@fffff&N@@U@ÍÌÌÌÌÌI@33333³2@@š™™™™™&@fffff&\@ffffffE@š™™™™3@@G@E@333333@š™™™™™ @ÀE@ÍÌÌÌÌÌ!@333333G@š™™™™4@€A@fffff&T@š™™™™A@6@D@ Y@ÍÌÌÌÌÌ&@ÍÌÌÌÌ E@333333?Àš™™™™I@š™™™™P@ffffffM@33333sBÀš™™™™™@ÍÌÌÌÌLD@A@š™™™™™U@333333@ffffff=@š™™™™¹R@ð¿ÍÌÌÌÌÌ"@€3ÀÍÌÌÌÌÌ?@ffffffÀ33333³8ÀÍÌÌÌÌL6@fffff¦A@ÍÌÌÌÌL1@8@š™™™™YQ@ÍÌÌÌÌÌ ÀÀN@š™™™™™@@ffffff?@33333sP@33333³<@ffffffI@ffffff @333333C@ffffff!ÀÍÌÌÌÌ F@ffffffMÀš™™™™™P@fffffæ1@.@33333óJ@š™™™™™&@š™™™™Ù@@š™™™™™2@fffff†S@š™™™™™;@š™™™™yQ@333333;@š™™™™™ Àš™™™™™(À333333KÀÍÌÌÌÌÌ"@š™™™™™@ÍÌÌÌÌ K@š™™™™M@š™™™™™P@ÍÌÌÌÌŒA@š™™™™™É¿333333L@š™™™™YB@ÍÌÌÌÌŒT@š™™™™9V@€=@R@33333sH@š™™™™™L@À`@8ÀfffffæK@ÍÌÌÌÌ JÀfffffæD@33333“Q@333333.@€7@333333+@ð?9@ffffff%Àffffff1@ÍÌÌÌÌÌM@ÍÌÌÌÌÌ@ÍÌÌÌ̬Q@"À33333T@À@ÀffffffFÀA@33333S]@ffffff"@ÍÌÌÌÌÌQÀ0@š™™™™™Àš™™™™™1@€N@33333³5@š™™™™™¹?ffffffH@ffffff@ffffff@fffff&B@ÀC@š™™™™D@ÍÌÌÌÌ P@ W@š™™™™<@@+@À33333³V@€2À333333>@š™™™™9T@33333sG@€\@ÍÌÌÌÌŒL@fffffæ6@333333@@0À333333?@333333û¿3333338@@š™™™™™/@fffff¦\@€W@š™™™™YY@ÍÌÌÌÌÌ!@š™™™™™E@š™™™™™:Àš™™™™ a@fffffÆQ@°`@fffff&E@333333Àfffffæ?@ÍÌÌÌÌL?ÀÍÌÌÌÌÌ@9@ÍÌÌÌÌÌ:Àš™™™™™K@€I@ÍÌÌÌÌ C@fffffFS@ @š™™™™™+@ÍÌÌÌÌÌ/Àffffff>@>@š™™™™3@fffffFR@fffff¦W@€>Àffffff#À33333³I@€8ÀÍÌÌÌÌÌ@ffffffS@š™™™™7@š™™™™YH@š™™™™6ÀÍÌÌÌÌLN@À333333O@fffffæ@@ Q@š™™™™yQ@fffff†U@š™™™™™)@š™™™™™(@33333³L@`R@ÍÌÌÌÌLF@àR@Àš™™™™5@€=@33333sA@š™™™™YJ@fffffÆQ@33333ÓP@ÍÌÌÌÌÌ!ÀÀC@š™™™™™E@$@š™™™™™!ÀÍÌÌÌÌÌC@€>@ÍÌÌÌÌL8À33333³E@ÍÌÌÌÌÌ!@š™™™™¹R@š™™™™™&@š™™™™Ù@@€1À/@3333337@ÀD@àZ@ÍÌÌÌÌÌÀ33333sY@ffffffÀÍÌÌÌÌÌ,@fffffæC@333333@-@€H@š™™™™¹`@ffffffÀ33333³3Àffffff,@@@ÍÌÌÌÌLIÀ333333)Àš™™™™ÙGÀš™™™™G@fffffæK@fffff¦F@ÍÌÌÌÌÌ@@ Q@333333;@š™™™™YS@š™™™™™@š™™™™™ @€O@3333337@ÍÌÌÌ̬\@ÍÌÌÌÌÌì?ÍÌÌÌÌ D@š™™™™9S@3@fffffæ1@333333ã¿fffffFR@9@fffff¦K@ÍÌÌÌÌL5À33333óT@333333*@š™™™™YQ@fffff†`@ffffff:À33333sE@fffffU@fffff†P@33333óL@A@333333>@33333sP@š™™™™YD@ÍÌÌÌÌŒJÀffffff@R@6À@š™™™™Y@@@ffffff8Àš™™™™™R@ÍÌÌÌÌŒG@ÍÌÌÌÌÌ7ÀÍÌÌÌÌÌ!À33333óE@ÍÌÌÌÌ VÀÍÌÌÌÌÌG@š™™™™™*@š™™™™8ÀàP@ÍÌÌÌÌLC@ÍÌÌÌÌ,P@ÍÌÌÌÌÌ(@333333G@€3@ÀI@33333óO@€8@€2@fffffÆX@333333)@33333SV@ffffff@š™™™™9a@fffffFbÀš™™™™™W@33333sN@333333Ó¿š™™™™™É¿ffffffæ?š™™™™™Ù?š™™™™™É¿@š™™™™™¹?š™™™™™¹?š™™™™™é?à¿ffffffö¿š™™™™™É¿š™™™™™É?š™™™™™¹?š™™™™™¹?š™™™™™Ù¿š™™™™™¹?333333㿚™™™™™¹?š™™™™™¹?š™™™™™É?333333Ó¿š™™™™™¹¿š™™™™™¹?333333ó?š™™™™™¹?š™™™™™Ù¿ffffff濚™™™™™Ù?š™™™™™É?š™™™™™¹?࿚™™™™™¹?š™™™™™¹?š™™™™™É¿š™™™™™¹?𿚙™™™™¹¿š™™™™™¹?š™™™™™ñ¿š™™™™™Ù¿š™™™™™¹¿333333ã¿à¿à¿š™™™™™¹?š™™™™™Ù¿333333Ó?š™™™™™¹?š™™™™™é¿š™™™™™Ù¿333333Ó¿š™™™™™Ù¿š™™™™™é¿š™™™™™É?333333ӿ𿚙™™™™É¿š™™™™™¹¿š™™™™™¹¿š™™™™™É¿š™™™™™¹¿š™™™™™¹?š™™™™™Ù¿333333ã?333333㿚™™™™™¹?à¿ffffff@ÍÌÌÌÌÌ쿚™™™™™É¿333333Ó¿333333㿚™™™™™Ù¿333333Ó¿333333Ó¿333333ã¿ffffff#@333333Àš™™™™™ñ?@š™™™™™¹¿à?š™™™™™é?š™™™™™é¿š™™™™™¹?š™™™™™É?š™™™™™¹?š™™™™™¹?š™™™™™¹?š™™™™™ñ¿š™™™™™É¿š™™™™™É?333333Ó?ffffffæ¿ffffff@š™™™™™Ù¿@ffffffæ¿333333Ó¿š™™™™™¹?š™™™™™É?š™™™™™ñ?333333Ó?333333ã?š™™™™™É¿à¿333333ã¿ffffff濚™™™™™¹?š™™™™™é?333333ã?333333Ó¿š™™™™™¹?š™™™™™¹?333333ã?333333ó?š™™™™™ñ¿š™™™™™É¿š™™™™™¹?š™™™™™Ù¿š™™™™™Ù¿333333㿚™™™™™É¿à?š™™™™™É?ÍÌÌÌÌÌü?š™™™™™¹¿š™™™™™É?š™™™™™Ù?š™™™™™Ù?333333Ó¿š™™™™™É¿à¿š™™™™™É?à?š™™™™™¹?š™™™™™ñ¿ÍÌÌÌÌÌ쿚™™™™™Ù¿š™™™™™¹?š™™™™™Ù¿š™™™™™É¿š™™™™™Ù¿š™™™™™¹?333333Ó¿ÍÌÌÌÌÌ쿚™™™™™¹?š™™™™™É?ð¿ÍÌÌÌÌÌì?š™™™™™¹¿ffffffæ?ð¿ÍÌÌÌÌÌì?࿚™™™™™É¿š™™™™™Ù¿à¿š™™™™™Ù¿š™™™™™é¿š™™™™™¹?333333Ó¿š™™™™™¹?333333ã?à¿333333Ó¿š™™™™™Ù¿ð?š™™™™™É?333333ã?ÍÌÌÌÌÌô¿š™™™™™é¿š™™™™™É?333333Ó¿š™™™™™¹?š™™™™™ñ¿à¿à¿š™™™™™Ù¿333333㿚™™™™™Ù?ffffff@š™™™™™É¿š™™™™™¹?ffffffþ?ÍÌÌÌÌÌì¿333333ó¿š™™™™™¹¿ffffffþ?š™™™™™¹?š™™™™™É¿š™™™™™ñ¿š™™™™™¹?ffffff濚™™™™™¹¿333333 @ÍÌÌÌÌÌì¿ffffffæ¿à?š™™™™™¹?š™™™™™¹¿ffffff濚™™™™™Ù?333333ã?š™™™™™¹?ffffffö¿à¿333333㿚™™™™™É?ÍÌÌÌÌÌô¿š™™™™™¹?š™™™™™ù¿š™™™™™¹?ÍÌÌÌÌÌ쿚™™™™™ñ¿333333Ó¿333333Ó¿333333ã¿ffffffæ?333333㿚™™™™™¹?š™™™™™¹?š™™™™™é?š™™™™™¹¿333333ã?ÍÌÌÌÌÌì?š™™™™™¹?š™™™™™É?ffffffæ¿ffffffþ?333333ã¿à?333333Ó¿ffffffæ?š™™™™™É¿š™™™™™Ù¿à¿š™™™™™¹?à?࿚™™™™™É¿š™™™™™Ù?333333Ó¿ð¿333333ã¿à¿š™™™™™¹?à¿à?š™™™™™Ù¿š™™™™™¹?à?š™™™™™Ù?333333㿚™™™™™¹?š™™™™™Ù¿333333ӿ𿚙™™™™¹?333333ã?333333ó¿ffffffæ¿ð¿š™™™™™Ù¿333333Ó?à?ÍÌÌÌÌÌÀš™™™™™ñ¿333333ó?333333Ó?ffffffþ?à?š™™™™™Ù¿ffffffÀš™™™™™Ù?š™™™™™¹?š™™™™™Ù¿à¿š™™™™™¹¿š™™™™™É¿à¿333333ó?š™™™™™¹?š™™™™™Ù¿š™™™™™¹?333333Ó?ffffffæ?ffffffþ¿ÍÌÌÌÌÌü¿ffffffæ?à¿ÍÌÌÌÌÌ쿚™™™™™Ù?333333ó¿333333ó¿ffffffæ¿ffffffæ?š™™™™™ñ?333333ã?à?š™™™™™é?ð¿ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?š™™™™™¹?š™™™™™¹?š™™™™™É¿333333ó¿š™™™™™ñ?à?ÍÌÌÌÌÌì¿à?š™™™™™É¿š™™™™™¹?ffffffæ¿333333㿚™™™™™ñ¿ÍÌÌÌÌÌì¿ffffff@ð?333333û?à¿333333Ó?333333û¿ÀÍÌÌÌÌÌô?š™™™™™É¿à?š™™™™™ñ¿š™™™™™ù¿š™™™™™¹?à?š™™™™™Ù?ð¿ÍÌÌÌÌÌì¿333333Ó¿à¿333333ã?š™™™™™¹¿à¿š™™™™™ À333333ã?333333ã?š™™™™™¹?š™™™™™É¿š™™™™™Ù?š™™™™™Ù¿333333ã?333333ã¿ffffffö¿š™™™™™Ù¿š™™™™™¹¿ÍÌÌÌÌÌü?333333㿚™™™™™@333333ã?š™™™™™Ù?ÍÌÌÌÌÌ@333333Ó¿à?š™™™™™é?ÍÌÌÌÌÌì?š™™™™™Ù?à¿333333Ó¿š™™™™™¹¿ffffffæ¿ffffffö¿ø¿ÍÌÌÌÌÌì?ffffffö?š™™™™™¹¿š™™™™™¹?š™™™™™é¿ffffffö?š™™™™™¹¿š™™™™™ñ?š™™™™™ñ¿ffffffæ¿333333Ó¿333333Ó¿ÍÌÌÌÌÌ@ffffffæ?ffffffæ?ø?333333Ó¿ffffffæ¿333333ã¿ð¿ÍÌÌÌÌÌü?333333Ó¿ffffffþ?š™™™™™É¿š™™™™™Ù¿à¿ffffffæ¿ffffff濚™™™™™¹?333333ã?š™™™™™Ù?š™™™™™¹?š™™™™™É?š™™™™™¹?š™™™™™Ù¿š™™™™™Ù?š™™™™™ñ¿333333û¿333333ã¿ø¿ffffffþ¿š™™™™™0À࿚™™™™™Ù¿š™™™™™ù¿ffffff濚™™™™™Ù¿š™™™™™é¿ÍÌÌÌÌÌì¿à¿ÀÍÌÌÌÌÌ쿚™™™™™ñ¿ð¿š™™™™™ñ¿š™™™™™é¿š™™™™™ñ¿ð¿ffffff4ÀÀ33333³0ÀÍÌÌÌÌÌÀffffff濚™™™™™Ù¿à¿à¿,À࿚™™™™™Ù¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌü¿š™™™™™Ù¿Àš™™™™™Ù¿à¿š™™™™™Ù¿"Àš™™™™™Ù¿ÍÌÌÌÌÌô¿ffffffæ¿333333㿚™™™™™Ù¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌ#À𿚙™™™™ù¿333333ã¿ffffff.@#@$@.@ffffff.@?@ÍÌÌÌÌÌ@ffffff@š™™™™™@@33333³0@ffffff@ffffff @š™™™™™4@ffffff)@333333#@ffffff@ÍÌÌÌÌÌ @&@ÍÌÌÌÌÌ2@'@€2@ÍÌÌÌÌÌ!@ÍÌÌÌÌÌ%@333333)@š™™™™™)@š™™™™™2@333333'@š™™™™™"@333333@š™™™™™@"@@š™™™™™-@ÍÌÌÌÌÌ"@ffffff$@š™™™™™(@ffffff,@@ffffff@&@ÍÌÌÌÌÌ@š™™™™™1@ffffff3@ÍÌÌÌÌÌ)@@%@ffffff"@ffffff@333333@ÍÌÌÌÌL0@ÍÌÌÌÌÌ%@fffffæ2@ffffff8@ÍÌÌÌÌÌ*@š™™™™™@333333&@333333%@3333336@ffffff@ffffff,@$@š™™™™™!@ÍÌÌÌÌÌ0@fffffæ1@@ÍÌÌÌÌÌ#@š™™™™1@š™™™™™$@333333#@ÍÌÌÌÌÌ#@ffffff+@333333@ffffff@333333)@ÍÌÌÌÌÌ:@ffffff@333333@3333332@ÍÌÌÌÌL1@3333333@ÍÌÌÌÌÌ @š™™™™™2@ÍÌÌÌÌÌ+@š™™™™™@333333B@*@ffffffö?š™™™™™@@ffffff#@š™™™™™@š™™™™™"@ffffff!@3333330@ÍÌÌÌÌÌ*@š™™™™™#@ffffff@ffffff.@ffffff-@fffffæ6@ffffff@ÍÌÌÌÌÌ>@ffffff)@š™™™™™3@š™™™™™@333333)@fffffæ2@ÍÌÌÌÌL2@ffffff@333333*@$@*@)@333333$@ÍÌÌÌÌÌ%@ffffff @ffffff@ÍÌÌÌÌÌ(@)@ffffff"@ÍÌÌÌÌÌ@š™™™™™(@ÍÌÌÌÌÌ@+@!@ÍÌÌÌÌÌ/@@ffffff @333333 @š™™™™™-@š™™™™™(@ÍÌÌÌÌÌ/@ffffff&@ÍÌÌÌÌL3@333333/@ÍÌÌÌÌÌ,@ffffff#@ÍÌÌÌÌÌ"@š™™™™™*@3333334@%@š™™™™™+@333333+@š™™™™™'@š™™™™™8@-@ffffff"@ÍÌÌÌÌÌ"@$@š™™™™2@333333#@ÍÌÌÌÌÌ(@ÍÌÌÌÌÌ%@333333)@333333(@š™™™™™.@š™™™™™+@š™™™™™@fffffæ3@ÍÌÌÌÌÌ!@ÍÌÌÌÌÌ7@š™™™™2@333333+@š™™™™™0@333333)@ffffff#@ÍÌÌÌÌÌ,@)@š™™™™™"@š™™™™™/@@>@ffffff#@@š™™™™™$@333333%@ffffff/@ÍÌÌÌÌÌ@š™™™™™/@š™™™™™0@š™™™™™!@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333'@š™™™™™@ffffff&@ffffff#@ffffff@ÍÌÌÌÌÌì?š™™™™™'@š™™™™™2@333333%@"@!@ÍÌÌÌÌÌ(@33333³=@ÍÌÌÌÌÌ6@33333³1@ffffff @ÍÌÌÌÌÌ@š™™™™™@333333!@ÍÌÌÌÌÌ+@š™™™™™,@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ*@€0@ÍÌÌÌÌÌ/@333333@ÍÌÌÌÌÌ"@ffffff)@ÍÌÌÌÌÌ'@š™™™™™ @ffffff*@ÍÌÌÌÌL5@@.@ffffff0@3333331@333333%@333333@ffffff+@333333@333333&@ÍÌÌÌÌL5@ffffff"@+@fffffæ4@33333³1@33333³2@ÍÌÌÌÌÌ.@ÍÌÌÌÌÌ'@š™™™™™&@ffffff-@š™™™™™,@š™™™™0@š™™™™™ @ffffff-@š™™™™™@333333(@ÍÌÌÌÌÌ&@ÍÌÌÌÌÌ@š™™™™™(@ð?ffffff)@ffffff$@fffffæ0@ffffffæ?333333@ÍÌÌÌÌÌ@ffffff)@š™™™™™$@!@ÍÌÌÌÌÌ$@ÍÌÌÌÌÌ@)@ffffff'@ÍÌÌÌÌÌ @@333333 @@š™™™™™@ffffff&@ÍÌÌÌÌÌ@š™™™™™'@333333.@ffffff*@ffffff@š™™™™™)@333333@$@333333!@š™™™™™@€3@ÍÌÌÌÌÌ@ffffff6@ÍÌÌÌÌÌ#@333333 @ÍÌÌÌÌÌ)@š™™™™™#@ÍÌÌÌÌÌ(@@ÍÌÌÌÌÌ%@@š™™™™™'@š™™™™7@333333=@ÍÌÌÌÌÌ.@'@3333332@ffffff,@@ÍÌÌÌÌL4@33333³0@333333;@333333 @333333.@ffffff#@ÍÌÌÌÌÌ2@š™™™™™ù?š™™™™0@ÍÌÌÌÌÌ-@ffffff!@ffffff*@333333)@fffffæ8@ÍÌÌÌÌÌ @š™™™™™ @fffffæ0@ffffff5@(@33333³6@š™™™™™ @š™™™™™ @š™™™™™+@ffffff4@€9@$@333333!@š™™™™™.@ffffff*@333333*@#@ffffff,@ÍÌÌÌÌL0@fffffæ3@ffffff5@fffffæ9@ÍÌÌÌÌÌ @#@ÍÌÌÌÌÌ&@0@ffffff@ÍÌÌÌÌÌ*@š™™™™™@š™™™™™)@š™™™™™.@333333-@'@ÍÌÌÌÌL8@!@333333@$@333333!@%@333333)@fffffæ6@à?ÍÌÌÌÌÌ@ffffff@1@333333@333333'@€4@ffffff@333333"@!@.@š™™™™™@33333³;@ÍÌÌÌÌÌ2@š™™™™™@ffffffþ?ÍÌÌÌÌÌ@333333*@ffffff@3333333@ÍÌÌÌÌÌ"@š™™™™™@333333@ffffff@ÍÌÌÌÌÌ+@333333@ÍÌÌÌÌÌ@š™™™™1@š™™™™™+@33333³2@ffffff$@333333(@š™™™™™"@333333%@š™™™™™@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ"@š™™™™™'@333333+@ÍÌÌÌÌÌ*@š™™™™™ @ffffff$@333333@š™™™™™ @ÍÌÌÌÌÌ@ffffff"@ÍÌÌÌÌL;@@333333@š™™™™™2@'@333333,@š™™™™™7@@33333³2@5@5@-@ÍÌÌÌÌL0@333333(@(@3333331@333333'@š™™™™™@333333$@š™™™™™#@ÍÌÌÌÌÌ%@333333*@ffffff$@š™™™™™@ÍÌÌÌÌÌ @š™™™™™'@š™™™™™/@ffffff"@ÍÌÌÌÌÌ(@1@ffffff4@333333&@333333û?ÍÌÌÌÌÌ"@š™™™™™%@ffffff+@š™™™™™ @ffffff-@ÍÌÌÌÌÌ%@ffffff0@š™™™™™1@ÍÌÌÌÌÌ@333333@š™™™™™@ffffff@,@333333@€6@š™™™™™4@ÍÌÌÌÌÌ'@š™™™™™7@ÍÌÌÌÌÌ ÀÀÍÌÌÌÌÌÀÀ333333À33333³9Àš™™™™™ÀffffffÀ333333Àš™™™™™ Àš™™™™™*ÀffffffÀ333333ÀÍÌÌÌÌÌÀš™™™™™ñ¿333333À333333 À333333ÀÍÌÌÌÌÌ#ÀÀ333333Àš™™™™™1ÀÍÌÌÌÌÌ ÀÀš™™™™™À333333/ÀÍÌÌÌÌÌÀš™™™™™ Àš™™™™™Àš™™™™™ÀÍÌÌÌÌÌÀffffffÀš™™™™™ÀÀ333333Àffffff À333333&ÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀÀš™™™™™ÀÍÌÌÌÌÌÀffffffÀÍÌÌÌÌÌü¿333333Àš™™™™™ÀffffffÀffffff À333333 Àš™™™™™"Àš™™™™™Àš™™™™™"Àš™™™™™#ÀÍÌÌÌÌÌì¿Àš™™™™™ñ¿ÍÌÌÌÌÌÀffffff!À333333+ÀÍÌÌÌÌÌÀ333333'ÀÍÌÌÌÌÌÀš™™™™™ÀffffffÀffffffÀÍÌÌÌÌÌ Àffffff#À333333û¿š™™™™™Àffffff&Àffffffþ¿ÍÌÌÌÌÌÀ333333À333333Àš™™™™™ ÀÍÌÌÌÌÌÀÀš™™™™™À333333%ÀÍÌÌÌÌÌÀffffffÀ333333"Àš™™™™™!ÀÀffffff<À333333!Àš™™™™™À(ÀÍÌÌÌÌÌÀ333333"ÀffffffÀffffff ÀÍÌÌÌÌÌÀÍÌÌÌÌÌ Àš™™™™™À333333Àš™™™™™ÀÍÌÌÌÌÌ"ÀffffffÀš™™™™™Àffffff<Àš™™™™™Àffffffæ¿ffffff-ÀÍÌÌÌÌÌ!À333333 À33333sEÀ333333ÀffffffÀ333333À333333 À(ÀÍÌÌÌÌÌ!Àš™™™™™ À3333334ÀÀš™™™™™ À333333À333333 À333333*Àffffff)Àffffff$À333333 À ÀÀÍÌÌÌÌÌ ÀÍÌÌÌÌÌÀ$Àffffff*ÀÍÌÌÌÌÌÀÀÍÌÌÌÌÌ$Àš™™™™™ÀÍÌÌÌÌÌ ÀffffffÀ À Àš™™™™™#ÀÍÌÌÌÌÌ À333333'Àš™™™™™ ÀÀ#Àš™™™™™!Àffffff Àš™™™™™À333333ÀÍÌÌÌÌÌÀ À333333"Àš™™™™™!Àà¿ÍÌÌÌÌÌô¿š™™™™™Àš™™™™™ À'ÀffffffÀ Àš™™™™™Àš™™™™™'À ÀÍÌÌÌÌÌü¿š™™™™™Àffffff&ÀffffffÀš™™™™™%À333333)Àffffff#Àš™™™™™.À(À ÀÀÀÍÌÌÌÌÌÀffffffÀÀÍÌÌÌÌÌÀffffffÀÍÌÌÌÌÌÀ333333À333333ÀÍÌÌÌÌÌ#ÀÍÌÌÌÌÌÀÀffffff$À"Àš™™™™™ÀffffffÀš™™™™™Àš™™™™™Àš™™™™™Àš™™™™™Àš™™™™™ Àš™™™™™À333333Àffffff)ÀÍÌÌÌÌÌ+ÀffffffÀ𿚙™™™™"ÀffffffÀffffffþ¿333333+ÀÍÌÌÌÌÌ&ÀffffffÀÍÌÌÌÌÌÀffffff/Àš™™™™3À+ÀÍÌÌÌÌÌÀffffffÀ333333ÀÍÌÌÌÌÌÀÍÌÌÌÌÌ,ÀffffffÀffffff$À333333"À333333ÀffffffÀ333333ÀÍÌÌÌÌÌÀÀffffffÀ333333Àffffff À333333ÀÀš™™™™™&ÀÀÍÌÌÌÌÌÀffffffÀÀš™™™™™%ÀÀ333333À333333Àš™™™™™Àffffff-ÀffffffÀÀffffffÀÍÌÌÌÌÌ*À333333ÀÍÌÌÌÌÌ!À333333À&Àš™™™™™ÀÍÌÌÌÌÌ#À333333Àš™™™™™À333333û¿ÍÌÌÌÌÌ À333333ÀÀš™™™™™ÀffffffÀš™™™™™À333333Àš™™™™™ÀffffffÀffffff Àø¿ÍÌÌÌÌÌ!ÀffffffÀÍÌÌÌÌÌ À333333Àffffffþ¿Àø¿Àš™™™™™ À(ÀÀÍÌÌÌÌÌÀÍÌÌÌÌÌ"Àš™™™™™ ÀÀffffffÀÀffffffÀš™™™™™ ÀÀÍÌÌÌÌÌÀš™™™™™ÀÀ Àffffff$Àffffff ÀÍÌÌÌÌÌÀ+À333333&ÀÍÌÌÌÌÌ*À333333ÀÍÌÌÌÌÌ)ÀÍÌÌÌÌL2ÀffffffÀ333333ó¿š™™™™™À333333ÀffffffÀš™™™™0ÀÍÌÌÌÌÌ4ÀÀffffffÀš™™™™™À333333*ÀffffffÀffffffÀ333333+À333333'ÀÍÌÌÌÌÌ'Àffffff&Àš™™™™™2À333333Àffffffþ¿ÍÌÌÌÌÌ$Àš™™™™™À333333 Àš™™™™™ÀÀš™™™™™*Àffffff&Àffffff$Àš™™™™™ÀÍÌÌÌÌÌ À À333333ÀffffffÀ333333À333333Àš™™™™™$ÀÀÀš™™™™™À Àš™™™™™À"Àffffff)Àš™™™™™ÀffffffÀš™™™™™Àš™™™™™À3333336À333333$Àš™™™™™*Àš™™™™™Àffffff!Àš™™™™™À333333!À333333Àš™™™™™Àš™™™™™ÀÍÌÌÌÌÌ(Àš™™™™™#Àš™™™™™(Àš™™™™™!À ÀÍÌÌÌÌÌÀÀÍÌÌÌÌÌÀÀš™™™™™ÀÍÌÌÌÌÌ,À333333 ÀÍÌÌÌÌÌÀ)ÀÀÍÌÌÌÌÌÀffffffÀffffff'ÀÀ(Àš™™™™™ À333333#À333333Àš™™™™™À*Àš™™™™™ Àà¿3333331Àfffffæ6À333333 Àš™™™™™ÀÍÌÌÌÌÌ,ÀÀÀ+Àš™™™™™$À333333#À333333û¿ffffff Àffffffö¿Àš™™™™™+ÀÍÌÌÌÌÌ#ÀÍÌÌÌÌÌ!À Àš™™™™™"À3333332Àš™™™™™ù¿š™™™™™Àš™™™™™À3333336ÀÍÌÌÌÌÌÀš™™™™™ ÀÍÌÌÌÌÌÀÍÌÌÌÌÌ"ÀffffffÀ333333$À€>ÀffffffÀÀÍÌÌÌÌÌ.Àš™™™™™ÀÀš™™™™™ÀÀš™™™™™,Àš™™™™™!À"ÀÍÌÌÌÌÌÀ333333À333333ÀÀÍÌÌÌÌÌÀÍÌÌÌÌÌ!ÀÀÀÀ À#ÀÍÌÌÌÌÌÀš™™™™™$À Àš™™™™YBÀÍÌÌÌÌÌÀ333333Àš™™™™™é?š™™™™™Ù?ffffffæ?à?333333@à?à?à?ð?ffffffö?š™™™™™Ù?333333ã?à?à?ÍÌÌÌÌÌì?ÍÌÌÌÌÌ쿚™™™™™Ù¿ÍÌÌÌÌÌì?à?à?š™™™™™¹?š™™™™™¹¿š™™™™™é?š™™™™™Ù?333333Ó?ÍÌÌÌÌÌì?š™™™™™ñ?ÍÌÌÌÌÌì?à?š™™™™™é?333333ã?333333Ó?ffffffæ?š™™™™™Ù?š™™™™™Ù?333333ã?à?š™™™™™Ù?333333ó?š™™™™™Ù?à?333333ã?333333ã?ð?333333Ó?š™™™™™É?š™™™™™É?ffffffæ?à?333333ã?ffffffæ?ð?ffffffæ?ffffffæ?š™™™™™ñ?ð?à?333333ã?š™™™™™É?š™™™™™ñ?333333Ó?333333ã?š™™™™™é?333333Ó?333333ã?š™™™™™Ù?š™™™™™é?333333ã?š™™™™™é?š™™™™™¹?à?š™™™™™Ù?333333ã?š™™™™™¹¿ffffffæ?333333ã?à?š™™™™™ñ?š™™™™™é?š™™™™™é?à?š™™™™™É¿à?à?ffffffæ¿ÍÌÌÌÌÌì?ð?š™™™™™é?š™™™™™Ù?à?ffffffæ?ffffffæ?à?š™™™™™é?à?ÍÌÌÌÌÌô?ÍÌÌÌÌÌì?à?ÍÌÌÌÌÌô?333333㿚™™™™™é?š™™™™™Ù?š™™™™™Ù?š™™™™™É?333333ã?à?à?333333Ó?ð?333333ã?333333ã?š™™™™™É¿ÍÌÌÌÌÌì?š™™™™™¹?333333Ó?ffffffæ?333333ã?ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?š™™™™™¹¿à?š™™™™™ñ?333333ã?š™™™™™é?ffffffþ?333333Ó?333333ã¿ÍÌÌÌÌÌì?à?à?š™™™™™Ù?à?š™™™™™Ù?à?333333Ó?333333ã?à?š™™™™™Ù?ÍÌÌÌÌÌì?333333ã?333333Ó?ÍÌÌÌÌÌì?à?ffffffæ?š™™™™™é?ffffffæ?333333ã?333333ó?333333ã?š™™™™™¹¿š™™™™™Ù?ÍÌÌÌÌÌì?333333ó?333333ã?ÍÌÌÌÌÌô?333333ã?ffffffæ?ffffffö?333333ã?š™™™™™é?ffffffæ?ð?š™™™™™é?à?ÍÌÌÌÌÌì?ffffffæ?š™™™™™É?ffffffæ?333333ã?333333Ó?ffffffæ?333333ã?š™™™™™é?à?333333ã?š™™™™™Ù¿333333ã?ffffffö?333333ã?à?š™™™™™Ù?ð?ffffffæ?š™™™™™é?š™™™™™Ù?à?333333ã?š™™™™™Ù?ÍÌÌÌÌÌÀà?333333Ó?333333Ó¿š™™™™™é?š™™™™™É?ð?š™™™™™Ù?š™™™™™É?333333ã?à?š™™™™™é?ffffffæ?333333Ó?š™™™™™É?š™™™™™Ù?š™™™™™¹¿à?ffffffæ?333333ã?à?333333ã?š™™™™™Ù?ffffffæ?ffffff@333333ã?333333Ó?à?š™™™™™Ù?333333Ó?š™™™™™ñ?š™™™™™Ù?š™™™™™Ù?333333Ó?333333ã?ffffffæ?ð?333333ã?ð?ð?à?à?š™™™™™Ù?š™™™™™é?š™™™™™É?333333ã?333333Ó?š™™™™™Ù?ð?333333Ó?š™™™™™é?ffffffæ?ð?333333ã?š™™™™™Ù?333333ã?ÍÌÌÌÌÌì?333333ã?333333ã?ð?š™™™™™É?š™™™™™Ù?à?ffffffæ?ð?333333Ó?333333Ó?à?ffffffæ¿ÍÌÌÌÌÌì?à?333333ã?333333ã?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?333333ã?ð?à?š™™™™™Ù?333333ã?š™™™™™é?333333Ó?š™™™™™é?333333Ó?333333ã?ÍÌÌÌÌÌì?333333ã?333333Ó?à?ffffffæ?333333ã?ð?à?à?333333ã?333333Ó¿333333ã?ffffffæ?333333Ó¿ffffffæ?š™™™™™ñ?š™™™™™Ù?333333ã?š™™™™™¹¿333333ã?à?à?à?࿚™™™™™Ù?à?à?à?š™™™™™Ù?š™™™™™¹?ffffffæ?š™™™™™ñ?ð¿à?333333Ó?š™™™™™Ù?333333ó?333333ó¿ffffffæ?ffffffö?333333ã?333333ã?333333ó?333333ã?ffffffæ?333333Ó?à?š™™™™™Ù¿š™™™™™¹¿ð?ÍÌÌÌÌÌì?à?à?š™™™™™é?333333Ó?š™™™™™é?ÍÌÌÌÌÌô?ffffffæ?š™™™™™ñ?š™™™™™é?333333Ó?ffffffæ?š™™™™™é?333333ã?333333ã?ffffffæ?ÍÌÌÌÌÌì?š™™™™™¹?333333ã?ø?333333ã?ffffffæ?333333ã?333333ã?š™™™™™é?š™™™™™ñ?à?ffffffæ?š™™™™™Ù?à?à?à?333333ã?ffffffæ?à?333333ã?ÍÌÌÌÌÌì?333333ã?š™™™™™é?333333Ó?š™™™™™Ù?š™™™™™é?ð?š™™™™™Ù?š™™™™™Ù?333333ã?à?š™™™™™Ù?š™™™™™ñ?ÍÌÌÌÌÌì?à?333333ã?à?à?ffffffæ¿à?š™™™™™Ù?333333Ó?ffffffæ?333333Ó?à?ffffffæ?à?š™™™™™Ù?š™™™™™é?ffffffæ?š™™™™™é?š™™™™™¹?ffffffæ?à?333333ã?š™™™™™é?š™™™™™Ù?ffffffæ?333333Ó¿à?š™™™™™É?š™™™™™¹?333333ã?š™™™™™Ù?à?š™™™™™é¿333333ã?š™™™™™é?š™™™™™É¿333333ã?š™™™™™Ù?š™™™™™Ù?à¿333333ã?333333ã?333333㿚™™™™™é?š™™™™™¹¿ð?ø?333333Ó?ÍÌÌÌÌÌ쿚™™™™™Ù?ffffffæ?š™™™™™¹¿333333ó?ffffffæ?ffffffæ?š™™™™™ñ?ffffffæ?333333ã?š™™™™™Ù?š™™™™™¹?š™™™™™é?333333ã?ÍÌÌÌÌÌì?š™™™™™é?à?š™™™™™Ù?ffffffæ?š™™™™™Ù?333333ó?š™™™™™¹?à?š™™™™™É?à?à?š™™™™™é?333333ã?ð?š™™™™™é?ffffffæ?š™™™™™é?ffffffæ?ffffffæ?ð?š™™™™™é?ÍÌÌÌÌÌì¿333333ã?š™™™™™é?ffffffæ?ð?333333ã?à?ffffff@ÍÌÌÌÌÌ@š™™™™™@ffffff @š™™™™™@š™™™™™@ffffffþ?ffffffö?@ÍÌÌÌÌÌô?ÍÌÌÌÌÌÀ333333Ó?š™™™™™ñ?š™™™™™@ffffffþ?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌì?@ÍÌÌÌÌÌ @@333333@333333@ÍÌÌÌÌÌì?ffffffþ?333333 @š™™™™™@ffffff @333333û¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌ@ffffffþ?@ffffff@333333ó?333333ó?š™™™™™@@ @ø?š™™™™™@ffffff@@333333û?333333@ø? @ÍÌÌÌÌÌô?ð?ø?š™™™™™ñ?ø?333333@@333333ã?ffffff@@š™™™™™ù?š™™™™™É?@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333@333333@@š™™™™™@ÍÌÌÌÌÌÀffffff @ÍÌÌÌÌÌ@333333û?ÍÌÌÌÌÌ@333333@333333 @ffffffþ?š™™™™™@š™™™™™@@333333ó¿š™™™™™@333333û?@ÍÌÌÌÌÌ@ÍÌÌÌÌÌì?333333@333333@333333/@ÍÌÌÌÌÌÀ@ÍÌÌÌÌÌô?ð?ÍÌÌÌÌÌì?ffffff@ÍÌÌÌÌÌ@š™™™™™ñ?š™™™™™@ffffff@ffffff @@333333ã?ffffffæ¿333333ó?à?ð?š™™™™™"@š™™™™™ù?333333ã?š™™™™™@ffffff@š™™™™™$@š™™™™™Ù?ffffff @ffffff@š™™™™™@333333@ÍÌÌÌÌÌ@@ffffffö?ffffffö?ÍÌÌÌÌÌô?ÍÌÌÌÌÌü?ffffff @ÍÌÌÌÌÌü?@š™™™™™@š™™™™™@ÍÌÌÌÌÌ@333333@ffffff@š™™™™™@ffffff@333333@ÍÌÌÌÌÌô?š™™™™™@@ffffff@@@š™™™™™é?š™™™™™é?@š™™™™™@333333û?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ffffff@ffffff@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333û?ð?@@333333@ÍÌÌÌÌÌü?ÍÌÌÌÌÌ@ffffff@333333ã?à?ffffff@ffffffþ?ÍÌÌÌÌÌ@333333Ó?š™™™™™@333333@ÍÌÌÌÌÌü?333333ó?ÍÌÌÌÌÌ@ffffff@ÍÌÌÌÌÌü?@š™™™™™Ù?ÍÌÌÌÌÌ@@ffffffö?333333û?333333Ó?333333û?ÍÌÌÌÌÌü?333333@à¿ÍÌÌÌÌÌü?@ÍÌÌÌÌÌì?ø?à?333333ã?à?ÍÌÌÌÌÌì?ffffff@ffffff @ÍÌÌÌÌÌ@333333û?ÍÌÌÌÌÌì¿333333ã¿@ÍÌÌÌÌÌ@š™™™™™@ffffff@ffffffæ?ø?š™™™™™É?ø?š™™™™™Ù¿ffffff @ffffff@333333@ffffff@333333@ÍÌÌÌÌÌ@@ÍÌÌÌÌÌ@333333@@ffffff@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌô¿à?ffffff@š™™™™™@333333ó?ÍÌÌÌÌÌ @@ÍÌÌÌÌÌ@333333@š™™™™™é? @@333333 @333333@š™™™™™Ù?à¿ffffff@š™™™™™ù?š™™™™™@ÍÌÌÌÌÌÀš™™™™™@333333À@ffffffþ? @333333ã?ð?ø?š™™™™™ù?333333ã?@ffffff@š™™™™™é?š™™™™™ @ffffffþ?ÍÌÌÌÌÌ@š™™™™™Ù?ÍÌÌÌÌÌì?ffffff@ffffff@š™™™™™ù¿ÍÌÌÌÌÌ@à?ø?333333Ó?@ffffff@à?ffffff@333333ã?š™™™™™ñ?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@@@ffffff@333333@š™™™™™Ù?š™™™™™¹?333333Ó?ffffff@ffffffö¿ffffff@333333@@333333@š™™™™™ @ÍÌÌÌÌÌü?ÍÌÌÌÌÌ@š™™™™™ñ?ÍÌÌÌÌÌì?š™™™™™ù?ffffffö?ÍÌÌÌÌÌì?š™™™™™é?š™™™™™@ÍÌÌÌÌÌ @@ø¿333333û?š™™™™™ Àffffffþ?š™™™™™@333333@ffffff@333333û?š™™™™™ @à?ffffffæ¿ÍÌÌÌÌÌ@ÍÌÌÌÌÌô?@ffffff@ÍÌÌÌÌÌ@@333333ó?@ffffffæ?š™™™™™ @š™™™™™ù?333333@š™™™™™@ffffff@@š™™™™™Ù?ÍÌÌÌÌÌì?333333ÀffffffÀ333333@333333ó?š™™™™™ù?š™™™™™ù?333333û¿ø¿ffffff@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ffffff@333333@š™™™™™¹¿š™™™™™@ffffff@š™™™™™@333333@ffffff@ÍÌÌÌÌÌ@š™™™™™ñ?ÍÌÌÌÌÌô?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ffffff@@š™™™™™@ÍÌÌÌÌÌ@ffffff @333333ã?ø¿à?ÍÌÌÌÌÌ@333333û?ffffff@ð¿333333@ÍÌÌÌÌÌü¿333333û¿333333@ffffffæ?ÍÌÌÌÌÌ@š™™™™™ @ffffff @333333ó?333333ã¿333333 ÀÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌì?À@333333@š™™™™™É?ffffffö?ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?Àffffffæ?333333@ffffff@@ÍÌÌÌÌÌ@š™™™™™ñ?š™™™™™@333333@333333û¿š™™™™™é¿š™™™™™ù?š™™™™™@ffffffæ? @ð?š™™™™™@ffffff @ÍÌÌÌÌÌ@š™™™™™@ø?š™™™™™ @ffffff @ÍÌÌÌÌÌ@š™™™™™ù?@@ffffff@š™™™™™ @š™™™™™É?š™™™™™@333333ã?333333 @ÍÌÌÌÌÌ@ÍÌÌÌÌÌ @ffffff@ffffffæ?@ÍÌÌÌÌÌ@š™™™™™Ù?@Àà?ÍÌÌÌÌÌ@ÍÌÌÌÌÌü?333333!@@ffffff@333333û?ffffffæ?333333Àš™™™™™é?@@š™™™™™Ù¿š™™™™™é¿ÍÌÌÌÌÌô?ÍÌÌÌÌÌ@š™™™™™ñ?333333 @333333@š™™™™™@333333@ÍÌÌÌÌÌì? @š™™™™™ @333333 @š™™™™™@ffffffö?š™™™™™@ffffff Àffffff@333333@33333³1@š™™™™™'@ÀÍÌÌÌÌÌ<@3333330@333333=@ffffffö?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ffffff*@)Àš™™™™™!Àffffff@ÍÌÌÌÌŒN@ÍÌÌÌÌÌ=@š™™™™™3@€0@š™™™™™,@ø¿333333K@š™™™™™5@!À1@#@ffffff4@>Àš™™™™™F@€3@@333333"ÀÍÌÌÌÌÌ@ÍÌÌÌÌÌ&@333333@€5@ÍÌÌÌÌÌ@€2@ffffffö¿3333333@)@ffffff1@3333336@ÀÍÌÌÌÌL?@333333L@ffffff*@"Àffffff @ÍÌÌÌÌÌ.@333333@ÍÌÌÌÌÌ!Àš™™™™™2@333333@2@ÀQ@333333,@š™™™™™-@3333330@ð¿ffffff.@ffffff@333333@333333/@ÍÌÌÌÌÌ+@ffffffC@@F@@333333ÀÍÌÌÌÌÌA@ffffff;@ÍÌÌÌÌÌÀ€7@1@333333+@@ÍÌÌÌÌÌ>@fffffæ?@ÍÌÌÌÌlQÀffffff@ffffffÀ333333-@ÍÌÌÌÌL?@ÍÌÌÌÌL7@ ÀÍÌÌÌÌÌ5@š™™™™<@33333“]À33333sQ@333333-@fffff†QÀ@š™™™™™"ÀÍÌÌÌÌÌ%@ffffffÀÍÌÌÌÌÌ@ffffffÀš™™™™A@ÍÌÌÌÌL8@ffffff%@1Àfffffæ5@€8@š™™™™ÙY@fffffFRÀàU@ffffffA@š™™™™™@ffffff&Àffffff7@IÀffffffA@š™™™™™@ffffff-@@333333#@š™™™™™@333333-@ffffffCÀø?33333³4@fffffæ7@š™™™™™(À,Àffffff!@#@€;@š™™™™™É?fffffæ=@@ÍÌÌÌÌÌÀ333333!À;@š™™™™™5@š™™™™™@ÍÌÌÌÌÌ @333333:@3333339@š™™™™™@3333330@š™™™™™Àš™™™™™;@fffffæ5@ÍÌÌÌÌÌü¿?@ffffff'@ffffff@ÍÌÌÌÌÌ5@;@ffffff @ÍÌÌÌÌÌ&@33333³1@ÍÌÌÌÌ B@ffffffB@;@!@33333³9@ÍÌÌÌÌÌì¿ÍÌÌÌÌ F@ffffffÀfffffæA@@ffffffö?333333G@ffffff@@fffff&A@@ÍÌÌÌÌÌ/@ÍÌÌÌÌÌÀffffff@š™™™™™ÀÍÌÌÌÌL1ÀÍÌÌÌÌÌÀ€E@333333@@ffffff7@ÍÌÌÌÌL2@3333332@333333'@33333³:@33333³<@š™™™™™Àš™™™™™é?-À333333;@333333Ó¿š™™™™™/À333333@33333³0À333333!À333333)@E@3333337@ÍÌÌÌÌÌ'@*@ffffff<@ÍÌÌÌÌÌM@3333333@333333@333333+@š™™™™™*À4@ffffffTÀš™™™™A@ÀÍÌÌÌÌŒCÀ33333s@@fffffæA@"Àš™™™™™7Àš™™™™™'À333333%@3@ffffff@š™™™™™2@ÍÌÌÌÌÌ À33333³PÀš™™™™™@333333@333333)@fffffæ9@ÍÌÌÌÌL:@ÍÌÌÌÌÌ#@ÍÌÌÌÌÌü¿3@-@333333$@ffffffF@3333330@ffffffE@fffffæC@š™™™™C@š™™™™YH@@fffffæ2@333333@š™™™™;@fffff&@@ÍÌÌÌÌÌ @ÍÌÌÌÌÌÀ€9@š™™™™™Àš™™™™6@š™™™™0À*@ÍÌÌÌÌÌ#@ffffff,À7@ÍÌÌÌÌÌ(Àfffff¦@@ffffff8Àš™™™™™#À3333331À333333(@š™™™™™ñ¿š™™™™™JÀffffff!@š™™™™™!@š™™™™™ @ffffff@ÍÌÌÌÌL;@&@333333@#@ffffff@ffffffÀš™™™™™(@ffffff@8@fffff¦A@š™™™™™@ð¿ÍÌÌÌÌÌ!@ffffffö¿š™™™™:@ffffff%@333333.@ÍÌÌÌÌÌD@š™™™™™@ffffff=@€0@@ÍÌÌÌÌÌ Àš™™™™™ÀÍÌÌÌÌL8@333333@333333)@š™™™™™Àš™™™™™@š™™™™™I@š™™™™ÙI@fffffæ?@€?@š™™™™ÙA@@š™™™™2@fffff&D@*@ÍÌÌÌÌL9@ÍÌÌÌÌL5Àš™™™™™:@3333338Àffffff(À3333337@š™™™™™-À€@@fffffæ4@ø¿fffff&A@ÍÌÌÌÌL1ÀIÀÍÌÌÌÌÌ%À@<@333333,Àffffff#@fffff&A@33333³@À333333Ó?ffffff'@ffffff/@333333/@š™™™™™@€2@ffffff#@ÍÌÌÌÌÌ @333333?@š™™™™™Ù¿ffffff.@ffffff@fffffæ4@š™™™™™:@ÀC@š™™™™™#@ffffff4@33333³1@fffff&C@ffffff@€1@ffffff)Àffffff@333333=@fffff¦B@ÍÌÌÌÌÌ2@š™™™™ÙE@ÍÌÌÌÌÌ@š™™™™™<Àffffff@ÍÌÌÌÌL@@ÍÌÌÌÌÌ4@#@š™™™™E@ÀRÀ#Àfffffæ<À@@333333ÀÍÌÌÌÌÌ;@@F@ÀEÀ3À33333³1@ÍÌÌÌÌÌ@ÍÌÌÌÌL>@€3Àfffff&D@ffffff @ffffffÀÍÌÌÌÌÌ*Àffffff3@ÍÌÌÌÌÌ@333333;@ffffff@@À333333û?-@ffffffJ@ÍÌÌÌÌÌ.À333333ó¿š™™™™™:@ÍÌÌÌÌÌ4@š™™™™3@ffffff2@š™™™™™Àš™™™™™&@ÍÌÌÌÌÌÀš™™™™™ÀffffffÀš™™™™™9@33333sD@€;À333333ZÀ1@ffffff"@>À333333$Àffffff@ÍÌÌÌÌÌ8@333333"ÀÍÌÌÌÌLFÀfffff¦E@33333³5@?@fffffæN@ffffff;Àffffff3@€?@ffffff@@€=@š™™™™™@ÍÌÌÌÌL?@1@š™™™™™>@33333sCÀš™™™™™Ù¿9@ffffff!@333333û?š™™™™?@ÍÌÌÌÌÌ#À333333_À+@3333337@fffffæ:À @ÍÌÌÌÌŒB@š™™™™™1@33333sA@š™™™™™Àš™™™™8Àš™™™™™@ffffff7@š™™™™<@ffffff@š™™™™7@ @š™™™™™8@fffff¦L@ÍÌÌÌÌÌ@@(@š™™™™™&Àfffffæ<@š™™™™4ÀÍÌÌÌÌLQ@ÍÌÌÌÌÌQÀ€1@F@ÍÌÌÌ̬Q@ÍÌÌÌÌLA@33333³4@ÍÌÌÌÌŒO@š™™™™™O@33333sU@ÍÌÌÌÌÌ.@ffffff,@33333sO@ÍÌÌÌÌL6@fffff¦BÀš™™™™™ÀÍÌÌÌÌÌ*@ÍÌÌÌÌÌY@š™™™™™G@€F@ÍÌÌÌÌLN@3333334@33333³:@€U@333333O@ffffff.@€C@š™™™™™2@ÍÌÌÌÌŒB@ø¿0`@fffffæH@š™™™™™-ÀÍÌÌÌÌÌ4Àš™™™™YF@33333³;@fffff¦A@š™™™™YE@ffffff&@33333³9@1@ÍÌÌÌÌLV@ÍÌÌÌÌLE@š™™™™™;@33333³F@ÍÌÌÌÌÌ*@33333óH@333333T@fffffæA@ÍÌÌÌÌÌì?€<@3333339@š™™™™™*@333333 @€:@ÍÌÌÌÌL4@333333G@fffff&Z@33333³1@ÀA@š™™™™ÙA@333333(@ffffff1@š™™™™™<@š™™™™™4@33333³B@ÍÌÌÌÌŒB@ÍÌÌÌÌLR@ÍÌÌÌÌìU@fffffæ9@333333:Àfffff¦N@fffff¦K@ÍÌÌÌÌÌ @š™™™™YI@ÍÌÌÌÌ J@ÀE@33333³5@ÍÌÌÌ̬[@š™™™™YO@ÍÌÌÌÌÌ@ÀÍÌÌÌÌÌ'@€;@fffff¦F@33333óN@š™™™™™@33333óD@G@R@fffffæ<@ÍÌÌÌÌÌC@š™™™™YJÀ1@à¿fffff¦D@€7@+@fffffÆQ@fffff&P@ÍÌÌÌÌLL@33333óF@ÍÌÌÌÌÌ%Àš™™™™0@@A@ÍÌÌÌ̬Z@àPÀš™™™™Éf@fffff&G@%@fffff&M@š™™™™™S@àQ@ÍÌÌÌÌLB@fffffæ=@š™™™™K@33333³5@333333Q@33333³7@ÍÌÌÌÌŒ@@33333³9Àš™™™™™&@š™™™™™ @33333³A@fffff&H@ @š™™™™™7@33333óS@ÍÌÌÌÌL9@fffffÆQ@fffffæ7@fffff&H@š™™™™™3@š™™™™™-@333333E@@A@33333óF@333333K@fffffæH@fffff&P@ÍÌÌÌ̬R@š™™™™™$@š™™™™™5@ÍÌÌÌÌÌ2@ÍÌÌÌÌLH@333333@@ffffff9@fffff&J@33333óP@ffffffD@ÍÌÌÌÌÌM@fffffæG@š™™™™™/@ffffff1@ÍÌÌÌÌ A@fffffF\@fffff&A@333333I@ÍÌÌÌÌ L@š™™™™™K@€>@ffffff@š™™™™ÙM@ÍÌÌÌÌÌ+@fffff†S@@@K@fffff&V@33333³F@š™™™™™D@33333³P@€W@ffffff#@ÍÌÌÌÌL=@ÍÌÌÌÌÌÀ@š™™™™YH@š™™™™J@ffffff>@333333@33333ó@@ÍÌÌÌÌÌ=@fffff&K@š™™™™™@š™™™™YD@ÀQ@@ffffff"@ Àfffffæ=@ffffffþ?š™™™™™ù¿š™™™™YE@fffffæ6@>@333333<@š™™™™™B@ÍÌÌÌÌÌ1@33333³@@ÍÌÌÌÌÌC@š™™™™YR@fffff&U@ÍÌÌÌÌL8@ffffff*@ÍÌÌÌÌÌ!@33333³8@3333331Àš™™™™ÙH@fffffæCÀfffff&M@ffffff9@=@š™™™™ÙQ@€N@33333³E@ffffffB@3333338@š™™™™™A@33333óE@ÍÌÌÌÌL:@ÍÌÌÌÌÌ @ÍÌÌÌÌÌÀffffffEÀ€5@&@ÍÌÌÌÌLH@fffffFW@33333R@B@333333ó?ffffffL@33333sB@fffff¦C@ÍÌÌÌÌlT@3@fffff¦B@fffffæT@33333³G@ÍÌÌÌÌìW@BÀÍÌÌÌ̬Q@(ÀÍÌÌÌÌÌG@33333sE@33333óC@ø?fffff¦@@ð?ÍÌÌÌÌL?@333333 ÀH@fffff&@@À333333J@à?33333“U@ffffff3Àffffff@333333@33333óQ@33333³2ÀÍÌÌÌÌÌ0ÀÍÌÌÌÌÌ%@ÍÌÌÌÌÌ4@@š™™™™YP@š™™™™YF@333333+@33333³8@ffffff(@ffffff,@š™™™™™5@ffffff;@š™™™™™<@š™™™™G@33333“U@E@@#@à?ÍÌÌÌÌŒE@ Àš™™™™™?@33333óT@fffffæ?@š™™™™YX@fffffæG@1@,@š™™™™™@ffffff>@ÍÌÌÌÌL0@š™™™™™6@ÍÌÌÌÌÌì?333333*@ffffffV@š™™™™)a@33333T@333333#@š™™™™F@ÍÌÌÌÌÌ5Àš™™™™ÙC@ÍÌÌÌÌ W@fffffQ@ÍÌÌÌÌLI@š™™™™™!ÀffffffJ@€3Àš™™™™™0À33333“Z@333333ÀffffffJ@š™™™™ÙI@š™™™™Ù@@33333sZ@333333À,ÀÍÌÌÌÌÌÀfffff¦A@š™™™™™C@š™™™™™'@ÍÌÌÌÌLG@ffffffI@ÍÌÌÌÌÌ3Àš™™™™™ù?š™™™™4@333333'Àš™™™™™*ÀÍÌÌÌÌÌH@š™™™™:@ÍÌÌÌÌÌ5@€<@ffffff,@€1Àš™™™™™I@333333K@š™™™™yR@ R@@Z@"@ÍÌÌÌÌìP@š™™™™™L@fffff&[@fffffæ6@33333sI@ÍÌÌÌÌL2@ffffff/@ÍÌÌÌÌ D@š™™™™W@333333P@ÍÌÌÌÌÌN@33333³9@333333-À€>@€K@š™™™™™=@š™™™™™À33333óF@€@Àffffff@ÍÌÌÌÌLD@ÍÌÌÌÌÌ4@333333M@š™™™™™+@333333=@ÍÌÌÌÌÌ%ÀÍÌÌÌÌÌ+ÀfffffæB@333333?@N@"ÀB@fffffæ?Àš™™™™™5@333333)@@333333ã¿ffffffC@ÍÌÌÌÌÌW@š™™™™™ @fffffæ0Àffffff@7@ffffff(ÀffffffÀ2@€H@ÍÌÌÌÌÌB@fffff&R@ffffff9@R@€A@ÍÌÌÌÌL;Àš™™™™™$Àffffff$@ffffffU@ÍÌÌÌÌÌ=@àP@š™™™™™*À@fffff¦G@333333B@ÀF@š™™™™™@ffffffD@fffffæI@@H@fffff¦BÀš™™™™ÙX@fffff&G@@R@š™™™™™W@ÍÌÌÌÌÌ9Àš™™™™YP@fffffæA@š™™™™™L@fffffÆW@š™™™™D@fffff&H@ÍÌÌÌÌL6@ÍÌÌÌÌ I@š™™™™™0Àš™™™™™ù?š™™™™YR@ÍÌÌÌÌÌ!Àffffff @fffffæH@333333@333333@š™™™™YL@ÍÌÌÌÌŒR@š™™™™™Àš™™™™™(@ffffffÀÍÌÌÌÌÌ8@33333³L@š™™™™ÙS@š™™™™™9Àš™™™™™@fffffæ@@@O@.@33333sI@3333339@@P@ÍÌÌÌÌŒT@333333$@š™™™™™B@€H@3333331@33333U@#Àš™™™™)`@ÍÌÌÌÌ,dÀš™™™™L@ffffffN@š™™™™™Ù¿š™™™™™Ù¿ÍÌÌÌÌÌô?š™™™™™¹?š™™™™™É¿333333ã?š™™™™™É¿š™™™™™¹?à¿ÍÌÌÌÌÌì¿333333Ó¿š™™™™™¹¿š™™™™™É?࿚™™™™™Ù¿š™™™™™¹¿à¿š™™™™™¹?š™™™™™Ù¿š™™™™™É¿333333Ó¿333333Ó¿333333㿚™™™™™¹?š™™™™™¹¿333333Ó¿š™™™™™Ù¿š™™™™™Ù¿š™™™™™¹¿š™™™™™¹?š™™™™™é¿333333Ó¿ffffffæ¿à¿š™™™™™Ù¿š™™™™™¹?333333Ó¿ffffff濚™™™™™É¿š™™™™™¹¿333333Ó¿š™™™™™¹¿ð¿333333ã¿à¿š™™™™™¹¿333333Ó¿333333ó¿333333Ó¿333333ã?š™™™™™Ù¿à¿š™™™™™É¿š™™™™™¹¿š™™™™™Ù¿š™™™™™¹¿š™™™™™Ù¿š™™™™™Ù?ffffffæ¿333333Ó?š™™™™™é¿š™™™™™É¿š™™™™™É¿ÍÌÌÌÌÌ@š™™™™™é¿ÍÌÌÌÌÌì¿333333ã¿ð¿333333Ó¿333333ã¿à¿333333ӿ࿚™™™™™ù¿š™™™™™ù¿ffffffö¿š™™™™™É¿333333ã?࿚™™™™™¹?š™™™™™É?š™™™™™É¿ð¿š™™™™™¹¿š™™™™™ñ¿š™™™™™Ù¿š™™™™™¹?š™™™™™é¿ÍÌÌÌÌÌ쿚™™™™™¹¿À333333ã¿333333㿚™™™™™¹?ÍÌÌÌÌÌì?࿚™™™™™É?à¿333333ã¿ð¿333333Ó¿333333Ó?š™™™™™É¿ffffff濚™™™™™¹?š™™™™™Ù¿š™™™™™¹?ffffff濚™™™™™É¿š™™™™™¹¿à¿333333㿚™™™™™É¿š™™™™™Ù¿š™™™™™¹?š™™™™™¹?333333Ó¿š™™™™™É?š™™™™™¹?š™™™™™Ù¿š™™™™™Ù¿š™™™™™É¿333333㿚™™™™™¹?š™™™™™¹?š™™™™™Ù?š™™™™™¹¿ð¿ÍÌÌÌÌÌ쿚™™™™™¹¿à¿š™™™™™Ù¿š™™™™™É¿š™™™™™É?š™™™™™É¿à¿š™™™™™É¿333333㿚™™™™™¹¿333333Ó¿š™™™™™¹¿š™™™™™¹?࿚™™™™™¹?࿚™™™™™Ù¿333333Ó¿à¿ffffffæ¿333333ã¿ffffff濚™™™™™Ù¿š™™™™™¹?࿚™™™™™Àš™™™™™Ù¿š™™™™™¹?š™™™™™¹?333333ó¿ÍÌÌÌÌÌ쿚™™™™™¹?š™™™™™Ù¿333333Ó¿ffffff濚™™™™™¹¿333333Ó¿š™™™™™Ù¿š™™™™™Ù¿š™™™™™Ù¿š™™™™™¹?ð?333333Ó¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌô¿ffffff濚™™™™™¹¿ð?333333㿚™™™™™é¿š™™™™™Ù¿š™™™™™¹?333333ó¿š™™™™™É¿ÍÌÌÌÌÌ@𿚙™™™™É¿š™™™™™¹?333333Ó¿š™™™™™ñ¿333333㿚™™™™™¹¿š™™™™™¹?š™™™™™¹?š™™™™™É¿š™™™™™¹¿š™™™™™É¿333333ó¿š™™™™™É¿ÍÌÌÌÌÌ쿚™™™™™¹?š™™™™™Àš™™™™™¹?ÍÌÌÌÌÌì¿ÍÌÌÌÌÌì¿à¿333333ӿ࿚™™™™™é¿à¿š™™™™™ñ¿š™™™™™É?333333㿚™™™™™É¿š™™™™™É?333333㿚™™™™™¹?š™™™™™¹?ÍÌÌÌÌÌü¿š™™™™™É?š™™™™™Ù¿š™™™™™É¿à¿š™™™™™Ù?333333Ó¿ffffffæ¿à¿š™™™™™¹?š™™™™™Ù?š™™™™™¹¿š™™™™™É¿š™™™™™¹?š™™™™™Ù¿333333ã¿333333㿚™™™™™é¿š™™™™™Ù¿333333Ó?š™™™™™Ù¿š™™™™™¹?š™™™™™¹?š™™™™™Ù¿š™™™™™¹¿333333Ó¿š™™™™™É¿š™™™™™¹¿š™™™™™Ù¿š™™™™™¹¿š™™™™™É¿333333ó¿333333Ó¿š™™™™™¹?ÍÌÌÌÌÌô¿š™™™™™ñ¿ÍÌÌÌÌÌô¿š™™™™™é?š™™™™™Ù?š™™™™™¹?ffffffþ¿š™™™™™ñ¿š™™™™™É¿š™™™™™ñ¿š™™™™™É?š™™™™™¹¿333333Àš™™™™™¹?š™™™™™¹¿ÍÌÌÌÌÌì¿333333㿚™™™™™¹?š™™™™™Ù¿à¿ffffff@š™™™™™¹?à¿333333㿚™™™™™É¿š™™™™™¹¿ffffffö¿333333Àà?333333㿚™™™™™é¿š™™™™™Ù¿333333ó¿ÍÌÌÌÌÌô¿š™™™™™Ù¿š™™™™™É¿ffffffæ?࿚™™™™™é?š™™™™™¹¿333333ó¿š™™™™™¹?š™™™™™É¿š™™™™™Ù¿š™™™™™¹?ffffffæ¿à?333333Ó?࿚™™™™™¹?333333㿚™™™™™É?š™™™™™ñ¿à¿š™™™™™ù¿ffffffæ¿ÍÌÌÌÌÌ@š™™™™™¹?š™™™™™Ù?à¿333333ó¿ø¿333333û¿š™™™™™É?š™™™™™É¿š™™™™™¹?š™™™™™ñ¿333333û¿à¿š™™™™™é¿š™™™™™Ù?ffffff濚™™™™™Ù¿à¿à¿ffffffæ?š™™™™™¹¿à¿ÍÌÌÌÌÌô¿š™™™™™É?à¿333333Ó¿333333Ó¿š™™™™™É¿333333ã¿ffffffæ¿ffffffæ¿333333ÀÍÌÌÌÌÌô¿ffffffæ?à¿ÍÌÌÌÌÌì¿ffffffö?333333ã?š™™™™™Ù?à?333333Ó¿š™™™™™É?š™™™™™¹?ÍÌÌÌÌÌ@š™™™™™Ù?š™™™™™¹?ÍÌÌÌÌÌì¿à¿à¿ÀÍÌÌÌÌÌô¿333333ó¿š™™™™™ñ¿š™™™™™¹?š™™™™™Ù¿š™™™™™É¿ÍÌÌÌÌÌì¿ffffff濚™™™™™É¿š™™™™™¹?ÍÌÌÌÌÌ쿚™™™™™Ù¿333333Ó¿333333Ó¿333333 @à?š™™™™™¹¿š™™™™™Ù?š™™™™™É¿š™™™™™Ù¿ffffffæ¿ffffffæ¿333333ã?333333Ó¿š™™™™™É?š™™™™™¹¿š™™™™™Ù?à¿333333ã¿à¿ffffffö¿š™™™™™¹¿š™™™™™É¿à¿333333Ó?š™™™™™Ù¿š™™™™™¹?ffffff濚™™™™™É?š™™™™™¹?š™™™™™Ù¿š™™™™™é¿š™™™™™ñ¿à¿à¿š™™™™™Ù¿ffffffæ¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌì¿ffffffþ¿333333㿚™™™™™Ù¿à¿š™™™™™Ù¿à¿333333û¿ÍÌÌÌÌÌÀš™™™™™Ù¿333333ã¿ÍÌÌÌÌÌü¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌô¿š™™™™™ñ¿Àš™™™™™é¿ÍÌÌÌÌÌì¿à¿à¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌì¿333333ó¿ffffffÀà¿ffffffæ¿ÍÌÌÌÌÌ쿚™™™™™Ù¿ffffffæ¿ffffffö¿ð¿333333ó¿333333ó¿š™™™™™Ù¿333333ã¿ÍÌÌÌÌÌì¿333333ó¿š™™™™™ñ¿š™™™™™Ù¿ð¿#Àffffff濚™™™™™ù¿š™™™™™ñ¿š™™™™™ñ¿š™™™™™Ù¿ffffff濚™™™™™Ù¿š™™™™™Ù¿š™™™™™Ù¿333333û¿š™™™™™Ù¿333333ó¿š™™™™™Ù¿333333ã¿333333ã¿ÍÌÌÌÌÌô¿š™™™™™ù¿333333û¿š™™™™™ñ¿ø¿ð¿š™™™™™é¿à¿333333㿚™™™™™é¿ÍÌÌÌÌÌô¿ffffffæ¿333333 À333333㿚™™™™™ñ¿ÍÌÌÌÌÌ쿚™™™™™Ù¿ð¿ffffff濚™™™™™é¿à¿333333ó¿š™™™™™é¿ffffff,Àð¿ffffff濚™™™™™ñ¿š™™™™™Ù¿š™™™™™é¿ffffffþ¿š™™™™™Ù¿333333ã¿ÍÌÌÌÌÌ Àš™™™™™é¿333333ã¿ Àà¿à¿+@&@333333&@ffffff.@333333"@€3@š™™™™™@!@š™™™™™"@ÍÌÌÌÌÌ%@ffffff)@333333@ffffff@3@333333+@333333#@'@ÍÌÌÌÌÌ @š™™™™™ @#@ÍÌÌÌÌÌ&@ffffff)@š™™™™™#@ffffff#@š™™™™™/@333333"@€5@ffffff*@@333333@š™™™™™@š™™™™™"@š™™™™™ @*@š™™™™™#@ffffff'@333333)@š™™™™™)@š™™™™™"@ÍÌÌÌÌÌ@š™™™™™)@š™™™™™@333333'@š™™™™™3@333333 @ÍÌÌÌÌÌ @@&@ffffff@š™™™™™@ffffff$@333333"@š™™™™1@€4@ÍÌÌÌÌÌ)@ffffff#@ffffff&@š™™™™™&@fffffæ5@š™™™™™%@ @ffffff$@ÍÌÌÌÌÌ@š™™™™™-@ÍÌÌÌÌÌ0@333333@333333!@ÍÌÌÌÌÌ,@ÍÌÌÌÌÌ/@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ @ÍÌÌÌÌÌ$@#@333333 @ffffff.@ÍÌÌÌÌÌ/@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ*@š™™™™™*@š™™™™™(@333333,@%@'@š™™™™™'@ÍÌÌÌÌÌ@ÍÌÌÌÌL6@ffffff(@333333'@š™™™™™@ffffff @@š™™™™™@š™™™™™@ÍÌÌÌÌÌ@333333+@.@š™™™™™$@ffffff @ffffff,@š™™™™™#@33333³0@š™™™™™(@š™™™™;@ffffff$@3@€8@333333"@fffffæ3@ffffff*@š™™™™™$@ffffff'@š™™™™™"@1@,@š™™™™™@"@ @š™™™™™@ffffff(@333333)@%@@ffffff)@333333@ÍÌÌÌÌÌ$@ffffff@ÍÌÌÌÌÌ/@š™™™™™@!@š™™™™™#@ffffff+@#@333333/@333333!@š™™™™™0@3333332@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ'@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ*@3333331@ @+@ÍÌÌÌÌÌ'@š™™™™™#@3333331@&@ffffff@"@333333$@333333.@@333333(@333333$@ffffff*@ffffff+@ffffff)@333333'@š™™™™™@/@ÍÌÌÌÌÌ"@š™™™™™,@ffffff&@š™™™™™'@(@333333&@ÍÌÌÌÌÌ%@ffffff)@333333@ffffff"@ffffff)@ÍÌÌÌÌÌ*@6@ÍÌÌÌÌÌ@333333@š™™™™™&@333333%@*@333333@ÍÌÌÌÌÌ/@š™™™™™$@333333#@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333@ffffff @ÍÌÌÌÌL=@š™™™™™ @ÍÌÌÌÌÌ@333333&@3333330@š™™™™™(@š™™™™™ @š™™™™™ @.@33333³3@š™™™™™'@€2@ÍÌÌÌÌÌ @333333@ffffff@š™™™™™$@ÍÌÌÌÌÌ@ffffff(@(@@fffffæ3@š™™™™™.@&@š™™™™™@ffffff@š™™™™™,@ÍÌÌÌÌÌ @ffffff@1@333333@š™™™™™.@š™™™™™ @333333%@/@ffffff1@333333#@ffffff @+@ÍÌÌÌÌÌ @š™™™™™$@fffffæ2@š™™™™™ @ÍÌÌÌÌÌ#@(@1@fffffæ1@3333331@333333$@$@ÍÌÌÌÌÌ+@š™™™™™*@-@ffffff@+@ @ffffff/@ÍÌÌÌÌÌ(@š™™™™™$@š™™™™™(@333333@š™™™™™@ffffff&@ffffff2@ffffffþ?ffffff$@š™™™™™@$@3333330@333333û?ffffff!@333333*@333333@333333@333333)@ffffff'@ÍÌÌÌÌÌ@333333"@333333!@š™™™™™@ffffff"@ffffff@ffffff&@ÍÌÌÌÌÌ(@ÍÌÌÌÌÌ$@333333@ÍÌÌÌÌÌ*@ffffff@š™™™™™$@ÍÌÌÌÌÌ#@%@0@!@ÍÌÌÌÌÌ0@ffffff@š™™™™™@#@š™™™™™(@ÍÌÌÌÌÌ+@ÍÌÌÌÌÌ@š™™™™™@333333@š™™™™™ @š™™™™2@š™™™™3@ffffff&@333333*@š™™™™™/@333333'@ffffff@š™™™™™*@333333-@š™™™™™*@333333@333333.@š™™™™™ @ÍÌÌÌÌÌ/@ffffff5@@ffffff%@.@š™™™™™@ÍÌÌÌÌÌ*@š™™™™™0@š™™™™3@@ffffff(@š™™™™™*@š™™™™™ @ÍÌÌÌÌÌ!@ffffff-@%@333333@ÍÌÌÌÌÌ*@š™™™™™,@6@š™™™™™@333333%@š™™™™™)@3333334@ÍÌÌÌÌÌ&@ÍÌÌÌÌÌ#@ÍÌÌÌÌÌ#@ffffff6@š™™™™1@3333336@ffffff.@333333'@ffffff@(@333333(@ffffff!@333333/@ffffff@ffffff%@š™™™™™$@š™™™™™+@š™™™™™#@ÍÌÌÌÌL0@š™™™™™@ÍÌÌÌÌÌ@333333 @&@#@333333+@ffffff%@ffffff&@š™™™™™@ÍÌÌÌÌL0@ÍÌÌÌÌÌ3@š™™™™™@333333-@ÍÌÌÌÌÌ @ÍÌÌÌÌÌ@333333%@@,@333333@33333³3@ÍÌÌÌÌÌ/@ffffff&@š™™™™™@@ÍÌÌÌÌÌ&@š™™™™™"@€3@š™™™™™!@š™™™™™@š™™™™™@ÍÌÌÌÌÌ!@ÍÌÌÌÌL1@ÍÌÌÌÌÌ@š™™™™™"@3333331@ÍÌÌÌÌÌ'@3333330@ÍÌÌÌÌÌ*@fffffæ0@333333$@ffffff%@š™™™™™+@ÍÌÌÌÌÌ@ÍÌÌÌÌL0@*@€3@333333#@š™™™™™ @ffffff"@@333333!@333333@ÍÌÌÌÌÌ&@33333³6@(@ÍÌÌÌÌÌ@š™™™™™/@ÍÌÌÌÌÌ!@š™™™™™*@ÍÌÌÌÌÌ5@333333 @š™™™™™3@ÍÌÌÌÌÌ,@š™™™™™.@š™™™™;@ÍÌÌÌÌÌ(@.@ffffff.@š™™™™™,@1@š™™™™™@ffffff,@ÍÌÌÌÌÌ-@!@ÍÌÌÌÌÌ.@ÍÌÌÌÌÌ&@ÍÌÌÌÌÌ@š™™™™™&@š™™™™™)@š™™™™™%@ffffff"@ÍÌÌÌÌÌ&@€=@š™™™™™-@ÍÌÌÌÌÌ7@ffffff@333333%@&@ÍÌÌÌÌÌ)@&@33333³0@ÍÌÌÌÌÌ#@€7@š™™™™™0@ffffff@ffffff0@š™™™™™"@š™™™™™ @333333"@ffffff@3333335@6@š™™™™™.@)@À333333À333333 Àš™™™™™ ÀÍÌÌÌÌÌì¿ÍÌÌÌÌÌ&ÀÀš™™™™™ÀffffffÀÀÍÌÌÌÌÌÀ333333ÀÍÌÌÌÌÌÀffffffö¿š™™™™™Ù¿š™™™™™ù¿ÍÌÌÌÌÌÀÍÌÌÌÌÌü¿ø¿ffffffö¿ffffffÀ333333ÀffffffÀš™™™™™ À333333À333333ÀffffffÀffffffÀ333333ÀÀ333333ÀffffffÀš™™™™™À333333À333333Àš™™™™™é¿ÍÌÌÌÌÌÀÍÌÌÌÌÌÀ333333û¿À333333 Àffffffþ¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌÀ ÀÀÍÌÌÌÌÌÀ333333ó¿333333ÀÍÌÌÌÌÌÀ333333ÀffffffÀffffffÀffffffþ¿ÍÌÌÌÌÌÀ333333ÀÍÌÌÌÌÌü¿š™™™™™ÀÍÌÌÌÌÌ Àffffffö¿š™™™™™À333333ã¿ÍÌÌÌÌÌô¿š™™™™™Àffffffæ¿333333û¿À333333ó¿ffffffö¿ÍÌÌÌÌÌÀø¿333333ÀÍÌÌÌÌÌ ÀÍÌÌÌÌÌÀÀš™™™™™ÀÀÀÍÌÌÌÌÌü¿ÍÌÌÌÌÌü¿333333 À333333À ÀÀ333333ÀÍÌÌÌÌÌÀ333333ÀÍÌÌÌÌÌÀš™™™™™ Àš™™™™™ Àffffffþ¿ffffffÀ333333 ÀÍÌÌÌÌÌÀÍÌÌÌÌÌ&À333333û¿ffffff"ÀÍÌÌÌÌÌì¿ÀffffffÀffffffÀš™™™™™Àš™™™™™ À333333ÀÍÌÌÌÌÌ Àš™™™™™Àš™™™™™#ÀÀÍÌÌÌÌÌÀš™™™™™ Àš™™™™™ù¿333333"Àš™™™™™!Àš™™™™™ÀÀø¿š™™™™™À333333 ÀffffffÀ333333À333333Àš™™™™™À333333ã¿Àffffff ÀÀffffffÀš™™™™™Àš™™™™™ÀÀš™™™™™ñ¿ffffffÀ333333Àø¿ffffffÀffffffÀš™™™™™ ÀffffffÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀffffffÀÀà¿ffffff濚™™™™™ÀÍÌÌÌÌÌÀš™™™™™ÀffffffÀÀš™™™™™ Àø¿š™™™™™Àð¿ÍÌÌÌÌÌÀš™™™™™À À333333À ÀÍÌÌÌÌÌÀ%À#À333333 À333333À333333Àš™™™™™ñ¿333333û¿ÍÌÌÌÌÌÀÀÀ333333û¿ffffffÀš™™™™™ù¿ffffffÀ333333ó¿ffffffþ¿333333À333333ÀÀffffff ÀÍÌÌÌÌÌÀÍÌÌÌÌÌ ÀÀffffffþ¿Àš™™™™™ÀÍÌÌÌÌÌÀ333333ã¿ÀÍÌÌÌÌÌÀ333333ÀÍÌÌÌÌÌô¿ffffffÀš™™™™™ñ¿333333!À333333À333333À À333333$ÀÍÌÌÌÌÌÀš™™™™™ù¿ÀÍÌÌÌÌÌ ÀÍÌÌÌÌÌÀø¿333333Àffffffæ¿ffffffÀÍÌÌÌÌÌ ÀÍÌÌÌÌÌÀÀffffffö¿ÍÌÌÌÌÌ Àš™™™™™ñ¿š™™™™™Àš™™™™™Ù¿ÍÌÌÌÌÌô¿333333ó¿ÍÌÌÌÌÌô¿333333À333333ÀÍÌÌÌÌÌü¿ÀÍÌÌÌÌÌÀš™™™™™ÀÍÌÌÌÌÌ ÀffffffÀš™™™™™À333333#À333333ÀÀ333333 Àffffff À333333 ÀffffffÀffffffÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀffffffÀffffffþ¿333333Àffffff À333333À333333û¿333333À333333ÀÍÌÌÌÌÌÀš™™™™™ À333333ÀÍÌÌÌÌÌÀš™™™™™ Àffffffþ¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌÀš™™™™™ ÀÍÌÌÌÌÌÀÍÌÌÌÌÌ Àffffffæ¿ffffffÀÍÌÌÌÌÌô¿š™™™™™ ÀffffffÀÍÌÌÌÌÌ!À333333û¿À333333À333333 Àffffffö¿ÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀffffffþ¿Àffffffþ¿ÀÍÌÌÌÌÌô¿333333ÀÍÌÌÌÌÌÀÀffffffÀ333333"ÀÍÌÌÌÌÌÀÀÀÀÍÌÌÌÌÌô¿ÍÌÌÌÌÌ%Àffffff)À333333À ÀÍÌÌÌÌÌÀš™™™™™"ÀffffffÀffffff Àffffff$À333333Àš™™™™™ÀÍÌÌÌÌÌÀffffffÀffffff Àš™™™™™ù¿ffffffÀÍÌÌÌÌÌÀÍÌÌÌÌÌü¿333333ÀÍÌÌÌÌÌô¿ À333333À333333ÀÀÀš™™™™™ñ¿Àš™™™™™ Àð¿Àš™™™™™À333333Àš™™™™™Àš™™™™™ Àš™™™™™é¿Àš™™™™™ÀffffffÀ333333Àš™™™™™ñ¿ÍÌÌÌÌÌÀffffffþ¿Àš™™™™™"ÀÍÌÌÌÌÌÀš™™™™™ÀffffffÀš™™™™™À ÀffffffÀffffffÀš™™™™™!À333333Àš™™™™™ Àš™™™™™ ÀÀš™™™™™ ÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀ333333À%À333333ÀÀÍÌÌÌÌÌÀffffff À333333 À333333ÀffffffÀÀ333333ó¿333333Àø¿ÀÍÌÌÌÌÌÀÀÀ333333À333333!À333333 Àš™™™™™ù¿ffffff À333333%À333333ÀÍÌÌÌÌÌÀffffffÀÍÌÌÌÌÌÀš™™™™™-Àð¿ÍÌÌÌÌÌ쿚™™™™™À333333ÀÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀ333333 Àš™™™™™Àš™™™™™é¿ÍÌÌÌÌÌ ÀÍÌÌÌÌÌÀÍÌÌÌÌÌ$À333333À333333Àš™™™™™ÀffffffÀÍÌÌÌÌÌÀÍÌÌÌÌÌÀÍÌÌÌÌÌ Àš™™™™™À333333ÀÀ333333û¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌÀ333333Àš™™™™™*ÀÀÍÌÌÌÌÌÀffffffþ¿ø¿š™™™™™Àffffff Àø¿ffffffÀffffffþ¿ÍÌÌÌÌÌÀffffffÀffffffÀffffffÀš™™™™™ù¿š™™™™™ ÀffffffÀÍÌÌÌÌÌ>À À333333À333333@à?š™™™™™Ù?š™™™™™Ù?à?ÍÌÌÌÌÌì?š™™™™™Ù?š™™™™™Ù?333333ã?š™™™™™É¿š™™™™™é?š™™™™™Ù?š™™™™™¹?š™™™™™é?ð?š™™™™™é?333333ã?š™™™™™é?à?š™™™™™É?ffffffæ?š™™™™™Ù?333333Ó?à?à?š™™™™™Ù?ð?à?333333ã?à?à?ð?š™™™™™Ù?à?š™™™™™Ù?333333ã?š™™™™™Ù?333333ã?333333ã?ÍÌÌÌÌÌì?ffffffæ?à?ð?ÍÌÌÌÌÌì?š™™™™™Ù?à?š™™™™™Ù?ð?333333Ó?ffffffæ?š™™™™™é?333333Ó?š™™™™™Ù?š™™™™™Ù?ÍÌÌÌÌÌì?333333ã?333333ã?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?333333ã?š™™™™™¹¿à?š™™™™™é?š™™™™™Ù?ÍÌÌÌÌÌì?š™™™™™é?ÍÌÌÌÌÌì?333333ã?š™™™™™¹¿333333ã?333333ã?333333ã¿ð?ÍÌÌÌÌÌì?ffffffæ?333333Ó?ffffffæ?ffffffæ?333333ã?à?ffffffæ?à?ÍÌÌÌÌÌô?ð?à?ð?š™™™™™É¿ÍÌÌÌÌÌì?333333Ó?š™™™™™¹?š™™™™™¹?333333ã?ð?à?333333Ó?ð?à?ð?š™™™™™É¿ffffffæ?š™™™™™É?š™™™™™Ù?333333ã?ffffffæ?ffffffæ?ÍÌÌÌÌÌô?š™™™™™¹¿à?š™™™™™ñ?š™™™™™é?ffffffæ?š™™™™™¹¿333333Ó?š™™™™™Ù¿ÍÌÌÌÌÌì?à?š™™™™™Ù?333333Ó?333333ã?š™™™™™Ù?à?š™™™™™Ù?ffffffæ?333333ã?š™™™™™Ù?ÍÌÌÌÌÌì?à?š™™™™™Ù?à?333333ã?š™™™™™é?333333ã?ffffffæ?š™™™™™Ù?ÍÌÌÌÌÌì?ffffffæ?š™™™™™¹¿à?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?333333ã?ffffffö?à?š™™™™™Ù?333333ó?à?š™™™™™é?à?ffffffæ?š™™™™™é?à?ð?ffffffæ?š™™™™™¹?333333ã?à?333333Ó?ffffffæ?à?š™™™™™é?š™™™™™Ù?333333Ó?š™™™™™É¿ffffffæ?ÍÌÌÌÌÌô?à?ffffffæ?š™™™™™Ù?333333ó?ffffffæ?333333ã?à?à?333333ã?333333Ó?333333Ó¿333333Ó?š™™™™™Ù?š™™™™™É¿ffffffæ?333333Ó?ÍÌÌÌÌÌì?š™™™™™Ù?333333Ó?š™™™™™é?ffffffæ?333333ã?š™™™™™é?à?š™™™™™é?š™™™™™É?š™™™™™¹¿ffffffæ?š™™™™™é?333333ã?ffffffæ?à?333333Ó?à?ffffff@333333ã?š™™™™™Ù?à?333333Ó?333333Ó?ÍÌÌÌÌÌì?à?š™™™™™Ù?š™™™™™¹?à?ffffffæ?ffffffæ?ffffffæ?à?ÍÌÌÌÌÌì?š™™™™™é?333333Ó?ÍÌÌÌÌÌì?š™™™™™é?š™™™™™Ù?à?à?š™™™™™Ù?ÍÌÌÌÌÌì?š™™™™™É?ÍÌÌÌÌÌì?ffffffæ?ÍÌÌÌÌÌì?š™™™™™Ù?333333Ó?à?ð?à?š™™™™™Ù?ÍÌÌÌÌÌì?ffffffæ?š™™™™™Ù?ffffffæ?333333ã?ð?333333Ó?333333Ó?333333ã?š™™™™™Ù¿š™™™™™é?ÍÌÌÌÌÌì?à?333333ã?ffffffæ?ffffffæ?333333ã?ÍÌÌÌÌÌì?333333ã?333333Ó?333333ã?ffffffæ?333333Ó?333333ã?š™™™™™Ù?š™™™™™ñ?ð?333333ã?š™™™™™Ù?š™™™™™Ù?à?š™™™™™é?ÍÌÌÌÌÌì?š™™™™™Ù?š™™™™™Ù?à?333333Ó¿à?à?333333Ó?333333ã?ð?à?à?š™™™™™¹¿333333ã?š™™™™™Ù?š™™™™™Ù?à?š™™™™™Ù¿š™™™™™Ù?à?à?š™™™™™Ù?š™™™™™Ù?š™™™™™É?333333ã?š™™™™™ñ?š™™™™™é¿333333ã?333333Ó?333333Ó?ÍÌÌÌÌÌì?ffffffæ¿333333ã?š™™™™™¹¿333333ã?à?ÍÌÌÌÌÌô?333333Ó?333333ã?š™™™™™Ù?š™™™™™Ù?࿚™™™™™É¿ÍÌÌÌÌÌì?š™™™™™é?333333ã?ffffffæ?ÍÌÌÌÌÌì?š™™™™™Ù?š™™™™™é?š™™™™™ñ?ffffffæ?ffffffö?ffffffæ?š™™™™™Ù?333333ã?ffffffæ?333333ã?à?333333ã?š™™™™™é?š™™™™™É?333333ã?š™™™™™ñ?š™™™™™é?š™™™™™ñ?333333ã?à?š™™™™™é?ÍÌÌÌÌÌì?à?ffffffæ?à?à?ffffffæ?à?ffffffæ?333333ã?š™™™™™Ù?à?333333û?ffffffæ?333333ã?š™™™™™É?ffffffæ?ffffffæ?333333ó?ffffffæ?à?ffffffæ?š™™™™™Ù?š™™™™™Ù?ð?š™™™™™é?333333ã?ffffffæ?à?š™™™™™Ù?š™™™™™É?333333ã?333333Ó?333333Ó?à?333333Ó?333333ã?ffffffæ?š™™™™™Ù?333333Ó?333333ã?š™™™™™é?ÍÌÌÌÌÌì?š™™™™™Ù?333333ã?š™™™™™é?ffffffæ?ffffffæ?š™™™™™Ù?ffffffæ?333333Ó¿333333ã?š™™™™™Ù?333333Ó?333333ã?š™™™™™Ù?333333ã?š™™™™™é¿à?ffffffæ?š™™™™™¹¿333333ã?ffffffæ?ffffffæ?š™™™™™Ù¿333333ã?š™™™™™Ù?ffffff濚™™™™™é?š™™™™™é?ø?à?ÍÌÌÌÌÌì¿à?à?š™™™™™¹¿š™™™™™ñ?ffffffæ?333333ã?ð?š™™™™™¹¿ffffffæ?š™™™™™Ù?š™™™™™É?ÍÌÌÌÌÌì?333333ã?ð?š™™™™™é?š™™™™™Ù?š™™™™™Ù?à?š™™™™™Ù?ÍÌÌÌÌÌì?š™™™™™Ù?à?333333ã?333333ã?š™™™™™Ù?š™™™™™é?ffffffæ?ð?333333ã?š™™™™™é?ÍÌÌÌÌÌì?ffffffæ?333333ã?š™™™™™é?ffffffæ?à¿à?š™™™™™é?š™™™™™Ù?ffffffþ?š™™™™™Ù?333333ã?333333@ÍÌÌÌÌÌ@333333 @@333333@ffffff @ÍÌÌÌÌÌô?š™™™™™é?ffffff@ð?333333Ó¿ÍÌÌÌÌÌì?à?333333 @ffffffþ?ÍÌÌÌÌÌ@ffffff@à?š™™™™™ñ?@ÍÌÌÌÌÌ@ffffffö?@333333ó?@@ÍÌÌÌÌÌ@ @333333û¿333333ó¿ÍÌÌÌÌÌ@ÍÌÌÌÌÌü?@ffffffö?ð?š™™™™™ù?ffffffþ?333333@š™™™™™ @ø?š™™™™™@ÍÌÌÌÌÌô?ÍÌÌÌÌÌ@ffffffö¿ÍÌÌÌÌÌì?ø?333333û?333333ã?ø?ÍÌÌÌÌÌì?ffffffö?š™™™™™ñ?333333ó?š™™™™™@š™™™™™É?ffffff@@333333ã?ffffffæ¿ffffffþ?ffffffö?ÍÌÌÌÌÌ@ffffff@š™™™™™ @333333 @š™™™™™ù?ÍÌÌÌÌÌì¿333333@@333333û?ffffff@333333@ÍÌÌÌÌÌ@333333û?ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ @ÍÌÌÌÌÌì¿ffffff@ÍÌÌÌÌÌì?333333ã?š™™™™™@š™™™™™ñ¿š™™™™™Ù?ffffff@ffffff@Àš™™™™™É?ÀÍÌÌÌÌÌô¿š™™™™™Ù?333333ó?š™™™™™@ffffffæ?ffffff@333333@ÍÌÌÌÌÌ@@š™™™™™Ù?333333ã¿ÍÌÌÌÌÌì?ð?ø?@ÍÌÌÌÌÌô?š™™™™™Ù?ÍÌÌÌÌÌü?333333 @333333ó?š™™™™™É?333333û?333333@ÍÌÌÌÌÌü?š™™™™™@333333û?333333@333333@333333û?333333ó?ø?333333@ð?ffffffþ?333333@ffffffö?ffffff@333333@š™™™™™@ø?333333ã?ÍÌÌÌÌÌ@š™™™™™Ù?ÍÌÌÌÌÌü?š™™™™™@ffffff @š™™™™™@@ffffffö?ÍÌÌÌÌÌì?ÍÌÌÌÌÌü?ÍÌÌÌÌÌ@ffffffæ?ffffff@333333@333333@@333333@ffffffþ?ffffffþ?ÍÌÌÌÌÌô?š™™™™™@ffffff@333333@ÍÌÌÌÌÌ@@š™™™™™ @ÍÌÌÌÌÌô?333333ã?ffffffþ?š™™™™™ù? @š™™™™™É?ffffff@ÍÌÌÌÌÌ@š™™™™™@š™™™™™ñ?ÍÌÌÌÌÌ@ffffff@333333ó?š™™™™™é?333333ã?š™™™™™ñ?@ÍÌÌÌÌÌô?ffffff@ffffffæ?333333û?ÍÌÌÌÌÌ@333333@࿚™™™™™ù?ffffff@š™™™™™Ù?333333Ó¿333333Ó?à?333333ã?à?ffffffþ¿à?ffffff @333333ó?333333ã¿ÍÌÌÌÌÌì¿@@ÍÌÌÌÌÌ@ffffff@ø¿š™™™™™é?333333Ó?ffffffö?333333Ó¿ffffff@š™™™™™@333333@@ffffff@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ @ÍÌÌÌÌÌ@333333 @š™™™™™é¿š™™™™™@à?ffffff@ÍÌÌÌÌÌÀ333333ã?š™™™™™ù?à?ffffffö?ffffff@@ÍÌÌÌÌÌ @ffffffþ?š™™™™™é?@ø?š™™™™™@ffffff@333333ã?à¿ÍÌÌÌÌÌ@333333ã?333333@ÍÌÌÌÌÌÀ@333333ó¿ffffffö?ÍÌÌÌÌÌ@š™™™™™ù?333333㿚™™™™™é?š™™™™™é?š™™™™™@š™™™™™É?š™™™™™ñ?š™™™™™@à?@333333@@š™™™™™Ù?333333ã?@333333 @š™™™™™Àš™™™™™Ù?à?ÍÌÌÌÌÌì?š™™™™™Ù?ffffff@ffffff@à?ffffffþ?š™™™™™é?š™™™™™é?š™™™™™@@333333@š™™™™™@333333@ffffff @à?333333Ó?333333Ó?ffffffþ?š™™™™™¹?@333333 @ø?333333@š™™™™™@š™™™™™ù?š™™™™™@333333Ó?ð?š™™™™™@ð?ÍÌÌÌÌÌô?à?333333@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌü¿ø?333333Àffffff@ffffff@ffffff@333333ã?ø?š™™™™™@ø?š™™™™™ À@ffffffæ?333333û?333333ã?@333333@à?ffffffþ?š™™™™™ñ?@š™™™™™é?š™™™™™ @333333@333333ã?ÍÌÌÌÌÌô?š™™™™™¹?š™™™™™¹?ffffffÀš™™™™™Àš™™™™™@ffffffö?š™™™™™@š™™™™™É¿333333ó¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ffffff@ @ÍÌÌÌÌÌ@ð?ffffff@š™™™™™@ffffff@333333ã?ÍÌÌÌÌÌü?ø?333333ó?š™™™™™é?@333333@333333ó?ÍÌÌÌÌÌü?š™™™™™ù?333333@š™™™™™@š™™™™™É?š™™™™™ñ¿ÍÌÌÌÌÌì¿ffffffþ?333333ó?333333 @š™™™™™é¿š™™™™™@ÍÌÌÌÌÌô¿š™™™™™ñ¿333333ó?š™™™™™Ù?ÍÌÌÌÌÌ@ffffff@š™™™™™@ffffffæ¿ÍÌÌÌÌÌô¿333333Àš™™™™™ù?333333û?à?333333Ó?333333 @@š™™™™™É?333333ó?ð?ÍÌÌÌÌÌü?@š™™™™™É?ð?333333û?š™™™™™ù?@ÍÌÌÌÌÌô?š™™™™™ù?ÍÌÌÌÌÌü?Àš™™™™™ñ¿333333û?ffffff@ÍÌÌÌÌÌì?ffffff@š™™™™™Ù?ÍÌÌÌÌÌü?@ffffff@ @ø?š™™™™™ @ @š™™™™™ @ @333333@ffffff@333333û?ffffff@333333Ó?@333333Ó?ÍÌÌÌÌÌü?š™™™™™@ÍÌÌÌÌÌü?ÍÌÌÌÌÌü?333333ã?ffffffö?š™™™™™é?ÍÌÌÌÌÌì?333333@ÍÌÌÌÌÌü¿š™™™™™É?š™™™™™ @š™™™™™ù?ÍÌÌÌÌÌ@š™™™™™ @ @ð?š™™™™™Ù?ÍÌÌÌÌÌô?ÍÌÌÌÌÌü?š™™™™™ù?š™™™™™ @à¿ÍÌÌÌÌÌô¿ø?ÍÌÌÌÌÌ@ÍÌÌÌÌÌü?ÍÌÌÌÌÌô?ffffffþ?@333333@š™™™™™é?ffffff@š™™™™™ @ÍÌÌÌÌÌü?@ð?š™™™™™ @Àš™™™™™ @333333û?€9@š™™™™™5@'@ffffffB@33333³5@š™™™™4@š™™™™™"@-@333333.@ÍÌÌÌÌÌ7@ffffff@ø¿ÍÌÌÌÌÌ@fffff¦G@ÍÌÌÌÌL=@3333334@ÍÌÌÌÌL9@fffffæ0@0@ÍÌÌÌÌÌ<@€2@333333%@ffffff3@3333330@fffffæA@š™™™™™@ÍÌÌÌÌLK@3333339@ffffff&@À333333#@š™™™™™,@ffffff&@3333335@ @ÍÌÌÌÌÌ:@333333)@9@ffffff6@ÍÌÌÌÌL3@ÍÌÌÌÌL9@@ffffff5@š™™™™YF@ffffff @š™™™™™¹?!@33333³6@ÍÌÌÌÌL0@š™™™™™É?3333330@ffffff@3333332@fffff&I@š™™™™™3@š™™™™=@ffffff7@ÍÌÌÌÌÌ+@ÍÌÌÌÌÌ@@ÍÌÌÌÌL4@333333û¿ffffff.@ÍÌÌÌÌÌ/@ÍÌÌÌÌÌ@@D@&@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ;@fffffæD@š™™™™™@€7@ÍÌÌÌÌÌ5@š™™™™1@333333!@fffffæC@š™™™™5@fffffæ@ÀÍÌÌÌÌÌ&@2@6@fffffæ?@ÍÌÌÌÌÌ9@š™™™™™2@&@ffffff;@33333sA@@G@€1@fffff&E@33333³A@š™™™™™ñ¿ÍÌÌÌÌÌ@333333 @@ÍÌÌÌÌÌì?š™™™™™=@š™™™™™4@ÍÌÌÌÌL5@ÍÌÌÌÌÌÀ333333;@ÍÌÌÌÌL1@fffff¦L@333333-Àš™™™™ÙP@€<@fffffæ3@š™™™™™S@ffffff2@fffffæ=@fffff&@@š™™™™™4@š™™™™™(@ffffff)@€8@3@333333'@3333331Àffffff@ffffff@š™™™™™5@333333>@š™™™™™ÀÍÌÌÌÌÌÀÍÌÌÌÌL0@ffffff!@3333336@ÍÌÌÌÌÌ@ÍÌÌÌÌL>@ÍÌÌÌÌÌ)@333333&@@ÍÌÌÌÌÌ>@fffffæ5@š™™™™™'@ffffff'@?@ÍÌÌÌÌLC@ffffff&@ÍÌÌÌÌL5@š™™™™™@ÍÌÌÌÌŒ@@+@333333$@333333?@š™™™™™%@)@š™™™™7@3333333@!@š™™™™™(@333333/@;@š™™™™™@fffffæ>@€9@ÍÌÌÌÌÌ7@š™™™™=@š™™™™™.@F@ffffff@€?@ÍÌÌÌÌÌ,@fffffæ9@33333³=@33333³:@fffffæ;@@š™™™™™-@333333#@@ÍÌÌÌÌÌ%@333333À333333*@fffffæA@&@333333$@š™™™™>@š™™™™™4@ffffff7@ÍÌÌÌÌÌ)@š™™™™ÙB@5@ffffff#@333333'@333333Àffffff'@'@ÍÌÌÌÌÌÀfffff†T@š™™™™™!@ÍÌÌÌÌÌô¿š™™™™5@33333³D@ffffff:@š™™™™™*@ÍÌÌÌÌÌ.@ÍÌÌÌÌLA@ÍÌÌÌÌ D@?@ÍÌÌÌÌÌ@@ffffff"@3@ffffffæ?33333³7@ÍÌÌÌÌÌ"Àš™™™™™>@š™™™™™ñ¿€I@@G@33333ó@@ÍÌÌÌÌÌÀÍÌÌÌÌÌ&À333333û¿33333³=@š™™™™™*@ÍÌÌÌÌÌ%@ÍÌÌÌÌŒB@ð?CÀ33333³1@@333333.@ÍÌÌÌÌÌ<@ÍÌÌÌÌÌ:@333333)@ffffff'@ffffff;@33333³6@3333330@ÍÌÌÌÌLD@fffffæ1@ÍÌÌÌÌL>@š™™™™7@333333K@€F@fffffæ;@š™™™™1@€4@ÍÌÌÌÌÌ>@33333³?@4@š™™™™™@ÍÌÌÌÌÌ7@!@C@ffffff@š™™™™™E@fffffæ0@ffffffÀffffff@ÍÌÌÌÌÌÀfffff&@@ÍÌÌÌÌÌ$Àš™™™™™@"Àffffff4@fffffæ4@ÍÌÌÌÌÌÀffffff(@ffffff=@ÍÌÌÌÌÌ,@@33333³=@€3@333333(@ffffff*@ÍÌÌÌÌÌ/@333333ã¿,@ÍÌÌÌÌÌ@7@333333=@ÍÌÌÌÌÌ$@!@(@333333"@33333³;@š™™™™5@ÍÌÌÌÌL:@š™™™™A@š™™™™™(@ÍÌÌÌÌÌ.@ffffff+@š™™™™™@333333@š™™™™™1@33333ó@@ffffff-@ÍÌÌÌÌÌ,@@@š™™™™YH@D@fffffæ2@ÍÌÌÌÌŒB@33333sE@š™™™™>@ Àš™™™™>@ffffff4@š™™™™YH@ffffffÀ333333?@š™™™™™À#@ÍÌÌÌÌÌ@@ø¿š™™™™ÙA@;@333333)@š™™™™™A@ffffff @ÍÌÌÌÌÌ>ÀÀ€5@3333335@š™™™™™'Àffffff+@ÍÌÌÌÌL6@ÍÌÌÌÌÌô¿ffffff @ÍÌÌÌÌÌ,@ÍÌÌÌÌL2@333333A@ÍÌÌÌÌÌ@š™™™™4@333333+@fffffæ:@ÍÌÌÌÌÌ:@ÍÌÌÌÌÌ@š™™™™2@€5@33333³:@33333sD@ÍÌÌÌÌL:@4@ÍÌÌÌÌÌ.@ffffff7@ÍÌÌÌÌL:@333333)@š™™™™™6@ffffff @333333-@ffffff4@33333³<@fffffæ4@š™™™™™>@"@š™™™™™ÀÍÌÌÌÌÌ@š™™™™™A@3333337@/@ÍÌÌÌÌÌ5@ffffffLÀ333333%@ÍÌÌÌÌÌ&ÀÍÌÌÌÌLC@ÍÌÌÌÌÌD@ffffff<@33333óB@ÍÌÌÌÌÌÀš™™™™™¹¿ÍÌÌÌÌÌ&@@ffffff@@ffffff3ÀfffffæB@ffffff'@333333%@D@@fffffæ4@,@š™™™™™7@ÍÌÌÌÌÌ#@š™™™™™2À@š™™™™3@ffffff@ffffff À/@333333C@3333335@ffffff<@ÍÌÌÌÌÌA@ÍÌÌÌÌL8@33333³2@fffff¦E@ÍÌÌÌÌL6@@ÍÌÌÌÌÌ4@š™™™™™>@š™™™™YI@333333À333333@À€0@ffffff/@ÍÌÌÌÌÌ)Àš™™™™™ù¿š™™™™0@š™™™™™;@.@fffff&@Àfffffæ@@ÍÌÌÌÌÌ2@@A@š™™™™YJ@ffffffÀÍÌÌÌÌL2@š™™™™™6@€;@ÍÌÌÌÌŒE@ÍÌÌÌÌÌ À33333óE@š™™™™™2@333333>@*@ffffff@ffffff@š™™™™:@@fffffæ@@š™™™™™@3333338Àfffffæ8@š™™™™™8@ffffff"À3333334@33333s@@š™™™™1@ÍÌÌÌÌL>@ffffff2@š™™™™™+Àffffff&@3333339@3333338@ÍÌÌÌÌÌ3@33333³@@ÍÌÌÌÌÌ1@ÍÌÌÌÌÌ>@@K@ÍÌÌÌÌÌÀ33333³@@š™™™™2@333333 @ÍÌÌÌÌÌ4@ÍÌÌÌÌÌ Àš™™™™YI@ÍÌÌÌÌÌCÀ33333³=@33333³:@33333sO@33333³E@š™™™™H@Q@33333sH@ÍÌÌÌÌŒH@fffffæ2@fffffæ4@@H@š™™™™™=@š™™™™™ @ÍÌÌÌÌÌ@ffffff @š™™™™ùS@333333G@ÀD@ÍÌÌÌÌŒG@š™™™™™3@fffffæ9@ÍÌÌÌÌÌE@ÍÌÌÌÌ,P@ÍÌÌÌÌÌ5@fffffæB@ffffff<@fffffæL@3333336@ÍÌÌÌÌŒX@33333³L@š™™™™™ ÀÍÌÌÌÌL0Àfffffæ>@33333³=@š™™™™™D@ÍÌÌÌÌL=@333333-@fffff&C@€<@š™™™™9S@š™™™™K@ffffff>@š™™™™YD@ffffff,@333333A@š™™™™™8@ffffff+@ÍÌÌÌÌÌ"@33333³3@33333³:@š™™™™;@@:@š™™™™2@fffffæ=@š™™™™9R@ÍÌÌÌÌÌ4@fffffæH@š™™™™C@ffffff2@ffffff8@š™™™™ÙB@ÍÌÌÌÌÌ!@C@fffff&C@333333O@ÍÌÌÌÌLQ@ffffff8@ffffffþ¿ÍÌÌÌÌ H@fffffæN@2@ffffffE@33333óG@ÀA@333333:@fffff&V@ÍÌÌÌÌLF@-@333333@33333³D@;@ÍÌÌÌÌŒA@333333H@š™™™™™ @ÍÌÌÌÌÌ,@€E@ T@š™™™™™@€3@ffffff@ÍÌÌÌÌÌ3@@š™™™™™,@33333³<@ÍÌÌÌÌÌ&@33333sC@ffffffJ@G@š™™™™ÙG@š™™™™™Ù?3333336@ÍÌÌÌÌÌ8@33333óO@š™™™™™ Àš™™™™9]@€B@€7@33333ÓW@€J@F@33333ó@@ÍÌÌÌÌ @@ÍÌÌÌÌ D@š™™™™™9@33333SV@333333>@ÍÌÌÌÌL=@333333@ffffff-@)@€@@@G@333333@333333)@P@š™™™™1@fffff¦I@3333336@š™™™™ÙD@š™™™™™8@.@š™™™™A@ÍÌÌÌÌÌ@@ÍÌÌÌÌ C@33333sK@33333³D@ÍÌÌÌÌ H@š™™™™ùQ@€5@333333;@š™™™™5@33333³H@š™™™™0@ÍÌÌÌÌL>@ffffffI@fffff&I@ÀC@33333óC@333333A@€6@š™™™™™4@33333sA@fffffR@33333³;@ffffffG@š™™™™ÙJ@ÍÌÌÌÌLP@fffff&B@33333³1@33333sL@š™™™™™3@š™™™™N@ÍÌÌÌÌÌ.@33333ÓP@33333sL@@E@ÍÌÌÌÌÌ@@fffff&E@š™™™™™U@ffffff4@ffffff)@ÍÌÌÌÌÌ-@ffffffæ?fffffæI@fffff&F@š™™™™™E@333333*@fffff&D@ffffffA@š™™™™ÙE@ÍÌÌÌÌÌ@ffffffI@fffff¦G@+@ffffff#@333333-@š™™™™™.@333333@š™™™™ÙM@š™™™™™-@=@333333>@33333sC@ÍÌÌÌÌL2@€>@ffffff?@@O@33333³L@fffffæ3@D@333333&@ÍÌÌÌÌL=@š™™™™™ÀÍÌÌÌÌLN@ffffff.@@H@š™™™™8@fffffæT@ÀR@`P@ÍÌÌÌÌLD@ÍÌÌÌÌL;@ÍÌÌÌÌÌ!À€H@š™™™™2@ÍÌÌÌÌL<@š™™™™™)@ÍÌÌÌÌÌ@ÍÌÌÌÌL8Àffffff5@333333.@ÍÌÌÌÌŒE@ÍÌÌÌÌÌL@ÍÌÌÌÌLN@š™™™™<@š™™™™0@€H@ÀC@33333³C@fffff&P@š™™™™5@ÍÌÌÌÌÌ8@@H@ÍÌÌÌÌLM@333333T@333333&ÀÍÌÌÌÌ F@333333(@š™™™™E@ÀF@333333@@333333Ó¿=@)@33333sJ@š™™™™™@ÍÌÌÌÌLM@ÍÌÌÌÌÌ?@ÍÌÌÌÌÌÀ€6@ffffff+@š™™™™YH@ÍÌÌÌÌÌÀffffff.@ÍÌÌÌÌÌ@ÍÌÌÌÌÌH@š™™™™™!À@333333-@333333C@€0@š™™™™E@ÍÌÌÌÌŒG@3333336@33333³9@3333331@ÍÌÌÌÌL6@4@ÍÌÌÌÌÌ<@ÍÌÌÌÌL;@ÀE@ÍÌÌÌÌ Q@ÍÌÌÌÌ C@š™™™™™(@ÍÌÌÌÌÌ+@&@š™™™™ÙD@ffffff6@š™™™™YD@fffffP@€<@333333M@fffff&A@ÍÌÌÌÌÌ1@33333³7@ÍÌÌÌÌL4@33333óC@ÍÌÌÌÌŒ@@š™™™™™4@š™™™™0@%@@R@š™™™™yU@ÍÌÌÌÌ G@ÍÌÌÌÌÌ%@ÍÌÌÌÌ I@ffffff!@fffffæC@š™™™™YK@33333óM@fffff¦K@@š™™™™™H@š™™™™™"@333333,À@W@š™™™™™@ÀF@ÍÌÌÌÌÌ@@ÍÌÌÌÌÌ@@ÀQ@š™™™™™)@333333#Àš™™™™™@@H@š™™™™:@333333 @C@š™™™™9@š™™™™™"@ffffff@š™™™™™.@š™™™™™@š™™™™™Ù?fffffæ=@33333³<@ffffff=@ÍÌÌÌÌÌ7@š™™™™™-@333333$Àš™™™™YF@33333³F@ Q@33333sR@33333T@333333<@333333@@ffffffC@fffffQ@š™™™™™0@333333A@ffffff-@š™™™™9@š™™™™;@fffffÆQ@ÍÌÌÌÌŒM@ÀB@ffffff8@ffffff$@ÍÌÌÌÌÌ9@š™™™™™J@š™™™™:@@33333³1@fffffæBÀfffffæ3@€4@33333³=@ÍÌÌÌÌ,U@ÍÌÌÌÌL2@<@ffffff*@ÍÌÌÌÌÌ@€?@33333³;@ÍÌÌÌÌŒL@€9À7@ÍÌÌÌÌL5À33333³7@fffff&M@%@š™™™™™8@33333óF@š™™™™S@333333&@À(@š™™™™Y@@ÍÌÌÌÌL@@333333À33333³7@J@ÍÌÌÌÌLA@€P@33333³E@ffffffE@33333³@@33333³5Àš™™™™™+@3333333@À\@ÍÌÌÌÌ B@33333ÓR@333333ó¿š™™™™™ñ¿ÍÌÌÌÌ F@33333³D@33333³4@333333!@ÍÌÌÌÌŒJ@fffff&L@@E@3333330ÀffffffS@š™™™™ÙD@N@àS@š™™™™™É¿@G@š™™™™9@š™™™™ÙE@fffff&P@ÍÌÌÌÌÌ$@33333³K@ÍÌÌÌÌÌ6@š™™™™ÙB@ÍÌÌÌÌÌ3@ @33333sC@&@333333!@33333³L@ÍÌÌÌÌL2@33333“T@333333N@ÍÌÌÌÌÌM@ÍÌÌÌÌÌ@fffffæ6@33333óF@>@ÍÌÌÌÌ E@š™™™™ù_@š™™™™0Àš™™™™™ñ?ÀB@ÍÌÌÌÌÌH@š™™™™™A@33333³D@33333³>@fffffæJ@fffffÆR@@M@ÍÌÌÌÌ I@ÍÌÌÌÌÌ1@€H@333333ã¿ T@ÍÌÌÌÌÌOÀ€N@33333sC@333333Ó¿à¿ÍÌÌÌÌÌì?š™™™™™¹?š™™™™™Ù¿š™™™™™Ù¿333333Ó¿š™™™™™¹?࿚™™™™™Ù¿š™™™™™¹?š™™™™™É¿333333㿚™™™™™¹?š™™™™™Ù¿333333ã¿à¿333333Ó¿š™™™™™Ù¿š™™™™™¹¿ffffffæ?333333ã¿ffffffæ¿333333Ó¿š™™™™™Ù¿ffffff濚™™™™™¹?š™™™™™É¿à¿š™™™™™¹?š™™™™™¹¿š™™™™™Ù¿š™™™™™É¿š™™™™™É?š™™™™™Ù¿š™™™™™É¿333333Ó¿š™™™™™Ù¿à¿333333Ó¿š™™™™™É?333333Ó¿333333ã¿333333Ó¿š™™™™™É¿333333Ó¿ffffff濚™™™™™Ù¿š™™™™™É¿š™™™™™Ù¿š™™™™™¹¿ÍÌÌÌÌÌì¿333333ã¿333333ã?š™™™™™Ù¿š™™™™™Ù¿š™™™™™É¿š™™™™™É¿š™™™™™Ù¿333333Ó¿ÍÌÌÌÌÌ쿚™™™™™Ù?š™™™™™¹¿š™™™™™¹¿š™™™™™é¿š™™™™™É¿š™™™™™É¿ÍÌÌÌÌÌô?à¿ð¿ffffff濚™™™™™Ù¿š™™™™™É¿š™™™™™é¿à¿ffffffæ¿À333333ó¿š™™™™™¹¿ÀÀš™™™™™¹?š™™™™™¹¿à?š™™™™™É¿š™™™™™Ù?š™™™™™¹¿š™™™™™¹¿š™™™™™¹¿ÍÌÌÌÌÌ쿚™™™™™¹¿ÍÌÌÌÌÌô¿à¿š™™™™™Ù¿ffffff濚™™™™™É¿ø¿ffffffæ¿333333㿚™™™™™¹¿š™™™™™¹?333333ã?࿚™™™™™¹?࿚™™™™™É¿333333ã¿ð¿333333ã¿333333Ó?š™™™™™É¿š™™™™™¹?š™™™™™¹?333333㿚™™™™™É?à¿ð¿š™™™™™É¿š™™™™™É¿š™™™™™¹¿š™™™™™é¿š™™™™™É¿š™™™™™¹¿š™™™™™É?à?š™™™™™¹¿š™™™™™É?š™™™™™¹?333333Ó¿333333Ó¿š™™™™™¹¿333333㿚™™™™™¹?š™™™™™¹?š™™™™™¹¿š™™™™™É?š™™™™™¹¿ÍÌÌÌÌÌì¿ffffff濚™™™™™¹¿š™™™™™¹¿333333Ó¿š™™™™™Ù¿š™™™™™É¿š™™™™™¹?š™™™™™Ù¿š™™™™™¹¿š™™™™™¹¿š™™™™™Ù¿ffffff濚™™™™™Ù¿š™™™™™Ù¿š™™™™™É¿š™™™™™É¿š™™™™™É¿š™™™™™É¿š™™™™™Ù¿š™™™™™¹¿333333Ó¿333333ã¿333333㿚™™™™™Ù¿ffffffæ¿à¿š™™™™™¹¿š™™™™™é?š™™™™™É¿à¿333333À𿚙™™™™¹?ÍÌÌÌÌÌì¿ÍÌÌÌÌÌ쿚™™™™™É¿š™™™™™Ù¿š™™™™™É¿š™™™™™Ù¿š™™™™™Ù¿š™™™™™¹¿333333㿚™™™™™É¿š™™™™™¹?š™™™™™É¿š™™™™™¹¿š™™™™™¹¿ÍÌÌÌÌÌü¿ffffffö¿š™™™™™Ù¿š™™™™™¹?ffffffæ¿ffffff濚™™™™™¹?š™™™™™¹?š™™™™™é¿š™™™™™Ù¿ÍÌÌÌÌÌì?ÍÌÌÌÌÌ쿚™™™™™¹?š™™™™™¹?š™™™™™¹¿š™™™™™Ù¿š™™™™™¹¿š™™™™™É?š™™™™™É¿š™™™™™É¿š™™™™™É?ffffff濚™™™™™¹¿ffffffæ¿ffffffö¿ð¿š™™™™™Ù?333333ã¿à¿333333ã¿ffffffæ¿ð¿š™™™™™Ù¿ð¿š™™™™™¹?à¿333333Ó¿š™™™™™É?š™™™™™¹?š™™™™™É¿š™™™™™¹¿333333Ó¿š™™™™™¹?ÍÌÌÌÌÌü¿š™™™™™É¿š™™™™™É¿š™™™™™É¿š™™™™™Ù¿š™™™™™Ù?š™™™™™Ù¿ffffff濚™™™™™¹¿à¿š™™™™™¹?š™™™™™¹?š™™™™™¹¿š™™™™™¹¿š™™™™™¹?š™™™™™Ù¿à¿à¿š™™™™™é¿š™™™™™É¿š™™™™™É?š™™™™™É¿š™™™™™¹?š™™™™™É¿š™™™™™É¿333333Ó¿š™™™™™É¿š™™™™™¹¿š™™™™™¹¿š™™™™™É¿ð¿š™™™™™¹?ÍÌÌÌÌÌì¿ÍÌÌÌÌÌ쿚™™™™™é¿333333ó?š™™™™™É?š™™™™™¹¿ÍÌÌÌÌÌ쿚™™™™™¹¿333333㿚™™™™™É¿333333ó¿š™™™™™¹?š™™™™™¹?Àš™™™™™¹?ÍÌÌÌÌÌì¿ffffff濚™™™™™¹?333333Ó¿333333Ó¿ffffffþ?š™™™™™¹?š™™™™™¹¿à¿333333㿚™™™™™É¿ð¿ffffffþ¿š™™™™™¹?à¿333333ã¿ð¿š™™™™™ñ¿ð¿š™™™™™¹¿š™™™™™Ù¿š™™™™™¹?ffffffæ¿333333ã?š™™™™™¹?ÍÌÌÌÌÌ쿚™™™™™¹?š™™™™™¹¿š™™™™™¹¿š™™™™™É¿š™™™™™¹?š™™™™™Ù¿š™™™™™Ù?š™™™™™Ù¿š™™™™™¹?333333㿚™™™™™¹¿ÍÌÌÌÌÌ쿚™™™™™Ù¿ø¿š™™™™™Ù¿ø?š™™™™™Ù¿333333Ó¿333333ã¿ð¿š™™™™™ñ¿š™™™™™é¿š™™™™™Ù¿š™™™™™Ù¿š™™™™™É¿ÍÌÌÌÌÌì¿ð¿š™™™™™é¿ÍÌÌÌÌÌô¿š™™™™™¹?à¿333333Ó¿333333Ó¿š™™™™™Ù¿à?š™™™™™¹¿à¿ÍÌÌÌÌÌ쿚™™™™™É?333333㿚™™™™™É¿333333Ó¿š™™™™™É¿ffffffæ¿333333ã¿333333ã¿ffffffÀø¿š™™™™™¹?š™™™™™É¿š™™™™™é¿š™™™™™É¿333333ó¿š™™™™™É?à?š™™™™™¹¿à¿š™™™™™¹?333333Ó¿š™™™™™@š™™™™™É¿š™™™™™É?ffffffö¿š™™™™™Ù¿à¿333333㿚™™™™™é¿ffffffæ¿ø¿à¿333333Ó¿ÍÌÌÌÌÌ쿚™™™™™ñ¿333333Ó¿ffffffö?š™™™™™é¿š™™™™™Ù¿š™™™™™Ù¿š™™™™™É¿š™™™™™é?š™™™™™É?š™™™™™¹?333333Ó¿333333Ó¿333333㿚™™™™™¹?š™™™™™¹?š™™™™™É¿š™™™™™¹¿à?333333㿚™™™™™Ù¿333333ӿ𿚙™™™™¹¿š™™™™™Ù¿š™™™™™¹?š™™™™™Ù¿à¿333333ó?š™™™™™É?333333Ó¿333333ã¿333333ã¿à¿ÍÌÌÌÌÌì¿333333ã¿333333ã¿333333ã¿à¿333333ã¿ffffffæ¿ð¿ÍÌÌÌÌÌ쿚™™™™™ù¿à¿à¿à¿š™™™™™Ù¿š™™™™™Ù¿ffffffæ¿ø¿ffffffþ¿à¿333333ã¿ÍÌÌÌÌÌÀ𿚙™™™™ù¿ÍÌÌÌÌÌô¿š™™™™™Ù¿333333û¿ffffffþ¿à¿š™™™™™ñ¿ÍÌÌÌÌÌ쿚™™™™™Ù¿333333㿚™™™™™Ù¿à¿š™™™™™Ù¿š™™™™™é¿ffffffæ¿333333ó¿š™™™™™Ù¿333333ã¿ÍÌÌÌÌÌü¿š™™™™™é¿š™™™™™Ù¿333333ã¿ffffff濚™™™™™Ù¿š™™™™™Ù¿ffffffæ¿ÍÌÌÌÌÌ쿚™™™™™Ù¿333333ó¿š™™™™™Ù¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌô¿ffffffæ¿333333û¿š™™™™™ñ¿ffffffæ¿ffffffæ¿ÍÌÌÌÌÌ쿚™™™™™Ù¿à¿ÍÌÌÌÌÌô¿š™™™™™Ù¿š™™™™™Ù¿š™™™™™Ù¿ð¿333333㿚™™™™™é¿ffffffÀÍÌÌÌÌÌì¿333333ã¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌô¿ffffffæ¿ffffff濚™™™™™ñ¿ffffffæ¿à¿333333ã¿333333Àffffff濚™™™™™Ù¿333333ã¿à¿333333ó¿ø¿š™™™™™ñ¿333333ó¿333333ã¿ffffffÀ࿚™™™™™é¿š™™™™™Ù¿š™™™™™é¿ffffffæ¿ffffffæ¿333333ã¿ð¿à¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌô¿à¿š™™™™™ñ¿ffffffæ¿333333㿚™™™™™Ù¿333333ã¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌü¿ Àš™™™™™Ù¿š™™™™™é¿š™™™™™é¿ð¿333333ã¿à¿š™™™™™é¿ð¿à¿ffffffþ¿š™™™™™Ù¿š™™™™™Ù¿ffffffþ¿ Àš™™™™™Ù¿š™™™™™ñ¿à¿š™™™™™Ù¿ffffff濚™™™™™Ù¿333333 Àà¿333333㿚™™™™™Ù¿ÍÌÌÌÌÌ쿚™™™™™Ù¿š™™™™™Ù¿"@!@#@ffffff(@333333*@ffffff(@@ffffff @@š™™™™™%@"@š™™™™™@333333@š™™™™™(@ÍÌÌÌÌÌ(@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ"@ffffff@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333"@š™™™™™ @ÍÌÌÌÌÌ!@ÍÌÌÌÌÌ@333333+@š™™™™™@š™™™™™.@ffffff)@š™™™™™@333333@@š™™™™™@333333 @ÍÌÌÌÌÌ%@333333@ffffff$@ÍÌÌÌÌÌ&@ffffff&@ffffff&@ÍÌÌÌÌÌ@š™™™™™%@ffffff@333333!@ÍÌÌÌÌL0@@333333@ffffff@#@š™™™™™@@ffffff@333333@ÍÌÌÌÌÌ(@€0@ffffff%@"@333333$@ffffff @š™™™™™)@$@ffffff@#@ffffff@)@,@ÍÌÌÌÌÌ@333333@333333(@/@333333@ÍÌÌÌÌÌ@š™™™™™@!@ @ffffff&@š™™™™™%@@š™™™™™-@333333&@š™™™™™"@ÍÌÌÌÌÌ#@ÍÌÌÌÌÌ@š™™™™™@!@(@ÍÌÌÌÌÌ.@š™™™™™!@333333$@@@š™™™™™@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ"@333333*@!@ffffff@š™™™™™'@@*@ffffff$@ÍÌÌÌÌÌ2@ffffff@%@333333*@ffffff"@ÍÌÌÌÌÌ)@š™™™™™!@#@333333#@ @ÍÌÌÌÌÌ(@ÍÌÌÌÌÌ'@333333@@ÍÌÌÌÌÌ@333333@ÍÌÌÌÌÌ%@š™™™™™$@333333"@ÍÌÌÌÌÌ@333333@@ffffff#@333333@(@333333@ffffff!@@ÍÌÌÌÌÌ'@333333@&@ffffff@š™™™™™&@ÍÌÌÌÌÌ+@@ffffff$@š™™™™™@333333'@@ffffff@š™™™™™'@333333@š™™™™™@š™™™™™'@333333!@333333@š™™™™™ @@ÍÌÌÌÌÌ#@333333@ffffff!@333333!@ffffff#@333333'@ÍÌÌÌÌÌ"@ffffff!@ffffff@(@!@ÍÌÌÌÌÌ!@333333@333333#@ffffff!@@ÍÌÌÌÌÌ"@#@š™™™™™@ffffff @333333 @(@ffffff*@š™™™™™@@š™™™™™$@ÍÌÌÌÌÌ!@333333$@333333@)@@ffffff @333333@ffffff @ffffff@š™™™™™@š™™™™™@š™™™™™7@ffffff@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ+@ffffff&@333333 @ffffff@ÍÌÌÌÌÌ)@333333&@š™™™™™&@ffffff0@ffffff@@š™™™™™@#@@š™™™™™#@333333 @š™™™™™$@ÍÌÌÌÌL1@ÍÌÌÌÌÌ$@333333!@333333@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ(@ÍÌÌÌÌÌ@333333@333333)@š™™™™™@333333%@@š™™™™™@ÍÌÌÌÌÌ%@333333*@@333333@š™™™™™#@ÍÌÌÌÌÌ @333333@š™™™™0@333333@ffffff!@333333!@333333,@ÍÌÌÌÌÌ.@333333%@š™™™™™@š™™™™™"@$@ffffff(@333333!@@š™™™™™%@@ffffff*@ffffff$@ÍÌÌÌÌÌ$@ÍÌÌÌÌÌ%@š™™™™™@ffffff@ffffff!@#@ffffff@#@333333@ffffff @š™™™™™,@š™™™™™@333333@&@š™™™™™@š™™™™™@333333&@ffffff'@@ffffff@333333"@333333@ÍÌÌÌÌÌ#@š™™™™™@ffffff!@333333#@ÍÌÌÌÌÌ@333333@ffffff&@@š™™™™™#@333333#@333333&@ffffff$@ffffff@%@333333@š™™™™™@ffffff@333333 @š™™™™™(@333333@ÍÌÌÌÌÌ@333333@@ffffff*@ÍÌÌÌÌÌ$@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ#@ÍÌÌÌÌÌ*@ÍÌÌÌÌÌ!@ffffff @š™™™™™!@ffffff%@ffffff@@ffffff@ÍÌÌÌÌÌ @š™™™™™'@š™™™™™,@333333@333333@š™™™™™.@š™™™™™@ÍÌÌÌÌÌ"@*@%@š™™™™™@ffffff%@!@@ÍÌÌÌÌÌ@!@š™™™™™&@ffffff @$@š™™™™™#@ÍÌÌÌÌÌ+@333333@ÍÌÌÌÌÌ @ @333333.@ffffff"@š™™™™™!@ÍÌÌÌÌÌ@€2@ffffff&@ffffff-@ffffff&@š™™™™™#@ÍÌÌÌÌÌ@333333$@ÍÌÌÌÌÌ @š™™™™™@333333*@ffffff@333333@š™™™™™@š™™™™™#@ffffff @ffffff$@@ffffff@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ%@333333 @'@333333 @ð?333333&@ffffff@š™™™™™(@333333+@333333@ÍÌÌÌÌÌ'@ffffff @ffffff@ffffff$@ÍÌÌÌÌÌ@š™™™™™&@ÍÌÌÌÌÌ@)@š™™™™™$@ffffff#@š™™™™™ @333333@333333#@ffffff"@333333,@!@ÍÌÌÌÌÌ@ffffff@333333"@)@ÍÌÌÌÌÌ@š™™™™™"@ffffff*@ÍÌÌÌÌÌ"@ÍÌÌÌÌÌ'@333333-@š™™™™™"@š™™™™™%@(@ffffff/@ÍÌÌÌÌÌ@ffffff.@ÍÌÌÌÌÌ(@ffffff0@333333@ffffff@333333@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ'@€0@ÍÌÌÌÌÌ+@333333@333333'@@ffffff$@3333331@ffffff#@š™™™™™,@ÍÌÌÌÌÌ"@ffffff @š™™™™5@333333'@333333+@ÍÌÌÌÌÌ*@ÍÌÌÌÌÌ)@.@ffffff@ffffff(@-@ffffff@333333,@333333#@$@š™™™™™$@ffffff!@š™™™™™@ÍÌÌÌÌÌ@š™™™™™&@š™™™™™3@!@š™™™™1@š™™™™™@ffffff$@ffffff#@ÍÌÌÌÌÌ#@ffffff%@+@ffffff @š™™™™™0@ÍÌÌÌÌÌ&@ffffff@%@333333@ffffff@ÍÌÌÌÌÌ@333333@333333/@ÍÌÌÌÌL0@(@ÍÌÌÌÌÌ$@ÍÌÌÌÌÌì¿333333Àš™™™™™ÀÍÌÌÌÌÌ ÀffffffÀffffffþ¿ÍÌÌÌÌÌô¿333333û¿Àš™™™™™ÀÍÌÌÌÌÌÀš™™™™™é¿333333û¿ÍÌÌÌÌÌì¿ffffffö¿ÍÌÌÌÌÌü¿ð¿à¿š™™™™™ñ¿ÍÌÌÌÌÌü¿333333Àš™™™™™ù¿š™™™™™é¿š™™™™™é¿š™™™™™é¿ÍÌÌÌÌÌÀÍÌÌÌÌÌô¿ð¿ø¿ÍÌÌÌÌÌô¿ð¿š™™™™™Àš™™™™™ù¿š™™™™™é¿ÍÌÌÌÌÌÀš™™™™™é¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌÀ333333û¿ffffffæ¿ÍÌÌÌÌÌÀ333333À333333Àð¿ø¿ÍÌÌÌÌÌÀð¿ Àffffffö¿š™™™™™À333333Àø¿333333ã¿ÍÌÌÌÌÌÀ333333ó¿ÍÌÌÌÌÌÀÍÌÌÌÌÌ ÀÍÌÌÌÌÌì¿ÍÌÌÌÌÌÀ333333ó¿ffffff Àffffffþ¿Àš™™™™™ ÀÍÌÌÌÌÌì¿ffffffÀÍÌÌÌÌÌÀ333333㿚™™™™™é¿š™™™™™ù¿ÍÌÌÌÌÌÀÍÌÌÌÌÌô¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌü¿š™™™™™ù¿ÍÌÌÌÌÌü¿À333333ÀÍÌÌÌÌÌô¿ffffffæ¿333333ÀffffffÀà¿333333ó¿š™™™™™ÀffffffÀffffffö¿ø¿š™™™™™ù¿ÍÌÌÌÌÌô¿ffffffÀà¿À333333û¿Àš™™™™™é¿ffffffæ¿ÍÌÌÌÌÌü¿ø¿Àffffff ÀÀÀffffffþ¿À333333ó¿š™™™™™Àffffffö¿ÍÌÌÌÌÌÀÍÌÌÌÌÌô¿ÀÀš™™™™™é¿333333ó¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌô¿š™™™™™Ù¿333333ó¿ÀÀ333333 Àffffffþ¿š™™™™™ù¿ÍÌÌÌÌÌì¿333333û¿ð¿ÍÌÌÌÌÌü¿333333À333333Àà¿333333ã¿333333 Àš™™™™™ñ¿ffffffÀš™™™™™ Àø¿à¿ÍÌÌÌÌÌÀÍÌÌÌÌÌô¿ÍÌÌÌÌÌì¿ffffffÀ333333ó¿š™™™™™ñ¿š™™™™™ÀÀffffffö¿ð¿š™™™™™é¿333333ã¿à¿ÍÌÌÌÌÌÀø¿š™™™™™ñ¿ÍÌÌÌÌÌÀš™™™™™ñ¿ÍÌÌÌÌÌÀÀš™™™™™Àš™™™™™À333333ÀffffffÀffffffÀÍÌÌÌÌÌÀš™™™™™é¿333333ã¿333333ó¿333333û¿ffffffÀš™™™™™Àš™™™™™é¿333333ÀÍÌÌÌÌÌÀ333333Àš™™™™™ÀÀffffffæ¿à¿333333ó¿š™™™™™ù¿ÍÌÌÌÌÌÀ333333ã¿ffffff濚™™™™™ñ¿ffffff ÀÀÍÌÌÌÌÌÀffffffö¿ð¿à¿333333ã¿À333333ã¿ÍÌÌÌÌÌÀ333333ó¿333333ã¿ÍÌÌÌÌÌì¿333333ã¿ÍÌÌÌÌÌÀffffffÀÀffffffþ¿ Àš™™™™™Àš™™™™™ù¿333333ó¿Àš™™™™™Ù¿š™™™™™é¿333333Àffffff ÀÍÌÌÌÌÌ À333333û¿š™™™™™ñ¿Àš™™™™™ù¿ÍÌÌÌÌÌü¿ÍÌÌÌÌÌì¿ÍÌÌÌÌÌô¿š™™™™™é¿333333ó¿333333ã¿ÍÌÌÌÌÌü¿ffffffö¿333333Àffffffþ¿333333ó¿ÍÌÌÌÌÌô¿ÍÌÌÌÌÌô¿š™™™™™ñ¿ÍÌÌÌÌÌô¿333333ÀffffffÀÀffffffþ¿333333 ÀffffffÀø¿Àš™™™™™ñ¿š™™™™™ñ¿333333Àš™™™™™ Àš™™™™™ñ¿333333û¿š™™™™™é¿ÍÌÌÌÌÌü¿š™™™™™Àš™™™™™ÀÀš™™™™™ù¿ÍÌÌÌÌÌ À333333ÀÍÌÌÌÌÌÀš™™™™™ù¿ÀÍÌÌÌÌÌÀš™™™™™Ù¿333333Àffffffþ¿Àffffffæ¿333333ÀÀ ÀÍÌÌÌÌÌü¿ffffffö¿333333ó¿.Àffffffæ¿À333333û¿š™™™™™ù¿333333Àš™™™™™ù¿ffffffö¿ÀÍÌÌÌÌÌô¿333333ÀffffffÀš™™™™™Àš™™™™™À ÀÍÌÌÌÌÌÀš™™™™™é¿à¿ Àš™™™™™Ù¿ÍÌÌÌÌÌÀÍÌÌÌÌÌÀffffffÀš™™™™™Àffffffæ¿333333ã¿333333ó¿ffffffæ¿333333 ÀÍÌÌÌÌÌì¿ffffffÀÀÍÌÌÌÌÌô¿333333Àffffffæ¿Àš™™™™™ù¿š™™™™™<Àffffffæ¿ÍÌÌÌÌÌü¿à?à?š™™™™™é?ÍÌÌÌÌÌì?š™™™™™É?333333Ó?š™™™™™Ù?š™™™™™¹¿ÍÌÌÌÌÌì?š™™™™™Ù?š™™™™™¹?333333ã?š™™™™™é?š™™™™™é?à?š™™™™™é?š™™™™™Ù?333333Ó?333333ã?333333ã?333333Ó?š™™™™™Ù?à?333333ã?š™™™™™é?à?š™™™™™Ù?à?š™™™™™Ù?333333ã?333333Ó?š™™™™™Ù?à?ffffffæ?š™™™™™Ù?ffffffæ?ffffffæ?ÍÌÌÌÌÌì?333333ã?à?ð?ÍÌÌÌÌÌì?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?ÍÌÌÌÌÌì?š™™™™™Ù?333333ã?ffffffæ?š™™™™™É?š™™™™™Ù?š™™™™™Ù?ffffffæ?ffffffæ?à?à?š™™™™™Ù?š™™™™™Ù?à?à?ffffffæ?š™™™™™Ù?š™™™™™é?ffffffæ?š™™™™™é?333333ã?š™™™™™¹¿à?333333ã?333333Ó¿š™™™™™é?ffffffæ?333333ã?š™™™™™É?ffffffæ?333333ã?š™™™™™Ù?š™™™™™Ù?333333ã?š™™™™™Ù?ffffffö?ÍÌÌÌÌÌì?à?ð?š™™™™™É¿š™™™™™é?š™™™™™É?š™™™™™¹?333333Ó?333333ã?333333ó?333333ã?333333Ó?ÍÌÌÌÌÌì?à?š™™™™™é?333333Ó¿333333ã?š™™™™™É?š™™™™™Ù?à?š™™™™™é?ffffffæ?š™™™™™ñ?š™™™™™É?š™™™™™Ù?š™™™™™é?ffffffæ¿ffffffæ?333333Ó¿š™™™™™Ù?333333Ó¿ÍÌÌÌÌÌì?à?333333Ó?333333Ó?333333ã?333333Ó?333333ã?ffffffæ?à?ffffffæ?333333Ó?š™™™™™é?š™™™™™Ù?š™™™™™Ù?à?ffffffæ?š™™™™™é?à?333333ã?š™™™™™Ù?ð?ffffffæ?š™™™™™¹¿333333ã?š™™™™™é?š™™™™™é?333333ã?ÍÌÌÌÌÌô?333333ã?à?ÍÌÌÌÌÌô?š™™™™™Ù?333333ã?š™™™™™Ù?333333ã?ffffffæ?š™™™™™Ù?ÍÌÌÌÌÌì?ffffffæ?š™™™™™¹?333333ã?š™™™™™Ù?š™™™™™Ù?ffffffæ?ffffffæ?š™™™™™é?š™™™™™Ù?333333Ó?š™™™™™É¿ffffffæ?ð?à?à?š™™™™™Ù?ÍÌÌÌÌÌô?ffffffæ?333333ã?à?333333ã?333333ã?333333Ó?š™™™™™É¿333333Ó?à?333333Ó¿333333ã?333333Ó?ffffffæ?š™™™™™Ù?š™™™™™Ù?ffffffæ?ffffffæ?à?š™™™™™é?à?333333ã?š™™™™™É?š™™™™™¹¿ffffffæ?š™™™™™é?à?à?à?333333ã?à?333333@š™™™™™é?333333Ó?š™™™™™Ù?š™™™™™Ù?333333Ó?š™™™™™é?š™™™™™Ù?š™™™™™Ù?š™™™™™¹?à?333333ã?333333ã?ffffffæ?š™™™™™Ù?š™™™™™é?ÍÌÌÌÌÌì?š™™™™™¹?ð?š™™™™™é?333333Ó?à?à?à?š™™™™™é?333333Ó?š™™™™™é?333333ã?š™™™™™é?333333Ó?333333Ó?ffffffæ?ÍÌÌÌÌÌì?š™™™™™Ù?à?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?333333Ó?333333ã?ffffffæ?ð?333333Ó?333333ã?š™™™™™é?š™™™™™É¿š™™™™™é?ð?š™™™™™Ù?333333ã?ffffffæ?ffffffæ?à?ffffffæ?à?š™™™™™Ù?333333ã?ffffffæ?š™™™™™Ù?à?à?ÍÌÌÌÌÌô?ð?333333ã?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™é?ffffffæ?333333Ó¿333333Ó?à?š™™™™™Ù¿š™™™™™Ù?333333Ó?š™™™™™Ù?à?ÍÌÌÌÌÌì?à?à?š™™™™™¹¿333333ã?à?š™™™™™Ù?à?333333Ó¿š™™™™™É?š™™™™™Ù?à?š™™™™™Ù?š™™™™™Ù?š™™™™™É?333333ã?š™™™™™é?à¿ffffffæ?333333Ó?333333Ó?š™™™™™é?š™™™™™Ù¿š™™™™™Ù?š™™™™™Ù¿à?à?ð?š™™™™™Ù?à?š™™™™™Ù?š™™™™™Ù?à¿333333Ó¿ÍÌÌÌÌÌì?ffffffæ?333333ã?ffffffæ?š™™™™™é?à?š™™™™™é?ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?š™™™™™ñ?333333ã?333333Ó?š™™™™™ñ?333333ã?à?š™™™™™Ù?š™™™™™Ù?ÍÌÌÌÌÌì?š™™™™™Ù?333333ã?ÍÌÌÌÌÌì?ð?š™™™™™¹¿à?à?333333ã?š™™™™™é?š™™™™™Ù?à?333333ã?à?š™™™™™é?à?333333ã?ffffffæ?à?š™™™™™Ù?ø?333333ã?333333Ó?š™™™™™¹?ffffffæ?333333Ó?ÍÌÌÌÌÌô?ffffffæ?à?š™™™™™Ù?š™™™™™É?333333Ó?ð?š™™™™™é?à?333333ã?à?333333Ó?š™™™™™Ù?à?333333ó?333333Ó?ffffffæ?š™™™™™É?333333ã?333333ã?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™é?ÍÌÌÌÌÌì?à?ffffffæ?ÍÌÌÌÌÌì?š™™™™™é?ffffffæ?š™™™™™Ù?š™™™™™é?333333Ó¿à?à?à?ffffffæ?333333ã?à?ffffffæ¿333333ã?333333ã?š™™™™™¹¿à?ÍÌÌÌÌÌì?333333ã?š™™™™™Ù¿š™™™™™Ù?333333Ó?š™™™™™é¿à?š™™™™™¹¿š™™™™™Ù?ÍÌÌÌÌÌô?à?š™™™™™Ù¿à?333333Ó?š™™™™™¹¿ð?ffffffæ?à?ÍÌÌÌÌÌì?š™™™™™É?ffffffæ?à?š™™™™™É?š™™™™™é?à?ð?ð?333333ã?à?š™™™™™Ù?333333Ó?ffffffæ?š™™™™™É?333333ã?ffffffæ?333333Ó?à?š™™™™™é?à?ð?à?š™™™™™é?ffffffæ?ffffffæ?à?à?à?š™™™™™Ù¿à?ffffffæ?š™™™™™Ù?ø?à?à?333333@ÍÌÌÌÌÌ@333333 @ÍÌÌÌÌÌ@333333@š™™™™™@ð?à?ÍÌÌÌÌÌ@ÍÌÌÌÌÌì?ÍÌÌÌÌÌì?ø?à?ÍÌÌÌÌÌ@333333û?333333û?ffffff@ffffffæ?à?ÍÌÌÌÌÌü?ffffff @š™™™™™ñ?333333û?š™™™™™ñ?@ÍÌÌÌÌÌô?ÍÌÌÌÌÌ@š™™™™™ @ffffffö¿š™™™™™ñ¿š™™™™™ñ?ffffffö?@333333ó?š™™™™™é?š™™™™™ù?š™™™™™ù?333333@ffffff@ð?ffffffþ?ÍÌÌÌÌÌì?ø?š™™™™™ñ¿ffffffæ?ø?ÍÌÌÌÌÌô?à?š™™™™™ù?333333ã?ÍÌÌÌÌÌô?333333ó?333333ã?ÍÌÌÌÌÌ@333333Ó¿ÍÌÌÌÌÌ@ø?š™™™™™É?š™™™™™é¿š™™™™™ù?ÍÌÌÌÌÌì?333333@@@333333@š™™™™™ñ?333333Ó¿@š™™™™™@š™™™™™ù?ffffffþ?ffffffþ?ø?š™™™™™ù? @š™™™™™@š™™™™™À333333ã¿333333@à?à?ÍÌÌÌÌÌü?ÍÌÌÌÌÌì¿333333Ó?ø?š™™™™™ Àš™™™™™À333333ã¿ffffff Àø¿333333ã?š™™™™™Ù?ffffff@ffffffæ?333333@š™™™™™@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ffffffæ?à¿333333ã?ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?š™™™™™ @ÍÌÌÌÌÌì?ffffff濚™™™™™Ù?ffffff@333333û¿333333Ó?ÍÌÌÌÌÌô?ÍÌÌÌÌÌ@ÍÌÌÌÌÌô?ffffff@333333û?ffffffþ?ÍÌÌÌÌÌ@š™™™™™ù?333333ó?333333ó?ÍÌÌÌÌÌü?ÍÌÌÌÌÌì?ÍÌÌÌÌÌô?@333333ó?ÍÌÌÌÌÌ@ffffffö?333333û?ð?333333Ó?@333333ã?333333ó?ÍÌÌÌÌÌ@333333@ÍÌÌÌÌÌô?š™™™™™ @333333ó?ð?ø?ÍÌÌÌÌÌü?ffffffæ?333333û?ffffff@@ÍÌÌÌÌÌ@ffffffö?ÍÌÌÌÌÌü?ffffffþ?ÍÌÌÌÌÌô?@333333@š™™™™™@ø?š™™™™™@ffffff@ffffffö?à?ÍÌÌÌÌÌô?ø?ffffff@š™™™™™¹?š™™™™™ @ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333ã?ÍÌÌÌÌÌ@333333@š™™™™™é?333333Ó?š™™™™™é?š™™™™™é?ffffff @š™™™™™ñ?@š™™™™™ñ?ø?ffffffþ?@333333ã¿333333ó?ÍÌÌÌÌÌ@š™™™™™Ù?ð¿333333Ó?š™™™™™Ù?ffffffæ?à?333333À𿚙™™™™@ffffffæ?ÍÌÌÌÌÌì?š™™™™™é¿ffffffþ?ø?ffffff@333333û?ÍÌÌÌÌÌü¿ø?š™™™™™Ù?š™™™™™ñ?333333Ó¿ @333333ó?ffffff@ffffff@š™™™™™ù¿@ffffff@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ffffffö¿ø?࿚™™™™™@333333û¿ÍÌÌÌÌÌì?š™™™™™É¿333333Ó?ffffffö?ffffff@š™™™™™@@333333û?ÍÌÌÌÌÌì?š™™™™™ù?333333ó?333333û?ÍÌÌÌÌÌ@š™™™™™é?š™™™™™Ù¿š™™™™™@à?ÍÌÌÌÌÌ@ffffffö¿333333û?ÍÌÌÌÌÌì¿ð?@ffffffæ?š™™™™™é¿š™™™™™ù?333333Ó?@š™™™™™É?š™™™™™é?333333û?š™™™™™É?ÍÌÌÌÌÌô?ÍÌÌÌÌÌ@š™™™™™É?à?333333Ó?333333û?š™™™™™@ÍÌÌÌÌÌÀš™™™™™Ù¿š™™™™™Ù?333333ã?š™™™™™Ù?ffffff@ffffffþ?š™™™™™Ù?ffffffö?š™™™™™é?š™™™™™é?š™™™™™ù?ffffff@š™™™™™@@333333@ffffff@à?333333ã?š™™™™™Ù?š™™™™™ù?š™™™™™Ù?ÍÌÌÌÌÌü?ÍÌÌÌÌÌ@š™™™™™ñ?@ø?ffffffö?333333û?࿚™™™™™ñ?ffffffþ?ffffffæ?ffffffö?333333Ó?ffffffö?333333@ÍÌÌÌÌÌ@333333û¿ffffffö?š™™™™™À@š™™™™™ù?ÍÌÌÌÌÌ@ø¿ÍÌÌÌÌÌü?333333ó?ø?š™™™™™ À333333 @à?ð?š™™™™™É?ffffffö?333333@š™™™™™Ù?ø?333333ó?ffffff@333333ã?@ffffffþ?333333ã?š™™™™™é?š™™™™™¹¿ð¿333333ó¿š™™™™™Àø?ÍÌÌÌÌÌì?@š™™™™™ñ¿ÍÌÌÌÌÌì¿333333ó¿š™™™™™@333333û?@ffffffþ?ffffff@333333ó?ÍÌÌÌÌÌì?š™™™™™ù?ÍÌÌÌÌÌ@333333Ó¿333333ó?333333ó?š™™™™™ñ?ffffffæ?š™™™™™@ffffff@š™™™™™é?š™™™™™ñ?š™™™™™é?ÍÌÌÌÌÌô?š™™™™™ù?š™™™™™é¿333333ó¿ð?ÍÌÌÌÌÌì¿ÍÌÌÌÌÌì?š™™™™™ù?ÍÌÌÌÌÌì¿ÍÌÌÌÌÌ@š™™™™™é¿š™™™™™é¿ø?š™™™™™É?ffffff@@ffffff@š™™™™™é¿333333ó¿ffffffþ¿ð?ÍÌÌÌÌÌÀ333333ã?333333Ó?ffffff@ffffff@š™™™™™É?š™™™™™é?ð?@ffffff@š™™™™™É?333333ã?ÍÌÌÌÌÌì?ffffffö?@ÍÌÌÌÌÌô?š™™™™™É?ffffffö?ÀÍÌÌÌÌÌô¿ø?@ð?ffffffþ?à?ffffffþ¿@ffffff@ø?ÍÌÌÌÌÌô? @@š™™™™™é?ÍÌÌÌÌÌ @@@š™™™™™ñ?ÍÌÌÌÌÌ@š™™™™™Ù?333333û?333333Ó?š™™™™™ñ?š™™™™™ÀÍÌÌÌÌÌ@š™™™™™ù?š™™™™™É?ÍÌÌÌÌÌì?š™™™™™Ù¿ð?ffffffþ?333333û¿š™™™™™Ù?@š™™™™™ù?@@ffffff@š™™™™™é?š™™™™™Ù?ÍÌÌÌÌÌ @ÍÌÌÌÌÌü?333333ó?@š™™™™™É¿š™™™™™Ù?ÍÌÌÌÌÌô?333333@ÍÌÌÌÌÌ@333333ã?333333û?ÍÌÌÌÌÌü?ÍÌÌÌÌÌü?à?ffffffö?ffffff@ÍÌÌÌÌÌô?ffffff@333333ã?333333@ð?333333@š™™™™™ù?ÍÌÌÌÌL7@ffffff4@š™™™™™(@ÍÌÌÌÌLA@ffffff9@+@š™™™™™,@š™™™™™/@)@š™™™™4@š™™™™™(@š™™™™™@š™™™™™!@€<@š™™™™™C@3333331@€3@š™™™™1@3333332@3333339@š™™™™1@ffffff'@33333³3@ffffff2@fffff¦A@33333³4@F@ffffff9@3333331@ÍÌÌÌÌÌ @333333!@+@-@š™™™™:@š™™™™™'@ÍÌÌÌÌLA@ffffff,@€:@ÍÌÌÌÌL<@%@€7@333333 @ffffff0@33333óB@ÍÌÌÌÌÌ$@@š™™™™™'@ÍÌÌÌÌÌ6@fffffæ1@š™™™™™!@š™™™™™'@ÍÌÌÌÌÌ'@š™™™™™7@F@š™™™™™3@;@333333:@š™™™™2@33333³9@š™™™™™5@ÍÌÌÌÌÌü¿(@ffffff)@š™™™™>@ÍÌÌÌÌÌ@@š™™™™0@&@ffffff:@33333³E@ffffff@š™™™™:@6@€?@ffffff-@š™™™™=@333333.@š™™™™™ Àffffff-@ÍÌÌÌÌÌ:@š™™™™™7@š™™™™™?@fffffæ3@š™™™™4@ffffff@ÍÌÌÌÌÌ7@333333V@333333@@,@š™™™™YG@€F@ffffff$@333333#@š™™™™™@@ÍÌÌÌÌÌ@€3@ÍÌÌÌÌL5@š™™™™6@333333!@ÍÌÌÌÌL<@š™™™™™#@33333³G@ÍÌÌÌÌÌ"@333333L@€9@ÍÌÌÌÌL1@@F@ÍÌÌÌÌÌ4@š™™™™™=@ffffff:@33333³8@ÍÌÌÌÌÌ-@ÍÌÌÌÌÌ,@ffffff.@ffffff4@ffffff @3333338@ÍÌÌÌÌÌü?š™™™™™@3333336@ÍÌÌÌÌÌ<@333333@333333ó¿ÍÌÌÌÌL0@ÍÌÌÌÌÌ"@3333336@ÍÌÌÌÌÌ@ffffff:@(@ffffff9@ffffff@€<@33333³;@ffffff-@š™™™™™)@333333C@ÍÌÌÌÌL>@333333 @1@(@333333G@š™™™™™@333333&@ffffffF@333333&@ÍÌÌÌÌÌ"@š™™™™1@€1@ffffff"@ÍÌÌÌÌÌ$@ffffff$@3333331@š™™™™™É¿š™™™™™6@333333=@fffffæ:@ÍÌÌÌÌÌ;@š™™™™™3@ffffffA@ffffff#@š™™™™™8@š™™™™2@€@@š™™™™1@33333³6@š™™™™™6@ÍÌÌÌÌÌ@š™™™™0@š™™™™™'@ÍÌÌÌÌÌ%@fffffæ1@š™™™™™À333333(@š™™™™™7@333333)@333333'@fffffæ>@š™™™™™,@ÍÌÌÌÌL7@ffffff+@ÀB@ÍÌÌÌÌÌ2@ÍÌÌÌÌÌ,@š™™™™2@ffffff@@fffffæ1@333333@K@8@ÍÌÌÌÌÌì?€5@333333F@ffffff<@.@'@€?@€6@ÍÌÌÌÌ @@ÍÌÌÌÌ J@š™™™™™@ÍÌÌÌÌÌ3@š™™™™™@€6@33333³2@fffffæ6@ffffff@š™™™™ùP@š™™™™™D@š™™™™9@ @333333ó?ffffff@?@š™™™™™@333333&@fffffæ;@ @ÍÌÌÌÌÌ+À33333³1@ÍÌÌÌÌÌ @ffffff)@7@8@333333%@ÍÌÌÌÌÌ,@ÍÌÌÌÌÌ7@š™™™™6@ÍÌÌÌÌÌ.@fffff¦B@ÍÌÌÌÌÌ9@š™™™™8@š™™™™2@ÍÌÌÌÌŒF@ÍÌÌÌÌLC@š™™™™:@fffffæ:@5@8@?@ÍÌÌÌÌL1@š™™™™™&@š™™™™™1@3333332@š™™™™YA@ffffff$@ÍÌÌÌÌÌD@3333332@333333ã?ffffff@ffffff@š™™™™™1@ÍÌÌÌÌÌ쿚™™™™2@ÍÌÌÌÌÌÀÍÌÌÌÌÌ2@À@@333333,@)@ffffff;@ffffff3@ø?š™™™™™:@9@ÍÌÌÌÌÌ(@'@fffffæ1@ @ffffff-@333333 @ffffff5@33333³6@333333$@ÍÌÌÌÌÌ#@3333330@fffffæ0@ÍÌÌÌÌŒD@ÍÌÌÌÌÌ9@ffffff<@š™™™™™8@.@š™™™™™.@ffffff)@333333@#@š™™™™™0@333333A@ÍÌÌÌÌÌ.@33333³1@ffffff0@333333*@ffffffA@33333³8@ÍÌÌÌÌÌ @š™™™™™?@ffffffC@333333>@š™™™™™é?8@33333³7@ffffff/@ffffff#@33333³3@š™™™™™É?0@ÍÌÌÌÌÌ8@ffffff*@€7@š™™™™YB@,@fffff¦D@33333³:@š™™™™™@ÍÌÌÌÌÌÀffffff6@š™™™™™,@š™™™™™'Àffffff/@š™™™™™/@ÍÌÌÌÌÌ!@š™™™™™@ÍÌÌÌÌÌ0@333333/@š™™™™™G@333333@€0@ffffff*@<@33333³9@,@ÍÌÌÌÌÌ-@€D@š™™™™6@ffffffE@š™™™™7@š™™™™3@333333#@3@š™™™™™1@š™™™™™5@33333³9@ffffff#@,@ÍÌÌÌÌÌ+@ffffff1@€=@€3@333333!@ffffffþ?ÍÌÌÌÌÌ@ffffffA@š™™™™™6@ffffff8@5@ffffff0Àffffff5@ÍÌÌÌÌÌ @fffffæD@333333?@ffffff8@š™™™™ÙA@š™™™™™8@'@0@ÍÌÌÌÌÌ"@333333=@š™™™™™!Àš™™™™™?@š™™™™™,@€4@fffffæH@333333"@4@š™™™™™4@8@.@(Àffffff!@ÍÌÌÌÌÌ4@ffffff"@333333㿚™™™™1@ÍÌÌÌÌÌC@6@š™™™™;@ffffffD@*@9@š™™™™™U@ÍÌÌÌÌL@@ÍÌÌÌÌÌ#@3333337@ÍÌÌÌÌL?@ÍÌÌÌÌLE@ @š™™™™™>@š™™™™™1@ffffff.@@ÍÌÌÌÌÌ@š™™™™™8@ÍÌÌÌÌL9@33333³>@ÍÌÌÌÌLBÀš™™™™8@€<@€:@ÍÌÌÌÌ H@ÍÌÌÌÌÌ@š™™™™<@fffffæ3@ffffff0@33333óH@à?ffffffD@€6@fffffæ9@ffffff:@ÍÌÌÌÌÌ@&@333333>@ÍÌÌÌÌÌ-@š™™™™Y@@ÍÌÌÌÌÌ"@š™™™™YB@š™™™™8@fffffæ4@333333À33333³7@š™™™™0@ffffff(@ffffff4@333333.@ffffffÀ8@š™™™™8@š™™™™3@š™™™™™9@š™™™™>@€5@fffffæ8@ÍÌÌÌÌ D@ffffffÀÍÌÌÌÌÌ1@333333,@!@š™™™™™1@š™™™™™ÀC@š™™™™FÀ9@ÍÌÌÌÌL5@33333³H@ffffffC@€H@ÍÌÌÌÌÌN@ffffffO@š™™™™?@€4@ÍÌÌÌÌÌ3@fffffæ>@š™™™™9@ÍÌÌÌÌL3@3333331@333333(@ÍÌÌÌÌLH@ÍÌÌÌÌ K@š™™™™?@ÍÌÌÌÌŒD@ÍÌÌÌÌÌ4@ffffff6@@C@š™™™™YK@š™™™™™3@fffff&A@fffffæ<@fffff¦L@333333=@š™™™™Q@ffffffK@@333333ÀÍÌÌÌÌL0@€9@ÀC@@@@3333330@333333G@š™™™™™;@ÍÌÌÌÌìP@33333sN@fffffæ1@333333B@3333330@33333³9@š™™™™6@ÍÌÌÌÌÌ,@(@€7@:@ffffff=@ffffff)@š™™™™5@33333³6@€<@ÍÌÌÌÌÌN@33333³1@š™™™™YG@€B@33333³3@ffffff.@€A@ÍÌÌÌÌÌ@ÍÌÌÌÌŒ@@š™™™™Y@@fffff&J@š™™™™ÙM@ÍÌÌÌÌÌ7@š™™™™™ @33333³E@ÍÌÌÌÌÌM@3@@D@fffff&C@ÀE@ÍÌÌÌÌL?@ÍÌÌÌÌLN@€>@ÍÌÌÌÌLCÀffffff%@š™™™™™I@ffffff;@33333ó@@ÍÌÌÌÌLA@333333*@333333!@@@@š™™™™ÙE@333333 @"@@;@ffffff0@š™™™™™)@€;@ffffff%@ffffff?@ÍÌÌÌÌŒB@fffffæD@33333sF@ffffff/@š™™™™8@333333-@fffff¦J@fffffæ1@š™™™™ÙU@fffffæ>@ÍÌÌÌÌÌ%@š™™™™H@fffff¦H@333333#@ÍÌÌÌÌÌ<@ÍÌÌÌÌÌA@fffff¦@@š™™™™™:@ÍÌÌÌÌŒO@š™™™™™>@3333336@ÍÌÌÌÌLH@*@/@?@€D@ffffff+@ÍÌÌÌÌÌ @ffffffC@š™™™™™0@š™™™™™H@ffffff/@@A@š™™™™™3@33333³;@3333337@33333³?@C@€F@33333sC@š™™™™ÙG@33333óJ@€0@š™™™™™7@8@333333N@@ÍÌÌÌÌL9@ÍÌÌÌÌÌO@š™™™™YC@ÍÌÌÌÌL<@33333³=@fffffæ?@3333337@ffffff2@=@ÍÌÌÌÌÌE@š™™™™3@33333³@@fffffæH@š™™™™ÙL@33333óA@ÍÌÌÌÌÌ5@ÍÌÌÌÌÌE@fffffæ6@fffff&I@š™™™™™2@fffffæN@ÀA@fffffæB@3333339@š™™™™?@fffffÆQ@3@333333+@š™™™™8@š™™™™™¹?333333D@33333³?@ÍÌÌÌÌLB@fffffæ1@ÍÌÌÌÌÌC@333333;@ÍÌÌÌÌ B@333333@ffffffG@@C@3333332@ffffff'@ @@ÍÌÌÌÌL6@ÍÌÌÌÌÌ#@ffffff6@(@š™™™™5@fffffæ;@fffffæG@3333335@€>@ffffff8@ÍÌÌÌÌLJ@š™™™™Y@@ÍÌÌÌÌÌ2@fffffP@%@333333;@ø?K@À@@ÍÌÌÌÌŒB@:@š™™™™™F@ÍÌÌÌÌ P@š™™™™ÙH@ÍÌÌÌÌ E@š™™™™9@Àš™™™™F@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ;@ffffff @š™™™™™)@333333.Àfffffæ3@3333330@š™™™™ÙA@33333sH@ffffffJ@€8@ffffff3@ÍÌÌÌÌ D@ÀA@>@fffff¦K@ffffff?@33333³3@š™™™™™A@ÍÌÌÌÌLH@ P@!@@D@€0@š™™™™@@ÍÌÌÌÌLF@fffffæ5@š™™™™™ @>@ÍÌÌÌÌL4@fffff&H@š™™™™™)@ffffffJ@333333@@š™™™™™@ffffff/@š™™™™™5@ÍÌÌÌÌÌ2@ÍÌÌÌÌÌ@3333336@ÍÌÌÌÌÌ@33333³D@333333ã¿!@-@33333³@@€6@fffffæ<@33333³D@š™™™™;@5@/@ÍÌÌÌÌL8@€2@33333³?@fffffæ;@fffffæA@š™™™™ÙI@ffffff=@ffffff+@š™™™™4@€3@33333sJ@š™™™™?@fffff¦D@fffffæD@33333³9@fffff¦F@333333:@33333³0@33333³7@ffffff)@š™™™™™D@ÍÌÌÌÌL?@fffffæ5@ÍÌÌÌÌL;@ÍÌÌÌÌÌ-@@G@ÍÌÌÌÌŒH@ÍÌÌÌÌÌ:@333333@ÀF@,@€A@C@ÍÌÌÌÌÌG@š™™™™™é¿7@€=@333333-@š™™™™™À33333³L@ÍÌÌÌÌÌ1@š™™™™=@33333sC@33333³9@ÍÌÌÌÌìP@ÍÌÌÌÌL>@€3@š™™™™™@ÍÌÌÌÌŒE@ffffff1@333333û?š™™™™>@š™™™™™2@ÍÌÌÌÌÌ.@š™™™™™@š™™™™™@ÍÌÌÌÌÌ@€3@2@š™™™™5@š™™™™™;@ÍÌÌÌÌÌ%@33333³0@š™™™™™Ù?š™™™™YA@š™™™™™K@ÍÌÌÌÌÌG@ÍÌÌÌÌLN@ÍÌÌÌÌLO@33333³<@ÍÌÌÌÌÌ,@š™™™™Ù@@ÍÌÌÌÌ F@33333³3@š™™™™B@33333³5@š™™™™™7@33333³3@G@33333sL@33333³9@ÍÌÌÌÌL0@333333 @,@33333óG@š™™™™™%@ÍÌÌÌÌÌ)@ffffff<@ffffff9À333333>@ÍÌÌÌÌÌ5@ÍÌÌÌÌÌ?@K@3333332@€<@E@333333*@C@;@33333óH@ffffff*À2@ÍÌÌÌÌÌÀš™™™™;@333333;@)@33333³7@fffff&G@333333N@3333330@ÍÌÌÌÌÌÀ-@ffffffB@š™™™™@@@6@š™™™™ÙF@33333s@@fffff¦K@fffff&H@š™™™™™.@333333B@ÍÌÌÌÌÌ @€6@š™™™™™5@ffffffX@ÀB@fffff¦M@333333%@333333ÀÍÌÌÌÌLD@33333sD@3333334@ÍÌÌÌÌÌ&@ÍÌÌÌÌÌM@33333óH@33333³B@š™™™™4Àš™™™™ÙI@€H@€E@@R@ffffff&@F@ffffff6@ÍÌÌÌÌL:@š™™™™Ù@@ÍÌÌÌÌL1@š™™™™YI@3333338@33333s@@7@333333'@9@0@š™™™™1@ÍÌÌÌÌ L@ffffff7@š™™™™ù\@33333sJ@ÍÌÌÌÌLD@@ffffff:@š™™™™™H@š™™™™9@=@š™™™™U@š™™™™™ Àffffff9@š™™™™Ù@@fffff&B@š™™™™™E@€A@š™™™™Ù@@33333³D@ÀK@š™™™™™Ù?€=@ÍÌÌÌÌŒB@š™™™™™1@š™™™™™C@à¿333333O@š™™™™™@ÀÍÌÌÌÌLG@ffffff@@YÿmatrixþÿÿÿÄ @@$@ð?@,@ð?ð?ð?$@@ð?$@@ð?ð?$@ð?ð?ð?ð?ð?ð?ð?ð?0@,@$@@@@ð?ð?ð?@ð?ð?$@ð?@ð?ð?ð?ð?ð?@@ð?@ð?ð?ð?$@0@0@@ð?ð?@ð?@@ð?ð?ð?ð?@ð?@ð?@0@ð?ð?ð?$@@@ð?ð?ð?ð?@@@"@@@"@"@ð?@ð?@$@ð?$@ð?ð?ð?@ð?ð?ð?@@@ð?@ð?ð?ð?ð?ð?@ð?0@ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?$@ð?ð?ð?ð?ð?ð?$@ð?ð?$@ð?ð?ð?@ð?ð?$@$@$@ð?ð?$@ð?ð?ð?@0@ð?ð?@ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?@@ð?ð?ð?$@.@@ð?ð?ð?@ð?0@ð?@@@@@,@"@ð?ð?@@ð?ð?ð?0@0@ð?@ð?ð?ð?@ð?ð?"@ð?$@$@ð?@@@ð?@.@@0@ð?ð?@ð?$@@ð?0@ð?ð?ð?@ð?ð?ð?@ð?@ð?ð?ð?ð?ð?.@ð?@ð?ð?@ð?$@@@@ð?ð?@@@@ð?0@ð?ð?ð?ð?$@ð?ð?ð?ð?ð?@ð?ð?@ð?@$@ð?ð?ð?ð?@$@@ð?ð?ð?ð?ð?$@ð?$@@@@$@$@ð?.@ð?@@@ð?0@@@@ð?ð?$@$@ð?ð?@ð?ð?$@@ð?@@$@ð?$@@ð?@ð?ð?0@ð?$@ð?$@ð?ð?ð?0@$@ð?@$@ð?$@ð?@ð?ð?@ð?$@0@ð?@@@@@@0@$@@ð?@@@ð?"@ð?@ð?@@ @ð?ð?$@0@@ð?ð?@ð?0@@"@ð?ð?ð?ð?ð?ð?"@ð?$@@ð?$@,@ð?@ð?ð?ð?ð?ð?0@@@0@ð?ð?ð?ð?$@ð?ð?.@ð?ð?ð?"@ð?ð?$@ð?0@$@@$@@ð?ð?ð?ð?ð?ð?ð?ð?ð?$@ð?ð?ð?ð?$@@ð?ð?statistics-release-2.1.2/inst/datasets/carbig.mat000066400000000000000000001472611527311104300220530ustar00rootroot00000000000000Octave-1-L Accelerationÿmatrixþÿÿÿ–(@'@&@(@%@$@"@!@$@!@€1@'@&@%@&@$@ @ @#@$@.@/@/@0@-@€4@€1@-@€1@)@.@,@.@+@€2@-@/@,@3@4@*@/@/@/@/@(@'@+@*@'@(@(@+@3@.@-@,@,@€3@-@3@2@3@€4@/@1@€7@€3@€0@(@(@+@*@'@&@+@+@)@+@)@,@0@,@-@2@€3@2@0@1@-@.@€0@*@'@*@-@)@'@(@*@-@&@&@&@€0@2@0@€0@0@5@,@)@*@)@.@3@€3@€0@+@€2@,@/@*@#@€3@/@,@/@&@,@+@&@€0@1@0@1@3@€0@5@1@1@2@€0@,@-@+@0@/@€0@/@-@€0@3@-@/@,@.@/@0@0@0@5@€3@'@,@-@+@5@€2@3@3@.@+@(@0@1@0@€2@+@€0@1@-@,@1@.@1@-@+@€1@/@fffffæ0@ÍÌÌÌÌÌ-@33333³1@š™™™™™.@*@*@ÍÌÌÌÌÌ+@š™™™™™)@ÍÌÌÌÌÌ.@-@š™™™™™1@š™™™™™1@3333336@š™™™™6@ffffff,@ffffff1@33333³1@5@3333330@ÍÌÌÌÌÌ1@ffffff(@1@ffffff0@333333+@ffffff/@ffffff*@fffffæ5@/@33333³0@333333(@(@.@,@€2@š™™™™™-@š™™™™™2@/@ÍÌÌÌÌÌ0@)@3@ffffff+@ÍÌÌÌÌÌ-@ffffff0@fffffæ0@33333³1@3@333333&@ÍÌÌÌÌÌ&@ffffff(@-@-@0@3333332@š™™™™™/@1@ÍÌÌÌÌÌ/@ffffff0@333333,@-@š™™™™™)@+@€5@ÍÌÌÌÌÌ,@ffffff3@š™™™™™2@ffffff0@/@ffffff*@š™™™™™)@3333333@3333332@š™™™™™/@ÍÌÌÌÌÌ.@3333331@3333331@š™™™™™/@33333³0@33333³2@333333.@ffffff*@ÍÌÌÌÌÌ*@ffffff&@ffffff+@€0@ffffff,@ffffff-@-@š™™™™™-@33333³0@š™™™™™1@ÍÌÌÌÌÌ-@ÍÌÌÌÌÌ/@333333+@ffffff/@š™™™™™/@ÍÌÌÌÌÌ-@š™™™™™0@ÍÌÌÌÌÌ.@3333332@ÍÌÌÌÌL1@3333332@š™™™™™0@ÍÌÌÌÌÌ.@ÍÌÌÌÌÌ*@ffffff*@ffffff.@ÍÌÌÌÌÌ-@š™™™™™,@.@*@,@ffffff.@ÍÌÌÌÌÌ,@.@š™™™™4@ffffff1@ÍÌÌÌÌÌ8@3333336@ffffff*@ÍÌÌÌÌÌ-@3333333@ffffff-@0@š™™™™™&@ÍÌÌÌÌÌ)@ffffff*@ffffff-@ÍÌÌÌÌÌ2@/@ffffff0@€0@š™™™™2@š™™™™4@33333³2@š™™™™™/@/@€1@.@ffffff.@fffffæ1@ÍÌÌÌÌÌ,@3333333@33333³5@33333³7@fffffæ3@ÍÌÌÌÌÌ5@š™™™™™+@ÍÌÌÌÌL1@2@š™™™™™.@ÍÌÌÌÌÌ&@)@333333.@š™™™™™,@1@ffffff/@ffffff0@ÍÌÌÌÌÌ,@333333)@ÍÌÌÌÌÌ)@fffffæ0@ffffff0@š™™™™0@ÍÌÌÌÌÌ1@ffffff3@ÍÌÌÌÌL1@0@ÍÌÌÌÌÌ-@3333330@33333³4@ffffff,@š™™™™™/@ÍÌÌÌÌÌ,@ÍÌÌÌÌÌ0@š™™™™™-@ÍÌÌÌÌL2@ffffff4@ÍÌÌÌÌÌ.@š™™™™™3@333333)@š™™™™™+@š™™™™™/@3@š™™™™1@š™™™™™0@š™™™™™3@š™™™™™2@2@3333330@0@2@ffffff0@€4@š™™™™™.@3333332@š™™™™™1@ffffff-@ÍÌÌÌÌL1@-@-@fffffæ0@.@ffffff/@3333330@ffffff0@1@-@ffffff-@ÍÌÌÌÌÌ+@*@ÍÌÌÌÌL1@333333/@š™™™™™8@333333'@š™™™™™2@ffffff3@ Cylindersÿmatrixþÿÿÿ– @ @ @ @ @ @ @ @ @ @@ @ @ @ @ @ @ @ @ @@@@@@@@@@@@ @ @ @ @@@@@@@@@@@ @ @ @ @ @ @ @@@@@@@@@@@@@@@@@@ @ @ @ @ @ @ @ @ @@ @ @ @ @@@@@@@@@@ @ @ @ @ @ @ @ @ @ @ @ @@@@@@@ @ @ @ @@@@@@@@@ @ @@@@@ @@@ @@@@@@@@@@@@ @ @ @ @ @@@@@@@@@@@@@@@@ @ @ @ @@@@@@ @ @@@@@@@@@@@@@@@@@@@@@ @ @ @ @@@@@@@@@@@@@@@@@@ @@@@ @ @ @ @@@@@@ @ @ @ @@@@@ @ @ @ @@@@@@@@@@@@@@@@@ @ @ @@@@@@@@@@@ @@ @ @@@@@@@@@@@@@@@@@@@@ @ @ @ @ @ @ @ @@@@@@ @@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Displacementÿmatrixþÿÿÿ–0s@àu@às@s@àr@Ðz@`|@€{@p|@`x@ `@àu@ðu@ðw@€v@ðw@@u@àr@y@p|@@\@Àh@àh@i@@X@@X@€[@ÀZ@Z@@^@àh@€v@0s@às@s@@X@€a@@\@€X@@X@m@ l@@o@@o@m@àu@y@ðu@às@ðw@y@y@ p@€a@@o@@o@€^@]@ÀS@V@ÀQ@R@@X@ÀV@@\@`X@@X@€a@€^@àu@y@às@ðu@s@Ðz@àu@àu@y@€Q@s@0s@àr@às@@^@@^@^@X@€^@@X@^@€X@@X@àu@s@àu@àr@às@Ðz@y@ðu@às@€{@p|@€v@ l@@o@m@@o@Àh@@X@y@y@€v@àu@m@@X@€a@[@€Q@€^@`c@€X@àu@y@Q@]@€\@@^@às@@^@€c@àu@Àh@i@m@@o@ÀS@€^@ÀQ@€a@@o@ p@ l@àr@àu@às@àr@s@€X@ÀS@@X@S@ÀT@€V@€V@]@^@[@ÀS@ l@@o@@o@@o@y@àu@às@ðu@àl@@o@ p@ l@àl@`p@àr@@X@€a@m@€a@À`@€V@À]@`e@€V@m@À\@^@@^@@^@ÀV@ÀZ@]@€a@€X@@Y@s@às@s@ðu@ l@@o@i@m@@U@€X@€V@ÀV@ l@@o@@o@ p@@X@@U@@X@€a@@`@às@^@€c@e@àu@àu@àr@às@€X@À[@ÀS@€^@@U@s@@p@às@àr@@o@àl@ l@@o@y@àu@y@ðu@@X@àb@@X@€a@€X@€X@@X@@X@@b@@^@T@€V@€X@€S@@U@ÀV@@p@às@àr@àl@i@i@€a@ l@m@àl@i@ l@ p@s@àl@àr@às@€X@À`@À]@@Z@À`@€c@àb@À]@``@`d@@^@`d@@V@€X@àl@i@€a@m@ l@s@àr@ðu@às@àu@ðu@°p@€v@@V@€U@€X@@^@àf@àu@ a@@p@@Z@@Z@@U@ÀV@àb@ e@ e@àb@€X@@V@€X@€U@àb@€a@àb@ l@@X@À`@^@À]@[@€U@€c@@U@€V@€V@@^@@b@ÀV@@U@@X@@V@e@€Q@€^@€a@ÀZ@à`@àb@€c@ e@à`@ÀS@€U@@T@@X@@U@@V@ÀV@@Z@€X@€X@@Z@Y@ÀZ@[@À]@^@ a@@^@ b@e@@b@àl@àu@i@ l@\@\@\@\@à`@àb@€a@àb@@Z@ÀV@ÀV@@Z@€X@^@ÀZ@[@ÀV@ÀV@ÀV@ f@`p@€c@m@b@à`@àb@€a@@X@à`@^@À]@ Horsepowerÿmatrixþÿÿÿ–@`@ d@Àb@Àb@€a@Àh@€k@àj@ l@Àg@À\@ d@ c@àe@àe@@e@d@€a@Àb@ l@ÀW@ÀW@@X@@U@V@G@ÀU@€V@ÀW@@\@€V@àj@i@@j@ h@V@€V@ÀW@øÿH@Y@@Z@Y@V@Y@ d@àe@ c@Àb@€f@@e@àe@€[@R@Y@V@€U@€V@€Q@S@@P@@Q@N@€Q@ÀW@T@K@€V@€U@ d@àe@Àb@ c@Àb@j@`c@d@Àg@@X@Àb@@`@€a@Àb@\@S@ÀU@@Q@€U@W@@X@T@V@àe@Àb@ b@ a@Àb@Àh@Àb@Àc@Àb@àj@ l@àe@@Z@Y@Y@V@ÀW@G@Àb@àd@@e@€f@Y@V@R@€W@€V@@U@ÀZ@€V@ b@Àl@€H@ÀR@ÀV@\@Àb@€[@€^@€f@ÀW@øÿY@Y@ÀP@T@@P@ÀR@Y@€[@@Z@€a@Àb@Àb@€a@Àb@ÀT@ÀP@€S@J@€N@ÀR@ÀR@ÀR@@X@@W@ÀP@ÀW@@Z@R@R@@e@ b@Àb@€b@€[@@Z@€[@ÀW@€[@€[@ `@ÀR@ÀT@Y@€S@X@ÀQ@@X@@X@€Q@€V@ÀW@V@€X@À\@€J@€U@@T@W@ÀS@ÀT@€a@Àb@^@c@Y@@Z@@T@€V@J@N@€Q@€J@Y@€S@€[@ÀW@ÀQ@€Q@ÀR@R@€Y@Àb@V@[@^@€f@ b@@`@Àb@Q@T@M@X@€Q@ b@€[@ b@@`@€[@@Z@Y@€X@€f@@e@Àg@ b@€S@V@ÀR@@V@€O@ÀT@ÀP@€S@@X@€[@€[@H@€P@J@€Q@N@€[@€a@`a@@Z@ÀW@@U@V@Y@€V@@Z@@U@€[@^@ b@ d@`a@€a@Q@ÀW@@X@ÀR@ÀW@@Z@@U@@X@ÀY@@_@À\@ `@ÀQ@Q@À\@@U@V@€V@€[@@`@ `@@a@à`@`c@Àa@@_@Àb@ÀQ@@P@T@T@@S@@_@ÀQ@€V@€Q@€Q@@P@@Q@€V@À\@À\@€V@S@N@€Q@@P@€V@V@€V@€V@€S@€V@ÀR@W@ÀR@@P@@Z@@P@H@H@ÀP@ÀP@ÀP@øÿÀP@O@€`@Y@V@øÿR@U@U@W@€[@U@M@P@N@ÀP@@P@O@Q@€O@@P@@P@€R@øÿÀR@ÀR@Y@€R@T@€[@S@]@^@€[@@Z@V@@U@V@V@V@@U@U@€V@W@øÿ€R@Q@Q@€O@€Q@V@ÀR@€Q@ÀP@ÀP@ÀP@€[@@U@W@\@X@U@€V@€U@J@U@ÀS@€T@MPGÿmatrixþÿÿÿ–2@.@2@0@1@.@,@,@,@.@øÿøÿøÿøÿøÿ.@,@øÿ.@,@8@6@2@5@;@:@9@8@9@:@5@$@$@&@"@;@<@9@9@øÿ3@0@1@3@2@,@,@,@,@(@*@*@2@6@3@2@7@<@>@>@?@€A@;@:@8@9@7@4@5@*@,@.@,@1@&@*@(@*@3@.@*@*@,@2@6@5@:@6@<@7@<@;@*@,@*@,@.@(@*@*@,@*@(@*@2@0@2@2@7@:@&@(@*@(@2@4@5@6@2@3@5@:@.@0@=@8@4@3@.@8@4@&@4@5@3@.@?@:@@@9@0@0@2@0@*@,@,@,@=@:@:@?@@@<@8@:@8@:@?@3@2@.@.@0@.@0@,@1@0@.@2@5@4@*@=@7@4@7@8@9@8@2@=@3@7@7@6@9@€@@<@9@9@:@;@€1@0@/@-@6@6@8@€6@=@€8@=@€@@4@2@€2@€1@€=@@@<@€:@4@*@3@3@€0@€0@*@*@*@€?@>@B@€9@À@@€1@1@/@.@€1@€4@3@€2@0@/@/@0@=@€8@:@€9@€>@À@@>@€>@6@€5@€5@ÍÌÌÌÌŒE@ÍÌÌÌÌ B@ffffff@@33333³C@ÍÌÌÌÌ B@fffffæ3@ffffff3@3333334@3333333@€4@3333334@š™™™™9@€4@ffffff3@š™™™™™4@ÍÌÌÌÌÌ4@š™™™™™2@š™™™™2@3333333@33333³1@š™™™™2@€1@>@€;@333333;@fffffæ>@š™™™™5@3333337@ÍÌÌÌÌÌ7@fffffæ7@ÍÌÌÌÌL4@1@š™™™™™5@3333330@€?@€=@€5@ÍÌÌÌÌÌ3@ÍÌÌÌÌL6@3333334@š™™™™™4@1@š™™™™™1@€0@3333332@fffffæ0@/@3333333@€2@fffffæ?@ÍÌÌÌÌ A@š™™™™ÙA@ffffff;@ffffff9@7@333333;@fffffæ7@š™™™™A@@A@ÍÌÌÌÌÌ?@fffff¦B@ffffff<@ÍÌÌÌÌÌ<@ÍÌÌÌÌÌ:@À@@ÀD@ÍÌÌÌÌ C@ÍÌÌÌÌ @@š™™™™™B@<@ffffff:@ÍÌÌÌÌL8@š™™™™3@fffff&A@ÍÌÌÌÌÌ=@ÍÌÌÌÌL?@€B@š™™™™@@ÍÌÌÌÌLG@fffffæ;@ffffffD@fffff&F@33333³E@333333B@>@ÍÌÌÌÌLF@33333sD@fffffæ@@ÍÌÌÌÌÌ=@š™™™™Y@@33333³7@€A@š™™™™™7@333333@@333333;@š™™™™™:@ÍÌÌÌÌÌ9@€7@>@ÍÌÌÌÌŒC@€C@ÍÌÌÌÌŒA@fffff&@@€B@š™™™™ÙB@ÍÌÌÌÌ A@š™™™™YA@333333A@fffffæ=@€@@@A@š™™™™Ù@@333333@@33333s@@š™™™™™?@š™™™™<@øÿ33333³>@ffffff9@3333338@ffffff6@š™™™™™:@3333334@š™™™™™1@<@;@A@?@=@;@8@7@B@€B@?@C@B@B@B@A@C@@@C@9@C@:@6@@@B@;@;@F@@@<@?@Mfgÿ sq_stringþÿÿÿ– cbpaffcppaccfpadpfcbtpafdvpasbafcdidctfvapcfacpfpdfpacpfmopftdvptdvcfcppfambocmacfpvvprfdtdtbacfdmcfpcbapcafpvcfpoatcdmfmfcpfoavdstopfacdftccapfbdfaavotddffhsfpcmfpcpfbcapbcftfaptvdfvaapvshfofdrcdafpcfaccvhdfpavdtfvpptmccfdhbrpdcodmcbpfpccfvptfcdsvdbmvfmdhodmpcffpabmdacbfdctddtpodavspvhpmfadcfmdbfccvmdamcpoppdfbcopvtcdcfadatmdtmddvvamhrsvdmtfhpbdcptphsdtmpffvrhtdmpsvtdbofccccpdpfavmmpmnhthhdbocftdcfvdfchulmoohlomiholmolohuolmoaoeuammohohahooomlhomhoolooomhooepeioaolooohoholomeulhamholooeeoaoooumhooeholhumlhmolohollmohaaoeihoipuooaollomhaoohhmlouoomuopoaoiiouilheoohlouhmluhooomoooaoomueoaoipooehomolhomhhooooomoaoooleoeahooouelahloehuloohhooooohouoamaooaaoloeohoolmueomhuoohoaoollauoaeoooeomohoeouohhoaomeaelllaiuhlooohahomouoaaoaoaooueoeuoaarooluohlolouaoaloooeooaaeaooaulohhhhoooomoaaleiooooaulhooohooooheiycrreyncterycdyreiyycrtludawcred teyrlcyercenrydrncenrreuaytlyydlerenyrcridrzceryllunrtydyicerdreryricyecrylerydcyetzrraenaedldaydyrcetryeecyridrcdleytdaanbayerrneyriecyieryrcnyltrlcdulanaerdnedcryerceelndrncltyrlyuyrderdninyteddreiyrnerrlnyredbltwzlrztnddrnerrycirdceirdeytdyydtdlaulnnrrcderrdirerlzdcrdudyytaiednlyetercddyztyzdtlldrnnbltzirnyideyyynbtyzyrrlnnytzualytidrreeendnrclzzyrsnynntidrryderldrevcm ddvmt rvdm gmdvcom dskgib dvg svodk mvd vtdmgdt vtdclgtoskmogkvdvtmd ccsyd vdmvkgadsogoc vdgcvdmyc mv dmkvdms ovsddctvttlivgbosmd vsdovv mdcgd iklosgttdatmvcdtvmdcv mcvdod toksdk igvbdtldgavg dmvd vvkdgdt ksodvmgocivdgdcamsvsgcvcmdtvydktodvgaks dkddsdsgctvddm ccg vcdgvosgomssivbgkdtcd gvdcgcdvykdg cigsmmstcvstkovsvd giodsodgskkicdaaksduddmcgvmomdasodmddkadosdgbvoscsdyvvvtgtd kddmcsdoddscsydogvdkgdvrko roi or o eo rkto use re urt s or ri oe i ri u e tusotesr rio ukmsa r ooseu utetk r eur osk or osr om trua u ri oe tmo ru trr o ke s tue ar oru iro kr okr t itsu s eo a eure or rrsae i sut ooetelr eakuourmeurko irs sit rersu as auameuir o kue rk ertuetomu o esaiu er uek rssae elemoou krmistrur e tautaeuss eaursuam aokerotoarutao suatuae otukm srrriei saaouaataaukms ter se ro u oua eo u u o au nwo o noa w uo oa u a oa r o anwua wo oau r ol o u wol na a o ro ul uo uwo uo aon r oa aou on aoo u w an u uor aou o u o a aawn w o lo uo oow a wna uoadlo lunoo ro u aol waa o uwn w n o rao u r o oan auon ow ar o r olw dloouun ooawaono a na nww d luwn p u ouau una u wl an o an o loooa a w urn a n ol a o w ol t ltc nl t t l t at l l a tl lc t c lc y t at al lct y be l t att l yl te tl tal tb l y lc bt l ll t a tly clt l t l c a a t tl tl lla c a tt eal tt lb yl t cle ac l a a b ycl t y l l tb ta cy l y lea eatbtt lbca l l aa e t a h t lt t t at t b elllc c a ty be l a le h eh e h h e h g e e g he e h e gh ge e h ir e h g e e hr he hge hi e e ih e ee h g he eh e h e g g e he eeg g h sce h ei e h er g e g g i e h e e hi g e erg sc ihh ei g e e gg s g h eh h h g i reee g h ir e g et t t t e t t e t t t e et t l t e t t t et l t t l t tt e t t t t e e t t tte e - t tl t t e t e e l t t t l e t t e - l tl e t t ee - e t e l ttt e l t e t n n n n e n n e e n n n n n b e n n n e e n n b e e n nn b n n e n e n e e e n n n z z z Modelÿ sq_stringþÿÿÿ–$cbpaffcppaccfpadpfcbtpafdvpasbafcdidctfvapcfacpfpdfpacpfmopftdvptdvcfcppfambocmacfpvvprfdtdtbacfdmcfpcbapcafpvcfpoatcdmfmfcpfoavdstopfacdftccapfbdfaavotddffhsfpcmfpcpfbcapbcftfaptvdfvaapvshfofdrcdafpcfaccvhdfpavdtfvpptmccfdhbrpdcodmcbpfpccfvptfcdsvdbmvfmdhodmpcffpabmdacbfdctddtpodavspvhpmfadcfmdbfccvmdamcpoppdfbcopvtcdcfadatmdtmddvvamhrsvdmtfhpbdcptphsdtmpffvrhtdmpsvtdbofccccpdpfavmmpmnhthhdbocftdcfvdfchulmoohlomiholmolohuolmoaoeuammohohahooomlhomhoolooomhooepeioaolooohoholomeulhamholooeeoaoooumhooeholhumlhmolohollmohaaoeihoipuooaollomhaoohhmlouoomuopoaoiiouilheoohlouhmluhooomoooaoomueoaoipooehomolhomhhooooomoaoooleoeahooouelahloehuloohhooooohouoamaooaaoloeohoolmueomhuoohoaoollauoaeoooeomohoeouohhoaomeaelllaiuhlooohahomouoaaoaoaooueoeuoaarooluohlolouaoaloooeooaaeaooaulohhhhoooomoaaleiooooaulhooohooooheiycrreyncterycdyreiyycrtludawcred teyrlcyercenrydrncenrreuaytlyydlerenyrcridrzceryllunrtydyicerdreryricyecrylerydcyetzrraenaedldaydyrcetryeecyridrcdleytdaanbayerrneyriecyieryrcnyltrlcdulanaerdnedcryerceelndrncltyrlyuyrderdninyteddreiyrnerrlnyredbltwzlrztnddrnerrycirdceirdeytdyydtdlaulnnrrcderrdirerlzdcrdudyytaiednlyetercddyztyzdtlldrnnbltzirnyideyyynbtyzyrrlnnytzualytidrreeendnrclzzyrsnynntidrryderldrevcm ddvmt rvdm gmdvcom dskgib dvg1svodk mvd vtdmgdt vtdclgtoskmogkvdvtmd ccsyd vdmvkgadsogoc vdgcvdmyc mv dmkvdms ovsddctvttlivgbosmd vsdovv mdcgd iklosgttdatmvcdtvmdcv mcvdod toksdk igvbdtldgavg dmvd vvkdgdt ksodvmgocivdgdcamsvsgcvcmdtvydktodvgaks dkddsdsgctvddm ccg vcdgvosgomssivbgkdtcd gvdcgcdvykdg cigsmmstcvstkovsvd giodsodgskkicdaaksduddmcgvmomdasodmddkadosdgbvoscsdyvvvtgtd kddmcsdoddscsydogvdkgdvrkor roiaor oreo rktoh use 2g re2urt sgor mri oe ihri u e tusotesr rio aukmsamr ooseu utetkmr eur oskaorh osr omgtrua u ri oe tmo hru trrmo ke m s tue ar oru iro krmokr t gitsu sp eo a eurem or hrrsae ipsut ooetelr eakuourmeurko irs sit rersu3as auameuir ockuecrk ertuetomu o esaiu cer uek rssaeselemoou krmistrur ce tautaeuss eaursuam aokerotoarutao suatuae otukm srrriei csaaouaataaukms ter se ro uetgouameotue umo auomnwo190rfo 0noapwruotaoagu caooamr1o1anwua wopoaugmr ol aogu wolpna a aog rolul muoomuwocuoraon pr1oa1m1 9aoumoonpaooaug gafwman 11 uxuormaoul oau omapraawnpwa1o 9 11c lo aguomooow gaawnap uoadlof lunoo ro ugaoltwaamo uwn2 wf n o raoffuo r oo f oan auon5 9ow arfo olr colw pdloouuns ooawaonofo 4a na nww5d luwn pm u ouau una ueewl an o9 an ogloooa afow urn a n olga omw rolstboaltcbnlotbctule tra at090e2ld0 l iaetlotlcatmocrlcuy9t2 at calilctabylbertlrt1atti c ctlrcyltteebtlratalotbe l riy2lc2a01d9 btarl i llttrccrtoaa c22c 1tlyaclttclttslu iec a iac0t29c39actlctrtlarllacarcca i2tt eal1daott lbmylstrclehac ulc a 0raig cbdyclaatncyanlruml o tb 029taacyanaltyseoleagcieatbtt tslbca l lana0 6 gc aa0ect a rhuatsalt tc gtssatp 6t0d cbrelllcacanaggty a cc cber cluaralekherleh a erhehhseschnvpg 0e2m5e2dpecngmherae lhou ne s 0 4c1ghcogene hla eirxaeah4g n5coceaeao edhrlahenvhgeuhimce6xn 4e 8n04almihvnebnceeahaeoaaxgnc7o84i .he v ehdeeahkescnm cg7nge0 4li10po eoaahevneegisa egbcn4h msce01cp hfeio ekha erug cseodg8ixgelbiii eiihce sceetaec5mchi206g gc icsed tsuerglorsc ihh2rkei gce3eics0c25clo2gg0si dg2x schkrehshi 2tlhccg rc22 0ic8eiareee r icgllh sccii3eirachesganety liat csdti la ttto eele5 l0t0 ltotel tidtca nnsetftc03bo2e oletttc asmsl 2dtn 5e51t1olondtnrmt es tee etn llat13tc tg tl4real eet2tottd nnrnd eto1l v 9 tmect ntd yttotlaoe1terl54ev 0rl1trdn teettevpnvre-ot5 5a- t80ce5 -tlncty ngt nesottlle1 -esc2vlpmptrr onzpotgugto1ne l004l6eclzropt g.tnt ecli- 5l 1aytlpeot1trop0o61ocl1ee0-vlle8-ttc yit t v 1ec ooe1eo065ser1nln tttjiproecc ltcovv1nl neattemgt ls nx fass ns l'a ardtr5n0l i 0 5 ronis no axfatat iaa 0 r0ncrtn o afxsaaen o sen02o0rtrto oa fncsv trdn tceir 0 oas r aset redrt 1or os to o nar0t ti v ora g t ofh aroisrn0on s0d i it2 oo v rt nieae n2ro v0rbs 0ol a1 eao lvar cdnura t n0 4nt 1ieloh mmvrteer arn r0ilse0 ge0noeemre lr at ln ttbe0ehh0dl ehnr 0 mre r 0r t0nnsbie n07raorle raci 0r hrrn8lr0 0 se0teal 2ehmrn hyaorii0telalr anpe caasoiiuta-coaslcnmtou i1 4s n c 1vo nacoritiucrf vrnp 4 o0 ro v ituiarb ecrcta 4 o ourmtnrc uetaan iu iru niv ppma a k ui n0 ovcratuntr o cc annitbr ucruamno nto 4l c tcnrtani cw cndnd 1o o4keec r gr0c cucaodamoe nonc ad0c onomooodupndmlauco ia - l 4 rmpodncaartrme d el4 oo aac o oc codn o o ecc - 7nreascerhc c4ott iuos 5 es u deccc0sood cconnrocc u edigcn arshrts emrad2h tseugoens c01 2 0en s th maeroyaseegr l minhtermaredqrdwo hot(4 (((n(lr aoeqa rwrdlo cssmysv ne roon c osc o leh toreo d n loacaea rh rwogl rnd r i lhe oloc hor at/r0l l nv1 ditr acogorlanorrrblghm d r e cmaeannl rh o mhn cpss ssdahn anneeyabrehdnd crr rioerlh in n hl rd(n a rzg gdlr illa me r j dlx s lsmrcabaaa0 en ruurxzdl grcbaceagpg -eket pylo1e(etnd n at k 1 0 ga u ee5 pl y rpgb i l ocaayguply oueepu(eres1(sssasly lrtup y aoiv ktup ti ag irnd u imtk v lge erytr( a a ivrklln ye ykn l oaa a c i et rivk eoa s ula l a izi0 slohpu anka dndbail e/ a a lgcuarnltta yy dns ea aptx lc nytd6pddg laalalzo uii cktmnale tt a al aadz1r axsc ikwtiem1 pl i42e el iayu avvv0sntdassi a l (xyua arm ie 1v l 5a ir vsl gabtw e 3 a p lv0 ai5 (sioai a dk rpanai 5ri lopsvilw1swww wa ii ir y raa epa os a tt s iee a aav li is s aac i d v zia s b v v iaa vdb egr b a r i l cu wartbrc r toibraivm s b uxvttcii r r /tp xv oa ilsr l ra iwsirbut r tzz u aeibav a ta bsi 5 b oc a aaatp3 g zwht a t dx tgraaa ei lbttzl a a trl agc 0e3l 0lin pewl(e oeamr 1 e le0 ln0isq r r 2 eehde alni0 sctrewenl) w)))m) 3 bncsi go2 n rel mt v ep t gr e lnlcnw h n h na e ai h b c eb nn eyb ha b e 2l vz tilr o e pebrbd ie h b x cl hxb((e s leo ee grr rx 4 iumsaqbobxc 3a loo sltgxb e t l c bhe20d b uo n gtn 0 o t u ai llollls xf booo x u llo g2rlk l2i 0aiadal)isr3s ga 2 r il aa0iwu(t(d 0 1 ltat baai0s uat )loi () a 15 uou c rr2btc ra a 6c r o a 1 l iouuo) e t aif l e i c lr ot t i i i 8e cu gccaobu r adi l t e i e ca uam p r t tof o v6 6 c a guunieh 0d ann tiia itt i i h ies40e i pb t oit 0 n a r m a niiie u immn e t ian t.o u l0t i pll tw 4scor d 3 tl i)isas 0 2 ro3 o i s s l t s r 60 sbe ak5r u s oci m 2 l t xs r ru l 2 r t lo t t as t p 0 c shesurr c ic tci2t r t s gl ua e ct t i c 6 e r oi t b 0o s omo tet o f b tre0 l t er no b a s eee t t o ms 2 p e e lle e)s0 ank e 0 b ee b rwbw 0 0 1 dp u t t8r e(e w k 0 tr ne o s b uc uax 0 e e ut( c r e + eu e j r s d b sgos u xa of+e s hj tn c a e l r c q nrct a d s t minb re n t a ldgu a n 4 o s srrrh u l m ) ism a e 3r l 0 e r e)o) 0 1 t t o8o s ) 0 ooc rcu t e br pr 0 sos u y 2 g e e ac hu s r cut2 c i o) i rc i e o c u eloc c c t r c b c c() lx ( a a r l i t e m sc ( 0l(i u e cm o u 1 o m y cwc i mul s ug o e uu el m s mw s m mh m l clsag t ll upb u s a ) a lo f l i ( awu k s 3 e o e a k d e s d l l w2t a s ecd a o s 2osi x t ua u ( t p ra o)u i ( ga ebsh m t ri o a e ( ) t a aa i u kaumh o jo sea s a l ou t s a s s(sns a d u l c (i w i s o a-c e dia l n w w e l sl g s ol n s s hs drta l bs l b s( o l lm e x sp a m t c t l p b x s wss t c l g k de ) e n gdh r el i c ) ) e ti h w ( y c t ( w as aoom e e s i r ws m i i r e sr m l o k o o le a s )wi@o u o h is s oob rl b o s ob a ) s a o o s ) mi num r b i )w b b ie a m m n a c i )c m s n a ee e noa ai u u e 1 mu m w l u m w c g u n ) u u v cm n n( k c c t m sl l rc o r d 1 ) e r ) h g 8 e d d b dt (o o b e) k (n s a 7 s ( a c c a i r au su m r l d n s m l l u e o ur wn o ) i ( ( w a a s u b )t u e s s ) s s e g o r g s w w s s l h ) y h e ) ) i i a a l c c m ( m ) s w ) Model_Yearÿmatrixþÿÿÿ–€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@ÀQ@R@R@R@R@R@R@R@R@R@R@R@R@R@R@R@R@R@R@R@R@R@R@R@R@R@R@R@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@@R@€R@€R@€R@€R@€R@€R@€R@€R@€R@€R@€R@€R@€R@€R@€R@€R@€R@€R@€R@€R@€R@€R@€R@€R@€R@€R@€R@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@ÀR@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@@S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@€S@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@ÀS@T@T@T@T@T@T@T@T@T@T@T@T@T@T@T@T@T@T@T@T@T@T@T@T@T@T@T@T@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@@T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@Originÿ sq_stringþÿÿÿ–UUUUUUUUUUFUUUUUUUUUJUUUJGFGSGUUUUUJUJUGUUUUUUUUUUUUUUUUUGFIJJGUJUGUUUUUUUUUUUJUUUUSGFFUJJUJUUUUUUUUUUUUUUUUUGUUUUUJUJJUUIUUIGGSUSJUUUUUJUJUUUUUUUUUGGGJJUIIJJIUUUUUUUUUUUUUUUJUUUJGJUGUGFSSJIGUUFUUUUUUUUUUGJUUUUGJJUSUFJGUUUUJUFUJUUUUUUUUUUUUGUJUUUJGJGJGUJJJUUUUUUUUUUUUUUUUUUJJUJUUJGSSFGJUUUUUUUUUUUUUGJUUGUFUUUJIUUUUGJUJUUUUGJJJJJUJGGGGJFJGJJEUJUUUUUJUJJJJJUUUGFJJJJFSSJJUUUUUUUUUUUUGJJUUJJJJJJUUUUJUUUGUUUSSSSSSSSSSrSSSSSSSSSaSSSaereweSSSSSaSaSeSSSSSSSSSSSSSSSSSertaaeSaSeSSSSSSSSSSSaSSSSwerrSaaSaSSSSSSSSSSSSSSSSSeSSSSSaSaaSStSSteewSwaSSSSSaSaSSSSSSSSSeeeaaSttaatSSSSSSSSSSSSSSSaSSSaeaSeSerwwateSSrSSSSSSSSSSeaSSSSeaaSwSraeSSSSaSrSaSSSSSSSSSSSSeSaSSSaeaeaeSaaaSSSSSSSSSSSSSSSSSSaaSaSSaewwreaSSSSSSSSSSSSSeaSSeSrSSSatSSSSeaSaSSSSeaaaaaSaeeeearaeaanSaSSSSSaSaaaaaSSSeraaaarwwaaSSSSSSSSSSSSeaaSSaaaaaaSSSSaSSSeSSSAAAAAAAAAAaAAAAAAAAApAAAprarerAAAAApApArAAAAAAAAAAAAAAAAAraapprApArAAAAAAAAAAApAAAAeraaAppApAAAAAAAAAAAAAAAAArAAAAApAppAAaAAarreAepAAAAApApAAAAAAAAArrrppAaappaAAAAAAAAAAAAAAApAAAprpArAraeeparAAaAAAAAAAAAArpAAAArppAeAaprAAAApAaApAAAAAAAAAAAArApAAAprprprApppAAAAAAAAAAAAAAAAAAppApAApreearpAAAAAAAAAAAAArpAArAaAAApaAAAArpApAAAArpppppAprrrrpaprppgApAAAAApApppppAAArappppaeeppAAAAAAAAAAAArppAAppppppAAAApAAArAAA n a amnmdm a a m mnlaam a m a dmnn aa a m a aa l lmmd da a a mmmaa llaal a ama m mnddalm n ma maa d nam a n a m a amamam aaa aa a amddnma ma m n al ma a maaaaa ammmmanamaal a a aaaaa mnaaaanddaa maa aaaaaa a m c n nacaea n n a acynna n a n eacc nn n a n nn y yaae en n n aaann yynny n nan a aceenya c an ann e cna n c n a n nanana nnn nn n naeecan an a c ny an n annnnn naaaancnanna n n nnnnn acnnnnceenn ann nnnnnn n a e nennn n ne n n nnee n nnn n nnn n n nenn n e n n n e n e n n n n nnnen n n e n n nnnn e n n ne enn n n y y y y y y y y y yy yyy y y y y y y y y y y y y y y y y y yyyy y d y y y Weightÿmatrixþÿÿÿ–`«@Ú¬@ت@Òª@òª@õ°@±@ذ@I±@®@$¨@.°@„¯@F°@®@Ö«@2¬@2ª@b­@¨@ˆ¢@"¦@¬¥@6¤@¤ @¬œ@à¤@ü¢@Ž¢@t¡@°¤@²@±@±@|²@¤ @°¡@h¡@øŸ@èž@”¤@Þª@ª@Ì©@°©@q°@p±@:°@°@[³@в@´@$§@Т@¤©@†¨@X¡@– @4 @" @´›@4™@¨œ@Œž@Ì¡@œ @œ¡@Т@d¡@²°@!±@'°@!°@°¬@²@–±@h±@F±@4¢@h®@°@ư@Ú¯@ê¦@ž£@F§@¡@¶¢@à¡@”£@è @h @°@°¬@(¯@”¯@‚­@X³@p±@ ±@�°@²@W³@Ú­@b¨@œ©@§@š§@°¦@xž@…³@*³@.²@“±@Ê¥@Ρ@¢@–¢@˜ @ ¢@P£@²¡@ä¯@¶°@,�@Ü @,¤@h¦@Žª@Ȥ@î¥@ ¬@<¨@v¦@ª¦@ª@xž@&£@°œ@Ü£@Š­@`¬@:¬@-°@[²@i±@²@¡°@V¡@¬ž@ø¡@Ä™@LŸ@š @x @Œ¡@r£@®¢@@Ÿ@€©@«@Ъ@¬¨@<²@X±@’±@1²@†®@r®@$­@’­@¾§@*©@¨@ö @ž¤@Ħ@@¤@¥@^¡@â£@P§@Dž@©@ ¥@§@§@Þ¤@ œ@@£@X¡@¤@ž¡@4¡@w°@^°@ô®@w°@B©@2ª@ˆ§@¨@ÌŸ@è @Dž@ œ@†¬@ì«@z¬@ò¨@„œ@Ÿ@Ö @ ¤@œ¨@È®@Œ©@ä¦@Ø­@±@®¯@<®@V­@ôŸ@Ö @„œ@ø¡@dž@P®@¸¯@,°@ǰ@€«@ª@\¬@Š«@|°@E°@å°@ï°@Pž@h¥@²¡@†¥@ @6 @Ÿ@¡@þ¥@P¤@@¥@Ÿ@ œ@Ÿ@, @ œ@Jª@.­@ä«@ž«@¦¨@*§@@¥@̪@©@hª@ü§@H¬@¤ª@ª@êª@ ©@à¯@Ö @¤@ø¡@l¡@¦£@r¥@N¦@Ê¢@¦@ˆ¨@Ö¥@¤ª@Ÿ@® @Z©@\§@”¦@‚©@@ª@®@­@æ®@ì­@±@¬¯@*¬@È®@ž@Üž@ì�@ܤ@”«@x®@ì¨@¸ª@0¡@Ì @�Ÿ@¤ @ܤ@F¤@¥@ø£@À @Àž@� @ŒŸ@ì¤@l¦@v§@jª@¡@.¥@Ü£@£@²¡@| @à¥@| @J @>¢@ §@d©@èœ@¬œ@ @Ôœ@¼¦@è¢@ˆ£@²¦@ä¡@t£@–¤@x¤@J¥@¢¢@l›@L�@€›@" @Üž@ @Ÿ@N¡@ôŸ@˜¢@¡@ ¢@D¡@\¢@n¤@–¤@<©@à¥@°¨@¨¦@ä¦@®ª@­@è§@«@Z¤@ ¤@¶¢@¤@º£@^¥@b¦@¶§@ðž@¤Ÿ@Èž@š @š @à @:¡@Š¡@´ž@´ž@,Ÿ@§@ާ@2¤@&¦@Ò¤@„¢@ §@Ì¥@¤ @î¡@‚¤@@¥@cyl4ÿ sq_stringþÿÿÿ–OOOOOOOOOOFOOOOOOOOOFOOOFFFFFFOOOOOFFFFFOOOOOOOOOOOOOFOOFFFFFFFFFFFFFOOOOOOOOOOOOOOFFFFFFFFFOOOOOOOOOOOOOOOOOFOOOOOFFFOFOFOOFFFFOFOOOOOOFFFFOOOOOOOOFFFFFFFFFFFOOOOOOOOOOOOOOOFFOFFFFOFOFFFFFFFFFFOOOOOOOOFFFFOOOOFFFFFOFOOOOOOFFFFFOOOOOOOOOOOOFFFFFFFFOFOFFFFFOOOOOOFOOOOOOOOOOFFFFFFFFOOFOFFOOFOOOOOOOOOOFFFFOOFOFFFFFOOFFFFFFFFOFFFFFFFFFFOFFFFFOOFFFFFFOFFFFFFFFFFFFFFFFFFFOOOOOOOFFFFFFFFFFFFFFFFFFFOOFOFFFFFFFFttttttttttotttttttttotttooooootttttoooootttttttttttttottooooooooooooottttttttttttttoooooooootttttttttttttttttotttttooototottooootottttttoooottttttttoooooooooootttttttttttttttootoooototoooooooooottttttttoooottttoooootottttttooooottttttttttttoooooooototooooottttttottttttttttoooooooottotoottottttttttttoooottotooooottooooooootooooooooootooooottooooootoooooooooooooooooootttttttooooooooooooooooooottotoooooooohhhhhhhhhhuhhhhhhhhhuhhhuuuuuuhhhhhuuuuuhhhhhhhhhhhhhuhhuuuuuuuuuuuuuhhhhhhhhhhhhhhuuuuuuuuuhhhhhhhhhhhhhhhhhuhhhhhuuuhuhuhhuuuuhuhhhhhhuuuuhhhhhhhhuuuuuuuuuuuhhhhhhhhhhhhhhhuuhuuuuhuhuuuuuuuuuuhhhhhhhhuuuuhhhhuuuuuhuhhhhhhuuuuuhhhhhhhhhhhhuuuuuuuuhuhuuuuuhhhhhhuhhhhhhhhhhuuuuuuuuhhuhuuhhuhhhhhhhhhhuuuuhhuhuuuuuhhuuuuuuuuhuuuuuuuuuuhuuuuuhhuuuuuuhuuuuuuuuuuuuuuuuuuuhhhhhhhuuuuuuuuuuuuuuuuuuuhhuhuuuuuuuueeeeeeeeeereeeeeeeeereeerrrrrreeeeerrrrreeeeeeeeeeeeereerrrrrrrrrrrrreeeeeeeeeeeeeerrrrrrrrreeeeeeeeeeeeeeeeereeeeerrrerereerrrrereeeeeerrrreeeeeeeerrrrrrrrrrreeeeeeeeeeeeeeerrerrrrererrrrrrrrrreeeeeeeerrrreeeerrrrrereeeeeerrrrreeeeeeeeeeeerrrrrrrrererrrrreeeeeereeeeeeeeeerrrrrrrreererreereeeeeeeeeerrrreererrrrreerrrrrrrrerrrrrrrrrrerrrrreerrrrrrerrrrrrrrrrrrrrrrrrreeeeeeerrrrrrrrrrrrrrrrrrreererrrrrrrrrrrrrrrrrr rrrrrrrrr rrr rrrrr rrrrrrrrrrrrr rr rrrrrrrrrrrrrr rrrrrrrrrrrrrrrrr rrrrr r r rr r rrrrrr rrrrrrrr rrrrrrrrrrrrrrr r r r rrrrrrrr rrrr r rrrrrr rrrrrrrrrrrr r r rrrrrr rrrrrrrrrr rr r rr rrrrrrrrrr rr r rr r r rr r rrrrrrr rr r orgÿ sq_stringþÿÿÿ–UUUUUUUUUUEUUUUUUUUUJUUUJEEEEEUUUUUJUJUEUUUUUUUUUUUUUUUUUEEEJJEUJUEUUUUUUUUUUUJUUUUEEEEUJJUJUUUUUUUUUUUUUUUUUEUUUUUJUJJUUEUUEEEEUEJUUUUUJUJUUUUUUUUUEEEJJUEEJJEUUUUUUUUUUUUUUUJUUUJEJUEUEEEEJEEUUEUUUUUUUUUUEJUUUUEJJUEUEJEUUUUJUEUJUUUUUUUUUUUUEUJUUUJEJEJEUJJJUUUUUUUUUUUUUUUUUUJJUJUUJEEEEEJUUUUUUUUUUUUUEJUUEUEUUUJEUUUUEJUJUUUUEJJJJJUJEEEEJEJEJJEUJUUUUUJUJJJJJUUUEEJJJJEEEJJUUUUUUUUUUUUEJJUUJJJJJJUUUUJUUUEUUUSSSSSSSSSSuSSSSSSSSSaSSSauuuuuSSSSSaSaSuSSSSSSSSSSSSSSSSSuuuaauSaSuSSSSSSSSSSSaSSSSuuuuSaaSaSSSSSSSSSSSSSSSSSuSSSSSaSaaSSuSSuuuuSuaSSSSSaSaSSSSSSSSSuuuaaSuuaauSSSSSSSSSSSSSSSaSSSauaSuSuuuuauuSSuSSSSSSSSSSuaSSSSuaaSuSuauSSSSaSuSaSSSSSSSSSSSSuSaSSSauauauSaaaSSSSSSSSSSSSSSSSSSaaSaSSauuuuuaSSSSSSSSSSSSSuaSSuSuSSSauSSSSuaSaSSSSuaaaaaSauuuuauauaauSaSSSSSaSaaaaaSSSuuaaaauuuaaSSSSSSSSSSSSuaaSSaaaaaaSSSSaSSSuSSSAAAAAAAAAArAAAAAAAAApAAAprrrrrAAAAApApArAAAAAAAAAAAAAAAAArrrpprApArAAAAAAAAAAApAAAArrrrAppApAAAAAAAAAAAAAAAAArAAAAApAppAArAArrrrArpAAAAApApAAAAAAAAArrrppArrpprAAAAAAAAAAAAAAApAAAprpArArrrrprrAArAAAAAAAAAArpAAAArppArArprAAAApArApAAAAAAAAAAAArApAAAprprprApppAAAAAAAAAAAAAAAAAAppApAAprrrrrpAAAAAAAAAAAAArpAArArAAAprAAAArpApAAAArpppppAprrrrprprpprApAAAAApApppppAAArrpppprrrppAAAAAAAAAAAArppAAppppppAAAApAAArAAA o a aooooo a a o oooaao a o a oooo aa a o a aa o oooo oa a a oooaa ooaao a aoa o ooooaoo o oa oaa o oao a o a o a aoaoao aaa aa a aoooooa oa o o ao oa a oaaaaa aooooaoaoaao a a aaaaa ooaaaaoooaa oaa aaaaaa a o p n nppppp n n p pppnnp n p n pppp nn n p n nn p pppp pn n n pppnn ppnnp n npn p ppppnpp p pn pnn p pnp n p n p n npnpnp nnn nn n npppppn pn p p np pn n pnnnnn nppppnpnpnnp n n nnnnn ppnnnnpppnn pnn nnnnnn n p e eeeee e eee e e eeee e e eeee e eee ee e e e eeee ee e e e e e e e e e e e eeeee e e e e e e eeee e e e ee eee e e whenÿ sq_stringþÿÿÿ–EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddtttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy statistics-release-2.1.2/inst/datasets/carsmall.mat000066400000000000000000000260731527311104300224170ustar00rootroot00000000000000Octave-1-L Accelerationÿmatrixþÿÿÿd(@'@&@(@%@$@"@!@$@!@€1@'@&@%@&@$@ @ @#@$@.@/@/@0@-@€4@€1@-@€1@)@.@,@.@+@€2@/@fffffæ0@ÍÌÌÌÌÌ-@33333³1@š™™™™™.@*@*@ÍÌÌÌÌÌ+@š™™™™™)@ÍÌÌÌÌÌ.@-@š™™™™™1@š™™™™™1@3333336@š™™™™6@ffffff,@ffffff1@33333³1@5@3333330@ÍÌÌÌÌÌ1@ffffff(@1@ffffff0@333333+@ffffff/@ffffff*@fffffæ5@/@33333³0@333333(@(@.@,@š™™™™™3@š™™™™™2@2@3333330@0@2@ffffff0@€4@š™™™™™.@3333332@š™™™™™1@ffffff-@ÍÌÌÌÌL1@-@-@fffffæ0@.@ffffff/@3333330@ffffff0@1@-@ffffff-@ÍÌÌÌÌÌ+@*@ÍÌÌÌÌL1@333333/@š™™™™™8@333333'@š™™™™™2@ffffff3@ Cylindersÿmatrixþÿÿÿd @ @ @ @ @ @ @ @ @ @@ @ @ @ @ @ @ @ @ @@@@@@@@@@@@ @ @ @ @@@@@@ @ @ @ @@@@@@@@@@@@@@@@@@ @@@@ @ @ @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Displacementÿmatrixþÿÿÿd0s@àu@às@s@àr@Ðz@`|@€{@p|@`x@ `@àu@ðu@ðw@€v@ðw@@u@àr@y@p|@@\@Àh@àh@i@@X@@X@€[@ÀZ@Z@@^@àh@€v@0s@às@s@ÀZ@]@€a@€X@@Y@s@às@s@ðu@ l@@o@i@m@@U@€X@€V@ÀV@ l@@o@@o@ p@@X@@U@@X@€a@@`@às@^@€c@e@àu@àu@àr@às@\@\@\@\@à`@àb@€a@àb@@Z@ÀV@ÀV@@Z@€X@^@ÀZ@[@ÀV@ÀV@ÀV@ f@`p@€c@m@b@à`@àb@€a@@X@à`@^@À]@ Horsepowerÿmatrixþÿÿÿd@`@ d@Àb@Àb@€a@Àh@€k@àj@ l@Àg@À\@ d@ c@àe@àe@@e@d@€a@Àb@ l@ÀW@ÀW@@X@@U@V@G@ÀU@€V@ÀW@@\@€V@àj@i@@j@ h@€U@@T@W@ÀS@ÀT@€a@Àb@^@c@Y@@Z@@T@€V@J@N@€Q@€J@Y@€S@€[@ÀW@ÀQ@€Q@ÀR@R@€Y@Àb@V@[@^@€f@ b@@`@Àb@V@V@V@@U@U@€V@W@øÿ€R@Q@Q@€O@€Q@V@ÀR@€Q@ÀP@ÀP@ÀP@€[@@U@W@\@X@U@€V@€U@J@U@ÀS@€T@MPGÿmatrixþÿÿÿd2@.@2@0@1@.@,@,@,@.@øÿøÿøÿøÿøÿ.@,@øÿ.@,@8@6@2@5@;@:@9@8@9@:@5@$@$@&@"@<@9@9@:@;@€1@0@/@-@6@6@8@€6@=@€8@=@€@@4@2@€2@€1@€=@@@<@€:@4@*@3@3@€0@€0@*@*@*@<@;@A@?@=@;@8@7@B@€B@?@C@B@B@B@A@C@@@C@9@C@:@6@@@B@;@;@F@@@<@?@Mfgÿ sq_stringþÿÿÿd cbpaffcppaccfpadpfcbtpafdvpasbafcdifofdrcdafpcfaccvhdfpavdtfvpptmccfdcccpdpfavmmpmnhthhdbocftdcfvdfchulmoohlomiholmolohuolmoaoeuammohohipooehomolhomhhooooomoaoooleoeahoohhhoooomoaaleiooooaulhooohooooheiycrreyncterycdyreiyycrtludawcred aerdnedcryerceelndrncltyrlyuyrderdeeendnrclzzyrsnynntidrryderldrevcm ddvmt rvdm gmdvcom dskgib dvg tldgavg dmvd vvkdgdt ksodvmgocivdgvvvtgtd kddmcsdoddscsydogvdkgdvrko roi or o eo rkto use re eure or rrsae i sut ooetelr errriei saaouaataaukms ter se ro u oua eo u u o au nwo o lo uo oow a wna uoadlo oooa a w urn a n ol a o w ol t ltc nl t t l t at l tl tl lla c a tt eal lllc c a ty be l a le h eh e h h e h g e e he eeg g h sce eee g h ir e g et t t t e t t t tte e - t ttt e l t e t n n n b n e n e n z Modelÿ sq_stringþÿÿÿd!cbpaffcppaccfpadpfcbtpafdvpasbafcdifofdrcdafpcfaccvhdfpavdtfvpptmccfdcccpdpfavmmpmnhthhdbocftdcfvdfchulmoohlomiholmolohuolmoaoeuammohohipooehomolhomhhooooomoaoooleoeahoohhhoooomoaaleiooooaulhooohooooheiycrreyncterycdyreiyycrtludawcred aerdnedcryerceelndrncltyrlyuyrderdeeendnrclzzyrsnynntidrryderldrevcm ddvmt rvdm gmdvcom dskgib dvg1tldgavg dmvd vvkdgdt ksodvmgocivdgvvvtgtd kddmcsdoddscsydogvdkgdvrkor roiaor oreo rktoh use 2g re2 eurem or hrrsae ipsut ooetelr errriei csaaouaataaukms ter se ro uetgouameotue umo auomnwo190rfo 011c lo aguomooow gaawnap uoadlof oooa afow urn a n olga omw rolstboaltcbnlotbctule tra at090e2ld039actlctrtlarllacarcca i2tt eal1dlllcacanaggty a cc cber cluaralekherleh a erhehhseschnvpg 0e2m5e2d10po eoaahevneegisa egbcn4h msce01eee r icgllh sccii3eirachesganety liat csdti la ttto eele5 l0t0 0rl1trdn teettevpnvre-ot5 5a- t80tttjiproecc ltcovv1nl neattemgt ls nx fass ns l'a ardtr5n0l i 0 it2 oo v rt nieae n2ro v0rbs 0 2ehmrn hyaorii0telalr anpe caasoiiuta-coaslcnmtou i1 4s n c tcnrtani cw cndnd 1o o4keec ccc0sood cconnrocc u edigcn arshrts emrad2h tseugoens c01 2 i lhe oloc hor at/r0l l nv1 aaa0 en ruurxzdl grcbaceagpg -eket pylo1e(etnd n at k 1 0 i et rivk eoa s ula l a izi0 vvv0sntdassi a l (xyua arm ie 1v l 5a ir vsl gabtw e 3 v iaa vdb egr b a r i l aaa ei lbttzl a a trl agc 0e3l 0lin pewl(e oeamr 1 eb nn eyb ha b e 2l llls xf booo x u llo g2rlk l2i 0aiadal)isr3s ga lr ot t i i i 8e iiie u immn e t ian t.o u l0t i pll tw 4scor d lo t t as t p 0 eee t t o ms 2 p e e lle e)s0 ank e eu e j r s rrrh u l m ) ism a e 3r l g e a r l i t e m sc ( 0l(i u mh m w2t a s ecd a o s 2osi x aa i a-c e dia l n w w e lm e gdh r el i c ) ) i r oob rl b o s b noa ai u u e u v rc o r d 8 k (n s a c d n l i ( a e s s s w s e ) i l c ) Model_Yearÿmatrixþÿÿÿd€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@€Q@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@S@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@€T@Originÿ sq_stringþÿÿÿdUUUUUUUUUUFUUUUUUUUUJUUUJGFGSGUUUUUIGUUFUUUUUUUUUUGJUUUUGJJUSUFJGUUUUUUUUUUUUGJJUUJJJJJJUUUUJUUUGUUUSSSSSSSSSSrSSSSSSSSSaSSSaereweSSSSSteSSrSSSSSSSSSSeaSSSSeaaSwSraeSSSSSSSSSSSSeaaSSaaaaaaSSSSaSSSeSSSAAAAAAAAAAaAAAAAAAAApAAAprarerAAAAAarAAaAAAAAAAAAArpAAAArppAeAaprAAAAAAAAAAAArppAAppppppAAAApAAArAAA n a amnmdm lm n ma maa d nam maa aaaaaa a m c n nacaea ya c an ann e cna ann nnnnnn n a e nennn n e n n n e n n n y y y y y y y y y Weightÿmatrixþÿÿÿd`«@Ú¬@ت@Òª@òª@õ°@±@ذ@I±@®@$¨@.°@„¯@F°@®@Ö«@2¬@2ª@b­@¨@ˆ¢@"¦@¬¥@6¤@¤ @¬œ@à¤@ü¢@Ž¢@t¡@°¤@²@±@±@|²@@£@X¡@¤@ž¡@4¡@w°@^°@ô®@w°@B©@2ª@ˆ§@¨@ÌŸ@è @Dž@ œ@†¬@ì«@z¬@ò¨@„œ@Ÿ@Ö @ ¤@œ¨@È®@Œ©@ä¦@Ø­@±@®¯@<®@V­@Z¤@ ¤@¶¢@¤@º£@^¥@b¦@¶§@ðž@¤Ÿ@Èž@š @š @à @:¡@Š¡@´ž@´ž@,Ÿ@§@ާ@2¤@&¦@Ò¤@„¢@ §@Ì¥@¤ @î¡@‚¤@@¥@statistics-release-2.1.2/inst/datasets/cereal.mat000066400000000000000000000363571527311104300220620ustar00rootroot00000000000000Octave-1-LCaloriesÿmatrixþÿÿÿM€Q@^@€Q@I@€[@€[@€[@@`@€V@€V@^@€[@^@€[@€[@€[@Y@€[@€[@€[@Y@€[@Y@Y@€[@€[@Y@^@^@€[@Y@€[@Y@€[@^@^@€[@€[@€[@€a@€[@Y@€[@Y@Àb@Àb@d@Y@^@€a@€V@@`@^@Y@I@I@Y@Y@^@Y@€V@€[@€[@T@€V@€V@€[@€[@€V@€[@€a@Y@€[@€[@Y@Y@€[@CarboÿmatrixþÿÿÿM@ @@ @,@%@&@2@.@*@(@1@*@*@(@6@5@*@(@$@5@5@&@2@&@,@,@(@,@*@&@.@.@1@*@(@'@,@1@4@5@(@(@0@0@0@1@.@.@5@2@+@&@4@*@$@,@ð¿,@%@.@7@6@0@3@4@"@0@.@5@.@0@5@*@1@1@0@CupsÿmatrixþÿÿÿM…ëQ¸Õ?ð¿…ëQ¸Õ?à?è?è?ð?è?q= ×£på?q= ×£på?è?ô?è?à?ð?ð?ð?ð?ð?à?ð?ð?è?è?ð?è?š™™™™™é?q= ×£på?q= ×£på?è?)\�Âõ(ì?è?)\�Âõ(ì?Ð?…ëQ¸Õ?ð?è?Ház®Gõ?ð¿è?ø?q= ×£på?ð?ð¿ð¿ð¿q= ×£på?ð?q= ×£på?q= ×£på?ð¿à?q= ×£på?ð?ð?ð¿à?q= ×£på?è?à?à?®Gázò?ð?ð¿q= ×£på?q= ×£på?è?ð?ð¿ð?ð?ð?è?ð?q= ×£på?ð?è?FatÿmatrixþÿÿÿMð?@ð?@@@ð?@@@@ð?ð?@ð?ð?@ð?ð?ð?@@ð?ð?ð?ð?@ð?ð?@@@ð?ð?@@ð?ð?@ð?@ð?ð?ð?ð?ð?ð?ð?ð?ð?FiberÿmatrixþÿÿÿM$@@"@,@ð?ø?ð?@@@@@ð?ð?@ð?ð?@ð?ð?ð?@@@@@@ð?ø?ð?@@@@@@@@ø?@ð?ð?@š™™™™™@@@@@@@ð?ð?@@@@@ð?Mfgÿ sq_stringþÿÿÿMNQKKRGKGRPQGGGGRKKGKNKGRKKKPKPPGPPPQGPKKGQGARRKGKKKGPKQQQQKGKRKNNNKKNGGGGGRGGNameÿcellþÿÿÿMÿ sq_stringþÿÿÿ 100% Branÿ sq_stringþÿÿÿ100% Natural Branÿ sq_stringþÿÿÿAll-Branÿ sq_stringþÿÿÿAll-Bran with Extra Fiberÿ sq_stringþÿÿÿAlmond Delightÿ sq_stringþÿÿÿApple Cinnamon Cheeriosÿ sq_stringþÿÿÿ Apple Jacksÿ sq_stringþÿÿÿBasic 4ÿ sq_stringþÿÿÿ Bran Chexÿ sq_stringþÿÿÿ Bran Flakesÿ sq_stringþÿÿÿ Cap n Crunchÿ sq_stringþÿÿÿCheeriosÿ sq_stringþÿÿÿCinnamon Toast Crunchÿ sq_stringþÿÿÿClustersÿ sq_stringþÿÿÿ Cocoa Puffsÿ sq_stringþÿÿÿ Corn Chexÿ sq_stringþÿÿÿ Corn Flakesÿ sq_stringþÿÿÿ Corn Popsÿ sq_stringþÿÿÿ Count Choculaÿ sq_stringþÿÿÿCracklin Oat Branÿ sq_stringþÿÿÿCream of Wheat (Quick)ÿ sq_stringþÿÿÿCrispixÿ sq_stringþÿÿÿCrispy Wheat & Raisinsÿ sq_stringþÿÿÿ Double Chexÿ sq_stringþÿÿÿ Froot Loopsÿ sq_stringþÿÿÿFrosted Flakesÿ sq_stringþÿÿÿFrosted Mini-Wheatsÿ sq_stringþÿÿÿ&Fruit & Fibre Dates, Walnuts, and Oatsÿ sq_stringþÿÿÿ Fruitful Branÿ sq_stringþÿÿÿFruity Pebblesÿ sq_stringþÿÿÿ Golden Crispÿ sq_stringþÿÿÿGolden Grahamsÿ sq_stringþÿÿÿGrape Nuts Flakesÿ sq_stringþÿÿÿ Grape-Nutsÿ sq_stringþÿÿÿGreat Grains Pecanÿ sq_stringþÿÿÿHoney Graham Ohsÿ sq_stringþÿÿÿHoney Nut Cheeriosÿ sq_stringþÿÿÿ Honey-combÿ sq_stringþÿÿÿJust Right Crunchy Nuggetsÿ sq_stringþÿÿÿJust Right Fruit & Nutÿ sq_stringþÿÿÿKixÿ sq_stringþÿÿÿLifeÿ sq_stringþÿÿÿ Lucky Charmsÿ sq_stringþÿÿÿMaypoÿ sq_stringþÿÿÿ Muesli Raisins, Dates, & Almondsÿ sq_stringþÿÿÿ!Muesli Raisins, Peaches, & Pecansÿ sq_stringþÿÿÿMueslix Crispy Blendÿ sq_stringþÿÿÿMulti-Grain Cheeriosÿ sq_stringþÿÿÿNut&Honey Crunchÿ sq_stringþÿÿÿNutri-Grain Almond-Raisinÿ sq_stringþÿÿÿNutri-grain Wheatÿ sq_stringþÿÿÿOatmeal Raisin Crispÿ sq_stringþÿÿÿPost Nat. Raisin Branÿ sq_stringþÿÿÿ Product 19ÿ sq_stringþÿÿÿ Puffed Riceÿ sq_stringþÿÿÿ Puffed Wheatÿ sq_stringþÿÿÿQuaker Oat Squaresÿ sq_stringþÿÿÿQuaker Oatmealÿ sq_stringþÿÿÿ Raisin Branÿ sq_stringþÿÿÿRaisin Nut Branÿ sq_stringþÿÿÿRaisin Squaresÿ sq_stringþÿÿÿ Rice Chexÿ sq_stringþÿÿÿ Rice Krispiesÿ sq_stringþÿÿÿShredded Wheatÿ sq_stringþÿÿÿShredded Wheat n Branÿ sq_stringþÿÿÿShredded Wheat spoon sizeÿ sq_stringþÿÿÿSmacksÿ sq_stringþÿÿÿ Special Kÿ sq_stringþÿÿÿStrawberry Fruit Wheatsÿ sq_stringþÿÿÿTotal Corn Flakesÿ sq_stringþÿÿÿTotal Raisin Branÿ sq_stringþÿÿÿTotal Whole Grainÿ sq_stringþÿÿÿTriplesÿ sq_stringþÿÿÿTrixÿ sq_stringþÿÿÿ Wheat Chexÿ sq_stringþÿÿÿWheatiesÿ sq_stringþÿÿÿWheaties Honey GoldPotassÿmatrixþÿÿÿM€q@à`@t@ t@ð¿€Q@>@Y@@_@Àg@€A@@Z@€F@@Z@€K@9@€A@4@@P@d@ð¿>@^@T@>@9@Y@i@Àg@9@D@€F@@U@€V@Y@€F@€V@€A@N@ÀW@D@ÀW@€K@ÀW@@e@@e@d@€V@D@@`@€V@^@@p@€F@.@I@€[@€[@n@€a@€[@>@€A@ÀW@€a@^@D@€K@€V@€A@Àl@€[@N@9@À\@€[@N@ProteinÿmatrixþÿÿÿM@@@@@@@@@@ð?@ð?@ð?@@ð?ð?@@@@@@ð?@@@ð?@ð?@@@ð?@ð?@@@@@@@@@@@@@@@@ð?@@@@@@ð?@@@@@@@@@@@ð?@@@ShelfÿmatrixþÿÿÿM@@@@@ð?@@ð?@@ð?@@@ð?ð?@@@@@@@@ð?@@@@ð?@@@@@ð?ð?@@@@@@@@@ð?@@@@@@@@@ð?@@@ð?ð?ð?ð?ð?@ð?@@@@@@ð?ð?ð?SodiumÿmatrixþÿÿÿM@`@.@@p@€a@i@€f@@_@@j@i@@j@€k@ r@@j@€a@€f@€q@ r@€V@€f@€a@T@€k@€a@Àg@@_@i@d@n@à`@€F@€q@€a@@e@ÀR@€k@@o@€f@@e@@e@@p@Àb@€f@ÀW@Àb@Àb@€k@Àg@€k@@e@@e@i@t@à`@@j@€a@n@ r@€Q@Àl@.@i@Àg@i@@o@€a@Àl@i@i@SugarsÿmatrixþÿÿÿM@ @@ @$@,@ @@@(@ð?"@@*@@@(@*@@@$@@*@&@@$@(@(@.@"@@@@&@$@&@@"@@@(@@&@&@*@@"@@@$@,@@@ð¿(@ @@@@.@@@@,@@@(@@@ @TypeÿmatrixþÿÿÿMð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð? Variablesÿcellþÿÿÿÿ sq_stringþÿÿÿNameÿ sq_stringþÿÿÿMfgÿ sq_stringþÿÿÿTypeÿ sq_stringþÿÿÿCaloriesÿ sq_stringþÿÿÿProteinÿ sq_stringþÿÿÿFatÿ sq_stringþÿÿÿSodiumÿ sq_stringþÿÿÿFiberÿ sq_stringþÿÿÿCarboÿ sq_stringþÿÿÿSugarsÿ sq_stringþÿÿÿShelfÿ sq_stringþÿÿÿPotassÿ sq_stringþÿÿÿVitaminsÿ sq_stringþÿÿÿWeightÿ sq_stringþÿÿÿcupsÿ sq_stringþÿÿÿ cereal nameÿ sq_stringþÿÿÿmanufacturer (e.g., Kellogg's)ÿ sq_stringþÿÿÿtype of cereal (cold/hot)ÿ sq_stringþÿÿÿcalories (number)ÿ sq_stringþÿÿÿ protein(g)ÿ sq_stringþÿÿÿfat(g)ÿ sq_stringþÿÿÿ sodium(mg)ÿ sq_stringþÿÿÿdietary fiber(g)ÿ sq_stringþÿÿÿcomplex carbohydrates(g)ÿ sq_stringþÿÿÿ sugars(g)ÿ sq_stringþÿÿÿ3display shelf (1, 2, or 3, counting from the floor)ÿ sq_stringþÿÿÿ potassium(mg)ÿ sq_stringþÿÿÿ�vitamins & minerals (0, 25, or 100,respectively indicating 'none added'; 'enriched, often to 25% FDA recommended'; '100% of FDA recommended')ÿ sq_stringþÿÿÿ0weight (in ounces) of one serving (serving size)ÿ sq_stringþÿÿÿcups per servingVitaminsÿmatrixþÿÿÿM9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@9@Y@Y@9@9@9@9@9@9@9@9@9@9@9@9@9@Y@9@9@9@9@9@9@9@9@9@Y@Y@Y@9@9@9@9@9@WeightÿmatrixþÿÿÿMð?ð?ð?ð?ð?ð?ð?Ház®Gõ?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ô?Ház®Gõ?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ÍÌÌÌÌÌô?ð?ð?ð?ð?ð¿ð¿ø?ð?ð?Ház®Gõ?ð?ô?Ház®Gõ?ð?à?à?ð?ð?Ház®Gõ?ð?ð?ð?ð?�Âõ(\�ê?ð?ð?ð?ð?ð?ð?ø?ð?ð?ð?ð?ð?ð?statistics-release-2.1.2/inst/datasets/examgrades.mat000066400000000000000000000113621527311104300227340ustar00rootroot00000000000000Octave-1-Lgradesÿmatrixþÿÿÿx@P@€N@@T@V@@Q@@V@€K@U@€U@U@ÀQ@@T@U@@T@€S@ÀP@X@€P@@R@ÀR@€M@ÀQ@@Q@€O@ÀS@S@€O@@U@ÀU@V@T@ÀQ@@P@U@ÀQ@ÀR@@T@ÀS@P@@P@U@@S@€Q@ÀR@U@ÀR@@R@W@€V@ÀS@T@ÀQ@@R@ÀQ@M@ÀS@@R@P@@S@€T@@T@€M@K@€T@€L@ÀS@ÀS@@R@€R@€T@€O@P@@R@@Q@ÀU@Q@@T@@R@ÀT@@R@T@@R@@R@ÀQ@€P@€S@P@€R@Q@ÀP@ÀR@ÀR@T@@U@€R@S@T@@S@@W@€Q@€U@T@@T@ÀT@Q@N@@U@P@€R@€T@@T@@S@€P@@U@ÀR@@T@@Q@N@ÀT@R@@S@€R@T@S@@S@@W@P@ÀT@ÀR@€T@€Q@V@€S@@S@€P@€R@ÀU@@Q@ÀR@Q@P@ÀQ@Q@ÀR@ÀT@T@ÀP@€S@U@ÀV@S@@R@ÀP@@V@ÀP@ÀR@R@@Q@ÀQ@R@R@@T@@S@ÀQ@@S@@S@ÀP@ÀR@ÀT@€R@€R@@T@ÀP@@Q@€O@@R@@S@ÀS@€U@@R@U@ÀQ@ÀP@U@ÀQ@€S@Q@Q@@S@U@€Q@@Q@@T@@T@@S@€P@S@ÀR@ÀT@@S@€S@@S@€R@R@Q@ÀR@€S@ÀQ@€R@€Q@V@€Q@€R@S@ÀQ@ÀS@O@@S@T@ÀQ@R@ÀS@S@@S@ÀQ@P@€R@€O@Q@€S@@Q@@R@ÀS@S@U@@V@€O@@Q@ÀS@ÀT@@Q@€Q@ÀQ@T@€R@€S@N@T@@T@€U@@R@T@T@@T@€V@@R@T@ÀR@P@€T@ÀP@@Q@Q@ÀR@ÀV@T@O@V@@U@@V@@R@S@@Q@@U@O@@R@L@@P@€S@@P@U@@U@T@ÀP@€M@ÀR@€T@ÀQ@V@@P@ÀT@@Q@€T@€T@€P@€Q@€R@ÀT@ÀS@S@€R@€P@ÀP@@Q@€O@€Q@ÀQ@€T@€S@€U@R@@S@@R@ÀQ@€S@€R@€S@€S@@V@€Q@S@ÀS@€Q@ÀR@U@@S@ÀR@ÀR@@R@ÀP@@T@R@R@€U@N@ÀS@€M@R@T@ÀP@@R@R@@T@€S@@S@€Q@R@€P@€S@R@Q@ÀR@€S@@R@@U@ÀT@@R@€R@€U@@S@ÀR@€P@€R@V@@Q@@S@I@@S@ÀU@W@@T@ÀS@€R@ÀT@U@S@€S@Q@S@@U@N@€S@N@@Q@€V@@Q@€P@ÀS@€V@@T@ÀR@€V@P@ÀT@@R@@P@@Q@P@@Q@ÀQ@€S@€U@€T@Q@€M@@U@ÀQ@@R@€S@€Q@ÀS@J@S@V@R@@Q@R@€Q@@W@ÀV@V@S@€P@€O@ÀP@€T@ÀU@€Q@@U@V@€S@ÀP@S@€R@@R@@Q@€R@Q@ÀV@ÀR@€Q@@T@€R@P@ÀR@€R@@S@€R@ÀQ@@R@@T@Q@R@U@@Q@T@N@ÀR@€S@€P@U@@U@@Q@€R@€T@€P@Q@ÀQ@S@S@€P@€S@@W@R@@T@@U@€Q@€P@€R@@Q@@Q@Q@ÀS@ÀS@S@T@€O@€S@ÀS@@U@ÀS@ÀT@T@ÀS@ÀR@R@€T@€Q@€R@ÀS@ÀP@ÀR@€P@@S@ÀU@S@R@ÀT@T@@S@@R@ÀS@@P@ÀS@ÀP@@S@Q@€Q@ÀQ@R@€S@€T@ÀS@ÀQ@€Q@ÀS@R@€R@€U@Q@@R@€R@€T@U@P@ÀQ@€Q@ÀS@U@€R@@S@€Q@€O@@S@@Q@S@S@S@ÀS@@S@€Q@€R@ÀQ@R@S@Q@€R@T@€U@@S@ÀQ@R@S@ÀQ@@R@€S@ÀQ@R@@R@ÀP@@T@€Q@€Q@€S@@R@@R@ÀP@ÀR@ÀT@@R@@T@ÀT@ÀR@@S@@S@€R@€R@ÀP@R@R@@R@ÀR@T@@R@@S@@U@@R@ÀR@€T@ÀR@statistics-release-2.1.2/inst/datasets/fail_load_model.mdl000066400000000000000000000115431527311104300237020ustar00rootroot00000000000000# Created by Octave 9.1.0, Sun Aug 18 14:52:45 2024 EEST # name: classdef_name # type: string # elements: 1 # length: 17 ClassificationKNN # name: variable_name # type: matrix # rows: 100 # columns: 2 4.7000000000000002 1.3999999999999999 4.5 1.5 4.9000000000000004 1.5 4 1.3 4.5999999999999996 1.5 4.5 1.3 4.7000000000000002 1.6000000000000001 3.2999999999999998 1 4.5999999999999996 1.3 3.8999999999999999 1.3999999999999999 3.5 1 4.2000000000000002 1.5 4 1 4.7000000000000002 1.3999999999999999 3.6000000000000001 1.3 4.4000000000000004 1.3999999999999999 4.5 1.5 4.0999999999999996 1 4.5 1.5 3.8999999999999999 1.1000000000000001 4.7999999999999998 1.8 4 1.3 4.9000000000000004 1.5 4.7000000000000002 1.2 4.2999999999999998 1.3 4.4000000000000004 1.3999999999999999 4.7999999999999998 1.3999999999999999 5 1.7 4.5 1.5 3.5 1 3.7999999999999998 1.1000000000000001 3.7000000000000002 1 3.8999999999999999 1.2 5.0999999999999996 1.6000000000000001 4.5 1.5 4.5 1.6000000000000001 4.7000000000000002 1.5 4.4000000000000004 1.3 4.0999999999999996 1.3 4 1.3 4.4000000000000004 1.2 4.5999999999999996 1.3999999999999999 4 1.2 3.2999999999999998 1 4.2000000000000002 1.3 4.2000000000000002 1.2 4.2000000000000002 1.3 4.2999999999999998 1.3 3 1.1000000000000001 4.0999999999999996 1.3 6 2.5 5.0999999999999996 1.8999999999999999 5.9000000000000004 2.1000000000000001 5.5999999999999996 1.8 5.7999999999999998 2.2000000000000002 6.5999999999999996 2.1000000000000001 4.5 1.7 6.2999999999999998 1.8 5.7999999999999998 1.8 6.0999999999999996 2.5 5.0999999999999996 2 5.2999999999999998 1.8999999999999999 5.5 2.1000000000000001 5 2 5.0999999999999996 2.3999999999999999 5.2999999999999998 2.2999999999999998 5.5 1.8 6.7000000000000002 2.2000000000000002 6.9000000000000004 2.2999999999999998 5 1.5 5.7000000000000002 2.2999999999999998 4.9000000000000004 2 6.7000000000000002 2 4.9000000000000004 1.8 5.7000000000000002 2.1000000000000001 6 1.8 4.7999999999999998 1.8 4.9000000000000004 1.8 5.5999999999999996 2.1000000000000001 5.7999999999999998 1.6000000000000001 6.0999999999999996 1.8999999999999999 6.4000000000000004 2 5.5999999999999996 2.2000000000000002 5.0999999999999996 1.5 5.5999999999999996 1.3999999999999999 6.0999999999999996 2.2999999999999998 5.5999999999999996 2.3999999999999999 5.5 1.8 4.7999999999999998 1.8 5.4000000000000004 2.1000000000000001 5.5999999999999996 2.3999999999999999 5.0999999999999996 2.2999999999999998 5.0999999999999996 1.8999999999999999 5.9000000000000004 2.2999999999999998 5.7000000000000002 2.5 5.2000000000000002 2.2999999999999998 5 1.8999999999999999 5.2000000000000002 2 5.4000000000000004 2.2999999999999998 5.0999999999999996 1.8 # name: Y # type: matrix # rows: 100 # columns: 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 # name: NumObservations # type: scalar 100 # name: RowsUsed # type: matrix # rows: 100 # columns: 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 # name: Standardize # type: bool 1 # name: Sigma # type: matrix # rows: 1 # columns: 2 0.82557846264289036 0.42476850498628405 # name: Mu # type: matrix # rows: 1 # columns: 2 4.9060000000000006 1.6760000000000006 # name: NumPredictors # type: scalar 2 # name: PredictorNames # type: cell # rows: 1 # columns: 2 # name: # type: sq_string # elements: 1 # length: 2 x1 # name: # type: sq_string # elements: 1 # length: 2 x2 # name: ResponseName # type: string # elements: 1 # length: 1 Y # name: ClassNames # type: cell # rows: 2 # columns: 1 # name: # type: sq_string # elements: 1 # length: 1 0 # name: # type: sq_string # elements: 1 # length: 1 1 # name: Prior # type: matrix # rows: 2 # columns: 1 0.5 0.5 # name: Cost # type: matrix # rows: 2 # columns: 2 0 1 1 0 # name: ScoreTransform # type: sq_string # elements: 1 # length: 4 none # name: BreakTies # type: string # elements: 1 # length: 8 smallest # name: NumNeighbors # type: scalar 10 # name: Distance # type: string # elements: 1 # length: 9 euclidean # name: DistanceWeight # type: function handle @ @(x) x # name: DistParameter # type: matrix # rows: 0 # columns: 0 # name: NSMethod # type: string # elements: 1 # length: 10 exhaustive # name: IncludeTies # type: bool 0 # name: BucketSize # type: scalar 50 statistics-release-2.1.2/inst/datasets/fail_loadmodel.mdl000066400000000000000000000115311527311104300235400ustar00rootroot00000000000000# Created by Octave 9.1.0, Sun Aug 18 14:52:45 2024 EEST # name: classdef_name # type: string # elements: 1 # length: 19 ClassificationModel # name: X # type: matrix # rows: 100 # columns: 2 4.7000000000000002 1.3999999999999999 4.5 1.5 4.9000000000000004 1.5 4 1.3 4.5999999999999996 1.5 4.5 1.3 4.7000000000000002 1.6000000000000001 3.2999999999999998 1 4.5999999999999996 1.3 3.8999999999999999 1.3999999999999999 3.5 1 4.2000000000000002 1.5 4 1 4.7000000000000002 1.3999999999999999 3.6000000000000001 1.3 4.4000000000000004 1.3999999999999999 4.5 1.5 4.0999999999999996 1 4.5 1.5 3.8999999999999999 1.1000000000000001 4.7999999999999998 1.8 4 1.3 4.9000000000000004 1.5 4.7000000000000002 1.2 4.2999999999999998 1.3 4.4000000000000004 1.3999999999999999 4.7999999999999998 1.3999999999999999 5 1.7 4.5 1.5 3.5 1 3.7999999999999998 1.1000000000000001 3.7000000000000002 1 3.8999999999999999 1.2 5.0999999999999996 1.6000000000000001 4.5 1.5 4.5 1.6000000000000001 4.7000000000000002 1.5 4.4000000000000004 1.3 4.0999999999999996 1.3 4 1.3 4.4000000000000004 1.2 4.5999999999999996 1.3999999999999999 4 1.2 3.2999999999999998 1 4.2000000000000002 1.3 4.2000000000000002 1.2 4.2000000000000002 1.3 4.2999999999999998 1.3 3 1.1000000000000001 4.0999999999999996 1.3 6 2.5 5.0999999999999996 1.8999999999999999 5.9000000000000004 2.1000000000000001 5.5999999999999996 1.8 5.7999999999999998 2.2000000000000002 6.5999999999999996 2.1000000000000001 4.5 1.7 6.2999999999999998 1.8 5.7999999999999998 1.8 6.0999999999999996 2.5 5.0999999999999996 2 5.2999999999999998 1.8999999999999999 5.5 2.1000000000000001 5 2 5.0999999999999996 2.3999999999999999 5.2999999999999998 2.2999999999999998 5.5 1.8 6.7000000000000002 2.2000000000000002 6.9000000000000004 2.2999999999999998 5 1.5 5.7000000000000002 2.2999999999999998 4.9000000000000004 2 6.7000000000000002 2 4.9000000000000004 1.8 5.7000000000000002 2.1000000000000001 6 1.8 4.7999999999999998 1.8 4.9000000000000004 1.8 5.5999999999999996 2.1000000000000001 5.7999999999999998 1.6000000000000001 6.0999999999999996 1.8999999999999999 6.4000000000000004 2 5.5999999999999996 2.2000000000000002 5.0999999999999996 1.5 5.5999999999999996 1.3999999999999999 6.0999999999999996 2.2999999999999998 5.5999999999999996 2.3999999999999999 5.5 1.8 4.7999999999999998 1.8 5.4000000000000004 2.1000000000000001 5.5999999999999996 2.3999999999999999 5.0999999999999996 2.2999999999999998 5.0999999999999996 1.8999999999999999 5.9000000000000004 2.2999999999999998 5.7000000000000002 2.5 5.2000000000000002 2.2999999999999998 5 1.8999999999999999 5.2000000000000002 2 5.4000000000000004 2.2999999999999998 5.0999999999999996 1.8 # name: Y # type: matrix # rows: 100 # columns: 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 # name: NumObservations # type: scalar 100 # name: RowsUsed # type: matrix # rows: 100 # columns: 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 # name: Standardize # type: bool 1 # name: Sigma # type: matrix # rows: 1 # columns: 2 0.82557846264289036 0.42476850498628405 # name: Mu # type: matrix # rows: 1 # columns: 2 4.9060000000000006 1.6760000000000006 # name: NumPredictors # type: scalar 2 # name: PredictorNames # type: cell # rows: 1 # columns: 2 # name: # type: sq_string # elements: 1 # length: 2 x1 # name: # type: sq_string # elements: 1 # length: 2 x2 # name: ResponseName # type: string # elements: 1 # length: 1 Y # name: ClassNames # type: cell # rows: 2 # columns: 1 # name: # type: sq_string # elements: 1 # length: 1 0 # name: # type: sq_string # elements: 1 # length: 1 1 # name: Prior # type: matrix # rows: 2 # columns: 1 0.5 0.5 # name: Cost # type: matrix # rows: 2 # columns: 2 0 1 1 0 # name: ScoreTransform # type: sq_string # elements: 1 # length: 4 none # name: BreakTies # type: string # elements: 1 # length: 8 smallest # name: NumNeighbors # type: scalar 10 # name: Distance # type: string # elements: 1 # length: 9 euclidean # name: DistanceWeight # type: function handle @ @(x) x # name: DistParameter # type: matrix # rows: 0 # columns: 0 # name: NSMethod # type: string # elements: 1 # length: 10 exhaustive # name: IncludeTies # type: bool 0 # name: BucketSize # type: scalar 50 statistics-release-2.1.2/inst/datasets/fisheriris.mat000066400000000000000000000322551527311104300227670ustar00rootroot00000000000000Octave-1-Lmeasÿmatrixþÿÿÿ–ffffff@š™™™™™@ÍÌÌÌÌÌ@ffffff@@š™™™™™@ffffff@@š™™™™™@š™™™™™@š™™™™™@333333@333333@333333@333333@ÍÌÌÌÌÌ@š™™™™™@ffffff@ÍÌÌÌÌÌ@ffffff@š™™™™™@ffffff@ffffff@ffffff@333333@@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333@š™™™™™@ÍÌÌÌÌÌ@@š™™™™™@@@š™™™™™@š™™™™™@ffffff@@@š™™™™™@@ffffff@333333@ffffff@ffffff@333333@@@š™™™™™@š™™™™™@@@ÍÌÌÌÌÌ@333333@š™™™™™@ffffff@ÍÌÌÌÌÌ@@š™™™™™@@ffffff@ffffff@ÍÌÌÌÌÌ@ffffff@333333@ÍÌÌÌÌÌ@ffffff@š™™™™™@ffffff@333333@ffffff@š™™™™™@ffffff@333333@ÍÌÌÌÌÌ@@ÍÌÌÌÌÌ@@@333333@@š™™™™™@@ÍÌÌÌÌÌ@333333@ffffff@@@ffffff@333333@@ffffff@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ffffff@ÍÌÌÌÌÌ@333333@333333@ffffff@333333@@ffffff@š™™™™™@333333@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@@š™™™™™@333333@ÍÌÌÌÌÌ@333333@š™™™™™@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@@š™™™™™@ffffff@ÍÌÌÌÌÌ@333333@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ffffff@š™™™™™@ÍÌÌÌÌÌ@š™™™™™@š™™™™™@š™™™™™@333333@ffffff@ÍÌÌÌÌÌ@333333@š™™™™™@@š™™™™™@ÍÌÌÌÌÌ@š™™™™™@333333@333333@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333@@ÍÌÌÌÌÌ@š™™™™™@ @@š™™™™™ @ÍÌÌÌÌÌ@ÍÌÌÌÌÌ @333333@333333 @333333 @333333@ÍÌÌÌÌÌ@š™™™™™ @333333 @@@@š™™™™™@333333@ @ffffff@ffffff@333333 @š™™™™™ @ÍÌÌÌÌÌ @ffffff @333333 @@333333 @ @333333 @š™™™™™ @ÍÌÌÌÌÌ@333333 @ffffff@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@š™™™™™ @ @ÍÌÌÌÌÌ @@333333 @ @ffffff@š™™™™™ @ @ffffff@@ffffff@š™™™™™ @š™™™™™ @ffffff @š™™™™™ @š™™™™™ @ÍÌÌÌÌÌ@ffffff@ffffff@ffffff@ffffff @333333@333333@š™™™™™@@@š™™™™™@333333@333333@ÍÌÌÌÌÌ@@š™™™™™@š™™™™™@@š™™™™™ @ffffff@@ffffff@333333@@ffffff@@333333@ÍÌÌÌÌÌ@333333@333333@š™™™™™@š™™™™™@@333333 @ÍÌÌÌÌÌ@ffffff@@@ÍÌÌÌÌÌ@@ÍÌÌÌÌÌ@ffffff@š™™™™™@@333333@333333@@ffffff@ffffff @š™™™™™@@333333@@@@333333@@ÍÌÌÌÌÌ @š™™™™™ @š™™™™™@@@ffffff@š™™™™™ @@ffffff@ÍÌÌÌÌÌ@š™™™™™@š™™™™™ @ffffff@ffffff@š™™™™™@ffffff @š™™™™™ @ffffff@@ffffff@@ffffff@ffffff@ffffff@ffffff@ÍÌÌÌÌÌ@@333333 @ÍÌÌÌÌÌ@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@š™™™™™@š™™™™™ @ffffff @@@@333333 @@ffffffö?ffffffö?ÍÌÌÌÌÌô?ø?ffffffö?333333û?ffffffö?ø?ffffffö?ø?ø?š™™™™™ù?ffffffö?š™™™™™ñ?333333ó?ø?ÍÌÌÌÌÌô?ffffffö?333333û?ø?333333û?ø?ð?333333û?ffffffþ?š™™™™™ù?š™™™™™ù?ø?ffffffö?š™™™™™ù?š™™™™™ù?ø?ø?ffffffö?ø?333333ó?ÍÌÌÌÌÌô?ffffffö?ÍÌÌÌÌÌô?ø?ÍÌÌÌÌÌô?ÍÌÌÌÌÌô?ÍÌÌÌÌÌô?š™™™™™ù?ffffffþ?ffffffö?š™™™™™ù?ffffffö?ø?ffffffö?ÍÌÌÌÌÌ@@š™™™™™@@ffffff@@ÍÌÌÌÌÌ@ffffff @ffffff@333333@ @ÍÌÌÌÌÌ@@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ @š™™™™™@@ffffff@@333333@333333@@š™™™™™@ÍÌÌÌÌÌ@333333@š™™™™™@333333@@@ @ffffff@š™™™™™ @333333@ffffff@@@ÍÌÌÌÌÌ@š™™™™™@ffffff@@š™™™™™@ffffff@@ffffff @ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@333333@@ffffff@@ffffff@š™™™™™@ffffff@333333@ffffff@@333333@333333@ffffff@ffffff@333333@@@ffffff@333333@@ÍÌÌÌÌÌ@š™™™™™@@ÍÌÌÌÌÌ@š™™™™™@ÍÌÌÌÌÌ@š™™™™™@ÍÌÌÌÌÌ@@333333@š™™™™™@ffffff@333333@ffffff@š™™™™™@ffffff@ffffff@ffffff@ffffff@ffffff@@333333@š™™™™™@ffffff@ffffff@ffffff@š™™™™™@ÍÌÌÌÌÌ@ÍÌÌÌÌÌ@@ÍÌÌÌÌÌ@š™™™™™@ffffff@š™™™™™É?š™™™™™É?š™™™™™É?š™™™™™É?š™™™™™É?š™™™™™Ù?333333Ó?š™™™™™É?š™™™™™É?š™™™™™¹?š™™™™™É?š™™™™™É?š™™™™™¹?š™™™™™¹?š™™™™™É?š™™™™™Ù?š™™™™™Ù?333333Ó?333333Ó?333333Ó?š™™™™™É?š™™™™™Ù?š™™™™™É?à?š™™™™™É?š™™™™™É?š™™™™™Ù?š™™™™™É?š™™™™™É?š™™™™™É?š™™™™™É?š™™™™™Ù?š™™™™™¹?š™™™™™É?š™™™™™É?š™™™™™É?š™™™™™É?š™™™™™¹?š™™™™™É?š™™™™™É?333333Ó?333333Ó?š™™™™™É?333333ã?š™™™™™Ù?333333Ó?š™™™™™É?š™™™™™É?š™™™™™É?š™™™™™É?ffffffö?ø?ø?ÍÌÌÌÌÌô?ø?ÍÌÌÌÌÌô?š™™™™™ù?ð?ÍÌÌÌÌÌô?ffffffö?ð?ø?ð?ffffffö?ÍÌÌÌÌÌô?ffffffö?ø?ð?ø?š™™™™™ñ?ÍÌÌÌÌÌü?ÍÌÌÌÌÌô?ø?333333ó?ÍÌÌÌÌÌô?ffffffö?ffffffö?333333û?ø?ð?š™™™™™ñ?ð?333333ó?š™™™™™ù?ø?š™™™™™ù?ø?ÍÌÌÌÌÌô?ÍÌÌÌÌÌô?ÍÌÌÌÌÌô?333333ó?ffffffö?333333ó?ð?ÍÌÌÌÌÌô?333333ó?ÍÌÌÌÌÌô?ÍÌÌÌÌÌô?š™™™™™ñ?ÍÌÌÌÌÌô?@ffffffþ?ÍÌÌÌÌÌ@ÍÌÌÌÌÌü?š™™™™™@ÍÌÌÌÌÌ@333333û?ÍÌÌÌÌÌü?ÍÌÌÌÌÌü?@@ffffffþ?ÍÌÌÌÌÌ@@333333@ffffff@ÍÌÌÌÌÌü?š™™™™™@ffffff@ø?ffffff@@@ÍÌÌÌÌÌü?ÍÌÌÌÌÌ@ÍÌÌÌÌÌü?ÍÌÌÌÌÌü?ÍÌÌÌÌÌü?ÍÌÌÌÌÌ@š™™™™™ù?ffffffþ?@š™™™™™@ø?ffffffö?ffffff@333333@ÍÌÌÌÌÌü?ÍÌÌÌÌÌü?ÍÌÌÌÌÌ@333333@ffffff@ffffffþ?ffffff@@ffffff@ffffffþ?@ffffff@ÍÌÌÌÌÌü?speciesÿcellþÿÿÿ–ÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿsetosaÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ versicolorÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicaÿ sq_stringþÿÿÿ virginicastatistics-release-2.1.2/inst/datasets/gradespaired.mat000066400000000000000000000036701527311104300232510ustar00rootroot00000000000000Octave-1-L gradespairedÿmatrixþÿÿÿx@P@€N@@T@V@@Q@@V@€K@U@€U@U@ÀQ@@T@U@@T@€S@ÀP@X@€P@@R@ÀR@€M@ÀQ@@Q@€O@ÀS@S@€O@@U@ÀU@V@T@ÀQ@@P@U@ÀQ@ÀR@@T@ÀS@P@@P@U@@S@€Q@ÀR@U@ÀR@@R@W@€V@ÀS@T@ÀQ@@R@ÀQ@M@ÀS@@R@P@@S@€T@@T@€M@K@€T@€L@ÀS@ÀS@@R@€R@€T@€O@P@@R@@Q@ÀU@Q@@T@@R@ÀT@@R@T@@R@@R@ÀQ@€P@€S@P@€R@Q@ÀP@ÀR@ÀR@T@@U@€R@S@T@@S@@W@€Q@€U@T@@T@ÀT@Q@N@@U@P@€R@€T@@T@@S@€P@@U@ÀR@@T@@Q@N@ÀT@R@ÀP@€N@€T@ÀU@@R@@V@€J@@V@€U@ÀT@ÀQ@ÀS@@T@@V@ÀT@Q@@W@P@€R@€S@L@@P@@P@@P@@T@€S@€O@€U@€U@ÀV@S@@R@€O@ÀT@@R@ÀS@€S@ÀT@€P@€P@U@€S@ÀQ@S@@U@€S@€S@€W@€V@@T@@T@Q@S@R@€M@@T@€R@O@@S@ÀT@T@@P@J@@T@L@S@ÀS@€R@ÀS@€T@N@@Q@@S@€Q@U@Q@€T@ÀR@ÀT@ÀR@€T@ÀQ@R@€Q@€P@ÀS@€P@€P@Q@ÀQ@@R@€S@€T@€U@ÀR@R@@T@€T@€W@R@@U@@T@ÀT@@U@ÀP@€N@W@€M@@T@@U@€U@€R@@P@€U@€S@@S@R@M@@U@ÀR@statistics-release-2.1.2/inst/datasets/hald.mat000066400000000000000000000046731527311104300215330ustar00rootroot00000000000000Octave-1-L Descriptionÿ sq_stringþÿÿÿ:= M icccc hh SW"dv RHW= u noooo ee ooEu. eai l gllll aa uofr2 fllP t ruuuu tt rdfi4 edeo i emmmm csen r,yr p dnnnn (o e,cgn eA,t l i1234 cf :Ht o n. l e e:::: a . H. c,1a n lh ,oa1 e 9n r t3342 /a fr1 :S6d e sCCCC gr H d t0 g aaaa md .Ce( a.C r (OOOO )e on1 t e e %.... :n Smi9 i m s )ASAS i tpn3 s e s :lili n eog2 t n i 2O2O g is,) i t o O2O2 ni", c n 3 3 a ot a D (.( f uiIp l a d (tFb t ronp t a tree e ,nd. T a t ri2t r u1 h a icOa Hos2 e = ca3- 1 .ft0 o = al d 8 r7 r lc(i 0 SPi- y citc toa1 iuea d arl2 w umtl a rt 1 i m rc y kla4 t sai s ean. h aicu ,nd llam d E uil E n mccs Cn g iaii eg i ntul mi n aemi en e t) c ne e e aa te r ) lt r i ue oi n m) nn g i g n H A o eC p f ah p e te l r m i r Ei c i vs a t ot t e lr i ) vy o e, n d s , haldÿmatrixþÿÿÿ @ð?&@&@@&@@ð?@5@ð?&@$@:@=@L@?@J@€K@ÀQ@?@K@€G@D@€P@Q@@.@ @ @@"@1@6@2@@7@"@ @N@J@4@€G@€@@6@@F@6@:@A@(@(@ S@33333“R@33333Z@fffffæU@š™™™™ùW@ÍÌÌÌÌL[@ÍÌÌÌ̬Y@ R@fffffFW@š™™™™ù\@33333óT@33333S\@š™™™™Y[@heatÿmatrixþÿÿÿ  S@33333“R@33333Z@fffffæU@š™™™™ùW@ÍÌÌÌÌL[@ÍÌÌÌ̬Y@ R@fffffFW@š™™™™ù\@33333óT@33333S\@š™™™™Y[@ ingredientsÿmatrixþÿÿÿ @ð?&@&@@&@@ð?@5@ð?&@$@:@=@L@?@J@€K@ÀQ@?@K@€G@D@€P@Q@@.@ @ @@"@1@6@2@@7@"@ @N@J@4@€G@€@@6@@F@6@:@A@(@(@statistics-release-2.1.2/inst/datasets/heart_scale.dat000066400000000000000000000660261527311104300230640ustar00rootroot00000000000000+1 1:0.708333 2:1 3:1 4:-0.320755 5:-0.105023 6:-1 7:1 8:-0.419847 9:-1 10:-0.225806 12:1 13:-1 -1 1:0.583333 2:-1 3:0.333333 4:-0.603774 5:1 6:-1 7:1 8:0.358779 9:-1 10:-0.483871 12:-1 13:1 +1 1:0.166667 2:1 3:-0.333333 4:-0.433962 5:-0.383562 6:-1 7:-1 8:0.0687023 9:-1 10:-0.903226 11:-1 12:-1 13:1 -1 1:0.458333 2:1 3:1 4:-0.358491 5:-0.374429 6:-1 7:-1 8:-0.480916 9:1 10:-0.935484 12:-0.333333 13:1 -1 1:0.875 2:-1 3:-0.333333 4:-0.509434 5:-0.347032 6:-1 7:1 8:-0.236641 9:1 10:-0.935484 11:-1 12:-0.333333 13:-1 -1 1:0.5 2:1 3:1 4:-0.509434 5:-0.767123 6:-1 7:-1 8:0.0534351 9:-1 10:-0.870968 11:-1 12:-1 13:1 +1 1:0.125 2:1 3:0.333333 4:-0.320755 5:-0.406393 6:1 7:1 8:0.0839695 9:1 10:-0.806452 12:-0.333333 13:0.5 +1 1:0.25 2:1 3:1 4:-0.698113 5:-0.484018 6:-1 7:1 8:0.0839695 9:1 10:-0.612903 12:-0.333333 13:1 +1 1:0.291667 2:1 3:1 4:-0.132075 5:-0.237443 6:-1 7:1 8:0.51145 9:-1 10:-0.612903 12:0.333333 13:1 +1 1:0.416667 2:-1 3:1 4:0.0566038 5:0.283105 6:-1 7:1 8:0.267176 9:-1 10:0.290323 12:1 13:1 -1 1:0.25 2:1 3:1 4:-0.226415 5:-0.506849 6:-1 7:-1 8:0.374046 9:-1 10:-0.83871 12:-1 13:1 -1 2:1 3:1 4:-0.0943396 5:-0.543379 6:-1 7:1 8:-0.389313 9:1 10:-1 11:-1 12:-1 13:1 -1 1:-0.375 2:1 3:0.333333 4:-0.132075 5:-0.502283 6:-1 7:1 8:0.664122 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:0.333333 2:1 3:-1 4:-0.245283 5:-0.506849 6:-1 7:-1 8:0.129771 9:-1 10:-0.16129 12:0.333333 13:-1 -1 1:0.166667 2:-1 3:1 4:-0.358491 5:-0.191781 6:-1 7:1 8:0.343511 9:-1 10:-1 11:-1 12:-0.333333 13:-1 -1 1:0.75 2:-1 3:1 4:-0.660377 5:-0.894977 6:-1 7:-1 8:-0.175573 9:-1 10:-0.483871 12:-1 13:-1 +1 1:-0.291667 2:1 3:1 4:-0.132075 5:-0.155251 6:-1 7:-1 8:-0.251908 9:1 10:-0.419355 12:0.333333 13:1 +1 2:1 3:1 4:-0.132075 5:-0.648402 6:1 7:1 8:0.282443 9:1 11:1 12:-1 13:1 -1 1:0.458333 2:1 3:-1 4:-0.698113 5:-0.611872 6:-1 7:1 8:0.114504 9:1 10:-0.419355 12:-1 13:-1 -1 1:-0.541667 2:1 3:-1 4:-0.132075 5:-0.666667 6:-1 7:-1 8:0.633588 9:1 10:-0.548387 11:-1 12:-1 13:1 +1 1:0.583333 2:1 3:1 4:-0.509434 5:-0.52968 6:-1 7:1 8:-0.114504 9:1 10:-0.16129 12:0.333333 13:1 -1 1:-0.208333 2:1 3:-0.333333 4:-0.320755 5:-0.456621 6:-1 7:1 8:0.664122 9:-1 10:-0.935484 12:-1 13:-1 -1 1:-0.416667 2:1 3:1 4:-0.603774 5:-0.191781 6:-1 7:-1 8:0.679389 9:-1 10:-0.612903 12:-1 13:-1 -1 1:-0.25 2:1 3:1 4:-0.660377 5:-0.643836 6:-1 7:-1 8:0.0992366 9:-1 10:-0.967742 11:-1 12:-1 13:-1 -1 1:0.0416667 2:-1 3:-0.333333 4:-0.283019 5:-0.260274 6:1 7:1 8:0.343511 9:1 10:-1 11:-1 12:-0.333333 13:-1 -1 1:-0.208333 2:-1 3:0.333333 4:-0.320755 5:-0.319635 6:-1 7:-1 8:0.0381679 9:-1 10:-0.935484 11:-1 12:-1 13:-1 -1 1:-0.291667 2:-1 3:1 4:-0.169811 5:-0.465753 6:-1 7:1 8:0.236641 9:1 10:-1 12:-1 13:-1 -1 1:-0.0833333 2:-1 3:0.333333 4:-0.509434 5:-0.228311 6:-1 7:1 8:0.312977 9:-1 10:-0.806452 11:-1 12:-1 13:-1 +1 1:0.208333 2:1 3:0.333333 4:-0.660377 5:-0.525114 6:-1 7:1 8:0.435115 9:-1 10:-0.193548 12:-0.333333 13:1 -1 1:0.75 2:-1 3:0.333333 4:-0.698113 5:-0.365297 6:1 7:1 8:-0.0992366 9:-1 10:-1 11:-1 12:-0.333333 13:-1 +1 1:0.166667 2:1 3:0.333333 4:-0.358491 5:-0.52968 6:-1 7:1 8:0.206107 9:-1 10:-0.870968 12:-0.333333 13:1 -1 1:0.541667 2:1 3:1 4:0.245283 5:-0.534247 6:-1 7:1 8:0.0229008 9:-1 10:-0.258065 11:-1 12:-1 13:0.5 -1 1:-0.666667 2:-1 3:0.333333 4:-0.509434 5:-0.593607 6:-1 7:-1 8:0.51145 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:0.25 2:1 3:1 4:0.433962 5:-0.086758 6:-1 7:1 8:0.0534351 9:1 10:0.0967742 11:1 12:-1 13:1 +1 1:-0.125 2:1 3:1 4:-0.0566038 5:-0.6621 6:-1 7:1 8:-0.160305 9:1 10:-0.709677 12:-1 13:1 +1 1:-0.208333 2:1 3:1 4:-0.320755 5:-0.406393 6:1 7:1 8:0.206107 9:1 10:-1 11:-1 12:0.333333 13:1 +1 1:0.333333 2:1 3:1 4:-0.132075 5:-0.630137 6:-1 7:1 8:0.0229008 9:1 10:-0.387097 11:-1 12:-0.333333 13:1 +1 1:0.25 2:1 3:-1 4:0.245283 5:-0.328767 6:-1 7:1 8:-0.175573 9:-1 10:-1 11:-1 12:-1 13:-1 -1 1:-0.458333 2:1 3:0.333333 4:-0.320755 5:-0.753425 6:-1 7:-1 8:0.206107 9:-1 10:-1 11:-1 12:-1 13:-1 -1 1:-0.208333 2:1 3:1 4:-0.471698 5:-0.561644 6:-1 7:1 8:0.755725 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:-0.541667 2:1 3:1 4:0.0943396 5:-0.557078 6:-1 7:-1 8:0.679389 9:-1 10:-1 11:-1 12:-1 13:1 -1 1:0.375 2:-1 3:1 4:-0.433962 5:-0.621005 6:-1 7:-1 8:0.40458 9:-1 10:-1 11:-1 12:-1 13:-1 -1 1:-0.375 2:1 3:0.333333 4:-0.320755 5:-0.511416 6:-1 7:-1 8:0.648855 9:1 10:-0.870968 11:-1 12:-1 13:-1 -1 1:-0.291667 2:1 3:-0.333333 4:-0.867925 5:-0.675799 6:1 7:-1 8:0.29771 9:-1 10:-1 11:-1 12:-1 13:1 +1 1:0.25 2:1 3:0.333333 4:-0.396226 5:-0.579909 6:1 7:-1 8:-0.0381679 9:-1 10:-0.290323 12:-0.333333 13:0.5 -1 1:0.208333 2:1 3:0.333333 4:-0.132075 5:-0.611872 6:1 7:1 8:0.435115 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:-0.166667 2:1 3:0.333333 4:-0.54717 5:-0.894977 6:-1 7:1 8:-0.160305 9:-1 10:-0.741935 11:-1 12:1 13:-1 +1 1:-0.375 2:1 3:1 4:-0.698113 5:-0.675799 6:-1 7:1 8:0.618321 9:-1 10:-1 11:-1 12:-0.333333 13:-1 +1 1:0.541667 2:1 3:-0.333333 4:0.245283 5:-0.452055 6:-1 7:-1 8:-0.251908 9:1 10:-1 12:1 13:0.5 +1 1:0.5 2:-1 3:1 4:0.0566038 5:-0.547945 6:-1 7:1 8:-0.343511 9:-1 10:-0.677419 12:1 13:1 +1 1:-0.458333 2:1 3:1 4:-0.207547 5:-0.136986 6:-1 7:-1 8:-0.175573 9:1 10:-0.419355 12:-1 13:0.5 -1 1:-0.0416667 2:1 3:-0.333333 4:-0.358491 5:-0.639269 6:1 7:-1 8:0.725191 9:-1 10:-1 11:-1 12:-1 13:-1 -1 1:0.5 2:-1 3:0.333333 4:-0.132075 5:0.328767 6:1 7:1 8:0.312977 9:-1 10:-0.741935 11:-1 12:-0.333333 13:-1 -1 1:0.416667 2:-1 3:-0.333333 4:-0.132075 5:-0.684932 6:-1 7:-1 8:0.648855 9:-1 10:-1 11:-1 12:0.333333 13:-1 -1 1:-0.333333 2:-1 3:-0.333333 4:-0.320755 5:-0.506849 6:-1 7:1 8:0.587786 9:-1 10:-0.806452 12:-1 13:-1 -1 1:-0.5 2:-1 3:-0.333333 4:-0.792453 5:-0.671233 6:-1 7:-1 8:0.480916 9:-1 10:-1 11:-1 12:-0.333333 13:-1 +1 1:0.333333 2:1 3:1 4:-0.169811 5:-0.817352 6:-1 7:1 8:-0.175573 9:1 10:0.16129 12:-0.333333 13:-1 -1 1:0.291667 2:-1 3:0.333333 4:-0.509434 5:-0.762557 6:1 7:-1 8:-0.618321 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:0.25 2:-1 3:1 4:0.509434 5:-0.438356 6:-1 7:-1 8:0.0992366 9:1 10:-1 12:-1 13:-1 +1 1:0.375 2:1 3:-0.333333 4:-0.509434 5:-0.292237 6:-1 7:1 8:-0.51145 9:-1 10:-0.548387 12:-0.333333 13:1 -1 1:0.166667 2:1 3:0.333333 4:0.0566038 5:-1 6:1 7:-1 8:0.557252 9:-1 10:-0.935484 11:-1 12:-0.333333 13:1 +1 1:-0.0833333 2:-1 3:1 4:-0.320755 5:-0.182648 6:-1 7:-1 8:0.0839695 9:1 10:-0.612903 12:-1 13:1 -1 1:-0.375 2:1 3:0.333333 4:-0.509434 5:-0.543379 6:-1 7:-1 8:0.496183 9:-1 10:-1 11:-1 12:-1 13:-1 -1 1:0.291667 2:-1 3:-1 4:0.0566038 5:-0.479452 6:-1 7:-1 8:0.526718 9:-1 10:-0.709677 11:-1 12:-1 13:-1 -1 1:0.416667 2:1 3:-1 4:-0.0377358 5:-0.511416 6:1 7:1 8:0.206107 9:-1 10:-0.258065 11:1 12:-1 13:0.5 +1 1:0.166667 2:1 3:1 4:0.0566038 5:-0.315068 6:-1 7:1 8:-0.374046 9:1 10:-0.806452 12:-0.333333 13:0.5 -1 1:-0.0833333 2:1 3:1 4:-0.132075 5:-0.383562 6:-1 7:1 8:0.755725 9:1 10:-1 11:-1 12:-1 13:-1 +1 1:0.208333 2:-1 3:-0.333333 4:-0.207547 5:-0.118721 6:1 7:1 8:0.236641 9:-1 10:-1 11:-1 12:0.333333 13:-1 -1 1:-0.375 2:-1 3:0.333333 4:-0.54717 5:-0.47032 6:-1 7:-1 8:0.19084 9:-1 10:-0.903226 12:-0.333333 13:-1 +1 1:-0.25 2:1 3:0.333333 4:-0.735849 5:-0.465753 6:-1 7:-1 8:0.236641 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:0.333333 2:1 3:1 4:-0.509434 5:-0.388128 6:-1 7:-1 8:0.0534351 9:1 10:0.16129 12:-0.333333 13:1 -1 1:0.166667 2:-1 3:1 4:-0.509434 5:0.0410959 6:-1 7:-1 8:0.40458 9:1 10:-0.806452 11:-1 12:-1 13:-1 -1 1:0.708333 2:1 3:-0.333333 4:0.169811 5:-0.456621 6:-1 7:1 8:0.0992366 9:-1 10:-1 11:-1 12:-1 13:-1 -1 1:0.958333 2:-1 3:0.333333 4:-0.132075 5:-0.675799 6:-1 8:-0.312977 9:-1 10:-0.645161 12:-1 13:-1 -1 1:0.583333 2:-1 3:1 4:-0.773585 5:-0.557078 6:-1 7:-1 8:0.0839695 9:-1 10:-0.903226 11:-1 12:0.333333 13:-1 +1 1:-0.333333 2:1 3:1 4:-0.0943396 5:-0.164384 6:-1 7:1 8:0.160305 9:1 10:-1 12:1 13:1 -1 1:-0.333333 2:1 3:1 4:-0.811321 5:-0.625571 6:-1 7:1 8:0.175573 9:1 10:-0.0322581 12:-1 13:-1 -1 1:-0.583333 2:-1 3:0.333333 4:-1 5:-0.666667 6:-1 7:-1 8:0.648855 9:-1 10:-1 11:-1 12:-1 13:-1 -1 1:-0.458333 2:-1 3:0.333333 4:-0.509434 5:-0.621005 6:-1 7:-1 8:0.557252 9:-1 10:-1 12:-1 13:-1 -1 1:0.125 2:1 3:-0.333333 4:-0.509434 5:-0.497717 6:-1 7:-1 8:0.633588 9:-1 10:-0.741935 11:-1 12:-1 13:-1 +1 1:0.208333 2:1 3:1 4:-0.0188679 5:-0.579909 6:-1 7:-1 8:-0.480916 9:-1 10:-0.354839 12:-0.333333 13:1 +1 1:-0.75 2:1 3:1 4:-0.509434 5:-0.671233 6:-1 7:-1 8:-0.0992366 9:1 10:-0.483871 12:-1 13:1 +1 1:0.208333 2:1 3:1 4:0.0566038 5:-0.342466 6:-1 7:1 8:-0.389313 9:1 10:-0.741935 11:-1 12:-1 13:1 -1 1:-0.5 2:1 3:0.333333 4:-0.320755 5:-0.598174 6:-1 7:1 8:0.480916 9:-1 10:-0.354839 12:-1 13:-1 -1 1:0.166667 2:1 3:1 4:-0.698113 5:-0.657534 6:-1 7:-1 8:-0.160305 9:1 10:-0.516129 12:-1 13:0.5 -1 1:-0.458333 2:1 3:-1 4:0.0188679 5:-0.461187 6:-1 7:1 8:0.633588 9:-1 10:-0.741935 11:-1 12:0.333333 13:-1 -1 1:0.375 2:1 3:-0.333333 4:-0.358491 5:-0.625571 6:1 7:1 8:0.0534351 9:-1 10:-1 11:-1 12:-1 13:-1 -1 1:0.25 2:1 3:-1 4:0.584906 5:-0.342466 6:-1 7:1 8:0.129771 9:-1 10:0.354839 11:1 12:-1 13:1 -1 1:-0.5 2:-1 3:-0.333333 4:-0.396226 5:-0.178082 6:-1 7:-1 8:0.40458 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:-0.125 2:1 3:1 4:0.0566038 5:-0.465753 6:-1 7:1 8:-0.129771 9:-1 10:-0.16129 12:-1 13:1 -1 1:0.25 2:1 3:-0.333333 4:-0.132075 5:-0.56621 6:-1 7:-1 8:0.419847 9:1 10:-1 11:-1 12:-1 13:-1 +1 1:0.333333 2:-1 3:1 4:-0.320755 5:-0.0684932 6:-1 7:1 8:0.496183 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:0.0416667 2:1 3:1 4:-0.433962 5:-0.360731 6:-1 7:1 8:-0.419847 9:1 10:-0.290323 12:-0.333333 13:1 +1 1:0.0416667 2:1 3:1 4:-0.698113 5:-0.634703 6:-1 7:1 8:-0.435115 9:1 10:-1 12:-0.333333 13:-1 +1 1:-0.0416667 2:1 3:1 4:-0.415094 5:-0.607306 6:-1 7:-1 8:0.480916 9:-1 10:-0.677419 11:-1 12:0.333333 13:1 +1 1:-0.25 2:1 3:1 4:-0.698113 5:-0.319635 6:-1 7:1 8:-0.282443 9:1 10:-0.677419 12:-0.333333 13:-1 -1 1:0.541667 2:1 3:1 4:-0.509434 5:-0.196347 6:-1 7:1 8:0.221374 9:-1 10:-0.870968 12:-1 13:-1 +1 1:0.208333 2:1 3:1 4:-0.886792 5:-0.506849 6:-1 7:-1 8:0.29771 9:-1 10:-0.967742 11:-1 12:-0.333333 13:1 -1 1:0.458333 2:-1 3:0.333333 4:-0.132075 5:-0.146119 6:-1 7:-1 8:-0.0534351 9:-1 10:-0.935484 11:-1 12:-1 13:1 -1 1:-0.125 2:-1 3:-0.333333 4:-0.509434 5:-0.461187 6:-1 7:-1 8:0.389313 9:-1 10:-0.645161 11:-1 12:-1 13:-1 -1 1:-0.375 2:-1 3:0.333333 4:-0.735849 5:-0.931507 6:-1 7:-1 8:0.587786 9:-1 10:-0.806452 12:-1 13:-1 +1 1:0.583333 2:1 3:1 4:-0.509434 5:-0.493151 6:-1 7:-1 8:-1 9:-1 10:-0.677419 12:-1 13:-1 -1 1:-0.166667 2:-1 3:1 4:-0.320755 5:-0.347032 6:-1 7:-1 8:0.40458 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:0.166667 2:1 3:1 4:0.339623 5:-0.255708 6:1 7:1 8:-0.19084 9:-1 10:-0.677419 12:1 13:1 +1 1:0.416667 2:1 3:1 4:-0.320755 5:-0.415525 6:-1 7:1 8:0.160305 9:-1 10:-0.548387 12:-0.333333 13:1 +1 1:-0.208333 2:1 3:1 4:-0.433962 5:-0.324201 6:-1 7:1 8:0.450382 9:-1 10:-0.83871 12:-1 13:1 -1 1:-0.0833333 2:1 3:0.333333 4:-0.886792 5:-0.561644 6:-1 7:-1 8:0.0992366 9:1 10:-0.612903 12:-1 13:-1 +1 1:0.291667 2:-1 3:1 4:0.0566038 5:-0.39726 6:-1 7:1 8:0.312977 9:-1 10:-0.16129 12:0.333333 13:1 +1 1:0.25 2:1 3:1 4:-0.132075 5:-0.767123 6:-1 7:-1 8:0.389313 9:1 10:-1 11:-1 12:-0.333333 13:1 -1 1:-0.333333 2:-1 3:-0.333333 4:-0.660377 5:-0.844749 6:-1 7:-1 8:0.0229008 9:-1 10:-1 12:-1 13:-1 +1 1:0.0833333 2:-1 3:1 4:0.622642 5:-0.0821918 6:-1 8:-0.29771 9:1 10:0.0967742 12:-1 13:-1 -1 1:-0.5 2:1 3:-0.333333 4:-0.698113 5:-0.502283 6:-1 7:-1 8:0.251908 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:0.291667 2:-1 3:1 4:0.207547 5:-0.182648 6:-1 7:1 8:0.374046 9:-1 10:-1 11:-1 12:-1 13:-1 -1 1:0.0416667 2:-1 3:0.333333 4:-0.226415 5:-0.187215 6:1 7:-1 8:0.51145 9:-1 10:-1 11:-1 12:-1 13:-1 -1 1:-0.458333 2:1 3:-0.333333 4:-0.509434 5:-0.228311 6:-1 7:-1 8:0.389313 9:-1 10:-1 11:-1 12:-1 13:-1 -1 1:-0.166667 2:-1 3:-0.333333 4:-0.245283 5:-0.3379 6:-1 7:-1 8:0.389313 9:-1 10:-1 12:-1 13:-1 +1 1:-0.291667 2:1 3:1 4:-0.509434 5:-0.438356 6:-1 7:1 8:0.114504 9:-1 10:-0.741935 11:-1 12:-1 13:1 +1 1:0.125 2:-1 3:1 4:1 5:-0.260274 6:1 7:1 8:-0.0534351 9:1 10:0.290323 11:1 12:0.333333 13:1 -1 1:0.541667 2:-1 3:-1 4:0.0566038 5:-0.543379 6:-1 7:-1 8:-0.343511 9:-1 10:-0.16129 11:1 12:-1 13:-1 +1 1:0.125 2:1 3:1 4:-0.320755 5:-0.283105 6:1 7:1 8:-0.51145 9:1 10:-0.483871 11:1 12:-1 13:1 +1 1:-0.166667 2:1 3:0.333333 4:-0.509434 5:-0.716895 6:-1 7:-1 8:0.0381679 9:-1 10:-0.354839 12:1 13:1 +1 1:0.0416667 2:1 3:1 4:-0.471698 5:-0.269406 6:-1 7:1 8:-0.312977 9:1 10:0.0322581 12:0.333333 13:-1 +1 1:0.166667 2:1 3:1 4:0.0943396 5:-0.324201 6:-1 7:-1 8:-0.740458 9:1 10:-0.612903 12:-0.333333 13:1 -1 1:0.5 2:-1 3:0.333333 4:0.245283 5:0.0684932 6:-1 7:1 8:0.221374 9:-1 10:-0.741935 11:-1 12:-1 13:-1 -1 1:0.0416667 2:1 3:0.333333 4:-0.415094 5:-0.328767 6:-1 7:1 8:0.236641 9:-1 10:-0.83871 11:1 12:-0.333333 13:-1 -1 1:0.0416667 2:-1 3:0.333333 4:0.245283 5:-0.657534 6:-1 7:-1 8:0.40458 9:-1 10:-1 11:-1 12:-0.333333 13:-1 +1 1:0.375 2:1 3:1 4:-0.509434 5:-0.356164 6:-1 7:-1 8:-0.572519 9:1 10:-0.419355 12:0.333333 13:1 -1 1:-0.0416667 2:-1 3:0.333333 4:-0.207547 5:-0.680365 6:-1 7:1 8:0.496183 9:-1 10:-0.967742 12:-1 13:-1 -1 1:-0.0416667 2:1 3:-0.333333 4:-0.245283 5:-0.657534 6:-1 7:-1 8:0.328244 9:-1 10:-0.741935 11:-1 12:-0.333333 13:-1 +1 1:0.291667 2:1 3:1 4:-0.566038 5:-0.525114 6:1 7:-1 8:0.358779 9:1 10:-0.548387 11:-1 12:0.333333 13:1 +1 1:0.416667 2:-1 3:1 4:-0.735849 5:-0.347032 6:-1 7:-1 8:0.496183 9:1 10:-0.419355 12:0.333333 13:-1 +1 1:0.541667 2:1 3:1 4:-0.660377 5:-0.607306 6:-1 7:1 8:-0.0687023 9:1 10:-0.967742 11:-1 12:-0.333333 13:-1 -1 1:-0.458333 2:1 3:1 4:-0.132075 5:-0.543379 6:-1 7:-1 8:0.633588 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:0.458333 2:1 3:1 4:-0.509434 5:-0.452055 6:-1 7:1 8:-0.618321 9:1 10:-0.290323 11:1 12:-0.333333 13:-1 -1 1:0.0416667 2:1 3:0.333333 4:0.0566038 5:-0.515982 6:-1 7:1 8:0.435115 9:-1 10:-0.483871 11:-1 12:-1 13:1 -1 1:-0.291667 2:-1 3:0.333333 4:-0.0943396 5:-0.767123 6:-1 7:1 8:0.358779 9:1 10:-0.548387 11:1 12:-1 13:-1 -1 1:0.583333 2:-1 3:0.333333 4:0.0943396 5:-0.310502 6:-1 7:-1 8:0.541985 9:-1 10:-1 11:-1 12:-0.333333 13:-1 +1 1:0.125 2:1 3:1 4:-0.415094 5:-0.438356 6:1 7:1 8:0.114504 9:1 10:-0.612903 12:-0.333333 13:-1 -1 1:-0.791667 2:-1 3:-0.333333 4:-0.54717 5:-0.616438 6:-1 7:-1 8:0.847328 9:-1 10:-0.774194 11:-1 12:-1 13:-1 -1 1:0.166667 2:1 3:1 4:-0.283019 5:-0.630137 6:-1 7:-1 8:0.480916 9:1 10:-1 11:-1 12:-1 13:1 +1 1:0.458333 2:1 3:1 4:-0.0377358 5:-0.607306 6:-1 7:1 8:-0.0687023 9:-1 10:-0.354839 12:0.333333 13:0.5 -1 1:0.25 2:1 3:1 4:-0.169811 5:-0.3379 6:-1 7:1 8:0.694656 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:-0.125 2:1 3:0.333333 4:-0.132075 5:-0.511416 6:-1 7:-1 8:0.40458 9:-1 10:-0.806452 12:-0.333333 13:1 -1 1:-0.0833333 2:1 3:-1 4:-0.415094 5:-0.60274 6:-1 7:1 8:-0.175573 9:1 10:-0.548387 11:-1 12:-0.333333 13:-1 +1 1:0.0416667 2:1 3:-0.333333 4:0.849057 5:-0.283105 6:-1 7:1 8:0.89313 9:-1 10:-1 11:-1 12:-0.333333 13:1 +1 2:1 3:1 4:-0.45283 5:-0.287671 6:-1 7:-1 8:-0.633588 9:1 10:-0.354839 12:0.333333 13:1 +1 1:-0.0416667 2:1 3:1 4:-0.660377 5:-0.525114 6:-1 7:-1 8:0.358779 9:-1 10:-1 11:-1 12:-0.333333 13:-1 +1 1:-0.541667 2:1 3:1 4:-0.698113 5:-0.812785 6:-1 7:1 8:-0.343511 9:1 10:-0.354839 12:-1 13:1 +1 1:0.208333 2:1 3:0.333333 4:-0.283019 5:-0.552511 6:-1 7:1 8:0.557252 9:-1 10:0.0322581 11:-1 12:0.333333 13:1 -1 1:-0.5 2:-1 3:0.333333 4:-0.660377 5:-0.351598 6:-1 7:1 8:0.541985 9:1 10:-1 11:-1 12:-1 13:-1 -1 1:-0.5 2:1 3:0.333333 4:-0.660377 5:-0.43379 6:-1 7:-1 8:0.648855 9:-1 10:-1 11:-1 12:-1 13:-1 -1 1:-0.125 2:-1 3:0.333333 4:-0.509434 5:-0.575342 6:-1 7:-1 8:0.328244 9:-1 10:-0.483871 12:-1 13:-1 -1 1:0.0416667 2:-1 3:0.333333 4:-0.735849 5:-0.356164 6:-1 7:1 8:0.465649 9:-1 10:-1 11:-1 12:-1 13:-1 -1 1:0.458333 2:-1 3:1 4:-0.320755 5:-0.191781 6:-1 7:-1 8:-0.221374 9:-1 10:-0.354839 12:0.333333 13:-1 -1 1:-0.0833333 2:-1 3:0.333333 4:-0.320755 5:-0.406393 6:-1 7:1 8:0.19084 9:-1 10:-0.83871 11:-1 12:-1 13:-1 -1 1:-0.291667 2:-1 3:-0.333333 4:-0.792453 5:-0.643836 6:-1 7:-1 8:0.541985 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:0.0833333 2:1 3:1 4:-0.132075 5:-0.584475 6:-1 7:-1 8:-0.389313 9:1 10:0.806452 11:1 12:-1 13:1 -1 1:-0.333333 2:1 3:-0.333333 4:-0.358491 5:-0.16895 6:-1 7:1 8:0.51145 9:-1 10:-1 11:-1 12:-1 13:-1 -1 1:0.125 2:1 3:-1 4:-0.509434 5:-0.694064 6:-1 7:1 8:0.389313 9:-1 10:-0.387097 12:-1 13:1 +1 1:0.541667 2:-1 3:1 4:0.584906 5:-0.534247 6:1 7:-1 8:0.435115 9:1 10:-0.677419 12:0.333333 13:1 +1 1:-0.625 2:1 3:-1 4:-0.509434 5:-0.520548 6:-1 7:-1 8:0.694656 9:1 10:0.225806 12:-1 13:1 +1 1:0.375 2:-1 3:1 4:0.0566038 5:-0.461187 6:-1 7:-1 8:0.267176 9:1 10:-0.548387 12:-1 13:-1 -1 1:0.0833333 2:1 3:-0.333333 4:-0.320755 5:-0.378995 6:-1 7:-1 8:0.282443 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:0.208333 2:1 3:1 4:-0.358491 5:-0.392694 6:-1 7:1 8:-0.0992366 9:1 10:-0.0322581 12:0.333333 13:1 -1 1:-0.416667 2:1 3:1 4:-0.698113 5:-0.611872 6:-1 7:-1 8:0.374046 9:-1 10:-1 11:-1 12:-1 13:1 -1 1:0.458333 2:-1 3:1 4:0.622642 5:-0.0913242 6:-1 7:-1 8:0.267176 9:1 10:-1 11:-1 12:-1 13:-1 -1 1:-0.125 2:-1 3:1 4:-0.698113 5:-0.415525 6:-1 7:1 8:0.343511 9:-1 10:-1 11:-1 12:-1 13:-1 -1 2:1 3:0.333333 4:-0.320755 5:-0.675799 6:1 7:1 8:0.236641 9:-1 10:-0.612903 11:1 12:-1 13:-1 -1 1:-0.333333 2:-1 3:1 4:-0.169811 5:-0.497717 6:-1 7:1 8:0.236641 9:1 10:-0.935484 12:-1 13:-1 +1 1:0.5 2:1 3:-1 4:-0.169811 5:-0.287671 6:1 7:1 8:0.572519 9:-1 10:-0.548387 12:-0.333333 13:-1 -1 1:0.666667 2:1 3:-1 4:0.245283 5:-0.506849 6:1 7:1 8:-0.0839695 9:-1 10:-0.967742 12:-0.333333 13:-1 +1 1:0.666667 2:1 3:0.333333 4:-0.132075 5:-0.415525 6:-1 7:1 8:0.145038 9:-1 10:-0.354839 12:1 13:1 +1 1:0.583333 2:1 3:1 4:-0.886792 5:-0.210046 6:-1 7:1 8:-0.175573 9:1 10:-0.709677 12:0.333333 13:-1 -1 1:0.625 2:-1 3:0.333333 4:-0.509434 5:-0.611872 6:-1 7:1 8:-0.328244 9:-1 10:-0.516129 12:-1 13:-1 -1 1:-0.791667 2:1 3:-1 4:-0.54717 5:-0.744292 6:-1 7:1 8:0.572519 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:0.375 2:-1 3:1 4:-0.169811 5:-0.232877 6:1 7:-1 8:-0.465649 9:-1 10:-0.387097 12:1 13:-1 +1 1:-0.0833333 2:1 3:1 4:-0.132075 5:-0.214612 6:-1 7:-1 8:-0.221374 9:1 10:0.354839 12:1 13:1 +1 1:-0.291667 2:1 3:0.333333 4:0.0566038 5:-0.520548 6:-1 7:-1 8:0.160305 9:-1 10:0.16129 12:-1 13:-1 +1 1:0.583333 2:1 3:1 4:-0.415094 5:-0.415525 6:1 7:-1 8:0.40458 9:-1 10:-0.935484 12:0.333333 13:1 -1 1:-0.125 2:1 3:0.333333 4:-0.339623 5:-0.680365 6:-1 7:-1 8:0.40458 9:-1 10:-1 11:-1 12:-1 13:-1 -1 1:-0.458333 2:1 3:0.333333 4:-0.509434 5:-0.479452 6:1 7:-1 8:0.877863 9:-1 10:-0.741935 11:1 12:-1 13:1 +1 1:0.125 2:-1 3:1 4:-0.245283 5:0.292237 6:-1 7:1 8:0.206107 9:1 10:-0.387097 12:0.333333 13:1 +1 1:-0.5 2:1 3:1 4:-0.698113 5:-0.789954 6:-1 7:1 8:0.328244 9:-1 10:-1 11:-1 12:-1 13:1 -1 1:-0.458333 2:-1 3:1 4:-0.849057 5:-0.365297 6:-1 7:1 8:-0.221374 9:-1 10:-0.806452 12:-1 13:-1 -1 2:1 3:0.333333 4:-0.320755 5:-0.452055 6:1 7:1 8:0.557252 9:-1 10:-1 11:-1 12:1 13:-1 -1 1:-0.416667 2:1 3:0.333333 4:-0.320755 5:-0.136986 6:-1 7:-1 8:0.389313 9:-1 10:-0.387097 11:-1 12:-0.333333 13:-1 +1 1:0.125 2:1 3:1 4:-0.283019 5:-0.73516 6:-1 7:1 8:-0.480916 9:1 10:-0.322581 12:-0.333333 13:0.5 -1 1:-0.0416667 2:1 3:1 4:-0.735849 5:-0.511416 6:1 7:-1 8:0.160305 9:-1 10:-0.967742 11:-1 12:1 13:1 -1 1:0.375 2:-1 3:1 4:-0.132075 5:0.223744 6:-1 7:1 8:0.312977 9:-1 10:-0.612903 12:-1 13:-1 +1 1:0.708333 2:1 3:0.333333 4:0.245283 5:-0.347032 6:-1 7:-1 8:-0.374046 9:1 10:-0.0645161 12:-0.333333 13:1 -1 1:0.0416667 2:1 3:1 4:-0.132075 5:-0.484018 6:-1 7:-1 8:0.358779 9:-1 10:-0.612903 11:-1 12:-1 13:-1 +1 1:0.708333 2:1 3:1 4:-0.0377358 5:-0.780822 6:-1 7:-1 8:-0.175573 9:1 10:-0.16129 11:1 12:-1 13:1 -1 1:0.0416667 2:1 3:-0.333333 4:-0.735849 5:-0.164384 6:-1 7:-1 8:0.29771 9:-1 10:-1 11:-1 12:-1 13:1 +1 1:-0.75 2:1 3:1 4:-0.396226 5:-0.287671 6:-1 7:1 8:0.29771 9:1 10:-1 11:-1 12:-1 13:1 -1 1:-0.208333 2:1 3:0.333333 4:-0.433962 5:-0.410959 6:1 7:-1 8:0.587786 9:-1 10:-1 11:-1 12:0.333333 13:-1 -1 1:0.0833333 2:-1 3:-0.333333 4:-0.226415 5:-0.43379 6:-1 7:1 8:0.374046 9:-1 10:-0.548387 12:-1 13:-1 -1 1:0.208333 2:-1 3:1 4:-0.886792 5:-0.442922 6:-1 7:1 8:-0.221374 9:-1 10:-0.677419 12:-1 13:-1 -1 1:0.0416667 2:-1 3:0.333333 4:-0.698113 5:-0.598174 6:-1 7:-1 8:0.328244 9:-1 10:-0.483871 12:-1 13:-1 -1 1:0.666667 2:-1 3:-1 4:-0.132075 5:-0.484018 6:-1 7:-1 8:0.221374 9:-1 10:-0.419355 11:-1 12:0.333333 13:-1 +1 1:1 2:1 3:1 4:-0.415094 5:-0.187215 6:-1 7:1 8:0.389313 9:1 10:-1 11:-1 12:1 13:-1 -1 1:0.625 2:1 3:0.333333 4:-0.54717 5:-0.310502 6:-1 7:-1 8:0.221374 9:-1 10:-0.677419 11:-1 12:-0.333333 13:1 +1 1:0.208333 2:1 3:1 4:-0.415094 5:-0.205479 6:-1 7:1 8:0.526718 9:-1 10:-1 11:-1 12:0.333333 13:1 +1 1:0.291667 2:1 3:1 4:-0.415094 5:-0.39726 6:-1 7:1 8:0.0687023 9:1 10:-0.0967742 12:-0.333333 13:1 +1 1:-0.0833333 2:1 3:1 4:-0.132075 5:-0.210046 6:-1 7:-1 8:0.557252 9:1 10:-0.483871 11:-1 12:-1 13:1 +1 1:0.0833333 2:1 3:1 4:0.245283 5:-0.255708 6:-1 7:1 8:0.129771 9:1 10:-0.741935 12:-0.333333 13:1 -1 1:-0.0416667 2:1 3:-1 4:0.0943396 5:-0.214612 6:1 7:-1 8:0.633588 9:-1 10:-0.612903 12:-1 13:1 -1 1:0.291667 2:-1 3:0.333333 4:-0.849057 5:-0.123288 6:-1 7:-1 8:0.358779 9:-1 10:-1 11:-1 12:-0.333333 13:-1 -1 1:0.208333 2:1 3:0.333333 4:-0.792453 5:-0.479452 6:-1 7:1 8:0.267176 9:1 10:-0.806452 12:-1 13:1 +1 1:0.458333 2:1 3:0.333333 4:-0.415094 5:-0.164384 6:-1 7:-1 8:-0.0839695 9:1 10:-0.419355 12:-1 13:1 -1 1:-0.666667 2:1 3:0.333333 4:-0.320755 5:-0.43379 6:-1 7:-1 8:0.770992 9:-1 10:0.129032 11:1 12:-1 13:-1 +1 1:0.25 2:1 3:-1 4:0.433962 5:-0.260274 6:-1 7:1 8:0.343511 9:-1 10:-0.935484 12:-1 13:1 -1 1:-0.0833333 2:1 3:0.333333 4:-0.415094 5:-0.456621 6:1 7:1 8:0.450382 9:-1 10:-0.225806 12:-1 13:-1 -1 1:-0.416667 2:-1 3:0.333333 4:-0.471698 5:-0.60274 6:-1 7:-1 8:0.435115 9:-1 10:-0.935484 12:-1 13:-1 +1 1:0.208333 2:1 3:1 4:-0.358491 5:-0.589041 6:-1 7:1 8:-0.0839695 9:1 10:-0.290323 12:1 13:1 -1 1:-1 2:1 3:-0.333333 4:-0.320755 5:-0.643836 6:-1 7:1 8:1 9:-1 10:-1 11:-1 12:-1 13:-1 -1 1:-0.5 2:-1 3:-0.333333 4:-0.320755 5:-0.643836 6:-1 7:1 8:0.541985 9:-1 10:-0.548387 11:-1 12:-1 13:-1 -1 1:0.416667 2:-1 3:0.333333 4:-0.226415 5:-0.424658 6:-1 7:1 8:0.541985 9:-1 10:-1 11:-1 12:-1 13:-1 -1 1:-0.0833333 2:1 3:0.333333 4:-1 5:-0.538813 6:-1 7:-1 8:0.267176 9:1 10:-1 11:-1 12:-0.333333 13:1 -1 1:0.0416667 2:1 3:0.333333 4:-0.509434 5:-0.39726 6:-1 7:1 8:0.160305 9:-1 10:-0.870968 12:-1 13:1 -1 1:-0.375 2:1 3:-0.333333 4:-0.509434 5:-0.570776 6:-1 7:-1 8:0.51145 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:0.0416667 2:1 3:1 4:-0.698113 5:-0.484018 6:-1 7:-1 8:-0.160305 9:1 10:-0.0967742 12:-0.333333 13:1 +1 1:0.5 2:1 3:1 4:-0.226415 5:-0.415525 6:-1 7:1 8:-0.145038 9:-1 10:-0.0967742 12:-0.333333 13:1 -1 1:0.166667 2:1 3:0.333333 4:0.0566038 5:-0.808219 6:-1 7:-1 8:0.572519 9:-1 10:-0.483871 11:-1 12:-1 13:-1 +1 1:0.416667 2:1 3:1 4:-0.320755 5:-0.0684932 6:1 7:1 8:-0.0687023 9:1 10:-0.419355 11:-1 12:1 13:1 -1 1:-0.75 2:-1 3:1 4:-0.169811 5:-0.739726 6:-1 7:-1 8:0.694656 9:-1 10:-0.548387 11:-1 12:-1 13:-1 -1 1:-0.5 2:1 3:-0.333333 4:-0.226415 5:-0.648402 6:-1 7:-1 8:-0.0687023 9:-1 10:-1 12:-1 13:0.5 +1 1:0.375 2:-1 3:0.333333 4:-0.320755 5:-0.374429 6:-1 7:-1 8:-0.603053 9:-1 10:-0.612903 12:-0.333333 13:1 +1 1:-0.416667 2:-1 3:1 4:-0.283019 5:-0.0182648 6:1 7:1 8:-0.00763359 9:1 10:-0.0322581 12:-1 13:1 -1 1:0.208333 2:-1 3:-1 4:0.0566038 5:-0.283105 6:1 7:1 8:0.389313 9:-1 10:-0.677419 11:-1 12:-1 13:-1 -1 1:-0.0416667 2:1 3:-1 4:-0.54717 5:-0.726027 6:-1 7:1 8:0.816794 9:-1 10:-1 12:-1 13:0.5 +1 1:0.333333 2:-1 3:1 4:-0.0377358 5:-0.173516 6:-1 7:1 8:0.145038 9:1 10:-0.677419 12:-1 13:1 +1 1:-0.583333 2:1 3:1 4:-0.54717 5:-0.575342 6:-1 7:-1 8:0.0534351 9:-1 10:-0.612903 12:-1 13:1 -1 1:-0.333333 2:1 3:1 4:-0.603774 5:-0.388128 6:-1 7:1 8:0.740458 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:-0.0416667 2:1 3:1 4:-0.358491 5:-0.410959 6:-1 7:-1 8:0.374046 9:1 10:-1 11:-1 12:-0.333333 13:1 -1 1:0.375 2:1 3:0.333333 4:-0.320755 5:-0.520548 6:-1 7:-1 8:0.145038 9:-1 10:-0.419355 12:1 13:1 +1 1:0.375 2:-1 3:1 4:0.245283 5:-0.826484 6:-1 7:1 8:0.129771 9:-1 10:1 11:1 12:1 13:1 -1 2:-1 3:1 4:-0.169811 5:-0.506849 6:-1 7:1 8:0.358779 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:-0.416667 2:1 3:1 4:-0.509434 5:-0.767123 6:-1 7:1 8:-0.251908 9:1 10:-0.193548 12:-1 13:1 -1 1:-0.25 2:1 3:0.333333 4:-0.169811 5:-0.401826 6:-1 7:1 8:0.29771 9:-1 10:-1 11:-1 12:-1 13:-1 -1 1:-0.0416667 2:1 3:-0.333333 4:-0.509434 5:-0.0913242 6:-1 7:-1 8:0.541985 9:-1 10:-0.935484 11:-1 12:-1 13:-1 +1 1:0.625 2:1 3:0.333333 4:0.622642 5:-0.324201 6:1 7:1 8:0.206107 9:1 10:-0.483871 12:-1 13:1 -1 1:-0.583333 2:1 3:0.333333 4:-0.132075 5:-0.109589 6:-1 7:1 8:0.694656 9:-1 10:-1 11:-1 12:-1 13:-1 -1 2:-1 3:1 4:-0.320755 5:-0.369863 6:-1 7:1 8:0.0992366 9:-1 10:-0.870968 12:-1 13:-1 +1 1:0.375 2:-1 3:1 4:-0.132075 5:-0.351598 6:-1 7:1 8:0.358779 9:-1 10:0.16129 11:1 12:0.333333 13:-1 -1 1:-0.0833333 2:-1 3:0.333333 4:-0.132075 5:-0.16895 6:-1 7:1 8:0.0839695 9:-1 10:-0.516129 11:-1 12:-0.333333 13:-1 +1 1:0.291667 2:1 3:1 4:-0.320755 5:-0.420091 6:-1 7:-1 8:0.114504 9:1 10:-0.548387 11:-1 12:-0.333333 13:1 +1 1:0.5 2:1 3:1 4:-0.698113 5:-0.442922 6:-1 7:1 8:0.328244 9:-1 10:-0.806452 11:-1 12:0.333333 13:0.5 -1 1:0.5 2:-1 3:0.333333 4:0.150943 5:-0.347032 6:-1 7:-1 8:0.175573 9:-1 10:-0.741935 11:-1 12:-1 13:-1 +1 1:0.291667 2:1 3:0.333333 4:-0.132075 5:-0.730594 6:-1 7:1 8:0.282443 9:-1 10:-0.0322581 12:-1 13:-1 +1 1:0.291667 2:1 3:1 4:-0.0377358 5:-0.287671 6:-1 7:1 8:0.0839695 9:1 10:-0.0967742 12:0.333333 13:1 +1 1:0.0416667 2:1 3:1 4:-0.509434 5:-0.716895 6:-1 7:-1 8:-0.358779 9:-1 10:-0.548387 12:-0.333333 13:1 -1 1:-0.375 2:1 3:-0.333333 4:-0.320755 5:-0.575342 6:-1 7:1 8:0.78626 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:-0.375 2:1 3:1 4:-0.660377 5:-0.251142 6:-1 7:1 8:0.251908 9:-1 10:-1 11:-1 12:-0.333333 13:-1 -1 1:-0.0833333 2:1 3:0.333333 4:-0.698113 5:-0.776256 6:-1 7:-1 8:-0.206107 9:-1 10:-0.806452 11:-1 12:-1 13:-1 -1 1:0.25 2:1 3:0.333333 4:0.0566038 5:-0.607306 6:1 7:-1 8:0.312977 9:-1 10:-0.483871 11:-1 12:-1 13:-1 -1 1:0.75 2:-1 3:-0.333333 4:0.245283 5:-0.196347 6:-1 7:-1 8:0.389313 9:-1 10:-0.870968 11:-1 12:0.333333 13:-1 -1 1:0.333333 2:1 3:0.333333 4:0.0566038 5:-0.465753 6:1 7:-1 8:0.00763359 9:1 10:-0.677419 12:-1 13:-1 +1 1:0.0833333 2:1 3:1 4:-0.283019 5:0.0365297 6:-1 7:-1 8:-0.0687023 9:1 10:-0.612903 12:-0.333333 13:1 +1 1:0.458333 2:1 3:0.333333 4:-0.132075 5:-0.0456621 6:-1 7:-1 8:0.328244 9:-1 10:-1 11:-1 12:-1 13:-1 -1 1:-0.416667 2:1 3:1 4:0.0566038 5:-0.447489 6:-1 7:-1 8:0.526718 9:-1 10:-0.516129 11:-1 12:-1 13:-1 -1 1:0.208333 2:-1 3:0.333333 4:-0.509434 5:-0.0228311 6:-1 7:-1 8:0.541985 9:-1 10:-1 11:-1 12:-1 13:-1 +1 1:0.291667 2:1 3:1 4:-0.320755 5:-0.634703 6:-1 7:1 8:-0.0687023 9:1 10:-0.225806 12:0.333333 13:1 +1 1:0.208333 2:1 3:-0.333333 4:-0.509434 5:-0.278539 6:-1 7:1 8:0.358779 9:-1 10:-0.419355 12:-1 13:-1 -1 1:-0.166667 2:1 3:-0.333333 4:-0.320755 5:-0.360731 6:-1 7:-1 8:0.526718 9:-1 10:-0.806452 11:-1 12:-1 13:-1 +1 1:-0.208333 2:1 3:-0.333333 4:-0.698113 5:-0.52968 6:-1 7:-1 8:0.480916 9:-1 10:-0.677419 11:1 12:-1 13:1 -1 1:-0.0416667 2:1 3:0.333333 4:0.471698 5:-0.666667 6:1 7:-1 8:0.389313 9:-1 10:-0.83871 11:-1 12:-1 13:1 -1 1:-0.375 2:1 3:-0.333333 4:-0.509434 5:-0.374429 6:-1 7:-1 8:0.557252 9:-1 10:-1 11:-1 12:-1 13:1 -1 1:0.125 2:-1 3:-0.333333 4:-0.132075 5:-0.232877 6:-1 7:1 8:0.251908 9:-1 10:-0.580645 12:-1 13:-1 -1 1:0.166667 2:1 3:1 4:-0.132075 5:-0.69863 6:-1 7:-1 8:0.175573 9:-1 10:-0.870968 12:-1 13:0.5 +1 1:0.583333 2:1 3:1 4:0.245283 5:-0.269406 6:-1 7:1 8:-0.435115 9:1 10:-0.516129 12:1 13:-1 statistics-release-2.1.2/inst/datasets/kmeansdata.mat000066400000000000000000000430551527311104300227300ustar00rootroot00000000000000Octave-1-LXÿmatrixþÿÿÿ0ë'_!u8 Àß(Bm¶ À¿0.îÃàû¿rV-äs¶ ÀÕˆàFÓâý¿kãX”×À�ò¹ÒÀEs�¡½ëÀ˜“aàB ÀjŸE…uØ À白�“ˆÀB–¢n ðÀ›·†Œj ú¿‚U–+ÔÉ ÀO…ȰîÀp9*»Rû¿ØüÞ£Ò ÀÃz’MÀ²;@|‡›Àwp¶ 8Àæ…[ÑoŽ ÀhÛ r)ýý¿e!Ë/ûÓ ÀÕ†c ÎdÀÔ2ö˜óVâ¿ÎgÞuôþ¿r™nh û¿0bZ¬Ò² À`ÖàõfãÀ~¬Ÿ ˆoÀ—$;ˆÀz¶Ô«²ÛÀIKA–àÀd¶wB÷ô¿îÄg\ÿ¿V·ušŽú ÀßFÊyœÿ¿Òáè:› Àª¦ÊGc�ÀY(ÁÙ¼= À�ã¢õ¼À¢¢Å¹³Vï¿1ûÞBÏÀTΡG‰!À:‡ý¾˜õ¿gº-ÁÀ2¸Žý…Wÿ¿N}Hì6òÀ(’éFI(À¹÷9µG ÀÕ5jö ñ¿¥éðRòÀ" ÊbÀÂj䣱Àê—«KùžÀÂá<‘ßÀ ­¹vg¿ÀÜ·�ˆöãÀñ�9ÈÀb�VÕÀ€OZSc˜Àgê ©öŒÀhª¦û˜ÎÀ|/OËgÀ‹ácð•áÀ?O¸ }ÀøhclÖVþ¿Õ|4+bÀó–†ÉÑÀ÷»Î�11 À˜u2ê*ÀÚ÷,ÉÛÀ`ÚfÅvÀRøè7ÝÀ†Töež5ý¿ÜÒ~©nÀ^oQi¬À$Q]ÈÍÀ⇣µyÈÀ  Ùæ*@¦ãÀvA @K¦vë3@ì&ÖžL�@2ÈŸOÃ@¶bÊ2Ñ@ êš‚¯@h «Vª³î?%"VˆN @Zpï¸ùgý?Å�OÄ<@ FÌö ƒ@’7ÂÐì@úÚ‘ˆ@Iw6Ø@M�ïµ @hïÉv¾– @x;Ø®@¯;Uÿ"ú@´ðÆ.@|žA•¿0@&½ÖUˆàõ?D¡*2zN@5\þB "ü?ªãJ|šý?BŒ™­Ûz @ ÁJa·Qÿ?ò®C~@‘pã¹�§@zdÂè~@Èý\rÅÏÕ?‡Á÷ÔE@Ö—z<_ù?¶‹r7Ø@ˆë%¯„@X¶ò·¥@êº?qm@LÊ|Ç·@'uc%™@¦Jë—F@.Éñ|Ì@ÆË¬ìÁ(@*éàíºF@%ÓKoqh@Ù7Ãj»@[Ýi#�c@L^µK@„^æk_¤@>~ƒÑ ˜@pG" ê@z6àÙ©0@\È3«†Xÿ?Jõ#2�û?X[¹ìr»÷?ïEhÍ�÷?gª¿~Î@Çíß›ó@ؼà3ƒW @†‘†p€Ò@ºÔí·~s@:ýc¿{õ?üÀöšàÿ?¯ÉO9“oñ?ö�¡…c—@L(÷—- @H9­{® @nâ¹?s @{‚,ëÛ‡ @NC’T£/ @²˜³æuû?—ÿ­>k½ @G*‹Æ"@P³†ø@ áþžû @Ÿò#Á_þ?Kµ ¯\5@Qä,ˆE.@P’¹¦eÆ@Þæ´t b@—4á¦d�þ?¬Ð|‚-@}Aôí @Šþ¨Z/´ @ç¶x…@fzÎÖ·&@ªœEPÂ@˜ÛؼHõ @°8åÆ @Mf‚þý@îP¢}-ûù?ç6 µ^@ì‚þG‹1 @–j³È¸› @B2ÍD!ÿ? ²ß@Ê<>ß@ÏdF%V@2¿ñ.t@$Á±’soæ?ôÍö,ãÌ@îg÷0 @úO€½@à¾6Ý @w=´šŠƒ@å+óSÁ@¼pv½7#@:�:Õ÷!ù?q±V²4@Ù�,^@p¦ê”Ǩî?h¥¹¶åú@(g½ y@'¯1Ã5 @<`RFœÆý?k"ZL  @R,Œp)@Þ—…€ @šø:Œ @ù_‚]Œ@\qe�b@[¥%ª³@(äYsͧ@‘÷oæá¥ú?Þq>¶@à@HMåŠø?Øž( �' @1˜ëœðô@í4“;= @ÞÜ„ª(ë?ÔÓ]Î á?ô„Þ›Ë@ã’ò©ú@$½qš?õ?7G—•§² @bøU¯ @I@Â;ƒÖ@m¸"Çãü?Œ£�ÚkL@Övt}±ˆ @4ÁIõ–¢ @dBÅ€ÅÄ @XZq¶ _@a91 @1>XÕdü?Âë?@Û ÚO@w‘»¢ @@ý\! @ †Û´j @ü‡z¯¹@Ašùó�ð@ª%®%-ý?{jóF@L|Ò?Ë?@àÉï�~J @9=qÚuý?¥Ì_î±@¨_ ˜þ@Ð2^ÉÝò@i%ñŒô@�ß(Ë¥@èɹ‰ÆÑ?Þžn™• @tÓí`‰7@Uä`q‡@\Fê* @æ‹\t@+K«DçR@ÑBË.– @#\?³œ@T¶Á#$cÿ?Ú^åõ®@üÒò©Ó @ªÕ.«ÉÝô?pvú *¿@µIÔIK @º¤”;Ô@“iäîš”û?È¥…@ {?„„@®ŒÞBc¾ @i˜øÃ€ @Ü;Ô¬Š @$©™i¢ @’cY>¿C@³6ØbN@ ¼Àº(o @¤ÕØç3@L‚bM?@©�Ftãxþ?"Í™Zt" @Nú˜¯4[ù?w8,Gñ¿@› ª¼'@H�’x¹ƒ@è¶H`m˜þ?5¹jŽÄ@ákœ2ö)@ ÖØ÷ÞA@z/·|ï?Ö±Ì,ó³ù¿j9îq@À68“€C ÀŽþ_¼¸ À,¼#²ÙZÀ0¸()šØ À«Ì&âŽDÀO Zyu‘Ày_aAh¢ÀBgXw|$ÀL/Õ|Þ ÀæŸÏ*ºh Àó©Ã…$Àó)ÁD/ À1w÷O˜ü¿‡�€Nœ ÀºwæîÊ™ÀEŽ8DI ÀמÔV}À sÆãêÀ �/Šsg鿹€ðòÀȶŒÈç”À¬„u9"ÀGÉX Ÿ ÀðT-71ÀbÎ äÀh¶º û¿R~ XW�À±„Ë+ ÀÏP/)µ;ô¿²—W ½À Ô«3™Ùý¿D[ëuÀ¼É‚ÞÀ`½·†ýùü¿ Ñý³ð$À€ƒãðÍèÀ@'§Q Àù7äÒ%…ü¿((§ÑãnÀ¹¡õ ÀomZxðºÀn QMÀ4¯.5 À9€M˜#À3`d Àùó´ ¬ À€”t�–<ÀkâHùðû¿Î5S¬Àåjè�¬ À¾‹Ø¾m¸ú¿è9Æh~2 ÀiºÅêí” À�1þŸ=´ÀpkÍÕa‡þ¿ÿ�0¿el ÀH…0$Öö¿j×TãZ` À×j`¢Ð8À;¥³3tQ À.üd›¼ À^4^ªMÀðfhí( ï¿;�gè|ÀбÕ�ç ÀF¶a¦À�—ÔëÏN Àbë°ÀV·ÚSbÀÔ±Þ8þ ÀÛ®î]´› À6eÀªÜÔ À€†’ðý\«¿µ÷#œûÛÀ4ÉÈTcÀëä vU À`ð+ê´ø¿ïŠnÀÆü7²ø¿÷V�Ùô÷ú¿xŸbƒWDÀ— ¯ð—ŸÀΜöyÉO ÀV7ÍŒ§À´y¹ÂÀºævx—À¸°¬ ÀáˆäSî| ÀB5/ÕK.ÀSLÕ®ã¿Æ6ÒOÀÃҼǘ{Àê• Ç�.Àò+TN¢dÀTÄyvžcç¿ÛŒG¨ÇÀcQg�?À­I ¼ø¿Þ¯þ—¥À¶–_W À�8 —ÆÀùÈ<ÀEzùÀQÀÎúÐ@¬\ À©,ª¿À(ÔmåÖ俼_mÔ”!À‹~]`ìÉÀÓ ›ˆš!ÀÛzYg�À ‹136bÀ0¡KZ¥Ö À€&§å—eÀؚξvI À‹Æ£ßt'ÀRúQaãþ Àz%}¥™ÜÀ_ùi5À DmKxÀbZSù|_À62L’ûÀ0Ïp½5ŽÀ±C9nÀ6ãßL%BÀ“¶[¶! ÀUT1ó�“Àb‘¯GŒ1ÀZï7pèÀ{�êì À迾 kâÀ×ð޳À„‰ŒgUÀ%©ÃžñlÀ’ Ö¬h$ À†»™øDR À ?�úÀ�9u5»ÀôçÞ½–{ À<™+à À÷¶£#,À)ô®–ÀƒZsîŸÀ�;ûU”ZÀ·Ìl·ý•Àsª�è À;í9ÁÀd&ðuã"Àþ|×aÚÿÀ…à§iT@DmÝJ¹@ú$ã6%€@B�é"% @�Ú–¬ƒ%ÿ?…©¶‡j@øXÊ4 @`®ù�[@bkò"µ[@žo©D¤Ý@®>þŸ@ÃR…ƒ-ü?�¹tIçŒ@×#‹u @¿ÂÖñ¼Ûû?±O0/ˆö?ܧóÖKW@+v.ô-@š g�Ÿº @~A”¡P†@替h @¨(fT']@ØÞ¢#�@_r¯ o…@ js­€ô?À|ÆÂÐã?Þ„2ªj@$ŠÀ™�À@~›ÑåÍN @ ?¡€ëø?{ã.ÐG@hþ, •@8j¹%@¢ÌcŠq@Ó°wÓ/0@ Õ ÔOü?ýæJùý?=¤’î6¾@‚/²6’Š@ݤëÝ5@®r-mµÿ?ö}ã[ÔR@ãÓo‰×@°ï|�µ° @ %K£ëÖ @`Weˆ¢2@?r¥û�ç@˜Å—;gÖ @TšñÇMy@ÎQÞ+7@mü¼ ù³ @޵:ÙŽ@^»óêy @Œì+[ù@½æÇñê@DùÞà Ì@¼ '9Mc @=KŇGù?(Ã@2ƒ @¢Ð²¢œ@ö¦…kªl @e›MB[V@ß$CѬñ?é?‡=¶@is^Z؈@h—R[Ķ@f:@Wô“À+@Ÿ1ÝT @VOÄPã~@¿DÈlÄ@¾E¨aì‹@ᵕ»¡@p‚sçþ3 @CúÿÕ¸@8õòäÿ?[�ËÇ @Ȫ)Ïõù?&»:>%>ÿ?7•áuµ?@éÕª[@hWù–žº@4¡8%Òá @doH%Ãÿ?·vGq‘ž@ð¸¶)÷Øû?猳\ëé@øÖÁ¿V€@¢ÀI¥’B @m¬}Ê @`*õ¸qŽú? ’„Æ Y @‰-x8”û?˜JÜðTw@J¬h\ @ï¾Ö÷@ª÷Ó¢âg@¯øå‡@ ?–ò?³ì‡ V'@@m`3s±ö?Èd<å©/ @@Zp÷+Ž @b êŸ@2TE �4@^昘)@X3ÜÖËæ@=ü¥¦@Â;>‹@óaÒ7l‚@äx+B› @(3¸‘(@mðØ|Yõ@ÎààòELý?JжiÇ@—´'©ßO@•>5ƒ¨@HgÁªVå?fÆ:7ûãþ?×A6·@б‡s@¡­ÿQ@ŠòQù…e @†#»å @,�hEM?@V€,† @Ø"¯ÉcŽ@ÌÆ¼eÅ4@†ì±›Ô @Õ�ð=¢÷? ë·üJ À3°ññùÀ�3šO� À P2Ù µÀybò¨ß1 À¤ÏKŒÀc{6À,m Ã_äá¿<ü»6ƒø¿$s�a‚ìÀ-WÞ) ÀÚ¿ÙMSò ÀÑTX�ຠÀ�Àè!g À'Ò„Q¢¸À85É©ÏÈâ¿N2ðÓ° Àºn{1› Àr6êJ\uÀï+÷‹ À"ÿJÒÓzÀßß“1vf@ý”ú–˜· @Wº5žƒ÷?òøüÛXø?ê1â…@¶‚©¾7@~â.¹½† @/€÷?2~„ë@‚A[Õ—E@UÀ(BA.þ?”Të…,7@è1ìSu @ 7›F¹@~A½Ö@…#» ÊW @Ü<¾Eò @ØQ7‚@(ñÜ$‡þ?®ûˆ]Ÿ@ p¥Æý?= <~ø?æãÊ›^rý?WƒÛ ø@È�Birl@¦Ó˜.Æ@Ô^aUH÷?3¤¼þ@ÆäÓ42ðý?5ãà—@–ùTKKÿð?"QlÃ^«@$@EQZ@BÀùæ°ý?YBh8Q @ÙécîÊù?A#*qÄ×ü?ºCO_@“ðDzpâ@„°·˜O@€=è†)@^JË:xV @+O÷o� @ÙúÚ­@„ÎD÷y.@K×pñ,ý?ï#ðÝÛ¯ @RÿNTo¢ù?^§!¥=™@&D•Ë@7йPö?{‡è Ô«ÿ?hš¤×D9é?.c&™³tó?é¶gÿÜþ?u½*†@ ÐÀÌÒ>@~À€7Ä @xôPöD.@¦¾¹3’�@Fò=>;´ò?›Øàð¦x@ŽÓMáö?É‘Òiý?ê³È(î@Hô1Æy@`àÈ‘@¬Ká)oC@ Òãÿö@Ú*1$(©@ÃG”�* @ð¯]äGOý?îz¦.ß @´Èb•œ\ @#ÿv†7@ÞÜz|¨@ öfÏ@,¬Á¿½@Ù¹BÅH@¼® ‡ð¥è?¶×�Mí?½¾&LÓ©@¸Íƒ'…ù@'dÌà@Ç Îóþ?;€Ôw@zý? puJ @º_8èß@=ñn@eßRV0Ç@Ú—ÆZV@éQ:O&@î#šg‘@æw± è1@¯1±j@Ù=G €[ @ó€6¶fäý?× äŽ{@N6¦÷?¾ìôcÞÔ @�ÑA›„2@!›ys@pSh%† @¦Ò9vÈ5@ÍþBj„ý?oÔ§Qý @t@þ‘ü@�åÜñ@ÀÖø•C@ ,&ù?"j–£ˆ�þ?Œíž^+@ÿ³°‘%õ@ºØO6)ë?Î ç`@f ¢=€@y¯töÒQ @îšG¯'Ü @Zõýî¸ @,ãŸuäø@ÌT�¸ý?^öĽQ¯ @!\ÑBXI@`Bféy@¸võöuú?`–bD°å@Ðáˆè¤ @bÌ|/;:@ÚX#„Òþ?Sñ©rá›@ÀlË˶£@©¨!ì@iµË‰A©÷?¦O]*%ò @ôV�ƒ@ñekR][@¸™û‘@ñ»WˆB @Z÷pR\?@¼aé±Q8@ª>¿W:ý?Èyjú @×RŽ>ñ÷?ÀºÁ²­´Ø?¾å÷DI @¤-%À×µ@J ˜² ‚@á?ùwV@S³/hå @gS徿ô?�ù9Бö@€Û‚#šcó?ßš�{b @¬ŽÇKv@b¬Îƒ…@|…-Ü o@¦ún´]_@9Út§@z@²gÛ|@зš¸2Á@æJ/mºý?€ &Ûk§Ú?ªT-ýãÖ @i},f(È @nêy�í{ @=|�i<@._IŰ•ÿ?=Ì8Š€@F�AŠ´lú?Q­ê'…¹ÿ?�T¿ƒ#w @ÊEÝ…Ò @Lw]œzA@¬ÔFš­@á[Ú–Dð@7€Tæ5@�¦3µX@¸éosùž÷?ÇG¹bÕv@àM?ô@ahıp@Úš¿9µ� @ä!â½\j@+gm_@:œe= @Žg³Ã£@3¶6«ü@%^&í@/vå-Qg @ âµ›Ìö?tˆÁo @2Ö:å!ü?f1á´¶ @Ðàû¾¼Ñ @¼RQô÷?t�ÝöÔù?Ú�(œø @»éz5‡@ à~÷78 @륀ƒ@JÁ“;bÂ@g&Ó®×ù?°qÀÙÙ@ŸCY+3:@’ÕÖ°Žù@k=ñóÖ@Їh/[�ÿ?, w‹D@�J0ÔÊ6 @^ôm—Ç´õ?a ]ó@m‹ Õ@þ³šGç’@€æîU ¼ @ˆy°+¾Ø@oLlgå/@ö�QòÛW @fÞžBó?ïO4&ãù?�K @3·¦“Œ @1dÏ)=6@Œ><³„·õ?Ôä~Ž’2@HñîãdÙ?j×[å¿� @RtËü|y@s/Éç[@Êcr9þm @àûÆJ Ìò?LÆ•�f‰@õy$;Rj@ÁÖ~¢G @&Çp÷«‚ú?RøK,W@n؃qǬ@k%>÷ý@ÔÊZCJ¯@Ü2™öÛ@(Ïð¬œj@öÞžI@vÆáö @ä"îé5°@¤;>°L@•õ):È@Ñv$@Í@ȬŠ�V @ÿ¶õ¬ç·@ò}§ç�ø?Ï©€õ)@Ë»¯ u @;þäi(g@¦‹N\Qò @Æj™õ{@È„¤ÅïYÿ?¬¡Í<5ù?ï¥È Çh@Š~YPvú?fX*p¯@ôdf @�ñDo|®ß?設ìòû?ƒä1ˆ…y@—ˆ+=Ê@qLII=@¤_ÌQŽ@wêTK�)@yÃ„Ì @Ìf_àR™@�ëŒ7«é?|$w~ @ÁÀN}F @z¿øxêú?&—:3Bú@oj“=ë @‘WS!@` [3”'@"|ôž†ü?ãÒX[º‰@¡HòW@æ¬n. Û @fe6‹Å7@t±Þçö?y:6¨Á@T�ÏNßú?è!�ŸI+@xá|¥üg @:“ ûLæ@•E­a|³@8 ¯Ó@nßì°*ñý?n+dÎo@÷P8>µ@â;’b@·�ÿµ @Îx“¬¥û?iŠ×-�‚@R½�GÌw@åXí—ùy@ælÜÙsÜ@ê5‚uÉPÿ?S{ï�3@Døa£ @O÷Æ’EP @Ì�^�<Í @F¨”M™ @}–"{H@VoÝ·EE@èѨt)¹ù?ÁZ§s†® @<~cK@Ìtϳòý?¹¶œ1EÄ@iÝv/RØþ?„*Tù�ó?42ËB-Cì?±[üc j@› å@ˆ¶¥DŽß?‹Êô]¤ÿ?_­¦vP@“ýe¡û?‹ï»GÓõ?½gmÞ¶#ô?ÿÙï’>Î@ùìÓ­~@½Ölao@¥™_ @¬BÈÕA @¦N³¨\ÿ?™�Z””@¼Ø“Ý^÷?ævèˆ,@sÖA©¬ù @‡±6Ù‚Û@‚®p7@¹Œ±IÒ@¯l38@"ÔÂÕ¨@Kc½fK @6FÈzãN@»ÄÌ#)@±ÓO£ô?XËGm7hð?ήÎü‚7÷?c{VG/ø@ÒdÑEX¸@ë\‡z1 @íaR­]É@’„*j @µ³ªl²a À–”ÛÓ�ø¿â'V{ Àv‡ó©ŠÕÀ�Ão žgÀ•Ó…¬ÌÐõ¿¤~ìÄýõ Àïýë�ìþ¿Ø³3 À›šÛð¾À~}¥€_À„•c… ÀX¾.ÙƒÀvÊáíä Àðþ f[öñ¿ª´C' K ÀMú$;o7û¿ÿY„bœL À�œܳÀÜ_ Vƒ¨À™Æ!ÍóeÀ\(¬~c1ö¿5i¬š~Ð Àßžòµâ7þ¿LìúKdö¿ Ù¦a«˜À |M&‹Àò«¬ÚfÈÀ{Ó‹/lÀŠkX0•}ÀÑ5á èþ¿N‘AÈú ÀhBž¨ÝËÀÊeÄ2]µÀíAsDæõ ÀýöEM™ÀjÌæŸÀ À|ªÎ`ÀïÜ.î´êÀ, ײÀyÇÿRÀÖ“š4²{ Àš¨|ª rú¿ ïŽkñ ÀnÛ¼225ÀcwÊÓa ú¿>óa�0Àbý&ëõ¿÷.©�j°ú¿^,÷ÎGKÀF¼a–õÀbìb¯Ìñì¿)d”âaj À@ë˜s8À GèóÀU#tÒÀ”Ú¶ÖàÀFŒI¶ž²ñ¿¤¢œ�>Z ÀðâÝ®*À°¹À”÷uˆÿ¿íÄÀôˆu Àó…®Xp;À‚,v°‡»À€ß¯Ûô@ÀN޵»÷¿üÈŸ~ÇÀÙ‰ÿ¿�Ϭç�ÀJâ 8 À#»u¨Â÷Àª 9MªÀjÓþë ÀEt‚¶t¯Àì=±Y/ À€åríÀN8˜ÏjÀ³Ô±øóÀ�M¢ÄÍ› ÀÛ•; sÀD1s_®À®´ên®ÿ¿Vé° ØÖÀ IN…/¨ ÀäïBG{ÙÀ0V7Ï”ÀJ½˜USSÀ�vR¦MBÀèVÈÀÃ[Vg±ºþ¿+‡ ´<£À”§#Ç9¡ã¿Üc�éÀÃp±}ê|À†6�Žâ ÀV;AívX À˜ÂSGÁ:ò¿«O€ô‚ÀÍçÅ�øÀ+"´$À}O"Îa7À SÃýâ¿ÁPíÐb À^%Þo|ÙÀl-ôNÀæ}Qù®À‘MÂ2À<û¶¼x À´˜…Kó Àç )ßmB À jAÞ� À¾…|,[?ÀB蓳 Àæ�äÕ¥ÀÙ‰­¡VÀÙ~ „è ÀûeœÜ[À|òhBQ÷ À£/#¬dÀøå}Þ·øþ¿K$ÔÄû¿öõoÐÄÀPGF�5À¨’Ï1ÊN À,k²¨­Q ÀzÏúµùÀÀïŽ =8÷¿®Ò*?Ò›ÀN�yO…„Àä�³±å¿3ñ…˜À-˜Ñ~ªÀu�ÙH ÀfŒ}íÀJF«…¼×À¤DÞØNU÷¿¹‚XÀÒDx…8ìÀ(˜GE¡ü¿J÷ n†) À&ÒVƒŽÀÎPŠÎ„û¿QaC–¹ºÀ…ý™U-ó¿{Ó5ú˜ ÀŽMü ׉ À§ÍIE^Ãø¿‚À¾Ré˜ À\%ô†¬ÀÃñ^”Ý Ày*û&éÀyì_e ¡Àjr¹ÛüÊÀ¶?»¥û±÷¿Ì–,Ä�À•šœf¤ãÀÎûZ?Vÿ¿cXhšóÀöQ9TXçÀÚ0<ÉuÀµ/ŸÂzoÀdýg³ î¿€ó} ÀM™D=Õ‹Àü[WŽ]ÓÀ Iq`° À;§€õwÀ�³Q^·0À¶’ •ÀwŒiÀS¼fÂÌñ ÀÂÑ –bQÿ¿Wß³ú¸�ÀrUpì¹xÀ?�Ù¹µ¹À@�ŠëŽ#ÀyÕeùk Àв5wÀÖßÛþÀ+¬ê†ÜÀÛ�¹#þ¿:¥ ¢úk À‘Ùã2dà À‡™ãÎ×øÀ8×î ·ÔÀ]Š÷Õ·2 @6 Pì @€òøê¿@hmÁ€TÕç?Ÿâ{àêé@¸~EŽ@ˆ36•5@ˆJdèý�ø?úªÐ?´s @f»U�þ?ÙuõKé@ÿaÑî%@0ÃÈŽZý?Hÿ`{¨” @ûÕüªUþ?ûÑ—îñ@ 9® ýF @—?Œ–ÒE@ÀlB…ø?/2è@}7ÝM‰@6K-04"@x½_ER@ÆÇýî‚@òÈŠ¬ˆø?•�@sœ@®g G@ø9v/<þ?~É ;G@@†žg·_;@šå5N,ø?gÖûþ @!r¬2©Ö@ô†T¿«Ðë?�çêSÓý@ƨüÅ/ü?Τröú'@D½½IŸ¥@ÖL×ó³9@%ùýbº@D·Õ‚à2@Tp´�v×@òÑ=Õ@VÅš°_n@ÓæNZz3@oA ]÷ó?ÿá’�] @žZ =î‰@±®¹õŸ@ÔGÍ€Î÷?ÅŠS»Óö?²5vQs"@й¹Á/I@dª^§©ê@.Òým¦û?~�w @œ§ý”^@uÜ>€@Ð)dÀ[ï?=ìŽî‰@ve*eÔ@•¹`ÿqÅ @‹›µ˜Æmù?vå3'w%@Ó¨ú @L…¿6Ú@b75#O @(sÚ @BÐݲ~Ì@Õ%LUZŒ@ìqL5sóÿ?3Ÿ­¨˜@ÑSknf@1N$m,ù@6õ`èâÿ?Ö¸°]qü?p”Ûªg@&ä§Rº@æ6nÒö@mŽ ?Û @Óÿ¸U@Ï53½ø@é�HRêD@¾•žòº@ ,ÞrËšù?4€`û§{@4«š @NH)¥R«@˜…‹lo�@ª‰ '7é@ÚÍLôÅ›@¹€ÄÑY4 @%dNå@ÊÐê¬ý²û?üæÖžŽ@âãÜdùþ?S4eL#ã@ƒd·tLÊ@ý)»ÝÒø?|ÍC@Ö¨í"¢ @ÅwÙ75@=t’9(¡ @pä×Èkî @�%*¿ý?6¨ÃíQú?ðž5/›@r•ñv92@1—×®¢ @-}c”ò×õ?ô[ëaJ.@s]ƒÆ @p�µèÃ÷@‰¦ŸÆ’ö?PÖ¡,“¥@ éÛ¶o¦ @C"Ð|åÀ@ר¤v}@ÏÓžQ% @twk˜ � @ÐÁÝv!R@öÿ«¥Õþ?9Žá=õö?ß*ªWk@1´QÆPª@ØÓßÒ.Ç@ªµ'|Þ@gi[B@dÚSÁ@h,èPªô?wV§ª´£@ÿ¬ü²Äú?A#–ÈÄ'@Ð~6eÄ @n¯2σ @`mËuÓY @èãr§V6ù?lZ£àc @™(ä¹× @ú¼Šgü[@HéüK�@¨ø½.‡Š @•ô@Í»@@Ð)Ÿ“ê?¾êš¹ì×@ øÕˆ9& @Z¦B<¯‘@7 u @8M“·òý@Å01i—z@Òqbv•F @Ö™Únãü?rS4Šù6 @¯!?ðŸ@¿wRþã@lœ”ùñ]ö?Â)Î�A@òÖgñr @è~¬Ìý@ÌÉ–ùÎŽ@¦l¸T!@¢À?P=ð?6Òô*ô@Ä>êÔÚ¯ @Œ„fKx| @`ªQW @F+¥Ç›V @X•9úc@y<}Q@ÏË_È @5—_”ž@œ·žÑ–)@ÎúŠôNÞü?ê÷ã¹ýû?u ÂZÁô?py†¸æ@âôÔûר@&Íœ|ø?XuB0cø@aÅÍ%bæ@êVLÕXº@ô�n×ç @÷W›‡o @àWÔú«ƒ @FÐO"h@6b8T†@1ôÃ/Rø?gQ@=S @ö�’yŒý?Ê7;iåþ÷?ö@ë¤)Í@^ˆLŪ @o_�U‚,@{†•‹ @;ª¨ß×z@S[2(@Ÿ>V²T@o ¯¨º%@ŒáœJÖµ @Ê¿—K\@n¼‚ç Àt(McÊÀ\p›UÛßÀ=vT€IÀPZn“- À.ŸŽÔ>ªÀ~`3ÛzÞÀ¸ØxûZ ÀÅ®`#þ¿ê“žx[è ÀrîF«…ÆÀ B�öA ÀV!¢ú$‡ ÀŸâyíšÀó­—<ÀC®!…À£Òí@ÀÜßgž™þÀ¶õ¢º¸Àx+7ÏLŽ ÀC?G2wéð¿" ‡"öÀ€˜ˆ‹$À}öoï›cü¿iÛJ îÀañN¨Rtÿ¿ý! ì§¤ñ¿»¡ªÈAÄö¿.kl=GçÀ6Óó`·ÀP÷Âà­²ô¿4s<|âÀ ø›>1 À6ÏU`†ÀûN>ãzÀµ29·™�ÀX–È�ÑÀX‰ÂþùºÀ} v÷�%À°ÑÌ1Z ÀyO¯uÌÀI0ìÀ‘zPêÙýÀ»oéâxÀnQÇï3�ù¿×/— cíÀѰÞšGÀƒµ¹”C˜À”ƒ*'ºÀàÎ<@¿\ö¿:1ÓÞTÀ¹ï¸Á¥]À§v»,sÚÀé=\ïò ÀŸ…Éy. À*€%âÂ"Àkf$˜7À- -Ö ›ÀX •g þ¿ #ü�‡ºÀÿ®,U§—ÀjÞèÒ—m ÀÇä8K´ŽÀ�¬W ÔÌ Àf€’]¯iø¿êÀTð¿$EC�À)1Y]#¨ÀõßBšÀdv“ÖúúÀ5Z¶KD@û¿Ž¿Ÿv¿À¨kþ`�Øý¿³†î(+À€Â]'bɱ¿�Þ4P)ÀёɊ"ÀÖIôÀâÀר E\ý¿fæ_²rþ¿,"ŒCkÙ迃·2æîoü¿'CÄå´À·CžcÇÀ¾ÕÉÊÖ›qk@0&^Ä @ÚS(�3þ?àû,· @ ·± ÜÝ @NxÈ,`ü?风µÁ/@È|(› @bY«§þn@uzSq @c ë)8ö @~ û…e@Ý“wÓ¾Ø@ˆVæMÑ@IM½yg@TC V6‚@Å >ÑV.@s)J8–õ@_·Ž¼ @±Ðý�j @æÀÂ}j* @DPÏ‚ÕÎ@Â3¹iì@b ïœù5@« â á§@´ÿàõÝÈ@–´(ƒù?@ÐØÏ’?@Lf·aY@@­cIf¤¸?¦Fü±Ëõý?iŒ¹fg� @@’å @›Ï¶ ‹(ÿ?±�‘]™ @4n•̤@ôí×[Ñ£@8]v*v%@²ò™ïÁ @ØgeÆ¿= @í>ÉõÌ;@N€Y ¬@>g“õi @ÜXvÁq @/GÄs“@kaÞ€ ð?‘]‡×Üv@¹97 þ@ÓÊ‹Ÿlúó?ô;I|²€@# ”Á(ÿ?*ÂÀ@øï**2y @µü³IoÀ@aÐg€°×@†¨5Z�ò?Q-õúÊÕ @^ë(Å´§@âWF)º @]`©%H+@Ûtò@J9þ?f2Ñc õ@¾±/Ép @@á´?qCû?`î Dkþ?šn ]�ë?öÂËõM@pMŒ*î?Ú¸¨{@ @â³”É+û@ôÇš’@n‰… Ä@–Çeað@!�€HÕõ?p�xT @øCý�ö¾@Ü  ð?ýâD÷ˆa@óxa›+Ÿ @î0¿ FZ @ÍPqK@̱€‰ @-£·1�@D7Q‘Îr@º0_i�@¹²À´™ì@t›„Œ•@¤ÍðÛ!k @ƒPÿäuù?| §&¾·@Tßíç�@l’«ýð?7¬°*õ @×o5dØ|@¡×¨qÕ” @¿”ŠáHð@×�“lÈ@6‡ú&×�ÀfúÚ«µÀ~;Ä]¡†À²`ñ”NÿÀ·yÿcö À˜³WõÀØ}§ÙMÀý,ÚéÇÀ™)†há] ÀÔõ §µÀ]“¿SÀ•B »G À%6ñÎÒó¿~ªÎ± Àɦ  ÀyZ§þ˜À¤É%Ü8ãÀñÐË©À0˜Iµ ÀœŽÛhQ ÀF‚ã–qø À3}X¿ÇÀEÜ1 ÀšBNªÙVÀøO{¡Ëê¿<úå«Ié¿>jÿ–®4ÀQG’q´À­‘Ñ>²-ÿ¿¤( Í$À¡)i'´ç¿‘ü©»> À°W<»§Àí¿,v [Ö(ÀvTð\¥ À’X‹”F>À˜âsÜXÿ¿>¼šmÀšQ?@ãP À`Á–ôM(ÀŽ µ€\ÿ¿F¾F'’±ñ¿D âdÖ„À± ŽGû€ü¿–Öù9\ý¿·œ½ 9(Àl †\5ú À‹a|´ÏÀ:%â‹÷¿_•d€xúÀ=¾ÏYU Àãá!ºSú¿ùåã�ÀÜKÙŒ‰QÀþâþl€ÀòÁÛº»Ãò¿dADYWn À4¹5[ À4ÏJ„XÀ l*ÃWÀ³u¹½ÖÀ•zTJ[æÀ°ƒÂÿ±ÀOåw s ÀºjýlSþÀ¼*·t2 ÀÄ ø³µ;À\ó#+=¸ÀäA(VÊ* À¶ï½Z_ÀF™{§£ÿÀ!¤4 43þ¿»?IH]ÀõvœùÖÀ¡@êv…oö¿Ãƒ•¹�ÒÀ-ƒšèï� À8{8Ñ7 À¬E%ýTHÀ…a]a�ñÀ–� × R @ …ÅYM @[ˆ‘†Aü?À×(†8KÝ?³¢«ÚF¾@è]$ˆ@ÍÅÌt»@к¿ Û¿Œ-p¸!@ K šô{ü?p¹á.úÿ?p¶ø@€@G»Nw%@}ÇÜãѵ @­Ïq4<Þ @3óO_ 0@ôú9×@Š”Š6@žÁãë!Ó@I&ƒ°Ã+@„ UÔ;@&«\_½d@ñ=ÕU¤À@Á÷z¦O ü?#Mé0qäÿ?»Ö)%õ@\�MÏ2@é.O6<Û @"ºqû·ÿ?jö„Úöy@˽š—1&ô?^šµ…ª’@�1 8ý?‹®ðÔ¦Ë?†x-$é@�‹”í @åï£=@y¨-3ö @d. þa€@vj’ñ,{@‘GÅ“ì @ã®àƒKdñ?ìçùtT@'Ö é @P(Â/ÆÜü?ë!¾Ö±ù?‚S®Ýj@vh9«n@¶úêaß_@€¯Ï¯Þ‹@ÔÏÒ?§‹@ë•‹5¡%û??ã¢T‘rð?ð¬GáOçþ?�™AþPÝ@¯ˌ |@H'|·\i@Fåø°~Í@šÆ4ÊL@Êî¬m„@cߎÇv@QU¿¡i@/?«ü˜Î@!Ñ!­»¾ø?€Eë\®:@—�v?R@9½Ü¾„)@“ºŽ€n @µ²™]°@ . Eu•@ÖX¨»w@Ûb²3›ˆ@;�³k@ÜEßIé�@Íöù=Ù;@ëúÔ;N÷?ö‹ˆÎýž@e':õYœ @ÜeÙclñ@‚u"]�÷?fÄ+˜FÕü?s³ÿa;·@¿—±£Ûæ @²²5áBâ@ŠvbL@ãX+‘‹è@ ¨#Ý•@š)Û-¥×@ÏÚÃà„º@ºžöÕ³ @/®0õ9 @»Ö5 ëä@Dë:!~@bp@à(þ?‹��š« @7بšf<†å‚ @-œ f3@Xa…ô[@TbŸÝ—@p}¨ñ1@èÊbB#ƒ@RRú®Ðo@L’$ É’ @œ;`;Cw@Î$Gº®@k{u·iÑ@€ z0¬´@*cÚÆ=ßü?pŸ¥É @²‘.5|N@¨ÊíDZ¿ @T®aF+‡ @öÃeN @àDU&7›¸¿GnÛvÿ@öøyú³î@ÜÒÄÉ@g·aÑAH@ÀEÏP" @-1›/ï@Rÿ¶ä@LY¸öé @®„Lt&ã@7!©‹¹@üMryR@¼[›³Q@( k¼¤é@“!�·ü@ƒ! aw‰ü?Ši•àS@Ó‡•Ô�â @Müñ$êk@‘3Ö¢(<@{ÿ•i/@Ì ZInˆ@oþ’FJ@9ÏŒ˜õ­@H�M…_!@µtåÇÿ @6‘�J´@ç Å-E@èG€¨n@Jh4†)y@_m€BC@g8F ˜´@Àå"ul¸ @^¤üU– @ ðiG§_@z#‡�Ð @q “ÄG‹@ùT0ly¬@jˆWʃb@Ôc ÝsV@à z°ùí?Þ5Ê2òsû?.Ò[›)Ñ @®Wiè@Æ_))ùV @‹�qAXò@ Gî7Þÿ?¸‹ñ˜ø@à!S{;@ðÌŠD–é?rôAùe@�nø‰L¥@Ç÷¬Òú?ðlðæ @È ÐØò4 @¦Ö’�Q¦@¨Gb}�@þ–c_A8@ÆM,·ó @”P}µÁú?ðOÑ®Õo@ºm…½*‡ @«°&åS‰@8'Œ þ?Ï–Û€Ø @£8œ†@À«Û÷8„@‘‘‹”U @ø˜çS�`ç?>— ƒŒ@œ‰Õ‹v @°X @pž_[âì?3;\]å@P§Äçµ@¹vZJ @@¸:ZNÂ@¦”ˆë@äãàâè#ö?…iCN @•k°ð1w@Oýüggü?xI[ËLÍ@|uT˜>¥ø?„géÊI @$I˜ ²@8AZåÁ@ò�Î:pg@á6ˆ:¨f@pSra @ÖÍ ô·¥ @Ì@Á@¬;÷?‚á'D�šú?vÅ:ƒ¯@BO3G ÷?éÊÌæ•dö?“©èÖØc@Ñ’Ét©€@Ì!Æ�(Ûö?ZoÀð@€qaÛ¯@�Ûà30 @©E4Ø>@‡À½2@<føÙE@&!8ÎÅ„@àÐ,bÄq@ œÿÁÆë?ˆŠ’Í8 @©Ùûw¥ @W™ö/#ü@�õ܇Ð@²–�‘f@�šU+HQô?êùÕIÎ û?;]º®¢Ñó?Âv±¦Øá@ë:Ї�@st3Qí@º±!P@J~W>//õ?sÆŸõ+ @|�õ[Tÿ?´õÎ8s§@´™Ø,@‰#_–�@›Ê]‡Â@N´cùµú?´fmÝ2ÿ?";èA.@=Ï¥o«@ŠGË> @³r•Êm@ C†Ð±@å‹fJnh@3©H‹Êx @s¨]Σ @ŸAâÅà@ò±E‚œš @Uµá³¶ @ñ¯�ßœ®@tõm¦½@:Už­–q @€&°@˜ ÁÚ@ÏÕ±^k@é³[üzm@@ DÓ @Õ§Óò7Æ@W÷Äù-@1ßÇ�hî @Ðæ«òƒô?@Ú‡G›f¬?  ·ñûÆý?ÚŽñÀ¯@:b2J4åõ?g%å§@¾Om&ÔÃ@<PhúA@`š²IVóú?FØë@c’,ên@æi)³‡±@òØS§6L@c‹rõ½Ô@�dÔ“ƒÄ@¤ÁA?¶@Ô–—”Oæ?\;ØÕ@šÊ:[öZ @¼þÄa @ïH‚!W�@ Õ$Èd0@<¦}yõ @(evF<ÿ @bBεӊ@Ì­n»Y„@�5‚„Wã@ï›O¬Ä@hÙ)Òü?__-qQ @i×Ñ>éö?»×ÜæÑƒó?ÉÑ ±+÷@‚þ_£Ò6 @_üR0°¸@È„´n¥-Õ?m´{»r@NŸÎ[‰ Àðçþ�ÀŠÿæ-ÌÊÀ«1 Våmû¿è\úÐtÀ€Ô¥¬ño À°šÏƒ\a À (ªçÀå©}™ÝØÀ€Ë»�¥Ý¿.±üuÀ JeõÀ À?5Å\3 ÀȈG YÇ À2 ^ÄÀˆ ÈW@À«ƒÏÀæ-€?¡ÀÛ ²ùQŽÀÁy²ËèÀø(š3 À…Ôûá3ù¿rÁY§ˆ— À½XÊc›ÀXÂtæk÷—ìô¿ 1ª­ý¿—A`”ù/À“äðÁ’Àe®Ö‚ISÀ…ÈN\µÜÀNʤüÀ¢[NJâÀ³¬!´�|ÀFÕâ¯fVÀô滯KVÀÐå͆½m Àˆ1þ¬ÿDÀ#£ÀávOÁW=ú¿FÐ1‰•ÉÀñ!i(óÀò̵ÛÍæÀm*A¼e¢À´83°80ý¿è2‘,ZÀnç°„JcÀîÖñÌ#Àék¹s°LÀð k_†À;—½˜v2Àn¥’.-ˆÀðP\ùªÁ Àšq uÀÞšÇ\Ï ÀÑ­ ÕÌLÀ8´öxgÓÿ¿’õ�ƒ`ÀÒµÿî0‹À0©3¨ 2 À\ ¾BÍ÷ÀîÙº«+ ÀFcñÃÅ Àà ²T‘žõ¿±BH%âì¿ýf~‘ßÀ�*¡E' éêÖ§ Àß©”5ZÌ À.sò:HÀ¾ôaå¸ý¿>D ×ÜIý¿ù‘•KýÀ~À/T°­Àú,ÎG| Àfå¤tÀÊò©+ÃrÀXñ¹þÀ(—óá:ü¿¥&¼~ôÉ ÀžàÍý‘6Àstatistics-release-2.1.2/inst/datasets/lightbulb.mat000066400000000000000000000046251527311104300225740ustar00rootroot00000000000000Octave-1-L lightbulbÿmatrixþÿÿÿd§¸¶ŽÓ¿@-Rd ™›À@W 2@´@mH3à yÅ@+ÄŸØ�oÄ@÷èÅœ É@ÔS&…3î®@nžÉ–V¨½@‡¹G~ Á@[t.å à½@” |Ý£ŠÀ@²už�˜½@½r[.ŽÀ@ÁY€ÂÇéº@­>mHéÀ@Á [ áœ@Êq¬­uÆ@b¨ýõ.”É@ž"áåÒpÉ@Uõ›“aË@B.UË3äÂ@dÖ0`´%Â@Âõ­9[‘Ã@c½5ìþ=¹@†gákE¾@'§ja£ä¸@Ž0n†–°@®ñÉDÜž§@Ÿ¯vvÇ@Üw«·b€È@äkI×»@ô¡n´ëÊ@Ý‚î…çÜÀ@úÅáÇÀ@ä;=]»´@®¸ø‰Á@)Såü]Ã@Ü?á½¼@�¤Ü²é!º@/ìÎ ­óÀ@‘’ç‰@äÃ@˜¢)r³-È@[·Íc²¿@®)‚Ó̆Å@ä�‚œè„Ì@’±tÍã–¹@£êŒ,îÅ@g¤Œ3q?Â@¶'’…&¼@ Z¯Ã@ƾÑAK–@åHmT™6Ÿ@ø�~€Ä’@¶t°ÁÂR•@œSn ¶�@<ô6ºŸ�@úc¥ñ‘@œîÚi œ¡@MFsAE @ËäfÏ}˜@h¡Üe ”@qVºl›‘@zYìJf›–@ED3¿W‹@xó¸÷Š5�@\Ñ2?Ï·}@i6?¡ÿ”@hÃ;�q]˜@=ÚEC¯³ž@ž &'ÿ‚’@! Eæ�‡�@˵—JH–@#ê…æ_Ÿ@™Ó·‡æ¹™@ö¦¼½ äœ@A2ʉ`™@ŸÂÕ¹¼ñ•@ü¤`h3”@j«TV'˜•@—ùggï‡@a&®Òa”@(Ö&Ó%‚@®U™lõ‘@–ð0«vs€@Ì42Œ^š@'gÙûÚ$‘@:R+Úï/™@ƒYî2§<‘@ /ªO�º�@Ã@jK @Éh¼«|š@™BÜ“Κ@–\ƒ»}R‘@é1/£Vì‰@üÁð—@’4$A=q�@øõoð8)‘@HÜ·  ¤@™b•¿ÿ°’@ÿÛF½ —@ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?statistics-release-2.1.2/inst/datasets/mileage.mat000066400000000000000000000003031527311104300222100ustar00rootroot00000000000000Octave-1-Lmileageÿmatrixþÿÿÿfffff¦@@33333³@@33333s@@ÍÌÌÌÌL@@@@@€@@@A@ffffffA@fffffæ@@33333³@@š™™™™Ù@@33333ó@@33333³B@ffffffB@ÍÌÌÌÌÌB@ÍÌÌÌÌLB@€B@š™™™™YB@statistics-release-2.1.2/inst/datasets/morse.mat000066400000000000000000000256461527311104300217530ustar00rootroot00000000000000Octave-1-LY0ÿmatrixþÿÿÿ$Ð?è?è?à?è?à?è?Ð?è?à?è?Ð?Ð?à?è?è?à?à?à?è?à?è?è?è?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?š™™™™™É?333333ã?š™™™™™Ù?š™™™™™Ù?š™™™™™É?333333ã?š™™™™™É?š™™™™™é?š™™™™™Ù?š™™™™™É?š™™™™™É?333333ã?š™™™™™É?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?333333ã?š™™™™™Ù?333333ã?š™™™™™É?š™™™™™Ù?š™™™™™É?š™™™™™Ù?š™™™™™É?š™™™™™Ù?333333ã?š™™™™™é?ð?š™™™™™é?333333ã?š™™™™™Ù?š™™™™™É?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?š™™™™™Ù?ð?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?ð?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?ð?š™™™™™Ù?ð?š™™™™™Ù?ð?š™™™™™Ù?š™™™™™Ù?ð?ð?š™™™™™Ù?š™™™™™Ù?ð?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?ð?ð?ð?ð?ð?š™™™™™Ù?ð?ð?ð?ð?š™™™™™Ù?ð?ð?ð?ð?š™™™™™Ù?ð?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?ð?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?š™™™™™Ù?V-²�?Év¾Ÿ/­?j¼t“¶?Év¾Ÿ/½?çû©ñÒMÂ?ÙÎ÷SãÅ?š™™™™™É?Zd;ßOÍ?¦›Ä °rÐ?çû©ñÒMÒ?j¼t“Ô?ÁÊ¡E¶óÕ?X9´Èv¾×?š™™™™™Ù?Ûù~j¼tÛ?sh‘í|?Ý?´Èv¾Ÿß?¦›Ä °rà?ÇK7‰A`á?ƒÀÊ¡Eâ?333333ã?Tã¥›Ä ä? /Ý$å?ÁÊ¡E¶óå? +‡Ùæ?-²�ï§Æç?yé&1¬è?š™™™™™é?ºI +‡ê?�•C‹lë?'1¬Zì?sh‘í|?í?“V-î?ßO�—nï?ð?dissimilaritiesÿmatrixþÿÿÿvàd@ e@àc@€f@€d@`d@`d@ÀR@ d@€c@ e@`a@^@ d@€d@Àd@``@€b@ e@À`@Àd@c@€d@@d@ e@€e@àd@@e@@e@Àd@àd@Àd@€e@@f@Àe@X@ÀS@`d@ÀW@`a@@Z@ d@ a@@]@€N@@d@`d@àb@À^@@`@ a@@a@e@@`@@_@ a@;@@]@ÀT@àb@Àa@€b@€\@@X@2@@W@ `@ c@`e@ a@Àd@À\@ a@b@@d@À\@]@ÀW@€d@€d@b@€U@€Z@À`@€d@f@@d@b@ `@@Y@=@ÀV@€b@`a@@^@àa@c@À\@€^@\@ a@Àc@€e@a@Y@€\@àc@àb@9@@R@@d@b@àc@€`@àc@ÀZ@@_@ e@€`@b@@_@^@b@_@Àd@`d@`d@@c@`c@€a@àb@ d@`e@Àe@àf@Àf@`e@`d@`f@Àf@àd@Àf@@f@ e@Àe@`f@ e@àe@T@àf@ f@€e@€f@ e@ f@Àe@`f@Àe@àe@€f@`f@ e@ f@g@àf@€c@]@@e@€]@€b@€Y@@d@ d@ c@ÀW@@c@[@@b@ f@@`@€_@À\@À`@À^@Àa@ d@àa@@X@ `@@[@``@ `@àc@@e@€e@`d@€e@à`@À^@€b@àa@d@C@ÀX@€b@@`@f@ f@ c@@d@À]@ b@``@@^@@b@c@ d@`c@€e@€d@c@ a@`b@àc@Àc@ c@Àc@€]@@e@€c@Àc@b@ d@Àa@ÀU@ e@€[@@T@ b@`a@Àb@ c@€d@€d@@b@@]@D@€_@àb@Àd@@e@Àe@`e@Àe@@c@e@àa@@e@@e@ f@@c@@a@àc@`c@àe@àd@ f@ e@Àd@ e@c@ e@Àd@@e@`e@àd@@f@@f@ f@À_@€[@@d@€e@À[@;@€Y@ b@f@e@@d@ c@ a@À]@@[@@^@@R@€T@À\@@a@@c@àb@€]@\@^@€_@@]@@c@€c@^@@b@`@à`@€e@`e@@]@€a@À\@@Q@ b@`a@ d@@d@@c@àc@ e@Àb@ b@ c@`e@`e@e@ e@ c@€U@À\@\@d@@e@``@]@À`@Z@ `@€O@b@€b@Àa@ a@@[@€]@ÀY@ b@`d@Àe@Q@àd@€d@ d@ b@€f@ f@ d@€f@@b@e@àc@€d@Àd@f@`e@f@`f@`e@`d@àe@ d@g@Àd@ d@ e@€a@d@Àe@ c@€f@ d@€e@àd@àd@ d@ f@Àe@àe@`f@ e@@e@Àe@`d@Àf@À[@^@àb@ e@€e@@d@`c@@^@€]@``@``@ a@ a@d@Àd@Àd@ b@Àb@Àb@`b@àa@T@€]@€d@€d@b@`a@@`@À^@€X@@T@ÀY@ÀV@[@b@ b@@a@[@ a@€]@àa@àc@Àf@€f@Àd@àc@€a@U@ÀR@€E@@\@^@ a@`a@ d@€^@@X@@S@@\@€`@ b@ f@@]@`a@I@@c@€c@`b@`c@d@àb@ d@ d@c@ c@ d@€e@ f@f@€T@ c@Àc@ d@ e@€e@e@@f@ e@ d@``@ d@@e@Àe@f@ g@àe@ f@Àe@Àf@f@Àe@ e@ f@ e@`f@ f@ e@€e@àe@Àd@€f@X@€_@`c@€d@d@e@@e@àc@``@àc@d@Àd@ e@Àe@`f@ a@@[@@`@Àc@@c@ c@À`@€G@@W@€\@`a@€d@`f@@f@À`@€_@`a@@b@Àa@`b@àb@€d@€c@àb@€d@Àd@`e@@U@]@@b@àa@``@]@^@P@@]@€a@@c@àd@[@@_@@Y@ÀZ@a@`c@€[@À]@ÀZ@ `@ b@ `@Àa@À`@€d@ c@À[@L@ÀS@`a@@b@H@`a@d@Àb@ b@`@ÀX@€N@@R@€L@€`@€d@ b@@_@`@@_@@b@Y@À^@ÀZ@ a@Àa@d@ d@Q@@[@À_@c@ d@ e@À]@`a@d@`e@Àe@€E@ `@`d@@b@F@€\@ b@€O@@Z@:@ morseCharsÿcellþÿÿÿ$ÿ sq_stringþÿÿÿAÿ sq_stringþÿÿÿBÿ sq_stringþÿÿÿCÿ sq_stringþÿÿÿDÿ sq_stringþÿÿÿEÿ sq_stringþÿÿÿFÿ sq_stringþÿÿÿGÿ sq_stringþÿÿÿHÿ sq_stringþÿÿÿIÿ sq_stringþÿÿÿJÿ sq_stringþÿÿÿKÿ sq_stringþÿÿÿLÿ sq_stringþÿÿÿMÿ sq_stringþÿÿÿNÿ sq_stringþÿÿÿOÿ sq_stringþÿÿÿPÿ sq_stringþÿÿÿQÿ sq_stringþÿÿÿRÿ sq_stringþÿÿÿSÿ sq_stringþÿÿÿTÿ sq_stringþÿÿÿUÿ sq_stringþÿÿÿVÿ sq_stringþÿÿÿWÿ sq_stringþÿÿÿXÿ sq_stringþÿÿÿYÿ sq_stringþÿÿÿZÿ sq_stringþÿÿÿ1ÿ sq_stringþÿÿÿ2ÿ sq_stringþÿÿÿ3ÿ sq_stringþÿÿÿ4ÿ sq_stringþÿÿÿ5ÿ sq_stringþÿÿÿ6ÿ sq_stringþÿÿÿ7ÿ sq_stringþÿÿÿ8ÿ sq_stringþÿÿÿ9ÿ sq_stringþÿÿÿ0ÿ sq_stringþÿÿÿ.-ÿ sq_stringþÿÿÿ-...ÿ sq_stringþÿÿÿ-.-.ÿ sq_stringþÿÿÿ-..ÿ sq_stringþÿÿÿ.ÿ sq_stringþÿÿÿ..-.ÿ sq_stringþÿÿÿ--.ÿ sq_stringþÿÿÿ....ÿ sq_stringþÿÿÿ..ÿ sq_stringþÿÿÿ.---ÿ sq_stringþÿÿÿ-.-ÿ sq_stringþÿÿÿ.-..ÿ sq_stringþÿÿÿ--ÿ sq_stringþÿÿÿ-.ÿ sq_stringþÿÿÿ---ÿ sq_stringþÿÿÿ.--.ÿ sq_stringþÿÿÿ--.-ÿ sq_stringþÿÿÿ.-.ÿ sq_stringþÿÿÿ...ÿ sq_stringþÿÿÿ-ÿ sq_stringþÿÿÿ..-ÿ sq_stringþÿÿÿ...-ÿ sq_stringþÿÿÿ.--ÿ sq_stringþÿÿÿ-..-ÿ sq_stringþÿÿÿ-.--ÿ sq_stringþÿÿÿ--..ÿ sq_stringþÿÿÿ.----ÿ sq_stringþÿÿÿ..---ÿ sq_stringþÿÿÿ...--ÿ sq_stringþÿÿÿ....-ÿ sq_stringþÿÿÿ.....ÿ sq_stringþÿÿÿ-....ÿ sq_stringþÿÿÿ--...ÿ sq_stringþÿÿÿ---..ÿ sq_stringþÿÿÿ----.ÿ sq_stringþÿÿÿ-----statistics-release-2.1.2/inst/datasets/patients.mat000066400000000000000000000660711527311104300224520ustar00rootroot00000000000000Octave-1-LAgeÿmatrixþÿÿÿdC@€E@C@D@€H@G@€@@D@<@?@€F@E@9@€C@B@H@@@;@€B@I@H@€C@€D@F@<@9@€C@9@B@>@€F@D@9@€G@F@H@F@€A@€@@C@€C@F@F@€B@€F@€B@>@€C@E@E@€H@F@€E@€G@I@C@€D@€F@B@C@=@<@>@<@=@B@€F@@@?@H@9@D@€C@€D@€@@?@€A@@@E@H@A@€C@<@=@@@€C@€B@€H@?@€B@C@€F@>@H@H@9@F@€H@€F@H@ Diastolicÿmatrixþÿÿÿd@W@@S@ÀT@ÀR@T@€Q@V@€T@€S@€U@@S@Q@€R@ÀW@ÀS@W@ÀW@ÀS@@S@S@ÀR@ÀS@V@€V@X@@S@T@S@ÀT@@V@W@ÀT@T@U@W@ÀT@€V@@U@€V@€R@W@T@@V@X@@V@@S@@T@S@ÀT@€S@ÀW@ÀV@ÀV@€U@@V@ÀS@€R@€T@S@@T@@S@@R@@U@S@T@T@ÀS@€T@ÀS@€T@ÀR@ÀV@€R@€S@@U@U@ÀR@€S@@T@ÀS@@U@ÀS@€T@T@T@W@W@X@ÀU@@T@€V@@S@ÀV@ÀS@@R@ÀX@W@€R@@W@€U@Genderÿcellþÿÿÿdÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿFemaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleÿ sq_stringþÿÿÿMaleHeightÿmatrixþÿÿÿdÀQ@@Q@P@ÀP@P@Q@P@Q@Q@€P@Q@€P@ÀQ@R@@P@ÀQ@@Q@@Q@€Q@Q@@P@P@O@€P@@P@€Q@€O@€O@Q@ÀP@€Q@€P@P@€Q@ÀQ@€P@ÀQ@€P@€P@€O@ÀQ@@Q@€Q@€Q@ÀP@@P@Q@O@€Q@ÀP@Q@O@P@€P@R@€O@€P@€Q@ÀQ@Q@€O@@P@ÀP@€P@Q@ÀQ@€Q@N@P@P@€P@P@R@@P@ÀP@R@P@Q@€P@P@Q@@Q@@Q@P@€O@Q@@P@€O@€P@@P@Q@ÀQ@€Q@ÀQ@€P@@Q@@Q@€Q@Q@€P@LastNameÿcellþÿÿÿdÿ sq_stringþÿÿÿSmithÿ sq_stringþÿÿÿJohnsonÿ sq_stringþÿÿÿWilliamsÿ sq_stringþÿÿÿJonesÿ sq_stringþÿÿÿBrownÿ sq_stringþÿÿÿDavisÿ sq_stringþÿÿÿMillerÿ sq_stringþÿÿÿWilsonÿ sq_stringþÿÿÿMooreÿ sq_stringþÿÿÿTaylorÿ sq_stringþÿÿÿAndersonÿ sq_stringþÿÿÿThomasÿ sq_stringþÿÿÿJacksonÿ sq_stringþÿÿÿWhiteÿ sq_stringþÿÿÿHarrisÿ sq_stringþÿÿÿMartinÿ sq_stringþÿÿÿThompsonÿ sq_stringþÿÿÿGarciaÿ sq_stringþÿÿÿMartinezÿ sq_stringþÿÿÿRobinsonÿ sq_stringþÿÿÿClarkÿ sq_stringþÿÿÿ Rodriguezÿ sq_stringþÿÿÿLewisÿ sq_stringþÿÿÿLeeÿ sq_stringþÿÿÿWalkerÿ sq_stringþÿÿÿHallÿ sq_stringþÿÿÿAllenÿ sq_stringþÿÿÿYoungÿ sq_stringþÿÿÿ Hernandezÿ sq_stringþÿÿÿKingÿ sq_stringþÿÿÿWrightÿ sq_stringþÿÿÿLopezÿ sq_stringþÿÿÿHillÿ sq_stringþÿÿÿScottÿ sq_stringþÿÿÿGreenÿ sq_stringþÿÿÿAdamsÿ sq_stringþÿÿÿBakerÿ sq_stringþÿÿÿGonzalezÿ sq_stringþÿÿÿNelsonÿ sq_stringþÿÿÿCarterÿ sq_stringþÿÿÿMitchellÿ sq_stringþÿÿÿPerezÿ sq_stringþÿÿÿRobertsÿ sq_stringþÿÿÿTurnerÿ sq_stringþÿÿÿPhillipsÿ sq_stringþÿÿÿCampbellÿ sq_stringþÿÿÿParkerÿ sq_stringþÿÿÿEvansÿ sq_stringþÿÿÿEdwardsÿ sq_stringþÿÿÿCollinsÿ sq_stringþÿÿÿStewartÿ sq_stringþÿÿÿSanchezÿ sq_stringþÿÿÿMorrisÿ sq_stringþÿÿÿRogersÿ sq_stringþÿÿÿReedÿ sq_stringþÿÿÿCookÿ sq_stringþÿÿÿMorganÿ sq_stringþÿÿÿBellÿ sq_stringþÿÿÿMurphyÿ sq_stringþÿÿÿBaileyÿ sq_stringþÿÿÿRiveraÿ sq_stringþÿÿÿCooperÿ sq_stringþÿÿÿ Richardsonÿ sq_stringþÿÿÿCoxÿ sq_stringþÿÿÿHowardÿ sq_stringþÿÿÿWardÿ sq_stringþÿÿÿTorresÿ sq_stringþÿÿÿPetersonÿ sq_stringþÿÿÿGrayÿ sq_stringþÿÿÿRamirezÿ sq_stringþÿÿÿJamesÿ sq_stringþÿÿÿWatsonÿ sq_stringþÿÿÿBrooksÿ sq_stringþÿÿÿKellyÿ sq_stringþÿÿÿSandersÿ sq_stringþÿÿÿPriceÿ sq_stringþÿÿÿBennettÿ sq_stringþÿÿÿWoodÿ sq_stringþÿÿÿBarnesÿ sq_stringþÿÿÿRossÿ sq_stringþÿÿÿ Hendersonÿ sq_stringþÿÿÿColemanÿ sq_stringþÿÿÿJenkinsÿ sq_stringþÿÿÿPerryÿ sq_stringþÿÿÿPowellÿ sq_stringþÿÿÿLongÿ sq_stringþÿÿÿ Pattersonÿ sq_stringþÿÿÿHughesÿ sq_stringþÿÿÿFloresÿ sq_stringþÿÿÿ Washingtonÿ sq_stringþÿÿÿButlerÿ sq_stringþÿÿÿSimmonsÿ sq_stringþÿÿÿFosterÿ sq_stringþÿÿÿGonzalesÿ sq_stringþÿÿÿBryantÿ sq_stringþÿÿÿ Alexanderÿ sq_stringþÿÿÿRussellÿ sq_stringþÿÿÿGriffinÿ sq_stringþÿÿÿDiazÿ sq_stringþÿÿÿHayesLocationÿcellþÿÿÿdÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿSt. Mary's Medical Centerÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿ VA Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General Hospitalÿ sq_stringþÿÿÿCounty General HospitalSelfAssessedHealthStatusÿcellþÿÿÿdÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿPoorÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿ Excellentÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿFairÿ sq_stringþÿÿÿGoodÿ sq_stringþÿÿÿFairSmokerÿ bool matrixþÿÿÿdSystolicÿmatrixþÿÿÿd_@@[@@_@@]@€^@@^@@`@À\@À\@€]@€\@À\@À_@@`@€\@@`@_@À^@À]@@_@@^@À^@€\@`@ `@€\@@\@@_@^@À_@À`@@^@À\@À_@@^@À_@a@@]@_@^@`@]@€`@ a@@]@]@À]@À^@]@_@ `@@`@€`@@]@ `@€]@^@@a@@]@@\@€^@À\@^@@]@À^@À^@À]@€[@@^@@a@@_@€^@^@@]@@_@_@@^@€]@^@€]@€]@€^@À`@``@@\@@_@à`@`@À^@€^@@a@_@@`@À^@ `@`@_@À]@a@€\@Weightÿmatrixþÿÿÿdf@`d@``@ `@À]@Àa@Àa@€f@àf@€`@`@ a@Àe@@i@ `@ f@àg@``@`f@€e@ `@@]@ a@@b@À^@ g@àa@€\@Àd@@g@€_@ a@@a@`g@ h@ a@h@€]@€f@`@€d@àf@ e@@h@€e@à`@Àf@@^@Àc@`f@@e@a@à`@`b@@g@_@À`@@e@€f@@`@@`@À_@ a@À[@À`@ g@ a@a@@`@ a@@g@À_@f@À_@À\@@f@``@àf@@h@€_@@g@€g@ g@^@€`@Àf@^@À^@ a@ `@g@ f@_@Àe@À`@`e@€g@@g@€e@ f@statistics-release-2.1.2/inst/datasets/popcorn.mat000066400000000000000000000003031527311104300222650ustar00rootroot00000000000000Octave-1-Lpopcornÿmatrixþÿÿÿ@@@@@@@@@@@@ @@@@@@statistics-release-2.1.2/inst/datasets/rundist.mat000066400000000000000000000227101527311104300223030ustar00rootroot00000000000000MATLAB 5.0 MAT-file, Platform: PCWIN, Created on: Fri Mar 31 11:35:28 2006 IM@%xœŒ™ <+õ;� @‰%ìß81ëÃÁ ßJŸfÛÉ™ÊA`ýÿôádÐgõJŸE ™ÇCAF{?±�ç¯13&}¸~ëC'­Ë"mÐ[ˆ¥��ÆdýÉÜYMÜýW_[&}¹ô•ÿ»/mñÌ<ÀEˆ HúÂ Ä SM@Ö=NàN"Ë?™õçaПøwœy2 ªÅ¸˜êò ñ@#%`4›ìÞ /ÿzN™ôçeÐ_éïþðõ3@(×Þ³b,k6α•SwrR> qI {±Ý`gÝnñOfóð1˜Gåïyd`hœÐßù€G§9´ÞôIîƒÀ¡Þƒ÷ƒßÛߤ šEO¼…�Ü"Vú<éLæáÿmúúxÿú+ß—[Vôí2‰D“ùÒÓtuN?ZGWÝ7‰hZÛžö˜¤ý¾Â#DS-7£P1š–~™¯¡_çz�^d¥)EŒtd”^WÄVÿ‰î/—‰¿Õ üùþî�Ú¿¢†2ûè}m3‘Ût?O"TÇhê˜n¹G„6/Ѳp³*M_’Ù: ´t$˜Ó®Ããl$-iÚù6õ›!M«ïM8èÐ4ÞŸ“sHï :L¬ø,dâS€�ÏÐß|R¨“tu¬r ¤ûô­h¦é™àTkÚ<1ÚÇ%õ@ Vq$yƉv½ëÙÐÉ0( ð5÷NÁ‡·jµSA Q»;K»/{Øz2·(±Þ  Ý_rüÚGzxÁP@¤û-eâW��_Êo~k©Tº–Š%ÓýšrMÒ´mt'A�ÆáÌ÷Û¼/W h~r?Wmÿvt÷võÖ×�€\k<ÜRøfo|×k�Î[®bá­ °ïÄFëKO@ 4*ùpí~A¶LÒp�3娷íŠïJ&¾…øŽÿÍ÷U€îWå,çušæG£t¾ƒ‡Jí@À×}®�œçÂu%öÔ‚”±èú“ƒ`­JZ nø˜+ÈÛ¥¤ÀåjÚ¹íÞ#ùrqÕ~z$ åB!�‹¸¡Ü�¶§.>,`Èa:m1Ù9E¢OA þø’ϱ³+þk™øfàÿÌoþ§¨²tÿS7É�iŸÎ¤LóϪ-oA��Y“A@ÃV£Øù xFa #ÛA*ʵ&—«@Ekt½ ]aÈñˆ¯Üi èSºˆ×�2¨gZfI¿5dk«Tæ:3(4D*^UN²í¾6.ÿD23>!ÍVCËE{]66­ähd’C„AŽôßrÌS•iþ)ÞÑ_»iŠí½h ­?s n€À7gýCB@‚õpHý”T˜±ë¡5;¼ 6­2u…8Y¢]²¢ ¬¬e‘3P\¬ç_¹K�§ÃOçÐËеêÇi™]— ²¼Ö™+Ug sœª÷9�A&B=ÖÏâ�¡›^%cæŸvÑrÕmÏâg‡èyZ™äe�'÷·<‹T=úótn(d†¦·ƒ¾–ù€€üéŽpñ&ÚóÜu'æ¶d±n&>€ ñéÉò7‡C/ÝVŤg@E¡RiòÈ=HËýãfŠí èrO~˜–b7ôéa�Ê»”A¨wéÓpÛÕ!脈b¤þ÷^¾L<Ô>ñêOÔ @?A‡m@qräÐiõDÃ-Œ¶NgÂö_$­äêb’KŒA®Âßr-SMiyúÛÕùiï'ú¶ ) áÉ‚1�Ïx¦¦ãÛÂ’´u�"ÌiˆgÍ*×]‡,o ˜¶B› =ÉG3'!›MÞ��0¸¡ó¼ ¸ÎZ-vu®dÌɯė÷ŠÏúE¬ Âü>ý¯Û×ÃWïµzuˆÀ»tõ7ßä`µi{±ø¶AH;½§’–s~�ÿeÔJ¾&ùÄä+ý-•z€–�8æ0®•Êu„?K@¢%^*…Ú®÷‹Æ»@Iò¹{{K  Óz‚1ý]� 9Ë^ª’ ¶Ú±]kᇬ>¬¾gž/…»u[¬a¿Ýu¢l®p®ù¾¯/Þ°(Á>æS�¬ßWû´³Äv3<ïxô¹+œÒ{Ô�xÕ¶FÇd#4H°‹zcÉm9ØÏ¹ hÖybqÜdÀ®\½’sŒIN 9+ÍI¡R}h9•·Åùèƒ@A¾Ÿd ¸q6³gÜ6ä¿Q@9›˜Éq„åý8ޙרf*¼á†³+æFHg5aͬ¹@!xÆ/>æ�Xl16faÜ’«Y‡ðôŸº óßù¹ ez>ùòª~Yx%œød�yQÿ�*˜^4¯€ÝZ|9l‹ás�»³>eÀ¤øB™høáŽQ]mgXX>¢[ÓCö3·ìÿ±’wŠIÞ5 òÖþš—öñtŒPLš®Û‚@™N‡Õ‡Ipvû#ùJGˆ+`š‹pñÈígyòõ(¾›r¸Öó–%\ ¿cÔþn­ªG>o’°³;žP?ˆ�y¡×°ž©4xÍq^W•/VÖð žP!PS¨pnÆé†46$¸µgJU€ ‹ýN}iµ—òó�-fOÃÍ^—F®+ÀFf¸:Û÷œè o¬.€ÞÎ íBaÀd!ß¿A¦çžg’{-ƒÜ�¿æ RShë¼,²&Ê$°Nl¢|€\yy&ãH êæ$xþ9]’M Ükºn·þ¶*ÈÍn ª~`t0žKý¾Eðl]—ê&NÁkM"L”‚ønÄ{úvž<ÒZ²D¹X£�Ȫ˜$}›…�¸g%ù BFÈùe©ÖRS¤)5mgD±rïÉ&È.F–ç"½‚Ì5‘¶IóÏ»äËrBI€ùHèåê÷ùÏ¿1M 0¸Ëuɼš* _ÉO÷å—d�¿õ×üSTj Ð_^çòð8L»¾*Cç:Z/Ý€NTŠP%?òÀÁé¬N.ØÁN[¿ðpdÁÛ…šïÈÉ´Ãk_RÍ® ² "šÁk¢´‰1³ìý·2iŠë8"⌈E}¦V(‡"|½ñ,ñÈ’c£ü€JR¦Yõ¶§#ér¯�î2ŽóáMfsç‘ÛPù¨Î�ÓÈ¡©�öçoCf±ËgF#*Í×¹'L‘·6tß‚°Ë*/Ÿ>²/Þ™É/«}¸Â�‹ )º~å0O¥–�€UÇÄ·4€”Ú@ÿôYSQ’ÕyÁÙ U°D¿N2Pƒù�m“K™�…ãˆzWϧ¼„]Ÿ&¼.Qæ@ú²Oˆùi"­Rä!‘n'¤kȶÌÞ5ñ˜½ã&Kò,­Uöºh¢ªœ¶yþòµÁb×°v7¢0T 6E!_ƒŒgï*�"^½9)×Fiµö�›3éTîùwˆüZ>kq‘§È£UÆuÖP".e1$¦xI_ ^¿™ì‹|Æ´-ùIÈ�&ëµm©Bç!È„‡4¿òX¤RkHÀ�GWÕk@«3o©Îû@#ÎÜ/„¾AÄ!;‹³ðh‰;ûy¬ê˸¿–c ðUˆ7¿¢�T¸LœˆõFÄ’‡ÔÊø3�>OÁ¼Zäú×çbk ‡y{øƒ�טÞ@ƒ£«†_pPJCÍ R”"Jy[6a5·�æjáPCƒ"ç»�™ª¢ÛöG#̶ ±*皎Ðç¶>I¡Õÿ˜d]3‡´‹}ž´Ò‹øù⣒…åHˆª„f»O bðÐ?â–,´Âe .ëpû•Ë2•ÚB„L�®ñxêÜìVø‘µ‰lçÆeÖÂ-ìZXâà'á)ã̓ð‰*ã9]¯­Èƒ*�³5ê"£ºwã“|ĆÃjØÛ†dØ\4×ïü�¬{=îë²}º©¦ÄI=÷5Ò æ‚9ÚÞ,0×âŒrÞSÉ¢ù£ùÏ_y†£zvssÊg�£dggwÓhìñ«“çÃŽ¡oÙ›ŸÚŒž%)v‡Ð¤„¡Oùr6èZ ƒ«5Aú¨ÏÉ]˜núzTø‰]v÷¹Hàð34Éø9BH¼:ÚØ»Â‡À„� >S¿ò¡R©ý¤ZÓŸ{%“§Á-¤q�¬Ÿ%PdÝ.‰¡#zpl÷rº‰I| ç‡Ë†d7Pðz{D�|2½îc.÷6ÙÒ�»%ìh^Ï‹bK:¶êèse6÷Þ½èήÄsoÆBѽ±~©¾2ièåÃÛk]G] }E·ÅU¢–·òçv×£Aéïש„¼B«2' ý÷¼FÓõ­®%oEçEôó£_ z ¹·ìëPyQyÿèïwÑ≥GNWPß>ÎH�¢8ôËtú¬ì�*\rŸÍ!¡"µ7aEA´ò\–3;ñå '"N² 8Íÿ‰¶½ N’TBã•÷ð@SÉæ‹-Ð(Â\Zè tÐÆsìoá”'WÌŒïè"ëç«�ë &#êq÷à½íH¹ÀR”�� zÁû>ºBèA]óÍö¬~(�­ßâr產b^¥)S‰¾ÿÑ0vÉûO”·oü�Ëö Tî8G]à*ãœßþEc5V\ïøªã™¦|døì¸¥,féÐÍw[�ž%´VõÉb{‡e\õ‚×`5eÞÀal =¨?UÀVkp§ë×}D߯½"I¿Fÿ°=T½O­ ÍrˆþpW0íc‰D/îA†›ê'„Е¿2áE`À럋^´í •J2z°÷Ú‡š Ð µâ帥IHiñoÿ…x¸·xpL;™ ¹ÕL²ç²õ@„C" šÌ!v†Ï"Ä9P×Ï2XðŒ¶xÆÌ^ G�#®7¡’�bÐîv4#Kª`�è:!]?[-†‰E' ­U°cs2~’�0Æã©->g´«VùúÙæìA¬âÞ¦ûjž˜Êè1‘µÏ|1iî3bÁž~XkÑ£Ž[¼±~¹‚fvXQˆûþ^Lcth�'ÐK¿þNzTB;—p‹[ÔG;Üî+¸€÷qnùЊ†%½�iÉByºû·%جpÓbÂm=n\¿p£mo¨ qmüÑþœ]Ž‘ª°,®VªN¯½  ±ø[§­ˆK*Bt:�Ø_x(ç•ø‘äÎÆ?µÑ бuaAúJÎ<',é.*j×·ƒá=á×´IZ;¹¡¤/¤G»b]²ÓÞ[>÷x¤üŠ?ÆzªW~wVV8¦ÖådyKó^�­t+Œu k»‡Y?|¿.ªÁÆ‚¿ªJê0ñÈÞÐĆÇÇù’«¡N±\+ïLûr,cbÛRØu,ÓÝøAÕî ØèÓÜAöã˜âXirF´ æ¸8‹Õ<±ÜÇ3¦«­Áòíy$µí=úî~¿ã|gÎ ?}&üäðü…m{D•'„7fŽkZCyÆ»)YËÊpÎYE G�vx|Ûè[!kÄO>¾µ¡é<ò<áàt:Ry:<Ë =Ûº¸MbºýžþT¿ø ݼq¿úù=ìkdšMª‘1&±¶y Kl ¶™hYslñ&SR_ús¦Sý¦Þ§}²[劳@ ÃØÍ›3Ź]ß°ÚªÎá{çXñ,ãç‹d)>ü2kXÍgaa¼sç�œ¢zü׎þÏû„ñŒçbJÞòáýϼuDØð³CC3\³ëÕ—-|ÒÃØSëí6R/±…í¯šôÌJ±ÌR¾Éáú$LÓ¯®ÍYê F}¿ø\iQ 3¿ï ŸÚ2ºÂgÂqŽk~áHÛ^Q•A�äýjS7|¡Ø*ÇÙeSØ>év÷<Ï<<™�¦[w–‚l g{-:…T{E\¯1A ‰îä›»sQ{›ÛªÉÇFPïL¹ŸG<6`9*B;œ­ì°Œ`M΄Éxìô¥¥J‚Ì]líŒå«S.o0ëƒ[žÚ sØŽŸ âòR"øðóÝç]”ñWüϋ᭻ñÝÕ½î5¡Æ8[¾wÁB¤-þNÞ©ÁÕÁ /Ü$+ÊçŠkenÓ¶ntÅw؈>ÕpÆ=âê×Õ<Ý�×.f|ßf��ñ úk@xµ§©÷‰ ®(H�Ý_-�÷‰Þ|•ú3»ä½ì–߉¼Šw\J+ÆFû&úx\‚0òþÕìÇÊØÊÿ ˜ð”gÀ“ð OÚöŒªšu$šŒ/ÇC3‰î\ZTØå�š˜?dýóÂ+ ‘‡¼FuÁÃҨ܌ìˆÏ1Ôð§›l–WÚö–Ï0JG ³ïã’sÒÙ‡ýÑrñ¾ä™4¬Q>)ØYºãuwjK¦±&Ö Q—$𸖺öo©ÚøWØ›õý[kü^•ƒŸÞ¾µwIÔë$>¢&%÷¬ó~¾(×E²¾�ÒaݱH,ÅC-Š+7¤Và=§ ÷UTâaío J|Pˆó§S9Þ-/`0 p �}Àë–“wÌZ»ŸÅ­‰=Üž\Q¸*4—¢å‚Kï“.¯Ý„âßl2Ø Hr¸­ì HðÔ,6Z±ìóI½+«“w YájË„«®Ä_¸JóÐV~ó.T¹&úRŠp¦¦Àï"mM 2“ÀG´¸Ù†L©W<ž°$£{g6—é)– ï©„ì†þUØ]ÑŸ•‹昃Šo ÖQß/e]3Ö™þ´È½‰Ï^›¼-RÁ)ÓmÇ·äÚàA� ßýŽÿ8}ÓòñªLÜX—»¶w¸ן²=tqäž¹sëi›Aüö�×VuÊÓ¸éØŒš‰Îþñ %'ʼn�¼ÁΟGl†‡¼Ñ¼©³â' ›/³á%ó^35´g'§\IÁL––pëD»ïoݧqͬ2ÇpáÓ“»ò_à’’Uã?Eïà’·MŒFSqïÔÆØ¢7þbã#-C '‡š`g4¸ñíü„êÏÅ�Wø:2ố_•_ø~¢u{$† ŸÉTC8ËcR¨%ðÇ´ÈqQîÈAµ ÎE$U¼/RpG0*|¢/¼éGMJEvuåjb#-.fTŸÄÒñÀ�jYMØÓˆd„4'®t‘xM³w®ØÅþº»ön{bKH]äEÜL¾ãø$ç#¼OÝ ÍìÇ/è¼;6[û¿˜&>ÙrJ”,|î\ß³™ŒÅ|�té’7«=Ÿ’É®ÒPšYwQC\Äm?ùQ³ÌgÉi½Ioú¢œÈésè‡[�?ï<•c3lCÞõáEÞ+S²éägrI‰¼&zÍ÷úC*ä”Û9ÎYkÉVd™/[«‚R…_ÐUï&yÏfy!ï?IÌò/Æ ¼°8£ù‡œo�ˆäD$ó~¯™y¯âqÐÔ•D>™ðÕ§åÝ>rX½c;Ç‚?™Œ®Ï½±>–,^¼UŒCçÙmTUK×0Ÿ¼."y£=ç-òøŒTÍòÌ2ÁÃ×='¢‚lú“ÖW�#fß5¸/–‘ïFîÒã©Ì¯šõa çf�­¤_eSþìÆ{½¹€¼¬kè-_ +²œÛÄ”ÌÖö°°�“# lj©ËøW‡ ‡™\ÔHFa]ìGÜU.&!{w.·{snYV)žÀ5·sHý6¾ I¶¿:�Ì÷ASa‡pÓîþ½}ç6ãõ— Ü7‡1±Ç9~)Y-™?�@ð'ìÿnNÂ÷�aûß­ôoˆÿË�áú7ØÿaA@³Š�gø…/Ú’YÆAjôÓíX]kìPr±Ø¶Ç8¸U^ÿ|¾ùò&ÎèÑ�eòê¶Ppù4%ÉêÃ`õZ…‹,R^!�ù) )œöÂÜ]ÔsR“Š1›‹}¦3X·™ågUœÑ“4ç|ØϦ‡DDŠ„àþá ç ¬t\Íðþ9®èfü¥f$U¦lßFÈÕÊMd &us©’`ÉŠÇjJC øleá.ÕNáß Zy¸"'Ä&Û <¯²¿Ô¥bJ›«Þn·Mî³ñCÅöà›[Ðzƒ;pÔß¿µîÕ>)öš‚àö츪ûF °iìÖdªȹ³I5.^HÙHü½Åžlí«ÝÆÁÄÎvé¦ñ'äÇ4kz&^xÑÚ¨„ÕgWlЉZ¸Îyé¡§Ï>ÿì?èýoýMÿ¦¿ö?~V€—�aXbLqú•,I^«dtpUÍØÀpïXOá’qÏù~T—I¶_Ù+¿•’—¤“í~3�RÿíÁÓ³F,߇éµôÃV7×cʹ1:vCZ& ˜Ma+J7xÛK•qþQí¯ÿœÀ›£ÓÞó=OÀ9Éÿþ k-Îwr¥é��­,åV¤Kmt´[¼±œ‘-ɆàaÎL‰� Vé˜ÉÑKç@£Qê¶ÎÏQÀÒ#y=ç˜Ý¸ü4UïàV�íö1(ÉæwüÒ^ƒ®4¹Ñ¥Œ·çEð”‚%­ãCŒÞÇÀÇ댒]K½»£ì¬GHg2N¼KÏ’<äDEÀÿÊÈ¥"/0Û§2w8Îì’ó¿PtJ¸Yì(ö-�Ý2\÷À4Nà€»jð°#kÒò/ãù[ú½÷à?"þà°í7–¿:Þª!ÅRhxºÎbR¡H›$–ÕÁX51–�T®Ÿ”%w’ŒLõèÍä(�ÖIÛ¿Ì)/Üx§�’yí¨–•&fC”Úe«|SÓãït"}Àª<¾E­oUĵªŽ£:g|�gËeöÉLœ_~VaqC7l]Öï6Æ ýt¸nÓU�5ûJ xÊ!°ãÃÆ§ âg�F˜Ó0F½” ªïÂ|°"æc~µøÚè‹úªõ�ûkü^HŒŸ‘Áœ «yÐÄPY¿G„§·xŒIqÂ-Åu†2Ÿø`㎽7¿´ôp ¥aüpâá¾vy..(RËÉ+º DÖV̳´Âä–ÔûK.¨F±<…I/b,D4SaC9³z „ãe9â,`¼B¶È§}*°txßóƒp,ùü}ž�½�D)¯ŸNô?8©ýÆÉÿ§Â󶕬ã¢FÀB¼¡lo&©­zª~bµÝxß_c#%dcõ‚0—ž”æÐÇ­ê£ÜØ“Ûß'\šb1άšëŸã‰x3á™Ú rÄÍÚ¦�#z5V‹ÔåMׂ«âB{—&Ì€¬=CÌõy"(*�~°­´qDN~c‡J±ž^r¡Z�4ÝT¾fð\ iÜ% n-irö ¤ðmΈy»ü†MÂiËÔÛ]EèŠË|+A•ÐÜ�¬ì@6ù-õ#FˆÝÀ½ÞÒó R:Ì©›aj‹"¼c±”MN(öÙB}^¥;úÐ*[#—艜¸{wk²z!⣄=+§Ð7cAŒèŠæ”—Ý:íÑ%ƃГVæ¨õÇ­þ ½ámì¥ûi!Ï¥ŠæK9¤ ©ñ^{7ºfÞžûqý4L·\×à×»×€²K…txV®ëlÀ§@ØÓÞšœa`€³ÖÑ9œ?~nËM÷rº«ˆTí˜\Ô>Ð+Æ™:"W)*uâˆcê”+�»ãx#lK:„:ä¸ÏóU¹¡Ùù˜>ûØ`rOZ¡;óºmç@÷ÊB~µöH¡jÑÜsËuåÈÑ YüâƒèöçšJ¡í høùãÔ’‚&¤-Ó6rÒ¼ ‰­ar&k×#îËceûŸ#á0ý±þRô™nâ-5Ÿ‹z‰›…»zÒЉ×<¯«hÑHM¿¥âä„/ ':ÌÛØ¡cûíV„ ‘e3žOÝ%‹\ØVšÙŽ.À„ã…­ÊíðãvÉh‘jôRmJß“j%tº«|êÖÂnOÿà¶ý7n¿¸íÊ{"ævkÂÈКõýìu㱫ÒÌ<š¹Ò"ü[‰4‘B¬ J?·ò‚ùú»I~Švò¡öÅ=S˜Z÷¢å''ü–b[@AE> Ê¿îPÜþ÷äƒ�x÷ �܈V°udò:ëÔ:xª)pchš)|ÊDeaOìŸKß"v¶ê›Ùvû1ad2U’*'ŠèöêêzHµ0í/¿{$«6¾2YŒ¦vuÙYr§#«êá�£�¥HÝ\¶÷.g:vP5¸Hb„K;?UžCÙ!욎_ÖP5Ç b¾%k©â)V4Jž•ëzè¾´›©µkr†ù›¨Íò<\¢›©‰Ú¦Vð«¿û�·\ýج¿uÄ]½Š¬¿Œ¾‚4cѬ¿Î’Z(™¬¿Pþî5&¬¿î }°Œ ­¿ù*8¬¿¿€^¸sa¬¿¬Žé ¬¿u‘BYøúª¿ƒÜE˜¢\ª¿ë©ÕWWª¿ôÞ€«¿ëR#ô3õª¿¤q¨ß…­©¿ûu§;O<§¿mãOT6¬©¿¿€^¸sa¬¿êÐéy7°¿;nøÝtË®¿²×»?Þ«®¿ Y2Çò®¿š´©ºG6¯¿V¹Pù×òª¿$—ÿ�~ûª¿ˆñšWuV«¿N 4Ÿs·«¿ÄAB”/h©¿ËLiý-¨¿ý0Bx´q¬¿�Z ¦}«¿€cÏžËÔ¬¿A¼®_°®¿|𓙀§¿Ì^¶�¶F¤¿UDÝ ¥¿8„*5{ ¥¿!å'Õ>§¿t ‡Þâ᥿ÒâŒaNЦ¿iêwak¦¿F–̱¼«¦¿±KXc§¿¡/½ý¹h¨¿<ùôØ–§¿hé ¶O¦¿�ÿA€ ¥¿Þ‘±Úü¿¢¿ˆôÛ×�s¦¿ãR•¶¸Æ§¿D¨R³Z©¿BëáËD©¿ÊÂ×׺Ԩ¿A~6rÝ”ª¿¯°à~À£¿Gÿ˵hª¿I‚pª¿˜Q,·´ª¿ßj�¸¯¨¿—üSªD©¿p|í™%ª¿ 4ØÔ©¿d¬6ÿ¯:ª¿ÀÎM›qª¿Ð}9³]©¿�´TÞŽpª¿N³@»CŠ©¿Jï_{f©¿ŠUƒ0·{©¿÷@¨¿,H3Mg§¿IºfòÍ6§¿$0ðÜ{¨¿®Ÿþ³æÇ§¿£“¥Öû�¦¿²Õ唀˜¤¿æèñ{›¦¿Ház®G©¿73úÑpʬ¿Âûª\¨ü«¿Åv÷Ý—«¿ßPøl¬¿0o�»¬¿rˆ¸9• ¨¿ˆ,ÒÄ;¨¿Ãð1%’¨¿(ó�¾IÓ¨¿ˆØÒ£©¦¿Ç.Q½5°¥¿‘œLÜ*ˆ©¿g›Ó–¨¿¯xꑦ¿Št?§ §¿­ü2#¥¿�„ÒBΣ¿ê‘·µ…§¿õKÄ[çߦ¿(`;±O¨¿Ð}9³]©¿¨SÝ£¿Z¼X"§Ÿ¿¢'eRC ¿¾Ý’°«¡¿¤ö{b�¢¿Å­j¡¿”Kã^I¢¿ì�±¾�¡¿Š�º�}å¡¿ÚX‰yVÒ¢¿O#-•·#¤¿|E·^Óƒ¢¿h”.ýKR¡¿%”¾rÞŸ¿9*7QKs›¿²ó66;¢¿±n¼;2V£¿´;¤ Ѥ¿¨8¼Z­ÞávhX¤¿æ‘?xwIœQ�¿:W”‚U¥¿[•DöA–¥¿L4HÁSÈ¥¿€�›6ã4¤¿iàG5ì÷¤¿›«æ9"ߥ¿a©.àe†¥¿Vïp;4,¦¿¦ ÐÒ¥¿®×ô  ¥¿†ŽTâ:¦¿T8‚TŠ¥¿ŽW zR&¥¿Ö70¹Qd¥¿ÄìeÛik¤¿‰$zÅr£¿õŸ5?þÒ¢¿ºW•}W¤¿‹3†9A›¤¿Ù@ºØ´¢¿‡¦ìôƒº ¿Àìž<,Ô¢¿*ÖT…¥¿|·y㤨¿8ýÚúé§¿ 7U†q§¿zR&5´¨¿³\6:秨¿Õæ!S>¤¿iý-ø§¤¿/iŒÖQÕ¤¿Ù²|]†ÿ¤¿¯þ·’£¿†¯¯u©¢¿,)wŸã£¥¿¤ÿåZ´¥¿ŸåypwÖ¦¿_b,Ó/§¿ê%Æ2ý¡¿ÿ²{ò°P›¿czÂ(›¿Mh’XRîž¿´�ü6Ä ¿75Ð|ÎÝž¿BÌ%UÛM ¿�‹jQLž¿g}Ê1YÜŸ¿)&o€™ï ¿úE ú =¢¿à€J• ¿Þ Šcž¿Aœ‡˜N›¿«?Â0`É•¿bLú{)< ¿®¶bÙ=¡¿=ºq¢¿K®bñ›Â¢¿tµûËî¡¿ÓjHÜc飿 yçP†ª˜¿qXøQ £¿ADjÚÅ4£¿óWÈ\T£¿GÌìóå¡¿Zg|_\ª¢¿¤aQ§£¿¹8*7QK£¿Ržy9ì¾£¿¾L!u£¿?S¯[Æ¢¿3ùf›Ó£¿ç6á^™·¢¿O"¿£¿ð¾**ÿ¢¿q¯Ì[u¢¿¸æŽþ—k¡¿¢'eRC ¿ š²Ó¢¿yËÕ�M¢¿µ‡½PÀv ¿È&ù¿b�¿då—Á‘ ¿ï9°!£¿oFÍWÉǦ¿[•DöA–¥¿ª€{ž?m¤¿OêËÒNÍ¥¿Ž®ÒÝu¦¿£ÉÅXÇ¡¿/ùŸüÝ;¢¿Ô¹¢”¬¢¿è£Œ¸4¢¿N`:­Û ¿.� ø1æž¿³A&9 £¿?Ȳ`â�¢¿aÅ©ÖÂ,¤¿mæ�ÔBɤ¿Ä QºôŸ¿G�J±£q˜¿æ;ø‰è—¿Õ[[%Xœ¿|`Ç�  ¿Bx´qÄZœ¿©÷TN{Jž¿žACÿ›¿>"¦D�¿bK�¦z2Ÿ¿ý-ø§T¡¿VðÛã5Ÿ¿¾/.Ui‹›¿ƒÚoíDI˜¿5Ó½NêË’¿x%És}ž¿ñFæ‘? ¿ÈzjõÕU¡¿òµg–¨¡¿ÈÑYùe ¿$—ÿ�~û¢¿»DõÖÀ–¿ñd73ú¡¿ µ‰“û¢¿¸\ýØ$?¢¿RóUò±» ¿úÏš¡¿vþí²_w¢¿+5{ ¢¿ ûrf»¢¿³{ò°Pk¢¿9€~ß¿y¡¿å(@̘¢¿}¢|¡¿þ¸ýòÉŠ¡¿jݵßÚ¡¿ƒmÄ“ÝÌ ¿O±jævŸ¿Òá!ŒŸÆ�¿ç©¹n ¿˜3Ûú`¡¿Ž¿·éÏž¿Ð³Yõ¹Úš¿MK¬ŒFž¿dyW=`¢¿†åÏ·K¥¿È¨p©¤¿ÏÏÙB£¿øO7Pà�¤¿\='½o|¥¿hvÝ[‘˜ ¿þÕã¾Õ:¡¿ ¥+ØF<¡¿mXSYv¡¿ûÍÄt!VŸ¿†Ç~K‘œ¿?ªa¿'Ö¡¿Swe ®¡¿'Ø�›6£¿l•`q8󣿅ÏÖÁÁž¿Þ Z+Ú—¿j‰•ÑÈç•¿�+J Áªš¿„*5{  ¿R€(˜1›¿ÙwEð¿•œ¿<ÁþëÜ´™¿,¾-Xª›¿¬Ê¾+‚ÿ�¿× /½ý¹ ¿ØºÔýL�¿5µl­/š¿Ð™´©ºG–¿ºóÄs¶€�¿B[Î¥¸ªœ¿4Lm©ƒ¼ž¿ù£¨3÷� ¿ñd73úÑ ¿I„+ PŸ¿q¯Ì[u¢¿÷W�ûV딿"ÝAìL¡¿�6ŽX‹O¡¿´­f�ñ}¡¿Øñ_  ¿8Ùî ¿Ù|\*Æ¡¿ÛkAï�¡¿t 34ž¢¿æ²Ñ9?Å¡¿F#ŸW<õ ¿ÀÎM›q¢¿-ê“Üa¡¿(›r…w¡¿¡º¹øÛž ¿ˆFw;S ¿ßRÎ{/ž¿�¶*‰ìƒœ¿ãÝ‘±ÚüŸ¿™*•Ô  ¿äø¡Òˆ™�¿úÏš™¿•žé%Æ2�¿’±Úü¿ê ¿’’¦¿Ù=yX¨5¥¿�ᱟÅR¤¿Ýξò =¥¿¡JÍh¦¿“o¶¹1=¡¿°ÿ:7mÆ¡¿aà¹÷pÉ¡¿w|ÓôÙ¡¿l|&ûçi ¿Oqž¿¸çùÓFu¢¿%És}¢¿€'-\Va£¿"¨½ ¤¿9ïÿã„¡¿d’‘³°§�¿xòé±-ž¿ïU+~©Ÿ¿—ÒþX£¿SäG ¿ßýñ^µ2¡¿%Ïõ}8H ¿Á¨SÝ ¿Æ2ýñÖ¡¿í¸áwÓ-£¿�*ß3¡¡¿ æ¯�¹2 ¿-´sšÚ�¿H‰]ÛÛ-™¿£ x|{× ¿¶�;P§<¢¿ùNÌz1”£¿³Z�!«£¿|(Ñ’ÇÓ¢¿ô¥·?¥¿Œ÷ãöË'›¿oÖà}U.¤¿�‘ao¤¿Áæ<š¤¿œ3¢´7ø¢¿¸ [–¯£¿*kg{¤¿Ÿp]1#¤¿žâ<œÀ¤¿±Ÿ`¤¿Â5wô¿\£¿¸éÏ~¤ˆ¤¿ ¾iú쀣¿{NzßøÚ£¿¡½úx軣¿ì…¶ƒ£¿(º.üà|¢¿«“3w¼¡¿�Øî û¢¿Ô×ó5Ëe£¿-å}Í¡¿,g~5 ¿kšwœ¢#¡¿Â†§WÊ2¤¿QJVÕ˧¿Qù×òÊõ¦¿¢©ÛÙW¦¿¢&ú|”§¿ýfbº«§¿âÊÙ;£­¢¿333333£¿§>�¼s(£¿ÎˆÒÞà £¿‹ˆbò˜¡¿ª, »(z ¿Æ4Ó½Nꣿ_|Ñ/¤£¿{‚Äv÷¥¿Ï ¡‚‹¥¿Jð†4*p¢¿ÁÅŠLà¿çR\Uö]¡¿ªÕWWj¡¿NA~6rݤ¿Útp³x¡¿rÞÿÇ £¿aþ ™+ƒ¢¿g(îx“ߢ¿c ¹§«£¿v‹ÀXßÀ¤¿Õ”dŽ®¢¿�C¨R³¢¿”™€_#¡¿ÑA—pè-ž¿õÙ×3¢¿jjÙZ_$¤¿¦^·Œ¥¿ëTùž‘¥¿îZB>èÙ¤¿ØžY ¦¦¿«Íÿ«ž¿C8Ù¦¿ ]Þ¦¿R¶HÚ�>¦¿'l?ãä¿Év¾Ÿ/¥¿°«ÉSVÓ¥¿û&7Ь¥¿D¥3û<¦¿ ¦–­õ¥¿˜iûWVš¤¿æCV¸¥¿·Aí·v¢¤¿×L¾ÙæÆ¤¿†!YÀ¦¿ã¦šÏ¹£¿må%ÿ“¿£¿�Ò¥I*£¿^J]2Ž‘¤¿0œk˜¡ñ¤¿€ôMšE£¿Ú‘ê;¿(¡¿!èhUK:¢¿Ý”òZ Ý¥¿Q†ª˜J?©¿OqN¨¿Ÿ>øù§¿Xÿç0_^¨¿ ¦šYK©¿�l®šçˆ¤¿pìÙs™š¤¿ìjò”Õ¤¿R)v4õ£¿LÃð1%¢¿ƒ3øûÅl¡¿Ù[Êùb勵EœN²Õ夿Tÿ ’!Ǧ¿z©Ø˜×§¿ÙìHõ�_¤¿ýi£:È¢¿ñ·=Ab»£¿aŠriü¢¿8-xÑW�¦¿¸Ku/£¿î\éE¥¿ö$°9Ϥ¿ -ëþ±¥¿eÅpuÄ¥¿ÿè›4 Ц¿ ¦}s¥¿!u;ûʃ¤¿$*T7£¿>ÏŸ6ªÓ¡¿8en¾Ý£¿?ªa¿'¦¿·D.8ƒ¿§¿ 'LÍʦ¿ó«9@0G§¿;�´TÞŽ¨¿�imÛk¡¿ƒOsò"¨¿'0�Öm¨¿6>“ýó4¨¿®)�ÙYô¦¿¦·? §¿æXÞU˜§¿odùƒ�§¿‡ˆ›Sɨ¿Ö ˜£Ç§¿~įXÃE¦¿sJ_9§¿h‘í|?5¦¿Ä@×¾€¦¿ãúw}欧¿ÚpXøQ¥¿QÛ†Q<¦¿{ø2Q„Ô¥¿L5³–Ò¦¿æ®ò¦¿v©ú™z¥¿÷‘[“n£¿4„c–= ¤¿ (ÔÓGà§¿„-vû¬2«¿÷ç¢!ãQª¿­„î’8+ª¿X¾Û¼qª¿Étèô¼«¿Ãžvøk²¦¿ŽË¸©�¦¿ßp¹5馿EÖJí¥¿{¡€í`¤¿IƒÛÚÂó¢¿Ë2�g§¿ô噗æ¿]þCúí먿 Qºô/©¿1 ò¦¿¯D ú‘¤¿¬„¹ÝË¥¿‹‹£rµ¤¿|¸ä¸S:¨¿Ø�›6㤿ÜõÒN§¿œ‡˜N릿F^Öħ¿)uÉ8F²§¿Èyÿ'L¨¿•ð„^§¿w…>XƆ¦¿d:tzÞ�¥¿NðMÓg¤¿F—7‡k¥¿„*5{ ¨¿É­I·%r©¿ùK‹ú$w¨¿6?þÒ¢>©¿_@/ܹ0ª¿eU„›Œ*£¿¼vié¿ã7…• *ª¿£!ãQ*á©¿¼± 0(Ó¨¿r¦ ÛOƨ¿*äJ= B©¿ï9°©¿G²�t±©¿žz¤Ámm©¿õôøÃϧ¿&Î5ÌШ¿"Þ:ÿvÙ§¿0™ò!¨¿ "RÓ.¦©¿³í´5"§¿‹6ǹM¸§¿‚9züÞ¦§¿¼Yƒ÷U¹¨¿kÔC4ºƒ¨¿Êû8š#+§¿øŠn½¦¥¿=›UŸ«¥¿é)rˆ¸©¿‚Uõò;­¿zlË€³”¬¿# Â¤R¬¿Ó¾¹¿z¬¿üÖMõ¬¿¥hå^`V¨¿BA)Z¹¨¿M/1–é—¨¿­lò–«§¿ˆÖŠ6ǹ¥¿¤á”¹ùF¤¿’”ô0´:©¿´Éá“N$¨¿�#�›ª¿âÊÙ;£­ª¿‰îY×h9¨¿}?qý¦¿ô‡fž\S¨¿7�íµ ÷¦¿á þ~1[ª¿@½5_%§¿–Tÿ ’©¿7À[ A©¿ý-ø§T©¿î<0€ð©¿� raŠª¿¡0(Óhr©¿sÙ蜟⨿³–Òþ¨¿Ï¾ò =E¦¿ÌDR·³§¿{Øœƒgª¿ùÙÈuSÊ«¿­C9Ñ®ª¿?É6‘™«¿1±ù¸6T¬¿&Š�º�}¥¿5·BX�%¬¿ÄìeÛik¬¿gd�»S¬¿C�B’Y½«¿½qR˜÷8«¿î•y«®«¿Œ½_´Ç«¿Mø¥~ÞT¬¿Û¿²Ò¤¬¿rm¨çoª¿ùNÌz1”«¿r‹ßVª¿ÿwD…ª¿â|~!¬¿—üSªD©¿/ùŸüÝ;ª¿ è…;Fª¿•ô0´:9«¿}гYõ¹ª¿ö–r¾Ø{©¿ ’>­¢§¿Màô.Þ§¿r4GV~¬¿k¶ò’ÿɯ¿C�ƒfÚ®¿€µjׄ´®¿Iô2Šå®¿z4Õ“ùG¯¿Œd�P3¤ª¿&¬�±^ª¿ÝéÎÏÙª¿ë©ÕWWª¿Ñ;pÏó§¿Çb›T4Ö¦¿þEИIÔ«¿Å’r÷9>ª¿¤§È!âæ¬¿Úå[Ö­¿_Ñ­×ô ¨¿l^ÕY-°§¿(ó�¾IÓ¨¿Œi¦{�Ô§¿î%�Ñ:ªª¿ä-W?6ɧ¿ãßg\8ª¿H§®|–ç©¿:–wÕæ©¿)í ¾0™ª¿>³$@M-«¿“V-ª¿¢–æV«©¿EeÚʢ¨¿‘·\ýØ$§¿Š!9™¸U¨¿¨SÝ«¿—6–~¬¿EÕ¯t><«¿›äGüŠ5¬¿Ë,B±4­¿õ…�óþ?¦¿b¾¼û謿Ž ­¿9™¸U­¿C —8ò@¬¿Œ½_´Ç«¿ŠÍǵ¡b¬¿¶Û.4×i¬¿ÂLÛ¿²Ò¬¿“ýó4`�¬¿Vn2ª «¿vü¬¿qXøQ «¿²¡›ý�r«¿×ÁÁÞÄ�¬¿yy:W”ª¿3Pÿ>«¿�Ò¥I*«¿ì.PR`¬¿¥0ïq¦ «¿¤TÂzý©¿^žÎ¥„¨¿[#‚qp騿”0Óö¯¬¬¿T7Û°¿B˜Û½Ü'¯¿ªðgx³¯¿:“6U¯¿Óiݵ¯¿ ø5’ᪿGW#»ª¿–Zï7Úq«¿� raŠª¿…–uÿX¨¿»òYžw§¿!ê>©M¬¿Àϸp $«¿D¤¦]L3­¿ËòuþÓ­¿ÃÔ–:Èë©¿#ºg]£å¨¿LàÖÝ<Õ©¿mrø¤©¿¤aQ§«¿½äòw憎F}’;l"«¿ÖR@Úÿ«¿d"¥Ù<«¿uWvÁàš«¿Ÿu�–=¬¿ê²˜Ø|\«¿fõ·Cê¿fh<Ä©¿T;ÃÔ–:¨¿ò]J]2Ž©¿°Œ Ý쬿wÜð»é–­¿Õ³ ”÷q¬¿ð£ö{b­¿—ª´Å5®¿â”¹ùFt§¿l—6–®¿ûÌYŸrL®¿@‰Ï�`ÿ­¿ßÜ_=î[­¿êy7­¿‡¯yUg­¿nߣþz…­¿EõÖÀV ®¿'�µ¿³­¿õÛ×�sF¬¿!YÀn­¿Ð—Þþ\4¬¿}—R—Œc¬¿¯xê‘·­¿Ç¡~¶f«¿– # Â¬¿– # Â¬¿B–­¿åïÞQcB¬¿/o×j«¿¤6qr¿C©¿» )?©ö©¿#„GG¬­¿ç4 ´;¤°¿¾¾Ö¥F°¿†Yhç4 °¿—ÿ�~û:°¿Ã¤Rìh°¿—6–~¬¿Âûª\¨ü«¿­iÞqŠŽ¬¿$ð‡Ÿÿ¬¿¿›nÙ!þ©¿›®'º.ü¨¿¼#cµù­¿—«›äG¬¿‘ñ(•ð„®¿ùÜ ö_箿:]›�«¿HÞ9”¡ª¿2“¨|š«¿¸"1A ߪ¿LÆ1’=B­¿ò^µ2á—ª¿ 34ž⬿¯w¼W­¬¿Áüýb¶¬¿"Æk^ÕY­¿C�=·Ð­¿UkaÚ9­¿ª€{ž?m¬¿ÂhV¶y«¿›ÿW9Ò©¿Ô×ó5Ëe«¿€d:tzÞ­¿s×òA¯¿ªÒ×øL®¿*Wx—‹ø®¿ ³³è� °¿j¿µ%!©¿.9î”Ö¯¿Ëø÷°¿¯ÒÝu6䯿PãÞü†‰®¿¹Æg²ž®¿Qù×òÊõ®¿œ‡˜N뮿øo^œøj¯¿=,Ԛ毿nj ùœ»­¿¿eN—ÅÄ®¿‰±L¿D¼­¿�¥½Á®¿ 7U†q¯¿9}=_³¬¿ãm¥×fc­¿ª�æsîv­¿‚Uõò;M®¿U1•~ÂÙ­¿À>:u峬¿*«éz¢ëª¿x úÒÛŸ«¿™�Êø÷¯¿á´àE_±¿X’<×÷á°¿a3ÀÙ²°¿]RµÝß°¿QO�?ü°¿�3MØ~2®¿P«”žé­¿iêwak®¿á%8õ�ä­¿“ÅýG¦«¿ºõš”ª¿ E�¹‡„¯¿ùf›Ó®¿ŸÉþy0°¿þ,–"ùJ°¿R)v4õ«¿ˆ×õ v«¿Q1Îß„B¬¿¶½Ý’°«¿›«æ9"ß­¿Pà�|zl«¿‹©ôÎn­¿ü¦°RAE­¿Úå[Ö­¿6Y£¢Ñ­¿–u®¿iþ˜Ö¦±­¿¸! _B­¿Î‰=´�¬¿Y4�� Žª¿\…zú¬¿‘ñ(•ð„®¿Màô.Þ¯¿¡K8ô¯¿n‰\p¯¿;‡ú]°¿(ôú“øÜ©¿ùÀŽÿA°¿ãl:¸Y°¿tîv½4E°¿Ü�µÛ.4¯¿ÆÂ9}=¯¿ø§T‰²¯¿!x|{× ¯¿{-è½1°¿qÊÜ|#º¯¿áëk]j„®¿Ð%z‹‡¯¿'f½ʉ®¿{„ò>Ž®¿%<¡×ŸÄ¯¿WÑšyr­¿ŸwcAaP®¿þ*Àw›7®¿ì1‘Òl¯¿-ÒÄ;À“®¿Ì:“6­¿¨T‰²·”«¿ýØ$?âW¬¿}%�»¶¯¿F—7‡k±¿ÓNÍåC±¿�:豿»¹øÛž ±¿q‘{ººc±¿µ�?QÙ°®¿Ä>#K®¿�@gҦꮿ÷<Ú¨®¿`’Ês¬¿úDž$]«¿O�?üü¯¿þHVñ®¿Ì EºŸS°¿ßºñîȰ¿Gªïü¢­¿paÝxwd¬¿Þ„€| ­¿ÅE�¹‡¬¿Y |E·®¿‰šèóQF¬¿ÀÙ²|]®¿ù¾¸T¥-®¿®€B=}®¿†!rúz¾®¿=·Ð•T¯¿( ß÷o®¿�¨PÝ\ü­¿Ë¡E¶óý¬¿>³$@M-«¿Y�|^ñ¬¿=·Ð•T¯¿Þ¬Áûª\°¿=}þðó¯¿p±¢Ó0°¿Û‰’�HÛ°¿Ð€z3j¾ª¿~©Ÿ7©°¿£té_’ʰ¿NŸt"Á°¿ãÝ‘±Úü¯¿K’çú>°¿ š]÷V°¿ôPÛ†Q°¿Š§wñ~°¿Ð›ŠT[°¿s�FZ*o¯¿ñJ’çú>°¿ýÛe¿ît¯¿ø§T‰²¯¿”¥Öû�v°¿´CV·z®¿è÷ý›¯¿u殿lê<*þ¯¿k+ö—Ý“¯¿Œ¢>+®¿W•}Wÿ«¿LnYk(­¿ìŠáíA°¿DÂ÷þí±¿[éµÙX‰±¿ñ*k›âq±¿_ÎlW胱¿¬„¹Ý˱¿»}V™)­¯¿øo^œøj¯¿,¹ŠÅ¯¿š´©ºG6¯¿Ç,{ج¿Õ³ ”÷q¬¿cÙ=yX°¿?�¨Œ¯¿­Lø¥~Þ°¿të5=((±¿kdWZFê­¿ÕyTüß­¿²�t±i­¿]5Ïù.­¿`=î[­¯¿s-Z€¶Õ¬¿q:É®¿Yà+ºõš®¿UOæ}“®¿ÁþëÜ´¯¿V¶yËÕ¯¿HüŠ5\䮿0c Ö8›®¿ô4`�ôi­¿*É:]¥«¿|HøÞß ­¿¢DKO˯¿8ƒ¿_Ì–°¿%Ïõ}8H°¿ª€{ž?m°¿{Cr2±¿é¹-@«¿±‰Ì\àò°¿Æ¬q6±¿P§<º±¿R �hE°¿Þ¬Áûª\°¿SvúA]¤°¿‚69|Ò‰°¿Õ–:ÈëÁ°¿4J—þ%©°¿,g~5°¿aTR' ‰°¿„~¦^·°¿K‘|%°¿¦~ÞT¤Â°¿W@¡ž>¯¿0G�ßÛô¯¿ñ»é–⯿_š"Àé]°¿ßmUÙ¯¿ûWV𔂮¿ù‚0º¬¿’®™|³Í­¿X”†…°¿BCÿ+²¿Þèc> б¿ß¿yq⫱¿½á>rkÒ±¿ñ�ÿ²¿ürf»B°¿^fØ(믿€�›6ã4°¿L8 ¥+°¿Ïƒ»³vÛ­¿ÏÚmšë¬¿ÁÅŠLð¿J·%rÁ°¿Ï„&‰%±¿‰&PÄ"†±¿ ²eùº ¯¿Ý'G¢`®¿Ÿ`<ƒ†®¿¹-@Ûj®¿Þu6䟰¿ÚæÆô„%®¿X‘ÑIد¿3ù¼â©¯¿æXÞU˜¯¿amŒ�ð°¿ î<0€°¿AJ˜iû¯¿ïU+~©¯¿š–X�|®¿Å_ѭ׬¿Mu€Ô®¿è„ÐA—p°¿�!«[='±¿€^»´á°¿xñ~Ü~ù°¿K­÷í¸±¿jMóŽS¬¿§wñ~Ü~±¿Ù$?âW¬±¿ƒ¾ôö碱¿¢E¶óýÔ°¿2w-!ô°¿àسç25±¿V¸å#)±¿¬Xü¦°R±¿[²*ÂMF±¿¦&ÁÒ¨°¿*Œ-9(±¿™ðKý¼©°¿Ûe6Ȱ¿DP5z5@±¿lÌëˆC6°¿ß‡ƒ„(_°¿ølìM°¿ä‚3øûŰ¿‡-y°¿`[?ýgͯ¿ÖµÂô­¿8ÀÌwð¯¿CqÇ›ü±¿Y2Çò®²¿=¶eÀYJ²¿ñH¼<�+²¿8öì¹LM²¿-z§îy²¿ŠXİØ°¿Îp>?Œ°¿@ÜիȰ¿ü8š#+¿°¿Z)r‰#¯¿9}=_³\®¿CÉäÔÎ0±¿ü8š#+¿°¿jÙZ_$´±¿Õz¿Ñ޲¿ Š·˜¯¿ö~£7ü®¿Ü M¯¿~âú}ÿ®¿Ì EºŸS°¿ªek}‘Ю¿ŸÉþy0°¿•ñï3.°¿=Õ!7à °¿“r÷9>Z°¿E½àÓœ¼°¿¾Mö#E°¿á+Ù±°¿²ºÕsÒû®¿®bñ›ÂJ­¿\�Âõ(\¯¿O=Òà¶¶°¿"Æk^ÕY±¿³ïŠà+±¿�Þp¹5±¿›sôø±¿8fÙ“À欿‰]ÛÛ-ɱ¿p$Ð`S籿�Ú¦¶Ô±¿¶¡bœ¿ ±¿¸¯çŒ(±¿ö–r¾Ø{±¿Þ­,ÑYf±¿'½o|í™±¿ Ê4š\Œ±¿©ÐDØð°¿õ"rl±¿Y�|^ñ°¿ R)v4±¿žÒÁú?‡±¿Mƒ¢y‹°¿äõ`R|°¿;‰ÿ"h°¿B>èÙ¬ú°¿•c²¸ÿȰ¿ ·_>Y1°¿!<Ú8b-®¿:“6U¯¿>”h±¿R||BvÞ²¿‚8'0�²¿CUL¥Ÿp²¿Y4�� ޲¿f¡�Ó,в¿|*§=%ç°¿s�ËcÍȰ¿ÖT…]±¿„Ó‚}±¿ñcÌ]Kȯ¿âeS®ð®¿:[@h=|±¿²dŽå]õ°¿g Ü¶ï±¿ñôJY†8²¿�Ì΢w*°¿¼°5[yɯ¿.®ñ™ìŸ¯¿»}V™)­¯¿A€ ;¨°¿s�FZ*o¯¿ÒUº»Î†°¿�I›ª{°¿jjÙZ_°¿à¾œ3¢°¿ÁãÛ»±¿.ÎR²œ°¿!ɬÞáv°¿¶Ov3£¯¿7R¶HÚ­¿scz°¿}]†ÿt±¿>w‚ý×¹±¿„fÚþ•±¿6"—ޱ¿¥Kÿ’T²¿åÓc[œ­¿B—pè-²¿rl=C8²¿cÑtv28²¿ìÕ[[±¿ÛûTˆ±¿‘fر¿ˆ‚S°Æ±¿²¹jž#ò±¿×¥Fèg걿 Qºô/I±¿ ~þ{ðÚ±¿ÀnÝÍS±¿¢ ê[æt±¿Ã(ßÞ±¿V ì1‘Ò°¿Ø�›6ã°¿vãÝ‘±Ú°¿µ©ºG6W±¿UgµÀ±¿~r f°¿J ,€)¯¿ö^|Ñ/°¿NÒü1­±¿óWya³¿I+¾¡ðÙ²¿÷ʼUס²¿[Í:ãûⲿˆº@j³¿àسç25±¿Èïmú³±¿¢³Ì"[±¿Û£7ÜGn±¿gd�»S°¿†r¢]…”¯¿.;Ä?l鱿á´àE_±¿¯]ÚpX²¿JÏôc™²¿ÿ>ãÂ�°¿Ž®ÒÝu6°¿ _B‡°¿�{ó&°¿Gˆ,Ò°¿XÎüj°¿B]¡·°¿ ò³‘린¿ž)t^c—°¿˜Ü(²Ö°¿-î?2:±¿Xæ­ºÕ°¿£•{�Y¡°¿ ýL½n°¿ÏIï_{®¿5–°6ÆN°¿±ÀWtë5±¿DÂ÷þí±¿%’èe˱¿ZEhæÉ±¿õhª'ó�²¿Kä‚3øû­¿#ö  Y²¿–u²¿nˆñšWu²¿6<½R–±¿;m�ÆÁ±¿çp­ö°²¿»¶·[’²¿¬U»&¤5²¿’‘³°§²¿š™™™™™±¿òwï¨1!²¿ØI}YÚ©±¿s‚69|Ò±¿q>?Œ²¿{Cr2±¿DP5z5@±¿ Qºô/±¿ûZ—¡Ÿ±¿œæ=Î4±¿®HLP÷°¿^�c@öz¯¿¯D ú‘°¿Ïƒ»³vÛ±¿ö@+0du³¿XÇñC¥³¿Ñæ8· ÷²¿ Tÿ ’!³¿Ø—qS³¿9Òy±¿tCSvúA±¿w0bŸŠ±¿n¾ݳ®±¿À_Ì–¬Š°¿¡ø1æ®%°¿[(™œÚ²¿³~31]ˆ±¿,Ÿåypw²¿L‰$zŲ¿ö&†ä°¿’zOå´§°¿´ް¿õfÔ|•|°¿¿ÑŽ~7±¿×i¤¥òv°¿2V›ÿW±¿µùÕ‘#±¿¶¡bœ¿ ±¿ùk¸È=±¿bÜ ¢µ¢±¿ÖXÂÚ;±¿*àžçO±¿7ünºe‡°¿²õ á˜e¯¿ÖqüPiİ¿QÁᩱ¿'‡O:‘`²¿8J^�c@²¿´Ë·>¬7²¿—wJ³¿7�íµ ÷®¿Ð³Yõ¹Ú²¿ÓÁú?‡ù²¿Wèƒel貿LkÓØ^ ²¿™Ø|\*²¿2æ®%䃲¿§ÌÍ7¢{²¿-¤ý°²¿¤ng_y�²¿E¡eÝ?²¿Qj/¢í˜²¿¯³!ÿÌ ²¿l"3¸<²¿pêÉ;‡²¿PŽDÁŒ±¿áBÁ�”±¿b0…Ì•±¿ ¦–­õ±¿Å °rh‘±¿Ý—3Ûú°¿H¾�D°¿Žx²›ý°¿Ž°¨ˆÓI²¿Ï1 {½û³¿2çû’�³¿$&¨á[X³¿¥ž¡¼�³¿ Ifõ·³¿%êŸæä±¿÷­Ö‰Ë±¿3SZK²¿”÷q4GV²¿ž?mT§±¿'‰%åîs°¿ušž²¿pÐ^}<ô±¿ñf ÞW岿sôø½M³¿ŸrL÷±¿�íµ ÷ư¿ßºñîȰ¿u¬Rz¦—°¿¦D½Œb±¿Ç-æç†¦°¿p² Ü�:±¿M¡ó»D±¿áçSÇ*±¿h†¬n±¿„d¸±¿àc°âTk±¿¡,|}­K±¿Û…æ:�´°¿í*¤ü¤Ú¯¿+0du«ç°¿9c˜´É±¿w…>XƆ²¿ô5Ëe£s²¿\ætYLl²¿TææÑ=³¿9~¨4bf¯¿ƒ§Z ³¿aÄ>#³¿7l[”Ù ³¿ñ ÙuoE²¿…>XƆn²¿CŒ×¼ª³²¿ï¬Ýv¡¹²¿&¤à)䲿€ ˆWβ¿d°âTka²¿.� ø1沿&¬�±^²¿±¤Ü}Ž�²¿È ~b¼²¿{ø2Q„Ô±¿fh<ı¿øTN{Jα¿&¨á[X7²¿1[²*±¿dWZFê=±¿ï Ñ!p°¿²G¨RE±¿þµ¼r½m²¿ØaLú{)´¿ˆ„ïý Ú³¿‚�ëßõ™³¿Êmûõ׳¿{-è½1´¿h^»ï²¿ò@d‘&Þ±¿¯³!ÿÌ ²¿p–’å$”²¿EhæÉ5±¿ˆ-=šêɰ¿ m9—ⲿQ/ø4'/²¿vŠUƒ0³¿7§’ Š³¿só�èžu±¿‹ÆÚßÙ±¿Èïmú³±¿{Oå´§ä°¿Y¾.ú±¿‚ÿ‚±¿À燣±¿wÐ}9³±¿é Œ¼¬‰±¿“ `ÊÀ±¿“å$”¾²¿Ü�:åѱ¿DÝ µ±¿} ^±¿5–°6ÆN°¿zŠ"nN±¿ººc±M*²¿§¥h岿Êf/Û²¿Âö“1>̲¿êé#ð‡³¿9DÜœJ°¿4¡l\³¿HPüs³¿|îû¯s³¿»|ëÃz£²¿ù0{ÙvÚ²¿Ó hÀ"³¿¢&ú|”³¿^fØ(ë7³¿�Üšt["³¿‹áꈻ²¿"rl=³¿ßlscz²¿Ô˜sIÕ²¿iQŸä³¿×ÜÑÿr-²¿Ä_“5ê!²¿š_Í‚9²¿1·{¹O޲¿@h=|™(²¿Pmp"úµ±¿âx>êͰ¿st´ª±¿‚oš>;ಿ Q¾ …´¿ÇAœ‡´¿Q¡º¹ø³¿¦ ÐÒ´¿%Ïõ}8H´¿÷?ÀZµk²¿~įXÃE²¿‡l ]l²¿/kb�¯è²¿÷uàœ¥±¿­Û ö[;±¿¾ …Œ.³¿߉Y/†²¿~8gDi³¿K[\ã3Ù³¿°ÿ:7mƱ¿Õ\n0Ôa±¿½þ$>w‚±¿ßýñ^µ2±¿.ÆÀ:޲¿Z/†r¢]±¿Ÿä›È̱¿¬r¡ò¯å±¿Ýa™¹À±¿íØÄëú±¿êŸæäE²¿¾4»î±¿R›8¹ß±¿'†ädâV±¿«>W[±¿°¿.9(a¦±¿Sé'œÝZ²¿•ð„^³¿5¶×‚Þ³¿Œ£rµ4³¿dèØA%®³¿]Œ�u?°¿Nµf¡�³¿~TÃ~O¬³¿pïô¥·³¿ö&†ädⲿºÙ(³¿s�FZ*o³¿K?ªa³¿1 Xr‹³¿Q»_øn³¿,¹ŠÅo ³¿»¶·[’³¿÷±‚߆³¿MŸp]1³¿2Çž=³¿xã§qo²¿ÎZ_²¿KËH½§r²¿]¤P¾¾²¿B&9 {²¿¿ïß¼8ñ±¿6;R}籿ȔAÕ豿ƒú–9]³¿J}YÚ©¹´¿/÷ÉQ€(´¿«x#óÈ´¿.È–åë2´¿çýœ0a´¿b…[>’’²¿»�}åAz²¿¿¹¿zÜ·²¿9ïÿã„ ³¿�¤¤‡¡Õ±¿NA~6r±¿:¯±KT³¿¡GŒž[貿�oïô¥³¿½ÅÃ{´¿eqÿ‘éб¿h”.ýKR±¿–\Åâ7…±¿·ÔA^&±¿®Ø_vO²¿™�žwc±¿oõœô¾ñ±¿Ñ=ë-²¿Æ2ýñÖ±¿É�c벿ػ?Þ«V²¿1 ò²¿Çkñ)²¿Q½5°U‚±¿ÛÝt_ΰ¿Œ³—m§±¿Kw×Ù�²¿¤4›Ça0³¿„-vû¬2³¿¥½Á&³¿Í#0ðܳ¿=dʇ j°¿{ö\¦&Á³¿¼°5[yɳ¿«• ¿Ôϳ¿ä0˜¿B沿K‘|%�³¿Äëú»a³¿\�Âõ(\³¿-{Øœƒ³¿�äòÒo³¿rÞÿÇ ³¿ìlÈ?3ˆ³¿Tâ:Ƴ¿÷±‚߆³¿­‰¾¢[³¿TpxADj²¿ÞªëPMI²¿�ßÛôg?²¿3Žç3 ²¿•*Qö–r²¿‡†Å¨kí±¿->Àx±¿�–=Ô¶±¿S³Z�!³¿ÔÔ²µ¾´¿Q1Îß„B´¿;þ 2´¿/ûu§;O´¿Q¼ÊÚ¦x´¿$ –\Ų¿pìÙs™²¿ðß¼8ñÕ²¿ÅçN°ÿ:³¿ósCSvú±¿¼#cµù±¿bÚ7÷W�³¿å·èd©õ²¿`"Ä•³³¿œ¿ …8´¿[{C²¿ÌîÉÃB­±¿q« ºö±¿¾1Çž±¿6wô¿\‹²¿Óhr1Ö±¿0,¾-X²¿j÷«ßm²¿ÚÅ4Ó½N²¿î!á{ƒ²¿¼¯Ê…Ê¿²¿¬8ÕZ˜…²¿­—㈲¿ÙëÝﱿá³up°7±¿™,î?2²¿Ééëùšå²¿RF\¥³¿a2U0*©³¿ºöôÂ�³¿Bx´qÄZ´¿‰`\:æ°¿ÝÍSr3´¿tîv½4E´¿]Œ�u?´¿æuÄ!H³¿HnMº-‘³¿}XoÔ Ó³¿ñ[z4Õ³¿˜÷8Ó„í³¿rPÂLÛ³¿?qý¾³¿Ï1 {½û³¿Ð%z‹‡³¿Ú©¹Ü`¨³¿ó>ŽæÈʳ¿ZòxZ~ಿ�¥]Pß²¿ÕËï4™ñ²¿s×òA³¿þðó߃ײ¿n0Ôa…[²¿¢·xxÏ�±¿Ô›QóU²¿Ž<Y¤‰³¿G 6µ¿'À°üù¶´¿W\•›´¿Ÿã£Åô¿ŸçOÕé´¿,D‡À‘@³¿ß£þz…³¿+½6+1³¿?T1³Ï³¿Ì¸©�æs²¿.çR\Uö±¿Ð¸p $ ´¿Äëú»a³¿š]÷V$&´¿ŒºÖÞ§ª´¿d¬6ÿ¯:²¿ÎÅßö‰±¿¤¨3÷�𱿠]Þ®±¿„¹ÝË}²¿~(F–̱¿0,¾-X²¿½§rÚSr²¿'/2¿F²¿Ö�¸ÇÒ‡²¿€µjׄ´²¿ñ+Öp‘{²¿œÅ‹…!r²¿DÂ÷þí±¿RµÝß4±¿©lXSY²¿Éå?¤ß²¿¿¶~úÏš³¿»ÑÇ|@ ³¿ EºŸS�³¿î@�òèF´¿e#Ù#Ô°¿Z*oG8-´¿Pª}:3´¿?ãÂ��,´¿2°Žã‡J³¿M.ÆÀ:޳¿&o€™ï೿�„ÒBγ¿Ab»{€î³¿R)v4õ³¿V„aÀ’³¿ï�b€D´¿¹ÇÒ‡.¨³¿ëpt•¿vŠUƒ0·³¿éH.ÿ!ý²¿`‘_?IJ¿ú =bôܲ¿'Ø�›6³¿hÐÐ?ÁŲ¿ƒ0·{¹O²¿ZôPÛ†±¿¸;k·]h²¿.VÔ`†³¿!;oc³#µ¿ãûâR•¶´¿Ò4(š°´¿æ±fd�»´¿©ÐDØð´¿Ç�ÒÁú?³¿Nïâý¸ý²¿Š‘%s,³¿ƒlY¾.ó¿ â<œÀt²¿78ýÚú±¿•™Òú[´¿qçÂH/j³¿9œùÕ ´¿Î’Z(™´¿7+1ÏJ²¿9ïÿㄱ¿xÒÂe²¿^.â;1뱿0c Ö8›²¿P«”žé±¿ønóÆIa²¿°ŒØ'€²¿·Œõ L²¿åÐ"Ûù~²¿*A*Ų¿à¼8ñÕŽ²¿áëk]j„²¿ƒM�GÅÿ±¿ª}:3P±¿[[x^*6²¿å·èd©õ²¿:�õÔꫳ¿ØŸÄçN°³¿iTàd¸³¿CSvúA]´¿ö&†ä°¿ ܺ›§:´¿2kœMG´¿ 4ØÔyT´¿Sê’qŒd³¿¸ [–¯³¿‡Ü 7à󳿣#¹ü‡ô³¿B•š=Ð ´¿vü´¿øúZ—¡³¿ï<ñœ- ´¿_(`;±³¿àœ¥½Á³¿sØ}ÇðØ³¿ót®(%³¿¤¥òv„Ó²¿R( __벿d­¡Ô^D³¿7á^™·ê²¿¸çùÓFu²¿ Q…?Û±¿Ãgëà`o²¿hÍ�¿´¨³¿‡$š@µ¿ãOT6¬©´¿zlË€³”´¿_)ËǺ´¿ÆÝ Z+Ú´¿§–­õEB³¿Ü M³¿ë5=((E³¿d”g^»³¿Kw×Ù�²¿§é³®+²¿¯±KTo ´¿®¼äòw³¿ŸÉþy0´¿A,›9$µ´¿à…­ÙÊK²¿5Cª(^e±¿®,ÑYf²¿$š@‹²¿Wj1x˜²¿DÂ÷þí±¿‘~û:p²¿¾ÚQœ£Ž²¿­4)Ý^²¿Üóüi£²¿3¤ŠâUÖ²¿Ô x'Ÿ²¿øùïÁk—²¿ÛikD0²¿&«"ÜdT±¿p[[x^*²¿zýI|ÍÓÚ4¶³¿-Ó¾¹³¿Tüߪ³¿ô߃×.m´¿ýJçó±¿gd�»S´¿;‡ú]´¿ –ê^f´¿RÑèb³¿c ¹§«³¿›­¼äò³¿÷³B‘øù³¿7ÿ¯:r¤³¿Ã.Šø´¿ïU+~©³¿žwgí¶³¿i¬ý�íѳ¿ ®¹£ÿ岿þðó߃ײ¿Å5| 벿$G:#/³¿@ÚÿkÕ²¿6‘™ \²¿THÞ9”±¿ˆœ¾ž¯Y²¿Š¯v稳¿;Sè¼Æ.µ¿¨4bfŸÇ´¿ JÑʽ´¿Ñ]gEÔ´¿5s»—û´¿( __ëR³¿÷±‚߆³¿’ê;¿(A³¿ö'ñ¹쳿²ºÕs²¿7Û$²¿÷_˜L´¿‘šv1Ít³¿$œ¼è+´¿Q0c Ö´¿9aÂhV²¿MIÖáè*±¿HS=™ô±¿%uš²¿ôáY‚Œ€²¿›ÿW9Ò±¿ûÌYŸrL²¿„»³vÛ…²¿[{C²¿)éahur²¿É9±‡ö±²¿jOÉ9±‡²¿TÈ•z„²¿*œÞÅû±¿m¨çoB±¿q>?Œ²¿¥,CëⲿìW\•³¿d´¿íñB:<´¿`tys¸V³¿qr¿CQ ³¿½ãÉ峿3Q„Ôí쳿ø‰è÷ý³¿Q¡º¹ø³¿>u¬Rz¦³¿š]÷V$&´¿•¶¸Æg²³¿+j0 ó¿LÞ3ßÁ³¿ó =E³¿�”ص½Ý²¿-è½1³¿O­¾º*P³¿;nøÝt˲¿r‹ßV²¿A)Z¹˜±¿»�}åAz²¿¬9@0G�³¿/ø4'/2µ¿,€)´´¿å*¿)¬´¿äÖ¤Û¹´¿ö|ÍrÙè´¿LpêÉ;³¿F^Öij¿]߇ƒ„(³¿P¨§�À³¿Á8¸tÌy²¿ ±ú# ²¿ÆàaÚ7÷³¿Õ²µ¾Hh³¿BA)Z¹´¿¼z�´¿�‡¾»•%²¿- &þ(ê°¿ì¥)œÞ±¿ªGÜÖ²¿e‹¤Ýèc²¿2Ïg@½±¿à-� ø1²¿§t°þÏa²¿úíëÀ9#²¿÷Žb²¿’Z(™œ²¿ƒÜE˜¢\²¿÷Žb²¿�“màÔ±¿¼@I�0±¿Ñ‘\þCú±¿›T4Öþβ¿»òYžw³¿tÐ%z‹³¿~âú}³¿!>°ã¿@´¿�Ù®Ð˰¿iTàd´¿�êD2´¿·e¥I)´¿YúÐõ-³¿1`ÉU,~³¿ жšuƳ¿'c`dz¿­K�ÐÏÔ³¿´:9Cqdz¿b‚¾…u³¿ Ôbð0í³¿YÝê9é}³¿t|´8c˜³¿~TÃ~O¬³¿~Wÿ[ɲ¿ÌC¦|ª²¿Ó¾¹¿²¿×Ù�f³¿—�z6«²¿"¦D²¿§uÔ~k³¿~©Ÿ7µ¿èˆ|—R—´¿%�•C‹´¿hÎú”c²´¿0½ý¹hÈ´¿S³Z�!³¿py¬䲿�Q²¿“å$”¾²¿/ùŸüÝ;²¿„¹ÝË}²¿‡nùHJ²¿õ¸oµN\²¿zqÈÒ±¿LnYk(±¿|+Ôð±¿1–é—ˆ·²¿Ã9}=_³¿CV·zNz³¿ö@+0du³¿1Ò‹Úý*´¿³´Ss¹Á°¿G¬Å§´¿¸ õô´¿ŸÉþy´¿à*O 쳿1´:9Cq³¿‚åȳ³¿Z¼X"§³¿�N]ù,ϳ¿?T1³³¿˜5±ÀWt³¿¾jeÂ/õ³¿Ÿ«­Ø_v³¿»ÑÇ|@ ³¿o¼;2V›³¿‘|%�»²¿…BB•²¿þe÷äa¡²¿×¦±½ô²¿6wô¿\‹²¿E¡eÝ?²¿ö#EdX±¿Z€¶Õ¬3²¿†ç¥bc^³¿ê!ÝAì´¿ñ�ÿ‚´¿d²¸ÿÈt´¿‘Gp#e‹´¿J´´¿u=Ñu᳿î#·&ݲ¿§wñ~ܲ¿µŠþÐÌ“³¿òÏ â;²¿}ZEh汿gÑ;pϳ¿úšå²Ñ9³¿ïY×h9г¿ô‹ôz´¿]S ³³è±¿=)“Ú°¿¯Ì[uª±¿IŸVѲ¿ŸF�6²¿ ïr߉±¿×QÕQ÷±¿„, &þ(²¿˜¡ñD籿î>ÇG‹3²¿"H›V²¿#k ¥ö"²¿ÊRëýF;²¿¤q¨ß…­±¿t È^ïþ°¿k :!tб¿aª™µ�²¿ŽYö$°9³¿ ßû´W³¿Ÿ ±Ý=@³¿¢ÏGq´¿šD½àÓœ°¿^, ‘Ó׳¿Ï…‘^Ô ¯$y®ï³¿rk²¿pê”G7²¿W"PýƒH²¿ôÃáÑÆ±¿ -ëþ±±¿¿CQ O䱿}x– # ²¿ƒ…“4L³¿Ì †:¬p³¿:“6U³¿UÙY´¿'À°üù¶°¿Ï1 {½û³¿¿ñµg–´¿—t”ƒÙ´¿~âú}ÿ²¿-wf‚á\³¿ÓL÷:©³¿&8õ�ä�³¿©‡ht±³¿E¼uþí²³¿÷”œ{h³¿.å|±÷⳿aûÉf³¿*q㊋³¿ôÞ€³¿¥MÕ=²¹²¿ŸVÑšy²¿}$%= ­²¿J ,€)³¿‰'»™Ñ�²¿ƒ¥º€—²¿£>+N±¿IeŠ9:²¿–{�Y¡H³¿šÏ¹ÛõÒ´¿y²›ýh´¿ÚÇ ~b´¿JFΞv´¿Ð~¤ˆ «´¿h¯>úRI�€&²¿�ù€@gÒ²¿odùƒ�³¿¯³!ÿÌ ²¿Ü�:åѱ¿²„µ1v³¿A‹v³¿¿º*P‹Á³¿œ¿ …8´¿˜¥�šË ²¿n¦B</¯¿fh<ı¿œQ}>²¿hé ¶O²¿(DÀ!T©±¿Ñ‘\þCú±¿ZÔ'¹Ã&²¿cšé^'õ±¿>^H‡‡0²¿\:æ±N•ﱿֵÂô±¿^‘š²¿�ñ}q©J³¿ó¯å•ëm³¿$&¨á[X³¿N ^ô´¿ À%W±°¿4ØÔyTü³¿Û$¶»´¿"‹4ñ´¿ˆº@j³¿ü¤6qr³¿u["œÁ³¿‡¥�Õ°³¿QJVÕ˳¿Zº‚mij¿W'g(îx³¿S°ÆÙt´¿m®šçˆ|³¿“üˆ_±†³¿±ù¸6TŒ³¿ÖµÂô½²¿]Tœˆ²¿ Hû`­²¿í-å|±÷²¿ºõš”²¿ÈDJ³y²¿ —8ò@d±¿+Àw›7N²¿Â‰è×ÖO³¿ô¥·? µ¿Ä˜ô÷Rx´¿ g·–Ép´¿7ünºe‡´¿…DÚÆŸ¨´¿I/j÷«³¿Pj’̲¿èO=ÒಿÛ5x_•³¿ñôJY†8²¿àö‰í;ÃÔ–:ȳ¿a\:æ<³¿Êmûõ׳¿™€_#I´¿…zúü±¿iêwak®¿=˜Ÿ�±¿­QÑ貿dyW=`²¿&Š�º�}±¿t ‡ÞⱿV¼‘y䲿É<òϱ¿®Gáz²¿dXÅ™G²¿dÍÈ w²¿Ì)1 ²¿¨ªÐ@,›±¿…{eު밿U…bÙ̱¿} yçP†²¿„�çÞÃ%³¿ IJ™CR³¿+½6+1³¿ØÖOÿYó³¿À_Ì–¬Š°¿DÀ!T©Ù³¿/À>:u峿R}ç%賿8h¯>ú²¿ËÚ¦x\T³¿Ó3½ÄX¦³¿B]¡³¿u¯“ú²´³¿Ôð-¬³¿fgÑ;p³¿Ü»}éí³¿Óƒ‚R´r³¿µŠþÐÌ“³¿ 퀵j³¿Å‰&PIJ¿Â¿3‰²¿og_y�ž²¿Qù×òÊõ²¿#N'Ùêr²¿g×½‰ ²¿2ZGUD±¿5 Šæ,²¿oÕu¨¦$³¿TýJçó´¿"ʼn&P´¿êu‘BY´¿aP¦Ñäb´¿ç4 ´;¤´¿øU¹Pùײ¿¿ 1^󪲿*§=%çIJ¿t$—ÿ�~³¿ôR±1¯#²¿|ÓôÙ×±¿õHƒÛÚ³¿ƒú–9]³¿ жšuƳ¿@¢CàH´¿‹¦³“Á±¿Ü�:åÑ­¿¥½Á&S±¿7R¶HÚ±¿�?üü÷౿̳’V|C±¿åòw僧¿+Ù±ˆ×±¿î$"ü‹ ±¿©h¬ý�í±¿ßú°Þ¨²¿)ϼvß±¿V¸å#)鱿÷>U…b±¿mâä~‡¢°¿‰ÒÞà “±¿~įXÃE²¿7á^™·ê²¿Ð#„G³¿Æßö‰í²¿J(}!ä¼³¿œ'¾ÚQ°¿b†ÆAœ³¿m�ÆÁ¥³¿Vס𒬳¿È²`â�¢²¿ ²Hﳿ IJ™CR³¿n…°KX³¿iÅ7>[³¿æ!S>U³¿ŒKUÚ⳿hur†â޳¿D¢Ð²î³¿kœMG7³¿!‘¶ñ'*³¿«ö˜Hi²¿°æÁ=²¿f3‡¤J²¿?tA}Ëœ²¿c%æYI+²¿¥øø„ì¼±¿åa¡Ö4ï°¿t ‡Þâ᱿=|™(B겿W[±¿ìž´¿´qÄZ| ´¿Ð`SçQñ³¿k™ Çó´¿kIG9˜M´¿�]Mž²š²¿sœÛ„{e²¿àªÔì�²¿v?T1³¿;%¯Î±¿ÏL0œk˜±¿Ìa÷Ãc³¿Va3ÀÙ²¿_EF$a³¿W•}Wÿ³¿Ïôc™~±¿²h:;­¿>"¦D±¿L©KÆ1’±¿þEИ±¿v½S÷°¿ãm¥×fc±¿-ÑYfб¿R û=±N±¿°t>>!³¿é™^b,Ó³¿ŽW zR&±¿ãOT6¬©¬¿î{Ô_¯°°¿çsîv½4±¿ ¸Ê;±¿�Õ{L¤°¿‚ÿ‚±¿2*A*±¿ôMšEó°¿¿ÑŽ~7±¿mXSYv±¿þÕã¾Õ:±¿¶€Ðzø2±¿�- ´¾°¿\Uö]ü¯¿®×gÎú°¿ "RÓ.¦±¿oض(³A²¿æ$”¾r²¿¤7ÜGnM²¿l˜¡ñD³¿÷”œ{h¯¿µ;າ¿ïOZ¸¬²¿Âö“1>̲¿ ȳ˷²¿šBç5v‰²¿¯°à~À³¿—Œc${„²¿(™œÚ¦²¿ ƒ2�²¿Í!©…’ɱ¿Òª–t”ƒ±¿~RíÓñ˜±¿Q¤û9ù±¿ÆOãÞü†±¿31]ˆÕ±¿*Æù›P°¿¶,_—á?±¿¯ ?8Ÿ:²¿!¯“â㳿 퀵j³¿Ü¡a1êZ³¿�„}³¿ÿA$CŽ­³¿…zúü±¿ðÞQcḆ¿Ëž6ç౿ œO«”²¿VF#ŸW<±¿±1¯#Ù°¿zrM�Ìβ¿&üR?o*²¿%“S;ÃÔ²¿XË�™`8³¿æ±fd�»°¿>´�ü6¬¿›äGüŠ5°¿Q}>ʰ¿ßºñîȰ¿¼ëlÈ?3°¿g›Ó–°¿Gsdå—Á°¿ro~ÃDƒ°¿––‘zOå°¿†®D ú±¿B_zûsѰ¿l’ñ+Ö°¿Õ¯t>&RšÍã°¿ 3¦`±¿©0¶ä°¿$™Õ;ܱ¿+Qö–r¾°¿†8ÖÅm4°¿ÍwðЯ¿*Æ3h诿[µkBZc°¿ƒ£äÕ9°¿\Æú&¯¿'½o|í™­¿aûÉf¯¿q à-�°¿�&†§W²¿L4HÁSȱ¿ú`º±¿Ù¯;ÝyⱿÉ <÷²¿8øÂdª`°¿ c AJ°¿â‘xy:W°¿¾¿A{õñ°¿B{õñÐw¯¿C�ƒfÚ®¿.qä�È"±¿±RAEÕ¯°¿»šzÃ}äÖ°¿Pþî5&°¿±Ý=@÷å°¿=˜Ÿ�±¿~įXÃE®¿+ß3¡¬¿ý¾ó⬿ K< lÊ­¿Ä@×¾€®¿ ¸çùÓF­¿g+/ùŸü­¿—ª´Å5®¿%êŸæä­¿õ¸oµN\®¿×Ù�f¯¿8¢{Ö5Z®¿^‚SH®¿CqÇ›ü­¿ßâá=–«¿?�{ó®¿lË€³”,¯¿;R}ç%°¿@Â0`ÉU°¿¡ø1æ®%°¿w� ùg±¿‹mRÑX«¿"3¸<Ö°¿„%Zò°¿Ùî@�ò°¿–AµÁ‰è¯¿5B?S¯[°¿¯w¼W­°¿u¬Rz¦—°¿Ù'€bdɰ¿Û…æ:�´°¿'0�Öm°¿»µL†ãù°¿ g·–Ép°¿`áC‰–°¿€(˜1k°¿£<órØ}¯¿ÆÂ9}=¯¿3‡¤J&¯¿[ï7ÚqᅩÖQÕQ¯¿ãã²ó6®¿è»[Y¢³¬¿În-“áx®¿êÉ;‡2°¿™Gþ`౿�Ÿâ8ðj±¿`r£ÈZC±¿“¦AÑ<€±¿Y¾.ú±¿0G�ßÛô¯¿aü4îͯ¿¤ª ¢î°¿4òyÅS�°¿1 íœf�®¿B´V´9έ¿›þìGŠÈ°¿{Ý"0Ö7°¿�St$—ÿ°¿ çfh<±¿ŠÇEµˆ(®¿ÑʽÀ¬P¬¿RGÇÕÈ®¬¿¡„™¶e­¿F³²}È[®¿c`Ç­¿�±¾�É­¿.çR\Uö­¿û&7Ь­¿Z€¶Õ¬3®¿¥,Cë⮿dËòu®¿÷XúÐõ­¿š'×È쬿JA·—4F«¿æ‘?xî­¿ÚV³Îø®¿Ú�f°¿… £YÙ>°¿WA tí °¿Y�|^ñ°¿ž™`8×0«¿`�™D½°¿ep”¼:ǰ¿ 34žâ°¿2rö´Ã¯¿�Ÿqá@°¿¸•^›�•°¿ÿ>ãÂ��°¿ÿB�=·°¿¡º¹øÛž°¿ –Í’Z°¿T÷<Ú°¿ š]÷V°¿Ìbbóqm°¿paÝxwd°¿K?ªa¯¿î#·&Ý®¿ù„ì¼�Í®¿*¬ÿs˜¯¿h’XRî>¯¿¢ÕÉŠ;®¿paÝxwd¬¿¯>úîV®¿{1”í*°¿ØÔyTüß±¿ífF?N±¿QÚ|a2±¿8ù-:Yj±¿ÔdÆÛJ¯±¿%”¾rÞ¯¿1?74e§¯¿èº�꯿D†U¼‘y°¿Ô‚}i®¿¸£®­¿´ã†ßM·°¿ ܺ›§:°¿K°8œùÕ°¿¡,|}­K±¿²¹jž#ò­¿¶+ôÁ26¬¿ m6 B¬¿€cÏžËÔ¬¿ÙÎ÷Sã­¿Ñ­×ô  ¬¿`vOj­¿\='½o|­¿Q29µ3L­¿j’Ìê­¿În-“áx®¿Ð+žz¤Á­¿Ù=yX¨­¿ çfh¬¿¶Go¸�ܪ¿™º+»`p­¿îyþ´Q�®¿×÷á !ʯ¿-é(³ °¿œý�rÛ¾¯¿€·@‚âǰ¿Pj’̪¿§Î£âÿް¿mâä~‡¢°¿r„Ѭ°¿â”¹ùFt¯¿ºÝË}r°¿¼"øßJv°¿;‡ú]°¿o¹ú±I~°¿Õ³ ”÷q°¿b k_@/°¿ò ú'¸°¿Ò¨ÀÉ6°¿´Éá“N°¿š †s 3°¿ Ü¶ïQ¯¿a5–°6Æ®¿£“¥Öû�®¿áìÖ2ޝ¿[yÉÿä﮿ÙëÝï­¿‰ê­�­¬¿œ6ã4D®¿3¥õ·௿„EEœN²±¿Ååx¢'±¿\Va3À±¿W!å'Õ>±¿§#€›Å‹±¿‚®}½p¯¿ÿíÕÇC¯¿Ë¹W•}¯¿‹¤Ýèc>°¿}‘Жs)®¿¿˜-Y­¿¥óáY‚Œ°¿µ§!ªð¯¿O‘CÄÍ©°¿» j¿µ±¿cÔµö>U­¿!¯“âã«¿ ñ+Öp‘«¿p–’å$¬¿p² Ü�:­¿K:ÊÁl¬¿ÔFu:�õ¬¿÷W�ûV묿ªc•Ò3½¬¿Ð˜IÔ >­¿ð6oœæ­¿UkaÚ9­¿÷XúЭ¿«í&ø¦é«¿Ïj�=&Rª¿0ôˆÑs ­¿cò˜ù®¿úšå²Ñ9¯¿“¨|š“¯¿¸Y¼X"¯¿Æ‚”°¿E,bØaLª¿Š!9™¸U°¿ŒƒKÇœg°¿Òýœ‚ül°¿Ív…>XÆ®¿%±¤Ü}ޝ¿(�Í9x&°¿,ºõš°¿Ýyâ9[@°¿Ÿp]1#°¿h%­ø†Â¯¿ç©¹n°¿"«[='½¯¿#ÁÆõ¯¿;äf¸Ÿ¯¿::ZÕ’®¿ Ö8›Ž®¿…(_ÐB®¿L5³–Ò®¿�`ãúw}®¿»ðƒó©c­¿¨T‰²·”«¿M,ðÝz­¿$~Å.r¯¿XXp?à�±¿âW¬á"÷°¿§/ú Ò°¿H›V ±¿‹¥H¾H±¿ ²Hﯿ¾÷7h¯®¿4/‡Ýw ¯¿ 6uÿ¯¿&â­óo—­¿¨Ã ·|$­¿£uT5A°¿�¢ÎÜC¯¿Hk :!t°¿š'×Èì°¿ ß÷o^¬¿¬ª—ßi2«¿Ë¼Uסšª¿úcZ›Æöª¿p À?¥J¬¿”i4¹«¿ì.PR`¬¿pΈÒÞà«¿‡¢@ŸÈ«¿¡ÛK£u¬¿iàG5ì÷¬¿óùõC¬¿\…zú¬¿Ã}äÖ¤Ûª¿ønóÆI©¿@j'÷«¿“�HÛø­¿™cyW=`®¿ �\7¥¼®¿èOÕé@®¿§;OW[©¿†Ê¿–W®¯¿rý»>sÖ¯¿¯ÒÝu6䯿ºƒØ™Bç­¿÷™�®¿Ž<Y¤‰¯¿­‰¾¢[¯¿ ’>­¢¯¿Ë¹W•}¯¿I×L¾Ù殿ˆc]ÜF°¿7�íµ ÷®¿�„};‰¯¿¿ðJ’çú®¿Ñ“2©¡­¿Ë¡E¶óý¬¿4-±2ù¬¿Cç5v‰ê­¿ž~P)”­¿ çfh¬¿j.7ê°ª¿*kg{¬¿áëk]j„®¿]þCúíë°¿QhY÷�…°¿‚«<�°S°¿—㈞°¿°Víš�Ö°¿p[[x^*®¿KY†8ÖÅ­¿ú™zÝ"0®¿\Æú&¯¿c¶dU„›¬¿O@aÃÓ«¿ÍÓÚ4¶¯¿Lª¶›à›®¿‡ˆ›Sɰ¿šìŸ§ƒ°¿µŠþÐÌ“«¿§ ?¹nª¿‰]ÛÛ-É©¿Püsת¿xµÜ™ †«¿CV·zNª¿×Ý<Õ!7«¿ä»”ºd«¿p%;6ñª¿ºžèºðƒ«¿e‰Î2‹P¬¿Ò¬lò–«¿TÆÝ Z«¿‚jÛ0 ª¿ñ�˜õb¨¿é'œÝZ&«¿2äØz†p¬¿×Â,´sš­¿Õ"¢˜¼®¿å`6†­¿B{õñÐw¯¿p@KW°�¨¿-è½1¯¿ ²eùº ¯¿¦]Pß2¯¿þ�rÛ¾G­¿kdWZFê­¿ï¬Ýv¡¹®¿¾ÚQœ£Ž®¿¸Î¿]ö뮿¶hÚV³®¿h‘í|?5®¿¹Œ›h>¯¿ÒYùe0®¿/0+é~®¿×ˆ`\:®¿np–’嬿Q1Îß„B¬¿]4d«¿ïÇí—OV¬¿6ÊúÍĬ¿Œ 1“¨¬¿¢}¬à·!®¿” ¿Ð#F§¿‘{ººc±­¿sÖ§“Å­¿±†‹ÜÓÕ­¿amŒ�ð¬¿Xæ­ºÕ¬¿´Yõ¹ÚŠ­¿Wya§X­¿sÖ§“Å­¿N_Ï×,—­¿÷XúЭ¿ø3¼Yƒ÷­¿ÿy0Hú¬¿ÔÑq5²+­¿FÏ-t%­¿RÑXû;Û«¿6\�-«¿H¨REñª¿óuþÓ ¬¿»)嵺«¿kÖß—ª¿À’«Xü¦¨¿2tì ª¿ß,Õ¼¬¿ª¹nÀ¯¿äÜ&Ü+ó®¿€*nÜb~®¿dÎ3ö%¯¿Ò‹Úý*À¯¿æÊ ÚàD¬¿�¦z2ÿè«¿�Ðzø2Q¬¿tCSvúA­¿> Й´©ª¿tµûËî©¿ê—ˆ·Î¿­¿Š“ûŠ­¿6<½R–!®¿Ot ‡®¿ðŠà+Ù©¿Ús™šo¨¿xìg±ɧ¿ðÐïû§¿$î±ô¡ ª¿˜½l;m�¨¿fÁÄE�©¿šž^©¿‚:vP©¿›WuV ì©¿‰ÓI¶ºœª¿…]=ð©¿é¸Ù•–©¿Ê¤†6¨¿°qý»>s¦¿(›r…w©¿Zg|_\ªª¿JÔ >ÍÉ«¿+Ôð-¬¿‹Š8�d««¿0bŸŠ‘­¿Ç/¼’书¿–!Žuq­¿þ�rÛ¾G­¿ß©€{ž?­¿á iTàd«¿eþÑ7i¬¿æ!S>­¿ð…ÉTÁ¨¬¿­/Úr.­¿ÁãÛ»­¿²Jé™^b¬¿~sõ¸o­¿‚Zº‚m¬¿ÆÝ Z+Ú¬¿©¥¹Âj¬¿‘cëÂ1«¿„gB“Ä’ª¿ûxè»[Yª¿Õ²µ¾Hh«¿eà€–®`«¿Ý—3Ûª¿B•š=¨¿èKo.ª¿å—Á‘(¬¿)êÌ=$|¯¿è‚ú–9]®¿ÞÇÑYù­¿$Õw~Q‚®¿h\W̯¿©‡ht±«¿0-ê“Üa«¿Í#0ðÜ«¿ìjò”Õ¬¿À&kÔC4ª¿�c${„š©¿.É»š<­¿LÅÆ¼Ž8¬¿‡ú]Øš­¿•Ò3½ÄX®¿¯–;3Á¨¿¾÷7h§¿Ež$]3ù¦¿@½5_%§¿t—ÄY5©¿ý¾óâħ¿`¬o`r£¨¿BÌ%UÛM¨¿^ÒƒN¨¿ØƒIññ ©¿-å}Í©¿]þCúí먿LüQÔ™{¨¿¢~¶f+§¿qåì�ÑV¥¿†ðùa„¨¿C㉠Îé¿ãUÖ6Å㪿{0)>>!«¿oB@¾„ª¿§z2ÿ蛬¿‘Ó×ó5Ë¥¿É6‘™ ¬¿å—Á‘(¬¿ð¢¯ ÍX¬¿¸çùÓFuª¿TææÑ=«¿Ýšt["¬¿ÅôûþÍ«¿ ·_>Y1¬¿æ?¤ß¾¬¿Pà�|zl«¿©Ø˜×‡¬¿(c|˜½l«¿ÐѪ–t”«¿· £ x|«¿¼ "5íbª¿ÄÎ:¯©¿šž^©¿á—úyS‘ª¿{Crª¿êX¥ôL/©¿:Yj½ßh§¿®ƒƒ½‰!©¿š`8×0C«¿ûÌYŸrL®¿ÃÕw­¿€»ì×�ŸY ¦–­¿é*Ý]gC®¿¥MÕ=²¹ª¿¼ "5íbª¿þœ‚ül䪿 Ifõ·«¿ŠÊ†5•E©¿½Œb¹¥Õ¨¿jMóŽS¬¿O­¾º*P«¿[ìöYe¦¬¿f‚á\à ­¿Ž²~31]¨¿‚�,`·¦¿aª™µ�¦¿ì¦”×J覿jÜ›ß0Ѩ¿Ï¿]öëN§¿O›sðL¨¿Û$¶»¨¿Éq§t°þ§¿3ÃFY¿™¨¿rÄZ| €©¿˜Št?§¨¿Ù蜟â8¨¿;nøÝt˦¿`;±O¥¿]ˆÕa¨¿ðO©eo©¿sHj¡drª¿ýi£:Ȫ¿pê”G7ª¿Ü�µÛ.¬¿lÑ´­f¥¿(îx“ߢ«¿6WÍsD¾«¿å Åoò«¿t 34žª¿©ÞØ*Áª¿fk}‘Ж«¿œnÙ!þa«¿™óŒ}ÉÆ«¿ô¤‹¦«¿ut\�쪿ˆìø/¬¿õ÷RxÐ쪿]›�k«¿ýôŸ5?þª¿|ƒöêã©¿3á—úyS©¿©|š“©¿g×½‰ ª¿$î±ô¡ ª¿íÓñ˜�ʨ¿èÚÐ §¿�íµ ÷ƨ¿ÚX‰yVÒª¿ä¾Õ:q9®¿Ë,B±4­¿IJzZ�¬¿äˆž”I­¿˜M€aùó­¿èKo.ª¿­QÑ調‘í|?5^ª¿¨qo~ÃD«¿ò?ù»wÔ¨¿:ZÕ’Žr¨¿ßmÞ8)Ì«¿Æ‹…!rúª¿jÂö“1>¬¿Ÿ“Þ7¾ö¬¿±à~À¨¿ú™zÝ"0¦¿—ª´Å5¦¿ý½4»¦¿Ãc?‹¥¨¿Ež$]3ù¦¿ÀA{õñЧ¿…—àÔ’§¿H¾�D„§¿1Ò‹Úý*¨¿(~Œ¹k ©¿:ÏØ—l<¨¿ð0í›û«§¿¢©ÛÙW¦¿@¥J”½¥¤¿‹Þ©€{ž§¿åa¡Ö4憎ùº ÿ骿+Àw›7Nª¿„ñÓ¸7¿©¿µmÁ«¿Ø×ºÔý¤¿ˆ¾»•%:«¿÷è ÷‘[«¿(c|˜½l«¿ú`º©¿šêÉü£oª¿„-vû¬2«¿t¶€Ðzøª¿÷‘[“n«¿ÿZ^¹Þ6«¿ÿwD…ª¿‚�ëßõ™«¿Üƒ�/¡ª¿%çÄÚǪ¿¥MÕ=²¹ª¿™fº×I}©¿jg˜ÚR©¿¯–;3Á¨¿™ñ¶Òk³©¿dæ—Çš©¿2kœMG¨¿µ5"—¦¿tîv½4E¨¿G’ \…ª¿û®þ·’­¿W³Îø¾¸¬¿Çò®zÀ<¬¿ Õ°ß묿Ϡ¡‚‹­¿…$³z‡Û©¿ì�±¾�©¿ÇeÜÔ@ó©¿Ã}äÖ¤Ûª¿R·³¯<¨¿ÓL÷:©§¿1`ÉU,~«¿0„œ÷ÿqª¿ß¿yqâ«¿}"O’®™¬¿‰yVÒŠo¨¿36t³?P¦¿^L3Ý뤦¿bhur†â¦¿:“6U÷¨¿¹Œ›h>§¿R·³¯<¨¿dw�’ ¨¿FzQ»_¨¿Õ’Žr0›¨¿`ÊÀ-]©¿Úþ••&¥¨¿Æk^ÕY-¨¿Ã)só�覿—Tm7Á7¥¿ËLiý-¨¿IZÖýc©¿•Ö߀ª¿n¿|²b¸ª¿´Ë·>¬7ª¿ÿ¬U»&¬¿ž´pY…¥¿x úÒÛŸ«¿DÀ!T©Ù«¿¯&OYM׫¿­„î’8+ª¿- PSËÖª¿åÑ�°¨ˆ«¿Ã9}=_«¿!WêYÊ«¿ðQ½Â‚«¿‚oš>;િä1•ñï«¿¶Go¸�ܪ¿ß§ªÐ@,«¿Æ‚”0«¿ú`º©¿N(DÀ!T©¿�?üü÷¨¿Å:U¾g$ª¿¶ö>U…ª¿üd訿ÀË eý¦¿{K9_콨¿Nïâý¸ýª¿´wF[•D®¿gµÀ)­¿ä‚3øûŬ¿_›�•˜g­¿®Gáz®¿A›>éDª¿78ýÚú©¿Â¿3‰ª¿Ž={.S«¿0½ý¹hȨ¿Î5ÌÐx"¨¿òšWuV ¬¿¦cÎ3ö%«¿�^ Pj¬¿ŸÛ2à,­¿ÿÍ‹_í¨¿å|±÷⋦¿£±öw¶G§¿º¿zÜ·Z§¿eŽå]õ€©¿4MØ~2Ƨ¿0½ý¹hȨ¿!!Ê´�¨¿p@KW°�¨¿ø6ýÙ�©¿§Z ³ÐΩ¿©J[\ã3©¿š#+¿ ƨ¿odùƒ�§¿£uT5AÔ¥¿ùõCl¨¿ÃÔ–:Èë©¿Nïâý¸ýª¿Ø¸þ]Ÿ9«¿ÖÇCßÝʪ¿~ý,œ¬¿R+Lßk¦¿é(³ 0¬¿tšÚR¬¿1%’èe¬¿Ð(]ú—¤ª¿lwÐ}9«¿`:­Û ö«¿&¶Øí«¿. ´¾L¬¿OË\å ¬¿ÔÖüøK«¿ g·–Ép¬¿Aœ‡˜N«¿2;‹Þ©€«¿ó:â� ¤«¿"Ã*ÞÈ<ª¿;q9^�è©¿’Ï+žz¤©¿†uãÝ‘±ª¿x �ý,–ª¿6rÝ”òZ©¿Ãï¦[vˆ§¿@¿ïß¼8©¿:]›�«¿2>Ì^¶�®¿ê?k~ü¥­¿Zd;ßO­¿H‰]ÛÛ­¿ž'ž³„®¿ZòxZ~િݵßÚ‰ª¿²fd�»«¿_ÔîW¾«¿€šZ¶Ö©¿¯yUgµ¨¿%®c\qq¬¿©¤N@a«¿:A›>鬿]ݱØ&­¿>”h©¿_ š]÷¦¿aü4îͧ¿ÉæªyŽÈ§¿sÚSrNì©¿žî<ñœ-¨¿³z‡Û¡a©¿©J[\ã3©¿2®¸8*7©¿’Z(™œÚ©¿×gÎú”cª¿zÅrK«©¿ÛÄÉýE©¿é|x– #¨¿nˆñšWu¦¿mUÙ©¿8N ógª¿pënžê�«¿ëâ6À«¿F�6ŽX«¿c|˜½l;­¿ÃFY¿™˜¦¿«>W[±¿¬¿T­…Yh笿¬q6ܬ¿–Ïò<¸;«¿dsÕY1\­¿í�ÑV%‘­¿¦H¾H‰­¿õ  ­Ü«¿ºŸS�Ÿ�¬¿Úå[Ö­¿#¾³^ ­¿˜ßi2ãm­¿^‘šv1­¿hÊN?¨‹¬¿A)Z¹˜­¿Ûúé?k~¬¿©0¶ä ¬¿'ŸÛ2ିúDž$]«¿šž^)«¿$š@«¿ßøÚ3K¬¿sÖ§¬¿EGrù鯿Éå?¤ß®¿Á8¸tÌy®¿¼’ä¹¾¯¿^Mž²š®¯¿©¾ó‹¬¿µŠþÐÌ“«¿êÐéy7¬¿[D“7À¬¿Î7¢{Ö5ª¿ósCSvú©¿J&§v†©­¿a‹Ý>«Ì¬¿Èì,z§®¿'f½ʉ®¿×gÎú”cª¿Ö§“Åý§¿(Ö©ò=#©¿´ç25 Þ¨¿r3܀ϫ¿ ¥+ØF<©¿jûWVš”ª¿ˆ jôj€ª¿ eýfbª¿Õw~Q‚þª¿:vP‰«¿Ù°¦²(쪿ÁäF‘µ†ª¿mXSYv©¿Ö§“Åý§¿ÞË}r ª¿çß.ûu§«¿s-Z€¶Õ¬¿ÛÝt_ά¿þc!:ެ¿=¶eÀYJ®¿ÎR²œ„Ò§¿ £YÙ>ä­¿U¯²¶)®¿7Û$®¿Õ³ ”÷q¬¿'÷;ú¬¿àg\8’­¿+øDk­¿ÔïÂÖlå­¿µŒÔ{*§­¿‰`\:欿¤¨3÷�ð­¿e#Ù#Ô¬¿Ø×ºÔý¬¿Í:ãûâR­¿í~à»Í«¿GÆjóÿª«¿€J•({«¿ý0Bx´q¬¿¦(—Æ/¬¿ÒyY«¿~oÓŸýH©¿%çÄÚǪ¿©öéxÌ@­¿6>“ýó4°¿Um7Á7M¯¿µ­¿®×ô  ­¿“�HÛø­¿]ûzáέ¿ž±/Ùx°­¿Î’Z(™¬¿Ð€z3j¾ª¿÷9>Zœ1¬¿ãQ*á ½®¿!°rh‘í°¿³%«"Üd°¿J–“PúB°¿ª, »(z°¿¢™'×Ȱ¿ o –ꮿ•�Zº‚­¿1 ò®¿Ùy›©®¿ W@ÜÕ«¿ ÐÒl«¿UPQõ+�¯¿^‘š®¿á' ß÷¯¿È%Ž<Y°¿ÄìeÛik¬¿°ÅnŸUfª¿Ò¬lò–«¿EJ³y«¿gµÀ)­¿«ZÒQf«¿ö|ÍrÙ謿3úÑpÊܬ¿Û…æ:�´¬¿Ž¯=³$@­¿ K< lÊ­¿—qS­¿'ŸÛ2ିŒ½_´Ç«¿Ó.¦™îuª¿Ðïû7/N¬¿©;‡ú­¿À#*T7¯¿u殿ÞÈ<ò¯¿²òË`ŒH°¿ö´Ã_“5ª¿þ|[°T°¿¬«µ<°¿ÎQÚ°¿’¯Rb×®¿QLÞ3¯¿¼°5[yɯ¿(š°È¯¯¿œà›¦Ï°¿0G�ßÛô¯¿çmlv¤ú®¿„~¦^·°¿È•z„ò®¿÷”œ{h¯¿`[?ýgͯ¿U1•~ÂÙ­¿)#.�Ò­¿1E¹4~á­¿,*ât’­®¿æWs€`Ž®¿ñ~Ü~ùd­¿’!ÇÖ3„«¿nÁR]Àˬ¿y<-?p•¯¿3á—úyS±¿¸tÌyƾ°¿>v()°°¿Ÿ;Áþëܰ¿Ç›ü�,±¿«˜J?á쮿÷Žb®¿.� ø1殿H¾�D„¯¿Fì@1²¬¿µP29µ3¬¿Ž®ÒÝu6°¿áaÚ7÷W¯¿@¡ž>°¿ir1Öq°¿Žç3 Þ¬¿Öa°äª¿Û4¶×‚Þ«¿Ì †:¬p«¿_ÎlW胭¿»)嵺«¿Í’5µl­¿çþêqßj­¿ù½Mö#­¿X7Þ«­¿Á­»yªC®¿¬�‡¾»•­¿J›ª{ds­¿o.þ¶'H¬¿3Pÿ>㪿#Ûù~j¼¬¿‰Ï�`ÿu®¿UPQõ+�¯¿|—wJ¯¿n1?74e¯¿Z ‰{,}°¿e�P3¤Šª¿…µ1vÂK°¿Ús™šo°¿��@¼®_°¿b£¬ßLL¯¿?T1³¯¿Ä$\È#°¿,ºõš°¿B•š=°¿ÿ¬U»&°¿Qgî!á{¯¿@j'÷;°¿· b k¯¿¹˜Št¯¿çfh<°¿d°âTka®¿AG«ZÒQ®¿îîº/g®¿á ½þ$>¯¿¸®˜Þ®¿G ^×/Ø­¿ÅŒðö ¬¿Ù•–‘zO­¿ÊPS鯿ZôPÛ†±¿æ!S>±¿Ž •bGã°¿HÝξò ±¿>”h±¿'„º„C¯¿¥,Cë⮿КiQ¯¿Œi¦{�Ô¯¿^‘šv1­¿Ô»x?n¿¬¿»›§:äf°¿Ï…‘^Ô¾Ü'G¢°¿<ŸõfÔ°¿­ü2#­¿wÚŒƒ«¿ PO�?¬¿§\á].â«¿‰±L¿D¼­¿=�°S¬¬¿qÈÒŦ­¿4KÔÔ²­¿7QKs+„­¿©lXSY®¿Ô‚}i®¿ £YÙ>ä­¿”Ù “Œœ­¿‹3†9A›¬¿€œ0a4+«¿Þ„€| ­¿$`tys¸®¿ÍXäׯ¿“5µl­¯¿P)”…¯¯¿…DÚÆŸ¨°¿gÓÀͪ¿äŸÄv°¿—㈞°¿RC€ ˆ°¿Ÿ«­Ø_v¯¿*Æ3h诿sµ4·B°¿7n1?7°¿ÖT…]°¿^ÒƒN°¿»}V™)­¯¿$'· b°¿d¯w¼¯¿vmo·$°¿³š®'º.°¿ØžY ¦®¿°Tð2î¿øQ û=±®¿(·í{Ô_¯¿´ ”÷q4¯¿G«ZÒQ®¿${„š!U¬¿àg\8’­¿¹‰Zš[!°¿F•aÜ ¢±¿,»`pͱ¿:tzÞ�±¿œæ=Î4±¿é`ýŸÃ|±¿È[®~l’¯¿R_–vj.¯¿LS8½‹¯¿Èš‘Aî"°¿`vOj­¿ë¬Øc"­¿·Aí·v¢°¿eRC€ °¿ÕBÉäÔΰ¿ŽÌ#0ð°¿‹ÆÚßÙ­¿ëpt•¿p À?¥J¬¿å—Á‘(¬¿°«ÉSVÓ­¿J·%rÁ¬¿+Nµf¡­¿st´ª­¿O²žZ}­¿j’Ìê­¿Ot ‡®¿?ÆÜ­¿(ðN>=¶­¿¼z�¬¿°;Ýyâ9«¿¢$$Ò6þ¬¿c¸:â®®¿´æÇ_ZÔ¯¿Ä"†Ƥ¯¿þ™A|`ǯ¿Óˆ™}£°¿2uWvÁિ‰yVÒŠo°¿'¼§>�°¿×3ÂÛƒ°¿�aNÐ&‡¯¿ÆàaÚ7÷¯¿×Š6ǹM°¿ú›Pˆ€C°¿T1³Ïc°¿¾Û¼qR°¿%<¡×ŸÄ¯¿ÒQf`°¿^€}têʯ¿ßmUÙ¯¿?�Qžy9°¿­k´è¡®¿øQ û=±®¿å|±÷â‹®¿•Óž’sb¯¿JbI¹û¯¿’’†®¿ ßû´¯¿¾ƒŸ8€~¯¿å%ÿ“¿{¯¿Îp>?Œ°¿uF^ÖĪ¿©ù*ùØ]°¿ƒŠª_é|°¿!Âøi°¿�{ž?mT¯¿QJVÕ˯¿)ßÞ5°¿c{-è½1°¿ÏdT°¿lÌëˆC6°¿“¨|š“¯¿^ÒƒN°¿¤oÒ4(š¯¿õôøÃϯ¿,g~5°¿$Õw~Q‚®¿¸“ˆð/‚®¿‘ñ(•ð„®¿s×òA¯¿Šå–VC⮿üÄôûþ­¿£uT5A¬¿ÿç0_^€­¿WA tí °¿­jIG9˜±¿óÊõ¶™ ±¿…{eު밿ë¬Øc"±¿<…\©g±¿Ë2�g¯¿þHVñ®¿÷”œ{h¯¿‡P¥f°¿h@½5_­¿Ç•F̬¿TÝ‹Š°¿T7Û°¿rNì¡}¬°¿²dŽå]õ°¿Ã×׺Ô­¿š±h:;¬¿¨rÚSrN¬¿ª x™a£¬¿ Ö8›Ž®¿Ø¹i3NC¬¿æCV¸­¿¦ ÐÒ­¿Ö�Mò#~­¿�—6Êú­¿X­Lø¥~®¿Ý”òZ Ý­¿å+�”ص­¿3oÕu¨¦¬¿R %“S;«¿>]ݱØ&­¿¿eN—ÅÄ®¿aŽ¿·é¯¿¨ÅàaÚ¯¿zÇ):’˯¿áñí]ƒ¾°¿¿ 1^óª¿ìÁ¤øø„°¿/Þ�Û/Ÿ°¿9^�èI™°¿¶¹1=a‰¯¿¯±KTo °¿`Xþ|[°¿l$ ÂP°¿Þß4}v°¿‚WË�™`°¿*Æ3h诿çU�Õ{°¿•fó8 毿Øñ_ °¿y®ïÃAB°¿µÀ)Í®¿KÔÔ²µ®¿ºùFtϺ®¿fgÑ;p¯¿‘·\ýØ$¯¿”lu9% ®¿˜Þþ\4d¬¿¾‰!9™¸­¿°8œùÕ°¿é Œ¼¬‰±¿]‰@õ"±¿ïå>9 ±¿%xC8±¿øq4GV~±¿¿œ3¢´¯¿½á´àE¯¿èf Ü¶¯¿Ž®ÒÝu6°¿B\9{g´­¿ù½Mö#­¿ãOT6¬©°¿•™Òú[°¿ð5Çeܰ¿2V›ÿW±¿{O崧䬿êÐéy7¬¿ÈбƒJ¬¿ò`‹Ý>«¬¿Ð^}<ôÝ­¿±¿ìž<,¬¿Rðr¥ž­¿(ðN>=¶­¿;ŠsÔÑq­¿²ˆ×õ ®¿j÷«ßm®¿|+Ôð­¿Ç.Q½5°­¿pìÙs™š¬¿X«vMH«¿µÆ B­¿›!U¯²®¿76;R}篿×÷á !ʯ¿�oïô¥¯¿³\6:ç§°¿‰^F±ÜÒª¿‡oaÝxw°¿Jy­„î’°¿E ¦aøˆ°¿C®Ô³ ”¯¿„Ÿ8€~߯¿pµN\ŽW°¿GN¶�;°¿ÖqüPi°¿Ô€AÒ§U°¿R}ç%诿Z ‰{,}°¿Å­‚èÚ¯¿‡3¿š°¿žî<ñœ-°¿ý½4»®¿ Ï.ßú°®¿Ùy›©®¿G�ßÛôg¯¿MóŽSt$¯¿ùf›Ó®¿H¾�D¬¿EºŸS�Ÿ­¿÷ZÐ{c°¿CX�%¬�±¿®×gÎú°¿Âøiܛ߰¿OÌz1”±¿¢_[?ýg±¿=·Ð•T¯¿RÔ™{Hø®¿÷<Ú¨N¯¿øá !ʰ¿¼ËE|'f­¿èy’tͬ¿W|Cá³u°¿áíAȯ¿zÄè¹…®°¿9}=_³°¿ÔÑq5²+­¿ù÷„¬¿œú@òΡ¬¿ÌÔ$xC­¿D¥3û<®¿}Ê1Yܬ¿¹¨Åä­¿3SZK®¿`YiR º­¿ìˆC6�.®¿Q¾¾Ö¥®¿@‰Ï�`ÿ­¿Ø(ë7Ó­¿Oé`ýŸÃ¬¿µÿÖª]«¿Z×h9ÐC­¿ À;ùôØ®¿\Uö]ü¯¿ÁtZ·Aí¯¿AJ˜iû¯¿J}YÚ©¹°¿;r¤30òª¿]¡·x°¿[@h=|™°¿„½‰!9™°¿œ0a4+Û¯¿ÎQÚ°¿±3…Îk°¿ëâ6À[°¿2<ö³Xа¿EÚÆŸ¨l°¿j¾J>v°¿²Õ唀˜°¿ºÝË}r°¿eVïp;4°¿n§­Á8°¿u=Ñu᯿¯\o›©¯¿'ù¿b ¯¿ƒlY¾.￯\o›©¯¿tì ×1®¿uÍä›mn¬¿ž MKÊ­¿:Ë,B±°¿júì€ëб¿–?ß,±¿1#¼=±¿©öéxÌ@±¿¢·xxÏ�±¿ÌDR·³¯¿"—Ž9¯¿“¨|š“¯¿Ë+×Ûf*°¿Ð^}<ôÝ­¿™b‚ŽV­¿ç4 ´;¤°¿©¾ó‹°¿éšÉ7Ûܰ¿vÂKp걿 ¸Ê;­¿Š§wñ~¬¿—6–~¬¿êy7­¿)®*û®®¿xÓ-;Ä?¬¿í�ÑV%‘­¿ß4}vÀ­¿¸’�x­¿ŽÍŽTßù­¿°¿ä£Åܰ¿)ë7Ó…°¿ù¢=^H‡¯¿±à~À°¿Mø¥~ÞT°¿Çò®zÀ<°¿LnYk°¿Ã¹†O°¿•fó8 毿ir1Öq°¿{úüá篿8iͰ¿5cÑtv2°¿�èI™ÔЮ¿£¢ÑÄ®¿»a̮ۢ¿Ÿ«­Ø_v¯¿J ,€)¯¿[(™œÚ®¿ŠÍǵ¡b¬¿¯xê‘·­¿žB®Ô³ °¿ŒgÐÐ?Á±¿?Œm±¿¯Z™ðKý°¿ YÝê9±¿í�ÑV%‘±¿d¯w¼¯¿q:ÉV¯¿>u¬Rz¦¯¿;þ 2°¿%êŸæä­¿ÀnÝÍS­¿+¥gz‰±°¿�í`Ä>°¿'ŸÛ2à°¿¨äœØCû°¿··[’v­¿ÆÝ Z+Ú¬¿(F–̱¬¿­‡/EH­¿òz0)>®¿ÌAÐѪ–¬¿oõœô¾ñ­¿ÒÄ;À“®¿YÂÚ;á­¿=ƒù+d®¿ã©GÜÖ®¿c}“E®¿”XS®¿Xqªµ0 ­¿Ý^Ò­«¿öBÛÁˆ­¿dÎ3ö%¯¿UÙY°¿Ã.Šø°¿vþÓ °¿§/ú Ò°¿ÆnŸUfJ«¿Wya§°¿¡×1®¸°¿RóUò±»°¿8ýÚú鯿b k_@/°¿Ûúé?k~°¿_F±ÜÒj°¿7¨ýÖN”°¿ î<0€°¿0K;5—°¿Ãc?‹¥°¿Bí·v¢$°¿ÊTÁ¨¤N°¿Ñ"Ûù~j°¿y ²H¯¿�©»² ¯¿×Ù�f¯¿“5µl­¯¿ü¤6qr¯¿ˆLùT�®¿ÅªA˜Û½¬¿�:è®¿ì½ø¢=^°¿HO‘CÄͱ¿Ö‹¡œhW±¿˜Û½Ü'G±¿/¨o™Óe±¿‘'I×L¾±¿‘´}̰¿ õôøÃ¯¿"Žuq °¿ùõCl°¿/¥.ÇH®¿4óäš™­¿a7l[”Ù°¿W$&¨á[°¿ŽW zR&±¿äˆž”I±¿ó:â�­¿�ñaö²í¬¿‘Òl‡Á¬¿J›ª{ds­¿c%æYI+®¿&9`W“§¬¿ðiN^d®¿ú™zÝ"0®¿G ^×/Ø­¿rl=C8®¿ºùFtϺ®¿¿Òùð,A®¿tì ×1®¿)Ý^Ò­¿ßÁÿV²«¿™Iô2Š­¿ÿ®Ïœõ)¯¿â|~!°¿BA)Z¹°¿AfgÑ;°¿#ºg]£å°¿sIÕv|«¿J´°¿]¦&ÁÒ°¿aßN"¿°¿@ÛjÖ߯¿Ô($™Õ;°¿?ÆÜµ„°¿M×]~°¿œN²Õå”°¿þc!:ް¿º/g¶+°¿¢A žB®°¿)\�Âõ(°¿›äGüŠ5°¿;5— u°¿ªH…±… ¯¿:3Pÿ®¿·&ݖȯ¿_³\6:篿6 B\9{¯¿’$W@¡®¿iUK:ÊÁ¬¿%ÍÓÚ4®¿·¶ð¼Tl°¿¤¨3÷�ð±¿+*ÿZ^±¿`r£ÈZC±¿€)´t±¿£7ünº±¿:w»^š"°¿—Ž9ÏØ¯¿K’çú>°¿QhY÷�…°¿‹RB°ª^®¿Ïø¾¸T¥­¿órØ}Çð°¿Š!9™¸U°¿Q.�_x%±¿/¨o™Óe±¿%êŸæä­¿¥½Á&S­¿—Ép<Ÿ­¿÷ÍýÕã¾­¿\ŽW zR®¿Ãº�﬿ oÖà}U®¿Kw×Ù�®¿%ÍÓÚ4®¿ó=#Á®¿EöA–¯¿ œO«”®¿¾ÚQœ£Ž®¿J›ª{ds­¿I„F°qý«¿Ïƒ»³vÛ­¿©PÝ\üm¯¿£’:M°¿)´¬ûÇB°¿^gCþ™A°¿¤ÿåZ´±¿ã¦šÏ¹«¿ý¾óâ°¿77¦',ñ°¿éòæp­ö°¿s ßû°¿~r f°¿¾ˆ¶cê®°¿ƒi>"¦°¿× /½ý¹°¿î{Ô_¯°°¿‚WË�™`°¿nmáy©Ø°¿» ”X°¿É°Š72�°¿½f¾ƒŸ°¿CV·zNz¯¿å%ÿ“¿{¯¿ïÊ.\s¯¿Lm©ƒ¼°¿h%­ø†Â¯¿ƒÃ "RÓ®¿$™Õ;Ü­¿Ôµö>U…®¿„ûPŒ°¿|+Ôð±¿{�„±¿´UIdd±¿áBÁ�”±¿ýläº)屿Î9x&4I°¿>\rÜ)°¿–Ð]gE°¿Ít¯“ú²°¿ý½4»®¿1 ò®¿Ïg@½±¿õc™~‰°¿ ¦šYK±¿“¦AÑ<€±¿%ÍÓÚ4®¿W\•›¨­¿Ò¦êÙ\­¿cò˜ù®¿Lª¶›à›®¿©öéxÌ@­¿'f½ʉ®¿:;%¯®¿‰Ï�`ÿu®¿–è,³Å®¿]7¥¼VB¯¿ësµûË®¿ÐÔë�±®¿Ç.Q½5°­¿UÂzýI¬¿Ä "RÓ.®¿­lò–«¯¿²Jé™^b°¿Ã¤Rìh°¿Tn¢–æV°¿'…y�3±¿ä1•ñï«¿5´Ø€±¿ïå>9 ±¿G 6u±¿÷åÌv…>°¿ð¦[vˆ°¿6ÇeÜÔ°¿6ÊúÍİ¿ýkyåzÛ°¿ò?ù»wÔ°¿M×]~°¿XÅ™Gþ°¿À³=zÃ}°¿ Q¾ …°¿+Qö–r¾°¿Þæ�“¼¯¿9¹ß¡(Я¿É>Ȳ`⯿Ÿu�–=°¿ â;þ °¿³í´5"¯¿À\‹ m­¿ †7kð®¿ý»>sÖ§°¿ŠsÔÑq5²¿óäš™�±¿(eRC€±¿^*6æuı¿l—6–²¿�9ÏØ—l°¿ PO�?°¿Ù˜×‡l°¿—ÅÄæãÚ°¿R_–vj.¯¿ÊÝçøhq®¿N|µ£8G±¿™Hi6�ð¿#M¼[¯¿ÚV³Îø®¿ÚV³Îø®¿LŒeú%â­¿N+…@.q¬¿¯>úîV®¿ý¾óâį¿Ûúé?k~°¿@¡ž>°¿~£<ór°¿éÕ¥¡F±¿'ÙêrJ@¬¿ôÂ� #±¿{2ÿè›4±¿V¸å#)±¿²žZ}uU°¿ú*ùØ] °¿œÝZ&Ãñ°¿�EÓÙÉà°¿æèñ{›þ°¿›™�Eï°¿*Õ"¢°¿*Œ-9(±¿†s 34ž°¿mæ�ÔBɰ¿ÛÝt_ΰ¿_³\6:篿çãÚP1ί¿šÒú[ð¯¿ô3õºE`°¿üÆ×žY°¿wÖn»Ð\¯¿ Q…?Û­¿OÎPÜñ&¯¿`�™D½°¿7Û$²¿9 {Úᯱ¿JzZ�œ±¿Í!©…’ɱ¿îÎÚm²¿&5´Ø€°¿ 4ØÔyT°¿àºbFx{°¿Y�|^ñ°¿o À±g¯¿Ô˜sIÕ®¿&«"ÜdT±¿�A'„º°¿@öz÷Ç{±¿¦',ñ€²±¿Ot ‡®¿ ~þ{ðÚ­¿ß4}vÀ­¿<3Áp®a®¿W˜¾×¯¿ ]Þ®­¿yvùÖ‡õ®¿¯²¶)¯¿?ÿ=xíÒ®¿uÈÍp>¯¿‡¥�Õ°¯¿¾ø¢=^H¯¿{Ü·Z'.¯¿ÞïU+®¿q¹5鶬¿âKº ®¿™Ö¦±½°¿„aÀ’«°¿È¨p©°¿Î’Z(™°¿NÔÒÜ a±¿V(Òýœ‚¬¿’”ô0´:±¿ÜK£uT±¿=a‰”M±¿o¹ú±I~°¿•AÕèÕ°¿¥2ű¿GV~Œ±¿ãâ¨ÜD-±¿o.2±¿üä(@̰¿ñKý¼©H±¿¦~ÞT¤Â°¿½Œb¹¥Õ°¿æèñ{›þ°¿ ©ÛÙW°¿€™ïà'°¿vÅŒðö °¿„elèf°¿Ýyâ9[@°¿HÜÖž¯¿2 {½ûã­¿�äòÒo¯¿ÿunڌӰ¿ ˜£Çïm²¿%’èe˱¿¢–æV«±¿Ç ¿›nÙ±¿Á-]Á6²¿Óˆ™}£°¿@¡ž>°¿Ë�e¡°¿Kçó±¿Ñ°u­½¯¿ÀË eý®¿ ¦}s±¿ ߺñî°¿4Ÿs·ë¥±¿ï�ÚÄɱ¿êè¸Ù•®¿°«ÉSVÓ­¿ÔdÆÛJ¯­¿@̘‚5®¿Î4aûɯ¿f 2þ}Æ­¿žbÕ Ìí®¿„�çÞÃ%¯¿ÄÑUº»Î®¿B#ظþ]¯¿LÞ3ßÁ¯¿åš™�E¯¿]7¥¼VB¯¿V¼‘y䮿¯D ú‘¬¿�Æ¡~¶®¿÷_˜L°¿²,˜ø£°¿4»î­°¿dT8‚°¿�Cý.l±¿Ð w.Œ¬¿ñKý¼©H±¿™b‚ŽV±¿Ì_!seP±¿2�g—o}°¿‰ÿ"h̰¿ø6ýÙ�±¿rÝ”òZ ±¿5 S"±¿7e±¿Ûe6Ȱ¿*�ÙYôN±¿#Ûù~j¼°¿Žlê°¿Ac&Q/ø°¿ãüM(°¿”¼Ǚ&°¿ßmÞ8)°¿À³=zÃ}°¿Ì^¶�¶F°¿�oïô¥¯¿¾4»î­¿°rh‘í|¯¿sÙ蜟ⰿSé'œÝZ²¿xî=\rܱ¿÷!o¹ú±±¿�ëŠáí±¿3ÞVzm6²¿mâä~‡¢°¿^ò?ù»w°¿r¥ž¡°¿Ä^(`;±¿-Ó¾¹¯¿h¯>ú”¢•{�±¿->Àx±¿’Ï+žz¤±¿èÀr„ 䱿I/j÷«¯¿kïSUh ®¿ø‹Ù’U®¿"ÇÖ3„c®¿.®ñ™ìŸ¯¿¸°n¼;2®¿4¡l\¯¿ê‘·µ…¯¿`ÈêVÏI¯¿õœô¾ñµ¯¿Hˆò-$°¿u["œÁ¯¿ÞŽpZ𢯿š–X�|®¿«–t”ƒÙ¬¿_• •-¯¿kIG9˜M°¿Óg\W̰¿©gA(ïã°¿‘~û:pΰ¿+Nµf¡±¿¾³^ 嬿/Ý$�±¿x $(~Œ±¿£>É6‘±¿5%Y‡£«°¿ÝCÂ÷þ±¿äg#×M±¿^‘šv1±¿8ù-:Yj±¿vùÖ‡õF±¿5´Ø€±¿îÌù†±¿Ù²|]†ÿ°¿â¯Éõ±¿Ñ@,›9$±¿™¹Àå±f°¿ÒQf`°¿“ÆhUM°¿‚ŽVµ¤£°¿Ç):’˰¿vmo·$°¿4iSu�l®¿‘Õ­ž“Þ¯¿: ûv±¿¤ö{b�²¿Ävü²¿Qøl챿ÆGå&²¿ s‚69|²¿Ï‚PÞÇѰ¿• •-¯°¿ÆÝ Z+Ú°¿~oÓŸýH±¿ÿÐÌ“k °¿fgÑ;p¯¿îаu­±¿¿ž¯Y.±¿‰ jøÖ±¿z‹üú!²¿ümO�Øî®¿=HO‘CÄ­¿Qølì­¿g+/ùŸü­¿£<órØ}¯¿dÍÈ w®¿S’u8ºJ¯¿Ìa÷Ãc¯¿i:;%¯¿‚9züÞ¦¯¿Èš‘Aî"°¿u["œÁ¯¿ÏJZñ …¯¿¯–;3Áp®¿4¢´7øÂ¬¿ÁV ‡3¯¿î@�òèF°¿#‡ˆ›Sɰ¿«–t”ƒÙ°¿"T©Ù­°¿âÊÙ;£±¿'ŸÛ2ି~sõ¸o±¿^-wf‚±¿uU ƒ‡±¿r„Ѭ°¿l–ËFçü°¿q9^�èI±¿@k~ü¥E±¿ ëÆ»#c±¿WÍsD¾K±¿ï�!8ö°¿³&øŠn±¿U¿Òùð°¿Ã×׺Ô±¿@aÃÓ+±¿'Ý–Èg°¿uuÇb›T°¿ú›Pˆ€C°¿[ìöYe¦°¿$}ZE°¿« ºö°¿ê]¼·_®¿”3w¼É¯¿K�¦z2ÿ°¿Ïõ}8Hˆ²¿5 Þ�F²¿œ†¨ÂŸá±¿%uš²¿×]~p²¿Gˆ,Ò°¿^»´á°°¿¿»•%:˰¿$ïÊP±¿Owžxΰ¿â<œÀtZ¯¿1•~ÂÙ­±¿¿ÑŽ~7±¿xB¯?‰Ï±¿ÀÎM›q²¿¾ø¢=^H¯¿&å`6®¿)H4�"®¿Hû`­Ú­¿o�»ì¯¿ƒˆÔ´‹i®¿hÍ�¿´¨¯¿-Ó¾¹¯¿¤µûU€¯¿‘`ª™µ°¿‡kµ‡½P°¿1Îß„B°¿¨ÅàaÚ¯¿?§ ?¹®¿ü´W­¿LS8½‹¯¿³Ñ9?Åq°¿iàG5ì÷°¿Tàd¸±¿ù†Âgëà°¿S#ô3õº±¿‹ÆÚßÙ­¿6<½R–±¿ àbE ¦±¿J&§v†©±¿a‹Ý>«Ì°¿{ö\¦&±¿èy’t±¿üª\¨ük±¿b„ðh㈱¿Tœˆ~±¿ïå>9 ±¿"ýöuàœ±¿ªò=#±¿.É»š<±¿§@fgÑ;±¿~Q‚þB�°¿ô‹ôz°¿Ù˜×‡l°¿ï®³!ÿ̰¿ƒ§Z ³°¿ˆg 2*°¿…î’8+¢®¿=·Ð•°¿.sž±/±¿²ƒJ\Ǹ²¿ÉËšXà+²¿Ì}r ²¿×4ï8EG²¿=™ôMš²¿œˆ~mý°¿Ç¸ââ¨Ü°¿Q£�dVï°¿'Þž´p±¿…]=ð1°¿èf Ü¶¯¿B´V´9α¿¥½Á&S±¿'÷;²¿²½ôÞ²¿y=˜¯¿Hû`­Ú­¿LbõG®¿p +TT­¿è¾œÙ®Ð¯¿õ¸oµN\®¿1е/ ¯¿˜Ÿ��¯¿~âú}¯¿ú¸6TŒó¯¿HŒž[èJ°¿¬tw� ù¯¿˜l<Øb·¯¿êè¸Ù•®¿0�[wó¬¿#£’°o¯¿¶Û.4×i°¿xEð¿•ì°¿bjKäõ°¿@„¸röΰ¿`­Ú5!­±¿å 0óü¬¿§wñ~Ü~±¿ïSUh –±¿§#€›Å‹±¿#‡ˆ›Sɰ¿ŸrL÷±¿Â(›r±¿8MŸp]±¿ˆKŽ;¥ƒ±¿9€~ß¿y±¿Bêvö•±¿x·²Dg™±¿lîè¹±¿LÂ…<‚±¿UkaÚ9±¿Ê‹LÀ¯‘°¿$Ó¡Óón°¿Jš?¦µi°¿aßN"¿°¿Ê7Ûܘž°¿’;l"3°¿ó�èžu�®¿ï«r¡ò¯¿ñó߃×.±¿‘Жs)®²¿ÐÒl#²¿g+/ùŸü±¿Ï¾ò =E²¿¬äcw�’²¿Z Ý!ű¿£ x|{×°¿©¿^aÁý°¿e6È$#g±¿ÇF ^×/°¿ –\Å⯿ÌFçüDZ¿lÍV^ò?±¿És}²¿n¥×fc%²¿K?ªa¯¿){K9_ì­¿JDøAc®¿Í:ãûâR­¿{-è½1°¿€‚‹5˜®¿—Ž9ÏØ¯¿Ÿ>øù¯¿2rö´Ã¯¿¹5é¶D.°¿‚Zº‚m°¿ý¡™'×°¿ßøÚ3K°¿%“S;ÃÔ®¿-!ôlV­¿ ßû´¯¿o¹ú±I~°¿ /Ý$±¿2ª ãn±¿Žx²›ý°¿�ΤMÕ±¿‹Q×ÚûT­¿0.sž±¿]£å@µ±¿´8c˜´±¿åa¡Ö4ï°¿Z×h9ÐC±¿¸>¬7j…±¿ÍçÜíz±¿ûZ—¡Ÿ±¿<½R–!ޱ¿ú˜t&±¿>w‚ý×¹±¿É"M¼<±¿ª)É:]±¿à0Ñ O±¿O‘CÄÍ©°¿“ýó4`�°¿t%Õ?ˆ°¿áÐ[<¼ç°¿ JÑʽ°¿æÊ ÚàD°¿›T4Öþή¿¯]ÚpX°¿Ház®G±¿”Þ7¾ö̲¿E€Ó»x?²¿dyW=`²¿n0Ôa…[²¿²×»?Þ«²¿G 6u±¿’]i©÷°¿?Œm±¿¬ãø¡Òˆ±¿‚ÿ­dÇF°¿‘Õ­ž“Þ¯¿Õv|Óô±¿À3‰z±¿¯³!ÿÌ ²¿#žìfF?²¿·ìÿ°¥¯¿uÇb›T4®¿75Ð|ÎÝ®¿6l±Ûg­¿/÷ÉQ€(°¿.� ø1殿>°ã¿@°¿‰–<ž–°¿¼;2V›ÿ¯¿´up°71°¿ò\߇ƒ„°¿k�¸¯@°¿á² ›.°¿R_–vj.¯¿]£å@µ­¿‚Äv÷ݯ¿`¬o`r£°¿J¸�Gp#±¿œæ=Î4±¿f.py¬±¿2Ì Úäð±¿r£ÈZC©­¿œSÉPű¿À—ƒf×±¿…ÐA—p豿¶Mñ¸¨±¿ ƈD¡e±¿[AÓ+£±¿Ü‚¥º€—±¿¡B]±¿ "RÓ.¦±¿¶Øí³ÊL±¿cBÌ%UÛ±¿�ö?ÀZ±¿åœØCûX±¿ú{)°¿_b,Ó/¯¿º�ê°¿Pª}:3°¿>\rÜ)°¿·^Óƒ‚R°¿3¸<ÖŒ°¿kIG9˜M°¿¤á”¹ùF°¿x N} y¯¿ÞïU+®¿ÀÌwð°¿´ã†ßM·°¿Ó–x@±¿c('ÚUH±¿L£uT5±¿ ±ú# ²¿j1x˜öÍ­¿™õb('Ú±¿EÖJí±¿aü4Þv¡¹N#±¿eâVA t±¿p]1#¼±¿öF­0}¯±¿C�=·Ð±¿l´è¡¶±¿+*ÿZ^±¿j‰•ÑÈ籿§ÔE e±¿Ó1çû’±¿fÁÄE�±¿Ç¸ââ¨Ü°¿×L¾ÙæÆ°¿B_zûsѰ¿z«®C5%±¿;]¥»ë°¿.ÿ!ýöu°¿+TTýJ¯¿ÒQf`°¿[éµÙX‰±¿i6�Ã`þ²¿5ð£ö{²¿\ætYLl²¿ºõš”²¿vÀuÅŒð²¿h”.ýKR±¿&§v†©-±¿ƒ/L¦ F±¿,-#õžÊ±¿µ‡½PÀv°¿Åä 0ó°¿l’ñ+²¿'½o|í™±¿W"PýƒH²¿:ZՒ޲¿qUÙwEð¯¿äIÒ5“o®¿—á?Ý@�¯¿{ö\®¿ðN>=¶e°¿ÝД�~P¯¿È%Ž<Y°¿y²›ýh°¿î@�òèF°¿³Ñ9?Åq°¿šwœ¢#¹°¿;5— u°¿ î<0€°¿ÊŠ;Þ䯿Ê52;‹®¿ 0,¾-°¿Ï.ßú°Þ°¿Í’5µl±¿¢ ê[æt±¿¸æŽþ—k±¿d¬6ÿ¯:²¿Qƒi>"®¿Ã,´sš²¿LÃð1%²¿Íù†²¿=a‰”M±¿‚7¤Q�“±¿_]¨Å౿È—PÁ᱿È@ž]¾õ±¿b™¹À屿Á;ùôØ–±¿ßú°Þ¨²¿° ÍX4�±¿€-¯\o›±¿n¾ݳ®±¿û”c²¸ÿ°¿€»ì×�î°¿!\…zú°¿ìÕ[[±¿ÞÊ�e±¿4J—þ%©°¿ÑXû;Û£¯¿½©H…±…°¿òµg–¨±¿jÁ‹¾‚4³¿c'¼§²¿K‘|%�²¿ª¹Ü`¨Ã²¿û–9]³¿×j{¡€±¿šë4ÒR±¿9€~ß¿y±¿HS=™ô±¿[ìöYe¦°¿ôPÛ†Q°¿¸�Üšt[²¿6Y£¢Ñ±¿]mÅþ²{²¿ªa¿'Ö©²¿‘`ª™µ°¿ˆôÛ×�s®¿^Mž²š®¯¿“âã²ó®¿ö•é)r°¿®¼äòw¯¿ƒÞCp°¿û­�( ‰°¿5B?S¯[°¿=C8fÙ“°¿ßºñîȰ¿pìÙs™š°¿þc!:ް¿³–Òþ°¿˜Ãî;†Ç®¿­†Ä=–>°¿Ç{ö°¿Œ0E¹4~±¿�·•^›�±¿þ oÖà}±¿^‚SH²¿"Ä•³w®¿ÍX4�� ²¿—©Ið†4²¿iå^`V(²¿�&Œf±¿a4+Û‡¼±¿U‰²·”ó±¿[$íF󱿮,ÑYf²¿'÷;²¿È F³²±¿—©Ið†4²¿ó�)‚ª±¿g'ƒ£äÕ±¿C�=·Ð±¿c}“±¿øŠn½¦±¿LÂ…<‚±¿®ºÕ”d±¿J¸�Gp#±¿ð1Xqªµ°¿h%­ø†Â¯¿‹3†9A›°¿Kº ¾±¿èøhqÆ0³¿ÐÔë�±²¿x¸£²¿àŸR%ʲ¿àÖÝ<Õ!³¿Ó1çû’±¿˜3Ûú`±¿÷Ãc?‹±¿uèô¼ ²¿XŽ��<»°¿‘›á|~°¿?p•'v²¿ç�“¼DZ¿…î’8+¢²¿M…x$^ž²¿ï_{fI°¿)$™Õ;Ü®¿Ÿqá@H°¿ õôøÃ¯¿ª·¶J°°¿Ÿ>øù¯¿˜Á‘(´°¿TUh –Ͱ¿Âmmáy©°¿B_zûsѰ¿Ì|?q±¿Êß½£Æ°¿§/ú Ò°¿,Eò•@J°¿:Yj½ßh¯¿RC€ ˆ°¿'…y�3±¿ÅÿQ¡º±¿‰ jøÖ±¿L4HÁSȱ¿U£W”†²¿¸®˜Þ®¿åñ´üÀU²¿l#žìf²¿£;ˆ�)t²¿´„Ö×±¿» )?©ö±¿U±¿[²*ÂMF±¿«‹Ã™_±¿€-¯\o›±¿m6 B\±¿Äy8�é°¿ø�¯=³$°¿¢ñDçá°¿78ýÚú±¿Ø›’“‰³¿«°¿,ØF<ÙͰ¿u“V±¿âuý‚ݰ¿Žlê°¿y²›ýh°¿V+~©Ÿ¯¿8ƒ¿_Ì–°¿äg#×M±¿ßÃ%Ç�Ò±¿oõœô¾ñ±¿÷Ý—3Û±¿½«0™²¿q¬‹Ûh¯¿£7ün²¿ËǺ¸�²¿tϺFË�²¿]£å@µ±¿®€B=}²¿Çô„%P²¿ÒÈçO=²¿8¢{Ö5Z²¿9Ñ®BÊO²¿Åâ7…• ²¿ï¨1!æ’²¿Zõ¹ÚŠý±¿³˜Ø|\²¿¸àŸR%²¿�imÛk±¿4¼Yƒ÷U±¿°p’æ�i±¿‰±L¿D¼±¿÷êã¡ïn±¿Æ5>“ýó°¿!’!ÇÖ3°¿ÏÚmšë°¿Ñ=ë-²¿>ê¯WXp³¿*Wx—‹ø²¿— uXᲿJíE´³¿REñ*k³¿xî=\rܱ¿[AÓ+£±¿2È]„)ʱ¿ÅÅQ¹‰Z²¿eS®ð.±¿Ý´§!ª°¿ˆƒ„(_в¿eTÆÝ ²¿]ûzᲿiQŸä³¿ÉXmþ_u°¿²ºÕsÒû®¿V�Õ{L°¿ï_{fI°¿ë˜Ü°¿é(³ 0°¿aãúw}æ°¿L÷™±¿þš¬QѰ¿øŠn½¦±¿Ååx¢'±¿]1#¼=±¿ß¥Ô%㱿u¬Rz¦—°¿*Æ3h诿�Ky ²°¿n‡†Å¨k±¿œÞÅûqû±¿ ‚Ç·w ²¿õÕU�Z ²¿a5–°6Ʋ¿F´Swe¯¿¨ÿ¬ùñ—²¿Ìï4™ñ¶²¿Ùy›©²¿k¸È=]ݱ¿è÷ý›'²¿�¾IÓ h²¿©Or‡Md²¿ŸVÑšy²¿Ô‚}i²¿¤30ò²&²¿ù,σ»³²¿Uh –Ͳ¿yX¨5Í;²¿hé ¶O²¿� B²€±¿§Ëbbóq±¿‰&PÄ"†±¿$cµùÕ±¿ØEу±¿â[X7Þ±¿"q�¥]°¿Àyq⫱¿eM.²¿u¯“ú²´³¿Öª]Ò³¿*´t³¿ò$éšÉ7³¿E¸É¨2Œ³¿©;‡ú±¿šxxÒ±¿j1x˜ö±¿5Dþ o²¿8IóÇ´6±¿‡¥�Õ°¿HüŠ5\䲿é*Ý]gC²¿ñœú@ò²¿Â1Ëž6³¿ƒÞCp°¿3‡¤J&¯¿ôPÛ†Q°¿�‰”fó8°¿6ÇeÜÔ°¿‘¸ÇÒ‡.°¿¤§È!âæ°¿ØƒIññ ±¿O•ï‰Ð°¿8Ùî°¿Àyq⫱¿=~oÓŸý°¿mɪ7±¿då—Á‘°¿µ§!ªð¯¿[ìöYe¦°¿¡0(Óhr±¿!±Ý=@÷±¿Ì}r ²¿ta¤µû±¿àŸR%ʲ¿-{Øœƒ¯¿ÃFY¿™˜²¿Eg™E(¶²¿DàH Á¦²¿ßÃ%Ç�Ò±¿Yùe0F$²¿Ê1YÜd²¿AŸÈ“¤k²¿j£:Èz²¿�`ãúw}²¿õ-sº,&²¿!# Â¤²¿?�{ó²¿ÏdT8²¿S‘ c A²¿Ó…Xý†±¿Öã¾Õ:q±¿'á_�±¿75Ð|α¿–Tÿ ’±¿ `­Ú5!±¿±o'á_°¿‡á#bJ$±¿û Ë‚‰?²¿ä¢ZD“³¿Åã¢ZD³¿‡3¿š³¿ÀDˆ+³¿É¯bƒ…³¿òDçᲿsÖ§“ű¿g Ü¶ï±¿ND¿¶~²¿—Tm7Á7±¿x™a£¬ß°¿ÚV³Îø²¿÷“1>Ì^²¿sÛ¾Gýõ²¿6é¶D.8³¿]¥»ël°¿y=˜¯¿ô3õºE`°¿¨n.þ¶'°¿€^»´á°¿›8¹ß¡(°¿îZB>èÙ°¿¿ò =E±¿ÊI»Ñǰ¿ �¡c±¿‡5•Ea±¿â;þ ±¿ëˆ»z±¿DŠM °¿AºØ´R°¿³\6:ç§°¿WÎÞm±¿Ê`æ;ø±¿èŸàbE ²¿ÊN?¨‹²¿bX9´È²¿1´:9Cq¯¿HÅÿQ¡²¿­ÃÑUº»²¿‚�,`·²¿­¡Ô^DÛ±¿�v‡$²¿û$wØDf²¿l#žìf²¿/0+é~²¿ÚÉà(yu²¿é~NA~6²¿<Û¤¢±²¿wö•é)²¿�ßÛôg?²¿¢©ÛÙW²¿§#€›Å‹±¿¨ŒŸq±¿‹ˆbò˜±¿xî=\rܱ¿™Iô2б¿ÉÊ/ƒ1"±¿!Âøi°¿{ö\¦&±¿„Ø™Bç5²¿¼§>�¼³¿Sy=˜³¿úBÈyÿ³¿6é¶D.8³¿–² q¬‹³¿;ü5Y£²¿„Iññ Ù±¿Æ6©h¬ý±¿Ÿ®îXl“²¿°=³$@M±¿—â¶ô°¿_b,Ó/³¿¦í_YiR²¿Öª]Ò³¿^fØ(ë7³¿Ig`äeM°¿«˜J?á쮿2kœMG°¿Òã÷6ýÙ¯¿–’å$”¾°¿N ^ô°¿¸tÌyƾ°¿^…”ŸTû°¿ž\S ³³°¿—ÅÄæãÚ°¿�B=}þ°¿ë˜Ü°¿ï9°!±¿&áBÁ�°¿Éq§t°þ¯¿°bƒ…“°¿°=³$@M±¿Ò9?Åq౿Q¤û9ù±¿/†Èé뱿w;Sè¼²¿:“6U¯¿]Tœˆ²¿GqŽ::®²¿nî•y«²¿ZEhæÉ±¿¸<ÖŒ ²¿æÌv…>X²¿n0Ôa…[²¿û$wØDf²¿1³Ïc”g²¿³˜Ø|\²¿O;ü5Y£²¿ýKR™b²¿õ�ä�C²¿–z„ò>²¿šé^'õe±¿�½Þýñ^±¿�_x%És±¿‹¦³“Á±¿âꈻz±¿(~Œ¹k ±¿ô‡fž\S°¿~8Hˆò±¿¦bc^G²¿]¡·xx³¿ž•´â ³¿Ê£aQ³¿”†…$³¿m«Yg|³¿­QÑ貿;m�ÆÁ±¿oõœô¾ñ±¿1 íœf�²¿:$µP29±¿ò—õIî°¿ ŠcãósCS²¿ÓÁú?‡ù²¿à»Í³¿£V˜¾×°¿BÒ§Uô‡®¿J·%rÁ°¿òÐw·²D¯¿Mƒ¢y‹°¿zo À±¯¿–³wF[•°¿ö$°9ϰ¿|ïoÐ^}°¿0o�»°¿à�|zl˰¿0eà€–®°¿Oé`ýŸÃ°¿ÉPÅ�[°¿ 6ªÓ�¬¯¿Ú|a2U°¿Q.�_x%±¿¤q¨ß…­±¿¥øø„ì¼±¿ÌšXà+º±¿fõ‚Os²¿¶óýÔx鮿ip[[x^²¿ÞZ&Ãñ|²¿íš�Öt²¿<Äy8�±¿Õ—¥�šË±¿=+iÅ7²¿Oyt#,*²¿/Màô.²¿�½3Úª$²¿Ç ¿›nÙ±¿vúA]¤P²¿Æ2ýñÖ±¿ Ã|yö±¿xµÜ™ ²¿À%ÿ”*±¿$ñòt®(±¿¡€í`Ä>±¿ÿç0_^€±¿ÜŸ‹†ŒG±¿a7l[”Ù°¿Ã.Šø°¿wþEа¿LŒeú%Ɀð¥ð Ùu³¿ý2WÕ²¿*ÿZ^¹Þ²¿äÜ&Ü+ó²¿= ByG³¿÷Xúб¿4GV~Œ±¿ÄÐêä ű¿˜0š•íC²¿Hþ`à¹÷°¿*Æù›Pˆ°¿LÝ•]0¸²¿É <÷²¿ìöYe¦´²¿uå³<ü‹ 1“¨¯¿Í°QÖo&®¿¥žÐ믿DÃbÔµö®¿0Ö70¹Q°¿—®`ñd¯¿-ÌB;§Y°¿5Ñ磌°¿Mø¥~ÞT°¿$}ZE°¿Jy­„î’°¿È}«uâr°¿|š“™°¿Ô|•|ì.°¿â‘xy:¯¿ýM(DÀ!°¿€»ì×�î°¿yY |±¿nߣþz…±¿N³@»Cб¿#J{ƒ/L²¿aR||Bv®¿°Žã‡J#²¿�Ò¥I²¿Ú’Un2²¿¥½Á&S±¿GéÒ¿$•±¿å ïrß±¿E�¹‡„ﱿôNÜóü±¿æå°ûŽá±¿zÅrK«±¿IŸVѲ¿gHÅ«¬±¿úÕ‘#�±¿ª›‹¿±¿ÔFu:�õ°¿ºÚŠýe÷°¿œ5x_• ±¿ ¸çùÓF±¿U¿Òù°¿ÍÈ w¦°¿Ð}9³]¡¯¿î#·&Ý–°¿¾Ý’°«±¿nú³)"³¿sL÷™²¿u®(%«²¿yëüÛe¿²¿W²c#³¿fÁÄE�±¿³z‡Û¡a±¿âꈻz±¿úA]¤P²¿ƒÁ5wô¿°¿tšÚR°¿¹Ù•–‘²¿Ü ¢µ¢Í±¿$Õw~Q‚²¿~«uâr¼²¿ÖþÎöè ¯¿‰~mýôŸ­¿§êÙ\5¯¿¬8ÕZ˜…®¿ö'ñ¹쯿-]Á6âÉ®¿'Nîw( °¿}?5^ºI°¿î]ƒ¾ôö¯¿1&ý½°¿­†Ä=–>°¿%ËI(}!°¿%Ïõ}8H°¿Äz£V˜¾¯¿^L3Ý뤮¿ð0í›û«¯¿�H¾�°¿^d~�$±¿í)H4±¿ÒÇ|@ 3±¿ *ª~¥ó±¿ùÛž ±Ý­¿ŽÉâþ#Ó±¿”¼:ǀ챿?ÆÜ±¿¦aøˆ˜±¿ÿ°¥GS±¿‡ú]Øš±¿?eÄ ±¿öF­0}¯±¿ÐÓ€AÒ§±¿W%‘}�e±¿V W@ܱ¿R¹‰Zš[±¿<Äy8�±¿°È¯bƒ±¿Ê7Ûܘž°¿[@h=|™°¿«’È>Ȳ°¿“8+¢&ú°¿o½¦¥°¿Õ¯t>°¿®bñ›ÂJ±¿t ò³‘벿†:¬pËG²¿÷“1>Ì^²¿v5yÊj²¿Ù@ºØ´²¿ÜŸ‹†ŒG±¿>Y1\±¿6;R}籿¿dãÁ»±¿ñ)Æ3h°¿ÀÌwð°¿ãã²ó6²¿*Çdqÿ‘±¿ÅæãÚP1²¿öëNwžx²¿Ëhä󊧮¿î\éE­¿lèf Ü®¿½vß1<®¿�äòÒo¯¿¥Kÿ’T®¿Ž”-’v£¯¿ôPÛ†Q°¿Ãï¦[vˆ¯¿å`6†å¯¿˜÷8Ó„í¯¿ÛàDôk믿ía/°°¿'Ü+óV]¯¿36t³?P®¿†7kð¾*¯¿�^ Pj°¿¦}sõ°¿Þ„€| ±¿Nñ¸¨±¿:’ËH¿±¿«´Å5>“­¿£êW:ž±¿nj ùœ»±¿ºøÛž ±±¿èy’tͰ¿ëˆ»z±¿ýÙ�‘a±¿³z‡Û¡a±¿ÍÊö!o±¿Rewƒh±¿@aÃÓ+±¿½Œb¹¥±¿ñGQgî!±¿Â/õó¦"±¿Ì³’V|C±¿Ús™šo°¿R<¾½k°¿~Q‚þB�°¿�x]¿°¿Ô€AÒ§U°¿8©0¶°¿ÌC¦|ª®¿­§V_]°¿Mjh°±¿ÃFY¿™˜²¿T5AÔ}²¿E¡eÝ?²¿ äÙå[²¿a¦í_Yi²¿#Ù#Ô ±¿šwœ¢#¹°¿•»ÏñÑâ°¿THÞ9”±¿ 0,¾-°¿-Ó¾¹¯¿”XS²¿ÖXÂÚ;±¿kdWZF걿{3j¾J>²¿5~á•$Ï­¿uXá–�¤¬¿.rOWw,®¿@øP¢­¿’Ìên‡®¿ÜÖž—Š­¿1–é—ˆ·®¿¢ ±ˆa¯¿V‚Åá̯®¿— uXᮿz©Ø˜×¯¿ †7k𮿠7U†q¯¿PãÞü†‰®¿ší }°Œ­¿ÎZ_®¿ï«r¡ò¯¿çU�Õ{°¿œú@òΡ°¿Ê7Ûܘž°¿ÿ°¥GS±¿J´¬¿VF#ŸW<±¿ÿ°¥GS±¿ÚpXøQ±¿ãÉ¡f°¿¼­ôÚl¬°¿A·—4Fë°¿Œ�ðœú°¿Ù²|]†ÿ°¿¢$$Ò6þ°¿%‘}�eÁ°¿_—á?Ý@±¿¯#Ù@º°¿Ë�™`8×°¿¾g$B#ذ¿7§’°¿•™Òú[°¿‹üú!6°¿ëâ6À[°¿¥-®ñ™ì¯¿¾ø¢=^H¯¿Ãõ(\�­¿áaÚ7÷W¯¿­iÞqŠŽ°¿añd73²¿Ó1çû’±¿Ïø¾¸T¥±¿ ©¢x•µ±¿'÷;²¿Ãð1%’°¿²žZ}uU°¿¤l‘´}°¿�hW!å'±¿õôøÃϯ¿Ü M¯¿aU½üN“±¿[%Xΰ¿F¶óýÔx±¿F•aÜ ¢±¿Év¾Ÿ/­¿½�£9²ò«¿<½R–!Ž­¿Ö¬3¾/.­¿Ü¹0Ò‹Ú­¿"3¸<Ö¬¿©Ÿ7©0®¿ðß¼8ñÕ®¿dËòu®¿ÏIï_{®¿¯–;3Áp®¿¿Òùð,A®¿˜Ãî;†Ç®¿“9–wÕ®¿Ôž’sb­¿àòX32È­¿ S�Ÿ�\¯¿Pª}:3°¿¥hå^`V°¿}—R—Œc°¿�?üü÷°¿æ?¤ß¾¬¿fO›sð°¿iŒÖQÕ±¿lBZcÐ ±¿Cëâ6°¿pµN\ŽW°¿h"lxz¥°¿8Ûܘž°°¿Û…æ:�´°¿áE_Aš±°¿=Y¤‰w°¿êͨù*ù°¿(¸XQƒi°¿|Cá³up°¿ž)t^c—°¿„ äÙå[¯¿s�FZ*o¯¿ÍwðЯ¿F&à×H°¿w£�ù€@¯¿f¡Ø š®¿�|^ñÔ#­¿9`W“§¬®¿^gCþ™A°¿ä×±Á±¿g s‚6±¿µ©ºG6W±¿<2V›ÿW±¿£êW:ž±¿ølìM°¿Øñ_ °¿¹‰Zš[!°¿_\ªÒ×°¿W•}W¯¿_DÛ1uW®¿]~p>±¿€ØÒ£©ž°¿‹LÃð±¿³Îø¾¸T±¿&¦ ±ú#¬¿ÈA 3mÿª¿CæÊ Úି šyrM�¬¿¯Z™ðKý¬¿®~l’ñ«¿lîè¹­¿†« îê­¿o.2­¿^h®ÓHK­¿‚¬§V_]­¿ðûY,­¿ pzïÇ­¿¿F’ \­¿Çò®zÀ<¬¿*ý„³[ˬ¿!”÷q4G®¿n1?74e¯¿ü‹ 1“¨¯¿S%ÊÞRί¿c^G²�°¿³™CR %«¿‚ã2nj°¿t%Õ?ˆ°¿»G6WÍs°¿jmÛkA¯¿ õôøÃ¯¿òÍ67¦'°¿çrƒ¡+°¿OÏ»± 0°¿o‚oš>;°¿léÑTO毿{®Gáz°¿UÛMðMÓ¯¿4ØÔyTü¯¿Éq§t°þ¯¿è‚ú–9]®¿‰'»™Ñ�®¿Ež$]3ù®¿apÍý/¯¿U¯²¶)®¿�·•^›�­¿©¾ó‹¬¿]p¿˜­¿®¼äòw¯¿Óú[ðO±¿Ð*3¥õ·°¿µÂô½†à°¿�>‘'I×°¿j¿µ%±¿Rò겯¿¶óýÔx鮿GãàÒ1¯¿¶/ î\°¿ëýF;nø­¿$ñòt®(­¿¬Å§ϰ¿"Þ:ÿvÙ¯¿×mPû­�°¿IÕv|Ó°¿U£W°¿�»¶·[ª¿Xp?à�°¿°Œ Ýì°¿‘Ešx°¿iÄÌ>�Q®¿ã©GÜÖ®¿K?ªa¯¿ô66;R}¯¿¤µûU€¯¿ Š·˜¯¿Òm‰\p¯¿á' ß÷¯¿HüŠ5\䮿”ÃÕ¯¿Aðøö®A¯¿MùT�^­¿*�ÙYôN­¿æ‘?xî­¿x%És}®¿6“o¶¹1­¿v3£ §¬¿�*øD«¿Œ¾‚4cѬ¿m­/Úr®¿`�™D½°¿³š®'º.°¿»›§:äf°¿?;àºbF°¿ÿêqßj�°¿sL÷™®¿ÁgÓÀ­¿þÒ¢>É®¿1?74e§¯¿[D“7À¬¿°Y.�ó«¿Ý%qVDM°¿Í‚9zü®¿½ÅÃ{°¿Ý%qVDM°¿Ï¤MÕ=²©¿ìm3⑨¿²GWéô1èLª¿Ù%ª·¶ª¿§Ï¸®˜©¿ƒú–9]«¿`:­Û ö«¿õ‚Osò"«¿¹à þ~1«¿²GWé۾Gýõ «¿ñ·=Ab»«¿Ž •bG«¿ÏM›q¢ª¿pÏó§�ª¿#½¨Ý¯¬¿y¯Z™ðK­¿'½o|í™­¿å^`V(Ò­¿u殿g)YNB©¿‡‡0~÷®¿è÷ý›¯¿É=]ݱخ¿)Ý^Ò­¿×j{¡€­¿ÃØB�ƒ®¿l"3¸<®¿¤30ò²&®¿ð‰Ð6®¿J&§v†©­¿,*ât’­®¿m7Á7MŸ­¿Žêt ë©­¿�´ÿÖª­¿š †s 3¬¿Yû;Û£7¬¿"3¸<Ö¬¿ï�!8ö¬¿†©-u�׫¿¬�±^‚«¿| €ñ ª¿o¸�Üšt«¿E×…œO­¿?�Qžy9°¿$&¨á[X¯¿léÑTO毿*¬ÿs˜¯¿,ºõš°¿]p¿˜­¿²Õ唀˜¬¿k» ¾iú¬¿«]Òƒ®¿Ržy9쾫¿í-å|±÷ª¿©ƒ¼LН¿¾4»î­¿É w¦(¯¿³³è� ¸¯¿‰îY×h9¨¿�Üšt["§¿qxµÜ™©¿9í)9'ö¨¿u?§ ?©¿¬ÿs˜//¨¿�ôiý¡©¿B±4-±ª¿ˆ*üÞ¬©¿Ÿ��·±Ù©¿ý-ø§T©¿‡Ä=–>t©¿sHj¡drª¿ýKR™bª¿JzZ�œ©¿üÇBt©¿$�›:�ª¿+‡ÙÎ÷«¿0º¼9¬¿éc> Й¬¿îx“ߢ“­¿¢œhW!å§¿ÒV%‘}�­¿ƒjƒѯ­¿3ÀÙ²|­¿%�»¶·«¿Ëø÷¬¿Qèy’¬¿K°8œùÕ¬¿‡ú]Øš­¬¿û]Øš­¼¬¿ ܺ›§:¬¿”ö_˜L­¿|dsÕeÄ­¿Ô‚}i®¿¡JÍh®¿àªÔì�®¿ßmÞ8)¬¿¢ †7«¿:ùÙÈu«¿»šVðÛ¨¿gCþ™A|¨¿@PnÛ÷¨§¿(~Œ¹k ©¿§ ?¹nª¿ {Úá¯Éª¿Ánض(«¿¬Žé ¬¿ýh8en¦¿¼çÀr„ ¬¿�ÓUø3¬¿1&ý½¬¿d¬6ÿ¯:ª¿ÙÍŒ~4œª¿ÎˆÒÞà «¿�]¢zk`«¿žACÿ«¿0ÕÌZ H«¿õ÷RxÐ쪿7Ь5”Ú«¿l”õ›‰éª¿¡­Ü ̪¿í-å|±÷ª¿6?þÒ¢>©¿E�>‘'©¿ªó¨ø¿#ª¿Þs`9Bª¿-ÌB;§Y¨¿©ù*ùØ]¨¿¢&ú|”§¿«’È>Ȳ¨¿´Ë·>¬7ª¿S@Úÿk­¿b k_@/¬¿»}V™)­¿z9ì¾cx¬¿å 0óü¬¿µá°4𣪿ò%Tpx©¿X;ŠsÔÑ©¿<ö³XŠä«¿âX·Ñ¨¿?;àºbF¨¿Î§ŽUJϬ¿}“EÖª¿ys¸V{Ø«¿ìÁ¤øø„¬¿´;¤ Ѥ¿ m6 B¤¿Êp<Ÿõ¦¿T^-w¦¿y¯Z™ðK¥¿OÌz1”¥¿1ì0&ý¥¿†r¢]…”§¿¸°n¼;2¦¿$D©¿ dv½S©¿«&ˆº@ª¿p^œøjG©¿N€aùóm©¿„H†[Ϩ¿4ºƒØ™B§¿ÿ®Ïœõ)§¿:ÏØ—l<¨¿öó娿ôú“øÜ ¦¿©Ÿ7©0¦¿Ü›ß0Ñ ¥¿Ò:ªš ꦿPR`L¨¿¤8GW«¿·)Õ"ª¿èLÚTÝ#«¿¸�Üšt[ª¿ÅX¦_"Þª¿"T©Ù­¨¿K?ªa§¿¢DKO˧¿dT8‚Tª¿DkE›ãܦ¿ʦ\á]¦¿´éàfñª¿O•ï‰Ð¨¿…Ì•AµÁ©¿»(zàc°ª¿Š‘%s,ºM¸Wæ­¢¿y¯Z™ðK¥¿s¸V{Ø ¥¿ ÅVдĢ¿/¢í˜º+£¿‚ŽVµ¤£¿ºøÛž ±¥¿£#¹ü‡ô£¿õ  ­Ü£¿}гYõ¹¢¿Õ•Ïò<¸£¿ vöE™¥¿–´â Ÿ¥¿ÅS�4¸­¥¿à ½úx裿ËI(}!䤿tì ×1¦¿Ù—l<Øb§¿Ý³®Ñr §¿ï�b€D¨¿pÏó§�¢¿£ZD“7¨¿ûvþE¨¿Xtë5=(¨¿(Óhr1¦¿¨Š©ôΦ¿¦·? §¿£ x|{§¿ã4Dþ §¿*ât’­.§¿C®Ô³ ”§¿)ë7Ó…¨¿(›r…§¿'Ü+óV]§¿ 'iþ˜Ö¦¿†åÏ·K¥¿4Õ“ùGߤ¿AŸÈ“¤k¦¿¥ƒõó¥¿�*øD£¿ÄÎ:¯±£¿¡GŒž[袿1'h“Ã'¥¿Å °rh‘¥¿Âmmáy©¨¿ÚUHùIµ§¿i©¼á´¨¿ÊPSé§¿°�¹2¨6¨¿{¾f¹lt¦¿ `­Ú5!¥¿ù g³ês¥¿¥óáY‚Œ¨¿Ì|?q¥¿ðRê’qŒ¤¿ØÓMÖ¨¿1 íœf�¦¿¡K8ô§¿l|&ûçi¨¿€ð¡DKŸ¿¶×‚ÞC ¿Ìï4™ñ¶¢¿RíÓñ˜�¢¿‘~û:pž¿SäG ¿¾-Xª  ¿ó‘”ô0´¢¿Ìbbóqm ¿¸ õô ¿V�Z ¦�¿GN¶�; ¿©ÞØ*Á¢¿ãý¸ýòÉ¢¿ 9¶ž!£¿µßÚ‰’ ¿· Íui¡¿Þ‘±Úü¿¢¿Õ³ ”÷q¤¿¸éÏ~¤ˆ¤¿ìL¡ó»¤¿ˆØÒ£©ž¿ù½Mö#¥¿î }°Œ ¥¿JVÕËï¤¿Æø0{Ùv¢¿ÄC?�{£¿Ý^Ò­£¿Kê46¤¿Rb×öv£¿ƒèÚУ¿rúz¾f¹¤¿†p̲'�¥¿t%Õ?ˆ¤¿Û…æ:�´¤¿�»S”K£¿ðÝzM¢¿€-¯\o›¡¿ˆ×õ v£¿„š!U¯¢¿£Xni5$ž¿gEÔDŸŸ¿×övKrÀž¿!�Ky ¢¿î|?5^º¡¿™º+»`p¥¿jÂö“1>¤¿ú{)?Œm¤¿�ý,–"ù¢¿ÎŽTßùE¡¿¯Ì[uª¡¿ ©¢x•µ¥¿XÇñC¥¡¿äôõ|Ír¡¿„d¸¥¿4õ»°5£¿w÷Ý—3£¿çÑ=륿GT¨n.þ–¿ áͼ¯š¿È•z„òž¿ç§8¼Zž¿±5[yÉÿ”¿J)èö’Ƙ¿o�Å�1—¿êz¢ëž¿à€J•˜¿}%�»¶—¿lí}ª ”¿’%s,流¿õhª'ó�ž¿˜¦pzŸ¿æ¾÷7 ¿ŠUƒ0·{™¿+Àw›7Nš¿h†¬nõœ¿ N} yç ¿áÐ[<¼ç ¿]¦&ÁÒ ¿1Ñ O!—¿ífF?N¡¿Ë€³”,'¡¿ÖÆØ /¡¿ŸŠ‘%sœ¿ ²H˜‰"¤ngŸ¿—ÿ�~û: ¿Ê52;‹ž¿™¼f¾ƒŸ¿³ïŠà+¡¿Ô›QóUò¡¿¿ò =E¡¿Àˆ¿)@̘‚…¿&«"ÜdTy¿X8IóÇ´†¿å—Á‘(”¿?sÖ§“•¿FzQ»_˜¿I„F°qý‹¿Gä»”ºdŒ¿iŒÖQÕ‘¿øŠn½¦—¿À{G� 1—¿³Dg™E(–¿—®`ñd‡¿®Gáz®—¿˜Në6¨ý–¿­jI—¿Ìí^î“£�¿t|´8c˜“¿be4òyÅ“¿5¸­-“¿ À;ùôØ–¿Y·ÑÞ’¿›sðLh’¿ÖÆØ /‘¿_)ËǺˆ¿/o×j‹¿)=ÓKŒeš¿=·Ð•T�¿ÝÑÿr-Z�¿ÆàaÚ7÷—¿›®'º.ü�¿Ecíïl�Ž¿õ-sº,–¿]Þ®ÕN?1{ÙvÚ�¿˜üOþî…¿°«ÉSVƒ¿¡½úxè»{?<-?p•'P¿ö|ÍrÙèl?î=\rÜ)}¿ù¿#*T7W?iSu�l®j?€'-\Vaƒ?ë§ÿ¬ùñg?”ùGߤi€¿©¾ó‹„¿gF?N™‹¿d!:ŽJ¿ìNwžxÎ&¿óæp­ö°g¿}w+Kt†¿Ã(ßÞ…¿xÐ캷"�¿ã©GÜÖV?ïÈXmþ_…¿*T7Ûƒ¿¤‹¦³ƒ¿ë�0 Xre¿„%Zòxz¿ùK‹ú$wx¿¬9@0G�¿è…;Fzq¿ 4Ô($y¿{…÷ˆ¿Ý—3ÛŠ¿îziЧ‡¿^×/Ø Û†¿2®¸8*7q¿§"Æ‚l¿6wô¿\‹6¿KÈ=›U¿£V˜¾×l¿¨9y‘ ø…?=›UŸ«­x?w×Ù�fp?§ÌÍ7¢{v¿£’°o'a?~£<órx¿WÎÞmUr¿Yni5$î�¿Š!9™¸Up¿Êß½£Æ„h¿ˆWÎÞi¿6wô¿\‹f?ú~j¼t“X?å™—Ãî‹¿d?‹¥H¾b¿st´ª%m¿é� ¸çùƒ¿º ¾eNg¿õ×+,¸??·_>Y1\}¿°ŒØ'€’?„H†[Ïp?ºöôÂm? ‹Q×Úût?å€]Mž²š?ë�ZaúŽ?äù ¨7£–?¼·_>Y�?v28J^�“?½ÿ�&Œ–?sœÛ„{ež?sÖ—?H§®|–ç©?Í�¿´¨O¢?/N|µ£8§?¸¯çŒ(�?+ö—Ý“‡¥?ÜõÒN§?lЗÞþ\¬?yvùÖ‡õ¦?M½në›?�ìJËH½—?rˆ¸9• �?%ÊÞRΣ? -ëþ±¥?¹à þ~1£?Ô·Ì鲘˜?„Iññ Ù™?\WÌoŸ?G¢`Ƥ?$*T7›?¬TPQõ+�?Mƒ¢y‹œ?¶ÚÃ^(`£?¡º¹øÛž ?oÓŸýH¡?¾÷7hŸ?pìÙs™¢?*äJ= B¡?>u¬Rz¦—?;©/K;5—?˜Ÿ��—?êu‘BY˜?ƒ4cÑtv¢?`�ºò¡?ËI(}!ä¤?c_²ñ`‹�?£®µ÷©¢?mýôŸ5?®?1A ߺ©?9&‹û�L§?pïô¥·Ÿ?ÛàDôkë§?XŒºÖÞ§¢?|F"4‚�£?w¦(—ÆŸ?j¼t“¤?NÒü1¥?À=ÏŸ6ª£?ë:TS’u¨?­¹Ä‘§?@léÑTO–?*6æuÄ!£?ä×±Á¡?I†[Ïž?8Hˆò-¤?äqs*¨?šë4ÒR¡?Ý 7àóð?Ê‹LÀ¯‘¤?”XS¦?Ž •b§?ãut\�´? 6u¯?bhur†â²?—o}XoÔª?ϤMÕ=²±?qTn¢–æ²?5˜†á#¶?‚<»|ëò?³ðõµ.5ª?r„Ѭl§?þÖN”„D¢?ÆíñB:°?À燣±?0¹Qd­¡°?Á:Ž*�¨? dv½S©?CqÇ›ü­?V€ï6oœ°?“«Xü¦°ª?©‡ht±«?5AÔ}R«?v3£ §°?$Diâ­? ô‰�Q²?Y0ñGQg²?$ÑË(–[²?t'Ø�›²?‹ÿ;¢Bu·??§ ?¹¶?4Õ“ùG߸?*Æù›P´?—®`ñd·?³êsµû¿?â镲 q¼?ðÝzMº?¯yUgµ´?1¬Zd»?��@¼®_¸?q¹5鶸?ðÁk—6¶?æ@µm¹?Ø·“ˆð/º?¥t{Ic¸? Š·˜»?•ð„^»? a5–°6²?„¸röÎh·?És}¶?Ù•–‘zOµ?,ØF<Ù͸?ï8EGrù»?ú·Ë~Ýé¶?µ�?QÙ°¾?àØ³ç2µ?�ר%ª·¶?ïU+~©·?‡Ü 7àóÁ?N`:­Û¼?‡R{mÇÀ?›’¬ÃÑUº?ú ¨7£æ¿?ÖÇCßÝÊÀ?ùe0F$ Ã?ïV–è,³À?å+�”ص¹?d@öz÷Ç·?j¾J>v´?ÇeÜÔ@ó½? ]lZ)À? ®¹£ÿå¾?àØ³ç25¹?gœ†¨ÂŸ¹?m6Vbž•¼?ðLh’XR¾?^ƒ¾ôöçº?V„aÀ’»?Sê’qŒd»?éH.ÿ!ý¾?ƒi>"¦¼?NÔÒÜ a½?æ¾÷7¼?²×»?Þ«¾?ˆŸÿ¼v½?0¹Qd­¡¸?´�ü6ĸ?7TŒó7¡¸?» j¿µ¹?IœQ}¾?'iþ˜Ö¦½?ßÅûqûå¿?^+¡»$κ?Ô~k'JB¾?ßú°Þ¨Ä?ª™µ�öÁ?b/°ŒÀ?w£�ù€@»?ÒSäqÁ?¿×—q¿?éEí~à¿?“�HÛø½?5 Šæ,À?úa„ðhãÀ?REñ*k¿?äL¶ŸŒÁ?N™›oDÁ?»&¤5�¸?žðœú@¾?§Y Ý!ż?’?xî=¼?ÔE eáë¿?�˜Ùç1ÊÁ?Žuq à½?𢯠ÍXÂ?)éahurº?’“‰[1¼?æ@µm½?ÀBæÊ ÚÄ?�8débÁ?�?üü÷àÃ?ºÛõÒÀ?R �hÃ? l#öÃ?p À?¥JÆ?:è½Ã?ÙCûXÁo¿?�yÈ”A½?q‘{ººc¹?5³–ÒþÁ?sž MÃ?øÆ{Â?Œó7¡¿?̵hÚV¿?úDž$]Á?mÊÞå"Â?]Á6âÉnÀ?à�|zlËÀ?_}<ôÝ­À?pÏó§�Â?= ByGÁ?é8h°Á?y$^žÎÁ?ì@1²dÂ?÷w¶Go¸Á?¯@ô¤L¾?£çº�¾?ÐECÆ£T¾? 5?þÒ¢¾?»CŠMÂ?Ò�°¨ˆÓÁ?Ñ’ÇÓòÃ?æv/÷ÉQÀ?bž•´âÂ?Vž@Ø)VÇ?! _BÅ?ûPŒ,™Ã?ZcÐ ¡ƒÀ?¨SÝ‹Ä?l’ñ+ÖÂ?Ë÷ŒDhÃ?þ`à¹÷pÁ?&«"ÜdTÃ?vmo·$Ä?Ó¾¹¿Â?ªa¿'Ö©Ä?G8-xÑWÄ?�‹jQL¾?iTàdÂ?d¯w¼WÁ?yvùÖ‡õÀ??ŒmÃ?py¬äÄ?©Åä Â?Š;Þä·Ä?9ïÿã„ ¿?š–X�|À?ZFê=•ÓÀ?jÞqŠŽäÆ?ÌFçüÇÃ?r‰#DÆ? @£té_Â?4hèŸàbÅ?ÙêrJ@LÆ?]øÁùÔ±È? ·_>Y1Æ?þÒ¢>ÉÂ?èô¼ Á?ìÜ´§!¾?v5yÊjÄ?LûæþêqÅ?|(Ñ’ÇÓÄ?t ]‰@õÁ?ÞɧǶ Â?âͼ¯ÊÃ?•,'¡ô…Ä?”4LkÓÂ?w÷Ý—3Ã?ã4Dþ Ã?» ¾iúìÄ?õiý¡™Ã?,g~5Ä?½¦¥hÃ? oB@¾Ä?LÁgÓÄ?p?à�„Á?Ç,{ØœÁ?a©.àe†Á?¯zÀ9 Ã?ñ/‚ÆL¢È?0Ÿ¬®Æ?ÑÌ“k dÈ?ÓÞà “©Ä?÷Ç{ÕÊ„Ç?mWèƒelÈ?+¥gz‰±Ê?=¶eÀYJÈ?Ûø• kÄ?Í<¹¦@fÃ?zïÇí—Á?ªµ0 íœÆ?ÏL0œk˜Ç?zQ»_øÆ?p]1#¼=Ä?>“ýó4`Ä?*¬TPQõÅ? ȳ˷Æ?ÝE˜¢\Å?rÌ_!sÅ?ú'¸XQÅ?0JÐ_èÇ?Z›ÆöZÐÅ?ÀÐ#FÏ-Æ? 4Ô($™Å?¸v¢$$ÒÆ?Fì@1Æ?vàœ¥½Ã?éCÔ·ÌÃ?d¯w¼Ã?�P3¤ŠâÃ?V ì1‘ÒÆ?Ò©+ŸåyÆ?¦D½ŒbÇ?J ,€)Å? ¨7£æ«Æ?LS8½‹Ë?Ü¡a1êZÉ?_aÁý€È?rÝ”òZ Å?g~5æÈ?–wÕæ!Ç?Â5wô¿\Ç?aŽ¿·éÅ?‹Þ©€{žÇ?È%Ž<YÈ?,cC7ûÇ?û�·\ýØÈ?«Íÿ«ŽÈ?�J±£q¨Ã?bƒ…“4Æ?¡i‰•ÑÈÅ?î—OV WÅ?ïÆ‚Â LÇ?B–É?Ì`ŒHZÆ?ªÔì�V`È?Žs›p¯ÌÃ?£Ì™däÄ?rüPiÄÌÄ?Û0 ‚Ç·É?ȳ˷>¬Ç?Ýa™¹ÀÉ?¶…ç¥bcÆ?†¬nõœôÈ?ȵ¡bœ¿É?ÜcéCÔË?Iõ�_” É?«x#óÈÆ?*äJ= BÅ?Ç.Q½5°Ã?Yùe0F$È?׆ŠqþÈ?È}«uârÈ?“áx>êÅ?Û¿²Ò¤Æ?ä „™¶Ç?7qr¿CQÈ?RóUò±»Æ?¨SÝÇ?xEð¿•ìÆ?C­iÞqŠÈ?®òÂNÇ?¹DkE›Ç?Å9êè¸Ç?_–vj.7È?¨V_]¨Ç?q;4,F]Å?±KXcÅ?®ºÕ”dÅ?ÅÈ’9–Å?²ô¡ ê[È?Éq§t°þÇ?¨Š©ôÎÈ?OçŠRB°Æ?"œÁß/È?�=·Ð•Ì?·˜ŸšÊ?¶„|гYÉ?'L5³–Æ?�¥½ÁÊ?óZ Ý%qÈ?r„ѬÈ?úÏšiÇ?{Oå´§äÈ?ìÀ9#J{É?ønóÆIaÈ?ùº ÿéÊ? 8€~ß¿É?x*àžçOÅ?;q9^�èÇ?•'vŠUÇ?Þ“‡…ZÓÆ? !çýœÈ?ô¨ø¿#*Ê?ð0í›û«Ç?¨ŒŸqáÈ?|,}è‚úÄ?†­ÙÊKþÅ?[™ðKý¼Å?Ÿ��·±É?Çô„%PÈ?ÄvüÊ?¨ÅàaÚ7Ç?]lZ)rÉ?+ß3¡Ê?K¯ÍÆJÌË?¶ö>U…Ê?aŠriüÆ?Ü ö[;QÆ?+4ËfÅ?ª x™a£È?×òAÏfÉ?¨ŒŸqáÈ?ø¥~ÞT¤Æ?}iÆ¢éÆ?_aÁý€È?›™�EïÈ?´«�ò“jÇ?‡O:‘`ªÇ?£>É6‘Ç?éòæp­öÈ?aŒHZÖÇ?Ýîå>9 È?¶»辜Ç?ÌëˆC6�È?°âTkaÈ?J m6 Æ?®GázÆ?µ¤£Ì&Æ?ÈÍp>?Æ?N¸Wæ­ºÈ?j£:ÈzÈ? YÝê9É?«\¨ükyÇ?S ³³è�È?#KæXÌ?ù‚0ºÊ?«“3w¼É?ÃbÔµö>Ç?T;ÃÔ–:Ê?lµ‡½PÀÈ?ÓÁú?‡ùÈ?"ŠÉ`æÇ?ý¢ý…É?ZÊû8šÉ?ãûâR•¶È?¯³!ÿÌ Ê?Ψù*ùØÉ?Ý—3ÛÆ?|~!<È?˜ËôKÄÇ?Ä?léÑTÇ?‰ÿ"hÌÈ?×ÜÑÿr-Ê?¿ñµg–È?òšWuV È?ç%è/ôÄ?b»{€îËÅ?^-wf‚Å?VGŽtFÈ?®›R^+¡Ç?Ö¨‡htÉ?÷vKrÀ®Æ?³Ñ9?ÅqÈ??­¢?4óÈ?—g)YNÊ?ìú»aÛÈ?S°ÆÙtÆ?ä1•ñïÅ?‹p“QeÅ? ~þ{ðÚÇ?­4)Ý^È?q©J[\ãÇ?Œöx!Æ?~p>u¬RÆ?sf»B,Ç?¾K©KÆ1È?rúz¾f¹Æ?.� ø1æÆ?N`:­ÛÆ?ÅŒðö È?[%XÎüÆ?Õ%ãÉÇ?²/Ùx°ÅÆ?)–[Z ‰Ç?úDž$Ç?ºöôÂ�Å?‰Ð6®Å?“ÅýG¦Å?˜Âƒf×½Å?¬„¹ÝËÇ?§%VF#ŸÇ?:w»^š"È?ZFê=•ÓÆ?$_ ¤Ä®Ç?™ðKý¼©Ê?}@ 3iSÉ?Ý a5–°È?y;ÂiÁ‹Æ?R||BvÞÈ?ù¢=^H‡Ç??þÒ¢>ÉÇ?’Y½ÃíÐÆ?‰_±†‹ÜÇ?3ˆìø/È?¶»辜Ç?Òà¶¶ð¼È?+Kt–Y„È?ž( ‰´�Å?@mT§YÇ?›™�EïÆ?õ¸oµN\Æ?¾Ý’°«Ç?LÝ•]0¸È?©¿^aÁýÆ?«zù�&3Æ?ÇeÜÔ@óÃ?gCþ™A|Ä?4 çfÄ?šÐ$±¤ÜÅ?Ê`æ;øÅ?Ú¬ú\mÅÆ?³•—üOþÄ?Ì EºŸSÆ? À%W±Æ?Àé]¼·Ç?oò[t²Æ?!Z+ÚçÄ?�`ãúw}Ä?F"4‚�ëÃ?-“áx>Æ?;TS’u8Æ?¿rÞÿÇÅ?b/°ŒÄ?î{Ô_¯°Ä?ê46<Å?ô1èLÆ?ä0˜¿BæÄ?ǼŽ8dÅ?šÏ¹ÛõÄ? ~b¼æÅ?:vP‰ëÅ?kœMG7Å?HüŠ5\äÄ?Ïôc™~Å?µ¥òz0Å?0™ò!Ä?züÞ¦?ûÃ?>ù*Ä?~!<Ú8Ä?¬Ä<+iÅÅ?$·&Ý–ÈÅ?Ô�'ž³Æ?ÛÂóR±1Å?N 4Ÿs·Å?ƒn/iŒÖÇ? U܏ů?jûWVš”Æ?Ì|?qÅ?Ð�`ÿunÆ? YÝê9Å?-'¡ô…�Å?<À“.«Ä?N ˆI¸�Å?%æYI+¾Å?ß4}vÀuÅ?ô‡fž\SÆ??9 3Æ?ž@Ø)V Ä?×¾€^¸sÅ?dWZFê=Å?l¯½7†Ä?jÛ0 ‚Å?.rOWw,Æ? ^ô¤Å?ÞªëPMIÄ?Ï, PSËÂ?}¯!8.ãÂ?ï;†Ç~Ã?«²ïŠàÃ?ªIð†4*Ä??n¿|²bÄ?oÕu¨¦$Ã?UÃ?iäóЧÃ?3mÿÊJ“Â?<Ýyâ9[Â?ä£ÅÜÂ?ÍÈ w¦Â?ªb*ý„Ã?]ûzáÎÃ?0óüÄÃ?]߇ƒ„(Ã?[éµÙX‰Ã?´“ÁQòêÄ?Ujö@+0Ä?�f,šÎNÄ?ðne‰Î2Ã?PÕé@ÖÃ?8 ¥+ØÂ? `­Ú5!Ã?ffffffÂ?§<ºÃ?iäóЧÃ?/¢í˜º+Ã?"Þ:ÿvÙÃ?vŠUƒ0·Ã?'‰%åîsÂ?Lý¼©H…Ã?çÁ=~Ã?ãÉ¡fÂ?Cs�FZ*Ã?BÏfÕçjÃ?"3¸<ÖÂ?"8öì¹Â?“áx>êÁ?'iþ˜Ö¦Á?PT6¬©,Â?»,D‡ÀÁ?ÖµÂô½Â?Ü ö[;QÂ?¤oÒ4(šÁ?aÃÓ+eÂ?�FZ*oGÂ?l’ñ+ÖÂ?õfÔ|•Â?ÿÊJ“RÐÁ?.VÔ`†Á?±Äʦ\Á?4òyÅS�Â?Hˆò-$Â?t(CUL¥Á?Lÿ’T¦˜Á?…^Ÿ;Á? ÛÝtÁ?¸•^›�•Â?¾¤1ZGUÁ?¬¬mŠÇEÁ?y±0DN_Á?Íui©¼Á?’#��‘—Á?;ŪA˜Á?|eÞªëPÁ?÷s ò³‘Á?]lZ)rÁ?$Ò6þDeÁ?B˜Û½Ü'Á?K %vmoÁ?o×j{Á?ÙÐÍþ@¹Á?Éw)uÉ8Â?h®ÓHKåÁ?ìW\•Á?�Ç TÆ¿Á?À]öëNwÂ?V]ûÂ?ŒƒKÇœgÂ?­Mc{-èÁ?Ð^»´Á?qËGRÒÃÀ?wKrÀ®&Á?ñº~ÁnÀ?�zˆFwÁ?C’Y½ÃíÀ?�PáRÁ?’á (ÔÁ?iR º½¤Á?‚ªÑ«JÁ?Ì'+†«Â?ì2ü§(Â?L3Ý뤾À?|îû¯sÁ?Æ‚”0Á? åD» )Á?ü§(ðNÂ?±Š72�üÁ?~sõ¸oÁ?…]=ð1Â?Õ{*§=%Á?Ks+„ÕXÂ?~sõ¸oÁ?st´ª%Á?ƒÛÚÂóRÁ?‘ð½¿A{Á?¥¤‡¡ÕÉÁ?/j÷«ßÁ?.�Ò¥Á?ޝ=³$@Á?žEïTÀ=Á?ÅŽÆ¡~Â?Þt_ÎlÁ?J²GWéÀ?·³¯Á?órØ}ÇðÀ?ÇóPoFÁ?,D‡À‘@Á?�G©„'ôÀ? K< lÊÁ?¬ª—ßi2Á?Þ Z+ÚÁ?oÓŸýHÁ?2Çž=Á?Ï2‹PlÁ?0du«ç¤Á?Ý µ‰“Á?¨4bfŸÇÀ?aÁý€À?ýØ$?âWÀ?îCÞrõc¿?«zù�&3À?h°©ó¨ø¿?½f¾ƒŸÀ?Ã×׺ÔÁ?÷x!ÂÀ?msczÂÁ? lÎÁ3¡Á?ÕÊ„_êçÁ?ö^|Ñ/À?ê”G7ÂÀ?ÑëOâs'À?;‹Þ©€{À?ÿ'LÍÂ?Ɖ¯vçÂ?É!âæT2Â?�˜„ yÃ?²Ôz¿ÑŽÁ?.æç†¦ìÂ?Éå?¤Á?ÓiݵÁ?ÿ••&¥ Á?(ðN>=¶Á?cÒßKáÁ?7mÆiˆ*Â?2åCP5Â?Ç»#cµùÁ?Ùµ½Ý’Â?öCl°p’Â?sÖ§“ÅÁ?Òs ]‰@Á?'¢_[?ýÁ?cÔµö>UÁ?‹¥H¾HÁ?eáëk]jÂ?‹¥H¾HÁ?¨mÃ(Á?�àFÊIÁ?Ë‚‰?Š:Á?Ç�WË�Á?qçÂH/jÁ?°‘$W@Á? YÝê9Á?w£�ù€@Á?c˜´ÉáÁ?î"LQ.�Á?N˜0š•íÁ?ÆàaÚ7÷Á?ýú!6X8Á?ZÖýc!:Â?r„ѬlÁ? ¥ö"ÚŽÁ?@ �RÁ?€D(bÁ?X}w+Á?t ]‰@õÁ?»íBs�FÂ?ý¾óâÀ?K>v()À?+MJA·—À?Ònô1À? q¬‹ÛhÀ?²gÏejÀ?¶�¶FãÀ?�ˆ)‘D/Á?�¥]PßÀ?D“7ÀÌÁ?ØØ%ª·Â? çfhÂ?ãÆ-æç†À?=Ô¶aÁ? ýL½nÀ?GW#»À?78ýÚúÃ?½§rÚSrÄ?ß¿yqâ«Ã?Cÿ+jÄ?†!rúz¾Â?qêé#Ä?ëÃz£V˜Â?î^î“£Ã?‚ŽVµ¤£Â?©2Œ»A´Â?’$W@¡Â?¬TPQõ+Ã?�ü6ÄxÃ? ë©ÕWWÃ?xcAaP¦Ã?p™Óe1±Ã?Hú´ŠþÐÂ?JìÚÞnIÂ?ýÚúé?Ã?ÿ”*Qö–Â?[µkBZcÂ?Žèžu�–Ã?½S÷<Â?¨“ÅýGÂ?¸u7OuÂ?Rb×övKÂ?{ù�&3ÞÂ?þ¹hÈx”Â?]¡·xÂ?S=™ôMÂ?OqN`Â?’ê;¿(AÃ?¨ŒŸqáÂ?x*àžçOÃ?&S£’:Ã?'ÛÀ¨SÂ?ë7Ó…XÃ?pìÙs™Â?z¥,CëÂ?¥Kÿ’Â?9 {ÚáÁ?Õ$xCÂ?Úã…txÃ?&4I,)wÃ?y’tÍäÁ?s¼Ñ“2Á?ÝAìL¡Á?šž^)Á?«®C5%YÁ?QMIÖáèÀ?r2q« Â?õÙ×3Â?á%8õ�äÁ?Ä`þ ™+Ã? Ü¶ïQÃ?øTN{JÎÃ?rý»>sÖÁ?-™cyW=Â?æ–VCâÁ?biàG5ìÁ?K̳’V|Å?¥hå^`VÆ?¶»辜Å?Ä "RÓ.Æ?̶ÓÖˆ`Ä?â!ŒŸÆ½Å?¼=ùÄ?Ó¾¹¿Ä?R º½¤1Ä?"ÁT3k)Ä?±ˆa‡1éÃ?TýJçóÄ?¹8*7QKÅ?¹6TŒó7Å?IM»˜fºÅ?ÓNÍåCÅ?,ñ€²)WÄ?ç9"ߥÔÃ?Ùî@�òÄ?cÓJ!�KÄ?½ãÉåÃ?o+½6+Å?©¾ó‹Ä?1ïq¦ ÛÃ?ë©ÕWWÄ?JÔ >ÍÉÃ?Ð�`ÿunÄ?ø‹Ù’UÄ?n÷rŸÄ?\ÊùbïÅÃ?QLÞ3ßÃ?ümO�ØîÄ?\>’’†Ä?üÄôûÄ?­¤ßPøÄ?‘}�eÁÄÃ?<õHƒÛÚÄ?B@¾„ Ä?Ûü¿êÈ‘Ä?ßú°Þ¨Ä?vÂKpêÃ?>ê¯WXpÃ?šìŸ§ƒÄ?Q‚þB�Å?st´ª%Ã?0 ÃGÄ”Â?Ñ’ÇÓòÃ?8‡kµ‡½Â?»DõÖÀÂ?ûY,EÂ?*ÖT…Ã?)–[Z ‰Ã?âÊÙ;Ã?<õHƒÛÚÄ?wþEÐÄ?†K®bÅ?÷è ÷‘[Ã?×lå%ÿ“Ã?~÷æ7LÂ?Hmâä~Ã?ÚV³ÎøÆ?£°‹¢>È?âàI —Ç?ÁtZ·AíÇ?ì†m‹2Æ?h”.ýKRÇ?3‰zÁ§Å?&�Ñ:ªšÆ? ÆÁ¥cÎÅ?õœô¾ñµÅ?Z�!«[=Å?ÆíñB:Æ?eýfbºÇ?k'JB"Ç?Í#0ðÜÇ?ð3.ÉÆ?¥-®ñ™ìÅ?UL¥ŸpvÅ?2tì Æ?Î5ÌÐx"Æ?3Mg'ƒÅ?ä.ÂåÒÆ?’®™|³ÍÅ?G=D£;ˆÅ?(ðN>=¶Å?]lZ)rÅ?DiâÆ?4õºE`¬Å?Ÿ��·±Å?·_>Y1\Å?jÛ0 ‚Å?"þaK�¦Æ?«zù�&3Æ?vß1<ö³Æ?%²²,˜Æ?Ù—l<ØbÅ?]¿`7l[Æ?“âã²Å?޲~31]Æ?9 {Úá¯Å?L¤4›ÇaÄ?, ü¨†ýÄ?LÃð1%Æ?‹�yqÈÆ?Ò4(š°Ä?†§WÊ2Ä?§z2ÿè›Ä?ÖŽâutÄ?tBè KÄ?®+f„·Ã?vÄ!HÅ?œˆ~mýÄ?h$B#ظÄ?HG¬Å§Æ?çÿUGŽtÆ?>"¦DÇ?2WÕÅ?;QiÅ?1[²*ÂÃ?ÓÁú?‡ùÄ?íôƒºH¡È?{Ý"0Ö7Ê?/áÐ[<¼É?©‰>eÄÉ?EóùõÇ?(~Œ¹k É?q:ÉVÇ?uÄ]½ŠÈ?—㈞”Ç?Ð}9³]Ç?¡fHÅ«Æ?íÕÇCßÇ?ðHPüÈ?Å�1w-É?DiâÊ?üÿ8aÂhÈ?ënžê�›Ç?«ÏÕVì/Ç?q<ŸõfÈ?‡Þâá=È?äiù�«<Ç?÷ʼUסÈ?ý,œ¤Ç?¿+‚ÿ­dÇ?¯™|³Í�Ç?ä²ó66Ç?ÈzjõÕÇ?ðMÓg\Ç?d±M*kÇ?h±ÉWÇ?$��‘—5Ç?Ô‚}iÈ?”‚UõòÇ?ç«äcw�È?ë8~¨4bÈ?‹LÃðÇ?È?Z/†r¢]Ç?lÎÁ3¡IÈ?TÇ*¥gzÇ?>ÍÉ‹LÀÅ?ŸZ}uU Æ?QôÀÇ`ÅÇ?…@.qä�È? è…;FÆ?Õ"¢˜¼Å?jÂö“1>Æ?Þ3ßÁOÆ?™dä,ìÅ?öµ.5B?Å?¶jׄ´ÆÆ?Úʢ°‹Æ?ip[[x^Æ?™·ê:TSÈ?àLLbõÇ?Iô2Šå–È?÷x!ÂÆ?RóUò±»Æ?+TTýJÅ?t&mªÆ?Dý.lÍVÊ?,óV]‡jÌ?JaÞãLÌ?â#bJ$ÑË?é� ¸çùÉ?F^ÖÄË?á ½þ$>É?P”i4¹Ê?*qãŠÉ?)³ 0,É?Ðïû7/NÈ?ÔdÆÛJ¯É?JíE´Ë?MÖ¨‡htË?A~6rÝ”Ì?H¾�DÊ?Ñy�]¢zÉ?íbšé^'É?÷ŽbÊ?s ßûÊ?fÚþ••&É?-¤ý°Ê?¬;Û¤¢É?{mÇÔ]É?ý‡ôÛ×�É?‘·\ýØ$É?”1>Ì^¶É?MŸp]1É?ìhêwaÉ?Øc"¥ÙÈ?‰•ÑÈçÉ?{ØœƒgÊ?—¡Ÿ©×É?I»ÑÇ|Ê?ò'*ÖTÊ?ÌÐx"ˆóÈ?³_wºóÄÉ?£h[Í:É?Ø»?Þ«VÊ?œnÙ!þaÉ?ë9é}ãkÇ?îBs�FZÈ?Ön»Ð\§É?TrNì¡}Ê?Àv0bŸÈ?,GÈ@žÇ?¤Q0c È?Èyÿ'LÈ? Й´©ºÇ?/m8, üÆ?¯ÍÆJ̳È?Ê?i¨QH2«É?"Ã*ÞÈ<Ê?'øŠn½Ê?H‰]ÛÛÉ?UDÝ É?M¾ÙæÆôÊ?ŒƒKÇœgÊ?PÅÈ’9Ê?Q¾ …ŒÌ?@1²dŽåË?äM~‹N–Ì?øˆ˜IôÊ?ѯ­Ÿþ³Ê?~Œ‰BÉ?Ð(]ú—¤Ê?söÎh«’Î?;äf¸ŸÐ?\’v5yÐ?óT‡Ü 7Ð?¦&ÁÒ¨Î?«®C5%YÏ?Ü�:åÑÍ?2åCP5zÏ?¦òz0)Î?â¶ôhªÍ?.Ui‹k|Ì?4„c–= Î?Çž=—©Ï?ñòt®(%Ð? |E·^ÓÐ?ó�èžu�Î?ºÙ(·íÍ?Õ"¢˜¼Í?½8ñÕŽâÎ?V ÂÜîåÎ?ǂ L£Í?àØ³ç25Ï?Ïh«’È>Î?÷ZÐ{cÎ?©lXSYÎ?ÐÓ€AÒ§Í?†!YÀÎ?Eb‚¾…Í?L4HÁSÈÍ?Ö¬3¾/.Í?�¨ŒÍ?Üóüi£Î?DiâÎ?ß,Õ¼Î?c ¬ãø¡Î? ú'¸XÍ?QúBÈyÿÍ?Xãl:¸Í?Ìz1”íÎ?|)êÍÌ?ãßg\8Î?‰²·”óÅÎ?Ñ$±¤Ü}Ì?½åêÇ&ùË?`#I®€Ì?]‰@õ"Í?zTüßÌ?åòÒo_Ë?NzßøÚ3Í?™Þ„€Ì?( ß÷oÌ?ž)t^c—Î?;ÈzjõÍ?´°§þšÎ?Cs�FZ*Í?ò ú'¸Ì?…?Û5xË?B_zûsÑÌ?ƒŠª_é|Ð?Zóã/-êÑ?$·&Ý–ÈÑ?%¯Î1 {Ñ?ü‹ 1“¨Ð?7á^™·êÐ?k›âqQ-Ð?YLl>® Ñ?®€¸«WÐ?ƒOsò"Ð?ÙZ_$´åÎ?¨(ðN>Ð?`=î[­Ñ?R<¾½kÑ?“V-Ò?·bÙ=yÐ?í¸áwÓ-Ð?q©;Ð?ê@ÖS«¯Ð?+Qö–r¾Ð?Á¨¤N@Ð?°­Ÿþ³æÐ?hA(ïãhÐ?‚Uõò;MÐ?¾Û¼qRÐ?Ñ@,›9$Ð?’?xî=Ð?xòé±-Ð?ººc±M*Ð?=Fyæå°Ï?\Uö]üÏ?»{€îË™Ð?+Àw›7NÐ?éd©õ~£Ð?M„ O¯”Ð?ÙëÝïÏ?—Tm7Á7Ð?ˆ»zÐ?6\�-ËÐ?Ì:“6Ð?O�?üüÍ?‘z6«>Ï?ޱ^‚SÐ?¡fHÅ«Ð?š{HøÞßÎ?ž#ò]J]Î?$¸‘²EÒÎ?•+¼ËE|Ï?Iò\߇ƒÎ?:”¡*¦ÒÍ? 6ªÓ�¬Ï?ææÑ=ëÎ?úcZ›ÆöÎ?�[wóT‡Ð?à‚lY¾.Ð?¸>¬7j…Ð?_&Š�º�Ï?8IóÇ´6Ï?%–”»ÏñÍ?÷êã¡ïnÏ?ÕÎ0µ¥Ò?xak¶ò’Ó?LûæþêqÓ?€E~ýÓ?ôˆÑs ]Ò?–~TÃ~Ò?ðˆ ÕÍÅÑ?r£ÈZC©Ò?òB:<„ñÑ?ü‹ 1“¨Ñ?ÍV^ò?ùÐ?辜ٮÐÑ?PÂLÛ¿²Ò?Ñ>VðÛÓ?RÑXû;ÛÓ?‡4*p² Ò?Ä%Ç�ÒÁÑ?¥÷�¯=³Ñ?¦B</OÒ?­‰¾¢[Ò?gòÍ67¦Ñ?7ú˜tÒ?Çkñ)Ò?Š�º�}åÑ?-`·îæÑ?—‘zOå´Ñ?õôøÃÏÑ?+¢&ú|”Ñ?RóUò±»Ñ?Ý}Ž�gÑ?£>É6‘Ñ?@½5_%Ò?=)“ÚÑ?é(³ 0Ò?ì1‘ÒlÒ?ô7¡‡Ñ?ê—ˆ·Î¿Ñ?»Ò2Rï©Ñ?èÚÐ wÒ?h%­ø†ÂÑ?o¹ú±I~Ð?‰–<ž–Ñ?£ x|{×Ñ?ù*8Ò?ƒL2röÐ?'ÁÒ¨ÀÐ?Ù²|]†ÿÐ?™ ÇóPÑ?ñ[z4ÕÐ?w.Œô¢vÐ?6l±ÛgÑ?Â¥cÎ3öÐ?µ“ýØ? L§uÔØ?”i4¹Ù? â;þ Ú?.È–åëØ?¹8*7QKØ? 5?þÒ¢Ù?±‰Ì\àòØ?°âÊØ?-Ðî�b€Ú?“‹1°ŽãÙ?$Ò6þDeÚ?,*ât’­Ù? cîZB>Ù?geû�·\Ø?ãŽ7ù-:Ù?„€| Ý?&Ãñ|Ôß?>ÏŸ6ªÓß?B]¡Þ?3âÐ(]Ý?gÓÀÍÝ?¡¾eN—ÅÜ?zm6VbžÞ?þ�rÛ¾GÝ?‰~mýôŸÜ?ÑvLÝ•]Û?>”hÉÜ?$�›:�Þ?¼t“Vß?. ´¾Là?n¤l‘´Ý? †7kðÜ?«w¸Ý?–çÁÝY»Ý?6<½R–!Þ?.;Ä?léÜ?tµûËîÝ?7S!‰—Ý?C«“3wÝ?Y†8ÖÅmÝ?,œ¤ùÜ?ûY,EÝ?Ç/¼’ä¹Ü?;ÿvÙ¯;Ý?IœQ}Ü?7R¶HÚÜ?ÓL÷:©Ý?P0žACÝ?¤rµ4·Ý?”„DÚÆŸÝ?qxµÜ™Ü?Ï, PSËÜ?Yk(µÑÜ?vp°71$Þ?:3PÿÜ?DÀ!T©ÙÚ?EÕ¯t><Ü?ÁÉ6pêÜ?U†q7ˆÝ? 34žâÛ?\sGÿ˵Û?®€B=}Ü?Àϸp $Ý?ž #½¨ÝÛ?}<ôÝ­,Û?¹oµN\ŽÜ?Ó¼ãÉÛ? $ ˜À­Û?G>¯xê‘Ý?Ÿ9ëSŽÉÜ?ý‚ݰmQÝ?´8c˜´Ü?Ù š–XÜ?öE™ 2Û?ž™`8×0Ü?| €ñ ß?�Ñ:ªš á?hwH1@"á?�¾IÓ hà?þî5&ÄÞ?I×L¾Ùæß?ú¸6TŒóÞ?¹4J—~à?þ oÖà}ß? JÑʽÞ?þEИIÝ?—Æ/¼’äÞ?×¥Fègjà?ÕÊ„_êçà?‰~mýôŸá?ÐÒl#ß?8ÀÌwðß?D§çÝXPß?§Z ³ÐÎß?=HO‘CDà?ëW:ž%ß?d‘&Þà? (ÔÓGàß?qËGRÒÃß? ÂP¨§ß?¡Ö4ï8Eß?9}=_³\ß?ßÄ�œLÜÞ?üTˆeß? vöE™Þ? ®¹£ÿÞ?ÀA{õñÐß?‹RB°ª^ß?4Õ“ùGßß?ù‚0ºß?¤¥òv„ÓÞ?Nì¡}¬àÞ?(*ÖTß?N&nÄ@à?“EÖJß?�N]ù,ÏÜ?ñÖù·Ë~Þ?Ü�µÛ.4ß?¾Þýñ^µß?”i4¹Þ?;‡ú]ØÝ?p–’å$Þ?IZÖýcß?z¦—ËôÝ?1Ò‹Úý*Ý?ÿÍ‹_íÞ?‚Ç·w úÝ?†!YÀÞ?ú´ŠþÐÌß?TVÓõD×Þ?Y†8ÖÅmß?†Xý†ß?ãósCSÞ?"Ä•³wÝ?DÛ1uWvÞ?-´sšÚß?LÃð±á? ý\¬á?�,µÞïà?ïâý¸ýòÞ?6sHj¡dà?/lÍV^òß?(Óhr1á?aq8ó«9à?ª ãn­ß?ÌÔ$xCÞ?¢DKOËß?=dʇ êà?Å­já?“¬ÃÑU:â?^óªÎjà?RíÓñ˜à?ëÄåx"à?Ö5ZôPà?d–= lÎà?» ”Xà?¡÷Æœà?·`©.àeà?kb�¯èVà?;nøÝtKà?}ÍrÙèà?µ5"à?Š)x ¹ß?þÒ¢>Éà?üU€ï6oß?X;ŠsÔÑß?:¯±KTà?2>Ì^¶à?5B?S¯[à?O›sðLà?õôøÃÏß?&åîs|´ß?ïãhެüß?û‘ 9¶à?>±N•ïà?öEB[Î¥Ý?»¹øÛž ß?[•DöAà?Ðïû7/Nà?Ú–?ßÞ?oFÍWÉÇÞ?Š?Š:sß?†1zn!à?Ù¯;ÝyâÞ?wIœQÞ?-]Á6âÉß?,)wŸã£Þ?É=]ݱØÞ?IŸVQà?n‰\pß?pënžêà?HPüs×ß?4,F]kïÞ?Äê�0 XÞ?JíE´Sß?¿ÔÏ›ŠTß?¨QH2«wá?˜Ü(²ÖPá?öµ.5B¿à?þî5&ÄÝ?KXc'à?'3ÞVzmß?rúz¾f¹à?ÂøiÜß?€ï6oœß?�¸4J—Ý?¨(ðN>ß? ]Þœà?Qèyá?6ZôPÛá?2çû’�ß?mV}®¶bß?\qqTn¢ß? üáç¿à?~Œ¹k yà?4w¼Éoß?S<.ªEDà?¿ [³•à?—ÊÛN à?Ó×øLöß?hvÝ[‘˜ß?ê^'õeiß? ]Þß?žÒÁú?‡ß?¤P¾¾ÖÞ?%Ì´ý++ß?¤‹M+à?\•›¨¥ß?RD†U¼à?m±Ÿà?³˜Ø|\ß?¿ò =Eß?š`8×0Cß?ÁÆõïúLà?š–X�|ß?ÇeÜÔ@óÜ?óX32È]Þ?×kzPPŠß?&6׆ à?Öà}U.TÞ?”Kã^IÞ?ší }°ŒÞ?겘Ø|\ß?3�&c`Þ?8�é´nƒÝ?/÷ÉQ€(ß?OË\å Þ?kž#ò]JÞ?an÷rŸà?�d=µúÞ?pënžê�ß?…`U½üNß?¡Ø š–XÞ?ëR#ô3õÝ?ožê�›áÞ?ÖÈ®´ŒÔÝ? ]lZ)„à?®›R^+!à?î#·&Ýß?� G�J±Û?ÙÎ÷Sã¥Þ?aþ ™+ƒÝ?bMeQØEß?75Ð|ÎÝÝ?7n1?7Ý?sÚSrNìÛ?;5— uÝ?÷9>Zœ1ß?jßÜ_=îß?óŽSt$—à?[yÉÿäïÝ?«\¨ükyÝ?Õ=²¹jžÝ?XSYvQÞ?ÊÂ×׺ÔÞ?Ý µ‰“Ý?¼z�Þ?À&kÔC4Þ?AºØ´RÞ?³–ÒþÞ?U¤ÂØB�Ý?ƒѯ­ŸÝ?,ŸåypwÝ?Ç/¼’ä¹Ý?óv„Ó‚Ý?¦Óº jÝ?ˆôÛ×�sÞ?AeüûŒ Þ?¶ºœ“Þ?¬á"÷tuÞ?æv/÷ÉQÝ?6x_• •Ý?CV·zNzÝ?­®€¸Þ?ìK6l±Ý?QÝ\ümOÛ?�jØï‰uÜ?×L¾ÙæÆÝ?Y1\qÞ?H¾�DÜ?apÍý/Ü?´Yõ¹ÚŠÜ?µ¥òz0Ý?Ñtv28JÜ?#„GG¬Û?É&pënÝ?:Ì—`Ü?®ð.ñ�Ü?ÚUHùIµÞ?uU ƒ‡Ý?î^î“£Þ?]¿ðJ’Ý?»ÕsÒûÆÜ?IÛø• Ü?÷<Ú¨NÝ?mŽs›p¯Û?ømˆñšWÞ?K�PáÝ?ØG§®|–Ý? `­Ú5!Ù?œÁß/fKÜ?-Z€¶Õ¬Ú?bJ$ÑË(Ü?órØ}ÇðÚ?Útp³xÚ?·bÙ=yÙ?¤P¾¾ÖÚ?¦B</OÜ?�¢ÎÜCÂÜ?å*¿)¬Ý?Ef.py¬Û?U†q7ˆÖÚ? 'LÍÊÚ?yËÕ�MòÛ?Œi¦{�ÔÛ?ú™zÝ"Û?ÊÅXÇñÛ?9 ¥/„œÛ?�]¢zk`Û?Ö:q9^�Û?s‚69|ÒÚ?åîs|´8Û?eª`TR'Û?lyåzÛLÛ?æ²Ñ9?ÅÚ?"� œlÛ?i©÷TNÜ?Ó×øLöÛ? eýfbÜ?Õ°ßëTÜ?ó‘”ô0´Ú?²KTo lÛ?Yk(µÑÚ?Sz¦—ËÛ?‡Ü 7àóÚ?YÃEîéêØ?³ ×ÜÑÙ?¦ ±ú# Û?úBÈyÿÜ?‘|%�»Ù?C®Ô³ ”Ù?&o€™ïàÙ?/ùŸüÝ;Ú?ȳ˷>¬Ù?QØEÑÙ?¿œ3¢´Ú?ò™ìŸ§Ú?mrø¤Ú?R˜÷8Ó„Ü?y²›ýhÛ?0óüÄÜ?]S ³³èÚ?Cª(^emÚ?­5”Ú‹hÙ?ÍXä×Ú?£Ó0|DÙ?�`ãúw}Û?BZcÐ ¡Ù?¡¢êW:Û?%ZòxZ~Ö?”‡…ZÓ¼Ù?óŽSt$—×?³¶)ÕØ?ÑËØÐÍ×?¢ U1•~×?-]Á6âÉÖ?XøQ û×?¸\ýØ$?Ù?'0�ÖmÙ?Œô¢v¿ Ú?333333Ù?2 {½ûã×?î'c|˜½×?]j„~¦^Ù?%²²,˜Ø?XU/¿ÓdØ?`;±OÙ?`�™D½Ø?ª*4ËfØ?½f¾ƒŸØ?-Ó¾¹×?_y�ž"‡Ø?»¶·[’Ø?Ü‚¥º€—Ø?aºÙØ? 4ØÔyTØ?áÐ[<¼çÙ?Ÿ`<ƒ†Ù?KÊÝçøÙ?0ðÜ{¸äÙ?ù.¥.Ç×?ëTùž‘Ù?­nõœô¾×?(F–̱Ø?¤ü¤Ú§ã×?­„î’8+Ö?#»Ò2RïÖ?Ñéy7Ø?µŒÔ{*§Ù?b¼æU�ÕÖ?›uÆ÷Å¥Ö?…|гYõÖ?!å'Õ>×?{÷Ç{ÕÊÖ?–$Ïõ}8Ö?Ív…>XÆ×?» ”X×?/£Xni5×?–B —8òÙ?ŸÉþyÙ?ø§T‰²Ù?dÎ3ö%Ø?5~á•$Ï×?{g´UIdÖ?ÕyTüߨ?mÇÔ]Ù×?c˜´ÉáØ?ZÊû8šÖ?75Ð|ÎØ?‹ßV*Ô?œ'¾ÚQ×?Z›ÆöZÐÔ?NA~6rÝÕ?›¯’�ÝÕ?Ó¼ãÉÔ?Ýyâ9[@Ô?šë4ÒRÕ?»œ“pÖ?Kw×Ù�Ö?“‹1°ŽãÖ?¹ü‡ôÛ×Ö?Ä "RÓ.Õ?$—ÿ�~ûÔ?ö~£7üÖ?ëÅPN´«Õ?¯&OYM×Õ?ÖT…]Ö?ÞË}r Ö?•·#œ¼Õ?Ä\RµÝÖ?¸Î¿]öëÔ?kÕ® Ö?ƿϸp Ö?r4GV~Ö?a2U0*©Õ?E›ãÜ&ÜÕ?Û5x_•×?°;Ýyâ9×?•µMñ¸¨×?u¬Rz¦—×?«x#óÈÕ?¥N@aÃÖ?ÞtËñÕ?ŒgÐÐ?ÁÕ?£’°o'Õ?ƧÏ Ó?Ház®GÔ?· ÍuiÕ?Ïg@½5×?Kê46Ô?Žx²›ýÓ?|¹OŽDÔ?ó¬¤ßPÔ?ìÜ´§!Ô?ŠZš[!¬Ó?‹üú!6Õ? ®¹£ÿÔ?Ø�î<ñœÔ?bÚ7÷W�×?¢ÑÄÎÖ? ByGs×?–Zï7ÚqÕ?É&pënÕ?ÿ–üSªÓ?ûå¶}�Õ?h’XRî>Õ?“6U÷ÈæÖ?lÑ´­fÔ?ŽÍŽTßùÖ?²öw¶GoÒ?SìhêwÕ?é)rˆ¸Ò?Õ•Ïò<¸Ó?±3…ÎkìÒ?a4+Û‡¼Ò?q:ÉV—SÒ?óùõCÓ?îBs�FZÔ?ºW•}WÔ?§$ëpt•Ô?å¶}�úÔ?>\rÜ)Ó?¸#œ¼èÒ?ÓJ!�KÕ?ìlÈ?3ˆÓ?ýÁÀsïáÓ?9&‹û�LÔ?£çºÔ?:�õÔê«Ó?ãÝ‘±ÚüÓ?1îÑZÑÒ?vÄ!HÔ?Ë‚‰?Š:Ô?ÿ¯:r¤3Ô?¦~ÞT¤ÂÓ?Ýì”ÛöÓ?p]1#¼Õ?<Þä·èdÕ?%è/ôˆÑÕ?E½àÓœ¼Õ?Üg•™ÒúÒ?g+/ùŸüÔ?ˆdȱõ Ó?ØòÊõ¶™Ó?MÀ¯‘$Ó?‹ßV*¨Ñ?¥hå^`VÒ?›�ÖtBÓ?1`ÉU,~Õ?Ù”+¼ËEÒ?t¶€ÐzøÑ?NÒü1­MÒ?„¸röÎhÒ?2•ñï3Ò?SÎ{/¾Ñ?Ë+×Ûf*Ó?9œùÕ Ó?†1zn¡Ò?¢Busñ·Õ?€šZ¶ÖÕ?Grùé·Õ?™Iô2ŠÓ?[éµÙX‰Ó?T¨n.þ¶Ñ?aª™µ�Ó?9c˜´ÉÓ?paÝxwdÕ?‹Ã™_ÍÒ?MØ~2ƇÕ?ÈDJ³yÑ?Úã…txÔ?T�4¸­-Ñ?†§WÊ2Ò?äRìhÑ?{Ý"0Ö7Ñ?!¬ÆÖÆÐ?¯xê‘·Ñ?­nõœô¾Ò?ÀA{õñÐÒ?®ÕöBÓ?+ö—Ý“‡Ó?x �ý,–Ñ?aûÉfÑ?»Ò2Rï©Ó?¯½7†Ò?j£:ÈzÒ?²×»?ÞÒ?&9`W“§Ò?œQ}>Ò?�¸4J—Ò?¿ÔÏ›ŠTÑ?7ÿ¯:r¤Ò?„H†[ÏÒ?Ž¿·éÏÒ?5B?S¯[Ò?)Bêvö•Ò?ÉU,~SXÔ?ÁãÛ»Ô?ÙCûXÁoÔ?FzQ»_Ô?¿3‰Ñ?A}Ëœ.‹Ó?Lÿ’T¦˜Ñ?ÒŦ•B Ò?ZôPÛ†Ñ?35 Þ�FÐ?ýI|îûÐ?eqÿ‘éÐÑ?ƒ÷U¹PùÓ?pÍý/×Ð?—á?Ý@�Ð?RóUò±»Ð?µûU€ïÐ?‚åȳÐ?˃ô9DÐ?ú´ŠþÐÌÑ?–“PúBÈÑ?Ò?ŠÊ†5•EÐ?Ct 4Ò? â8ðj¹Ò?ÿ±GÔ?Û…æ:�´Ñ?4iSu�lÔ?ˆWÎÞÐ?Ž!8öìÒ?½nëÐ?ä.ÂÑ?}ëÃz£VÐ?§çÝXPÐ?™b‚ŽVÏ?fj¼!�Ð?mŒ�ðœÑ?Ržy9ì¾Ñ?“�[ìöÑ?²ºÕsÒ?PŒ,™cyÐ?‹¦³“ÁQÐ?û®þ·’Ò?¸xxÏ�åÐ?Rºô/IeÑ?¦ ÐÒÑ?Ž={.S“Ñ?–x@Ù”+Ñ?%ZòxZ~Ñ?†9A›>Ð?ɰŠ72�Ñ?�—nƒÀÑ?5ÒRy;ÂÑ?î\éEÑ?”¢•{�Ñ?±k{»%9Ó?~6rÝ”òÒ?Xþ|[°TÓ?–Ïò<¸;Ó?QÚ|aÐ?m«Yg|Ò?jøÖ�wÐ?WêYÊûÐ?T1³ÏcÐ?{¡€í`Î?]ýØ$?âÏ?œ¦Ï¸®Ð?éFXTÄéÒ?{L¤4›Ï?ï%ÀÎ?Uø3¼YÏ?øÝtËñÏ?µ©ºG6WÏ?%«êåwÎ?Ð+žz¤ÁÐ?en¾ݳÐ?ˆ-y<-Ð?Ç›ü�,Ó?&9`W“§Ò?ÌoB@Ó?¡÷ÆÑ?E�>‘'Ñ? !çýœÎ?,»`pÍÑ?l#ö  Ñ?'Ø�›6Ó?�Ky ²Ð?Z.�óSÓ?D÷¬k´Î?31]ˆÕÑ?*á ½þ$Î?¬Žé Ð?UûtŽÊ?Õ’weÊ?µ4·BXÉ?�ûVëÄåÎ?¿ïß¼8ñÍ?,¨þA$Ï?}è‚ú–9Ë?„ƒ½‰!9Ë?0ÈбÇ?Q.�_x%Ë?w,¶IEcË? š]÷VÎ? eýfbÊ?àV*¨¨Î?¢*¦ÒO8Ç?DjÚÅ4ÓË?¥.ÇHöÆ?Œ�ðœúÈ?¯C5%Y‡Ç?b.äÜÆ?"ÿÌ >°Å?uU ƒ‡Ç?uÞɧÉ?~įXÃEÊ?((E+÷Ë?"‰^F±ÜÊ?-{ØœƒÇ?0×¢h[Ç?ñI'L5Ë?éµÙX‰yÈ?&ûçiÀ É?ÀÐ#FÏ-Ê?üߪ›É?öÒNïÈ?š•íCÞrÉ?DP5z5@Ç?Äëú»aÉ?‰Ð6®É?s.ÅUeßÉ?aãúw}æÈ?EÙ[ÊùbÉ? ˜£ÇïmÌ?^*6æuÄË?ØH„+ Ì?iÆ¢éìdÌ?¦¶ÔA^Ç?C’Y½ÃíÊ?ÐѪ–t”Ç?Š® ?8ŸÈ?iÇ ¿›nÇ?®ÙÊKþ'Å?6ÊúÍÄÆ?R)v4õÇ?¦^·ŒõË?‡Áü2WÆ?ûzáÎ…Å?G¢`ÆÆ?®�­,Ç?VfJëo Æ?þÕã¾Õ:Å?S\Uö]È?W`ÈêVÏÇ?Ü ‹Qׯ?࢓¥ÖûË?ømˆñšWË?6‘™ \Ì?íHõ�_”È?`áC‰–È?¹à þ~1Å?íDIH¤mÈ?âr¼Ñ“È?Ü,^, ‘Ë?Ó…XýÈ?ÊoÑÉRëË?¡MŸt"Å?‰A`åÐ"É?=E7§Ä?Ý´§!ªÆ?]‹ m5Å?Ze¦´þ–Ä?µ4·BXÃ?G¬Å§Å?w£�ù€@Ç?@Ý@�wòÇ?‘|%�»È?µüÀUž@È?˜¦pzÅ?»_ønóÄ?ûY,Eò•È?ó!¨½Æ?éc> ЙÆ?l dv½Ç?(,ñ€²)Ç?!u;ûʃÆ?·$ìjòÆ?Ôšæ§èÄ?7‹ CäÆ?ªžÌ?ú&Ç?Ô+eâXÇ?‚WË�™`Æ?TW>ËóàÆ?D¾K©KÆÉ?*8¼ "É?™Kª¶›àÉ? &áBÁÉ?6\äž®îÄ?Oyt#,*È?ðl�ÞpÅ?Äè¹…®DÆ?79|Ò‰Å?7‰A`åÐÂ?1%’èeÄ?^¡–±¡Å?è÷ý›É?mÆiˆ*üÃ?c('ÚUHÃ?ÂKpêÉÃ?ÖÇCßÝÊÄ?(ðN>=¶Ã?xEð¿•ìÂ?|ðÚ¥ ‡Å?Ww,¶IEÅ?â翯]Ä?x( ô‰<É?$0ðÜ{È?ë;¿(AÉ?ׄ´Æ Æ?ç�½ÞýÅ?K®bñ›ÂÂ?M JÑÊÅ?k :!tÐÅ?{ŸªB±È?B°ª^~§Å?zýI|îÉ?þžX§Ê÷Â?ß3¡lÆ?\8’LÂ?ÇHö5CÄ?Å_Ñ­×Â?Šuª|ÏHÂ?¨äœØCûÀ?RGÇÕÈ®Â?lµ‡½PÀÄ?äôõ|ÍrÅ?a¦í_YiÆ?Y32È]„Å?DàH Á¦Â?ö—Ý“‡…Â?â#bJ$ÑÅ?�I›ªÃ?Ä?Í[uª)Å?ß7¾öÌ’Ä?&¶ØíÃ?�| ^Ä?áëk]j„Â?#ô3õºEÄ?ò°PkšwÄ?ÊÂ×׺Ä?oñðžËÃ?4e§ÔEÄ?Ã+IžëûÆ?ÀYJ–“PÆ?Ú�‘aÇ?“Œœ…=íÆ?¸u7OuÂ?,F]kïSÅ?²+-#õžÂ?0fKVE¸Ã?Á�”-’Â?§>�¼sÀ?Õxé&1Â?CV¸å#Ã?Ê1YÜdÆ?‡D¤¦Á?qý¾óÀ?‹2d’‘Á?PãÞü†‰Â?¢_[?ýgÁ?ÓÜ a5–À?u;ûʃôÂ?¡õðe¢Â?“¬ÃÑUºÁ?ÀJÆ?A)Z¹˜Å?\qqTn¢Æ?ÜŸ‹†ŒGÃ?“Ã'�H0Ã?f…"ÝÏ)À?eû�·\ýÂ?F²G¨RÃ?À”�Æ?vÿXˆ�Ã?p –ê^Æ?õ€yÈ”Á?úbïÅíÃ?™.Äê�0À?Ë¢°‹¢Â?ùõCl°À?ÀÀ?Rðr¥ž½?ý…1zÀ?$0ðÜ{Â?ûÉf/Ã?;ªš ê>Ä?+4ËfÃ?K!�KyÀ?Äê�0 XÀ?Z/†r¢]Ã?øq4GV~Á?é Œ¼¬Á?.� ø1æÂ? æ¯�¹2Â?“ªí&ø¦Á?ÂQòêÂ?Ý{¸ä¸SÀ?±4ð£öÁ?o~ÃDƒÂ?`sž MÂ?,žz¤ÁmÁ?’°o'áÁ?6‘™ \Ä?B²€ ܺÃ?½ý¹hÈxÄ?~÷æ7LÄ?Ujö@+0À?Ÿ9ëSŽÉÂ?9+¢&ú|À?Øñ_ �Á?CUL¥ŸpÀ? 4ØÔy¼?‹6ǹM¸¿?eû�·\ýÀ?¸Ë~ÝéÎÃ?õKÄ[çß¾?òaö²í´½?Q¾ …Œ¾?Ûúé?k~À?׈`\:¾?qªµ0 í¼?hÌ$êŸÀ?S=™ôMÀ?÷ʼUס¾?ô66;R}Ã?”½¥œ/öÂ?ÊmûõÃ?ú·Ë~ÝéÀ?‰´�?QÙÀ?l¸ [–»?::ZÕ’À?`�‰”fóÀ?~q©J[\Ã?\‘˜ †oÁ?Nïâý¸Ã?\ŽW zR¾?íñB:<„Á?£’:M¼?Œi¦{�Ô¿?I¼<�+J½?*�n„EE¼?‰&PÄ"†¹?��‘—5±¼?"o¹ú±IÀ?·|$%= Á?bôÜBW"Â?1@¢ ±À?Âmmáy©¼?8øÂdª`¼?iàG5ì÷À?ó9w»^š¾?šÍã0˜¿¾?7§’ À?CpìÙ¿?py¬ä¾?ÅÚÇ ~¿??ÆÜµ„¼?�›:�Š¿?ïU+~©¿?”Àæ<À?j£:Èz¾?ÙëÝïU¿?EóùÁ?#KæXÞUÁ? ËŸo Â? î\éÁ?òÍ67¦'¼?l¯½7†À?_y�ž"‡¼?ä0˜¿Bæ¾?µßÚ‰’¼?ù÷„¸?.�_x%É»?§#€›Å‹½? áÑÆkÁ?Öª]Òº?ÌFçüǹ?Q¾¾Ö¥º?c}“½?~įXÃEº?vQôÀǸ?ØœƒgB“¼?j3NCTá»?³Dg™E(º?­NÎPÜñÀ?UOæ}“À?�ÁŠS­…Á?·³¯eĹ?4»î­¸?Åâ7…• ¶?7¢"N'¹?�Ky ²¼?Z€¶Õ¬3¾?r‰#DÀ?FzQ»_½?©kí}ª ¹?ƒi>"¦¸?´�€Ñåͽ?"5íbšéº?ÖµÂô½º?)ãÂ�¼?ioð…ÉT¹?‹Q×ÚûTµ?@ˆdȱõ´?SËÖú"¡¹?j½ßhÇ ·?ßlscz¶?†p̲'�¹?)ÍæqÌ·?U·?Ž…A™F»?hñ�ÿ¹?ò|Ô›Q»?%ÊÞRλ?ªc•Ò3½´?Ê7Ûܘž¸?š ñH¼<µ?¡ÚàDôk·?+*ÿZ^µ?–“PúBȱ?ÎOqxµ´?™„ y7¶? ÇóPoº?>u¬Rz¦³?ÖÇCßÝʲ?gEÔDŸ³?Ý@�wòéµ?ˆ�#�³?³aMeQر?Yá&£Ê´?¦(—Æ/´?åÐ"Ûù~²?.’v£�ù¸?Ð ¡ƒ.á¸?ÐECÆ£Tº?d[œ¥dµ?/ø4'/2µ?è3 ÞŒš¯?¶×‚ÞC´?î$"ü‹ µ?E/£Xni¹?¹Œ›h>·?œÞÅûqû¹?�€&†§³?Y¿™˜.Ķ?!YÀnݱ?ñº~Án´?ÇמY ²?áy©Ø˜×±?C«“3¯? ]‰@õ²?÷W�ûVë´?«Îj�=&¶?ÉüI‚¸?AJìÚÞnµ?™õb('Ú±?‘D/£Xn±?è1Ê3/‡µ?­‰¾¢[³?_ š]÷²?k�=&Ršµ?‹Áôoî³?Z)r‰#³?÷s ò³‘³?—;3Áp®±?+Û‡¼åê³?×÷á !ʳ?p–’å$´?ú_®E в?ÙvÚŒ³? ×ÜÑÿ¶?—¡Ÿ©×µ?¯°à~À·?]øÁùÔ±¶?ü¦°RAE±?pìÙs™š´?±§þš¬±?ÉW)±³?M»˜fº×±?kdWZFê­? ýHV±?tÓfœ†¨²?Œ�u?T¶?ølìM°?‡‡0~÷®?ÂÚ;á%°?M.ÆÀ:²?gš°ýdŒ¯?7n1?74­?žï§ÆK7±?±RAEÕ¯°?9}=_³\®?˾+‚ÿ­´?Ü~ùdÅ´?ެü2#¶?/ÛN[#‚±?´Ø€q±?ê%Æ2ý©?()°¦°?�µ¿³=²?�\âÈ‘µ?…$³z‡³?Î3ö%¶?“Úl@°?°�N]ù,³?@1²dŽ­?Ëõ¶™ ñ°?âvhXŒº®? ë©ÕWW­?߈îY×h©?ó<¸;k·­?†‘^ÔîW±?¿‚4cÑt²?ÚR �´?¨9y‘ ø±?‡À‘@ƒM­?Võò;Mf¬?Ç ¿›nÙ±?—Ž9ÏØ¯?ÎŒ~4œ2¯?üo%;6²?G�J±£q°?rjg˜ÚR¯?ÁÿV²c#°?ÏÚmšë¬? š]÷V°?÷@°?c^G²�°?ümO�Øî®?‡P¥f°? ÛOÆø0³?Ðb)’¯²?$G:#/³?iâàI ³?PV W@¬?6çà™Ð$±?…²ðõµ.­?’?xî=°?Z/†r¢]­?h%­ø†Â§?}Ê1Yܬ?š´©ºG6¯?UûtVð£?0Ø Ûe¦?»€—6Ê¢?õ0´:9C¡?õ¢v¿ ð¥?an÷rŸ¥?¤¥òv„Ó¢?ðQ½Â‚«?y@Ù”+¼«?™cyW=`®?·}�úë¦?õ-sº,&¦? `­Ú5!�?�ñaö²í¤?’릔×J¨?¿eN—ÅÄ®?ÎR²œ„ª?áaÚ7÷W¯?ðRê’qŒ¤?v“þ^ª?õ÷RxÐì¢?Û¦¶ÔA¦?Þ:ÿvÙ¯£?{jõÕU�¢?…y�3MØž?&¤à)ä¢?›!U¯²¦?6!­1脨?B•š=Ð ¬?Xtë5=(¨?„·! _¢?+*ÿZ^¡?üǙ&l§?UÂzýI¤?333333£?%ZòxZ~¨?õfÔ|•|¤?ºk ù g£?ØaLú{)¤?>w‚ý×¹¡?@j'÷£?~7ݲC¤?È}«uâr¤?Öª]Ò¢?l=C8fÙ£?ï9°!©?œß0Ñ §?¶Mñ¸¨©?zûsÑ�ñ¨?#‡ˆ›SÉ ?¤¨3÷�ð¥?…$³z‡Û¡?]¿`7l[¤?üs×ò¡?‰$zÅr›?ÔòWy¡?-íÔ\n0¤?èú>$D©?üǙ&lŸ?0ÕÌZ H›?ã‹öx!ž?1{ÙvÚ¡?zVÒŠo(œ? ߺñî˜?nøÝtË¡?Tn¢–æV ?Tœˆ~m�?­Mc{-è¥?U¯²¶)¦?E½àÓœ¼¨?ßj�¸¯ ?E½àÓœ¼ ?I¢—Q,·”?_(`;±Ÿ?ýÁÀsïá¢?‹¥H¾H©?»šrkÒ�?»Õ”dž?èLÚTÝ#›?ª»² ל?øÃÏ^£?žz¤Ámm¡?ëåwšÌx£?mÇÔ]Ù£?9ÖÅm4€—?3d’‘³ ?»ïû™?æ®òž?g �ר%š?ønóÆI‘?]¿ðJ’—?�œ¡¸ãMž?¤8GW£?£W”†•?‹72�üÁ�?—pè-Þ“?( µ¦y—?U[rP’?SAEÕ¯tŽ?(Õ>�˜?rÁüýb–?ÝAìL¡“?ìm3â‘ ?hÎú”c² ?ôÄs¶€Ð¢?ôS^-—?¯—¦pz—?í»"øßJ†?û\Äwb–?1í›û«Ç�?èy’tͤ?‰—§sE)¡?|*§=%ç¤?ª‚QI�€–?ð¦[vˆ ?K’çú>”?–vj.7š?çV«±„•?ýÁÀsïá’?¹à þ~1‹?–íCÞrõ“?4HÁSÈ•š?6׆Šqž?(º.üà|¢?$¶»辜?gEÔDŸ�’?óÊõ¶™ ‘?Doñðž›?Ä "RÓ.–?‘îçäg“?®dÇF ž?rÞÿÇ –?F&à×H”?(*ÖT•?ò@d‘&Þ‘?@Û5x_•?ÆÞ‹/Úã•?™(Bêvö•?�wòé±-“?áñí]ƒ¾”?¨ÄuŒ+.ž?ê<*þ?‹RB°ª^ž?±†‹ÜÓÕ�?V™)­¿%�?ó8 æ¯�™?1AG«Z’?]j„~¦^—?-Îæm’? |(ђǃ?9'öÐ>V�?†TQ¼ÊÚ–?æË °�ž?ñ+Öp‘{Š?§!ªðgxƒ?³\6:秈?ÞqŠŽäò�?0…Ì•A…?7ê°Â-?ì2ü§(�?L÷™�?XWj1xˆ?¨ƒ¤O«˜?|a2U0*™?‰&PÄ"†�?c%æYI+Ž?Y |E·Ž?C’Y½Ãíp?„.áÐ[<Œ?ïq¦ ÛO–?>&RšÍã ?�ñ}q©J›?‚:åÑ�° ?íïl�Þp�?¶»辜™?·zNzßøŠ?úîV–è,“?!v¦Ðy��?äôõ|Ír‰?Õ[[%X|?IeŠ9:Š?óâÄW;Š“?ì«an—?U¯²¶)ž?]5Ïù.•?9œùÕ ˆ?dT8‚„?Ww,¶IE“?…y�3MØŽ?+¾¡ðÙ:ˆ?ž^)Ë—?¦{�Ô—¥�?B`åÐ"Û‰? :!tÐ%Œ?PáR)†? Íui©Œ?AØ)V ÂŒ?+Nµf¡�?½nëˆ?ÕÏ›ŠT‹?�"1ì0–?ÎR²œ„’?8-xÑW�–?•›¨¥¹•?’ñ+Öp�?סš’¬Ã‘?C®Ô³ ”‡?穹n�?ü‹ 1“¨‡?>?Œml?â©iƒ?ìOâs'Ø�?dv½S—?D¿¶~úÏz?+û®þ·b?0�[wót?=Ú¨N‚?R臭ö”l?êÐéy7D?ެü2#‚?éd©õ~£}?YLl>® u?Êõ�î�?~RíÓñ˜‘?Ö�Mò#~•?‹T[r€?(š°È¯?+¢&ú|”a¿ ß÷o^|?ùõCl�?5$î±ô¡›?¸ê:TS’•?� ܺ›§š?ß‹/Úã…„? ™�žw“?w×Ù�f€?ÙÎ÷S㥋?åÑ�°¨ˆƒ?Ç,{Ø|?¿¶~úÏš_?Í‚9zü~?sJ@LÂ…Œ?¹ü‡ôÛב?T;ÃÔ–:˜?_Ÿ;ÁŽ?7+1ÏJz?ìi‡¿&kt?'jin…°Š?ï<ñœ- „?ض(³A&y?Òo_Α?X;ŠsÔÑ�?5&Ä\Rµ}?¨©ek}‘€??¨‹ÊÂw?"ýöuàœ�?³z‡Û¡a�?†àسç‚?Uð2ÃFy?{g´UId?'ú|”�?‹Â.Šøˆ?{ò%T�?óŽSt$—�?{g´UIdo?!æ’ªí&ˆ?lìÕ[{?¯xê‘·…?ݵ„|гy?zUgµÀS¿²ºÕsr? ‹Q×Úû„?ÔòWy‘?�Ž«‘]iY?€Õ‘#��a¿]éEí~¿€(˜1kl?ýÙ�‘aU¿%Ί¨‰>o¿d’‘³°§m?’xy:W”b??«Ì”Ößò>nü‰Ê†5…?±0DN_χ?hêu‹ÀX�?�í`Ä>d?àœ¥½Ág?´®Ñr ‡z¿5¸­-�Qn?œŠT[‚?Ó–x@Ùt?>u¬Rz¦g?léÑTOæ_¿;s ßûk?ë˜Ü(‚?Q¤û9ù‰?¡Ÿ©×-“?ÐGqh„?ßlsczÂb?¿ò =EQ?uŽÙëÝ? ¨lXSYt?ý»>sÖ§\?eȱõ áˆ?bMeQØEq?-y<-?pe?|µ£8Gm?­ûÇBt\? Ž’Wçp?�¾IÓ hn?JÑʽÀ¬p?Vbž•´â[?Èyÿ'Lh?@öz÷Ç{…?O#-•·#|?¾‰!9™¸…?~sõ¸o…?a2U0*©C¿Öª]Òz?ƒú–9]c?(¸XQƒix?Mjh°a?Ot]øÁùt¿.2¥B?æÎL0œkx?‡jJ²G‡?gd�»Sd¿rÝ”òZ }¿·_>Y1\m¿¼êó�)O¿½VBwIœu¿&6׆Š�¿ì¿ÎM›qJ¿“ŠÆÚßÙ^¿?ÆÜµ„|p¿Ä•³wF[u?Íù†z?’]i©÷„?r„Ѭl_¿]Þ®Õ^¿Kiÿ¬…¿û®þ·’m¿ãÁ»}Vy?ˆž”I m�?­k´衆?ê—ˆ·Î¿�?Ǻ¸�ðV?~p>u¬‚?f½ʉvE¿#0Ö70¹q?Î4aûÉO?}!ä¼ÿ�S¿órØ}Çðx¿P¨§�ÀO¿È#¸‘²Er?ÁÿV²c#€?–x@Ù”+Œ?LàÖÝ¿ ì1‘Òl^¿«ZÒQfS¿¼è+H3f¿³B‘îçD¿Mjh°Q¿ü©ñÒMb@¿Šè÷ý›g¿Ä_“5ê!Z¿•�~P)t?n…°KX[?½VBwIœu? IJ™Cr?òB:<„ñs¿/n£¼b?a2U0*©c¿'jin…°Z?CÁ�”-b¿ŠsÔÑq5‚¿�¾IÓ hn¿A ]Þ\?ض(³A&y?ÐDØðôJy¿ŽvÜð»é†¿QiÄÌ>��¿V›ÿW9r¿›Ì^†¿�¾IÓ hŽ¿0…Ì•A…¿È^ïþx¯z¿ Òo_Îi¿£V˜¾×\?ªek}‘Ðv¿sµ4·Bˆ¿êìdp”¼Š¿t—ÄY5�¿šÏ¹ÛõÒ„¿7mÆiˆ*Œ¿8N ógj¿Cá³up°‡¿PqxµÜ‰¿ègêu‹Àˆ¿�ÁŠS­…‰¿ºÙ(‡¿=Õ!7à ˆ¿Çº¸�ð†¿`®E Ð¶Š¿Š�OÈÎÛˆ¿ÂÞÄ�œL|¿Ot]øÁù„¿/ܹ0Ò‹z¿_ÔîW¾{¿Lo.2Ž¿°® Ôbð€¿Qù×òÊõ†¿WÎÞmU‚¿À¯]Úpˆ¿�ëŠáí‘¿ 4ØÔ‰¿pÐ^}<ô}¿¬r¡ò¯åu¿¢ †7k�¿Ù[Êùbï•¿­C9Ñ®’¿0Ùx°Ån�¿&¶Øí“¿^×/Ø Û–¿Ÿ[èJª�¿ÂP¨§��¿›:�Šÿ;’¿ÖþÎöè ‡¿'õei§æ‚¿-@ÛjÖy¿dÉ˻ꑿ­mŽs›�¿o l•`q˜¿>xí҆Ò¿@Þ«V&ü‚¿!¯“âãS¿øí¸áws¿gÕçj+ög¿kóÿª#GŠ¿’Z(™œÚy¿ßŒš¯’��¿´äñ´üÀ…¿¨68ýÚŠ¿–]0¸æŽŽ¿”i4¹“¿U¯²¶)Ž¿— uXᆿÂ26t³?€¿ö²í´5"h¿™œÚ¦¶„¿w×Ù�f�¿•¸ŽqÅÅ‘¿€ÖüøK‹Š¿Á7MŸp�¿eÂ/õó¦’¿k¶ò’ÿÉ¿—⪲ïŠ�¿Ë;Ū‘¿�£9²òË�¿ÇG‹3†9‘¿Öÿ9Ì—�¿.ÿ!ýöu�¿0Ÿ¬®�¿,¸ðÀ’¿‰´�?QÙ�¿¸ õôˆ¿+£‘Ï+Ž¿<Ü ‹Q‡¿U„›Œ*È¿MeQØEÑ“¿nùHJzŠ¿°¶-Êl�¿0™ò!¨Š¿˜ø£¨3÷�¿;Èzjõ•¿L£uT5‘¿›sðLh’ˆ¿û:pΈ‚¿§]L3Ý딿ú ÒŒEÓ™¿.8ƒ¿_Ì–¿}þðóß“¿z‹‡÷˜¿ +TTýš¿–Ð]gE”¿ ­‡/•¿¿eN—ÅÄ–¿Ìí^î“£�¿˜ËôKÄ‹¿¾öÌ’5…¿×kzPPŠ–¿¾‰!9™¸•¿j¥È%Žœ¿¡Ö4ï8E—¿£7ünŠ¿ a°ä*v¿¡0(Óhr�¿9™¸U}¿<À“.«�¿•c²¸ÿÈ„¿œmnLOX’¿Ü¹0Ò‹Ú�¿®+f„·‘¿2Tqã“¿~ÿæÅ‰¯–¿UÞŽpZð’¿ð6oœæ�¿ðÐïû‡¿>?Œm|¿9Ó„í'cŒ¿=Ô¶a”¿íõî�÷ª•¿{ö\¦&‘¿@KW°�x’¿ˆe3‡¤–¿;oc³#Õ‡¿|G� 1—”¿m�Þp¹•¿ôQF\•¿ÏÚmšë”¿ â8ðj¹“¿\<¼çÀr”¿ |E·^Ó“¿¡JÍh–¿w¾Ÿ/Ý”¿2rö´Ã�¿ù»wÔ˜“¿ë�ZaúŽ¿û“øÜ ö�¿r7ˆÖŠ6—¿e6È$#g‘¿¨ýÖN”„”¿ÿëÜ´§‘¿Ÿ;ÁþëÜ”¿üª\¨ük™¿Ýξò =•¿Ãe6\�¿Zhç4 ´‹¿¡/½ý¹h˜¿ÜÖž—Š�¿ßávhXŒš¿„GG¬Å—¿^¡–±¡›¿:3Pÿž¿BA)Z¹˜¿˜m§­Á˜¿)=ÓKŒeš¿õ¡ ê[政üûŒ B’¿#KæXŽ¿Èбƒš¿ÚÿkÕ®™¿Û$¶» ¿h–¨©e›¿È]„)Ê¥‘¿ŠXİØ„¿<ÁþëÜ´‰¿ �ר%ª‡¿D6�.6­”¿šÐ$±¤Ü�¿ã5¯ê¬–¿BwIœQ“¿—¬Šp“Q•¿)¯•Ð]—¿çá¦Óš¿U2Tqã–¿OÜóüi“¿á (ÔÓG�¿§ÌÍ7¢{†¿·µ…祒¿¬W‘ÑI˜¿YLüQÔ™¿ˆ.¨o™Ó•¿¹©�æsî–¿ôÄs¶€Ðš¿‹qþ&"�¿ýgÍ�¿´˜¿žµÛ.4×™¿®f�ñ}q™¿—üSªD™¿:ÏØ—l<˜¿zûsÑ�ñ˜¿e5]Ot]˜¿�Ò¥Iš¿}”€F™¿;á%8õ�”¿,+MJA·—¿®Ö‰Ëñ ”¿ %“S;Ô¿™¸U]›¿ŽÍŽTßù•¿Ô·Ì鲘¿³Dg™E(–¿¤«tw� ™¿¦H¾H‰�¿ŠriüÂ+™¿ú*ùØ] ”¿d\qqTn’¿ò`‹Ý>«œ¿Eð¿•ìØ ¿^L3Ý뤞¿cð0í›û›¿R}ç%蟿ì¾cxìg¡¿| Vœj-œ¿§/ú Òœ¿m­/Úrž¿íI`sž™¿�èI™ÔЖ¿RÑXû;Û“¿GÉ«s Èž¿–¨©ek�¿‡Šqþ&¢¿n‰\pŸ¿‹Q×ÚûT•¿Ç,{ØŒ¿HG¬Å§�¿CV·zNz�¿;R}ç%˜¿A~6rÝ”’¿Ñ=ë-š¿™ž°ÄÊ–¿îÑZÑæ˜¿ûå¶}�š¿±i¥È%ž¿°u©ú™š¿êËÒNÍå–¿ ¯$y®ï“¿Öà}U.TŽ¿÷“1>Ì^–¿F³²}È›¿ûËîÉÃB�¿Q§“lu™¿ÚÅ4Ó½Nš¿¡…Œ.ož¿èLÚTÝ“¿á¶¶ð¼Tœ¿wÜð»é–�¿õ,å}�¿»Ò2R臭¿ys¸V{Ø›¿¯w¼W­œ¿“ߢ“¥Ö›¿�—6Êú�¿vãÝ‘±Úœ¿d²¸ÿÈt˜¿�„}›¿vþÓ ˜¿ÒQf`˜¿b.©Úž¿a¥‚Šª_™¿ö#EdXÅ›¿CpìÙs™¿¥Ÿpvk™œ¿y²›ýh ¿|ïoÐ^}œ¿NœÜïP˜¿¥hå^`–¿¯½7† ¿Zg|_\ª¢¿„+ PO¡¿ðO©eŸ¿—Æ/¼’ä¡¿EÕ¯t><£¿€FéÒŸ¿¨n.þ¶' ¿–B —8ò ¿~b¼æU�¿÷ʼUסš¿¯zÀVðÛ“¿n‡†Å¨›¿D¥3û<–¿×ÜÑÿr�¿Íù†š¿ølìMœ¿U1•~ÂÙ�¿7TŒó7¡ ¿ÇÖ3„c–�¿vþí²_wš¿)uÉ8F²—¿“Žr0›“¿l°p’æ�™¿º„Coñðž¿g¸ŸF ¿wIœQ�¿\ AñcÌ�¿ë˜Ü ¿¹oµN\Ž—¿ûëÜ ¿CW"Pýƒ ¿#ô3õºE ¿3ÂÛƒ ¿ZÙ>ä-WŸ¿ˆ»z ¿ û=±N•Ÿ¿‹ßV*¨ ¿š]÷V$& ¿&¦ ±ú#œ¿ /Á©$Ÿ¿ÍsD¾K©›¿UÙYœ¿£çº¡¿Ì|?q�¿7ê°Â-Ÿ¿déCÔ·œ¿€fØñŸ¿LkÓØ^ ¢¿\Uö]üŸ¿REñ*k›¿´„Ö×™¿áFÊI»¡¿ølìM¤¿!®œ½3Ú¢¿ïW¾Û¼¡¿á@H0�£¿rÝ”òZ ¥¿áî¬Ýv¡¡¿¨5Í;NÑ¡¿Ê‰vR~¢¿]kïSUh ¿Èì,z§ž¿M÷:©/K›¿­¢?4ó䢿d¬6ÿ¯:¢¿£’:M„¥¿rÞÿÇ £¿›©¾ó›¿NšEó–¿)%«êå—¿óªÎj�=–¿ L§uÔž¿•-’v£�™¿*;ý .R ¿Õ—¥�šË�¿”ƒÙ–Ÿ¿þ°Víš ¿wõ*2¢¿ãpæWs€ ¿`<ƒ†þ ž¿™¸U]›¿|,}è‚ú–¿¯Z™ðKýœ¿Q.�_x%¡¿‹Ã™_Í¢¿Â26t³? ¿—⪲¿’xy:W”¢¿sž M›¿­¡Ô^DÛ¡¿„„(_ÐB¢¿'÷;¢¿ "RÓ.¦¡¿vý‚ݰm¡¿“ `ÊÀ¡¿¬ãø¡Òˆ¡¿¸çùÓFu¢¿UÝ#›«æ¡¿Y‰yVÒŠŸ¿Ó0|DL¡¿’ê;¿(AŸ¿VñF摟¿ºM¸Wæ­¢¿Tã¥›Ä  ¿6çà™Ð$¡¿½në ¿Û§ã1•¡¿(îx“ߢ£¿IŸVÑš¡¿$Õw~Q‚ž¿ÄZ| €ñœ¿{Cr2q£¿h^»ï¦¿mw Ny¤¿%]3ùf›£¿Ov3£¥¿fI€šZ¶¦¿LkÓØ^£¿Ã9}=_£¿’?xî=¤¿“þ^ ¢¿ÍÈ w¦ ¿/ýKR™bž¿zÄè¹…®¤¿ä1•ñ÷±‚߆§¿Dioð…ɤ¿tϺFË�ž¿ègêu‹À˜¿£çº�š¿n§­Á8˜¿ÒIØ·“ ¿üÂ+Ižë›¿ñ*k›âq¡¿µ¤£Ì& ¿eû�·\ý ¿U…bÙÌ¡¿ò$éšÉ7£¿Ánض(³¡¿6>“ýó4 ¿%ušž¿D3O®)�™¿¦¶ÔA^Ÿ¿²ºÕs¢¿X}w+£¿h+ømˆ¡¿T5AÔ}¢¿å™—ÃL©KÆ1’�¿0JÐ_裿Åv÷Ý—£¿úDž$]£¿t^c—¨Þ¢¿ç6á^™·¢¿î`Ä>£¿ÝéÎÏÙ¢¿ÐѪ–t”£¿ƒR´r/0£¿±á镲 ¡¿Lþ'÷Ž¢¿àõ™³>å ¿GË�jÛ ¿ßÞ5裿šë4ÒR¡¿ÅÅQ¹‰Z¢¿;«ö˜H¡¿kaÚ9Í¢¿Áüýb¶¤¿­¢?4ó䢿ëâ6À[ ¿�äòÒoŸ¿Í鲘Ø|¤¿@øP¢%§¿$·&Ý–È¥¿Tàd¸¥¿qæWs€`¦¿æXÞU˜§¿¸éÏ~¤ˆ¤¿³}È[®~¤¿c|˜½l;¥¿iâàI£¿§Z ³ÐΡ¿ àfñba ¿ ¦–­õ¥¿˜‡LùT¥¿ˆc]ÜF¨¿Èì,z§¦¿‚âǘ»– ¿™Kª¶›à›¿ÓNïâ�¿¤ö{b�š¿bž•´â¢¿²/Ùx°Åž¿L‰$zÅ¢¿û²´Ss¹¡¿v“þ^¢¿”i4¹£¿[aú^Cp¤¿IƒÛÚÂó¢¿ôzÄè¡¿7ünºe‡ ¿»µL†ãùœ¿: ûv¡¿?T1³Ï£¿hÊN?¨‹¤¿à÷o^œø¢¿‘îçäg£¿�yÈ”A¥¿FÒnô1 ¿úÒÛŸ‹†¤¿Ü¸ÅüÜФ¿áñí]ƒ¾¤¿XSYvQ¤¿Bí·v¢$¤¿-xÑW�f¤¿Mø¥~ÞT¤¿xñ~Ü~ù¤¿ä÷6ýÙ�¤¿pìÙs™¢¿òšWuV ¤¿Åoò[t¢¿TS’u8º¢¿ÑË(–[Z¥¿Ž?QÙ°¦¢¿¸Ë~ÝéΣ¿Ñ[<¼çÀ¢¿µP29µ3¤¿�¨PÝ\ü¥¿C —8ò@¤¿n/†È¡¿çÇ_ZÔ'¡¿âú}ÿ楿Õ’we¨¿®)�ÙYô¦¿¦™îuR_¦¿©ƒ¼LЧ¿zûsÑ�ñ¨¿6±ÀWt륿ÙÎ÷S㥿¹nJy­„¦¿\wóT‡Ü¤¿!‘¶ñ'*£¿Êk%t—Ä¡¿Ÿ ±Ý=@§¿Ͻ‡K¦¿Ó–x@©¿'Ü+óV]§¿˜Û½Ü'G¡¿Œ¿í Û�¿…"ÝÏ)ÈŸ¿@/ܹ0Ò›¿QMIÖá袿¾ù  R ¿‚ŽVµ¤£¿ê”G7¢¢¿ŠÌ\àòX£¿8½‹÷ãö£¿:Ì—`¥¿ùÙÈuSÊ£¿`V(Òýœ¢¿žÒÁú?‡¡¿»·"1A Ÿ¿a8×0Cã¡¿_%» ”¤¿@Û5x_¥¿%è/ôˆÑ£¿6Ü,^¤¿Œ¢>+¦¿,ÒSä¡¿£’:M„¥¿†« î꥿0º¼9\«¥¿ë¬Øc"¥¿Ø�›6㤿œÀtZ·A¥¿Ïg@½¥¿1[²*Â¥¿MùT�^¥¿@¤ß¾œ£¿U¿Òù𤿃§Z£¿?æ�I£¿ì0&ý½¦¿…$³z‡£¿¼z�¤¿ |(Ñ’Ç£¿0�[wó¤¿Ç/¼’书¿¾0™*¥¿Ù@ºØ´¢¿ÿ‚¢¿ÕèÕ¥¡¦¿g s‚6©¿31]ˆÕ§¿:“6U§¿~£<ór¨¿…Ì•AµÁ©¿?§ ?¹¦¿µ5"—¦¿£±öw¶G§¿ Q…?Û¥¿s ßû¤¿‚<»|ëâ¿ 0,¾-¨¿ Ü¶ïQ§¿øßJvlª¿ÛàDôkë§¿°’�ÝJ¢¿ õôøÃŸ¿¡MŸt"¡¿ž—Š�y�¿Ú©¹Ü`¨£¿Uð2ÃF¡¿}Ê1Yܤ¿t|´8c˜£¿b k_@/¤¿NŸt"Á¤¿oõœô¾ñ¥¿.ÎR²œ¤¿‡Q<¾½£¿üÞ¦?û‘¢¿4»î­ ¿n¿|²b¸¢¿£êW:ž¥¿‡Áü2W¦¿CæÊ ÚिyxÒÂe¥¿FAðøö¦¿²½ôÞ¢¿aR||Bv¦¿ 'iþ˜Ö¦¿{„ò>ަ¿dËòu¦¿j1x˜ö¥¿ªÒ×øL¦¿ÒYùe0¦¿LÝ•]0¸¦¿ì@1²d¦¿ 3iSu�¤¿U‰²·”ó¥¿w+Kt–Y¤¿<š$–¤¿Ä\RµÝ§¿ŒÛho�¤¿8, ü¨†¥¿J´¤¿ ûvþ¥¿Þt_Îl§¿j¼t“¦¿MÚTÝ#›£¿úîV–è,£¿Ø¼ª³Z`§¿�“màÔ©¿Cr2q«¨¿j¡drjg¨¿.u�׃I©¿Ó£©žÌ?ª¿›r…w¹ˆ§¿ò[t²Ôz§¿ŸÉþy¨¿¬�ò“jŸ¦¿ŸÛ2à,¥¿+‡ÙÎ÷£¿Žx²›ý¨¿-ÌB;§Y¨¿Ìï4™ñ¶ª¿ž)t^c—¨¿’v5yÊ¢¿ TûtËóàT�4¸­-¤¿qÇ›ü�¤¿«Ñ«JC¥¿�3MØ~2¦¿ëTùž‘¥¿$ð‡Ÿÿ¤¿™Õ;Ü £¿.äÜH¡¿à‚lY¾.£¿¡JÍh¦¿}$%= ­¦¿²ó66;R¥¿DÂ÷þí¥¿}uU ƒ§¿îÍo˜h�¢¿~ŠãÀ«å¦¿½á´àE§¿Æjóÿª#§¿ž'ž³„¦¿Ùî ûr¦¿ �\7¥¼¦¿T§YO­¦¿Êû8š#+§¿75Ð|Îݦ¿œ5x_• ¥¿ŽË¸©�¦¿Üã5¯ê¤¿NA~6rݤ¿K?ªa§¿Äy8�餿ta¤µû¥¿'…y�3¥¿Œ-9(a¦¿ßÁO@¿§¿‘aod¦¿�Ïd¤¿_|Ñ/¤£¿AƒM�Gŧ¿<‡2TÅTª¿U¿Òù¨¿’±Úü¿ê¨¿"rúz¾f©¿[š[!¬Æª¿{úüáç§¿uæ¾§¿��@¼®_¨¿'ù¿b §¿½VBwIœ¥¿‚«<�°S¤¿ñ*k›âq©¿®e2Ïg¨¿3¨68ýª¿Ð캷"1©¿{0)>>!£¿ ý\¬ ¿NÑ‘\þC¢¿fgÑ;pŸ¿rúz¾f¹¤¿wõ*2¢¿—¬Šp“Q¥¿ Íui©¤¿B–¥¿´Yõ¹ÚŠ¥¿0Ø Ûe¦¿HÀèòæp¥¿ò`‹Ý>«¤¿ ñ+Öp‘£¿w|ÓôÙ¡¿ $(~Œ¹£¿Üd:tz¦¿ ×£p= §¿#ÜdTÆ¥¿ïäÓc[¦¿ãÝ‘±Úü§¿É˻ꣿ¿+‚ÿ­d§¿œý�rÛ¾§¿( µ¦y§¿¯Ñr ‡Ú¦¿…y�3Mئ¿„�çÞÃ%§¿Œó7¡§¿Ð}9³]¡§¿ûu§;O<§¿¦{�Ô—¥¥¿ÌÑã÷¦¿3ÀÙ²|¥¿s-Z€¶¥¿h%­ø†Â§¿Zd;ßO¥¿žÏ€z3j¦¿‘{ººc±¥¿‘|%�»¦¿[z4Õ“ù§¿||BvÞÆ¦¿±¢Ó0|¤¿’—5±ÀW¤¿>°ã¿@¨¿€~ß¿yqª¿Ù\5Ï©¿B_zûsѨ¿TªDÙ[Ê©¿¸­-#£¿¡/½ý¹h ¿¥žÐëO¢¿%”¾rÞŸ¿þî5&Ĥ¿ÿ±G¢¿Ù=yX¨5¥¿ÅªA˜Û½¤¿wIœQ¥¿ÛûTˆ¥¿AG«ZÒQ¦¿7ÆNx N¥¿À>:u峤¿³_wºóÄ£¿K�Pᢿ6WÍsD¾£¿Éª7U¦¿í S[ê §¿P«”žé¥¿·Œõ L¦¿yGsdå§¿=€E~ý£¿(·í{Ô_§¿ñcÌ]Kȧ¿xak¶ò’§¿!yvù¦¿�½Þýñ¦¿T:Xÿç0§¿Â1Ëž6§¿o¼;2V›§¿ ÛÝt§¿Èa0…Ì¥¿¦·? §¿û®þ·’¥¿° ÍX4�¥¿û“øÜ ö§¿ñKý¼©H¥¿EØðôJY¦¿ß—ª´¥¿”2©¡ À¦¿Dl°p’æ§¿î#·&ݦ¿z9ì¾cx¤¿ÓMbX9¤¿ˆc]ÜF¨¿ÚX‰yVÒª¿s¼Ñ“2©¿œá|~©¿„Iññ Ù©¿½5_%«¿(¸XQƒi¨¿ˆc]ÜF¨¿�x]¿¨¿õiý¡™§¿¶¢Íqn¦¿• •-¯¤¿Ì&À°ü©¿ N} y稿èô¼ «¿æ²Ñ9?Å©¿‘cëÂ1£¿«Ì”Öß ¿wÙ¯;Ýy¢¿0*©ÐD ¿4¢´7øÂ¤¿•~ÂÙ­e¢¿‚TŠ�C¥¿¥„`U½¤¿OÌz1”¥¿ší }°Œ¥¿VGŽtF¦¿ÑË(–[Z¥¿RGÇÕÈ®¤¿“ߢ“¥Ö£¿Ú:8Ø›¢¿HùIµOÇ£¿Û¦¶ÔA¦¿=,Ôšæ§¿¤¨3÷�𥿑aod¦¿[z4Õ“ù§¿¤ý°Ví¢¿åòÒo_§¿["œÁß§¿ EºŸS�§¿ñœú@ò¦¿ iQŸä¦¿§’ Š§¿&©L1A§¿M.ÆÀ:ާ¿ïÊ.\s§¿ ~þ{ðÚ¥¿š\Œ�u§¿!®œ¥¿>#ÁÆ¥¿õœô¾ñµ§¿ÃbÔµö>¥¿*9'öÐ>¦¿5&Ä\Rµ¥¿µÀ)ͦ¿)Íæq̧¿Õ@ó9w»¦¿f¼­ôÚl¤¿ûʃô9¤¿~R›8¹§¿¨«;Û¤ª¿¨o™Óe1©¿g)YNB©¿ÒåÍáZí©¿žACÿ«¿5–°6ÆN¨¿ébÓJ!¨¿Ï£âÿލ¨¿4MØ~2Ƨ¿ÚŽ©»² ¦¿MÛ¿²Ò¤¤¿Ù¯;Ýyâ©¿~©Ÿ7©¨¿333333«¿ÿ“¿{G�©¿[rPÂL£¿ÍXäן¿]øÁùÔ±¢¿ y7R¶ ¿ÛÝt_Τ¿vþí²_w¢¿D¤¦]L3¥¿ÛÝt_Τ¿ R)v4¥¿�KqUÙw¥¿JìÚÞnI¦¿¿˜-Y¥¿šÏ¹ÛõÒ¤¿”‚Uõò£¿�»¶·[¢¿þEИIÔ£¿ˆißÜ_=¦¿ì1‘Òl§¿ðiN^d¦¿ÊÝçøhq¦¿XÎüj¨¿ ^ô¤£¿þE�>‘§¿lê<*þ§¿M†ãù ¨§¿Ú–?ߦ¿U2Tqã¦¿š´©ºG6§¿ =bôÜB§¿—9]›§¿PŒ,™cy§¿ePmp"ú¥¿=_³\6:§¿rûå“Ã¥¿¶¿³=zÃ¥¿%<¡×ŸÄ§¿¡„™¶e¥¿[³•—üO¦¿ê—ˆ·Î¿¥¿ësµû˦¿Y‰yVÒŠ§¿Õ@ó9w»¦¿�^ Pj¤¿w+Kt–Y¤¿+j0 ç¿&rÁüýª¿šë4ÒR©¿�%W±øM©¿áy©Ø˜×©¿�Ò¥I*«¿jhwH1¨¿¥žÐë§¿BÌ%UÛM¨¿‡ýžX§Ê§¿ÃØB�ƒ¦¿ %“S;ä¿�üÁÀsï©¿\Ëd8žÏ¨¿4õ»°5«¿*äJ= B©¿ñf ÞW墿ˆLùT�ž¿£7ün¢¿¯–;3Á ¿õfÔ|•¤¿+5{ ¢¿nLOX⥿‚ŽVµ¤£¤¿A ]Þ¤¿ÀnÝÍS¥¿—¡Ÿ©×¥¿FÏ-t%¥¿ìÁ¤øø„¤¿î•y«®£¿Ûmšë4¢¿Žèžu�–£¿!±Ý=@÷¥¿µÀ)ͦ¿ pzïÇ¥¿Ñtv28J¦¿ìOâs'ا¿p%;6ñ¢¿+TTýJ§¿ Й´©º§¿5E€Ó»x§¿µ5"—¦¿Ý a5–°¦¿®)�ÙYô¦¿q¬‹Ûh§¿¦Óº j§¿‘·\ýØ$§¿üá翯¥¿¶óýÔxé¦¿é Œ¼¬‰¥¿EºŸS�Ÿ¥¿Tr3Ü€§¿µÆ B¥¿ÀzÜ·Z'¦¿ŒÜÓÕ‹¥¿?tA}Ëœ¦¿ö  Y2§¿e‹¤Ýèc¦¿'ÙêrJ@¤¿¬q6¤¿N™›oD§¿Aó9w»^ª¿6\䞮^d~�$©¿w0bŸŠ©¿“6U÷Èæª¿"Þ:ÿvÙ§¿¨¨ú•·§¿SäG¨¿· b k§¿Ãõ(\�Â¥¿œú@òΡ¤¿yæå°ûŽ©¿ c AJ¨¿ÝéÎÏÙª¿…^Ÿ;©¿&rÁüý¢¿e‹¤Ýècž¿G’ \…¢¿p^œøjG¡¿[ìöYe¦¤¿­„î’8+¢¿øŠn½¦¥¿=Gä»”º¤¿ÁÆõïú¤¿?8Ÿ:V)¥¿Ü�:åÑ¥¿‰ëW\¥¿;9CqÇ›¤¿TTýJ磿'‡O:‘`¢¿ï©œö”œ£¿ Ö8›Ž¦¿2!æ’ªí¦¿Ç¹M¸W楿ÿ]Ÿ9ëS¦¿í‚Á5wô§¿³A&9 £¿øo^œøj§¿E¼uþí²§¿¬9@0G�§¿,*ât’­¦¿ð‡Ÿÿ¼¦¿3TÅTú §¿ƒ¦%VF#§¿ý3ƒøÀާ¿°çk–ËF§¿¬„¹ÝË¥¿&Q/ø4'§¿ ¦}s¥¿sôø½¥¿mýôŸ5§¿\ªÒ×ø¤¿`Ç� @¦¿st´ª¥¿’Ìên‡¦¿+2: û¦¿iÄÌ>�Q¦¿­†Ä=–>¤¿›äGüŠ5¤¿j ùœ»]§¿£®µ÷©ª¿üo%;6©¿¤«tw� ©¿ Ê4š\Œ©¿&rÁüýª¿J³yó§¿ÑÍþ@¹m§¿‹üú!6¨¿H¾�D„§¿Õ—¥�šË¥¿¢+Üò‘¤¿óèFXTÄ©¿!ɬÞáv¨¿í-å|±÷ª¿¨o™Óe1©¿ |(Ñ¢¿"©…’É©�¿¼ "5íb¢¿9€~ß¿y¡¿Võò;Mf¤¿Gÿ˵h¢¿(F–̱¤¿ß‹/Úã…¤¿¦ÒO8»µ¤¿CqÇ›ü¥¿Å °rh‘¥¿#Ûù~j¼¤¿^ò?ù»w¤¿Eh׿£¿!èhUK:¢¿Â5wô¿\£¿• Uܸ¥¿­k´衦¿ß¿yqâ«¥¿ƒ0·{¹O¦¿œMG7‹§¿ì¢è��Á¢¿T:Xÿç0§¿¤µûU€§¿F´Swe§¿–u¦¿G>¯xꑦ¿¤Q�“mি³•—üOþ¦¿š´©ºG6§¿ÞÈ<ò§¿™E(¶‚¦¥¿l@„¸rö¦¿;ŠsÔÑq¥¿3ÀÙ²|¥¿Ú�‘a§¿\wóT‡Ü¤¿j1x˜ö¥¿þ oÖà}¥¿Ͻ‡K¦¿3ü§(𦿵ÑvLÝ¥¿¸#œ¼è£¿¹‰Zš[!¤¿ÁþëÜ´§¿V]ûª¿tys¸V{¨¿ÅËÓ¹¢”¨¿]‰@õ"©¿¦E}’;lª¿ýfbº«§¿” ¿Ð#F§¿ –\Åâ§¿úšå²Ñ9§¿_&Š�º�¥¿–Ð]gE¤¿÷!o¹ú±©¿o l•`q¨¿‰{,}肪¿MõdþÑ7©¿qXøQ £¿Ã€%W±ø�¿¿a¢A ž¢¿f¤ÞS9í¡¿}—R—Œc¤¿è£Œ¸4¢¿’]i©÷¤¿Sz¦—ˤ¿i­hsœÛ¤¿ p¥¿�ÖmPû­¥¿0œk˜¡ñ¤¿ð…ÉTÁ¨¤¿¬q6¤¿+Kt–Y„¢¿t|´8c˜£¿¬r¡ò¯å¥¿B]¤P¾¦¿Žuq ॿ"ÇÖ3„c¦¿uŽÙëݧ¿DÔ·Ì颿ïr߉Y§¿Ù"i7ú˜§¿ò[t²Ôz§¿‹v“¦¿ô噗æ¿MÀ¯‘$§¿ùgñ�§¿¨þš¬Q§¿¹4~á•$§¿ê"…²ðõ¥¿º ¾eN§¿Ýa™¹À¥¿Öýc!:¦¿/N|µ£8§¿ `­Ú5!¥¿9· ÷ʼ¥¿àg\8’¥¿ÚæÆô„%¦¿@2:=樂g+/ùŸü¥¿ŒHZÖý£¿\Y¢³Ì"¤¿ pA¶,_§¿�µ¿³=ª¿Ü~ùdŨ¿¹û-Ψ¿p^œøjG©¿ò˜ù~ª¿/lÍV^ò§¿ û=±N•§¿>ù*¨¿d”g^»§¿–´â Ÿ¥¿dT8‚¤¿5*p² Ü©¿U¡�X6s¨¿CŒ×¼ª³ª¿Ö‹¡œhW©¿O"¿£¿Ýì”Ûö�¿6#ƒÜE˜¢¿"Ã*ÞÈ<¢¿CSvúA]¤¿†:¬pËG¢¿¬ÉSVÓõ¤¿N¶�;P§¤¿nÁR]Àˤ¿ô¥·?¥¿¦H¾H‰¥¿ÛÝt_Τ¿E¹4~ᕤ¿ß¿yq⣿¿a¢A ž¢¿‚�ëßõ™£¿¶¿³=zÃ¥¿îyþ´Q�¦¿›sôø¥¿¢©ÛÙW¦¿ù…W’<×§¿l”õ›‰é¢¿ïr߉Y§¿)Bêvö•§¿žxÎZ§¿PãÞü†‰¦¿Õ@ó9w»¦¿=Ô¶a§¿yÎZ§¿Ž •b§¿ÒŦ•B §¿dÍÈ w¦¿—®`ñd§¿�—6Êú¥¿:êè¸Ù¥¿#KæXÞU§¿R �h¥¿ºÙ(·¥¿Z�!«[¥¿dXÅ™G¦¿ Ï.ßú°¦¿ª›‹¿¥¿Qkšwœ¢£¿]©gA(m±§¿78ýÚú©¿íHõ�_”¨¿ŒºÖÞ§ª¨¿˜PÁá©¿dT8‚Tª¿,Ó/o�§¿¬Zd;§¿@ÛjÖß§¿Œi¦{�Ô§¿P:‘`ª™¥¿–Ð]gE¤¿´�€ÑåÍ©¿¤�#ÖâS¨¿m¡õðeª¿è…;Fz©¿iâàI£¿Aœ‡˜ž¿H¨REñ¢¿µá°4𣢿Ú(·í{¤¿ˆ jôj€¢¿ˆ…ZӼ㤿mæ�ÔBɤ¿AØ)V ¤¿ô¥·?¥¿ÎÅßö‰¥¿š'×È줿Òබ𼤿«x#óȤ¿HøÞß ½¢¿_ÔîW¾£¿Šä+�”Ø¥¿„F°qý»¦¿ì0&ý½¦¿rÁüýb¦¿¯zÀU¥¿èl¡õð¥¿X7Þ«¥¿t'Ø�›¦¿-²�罹¿bg �ר¥¿¡eÝ?¢£¿gd�»S¤¿H‡‡0~§¿pê”G7ª¿õ�_” ¿¨¿�pZ𢯨¿BÎûÿ8©¿¦E}’;lª¿·Ï*3¥õ§¿:Yj½ßh§¿4ØÔyTü§¿'¢_[?ý§¿KY†8ÖÅ¥¿ÂLÛ¿²Ò¤¿ÚâŸÉþ©¿è¡¶ £¨¿òÓ¸7¿aª¿šë4ÒR©¿-'¡ô…�£¿Íä›mnLŸ¿d"¥Ù<£¿Ø}ÇðØÏ¢¿Š§wñ~¤¿Õ”dŽ®¢¿­¤ßPø¤¿€»ì×�Ø�›6㤿x|{× /¥¿"QhY÷�¥¿òC¥3û¤¿Ù²|]†ÿ¤¿Z.�óS¤¿‡3¿š£¿ÒÂe6¤¿ £YÙ>䥿‰²·”óŦ¿‹RB°ª^¦¿¨ÿ¬ùñ—¦¿\Uö]ü§¿vOjM£¿—á?Ý@�§¿v6äŸħ¿/áÐ[<¼§¿Ç/¼’书¿Å7>[§¿+TTýJ§¿0)>>!;§¿Í”Ö߀§¿XVš”‚n§¿\>’’†¦¿ßÁO@¿§¿Üôg?RD¦¿G>¯xꑦ¿x N} y§¿=e5]Ot¥¿2«w¸¦¿s¹ÁP‡¦¿²×»?Þ«¦¿¸®˜Þ¦¿, ‘Ó×ó¥¿íÖ2Žç£¿ÄwbÖ‹¡¤¿Ž±^‚S§¿CV·zNª¿âuý‚ݨ¿Á¨Sݨ¿IZÖýc©¿—ä€]Mžª¿–̱¼«¨¿õiý¡™§¿ûõ×+,¨¿ üÝ;jL¨¿…(_ÐB¦¿î²_wºó¤¿J m6 ª¿Z Ý!Å©¿yËÕ�Mª¿ÍçÜíz©¿Vס𒬣¿âZía/ ¿´tÛˆ'£¿�Øî û¢¿+Üò‘”¤¿Ø}ÇðØÏ¢¿0�[wó¤¿òC¥3û¤¿¹§«;Û¤¿o.2¥¿ò Ùy›¥¿]ݱØ&¥¿B>èÙ¬ú¤¿›?Œ ¿IØÕä)£¿r3܀ϣ¿ò`‹Ý>«¤¿I+¾¡ðÙ¢¿:tzÞ�¥¿ýöuàœ¥¿÷XúÐ¥¿èÙ¬ú\m¥¿d’‘³°§¥¿$ñòt®(¥¿‡À‘@ƒM¥¿7¨ýÖN”¤¿û!6X8I£¿sg&Î5¤¿–>tA}£¿31]ˆÕ§¿éI™ÔШ¿9(a¦í§¿�Æ¡~¶¦¿OÎPÜñ&§¿>BÍ�*Ч¿f¿ît牧¿•Öÿ9̧¿áìÖ2ާ¿Tÿ ’!Ǧ¿Ð¸p $ ¨¿[ìöYe¦¿Ô x'Ÿ¦¿/áÐ[<¼§¿X7Þ«¥¿Ö­ž“Þ7¦¿’‘³°§¦¿»a̦ۢ¿½:Ç€ìõ¦¿ª*4Ëf¦¿y ý\¤¿µùÕ‘#¥¿Ž<Y¤‰§¿|E·^Óƒª¿oÓŸýH©¿¨o™Óe1©¿"¥Ù<ƒ©¿6Vbž•´ª¿}ëÃz£V¨¿^€}têʧ¿—W®·ÍT¨¿‚âǘ»–¨¿—Çš‘A¦¿geû�·\¥¿¯@ô¤Lª¿ã6À[ ©¿B•š=Ъ¿Õv|Óô©¿�ÙYôN¤¿DP5z5@¡¿ˆ×õ v£¿ ‹Š8�d£¿Xqªµ0 ¥¿«ÏÕVì/£¿‹Q×ÚûT¥¿Až]¾õa¥¿'…y�3¥¿ù g³ês¥¿õ¢v¿ ð¥¿_›�•˜g¥¿ìjò”Õt¥¿B³ëފĤ¿ÄC?�{£¿ãÄW;Šs¤¿ýh8en¦¿4ºƒØ™B§¿‚9züÞ¦¿‚sF”ö§¿2�g—o}¨¿o›©�Ä£¿i㈵ø¨¿5–°6ÆN¨¿B™F“‹1¨¿y ²H§¿UPQõ+�§¿¸w úÒÛ§¿�œ…=íð§¿æë2ü§¨¿/lÍV^ò§¿×1®¸8*§¿Ž²~31]¨¿Ý•]0¸æ¦¿ë�Zaú¦¿‘(´¬û§¿ú™zÝ"0¦¿°Éõ�¦¿nˆñšWu¦¿½ÅÃ{,§¿6•EaE§¿Üd:tz¦¿Öß—ª¤¿Á7MŸp¥¿ˆ0~÷æ§¿ûèԕϪ¿ÞU˜‡L©¿úœ»]/M©¿Ð YÝê©¿aÄ>#«¿rNì¡}¬¨¿‰–<ž–¨¿‹ßV*¨¨¿›V �\⨿þðó߃צ¿s.ÅUeߥ¿£çº�ª¿´„Öש¿¥ØÑ8Ô謹úE ú =ª¿ñgx³é`ýŸÃ|¡¿ä²ó66£¿×Ý<Õ!7£¿rR˜÷8Ó¤¿‘ Îà2ZGUD¥¿­‡/EH¥¿x|{× /¥¿¢·xxÏ�¥¿Ù[Êùb勵Rewƒh¥¿+ö—Ý“‡¥¿4J—þ%©¤¿iqÆ0'h£¿uÍä›mn¤¿ªÒ×øL¦¿IºfòÍ6§¿ÒâŒaNЦ¿�©»² §¿­½OU¡�¨¿!u;ûÊ£¿¢œhW!å§¿LqUÙwE¨¿ú™zÝ"¨¿c›T4Öþ¦¿‚®}½p§¿4MØ~2Ƨ¿}XoÔ Ó§¿=}þð󧿯zÀ§¿eãÁ»}¦¿Øô  ­¤¿º½¤1ZG¥¿õôøÃϧ¿°ŒØ'€ª¿Ñ”�~P©¿Í[uª)©¿¯Ì[uª©¿» ”X«¿)ë7Ó…¨¿¸ õô¨¿ ‡Ú6Œ‚¨¿ò—õI²/Ùx°Å¦¿…ÐA—p西ÆPN´«�ª¿ðĬC9©¿M¼Zœ©¿g Ü¶ï©¿czÂ(«¿辜ٮ¨¿—W®·ÍT¨¿üd訿,ÒSä©¿@2:=樂G«ZÒQ¦¿;‡ú]ت¿,ռ̰©¿ˆfž\S «¿pê”G7ª¿óuþÓ ¤¿í”Ûö=¢¿½qR˜÷8£¿ó¯å•ëm£¿¸Ì鲘ؤ¿½5_%£¿ðûY,¥¿_Cp\ÆM¥¿Â/õó¦"¥¿ÛûTˆ¥¿ÕÊ„_ê祿�ö?ÀZ¥¿œ’“‰[¥¿ÞãL¶Ÿ¤¿óWÈ\T£¿õfÔ|•|¤¿[ÏŽY¦¿Ç�ÒÁú?§¿ø©*4˦¿Ú–?ߦ¿Hk :!t¨¿³Z�!«£¿¶ƒû¨¿ŸÉþy0¨¿ñFæ‘?¨¿I/j÷«§¿4fõ‚§¿¼S”Kã§¿É>Ȳ`â§¿¸ õô¨¿Ûˆ'»™Ñ§¿ì1‘Òl§¿ô‡fž\S¨¿�@gҦꦿÃ)só�覿¥žÐë§¿ÈDJ³y¦¿[ ³ÐÎi¦¿/¥.ÇH¦¿Éå?¤ß¦¿ùé·¯§¿þµ¼r½m¦¿Q¼ÊÚ¦x¤¿‚Uõò;¥¿œ¥d9 ¥§¿Ð�`ÿunª¿j¿µ%©¿ÎŽTßùE©¿ÔUø3¼©¿™J?áìÖª¿É¬Þávh¨¿ã4ô§¿j¡drjg¨¿nmáy©Ø¨¿ËǺ¸�¦¿¦^·Œ¥¿³#Õw~Qª¿¶Øí³ÊL©¿ŽÊMÔÒܪ¿$cµùÕ©¿ÍËa÷ã¿•~ÂÙ­e¢¿û–9]£¿{ˆFw;£¿¿ [³•—¤¿‹lçû©ñ¢¿\wóT‡Ü¤¿Ÿ;Áþ뤿¯Ïœõ)Ǥ¿ R)v4¥¿¢·xxÏ�¥¿£Ì™d䤿j'÷;¥¿Võò;Mf¤¿r3܀ϣ¿vmo·$¤¿?VðÛ㥿g|_\ªÒ¦¿Ùî ûr¦¿‘ñ(•ð„¦¿ÇF ^×/¨¿,ôÁ26t£¿ì÷Ä:U¾§¿‘Ešx¨¿ÍwðЧ¿Ú¬ú\mŦ¿sôø½M§¿ãúw}欧¿Ùz†p̲§¿ÉæªyŽÈ§¿&䃞ͪ§¿âeS®ð¦¿Tã¥›Ä ¨¿¶hÚV³¦¿äÜ&Ü+󦿣 x|{§¿ ®¹£ÿ¥¿[[x^*6¦¿Èì,z§¦¿ˆØÒ£©¦¿…ÏÖÁÁ¦¿¼?¦¿ï<ñœ- ¤¿¾0™*¥¿XVš”‚n§¿ãÂ��,`ª¿SYvQô¨¿TŒó7¡©¿�ÁŠS­…©¿¿¹¿zÜ·ª¿{Ý"0Ö7¨¿—qSͧ¿‰îY×h9¨¿“Qewƒ¨¿Üôg?RD¦¿/üà|êX¥¿;TS’u8ª¿—uÿXˆ©¿ª¹Ü`¨Ãª¿æZ´m«©¿^¾õa½Q£¿®,ÑYf¢¿–±¡›ý�¢¿²ƒJ\Ǹ¢¿™~‰xëü£¿+•Ô h¢¿f¼­ôÚl¤¿œoD÷¬k¤¿£’:M¤¿«Íÿ«Ž¤¿¢$$Ò6þ¤¿?ÆÜµ„¤¿L¨àð‚ˆ¤¿¸#œ¼è£¿i¢¢¿ŠWYÛ�£¿··[’v¥¿Ø»?Þ«V¦¿�:覿j¼t“¦¿ÚUHùIµ§¿—Çš‘AÜõÒN§¿k+ö—Ý“§¿´«�ò“j§¿I¹û-¦¿5Ïù.¥¦¿@Š:s §¿ÖþÎöè §¿]j„~¦^§¿à.ûu§;§¿û$wØDf¦¿E¼uþí²§¿ç<š$¦¿ÁUž@Ø)¦¿Å5| 릿E×…œO¥¿åÓc[œ¥¿˜ßi2ãm¥¿ÞïU+¦¿ûÌYŸrL¦¿,)wŸã£¥¿ $(~Œ¹£¿bƒ…“4¤¿»_øn󦿈‚S°Æ©¿øpÉq§t¨¿»&¤5�¨¿Ð¹ÛõÒ©¿Êþy0Hª¿ƒlY¾.ç¿ÐšiQ§¿cì„—àÔ§¿KæXÞU¨¿È—PÁ᥿÷¯¬4)¥¿°WXp?à©¿Ní S[꨿ªó¨ø¿#ª¿Ñw·²Dg©¿ëR#ô3õ¢¿PqxµÜ¡¿†¯¯u©¢¿ö á˜eO¢¿æZ´m£¿ Oèõ'ñ¡¿u:�õÔꣿ�ÙYôN¤¿CV¸å£¿Üò‘”ô0¤¿z9ì¾cx¤¿=)“Ú¤¿š †s 3¤¿ùNÌz1”£¿;TS’u8¢¿r‹ù¹¡)£¿Ÿ;Áþ뤿…ÐA—p西r£ÈZC©¥¿@1²dŽ¥¿³x±0DN§¿ö—Ý“‡…¢¿Q¡º¹øÛ¦¿JbI¹û§¿ß£þz…§¿ ©¢x•µ¥¿Ðïû7/¦¿¹Æg²ž¦¿PnÛ÷¨¿¦¿žbÕ Ìí¦¿Ú¬ú\mŦ¿ºƒØ™B祿‘BYøúZ§¿Ë¿–W®·¥¿Ä[çß.û¥¿]¤P¾¾¦¿w¾Ÿ/ݤ¿P§<º¥¿f‚á\à ¥¿&â­óo—¥¿cšé^'õ¥¿ŸÛ2à,¥¿G°qý»>£¿ ܺ›§:¤¿IœQ}¦¿íI`sž©¿\­—㨿ûõ×+,¨¿"T©Ù­¨¿Þèc> Щ¿JíE´S§¿UÚâŸÉ¦¿Ž •b§¿Ó¾¹¿zܧ¿ñÖù·Ë~¥¿?rkÒm‰¤¿Å­j©¿3ßÁO¨¿¸É¨2Œ»©¿órØ}Çð¨¿OqN`¢¿\•›¨¥¡¿“ `ÊÀ¡¿¥/„œ÷ÿ¡¿Üñ&¿E'£¿åòw﨡¿†1zn¡£¿hyÜ�µ£¿ã§qo~£¿*É:]¥£¿Œ 1“¨¤¿Ý µ‰“£¿È_ZÔ'¹£¿NzßøÚ3£¿ë©ÕWW¢¿Ð³Yõ¹Ú¢¿pìÙs™š¤¿X7Þ«¥¿·_>Y1\¥¿p +TT¥¿lèf Ü¦¿«&ˆº@¢¿UOæ}“¦¿k ¥ö"Ú¦¿ �\7¥¼¦¿¢_[?ýg¥¿Ñ‘\þCú¥¿9DÜœJ¦¿AŸÈ“¤k¦¿°Éõ�¦¿×]~p¦¿|HøÞß ¥¿J ,€)§¿ÅÈ’9–w¥¿»ðƒó©c¥¿g¶+ôÁ2¦¿z9ì¾cx¤¿J%<¡×Ÿ¤¿ÞãL¶Ÿ¤¿º½¤1ZG¥¿'2s�Ëc¥¿ˆ NÒ¤¿Í"[AÓ¢¿<ö³XŠä£¿`” ¿Ð#¦¿ÎSr3ܨ¿ÅUeßÁ§¿ –\Åâ§¿÷æ7L4¨¿Êˆ @£t©¿«˜J?á즿( ß÷o¦¿c›T4Öþ¦¿œh>çn§¿ZKþ)¥¿÷‘[“nK¤¿2V›ÿW©¿õôøÃϧ¿ð÷‹Ù’U©¿æ¾÷7¨¿Êà(yuŽ¡¿2V›ÿW¡¿ ´¾L¡¿óUò±»@¡¿G’ \…¢¿¾¿A{õñ ¿ ŠcßO�—n£¿¼³vۅ梿ADjÚÅ4£¿æ!S>U£¿ëR#ô3õ¢¿ÂÝY»íB£¿Ð(]ú—¤¢¿‚X6sHj¡¿WCâK¢¿h׿룿:tzÞ�¥¿`°¶-ʤ¿éî:òϤ¿vR_–vj¦¿hwH1@¢¡¿ÖµÂô¥¿[[x^*6¦¿¥Û¹à ¦¿¼?Ƥ¿Ýξò =¥¿×Â,´sš¥¿šxxÒÂ¥¿í¸áwÓ¥¿|{× /½¥¿K;5— ¥¿n0Ôa…[¦¿Øô  ­¤¿ÿy0Hú¤¿ÂÂIš?¦¥¿ç7L4HÁ£¿Ëø÷¤¿Xp?à�¤¿bƒ…“4¤¿ÖqüPiĤ¿¹‰Zš[!¤¿32È]„)¢¿v?T1£¿ÚÈuSÊk¥¿7qr¿CQ¨¿±‡ö±‚ߦ¿ÒŦ•B §¿Õ^DÛ1u§¿!!Ê´�¨¿ŸwcAaP¦¿-\Va3À¥¿iÄÌ>�Q¦¿GqŽ::®¦¿1 òn¤¿>–>tA}£¿.ÿ!ýöu¨¿J•({K9§¿¡º¹øÛž¨¿ZGUDݧ¿«uâr¼¡¿¯yUgµ ¿W_]¨Å ¿Š;Þä· ¿E¡eÝ?¢¿!!Ê´� ¿·˜Ÿš¢¿-±2ù¼¢¿|E·^Óƒ¢¿pìÙs™¢¿@†ŽT⢿ŒÙ’Un¢¿}гYõ¹¢¿?�{ó&¢¿Ãƒf×½¡¿n¾ݳ®¡¿;8Ø›’£¿<š$–¤¿# Â¤R¤¿;V)=ÓK¤¿2 {½û㥿눻z¡¿J›ª{ds¥¿:’ËH¿¥¿½þ$>w‚¥¿�,µÞo¤¿A ]Þ¤¿ùk¸È=¥¿+øDk¥¿*ÖT…¥¿­‡/EH¥¿œú@òΡ¤¿È—PÁ᥿3Œ»A´V¤¿{¡€í`¤¿Ç›ü�,¥¿±n¼;2V£¿�~7ݲ£¿èÙ¬ú¤¿ Š·˜§¿NÕ=²¹j¦¿ô噗æ¿;ÆG妿Å8 ¨¿B\9{g´¥¿$ñòt®(¥¿sôø½¥¿Þs`9B¦¿ž·±Ù‘ꣿøý›'¾¢¿>°ã¿@¨¿õ¸oµN\¦¿XS¨¿»·"1A §¿ÌH¿} ¿u«ç¤÷�Ÿ¿vÁàš;úŸ¿sôø½MŸ¿’<×÷á ¡¿JbI¹ûŸ¿0fKVE¸¡¿2Ì Úäð¡¿9 {Úᯡ¿Ý@�wòé¡¿ ]lZ)¢¿Æ§Ï ¡¿>ÏŸ6ªÓ¡¿^=ð1X¡¿ï_{fI ¿à�|zlË ¿ {½ûã½¢¿‚åȳ£¿9*7QKs£¿ÙvÚŒ£¿np–’夿2kœMG ¿·Aí·v¢¤¿ˆ-=šêɤ¿çà™Ð$±¤¿kg{ô†£¿®~l’ñ£¿ŠÍǵ¡b¤¿c^G²�¤¿õ¾ñµg–¤¿$Ó¡Óón¤¿Žèžu�–£¿òë‡Ø`᤿겘Ø|\£¿…A™F“£¿Šuª|ÏH¤¿`Ë+×Ûf¢¿}“EÖ¢¿Pj’Ì¢¿„¸röÎh£¿…`U½üN£¿)x ¹RÏ¢¿áE_Aš± ¿&Ž<Y¤¡¿W•}Wÿ£¿OÎPÜñ&§¿wÐ}9³¥¿^.â;1륿Œ¢>+¦¿rjg˜ÚR§¿ Xr‹ß¤¿›–>tA}£¿öî�÷ª•¡¿ñH¼<�+¢¿‹N–Zï7¢¿� ܺ›§¢¿;‡ú]Ø¢¿=×÷á !¢¿WA tí  ¿K�¦z2ÿ ¿?qý¾£¿—ýºÓ�'¦¿—Ép<Ÿ¥¿è1Ê3/‡¥¿�mp–’¥¿:“6U÷Ȧ¿ÞXP”i¤¿©‡ht±£¿cÓJ!�K¤¿ô3õºE`¤¿}=_³\6¢¿kšwœ¢#¡¿¨§�À~¦¿î²_wºó¤¿{ö\¦¦¿�B’Y½Ã¥¿ðÝæ�“œ¿óüÄô›¿3¸<ÖŒœ¿Gu:�õÔš¿óÈ <÷ž¿<0€ð¡D›¿Ø‚ÞC ¿´Éá“N$ ¿Ǻ¸� ¿Øñ_  ¿žî<ñœ- ¿ÜõÒNŸ¿6[yÉÿ䟿[\ã3Ù?Ÿ¿ª�æsîv�¿xB¯?‰Ï�¿¬Å9êè ¿‹Ã™_Í¢¿ƒ3øûÅl¡¿áFÊI»¡¿4Ûú`£¿ˆ»z�¿B•š=Т¿ò$éšÉ7£¿p%;6ñ¢¿$cµùÕ¡¿I‚p¢¿ÿ< $}¢¿,~SX© ¢¿»(zàc°¢¿P�mp–¢¿j…é{ Á¡¿�d=µú¢¿äL¶ŸŒ¡¿×ž—Š�¡¿v5yÊj¢¿É¬Þávh ¿Ù^ zo ¡¿êX¥ôL/¡¿ûzáÎ…¡¿ k_@/Ü¡¿Z+Úç6¡¿m­/Úrž¿Ùƒkî蟿HÅÿQ¡¢¿´Yõ¹ÚŠ¥¿ÒÂe6¤¿ö™³>嘤¿DÞrõc“¤¿rûå“Ã¥¿a§X5s£¿m±Ÿ¢¿½qR˜÷8£¿0º¼9\£¿—¨ÞØ*¡¿›�ÖtB ¿J›ª{ds¥¿q©J[\㣿Ýa™¹À¥¿¹ÿÈtèô¤¿¡ÚàDôk›¿z0Hú´š¿ˆñšWuV›¿,šÎNG™¿ÁgÓÀ�¿°âÊ™¿ÐÔë�±ž¿kð¾**Ÿ¿…ÏÖÁÁž¿nÃ(ßž¿yÎZŸ¿Î‹_í(ž¿¹Qd­¡Ôž¿ÐÒl#ž¿ 4ØÔyTœ¿û¯sÓfœ¿‹ßV* ¿ÃcÒßK¡¿ÿunڌӠ¿L£uT5¡¿ûå¶}�¢¿”‚Uõò›¿%!‘¶ñ'¢¿›©¾ó‹¢¿´Ë·>¬7¢¿ò"ðk$¡¿”ú²´Ss¡¿#0Ö70¹¡¿Ã,´sš¢¿ÙYLü¡¿}ZEhæ¡¿Üx`¡¿‘í|?5^¢¿¨ŒŸqá ¿û“ýó4 ¿ßˆîY×h¡¿ ¹RÏ‚Pž¿# Â¤Rœ¿A�wòé±�¿Mh’XRîž¿Œ�u?Tš¿wLÝ•]0˜¿c€D(b¡¿ÿÌ >°ãŸ¿^d~�$¡¿U¡�X6s ¿á³up°7‘¿’ñ+Öp‘¿ŒgÐÐ?‘¿ñ�ÿ‚�¿ž#ò]J]’¿2U0*©�¿<ˆ�)t^“¿�³”,'¡”¿ÄC?�{“¿^, ‘Óד¿`æ;ø‰“¿í}ª Ä’¿ÖqüPi”¿*T7Û“¿k›âqQ-’¿&¬�±^’¿,F]kïS•¿Å.rOW—¿ÅçN°ÿ:—¿º¢”¬ª—¿BCÿ+š¿ÿXˆ�#‘¿MØ~2Ƈ™¿æ=Î4aû™¿ª-u�׃™¿°71$'—¿âåé\QJ˜¿YP”i4™¿Öã¾Õ:q™¿õ»°5[™¿½Ç™&l?™¿Ž•˜g%­˜¿þœ0a4›¿³–Òþ˜¿_y�ž"‡˜¿*ãßg\˜¿îx“ߢ“•¿`äeM,ð•¿U£W”–¿^ºI +—¿�"1ì0–¿Eb‚¾…•¿ ÚäðI'’¿v‹ÀXßÀ”¿z‹‡÷˜¿‹5\äž®ž¿n…°KX›¿Z5Ñ磜¿^J]2Ž‘œ¿¶KKŸ¿È^ïþx¯š¿bg �ט¿/ùŸüÝ;š¿9 Q…?Û¿KZñ …Ï–¿UDÝ •¿ßû´WŸ¿$ nk Ï›¿U÷Èæªyž¿»H¡,|}�¿jßÜ_=î‹¿Œ 1“¨Œ¿�„}‹¿hæÉ52‹¿ýˆ_±†‹Œ¿BÍ�*ŠW‰¿Ó hÀ"�¿<À“.«�¿ ’>­¢�¿/lÍV^ò�¿G�ßÛôg�¿ ²eùº �¿(Ö©ò=#‘¿Ô_¯°à~�¿Á7MŸp�¿æ‘?xî�¿w0bŸŠ‘¿ZÊû8š“¿´up°71”¿Eóùõ“¿p>u¬Rz–¿i¬ý�íÑ‹¿g~5–¿±ÁÂIš?–¿xšÌx[é•¿H3Mg'“¿ªc•Ò3½”¿™Iô2Š•¿A tí è•¿ÎPÜñ&¿•¿,)wŸã£•¿5¸­-!;oc³“¿(îx“ߢ“¿°«ÉSVÓ•¿Ön»Ð\§‘¿‘`ª™µ�¿.â;1ëÅ�¿Q»_øn“¿ŒòÌËa÷�¿ñ+Öp‘{Š¿HÁSÈ•z–¿‰|—R—Œ“¿§@fgÑ;•¿K¯ÍÆJÌ“¿Û4¶×‚Þk¿hY÷�…è`¿¥È%Ž‹¿¼“O�m�¿'…y�3Mˆ¿€FéÒ¿$…¿›T4ÖþΆ¿Û4¶×‚Þ‹¿�I›ª{dƒ¿€I*SÌA€¿Œ�ðœú�¿qä�È"MŒ¿=·Ð•T�¿+é~NAŽ¿¹¥Õ�¸Çr?j’Ìê}?—Mõdþ�?øí¸áws?#ÜdTÆ}?ÅÅQ¹‰Zz?ßøÚ3Kt?ù�«<�°s?VðÛ€?µÆ B}? IJ™Cr?Áªzù�&ƒ?ÁäF‘µ†‚?2Tqãƒ?ÂøiÜ{?SçQñG„?oG8-xч?¨½ 4t?oÓŸýHy?Ï…‘^Ôîw?¯#Ù@ºx?“p!� ?#½¨Ý¯¤?x%És}¦? Ùy›¡?ŠWYÛ�£?_ Pj¢?ïW¾Û¼¡?¹Ä‘"£?â¦Óº ¢?Ð}9³]¡?K?ªaŸ?šEó¡?8öì¹LM¢?™Kª¶›à£?Á�”-’¦?¿ÔÏ›ŠT ?ðN>=¶e ?øª• ¿ÔŸ?›sôø�?†9A›>¡?Œd�P3¤š?éahur†¢?fŸÇ(ϼœ?©iÓœ?ÐÒl#žœ?ÐCmFA ?Î’Z(™œ?YÜd:tš?�ñ}q©J›?äº)嵚?×¢h[Íš?&S£’š?>u¬Rz¦—?)Ð'ò$éš?ææÑ=ëš?/‡Ýw ��?îw( ô‰œ?\8’L ?%@7n¡?÷åÌv…> ?ÃDƒ<…œ?1yÌ|Ÿ?çp­ö°¢?j½ßhÇ Ÿ?L©KÆ1’�?tÐ%z‹—?¶÷©*4›?¸uÊ£›?NE*Œ-™?¦ï5Ç•?J$ÑË(–›?ƒÜE˜¢\š?lÏ, PS›?DŠM ˜?�zˆFw›?¿îtç‰çœ?Ív…>XÆ–?MóŽSt$—?Å.rOW—?›çˆ|—R—?†§WÊ2¬?$Ó¡Óón°?¨á[X7Þ±?¢ñDçá¬?O�?üü¯?:êè¸Ù­?œ6ã4D®?ÓõD×…°?ˆ‚S°®?lyåzÛL­?€ôMšE«?µ4·BX­?iÅ7>[¯?ŒºÖÞ§ª°?¸;k·]h²?e¦´þ–¬?ý»>sÖ§¬?Ðïû7/N¬?Étèô¼«?¼t“V®?»DõÖÀV©?I×L¾Ùæ®?]øÁùÔ±ª?ÝéÎÏÙª?´V´9Îmª?*ý„³[ˬ?ö á˜eOª?’±Úü¿ê¨?/3l”õ›©?³´Ss¹Á¨?_—á?Ý@©?Bêvö•©?Ùz†p̲§?žz¤Ámm©?,ÒSä©?d"¥Ù<«?-`·îæ©?-Ðî�b€¬?'ÛÀ¨S®?•ص½Ý’¬?Çž=—©?³Z�!««?Ø»?Þ«V®?D5%Y‡£«?ó9w»^šª?µ¤£Ì&¨?…™¶e¥©?“6U÷Èæª?Ê7Ûܘž¨?ã©GÜÖ¦?æ=Î4aû©?‚âǘ»–¨?¿}8gD©?ŽDÁŒ)¨?•AÕèÕ¨?êvö•é©?ïU+~©§?u=Ñuá§?+TTýJ§?…?Û5x§?îAÈ—Pµ?pµN\ŽW¸?Á­»yªCº? pzïǵ?¿¶~úÏš·?o·$ìj¶?~p>u¬¶?… £YÙ>¸?ÁR]ÀË ·?AG«ZÒQ¶?RóUò±»´?Ûú`¶? EºŸS�·?îÑZÑæ¸?§ærƒ¡»?.É»š<µ?y!Âøµ?R›8¹ßµ?‡5•Eaµ?½ÅÃ{,·?± ØF´?«{dsÕ<·?[œ¥d9µ?²G¨REµ?IJ™CR µ?�:è¶?ÀÍâÅ´?JÔ >Íɳ?E‚©fÖR´?ÌDR·³³?ûëÜ´? Šæ,ò³?4/‡Ýw ³?çfh<´?æ?¤ß¾´?Ë¡E¶óý´?ÏŽYö$´?.;Ä?léµ?�f�v‡·?ÕÎ0µ¥¶?‰ @£té³?£>+Nµ?ZòxZ~à¶?ׇõF­0µ?ÎOqxµ´?>>!;oc³?¬W‘ÑI´?cÐ ¡ƒ.µ?d@öz÷dz?À”�Zº²?]¥»ël´?�¡¸ãM~³?Ý–Ègð³?ëåwšÌx³?*q㊳?óT‡Ü 7´?¤8GW³?çá¦Ó²?3¨68ý²?‰xëüÛe³?7À[ A½?”£Q0cÀ?ë-z¨Á?:>Zœ1̽?Ð}9³]¡¿?¿3‰¾?pÑÉRëý¾?^ò?ù»wÀ?¢ ÀDˆ¿?SAEÕ¯t¾?‹3†9A›¼?`sž M¾?Ã.ŠøÀ?8 ¥+ØÀ?•�~P)Â?Þv¡¹N#½?ú™zÝ"0¾?d¬6ÿ¯:¾?˜//À>:½?qvk™ Ç¿?V(Òýœ‚¼?Q¿ [³•¿?`L8 ½?ƒjƒѯ½?)øùÁ?Iõ�_” Ã?/Šø¬Â?81$'·Â?àªÔì�Â?«w¸Ã?‰˜Iô2Â?á–�¤¤‡Á?,bØaLúÁ?í›û«Ç}Á?ø¨¿^aÁÁ?Œ¹k ù Á?…\©gA(Á?˜õIî°Á?ó�)‚ªÁ?´CV·zÂ?¥J”½¥œÁ?ËI(}!äÂ?ä-W?6ÉÃ?†®D úÃ?–~TÃ~Á?.ÿ!ýöuÂ?“�x]Ã?B"mãOTÂ?œŒ*øÂ?b‚¾…uÁ?ìQ¸…ëÁ?‹‹£rµÂ?Ý ö_ç¦Á?lBZcÐ Á?Ò­£ª Â?¾ø¢=^HÁ?B°ª^~§Á?9ÔïÂÖlÁ?[\ã3Ù?Á?ïÿã„ £Á?ó-$`tÁ?ܼqR˜÷À?ä²ó66Á?e4òyÅSÁ?²¾�É�"Å?0/À>:uÇ?õ¡ ê[æÈ?LUÚâŸÅ?ó¬¤ßPÆ?Ñ‘\þCúÅ?( Ê4š\Æ?®ïÃAB”Ç?—�z6«Æ?q­ö° Æ?ò•@JìÚÄ?иp $ Æ? ‘Ó×ó5Ç?LÃð1%È?µŒÔ{*§É?X«vMHÅ?„~¦^·Æ?€E~ýÆ?:Yj½ßhÅ?ÆOãÞüÆ?¡Ÿ©×-Å?• •-¯Æ?üã½jeÂÅ?¸Ë~ÝéÎÅ?Lÿ’T¦˜Å?£uT5AÆ?î“£Q0Å?Ü×�sF”Ä?Ä\RµÝÅ?ØœƒgB“Ä?š#+¿ ÆÄ?E» )?©Ä?¹ß¡(Ð'Ä?ç6á^™·Ä?’Z(™œÄ?…A™F“Å?†q7ˆÖŠÄ?†W’<×÷Å?Êõ�îÆ?ø�¯=³$Æ?ê]¼·_Ä?¤8GWÅ?^ò?ù»wÆ?v¥e¤ÞSÅ?©|š“Å?j£:ÈzÄ?šÏ¹ÛõÄ?Œ‰B˺Å?7þDeÚÄ?QúBÈyÿÃ?]‡jJ²Å?jhwH1Ä?‹‡÷XŽÄ?ÈÓòWyÄ?HÞ9”¡*Ä?¦�šË †Ä?ÇL¢^ðiÄ?þ)U¢ìÃ?¬©, »(Ä?žðœú@Ä?X}w+Ç?h%­ø†ÂÉ? 1—Tm7Ë?éCÔ·ÌÇ?l$ ÂPÈ? µ‰“ûÈ?`Í‚9zÈ?­K�ÐÏÔÉ?hÐÐ?ÁÅÈ?“EÖÈ?Šå–VCâÆ?¹ß¡(Ð'È?0/À>:uÉ?(bÃcÊ?!ä¼ÿ�Ì? ƈD¡eÇ?0¡‚à "È?ÿ‚:È?ò]J]2ŽÇ? Ý%qVDÉ?Št?§ Ç?bX9´ÈÈ?9 {ÚáÇ?ï«r¡òÇ?ˆÖŠ6ǹÇ?¥hå^`È?ê ¼“OÇ?±¨ˆÓI¶Æ?`“5ê!Ç?‹5\äž®Æ?±1¯#ÙÆ?ÛÝt_ÎÆ?U[rPÆ? )"Ã*ÞÆ?;Ä?léÑÆ?eÚʢ°Ç??tA}ËœÆ?Ä QºôÇ?#1ì0&É?º/g¶+È?¿Ö¥FègÆ?·³¯Ëóàî¬É? fLÁË?™€_#IÊ?·ð¼TlÌÇ?ÿ˵hÚÈ?¸¶J°8Ê?ÓNÍåCÉ?Ž•˜g%­È?xÑW�f,È?UQ¼ÊÚ¦È?5— uXÉ?çû©ñÒMÈ?THÞ9”Ç?ÀêÈ‘ÎÀÈ?‹à+Ù±Ç?&¤à)È?Ì|?qÈ?¤P¾¾ÖÇ?<1ëÅPNÈ?ÀJÈ?Âj,amŒÇ?hyÜ�µÇ?]†ÿtÈ?“S;ÃÔ–Ê?µkBZcÐÍ?1Ñ O!Ï?ò_ �¡Ë?¸uÊ£Ë?=œÀtZ·Ë?�¹‡„ïýË?ݵ„|гÍ?iÄÌ>�QÌ?¿ ƈD¡Ë?èKo.Ê?ê?k~ü¥Ë?ŒgÐÐ?Í? oÖà}UÎ? ´¾LÐ?c™~‰xëÊ?'iþ˜Ö¦Ë?IM»˜fºË?ÑZÑæ8Ë?mìM Í?ÃH/j÷«Ê?¼viÃaiÌ?©ƒ¼LŠË?Zd;ßO�Ë?�ŒÕæÿUË?² Ü�:åË?]þCúíëÊ?•ŸTût<Ê?Ì™í }°Ê?þÒ¢>ÉÊ?˜4Fë¨jÊ?|ñE{¼�Ê?Øñ_ Ê?V,~SX©Ê?ï¨1!æ’Ê?„�çÞÃ%Ë?èKo.Ê?È~K‘|Ë?î\éEíÌ?ªºG6WÍË?‹O0žAÉ?ð¦[vˆÊ?ÍV^ò?ùË?UDÝ Ë?NÑ‘\þCÊ?•›¨¥¹É?Ô€AÒ§UÊ?é}ãkÏ,Ë?¸#œ¼èÉ?‘ ÎàïÉ?5ð£ö{Ê?Ä?léÑTÉ?*T7ÛÉ?¯@ô¤LÊ?�c${„šÉ?õ�ä�CÊ?m±ŸÊ?¬ª—ßi2É?´"j¢ÏGÉ?oóÆIaÞÉ?¦*mq�ÏÌ?׆Šqþ&Ð?&Ãñ|ÔÐ?p&¦ ±úÍ?€óåØÍ?ÂO@¿ïÍ?aÅ©ÖÂ,Î?a§X5Ð?1·{¹OŽÎ??qý¾Í?„€| Ì?Eh׿Í?y>êͨÏ?Ç¡~¶fÐ?À°üù¶`Ñ?Q‚þB�Í?föyŒòÌÍ?=}þðóÍ?Æù›Pˆ€Í?Ìa÷ÃcÏ?t ò³‘ëÌ?ò`‹Ý>«Î?&Ãñ|ÔÍ?\vˆØÒÍ? 4Ô($™Í?Ã.ŠøÎ?„-vû¬2Í?Ó.¦™îuÌ?KÊÝçøÌ?. ´¾LÌ?ø¥~ÞT¤Ì?•»ÏñÑâÌ?1[²*ÂMÌ?AÕèÕÍ?÷«ßmÞÌ?šë4ÒRÍ?!”÷q4GÌ?ÜØìHõ�Í?á ½þ$>Ï?ò˜�Êø÷Í?J•({K9Ë?÷™�Ì?\Y¢³Ì"Î?겘Ø|\Í?†è8hÌ?^fØ(ëË?e73úÑpÌ?]lZ)rÍ?És}Ì?8ó«9@0Ë?ó‘”ô0´Ì?·a�oË?Ù[ÊùbïË?\qqTn¢Ì?5Ô($™ÕË?�ô¾ñµgÌ?"ʼn&PÌ?~q©J[\Ë?f»B,cË?�ÙYôNÌ?4hèŸàbÏ?§Ï¸®˜Ñ?úñ—õIÒ?MùT�^Ð?·]h®ÓHÐ?£’:MÐ?õc“üˆ_Ð?¶†R{mÑ?›É7ÛܘÐ?Ÿp]1#Ð?Ð?šEóÑ?PŽDÁŒÏ?çà™Ð$±Ð?IVñFÐ?SçQñGÐ?åì�ÑV%Ð?rö´Ã_Ð?ƒn/iŒÖÏ?u“VÏ?píDIH¤Ï?EœN²ÕåÎ? $}ZEÏ?Ÿ­ƒƒ½‰Ï?î§/úÎ?L8 ¥Ï?kÕ® i�Ï?›Z¶Ö Ð?ð‹KUÚâÎ?,f„·!Ð?,¸ðÀÑ?q�¥]PÐ?0du«ç¤Í?+½6+1Ï?®ºÕ”dÐ?eS®ð.Ð?霟â8ðÎ?Ê1YÜdÎ?¡Ÿ©×-Ï?¡÷ÆÐ?+ùØ] ¤Î?-°ÇDJ³Í?ÎÁ3¡IbÏ?Ä[çß.ûÍ?öA–Î?p¶¹1=aÏ?;‰ÿ"hÎ?¼æU�ÕÏ?0žACÿÏ?!±Ý=@÷Í?l•`q8óÍ?ËhäóЧÎ??�{óÑ?kïSUh Ó?g|_\ªÒÓ?²×»?ÞÑ?Êß½£ÆÑ?hz‰±L¿Ñ?\È#¸‘²Ñ?Q¡º¹øÛÒ?=Òà¶¶ðÑ?Ó/o�Ñ?2<ö³XŠÐ?å`6†Ñ?pî¯÷­Ò?Xþ|[°TÓ?=ƒù+dÔ?ËfI-Ñ? uXá–Ñ?ð1XqªµÑ?YÞU˜‡Ñ?„�/¡‚Ò?!çýœ0Ñ?H2«w¸Ò?ßÁO@¿Ñ?®óo—ýºÑ?7§’ Ñ?ȶ 8KÉÑ?ŒÖQÕQÑ?%?âW¬áÐ?5cÑtv2Ñ?Pj’ÌÐ?ƒ÷U¹PùÐ?Täqs*Ñ?õKÄ[çßÐ?!=E7Ñ?ƒQI�€&Ñ?Zd;ßOÑ?ÒŒEÓÙÉÐ?Tr3Ü€Ñ?ÝÓÕ‹mÒ?û&7ЬÑ?äGˆ,Ð?¤ÿåZ´Ñ?·ð¼TlÌÑ?L¥ŸpvkÑ?¥,CëâÐ?²,˜ø£Ð?Ã)só�èÐ?Ô)�n„Ñ?en¾ݳÐ?’?xî=Ð?Q.�_x%Ñ?(c|˜½lÐ?ÅS�4¸­Ð?Ë+×Ûf*Ñ?5Ïù.¥Ð?0�[wóÐ?+Üò‘”ôÐ?yxÒÂeÐ?NÕ=²¹jÐ?ŒgÐÐ?ÁÐ?²Õ唀˜Ò?Ön»Ð\§Ô?©MœÜïPÕ?>ê¯WXpÓ?Ï:¯±KÓ?î?2:=Ó?¬ª—ßi2Ó?>>!;ocÔ?v©ú™zÓ?÷@øÒ? ¦šYKÒ?9™¸UÓ?ù½Mö#Ô?ûèÔ•ÏÔ?œ‡˜NëÕ?ù�«<�°Ò?¢&ú|”Ó?ÛÂóR±1Ó?1ÏJZñ Ó?ìø/Ô?-¯\o›©Ò?î"LQ.�Ó?„-vû¬2Ó?lv¤úÎ/Ó? ‚Ç·w Ó?C9Ñ®BÓ?]Þ®ÕÒ?�Ž«‘]iÒ?sôø½Ò?{ò%TÒ?ëæâo{‚Ò?u¯“ú²´Ò?ê^'õeiÒ?Ü~ùdÅÒ?l^ÕY-°Ò?L5³–ÒÒ?geû�·\Ò?» ”XÓ?þ oÖàÓ?)²ÖPj/Ó?ÜÖž—ŠÑ?”ú²´SsÒ?J•({K9Ó?ÙÎ÷SãÒ?Š}"OÒ?h^»ïÒ?d°âTkaÒ?cò˜ùÓ?�ÓUø3Ò?‘Жs)®Ñ?ôlV}®Ò?]S ³³èÑ? <÷.Ò?ðÜ{¸ä¸Ò?­/Úr.Ò?Ê4š\Œ�Ò?p”¼:Ç€Ò?L8ôïÑ?dsÕ<Õ?{£V˜¾×Ô?g �ר%Õ?w;Sè¼Ô?õ¢v¿ ðÔ?x|{× /Õ?ö{b�*ßÔ?Â26t³?Õ?ØaLú{)Õ?gaO;ü5Õ?Ó¼ãÉÔ?¤9²òË`Õ?&©L1AÖ?w1Ít¯“Õ?’]i©÷Ó?�N]ù,ÏÔ?âÈ‘EšÕ?:¸Y¼XÕ?l’ñ+ÖÔ?rÌ_!sÔ?éï¥ð ÙÔ?Ó0|DLÕ?_zûsÑ�Ô? ¦šYKÔ?ËMÔÒÜ Õ?®¹£ÿåZÔ?Â&S£Ô?@i¨QH2Õ?nj ùœ»Ô?g+/ùŸüÔ?K±£q¨ßÔ?—¬Šp“QÔ?Ød�zˆFÔ?ßmÞ8)ÌÔ?€^»´áÕ?ÚYôNÜ×?²žZ}uUØ?û?‡ùò×?‚Êø÷Ö?†p̲'�Ö?^h®ÓHKÖ?.ÿ!ýöu×?Ê4š\Œ�Ö?]2Ž‘ìÖ?A¼®_°Õ?•œ{hÖ?G°qý»>×?_9ïÿã×? ¦–­õØ?’]i©÷Õ?ßRÎ{/Ö?˜Û½Ü'GÖ?²HïOÖ?:Ì—`×?î#·&ÝÕ?æCV¸Ö?jLˆ¹¤jÖ?™cyW=`Ö?Ä>#KÖ?:¸Y¼XÖ?¢ÏGqÖ?­À�Õ­žÕ?PÈÎÛØìÕ?—6–~Õ?N·ìÿ°Õ?L5³–Ö?� ø1æ®Õ?Ý—3ÛÖ?Š?Š:sÖ?€É�cëÕ?Àx Õ?ÇeÜÔ@Õ?pÐ^}<ôÕ?òÓ¸7¿aÕ? JÑʽÕ? ¼“O�Õ?Àw›7N Õ?ê!ÝAìÔ?‘™ \kÕ? ÇóPoÖ?‹T[rØ?ÎR²œ„ÒØ?U.Tþµ¼×?®f�ñ}qÖ?äž{×?ÃIš?¦µÖ?0œk˜¡ñ×?EóùõÖ?û¬2SZÖ?$Õw~Q‚Õ? jøÖ�Ö?ºùFtϺ×?wÖn»Ð\Ø?£�dVïpÙ?�¯èÖkzÖ?”Ù “ŒœÖ?õñÐw·²Ö?Ü�:åÑÖ?~RíÓñ˜×?Ù—l<ØbÖ?²ó66;×?CV¸åÖ?=)“ÚÖ?ÁÅŠLÃÖ?ÉæªyŽÈÖ?ʉvR~Ö?i:;%Ö?Á=~oÖ?…(_ÐBÖ?@7n1Ö?ùNÌz1”Ö?�Ÿqá@Ö?¤aQ§Ö?C®Ô³ ”Ö?h@½5_Ö?c%æYI+Ö?ÙvÚŒÖ?›Ça0…×?�B’Y½ÃÖ?ì…¶ƒÕ?¸®˜ÞÕ?*¨¨ú•ÎÖ?z›©¾Ö?Ä&2s�ËÕ?}!ä¼ÿ�Õ?UÛMðMÓÕ?$~Å.rÖ?¥ö"ÚŽ©Õ?‹ßV*Õ? åD» )Ö?5ð£ö{Õ?ò DOʤÕ?:\«=ì…Ö?ý¡™'×Õ?"Ã*ÞÈ<Ö?öóåÖ? ¾iúì€Õ? dv½SÕ?·œKqUÙÕ?yGsdåÖ?;q9^�èØ?åïÞQcBÙ?åîs|´8Ø?v‹ÀXßÀÖ?ºóÄs¶€×?¦`�³é×?¯]ÚpXØ?'ÛÀ¨S×? ž^)ËÖ?„H†[ÏÕ?à ½úxèÖ?E�>‘'Ø?Ò7iÍØ?dsÕÖ?Aœ‡˜N×?‚X6sHjÙ?É:]¥»Ù?0fKVE¸Ø?8ó«9@0×?ôNÜóü×?�½Þýñ^×?³]¡–±Ø?˜Št?§×?ØaLú{)×?N ^ôÖ?VGŽtF×?@N˜0š•Ø?‚Uõò;Ù?åñ´üÀUÚ?î—OV W×?${„š!U×?Ê2ı.n×?m:¸Y¼×?_DÛ1uWØ?[°Tð2×?Ü,^, Ø?£¢ÑÄ×?¾Þýñ^µ×?EºŸS�Ÿ×?ÚÊKþ'×?\:æçn×KS×?+iÅ7>×?@h=|™(×?¼’ä¹¾×?}¯!8.ãÖ?~Q‚þB�Ö? –\ÅâÖ?—®`ñdÖ?®ð.ñ�Ö?kF¹‹0×?ÿunÚŒÓÖ?) ‰´�?×?á² ›.×?~©Ÿ7©Ö?jûWVš”Ö?Ú¬ú\mÅÖ?be4òyÅ×?ZžwgíÖ?…[>’’Õ?ÀÎM›qÖ?�C¨R³×?éÓ*úC3×?J_9ïÿÕ?Ѐz3j¾Õ?}?qýÕ?øRxÐìºÖ?ù…W’<×Õ?w£�ù€@Õ?ðû7/N|Ö?§Y Ý!ÅÕ?ÛÝt_ÎÕ?¬Žé ×?ëâ6À[Ö?áÎ…‘^ÔÖ?¦E}’;lÖ?å³<îÎÕ?¯–;3ÁpÕ?¡¼�£9Ö?¾¼ûèÔÕ?¬Å9êè×?ÝïPèØ?©|š“×?÷.9î”Õ?ðû7/N|Ö?¾½kЗÞÕ?DN_Ï×,×?ãŒaNÐ&Ö?7ÿ¯:r¤Õ?B‡D¤Ô?4÷�ð½¿Õ?uèô¼ ×?c¶dU„›×?ýfbº«Ø?RÑXû;ÛÕ?@/ܹ0ÒÕ?T­…YhçÕ?Ë,B±4Ö?aà¹÷pÉÖ?ŪA˜Û½Õ?Å °rh‘Ö?²G¨REÖ? a°ä*Ö?*á ½þ$Ö?K;5— Ö?V ì1‘ÒÕ?oe‰Î2‹Õ?vâr¼ÑÕ?q:ÉVÕ?VÔ`†�Õ?ñ�˜õb(Ö?ÊI»ÑÇÕ?{3j¾J>Ö?Mž²š®'Ö?x¸£Õ?˜½l;m�Õ? ȳ˷Õ?SÌAÐѪÖ?°Y.�óÕ?ߦ?û‘"Ô?ÁþëÜ´Õ? B²€ Ö?�ˆ)‘D/Ö?¯½7†Õ?�N]ù,ÏÔ?Å8 Õ?ÌDR·³Õ?gÔ|•|ìÔ?Ö‹¡œhWÔ? ñ+Öp‘Õ?nmáy©ØÔ?xñ~Ü~ùÔ?yöÑ©+Ö?„%ZòxÕ?N`:­ÛÕ?"úµõÓÕ?¿ 1^óÔ?“QewƒÔ?�¨PÝ\Õ?-@ÛjÖÔ?¡­Ü ÌÖ?b.©ÚÖ?nøÝtËÖ?_zûsÑ�Ô?‡Ä=–>tÕ?ñ¸¨ÅÔ?•!ÿÕ?<ùôØ–Õ?yæå°ûŽÔ?ãOT6¬©Ó?#…²ðõµÔ?A ]ÞÕ?ò(•ð„^Ö?Ù@ºØ´R×?>$|ïoÐÔ?û²´Ss¹Ô?¡i‰•ÑÈÔ?ú—¤2ÅÕ?¡º¹øÛžÕ?¤Æ„˜KªÔ?þµ¼r½mÕ?ެü2#Õ?xµÜ™ Õ?W•}WÕ?Çdqÿ‘éÔ?cdÉË»Ô?›ÆöZÐ{Ô?J}YÚ©¹Ô?*嵺KÔ?ú'¸XQƒÔ??6É�øÕ?w;Sè¼Ô?`” ¿Ð#Õ?Õz¿ÑŽÕ?­iÞqŠŽÔ?°Éõ�Ô?qÈÒŦÔ?p•'vŠÕ?ÂMF•aÜÔ?—¬Šp“QÓ?�:èÔ?›:�ŠÿÔ?§“lu9%Õ?Ô�'ž³Ô?éî:òÏÓ?dw�’ Ô?ž~P)”Ô?ˆ0~÷æÓ?ÈÑYùeÓ?ÁãÛ»}Ô?¾½kЗÞÓ?‰a‡1éïÓ?’‘³°§Õ?¸’�xÔ?RÑXû;ÛÔ? ÐÒlÔ?éñ{›þìÓ?î"LQ.�Ó?`ÈêVÏIÔ?GV~ŒÔ?ù1æ®%äÕ?ÑËØÐÍÕ?÷:©/K;Õ? JÑʽÓ?Ç,{ØœÔ?ú ¨7£æÓ?pUjöÔ?ƒú–9]Ô?× /½ý¹Ó?›WuV ìÒ?é·¯çÓ?Ók³±óÔ?ÉV—SbÕ?6>“ýó4Ö?´r/0+Ô?Ý@�wòéÓ?N˜0š•íÓ?È#¸‘²EÔ?ñ„^ŸÔ?°® ÔbðÓ?MÛ¿²Ò¤Ô?Ô+eâXÔ?Z,Eò•@Ô?PV W@Ô?aÂhV¶Ô?ÕËï4™ñÓ?iþ˜Ö¦±Ó?˜öÍýÕãÓ?¾.ú�Ó?B±4-±Ó?—«›äGÔ?÷@øÓ?¯]ÚpXÔ?½á´àEÔ?ƒÁ5wô¿Ó?Ú×3ÂÓ?"«[='½Ó?õiý¡™Ô?–B —8òÓ?4œ27߈Ò? ˆI¸�GÓ?•�~P)Ô?üǙ&lÔ?œQ}>Ó?÷¬k´èÒ?CÉäÔÎ0Ó?ïTÀ=ÏŸÓ?IÛø• Ó?�l®šçˆÒ?àëTùžÓ?[}uU Ó? :!tÐ%Ó?â:ÆGÔ?yé&1¬Ó?ÒyYÔ?äL¶ŸŒÓ?žíÑî#Ó?v4õ»°Ò?n‰\pÓ?ª x™a£Ó?Ó0|DLÕ?6å ïrÕ?¼Yƒ÷U¹Ô?Êû8š#+Ó?>±N•ïÔ?n…°KXÓ?Eƒ<…\Ô?ºJw×Ù�Ó?±k{»%9Ó?Ëd8žÏ€Ò?]kïSUhÓ?�½Þýñ^Ô?z›©¾Ô?Q½5°U‚Õ?0™ò!¨Ó?L¤4›ÇaÓ?ßÜ_=î[Ó?8ºJw×ÙÓ?ÍYŸrLÔ?²Jé™^bÓ?†Œ.oÔ?J)èö’ÆÓ?L8 ¥Ó?#Ù#Ô ©Ó?}¢|Ó?ˆ×õ vÓ?›�ÖtBÓ?Y†8ÖÅmÓ?n£¼Ó?“o¶¹1=Ó?�š¯’�ÝÓ?HnMº-‘Ó?*¬TPQõÓ?š²ÓêÓ?H5ì÷Ä:Ó?_ðiN^dÓ?KrÀ®&OÓ?IÚ�>æÔ?‚ã2njÓ?i�Òá!Ò?æ°ûŽá±Ò?èÝXP”Ó?7R¶HÚÓ?}$%= ­Ò?ÑèbgÒ?…î’8+¢Ò?“�[ìöÒ?X­Lø¥~Ò?Ž Ò?xµÜ™ Ó?°t>Ò?ÈCßÝÊÓ?…]=ðÓ?‚©fÖR@Ô?Í#0ðÜÔ?O²žZ}Ó?—wJÓ?Ë¡E¶óýÒ?ËgyÜ�Ó? ‡3¿šÓ?=Ô¶aÓ?9 {Úá¯Ó?óZ Ý%qÓ?Y§Ê÷ŒDÓ?{ò%TÓ?Ș»–�Ó?<„ñÓ¸7Ó?$î±ô¡ Ó? 1—Tm7Ó?ÖsÒûÆ×Ò?+3¥õ·Ó?§îyþ´Ó?4iSu�lÓ?Þ’°«ÉÓ?(œÝZ&ÃÓ?ƒS°ÆÙÒ?ÜõÒNÓ?órØ}ÇðÒ?á—úyS‘Ó?”i4¹Ó?6Y£¢ÑÑ?÷”œ{hÒ?@i¨QH2Ó?st´ªÓ?Œ„¶œKqÒ?Ì(–[Z Ò?ž$]3ùfÒ?Ô¸7¿a¢Ò?þÕã¾Õ:Ò?—<ž–¸Ñ?ù‚0ºÒ?穹nÒ?ƒÜE˜¢\Ò?"ýöuàœÓ?Öwõ*Ó?aU½üN“Ó?-²�ï§Ò?Q§“luÒ?$cµùÕÑ?î•y«®Ò?X9´ÈvÓ?ÚâŸÉþÔ?\ætYLlÔ?®œ½3ÚªÔ?®ž“Þ7¾Ò?¸#œ¼èÓ?%‘}�eÁÒ? ò³‘ë¦Ó?ƒ¤O«èÒ?K«!q�¥Ò?Gªïü¢Ò?€d:tzÞÒ?”M¹Â»Ó?øÃÏÔ?¾2oÕu¨Ô?é×ÖOÿYÓ?*¨¨ú•ÎÒ?�Ù®ÐËÒ?H›V �Ó?Ìa÷ÃcÓ?üÖMõÒ?Ž<Y¤‰Ó?lÎÁ3¡IÓ? `­Ú5!Ó? <÷.Ó?ÝéÎÏÙÒ?´qÄZ| Ó?s…w¹ˆïÒ?Íù†Ó?É:]¥»Ò?Ý@�wòéÒ?7TŒó7¡Ó?÷=ê¯WXÓ?9 æ¯�¹Ó?¼Ñ“2©Ó?}ÎÝ®—¦Ò?öE™ 2Ó?4÷�ð½¿Ò?z7eÓ?ÊÂ×׺ÔÒ?Üóüi£Ñ?˜ˆ·Î¿]Ò?-ÎæÓ?†1zn¡Ó?LàÖÝ<Ò?°âÊÑ?{0)>>!Ò?MØ~2ƇÒ?@‰Ï�`ÿÑ?bÚ7÷W�Ñ?‡ú]ØšÒ?±Â-IIÒ?%Ì´ý++Ò?Ÿ`<ƒ†Ó?)¯•Ð]Ó?ÄíаuÓ?ãǘ»–�Ò?øpÉq§tÒ?ÿêqßj�Ñ?]Åâ7…•Ò?â[X7ÞÓ?è¡¶ £Ô?ª™µ�öÓ?-ÌB;§YÔ? Åoò[Ò?}Ê1YÜÓ?Käõ`RÒ?öµ.5B?Ó?L¨àð‚ˆÒ?IeŠ9:Ò?}ÍrÙèœÑ?‰yVÒŠoÒ?[ÏŽYÓ?‹Š8�d«Ó?ò{›þìGÔ?…zúüÒ?Cÿ+jÒ?W%‘}�eÒ?ÛÁˆ}(Ó?ÞÈ<òÓ?¢+Üò‘Ò?<Øb·Ï*Ó?W>ËóàîÒ?æ®òÂÒ?éî:òÏÒ?( ß÷oÒ?yé&1¬Ò?„fÚþ•Ò?"«[='½Ò?˜3Ûú`Ò?…$³z‡Ò?ò'*ÖTÓ?Ù³ç25 Ó?BzŠ"nÓ?¢´7øÂdÓ?g¹ltÎOÒ?ŸåypwÖÒ?ÅqàÕrgÒ?h°©ó¨øÒ?Ø›’“‰Ò?…]=ð1Ñ?ˆÙ˶ÓÖÑ?ôlV}®Ò?ÜIDøAÓ?.â;1ëÅÑ?( Ê4š\Ñ?¦{�Ô—¥Ñ?Ǻ¸�Ò?Égð÷‹Ñ?ÎQÚÑ?E�>‘'Ò?ZFê=•ÓÑ?Cá³up°Ñ?Ùµ½Ý’Ó?ª¹nÀÒ??áìÖ2Ó?Õ%ãÉÒ?xšÌx[éÑ?U¯²¶)Ñ?NG7‹Ò?â:ÆGÒ?r¦ ÛOÆÓ?ßPølÓ?d:tzÞ�Ó?}vÀuÅŒÑ?_Ñ­×ô Ò?á@H0�Ñ?[¶Ö mÒ?ER·³¯Ñ?YÚ©¹Ü`Ñ?è½ÅÃÐ?mŒ�ðœÑ?á•$Ïõ}Ò?mæ�ÔBÉÒ?-z¨mÓ?bõG,Ò?C®Ô³ ”Ñ?–² q¬‹Ñ?¡Ø š–XÒ?4J—þ%Ò?Û†Q<¾Ñ?KrÀ®&OÒ?œ6ã4DÒ?¹¨ÅäÑ?¦}sõÑ?Ü×�sF”Ñ?áy©Ø˜×Ñ?H¤mü‰ÊÑ?þ)U¢ìÑ?ˆ÷XŽ�Ñ?>Ì^¶�¶Ñ?ˆLùT�Ò?£uT5AÒ?È=›UŸÒ?Q¿ [³•Ò?¦îÊ.\Ñ?‘(´¬ûÑ?ÂzýI|Ñ?Öª]ÒÒ?ˆ jôj€Ñ?ÿ]Ÿ9ëSÐ?¹ªì»"øÐ?¦ÒO8»µÑ?¦E}’;lÒ?é·¯çÐ?‘�·±Ù‘Ð?ßmUÙÐ?qâ«Å9Ñ?ëÇ&ù¿Ð?Þ3ßÁOÐ?áaÚ7÷WÑ? 7àóÃÑ?˜öÍýÕãÐ?bøˆ˜IÒ?yÊjºžèÑ?#KæXÞUÒ?¿ÔÏ›ŠTÑ?)±k{»%Ñ?Üx`Ð?# Â¤RÑ? :!tÐ%Ñ?£?4óäšÒ?0œk˜¡ñÑ?|îû¯sÒ?*oG8-xÐ?³)Wx—‹Ñ?Tn¢–æVÐ?-î?2:Ñ?-{ØœƒÐ?ü¥E}’;Ð?²›ýh8Ï?¦E}’;lÐ?y7R¶HÑ?ݲCüÖÑ?õ„%P6Ò?�ÏdÑ?·`©.àeÐ?v“þ^Ð?’“‰[1Ñ?¤SW>ËóÐ?UOæ}“Ð?ôÂ� #Ñ?:–wÕæÐ?8ÜGnMºÐ?;nøÝtËÐ?Ûø• kÐ?mãOT6¬Ð?XÇñC¥Ð?Í >°ã¿Ð??o*RalÐ?’ÍUó‘Ð?ðN>=¶eÑ?ZÖýc!Ñ?—6–~Ñ?¦ð ÙuoÑ?Q†ª˜J?Ð?ìOâs'ØÐ?:“6UÐ?7á^™·êÐ?Kt–Y„bÐ?ÖâSŒgÎ?ž±/Ùx°Ï?«´Å5>“Ð? IJ™CRÑ?óâÄW;ŠÏ?§Z ³ÐÎ?l{»%9`Ï?žê�›áÐ?,¨þA$Ï?üQÔ™{HÎ?—©Ið†4Ð?.sž±Ï?;4,F]kÏ?gaO;ü5Ñ?§Y Ý!ÅÐ?¹à þ~1Ñ?Ž®ÒÝu6Ð?lê<*þÏ?ùÙÈuSÎ?ü¥E}’;Ð?ž±/Ùx°Ï?o‚oš>;Ñ?ÒV%‘}�Ð? ƒ2�&Ñ?îBs�FZÎ?o×KS8Ð?ak¶ò’ÿÍ?¬‘]i©Ï?NÓg\WÎ?é™^b,ÓÍ?™ðKý¼©Ì?€óâÄW;Î?bg �רÏ?ÇG‹3†9Ð?#‡ˆ›SÉÐ?ã4ôOpÏ?þÒ¢>ÉÎ?­ûÇBtÎ?pïô¥·Ï?Û2à,%Ï? ü¨†ýžÎ?Ánض(³Ï?É w¦(Ï? ¢î�ÚÎ?¨SÝÏ?ãá=–#Î?�ÄËÓ¹Î?‘&Þž´Î?•eˆc]ÜÎ?“V-Î?aTR' ‰Î? þ·’Ð?�^¸sa¤Ï?ÇF ^×/Ð?*6æuÄ!Ð?ÿ ’!ÇÖÍ?ý¢ý…Ï?Üx`áÍ?©|š“Ï?i‹k|&ûÍ?ã4ôË?OÌz1”Í?Ùî ûrÎ?eRC€ Ð?á&£Ê0îÌ?Èyÿ'LÌ?‘Òl‡ÁÌ?ÇK7‰A`Í?®€B=}Ì?t ‡ÞâË?¨V_]¨Í?% &áBÍ?!®œ½3ÚÌ?ž±/Ùx°Ï?e¥I)èöÎ?Ò‹Úý*ÀÏ?‹à+Ù±Í?­‰¾¢[Í?™Ÿš²ÓË?€ ;¨ÄÍ?#M¼Ì?vÞÆfGªÍ?Yà+ºõšÊ?k¸È=]ÝÉ?æx¢'eÊ?¤SW>ËóÊ?.É&Ê?èƒelèfÉ?üü÷àµKË?Ä%Ç�ÒÁÊ? î<0€Ê?;M„ OÍ?_%» ”Ì?ÉU,~SXÍ? nk ÏKË?nN%@Ë?ÚÆŸ¨lXÉ?¼·_>YË?²€ ܺ›Ë?ÙëÝïÍ?`#I®€Ì?ù/ÈÐÍ?J˜iûWVÊ?¡¢êW:Ì?„ó©c•ÒÉ?Ÿ ±Ý=@Ë?®Ø_vOÊ?÷uàœ¥É?”†…$³È?3ÂÛƒÊ?‚‹5˜†Ë?5æèñË?¨äœØCûÌ? ?8Ÿ:VË?yuŽÙëÉ?¤P¾¾ÖÉ?¶yËÕ�Ë?±1¯#ÙÊ?!ê>©MÊ?0º¼9\Ë?f÷äa¡ÖÊ?ʉvR~Ê?M1AG«Ê?/À>:uåÉ?«]ÒƒÊ? ‰°áé•Ê?»|ëÃz£Ê?8kð¾*Ê?n0Ôa…[Ê?åb ¬ãøË?®¼äòwË?D÷¬k´Ì?p(|¶Ì? uXá–É?üo%;6Ë?„›Œ*øÉ?fŸÇ(ϼÊ?/°ŒØÉ?^‚SHÞÇ?I�0eàÈ?ÓMbX9Ê?)ÍæqÌË?cdÉË»È?ëó�)È?³²}È[®È?: ûvÉ?EÚÆŸ¨lÈ?0º¼9\«Ç?ÞJ ,€É?GV~ŒÉ?hz‰±L¿È?©¤N@aË?£té_’ÊÊ?}iÆ¢Ë?Û÷¨¿^aÉ?£>+NÉ?ûPŒ,™Ç?~ü¥E}’É?ÿwD…Ê?2:=ïÆÌ?j ùœ»]Ë?’xy:W”Ì?Ø—qSÉ?ªðgx³Ë?:=ïÆ‚ÂÈ?Cr2q« Ê?Ì(–[Z É?îÏECÆ£È?¾3Úª$²Ç?~Œ¹k ùÈ?±à‚lYÊ?*ý„³[ËÊ?ç�“¼ÇË?Á-]Á6Ê?_\ªÒ×È?]N ˆI¸È?6É�økÊ?RF\¥É?µ¥òz0É?œ3¢´7Ê?Š[1еÉ?y±0DN_É?V)=ÓKŒÉ?¢ïne‰ÎÈ?6t³?PnÉ?œÁß/fÉ?(›r…É?Êp<ŸõÈ?âä~‡¢@É?Û3KÔÔÊ? ·|$%=Ê?k» ¾iúÊ?iÚV³ÎÊ?ž}åAzŠÈ?§²(ì¢èÉ?'À°üù¶È?§%VF#ŸÉ?ߺñîÈÈ?¢E¶óýÔÆ?œ0a4+ÛÇ?vp°71$É?¤ö{b�Ê?Z�œ¡¸Ç?GV~ŒÇ?o­mŽÇ?ã3Ù?OÈ? ýHVÇ?‡¢@ŸÈ“Æ?Jð†4*pÈ?Çg²žÈ?jÙZ_$´Ç?¾Û¼qRÊ?סš’¬ÃÉ?)A¡GŒÊ?�hE,bÈ?[[x^*6È?Òá!ŒŸÆ?W|Cá³uÈ?aâ�¢ÎÜÉ??�Qžy9Ì?X<õHƒÛÊ?DiâàË?Nïâý¸ýÈ?Y2Çò®zÊ?:#J{ƒ/È?îx“ߢ“É?¬8ÕZ˜…È?ˆìø/È?LÂ…<‚Ç?õ·CÃbÈ?ëÙ\5ÏÉ?R·³¯<Ê? ¸Ê;Ë?€-¯\o›É?¹ãM~‹NÈ?:#J{ƒ/È?ü�L‡NÏÉ?*àžçOÉ?ÿ>ãÂ��È?Ÿ¡¼�£É?¢&ú|”É?6X8IóÇÈ?ò—õIîÈ?áC‰–<È?6ZôPÛÈ?ž^)ËÇÈ?¥ØÑ8ÔïÈ?)狽_È?t&mªÈ?Mž²š®'Ê?Õ‘#��‘É?/OçŠRBÊ?Éw)uÉ8Ê?¨ã1•ñÇ?ÁXßÀäFÉ? <÷.9È?øàµKÉ?Î�é K<È?¶ƒûPÆ?É«s È^Ç?&ãÉ¡È?@‰Ï�`ÿÉ?jmÛkAÇ?Ûü¿êÈ‘Æ?ÞÈ<òÇ?´Yõ¹ÚŠÇ?µÀ)ÍÆ?F|'f½Æ?>{.S“àÇ?odùƒ�Ç?µOÇc*Ç?ÓL÷:©É?ß§ªÐ@,É?¤ü¤Ú§ãÉ?Ѳî ÑÇ?»}V™)­Ç?›âqQ-"Æ?ǹM¸WæÇ?ñ[z4ÕÉ?�ßÛôg?Ì?}±÷â‹öÊ?™Gþ`à¹Ë?žACÿÉ?â镲 qÊ?þ,–"ùJÈ?®�( ‰´É?§z2ÿè›È?SäGÈ?‘ ¤‹M+Ç?~r fÈ?—ÄY5ÑÉ?pµN\ŽWÊ?çP†ª˜JË?‚‹5˜†É?£\¿ðJÈ?}“¦AÑ<È?¡½úxè»É?-B±4-É?K‘|%�È?'iþ˜Ö¦É?y=˜É?.8ƒ¿_ÌÈ?á|êX¥ôÈ?âåé\QJÈ?Q}>ÊÈ?_Ÿ;ÁÈ?FÍWÉÇîÈ?Â4 SÈ?º¡);ý È?:ž%ÈÊ?g˜ÚRyÉ?�êD2Ê?\«=ì…Ê?1x˜öÍýÇ?©öéxÌ@É?“Úl@È?„�çÞÃ%É?³Í�é KÈ?M×]~Æ?½TlÌëˆÇ?Äy8�é´È?AJ˜iûÉ?I-”LNÇ?­k´è¡Æ?�«‘]iÇ?¤ÅÜ Ç?ú�ïÆ?«±„µ1Æ?“�[ìöÇ?¨¨ú•·Ç?c~nhÊNÇ?p—ýºÓ�É?™�Êø÷É?ÊÁl ËÉ?.㦚ÏÇ?Çž=—©Ç?cÓJ!�KÆ?£w*àžçÇ?°­Ÿþ³æÉ?¼Ì°QÖoÌ?,D‡À‘@Ë?À?¥J”½Ë?|—wJÉ?QhY÷�…Ê?• k*‹È?SX© ¢êÉ?«ì»"øßÈ?ÏÀÈËšXÈ?}–çÁÝYÇ?Ý‹Š8�È?ÝBW"PýÉ?ñƒó©c•Ê?»' µ¦Ë?–´â ŸÉ?ZcÐ ¡ƒÈ?³ìI`sÈ?%è/ôˆÑÉ?hB“Ä’rÉ?ç6á^™·È?YLüQÔÉ? Ý%qVDÉ?š}£<óÈ?x'ŸÉ?ç,òë‡È?�›h>çÈ?ëÉü£oÒÈ?]1#¼=É?àd¸uÈ?5Ñ磌¸È?,¸ðÀÊ?]éEí~É?%wØDf.Ê?)°¦ Ê?|~!<È?Õé@ÖSÉ?b×övKrÈ?æÍáZíaÉ?¾.ú�È?eÄ QºÆ?'c`ÇÇ?øˆ˜IôÈ?+ß3¡Ê?îv½4E€Ç?U¿ÒùðÆ?FzQ»_Ç?ŸæäE&àÇ?ö³XŠä+Ç?`!sePmÆ?lê<*È?�I›ªÇ?Ž��<»|Ç??eÄ É?€ð¡DKÉ?Ò�°¨ˆÓÉ?È@ž]¾õÇ?TªDÙ[ÊÇ?\>’’†Æ?ž–¸ÊÈ?d–= lÎÉ?Ž®ÒÝuÌ?Îm½2oË?W\•›¨Ë?4¡l\É?gCþ™A|Ê?ê@ÖS«¯È?1&ý½Ê?¼æU�ÕÉ?ÑÎihwÈ?6t³?PnÇ?Ôê"…²È?têÊgyÊ?‘Òl‡ÁÊ?ËH½§rÚË?Ûˆ'»™É?`V(ÒýœÈ?ûY,Eò•È?uÞÉÉ?Uú g·–É?»*P‹ÁÃÈ?Ù±ˆ×õÉ?KOË\É?;ûʃôÉ?ßýñ^µ2É?ÒþX«È?÷«ßmÞÈ?Gˆ,ÒÈ?•DöA–É?‰B˺È?‰\p¿È?©¾ó‹ôÉ?\:æU…È?˜5±ÀWtÉ?¯îXl“ŠÈ?fKVE¸ÉÆ?ª¹nÀÇ?ýJçóÉ?‘)‚ªÑÉ?ÓÛŸ‹†ŒÇ?î\éEíÆ?ºk ù gÇ? ˜£ÇïÇ?Û2à,%Ç?=ð1XqÆ?k™ ÇóÈ?"û Ë‚‰Ç?^Ÿ9ëSŽÇ?6È$#gaÉ?ën‡†ÅÈ?f»B,cÉ?–AµÁ‰èÇ?„¼LŠ�Ç?¤p= ×£Æ?Wéî:òÇ?èy’tÍÈ?ºöôÂ�Ë?q:ÉÊ?™ðKý¼©Ê?°ÅnŸUfÈ?½ª³Z`�É?¢}¬à·!È?Ž��<»|É?¶…ç¥bcÈ?Ò�°¨ˆÓÇ?ìL¡ó»Æ?�-Ë×eøÇ?îí–ä€]É?LbõGÊ?!?¹nJË?ŸZ}uU È?ßCpìÇ?ûçiÀ éÇ?ÎQGÇÕÈÈ?Ñ�ñ(•ðÈ?È@ž]¾õÇ?ö³XŠä+É?&áBÁ�È?» ”XÈ?ºÀå±fdÈ?²gÏejÈ?ÇAœ‡È?zR&5´È?@¾„ /È?0º¼9\«Ç?Ü»}éíÇ?Ëõ¶™ ñÈ?ë8~¨4bÈ?aÂhV¶É?J^�c@öÈ?’Ï+žz¤Ç?Žlê<È?9 {ÚáÇ?f£s~ŠãÈ?,¸ðÀÈ? <÷.9Æ?‰—§sE)Ç?ùdÅpuÈ?�ÚÄÉýÉ?Ùî@�òÆ?ølìMÆ?j†TQ¼ÊÆ?~b¼æUÇ?*A*ÅŽÆ?Q¡º¹øÅ?]ÄwbÖ‹Ç?©½ˆ¶cêÆ?"á{ƒöÆ?|ó&¤È?qh”.ýÇ?ÐÒl#žÈ?Z×h9ÐCÇ?þžX§Ê÷Æ?nO�ØîÆ?N(DÀ!TÇ?¹¨ÅäÇ?öBÛÁÊ?‡†Å¨kíÉ?eo)狽É?]¿ðJ’Ç?ò`‹Ý>«È?$ïÊPÇ?ÚTÝ#›«È?Õçj+ö—Ç?ΈÒÞà Ç?È@ž]¾õÅ?ë^fØ(Ç?•Ö߀È?PU¡�X6É?!ÂøiÊ?ÇÚßÙ½Ç?Úå[ÖÇ?#ö  Ç?fN—ÅÄæÇ?…[>’’È? ´¾LÇ?,Eò•@JÈ?› ê>©Ç?û¬2SZÇ?î"LQ.�Ç?kœMG7Ç?%Ì´ý++Ç?t´ª%Ç?ƒ/L¦ FÇ?6®×gÎÆ?èÚÐ Ç? ®¹£ÿÇ?Ê2ı.nÇ?¼é–âÈ?:uå³<È?vãÝ‘±ÚÆ?âæT2TÇ?W²c#Ç?Ÿ;ÁþÇ?ÎŒ~4œ2Ç?#,*ât’Å?äóЧiÆ?m�ÆÁ¥Ç?ãá=–#È?7à øü0Æ?˜Ÿ��Å?‰&PÄ"Æ?Á:Ž*�Æ?f 2þ}ÆÅ?ßO�—nÅ?CŒ×¼ª³Æ?32È]„)Æ?%wØDf.Æ?Nïâý¸Ç?6çà™Ð$Ç?¯xê‘·Ç?'0�ÖmÆ?§é³®+Æ?Ù•–‘zOÅ?Ęô÷RxÆ?ÁÇ`ũ֯?"S>U£É?'†§WÊÈ?ÙwEð¿•È?˜àÔ’wÆ?0bŸŠ‘Ç? PO�?Æ?Zd;ßO�Ç?Ëd8žÏ€Æ?t·ë¥)Æ?#föyŒòÄ?]4du¬Æ?$Dù‚Æ?„~¦^·Æ?ÞÉ§ÇÆ?¼æU�ÕÇ?­¥€´ÿÆ?NÐ&‡O:Ç?¼z�Æ?c+hZbeÆ?j£:ÈzÆ?YO­¾º*Æ?zVÒŠo(Æ?Øñ_ Æ?Ó£©žÌ?Æ?úüáç¿Å?\Uö]üÅ?÷W�ûVëÆ?Xÿç0_^Æ?Í‘•_Ç?Šçl¡õÆ?jÛ0 ‚ÇÅ?ªÒ×øLÆ?ÈBt Æ?ø2Q„ÔíÆ?ÓŸýHÆ?¼Ì°QÖoÄ?¢ ±ˆaÅ?µ‰“ûŠÆ?CqÇ›üÇ?g_y�ž"Å?U¡�X6sÄ?²ºÕsÒûÄ?ˆ÷XŽ�Å?81$'·Ä?Ðb)’¯Ä?Š[1еÅ? ¦šYKÅ?¥0ïq¦ Å?¹Ù•–‘Æ?`<ƒ†þ Æ?s¢]…”ŸÆ?BÍ�*ŠWÅ?i6�Ã`þÄ?YO­¾º*Ä??ÅqàÕrÅ?+Û‡¼åêÅ?|š“™È?>w‚ý×¹Ç?‚‹5˜†Ç?2;‹Þ©€Å?±ú# –Æ?;«ö˜HÅ?:\«=ì…Æ?Êà(yuŽÅ?¡¡‚‹Å?ñðžËÄ?P©eo)Å?· 8KÉrÆ?æêÇ&ùÇ?Ÿp]1#È?s*ª¸Å?¿ž¯Y.Å?iެü2Å?å^`V(ÒÅ?™(BêvöÅ?rÝ”òZ Å?òz0)>Æ?áBÁ�”Å?ãÃìeÛiÅ?X¬á"÷tÅ?€FéÒ¿$Å?ÂÛƒ�/Å?M ˆE Å?uÿwDÅ?íÓñ˜�ÊÄ? ×£p= Å?‡†Å¨kíÅ?ˆ5•EaÅ?_·Œõ Æ?£#¹ü‡ôÅ?ΧŽUJÏÄ?‹©ôÎnÅ?õ„%P6Å?ž·±Ù‘êÅ?ÿ+j0Å?Uø3¼YƒÃ?Ï ¡Ä?-Z€¶Õ¬Å? µ‰“ûÆ?iå^`V(Ä?Á‘@ƒM�Ã?,g~5Ä?JÏôc™Ä?ýjÌÑÃ?$EdXÅÃ?Q¾¾Ö¥Ä?J —UØ Ä?ï�b€DÄ?üߪ›Å?1'h“Ã'Å?Cá³up°Å? ”XSÄ?CÆ£TÂÄ?§–­õEBÃ? eáëk]Ä?öÔê««Å?;¨ÄuŒÇ?éÍ<¹¦Æ?§x\T‹ˆÆ?Åoò[tÄ?]Pß2§Å?Z.�óSÄ?õ»°5[yÅ?f¡Ø šÄ?‰–<ž–Ä?f0F$ -Ã?“Úl@Ä?|BvÞÆfÅ?χg 2Æ?I/j÷«Ç?š#+¿ ÆÄ?ˆ½PÀv0Ä?mÊÞå"Ä?6®×gÎÄ?ÿ”*QöÄ?u>­¢?4Ã?­¥€´ÿÄ?ÇHö5Ã?�|^ñÔÁ?üä(@ÌÂ?QÁá©Ã?g¶+ôÁ2Ä?¢µ¢ÍqÂ?HùIµOÇÁ?¾£Æ„˜KÂ?ŒjQLÞÂ?zR&5´Â?åœØCûXÁ?4¢´7øÂÂ?¯]ÚpXÂ?Í™dä,Â?6ÌÐx"ˆÃ?Ÿt"ÁTÃ?†©-u�×Ã?Ñ"Ûù~jÂ?"ʼn&PÂ?,F]kïSÁ?Ḍ›hÂ?Î4aûÉÃ?>”hÅ?°ÇDJ³yÄ?Ëd8žÏ€Ä?n4€·@‚Â?ÔdÆÛJ¯Ã?¸å#)éaÂ?Ë2�gÃ?.Yá&£Â?‹¤Ýèc>Â?l{»%9`Á?C©½ˆ¶cÂ?nÛ÷¨¿^Ã?âVA tíÃ?|(Ñ’ÇÓÄ?2!æ’ªíÂ?}?5^ºIÂ?÷@Â?C�ƒfÚÂ?ßp¹5éÂ?°Žã‡J#Â?LàÖÝ<Ã?‘óþ?N˜Â?�,µÞoÂ?‰B˺Â?Ñ/¤ÃCÂ?è‚ú–9]Â?’éÐéy7Â?�´TÞŽpÂ?ÆàaÚ7÷Á?òÏ â;Â? ×ÜÑÿÂ?Œ…!rúzÂ?¿ž¯Y.Ã?ªœö”œÃ?Ç ¿›nÙÁ?Gègêu‹Â?Ö­ž“Þ7Â?wj.7êÂ?¤5�:Â?å „�bÕÀ?YvQôÀÁ?�Õ{L¤Â?!;oc³#Ã?NÔÒÜ aÁ?}$%= ­À?ÎâÅÂ9Á?4MØ~2ÆÁ?çmlv¤úÀ?�f,šÎNÀ?0º¼9\«Á?‘z6«>Á?B–Á?Ùî ûrÂ?Ë/ƒ1"QÂ?§wñ~ÜÂ?¾¤1ZGUÁ?�Þp¹5Á?…µ1vÂKÀ?Xþ|[°TÁ?ò–«›äÁ?`æ;ø‰Ä?Tâ:ÆÃ?#ŸW<õHÃ?Î67¦',Á?\ætYLlÂ?f0F$ -Á?:w»^š"Â?6l±ÛgÁ?ߣþz…Á?Œ¢>+À?…²ðõµ.Á?vþÓ Â?ÈÒŦ•Â?çÁ=~Ã?î|?5^ºÁ?ž^)ËÁ?Ì&À°üùÀ? Ì EºŸÁ?�*ß3¡Á?_²ñ`‹ÝÀ?²eùº ÿÁ?—ÒþXÁ?Üñ&¿E'Á?�Þp¹5Á?ÉË»êÁ?’æ�imÁ?ë�ZaúÀ?Ké™^b,Á?Úª$²²À?Y�|^ñÀ?ÇÚßÙ½Á?JA·—4FÁ?¢F!ɬÞÁ?uÞÉÁ?—㈞À?®¸8*7QÁ?7�íµ ÷À?a2U0*©Á?‹lçû©ñÀ?úBÈyÿ¿?ÁâpæWsÀ?~q©J[\Á?€d:tzÞÁ?þ|[°TÀ?dv½S¿?9 {Úá¿??p•'vÀ?œÞÅûq¿?�œ¡¸ãM¾?Gå&jiÀ?g×½‰ À?xìg±É¿?ÂÙ­e2Á?,¹ŠÅo Á?+ö—Ý“‡Á?¸Y¼XÀ?4MØ~2Æ¿?ÒpÊÜ|#¾?8žÏ€z3À?>éD‚©À?”2©¡ ÀÂ?»+»`pÍÁ?ÈDJ³yÂ?Ø‚ÞCÀ?‡Áü2Á?ÖÈ®´ŒÔ¿?ÒŽ~7ÝÀ?‹qþ&"À? EºŸS�¿?}^ñÔ# ¾?µmÁ¿?‡ü3ƒøÀÀ?áµKKÁ?{…÷Â?•*Qö–rÀ?1 Xr‹¿?,œ¤ùcZ¿?À¯‘$WÀ?AG«ZÒQÀ?Ød�zˆF¿?à‚lY¾À?À"¿~ˆ À?È éðÆ¿?‹Áôoî¿?ÏžËÔ$x¿?L8 ¥¿?Ñy�]¢z¿?úüáç¿¿?ÉË»ê¿?óWya¿?é_’ÊsÀ?�ÏdÀ?_%» ”À?%�•C‹À?þDeÚʾ?ì†m‹2À?üǙ&l¿?n0Ôa…[À?ì«an¿?�EÓÙÉà¼?®c\qqT¾?|`Ç� À?CYøúZ—À?“ªí&ø¦½?N ógš¼?äL¶ŸŒ½?ÿ .VÔ`¾?˜PÁá½?9 {Úá»?Ûmšë4¾?ƒ¾ôö碽?Þ„€|½?_ÔîW¾¿?7OuÈÍp¿?^gCþ™AÀ?ó:â�½?+¢&ú|”½?W �\âÈ»?Ÿä›È̽?)Ð'ò$é¾?§yÇ):’Á?æ�I›À?Å�[ÌÏ Á?ØI}YÚ©½?'öÐ>Vð¿?žÎ¥„`½?žACÿ¿?ˆ‚S°Æ½?-ê“Üa½?I¡,|}­»?WÎÞm½?Òm‰\p¿?wR~Rí¿?˜öÍýÕãÀ?;ãûâR•¾?H‰]ÛÛ-½?S£’:½?ÖŒ ra¾?¢ÕÉŠ;¾?¢E¶óýÔ¼?צ±½ô¾?emS<.ª½?_Cp\ÆM½?&Š�º�}½?£çº½?ðûY,½?Ñèbg ½?­‡/EH½?/†r¢]…¼?>&RšÍã¼?ØF<ÙÍŒ¾?ŠY/†r¢½?zƾdãÁ¾?i§ærƒ¡¾?LnYk¼?‹ŒH¾½?UˆGâåé¼?׈`\:¾?,cC7û½?Ot ‡º?>U£W¼?zqÈÒ½?¿¹¿zÜ·¾?�{ž?mT»?pê”G7º?‹O0žA»?ÅUeßÁ»?çâo{‚ĺ?Îm½2o¹?«• ¿ÔÏ»?º»Î†ü3»?ûêª@-»?è-Þs`½?áÔ’w½?ö)Çdqÿ½?K= By»?Ñ>VðÛ»?¨ŒŸq¹?Ý[‘˜ †»? Ùy›½?™ðKý¼©À?«{dsÕ<¿?R º½¤1À?ú¸6TŒó»?�C¨R³¾?.VÔ`†»?�Í9x&4½?Qö–r¾Ø»?�~4œ27»?Õ—¥�šË¹?è/ôˆÑs»?‰ëW\½?.çR\Uö½?ÉŽ�@¼®¿?²�t±i¥¼?òxZ~à*»?éH.ÿ!ýº?uÍä›mn¼?!>°ã¿@¼?ÓÁú?‡ùº? ´¾L½?¼§>�¼»?f»B,c»?LüQÔ™»?K= By»?òxZ~à*»?ΈÒÞà »?8Ÿ:V)=»?::ZÕ’º?*Wx—‹øº? Q¾ …¼?ÍsD¾K©»?9b->À¼?™œÚ¦¶¼?Í?ú&Mƒº?0¼’ä¹¾»?=Ô¶a»?�x]¿`7¼?Åã¢ZD»?•·#œ¼¸?mÊÞå"º?ë««µ¼?0ôˆÑs ½?4óäš™¹?à øü0B¸?“o¶¹1=¹?²Úü¿êȹ?ª·¶J°¸?Q¿ [³•·?õÙ×3º?'Þž´p¹?�PáR¹?@øP¢%�»?–Ïò<¸;»?åïÞQcB¼?b„ðh㈹?Ý"0Ö70¹?Tüߪ·?f…"ÝϹ?Ù²|]†»?–x@Ù”¿?)wŸã£Å½?.� ø1æ¾?Gå&jiº?ÉüI‚¼?=Ú¨Nº?#†Ƥ¿»?‡m‹2dº?о¢[¯¹?,ñ€²)W¸?M¿D¼uþ¹?{/¾h�»?öA–¼?Êþy0H¾?Àϸp $»?1A ߺ¹?à?ÿ=x¹?Ééëùšåº?“ŠÆÚßÙº?-!ôlV¹?øo^œøj»?DMôù(#º?¡·xxϹ? Ö8›Žº?&â­óo—¹?€-¯\o›¹?ÃòçÛ‚¹?ÅS�4¸­¹?Ð MÙé¹?8ù-:Yj¹?H¨REñº?Ñ=ë-º?\Æú&»?ù.¥.»?Ëõ¶™ ñ¸?�½3Úª$º?+ö—Ý“‡¹?zƾdãÁº?£>É6‘¹?sf»B,·?u¬Rz¦—¸?é ÷‘[“º?D1yÌ|»?W=`2å·?­ÃÑUº»¶?À=ÏŸ6ª·?%wØDf.¸?[°Tð2·?®,ÑYf¶?t%Õ?ˆ¸?¯&OYM×·?MeQØEÑ·?1 òº?ÙÐÍþ@¹¹?÷vKrÀ®º?["œÁß·?I¡,|}­·?ý÷àµK¶?(�Í9x&¸?8-xÑW�º?UOæ}“¾?KÊÝçø¼? µ‰“û¾?ÊŒ·•^›¹?+ømˆñš»?_Cp\ÆM¹?Nïâý¸ýº?>Ëóà?k» ¾iú¸??�¨Œ·?�Þp¹5¹?�¥]Pߺ?kƒѯ­»?žÒÁú?‡½?Ü ö[;Qº?´“ÁQòê¸?áñí]ƒ¾¸?çÄÚÇ º?V¼‘yäº? F³²}¸?¿¹¿zÜ·º?·³¯À¸?§/ú Ò¸?ÂÚ;á%¸? Q¾ …¸?ÊúÍÄt!º?3‰zÁ§9¹?:Xÿç0_º?‹ú$wØDº?Í‘•_c¸?E×…œO¹?:é}ãkϸ?ePmp"ú¹?N`:­Û¸?³ðõµ.5¶?ƒh­hsœ·?çá¦Óº¹?B&9 {º?Ø�sF”ö¶?¼‘yä¶?ö&†ädâ¶?5íbšé^·?ønóÆIa¶?u­½OUµ?è3 ÞŒš·?_¶�¶F·?Û¾Gýõ ·?ŠriüÂ+¹?φü3ƒø¸?âú}ÿæ¹?¯}½pç¶?–~Tö?É;‡2Tµ?–{�Y¡H·?Ÿ<,Ôšæ¹?X�ûVëĽ? B\9{g¼?eâVA t½?G 6u¹?Wèƒelèº?× /½ý¹¸?d°âTkaº?j'÷;¹?‹T[r¸?¯°à~À·?·Aí·v¢¸?›æ§èHº?€H¿}8»?uç‰çl½?áš;ú_®¹?}ëÃz£V¸?º«?Â0¸?| Vœj¹?G=D£;ˆ¹?8ýÚúé·?±i¥È%º?'À°üù¶¸?`Í‚9z¸?©„'ôú“¸?ø¬8ÕZ¸?ŽDÁŒ)¸?kE›ãÜ&¸?äÀ«åÎL¸?BZcÐ ¡·?¸?°È¯bƒ¹?‹3†9A›¸?Þ�FN¶¹?CB•š¹?ž³„Ö÷?‹ßV*¨¸?!>°ã¿@¸?NA~6r¹?¤á”¹ùF¸?]Ot]øÁµ?ì…¶ƒ·? ´;¤ ¹?€É�cë¹?¬ŒF>¯x¶?3�&c`µ?ʉvR¶?�›h>ç¶?‰]ÛÛ-ɵ?#/kb�¯´?šÏ¹Ûõ¶?eÄ Q¶?¤7ÜGnM¶?¯˜Þ„¸? 0,¾-¸?9EGrù¹?þÖN”„D¶?³Dg™E(¶?”M¹Â»´?æ¯�¹2¨¶?î$"ü‹ ¹?R û=±N½?¬ÿs˜//¼?!�àFʽ?÷Xúй?gñbaˆœº?JFΞv¸??�{óº?Mº-‘ θ?²^‚S¸?`‘_?Ķ?y ý\¸?“áx>ê¹?ܺ›§:äº?¯#Ù@º¼?-!ôlV¹?#½¨Ý¯¸?¦�Õ°ß·?”k dv¹?L£uT5¹?ÆýG¦C§·?ìùšå²Ñ¹??N™›o¸?‹üú!6¸?® ãüM¸?½ÅÃ{¸?Ò7iÍ·?¤6qr¿·?ÊPSé·?›;ú_®E·?yè»[Y¢·?õ€yÈ”¹?iTàd¸?Q†ª˜J?¹?lîè¹¹?Äëú»a·?£’:M¸?+Û‡¼åê·?Ï„&‰%¹?biàG5ì·?Øñ_ �µ?RÔ™{Hø¶?Ëõ¶™ ñ¸?êçME*Œ¹?åÒø…W¶?�2âÐ(µ? š²Ó¶?"¦D½Œ¶?öBÛÁˆµ?Y2Çò®z´?ãQ*á ½¶?À"¿~ˆ ¶? ®¹£ÿµ?¢ÏGq¸?a6†åÏ·?JÑʽÀ¬¸?<ø‰è÷µ?Ï×,—�ε?SëýF;n´?¥Kÿ’T¶?Eƒ<…\¹?¥¡F!ɼ?¹þ]Ÿ9ë»?š#+¿ Ƽ?û”c²¸ÿ¸?ãÂ��,`º?CSvúA]¸?j’Ìê¹?Iö5Cª¸?¦`�³é¸?Þvøk²¶?+Ôð-¸? K< lʹ?,*ât’­º? šyrM�¼?kšwœ¢#¹?o�»ì·?Û$¶»·?Ǹââ¨Ü¸?†®D ú¹?uÌyƾd·?GéÒ¿$•¹?Ü�µÛ.¸?ßÞ5è·?WA tí ¸?õL/1–é·?iTàd¸·?¾Û¼qR˜·?,×Ûf*Ä·? Tÿ ’!·?D…êæâo·?jÜ›ß0Ѹ?A¶,_—á·?œ5x_• ¹?á(yu޹?¬ÿs˜/·?„Ö׉"¸?"«[='½·?ÝCÂ÷þ¹?m‘´}Ì·?F—7‡kµ?çmlv¤ú¶?ß,Õ¼¸?¥e¤ÞS9¹?tðLh’X¶?„+ POµ?ºÛõÒ¶?ݵßÚ‰¶?¦œ/ö^|µ?y ý\´?�±¾�É�¶?t ‡Þâµ?Šä+�”ص?œ¥d9 ¥·?Æù›Pˆ€·?úGߤiP¸?“ `ÊÀµ?Ø�î<ñœµ?¶+ôÁ26´?ýKR™b¶?}iÆ¢é¸?B žB®Ô»?º ¾eN»?ÊŠ;Þä»?"¨½ ¸?‘{ººc±¹?¶ƒû¸?w„Ó‚}¹?fÝ?¢C¸?D5%Y‡£·?¯ê¬Øc¶?aâ�¢ÎÜ·?Õ°ßëT¹?<Û£7ÜGº?$˜jf-¼?)ÎQGÇÕ¸?Î'…y�·?U ƒ‡i·?~£<ór¸?Ižëûp�¸?Xc'¼·?ИIÔ >¹?ì£SW>Ë·?4fõ‚·?©‡ht±·?1 Xr‹·?sñ·=Ab·?XŽ��<·?œh>çn·?õŸ5?þÒ¶?JbI¹û·?Òýœ‚ül¸?Þ¯|·y·?ÓÜ a5–¸?ÑvLÝ•]¸?‚)[$í¶?Ófœ†¨Â·?Ìa÷Ãc·?‹3†9A›¸?ƒ5Φ#€·?ßýñ^µ2µ?ò²&øŠ¶?ÈбƒJ¸?ÐÖÁÁÞĸ?Þs`9B¶?¬Å9êè´?>w‚ý×¹µ?OXâeS¶?™ƒ £U-µ?½ÅÃ{´?Î7¢{Ö5¶?±£q¨ß…µ?èÙ¬ú\mµ?–˜g%­ø¶?»_ønó¶?õL/1–é·?öE™ 2µ? dv½Sµ?ðˆ ÕÍų?à?ÿ=xµ?Ö:q9^�¸?«#G:#»? ô‰å˜,¶?lë§ÿ¬ùµ?áy©Ø˜×µ?È@ž]¾õµ?mXSYvµ?K­÷í¸µ?ñð¤…˶?½åêÇ&ùµ?Þ®Õö¶?- PSËÖ¶?n‹2d’µ? @£té_¶?ßN"¿¶?B˜Û½Ü'·?£ý…1¶?® µ?L5³–¶?ñó߃×.µ?ë8~¨4¶?ZÔ'¹Ã&¶?"ߥÔ%ã´?#0Ö70¹µ?¸æŽþ—kµ?¯ê¬Øc¶?>@÷åÌvµ?I,)wŸã³?GÇÕÈ®´´?JDøAc¶?›©¾ó‹¶?ò%TpxA´?–wÕæ!³?ÇAœ‡´? Q¾ …´?(›r…³?XŒºÖÞ§²?ñº~Án´?ˆ„ïý Ú³?Õ•Ïò<¸³?­iÞqŠŽ´?Ÿ�2âд?f…"Ýϵ?•œ{h³?Á=~o³?¸àŸR%²?}ÍrÙ蜳?Vïp;4,¶?‡4*p² ¸?4,F]kï·?†�óþ?N¸?Êþy0H¶?î@�òè¶?R û=±Nµ?WCâK¶?ƒ3øûÅlµ?±5[yÉÿ´? „�bÕ ´?ÇhUMµ?ƒù+d® ¶?]¤P¾¾¶?Bí·v¢$¸?ç9"ߥԵ?¬ä.´?�À~þ{´?/PR`Lµ?ñ~Ü~ùdµ?¬ÿs˜//´?‚ý×¹i3¶?Åþ²{ò°´?•ص½Ý’´?MÛ¿²Ò¤´?'À°üù¶´?ŒƒKÇœg´? PO�?´?Š!9™¸U´?mp"úµõ³?DOʤ†6´?bI¹ûµ?“Úl@´?y¯Z™ðKµ?ŒM+…@.µ?G­0}¯!´?…{eÞªë´?;�´TÞŽ´?¹ÁP‡nµ?¦ÒO8»µ´?*æ èhU³?pÒ4(š´?V)=ÓKŒµ?÷!o¹ú±µ?­hsœÛ„³?€Ÿqá@H²?ÿ®Ïœõ)³? 7U†q³?ûWV𔂲?‡O:‘`ª±?8Ÿ:V)=³?lèf Ü²?ëD2䨲?·˜Ÿš²³?mp"úµõ³?ni5$î±´?m©ƒ¼L²?ªÒ×øL²?ù½Mö#±?ø¥~ÞT¤²?Tþµ¼r½µ?¾ƒŸ8€~·?øí¸áw·?«=ì…¶·?Ãõ(\�µ?�Âõ(\�¶?ãÞü†‰µ?=œÀtZ·µ?Ñ”�~Pµ?S"‰^F±´?["œÁß³?³ÅVд´?]ûzµ?CÆ£T¶?£<órØ}·?"rúz¾fµ?â‘xy:W´?:ž%È´?,,¸ðÀ´?Ž •bGã´?œ¥d9 ¥³?#„GG¬µ?… £YÙ>´?=·Ð•´?�'‚8'´?�‰”fó8´?vâr¼ѳ?XåBå_˳?«• ¿Ôϳ?å%ÿ“¿{³?”‡…ZÓ¼³?^žÎ¥„´?Þ:ÿvÙ¯³?å*¿)¬´?r„Ѭ´?¦}sõ¸³?穹n´?ëW:ž%´?Ó¿$•)æ´?Ct 4´?7á^™·ê²?¤oÒ4(š³?Z+Úç6µ?ŠÊ†5•Eµ?Êû8š#+³?oñðž˱?<¡×ŸÄç²?4/‡Ýw ³?}=_³\6²?¦D½Œb±?ìÙs™š³?GW#»²?Yà+ºõš²?–{�Y¡H³? ñH¼<�³?Z.�óS´?2Ì Úäð±?Í\àòX3²?w� ùg±?ïäÓc[²?˾+‚ÿ­´? ÇóPo¶?m¡õðe¶? 5?þÒ¢¶?ú~j¼t“´?&6׆е?óùõC´?Ɖ¯vç´?çQñGT´?‚pêé³?¹4~á•$³?h׿ë³?¼­ôÚl¬´?±ÀWtë5µ?¬äcw�’¶?¸éÏ~¤ˆ´?ºžèºðƒ³?€ôMšE³?š&l?ã³?5 ´;¤´?g|_\ªÒ²??U…bÙ´?©¤N@a³?mýôŸ5³?“qŒd�P³?ƒ5Φ#€³?IƒÛÚÂó²?_¶�¶F³?*´t³?†!rúz¾²?¸"1A ß²?Нv稳?ÎýÕã¾Õ²?†©-u�׳?»ÕsÒûƳ?ÖÇCßÝʲ?~¨4bfŸ³?· b k³?$ð‡Ÿÿ´?{Cr2q³?7Û$²?/àe†�²²?Š!9™¸U´? š]÷V´?|~!<²?E�>‘'±?-“áx>²?+hZbe4²?µˆ(&o€±?Æ‚”°?·Õ¬3¾/²?œ27߈î±?2Ì Úäð±?@ÀZµkB²?âvhXŒº²?€Ó»x?n³?çÇ_ZÔ'±?¸u7O±?1 òn°?Ó1çû’±?Ü»}éí³?0º¼9\«µ?†txã§µ?ôzÄè¹µ?Ѱu­½³?•·#œ¼´?­5”Ú‹h³?œà›¦Ï´?)êÌ=$|³?¨Sݳ?'/2¿F²?4Ûú`³?´:9Cqdz?èØA%®c´?€¸«W‘ѵ?øùïÁ³?êìdp”¼²?Ëd8žÏ€²?M ˆE ³?YRî>ÇG³?“å$”¾²?1&ý½´?�n„EEœ²?‘~û:p²?ê<*þ?sõ¸oµ²?"Ã*ÞÈ<²?׈`\:²?Ö­ž“Þ7²?^ÚpXø±?WCâK²?8 ¥+ز?jh°²?¸ŸF³?Æ‹…!rú²?ͰQÖo&²?ôÄs¶€Ð²?i¢²?)_ÐBF³?²×»?Þ«²?¿)¬TPQ±?ñ�ÿ²?Zd;ßO�³?%±¤Ü}޳?ƒ3øûÅl±?ë6¨ýÖN°?Zd;ßO±?šë4ÒR±?B[Î¥¸ª°?æXÞU˜¯?8IóÇ´6±?³—m§­±?È—PÁá±?æ®%䃞±?´”,'¡ô±?±Pkšwœ²?]¥»ël°?RC€ ˆ°?Ü�µÛ.4¯?¯{+Ô°?ué_’ʳ?º†Oµ?ýkyåzÛ´?EœN²Õå´?˜¢\¿ð²?éI™Ôд?sõ¸oµ²?0ÕÌZ H³?‚�,`·²?ƒÜE˜¢\²?`"Þ:ÿv±?Çô„%P²?ÓÁú?‡ù²?±ù¸6TŒ³?ŸçOÕé´?!Z+Úç²?Z�œ¡¸ã±?r£ÈZC©±?7oœæ=²?^ô¤‹²? YÝê9±?Ýxwd¬6³?l´è¡¶±? À?¥J”±?AÕèÕ¥±?t ‡Þâá±?dY0ñG±?0×¢h[±?QÞÇÑY±?eS®ð.±?LÆ1’=B±?.çR\Uö±?Év¾Ÿ/±?32È]„)²?*á ½þ$²?BëáËD±?C;§Y Ý±?®I·%rÁ±?³Ïc”g^²?¾‰!9™¸±?FaE|°?->Àx±?6Vbž•´²?ó‘”ô0´²?#/kb�¯°?“�x]¯?•€˜„ y°?´¬ûÇBt°?Ã.Šø°?IVñF®?%ZòxZ~°?:ÏØ—l<°?á (ÔÓG°??U…bÙ°?Vš”‚n/±?-@ÛjÖ±?ö~£7ü®?úüá翯?œiÂö“1®?·Ï*3¥õ¯?ž˜õb('²?-y<-?´?Ëø÷´?÷®A_zû³?µÃ_“5ê±?W•}W³?Ü�:åѱ?ê<*þ?o�»ì×±?8MŸp]±?¶ö&†°?Û£7ÜGn±?äf¸Ÿ²?™ò!¨½²?$ð‡Ÿÿ´?¥/„œ÷ÿ±?€îË™í ±?ܸÅüÜа?`ÊÀ-]±?¾Ý’°«±?w×Ù�f°?e‹¤Ýèc²?š'×Èì°?Ý 7àóð?àI —Uذ?Þrõc“ü°?D†U¼‘y°?fh<Äy°?iÀ"¿~°?7n1?7°?5B?S¯[°?ò"ðk$±?jõÕU�Z°?è-Þs`±?=a‰”M±?œÃµÚÃ^°?>Y1\±?å „�bÕ°?Îqn?fO›sð°?Å7>[¯?(�Í9x&°?s‚69|Ò±?¢Busñ·±?s×òA¯?¬ÉSVÓõ¬?ðß¼8ñÕ®?ÄÑUº»Î®?hñ�ÿ­?b k_@/¬?FAðøö®?îîº/g®?üŠ5\äž®?�-Ë×eø¯?á² ›.°?ÎSr3ܰ? p­?�Šÿ;¢B­?X zR&5¬?4iSu�l®?6<½R–±?Ø*Áâpæ³?†1zn¡³?ÙvÚŒ³?ñKý¼©H±?ýh8en²? -ëþ±±?l“ŠÆÚß±?åì�ÑV%±?äÖ¤Û¹°?*¬ÿs˜¯?×L¾ÙæÆ°?#ùJ %v±?xÑW�f,²?JÐ_裳?g)YNB±?±3…Îk°?å—Á‘(°?8Ûܘž°°?Òo_α?µSs¹ÁP¯?eqÿ‘éб?©ù*ùØ]°?| Vœj-°?»—ûä(@°?‚«<�°S°?%±¤Ü}ޝ?÷Ç{ÕÊ„¯?‚‘—5±À¯?µ:uå«?`�ºò©?¶eÀYJ–«?ía/°¬?§x\T‹ˆª?·ÑÞ©?r4GV~¬?¤8GW«?ÏkìÕ[«?ãOT6¬©¬?óÆIaÞã¬?ÃFY¿™˜®??ªa¿'Ö©?ûY,Eª?&Î5ÌШ?­¢?4óäª?è3 ÞŒš¯?Wzm6Vb²?r‰#D²?ǹM¸Wæ±?JbI¹û¯?²,˜ø£°?:;%¯®?~£<ór°?–#d Ï.¯?^¹Þ6S!®?ãþ#Ó¡Ó«?DMôù(#®?„~¦^·°?£ x|{×°?HÅÿQ¡²?ßû´W¯?8, ü¨†­?35 Þ�F­?¯ ?8Ÿ:®?ÛÂóR±1¯?…±… %¬?)ßÞ5°?lâuý‚­?D¤¦]L3­?2ZGUD­?–´â Ÿ­?�l®šçˆ¬?äRìh¬? š–X�¬?_ÔîW¾«?f1±ù¸6¬?HS=™ô­?f1±ù¸6¬?( 5 I®?ªÒ×øL®?]©gA(ï«?>]ݱØ&­?4hèŸàb­?²ºÕsÒû®?uÿwD­?Æ2ýñÖ©?@üü÷൫?*Wx—‹ø®?iâàI ¯?ʉvR~ª?ó¬¤ßP¨?Ìyƾdã©?CŒ×¼ª³ª?Ùî@�ò¨?Ý(²ÖPj§?A~6rÝ”ª?¹nÀ燩?W°�x²›©?,œ¤ùcZ«?Ò¬lò–«?½Ãíа­?�Ky ²¨?ùõCl¨?]Þ®Õ¦?ª¸q‹©?4hèŸàb­? ¥ö"ÚŽ±?‘ð½¿A{±?ïá’ãNé°?LàÖÝ<­?º ¾eN¯?}Ê1Yܬ?ñœú@ò®?º2¨68­?ß¿yqâ«?�ÁŠS­…©?[Î¥¸ªì«??�{ó®?3¥õ·à¯?”ÝÌèGñ?"àªÔ¬?+iÅ7>«?³A&9 «?¦´þ–ü«?yW=`2­?-@ÛjÖ©?»Õ”d®?0-ê“Üa«?”ÁQòê«? ^ô¤«?f»B,c«?eßÁÿVª?)Z¹˜ª?åÐ"Ûù~ª?žz¤Ámm©?®,ÑYfª?À=ÏŸ6ª«?}²b¸:ª?¬Så{F"¬?I,)wŸã«?O>=¶eÀ©?S ³³è�ª?a§X5«?ógš°ý¬?�0 Xr«?2rö´Ã§?åòw﨩?Y¤‰w€'­?ëTùž‘­?ú~j¼t“¨?rÁüýb¦?€ñ ú'¨?�ÐÏÔë©?ÌÑã÷¦?^‘šv1¥?ì ×1®¨?¡l\ÿ®§?zÇ):’˧?Z+Úç6©?Ó…Xý†©?ÁR]ÀË «?1îÑZѦ?üâR•¶¸¦?¹ÿÈtèô¤?,H3Mg§?$*T7«?ÒŒEÓÙɰ?Kªɰ?‡3¿š°?†vN³@»«?Tþµ¼r½­?þœ‚üläª?¥ßPøl­?›ÆöZÐ{«?q¯Ì[uª?;oc³#Õ§?«&ˆº@ª?]~p>u¬?[ÏŽY®?•!ÿ°?.VðÛ¨?I€šZ¶Ö§? îêUdt¨?g×½‰ ª?ùómÁR]¨? s‚69|ª?&ßlsczª?æ¾÷7¨?eû�·\ý¨?+*ÿZ^©?{Cr2q«?�ôiý¡©?O’®™|³¥?ÝÑÿr-Z¨?hæÉ52«?«w¸«?·›à›¦Ï¦?_%» ”¤?9ÕZ˜…v¦?â¶ôhª§?ßÜ_=î[¥?5™ñ¶Òk£?‘ñ(•ð„¦?íE´Sw¥?M,ðÝz¥?Ïg@½5§?æÍáZía§?=órØ}Ǩ?l³±ó¬¤? ß÷o^¤?ûèÔ•Ï¢?‰ëW\¥?æ}“¦A©?´W}¯? ßû´W¯?Ü.4×i¤­? k_@/Ü©?-σ»³v«?vß1<ö³¨?•'vŠU«?Úç6á^©??;àºbF¨?Øõ vö¥?±k{»%9¨?v¦Ðy�]ª?ÅE�¹‡¬?KæXÞU°?t È^ïþ¨?¥J”½¥œ§?Y1\q§?™€_#I¨?s÷9>Zœ©?Û¦¶ÔA¦?Ïõ}8Hˆª?ý¾óâħ?l¸ [–§?²€ ܺ›§?["œÁß§?ožê�›á¦?/0+é~¦?Ž¿·éϦ??VðÛã¥?"úµõÓ¦?ó!¨½¨?¿Òùð,A¦?穹n¨?Ø Ûe6¨?Á­»yªC¦?†¬nõœô¦?ð¥ð Ùu§?ªÕWWj©?€Ó»x?n§?l dv½£?XøQ û¥?±á镲 ©?â[X7Þ©?õfÔ|•¤?ŒÙ’Un¢?,ºõš¤?è1Ê3/‡¥?J¶ºœ£?>Y1\¡?<š$–¤?¨T‰²·”£?ùö®A_z£?gµÀ)¥?35 Þ�F¥?ôpÓiݦ?}гYõ¹¢?ž#ò]J]¢?ëTùž‘ ?Cª(^em£?Šå–VCâ¦?>Àx ­?+Üò‘”¬?nú³)"«?»òYžw§?­P¤û9©?õ-sº,&¦?�¯èÖkz¨?ÆüÜД�¦?å`6†¥?qW¯"£?*ÖT…¥?­lò–«§?Ÿ��·±©?æ@µm­?AG«ZÒQ¦?—qS¥?XŽ��<»¤?ÊŒ·•^›¥?~Wÿ[ɦ?‚�ëßõ™£?·D.8ƒ¿§?›Z¶Ö ¥?î'c|˜½¤?î'c|˜½¤?y$^žÎ¥?ú—¤2Ť?�N]ù,Ï£?O#-•·#¤?ADjÚÅ4£?þEИIÔ£?HÀèòæp¥?-Z€¶Õ¬£?=HO‘CÄ¥?¨V_]¨¥?šë4ÒRy£?Í‘•_c¤?ö$°9Ϥ?�Æ¡~¶¦?„œ÷ÿq¤?1{ÙvÚ¡?Xª x™a£?—Çš‘A¦?ÊRëýF;¦?¦bc^G¢?Ä QºôŸ?]Ot]øÁ¡?Ìz1”í¢?ŒôzÄ ?†=íð×d�?NF•aÜ ¢?�«‘]i¡?"ߥÔ%ã ?ì4ÒRy;¢? m9—â¢?z9ì¾cx¤?cD¢Ð²îŸ?‰îY×h9 ?nÞ8)Ì{œ?r¦ ÛOÆ ?ðúÌYŸr¤?�‡¾»•%ª?)"¦?PÈÎÛØì ?·¸Æg²ž?° ÍX4��?Ý Ì EºŸ?@¡ž> ?‡4*p² œ?“�[ìö¡?“§¬¦ë‰ž?.7ê°Â�?U¯²¶)ž??Ä 'iž?=‚)[$�?GÇÕÈ®´œ?Ë,B±4�?Ý µ‰“›?¯D ú‘œ?Š[1еŸ?‹PlMKœ?Š!9™¸U ?‘`ª™µ ?ƒ5Φ#€›?FИIÔ ž?À%W±ø�?1]ˆÕa ?‡ú]Øš�?q�å Z˜?N 4Ÿs·›?ñd73úÑ ?0Hú´Šþ ?sŸˆ‚™?–Ð]gE”?)uÉ8F²—?‹ˆbò˜™?zqÈÒ•?IÚ�>æ’?J +‡™?·Ï*3¥õ—? �ר%ª—?ÿ .VÔ`š?/\sGÿ›?º„Coñðž?˜üOþî•?ž~P)”•?j…é{ Á‘?ï7ÚqÃï–?éðÆOãž?_%» ”¤?rÞÿÇ £?¤ÃC?�£?NBé !ç�?U¿Òùð,¡?\;Qi›?Êf/Ûž?Kê46œ?M�Ì΢wš?¤§È!â–?ÄB­iÞqš?ð†4*p²�?žî<ñœ- ?®ÕöB£?™×‡l �?ˆŸÿ¼v™?aü4îÍo˜?ÝAìL¡óš?gF?N™›?|,}è‚ú–?Q¡º¹øÛž?’we¨Š™?Úl@„˜?؃Iññ ™?s¼Ñ“2™?3ýñÖù—?êAA)Z¹—?J–“PúB˜?¦|ªF¯–?>u¬Rz¦—?5^ºI ›?ä¢ZD“—?{ö\¦&Á›? $(~Œ¹›?6ZôPÛ–?ú`º™?àõ™³>å˜?L§uÔ~›?>ë-z˜?¾ݳ®Ñ’?š]÷V$–?]QJVÕ›? JÑʽÀœ?†vN³@»“?3¥õ·à�? m9—⪒?p–’å$”?f£s~Šã�?}w+Kt–‰?ðùa„ðh“?£?4óäš’?Ánض(³‘?Æ5>“ýó”?&ãÉ¡–?'ÁÒ¨À™?mýôŸ5�?7TŒó7¡�?êu‘BYˆ?u­½OU‘?§wñ~Ü~™?³Ò¤t{¡?jP4`‘Ÿ?½Œb¹¥Õ ?¡J͘?õI̜?×¥Fègê•?÷[;Q™?Ž®ÒÝu–?:A›>é”?íÔ\n0Ô‘?†Èéëùš•?t–Y„b+˜?³{ò°Pkš?kÓØ^ zŸ?:ž%Ș?ßøÚ3K”?¾Á&S“?Ò‰Sͬ•?¬r¡ò¯å•?(œÝZ&Ñ?ä„ £Y™?˜£Çïmú“?2ãm¥×f“?ñ,AF@…“?aO;ü5Y“?–±¡›ý�’?܃�/¡’?ø¥~ÞT¤’?4GV~Œ‘?ŠsÔÑq5’?õg?RD†•?å(@̘’?"Ä•³w–?´”,'¡ô•?ÍæqÌ_‘?±RAEÕ¯”?®-9 p?#0Ö70¹q?¦(—Æ/¼b?ŒòÌËa÷]¿Ž‘ìj†t?×߀Ju?í+ÒSäp?‡ˆ›SÉ€? |(ђǃ?Ù|\*Ɖ?¨p©;Z?�ôiý¡i?Ôº j¿µS¿éd©õ~£m?É&pënŽ? …8„*•?>v?‰{,}肊?5&Ä\Rµ}?Ö©ò=#z?œ'¾ÚQ|?m:¸Y¼x?xìg±Éw??¨‹ÊÂw?:ÏØ—lw?`­Ú5!­�?(¸XQƒix?__ëR#ôƒ?¢+Üò‘„?`sž Mr?¢c•¸Ž�?~£<órx? S"‰~?÷åÌv…>x?A‚âǘ»f?sµ4·Bx?6l±Ûg…?qs*ªˆ?]ú—¤2Ål?†7kð¾*W¿—=Ô¶aT?î=\rÜ)]?°t>d?,ºõšt?f`Xþ|{?&üR?o*‚?Ä™_Í‚‰?)v4õ»€?8fÙ“Àæl?Åã¢ZDc?JΉ=´�u?¤6qr¿Cq?ôï9°\?6sHj¡d‚?¯þ·’k?iqÆ0'hc?ãàÒ1çi?§çÝXPd?a‹Ý>«Ìd?æmrø¤c?ÇIaÞãLc?Œ�u?TZ?l%t—ÄYa?¯ ?8Ÿ:v?Ôº j¿µc?ÛOÆø0{y?‹üú!6x?ñ»é–âO?y:W”‚u?ßÝÊ�ef?ébÓJ!p?kׄ´Æ c?*6æuÄ![¿ûÌYŸrLV?1е/ w?½8ñÕŽâ|?¯þ·’[¿Õ‘#��‘w¿F[•DöAf¿÷è ÷‘[c¿ˆØ`á$Ío¿€fØñ¿G‘µ†R{a¿p˜h�‚§`¿t^c—¨Þj¿ÓÝu6äŸ9?M„ O¯d?"ÇÖ3„cv?Ùéu‘By¿FÒnô1p¿ÕýL½n�¿1?74e§o¿v7OuÈÍ€?Œ‰BËŠ?Vº»Î†üƒ?ªî‘ÍUóŒ?Tÿ ’!Çv?MŒJê„?¨¦$ëpte?=·Ð•t?Þèc> Ði?ôï9°\?$Dù‚2¿‚¬§V_]e?T5AÔ}r?$EdXÅy?/4×i¤¥‚?´Éá“N$x?ÕÌZ Hû??ÓÝu6äŸI¿ô4`�ôie?N)¯•Ð]R?<-?p•'P¿Ÿ��·±y?ŒJê4A?êÐéy7D?F´Swe7¿›ÿW9Ò)¿õ×+,¸¿·_>Y1\¿Îp>?ŒP¿óWÈ\9¿Êß½£Æ„h?MÛ¿²Ò¤4¿QÝ\ümOp?™€_#Ip?µRäG^¿P”i4¹h?¢&ú|”'?ú  RðT?]§‘–ÊÛA¿½Œb¹¥Õp¿6�:èR¿_–vj.7h?¨5Í;NÑq?A,›9$µp¿ª'ó�¾Iƒ¿…BB•z¿Ýµ„|гy¿z4Õ“ùG¿@7n1?‡¿÷è ÷‘[s¿áy©Ø˜×q¿ �\7¥¼v¿­lò–«_¿�¾IÓ hþ>©|š“i?Åoò[t‚¿Lþ'÷Žz¿5Ð|ÎÝ®‡¿]n0Ôa…{¿q­ö° x?Ø�›6ã„?àòX32È}?ûë܈? ß÷o^l?F^ÖÄ?a2U0*©?úC3O®)`?�¾IÓ h>?F–̱¼«.¿ÐGqhd¿Þ3ßÁO?Vbž•´â[?�ôiý¡i?+Qö–r¾x?lxz¥,Cl?Öª]ÒZ¿VJÏôci¿f.py¬9?D6�.6­T¿H£'ÛÀm¿Ô x'Ÿn?¸®˜Þ^¿‚ÿ‚i¿_%» ”d¿Aš±h:;i¿ø§T‰²·d¿M„ O¯d¿¨¦$ëpte¿ƒM�GÅÿm¿ífF?Ni¿!‘¶ñ'*K?›Žn/f¿ü‹ 1“¨W?¾‰ jøV?dæ—Çšq¿ú~j¼t“H?k~ü¥E}b¿ÛÃ^(`;X¿Ù'€bdÉl¿ÖÄ_Ñ­w¿}\*Æùk¿‡¨ÂŸáÍJ?êè¸Ù•f?©J[\ã3y¿#ö  ‰¿÷­Ö‰Ë�¿g¹ltÎO�¿�÷ª• ¿„¿�•C‹lç‹¿¬q6|¿ææÑ=ëz¿AfgÑ;€¿j¤¥òv„s¿ò'*ÖTV¿+~©Ÿ7U?…Ѭlò†¿ƒ¡+Üò�¿Û4¶×‚Þ‹¿ŽyqÈ‚¿XÎüjp?GN¶�;€?æmrø¤s?®óo—ýºƒ?‘¹2¨68Q?¯ ?8Ÿ:v?|b�*ß3b¿ÅËÓ¹¢”@¿U¯²¶)^¿ß6S!‰g¿dæ—Çšq¿²Úü¿êÈa¿[ ³ÐÎiF¿†7kð¾*G?ì†m‹2k?Ð_è£çV?Âj,amŒm¿oÖà}U.t¿™cyW=`^¿’$W@¡n¿ú™zÝ"0v¿%]3ùf›[?ýÇ�WËm¿�Ò¥I*s¿¸…ëQ¸n¿rö´Ã_s¿²ó66;r¿úœ»]/Mq¿O\ŽW zr¿fI€šZ¶v¿zUgµÀs¿cbóqm¨X¿ò]J]2Žq¿�^ PjD¿®I·%rÁI¿±‰Ì\àòx¿*ÅŽÆ¡~W¿ž]¾õa½q¿_`V(Òýl¿ë6¨ýÖNt¿ö–r¾Ø{�¿Ü�µÛ.4w¿i¬ý�íÑ[¿ümO�Øî?HŠÈ°Š7‚¿ CÇ*q�¿aÝxwd¬†¿äIÒ5“o†¿Úç6á^‰¿F˜¢\¿�¿o­mŽƒ¿ IJ™C‚¿ò”Õt=Ñ…¿mò–«{¿/ó:âp¿ª ãn­U¿“¬ÃÑUº‹¿�Cý.l͆¿(CUL¥Ÿ�¿¸u7O…¿E+÷³Ba?£ x|{w?€Ÿqá@Hf?sõc“üˆ?Í®{+S¿ˆ»zm?l\ÿ®Ïl¿h^»ï^¿+ømˆñj¿¾PÀv0bo¿µÑvLÝu¿&«"ÜdTi¿ÿ>ãÂ�`¿ ¼“O�mI¿â̯æÁ\?ümO�Øî?é� ¸çùs¿ÿ< $}z¿�Ž«‘]ii¿\Y¢³Ì"t¿`�ºòy¿·_>Y1\ý>’]i©÷t¿Q¿ [³•w¿¬r¡ò¯åu¿¹û-Îx¿ˆ�)t^cw¿c}“Ev¿ˆe3‡¤v¿¨ükyåz{¿¥Kÿ’T¦x¿w‚ý‡¿±ýdŒ³‡¿=Õ!7à ˆ¿BÍ�*ŠW‰¿_)ËǺˆ¿ŠsÔÑq5‚¿©„'ôú“ˆ¿ñ»é–â¿y=˜Ÿ€¿�ñ}q©J‹¿ Tƿϸ€¿—á?Ý@�‡¿‰Ð6®‡¿}w+Kt–‰¿2ZGUD�¿eŽå]õ€‰¿ÝµßÚ‰‚¿3¥õ·à¿ÁV ‡3�¿�.‹‰ÍÇ•¿ðmú³)’¿Í?ú&Mƒ’¿ê=•Óž’“¿·Ï*3¥õ—¿3d’‘³�¿g}Ê1YÜ�¿öyŒòÌË‘¿Ã×׺Ô�¿8½‹÷ㆿ•¸ŽqÅÅ�¿o.2•¿]3ùf›“¿Fу—¿og_y�ž’¿íÿ°¥Gs¿Ó–x@Ùd¿OYM×]w¿nùHJzX?@KW°�x‚¿=ñœ- ´n¿C®Ô³ ”‡¿×¥Fègê…¿'ù¿b ‡¿`�‰”fóˆ¿ù�&3ÞVŠ¿ qåì�ц¿ñ.ñ�˜…¿82�üÁÀƒ¿„ äÙå[¿þÓ x'¿~p>u¬RŠ¿0ôˆÑs �¿ó�èžu�†¿\;Qi‹¿ü´W�¿<À“.«€¿ó‘”ô0´Š¿UܸÅüŒ¿á iTàd‹¿:d¯wŒ¿ÈбƒJŒ¿i©÷TN‹¿Åä 0óŒ¿0ôˆÑs �¿~U.TþµŒ¿^×/Ø Û†¿zVÒŠo(Œ¿ŠXİØ„¿–= lÎÁƒ¿èº�ê�¿œˆ~mý„¿Ÿp]1#Œ¿±Ûg•™ÒŠ¿ Rðr¥Ž¿:=ïÆ‚Â�¿2ZGUD�¿€ ;¨Ä…¿´9Îm½‚¿ŒJê4‘¿«˜J?áì–¿1C㉠Γ¿ê³®+f”¿‡5•Ea•¿ìm3⑘¿äGˆ,’¿Ì³’V|C‘¿Õw~Q‚þ’¿„H†[Ï�¿l±Ûg•‰¿ˆò-$`„¿1 íœf�–¿fk}‘Ж“¿ôÂ� #½˜¿>—©Ið†”¿>³$@M-{¿Åä 0óm¿E×…œO}¿™ò!¨½¿‹‹£rµ„¿ú´ŠþÐÌs¿y7R¶HŠ¿ÿêqßj�ˆ¿'á_�‰¿²GWéAØ)V ÂŒ¿á(yuމ¿QKs+„Õˆ¿äÜ&Ü+󆿶ŸŒñaö‚¿ñƒó©c•‚¿:Ì—`�¿eª`TR'�¿€J•({K‰¿´å\Š«ÊŽ¿`>Y1\�¿4fõ‚Oƒ¿âX·ÑŽ¿z4Õ“ùG�¿˜0š•íCŽ¿ë²×»�¿H¾�D„�¿�¾IÓ hŽ¿:±‡ö±‚�¿®‚èÚ�¿*Æ3hè�¿šÌx[鵉¿ Ü¶ïQ�¿HÜÖž‡¿ßºñîȈ¿N�GÅÿ‘¿ÝД�~P‡¿Ò§Uô‡fŽ¿•*Qö–rŽ¿^žÎ¥„�¿Ü½Ü'G’¿eª`TR'�¿Täqs*‰¿�ô¾ñµg†¿1`ÉU,~“¿{.S“à ™¿;:®Fv¥•¿^üo%;–¿1îÑZÑ–¿ýi£:Èš¿dT8‚”¿ñ,AF@…“¿0�[wó”¿"àªÔì‘¿ CÇ*q�¿‚‘—5±À‡¿ÔÔ²µ¾˜¿f3‡¤J–¿ ˜À­»yš¿E/£Xni•¿$��‘—5�¿`"Þ:ÿvy¿ŒøNÌz1„¿yxÏ�åi¿ÉËšXà+Š¿öa½Q+L¿ÏÛØìHõ�¿üU€ï6oŒ¿î }°Œ �¿�¸4J—Ž¿ñ»é–â�¿yé&1¬Œ¿ÎOqxµŒ¿ Šc�Üšt["‡¿óÇ´6�í…¿äqs*�¿ÌFçüÇ‘¿-î?2:�¿­Û ö[;‘¿Pÿ>ã‘¿ŸZ}uU †¿9 3¦�¿À3‰z‘¿ž´pY…Í�¿ÛÄÉýE‘¿´­f�ñ}‘¿~Œ¹k ù�¿±ÉW)‘¿–Tÿ ’‘¿¸æŽþ—k‘¿tCSvúA�¿/ÛN[#‚‘¿sIÕvŒ¿¬µ“ýó”¿àœ¥½Á—¿*¨¨ú•Η¿!�àFÊ™¿3âÐ(]š¿­P¤û9™¿ÛûTˆ•¿…ëQ¸•¿‡Q<¾½›¿Îp>?Œ ¿ð‰Ð6ž¿@½5_%Ÿ¿t ]‰@õŸ¿JÒ5“o¶¡¿Až]¾õa�¿û"¡-çRœ¿ªd¨âÆ�¿œ4 Šæœ¿ûõ×+,˜¿Ó…Xý–¿1#¼=¡¿_b,Ó/Ÿ¿[éµÙX‰¡¿ˆØÒ£©ž¿¥-®ñ™ì�¿®ð.ñ�ˆ¿ñõµ.5B�¿]7¥¼VB‡¿v¦Ðy�]’¿an÷rŸŒ¿p² Ü�:•¿$^žÎ¥”¿—Ép<Ÿ•¿FEœN²Õ•¿JDøAc–¿>ʈ @£”¿"¨½ ”¿|dsÕ+Nµ–¿Ð_è£ç–¿=ð1Xqš¿†ŽTâ:–¿ïV–è,³˜¿!æ’ªí&˜¿gaO;ü5™¿·zNzßøš¿7¥¼VBw™¿íE´Sw•¿Sz¦—Ë”¿» ÿé œ¿Ü~ùdÅ ¿yÎZŸ¿9DÜœJ ¿5cÑtv2 ¿¬ÆÖÆØ¡¿Lo.2ž¿•»ÏñÑ✿+£‘Ï+ž¿øXŽ��œ¿àõ™³>嘿€µjׄ´–¿u­½OU¡¿GN¶�; ¿³˜Ø|\¢¿ ÞŒš¯’Ÿ¿”ùGߤi�¿Ñ=ë-Š¿Ùƒkîè�¿OYM×]‡¿kaÚ9Í’¿…³[Ëd8Ž¿xšÌx[é•¿M¾ÙæÆô”¿3#…–•¿Í°QÖo&–¿·&Ý–È—¿ˆht±3•¿Ððf ÞW•¿‡¢@ŸÈ“”¿ZÔ'¹Ã&’¿FÍWÉÇî’¿/1–é—ˆ—¿BÍ�*ŠW™¿“âã²ó–¿5cÑtv2˜¿I‚pš¿Lÿ’T¦˜“¿.sž±/™¿²ŸÅR$_™¿¢E¶óýÔ˜¿�Ù®Ð˘¿Í>�Qžy™¿ÜŸ‹†ŒG™¿´è� ¸ç™¿Ž!8ö왿é8h°™¿HPüs—¿»Ð\§‘–š¿ Ý!ʼn–¿žD„4–¿•Ö߀š¿' ‰°áé•¿G�J±£q˜¿in…°K˜¿F™ 2ÉÈ™¿ù0{ÙvÚš¿•-’v£�™¿�P3¤Šâ•¿‰xëüÛ•¿5š\Œ�uœ¿Ymþ_uä ¿Æiˆ*üž¿æèñ{›ž¿ÍwðП¿Ef.py¬¡¿áC‰–<ž¿qN`:�¿\ætYLlž¿wJëÿœ¿À“.«°™¿ãŒaNÐ&—¿Sìhêw¡¿Ùƒkî蟿ÇeÜÔ@ó¡¿°8œùÕ ¿|š“™�¿†Ê¿–W®‡¿4-±2ùŒ¿Ü¸ÅüÜЄ¿û²´Ss¹‘¿ýˆ_±†‹Œ¿ÿæÅ‰¯v”¿>”hÉ“¿™¹Àå±f”¿Å_Ñ­×”¿Z ¦–­•¿ú—¤2Å”¿fN—ÅÄæ“¿û!6X8I“¿ÁÅŠLÃ�¿Ç ¿›nÙ‘¿2«w¸–¿ Й´©º—¿ÇK7‰A`•¿`wºóÄs–¿YŠä+�”˜¿©KÆ1’=’¿iÅ7>[—¿¶il¯½—¿� 1—Tm—¿�x]¿`—¿„d˜¿$ ÂP¨—¿ºhÈx”J˜¿ �R O˜¿ÇF ^×/˜¿EÖJí•¿Ôšæ§è˜¿¯²¶)•¿&W±øMa•¿4ö%¶˜¿†¬nõœ”¿ëþ±—¿ÅTú g·–¿Ú­e2Ï—¿ÆOãÞü†™¿ò ú'¸˜¿µ¨Or‡M”¿vß1<ö“¿Á˜2p@›¿¨ÆK7‰A ¿k�=&Rš�¿Ûü¿êÈ‘ž¿#…–uŸ¿èú>$D¡¿ýN“o+�¿¦ï5Çeœ¿ ¼“O��¿ÔÖüøK›¿ñ�˜õb˜¿:轕¿á(yuŽ¡¿Ý[‘˜ †Ÿ¿@¢ ±ˆ¡¿y ²HŸ¿Xÿç0_^�¿‚¬§V_]…¿ì†m‹2‹¿,.ŽÊMÔ‚¿°‘$W@‘¿¡1“¨|Š¿Ý_=î[­“¿Þ®Õö’¿YÝê9é}“¿ÿ¬U»&”¿´¾L!•¿UÞŽpZð’¿þ'÷Ž“¿.2¥’¿AfgÑ;�¿^=ð1X‘¿3#…–•¿ƒ1"QhY—¿dt@ö픿Ÿ”I m–¿¿ñµg–˜¿;%¯Î‘¿°Tð2Ö¿�{ž?mT—¿C€ ˆ—¿Ãžvøk²–¿�¿´¨Or—¿‹p“Qe—¿ìø/˜¿™*•Ô ˜¿Õ‘#��‘—¿‰Î2‹Pl•¿*ãßg\˜¿4¢´7øÂ”¿n†ðùa”¿³–Òþ˜¿¼çÀr„ ”¿(ðN>=–¿2«w¸–¿]¿ðJ’—¿vÂKpꙿÙ] ¤À˜¿__ëR#ô“¿S!‰—§“¿ê K< lš¿£ZD“7 ¿UܸÅüœ¿�¨PÝ\ü�¿ç§8¼Zž¿¶õÓÖü ¿üSªDÙ›¿Ü¡a1êZ›¿ÙYLœ¿5{ ²š¿B{õñÐw—¿ƒS°ÆÙ”¿Ì_!se ¿P¨§�Àž¿»ì×�î<¡¿UkaÚ9�¿²Ø&�µ�¿Öß—ª„¿¢*¦ÒO8‹¿)!XU/¿ƒ¿ÜGnMº-‘¿p_ÎQŠ¿ÚYôNÜ“¿.Ç+=)“¿¤aQ§“¿ï<ñœ- ”¿¢ñDçᔿ”¤k&ßl“¿«#G:“¿×gÎú”c’¿þÓ x'�¿.â;1ëÅ�¿2åCP5z•¿ƒNt —¿Öß—ª”¿üVëÄåx•¿•Öÿ9Ì—¿õc“üˆ_‘¿†q7ˆÖŠ–¿q:É–¿'f½ʉ–¿ÑÌ“k d–¿ïô¥·?—¿¿Hh˹—¿b†ÆAœ—¿O—¿'ÚUHùI•¿RÕQ÷˜¿hÊN?¨‹”¿l³±󬔿x¹ˆïĬ—¿{NzßøÚ“¿=ÓKŒeú•¿‡¯yUg•¿bX9´È–¿¶ºK⬘¿íGŠÈ°Š—¿’[“nKä’¿â"÷tuÇ’¿ª`TR' ™¿ƒ¦%VF#Ÿ¿ Ôbð0훿 ƈD¡e�¿vŒ+.ŽÊ�¿ JÑʽ ¿2¥žÐ›¿ þ~1[²š¿ð³%«"œ¿}=_³\6š¿˜Št?—¿‚SHÞ9”¿•ñï3. ¿ YÝê9é�¿ï®³!ÿÌ ¿¾NêËÒN�¿|µ£8G�¿“ÆhUM€¿ŸrL÷‰¿¨5Í;NÑ�¿cD¢Ð²î�¿`ÉU,~Sˆ¿A~6rÝ”’¿n‹2d’‘¿Äê�0 X’¿kaÚ9Í’¿É’9–wÕ“¿L5³–’¿SÏ‚PÞÇ‘¿¡0(Óhr‘¿¿ž¯Y.�¿çãÚP1Î�¿ú—¤2Å”¿ äÙå[–¿nÀ燓¿?ýgÍ�¿”¿=,Ôšæ—¿Ižëûp��¿PáR)–¿âvhXŒº–¿K”½¥œ/–¿ò”Õt=Ñ•¿‡Áü2W–¿Vïp;4,–¿é˜óŒ}É–¿Ù´Rä—¿ï7ÚqÃï–¿š †s 3”¿2tì —¿Lÿ’T¦˜“¿Òq5²+-“¿³³è� ¸—¿�Øî û’¿‰a‡1éï•¿›�kC•¿¼Ì°QÖo–¿aºÙ˜¿ 'iþ˜Ö–¿ÙêrJ@L’¿ŠsÔÑq5’¿Jy­„î’˜¿M.ÆÀ:ž¿¤ý°Víš¿| Vœj-œ¿õfÔ|•|œ¿vþÓ  ¿ò^µ2á—š¿ÁãÛ»}™¿øý›'¾š¿eqÿ‘éЙ¿š]÷V$–¿Î8 Q…?“¿v“þ^ Ÿ¿mq�Ïdÿœ¿Ùµ½Ý’ ¿à ½úx蛿’?xî=Œ¿6t³?Pn{¿U2TqㆿÆk^ÕY-€¿Xç�½Þ�¿ÕËï4™ñ†¿sGÿ˵h‘¿ Ñ!p$Ð�¿³]¡–±‘¿S•¶¸Æg’¿ëª@-“¿iŒÖQÕ‘¿a7l[”Ù�¿O!WêY�¿Ôœ¼ÈüŠ¿+Nµf¡�¿~q©J[\“¿øŠn½¦•¿ÓÀÍâÅ’¿^Iò\߇“¿¥ƒõó•¿ Á¦Î£â�¿fË-­†”¿µQ�d=•¿¹‹0E¹”¿;á%8õ�”¿ p•¿0œk˜¡ñ”¿âàI —•¿#„GG¬•¿åðI'L•¿ø8Ó„í'“¿óÇ´6�í•¿ò˜ù~’¿y\T‹ˆb’¿K±£q¨ß•¿0ñGQgî‘¿îì+ÒS”¿@‡ùòì“¿Âü2W•¿Þæ�“¼—¿ž'ž³„–¿Á;ùôØ–‘¿¬Xü¦°R‘¿÷x!˜¿† %Ì´�¿{× /½ý™¿¶ÚÃ^(`›¿o›©�Ä›¿†U¼‘y䟿kóÿª#Gš¿ŠUƒ0·{™¿?p•'vš¿7ünºe‡˜¿è¼Æ.Q½•¿�r0›Ã’¿‚sF”öŸ¿„aÀ’«Xœ¿‹Áôoª(^emSœ¿“r÷9>ZŒ¿ta¤µûu¿&ÿ”*Q†¿IJzZ�|¿l—6–Ž¿„fÚþ•…¿$Ð`SçQ‘¿~©Ÿ7©�¿�%W±øM‘¿¢–æV«‘¿p°71$'“¿„H†[Ï�¿Ù蜟â8�¿S“à iT�¿*´t‹¿±S¬„¹�¿çqÌ_!“¿:A›>锿õ¼ ƒ’¿úÑpÊÜ|“¿?�{ó–¿JbI¹û�¿�÷ª• ¿”¿'½o|확¿Dioð…É”¿è0_^€}”¿³ëÞŠÄ•¿›™�EN·ìÿ°•¿ß4}vÀ•¿£êW:ž•¿¨ŽUJÏô’¿dê®ì‚Á•¿Ñ•Tÿ ’¿ à-� ø‘¿¼?–¿Þ�FN¶‘¿ÚŒÓUø“¿ÇHö5“¿VF#ŸW<•¿C«“3—¿zã¤0ïq–¿”¿{G� ‘¿V,~SX©�¿­‡/E˜¿Ð{cŽ�¿:ç§8¼š¿õƒºH¡,œ¿ù*8œ¿ÅUeßÁŸ¿ l#ö™¿jg˜ÚR™¿ÕY-°ÇDš¿SäG˜¿yxÒÂe•¿ó9w»^š’¿Þå"¾³ž¿.�5#ƒœ¿Õ¯t>�‘¿kò”Õt=‘¿ÇðØÏb)’¿Eñ*k›â‘¿zßøÚ3K’¿³ï«r¡’¿Tå{F"4’¿í‚Á5wô�¿e�P3¤Š’¿îаu­�¿àòX32È�¿sƒ¡+Ü’¿Û†Q<¾�¿[{ŸªB‘¿"‹4ñ�¿u­½OU¡‘¿¶Û.4×i”¿.«°à’¿È·w úÒ‹¿£�dVïp‹¿vß1<ö“¿=¸;k·]˜¿øü0Bx”¿¾l;m�–¿ffffff–¿} yçP†š¿-?p•'–¿ÕyTüß•¿ŽÍŽTßù•¿jjÙZ_”¿ˆ NÒü‘¿‘+õ,å�¿LkÓØ^ š¿ÒŦ•B —¿ëD2äØš¿QÜñ&¿E—¿3NCTáÏ€¿¾†à¸Œ›:¿Áá©iw¿ª ãn­e¿W³Îø¾¸„¿�C¨R³z¿>Zœ1Ì Š¿R_–vj.‡¿fÁÄE�‰¿¬<�°S¬Š¿±ÀWtë5�¿ðÐïû‡¿Ñ’ÇÓò‡¿\>’’††¿öa½Q+L¿ Xr‹ß„¿2ZGUD�¿Æ3hèŸ�¿/¢í˜º+‹¿(‚8'0�¿bMeQØE‘¿g¶+ôÁ2†¿úC3O®)�¿ Ñ!p$Ð�¿Â26t³?�¿¨Ï ¡�¿ô7¡‡�¿ü©ñÒMb�¿ž´pY…Í�¿�žwcA‘¿*©ÐDØ�¿HÝξò �¿Útp³x‘¿ÙÎ÷S㥋¿KW°�x²‹¿Yá&£Ê�¿ì…¶ƒ‹¿#KæXŽ¿Ï‚PÞÇÑŒ¿½á´àE�¿.È–åë’¿THÞ9”‘¿)ë7Ó…ˆ¿·œKqUÙ‡¿8h°©“¿:“6U—¿û]Øš­¼”¿/Ý$�•¿Š”fó8 –¿¶-Êl�™¿i�‚§�+•¿`uäHg`”¿�.‹‰ÍÇ•¿^fØ(ë7“¿CÅ8 ‘¿éEí~à‹¿Aó9w»^š¿ZGUDÝ—¿ùMa¥‚Šš¿Tÿ ’!Ç–¿.c}“{¿”ö_˜LU?ú`ºÙo¿Äî;†Ç~V¿u>—©Ið†”¿GW#»r¿CpìÙsi?j>"¦DR¿Ü ö[;QB?ç<š$v¿‡m‹2db¿¸Z'.Ç+€¿%�»¶·{¿~©Ÿ7©€¿[AÓ+£�¿,ºõš„¿]S ³³è}¿3ÀÙ²|}¿¢ ê[æty¿ Ôbð0ík¿`�ºòy¿ßÅûqû僿JíE´S‡¿(»™Ñ�†ƒ¿9ÒyYƒ¿3âÐ(]Š¿|�Ô—¥�z¿ 6uÿ‡¿æ”€˜„ ‰¿·Ï*3¥õ‡¿À"¿~ˆ †¿(¸XQƒiˆ¿kÔC4ºƒˆ¿­jIG‰¿ÀYJ–“PŠ¿ ÂÜîå>‰¿˜iûWVš„¿¹§«;‹¿íÿ°¥Gƒ¿Õíì+Òƒ¿2¯#Ù@Š¿¶ŸŒñaö‚¿‘*ŠWYÛ„¿XSYvQ„¿ pA¶,_‡¿ëqßj�¸Œ¿­¢?4ó䊿ŒKUÚâ¿ã4ôO€¿òz0)>Ž¿.�_x%É“¿—⪲ïŠ�¿èKo.’¿ÁäF‘µ†’¿[ ³ÐÎi–¿ºi3NCT‘¿d=µúêª�¿þ¸ýòÉŠ‘¿öa½Q+L�¿ÁŒ)XãlŠ¿ôú“øÜ †¿!‰—§s•¿ ²ºÕ“¿5Dþ o–¿ûËîÉÃ’¿ÓõD×…l¿�(í ¾0i?¢&ú|”'?b£¬ßLLW?ñ*k›âqq¿ã©GÜÖV¿t^c—¨Þz¿`Xþ|[°t¿îÎÚmz¿W•}Wÿ{¿ý¢ý…�¿Ùéu‘By¿EœN²Õåt¿|îû¯ss¿{/¾h�W¿p À?¥Jt¿ë‹„¶œK�¿¨½ 4„¿g¹ltÎO�¿î^î“£�¿/1–é—ˆ‡¿*T7Ûs¿–?ß,…¿6±ÀWtë…¿Æ5>“ýó„¿8Ÿ:V)=ƒ¿A¼®_°†¿j‰•ÑÈç…¿lèf Ü†¿s€`Ž¿‡¿ßÝÊ�e†¿Åã¢ZDƒ¿UQ¼ÊÚ¦ˆ¿Ä$\È#¸�¿ª$ïÊ€¿àªÔì�†¿=`2åC€¿ž#ò]J]‚¿eS®ð.�¿ßøÚ3K„¿)Íæqˆ¿�?üü÷à…¿$EdXÅy¿ð¾**ÿz¿šë4ÒRy‹¿Ž\7¥¼V’¿‹RB°ª^Ž¿k,amŒ��¿©MœÜï�¿A€ ;¨”¿‘ñ(•ð„Ž¿ÔGà?ÿ�¿J•({K9�¿@ß,ÕŒ¿™�Êø÷‡¿[AÓ+£�¿X zR&5”¿[$íFó‘¿5s»—û”¿Ê7Ûܘž�¿a2U0*©S¿ s‚69|r?ÎáZía/d?ƒ5Φ#€k?â !Ê´`¿)ÍæqH?סš’¬Ãq¿Õê««µh¿$cµùÕq¿î[­—ãu¿F~ý,|¿uÊ£aQq¿Gä»”ºdl¿Ðî�b€Dc¿Ê¤†6P?5Ð|ÎÝ®g¿]n0Ôa…{¿Ï£âÿލ€¿k¹3 çz¿ž Ž’Wçx¿=·Ð•„¿_–vj.7h¿hìK6l�¿€›Å‹…!‚¿w0bŸŠ�¿¸®˜Þ~¿kQLÞƒ¿÷"ÚŽ©»‚¿$µP29µƒ¿ŒøNÌz1„¿Ánض(ƒ¿Y |E·~¿CÉäÔÎ0…¿2ZGUD}¿†ZÓ¼ã}¿ú(#.�‚¿ZI+¾¡ðy¿[aú^Cp|¿ž Ž’Wçx¿*ÿZ^¹~¿�ñaö²í„¿h+ømˆ�¿uæn¿“«Xü¦°r¿®E жš…¿X9´Èv¾�¿®¶bÙ=‰¿Ì¶ÓÖˆ`Œ¿Õ´‹i¦{�¿ÛP1Îß„’¿F™ 2Éȉ¿”4LkÓˆ¿Mž²š®'Š¿-ê“Üa‰¿ùNÌz1”ƒ¿o¸�Üšt{¿d–= lΑ¿º„CoñðŽ¿t#,*ât’¿xî=\rŒ¿úœ»]/M¿÷Ý—3Ûu?Ô|•|ì.p?°qý»>sv?f½ʉvE¿§v†©-u`?A‚âǘ»f¿}“EÖZ¿d°âTkaf¿^H‡‡0~j¿â‘xy:Wt¿¢µ¢Íqnc¿«B±læ`¿ ûrf»BO¿_%» ”d?Û¥ ‡¥�_¿1`ÉU,~s¿ç4 ´;¤x¿‰ëW\u¿‚Ç·w úr¿î^î“£�¿T�^ PZ¿÷ʼUסz¿)èö’Æh}¿ò$éšÉ7{¿`=î[­w¿]S ³³è}¿uæ~¿Pª}:3€¿´ç25 Þ€¿µTÞŽpZ€¿ÎR²œ„Òw¿å ZHÀè‚¿j‰•ÑÈçu¿÷è ÷‘[s¿Ç{ö|¿Òà¶¶ð¼t¿1%’èet¿Ê‰vR~r¿Tâ:Æw¿åòwï¨�¿h^»ï~¿ÖJíE´]¿€·@‚âÇh¿w‚ý×¹iƒ¿Ô($™Õ;Œ¿[ ³ÐÎi†¿Ä$\È#ˆ¿j4¹눿tÑ�ñ(•�¿ƒ1"QhY‡¿S’u8ºJ‡¿¦~ÞT¤Âˆ¿Œ¾‚4cÑ„¿ºÀå±fd€¿­¥€´ÿv¿ô7¡‡�¿ìjò”Õt�¿°‘$W@‘¿iSu�l®Š¿f¡Ø šf?Þ­,ÑYf�?´­f�ñ}�?_%» ”„? Ž’Wçp?ràÕrg&x?À“.«°Y?¬ä.Âd?Lþ'÷ŽZ?°t>{?âàI —…?JΉ=´�u?¹û-Îh?ùNÌz1”S?)[$íF_?²¼«0i?dËòuI¿zR&5´x?äRìhL?hY÷�…è@?dËòuI?¸­-Y1\í>Rœ£ŽŽ«a?ú´ŠþÐÌc?½Ç™&l?i?st´ª%]?¯]ÚpXJ¿{£V˜¾×0?Ot]øÁùt?zpwÖn»p?ÎR²œ„ÒW¿/÷ÉQ€(x¿E×…œOm¿: ûvq¿!¯“âãs¿Ì)1 ‚¿š±h:;l¿z7ej¿úœ»]/Mq¿ÖJíE´m¿ÁÿV²c#P¿Û4¶×‚Þ[?+¢&ú|”�¿æ?¤ß¾|¿ÿ”*Qö–‚¿>BÍ�*Šw¿Óhr1Ö�?Ó–x@‰?å™—Ãî‹?S°ÆÙt�?[AÓ+£�?)H4�"†?E| V|?«7U†�?ÛL…x$^~?ð¾**ÿz?É;‡2TÅt?]Þ®Õ~?¾…uãÝ�?·˜Ÿš‚?/†Èéë‰?+¿)¬T€?å}Í‘u?¨:äf¸o?Ön»Ð\§q?J³yów?1е/ W?S"‰^F�?[#‚qpéh?ª ãn­e?ú~j¼t“h?Ã}äÖ¤Ûr?W'g(îxc?<†Ç~Ka?8ºJw×Ù`?¬<�°S¬Z?x›7N ó^?1'h“Ã'm?�^ PjD?öµ.5B?s?—Mõdþq?÷�…è8b?ìQ¸…ëq?m±o?\>’’†v?ºÀå±fdp?nYk(µW?žB®Ô³ d?¾ù  R€?cbóqm¨x?!¯“âãC?£’°o'q¿î[­—ã5¿t^c—¨ÞZ¿ÎˆÒÞà c¿å Åoò{¿úïÁk—6\¿W?6É�øU¿ðPèyb¿tí è…;W¿¾¤1ZGUS?Ø€qåìm?PqxµÜy¿²fd�»s¿ÈCßÝÊ}¿˜3Ûú`i¿ÇF ^×/ˆ?ûÍÄt!V�?©¢x•µM‘?d?‹¥H¾’?_\ªÒ׈?€»ì×�îŒ?§çÝXP„?ÿ˵hÚ†?¥Kÿ’„?3mÿÊJ“‚?߀cÏ~?�ñaö²í„?„EEœN²…?g›Ó–ˆ?8KÉrJ�?�yÈ”A…?¯ÒÝu6ä?îÎÚmz?ƒÁ5wô¿|?¿ò =E�?Ï/JÐ_èq?è‚ú–9]†?�^¸sa¤w?¿ïß¼8ñu?ÿ$>w‚ýw?¬TPQõ+}?êÐéy7t?ÖÇCßÝÊr?~TÃ~O¬s?g Ü¶ïq?¦�Õ°ßs?%xC8y?H£'ÛÀm?«Íÿ«Ž|?;ÿvÙ¯;}?’xy:W”r?Z.�óS|?4óäš™}?ª$ïÊ€?”¥Öû�v|?¨p©;j?ˆ…ZÓ¼ãt?„EEœN²…?[{C‚? Ž’Wçp?%És}B¿—=Ô¶ad?¯&OYM×S?-ÒÄ;À“F?Ælɪ7i¿nùHJzX?‘›á|~X?‘*ŠWYÛT?ƒú–9]S?e9 ¥/„l?• •-¯|?°t>”hÉãi‰?±ú# –Œ?ººc±M*Š?#LQ.�_ˆ?.9?oò[t²ÔŠ?V(Òýœ‚Œ?)èö’Æh�?ŒÙ’Un’?XŽ��<‹?-ÒÄ;À“†?æ>9 ƒ?${„š!U„?GT¨n.þ†?‹3†9A›|? Š·˜Ÿ‹?î$"ü‹ �?ßà “©‚�?¯A_zûs�?T1³Ïc„?EeÚʢ€?0Ùx°Ån?¥-®ñ™ì?1'h“Ã'}?ïU+~©?K‘|%�‚?ùK‹ú$wx?óWÈ\„?+ÁâpæWƒ?€I*SÌA€?Ÿu�–=„?…Ѭlò†?aÝxwd¬†?à+ºõš„?š±h:;|?B_zûsÑ€?Ï‚PÞÇÑŒ?&åîs|´ˆ?Ö©ò=#z?nùHJzX?ÒŦ•B w?ïU+~©o?Í΢w*àn?{®GázD¿1?74e§o?@ù»wÔ˜p?’³°§þj?„-vû¬2s?®ÕöB{?ù»wÔ˜ƒ?Ñ<€E~ýP¿]ú—¤2ÅL?ßPøl\¿™cyW=`^?¢–æV«‘?ô4`�ôi•?Ã��,`—?ï7ÚqÃï–? PSËÖú’?${„š!U”?ÊTÁ¨¤N�?^-wf‚‘?ô‡fž\S�?{g´UId�?ëâ6À‹?HN&nÄ�?ÇG‹3†9‘?9c˜´É‘?N·ìÿ°•?sž±/Ùx�?È}«uârŒ?ߺñîȈ?|DL‰$z‰?/3l”õ‹?Øó5Ëe£ƒ?8‡kµ‡½�?Ñ’ÇÓò‡?pî¯÷­†?®ž“Þ7¾†?TªDÙ[ʉ?äIÒ5“o†?O²žZ}…?¦^·Œ…? ¨lXSY„?DüÖM…?wLÝ•]0ˆ?‡m‹2d‚?ì3g}Ê1‰?•óÅÞ‹/Š?·´÷XŠ?½8ñÕŽâŒ?dËòuŽ?­¦ë‰® �?AØ)V ÂŒ?M„ O¯„?àõ™³>åˆ?äL¶ŸŒ‘?³´Ss¹Á�?ðHPüƒ?–ëm3âq?Ot ‡~?-ê“Üay?GçüÇ�w?mäº)åµb?ÙÎ÷Sã¥{?Ÿ<,Ôšæ}?àõ™³>åx?ÅrK«!�?¶J°8œù…?^gCþ™AŒ?Îà L§u[?z7ej? -ëþ±M?{ˆFw;s?Ü›ß0Ñ •?àØ³ç25™?]øÁùÔ±š?Ÿ¡¼�£™?”ÃÕ—?¤Rìhê—?.=šêÉü“?º½¤1ZG•? ÆÁ¥cΓ?Áªzù�&“?&9 {Ú‘?ò%TpxA”?î²_wºó”?É;‡2T•?×mPû­�˜?ì†m‹2”?ˆ NÒü‘?É!âæT2�?ôj€ÒP£�?’²EÒnô‘?o¸�Üšt‹?áñí]ƒ¾”?tbíc�?9¸tÌyÆŽ?¸®˜ÞŽ?øS㥛Ä�?Lo.2Ž?bùómÁR�?{‚Äv÷�?zVÒŠo(Œ?o×KS8�? Ü¶ïQ�?Ÿ¡¼�£‰?Ì EºŸS�?ðk$ Â�?Чwñ~Œ?@ù»wÔ˜�?˜Û½Ü'G‘?†:¬pËG’?[{ŸªB‘?2ZGUD�?c%æYI+Ž?„¹ÝË}r”?² 0(Óh’?Žs›p¯Ì‹?8‡kµ‡½€?±Ûg•™ÒŠ?Tÿ ’!dž?mɪ7…?Iô2Šåv?ò'*ÖT†?\«=ì…†?0…Ì•A…?>]ݱØ&…?�C¨R³Š?|Ô_¯°àŽ?¶�ö?Àz?CÅ8 �?zóWÈ|?·Òk³±ƒ?�ôiý¡™?ãÿލPÝœ?»ñîÈXmž?�[wóT‡œ?ìhêwa›?È·w úÒ›?0 ÃGÄ”˜?�ÁŠS­…™?P5z5@i˜?ZGUDÝ—?"ÇÖ3„c–?Á¨Sݘ?@ÜիȘ?,g*™?ÅÇ'dçmœ?ãÉ¡f˜?AŸÈ“¤k–?1 òn”?èy’tÍ”??�{ó–?üÈ­I·%’?Ó„í'c|˜?8Ÿ:V)=“?€'-\Va“?3Mg'ƒ“?ÊQ€(˜1•?Í.�5#“?i«’È>È’?‡m‹2d’?MöÏÓ€A’?{Cr’?2“¨|š“?&�Ñ:ªš�?o¹ú±I~”?�÷ª• ¿”?(»™Ñ�†“?Âß/fKV•?±Þ¨¦ï•?w;Sè¼–?R �h•?1Ít¯“ú’?ü7/N|µ“?‰ïĬC™?M-[ë‹„–?íñB:<„‘?Æø0{ÙvŠ?ØÒ£©žÌ�?ŪA˜Û½Œ?qªµ0 íŒ?åBå_Ë+‡?É&pënŽ?`x%És}�?¶ò’ÿÉß�?p]1#¼�?¯A_zûs‘?%ËI(}!”?•�~P)„?x¹ˆïĬ‡?È F³‚?ª¸q‹ù‰? †s 34ž?Š?Š:s¡?ÿ“¿{G�¡?£•{�Y¡ ?ð¦[vˆ ?ú~j¼t“ ?.9(a¦�?1ì0&ý�?®ºÕ”d�?Mf¼­ôÚœ?;ÃÔ–:È›?TáÏðf ž?ÍX4�� ž?ÞïU+ž?–ê^fØ ?5`�ôi�?\;Qi›?þEИ™?lÍV^ò?™?Œ.oך?ëþ±—?Ý ö_ç¦�?Ë/ƒ1"Q˜?V-²�ï—?Î5ÌÐx"˜?Ñ O!Wš?Ó¾¹¿zÜ—?&ÅÇ'd—?:vP‰ë—?ƒNt —?8ó«9@0—?¤5�:˜?}@ 3iS•?•·#œ¼˜?8h°©ó˜?í ¾0™*˜?QôÀÇ`Å™?XŒºÖÞ§š?©iÓL÷š?Ș»–�š?«A˜Û½Ü—?ƒÚoíDI˜?‚7¤Q�“�?Ñ–s)®*›?g,šÎN—?ÄAB”/h‘?h]£å@•?b.©Ún’?ûËîÉÃ’?XøQ û�?.=šêÉü“?bøˆ˜I”?•�~P)”?L⬈šè“?Ù[Êùbï•?Ä#ñòt®˜?n¤l‘´�?b×övKr�?rÝ”òZ �?‹¦³“ÁQ’?‘œLÜ*ˆ¡?¢]…”ŸT£?4„c–= ¤?|(Ñ’ÇÓ¢?ð¾**ÿ¢?ê”G7¢¢?PÈÎÛØì ?>”h¡?…{eÞªë ?³´Ss¹Á ?]¡·x ?MõdþÑ7¡?óWÈ\¡?¦D½Œb¡?%ÊÞRΣ?§<º¡?'ú|” ?²/Ùx°Åž?�z3j¾Jž?þ™A|`ÇŸ?£#¹ü‡ô›?BëáËD¡?2=a‰”�?=e5]Ot�?ª�æsîv�?\�Âõ(\Ÿ?Þ3ßÁOœ?š”‚n/iœ? ¡ƒ.áЛ?6:ç§8œ?ÎQÚœ?NÓg\Wœ?>w‚ý×¹™?éd©õ~£�?®€B=}ž?û¯sÓfœ?ª*4Ëfž?Ñêä ÅŸ?]¥»ëlÈŸ?àŸR%Êž?â’ãNé`�?A�wòé±�?¿rÞÿÇ¡? »(zàc ?{ö\¦&Á›?Îo˜h�‚—?’Z(™œš?¸å#)éa˜?wþEИ?ì/»' •?¢(Ð'ò$™?¦',ñ€²™?Ûl¬Ä<+™?ÎR²œ„Ò—?-σ»³v›?I¹û-ž?z8�é´n“?pxADjÚ•?[ìöYe¦”?»@j'—?Õ[[%X¤?dXÅ™G¦?ºÙ(§?ê?k~ü¥¥?“Ä’r÷9¦?[•DöA–¥?sò"ðk¤?$Ó¡Óón¤?n£¼¤?p–’å$¤?¦)œÞÅ£? ò³‘릤?𢯠ÍX¤?5·BX�%¤?' ‰°áé¥?î±ô¡ ê£?’–ÊÛN£?™·ê:TS¢?ôNÜóü¡?t^c—¨Þ¢?(ó�¾IÓ ?:#J{ƒ/¤?ý-ø§T¡?N(DÀ!T¡?žÒÁú?‡¡?¾ݳ®Ñ¢?õ»°5[¡?ض(³A&¡?+„ÕXÂÚ ?o+½6+¡?„Ó‚}¡?j¿µ%!¡?>ÍÉ‹LÀŸ?h+ømˆ¡?¼viá?‚‹5˜†¡?Š}"O¢?ù»wÔ˜£?GãP¿ [£?·ïQ½Â¢?Oæ}“¦¡?9c˜´É¡?פÛ¹à¤?ÖR@Úÿ£?-Í­Vc¡?gµÀ)�?ð¦[vˆ ?M JÑÊ�?tbícŸ?·zNzßøš?ƒ‡ißÜŸ? Áq75 ?Á9#J{ƒŸ?7R¶HÚ�? àfñba ?q¯Ì[u¢?ÓJ!�K™? ß÷o^œ?-σ»³v›?þ´Q�d�?ª™µ�ö§?å ïrß©?ݵßÚ‰ª?kšwœ¢#©?y7R¶Hª?²¼«0©?ÍwðЧ?Àé]¼·§?¶†R{m§?§uÔ~k§?+½6+1§?ÛàDôkë§?8†àس§?F´Swe§?“ÇÓòW©?‹p“Qe§?9`W“§¬¦?ÛûTˆ¥?k» ¾iú¤?%uš¦?ûçiÀ é£?ÊSVÓõD§? šyrM�¤?‡-y¤?9}=_³¤?Šä+�”Ø¥?CSvúA]¤?;‰ÿ"h¤?K’çú>¤?½Â‚û¤?ê[ætYL¤?׆Šqþ&¤?oò[t²Ô¢?Ûúé?k~¤?«>W[±¿¤?”0Óö¯¬¤?²ó66;R¥?h^»ï¦?ôú“øÜ ¦?X�ûVëÄ¥?ñ.ñ�˜¥?ÃbÔµö>¥?I€šZ¶Ö§? ËŸo ¦?³}È[®~¤?Ìyƾdã¡?TTýJç£?Î7¢{Ö5¢?’v5yÊ¢?ï9°!¡?=€E~ý£?#÷tuÇb£?v?T1£?lë§ÿ¬ù¡?ÐѪ–t”£?W[±¿ìž¤?‚”0ÓöŸ?SYvQô ?öÐ>VðÛ ?oÔ Ó÷¢?’!ÇÖ3„«?s-Z€¶­?“Ä’r÷9®?”0Óö¯¬?:°!y®?‘šv1ͬ?s ßû¬? õôø«?‹Š8�d««?ÙvÚŒ«?Ž��<»|«?Ÿp]1#¬?Õ=²¹jž«?ŸÌ?ú&M«?ИIÔ >­?¿ 1^óª?®J"û ˪?–ëm3â©?¾_´Ç ©?ùjGqŽ:ª?V¶yËÕ§?Æ‚”0«?jÜ›ß0Ѩ?¶ºœ“¨?³´Ss¹Á¨?Ûmšë4ª?ÒŒEÓÙɨ?[˜…vN³¨?›8¹ß¡(¨? Tƿϸ¨?kÔC4ºƒ¨?T;ÃÔ–:¨??§ ?¹¦?tÑ�ñ(•¨?„ £U-é¨?§wñ~Ü~©?¢²aMe©?>•Óž’sª?ÀYJ–“Pª?îÎÚmª?¢–æV«©? —8ò@d©?ˆìø/¬?—Mõdþ©?sž±/Ù¨?P«”žé¥?ìOâs'ا?õ-sº,¦?º¡);ý ¦?=Òà¶¶ð¤?ÕËï4™ñ¦?—9]›§?#£’°o§?�n/¦?&©L1A§?¯˜Þ„¨?©‡ht±£?G 6u¥?T­…Yhç¤?ðiN^d¦?«Ì”Öß°?Y¤‰w€'±?ö´Ã_“±?ÊÂ×׺°?eqÿ‘éб?¢A žB®°?Xÿç0_^°?¡×ŸÄçN°?ð¤…Ë*°?Pª}:3°?:ÏØ—l<°?ÒUº»Î†°?œ3¢´7°?…÷°?Ní S[ê°?‚‘—5±À¯?³³è� ¸¯?L5³–Ò®?å`6†­?ùgñ�¯?#Ûù~j¼¬?á' ß÷¯?ÉYØÓ­?zâ9[@h­?lâuý‚­?B˜Û½Ü'¯?ß4}vÀu­?«Ñ«JC­?/iŒÖQÕ¬? ¸çùÓF­?'…y�3­?Øô  ­¬?Y…Íd«?æ!S>­?©öéxÌ@­?Ù³ç25 ®?X�ûVëÄ­?ÏJZñ …¯?ÕËï4™ñ®?[yÉÿäï®?ƒ¦%VF#¯?4œ27߈®?¬:«ö˜°?±‡ö±‚ß®?Fí~à»­?R %“S;«?íôƒºH¡¬?ù»wÔ˜«?þEИIÔ«?ªó¨ø¿#ª?øá !ʬ?íœf�v‡¬?<š$–¬?›ÃòçÛª?lЗÞþ\¬?ŒÜÓÕ‹­?�Ky ²¨?^‚SHÞ©?ù*8¼ ª? Hû`­ª??ÿ=xíÒ²?‰ @£té³?†�óþ?N´?‹S­…Yh³?.â;1ëÅ´?Ì †:¬p³?œh>çn³?7ê°Â-³?¨qo~ÃD³?÷<Ú¨N³?#÷tuÇb³?®›R^+¡³?ÂÝY»íB³?$¶»è²?îv½4E€³?W ‡3¿²?ßp¹5é²?XU/¿Ód²?J&§v†±?¬à·!Æk²?ÊQ€(˜1±?¬o`r£È²?´Yõ¹Úб?ʈ @£t±?'Þž´p±?kÖß—²?‰±L¿D¼±?>Ëóà?Q29µ3L±?X7Þ«±?´­f�ñ}±?mu9% &±?Òýœ‚ül°? “©‚QI±?‘œLÜ*ˆ±??øùï±?ÙÐÍþ@¹±?·›à›¦Ï²?W"PýƒH²?Ó.¦™îu²?úñ—õI²?u”ƒÙ²?G�ßÛôg³?ôzÄè±?–çÁÝY»±?³}È[®~°?•}Wÿ[±?@Â0`ÉU°?Ñ<€E~ý°?J³yó¯?¥¡F!ɰ?~�eÁı?Ü›ß0Ñ ±?Œô¢v¿ °?3úÑpÊܰ?‘ð½¿A{±?¦òz0)®?¤oÒ4(š¯?’çú>$°??;àºbF°?åñ´üÀU¶?n…°KX·?D5%Y‡£·?^+¡»$ζ?‰)‘D/£¸?”i4¹·?¨qo~ÃD·?øý›'¾¶?àKáA³ë¶?»·"1A ·?¨qo~ÃD·?ÐF®›R^·?Y¿™˜.Ķ?#¢˜¼f¶?ÝéÎÏÙ¶?‡Áü2W¶?®ž“Þ7¾¶?Oq¶?ÒƒNµ?EõÖÀV ¶? 34žâ´?µÝß4}¶?�ÐÏÔëµ?¬TPQõ+µ?Ú‘ê;¿(µ?qâ«Å9¶?÷’ÆhUµ?¤ŠâUÖ6µ?ZHÀèòæ´?BëáËDµ?ï‘ÍUóµ?³\6:ç§´?'¢_[?ý³?e#Ù#Ô´?w¾Ÿ/Ý´?cc^G²µ?³z‡Û¡aµ?=™ôMš¶?Yùe0F$¶?46<½R¶?ëÇ&ù¿¶?“EÖ¶?G“‹1°Ž·?)#.�Òµ?|'f½ʵ?þEИI´?4-±2µ? B²€ ´?B_zûsÑ´?“âã²³? Ñ!p$д?p² Ü�:µ?/PR`Lµ?á (ÔÓ³?¹û-δ?_—á?Ý@µ?~âú}ÿ²?¥¢±öw¶³?,ºõš´?d²¸ÿÈt´?9´Èv¾Ÿº?¸ä¸S:X»?½¬‰¾¢»?l@„¸röº?ö&†ä¼?ˆñšWuV»?˜À­»yª»?úcZ›Æöº?\ã3Ù?O»?˜À­»yª»?S°ÆÙt¼?0™ò!¼?O"¿»?Ž;¥ƒõº?î%�Ñ:ªº?÷ʼUסº?O­¾º*P»?{Øœƒgº?Å‘"‹4¹?ŸF�6º? ´;¤ ¹?`V(Òýœº?îí–ä€]¹?”¢•{�Y¹?‰zÁ§9y¹?CYøúZ—º?·FãàÒ¹?ŠY/†r¢¹?-B±4-¹?Ÿ��·±¹?[•DöA–¹?UܸÅü¸?Ë+×Ûf*¸?�:è¹?g¹ltÎO¹?rl=C8º?‰±L¿D¼¹?\X7Þ»?PÅÈ’9º?Ä%Ç�ÒÁº?9&‹û�L»?øý›'¾º?Ø*Áâpæ»?‚rÛ¾Gý¹?é~NA~6º?þ oÖà}¹?¥Û¹à º?4žâ<œ¸?ºÙ(·¹?I¢—Q,·¸?:x&4I,¹?V W@ܹ?:êè¸Ù¹?,ºõš¸?Ä ·|$%¹?±§þš¬¹?æèñ{·?c+hZbe¸?,ÒSä¹?éšÉ7Ûܸ?•Öÿ9Ì¿?øRxÐ캿?Ó‡.¨o™¿?ô66;R}¿?>Î4aûÉÀ?ÍXä¿?~įXÃEÀ?’ÍUó‘¿?Wéî:ò¿?éÖkzPPÀ?¾ˆ¶cê®À?M1AG«À?=)“ÚÀ?ߣþz…¿?øU¹Pù×¾?¸sa¤µ¿?±¿ìž<,À?ì£SW>¿?f¤ÞS9í½?i¢¾? Ö8›Ž¾?‘îçäg¿?¸Y¼X¾?Yùe0F$¾?¬JC�B¾?Õ'¢_¿? 'LÍʾ?|E·^Óƒ¾?†!YÀ¾?PÂLÛ¿²¾?CUL¥Ÿp¾?6Y£¢Ñ½?ÓNÍåC½?–ëm3â½?¸Y¼X¾?ë�Zaú¾?*SÌAÐѾ?5Ô($™Õ¿?ΈÒÞà ¿?ú´ŠþÐÌ¿?v¨¦$ëpÀ?ý¾óâÄ¿?ÖâSŒgÀ?ⱟÅR$¿?)Bêvö•¿?,¼ËE|'¾?rßj�¸¿?M¾ÙæÆô¼?vÝ[‘˜ ¾?’Ï+žz¤½?”Kã^I¾?G¬Å§¿?0žACÿ¿?°p’æ�i½?¿3‰¾?̘‚5ξ?¯yUgµ¼?åòw悔?Üôg?RD¾?<Û£7ÜG¾?Š‘%s,ïÂ?‚:åÑ�°Â?ŒØ'€bÂ?ž%ȨÂ?¬;Û¤¢Ã?®×gÎúÂ?l\ÿ®ÏÂ?1~÷æ7Â? S"‰Â?Ëõ¶™ ñÂ?Ó…Xý†Ã?YÝê9é}Ã?�A'„ºÂ?¦´þ–üÁ?ó8 æ¯�Á?0¹Qd­¡Â?AaP¦ÑäÂ?tD¾K©KÂ?¼ÏñÑâŒÁ?V W@ÜÁ?/‰³"j¢Á?…]=ð1Â?&Œfeû�Á?aU½üN“Á?½Œb¹¥Á?j>"¦DÂ?»íBs�FÂ?�ÙYôNÂ?¶½Ý’°Á?Õz¿ÑŽÂ?mÇÔ]ÙÁ? $ ˜À­Á??ãÃìeÁ?·D.8ƒ¿Á?kÌÑãÁ?›8¹ß¡(Â?Ü�µÛ.Â?cAaP¦ÑÂ?ý¡™'×Â?ùdÅpuÂ?£:ÈzjÃ?â"÷tuÇÂ?(¹Ã&2sÃ?׿ë3g}Â?&�Ñ:ªšÂ?¥ƒõóÁ?\�-Ë×eÂ?©MœÜïÀ?¾K©KÆ1Â?@¤ß¾œÁ? 8KÉrÂ?˜`�ºÂ?ò´üÀUžÂ?£:ÈzjÁ?8 ¥+ØFÂ?å*¿)¬Â?&¤à)äÀ?™Gþ`àÁ?’;l"3Â?#½¨Ý¯Â?îîº/gÆ?3SZKÆ?g¹ltÎOÅ?32È]„)Æ?9débÓÆ?uuÇb›TÆ?cšé^'õÅ?³^ åDÅ?ß—ª´Å?d¬6ÿ¯:Æ?ˆfž\S Ç?XÅ™GþÆ? a°ä*Æ?ÏÜCÂ÷þÄ?…]=ð1Ä?�'‚8'Æ?’•_cDÆ?úÑpÊÜ|Å?Æ3hèŸàÄ?í+ÒSäÄ?Æ5>“ýóÄ?îCÞrõcÅ?3d’‘³Ä?…î’8+¢Ä?a7l[”ÙÄ?¨ŒŸqÅ?Í#0ðÜÅ?ì�±¾�Å?­NÎPÜñÄ? ™+ƒjƒÅ?¬¬mŠÇEÅ?ÇñC¥3Å?AaP¦ÑäÄ?ð2ÃFYÅ?Œ„¶œKqÅ?ã6À[ Å?>–>tA}Å?‰ jøÖÅ?J/…ÍÄ?Z�!«[Å?µßÚ‰’�Æ?F\¥KÅ?*ãßg\Æ?ÊýE�>Å?°«ÉSVÅ?VeßÁÿÄ?g˜ÚRyÅ?Ëž6çàÃ?Z-°ÇDJÅ?¡¼�£9²Ä? Ï.ßú°Ä?ÈDJ³yÆ?.�_x%ÉÅ?ù†Å?%è/ôˆÑÅ?€ñ ú'Æ?@ÛjÖßÃ?mìM Å?&Ä\RµÝÄ?XŽ��<»Ä?å ÅoòÉ?é)rˆ¸9É?m‡ÁüÈ?K­÷í¸É?¨á[X7ÞÉ?Á=~oÓÉ?É=]ݱØÈ?‘¶ñ'*È?¥À˜2pÈ?"Ä•³wFÉ?G’ \…Ê?àºbFx{Ê?¿+‚ÿ­dÉ?ÅY5ÑçÇ?>Î4aûÉÆ?=›UŸ«É?· £ x|É?5š\Œ�uÈ?Käõ`RÈ?Ú­À�ÕÇ?Ú<ƒù+È?ɪ7UÈ? õôøÃÇ?[C©½ˆ¶Ç?œà›¦ÏÈ?¤9²òË`È?ØFì@É?xíÒ†ÃÒÈ?E€Ó»x?È?�Mò#~ÅÈ?¬äcw�’È?qÈÒÅÈ?*Æù›PˆÈ? 2tìÈ?¢zk`«É?û]Øš­¼È?åBå_Ë+É?st´ª%É?f1±ù¸6È?ô£á”¹ùÈ?ùƒ�çÞÃÉ?ãý¸ýòÉÈ?„¼LŠ�É?_—á?Ý@É?pÍý/×È?â¶ôhÈ? á˜eOÉ?¼UË?Kè.‰³"Ì?RµÝß4É?p—ýºÓ�Ë?©iÓÊ?x*àžçOË?}±÷â‹öÌ?xÓ-;Ä?Ì?i©¼á´Ì?�ÏdÎ?Jî°‰Ì\Î?¿cxìg±Ê?Kê46Ì?_—á?Ý@Ë?úA]¤PÌ?r¥ž¡Ð?¢ÏGqÐ?ªb*ý„³Í?‘ 9¶žÐ?½ 4Ô(Ð?ØF<ÙÍŒÐ?¨ŒŸqáÎ?›©¾óÍ? š]÷VÎ?Ë¿–W®·Ï?LŠ�OÈÐ?�—nƒÀÐ?ÜFx $Ð?I÷s ò³Í?´æÇ_ZÔË?ÿ?N˜0šÐ?¬TPQõ+Ð?ºõš”Î?Ih˹WÏ?ž #½¨ÝÍ?’¯Rb×Î?âKº Î?÷@Î?­Mc{-èÍ?LR™b‚Î?„elèfÎ?+iÅ7>Ð?Ä[çß.ûÏ?<£­J"ûÎ?6­¹ÄÏ?¿Õ:q9^Ï?5cÑtv2Ð?TáÏðf Ð?ˆißÜ_=Ð?>çn×KSÐ?÷êã¡ïnÏ?>çn×KSÐ?¸!Æk^ÕÏ?O=Òà¶¶Î?¹Pù×òÊÏ?x¼W­LÐ?<2V›ÿWÏ?R×ÚûTÐ?óUò±»@Ð?T8‚TŠÏ?·Aí·v¢Î?ØEуÏ?û‘"2¬âË?vãÝ‘±ÚÎ?ý¡™'×Î?�/ö^|ÑÎ?¼êó�)Ð?X¬á"÷tÏ?¬Zd;Ð?GqŽ::®Ð?+0du«çÐ?ÑÌ“k dÎ?�Ô—¥�šÏ?W[±¿ìžÎ?BÑ<€E~Ï? R)v4Ò?7+1ÏJÑ?TTýJçÏ?¢Ð²î Ò?ô‹ôzÑ?+ÞÈ<òÒ?öF­0}¯Ð?dyW=`Ð?§ ?¹nÐ?’“‰[1Ñ?‘º�}åAÒ?32È]„)Ò?w‚ý×¹iÑ?’�`ãúÏ?ñ·=Ab»Í?{.S“à Ò?-Ðî�b€Ñ?HnMº-‘Ð?_ PjÑ?¸<ÖŒ Ð?f¡�Ó,ÐÐ?&â­óo—Ð?!ɬÞávÐ?TææÑ=Ð?¹Æg²žÐ?’Ìên‡Ð?¥Kÿ’T¦Ñ?8N ógÑ?|(Ñ’ÇÓÐ?óUò±»@Ñ?oÓŸýHÑ?.®ñ™ìŸÑ?Ù•–‘zÑ?3ù¼â©Ñ?Ív…>XÆÑ?ÊmûõÐ?cC7ûåÑ?D¢Ð²îÑ?Áàš;ú_Ð?:ÉV—SÑ?ÄÏ^»Ñ?2ÉÈYØÓÐ?•|ì.PRÑ?>Ì^¶�¶Ñ?^fØ(ëÐ?RD†U¼‘Ð?8ÙîÐ? __ëR#Î?ð1XqªµÐ?w+Kt–YÐ?+ùØ] ¤Ð?Ú¨N²Ñ?ðl�ÞpÑ?,Öp‘{ºÑ?½ 4Ô(Ò?ê K< lÒ?ÒÈçO=Ð? š]÷VÑ?ÑÎihwÐ?�»]/MÑ?wõ*2Ó?ˆ-y<-Ò?Ìï4™ñ¶Ð?ÕÎ0µ¥Ó?·–Ép<ŸÒ?ÊPSé'Ó?1DN_Ï×Ñ?—qSÑ?ßß ½úxÑ?Z×h9ÐCÒ?GéÒ¿$•Ó?ó¬¤ßPÓ?³x±0DNÒ?x—‹øNÌÐ?øŒDhÏ?v?T1Ó?ä÷6ýÙ�Ò?ú+d® ªÑ?q©;Ò?*œÞÅûÐ?Ú7÷W�ûÑ?÷vKrÀ®Ñ?²žZ}uÑ?Êþy0HÑ?±§þš¬Ñ?EºŸS�ŸÑ?a4+Û‡¼Ò?’!ÇÖ3„Ò?ŽÍŽTßùÑ?,óV]‡jÒ?Ð캷"1Ò?Ùy›©Ò?ðRê’qŒÒ?]øÁùÔ±Ò?áÎ…‘^ÔÒ?%ušÒ?ÿx¯Z™ðÒ?c('ÚUHÒ?ü¨†ýžXÑ?F}’;l"Ò? ‹Q×ÚûÒ?î ÛÝÒ?�ü6ÄxÒ?[ï7ÚqÃÒ?Cr2q« Ò?€aùómÁÑ?(Ö©ò=#Ò?ù¾¸T¥-Ð?{úüáçÑ?öBÛÁˆÑ? ßû´Ñ?Ç,{ØÒ?í”Ûö=Ò?be4òyÅÒ?á iTàdÓ?Ȩp©Ó?t›p¯Ì[Ñ?ž$]3ùfÒ?¤aQ§Ñ?Üž ±Ý=Ò?½Â‚ûÔ?æ�I›Ò?CSvúA]Ñ?p]1#¼Ó?ßÁÿV²Ó?jßÜ_=îÓ?ØÓMÖÒ?ëÿæËÑ?%@7nÒ?·_>Y1\Ó?rûå“ÃÔ?5ëŒï‹KÔ?ëþ±Ó?úÐõ-sÑ?Ú:8Ø›Ð?vþÓ Ô?ÅËÓ¹¢”Ó?°u©ú™Ò?j’ÌêÒ?ï�ÚÄÉÑ?aãúw}æÒ?ÅËÓ¹¢”Ò?˜Ü(²ÖPÒ?<Øb·Ï*Ò?Õ‘#��‘Ò?‹v“Ò?�Ã`þ ™Ó?5ñð¤…Ó?ÄÑUº»ÎÒ?zã¤0ïqÓ?ŠÇEµˆ(Ó?Ê2ı.nÓ?TÆÝ ZÓ?Útp³xÓ?iR º½¤Ó?÷XúÐõÒ?é� ¸çùÓ?4Ûú`Ó?à»ÍÒ?.å|±÷âÒ?áµKKÔ?œß0Ñ Ó?™·ê:TSÓ?‰$zÅrÓ?óÊõ¶™ Ó?iUK:ÊÁÒ?Tàd¸Ó?}^ñÔ# Ñ?>zÃ}äÖÒ?%]3ùf›Ò?ªÉ�Ò?ð‹KUÚâÓ?oƒÚoíDÓ?]›�kÓ?¤ý°VÔ?{ØœƒgÔ?!<Ú8b-Ò?Ð�`ÿunÓ?Ü.4×i¤Ò?˜¦pzÓ?`·îæ©Ô?�™D½àÓÒ?J}YÚ©¹Ñ?æ‘?xîÓ?èˆ|—R—Ô?.ÿ!ýöuÔ?ë˜Ü(²Ó?9&‹û�LÒ?!”÷q4GÓ?‡‹ÜÓÕÔ?åÓc[œÕ?ÐDØðôÔ?ô4`�ôiÓ?Ñ\§‘–ÊÑ?çU�Õ{Ð?Ž?QÙ°¦Ô?²›ýh8Ô?VJÏôcÓ?VIddYÓ?” ¿Ð#FÒ?{Á§9y‘Ó??;àºbFÓ?p|í™%Ó?‰`\:æÒ?_ì½ø¢=Ó?REñ*kÓ?´r/0+Ô?\…zúÔ?z9ì¾cxÓ?ÒyYÔ? 'LÍÊÓ?žâ<œÀÓ?9}=_³Ó?½àÓœ¼ÈÓ?CV¸åÓ?{K9_ì½Ó?vª|ÏH„Ô?¹S:XÿçÓ?"¦D½ŒÒ?—åë2ü§Ó?Õ^DÛ1uÕ?cGãP¿ Ô?@7n1Ô? +TTýÓ?zUgµÀÔ?äõ`R||Ó?=Òà¶¶ðÓ?Õv|ÓôÑ?‡ú]Øš­Ó?€+Ù±ˆÓ?½üN“oÓ?Ø}ÇðØÏÔ?ãŽ7ù-:Ô?"8öì¹Ó? жšuÆÔ?€¸«W‘ÑÔ?­ö° ØÒ?¬Så{F"Ô?þî5&ÄÓ?�¥]PßÓ?®~l’ñÔ?o›©�ÄÒ?ßÅûqûåÑ?ú·Ë~ÝéÓ?¼A´V´9Õ?=›UŸ«Ô?²�t±iÔ?ïOZ¸Ò?i©¼áÓ?õHƒÛÚÂÔ?Vó‘ïRÖ?ª, »(zÕ?M,ðÝzÓ?Íí)Ò?ûZ—¡ŸÐ?‡1éï¥ðÔ? pzïÇÔ?´“ÁQòêÓ?y‘ ø5’Ó?qs*ªÒ??áìÖ2Ô?€ ;¨ÄÓ?PáR)vÓ?üU€ï6oÓ?Úþ••&¥Ó?òC¥3ûÓ?sñ·=AbÔ?CV·zNzÔ?Ý•]0¸æÓ?�ôiý¡Ô?Vò±»@IÔ?÷«ßmÞÓ?ZòxZ~àÓ?ð6oœæÓ?ì…¶ƒÔ? ¦šYKÔ?¬Å9êèÔ?U†q7ˆÔ?p|í™%Ó?ñE{¼�Ô?i©÷TNÖ?ž`ÿunÚÔ?LÝ•]0¸Ô?‹·˜ŸÔ?´’V|CáÔ?¾öÌ’5Ô? îêUdtÔ?Õ‘#��‘Ò?ʦ\á]Ô?<Þä·èdÔ?®*û®þÓ?÷”œ{hÕ?Ø}ÇðØÏÔ?Ênfô£áÓ?8©0¶Õ?Ñ;pÏóÔ?ÈбƒJÓ?¼Ñ“2©Ô? F%ušÔ?~p>u¬RÔ?í”Ûö=Õ?£“¥Öû�Ò?òB:<„ñÑ?NŸt"ÁÓ?Ç�WË�Õ?”ˆð/‚ÆÔ?ª™µ�öÔ?»¶·[’Ó?Ç¡~¶fÔ?1~÷æ7Õ?®~l’ñÖ?#‡ˆ›SÉÕ?<š$–Ó?Îã0˜¿BÒ?¯&OYM×Ð?h‘í|?5Õ?.2¥Õ?+*ÿZ^Ô?ð1XqªµÓ?™)­¿%Ó?ŸŠ‘%sÔ?Ž={.Ô?9¼ZîÌÓ?<¾½kÐÓ?ÀË eýÓ?nÜb~nhÔ?½VBwIœÔ?‰ jøÖÔ?Åä 0óÔ? B²€ Õ? œO«”Ô?:–wÕæÓ?Ôšæ§èÓ?ÆÞ‹/ÚãÓ?îÎÚmÔ?N¶�;P§Ô?·~úÏšÕ?òë‡Ø`áÔ?Kþ)UÓ?Ëe£s~ŠÔ?¿›nÙ!×?kÔC4ºƒÕ?ºÙ(Õ?¾Û¼qRÔ?aR||BvÕ?C�=·ÐÔ?{ÙvÚÕ?AG«ZÒQÓ?¹«W‘ÑÕ?¢&ú|”Õ?xî=\rÔ?iàG5ì÷Õ?a¤µûUÕ?óèFXTÄÓ?!®œ½3Õ?I†[ÏÕ?¡fHÅ«Ó?Õ?j.7ê°Ô?Ø›’“‰Õ?¦&ÁÒ¨Ò?Ë×eøO7Ò?W_]¨ÅÓ?¢&ú|”Ö?%–”»ÏñÔ?ó�ΤMÕ?ÁÿV²c#Ó?î'c|˜½Ô?kg{ô†Õ?Üò‘”ô0×?\Uö]üÕ?|·yã¤Ó?Ô›QóUÒ?pΈÒÞàÐ?¸ä¸S:XÕ?ñõµ.5BÕ?A,›9$µÔ?J)èö’ÆÓ?ƿϸp Ó?.ÎR²œÔ?ʦ\á]Ô?ZJ–“PúÓ?"� œlÔ?ÖÆØ /Ô?ë²×»Ô?3oÕu¨¦Ô?¢ñDçáÔ?vOjMÔ?è÷ý›'Õ?™Ÿš²ÓÔ?Í"[AÓÓ?/ó:âÓ?Ð^}<ôÝÓ?z©Ø˜×Ô?ˆ…ZÓ¼ãÔ?_ì½ø¢=Õ?ía/°Õ? šyrM�Ó?vàœ¥½Ô?@¤ß¾œ×?§pzïÕ?Q§“luÕ?gCþ™A|Ô?Ø€qåìÕ?^‘šv1Õ?è.‰³"jÕ?÷ÍýÕã¾Ó?Võò;MfÕ?èÝXP”Õ?ŒôzÄÔ?´Ì"[AÖ?–~TÃÕ?ÿB�=·Ó?Êû8š#+Õ?ʤ†6Õ?5ÒRy;ÂÓ?8öì¹LMÕ?µÀ)ÍÕ?ä0˜¿BæÔ?èO=ÒàÕ?š$–”»ÏÒ?©ƒ¼LŠÒ?ÐDØðôÓ?N³@»CŠÖ?BÌ%UÛMÕ?["œÁßÕ?uXá–�¤Ó?e‰Î2‹PÕ?ã‹öx!Ö?ë²×»×?«\¨ükyÖ?ªED1yÔ?Õ>�¨Ò?„‚R´r/Ñ?Ù%ª·¶Õ?ÄÑUº»ÎÕ?ñH¼<�+Õ?¨o™Óe1Ô?‘ïRê’Ó?9EGrùÕ?ã©GÜÖÔ?ü«Ç}«uÔ?ŸË2�Ô?d’‘³°§Ô? YÝê9Õ?±6ÆNx Õ?ÇJ̳’VÕ?”‡…ZÓ¼Ô?ƒjƒѯÕ?^üo%;Õ?ì3g}Ê1Ô?åîs|´8Ô?B•š=Ô?èÙ¬ú\mÔ?úñîÈXÕ?�ÖmPû­Õ?ÒV%‘}�Õ?J´äñ´üÓ?!‘¶ñ'*Õ?Ƭq6Ø?ˆKŽ;¥ƒÖ?¬„¹ÝËÕ?@QÙ°¦²Ô?ñ~Ü~ùdÖ?þí²_wºÕ?ºØ´RäÕ?p +TTÔ?L�ÐÏÔëÕ?£®µ÷©*Ö?o+½6Õ?1·{¹OŽÖ?9EGrùÖ?¥/„œ÷ÿÓ?Uø3¼YƒÕ?@ �RÕ?ù¾¸T¥-Ô?]p¿˜Õ?ÝÍSr3Ö?¿Òùð,AÕ?Uö]üoÖ?Ö5ZôPÓ?�…è8Ó?c™~‰xÔ?eVïp;4×?FÏ-t%Ö?¦{�Ô—¥Ö?b¡Ö4ï8Ô?ÀÌwðÖ? õôøÃÖ?¬�±^‚Ø?ï<ñœ- ×?ÛRy=˜Ô?©KÆ1’=Ó??T1³Ñ?§>�¼sÖ? E�¹‡„Ö?]RµÝßÕ?×L¾ÙæÆÔ?§AÑ<€EÔ?‘fØÕ?4ôOp±¢Õ?©lXSYÕ?•ô0´:9Õ?mªî‘ÍUÕ?!ºöôÕ?ëÇ&ù¿Õ?F{¼�Ö?;6ñº~Õ?Y1\qÖ?=}þðóÕ?o›©�ÄÔ?Á8¸tÌÔ?ð3.ÉÔ?º†OÕ?ï9°Ö?v“þ^Ö?†‘^ÔîWÖ?TÉPÅ�Ô? 34žâÕ?õ  ­ÜØ?{ˆFw;×?}vÀuÅŒÖ?Ï¿]öëNÕ?ôR±1¯#×?ù g³êsÖ?¾÷7h¯Ö?d#¯ëÕ?p]1#¼Ö?Ü»}éíÖ?äÙå[ÖÕ?MöÏÓ€A×?Ø(ë7ÓÖ?Á;ùôØ–Ô?þÓ x'Ö?cD¢Ð²îÕ?‡ýžX§ÊÔ?aO;ü5YÖ?¢ñDçáÖ?ˆWÎÞÕ? jøÖ�×?Üôg?RDÔ?ªðgx³Ô?ÎQÚ|Õ?‹ú$wØDØ?{ÙvÚ×?ƒÁ5wô¿×?Ýyâ9[@Õ?| €ñ ×?p%;6ñ×? ]Þ®Ù?BÌ%UÛMØ?å(@̘Õ?eS®ð.Ô?Ot ‡Ò?°rh‘í|×?ñ.ñ�˜×?¼�aüÖ?a7l[”ÙÕ?wøk²F=Õ?j‰•ÑÈçÖ?_&Š�º�Ö?ϼvß1Ö?È%Ž<YÖ?߈îY×hÖ?èô¼ ×?·ð¼TlÌÖ?I¹û-×?p@KW°�Ö?�aNÐ&‡×?½Â‚û×?ãþ#Ó¡ÓÕ?¥ØÑ8ÔïÕ?©0¶äÕ?1{ÙvÚÖ?_• •-×?ùŸüÝ;j×?-#õžÊi×?âÊÙ;£­Õ? ¯$y®ïÖ?å 0óüÙ?Š}"OØ?˜½l;m�×?±n¼;2VÖ?•Ò3½ÄXØ?ÁŒ)Xãl×?Ze¦´þ–×?|`Ç� Ö?]øÁùÔ±×?DÂ÷þí×??ªa¿'ÖÖ?ðúÌYŸrØ? ¤Ä®íí×?2“¨|šÕ?´Ì"[A×?/ó:âÖ?G ^×/ØÕ?F жšu×?£çºØ?$B#ظþÖ?¸Ku/Ù?Í•AµÁ‰Õ?²ŸÅR$_Õ?ŒôzÄÖ?LŒeú%âÙ?ûå¶}�Ø?"rúz¾fÙ?šxxÒÂÖ?ômÁR]ÀØ?Ý µ‰“Ù?"úµõÓÛ?RÔ™{HøÙ?LbõG×?—㈞Õ?¼Ì°QÖÓ?ž–¸ÊÙ?_Aš±h:Ù?Ž?QÙ°¦Ø?Rf`X×?/6­¹Ö?:vP‰Ø?[°Tð2Ø?/áÐ[<¼×?—Æ/¼’ä×?Õw~Q‚þ×?}гYõ¹Ø?¦D½ŒbØ?½ŠŒHÂØ?Z)r‰#Ø?§“lu9%Ù?©‡ht±Ø?[_$´å\×?sò"ðk×?0º¼9\×?€-¯\o›×?þ)U¢ìØ?Íí)Ù?]2Ž‘ìÙ?=Ô¶a×?E¹4~á•Ø?U‰²·”óÛ?³•—üOþÙ?8öì¹LMÙ?py¬ä×?¾h�ÒáÙ?Òq5²+-Ù?»ðƒó©cÙ?½«0™×?Ý(²ÖPjÙ?RóUò±»Ù?Égð÷‹Ø?À#*T7Ú?5ñð¤…Ù?�…è8×? â8ðj¹Ø?2;‹Þ©€Ø?.©Ún‚o×?Â¥cÎ3öØ?ëÈ‘ÎÀÈÙ?ÿ?N˜0šØ?D¨R³ZÛ?*9'öÐ>×?ÓL÷:©/×?ïTÀ=ÏŸØ?²œ„ÒBÜ?sõ¸oµÚ?Ü~ùdÅÛ?¿¹¿zÜ·Ø?:uå³<Û?__ëR#ôÛ?Wéî:òÝ?›äGüŠ5Ü?/Äê�0 Ù?Ñzø2Q„×?å`6†Õ?ÎŽTßùEÛ?Ô_¯°à~Û?°WXp?àÚ?xÔ˜sIÙ?zóWÈØ?8Ûܘž°Ú?n1?74eÚ?B•š=ÐÙ?Žx²›ýÙ?˜üOþîÚ?ˆ¸9• Û?œùÕ ˜Ú?AºØ´RÛ?á¶¶ð¼TÚ?h—o}XoÛ?ÂP‡nùÚ?gd�»SÙ?”N$˜jfÙ?`sž MÙ?u¬Rz¦—Ù?eS®ð.Û?ŸÛ2à,Û?Ì EºŸSÛ?úC3O®)Ù?ÌFçüÇÚ?áy©Ø˜×Þ?µ‰“ûŠÜ?Ü‚¥º€—Û?Dl°p’æÙ?4½ÄX¦_Ü?s¡ò¯å•Û?J´Û?xìg±ÉÙ?„Iññ ÙÛ?µN\ŽW Ü?Eð¿•ìØÚ?:�õÔê«Ü?Úã…txÜ?�ÙYôNÙ?+‡ÙÎ÷Ú?X�ûVëÄÚ?§#€›Å‹Ù?:Yj½ßhÛ?ƒ3øûÅlÜ?€cÏžËÚ?ÖXÂÚ;Þ?pÏó§�Ù?#ظþ]ŸÙ?)²ÖPj/Û?øO7Pà�ß?;ŠsÔÑqÝ?Á=~oÓÞ?¢ ê[ætÛ?˜ù~âÞ?¶ö>U…ß?·)Õ¢à?Ii6�Ã`ß?(HlwÐÛ?Ä\RµÝÚ?Þ:ÿvÙ¯×?Â/õó¦"Þ?Ž“Â¼Ç™Þ?šÐ$±¤ÜÝ?ŒHZÖýÛ?«7U†Û?I¢—Q,·Ý?( 5 IÝ?'�µ¿³Ü??ÿ=xíÜ?æèñ{›þÜ?º/g¶+ôÝ?‰|—R—ŒÝ?]†ÿtÞ?~Œ‰BÝ?‰Ð6®Þ?ŸèºðƒóÝ?eRC€ Ü?¤ŠâUÖ6Ü?ÀÌwðÜ?@ �RÜ?™šoH£Þ?¡fHÅ«Þ?U‰²·”óÞ?ÍXä×Û?>ÏŸ6ªÓÝ?g×½‰ á?§/ú Òß?»}V™)ß?>zÃ}äÖÜ?˜Ãî;†Çß?i㈵øß?PSËÖú"ß?ÊPSéÜ?¿3‰ß?H¥ØÑ8Ôß?ˆñšWuVÞ?ë8~¨ß?+¾¡ðÙ:ß?„ô9DÜÛ?„4fÞ?S°ÆÙtÞ?Ù=yX¨Ü?É8F²G¨Þ?†s 34žß?£ x|{Þ?X«vMHëà?l|&ûçiÜ?\>’’†Ü?ñI'L5Þ?œmnLOØá? Šcnà?CSvúA]á?µmÁÞ?<3Áp®áà?Ô›QóUrá?÷Žb®â?/ˆHM»˜á? pß?K‘|%Ý?sò"ðkÚ?rþ&"àà?j†TQ<á?_\ªÒ×à?–Ïò<¸;ß?¸Ì鲘ØÞ?åÔÎ0µ¥à?Ê2ı.nà?¶»è¾à?ޱ^‚Sà?Ý%qVDMà?ÝÑÿr-Úà?æ—Çš‘à?#KæXÞÕà?‚X6sHjà?pCŒ×¼*á?BÌ%UÛÍà?{»%9`Wß?pÏó§�ß?Vó‘ïRß?£Ë›Ãµß?„ £U-éà?÷éxÌ@åà?Ù"i7úá?ä „™¶ß?,šÎNÇà?K’çú>ã?Ù±ˆ×õá?¼•%:Ë,á?óŽSt$à?ûY,Eòá?÷­Ö‰Ká?œ’“‰[á?i«’È>Hà?ô£á”¹yá?š#+¿ Æá?:ùÙÈõà?•�Zºâ?i�Òá¡á?HŠÈ°Š7ß?ýÁÀsïáà?ˆ¡ÕÉŠà?st´ª%à?È$#gaOá?Ęô÷Røá?:Ì—àà?M!u;ûâ?šÒú[ðß?Z)r‰#à?å?mXSYöä?ÌÔ$xCšä?øª• ¿Ôä?föyŒòÌä?ž%Ȩå?V+~©å?ÕýL½nå?�[wóTå?Ž!8öìå?ˆ jôj€å?§>�¼óã?j¥È%ä?É&pëîã?ÃòçÛ‚%ä?ùƒ�çÞÃå?aÃÓ+eå?ìm3âæ?ât’­.'ä?8½‹÷ãvå?í ¾0™ªè?lâuýç?˜Št?'æ? ¦šYK�ä?é ¶Oöæ?¸w úÒ[æ?ñœú@ræ?ûPŒ,å?Øó5Ëe£æ?.:Yj½ßæ?=¸;k·Ýå?Y"§¯gç?�‚§�+õæ?€GT¨n®ã?š?¦µilå?:�õÔê+å?¦€´ÿÖä?”¼Ǚ&æ?it±3…ç?©L1AÇå?º ¾eÎç?m®šçˆüã?ö LnYä?Y |E7å?ø§T‰²é?*ât’­.ç?Êk%t—Äè?46<=æ?UlÌëˆCè?QhY÷�é?vÿXˆ�ê?·bÙ=ùè?K?ªa?æ?e73úÑðä?)?©öéøâ?{1”í*è?Kw×Ù�è? uXáè?­ÃÑUº;æ?úµõÓVæ?ߨ*Áâç?þ¸ýòÉŠç?\ÿ®Ïœõæ?µý++MJç?+TTýJç?ý\¬¨Aè?‹O0žÁç?¾0™*è?²¼«0�ç?^ÛÛ-É�è?¤ß¾œ3è?ÅS�4¸-æ?Sê’qŒdæ?üáç¿/æ?Ø,—�Îyæ?.É»š<è?ñÖù·Ëþç?¯_°¶­è?¢);ý ®æ?ŒjQL^è?úDž$]ë?qÿ‘éÐéé?d Ï.ßúè?'iþ˜Ö&ç? 4ØÔé?›äGüŠ5é?Ñ\§‘–Jé?è1Ê3/è?jÛ0 ‚é?H¾�Äé?»&¤5�è?�Ïdê?ÏJZñ …é?ÚV³Îø>æ?\�-Ë×åç?—¬Šp“Ñç?2rö´Cç?k¸È=Ýè?$Dù‚ê?Tn¢–æVè?gó8 æ¯ê?á “©‚Qæ?Ù|\*Ææ?3NCTáÏç?LüQÔ™ì?£�#Öé?E·^Óƒ‚ë?»}V™©è?ËôKÄÛê?O!WêY�ë?� ܺ›'í?>“ýó4àë?“ÅýG¦Ãè?eüûŒ ‡ç?žB®Ô³ å? Rðrê?ñ¸¨Eë?_]¨Åàê?Á:Ž*�è?ðÚ¥ ‡¥è?Ø�›6cê?an÷rŸê?Í[uª©é?ô߃×.íé?ïäÓcÛé?PÅ�[Ìê?ó‘”ô04ê?ÄáÑÆ‘ê?ÌAÐѪê?¶J°8ë?b†ÆAœê?…[>’’žè?ü¨†ýžØè?Á;ùôØ–è?uv28JÞè?LP÷°îê?:3Pê?´äñ´ü@ë?X zR&5é?||BvÞÆê?Rðr¥î?ÆüÜД�ì?`V(Òýœë?jÂö“1¾é?Êû8š£ì?‡¿&kÔÃë?‚”0Óöë?é|x– £ê?ã§qo~Cì?йÛõÒ”ì?SçQñGë?ÙwEð¿•ì?äõ`R|üë?>î?VðÛãµï?-è½1€î?EŸ�2â‚ë?$ nk Ïé?ø§T‰²ç?~p>u¬Rí?4iSu�ìí?ý‡ôÛ×�í?LŠ�OÈNë?;‡ú]Xë?LÄ[çß.í?ù0{ÙvÚì?¦ ÐRì?ŸÇ(ϼœì?w1Ít¯“ì?c ¹§«í?}þðóßì?‘�<»|kí?·ð¼TlÌì?àI —UØí?ÂzýI|í?"8öì9ë?é~NA~ë?þÖN”„Dë?•-’v£�ë?ŸZ}uU í?¹Æg²í?!u;ûÊî?øÁùÔ±Êë?(Óhr1†í?¢DKOKð?Ñ´­fï?à‚lY¾.î?»ÑÇ|@ ì?å'Õ>ï?vOjÍî? ø5’áî?X)±k{í?ƒ0º<ï?_ðiN^dï?é+H3Íí?è25 Þï?è÷ý›—î?‰Ð6®ë?~SX© "í?ÇL¢^ðéì? ©ÛÙWžì?ßû´Wî?ÎÅßö ï?KY†8ÖÅí?yvùÖ‡ï?Ùy›)ë?_›�•˜çë?!Îà L§ì?¨Š©ôÎð?Z/†r¢Ýî?fLÁgSð?ïs|´8ãí?€(˜1ëï?C«“3Tð?ÁR]ÀË ñ?³(ì¢è�ð?aSçQñÿí?é×ÖOÿYì?‡‡0~÷é?‹ßV*¨ï?Q/ø4'/ð?Í;NÑ‘Üï?f‡ø‡-½í?l%t—ÄÙí?Q}>ʈï?^emS<.ï?-[ë‹„¶î?ï‘ÍUóï?}¢üî?6uÿ÷ï?˜`�:ï?�I›ªï?ò¶Òk³1ï?¡õðe"ð?2ZGUÄï?IºfòͶí?IØ·“ˆðí?=œÀtZ·í?Ì}r î?_EF$áï?Cÿ+jï?y<-?pð?�âut\î?‡kµ‡½Ðï?ê]¼·_ñ?Gu:�õ”ð?|a2U0*ð?OçŠRB°î?KÈ=›•ð?±¢Ó0<ð?IóÇ´6Mð?–%:Ë,Âï?0Úr.…ð?€E~ý›ð?ðN>=¶%ð?¾¼ûèÔð?Õ³ ”÷qð?F�6ŽØí?¼é–âŸï?õ×+,8ï?obHN&îî?êu‘Bð?^»´á°ð?ï9°ð?GV~ŒÑð?†æ:�´Tí?³)Wx— î?}=_³\¶î?·¶ð¼T¬ñ?¦™îuRŸð?uÇb›Ttñ?…$³z‡ð?h@½5ñ?iàG5ìwñ?Ïõ}8Hò?e�P3¤Šñ?o�»,ð?€D(b‘î?¸\ýØ$?ì?’?xîýð?JA·—4Fñ?¿b ¹'ñ?®J"û ð?eS®ð.ð?w Nytãð?{£V˜¾×ð? Ý%qV„ð?ËLiý-Áð?~TÃ~O¬ð?Û6Œ‚à1ñ?µ÷Xºð? ¦}sÿð?šxxÒÂð?ÐDØðôJñ?j0 ÃGñ?çÂH/j÷ï?e#Ù#ð?h\WÌð?¾Ý’°+ð?í¸áwÓíð?¬o`r£Èð?ΪÏÕV,ñ?÷Ž"ð?àÙ½áþð?Ì&À°üyò?:<„ñÓ¸ñ?4-±29ñ?â镲 1ð?¢´7øÂ¤ñ?°Œ ÝìOñ?¾÷7hñ? ]Þœð?ª·¶Jpñ?ª¸q‹yñ?²ºÕ3ñ?kaÚ9Íñ?�Ÿqá€ñ?Êß½£ð?y ýÜð?¿‚4cÑôð?Pj’Œð?Ž¿·éOñ?]ݱØ&Õñ?£�dVï0ñ?ÌÑã·ñ?w÷Ý—3ï?PäIÒ5ð?*¬ÿsXð? raŠrò?Š}"O’ñ?‡P¥&ò?JíE´ñ?Žàñ?ŠriüÂ+ò?\ÊùbïÅò?5´Ø€Hò?6çà™Ð$ñ?J ,€)Cð?#ÝÏ)ÈOî?_š"ÀéÝñ?&S£’úñ?‚69|ÒÉñ?žðœúñ?2WÕ'ñ?ãý¸ýòÉñ?÷ʼUסñ?ÃJUñ?,H3M§ñ? …8„ªñ?¥žÐëñ?]7¥¼V‚ñ?ÿunÚŒÓñ?ëTùž‘ñ?`sž ò?çýœ0áñ?¨SÝñ?¯&OYMñ?Âû ñ?ï7ÚqÃ/ñ?Jôñ?IÛø•Íñ?G�ßÛô'ò?@‡ùò,ñ?YRî>Çò?UˆGâå)ó?┹ùFtò?×øLöÏSò?ù¿b Wñ?‡Q<¾}ò?}–çÁÝYò?ÉÊ/ƒ1bò?V+~©Ÿñ?©2Œ»Atò?8½‹÷ãvò?I¹ûíñ? ’>­bò?Ð}9³ò?„ò>Ž&ñ?ò–«›¤ñ?L¤4›Ç¡ñ?€H¿}xñ?EHÝÎþñ?Ð(]úWò?€™ïà'ò?g*‰ò?$cµù•ð?[•DöAñ?0JÐ_èQñ?±Ã˜ô÷ó?‹¥H¾Hò?fÚþ•Õò?ð£ö{âñ?¿3‰ò?=Ô¶aÄò? $}ZEó?ßÞ5è ó?kÕ® ò?l^ÕY-0ñ?ؼª³Z ð?» ”˜ò? =bôÜÂò?pënžê�ò?#ö  Ùñ?˜¾××ñ?€ ˆWŽò?z9ì¾cxò?©h¬ý�-ò?¢ ê[ætò?Ôšæ§hò?¶øã™ò?žxÎZò?F^ÖÄ‚ò?Lª¶›à[ò?�œ¡¸ãÍò?Ð}9³�ò?ªò=#Úñ?LŒeú%âñ?–“PúBÈñ?3§Ëbbóñ?*©ÐD˜ò?>\rÜ)�ò?cì„—àÔò?°rh‘íüñ?Óg\WŒò?á—úyS‘ó?…A™Fó?ðN>=¶¥ò?¥Û¹à ò?Ñ@,›9äò?qåì�ÑÖò?†7kð¾êò?3¥õ·`ò?§wñ~Üþò?ë8~èò?/3l”õ›ò?ZÊû8Zó?ožê�›áò?ˆÙ˶Ӗñ?&S£’zò?[²*ÂM†ò?>BÍ�* ò?Êjºžèºò?ùÙÈuS ó?ïs|´8£ò?²ˆ×õËò?V€ï6o\ñ? F³²½ñ?‡6áñ?cFx{‚ó?½f¾ƒßò?®HLPÃ7ó?Ç.Q½5pò?¼ÉoÑÉó?²ºÕsÒ;ó?À%ÿ”ªó?úDž$�ó?]¿`7l›ò?{.S“àÍñ?ÆPN´«ñ?ÅS�4¸-ó?}"O’®ó?Fê=•ÓÞò?Q¾¾Öeò?µÃ_“5jò?3‡¤J&ó?•»ÏñÑ"ó?—«›äÇò?³˜Ø|\ó?Æk^ÕYíò?Tn¢–æó?xak¶òÒò?ð2ÃFó?áçSÇêò?I®€B=ó?<‡2TÅó?Ánض(sò?·ìÿ°eò?ßÜ_=î[ò?Mu€”ò?Ó÷‚ã2ó?ÇeÜÔ@óò?ÆnŸUfJó?…—àÔ’ò?fj¼! ó?Å °rhô?߀cOó?ØÖOÿY3ó?Z.�óÓò?µ§äœØCó?óùõCó?LS8½Kó?%És}Îò?o+½6[ó?4óäšYó?oÖà}U.ó?$™Õ;ÜÎó?óWÈ\ó?–$Ïõ}8ò?ÀêÈ‘ÎÀò?ö@+0d5ó?±øMa¥Bò? ËŸoKó?º2¨68‘ó?odùƒÁò?Ø€qå,ó?3NCTáò?øÃÏ^ò?ƒ0·{¹�ò?uèô¼ ô?o�»ló?-y<-¿ó?vüó?Mjh°�ó?ެü2£ó?'/2¿ô? Qºô/Éó?HÀèòæ0ó?ÿëÜ´gò?Ęô÷R¸ñ?en¾ݳó?\8’Œó? W@ÜUó?J}YÚ©ùò?¨�+õ,ó?öÐ>Vð[ó?7ê°Âmó?Ÿ ±Ý=@ó?q‘{ººcó?>ê¯WXpó?�œ¡¸ã�ó?¯½7†@ó?×û�vÜpó?ÿ²{ò°Pó?ŒÙ’U®ó?3NCTá�ó?ÎÄt!Vÿò?Ú(·íûò?øÞß ½úò?ªžÌ?ú&ó?øá !ÊWó?¼#cµùó?Äy8�©ó?cò˜ùó?ÀzÜ·Zgó?¼•%:Ëlô?Wÿ[ÉÎó?)í ¾0™ó?\Æú&ó?ZFê=•“ó?DP5z5€ó?Üóüi£ó?¾÷7(ó?‘`ª™µ”ó?gð÷‹™ó?øÁùÔ±Jó?õœô¾ñµó?·bÙ=9ó?~R›8¹ò?ûõ×+ìò?�%W±øMó?1•~ÂÙ­ò?,œ¤yó?b‚¾…õó?¢ ±ˆaó?úµõÓ–ó?ÂøiÜ›_ò?êX¥ôL¯ò?Ù=yX¨µò?–]0¸æÎó?A€ ]ó?ÐF®›Ržó?äÙå[ó?<ƒ†þ nó?¸Ì鲘˜ó?N³@»CÊó?«#G:ãó?9í)9'vó?DÀ!T©™ò?s÷9>Z\ò?¯`ñd÷ó?�c${„šó?$š@‹Xó?hz‰±Lÿò?ìW\ó?}ëÃz£–ó?THÞ9”ó?ø‹Ù’UQó?7ünºe‡ó?FΞvxó?óWÈ\”ó?p|í™%Aó?Òýœ‚üló?¬ÿs˜/oó?�ôiý¡ó?_zûsÑ�ó?PR`Ló?°�¹2¨öò?¼’ä¹¾ó?K %vm/ó?ž–¸Ê“ó?bž•´â›ó?E¡eÝ?ô?d=µúêjó?Wzm6Vâó?AeüûŒKô?™*•ÔÉó?tµûË.ô?úµõÓ–ó?¥¡F!Éìó?ÐñÑâŒáó?S=™ô ô?Eò•@Jló?}¯!8.#ô?ªó¨ø¿ãó?t±3…Žó?RD†U¼Ñó?ÊÄ­‚hó?¿dãÁ;ó?X­Lø¥>ó?b�*ß3’ó? ÓÚ4¶ó?+‡ÙŽó?ó:â�Íó?,H3M§ó?R›8¹ßáó?Çg²žó?�Cý.,ó?Œfeû�7ó?KXc'ô?AH0�Ûó?F~ýìó?(`;±�ó?b.©ÚnÂó?-'¡ô…Ðó?„�bÕ ô?ô¦"ÆVô?N¶�;Pçó?‚ŽVµ$ó?=·Ð•Èò?sôø½ ô?!ÍX4�Ýó?H0�[·ó?ñÕŽâuó?¬9@0G�ó?„œ÷ÿqô?  Y2ô?éCÔ·Ìó?o.2Þó?ë7Ó…Øó?µùÕ‘ãó?rNì¡}¬ó?!WêYÊó?Õ•Ïò<¸ó?n¡+¨þó?¼S”Kãó?צ±½tó?r7ˆÖŠvó?w;Sè|ó?n¾ݳ®ó?TÉPÅ ô?|ÓôÙô?…%P6%ô?v3£ §ó?«x#óÈßó?©¥¹ªô?>Ab»{ô?ur†âŽ7ô?, ‘Ó׳ó?²,˜ø£èó?·Ï*3¥uô?Ž<Y¤‰ô?w„Ó‚ýó?¿ïß¼8±ô? m9—bô?Ù@ºØ´Òó? †7k0ô? 6®×§ó?˜Q,·´šò? ý\¬ó?, ‘Ó×óó?ßÜ_=î[ó?k :!tÐó?4w¼Éïó?ådâVAÌó? %“S;ô?„4f’ó?šÚRÌó?5s»—»ó?=×÷á aô?Ÿ��·1ô?÷@ô?§ ?¹îó??;àºbô?äÜ&Ü+3ô?âåé\QJô?õÕU�ZŒô?A¹mߣ>ô?EõÖÀV‰ó?4�"1ló?V ÂÜîeô?Üôg?Rô?÷.9îô?Š9:ZÕó?ÃIš?¦õó?t`9B2ô?ûÍÄt!Vô?¢ÏGqô?�Ÿâ8ð*ô?·Aí·v"ô?'á_Mô?ëŽÅ6©èó?O�?ü<ô?ú'¸XQô?Å7>[Gô?‚rÛ¾G=ô?ÁÆõïúó?<2V›ÿ×ó?LŒeú%âó?cÙ=yô?¼=ùRô?E¼uþírô?åÑ�°¨ˆô?é Œ¼¬ ô?xµÜ™ Fô?1]ˆÕ¡ô?ÎÝ®—¦Hô?9~¨4bfô?- PSËô?s‚69|Rô?Ó+£‘Oô?¿ó‹ôWô?&üR?oêó?€J•({ô?1îÑZQô?6t³?Pîó?€'-\V¡ô? 5?þÒâó?(›rÅò?À éÓ*ºò?Ç€ìõîÏô?¯\o›©Ðò?iâà ô? EºŸSô?·D.8ƒ?ó?$~Å.òó?מ—Š ô?¥ßPø,ô?æ[Ö5ô?iª'ó�¾ô?ž#ò]J�ô?t(CULeô? åD» )ô?¡ƒ.áÐ[ô?u?§ ?[ô?OYM×�ô?@1²dõ?zóWˆô?†Ê¿–Wîó?¶Mñ¸¨Öó?§!ªðg¸ô?¾Ý’°kô?:\«=ìEô?¡GŒž[(ô?%ZòxZ>ô?ޱ^‚“ô?ÜÖž—Êô?QLÞ3_ô?ó:â� dô?;5— uô?Ôe1±ùxô?ÔÔ²µ¾Hô?»G6WÍsô?^ÒƒNô?sôø½�ô?øpÉq§tô?~p>u¬Rô?Ý^Ò-ô?ò˜ù>ô??ªa¿'Vô?mrø¤“ô?|HøÞß ô?j‰•Ñȧô?7Þ«�ô?iqÆ0'¨ô?­ø†Âgëô?Â26t³ô?zÄè¹…®ô?©MœÜï�ô?—qS�ô?¦bc^Gœô?O!Wê™ô?ê‘·µEô?!�Ky ô?'¢_[ô?å}Íó?†!rúz~ó?q!�àF ó?£W”†šò?íÿ°¥ó?Ãdª`TRó?áÔ’wŽò?å�Cª"ó?¾h�Òaó?¨½ 4ó?À•ìØô?Ì^¶�¶†ô?ãâ¨ÜDmô?¥2Å]ô?çÄÚÇÊô?LnYk¨ô?'0�Ö­ô?0Ø Û¥ô?aO;ü5™ô?�ôiý¡ô?dËòuÙô?1ëÅPN´ó?µÿÖªô?€šZ¶Öô?('ÚUH9ô?Um7Á7Íô?m±ô?"Æk^ÕYô?›YKiô?©iÓLwô?I+¾¡ð™ó?™~‰xë¼ó?½ûã½jeó?}—R—Œ£ó?ç7L4Hô?ý†K®ô?G�J±£qô?.�óSœô?d:tzÞ�ô?j ùœ»Ýô?­5”Ú‹¨ô?:\«=ì…ô?eU„›Œjô?w�’ `ô?¾†à¸Œ›ô?“þ^ �ó?ÑvLÝ•Ýô?i­hsœÛô?&P6åÊô?Õ"¢Øô?œˆ~mýó?IZÖý#ô?9 {Úáô?~Q‚þBÏô?4ØÔyT¼ô?*œÞÅ»ô?io�õ?G­0}¯aô?$´å\Šëô?¸�Üšt›ô?¡l\ÿ.ó?¹¨Ådó?XU/¿Ó$ó?5cÑtv²ò?SçQñÇò?±ÀWtëµó?o¸�Üšôò?4¡l\ó?ß©€{ž?ó?¯]ÚpXZó?8h¯>:ô?T:Xÿçpó?`¯°à~€ó?̶ÓÖˆ`ó?êé#ð‡Ÿó?k'JB"­ó?Å’r÷9~ó?©ƒ¼LŠó?ø¥~ÞTdó?/ßú°Þhó?ep”¼:‡ó?Í‘•_ãó?h@½5ßó?kׄ´Æ ó?¸éÏ~¤Èó?b†ÆA\ô?ÕèÕ¥aó?³¶)Uó?F–̱¼kó?e73úÑpó?¢ðÙ:8˜ó?Ç€ìõîÏó?=ƒù+¤ó?”¥Öû�¶ó?¨Åàašó?n£¼’ó?¢³Ì"[ó? ˆI¸�‡ó?[| €ñŒó?ê?k~ü¥ó?©„'ôú“ó?¦~ÞT¤‚ó?dyW=`^ó?t—ÄYuó?Ôž’sb�ó?æ]õ€yÈó?Jëÿ¦ó?„~¦^·Èô?¸£îô?ÿy0Húô?=›UŸ«íó?OÏ»± °ó?`YiR zô?ÿêqßj�ô?õL/1–©ó?r1Öq¼ô?iÚV³õ?qXøQ�ô?ê¯WXpÿô?ÇeÜÔÀô?W=`2åò?—qSÃó?cÐ ¡ƒ.ó?G 6u^ò?NÒü1-ó?Ô¹¢”ìó?kò”Õt}ò?ë˜Ü(2ó?Î4aûÉXó?-B±4íò?žÏ€z3êó?ù0{Ùvšó?¢+Üò‘ó?e73úÑpó?’“‰[±ó?¿CQ O¤ó?8‡kµ‡}ó?JA·—4†ó?J&§v†ió?Ɖ¯vgó?íFó�ó?6\�íó?±§þšìó?:<„ñÓ8ó?Ní S[jó?Ä&2s�Ëó?ý¡™'—ó?&rÁü=ó?F жšuó?‡¥�Õpó?�Øî »ó?õfÔ|Õó?3oÕu¨¦ó?›�kCŸó?vüÂó?ÙCûXÁ¯ó?Ò4(špó?[%XŽó?nߣþz…ó?X¾Û¼±ó?ظþ]Ÿ¹ó?âeS®°ó?¬�±^‚ó?ý‡ôÛ×�ó?ˆŸÿ¼¶ó?G­0}¯¡ó?Õ@ó9wûó?7n1?7ôó?´Swe×ó?¦�Õ°ßó?[[x^*öó?AŸÈ“¤«ó?£V˜¾×ô?°|·yãó?8¿a¢AÊó?Ÿ;Á¾ó?©MœÜ/õ?C —8òÀô?ŽW zR¦ô??ÿ=xíÒô?$Dù‚Òò?ޝ–;só?k‚¨û¤ò?Ùëݯò?ÁþëÜ´™ò?ÖÆØ ¯ó?ðÝzMÏò?€ôMšÅò?Ð캷"ñò?÷!o¹úñò?y”JxBïó?/¨o™Ó¥ó?ðO©e¯ó?›sô¸ó?㦚Ϲó?jjÙZ_¤ó?zŠ"nŽó?~ŠãÀ«¥ó?©2Œ»Ató?Â1Ëžvó?Àêȑ΀ó?�ßÛôgó?ˆdȱõŒó?:°!yó?Ù /Á©�ó?´�€Ñå ô?‹ÀXßÀ$ó?U/¿Ód†ó?dèØAeó?‚âǘ»–ó?o+½6+ó?h°©ó¨xó?¦º€—6ó?á@H0Aó?ñÖù·Ë>ó?jÛ0 Âó?~âú}ó?^€}têÊó?H‰]Û›ó?äø¡Òˆ™ó?Ú|a2•ó?ÿ$>w‚}ó?m‘´}Œó?Û¿²Ò¤”ó?µó¿ˆÈÍêD¡?5\!­ñ?'@1òÒ@¬y;Ýê¿(ÜÀ˜<±?¦ íjNù?r7ØéÚ2è?3‘àl4æ¿Áå3*x|õ¿d¯Ô](…ῘRyJ­é¿]Vçnñ¿eŸãÀ:@3Å;Džá¿»`…JóGÙ¿Ø ·*³û¿ñÚV «˜ÀÁÔG Ió¿4Ë>³Ð@kqK³ž ø¿o¢S�’H¬?¬èŽ’c¶?|s0n/sÀCÔ@±2á¿…Ñ¿£õ¹ˆôä¿-FÞV!]²?й1¢_Øù¿à¼ò§)˜ñ¿_4ÉX¡Ðå?ÙÈ –óä¿›QE{ýö¿á¾¬ª%ô?’¼>°|XÜ¿ïÍf¦ì¿Ì} ÂßwÞ¿ xç?ÐË?¦M²7d�Ó¿± ZRå¿Þ¸r~<=ð¿rÉmln¡ù?G°%\7‚ô? ÁA’¤Ã£?•UäéüHÔ?x>gjêÀŽo0{G�õ¿Ë¯¥´>7ó¿ÄnÚÑL�û¿ô¾3‡î¿ @mŒpæ?Ú”–€—²ý¿-nÜæ_ÀéáÈáR—è?eàŸgl@äÛ �ceë?¯ã7Ä#Ûè¿¡òê-Ðð?8˜ˆx°˜Ç?$wT·>^à?I ùÚ)Ù?X võÍ?žLˆdÀë¯�ç?â]A‰y¥ß¿¸‚D-Éóõ?’bYMâaê?J×Q9Ðí¿øA)ƒqr @Øíã{'A÷¿ikUË�Ä÷?ÎK��à ò¿ué“Ìœ~î?‡•À�p@ w‡•܇—¿Ì qiV ÿ¿Âµ\•—~ÀtŸg±‘…¸¿†ÏFã¿#†Ù+ÔÂ?@î'VŒ£º¿ð¿°„É£¢?$,X¦/É?b ËÜ æ¿p!"�xõÀ�~0´—Ù¬¿q7å5sÕñ?€Wºé̶翢üŠö©û?\-©H¡Þ?r¾B*mÞ¿eLÆ!¥ÖÒ¿vZâ6j@gõ‘Úwû?¤]á Éõõ¿§=³b=Ã?¬ª|Mc�ã¿æ¼?ô°pÀó³, «=Ð?4Y±ºÔ„À7¸á¶iï?©Îª@;#ò¿’¬”{ë®Àì©Þ�HÒ@ øëÒ¾? ,Kɽàþ¿‚‚�–Rö?Jbà¶­®é?|�sq¡Nð¿(nÂu·ç?Ùdž:Æ7⿹H‰J]é?¸ÊoJÜï¿màCÚÀèœÒ 翨™-Ú– ÷?ÏF=ŠŒÉö?^È)¹ÀÎäq¤ˆÀ¦ÑLçûz¿.+_=Øà¿„âi?9Uæ?µ¨˜3À¶O¦#Ý¿³ÿn}ò?1Š ƒ!;á¿PþûÚ{›è¿µ‹³Êž¶í?·úÅvó¿÷§6Ú—?ÓŽê/oÀ®—&ÒÕý¿tVÈ�^Bæ¿–ñ ©h…ç¿î¬‰(.@Àaq!Ãø?<(ni[À†r…Õ{`á¿$¤íÙ�ÖÀö¿ãg@”µ@Ž_ÞÃ?b¬¸<2@�Í>; ½¿ÛRŸÚd(@ÇÎÛyÅ À²”œPYóò¿Òƒ_øÆÁ¿.Tß„üô¿©f6°Ö¾?¸Vk­îâ¿È“�šÑ¿›0,“éò¿Ï3Pþ”0À¡¹¹ÿ?à†«û-çÜ¿ìM° »5ñ?ŽÞî±ðÀÀxÚYLÌÔÞ?œ½È“+í¿Z¯�½Àù¨«“{Ú¿ü¯¸s$Ú?¤|™ñAà÷¿L¾¡%õíù?,³[qv'õ?Tc×�uið?V”Ãîüô?0ž¤GT^«¿‚)ù6åù?XÂlîÀ`"5…–Rð?Ë89Äq Àð#²½âË¿øì”C¥ä¿€k1e? Æe·Ã" À‚왲P:׿ÐNB�À¬Ï>Žx@Âý á λ?0ÿRÝÜò?Ô‘Ù.è0à? ÖŸc¿ À<“å5€Eú¿ÓÕ¯¢¯~ÀLUwçû¿�ÜB Æ?�Ki=ŸÀÚ^ï@&û?½äŠ‚€ Ú?Ù:µâgì?=,Ñ.¬» Àô¢þB©í?$kDˆ)ú?8&QtÖ?¿ÐÂIî@íZ?h˜ö@]Å1׿â¡�ʬ@�·A.oCÀ4Bê�×k@$þƒ+ôrð¿7ˆet�Ú À|{û<¢+ÿ?ýGñ›Âß@*Žbò· @‹a (ªë¿, uìzGÀâi4?„À¾Ð¬Á[Ö¿­¦ Í“ì¿1i ·Ê ü?î¾*‡BÚæ¿BVË´8ó¿vÌžÑŠß ÀoˆRvÌZå?@ROÅí @Ã= á @Ä_RR@kÂ<jÅí?†vð„BËù¿˜mOë ˆÛ?цÇßuß@úÀvÁjÖÿ¿VÃ�}9@-’«„¹@�ë}üÄ¿ŸAÔWWêô?4ˆß�ß¿”8OÛ�Àx¼æ Z@Á¿ÇÊ¢ @ÆÿùÄQ ÀàdnÒIä¿vbæÄK¹æ¿ø”ðWIÀ�ÙûÞÿ„ã?j€äð™rø¿ÀY>¹ Ë?ÆZ�•_Ð÷¿hméT î?ÄŽ(O›�ò?àÖö,…¶¿~Bû²À®~—¯£ @ÐÅmwÒ¿•îs 'š@Çâi�š8À`aŸ–ãuß?àÞ¬D‚½Ù?uýÖ¯†^í¿‘ˆ½e�À@aÕ*°À|±šˆS²@ìG�ä¡ßÀÔWUUéæ@æ�Ïá¿(ê?ó\!«{Àòµâ ^£å?ÈÓo äß?òÔÈoà@yµ ½TCé?ÚÞäöH@lÖk0u¤å?óc®Ä9Àÿ�ÕâçÅú¿º K¯-rÒ¿ámÝØ2ÐÀ0>‹*ªiô¿´UÙÌþxÀ#ãf)€b÷?ø®öZ3¼ó¿"©æ‘ºqô¿ˆŸÓaVÇþ?ž¨Òüõ¿8KزõÃÛ?EÊiÌ #à?…ªñÁnü¿?‚ñÔ?íø?n9JxÄ5@Ð+©vó¡ò?ñ]_„t™ä¿(� }Nú?;3v£ûê¿ìùž6`ÀܿȄFd”À5ÍëÃîñð?n¹EÅ”^ÀHЩؙ‘ @d·á�§@ÄJŒÀƒɲ¤Ø?}%Žá?àŸg­n÷?ćÔ¶óæ¿ä©KÝÀ•Ê¿ÊNn·+%á?3»×Ì5óõ?†1¬"‡�â?Ô‹ûMëÇ¿}™v[ÛÈÿ¿©`í´?Åà ”¼7é¿×NÉj}²?#ÑK‰Ìø?¯”ÄîBí¿Šš0¨ˆsñ¿ÿtd¬–±õ¿VyV‹å–@±ä5SÝ¿Áþ«Ú?ï?à¦<@Ä*‚?ðÍâÂ#Ùý¿‹�ƒÍ>ïé?‹ ìŽ7Ö?ÄbÑ?á¿àTñ©`ôè¿0¥%|¤4οʗÔ=ì=ø¿ÄÚ”Zí×?b;�Êð Ó¿½h�@òñ¿ GÇE¤åå¿TVËy9Ö?»v Ø—@ òÞN‰!¼¿ct\Xÿ×ò?ŽõÅ)�ù¿î*8Ó8Ï?÷õTù³¾ß?­šà�­ÿ¿3tnîw[ö?;²(œ.³Ú¿‚ì¢|@ö?ö*MðÞ?Ñ&á+R׿Hé“Ûyï?`¾Éá�¹?w/w×dÀ?´€Ÿ3áÍ?(½"Õm4ž¿hg5Üȹä?aŸ;NC¸ñ?HUõTAÈ?9õ¶SóÑ¿X4̃þ÷¿ÈKÜzÝ?SSïå¥ðÜ¿°Åoï?-Ê?HÙôSnñÓ?ˆ~š¥«Xä?öß�ª²Jå?Fž¿`Öü¿Í÷>¡(Ö¿� ‘&¡ê?sJ³&Xó¿;{õJbzê¿ 3ÔÕê¿wÕÖùÁ?Ì»�8ô 쿸‘�®?¤ç?nt9Чrð?Âò’¨tá?ºH}#u‰À[â#ÍwÉ¿ž²•ûÓ?2PKÑ=æ¿?ƈ7¦6Í?}HÎ[òØÑ?V ×eÍê¿xÎ×§x@[|2Q]cù?`–î28á¿ç8ÕZèá?’6.?â¿?ׯò¼NÛ¿ù«E#HÇ¿['*�ž%ù?‰kfL©ä¿æPèÉ©?œ²è?Ô&õ§£·?¤Îô²ÿ™í¿þ^�aèþ¿Wš TÚƒÀà�…нë?Ø,1åQ°?°5ñ’mWð¿kœU}j7ú?ÂúEI¤Î¿Yâþš¿>ý¿y<%’�Æ?t±JpÕ8æ¿z%lØo¬ò?,ÿ*<‡Õ?÷�ƒ.Ý?ÏšB±¸àâ?f?4V=fí?Î��xdý?Nðf¹Æ¿ì“MÓtþ¿n¡ó²U@Ü·Ë&†À=`œÕöÊ?^Ö•÷”Øé¿h¡y‹6Qå¿ò=Ï©ÿ¿È À²15ñ¿&.±¾Éêú?0¼„‰`{¸¿U=)æS>ï?'Ç-CÇé¿@–Lš»Uú¿w€ysçö?®ñél¥ø?9eèpôdÔ¿ ‘ô[á?¿J*ˆù7ø¿,çVÓtÕô¿žÙ1òÖ?|Ò묪„ÿžHù›ó忤äÕ$í¿ÝÛ{ ›Ë?‡Çwõ?ÙmÎJÔ?Åãará@ ;÷Þ¼vÀ8Ú‘ ÆY²¿D/æsâ•÷?4Pé²åú¿À.&Ž´¯¿RJ5½¹Mþ¿(,Aoó¡÷?/� fö¸ö?ÙÄV¸Ù¿™A9_Úó?kóáØHÏú?>S+´â¿Ø|‹N>ñ¿vÍ"¯oÚú?~@±‘Ö?ýïLgøÄï?͵)‘¥—Ù?ŒXÓýOh¢¿òµŒ~ë\À,ƒ¶þßê?&Ï8p¬Àø#èÚÈšó?P˜«‰²¿èc‰2糿D>QZ ê?ÇK·¼ÊÀœ?15)Ví¿L–f>î¿ÁS e4ø¿….�FLá¿ÂÒ-*ÖòÀÄ÷WrÎe@Ð*71Ýû¿A§ƒ”íæõ?-Z2Ÿö? y˜§T³ô?ÃHû[ À \®>é�‘?ÒV‚yâº? }#B¬³À?šyýÿóÍÜ¿8‹“%)Ü¿ ¼/’–â˜?ª�*�ší@„„[sCŸö?üÌ6éhã¿ 5¶d©í@(±Æ ¡Ü¿?(§Ù™žªÏ¿¿27#:mË?P(YíË߯?c’�0d�À©ÁÔ˜Öúê¿FÁ8A?¾å¿Ú÷ÔS–ê?Ük¹èvÐ÷¿é]J#‰À`³UCßø¿¥—ÜÊ¿ŒL.XÄð¿PöÙNšñ?Dá•Ñü?hp[dPVä?¥Ä].".ñ¿4$ðŒà¿,Joœßuñ¿ ¸�ˆŽ¬É?%&Uaóû? ö+9pþè¿ìÑé“Ró?ì øŒ»Õñ?c°‚÷mí?$©\.ÊÃ?tòãfQÀî©ÊV˜í?ørtç™@ÙœK�Úéú¿°«SX¡?¦¥å�ò?·-Ø<ôü?É”§»Ž/ø¿ú�„ixò?Ì?ô.VHý?rIàN˜[ÿ?/=–]�#ø¿è;B,Ýs÷¿±DªÅ·@”²XUúô¿u¹F‹T!ï¿,Wœ¨ßì?7/ðÄðW@^\/ŸÎ ÀâA¹ŒÄñ?h9yhí¿ˆæî]äˆá¿ìš#ôù¿éH�Xà?Ô±­Éµ‹¿Å‡2÷|š @ŽÔ'»¤à¿.»`À~¿×1ú?í˜é«<Ð?QbCÅ›™Àð¢�ª¤ê?VÒ°è?‹Ÿ9öêò÷?Eqº¡¦üæ?1·j(æ¿@ŠÌBÒ¸?4†µ�âä @¤ç= =ñ?4ïvÈË”¿°ß} 5Ê¿ ™¨ÖWgп N"Cç,ö?ÈÞBþ„‘í¿Ch�A¸ò?¦Sé¸Ë?&GíHp@tü b¹Iâ¿Éâ¢#¤ìæ? Y¤ ^ð¿ÏÂU›°àâ?ˆ“^¬F%ø?žûlÒ- ÷?W ­ü¿ÆDÛñÃ?8›ÃyoÊ¿0‘šÙØBè¿™û>ŒžÓÀÀ7ÿ‡±¿ÙäëãÃbá?¿Í;ß?º©¤'@×?X3xÙEƒÛ¿ñ»�—FCÀÛÏœT2Å¿,˜ÒǘõÌ¿Ž¶Œ1Cóñ?X‘hwпwøI.ÏŽó¿â�)’üé?žìáÛw@À×Î1öE˜?>â�@v#À0UÚçŒl@•ˆS|ËÑ¿�’ˆÉò*À&Çý˜Žð?°�¶nO@–Y?%@,ЪÊt¸È?+K=™ä(Àò|±ò®uô?rßÉÑ€ÊÚ?9yX.9­ì¿íá¥Æ°ó¿ŽIk$Ì@œÂá·änï?Ü)Iìßøç?¦œaîüÞî?O ƒÇ»{Û¿@dœ×ô�§?‰¯XÝ „Àƒ€6òtÀ(?¤:´Öù?é̤›&ý?cîà|QEÊ?ó• ñ?X¶þÍ?¸ø?† �æJÛ?*2²‚œ©ñ¿¸2¥["vÀ¢cÇ9q1ü¿Zåó{nò¿ »kïÀ«Ñ?òíǤù?d þ‘·á¿É7<¼ËaÀžüKÝý¿›ëJœ::ã¿Ý|ØG:…ú?`<­Q*=å¿Æ=6Øó¿12aõ*Ñä¿(Ã3ÝÝ\ÀYjµ QTÔ?†oj$3ì¿ÖíÃõµø¿é‹c ;‘ý¿<º–ÓuÁå?ªX¢ÿ7Òsw-WÓ¿óì£�Û¿GßÑC?qñ¿aD€Ìì Å¿‘ñ:)uø?ø㞦¿¦UêsØGÆ?ÕŸÅç.kâ?ô0[ÄbÈò?×AÄ4ö?á?KWåëp&Ò¿M<þ� Û?cáµÙî¿ÐqDZÚAä?¸œ*¹c ?çÒÏÁ?Ó?A$CÇ>;ÀÙ¼æ©ö?¾¶¹|¸? FÄóÌià?¡l�BµÌ¿äIO5èëæ¿JÝK2,»ë?ò•{ØœLÿ?=,a², å¿ÔS@¶'v¯ÜÓ.Û¿(Dµ`kö¿òܹ�î`á¿)ÊkDÿñ¿Ï%3zD0â?Â\}oYÌ¿�J¡]ùÀ�O hEVã?¼zøMçÔ?ð¼+.7ˆÙ¿qù(R”ÿé¿c»Ø?‚FüÁû¿,gj`Ø¿zh<‹œã?´ys\EÕ¿¬wYéüoп@éí‹ÁÀ¿‚øÆÀ—nÀ¨Þ¶VT`Þ?¼BæYç?€ê»(èv¿‰û �ç?0j‡šÄí¿r)‡ªÿò?moEHLû?4°¸ñÙó?2¦4>§žù?‚ÿx4bÙ¿2ä`ÊXÕó¿™�cñӣ迈Ã$Ø;ùâ?DLÜs~Ñ¿Á`öª ö?£+"­¹’ü?FV þ¿æÞ‹EÛ«ç? §!¢p|Ô¿‰xÈèM<ñ¿. y3^œÓ?È<×ôßÿ¿ðÓx¾'¢?4à›¼‹„ë?ï|H>µÚ?èÃÄQÖFê?ìMYÏ8)¿?Ä�xƒ*êó?ìíöNzQÛ¿‡¾¼Æá?€ØûMiV¿$&�Yõ¦á¿†}c„^ø?õ^�¾ ²Ú¿xÊoxüÿÉ?’ªX&`¢ý¿Î¿ƒy—óò¿Slä?’пAâ|±u*ò?H.•Ô7Ü?<Å»*V'¡¿´G oíÀ^âK uüú¿rÓIG¬$Àü? ˆf @{�‚þ,ûÀ6´Þ~Û'Ö?SÜ?ýìû“�ð?°^�³1í?û–zaðÞ÷¿Ò? .éWÁ¿-õ5å½Ç¿,:í�ù¿’òúÁ[Þï?©6à„uÖð¿F£„Aêó?@ÎFiÐõ�¿Š!ãkÄPö?}}ÿ Øn÷?�²¿éðÉ?(ÇøZzBÖ?GÿÞlD&ð¿u­H,¶õÿ?òaÊ©—ÀŒïPÆ À%ªîÑbGð¿˜Háaf5ú?+… ÑÈù?’‘q£Çã¿@bYèîü¿x@¢¸6`á¿¿ê®éü*õ¿BsŠýá?$¬Ë9rÝ¿ØT˜DÙŒÛ?íÈó‡8Úå?ìÉ®N’’ ÀBT©üßÍ?�âê-÷;è¿.Î÷SRÔ¿†ñL>}ž@PÙ I´å?R9LUmð?¾ ‰~Ò§ë?Q}Eo�»è?Äö´¢µÜ¿6ñNMkõ¿}5Œ‰T–À†!Ïùî¿p _½s¦ñ?°Ž„”X/Ü?dËÍÙ/ÊÛ?²b­vŠoØ?/#÷_Šmð¿ÿÉøVÜt翇¥œ‰ѿ֞{’Oç?òµ"¿»PÁ?2{kAS�@Ø @}"ë¿›NNu%ââ?øi’ßQö?R-�j%Œò? ñ•‰�Nß?"%;â¡Àå¿@èfÆ`‹¡?w¾œ0s @­¸>þgaá¿í¯9púÞ¿?¸Gº’ï¿ZÅÝ{Îû¿X Ë„;ü¿ðº‡êú¤Ý¿Xþ–YÁÖ¤¿v®T½ @CXÕ‰sùë?p�G(ÙÝá?‚)p™µ@PÅ;;½ø?\¹Ãú»‚ð¿J¶€t äà?jQúÏÔ­ï¿8 20âá?w]÷‰îpù?ºî[–ãè@o/ß¿€YTg»ó÷?ßL�KåäÀ´fŠ´Áæ?�ó5ÖOÊê?ägf„¼Žù¿¯7ZPÖò @Ï“*Ö˜þ¿ÆVžh¹Œé¿ï«xÍö¨î?4ÊbòÒã¿›MxþI2ò?¸a!É�­¿þs�ªÑí?Ôä«ðÇÀÀË�§ó!ì?0ò†v©©”¿Ï±$¶é¿®[¶�ßâ?*B8)�Zî¿;¹ëZ_@·É„mIý¿¶:?yêmø?¸Ëö¸pð?ÞwŸi,—À µž¾%hö¿¡Ö@ë¡ó?½GµI1á鿬·ü‹ Ïó¿‹Š˜·Lå¿+–�«ò„Ñ?›¼RlÌ2ÀD?Ŧšê?statistics-release-2.1.2/inst/datasets/weather.mat000066400000000000000000000010671527311104300222540ustar00rootroot00000000000000Octave-1-Lyear1ÿmatrixþÿÿÿ€I@J@O@O@J@J@€I@€J@€M@€O@€M@L@€O@€R@Q@€U@€T@€Q@@Q@ÀR@@R@€H@€G@I@N@€M@N@O@€N@ÀQ@year2ÿmatrixþÿÿÿK@€J@P@€P@€L@€J@K@K@O@€P@€M@€M@ÀP@S@ÀR@€U@€T@ÀP@€R@T@ÀR@K@I@€J@O@O@O@R@N@ÀP@statistics-release-2.1.2/inst/demos/000077500000000000000000000000001527311104300174055ustar00rootroot00000000000000statistics-release-2.1.2/inst/demos/ClassificationGAM.edge000066400000000000000000000022231527311104300235120ustar00rootroot00000000000000%!demo %! ## 1. The edge is the mean margin over the data %! %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! X = meas(inds, :); %! Y = species(inds); %! mdl = fitcgam (X, Y); %! %! [edge(mdl, X, Y), mean(margin (mdl, X, Y))] %!demo %! ## 2. Weights let some observations count for more %! %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! X = meas(inds, :); %! Y = species(inds); %! mdl = fitcgam (X, Y, 'NumTreesPerPredictor', 20); %! %! ## Weighting the second class three times as heavily moves the mean %! w = ones (rows (X), 1); %! w(strcmp (Y, 'virginica')) = 3; %! [edge(mdl, X, Y), edge(mdl, X, Y, 'Weights', w)] %!demo %! ## 3. Scoring a table %! %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! X = meas(inds, :); %! T = table (X(:,1), X(:,2), X(:,3), X(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species(inds)); %! mdl = fitcgam (T, 'Species'); %! %! ## The response is named by its column, left out, when it is the variable %! ## the model was fitted on, or given beside the predictors %! [edge(mdl, T, 'Species'), edge(mdl, T), edge(mdl, T(:, 1:4), T.Species)] statistics-release-2.1.2/inst/demos/ClassificationGAM.loss000066400000000000000000000033451527311104300235740ustar00rootroot00000000000000%!demo %! ## 1. The proportion of observations the model gets wrong %! %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! X = meas(inds, :); %! Y = species(inds); %! mdl = fitcgam (X, Y); %! %! ## classiferror counts mistakes; mincost, the default, charges what the %! ## least costly assignment costs given the true class %! [loss(mdl, X, Y, 'LossFun', 'classiferror'), loss(mdl, X, Y)] %!demo %! ## 2. The losses differ in how hard they punish an uncertain answer %! %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! X = meas(inds, :); %! Y = species(inds); %! mdl = fitcgam (X, Y, 'NumTreesPerPredictor', 20); %! %! ## Each is a function of the same margins, so they rank models alike but %! ## not on the same scale %! names = {'classiferror', 'mincost', 'hinge', 'quadratic', 'logit'}; %! for i = 1:numel (names) %! printf ("%-13s %.4f\n", names{i}, loss (mdl, X, Y, 'LossFun', names{i})); %! endfor %!demo %! ## 3. resubLoss is the loss on the data the model was fitted on %! %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! X = meas(inds, :); %! Y = species(inds); %! mdl = fitcgam (X, Y); %! %! ## The same number, without handing the training data back in %! [resubLoss(mdl), loss(mdl, X, Y)] %!demo %! ## 4. Scoring a table %! %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! X = meas(inds, :); %! T = table (X(:,1), X(:,2), X(:,3), X(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species(inds)); %! mdl = fitcgam (T, 'Species'); %! %! ## The response is named by its column, left out, when it is the variable %! ## the model was fitted on, or given beside the predictors %! [loss(mdl, T, 'Species'), loss(mdl, T), loss(mdl, T(:, 1:4), T.Species)] statistics-release-2.1.2/inst/demos/ClassificationGAM.margin000066400000000000000000000023541527311104300240700ustar00rootroot00000000000000%!demo %! ## 1. The margin says how confidently each observation is classified %! %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! X = meas(inds, :); %! Y = species(inds); %! mdl = fitcgam (X, Y); %! %! ## Positive wherever the model is right, and larger the more sure it is %! m = margin (mdl, X, Y); %! [min(m), median(m), max(m)] %!demo %! ## 2. A margin turns negative where the model is wrong %! %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! X = meas(inds, :); %! Y = species(inds); %! mdl = fitcgam (X, Y, 'NumTreesPerPredictor', 20); %! %! ## Count the observations the model places on the wrong side %! m = margin (mdl, X, Y); %! sum (m < 0) %!demo %! ## 3. Scoring a table %! %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! X = meas(inds, :); %! T = table (X(:,1), X(:,2), X(:,3), X(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species(inds)); %! mdl = fitcgam (T, 'Species'); %! %! ## The response is named by its column, left out, when it is the variable %! ## the model was fitted on, or given beside the predictors %! a = margin (mdl, T, 'Species'); %! b = margin (mdl, T); %! c = margin (mdl, T(:, 1:4), T.Species); %! [mean(a), mean(b), mean(c)] statistics-release-2.1.2/inst/demos/ClassificationNeuralNetwork.compact000066400000000000000000000010371527311104300264320ustar00rootroot00000000000000%!demo %! ## 1. Dropping the training data from a fitted model %! %! load fisheriris %! mdl = fitcnet (meas, species, 'IterationLimit', 200); %! %! ## The compact model keeps what it needs to predict and nothing else %! cmdl = compact (mdl) %!demo %! ## 2. It predicts exactly as the full model does %! %! load fisheriris %! mdl = fitcnet (meas, species, 'IterationLimit', 200); %! cmdl = compact (mdl); %! %! ## Same weights, so the same answers %! xc = [min(meas); mean(meas); max(meas)]; %! isequal (predict (mdl, xc), predict (cmdl, xc)) statistics-release-2.1.2/inst/demos/ClassificationNeuralNetwork.crossval000066400000000000000000000024601527311104300266410ustar00rootroot00000000000000%!demo %! ## 1. A five-fold cross-validated model %! %! load fisheriris %! mdl = fitcnet (meas, species, 'IterationLimit', 200); %! %! ## Each fold holds out a fifth of the data and refits on the rest %! cv = crossval (mdl, 'KFold', 5) %!demo %! ## 2. What the held-out data costs, fold by fold %! %! load fisheriris %! mdl = fitcnet (meas, species, 'IterationLimit', 200); %! cv = crossval (mdl, 'KFold', 5); %! %! ## Spread across the folds says how much the estimate itself can be %! ## trusted; a wide spread means five folds were not enough %! L = zeros (1, 5); %! for k = 1:5 %! L(k) = kfoldLoss (cv, 'Folds', k, 'LossFun', 'classiferror'); %! endfor %! bar (L, 'facecolor', [0.6 0.4 0.6], 'edgecolor', 'none'); %! hold on; %! plot ([0.5, 5.5], [1, 1] * mean (L), 'r-', 'linewidth', 1.5); %! hold off; %! xlabel ('Fold'); %! ylabel ('Misclassification rate'); %! title ('Held-out error of each fold, and their mean'); %!demo %! ## 3. Holding out a single observation at a time %! %! load fisheriris %! keep = [1:15, 51:65, 101:115]; %! mdl = fitcnet (meas(keep,:), species(keep), 'IterationLimit', 100); %! %! ## Leaveout fits as many models as there are observations, so it is the %! ## most expensive choice and the least biased %! cv = crossval (mdl, 'Leaveout', 'on'); %! kfoldLoss (cv, 'LossFun', 'classiferror') statistics-release-2.1.2/inst/demos/ClassificationNeuralNetwork.edge000066400000000000000000000024261527311104300257130ustar00rootroot00000000000000%!demo %! ## 1. The mean margin over a data set %! %! load fisheriris %! mdl = fitcnet (meas, species, 'IterationLimit', 150); %! %! ## edge is the average of what margin returns %! [edge(mdl, meas, species), mean(margin (mdl, meas, species))] %!demo %! ## 2. Weighting the observations %! %! load fisheriris %! mdl = fitcnet (meas, species, 'IterationLimit', 150); %! %! ## Weighting one species only gives the edge over that species alone. %! ## The weights are normalised before they are applied %! classes = unique (species); %! e = zeros (1, 3); %! for k = 1:3 %! w = double (strcmp (species, classes{k})); %! e(k) = edge (mdl, meas, species, 'Weights', w); %! endfor %! bar ([edge(mdl, meas, species), e]); %! set (gca, 'xticklabel', {'all', classes{:}}); %! ylabel ('Edge'); %! title ('Edge overall and within each species'); %!demo %! ## 3. Scoring a table %! %! load fisheriris %! T = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species); %! mdl = fitcnet (T, 'Species'); %! %! ## The response is named by its column, left out, when it is the variable %! ## the model was fitted on, or given beside the predictors %! [edge(mdl, T, 'Species'), edge(mdl, T), edge(mdl, T(:, 1:4), T.Species)] statistics-release-2.1.2/inst/demos/ClassificationNeuralNetwork.loss000066400000000000000000000036521527311104300257710ustar00rootroot00000000000000%!demo %! ## 1. The proportion of observations the model gets wrong %! %! load fisheriris %! mdl = fitcnet (meas, species, 'IterationLimit', 150); %! %! ## classiferror counts mistakes; mincost, the default, charges what the %! ## least costly assignment costs given the true class %! [loss(mdl, meas, species, 'LossFun', 'classiferror'), ... %! loss(mdl, meas, species)] %!demo %! ## 2. The losses differ in how hard they punish an uncertain answer %! %! load fisheriris %! mdl = fitcnet (meas, species, 'IterationLimit', 150); %! %! ## Every one is a function of the same margins, so they rank models alike %! ## but not on the same scale %! names = {'classiferror', 'mincost', 'hinge', 'quadratic', ... %! 'binodeviance', 'logit', 'exponential'}; %! L = cellfun (@(f) loss (mdl, meas, species, 'LossFun', f), names); %! barh (L, 'facecolor', [0.4 0.6 0.4], 'edgecolor', 'none'); %! set (gca, 'yticklabel', names); %! xlabel ('Loss'); %! title ('The same fit under each loss function'); %!demo %! ## 3. Watching the loss fall as training proceeds %! %! load fisheriris %! iters = [5, 10, 25, 50, 100, 200, 400]; %! L = zeros (size (iters)); %! for k = 1:numel (iters) %! mdl = fitcnet (meas, species, 'IterationLimit', iters(k)); %! L(k) = loss (mdl, meas, species, 'LossFun', 'classiferror'); %! endfor %! semilogx (iters, L, 'o-', 'linewidth', 1.5); %! xlabel ('Iteration limit'); %! ylabel ('Misclassification rate'); %! title ('Training longer buys accuracy, up to a point'); %!demo %! ## 4. Scoring a table %! %! load fisheriris %! T = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species); %! mdl = fitcnet (T, 'Species'); %! %! ## The response is named by its column, left out, when it is the variable %! ## the model was fitted on, or given beside the predictors %! [loss(mdl, T, 'Species'), loss(mdl, T), loss(mdl, T(:, 1:4), T.Species)] statistics-release-2.1.2/inst/demos/ClassificationNeuralNetwork.margin000066400000000000000000000026751527311104300262720ustar00rootroot00000000000000%!demo %! ## 1. How far each observation sits from being misclassified %! %! load fisheriris %! mdl = fitcnet (meas, species, 'IterationLimit', 150); %! %! ## The score of the true class less the best score among the others %! m = margin (mdl, meas, species); %! table (min (m), median (m), max (m), 'VariableNames', ... %! {'Smallest', 'Median', 'Largest'}) %!demo %! ## 2. A negative margin marks a misclassified observation %! %! load fisheriris %! mdl = fitcnet (meas, species, 'IterationLimit', 150); %! m = sort (margin (mdl, meas, species)); %! %! ## Sorting shows how much of the data the model is confident about, and %! ## how far below zero the mistakes fall %! bar (m, 'facecolor', [0.3 0.5 0.8], 'edgecolor', 'none'); %! hold on; %! plot ([1, numel(m)], [0, 0], 'r-', 'linewidth', 1.5); %! hold off; %! xlabel ('Observation, sorted by margin'); %! ylabel ('Margin'); %! title ('Margins below zero are the misclassified rows'); %!demo %! ## 3. Scoring a table %! %! load fisheriris %! T = table (meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'SL', 'SW', 'PL', 'PW'}); %! T.Species = categorical (species); %! mdl = fitcnet (T, 'Species'); %! %! ## The response is named by its column, left out, when it is the variable %! ## the model was fitted on, or given beside the predictors %! a = margin (mdl, T, 'Species'); %! b = margin (mdl, T); %! c = margin (mdl, T(:, 1:4), T.Species); %! [mean(a), mean(b), mean(c)] statistics-release-2.1.2/inst/demos/ClassificationNeuralNetwork.predict000066400000000000000000000027601527311104300264420ustar00rootroot00000000000000%!demo %! ## 1. Labels and scores for new observations %! %! load fisheriris %! mdl = fitcnet (meas, species, 'IterationLimit', 150); %! %! ## One row per observation asked about %! xc = [min(meas); mean(meas); max(meas)]; %! [label, score] = predict (mdl, xc); %! table (label, score(:,1), score(:,2), score(:,3), 'VariableNames', ... %! {'Label', 'setosa', 'versicolor', 'virginica'}) %!demo %! ## 2. The scores are a posterior, so each row sums to one %! %! load fisheriris %! mdl = fitcnet (meas, species, 'IterationLimit', 150); %! [~, score] = predict (mdl, meas(1:8,:)); %! %! ## The output layer is softmax by default, which is what makes this hold %! bar (score, 'stacked'); %! xlabel ('Observation'); %! ylabel ('Posterior probability'); %! title ('Softmax scores stack to one'); %! legend ({'setosa', 'versicolor', 'virginica'}, 'location', 'eastoutside'); %!demo %! ## 3. What the classifier has learned, over two predictors %! %! load fisheriris %! X = meas(:,3:4); %! mdl = fitcnet (X, species, 'IterationLimit', 300); %! %! ## Ask the model about a grid, and paint each point by its answer %! [gx, gy] = meshgrid (linspace (0.5, 7.5, 120), linspace (0, 3, 120)); %! [~, ~, region] = unique (predict (mdl, [gx(:), gy(:)])); %! contourf (gx, gy, reshape (region, size (gx)), [1 2 3]); %! colormap (summer); %! hold on; %! gscatter (X(:,1), X(:,2), species, 'krb', 'ox+'); %! hold off; %! xlabel ('Petal length'); %! ylabel ('Petal width'); %! title ('Regions the network assigns to each species'); statistics-release-2.1.2/inst/demos/ClassificationNeuralNetwork.resubEdge000066400000000000000000000012141527311104300267060ustar00rootroot00000000000000%!demo %! ## 1. The mean training margin %! %! load fisheriris %! mdl = fitcnet (meas, species, 'IterationLimit', 200); %! %! ## resubEdge is the weighted mean of what resubMargin returns %! [resubEdge(mdl), mean(resubMargin (mdl))] %!demo %! ## 2. The edge grows as the fit sharpens %! %! load fisheriris %! iters = [5, 10, 25, 50, 100, 200, 400]; %! e = zeros (size (iters)); %! for k = 1:numel (iters) %! e(k) = resubEdge (fitcnet (meas, species, 'IterationLimit', iters(k))); %! endfor %! semilogx (iters, e, 'o-', 'linewidth', 1.5); %! xlabel ('Iteration limit'); %! ylabel ('Resubstitution edge'); %! title ('Confidence rises with training'); statistics-release-2.1.2/inst/demos/ClassificationNeuralNetwork.resubLoss000066400000000000000000000015161527311104300267670ustar00rootroot00000000000000%!demo %! ## 1. Loss on the training data %! %! load fisheriris %! mdl = fitcnet (meas, species, 'IterationLimit', 200); %! %! ## resubLoss is loss over the rows the model was fitted on, weighted by %! ## the observation weights the model carries %! [resubLoss(mdl), loss(mdl, meas, species, 'Weights', mdl.W)] %!demo %! ## 2. Resubstitution flatters a model, cross-validation does not %! %! load fisheriris %! mdl = fitcnet (meas, species, 'IterationLimit', 300); %! cv = crossval (mdl, 'KFold', 5); %! %! ## The gap between the two is what the model gained by seeing the answers %! bar ([resubLoss(mdl, 'LossFun', 'classiferror'), ... %! kfoldLoss(cv, 'LossFun', 'classiferror')]); %! set (gca, 'xticklabel', {'resubstitution', '5-fold'}); %! ylabel ('Misclassification rate'); %! title ('Training error against cross-validated error'); statistics-release-2.1.2/inst/demos/ClassificationNeuralNetwork.resubMargin000066400000000000000000000012511527311104300272600ustar00rootroot00000000000000%!demo %! ## 1. Margins on the training data %! %! load fisheriris %! mdl = fitcnet (meas, species, 'IterationLimit', 200); %! %! ## Same as margin called with the training predictors and labels %! m = resubMargin (mdl); %! table (sum (m > 0), sum (m <= 0), 'VariableNames', {'Correct', 'Wrong'}) %!demo %! ## 2. Where the classes are easy and where they overlap %! %! load fisheriris %! mdl = fitcnet (meas, species, 'IterationLimit', 200); %! m = resubMargin (mdl); %! %! ## One species separates cleanly; the other two share a boundary, and %! ## that is where the small margins are %! boxplot (m, species); %! ylabel ('Margin'); %! title ('Resubstitution margin by species'); statistics-release-2.1.2/inst/demos/ClassificationNeuralNetwork.resubPredict000066400000000000000000000012731527311104300274410ustar00rootroot00000000000000%!demo %! ## 1. Predicting the data the model was fitted on %! %! load fisheriris %! mdl = fitcnet (meas, species, 'IterationLimit', 200); %! %! ## Same as calling predict with the training predictors %! label = resubPredict (mdl); %! confusionchart (species, label, 'Title', 'Resubstitution confusion'); %!demo %! ## 2. Scores as well as labels %! %! load fisheriris %! mdl = fitcnet (meas, species, 'IterationLimit', 200); %! [label, score] = resubPredict (mdl); %! %! ## How sure the model is about the class it picked, for each row %! hist (max (score, [], 2), 20); %! xlabel ('Score of the chosen class'); %! ylabel ('Observations'); %! title ('Confidence of the resubstitution predictions'); statistics-release-2.1.2/inst/demos/CompactClassificationGAM.predict000066400000000000000000000014311527311104300255470ustar00rootroot00000000000000%!demo %! ## 1. A compact model drops the training data and predicts identically %! %! load fisheriris %! inds = ! strcmp (species, 'virginica'); %! X = meas(inds, :); %! Y = species(inds); %! mdl = fitcgam (X, Y); %! cmdl = compact (mdl); %! %! ## No X and no Y on the compact model, and the same labels out of it %! [isprop(mdl, 'X'), isprop(cmdl, 'X'), isequal(predict (mdl, X), predict (cmdl, X))] %!demo %! ## 2. The compact model assesses new data just as the full one does %! %! load fisheriris %! inds = ! strcmp (species, 'setosa'); %! X = meas(inds, :); %! Y = species(inds); %! cmdl = compact (fitcgam (X, Y, 'NumTreesPerPredictor', 20)); %! %! ## margin, edge and loss are all available without the training data %! [edge(cmdl, X, Y), loss(cmdl, X, Y, 'LossFun', 'classiferror')] statistics-release-2.1.2/inst/demos/CompactRegressionGAM.predict000066400000000000000000000011411527311104300247320ustar00rootroot00000000000000%!demo %! ## 1. A compact model drops the training data and predicts identically %! %! load fisheriris %! X = meas(:,1:3); %! Y = meas(:,4); %! mdl = fitrgam (X, Y); %! cmdl = compact (mdl); %! %! ## No X on the compact model, and the same fitted values out of it %! [isprop(mdl, 'X'), isprop(cmdl, 'X'), ... %! max(abs (predict (mdl, X) - predict (cmdl, X)))] %!demo %! ## 2. The compact model scores new data just as the full one does %! %! load fisheriris %! X = meas(:,1:3); %! Y = meas(:,4); %! cmdl = compact (fitrgam (X, Y)); %! %! ## loss is available without the training data %! loss (cmdl, X, Y) statistics-release-2.1.2/inst/demos/CoxModel.coefci000066400000000000000000000021521527311104300222710ustar00rootroot00000000000000%!demo %! ## 1. Confidence intervals for the coefficients %! %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = fitcox (X, T); %! %! ## One row per coefficient, 95% by default %! ci = coefci (mdl); %! table (mdl.Coefficients.Beta, ci(:,1), ci(:,2), ... %! 'VariableNames', {'Beta', 'Lower', 'Upper'}, ... %! 'RowNames', mdl.Coefficients.Properties.RowNames) %!demo %! ## 2. The argument is a significance level, not a coverage %! %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = fitcox (X, T); %! %! ## 0.01 asks for a 99% interval, which is the wider of the two %! coefci (mdl, 0.05) %! coefci (mdl, 0.01) %!demo %! ## 3. On the hazard-ratio scale %! %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = fitcox (X, T); %! %! ## Exponentiating turns a coefficient interval into one for the hazard %! ## ratio of a unit change. An interval covering 1 is one the data cannot %! ## tell from no effect %! exp (coefci (mdl)) statistics-release-2.1.2/inst/demos/CoxModel.discardResiduals000066400000000000000000000013341527311104300243270ustar00rootroot00000000000000%!demo %! ## 1. Dropping the residuals a fitted model carries %! %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = fitcox (X, T); %! %! ## Seven residual types, one row per observation, is the largest thing a %! ## fitted model holds and the only part that grows with the data %! size (mdl.Residuals) %! mdl = discardResiduals (mdl); %! size (mdl.Residuals) %!demo %! ## 2. Everything else survives %! %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = discardResiduals (fitcox (X, T)); %! %! ## A model kept for prediction alone loses nothing it needs %! mdl.Coefficients %! hazardratio (mdl, X(1,:)) statistics-release-2.1.2/inst/demos/CoxModel.hazardratio000066400000000000000000000032301527311104300233470ustar00rootroot00000000000000%!demo %! ## 1. The hazard of each observation relative to an average one %! %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = fitcox (X, T); %! %! ## A ratio above 1 is an observation at greater risk than the baseline, %! ## which by default is the mean of the predictors %! hr = hazardratio (mdl, X); %! table (X(:,1), X(:,2), hr, 'VariableNames', {'X1', 'X2', 'HazardRatio'}) %!demo %! ## 2. Choosing what the ratio is measured against %! %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = fitcox (X, T); %! %! ## Against the origin rather than the mean. The coefficients do not %! ## change with the baseline; only what the ratio is relative to does %! mdl.Baseline %! hazardratio (mdl, X(1:3,:)) %! hazardratio (mdl, X(1:3,:), 'Baseline', 0) %!demo %! ## 3. What a coefficient means %! %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = fitcox (X, T); %! %! ## Raising one predictor by one unit multiplies the hazard by exp (beta) %! x0 = mdl.Baseline; %! x1 = x0 + [1, 0]; %! [hazardratio(mdl, x1), exp(mdl.Coefficients.Beta(1))] %!demo %! ## 4. A stratified model needs to be told the stratum %! %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! S = [1; 1; 1; 1; 1; 2; 2; 2; 2; 2]; %! mdl = fitcox (X, T, 'Stratification', S); %! %! ## Each stratum is centred on its own baseline, so the same predictor %! ## values give a different ratio in each %! mdl.Baseline %! [hazardratio(mdl, X(1,:), 1), hazardratio(mdl, X(1,:), 2)] statistics-release-2.1.2/inst/demos/CoxModel.linhyptest000066400000000000000000000023461527311104300232510ustar00rootroot00000000000000%!demo %! ## 1. Sequential tests on the coefficients %! %! X = [2 0 1; 5 1 3; 3 0 2; 8 1 5; 4 0 4; 7 1 6; 6 0 8; 9 1 7; 5 0 9; 10 1 10]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = fitcox (X, T); %! %! ## Each row tests that the predictors it does *not* name are jointly zero, %! ## so the first row tests the whole model against no model at all %! linhyptest (mdl) %!demo %! ## 2. The last row is the last coefficient's own p-value %! %! X = [2 0 1; 5 1 3; 3 0 2; 8 1 5; 4 0 4; 7 1 6; 6 0 8; 9 1 7; 5 0 9; 10 1 10]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = fitcox (X, T); %! %! ## Its hypothesis leaves every other predictor in the model, which is what %! ## the coefficient table already reports %! tbl = linhyptest (mdl); %! [tbl.pValue(end), mdl.Coefficients.pValue(end)] %!demo %! ## 3. Reading the sequence %! %! X = [2 0 1; 5 1 3; 3 0 2; 8 1 5; 4 0 4; 7 1 6; 6 0 8; 9 1 7; 5 0 9; 10 1 10]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = fitcox (X, T); %! %! ## The order of the predictors is the order of the test, so the table %! ## answers "does what follows add anything?" at each step. Reordering the %! ## columns of X asks a different question %! linhyptest (mdl) %! linhyptest (fitcox (X(:,[3 2 1]), T)) statistics-release-2.1.2/inst/demos/CoxModel.plotSurvival000066400000000000000000000022071527311104300235540ustar00rootroot00000000000000%!demo %! ## 1. The survival curve of an average observation %! %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = fitcox (X, T); %! %! ## The curve steps down at every event time, and nowhere else: the model %! ## learns about survival only where something happened %! plotSurvival (mdl); %!demo %! ## 2. Comparing two sets of predictor values %! %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = fitcox (X, T); %! %! ## Proportional hazards means the curves are powers of one another, so %! ## they cannot cross %! plotSurvival (mdl, [3 0; 8 1]); %! legend ({'X = [3 0]', 'X = [8 1]'}); %!demo %! ## 3. One curve per stratum %! %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! S = [1; 1; 1; 1; 1; 2; 2; 2; 2; 2]; %! mdl = fitcox (X, T, 'Stratification', S); %! %! ## Stratification is what to reach for when the baseline hazards differ: %! ## these two curves are under no obligation to be proportional %! plotSurvival (mdl); %! legend ({'stratum 1', 'stratum 2'}); statistics-release-2.1.2/inst/demos/CoxModel.survival000066400000000000000000000037261527311104300227240ustar00rootroot00000000000000%!demo %! ## 1. The survival function at the model's baseline %! %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = fitcox (X, T); %! %! ## Without predictor values the curve is that of an average observation %! [s, t] = survival (mdl); %! table (t, s, 'VariableNames', {'Time', 'Survival'}) %!demo %! ## 2. One curve per observation %! %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = fitcox (X, T); %! %! ## The columns follow the rows of X, all on the one grid of event times %! s = survival (mdl, X(1:3,:)); %! size (s) %! s(1:4,:) %!demo %! ## 3. Survival at times of your own %! %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = fitcox (X, T); %! %! ## The baseline survival is interpolated linearly between the event times %! ## and then raised to the hazard ratio of the given predictors %! survival (mdl, X(1,:), 'Time', [5; 12; 20]) %!demo %! ## 4. Outside the event times the extrapolation rule decides %! %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! mdl = fitcox (X, T); %! %! ## The model knows nothing before t = 4 or after t = 30. 'nearest', the %! ## default, carries the end values outward; 'none' refuses to answer %! tq = [1; 15; 40]; %! nearest = survival (mdl, 'Time', tq); %! none = survival (mdl, 'Time', tq, 'ExtrapolationMethod', 'none'); %! table (tq, nearest, none) %!demo %! ## 5. A stratified model gives each stratum its own curve %! %! X = [2 0; 5 1; 3 0; 8 1; 4 0; 7 1; 6 0; 9 1; 5 0; 10 1]; %! T = [4; 6; 8; 11; 13; 16; 18; 21; 25; 30]; %! S = [1; 1; 1; 1; 1; 2; 2; 2; 2; 2]; %! mdl = fitcox (X, T, 'Stratification', S); %! %! ## Each stratum carries its own baseline hazard, so the curves are %! ## returned separately, on their own event times %! [s, t] = survival (mdl); %! [t{1}, s{1}] %! [t{2}, s{2}] statistics-release-2.1.2/inst/demos/LinearModel.addTerms000066400000000000000000000046531527311104300232750ustar00rootroot00000000000000%!demo %! %! ## Wilkinson-notation shorthand, and the bare-power hierarchy rule. %! ## Ten students' exam scores depend on study hours and hours of sleep. %! ## `'x1*x2'` adds both main effects and their interaction in a single %! ## call; a bare power term such as `'x2^2'` pulls in `x2` itself %! ## alongside `x2^2` if it is not already present, following the same %! ## hierarchy convention used throughout the package. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Sleep = [5;6;5;7;6;8;7;6;8;7]; %! Score = [50;54;56;62;64;70;71;73;79;78]; %! mdl = fitlm ([Hours, Sleep], Score); %! mdl.CoefficientNames %! %! ## Adding the interaction between two existing main effects. %! mdl_int = addTerms (mdl, 'x1:x2'); %! mdl_int.CoefficientNames %! %! ## Adding a bare power term. %! mdl_sq = addTerms (mdl, 'x2^2'); %! mdl_sq.CoefficientNames %!demo %! %! ## Terms already in the model are silently skipped; specifying only %! ## terms that already exist issues a warning and returns the model %! ## unchanged. %! ## Reusing the same study-hours-and-sleep model, mixing an existing term %! ## with a new one adds only the new one; asking to add nothing but an %! ## existing term does nothing at all, beyond the warning. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Sleep = [5;6;5;7;6;8;7;6;8;7]; %! Score = [50;54;56;62;64;70;71;73;79;78]; %! mdl = fitlm ([Hours, Sleep], Score); %! %! ## x1 is already in the model, so only the interaction is actually added. %! mdl_mix = addTerms (mdl, 'x1 + x1:x2'); %! mdl_mix.CoefficientNames %! %! ## Every term requested is already present: a warning, model unchanged. %! mdl_same = addTerms (mdl, 'x1'); %! mdl_same.CoefficientNames %!demo %! %! ## A categorical predictor is added as one whole indicator group, not %! ## column by column. %! ## Nine stores in three regions report ad spend and sales. Starting %! ## from a model that only uses `AdSpend`, adding `Region` brings in %! ## both of its indicator columns together in a single call. %! AdSpend = [10;20;30;15;25;35;12;22;32]; %! Region = {'North';'North';'North';'South';'South';'South'; ... %! 'East';'East';'East'}; %! Sales = [15;18;24;20;27;33;12;19;26]; %! T = table (AdSpend, Region, Sales); %! mdl = fitlm (T, 'Sales ~ AdSpend', 'CategoricalVars', {'Region'}, ... %! 'PredictorVars', {'AdSpend', 'Region'}); %! mdl.CoefficientNames %! %! ## Both Region indicator columns are added together. %! mdl_region = addTerms (mdl, 'Region'); %! mdl_region.CoefficientNames statistics-release-2.1.2/inst/demos/LinearModel.coefCI000066400000000000000000000045051527311104300226560ustar00rootroot00000000000000%!demo %! %! ## Default 95% confidence intervals versus a custom significance level. %! ## Ten students' exam scores are modeled against study hours. The %! ## default call gives 95% intervals; passing `alpha` explicitly gives %! ## intervals of a different width, always centered on the same point %! ## estimate. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Score = [52;55;61;64;70;73;77;81;85;90]; %! mdl = fitlm (Hours, Score); %! %! ## Default 95% confidence intervals. %! ci95 = coefCI (mdl) %! %! ## Narrower 90% intervals, using alpha = 0.10. %! ci90 = coefCI (mdl, 0.10) %!demo %! %! ## A rank-deficient model: an aliased coefficient's interval collapses %! ## to [0, 0]. %! ## `x2` is defined as exactly twice `x1`, so the design matrix is rank %! ## deficient and the two predictors cannot be told apart. The model %! ## keeps `x1` and aliases `x2`, whose standard error is zero and whose %! ## confidence interval is reported as `[0, 0]` rather than a %! ## meaningless point estimate with no uncertainty. %! x1 = [1;2;3;4;5;6;7;8]; %! x2 = 2*x1; %! y = 3 + 1.5*x1 + 0.5*sin ((1:8)'); %! mdl = fitlm ([x1, x2], y); %! %! ## The second row, for x2, is exactly [0, 0]. %! ci = coefCI (mdl) %!demo %! %! ## Confidence intervals for a four-predictor model, at two confidence %! ## levels, using the `hald` cement data set. %! ## Four chemical percentages in cement (`ingredients`) are used to %! ## predict heat given off while hardening (`heat`). With only 13 %! ## observations and four predictors, several intervals are wide enough %! ## to include zero. %! load hald %! mdl = fitlm (ingredients, heat); %! %! ## 95% confidence intervals for all four coefficients. %! ci_95 = coefCI (mdl) %! %! ## Narrower 90% confidence intervals for comparison. %! ci_90 = coefCI (mdl, 0.10) %!demo %! %! ## coefCI on a CompactLinearModel gives the same intervals as the %! ## original model. %! ## `compact` discards the training data but keeps everything `coefCI` %! ## needs: the coefficient estimates, their standard errors, and the %! ## error degrees of freedom. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Score = [52;55;61;64;70;73;77;81;85;90]; %! mdl = fitlm (Hours, Score); %! cmdl = compact (mdl); %! %! ## Confidence intervals from the full model. %! ci_full = coefCI (mdl) %! %! ## Confidence intervals from the compact model match exactly. %! ci_compact = coefCI (cmdl) statistics-release-2.1.2/inst/demos/LinearModel.coefTest000066400000000000000000000060241527311104300233000ustar00rootroot00000000000000%!demo %! %! ## The overall model F-test, with no `H` supplied. %! ## Ten students' exam scores are modeled against study hours. %! ## `coefTest` with no arguments tests the joint null hypothesis that %! ## every coefficient except the intercept is zero -- the same test %! ## summarized in the "F-statistic vs. constant model" line at the %! ## bottom of the model display. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Score = [52;55;61;64;70;73;77;81;85;90]; %! mdl = fitlm (Hours, Score); %! %! ## The overall F-test: p-value, F-statistic, and numerator DF. %! [p, F, r] = coefTest (mdl) %! %! ## Compare against the F-statistic line printed for the model itself. %! mdl %!demo %! %! ## Jointly testing every indicator column of a categorical predictor %! ## at once. %! ## Nine stores in three regions report ad spend and sales. `Region` %! ## expands to two indicator coefficients, `Region_South` and %! ## `Region_East`; a two-row `H`, one row per indicator, tests whether %! ## both are zero together -- whether region has any effect on sales at %! ## all, as a single joint test rather than two separate ones. %! AdSpend = [10;20;30;15;25;35;12;22;32]; %! Region = {'North';'North';'North';'South';'South';'South'; ... %! 'East';'East';'East'}; %! Sales = [15;18;24;20;27;33;12;19;26]; %! T = table (AdSpend, Region, Sales); %! mdl = fitlm (T, 'Sales ~ AdSpend + Region', 'CategoricalVars', {'Region'}); %! %! ## Coefficient order: (Intercept), AdSpend, Region_South, Region_East. %! mdl.CoefficientNames %! %! ## Joint test that both region coefficients are zero. %! [p, F, r] = coefTest (mdl, [0 0 1 0; 0 0 0 1]) %!demo %! %! ## Testing a coefficient against a specific hypothesized value, not %! ## just zero, using the `H`, `C` form. %! ## Four chemical percentages in cement (`ingredients`) are used to %! ## predict heat given off while hardening (`heat`), using the `hald` %! ## data set. Rather than testing whether the coefficient for `x1` is %! ## zero, `H` picks out that coefficient and `C` supplies the %! ## hypothesized value 1.5 to test it against instead. %! load hald %! mdl = fitlm (ingredients, heat); %! %! ## Coefficient order: (Intercept), x1, x2, x3, x4. %! mdl.CoefficientNames %! %! ## Test whether the coefficient for x1 equals 1.5. %! [p, F, r] = coefTest (mdl, [0 1 0 0 0], 1.5) %!demo %! %! ## coefTest on a CompactLinearModel gives the same results as the %! ## original model, for both the overall test and a specific `H`. %! ## `compact` discards the training data but keeps everything %! ## `coefTest` needs: the coefficient estimates, their covariance, and %! ## the error degrees of freedom. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Score = [52;55;61;64;70;73;77;81;85;90]; %! mdl = fitlm (Hours, Score); %! cmdl = compact (mdl); %! %! ## The overall F-test matches exactly. %! [p_full, F_full, r_full] = coefTest (mdl) %! [p_compact, F_compact, r_compact] = coefTest (cmdl) %! %! ## A specific single-coefficient test also matches exactly. %! [p_full2, F_full2] = coefTest (mdl, [0 1]) %! [p_compact2, F_compact2] = coefTest (cmdl, [0 1]) statistics-release-2.1.2/inst/demos/LinearModel.dwtest000066400000000000000000000055271527311104300230450ustar00rootroot00000000000000%!demo %! %! ## Basic usage: the Durbin-Watson statistic and its p-value. %! ## Fifteen measurements are taken over time, with a small oscillation %! ## layered on top of an otherwise linear trend. The DW statistic comes %! ## out close to but above 2, and the default two-sided p-value gives no %! ## strong evidence of autocorrelation at the usual 0.05 threshold. %! Time = (1:15)'; %! Value = 10 + 0.5*Time + 0.4*sin (Time*2.1); %! mdl = fitlm (Time, Value); %! %! ## The p-value and the DW statistic itself. %! [p, DW] = dwtest (mdl) %!demo %! %! ## Comparing the 'exact' and 'approximate' methods, and confirming the %! ## one-sided tails sum to 1. %! ## The DW statistic itself never depends on `method`; only the p-value %! ## computation changes. The right-tailed and left-tailed p-values %! ## always add up to exactly 1, and the two-sided p-value equals twice %! ## the smaller of the two. %! Time = (1:15)'; %! Value = 10 + 0.5*Time + 0.4*sin (Time*2.1); %! mdl = fitlm (Time, Value); %! %! ## The exact and normal-approximation methods give close but distinct %! ## p-values, for the same DW statistic. %! [p_exact, DW] = dwtest (mdl, 'exact') %! [p_approx, DW] = dwtest (mdl, 'approximate') %! %! ## The two one-sided tails always sum to 1. %! p_right = dwtest (mdl, 'exact', 'right') %! p_left = dwtest (mdl, 'exact', 'left') %! sum_tails = p_right + p_left %!demo %! %! ## Detecting autocorrelation introduced by the natural row order of a %! ## real data set. %! ## Fitting fuel economy against weight in the `carsmall` data set gives %! ## a DW statistic well below 2 and a tiny p-value, since cars from the %! ## same manufacturer or model year tend to sit near each other in the %! ## table and share similar residuals -- autocorrelation can show up %! ## from any meaningful row ordering, not only from a time series. %! load carsmall %! mdl = fitlm (Weight, MPG); %! %! ## A DW statistic well below 2, with a highly significant p-value. %! [p, DW] = dwtest (mdl) %!demo %! %! ## Distinguishing positive from negative autocorrelation. %! ## Two series share the same linear trend but oscillate at different %! ## speeds. The fast oscillation makes neighbouring residuals swing to %! ## opposite signs, which `dwtest` reports as negative autocorrelation %! ## (DW pushed above 2); the slow oscillation makes neighbouring %! ## residuals stay similar for long stretches, which reads as positive %! ## autocorrelation instead (DW pushed toward 0). Both are strongly %! ## significant, just in opposite directions. %! x = (1:20)'; %! %! ## Fast oscillation: negative autocorrelation (DW above 2). %! y_fast = 5 + 2*x + 3*sin (x*2.3); %! mdl_fast = fitlm (x, y_fast); %! [p_fast, DW_fast] = dwtest (mdl_fast) %! %! ## Slow oscillation: positive autocorrelation (DW near 0). %! y_slow = 5 + 2*x + 4*sin (x*0.3); %! mdl_slow = fitlm (x, y_slow); %! [p_slow, DW_slow] = dwtest (mdl_slow) statistics-release-2.1.2/inst/demos/LinearModel.feval000066400000000000000000000065751527311104300226340ustar00rootroot00000000000000%!demo %! %! ## The matrix form, the separate-argument form, and broadcasting a %! ## scalar against a vector. %! ## Ten students' exam scores depend on both study hours and hours of %! ## sleep. `feval` accepts predictors either as one matrix, one column %! ## per predictor, or as separate arguments in the same order; a scalar %! ## argument is broadcast against any non-scalar ones, so a single sleep %! ## value can be paired with several study-hour values at once. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Sleep = [5;6;5;7;6;8;7;6;8;7]; %! Score = [50;54;56;62;64;70;71;73;79;78]; %! mdl = fitlm ([Hours, Sleep], Score); %! %! ## Predictor data as a single matrix, one column per predictor. %! ypred_matrix = feval (mdl, [6 7; 10 8]) %! %! ## The same points, as separate arguments instead. %! ypred_sep = feval (mdl, [6;10], [7;8]) %! %! ## Three study-hour values, all paired with the same 7 hours of sleep. %! ypred_broadcast = feval (mdl, [4;6;8], 7) %!demo %! %! ## Passing a categorical predictor's level directly as a plain string, %! ## no table required. %! ## Nine stores in three regions report ad spend and sales. In the %! ## separate-argument form, a categorical predictor's value can be given %! ## as its level name directly, giving the same answer as building a %! ## one-row table by hand. %! AdSpend = [10;20;30;15;25;35;12;22;32]; %! Region = {'North';'North';'North';'South';'South';'South'; ... %! 'East';'East';'East'}; %! Sales = [15;18;24;20;27;33;12;19;26]; %! T = table (AdSpend, Region, Sales); %! mdl = fitlm (T, 'Sales ~ AdSpend + Region', 'CategoricalVars', {'Region'}); %! %! ## A one-row table with AdSpend 20 and Region 'South'. %! ypred_table = feval (mdl, table (20, {'South'}, ... %! 'VariableNames', {'AdSpend','Region'})) %! %! ## The same prediction, passing the level name directly. %! ypred_sep = feval (mdl, 20, 'South') %!demo %! %! ## A fitted model behaves like an ordinary function handle, so it can be %! ## passed straight into routines such as `fminsearch`. %! ## A quadratic model of fuel economy against vehicle weight is fit on %! ## the `carsmall` data set. Wrapping `feval` in an anonymous function %! ## lets `fminsearch` search for the weight at which the model predicts %! ## exactly 25 MPG, without ever calling `predict` directly. %! load carsmall %! X = Weight; %! y = MPG; %! mdl = fitlm (X, y, 'quadratic'); %! target = 25; %! %! ## Search for the weight giving a predicted fuel economy of 25 MPG. %! wopt = fminsearch (@(w) (feval (mdl, w) - target)^2, 3000) %! %! ## Confirm the prediction at that weight. %! mpg_at_wopt = feval (mdl, wopt) %!demo %! %! ## `feval` on a CompactLinearModel gives identical results to the %! ## original model, in both calling forms. %! ## Reusing the study-hours-and-sleep model, `compact` discards the %! ## training data but keeps everything `feval` needs, so both the matrix %! ## form and the separate-argument form still work exactly as before. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Sleep = [5;6;5;7;6;8;7;6;8;7]; %! Score = [50;54;56;62;64;70;71;73;79;78]; %! mdl = fitlm ([Hours, Sleep], Score); %! cmdl = compact (mdl); %! %! ## Matrix form: full model versus compact model. %! ypred_full = feval (mdl, [6 7; 10 8]) %! ypred_compact = feval (cmdl, [6 7; 10 8]) %! %! ## Separate-argument form: full model versus compact model. %! ypred_full_sep = feval (mdl, [6;10], [7;8]) %! ypred_compact_sep = feval (cmdl, [6;10], [7;8]) statistics-release-2.1.2/inst/demos/LinearModel.plotAdjustedResponse000066400000000000000000000036551527311104300257140ustar00rootroot00000000000000%!demo %! %! ## The adjusted response for one predictor, with the other predictor %! ## averaged out. %! ## Ten observations depend on study hours and hours of sleep. %! ## `plotAdjustedResponse` shows the fitted response as a function of %! ## `x1` alone, with `x2` held at its average observed value across the %! ## fit, alongside the actual adjusted data points (fitted value plus %! ## residual) for comparison. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Sleep = [5;6;5;7;6;8;7;6;8;7]; %! Score = [50;54;56;62;64;70;71;73;79;78]; %! mdl = fitlm ([Hours, Sleep], Score); %! %! ## Adjusted response for x1, with x2 averaged out. %! plotAdjustedResponse (mdl, 'x1'); %!demo %! %! ## For a categorical predictor, the adjusted response function is %! ## evaluated at each level instead of a continuous grid. %! ## Nine stores in three regions report ad spend and sales. The adjusted %! ## response for `Region` is a step function through its three levels, %! ## with `AdSpend` averaged out. %! AdSpend = [200;350;500;220;370;520;240;390;540]; %! Region = {'North';'North';'North';'South';'South';'South'; ... %! 'East';'East';'East'}; %! RegionEffect = [0;0;0;300;300;300;700;700;700]; %! Sales = 1000 + 0.5*AdSpend + RegionEffect + 5*sin ((1:9)'); %! T = table (AdSpend, Region, Sales); %! mdl = fitlm (T, 'Sales ~ AdSpend + Region', 'CategoricalVars', {'Region'}); %! %! ## Adjusted response for Region, with AdSpend averaged out. %! plotAdjustedResponse (mdl, 'Region'); %!demo %! %! ## Isolating one predictor's adjusted response among several, using the %! ## `hald` cement data set. %! ## Four chemical percentages in cement (`ingredients`) are used to %! ## predict heat given off while hardening (`heat`). The adjusted %! ## response for the first ingredient is shown with the other three %! ## averaged out. %! load hald %! mdl = fitlm (ingredients, heat); %! %! ## Adjusted response for the first ingredient. %! plotAdjustedResponse (mdl, 'x1'); statistics-release-2.1.2/inst/demos/LinearModel.plotDiagnostics000066400000000000000000000041251527311104300246720ustar00rootroot00000000000000%!demo %! %! ## The default leverage plot, and Cook's distance -- both flagging the %! ## same unusual observation. %! ## Ten students' exam scores are modeled against study hours, with one %! ## student whose hours and score are both far outside the rest of the %! ## group. With no arguments, `plotDiagnostics` plots each observation's %! ## leverage against its row number, with a dotted reference line at %! ## the conventional threshold `2*p/n`. Passing `'cookd'` instead %! ## shows each observation's overall influence on the fitted model. %! Hours = [1;2;3;4;5;6;7;8;9;15]; %! Score = [52;55;61;64;70;73;77;81;85;130]; %! mdl = fitlm (Hours, Score); %! %! ## Default: leverage of each observation. %! plotDiagnostics (mdl); %! %! ## Cook's distance, measuring each observation's overall influence. %! plotDiagnostics (mdl, 'cookd'); %!demo %! %! ## Delete-1 scaled change in each coefficient, one line per predictor. %! ## Ten observations depend on study hours and hours of sleep, with one %! ## observation whose sleep value and score are both unusually large. %! ## `'dfbetas'` shows how much each coefficient estimate would change if %! ## that single observation were removed, scaled to be comparable across %! ## coefficients -- useful for seeing which specific predictor a given %! ## observation is most influential on. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Sleep = [5;6;5;7;6;8;7;9;8;20]; %! Score = [50;54;56;62;64;70;71;73;79;150]; %! mdl = fitlm ([Hours, Sleep], Score); %! %! ## Scaled change in each coefficient if each observation were removed. %! plotDiagnostics (mdl, 'dfbetas'); %!demo %! %! ## Delete-1 scaled change in the fitted value, using the `carsmall` %! ## data set. %! ## Fuel economy is modeled against vehicle weight. `'dffits'` shows how %! ## much each observation's own fitted value would change if that %! ## observation were removed from the fit, with a dotted reference line %! ## at the conventional threshold `2*sqrt (p/n)`. %! load carsmall %! mdl = fitlm (Weight, MPG); %! %! ## Scaled change in the fitted value if each observation were removed. %! plotDiagnostics (mdl, 'dffits'); statistics-release-2.1.2/inst/demos/LinearModel.plotEffects000066400000000000000000000046601527311104300240060ustar00rootroot00000000000000%!demo %! %! ## The main effect and 95% confidence interval for each predictor. %! ## Ten observations depend on study hours and hours of sleep. %! ## `plotEffects` shows, for each predictor, how much the fitted response %! ## changes when that predictor moves from its observed minimum to its %! ## observed maximum, with all other predictors held at their observed %! ## means. The dotted vertical line marks zero effect. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Sleep = [5;6;5;7;6;8;7;6;8;7]; %! Score = [50;54;56;62;64;70;71;73;79;78]; %! mdl = fitlm ([Hours, Sleep], Score); %! %! ## Main effect of each predictor, from its minimum to its maximum. %! plotEffects (mdl); %!demo %! %! ## A categorical predictor's effect is shown the same way, labeled by %! ## its levels rather than a numeric range. %! ## Nine stores in three regions report ad spend and sales. The %! ## categorical `Region` gets a y-axis label showing its reference and %! ## comparison levels, just as a numeric predictor's label shows its %! ## minimum and maximum. %! AdSpend = [10;20;30;15;25;35;12;22;32]; %! Region = {'North';'North';'North';'South';'South';'South'; ... %! 'East';'East';'East'}; %! Sales = [15;18;24;20;27;33;12;19;26]; %! T = table (AdSpend, Region, Sales); %! mdl = fitlm (T, 'Sales ~ AdSpend + Region', 'CategoricalVars', {'Region'}); %! %! ## Main effect of AdSpend and of moving between Region levels. %! plotEffects (mdl); %!demo %! %! ## Comparing the main effects of four predictors at once, using the %! ## `hald` cement data set. %! ## Four chemical percentages in cement (`ingredients`) are used to %! ## predict heat given off while hardening (`heat`). With four %! ## correlated predictors and only 13 observations, several confidence %! ## intervals are wide enough to cross zero. %! load hald %! mdl = fitlm (ingredients, heat); %! %! ## Main effect of each of the four ingredients. %! plotEffects (mdl); %!demo %! %! ## plotEffects on a CompactLinearModel gives the same plot as the %! ## original model. %! ## `compact` discards the training data but keeps everything %! ## `plotEffects` needs: the coefficient estimates, their covariance, %! ## and the effect contrasts computed at fit time. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Sleep = [5;6;5;7;6;8;7;6;8;7]; %! Score = [50;54;56;62;64;70;71;73;79;78]; %! mdl = fitlm ([Hours, Sleep], Score); %! cmdl = compact (mdl); %! %! ## The same plot, produced from the compact model. %! plotEffects (cmdl); statistics-release-2.1.2/inst/demos/LinearModel.plotInteraction000066400000000000000000000052741527311104300247100ustar00rootroot00000000000000%!demo %! %! ## The main effect and conditional effects of two predictors, the %! ## default 'effects' plot. %! ## Ten observations depend on study hours and hours of sleep, fit with %! ## an interaction term. For each predictor, the top marker is its main %! ## effect; the markers below it are its effect recomputed with the %! ## other predictor held fixed at a few conditioning values. When those %! ## markers do not line up vertically, the predictors interact. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Sleep = [5;6;5;7;6;8;7;6;8;7]; %! Score = [50;54;56;62;64;70;71;73;79;78]; %! mdl = fitlm ([Hours, Sleep], Score, 'y ~ x1*x2'); %! %! ## Main and conditional effects of x1 and x2. %! plotInteraction (mdl, 'x1', 'x2'); %!demo %! %! ## The 'predictions' plot type: adjusted response curves for x2, one %! ## per conditioning value of x1. %! ## Reusing the same interaction model, each curve shows how the %! ## adjusted response changes with x2, holding x1 fixed at a different %! ## value. Curves with different slopes indicate an interaction; parallel %! ## curves would indicate none. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Sleep = [5;6;5;7;6;8;7;6;8;7]; %! Score = [50;54;56;62;64;70;71;73;79;78]; %! mdl = fitlm ([Hours, Sleep], Score, 'y ~ x1*x2'); %! %! ## Adjusted response vs. x2, for three fixed values of x1. %! plotInteraction (mdl, 'x1', 'x2', 'predictions'); %!demo %! %! ## A categorical predictor's main and conditional effects, alongside a %! ## numeric one. %! ## Nine stores in three regions report ad spend and sales, fit with an %! ## interaction between AdSpend and Region. The categorical predictor's %! ## conditional effects are shown once per level rather than at a %! ## handful of numeric conditioning points. %! AdSpend = [10;20;30;15;25;35;12;22;32]; %! Region = {'North';'North';'North';'South';'South';'South'; ... %! 'East';'East';'East'}; %! Sales = [15;18;24;20;27;33;12;19;26]; %! T = table (AdSpend, Region, Sales); %! mdl = fitlm (T, 'Sales ~ AdSpend*Region', 'CategoricalVars', {'Region'}); %! %! ## Main and conditional effects of AdSpend and Region. %! plotInteraction (mdl, 'AdSpend', 'Region'); %!demo %! %! ## plotInteraction on a CompactLinearModel gives the same plot as the %! ## original model. %! ## `compact` discards the training data but keeps everything %! ## `plotInteraction` needs: the coefficient estimates, their covariance, %! ## and the interaction contrasts computed at fit time. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Sleep = [5;6;5;7;6;8;7;6;8;7]; %! Score = [50;54;56;62;64;70;71;73;79;78]; %! mdl = fitlm ([Hours, Sleep], Score, 'y ~ x1*x2'); %! cmdl = compact (mdl); %! %! ## The same plot, produced from the compact model. %! plotInteraction (cmdl, 'x1', 'x2'); statistics-release-2.1.2/inst/demos/LinearModel.plotResiduals000066400000000000000000000033641527311104300243620ustar00rootroot00000000000000%!demo %! %! ## The default histogram, and the fitted-values plot with a chosen %! ## residual type. %! ## Ten students' exam scores are modeled against study hours. With no %! ## arguments, `plotResiduals` draws a probability density histogram of %! ## the raw residuals. Passing `'fitted'` instead plots residuals %! ## against the fitted values, and `'ResidualType'` selects which kind %! ## of residual is shown -- here, studentized rather than raw. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Score = [52;55;61;64;70;73;77;81;85;90]; %! mdl = fitlm (Hours, Score); %! %! ## Default: a histogram of the raw residuals. %! plotResiduals (mdl); %! %! ## Studentized residuals against the fitted values. %! plotResiduals (mdl, 'fitted', 'ResidualType', 'studentized'); %!demo %! %! ## The lagged residual plot, useful for spotting autocorrelation. %! ## Twenty observations follow a slow oscillation layered on top of a %! ## linear trend, so consecutive residuals tend to be similar to one %! ## another. Plotting each residual against the one before it makes %! ## that pattern visible as a diagonal trend rather than a random %! ## scatter. %! x = (1:20)'; %! y = 5 + 2*x + 4*sin (x*0.3); %! mdl = fitlm (x, y); %! %! ## Each residual plotted against the residual immediately before it. %! plotResiduals (mdl, 'lagged'); %!demo %! %! ## A normal probability plot of the residuals, using the `carsmall` %! ## data set. %! ## Fuel economy is modeled against vehicle weight. The probability %! ## plot sorts the residuals and compares them against a normal %! ## reference line, making departures from normality easy to spot at %! ## either tail. %! load carsmall %! mdl = fitlm (Weight, MPG); %! %! ## Normal probability plot of the residuals. %! plotResiduals (mdl, 'probability'); statistics-release-2.1.2/inst/demos/LinearModel.predict000066400000000000000000000062421527311104300231600ustar00rootroot00000000000000%!demo %! %! ## Predicting with and without new data. %! ## Ten students study for varying numbers of hours before an exam, with %! ## their resulting scores recorded. Calling `predict` with no `Xnew` %! ## returns the fitted values for the training data itself (the same as %! ## `mdl.Fitted`); calling it with new hour values predicts scores at %! ## points not in the original sample, including one beyond the range of %! ## the data. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Score = [52;55;61;64;70;73;77;81;85;90]; %! mdl = fitlm (Hours, Score); %! %! ## Fitted values for the original training data. %! fitted = predict (mdl) %! %! ## Predictions at 5.5 hours (interpolation) and 11 hours (extrapolation). %! ypred = predict (mdl, [5.5; 11]) %!demo %! %! ## Confidence intervals versus prediction intervals, and simultaneous %! ## bounds. %! ## Twelve students' exam scores depend on both study hours and hours of %! ## sleep. The default output is a 95% confidence interval on the mean %! ## response; `'Prediction', 'observation'` widens it to account for a %! ## single future observation instead of the average; and %! ## `'Simultaneous', true` widens it further so the whole predicted %! ## surface, not just each point individually, holds at the stated %! ## confidence level. %! Hours = [1;2;3;4;5;6;7;8;9;10;11;12]; %! Sleep = [5;6;5;7;6;8;7;6;8;7;9;8]; %! Score = [50;54;56;62;64;70;71;73;79;78;85;84]; %! X = [Hours, Sleep]; %! mdl = fitlm (X, Score); %! Xnew = [6 7; 10 8]; %! %! ## Default: 95% confidence interval on the mean response. %! [ypred, yci_curve] = predict (mdl, Xnew) %! %! ## Wider prediction interval for a single new observation. %! [ypred, yci_obs] = predict (mdl, Xnew, 'Prediction', 'observation') %! %! ## 90% simultaneous confidence band over both points at once. %! [ypred, yci_sim] = predict (mdl, Xnew, 'Alpha', 0.1, 'Simultaneous', true) %!demo %! %! ## Predicting fuel economy from a quadratic model of vehicle weight, %! ## using the `carsmall` data set. %! ## `fitlm` fits `MPG` as a quadratic function of `Weight`, and %! ## `predict` computes the fitted curve at every original weight value, %! ## showing how well the quadratic term captures the curvature that a %! ## straight line would miss. %! load carsmall %! X = Weight; %! y = MPG; %! mdl = fitlm (X, y, 'quadratic'); %! %! ## Predicted fuel economy for the first five cars in the data set. %! ypred = predict (mdl, X); %! ypred(1:5) %!demo %! %! ## Predicting from a CompactLinearModel gives the same answer as the %! ## original model, at a fraction of the memory footprint. %! ## Eight months of advertising spend and sales are used to fit a simple %! ## model. `compact` discards the training data but keeps everything %! ## `predict` needs; unlike `LinearModel`, `Xnew` cannot be omitted here, %! ## since a compact model has no training data to fall back on. %! AdSpend = [10;20;30;40;50;60;70;80]; %! Sales = [15;18;24;27;33;36;42;45]; %! mdl = fitlm (AdSpend, Sales); %! cmdl = compact (mdl); %! Xnew = [25; 55]; %! %! ## Predictions from the full model. %! [ypred_full, yci_full] = predict (mdl, Xnew) %! %! ## Predictions from the compact model match exactly. %! [ypred_compact, yci_compact] = predict (cmdl, Xnew) statistics-release-2.1.2/inst/demos/LinearModel.random000066400000000000000000000053221527311104300230040ustar00rootroot00000000000000%!demo %! %! ## Simulating noisy responses versus the deterministic fitted value. %! ## Seven days' temperatures and ice-cream sales are used to fit a %! ## simple model. `predict` gives the deterministic fitted value at each %! ## new temperature; `random` adds independent Gaussian noise scaled by %! ## the model's mean squared error, giving a plausible simulated %! ## observation instead. %! Temp = [60;65;70;75;80;85;90]; %! Sales = [200;230;260;300;340;370;410]; %! mdl = fitlm (Temp, Sales); %! Xnew = [72; 88]; %! %! ## The deterministic fitted value, with no noise. %! ypred = predict (mdl, Xnew) %! %! ## A simulated observation at the same points, with noise added. %! ysim = random (mdl, Xnew) %!demo %! %! ## Reproducible simulated responses by seeding the generator that %! ## `random` actually draws from. %! ## `random` adds its noise with `randn`, so reproducibility comes from %! ## seeding `randn` itself, not `rand`. Resetting to the same state %! ## before each call gives back the exact same simulated values. %! rng (42); %! randg ('state', 42); %! Temp = [60;65;70;75;80;85;90]; %! Sales = [200;230;260;300;340;370;410]; %! mdl = fitlm (Temp, Sales); %! Xnew = [72; 88]; %! %! ## Two calls from the same seed give identical simulated responses. %! ysimA = random (mdl, Xnew); %! ysimB = random (mdl, Xnew); %! same_seed_matches = isequal (ysimA, ysimB) %!demo %! %! ## Simulating noisy fuel-economy readings from a quadratic model of %! ## vehicle weight, using the `carsmall` data set. %! ## The deterministic quadratic fit gives a smooth predicted curve; %! ## `random` adds noise scaled by the model's own MSE to produce %! ## plausible individual readings a new car of that weight might show. %! rng (42); %! randg ('state', 42); %! load carsmall %! X = Weight; %! y = MPG; %! mdl = fitlm (X, y, 'quadratic'); %! Xsub = X(1:5); %! %! ## Deterministic predictions for the first five cars. %! ypred = predict (mdl, Xsub) %! %! ## Simulated readings for the same five cars, with noise added. %! ysim = random (mdl, Xsub) %!demo %! %! ## `random` on a CompactLinearModel gives the same simulated draw as %! ## the original model, given the same seed. %! ## `compact` discards the training data but keeps everything `random` %! ## needs; reseeding identically before each call confirms the compact %! ## model simulates exactly the same values as the original. %! rng (42); %! randg ('state', 42); %! Temp = [60;65;70;75;80;85;90]; %! Sales = [200;230;260;300;340;370;410]; %! mdl = fitlm (Temp, Sales); %! cmdl = compact (mdl); %! Xnew = [72; 88]; %! %! ## Same seed, same simulated draw from either model. %! ysim_full = random (mdl, Xnew); %! ysim_compact = random (cmdl, Xnew); %! compact_matches_full = isequal (ysim_full, ysim_compact) statistics-release-2.1.2/inst/demos/LinearModel.removeTerms000066400000000000000000000035611527311104300240370ustar00rootroot00000000000000%!demo %! %! ## The star operator removes main effects together with their %! ## interaction; terms already absent are silently skipped, and %! ## specifying only absent terms warns and leaves the model unchanged. %! ## Ten students' exam scores depend on study hours and hours of sleep. %! ## `'x1*x2'` removes `x1`, `x2`, and `x1:x2` together in one call. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Sleep = [5;6;5;7;6;8;7;6;8;7]; %! Score = [50;54;56;62;64;70;71;73;79;78]; %! mdl_full = fitlm ([Hours, Sleep], Score, 'y ~ x1*x2'); %! mdl_full.CoefficientNames %! %! ## Removing both main effects and their interaction in one call. %! mdl_empty = removeTerms (mdl_full, 'x1*x2'); %! mdl_empty.CoefficientNames %! %! ## Starting over from an additive model with no interaction term. %! mdl_add = fitlm ([Hours, Sleep], Score); %! mdl_add.CoefficientNames %! %! ## x1 is removed; x1:x2 was never in the model, so it is silently skipped. %! mdl_mix = removeTerms (mdl_add, 'x1 + x1:x2'); %! mdl_mix.CoefficientNames %! %! ## Requesting only a term that is not in the model: a warning, unchanged. %! mdl_same = removeTerms (mdl_add, 'x1:x2'); %! mdl_same.CoefficientNames %!demo %! %! ## A categorical predictor is removed as one whole indicator group, not %! ## column by column. %! ## Nine stores in three regions report ad spend and sales. Removing %! ## `Region` drops both of its indicator columns together in a single %! ## call. %! AdSpend = [10;20;30;15;25;35;12;22;32]; %! Region = {'North';'North';'North';'South';'South';'South'; ... %! 'East';'East';'East'}; %! Sales = [15;18;24;20;27;33;12;19;26]; %! T = table (AdSpend, Region, Sales); %! mdl = fitlm (T, 'Sales ~ AdSpend + Region', 'CategoricalVars', {'Region'}); %! mdl.CoefficientNames %! %! ## Both Region indicator columns are removed together. %! mdl_no_region = removeTerms (mdl, 'Region'); %! mdl_no_region.CoefficientNames statistics-release-2.1.2/inst/demos/LinearModel.step000066400000000000000000000033631527311104300225020ustar00rootroot00000000000000%!demo %! %! ## A single default step, improving a starting model by one term. %! ## Ten observations depend on study hours and hours of sleep. Starting %! ## from a model containing only `x1`, `step` examines whether adding or %! ## removing a single term improves the fit and, by default, takes at %! ## most one such step. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Sleep = [5;6;5;7;6;8;7;6;8;7]; %! Score = [50;54;56;62;64;70;71;73;79;78]; %! mdl0 = fitlm ([Hours, Sleep], Score, 'y ~ x1'); %! mdl0.CoefficientNames %! %! ## One improvement step, starting from x1 alone. %! mdl1 = step (mdl0); %! mdl1.CoefficientNames %!demo %! %! ## Taking several steps at once with `NSteps`, and following the trace %! ## with `Verbose`. %! ## Reusing the same predictors, starting from a constant model this %! ## time, `NSteps` lets `step` keep improving the model over more than %! ## one call, up to the given upper bound. %! Hours = [1;2;3;4;5;6;7;8;9;10]; %! Sleep = [5;6;5;7;6;8;7;6;8;7]; %! Score = [50;54;56;62;64;70;71;73;79;78]; %! mdl0 = fitlm ([Hours, Sleep], Score, 'y ~ 1'); %! %! ## Up to three steps, bounded above by the full interaction model. %! mdl = step (mdl0, 'Upper', 'y ~ x1*x2', 'NSteps', 3, 'Verbose', 1); %! mdl.CoefficientNames %!demo %! %! ## Starting from a constant model on the `hald` cement data set. %! ## Four chemical percentages in cement (`ingredients`) are used to %! ## predict heat given off while hardening (`heat`). Starting from an %! ## intercept-only model, `step` adds the two most useful ingredients %! ## over several steps. %! load hald %! mdl0 = fitlm (ingredients, heat, 'y ~ 1'); %! %! ## Up to four steps, bounded above by the additive model. %! mdl = step (mdl0, 'Upper', 'linear', 'NSteps', 4, 'Verbose', 1); %! mdl.CoefficientNames statistics-release-2.1.2/inst/demos/RegressionGAM.loss000066400000000000000000000024151527311104300227560ustar00rootroot00000000000000%!demo %! ## 1. The mean squared error of the model on its own training data %! %! load fisheriris %! X = meas(:,1:3); %! Y = meas(:,4); %! mdl = fitrgam (X, Y); %! %! ## resubLoss asks the same question without handing the data back in %! [loss(mdl, X, Y), resubLoss(mdl)] %!demo %! ## 2. Interaction terms lower the loss on this data %! %! load fisheriris %! X = meas(:,1:3); %! Y = meas(:,4); %! %! ## 'all' fits every pairwise term on top of the additive ones %! [resubLoss(fitrgam (X, Y)), resubLoss(fitrgam (X, Y, 'Interactions', 'all'))] %!demo %! ## 3. A loss function of your own, taking the truth, the fit and the weights %! %! load fisheriris %! X = meas(:,1:3); %! Y = meas(:,4); %! mdl = fitrgam (X, Y); %! %! ## Mean absolute error instead of mean squared error %! mae = @(y, yfit, w) sum (w .* abs (y - yfit)); %! [loss(mdl, X, Y), loss(mdl, X, Y, 'LossFun', mae)] %!demo %! ## 4. Scoring a table %! %! load fisheriris %! T = table (meas(:,2), meas(:,3), meas(:,4), meas(:,1), ... %! 'VariableNames', {'SW', 'PL', 'PW', 'SL'}); %! mdl = fitrgam (T, 'SL'); %! %! ## The response is named by its column, left out, when it is the variable %! ## the model was fitted on, or given beside the predictors %! [loss(mdl, T, 'SL'), loss(mdl, T), loss(mdl, T(:, 1:3), T.SL)] statistics-release-2.1.2/inst/demos/RepeatedMeasuresModel.multcompare000066400000000000000000000017441527311104300261040ustar00rootroot00000000000000%!demo %! ## 1. Which species differ %! %! load fisheriris %! t = table (species, meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'species', 'meas1', 'meas2', 'meas3', 'meas4'}); %! rm = fitrm (t, 'meas1-meas4 ~ species'); %! %! ## Every ordered pair of species, on the mean of the four measurements, %! ## with Tukey-Kramer p-values and simultaneous limits %! multcompare (rm, 'species') %!demo %! ## 2. Comparisons within each group %! %! load fisheriris %! t = table (species, meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'species', 'meas1', 'meas2', 'meas3', 'meas4'}); %! Meas = table ([1, 2, 3, 4]', 'VariableNames', {'Measurements'}); %! rm = fitrm (t, 'meas1-meas4 ~ species', 'WithinDesign', Meas); %! %! ## `By` repeats the comparison of the measurements for each species, %! ## here with Bonferroni's adjustment %! multcompare (rm, 'Measurements', 'By', 'species', ... %! 'ComparisonType', 'bonferroni') statistics-release-2.1.2/inst/demos/RepeatedMeasuresModel.plot000066400000000000000000000014031527311104300245220ustar00rootroot00000000000000%!demo %! ## 1. Every subject's measurements %! %! load fisheriris %! t = table (species, meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'species', 'meas1', 'meas2', 'meas3', 'meas4'}); %! rm = fitrm (t, 'meas1-meas4 ~ species'); %! %! ## One line per flower through its four measurements %! plot (rm); %!demo %! ## 2. Lines coloured by group %! %! load fisheriris %! t = table (species, meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'species', 'meas1', 'meas2', 'meas3', 'meas4'}); %! rm = fitrm (t, 'meas1-meas4 ~ species'); %! %! ## `Group` colours the lines by species, so a profile that one species %! ## follows and the others do not stands out %! plot (rm, 'Group', 'species', 'Marker', 'none'); statistics-release-2.1.2/inst/demos/RepeatedMeasuresModel.plotprofile000066400000000000000000000016501527311104300261070ustar00rootroot00000000000000%!demo %! ## 1. The marginal means of a between-subject factor %! %! load fisheriris %! t = table (species, meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'species', 'meas1', 'meas2', 'meas3', 'meas4'}); %! rm = fitrm (t, 'meas1-meas4 ~ species'); %! %! ## The mean over the four measurements, one point per species %! plotprofile (rm, 'species'); %!demo %! ## 2. A profile over the measurements, one line per group %! %! load fisheriris %! t = table (species, meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'species', 'meas1', 'meas2', 'meas3', 'meas4'}); %! Meas = table ([1, 2, 3, 4]', 'VariableNames', {'Measurements'}); %! rm = fitrm (t, 'meas1-meas4 ~ species', 'WithinDesign', Meas); %! %! ## Lines that are not parallel are what `ranova` reports as the %! ## interaction of species with the measurements %! plotprofile (rm, 'Measurements', 'Group', 'species'); statistics-release-2.1.2/inst/demos/RepeatedMeasuresModel.ranova000066400000000000000000000021411527311104300250320ustar00rootroot00000000000000%!demo %! ## 1. Whether the measurements differ, and whether species changes that %! %! load fisheriris %! t = table (species, meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'species', 'meas1', 'meas2', 'meas3', 'meas4'}); %! Meas = table ([1, 2, 3, 4]', 'VariableNames', {'Measurements'}); %! rm = fitrm (t, 'meas1-meas4 ~ species', 'WithinDesign', Meas); %! %! ## The corrected p-values sit beside the uncorrected ones; `mauchly` %! ## says whether the correction is needed and `epsilon` how large it is %! ranova (rm) %! mauchly (rm) %! epsilon (rm) %!demo %! ## 2. Linear, quadratic and cubic trends over a numeric factor %! %! load fisheriris %! t = table (species, meas(:,1), meas(:,2), meas(:,3), meas(:,4), ... %! 'VariableNames', {'species', 'meas1', 'meas2', 'meas3', 'meas4'}); %! Meas = table ([1, 2, 3, 4]', 'VariableNames', {'Measurements'}); %! rm = fitrm (t, 'meas1-meas4 ~ species', 'WithinDesign', Meas); %! %! ## One block of rows per polynomial degree, the between-subject tests of %! ## the mean first %! ranova (rm, 'WithinModel', 'orthogonalcontrasts') statistics-release-2.1.2/inst/demos/cvpartition.summary000066400000000000000000000042021527311104300233640ustar00rootroot00000000000000%!demo %! ## 1. Basic Usage %! %! ## Create simple numeric labels %! labels = [ones(10, 1); 2 * ones(10, 1)]; %! c = cvpartition (labels, 'KFold', 2); %! summary (c) %!demo %! ## 2. Grouped K-Fold Partition %! %! rng (42); %! Region = repelem ({'North'; 'South'; 'East'; 'West'}, [20; 15; 15; 20]); %! Success = repelem ({'Success'; 'Fail'}, [49; 21]); %! Success = Success(randperm (70)); %! Tbl = table (Region, Success); %! %! ## Create Grouped Partition using the 'Region' variable %! c_group = cvpartition (height (Tbl), 'KFold', 4, 'GroupingVariables', Tbl.Region); %! %! ## Generate Summary %! summary (c_group) %!demo %! ## 3. Stratified K-Fold Partition %! %! rng (42); %! Success = repelem ({'ClassA'; 'ClassB'}, [25; 25]); %! Success = Success(randperm (50)); %! Tbl = table (Success); %! %! ## Create Stratified Partition %! c_strat = cvpartition (Tbl.Success, 'KFold', 5); %! %! ## Generate Summary %! summary (c_strat) %!demo %! ## 4. Handling Missing Values (NaN) in Stratification %! %! ## Create data with missing values (NaN) %! labels = [ones(10, 1); 2 * ones(10, 1); NaN(5, 1)]; %! %! ## Create partition %! c_missing = cvpartition (labels, 'KFold', 2); %! %! ## Generate Summary %! summary (c_missing) %!demo %! ## 5. Filtering Summary %! %! rng (42); %! Success = repelem ({'Yes'; 'No'}, [20; 10]); %! Success = Success(randperm (30)); %! %! ## Create Partition and Summary %! c_strat = cvpartition (Success, 'KFold', 3); %! summaryStrat = summary (c_strat); %! %! ## A. Filtering by Exact Set Name %! summaryTest1 = summaryStrat (strcmp (summaryStrat.Set, 'test1'), :) %! %! ## B. Filtering by Partial Match %! is_test = ! cellfun ('isempty', strfind (cellstr (summaryStrat.Set), 'test')); %! testSummaryTbl = summaryStrat (is_test, :) %!demo %! ## 6. Unstacking %! %! rng (42); %! Success = repelem ({'Yes'; 'No'}, [20; 10]); %! Success = Success(randperm (30)); %! %! ## Create Partition and Summary %! c_strat = cvpartition (Success, 'KFold', 3); %! summaryStrat = summary (c_strat); %! %! ## Pivot 'StratificationLabel' (Yes/No) into new columns %! speciesSummaryTbl = unstack (summaryStrat(:,1:4), 'StratificationCount', 'StratificationLabel') statistics-release-2.1.2/inst/demos/lime.fit000066400000000000000000000034611527311104300210430ustar00rootroot00000000000000%!demo %! ## 1. Explaining one prediction with a simple model %! %! load fisheriris %! mdl = fitrtree (meas(:,2:4), meas(:,1)); %! ex = lime (mdl, 'NumSyntheticData', 2000); %! %! ## The explainer draws its observations when it is built, and explains %! ## nothing until a query point is given %! ex = fit (ex, meas(1,2:4), 2); %! %! ## The two predictors the simple model was fitted on, and its weights %! ex.ImportantPredictors' %! ex.SimpleModel.Beta' %!demo %! ## 2. Reading whether the explanation is worth trusting %! %! load fisheriris %! mdl = fitrtree (meas(:,2:4), meas(:,1)); %! ex = fit (lime (mdl, 'NumSyntheticData', 2000), meas(1,2:4), 3); %! %! ## A simple model that answers near what the explained model answered is %! ## worth reading; one that does not explains nothing %! [ex.BlackboxFitted, ex.SimpleModelFitted] %!demo %! ## 3. Drawing near the query point rather than over everything %! %! load fisheriris %! mdl = fitrtree (meas(:,2:4), meas(:,1)); %! %! ## `'local'` fits the draw to the neighbours of the query point, so the %! ## simple model sees the shape of the model there rather than everywhere %! ex = lime (mdl, 'DataLocality', 'local', 'NumNeighbors', 30, ... %! 'NumSyntheticData', 2000, 'QueryPoint', meas(1,2:4), ... %! 'NumImportantPredictors', 2); %! ex.ImportantPredictors' %! ex.SimpleModel.Beta' %!demo %! ## 4. A tree in place of the weighted sum %! %! load fisheriris %! mdl = fitcknn (meas, species); %! ex = lime (mdl, 'NumSyntheticData', 2000); %! %! ## A classifier is explained one class at a time: the simple model %! ## separates the class the explained model predicted from every other, %! ## answering 1 for it and -1 for the rest %! ex = fit (ex, meas(1,:), 2, 'SimpleModelType', 'tree'); %! class (ex.SimpleModel) %! ex.BlackboxFitted %! ex.SimpleModelFitted statistics-release-2.1.2/inst/demos/lime.plot000066400000000000000000000020411527311104300212300ustar00rootroot00000000000000%!demo %! ## 1. What the simple model says about the query point %! %! load fisheriris %! mdl = fitrtree (meas(:,2:4), meas(:,1)); %! ex = fit (lime (mdl, 'NumSyntheticData', 2000), meas(1,2:4), 3); %! %! ## One bar per predictor the simple model was fitted on, holding its %! ## weight, so a long bar is a predictor the prediction turns on here %! plot (ex); %!demo %! ## 2. A narrower explanation %! %! load fisheriris %! mdl = fitrtree (meas(:,2:4), meas(:,1)); %! %! ## Asking for fewer predictors gives a simple model that is easier to %! ## read and further from the one it explains %! ex = fit (lime (mdl, 'NumSyntheticData', 2000), meas(1,2:4), 1); %! plot (ex); %!demo %! ## 3. A tree is drawn by what it splits on %! %! load fisheriris %! mdl = fitrtree (meas(:,2:4), meas(:,1)); %! ex = fit (lime (mdl, 'NumSyntheticData', 2000), meas(1,2:4), 2, ... %! 'SimpleModelType', 'tree'); %! %! ## A tree has no weights to show, so the bars hold predictor importance %! ## and the chart is titled after what the simple model is %! plot (ex); statistics-release-2.1.2/inst/demos/prob.BetaDistribution.a000066400000000000000000000007571527311104300237740ustar00rootroot00000000000000%!demo %! ## Create a beta distribution with default parameters %! pd = makedist ("Beta") %! %! ## Query parameter 'alpha' (first shape parameter) %! pd.a %! %! ## Set parameter 'alpha' %! pd.a = 2 %! %! ## Use this to initialize or modify the first shape parameter of a beta %! ## distribution. The parameter 'alpha' must be a positive real scalar. %!demo %! ## Create a beta distribution object by calling its constructor %! pd = BetaDistribution (2, 3) %! ## Query parameter 'alpha' %! pd.a statistics-release-2.1.2/inst/demos/prob.BetaDistribution.b000066400000000000000000000007601527311104300237670ustar00rootroot00000000000000%!demo %! ## Create a beta distribution with default parameters %! pd = makedist ("Beta") %! %! ## Query parameter 'beta' (second shape parameter) %! pd.b %! %! ## Set parameter 'beta' %! pd.b = 2 %! %! ## Use this to initialize or modify the second shape parameter of a beta %! ## distribution. The parameter 'beta' must be a positive real scalar. %!demo %! ## Create a beta distribution object by calling its constructor %! pd = BetaDistribution (2, 3) %! %! ## Query parameter 'beta' %! pd.b statistics-release-2.1.2/inst/demos/prob.BetaDistribution.cdf000066400000000000000000000013041527311104300242750ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Beta distribution %! x = 0:0.01:1; %! pd1 = makedist ("Beta", "a", 0.5, "b", 0.5); %! pd2 = makedist ("Beta", "a", 2, "b", 2); %! pd3 = makedist ("Beta", "a", 5, "b", 2); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"a = 0.5, b = 0.5", "a = 2, b = 2", "a = 5, b = 2"}, ... %! "location", "southeast") %! title ("Beta CDF") %! xlabel ("Value") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different beta distributions, showing how probability accumulates %! ## over the interval [0, 1]. statistics-release-2.1.2/inst/demos/prob.BetaDistribution.icdf000066400000000000000000000012401527311104300244450ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Beta distribution %! p = 0.001:0.001:0.999; %! pd1 = makedist ("Beta", "a", 0.5, "b", 0.5); %! pd2 = makedist ("Beta", "a", 2, "b", 2); %! pd3 = makedist ("Beta", "a", 5, "b", 2); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"a = 0.5, b = 0.5", "a = 2, b = 2", "a = 5, b = 2"}, ... %! "location", "northwest") %! title ("Beta iCDF") %! xlabel ("Probability") %! ylabel ("Value") %! %! ## This demonstrates the inverse CDF (quantiles) for beta distributions, %! ## useful for finding values corresponding to given probabilities. statistics-release-2.1.2/inst/demos/prob.BetaDistribution.iqr000066400000000000000000000004131527311104300243340ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Beta distribution %! pd = makedist ("Beta", "a", 2, "b", 5) %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the beta distribution. statistics-release-2.1.2/inst/demos/prob.BetaDistribution.mean000066400000000000000000000005301527311104300244610ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Beta distributions %! pd1 = makedist ("Beta", "a", 1, "b", 1); %! pd2 = makedist ("Beta", "a", 2, "b", 5); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value of beta distributions with %! ## different shape parameters, representing the average outcome. statistics-release-2.1.2/inst/demos/prob.BetaDistribution.median000066400000000000000000000005051527311104300250000ustar00rootroot00000000000000%!demo %! ## Compute the median for different Beta distributions %! pd1 = makedist ("Beta", "a", 1, "b", 1); %! pd2 = makedist ("Beta", "a", 2, "b", 5); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which splits the beta distribution %! ## into two equal probability halves. statistics-release-2.1.2/inst/demos/prob.BetaDistribution.negloglik000066400000000000000000000006541527311104300255230ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Beta distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Beta", "a", 2, "b", 5) %! data = random (pd, 100, 1); %! pd_fitted = fitdist (data, "Beta") %! nlogL = negloglik (pd_fitted) %! %! ## This is useful for assessing the fit of a beta distribution to data, %! ## lower values indicate a better fit. statistics-release-2.1.2/inst/demos/prob.BetaDistribution.paramci000066400000000000000000000007311527311104300251600ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Beta distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Beta", "a", 2, "b", 5) %! data = random (pd, 1000, 1); %! pd_fitted = fitdist (data, "Beta") %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameters (a, b), %! ## providing a range of plausible values given the data. statistics-release-2.1.2/inst/demos/prob.BetaDistribution.pdf000066400000000000000000000012171527311104300243150ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Beta distribution %! x = 0:0.01:1; %! pd1 = makedist ("Beta", "a", 0.5, "b", 0.5); %! pd2 = makedist ("Beta", "a", 2, "b", 2); %! pd3 = makedist ("Beta", "a", 5, "b", 2); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"a = 0.5, b = 0.5", "a = 2, b = 2", "a = 5, b = 2"}, ... %! "location", "north") %! title ("Beta PDF") %! xlabel ("Value") %! ylabel ("Probability Density") %! %! ## This visualizes the probability density function for beta distributions, %! ## showing the likelihood of different values in [0, 1]. statistics-release-2.1.2/inst/demos/prob.BetaDistribution.plot000066400000000000000000000030051527311104300245170ustar00rootroot00000000000000%!demo %! ## Create a Beta distribution with fixed parameters a = 2 and b = 5, and %! ## plot its PDF. %! %! pd = makedist ("Beta", "a", 2, "b", 5) %! plot (pd) %! title ("Fixed Beta distribution with a = 2 and b = 5") %!demo %! ## Generate a data set of 100 random samples from a Beta distribution with %! ## parameters a = 2 and b = 4. Fit a Beta distribution to this data and plot %! ## its CDF superimposed over an empirical CDF of the data %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("Beta", "a", 2, "b", 4) %! data = random (pd_fixed, 100, 1); %! pd_fitted = fitdist (data, "Beta") %! plot (pd_fitted, "plottype", "cdf") %! txt = "Fitted Beta distribution with a = %0.2f and b = %0.2f"; %! title (sprintf (txt, pd_fitted.a, pd_fitted.b)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "east") %!demo %! ## Generate a data set of 200 random samples from a Beta distribution with %! ## parameters a = 2 and b = 4. Display a probability plot for the Beta %! ## distribution fit to the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("Beta", "a", 2, "b", 4) %! data = random (pd_fixed, 200, 1); %! pd_fitted = fitdist (data, "Beta") %! plot (pd_fitted, "plottype", "probability") %! txt = "Probability plot of a fitted Beta distribution with a = %0.2f and b = %0.2f"; %! title (sprintf (txt, pd_fitted.a, pd_fitted.b)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") statistics-release-2.1.2/inst/demos/prob.BetaDistribution.proflik000066400000000000000000000010151527311104300252060ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the first shape parameter of %! ## a fitted Beta distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Beta", "a", 2, "b", 5) %! data = random (pd, 1000, 1); %! pd_fitted = fitdist (data, "Beta") %! [nlogL, param] = proflik (pd_fitted, 1, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the shape parameter (a), %! ## helping to understand the uncertainty in parameter estimates. statistics-release-2.1.2/inst/demos/prob.BetaDistribution.random000066400000000000000000000007401527311104300250240ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Beta distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Beta", "a", 2, "b", 5) %! samples = random (pd, 500, 1); %! hist (samples, 20) %! title ("Histogram of 500 random samples from Beta(a=2, b=5)") %! xlabel ("Value") %! ylabel ("Frequency") %! %! ## This generates random samples from a beta distribution, useful for %! ## simulating data within the [0, 1] interval. statistics-release-2.1.2/inst/demos/prob.BetaDistribution.std000066400000000000000000000003741527311104300243410ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Beta distribution %! pd = makedist ("Beta", "a", 2, "b", 5) %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the %! ## variability of the beta distribution. statistics-release-2.1.2/inst/demos/prob.BetaDistribution.truncate000066400000000000000000000011101527311104300253610ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Beta distribution, with parameters a = 2 and b = 4, %! ## truncated at [0.1, 0.8] intervals. Generate 10000 random samples from %! ## this truncated distribution and superimpose a histogram with 100 bins %! ## scaled accordingly %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Beta", "a", 2, "b", 4) %! t = truncate (pd, 0.1, 0.8) %! data = random (t, 10000, 1); %! plot (t) %! title ("Beta distribution (a = 2, b = 4) truncated at [0.1, 0.8]") %! hold on %! hist (data, 100, 140) %! hold off statistics-release-2.1.2/inst/demos/prob.BetaDistribution.var000066400000000000000000000003451527311104300243350ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Beta distribution %! pd = makedist ("Beta", "a", 2, "b", 5) %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## beta distribution. statistics-release-2.1.2/inst/demos/prob.BinomialDistribution.N000066400000000000000000000012071527311104300246170ustar00rootroot00000000000000%!demo %! ## Create a binomial distribution with default parameters %! pd = makedist ("Binomial") %! %! ## Query parameter 'N' (number of trials) %! pd.N %! %! ## Set parameter 'N' %! pd.N = 10 %! %! ## Use this to initialize or modify the number of trials in a binomial %! ## distribution. The number of trials must be a positive integer. %!demo %! ## Create a binomial distribution object by calling its constructor %! pd = BinomialDistribution (10, 0.3) %! %! ## Query parameter 'N' %! pd.N %! %! ## This demonstrates direct construction with specific parameters, useful %! ## for defining a binomial distribution with known number of trials. statistics-release-2.1.2/inst/demos/prob.BinomialDistribution.cdf000066400000000000000000000013221527311104300251540ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Binomial distribution %! x = 0:10; %! pd1 = makedist ("Binomial", "N", 10, "p", 0.2); %! pd2 = makedist ("Binomial", "N", 10, "p", 0.5); %! pd3 = makedist ("Binomial", "N", 10, "p", 0.8); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "*b", x, p2, "*g", x, p3, "*r") %! grid on %! legend ({"N = 10, p = 0.2", "N = 10, p = 0.5", "N = 10, p = 0.8"}, ... %! "location", "southeast") %! title ("Binomial CDF") %! xlabel ("Number of successes") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different binomial distributions, showing how probability accumulates. statistics-release-2.1.2/inst/demos/prob.BinomialDistribution.icdf000066400000000000000000000013501527311104300253260ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Binomial distribution %! p = 0.001:0.001:0.999; %! pd1 = makedist ("Binomial", "N", 10, "p", 0.2); %! pd2 = makedist ("Binomial", "N", 10, "p", 0.5); %! pd3 = makedist ("Binomial", "N", 10, "p", 0.8); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, ".b", p, x2, ".g", p, x3, ".r") %! grid on %! legend ({"N = 10, p = 0.2", "N = 10, p = 0.5", "N = 10, p = 0.8"}, ... %! "location", "northwest") %! title ("Binomial iCDF") %! xlabel ("Probability") %! ylabel ("Number of successes") %! %! ## This demonstrates the inverse CDF (quantiles) for binomial distributions, %! ## useful for finding the number of successes corresponding to given %! ## probabilities. statistics-release-2.1.2/inst/demos/prob.BinomialDistribution.iqr000066400000000000000000000004211527311104300252120ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Binomial distribution %! pd = makedist ("Binomial", "N", 20, "p", 0.3) %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution. statistics-release-2.1.2/inst/demos/prob.BinomialDistribution.mean000066400000000000000000000005251527311104300253440ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Binomial distributions %! pd1 = makedist ("Binomial", "N", 10, "p", 0.2); %! pd2 = makedist ("Binomial", "N", 10, "p", 0.5); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected number of successes for binomial %! ## distributions with different parameters. statistics-release-2.1.2/inst/demos/prob.BinomialDistribution.median000066400000000000000000000005401527311104300256560ustar00rootroot00000000000000%!demo %! ## Compute the median for different Binomial distributions %! pd1 = makedist ("Binomial", "N", 10, "p", 0.2); %! pd2 = makedist ("Binomial", "N", 10, "p", 0.5); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median number of successes, which splits the %! ## distribution into two equal probability halves. statistics-release-2.1.2/inst/demos/prob.BinomialDistribution.negloglik000066400000000000000000000007161527311104300264010ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Binomial distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Binomial", "N", 10, "p", 0.3) %! data = random (pd, 100, 1); %! pd_fitted = fitdist (data, "Binomial", "ntrials", 10) %! nlogL = negloglik (pd_fitted) %! %! ## This is useful for assessing the fit of a binomial distribution to data, %! ## lower values indicate a better fit. statistics-release-2.1.2/inst/demos/prob.BinomialDistribution.p000066400000000000000000000011551527311104300246630ustar00rootroot00000000000000%!demo %! ## Create a binomial distribution with default parameters %! pd = makedist ("Binomial") %! %! ## Query parameter 'p' (probability of success) %! pd.p %! %! ## Set parameter 'p' %! pd.p = 0.4 %! %! ## Use this to initialize or modify the probability of success in each %! ## trial. The probability must be between 0 and 1. %!demo %! ## Create a binomial distribution object by calling its constructor %! pd = BinomialDistribution (10, 0.3) %! %! ## Query parameter 'p' %! pd.p %! %! ## This shows how to set the success probability directly via the %! ## constructor, ideal for modeling specific success rates. statistics-release-2.1.2/inst/demos/prob.BinomialDistribution.paramci000066400000000000000000000007741527311104300260460ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Binomial %! ## distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Binomial", "N", 10, "p", 0.3) %! data = random (pd, 1000, 1); %! pd_fitted = fitdist (data, "Binomial", "ntrials", 10) %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameters, %! ## providing a range of plausible values for p given the data. statistics-release-2.1.2/inst/demos/prob.BinomialDistribution.pdf000066400000000000000000000012651527311104300251770ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Binomial distribution %! x = 0:10; %! pd1 = makedist ("Binomial", "N", 10, "p", 0.2); %! pd2 = makedist ("Binomial", "N", 10, "p", 0.5); %! pd3 = makedist ("Binomial", "N", 10, "p", 0.8); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "*b", x, y2, "*g", x, y3, "*r") %! grid on %! legend ({"N = 10, p = 0.2", "N = 10, p = 0.5", "N = 10, p = 0.8"}, ... %! "location", "north") %! title ("Binomial PDF") %! xlabel ("Number of successes") %! ylabel ("Probability") %! %! ## This visualizes the probability mass function for binomial distributions, %! ## showing the likelihood of different numbers of successes. statistics-release-2.1.2/inst/demos/prob.BinomialDistribution.plot000066400000000000000000000035561527311104300254110ustar00rootroot00000000000000%!demo %! ## Create a Binomial distribution with fixed parameters N = 20 and p = 0.4, %! ## and plot its PDF. %! %! pd = makedist ("Binomial", "N", 20, "p", 0.4) %! plot (pd) %! title ("Fixed Binomial distribution with N = 20 and p = 0.4") %!demo %! ## Generate a data set of 100 random samples from a Binomial distribution %! ## with parameters N = 10 and p = 0.3. Fit a Binomial distribution to this %! ## data and plot its CDF superimposed over an empirical CDF of the data %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Binomial", "N", 10, "p", 0.3) %! data = random (pd, 100, 1); %! pd = fitdist (data, "Binomial", "ntrials", 10) %! plot (pd, "PlotType", "cdf", "Discrete", true) %! title (sprintf ("Fitted Binomial distribution with N = %d and p = %0.2f", ... %! pd.N, pd.p)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of %! ## the data, useful for assessing model fit. %!demo %! ## Generate a data set of 200 random samples from a Binomial distribution %! ## with parameters N = 10 and p = 0.3. Display a probability plot for the %! ## Binomial distribution fit to the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Binomial", "N", 10, "p", 0.3) %! data = random (pd, 200, 1); %! pd = fitdist (data, "Binomial", "ntrials", 10) %! plot (pd, "PlotType", "probability", "Discrete", true) %! title (sprintf (["Probability plot of fitted Binomial distribution with " ... %! "N = %d and p = %0.2f"], pd.N, pd.p)); %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast"); %! %! ## This creates a probability plot to compare the fitted distribution to the %! ## data, useful for checking if the binomial model is appropriate. statistics-release-2.1.2/inst/demos/prob.BinomialDistribution.proflik000066400000000000000000000010571527311104300260730ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the probability parameter of %! ## a fitted Binomial distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Binomial", "N", 10, "p", 0.3) %! data = random (pd, 1000, 1); %! pd_fitted = fitdist (data, "Binomial", "ntrials", 10) %! [nlogL, param] = proflik (pd_fitted, 2, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the success probability %! ## (p), helping to understand the uncertainty in parameter estimates. statistics-release-2.1.2/inst/demos/prob.BinomialDistribution.random000066400000000000000000000010561527311104300257040ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Binomial distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Binomial", "N", 10, "p", 0.3) %! samples = random (pd, 100, 1); %! hist (samples, 0:10) %! title ("Histogram of 100 random samples from Binomial(N=10, p=0.3)") %! xlabel ("Number of successes") %! ylabel ("Frequency") %! %! ## This generates random samples from a binomial distribution, useful for %! ## simulating experiments with a fixed number of trials and success %! ## probability. statistics-release-2.1.2/inst/demos/prob.BinomialDistribution.std000066400000000000000000000004111527311104300252100ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Binomial distribution %! pd = makedist ("Binomial", "N", 20, "p", 0.3) %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the %! ## variability in the number of successes. statistics-release-2.1.2/inst/demos/prob.BinomialDistribution.truncate000066400000000000000000000023041527311104300262460ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Binomial distribution, with parameters N = 10 and %! ## p = 0.3, truncated at [2, 8] intervals. Generate 10000 random samples %! ## from this truncated distribution and superimpose a histogram scaled %! ## accordingly %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Binomial", "N", 10, "p", 0.3) %! t = truncate (pd, 2, 8) %! data = random (t, 10000, 1); %! %! ## Histogram data for range 2 to 8 %! edges = 1.5:1:8.5; %! centers = 2:8; %! counts = histc (data, edges); %! counts = counts(1:end-1); # Remove extra edge bin %! probs = counts / sum(counts); # Normalize to get probabilities %! %! ## Plot histogram bars %! bar (centers, probs, 0.5, "facecolor", [0.6 0.6 1]); %! hold on; %! %! ## PMF of the truncated distribution %! pmf = pdf (t, centers); %! plot (centers, pmf, 'r-', "linewidth", 2); %! %! title ("Binomial distribution (N = 10, p = 0.3) truncated at [2, 8]"); %! xlabel ("x"); %! ylabel ("Probability"); %! legend ("Histogram", "Truncated PMF"); %! hold off %! %! ## This demonstrates truncating a binomial distribution to a specific range %! ## and visualizing the resulting distribution with random samples. statistics-release-2.1.2/inst/demos/prob.BinomialDistribution.var000066400000000000000000000004061527311104300252120ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Binomial distribution %! pd = makedist ("Binomial", "N", 20, "p", 0.3) %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## number of successes in the distribution. statistics-release-2.1.2/inst/demos/prob.BirnbaumSaundersDistribution.beta000066400000000000000000000013001527311104300270410ustar00rootroot00000000000000%!demo %! ## Create a Birnbaum-Saunders distribution with default parameters %! pd = makedist ("BirnbaumSaunders") %! %! ## Query parameter 'beta' (scale parameter) %! pd.beta %! %! ## Set parameter 'beta' %! pd.beta = 2 %! %! ## Use this to initialize or modify the scale parameter of a Birnbaum-Saunders %! ## distribution. The scale parameter must be a positive real scalar. %!demo %! ## Create a Birnbaum-Saunders distribution object by calling its constructor %! pd = BirnbaumSaundersDistribution (1.5, 0.5) %! %! ## Query parameter 'beta' %! pd.beta %! %! ## This demonstrates direct construction with a specific scale parameter, %! ## useful for modeling time-to-failure data with a known scale. statistics-release-2.1.2/inst/demos/prob.BirnbaumSaundersDistribution.cdf000066400000000000000000000015031527311104300266670ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Birnbaum-Saunders distribution %! x = 0:0.01:5; %! pd1 = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.2); %! pd2 = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.5); %! pd3 = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.8); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"beta = 1, gamma = 0.2", "beta = 1, gamma = 0.5", "beta = 1, gamma = 0.8"}, ... %! "location", "southeast") %! title ("Birnbaum-Saunders CDF") %! xlabel ("Time to failure") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Birnbaum-Saunders distributions, showing how probability %! ## accumulates over time-to-failure. statistics-release-2.1.2/inst/demos/prob.BirnbaumSaundersDistribution.gamma000066400000000000000000000013061527311104300272160ustar00rootroot00000000000000%!demo %! ## Create a Birnbaum-Saunders distribution with default parameters %! pd = makedist ("BirnbaumSaunders") %! %! ## Query parameter 'gamma' (shape parameter) %! pd.gamma %! %! ## Set parameter 'gamma' %! pd.gamma = 0.8 %! %! ## Use this to initialize or modify the shape parameter in a Birnbaum-Saunders %! ## distribution. The shape parameter must be a positive real scalar. %!demo %! ## Create a Birnbaum-Saunders distribution object by calling its constructor %! pd = BirnbaumSaundersDistribution (1.5, 0.5) %! %! ## Query parameter 'gamma' %! pd.gamma %! %! ## This shows how to set the shape parameter directly via the constructor, %! ## ideal for modeling specific variability in failure times. statistics-release-2.1.2/inst/demos/prob.BirnbaumSaundersDistribution.icdf000066400000000000000000000014661527311104300270500ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Birnbaum-Saunders distribution %! p = 0.001:0.001:0.999; %! pd1 = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.2); %! pd2 = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.5); %! pd3 = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.8); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"beta = 1, gamma = 0.2", "beta = 1, gamma = 0.5", "beta = 1, gamma = 0.8"}, ... %! "location", "northwest") %! title ("Birnbaum-Saunders iCDF") %! xlabel ("Probability") %! ylabel ("Time to failure") %! %! ## This demonstrates the inverse CDF (quantiles) for Birnbaum-Saunders %! ## distributions, useful for finding the time-to-failure corresponding to %! ## given probabilities. statistics-release-2.1.2/inst/demos/prob.BirnbaumSaundersDistribution.iqr000066400000000000000000000005451527311104300267330ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Birnbaum-Saunders distribution %! pd = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.5) %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, useful for understanding variability %! ## in failure times. statistics-release-2.1.2/inst/demos/prob.BirnbaumSaundersDistribution.mean000066400000000000000000000006051527311104300270550ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Birnbaum-Saunders distributions %! pd1 = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.2); %! pd2 = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.5); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected time to failure for Birnbaum-Saunders %! ## distributions with different shape parameters. statistics-release-2.1.2/inst/demos/prob.BirnbaumSaundersDistribution.median000066400000000000000000000006011527311104300273660ustar00rootroot00000000000000%!demo %! ## Compute the median for different Birnbaum-Saunders distributions %! pd1 = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.2); %! pd2 = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.5); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median time to failure, which splits the distribution %! ## into two equal probability halves. statistics-release-2.1.2/inst/demos/prob.BirnbaumSaundersDistribution.negloglik000066400000000000000000000007471527311104300301170ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Birnbaum-Saunders distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.5) %! data = random (pd, 100, 1); %! pd_fitted = fitdist (data, "BirnbaumSaunders") %! nlogL = negloglik (pd_fitted) %! %! ## This is useful for assessing the fit of a Birnbaum-Saunders distribution to %! ## data, lower values indicate a better fit. statistics-release-2.1.2/inst/demos/prob.BirnbaumSaundersDistribution.paramci000066400000000000000000000010271527311104300275500ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Birnbaum-Saunders %! ## distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.5) %! data = random (pd, 1000, 1); %! pd_fitted = fitdist (data, "BirnbaumSaunders") %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameters (beta %! ## and gamma), providing a range of plausible values given the data. statistics-release-2.1.2/inst/demos/prob.BirnbaumSaundersDistribution.pdf000066400000000000000000000014261527311104300267100ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Birnbaum-Saunders distribution %! x = 0:0.01:5; %! pd1 = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.2); %! pd2 = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.5); %! pd3 = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.8); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"beta = 1, gamma = 0.2", "beta = 1, gamma = 0.5", "beta = 1, gamma = 0.8"}, ... %! "location", "northeast") %! title ("Birnbaum-Saunders PDF") %! xlabel ("Time to failure") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Birnbaum-Saunders %! ## distributions, showing the likelihood of different times to failure. statistics-release-2.1.2/inst/demos/prob.BirnbaumSaundersDistribution.plot000066400000000000000000000040511527311104300271120ustar00rootroot00000000000000%!demo %! ## Create a Birnbaum-Saunders distribution with fixed parameters β = 1 and %! ## γ = 0.5 and plot its PDF. %! %! pd = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.5) %! plot (pd) %! title ("Fixed Birnbaum-Saunders distribution with beta = 1 and gamma = 0.5") %!demo %! ## Generate a data set of 100 random samples from a Birnbaum-Saunders %! ## distribution with parameters β = 1 and γ = 0.5. Fit a Birnbaum-Saunders %! ## distribution to this data and plot its CDF superimposed over an empirical %! ## CDF. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.5) %! data = random (pd_fixed, 100, 1); %! pd_fitted = fitdist (data, "BirnbaumSaunders") %! plot (pd_fitted, "PlotType", "cdf") %! txt = "Fitted Birnbaum-Saunders distribution with β = %0.2f and γ = %0.2f"; %! title (sprintf (txt, pd_fitted.beta, pd_fitted.gamma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit. %!demo %! ## Generate a data set of 200 random samples from a Birnbaum-Saunders %! ## distribution with parameters β = 1 and γ = 0.5. Display a probability %! ## plot for the Birnbaum-Saunders distribution fit to the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.5) %! data = random (pd_fixed, 200, 1); %! pd_fitted = fitdist (data, "BirnbaumSaunders") %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted Birnbaum-Saunders", ... %! " distribution with β = %0.2f and γ = %0.2f"); %! title (sprintf (txt, pd_fitted.beta, pd_fitted.gamma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted distribution to the %! ## data, useful for checking if the Birnbaum-Saunders model is appropriate. statistics-release-2.1.2/inst/demos/prob.BirnbaumSaundersDistribution.proflik000066400000000000000000000010711527311104300276010ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the shape parameter of a fitted %! ## Birnbaum-Saunders distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.5) %! data = random (pd, 1000, 1); %! pd_fitted = fitdist (data, "BirnbaumSaunders") %! [nlogL, param] = proflik (pd_fitted, 2, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the shape parameter (gamma), %! ## helping to understand the uncertainty in parameter estimates. statistics-release-2.1.2/inst/demos/prob.BirnbaumSaundersDistribution.random000066400000000000000000000010711527311104300274130ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Birnbaum-Saunders distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.5) %! samples = random (pd, 500, 1); %! hist (samples, 50) %! title ("Histogram of 500 random samples from Birnbaum-Saunders(beta=1, gamma=0.5)") %! xlabel ("Time to failure") %! ylabel ("Frequency") %! %! ## This generates random samples from a Birnbaum-Saunders distribution, useful %! ## for simulating time-to-failure data under cyclic loading. statistics-release-2.1.2/inst/demos/prob.BirnbaumSaundersDistribution.std000066400000000000000000000004301527311104300267230ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Birnbaum-Saunders distribution %! pd = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.5) %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## in time to failure. statistics-release-2.1.2/inst/demos/prob.BirnbaumSaundersDistribution.truncate000066400000000000000000000015441527311104300277650ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Birnbaum-Saunders distribution, with parameters beta = 1 %! ## and gamma = 0.5, truncated at [0.5, 2] intervals. Generate 10000 random %! ## samples from this truncated distribution and superimpose a histogram scaled %! ## accordingly %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.5) %! t = truncate (pd, 0.5, 2) %! data = random (t, 10000, 1); %! %! ## Plot histogram and fitted PDF %! plot (t) %! hold on %! hist (data, 100, 50) %! hold off %! title ("Birnbaum-Saunders distribution (beta = 1, gamma = 0.5) truncated at [0.5, 2]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a Birnbaum-Saunders distribution to a specific %! ## range and visualizing the resulting distribution with random samples. statistics-release-2.1.2/inst/demos/prob.BirnbaumSaundersDistribution.var000066400000000000000000000004311527311104300267220ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Birnbaum-Saunders distribution %! pd = makedist ("BirnbaumSaunders", "beta", 1, "gamma", 0.5) %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## time to failure in the distribution. statistics-release-2.1.2/inst/demos/prob.BurrDistribution.alpha000066400000000000000000000012071527311104300246670ustar00rootroot00000000000000%!demo %! ## Create a Burr distribution with default parameters %! pd = makedist ("Burr") %! %! ## Query parameter 'alpha' (scale parameter) %! pd.alpha %! %! ## Set parameter 'alpha' %! pd.alpha = 2 %! %! ## Use this to initialize or modify the scale parameter of a Burr %! ## distribution. The scale parameter must be a positive real scalar. %!demo %! ## Create a Burr distribution object by calling its constructor %! pd = BurrDistribution (1.5, 2, 1) %! %! ## Query parameter 'alpha' %! pd.alpha %! %! ## This demonstrates direct construction with a specific scale parameter, %! ## ideal for modeling specific household income distributions. statistics-release-2.1.2/inst/demos/prob.BurrDistribution.c000066400000000000000000000012261527311104300240250ustar00rootroot00000000000000%!demo %! ## Create a Burr distribution with default parameters %! pd = makedist ("Burr") %! %! ## Query parameter 'c' (first shape parameter) %! pd.c %! %! ## Set parameter 'c' %! pd.c = 3 %! %! ## Use this to initialize or modify the first shape parameter in a Burr %! ## distribution. The first shape parameter must be a positive real scalar. %!demo %! ## Create a Burr distribution object by calling its constructor %! pd = BurrDistribution (1, 3, 1) %! %! ## Query parameter 'c' %! pd.c %! %! ## This shows how to set the first shape parameter directly via the constructor, %! ## useful for modeling variability in non-negative data like household income. statistics-release-2.1.2/inst/demos/prob.BurrDistribution.cdf000066400000000000000000000014101527311104300243320ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Burr distribution %! x = 0:0.01:5; %! pd1 = makedist ("Burr", "alpha", 1, "c", 2, "k", 1); %! pd2 = makedist ("Burr", "alpha", 1, "c", 3, "k", 1); %! pd3 = makedist ("Burr", "alpha", 1, "c", 4, "k", 1); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"alpha=1, c=2, k=1", "alpha=1, c=3, k=1", "alpha=1, c=4, k=1"}, ... %! "location", "southeast") %! title ("Burr CDF") %! xlabel ("values in x") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Burr distributions, showing how probability accumulates %! ## over non-negative values like income levels. statistics-release-2.1.2/inst/demos/prob.BurrDistribution.icdf000066400000000000000000000013571527311104300245150ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Burr distribution %! p = 0.001:0.001:0.999; %! pd1 = makedist ("Burr", "alpha", 1, "c", 2, "k", 1); %! pd2 = makedist ("Burr", "alpha", 1, "c", 3, "k", 1); %! pd3 = makedist ("Burr", "alpha", 1, "c", 4, "k", 1); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"alpha=1, c=2, k=1", "alpha=1, c=3, k=1", "alpha=1, c=4, k=1"}, ... %! "location", "northwest") %! title ("Burr iCDF") %! xlabel ("Probability") %! ylabel ("values in x") %! %! ## This demonstrates the inverse CDF (quantiles) for Burr distributions, %! ## useful for finding the value (e.g., income level) corresponding to given %! ## probabilities. statistics-release-2.1.2/inst/demos/prob.BurrDistribution.iqr000066400000000000000000000005341527311104300243770ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Burr distribution %! pd = makedist ("Burr", "alpha", 1, "c", 2, "k", 1) %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, useful for understanding variability %! ## in data like household income. statistics-release-2.1.2/inst/demos/prob.BurrDistribution.k000066400000000000000000000012261527311104300240350ustar00rootroot00000000000000%!demo %! ## Create a Burr distribution with default parameters %! pd = makedist ("Burr") %! %! ## Query parameter 'k' (second shape parameter) %! pd.k %! %! ## Set parameter 'k' %! pd.k = 2 %! %! ## Use this to initialize or modify the second shape parameter in a Burr %! ## distribution. The second shape parameter must be a positive real scalar. %!demo %! ## Create a Burr distribution object by calling its constructor %! pd = BurrDistribution (1, 2, 2) %! %! ## Query parameter 'k' %! pd.k %! %! ## This shows how to set the second shape parameter directly via the constructor, %! ## ideal for tailoring the tail behavior in distributions for income data. statistics-release-2.1.2/inst/demos/prob.BurrDistribution.mean000066400000000000000000000006311527311104300245220ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Burr distributions %! pd1 = makedist ("Burr", "alpha", 1, "c", 2, "k", 1); %! pd2 = makedist ("Burr", "alpha", 1, "c", 3, "k", 1); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Burr distributions with %! ## different shape parameters, provided the parameters allow a finite mean %! ## (e.g., when c * k > 1). statistics-release-2.1.2/inst/demos/prob.BurrDistribution.median000066400000000000000000000005671527311104300250470ustar00rootroot00000000000000%!demo %! ## Compute the median for different Burr distributions %! pd1 = makedist ("Burr", "alpha", 1, "c", 2, "k", 1); %! pd2 = makedist ("Burr", "alpha", 1, "c", 3, "k", 1); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which splits the distribution into %! ## two equal probability halves, e.g., median household income. statistics-release-2.1.2/inst/demos/prob.BurrDistribution.negloglik000066400000000000000000000007231527311104300255570ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Burr distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Burr", "alpha", 1, "c", 2, "k", 1) %! data = random (pd, 100, 1); %! pd_fitted = fitdist (data, "Burr") %! nlogL = negloglik (pd_fitted) %! %! ## This is useful for assessing the fit of a Burr distribution to data; %! ## lower values indicate a better fit, e.g., for income modeling. statistics-release-2.1.2/inst/demos/prob.BurrDistribution.paramci000066400000000000000000000010531527311104300252150ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Burr distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Burr", "alpha", 1, "c", 2, "k", 1) %! data = random (pd, 1000, 1); %! pd_fitted = fitdist (data, "Burr") %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameters (alpha, %! ## c, k), providing a range of plausible values given the data, e.g., for %! ## uncertainty in income distribution parameters. statistics-release-2.1.2/inst/demos/prob.BurrDistribution.pdf000066400000000000000000000013171527311104300243550ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Burr distribution %! x = 0:0.01:5; %! pd1 = makedist ("Burr", "alpha", 1, "c", 2, "k", 1); %! pd2 = makedist ("Burr", "alpha", 1, "c", 3, "k", 1); %! pd3 = makedist ("Burr", "alpha", 1, "c", 4, "k", 1); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"alpha=1, c=2, k=1", "alpha=1, c=3, k=1", "alpha=1, c=4, k=1"}, ... %! "location", "northeast") %! title ("Burr PDF") %! xlabel ("values in x") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Burr distributions, %! ## showing the likelihood of different values, e.g., income levels. statistics-release-2.1.2/inst/demos/prob.BurrDistribution.plot000066400000000000000000000037151527311104300245660ustar00rootroot00000000000000%!demo %! ## Create a Burr distribution with fixed parameters alpha = 1, c = 2, k = 1 %! ## and plot its PDF. %! %! pd = makedist ("Burr", "alpha", 1, "c", 2, "k", 1) %! plot (pd) %! title ("Fixed Burr distribution with alpha = 1, c = 2, k = 1") %!demo %! ## Generate a data set of 100 random samples from a Burr distribution with %! ## parameters alpha = 1, c = 2, k = 1. Fit a Burr distribution to this data %! ## and plot its CDF superimposed over an empirical CDF. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("Burr", "alpha", 1, "c", 2, "k", 1) %! data = random (pd_fixed, 100, 1); %! pd_fitted = fitdist (data, "Burr") %! plot (pd_fitted, "PlotType", "cdf") %! txt = "Fitted Burr distribution with alpha = %0.2f, c = %0.2f, k = %0.2f"; %! title (sprintf (txt, pd_fitted.alpha, pd_fitted.c, pd_fitted.k)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit in income data. %!demo %! ## Generate a data set of 200 random samples from a Burr distribution with %! ## parameters alpha = 1, c = 2, k = 1. Display a probability plot for the %! ## Burr distribution fit to the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("Burr", "alpha", 1, "c", 2, "k", 1) %! data = random (pd_fixed, 200, 1); %! pd_fitted = fitdist (data, "Burr") %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted Burr distribution with ", ... %! "alpha = %0.2f, c = %0.2f, k = %0.2f"); %! title (sprintf (txt, pd_fitted.alpha, pd_fitted.c, pd_fitted.k)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted distribution to the %! ## data, useful for checking if the Burr model is appropriate for the dataset. statistics-release-2.1.2/inst/demos/prob.BurrDistribution.proflik000066400000000000000000000011061527311104300252460ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the first shape parameter of a %! ## fitted Burr distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Burr", "alpha", 1, "c", 2, "k", 1) %! data = random (pd, 1000, 1); %! pd_fitted = fitdist (data, "Burr") %! [nlogL, param] = proflik (pd_fitted, 2, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the first shape parameter (c), %! ## helping to understand the uncertainty in parameter estimates for models %! ## like household income. statistics-release-2.1.2/inst/demos/prob.BurrDistribution.random000066400000000000000000000010331527311104300250570ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Burr distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Burr", "alpha", 1, "c", 2, "k", 3) %! samples = random (pd, 50, 1); %! hist (samples, 90) %! title ("Histogram of 50 random samples from Burr(alpha=1, c=2, k=3)") %! xlabel ("values in x") %! ylabel ("Frequency") %! %! ## This generates random samples from a Burr distribution, useful for %! ## simulating non-negative data like household income under specific parameters. statistics-release-2.1.2/inst/demos/prob.BurrDistribution.std000066400000000000000000000004451527311104300243770ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Burr distribution %! pd = makedist ("Burr", "alpha", 1, "c", 2, "k", 1) %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## in the distribution, e.g., spread of household income. statistics-release-2.1.2/inst/demos/prob.BurrDistribution.truncate000066400000000000000000000015361527311104300254340ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Burr distribution, with parameters alpha = 1, c = 2, k = 1, %! ## truncated at [0.5, 2.5] intervals. Generate 10000 random samples from this %! ## truncated distribution and superimpose a histogram scaled accordingly %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Burr", "alpha", 1, "c", 2, "k", 1) %! t = truncate (pd, 0.5, 2.5) %! data = random (t, 10000, 1); %! %! ## Plot histogram and fitted PDF %! plot (t) %! hold on %! hist (data, 100, 50) %! hold off %! title ("Burr distribution (alpha = 1, c = 2, k = 1) truncated at [0.5, 2.5]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a Burr distribution to a specific range and %! ## visualizing the resulting distribution with random samples, useful for %! ## bounded data scenarios. statistics-release-2.1.2/inst/demos/prob.BurrDistribution.var000066400000000000000000000004231527311104300243710ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Burr distribution %! pd = makedist ("Burr", "alpha", 1, "c", 2, "k", 1) %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## distribution, e.g., variability in household income. statistics-release-2.1.2/inst/demos/prob.ExponentialDistribution.cdf000066400000000000000000000012521527311104300257120ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Exponential distribution %! x = 0:0.01:10; %! pd1 = makedist ("Exponential", "mu", 1); %! pd2 = makedist ("Exponential", "mu", 2); %! pd3 = makedist ("Exponential", "mu", 3); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"mu = 1", "mu = 2", "mu = 3"}, "location", "southeast") %! title ("Exponential CDF") %! xlabel ("values in x") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Exponential distributions, showing how probability %! ## accumulates over values. statistics-release-2.1.2/inst/demos/prob.ExponentialDistribution.icdf000066400000000000000000000012301527311104300260570ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Exponential distribution %! p = 0.001:0.001:0.999; %! pd1 = makedist ("Exponential", "mu", 1); %! pd2 = makedist ("Exponential", "mu", 2); %! pd3 = makedist ("Exponential", "mu", 3); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"mu = 1", "mu = 2", "mu = 3"}, "location", "northwest") %! title ("Exponential iCDF") %! xlabel ("Probability") %! ylabel ("values in x") %! %! ## This demonstrates the inverse CDF (quantiles) for Exponential %! ## distributions, useful for finding values corresponding to %! ## given probabilities. statistics-release-2.1.2/inst/demos/prob.ExponentialDistribution.iqr000066400000000000000000000005131527311104300257500ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for an Exponential distribution %! pd = makedist ("Exponential", "mu", 2) %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, useful for understanding variability %! ## in waiting times. statistics-release-2.1.2/inst/demos/prob.ExponentialDistribution.mean000066400000000000000000000005041527311104300260750ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Exponential distributions %! pd1 = makedist ("Exponential", "mu", 1); %! pd2 = makedist ("Exponential", "mu", 2); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Exponential %! ## distributions with different mean parameters. statistics-release-2.1.2/inst/demos/prob.ExponentialDistribution.median000066400000000000000000000005071527311104300264150ustar00rootroot00000000000000%!demo %! ## Compute the median for different Exponential distributions %! pd1 = makedist ("Exponential", "mu", 1); %! pd2 = makedist ("Exponential", "mu", 2); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which splits the distribution %! ## into two equal probability halves. statistics-release-2.1.2/inst/demos/prob.ExponentialDistribution.mu000066400000000000000000000012141527311104300255750ustar00rootroot00000000000000%!demo %! ## Create an Exponential distribution with default parameter %! pd = makedist ("Exponential") %! %! ## Query parameter 'mu' (mean parameter) %! pd.mu %! %! ## Set parameter 'mu' %! pd.mu = 2 %! %! ## Use this to initialize or modify the mean parameter of an Exponential %! ## distribution. The mean parameter must be a positive real scalar. %!demo %! ## Create an Exponential distribution object by calling its constructor %! pd = ExponentialDistribution (1.5) %! %! ## Query parameter 'mu' %! pd.mu %! %! ## This demonstrates direct construction with a specific mean parameter, %! ## useful for modeling waiting times with a known average. statistics-release-2.1.2/inst/demos/prob.ExponentialDistribution.negloglik000066400000000000000000000007021527311104300271300ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Exponential distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Exponential", "mu", 2) %! data = random (pd, 100, 1); %! pd_fitted = fitdist (data, "Exponential") %! nlogL = negloglik (pd_fitted) %! %! ## This is useful for assessing the fit of an Exponential distribution to %! ## data, lower values indicate a better fit. statistics-release-2.1.2/inst/demos/prob.ExponentialDistribution.paramci000066400000000000000000000007521527311104300265760ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Exponential %! ## distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Exponential", "mu", 2) %! data = random (pd, 1000, 1); %! pd_fitted = fitdist (data, "Exponential") %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameter (mu), %! ## providing a range of plausible values given the data. statistics-release-2.1.2/inst/demos/prob.ExponentialDistribution.pdf000066400000000000000000000011741527311104300257320ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Exponential distribution %! x = 0:0.01:10; %! pd1 = makedist ("Exponential", "mu", 1); %! pd2 = makedist ("Exponential", "mu", 2); %! pd3 = makedist ("Exponential", "mu", 3); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"mu = 1", "mu = 2", "mu = 3"}, "location", "northeast") %! title ("Exponential PDF") %! xlabel ("values in x") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Exponential %! ## distributions, showing the likelihood of different values. statistics-release-2.1.2/inst/demos/prob.ExponentialDistribution.plot000066400000000000000000000034501527311104300261360ustar00rootroot00000000000000%!demo %! ## Create an Exponential distribution with fixed parameter mu = 2 and plot its PDF. %! %! pd = makedist ("Exponential", "mu", 2) %! plot (pd) %! title ("Fixed Exponential distribution with mu = 2") %!demo %! ## Generate a data set of 100 random samples from an Exponential %! ## distribution with parameter mu = 2. Fit an Exponential %! ## distribution to this data and plot its CDF superimposed over an empirical %! ## CDF. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("Exponential", "mu", 2) %! data = random (pd_fixed, 100, 1); %! pd_fitted = fitdist (data, "Exponential") %! plot (pd_fitted, "PlotType", "cdf") %! txt = "Fitted Exponential distribution with mu = %0.2f"; %! title (sprintf (txt, pd_fitted.mu)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit. %!demo %! ## Generate a data set of 200 random samples from an Exponential %! ## distribution with parameter mu = 2. Display a probability %! ## plot for the Exponential distribution fit to the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("Exponential", "mu", 2) %! data = random (pd_fixed, 200, 1); %! pd_fitted = fitdist (data, "Exponential") %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted Exponential", ... %! " distribution with mu = %0.2f"); %! title (sprintf (txt, pd_fitted.mu)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted distribution to the %! ## data, useful for checking if the Exponential model is appropriate. statistics-release-2.1.2/inst/demos/prob.ExponentialDistribution.proflik000066400000000000000000000010431527311104300266220ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the mean parameter of a fitted %! ## Exponential distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Exponential", "mu", 2) %! data = random (pd, 1000, 1); %! pd_fitted = fitdist (data, "Exponential") %! [nlogL, param] = proflik (pd_fitted, 1, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the mean parameter (mu), %! ## helping to understand the uncertainty in parameter estimates given the data. statistics-release-2.1.2/inst/demos/prob.ExponentialDistribution.random000066400000000000000000000007761527311104300264500ustar00rootroot00000000000000%!demo %! ## Generate random samples from an Exponential distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Exponential", "mu", 2) %! samples = random (pd, 500, 1); %! hist (samples, 50) %! title ("Histogram of 500 random samples from Exponential(mu=2)") %! xlabel ("values in x") %! ylabel ("Frequency") %! %! ## This generates random samples from an Exponential distribution, useful %! ## for simulating waiting times or inter-arrival times. statistics-release-2.1.2/inst/demos/prob.ExponentialDistribution.std000066400000000000000000000003771527311104300257570ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for an Exponential distribution %! pd = makedist ("Exponential", "mu", 2) %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## in the distribution. statistics-release-2.1.2/inst/demos/prob.ExponentialDistribution.truncate000066400000000000000000000014271527311104300270070ustar00rootroot00000000000000%!demo %! ## Plot the PDF of an Exponential distribution, with parameter mu = 2, %! ## truncated at [1, 5] intervals. Generate 10000 random %! ## samples from this truncated distribution and superimpose a histogram scaled %! ## accordingly %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Exponential", "mu", 2) %! t = truncate (pd, 1, 3) %! data = random (t, 10000, 1); %! %! ## Plot histogram and fitted PDF %! plot (t) %! hold on %! hist (data, 100, 50) %! hold off %! title ("Exponential distribution (mu = 2) truncated at [1, 5]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating an Exponential distribution to a specific %! ## range and visualizing the resulting distribution with random samples. statistics-release-2.1.2/inst/demos/prob.ExponentialDistribution.var000066400000000000000000000003501527311104300257440ustar00rootroot00000000000000%!demo %! ## Compute the variance for an Exponential distribution %! pd = makedist ("Exponential", "mu", 2) %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## distribution. statistics-release-2.1.2/inst/demos/prob.ExtremeValueDistribution.cdf000066400000000000000000000014231527311104300260320ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Extreme Value distribution %! x = -5:0.01:10; %! pd1 = makedist ("ExtremeValue", "mu", 0, "sigma", 0.5); %! pd2 = makedist ("ExtremeValue", "mu", 0, "sigma", 1); %! pd3 = makedist ("ExtremeValue", "mu", 0, "sigma", 1.5); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5", "mu = 0, sigma = 1", "mu = 0, sigma = 1.5"}, ... %! "location", "southeast") %! title ("Extreme Value CDF") %! xlabel ("Values") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Extreme Value distributions, showing how probability %! ## accumulates over extreme values. statistics-release-2.1.2/inst/demos/prob.ExtremeValueDistribution.icdf000066400000000000000000000014251527311104300262050ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Extreme Value distribution %! p = 0.001:0.001:0.999; %! pd1 = makedist ("ExtremeValue", "mu", 0, "sigma", 0.5); %! pd2 = makedist ("ExtremeValue", "mu", 0, "sigma", 1); %! pd3 = makedist ("ExtremeValue", "mu", 0, "sigma", 1.5); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5", "mu = 0, sigma = 1", "mu = 0, sigma = 1.5"}, ... %! "location", "northwest") %! title ("Extreme Value iCDF") %! xlabel ("Probability") %! ylabel ("Values") %! %! ## This demonstrates the inverse CDF (quantiles) for Extreme Value %! ## distributions, useful for finding the value corresponding to %! ## given probabilities in extreme event modeling. statistics-release-2.1.2/inst/demos/prob.ExtremeValueDistribution.iqr000066400000000000000000000005331527311104300260720ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for an Extreme Value distribution %! pd = makedist ("ExtremeValue", "mu", 0, "sigma", 1) %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, useful for understanding variability %! ## in extreme values. statistics-release-2.1.2/inst/demos/prob.ExtremeValueDistribution.mean000066400000000000000000000005451527311104300262220ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Extreme Value distributions %! pd1 = makedist ("ExtremeValue", "mu", 0, "sigma", 0.5); %! pd2 = makedist ("ExtremeValue", "mu", 0, "sigma", 1); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Extreme Value %! ## distributions with different scale parameters. statistics-release-2.1.2/inst/demos/prob.ExtremeValueDistribution.median000066400000000000000000000005451527311104300265370ustar00rootroot00000000000000%!demo %! ## Compute the median for different Extreme Value distributions %! pd1 = makedist ("ExtremeValue", "mu", 0, "sigma", 0.5); %! pd2 = makedist ("ExtremeValue", "mu", 0, "sigma", 1); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which splits the distribution %! ## into two equal probability halves. statistics-release-2.1.2/inst/demos/prob.ExtremeValueDistribution.mu000066400000000000000000000012421527311104300257160ustar00rootroot00000000000000%!demo %! ## Create an Extreme Value distribution with default parameters %! pd = makedist ("ExtremeValue") %! %! ## Query parameter 'mu' (location parameter) %! pd.mu %! %! ## Set parameter 'mu' %! pd.mu = 2 %! %! ## Use this to initialize or modify the location parameter of an Extreme Value %! ## distribution. The location parameter must be a real scalar. %!demo %! ## Create an Extreme Value distribution object by calling its constructor %! pd = ExtremeValueDistribution (1.5, 0.5) %! %! ## Query parameter 'mu' %! pd.mu %! %! ## This demonstrates direct construction with a specific location parameter, %! ## useful for modeling the position of maxima in data sets. statistics-release-2.1.2/inst/demos/prob.ExtremeValueDistribution.negloglik000066400000000000000000000007241527311104300272540ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Extreme Value distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("ExtremeValue", "mu", 0, "sigma", 1) %! data = random (pd, 100, 1); %! pd_fitted = fitdist (data, "ExtremeValue") %! nlogL = negloglik (pd_fitted) %! %! ## This is useful for assessing the fit of an Extreme Value distribution to %! ## data, lower values indicate a better fit. statistics-release-2.1.2/inst/demos/prob.ExtremeValueDistribution.paramci000066400000000000000000000010051527311104300267060ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Extreme Value %! ## distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("ExtremeValue", "mu", 0, "sigma", 1) %! data = random (pd, 1000, 1); %! pd_fitted = fitdist (data, "ExtremeValue") %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameters (mu %! ## and sigma), providing a range of plausible values given the data. statistics-release-2.1.2/inst/demos/prob.ExtremeValueDistribution.pdf000066400000000000000000000013451527311104300260520ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Extreme Value distribution %! x = -5:0.01:10; %! pd1 = makedist ("ExtremeValue", "mu", 0, "sigma", 0.5); %! pd2 = makedist ("ExtremeValue", "mu", 0, "sigma", 1); %! pd3 = makedist ("ExtremeValue", "mu", 0, "sigma", 1.5); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5", "mu = 0, sigma = 1", "mu = 0, sigma = 1.5"}, ... %! "location", "northeast") %! title ("Extreme Value PDF") %! xlabel ("Values") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Extreme Value %! ## distributions, showing the likelihood of different extreme values. statistics-release-2.1.2/inst/demos/prob.ExtremeValueDistribution.plot000066400000000000000000000037521527311104300262630ustar00rootroot00000000000000%!demo %! ## Create an Extreme Value distribution with fixed parameters mu = 0 and %! ## sigma = 1 and plot its PDF. %! %! pd = makedist ("ExtremeValue", "mu", 0, "sigma", 1) %! plot (pd) %! title ("Fixed Extreme Value distribution with mu = 0 and sigma = 1") %!demo %! ## Generate a data set of 100 random samples from an Extreme Value %! ## distribution with parameters mu = 0 and sigma = 1. Fit an Extreme Value %! ## distribution to this data and plot its CDF superimposed over an empirical %! ## CDF. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("ExtremeValue", "mu", 0, "sigma", 1) %! data = random (pd_fixed, 100, 1); %! pd_fitted = fitdist (data, "ExtremeValue") %! plot (pd_fitted, "PlotType", "cdf") %! txt = "Fitted Extreme Value distribution with mu = %0.2f and sigma = %0.2f"; %! title (sprintf (txt, pd_fitted.mu, pd_fitted.sigma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit. %!demo %! ## Generate a data set of 200 random samples from an Extreme Value %! ## distribution with parameters mu = 0 and sigma = 1. Display a probability %! ## plot for the Extreme Value distribution fit to the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("ExtremeValue", "mu", 0, "sigma", 1) %! data = random (pd_fixed, 200, 1); %! pd_fitted = fitdist (data, "ExtremeValue") %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted Extreme Value", ... %! " distribution with mu = %0.2f and sigma = %0.2f"); %! title (sprintf (txt, pd_fitted.mu, pd_fitted.sigma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted distribution to the %! ## data, useful for checking if the Extreme Value model is appropriate. statistics-release-2.1.2/inst/demos/prob.ExtremeValueDistribution.proflik000066400000000000000000000010511527311104300267410ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the scale parameter of a fitted %! ## Extreme Value distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("ExtremeValue", "mu", 0, "sigma", 1) %! data = random (pd, 1000, 1); %! pd_fitted = fitdist (data, "ExtremeValue") %! [nlogL, param] = proflik (pd_fitted, 2, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the scale parameter (sigma), %! ## helping to understand the uncertainty in parameter estimates. statistics-release-2.1.2/inst/demos/prob.ExtremeValueDistribution.random000066400000000000000000000010371527311104300265570ustar00rootroot00000000000000%!demo %! ## Generate random samples from an Extreme Value distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("ExtremeValue", "mu", 0, "sigma", 1) %! samples = random (pd, 500, 1); %! hist (samples, 50) %! title ("Histogram of 500 random samples from ExtremeValue(mu=0, sigma=1)") %! xlabel ("Values") %! ylabel ("Frequency") %! %! ## This generates random samples from an Extreme Value distribution, useful %! ## for simulating extreme events like maximum values in data sets. statistics-release-2.1.2/inst/demos/prob.ExtremeValueDistribution.sigma000066400000000000000000000012601527311104300263750ustar00rootroot00000000000000%!demo %! ## Create an Extreme Value distribution with default parameters %! pd = makedist ("ExtremeValue") %! %! ## Query parameter 'sigma' (scale parameter) %! pd.sigma %! %! ## Set parameter 'sigma' %! pd.sigma = 0.8 %! %! ## Use this to initialize or modify the scale parameter in an Extreme Value %! ## distribution. The scale parameter must be a positive real scalar. %!demo %! ## Create an Extreme Value distribution object by calling its constructor %! pd = ExtremeValueDistribution (1.5, 0.5) %! %! ## Query parameter 'sigma' %! pd.sigma %! %! ## This shows how to set the scale parameter directly via the constructor, %! ## ideal for modeling the spread in extreme value data. statistics-release-2.1.2/inst/demos/prob.ExtremeValueDistribution.std000066400000000000000000000004141527311104300260670ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for an Extreme Value distribution %! pd = makedist ("ExtremeValue", "mu", 0, "sigma", 1) %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## in extreme values. statistics-release-2.1.2/inst/demos/prob.ExtremeValueDistribution.truncate000066400000000000000000000015111527311104300271210ustar00rootroot00000000000000%!demo %! ## Plot the PDF of an Extreme Value distribution, with parameters mu = 0 %! ## and sigma = 1, truncated at [-2, 2] intervals. Generate 10000 random %! ## samples from this truncated distribution and superimpose a histogram scaled %! ## accordingly %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("ExtremeValue", "mu", 0, "sigma", 1) %! t = truncate (pd, 0.5, 4) %! data = random (t, 10000, 1); %! %! ## Plot histogram and fitted PDF %! plot (t) %! hold on %! hist (data, 100, 50) %! hold off %! title ("Extreme Value distribution (mu = 0, sigma = 1) truncated at [0.5, 4]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating an Extreme Value distribution to a specific %! ## range and visualizing the resulting distribution with random samples. statistics-release-2.1.2/inst/demos/prob.ExtremeValueDistribution.var000066400000000000000000000004151527311104300260660ustar00rootroot00000000000000%!demo %! ## Compute the variance for an Extreme Value distribution %! pd = makedist ("ExtremeValue", "mu", 0, "sigma", 1) %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## extreme values in the distribution. statistics-release-2.1.2/inst/demos/prob.GammaDistribution.a000066400000000000000000000011551527311104300241340ustar00rootroot00000000000000%!demo %! ## Create a Gamma distribution with default parameters %! pd = makedist ("Gamma") %! %! ## Query parameter 'a' (shape parameter) %! pd.a %! %! ## Set parameter 'a' %! pd.a = 2 %! %! ## Use this to initialize or modify the shape parameter of a Gamma %! ## distribution. The shape parameter must be a positive real scalar. %!demo %! ## Create a Gamma distribution object by calling its constructor %! pd = GammaDistribution (2, 1) %! %! ## Query parameter 'a' %! pd.a %! %! ## This demonstrates direct construction with a specific shape parameter, %! ## useful for modeling waiting times with known variability. statistics-release-2.1.2/inst/demos/prob.GammaDistribution.b000066400000000000000000000011531527311104300241330ustar00rootroot00000000000000%!demo %! ## Create a Gamma distribution with default parameters %! pd = makedist ("Gamma") %! %! ## Query parameter 'b' (scale parameter) %! pd.b %! %! ## Set parameter 'b' %! pd.b = 2 %! %! ## Use this to initialize or modify the scale parameter in a Gamma %! ## distribution. The scale parameter must be a positive real scalar. %!demo %! ## Create a Gamma distribution object by calling its constructor %! pd = GammaDistribution (2, 1) %! %! ## Query parameter 'b' %! pd.b %! %! ## This shows how to set the scale parameter directly via the constructor, %! ## ideal for scaling the distribution in lifetime models. statistics-release-2.1.2/inst/demos/prob.GammaDistribution.cdf000066400000000000000000000012671527311104300244540ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Gamma distribution %! x = 0:0.01:10; %! pd1 = makedist ("Gamma", "a", 1, "b", 1); %! pd2 = makedist ("Gamma", "a", 2, "b", 1); %! pd3 = makedist ("Gamma", "a", 5, "b", 1); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"a = 1, b = 1", "a = 2, b = 1", "a = 5, b = 1"}, ... %! "location", "southeast") %! title ("Gamma CDF") %! xlabel ("Values") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Gamma distributions, showing how probability accumulates %! ## over values. statistics-release-2.1.2/inst/demos/prob.GammaDistribution.icdf000066400000000000000000000012451527311104300246210ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Gamma distribution %! p = 0.001:0.001:0.999; %! pd1 = makedist ("Gamma", "a", 1, "b", 1); %! pd2 = makedist ("Gamma", "a", 2, "b", 1); %! pd3 = makedist ("Gamma", "a", 5, "b", 1); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"a = 1, b = 1", "a = 2, b = 1", "a = 5, b = 1"}, ... %! "location", "northwest") %! title ("Gamma iCDF") %! xlabel ("Probability") %! ylabel ("Values") %! %! ## This demonstrates the inverse CDF (quantiles) for Gamma %! ## distributions, useful for finding values corresponding to %! ## given probabilities. statistics-release-2.1.2/inst/demos/prob.GammaDistribution.iqr000066400000000000000000000005051527311104300245050ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Gamma distribution %! pd = makedist ("Gamma", "a", 2, "b", 1) %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, useful for understanding variability %! ## in waiting times. statistics-release-2.1.2/inst/demos/prob.GammaDistribution.mean000066400000000000000000000004731527311104300246360ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Gamma distributions %! pd1 = makedist ("Gamma", "a", 1, "b", 1); %! pd2 = makedist ("Gamma", "a", 2, "b", 1); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Gamma %! ## distributions with different shape parameters. statistics-release-2.1.2/inst/demos/prob.GammaDistribution.median000066400000000000000000000005031527311104300251450ustar00rootroot00000000000000%!demo %! ## Compute the median for different Gamma distributions %! pd1 = makedist ("Gamma", "a", 1, "b", 1); %! pd2 = makedist ("Gamma", "a", 2, "b", 1); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which splits the distribution %! ## into two equal probability halves. statistics-release-2.1.2/inst/demos/prob.GammaDistribution.negloglik000066400000000000000000000006621527311104300256710ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Gamma distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Gamma", "a", 2, "b", 1) %! data = gamrnd (2, 1, 100, 1); %! pd_fitted = fitdist (data, "Gamma") %! nlogL = negloglik (pd_fitted) %! %! ## This is useful for assessing the fit of a Gamma distribution to %! ## data, lower values indicate a better fit. statistics-release-2.1.2/inst/demos/prob.GammaDistribution.paramci000066400000000000000000000007471527311104300253360ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Gamma %! ## distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Gamma", "a", 2, "b", 1) %! data = gamrnd (2, 1, 1000, 1); %! pd_fitted = fitdist (data, "Gamma") %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameters (a %! ## and b), providing a range of plausible values given the data. statistics-release-2.1.2/inst/demos/prob.GammaDistribution.pdf000066400000000000000000000012111527311104300244560ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Gamma distribution %! x = 0:0.01:10; %! pd1 = makedist ("Gamma", "a", 1, "b", 1); %! pd2 = makedist ("Gamma", "a", 2, "b", 1); %! pd3 = makedist ("Gamma", "a", 5, "b", 1); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"a = 1, b = 1", "a = 2, b = 1", "a = 5, b = 1"}, ... %! "location", "northeast") %! title ("Gamma PDF") %! xlabel ("Values") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Gamma %! ## distributions, showing the likelihood of different values. statistics-release-2.1.2/inst/demos/prob.GammaDistribution.plot000066400000000000000000000034741527311104300247000ustar00rootroot00000000000000%!demo %! ## Create a Gamma distribution with fixed parameters a = 2 and %! ## b = 1 and plot its PDF. %! %! pd = makedist ("Gamma", "a", 2, "b", 1) %! plot (pd) %! title ("Fixed Gamma distribution with a = 2 and b = 1") %!demo %! ## Generate a data set of 100 random samples from a Gamma %! ## distribution with parameters a = 2 and b = 1. Fit a Gamma %! ## distribution to this data and plot its CDF superimposed over an empirical %! ## CDF. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("Gamma", "a", 2, "b", 1) %! data = gamrnd (2, 1, 100, 1); %! pd_fitted = fitdist (data, "Gamma") %! plot (pd_fitted, "PlotType", "cdf") %! txt = "Fitted Gamma distribution with a = %0.2f and b = %0.2f"; %! title (sprintf (txt, pd_fitted.a, pd_fitted.b)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit. %!demo %! ## Generate a data set of 200 random samples from a Gamma %! ## distribution with parameters a = 2 and b = 1. Display a probability %! ## plot for the Gamma distribution fit to the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("Gamma", "a", 2, "b", 1) %! data = gamrnd (2, 1, 200, 1); %! pd_fitted = fitdist (data, "Gamma") %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted Gamma", ... %! " distribution with a = %0.2f and b = %0.2f"); %! title (sprintf (txt, pd_fitted.a, pd_fitted.b)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted distribution to the %! ## data, useful for checking if the Gamma model is appropriate. statistics-release-2.1.2/inst/demos/prob.GammaDistribution.proflik000066400000000000000000000010141527311104300253540ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the shape parameter of a fitted %! ## Gamma distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Gamma", "a", 2, "b", 1) %! data = gamrnd (2, 1, 1000, 1); %! pd_fitted = fitdist (data, "Gamma") %! [nlogL, param] = proflik (pd_fitted, 1, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the shape parameter (a), %! ## helping to understand the uncertainty in parameter estimates. statistics-release-2.1.2/inst/demos/prob.GammaDistribution.random000066400000000000000000000007611527311104300251760ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Gamma distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Gamma", "a", 2, "b", 1) %! samples = random (pd, 500, 1); %! hist (samples, 50) %! title ("Histogram of 500 random samples from Gamma(a=2, b=1)") %! xlabel ("Values") %! ylabel ("Frequency") %! %! ## This generates random samples from a Gamma distribution, useful %! ## for simulating waiting times or other positive skewed data. statistics-release-2.1.2/inst/demos/prob.GammaDistribution.std000066400000000000000000000003711527311104300245050ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Gamma distribution %! pd = makedist ("Gamma", "a", 2, "b", 1) %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## in the distribution. statistics-release-2.1.2/inst/demos/prob.GammaDistribution.truncate000066400000000000000000000014231527311104300255370ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Gamma distribution, with parameters a = 2 %! ## and b = 1, truncated at [1, 5] intervals. Generate 10000 random %! ## samples from this truncated distribution and superimpose a histogram scaled %! ## accordingly %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Gamma", "a", 2, "b", 1) %! t = truncate (pd, -1, 2) %! data = random (t, 10000, 1); %! ## Plot histogram and fitted PDF %! plot (t) %! hold on %! hist (data, 100, 50) %! hold off %! title ("Gamma distribution (a = 2, b = 1) truncated at [-1, 2]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a Gamma distribution to a specific %! ## range and visualizing the resulting distribution with random samples. statistics-release-2.1.2/inst/demos/prob.GammaDistribution.var000066400000000000000000000003421527311104300245010ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Gamma distribution %! pd = makedist ("Gamma", "a", 2, "b", 1) %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## distribution. statistics-release-2.1.2/inst/demos/prob.GeneralizedExtremeValueDistribution.cdf000066400000000000000000000016151527311104300302070ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Generalized Extreme Value distribution %! x = -2:0.01:5; %! pd1 = makedist ("GeneralizedExtremeValue", "k", 0, "sigma", 1, "mu", 0); %! pd2 = makedist ("GeneralizedExtremeValue", "k", 0.2, "sigma", 1, "mu", 0); %! pd3 = makedist ("GeneralizedExtremeValue", "k", -0.2, "sigma", 1, "mu", 0); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"k = 0, sigma = 1, mu = 0", "k = 0.2, sigma = 1, mu = 0", "k = -0.2, sigma = 1, mu = 0"}, ... %! "location", "southeast") %! title ("Generalized Extreme Value CDF") %! xlabel ("Value") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function for %! ## different GEV distributions, showing how probability accumulates over extreme %! ## values with varying shape parameters. statistics-release-2.1.2/inst/demos/prob.GeneralizedExtremeValueDistribution.icdf000066400000000000000000000015271527311104300303620ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Generalized Extreme Value distribution %! p = 0.001:0.001:0.999; %! pd1 = makedist ("GeneralizedExtremeValue", "k", 0, "sigma", 1, "mu", 0); %! pd2 = makedist ("GeneralizedExtremeValue", "k", 0.2, "sigma", 1, "mu", 0); %! pd3 = makedist ("GeneralizedExtremeValue", "k", -0.2, "sigma", 1, "mu", 0); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"k = 0, sigma = 1, mu = 0", "k = 0.2, sigma = 1, mu = 0", "k = -0.2, sigma = 1, mu = 0"}, ... %! "location", "northwest") %! title ("Generalized Extreme Value iCDF") %! xlabel ("Probability") %! ylabel ("Value") %! %! ## This demonstrates the inverse CDF (quantiles) for GEV distributions, useful %! ## for finding extreme values corresponding to given probabilities. statistics-release-2.1.2/inst/demos/prob.GeneralizedExtremeValueDistribution.iqr000066400000000000000000000005751527311104300302520ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Generalized Extreme Value distribution %! pd = makedist ("GeneralizedExtremeValue", "k", 0, "sigma", 1, "mu", 0) %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread of %! ## the middle 50% of the GEV distribution, useful for understanding variability %! ## in extreme values. statistics-release-2.1.2/inst/demos/prob.GeneralizedExtremeValueDistribution.k000066400000000000000000000013711527311104300277040ustar00rootroot00000000000000%!demo %! ## Create a Generalized Extreme Value distribution with default parameters %! pd = makedist ("GeneralizedExtremeValue") %! %! ## Query parameter 'k' (shape parameter) %! pd.k %! %! ## Set parameter 'k' %! pd.k = 0.2 %! %! ## Use this to initialize or modify the shape parameter of a Generalized Extreme %! ## Value distribution. The shape parameter determines the tail behavior and must %! ## be a real scalar. %!demo %! ## Create a Generalized Extreme Value distribution object by calling its constructor %! pd = GeneralizedExtremeValueDistribution (0.1, 1, 0) %! %! ## Query parameter 'k' %! pd.k %! %! ## This demonstrates direct construction with a specific shape parameter, %! ## useful for modeling extreme value data with known tail behavior. statistics-release-2.1.2/inst/demos/prob.GeneralizedExtremeValueDistribution.mean000066400000000000000000000006741527311104300303770ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Generalized Extreme Value distributions %! pd1 = makedist ("GeneralizedExtremeValue", "k", 0, "sigma", 1, "mu", 0); %! pd2 = makedist ("GeneralizedExtremeValue", "k", -0.2, "sigma", 1, "mu", 0); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for GEV distributions with %! ## different shape parameters, useful for predicting average extreme values. statistics-release-2.1.2/inst/demos/prob.GeneralizedExtremeValueDistribution.median000066400000000000000000000006441527311104300307110ustar00rootroot00000000000000%!demo %! ## Compute the median for different Generalized Extreme Value distributions %! pd1 = makedist ("GeneralizedExtremeValue", "k", 0, "sigma", 1, "mu", 0); %! pd2 = makedist ("GeneralizedExtremeValue", "k", -0.2, "sigma", 1, "mu", 0); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median extreme value, which splits the GEV distribution %! ## into two equal probability halves. statistics-release-2.1.2/inst/demos/prob.GeneralizedExtremeValueDistribution.mu000066400000000000000000000013431527311104300300720ustar00rootroot00000000000000%!demo %! ## Create a Generalized Extreme Value distribution with default parameters %! pd = makedist ("GeneralizedExtremeValue") %! %! ## Query parameter 'mu' (location parameter) %! pd.mu %! %! ## Set parameter 'mu' %! pd.mu = 0.5 %! %! ## Use this to initialize or modify the location parameter, which shifts the %! ## Generalized Extreme Value distribution. It must be a real scalar. %!demo %! ## Create a Generalized Extreme Value distribution object by calling its constructor %! pd = GeneralizedExtremeValueDistribution (0.1, 1, 0.5) %! %! ## Query parameter 'mu' %! pd.mu %! %! ## This demonstrates setting the location parameter directly via the constructor, %! ## useful for modeling the central tendency of extreme value data. statistics-release-2.1.2/inst/demos/prob.GeneralizedExtremeValueDistribution.negloglik000066400000000000000000000007631527311104300314310ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Generalized Extreme Value distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("GeneralizedExtremeValue", "k", 0, "sigma", 1, "mu", 0) %! data = random (pd, 100, 1); %! pd_fitted = fitdist (data, "GeneralizedExtremeValue") %! nlogL = negloglik (pd_fitted) %! %! ## This is useful for assessing the fit of a GEV distribution to data, lower %! ## values indicate a better fit. statistics-release-2.1.2/inst/demos/prob.GeneralizedExtremeValueDistribution.paramci000066400000000000000000000010621527311104300310630ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Generalized Extreme Value %! ## distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("GeneralizedExtremeValue", "k", -0.5, "sigma", 1, "mu", 2) %! data = random (pd, 5000, 1); %! pd_fitted = fitdist (data, "GeneralizedExtremeValue") %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameters (k, %! ## sigma, mu), providing a range of plausible values given the data. statistics-release-2.1.2/inst/demos/prob.GeneralizedExtremeValueDistribution.pdf000066400000000000000000000015011527311104300302160ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Generalized Extreme Value distribution %! x = -2:0.01:5; %! pd1 = makedist ("GeneralizedExtremeValue", "k", 0, "sigma", 1, "mu", 0); %! pd2 = makedist ("GeneralizedExtremeValue", "k", 0.2, "sigma", 1, "mu", 0); %! pd3 = makedist ("GeneralizedExtremeValue", "k", -0.2, "sigma", 1, "mu", 0); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"k = 0, sigma = 1, mu = 0", "k = 0.2, sigma = 1, mu = 0", "k = -0.2, sigma = 1, mu = 0"}, ... %! "location", "northeast") %! title ("Generalized Extreme Value PDF") %! xlabel ("Value") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for GEV distributions, %! ## showing the likelihood of different extreme values. statistics-release-2.1.2/inst/demos/prob.GeneralizedExtremeValueDistribution.plot000066400000000000000000000043461527311104300304350ustar00rootroot00000000000000%!demo %! ## Create a Generalized Extreme Value distribution with fixed parameters k = 0, %! ## sigma = 1, mu = 0 and plot its PDF. %! pd = makedist ("GeneralizedExtremeValue", "k", 0, "sigma", 1, "mu", 0) %! plot (pd) %! title ("Fixed Generalized Extreme Value distribution with k = 0, sigma = 1, mu = 0") %! %! ## Use this to visualize the PDF of a GEV distribution with specified parameters. %!demo %! ## Generate a data set of 100 random samples from a Generalized Extreme Value %! ## distribution with parameters k = 0, sigma = 1, mu = 0. Fit a GEV %! ## distribution to this data and plot its CDF superimposed over an empirical CDF. %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("GeneralizedExtremeValue", "k", 0, "sigma", 1, "mu", 0) %! data = random (pd_fixed, 100, 1); %! pd_fitted = fitdist (data, "GeneralizedExtremeValue") %! plot (pd_fitted, "PlotType", "cdf") %! txt = "Fitted Generalized Extreme Value distribution with k = %0.2f, sigma = %0.2f, mu = %0.2f"; %! title (sprintf (txt, pd_fitted.k, pd_fitted.sigma, pd_fitted.mu)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit. %!demo %! ## Generate a data set of 200 random samples from a Generalized Extreme Value %! ## distribution with parameters k = 0, sigma = 1, mu = 0. Display a probability %! ## plot for the GEV distribution fit to the data. %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("GeneralizedExtremeValue", "k", 0, "sigma", 1, "mu", 0) %! data = random (pd_fixed, 200, 1); %! pd_fitted = fitdist (data, "GeneralizedExtremeValue") %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted Generalized Extreme Value", ... %! " distribution with k = %0.2f, sigma = %0.2f, mu = %0.2f"); %! title (sprintf (txt, pd_fitted.k, pd_fitted.sigma, pd_fitted.mu)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted GEV distribution to %! ## the data, useful for checking if the GEV model is appropriate. statistics-release-2.1.2/inst/demos/prob.GeneralizedExtremeValueDistribution.proflik000066400000000000000000000011221527311104300311120ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the shape parameter of a fitted %! ## Generalized Extreme Value distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("GeneralizedExtremeValue", "k", -0.5, "sigma", 1, "mu", 2) %! data = random (pd, 5000, 1); %! pd_fitted = fitdist (data, "GeneralizedExtremeValue") %! [nlogL, param] = proflik (pd_fitted, 1, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the shape parameter (k), %! ## helping to understand the uncertainty in parameter estimates. statistics-release-2.1.2/inst/demos/prob.GeneralizedExtremeValueDistribution.random000066400000000000000000000011231527311104300307250ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Generalized Extreme Value distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("GeneralizedExtremeValue", "k", 0, "sigma", 1, "mu", 0) %! samples = random (pd, 500, 1); %! hist (samples, 50) %! title ("Histogram of 500 random samples from Generalized Extreme Value(k=0, sigma=1, mu=0)") %! xlabel ("Value") %! ylabel ("Frequency") %! %! ## This generates random samples from a GEV distribution, useful for simulating %! ## extreme value data, such as maximum wind speeds or flood levels. statistics-release-2.1.2/inst/demos/prob.GeneralizedExtremeValueDistribution.sigma000066400000000000000000000013741527311104300305550ustar00rootroot00000000000000%!demo %! ## Create a Generalized Extreme Value distribution with default parameters %! pd = makedist ("GeneralizedExtremeValue") %! %! ## Query parameter 'sigma' (scale parameter) %! pd.sigma %! %! ## Set parameter 'sigma' %! pd.sigma = 1.5 %! %! ## Use this to initialize or modify the scale parameter, which controls the %! ## spread of the Generalized Extreme Value distribution. It must be a positive %! ## real scalar. %!demo %! ## Create a Generalized Extreme Value distribution object by calling its constructor %! pd = GeneralizedExtremeValueDistribution (0.1, 1.5, 0) %! %! ## Query parameter 'sigma' %! pd.sigma %! %! ## This shows how to set the scale parameter directly via the constructor, %! ## ideal for modeling the spread of extreme value data. statistics-release-2.1.2/inst/demos/prob.GeneralizedExtremeValueDistribution.std000066400000000000000000000005031527311104300302400ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Generalized Extreme Value distribution %! pd = makedist ("GeneralizedExtremeValue", "k", 0, "sigma", 1, "mu", 0) %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## in extreme values for the GEV distribution. statistics-release-2.1.2/inst/demos/prob.GeneralizedExtremeValueDistribution.truncate000066400000000000000000000015451527311104300313020ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Generalized Extreme Value distribution, with parameters %! ## k = 0, sigma = 1, mu = 0, truncated at [0, 2] intervals. Generate 10000 %! ## random samples from this truncated distribution and superimpose a histogram %! ## scaled accordingly %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("GeneralizedExtremeValue", "k", 0, "sigma", 1, "mu", 0) %! t = truncate (pd, 0, 2) %! data = random (t, 10000, 1); %! %! ## Plot histogram and fitted PDF %! plot (t) %! hold on %! hist (data, 100, 50) %! hold off %! title ("Generalized Extreme Value distribution (k=0, sigma=1, mu=0) truncated at [0, 2]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a GEV distribution to a specific range and %! ## visualizing the resulting distribution with random samples. statistics-release-2.1.2/inst/demos/prob.GeneralizedExtremeValueDistribution.var000066400000000000000000000004531527311104300302420ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Generalized Extreme Value distribution %! pd = makedist ("GeneralizedExtremeValue", "k", 0, "sigma", 1, "mu", 0) %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of extreme %! ## values in the GEV distribution. statistics-release-2.1.2/inst/demos/prob.GeneralizedParetoDistribution.cdf000066400000000000000000000016311527311104300270310ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Generalized Pareto distribution %! x = 0:0.01:5; %! pd1 = makedist ("GeneralizedPareto", "k", 0.2, "sigma", 1, "theta", 0); %! pd2 = makedist ("GeneralizedPareto", "k", 0.5, "sigma", 1, "theta", 0); %! pd3 = makedist ("GeneralizedPareto", "k", 0.8, "sigma", 1, "theta", 0); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"k = 0.2, sigma = 1, theta = 0", "k = 0.5, sigma = 1, theta = 0", "k = 0.8, sigma = 1, theta = 0"}, ... %! "location", "southeast") %! title ("Generalized Pareto CDF") %! xlabel ("Exceedance value") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Generalized Pareto distributions, showing how probability %! ## accumulates over exceedance values in extreme value modeling. statistics-release-2.1.2/inst/demos/prob.GeneralizedParetoDistribution.icdf000066400000000000000000000016131527311104300272020ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Generalized Pareto distribution %! p = 0.001:0.001:0.999; %! pd1 = makedist ("GeneralizedPareto", "k", 0.2, "sigma", 1, "theta", 0); %! pd2 = makedist ("GeneralizedPareto", "k", 0.5, "sigma", 1, "theta", 0); %! pd3 = makedist ("GeneralizedPareto", "k", 0.8, "sigma", 1, "theta", 0); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"k = 0.2, sigma = 1, theta = 0", "k = 0.5, sigma = 1, theta = 0", "k = 0.8, sigma = 1, theta = 0"}, ... %! "location", "northwest") %! title ("Generalized Pareto iCDF") %! xlabel ("Probability") %! ylabel ("Exceedance value") %! %! ## This demonstrates the inverse CDF (quantiles) for Generalized Pareto %! ## distributions, useful for finding the exceedance value corresponding to %! ## given return probabilities in risk assessment. statistics-release-2.1.2/inst/demos/prob.GeneralizedParetoDistribution.iqr000066400000000000000000000005641527311104300270740ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Generalized Pareto distribution %! pd = makedist ("GeneralizedPareto", "k", 0.5, "sigma", 1, "theta", 0) %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, useful for understanding variability %! ## in exceedance values. statistics-release-2.1.2/inst/demos/prob.GeneralizedParetoDistribution.k000066400000000000000000000013501527311104300265250ustar00rootroot00000000000000%!demo %! ## Create a Generalized Pareto distribution with default parameters %! pd = makedist ("GeneralizedPareto") %! %! ## Query parameter 'k' (shape parameter) %! pd.k %! %! ## Set parameter 'k' %! pd.k = 0.5 %! %! ## Use this to initialize or modify the shape parameter of a Generalized Pareto %! ## distribution. The shape parameter can be any real scalar and determines the %! ## tail behavior (heavy-tailed if k>0). %!demo %! ## Create a Generalized Pareto distribution object by calling its constructor %! pd = GeneralizedParetoDistribution (0.5, 1, 0) %! %! ## Query parameter 'k' %! pd.k %! %! ## This demonstrates direct construction with a specific shape parameter, %! ## useful for modeling extreme values with known tail index. statistics-release-2.1.2/inst/demos/prob.GeneralizedParetoDistribution.mean000066400000000000000000000006771527311104300272260ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Generalized Pareto distributions %! pd1 = makedist ("GeneralizedPareto", "k", 0.2, "sigma", 1, "theta", 0); %! pd2 = makedist ("GeneralizedPareto", "k", 0.4, "sigma", 1, "theta", 0); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected exceedance value for Generalized Pareto %! ## distributions with different shape parameters (note: mean is finite only if k<1). statistics-release-2.1.2/inst/demos/prob.GeneralizedParetoDistribution.median000066400000000000000000000006271527311104300275360ustar00rootroot00000000000000%!demo %! ## Compute the median for different Generalized Pareto distributions %! pd1 = makedist ("GeneralizedPareto", "k", 0.2, "sigma", 1, "theta", 0); %! pd2 = makedist ("GeneralizedPareto", "k", 0.5, "sigma", 1, "theta", 0); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median exceedance value, which splits the distribution %! ## into two equal probability halves. statistics-release-2.1.2/inst/demos/prob.GeneralizedParetoDistribution.negloglik000066400000000000000000000007741527311104300302570ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Generalized Pareto distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("GeneralizedPareto", "k", 0.5, "sigma", 1, "theta", 0) %! data = random (pd, 100, 1); %! pd_fitted = GeneralizedParetoDistribution.fit (data, 0) %! nlogL = negloglik (pd_fitted) %! %! ## This is useful for assessing the fit of a Generalized Pareto distribution to %! ## data, lower values indicate a better fit. statistics-release-2.1.2/inst/demos/prob.GeneralizedParetoDistribution.paramci000066400000000000000000000012001527311104300277010ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Generalized Pareto %! ## distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("GeneralizedPareto", "k", 0.5, "sigma", 1, "theta", 0) %! data = random (pd, 1000, 1); %! [phat, ci] = gpfit (data, 0, 0.05, []); %! disp ("Estimated parameters (k, sigma, theta):"), disp (phat) %! disp ("95% Confidence Intervals for k and sigma:"), disp (ci) %! %! ## Use this to obtain confidence intervals for the estimated parameters (k and sigma), %! ## providing a range of plausible values given the data (theta is fixed). statistics-release-2.1.2/inst/demos/prob.GeneralizedParetoDistribution.pdf000066400000000000000000000015211527311104300270440ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Generalized Pareto distribution %! x = 0:0.01:5; %! pd1 = makedist ("GeneralizedPareto", "k", 0.2, "sigma", 1, "theta", 0); %! pd2 = makedist ("GeneralizedPareto", "k", 0.5, "sigma", 1, "theta", 0); %! pd3 = makedist ("GeneralizedPareto", "k", 0.8, "sigma", 1, "theta", 0); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"k = 0.2, sigma = 1, theta = 0", "k = 0.5, sigma = 1, theta = 0", "k = 0.8, sigma = 1, theta = 0"}, ... %! "location", "northeast") %! title ("Generalized Pareto PDF") %! xlabel ("Exceedance value") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Generalized Pareto %! ## distributions, showing the likelihood of different exceedance values. statistics-release-2.1.2/inst/demos/prob.GeneralizedParetoDistribution.plot000066400000000000000000000043311527311104300272530ustar00rootroot00000000000000%!demo %! ## Create a Generalized Pareto distribution with fixed parameters k = 0.5, sigma = 1, theta = 0 %! ## and plot its PDF. %! %! pd = makedist ("GeneralizedPareto", "k", 0.3, "sigma", 1, "theta", 0) %! plot (pd) %! title ("Fixed Generalized Pareto distribution with k = 0.3, sigma = 1, theta = 0") %!demo %! ## Generate a data set of 100 random samples from a Generalized Pareto %! ## distribution with parameters k = 0.5, sigma = 1, theta = 0. Fit a Generalized Pareto %! ## distribution to this data (fixing theta=0) and plot its CDF superimposed over an empirical %! ## CDF. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("GeneralizedPareto", "k", 0.5, "sigma", 1, "theta", 0) %! data = random (pd_fixed, 100, 1); %! pd_fitted = GeneralizedParetoDistribution.fit (data, 0) %! plot (pd_fitted, "PlotType", "cdf") %! txt = "Fitted Generalized Pareto distribution with k = %0.2f, sigma = %0.2f, theta = 0"; %! title (sprintf (txt, pd_fitted.k, pd_fitted.sigma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit in extreme value analysis. %!demo %! ## Generate a data set of 200 random samples from a Generalized Pareto %! ## distribution with parameters k = 0.5, sigma = 1, theta = 0. Display a probability %! ## plot for the Generalized Pareto distribution fit to the data (fixing theta=0). %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("GeneralizedPareto", "k", 0.5, "sigma", 1, "theta", 0) %! data = random (pd_fixed, 200, 1); %! pd_fitted = GeneralizedParetoDistribution.fit (data, 0) %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted Generalized Pareto", ... %! " distribution with k = %0.2f, sigma = %0.2f, theta = 0"); %! title (sprintf (txt, pd_fitted.k, pd_fitted.sigma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted distribution to the %! ## data, useful for checking if the Generalized Pareto model is appropriate for tails. statistics-release-2.1.2/inst/demos/prob.GeneralizedParetoDistribution.proflik000066400000000000000000000011331527311104300277400ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the shape parameter of a fitted %! ## Generalized Pareto distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("GeneralizedPareto", "k", 0.5, "sigma", 1, "theta", 0) %! data = random (pd, 1000, 1); %! pd_fitted = GeneralizedParetoDistribution.fit (data, 0) %! [nlogL, param] = proflik (pd_fitted, 1, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the shape parameter (k), %! ## helping to understand the uncertainty in parameter estimates for tail behavior. statistics-release-2.1.2/inst/demos/prob.GeneralizedParetoDistribution.random000066400000000000000000000011071527311104300275530ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Generalized Pareto distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("GeneralizedPareto", "k", 0.5, "sigma", 1, "theta", 0) %! samples = random (pd, 500, 1); %! hist (samples, 50) %! title ("Histogram of 500 random samples from GeneralizedPareto(k=0.5, sigma=1, theta=0)") %! xlabel ("Exceedance value") %! ylabel ("Frequency") %! %! ## This generates random samples from a Generalized Pareto distribution, useful %! ## for simulating extreme exceedances in risk modeling. statistics-release-2.1.2/inst/demos/prob.GeneralizedParetoDistribution.sigma000066400000000000000000000013051527311104300273730ustar00rootroot00000000000000%!demo %! ## Create a Generalized Pareto distribution with default parameters %! pd = makedist ("GeneralizedPareto") %! %! ## Query parameter 'sigma' (scale parameter) %! pd.sigma %! %! ## Set parameter 'sigma' %! pd.sigma = 2 %! %! ## Use this to initialize or modify the scale parameter in a Generalized Pareto %! ## distribution. The scale parameter must be a positive real scalar. %!demo %! ## Create a Generalized Pareto distribution object by calling its constructor %! pd = GeneralizedParetoDistribution (0.5, 2, 0) %! %! ## Query parameter 'sigma' %! pd.sigma %! %! ## This shows how to set the scale parameter directly via the constructor, %! ## ideal for modeling the spread in extreme value data. statistics-release-2.1.2/inst/demos/prob.GeneralizedParetoDistribution.std000066400000000000000000000004741527311104300270730ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Generalized Pareto distribution %! pd = makedist ("GeneralizedPareto", "k", 0.3, "sigma", 1, "theta", 0) %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## in exceedance values (finite only if k<0.5). statistics-release-2.1.2/inst/demos/prob.GeneralizedParetoDistribution.theta000066400000000000000000000013671527311104300274100ustar00rootroot00000000000000%!demo %! ## Create a Generalized Pareto distribution with default parameters %! pd = makedist ("GeneralizedPareto") %! %! ## Query parameter 'theta' (location parameter) %! pd.theta %! %! ## Set parameter 'theta' %! pd.theta = 1 %! %! ## Use this to initialize or modify the location parameter in a Generalized Pareto %! ## distribution. The location parameter can be any real scalar, often set to a threshold in extreme value analysis. %!demo %! ## Create a Generalized Pareto distribution object by calling its constructor %! pd = GeneralizedParetoDistribution (0.5, 1, 1) %! %! ## Query parameter 'theta' %! pd.theta %! %! ## This demonstrates setting the location parameter directly, useful for shifting the distribution in threshold-exceedance models. statistics-release-2.1.2/inst/demos/prob.GeneralizedParetoDistribution.truncate000066400000000000000000000015701527311104300301240ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Generalized Pareto distribution, with parameters k = 0.5, sigma = 1, theta = 0, %! ## truncated at [0.5, 2] intervals. Generate 10000 random samples from this truncated distribution %! ## and superimpose a histogram scaled accordingly %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("GeneralizedPareto", "k", 0.5, "sigma", 1, "theta", 0) %! t = truncate (pd, 0.5, 2) %! data = random (t, 10000, 1); %! %! ## Plot histogram and fitted PDF %! plot (t) %! hold on %! hist (data, 100, 50) %! hold off %! title ("Generalized Pareto distribution (k = 0.5, sigma = 1, theta = 0) truncated at [0.5, 2]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a Generalized Pareto distribution to a specific %! ## range and visualizing the resulting distribution with random samples. statistics-release-2.1.2/inst/demos/prob.GeneralizedParetoDistribution.var000066400000000000000000000004751527311104300270720ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Generalized Pareto distribution %! pd = makedist ("GeneralizedPareto", "k", 0.3, "sigma", 1, "theta", 0) %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## exceedance values in the distribution (finite only if k<0.5). statistics-release-2.1.2/inst/demos/prob.HalfNormalDistribution.cdf000066400000000000000000000016231527311104300254510ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Half-normal distribution %! rng (42); %! x = -1:0.01:5; %! data1 = 0.5 * abs (randn (10000, 1)); %! data2 = 1.0 * abs (randn (10000, 1)); %! data3 = 2.0 * abs (randn (10000, 1)); %! pd1 = fitdist (data1, "HalfNormal"); %! pd2 = fitdist (data2, "HalfNormal"); %! pd3 = fitdist (data3, "HalfNormal"); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5", "mu = 0, sigma = 1", "mu = 0, sigma = 2"}, ... %! "location", "southeast") %! title ("Half-normal CDF") %! xlabel ("values in x (x >= mu)") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Half-normal distributions, showing how probability %! ## accumulates for positive values, useful in reliability or error modeling. statistics-release-2.1.2/inst/demos/prob.HalfNormalDistribution.icdf000066400000000000000000000015671527311104300256310ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Half-normal distribution %! rng (42); %! p = 0.001:0.001:0.999; %! data1 = 0.5 * abs (randn (10000, 1)); %! data2 = 1.0 * abs (randn (10000, 1)); %! data3 = 2.0 * abs (randn (10000, 1)); %! pd1 = fitdist (data1, "HalfNormal"); %! pd2 = fitdist (data2, "HalfNormal"); %! pd3 = fitdist (data3, "HalfNormal"); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5", "mu = 0, sigma = 1", "mu = 0, sigma = 2"}, ... %! "location", "northwest") %! title ("Half-normal iCDF") %! xlabel ("Probability") %! ylabel ("values in x (x >= mu)") %! %! ## This demonstrates the inverse CDF (quantiles) for Half-normal %! ## distributions, useful for finding values corresponding to given %! ## probabilities, such as thresholds in quality control. statistics-release-2.1.2/inst/demos/prob.HalfNormalDistribution.iqr000066400000000000000000000005761527311104300255160ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Half-normal distribution %! rng (42); %! data = abs (randn (10000, 1)); %! pd = fitdist (data, "HalfNormal"); %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, helpful for understanding central %! ## variability in positive data. statistics-release-2.1.2/inst/demos/prob.HalfNormalDistribution.mean000066400000000000000000000007171527311104300256400ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Half-normal distributions %! rng (42); %! data1 = 0.5 * abs (randn (10000, 1)); %! data2 = 1.0 * abs (randn (10000, 1)); %! pd1 = fitdist (data1, "HalfNormal"); %! pd2 = fitdist (data2, "HalfNormal"); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Half-normal %! ## distributions with different scale parameters, representing average %! ## deviation or magnitude. statistics-release-2.1.2/inst/demos/prob.HalfNormalDistribution.median000066400000000000000000000007031527311104300261500ustar00rootroot00000000000000%!demo %! ## Compute the median for different Half-normal distributions %! rng (42); %! data1 = 0.5 * abs (randn (10000, 1)); %! data2 = 1.0 * abs (randn (10000, 1)); %! pd1 = fitdist (data1, "HalfNormal"); %! pd2 = fitdist (data2, "HalfNormal"); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which splits the distribution %! ## into two equal probability halves, robust to skewness in positive data. statistics-release-2.1.2/inst/demos/prob.HalfNormalDistribution.mu000066400000000000000000000014721527311104300253400ustar00rootroot00000000000000%!demo %! ## Create a Half-normal distribution with default parameters %! rng (42); %! data = abs (randn (10000, 1)); %! pd = fitdist (data, "HalfNormal"); %! %! ## Query parameter 'mu' (location parameter) %! pd.mu %! %! ## Set parameter 'mu' %! pd.mu = 1 %! %! ## Use this to initialize or modify the location parameter of a Half-normal %! ## distribution. The location parameter must be a real scalar, often set to 0 %! ## for modeling positive deviations from zero, such as measurement errors. %!demo %! ## Create a Half-normal distribution object by calling its constructor %! pd = HalfNormalDistribution (1.5, 2) %! %! ## Query parameter 'mu' %! pd.mu %! %! ## This demonstrates direct construction with a specific location parameter, %! ## useful for modeling data shifted from zero, like distances or folded normals. statistics-release-2.1.2/inst/demos/prob.HalfNormalDistribution.negloglik000066400000000000000000000006531527311104300266720ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Half-normal distribution %! rng (42); %! data = abs (randn (100, 1)); %! pd_fitted = fitdist (data, "HalfNormal"); %! params = [pd_fitted.mu, pd_fitted.sigma]; %! nlogL_hnlike = hnlike (params, data) %! %! ## This is useful for assessing the fit of a Half-normal distribution to %! ## data, with lower values indicating a better fit, often used in model comparison. statistics-release-2.1.2/inst/demos/prob.HalfNormalDistribution.paramci000066400000000000000000000006211527311104300263260ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Half-normal %! ## distribution %! rng (42); %! data = abs (randn (1000, 1)); %! pd_fitted = fitdist (data, "HalfNormal"); %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameters (mu %! ## fixed, sigma estimated), providing a range of plausible values given the data. statistics-release-2.1.2/inst/demos/prob.HalfNormalDistribution.pdf000066400000000000000000000014641527311104300254710ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Half-normal distribution %! rng (42); %! x = -1:0.01:5; %! data1 = 0.5 * abs (randn (10000, 1)); %! data2 = 1.0 * abs (randn (10000, 1)); %! data3 = 2.0 * abs (randn (10000, 1)); %! pd1 = fitdist (data1, "HalfNormal"); %! pd2 = fitdist (data2, "HalfNormal"); %! pd3 = fitdist (data3, "HalfNormal"); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5", "mu = 0, sigma = 1", "mu = 0, sigma = 2"}, ... %! "location", "northeast") %! title ("Half-normal PDF") %! xlabel ("values in x (x >= mu)") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Half-normal %! ## distributions, showing the likelihood for positive values. statistics-release-2.1.2/inst/demos/prob.HalfNormalDistribution.plot000066400000000000000000000033341527311104300256740ustar00rootroot00000000000000%!demo %! ## Create a Half-normal distribution with fixed parameters mu = 0 and %! ## sigma = 1 and plot its PDF. %! %! rng (42); %! data = abs (randn (10000, 1)); %! pd = fitdist (data, "HalfNormal"); %! plot (pd) %! title ("Fixed Half-normal distribution with mu = 0 and sigma = 1") %!demo %! ## Generate a data set of 100 random samples from a Half-normal %! ## distribution with parameters mu = 0 and sigma = 1. Fit a Half-normal %! ## distribution to this data and plot its CDF superimposed over an empirical %! ## CDF. %! %! rng (42); %! data = abs (randn (100, 1)); %! pd_fitted = fitdist (data, "HalfNormal"); %! plot (pd_fitted, "PlotType", "cdf") %! txt = "Fitted Half-normal distribution with mu = %0.2f and sigma = %0.2f"; %! title (sprintf (txt, pd_fitted.mu, pd_fitted.sigma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit. %!demo %! ## Generate a data set of 200 random samples from a Half-normal %! ## distribution with parameters mu = 0 and sigma = 1. Display a probability %! ## plot for the Half-normal distribution fit to the data. %! %! rng (42); %! data = abs (randn (200, 1)); %! pd_fitted = fitdist (data, "HalfNormal"); %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted Half-normal", ... %! " distribution with mu = %0.2f and sigma = %0.2f"); %! title (sprintf (txt, pd_fitted.mu, pd_fitted.sigma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted distribution to the %! ## data, useful for checking if the Half-normal model is appropriate. statistics-release-2.1.2/inst/demos/prob.HalfNormalDistribution.proflik000066400000000000000000000006501527311104300263620ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the scale parameter of a fitted %! ## Half-normal distribution %! rng (42); %! data = abs (randn (1000, 1)); %! pd_fitted = fitdist (data, "HalfNormal"); %! [nlogL, param] = proflik (pd_fitted, 2, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the scale parameter (sigma), %! ## helping to understand the uncertainty in parameter estimates. statistics-release-2.1.2/inst/demos/prob.HalfNormalDistribution.random000066400000000000000000000006521527311104300261760ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Half-normal distribution %! rng (42); %! samples = abs (randn (500, 1)); %! hist (samples, 50) %! title ("Histogram of 500 random samples from Half-normal(mu=0, sigma=1)") %! xlabel ("values in x (x >= mu)") %! ylabel ("Frequency") %! %! ## This generates random samples from a Half-normal distribution, useful %! ## for simulating positive data like absolute errors or magnitudes. statistics-release-2.1.2/inst/demos/prob.HalfNormalDistribution.sigma000066400000000000000000000014361527311104300260170ustar00rootroot00000000000000%!demo %! ## Create a Half-normal distribution with default parameters %! rng (42); %! data = abs (randn (10000, 1)); %! pd = fitdist (data, "HalfNormal"); %! %! ## Query parameter 'sigma' (scale parameter) %! pd.sigma %! %! ## Set parameter 'sigma' %! pd.sigma = 2 %! %! ## Use this to initialize or modify the scale parameter in a Half-normal %! ## distribution. The scale parameter must be a positive real scalar, controlling %! ## the spread of the distribution. %!demo %! ## Create a Half-normal distribution object by calling its constructor %! pd = HalfNormalDistribution (0, 1.5) %! %! ## Query parameter 'sigma' %! pd.sigma %! %! ## This shows how to set the scale parameter directly via the constructor, %! ## ideal for modeling variability in positive data, such as absolute residuals. statistics-release-2.1.2/inst/demos/prob.HalfNormalDistribution.std000066400000000000000000000005001527311104300255000ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Half-normal distribution %! rng (42); %! data = abs (randn (10000, 1)); %! pd = fitdist (data, "HalfNormal"); %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## in the positive values of the distribution. statistics-release-2.1.2/inst/demos/prob.HalfNormalDistribution.truncate000066400000000000000000000014771527311104300265510ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Half-normal distribution, with parameters mu = 0 %! ## and sigma = 1, truncated at [0.5, 2] intervals. Generate 10000 random %! ## samples from this truncated distribution and superimpose a histogram scaled %! ## accordingly %! %! rng (42); %! data_all = abs (randn (20000, 1)); %! data = data_all(data_all >= 0.5 & data_all <= 2); %! data = data(1:10000); %! %! pd = fitdist (data, "HalfNormal"); %! t = truncate (pd, 0.5, 2); %! %! ## Plot histogram and truncated PDF %! plot (t) %! hold on %! hist (data, 50) %! hold off %! title ("Half-normal distribution (mu = 0, sigma = 1) truncated at [0.5, 2]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a Half-normal distribution to a specific %! ## range and visualizing the resulting distribution with random samples. statistics-release-2.1.2/inst/demos/prob.HalfNormalDistribution.var000066400000000000000000000004511527311104300255030ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Half-normal distribution %! rng (42); %! data = abs (randn (10000, 1)); %! pd = fitdist (data, "HalfNormal"); %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## positive values in the distribution. statistics-release-2.1.2/inst/demos/prob.InverseGaussianDistribution.cdf000066400000000000000000000014311527311104300265310ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Inverse Gaussian distribution %! x = 0:0.01:5; %! pd1 = makedist ("InverseGaussian", "mu", 1, "lambda", 1); %! pd2 = makedist ("InverseGaussian", "mu", 1, "lambda", 2); %! pd3 = makedist ("InverseGaussian", "mu", 1, "lambda", 3); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"mu = 1, lambda = 1", "mu = 1, lambda = 2", "mu = 1, lambda = 3"}, ... %! "location", "southeast") %! title ("Inverse Gaussian CDF") %! xlabel ("values") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Inverse Gaussian distributions, showing how probability %! ## accumulates over values. statistics-release-2.1.2/inst/demos/prob.InverseGaussianDistribution.icdf000066400000000000000000000014131527311104300267020ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Inverse Gaussian distribution %! p = 0.001:0.001:0.999; %! pd1 = makedist ("InverseGaussian", "mu", 1, "lambda", 1); %! pd2 = makedist ("InverseGaussian", "mu", 1, "lambda", 2); %! pd3 = makedist ("InverseGaussian", "mu", 1, "lambda", 3); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"mu = 1, lambda = 1", "mu = 1, lambda = 2", "mu = 1, lambda = 3"}, ... %! "location", "northwest") %! title ("Inverse Gaussian iCDF") %! xlabel ("Probability") %! ylabel ("values") %! %! ## This demonstrates the inverse CDF (quantiles) for Inverse Gaussian %! ## distributions, useful for finding the value corresponding to %! ## given probabilities. statistics-release-2.1.2/inst/demos/prob.InverseGaussianDistribution.iqr000066400000000000000000000005451527311104300265750ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for an Inverse Gaussian distribution %! pd = makedist ("InverseGaussian", "mu", 1, "lambda", 2) %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, useful for understanding variability %! ## in non-negative data. statistics-release-2.1.2/inst/demos/prob.InverseGaussianDistribution.lambda000066400000000000000000000013121527311104300272130ustar00rootroot00000000000000%!demo %! ## Create an Inverse Gaussian distribution with default parameters %! pd = makedist ("InverseGaussian") %! %! ## Query parameter 'lambda' (shape parameter) %! pd.lambda %! %! ## Set parameter 'lambda' %! pd.lambda = 3 %! %! ## Use this to initialize or modify the shape parameter in an Inverse Gaussian %! ## distribution. The shape parameter must be a positive real scalar. %!demo %! ## Create an Inverse Gaussian distribution object by calling its constructor %! pd = InverseGaussianDistribution (1.5, 2) %! %! ## Query parameter 'lambda' %! pd.lambda %! %! ## This shows how to set the shape parameter directly via the constructor, %! ## ideal for modeling specific variability in non-negative data. statistics-release-2.1.2/inst/demos/prob.InverseGaussianDistribution.mean000066400000000000000000000005611527311104300267200ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Inverse Gaussian distributions %! pd1 = makedist ("InverseGaussian", "mu", 1, "lambda", 1); %! pd2 = makedist ("InverseGaussian", "mu", 1, "lambda", 2); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Inverse Gaussian %! ## distributions with different shape parameters. statistics-release-2.1.2/inst/demos/prob.InverseGaussianDistribution.median000066400000000000000000000005561527311104300272410ustar00rootroot00000000000000%!demo %! ## Compute the median for different Inverse Gaussian distributions %! pd1 = makedist ("InverseGaussian", "mu", 1, "lambda", 1); %! pd2 = makedist ("InverseGaussian", "mu", 1, "lambda", 2); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which splits the distribution %! ## into two equal probability halves. statistics-release-2.1.2/inst/demos/prob.InverseGaussianDistribution.mu000066400000000000000000000012571527311104300264240ustar00rootroot00000000000000%!demo %! ## Create an Inverse Gaussian distribution with default parameters %! pd = makedist ("InverseGaussian") %! %! ## Query parameter 'mu' (mean parameter) %! pd.mu %! %! ## Set parameter 'mu' %! pd.mu = 2 %! %! ## Use this to initialize or modify the mean parameter of an Inverse Gaussian %! ## distribution. The mean parameter must be a positive real scalar. %!demo %! ## Create an Inverse Gaussian distribution object by calling its constructor %! pd = InverseGaussianDistribution (1.5, 2) %! %! ## Query parameter 'mu' %! pd.mu %! %! ## This demonstrates direct construction with a specific mean parameter, %! ## useful for modeling non-negative skewed data with a known mean. statistics-release-2.1.2/inst/demos/prob.InverseGaussianDistribution.negloglik000066400000000000000000000007411527311104300277530ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Inverse Gaussian distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("InverseGaussian", "mu", 1, "lambda", 2) %! data = random (pd, 100, 1); %! pd_fitted = fitdist (data, "InverseGaussian") %! nlogL = negloglik (pd_fitted) %! %! ## This is useful for assessing the fit of an Inverse Gaussian distribution to %! ## data, lower values indicate a better fit. statistics-release-2.1.2/inst/demos/prob.InverseGaussianDistribution.paramci000066400000000000000000000010201527311104300274030ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Inverse Gaussian %! ## distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("InverseGaussian", "mu", 1, "lambda", 2) %! data = random (pd, 1000, 1); %! pd_fitted = fitdist (data, "InverseGaussian") %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameters (mu %! ## and lambda), providing a range of plausible values given the data. statistics-release-2.1.2/inst/demos/prob.InverseGaussianDistribution.pdf000066400000000000000000000013531527311104300265510ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Inverse Gaussian distribution %! x = 0:0.01:5; %! pd1 = makedist ("InverseGaussian", "mu", 1, "lambda", 1); %! pd2 = makedist ("InverseGaussian", "mu", 1, "lambda", 2); %! pd3 = makedist ("InverseGaussian", "mu", 1, "lambda", 3); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"mu = 1, lambda = 1", "mu = 1, lambda = 2", "mu = 1, lambda = 3"}, ... %! "location", "northeast") %! title ("Inverse Gaussian PDF") %! xlabel ("values") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Inverse Gaussian %! ## distributions, showing the likelihood of different values. statistics-release-2.1.2/inst/demos/prob.InverseGaussianDistribution.plot000066400000000000000000000040131527311104300267520ustar00rootroot00000000000000%!demo %! ## Create an Inverse Gaussian distribution with fixed parameters μ = 1 and %! ## λ = 2 and plot its PDF. %! %! pd = makedist ("InverseGaussian", "mu", 1, "lambda", 2) %! plot (pd) %! title ("Fixed Inverse Gaussian distribution with mu = 1 and lambda = 2") %!demo %! ## Generate a data set of 100 random samples from an Inverse Gaussian %! ## distribution with parameters μ = 1 and λ = 2. Fit an Inverse Gaussian %! ## distribution to this data and plot its CDF superimposed over an empirical %! ## CDF. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("InverseGaussian", "mu", 1, "lambda", 2) %! data = random (pd_fixed, 100, 1); %! pd_fitted = fitdist (data, "InverseGaussian") %! plot (pd_fitted, "PlotType", "cdf") %! txt = "Fitted Inverse Gaussian distribution with μ = %0.2f and λ = %0.2f"; %! title (sprintf (txt, pd_fitted.mu, pd_fitted.lambda)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit. %!demo %! ## Generate a data set of 200 random samples from an Inverse Gaussian %! ## distribution with parameters μ = 1 and λ = 2. Display a probability %! ## plot for the Inverse Gaussian distribution fit to the data. %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("InverseGaussian", "mu", 1, "lambda", 2) %! data = random (pd_fixed, 200, 1); %! pd_fitted = fitdist (data, "InverseGaussian") %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted Inverse Gaussian", ... %! " distribution with μ = %0.2f and λ = %0.2f"); %! title (sprintf (txt, pd_fitted.mu, pd_fitted.lambda)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted distribution to the %! ## data, useful for checking if the Inverse Gaussian model is appropriate. statistics-release-2.1.2/inst/demos/prob.InverseGaussianDistribution.proflik000066400000000000000000000010641527311104300274450ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the shape parameter of a fitted %! ## Inverse Gaussian distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("InverseGaussian", "mu", 1, "lambda", 2) %! data = random (pd, 1000, 1); %! pd_fitted = fitdist (data, "InverseGaussian") %! [nlogL, param] = proflik (pd_fitted, 2, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the shape parameter (lambda), %! ## helping to understand the uncertainty in parameter estimates. statistics-release-2.1.2/inst/demos/prob.InverseGaussianDistribution.random000066400000000000000000000010511527311104300272530ustar00rootroot00000000000000%!demo %! ## Generate random samples from an Inverse Gaussian distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("InverseGaussian", "mu", 1, "lambda", 2) %! samples = random (pd, 500, 1); %! hist (samples, 50) %! title ("Histogram of 500 random samples from Inverse Gaussian(mu=1, lambda=2)") %! xlabel ("values") %! ylabel ("Frequency") %! %! ## This generates random samples from an Inverse Gaussian distribution, useful %! ## for simulating non-negative skewed data like repair times. statistics-release-2.1.2/inst/demos/prob.InverseGaussianDistribution.std000066400000000000000000000004131527311104300265660ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for an Inverse Gaussian distribution %! pd = makedist ("InverseGaussian", "mu", 1, "lambda", 2) %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## in values. statistics-release-2.1.2/inst/demos/prob.InverseGaussianDistribution.truncate000066400000000000000000000015311527311104300276230ustar00rootroot00000000000000%!demo %! ## Plot the PDF of an Inverse Gaussian distribution, with parameters mu = 1 %! ## and lambda = 2, truncated at [0.5, 2] intervals. Generate 10000 random %! ## samples from this truncated distribution and superimpose a histogram scaled %! ## accordingly %! %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("InverseGaussian", "mu", 1, "lambda", 2) %! t = truncate (pd, 0.5, 2) %! data = random (t, 10000, 1); %! %! ## Plot histogram and fitted PDF %! plot (t) %! hold on %! hist (data, 100, 50) %! hold off %! title ("Inverse Gaussian distribution (mu = 1, lambda = 2) truncated at [0.5, 2]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating an Inverse Gaussian distribution to a specific %! ## range and visualizing the resulting distribution with random samples. statistics-release-2.1.2/inst/demos/prob.InverseGaussianDistribution.var000066400000000000000000000004141527311104300265650ustar00rootroot00000000000000%!demo %! ## Compute the variance for an Inverse Gaussian distribution %! pd = makedist ("InverseGaussian", "mu", 1, "lambda", 2) %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## values in the distribution. statistics-release-2.1.2/inst/demos/prob.LogisticDistribution.cdf000066400000000000000000000014341527311104300252030ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Logistic distribution %! x = -5:0.01:5; %! pd1 = makedist ("Logistic", "mu", 0, "sigma", 0.5); %! pd2 = makedist ("Logistic", "mu", 0, "sigma", 1); %! pd3 = makedist ("Logistic", "mu", 0, "sigma", 1.5); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5", "mu = 0, sigma = 1", "mu = 0, sigma = 1.5"}, ... %! "location", "southeast") %! title ("Logistic CDF") %! xlabel ("Value") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Logistic distributions, showing how probability accumulates %! ## over values, useful in regression or classification tasks. statistics-release-2.1.2/inst/demos/prob.LogisticDistribution.icdf000066400000000000000000000014121527311104300253500ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Logistic distribution %! p = 0.001:0.001:0.999; %! pd1 = makedist ("Logistic", "mu", 0, "sigma", 0.5); %! pd2 = makedist ("Logistic", "mu", 0, "sigma", 1); %! pd3 = makedist ("Logistic", "mu", 0, "sigma", 1.5); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5", "mu = 0, sigma = 1", "mu = 0, sigma = 1.5"}, ... %! "location", "northwest") %! title ("Logistic iCDF") %! xlabel ("Probability") %! ylabel ("Value") %! %! ## This demonstrates the inverse CDF (quantiles) for Logistic distributions, %! ## useful for finding values corresponding to specific probabilities, such as %! ## thresholds in logistic regression. statistics-release-2.1.2/inst/demos/prob.LogisticDistribution.iqr000066400000000000000000000005201527311104300252350ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Logistic distribution %! pd = makedist ("Logistic", "mu", 0, "sigma", 1) %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, useful for understanding variability %! ## in logistic data. statistics-release-2.1.2/inst/demos/prob.LogisticDistribution.mean000066400000000000000000000005661527311104300253740ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Logistic distributions %! pd1 = makedist ("Logistic", "mu", 0, "sigma", 0.5); %! pd2 = makedist ("Logistic", "mu", 0, "sigma", 1); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Logistic distributions %! ## with different scale parameters, representing the central tendency. statistics-release-2.1.2/inst/demos/prob.LogisticDistribution.median000066400000000000000000000006051527311104300257030ustar00rootroot00000000000000%!demo %! ## Compute the median for different Logistic distributions %! pd1 = makedist ("Logistic", "mu", 0, "sigma", 0.5); %! pd2 = makedist ("Logistic", "mu", 0, "sigma", 1); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which splits the distribution into %! ## two equal probability halves, useful for robust central tendency measures. statistics-release-2.1.2/inst/demos/prob.LogisticDistribution.mu000066400000000000000000000014031527311104300250640ustar00rootroot00000000000000%!demo %! ## Create a Logistic distribution with default parameters %! pd = makedist ("Logistic") %! %! ## Query parameter 'mu' (location parameter) %! pd.mu %! %! ## Set parameter 'mu' %! pd.mu = 2 %! %! ## Use this to initialize or modify the location parameter of a Logistic %! ## distribution. The location parameter must be a finite real scalar and %! ## represents the center of the distribution, often used in regression models. %!demo %! ## Create a Logistic distribution object by calling its constructor %! pd = LogisticDistribution (1.5, 0.5) %! %! ## Query parameter 'mu' %! pd.mu %! %! ## This demonstrates direct construction with a specific location parameter, %! ## useful for modeling data centered around a known value, such as in logistic regression. statistics-release-2.1.2/inst/demos/prob.LogisticDistribution.negloglik000066400000000000000000000007411527311104300264220ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Logistic distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Logistic", "mu", 0, "sigma", 1) %! data = random (pd, 100, 1); %! pd_fitted = fitdist (data, "Logistic") %! nlogL = negloglik (pd_fitted) %! %! ## This is useful for assessing the fit of a Logistic distribution to data, %! ## lower values indicate a better fit, often used in model evaluation. statistics-release-2.1.2/inst/demos/prob.LogisticDistribution.paramci000066400000000000000000000007621527311104300260660ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Logistic distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Logistic", "mu", 0, "sigma", 1) %! data = random (pd, 1000, 1); %! pd_fitted = fitdist (data, "Logistic") %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameters (mu %! ## and sigma), providing a range of plausible values given the data. statistics-release-2.1.2/inst/demos/prob.LogisticDistribution.pdf000066400000000000000000000013471527311104300252230ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Logistic distribution %! x = -5:0.01:5; %! pd1 = makedist ("Logistic", "mu", 0, "sigma", 0.5); %! pd2 = makedist ("Logistic", "mu", 0, "sigma", 1); %! pd3 = makedist ("Logistic", "mu", 0, "sigma", 1.5); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5", "mu = 0, sigma = 1", "mu = 0, sigma = 1.5"}, ... %! "location", "northeast") %! title ("Logistic PDF") %! xlabel ("Value") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Logistic distributions, %! ## showing the likelihood of different values, useful for understanding data density. statistics-release-2.1.2/inst/demos/prob.LogisticDistribution.plot000066400000000000000000000037571527311104300254370ustar00rootroot00000000000000%!demo %! ## Create a Logistic distribution with fixed parameters mu = 0 and sigma = 1 %! ## and plot its PDF. %! pd = makedist ("Logistic", "mu", 0, "sigma", 1) %! plot (pd) %! title ("Fixed Logistic distribution with mu = 0 and sigma = 1") %! %! ## Use this to visualize the PDF of a Logistic distribution with fixed parameters. %!demo %! ## Generate a data set of 100 random samples from a Logistic distribution %! ## with parameters mu = 0 and sigma = 1. Fit a Logistic distribution to this %! ## data and plot its CDF superimposed over an empirical CDF. %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("Logistic", "mu", 0, "sigma", 1) %! data = random (pd_fixed, 100, 1); %! pd_fitted = fitdist (data, "Logistic") %! plot (pd_fitted, "PlotType", "cdf") %! txt = "Fitted Logistic distribution with mu = %0.2f and sigma = %0.2f"; %! title (sprintf (txt, pd_fitted.mu, pd_fitted.sigma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit. %!demo %! ## Generate a data set of 200 random samples from a Logistic distribution %! ## with parameters mu = 0 and sigma = 1. Display a probability plot for the %! ## Logistic distribution fit to the data. %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("Logistic", "mu", 0, "sigma", 1) %! data = random (pd_fixed, 200, 1); %! pd_fitted = fitdist (data, "Logistic") %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted Logistic distribution", ... %! " with mu = %0.2f and sigma = %0.2f"); %! title (sprintf (txt, pd_fitted.mu, pd_fitted.sigma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted distribution to the %! ## data, useful for checking if the Logistic model is appropriate. statistics-release-2.1.2/inst/demos/prob.LogisticDistribution.proflik000066400000000000000000000010341527311104300261110ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the scale parameter of a fitted %! ## Logistic distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Logistic", "mu", 0, "sigma", 1) %! data = random (pd, 1000, 1); %! pd_fitted = fitdist (data, "Logistic") %! [nlogL, param] = proflik (pd_fitted, 2, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the scale parameter (sigma), %! ## helping to understand the uncertainty in parameter estimates. statistics-release-2.1.2/inst/demos/prob.LogisticDistribution.random000066400000000000000000000010221527311104300257200ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Logistic distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Logistic", "mu", 0, "sigma", 1) %! samples = random (pd, 500, 1); %! hist (samples, 50) %! title ("Histogram of 500 random samples from Logistic(mu=0, sigma=1)") %! xlabel ("Value") %! ylabel ("Frequency") %! %! ## This generates random samples from a Logistic distribution, useful for %! ## simulating data for logistic regression or neural network modeling. statistics-release-2.1.2/inst/demos/prob.LogisticDistribution.sigma000066400000000000000000000013511527311104300255450ustar00rootroot00000000000000%!demo %! ## Create a Logistic distribution with default parameters %! pd = makedist ("Logistic") %! %! ## Query parameter 'sigma' (scale parameter) %! pd.sigma %! %! ## Set parameter 'sigma' %! pd.sigma = 0.8 %! %! ## Use this to initialize or modify the scale parameter in a Logistic %! ## distribution. The scale parameter must be a positive real scalar and %! ## controls the spread of the distribution. %!demo %! ## Create a Logistic distribution object by calling its constructor %! pd = LogisticDistribution (1.5, 0.5) %! %! ## Query parameter 'sigma' %! pd.sigma %! %! ## This shows how to set the scale parameter directly via the constructor, %! ## ideal for modeling data with specific variability, such as in neural network outputs. statistics-release-2.1.2/inst/demos/prob.LogisticDistribution.std000066400000000000000000000004521527311104300252400ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Logistic distribution %! pd = makedist ("Logistic", "mu", 0, "sigma", 1) %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## in the distribution, useful for understanding data spread. statistics-release-2.1.2/inst/demos/prob.LogisticDistribution.truncate000066400000000000000000000014031527311104300262700ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Logistic distribution, with parameters mu = 0 and sigma = 1, %! ## truncated at [-2, 2] intervals. Generate 10000 random samples from this %! ## truncated distribution and superimpose a histogram scaled accordingly. %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Logistic", "mu", 0, "sigma", 1) %! t = truncate (pd, -2, 2) %! data = random (t, 10000, 1); %! plot (t) %! hold on %! hist (data, 100, 50) %! hold off %! title ("Logistic distribution (mu = 0, sigma = 1) truncated at [-2, 2]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a Logistic distribution to a specific range %! ## and visualizing the resulting distribution with random samples. statistics-release-2.1.2/inst/demos/prob.LogisticDistribution.var000066400000000000000000000004161527311104300252360ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Logistic distribution %! pd = makedist ("Logistic", "mu", 0, "sigma", 1) %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## distribution, useful for statistical analysis. statistics-release-2.1.2/inst/demos/prob.LoglogisticDistribution.cdf000066400000000000000000000016221527311104300257040ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Log-logistic distribution %! rng (42); %! x = 0:0.01:10; %! data1 = loglrnd (0, 0.5, 10000, 1); %! data2 = loglrnd (0, 1, 10000, 1); %! data3 = loglrnd (0, 2, 10000, 1); %! pd1 = fitdist (data1, "Loglogistic"); %! pd2 = fitdist (data2, "Loglogistic"); %! pd3 = fitdist (data3, "Loglogistic"); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5", "mu = 0, sigma = 1", "mu = 0, sigma = 2"}, ... %! "location", "southeast") %! title ("Log-logistic CDF") %! xlabel ("values in x (x > 0)") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Log-logistic distributions, showing how probability %! ## accumulates for positive values, useful in survival analysis or risk modeling. statistics-release-2.1.2/inst/demos/prob.LoglogisticDistribution.icdf000066400000000000000000000016041527311104300260550ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Log-logistic distribution %! rng (42); %! p = 0.0001:0.001:0.95; %! data1 = loglrnd (0, 0.5, 10000, 1); %! data2 = loglrnd (0, 1, 10000, 1); %! data3 = loglrnd (0, 2, 10000, 1); %! pd1 = fitdist (data1, "Loglogistic"); %! pd2 = fitdist (data2, "Loglogistic"); %! pd3 = fitdist (data3, "Loglogistic"); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5", "mu = 0, sigma = 1", "mu = 0, sigma = 2"}, ... %! "location", "northwest") %! title ("Log-logistic iCDF") %! xlabel ("Probability") %! ylabel ("values in x (x > 0)") %! %! ## This demonstrates the inverse CDF (quantiles) for Log-logistic %! ## distributions, useful for finding values corresponding to given %! ## probabilities, such as percentiles in income data or survival quantiles. statistics-release-2.1.2/inst/demos/prob.LoglogisticDistribution.iqr000066400000000000000000000006301527311104300257410ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Log-logistic distribution %! rng (42); %! data = loglrnd (0, 1, 10000, 1); %! pd = fitdist (data, "Loglogistic"); %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, helpful for understanding central %! ## variability in positive skewed data like lifetimes. statistics-release-2.1.2/inst/demos/prob.LoglogisticDistribution.mean000066400000000000000000000007641527311104300260760ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Log-logistic distributions %! rng (42); %! data1 = loglrnd (0, 0.5, 10000, 1); %! data2 = loglrnd (0, 1, 10000, 1); %! pd1 = fitdist (data1, "Loglogistic"); %! pd2 = fitdist (data2, "Loglogistic"); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Log-logistic %! ## distributions with different sigma parameters. Note that the mean may be %! ## infinite for sigma <= 1, useful in heavy-tailed modeling. statistics-release-2.1.2/inst/demos/prob.LoglogisticDistribution.median000066400000000000000000000007131527311104300264050ustar00rootroot00000000000000%!demo %! ## Compute the median for different Log-logistic distributions %! rng (42); %! data1 = loglrnd (0, 0.5, 10000, 1); %! data2 = loglrnd (0, 1, 10000, 1); %! pd1 = fitdist (data1, "Loglogistic"); %! pd2 = fitdist (data2, "Loglogistic"); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which splits the distribution %! ## into two equal probability halves, robust to the heavy tails in Log-logistic data. statistics-release-2.1.2/inst/demos/prob.LoglogisticDistribution.mu000066400000000000000000000016431527311104300255740ustar00rootroot00000000000000%!demo %! ## Create a Log-logistic distribution with default parameters %! rng (42); %! data = loglrnd (0, 1, 10000, 1); %! pd = fitdist (data, "Loglogistic"); %! %! ## Query parameter 'mu' (mean of logarithmic values) %! pd.mu %! %! ## Set parameter 'mu' %! pd.mu = 1 %! %! ## Use this to initialize or modify the mean of the logarithmic values in a %! ## Log-logistic distribution. The mu parameter must be a nonnegative real %! ## scalar, often representing the location in log-space for modeling %! ## positive skewed data like survival times or income distributions. %!demo %! ## Create a Log-logistic distribution object by calling its constructor %! pd = LoglogisticDistribution (1.5, 2); %! %! ## Query parameter 'mu' %! pd.mu %! %! ## This demonstrates direct construction with a specific mu parameter, %! ## useful for modeling data with a known log-mean, such as in reliability %! ## engineering or financial modeling. statistics-release-2.1.2/inst/demos/prob.LoglogisticDistribution.negloglik000066400000000000000000000006211527311104300271210ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Log-logistic distribution %! rng (42); %! data = loglrnd (0, 1, 100, 1); %! pd_fitted = fitdist (data, "Loglogistic"); %! nlogL = negloglik (pd_fitted) %! %! ## This is useful for assessing the fit of a Log-logistic distribution to %! ## data, with lower values indicating a better fit, often used in model %! ## selection or optimization. statistics-release-2.1.2/inst/demos/prob.LoglogisticDistribution.paramci000066400000000000000000000007051527311104300265650ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Log-logistic %! ## distribution %! rng (42); %! data = loglrnd (0, 1, 1000, 1); %! pd_fitted = fitdist (data, "Loglogistic"); %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameters (mu %! ## and sigma), providing a range of plausible values given the data, especially %! ## useful in survival or reliability analysis. statistics-release-2.1.2/inst/demos/prob.LoglogisticDistribution.pdf000066400000000000000000000015011527311104300257150ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Log-logistic distribution %! rng (42); %! x = 0:0.01:10; %! data1 = loglrnd (0, 0.5, 10000, 1); %! data2 = loglrnd (0, 1, 10000, 1); %! data3 = loglrnd (0, 2, 10000, 1); %! pd1 = fitdist (data1, "Loglogistic"); %! pd2 = fitdist (data2, "Loglogistic"); %! pd3 = fitdist (data3, "Loglogistic"); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5", "mu = 0, sigma = 1", "mu = 0, sigma = 2"}, ... %! "location", "northeast") %! title ("Log-logistic PDF") %! xlabel ("values in x (x > 0)") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Log-logistic %! ## distributions, showing the likelihood for positive values with varying tails. statistics-release-2.1.2/inst/demos/prob.LoglogisticDistribution.plot000066400000000000000000000041031527311104300261230ustar00rootroot00000000000000%!demo %! ## Create a Log-logistic distribution with fixed parameters mu = 0 and %! ## sigma = 1 and plot its PDF. %! %! rng (42); %! data = loglrnd (0, 1, 10000, 1); %! pd = fitdist (data, "Loglogistic"); %! x = linspace (0.01, 20, 1000); %! y = pdf (pd, x); %! plot (x, y, "b", "LineWidth", 2) %! grid on %! title ("Fixed Log-logistic distribution with mu = 0 and sigma = 1") %! xlabel ("x") %! ylabel ("PDF") %!demo %! ## Generate a data set of 100 random samples from a Log-logistic %! ## distribution with parameters mu = 0 and sigma = 1. Fit a Log-logistic %! ## distribution to this data and plot its CDF superimposed over an empirical %! ## CDF. %! %! rng (42); %! data = loglrnd (0, 1, 100, 1); %! pd_fitted = fitdist (data, "Loglogistic"); %! ecdf (data); %! hold on; %! x = linspace (icdf (pd_fitted, 0.01), icdf (pd_fitted, 0.99), 1000); %! y = cdf (pd_fitted, x); %! plot (x, y, "r", "LineWidth", 2); %! txt = "Fitted Log-logistic distribution with mu = %0.2f and sigma = %0.2f"; %! title (sprintf (txt, pd_fitted.mu, pd_fitted.sigma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! xlabel ("x") %! ylabel ("CDF") %! grid on %! hold off; %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit in skewed positive data. %!demo %! ## Generate a data set of 200 random samples from a Log-logistic %! ## distribution with parameters mu = 0 and sigma = 1. Display a probability %! ## plot for the Log-logistic distribution fit to the data. %! %! rng (42); %! data = loglrnd (0, 1, 200, 1); %! pd_fitted = fitdist (data, "Loglogistic"); %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted Log-logistic", ... %! " distribution with mu = %0.2f and sigma = %0.2f"); %! title (sprintf (txt, pd_fitted.mu, pd_fitted.sigma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted distribution to the %! ## data, useful for checking if the Log-logistic model captures the tail behavior. statistics-release-2.1.2/inst/demos/prob.LoglogisticDistribution.proflik000066400000000000000000000006711527311104300266210ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the sigma parameter of a fitted %! ## Log-logistic distribution %! rng (42); %! data = loglrnd (0, 1, 1000, 1); %! pd_fitted = fitdist (data, "Loglogistic"); %! [nlogL, param] = proflik (pd_fitted, 2, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the scale parameter (sigma), %! ## helping to understand the uncertainty and shape of the likelihood surface. statistics-release-2.1.2/inst/demos/prob.LoglogisticDistribution.random000066400000000000000000000007561527311104300264370ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Log-logistic distribution %! rng (42); %! samples = loglrnd (0, 1, 500, 1); %! hist (samples, 50) %! p99 = prctile (samples, 99); %! xlim ([0, p99]); %! title ("Histogram of 500 random samples from Log-logistic(mu=0, sigma=1)") %! xlabel ("values in x (x > 0)") %! ylabel ("Frequency") %! %! ## This generates random samples from a Log-logistic distribution, useful %! ## for simulating positive skewed data like waiting times or economic variables. statistics-release-2.1.2/inst/demos/prob.LoglogisticDistribution.sigma000066400000000000000000000015261527311104300262530ustar00rootroot00000000000000%!demo %! ## Create a Log-logistic distribution with default parameters %! rng (42); %! data = loglrnd (0, 1, 10000, 1); %! pd = fitdist (data, "Loglogistic"); %! %! ## Query parameter 'sigma' (scale of logarithmic values) %! pd.sigma %! %! ## Set parameter 'sigma' %! pd.sigma = 2 %! %! ## Use this to initialize or modify the scale of the logarithmic values in a %! ## Log-logistic distribution. The sigma parameter must be a positive real %! ## scalar, controlling the shape and tail heaviness of the distribution. %!demo %! ## Create a Log-logistic distribution object by calling its constructor %! pd = LoglogisticDistribution (0, 1.5); %! %! ## Query parameter 'sigma' %! pd.sigma %! %! ## This shows how to set the sigma parameter directly via the constructor, %! ## ideal for modeling variability in positive skewed data, such as failure times. statistics-release-2.1.2/inst/demos/prob.LoglogisticDistribution.std000066400000000000000000000005511527311104300257420ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Log-logistic distribution %! rng (42); %! data = loglrnd (0, 0.4, 10000, 1); %! pd = fitdist (data, "Loglogistic"); %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## in the positive skewed values. Note it may be infinite for certain parameters. statistics-release-2.1.2/inst/demos/prob.LoglogisticDistribution.truncate000066400000000000000000000020161527311104300267730ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Log-logistic distribution, with parameters mu = 0 %! ## and sigma = 1, truncated at [0.5, 5] intervals. Generate 10000 random %! ## samples from this truncated distribution and superimpose a histogram scaled %! ## accordingly %! %! rng (42); %! data_all = loglrnd (0, 1, 30000, 1); %! data = data_all(data_all >= 0.5 & data_all <= 5); %! data = data(1:10000); %! %! pd = fitdist (data, "Loglogistic"); %! t = truncate (pd, 0.5, 5); %! %! [counts, centers] = hist (data, 50); %! bin_width = centers(2) - centers(1); %! bar (centers, counts / (sum (counts) * bin_width), 1); %! hold on; %! %! x = linspace (0.5, 5, 500); %! y = pdf (t, x); %! plot (x, y, "r", "linewidth", 2); %! title ("Log-logistic distribution (mu = 0, sigma = 1) truncated at [0.5, 5]") %! legend ("Truncated PDF", "Histogram") %! hold off; %! %! ## This demonstrates truncating a Log-logistic distribution to a specific %! ## range and visualizing the resulting distribution with random samples, %! ## useful for bounded positive data. statistics-release-2.1.2/inst/demos/prob.LoglogisticDistribution.var000066400000000000000000000005121527311104300257350ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Log-logistic distribution %! rng (42); %! data = loglrnd (0, 0.4, 10000, 1); %! pd = fitdist (data, "Loglogistic"); %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## positive skewed values. Note it may be infinite for sigma <= 2. statistics-release-2.1.2/inst/demos/prob.LognormalDistribution.cdf000066400000000000000000000016071527311104300253620ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Lognormal distribution %! rng (42); %! x = 0:0.01:10; %! data1 = lognrnd (0, 0.5, 10000, 1); %! data2 = lognrnd (0, 1.0, 10000, 1); %! data3 = lognrnd (0, 1.5, 10000, 1); %! pd1 = fitdist (data1, "Lognormal"); %! pd2 = fitdist (data2, "Lognormal"); %! pd3 = fitdist (data3, "Lognormal"); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5", "mu = 0, sigma = 1", "mu = 0, sigma = 1.5"}, ... %! "location", "southeast") %! title ("Lognormal CDF") %! xlabel ("values in x (x > 0)") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Lognormal distributions, showing how probability accumulates %! ## for positive skewed data, useful in finance or biology modeling. statistics-release-2.1.2/inst/demos/prob.LognormalDistribution.icdf000066400000000000000000000015451527311104300255340ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Lognormal distribution %! rng (42); %! p = 0.001:0.001:0.999; %! data1 = lognrnd (0, 0.5, 10000, 1); %! data2 = lognrnd (0, 1.0, 10000, 1); %! data3 = lognrnd (0, 1.5, 10000, 1); %! pd1 = fitdist (data1, "Lognormal"); %! pd2 = fitdist (data2, "Lognormal"); %! pd3 = fitdist (data3, "Lognormal"); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5", "mu = 0, sigma = 1", "mu = 0, sigma = 1.5"}, ... %! "location", "northwest") %! title ("Lognormal iCDF") %! xlabel ("Probability") %! ylabel ("values in x (x > 0)") %! %! ## This demonstrates the inverse CDF (quantiles) for Lognormal %! ## distributions, useful for finding values corresponding to given %! ## probabilities, such as risk thresholds in finance. statistics-release-2.1.2/inst/demos/prob.LognormalDistribution.iqr000066400000000000000000000006041527311104300254150ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Lognormal distribution %! rng (42); %! data = lognrnd (0, 1, 10000, 1); %! pd = fitdist (data, "Lognormal"); %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, helpful for understanding central %! ## variability in skewed positive data. statistics-release-2.1.2/inst/demos/prob.LognormalDistribution.mean000066400000000000000000000007231527311104300255440ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Lognormal distributions %! rng (42); %! data1 = lognrnd (0, 0.5, 10000, 1); %! data2 = lognrnd (0, 1.0, 10000, 1); %! pd1 = fitdist (data1, "Lognormal"); %! pd2 = fitdist (data2, "Lognormal"); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Lognormal %! ## distributions with different sigma parameters, representing average %! ## outcomes in multiplicative processes. statistics-release-2.1.2/inst/demos/prob.LognormalDistribution.median000066400000000000000000000006731527311104300260650ustar00rootroot00000000000000%!demo %! ## Compute the median for different Lognormal distributions %! rng (42); %! data1 = lognrnd (0, 0.5, 10000, 1); %! data2 = lognrnd (0, 1.0, 10000, 1); %! pd1 = fitdist (data1, "Lognormal"); %! pd2 = fitdist (data2, "Lognormal"); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which splits the distribution %! ## into two equal probability halves, robust to skewness in positive data. statistics-release-2.1.2/inst/demos/prob.LognormalDistribution.mu000066400000000000000000000015141527311104300252440ustar00rootroot00000000000000%!demo %! ## Create a Lognormal distribution with default parameters %! rng (42); %! data = lognrnd (0, 1, 10000, 1); %! pd = fitdist (data, "Lognormal"); %! %! ## Query parameter 'mu' (mean of logarithmic values) %! pd.mu %! %! ## Set parameter 'mu' %! pd.mu = 1 %! %! ## Use this to initialize or modify the mean parameter of the logarithmic values %! ## in a Lognormal distribution. The mu parameter must be a real scalar, often %! ## representing the log-mean of multiplicative processes, like growth rates. %!demo %! ## Create a Lognormal distribution object by calling its constructor %! pd = LognormalDistribution (1.5, 0.5) %! %! ## Query parameter 'mu' %! pd.mu %! %! ## This demonstrates direct construction with a specific mu parameter, %! ## useful for modeling skewed positive data shifted in log-scale, such as incomes or sizes. statistics-release-2.1.2/inst/demos/prob.LognormalDistribution.negloglik000066400000000000000000000006541527311104300266020ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Lognormal distribution %! rng (42); %! data = lognrnd (0, 1, 100, 1); %! pd_fitted = fitdist (data, "Lognormal"); %! params = [pd_fitted.mu, pd_fitted.sigma]; %! nlogL_lognlike = lognlike (params, data) %! %! ## This is useful for assessing the fit of a Lognormal distribution to %! ## data, with lower values indicating a better fit, often used in model comparison. statistics-release-2.1.2/inst/demos/prob.LognormalDistribution.paramci000066400000000000000000000006031527311104300262350ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Lognormal %! ## distribution %! rng (42); %! data = lognrnd (0, 1, 1000, 1); %! pd_fitted = fitdist (data, "Lognormal"); %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameters (mu %! ## and sigma), providing a range of plausible values given the data. statistics-release-2.1.2/inst/demos/prob.LognormalDistribution.pdf000066400000000000000000000014541527311104300253770ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Lognormal distribution %! rng (42); %! x = 0:0.01:10; %! data1 = lognrnd (0, 0.5, 10000, 1); %! data2 = lognrnd (0, 1.0, 10000, 1); %! data3 = lognrnd (0, 1.5, 10000, 1); %! pd1 = fitdist (data1, "Lognormal"); %! pd2 = fitdist (data2, "Lognormal"); %! pd3 = fitdist (data3, "Lognormal"); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5", "mu = 0, sigma = 1", "mu = 0, sigma = 1.5"}, ... %! "location", "northeast") %! title ("Lognormal PDF") %! xlabel ("values in x (x > 0)") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Lognormal %! ## distributions, showing the likelihood for positive skewed values. statistics-release-2.1.2/inst/demos/prob.LognormalDistribution.plot000066400000000000000000000033151527311104300256020ustar00rootroot00000000000000%!demo %! ## Create a Lognormal distribution with fixed parameters mu = 0 and %! ## sigma = 1 and plot its PDF. %! %! rng (42); %! data = lognrnd (0, 1, 10000, 1); %! pd = fitdist (data, "Lognormal"); %! plot (pd) %! title ("Fixed Lognormal distribution with mu = 0 and sigma = 1") %!demo %! ## Generate a data set of 100 random samples from a Lognormal %! ## distribution with parameters mu = 0 and sigma = 1. Fit a Lognormal %! ## distribution to this data and plot its CDF superimposed over an empirical %! ## CDF. %! %! rng (42); %! data = lognrnd (0, 1, 100, 1); %! pd_fitted = fitdist (data, "Lognormal"); %! plot (pd_fitted, "PlotType", "cdf") %! txt = "Fitted Lognormal distribution with mu = %0.2f and sigma = %0.2f"; %! title (sprintf (txt, pd_fitted.mu, pd_fitted.sigma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit. %!demo %! ## Generate a data set of 200 random samples from a Lognormal %! ## distribution with parameters mu = 0 and sigma = 1. Display a probability %! ## plot for the Lognormal distribution fit to the data. %! %! rng (42); %! data = lognrnd (0, 1, 200, 1); %! pd_fitted = fitdist (data, "Lognormal"); %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted Lognormal", ... %! " distribution with mu = %0.2f and sigma = %0.2f"); %! title (sprintf (txt, pd_fitted.mu, pd_fitted.sigma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted distribution to the %! ## data, useful for checking if the Lognormal model is appropriate. statistics-release-2.1.2/inst/demos/prob.LognormalDistribution.proflik000066400000000000000000000006371527311104300262760ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the sigma parameter of a fitted %! ## Lognormal distribution %! rng (42); %! data = lognrnd (0, 1, 1000, 1); %! pd_fitted = fitdist (data, "Lognormal"); %! [nlogL, param] = proflik (pd_fitted, 2, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the sigma parameter, %! ## helping to understand the uncertainty in parameter estimates. statistics-release-2.1.2/inst/demos/prob.LognormalDistribution.random000066400000000000000000000006651527311104300261110ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Lognormal distribution %! rng (42); %! samples = lognrnd (0, 1, 500, 1); %! hist (samples, 50) %! title ("Histogram of 500 random samples from Lognormal(mu=0, sigma=1)") %! xlabel ("values in x (x > 0)") %! ylabel ("Frequency") %! %! ## This generates random samples from a Lognormal distribution, useful %! ## for simulating skewed positive data like asset prices or biological measurements. statistics-release-2.1.2/inst/demos/prob.LognormalDistribution.sigma000066400000000000000000000015021527311104300257200ustar00rootroot00000000000000%!demo %! ## Create a Lognormal distribution with default parameters %! rng (42); %! data = lognrnd (0, 1, 10000, 1); %! pd = fitdist (data, "Lognormal"); %! %! ## Query parameter 'sigma' (standard deviation of logarithmic values) %! pd.sigma %! %! ## Set parameter 'sigma' %! pd.sigma = 0.5 %! %! ## Use this to initialize or modify the sigma parameter in a Lognormal %! ## distribution. The sigma parameter must be a positive real scalar, controlling %! ## the skewness and spread of the distribution. %!demo %! ## Create a Lognormal distribution object by calling its constructor %! pd = LognormalDistribution (0, 1.5) %! %! ## Query parameter 'sigma' %! pd.sigma %! %! ## This shows how to set the sigma parameter directly via the constructor, %! ## ideal for modeling variability in positive skewed data, such as stock returns. statistics-release-2.1.2/inst/demos/prob.LognormalDistribution.std000066400000000000000000000005061527311104300254150ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Lognormal distribution %! rng (42); %! data = lognrnd (0, 1, 10000, 1); %! pd = fitdist (data, "Lognormal"); %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## in the positive skewed values of the distribution. statistics-release-2.1.2/inst/demos/prob.LognormalDistribution.truncate000066400000000000000000000017121527311104300264500ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Lognormal distribution, with parameters mu = 0 %! ## and sigma = 1, truncated at [0.5, 5] intervals. Generate 10000 random %! ## samples from this truncated distribution and superimpose a histogram scaled %! ## accordingly %! %! rng (42); %! data_all = lognrnd (0, 1, 20000, 1); %! data = data_all(data_all >= 0.5 & data_all <= 5); %! data = data(1:10000); %! %! pd = fitdist (data, "Lognormal"); %! t = truncate (pd, 0.5, 5); %! [counts, centers] = hist (data, 50); %! bin_width = centers(2) - centers(1); %! bar (centers, counts / (sum (counts) * bin_width), 1); %! hold on; %! %! x = linspace (0.5, 5, 500); %! y = pdf (t, x); %! plot (x, y, "r", "linewidth", 2); %! title ("Lognormal distribution (mu = 0, sigma = 1) truncated at [0.5, 5]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a Lognormal distribution to a specific %! ## range and visualizing the resulting distribution with random samples. statistics-release-2.1.2/inst/demos/prob.LognormalDistribution.var000066400000000000000000000004571527311104300254200ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Lognormal distribution %! rng (42); %! data = lognrnd (0, 1, 10000, 1); %! pd = fitdist (data, "Lognormal"); %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## positive skewed values in the distribution. statistics-release-2.1.2/inst/demos/prob.LoguniformDistribution.Lower000066400000000000000000000016131527311104300261020ustar00rootroot00000000000000%!demo %! ## Create a Log-uniform distribution with default parameters %! pd = LoguniformDistribution (); %! %! ## Query parameter 'Lower' (lower limit) %! pd.Lower %! %! ## Set parameter 'Lower' %! pd.Lower = 0.5 %! %! ## Use this to initialize or modify the lower limit of a Log-uniform %! ## distribution. The lower limit must be a positive real scalar, defining %! ## the minimum value in the support range, useful for modeling quantities %! ## spanning orders of magnitude, like particle sizes or frequencies. %!demo %! ## Create a Log-uniform distribution object by calling its constructor %! pd = LoguniformDistribution (0.1, 10) %! %! ## Query parameter 'Lower' %! pd.Lower %! %! ## This demonstrates direct construction with a specific lower limit, %! ## suitable for scenarios where the distribution starts from a small %! ## positive value, such as in Bayesian priors for scale parameters. statistics-release-2.1.2/inst/demos/prob.LoguniformDistribution.Upper000066400000000000000000000025561527311104300261140ustar00rootroot00000000000000%!demo %! ## Create a Log-uniform distribution with default parameters %! pd = LoguniformDistribution (); %! %! ## Query parameter 'Upper' (upper limit) %! pd.Upper %! %! ## Set parameter 'Upper' %! pd.Upper = 5 %! %! ## Use this to initialize or modify the upper limit of a Log-uniform %! ## distribution. The upper limit must be a positive real scalar greater %! ## than Lower, defining the maximum value in the support range. %!demo %! ## Create a Log-uniform distribution object by calling its constructor %! pd = LoguniformDistribution (1, 100) %! %! ## Query parameter 'Upper' %! pd.Upper %! %! ## This shows how to set the upper limit directly via the constructor, %! ## ideal for bounding the distribution in applications like parameter %! ## estimation over wide ranges. LoguniformDistribution: %!demo %! ## Create a Log-uniform distribution with default parameters (Lower=1, Upper=4) %! pd = LoguniformDistribution () %! %! ## This is the basic constructor call, useful for quick initialization %! ## with standard bounds for exploratory analysis. %!demo %! ## Create a Log-uniform distribution with specified parameters %! pd = LoguniformDistribution (0.01, 100) %! %! ## Use this constructor to define custom bounds, appropriate for modeling %! ## variables with logarithmic uniformity, such as in astrophysics or %! ## economics for quantities like incomes or sizes. statistics-release-2.1.2/inst/demos/prob.LoguniformDistribution.cdf000066400000000000000000000014051527311104300255450ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Log-uniform distribution %! x = 0:0.01:10; %! pd1 = LoguniformDistribution (1, 4); %! pd2 = LoguniformDistribution (1, 6); %! pd3 = LoguniformDistribution (1, 8); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"Lower=1, Upper=4", "Lower=1, Upper=6", "Lower=1, Upper=8"}, ... %! "location", "southeast") %! title ("Log-uniform CDF") %! xlabel ("values in x (Lower <= x <= Upper)") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Log-uniform distributions, showing how probability %! ## accumulates over the range, useful in uncertainty modeling. statistics-release-2.1.2/inst/demos/prob.LoguniformDistribution.icdf000066400000000000000000000013511527311104300257160ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Log-uniform distribution %! p = 0.001:0.001:0.999; %! pd1 = LoguniformDistribution (1, 4); %! pd2 = LoguniformDistribution (1, 6); %! pd3 = LoguniformDistribution (1, 8); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"Lower=1, Upper=4", "Lower=1, Upper=6", "Lower=1, Upper=8"}, ... %! "location", "northwest") %! title ("Log-uniform iCDF") %! xlabel ("Probability") %! ylabel ("values in x (Lower <= x <= Upper)") %! %! ## This demonstrates the inverse CDF (quantiles) for Log-uniform %! ## distributions, useful for finding values at specific probabilities, %! ## such as in Monte Carlo simulations. statistics-release-2.1.2/inst/demos/prob.LoguniformDistribution.iqr000066400000000000000000000005121527311104300256020ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Log-uniform distribution %! pd = LoguniformDistribution (1, 10); %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, measuring the spread %! ## of the middle 50% of the distribution on a log scale, helpful for %! ## robust statistics in skewed data. statistics-release-2.1.2/inst/demos/prob.LoguniformDistribution.mean000066400000000000000000000005601527311104300257320ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Log-uniform distributions %! pd1 = LoguniformDistribution (1, 5); %! pd2 = LoguniformDistribution (1, 10); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Log-uniform %! ## distributions with different upper limits, representing the average %! ## value over the range. statistics-release-2.1.2/inst/demos/prob.LoguniformDistribution.median000066400000000000000000000005451527311104300262520ustar00rootroot00000000000000%!demo %! ## Compute the median for different Log-uniform distributions %! pd1 = LoguniformDistribution (1, 5); %! pd2 = LoguniformDistribution (1, 10); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which is the geometric mean of %! ## the bounds, splitting the distribution into equal probability halves. statistics-release-2.1.2/inst/demos/prob.LoguniformDistribution.pdf000066400000000000000000000012761527311104300255700ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Log-uniform distribution %! x = 0.1:0.01:10; %! pd1 = LoguniformDistribution (1, 4); %! pd2 = LoguniformDistribution (1, 6); %! pd3 = LoguniformDistribution (1, 8); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"Lower=1, Upper=4", "Lower=1, Upper=6", "Lower=1, Upper=8"}, ... %! "location", "northeast") %! title ("Log-uniform PDF") %! xlabel ("values in x (Lower <= x <= Upper)") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Log-uniform %! ## distributions, showing the likelihood decreasing inversely with x. statistics-release-2.1.2/inst/demos/prob.LoguniformDistribution.plot000066400000000000000000000012741527311104300257730ustar00rootroot00000000000000%!demo %! ## Create a Log-uniform distribution with fixed parameters Lower=1 and %! ## Upper=4 and plot its PDF. %! pd = LoguniformDistribution (1, 4); %! plot (pd) %! title ("Fixed Log-uniform distribution with Lower=1 and Upper=4") %!demo %! ## Generate a data set of 100 random samples from a Log-uniform %! ## distribution with parameters Lower=1 and Upper=10. Plot its CDF. %! rng (42); %! data = exp (unifrnd (log (1), log (10), 100, 1)); %! pd = LoguniformDistribution (1, 10); %! plot (pd, "PlotType", "cdf") %! title ("Log-uniform distribution with Lower=1 and Upper=10") %! %! ## Use this to visualize the CDF, useful for understanding cumulative %! ## probabilities in the distribution. statistics-release-2.1.2/inst/demos/prob.LoguniformDistribution.random000066400000000000000000000007521527311104300262750ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Log-uniform distribution %! rng (42); %! pd = LoguniformDistribution (1, 10); %! samples = random (pd, 500, 1); %! hist (samples, 50) %! title ("Histogram of 500 random samples from Log-uniform(Lower=1, Upper=10)") %! xlabel ("values in x (Lower <= x <= Upper)") %! ylabel ("Frequency") %! %! ## This generates random samples from a Log-uniform distribution, useful %! ## for simulating data uniform on a log scale, like in power-law phenomena. statistics-release-2.1.2/inst/demos/prob.LoguniformDistribution.std000066400000000000000000000004011527311104300255760ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Log-uniform distribution %! pd = LoguniformDistribution (1, 10); %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, measuring the variability %! ## in the distribution's values. statistics-release-2.1.2/inst/demos/prob.LoguniformDistribution.truncate000066400000000000000000000020101527311104300266270ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Log-uniform distribution, with parameters Lower=1 %! ## and Upper=10, truncated at [2, 5] intervals. Generate 10000 random %! ## samples from this truncated distribution and superimpose a histogram scaled %! ## accordingly %! rng (42); %! data_all = exp (unifrnd (log (1), log (10), 20000, 1)); %! data = data_all(data_all >= 2 & data_all <= 5); %! data = data(1:7500); %! %! pd = LoguniformDistribution (1, 10); %! t = truncate (pd, 2, 5); %! %! [counts, centers] = hist (data, 50); %! bin_width = centers(2) - centers(1); %! bar (centers, counts / (sum (counts) * bin_width), 1); %! hold on; %! %! ## Plot histogram and truncated PDF %! x = linspace (0.5, 5, 500); %! y = pdf (t, x); %! plot (x, y, "r", "linewidth", 2); %! title ("Log-uniform distribution (Lower=1, Upper=10) truncated at [2, 5]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a Log-uniform distribution to a specific %! ## range and visualizing the resulting distribution with random samples. statistics-release-2.1.2/inst/demos/prob.LoguniformDistribution.var000066400000000000000000000003571527311104300256060ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Log-uniform distribution %! pd = LoguniformDistribution (1, 10); %! var_value = var (pd) %! %! ## Use this to calculate the variance, quantifying the spread of the %! ## values in the distribution. statistics-release-2.1.2/inst/demos/prob.MultinomialDistribution.Probabilities000066400000000000000000000024161527311104300277550ustar00rootroot00000000000000%!demo %! ## Create a Multinomial distribution with default parameters %! pd = MultinomialDistribution (); %! %! ## Query parameter 'Probabilities' (outcome probabilities) %! pd.Probabilities %! %! ## Use this to query the vector of probabilities for each outcome in a %! ## Multinomial distribution. The probabilities must sum to 1 and represent %! ## the likelihood of each category in a single trial. %!demo %! ## Create a Multinomial distribution with specified parameters %! pd = MultinomialDistribution ([0.1, 0.2, 0.3, 0.2, 0.1, 0.1]); %! %! ## Query parameter 'Probabilities' %! pd.Probabilities %! %! ## Set parameter 'Probabilities' %! pd.Probabilities = [0.4, 0.3, 0.3] %! %! ## Use this to initialize or modify the probabilities vector in a Multinomial %! ## distribution. The vector must be positive real scalars summing to 1, useful %! ## for modeling categorical outcomes like dice rolls or survey responses. %!demo %! ## Create a Multinomial distribution object by calling its constructor %! pd = MultinomialDistribution ([1/6, 1/6, 1/6, 1/6, 1/6, 1/6]); %! %! ## Query parameter 'Probabilities' %! pd.Probabilities %! %! ## This demonstrates direct construction with specific probabilities, %! ## ideal for modeling fair or biased categorical events, such as a dice roll. statistics-release-2.1.2/inst/demos/prob.MultinomialDistribution.cdf000066400000000000000000000014701527311104300257200ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Multinomial distribution %! x = 1:0.1:6; %! pd1 = MultinomialDistribution ([0.4, 0.3, 0.3]); %! pd2 = MultinomialDistribution ([0.2, 0.2, 0.2, 0.2, 0.2]); %! pd3 = MultinomialDistribution ([1/6, 1/6, 1/6, 1/6, 1/6, 1/6]); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"3 outcomes", "5 outcomes", "6 outcomes (dice)"}, ... %! "location", "southeast") %! title ("Multinomial CDF") %! xlabel ("values in x (outcome index)") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Multinomial distributions, showing how probability %! ## accumulates across categorical outcomes, useful in decision analysis. statistics-release-2.1.2/inst/demos/prob.MultinomialDistribution.icdf000066400000000000000000000014431527311104300260710ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Multinomial distribution %! p = 0.001:0.001:0.999; %! pd1 = MultinomialDistribution ([0.4, 0.3, 0.3]); %! pd2 = MultinomialDistribution ([0.2, 0.2, 0.2, 0.2, 0.2]); %! pd3 = MultinomialDistribution ([1/6, 1/6, 1/6, 1/6, 1/6, 1/6]); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"3 outcomes", "5 outcomes", "6 outcomes (dice)"}, ... %! "location", "northwest") %! title ("Multinomial iCDF") %! xlabel ("Probability") %! ylabel ("values in x (outcome index)") %! %! ## This demonstrates the inverse CDF (quantiles) for Multinomial %! ## distributions, useful for finding outcome thresholds corresponding to %! ## given probabilities, such as in risk assessment. statistics-release-2.1.2/inst/demos/prob.MultinomialDistribution.iqr000066400000000000000000000005631527311104300257610ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Multinomial distribution %! pd = MultinomialDistribution ([0.1, 0.2, 0.3, 0.2, 0.1, 0.1]); %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the categorical outcomes, helpful for understanding %! ## central variability in discrete data. statistics-release-2.1.2/inst/demos/prob.MultinomialDistribution.mean000066400000000000000000000006361527311104300261070ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Multinomial distributions %! pd1 = MultinomialDistribution ([0.4, 0.3, 0.3]); %! pd2 = MultinomialDistribution ([0.2, 0.2, 0.2, 0.2, 0.2]); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Multinomial %! ## distributions with different probabilities, representing the average %! ## outcome index in categorical data. statistics-release-2.1.2/inst/demos/prob.MultinomialDistribution.median000066400000000000000000000006111527311104300264150ustar00rootroot00000000000000%!demo %! ## Compute the median for different Multinomial distributions %! pd1 = MultinomialDistribution ([0.4, 0.3, 0.3]); %! pd2 = MultinomialDistribution ([0.2, 0.2, 0.2, 0.2, 0.2]); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median outcome index, which splits the distribution %! ## into two equal probability halves, robust to uneven probabilities. statistics-release-2.1.2/inst/demos/prob.MultinomialDistribution.pdf000066400000000000000000000013301527311104300257300ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Multinomial distribution %! x = 1:0.1:6; %! pd1 = MultinomialDistribution ([0.4, 0.3, 0.3]); %! pd2 = MultinomialDistribution ([0.2, 0.2, 0.2, 0.2, 0.2]); %! pd3 = MultinomialDistribution ([1/6, 1/6, 1/6, 1/6, 1/6, 1/6]); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"3 outcomes", "5 outcomes", "6 outcomes (dice)"}, ... %! "location", "northeast") %! title ("Multinomial PDF") %! xlabel ("values in x (outcome index)") %! ylabel ("Probability") %! %! ## This visualizes the probability mass function for Multinomial %! ## distributions, showing the likelihood for each discrete outcome. statistics-release-2.1.2/inst/demos/prob.MultinomialDistribution.plot000066400000000000000000000010541527311104300261400ustar00rootroot00000000000000%!demo %! ## Create a Multinomial distribution with fixed parameters and plot its PDF. %! pd = MultinomialDistribution ([0.1, 0.2, 0.3, 0.2, 0.1, 0.1]); %! plot (pd) %! title ("Multinomial distribution PDF") %!demo %! ## Create a Multinomial distribution and plot its CDF. %! pd = MultinomialDistribution ([0.1, 0.2, 0.3, 0.2, 0.1, 0.1]); %! plot (pd, "PlotType", "cdf") %! title ("Multinomial distribution CDF") %! %! ## Use this to visualize the cumulative distribution function, %! ## useful for understanding probability accumulation across outcomes. statistics-release-2.1.2/inst/demos/prob.MultinomialDistribution.random000066400000000000000000000007631527311104300264500ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Multinomial distribution %! rng (42); %! pd = MultinomialDistribution ([1/6, 1/6, 1/6, 1/6, 1/6, 1/6]); %! samples = random (pd, 500, 1); %! hist (samples, 6) %! title ("Histogram of 500 random samples from Multinomial (fair dice)") %! xlabel ("values in x (outcome index)") %! ylabel ("Frequency") %! %! ## This generates random categorical outcomes from a Multinomial %! ## distribution, useful for simulating events like dice rolls or classifications. statistics-release-2.1.2/inst/demos/prob.MultinomialDistribution.std000066400000000000000000000004561527311104300257610ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Multinomial distribution %! pd = MultinomialDistribution ([0.1, 0.2, 0.3, 0.2, 0.1, 0.1]); %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## in the outcome indices for categorical data. statistics-release-2.1.2/inst/demos/prob.MultinomialDistribution.truncate000066400000000000000000000015011527311104300270040ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Multinomial distribution truncated at [2, 5] intervals. %! ## Generate 10000 random samples from this truncated distribution and %! ## superimpose a histogram. %! %! rng (42); %! pd = MultinomialDistribution ([0.1, 0.2, 0.3, 0.2, 0.1, 0.1]); %! t = truncate (pd, 2, 5); %! data_all = random (pd, 20000, 1); %! data = data_all(data_all >= 2 & data_all <= 5); %! data = data(1:10000); %! %! ## Plot histogram and truncated PDF %! x = 2:5; %! y = pdf (t, x); %! plot (x, y * numel (data), "bo-") %! hold on %! hist (data, 4) %! hold off %! title ("Multinomial distribution truncated at [2, 5]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a Multinomial distribution to a specific %! ## range of outcomes and visualizing the resulting distribution with random samples. statistics-release-2.1.2/inst/demos/prob.MultinomialDistribution.var000066400000000000000000000004261527311104300257540ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Multinomial distribution %! pd = MultinomialDistribution ([0.1, 0.2, 0.3, 0.2, 0.1, 0.1]); %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## outcome indices in the distribution. statistics-release-2.1.2/inst/demos/prob.NakagamiDistribution.cdf000066400000000000000000000016111527311104300251330ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Nakagami distribution %! randg ('state', 42); %! x = -1:0.01:5; %! data1 = nakarnd (0.6, 1, 10000, 1); %! data2 = nakarnd (1, 1, 10000, 1); %! data3 = nakarnd (2, 1, 10000, 1); %! pd1 = fitdist (data1, "Nakagami"); %! pd2 = fitdist (data2, "Nakagami"); %! pd3 = fitdist (data3, "Nakagami"); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"mu = 0.6, omega = 1", "mu = 1, omega = 1", "mu = 2, omega = 1"}, ... %! "location", "southeast") %! title ("Nakagami CDF") %! xlabel ("values in x (x >= 0)") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Nakagami distributions, showing how probability accumulates %! ## for signal amplitudes, useful in wireless communications analysis. statistics-release-2.1.2/inst/demos/prob.NakagamiDistribution.icdf000066400000000000000000000015661527311104300253150ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Nakagami distribution %! randg ('state', 42); %! p = 0.001:0.001:0.999; %! data1 = nakarnd (0.6, 1, 10000, 1); %! data2 = nakarnd (1, 1, 10000, 1); %! data3 = nakarnd (2, 1, 10000, 1); %! pd1 = fitdist (data1, "Nakagami"); %! pd2 = fitdist (data2, "Nakagami"); %! pd3 = fitdist (data3, "Nakagami"); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"mu = 0.6, omega = 1", "mu = 1, omega = 1", "mu = 2, omega = 1"}, ... %! "location", "northwest") %! title ("Nakagami iCDF") %! xlabel ("Probability") %! ylabel ("values in x (x >= 0)") %! %! ## This demonstrates the inverse CDF (quantiles) for Nakagami distributions, %! ## useful for finding amplitude thresholds corresponding to given probabilities, %! ## such as in fading channel simulations. statistics-release-2.1.2/inst/demos/prob.NakagamiDistribution.iqr000066400000000000000000000006021527311104300251710ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Nakagami distribution %! randg ('state', 42); %! data = nakarnd (1, 1, 10000, 1); %! pd = fitdist (data, "Nakagami"); %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, helpful for understanding variability %! ## in signal amplitudes. statistics-release-2.1.2/inst/demos/prob.NakagamiDistribution.mean000066400000000000000000000006761527311104300253310ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Nakagami distributions %! randg ('state', 42); %! data1 = nakarnd (0.6, 1, 10000, 1); %! data2 = nakarnd (1, 1, 10000, 1); %! pd1 = fitdist (data1, "Nakagami"); %! pd2 = fitdist (data2, "Nakagami"); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Nakagami distributions %! ## with different shape parameters, representing average signal amplitude. statistics-release-2.1.2/inst/demos/prob.NakagamiDistribution.median000066400000000000000000000007021527311104300256340ustar00rootroot00000000000000%!demo %! ## Compute the median for different Nakagami distributions %! randg ('state', 42); %! data1 = nakarnd (0.6, 1, 10000, 1); %! data2 = nakarnd (1, 1, 10000, 1); %! pd1 = fitdist (data1, "Nakagami"); %! pd2 = fitdist (data2, "Nakagami"); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which splits the distribution into %! ## two equal probability halves, robust to skewness in amplitude data. statistics-release-2.1.2/inst/demos/prob.NakagamiDistribution.mu000066400000000000000000000014551527311104300250260ustar00rootroot00000000000000%!demo %! ## Create a Nakagami distribution with default parameters %! randg ('state', 42); %! data = nakarnd (1, 1, 10000, 1); %! pd = fitdist (data, "Nakagami"); %! %! ## Query parameter 'mu' (shape parameter) %! pd.mu %! %! ## Set parameter 'mu' %! pd.mu = 2 %! %! ## Use this to initialize or modify the shape parameter of a Nakagami %! ## distribution. The shape parameter must be a real scalar >= 0.5, controlling %! ## the fading severity in signal modeling; higher mu indicates less fading. %!demo %! ## Create a Nakagami distribution object by calling its constructor %! pd = NakagamiDistribution (1.5, 2) %! %! ## Query parameter 'mu' %! pd.mu %! %! ## This demonstrates direct construction with a specific shape parameter, %! ## useful for modeling wireless channel fading with known characteristics. statistics-release-2.1.2/inst/demos/prob.NakagamiDistribution.negloglik000066400000000000000000000007011527311104300263510ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Nakagami distribution %! rng (42); %! randg ('state', 42); %! data = nakarnd (1, 1, 100, 1); %! pd_fitted = fitdist (data, "Nakagami"); %! params = [pd_fitted.mu, pd_fitted.omega]; %! nlogL_nakalike = nakalike (params, data) %! %! ## This is useful for assessing the fit of a Nakagami distribution to data, %! ## with lower values indicating a better fit, often used in model comparison. statistics-release-2.1.2/inst/demos/prob.NakagamiDistribution.omega000066400000000000000000000014511527311104300254710ustar00rootroot00000000000000%!demo %! ## Create a Nakagami distribution with default parameters %! randg ('state', 42); %! data = nakarnd (1, 1, 10000, 1); %! pd = fitdist (data, "Nakagami"); %! %! ## Query parameter 'omega' (spread parameter) %! pd.omega %! %! ## Set parameter 'omega' %! pd.omega = 2 %! %! ## Use this to initialize or modify the spread parameter in a Nakagami %! ## distribution. The spread parameter must be a positive real scalar, representing %! ## the average power in signal amplitude modeling. %!demo %! ## Create a Nakagami distribution object by calling its constructor %! pd = NakagamiDistribution (1, 1.5) %! %! ## Query parameter 'omega' %! pd.omega %! %! ## This shows how to set the spread parameter directly via the constructor, %! ## ideal for simulating signal envelopes with specified power levels. statistics-release-2.1.2/inst/demos/prob.NakagamiDistribution.paramci000066400000000000000000000006231527311104300260150ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Nakagami distribution %! rng (42); %! randg ('state', 42); %! data = nakarnd (1, 1, 1000, 1); %! pd_fitted = fitdist (data, "Nakagami"); %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameters (mu and %! ## omega), providing a range of plausible values given the data. statistics-release-2.1.2/inst/demos/prob.NakagamiDistribution.pdf000066400000000000000000000014471527311104300251570ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Nakagami distribution %! randg ('state', 42); %! x = -1:0.01:5; %! data1 = nakarnd (0.6, 1, 10000, 1); %! data2 = nakarnd (1, 1, 10000, 1); %! data3 = nakarnd (2, 1, 10000, 1); %! pd1 = fitdist (data1, "Nakagami"); %! pd2 = fitdist (data2, "Nakagami"); %! pd3 = fitdist (data3, "Nakagami"); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"mu = 0.6, omega = 1", "mu = 1, omega = 1", "mu = 2, omega = 1"}, ... %! "location", "northeast") %! title ("Nakagami PDF") %! xlabel ("values in x (x >= 0)") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Nakagami distributions, %! ## showing the likelihood for signal amplitudes. statistics-release-2.1.2/inst/demos/prob.NakagamiDistribution.plot000066400000000000000000000033741527311104300253650ustar00rootroot00000000000000%!demo %! ## Create a Nakagami distribution with fixed parameters mu = 1 and %! ## omega = 1 and plot its PDF. %! %! randg ('state', 42); %! data = nakarnd (1, 1, 10000, 1); %! pd = fitdist (data, "Nakagami"); %! plot (pd) %! title ("Fixed Nakagami distribution with mu = 1 and omega = 1") %!demo %! ## Generate a data set of 100 random samples from a Nakagami %! ## distribution with parameters mu = 1 and omega = 1. Fit a Nakagami %! ## distribution to this data and plot its CDF superimposed over an empirical %! ## CDF. %! %! rng (42); %! randg ('state', 42); %! data = nakarnd (1, 1, 100, 1); %! pd_fitted = fitdist (data, "Nakagami"); %! plot (pd_fitted, "PlotType", "cdf") %! txt = "Fitted Nakagami distribution with mu = %0.2f and omega = %0.2f"; %! title (sprintf (txt, pd_fitted.mu, pd_fitted.omega)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit. %!demo %! ## Generate a data set of 200 random samples from a Nakagami %! ## distribution with parameters mu = 1 and omega = 1. Display a probability %! ## plot for the Nakagami distribution fit to the data. %! %! rng (42); %! randg ('state', 42); %! data = nakarnd (1, 1, 200, 1); %! pd_fitted = fitdist (data, "Nakagami"); %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted Nakagami", ... %! " distribution with mu = %0.2f and omega = %0.2f"); %! title (sprintf (txt, pd_fitted.mu, pd_fitted.omega)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted distribution to the %! ## data, useful for checking if the Nakagami model is appropriate. statistics-release-2.1.2/inst/demos/prob.NakagamiDistribution.proflik000066400000000000000000000006721527311104300260530ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the shape parameter of a fitted %! ## Nakagami distribution %! rng (42); %! randg ('state', 42); %! data = nakarnd (1, 1, 1000, 1); %! pd_fitted = fitdist (data, "Nakagami"); %! [nlogL, param] = proflik (pd_fitted, 1, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the shape parameter (mu), %! ## helping to understand the uncertainty in parameter estimates. statistics-release-2.1.2/inst/demos/prob.NakagamiDistribution.random000066400000000000000000000006771527311104300256720ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Nakagami distribution %! rng (42); %! randg ('state', 42); %! samples = nakarnd (1, 1, 500, 1); %! hist (samples, 50) %! title ("Histogram of 500 random samples from Nakagami(mu=1, omega=1)") %! xlabel ("values in x (x >= 0)") %! ylabel ("Frequency") %! %! ## This generates random samples from a Nakagami distribution, useful for %! ## simulating fading channels or signal envelopes in communications. statistics-release-2.1.2/inst/demos/prob.NakagamiDistribution.std000066400000000000000000000004621527311104300251740ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Nakagami distribution %! randg ('state', 42); %! data = nakarnd (1, 1, 10000, 1); %! pd = fitdist (data, "Nakagami"); %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## in signal amplitudes. statistics-release-2.1.2/inst/demos/prob.NakagamiDistribution.truncate000066400000000000000000000020051527311104300262220ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Nakagami distribution, with parameters mu = 1 %! ## and omega = 1, truncated at [0.5, 2] intervals. Generate 10000 random %! ## samples from this truncated distribution and superimpose a histogram scaled %! ## accordingly %! %! rng (42); %! randg ('state', 42); %! data_all = nakarnd (1, 1, 20000, 1); %! data = data_all(data_all >= 0.5 & data_all <= 2); %! data = data(1:10000); %! %! pd = fitdist (data, "Nakagami"); %! t = truncate (pd, 0.5, 2); %! [counts, centers] = hist (data, 50); %! bin_width = centers(2) - centers(1); %! bar (centers, counts / (sum (counts) * bin_width), 1); %! hold on; %! %! ## Plot histogram and truncated PDF %! x = linspace (0.5, 5, 500); %! y = pdf (t, x); %! plot (x, y, "r", "linewidth", 2); %! title ("Nakagami distribution (mu = 1, omega = 1) truncated at [0.5, 2]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a Nakagami distribution to a specific range %! ## and visualizing the resulting distribution with random samples. statistics-release-2.1.2/inst/demos/prob.NakagamiDistribution.var000066400000000000000000000004631527311104300251730ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Nakagami distribution %! randg ('state', 42); %! data = nakarnd (1, 1, 10000, 1); %! pd = fitdist (data, "Nakagami"); %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## signal amplitudes in the distribution. statistics-release-2.1.2/inst/demos/prob.NegativeBinomialDistribution.P000066400000000000000000000015031527311104300263030ustar00rootroot00000000000000%!demo %! ## Create a Negative Binomial distribution with default parameters %! randg ('state', 42); %! randp ('state', 42); %! data = nbinrnd(5, 0.5, 10000, 1); %! pd = fitdist (data, "NegativeBinomial"); %! %! ## Query parameter 'P' (probability of success) %! pd.P %! %! ## Set parameter 'P' %! pd.P = 0.3 %! %! ## Use this to initialize or modify the success probability in a Negative %! ## Binomial distribution. P must be a real scalar in (0,1], influencing the %! ## mean and variance of the distribution. %!demo %! ## Create a Negative Binomial distribution object by calling its constructor %! pd = NegativeBinomialDistribution(5, 0.3) %! %! ## Query parameter 'P' %! pd.P %! %! ## This shows how to set the success probability directly via the constructor, %! ## ideal for modeling varying success rates in repeated trials. statistics-release-2.1.2/inst/demos/prob.NegativeBinomialDistribution.R000066400000000000000000000016521527311104300263120ustar00rootroot00000000000000%!demo %! ## Create a Negative Binomial distribution with default parameters %! randg ('state', 42); %! randp ('state', 42); %! data = nbinrnd(5, 0.5, 10000, 1); %! pd = fitdist (data, "NegativeBinomial"); %! %! ## Query parameter 'R' (number of successes) %! pd.R %! %! ## Set parameter 'R' %! pd.R = 10 %! %! ## Use this to initialize or modify the number of successes parameter in a %! ## Negative Binomial distribution. R must be a positive scalar, controlling %! ## the shape and often fixed based on the problem context, like successes in trials. %!demo %! ## Create a Negative Binomial distribution object by calling its constructor %! pd = NegativeBinomialDistribution(10, 0.3) %! %! ## Query parameter 'R' %! pd.R %! %! ## This demonstrates direct construction with a specific number of successes, %! ## useful for modeling scenarios with a known fixed number of events, such as %! ## in reliability testing or count processes. statistics-release-2.1.2/inst/demos/prob.NegativeBinomialDistribution.cdf000066400000000000000000000016541527311104300266470ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Negative Binomial distribution %! randg ('state', 42); %! randp ('state', 42); %! x = 0:20; %! data1 = nbinrnd(5, 0.3, 10000, 1); %! data2 = nbinrnd(5, 0.5, 10000, 1); %! data3 = nbinrnd(5, 0.7, 10000, 1); %! pd1 = fitdist (data1, "NegativeBinomial"); %! pd2 = fitdist (data2, "NegativeBinomial"); %! pd3 = fitdist (data3, "NegativeBinomial"); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"R=5, P=0.3", "R=5, P=0.5", "R=5, P=0.7"}, "location", "southeast") %! title ("Negative Binomial CDF") %! xlabel ("values in x (number of failures)") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Negative Binomial distributions, showing how probability %! ## accumulates for count data, useful in modeling overdispersed counts. statistics-release-2.1.2/inst/demos/prob.NegativeBinomialDistribution.icdf000066400000000000000000000016551527311104300270210ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Negative Binomial distribution %! randg ('state', 42); %! randp ('state', 42); %! p = 0.001:0.001:0.999; %! data1 = nbinrnd(5, 0.3, 10000, 1); %! data2 = nbinrnd(5, 0.5, 10000, 1); %! data3 = nbinrnd(5, 0.7, 10000, 1); %! pd1 = fitdist (data1, "NegativeBinomial"); %! pd2 = fitdist (data2, "NegativeBinomial"); %! pd3 = fitdist (data3, "NegativeBinomial"); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"R=5, P=0.3", "R=5, P=0.5", "R=5, P=0.7"}, "location", "northwest") %! title ("Negative Binomial iCDF") %! xlabel ("Probability") %! ylabel ("values in x (number of failures)") %! %! ## This demonstrates the inverse CDF (quantiles) for Negative Binomial %! ## distributions, useful for finding count values corresponding to given %! ## probabilities, such as in risk assessment or inventory thresholds. statistics-release-2.1.2/inst/demos/prob.NegativeBinomialDistribution.iqr000066400000000000000000000006451527311104300267050ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Negative Binomial distribution %! randg ('state', 42); %! randp ('state', 42); %! data = nbinrnd(5, 0.5, 10000, 1); %! pd = fitdist (data, "NegativeBinomial"); %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, helpful for understanding variability %! ## in count data. statistics-release-2.1.2/inst/demos/prob.NegativeBinomialDistribution.mean000066400000000000000000000010051527311104300270210ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Negative Binomial distributions %! randg ('state', 42); %! randp ('state', 42); %! data1 = nbinrnd(5, 0.3, 10000, 1); %! data2 = nbinrnd(5, 0.5, 10000, 1); %! pd1 = fitdist (data1, "NegativeBinomial"); %! pd2 = fitdist (data2, "NegativeBinomial"); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Negative Binomial %! ## distributions with different success probabilities, representing average %! ## number of failures. statistics-release-2.1.2/inst/demos/prob.NegativeBinomialDistribution.median000066400000000000000000000007571527311104300273530ustar00rootroot00000000000000%!demo %! ## Compute the median for different Negative Binomial distributions %! randg ('state', 42); %! randp ('state', 42); %! data1 = nbinrnd(5, 0.3, 10000, 1); %! data2 = nbinrnd(5, 0.5, 10000, 1); %! pd1 = fitdist (data1, "NegativeBinomial"); %! pd2 = fitdist (data2, "NegativeBinomial"); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which splits the distribution %! ## into two equal probability halves, robust to skewness in count data. statistics-release-2.1.2/inst/demos/prob.NegativeBinomialDistribution.negloglik000066400000000000000000000007571527311104300300710ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Negative Binomial distribution %! rng (42); %! randg ('state', 42); %! randp ('state', 42); %! data = nbinrnd(5, 0.5, 100, 1); %! pd_fitted = fitdist (data, "NegativeBinomial"); %! params = [pd_fitted.R, pd_fitted.P]; %! nlogL_nbinlike = nbinlike (params, data) %! %! ## This is useful for assessing the fit of a Negative Binomial distribution to %! ## data, with lower values indicating a better fit, often used in model comparison. statistics-release-2.1.2/inst/demos/prob.NegativeBinomialDistribution.paramci000066400000000000000000000006761527311104300275320ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Negative Binomial %! ## distribution %! rng (42); %! randg ('state', 42); %! randp ('state', 42); %! data = nbinrnd(5, 0.5, 1000, 1); %! pd_fitted = fitdist (data, "NegativeBinomial"); %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameters (R and P), %! ## providing a range of plausible values given the data. statistics-release-2.1.2/inst/demos/prob.NegativeBinomialDistribution.pdf000066400000000000000000000015251527311104300266610ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Negative Binomial distribution %! randg ('state', 42); %! randp ('state', 42); %! x = 0:20; %! data1 = nbinrnd(5, 0.3, 10000, 1); %! data2 = nbinrnd(5, 0.5, 10000, 1); %! data3 = nbinrnd(5, 0.7, 10000, 1); %! pd1 = fitdist (data1, "NegativeBinomial"); %! pd2 = fitdist (data2, "NegativeBinomial"); %! pd3 = fitdist (data3, "NegativeBinomial"); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"R=5, P=0.3", "R=5, P=0.5", "R=5, P=0.7"}, "location", "northeast") %! title ("Negative Binomial PDF") %! xlabel ("values in x (number of failures)") %! ylabel ("Probability density") %! %! ## This visualizes the probability mass function for Negative Binomial %! ## distributions, showing the likelihood for discrete count values. statistics-release-2.1.2/inst/demos/prob.NegativeBinomialDistribution.plot000066400000000000000000000036061527311104300270700ustar00rootroot00000000000000%!demo %! ## Create a Negative Binomial distribution with fixed parameters R=5 and %! ## P=0.5 and plot its PDF. %! %! randg ('state', 42); %! randp ('state', 42); %! data = nbinrnd(5, 0.5, 10000, 1); %! pd = fitdist (data, "NegativeBinomial"); %! plot (pd) %! title ("Fixed Negative Binomial distribution with R=5 and P=0.5") %!demo %! ## Generate a data set of 100 random samples from a Negative Binomial %! ## distribution with parameters R=5 and P=0.5. Fit a Negative Binomial %! ## distribution to this data and plot its CDF superimposed over an empirical %! ## CDF. %! %! rng (42); %! randg ('state', 42); %! randp ('state', 42); %! data = nbinrnd(5, 0.5, 100, 1); %! pd_fitted = fitdist (data, "NegativeBinomial"); %! plot (pd_fitted, "PlotType", "cdf") %! txt = "Fitted Negative Binomial distribution with R=%0.2f and P=%0.2f"; %! title (sprintf (txt, pd_fitted.R, pd_fitted.P)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit in count data. %!demo %! ## Generate a data set of 200 random samples from a Negative Binomial %! ## distribution with parameters R=5 and P=0.5. Display a probability %! ## plot for the Negative Binomial distribution fit to the data. %! %! rng (42); %! randg ('state', 42); %! randp ('state', 42); %! data = nbinrnd(5, 0.5, 200, 1); %! pd_fitted = fitdist (data, "NegativeBinomial"); %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted Negative Binomial", ... %! " distribution with R=%0.2f and P=%0.2f"); %! title (sprintf (txt, pd_fitted.R, pd_fitted.P)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted distribution to the %! ## data, useful for checking if the Negative Binomial model is appropriate. statistics-release-2.1.2/inst/demos/prob.NegativeBinomialDistribution.proflik000066400000000000000000000007651527311104300275630ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the success probability parameter %! ## of a fitted Negative Binomial distribution %! rng (42); %! randg ('state', 42); %! randp ('state', 42); %! data = nbinrnd(5, 0.5, 1000, 1); %! pd_fitted = fitdist (data, "NegativeBinomial"); %! [nlogL, param] = proflik (pd_fitted, 2, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the success probability (P), %! ## helping to understand the uncertainty in parameter estimates. statistics-release-2.1.2/inst/demos/prob.NegativeBinomialDistribution.random000066400000000000000000000010511527311104300273620ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Negative Binomial distribution %! rng (42); %! randg ('state', 42); %! randp ('state', 42); %! samples = random (NegativeBinomialDistribution(5, 0.5), 500, 1); %! hist (samples, 50) %! title ("Histogram of 500 random samples from NegativeBinomial(R=5, P=0.5)") %! xlabel ("values in x (number of failures)") %! ylabel ("Frequency") %! %! ## This generates random samples from a Negative Binomial distribution, useful %! ## for simulating count data like failures in trials or overdispersed Poisson processes. statistics-release-2.1.2/inst/demos/prob.NegativeBinomialDistribution.std000066400000000000000000000005571527311104300267060ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Negative Binomial distribution %! randg ('state', 42); %! randp ('state', 42); %! data = nbinrnd(5, 0.5, 10000, 1); %! pd = fitdist (data, "NegativeBinomial"); %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## in the count values of the distribution. statistics-release-2.1.2/inst/demos/prob.NegativeBinomialDistribution.truncate000066400000000000000000000017171527311104300277400ustar00rootroot00000000000000%!demo %! ## Plot the PMF of a Negative Binomial distribution, with parameters R = 5 %! ## and P = 0.5, truncated at [2, 10]. Generate 10000 random samples from %! ## this truncated distribution and superimpose a histogram of their relative %! ## frequencies. %! %! rng (42); %! randg ('state', 42); %! randp ('state', 42); %! pd = makedist ("NegativeBinomial", "R", 5, "P", 0.5); %! t = truncate (pd, 2, 10); %! data = random (t, 10000, 1); %! %! ## Relative frequency of each value from 2 to 10 %! centers = 2:10; %! probs = histc (data, centers) / numel (data); %! %! bar (centers, probs, 0.5, "facecolor", [0.6 0.6 1]); %! hold on %! plot (centers, pdf (t, centers), 'r-', "linewidth", 2); %! hold off %! title ("Negative Binomial distribution (R = 5, P = 0.5) truncated at [2, 10]") %! legend ("Histogram", "Truncated PMF") %! %! ## This demonstrates truncating a Negative Binomial distribution to a %! ## specific range and checking the result against random samples. statistics-release-2.1.2/inst/demos/prob.NegativeBinomialDistribution.var000066400000000000000000000005301527311104300266730ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Negative Binomial distribution %! randg ('state', 42); %! randp ('state', 42); %! data = nbinrnd(5, 0.5, 10000, 1); %! pd = fitdist (data, "NegativeBinomial"); %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## count values in the distribution. statistics-release-2.1.2/inst/demos/prob.NormalDistribution.cdf000066400000000000000000000015441527311104300246600ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Normal distribution %! rng (42); %! x = -5:0.01:5; %! data1 = 0 + 0.5 * randn (10000, 1); %! data2 = 0 + 1.0 * randn (10000, 1); %! data3 = 0 + 2.0 * randn (10000, 1); %! pd1 = fitdist (data1, "Normal"); %! pd2 = fitdist (data2, "Normal"); %! pd3 = fitdist (data3, "Normal"); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5", "mu = 0, sigma = 1", "mu = 0, sigma = 2"}, ... %! "location", "southeast") %! title ("Normal CDF") %! xlabel ("values in x") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Normal distributions, showing how probability accumulates, %! ## essential in hypothesis testing or confidence intervals. statistics-release-2.1.2/inst/demos/prob.NormalDistribution.icdf000066400000000000000000000014771527311104300250360ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Normal distribution %! rng (42); %! p = 0.001:0.001:0.999; %! data1 = 0 + 0.5 * randn (10000, 1); %! data2 = 0 + 1.0 * randn (10000, 1); %! data3 = 0 + 2.0 * randn (10000, 1); %! pd1 = fitdist (data1, "Normal"); %! pd2 = fitdist (data2, "Normal"); %! pd3 = fitdist (data3, "Normal"); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5", "mu = 0, sigma = 1", "mu = 0, sigma = 2"}, ... %! "location", "northwest") %! title ("Normal iCDF") %! xlabel ("Probability") %! ylabel ("values in x") %! %! ## This demonstrates the inverse CDF (quantiles) for Normal %! ## distributions, useful for finding critical values in statistics, %! ## such as z-scores for confidence levels. statistics-release-2.1.2/inst/demos/prob.NormalDistribution.iqr000066400000000000000000000005171527311104300247160ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Normal distribution %! rng (42); %! data = randn (10000, 1); %! pd = fitdist (data, "Normal"); %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, robust to outliers in symmetric data. statistics-release-2.1.2/inst/demos/prob.NormalDistribution.mean000066400000000000000000000006461527311104300250460ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Normal distributions %! rng (42); %! data1 = 0 + 0.5 * randn (10000, 1); %! data2 = 0 + 1.0 * randn (10000, 1); %! pd1 = fitdist (data1, "Normal"); %! pd2 = fitdist (data2, "Normal"); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Normal %! ## distributions with different scale parameters, representing the average value. statistics-release-2.1.2/inst/demos/prob.NormalDistribution.median000066400000000000000000000006531527311104300253610ustar00rootroot00000000000000%!demo %! ## Compute the median for different Normal distributions %! rng (42); %! data1 = 0 + 0.5 * randn (10000, 1); %! data2 = 0 + 1.0 * randn (10000, 1); %! pd1 = fitdist (data1, "Normal"); %! pd2 = fitdist (data2, "Normal"); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which equals the mean in symmetric %! ## Normal distributions, useful for central tendency measures. statistics-release-2.1.2/inst/demos/prob.NormalDistribution.mu000066400000000000000000000014521527311104300245430ustar00rootroot00000000000000%!demo %! ## Create a Normal distribution with default parameters %! rng (42); %! data = randn (10000, 1); %! pd = fitdist (data, "Normal"); %! %! ## Query parameter 'mu' (location parameter, mean) %! pd.mu %! %! ## Set parameter 'mu' %! pd.mu = 1 %! %! ## Use this to initialize or modify the mean of a Normal distribution. %! ## The mean parameter must be a real scalar, representing the center of symmetry, %! ## useful for shifting the distribution, such as modeling centered data like errors. %!demo %! ## Create a Normal distribution object by calling its constructor %! pd = NormalDistribution (1.5, 2) %! %! ## Query parameter 'mu' %! pd.mu %! %! ## This demonstrates direct construction with a specific mean, %! ## suitable for modeling data with a known central tendency, like IQ scores or heights. statistics-release-2.1.2/inst/demos/prob.NormalDistribution.negloglik000066400000000000000000000006421527311104300260750ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Normal distribution %! rng (42); %! data = randn (100, 1); %! pd_fitted = fitdist (data, "Normal"); %! params = [pd_fitted.mu, pd_fitted.sigma]; %! nlogL_normlike = normlike (params, data) %! %! ## This is useful for assessing the fit of a Normal distribution to %! ## data, with lower values indicating a better fit, often used in optimization or AIC/BIC. statistics-release-2.1.2/inst/demos/prob.NormalDistribution.paramci000066400000000000000000000005651527311104300255420ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Normal %! ## distribution %! rng (42); %! data = randn (1000, 1); %! pd_fitted = fitdist (data, "Normal"); %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameters (mu %! ## and sigma), providing a range of plausible values given the data. statistics-release-2.1.2/inst/demos/prob.NormalDistribution.pdf000066400000000000000000000014171527311104300246740ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Normal distribution %! rng (42); %! x = -5:0.01:5; %! data1 = 0 + 0.5 * randn (10000, 1); %! data2 = 0 + 1.0 * randn (10000, 1); %! data3 = 0 + 2.0 * randn (10000, 1); %! pd1 = fitdist (data1, "Normal"); %! pd2 = fitdist (data2, "Normal"); %! pd3 = fitdist (data3, "Normal"); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5", "mu = 0, sigma = 1", "mu = 0, sigma = 2"}, ... %! "location", "northeast") %! title ("Normal PDF") %! xlabel ("values in x") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Normal %! ## distributions, showing the bell-shaped curve for symmetric data. statistics-release-2.1.2/inst/demos/prob.NormalDistribution.plot000066400000000000000000000032371527311104300251030ustar00rootroot00000000000000%!demo %! ## Create a Normal distribution with fixed parameters mu = 0 and %! ## sigma = 1 and plot its PDF. %! %! rng (42); %! data = randn (10000, 1); %! pd = fitdist (data, "Normal"); %! plot (pd) %! title ("Fixed Normal distribution with mu = 0 and sigma = 1") %!demo %! ## Generate a data set of 100 random samples from a Normal %! ## distribution with parameters mu = 0 and sigma = 1. Fit a Normal %! ## distribution to this data and plot its CDF superimposed over an empirical %! ## CDF. %! %! rng (42); %! data = randn (100, 1); %! pd_fitted = fitdist (data, "Normal"); %! plot (pd_fitted, "PlotType", "cdf") %! txt = "Fitted Normal distribution with mu = %0.2f and sigma = %0.2f"; %! title (sprintf (txt, pd_fitted.mu, pd_fitted.sigma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit in symmetric distributions. %!demo %! ## Generate a data set of 200 random samples from a Normal %! ## distribution with parameters mu = 0 and sigma = 1. Display a probability %! ## plot for the Normal distribution fit to the data. %! %! rng (42); %! data = randn (200, 1); %! pd_fitted = fitdist (data, "Normal"); %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted Normal", ... %! " distribution with mu = %0.2f and sigma = %0.2f"); %! title (sprintf (txt, pd_fitted.mu, pd_fitted.sigma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted distribution to the %! ## data, useful for checking normality assumptions. statistics-release-2.1.2/inst/demos/prob.NormalDistribution.proflik000066400000000000000000000006541527311104300255730ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the scale parameter of a fitted %! ## Normal distribution %! rng (42); %! data = randn (1000, 1); %! pd_fitted = fitdist (data, "Normal"); %! [nlogL, param] = proflik (pd_fitted, 2, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the scale parameter (sigma), %! ## helping to understand the uncertainty in parameter estimates for symmetric data. statistics-release-2.1.2/inst/demos/prob.NormalDistribution.random000066400000000000000000000006121527311104300253770ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Normal distribution %! rng (42); %! samples = randn (500, 1); %! hist (samples, 50) %! title ("Histogram of 500 random samples from Normal(mu=0, sigma=1)") %! xlabel ("values in x") %! ylabel ("Frequency") %! %! ## This generates random samples from a Normal distribution, useful %! ## for simulating symmetric data like noise or natural variations. statistics-release-2.1.2/inst/demos/prob.NormalDistribution.sigma000066400000000000000000000014301527311104300252160ustar00rootroot00000000000000%!demo %! ## Create a Normal distribution with default parameters %! rng (42); %! data = randn (10000, 1); %! pd = fitdist (data, "Normal"); %! %! ## Query parameter 'sigma' (scale parameter, standard deviation) %! pd.sigma %! %! ## Set parameter 'sigma' %! pd.sigma = 2 %! %! ## Use this to initialize or modify the standard deviation in a Normal %! ## distribution. The scale parameter must be a positive real scalar, controlling %! ## the spread around the mean. %!demo %! ## Create a Normal distribution object by calling its constructor %! pd = NormalDistribution (0, 1.5) %! %! ## Query parameter 'sigma' %! pd.sigma %! %! ## This shows how to set the scale parameter directly via the constructor, %! ## ideal for modeling variability in symmetric data, such as measurement precision. statistics-release-2.1.2/inst/demos/prob.NormalDistribution.std000066400000000000000000000004751527311104300247200ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Normal distribution %! rng (42); %! data = randn (10000, 1); %! pd = fitdist (data, "Normal"); %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which equals sigma %! ## and measures the spread around the mean in symmetric distributions. statistics-release-2.1.2/inst/demos/prob.NormalDistribution.truncate000066400000000000000000000020131527311104300257410ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Normal distribution, with parameters mu = 0 %! ## and sigma = 1, truncated at [-2, 2] intervals. Generate 10000 random %! ## samples from this truncated distribution and superimpose a histogram scaled %! ## accordingly %! %! rng (42); %! data_all = randn (20000, 1); %! data = data_all(data_all >= -2 & data_all <= 2); %! data = data(1:10000); %! pd = fitdist (data, "Normal"); %! t = truncate (pd, -2, 2); %! [counts, centers] = hist (data, 50); %! bin_width = centers(2) - centers(1); %! bar (centers, counts / (sum (counts) * bin_width), 1); %! hold on; %! %! ## Plot histogram and truncated PDF %! x = linspace (-2, 2, 500); %! y = pdf (t, x); %! plot (x, y, "r", "linewidth", 2); %! hold off %! title ("Normal distribution (mu = 0, sigma = 1) truncated at [-2, 2]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a Normal distribution to a specific %! ## range and visualizing the resulting distribution with random samples, %! ## useful for bounded symmetric data. statistics-release-2.1.2/inst/demos/prob.NormalDistribution.var000066400000000000000000000004371527311104300247140ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Normal distribution %! rng (42); %! data = randn (10000, 1); %! pd = fitdist (data, "Normal"); %! var_value = var (pd) %! %! ## Use this to calculate the variance, which is sigma squared %! ## and quantifies the spread in symmetric distributions. statistics-release-2.1.2/inst/demos/prob.PiecewiseLinearDistribution.Fx000066400000000000000000000005371527311104300263220ustar00rootroot00000000000000%!demo %! ## Create a Piecewise Linear distribution object by calling its constructor %! pd = PiecewiseLinearDistribution ([0; 1; 2; 3], [0; 0.3; 0.7; 1]) %! %! ## Query parameter 'Fx' %! pd.Fx %! %! ## This shows how to set CDF values directly via the constructor, ideal for %! ## defining piecewise linear approximations of arbitrary distributions. statistics-release-2.1.2/inst/demos/prob.PiecewiseLinearDistribution.cdf000066400000000000000000000013711527311104300264760ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Piecewise Linear distribution %! load patients %! [f1, x1] = ecdf (Weight); %! [f2, x2] = ecdf (Height); %! pd1 = PiecewiseLinearDistribution (x1(1:10:end), f1(1:10:end)); %! pd2 = PiecewiseLinearDistribution (x2(1:10:end), f2(1:10:end)); %! vals = 50:0.1:250; %! p1 = cdf (pd1, vals); %! p2 = cdf (pd2, vals); %! plot (vals, p1, "-b", vals, p2, "-r") %! grid on %! legend ({"Weight", "Height"}, "location", "southeast") %! title ("Piecewise Linear CDF") %! xlabel ("values in x") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Piecewise Linear distributions, showing how probability %! ## accumulates across the defined segments. statistics-release-2.1.2/inst/demos/prob.PiecewiseLinearDistribution.icdf000066400000000000000000000013541527311104300266500ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Piecewise Linear distribution %! load patients %! [f1, x1] = ecdf (Weight); %! [f2, x2] = ecdf (Height); %! pd1 = PiecewiseLinearDistribution (x1(1:10:end), f1(1:10:end)); %! pd2 = PiecewiseLinearDistribution (x2(1:10:end), f2(1:10:end)); %! p = 0.001:0.001:0.999; %! x_vals1 = icdf (pd1, p); %! x_vals2 = icdf (pd2, p); %! plot (p, x_vals1, "-b", p, x_vals2, "-r") %! grid on %! legend ({"Weight", "Height"}, "location", "northwest") %! title ("Piecewise Linear iCDF") %! xlabel ("Probability") %! ylabel ("values in x") %! %! ## This demonstrates the inverse CDF (quantiles) for Piecewise Linear %! ## distributions, useful for finding values corresponding to given %! ## probabilities in empirical data. statistics-release-2.1.2/inst/demos/prob.PiecewiseLinearDistribution.iqr000066400000000000000000000006231527311104300265340ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Piecewise Linear distribution %! load patients %! [f, x] = ecdf (Weight); %! pd = PiecewiseLinearDistribution (x(1:5:end), f(1:5:end)); %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, helpful for summarizing variability %! ## in empirical datasets. statistics-release-2.1.2/inst/demos/prob.PiecewiseLinearDistribution.mean000066400000000000000000000007341527311104300266640ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Piecewise Linear distributions %! load patients %! [f1, x1] = ecdf (Weight); %! [f2, x2] = ecdf (Height); %! pd1 = PiecewiseLinearDistribution (x1(1:5:end), f1(1:5:end)); %! pd2 = PiecewiseLinearDistribution (x2(1:5:end), f2(1:5:end)); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Piecewise Linear %! ## distributions based on empirical data, representing the average value. statistics-release-2.1.2/inst/demos/prob.PiecewiseLinearDistribution.median000066400000000000000000000007431527311104300272010ustar00rootroot00000000000000%!demo %! ## Compute the median for different Piecewise Linear distributions %! load patients %! [f1, x1] = ecdf (Weight); %! [f2, x2] = ecdf (Height); %! pd1 = PiecewiseLinearDistribution (x1(1:5:end), f1(1:5:end)); %! pd2 = PiecewiseLinearDistribution (x2(1:5:end), f2(1:5:end)); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which splits the distribution %! ## into two equal probability halves, robust for skewed empirical data. statistics-release-2.1.2/inst/demos/prob.PiecewiseLinearDistribution.pdf000066400000000000000000000012641527311104300265140ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Piecewise Linear distribution %! load patients %! [f1, x1] = ecdf (Weight); %! [f2, x2] = ecdf (Height); %! pd1 = PiecewiseLinearDistribution (x1(1:10:end), f1(1:10:end)); %! pd2 = PiecewiseLinearDistribution (x2(1:10:end), f2(1:10:end)); %! vals = 50:0.1:250; %! y1 = pdf (pd1, vals); %! y2 = pdf (pd2, vals); %! plot (vals, y1, "-b", vals, y2, "-r") %! grid on %! legend ({"Weight", "Height"}, "location", "northeast") %! title ("Piecewise Linear PDF") %! xlabel ("values in x") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Piecewise Linear %! ## distributions, showing the density across segments. statistics-release-2.1.2/inst/demos/prob.PiecewiseLinearDistribution.plot000066400000000000000000000012211527311104300267120ustar00rootroot00000000000000%!demo %! ## Create a Piecewise Linear distribution and plot its PDF. %! load patients %! [f, x] = ecdf (Weight); %! pd = PiecewiseLinearDistribution (x(1:5:end), f(1:5:end)); %! plot (pd) %! title ("Piecewise Linear distribution from Weight data") %!demo %! ## Create a Piecewise Linear distribution from data and plot its CDF. %! load patients %! [f, x] = ecdf (Weight); %! pd = PiecewiseLinearDistribution (x(1:5:end), f(1:5:end)); %! plot (pd, "PlotType", "cdf") %! title ("CDF of Piecewise Linear distribution from Weight data") %! %! ## Use this to visualize the CDF of the Piecewise Linear distribution, %! ## useful for comparing to empirical CDF. statistics-release-2.1.2/inst/demos/prob.PiecewiseLinearDistribution.random000066400000000000000000000007771527311104300272330ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Piecewise Linear distribution %! load patients %! [f, x] = ecdf (Weight); %! pd = PiecewiseLinearDistribution (x(1:5:end), f(1:5:end)); %! samples = random (pd, 500, 1); %! hist (samples, 50) %! title ("Histogram of 500 random samples from Piecewise Linear (Weight data)") %! xlabel ("values in x") %! ylabel ("Frequency") %! %! ## This generates random samples from a Piecewise Linear distribution, %! ## useful for simulating data based on empirical distributions. statistics-release-2.1.2/inst/demos/prob.PiecewiseLinearDistribution.std000066400000000000000000000005041527311104300265310ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Piecewise Linear distribution %! load patients %! [f, x] = ecdf (Weight); %! pd = PiecewiseLinearDistribution (x(1:5:end), f(1:5:end)); %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, measuring variability %! ## in the empirical distribution. statistics-release-2.1.2/inst/demos/prob.PiecewiseLinearDistribution.truncate000066400000000000000000000014761527311104300275750ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Piecewise Linear distribution truncated at [130, 180]. %! ## Generate 10000 random samples from this truncated distribution and %! ## superimpose a histogram. %! %! load patients %! [f, x] = ecdf (Weight); %! pd = PiecewiseLinearDistribution (x(1:5:end), f(1:5:end)); %! t = truncate (pd, 130, 180); %! data = random (t, 10000, 1); %! %! ## Plot histogram and truncated PDF %! [counts, centers] = hist (data, 20); %! bin_width = centers(2) - centers(1); %! bar (centers, counts / (sum (counts) * bin_width), 1); %! hold on %! plot (t) %! hold off %! title ("Piecewise Linear distribution truncated at [130, 180]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a Piecewise Linear distribution to a specific %! ## range and visualizing the result with random samples. statistics-release-2.1.2/inst/demos/prob.PiecewiseLinearDistribution.var000066400000000000000000000004611527311104300265310ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Piecewise Linear distribution %! load patients %! [f, x] = ecdf (Weight); %! pd = PiecewiseLinearDistribution (x(1:5:end), f(1:5:end)); %! var_value = var (pd) %! %! ## Use this to calculate the variance, quantifying the spread in the %! ## empirical distribution. statistics-release-2.1.2/inst/demos/prob.PiecewiseLinearDistribution.x000066400000000000000000000005471527311104300262150ustar00rootroot00000000000000%!demo %! ## Create a Piecewise Linear distribution object by calling its constructor %! pd = PiecewiseLinearDistribution ([0; 1; 2; 3], [0; 0.3; 0.7; 1]) %! %! ## Query parameter 'x' %! pd.x %! %! ## This demonstrates direct construction with specific x values, useful for %! ## modeling custom empirical distributions or interpolating between known points. statistics-release-2.1.2/inst/demos/prob.PoissonDistribution.cdf000066400000000000000000000015541527311104300250630ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Poisson distribution %! rng (42); %! randp ('state', 42); %! x = 0:20; %! data1 = poissrnd (2, 10000, 1); %! data2 = poissrnd (5, 10000, 1); %! data3 = poissrnd (10, 10000, 1); %! pd1 = fitdist (data1, "Poisson"); %! pd2 = fitdist (data2, "Poisson"); %! pd3 = fitdist (data3, "Poisson"); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"lambda = 2", "lambda = 5", "lambda = 10"}, "location", "southeast") %! title ("Poisson CDF") %! xlabel ("values in x (non-negative integers)") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Poisson distributions, showing the probability of observing %! ## at most k events, useful in risk assessment or queueing theory. statistics-release-2.1.2/inst/demos/prob.PoissonDistribution.icdf000066400000000000000000000015531527311104300252330ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Poisson distribution %! rng (42); %! randp ('state', 42); %! p = 0.001:0.001:0.999; %! data1 = poissrnd (2, 10000, 1); %! data2 = poissrnd (5, 10000, 1); %! data3 = poissrnd (10, 10000, 1); %! pd1 = fitdist (data1, "Poisson"); %! pd2 = fitdist (data2, "Poisson"); %! pd3 = fitdist (data3, "Poisson"); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"lambda = 2", "lambda = 5", "lambda = 10"}, "location", "northwest") %! title ("Poisson iCDF") %! xlabel ("Probability") %! ylabel ("values in x (non-negative integers)") %! %! ## This demonstrates the inverse CDF (quantiles) for Poisson distributions, %! ## useful for finding the number of events corresponding to a given probability, %! ## such as in inventory management for stock levels. statistics-release-2.1.2/inst/demos/prob.PoissonDistribution.iqr000066400000000000000000000005711527311104300251200ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Poisson distribution %! rng (42); %! randp ('state', 42); %! data = poissrnd (5, 10000, 1); %! pd = fitdist (data, "Poisson"); %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, measuring the spread of the %! ## middle 50% of the distribution, helpful for understanding variability in count data. statistics-release-2.1.2/inst/demos/prob.PoissonDistribution.lambda000066400000000000000000000014551527311104300255470ustar00rootroot00000000000000%!demo %! ## Create a Poisson distribution from data %! rng (42); %! randp ('state', 42); %! data = poissrnd (5, 100, 1); %! pd = fitdist (data, "Poisson"); %! %! ## Query parameter 'lambda' (rate parameter) %! pd.lambda %! %! ## Set parameter 'lambda' %! pd.lambda = 10 %! %! ## Use this to initialize or modify the rate parameter of a Poisson distribution. %! ## The rate parameter must be a positive real scalar, representing both the mean %! ## and variance, ideal for count data modeling. %!demo %! ## Create a Poisson distribution object by calling its constructor %! pd = PoissonDistribution (7.5) %! %! ## Query parameter 'lambda' %! pd.lambda %! %! ## This shows direct construction with a specific rate parameter, useful for %! ## theoretical analysis or when lambda is estimated from prior knowledge. statistics-release-2.1.2/inst/demos/prob.PoissonDistribution.mean000066400000000000000000000006721527311104300252470ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Poisson distributions %! rng (42); %! randp ('state', 42); %! data1 = poissrnd (2, 10000, 1); %! data2 = poissrnd (5, 10000, 1); %! pd1 = fitdist (data1, "Poisson"); %! pd2 = fitdist (data2, "Poisson"); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Poisson distributions, %! ## which equals lambda, representing the average number of events. statistics-release-2.1.2/inst/demos/prob.PoissonDistribution.median000066400000000000000000000006761527311104300255700ustar00rootroot00000000000000%!demo %! ## Compute the median for different Poisson distributions %! rng (42); %! randp ('state', 42); %! data1 = poissrnd (2, 10000, 1); %! data2 = poissrnd (5, 10000, 1); %! pd1 = fitdist (data1, "Poisson"); %! pd2 = fitdist (data2, "Poisson"); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median count, which splits the distribution into %! ## two equal probability halves, robust for skewed count data. statistics-release-2.1.2/inst/demos/prob.PoissonDistribution.negloglik000066400000000000000000000006011527311104300262720ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Poisson distribution %! rng (42); %! randp ('state', 42); %! data = poissrnd (5, 100, 1); %! pd_fitted = fitdist (data, "Poisson"); %! nlogL = negloglik (pd_fitted) %! %! ## This is useful for assessing the fit of a Poisson distribution to data, %! ## with lower values indicating better fit, often used in model selection. statistics-release-2.1.2/inst/demos/prob.PoissonDistribution.paramci000066400000000000000000000006131527311104300257360ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Poisson distribution %! rng (42); %! randp ('state', 42); %! data = poissrnd (5, 1000, 1); %! pd_fitted = fitdist (data, "Poisson"); %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameter (lambda), %! ## providing a range of plausible values based on the data. statistics-release-2.1.2/inst/demos/prob.PoissonDistribution.pdf000066400000000000000000000014011527311104300250670ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Poisson distribution %! rng (42); %! randp ('state', 42); %! x = 0:20; %! data1 = poissrnd (2, 10000, 1); %! data2 = poissrnd (5, 10000, 1); %! data3 = poissrnd (10, 10000, 1); %! pd1 = fitdist (data1, "Poisson"); %! pd2 = fitdist (data2, "Poisson"); %! pd3 = fitdist (data3, "Poisson"); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"lambda = 2", "lambda = 5", "lambda = 10"}, "location", "northeast") %! title ("Poisson PDF") %! xlabel ("values in x (non-negative integers)") %! ylabel ("Probability") %! %! ## This visualizes the probability mass function for Poisson distributions, %! ## showing the likelihood of exact count values. statistics-release-2.1.2/inst/demos/prob.PoissonDistribution.plot000066400000000000000000000031451527311104300253030ustar00rootroot00000000000000%!demo %! ## Create a Poisson distribution with fixed parameter lambda=5 and plot its PDF. %! rng (42); %! randp ('state', 42); %! data = poissrnd (5, 10000, 1); %! pd = fitdist (data, "Poisson"); %! plot (pd) %! title ("Fixed Poisson distribution with lambda = 5") %!demo %! ## Generate a data set of 100 random samples from a Poisson distribution with %! ## lambda=5. Fit a Poisson distribution to this data and plot its CDF superimposed %! ## over an empirical CDF. %! rng (42); %! randp ('state', 42); %! data = poissrnd (5, 100, 1); %! pd_fitted = fitdist (data, "Poisson"); %! plot (pd_fitted, "PlotType", "cdf") %! txt = "Fitted Poisson distribution with lambda = %0.2f"; %! title (sprintf (txt, pd_fitted.lambda)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the data, %! ## useful for assessing model fit in count data. %!demo %! ## Generate a data set of 200 random samples from a Poisson distribution with %! ## lambda=5. Display a probability plot for the Poisson distribution fit to the data. %! rng (42); %! randp ('state', 42); %! data = poissrnd (5, 200, 1); %! pd_fitted = fitdist (data, "Poisson"); %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted Poisson distribution with lambda = %0.2f"); %! title (sprintf (txt, pd_fitted.lambda)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted distribution to the data, %! ## useful for checking if the Poisson model is appropriate for count data. statistics-release-2.1.2/inst/demos/prob.PoissonDistribution.proflik000066400000000000000000000007051527311104300257720ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the rate parameter of a fitted %! ## Poisson distribution %! rng (42); %! randp ('state', 42); %! data = poissrnd (5, 1000, 1); %! pd_fitted = fitdist (data, "Poisson"); %! [nlogL, param] = proflik (pd_fitted, 1, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the rate parameter (lambda), %! ## helping to understand uncertainty in parameter estimates for count models. statistics-release-2.1.2/inst/demos/prob.PoissonDistribution.random000066400000000000000000000007151527311104300256050ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Poisson distribution %! rng (42); %! randp ('state', 42); %! samples = poissrnd (5, 500, 1); %! hist (samples, 20) %! title ("Histogram of 500 random samples from Poisson(lambda=5)") %! xlabel ("values in x (non-negative integers)") %! ylabel ("Frequency") %! %! ## This generates random count samples from a Poisson distribution, useful %! ## for simulating event occurrences like defect counts or traffic arrivals. statistics-release-2.1.2/inst/demos/prob.PoissonDistribution.std000066400000000000000000000005101527311104300251100ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Poisson distribution %! rng (42); %! randp ('state', 42); %! data = poissrnd (5, 10000, 1); %! pd = fitdist (data, "Poisson"); %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which equals sqrt(lambda), %! ## measuring variability in event counts. statistics-release-2.1.2/inst/demos/prob.PoissonDistribution.truncate000066400000000000000000000016411527311104300261510ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Poisson distribution, with lambda=5, truncated at [2, 10] %! ## intervals. Generate 10000 random samples from this truncated distribution %! ## and superimpose a histogram scaled accordingly. %! %! rng (42); %! randp ('state', 42); %! data_all = poissrnd (5, 20000, 1); %! data = data_all(data_all >= 2 & data_all <= 10); %! data = data(1:10000); %! %! pd = fitdist (data, "Poisson"); %! t = truncate (pd, 2, 10); %! %! ## Plot histogram and truncated %! [counts, centers] = hist (data, 20); %! bin_width = centers(2) - centers(1); %! bar (centers, counts / (sum (counts) * bin_width), 1); %! hold on %! plot (t) %! hold off %! title ("Poisson distribution (lambda=5) truncated at [2, 10]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a Poisson distribution to a specific range %! ## and visualizing the result with random samples, useful for bounded count data. statistics-release-2.1.2/inst/demos/prob.PoissonDistribution.var000066400000000000000000000004551527311104300251160ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Poisson distribution %! rng (42); %! randp ('state', 42); %! data = poissrnd (5, 10000, 1); %! pd = fitdist (data, "Poisson"); %! var_value = var (pd) %! %! ## Use this to calculate the variance, which equals lambda, quantifying the %! ## spread in count data. statistics-release-2.1.2/inst/demos/prob.RayleighDistribution.cdf000066400000000000000000000015361527311104300251750ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Rayleigh distribution %! rng (42); %! x = -1:0.01:5; %! data1 = raylrnd (0.5, 10000, 1); %! data2 = raylrnd (1.0, 10000, 1); %! data3 = raylrnd (2.0, 10000, 1); %! pd1 = fitdist (data1, "Rayleigh"); %! pd2 = fitdist (data2, "Rayleigh"); %! pd3 = fitdist (data3, "Rayleigh"); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"sigma = 0.5", "sigma = 1", "sigma = 2"}, ... %! "location", "southeast") %! title ("Rayleigh CDF") %! xlabel ("values in x (x >= 0)") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Rayleigh distributions, showing how probability %! ## accumulates for nonnegative values, useful in signal processing or physics. statistics-release-2.1.2/inst/demos/prob.RayleighDistribution.icdf000066400000000000000000000015001527311104300253350ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Rayleigh distribution %! rng (42); %! p = 0.001:0.001:0.999; %! data1 = raylrnd (0.5, 10000, 1); %! data2 = raylrnd (1.0, 10000, 1); %! data3 = raylrnd (2.0, 10000, 1); %! pd1 = fitdist (data1, "Rayleigh"); %! pd2 = fitdist (data2, "Rayleigh"); %! pd3 = fitdist (data3, "Rayleigh"); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"sigma = 0.5", "sigma = 1", "sigma = 2"}, ... %! "location", "northwest") %! title ("Rayleigh iCDF") %! xlabel ("Probability") %! ylabel ("values in x (x >= 0)") %! %! ## This demonstrates the inverse CDF (quantiles) for Rayleigh %! ## distributions, useful for finding values corresponding to given %! ## probabilities, such as thresholds in radar detection. statistics-release-2.1.2/inst/demos/prob.RayleighDistribution.iqr000066400000000000000000000005731527311104300252340ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Rayleigh distribution %! rng (42); %! data = raylrnd (1, 10000, 1); %! pd = fitdist (data, "Rayleigh"); %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, helpful for understanding central %! ## variability in nonnegative data. statistics-release-2.1.2/inst/demos/prob.RayleighDistribution.mean000066400000000000000000000007151527311104300253570ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Rayleigh distributions %! rng (42); %! data1 = raylrnd (0.5, 10000, 1); %! data2 = raylrnd (1.0, 10000, 1); %! pd1 = fitdist (data1, "Rayleigh"); %! pd2 = fitdist (data2, "Rayleigh"); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Rayleigh %! ## distributions with different scale parameters, representing average %! ## magnitude in applications like acoustics. statistics-release-2.1.2/inst/demos/prob.RayleighDistribution.median000066400000000000000000000006651527311104300257000ustar00rootroot00000000000000%!demo %! ## Compute the median for different Rayleigh distributions %! rng (42); %! data1 = raylrnd (0.5, 10000, 1); %! data2 = raylrnd (1.0, 10000, 1); %! pd1 = fitdist (data1, "Rayleigh"); %! pd2 = fitdist (data2, "Rayleigh"); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which splits the distribution %! ## into two equal probability halves, robust to skewness in nonnegative data. statistics-release-2.1.2/inst/demos/prob.RayleighDistribution.negloglik000066400000000000000000000005561527311104300264150ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Rayleigh distribution %! rng (42); %! data = raylrnd (1, 100, 1); %! pd_fitted = fitdist (data, "Rayleigh"); %! nlogL = negloglik (pd_fitted) %! %! ## This is useful for assessing the fit of a Rayleigh distribution to %! ## data, with lower values indicating a better fit, often used in model comparison. statistics-release-2.1.2/inst/demos/prob.RayleighDistribution.paramci000066400000000000000000000005661527311104300260570ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Rayleigh %! ## distribution %! rng (42); %! data = raylrnd (1, 1000, 1); %! pd_fitted = fitdist (data, "Rayleigh"); %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameter (sigma), %! ## providing a range of plausible values given the data. statistics-release-2.1.2/inst/demos/prob.RayleighDistribution.pdf000066400000000000000000000014001527311104300252000ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Rayleigh distribution %! rng (42); %! x = -1:0.01:5; %! data1 = raylrnd (0.5, 10000, 1); %! data2 = raylrnd (1.0, 10000, 1); %! data3 = raylrnd (2.0, 10000, 1); %! pd1 = fitdist (data1, "Rayleigh"); %! pd2 = fitdist (data2, "Rayleigh"); %! pd3 = fitdist (data3, "Rayleigh"); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"sigma = 0.5", "sigma = 1", "sigma = 2"}, ... %! "location", "northeast") %! title ("Rayleigh PDF") %! xlabel ("values in x (x >= 0)") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Rayleigh %! ## distributions, showing the likelihood for nonnegative values. statistics-release-2.1.2/inst/demos/prob.RayleighDistribution.plot000066400000000000000000000031111527311104300254060ustar00rootroot00000000000000%!demo %! ## Create a Rayleigh distribution with fixed parameter sigma = 1 and plot its PDF. %! %! rng (42); %! data = raylrnd (1, 10000, 1); %! pd = fitdist (data, "Rayleigh"); %! plot (pd) %! title ("Fixed Rayleigh distribution with sigma = 1") %!demo %! ## Generate a data set of 100 random samples from a Rayleigh %! ## distribution with parameter sigma = 1. Fit a Rayleigh %! ## distribution to this data and plot its CDF superimposed over an empirical %! ## CDF. %! %! rng (42); %! data = raylrnd (1, 100, 1); %! pd_fitted = fitdist (data, "Rayleigh"); %! plot (pd_fitted, "PlotType", "cdf") %! txt = "Fitted Rayleigh distribution with sigma = %0.2f"; %! title (sprintf (txt, pd_fitted.sigma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit. %!demo %! ## Generate a data set of 200 random samples from a Rayleigh %! ## distribution with parameter sigma = 1. Display a probability %! ## plot for the Rayleigh distribution fit to the data. %! %! rng (42); %! data = raylrnd (1, 200, 1); %! pd_fitted = fitdist (data, "Rayleigh"); %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted Rayleigh", ... %! " distribution with sigma = %0.2f"); %! title (sprintf (txt, pd_fitted.sigma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted distribution to the %! ## data, useful for checking if the Rayleigh model is appropriate. statistics-release-2.1.2/inst/demos/prob.RayleighDistribution.proflik000066400000000000000000000006421527311104300261040ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the scale parameter of a fitted %! ## Rayleigh distribution %! rng (42); %! data = raylrnd (1, 1000, 1); %! pd_fitted = fitdist (data, "Rayleigh"); %! [nlogL, param] = proflik (pd_fitted, 1, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the scale parameter (sigma), %! ## helping to understand the uncertainty in parameter estimates. statistics-release-2.1.2/inst/demos/prob.RayleighDistribution.random000066400000000000000000000006351527311104300257200ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Rayleigh distribution %! rng (42); %! samples = raylrnd (1, 500, 1); %! hist (samples, 50) %! title ("Histogram of 500 random samples from Rayleigh(sigma=1)") %! xlabel ("values in x (x >= 0)") %! ylabel ("Frequency") %! %! ## This generates random samples from a Rayleigh distribution, useful %! ## for simulating nonnegative data like envelope of narrowband signals. statistics-release-2.1.2/inst/demos/prob.RayleighDistribution.sigma000066400000000000000000000014561527311104300255420ustar00rootroot00000000000000%! ## Create a Rayleigh distribution with default parameters %! data = raylrnd (1, 10000, 1); %! pd = fitdist (data, "Rayleigh"); %! %! ## Query parameter 'sigma' (scale parameter) %! pd.sigma %! %! ## Set parameter 'sigma' %! pd.sigma = 2 %! %! ## Use this to initialize or modify the scale parameter of a Rayleigh %! ## distribution. The scale parameter must be a positive real scalar, controlling %! ## the spread of the distribution, often used in modeling magnitudes like wind speeds. %!demo %! ## Create a Rayleigh distribution object by calling its constructor %! pd = RayleighDistribution (1.5) %! %! ## Query parameter 'sigma' %! pd.sigma %! %! ## This demonstrates direct construction with a specific scale parameter, %! ## useful for modeling nonnegative data like signal amplitudes in communications. statistics-release-2.1.2/inst/demos/prob.RayleighDistribution.std000066400000000000000000000004751527311104300252340ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Rayleigh distribution %! rng (42); %! data = raylrnd (1, 10000, 1); %! pd = fitdist (data, "Rayleigh"); %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## in the nonnegative values of the distribution. statistics-release-2.1.2/inst/demos/prob.RayleighDistribution.truncate000066400000000000000000000017311527311104300262630ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Rayleigh distribution, with parameter sigma = 1, %! ## truncated at [0.5, 2] intervals. Generate 10000 random %! ## samples from this truncated distribution and superimpose a histogram scaled %! ## accordingly %! %! rng (42); %! data_all = raylrnd (1, 20000, 1); %! data = data_all(data_all >= 0.5 & data_all <= 2); %! data = data(1:10000); %! %! pd = fitdist (data, "Rayleigh"); %! t = truncate (pd, 0.5, 2); %! %! [counts, centers] = hist (data, 50); %! bin_width = centers(2) - centers(1); %! bar (centers, counts / (sum (counts) * bin_width), 1); %! hold on; %! %! ## Plot histogram and truncated PDF %! x = linspace (0.5, 5, 500); %! y = pdf (t, x); %! plot (x, y, "r", "linewidth", 2); %! title ("Rayleigh distribution (sigma = 1) truncated at [0.5, 2]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a Rayleigh distribution to a specific %! ## range and visualizing the resulting distribution with random samples. statistics-release-2.1.2/inst/demos/prob.RayleighDistribution.var000066400000000000000000000004461527311104300252300ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Rayleigh distribution %! rng (42); %! data = raylrnd (1, 10000, 1); %! pd = fitdist (data, "Rayleigh"); %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## nonnegative values in the distribution. statistics-release-2.1.2/inst/demos/prob.RicianDistribution.cdf000066400000000000000000000016251527311104300246350ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Rician distribution %! randg ('state', 42); %! randp ('state', 42); %! x = 0:0.01:5; %! data1 = ricernd (1, 0.5, [10000, 1]); %! data2 = ricernd (1, 1, [10000, 1]); %! data3 = ricernd (1, 2, [10000, 1]); %! pd1 = fitdist (data1, "Rician"); %! pd2 = fitdist (data2, "Rician"); %! pd3 = fitdist (data3, "Rician"); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"s = 1, sigma = 0.5", "s = 1, sigma = 1", "s = 1, sigma = 2"}, ... %! "location", "southeast") %! title ("Rician CDF") %! xlabel ("values in x (x >= 0)") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Rician distributions, showing how probability accumulates %! ## for non-negative signal magnitudes, useful in signal processing. statistics-release-2.1.2/inst/demos/prob.RicianDistribution.icdf000066400000000000000000000015251527311104300250050ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Rician distribution %! randg ('state', 42); %! randp ('state', 42); %! p = 0.001:0.001:0.999; %! data1 = ricernd (1, 0.5, [10000, 1]); %! data2 = ricernd (1, 1, [10000, 1]); %! data3 = ricernd (1, 2, [10000, 1]); %! pd1 = fitdist (data1, "Rician"); %! pd2 = fitdist (data2, "Rician"); %! pd3 = fitdist (data3, "Rician"); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"s = 1, sigma = 0.5", "s = 1, sigma = 1", "s = 1, sigma = 2"}, ... %! "location", "northwest") %! title ("Rician iCDF") %! xlabel ("Probability") %! ylabel ("values in x (x >= 0)") %! %! ## This demonstrates the inverse CDF (quantiles) for Rician distributions, %! ## useful for finding signal magnitude thresholds in applications like radar. statistics-release-2.1.2/inst/demos/prob.RicianDistribution.iqr000066400000000000000000000006301527311104300246670ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Rician distribution %! randg ('state', 42); %! randp ('state', 42); %! data = ricernd (1, 1, [10000, 1]); %! pd = fitdist (data, "Rician"); %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, helpful for understanding variability %! ## in signal magnitudes. statistics-release-2.1.2/inst/demos/prob.RicianDistribution.mean000066400000000000000000000007261527311104300250220ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Rician distributions %! randg ('state', 42); %! randp ('state', 42); %! data1 = ricernd (1, 0.5, [10000, 1]); %! data2 = ricernd (1, 1, [10000, 1]); %! pd1 = fitdist (data1, "Rician"); %! pd2 = fitdist (data2, "Rician"); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Rician distributions %! ## with different scale parameters, representing the average signal magnitude. statistics-release-2.1.2/inst/demos/prob.RicianDistribution.median000066400000000000000000000007251527311104300253360ustar00rootroot00000000000000%!demo %! ## Compute the median for different Rician distributions %! randg ('state', 42); %! randp ('state', 42); %! data1 = ricernd (1, 0.5, [10000, 1]); %! data2 = ricernd (1, 1, [10000, 1]); %! pd1 = fitdist (data1, "Rician"); %! pd2 = fitdist (data2, "Rician"); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which splits the distribution into %! ## two equal probability halves, robust to skewness in signal data. statistics-release-2.1.2/inst/demos/prob.RicianDistribution.negloglik000066400000000000000000000006351527311104300260540ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Rician distribution %! rng (42); %! randg ('state', 42); %! randp ('state', 42); %! data = ricernd (1, 1, [100, 1]); %! pd_fitted = fitdist (data, "Rician"); %! nlogL = negloglik (pd_fitted) %! %! ## This is useful for assessing the fit of a Rician distribution to data, %! ## with lower values indicating a better fit, often used in model comparison. statistics-release-2.1.2/inst/demos/prob.RicianDistribution.paramci000066400000000000000000000006501527311104300255120ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Rician distribution %! rng (42); %! randg ('state', 42); %! randp ('state', 42); %! data = ricernd (1, 1, [1000, 1]); %! pd_fitted = fitdist (data, "Rician"); %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameters (s %! ## and sigma), providing a range of plausible values given the data. statistics-release-2.1.2/inst/demos/prob.RicianDistribution.pdf000066400000000000000000000015021527311104300246440ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Rician distribution %! randg ('state', 42); %! randp ('state', 42); %! x = 0:0.01:5; %! data1 = ricernd (1, 0.5, [10000, 1]); %! data2 = ricernd (1, 1, [10000, 1]); %! data3 = ricernd (1, 2, [10000, 1]); %! pd1 = fitdist (data1, "Rician"); %! pd2 = fitdist (data2, "Rician"); %! pd3 = fitdist (data3, "Rician"); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"s = 1, sigma = 0.5", "s = 1, sigma = 1", "s = 1, sigma = 2"}, ... %! "location", "northeast") %! title ("Rician PDF") %! xlabel ("values in x (x >= 0)") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Rician distributions, %! ## showing the likelihood for non-negative signal magnitudes. statistics-release-2.1.2/inst/demos/prob.RicianDistribution.plot000066400000000000000000000035251527311104300250600ustar00rootroot00000000000000%!demo %! ## Create a Rician distribution with fixed parameters s = 1 and sigma = 1 %! ## and plot its PDF. %! pd = RicianDistribution (1, 1); %! plot (pd) %! title ("Rician distribution with s = 1 and sigma = 1") %! %! ## Use this to visualize the PDF of a Rician distribution with fixed parameters, %! ## useful for understanding the shape of the distribution. %!demo %! ## Generate a data set of 100 random samples from a Rician distribution %! ## with parameters s = 1 and sigma = 1. Fit a Rician distribution to this %! ## data and plot its CDF superimposed over an empirical CDF. %! rng (42); %! randg ('state', 42); %! randp ('state', 42); %! data = ricernd (1, 1, [100, 1]); %! pd_fitted = fitdist (data, "Rician"); %! plot (pd_fitted, "PlotType", "cdf") %! txt = "Fitted Rician distribution with s = %0.2f and sigma = %0.2f"; %! title (sprintf (txt, pd_fitted.s, pd_fitted.sigma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit. %!demo %! ## Generate a data set of 200 random samples from a Rician distribution %! ## with parameters s = 1 and sigma = 1. Display a probability plot for the %! ## Rician distribution fit to the data. %! rng (42); %! randg ('state', 42); %! randp ('state', 42); %! data = ricernd (1, 1, [200, 1]); %! pd_fitted = fitdist (data, "Rician"); %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted Rician distribution", ... %! " with s = %0.2f and sigma = %0.2f"); %! title (sprintf (txt, pd_fitted.s, pd_fitted.sigma)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted distribution to the %! ## data, useful for checking if the Rician model is appropriate. statistics-release-2.1.2/inst/demos/prob.RicianDistribution.proflik000066400000000000000000000007231527311104300255450ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the scale parameter of a fitted %! ## Rician distribution %! rng (42); %! randg ('state', 42); %! randp ('state', 42); %! data = ricernd (1, 1, [1000, 1]); %! pd_fitted = fitdist (data, "Rician"); %! [nlogL, param] = proflik (pd_fitted, 2, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the scale parameter (sigma), %! ## helping to understand the uncertainty in parameter estimates. statistics-release-2.1.2/inst/demos/prob.RicianDistribution.random000066400000000000000000000007331527311104300253600ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Rician distribution %! rng (42); %! randg ('state', 42); %! randp ('state', 42); %! samples = ricernd (1, 1, [500, 1]); %! hist (samples, 50) %! title ("Histogram of 500 random samples from Rician(s=1, sigma=1)") %! xlabel ("values in x (x >= 0)") %! ylabel ("Frequency") %! %! ## This generates random samples from a Rician distribution, useful for %! ## simulating signal magnitudes in applications like wireless communications. statistics-release-2.1.2/inst/demos/prob.RicianDistribution.s000066400000000000000000000015341527311104300243420ustar00rootroot00000000000000%!demo %! ## Create a Rician distribution by fitting to data %! randg ('state', 42); %! randp ('state', 42); %! data = ricernd (1, 1, [10000, 1]); % Generate data with s=1, sigma=1 %! pd = fitdist (data, "Rician"); %! %! ## Query parameter 's' (noncentrality parameter) %! pd.s %! %! ## Set parameter 's' %! pd.s = 1.5 %! %! ## Use this to initialize or modify the noncentrality parameter of a Rician %! ## distribution. The noncentrality parameter 's' must be a non-negative real %! ## scalar, representing the magnitude of the signal in the presence of noise. %!demo %! ## Create a Rician distribution object by calling its constructor %! pd = RicianDistribution (2, 1) %! %! ## Query parameter 's' %! pd.s %! %! ## This demonstrates direct construction with a specific noncentrality %! ## parameter, useful for modeling data with a known signal strength. statistics-release-2.1.2/inst/demos/prob.RicianDistribution.sigma000066400000000000000000000023261527311104300252000ustar00rootroot00000000000000%!demo %! ## Create a Rician distribution with fitted parameters %! randg ('state', 42); %! randp ('state', 42); %! data = ricernd (1, 1, [10000, 1]); %! pd = fitdist (data, "Rician"); %! %! ## Query parameter 'sigma' (scale parameter) %! pd.sigma %! %! ## Set parameter 'sigma' %! pd.sigma = 1.2 %! %! ## Use this to initialize or modify the scale parameter in a Rician %! ## distribution. The scale parameter 'sigma' must be a positive real scalar, %! ## controlling the spread due to Gaussian noise. %!demo %! ## Create a Rician distribution object by calling its constructor %! pd = RicianDistribution (1, 1.5) %! %! ## Query parameter 'sigma' %! pd.sigma %! %! ## This shows how to set the scale parameter directly via the constructor, %! ## ideal for modeling variability in signal magnitude data. RicianDistribution constructor: %!demo %! ## Create a Rician distribution with specific parameters %! pd = RicianDistribution (2, 1) %! %! ## Display the distribution parameters %! pd.s %! pd.sigma %! %! ## Use the constructor to create a Rician distribution with fixed parameters %! ## 's' and 'sigma', suitable for scenarios where signal and noise parameters %! ## are known, such as in communication systems or image processing. statistics-release-2.1.2/inst/demos/prob.RicianDistribution.std000066400000000000000000000005401527311104300246660ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Rician distribution %! randg ('state', 42); %! randp ('state', 42); %! data = ricernd (1, 1, [10000, 1]); %! pd = fitdist (data, "Rician"); %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## in the signal magnitudes of the distribution. statistics-release-2.1.2/inst/demos/prob.RicianDistribution.truncate000066400000000000000000000020171527311104300257220ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Rician distribution, with parameters s = 1 and sigma = 1, %! ## truncated at [0.5, 3] intervals. Generate 10000 random samples from this %! ## truncated distribution and superimpose a histogram scaled accordingly %! rng (42); %! randg ('state', 42); %! randp ('state', 42); %! data_all = ricernd (1, 1, [20000, 1]); %! data = data_all(data_all >= 0.5 & data_all <= 3); %! data = data(1:10000); %! %! pd = fitdist (data, "Rician"); %! t = truncate (pd, 0.5, 3); %! %! [counts, centers] = hist (data, 50); %! bin_width = centers(2) - centers(1); %! bar (centers, counts / (sum (counts) * bin_width), 1); %! hold on; %! %! ## Plot histogram and truncated PDF %! x = linspace (0.5, 5, 500); %! y = pdf (t, x); %! plot (x, y, "r", "linewidth", 2); %! title ("Rician distribution (s = 1, sigma = 1) truncated at [0.5, 3]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a Rician distribution to a specific range and %! ## visualizing the resulting distribution with random samples. statistics-release-2.1.2/inst/demos/prob.RicianDistribution.var000066400000000000000000000005111527311104300246620ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Rician distribution %! randg ('state', 42); %! randp ('state', 42); %! data = ricernd (1, 1, [10000, 1]); %! pd = fitdist (data, "Rician"); %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## signal magnitudes in the distribution. statistics-release-2.1.2/inst/demos/prob.TriangularDistribution.A000066400000000000000000000015111527311104300251560ustar00rootroot00000000000000%!demo %! ## Create a Triangular distribution with default parameters %! pd = makedist ("Triangular", "A", 0, "B", 0.5, "C", 1); %! %! ## Query parameter 'A' (lower limit) %! pd.A %! %! ## Set parameter 'A' %! pd.A = -1 %! %! ## Use this to initialize or modify the lower limit parameter of a Triangular %! ## distribution. The lower limit must be a real scalar less than the upper %! ## limit C, useful for defining the minimum possible value in scenarios like %! ## cost estimation or time to completion. %!demo %! ## Create a Triangular distribution object by calling its constructor %! pd = TriangularDistribution (1, 2, 3); %! %! ## Query parameter 'A' %! pd.A %! %! ## This demonstrates direct construction with a specific lower limit, %! ## useful for modeling bounded data, such as project durations or resource %! ## requirements. statistics-release-2.1.2/inst/demos/prob.TriangularDistribution.B000066400000000000000000000014601527311104300251620ustar00rootroot00000000000000%!demo %! ## Create a Triangular distribution with default parameters %! pd = makedist ("Triangular", "A", 0, "B", 0.5, "C", 1); %! %! ## Query parameter 'B' (peak location) %! pd.B %! %! ## Set parameter 'B' %! pd.B = 0.7 %! %! ## Use this to initialize or modify the peak location parameter of a Triangular %! ## distribution. The peak location must be a real scalar between A and C, %! ## representing the most likely value in applications like risk analysis. %!demo %! ## Create a Triangular distribution object by calling its constructor %! pd = TriangularDistribution (1, 2, 3); %! %! ## Query parameter 'B' %! pd.B %! %! ## This shows how to set the peak location directly via the constructor, %! ## ideal for modeling the mode of triangularly distributed data, such as %! ## expected task completion times. statistics-release-2.1.2/inst/demos/prob.TriangularDistribution.C000066400000000000000000000014301527311104300251600ustar00rootroot00000000000000%!demo %! ## Create a Triangular distribution with default parameters %! pd = makedist ("Triangular", "A", 0, "B", 0.5, "C", 1); %! %! ## Query parameter 'C' (upper limit) %! pd.C %! %! ## Set parameter 'C' %! pd.C = 2 %! %! ## Use this to initialize or modify the upper limit parameter of a Triangular %! ## distribution. The upper limit must be a real scalar greater than A, %! ## defining the maximum possible value in scenarios like budgeting or scheduling. %!demo %! ## Create a Triangular distribution object by calling its constructor %! pd = TriangularDistribution (1, 2, 3); %! %! ## Query parameter 'C' %! pd.C %! %! ## This demonstrates setting the upper limit directly via the constructor, %! ## useful for modeling the maximum bound in applications like cost or time estimates. statistics-release-2.1.2/inst/demos/prob.TriangularDistribution.cdf000066400000000000000000000014061527311104300255350ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Triangular distribution %! x = -1:0.01:6; %! pd1 = TriangularDistribution (0, 1, 2); %! pd2 = TriangularDistribution (1, 2, 3); %! pd3 = TriangularDistribution (2, 3, 4); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"A=0, B=1, C=2", "A=1, B=2, C=3", "A=2, B=3, C=4"}, ... %! "location", "southeast") %! title ("Triangular CDF") %! xlabel ("Values in x (A <= x <= C)") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Triangular distributions, showing how probability accumulates %! ## within the bounds, useful in risk assessment or forecasting. statistics-release-2.1.2/inst/demos/prob.TriangularDistribution.icdf000066400000000000000000000013771527311104300257150ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Triangular distribution %! p = 0.001:0.001:0.999; %! pd1 = TriangularDistribution (0, 1, 2); %! pd2 = TriangularDistribution (1, 2, 3); %! pd3 = TriangularDistribution (2, 3, 4); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"A=0, B=1, C=2", "A=1, B=2, C=3", "A=2, B=3, C=4"}, ... %! "location", "northwest") %! title ("Triangular iCDF") %! xlabel ("Probability") %! ylabel ("Values in x (A <= x <= C)") %! %! ## This demonstrates the inverse CDF (quantiles) for Triangular distributions, %! ## useful for finding values corresponding to given probabilities, such as %! ## thresholds in project planning or quality control. statistics-release-2.1.2/inst/demos/prob.TriangularDistribution.iqr000066400000000000000000000005451527311104300255770ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Triangular distribution %! pd = TriangularDistribution (0, 1, 2); %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, helpful for understanding central %! ## variability in bounded data like cost estimates. statistics-release-2.1.2/inst/demos/prob.TriangularDistribution.mean000066400000000000000000000006201527311104300257160ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Triangular distributions %! pd1 = TriangularDistribution (0, 1, 2); %! pd2 = TriangularDistribution (1, 2, 3); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Triangular distributions %! ## with different parameters, representing the average value in scenarios %! ## like budgeting or time estimation. statistics-release-2.1.2/inst/demos/prob.TriangularDistribution.median000066400000000000000000000006151527311104300262370ustar00rootroot00000000000000%!demo %! ## Compute the median for different Triangular distributions %! pd1 = TriangularDistribution (0, 1, 2); %! pd2 = TriangularDistribution (1, 2, 3); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which splits the distribution into two %! ## equal probability halves, robust for skewed triangular data in applications %! ## like scheduling. statistics-release-2.1.2/inst/demos/prob.TriangularDistribution.pdf000066400000000000000000000013511527311104300255510ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Triangular distribution %! x = -1:0.01:6; %! pd1 = TriangularDistribution (0, 1, 2); %! pd2 = TriangularDistribution (1, 2, 3); %! pd3 = TriangularDistribution (2, 3, 4); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"A=0, B=1, C=2", "A=1, B=2, C=3", "A=2, B=3, C=4"}, ... %! "location", "northeast") %! title ("Triangular PDF") %! xlabel ("Values in x (A <= x <= C)") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Triangular %! ## distributions, showing the likelihood within the bounds, useful for %! ## understanding data distribution in risk analysis. statistics-release-2.1.2/inst/demos/prob.TriangularDistribution.plot000066400000000000000000000017161527311104300257630ustar00rootroot00000000000000%!demo %! ## Create a Triangular distribution with fixed parameters A=0, B=1, C=2 %! ## and plot its PDF. %! pd = TriangularDistribution (0, 1, 2); %! plot (pd) %! title ("Triangular distribution with A=0, B=1, C=2") %! %! ## Use this to visualize the PDF of a Triangular distribution with fixed %! ## parameters, useful for understanding the shape of the distribution. %!demo %! ## Generate a data set of 100 random samples from a Triangular distribution %! ## with parameters A=0, B=1, C=2. Plot its CDF. %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! data = trirnd (0, 1, 2, 100, 1); %! pd = makedist ("Triangular", "A", 0, "B", 1, "C", 2); %! plot (pd, "PlotType", "cdf") %! title ("Triangular distribution with A=0, B=1, C=2") %! legend ({"Fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the CDF of a Triangular distribution, useful for %! ## assessing cumulative probabilities in bounded data scenarios. statistics-release-2.1.2/inst/demos/prob.TriangularDistribution.random000066400000000000000000000006621527311104300262640ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Triangular distribution %! rng (42); %! samples = trirnd (0, 1, 2, 500, 1); %! hist (samples, 50) %! title ("Histogram of 500 random samples from Triangular(A=0, B=1, C=2)") %! xlabel ("Values in x (A <= x <= C)") %! ylabel ("Frequency") %! %! ## This generates random samples from a Triangular distribution, useful for %! ## simulating bounded data like project costs or completion times. statistics-release-2.1.2/inst/demos/prob.TriangularDistribution.std000066400000000000000000000004661527311104300256000ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Triangular distribution %! pd = TriangularDistribution (0, 1, 2); %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## within the bounds of the Triangular distribution, useful for risk assessment. statistics-release-2.1.2/inst/demos/prob.TriangularDistribution.truncate000066400000000000000000000022061527311104300266250ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Triangular distribution with parameters A=0, B=1, C=2, %! ## truncated at [0.5, 1.5] intervals. Generate 10000 random samples from this %! ## truncated distribution and superimpose a histogram scaled accordingly. %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! data_all = trirnd (0, 1, 2, 20000, 1); %! data = data_all(data_all >= 0.5 & data_all <= 1.5); %! data = data(1:10000); %! %! pd = makedist ("Triangular", "A", 0, "B", 1, "C", 2); %! t = truncate (pd, 0.5, 1.5); %! %! [counts, centers] = hist (data, 20); %! bin_width = centers(2) - centers(1); %! bar (centers, counts / (sum (counts) * bin_width), 1); %! hold on; %! %! ## Plot histogram and truncated PDF %! x = linspace (0.5, 5, 500); %! y = pdf (t, x); %! plot (x, y, "r", "linewidth", 2); %! title ("Triangular distribution (A=0, B=1, C=2) truncated at [0.5, 1.5]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a Triangular distribution to a specific range %! ## and visualizing the resulting distribution with random samples, useful for %! ## constrained scenarios like limited budgets. statistics-release-2.1.2/inst/demos/prob.TriangularDistribution.var000066400000000000000000000004561527311104300255750ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Triangular distribution %! pd = TriangularDistribution (0, 1, 2); %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of values %! ## within the bounds of the Triangular distribution, useful for uncertainty analysis. statistics-release-2.1.2/inst/demos/prob.UniformDistribution.Lower000066400000000000000000000015041527311104300253770ustar00rootroot00000000000000%!demo %! ## Create a Uniform distribution with default parameters %! pd = makedist ("Uniform", "Lower", 2, "Upper", 5); %! %! ## Query parameter 'Lower' (lower limit parameter) %! pd.Lower %! %! ## Set parameter 'Lower' %! pd.Lower = 2 %! %! ## Use this to initialize or modify the lower bound of a Uniform distribution. %! ## The lower limit must be a real scalar less than the upper limit, useful for %! ## defining the range of a uniform variable, such as random measurements or time intervals. %!demo %! ## Create a Uniform distribution object by calling its constructor %! pd = UniformDistribution (1, 10) %! %! ## Query parameter 'Lower' %! pd.Lower %! %! ## This demonstrates direct construction with a specific lower limit, ideal for %! ## modeling data uniformly distributed over a known interval, such as random selections. statistics-release-2.1.2/inst/demos/prob.UniformDistribution.Upper000066400000000000000000000013271527311104300254050ustar00rootroot00000000000000%!demo %! ## Create a Uniform distribution with default parameters %! pd = makedist ("Uniform") %! %! ## Query parameter 'Upper' (upper limit parameter) %! pd.Upper %! %! ## Set parameter 'Upper' %! pd.Upper = 8 %! %! ## Use this to initialize or modify the upper bound of a Uniform distribution. %! ## The upper limit must be a real scalar greater than the lower limit, controlling %! ## the range of the distribution. %!demo %! ## Create a Uniform distribution object by calling its constructor %! pd = UniformDistribution (1, 10) %! %! ## Query parameter 'Upper' %! pd.Upper %! %! ## This shows how to set the upper limit directly via the constructor, useful %! ## for defining the maximum value in a uniform distribution. statistics-release-2.1.2/inst/demos/prob.UniformDistribution.cdf000066400000000000000000000014731527311104300250500ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Uniform distribution %! x = -1:0.01:12; %! pd1 = makedist ("Uniform", "Lower", 0, "Upper", 5); %! pd2 = makedist ("Uniform", "Lower", 2, "Upper", 8); %! pd3 = makedist ("Uniform", "Lower", 4, "Upper", 10); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"Lower = 0, Upper = 5", "Lower = 2, Upper = 8", "Lower = 4, Upper = 10"}, ... %! "location", "southeast") %! title ("Uniform CDF") %! xlabel ("Values in x") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Uniform distributions, showing how probability accumulates %! ## over the defined interval, useful in probability assessments or simulations. statistics-release-2.1.2/inst/demos/prob.UniformDistribution.icdf000066400000000000000000000014201527311104300252110ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Uniform distribution %! p = 0.001:0.001:0.999; %! pd1 = makedist ("Uniform", "Lower", 0, "Upper", 5); %! pd2 = makedist ("Uniform", "Lower", 2, "Upper", 8); %! pd3 = makedist ("Uniform", "Lower", 4, "Upper", 10); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"Lower = 0, Upper = 5", "Lower = 2, Upper = 8", "Lower = 4, Upper = 10"}, ... %! "location", "northwest") %! title ("Uniform iCDF") %! xlabel ("Probability") %! ylabel ("Values in x") %! %! ## This demonstrates the inverse CDF (quantiles) for Uniform distributions, %! ## useful for finding values corresponding to given probabilities, such as %! ## thresholds in uniform sampling. statistics-release-2.1.2/inst/demos/prob.UniformDistribution.iqr000066400000000000000000000005501527311104300251020ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Uniform distribution %! pd = makedist ("Uniform", "Lower", 0, "Upper", 10) %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, helpful for understanding central %! ## variability in uniformly distributed data. statistics-release-2.1.2/inst/demos/prob.UniformDistribution.mean000066400000000000000000000005751527311104300252360ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Uniform distributions %! pd1 = makedist ("Uniform", "Lower", 0, "Upper", 5); %! pd2 = makedist ("Uniform", "Lower", 2, "Upper", 8); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Uniform distributions %! ## with different bounds, representing the average value within the interval. statistics-release-2.1.2/inst/demos/prob.UniformDistribution.median000066400000000000000000000006061527311104300255460ustar00rootroot00000000000000%!demo %! ## Compute the median for different Uniform distributions %! pd1 = makedist ("Uniform", "Lower", 0, "Upper", 5); %! pd2 = makedist ("Uniform", "Lower", 2, "Upper", 8); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which splits the distribution %! ## into two equal probability halves, useful for central tendency in uniform data. statistics-release-2.1.2/inst/demos/prob.UniformDistribution.pdf000066400000000000000000000013361527311104300250630ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Uniform distribution %! x = -1:0.01:12; %! pd1 = makedist ("Uniform", "Lower", 0, "Upper", 5); %! pd2 = makedist ("Uniform", "Lower", 2, "Upper", 8); %! pd3 = makedist ("Uniform", "Lower", 4, "Upper", 10); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"Lower = 0, Upper = 5", "Lower = 2, Upper = 8", "Lower = 4, Upper = 10"}, ... %! "location", "northeast") %! title ("Uniform PDF") %! xlabel ("Values in x") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Uniform distributions, %! ## showing the constant likelihood within the defined interval. statistics-release-2.1.2/inst/demos/prob.UniformDistribution.plot000066400000000000000000000027741527311104300252770ustar00rootroot00000000000000%!demo %! ## Create a Uniform distribution with fixed parameters Lower = 0 and Upper = 5 %! ## and plot its PDF. %! pd = makedist ("Uniform", "Lower", 0, "Upper", 5) %! plot (pd) %! title ("Uniform distribution with Lower = 0 and Upper = 5") %! %! ## Use this to visualize the PDF of a Uniform distribution with fixed bounds, %! ## useful for understanding the uniform probability density. %!demo %! ## Generate a data set of 100 random samples from a Uniform distribution %! ## with parameters Lower = 0 and Upper = 5. Fit a Uniform distribution to this %! ## data and plot its CDF superimposed over an empirical CDF. %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd_fixed = makedist ("Uniform", "Lower", 0, "Upper", 5); %! data = random (pd_fixed, 100, 1); %! %! lower_hat = min (data); %! upper_hat = max (data); %! %! [f_empirical, x_empirical] = ecdf (data); %! stairs (x_empirical, f_empirical, 'b', 'LineWidth', 2); %! hold on; %! %! x = linspace (lower_hat - 1, upper_hat + 1, 200); %! y = (x - lower_hat) / (upper_hat - lower_hat); %! y(x < lower_hat) = 0; %! y(x > upper_hat) = 1; %! plot (x, y, 'r-', 'LineWidth', 2); %! %! % Title and legend %! txt = "Fitted Uniform distribution with Lower = %0.2f and Upper = %0.2f"; %! title (sprintf (txt, lower_hat, upper_hat)); %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast"); %! hold off; %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit. statistics-release-2.1.2/inst/demos/prob.UniformDistribution.random000066400000000000000000000010241527311104300255640ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Uniform distribution %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Uniform", "Lower", 0, "Upper", 5) %! samples = random (pd, 500, 1); %! hist (samples, 50) %! title ("Histogram of 500 random samples from Uniform(Lower=0, Upper=5)") %! xlabel ("Values in x") %! ylabel ("Frequency") %! %! ## This generates random samples from a Uniform distribution, useful %! ## for simulating data with equal probability over a fixed interval. statistics-release-2.1.2/inst/demos/prob.UniformDistribution.std000066400000000000000000000004161527311104300251020ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Uniform distribution %! pd = makedist ("Uniform", "Lower", 0, "Upper", 10) %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## within the uniform interval. statistics-release-2.1.2/inst/demos/prob.UniformDistribution.truncate000066400000000000000000000017541527311104300261430ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Uniform distribution, with parameters Lower = 0 %! ## and Upper = 10, truncated at [2, 8] intervals. Generate 10000 random %! ## samples from this truncated distribution and superimpose a histogram scaled %! ## accordingly %! rng (42); %! randg ('state', 42); %! rande ('state', 42); %! randp ('state', 42); %! pd = makedist ("Uniform", "Lower", 0, "Upper", 10) %! t = truncate (pd, 2, 8) %! data = random (t, 10000, 1); %! %! [counts, centers] = hist (data, 50); %! bin_width = centers(2) - centers(1); %! bar (centers, counts / (sum (counts) * bin_width), 1); %! hold on; %! %! ## Plot histogram and truncated PDF %! x = linspace (0.5, 5, 500); %! y = pdf (t, x); %! plot (x, y, "r", "linewidth", 2); %! title ("Uniform distribution (Lower = 0, Upper = 10) truncated at [2, 8]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a Uniform distribution to a specific %! ## range and visualizing the resulting distribution with random samples. statistics-release-2.1.2/inst/demos/prob.UniformDistribution.var000066400000000000000000000004051527311104300250760ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Uniform distribution %! pd = makedist ("Uniform", "Lower", 0, "Upper", 10) %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## values within the uniform interval. statistics-release-2.1.2/inst/demos/prob.WeibullDistribution.cdf000066400000000000000000000016101527311104300250250ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the Weibull distribution %! rande ('state', 42); %! x = 0:0.01:5; %! data1 = wblrnd (1, 0.5, 10000, 1); %! data2 = wblrnd (1, 1, 10000, 1); %! data3 = wblrnd (1, 2, 10000, 1); %! pd1 = fitdist (data1, "Weibull"); %! pd2 = fitdist (data2, "Weibull"); %! pd3 = fitdist (data3, "Weibull"); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"lambda = 1, k = 0.5", "lambda = 1, k = 1", "lambda = 1, k = 2"}, ... %! "location", "southeast") %! title ("Weibull CDF") %! xlabel ("values in x (x >= 0)") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different Weibull distributions, showing how probability accumulates %! ## for positive values, useful in survival analysis or time-to-event modeling. statistics-release-2.1.2/inst/demos/prob.WeibullDistribution.icdf000066400000000000000000000015661527311104300252100ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the Weibull distribution %! rande ('state', 42); %! p = 0.001:0.001:0.999; %! data1 = wblrnd (1, 0.5, 10000, 1); %! data2 = wblrnd (1, 1, 10000, 1); %! data3 = wblrnd (1, 2, 10000, 1); %! pd1 = fitdist (data1, "Weibull"); %! pd2 = fitdist (data2, "Weibull"); %! pd3 = fitdist (data3, "Weibull"); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"lambda = 1, k = 0.5", "lambda = 1, k = 1", "lambda = 1, k = 2"}, ... %! "location", "northwest") %! title ("Weibull iCDF") %! xlabel ("Probability") %! ylabel ("values in x (x >= 0)") %! %! ## This demonstrates the inverse CDF (quantiles) for Weibull distributions, %! ## useful for finding values corresponding to given probabilities, such as %! ## predicting failure times in engineering applications. statistics-release-2.1.2/inst/demos/prob.WeibullDistribution.iqr000066400000000000000000000005731527311104300250730ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a Weibull distribution %! rande ('state', 42); %! data = wblrnd (1, 1, 10000, 1); %! pd = fitdist (data, "Weibull"); %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, helpful for understanding variability %! ## in lifetime data. statistics-release-2.1.2/inst/demos/prob.WeibullDistribution.k000066400000000000000000000014451527311104300245310ustar00rootroot00000000000000%!demo %! ## Create a Weibull distribution with default parameters %! rande ('state', 42); %! data = wblrnd (1, 1, 10000, 1); %! pd = fitdist (data, "Weibull"); %! %! ## Query parameter 'k' (shape parameter) %! pd.k %! %! ## Set parameter 'k' %! pd.k = 2 %! %! ## Use this to initialize or modify the shape parameter in a Weibull %! ## distribution. The shape parameter must be a positive real scalar, controlling %! ## the failure rate behavior (k<1 decreasing, k=1 constant, k>1 increasing). %!demo %! ## Create a Weibull distribution object by calling its constructor %! pd = WeibullDistribution (1, 3) %! %! ## Query parameter 'k' %! pd.k %! %! ## This shows how to set the shape parameter directly via the constructor, %! ## ideal for modeling different hazard functions in reliability engineering. statistics-release-2.1.2/inst/demos/prob.WeibullDistribution.lambda000066400000000000000000000016111527311104300255120ustar00rootroot00000000000000%!demo %! ## Create a Weibull distribution with default parameters %! rande ('state', 42); %! data = wblrnd (1, 1, 10000, 1); %! pd = fitdist (data, "Weibull"); %! %! ## Query parameter 'lambda' (scale parameter) %! pd.lambda %! %! ## Set parameter 'lambda' %! pd.lambda = 1.5 %! %! ## Use this to initialize or modify the scale parameter of a Weibull %! ## distribution. The scale parameter must be a positive real scalar, often %! ## representing the characteristic life in reliability analysis or the spread %! ## in lifetime modeling. %!demo %! ## Create a Weibull distribution object by calling its constructor %! pd = WeibullDistribution (2, 1.5) %! %! ## Query parameter 'lambda' %! pd.lambda %! %! ## This demonstrates direct construction with a specific scale parameter, %! ## useful for modeling failure times where lambda shifts the distribution, %! ## such as in survival or wind speed data. statistics-release-2.1.2/inst/demos/prob.WeibullDistribution.mean000066400000000000000000000006741527311104300252220ustar00rootroot00000000000000%!demo %! ## Compute the mean for different Weibull distributions %! rande ('state', 42); %! data1 = wblrnd (1, 0.5, 10000, 1); %! data2 = wblrnd (1, 2, 10000, 1); %! pd1 = fitdist (data1, "Weibull"); %! pd2 = fitdist (data2, "Weibull"); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for Weibull distributions %! ## with different shape parameters, representing average lifetime or duration. statistics-release-2.1.2/inst/demos/prob.WeibullDistribution.median000066400000000000000000000007051527311104300255320ustar00rootroot00000000000000%!demo %! ## Compute the median for different Weibull distributions %! rande ('state', 42); %! data1 = wblrnd (1, 0.5, 10000, 1); %! data2 = wblrnd (1, 2, 10000, 1); %! pd1 = fitdist (data1, "Weibull"); %! pd2 = fitdist (data2, "Weibull"); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which splits the distribution into %! ## two equal probability halves, robust to skewness in positive lifetime data. statistics-release-2.1.2/inst/demos/prob.WeibullDistribution.negloglik000066400000000000000000000007251527311104300262520ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted Weibull distribution %! rng (42); %! rande ('state', 42); %! data = wblrnd (1, 1, 100, 1); %! pd_fitted = fitdist (data, "Weibull"); %! params = [pd_fitted.lambda, pd_fitted.k]; %! nlogL_wbllike = wbllike (params, data) %! %! ## This is useful for assessing the fit of a Weibull distribution to data, %! ## with lower values indicating a better fit, often used in model selection %! ## for reliability data. statistics-release-2.1.2/inst/demos/prob.WeibullDistribution.paramci000066400000000000000000000007221527311104300257100ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted Weibull distribution %! rng (42); %! rande ('state', 42); %! data = wblrnd (1, 1, 1000, 1); %! pd_fitted = fitdist (data, "Weibull"); %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameters %! ## (lambda and k), providing a range of plausible values given the data, %! ## essential in uncertainty quantification for failure models. statistics-release-2.1.2/inst/demos/prob.WeibullDistribution.pdf000066400000000000000000000015121527311104300250430ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the Weibull distribution %! rande ('state', 42); %! x = 0:0.01:5; %! data1 = wblrnd (1, 0.5, 10000, 1); %! data2 = wblrnd (1, 1, 10000, 1); %! data3 = wblrnd (1, 2, 10000, 1); %! pd1 = fitdist (data1, "Weibull"); %! pd2 = fitdist (data2, "Weibull"); %! pd3 = fitdist (data3, "Weibull"); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"lambda = 1, k = 0.5", "lambda = 1, k = 1", "lambda = 1, k = 2"}, ... %! "location", "northeast") %! title ("Weibull PDF") %! xlabel ("values in x (x >= 0)") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for Weibull distributions, %! ## showing the likelihood for positive values, common in wind speed or failure rate modeling. statistics-release-2.1.2/inst/demos/prob.WeibullDistribution.plot000066400000000000000000000034201527311104300252500ustar00rootroot00000000000000%!demo %! ## Create a Weibull distribution with fixed parameters lambda = 1 and %! ## k = 1 and plot its PDF. %! %! rande ('state', 42); %! data = wblrnd (1, 1, 10000, 1); %! pd = fitdist (data, "Weibull"); %! plot (pd) %! title ("Fixed Weibull distribution with lambda = 1 and k = 1") %!demo %! ## Generate a data set of 100 random samples from a Weibull distribution %! ## with parameters lambda = 1 and k = 1. Fit a Weibull distribution to %! ## this data and plot its CDF superimposed over an empirical CDF. %! %! rng (42); %! rande ('state', 42); %! data = wblrnd (1, 1, 100, 1); %! pd_fitted = fitdist (data, "Weibull"); %! plot (pd_fitted, "PlotType", "cdf") %! txt = "Fitted Weibull distribution with lambda = %0.2f and k = %0.2f"; %! title (sprintf (txt, pd_fitted.lambda, pd_fitted.k)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit in time-to-failure scenarios. %!demo %! ## Generate a data set of 200 random samples from a Weibull distribution %! ## with parameters lambda = 1 and k = 1. Display a probability plot for %! ## the Weibull distribution fit to the data. %! %! rng (42); %! rande ('state', 42); %! data = wblrnd (1, 1, 200, 1); %! pd_fitted = fitdist (data, "Weibull"); %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted Weibull distribution", ... %! " with lambda = %0.2f and k = %0.2f"); %! title (sprintf (txt, pd_fitted.lambda, pd_fitted.k)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted distribution to the %! ## data, useful for validating the Weibull assumption in reliability studies. statistics-release-2.1.2/inst/demos/prob.WeibullDistribution.proflik000066400000000000000000000007161527311104300257450ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the shape parameter of a fitted %! ## Weibull distribution %! rng (42); %! rande ('state', 42); %! data = wblrnd (1, 1, 1000, 1); %! pd_fitted = fitdist (data, "Weibull"); %! [nlogL, param] = proflik (pd_fitted, 2, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the shape parameter (k), %! ## helping to understand the uncertainty in parameter estimates for failure rate models. statistics-release-2.1.2/inst/demos/prob.WeibullDistribution.random000066400000000000000000000006641527311104300255610ustar00rootroot00000000000000%!demo %! ## Generate random samples from a Weibull distribution %! rng (42); %! rande ('state', 42); %! samples = wblrnd (1, 1, 500, 1); %! hist (samples, 50) %! title ("Histogram of 500 random samples from Weibull(lambda=1, k=1)") %! xlabel ("values in x (x >= 0)") %! ylabel ("Frequency") %! %! ## This generates random samples from a Weibull distribution, useful for %! ## simulating lifetime data or positive skewed distributions. statistics-release-2.1.2/inst/demos/prob.WeibullDistribution.std000066400000000000000000000005121527311104300250630ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a Weibull distribution %! rande ('state', 42); %! data = wblrnd (1, 1, 10000, 1); %! pd = fitdist (data, "Weibull"); %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## in lifetime or positive data modeled by Weibull. statistics-release-2.1.2/inst/demos/prob.WeibullDistribution.truncate000066400000000000000000000020501527311104300261150ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a Weibull distribution, with parameters lambda = 1 %! ## and k = 1, truncated at [0.5, 2] intervals. Generate 10000 random %! ## samples from this truncated distribution and superimpose a histogram scaled accordingly %! %! rng (42); %! rande ('state', 42); %! data_all = wblrnd (1, 1, 20000, 1); %! data = data_all(data_all >= 0.5 & data_all <= 2); %! data = data(1:9000); %! %! pd = fitdist (data, "Weibull"); %! t = truncate (pd, 0.5, 2); %! %! [counts, centers] = hist (data, 50); %! bin_width = centers(2) - centers(1); %! bar (centers, counts / (sum (counts) * bin_width), 1); %! hold on; %! %! ## Plot histogram and truncated PDF %! x = linspace (0.5, 5, 500); %! y = pdf (t, x); %! plot (x, y, "r", "linewidth", 2); %! title ("Weibull distribution (lambda = 1, k = 1) truncated at [0.5, 2]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a Weibull distribution to a specific range %! ## and visualizing the resulting distribution with random samples, useful for %! ## bounded lifetime analysis. statistics-release-2.1.2/inst/demos/prob.WeibullDistribution.var000066400000000000000000000004721527311104300250660ustar00rootroot00000000000000%!demo %! ## Compute the variance for a Weibull distribution %! rande ('state', 42); %! data = wblrnd (1, 1, 10000, 1); %! pd = fitdist (data, "Weibull"); %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## positive values in lifetime or reliability data. statistics-release-2.1.2/inst/demos/prob.tLocationScaleDistribution.cdf000066400000000000000000000017021527311104300263300ustar00rootroot00000000000000%!demo %! ## Plot various CDFs from the t Location-Scale distribution %! rng (42); %! randg ('state', 42); %! x = -5:0.01:5; %! data1 = tlsrnd (0, 0.5, 5, 10000, 1); %! data2 = tlsrnd (0, 1, 5, 10000, 1); %! data3 = tlsrnd (0, 2, 5, 10000, 1); %! pd1 = fitdist (data1, "tLocationScale"); %! pd2 = fitdist (data2, "tLocationScale"); %! pd3 = fitdist (data3, "tLocationScale"); %! p1 = cdf (pd1, x); %! p2 = cdf (pd2, x); %! p3 = cdf (pd3, x); %! plot (x, p1, "-b", x, p2, "-g", x, p3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5, nu = 5", "mu = 0, sigma = 1, nu = 5", ... %! "mu = 0, sigma = 2, nu = 5"}, "location", "southeast") %! title ("t Location-Scale CDF") %! xlabel ("Values in x") %! ylabel ("Cumulative probability") %! %! ## Use this to compute and visualize the cumulative distribution function %! ## for different t Location-Scale distributions, showing how probability %! ## accumulates, useful in risk analysis or hypothesis testing. statistics-release-2.1.2/inst/demos/prob.tLocationScaleDistribution.icdf000066400000000000000000000016201527311104300265000ustar00rootroot00000000000000%!demo %! ## Plot various iCDFs from the t Location-Scale distribution %! rng (42); %! randg ('state', 42); %! p = 0.001:0.001:0.999; %! data1 = tlsrnd (0, 0.5, 5, 10000, 1); %! data2 = tlsrnd (0, 1, 5, 10000, 1); %! data3 = tlsrnd (0, 2, 5, 10000, 1); %! pd1 = fitdist (data1, "tLocationScale"); %! pd2 = fitdist (data2, "tLocationScale"); %! pd3 = fitdist (data3, "tLocationScale"); %! x1 = icdf (pd1, p); %! x2 = icdf (pd2, p); %! x3 = icdf (pd3, p); %! plot (p, x1, "-b", p, x2, "-g", p, x3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5, nu = 5", "mu = 0, sigma = 1, nu = 5", ... %! "mu = 0, sigma = 2, nu = 5"}, "location", "northwest") %! title ("t Location-Scale iCDF") %! xlabel ("Probability") %! ylabel ("Values in x") %! %! ## This demonstrates the inverse CDF (quantiles) for t Location-Scale %! ## distributions, useful for finding critical values or thresholds in statistical testing. statistics-release-2.1.2/inst/demos/prob.tLocationScaleDistribution.iqr000066400000000000000000000006101527311104300263640ustar00rootroot00000000000000%!demo %! ## Compute the interquartile range for a t Location-Scale distribution %! rng (42); %! randg ('state', 42); %! data = tlsrnd (0, 1, 5, 10000, 1); %! pd = fitdist (data, "tLocationScale"); %! iqr_value = iqr (pd) %! %! ## Use this to calculate the interquartile range, which measures the spread %! ## of the middle 50% of the distribution, helpful for assessing central variability. statistics-release-2.1.2/inst/demos/prob.tLocationScaleDistribution.mean000066400000000000000000000007651527311104300265240ustar00rootroot00000000000000%!demo %! ## Compute the mean for different t Location-Scale distributions %! rng (42); %! randg ('state', 42); %! data1 = tlsrnd (0, 0.5, 5, 10000, 1); %! data2 = tlsrnd (0, 1, 5, 10000, 1); %! pd1 = fitdist (data1, "tLocationScale"); %! pd2 = fitdist (data2, "tLocationScale"); %! mean1 = mean (pd1) %! mean2 = mean (pd2) %! %! ## This shows how to compute the expected value for t Location-Scale %! ## distributions, representing the average value, useful in financial modeling or quality control. statistics-release-2.1.2/inst/demos/prob.tLocationScaleDistribution.median000066400000000000000000000007361527311104300270370ustar00rootroot00000000000000%!demo %! ## Compute the median for different t Location-Scale distributions %! rng (42); %! randg ('state', 42); %! data1 = tlsrnd (0, 0.5, 5, 10000, 1); %! data2 = tlsrnd (0, 1, 5, 10000, 1); %! pd1 = fitdist (data1, "tLocationScale"); %! pd2 = fitdist (data2, "tLocationScale"); %! median1 = median (pd1) %! median2 = median (pd2) %! %! ## Use this to find the median value, which splits the distribution into %! ## two equal probability halves, robust to heavy-tailed data. statistics-release-2.1.2/inst/demos/prob.tLocationScaleDistribution.mu000066400000000000000000000016261527311104300262220ustar00rootroot00000000000000%!demo %! ## Create a t Location-Scale distribution by fitting to data %! rng (42); %! randg ('state', 42); %! data = tlsrnd (0, 1, 5, 10000, 1); % Generate data with mu=0, sigma=1, nu=5 %! pd = fitdist (data, "tLocationScale"); %! %! ## Query parameter 'mu' (location parameter) %! pd.mu %! %! ## Set parameter 'mu' %! pd.mu = 1 %! %! ## Use this to initialize or modify the location parameter of a t Location-Scale %! ## distribution. The location parameter (mu) is a real scalar that shifts the %! ## distribution, useful for modeling data centered around a specific value. %!demo %! ## Create a t Location-Scale distribution object by calling its constructor %! pd = tLocationScaleDistribution (2, 1, 5); %! %! ## Query parameter 'mu' %! pd.mu %! %! ## This demonstrates direct construction with a specific location parameter, %! ## ideal for modeling data with a known center, such as test scores or residuals. statistics-release-2.1.2/inst/demos/prob.tLocationScaleDistribution.negloglik000066400000000000000000000007451527311104300275550ustar00rootroot00000000000000%!demo %! ## Compute the negative loglikelihood for a fitted t Location-Scale distribution %! rng (42); %! randg ('state', 42); %! data = tlsrnd (0, 1, 5, 100, 1); %! pd_fitted = fitdist (data, "tLocationScale"); %! params = [pd_fitted.mu, pd_fitted.sigma, pd_fitted.nu]; %! nlogL_tlslike = tlslike (params, data) %! %! ## This is useful for assessing the fit of a t Location-Scale distribution to %! ## data, with lower values indicating a better fit, often used in model comparison. statistics-release-2.1.2/inst/demos/prob.tLocationScaleDistribution.nu000066400000000000000000000016421527311104300262210ustar00rootroot00000000000000%!demo %! ## Create a t Location-Scale distribution with fitted parameters %! rng (42); %! randg ('state', 42); %! data = tlsrnd (0, 1, 5, 10000, 1); % Generate data with mu=0, sigma=1, nu=5 %! pd = fitdist (data, "tLocationScale"); %! %! ## Query parameter 'nu' (degrees of freedom) %! pd.nu %! %! ## Set parameter 'nu' %! pd.nu = 10 %! %! ## Use this to initialize or modify the degrees of freedom, which controls the %! ## tail heaviness of the t Location-Scale distribution. Nu must be a positive %! ## real scalar, useful for modeling heavy-tailed data like stock returns. %!demo %! ## Create a t Location-Scale distribution object by calling its constructor %! pd = tLocationScaleDistribution (0, 1, 3); %! %! ## Query parameter 'nu' %! pd.nu %! %! ## This demonstrates setting the degrees of freedom directly via the constructor, %! ## ideal for modeling data with specific tail behavior, such as outlier-prone datasets. statistics-release-2.1.2/inst/demos/prob.tLocationScaleDistribution.paramci000066400000000000000000000006521527311104300272130ustar00rootroot00000000000000%!demo %! ## Compute confidence intervals for parameters of a fitted t Location-Scale %! ## distribution %! rng (42); %! randg ('state', 42); %! data = tlsrnd (0, 1, 5, 1000, 1); %! pd_fitted = fitdist (data, "tLocationScale"); %! ci = paramci (pd_fitted, "Alpha", 0.05) %! %! ## Use this to obtain confidence intervals for the estimated parameters (mu, %! ## sigma, nu), providing a range of plausible values given the data. statistics-release-2.1.2/inst/demos/prob.tLocationScaleDistribution.pdf000066400000000000000000000016051527311104300263470ustar00rootroot00000000000000%!demo %! ## Plot various PDFs from the t Location-Scale distribution %! rng (42); %! randg ('state', 42); %! x = -5:0.01:5; %! data1 = tlsrnd (0, 0.5, 5, 10000, 1); %! data2 = tlsrnd (0, 1, 5, 10000, 1); %! data3 = tlsrnd (0, 2, 5, 10000, 1); %! pd1 = fitdist (data1, "tLocationScale"); %! pd2 = fitdist (data2, "tLocationScale"); %! pd3 = fitdist (data3, "tLocationScale"); %! y1 = pdf (pd1, x); %! y2 = pdf (pd2, x); %! y3 = pdf (pd3, x); %! plot (x, y1, "-b", x, y2, "-g", x, y3, "-r") %! grid on %! legend ({"mu = 0, sigma = 0.5, nu = 5", "mu = 0, sigma = 1, nu = 5", ... %! "mu = 0, sigma = 2, nu = 5"}, "location", "northeast") %! title ("t Location-Scale PDF") %! xlabel ("Values in x") %! ylabel ("Probability density") %! %! ## This visualizes the probability density function for t Location-Scale %! ## distributions, showing the likelihood across values, useful for data analysis. statistics-release-2.1.2/inst/demos/prob.tLocationScaleDistribution.plot000066400000000000000000000032431527311104300265540ustar00rootroot00000000000000%!demo %! ## Create a t Location-Scale distribution with fixed parameters and plot its PDF %! pd = tLocationScaleDistribution (0, 1, 5); %! plot (pd) %! title ("t Location-Scale distribution with mu = 0, sigma = 1, nu = 5") %! %! ## Use this to visualize the PDF of a t Location-Scale distribution with %! ## fixed parameters, helpful for theoretical exploration. %!demo %! ## Generate a data set and plot the CDF of a fitted t Location-Scale distribution %! rng (42); %! randg ('state', 42); %! data = tlsrnd (0, 1, 5, 100, 1); %! pd_fitted = fitdist (data, "tLocationScale"); %! plot (pd_fitted, "PlotType", "cdf") %! txt = "Fitted t Location-Scale distribution with mu = %0.2f, sigma = %0.2f, nu = %0.2f"; %! title (sprintf (txt, pd_fitted.mu, pd_fitted.sigma, pd_fitted.nu)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## Use this to visualize the fitted CDF compared to the empirical CDF of the %! ## data, useful for assessing model fit. %!demo %! ## Generate a data set and display a probability plot for a fitted t Location-Scale distribution %! rng (42); %! randg ('state', 42); %! data = tlsrnd (0, 1, 5, 200, 1); %! pd_fitted = fitdist (data, "tLocationScale"); %! plot (pd_fitted, "PlotType", "probability") %! txt = strcat ("Probability plot of fitted t Location-Scale", ... %! " distribution with mu = %0.2f, sigma = %0.2f, nu = %0.2f"); %! title (sprintf (txt, pd_fitted.mu, pd_fitted.sigma, pd_fitted.nu)) %! legend ({"empirical CDF", "fitted CDF"}, "location", "southeast") %! %! ## This creates a probability plot to compare the fitted distribution to the %! ## data, useful for checking if the t Location-Scale model is appropriate. statistics-release-2.1.2/inst/demos/prob.tLocationScaleDistribution.proflik000066400000000000000000000006351527311104300272460ustar00rootroot00000000000000%!demo %! ## Compute and plot the profile likelihood for the scale parameter %! rng (42); %! randg ('state', 42); %! data = tlsrnd (0, 1, 5, 1000, 1); %! pd_fitted = fitdist (data, "tLocationScale"); %! [nlogL, param] = proflik (pd_fitted, 2, "Display", "on"); %! %! ## Use this to analyze the profile likelihood of the scale parameter (sigma), %! ## helping to understand the uncertainty in parameter estimates. statistics-release-2.1.2/inst/demos/prob.tLocationScaleDistribution.random000066400000000000000000000007361527311104300270620ustar00rootroot00000000000000%!demo %! ## Generate random samples from a t Location-Scale distribution %! rng (42); %! randg ('state', 42); %! samples = tlsrnd (0, 1, 5, 500, 1); %! hist (samples, 50) %! title ("Histogram of 500 random samples from t Location-Scale(mu=0, sigma=1, nu=5)") %! xlabel ("Values in x") %! ylabel ("Frequency") %! %! ## This generates random samples from a t Location-Scale distribution, useful %! ## for simulating data with heavy tails, such as financial or experimental data. statistics-release-2.1.2/inst/demos/prob.tLocationScaleDistribution.sigma000066400000000000000000000016401527311104300266750ustar00rootroot00000000000000%!demo %! ## Create a t Location-Scale distribution with fitted parameters %! rng (42); %! randg ('state', 42); %! data = tlsrnd (0, 1, 5, 10000, 1); % Generate data with mu=0, sigma=1, nu=5 %! pd = fitdist (data, "tLocationScale"); %! %! ## Query parameter 'sigma' (scale parameter) %! pd.sigma %! %! ## Set parameter 'sigma' %! pd.sigma = 2 %! %! ## Use this to initialize or modify the scale parameter, which controls the %! ## spread of the t Location-Scale distribution. Sigma must be a positive real %! ## scalar, useful for modeling variability in data like financial returns. %!demo %! ## Create a t Location-Scale distribution object by calling its constructor %! pd = tLocationScaleDistribution (0, 1.5, 5); %! %! ## Query parameter 'sigma' %! pd.sigma %! %! ## This shows how to set the scale parameter directly via the constructor, %! ## useful for modeling data with specific variability, such as process errors. statistics-release-2.1.2/inst/demos/prob.tLocationScaleDistribution.std000066400000000000000000000005701527311104300263700ustar00rootroot00000000000000%!demo %! ## Compute the standard deviation for a t Location-Scale distribution %! rng (42); %! randg ('state', 42); %! data = tlsrnd (0, 1, 5, 10000, 1); %! pd = fitdist (data, "tLocationScale"); %! std_value = std (pd) %! %! ## Use this to calculate the standard deviation, which measures the variability %! ## in the distribution, useful for understanding data dispersion. statistics-release-2.1.2/inst/demos/prob.tLocationScaleDistribution.truncate000066400000000000000000000015541527311104300274260ustar00rootroot00000000000000%!demo %! ## Plot the PDF of a truncated t Location-Scale distribution %! rng (42); %! randg ('state', 42); %! data_all = tlsrnd (0, 1, 5, 20000, 1); %! data = data_all(data_all >= -1 & data_all <= 1); %! data = data(1:10000); %! %! pd = fitdist (data, "tLocationScale"); %! t = truncate (pd, -1, 1); %! %! [counts, centers] = hist (data, 50); %! bin_width = centers(2) - centers(1); %! bar (centers, counts / (sum (counts) * bin_width), 1); %! hold on; %! %! ## Plot histogram and truncated PDF %! x = linspace (0.5, 5, 500); %! y = pdf (t, x); %! plot (x, y, "r", "linewidth", 2); %! title ("t Location-Scale distribution (mu = 0, sigma = 1, nu = 5) truncated at [-1, 1]") %! legend ("Truncated PDF", "Histogram") %! %! ## This demonstrates truncating a t Location-Scale distribution to a specific %! ## range and visualizing the resulting distribution with random samples. statistics-release-2.1.2/inst/demos/prob.tLocationScaleDistribution.var000066400000000000000000000005471527311104300263720ustar00rootroot00000000000000%!demo %! ## Compute the variance for a t Location-Scale distribution %! rng (42); %! randg ('state', 42); %! data = tlsrnd (0, 1, 5, 10000, 1); %! pd = fitdist (data, "tLocationScale"); %! var_value = var (pd) %! %! ## Use this to calculate the variance, which quantifies the spread of the %! ## distribution, useful for statistical analysis of variability. statistics-release-2.1.2/inst/demos/shapley.boxchart000066400000000000000000000023721527311104300226120ustar00rootroot00000000000000%!demo %! ## 1. How widely a predictor's contribution varies %! %! load fisheriris %! mdl = fitrtree (meas(:,2:4), meas(:,1)); %! s = shapley (mdl, 'QueryPoints', meas(1:5:150,2:4), ... %! 'NumObservationsToSample', 'all'); %! %! ## One box per predictor over the query points: the box spans the %! ## quartiles, and a predictor whose box is wide contributes differently %! ## from one point to the next %! boxchart (s); %!demo %! ## 2. The values that lie beyond the whiskers %! %! load fisheriris %! mdl = fitrtree (meas(:,2:4), meas(:,1)); %! s = shapley (mdl, 'QueryPoints', meas(1:5:150,2:4), ... %! 'NumObservationsToSample', 'all'); %! %! ## `JitterOutliers` spreads the outlier markers across the width of the %! ## box rather than drawing them in a line, so points at the same value %! ## stop hiding one another %! boxchart (s, 'JitterOutliers', 'on'); %!demo %! ## 3. One class of a classifier %! %! load fisheriris %! mdl = fitctree (meas, species); %! s = shapley (mdl, 'QueryPoints', meas(1:5:150,:), ... %! 'NumObservationsToSample', 'all'); %! %! ## A box chart draws one class at a time, the first of them where none %! ## is named, and orders the predictors by that class %! boxchart (s, 'ClassName', 'virginica'); statistics-release-2.1.2/inst/demos/shapley.plot000066400000000000000000000031111527311104300217460ustar00rootroot00000000000000%!demo %! ## 1. Explaining one prediction %! %! load fisheriris %! mdl = fitrtree (meas(:,2:4), meas(:,1)); %! s = shapley (mdl, 'QueryPoints', meas(1,2:4), ... %! 'NumObservationsToSample', 'all'); %! %! ## One value per predictor, summing to the deviation of the prediction %! ## from the average prediction %! s.Shapley %! %! ## A bar per predictor, the most important at the top %! plot (s); %!demo %! ## 2. Which predictors matter over many points %! %! load fisheriris %! mdl = fitrtree (meas(:,2:4), meas(:,1)); %! s = shapley (mdl, 'QueryPoints', meas(1:5:150,2:4), ... %! 'NumObservationsToSample', 'all'); %! %! ## Over several query points the bars hold the mean of the absolute %! ## values, so the chart reads as an importance plot rather than as an %! ## explanation of any one point %! plot (s); %!demo %! ## 3. The lesser predictors gathered into one bar %! %! load fisheriris %! mdl = fitrtree (meas(:,2:4), meas(:,1)); %! s = shapley (mdl, 'QueryPoints', meas(1:5:150,2:4), ... %! 'NumObservationsToSample', 'all'); %! %! ## Whatever `NumImportantPredictors` leaves out is summed into one %! ## further bar, so nothing goes missing from the picture %! plot (s, 'NumImportantPredictors', 1); %!demo %! ## 4. A classifier is explained one class at a time %! %! load fisheriris %! mdl = fitctree (meas, species); %! s = shapley (mdl, 'QueryPoints', meas([1, 60, 120],:), ... %! 'NumObservationsToSample', 'all'); %! %! ## Over several query points every class is drawn, in the model's own %! ## order, and a legend names them %! plot (s); statistics-release-2.1.2/inst/demos/shapley.plotDependence000066400000000000000000000025201527311104300237240ustar00rootroot00000000000000%!demo %! ## 1. A predictor's contribution against its own value %! %! load fisheriris %! mdl = fitrtree (meas(:,2:4), meas(:,1)); %! s = shapley (mdl, 'QueryPoints', meas(1:5:150,2:4), ... %! 'NumObservationsToSample', 'all'); %! %! ## One point per query point: the predictor's value across, its Shapley %! ## value up. A rising cloud says the predictor pushes the prediction up %! ## the larger it gets %! plotDependence (s, 'x2'); %!demo %! ## 2. A second predictor brought in as colour %! %! load fisheriris %! mdl = fitrtree (meas(:,2:4), meas(:,1)); %! s = shapley (mdl, 'QueryPoints', meas(1:5:150,2:4), ... %! 'NumObservationsToSample', 'all'); %! %! ## `ColorPredictor` colours each point by another predictor's value and %! ## draws a colour bar for the scale, which is how an interaction between %! ## the two shows itself %! plotDependence (s, 'x2', 'ColorPredictor', 'x3'); %!demo %! ## 3. A predictor holding levels %! %! load fisheriris %! X = [meas(:,2:3), double(strcmp (species, 'setosa'))]; %! mdl = fitrtree (X, meas(:,1), 'CategoricalPredictors', 3); %! s = shapley (mdl, 'QueryPoints', X(1:5:150,:), ... %! 'NumObservationsToSample', 'all'); %! %! ## Where the predictor holds levels rather than numbers the chart is a %! ## box over each level instead of a scatter %! plotDependence (s, 'x3'); statistics-release-2.1.2/inst/demos/shapley.swarmchart000066400000000000000000000024141527311104300231500ustar00rootroot00000000000000%!demo %! ## 1. Every query point, coloured by the predictor's own value %! %! load fisheriris %! mdl = fitrtree (meas(:,2:4), meas(:,1)); %! s = shapley (mdl, 'QueryPoints', meas(1:5:150,2:4), ... %! 'NumObservationsToSample', 'all'); %! %! ## One point per query point per predictor, spread by how crowded its %! ## neighbourhood is. Colour says where the predictor's value stands in %! ## its own range, so a row whose colours run from one end to the other %! ## shows the contribution following the predictor %! swarmchart (s); %!demo %! ## 2. Choosing the colours and the spreading %! %! load fisheriris %! mdl = fitrtree (meas(:,2:4), meas(:,1)); %! s = shapley (mdl, 'QueryPoints', meas(1:5:150,2:4), ... %! 'NumObservationsToSample', 'all'); %! %! ## `ColorMap` takes a name or a matrix of RGB triplets, and `YJitter` %! ## takes `'rand'` where an even spread reads better than a dense one %! swarmchart (s, 'ColorMap', 'cool', 'YJitter', 'rand'); %!demo %! ## 3. One class of a classifier %! %! load fisheriris %! mdl = fitctree (meas, species); %! s = shapley (mdl, 'QueryPoints', meas(1:5:150,:), ... %! 'NumObservationsToSample', 'all'); %! %! ## As for a box chart, one class is drawn at a time %! swarmchart (s, 'ClassName', 'setosa'); statistics-release-2.1.2/inst/doc-cache000066400000000000000000000263341527311104300200370ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 7 # name: # type: sq_string # elements: 1 # length: 7 cholcov # name: # type: sq_string # elements: 1 # length: 1250 statistics: T = cholcov (sigma) statistics: [T, p] = cholcov (sigma) statistics: [...] = cholcov (sigma, flag) Cholesky-like decomposition for covariance matrix. T = cholcov (sigma) computes matrix T such that sigma = T' T. sigma must be square, symmetric, and positive semi-definite. If sigma is positive definite, then T is the square, upper triangular Cholesky factor. If sigma is not positive definite, T is computed with an eigenvalue decomposition of sigma, but in this case T is not necessarily triangular or square. Any eigenvectors whose corresponding eigenvalue is close to zero (within a tolerance) are omitted. If any remaining eigenvalues are negative, T is empty. The tolerance is calculated as 10 * eps (max (abs (diag (sigma)))). [T, p] = cholcov (sigma) returns in p the number of negative eigenvalues of sigma. If p > 0, then T is empty, whereas if p = 0, sigma) is positive semi-definite. If sigma is not square and symmetric, P is NaN and T is empty. [T, p] = cholcov (sigma, 0) returns p = 0 if sigma is positive definite, in which case T is the Cholesky factor. If sigma is not positive definite, p is a positive integer and T is empty. [...] = cholcov (sigma, 1) is equivalent to [...] = cholcov (sigma). See also: chol # name: # type: sq_string # elements: 1 # length: 50 Cholesky-like decomposition for covariance matrix. # name: # type: sq_string # elements: 1 # length: 9 loadmodel # name: # type: sq_string # elements: 1 # length: 395 statistics: obj = loadmodel (filename) Load a Classification or Regression model from a file. obj = loadmodel (filename) loads a Classification or Regression object, obj, from a file defined in filename. See also: ClassificationDiscriminant, ClassificationGAM, ClassificationKNN, ClassificationNeuralNetwork, ClassificationPartitionedModel, ClassificationSVM, ClassificationTree, RegressionGAM # name: # type: sq_string # elements: 1 # length: 54 Load a Classification or Regression model from a file. # name: # type: sq_string # elements: 1 # length: 5 logit # name: # type: sq_string # elements: 1 # length: 166 statistics: x = logit (p) Compute the logit for each value of p The logit is defined as $$ {\rm logit}(p) = \log\Big({p \over 1-p}\Big) $$ See also: probit, logicdf # name: # type: sq_string # elements: 1 # length: 37 Compute the logit for each value of p # name: # type: sq_string # elements: 1 # length: 6 makima # name: # type: sq_string # elements: 1 # length: 1776 statistics: yi = makima (x, y, xq) statistics: yi = makima (y, xq) statistics: yi = makima (..., 'extrap') Compute the 1-D Modified Akima piecewise cubic Hermite interpolant of sample data x and y. The Modified Akima (MAKIMA) algorithm generates a shape-preserving piecewise cubic interpolant. It differs from standard splines by avoiding excessive local undulations and overshoots, and it connects collinear points (flat regions) with straight lines. It is particularly well-suited for oscillatory data where pchip might aggressively flatten local extrema. The sample points x must be a vector of unique values. If x is not sorted, the function will automatically sort it and rearrange y accordingly. The sample values y can be a scalar, vector, or an N-dimensional array. If y is an N-dimensional array, the interpolation is performed along its last dimension, which must have the same length as x. Complex values for y are supported, and their real and imaginary parts are interpolated independently. Sample points where x or y is NaN are dropped in pairs before the fit, and a warning is issued. If query points xq are provided, the function evaluates the interpolant and returns the interpolated values yi. By default, makima uses the boundary polynomials to extrapolate for points outside the range of x. The optional string argument 'extrap' is accepted for compatibility with other interpolation functions. If only x and y are provided, the function returns a piecewise polynomial structure pp that represents the interpolant. This structure can be evaluated later at specific query points using ppval. Evaluating the interpolant at query points outside the domain of x automatically extrapolates using the boundary polynomials. See also: interp1, pchip, spline # name: # type: sq_string # elements: 1 # length: 90 Compute the 1-D Modified Akima piecewise cubic Hermite interpolant of sample data x and y. # name: # type: sq_string # elements: 1 # length: 6 probit # name: # type: sq_string # elements: 1 # length: 158 statistics: x = probit (p) Probit transformation Return the probit (the quantile of the standard normal distribution) for each element of p. See also: logit # name: # type: sq_string # elements: 1 # length: 21 Probit transformation # name: # type: sq_string # elements: 1 # length: 7 statget # name: # type: sq_string # elements: 1 # length: 1014 statistics: value = statget (options, name) statistics: value = statget (options, name, default) Read one option out of a statistics options structure. value = statget (options, name) returns the value the option name carries in options, or [] when that option is unset. options is a structure as built by statset, although any structure is accepted. value = statget (options, name, default) returns default instead whenever the option is unset, which is the form a calling function uses to fall back on its own default. Note that default is returned when the option is empty, not only when it is absent, since an empty option is precisely how statset spells "unset". name is matched case-insensitively, and may be abbreviated to any leading portion that singles out one option: statget (options, "MaxI") reads "MaxIter". An abbreviation matching more than one option raises, rather than choosing between them; an exact match is taken as exact even where it is also a prefix of a longer name. See also: statset # name: # type: sq_string # elements: 1 # length: 54 Read one option out of a statistics options structure. # name: # type: string # elements: 1 # length: 7 statset # name: # type: sq_string # elements: 1 # length: 4710 statistics: options = statset () statistics: options = statset (funcname) statistics: options = statset (name, value, ...) statistics: options = statset (oldopts, name, value, ...) statistics: options = statset (oldopts, newopts) statistics: statset () Create or modify an options structure for iterative statistics algorithms. options = statset () returns a structure carrying every recognized option name, each set to an empty value. An empty option means "use the calling function's own default", so an all-empty structure changes nothing wherever it is passed. options = statset (funcname) returns the options that funcname uses by default, with the remaining fields left empty. funcname must name a function of this package that documents an "Options" argument; see the list below. Unlike the name/value forms, this form takes no further arguments. options = statset (name, value, ...) returns an otherwise empty structure with the named options set. Option names are matched case-insensitively and must be given in full. options = statset (oldopts, name, value, ...) copies oldopts and applies the given name/value pairs to the copy. oldopts is left unchanged. options = statset (oldopts, newopts) merges two structures: every non-empty field of newopts overrides its counterpart in oldopts, while an empty field of newopts leaves the oldopts value in place. Fields that are not recognized option names are ignored in both structures. Any number of structures may lead the arguments, each merged over the ones before it in this way, and name/value pairs may follow them. An empty value, such as [] or struct ([]), in place of a structure stands for no structure, so statset ([]) returns the same as statset (). statset () called with no output argument displays the recognized option names together with their permitted values, marking each default in braces. The recognized options are: Option Description "Display" Level of reporting: "off", "final", or "iter". "MaxFunEvals" Maximum number of objective function evaluations, a non-negative integer or Inf. "MaxIter" Maximum number of iterations, a non-negative integer or Inf. A limit of 0 asks for no iteration, so a function honouring it returns its starting point. "TolBnd" Positive scalar tolerance on parameter bounds. "TolFun" Non-negative scalar tolerance on the objective function value. A tolerance of 0 never stops on the change in the objective, leaving "MaxIter" to end the iterations. "TolTypeFun" Whether "TolFun" is absolute, "abs", or relative, "rel". "TolX" Positive scalar tolerance on the parameters. "TolTypeX" Whether "TolX" is absolute, "abs", or relative, "rel". "GradObj" Whether the objective function returns a gradient, "off" or "on". "Jacobian" Whether the model function returns a Jacobian, "off" or "on". "DerivStep" Relative step size for finite-difference derivatives, a positive scalar or vector. "FunValCheck" Whether to check the objective function for invalid values, "off" or "on". "Robust" Whether to invoke a robust fit, "off" or "on". Superseded by "RobustWgtFun". "RobustWgtFun" Weight function for robust fitting: one of "andrews", "bisquare", "cauchy", "fair", "huber", "logistic", "talwar", "welsch", a function handle, or empty for a non-robust fit. "WgtFun" Weight function used with "Robust". Superseded by "RobustWgtFun". "Tune" Positive tuning constant for the robust weight function. Set automatically for a named weight function; required for a function handle. "UseParallel" Logical flag requesting parallel computation. "UseSubstreams" Logical flag requesting reproducible random substreams. "Streams" A random stream or a cell array of them. "OutputFcn" A function handle, or a cell array of them, called after each iteration. funcname may name any of the following functions, each of which documents an "Options" argument: copulafit, coxphfit, crossval, evfit, factoran, fitcox, fitglm, fitglme, fitlme, fitlmematrix, fitnlm, gamfit, gevfit, glmfit, gmdistribution, gpfit, kmeans, kmedoids, lasso, lassoglm, lognfit, mdscale, mlecov, mlecustom, mvncdf, mvtcdf, nbinfit, nlinfit, nnmf, normfit, pca, plsregress, ppca, rocmetrics, sequentialfs, tsne, wblfit, GeneralizedLinearMixedModel, and LinearMixedModel. Any function accepting an "Options" argument also accepts a plain structure carrying only the fields it needs, so statset is a convenience rather than a requirement. MATLAB's statset additionally accepts the names of functions this package does not provide. Those names are rejected here rather than answered, since returning options for an absent function would assert a capability that does not exist. See also: statget, nlinfit, fitnlm, nnmf, mdscale, ppca, tsne, kmedoids # name: # type: sq_string # elements: 1 # length: 74 Create or modify an options structure for iterative statistics algorithms. statistics-release-2.1.2/inst/loadmodel.m000066400000000000000000000714071527311104300204250ustar00rootroot00000000000000## Copyright (C) 2024 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{obj} = } loadmodel (@var{filename}) ## ## Load a Classification or Regression model from a file. ## ## @code{@var{obj} = loadmodel (@var{filename})} loads a Classification or ## Regression object, @var{obj}, from a file defined in @var{filename}. ## ## @seealso{ClassificationDiscriminant, ClassificationGAM, ## ClassificationKNN, ClassificationNeuralNetwork, ## ClassificationPartitionedModel, ClassificationSVM, ClassificationTree, ## RegressionGAM} ## @end deftypefn function obj = loadmodel (filename) ## Check input parameters if (nargin < 1) error ("loadmodel: too few arguments."); endif ## Supported Classification and Regression objects supported = {'ClassificationKNN'}; ## Read file into a structure data = load (filename); ## Check that 'classdef_name' variable exists and that it ## contains a valid Classification or Regression object if (! isfield (data, 'classdef_name')) msg = ' ''%s'' does not contain a Classification or Regression object.'; error (strcat ("loadmodel:", msg), filename); endif ## Remove 'classdef_name' field from data structure classdef_name = data.classdef_name; data = rmfield (data, 'classdef_name'); ## Parse data structure to the static load method of specified classdef switch (classdef_name) case 'ClassificationDiscriminant' obj = ClassificationDiscriminant.load_model (filename, data); case 'CompactClassificationDiscriminant' obj = CompactClassificationDiscriminant.load_model (filename, data); case 'ClassificationGAM' obj = ClassificationGAM.load_model (filename, data); case 'CompactClassificationGAM' obj = CompactClassificationGAM.load_model (filename, data); case 'ClassificationKernel' obj = ClassificationKernel.load_model (filename, data); case 'ClassificationKNN' obj = ClassificationKNN.load_model (filename, data); case 'ClassificationLinear' obj = ClassificationLinear.load_model (filename, data); case 'ClassificationNaiveBayes' obj = ClassificationNaiveBayes.load_model (filename, data); case 'CompactClassificationNaiveBayes' obj = CompactClassificationNaiveBayes.load_model (filename, data); case 'ClassificationNeuralNetwork' obj = ClassificationNeuralNetwork.load_model (filename, data); case 'CompactClassificationNeuralNetwork' obj = CompactClassificationNeuralNetwork.load_model (filename, data); case 'ClassificationSVM' obj = ClassificationSVM.load_model (filename, data); case 'ClassificationTree' obj = ClassificationTree.load_model (filename, data); case 'CompactClassificationTree' obj = CompactClassificationTree.load_model (filename, data); case 'CompactClassificationSVM' obj = CompactClassificationSVM.load_model (filename, data); case 'RegressionGP' obj = RegressionGP.load_model (filename, data); case 'CompactRegressionGP' obj = CompactRegressionGP.load_model (filename, data); case 'RegressionGAM' obj = RegressionGAM.load_model (filename, data); case 'RegressionKernel' obj = RegressionKernel.load_model (filename, data); case 'RegressionLinear' obj = RegressionLinear.load_model (filename, data); case 'CompactRegressionGAM' obj = CompactRegressionGAM.load_model (filename, data); case 'RegressionNeuralNetwork' obj = RegressionNeuralNetwork.load_model (filename, data); case 'RegressionSVM' obj = RegressionSVM.load_model (filename, data); case 'RegressionTree' obj = RegressionTree.load_model (filename, data); case 'CompactRegressionTree' obj = CompactRegressionTree.load_model (filename, data); case 'CompactRegressionSVM' obj = CompactRegressionSVM.load_model (filename, data); case 'CompactRegressionNeuralNetwork' obj = CompactRegressionNeuralNetwork.load_model (filename, data); otherwise error ("loadmodel: '%s' is not supported.", classdef_name); endswitch endfunction ## A saved model must load back as the same class with the same state. Every ## loader but the neural-network one compared the saved fieldnames against ## fieldnames (mdl) for exact equality; a private property such as STname is ## saved but never reported by fieldnames, so the comparison could not match ## and the load always failed. ## Every property of a saved ClassificationKNN comes back as it was. %!test %! load fisheriris %! Yb = strcmp (species, 'setosa'); %! m = fitcknn (meas, species, 'ScoreTransform', 'logit'); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (class (m2), class (m)); %! assert_equal (m2.W, m.W); %! assert_equal (m2.X, m.X); %! assert_equal (m2.Y, m.Y); %! assert_equal (m2.NumObservations, m.NumObservations); %! assert_equal (m2.RowsUsed, m.RowsUsed); %! assert_equal (m2.NumPredictors, m.NumPredictors); %! assert_equal (m2.PredictorNames, m.PredictorNames); %! assert_equal (m2.ResponseName, m.ResponseName); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (m2.Sigma, m.Sigma); %! assert_equal (m2.Mu, m.Mu); %! assert_equal (m2.BreakTies, m.BreakTies); %! assert_equal (m2.NumNeighbors, m.NumNeighbors); %! assert_equal (m2.Distance, m.Distance); %! assert_equal (m2.DistanceWeight, m.DistanceWeight); %! assert_equal (m2.DistParameter, m.DistParameter); %! assert_equal (m2.NSMethod, m.NSMethod); %! assert_equal (m2.IncludeTies, m.IncludeTies); %! assert_equal (m2.BucketSize, m.BucketSize); %! assert_equal (m2.CacheSize, m.CacheSize); %! assert_equal (m2.Cost, m.Cost); %! assert_equal (m2.Prior, m.Prior); %! assert_equal (m2.ScoreTransform, m.ScoreTransform); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## Every property of a saved ClassificationDiscriminant comes back as it was. %!test %! load fisheriris %! Yb = strcmp (species, 'setosa'); %! m = fitcdiscr (meas, species, 'ScoreTransform', 'logit'); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (class (m2), class (m)); %! assert_equal (m2.W, m.W); %! assert_equal (m2.X, m.X); %! assert_equal (m2.Y, m.Y); %! assert_equal (m2.NumObservations, m.NumObservations); %! assert_equal (m2.RowsUsed, m.RowsUsed); %! assert_equal (m2.NumPredictors, m.NumPredictors); %! assert_equal (m2.PredictorNames, m.PredictorNames); %! assert_equal (m2.ResponseName, m.ResponseName); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (m2.Sigma, m.Sigma); %! assert_equal (m2.Mu, m.Mu); %! assert_equal (m2.Coeffs, m.Coeffs); %! assert_equal (m2.Delta, m.Delta); %! assert_equal (m2.DiscrimType, m.DiscrimType); %! assert_equal (m2.Gamma, m.Gamma); %! assert_equal (m2.MinGamma, m.MinGamma); %! assert_equal (m2.LogDetSigma, m.LogDetSigma); %! assert_equal (m2.XCentered, m.XCentered); %! assert_equal (m2.Cost, m.Cost); %! assert_equal (m2.Prior, m.Prior); %! assert_equal (m2.ScoreTransform, m.ScoreTransform); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## Every property of a saved ClassificationSVM comes back as it was. %!test %! load fisheriris %! Yb = strcmp (species, 'setosa'); %! m = fitcsvm (meas(1:100,:), Yb(1:100), 'ScoreTransform', 'logit'); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (class (m2), class (m)); %! assert_equal (m2.X, m.X); %! assert_equal (m2.Y, m.Y); %! assert_equal (m2.NumObservations, m.NumObservations); %! assert_equal (m2.RowsUsed, m.RowsUsed); %! assert_equal (m2.NumPredictors, m.NumPredictors); %! assert_equal (m2.PredictorNames, m.PredictorNames); %! assert_equal (m2.ResponseName, m.ResponseName); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (m2.Sigma, m.Sigma); %! assert_equal (m2.Mu, m.Mu); %! assert_equal (m2.ModelParameters, m.ModelParameters); %! assert_equal (m2.Model, m.Model); %! assert_equal (m2.Alpha, m.Alpha); %! assert_equal (m2.Beta, m.Beta); %! assert_equal (m2.Bias, m.Bias); %! assert_equal (m2.IsSupportVector, m.IsSupportVector); %! assert_equal (m2.SupportVectorLabels, m.SupportVectorLabels); %! assert_equal (m2.SupportVectors, m.SupportVectors); %! assert_equal (m2.Prior, m.Prior); %! assert_equal (m2.Cost, m.Cost); %! assert_equal (m2.W, m.W); %! assert_equal (m2.CategoricalPredictors, m.CategoricalPredictors); %! assert_equal (m2.ExpandedPredictorNames, m.ExpandedPredictorNames); %! assert_equal (m2.ScoreTransform, m.ScoreTransform); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## Every property of a saved ClassificationGAM comes back as it was. %!test %! load fisheriris %! Yb = strcmp (species, 'setosa'); %! m = fitcgam (meas(1:100,:), Yb(1:100)); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (class (m2), class (m)); %! assert_equal (m2.X, m.X); %! assert_equal (m2.Y, m.Y); %! assert_equal (m2.NumObservations, m.NumObservations); %! assert_equal (m2.RowsUsed, m.RowsUsed); %! assert_equal (m2.NumPredictors, m.NumPredictors); %! assert_equal (m2.PredictorNames, m.PredictorNames); %! assert_equal (m2.ResponseName, m.ResponseName); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (m2.Prior, m.Prior); %! assert_equal (m2.Formula, m.Formula); %! assert_equal (m2.Interactions, m.Interactions); %! assert_equal (m2.Knots, m.Knots); %! assert_equal (m2.Order, m.Order); %! assert_equal (m2.DoF, m.DoF); %! assert_equal (m2.LearningRate, m.LearningRate); %! assert_equal (m2.NumIterations, m.NumIterations); %! assert_equal (m2.Intercept, m.Intercept); %! assert_equal (m2.W, m.W); %! assert_equal (m2.CategoricalPredictors, m.CategoricalPredictors); %! assert_equal (m2.ExpandedPredictorNames, m.ExpandedPredictorNames); %! assert_equal (m2.BaseModel, m.BaseModel); %! assert_equal (m2.ModelwInt, m.ModelwInt); %! assert_equal (m2.IntMatrix, m.IntMatrix); %! assert_equal (m2.Cost, m.Cost); %! assert_equal (m2.ScoreTransform, m.ScoreTransform); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## A compact model must come back compact. CompactClassificationSVM was ## dispatched to ClassificationSVM.load_model, which builds the wrong class. %!test %! load fisheriris %! Yb = strcmp (species, 'setosa'); %! c = compact (fitcsvm (meas(1:100,:), Yb(1:100))); %! fn = tempname (); %! unwind_protect %! savemodel (c, fn); %! c2 = loadmodel (fn); %! assert_equal (class (c2), 'CompactClassificationSVM'); %! assert_equal (predict (c2, meas(1:10,:)), predict (c, meas(1:10,:))); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## The score transform's name is private state and must survive. It was not ## saved at all by ClassificationSVM, so it silently reverted to 'none'. %!test %! load fisheriris %! Yb = strcmp (species, 'setosa'); %! m = fitcsvm (meas(1:100,:), Yb(1:100), 'ScoreTransform', 'logit'); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (strfind (evalc ('disp (m2)'), "'logit'") > 0, true); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## ClassificationGAM did not save LearningRate or NumIterations at all. %!test %! load fisheriris %! Yb = strcmp (species, 'setosa'); %! m = fitcgam (meas(1:100,:), Yb(1:100)); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.LearningRate, m.LearningRate); %! assert_equal (m2.NumIterations, m.NumIterations); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # ClassificationDiscriminant: categorical labels save and load %! load fisheriris %! m = fitcdiscr (meas, categorical (species)); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! assert_equal (m2.Y, m.Y); %! assert_equal (class (m2.Coeffs(1,2).Class1), 'categorical'); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # ClassificationDiscriminant: string labels save and load %! load fisheriris %! m = fitcdiscr (meas, string (species)); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! assert_equal (m2.Y, m.Y); %! assert_equal (class (m2.Coeffs(1,2).Class1), 'string'); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # CompactClassificationDiscriminant: categorical labels save and load %! load fisheriris %! m = compact (fitcdiscr (meas, categorical (species))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! assert_equal (class (m2.Coeffs(1,2).Class1), 'categorical'); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # CompactClassificationDiscriminant: string labels save and load %! load fisheriris %! m = compact (fitcdiscr (meas, string (species))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! assert_equal (class (m2.Coeffs(1,2).Class1), 'string'); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # ClassificationGAM: categorical labels save and load %! load fisheriris %! m = fitcgam (meas(51:150,:), categorical (species(51:150))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! assert_equal (m2.Y, m.Y); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # ClassificationGAM: string labels save and load %! load fisheriris %! m = fitcgam (meas(51:150,:), string (species(51:150))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! assert_equal (m2.Y, m.Y); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # CompactClassificationGAM: categorical labels save and load %! load fisheriris %! m = compact (fitcgam (meas(51:150,:), categorical (species(51:150)))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # CompactClassificationGAM: string labels save and load %! load fisheriris %! m = compact (fitcgam (meas(51:150,:), string (species(51:150)))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # ClassificationKernel: categorical labels save and load %! load fisheriris %! m = fitckernel (meas(51:150,:), categorical (species(51:150))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # ClassificationKernel: string labels save and load %! load fisheriris %! m = fitckernel (meas(51:150,:), string (species(51:150))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # ClassificationKNN: categorical labels save and load %! load fisheriris %! m = fitcknn (meas, categorical (species)); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! assert_equal (m2.Y, m.Y); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # ClassificationKNN: string labels save and load %! load fisheriris %! m = fitcknn (meas, string (species)); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! assert_equal (m2.Y, m.Y); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # ClassificationLinear: categorical labels save and load %! load fisheriris %! m = fitclinear (meas(51:150,:), categorical (species(51:150))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # ClassificationLinear: string labels save and load %! load fisheriris %! m = fitclinear (meas(51:150,:), string (species(51:150))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # ClassificationNaiveBayes: categorical labels save and load %! load fisheriris %! m = fitcnb (meas, categorical (species)); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! assert_equal (m2.Y, m.Y); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # ClassificationNaiveBayes: string labels save and load %! load fisheriris %! m = fitcnb (meas, string (species)); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! assert_equal (m2.Y, m.Y); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # CompactClassificationNaiveBayes: categorical labels save and load %! load fisheriris %! m = compact (fitcnb (meas, categorical (species))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # CompactClassificationNaiveBayes: string labels save and load %! load fisheriris %! m = compact (fitcnb (meas, string (species))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # ClassificationNeuralNetwork: categorical labels save and load %! load fisheriris %! m = fitcnet (meas, categorical (species)); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! assert_equal (m2.Y, m.Y); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # ClassificationNeuralNetwork: string labels save and load %! load fisheriris %! m = fitcnet (meas, string (species)); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! assert_equal (m2.Y, m.Y); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # CompactClassificationNeuralNetwork: categorical labels save and load %! load fisheriris %! m = compact (fitcnet (meas, categorical (species))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # CompactClassificationNeuralNetwork: string labels save and load %! load fisheriris %! m = compact (fitcnet (meas, string (species))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # ClassificationSVM: categorical labels save and load %! load fisheriris %! m = fitcsvm (meas(51:150,:), categorical (species(51:150))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! assert_equal (m2.Y, m.Y); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # ClassificationSVM: string labels save and load %! load fisheriris %! m = fitcsvm (meas(51:150,:), string (species(51:150))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! assert_equal (m2.Y, m.Y); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # CompactClassificationSVM: categorical labels save and load %! load fisheriris %! m = compact (fitcsvm (meas(51:150,:), categorical (species(51:150)))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # CompactClassificationSVM: string labels save and load %! load fisheriris %! m = compact (fitcsvm (meas(51:150,:), string (species(51:150)))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # ClassificationTree: categorical labels save and load %! load fisheriris %! m = fitctree (meas, categorical (species)); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! assert_equal (m2.Y, m.Y); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # ClassificationTree: string labels save and load %! load fisheriris %! m = fitctree (meas, string (species)); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! assert_equal (m2.Y, m.Y); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # CompactClassificationTree: categorical labels save and load %! load fisheriris %! m = compact (fitctree (meas, categorical (species))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # CompactClassificationTree: string labels save and load %! load fisheriris %! m = compact (fitctree (meas, string (species))); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (predict (m2, meas(51:55,:)), predict (m, meas(51:55,:))); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a categorical model saved by version 1.9.2 loads %! load fisheriris %! m = fitcknn (meas, categorical (species)); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! d = load (fn); %! c = m.ClassNames; %! d.ClassNames = struct ('cats', {categories(c)}, ... %! 'code', uint16 (double (c)), ... %! 'isMissing', isundefined (c), ... %! 'isOrdinal', isordinal (c), ... %! 'isProtected', isprotected (c)); %! c = m.Y; %! d.Y = struct ('cats', {categories(c)}, 'code', uint16 (double (c)), ... %! 'isMissing', isundefined (c), 'isOrdinal', isordinal (c), ... %! 'isProtected', isprotected (c)); %! save ('-binary', fn, '-struct', 'd'); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (m2.Y, m.Y); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect %!test # a string model saved by version 1.9.2 loads %! load fisheriris %! m = fitcknn (meas, string (species)); %! fn = tempname (); %! unwind_protect %! savemodel (m, fn); %! d = load (fn); %! d.ClassNames = struct ('strs', {cellstr(m.ClassNames)}, ... %! 'isMissing', ismissing (m.ClassNames)); %! d.Y = struct ('strs', {cellstr(m.Y)}, 'isMissing', ismissing (m.Y)); %! save ('-binary', fn, '-struct', 'd'); %! m2 = loadmodel (fn); %! assert_equal (m2.ClassNames, m.ClassNames); %! assert_equal (m2.Y, m.Y); %! unwind_protect_cleanup %! if (exist (fn, 'file')) %! delete (fn); %! endif %! end_unwind_protect ## Test input validation %!error loadmodel () %!error ... %! loadmodel ('fisheriris.mat') %!error ... %! loadmodel ('fail_loadmodel.mdl') %!error ... %! loadmodel ('fail_load_model.mdl') statistics-release-2.1.2/inst/logit.m000066400000000000000000000027071527311104300176000ustar00rootroot00000000000000## Copyright (C) 1995-2017 Kurt Hornik ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} logit (@var{p}) ## ## Compute the logit for each value of @var{p} ## ## The logit is defined as ## @tex ## $$ {\rm logit}(p) = \log\Big({p \over 1-p}\Big) $$ ## @end tex ## @ifnottex ## ## @example ## logit (@var{p}) = log (@var{p} / (1-@var{p})) ## @end example ## ## @end ifnottex ## @seealso{probit, logicdf} ## @end deftypefn function x = logit (p) if (nargin != 1) print_usage (); endif x = logiinv (p, 0, 1); endfunction %!test %! p = [0.01:0.01:0.99]; %! assert_equal (logit (p), log (p ./ (1-p)), 25*eps); %!assert_equal (logit ([-1, 0, 0.5, 1, 2]), [NaN, -Inf, 0, +Inf, NaN]) ## Test input validation %!error logit () %!error logit (1, 2) statistics-release-2.1.2/inst/makima.m000066400000000000000000000412051527311104300177150ustar00rootroot00000000000000## Copyright (C) 2025-2026 Avanish Salunke ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{yi} =} makima (@var{x}, @var{y}, @var{xq}) ## @deftypefnx {statistics} {@var{yi} =} makima (@var{y}, @var{xq}) ## @deftypefnx {statistics} {@var{yi} =} makima (@dots{}, @qcode{'extrap'}) ## ## Compute the 1-D Modified Akima piecewise cubic Hermite interpolant of ## sample data @var{x} and @var{y}. ## ## The Modified Akima (MAKIMA) algorithm generates a shape-preserving ## piecewise cubic interpolant. It differs from standard splines by avoiding ## excessive local undulations and overshoots, and it connects collinear points ## (flat regions) with straight lines. It is particularly well-suited for ## oscillatory data where @code{pchip} might aggressively flatten local ## extrema. ## ## The sample points @var{x} must be a vector of unique values. If @var{x} ## is not sorted, the function will automatically sort it and rearrange ## @var{y} accordingly. ## ## The sample values @var{y} can be a scalar, vector, or an N-dimensional ## array. If @var{y} is an N-dimensional array, the interpolation is ## performed along its last dimension, which must have the same length as ## @var{x}. Complex values for @var{y} are supported, and their real and ## imaginary parts are interpolated independently. Sample points where ## @var{x} or @var{y} is @code{NaN} are dropped in pairs before the fit, ## and a warning is issued. ## ## If query points @var{xq} are provided, the function evaluates the ## interpolant and returns the interpolated values @var{yi}. By default, ## @code{makima} uses the boundary polynomials to extrapolate for points ## outside the range of @var{x}. The optional string argument @qcode{'extrap'} ## is accepted for compatibility with other interpolation functions. ## ## If only @var{x} and @var{y} are provided, the function returns a ## piecewise polynomial structure @var{pp} that represents the interpolant. ## This structure can be evaluated later at specific query points using ## @code{ppval}. ## ## Evaluating the interpolant at query points outside the domain of @var{x} ## automatically extrapolates using the boundary polynomials. ## ## @seealso{interp1, pchip, spline} ## @end deftypefn function yi = makima (x, y, xq, varargin) if (nargin < 2 || nargin > 4) error ("makima: invalid number of inputs"); endif if (nargin == 4 && ! strcmpi (varargin{1}, 'extrap')) error ("makima: unknown option '%s'", varargin{1}); endif return_pp = (nargin == 2); if (! return_pp) size_xq = size (xq); endif x = x(:); n = numel (x); is_y_vector = isvector (y); size_y = size (y); if (is_y_vector) if (numel (y) != n) error ("makima: the number of sample points X, %d, is incompatible with the number of values Y, %d.", n, numel (y)); endif y = y(:); dim_y = 1; nc = 1; else dim_y = ndims (y); if (size_y(dim_y) != n) error ("makima: the number of sample points X, %d, is incompatible with the number of values Y, %d.", n, size_y(dim_y)); endif ## move the interpolation dimension to the front, keeping the others in ## order so that the series columns follow the layout mkpp expects perm_order = [dim_y, 1:dim_y-1]; y = permute (y, perm_order); nc = numel (y) / n; y = reshape (y, n, nc); endif bad = isnan (x) | any (isnan (y), 2); if (any (bad)) warning ("makima: NaN entries in X or Y have been ignored."); x = x(! bad); y = y(! bad, :); n = numel (x); endif if (iscomplex (y)) if (is_y_vector) y_orig = y; else size_perm = size_y(perm_order); size_perm(1) = n; y_orig = ipermute (reshape (y, size_perm), perm_order); endif if (return_pp) ## Build complex pp struct pp_real = makima (x, real (y_orig)); pp_imag = makima (x, imag (y_orig)); yi = pp_real; yi.coefs = pp_real.coefs + 1i * pp_imag.coefs; else yi = makima (x, real (y_orig), xq, varargin{:}) ... + 1i * makima (x, imag (y_orig), xq, varargin{:}); endif return; endif if (! return_pp) xqv = xq(:); nq = numel (xqv); endif if (n < 2) error ("makima: the first two inputs must have at least two elements."); endif if (! issorted (x)) [x, sort_idx] = sort (x); y = y(sort_idx, :); endif math_done = false; if (n == 2) if (return_pp) ## Linear coefficients for 2-point pp struct coefs = zeros (nc, 4, class (y)); coefs(:, 3) = (y(2, :).' - y(1, :).') ./ (x(2) - x(1)); coefs(:, 4) = y(1, :).'; if (is_y_vector) dim_out = 1; else dim_out = size_y; dim_out(dim_y) = []; endif yi = mkpp (x.', coefs, dim_out); return; else yi = interp1 (x, y, xqv, 'linear', 'extrap'); yi = reshape (yi, [nq, nc]); math_done = true; endif endif if (! math_done) dx = diff (x); if (any (dx <= 0)) error ("makima: the sample points x must be unique."); endif dy = diff (y); m = dy ./ dx; m_0 = 2 * m(1, :) - m(2, :); m_m1 = 2 * m_0 - m(1, :); m_n = 2 * m(end, :) - m(end-1, :); m_n1 = 2 * m_n - m(end, :); m_ext = [m_m1; m_0; m; m_n; m_n1]; d = zeros (n, nc, class (y)); k_idx = (1 : n)'; s_im2 = m_ext(k_idx , :); s_im1 = m_ext(k_idx + 1, :); s_i = m_ext(k_idx + 2, :); s_ip1 = m_ext(k_idx + 3, :); w1 = abs (s_ip1 - s_i) + abs (s_ip1 + s_i) / 2; w2 = abs (s_im1 - s_im2) + abs (s_im1 + s_im2) / 2; W = w1 + w2; numer = (w1 .* s_im1 + w2 .* s_i); denom = max (W, eps); d = numer ./ denom; zero_mask = (W == 0); if (any (zero_mask(:))) fallback = (s_im1 + s_i) / 2; d(zero_mask) = fallback(zero_mask); endif if (return_pp) hseg = dx; delta = m; d0 = d(1:end-1, :); d1 = d(2:end, :); y0 = y(1:end-1, :); c3 = (d0 + d1 - 2*delta) ./ (hseg .* hseg); c2 = (3*delta - 2*d0 - d1) ./ hseg; c1 = d0; c0 = y0; c3_t = c3.'; c2_t = c2.'; c1_t = c1.'; c0_t = c0.'; coefs = [c3_t(:), c2_t(:), c1_t(:), c0_t(:)]; if (is_y_vector) dim_out = 1; else dim_out = size_y; dim_out(dim_y) = []; if (isempty (dim_out)) dim_out = 1; endif endif yi = mkpp (x.', coefs, dim_out); return; endif yi = NaN (nq, nc, class (y)); if (nq > 0) idx = lookup (x, xqv); idx(idx >= n) = n - 1; idx(idx == 0) = 1; x_left = x(idx); hseg = dx(idx); s = xqv - x_left; for c = 1:nc y0 = y(idx, c); y1 = y(idx + 1, c); d0 = d(idx, c); d1 = d(idx + 1, c); delta = (y1 - y0) ./ hseg; c2 = (3*delta - 2*d0 - d1) ./ hseg; c3 = (d0 + d1 - 2*delta) ./ (hseg.^2); yi(:, c) = y0 + s .* (d0 + s .* (c2 + s .* c3)); endfor endif endif if (! return_pp) if (is_y_vector) yi = reshape (yi, size_xq); else out_shape = size_y; out_shape(dim_y) = nq; yi = reshape (yi, out_shape(perm_order)); yi = ipermute (yi, perm_order); ## only append size_xq if multi-dimensional array is given if (! isvector (xq)) final_shape = size_y; final_shape(dim_y) = []; final_shape = [final_shape, size_xq]; yi = reshape (yi, final_shape); endif endif endif endfunction %!test %! ## Basic linear-like data %! x = [1; 2; 3; 4]; %! y = [2; 4; 6; 8]; %! xi = [1.5; 2.5; 3.5]; %! yi = makima (x, y, xi); %! assert_equal (yi, [3; 5; 7], 1e-12); %!test %! ## Nonlinear dataset (finite check) %! x = [0; 1; 2; 3; 4]; %! y = [0; 1; 0; 1; 0]; %! xi = linspace (0,4,20)'; %! yi = makima (x, y, xi); %! assert_equal (all (isfinite (yi)), true); %!test %! ## pp structure output %! x = [1; 2; 3; 4]; %! y = [2; 4; 6; 8]; %! pp = makima (x, y); %! assert_equal (isstruct (pp), true); %! assert_equal (strcmp (pp.form, 'pp'), true); %! assert_equal (pp.pieces, 3); %! assert_equal (pp.order, 4); %!test %! ## Matrix y input. %! x = [1; 3; 5]; %! y = [1 3 2; 2 4 6]; %! xi = 2; %! yi = makima (x, y, xi); %! assert_equal (size (yi), [2, 1]); %! assert_equal (all (isfinite (yi)), true); %! assert_equal (yi(1), 2.304086538461538, 1e-12); %! assert_equal (yi(2), 3.000000000000000, 1e-12); %!test %! ## Extrapolation through default method. %! x = [1; 2; 3]; %! y = [5; 10; 15]; %! xi = [0; 4]; %! yi = makima (x, y, xi); %! assert_equal (all (isfinite (yi)), true); %! assert_equal (yi, [0; 20], 1e-12); %!test %! ## Complex interpolation. %! x = [1; 2; 4]; %! y = [1+2i; 2+3i; 4+8i]; %! xi = 3; %! yi = makima (x, y, xi); %! assert_equal (yi, 3 + 5.09767206477733i, 1e-12); %! assert_equal (iscomplex (yi), true); %!test %! ## Two-point interpolation. %! x = [1; 5]; %! y = [10; 30]; %! xi = 3; %! yi = makima (x, y, xi); %! assert_equal (yi, 20, 1e-12); %!test %! ## Single Precision Input. %! x = single ([1; 2; 3]); %! y = single ([10; 20; 30]); %! xi = single (1.5); %! yi = makima (x, y, xi); %! assert_equal (isa (yi, 'single'), true); %! assert_equal (yi, single (15), 1e-6); %!test %! ## Row vector inputs. %! x = [1 2 3]; %! y = [4 5 6]; %! xi = [1.5 2.5]; %! yi = makima (x, y, xi); %! assert_equal (yi, [4.5 5.5], 1e-12); %!test %! ## Step function. %! x = [1 2 3 4 5 6]; %! y = [0 0 1 1 0 0]; %! xi = [2.5 3.5 4.5]; %! yi = makima (x, y, xi); %! expected_11 = [0.5000, 1.1250, 0.5000]; %! assert_equal (yi, expected_11, 1e-12); %!test %! ## Runge function (Oscillation Check) %! x = linspace (-1, 1, 7)'; %! y = 1 ./ (1 + 25 * x.^2); %! xi = [-0.5; 0.1; 0.5]; %! yi = makima (x, y, xi); %! expected_12 = [0.148690385982729; 0.857734549516009; 0.148690385982729]; %! assert_equal (yi, expected_12, 1e-12); %!test %! ## Constant Slopes / Zero Weights %! x = [1; 2; 3; 4; 5]; %! y = [1; 1; 1; 1; 1]; %! xi = 3.5; %! yi = makima (x, y, xi); %! expected_13 = [1]; %! assert_equal (yi, expected_13, 1e-12); %!test %! ## Empty xq input %! x = [1; 2; 3]; %! y = [4; 5; 6]; %! xi = []; %! yi = makima (x, y, xi); %! assert_equal (isempty (yi), true); %! assert_equal (! (iscolumn (yi)), true); %!test %! ## Wide range of y-values %! x = [1e-10; 2e-10; 3e-10; 4e-10]; %! y = [1e10; 2e10; 3e10; 4e10]; %! xi = 2.5e-10; %! yi = makima (x, y, xi); %! assert_equal (yi, 2.5e10, 1e-12); %!test %! ## Single column matrix input. %! x = [1; 2; 3]; %! y = [10; 20; 30]; %! xi = [1.5 2.5]; % Row input %! yi = makima (x, y, xi); %! assert_equal (yi, [15 25], 1e-12); %! assert_equal (isrow (yi), true); %!test %! ## Evaluate pp structure with ppval %! x = [1; 2; 3; 4]; %! y = [2; 4; 6; 8]; %! xi = [1.5; 2.5; 3.5]; %! pp = makima (x, y); %! yi_ppval = ppval (pp, xi); %! yi_direct = makima (x, y, xi); %! assert_equal (yi_ppval, yi_direct, 1e-12); %!test %! ## xq is a 2x2 matrix %! x = [1; 2; 3; 4; 5]; %! y = [10; 20; 15; 5; 25]; %! xq = [1.5, 2.5; 3.5, 4.5]; %! yi = makima (x, y, xq); %! assert_equal (size (yi), [2, 2]); %! expected = [16.85897435897436, 18.22916666666667; 9.81182795698925, 10.84522332506203]; %! assert_equal (yi, expected, 1e-12); %!test %! ## xq is a 3D array %! x = [1; 2; 3; 4; 5]; %! y = [10; 20; 15; 5; 25]; %! xq = ones (2, 2, 2) * 2.5; %! yi = makima (x, y, xq); %! assert_equal (size (yi), [2, 2, 2]); %!test %! ## pp structure with matrix y input %! x = [1; 3; 5]; %! y = [1 3 2; 2 4 6]; %! pp = makima (x, y); %! assert_equal (isstruct (pp), true); %! assert_equal (pp.pieces, 2); %! assert_equal (pp.dim, 2); %! yi_ppval = ppval (pp, 2); %! yi_direct = makima (x, y, 2); %! assert_equal (yi_ppval, yi_direct, 1e-12); %!test %! ## y is a 3D array [2x3x4] and x is length 4 %! x = [1, 2, 3, 4]; %! xq = [1.5, 2.5, 3.5]; %! y3 = reshape (1:24, [2, 3, 4]); %! yi = makima (x, y3, xq); %! assert_equal (size (yi), [2, 3, 3]); %!test %! ## Unsorted 'x' inputs %! x_unsorted = [3; 1; 2; 4]; %! y_unsorted = [9; 1; 4; 16]; %! xq = [1.5; 2.5]; %! x_sorted = [1; 2; 3; 4]; %! y_sorted = [1; 4; 9; 16]; %! assert_equal (makima (x_unsorted, y_unsorted, xq), makima (x_sorted, y_sorted, xq), 1e-12); %!test %! ## Complex piecewise polynomial (pp) structure %! x = [1 2 3]; %! y = [1 4 9] + 1i * [2 8 18]; %! pp = makima (x, y); %! assert_equal (isstruct (pp), true); %! assert_equal (iscomplex (pp.coefs), true); %! assert_equal (ppval (pp, 1.5), makima (x, y, 1.5), 1e-12); %!test %! ## Complex matrix y with vector xq. %! x = [1 2 3]; %! y = [1+2i 3 2; 2 4 6+1i]; %! yi = makima (x, y, [1.5 2.5]); %! expected = [2.304086538461538+0.768750000000000i, ... %! 2.781250000000000-0.187500000000000i; ... %! 3.000000000000000-0.093750000000000i, ... %! 5.000000000000000+0.384375000000000i]; %! assert_equal (yi, expected, 1e-12); %!test %! ## Complex y with as many series as sample points. %! x = [1 2 3]; %! y = [1+2i 3 2+1i; 2 4+3i 6; 5 1i 2]; %! yi = makima (x, y, 1.5); %! expected = [2.304086538461538+0.695913461538462i; ... %! 3.000000000000000+2.062500000000000i; ... %! 1.775705645161290+0.687500000000000i]; %! assert_equal (yi, expected, 1e-12); %!test %! ## Complex 3D y. %! x = 1:4; %! y = reshape ((1:24) + 1i * (24:-1:1), [2 3 4]); %! yi = makima (x, y, [1.5 2.5 3.5]); %! expected = cat (3, [4+21i, 6+19i, 8+17i; 5+20i, 7+18i, 9+16i], ... %! [10+15i, 12+13i, 14+11i; 11+14i, 13+12i, 15+10i], ... %! [16+9i, 18+7i, 20+5i; 17+8i, 19+6i, 21+4i]); %! assert_equal (yi, expected, 1e-12); %!test %! ## Complex matrix y in pp form. %! x = [1 2 3]; %! y = [1+2i 3 2; 2 4 6+1i]; %! pp = makima (x, y); %! expected = [2.304086538461538+0.768750000000000i; ... %! 3.000000000000000-0.093750000000000i]; %! assert_equal (ppval (pp, 1.5), expected, 1e-12); %!test %! ## N-dimensional y in pp form. %! pp = makima (1:4, reshape (1:24, [2 3 4])); %! assert_equal (ppval (pp, 2.5), [10, 12, 14; 11, 13, 15], 1e-12); %!test %! ## Complex N-dimensional y in pp form. %! pp = makima (1:4, reshape ((1:24) + 1i * (24:-1:1), [2 3 4])); %! expected = [10+15i, 12+13i, 14+11i; 11+14i, 13+12i, 15+10i]; %! assert_equal (ppval (pp, 2.5), expected, 1e-12); %!test %! ## N-dimensional y (3D) with N-dimensional xq (2x2 matrix) %! x = [1 2 3 4]; %! y3 = reshape (1:24, [2 3 4]); %! xq = [1.5 2.5; 3.5 1.5]; %! yi = makima (x, y3, xq); %! assert_equal (size (yi), [2 3 2 2]); %!test %! ## 2-point pp struct %! x = [1; 5]; %! y = [10; 30]; %! pp = makima (x, y); %! assert_equal (pp.pieces, 1); %! assert_equal (pp.order, 4); %! assert_equal (ppval (pp, 3), 20, 1e-12); %!test %! ## Exact Collinearity %! x = [1 2 3 4]; %! y = [2 4 6 8]; %! xi = 2.5; %! yi = makima (x, y, xi); %! assert_equal (yi, 5, 1e-12); %!test %! ## Extrapolation check. %! x = [1; 2; 3]; %! y = [5; 10; 15]; %! xi = [0; 4]; %! yi = makima (x, y, xi, 'extrap'); %! assert_equal (all (isfinite (yi)), true); %! assert_equal (yi, [0; 20], 1e-12); %!warning %! ## NaN entry in y is dropped before the fit. %! x = [1; 2; 3; 4; 5]; %! y = [1; NaN; 3; 4; 5]; %! xq = [1.5; 2.5; 3.5; 4.5]; %! yi = makima (x, y, xq); %! assert_equal (yi, [1.5; 2.5; 3.5; 4.5], 1e-12); %!warning ... %! makima ([1; 2; NaN; 4; 5], [1; 2; 3; 4; 5], 1.5); %!warning %! ## A NaN in one series drops that sample point from every series, so a %! ## series with no NaN of its own is refitted without it. Fitted alone, %! ## the first row would give 5.535714286 at both query points. %! y = [0, 0, 10, 0, 0; 1, NaN, 3, 4, 5]; %! yi = makima (1:5, y, [2.5, 3.5]); %! assert_equal (yi(1,:), [10.6399, 4.9887], 1e-4); %! assert_equal (yi(2,:), [2.5, 3.5], 1e-12); ## Test input validation %!error makima ([1 1 2], [3 4 5], 1.5) %!error makima (1) %!error makima (1, 2, 1.5) %!error makima ([1 2 3 4], [1 2 3 4 5], 2) %!error makima ([1 2 3], [1 2 3], 2, 'linear') %!error makima ([1 2 3], [1 2 3], 2, 'extrap', 'too_many') statistics-release-2.1.2/inst/probit.m000066400000000000000000000025761527311104300177650ustar00rootroot00000000000000## Copyright (C) 1995-2017 Kurt Hornik ## Copyright (C) 2023 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{x} =} probit (@var{p}) ## ## Probit transformation ## ## Return the probit (the quantile of the standard normal distribution) for ## each element of @var{p}. ## ## @seealso{logit} ## @end deftypefn function x = probit (p) if (nargin != 1) print_usage (); endif x = -sqrt (2) * erfcinv (2 * p); endfunction ## Test output %!assert_equal (probit ([-1, 0, 0.5, 1, 2]), [NaN, -Inf, 0, Inf, NaN]) %!assert_equal (probit ([0.2, 0.99]), norminv ([0.2, 0.99])) ## Test input validation %!error probit () %!error probit (1, 2) statistics-release-2.1.2/inst/statget.m000066400000000000000000000137121527311104300201330ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{value} =} statget (@var{options}, @var{name}) ## @deftypefnx {statistics} {@var{value} =} statget (@var{options}, @var{name}, @var{default}) ## ## Read one option out of a statistics options structure. ## ## @code{@var{value} = statget (@var{options}, @var{name})} returns the value ## the option @var{name} carries in @var{options}, or @code{[]} when that ## option is unset. @var{options} is a structure as built by @code{statset}, ## although any structure is accepted. ## ## @code{@var{value} = statget (@var{options}, @var{name}, @var{default})} ## returns @var{default} instead whenever the option is unset, which is the ## form a calling function uses to fall back on its own default. Note that ## @var{default} is returned when the option is @emph{empty}, not only when it ## is absent, since an empty option is precisely how @code{statset} spells ## @qcode{"unset"}. ## ## @var{name} is matched case-insensitively, and may be abbreviated to any ## leading portion that singles out one option: @code{statget (@var{options}, ## "MaxI")} reads @qcode{"MaxIter"}. An abbreviation matching more than one ## option raises, rather than choosing between them; an exact match is taken ## as exact even where it is also a prefix of a longer name. ## ## @seealso{statset} ## @end deftypefn function value = statget (options, name, default) if (nargin < 2) print_usage (); endif if (nargin < 3) default = []; endif if (! (isstruct (options) && isscalar (options))) error (strcat ("statget: OPTIONS must be a scalar structure, as", ... " built by statset.")); endif if (! (ischar (name) || (isa (name, 'string') && isscalar (name)))) error ("statget: NAME must be a character vector or a string scalar."); endif name = char (name); fn = fieldnames (options); ## An exact match wins outright, so that a name which is also the prefix of ## a longer one is never treated as ambiguous. idx = find (strcmpi (name, fn)); if (isempty (idx)) idx = find (strncmpi (name, fn, numel (name))); if (numel (idx) > 1) error (strcat ("statget: '%s' matches more than one option name:", ... " %s."), name, strjoin (fn(idx)', ", ")); endif endif if (isempty (idx)) error ("statget: unrecognized option name '%s'.", name); endif value = options.(fn{idx}); if (isempty (value)) value = default; endif endfunction %!demo %! ## Read an option, falling back on a default when it is unset %! options = statset ('nlinfit'); %! maxiter = statget (options, 'MaxIter') %! tolbnd = statget (options, 'TolBnd', 1e-6) ## Reading a set option %!test %! assert_equal (statget (statset ('factoran'), 'TolX'), 1e-8); %!test %! assert_equal (statget (statset ('nlinfit'), 'MaxIter'), 200); %!test %! assert_equal (statget (statset ('nlinfit'), 'WgtFun'), 'bisquare'); ## An unset option reads as empty %!test %! assert_equal (statget (statset ('factoran'), 'TolBnd'), []); %!test %! assert_equal (statget (statset (), 'MaxIter'), []); ## The default is returned only when the option is unset %!test %! assert_equal (statget (statset ('factoran'), 'TolBnd', 42), 42); %!test %! assert_equal (statget (statset ('factoran'), 'TolX', 42), 1e-8); ## The default may be of any type %!test %! assert_equal (statget (statset (), 'Display', 'final'), 'final'); %!test %! assert_equal (statget (statset (), 'OutputFcn', {@sin}), {@sin}); ## The name is matched case-insensitively %!test %! assert_equal (statget (statset ('factoran'), 'tolx'), 1e-8); %!test %! assert_equal (statget (statset ('factoran'), 'MAXITER'), 100); ## The name may be abbreviated when the abbreviation is unique %!test %! assert_equal (statget (statset ('factoran'), 'MaxI'), 100); %!test %! assert_equal (statget (statset ('factoran'), 'Displ'), 'off'); %!test %! assert_equal (statget (statset ('nlinfit'), 'Deriv'), 6.0554544523933429e-06, 1e-20); ## An exact match is preferred over the longer names it prefixes %!test %! assert_equal (statget (statset ('nnmf'), 'TolX'), 1e-4); %!test %! assert_equal (statget (statset ('fitnlm'), 'Robust'), 'off'); ## Any structure is accepted, not only one built by statset %!test %! assert_equal (statget (struct ('MaxIter', 7), 'MaxIter'), 7); %!test %! assert_equal (statget (struct ('MaxIter', 7), 'MaxI'), 7); ## Error conditions %!error statget () %!error statget (statset ()) %!error ... %! statget (1, 'MaxIter') %!error ... %! statget (struct ('MaxIter', {1, 2}), 'MaxIter') %!error ... %! statget (statset (), 5) %!error ... %! statget (statset (), 'Tol') %!error ... %! statget (statset (), 'TolT') %!error ... %! statget (statset (), 'NoSuchOption') %!error ... %! statget (struct ('MaxIter', 7), 'TolX') statistics-release-2.1.2/inst/statset.m000066400000000000000000000751231527311104300201530ustar00rootroot00000000000000## Copyright (C) 2026 Andreas Bertsatos ## ## This file is part of the statistics package for GNU Octave. ## ## This program is free software; you can redistribute it and/or modify it under ## the terms of the GNU General Public License as published by the Free Software ## Foundation; either version 3 of the License, or (at your option) any later ## version. ## ## This program is distributed in the hope that it will be useful, but WITHOUT ## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ## more details. ## ## You should have received a copy of the GNU General Public License along with ## this program; if not, see . ## -*- texinfo -*- ## @deftypefn {statistics} {@var{options} =} statset () ## @deftypefnx {statistics} {@var{options} =} statset (@var{funcname}) ## @deftypefnx {statistics} {@var{options} =} statset (@var{name}, @var{value}, @dots{}) ## @deftypefnx {statistics} {@var{options} =} statset (@var{oldopts}, @var{name}, @var{value}, @dots{}) ## @deftypefnx {statistics} {@var{options} =} statset (@var{oldopts}, @var{newopts}) ## @deftypefnx {statistics} {} statset () ## ## Create or modify an options structure for iterative statistics algorithms. ## ## @code{@var{options} = statset ()} returns a structure carrying every ## recognized option name, each set to an empty value. An empty option means ## @qcode{"use the calling function's own default"}, so an all-empty structure ## changes nothing wherever it is passed. ## ## @code{@var{options} = statset (@var{funcname})} returns the options that ## @var{funcname} uses by default, with the remaining fields left empty. ## @var{funcname} must name a function of this package that documents an ## @qcode{"Options"} argument; see the list below. Unlike the name/value ## forms, this form takes no further arguments. ## ## @code{@var{options} = statset (@var{name}, @var{value}, @dots{})} returns an ## otherwise empty structure with the named options set. Option names are ## matched case-insensitively and must be given in full. ## ## @code{@var{options} = statset (@var{oldopts}, @var{name}, @var{value}, ## @dots{})} copies @var{oldopts} and applies the given name/value pairs to the ## copy. @var{oldopts} is left unchanged. ## ## @code{@var{options} = statset (@var{oldopts}, @var{newopts})} merges two ## structures: every @emph{non-empty} field of @var{newopts} overrides its ## counterpart in @var{oldopts}, while an empty field of @var{newopts} leaves ## the @var{oldopts} value in place. Fields that are not recognized option ## names are ignored in both structures. ## ## Any number of structures may lead the arguments, each merged over the ones ## before it in this way, and name/value pairs may follow them. An empty ## value, such as @code{[]} or @code{struct ([])}, in place of a structure ## stands for no structure, so @code{statset ([])} returns the same as ## @code{statset ()}. ## ## @code{statset ()} called with no output argument displays the recognized ## option names together with their permitted values, marking each default ## in braces. ## ## The recognized options are: ## ## @multitable @columnfractions 0.25 0.75 ## @headitem Option @tab Description ## @item @qcode{"Display"} @tab Level of reporting: @qcode{"off"}, ## @qcode{"final"}, or @qcode{"iter"}. ## @item @qcode{"MaxFunEvals"} @tab Maximum number of objective function ## evaluations, a non-negative integer or @code{Inf}. ## @item @qcode{"MaxIter"} @tab Maximum number of iterations, a non-negative ## integer or @code{Inf}. A limit of 0 asks for no iteration, so a function ## honouring it returns its starting point. ## @item @qcode{"TolBnd"} @tab Positive scalar tolerance on parameter bounds. ## @item @qcode{"TolFun"} @tab Non-negative scalar tolerance on the objective ## function value. A tolerance of 0 never stops on the change in the ## objective, leaving @qcode{"MaxIter"} to end the iterations. ## @item @qcode{"TolTypeFun"} @tab Whether @qcode{"TolFun"} is absolute, ## @qcode{"abs"}, or relative, @qcode{"rel"}. ## @item @qcode{"TolX"} @tab Positive scalar tolerance on the parameters. ## @item @qcode{"TolTypeX"} @tab Whether @qcode{"TolX"} is absolute, ## @qcode{"abs"}, or relative, @qcode{"rel"}. ## @item @qcode{"GradObj"} @tab Whether the objective function returns a ## gradient, @qcode{"off"} or @qcode{"on"}. ## @item @qcode{"Jacobian"} @tab Whether the model function returns a Jacobian, ## @qcode{"off"} or @qcode{"on"}. ## @item @qcode{"DerivStep"} @tab Relative step size for finite-difference ## derivatives, a positive scalar or vector. ## @item @qcode{"FunValCheck"} @tab Whether to check the objective function for ## invalid values, @qcode{"off"} or @qcode{"on"}. ## @item @qcode{"Robust"} @tab Whether to invoke a robust fit, @qcode{"off"} or ## @qcode{"on"}. Superseded by @qcode{"RobustWgtFun"}. ## @item @qcode{"RobustWgtFun"} @tab Weight function for robust fitting: one of ## @qcode{"andrews"}, @qcode{"bisquare"}, @qcode{"cauchy"}, @qcode{"fair"}, ## @qcode{"huber"}, @qcode{"logistic"}, @qcode{"talwar"}, @qcode{"welsch"}, a ## function handle, or empty for a non-robust fit. ## @item @qcode{"WgtFun"} @tab Weight function used with @qcode{"Robust"}. ## Superseded by @qcode{"RobustWgtFun"}. ## @item @qcode{"Tune"} @tab Positive tuning constant for the robust weight ## function. Set automatically for a named weight function; required for a ## function handle. ## @item @qcode{"UseParallel"} @tab Logical flag requesting parallel ## computation. ## @item @qcode{"UseSubstreams"} @tab Logical flag requesting reproducible ## random substreams. ## @item @qcode{"Streams"} @tab A random stream or a cell array of them. ## @item @qcode{"OutputFcn"} @tab A function handle, or a cell array of them, ## called after each iteration. ## @end multitable ## ## @var{funcname} may name any of the following functions, each of which ## documents an @qcode{"Options"} argument: @code{copulafit}, @code{coxphfit}, ## @code{crossval}, @code{evfit}, @code{factoran}, @code{fitcox}, ## @code{fitglm}, @code{fitglme}, @code{fitlme}, ## @code{fitlmematrix}, @code{fitnlm}, @code{gamfit}, @code{gevfit}, ## @code{glmfit}, @code{gmdistribution}, @code{gpfit}, @code{kmeans}, ## @code{kmedoids}, @code{lasso}, @code{lassoglm}, @code{lognfit}, ## @code{mdscale}, @code{mlecov}, @code{mlecustom}, @code{mvncdf}, ## @code{mvtcdf}, @code{nbinfit}, @code{nlinfit}, @code{nnmf}, @code{normfit}, ## @code{pca}, @code{plsregress}, @code{ppca}, @code{rocmetrics}, ## @code{sequentialfs}, @code{tsne}, @code{wblfit}, ## @code{GeneralizedLinearMixedModel}, and ## @code{LinearMixedModel}. ## ## Any function accepting an @qcode{"Options"} argument also accepts a plain ## structure carrying only the fields it needs, so @code{statset} is a ## convenience rather than a requirement. ## ## MATLAB's @code{statset} additionally accepts the names of functions this ## package does not provide. Those names are rejected here rather than ## answered, since returning options for an absent function would assert a ## capability that does not exist. ## ## @seealso{statget, nlinfit, fitnlm, nnmf, mdscale, ppca, tsne, kmedoids} ## @end deftypefn function options = statset (varargin) ## The recognized option names, in MATLAB's own field order. 'Robust' and ## 'WgtFun' are present in MATLAB's structure but absent from its published ## table; they are kept because nlinfit and fitnlm consume them. names = {'Display', 'MaxFunEvals', 'MaxIter', 'TolBnd', 'TolFun', ... 'TolTypeFun', 'TolX', 'TolTypeX', 'GradObj', 'Jacobian', ... 'DerivStep', 'FunValCheck', 'Robust', 'RobustWgtFun', 'WgtFun', ... 'Tune', 'UseParallel', 'UseSubstreams', 'Streams', 'OutputFcn'}; ## No output and no input: report the option names and their values. if (nargin == 0 && nargout == 0) display_options (); return; endif ## Start from an all-empty structure; 'Streams' is an empty cell. options = cell2struct (repmat ({[]}, numel (names), 1), names, 1); options.Streams = {}; if (nargin == 0) return; endif ## A single character or string argument names a function. if (nargin == 1 && (ischar (varargin{1}) || isstring_scalar (varargin{1}))) options = func_defaults (options, char (varargin{1})); return; endif ## Leading structures supply the starting values, each overriding the ones ## before it where it is not empty. An empty value stands for none. args = varargin; if (! is_options (args{1}) && ! ischar (args{1}) ... && ! isstring_scalar (args{1})) error (strcat ("statset: first argument must be a function name,", ... " an option name, or an options structure.")); endif while (numel (args) > 0 && is_options (args{1})) if (! isempty (args{1})) options = merge_struct (options, args{1}, names); endif args(1) = []; endwhile if (mod (numel (args), 2) != 0) error ("statset: arguments must occur in NAME/VALUE pairs."); endif ## Apply the name/value pairs. for i = 1:2:numel (args) name = args{i}; if (! ischar (name) && ! isstring_scalar (name)) error ("statset: option name must be a character vector or a string."); endif idx = find (strcmpi (char (name), names)); if (isempty (idx)) error ("statset: unrecognized option name '%s'.", char (name)); endif field = names{idx}; options.(field) = check_value (field, args{i+1}); endfor ## A named weight function carries a default tuning constant, but only into ## an empty 'Tune'. A tuning constant already in the structure is never ## re-derived, not even by a call that changes the weight function itself: ## MATLAB keeps a Tune of 3 across statset (options, 'RobustWgtFun', 'huber'). if (isempty (options.Tune) && ! isempty (options.RobustWgtFun) ... && ischar (options.RobustWgtFun)) options.Tune = default_tune (options.RobustWgtFun); endif endfunction ## True for an options structure, or an empty value standing for none. function tf = is_options (arg) tf = isstruct (arg) || (isempty (arg) && ! ischar (arg)); endfunction ## Copy the recognized, non-empty fields of S over those of OPTIONS. function options = merge_struct (options, s, names) if (! isscalar (s)) error ("statset: an options structure must be a scalar structure."); endif fn = fieldnames (s); for i = 1:numel (fn) idx = find (strcmpi (fn{i}, names)); if (isempty (idx)) continue; # unrecognized fields are ignored endif value = s.(fn{i}); if (! isempty (value)) options.(names{idx}) = value; endif endfor endfunction ## Validate VALUE for the option FIELD and return it in canonical form. function value = check_value (field, value) ## An empty value always means "unset", whatever the option. if (isempty (value) && ! iscell (value)) value = []; return; endif switch (field) case 'Display' value = check_string (field, value, {'off', 'final', 'iter'}); case {'GradObj', 'Jacobian', 'FunValCheck', 'Robust'} value = check_string (field, value, {'off', 'on'}); case {'TolTypeFun', 'TolTypeX'} value = check_string (field, value, {'abs', 'rel'}); case {'MaxFunEvals', 'MaxIter'} if (! (isnumeric (value) && isscalar (value) && isreal (value) && isfloat (value) && value >= 0 && value == fix (value))) error (strcat ("statset: option '%s' must be a non-negative", ... " integer or Inf."), field); endif case 'TolFun' if (! (isnumeric (value) && isscalar (value) && isreal (value) && isfloat (value) && value >= 0)) error (strcat ("statset: option 'TolFun' must be a real", ... " non-negative scalar.")); endif case {'TolBnd', 'TolX', 'Tune'} if (! (isnumeric (value) && isscalar (value) && isreal (value) && isfloat (value) && value > 0)) error (strcat ("statset: option '%s' must be a real positive", ... " scalar."), field); endif case 'DerivStep' if (! (isnumeric (value) && isreal (value) && isfloat (value) && all (value(:) > 0))) error (strcat ("statset: option 'DerivStep' must be a real", ... " positive scalar or vector.")); endif case {'RobustWgtFun', 'WgtFun'} if (is_function_handle (value)) return; endif value = check_string (field, value, {'andrews', 'bisquare', 'cauchy', ... 'fair', 'huber', 'logistic', ... 'talwar', 'welsch'}); case {'UseParallel', 'UseSubstreams'} if (! (isscalar (value) && (islogical (value) || (isnumeric (value) && (value == 0 || value == 1))))) error (strcat ("statset: option '%s' must be a logical", ... " scalar."), field); endif value = logical (value); case 'OutputFcn' if (! (is_function_handle (value) || (iscell (value) && all (cellfun (@is_function_handle, value))))) error (strcat ("statset: option 'OutputFcn' must be a function", ... " handle or a cell array of function handles.")); endif case 'Streams' if (! (isa (value, 'RandStream') || iscell (value))) error (strcat ("statset: option 'Streams' must be a random stream", ... " or a cell array of random streams.")); endif endswitch endfunction ## Validate a character option against the list of values it accepts. function value = check_string (field, value, valid) if (! (ischar (value) || isstring_scalar (value))) error (strcat ("statset: option '%s' must be a character vector or", ... " a string scalar."), field); endif value = char (value); idx = find (strcmpi (value, valid)); if (isempty (idx)) quoted = strcat ("'", strjoin (valid, "', '"), "'"); error ("statset: option '%s' must be one of %s.", field, quoted); endif value = valid{idx}; endfunction ## The default tuning constant of each named robust weight function. This ## repeats the table in inst/Regression/private/robusttune.m, which robustfit ## and nlinfit share: a private resolves only for functions in its own ## directory, so statset, sitting in inst/, cannot reach that one. The two ## differ at the tail -- an unknown name is [] here and 1 there -- so keep both ## in step when a weight function is added. function t = default_tune (wgtfun) switch (lower (wgtfun)) case 'andrews'; t = 1.339; case 'bisquare'; t = 4.685; case 'cauchy'; t = 2.385; case 'fair'; t = 1.400; case 'huber'; t = 1.345; case 'logistic'; t = 1.205; case 'talwar'; t = 2.795; case 'welsch'; t = 2.985; otherwise; t = []; endswitch endfunction ## The options each supported function uses by default. function options = func_defaults (options, fname) ## The finite-difference step is eps^(1/3) throughout, and mlecov's is 2^-13. dstep = eps ^ (1/3); switch (lower (fname)) case {'crossval', 'lasso', 'lassoglm', 'plsregress', 'rocmetrics'} options.Display = 'off'; options.UseParallel = false; options.UseSubstreams = false; case {'kmeans', 'kmedoids'} options.Display = 'off'; options.MaxIter = 100; options.UseParallel = false; options.UseSubstreams = false; case 'nnmf' options.Display = 'off'; options.MaxIter = 100; options.TolFun = 1e-4; options.TolX = 1e-4; options.UseParallel = false; options.UseSubstreams = false; case {'fitglme', 'fitlme', 'fitlmematrix', ... 'generalizedlinearmixedmodel', 'linearmixedmodel'} options.Display = 'off'; options.MaxIter = 10000; options.TolFun = 1e-6; options.TolX = 1e-12; case {'fitglm', 'glmfit'} options.Display = 'off'; options.MaxIter = 100; options.TolX = 1e-6; case {'fitnlm', 'nlinfit'} options.Display = 'off'; options.MaxIter = 200; options.TolFun = 1e-8; options.TolX = 1e-8; options.DerivStep = dstep; options.FunValCheck = 'on'; options.Robust = 'off'; options.WgtFun = 'bisquare'; case {'gamfit', 'lognfit', 'normfit'} options.Display = 'off'; options.MaxFunEvals = 200; options.MaxIter = 100; options.TolBnd = 1e-6; options.TolFun = 1e-8; options.TolX = 1e-8; case {'gevfit', 'gpfit', 'nbinfit'} options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolBnd = 1e-6; options.TolFun = 1e-6; options.TolX = 1e-6; case {'evfit', 'wblfit'} options.Display = 'off'; options.TolX = 1e-6; case {'mvncdf', 'mvtcdf'} options.Display = 'off'; options.MaxFunEvals = 1e7; case {'pca', 'ppca'} options.Display = 'off'; options.MaxIter = 1000; options.TolFun = 1e-6; options.TolX = 1e-6; case {'coxphfit', 'fitcox'} options.Display = 'off'; options.MaxFunEvals = 200; options.MaxIter = 100; options.TolFun = 1e-8; options.TolX = 1e-8; case 'copulafit' options.Display = 'off'; options.MaxFunEvals = 200; options.MaxIter = 100; options.TolBnd = 1e-6; options.TolX = 1e-6; case 'factoran' options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 100; options.TolFun = 1e-8; options.TolX = 1e-8; case 'gmdistribution' options.Display = 'off'; options.MaxIter = 100; options.TolFun = 1e-6; case 'mdscale' options.Display = 'off'; options.MaxIter = 200; options.TolFun = 1e-6; options.TolX = 1e-6; case 'mlecov' options.Display = 'off'; options.GradObj = 'off'; options.DerivStep = 2 ^ -13; case 'mlecustom' options.Display = 'off'; options.MaxFunEvals = 400; options.MaxIter = 200; options.TolBnd = 1e-6; options.TolFun = 1e-6; options.TolX = 1e-6; options.GradObj = 'off'; options.DerivStep = dstep; options.FunValCheck = 'on'; case 'sequentialfs' options.Display = 'off'; options.MaxIter = Inf; options.TolTypeFun = 'rel'; options.UseParallel = false; options.UseSubstreams = false; case 'tsne' options.Display = 'off'; options.MaxIter = 1000; options.TolFun = 1e-10; options.OutputFcn = ''; otherwise error ("statset: no default options available for the function '%s'.", ... fname); endswitch endfunction ## Print the option names and the values each accepts. function display_options () tbl = {'Display', '[ {off} | final | iter ]'; ... 'MaxFunEvals', '[ non-negative integer ]'; ... 'MaxIter', '[ non-negative integer ]'; ... 'TolBnd', '[ positive scalar ]'; ... 'TolFun', '[ non-negative scalar ]'; ... 'TolTypeFun', '[ abs | rel ]'; ... 'TolX', '[ positive scalar ]'; ... 'TolTypeX', '[ abs | rel ]'; ... 'GradObj', '[ {off} | on ]'; ... 'Jacobian', '[ {off} | on ]'; ... 'DerivStep', '[ positive scalar or vector ]'; ... 'FunValCheck', '[ off | {on} ]'; ... 'Robust', '[ {off} | on ]'; ... 'RobustWgtFun', ... '[ {[]} | andrews | bisquare | cauchy | fair | huber | logistic | talwar | welsch | function handle ]'; ... 'WgtFun', '[ {[]} | andrews | bisquare | cauchy | fair | huber | logistic | talwar | welsch | function handle ]'; ... 'Tune', '[ positive scalar ]'; ... 'UseParallel', '[ {false} | true ]'; ... 'UseSubstreams', '[ {false} | true ]'; ... 'Streams', '[ {} | RandStream or cell array ]'; ... 'OutputFcn', '[ {[]} | function handle or cell array ]'}; for i = 1:size (tbl, 1) printf ("%20s: %s\n", tbl{i,1}, tbl{i,2}); endfor endfunction ## Return true for a string scalar. isa returns false when the class is ## absent, so this does not require the datatypes package to be loaded. function tf = isstring_scalar (x) tf = isa (x, 'string') && isscalar (x); endfunction %!demo %! ## The default options of a given function %! options = statset ('nlinfit') %!demo %! ## Raise the iteration limit of an existing options structure %! options = statset ('nlinfit'); %! options = statset (options, 'MaxIter', 500); %! [options.MaxIter, options.TolFun] ## Structure shape %!test %! options = statset (); %! assert_equal (isstruct (options), true); %! assert_equal (numel (fieldnames (options)), 20); %!test %! assert_equal (fieldnames (statset ())', {'Display', 'MaxFunEvals', ... %! 'MaxIter', 'TolBnd', 'TolFun', 'TolTypeFun', 'TolX', 'TolTypeX', ... %! 'GradObj', 'Jacobian', 'DerivStep', 'FunValCheck', 'Robust', ... %! 'RobustWgtFun', 'WgtFun', 'Tune', 'UseParallel', 'UseSubstreams', ... %! 'Streams', 'OutputFcn'}); %!test %! options = statset (); %! assert_equal (options.MaxIter, []); %! assert_equal (options.Streams, {}); ## Per-function defaults, measured against MATLAB R2024a %!test %! options = statset ('nlinfit'); %! assert_equal (options.Display, 'off'); %! assert_equal (options.MaxIter, 200); %! assert_equal (options.TolFun, 1e-8); %! assert_equal (options.TolX, 1e-8); %! assert_equal (options.DerivStep, 6.0554544523933429e-06, 1e-20); %! assert_equal (options.FunValCheck, 'on'); %! assert_equal (options.Robust, 'off'); %! assert_equal (options.WgtFun, 'bisquare'); %!test %! assert_equal (statset ('fitnlm'), statset ('nlinfit')); %!test %! options = statset ('factoran'); %! assert_equal (options.MaxFunEvals, 400); %! assert_equal (options.MaxIter, 100); %! assert_equal (options.TolFun, 1e-8); %! assert_equal (options.TolX, 1e-8); %! assert_equal (options.TolBnd, []); %!test %! options = statset ('fitlme'); %! assert_equal (options.MaxIter, 10000); %! assert_equal (options.TolFun, 1e-6); %! assert_equal (options.TolX, 1e-12); %!test %! options = statset ('tsne'); %! assert_equal (options.MaxIter, 1000); %! assert_equal (options.TolFun, 1e-10); %! assert_equal (options.OutputFcn, ''); %!test %! assert_equal (statset ('MaxIter', 0).MaxIter, 0); %!test %! assert_equal (statset ('MaxFunEvals', 0).MaxFunEvals, 0); %!test %! assert_equal (statset ('TolFun', 0).TolFun, 0); %!test %! assert_equal (statset ('TolFun', Inf).TolFun, Inf); %!test %! options = statset ('sequentialfs'); %! assert_equal (options.Display, 'off'); %! assert_equal (options.MaxIter, Inf); %! assert_equal (options.TolFun, []); %! assert_equal (options.TolTypeFun, 'rel'); %!test %! options = statset ('nnmf'); %! assert_equal (options.TolFun, 1e-4); %! assert_equal (options.UseParallel, false); %!test %! options = statset ('mlecov'); %! assert_equal (options.GradObj, 'off'); %! assert_equal (options.DerivStep, 0.0001220703125); %!test %! options = statset ('coxphfit'); %! assert_equal (options.MaxFunEvals, 200); %! assert_equal (options.MaxIter, 100); %! assert_equal (options.TolFun, 1e-8); %! assert_equal (options.TolX, 1e-8); ## fitcox takes the same defaults as coxphfit, as it does in MATLAB %!test %! assert_equal (statset ('fitcox'), statset ('coxphfit')); %!test %! options = statset ('mvncdf'); %! assert_equal (options.MaxFunEvals, 1e7); %!test %! options = statset ('kmedoids'); %! assert_equal (options.MaxIter, 100); %! assert_equal (options.UseSubstreams, false); ## A function name is matched case-insensitively %!test %! assert_equal (statset ('NLINFIT'), statset ('nlinfit')); %!test %! assert_equal (statset ('LinearMixedModel'), statset ('fitlme')); ## Name/value pairs %!test %! options = statset ('MaxIter', 100); %! assert_equal (options.MaxIter, 100); %! assert_equal (options.TolX, []); %!test %! options = statset ('maxiter', 50); %! assert_equal (options.MaxIter, 50); %!test %! options = statset ('MaxIter', 10, 'TolX', 1e-3); %! assert_equal (options.MaxIter, 10); %! assert_equal (options.TolX, 1e-3); ## An empty value leaves the option unset %!test %! assert_equal (statset ('MaxIter', []), statset ()); %!test %! assert_equal (statset ('MaxIter', Inf).MaxIter, Inf); ## Modifying an existing structure leaves the original alone %!test %! old = statset ('nlinfit'); %! new = statset (old, 'MaxIter', 999); %! assert_equal (new.MaxIter, 999); %! assert_equal (old.MaxIter, 200); %! assert_equal (new.TolFun, 1e-8); ## Merging two structures: non-empty fields of the second win %!test %! old = statset ('nlinfit'); %! new = statset (old, statset ('MaxIter', 777)); %! assert_equal (new.MaxIter, 777); %! assert_equal (new.TolFun, 1e-8); ## An all-empty second structure overrides nothing %!test %! old = statset ('nlinfit'); %! assert_equal (statset (old, statset ()), old); ## An empty value in place of a structure stands for none %!test %! assert_equal (statset ([]), statset ()); %!test %! assert_equal (statset ({}), statset ()); %!test %! assert_equal (statset (zeros (1, 0)), statset ()); %!test %! assert_equal (statset (struct ([])), statset ()); %!test %! assert_equal (statset ([], 'Display', 'iter').Display, 'iter'); %!test %! old = statset ('nlinfit'); %! assert_equal (statset ([], old), old); %!test %! old = statset ('nlinfit'); %! assert_equal (statset (old, []), old); %!test %! options = statset (statset ('nlinfit'), [], 'MaxIter', 5); %! assert_equal (options.MaxIter, 5); %! assert_equal (options.TolFun, 1e-8); %!test %! assert_equal (statset ([], [], [], 'MaxIter', 5).MaxIter, 5); %!test %! options = statset (struct ('MaxIter', {}), 'TolX', 1); %! assert_equal (options.MaxIter, []); %! assert_equal (options.TolX, 1); ## Any number of structures may lead, each merged over the ones before it %!test %! options = statset (statset ('nlinfit'), statset ('MaxIter', 7), ... %! statset ('TolX', 1)); %! assert_equal (options.MaxIter, 7); %! assert_equal (options.TolX, 1); %! assert_equal (options.TolFun, 1e-8); ## Unrecognized fields of a supplied structure are ignored %!test %! options = statset (struct ('NotAnOption', 1), 'MaxIter', 5); %! assert_equal (options.MaxIter, 5); %! assert_equal (numel (fieldnames (options)), 20); %!test %! assert_equal (statset (struct ('NotAnOption', 1)), statset ()); ## A named weight function brings its own tuning constant %!test %! options = statset ('RobustWgtFun', 'bisquare'); %! assert_equal (options.RobustWgtFun, 'bisquare'); %! assert_equal (options.Tune, 4.685); %!test %! assert_equal (statset ('RobustWgtFun', 'huber').Tune, 1.345); ## An explicit tuning constant is not overwritten %!test %! options = statset ('RobustWgtFun', 'bisquare', 'Tune', 3); %! assert_equal (options.Tune, 3); ## nor re-derived later, not even by a call that changes the weight function %!test %! options = statset ('RobustWgtFun', 'bisquare', 'Tune', 3); %! assert_equal (statset (options, 'MaxIter', 50).Tune, 3); %! assert_equal (statset (options, struct ('MaxIter', 50)).Tune, 3); %! assert_equal (statset (options, 'RobustWgtFun', 'huber').Tune, 3); %! assert_equal (statset (options, struct ('RobustWgtFun', 'huber')).Tune, 3); %! assert_equal (statset (options, 'RobustWgtFun', 'huber', 'Tune', 9).Tune, 9); %!test %! options = statset ('RobustWgtFun', 'andrews'); %! assert_equal (statset (options, 'Display', 'off').Tune, 1.339); ## A weight function given in a structure brings its tuning constant too %!test %! assert_equal (statset (struct ('RobustWgtFun', 'andrews')).Tune, 1.339); ## A function handle carries no default tuning constant %!test %! options = statset ('RobustWgtFun', @(r) 1 ./ (1 + r .^ 2)); %! assert_equal (is_function_handle (options.RobustWgtFun), true); %! assert_equal (options.Tune, []); ## Enumerated options are matched case-insensitively and stored canonically %!test %! assert_equal (statset ('Display', 'ITER').Display, 'iter'); %!test %! assert_equal (statset ('TolTypeFun', 'REL').TolTypeFun, 'rel'); ## A logical option is stored as a logical %!test %! options = statset ('UseParallel', 1); %! assert_equal (islogical (options.UseParallel), true); %! assert_equal (options.UseParallel, true); ## Error conditions %!error ... %! statset ('nosuchfun') %!error ... %! statset ('MaxIter') %!error ... %! statset ('TreeBagger') %!error ... %! statset (1, 2) %!error ... %! statset ([], 'MaxIter') %!error ... %! statset ('MaxIter', 5, 'TolX') %!error ... %! statset (statset (), 'MaxIter') %!error ... %! statset ('NoSuchOption', 1) %!error ... %! statset ('MaxIter', 'abc') %!error ... %! statset ('MaxIter', -1) %!error ... %! statset ('MaxIter', int32 (5)) %!error ... %! statset ('MaxIter', 1.5) %!error ... %! statset ('MaxFunEvals', -1) %!error ... %! statset ('TolX', -1) %!error ... %! statset ('TolX', 0) %!error ... %! statset ('TolBnd', 0) %!error ... %! statset ('TolFun', -1) %!error ... %! statset ('Tune', 0) %!error ... %! statset ('Display', 'bogus') %!error ... %! statset ('GradObj', 'maybe') %!error ... %! statset ('UseParallel', 'maybe') %!error ... %! statset ('DerivStep', -1) %!error ... %! statset ('OutputFcn', 'notafunction') %!error ... %! statset ('Streams', 5) %!error ... %! statset ('RobustWgtFun', 'nosuchweight') %!error ... %! statset (struct ('MaxIter', {1, 2}), 'TolX', 1) statistics-release-2.1.2/io.github.gnu_octave.statistics.metainfo.xml000066400000000000000000000022271527311104300260610ustar00rootroot00000000000000 io.github.gnu_octave.statistics org.octave.Octave FSFAP GPL-3.0-or-later Statistics

The Statistics package for GNU Octave Octave Statistics

The Statistics package for GNU Octave is a collection of functions for statistical analysis.

https://github.com/gnu-octave/statistics/issues https://gnu-octave.github.io/statistics Octave Community octave-maintainers@gnu.org statistics-release-2.1.2/src/000077500000000000000000000000001527311104300161105ustar00rootroot00000000000000statistics-release-2.1.2/src/Makefile000066400000000000000000000072701527311104300175560ustar00rootroot00000000000000# Makefile for compiling required oct files (parallel version) # List all target oct files OCTFILES = editDistance.oct libsvmread.oct libsvmwrite.oct svmpredict.oct \ svmtrain.oct fcnntrain.oct fcnnpredict.oct \ gamtrain.oct gampredict.oct gamboosttrain.oct gamboostpredict.oct \ gamboostpairs.oct gamboostinter.oct __lbfgs__.oct __bhtsne__.oct \ __knnselect__.oct __knnbrute__.oct \ treetrain.oct treepredict.oct __treeprune__.oct \ __shapleytree__.oct __nomdist__.oct # Default target - depends on all oct files (enables parallel build) all: $(OCTFILES) # Individual targets for each oct file with dependencies editDistance.oct: editDistance.cc $(MKOCTFILE) editDistance.cc libsvmread.oct: libsvmread.cc $(MKOCTFILE) libsvmread.cc libsvmwrite.oct: libsvmwrite.cc $(MKOCTFILE) libsvmwrite.cc svmpredict.oct: svmpredict.cc svm.cpp svm_model_octave.cpp $(MKOCTFILE) svmpredict.cc svm.cpp svm_model_octave.cpp svmtrain.oct: svmtrain.cc svm.cpp svm_model_octave.cpp $(MKOCTFILE) svmtrain.cc svm.cpp svm_model_octave.cpp # fcnn.cpp is #included by both, so it is a prerequisite of both: without it # an edit to the network code leaves the oct-files stale and the build silent. # fcnntrain also includes the lbfgs engine, and for the same reason. fcnntrain.oct: fcnntrain.cc fcnn.cpp lbfgs.h $(MKOCTFILE) fcnntrain.cc fcnnpredict.oct: fcnnpredict.cc fcnn.cpp $(MKOCTFILE) fcnnpredict.cc # gam.cpp is #included by both, exactly as fcnn.cpp is, and for the same # reason: the engine is shared between the classification and the regression # learner, and an edit to it must rebuild both oct-files. gamboostinter.oct: gamboostinter.cc gamboost.cpp $(MKOCTFILE) gamboostinter.cc gamboostpairs.oct: gamboostpairs.cc gamboost.cpp $(MKOCTFILE) gamboostpairs.cc gamboosttrain.oct: gamboosttrain.cc gamboost.cpp $(MKOCTFILE) gamboosttrain.cc gamboostpredict.oct: gamboostpredict.cc gamboost.cpp $(MKOCTFILE) gamboostpredict.cc gamtrain.oct: gamtrain.cc gam.cpp $(MKOCTFILE) gamtrain.cc gampredict.oct: gampredict.cc gam.cpp $(MKOCTFILE) gampredict.cc # lbfgs.h is the engine both this wrapper and any compiled learner include, so # an edit to it must rebuild the oct-file. __lbfgs__.oct: __lbfgs__.cc lbfgs.h $(MKOCTFILE) __lbfgs__.cc # The Barnes-Hut summation behind tsne's 'barneshut' algorithm. __bhtsne__.oct: __bhtsne__.cc $(MKOCTFILE) __bhtsne__.cc # The partial selection the nearest-neighbour searches take their K from. __knnselect__.oct: __knnselect__.cc $(MKOCTFILE) __knnselect__.cc # The exhaustive search that never forms the distance matrix. __knnbrute__.oct: __knnbrute__.cc $(MKOCTFILE) __knnbrute__.cc # The nominal dissimilarity measures behind nomdist and nomdist2. __nomdist__.oct: __nomdist__.cc $(MKOCTFILE) __nomdist__.cc # Clean target clean: rm -f $(OCTFILES) # Declare targets that don't create files .PHONY: all clean # tree.cpp is #included by both, exactly as gam.cpp is, and for the same # reason: the engine is shared between the classification and the regression # learner, and an edit to it must rebuild both oct-files. treetrain.oct: treetrain.cc tree.cpp $(MKOCTFILE) treetrain.cc treepredict.oct: treepredict.cc tree.cpp $(MKOCTFILE) treepredict.cc # The pruning sequence on its own, for a tree that is already grown: prune # has no data left and a model read back from a file never had any. __treeprune__.oct: __treeprune__.cc $(MKOCTFILE) __treeprune__.cc # The Shapley values of a tree, and of an ensemble of them, taken leaf by # leaf: an ensemble of any width costs its trees, where the subsets the .m # path uses stop at a budget and estimate past it. __shapleytree__.oct: __shapleytree__.cc $(MKOCTFILE) __shapleytree__.cc statistics-release-2.1.2/src/__bhtsne__.cc000066400000000000000000000223571527311104300205070ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include // Barnes-Hut summation of the t-SNE repulsive term. // // The gradient of the t-SNE cost splits into an attraction over the pairs // carrying a nonzero P, which is cheap and stays in the interpreter, and a // repulsion over EVERY pair, which is what makes the exact algorithm quadratic. // This file computes the repulsion in O(N log N) by the method of van der // Maaten (2014): build a 2^D-ary tree over the embedding, then walk it once per // point, collapsing a whole cell into its centre of mass whenever the cell is // far enough away that its internal structure cannot matter. // // Returned unnormalized, because the normalizer is itself one of the sums the // walk accumulates and the caller needs both: // // FREP(i,:) = sum over cells n * q^2 * (y_i - com), q = 1 / (1 + d2) // Z = sum over cells n * q (over every i) // // so the repulsive force on point i is FREP(i,:) / Z and the low-dimensional // affinities are q / Z. // The subdivision stops here whatever the criterion says. Coincident points // never separate, so a tree that splits until every cell holds one point does // not terminate on duplicated rows, which a t-SNE embedding produces readily // in its first iterations while every point still sits on the origin. static const int BHT_MAX_DEPTH = 40; struct BHNode { // Geometry of the cell, as centre and half-width per dimension. std::vector centre; std::vector halfw; // What the cell carries: how many points, and where their centre of mass is. octave_idx_type count; std::vector com; // The longest edge, which is the length scale the opening criterion uses. double width; // A leaf names its points; an internal node names its children. Exactly one // of the two is ever non-empty. std::vector points; std::vector children; }; // Build the subtree rooted at NODE over the point indices IDX, subdividing // until a cell holds a single point or the depth cap stops it. static void bht_build (std::vector& tree, octave_idx_type node, const Matrix& Y, std::vector& idx, int depth) { const octave_idx_type d = Y.columns (); const octave_idx_type n = idx.size (); tree[node].count = n; tree[node].com.assign (d, 0.0); for (octave_idx_type k = 0; k < n; k++) { for (octave_idx_type j = 0; j < d; j++) { tree[node].com[j] += Y(idx[k], j); } } for (octave_idx_type j = 0; j < d; j++) { tree[node].com[j] /= (double) n; } tree[node].width = 0.0; for (octave_idx_type j = 0; j < d; j++) { double w = 2.0 * tree[node].halfw[j]; if (w > tree[node].width) { tree[node].width = w; } } if (n <= 1 || depth >= BHT_MAX_DEPTH) { tree[node].points = idx; return; } // Sort the points into the 2^d octants of this cell, one bit per dimension. const octave_idx_type nsub = (octave_idx_type) 1 << d; std::vector> bucket (nsub); for (octave_idx_type k = 0; k < n; k++) { octave_idx_type b = 0; for (octave_idx_type j = 0; j < d; j++) { if (Y(idx[k], j) > tree[node].centre[j]) { b |= ((octave_idx_type) 1 << j); } } bucket[b].push_back (idx[k]); } // Every point landing in one octant means the cell cannot be split usefully // at this level, but the halved cell still tightens, so recursion continues // and the depth cap is what ends it. for (octave_idx_type b = 0; b < nsub; b++) { if (bucket[b].empty ()) { continue; } BHNode child; child.centre.resize (d); child.halfw.resize (d); for (octave_idx_type j = 0; j < d; j++) { double h = 0.5 * tree[node].halfw[j]; child.halfw[j] = h; child.centre[j] = (b & ((octave_idx_type) 1 << j)) ? tree[node].centre[j] + h : tree[node].centre[j] - h; } child.count = 0; tree.push_back (child); octave_idx_type c = tree.size () - 1; tree[node].children.push_back (c); bht_build (tree, c, Y, bucket[b], depth + 1); } } // Accumulate the repulsion felt by point I from the subtree at NODE. A leaf is // summed pair by pair, skipping I itself; an internal node is collapsed into // its centre of mass when the opening criterion allows and descended otherwise. static void bht_forces (const std::vector& tree, octave_idx_type node, const Matrix& Y, octave_idx_type i, double theta, double *frep, double& Z) { const octave_idx_type d = Y.columns (); const BHNode& nd = tree[node]; if (nd.count == 0) { return; } if (! nd.points.empty ()) { for (std::size_t k = 0; k < nd.points.size (); k++) { octave_idx_type j = nd.points[k]; if (j == i) { continue; } double d2 = 0.0; for (octave_idx_type m = 0; m < d; m++) { double t = Y(i, m) - Y(j, m); d2 += t * t; } double q = 1.0 / (1.0 + d2); Z += q; double qq = q * q; for (octave_idx_type m = 0; m < d; m++) { frep[m] += qq * (Y(i, m) - Y(j, m)); } } return; } // Distance from the point to the cell's centre of mass. double d2 = 0.0; for (octave_idx_type m = 0; m < d; m++) { double t = Y(i, m) - nd.com[m]; d2 += t * t; } // The opening criterion. A cell counts as distant when its width is small // against that distance; theta = 0 therefore opens every cell and the walk // degenerates to the exact pairwise sum, which is what pins this file's // correctness without an oracle. bool distant = (nd.width < theta * std::sqrt (d2)); if (distant) { double q = 1.0 / (1.0 + d2); double n = (double) nd.count; Z += n * q; double nqq = n * q * q; for (octave_idx_type m = 0; m < d; m++) { frep[m] += nqq * (Y(i, m) - nd.com[m]); } return; } for (std::size_t k = 0; k < nd.children.size (); k++) { bht_forces (tree, nd.children[k], Y, i, theta, frep, Z); } } DEFUN_DLD(__bhtsne__, args, , "-*- texinfo -*-\n\ @deftypefn {statistics} {[@var{Frep}, @var{Z}] =} __bhtsne__ (@var{Y}, @var{theta})\n\ \n\ Barnes-Hut summation of the t-SNE repulsive term. Internal; called by\n\ @code{tsne} and not meant to be used directly.\n\ \n\ @var{Y} is the @math{N*D} embedding, @math{D} being 1, 2 or 3, and @var{theta}\n\ the opening criterion, a non-negative scalar. @var{Frep} is the @math{N*D}\n\ matrix of unnormalized repulsive forces and @var{Z} the normalizer, so the\n\ force on a point is its row of @var{Frep} divided by @var{Z}.\n\ \n\ @var{theta} of zero opens every cell and reproduces the exact pairwise sum.\n\ \n\ @end deftypefn") { if (args.length () != 2) { print_usage (); } if (! args(0).isnumeric () || args(0).iscomplex () || args(0).isempty ()) { error ("__bhtsne__: Y must be a real numeric matrix."); } if (! args(1).is_scalar_type () || ! args(1).isnumeric () || args(1).iscomplex ()) { error ("__bhtsne__: THETA must be a real scalar."); } Matrix Y = args(0).matrix_value (); double theta = args(1).scalar_value (); if (theta < 0.0 || ! octave::math::isfinite (theta)) { error ("__bhtsne__: THETA must be non-negative and finite."); } const octave_idx_type n = Y.rows (); const octave_idx_type d = Y.columns (); if (d < 1 || d > 3) { error ("__bhtsne__: Y must have 1, 2, or 3 columns."); } // The root cell, sized to contain every point with room to spare so that a // point never sits exactly on a boundary. BHNode root; root.centre.resize (d); root.halfw.resize (d); for (octave_idx_type j = 0; j < d; j++) { double lo = Y(0, j); double hi = Y(0, j); for (octave_idx_type i = 1; i < n; i++) { if (Y(i, j) < lo) { lo = Y(i, j); } if (Y(i, j) > hi) { hi = Y(i, j); } } root.centre[j] = 0.5 * (lo + hi); root.halfw[j] = 0.5 * (hi - lo) + 1e-5; } root.count = 0; std::vector tree; tree.push_back (root); std::vector idx (n); for (octave_idx_type i = 0; i < n; i++) { idx[i] = i; } bht_build (tree, 0, Y, idx, 0); Matrix Frep (n, d, 0.0); double Z = 0.0; std::vector frep (d); for (octave_idx_type i = 0; i < n; i++) { for (octave_idx_type m = 0; m < d; m++) { frep[m] = 0.0; } bht_forces (tree, 0, Y, i, theta, &frep[0], Z); for (octave_idx_type m = 0; m < d; m++) { Frep(i, m) = frep[m]; } } return ovl (Frep, Z); } statistics-release-2.1.2/src/__knnbrute__.cc000066400000000000000000000262501527311104300210500ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include // Exhaustive K nearest neighbours, without forming the distance matrix. // // The straightforward way to search exhaustively is to compute every distance, // keep the whole M-by-N matrix, and then take the K smallest of each row. The // matrix is the problem: at four thousand points against four thousand it is // 128 MB that is written once, read once and thrown away, and it puts a // ceiling on N long before patience runs out. // // Here each distance is used the moment it is computed, against a running list // of the K best for that query, and then discarded. Memory is the size of the // answer. The list is kept sorted, so the output needs no sorting afterwards // and the rejection test is a single compare against its last element, which // fails for almost every point once the list has filled. // // Cosine is deliberately absent. Its distance is 1 minus a quantity that is // exactly 1 for parallel rows, so on collinear or one-dimensional data every // true distance is 0 and the ordering is decided entirely by which way that // subtraction rounds. pdist2 does not agree with its own written formula // there, returning 1.11e-16 where 1 - dot*sx*sy gives 0, so there is no fixed // result to reproduce. Cosine keeps the pdist2 route, where whatever it // answers is what it has always answered. // // Ties resolve as a stable sort resolves them, the lower index first, because // a candidate is admitted only when it is strictly better than the worst held // and is inserted after every entry it ties with. enum MetricCode { MET_EUCLIDEAN = 0, MET_CITYBLOCK, MET_CHEBYCHEV, MET_MINKOWSKI }; // Ordering of a candidate against a held entry, matching sort: a NaN comes // after every number, and equal values keep the lower index. template static inline bool better (T da, octave_idx_type ia, T db, octave_idx_type ib) { bool na = std::isnan (da); bool nb = std::isnan (db); if (na != nb) { return nb; } if (! na && da != db) { return da < db; } return ia < ib; } template static octave_value_list knnbrute (const MT& X, const MT& Y, octave_idx_type K, int metric, double q) { const octave_idx_type n = X.rows (); const octave_idx_type p = X.columns (); const octave_idx_type m = Y.rows (); // Pack both sets so that a point's coordinates are contiguous. Octave holds // them column-major, where the inner loop would stride by the row count and // miss the cache on every coordinate. std::vector xr ((std::size_t) n * p); std::vector yr ((std::size_t) m * p); for (octave_idx_type c = 0; c < p; c++) { for (octave_idx_type i = 0; i < n; i++) { xr[(std::size_t) i * p + c] = X(i, c); } for (octave_idx_type j = 0; j < m; j++) { yr[(std::size_t) j * p + c] = Y(j, c); } } Matrix OI (m, K); MT OD (m, K); std::vector bd (K); std::vector bi (K); for (octave_idx_type j = 0; j < m; j++) { const T *yp = &yr[(std::size_t) j * p]; octave_idx_type held = 0; for (octave_idx_type i = 0; i < n; i++) { const T *xp = &xr[(std::size_t) i * p]; // Euclidean and Minkowski are ranked on the sum and rooted at the end, // which saves a root per pair and cannot reorder anything, both roots // being monotone. T d; switch (metric) { case MET_CITYBLOCK: { d = 0; for (octave_idx_type c = 0; c < p; c++) { d += std::abs (xp[c] - yp[c]); } break; } case MET_CHEBYCHEV: { d = 0; for (octave_idx_type c = 0; c < p; c++) { T t = std::abs (xp[c] - yp[c]); if (t > d || std::isnan (t)) { d = t; } } break; } case MET_MINKOWSKI: { d = 0; for (octave_idx_type c = 0; c < p; c++) { d += std::pow (std::abs (xp[c] - yp[c]), (T) q); } break; } default: { d = 0; for (octave_idx_type c = 0; c < p; c++) { T t = xp[c] - yp[c]; d += t * t; } break; } } // The list is sorted, so its last entry is the one to beat. if (held == K && ! better (d, i, bd[K-1], bi[K-1])) { continue; } octave_idx_type at = (held < K) ? held : K - 1; while (at > 0 && better (d, i, bd[at-1], bi[at-1])) { bd[at] = bd[at-1]; bi[at] = bi[at-1]; at--; } bd[at] = d; bi[at] = i; if (held < K) { held++; } } for (octave_idx_type c = 0; c < K; c++) { OI(j, c) = (double) (bi[c] + 1); T v = bd[c]; if (metric == MET_EUCLIDEAN) { v = std::sqrt (v); } else if (metric == MET_MINKOWSKI) { v = std::pow (v, (T) (1.0 / q)); } OD(j, c) = v; } } return ovl (OI, OD); } DEFUN_DLD(__knnbrute__, args, , "-*- texinfo -*-\n\ @deftypefn {statistics} {[@var{idx}, @var{D}] =} __knnbrute__ (@var{X}, @var{Y}, @var{K}, @var{metric}, @var{param})\n\ \n\ Exhaustive nearest-neighbour search without forming the distance matrix.\n\ Internal; called by the nearest-neighbour searches and not meant to be used\n\ directly.\n\ \n\ @var{X} holds the reference points and @var{Y} the queries, one per row.\n\ @var{metric} is one of @qcode{'euclidean'}, @qcode{'cityblock'},\n\ @qcode{'chebychev'} or @qcode{'minkowski'}, and @var{param}\n\ the exponent for @qcode{'minkowski'} and empty otherwise.\n\ \n\ @var{idx} and @var{D} are the @math{M*K} indices and distances of the @var{K}\n\ nearest reference points to each query, in increasing distance, ordered as\n\ sorting the full row would order them.\n\ \n\ @end deftypefn") { if (args.length () != 5) { print_usage (); } if (! args(0).isnumeric () || args(0).iscomplex () || args(0).isempty ()) { error ("__knnbrute__: X must be a real numeric matrix."); } if (! args(1).isnumeric () || args(1).iscomplex () || args(1).isempty ()) { error ("__knnbrute__: Y must be a real numeric matrix."); } if (args(0).columns () != args(1).columns ()) { error ("__knnbrute__: X and Y must have the same number of columns."); } if (! args(2).is_scalar_type () || ! args(2).isnumeric ()) { error ("__knnbrute__: K must be a real scalar."); } if (! args(3).is_string ()) { error ("__knnbrute__: METRIC must be a character vector."); } const octave_idx_type n = args(0).rows (); double kd = args(2).scalar_value (); if (kd != std::floor (kd) || kd < 1.0 || kd > (double) n) { error ("__knnbrute__: K must be an integer between 1 and rows (X)."); } octave_idx_type K = (octave_idx_type) kd; std::string name = args(3).string_value (); int metric; if (name == "euclidean") { metric = MET_EUCLIDEAN; } else if (name == "cityblock") { metric = MET_CITYBLOCK; } else if (name == "chebychev") { metric = MET_CHEBYCHEV; } else if (name == "minkowski") { metric = MET_MINKOWSKI; } else { error ("__knnbrute__: unsupported METRIC '%s'.", name.c_str ()); } double q = 2.0; if (metric == MET_MINKOWSKI) { if (! args(4).is_scalar_type () || ! args(4).isnumeric ()) { error ("__knnbrute__: PARAM must be the minkowski exponent."); } q = args(4).scalar_value (); if (! (q > 0) || ! octave::math::isfinite (q)) { error ("__knnbrute__: PARAM must be a positive finite scalar."); } } if (args(0).is_single_type () || args(1).is_single_type ()) { return knnbrute (args(0).float_matrix_value (), args(1).float_matrix_value (), K, metric, q); } return knnbrute (args(0).matrix_value (), args(1).matrix_value (), K, metric, q); } /* %!test %! ## It answers what pdist2 followed by a sort answers, exactly. %! X = [1, 2; 3, 4; 5, 6; 7, 8; 2, 2; -1, 0]; %! Y = [2, 3; 6, 7; 0, 0]; %! for m = {"euclidean", "cityblock", "chebychev"} %! D = pdist2 (X, Y, m{1})'; %! [sv, so] = sort (D, 2); %! for k = 1:6 %! [idx, dst] = __knnbrute__ (X, Y, k, m{1}, []); %! assert_equal (idx, so(:,1:k)); %! assert_equal (dst, sv(:,1:k)); %! endfor %! endfor %!test %! ## Minkowski takes its exponent and agrees at each of them. %! X = [1, 2; 3, 4; 5, 6; 7, 8]; %! Y = [2, 3; 6, 7]; %! for q = [1, 1.5, 2, 3] %! D = pdist2 (X, Y, "minkowski", q)'; %! [sv, so] = sort (D, 2); %! [idx, dst] = __knnbrute__ (X, Y, 3, "minkowski", q); %! assert_equal (idx, so(:,1:3)); %! assert_equal (dst, sv(:,1:3), 1e-12); %! endfor %!test %! ## Ties keep the lower index, as a stable sort does. %! X = [0, 0; 0, 0; 0, 0]; %! [idx, dst] = __knnbrute__ (X, [1, 1], 2, "euclidean", []); %! assert_equal (idx, [1, 2]); %! assert_equal (dst, [sqrt(2), sqrt(2)], 1e-12); %!test %! ## A NaN in the data puts that neighbour last, never first. %! X = [0, 0; NaN, 0; 1, 1]; %! [idx, dst] = __knnbrute__ (X, [0, 0], 3, "euclidean", []); %! assert_equal (idx(1:2), [1, 3]); %! assert_equal (idx(3), 2); %! assert_equal (isnan (dst(3)), true); %!test %! ## Single data gives single distances; the indices stay double. %! [idx, dst] = __knnbrute__ (single ([1, 1; 4, 4]), single ([0, 0]), 1, ... %! "euclidean", []); %! assert_equal (class (dst), 'single'); %! assert_equal (class (idx), 'double'); %!test %! ## K may be the whole reference set. %! X = [3, 0; 1, 0; 2, 0]; %! [idx, dst] = __knnbrute__ (X, [0, 0], 3, "euclidean", []); %! assert_equal (idx, [2, 3, 1]); %! assert_equal (dst, [1, 2, 3], 1e-12); %!error __knnbrute__ (ones (2, 2), ones (1, 2), 1) %!error<__knnbrute__: X must be a real numeric matrix.> ... %! __knnbrute__ ({1}, ones (1, 2), 1, "euclidean", []) %!error<__knnbrute__: X and Y must have the same number of columns.> ... %! __knnbrute__ (ones (2, 3), ones (1, 2), 1, "euclidean", []) %!error<__knnbrute__: K must be an integer between 1 and rows \(X\).> ... %! __knnbrute__ (ones (2, 2), ones (1, 2), 3, "euclidean", []) %!error<__knnbrute__: unsupported METRIC 'cosine'.> ... %! __knnbrute__ (ones (2, 2), ones (1, 2), 1, "cosine", []) %!error<__knnbrute__: PARAM must be a positive finite scalar.> ... %! __knnbrute__ (ones (2, 2), ones (1, 2), 1, "minkowski", -1) */ statistics-release-2.1.2/src/__knnselect__.cc000066400000000000000000000161171527311104300212070ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include // The K smallest entries of each row, by partial selection. // // A nearest-neighbour search spends most of its time here rather than in the // distances: taking the five smallest of four thousand by sorting all four // thousand costs O(N log N) per row where O(N) will do, and on a 4000-by-4000 // matrix the sort is roughly nine times the cost of forming the distances. // // The order returned is Octave's own, so this is a drop-in replacement for // sort rather than an approximation of it: non-NaN before NaN, then by value, // then by column index, which is what a stable sort gives for ties. template struct ValIdxLess { typedef std::pair ValIdx; bool operator() (const ValIdx& a, const ValIdx& b) const { bool na = std::isnan (a.first); bool nb = std::isnan (b.first); if (na != nb) { // A NaN sorts after every number, as it does in sort. return nb; } if (! na && a.first != b.first) { return a.first < b.first; } // Ties, NaN against NaN included, keep the lower column first. return a.second < b.second; } }; // The class of the distances is carried through, single in and single out, as // the searches promise. The indices are always double. template static octave_value_list knnselect (const MT& D, octave_idx_type K) { typedef std::pair ValIdx; const octave_idx_type m = D.rows (); const octave_idx_type n = D.columns (); Matrix OI (m, K); MT OD (m, K); const T *dp = D.data (); std::vector row (n); ValIdxLess cmp; for (octave_idx_type i = 0; i < m; i++) { for (octave_idx_type j = 0; j < n; j++) { row[j] = ValIdx (dp[i + j * m], j); } // nth_element leaves the K smallest below the pivot in no order, so the // kept head is sorted afterwards; both together stay linear in N. if (K < n) { std::nth_element (row.begin (), row.begin () + K, row.end (), cmp); } std::sort (row.begin (), row.begin () + K, cmp); for (octave_idx_type j = 0; j < K; j++) { OI(i, j) = (double) (row[j].second + 1); OD(i, j) = row[j].first; } } return ovl (OI, OD); } DEFUN_DLD(__knnselect__, args, , "-*- texinfo -*-\n\ @deftypefn {statistics} {[@var{idx}, @var{D}] =} __knnselect__ (@var{dist}, @var{K})\n\ \n\ The @var{K} smallest entries of each row of @var{dist}. Internal; called by\n\ the nearest-neighbour searches and not meant to be used directly.\n\ \n\ @var{dist} is an @math{M*N} matrix of distances whose rows are query points,\n\ and @var{K} a positive integer no greater than @math{N}. @var{idx} is the\n\ @math{M*K} matrix of column indices of the @var{K} smallest entries of each\n\ row, and @var{D} their values, in the class @var{dist} carries.\n\ \n\ The result is identical to sorting each row and taking the first @var{K}\n\ columns, ties and @qcode{NaN} included, and is obtained by partial selection\n\ rather than by a full sort.\n\ \n\ @end deftypefn") { if (args.length () != 2) { print_usage (); } if (! args(0).isnumeric () || args(0).iscomplex () || args(0).isempty ()) { error ("__knnselect__: DIST must be a real numeric matrix."); } if (! args(1).is_scalar_type () || ! args(1).isnumeric () || args(1).iscomplex ()) { error ("__knnselect__: K must be a real scalar."); } const octave_idx_type n = args(0).columns (); double kd = args(1).scalar_value (); if (kd != std::floor (kd) || kd < 1.0 || kd > (double) n) { error ("__knnselect__: K must be an integer between 1 and columns (DIST)."); } octave_idx_type K = (octave_idx_type) kd; if (args(0).is_single_type ()) { return knnselect (args(0).float_matrix_value (), K); } return knnselect (args(0).matrix_value (), K); } /* %!test %! ## The result is sort's, not an approximation of it. %! D = [3, 1, 2, 1; 5, 5, 5, 5; 9, 8, 1, 2; 0, -1, -1, 4]; %! [sv, so] = sort (D, 2); %! for k = 1:4 %! [idx, dst] = __knnselect__ (D, k); %! assert_equal (idx, so(:,1:k)); %! assert_equal (dst, sv(:,1:k)); %! endfor %!test %! ## Ties keep the lower column first, as a stable sort does. %! [idx, dst] = __knnselect__ ([5, 5, 5, 5], 3); %! assert_equal (idx, [1, 2, 3]); %! assert_equal (dst, [5, 5, 5]); %!test %! ## A NaN sorts after every number and never displaces one. %! [idx, dst] = __knnselect__ ([3, NaN, 1, NaN, 2], 3); %! assert_equal (idx, [3, 5, 1]); %! assert_equal (dst, [1, 2, 3]); %!test %! ## A row of nothing but NaN keeps them in column order. %! [idx, dst] = __knnselect__ ([NaN, NaN, NaN], 2); %! assert_equal (idx, [1, 2]); %! assert_equal (isnan (dst), [true, true]); %!test %! ## Inf is a value like any other and comes before NaN. %! [idx, dst] = __knnselect__ ([Inf, NaN, 2, -Inf], 3); %! assert_equal (idx, [4, 3, 1]); %! assert_equal (dst, [-Inf, 2, Inf]); %!test %! ## K equal to the width returns the whole row, sorted. %! [idx, dst] = __knnselect__ ([4, 2, 9, 1], 4); %! assert_equal (idx, [4, 2, 1, 3]); %! assert_equal (dst, [1, 2, 4, 9]); %!test %! ## Single distances come back single; the indices stay double. %! [idx, dst] = __knnselect__ (single ([4, 2, 9, 1]), 2); %! assert_equal (class (dst), 'single'); %! assert_equal (class (idx), 'double'); %! assert_equal (dst, single ([1, 2])); %!test %! ## It agrees with sort over random matrices at every width of K. %! rand ("seed", 42); %! for t = 1:50 %! A = round (rand (6, 9) * 4); %! k = 1 + mod (t, 9); %! [idx, dst] = __knnselect__ (A, k); %! [sv, so] = sort (A, 2); %! assert_equal (idx, so(:,1:k)); %! assert_equal (dst, sv(:,1:k)); %! endfor %!error __knnselect__ (ones (2, 2)) %!error<__knnselect__: DIST must be a real numeric matrix.> ... %! __knnselect__ ({1, 2}, 1) %!error<__knnselect__: DIST must be a real numeric matrix.> ... %! __knnselect__ (ones (2, 2) * i, 1) %!error<__knnselect__: K must be a real scalar.> ... %! __knnselect__ (ones (2, 2), [1, 2]) %!error<__knnselect__: K must be an integer between 1 and columns \(DIST\).> ... %! __knnselect__ (ones (2, 3), 0) %!error<__knnselect__: K must be an integer between 1 and columns \(DIST\).> ... %! __knnselect__ (ones (2, 3), 4) %!error<__knnselect__: K must be an integer between 1 and columns \(DIST\).> ... %! __knnselect__ (ones (2, 3), 1.5) */ statistics-release-2.1.2/src/__lbfgs__.cc000066400000000000000000000551541527311104300203220ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include "lbfgs.h" using namespace std; // Objective adaptor for an Octave function handle. One call is one // interpreter round trip returning [f, g], which is why the compiled learners // include lbfgs.h directly instead of coming through here. class octave_objective { public: octave_objective (const octave_value& fcn, octave_idx_type n) : m_fcn (fcn), m_n (n) { } double operator () (const vector& x, vector& g) { octave_quit (); ColumnVector xv (m_n); for (octave_idx_type i = 0; i < m_n; i++) { xv(i) = x[i]; } octave_value_list in (1); in(0) = xv; octave_value_list out = octave::feval (m_fcn, in, 2); if (out.length () < 2) { error ("__lbfgs__: FCN must return both a value and a gradient."); } if (! out(0).isnumeric () || ! out(0).is_scalar_type () || out(0).iscomplex ()) { error ("__lbfgs__: FCN must return a real scalar as its first output."); } if (! out(1).isnumeric () || out(1).iscomplex () || out(1).numel () != m_n) { error ("__lbfgs__: FCN must return a real gradient with as many " "elements as X0."); } NDArray gv = out(1).array_value (); for (octave_idx_type i = 0; i < m_n; i++) { g[i] = gv(i); } return out(0).double_value (); } private: octave_value m_fcn; octave_idx_type m_n; }; static bool is_real_scalar (const octave_value& v) { if (! v.isnumeric () || ! v.is_scalar_type () || v.iscomplex ()) { return false; } double d = v.double_value (); return (d == d); } static bool is_whole (double d) { return (d < HUGE_VAL && d > -HUGE_VAL && d == floor (d)); } DEFUN_DLD (__lbfgs__, args, nargout, "-*- texinfo -*-\n\ @deftypefn {statistics} {@var{x} =} __lbfgs__ (@var{fcn}, @var{x0})\n\ @deftypefnx {statistics} {@var{x} =} __lbfgs__ (@var{fcn}, @var{x0}, @var{options})\n\ @deftypefnx {statistics} {[@var{x}, @var{info}] =} __lbfgs__ (@dots{})\n\ \n\ Minimize @var{fcn} by limited-memory BFGS with a strong Wolfe line search.\n\ \n\ @var{fcn} is a function handle called as @code{[f, g] = fcn (x)}, returning\n\ the objective value at @var{x} and its gradient. @var{x0} is the real\n\ numeric vector the iteration starts from. @var{x} is returned as a column\n\ vector whatever the orientation of @var{x0}.\n\ \n\ @var{options} is a scalar struct. Every field is optional and the defaults\n\ are MATLAB's, as measured on R2024a.\n\ \n\ @multitable @columnfractions 0.25 0.15 0.60\n\ @headitem Field @tab Default @tab Description\n\ \n\ @item @qcode{IterationLimit} @tab @qcode{1000} @tab Largest number of\n\ iterations taken.\n\ \n\ @item @qcode{GradientTolerance} @tab @qcode{1e-6} @tab Stop once the\n\ gradient's infinity norm falls to or below this value.\n\ \n\ @item @qcode{StepTolerance} @tab @qcode{1e-6} @tab Stop once the step's\n\ infinity norm falls to or below this value.\n\ \n\ @item @qcode{LossTolerance} @tab @qcode{1e-6} @tab Stop once the objective\n\ VALUE falls to or below this value. This is not a test on the change in the\n\ objective, which is what the name suggests; it matches MATLAB. Pass\n\ @code{-Inf} to disable it, which any objective that can go negative, or whose\n\ minimum is zero, will want.\n\ \n\ @item @qcode{BetaTolerance} @tab @qcode{0} @tab Stop once the RELATIVE change\n\ in the iterate falls to or below this value, measured as the two-norm of the\n\ step over the two-norm of the point it landed on, with the denominator\n\ unguarded. A value of @qcode{0} does not test against zero: it switches the\n\ test off and leaves @qcode{RelativeChangeInBeta} at @code{NaN}, as MATLAB\n\ does. Note that this disables with @qcode{0} where @qcode{LossTolerance}\n\ disables with @code{-Inf}; each follows MATLAB for its own quantity.\n\ \n\ @item @qcode{HistorySize} @tab @qcode{10} @tab Number of curvature pairs the\n\ inverse Hessian approximation is built from.\n\ \n\ @item @qcode{InitialStepSize} @tab @qcode{[]} @tab Step length tried first on\n\ the opening iteration. Scaled from the gradient when left empty.\n\ \n\ @item @qcode{Method} @tab @qcode{'lbfgs'} @tab @qcode{'lbfgs'} builds the\n\ inverse Hessian from the last @qcode{HistorySize} curvature pairs;\n\ @qcode{'bfgs'} keeps the full inverse Hessian, scaled by @math{s'y / y'y}\n\ once before its first update.\n\ \n\ @item @qcode{LineSearch} @tab @qcode{'strongwolfe'} @tab\n\ @qcode{'strongwolfe'} searches for a step meeting both strong Wolfe\n\ conditions; @qcode{'weakwolfe'} bisects and doubles from a unit step until\n\ the weak ones hold, as MATLAB's fitclinear does, and a search that cannot\n\ is not retried.\n\ @end multitable\n\ \n\ When more than one tolerance is met at the same iteration the reported\n\ criterion follows MATLAB's order: the relative change in the iterate, then\n\ gradient, then step, then loss.\n\ \n\ @var{info} is a struct with fields @qcode{Iterations}, @qcode{FuncCount},\n\ @qcode{Fval}, @qcode{Gradient}, @qcode{Step}, @qcode{RelativeChangeInBeta},\n\ @qcode{Criterion} and @qcode{History}, the last being a struct of\n\ per-iteration column vectors of the same names.\n\ \n\ @qcode{Criterion} is one of @qcode{'gradient'}, @qcode{'beta'},\n\ @qcode{'step'}, @qcode{'loss'}, @qcode{'iteration'} or\n\ @qcode{'linesearch'}. It is a token and not a sentence on purpose: MATLAB\n\ words the same criterion differently per function, so each caller maps it to\n\ the wording of the function it mirrors.\n\ \n\ This is an internal function and is not meant to be called directly.\n\ @end deftypefn") { octave_value_list retval; if (args.length () < 2 || args.length () > 3) { print_usage (); } if (! args(0).is_function_handle ()) { error ("__lbfgs__: FCN must be a function handle."); } if (! args(1).isnumeric () || args(1).iscomplex () || args(1).isempty () || (args(1).rows () != 1 && args(1).columns () != 1)) { error ("__lbfgs__: X0 must be a real numeric vector."); } lbfgs::options opt; if (args.length () == 3) { if (! args(2).isstruct () || args(2).numel () != 1) { error ("__lbfgs__: OPTIONS must be a scalar struct."); } octave_scalar_map map = args(2).scalar_map_value (); string_vector fields = map.fieldnames (); for (octave_idx_type i = 0; i < fields.numel (); i++) { std::string name = fields(i); octave_value val = map.contents (name); if (name == "IterationLimit") { if (! is_real_scalar (val) || val.double_value () < 0 || ! is_whole (val.double_value ())) { error ("__lbfgs__: 'IterationLimit' must be a nonnegative " "integer scalar."); } opt.iteration_limit = val.int_value (); } else if (name == "GradientTolerance" || name == "StepTolerance" || name == "BetaTolerance") { if (! is_real_scalar (val) || val.double_value () < 0) { error ("__lbfgs__: '%s' must be a nonnegative scalar.", name.c_str ()); } if (name == "GradientTolerance") { opt.gradient_tolerance = val.double_value (); } else if (name == "StepTolerance") { opt.step_tolerance = val.double_value (); } else { opt.beta_tolerance = val.double_value (); } } else if (name == "LossTolerance") { // This one is a value rather than a norm, so it is allowed to go // negative. An objective that can take negative values needs -Inf // here, or the test fires on the first step that crosses zero. if (! is_real_scalar (val)) { error ("__lbfgs__: 'LossTolerance' must be a real scalar."); } opt.loss_tolerance = val.double_value (); } else if (name == "HistorySize") { if (! is_real_scalar (val) || val.double_value () < 1 || ! is_whole (val.double_value ())) { error ("__lbfgs__: 'HistorySize' must be a positive integer " "scalar."); } opt.history_size = val.int_value (); } else if (name == "InitialStepSize") { // An empty value is MATLAB's way of asking for the automatic step. if (! val.isempty ()) { if (! is_real_scalar (val) || val.double_value () <= 0) { error ("__lbfgs__: 'InitialStepSize' must be a positive " "scalar."); } opt.initial_step_size = val.double_value (); } } else if (name == "Method" || name == "LineSearch") { std::string v = (val.is_string () ? val.string_value () : ""); if (name == "Method" && (v == "lbfgs" || v == "bfgs")) { opt.full_bfgs = (v == "bfgs"); } else if (name == "LineSearch" && (v == "strongwolfe" || v == "weakwolfe")) { opt.weak_wolfe = (v == "weakwolfe"); } else if (name == "Method") { error ("__lbfgs__: 'Method' must be 'lbfgs' or 'bfgs'."); } else { error ("__lbfgs__: 'LineSearch' must be 'strongwolfe' or " "'weakwolfe'."); } } else { error ("__lbfgs__: '%s' is not a valid option.", name.c_str ()); } } } octave_idx_type n = args(1).numel (); NDArray x0 = args(1).array_value (); vector x (n); for (octave_idx_type i = 0; i < n; i++) { x[i] = x0(i); } octave_objective fun (args(0), n); lbfgs::result res = lbfgs::minimize (fun, x, opt); ColumnVector xout (n); for (octave_idx_type i = 0; i < n; i++) { xout(i) = x[i]; } retval(0) = xout; if (nargout > 1) { octave_idx_type nh = res.history.size (); ColumnVector h_iter (nh), h_fval (nh), h_grad (nh), h_step (nh), h_beta (nh); for (octave_idx_type i = 0; i < nh; i++) { h_iter(i) = i + 1; h_fval(i) = res.history[i].fval; h_grad(i) = res.history[i].gradient; h_step(i) = res.history[i].step; h_beta(i) = res.history[i].rel_beta; } octave_scalar_map history; history.assign ("Iteration", h_iter); history.assign ("Fval", h_fval); history.assign ("Gradient", h_grad); history.assign ("Step", h_step); history.assign ("RelativeChangeInBeta", h_beta); octave_scalar_map info; info.assign ("Iterations", static_cast (res.iterations)); info.assign ("FuncCount", static_cast (res.funcount)); info.assign ("Fval", res.fval); info.assign ("Gradient", res.gradient); info.assign ("Step", res.step); info.assign ("RelativeChangeInBeta", res.rel_beta); info.assign ("Criterion", lbfgs::criterion_token (res.crit)); info.assign ("History", history); retval(1) = info; } return retval; } /* %!function [f, g] = __rosen__ (x) %! t = x(2) - x(1)^2; %! f = 100 * t^2 + (1 - x(1))^2; %! g = zeros (2, 1); %! g(1) = -400 * x(1) * t - 2 * (1 - x(1)); %! g(2) = 200 * t; %!endfunction %!function [f, g] = __exrosen__ (x) %! n = numel (x); %! o = (1:2:n-1)'; %! e = (2:2:n)'; %! t1 = x(e) - x(o).^2; %! t2 = 1 - x(o); %! f = sum (100 * t1.^2 + t2.^2); %! g = zeros (n, 1); %! g(o) = -400 * x(o) .* t1 - 2 * t2; %! g(e) = 200 * t1; %!endfunction %!function [f, g] = __beale__ (x) %! y = [1.5; 2.25; 2.625]; %! k = (1:3)'; %! t = 1 - x(2).^k; %! r = y - x(1) * t; %! f = sum (r.^2); %! g = zeros (2, 1); %! g(1) = -2 * sum (r .* t); %! g(2) = 2 * x(1) * sum (r .* k .* x(2).^(k - 1)); %!endfunction %!function [f, g] = __wood__ (x) %! t1 = 100 * (x(2) - x(1)^2)^2 + (1 - x(1))^2; %! t2 = 90 * (x(4) - x(3)^2)^2 + (1 - x(3))^2; %! t3 = 10.1 * ((1 - x(2))^2 + (1 - x(4))^2); %! t4 = 19.8 * (1 - x(2)) * (1 - x(4)); %! f = t1 + t2 + t3 + t4; %! g = zeros (4, 1); %! g(1) = -400 * x(1) * (x(2) - x(1)^2) - 2 * (1 - x(1)); %! g(2) = 200 * (x(2) - x(1)^2) - 20.2 * (1 - x(2)) - 19.8 * (1 - x(4)); %! g(3) = -360 * x(3) * (x(4) - x(3)^2) - 2 * (1 - x(3)); %! g(4) = 180 * (x(4) - x(3)^2) - 20.2 * (1 - x(4)) - 19.8 * (1 - x(2)); %!endfunction %!function [f, g] = __powellsq__ (x) %! a = x(1) + 10 * x(2); %! b = x(3) - x(4); %! c = x(2) - 2 * x(3); %! d = x(1) - x(4); %! f = a^2 + 5 * b^2 + c^4 + 10 * d^4; %! g = zeros (4, 1); %! g(1) = 2 * a + 40 * d^3; %! g(2) = 20 * a + 4 * c^3; %! g(3) = 10 * b - 8 * c^3; %! g(4) = -10 * b - 40 * d^3; %!endfunction %!function [f, g] = __quadtri__ (x) %! n = numel (x); %! A = 2 * eye (n) - diag (ones (n - 1, 1), 1) - diag (ones (n - 1, 1), -1); %! xs = ((1:n)' / n).^2; %! r = x - xs; %! f = 0.5 * r' * A * r; %! g = A * r; %!endfunction %!function [f, g] = __badgrad__ (x) %! f = sum (x.^2); %! g = 1; %!endfunction ## Every Moré, Garbow and Hillstrom problem below has a zero minimum, so the ## loss test is switched off and convergence is judged by the gradient. Left ## on, it stops the iteration first; that is the point of the next test. ## Problem 1, the standard start. %!test %! opt = struct ("LossTolerance", -Inf); %! [x, info] = __lbfgs__ (@__rosen__, [-1.2; 1], opt); %! crit = info.Criterion; %! assert_equal (x, [1; 1], 1e-5); %! assert_equal (info.Fval < 1e-8, true); %! assert_equal (crit, "gradient"); ## LossTolerance tests the objective VALUE, not the change in it, so on a ## problem whose minimum is zero the default 1e-6 pre-empts the gradient test ## while the gradient is still six orders of magnitude above its own ## tolerance. Measured on MATLAB R2024a, where a LossTolerance of 0.5 against ## a first-iteration loss of 0.3551 stopped fitcnet at iteration 1. %!test %! [x, info] = __lbfgs__ (@__rosen__, [-1.2; 1]); %! crit = info.Criterion; %! assert_equal (crit, "loss"); %! assert_equal (info.Fval <= 1e-6, true); %! assert_equal (info.Gradient > 1e-6, true); ## Problem 21 at n = 20, where the parameters outnumber the stored curvature ## pairs and the limited-memory recursion is what is actually being tested. %!test %! n = 20; %! x0 = zeros (n, 1); %! x0(1:2:n-1) = -1.2; %! x0(2:2:n) = 1; %! opt = struct ("LossTolerance", -Inf); %! [x, info] = __lbfgs__ (@__exrosen__, x0, opt); %! assert_equal (x, ones (n, 1), 1e-4); ## Problem 5. %!test %! opt = struct ("LossTolerance", -Inf); %! [x, info] = __lbfgs__ (@__beale__, [1; 1], opt); %! assert_equal (x, [3; 0.5], 1e-4); ## Problem 14. %!test %! opt = struct ("LossTolerance", -Inf); %! [x, info] = __lbfgs__ (@__wood__, [-3; -1; -3; -1], opt); %! assert_equal (x, ones (4, 1), 1e-4); ## Problem 13. The Hessian is singular at the solution, so convergence is ## linear and the objective is the only thing worth asserting on. %!test %! opt = struct ("LossTolerance", -Inf); %! [x, info] = __lbfgs__ (@__powellsq__, [3; -1; 0; 1], opt); %! assert_equal (info.Fval < 1e-8, true); ## A quadratic in fifty variables, well past the ten curvature pairs the ## history holds, against its known minimiser. StepTolerance is switched off ## along with the loss test: the steps go below 1e-6 while the gradient is ## still far from its own tolerance, and stopping there is correct but is not ## what this test is measuring. %!test %! n = 50; %! xs = ((1:n)' / n).^2; %! opt = struct ("GradientTolerance", 1e-10, "LossTolerance", -Inf, ... %! "StepTolerance", 0); %! [x, info] = __lbfgs__ (@__quadtri__, zeros (n, 1), opt); %! crit = info.Criterion; %! assert_equal (x, xs, 1e-6); %! assert_equal (crit, "gradient"); ## A single stored pair still converges, it just takes longer. %!test %! opt = struct ("HistorySize", 1, "LossTolerance", -Inf); %! [x, info] = __lbfgs__ (@__rosen__, [-1.2; 1], opt); %! assert_equal (x, [1; 1], 1e-4); ## X0 may be a row, X never is. %!test %! opt = struct ("LossTolerance", -Inf); %! [x, info] = __lbfgs__ (@__rosen__, [-1.2, 1], opt); %! assert_equal (x, [1; 1], 1e-5); ## An empty InitialStepSize asks for the automatic one, as it does in MATLAB. %!test %! opt = struct ("InitialStepSize", 1e-3, "LossTolerance", -Inf); %! [x1, i1] = __lbfgs__ (@__rosen__, [-1.2; 1], opt); %! opt = struct ("InitialStepSize", [], "LossTolerance", -Inf); %! [x2, i2] = __lbfgs__ (@__rosen__, [-1.2; 1], opt); %! assert_equal (x1, [1; 1], 1e-5); %! assert_equal (x2, [1; 1], 1e-5); ## A loss tolerance well above the starting value stops at once. %!test %! opt = struct ("LossTolerance", 1); %! [x, info] = __lbfgs__ (@__rosen__, [-1.2; 1], opt); %! crit = info.Criterion; %! assert_equal (crit, "loss"); %! assert_equal (info.Fval <= 1, true); ## Reported criterion when several are met at once: gradient outranks step. %!test %! opt = struct ("GradientTolerance", 1e3, "StepTolerance", 1e3); %! [x, info] = __lbfgs__ (@__rosen__, [-1.2; 1], opt); %! crit = info.Criterion; %! assert_equal (crit, "gradient"); %! assert_equal (info.Iterations, 1); ## And step outranks loss. %!test %! opt = struct ("StepTolerance", 1e3, "LossTolerance", 1e3); %! [x, info] = __lbfgs__ (@__rosen__, [-1.2; 1], opt); %! crit = info.Criterion; %! assert_equal (crit, "step"); %! assert_equal (info.Iterations, 1); ## The history holds one row per iteration taken, and the line search costs ## objective calls over and above them. %!test %! opt = struct ("IterationLimit", 4); %! [x, info] = __lbfgs__ (@__rosen__, [-1.2; 1], opt); %! crit = info.Criterion; %! assert_equal (crit, "iteration"); %! assert_equal (info.Iterations, 4); %! assert_equal (info.History.Iteration, (1:4)'); %! assert_equal (numel (info.History.Fval), 4); %! assert_equal (info.FuncCount >= info.Iterations, true); ## A zero limit evaluates the start point and stops there. %!test %! opt = struct ("IterationLimit", 0); %! [x, info] = __lbfgs__ (@__rosen__, [-1.2; 1], opt); %! assert_equal (x, [-1.2; 1]); %! assert_equal (info.Iterations, 0); %! assert_equal (isempty (info.History.Fval), true); ## BetaTolerance is off by default, and off means the quantity is not ## computed rather than tested against zero: MATLAB reports NaN for it, and ## so does this. %!test %! opt = struct ("LossTolerance", -Inf, "IterationLimit", 5); %! [x, info] = __lbfgs__ (@__rosen__, [-1.2; 1], opt); %! assert_equal (info.RelativeChangeInBeta, NaN); %! assert_equal (info.History.RelativeChangeInBeta, NaN (5, 1)); ## The relative change is the two-norm of the step over the two-norm of the ## point it landed on, unguarded. From a zero start the first iteration is ## therefore exactly 1, which is what R2024a's fitclinear reports and what ## rules out every max (1, .) guarded candidate. %!test %! n = 50; %! opt = struct ("LossTolerance", -Inf, "BetaTolerance", 1e-12, ... %! "IterationLimit", 4); %! [x, info] = __lbfgs__ (@__quadtri__, zeros (n, 1), opt); %! assert_equal (info.History.RelativeChangeInBeta(1), 1); ## It stops on the relative change, and reports which test did it. %!test %! opt = struct ("LossTolerance", -Inf, "BetaTolerance", 1e3); %! [x, info] = __lbfgs__ (@__rosen__, [-1.2; 1], opt); %! assert_equal (info.Criterion, "beta"); %! assert_equal (info.Iterations, 1); ## It outranks the gradient, which outranks step, as fitclinear orders them. %!test %! opt = struct ("GradientTolerance", 1e3, "BetaTolerance", 1e3, ... %! "StepTolerance", 1e3); %! [x, info] = __lbfgs__ (@__rosen__, [-1.2; 1], opt); %! assert_equal (info.Criterion, "beta"); %!test %! opt = struct ("BetaTolerance", 1e3, "StepTolerance", 1e3, ... %! "LossTolerance", 1e3); %! [x, info] = __lbfgs__ (@__rosen__, [-1.2; 1], opt); %! assert_equal (info.Criterion, "beta"); %!error __lbfgs__ (@__rosen__) %!error <__lbfgs__: FCN must be a function handle.> ... %! __lbfgs__ (1, [1; 1]) %!error <__lbfgs__: X0 must be a real numeric vector.> ... %! __lbfgs__ (@__rosen__, {1}) %!error <__lbfgs__: X0 must be a real numeric vector.> ... %! __lbfgs__ (@__rosen__, ones (2, 2)) %!error <__lbfgs__: X0 must be a real numeric vector.> ... %! __lbfgs__ (@__rosen__, []) %!error <__lbfgs__: X0 must be a real numeric vector.> ... %! __lbfgs__ (@__rosen__, complex ([1; 1])) %!error <__lbfgs__: OPTIONS must be a scalar struct.> ... %! __lbfgs__ (@__rosen__, [1; 1], 5) %!error <__lbfgs__: 'IterationLimit' must be a nonnegative integer scalar.> ... %! __lbfgs__ (@__rosen__, [1; 1], struct ("IterationLimit", -1)) %!error <__lbfgs__: 'IterationLimit' must be a nonnegative integer scalar.> ... %! __lbfgs__ (@__rosen__, [1; 1], struct ("IterationLimit", 2.5)) %!error <__lbfgs__: 'GradientTolerance' must be a nonnegative scalar.> ... %! __lbfgs__ (@__rosen__, [1; 1], struct ("GradientTolerance", -1)) %!error <__lbfgs__: 'StepTolerance' must be a nonnegative scalar.> ... %! __lbfgs__ (@__rosen__, [1; 1], struct ("StepTolerance", -1)) %!error <__lbfgs__: 'LossTolerance' must be a real scalar.> ... %! __lbfgs__ (@__rosen__, [1; 1], struct ("LossTolerance", NaN)) %!error <__lbfgs__: 'BetaTolerance' must be a nonnegative scalar.> ... %! __lbfgs__ (@__rosen__, [1; 1], struct ("BetaTolerance", -1)) %!error <__lbfgs__: 'HistorySize' must be a positive integer scalar.> ... %! __lbfgs__ (@__rosen__, [1; 1], struct ("HistorySize", 0)) %!test %! ## Full BFGS and a weak Wolfe search reach the minimum of a quadratic. %! A = [4, 1; 1, 3]; %! fcn = @(x) deal (0.5 * x' * A * x - [1; 2]' * x, A * x - [1; 2]); %! opt = struct ("Method", "bfgs", "LineSearch", "weakwolfe", ... %! "LossTolerance", -Inf, "GradientTolerance", 1e-10); %! [x, info] = __lbfgs__ (fcn, [0; 0], opt); %! assert_equal (x, A \ [1; 2], 1e-8); %! assert_equal (info.Criterion, 'gradient'); %!error <__lbfgs__: 'Method' must be 'lbfgs' or 'bfgs'.> ... %! __lbfgs__ (@__rosen__, [1; 1], struct ("Method", "newton")) %!error <__lbfgs__: 'LineSearch' must be 'strongwolfe' or 'weakwolfe'.> ... %! __lbfgs__ (@__rosen__, [1; 1], struct ("LineSearch", "exact")) %!error <__lbfgs__: 'InitialStepSize' must be a positive scalar.> ... %! __lbfgs__ (@__rosen__, [1; 1], struct ("InitialStepSize", -1)) %!error <__lbfgs__: 'Bogus' is not a valid option.> ... %! __lbfgs__ (@__rosen__, [1; 1], struct ("Bogus", 1)) %!error <__lbfgs__: FCN must return a real gradient with as many elements as X0.> ... %! __lbfgs__ (@__badgrad__, [1; 1]) */ statistics-release-2.1.2/src/__nomdist__.cc000066400000000000000000001333141527311104300206750ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include // Dissimilarity measures for nominal data, after the R package nomclust // 2.8.1 (Sulc and Rezankova), whose formulas and order of arithmetic are // followed. Level frequencies come from a reference sample R, so every // per-level quantity is computed once and each pair costs O(K). // // Levels are codes 1..L per variable, shared by X, Y and R. A level of Y // that R does not hold has a count of 0; each measure answers it with the // limit of its formula as that count falls to 0, and returns NaN where no // such limit exists (IOF, and OF and Burnaby against a level filling R). // The callers refuse those cases before they get here. static const double Inf = std::numeric_limits::infinity (); static const double NaN = std::numeric_limits::quiet_NaN (); enum MeasureCode { M_SM = 0, M_ESKIN, M_ANDERBERG, M_BURNABY, M_GAMBARYAN, M_GOODALL1, M_GOODALL2, M_GOODALL3, M_GOODALL4, M_IOF, M_LIN, M_LIN1, M_OF, M_SMIRNOV, M_VE, M_VM }; // What the reference sample says about each level. A level is addressed by // its flat index, off[k] plus its zero-based code in variable k. struct Stats { double r; octave_idx_type K; std::vector off; std::vector f; std::vector nlev; std::vector w; double W; double sumlev; // Per level: the match term, and a second quantity some measures need std::vector m; std::vector q; // Per variable: a constant some measures need std::vector c; // Lin1: each level's range of positions among its variable's levels // sorted by count, and the running sum of counts over them std::vector first; std::vector last; std::vector> P; }; // The levels of variable k that R holds, in increasing order of count. static std::vector sortedLevels (const Stats& S, octave_idx_type k) { std::vector idx; for (octave_idx_type v = S.off[k]; v < S.off[k+1]; v++) { if (S.f[v] > 0) { idx.push_back (v); } } std::stable_sort (idx.begin (), idx.end (), [&S] (octave_idx_type a, octave_idx_type b) { return S.f[a] < S.f[b]; }); return idx; } // Every measure accumulates two sums over the variables of a pair and // finishes them into a dissimilarity. X is the side that R covers. struct SM { const Stats& S; inline void add (octave_idx_type k, octave_idx_type x, octave_idx_type y, double& a, double&) const { if (x == y) { a += S.w[k]; } } inline double finish (double a, double) const { return 1.0 - (1.0 / S.W * a); } }; struct Eskin { const Stats& S; inline void add (octave_idx_type k, octave_idx_type x, octave_idx_type y, double& a, double&) const { a += (x == y) ? S.w[k] : S.w[k] * S.c[k]; } inline double finish (double a, double) const { return (1.0 / (1.0 / S.W * a)) - 1.0; } }; struct Anderberg { const Stats& S; inline void add (octave_idx_type k, octave_idx_type x, octave_idx_type y, double& a, double& b) const { const double px = S.f[x] / S.r; const double py = S.f[y] / S.r; if (x == y) { a += (1.0 / px) * (1.0 / px) * S.c[k]; } else if (py == 0) { b = Inf; } else { b += 1.0 / 2.0 / px / py * S.c[k]; } } inline double finish (double a, double b) const { return 1.0 - (a / (a + b)); } }; struct Burnaby { const Stats& S; inline void add (octave_idx_type k, octave_idx_type x, octave_idx_type y, double& a, double&) const { if (x == y) { a += S.w[k]; return; } const double px = S.f[x] / S.r; const double py = S.f[y] / S.r; if (py == 0) { a += (px == 1) ? NaN : 0.0; return; } const double N = S.c[k]; a += S.w[k] * (N / (std::log (px * py / (1.0 - px) / (1.0 - py)) + N)); } inline double finish (double a, double) const { return 1.0 - (a / S.W); } }; // Match terms only, over a divisor fixed for the measure. struct MatchOnly { const Stats& S; double div; bool weighted; inline void add (octave_idx_type k, octave_idx_type x, octave_idx_type y, double& a, double&) const { if (x == y) { a += weighted ? S.w[k] * S.m[x] : S.m[x]; } } inline double finish (double a, double) const { return 1.0 - (1.0 / div * a); } }; struct IOF { const Stats& S; inline void add (octave_idx_type k, octave_idx_type x, octave_idx_type y, double& a, double&) const { if (x == y) { a += S.w[k]; } else if (S.f[y] == 0) { a += NaN; } else { a += S.w[k] / (1.0 + S.m[x] * S.m[y]); } } inline double finish (double a, double) const { return (1.0 / (1.0 / S.W * a)) - 1.0; } }; struct OF { const Stats& S; inline void add (octave_idx_type k, octave_idx_type x, octave_idx_type y, double& a, double&) const { if (x == y) { a += S.w[k]; } else if (S.f[y] == 0) { a += (S.f[x] == S.r) ? NaN : 0.0; } else { a += S.w[k] / (1.0 + S.m[x] * S.m[y]); } } inline double finish (double a, double) const { if (a == 0) { return Inf; } return (1.0 / (1.0 / S.W * a)) - 1.0; } }; // Lin and Lin1 divide the shared information by the total, which an unseen // level makes infinite, so the pair is infinitely far apart. Frequencies // are summed as counts, so a sum covering every level is exactly 1 and its // log exactly 0, where nomclust's rounding leaves about 1e16. A total of 0 // needs every variable to hold one level in R, and the limit there is that // of identical rows. struct Lin { const Stats& S; inline void add (octave_idx_type k, octave_idx_type x, octave_idx_type y, double& a, double& b) const { if (S.f[y] == 0) { b = -Inf; return; } const double px = S.f[x] / S.r; const double py = S.f[y] / S.r; if (x == y) { a += S.w[k] * (2.0 * std::log (px)); } else { a += S.w[k] * (2.0 * std::log ((S.f[x] + S.f[y]) / S.r)); } b += (std::log (px) + std::log (py)) * S.w[k]; } inline double finish (double a, double b) const { if (std::isinf (b)) { return Inf; } if (b == 0) { return 0.0; } const double s = 1.0 / b * a; return (s == 0) ? Inf : (1.0 / s) - 1.0; } }; struct Lin1 { const Stats& S; inline void add (octave_idx_type k, octave_idx_type x, octave_idx_type y, double& a, double& b) const { if (S.f[y] == 0) { b = -Inf; return; } const double px = S.f[x] / S.r; const double py = S.f[y] / S.r; if (x == y) { a += S.w[k] * std::log (S.m[x]); } else { octave_idx_type lo = (S.f[x] <= S.f[y]) ? x : y; octave_idx_type hi = (S.f[x] <= S.f[y]) ? y : x; const std::vector& P = S.P[k]; a += S.w[k] * (2.0 * std::log ((P[S.last[hi] + 1] - P[S.first[lo]]) / S.r)); } b += (std::log (px) + std::log (py)) * S.w[k]; } // With every variable holding one level, each match term is half its // weight in the limit, so identical rows sit at 1 as they do elsewhere. inline double finish (double a, double b) const { if (std::isinf (b)) { return Inf; } if (b == 0) { return 1.0; } const double s = 1.0 / b * a; return (s == 0) ? Inf : (1.0 / s) - 1.0; } }; struct Smirnov { const Stats& S; inline void add (octave_idx_type k, octave_idx_type x, octave_idx_type y, double& a, double&) const { if (x == y) { a += 2.0 + (S.r - S.f[x]) / S.f[x] + (S.c[k] - S.q[x]); } else { a += std::max (0.0, S.c[k] - S.q[x] - S.q[y]); } } inline double finish (double a, double) const { return 1.0 / (1.0 / S.sumlev * a + 1.0); } }; // The per-level and per-variable quantities each measure reads. static void prepare (Stats& S, int measure) { const octave_idx_type K = S.K; const octave_idx_type nl = S.off[K]; const double r = S.r; S.m.assign (nl, 0.0); S.q.assign (nl, 0.0); S.c.assign (K, 0.0); // The chance of drawing a level twice without replacement; a reference of // one row cannot draw twice auto p2 = [r] (double f) { return (r > 1) ? f * (f - 1.0) / r / (r - 1.0) : 0.0; }; for (octave_idx_type k = 0; k < K; k++) { const double n = S.nlev[k]; switch (measure) { case M_ESKIN: { S.c[k] = (n * n) / ((n * n) + 2.0); break; } case M_ANDERBERG: { S.c[k] = 2.0 / n / (n + 1.0); break; } case M_BURNABY: { double N = 0.0; for (octave_idx_type v = S.off[k]; v < S.off[k+1]; v++) { N = N + (2.0 * std::log (1.0 - S.f[v] / r)); } S.c[k] = N; break; } case M_GAMBARYAN: { // A level held by every row has entropy 0, not 0 * log2 (0) for (octave_idx_type v = S.off[k]; v < S.off[k+1]; v++) { const double p = S.f[v] / r; if (p > 0 && p < 1) { S.m[v] = - (p * std::log2 (p) + (1.0 - p) * std::log2 (1.0 - p)); } } break; } case M_GOODALL1: case M_GOODALL2: { // Levels at most (Goodall 1) or at least (Goodall 2) as frequent, // as a running sum over the levels sorted by count std::vector idx = sortedLevels (S, k); const std::size_t L = idx.size (); std::vector T (L + 1, 0.0); for (std::size_t i = 0; i < L; i++) { T[i+1] = T[i] + p2 (S.f[idx[i]]); } std::size_t i = 0; while (i < L) { std::size_t j = i; while (j < L && S.f[idx[j]] == S.f[idx[i]]) { j++; } const double s = (measure == M_GOODALL1) ? T[j] : T[L] - T[i]; for (std::size_t u = i; u < j; u++) { S.m[idx[u]] = 1.0 * (1.0 - s); } i = j; } break; } case M_GOODALL3: case M_GOODALL4: { for (octave_idx_type v = S.off[k]; v < S.off[k+1]; v++) { S.m[v] = (measure == M_GOODALL3) ? 1.0 - p2 (S.f[v]) : p2 (S.f[v]); } break; } case M_IOF: { for (octave_idx_type v = S.off[k]; v < S.off[k+1]; v++) { S.m[v] = std::log (S.f[v]); } break; } case M_OF: { for (octave_idx_type v = S.off[k]; v < S.off[k+1]; v++) { S.m[v] = std::log (r / S.f[v]); } break; } case M_LIN1: { std::vector idx = sortedLevels (S, k); const std::size_t L = idx.size (); std::vector P (L + 1, 0.0); for (std::size_t i = 0; i < L; i++) { P[i+1] = P[i] + S.f[idx[i]]; } std::size_t i = 0; while (i < L) { std::size_t j = i; double t = 0.0; while (j < L && S.f[idx[j]] == S.f[idx[i]]) { t = t + S.f[idx[j]]; j++; } for (std::size_t u = i; u < j; u++) { S.first[idx[u]] = i; S.last[idx[u]] = j - 1; S.m[idx[u]] = t / r; } i = j; } S.P[k] = P; break; } case M_SMIRNOV: { // A level held by every row is the only one, so never among the // others any sum here runs over double T = 0.0; for (octave_idx_type v = S.off[k]; v < S.off[k+1]; v++) { if (S.f[v] < r) { S.q[v] = S.f[v] / (r - S.f[v]); } T = T + S.q[v]; } S.c[k] = T; break; } case M_VE: { double e = 0.0; for (octave_idx_type v = S.off[k]; v < S.off[k+1]; v++) { const double p = S.f[v] / r; if (p > 0) { e = e + p * std::log (p); } } const double ne = (n > 1) ? - e / std::log (n) : 0.0; for (octave_idx_type v = S.off[k]; v < S.off[k+1]; v++) { S.m[v] = ne; } break; } case M_VM: { double g = 0.0; for (octave_idx_type v = S.off[k]; v < S.off[k+1]; v++) { const double p = S.f[v] / r; g = g + p * p; } const double ng = (n > 1) ? (1.0 - g) * n / (n - 1.0) : 0.0; for (octave_idx_type v = S.off[k]; v < S.off[k+1]; v++) { S.m[v] = ng; } break; } default: break; } } } // Every pair within X in pdist order, or every row of X against every row // of Y. Codes arrive as flat level indices, one row contiguous. template static void pairs (const F& fn, const std::vector& xc, const std::vector& yc, octave_idx_type n, octave_idx_type m, octave_idx_type K, bool within, const std::vector& act, double *out) { std::size_t at = 0; for (octave_idx_type i = 0; i < n; i++) { octave_quit (); const octave_idx_type *xp = &xc[(std::size_t) i * K]; const octave_idx_type j0 = within ? i + 1 : 0; const octave_idx_type j1 = within ? n : m; for (octave_idx_type j = j0; j < j1; j++) { const octave_idx_type *yp = within ? &xc[(std::size_t) j * K] : &yc[(std::size_t) j * K]; double a = 0.0; double b = 0.0; for (octave_idx_type k : act) { fn.add (k, xp[k], yp[k], a, b); } if (within) { out[at++] = fn.finish (a, b); } else { out[i + j * n] = fn.finish (a, b); } } } } // Codes as flat level indices, one row contiguous, checked on the way. static std::vector pack (const Matrix& A, const Stats& S) { const octave_idx_type n = A.rows (); const octave_idx_type K = S.K; std::vector out ((std::size_t) n * K); for (octave_idx_type k = 0; k < K; k++) { for (octave_idx_type i = 0; i < n; i++) { const double v = A(i, k); out[(std::size_t) i * K + k] = S.off[k] + (octave_idx_type) v - 1; } } return out; } static bool validCodes (const Matrix& A) { const octave_idx_type N = A.numel (); for (octave_idx_type i = 0; i < N; i++) { const double v = A.xelem (i); if (! (v >= 1) || v != std::floor (v) || ! std::isfinite (v)) { return false; } } return true; } // The measure named, over the pairs pairs () visits. static void score (const Stats& S, int measure, const std::vector& xc, const std::vector& yc, octave_idx_type n, octave_idx_type m, octave_idx_type K, bool within, const std::vector& act, double *out) { switch (measure) { case M_SM: pairs (SM {S}, xc, yc, n, m, K, within, act, out); break; case M_ESKIN: pairs (Eskin {S}, xc, yc, n, m, K, within, act, out); break; case M_ANDERBERG: pairs (Anderberg {S}, xc, yc, n, m, K, within, act, out); break; case M_BURNABY: pairs (Burnaby {S}, xc, yc, n, m, K, within, act, out); break; case M_GAMBARYAN: pairs (MatchOnly {S, S.sumlev, false}, xc, yc, n, m, K, within, act, out); break; case M_GOODALL1: case M_GOODALL2: case M_GOODALL3: case M_GOODALL4: case M_VE: case M_VM: pairs (MatchOnly {S, S.W, true}, xc, yc, n, m, K, within, act, out); break; case M_IOF: pairs (IOF {S}, xc, yc, n, m, K, within, act, out); break; case M_LIN: pairs (Lin {S}, xc, yc, n, m, K, within, act, out); break; case M_LIN1: pairs (Lin1 {S}, xc, yc, n, m, K, within, act, out); break; case M_OF: pairs (OF {S}, xc, yc, n, m, K, within, act, out); break; case M_SMIRNOV: pairs (Smirnov {S}, xc, yc, n, m, K, within, act, out); break; } } DEFUN_DLD(__nomdist__, args, , "-*- texinfo -*-\n\ @deftypefn {statistics} {@var{D} =} __nomdist__ (@var{X}, [], @var{R}, @var{measure}, @var{w})\n\ @deftypefnx {statistics} {@var{D} =} __nomdist__ (@var{X}, @var{Y}, @var{R}, @var{measure}, @var{w})\n\ @deftypefnx {statistics} {@var{D} =} __nomdist__ (@var{X}, @var{Y}, @var{R}, @var{measure}, @var{w}, @var{addq})\n\ \n\ Dissimilarity measures for nominal data. Internal; called by @code{nomdist}\n\ and @code{nomdist2} and not meant to be used directly.\n\ \n\ @var{X}, @var{Y} and @var{R} hold level codes, positive integers shared\n\ across the three, one variable per column. Level frequencies are counted\n\ over @var{R}, which must hold every level @var{X} holds, or over @var{X}\n\ itself where @var{R} is empty. @var{measure} is one of\n\ @qcode{'anderberg'}, @qcode{'burnaby'}, @qcode{'eskin'},\n\ @qcode{'gambaryan'}, @qcode{'goodall1'}, @qcode{'goodall2'},\n\ @qcode{'goodall3'}, @qcode{'goodall4'}, @qcode{'iof'}, @qcode{'lin'},\n\ @qcode{'lin1'}, @qcode{'of'}, @qcode{'sm'}, @qcode{'smirnov'},\n\ @qcode{'ve'} or @qcode{'vm'}, and @var{w} holds one weight per variable,\n\ read by every measure but @qcode{'anderberg'}, @qcode{'gambaryan'} and\n\ @qcode{'smirnov'}.\n\ \n\ With @var{Y} empty, @var{D} is a row vector over every pair of rows of\n\ @var{X}, in the order @code{pdist} returns them. Otherwise @var{D} is\n\ the @math{N*M} matrix of each row of @var{X} against each row of @var{Y}.\n\ Where @var{addq} is true, each row of @var{Y} is counted into @var{R}\n\ while it is measured, so no level of @var{Y} is ever absent from it.\n\ \n\ @end deftypefn") { if (args.length () < 5 || args.length () > 6) { print_usage (); } for (int i = 0; i < 3; i++) { if (! args(i).isnumeric () || args(i).iscomplex () || args(i).ndims () != 2) { error ("__nomdist__: X, Y and R must be real numeric matrices."); } } const Matrix X = args(0).matrix_value (); const Matrix Y = args(1).matrix_value (); const Matrix R = args(2).isempty () ? X : args(2).matrix_value (); const bool within = Y.isempty (); const octave_idx_type K = X.columns (); if (R.columns () != K || (! within && Y.columns () != K)) { error ("__nomdist__: X, Y and R must have the same number of columns."); } if (! validCodes (X) || ! validCodes (Y) || ! validCodes (R)) { error ("__nomdist__: X, Y and R must hold positive integer codes."); } if (! args(3).is_string ()) { error ("__nomdist__: MEASURE must be a character vector."); } if (! args(4).isnumeric () || args(4).numel () != K) { error ("__nomdist__: W must hold one weight per variable."); } bool addq = false; if (args.length () == 6) { if (! (args(5).islogical () || args(5).isnumeric ()) || ! args(5).is_scalar_type ()) { error ("__nomdist__: ADDQ must be a logical scalar."); } addq = args(5).bool_value (); } if (addq && within) { error ("__nomdist__: ADDQ needs a nonempty Y."); } static const char *names[] = {"sm", "eskin", "anderberg", "burnaby", "gambaryan", "goodall1", "goodall2", "goodall3", "goodall4", "iof", "lin", "lin1", "of", "smirnov", "ve", "vm"}; const std::string name = args(3).string_value (); int measure = -1; for (int i = 0; i < 16; i++) { if (name == names[i]) { measure = i; } } if (measure < 0) { error ("__nomdist__: unsupported MEASURE '%s'.", name.c_str ()); } // Levels per variable, over all three sets, and their counts in R Stats S; S.K = K; S.r = (double) R.rows (); S.off.assign (K + 1, 0); for (octave_idx_type k = 0; k < K; k++) { double L = 0; for (octave_idx_type i = 0; i < X.rows (); i++) { L = std::max (L, X(i, k)); } for (octave_idx_type i = 0; i < Y.rows (); i++) { L = std::max (L, Y(i, k)); } for (octave_idx_type i = 0; i < R.rows (); i++) { L = std::max (L, R(i, k)); } S.off[k+1] = S.off[k] + (octave_idx_type) L; } S.f.assign (S.off[K], 0.0); S.nlev.assign (K, 0.0); for (octave_idx_type k = 0; k < K; k++) { for (octave_idx_type i = 0; i < R.rows (); i++) { S.f[S.off[k] + (octave_idx_type) R(i, k) - 1] += 1.0; } for (octave_idx_type v = S.off[k]; v < S.off[k+1]; v++) { S.nlev[k] += (S.f[v] > 0) ? 1.0 : 0.0; } } S.sumlev = 0.0; for (octave_idx_type k = 0; k < K; k++) { S.sumlev += S.nlev[k]; } const NDArray wv = args(4).array_value (); S.w.assign (K, 1.0); const bool weighted = (measure != M_ANDERBERG && measure != M_GAMBARYAN && measure != M_SMIRNOV); if (weighted) { for (octave_idx_type k = 0; k < K; k++) { S.w[k] = wv(k); } } S.W = 0.0; for (octave_idx_type k = 0; k < K; k++) { S.W += S.w[k]; } // A variable of weight 0 adds nothing, and skipping it keeps an unseen // level there from reaching a log std::vector act; for (octave_idx_type k = 0; k < K; k++) { if (S.w[k] != 0) { act.push_back (k); } } const std::vector xc = pack (X, S); const std::vector yc = pack (Y, S); for (octave_idx_type v : xc) { if (S.f[v] == 0) { error ("__nomdist__: R must hold every level X holds."); } } if (measure == M_LIN1) { S.first.assign (S.off[K], 0); S.last.assign (S.off[K], 0); S.P.assign (K, std::vector ()); } const octave_idx_type n = X.rows (); const octave_idx_type m = Y.rows (); NDArray D; if (within) { D = NDArray (dim_vector (1, n > 1 ? n * (n - 1) / 2 : 0)); } else { D = NDArray (dim_vector (n, m)); } double *out = D.fortran_vec (); if (! addq) { prepare (S, measure); score (S, measure, xc, yc, n, m, K, within, act, out); return ovl (D); } // Each row of Y counted into the reference while it is measured: its // levels gain a count, R a row, and every per-level quantity is taken // again, since each depends on all the counts for (octave_idx_type j = 0; j < m; j++) { octave_quit (); Stats T = S; const std::size_t at = (std::size_t) j * K; const std::vector yj (yc.begin () + at, yc.begin () + at + K); for (octave_idx_type k = 0; k < K; k++) { if (T.f[yj[k]] == 0) { T.nlev[k] += 1.0; T.sumlev += 1.0; } T.f[yj[k]] += 1.0; } T.r += 1.0; prepare (T, measure); score (T, measure, xc, yj, n, 1, K, false, act, out + (std::size_t) j * n); } return ovl (D); } /* %!shared X, W, Q %! X = [1, 1, 1; 1, 2, 1; 1, 1, 2; 2, 2, 2; 2, 1, 1; 3, 2, 2; 4, 1, 3]; %! W = [0.7, 1, 0.4]; %! Q = [1, 2, 2; 5, 1, 3]; ## Expectations from nomclust 2.8.1 on the same data %!test %! D = [0.3333333333333334, 0.3333333333333334, 1, 0.3333333333333334, 1, ... %! 0.6666666666666667, 0.6666666666666667, 0.6666666666666667, ... %! 0.6666666666666667, 0.6666666666666667, 1, 0.6666666666666667, ... %! 0.6666666666666667, 0.6666666666666667, 0.6666666666666667, ... %! 0.6666666666666667, 0.3333333333333334, 1, 1, 0.6666666666666667, ... %! 1]; %! assert_equal (__nomdist__ (X, [], X, 'sm', ones (1, 3)), D, -1e-14); %!test %! D = [0.125, 0.06451612903225801, 0.2638297872340427, 0.03846153846153855, ... %! 0.2638297872340427, 0.1082089552238805, 0.2073170731707317, ... %! 0.1082089552238805, 0.173913043478261, 0.1082089552238805, ... %! 0.2638297872340427, 0.173913043478261, 0.1082089552238805, ... %! 0.173913043478261, 0.1082089552238805, 0.2073170731707317, ... %! 0.03846153846153855, 0.2638297872340427, 0.2638297872340427, ... %! 0.1082089552238805, 0.2638297872340427]; %! assert_equal (__nomdist__ (X, [], X, 'eskin', ones (1, 3)), D, -1e-14); %!test %! D = [0.3191489361702128, 0.2247191011235955, 1, 0.174757281553398, 1, ... %! 0.6808510638297873, 0.6756756756756757, 0.3220338983050848, ... %! 0.5454545454545454, 0.4117647058823529, 1, 0.5454545454545454, ... %! 0.4578313253012049, 0.6226415094339623, 0.6808510638297873, ... %! 0.4807692307692308, 0.3103448275862069, 1, 1, 0.7169811320754718, ... %! 1]; %! assert_equal (__nomdist__ (X, [], X, 'anderberg', ones (1, 3)), D, -1e-14); %!test %! D = [0, 0.06142861791239795, 0.1725141049787992, 0.1110854870664012, ... %! 0.2158655957496493, 0.3042675674408121, 0.06142861791239795, ... %! 0.1725141049787992, 0.1110854870664012, 0.2158655957496493, ... %! 0.3042675674408121, 0.1110854870664012, 0.1725141049787992, ... %! 0.1544369778372513, 0.3042675674408121, 0.06142861791239795, ... %! 0.1764146125938005, 0.3262452021973615, 0.2378432305061988, ... %! 0.3262452021973615, 0.3491708712867553]; %! assert_equal (__nomdist__ (X, [], X, 'burnaby', ones (1, 3)), D, -1e-14); %!test %! D = [0.7810604142146108, 0.7810604142146108, 1, 0.7810604142146108, 1, ... %! 0.8905302071073053, 0.8905302071073053, 0.8905302071073053, ... %! 0.8905302071073053, 0.8905302071073053, 1, 0.8905302071073053, ... %! 0.8905302071073053, 0.8905302071073053, 0.8905302071073053, ... %! 0.9040977146037077, 0.7810604142146108, 1, 1, 0.8905302071073053, ... %! 1]; %! assert_equal (__nomdist__ (X, [], X, 'gambaryan', ones (1, 3)), D, -1e-14); %!test %! D = [0.4920634920634921, 0.5396825396825398, 1, 0.5714285714285715, 1, ... %! 0.8095238095238095, 0.7301587301587302, 0.7142857142857143, ... %! 0.7619047619047619, 0.7142857142857143, 1, 0.7619047619047619, ... %! 0.8095238095238095, 0.7619047619047619, 0.8095238095238095, ... %! 0.6825396825396826, 0.4761904761904762, 1, 1, 0.8095238095238095, ... %! 1]; %! assert_equal (__nomdist__ (X, [], X, 'goodall1', ones (1, 3)), D, -1e-14); %!test %! D = [0.4761904761904762, 0.4761904761904762, 1, 0.5238095238095238, 1, ... %! 0.7619047619047619, 0.7142857142857143, 0.8095238095238095, ... %! 0.7619047619047619, 0.8095238095238095, 1, 0.7619047619047619, ... %! 0.7619047619047619, 0.7619047619047619, 0.7619047619047619, ... %! 0.7301587301587302, 0.5714285714285715, 1, 1, 0.7619047619047619, ... %! 1]; %! assert_equal (__nomdist__ (X, [], X, 'goodall2', ones (1, 3)), D, -1e-14); %!test %! D = [0.4285714285714286, 0.4761904761904762, 1, 0.4761904761904762, 1, ... %! 0.7619047619047619, 0.7142857142857143, 0.7142857142857143, ... %! 0.7142857142857143, 0.7142857142857143, 1, 0.7142857142857143, ... %! 0.7619047619047619, 0.7142857142857143, 0.7619047619047619, ... %! 0.6825396825396826, 0.4285714285714286, 1, 1, 0.7619047619047619, ... %! 1]; %! assert_equal (__nomdist__ (X, [], X, 'goodall3', ones (1, 3)), D, -1e-14); %!test %! D = [0.9047619047619048, 0.8571428571428572, 1, 0.8571428571428572, 1, ... %! 0.9047619047619048, 0.9523809523809523, 0.9523809523809523, ... %! 0.9523809523809523, 0.9523809523809523, 1, 0.9523809523809523, ... %! 0.9047619047619048, 0.9523809523809523, 0.9047619047619048, ... %! 0.9841269841269842, 0.9047619047619048, 1, 1, 0.9047619047619048, ... %! 1]; %! assert_equal (__nomdist__ (X, [], X, 'goodall4', ones (1, 3)), D, -1e-14); %!test %! D = [0.2519020857326395, 0.222935300643843, 1.116901264683761, ... %! 0.1683617083596181, 0.6220882713463793, 0, 0.6220882713463793, ... %! 0.4845515922241048, 0.5274548356775992, 0.222935300643843, ... %! 0.2519020857326395, 0.5274548356775992, 0.4845515922241048, ... %! 0.2519020857326395, 0, 0.6220882713463793, 0, 0.2519020857326395, ... %! 0.6220882713463793, 0, 0.2519020857326395]; %! assert_equal (__nomdist__ (X, [], X, 'iof', ones (1, 3)), D, -1e-14); %!test %! D = [0.4151177862291795, 0.4440221764476788, 4.300985770938718, ... %! 0.4092944562546224, 3.129303942378162, 0.9970986461666451, ... %! 1.394583893868549, 1.051411550470893, 1.197035645318646, ... %! 0.9801872797639546, 2.124165640951619, 1.197035645318646, ... %! 1.33941488968952, 1.094910865911154, 0.9970986461666451, ... %! 0.9926721560959628, 0.2958576645228652, 1.629441680424714, ... %! 2.145534809656081, 0.8080361706650088, 1.23240918245247]; %! assert_equal (__nomdist__ (X, [], X, 'lin', ones (1, 3)), D, -1e-14); %!test %! ## Pair 11 shares nothing, so its similarity is exactly 0, where %! ## nomclust's rounding returns about 1.6e16. %! D = [3.789167787737098, 1.628488554759508, 4.300985770938718, ... %! 2.543556180472745, 18.11998502097161, 10.98259187699986, ... %! 3.150318732214346, 2.001980368635569, 5.288962253828324, ... %! 4.34995121471969, Inf, 5.288962253828324, 2.189052189432408, ... %! 37.23997004194317, 10.98259187699986, 2.591802852053521, ... %! 2.106486692231752, 5.61060225142327, 3.413335993771041, ... %! 3.235711272013311, 2.229638071755347]; %! assert_equal (__nomdist__ (X, [], X, 'lin1', ones (1, 3)), D, -1e-14); %!test %! D = [0.1200918256837458, 0.1618443665773319, 0.7186603816400106, ... %! 0.2071986251911722, 0.8315156613470343, 0.7093347701634469, ... %! 0.3271674504213862, 0.4512427967395944, 0.3866778238431001, ... %! 0.5308967156237416, 1.092895882914944, 0.3866778238431001, ... %! 0.4512427967395944, 0.4592247360215442, 0.7093347701634469, ... %! 0.3271674504213862, 0.3095365880953216, 1.227546987265509, ... %! 0.933812054699924, 0.7981072047001967, 1.371908574090516]; %! assert_equal (__nomdist__ (X, [], X, 'of', ones (1, 3)), D, -1e-14); %!test %! D = [0.519730510105871, 0.5378486055776893, 0.9473684210526315, ... %! 0.526829268292683, 0.9246575342465754, 0.6513872135102533, ... %! 0.6801007556675063, 0.6352941176470589, 0.6625766871165645, 0.625, ... %! 0.8723747980613893, 0.6625766871165645, 0.6923076923076923, ... %! 0.6513872135102533, 0.6513872135102533, 0.6101694915254238, ... %! 0.4778761061946903, 0.84375, 0.8925619834710745, 0.6352941176470589, ... %! 0.8256880733944955]; %! assert_equal (__nomdist__ (X, [], X, 'smirnov', ones (1, 3)), D, -1e-14); %!test %! D = [0.3882378704642936, 0.3645287891257314, 1, 0.3668903239823945, 1, ... %! 0.6715906213219162, 0.6929381678038152, 0.6715906213219162, ... %! 0.6952997026604784, 0.6715906213219162, 1, 0.6952997026604784, ... %! 0.6715906213219162, 0.6952997026604784, 0.6715906213219162, ... %! 0.6929381678038152, 0.3668903239823945, 1, 1, 0.6715906213219162, ... %! 1]; %! assert_equal (__nomdist__ (X, [], X, 've', ones (1, 3)), D, -1e-14); %!test %! D = [0.3854875283446713, 0.365079365079365, 1, 0.3673469387755102, 1, ... %! 0.673469387755102, 0.691609977324263, 0.673469387755102, ... %! 0.6938775510204082, 0.673469387755102, 1, 0.6938775510204082, ... %! 0.673469387755102, 0.6938775510204082, 0.673469387755102, ... %! 0.691609977324263, 0.3673469387755102, 1, 1, 0.673469387755102, 1]; %! assert_equal (__nomdist__ (X, [], X, 'vm', ones (1, 3)), D, -1e-14); %!test %! D = [0.4761904761904762, 0.1904761904761906, 1, 0.3333333333333334, 1, ... %! 0.5238095238095238, 0.6666666666666667, 0.5238095238095238, ... %! 0.8095238095238095, 0.5238095238095238, 1, 0.8095238095238095, ... %! 0.5238095238095238, 0.8095238095238095, 0.5238095238095238, ... %! 0.6666666666666667, 0.3333333333333334, 1, 1, 0.5238095238095238, ... %! 1]; %! assert_equal (__nomdist__ (X, [], X, 'sm', W), D, -1e-14); %!test %! D = [0.188679245283019, 0.03587443946188351, 0.2993750000000002, ... %! 0.03846153846153855, 0.2993750000000002, 0.07720207253886002, ... %! 0.2397137745974953, 0.07720207253886002, 0.2434210526315792, ... %! 0.07720207253886002, 0.2993750000000002, 0.2434210526315792, ... %! 0.07720207253886002, 0.2434210526315792, 0.07720207253886002, ... %! 0.2397137745974953, 0.03846153846153855, 0.2993750000000002, ... %! 0.2993750000000002, 0.07720207253886002, 0.2993750000000002]; %! assert_equal (__nomdist__ (X, [], X, 'eskin', W), D, -1e-14); %!test %! D = [0, 0.03510206737851329, 0.1461875544449145, ... %! 0.1110854870664013, 0.1895390452157646, 0.2400544576107146, ... %! 0.03510206737851329, 0.1461875544449145, 0.1110854870664013, ... %! 0.1895390452157645, 0.2400544576107146, 0.1110854870664013, ... %! 0.1461875544449145, 0.1544369778372515, 0.2400544576107146, ... %! 0.03510206737851329, 0.1764146125938008, 0.262032092367264, ... %! 0.2115166799723139, 0.262032092367264, 0.2849577614566577]; %! assert_equal (__nomdist__ (X, [], X, 'burnaby', W), D, 1e-15); %!test %! D = [0.5941043083900226, 0.4580498866213153, 1, 0.5918367346938775, 1, ... %! 0.7278911564625851, 0.7301587301587302, 0.5918367346938775, ... %! 0.8639455782312925, 0.5918367346938775, 1, 0.8639455782312925, ... %! 0.7278911564625851, 0.8639455782312925, 0.7278911564625851, ... %! 0.6825396825396826, 0.45578231292517, 1, 1, 0.7278911564625851, 1]; %! assert_equal (__nomdist__ (X, [], X, 'goodall1', W), D, -1e-14); %!test %! D = [0.5782312925170068, 0.3741496598639457, 1, 0.5238095238095238, 1, ... %! 0.6598639455782314, 0.7142857142857143, 0.7278911564625851, ... %! 0.8639455782312925, 0.7278911564625851, 1, 0.8639455782312925, ... %! 0.6598639455782314, 0.8639455782312925, 0.6598639455782314, ... %! 0.7301587301587302, 0.5918367346938775, 1, 1, 0.6598639455782314, ... %! 1]; %! assert_equal (__nomdist__ (X, [], X, 'goodall2', W), D, -1e-14); %!test %! D = [0.5510204081632654, 0.3741496598639457, 1, 0.4965986394557823, 1, ... %! 0.6598639455782314, 0.7142857142857143, 0.5918367346938775, ... %! 0.8367346938775511, 0.5918367346938775, 1, 0.8367346938775511, ... %! 0.6598639455782314, 0.8367346938775511, 0.6598639455782314, ... %! 0.6825396825396826, 0.4285714285714285, 1, 1, 0.6598639455782314, ... %! 1]; %! assert_equal (__nomdist__ (X, [], X, 'goodall3', W), D, -1e-14); %!test %! D = [0.9251700680272109, 0.8163265306122449, 1, 0.8367346938775511, 1, ... %! 0.8639455782312926, 0.9523809523809524, 0.9319727891156463, ... %! 0.9727891156462585, 0.9319727891156463, 1, 0.9727891156462585, ... %! 0.8639455782312926, 0.9727891156462585, 0.8639455782312926, ... %! 0.9841269841269842, 0.9047619047619048, 1, 1, 0.8639455782312926, ... %! 1]; %! assert_equal (__nomdist__ (X, [], X, 'goodall4', W), D, -1e-14); %!test %! D = [0.4034116524755296, 0.1162816237598046, 1.153873045642792, ... %! 0.1683617083596181, 0.6437079284563265, 0, 0.6437079284563265, ... %! 0.3302637748244865, 0.7591738998286246, 0.1162816237598046, ... %! 0.4034116524755296, 0.7591738998286246, 0.3302637748244865, ... %! 0.4034116524755296, 0, 0.6437079284563265, 0, 0.4034116524755296, ... %! 0.6437079284563265, 0, 0.4034116524755296]; %! assert_equal (__nomdist__ (X, [], X, 'iof', W), D, -1e-14); %!test %! D = [0.7547596113257813, 0.2283122503928992, 4.98065967640019, ... %! 0.4404425718584952, 3.455309722495803, 0.7834346505595069, ... %! 1.497804622067723, 0.678686996286928, 2.094086789730795, ... %! 0.663610211168179, 2.638407372343877, 2.094086789730795, ... %! 0.9065634806963383, 1.764659180935495, 0.7834346505595069, ... %! 1.044877050999849, 0.2958576645228654, 1.915257938793264, ... %! 2.301793922738517, 0.6256289159595232, 1.383977355327267]; %! assert_equal (__nomdist__ (X, [], X, 'lin', W), D, -1e-14); %!test %! ## Pair 11 shares nothing, so its similarity is exactly 0, where %! ## nomclust's rounding returns about 1.6e16. %! D = [3.995609244135445, 1.337916967393231, 4.98065967640019, ... %! 1.990937661841057, 31.76013401116178, 6.490425532349926, ... %! 3.565656513831422, 1.66527788918046, 5.672895993089274, ... %! 3.458682832253068, Inf, 5.672895993089274, 1.831126193046863, ... %! 64.52026802232339, 6.490425532349926, 2.837642944938641, ... %! 1.72479913371176, 5.079768268917555, 3.768312597687203, ... %! 2.332335370699156, 1.992511747898871]; %! assert_equal (__nomdist__ (X, [], X, 'lin1', W), D, -1e-14); %!test %! D = [0.1808686870497029, 0.08648381587311493, 0.6789831874074297, ... %! 0.2071986251911724, 0.7865248082245031, 0.4837971469738953, ... %! 0.3033825055232831, 0.3355336337184947, 0.4810463690758979, ... %! 0.4026982054804156, 0.9201937519825203, 0.4810463690758979, ... %! 0.3355336337184947, 0.5640991370603945, 0.4837971469738953, ... %! 0.3033825055232831, 0.3095365880953216, 1.032940807336712, ... %! 0.8837237854764428, 0.5502337057652003, 1.152503424548615]; %! assert_equal (__nomdist__ (X, [], X, 'of', W), D, -1e-14); %!test %! D = [0.5188237121812314, 0.2237819125494098, 1, 0.3567292891230108, 1, ... %! 0.5308437447455945, 0.6929381678038152, 0.5308437447455945, ... %! 0.8258855443774162, 0.5308437447455945, 1, 0.8258855443774162, ... %! 0.5308437447455945, 0.8258855443774162, 0.5308437447455945, ... %! 0.6929381678038152, 0.3567292891230108, 1, 1, 0.5308437447455945, ... %! 1]; %! assert_equal (__nomdist__ (X, [], X, 've', W), D, -1e-14); %!test %! D = [0.5166828636216392, 0.2251376741172659, 1, 0.358600583090379, 1, ... %! 0.5335276967930028, 0.691609977324263, 0.5335276967930028, ... %! 0.8250728862973761, 0.5335276967930028, 1, 0.8250728862973761, ... %! 0.5335276967930028, 0.8250728862973761, 0.5335276967930028, ... %! 0.691609977324263, 0.358600583090379, 1, 1, 0.5335276967930028, 1]; %! assert_equal (__nomdist__ (X, [], X, 'vm', W), D, -1e-14); %!test %! ## Row against row matches the pairs, bit for bit %! D = squareform (__nomdist__ (X, [], X, 'goodall3', ones (1, 3))); %! D2 = __nomdist__ (X, X, X, 'goodall3', ones (1, 3)); %! assert_equal (D2(! eye (7)), D(! eye (7))); %!test %! ## Identical rows need not be at 0: 1 - (3 - 24/42) / 3 %! D2 = __nomdist__ (X, X, X, 'goodall3', ones (1, 3)); %! assert_equal (D2(1,1), 4 / 21, -1e-14); %!assert_equal (__nomdist__ ([1; 2], [1], [1; 2], 'goodall3', 1), [0; 1]) %!assert_equal (__nomdist__ ([1; 2], [1], [], 'goodall3', 1), [0; 1]) %!assert_equal (__nomdist__ (X, [], [], 'lin1', W), ... %! __nomdist__ (X, [], X, 'lin1', W)) %!assert_equal (__nomdist__ ([1; 2], [1], [1; 1; 1; 2], 'goodall3', 1), ... %! [0.5; 1]) %!assert_equal (__nomdist__ ([1 1 1], [], [1 1 1], 'sm', ones (1, 3)), ... %! zeros (1, 0)) %!assert_equal (__nomdist__ (X, [], X, 'sm', [1, 0, 1]), ... %! __nomdist__ (X(:,[1, 3]), [], X(:,[1, 3]), 'sm', [1, 1])) ## A level of Y that R does not hold %!assert_equal (__nomdist__ ([1, 1; 2, 1; 2, 2], [3, 1; 3, 2], ... %! [1, 1; 2, 1; 2, 2], 'goodall3', [1, 1]), ... %! [2/3, 1; 2/3, 1; 1, 0.5], -1e-14) %!test %! Z = [1, 1; 2, 1; 2, 2]; %! s = 1 / (1 + log (3) * log (3 / 2)); %! D = __nomdist__ (Z, [3, 1; 3, 2], Z, 'of', [1, 1]); %! assert_equal (D, [1, 2/s-1; 1, 2/s-1; 2/s-1, 1], -1e-14); %!assert_equal (__nomdist__ ([1, 1; 2, 1; 2, 2], [3, 1; 3, 2], ... %! [1, 1; 2, 1; 2, 2], 'burnaby', [1, 1]), ... %! 0.5 * ones (3, 2)) %!assert_equal (__nomdist__ ([1, 1; 2, 1; 2, 2], [3, 1; 3, 2], ... %! [1, 1; 2, 1; 2, 2], 'anderberg', [1, 1]), ... %! ones (3, 2)) %!assert_equal (__nomdist__ ([1, 1; 2, 1; 2, 2], [3, 1; 3, 2], ... %! [1, 1; 2, 1; 2, 2], 'lin', [1, 1]), ... %! Inf (3, 2)) %!assert_equal (__nomdist__ ([1, 1; 2, 1; 2, 2], [3, 1; 3, 2], ... %! [1, 1; 2, 1; 2, 2], 'lin1', [1, 1]), ... %! Inf (3, 2)) %!assert_equal (__nomdist__ ([1, 1; 2, 1; 2, 2], [3, 1; 3, 2], ... %! [1, 1; 2, 1; 2, 2], 'lin', [0, 1]), ... %! [0, Inf; 0, Inf; Inf, 0]) %!assert_equal (__nomdist__ ([1, 1; 2, 1; 2, 2], [3, 1; 3, 2], ... %! [1, 1; 2, 1; 2, 2], 'iof', [1, 1]), NaN (3, 2)) %!assert_equal (__nomdist__ ([1; 1], 2, [1; 1], 'of', 1), [NaN; NaN]) %!assert_equal (__nomdist__ ([1; 1], 2, [1; 1], 'burnaby', 1), [NaN; NaN]) ## Limits where nomclust returns NaN or a substitute %!assert_equal (__nomdist__ ([1; 2], [], [1; 2], 'lin', 1), Inf) %!assert_equal (__nomdist__ ([1, 1; 1, 1], [], [1, 1; 1, 1], 'lin', [1, 1]), 0) %!assert_equal (__nomdist__ ([1, 1; 1, 1], [], [1, 1; 1, 1], 'lin1', ... %! [1, 1]), 1) %!test %! h = - (2/3 * log2 (2/3) + 1/3 * log2 (1/3)); %! D = __nomdist__ ([1, 1; 1, 2; 1, 1], [], [1, 1; 1, 2; 1, 1], ... %! 'gambaryan', [1, 1]); %! assert_equal (D, [1, 1 - h / 3, 1], -1e-14); %!assert_equal (__nomdist__ (1, 1, 1, 'goodall3', 1), 0) %!assert_equal (__nomdist__ (1, 1, 1, 'goodall4', 1), 1) ## Each row of Y counted into R equals R with that row appended %!test %! E = [__nomdist__(X, Q(1,:), [X; Q(1,:)], 'sm', W), ... %! __nomdist__(X, Q(2,:), [X; Q(2,:)], 'sm', W)]; %! assert_equal (__nomdist__ (X, Q, [], 'sm', W, true), E); %!test %! E = [__nomdist__(X, Q(1,:), [X; Q(1,:)], 'eskin', W), ... %! __nomdist__(X, Q(2,:), [X; Q(2,:)], 'eskin', W)]; %! assert_equal (__nomdist__ (X, Q, [], 'eskin', W, true), E); %!test %! E = [__nomdist__(X, Q(1,:), [X; Q(1,:)], 'anderberg', ones (1, 3)), ... %! __nomdist__(X, Q(2,:), [X; Q(2,:)], 'anderberg', ones (1, 3))]; %! assert_equal (__nomdist__ (X, Q, [], 'anderberg', ones (1, 3), true), E); %!test %! E = [__nomdist__(X, Q(1,:), [X; Q(1,:)], 'burnaby', W), ... %! __nomdist__(X, Q(2,:), [X; Q(2,:)], 'burnaby', W)]; %! assert_equal (__nomdist__ (X, Q, [], 'burnaby', W, true), E); %!test %! E = [__nomdist__(X, Q(1,:), [X; Q(1,:)], 'gambaryan', ones (1, 3)), ... %! __nomdist__(X, Q(2,:), [X; Q(2,:)], 'gambaryan', ones (1, 3))]; %! assert_equal (__nomdist__ (X, Q, [], 'gambaryan', ones (1, 3), true), E); %!test %! E = [__nomdist__(X, Q(1,:), [X; Q(1,:)], 'goodall1', W), ... %! __nomdist__(X, Q(2,:), [X; Q(2,:)], 'goodall1', W)]; %! assert_equal (__nomdist__ (X, Q, [], 'goodall1', W, true), E); %!test %! E = [__nomdist__(X, Q(1,:), [X; Q(1,:)], 'goodall2', W), ... %! __nomdist__(X, Q(2,:), [X; Q(2,:)], 'goodall2', W)]; %! assert_equal (__nomdist__ (X, Q, [], 'goodall2', W, true), E); %!test %! E = [__nomdist__(X, Q(1,:), [X; Q(1,:)], 'goodall3', W), ... %! __nomdist__(X, Q(2,:), [X; Q(2,:)], 'goodall3', W)]; %! assert_equal (__nomdist__ (X, Q, [], 'goodall3', W, true), E); %!test %! E = [__nomdist__(X, Q(1,:), [X; Q(1,:)], 'goodall4', W), ... %! __nomdist__(X, Q(2,:), [X; Q(2,:)], 'goodall4', W)]; %! assert_equal (__nomdist__ (X, Q, [], 'goodall4', W, true), E); %!test %! E = [__nomdist__(X, Q(1,:), [X; Q(1,:)], 'iof', W), ... %! __nomdist__(X, Q(2,:), [X; Q(2,:)], 'iof', W)]; %! assert_equal (__nomdist__ (X, Q, [], 'iof', W, true), E); %!test %! E = [__nomdist__(X, Q(1,:), [X; Q(1,:)], 'lin', W), ... %! __nomdist__(X, Q(2,:), [X; Q(2,:)], 'lin', W)]; %! assert_equal (__nomdist__ (X, Q, [], 'lin', W, true), E); %!test %! E = [__nomdist__(X, Q(1,:), [X; Q(1,:)], 'lin1', W), ... %! __nomdist__(X, Q(2,:), [X; Q(2,:)], 'lin1', W)]; %! assert_equal (__nomdist__ (X, Q, [], 'lin1', W, true), E); %!test %! E = [__nomdist__(X, Q(1,:), [X; Q(1,:)], 'of', W), ... %! __nomdist__(X, Q(2,:), [X; Q(2,:)], 'of', W)]; %! assert_equal (__nomdist__ (X, Q, [], 'of', W, true), E); %!test %! E = [__nomdist__(X, Q(1,:), [X; Q(1,:)], 'smirnov', ones (1, 3)), ... %! __nomdist__(X, Q(2,:), [X; Q(2,:)], 'smirnov', ones (1, 3))]; %! assert_equal (__nomdist__ (X, Q, [], 'smirnov', ones (1, 3), true), E); %!test %! E = [__nomdist__(X, Q(1,:), [X; Q(1,:)], 've', W), ... %! __nomdist__(X, Q(2,:), [X; Q(2,:)], 've', W)]; %! assert_equal (__nomdist__ (X, Q, [], 've', W, true), E); %!test %! E = [__nomdist__(X, Q(1,:), [X; Q(1,:)], 'vm', W), ... %! __nomdist__(X, Q(2,:), [X; Q(2,:)], 'vm', W)]; %! assert_equal (__nomdist__ (X, Q, [], 'vm', W, true), E); %!test %! R = [X; 2, 2, 1; 1, 1, 1]; %! E = [__nomdist__(X, Q(1,:), [R; Q(1,:)], 'lin1', W), ... %! __nomdist__(X, Q(2,:), [R; Q(2,:)], 'lin1', W)]; %! assert_equal (__nomdist__ (X, Q, R, 'lin1', W, true), E); %!assert_equal (__nomdist__ (X, X, [], 'of', W, false), ... %! __nomdist__ (X, X, [], 'of', W)) %!error __nomdist__ (1, [], 1, 'sm') %!error<__nomdist__: X, Y and R must be real numeric matrices.> ... %! __nomdist__ ({1}, [], 1, 'sm', 1) %!error<__nomdist__: X, Y and R must have the same number of columns.> ... %! __nomdist__ ([1, 1], [], 1, 'sm', [1, 1]) %!error<__nomdist__: X, Y and R must hold positive integer codes.> ... %! __nomdist__ (1.5, [], 1, 'sm', 1) %!error<__nomdist__: MEASURE must be a character vector.> ... %! __nomdist__ (1, [], 1, 1, 1) %!error<__nomdist__: W must hold one weight per variable.> ... %! __nomdist__ (1, [], 1, 'sm', [1, 1]) %!error<__nomdist__: unsupported MEASURE 'morlini'.> ... %! __nomdist__ (1, [], 1, 'morlini', 1) %!error<__nomdist__: R must hold every level X holds.> ... %! __nomdist__ (2, [], 1, 'sm', 1) %!error<__nomdist__: ADDQ must be a logical scalar.> ... %! __nomdist__ (1, 1, [], 'sm', 1, [true, true]) %!error<__nomdist__: ADDQ needs a nonempty Y.> ... %! __nomdist__ (1, [], [], 'sm', 1, true) */ statistics-release-2.1.2/src/__shapleytree__.cc000066400000000000000000000374511527311104300215520ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include // One demand a leaf makes: the value of predictor VAR must reach the child // the path took. A cut on a threshold sends a value below it left, and a cut // on levels sends the levels it lists either way and anything else neither, // which is what a missing value does too. struct Demand { octave_idx_type var; bool left; bool categorical; double cut; std::vector levels; // the levels of the side the path took }; // A leaf: what it answers, and everything the way to it demands, gathered by // predictor so that a predictor cut more than once counts as one. struct Leaf { octave_idx_type node; std::vector vars; // distinct, ascending std::vector > demands; // one list per entry of vars }; // A tree as this needs it: the node table, and what it answers at each node. struct Tree { Matrix children; ColumnVector cutvar; ColumnVector cutpoint; Cell catleft; Cell catright; Matrix leafval; bool hascat; std::vector leaves; }; static bool demand_met (const Demand& d, double v) { if (d.categorical) { for (size_t i = 0; i < d.levels.size (); i++) if (v == d.levels[i]) return true; return false; } if (octave::math::isnan (v)) return false; return d.left ? (v < d.cut) : (v >= d.cut); } // Walk the tree once and record every leaf with the demands on the way to it. static void collect_leaves (Tree& T) { const octave_idx_type nn = T.children.rows (); if (nn <= 0) return; std::vector stack_node; std::vector > stack_path; stack_node.push_back (0); stack_path.push_back (std::vector ()); while (! stack_node.empty ()) { const octave_idx_type n = stack_node.back (); std::vector path = stack_path.back (); stack_node.pop_back (); stack_path.pop_back (); const octave_idx_type var = static_cast (T.cutvar(n)); if (var == 0) { Leaf lf; lf.node = n; // Gather the demands by predictor, keeping the predictors ascending for (size_t i = 0; i < path.size (); i++) { size_t k = 0; while (k < lf.vars.size () && lf.vars[k] < path[i].var) k++; if (k == lf.vars.size () || lf.vars[k] != path[i].var) { lf.vars.insert (lf.vars.begin () + k, path[i].var); lf.demands.insert (lf.demands.begin () + k, std::vector ()); } lf.demands[k].push_back (path[i]); } T.leaves.push_back (lf); continue; } for (int kid = 0; kid < 2; kid++) { const octave_idx_type child = static_cast (T.children(n, kid)) - 1; if (child < 0 || child >= nn) continue; Demand d; d.var = var - 1; d.left = (kid == 0); d.categorical = false; d.cut = T.cutpoint(n); if (T.hascat) { const octave_value cv = (kid == 0 ? T.catleft(n) : T.catright(n)); if (! cv.isempty ()) { d.categorical = true; const NDArray lv = cv.array_value (); for (octave_idx_type j = 0; j < lv.numel (); j++) d.levels.push_back (lv(j)); } } std::vector next = path; next.push_back (d); stack_node.push_back (child); stack_path.push_back (next); } } } // The sum, over every way of adding T of the M-A-B predictors a leaf does not // demand, of the weight the Shapley definition gives a subset of that size. // Taken in logs, so that neither the binomial coefficient nor the factorials // overflow however wide the model is. static double kernel_sum (octave_idx_type k, octave_idx_type m, octave_idx_type M, const std::vector& lg) { double tot = 0.0; for (octave_idx_type t = 0; t <= m; t++) { const octave_idx_type s = k + t; const double lt = lg[m] - lg[t] - lg[m - t] + lg[s] + lg[M - s - 1] - lg[M]; tot += std::exp (lt); } return tot; } // The Shapley values of one tree at one query point, added into PHI. // // A leaf answers for the rows meeting every demand on the way to it. Hold // the predictors of a subset at the query point and leave the rest as an // observation has them, and that row reaches the leaf exactly when every // predictor the query fails is outside the subset and every predictor the // observation fails is inside it. Writing A for the predictors the query // passes and the observation fails and B for the other way about, the leaf is // reached exactly for the subsets holding all of A and none of B, so only // those predictors can move a value, and by how much turns on nothing but how // many there are. static void tree_values (const Tree& T, const Matrix& X, const RowVector& q, octave_idx_type M, octave_idx_type K, const std::vector& lg, Matrix& phi) { const octave_idx_type n = X.rows (); std::vector passX; std::vector passZ; std::vector acount; std::vector keep; for (size_t li = 0; li < T.leaves.size (); li++) { const Leaf& lf = T.leaves[li]; const size_t nf = lf.vars.size (); if (nf == 0) continue; // Which of the leaf's predictors the query passes passX.assign (nf, 1); for (size_t j = 0; j < nf; j++) for (size_t d = 0; d < lf.demands[j].size (); d++) if (! demand_met (lf.demands[j][d], q(lf.vars[j]))) { passX[j] = 0; break; } // And which each observation passes passZ.assign (static_cast (n) * nf, 1); for (size_t j = 0; j < nf; j++) { const octave_idx_type v = lf.vars[j]; for (octave_idx_type i = 0; i < n; i++) { char ok = 1; for (size_t d = 0; d < lf.demands[j].size (); d++) if (! demand_met (lf.demands[j][d], X(i, v))) { ok = 0; break; } passZ[j * static_cast (n) + i] = ok; } } // An observation failing a predictor the query fails too puts the leaf // out of reach whatever the subset is octave_idx_type b = 0; for (size_t j = 0; j < nf; j++) if (! passX[j]) b++; keep.assign (n, 1); for (size_t j = 0; j < nf; j++) if (! passX[j]) for (octave_idx_type i = 0; i < n; i++) if (! passZ[j * static_cast (n) + i]) keep[i] = 0; acount.assign (n, 0); octave_idx_type nkeep = 0; octave_idx_type amax = 0; for (octave_idx_type i = 0; i < n; i++) { if (! keep[i]) continue; nkeep++; octave_idx_type a = 0; for (size_t j = 0; j < nf; j++) if (passX[j] && ! passZ[j * static_cast (n) + i]) a++; acount[i] = a; if (a > amax) amax = a; } if (nkeep == 0) continue; // The weight turns on an observation only through how many predictors // fall in A, so it is worked out once for each count that occurs std::vector gA (amax + 1, 0.0), gB (amax + 1, 0.0); std::vector nat (amax + 1, 0); for (octave_idx_type a = 0; a <= amax; a++) { const octave_idx_type mv = M - a - b; if (mv < 0) continue; gB[a] = kernel_sum (a, mv, M, lg); if (a >= 1) gA[a] = kernel_sum (a - 1, mv, M, lg); } for (octave_idx_type i = 0; i < n; i++) if (keep[i]) nat[acount[i]]++; for (octave_idx_type kk = 0; kk < K; kk++) { const double vL = T.leafval(lf.node, kk); if (vL == 0.0) continue; if (b > 0) { double wsum = 0.0; for (octave_idx_type a = 0; a <= amax; a++) wsum += static_cast (nat[a]) * gB[a]; for (size_t j = 0; j < nf; j++) if (! passX[j]) phi(lf.vars[j], kk) -= wsum * vL; } for (size_t j = 0; j < nf; j++) { if (! passX[j]) continue; double wsum = 0.0; for (octave_idx_type i = 0; i < n; i++) if (keep[i] && ! passZ[j * static_cast (n) + i]) wsum += gA[acount[i]]; phi(lf.vars[j], kk) += wsum * vL; } } } } DEFUN_DLD (__shapleytree__, args, , "-*- texinfo -*-\n\ @deftypefn {statistics} {@var{phi} =} \ __shapleytree__ (@var{trees}, @var{X}, @var{Q})\n\ \n\ Shapley values of an ensemble of decision trees, taken leaf by leaf.\n\ Internal; called by @code{shapley} and not meant to be used directly.\n\ \n\ @var{trees} is a cell array of structures, one per tree, each holding\n\ @qcode{Children}, the @math{Nx2} table of child node numbers and zero on a\n\ leaf; @qcode{CutVar}, the predictor each node cuts on and zero on a leaf;\n\ @qcode{CutPoint}, the value it cuts at; @qcode{CatLeft} and @qcode{CatRight},\n\ the levels each side of a cut on a categorical predictor takes and empty\n\ where the cut is on a threshold; and @qcode{Leaf}, what the tree answers at\n\ each node, one column per class and one column for a response, already\n\ carrying whatever weight the model gives the tree.\n\ \n\ @var{X} holds the observations averaged over, one per row, and @var{Q} the\n\ query points, one per row. Both have one column per predictor.\n\ \n\ @var{phi} is @math{MxKxnq}: one row per predictor, one column per column of\n\ @qcode{Leaf}, and one page per query point. The values of a query point sum\n\ to the deviation of the prediction from the average prediction.\n\ \n\ @end deftypefn") { if (args.length () != 3) print_usage (); if (! args(0).iscell ()) error ("__shapleytree__: TREES must be a cell array of structures."); if (! args(1).isnumeric () || args(1).iscomplex () || args(1).isempty ()) error ("__shapleytree__: X must be a real numeric matrix."); if (! args(2).isnumeric () || args(2).iscomplex () || args(2).isempty ()) error ("__shapleytree__: Q must be a real numeric matrix."); const Cell tc = args(0).cell_value (); const Matrix X = args(1).matrix_value (); const Matrix Q = args(2).matrix_value (); const octave_idx_type M = X.columns (); const octave_idx_type n = X.rows (); const octave_idx_type nq = Q.rows (); if (Q.columns () != M) error ("__shapleytree__: X and Q must have the same number of columns."); if (tc.numel () == 0) error ("__shapleytree__: TREES must hold at least one tree."); std::vector trees (tc.numel ()); octave_idx_type K = -1; for (octave_idx_type t = 0; t < tc.numel (); t++) { if (! tc(t).isstruct ()) error ("__shapleytree__: TREES must be a cell array of structures."); const octave_scalar_map sm = tc(t).scalar_map_value (); Tree& T = trees[t]; T.children = sm.contents ("Children").matrix_value (); T.cutvar = sm.contents ("CutVar").column_vector_value (); T.cutpoint = sm.contents ("CutPoint").column_vector_value (); T.leafval = sm.contents ("Leaf").matrix_value (); T.hascat = sm.isfield ("CatLeft") && ! sm.contents ("CatLeft").isempty (); if (T.hascat) { T.catleft = sm.contents ("CatLeft").cell_value (); T.catright = sm.contents ("CatRight").cell_value (); } const octave_idx_type nn = T.children.rows (); if (T.cutvar.numel () != nn || T.cutpoint.numel () != nn || T.leafval.rows () != nn) error ("__shapleytree__: tree %d is not described consistently.", static_cast (t + 1)); if (K < 0) K = T.leafval.columns (); else if (T.leafval.columns () != K) error ("__shapleytree__: every tree must answer with as many columns."); collect_leaves (T); } // log-gamma of 1 .. M+1, so that lg[i] is log (i!) std::vector lg (M + 2, 0.0); for (octave_idx_type i = 0; i <= M + 1; i++) lg[i] = std::lgamma (static_cast (i) + 1.0); dim_vector dv (M, K, nq); NDArray out (dv, 0.0); const double invn = 1.0 / static_cast (n); for (octave_idx_type p = 0; p < nq; p++) { Matrix phi (M, K, 0.0); RowVector q (M); for (octave_idx_type j = 0; j < M; j++) q(j) = Q(p, j); for (size_t t = 0; t < trees.size (); t++) tree_values (trees[t], X, q, M, K, lg, phi); for (octave_idx_type i = 0; i < M; i++) for (octave_idx_type k = 0; k < K; k++) out(i, k, p) = phi(i, k) * invn; } return ovl (out); } /* %!error __shapleytree__ () %!error __shapleytree__ (1, 2, 3) %!error __shapleytree__ ({1}, {}, 3) %!error __shapleytree__ ({1}, [1 2], {}) %!error __shapleytree__ ({}, [1 2], [1 2]) %!error __shapleytree__ ({1}, [1 2], [1 2 3]) ## A stump on the first of two predictors: holding it at the query point ## moves the answer by the whole deviation and the other predictor by nothing %!test %! T.Children = [2, 3; 0, 0; 0, 0]; %! T.CutVar = [1; 0; 0]; %! T.CutPoint = [0.5; NaN; NaN]; %! T.Leaf = [0; 10; 20]; %! X = [0, 7; 0, 8; 1, 9; 1, 6]; %! phi = __shapleytree__ ({T}, X, [1, 7]); %! assert (size (phi), [2, 1]); %! assert (phi(1), 5, 1e-12); %! assert (phi(2), 0, 1e-12); ## Two stumps added together are the sum of what each gives on its own %!test %! T1.Children = [2, 3; 0, 0; 0, 0]; %! T1.CutVar = [1; 0; 0]; %! T1.CutPoint = [0.5; NaN; NaN]; %! T1.Leaf = [0; 10; 20]; %! T2.Children = [2, 3; 0, 0; 0, 0]; %! T2.CutVar = [2; 0; 0]; %! T2.CutPoint = [7.5; NaN; NaN]; %! T2.Leaf = [0; 1; 3]; %! X = [0, 7; 0, 8; 1, 9; 1, 6]; %! a = __shapleytree__ ({T1}, X, [1, 7]); %! b = __shapleytree__ ({T2}, X, [1, 7]); %! both = __shapleytree__ ({T1, T2}, X, [1, 7]); %! assert (both, a + b, 1e-12); ## A cut on the levels of a categorical predictor sends the levels it lists %!test %! T.Children = [2, 3; 0, 0; 0, 0]; %! T.CutVar = [1; 0; 0]; %! T.CutPoint = [NaN; NaN; NaN]; %! T.CatLeft = {[1, 2]; []; []}; %! T.CatRight = {[3]; []; []}; %! T.Leaf = [0; 4; 10]; %! X = [1; 2; 3; 3]; %! phi = __shapleytree__ ({T}, X, 3); %! assert (phi, 3, 1e-12); ## One page of values per query point %!test %! T.Children = [2, 3; 0, 0; 0, 0]; %! T.CutVar = [1; 0; 0]; %! T.CutPoint = [0.5; NaN; NaN]; %! T.Leaf = [0; 10; 20]; %! X = [0, 7; 0, 8; 1, 9; 1, 6]; %! phi = __shapleytree__ ({T}, X, [1, 7; 0, 7]); %! assert (size (phi), [2, 1, 2]); %! assert (phi(1,1,1), 5, 1e-12); %! assert (phi(1,1,2), -5, 1e-12); */ statistics-release-2.1.2/src/__treeprune__.cc000066400000000000000000000457131527311104300212360ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include // How close two risks must be to count as equal. Links equal in exact // arithmetic differ in their last bits once they have been through a // division, and treating them as distinct would split one pruning level into // several. It is a tolerance on risks, not the one the split gains use. static const double RISK_TIE_TOL = 1e-12; // Cost complexity pruning, by weakest link. A branch node's link is the risk // it would take on as a leaf, less the risk its subtree carries now, spread // over the leaves the subtree would give up. The branch with the smallest // link is pruned, the sequence repeats on what is left, and the level a node // is pruned at is its place in that sequence. Leaves never carry a level, so // a tree of B branches gives B levels and B + 1 alphas, the first of which is // zero and stands for the unpruned tree. // // RISK is what each node would carry as a leaf and HELD what a branch carries // for the rows that stop there, on whatever scale the caller measures by. // Taking both rather than deriving them is what lets a tree be sequenced // after it was grown, and on a scale the engine knows nothing of: a caller // holding a cost matrix passes the risk that matrix defines. static void tree_prune_sequence (const Matrix& children, const ColumnVector& parent, const std::vector& risk, const std::vector& held, ColumnVector& prunelist, ColumnVector& prunealpha) { const octave_idx_type out = children.rows (); prunelist = ColumnVector (out, 0.0); prunealpha = ColumnVector (0, 0.0); if (out <= 0) return; std::vector kidl (out), kidr (out); for (octave_idx_type i = 0; i < out; i++) { kidl[i] = static_cast (children(i, 0)); kidr[i] = static_cast (children(i, 1)); } std::vector subrisk (out); std::vector subleaves (out); std::vector reach (out); // The branches given up at no cost, which are no part of the sequence std::vector freed (out, false); std::vector alphas; octave_idx_type level = 0; while (kidl[0] != 0) { // A pruned branch takes its whole subtree with it, so only what is // still reachable from the root can be a candidate. Left in, those // orphans go on offering links of their own and split one level // into several. std::fill (reach.begin (), reach.end (), false); reach[0] = true; for (octave_idx_type i = 0; i < out; i++) if (reach[i] && kidl[i] != 0) { reach[kidl[i]-1] = true; reach[kidr[i]-1] = true; } // Subtree risk and leaf count, deepest first. for (octave_idx_type i = out - 1; i >= 0; i--) { if (kidl[i] == 0) { subrisk[i] = risk[i]; subleaves[i] = 1; } else { subrisk[i] = subrisk[kidl[i]-1] + subrisk[kidr[i]-1] + held[i]; subleaves[i] = subleaves[kidl[i]-1] + subleaves[kidr[i]-1]; } } bool any = false; double weakest = 0.0; for (octave_idx_type i = 0; i < out; i++) { if (kidl[i] == 0 || ! reach[i]) continue; double link = (risk[i] - subrisk[i]) / (subleaves[i] - 1); if (! any || link < weakest) { weakest = link; any = true; } } if (! any) break; // A subtree that costs nothing to give up is no step of the // sequence. It is what merging leaves would have removed, and it // survives to be seen here only when the caller asked for a // sequence without a merge. Such a branch is given up all the // same, so that the branches above it are priced on what is left, // but it opens no level and takes no alpha: measured on R2024a, // where an unmerged iris tree of eleven nodes carries the merged // tree's five alphas rather than six. const double tol = RISK_TIE_TOL * std::max (std::fabs (risk[0]), 1.0); const bool free = (weakest <= tol); // Every branch whose link is the weakest goes at this level, not // just one of them, and links equal in exact arithmetic can differ // in their last bits here as split gains do. if (! free) { level++; alphas.push_back (weakest); } const double cut = weakest + std::fabs (weakest) * RISK_TIE_TOL + tol; for (octave_idx_type i = 0; i < out; i++) { if (kidl[i] == 0 || ! reach[i]) continue; if ((risk[i] - subrisk[i]) / (subleaves[i] - 1) <= cut) { if (free) freed[i] = true; else prunelist(i) = level; kidl[i] = kidr[i] = 0; } } } // A branch inside a subtree given up at no cost is no part of the // sequence either, its ancestor having left it at no level. A parent // always precedes its children, so one forward pass carries the mark // down. for (octave_idx_type i = 1; i < out; i++) { octave_idx_type a = static_cast (parent(i)); if (a > 0 && freed[a - 1]) freed[i] = true; } // A branch that lost an ancestor never came up for pruning on its own // account, but it stopped being a branch when that ancestor went, and // that is the level it carries. for (octave_idx_type i = 0; i < out; i++) { if (children(i, 0) == 0 || prunelist(i) != 0 || freed[i]) continue; octave_idx_type a = static_cast (parent(i)); while (a > 0 && prunelist(a - 1) == 0) a = static_cast (parent(a - 1)); if (a > 0) prunelist(i) = prunelist(a - 1); } prunealpha.resize (level + 1); prunealpha(0) = 0.0; for (octave_idx_type i = 0; i < level; i++) prunealpha(i + 1) = alphas[i]; } // The cost complexity pruning sequence of a tree that has already been grown. // // treetrain sequences the tree it grows, but a tree outlives its data: prune // collapses nodes and the tree that is left has a sequence of its own, and a // model read back from a file has no data at all. Taking the risk rather // than the observations is what serves both, and it is also what lets a // classifier measure risk on a cost matrix this engine knows nothing about. DEFUN_DLD (__treeprune__, args, , "-*- texinfo -*-\n\ @deftypefn {statistics} {[@var{PruneList}, @var{PruneAlpha}] =} \ __treeprune__ (@var{Children}, @var{Parent}, @var{risk}, @var{held})\n\ \n\ Cost complexity pruning sequence of a grown tree, by weakest link.\n\ Internal; called by @code{ClassificationTree} and @code{RegressionTree} and\n\ not meant to be used directly.\n\ \n\ A branch node's link is the risk it would take on as a leaf, less the risk\n\ its subtree carries now, spread over the leaves the subtree would give up.\n\ The weakest link is pruned, the sequence repeats on what is left, and the\n\ level a node is pruned at is its place in that sequence. Leaves never carry\n\ a level, so a tree of @math{B} branch nodes gives @math{B} levels at most and\n\ one more alpha than levels, the first of which is zero and stands for the\n\ unpruned tree.\n\ \n\ @var{Children} is the @math{Nx2} table of child node numbers, zero on a leaf,\n\ and @var{Parent} the node number each node hangs from, zero at the root.\n\ \n\ @var{risk} is the risk each node would carry as a leaf, on whatever scale the\n\ caller measures by: the expected misclassification cost for a classifier and\n\ the squared error about the node's mean for a regression. Nothing here\n\ interprets it, so a cost matrix enters through this argument alone.\n\ \n\ @var{held} is the risk a node carries on account of the observations that\n\ stop there, missing the predictor it cuts on. Those observations are in\n\ neither child, so a subtree's risk is its children's plus this, and leaving\n\ it out overstates every link above a node that holds rows back.\n\ \n\ A subtree that costs nothing to give up is no step of the sequence. It is\n\ what merging leaves would have removed, and MATLAB records neither a level\n\ nor an alpha for it.\n\ \n\ @end deftypefn") { if (args.length () != 4) print_usage (); if (! args(0).isnumeric () || args(0).iscomplex () || args(0).isempty () || args(0).columns () != 2) error ("__treeprune__: Children must be a non-empty N-by-2 real matrix."); const Matrix children = args(0).matrix_value (); const octave_idx_type n = children.rows (); if (! args(1).isnumeric () || args(1).iscomplex () || args(1).numel () != n) error ("__treeprune__: Parent must hold one node number per node."); if (! args(2).isnumeric () || args(2).iscomplex () || args(2).numel () != n) error ("__treeprune__: risk must hold one value per node."); if (! args(3).isnumeric () || args(3).iscomplex () || args(3).numel () != n) error ("__treeprune__: held must hold one value per node."); const ColumnVector parent = args(1).column_vector_value (); const ColumnVector riskv = args(2).column_vector_value (); const ColumnVector heldv = args(3).column_vector_value (); // A child number outside the table would index past the end of every // vector below, so it is refused here rather than trusted. for (octave_idx_type i = 0; i < n; i++) { const double l = children(i, 0); const double r = children(i, 1); if (l != std::floor (l) || r != std::floor (r) || l < 0 || r < 0 || l > n || r > n || (l == 0) != (r == 0)) error ("__treeprune__: Children must hold a node number in each row, \ or zero in both on a leaf."); if (parent(i) != std::floor (parent(i)) || parent(i) < 0 || parent(i) > n) error ("__treeprune__: Parent must hold a node number, or zero at \ the root."); } std::vector risk (n), held (n); for (octave_idx_type i = 0; i < n; i++) { risk[i] = riskv(i); held[i] = heldv(i); } ColumnVector prunelist, prunealpha; tree_prune_sequence (children, parent, risk, held, prunelist, prunealpha); return ovl (prunelist, prunealpha); } /* %!test %! ## The link of a root whose two leaves cost less than it does %! C = [2, 3; 0, 0; 0, 0]; %! P = [0; 1; 1]; %! [L, A] = __treeprune__ (C, P, [1; 0.4; 0.4], zeros (3, 1)); %! assert_equal (L', [1, 0, 0]); %! assert_equal (A', [0, 0.2], 1e-14); %!test %! ## What a node holds back is part of its subtree's risk %! ## The same tree, charged 0.1 for rows that reached neither child, gives %! ## a link of 0.1 where it gave 0.2. %! C = [2, 3; 0, 0; 0, 0]; %! P = [0; 1; 1]; %! [L, A] = __treeprune__ (C, P, [1; 0.4; 0.4], [0.1; 0; 0]); %! assert_equal (L', [1, 0, 0]); %! assert_equal (A', [0, 0.1], 1e-14); %!test %! ## A subtree costing nothing takes no level and no alpha %! C = [2, 3; 0, 0; 0, 0]; %! P = [0; 1; 1]; %! [L, A] = __treeprune__ (C, P, [0.8; 0.4; 0.4], zeros (3, 1)); %! assert_equal (L', [0, 0, 0]); %! assert_equal (A', 0); %!test %! ## Deeper branches go at their own level, weakest first %! C = [2, 3; 4, 5; 0, 0; 0, 0; 0, 0]; %! P = [0; 1; 1; 2; 2]; %! [L, A] = __treeprune__ (C, P, [1; 0.5; 0.3; 0.2; 0.2], zeros (5, 1)); %! assert_equal (L', [2, 1, 0, 0, 0]); %! assert_equal (A', [0, 0.1, 0.2], 1e-14); %!test %! ## A branch that lost an ancestor carries the ancestor's level %! ## Node 2 has the stronger link and never comes up on its own account, %! ## but it stopped being a branch when node 1 went. %! C = [2, 3; 4, 5; 0, 0; 0, 0; 0, 0]; %! P = [0; 1; 1; 2; 2]; %! [L, A] = __treeprune__ (C, P, [1; 0.9; 0.3; 0.25; 0.25], zeros (5, 1)); %! assert_equal (L', [1, 1, 0, 0, 0]); %! assert_equal (A', [0, 0.1], 1e-14); %!test %! ## The scale is the caller's and nothing here interprets it %! C = [2, 3; 0, 0; 0, 0]; %! P = [0; 1; 1]; %! [~, A] = __treeprune__ (C, P, [10; 4; 4], zeros (3, 1)); %! assert_equal (A', [0, 2], 1e-13); %!test %! ## A tree of one node has no branch to prune %! [L, A] = __treeprune__ ([0, 0], 0, 1, 0); %! assert_equal (L, 0); %! assert_equal (A, 0); ## The sequence of a tree treetrain grew, on the risk that engine measures ## by: misclassified weight for a classifier, squared error for a regression. ## A learner with a cost matrix builds its own risk and does not come here. %!function [L, A] = seq (T) %! if (isempty (T.ClassWeight)) %! risk = T.NodeWeight .* T.NodeError; %! else %! risk = T.NodeWeight - max (T.ClassWeight, [], 2); %! endif %! held = zeros (T.NumNodes, 1); %! br = find (T.Children(:,1) > 0); %! if (! isempty (br)) %! kd = T.Children(br,:); %! wh = T.NodeWeight(br) - T.NodeWeight(kd(:,1)) - T.NodeWeight(kd(:,2)); %! held(br) = wh .* risk(br) ./ T.NodeWeight(br); %! endif %! [L, A] = __treeprune__ (T.Children, T.Parent, risk, held); %!endfunction %!test %! ## Levels and alphas of the merged iris tree. Measured on R2024a. %! load fisheriris %! y = grp2idx (species); %! o = struct ('NumClasses', 3, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', 'MergeLeaves', true); %! [L, A] = seq (treetrain (meas, y, ones (150, 1) / 150, o)); %! assert_equal (L', [4, 0, 3, 2, 0, 1, 0, 0, 0]); %! assert_equal (A', [0, 1/150, 2/150, 44/150, 50/150], 1e-12); %!test %! ## A deeper tree, every branch pruned at its own level %! load fisheriris %! y = grp2idx (species); %! o = struct ('NumClasses', 3, 'MinParent', 2, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', 'MergeLeaves', true); %! L = seq (treetrain (meas, y, ones (150, 1) / 150, o)); %! assert_equal (L', [5, 0, 4, 3, 1, 2, 2, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0]); %!test %! ## A subtree that costs nothing to give up opens no level of the sequence %! ## The pair of leaves that merging would have removed survives here, and %! ## giving it up costs nothing, so the eleven node tree carries the merged %! ## tree's five alphas rather than six. Measured on R2024a. %! load fisheriris %! y = grp2idx (species); %! o = struct ('NumClasses', 3, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', 'MergeLeaves', false); %! [L, A] = seq (treetrain (meas, y, ones (150, 1) / 150, o)); %! assert_equal (L', [4, 0, 3, 2, 0, 1, 0, 0, 0, 0, 0]); %! assert_equal (A', [0, 1/150, 2/150, 44/150, 50/150], 1e-12); %!test %! ## A node holding rows back pays for them in the pruning sequence %! ## Twenty rows have no fourth predictor, so the node cutting on it sends %! ## them to neither child. A subtree's risk is its children's plus what %! ## the node holds back; without it the sequence came out a level short. %! ## Measured on R2024a, the fit fitctree makes on this fixture. %! load fisheriris %! y = grp2idx (species); %! x = meas; %! x(51:70, 4) = NaN; %! o = struct ('NumClasses', 3, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', 'MergeLeaves', true); %! [L, A] = seq (treetrain (x, y, ones (150, 1) / 150, o)); %! assert_equal (L', [4, 0, 3, 1, 2, 0, 0, 0, 0]); %! assert_equal (A', [0, 0.00385185185185185, 0.00593939393939394, ... %! 0.286666666666666, 0.333333333333333], 1e-14); %!test %! ## A held back node keeps the free subtree rule honest %! ## Charged for what it holds back, such a node gives a positive link %! ## where an uncharged one gave zero, so the free subtree left by %! ## MergeLeaves off must still be the one recognised. Measured on R2024a. %! load fisheriris %! y = grp2idx (species); %! x = meas; %! x(51:70, 4) = NaN; %! o = struct ('NumClasses', 3, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', 'MergeLeaves', false); %! [L, A] = seq (treetrain (x, y, ones (150, 1) / 150, o)); %! assert_equal (L', [4, 0, 3, 1, 2, 0, 0, 0, 0, 0, 0]); %! assert_equal (A', [0, 0.00385185185185185, 0.00593939393939394, ... %! 0.286666666666666, 0.333333333333333], 1e-14); %!test %! ## A regression tree, sequenced on the squared error about each mean %! g = mod ((1:20)', 3); %! x = [(1:20)', g]; %! y = [ones(10,1); 5 * ones(10,1)]; %! o = struct ('NumClasses', 1, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 19, 'SplitCriterion', 'mse', ... %! 'MergeLeaves', true, 'QEToler', 1e-6); %! L = seq (treetrain (x, y, ones (20, 1) / 20, o)); %! assert_equal (L', [1, 0, 0]); %!test %! ## A regression node holding a row back pays for it too %! ## One carsmall row has no horsepower and node 2 cuts on horsepower, so %! ## that row stops there and is in neither child. Charging it the node's %! ## own error puts this alpha at 5.99 rather than 6.32. Measured on %! ## R2024a, the fit fitrtree makes on this fixture. %! load carsmall %! X = [Weight, Cylinders, Horsepower]; %! ok = ! isnan (MPG); %! o = struct ('NumClasses', 1, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 93, 'SplitCriterion', 'mse', ... %! 'MergeLeaves', true, 'QEToler', 1e-6); %! [L, A] = seq (treetrain (X(ok, :), MPG(ok), ones (94, 1) / 94, o)); %! assert_equal (L(1:5)', [17, 16, 14, 15, 13]); %! assert_equal (numel (A), 18); %! assert_equal (A(17), 5.99325416896717, 1e-12); %! assert_equal (A(18), 41.4954735525515, 1e-11); ## Test input validation %!error __treeprune__ ([0, 0], 0, 1) %!error <__treeprune__: Children must be a non-empty N-by-2 real matrix.> ... %! __treeprune__ ([], [], [], []) %!error <__treeprune__: Children must be a non-empty N-by-2 real matrix.> ... %! __treeprune__ ([0, 0, 0], 0, 1, 0) %!error <__treeprune__: Parent must hold one node number per node.> ... %! __treeprune__ ([0, 0], [0; 0], 1, 0) %!error <__treeprune__: risk must hold one value per node.> ... %! __treeprune__ ([0, 0], 0, [1; 1], 0) %!error <__treeprune__: held must hold one value per node.> ... %! __treeprune__ ([0, 0], 0, 1, [0; 0]) %!error <__treeprune__: Children must hold a node number in each row, or zero in both on a leaf.> ... %! __treeprune__ ([2, 0; 0, 0], [0; 1], [1; 0.5], [0; 0]) %!error <__treeprune__: Children must hold a node number in each row, or zero in both on a leaf.> ... %! __treeprune__ ([2, 9; 0, 0], [0; 1], [1; 0.5], [0; 0]) %!error <__treeprune__: Parent must hold a node number, or zero at the root.> ... %! __treeprune__ ([2, 3; 0, 0; 0, 0], [0; 9; 1], [1; 0.4; 0.4], zeros (3, 1)) */ statistics-release-2.1.2/src/doc-cache000066400000000000000000001141701527311104300176450ustar00rootroot00000000000000# doc-cache created by Octave 11.2.0 # name: cache # type: cell # rows: 3 # columns: 15 # name: # type: sq_string # elements: 1 # length: 12 editDistance # name: # type: sq_string # elements: 1 # length: 4345 statistics: d = editDistance (str) statistics: d = editDistance (doc) statistics: C = editDistance (..., minDist) statistics: [C, IA, IC] = editDistance (..., minDist) statistics: [C, IA, IC] = editDistance (..., minDist, "OutputAllIndices", value) statistics: d = editDistance (str1, str2) statistics: d = editDistance (doc1, doc2) Compute the edit (Levenshtein) distance between strings or documents. d = editDistance (str) takes a cell array of character vectors and computes the Levenshtein distance between each pair of strings in str as the lowest number of grapheme insertions, deletions, and substitutions required to convert string str{1} to string str{2}. If str is a cellstr vector with N elements, the returned distance d is an (N x (N-1)) / 2) column vector of doubles. If str is an array (that is all (size (str) > 1) = true), then it is transformed to a column vector as in str = str(:). editDistance expects str to be a column vector, if it is row vector, it is transformed to a column vector. d = editDistance (doc) can also take a cell array containing cell arrays of character vectors, in which case each element of doc is regarded as a document, and the character vector in each element of the cell string array is regarded a token. editDistance computes the Levenshtein distance between each pair of cell elements in doc as the lowest number of token insertions, deletions, and substitutions required to convert document doc{1} to document doc{2}. If doc is a cell vector with N elements, the distance d is an (N x (N-1)) / 2) column vector of doubles. If doc is an array (that is all (size (doc) > 1) = true), then it is converted to a column vector as in doc = doc(:). C = editDistance (..., minDist) specifies a minimum distance, minDist, which is regarded as a similarity threshold between each pair of strings or documents, defined in the previous syntaxes. In this case, editDistance resembles the functionality of the uniquetol function and returns the unique strings or documents that are similar up to minDist distance. C is either a cellstring array or a cell array of cellstrings, depending on the first input argument. [C, IA, IC] = editDistance (..., minDist) also returns index vectors IA and IC. Assuming A contains either strings str or documents doc as defined above, IA is a column vector of indices to the first occurrence of similar elements such that C = A(IA), and IC is a column vector of indices such that A ~ C(IC) where ~ means that the strings or documents are within the specified distance minDist of each other. [C, IA, IC] = editDistance (..., minDist, "OutputAllIndices", value) specifies the type of the second output index IA. value must be a logical scalar. When set to true, IA is a cell array containing the vectors of indices for ALL elements in A that are within the specified distance minDist of each other. Each cell in IA corresponds to a value in C and the values in each cell correspond to locations in A. If value is set to false, then IA is returned as an index vector described in the previous syntax. d = editDistance (str1, str2) can also take two character vectors, str1 and str2 and compute the Levenshtein distance d as the lowest number of grapheme insertions, deletions, and substitutions required to convert str1 to str2. str1 and str2 may also be cellstring arrays, in which case the pairwise distance is computed between str1{n} and str1{n}. The cellstring arrays must be of the same size or scalars, in which case the scalar is expanded to the size of the other cellstring input. The returned distance d is a column vector with the same number of elements as the cellstring arrays. If str1 or str2 is an array, then it is transformed to a column vector. editDistance expects both str1 and str2 to be a column vectors, if not, they are transformed into column vectors. d = editDistance (doc1, doc2) can also take two cell array containing cell arrays of character vectors, in which case each element of doc1 and doc2 is regarded as a document, and the character vector in each element of the cell string array is regarded a token. editDistance computes the pairwise Levenshtein distance between the of cell elements in doc1 and doc2 as the lowest number of token insertions, deletions, and substitutions required to convert document doc1{n} to document doc1{n}. # name: # type: sq_string # elements: 1 # length: 69 Compute the edit (Levenshtein) distance between strings or documents. # name: # type: sq_string # elements: 1 # length: 11 fcnnpredict # name: # type: sq_string # elements: 1 # length: 2003 statistics: pred_Y = fcnnpredict (LayerWeights, LayerBiases, Activations, OutputLayerActivation, XC) statistics: pred_Y = fcnnpredict (..., NumThreads) statistics: [pred_Y, scores] = fcnnpredict (...) Make predictions from a fully connected Neural Network. pred_Y = fcnnpredict (LayerWeights, LayerBiases, Activations, OutputLayerActivation, XC) requires the following input arguments. LayerWeights : A cell row vector holding one matrix per layer, each with one row per neuron of that layer and one column per input to it. LayerBiases : A cell row vector holding one bias column per layer, matching LayerWeights layer for layer and row for row. Activations : The activation function of the hidden layers, named as a character vector applying to all of them or as a cellstring naming them one by one. The supported names are listed under fcnntrain. OutputLayerActivation : The activation function of the output layer, named as a character vector. XC : An NxM matrix containing the data set to be predicted upon. Rows N correspond to individual samples and columns M correspond to features (dimensions). Type of XC must be double and the number of features must correspond to those of the trained model. fcnnpredict can also be called with a sixth input argument, in which case, NumThreads, a positive scalar integer value, defines the number of threads to be used when computing the activation layers. For layers with less than 1000 neurons, NumThreads always defaults to 1. fcnnpredict returns the predicted labels, pred_Y, and if a second output argument is requested, it also returns the corresponding values of the neural networks output in scores. Installation Note: in order to support parallel processing on MacOS, users have to manually add support for OpenMP by adding the following flags to CFLAGS and CXXFLAGS prior to installing the statistics package: setenv ("CPPFLAGS", "-I/opt/homebrew/opt/libomp/include -Xclang -fopenmp") See also: fcnntrain, fitcnet, ClassificationNeuralNetwork # name: # type: sq_string # elements: 1 # length: 55 Make predictions from a fully connected Neural Network. # name: # type: sq_string # elements: 1 # length: 9 fcnntrain # name: # type: sq_string # elements: 1 # length: 4494 statistics: Mdl = fcnntrain (X, Y, LayerSizes, Activations, OutputLayerActivation, NumThreads, LearningRate, Epochs, DisplayInfo) statistics: Mdl = fcnntrain (..., LossFunction) statistics: Mdl = fcnntrain (..., LossFunction, SolverOptions) Train a fully connected Neural Network. Mdl = fcnntrain (...) requires the following input arguments. X : An NxM matrix containing the data set to be trained upon. Rows N correspond to individual samples and columns M correspond to features (dimensions). Type of X must be double. Y : An Nx1 column vector containing the labels of the training dataset. The labels must be natural numbers (positive integers) starting from 1 up to the number of classes, similarly as returned by the `grp2idx` function. Type of Y must be double. Under regression, selected by LossFunction 2, Y is instead an NxR matrix of response values, which may take any finite value, and the output layer is sized to its R columns rather than to a number of classes. LayerSizes : A numeric row vector of integer values defining the size of the hidden layers of the network. Input and output layers are automatically determined by the training data and their labels. Activations : The activation function of the hidden layers, named as a character vector applying to all of them or as a cellstring naming them one by one, in which case it must have one name per hidden layer. The supported names are: 'linear' or 'none' : the identity 'sigmoid' 'relu' : rectified linear unit 'tanh' : hyperbolic tangent 'softmax' 'lrelu' or 'prelu' : leaky rectified linear unit, whose negative slope is a constant 0.01 'elu' : exponential linear unit, saturating at -1 'gelu' : Gaussian error linear unit OutputLayerActivation : The activation function of the output layer, named as a character vector from the same list. NumThreads : A positive scalar integer value defining the number of threads used for computing the activation layers. For layers with less than 1000 neurons, NumThreads always defaults to 1. LearningRate : A positive scalar value defining the learning rate used by the gradient descend algorithm during training. Epochs : A positive scalar value defining the number of epochs for training the model. DisplayInfo : A boolean scalar indicating whether to print information during training. Mdl = fcnntrain (..., LossFunction) also selects the loss the network is trained against. LossFunction is a scalar: 0 for mean squared error over a one-hot target, which is the default, 1 for cross entropy, and 2 for mean squared error over a continuous response. Cross entropy expects the output layer to report a probability over the classes, so it belongs with a softmax output; paired that way the two gradients compose to y - t. Its loss is undefined where the predicted probability of the true class is zero, so both the logarithm and its derivative are floored. Code 2 is regression: Y holds response values rather than labels, the output layer belongs with the identity activation, and the returned model carries no Accuracy field, there being no labels to count. Mdl = fcnntrain (..., LossFunction, SolverOptions) also takes a scalar structure setting the solver. Its Solver field is 'sgd', the epoch loop and the default, or 'lbfgs', whose GradientTolerance, LossTolerance, StepTolerance and HistorySize fields tune it. Its Weights field holds one nonnegative weight per row of X; they are scaled to sum to one and the loss becomes the weighted mean over the observations, while without it every observation weighs the same. fcnntrain returns the trained model, Mdl, as a structure containing the following fields: LayerWeights : A cell array with each element containing a matrix with the Weights and Biases of each layer including the output layer. Activations : A numeric row vector of integer values defining the activation functions to be used at each layer including the output layer. Accuracy : The prediction accuracy at each iteration during the neural network model's training process. Absent under regression. Loss : The loss value recorded at each iteration during the neural network model's training process. Installation Note: in order to support parallel processing on MacOS, users have to manually add support for OpenMP by adding the following flags to CFLAGS and CXXFLAGS prior to installing the statistics package: setenv ("CPPFLAGS", "-I/opt/homebrew/opt/libomp/include -Xclang -fopenmp") See also: fcnnpredict, fitcnet, ClassificationNeuralNetwork # name: # type: sq_string # elements: 1 # length: 39 Train a fully connected Neural Network. # name: # type: sq_string # elements: 1 # length: 13 gamboostinter # name: # type: sq_string # elements: 1 # length: 2821 statistics: Mdl = gamboostinter (X, Y, F0, Method, Pairs, NumTrees, LearnRate, MaxNumSplits) statistics: Mdl = gamboostinter (..., W) statistics: Mdl = gamboostinter (..., W, Categorical) Boost trees over selected pairs of predictors. Mdl = gamboostinter (...) fits the interaction phase of a generalized additive model, continuing from the additive prediction the predictor phase left rather than refitting it. It is used by ClassificationGAM and RegressionGAM, and it is not meant to be called directly. X is an NxP numeric matrix of predictors and Y the Nx1 response, as gamboosttrain takes them. F0 is the Nx1 additive prediction of the predictor phase. The interaction phase starts from it, so its deviance is where this phase begins. Method selects what is boosted, 1 the logistic deviance and 2 the squared error. Pairs is an Mx2 matrix of predictor index pairs, one-based and within range. Choosing them is the caller's business; see gamboostpairs. NumTrees, LearnRate and MaxNumSplits are the interaction phase's own budget, initial step and split limit. W, if given, is an Nx1 vector of non-negative observation weights, as gamboosttrain takes them, and may be empty. Categorical, if given, flags the columns holding level codes, as gamboosttrain takes it. A pair tree splits such a predictor into two sets of levels, and its grid holds every level. Mdl is a structure with the following fields. PairBinEdges, a 1xP cell of the detection grid, eight equal-frequency bins per predictor, as MATLAB reports it. PairEdges, a 1xM cell with one element per pair, a 1x2 cell of the cut points its trees used on its two predictors. A tree is fitted to the rows, cutting halfway between two values a node holds, keeping at least five rows in a leaf and growing a layer at a time within MaxNumSplits. A categorical predictor is cut into two sets of levels after sorting them by the step each would take alone; of cuts with equal gain the first in that order is kept, and a level none of a node's rows holds stops at that node. A tree that splits on only one of the pair's predictors is a main effect and adds nothing, so a round may leave a pair untouched. PairValues, a 1xM cell of matrices, one value per cell of the pair's own grid. PairMissing, a 1xM cell, one element per pair: a 1x3 cell of what a row missing the first predictor takes, one value per cell of the second; what a row missing the second takes, one value per cell of the first; and what a row missing both takes. A row missing the predictor a node splits on stops there, in training as in prediction. Intercept, the constant the recentred surfaces gave up. Add it to the intercept of the predictor phase. NumTrees, ReasonForTermination, Deviance and Residuals, as gamboosttrain reports them. See also: gamboosttrain, gamboostpairs, gamboostpredict # name: # type: sq_string # elements: 1 # length: 176 F0, Method, Pairs, NumTrees, LearnRate, @ MaxNumSplits) @deftypefnx {statistics} {Mdl =} gamboostinter (, W) @deftypefnx {statistics} {Mdl =} gamboostinter (, W, @ Categorical) # name: # type: sq_string # elements: 1 # length: 13 gamboostpairs # name: # type: sq_string # elements: 1 # length: 1687 statistics: S = gamboostpairs (X, R) statistics: S = gamboostpairs (X, R, Categorical) Score every pair of predictors for an interaction. S = gamboostpairs (X, R) lays the residuals R of a fitted additive model on the coarse grid of each pair of columns of X and returns the two-way analysis of variance F ratio testing what only the cells explain. It is used to rank candidate interactions for ClassificationGAM and RegressionGAM, and it is not meant to be called directly. The p-values are deliberately not computed here. Turning F into a probability needs fcdf, which the package already ships and which is verified against MATLAB, so the caller applies it rather than a second implementation being carried in the compiled engine. Which pairs to keep is policy and belongs beside that. X is an NxP numeric matrix of predictors, and P must be at least 2 for any pair to exist. R is the Nx1 residual vector of the additive fit. Categorical, if given, flags the columns holding level codes, as gamboosttrain takes it. Such a predictor is laid on its levels rather than on eight bins. S is a structure with the following fields, one row per pair, ordered as nchoosek orders them. Pairs, the Mx2 matrix of predictor index pairs. F, the Mx1 vector of F ratios. A pair with too few observations, no spare degrees of freedom or no within-cell scatter scores 0. DF1 and DF2, the Mx1 numerator and denominator degrees of freedom. BinEdges, a 1xP cell of the coarse cut points each predictor was laid on. The grid is fixed at eight equal-frequency bins, which is what MATLAB reports for pair detection at every sample size. See also: gamboosttrain, gamboostpredict, fcdf, ClassificationGAM # name: # type: sq_string # elements: 1 # length: 12 Categorical) # name: # type: sq_string # elements: 1 # length: 15 gamboostpredict # name: # type: sq_string # elements: 1 # length: 1759 statistics: Y = gamboostpredict (BinEdges, ShapeValues, X, Intercept) statistics: Y = gamboostpredict (..., Link) statistics: Y = gamboostpredict (..., Link, PairEdges, PairValues, Pairs) statistics: Y = gamboostpredict (..., Pairs, PairMissing) Predict from a generalized additive model of boosted trees. BinEdges is a 1xP cell of row vectors and ShapeValues a 1xP cell of column vectors, as gamboosttrain returns them in the fields of the same names. Each shape function is a step function over its predictor's bins, so a term is evaluated by a lookup. X is an NxP numeric matrix with one column per additive term, and a count that does not match is an error. A missing value contributes nothing from that term rather than making the whole prediction NaN, which is what a tree does with a value it cannot place. A value outside the range the term was fitted over falls in the nearest bin, so a shape function is constant beyond its data rather than extrapolated. Intercept is the model's constant term. Link, if given, selects what the additive prediction is mapped through: 0 returns it as it stands and 1 takes it as a log-odds, returning the Nx2 matrix of class probabilities whose second column is the logistic function of it. The default is 0. PairEdges, PairValues and Pairs carry the interaction terms, as gamboostinter returns the first two and as it was given the third. Each pair contributes the value of the cell its two predictors fall in on the pair's own cut points. All three must be given together or none of them. PairMissing, as gamboostinter returns it, gives what a row missing either predictor of a pair, or both, takes. Without it such a pair contributes nothing. See also: gamboosttrain, gampredict, ClassificationGAM, RegressionGAM # name: # type: sq_string # elements: 1 # length: 242 ShapeValues, X, Intercept) @deftypefnx {statistics} {Y =} gamboostpredict (, Link) @deftypefnx {statistics} {Y =} gamboostpredict (, Link, @ PairEdges, PairValues, Pairs) @deftypefnx {statistics} {Y =} gamboostpredict (, Pairs, @ PairMissing) # name: # type: sq_string # elements: 1 # length: 13 gamboosttrain # name: # type: sq_string # elements: 1 # length: 3418 statistics: Mdl = gamboosttrain (X, Y, Method, NumTrees, LearnRate, MaxNumSplits) statistics: Mdl = gamboosttrain (X, Y, Method, NumTrees, LearnRate, MaxNumSplits, Verbose, NumPrint, F0) statistics: Mdl = gamboosttrain (..., F0, W) statistics: Mdl = gamboosttrain (..., F0, W, Categorical) statistics: Mdl = gamboosttrain (..., Verbose, NumPrint) Fit a generalized additive model of boosted trees. Mdl = gamboosttrain (X, Y, Method, NumTrees, LearnRate, MaxNumSplits) boosts one tree per column of X in each round and returns the additive model as a structure. It is the fitting engine shared by ClassificationGAM and RegressionGAM, and it is not meant to be called directly. X is an NxP numeric matrix of predictors. A missing value is not an error: the observation takes no part in the affected predictor's trees and that term contributes nothing to its prediction. Y is an Nx1 numeric vector of responses. For Method 1 it must hold zeros and ones. Method selects what is boosted: 1 the logistic deviance, as a classifier is fitted, and 2 the squared error, as a regression is fitted. NumTrees is the number of rounds, each fitting one tree per predictor. It is a budget rather than a count: a fit that stops improving ends earlier and says so. LearnRate is the step a round starts at. A round that fails to earn its place is retried at half the step, so this is an initial value and not a fixed one. MaxNumSplits is the largest number of splits any one tree may make. 1 is a stump. Verbose, if greater than zero, prints a trace of the fit, and NumPrint how often: the first round and then every NumPrint rounds. The RelTol column is the relative improvement the round bought, which is what the stopping rule reads. MATLAB prints a column under the same heading holding a quantity of its own that cannot be derived from anything else it reports, so the two are not comparable. F0, if given and not empty, continues a fit: it is the prediction the fit reached over these rows, and what comes back is the increment. W, if given, is an Nx1 vector of non-negative observation weights. Only their proportions matter. Every sum the fit takes over the observations is weighted; the fewest observations a leaf may hold still counts rows. F0 and W may each be empty. Categorical, if given, is a logical vector with one element per column of X. A flagged column holds level codes, positive integers, with a missing or unseen level NaN. Each level is a bin of its own, and a tree sorts the levels by the step each would take alone before it chooses its cut, as R2024a does. Mdl is a structure with the following fields. Intercept, the constant term the additive terms are added to. For a classifier it is fitted rather than fixed: it is seeded with the log-odds of the response mean and then collects the constant each shape function gives up when it is recentred. For a regression it is the response mean and stays there. BinEdges, a 1xP cell of row vectors, the cut points each predictor was binned at. ShapeValues, a 1xP cell of column vectors, one value per bin. A shape function is a step function, so this is the whole of it however many trees produced it. NumTrees, the number of rounds actually performed. ReasonForTermination, why fitting stopped. Deviance, the deviance at the last round. Residuals, the Nx1 residual vector at the last round. See also: gamboostpredict, gamtrain, ClassificationGAM, RegressionGAM # name: # type: sq_string # elements: 1 # length: 368 Method, NumTrees, LearnRate, MaxNumSplits) @deftypefnx {statistics} {Mdl =} gamboosttrain (X, Y, @ Method, NumTrees, LearnRate, MaxNumSplits, @ Verbose, NumPrint, F0) @deftypefnx {statistics} {Mdl =} gamboosttrain (, F0, @ W) @deftypefnx {statistics} {Mdl =} gamboosttrain (, F0, @ W, Categorical) @deftypefnx {statistics} {Mdl =} gamboosttrain (, Verbose, @ NumPrint) # name: # type: sq_string # elements: 1 # length: 10 gampredict # name: # type: sq_string # elements: 1 # length: 1321 statistics: yFit = gampredict (Parameters, X, Intercept) statistics: score = gampredict (Parameters, X, Intercept, Link) Evaluate a generalized additive model on new data. yFit = gampredict (Parameters, X, Intercept) adds the intercept to the sum of the additive terms evaluated at each row of X and returns the Nx1 result. It is the prediction engine shared by ClassificationGAM and RegressionGAM, and it is not meant to be called directly. Parameters is a 1xP structure array of piecewise polynomials, as returned by gamtrain in the field of the same name. X is an NxP numeric matrix with one column per additive term, and a count that does not match is an error. A model carrying interaction terms must therefore be given the augmented matrix, not the predictors alone. A missing value predicts NaN, since no term of the model is defined at it. A value outside the range the term was fitted over is extrapolated from the nearest piece, as ppval extrapolates. Intercept is the model's constant term. Link, if given, selects what the additive prediction is mapped through: 0 returns it as it stands and 1 takes it as a log-odds, returning the Nx2 matrix of class probabilities whose second column is the logistic function of it. The default is 0. See also: gamtrain, ClassificationGAM, RegressionGAM, fitcgam, fitrgam # name: # type: sq_string # elements: 1 # length: 94 X, Intercept) @deftypefnx {statistics} {score =} gampredict (Parameters, @ X, Intercept, Link) # name: # type: sq_string # elements: 1 # length: 8 gamtrain # name: # type: sq_string # elements: 1 # length: 2098 statistics: Mdl = gamtrain (X, Y, Knots, Order, Method, Inter, P1, P2) Fit a generalized additive model of smoothing splines. Mdl = gamtrain (X, Y, Knots, Order, Method, Inter, P1, P2) fits one univariate spline per column of X and returns the additive model as a structure. It is the fitting engine shared by ClassificationGAM and RegressionGAM, and it is not meant to be called directly. X is an NxP numeric matrix of predictors. A missing value is not an error: the observation is left out of the affected predictor's spline and its prediction from that term is NaN. Y is an Nx1 numeric vector of responses. For Method 1 it must hold zeros and ones. Knots is a 1xP vector giving the number of spline pieces for each predictor, and Order a 1xP vector giving the degree of the polynomial on each piece. A spline of K pieces and degree D spans a space of K + D dimensions. Method selects the fitting scheme: 1 boosts the log-odds by gradient descent, as a classifier is fitted, and 2 backfits the partial residuals, as a regression is fitted. Inter is the intercept the fit starts from: a proportion for Method 1, which is stored as its log-odds, and the response mean for Method 2. A proportion of zero or one is not an error: its log-odds is infinite, the gradient is zero throughout and every additive term stays at zero, which is the fit a single-class response has. P1 and P2 are the scheme's two parameters. For Method 1 they are the learning rate and the number of boosting iterations; for Method 2 the convergence tolerance and the maximum number of backfitting cycles. Mdl is a structure with the following fields. Intercept, the constant term the additive terms are added to. Parameters, a 1xP structure array of piecewise polynomials in the form ppval consumes, one per predictor. Iterations, the number of iterations performed. Residuals, the Nx1 residual vector at the last iteration. RSS, the scalar residual sum of squares for Method 1 and the 1xP per-term criterion the backfitting stops on for Method 2. See also: gampredict, ClassificationGAM, RegressionGAM, fitcgam, fitrgam # name: # type: sq_string # elements: 1 # length: 36 Knots, Order, Method, Inter, P1, P2) # name: # type: sq_string # elements: 1 # length: 10 libsvmread # name: # type: sq_string # elements: 1 # length: 261 statistics: [labels, data] = libsvmread (filename) This function reads the labels and the corresponding instance_matrix from a LIBSVM data file and stores them in labels and data respectively. These can then be used as inputs to svmtrain or svmpredict function. # name: # type: sq_string # elements: 1 # length: 141 This function reads the labels and the corresponding instance_matrix from a LIBSVM data file and stores them in labels and data respectively. # name: # type: sq_string # elements: 1 # length: 11 libsvmwrite # name: # type: sq_string # elements: 1 # length: 222 statistics: libsvmwrite (filename, labels, data) This function saves the labels and the corresponding instance_matrix in a file specified by filename. data must be a sparse matrix. Both labels, data must be of double type. # name: # type: sq_string # elements: 1 # length: 101 This function saves the labels and the corresponding instance_matrix in a file specified by filename. # name: # type: sq_string # elements: 1 # length: 10 svmpredict # name: # type: sq_string # elements: 1 # length: 2193 statistics: predicted_label = svmpredict (labels, data, model) statistics: predicted_label = svmpredict (labels, data, model, "libsvm_options") statistics: [predicted_label, accuracy, decision_values] = svmpredict (labels, data, model, "libsvm_options") statistics: [predicted_label, accuracy, prob_estimates] = svmpredict (labels, data, model, "libsvm_options") This function predicts new labels from a testing instance matrix based on an SVM model created with svmtrain. labels : An m by 1 vector of prediction labels. If labels of test data are unknown, simply use any random values. (type must be double) data : An m by n matrix of m testing instances with n features. It can be dense or sparse. (type must be double) model : The output of svmtrain function. libsvm_options : A string of testing options in the same format as that of LIBSVM. libsvm_options : -b : probability_estimates; whether to predict probability estimates. 0 return decision values. (default) 1 return probability estimates. -q : quiet mode. (no outputs) The svmpredict function has three outputs. The first one, predicted_label, is a vector of predicted labels. The second output, accuracy, is a vector including accuracy (for classification), mean squared error, and squared correlation coefficient (for regression). The third is a matrix containing decision values or probability estimates (if -b 1' is specified). If k is the number of classes in training data, for decision values, each row includes results of predicting k(k-1)/2 binary-class SVMs. For classification, k = 1 is a special case. Decision value +1 is returned for each testing instance, instead of an empty vector. For probabilities, each row contains k values indicating the probability that the testing instance is in each class. Note that the order of classes here is the same as Label field in the model structure. Note on LIBSVM 3.36 Update: This implementation is based on LIBSVM 3.36 (2025) and now supports probability estimates for One-Class SVM (-s 2) when combined with the probability flag (-b 1). For One-Class SVM, the prob_estimates output is a single column vector containing the probability of the instance being an inlier. # name: # type: sq_string # elements: 1 # length: 109 This function predicts new labels from a testing instance matrix based on an SVM model created with svmtrain. # name: # type: sq_string # elements: 1 # length: 8 svmtrain # name: # type: sq_string # elements: 1 # length: 3691 statistics: model = svmtrain (labels, data, "libsvm_options") statistics: model = svmtrain (labels, data, "libsvm_options", weights) statistics: [model, converged] = svmtrain (...) This function trains an SVM model based on known labels and their corresponding data which comprise an instance matrix. labels : An m by 1 vector of prediction labels. (type must be double) data : An m by n matrix of m testing instances with n features. It can be dense or sparse. (type must be double) libsvm_options : A string of testing options in the same format as that of LIBSVM. weights : An m by 1 vector of nonnegative instance weights, one per instance. Each scales the box constraint C of its instance, and an instance of zero weight is left out of the training. (type must be double) libsvm_options : -s : svm_type; set type of SVM (default 0) 0 C-SVC (multi-class classification) 1 nu-SVC (multi-class classification) 2 one-class SVM 3 epsilon-SVR (regression) 4 nu-SVR (regression) -t : kernel_type; set type of kernel function (default 2) 0 linear: u'*v 1 polynomial: (gamma x u' x v + coef0) ^ degree 2 radial basis function: exp(-gamma x |u-v| ^ 2) 3 sigmoid: tanh(gamma x u' x v + coef0) 4 precomputed kernel (kernel values in training_instance_matrix) -d : degree; set degree in kernel function (default 3) -g : gamma; set gamma in kernel function (default 1/num_features) -r : coef0; set coef0 in kernel function (default 0) -c : cost; set the parameter C of C-SVC, epsilon-SVR, and nu-SVR (default 1) -n : nu; set the parameter nu of nu-SVC, one-class SVM, and nu-SVR (default 0.5) -p : epsilon; set the epsilon in loss function of epsilon-SVR (default 0.1) -m : cachesize; set cache memory size in MB (default 100) -e : epsilon; set tolerance of termination criterion (default 0.001) -h : shrinking; whether to use the shrinking heuristics, 0 or 1 (default 1) -b : probability_estimates; whether to train a SVC or SVR model for probability estimates, 0 or 1 (default 0) -w : weight; set the parameter C of class i to weight*C, for C-SVC (default 1) -v : n; n-fold cross validation mode -q : quiet mode (no outputs) The function svmtrain function returns a model structure which can be used for future prediction and it contains the following fields: Parameters : parameters nr_class : number of classes; = 2 for regression/one-class svm totalSV : total #SV rho : -b of the decision function(s) wx+b Label : label of each class; empty for regression/one-class SVM sv_indices : values in [1,...,num_training_data] to indicate SVs in the training set ProbA : pairwise probability information; empty if -b 0 or in one-class SVM ProbB : pairwise probability information; empty if -b 0 or in one-class SVM ProbDensityMarks : density marks for one-class SVM probability estimates; empty if -b 0 or not one-class SVM. nSV : number of SVs for each class; empty for regression/one-class SVM sv_coef : coefficients for SVs in decision functions SVs : support vectors If you do not use the option -b 1, ProbA and ProbB are empty matrices. If the '-v' option is specified, cross validation is conducted and the returned model is just a scalar: cross-validation accuracy for classification and mean-squared error for regression. The optional output converged is false when any solver run stopped at its iteration limit before meeting the tolerance, and true otherwise. Note on LIBSVM 3.36 Update: This implementation is based on LIBSVM 3.36 (2025) and now supports probability estimates for One-Class SVM (-s 2) when combined with the probability flag (-b 1). For One-Class SVM, the prob_estimates output is a single column vector containing the probability of the instance being an inlier. # name: # type: sq_string # elements: 1 # length: 119 This function trains an SVM model based on known labels and their corresponding data which comprise an instance matrix. # name: # type: sq_string # elements: 1 # length: 11 treepredict # name: # type: sq_string # elements: 1 # length: 1334 statistics: V = treepredict (X, Children, CutPredictorIndex, CutPoint, Value) statistics: V = treepredict (..., CutCategories) statistics: [V, node] = treepredict (...) Evaluate a binary decision tree on new data. V = treepredict (X, Children, CutPredictorIndex, CutPoint, Value) sends each row of the NxP matrix X down the tree and returns the value of the node it comes to rest at. It is the prediction engine shared by ClassificationTree and RegressionTree, and is not meant to be called directly. Children, CutPredictorIndex and CutPoint are the node table returned by treetrain, a zero in Children marking a leaf. Value carries one row per node: the class probabilities of a classifier, in which case V is NxK, or the mean of a regression, in which case it is Nx1. [V, node] = treepredict (...) also returns the index of the node each row came to rest at. A row is stopped by the first node whose split predictor it is missing, and takes that node's value, which is how such a row was held back rather than sent to a child while the tree was grown. CutCategories, the Mx2 cell array treetrain returns, gives the levels a categorical cut sends left and right. A row goes the way its level is listed, and a row whose level is in neither list, one the node never saw, is stopped there like a missing value. See also: treetrain # name: # type: sq_string # elements: 1 # length: 150 CutPredictorIndex, CutPoint, Value) @deftypefnx {statistics} {V =} treepredict (, CutCategories) @deftypefnx {statistics} {[V, node] =} treepredict () # name: # type: sq_string # elements: 1 # length: 9 treetrain # name: # type: sq_string # elements: 1 # length: 2175 statistics: Mdl = treetrain (X, Y, W, opts) Grow a binary decision tree by recursive partitioning. Mdl = treetrain (X, Y, W, opts) grows a tree on the NxP predictor matrix X and returns it as a structure with one row per node. It is the fitting engine shared by ClassificationTree and RegressionTree, and is not meant to be called directly. Y is an Nx1 vector of class indices in 1:K when fitting a classifier and the response itself when fitting a regression. W is an Nx1 vector of observation weights, already adjusted for any prior. opts is a structure carrying NumClasses, MinParent, MinLeaf, MaxSplits, SplitCriterion, MergeLeaves and, for a regression, QEToler. SplitCriterion is "gdi" or "deviance" for a classifier and "mse" for a regression, and it is what selects between the two. NumVariablesToSample, when present, is the number of predictors each node chooses its split from, drawn afresh at every node, and Seed, an integer from 0 to 2^32-1, seeds that draw, so the same seed grows the same tree. A node whose predictors hold no valid split is a leaf. Without NumVariablesToSample every predictor is tried and nothing is drawn. CategoricalPredictors, when present, lists the columns of X that hold levels rather than numbers, and such a column is split into two sets of levels. A regression orders the levels by mean response and two classes by the probability of the first class. More classes search every partition when the node holds at most MaxNumCategories levels, 10 by default, and otherwise take MATLAB's heuristics, as AlgorithmForCategorical selects: "auto" (default), "exact", "pullleft", "pca" or "ovabyclass". The returned structure holds Children, Parent, CutPredictorIndex, CutPoint, CutCategories, IsBranchNode, NodeSize, NodeWeight and NumNodes, plus ClassWeight and ClassCount for a classifier or NodeMean and NodeError for a regression. Nodes are numbered as they are created, so a parent always precedes its children. The cost complexity pruning sequence is not built here. A tree outlives the data it was grown on, so the sequence is __treeprune__, which takes a node table and the risk the caller measures by. See also: treepredict # name: # type: sq_string # elements: 1 # length: 5 opts) statistics-release-2.1.2/src/editDistance.cc000066400000000000000000000562101527311104300210230ustar00rootroot00000000000000/* Copyright (C) 2024 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include #include #include #include using namespace std; struct UniqueVecOut { ColumnVector IA; ColumnVector IC; Cell IA_c; }; // Function for computing the minimum of three integer values int minimum (int a, int b, int c) { int min = a; if (b < min) min = b; if (c < min) min = c; return min; } // Function for computing the Levenshtein distance between two strings int LevensDistStr (const string& s1, const string& s2) { const size_t rows = s1.length(); const size_t cols = s2.length(); vector curr(cols+1, 0); int prev; // Prepopulate 1st column for (size_t j = 0; j <= cols; j++) { curr[j] = j; } // Compute all other elements in distance matrix for (size_t i = 1; i <= rows; i++) { prev = curr[0]; curr[0] = i; for (size_t j = 1; j <= cols; j++) { int temp = curr[j]; if (s1[i - 1] == s2[j - 1]) { curr[j] = prev; } else { curr[j] = 1 + minimum (prev, curr[j - 1], curr[j]); } prev = temp; } } return curr[cols]; } // Function for computing the Levenshtein distance between two documents int LevensDistDoc (const Cell& d1, const Cell& d2) { const size_t rows = d1.numel(); const size_t cols = d2.numel(); vector curr(cols+1, 0); int prev; // Prepopulate 1st column for (size_t j = 0; j <= cols; j++) { curr[j] = j; } // Compute all other elements in distance matrix for (size_t i = 1; i <= rows; i++) { prev = curr[0]; curr[0] = i; for (size_t j = 1; j <= cols; j++) { int temp = curr[j]; if (d1(i - 1).string_value() == d2(i - 1).string_value()) { curr[j] = prev; } else { curr[j] = 1 + minimum (prev, curr[j - 1], curr[j]); } prev = temp; } } return curr[cols]; } // Transform a distance triu matrix to a boolean triu matrix boolMatrix double2bool (const Matrix& D, const int& minDist) { const size_t sz = D.rows(); boolMatrix Bmat(sz, sz); for (size_t i = 0; i < sz - 1; i++) { Bmat(i,i) = true; for (size_t j = i + 1; j < sz; j++) { if (D(i,j) <= minDist) { Bmat(i,j) = true; Bmat(j,i) = false; } else { Bmat(i,j) = false; Bmat(j,i) = false; } } } Bmat(sz - 1,sz - 1) = true; return Bmat; } // Transform a distance triu matrix to a distance vector Matrix triu2Dvec (const Matrix& D) { const size_t szA = D.rows(); const size_t sz = szA * (szA - 1) / 2; octave_idx_type idx = 0; Matrix Dvec(sz, 1); for (size_t i = 0; i < szA - 1; i++) { for (size_t j = i + 1; j < szA; j++) { Dvec(idx++,0) = D(i,j); } } return Dvec; } // Compute unique indexing IA cell of vectors vector> IAcellvec (const boolMatrix& B) { const size_t rows = B.rows(); const size_t cols = B.columns(); vector> IAcell; for (size_t i = 0; i < rows; i++) { vector IA_cIdx; IA_cIdx.push_back(i); for (size_t j = i + 1; j < cols; j++) { if (B(i,j)) { IA_cIdx.push_back(j); } } IAcell.push_back(IA_cIdx); } return IAcell; } // Transform to IAcellvec to Cell Cell IA2cell (const vector>& IAc, const vector& IAv) { const size_t sz_v = IAv.size(); Cell IA(sz_v, 1); for (size_t i = 0; i < sz_v; i++) { int idx = IAv[i]; const size_t sz_c = IAc[idx].size(); Matrix IAidx(sz_c, 1); for (size_t j = 0; j < sz_c; j++) { IAidx(j,0) = IAc[idx][j] + 1; } IA(i,0) = IAidx; } return IA; } // Compute unique indexing IA vector vector IAvector (const vector>& IAcell) { vector IA; vector IA_done; for (size_t i = 0; i < IAcell.size(); i++) { for (size_t j = 0; j < IAcell[i].size(); j++) { if (binary_search(IA_done.begin(), IA_done.end(), IAcell[i][j])) { break; } else { if (j == 0) { IA.push_back(IAcell[i][j]); } else { IA_done.push_back(IAcell[i][j]); } } } sort (IA_done.begin(), IA_done.end()); } return IA; } // Transform to IAvector to Matrix Matrix IA2mat (const vector& IAv) { const size_t sz_v = IAv.size(); Matrix IA(sz_v, 1); for (size_t i = 0; i < sz_v; i++) { IA(i,0) = IAv[i] + 1; } return IA; } // Compute unique indexing IA vector Matrix ICvector (const vector>& IAc, const size_t& szA) { Matrix IC(szA, 1); vector IC_done; for (size_t i = 0; i < IAc.size(); i++) { for (size_t j = 0; j < IAc[i].size(); j++) { if (binary_search(IC_done.begin(), IC_done.end(), IAc[i][j])) { break; } else { octave_idx_type idx = IAc[i][j]; IC(idx,0) = i + 1; IC_done.push_back(IAc[i][j]); } } } return IC; } // Functionality for uniquetol octave_value_list uniquetol (const int& nargout, const Cell& A, const Matrix& D, const int& minDist, const bool& OutputAllIndices) { octave_value_list retval (nargout); boolMatrix B = double2bool (D, minDist); vector> IAc = IAcellvec (B); vector IAv = IAvector (IAc); // Build cellstr with unique elements Cell C(IAv.size(), 1); if (A.iscellstr()) { for (size_t i = 0; i < IAv.size(); i++) { C(i,0) = A(IAv[i]).string_value(); } } else { for (size_t i = 0; i < IAv.size(); i++) { C(i,0) = A.elem(IAv[i]); } } retval(0) = C; // Build IA vector output if (nargout > 1 && OutputAllIndices) { retval(1) = IA2cell (IAc, IAv); } else if (nargout > 1) { retval(1) = IA2mat (IAv); } // Build IC vector output if (nargout > 2) { retval(2) = ICvector (IAc, A.numel()); } return retval; } // Expand a cell scalar to a cell vector Cell expand (const Cell& IN, const size_t& sz) { //octave_idx_type sz = static_cast(sz_out); Cell OUT(sz, 1); for (size_t i = 0; i < sz; i++) { OUT(i,0) = IN.elem(0); } return OUT; } DEFUN_DLD(editDistance, args, nargout, "-*- texinfo -*-\n\ @deftypefn {statistics} {@var{d} =} editDistance (@var{str})\n\ @deftypefnx {statistics} {@var{d} =} editDistance (@var{doc})\n\ @deftypefnx {statistics} {@var{C} =} editDistance (@dots{}, @var{minDist})\n\ @deftypefnx {statistics} {[@var{C}, @var{IA}, @var{IC}] =} editDistance @\ (@dots{}, @var{minDist})\n\ @deftypefnx {statistics} {[@var{C}, @var{IA}, @var{IC}] =} editDistance @\ (@dots{}, @var{minDist}, @qcode{\"OutputAllIndices\"}, @var{value})\n\ @deftypefnx {statistics} {@var{d} =} editDistance (@var{str1}, @var{str2})\n\ @deftypefnx {statistics} {@var{d} =} editDistance (@var{doc1}, @var{doc2})\n\ \n\ \n\ Compute the edit (Levenshtein) distance between strings or documents. \ \n\n\ @code{@var{d} = editDistance (@var{str})} takes a cell array of character \ vectors and computes the Levenshtein distance between each pair of strings in \ @var{str} as the lowest number of grapheme insertions, deletions, and \ substitutions required to convert string @qcode{@var{str}@{1@}} to string \ @qcode{@var{str}@{2@}}. If @var{str} is a @qcode{cellstr} vector with \ @math{N} elements, the returned distance @var{d} is an @math{(N * (N-1)) / 2)} \ column vector of doubles. If @var{str} is an array (that is @code{all (size \ (str) > 1) = true}), then it is transformed to a column vector as in \ @code{str = str(:)}. @code{editDistance} expects @var{str} to be a column \ vector, if it is row vector, it is transformed to a column vector.\n\n\ \ @code{@var{d} = editDistance (@var{doc})} can also take a cell array \ containing cell arrays of character vectors, in which case each element of \ @var{doc} is regarded as a document, and the character vector in each element \ of the cell string array is regarded a token. @code{editDistance} computes \ the Levenshtein distance between each pair of cell elements in @var{doc} as \ the lowest number of token insertions, deletions, and substitutions required \ to convert document @qcode{@var{doc}@{1@}} to document @qcode{@var{doc}@{2@}}. \ If @var{doc} is a @qcode{cell} vector with @math{N} elements, the distance \ @var{d} is an @math{(N * (N-1)) / 2)} column vector of doubles. If @var{doc} \ is an array (that is @code{all (size (doc) > 1) = true}), then it is converted \ to a column vector as in @code{doc = doc(:)}.\n\n\ \ @code{@var{C} = editDistance (@dots{}, @var{minDist})} specifies a minimum \ distance, @var{minDist}, which is regarded as a similarity threshold between \ each pair of strings or documents, defined in the previous syntaxes. In this \ case, @code{editDistance} resembles the functionality of the @code{uniquetol} \ function and returns the unique strings or documents that are similar up to \ @var{minDist} distance. @var{C} is either a cellstring array or a cell array \ of cellstrings, depending on the first input argument.\n\n\ \ @code{[@var{C}, @var{IA}, @var{IC}] = editDistance (@dots{}, @var{minDist})} \ also returns index vectors @var{IA} and @var{IC}. Assuming @var{A} contains \ either strings @var{str} or documents @var{doc} as defined above, @var{IA} \ is a column vector of indices to the first occurrence of similar elements such \ that @qcode{@var{C} = @var{A}(@var{IA})}, and @var{IC} is a column vector of \ indices such that @qcode{@var{A} ~ @var{C}(@var{IC})} where @qcode{~} means \ that the strings or documents are within the specified distance @var{minDist} \ of each other.\n\n\ \ @code{[@var{C}, @var{IA}, @var{IC}] = editDistance (@dots{}, @var{minDist}, \ @qcode{\"OutputAllIndices\"}, @var{value})} specifies the type of the second \ output index @var{IA}. @var{value} must be a logical scalar. When set to \ @code{true}, @var{IA} is a cell array containing the vectors of indices for \ ALL elements in @var{A} that are within the specified distance @var{minDist} \ of each other. Each cell in @var{IA} corresponds to a value in @var{C} and \ the values in each cell correspond to locations in @var{A}. If @var{value} is \ set to @code{false}, then @var{IA} is returned as an index vector described in \ the previous syntax.\n\n\ \ @code{@var{d} = editDistance (@var{str1}, @var{str2})} can also take two \ character vectors, @var{str1} and @var{str2} and compute the Levenshtein \ distance @var{d} as the lowest number of grapheme insertions, deletions, and \ substitutions required to convert @var{str1} to @var{str2}. @var{str1} and \ @var{str2} may also be cellstring arrays, in which case the pairwise distance \ is computed between @qcode{@var{str1}@{n@}} and @qcode{@var{str1}@{n@}}. The \ cellstring arrays must be of the same size or scalars, in which case the \ scalar is expanded to the size of the other cellstring input. The returned \ distance @var{d} is a column vector with the same number of elements as the \ cellstring arrays. If @var{str1} or @var{str2} is an array, then it is \ transformed to a column vector. @code{editDistance} expects both @var{str1} \ and @var{str2} to be a column vectors, if not, they are transformed into \ column vectors.\n\n\ \ @code{@var{d} = editDistance (@var{doc1}, @var{doc2})} can also take two cell \ array containing cell arrays of character vectors, in which case each element \ of @var{doc1} and @var{doc2} is regarded as a document, and the character \ vector in each element of the cell string array is regarded a token. \ @code{editDistance} computes the pairwise Levenshtein distance between the \ of cell elements in @var{doc1} and @var{doc2} as the lowest number of token \ insertions, deletions, and substitutions required to convert document \ @qcode{@var{doc1}@{n@}} to document @qcode{@var{doc1}@{n@}}.\n\n\ @end deftypefn") { int nargin = args.length(); // Add default options bool OutputAllIndices = false; // Parse Name-Value paired arguments if (nargin > 2 && args(nargin-2).is_string()) { string ParamName = "OutputAllIndices"; if (args(nargin - 2).string_value() == ParamName) { const int idx = nargin - 1; if (args(idx).islogical() && args(idx).numel() == 1) { const boolMatrix tmp = args(idx).bool_matrix_value(); OutputAllIndices = tmp(0); } else { error ("editDistance: value for OutputAllIndices " "must be a logical scalar."); } nargin--; nargin--; } } // Check for invalid number of input arguments if (nargin > 3) { error ("editDistance: too many input arguments."); } // Check for last argument being numeric (minDist) int minDist; bool doMinDist; if (nargin > 1 && args(nargin-1).isnumeric()) { // Check minDist input argument if (args(nargin -1 ).numel() != 1) { error ("editDistance: minDist must be a scalar value."); } Matrix tmp = args(nargin - 1).matrix_value(); if (tmp(0,0) < 0 || floor (tmp(0,0)) != tmp(0,0)) { error ("editDistance: minDist must be a nonnegative integer."); } minDist = static_cast(tmp(0,0)); doMinDist = true; nargin--; } else { doMinDist = false; } // Check for invalid number of output arguments if ((nargout > 3 && doMinDist) || (nargout > 1 && ! doMinDist)) { error ("editDistance: too many output arguments."); } // Check cases of string arguments octave_value_list retval (nargout); if (nargin == 1) { if (args(0).iscellstr()) { // Get cellstr input argument const Cell strA = args(0).cellstr_value(); size_t szA = strA.numel(); // For scalar input return distance to itself, i.e. 0 if (szA == 1) { retval(0) = double (0); return retval; } // Compute the edit distance Matrix D(szA, szA); #pragma omp parallel { #pragma omp parallel for for (size_t i = 0; i < szA - 1; i++) { D(i,i) = 0; string s1 = strA(i).string_value(); for (size_t j = i + 1; j < szA; j++) { D(i,j) = LevensDistStr (s1, strA(j).string_value()); } } D(szA - 1,szA - 1) = 0; } // If minDist is given, change functionality from 'pdist' to 'uniquetol' if (doMinDist) { retval = uniquetol (nargout, strA, D, minDist, OutputAllIndices); } else { // Transform to distance vector retval(0) = triu2Dvec (D); } return retval; } else if (args(0).iscell()) { // Get cell input argument const Cell docA = args(0).cell_value(); size_t szA = docA.numel(); // Check that all cell elements contain cellstring arrays for (size_t i = 0; i < szA; i++) { Cell tmp = docA.elem(i); if (! tmp.iscellstr()) { error ("editDistance: tokenizedDocument " "must contain cellstr arrays."); } } // For scalar input return distance to itself, i.e. 0 if (szA == 1) { retval(0) = double (0); return retval; } // Compute the edit distance Matrix D(szA, szA); #pragma omp parallel { #pragma omp parallel for for (size_t i = 0; i < szA - 1; i++) { D(i,i) = 0; Cell d1 = docA.elem(i); for (size_t j = i + 1; j < szA; j++) { D(i,j) = LevensDistDoc (d1, docA.elem(j)); } } D(szA - 1,szA - 1) = 0; } // If minDist is given, change functionality from 'pdist' to 'uniquetol' if (doMinDist) { retval = uniquetol (nargout, docA, D, minDist, OutputAllIndices); } else { // Transform to distance vector retval(0) = triu2Dvec (D); } return retval; } else { error ("editDistance: STR1 must be a cellstr."); } } else if (nargin == 2) { if (args(0).iscellstr() && args(1).iscellstr()) { // Get cellstr input arguments Cell strA = args(0).cellstr_value(); Cell strB = args(1).cellstr_value(); // Check cellstr sizes match size_t szA = strA.numel(); size_t szB = strB.numel(); if (szA != 1 && szB != 1 && szA != szB) { error ("editDistance: cellstr input arguments size mismatch."); } // Preallocate the distance vector and expand as necessary size_t sz = szA; if (szA == 1 && szB != 1) { sz = szB; strA = expand (strA, sz); } else if (szA != 1 && szB == 1) { strB = expand (strB, sz); } Matrix D(sz, 1); // Compute the distance vector for (size_t i = 0; i < sz; i++) { D(i,0) = LevensDistStr (strA(i).string_value(), strB(i).string_value()); } retval(0) = D; } else if (args(0).iscell() && args(1).iscell()) { // Get cell input arguments Cell docA = args(0).cell_value(); Cell docB = args(1).cell_value(); // Check cell sizes match size_t szA = docA.numel(); size_t szB = docB.numel(); if (szA != 1 && szB != 1 && szA != szB) { error ("editDistance: cellstr input arguments size mismatch."); } // Check both cell arrays contain cellstring arrays for (size_t i = 0; i < szA; i++) { Cell tmp = docA.elem(i); if (! tmp.iscellstr()) { error ("editDistance: first tokenizedDocument " "does not contain cellstr arrays."); } } for (size_t i = 0; i < szB; i++) { Cell tmp = docB.elem(i); if (! tmp.iscellstr()) { error ("editDistance: second tokenizedDocument " "does not contain cellstr arrays."); } } // Preallocate the distance vector and expand as necessary size_t sz = szA; if (szA == 1 && szB != 1) { sz = szB; docA = expand (docA, sz); } else if (szA != 1 && szB == 1) { docB = expand (docB, sz); } Matrix D(sz, 1); // Compute the distance vector for (size_t i = 0; i < sz; i++) { D(i,0) = LevensDistDoc (docA.elem(i), docB.elem(i)); } retval(0) = D; } else if (args(0).is_string() && args(1).is_string()) { retval(0) = LevensDistStr (args(0).string_value(),args(1).string_value()); } else { error ("editDistance: STR1 and STR2 must be either strings or cellstr."); } } return retval; } /* %!error d = editDistance (1, 2, 3, 4); %!error ... %! [C, IA, IC, I] = editDistance ({"AS","SD","AD"}, 1); %!error ... %! [C, IA] = editDistance ({"AS","SD","AD"}); %!error ... %! d = editDistance ({"AS","SD","AD"}, [1, 2]); %!error ... %! d = editDistance ({"AS","SD","AD"}, -2); %!error ... %! d = editDistance ({"AS","SD","AD"}, 1.25); %!error ... %! d = editDistance ({"AS","SD","AD"}, {"AS","SD","AD"}, [1, 2]); %!error ... %! d = editDistance ({"AS","SD","AD"}, {"AS","SD","AD"}, -2); %!error ... %! d = editDistance ({"AS","SD","AD"}, {"AS","SD","AD"}, 1.25); %!error ... %! d = editDistance ("string1", "string2", [1, 2]); %!error ... %! d = editDistance ("string1", "string2", -2); %!error ... %! d = editDistance ("string1", "string2", 1.25); %!error ... %! d = editDistance ({{"string1", "string2"}, 2}); %!error ... %! d = editDistance ({{"string1", "string2"}, 2}, 2); %!error ... %! d = editDistance ([1, 2, 3]); %!error ... %! d = editDistance (["AS","SD","AD","AS"]); %!error ... %! d = editDistance (["AS","SD","AD"], 2); %!error ... %! d = editDistance (logical ([1,2,3]), {"AS","AS","AD"}); %!error ... %! d = editDistance ({"AS","SD","AD"}, logical ([1,2,3])); %!error ... %! d = editDistance ([1,2,3], {"AS","AS","AD"}); %!error ... %! d = editDistance ({1,2,3}, {"AS","SD","AD"}); %!error ... %! d = editDistance ({"AS","SD","AD"}, {1,2,3}); %!error ... %! d = editDistance ({"AS","SD","AD"}, {"AS", "AS"}); %!test %! d = editDistance ({"AS","SD","AD"}); %! assert_equal (d, [2; 1; 1]); %! assert_equal (class (d), "double"); %!test %! C = editDistance ({"AS","SD","AD"}, 1); %! assert_equal (iscellstr (C), true); %! assert_equal (C, {"AS";"SD"}); %!test %! [C, IA] = editDistance ({"AS","SD","AD"}, 1); %! assert_equal (class (IA), "double"); %! assert_equal (IA, [1;2]); %!test %! A = {"ASS"; "SDS"; "FDE"; "EDS"; "OPA"}; %! [C, IA] = editDistance (A, 2, "OutputAllIndices", false); %! assert_equal (class (IA), "double"); %! assert_equal (A(IA), C); %!test %! A = {"ASS"; "SDS"; "FDE"; "EDS"; "OPA"}; %! [C, IA] = editDistance (A, 2, "OutputAllIndices", true); %! assert_equal (class (IA), "cell"); %! assert_equal (C, {"ASS"; "FDE"; "OPA"}); %! assert_equal (A(IA{1}), {"ASS"; "SDS"; "EDS"}); %! assert_equal (A(IA{2}), {"FDE"; "EDS"}); %! assert_equal (A(IA{3}), {"OPA"}); %!test %! A = {"ASS"; "SDS"; "FDE"; "EDS"; "OPA"}; %! [C, IA, IC] = editDistance (A, 2); %! assert_equal (class (IA), "double"); %! assert_equal (A(IA), C); %! assert_equal (IC, [1; 1; 3; 1; 5]); %!test %! d = editDistance ({"AS","SD","AD"}, {"AS", "AD", "SE"}); %! assert_equal (d, [0; 1; 2]); %! assert_equal (class (d), "double"); %!test %! d = editDistance ({"AS","SD","AD"}, {"AS"}); %! assert_equal (d, [0; 2; 1]); %! assert_equal (class (d), "double"); %!test %! d = editDistance ({"AS"}, {"AS","SD","AD"}); %! assert_equal (d, [0; 2; 1]); %! assert_equal (class (d), "double"); %!test %! b = editDistance ("Octave", "octave"); %! assert_equal (b, 1); %! assert_equal (class (b), "double"); */ statistics-release-2.1.2/src/fcnn.cpp000066400000000000000000000572031527311104300175470ustar00rootroot00000000000000/* Copyright (C) 2024 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include #include #include #include #if defined (_OPENMP) #include #define MY_OMP_SET_THREADS (omp_set_num_threads (this->n_threads)) #else #define MY_OMP_SET_THREADS #endif using namespace std; // Helper functions // Return random number between -1 and 1, drawn from Octave's generator rather // than the C library's. The two are unconnected, so a network seeded from // rand () ignored rand ('seed', s) and rng () entirely and every fit on the // same data returned the same model. double get_random () { return octave::rand::scalar () * 2 - 1; } // Select the uniform distribution for as long as the object is in scope, and // put back whatever the caller had selected. The distribution is global state // owned by the caller, and an error raised while a network is being built must // not leave it changed. class uniform_scope { public: uniform_scope () : saved (octave::rand::distribution ()) { octave::rand::uniform_distribution (); } ~uniform_scope () { octave::rand::distribution (this->saved); } private: std::string saved; }; // Compute accuracy of predicted samples during training double accuracy (vector predictions, vector labels) { double correct = 0.0; for (size_t i = 0; i < predictions.size (); i++) { if (predictions[i] == labels[i] - 1) { correct += 1.0; } } double accuracy = correct / (double) predictions.size (); return accuracy; } // Class definitions // Negative-side parameters of the leaky family. MathWorks uses the same two // values, leakyReluLayer defaulting to a scale of 0.01 and eluLayer to an // alpha of 1, the latter being what makes an ELU saturate at -alpha. Neither // is a fitted quantity and neither is reachable from the m-code. static const double LRELU_ALPHA = 0.01; static const double ELU_ALPHA = 1.0; // Half-width of the uniform range a layer's weights are drawn from. A // constant range ignores how many inputs each neuron sums, so the variance of // a pre-activation grows with the width of the layer feeding it: deep networks // then saturate a sigmoid or drive every ReLU unit negative, where it stays // with a gradient of exactly zero. Scaling by fan-in holds that variance // steady from layer to layer. A rectifier passes only half its input, so it // needs the wider He range; the symmetric activations take Glorot, which // accounts for the backward pass through the layer as well. double init_scale (int activation, int fan_in, int fan_out) { bool rectifier = (activation == 2 || activation == 5 || activation == 6 || activation == 7); if (rectifier) { return sqrt (6.0 / fan_in); } return sqrt (6.0 / (fan_in + fan_out)); } class Neuron { public: // constructors Neuron (int input_size, double scale); Neuron (int input_size); // destructor ~Neuron (); // methods double forward (vector inputs); void backward (vector last_input, double grad); void descend (double learning_rate); vector get_neuron (); void set_neuron (vector Wb_vector); void zero_gradient (); // Flat view, laid out as [weights, bias] to match get_neuron. It exists // so that a whole network can be handed to an optimizer as one vector and // set back from one, which the per-neuron accessors above cannot do // without copying the model on every trial step of a line search. int nparams () const; void pack (double *p) const; void unpack (const double *p); void pack_grad (double *p) const; // data vector weights; vector wgrad; double bias; double bgrad; // The gradient this neuron received from the sample being processed, as // distinct from bgrad, which accumulates it. Backpropagation into the // layer below needs the one and full-batch training needs the other, and // reading bgrad for both is only correct while the gradient is cleared // between samples. double delta; }; Neuron::Neuron (int input_size, double scale) { this->weights = vector (input_size); this->wgrad = vector (input_size, 0.0); this->bias = 0.01 * get_random (); this->bgrad = 0.0; this->delta = 0.0; for (int i = 0; i < input_size; i++) { this->weights[i] = scale * get_random (); } } // Parameters left at zero, for a model that is about to have them loaded. // Drawing them would take numbers from Octave's generator and move the // caller's random stream on every call to fcnnpredict. Neuron::Neuron (int input_size) { this->weights = vector (input_size); this->wgrad = vector (input_size, 0.0); this->bias = 0.0; this->bgrad = 0.0; this->delta = 0.0; } Neuron::~Neuron () {} double Neuron::forward (vector inputs) { double total = this->bias; for (size_t i = 0; i < inputs.size (); i++) { total += inputs[i] * this->weights[i]; } return total; } void Neuron::backward (vector last_input, double grad) { this->delta = grad; this->bgrad += grad; for (size_t i = 0; i < this->wgrad.size (); i++) { this->wgrad.at (i) = this->wgrad.at (i) + grad * last_input.at (i); } } void Neuron::descend (double learning_rate) { this->bias -= this->bgrad * learning_rate; for (size_t i = 0; i < this->weights.size (); i++) { this->weights.at (i) -= this->wgrad.at (i) * learning_rate; } } vector Neuron::get_neuron () { vector Wb_vector = this->weights; Wb_vector.push_back (this->bias); return Wb_vector; } void Neuron::set_neuron (vector Wb_vector) { int w_len = Wb_vector.size () - 1; for (int i = 0; i < w_len; i++) { this->weights.at (i) = Wb_vector[i]; } this->bias = Wb_vector[w_len]; } void Neuron::zero_gradient () { this->wgrad = vector (this->weights.size ()); this->bgrad = 0.0; } int Neuron::nparams () const { return this->weights.size () + 1; } void Neuron::pack (double *p) const { int n = this->weights.size (); for (int i = 0; i < n; i++) { p[i] = this->weights[i]; } p[n] = this->bias; } void Neuron::unpack (const double *p) { int n = this->weights.size (); for (int i = 0; i < n; i++) { this->weights[i] = p[i]; } this->bias = p[n]; } void Neuron::pack_grad (double *p) const { int n = this->wgrad.size (); for (int i = 0; i < n; i++) { p[i] = this->wgrad[i]; } p[n] = this->bgrad; } class DenseLayer { public: // constructors DenseLayer (int input_size, int output_size, int activation); DenseLayer (int input_size, int output_size); // destructor ~DenseLayer (); // methods vector forward (vector inputs); void backward (vector grad); void descend (double learning_rate); vector> get_layer (); void set_layer (vector> Wb_matrix); void zero_gradient (); // Flat view over every neuron of the layer, in neuron order. int nparams () const; void pack (double *p) const; void unpack (const double *p); void pack_grad (double *p) const; // data vector neurons; vector last_input; }; DenseLayer::DenseLayer (int input_size, int output_size, int activation) { // initialize neurons on a range set by the fan-in and the activation double scale = init_scale (activation, input_size, output_size); this->neurons = vector (); for (int i = 0; i < output_size; i++) { Neuron to_add = Neuron (input_size, scale); this->neurons.push_back (to_add); } } // Layer of zeroed neurons, for a model whose weights are about to be loaded. DenseLayer::DenseLayer (int input_size, int output_size) { this->neurons = vector (); for (int i = 0; i < output_size; i++) { this->neurons.push_back (Neuron (input_size)); } } DenseLayer::~DenseLayer () {} vector DenseLayer::forward (vector inputs) { this->last_input = inputs; vector outputs = vector (this->neurons.size()); for (size_t i = 0; i < this->neurons.size (); i++) { outputs[i] = this->neurons[i].forward (inputs); } return outputs; } void DenseLayer::backward (vector grad) { for (size_t i = 0; i < this->neurons.size (); i++) { this->neurons[i].backward (last_input, grad[i]); } } void DenseLayer::descend (double learning_rate) { for (size_t i = 0; i < this->neurons.size (); i++) { this->neurons[i].descend (learning_rate); } } vector> DenseLayer::get_layer () { vector> Wb_matrix; for (size_t i = 0; i < this->neurons.size (); i++) { vector WB_vector = this->neurons[i].get_neuron (); Wb_matrix.push_back (WB_vector); } return Wb_matrix; } void DenseLayer::set_layer (vector> Wb_matrix) { for (size_t i = 0; i < Wb_matrix.size (); i++) { this->neurons[i].set_neuron (Wb_matrix[i]); } } void DenseLayer::zero_gradient () { for (size_t i = 0; i < this->neurons.size (); i++) { this->neurons[i].zero_gradient (); } } int DenseLayer::nparams () const { int total = 0; for (size_t i = 0; i < this->neurons.size (); i++) { total += this->neurons[i].nparams (); } return total; } void DenseLayer::pack (double *p) const { for (size_t i = 0; i < this->neurons.size (); i++) { this->neurons[i].pack (p); p += this->neurons[i].nparams (); } } void DenseLayer::unpack (const double *p) { for (size_t i = 0; i < this->neurons.size (); i++) { this->neurons[i].unpack (p); p += this->neurons[i].nparams (); } } void DenseLayer::pack_grad (double *p) const { for (size_t i = 0; i < this->neurons.size (); i++) { this->neurons[i].pack_grad (p); p += this->neurons[i].nparams (); } } // The activation functions a layer can carry, and the names the m-code spells // them with. The mapping lives here because this is the only place the codes // mean anything: nothing outside the engine needs to know the numbers. enum activation_kind { ACT_LINEAR = 0, ACT_SIGMOID = 1, ACT_RELU = 2, ACT_TANH = 3, ACT_SOFTMAX = 4, ACT_LRELU = 5, ACT_ELU = 6, ACT_GELU = 7 }; struct activation_entry { const char *name; int code; }; // 'none' is MATLAB's name for the identity, which fitcnet has always // documented as available; 'linear' is this package's older spelling of the // same map. 'prelu' and 'lrelu' are one function here, the negative slope // being a constant rather than a fitted parameter. static const activation_entry ACTIVATION_TABLE[] = { { "linear", ACT_LINEAR }, { "none", ACT_LINEAR }, { "sigmoid", ACT_SIGMOID }, { "relu", ACT_RELU }, { "tanh", ACT_TANH }, { "softmax", ACT_SOFTMAX }, { "lrelu", ACT_LRELU }, { "prelu", ACT_LRELU }, { "elu", ACT_ELU }, { "gelu", ACT_GELU } }; // The code for NAME, matched without regard to case, or -1 if there is none. static int activation_code (const string& name) { string key = name; for (size_t i = 0; i < key.size (); i++) { key[i] = tolower (key[i]); } const int n = sizeof (ACTIVATION_TABLE) / sizeof (ACTIVATION_TABLE[0]); for (int i = 0; i < n; i++) { if (key == ACTIVATION_TABLE[i].name) { return ACTIVATION_TABLE[i].code; } } return -1; } // One code per layer, the output layer last. ACTS names the hidden layers, // either as one character vector applying to all NLAYERS of them or as a // cellstring naming them one by one; OUT_ACT names the output layer. CALLER // prefixes every message, so each front end reports under its own name. static RowVector activation_codes (const octave_value& acts, const octave_value& out_act, int nlayers, const string& caller) { RowVector codes (nlayers + 1); if (acts.is_string () && acts.rows () == 1) { int code = activation_code (acts.string_value ()); if (code < 0) { error ("%s: unsupported 'Activations' function: '%s'.", caller.c_str (), acts.string_value ().c_str ()); } for (int i = 0; i < nlayers; i++) { codes(i) = code; } } else if (acts.iscellstr ()) { Cell c = acts.cell_value (); if (c.numel () != nlayers) { error ("%s: 'Activations' does not match the number of layers.", caller.c_str ()); } for (int i = 0; i < nlayers; i++) { int code = activation_code (c.elem(i).string_value ()); if (code < 0) { error ("%s: unsupported 'Activations' function: '%s'.", caller.c_str (), c.elem(i).string_value ().c_str ()); } codes(i) = code; } } else { error ("%s: 'Activations' must be a character vector or a cellstring.", caller.c_str ()); } if (! (out_act.is_string () && out_act.rows () == 1)) { error ("%s: 'OutputLayerActivation' must be a character vector.", caller.c_str ()); } int code = activation_code (out_act.string_value ()); if (code < 0) { error ("%s: unsupported 'OutputLayerActivation' function: '%s'.", caller.c_str (), out_act.string_value ().c_str ()); } codes(nlayers) = code; return codes; } class ActivationLayer { public: // constructor ActivationLayer (int activation, int n_threads); // destructor ~ActivationLayer (); // methods vector forward (vector inputs); void backward (vector grad); void backward (DenseLayer &prev_layer); // data vector last_input; vector grad; vector last_output; private: int activation; int n_threads; }; ActivationLayer::ActivationLayer (int activation, int n_threads) { this->activation = activation; this->n_threads = n_threads; } ActivationLayer::~ActivationLayer () {} vector ActivationLayer::forward (vector inputs) { this->last_input = inputs; int layer_size = inputs.size (); if (layer_size < 1000) { this->n_threads = 1; } vector outputs = vector (layer_size); if (this->activation == 0) // 'Linear' { outputs = inputs; } else if (this->activation == 1) // Sigmoid function { MY_OMP_SET_THREADS; #pragma omp parallel { #pragma omp parallel for for (int i = 0; i < layer_size; i++) { outputs[i] = 1 / (1 + exp (-inputs[i])); } } } else if (this->activation == 2) // Rectified Linear Unit (ReLU) { MY_OMP_SET_THREADS; #pragma omp parallel { #pragma omp parallel for for (int i = 0; i < layer_size; i++) { outputs[i] = inputs[i] > 0 ? inputs[i] : 0; } } } else if (this->activation == 3) // Hyperbolic tangent (tanh) { MY_OMP_SET_THREADS; #pragma omp parallel { #pragma omp parallel for for (int i = 0; i < layer_size; i++) { double ex = exp (inputs[i]); double e_x = exp (-inputs[i]); outputs[i] = (ex - e_x) / (ex + e_x); } } } else if (this->activation == 4) // Softmax activation { double total = 0.0; double maxel = *max_element (inputs.begin (), inputs.end ()); for (int i = 0; i < layer_size; i++) { outputs[i] = exp (inputs[i] - maxel); total += outputs[i]; } for (int i = 0; i < layer_size; i++) { outputs[i] /= total; } } else if (this->activation == 5) // Parametric or Leaky ReLU { MY_OMP_SET_THREADS; #pragma omp parallel { #pragma omp parallel for for (int i = 0; i < layer_size; i++) { outputs[i] = inputs[i] >= 0 ? inputs[i] : inputs[i] * LRELU_ALPHA; } } } else if (this->activation == 6) // Exponential Linear Unit (ELU) { MY_OMP_SET_THREADS; #pragma omp parallel { #pragma omp parallel for for (int i = 0; i < layer_size; i++) { outputs[i] = inputs[i] >= 0 ? inputs[i] : (exp (inputs[i]) - 1) * ELU_ALPHA; } } } else if (this->activation == 7) // Gaussian Error Linear Unit (GELU) { MY_OMP_SET_THREADS; #pragma omp parallel { #pragma omp parallel for for (int i = 0; i < layer_size; i++) { // x * Phi(x), the standard normal CDF taken from erfc so that the // tails are accurate; the tanh form is an approximation of this. outputs[i] = inputs[i] * 0.5 * erfc (-inputs[i] * M_SQRT1_2); } } } this->last_output = outputs; return outputs; } void ActivationLayer::backward (vector chain_grad) { int layer_size = this->last_input.size (); if (layer_size < 1000) { this->n_threads = 1; } this->grad = vector (layer_size); if (this->activation == 0) // 'Linear' { this->grad = chain_grad; } else if (this->activation == 1) // Sigmoid function { MY_OMP_SET_THREADS; #pragma omp parallel { #pragma omp parallel for for (int i = 0; i < layer_size; i++) { this->grad[i] = this->last_output[i] * (1 - this->last_output[i]) * chain_grad[i]; } } } else if (this->activation == 2) // Rectified Linear Unit (ReLU) { MY_OMP_SET_THREADS; #pragma omp parallel { #pragma omp parallel for for (int i = 0; i < layer_size; i++) { this->grad[i] = this->last_input[i] > 0 ? chain_grad[i] : 0; } } } else if (this->activation == 3) // Hyperbolic tangent (tanh) { MY_OMP_SET_THREADS; #pragma omp parallel { #pragma omp parallel for for (int i = 0; i < layer_size; i++) { this->grad[i] = (1 - pow (this->last_output[i], 2)) * chain_grad[i]; } } } else if (this->activation == 4) // Softmax activation { // The Jacobian is diag (y) - y * y', so the product with the incoming // gradient is y .* (g - y' * g). Passing g through unchanged is only // right when softmax is paired with a cross-entropy loss, which this // implementation does not provide. double dot = 0.0; for (int i = 0; i < layer_size; i++) { dot += this->last_output[i] * chain_grad[i]; } for (int i = 0; i < layer_size; i++) { this->grad[i] = this->last_output[i] * (chain_grad[i] - dot); } } else if (this->activation == 5) // Parametric or Leaky ReLU { MY_OMP_SET_THREADS; #pragma omp parallel { #pragma omp parallel for for (int i = 0; i < layer_size; i++) { this->grad[i] = this->last_input[i] >= 0 ? chain_grad[i] : chain_grad[i] * LRELU_ALPHA; } } } else if (this->activation == 6) // Exponential Linear Unit (ELU) { MY_OMP_SET_THREADS; #pragma omp parallel { #pragma omp parallel for for (int i = 0; i < layer_size; i++) { this->grad[i] = this->last_input[i] >= 0 ? chain_grad[i] : chain_grad[i] * exp (this->last_input[i]) * ELU_ALPHA; } } } else if (this->activation == 7) // Gaussian Error Linear Unit (GELU) { // d/dx [x * Phi(x)] = Phi(x) + x * phi(x), both taken at the input static const double inv_sqrt_2pi = 0.3989422804014327; MY_OMP_SET_THREADS; #pragma omp parallel { #pragma omp parallel for for (int i = 0; i < layer_size; i++) { double x = this->last_input[i]; double cdf = 0.5 * erfc (-x * M_SQRT1_2); double pdf = inv_sqrt_2pi * exp (-0.5 * x * x); this->grad[i] = (cdf + x * pdf) * chain_grad[i]; } } } } void ActivationLayer::backward (DenseLayer &prev_layer) { // The gradient arriving here is W' * delta, where delta is the gradient at // each neuron of the layer that follows. Every neuron stores the delta of // the sample being processed, so it is read from there rather than // recovered by dividing the weight gradient by the input, which is // undefined wherever an activation output is zero. Reading the bias // gradient instead is only correct while it is cleared between samples, // which full-batch training does not do. The local derivative is // then applied by the overload above, so both paths share one definition of // every activation. int layer_size = this->last_input.size (); vector chain_grad = vector (layer_size, 0.0); for (size_t n = 0; n < prev_layer.neurons.size (); n++) { double delta = prev_layer.neurons[n].delta; for (int i = 0; i < layer_size; i++) { chain_grad[i] += prev_layer.neurons[n].weights[i] * delta; } } this->backward (chain_grad); } class MeanSquaredErrorLoss { public: MeanSquaredErrorLoss (); ~MeanSquaredErrorLoss (); double forward (vector inputs, vector targets); void backward (double grad); // data vector last_input; vector last_target; vector grad; }; MeanSquaredErrorLoss::MeanSquaredErrorLoss () {} MeanSquaredErrorLoss::~MeanSquaredErrorLoss () {} double MeanSquaredErrorLoss::forward (vector inputs, vector targets) { // we only need to calculate the loss for the target class this->last_input = inputs; this->last_target = targets; double total = 0; for (size_t i = 0; i < inputs.size (); i++) { total += pow (inputs[i] - targets[i], 2); } double loss = total; return loss; } void MeanSquaredErrorLoss::backward (double grad) { this->grad = vector (this->last_input.size ()); for (size_t i = 0; i < this->last_input.size (); i++) { // d/dy of sum (y - t)^2 is 2 * (y - t); the bracket matters, since // 2 * y - t is a different function wherever the target is not zero. this->grad.at(i) = 2 * (this->last_input[i] - this->last_target[i]); this->grad.at(i) *= grad; } } // Cross-entropy loss, for an output layer that reports a probability over the // classes. Paired with softmax the composition of the two gradients reduces // to y - t, which is the pairing this loss exists for. class CrossEntropyLoss { public: CrossEntropyLoss (); ~CrossEntropyLoss (); double forward (vector inputs, vector targets); void backward (double grad); // data vector last_input; vector last_target; vector grad; }; CrossEntropyLoss::CrossEntropyLoss () {} CrossEntropyLoss::~CrossEntropyLoss () {} // A predicted probability of zero for the true class carries infinite loss, so // the logarithm and the division below are floored rather than allowed to // diverge. static const double CE_FLOOR = 1e-15; double CrossEntropyLoss::forward (vector inputs, vector targets) { this->last_input = inputs; this->last_target = targets; double total = 0.0; for (size_t i = 0; i < inputs.size (); i++) { if (targets[i] != 0.0) { total -= targets[i] * log (inputs[i] > CE_FLOOR ? inputs[i] : CE_FLOOR); } } return total; } void CrossEntropyLoss::backward (double grad) { this->grad = vector (this->last_input.size ()); for (size_t i = 0; i < this->last_input.size (); i++) { double y = this->last_input[i] > CE_FLOOR ? this->last_input[i] : CE_FLOOR; this->grad.at (i) = -this->last_target[i] / y; this->grad.at (i) *= grad; } } statistics-release-2.1.2/src/fcnnpredict.cc000066400000000000000000000337011527311104300207220ustar00rootroot00000000000000/* Copyright (C) 2024 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include #include #include #include #include "fcnn.cpp" using namespace std; DEFUN_DLD(fcnnpredict, args, nargout, "-*- texinfo -*-\n\ @deftypefn {statistics} {@var{pred_Y} =} fcnnpredict (@var{LayerWeights}, @\ @var{LayerBiases}, @var{Activations}, @var{OutputLayerActivation}, @var{XC})\n\ @deftypefnx {statistics} {@var{pred_Y} =} fcnnpredict @\ (@dots{}, @var{NumThreads})\n\ @deftypefnx {statistics} {[@var{pred_Y}, @var{scores}] =} fcnnpredict (@dots{})\n\ \n\ \n\ Make predictions from a fully connected Neural Network. \n\ \n\n\ \n\n\ @code{@var{pred_Y} = fcnnpredict (@var{LayerWeights}, @var{LayerBiases}, \ @var{Activations}, @var{OutputLayerActivation}, @var{XC})} requires the \ following input arguments.\ \n\n\ @itemize \n\ @item @var{LayerWeights} : A cell row vector holding one matrix per layer, \ each with one row per neuron of that layer and one column per input to it. \ \n\ \n\ @item @var{LayerBiases} : A cell row vector holding one bias column per \ layer, matching @var{LayerWeights} layer for layer and row for row. \ \n\ \n\ @item @var{Activations} : The activation function of the hidden layers, \ named as a character vector applying to all of them or as a cellstring naming \ them one by one. The supported names are listed under @code{fcnntrain}. \ \n\ \n\ @item @var{OutputLayerActivation} : The activation function of the output \ layer, named as a character vector. \ \n\ \n\ @item @var{XC} : An @math{NxM} matrix containing the data set to be predicted \ upon. Rows @math{N} correspond to individual samples and columns @math{M} \ correspond to features (dimensions). Type of @var{XC} must be double and the \ number of features must correspond to those of the trained model. \n\ @end itemize \n\ @code{fcnnpredict} can also be called with a sixth input argument, in which \ case, @var{NumThreads}, a positive scalar integer value, defines the number \ of threads to be used when computing the activation layers. For layers with \ less than 1000 neurons, @var{NumThreads} always defaults to 1. \ \n\ @code{fcnnpredict} returns the predicted labels, @var{pred_Y}, and if a second \ output argument is requested, it also returns the corresponding values of the \ neural networks output in @var{scores}. \ \n\ \n\ Installation Note: in order to support parallel processing on MacOS, users \ have to manually add support for OpenMP by adding the following flags to \ @qcode{CFLAGS} and @qcode{CXXFLAGS} prior to installing the statistics \ package:\n\n\ @code{setenv (\"CPPFLAGS\", \"-I/opt/homebrew/opt/libomp/include -Xclang -fopenmp\")} \ \n\ \n\ @seealso{fcnntrain, fitcnet, ClassificationNeuralNetwork} \n\ @end deftypefn") { // Check for correct number of input/output arguments if (args.length () < 5) { error ("fcnnpredict: too few input arguments."); } if (nargout > 2) { error ("fcnnpredict: too many output arguments."); } // The weights and the biases arrive as the classes hold them, one cell per // layer each, rather than as the augmented [W b] matrices fcnntrain emits. if (! args(0).iscell () || ! (args(0).rows () == 1 && args(0).columns () > 1)) { error ("fcnnpredict: 'LayerWeights' must be a cell row vector."); } Cell LayerWeights = args(0).cell_value (); if (! args(1).iscell () || ! (args(1).rows () == 1 && args(1).columns () > 1)) { error ("fcnnpredict: 'LayerBiases' must be a cell row vector."); } Cell LayerBiases = args(1).cell_value (); if (LayerBiases.numel () != LayerWeights.numel ()) { error ("fcnnpredict: 'LayerBiases' must match 'LayerWeights'."); } // The layer count comes from the weights, so the names alone say what each // layer computes: the hidden layers from 'Activations' and the last from // 'OutputLayerActivation'. RowVector ActiveCode = activation_codes (args(2), args(3), LayerWeights.numel () - 1, "fcnnpredict"); // Do some input validation while loading the testing data if (! args(4).isnumeric () || args(4).iscomplex ()) { error ("fcnnpredict: XC must be a real numeric matrix."); } if (args(4).isempty ()) { error ("fcnnpredict: XC cannot be empty."); } if (args(4).columns () != LayerWeights.elem(0).columns ()) { error ("fcnnpredict: the features in XC do not match the trained model."); } Matrix X = args(4).matrix_value (); int n = args(4).rows (); int d = args(4).columns (); // Check for optional sixth input argument to set number of threads int NumThreads = 1; if (args.length () == 6) { if (! args(5).is_scalar_type () || ! args(5).isnumeric () || args(5).scalar_value () < 1 || args(5).iscomplex ()) { error ("fcnnpredict: NumThreads must be a positive integer scalar value."); } NumThreads = args(5).scalar_value (); } // Construct 2D vector from data in XC vector> data (n, vector(d, 0)); for (int i = 0; i < n; i++) { for (int j = 0; j < d; j++) { data[i][j] = X(i,j); } } // Create a vector of layers sized appropriately vector WeightBias; vector Activation; int numlayers = LayerWeights.numel (); int input_size = d; int output_size = 0; for (int i = 0; i < numlayers; i++) { Matrix W = LayerWeights.elem(i).matrix_value (); Matrix B = LayerBiases.elem(i).matrix_value (); output_size = (int) W.rows (); if (B.numel () != output_size) { error ("fcnnpredict: 'LayerBiases' must match 'LayerWeights'."); } // set_layer wants each neuron as [weights, bias], which is how the layer // packs itself; the two cells are joined a row at a time here rather than // as a matrix in the m-code. vector> Wb_matrix; for (int r = 0; r < W.rows (); r++) { vector WB_vector; for (int c = 0; c < W.columns (); c++) { WB_vector.push_back (W(r,c)); } WB_vector.push_back (B(r)); Wb_matrix.push_back (WB_vector); } // Create dense layer and set its values DenseLayer DL = DenseLayer (input_size, output_size); DL.set_layer (Wb_matrix); // Create activation layer ActivationLayer AL = ActivationLayer (ActiveCode(i), NumThreads); WeightBias.push_back (DL); Activation.push_back (AL); input_size = output_size; } // Initialize Prediction and Score vector predictions = vector (); vector> scores; // Go through all testing samples for (int sample_idx = 0; sample_idx < n; sample_idx++) { vector sample = data[sample_idx]; // Forward pass for (int layer_idx = 0; layer_idx < numlayers; layer_idx++) { sample = WeightBias[layer_idx].forward (sample); sample = Activation[layer_idx].forward (sample); } // Save scores scores.push_back (sample); // Get the prediction for this sample and store it to vector int prediction = 0; // Search for highest value for (int j = 0; j < output_size; j++) { if (sample[j] > sample[prediction]) { prediction = j; } } predictions.push_back (prediction + 1); } // Store predicted labels in ColumnVector ColumnVector Y_pred(n); for (int sample_idx = 0; sample_idx < n; sample_idx++) { Y_pred(sample_idx) = predictions[sample_idx]; } // Store predicted scores in Matrix Matrix Y_scores(n,output_size); for (int r = 0; r < n; r++) { for (int c = 0; c < output_size; c++) { Y_scores(r,c) = scores[r][c]; } } // Prepare returning arguments octave_value_list retval (nargout); retval(0) = Y_pred; if (nargout > 0) { retval(1) = Y_scores; } return retval; } /* %!shared X, Y, MODEL, W, B %! load fisheriris %! X = meas; %! Y = grp2idx (species); %! MODEL = fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, 0.025, 100, false); %! W = cellfun (@(m) m(:,1:end-1), MODEL.LayerWeights, "UniformOutput", false); %! B = cellfun (@(m) m(:,end), MODEL.LayerWeights, "UniformOutput", false); %!test %! [Y_pred, Y_scores] = fcnnpredict (W, B, "sigmoid", "sigmoid", X); %! assert_equal (numel (Y_pred), numel (Y)); %! assert_equal (isequal (size (Y_pred), size (Y)), true); %! assert_equal (columns (Y_scores), numel (unique (Y))); %! assert_equal (rows (Y_scores), numel (Y)); ## A trained network drives its outputs to the targets, not to a fraction of ## them. A gradient of 2*y-t rather than 2*(y-t) settles at y = t/2, which ## leaves every label right and every score halved, so the scores are what ## has to be checked. %!test %! rand ("seed", 42); %! randn ("seed", 42); %! Xs = [randn(40,2)*0.3 + 3; randn(40,2)*0.3 - 3]; %! Ys = [ones(40,1); 2*ones(40,1)]; %! M = fcnntrain (Xs, Ys, [8, 8], "sigmoid", "sigmoid", 1, 0.05, 400, false); %! Wm = cellfun (@(m) m(:,1:end-1), M.LayerWeights, "UniformOutput", false); %! Bm = cellfun (@(m) m(:,end), M.LayerWeights, "UniformOutput", false); %! [pred, scores] = fcnnpredict (Wm, Bm, "sigmoid", "sigmoid", [3, 3; -3, -3]); %! assert_equal (pred, [1; 2]); %! assert_equal (max (scores(1,:)) > 0.8, true); %! assert_equal (max (scores(2,:)) > 0.8, true); %! assert_equal (all (abs (sum (scores, 2) - 1) < 0.1), true); ## Cross entropy with a softmax output trains at least as confidently as the ## mean squared error does, and its rows are a probability by construction. %!test %! rand ("seed", 42); %! randn ("seed", 42); %! Xs = [randn(40,2)*0.3 + 3; randn(40,2)*0.3 - 3]; %! Ys = [ones(40,1); 2*ones(40,1)]; %! Mm = fcnntrain (Xs, Ys, [8, 8], "sigmoid", "softmax", 1, 0.05, 400, false, 0); %! Mc = fcnntrain (Xs, Ys, [8, 8], "sigmoid", "softmax", 1, 0.05, 400, false, 1); %! Wm = cellfun (@(m) m(:,1:end-1), Mm.LayerWeights, "UniformOutput", false); %! Bm = cellfun (@(m) m(:,end), Mm.LayerWeights, "UniformOutput", false); %! Wc = cellfun (@(m) m(:,1:end-1), Mc.LayerWeights, "UniformOutput", false); %! Bc = cellfun (@(m) m(:,end), Mc.LayerWeights, "UniformOutput", false); %! [pm, sm] = fcnnpredict (Wm, Bm, "sigmoid", "softmax", [3, 3; -3, -3]); %! [pc, sc] = fcnnpredict (Wc, Bc, "sigmoid", "softmax", [3, 3; -3, -3]); %! assert_equal (pm, [1; 2]); %! assert_equal (pc, [1; 2]); %! assert_equal (all (abs (sum (sc, 2) - 1) < 1e-8), true); %! assert_equal (min (max (sc, [], 2)) >= min (max (sm, [], 2)), true); ## Omitting the loss selector keeps the mean squared error. %!test %! rand ("seed", 42); %! randn ("seed", 42); %! Xs = [randn(20,2)*0.3 + 3; randn(20,2)*0.3 - 3]; %! Ys = [ones(20,1); 2*ones(20,1)]; %! rand ("seed", 1); %! M9 = fcnntrain (Xs, Ys, 6, "sigmoid", "sigmoid", 1, 0.05, 100, false); %! rand ("seed", 1); %! M0 = fcnntrain (Xs, Ys, 6, "sigmoid", "sigmoid", 1, 0.05, 100, false, 0); %! assert_equal (M9.Loss, M0.Loss, 0); %!error ... %! fcnnpredict (W, B, "sigmoid", "sigmoid"); %!error ... %! [Q, E, R] = fcnnpredict (W, B, "sigmoid", "sigmoid", X); %!error ... %! fcnnpredict (1, B, "sigmoid", "sigmoid", X); %!error ... %! fcnnpredict ({1}, B, "sigmoid", "sigmoid", X); %!error ... %! fcnnpredict ({1; 2; 3}, B, "sigmoid", "sigmoid", X); %!error ... %! fcnnpredict (W, 1, "sigmoid", "sigmoid", X); %!error ... %! fcnnpredict (W, {1}, "sigmoid", "sigmoid", X); %!error ... %! fcnnpredict (W, [B, B], "sigmoid", "sigmoid", X); %!error ... %! fcnnpredict (W, {B{1}(1:end-1), B{2}}, "sigmoid", "sigmoid", X); %!error ... %! fcnnpredict (W, B, 2, "sigmoid", X); %!error ... %! fcnnpredict (W, B, {2, 2}, "sigmoid", X); %!error ... %! fcnnpredict (W, B, {"sigmoid", "relu"}, "sigmoid", X); %!error ... %! fcnnpredict (W, B, "sgmoid", "sigmoid", X); %!error ... %! fcnnpredict (W, B, "sigmoid", 4, X); %!error ... %! fcnnpredict (W, B, "sigmoid", "softmx", X); %!error ... %! fcnnpredict (W, B, "sigmoid", "sigmoid", complex (X)); %!error ... %! fcnnpredict (W, B, "sigmoid", "sigmoid", {1, 2, 3, 4}); %!error ... %! fcnnpredict (W, B, "sigmoid", "sigmoid", "asd"); %!error ... %! fcnnpredict (W, B, "sigmoid", "sigmoid", []); %!error ... %! fcnnpredict (W, B, "sigmoid", "sigmoid", X(:,[1:3])); %!error ... %! fcnnpredict (W, B, "sigmoid", "sigmoid", X, 0); */ statistics-release-2.1.2/src/fcnntrain.cc000066400000000000000000001202711527311104300204040ustar00rootroot00000000000000/* Copyright (C) 2024 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include #include #include #include #include "fcnn.cpp" #include "lbfgs.h" using namespace std; // fitcnet's wording for each stopping criterion, measured on R2024a. It // lives here and not in lbfgs.h because MATLAB words the same criterion // differently per function: fitclinear reports "Tolerance on gradient // satisfied." where this reports "Relative gradient tolerance reached.", // so the engine hands out a token and every caller supplies its own prose. static const char * criterion_message (int crit) { switch (crit) { case lbfgs::CRIT_GRADIENT: return "Relative gradient tolerance reached."; case lbfgs::CRIT_STEP: return "Step size tolerance reached."; case lbfgs::CRIT_LOSS: return "Loss tolerance reached."; case lbfgs::CRIT_ITERATION_LIMIT: return "Iteration limit reached."; default: return "Line search could not improve the objective."; } } // Full-batch objective: the mean loss over every training sample together // with its gradient against the flat parameter vector. One call is one sweep // forward and back over the whole training set, which is what each trial step // of a line search costs. // // This is the piece the epoch loop cannot supply. That loop clears the // gradient before every sample and consumes it immediately, so no gradient of // the summed loss ever exists; here it is cleared once and accumulated across // the batch. class fcnn_objective { public: fcnn_objective (vector& wb, vector& act, const vector>& data, const vector>& targets, const vector& labels, int output_size, int lossfcn, bool regression, const vector& obs_weights) : m_wb (wb), m_act (act), m_data (data), m_targets (targets), m_labels (labels), m_output_size (output_size), m_lossfcn (lossfcn), m_regression (regression), m_w (obs_weights) { } double operator () (const vector& w, vector& g) { octave_quit (); const int numlayers = m_wb.size (); const int n = m_data.size (); const double *p = w.data (); for (int l = 0; l < numlayers; l++) { m_wb[l].unpack (p); p += m_wb[l].nparams (); } for (int l = 0; l < numlayers; l++) { m_wb[l].zero_gradient (); } double total = 0.0; for (int s = 0; s < n; s++) { vector sample = m_data[s]; for (int l = 0; l < numlayers; l++) { sample = m_wb[l].forward (sample); sample = m_act[l].forward (sample); } vector label_vector; if (m_regression) { label_vector = m_targets[s]; } else { label_vector = vector (m_output_size, 0.0); label_vector[m_labels[s]-1] = 1.0; // labels in Y start from 1 } // The objective is the mean, so each sample's gradient carries 1/n, // or the weighted mean, each sample's gradient carrying its weight. // The two are kept apart so that an unweighted fit keeps its arithmetic. vector loss_grad; if (m_lossfcn == 1) { CrossEntropyLoss loss = CrossEntropyLoss (); double lval = loss.forward (sample, label_vector); if (m_w.empty ()) { total += lval; loss.backward (1.0 / n); } else { total += m_w[s] * lval; loss.backward (m_w[s]); } loss_grad = loss.grad; } else { MeanSquaredErrorLoss loss = MeanSquaredErrorLoss (); double lval = loss.forward (sample, label_vector); if (m_w.empty ()) { total += lval; loss.backward (1.0 / n); } else { total += m_w[s] * lval; loss.backward (m_w[s]); } loss_grad = loss.grad; } m_act[numlayers-1].backward (loss_grad); for (int l = numlayers; l > 0; l--) { m_wb[l-1].backward (m_act[l-1].grad); if (l > 1) { m_act[l-2].backward (m_wb[l-1]); } } } double *q = &g[0]; for (int l = 0; l < numlayers; l++) { m_wb[l].pack_grad (q); q += m_wb[l].nparams (); } return m_w.empty () ? total / n : total; } private: vector& m_wb; vector& m_act; const vector>& m_data; const vector>& m_targets; const vector& m_labels; int m_output_size; int m_lossfcn; bool m_regression; // One weight per sample summing to one, or empty for the plain mean const vector& m_w; }; DEFUN_DLD(fcnntrain, args, nargout, "-*- texinfo -*-\n\ @deftypefn {statistics} {@var{Mdl} =} fcnntrain (@var{X}, @var{Y}, @\ @var{LayerSizes}, @var{Activations}, @var{OutputLayerActivation}, @\ @var{NumThreads}, @var{LearningRate}, @var{Epochs}, @var{DisplayInfo})\n\ @deftypefnx {statistics} {@var{Mdl} =} fcnntrain (@dots{}, @\ @var{LossFunction})\n\ @deftypefnx {statistics} {@var{Mdl} =} fcnntrain (@dots{}, @\ @var{LossFunction}, @var{SolverOptions})\n\ \n\ \n\ Train a fully connected Neural Network. \n\ \n\n\ @code{@var{Mdl} = fcnntrain (@dots{})} requires the following input arguments.\ \n\n\ @itemize \n\ @item @var{X} : An @math{NxM} matrix containing the data set to be trained \ upon. Rows @math{N} correspond to individual samples and columns @math{M} \ correspond to features (dimensions). Type of @var{X} must be double. \ \n\ \n\ @item @var{Y} : An @math{Nx1} column vector containing the labels of the \ training dataset. The labels must be natural numbers (positive integers) \ starting from 1 up to the number of classes, similarly as returned by the \ `grp2idx` function. Type of @var{Y} must be double. Under regression, \ selected by @var{LossFunction} 2, @var{Y} is instead an @math{NxR} matrix of \ response values, which may take any finite value, and the output layer is \ sized to its @math{R} columns rather than to a number of classes. \ \n\ \n\ @item @var{LayerSizes} : A numeric row vector of integer values defining the \ size of the hidden layers of the network. Input and output layers are \ automatically determined by the training data and their labels. \ \n\ \n\ @item @var{Activations} : The activation function of the hidden layers, named \ as a character vector applying to all of them or as a cellstring naming them \ one by one, in which case it must have one name per hidden layer. The \ supported names are: \n\ @itemize \n\ @item @qcode{'linear'} or @qcode{'none'} : the identity \n\ @item @qcode{'sigmoid'} \n\ @item @qcode{'relu'} : rectified linear unit \n\ @item @qcode{'tanh'} : hyperbolic tangent \n\ @item @qcode{'softmax'} \n\ @item @qcode{'lrelu'} or @qcode{'prelu'} : leaky rectified linear unit, whose \ negative slope is a constant 0.01 \n\ @item @qcode{'elu'} : exponential linear unit, saturating at @math{-1} \n\ @item @qcode{'gelu'} : Gaussian error linear unit \n\ @end itemize \n\ \n\ \n\ @item @var{OutputLayerActivation} : The activation function of the output \ layer, named as a character vector from the same list. \ \n\ \n\ @item @var{NumThreads} : A positive scalar integer value defining the number \ of threads used for computing the activation layers. For layers with less \ than 1000 neurons, @var{NumThreads} always defaults to 1. \ \n\ \n\ @item @var{LearningRate} : A positive scalar value defining the learning rate \ used by the gradient descend algorithm during training. \ \n\ \n\ @item @var{Epochs} : A positive scalar value defining the number of epochs for \ training the model. \ \n\ \n\ @item @var{DisplayInfo} : A boolean scalar indicating whether to print \ information during training. \n\ @end itemize \n\ \n\ @code{@var{Mdl} = fcnntrain (@dots{}, @var{LossFunction})} also selects the \ loss the network is trained against. @var{LossFunction} is a scalar: 0 for \ mean squared error over a one-hot target, which is the default, 1 for cross \ entropy, and 2 for mean squared error over a continuous response. Cross \ entropy expects the output layer to report a probability over the classes, \ so it belongs with a softmax output; paired that way the two gradients \ compose to @math{y - t}. Its loss is undefined where the predicted \ probability of the true class is zero, so both the logarithm and its \ derivative are floored. Code 2 is regression: @var{Y} holds response values \ rather than labels, the output layer belongs with the identity activation, \ and the returned model carries no @code{Accuracy} field, there being no \ labels to count. \n\ \n\ \n\ @code{@var{Mdl} = fcnntrain (@dots{}, @var{LossFunction}, \ @var{SolverOptions})} \ also takes a scalar structure setting the solver. Its @qcode{Solver} field is \ @qcode{'sgd'}, the epoch loop and the default, or @qcode{'lbfgs'}, whose \ @qcode{GradientTolerance}, @qcode{LossTolerance}, @qcode{StepTolerance} and \ @qcode{HistorySize} fields tune it. Its @qcode{Weights} field holds one \ nonnegative weight per row of @var{X}; they are scaled to sum to one and the \ loss becomes the weighted mean over the observations, while without it every \ observation weighs the same. \n\ \n\ \n\ @code{fcnntrain} returns the trained model, @var{Mdl}, as a structure \ containing the following fields: \ \n\ \n\ @itemize \n\ @item @code{LayerWeights} : A cell array with each element containing a matrix \ with the Weights and Biases of each layer including the output layer.\n\ \n\ \n\ @item @code{Activations} : A numeric row vector of integer values defining the \ activation functions to be used at each layer including the output layer. \ \n\ \n\ @item @code{Accuracy} : The prediction accuracy at each iteration during the \ neural network model's training process. Absent under regression. \ \n\ \n\ @item @code{Loss} : The loss value recorded at each iteration during the \ neural network model's training process. \ \n\ \n\ @end itemize \ \n\ \n\ Installation Note: in order to support parallel processing on MacOS, users \ have to manually add support for OpenMP by adding the following flags to \ @qcode{CFLAGS} and @qcode{CXXFLAGS} prior to installing the statistics \ package:\n\n\ @code{setenv (\"CPPFLAGS\", \"-I/opt/homebrew/opt/libomp/include -Xclang -fopenmp\")} \ \n\ \n\ @seealso{fcnnpredict, fitcnet, ClassificationNeuralNetwork} \n\ @end deftypefn") { // Check for correct number of input/output arguments if (args.length () < 9) { error ("fcnntrain: too few input arguments."); } if (args.length () > 11) { error ("fcnntrain: too many input arguments."); } if (nargout > 1) { error ("fcnntrain: too many output arguments."); } // Optional tenth argument selecting the loss: 0 for mean squared error over // a one-hot target, which is the default and what earlier releases always // used, 1 for cross entropy, which expects the output layer to report a // probability, and 2 for mean squared error over a continuous response, // which is regression. It is read first because it decides whether Y holds // class labels or response values. int lossfcn = 0; if (args.length () >= 10) { if (! args(9).isnumeric () || ! args(9).is_scalar_type () || args(9).iscomplex ()) { error ("fcnntrain: 'LossFunction' must be a numeric scalar value."); } lossfcn = args(9).int_value (); if (lossfcn < 0 || lossfcn > 2) { error ("fcnntrain: invalid 'LossFunction' code."); } } bool regression = (lossfcn == 2); // Do some input validation while loading training data and labels if (! args(0).isnumeric () || args(0).iscomplex ()) { error ("fcnntrain: X must be a real numeric matrix."); } if (args(0).isempty ()) { error ("fcnntrain: X cannot be empty."); } if (! args(1).isnumeric () || args(1).iscomplex ()) { error ("fcnntrain: Y must be a real numeric matrix."); } if (args(1).isempty ()) { error ("fcnntrain: Y cannot be empty."); } if (args(0).rows () != args(1).rows ()) { error ("fcnntrain: X and Y must have the same number of rows."); } // Construct 2D vector from data in X int n = args(0).rows (); int d = args(0).columns (); vector> data (n, vector(d, 0)); Matrix X = args(0).matrix_value (); for (int i = 0; i < n; i++) { for (int j = 0; j < d; j++) { data[i][j] = X(i,j); } } // Construct the training targets from Y. Under regression Y holds the // response itself, one column per response, and is taken as it stands; // otherwise it holds class labels, which index a one-hot target built below. vector labels; vector> targets; int n_response = 0; if (regression) { n_response = args(1).columns (); Matrix R = args(1).matrix_value (); targets = vector> (n, vector (n_response, 0.0)); for (int i = 0; i < n; i++) { for (int j = 0; j < n_response; j++) { if (! octave::math::isfinite (R(i,j))) { error ("fcnntrain: Y must be finite."); } targets[i][j] = R(i,j); } } } else { labels = vector (n, 0); ColumnVector Y = args(1).column_vector_value (); for (int i = 0; i < n; i++) { labels[i] = Y(i); if (labels[i] < 1) { error ("fcnntrain: labels in Y must be positive integers."); } } } // Check LayerSizes and Activations input arguments if (! args(2).isnumeric () || args(2).iscomplex () || args(2).isempty () || args(2).rows () != 1) { error ("fcnntrain: 'LayerSizes' must be a row vector of integer values."); } // Check the NumThreads input argument if (! args(5).is_scalar_type () || ! args(5).isnumeric () || args(5).scalar_value () < 1 || args(5).iscomplex ()) { error ("fcnntrain: 'NumThreads' must be a positive integer scalar value."); } int NumThreads = args(5).scalar_value (); // Create a vector of layers sized appropriately. The initial weights are // drawn from Octave's generator, so rand ('seed', s) reproduces a fit. uniform_scope draw_uniform; vector WeightBias; vector Activation; RowVector LayerSizes = args(2).row_vector_value (); RowVector ActiveCode = activation_codes (args(3), args(4), args(2).numel (), "fcnntrain"); int numlayers = args(2).numel () + 1; int input_size = d; for (int i = 0; i < args(2).numel (); i++) { int output_size = (int) LayerSizes(i); if (output_size < 1) { error ("fcnntrain: cannot have a layer of zero size."); } int code = ActiveCode(i); DenseLayer DL = DenseLayer (input_size, output_size, code); ActivationLayer AL = ActivationLayer (code, NumThreads); WeightBias.push_back (DL); Activation.push_back (AL); input_size = output_size; } // Push back last dense layer int output_size = regression ? n_response : (int) set (labels.begin (), labels.end ()).size (); int last_AC = args(2).numel (); DenseLayer DL = DenseLayer (input_size, output_size, (int) ActiveCode(last_AC)); ActivationLayer AL = ActivationLayer (ActiveCode(last_AC), NumThreads); WeightBias.push_back (DL); Activation.push_back (AL); // Input validation on LearningRate, Epochs, and DisplayInfo if (! args(6).is_scalar_type () || ! args(6).isnumeric ()) { error ("fcnntrain: 'LearningRate' must be a positive scalar value."); } double learning_rate = args(6).scalar_value (); if (learning_rate <= 0) { error ("fcnntrain: 'LearningRate' must be a positive scalar value."); } if (! args(7).is_scalar_type () || ! args(7).isnumeric ()) { error ("fcnntrain: 'Epochs' must be a positive scalar value."); } if (args(7).scalar_value () < 1) { error ("fcnntrain: 'Epochs' must be a positive scalar value."); } if (! args(8).is_bool_scalar ()) { error ("fcnntrain: 'DisplayInfo' must be a boolean scalar."); } // Initialize return variables octave_idx_type max_epochs = args(7).idx_type_value (); // Reserved, not sized: these are filled with push_back below, and sizing // them here would leave max_epochs zeros in front of the values and the // reported history reading back as all zero. // Optional eleventh argument: the solver and its tolerances, as a scalar // struct. Absent, the epoch loop below runs exactly as it always has, and // every default in this file is unchanged. bool use_lbfgs = false; // Observation weights, summing to one; empty leaves every sample the same vector obs_weights; lbfgs::options lbopt; lbopt.iteration_limit = max_epochs; lbfgs::result lbres = {}; if (args.length () > 10) { if (! args(10).isstruct () || args(10).numel () != 1) { error ("fcnntrain: 'SolverOptions' must be a scalar struct."); } octave_scalar_map so = args(10).scalar_map_value (); if (so.isfield ("Solver")) { std::string sv = so.contents ("Solver").string_value (); if (sv == "lbfgs") { use_lbfgs = true; } else if (sv != "sgd") { error ("fcnntrain: 'Solver' must be 'sgd' or 'lbfgs'."); } } if (so.isfield ("GradientTolerance")) { lbopt.gradient_tolerance = so.contents ("GradientTolerance").double_value (); } if (so.isfield ("LossTolerance")) { lbopt.loss_tolerance = so.contents ("LossTolerance").double_value (); } if (so.isfield ("StepTolerance")) { lbopt.step_tolerance = so.contents ("StepTolerance").double_value (); } if (so.isfield ("HistorySize")) { lbopt.history_size = so.contents ("HistorySize").int_value (); } if (so.isfield ("Weights")) { octave_value wval = so.contents ("Weights"); if (! wval.isnumeric () || wval.iscomplex () || wval.numel () != n) { error ("fcnntrain: 'Weights' must hold one number per row of X."); } ColumnVector wv = wval.column_vector_value (); double wsum = 0.0; for (int i = 0; i < n; i++) { if (! (wv(i) >= 0.0) || ! std::isfinite (wv(i))) { error ("fcnntrain: 'Weights' must be nonnegative and finite."); } wsum += wv(i); } if (! (wsum > 0.0)) { error ("fcnntrain: 'Weights' must not be all zero."); } obs_weights.resize (n); for (int i = 0; i < n; i++) { obs_weights[i] = wv(i) / wsum; } } } vector Accuracy; vector Loss; Accuracy.reserve (max_epochs); Loss.reserve (max_epochs); // Order the samples are visited in, reshuffled every epoch below. Visiting // them in a fixed order makes every update for one class precede the first // update for the next whenever the labels arrive sorted, which is what // grp2idx returns, and the weights then swing between the classes instead of // settling. vector order (n); for (int i = 0; i < n; i++) { order[i] = i; } // The epoch loop is the stochastic solver. LBFGS replaces it whole rather // than adjusting it: it needs the gradient of the summed loss, which no // per-sample update can produce, so the two paths share the network and // nothing else. The sample order is not shuffled here, the fit being // deterministic once the weights are drawn. if (use_lbfgs) { int nparams = 0; for (int layer_idx = 0; layer_idx < numlayers; layer_idx++) { nparams += WeightBias[layer_idx].nparams (); } vector w (nparams, 0.0); double *p = &w[0]; for (int layer_idx = 0; layer_idx < numlayers; layer_idx++) { WeightBias[layer_idx].pack (p); p += WeightBias[layer_idx].nparams (); } fcnn_objective fobj (WeightBias, Activation, data, targets, labels, output_size, lossfcn, regression, obs_weights); lbres = lbfgs::minimize (fobj, w, lbopt); // minimize leaves the network holding whatever the last trial step set, // which is the accepted point only by construction of the line search; // writing the returned vector back makes that explicit. const double *q = w.data (); for (int layer_idx = 0; layer_idx < numlayers; layer_idx++) { WeightBias[layer_idx].unpack (q); q += WeightBias[layer_idx].nparams (); } if (args(8).scalar_value () != 0) { cout << "Iterations: " << lbres.iterations << " | Loss: " << lbres.fval << " | " << criterion_message (lbres.crit) << endl; } } else { // Start training octave_idx_type epoch = 0; for (; epoch < max_epochs; epoch++) { // Fisher-Yates over Octave's generator, so that a seeded fit stays // reproducible for (int i = n - 1; i > 0; i--) { int j = (int) (octave::rand::scalar () * (i + 1)); if (j > i) // scalar () is documented on [0, 1); guard the end { j = i; } int keep = order[i]; order[i] = order[j]; order[j] = keep; } // Running loss, for the progress line only: the weights move under it, so // it is not the loss of any one network and is never recorded. double running_loss = 0.0; // Go through all training samples for (int visit = 0; visit < n; visit++) { int sample_idx = order[visit]; vector sample = data[sample_idx]; // Forward pass for (int layer_idx = 0; layer_idx < numlayers; layer_idx++) { sample = WeightBias[layer_idx].forward (sample); sample = Activation[layer_idx].forward (sample); } vector label_vector; if (regression) { label_vector = targets[sample_idx]; } else { label_vector = vector (output_size); label_vector[labels[sample_idx]-1] = 1.0; // Labels in Y start from 1 } // Compute loss and the gradient it hands back. A weighted sample's // step is scaled by n times its weight, which is exactly one when // no weights were given. double step = obs_weights.empty () ? 1.0 : n * obs_weights[sample_idx]; double loss_output; vector loss_grad; if (lossfcn == 1) { CrossEntropyLoss loss = CrossEntropyLoss (); loss_output = loss.forward (sample, label_vector); loss.backward (step); loss_grad = loss.grad; } else { MeanSquaredErrorLoss loss = MeanSquaredErrorLoss (); loss_output = loss.forward (sample, label_vector); loss.backward (step); loss_grad = loss.grad; } running_loss += loss_output; // Print output if (args(8).scalar_value () != 0) { if (visit % 500 == 0) { cout << setprecision(4) << "i:" << visit << " | Mean Loss: "; cout << (running_loss / (visit + 1)) << "\r" << flush; } } // Backward pass for (int layer_idx = 0; layer_idx < numlayers; layer_idx++) { WeightBias[layer_idx].zero_gradient (); // Reset gradients to zero } // Compute gradients Activation[numlayers-1].backward (loss_grad); for (int layer_idx = numlayers; layer_idx > 0; layer_idx--) { WeightBias[layer_idx-1].backward (Activation[layer_idx-1].grad); if (layer_idx > 1) { Activation[layer_idx-2].backward (WeightBias[layer_idx-1]); } } // Update weights for (int layer_idx = 0; layer_idx < numlayers; layer_idx++) { WeightBias[layer_idx].descend (learning_rate); } } // Loss and accuracy of the network as it stands at the end of the // epoch, measured in one forward-only pass with the weights held // still. Summing them inside the loop above instead scores every // sample against different weights, so the figure belongs to no // network that ever existed: on class-interleaved data a network stuck // on a constant output reports an accuracy of exactly zero, each sample // being scored against weights just pulled toward the one before it. double sum_loss = 0.0; vector predictions = vector (); predictions.reserve (n); for (int sample_idx = 0; sample_idx < n; sample_idx++) { vector sample = data[sample_idx]; for (int layer_idx = 0; layer_idx < numlayers; layer_idx++) { sample = WeightBias[layer_idx].forward (sample); sample = Activation[layer_idx].forward (sample); } vector label_vector; if (regression) { label_vector = targets[sample_idx]; } else { int prediction = 0; // Search for highest value for (int j = 0; j < output_size; j++) { if (sample[j] > sample[prediction]) { prediction = j; } } predictions.push_back (prediction); label_vector = vector (output_size); label_vector[labels[sample_idx]-1] = 1.0; } // The recorded loss is the weighted mean when weights were given double share = obs_weights.empty () ? 1.0 : n * obs_weights[sample_idx]; if (lossfcn == 1) { CrossEntropyLoss loss = CrossEntropyLoss (); sum_loss += share * loss.forward (sample, label_vector); } else { MeanSquaredErrorLoss loss = MeanSquaredErrorLoss (); sum_loss += share * loss.forward (sample, label_vector); } } // Accuracy counts correct labels, which regression has none of. double A = regression ? 0.0 : accuracy (predictions, labels); double L = sum_loss / n; Accuracy.push_back (A); Loss.push_back (L); // Print output if (args(8).scalar_value () != 0) { cout << " \r" << "Epoch: " << epoch + 1 << " | Loss: " << L; if (! regression) { cout << " | Train Accuracy: " << A; } cout << endl; } } } // Get weights and biases from each layer and store them in a cell array Cell LayerWeights(1, numlayers); for (int layer_idx = 0; layer_idx < numlayers; layer_idx++) { DenseLayer DL = WeightBias[layer_idx]; vector> Wb_matrix = DL.get_layer (); vector WB_vector = Wb_matrix[0]; octave_idx_type row = Wb_matrix.size (); octave_idx_type col = WB_vector.size (); Matrix WB (row, col); for (int r = 0; r < row; r++) { for (int c = 0; c < col; c++) { WB(r,c) = Wb_matrix[r][c]; } } LayerWeights.elem(layer_idx) = WB; } // The recorded history: one row per epoch under the stochastic solver, one // per iteration under LBFGS, which also reports the two quantities it // measured to decide it had converged and which of them stopped it. // Accuracy is not among them, MATLAB not reporting it either, and computing // it would cost a forward pass over the whole set at every iteration. octave_idx_type nrec = use_lbfgs ? (octave_idx_type) lbres.history.size () : (octave_idx_type) Loss.size (); RowVector A(nrec), L(nrec), G(nrec), S(nrec); for (octave_idx_type i = 0; i < nrec; i++) { if (use_lbfgs) { L(i) = lbres.history[i].fval; G(i) = lbres.history[i].gradient; S(i) = lbres.history[i].step; } else { A(i) = Accuracy[i]; L(i) = Loss[i]; } } // Prepare returning arguments octave_scalar_map fcnn_model; fcnn_model.assign ("LayerWeights", LayerWeights); fcnn_model.assign ("Activations", ActiveCode); if (use_lbfgs) { fcnn_model.assign ("Loss", L); fcnn_model.assign ("Gradient", G); fcnn_model.assign ("Step", S); fcnn_model.assign ("Criterion", criterion_message (lbres.crit)); } else { if (! regression) { fcnn_model.assign ("Accuracy", A); } fcnn_model.assign ("Loss", L); } octave_value_list retval (1); retval(0) = fcnn_model; return retval; } /* %!shared X, Y, MODEL %! load fisheriris %! X = meas; %! Y = grp2idx (species); %!error ... %! model = fcnntrain (X, Y); %!error ... %! [Q, W] = fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, 0.025, 50, false); %!error ... %! fcnntrain (complex (X), Y, 10, "sigmoid", "sigmoid", 1, 0.025, 50, false); %!error ... %! fcnntrain ({X}, Y, 10, "sigmoid", "sigmoid", 1, 0.025, 50, false); %!error ... %! fcnntrain ([], Y, 10, "sigmoid", "sigmoid", 1, 0.025, 50, false); %!error ... %! fcnntrain (X, complex (Y), 10, "sigmoid", "sigmoid", 0.01, 0.025, 50, false); %!error ... %! fcnntrain (X, {Y}, 10, "sigmoid", "sigmoid", 1, 0.025, 50, false); %!error ... %! fcnntrain (X, [], 10, "sigmoid", "sigmoid", 1, 0.025, 50, false); %!error ... %! fcnntrain (X, Y([1:50]), 10, "sigmoid", "sigmoid", 1, 0.025, 50, false); %!error ... %! fcnntrain (X, Y - 1, 10, "sigmoid", "sigmoid", 1, 0.025, 50, false); %!error ... %! fcnntrain (X, Y, [10; 5], "sigmoid", "sigmoid", 1, 0.025, 50, false); %!error ... %! fcnntrain (X, Y, "10", "sigmoid", "sigmoid", 1, 0.025, 50, false); %!error ... %! fcnntrain (X, Y, {10}, "sigmoid", "sigmoid", 1, 0.025, 50, false); %!error ... %! fcnntrain (X, Y, complex (10), "sigmoid", "sigmoid", 1, 0.025, 50, false); %!error ... %! fcnntrain (X, Y, 10, [1; 1], "sigmoid", 1, 0.025, 50, false); %!error ... %! fcnntrain (X, Y, 10, {1, 1}, "sigmoid", 1, 0.025, 50, false); %!error ... %! fcnntrain (X, Y, 10, complex ([1, 1]), "sigmoid", 1, 0.025, 50, false); %!error ... %! fcnntrain (X, Y, 10, {"sigmoid", "relu"}, "sigmoid", 1, 0.025, 50, false); %!error ... %! fcnntrain (X, Y, [10, 0, 5], "sigmoid", "sigmoid", 1, 0.025, 50, false); %!error ... %! fcnntrain (X, Y, 10, "sgmoid", "sigmoid", 1, 0.025, 50, false); %!error ... %! fcnntrain (X, Y, 10, {"bogus"}, "sigmoid", 1, 0.025, 50, false); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", 4, 1, 0.025, 50, false); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "softmx", 1, 0.025, 50, false); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 0, 0.025, 50, false); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, -0.025, 50, false); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, 0, 50, false); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, [0.025, 0.001], 50, false); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, {0.025}, 50, false); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, 0.025, 0, false); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, 0.025, [50, 25], false); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, 0.025, 50, 0); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, 0.025, 50, 1); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, 0.025, 50, [false, false]); ## The reported training history has one entry per epoch and holds the values ## actually recorded, not the zeros a pre-sized vector would leave in front of ## them. %!test %! rand ('seed', 42); %! randn ('seed', 42); %! Xs = [randn(30,2)*0.4 + 2; randn(30,2)*0.4 - 2]; %! Ys = [ones(30,1); 2*ones(30,1)]; %! M = fcnntrain (Xs, Ys, 8, "relu", "softmax", 1, 0.05, 60, false, 1); %! assert_equal (numel (M.Loss), 60); %! assert_equal (numel (M.Accuracy), 60); %! assert_equal (any (M.Loss != 0), true); %! assert_equal (M.Loss(end) < M.Loss(1), true); %! assert_equal (M.Accuracy(end) >= M.Accuracy(1), true); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, 0.025, 50, false, 0, struct (), 0); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, 0.025, 50, false, 0, 0); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, 0.025, 50, false, 0, ... %! struct ("Solver", "bogus")); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, 0.025, 50, false, 'ce'); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, 0.025, 50, false, [0, 1]); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, 0.025, 50, false, 3); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, 0.025, 50, false, -1); ## Loss function 2 is regression: Y holds the response, the output layer is ## sized to its columns, and no Accuracy is reported because there are no ## labels to count. %!test %! rand ('seed', 42); %! randn ('seed', 42); %! Xr = linspace (-2, 2, 60)'; %! Yr = 3 * Xr - 1; %! M = fcnntrain (Xr, Yr, [8, 8], "relu", "linear", 1, 0.005, 300, false, 2); %! assert_equal (fieldnames (M), {'LayerWeights'; 'Activations'; 'Loss'}); %! assert_equal (rows (M.LayerWeights{end}), 1); %! assert_equal (M.Loss(end) < M.Loss(1), true); ## The recorded loss is the mean squared error of the network it belongs to. %!test %! rand ('seed', 42); %! Xr = linspace (0, 1, 40)'; %! Yr = 5 * Xr + 2; %! M = fcnntrain (Xr, Yr, 10, "relu", "linear", 1, 0.005, 200, false, 2); %! Wm = cellfun (@(m) m(:,1:end-1), M.LayerWeights, "UniformOutput", false); %! Bm = cellfun (@(m) m(:,end), M.LayerWeights, "UniformOutput", false); %! [~, yFit] = fcnnpredict (Wm, Bm, "relu", "linear", Xr); %! assert_equal (M.Loss(end), mean ((Yr - yFit) .^ 2), 1e-12); ## A response of several columns gets one output unit per column. %!test %! rand ('seed', 42); %! Xr = linspace (0, 1, 30)'; %! M = fcnntrain (Xr, [Xr, 2 * Xr, 3 * Xr], 6, "relu", "linear", 1, 0.005, ... %! 50, false, 2); %! assert_equal (rows (M.LayerWeights{end}), 3); %! Wm = cellfun (@(m) m(:,1:end-1), M.LayerWeights, "UniformOutput", false); %! Bm = cellfun (@(m) m(:,end), M.LayerWeights, "UniformOutput", false); %! [~, yFit] = fcnnpredict (Wm, Bm, "relu", "linear", Xr); %! assert_equal (columns (yFit), 3); ## Regression takes a response the classification path would refuse. %!test %! rand ('seed', 42); %! Xr = linspace (0, 1, 20)'; %! Yr = linspace (-3.5, 2.25, 20)'; %! M = fcnntrain (Xr, Yr, 6, "relu", "linear", 1, 0.005, 50, false, 2); %! assert_equal (all (isfinite (M.Loss)), true); %!error ... %! fcnntrain ([1; 2; 3], [1; Inf; 3], 4, "relu", "linear", 1, 0.01, 10, false, 2); %!error ... %! fcnntrain ([1; 2; 3], [1; NaN; 3], 4, "relu", "linear", 1, 0.01, 10, false, 2); ## The full-batch solver drives the loss down and reports what it measured to ## decide it had stopped. Accuracy is not among them: MATLAB does not report ## it either, and it would cost a forward pass over the whole set per ## iteration. %!test %! so = struct ("Solver", "lbfgs"); %! M = fcnntrain (X, Y, 10, "relu", "softmax", 1, 0.005, 100, false, 1, so); %! assert_equal (fieldnames (M), ... %! {'LayerWeights'; 'Activations'; 'Loss'; 'Gradient'; ... %! 'Step'; 'Criterion'}); %! assert_equal (M.Loss(end) < M.Loss(1), true); %! assert_equal (numel (M.Gradient), numel (M.Loss)); %! assert_equal (numel (M.Step), numel (M.Loss)); ## It reaches a lower training loss than the epoch loop does, in fewer passes ## over the data, which is the whole reason for offering it. %!test %! rand ("state", 3); randn ("state", 3); %! Ms = fcnntrain (X, Y, 10, "relu", "softmax", 1, 0.005, 200, false, 1); %! rand ("state", 3); randn ("state", 3); %! so = struct ("Solver", "lbfgs"); %! Ml = fcnntrain (X, Y, 10, "relu", "softmax", 1, 0.005, 200, false, 1, so); %! assert_equal (Ml.Loss(end) < Ms.Loss(end), true); %! assert_equal (numel (Ml.Loss) < numel (Ms.Loss), true); ## An explicit 'sgd' is the epoch loop, unchanged. %!test %! rand ("state", 5); randn ("state", 5); %! Ma = fcnntrain (X, Y, 10, "relu", "softmax", 1, 0.005, 30, false, 1); %! rand ("state", 5); randn ("state", 5); %! so = struct ("Solver", "sgd"); %! Mb = fcnntrain (X, Y, 10, "relu", "softmax", 1, 0.005, 30, false, 1, so); %! assert_equal (Mb.Loss, Ma.Loss); %! assert_equal (Mb.LayerWeights, Ma.LayerWeights); ## The tolerances reach the solver: from the same starting weights, a loose ## gradient tolerance stops sooner than a tight one. %!test %! rand ("state", 9); randn ("state", 9); %! so = struct ("Solver", "lbfgs", "GradientTolerance", 1e3); %! Ma = fcnntrain (X, Y, 10, "relu", "softmax", 1, 0.005, 100, false, 1, so); %! rand ("state", 9); randn ("state", 9); %! so = struct ("Solver", "lbfgs", "GradientTolerance", 1e-8); %! Mb = fcnntrain (X, Y, 10, "relu", "softmax", 1, 0.005, 100, false, 1, so); %! assert_equal (Ma.Criterion, "Relative gradient tolerance reached."); %! assert_equal (numel (Ma.Loss) < numel (Mb.Loss), true); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, 0.025, 50, false, 0, ... %! struct ("Weights", ones (5, 1))); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, 0.025, 50, false, 0, ... %! struct ("Weights", -ones (150, 1))); %!error ... %! fcnntrain (X, Y, 10, "sigmoid", "sigmoid", 1, 0.025, 50, false, 0, ... %! struct ("Weights", zeros (150, 1))); %!test %! ## A weight of two trains as the sample given twice %! w = ones (150, 1); %! w(1:10) = 2; %! so = struct ("Solver", "lbfgs"); %! rand ("seed", 3); %! A = fcnntrain (X, Y, 3, "sigmoid", "softmax", 1, 0.01, 50, false, 1, ... %! setfield (so, "Weights", w)); %! rand ("seed", 3); %! B = fcnntrain ([X; X(1:10,:)], [Y; Y(1:10)], 3, "sigmoid", "softmax", 1, ... %! 0.01, 50, false, 1, so); %! assert_equal (A.Loss, B.Loss, 1e-12); */ statistics-release-2.1.2/src/gam.cpp000066400000000000000000000551121527311104300173640ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ // The spline engine shared by the GAM learners. Both fitting schemes refit // the same predictor at the same breaks and the same order in every round and // vary only the response, so the design matrix of each predictor is fixed for // the whole fit. It is built and factorised once here, which reduces a round // to two products against the factors; the m-code it replaces called splinefit // inside the loop, where the fit was dominated not by the solve but by the // interpreter's overhead on very small arrays. // The spline space is the one splinefit builds: a B-spline basis of order N // over BREAKS, evaluated by the same recursion, so the piecewise polynomial // returned here is splinefit's to rounding. What is not reproduced is its // option surface -- periodic boundaries, robust fitting and linear constraints // are not used by either learner and are absent. #include #include #include #include #include using namespace std; // The interval a value falls in, as lookup (BREAKS, V, "lr") returns it: the // last break at or below V, clamped to the first and last piece so that a // point outside the domain is extrapolated from the nearest polynomial rather // than dropped. Returned 0-based. octave_idx_type gam_interval (const RowVector& breaks, double v) { octave_idx_type pieces = breaks.numel () - 1; octave_idx_type lo = 0; octave_idx_type hi = pieces - 1; if (! (v >= breaks(1))) // false for NaN as well, which lands on piece 0 { return 0; } if (v >= breaks(pieces - 1)) { return pieces - 1; } while (hi - lo > 1) { octave_idx_type mid = (lo + hi) / 2; if (v >= breaks(mid)) { lo = mid; } else { hi = mid; } } return lo; } // A basis of the spline space of order N over BREAKS, held as the polynomial // coefficients of the N basis functions that are nonzero on each piece. struct SplineBasis { RowVector breaks; // 1 x (pieces + 1) Matrix bcoefs; // (N * pieces) x N, row (p * N + i) is basis i on p octave_idx_type n; // spline order, the number of coefficients a piece octave_idx_type pieces; octave_idx_type dim; // pieces + n - 1, the dimension of the space }; // Breaks from a piece count, interpolated linearly from the sorted data as // splinefit does. X must already be sorted ascending and hold no NaN. RowVector gam_breaks_from_count (const ColumnVector& x, octave_idx_type p) { octave_idx_type mx = x.numel (); RowVector breaks (p + 1); if (x(0) < x(mx - 1)) { for (octave_idx_type k = 0; k <= p; k++) { // linspace (1, mx, p+1), in the m-code's 1-based indexing double ib = (p == 0) ? 1.0 : 1.0 + (double) k * (mx - 1) / p; if (k == p) { ib = (double) mx; // linspace pins its last point exactly } octave_idx_type ibin = (octave_idx_type) floor (ib); if (ibin < 1) { ibin = 1; } if (ibin > mx - 1) { ibin = mx - 1; } double dx = x(ibin) - x(ibin - 1); breaks(k) = x(ibin - 1) + dx * (ib - ibin); } } else { // Every observation the same: splinefit spreads unit breaks from it for (octave_idx_type k = 0; k <= p; k++) { breaks(k) = x(0) + ((p == 0) ? 0.0 : (double) k / p); } } return breaks; } // Drop breaks that do not increase, which interpolation from tied data can // produce, matching splinefit's unique () pass. RowVector gam_unique_breaks (const RowVector& breaks) { octave_idx_type nb = breaks.numel (); bool monotone = true; for (octave_idx_type k = 1; k < nb; k++) { if (breaks(k) <= breaks(k - 1)) { monotone = false; break; } } if (monotone) { return breaks; } vector v (breaks.data (), breaks.data () + nb); sort (v.begin (), v.end ()); v.erase (unique (v.begin (), v.end ()), v.end ()); RowVector out (v.size ()); for (size_t k = 0; k < v.size (); k++) { out(k) = v[k]; } return out; } // Build the B-spline basis of order N over BREAKS. This is splinefit's // splinebase, transliterated: the breaks are extended periodically by DEG on // each side, the basis is generated by repeated antidifferentiation and // normalisation, and the pieces added for the extension are dropped again. SplineBasis gam_splinebase (const RowVector& breaks0, octave_idx_type n) { SplineBasis base; base.breaks = breaks0; base.n = n; base.pieces = breaks0.numel () - 1; base.dim = base.pieces + n - 1; octave_idx_type deg = n - 1; octave_idx_type pieces = base.pieces; // Extended breaks vector br; if (deg > 0) { vector h (pieces); for (octave_idx_type k = 0; k < pieces; k++) { h[k] = breaks0(k + 1) - breaks0(k); } vector hcopy = h; while ((octave_idx_type) hcopy.size () < deg) { hcopy.insert (hcopy.end (), h.begin (), h.end ()); } // To the left: bl(t) = breaks(1) - cumsum (hcopy(end:-1:end-deg+1))(t), // laid down in reverse so the sequence stays increasing. vector bl (deg); double acc = breaks0(0); for (octave_idx_type t = 0; t < deg; t++) { acc -= hcopy[hcopy.size () - 1 - t]; bl[t] = acc; } for (octave_idx_type t = deg - 1; t >= 0; t--) { br.push_back (bl[t]); } for (octave_idx_type k = 0; k <= pieces; k++) { br.push_back (breaks0(k)); } // And to the right acc = breaks0(pieces); for (octave_idx_type t = 0; t < deg; t++) { acc += hcopy[t]; br.push_back (acc); } pieces = (octave_idx_type) br.size () - 1; } else { for (octave_idx_type k = 0; k <= pieces; k++) { br.push_back (breaks0(k)); } } vector h (pieces); for (octave_idx_type k = 0; k < pieces; k++) { h[k] = br[k + 1] - br[k]; } // H(p * n + i) is the spacing of the piece basis function i reaches from p vector H (n * pieces); for (octave_idx_type p = 0; p < pieces; p++) { for (octave_idx_type i = 0; i < n; i++) { octave_idx_type q = p + i; H[p * n + i] = h[q < pieces ? q : pieces - 1]; } } Matrix coefs (n * pieces, n, 0.0); for (octave_idx_type p = 0; p < pieces; p++) { coefs(p * n, 0) = 1.0; } vector Q (n * pieces); for (octave_idx_type k = 1; k < n; k++) // k is the m-code's k - 1 { // Antiderivatives of the splines of the previous order for (octave_idx_type j = 0; j < k; j++) { for (octave_idx_type r = 0; r < n * pieces; r++) { coefs(r, j) = coefs(r, j) * H[r] / (k - j); } } // Q, cumulated down each piece, is the antiderivative at the break above for (octave_idx_type p = 0; p < pieces; p++) { double run = 0.0; for (octave_idx_type i = 0; i < n; i++) { double s = 0.0; for (octave_idx_type j = 0; j < n; j++) { s += coefs(p * n + i, j); } run += s; Q[p * n + i] = run; } } for (octave_idx_type p = 0; p < pieces; p++) { coefs(p * n, k) = 0.0; for (octave_idx_type i = 1; i < n; i++) { coefs(p * n + i, k) = Q[p * n + i - 1]; } } // Normalise by the value the antiderivative reaches over the piece for (octave_idx_type p = 0; p < pieces; p++) { double fmax = Q[p * n + n - 1]; for (octave_idx_type i = 0; i < n; i++) { for (octave_idx_type j = 0; j <= k; j++) { coefs(p * n + i, j) /= fmax; } } } // Difference of adjacent antiderivatives. Row r reads row r + n - 1, // which is never one this pass has already written, so ascending order is // the simultaneous assignment the m-code performs. for (octave_idx_type r = 0; r < n * pieces - deg; r++) { for (octave_idx_type j = 0; j <= k; j++) { coefs(r, j) -= coefs(r + n - 1, j); } } for (octave_idx_type p = 0; p < pieces; p++) { coefs(p * n, k) = 0.0; } } // Scale for the width of each piece vector scale (n * pieces, 1.0); for (octave_idx_type k = 1; k < n; k++) { for (octave_idx_type r = 0; r < n * pieces; r++) { scale[r] /= H[r]; coefs(r, n - k - 1) *= scale[r]; } } // Drop the pieces the extension added, keeping for each remaining piece the // N basis functions that are nonzero on it. octave_idx_type kept = pieces - 2 * deg; base.bcoefs = Matrix (n * kept, n, 0.0); for (octave_idx_type p = 0; p < kept; p++) { for (octave_idx_type i = 0; i < n; i++) { octave_idx_type src = n * (p + 1) + i * deg - 1; for (octave_idx_type j = 0; j < n; j++) { base.bcoefs(p * n + i, j) = coefs(src, j); } } } return base; } // One predictor's design, factorised once for the whole fit. class SplineFitter { public: SplineBasis basis; Matrix Ur; // used x rank, the left singular vectors Matrix Wr; // dim x rank, V scaled by 1 / sigma Array used; // rows that carry finite data octave_idx_type nobs; bool all_used; // Fit one response: PRED is its projection on the spline space at every // observation, U the B-spline coefficients of that projection. Rows that // were dropped predict NaN, which is what evaluating the fitted spline at a // missing predictor returns. void fit_round (const ColumnVector& y, ColumnVector& pred, ColumnVector& u) const { octave_idx_type m = Ur.rows (); octave_idx_type r = Ur.columns (); ColumnVector z (r, 0.0); for (octave_idx_type k = 0; k < r; k++) { double s = 0.0; for (octave_idx_type i = 0; i < m; i++) { s += Ur(i, k) * y(all_used ? i : used(i)); } z(k) = s; } pred = ColumnVector (nobs, octave::numeric_limits::NaN ()); for (octave_idx_type i = 0; i < m; i++) { double s = 0.0; for (octave_idx_type k = 0; k < r; k++) { s += Ur(i, k) * z(k); } pred(all_used ? i : used(i)) = s; } u = ColumnVector (basis.dim, 0.0); for (octave_idx_type i = 0; i < basis.dim; i++) { double s = 0.0; for (octave_idx_type k = 0; k < r; k++) { s += Wr(i, k) * z(k); } u(i) = s; } } // The piecewise polynomial coefficients of a spline given as B-spline // coefficients: on piece p the polynomial is the combination of the N basis // functions that reach it. Matrix coefs_from_u (const ColumnVector& u) const { octave_idx_type n = basis.n; Matrix coefs (basis.pieces, n, 0.0); for (octave_idx_type p = 0; p < basis.pieces; p++) { for (octave_idx_type j = 0; j < n; j++) { double s = 0.0; for (octave_idx_type i = 0; i < n; i++) { s += u(p + i) * basis.bcoefs(p * n + i, j); } coefs(p, j) = s; } } return coefs; } }; // Build the fitter for one predictor. KNOTS is either a piece count or an // explicit break vector, ORD is splinefit's order (the polynomial degree). // ROWOK marks the observations whose predictor and response are both finite; // the breaks are interpolated from those alone, as splinefit interpolates them // from the data left after it has dropped the rest. SplineFitter gam_make_fitter (const ColumnVector& x, const RowVector& knots, octave_idx_type ord, const boolNDArray& rowok, const string& caller) { SplineFitter F; octave_idx_type n = ord + 1; F.nobs = x.numel (); octave_idx_type nused = 0; for (octave_idx_type i = 0; i < F.nobs; i++) { if (rowok(i) && ! octave::math::isnan (x(i))) { nused++; } } if (nused == 0) { error ("%s: there must be at least one data point.", caller.c_str ()); } F.used = Array (dim_vector (nused, 1)); octave_idx_type c = 0; for (octave_idx_type i = 0; i < F.nobs; i++) { if (rowok(i) && ! octave::math::isnan (x(i))) { F.used(c++) = i; } } F.all_used = (nused == F.nobs); ColumnVector xs (nused); for (octave_idx_type i = 0; i < nused; i++) { xs(i) = x(F.used(i)); } RowVector breaks; if (knots.numel () == 1) { ColumnVector xsorted = xs; sort (xsorted.fortran_vec (), xsorted.fortran_vec () + nused); breaks = gam_breaks_from_count (xsorted, (octave_idx_type) knots(0)); } else { breaks = knots; } breaks = gam_unique_breaks (breaks); if (breaks.numel () < 2) { error ("%s: at least two unique breaks are required.", caller.c_str ()); } F.basis = gam_splinebase (breaks, n); // Design matrix: row i holds the N basis functions that are nonzero at // x(i), at the columns their B-spline indices name. Matrix D (nused, F.basis.dim, 0.0); for (octave_idx_type i = 0; i < nused; i++) { octave_idx_type p = gam_interval (breaks, xs(i)); double dx = xs(i) - breaks(p); for (octave_idx_type k = 0; k < n; k++) { double v = 0.0; for (octave_idx_type j = 0; j < n; j++) { v = v * dx + F.basis.bcoefs(p * n + k, j); } D(i, p + k) = v; } } // Factorise once. The fit is the minimum-norm least squares solution, which // is what splinefit's u = y / A takes wherever the design has full rank. // // Where it does not -- fewer observations than basis functions, tied // predictor values, or a piece no observation falls in -- the solution is // not unique, and the direction that makes it so must be discarded rather // than divided by. Octave's operator keeps every singular value above // eps / 2, which on such a design means keeping one that is rounding noise // and scaling it by its reciprocal: on the four-observation fixture in // ClassificationGAM the coefficients came back at 1e14 and the residual at // 1.45, where no spline of that space can do worse than 0.71. The cut here // is the rank-revealing one, max (rows, columns) * eps, which rank () itself // uses. It changes nothing for a design of full rank and gives the genuine // minimum-norm fit for one that is deficient. octave::math::svd fact (D, octave::math::svd::Type::economy, octave::math::svd::Driver::GESVD); Matrix U = fact.left_singular_matrix (); Matrix V = fact.right_singular_matrix (); DiagMatrix S = fact.singular_values (); octave_idx_type nsv = S.rows () < S.columns () ? S.rows () : S.columns (); double smax = (nsv > 0) ? S(0, 0) : 0.0; octave_idx_type dmax = (nused > F.basis.dim) ? nused : F.basis.dim; double tol = smax * std::numeric_limits::epsilon () * dmax; octave_idx_type rank = 0; while (rank < nsv && S(rank, rank) > tol) { rank++; } F.Ur = Matrix (nused, rank); for (octave_idx_type i = 0; i < nused; i++) { for (octave_idx_type k = 0; k < rank; k++) { F.Ur(i, k) = U(i, k); } } F.Wr = Matrix (F.basis.dim, rank); for (octave_idx_type i = 0; i < F.basis.dim; i++) { for (octave_idx_type k = 0; k < rank; k++) { F.Wr(i, k) = V(i, k) / S(k, k); } } return F; } // Evaluate a piecewise polynomial, as ppval does: Horner on the offset from // the interval's left break, with points outside the domain extrapolated from // the nearest piece. double gam_ppval (const RowVector& breaks, const Matrix& coefs, double v) { if (octave::math::isnan (v)) { return octave::numeric_limits::NaN (); } octave_idx_type p = gam_interval (breaks, v); double dx = v - breaks(p); double y = 0.0; for (octave_idx_type j = 0; j < coefs.columns (); j++) { y = y * dx + coefs(p, j); } return y; } // Pack the per-predictor splines as the 1 x P struct array of pp forms the // classdefs store and ppval consumes. octave_map gam_pack_params (const vector& breaks, const vector& coefs) { octave_idx_type p = (octave_idx_type) breaks.size (); Cell c_form (1, p), c_breaks (1, p), c_coefs (1, p); Cell c_pieces (1, p), c_order (1, p), c_dim (1, p); for (octave_idx_type j = 0; j < p; j++) { c_form(j) = octave_value ("pp"); c_breaks(j) = octave_value (breaks[j]); c_coefs(j) = octave_value (coefs[j]); c_pieces(j) = octave_value ((double) coefs[j].rows ()); c_order(j) = octave_value ((double) coefs[j].columns ()); c_dim(j) = octave_value (1.0); } octave_map params (dim_vector (1, p)); params.assign ("form", c_form); params.assign ("breaks", c_breaks); params.assign ("coefs", c_coefs); params.assign ("pieces", c_pieces); params.assign ("order", c_order); params.assign ("dim", c_dim); return params; } // What a fit produces, in the shape the learners store it. struct GamFit { octave_map params; ColumnVector res; Matrix RSS; double iterations; double intercept; }; // Build one fitter per predictor. Every round of either scheme works in // these same spaces, so this is the whole of the per-fit setup. vector gam_fitters (const Matrix& X, const ColumnVector& Y, const Matrix& knots, const Matrix& order, const string& caller) { octave_idx_type n = X.rows (); octave_idx_type d = X.columns (); boolNDArray rowok (dim_vector (n, 1)); for (octave_idx_type i = 0; i < n; i++) { rowok(i) = ! octave::math::isnan (Y(i)); } vector F; F.reserve (d); for (octave_idx_type j = 0; j < d; j++) { ColumnVector x (n); for (octave_idx_type i = 0; i < n; i++) { x(i) = X(i, j); } RowVector k (1); k(0) = knots(j); F.push_back (gam_make_fitter (x, k, (octave_idx_type) order(j), rowok, caller)); } return F; } // Gradient boosting on the log-odds, the scheme ClassificationGAM fits. Every // round takes one step of the negative gradient of the log loss and adds a // spline of it to each additive term; the terms accumulate because every round // fits the same breaks at the same order, so their polynomials add. GamFit gam_boost (const Matrix& X, const ColumnVector& Y, double Inter, const Matrix& knots, const Matrix& order, double lrate, octave_idx_type niter) { octave_idx_type n = X.rows (); octave_idx_type d = X.columns (); vector F = gam_fitters (X, Y, knots, order, "ClassificationGAM"); vector breaks (d); vector coefs (d); for (octave_idx_type j = 0; j < d; j++) { breaks[j] = F[j].basis.breaks; coefs[j] = Matrix (F[j].basis.pieces, F[j].basis.n, 0.0); } double intercept = log (Inter / (1 - Inter)); ColumnVector f (n, intercept); ColumnVector grad (n), pred, u; for (octave_idx_type it = 0; it < niter; it++) { for (octave_idx_type i = 0; i < n; i++) { grad(i) = Y(i) - 1.0 / (1.0 + exp (-f(i))); } ColumnVector fnew (n, 0.0); for (octave_idx_type j = 0; j < d; j++) { F[j].fit_round (grad, pred, u); Matrix rc = F[j].coefs_from_u (u); for (octave_idx_type p = 0; p < rc.rows (); p++) { for (octave_idx_type c = 0; c < rc.columns (); c++) { coefs[j](p, c) += lrate * rc(p, c); } } for (octave_idx_type i = 0; i < n; i++) { fnew(i) += lrate * pred(i); } } for (octave_idx_type i = 0; i < n; i++) { f(i) += fnew(i); } } GamFit out; out.res = ColumnVector (n); double rss = 0.0; for (octave_idx_type i = 0; i < n; i++) { out.res(i) = Y(i) - 1.0 / (1.0 + exp (-f(i))); rss += out.res(i) * out.res(i); } out.RSS = Matrix (1, 1, rss); out.params = gam_pack_params (breaks, coefs); out.iterations = (double) niter; out.intercept = intercept; return out; } // Backfitting, the scheme RegressionGAM fits. A cycle takes each predictor's // own contribution back out of the partial residual, refits it, and puts the // new one in; it stops when no term's residual sum of squares moves by more // than TOL. GamFit gam_backfit (const Matrix& X, const ColumnVector& Y, double Inter, const Matrix& knots, const Matrix& order, double tol, octave_idx_type maxiter) { octave_idx_type n = X.rows (); octave_idx_type d = X.columns (); vector F = gam_fitters (X, Y, knots, order, "RegressionGAM"); vector breaks (d); vector coefs (d); for (octave_idx_type j = 0; j < d; j++) { breaks[j] = F[j].basis.breaks; coefs[j] = Matrix (F[j].basis.pieces, F[j].basis.n, 0.0); } ColumnVector res (n); for (octave_idx_type i = 0; i < n; i++) { res(i) = Y(i) - Inter; } Matrix RSS (1, d, 0.0); Matrix RSSk (1, d, 0.0); vector contrib (d); ColumnVector pred, u; bool converged = false; octave_idx_type iter = 0; while (! (converged || iter > maxiter)) { iter++; for (octave_idx_type j = 0; j < d; j++) { if (iter > 1) { for (octave_idx_type i = 0; i < n; i++) { res(i) += contrib[j](i); } } F[j].fit_round (res, pred, u); coefs[j] = F[j].coefs_from_u (u); // The m-code's own formula, sum of absolute deviations squared over the // sample size. It is not a residual sum of squares despite the name and // is reproduced as it stands: it decides only when the loop stops. double s = 0.0; for (octave_idx_type i = 0; i < n; i++) { s += fabs (Y(i) - pred(i) - Inter); } RSSk(j) = fabs (s * s) / n; contrib[j] = pred; for (octave_idx_type i = 0; i < n; i++) { res(i) -= pred(i); } } converged = true; for (octave_idx_type j = 0; j < d; j++) { if (! (fabs (RSS(j) - RSSk(j)) <= tol)) { converged = false; } } RSS = RSSk; } GamFit out; out.res = res; out.RSS = RSS; out.params = gam_pack_params (breaks, coefs); out.iterations = (double) iter; out.intercept = Inter; return out; } statistics-release-2.1.2/src/gamboost.cpp000066400000000000000000001644661527311104300204500ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ // The boosted-tree engine shared by the GAM learners, beside the spline engine // // This unit is #included by gamboosttrain.cc, gamboostpredict.cc, // gamboostpairs.cc and gamboostinter.cc, each of which // calls a different subset of it, so a helper unused in one is used in // another. Hence [[maybe_unused]] on those definitions: they are shared, // not dead, and removing one breaks a sibling oct-file. // in gam.cpp. A generalized additive model is additive by construction, so // every tree of the predictor phase splits on one predictor only. That is // what makes this small: no multivariate tree is needed, no surrogate splits, // no pruning, and the whole fitted shape function of a predictor is a step // function over that predictor's bins, whatever number of trees produced it. // // The scheme is Newton boosting on the deviance. Each round fits one tree per // predictor to the current gradient and Hessian, adds it at the running step // size, and recentres: a shape function is held to mean zero and the constant // it gives up is absorbed into the intercept, which is the usual GAM // identifiability convention and is why a fitted intercept moves away from the // value it was seeded with. // // The step is not fixed. When a round fails to reduce the deviance by enough // it is retried at half the step, and the fit stops when even the reduced step // cannot improve it. That is what MATLAB reports as "Unable to improve the // model fit" against "Terminated after training the requested number of // trees", measured on R2024a. The constants below are ours: the tolerance // MATLAB stops on is not recoverable from anything it reports, so this engine // documents its own rather than pretending to match one it cannot read. #include #include #include #include #include // The largest number of cut points a predictor is binned at. MATLAB reports // 255 edges for any predictor carrying more distinct values than that // (measured at 600 and at 2000), so a predictor with fewer distinct values is // split at every midpoint between them and a richer one at 255 quantiles. static const octave_idx_type GAMB_MAX_EDGES = 255; // How many rounds are allowed to pass without a meaningful improvement before // the fit is declared unable to improve, and what counts as meaningful, // relative to the deviance the window opened at. This is the patience form // scikit-learn and the Explainable Boosting Machine both use; it is measured // on the training deviance rather than on a held-out split, because a random // split would make the fit irreproducible and MATLAB's is deterministic. The // tolerance is relative and not absolute because this deviance spans fifteen // orders of magnitude over a run, where an absolute one would be meaningless. static const octave_idx_type GAMB_PATIENCE = 10; static const double GAMB_REL_TOL = 1e-6; // How many times a round may halve its step before it gives up on improving at // all. MATLAB reduces its step the same way, which is why its learn-rate // argument is named an *initial* rate; three is ours, its limit not being // recoverable from anything it reports. static const int GAMB_MAX_HALVINGS = 3; // The fewest observations a predictor tree's leaf may hold. Measured against // R2024a on 2026-08-25: over a fixture whose only useful split isolates the // top K points, MATLAB splits there for K of 5 and above and falls back to the // cut leaving 5 for every K below it, on fitcgam and fitrgam alike, with bin // edges identical to ours. It is fitrtree's own default, which is the tree a // GAM fits internally. The interaction fitter is not measured and does not // apply this. static const octave_idx_type GAMB_MIN_LEAF = 5; // One predictor reduced to bin indices, with the cut points that produced // them. A missing value gets bin -1 and takes no part in any split. struct BinnedPredictor { RowVector edges; // nbins - 1 cut points, ascending Array bin; // one per observation, -1 if missing octave_idx_type nbins; }; // Bin one predictor. The cut points are the midpoints between consecutive // distinct finite values, which is every split a tree could make; where that // would exceed GAMB_MAX_EDGES the midpoints are thinned to that many, taken at // equally spaced positions through the sorted distinct values so the cuts stay // where the data is rather than where its range is. // The detection grid is coarser than the fitting grid and its size is fixed: // MATLAB reports 7 cut points, 8 equal-frequency bins, at every sample size. static const octave_idx_type GAMB_PAIR_EDGES = 7; static BinnedPredictor gamb_bin (const ColumnVector& x, octave_idx_type maxedges, bool categorical = false) { octave_idx_type n = x.numel (); std::vector v; v.reserve (n); for (octave_idx_type i = 0; i < n; i++) { if (! octave::math::isnan (x(i))) { v.push_back (x(i)); } } std::sort (v.begin (), v.end ()); std::vector sorted (v); // the data, still with its repeats v.erase (std::unique (v.begin (), v.end ()), v.end ()); octave_idx_type nd = (octave_idx_type) v.size (); octave_idx_type nf = (octave_idx_type) sorted.size (); BinnedPredictor B; // A categorical predictor keeps every level as a bin of its own. if (categorical) { maxedges = nd; } // A constant predictor admits no split at all: one bin, no cut points. if (nd < 2) { B.edges = RowVector (0); B.nbins = 1; B.bin = Array (dim_vector (n, 1), 0); for (octave_idx_type i = 0; i < n; i++) { B.bin(i) = octave::math::isnan (x(i)) ? -1 : 0; } return B; } octave_idx_type ne = nd - 1; if (ne > maxedges) { ne = maxedges; } B.edges = RowVector (ne); if (ne == nd - 1) { // Below the cap every midpoint between consecutive distinct values is a // cut, so the bins are as fine as any split could be. MATLAB, scikit-learn // and the Explainable Boosting Machine all agree on this rule to the last // digit, and it is the case almost every predictor falls into. for (octave_idx_type k = 0; k < ne; k++) { B.edges(k) = 0.5 * (v[k] + v[k + 1]); } } else { // Above the cap the cuts are equally spaced through the observations, so // the bins carry equal counts, and follow R2024a exactly: cut k is the // midpoint of the sorted values at 1-based positions ceil (k*n/(m+1)) and // ceil (k*n/(m+1)) + 1, m the number of cuts. Where those two values are // equal the cut moves up to the midpoint with the next larger value, and // is dropped if there is none; repeated cuts are removed below. Measured // on 2026-09-15 for the fitting grid at n = 700 and 1000, with and without // repeated values, and for the detection grid at n = 285 and 300. octave_idx_type m = 0; for (octave_idx_type k = 0; k < ne; k++) { double r = (double) (k + 1) * nf / (double) (ne + 1); octave_idx_type lo = (octave_idx_type) std::ceil (r) - 1; if (lo < 0) { lo = 0; } if (lo > nf - 2) { lo = nf - 2; } octave_idx_type up = lo + 1; while (up < nf && sorted[up] == sorted[lo]) { up++; } if (up >= nf) { continue; } B.edges(m++) = 0.5 * (sorted[lo] + sorted[up]); } if (m < ne) { RowVector e (m); for (octave_idx_type k = 0; k < m; k++) { e(k) = B.edges(k); } B.edges = e; ne = m; } } // Thinning can put two cuts at the same place when the distinct values are // very unevenly spaced; drop the repeats so every bin can be reached. octave_idx_type keep = 0; for (octave_idx_type k = 0; k < ne; k++) { if (keep == 0 || B.edges(k) > B.edges(keep - 1)) { B.edges(keep++) = B.edges(k); } } if (keep < ne) { RowVector e (keep); for (octave_idx_type k = 0; k < keep; k++) { e(k) = B.edges(k); } B.edges = e; ne = keep; } B.nbins = ne + 1; B.bin = Array (dim_vector (n, 1), 0); for (octave_idx_type i = 0; i < n; i++) { if (octave::math::isnan (x(i))) { B.bin(i) = -1; continue; } // The bin is the count of cut points at or below the value: a binary // search, since the cut points are ascending. octave_idx_type lo = 0; octave_idx_type hi = ne; while (lo < hi) { octave_idx_type mid = lo + (hi - lo) / 2; if (x(i) > B.edges(mid)) { lo = mid + 1; } else { hi = mid; } } B.bin(i) = lo; } return B; } // One contiguous run of bins, as the tree grows it. struct BinRegion { octave_idx_type lo; // first bin, inclusive octave_idx_type hi; // last bin, inclusive double gain; // best gain available from splitting it octave_idx_type cut; // bin the split would end the left side at }; // The Newton gain of splitting a region whose gradient and Hessian totals are // (GL, HL) on the left and (GR, HR) on the right. A leaf's value is -G/H, so // this is the deviance drop the split buys. static inline double gamb_gain (double GL, double HL, double GR, double HR) { const double eps = 1e-12; if (HL <= eps || HR <= eps) { return -1.0; } return GL * GL / HL + GR * GR / HR - (GL + GR) * (GL + GR) / (HL + HR); } // Find the best cut inside a region, given the prefix sums of the gradient, the // Hessian and the observation count over bins. Returns the gain and sets CUT // to the last bin of the left side; a region that cannot be split usefully // returns a gain of -1. // // A cut leaving fewer than GAMB_MIN_LEAF observations on either side is not // considered, whatever it gains. The Hessian guard in gamb_gain cannot stand // in for this: a logistic Hessian is p*(1-p) and says nothing about how many // observations produced it, so a single well separated point can carry enough // curvature to look like a leaf worth having. static double gamb_best_cut (const std::vector& G, const std::vector& H, const std::vector& C, octave_idx_type lo, octave_idx_type hi, octave_idx_type& cut, bool categorical = false) { double best = -1.0; cut = -1; double Gtot = G[hi + 1] - G[lo]; double Htot = H[hi + 1] - H[lo]; double Ctot = C[hi + 1] - C[lo]; for (octave_idx_type b = lo; b < hi; b++) { double CL = C[b + 1] - C[lo]; if (CL < GAMB_MIN_LEAF || Ctot - CL < GAMB_MIN_LEAF) { continue; } double GL = G[b + 1] - G[lo]; double HL = H[b + 1] - H[lo]; double g = gamb_gain (GL, HL, Gtot - GL, Htot - HL); // Cuts between categorical levels often tie up to rounding, and the // first in the order of the levels is kept, as R2024a keeps it. if (categorical ? (g > best + 1e-9 * std::abs (best)) : (g > best)) { best = g; cut = b; } } return best; } // Fit one tree to a single binned predictor and accumulate its leaf values, // scaled by the step, into VAL. The tree is grown best-first: the region // offering the largest gain is split, up to MAXSPLITS splits in all, which is // what MATLAB's MaxNumSplitsPerPredictor counts. With the default of 1 this // is a stump. static void gamb_fit_tree (const BinnedPredictor& B, const ColumnVector& grad, const ColumnVector& hess, octave_idx_type maxsplits, double step, ColumnVector& val, bool categorical = false) { octave_idx_type n = grad.numel (); octave_idx_type nb = B.nbins; // Gradient and Hessian totals per bin, then their prefix sums, so any // region's totals are one subtraction. std::vector G (nb + 1, 0.0); std::vector H (nb + 1, 0.0); std::vector C (nb + 1, 0.0); for (octave_idx_type i = 0; i < n; i++) { octave_idx_type b = B.bin(i); if (b < 0) { continue; } G[b + 1] += grad(i); H[b + 1] += hess(i); C[b + 1] += 1.0; } // A categorical predictor has no order of its own: its levels are sorted // by the Newton step each would take alone, so a cut between neighbours in // that order splits the levels into two sets. Measured against R2024a on // 2026-09-15, this reproduces fitrgam and fitcgam with categorical // predictors to 2e-9 where the natural order of the levels does not. std::vector order ((std::size_t) nb); for (octave_idx_type b = 0; b < nb; b++) { order[(std::size_t) b] = b; } if (categorical) { std::vector g0 (G.begin () + 1, G.end ()); std::vector h0 (H.begin () + 1, H.end ()); std::vector c0 (C.begin () + 1, C.end ()); auto ratio = [&g0, &h0] (octave_idx_type b) -> double { return (h0[(std::size_t) b] > 1e-12) ? g0[(std::size_t) b] / h0[(std::size_t) b] : octave::numeric_limits::Inf (); }; std::stable_sort (order.begin (), order.end (), [&ratio] (octave_idx_type a, octave_idx_type b) { return ratio (a) < ratio (b); }); for (octave_idx_type q = 0; q < nb; q++) { G[(std::size_t) q + 1] = g0[(std::size_t) order[(std::size_t) q]]; H[(std::size_t) q + 1] = h0[(std::size_t) order[(std::size_t) q]]; C[(std::size_t) q + 1] = c0[(std::size_t) order[(std::size_t) q]]; } } for (octave_idx_type b = 0; b < nb; b++) { G[b + 1] += G[b]; H[b + 1] += H[b]; C[b + 1] += C[b]; } std::vector leaves; BinRegion root; root.lo = 0; root.hi = nb - 1; root.gain = gamb_best_cut (G, H, C, root.lo, root.hi, root.cut, categorical); leaves.push_back (root); for (octave_idx_type s = 0; s < maxsplits; s++) { // The leaf that buys the most. std::size_t pick = 0; double best = -1.0; for (std::size_t k = 0; k < leaves.size (); k++) { if (leaves[k].gain > best) { best = leaves[k].gain; pick = k; } } if (best <= 0.0) { break; } BinRegion left, right; left.lo = leaves[pick].lo; left.hi = leaves[pick].cut; right.lo = leaves[pick].cut + 1; right.hi = leaves[pick].hi; left.gain = gamb_best_cut (G, H, C, left.lo, left.hi, left.cut, categorical); right.gain = gamb_best_cut (G, H, C, right.lo, right.hi, right.cut, categorical); leaves[pick] = left; leaves.push_back (right); } // Every leaf contributes its Newton step to the bins it covers. for (std::size_t k = 0; k < leaves.size (); k++) { double Gk = G[leaves[k].hi + 1] - G[leaves[k].lo]; double Hk = H[leaves[k].hi + 1] - H[leaves[k].lo]; if (Hk <= 1e-12) { continue; } double leaf = step * Gk / Hk; for (octave_idx_type b = leaves[k].lo; b <= leaves[k].hi; b++) { val(order[(std::size_t) b]) += leaf; } } } // What a boosted fit produces, in the shape the learners store it. A shape // function is a step function: VALUE[j](b) is what predictor j contributes for // any observation falling in its bin b, so prediction is a lookup and does not // depend on how many trees built it. struct GamBoostFit { double intercept; std::vector edges; std::vector value; octave_idx_type ntrees; std::string reason; double deviance; ColumnVector residuals; }; // The bin a value falls in, for a predictor already binned at EDGES. Matches // the binary search gamb_bin used, so a training row bins the same way at // predict time; a missing value has no bin and is signalled by -1. static octave_idx_type gamb_bin_of (const RowVector& edges, double v) { if (octave::math::isnan (v)) { return -1; } octave_idx_type lo = 0; octave_idx_type hi = edges.numel (); while (lo < hi) { octave_idx_type mid = lo + (hi - lo) / 2; if (v > edges(mid)) { lo = mid + 1; } else { hi = mid; } } return lo; } // The additive prediction of a fitted model at one row of X. A predictor that // is missing contributes nothing rather than poisoning the sum, which is what // a tree does with a missing value it was never given a surrogate for. [[maybe_unused]] static double gamb_predict_row (const GamBoostFit& F, const Matrix& X, octave_idx_type i) { double f = F.intercept; for (std::size_t j = 0; j < F.value.size (); j++) { octave_idx_type b = gamb_bin_of (F.edges[j], X(i, (octave_idx_type) j)); if (b >= 0) { f += F.value[j](b); } } return f; } // The deviance of the current additive prediction. For a classifier that is // -2 times the Bernoulli log likelihood, which is the quantity MATLAB reports // and which starts at 2 n log 2 for a balanced response; for a regression it // is the residual sum of squares, the Gaussian deviance up to the scale. static double gamb_deviance (const ColumnVector& Y, const ColumnVector& f, int method, const ColumnVector& w) { octave_idx_type n = Y.numel (); double dev = 0.0; if (method == 1) { for (octave_idx_type i = 0; i < n; i++) { double p = 1.0 / (1.0 + std::exp (-f(i))); // A saturated fit puts p at 0 or 1 exactly; clamp so the log is finite // and the deviance keeps falling instead of turning into a NaN. if (p < 1e-300) { p = 1e-300; } if (p > 1.0 - 1e-16) { p = 1.0 - 1e-16; } dev += w(i) * (Y(i) * std::log (p) + (1.0 - Y(i)) * std::log (1.0 - p)); } dev *= -2.0; } else { for (octave_idx_type i = 0; i < n; i++) { double r = Y(i) - f(i); dev += w(i) * r * r; } } return dev; } // Observation weights for a fit, rescaled to average one. Without weights // every row weighs one, which is the unweighted fit exactly; with them the // rescaling leaves the Newton steps, the curvature guards and the minimum leaf // size, which counts rows and not weight, meaning what they meant before. static ColumnVector gamb_weights (octave_idx_type n, const ColumnVector *W) { ColumnVector wt (n, 1.0); if (W != nullptr) { double sw = 0.0; for (octave_idx_type i = 0; i < n; i++) { sw += (*W)(i); } for (octave_idx_type i = 0; i < n; i++) { wt(i) = (*W)(i) * (double) n / sw; } } return wt; } // The categorical mask a wrapper was handed, validated: one flag per column of // X, and every value of a flagged column a positive integer code or missing. [[maybe_unused]] static std::vector gamb_categorical_arg (const octave_value& arg, const Matrix& X, const char *who) { octave_idx_type d = X.columns (); if (! (arg.islogical () || arg.isnumeric ()) || arg.numel () != d) { error ("%s: Categorical must be a logical vector with one element per " "column of X.", who); } NDArray flags = arg.array_value (); std::vector cat ((std::size_t) d, false); for (octave_idx_type j = 0; j < d; j++) { cat[(std::size_t) j] = (flags(j) != 0.0); if (! cat[(std::size_t) j]) { continue; } for (octave_idx_type i = 0; i < X.rows (); i++) { double v = X(i, j); if (! octave::math::isnan (v) && (v < 1.0 || v != std::floor (v))) { error ("%s: a categorical column must hold positive integer codes.", who); } } } return cat; } // Newton boosting of one tree per predictor per round. METHOD 1 fits the // logistic deviance, as a classifier is fitted, and METHOD 2 the squared // error, as a regression is fitted; the two differ only in the seed, the // gradient and the Hessian, so the loop is shared. [[maybe_unused]] static GamBoostFit gamb_boost (const Matrix& X, const ColumnVector& Y, int method, octave_idx_type maxtrees, double lrate, octave_idx_type maxsplits, int verbose, octave_idx_type numprint, const ColumnVector *F0 = nullptr, const ColumnVector *W = nullptr, const std::vector *cat = nullptr) { octave_idx_type n = X.rows (); octave_idx_type d = X.columns (); ColumnVector wt = gamb_weights (n, W); std::vector B (d); for (octave_idx_type j = 0; j < d; j++) { ColumnVector xj (n); for (octave_idx_type i = 0; i < n; i++) { xj(i) = X(i, j); } B[j] = gamb_bin (xj, GAMB_MAX_EDGES, cat != nullptr && (*cat)[(std::size_t) j]); } // Which predictors miss a value somewhere in the training rows. A row // missing a predictor takes no part in that predictor's splits, but it is // still in the tree's root, as a tree keeps every row it was given, so it // is fitted with the root's value. Measured against R2024a on 2026-09-15: // with rows missing x1, scores and resubstitution agree to twelve digits // under this rule and not under one that leaves those rows out. std::vector hasmiss (d, false); for (octave_idx_type j = 0; j < d; j++) { for (octave_idx_type i = 0; i < n; i++) { if (B[j].bin(i) < 0) { hasmiss[j] = true; break; } } } GamBoostFit F; F.edges.resize (d); F.value.resize (d); for (octave_idx_type j = 0; j < d; j++) { F.edges[j] = B[j].edges; F.value[j] = ColumnVector (B[j].nbins, 0.0); } // The seed. A classifier starts at zero, an even chance, and a regression at // the response mean; boosting moves the first, because recentring hands it // the constants the shape functions give up, and leaves the second where it // is, because a squared-error residual already has mean zero and there is // nothing to hand over. // // Zero and not the log-odds of the response mean, which is what a classifier // seeded here until 2026-08-25. Measured against R2024a on a response with // a mean of 0.4, MATLAB's fitted intercept is exactly -0.4, which is the // mean of the working response the first cycle forms from p = 1/2, and not // the -0.40546510810816 a log-odds seed gives. A balanced response hides // the difference, both being zero there. // // Given F0 there is no seed to find: the caller is continuing a fit and // hands over the prediction it reached. What comes back is then the // increment, shape values to add to the ones already held and an intercept // to add to the one already there, which is the contract gamb_boost_inter // works to as well. A round starts at LRATE whatever its number, so the // trees this adds are the trees a longer run would have added. ColumnVector f (n); if (F0 != nullptr) { F.intercept = 0.0; f = *F0; } else { double ybar = 0.0; for (octave_idx_type i = 0; i < n; i++) { ybar += wt(i) * Y(i); } ybar /= (double) n; if (method == 1) { if (ybar <= 0.0 || ybar >= 1.0) { // A single-class response has an infinite log-odds and no gradient to // follow; the fit is the constant and every shape function stays flat. F.intercept = (ybar <= 0.0) ? -octave::numeric_limits::Inf () : octave::numeric_limits::Inf (); F.ntrees = 0; F.reason = "Unable to improve the model fit."; F.deviance = 0.0; F.residuals = ColumnVector (n, 0.0); return F; } F.intercept = 0.0; } else { F.intercept = ybar; } f = ColumnVector (n, F.intercept); } double dev = gamb_deviance (Y, f, method, wt); // The printed trace, in the columns MATLAB prints. RelTol here is the // relative improvement the round bought, which is what the patience test // reads. MATLAB prints a quantity of its own under that heading which is // not recoverable from anything it reports, so this one is ours and the // documentation says so rather than implying they agree. if (verbose > 0) { static const char *bar = "|========================================================|\n"; static const char *hdr = "| Type | NumTrees | Deviance | RelTol | LearnRate |\n"; octave_stdout << bar << hdr << bar << "| 1D| 0|" << std::setw (12) << dev << "| - | - |\n"; } ColumnVector grad (n), hess (n); std::vector devhist; devhist.push_back (dev); F.ntrees = 0; F.reason = "Terminated after training the requested number of trees."; for (octave_idx_type t = 0; t < maxtrees; t++) { // A round is tried at the running step and retried at half of it while it // fails to earn its place, which is the shape of MATLAB's own reported // learn rate: constant until the fit plateaus, then reduced once or more // on the last round it manages to accept. bool accepted = false; double step = lrate; for (int h = 0; h <= GAMB_MAX_HALVINGS; h++) { std::vector trial (d); ColumnVector fnew = f; double shift = 0.0; // Local scoring. The working response and the weights are formed ONCE // from the fit the round starts at and then held fixed across the whole // cycle, which is what MATLAB does: // // w = p * (1 - p) the curvature at the current fit // z = f + (Y - p) / w the response a weighted least squares sees // // and each predictor is then fitted to what is left of Z, under those // frozen weights. Passing W * (Z - FNEW) as the gradient and W as the // Hessian makes the tree's own SUM(G)/SUM(H) the weighted mean of the // partial residual, so the fitter needs no special case. // // Z is never formed. Written out, // // W * (Z - FNEW) = (Y - p) - W * (FNEW - F) // // which is the same number with no division by W at all. That matters // once the fit separates the classes: W goes to zero, Z to infinity, and // a floor under W to keep the quotient finite caps the step and stalls // the tail. The product is finite whatever W does. // // Recomputing W after every predictor, which is what this did until // 2026-08-25, is a defensible algorithm and not what MATLAB has. It // takes a full Newton step per predictor and so counts the curvature the // earlier predictors of the same cycle already spent, which overshoots: // on the two-predictor fixture the second tree's step came out 1.1625 // against MATLAB's 1.0423, and the fit was consistently the worse of the // two at equal budget. At one predictor there is no cycle and the two // rules agree exactly, which is why this went unseen until the boosted // engine became the default and several predictors could be compared. // // For squared error the two are the same rule written twice: Z is Y and // W is one, so W * (Z - FNEW) is the recomputed gradient. The regression // fit is unchanged by any of this, and was measured against R2024a to be // exact before and after. ColumnVector r0 (n), ww (n); for (octave_idx_type i = 0; i < n; i++) { if (method == 1) { double p = 1.0 / (1.0 + std::exp (-f(i))); ww(i) = wt(i) * p * (1.0 - p); r0(i) = wt(i) * (Y(i) - p); } else { ww(i) = wt(i); r0(i) = wt(i) * (Y(i) - f(i)); } } // The predictors are fitted in sequence and not in parallel: each tree // sees what the ones before it have already taken out of the working // response. Fitting them all to the same residual and adding them would // overshoot by a factor of the predictor count, and it would also drag // the intercept off the response mean in a regression, where MATLAB // holds it there. for (octave_idx_type j = 0; j < d; j++) { for (octave_idx_type i = 0; i < n; i++) { grad(i) = r0(i) - ww(i) * (fnew(i) - f(i)); hess(i) = ww(i); } // The root's value, which a row missing this predictor is fitted // with. Zero, and no work, for a predictor missing nothing. double miss = 0.0; if (hasmiss[j]) { double Gr = 0.0; double Hr = 0.0; for (octave_idx_type i = 0; i < n; i++) { Gr += grad(i); Hr += hess(i); } if (Hr > 1e-12) { miss = step * Gr / Hr; } } trial[j] = ColumnVector (B[j].nbins, 0.0); gamb_fit_tree (B[j], grad, hess, maxsplits, step, trial[j], cat != nullptr && (*cat)[(std::size_t) j]); // The model value the tree just added, before any bookkeeping. for (octave_idx_type i = 0; i < n; i++) { if (B[j].bin(i) >= 0) { fnew(i) += trial[j](B[j].bin(i)); } else { fnew(i) += miss; } } // Recentre over the observations, not over the bins: a shape function // is held to mean zero on the data it was fitted to, and the constant // it gives up goes to the intercept. This moves value between the two // and leaves the prediction alone, which is why a regression intercept // stays at the response mean while a classifier's walks away from its // seed: a squared-error residual keeps mean zero from round to round // and hands over nothing, a logistic one does not. // // The mean is taken under the cycle's own weights, which is the mean a // weighted least squares holds to zero. A plain observation mean is // the same thing on the first cycle, where every weight is 1/4, and // parts from it on every cycle after: it left the intercept 0.003 out // at two cycles and 0.0006 at three, against R2024a, with every shape // function already exact. Only the split between the intercept and // the shape functions moves, never the prediction. double sw = 0.0; double sv = 0.0; for (octave_idx_type i = 0; i < n; i++) { if (B[j].bin(i) >= 0) { sw += ww(i); sv += ww(i) * trial[j](B[j].bin(i)); } } // // A predictor missing a value is held to zero there instead, which is // where R2024a holds it: a missing value then adds nothing from the // term, as it does from a model fitted on complete rows, and its // intercept is the one MATLAB reports. if (hasmiss[j] || sw > 0.0) { double m = hasmiss[j] ? miss : sv / sw; for (octave_idx_type b = 0; b < B[j].nbins; b++) { trial[j](b) -= m; } shift += m; } } double devnew = gamb_deviance (Y, fnew, method, wt); double devold = dev; // A round only has to improve the deviance at all to be kept. Whether // it improved enough to be worth continuing is the patience test below, // which looks across a window of rounds rather than at this one. if (devnew < dev) { for (octave_idx_type j = 0; j < d; j++) { for (octave_idx_type b = 0; b < B[j].nbins; b++) { F.value[j](b) += trial[j](b); } } F.intercept += shift; f = fnew; dev = devnew; F.ntrees++; accepted = true; if (verbose > 0 && (F.ntrees == 1 || F.ntrees % numprint == 0)) { double rel = (devold > 0.0) ? (devold - devnew) / devold : 0.0; octave_stdout << "| 1D|" << std::setw (10) << F.ntrees << "|" << std::setw (12) << devnew << "|" << std::setw (12) << rel << "|" << std::setw (12) << step << "|\n"; } break; } step *= 0.5; } if (! accepted) { F.reason = "Unable to improve the model fit."; break; } // Patience: stop once a whole window of rounds has failed to better the // deviance the window opened at by more than the tolerance. A single // round that buys little is not evidence of convergence, since the gain // from round to round is noisy; a window of them is. devhist.push_back (dev); octave_idx_type h = (octave_idx_type) devhist.size (); if (h > GAMB_PATIENCE) { double ref = devhist[(std::size_t) (h - 1 - GAMB_PATIENCE)]; double need = ref - GAMB_REL_TOL * std::fabs (ref); bool better = false; for (octave_idx_type k = h - GAMB_PATIENCE; k < h; k++) { if (devhist[(std::size_t) k] < need) { better = true; break; } } if (! better) { F.reason = "Unable to improve the model fit."; break; } } } F.deviance = dev; F.residuals = ColumnVector (n); for (octave_idx_type i = 0; i < n; i++) { if (method == 1) { F.residuals(i) = Y(i) - 1.0 / (1.0 + std::exp (-f(i))); } else { F.residuals(i) = Y(i) - f(i); } } return F; } // What the interaction search reports for one candidate pair. The p-value is // not computed here: turning F into a probability needs the incomplete beta, // and the package already ships a MATLAB-verified fcdf, so the caller does it // there rather than a second implementation being carried in C++. Selecting // which pairs to keep is policy and belongs beside it. struct GamPairStat { octave_idx_type j; octave_idx_type k; double F; double df1; double df2; }; // Test one pair for an interaction the additive terms have not already // explained. The residuals of the predictor phase are laid on the coarse // grid of the two predictors and decomposed as a two-way layout: what the rows // explain, what the columns explain, and what only the cells explain. The // last is the interaction, and the F ratio against the within-cell error is // what ranks the pair. // // The decomposition is sequential (Type I): with unequal cell counts the row // and column sums of squares are not orthogonal, so this is an approximation // rather than an identity, and a small negative interaction term is clamped to // zero. That is sound for ranking candidates, which is all it is used for. [[maybe_unused]] static GamPairStat gamb_pair_stat (const BinnedPredictor& Bj, const BinnedPredictor& Bk, const ColumnVector& r, octave_idx_type j, octave_idx_type k) { octave_idx_type n = r.numel (); octave_idx_type rj = Bj.nbins; octave_idx_type rk = Bk.nbins; std::vector csum ((std::size_t) (rj * rk), 0.0); std::vector ccnt ((std::size_t) (rj * rk), 0.0); std::vector rsum ((std::size_t) rj, 0.0); std::vector rcnt ((std::size_t) rj, 0.0); std::vector ksum ((std::size_t) rk, 0.0); std::vector kcnt ((std::size_t) rk, 0.0); double tot = 0.0; double cnt = 0.0; for (octave_idx_type i = 0; i < n; i++) { octave_idx_type a = Bj.bin(i); octave_idx_type b = Bk.bin(i); if (a < 0 || b < 0) { continue; } std::size_t c = (std::size_t) (a * rk + b); csum[c] += r(i); ccnt[c] += 1.0; rsum[(std::size_t) a] += r(i); rcnt[(std::size_t) a] += 1.0; ksum[(std::size_t) b] += r(i); kcnt[(std::size_t) b] += 1.0; tot += r(i); cnt += 1.0; } GamPairStat S; S.j = j; S.k = k; S.F = 0.0; S.df1 = 0.0; S.df2 = 0.0; if (cnt < 4.0) { return S; } double gm = tot / cnt; double sscell = 0.0; octave_idx_type ncell = 0; for (std::size_t c = 0; c < csum.size (); c++) { if (ccnt[c] > 0.0) { double m = csum[c] / ccnt[c]; sscell += ccnt[c] * (m - gm) * (m - gm); ncell++; } } double ssrow = 0.0; octave_idx_type nrow = 0; for (std::size_t a = 0; a < rsum.size (); a++) { if (rcnt[a] > 0.0) { double m = rsum[a] / rcnt[a]; ssrow += rcnt[a] * (m - gm) * (m - gm); nrow++; } } double sscol = 0.0; octave_idx_type ncol = 0; for (std::size_t b = 0; b < ksum.size (); b++) { if (kcnt[b] > 0.0) { double m = ksum[b] / kcnt[b]; sscol += kcnt[b] * (m - gm) * (m - gm); ncol++; } } double ssint = sscell - ssrow - sscol; if (ssint < 0.0) { ssint = 0.0; } double sserr = 0.0; for (octave_idx_type i = 0; i < n; i++) { octave_idx_type a = Bj.bin(i); octave_idx_type b = Bk.bin(i); if (a < 0 || b < 0) { continue; } std::size_t c = (std::size_t) (a * rk + b); double m = csum[c] / ccnt[c]; sserr += (r(i) - m) * (r(i) - m); } double df1 = (double) ((nrow - 1) * (ncol - 1)); double df2 = cnt - (double) ncell; if (df1 <= 0.0 || df2 <= 0.0 || sserr <= 0.0) { return S; } S.F = (ssint / df1) / (sserr / df2); S.df1 = df1; S.df2 = df2; return S; } // A pair term is a sum of small trees over its two predictors, fitted to the // rows rather than to a grid. Measured against R2024a on 2026-09-15 from dense // maps of the pair contribution, for regression and classification alike to // 5e-10: a node is cut halfway between two adjacent values of the rows it // holds, a leaf keeps at least GAMB_MIN_LEAF rows, the tree grows a layer at a // time and a layer that would exceed the split budget keeps the splits of // largest gain, and a node takes the Newton step of the rows it holds. // // A row missing the predictor a node splits on goes no further: it takes that // node's value, in training and in prediction, and the gain of a split counts // only the rows holding its predictor. Of 19 maps measured this way 18 agree // to 5e-10. The exception is a regression fitted with missing values whose // second interaction tree R2024a roots on residuals equal to ours but not at // the split of largest gain; no rule tried reproduces that choice. // // A categorical predictor of a pair is split into two sets of levels: at each // node its levels are sorted by the Newton step each would take alone and cut // between neighbours, as a predictor's own trees cut them. A level none of // the node's rows holds belongs to neither set and stops there, as a missing // value does; R2024a gives such a level the node's value. // // The accepted trees are flattened at the end into a grid over the cut points // they used, every level of a categorical predictor, plus what a row missing // either predictor or both takes, so prediction stays a lookup. struct PairTreeNode { int dim; // -1 for a leaf, else 0 (j) or 1 (k) double cut; octave_idx_type left; octave_idx_type right; double value; std::vector inleft; // categorical split: 1 left, 2 right, // 0 a code the node never held }; struct GamInterTerm { octave_idx_type j; octave_idx_type k; bool catj; bool catk; octave_idx_type levj; // levels of a categorical predictor octave_idx_type levk; std::vector> trees; // accepted, recentred RowVector ej; // the pair's cut points on predictor j RowVector ek; // and on predictor k Matrix value; // numel (ej) + 1 by numel (ek) + 1 RowVector missj; // x_j missing, one value per cell of x_k RowVector missk; // x_k missing, one value per cell of x_j double missboth; // both missing }; // The best cut of a node over both predictors. Returns the gain, or -1 when // no cut leaves GAMB_MIN_LEAF rows holding the predictor on each side, and // sets DIM (0 for j, 1 for k) with either CUT, halfway between two adjacent // values, or for a categorical predictor INLEFT, the codes sent left. The // gain is taken over the rows holding the predictor. A tie keeps the first // cut found, scanning j before k. static double gamb_pair_best_cut (const std::vector& rows, const Matrix& X, octave_idx_type j, octave_idx_type k, bool catj, bool catk, const ColumnVector& grad, const ColumnVector& hess, int& dim, double& cut, std::vector& inleft) { double best = -1.0; dim = -1; cut = 0.0; inleft.clear (); for (int dd = 0; dd < 2; dd++) { octave_idx_type col = (dd == 0) ? j : k; bool iscat = (dd == 0) ? catj : catk; std::vector srt; double G = 0.0; double H = 0.0; for (octave_idx_type r : rows) { if (! octave::math::isnan (X(r, col))) { srt.push_back (r); G += grad(r); H += hess(r); } } octave_idx_type m = (octave_idx_type) srt.size (); if (m < 2 * GAMB_MIN_LEAF) { continue; } if (iscat) { octave_idx_type L = 0; for (octave_idx_type r : srt) { L = std::max (L, (octave_idx_type) X(r, col)); } std::vector lg ((std::size_t) L + 1, 0.0); std::vector lh ((std::size_t) L + 1, 0.0); std::vector lc ((std::size_t) L + 1, 0); for (octave_idx_type r : srt) { std::size_t c = (std::size_t) X(r, col); lg[c] += grad(r); lh[c] += hess(r); lc[c]++; } std::vector lev; for (octave_idx_type c = 1; c <= L; c++) { if (lc[(std::size_t) c] > 0) { lev.push_back (c); } } auto ratio = [&lg, &lh] (octave_idx_type c) -> double { return (lh[(std::size_t) c] > 1e-12) ? lg[(std::size_t) c] / lh[(std::size_t) c] : octave::numeric_limits::Inf (); }; std::stable_sort (lev.begin (), lev.end (), [&ratio] (octave_idx_type a, octave_idx_type b) { return ratio (a) < ratio (b); }); double GL = 0.0; double HL = 0.0; octave_idx_type CL = 0; for (std::size_t q = 0; q + 1 < lev.size (); q++) { std::size_t c = (std::size_t) lev[q]; GL += lg[c]; HL += lh[c]; CL += lc[c]; if (CL < GAMB_MIN_LEAF || m - CL < GAMB_MIN_LEAF) { continue; } // Fitting a pair's cells leaves residual sums that cancel exactly, // so two cuts often have the same gain up to rounding. The first in // the order of the levels is kept, as R2024a keeps it. double g = gamb_gain (GL, HL, G - GL, H - HL); if (g > best + 1e-9 * std::abs (best)) { best = g; dim = dd; cut = 0.0; inleft.assign ((std::size_t) L + 1, 0); for (std::size_t t = 0; t < lev.size (); t++) { inleft[(std::size_t) lev[t]] = (t <= q) ? 1 : 2; } } } continue; } std::stable_sort (srt.begin (), srt.end (), [&X, col] (octave_idx_type a, octave_idx_type b) { return X(a, col) < X(b, col); }); double GL = 0.0; double HL = 0.0; for (octave_idx_type q = 0; q < m - 1; q++) { GL += grad(srt[(std::size_t) q]); HL += hess(srt[(std::size_t) q]); double v = X(srt[(std::size_t) q], col); double w = X(srt[(std::size_t) q + 1], col); if (v == w) { continue; } if (q + 1 < GAMB_MIN_LEAF || m - q - 1 < GAMB_MIN_LEAF) { continue; } double g = gamb_gain (GL, HL, G - GL, H - HL); if (g > best) { best = g; dim = dd; cut = 0.5 * (v + w); inleft.clear (); } } } return best; } // Where a node sends the value V: -1 left, 1 right, 0 nowhere, for a level // of a categorical split that none of the node's rows held. static inline int gamb_pair_side (const PairTreeNode& node, double v) { if (node.inleft.empty ()) { return (v <= node.cut) ? -1 : 1; } octave_idx_type c = (octave_idx_type) v; if (c < 0 || (std::size_t) c >= node.inleft.size ()) { return 0; } char side = node.inleft[(std::size_t) c]; return (side == 1) ? -1 : ((side == 2) ? 1 : 0); } // Grow one pair tree over every row and return its nodes, the root first, // each carrying the Newton step of the rows it holds scaled by STEP. static std::vector gamb_pair_tree (const Matrix& X, octave_idx_type j, octave_idx_type k, bool catj, bool catk, const ColumnVector& grad, const ColumnVector& hess, octave_idx_type maxsplits, double step) { octave_idx_type n = X.rows (); std::vector nodes (1); nodes[0] = PairTreeNode {-1, 0.0, -1, -1, 0.0, {}}; std::vector> held (1); held[0].resize ((std::size_t) n); for (octave_idx_type i = 0; i < n; i++) { held[0][(std::size_t) i] = i; } struct Cand { double gain; std::size_t leaf; int dim; double cut; std::vector inleft; }; std::vector leaves (1, 0); octave_idx_type budget = maxsplits; while (budget > 0) { std::vector cand; for (std::size_t l = 0; l < leaves.size (); l++) { int dim; double cut; std::vector inleft; double g = gamb_pair_best_cut (held[(std::size_t) leaves[l]], X, j, k, catj, catk, grad, hess, dim, cut, inleft); if (dim >= 0 && g > 0.0) { cand.push_back ({g, l, dim, cut, inleft}); } } if (cand.empty ()) { break; } // A layer over the budget keeps the splits of largest gain. std::stable_sort (cand.begin (), cand.end (), [] (const Cand& a, const Cand& b) { return a.gain > b.gain; }); if ((octave_idx_type) cand.size () > budget) { cand.resize ((std::size_t) budget); } std::vector pick (leaves.size (), -1); for (std::size_t c = 0; c < cand.size (); c++) { pick[cand[c].leaf] = (int) c; } std::vector next; for (std::size_t l = 0; l < leaves.size (); l++) { octave_idx_type id = leaves[l]; if (pick[l] < 0) { next.push_back (id); continue; } const Cand& c = cand[(std::size_t) pick[l]]; octave_idx_type col = (c.dim == 0) ? j : k; nodes[(std::size_t) id].dim = c.dim; nodes[(std::size_t) id].cut = c.cut; nodes[(std::size_t) id].inleft = c.inleft; std::vector lo; std::vector hi; for (octave_idx_type r : held[(std::size_t) id]) { double v = X(r, col); if (octave::math::isnan (v)) { continue; } if (gamb_pair_side (nodes[(std::size_t) id], v) < 0) { lo.push_back (r); } else { hi.push_back (r); } } octave_idx_type lid = (octave_idx_type) nodes.size (); nodes.push_back (PairTreeNode {-1, 0.0, -1, -1, 0.0, {}}); nodes.push_back (PairTreeNode {-1, 0.0, -1, -1, 0.0, {}}); held.push_back (lo); held.push_back (hi); nodes[(std::size_t) id].left = lid; nodes[(std::size_t) id].right = lid + 1; next.push_back (lid); next.push_back (lid + 1); } budget -= (octave_idx_type) cand.size (); leaves.swap (next); } for (std::size_t id = 0; id < nodes.size (); id++) { double G = 0.0; double H = 0.0; for (octave_idx_type r : held[id]) { G += grad(r); H += hess(r); } nodes[id].value = (H > 1e-12) ? step * G / H : 0.0; } return nodes; } // The value a pair tree gives the point (XJ, XK). A missing value stops at // the first node that splits on its predictor, and so does a level that node // never held. static double gamb_pair_tree_value (const std::vector& tree, double xj, double xk) { std::size_t id = 0; while (tree[id].dim >= 0) { double v = (tree[id].dim == 0) ? xj : xk; if (octave::math::isnan (v)) { return tree[id].value; } int side = gamb_pair_side (tree[id], v); if (side == 0) { return tree[id].value; } id = (std::size_t) ((side < 0) ? tree[id].left : tree[id].right); } return tree[id].value; } // The summed value of a pair's trees at (XJ, XK). static double gamb_pair_value (const std::vector>& trees, double xj, double xk) { double v = 0.0; for (const std::vector& t : trees) { v += gamb_pair_tree_value (t, xj, xk); } return v; } // Flatten a pair's trees into a lookup: a grid over every cut point its trees // used, or every level of a categorical predictor, read at a point inside each // cell, and the values a row missing x_j, x_k or both takes, read the same way. static void gamb_pair_flatten (GamInterTerm& T) { std::vector cj; std::vector ck; for (const std::vector& t : T.trees) { for (const PairTreeNode& node : t) { if (node.dim == 0 && ! T.catj) { cj.push_back (node.cut); } else if (node.dim == 1 && ! T.catk) { ck.push_back (node.cut); } } } if (T.catj) { for (octave_idx_type c = 1; c < T.levj; c++) { cj.push_back (c + 0.5); } } if (T.catk) { for (octave_idx_type c = 1; c < T.levk; c++) { ck.push_back (c + 0.5); } } std::sort (cj.begin (), cj.end ()); cj.erase (std::unique (cj.begin (), cj.end ()), cj.end ()); std::sort (ck.begin (), ck.end ()); ck.erase (std::unique (ck.begin (), ck.end ()), ck.end ()); T.ej = RowVector ((octave_idx_type) cj.size ()); for (std::size_t a = 0; a < cj.size (); a++) { T.ej((octave_idx_type) a) = cj[a]; } T.ek = RowVector ((octave_idx_type) ck.size ()); for (std::size_t b = 0; b < ck.size (); b++) { T.ek((octave_idx_type) b) = ck[b]; } // A point inside cell A: the level itself for a categorical predictor. auto inside = [] (const std::vector& c, std::size_t a, bool categorical) -> double { if (categorical) { return (double) a + 1.0; } if (c.empty ()) { return 0.0; } if (a == 0) { return c[0] - 1.0; } if (a == c.size ()) { return c.back () + 1.0; } return 0.5 * (c[a - 1] + c[a]); }; const double nan = octave::numeric_limits::NaN (); octave_idx_type nj = (octave_idx_type) cj.size () + 1; octave_idx_type nk = (octave_idx_type) ck.size () + 1; T.value = Matrix (nj, nk, 0.0); T.missj = RowVector (nk, 0.0); T.missk = RowVector (nj, 0.0); for (octave_idx_type a = 0; a < nj; a++) { double xj = inside (cj, (std::size_t) a, T.catj); for (octave_idx_type b = 0; b < nk; b++) { T.value(a, b) = gamb_pair_value (T.trees, xj, inside (ck, (std::size_t) b, T.catk)); } T.missk(a) = gamb_pair_value (T.trees, xj, nan); } for (octave_idx_type b = 0; b < nk; b++) { T.missj(b) = gamb_pair_value (T.trees, nan, inside (ck, (std::size_t) b, T.catk)); } T.missboth = gamb_pair_value (T.trees, nan, nan); } // What the interaction phase produces. struct GamInterFit { std::vector edges; // the detection grid, one per predictor std::vector term; // one per selected pair double shift; // what recentring handed the intercept octave_idx_type ntrees; std::string reason; double deviance; ColumnVector residuals; }; // Boost trees over the selected pairs, continuing from the additive prediction // the predictor phase left in F0 rather than refitting it. MATLAB's own trace // opens its interaction block at the deviance the predictor block ended on, so // the two phases share a running fit and differ only in what they are allowed // to split. The pairs are fitted in sequence within a round, for the same // reason the predictors are. [[maybe_unused]] static GamInterFit gamb_boost_inter (const Matrix& X, const ColumnVector& Y, const ColumnVector& F0, int method, const Matrix& pairs, octave_idx_type maxtrees, double lrate, octave_idx_type maxsplits, const ColumnVector *W = nullptr, const std::vector *cat = nullptr) { octave_idx_type n = X.rows (); octave_idx_type d = X.columns (); octave_idx_type np = pairs.rows (); ColumnVector wt = gamb_weights (n, W); auto iscat = [cat] (octave_idx_type j) -> bool { return cat != nullptr && (*cat)[(std::size_t) j]; }; // The detection grid, reported with the fit as MATLAB reports it. GamInterFit F; F.edges.resize ((std::size_t) d); for (octave_idx_type j = 0; j < d; j++) { ColumnVector xj (n); for (octave_idx_type i = 0; i < n; i++) { xj(i) = X(i, j); } F.edges[(std::size_t) j] = gamb_bin (xj, GAMB_PAIR_EDGES, iscat (j)).edges; } F.term.resize ((std::size_t) np); for (octave_idx_type q = 0; q < np; q++) { GamInterTerm& T = F.term[(std::size_t) q]; T.j = (octave_idx_type) pairs(q, 0) - 1; T.k = (octave_idx_type) pairs(q, 1) - 1; T.catj = iscat (T.j); T.catk = iscat (T.k); T.levj = 0; T.levk = 0; for (octave_idx_type i = 0; i < n; i++) { if (T.catj && ! octave::math::isnan (X(i, T.j))) { T.levj = std::max (T.levj, (octave_idx_type) X(i, T.j)); } if (T.catk && ! octave::math::isnan (X(i, T.k))) { T.levk = std::max (T.levk, (octave_idx_type) X(i, T.k)); } } } F.shift = 0.0; F.ntrees = 0; F.reason = "Terminated after training the requested number of trees."; ColumnVector f = F0; double dev = gamb_deviance (Y, f, method, wt); std::vector devhist; devhist.push_back (dev); ColumnVector grad (n), hess (n); for (octave_idx_type t = 0; t < maxtrees; t++) { bool accepted = false; double step = lrate; for (int h = 0; h <= GAMB_MAX_HALVINGS; h++) { std::vector> trial ((std::size_t) np); ColumnVector fnew = f; double shift = 0.0; bool grown = false; for (octave_idx_type q = 0; q < np; q++) { const GamInterTerm& T = F.term[(std::size_t) q]; for (octave_idx_type i = 0; i < n; i++) { if (method == 1) { double p = 1.0 / (1.0 + std::exp (-fnew(i))); grad(i) = Y(i) - p; hess(i) = p * (1.0 - p); if (hess(i) < 1e-12) { hess(i) = 1e-12; } } else { grad(i) = Y(i) - fnew(i); hess(i) = 1.0; } grad(i) *= wt(i); hess(i) *= wt(i); } std::vector tree = gamb_pair_tree (X, T.j, T.k, T.catj, T.catk, grad, hess, maxsplits, step); // A tree that splits on only one of the pair's predictors is a main // effect rather than an interaction, and adds nothing. Measured // against R2024a on 2026-09-15: with 'Interactions', 'all' its fit // skips such trees round by round, reproduced to 5e-11, and a pair // whose trees are all of this kind is never fitted at all. bool usesj = false; bool usesk = false; for (const PairTreeNode& node : tree) { usesj = usesj || (node.dim == 0); usesk = usesk || (node.dim == 1); } if (! (usesj && usesk)) { continue; } // Every row takes a value, a row missing a predictor that of the node // splitting on it. R2024a moves the root's value into the intercept: // its Intercept shifts by exactly that, with or without missing values, // while the pair contribution it predicts stays the tree's own. On // complete rows the root value is the tree's curvature-weighted mean. for (octave_idx_type i = 0; i < n; i++) { fnew(i) += gamb_pair_tree_value (tree, X(i, T.j), X(i, T.k)); } double m = tree[0].value; for (PairTreeNode& node : tree) { node.value -= m; } shift += m; trial[(std::size_t) q] = tree; grown = true; } // No pair kept a tree, so the fit is unchanged at any step. Comparing // the two equal deviances instead lets x87 excess precision on i386 // accept an empty round. if (! grown) { break; } double devnew = gamb_deviance (Y, fnew, method, wt); if (devnew < dev) { for (octave_idx_type q = 0; q < np; q++) { if (! trial[(std::size_t) q].empty ()) { F.term[(std::size_t) q].trees.push_back (trial[(std::size_t) q]); } } F.shift += shift; f = fnew; dev = devnew; F.ntrees++; accepted = true; break; } step *= 0.5; } if (! accepted) { F.reason = "Unable to improve the model fit."; break; } devhist.push_back (dev); octave_idx_type hn = (octave_idx_type) devhist.size (); if (hn > GAMB_PATIENCE) { double ref = devhist[(std::size_t) (hn - 1 - GAMB_PATIENCE)]; double need = ref - GAMB_REL_TOL * std::fabs (ref); bool better = false; for (octave_idx_type q = hn - GAMB_PATIENCE; q < hn; q++) { if (devhist[(std::size_t) q] < need) { better = true; break; } } if (! better) { F.reason = "Unable to improve the model fit."; break; } } } for (GamInterTerm& T : F.term) { gamb_pair_flatten (T); } F.deviance = dev; F.residuals = ColumnVector (n); for (octave_idx_type i = 0; i < n; i++) { if (method == 1) { F.residuals(i) = Y(i) - 1.0 / (1.0 + std::exp (-f(i))); } else { F.residuals(i) = Y(i) - f(i); } } return F; } statistics-release-2.1.2/src/gamboostinter.cc000066400000000000000000000363371527311104300213100ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include #include "gamboost.cpp" DEFUN_DLD(gamboostinter, args, , "-*- texinfo -*-\n\ @deftypefn {statistics} {@var{Mdl} =} gamboostinter (@var{X}, @var{Y}, @\n\ @var{F0}, @var{Method}, @var{Pairs}, @var{NumTrees}, @var{LearnRate}, @\n\ @var{MaxNumSplits})\n\ @deftypefnx {statistics} {@var{Mdl} =} gamboostinter (@dots{}, @var{W})\n\ @deftypefnx {statistics} {@var{Mdl} =} gamboostinter (@dots{}, @var{W}, @\n\ @var{Categorical})\n\ \n\ Boost trees over selected pairs of predictors.\n\ \n\ @code{@var{Mdl} = gamboostinter (@dots{})} fits the interaction phase of a\n\ generalized additive model, continuing from the additive prediction the\n\ predictor phase left rather than refitting it. It is used by\n\ @code{ClassificationGAM} and @code{RegressionGAM}, and it is not meant to be\n\ called directly.\n\ \n\ @itemize\n\ @item @var{X} is an @math{NxP} numeric matrix of predictors and @var{Y} the\n\ @math{Nx1} response, as @code{gamboosttrain} takes them.\n\ \n\ @item @var{F0} is the @math{Nx1} additive prediction of the predictor phase.\n\ The interaction phase starts from it, so its deviance is where this phase\n\ begins.\n\ \n\ @item @var{Method} selects what is boosted, @qcode{1} the logistic deviance\n\ and @qcode{2} the squared error.\n\ \n\ @item @var{Pairs} is an @math{Mx2} matrix of predictor index pairs, one-based\n\ and within range. Choosing them is the caller's business; see\n\ @code{gamboostpairs}.\n\ \n\ @item @var{NumTrees}, @var{LearnRate} and @var{MaxNumSplits} are the\n\ interaction phase's own budget, initial step and split limit.\n\ \n\ @item @var{W}, if given, is an @math{Nx1} vector of non-negative observation\n\ weights, as @code{gamboosttrain} takes them, and may be empty.\n\ \n\ @item @var{Categorical}, if given, flags the columns holding level codes, as\n\ @code{gamboosttrain} takes it. A pair tree splits such a predictor into two\n\ sets of levels, and its grid holds every level.\n\ @end itemize\n\ \n\ @var{Mdl} is a structure with the following fields.\n\ \n\ @itemize\n\ @item @qcode{PairBinEdges}, a @math{1xP} cell of the detection grid, eight\n\ equal-frequency bins per predictor, as MATLAB reports it.\n\ @item @qcode{PairEdges}, a @math{1xM} cell with one element per pair, a\n\ @math{1x2} cell of the cut points its trees used on its two predictors. A\n\ tree is fitted to the rows, cutting halfway between two values a node holds,\n\ keeping at least five rows in a leaf and growing a layer at a time within\n\ @var{MaxNumSplits}. A categorical predictor is cut into two sets of levels\n\ after sorting them by the step each would take alone; of cuts with equal\n\ gain the first in that order is kept, and a level none of a node's rows\n\ holds stops at that node. A tree that splits on only one of the pair's\n\ predictors is a main effect and adds nothing, so a round may leave a pair\n\ untouched.\n\ @item @qcode{PairValues}, a @math{1xM} cell of matrices, one value per\n\ cell of the pair's own grid.\n\ @item @qcode{PairMissing}, a @math{1xM} cell, one element per pair: a\n\ @math{1x3} cell of what a row missing the first predictor takes, one value\n\ per cell of the second; what a row missing the second takes, one value per\n\ cell of the first; and what a row missing both takes. A row missing the\n\ predictor a node splits on stops there, in training as in prediction.\n\ @item @qcode{Intercept}, the constant the recentred surfaces gave up. Add it\n\ to the intercept of the predictor phase.\n\ @item @qcode{NumTrees}, @qcode{ReasonForTermination}, @qcode{Deviance} and\n\ @qcode{Residuals}, as @code{gamboosttrain} reports them.\n\ @end itemize\n\ \n\ @seealso{gamboosttrain, gamboostpairs, gamboostpredict}\n\ @end deftypefn") { octave_idx_type nargin = args.length (); if (nargin != 8 && nargin != 9 && nargin != 10) { print_usage (); } if (! args(0).isnumeric () || args(0).iscomplex () || args(0).isempty ()) { error ("gamboostinter: X must be a numeric matrix."); } if (! args(1).isnumeric () || args(1).iscomplex () || args(1).isempty () || args(1).columns () != 1) { error ("gamboostinter: Y must be a numeric column vector."); } if (! args(2).isnumeric () || args(2).iscomplex () || args(2).isempty () || args(2).columns () != 1) { error ("gamboostinter: F0 must be a numeric column vector."); } if (args(0).rows () != args(1).rows () || args(0).rows () != args(2).rows ()) { error ("gamboostinter: X, Y and F0 must have the same number of rows."); } Matrix X = args(0).matrix_value (); ColumnVector Y = args(1).column_vector_value (); ColumnVector F0 = args(2).column_vector_value (); octave_idx_type d = X.columns (); if (! args(3).is_scalar_type () || ! args(3).isnumeric () || args(3).iscomplex ()) { error ("gamboostinter: Method must be a numeric scalar."); } int method = args(3).int_value (); if (method != 1 && method != 2) { error ("gamboostinter: Method must be 1 or 2."); } if (! args(4).isnumeric () || args(4).iscomplex () || args(4).isempty () || args(4).columns () != 2) { error ("gamboostinter: Pairs must be a numeric matrix with two columns."); } Matrix pairs = args(4).matrix_value (); for (octave_idx_type q = 0; q < pairs.rows (); q++) { for (octave_idx_type c = 0; c < 2; c++) { double v = pairs(q, c); if (v != std::floor (v) || v < 1.0 || v > (double) d) { error ("gamboostinter: Pairs must hold predictor indices " "between 1 and %d.", (int) d); } } if (pairs(q, 0) == pairs(q, 1)) { error ("gamboostinter: a pair must name two different predictors."); } } if (! args(5).is_scalar_type () || ! args(5).isnumeric () || args(5).iscomplex ()) { error ("gamboostinter: NumTrees must be a numeric scalar."); } octave_idx_type maxtrees = (octave_idx_type) args(5).int_value (); if (maxtrees < 1) { error ("gamboostinter: NumTrees must be a positive integer."); } if (! args(6).is_scalar_type () || ! args(6).isnumeric () || args(6).iscomplex ()) { error ("gamboostinter: LearnRate must be a numeric scalar."); } double lrate = args(6).scalar_value (); if (! (lrate > 0.0) || lrate > 1.0) { error ("gamboostinter: LearnRate must be greater than 0 and at most 1."); } if (! args(7).is_scalar_type () || ! args(7).isnumeric () || args(7).iscomplex ()) { error ("gamboostinter: MaxNumSplits must be a numeric scalar."); } octave_idx_type maxsplits = (octave_idx_type) args(7).int_value (); if (maxsplits < 1) { error ("gamboostinter: MaxNumSplits must be a positive integer."); } if (method == 1) { for (octave_idx_type i = 0; i < Y.numel (); i++) { if (Y(i) != 0.0 && Y(i) != 1.0) { error ("gamboostinter: Y must hold zeros and ones for Method 1."); } } } // Observation weights, one per row of X. The fit depends only on their // proportions, so their scale is free. ColumnVector W; bool weighted = false; if (nargin >= 9 && ! args(8).isempty ()) { if (! args(8).isnumeric () || args(8).iscomplex () || args(8).columns () != 1 || args(8).rows () != X.rows ()) { error ("gamboostinter: W must be a numeric column vector with one element per row of X."); } W = args(8).column_vector_value (); double sw = 0.0; for (octave_idx_type i = 0; i < W.numel (); i++) { if (! (W(i) >= 0.0) || octave::math::isinf (W(i))) { error ("gamboostinter: W must hold finite non-negative values."); } sw += W(i); } if (! (sw > 0.0)) { error ("gamboostinter: W must not sum to zero."); } weighted = true; } // Which columns are categorical, holding level codes. std::vector cat; if (nargin == 10) { cat = gamb_categorical_arg (args(9), X, "gamboostinter"); } GamInterFit F = gamb_boost_inter (X, Y, F0, method, pairs, maxtrees, lrate, maxsplits, weighted ? &W : nullptr, cat.empty () ? nullptr : &cat); Cell edges (1, d); for (octave_idx_type j = 0; j < d; j++) { edges(j) = octave_value (F.edges[(std::size_t) j]); } octave_idx_type np = (octave_idx_type) F.term.size (); Cell values (1, np); Cell pedges (1, np); Cell pmiss (1, np); for (octave_idx_type q = 0; q < np; q++) { const GamInterTerm& T = F.term[(std::size_t) q]; values(q) = octave_value (T.value); Cell ep (1, 2); ep(0) = octave_value (T.ej); ep(1) = octave_value (T.ek); pedges(q) = octave_value (ep); Cell pm (1, 3); pm(0) = octave_value (T.missj); pm(1) = octave_value (T.missk); pm(2) = octave_value (T.missboth); pmiss(q) = octave_value (pm); } octave_scalar_map Mdl; Mdl.assign ("PairBinEdges", octave_value (edges)); Mdl.assign ("PairEdges", octave_value (pedges)); Mdl.assign ("PairValues", octave_value (values)); Mdl.assign ("PairMissing", octave_value (pmiss)); Mdl.assign ("Intercept", octave_value (F.shift)); Mdl.assign ("NumTrees", octave_value ((double) F.ntrees)); Mdl.assign ("ReasonForTermination", octave_value (F.reason)); Mdl.assign ("Deviance", octave_value (F.deviance)); Mdl.assign ("Residuals", octave_value (F.residuals)); return ovl (Mdl); } /* %!test %! ## Every selected pair gets a surface on the grid of the cut points its %! ## trees used, and the phase reports the usual fields. %! x = randn (200, 3); %! y = double (x(:,1) .* x(:,2) + 0.1 * randn (200, 1) > 0); %! M = gamboosttrain (x, y, 1, 20, 1, 1); %! f0 = gamboostpredict (M.BinEdges, M.ShapeValues, x, M.Intercept); %! I = gamboostinter (x, y, f0, 1, [1, 2], 20, 1, 4); %! assert_equal (numel (I.PairValues), 1); %! assert_equal (numel (I.PairEdges), 1); %! assert_equal (size (I.PairValues{1}), ... %! [numel(I.PairEdges{1}{1}), numel(I.PairEdges{1}{2})] + 1); %! assert_equal (numel (I.PairMissing{1}{1}), numel (I.PairEdges{1}{2}) + 1); %! assert_equal (numel (I.PairMissing{1}{2}), numel (I.PairEdges{1}{1}) + 1); %! assert_equal (numel (I.PairBinEdges), 3); %! assert_equal (numel (I.Residuals), 200); %!test %! ## A row missing a predictor of the pair still takes part in the fit. %! x = randn (200, 2); %! y = double (x(:,1) .* x(:,2) > 0); %! x(1:10,1) = NaN; %! M = gamboosttrain (x, y, 1, 20, 1, 1); %! f0 = gamboostpredict (M.BinEdges, M.ShapeValues, x, M.Intercept); %! I = gamboostinter (x, y, f0, 1, [1, 2], 5, 1, 4); %! assert_equal (all (isfinite (I.Residuals)), true); %! assert_equal (all (isfinite ([I.PairMissing{1}{1}, I.PairMissing{1}{2}, ... %! I.PairMissing{1}{3}])), true); %!test %! ## The phase starts from the deviance the predictor phase left, so it can %! ## only improve on it. %! x = randn (200, 3); %! y = double (x(:,1) .* x(:,2) + 0.1 * randn (200, 1) > 0); %! M = gamboosttrain (x, y, 1, 20, 1, 1); %! f0 = gamboostpredict (M.BinEdges, M.ShapeValues, x, M.Intercept); %! I = gamboostinter (x, y, f0, 1, [1, 2], 30, 1, 4); %! assert_equal (I.Deviance <= M.Deviance, true); %!test %! ## A pair term is recentred like a shape function, so what it gives up is %! ## reported for the intercept rather than left inside the surface. %! x = randn (150, 2); %! y = double (x(:,1) .* x(:,2) > 0); %! M = gamboosttrain (x, y, 1, 20, 1, 1); %! f0 = gamboostpredict (M.BinEdges, M.ShapeValues, x, M.Intercept); %! I = gamboostinter (x, y, f0, 1, [1, 2], 25, 1, 4); %! assert_equal (isfinite (I.Intercept), true); %!test %! ## Several pairs are fitted in one call, each on its own grid. %! x = randn (200, 4); %! y = double (x(:,1) .* x(:,2) + x(:,3) .* x(:,4) > 0); %! M = gamboosttrain (x, y, 1, 20, 1, 1); %! f0 = gamboostpredict (M.BinEdges, M.ShapeValues, x, M.Intercept); %! I = gamboostinter (x, y, f0, 1, [1, 2; 3, 4], 20, 1, 4); %! assert_equal (numel (I.PairValues), 2); %!test %! ## The budget is a budget here too: a phase that stops improving says so. %! x = randn (120, 2); %! y = double (x(:,1) > 0); %! M = gamboosttrain (x, y, 1, 100, 1, 1); %! f0 = gamboostpredict (M.BinEdges, M.ShapeValues, x, M.Intercept); %! I = gamboostinter (x, y, f0, 1, [1, 2], 100000, 1, 4); %! assert_equal (I.ReasonForTermination, 'Unable to improve the model fit.'); %! assert_equal (I.NumTrees < 100000, true); %!test %! ## A regression pair term works the same way and lowers the residual sum %! ## of squares it was handed. %! x = randn (200, 2); %! y = x(:,1) .* x(:,2); %! M = gamboosttrain (x, y, 2, 20, 1, 1); %! f0 = gamboostpredict (M.BinEdges, M.ShapeValues, x, M.Intercept); %! I = gamboostinter (x, y, f0, 2, [1, 2], 40, 1, 4); %! assert_equal (I.Deviance < M.Deviance, true); %!test %! ## A categorical predictor of a pair is split by sets of levels, and its %! ## grid holds every level. %! k = (1:300)'; %! c = mod (k, 4) + 1; %! x = sin (k); %! y = (c == 2 | c == 4) .* x + 0.1 * cos (k); %! M = gamboosttrain ([c, x], y, 2, 20, 1, 1, 0, 10, [], [], [true, false]); %! f = gamboostpredict (M.BinEdges, M.ShapeValues, [c, x], M.Intercept); %! I = gamboostinter ([c, x], y, f, 2, [1, 2], 10, 1, 4, [], [true, false]); %! assert_equal (I.PairEdges{1}{1}, [1.5, 2.5, 3.5]); %! assert_equal (I.Deviance < M.Deviance, true); %!error gamboostinter (1, 2, 3) %!error ... %! gamboostinter ('a', [1;0], [0;0], 1, [1,2], 10, 1, 4) %!error ... %! gamboostinter ([1,2;3,4], [1;0], [0;0;0], 1, [1,2], 10, 1, 4) %!error ... %! gamboostinter ([1,2;3,4], [1;0], [0;0], 3, [1,2], 10, 1, 4) %!error ... %! gamboostinter ([1,2;3,4], [1;0], [0;0], 1, [1,2,3], 10, 1, 4) %!error ... %! gamboostinter ([1,2;3,4], [1;0], [0;0], 1, [1,5], 10, 1, 4) %!error ... %! gamboostinter ([1,2;3,4], [1;0], [0;0], 1, [2,2], 10, 1, 4) %!error ... %! gamboostinter ([1,2;3,4], [1;0], [0;0], 1, [1,2], 0, 1, 4) %!error ... %! gamboostinter ([1,2;3,4], [1;0], [0;0], 1, [1,2], 10, 2, 4) %!error ... %! gamboostinter ([1,2;3,4], [1;0], [0;0], 1, [1,2], 10, 1, 0) %!error ... %! gamboostinter ([1,2;3,4], [1;2], [0;0], 1, [1,2], 10, 1, 4) */ statistics-release-2.1.2/src/gamboostpairs.cc000066400000000000000000000212231527311104300212710ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include #include "gamboost.cpp" DEFUN_DLD(gamboostpairs, args, , "-*- texinfo -*-\n\ @deftypefn {statistics} {@var{S} =} gamboostpairs (@var{X}, @var{R})\n\ @deftypefnx {statistics} {@var{S} =} gamboostpairs (@var{X}, @var{R}, @\n\ @var{Categorical})\n\ \n\ Score every pair of predictors for an interaction.\n\ \n\ @code{@var{S} = gamboostpairs (@var{X}, @var{R})} lays the residuals @var{R}\n\ of a fitted additive model on the coarse grid of each pair of columns of\n\ @var{X} and returns the two-way analysis of variance @math{F} ratio testing\n\ what only the cells explain. It is used to rank candidate interactions for\n\ @code{ClassificationGAM} and @code{RegressionGAM}, and it is not meant to be\n\ called directly.\n\ \n\ The @math{p}-values are deliberately not computed here. Turning @math{F}\n\ into a probability needs @code{fcdf}, which the package already ships and\n\ which is verified against MATLAB, so the caller applies it rather than a\n\ second implementation being carried in the compiled engine. Which pairs to\n\ keep is policy and belongs beside that.\n\ \n\ @itemize\n\ @item @var{X} is an @math{NxP} numeric matrix of predictors, and @var{P} must\n\ be at least 2 for any pair to exist.\n\ \n\ @item @var{R} is the @math{Nx1} residual vector of the additive fit.\n\ \n\ @item @var{Categorical}, if given, flags the columns holding level codes, as\n\ @code{gamboosttrain} takes it. Such a predictor is laid on its levels\n\ rather than on eight bins.\n\ @end itemize\n\ \n\ @var{S} is a structure with the following fields, one row per pair, ordered\n\ as @code{nchoosek} orders them.\n\ \n\ @itemize\n\ @item @qcode{Pairs}, the @math{Mx2} matrix of predictor index pairs.\n\ @item @qcode{F}, the @math{Mx1} vector of @math{F} ratios. A pair with too\n\ few observations, no spare degrees of freedom or no within-cell scatter\n\ scores @qcode{0}.\n\ @item @qcode{DF1} and @qcode{DF2}, the @math{Mx1} numerator and denominator\n\ degrees of freedom.\n\ @item @qcode{BinEdges}, a @math{1xP} cell of the coarse cut points each\n\ predictor was laid on. The grid is fixed at eight equal-frequency bins,\n\ which is what MATLAB reports for pair detection at every sample size.\n\ @end itemize\n\ \n\ @seealso{gamboosttrain, gamboostpredict, fcdf, ClassificationGAM}\n\ @end deftypefn") { if (args.length () != 2 && args.length () != 3) { print_usage (); } if (! args(0).isnumeric () || args(0).iscomplex () || args(0).isempty ()) { error ("gamboostpairs: X must be a numeric matrix."); } if (! args(1).isnumeric () || args(1).iscomplex () || args(1).isempty () || args(1).columns () != 1) { error ("gamboostpairs: R must be a numeric column vector."); } if (args(0).rows () != args(1).rows ()) { error ("gamboostpairs: X and R must have the same number of rows."); } Matrix X = args(0).matrix_value (); ColumnVector R = args(1).column_vector_value (); octave_idx_type n = X.rows (); octave_idx_type d = X.columns (); if (d < 2) { error ("gamboostpairs: X must have at least two columns."); } // Which columns are categorical, holding level codes. std::vector cat ((std::size_t) d, false); if (args.length () == 3) { cat = gamb_categorical_arg (args(2), X, "gamboostpairs"); } std::vector B ((std::size_t) d); Cell edges (1, d); for (octave_idx_type j = 0; j < d; j++) { ColumnVector xj (n); for (octave_idx_type i = 0; i < n; i++) { xj(i) = X(i, j); } B[(std::size_t) j] = gamb_bin (xj, GAMB_PAIR_EDGES, cat[(std::size_t) j]); edges(j) = octave_value (B[(std::size_t) j].edges); } octave_idx_type m = d * (d - 1) / 2; Matrix pairs (m, 2); ColumnVector F (m), DF1 (m), DF2 (m); octave_idx_type row = 0; for (octave_idx_type j = 0; j < d - 1; j++) { for (octave_idx_type k = j + 1; k < d; k++) { GamPairStat S = gamb_pair_stat (B[(std::size_t) j], B[(std::size_t) k], R, j, k); pairs(row, 0) = (double) (j + 1); pairs(row, 1) = (double) (k + 1); F(row) = S.F; DF1(row) = S.df1; DF2(row) = S.df2; row++; } } octave_scalar_map out; out.assign ("Pairs", octave_value (pairs)); out.assign ("F", octave_value (F)); out.assign ("DF1", octave_value (DF1)); out.assign ("DF2", octave_value (DF2)); out.assign ("BinEdges", octave_value (edges)); return ovl (out); } /* %!test %! ## Every pair is scored once, in nchoosek order, and the grid is reported. %! x = randn (200, 4); %! r = randn (200, 1); %! S = gamboostpairs (x, r); %! assert_equal (size (S.Pairs), [6, 2]); %! assert_equal (S.Pairs, nchoosek (1:4, 2)); %! assert_equal (numel (S.F), 6); %! assert_equal (numel (S.BinEdges), 4); %!test %! ## The detection grid is fixed at eight equal-frequency bins, so seven cut %! ## points, whatever the sample size. MATLAB reports seven at 60, 250, 1000 %! ## and 4000 observations alike. %! for n = [60, 250, 1000] %! S = gamboostpairs (randn (n, 2), randn (n, 1)); %! assert_equal (numel (S.BinEdges{1}), 7); %! endfor %!test %! ## The cut points are the octiles, which is what MATLAB's coincide with. %! x = [(1:800)', (1:800)']; %! S = gamboostpairs (x, randn (800, 1)); %! q = linspace (0, 1, 9); %! assert_equal (S.BinEdges{1}, quantile (x(:,1), q(2:end-1)), 1); %!test %! ## A planted interaction outscores every pair that carries none. The %! ## response depends on x1 * x2 alone, so that pair must rank first. %! rand ('seed', 7); %! randn ('seed', 7); %! x = randn (400, 4); %! r = x(:,1) .* x(:,2) + 0.1 * randn (400, 1); %! S = gamboostpairs (x, r); %! [~, best] = max (S.F); %! assert_equal (S.Pairs(best,:), [1, 2]); %!test %! ## Structureless residuals score low: an F ratio near one is what a pair %! ## with no interaction should give, and the planted pair above is orders %! ## above it. %! randn ('seed', 11); %! S = gamboostpairs (randn (400, 3), randn (400, 1)); %! assert_equal (max (S.F) < 5, true); %!test %! ## A pair with nothing to explain scores zero rather than a NaN: a constant %! ## residual leaves no within-cell scatter to divide by. %! S = gamboostpairs (randn (50, 2), ones (50, 1)); %! assert_equal (S.F, 0); %! assert_equal (S.DF1, 0); %!test %! ## Degrees of freedom follow the occupied grid, not its nominal size. %! x = [(1:200)', (1:200)']; %! S = gamboostpairs (x, randn (200, 1)); %! assert_equal (S.DF2 > 0, true); %! assert_equal (S.DF1 > 0, true); %!test %! ## The detection grid places cut k at the midpoint of the sorted values at %! ## positions ceil (k*n/8) and ceil (k*n/8) + 1, as R2024a does. %! k = (1:300)'; %! S = gamboostpairs ([sin(k), cos(2 * k)], k / 300); %! e = [-0.92677666250429258, -0.70864210760326785, -0.37141809543750642, ... %! 0.0044253079075048506, 0.38778038850556862, 0.70866977421076549, ... %! 0.92681284546023668]; %! assert_equal (S.BinEdges{1}, e, 1e-15); %!test %! ## A categorical predictor keeps every level as a bin of its own. %! k = (1:240)'; %! S = gamboostpairs ([mod(k, 12) + 1, sin(k)], cos (k), [true, false]); %! assert_equal (S.BinEdges{1}, 1.5:11.5); %! assert_equal (numel (S.BinEdges{2}), 7); %!error ... %! gamboostpairs ([1, 2; 3, 4; 5, 6; 7, 8], [1; 2; 3; 4], true) %!error ... %! gamboostpairs ([1.5, 2; 3, 4; 5, 6; 7, 8], [1; 2; 3; 4], [true, false]) %!error gamboostpairs (1) %!error ... %! gamboostpairs ('a', [1;2]) %!error ... %! gamboostpairs ([1, 2; 3, 4], [1, 2]) %!error ... %! gamboostpairs ([1, 2; 3, 4], [1; 2; 3]) %!error ... %! gamboostpairs ([1; 2; 3], [1; 2; 3]) */ statistics-release-2.1.2/src/gamboostpredict.cc000066400000000000000000000363711527311104300216170ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include #include "gamboost.cpp" DEFUN_DLD(gamboostpredict, args, , "-*- texinfo -*-\n\ @deftypefn {statistics} {@var{Y} =} gamboostpredict (@var{BinEdges}, @\n\ @var{ShapeValues}, @var{X}, @var{Intercept})\n\ @deftypefnx {statistics} {@var{Y} =} gamboostpredict (@dots{}, @var{Link})\n\ @deftypefnx {statistics} {@var{Y} =} gamboostpredict (@dots{}, @var{Link}, @\n\ @var{PairEdges}, @var{PairValues}, @var{Pairs})\n\ @deftypefnx {statistics} {@var{Y} =} gamboostpredict (@dots{}, @var{Pairs}, @\n\ @var{PairMissing})\n\ \n\ Predict from a generalized additive model of boosted trees.\n\ \n\ @itemize\n\ @item @var{BinEdges} is a @math{1xP} cell of row vectors and\n\ @var{ShapeValues} a @math{1xP} cell of column vectors, as\n\ @code{gamboosttrain} returns them in the fields of the same names. Each\n\ shape function is a step function over its predictor's bins, so a term is\n\ evaluated by a lookup.\n\ \n\ @item @var{X} is an @math{NxP} numeric matrix with one column per additive\n\ term, and a count that does not match is an error. A missing value\n\ contributes nothing from that term rather than making the whole prediction\n\ @qcode{NaN}, which is what a tree does with a value it cannot place. A\n\ value outside the range the term was fitted over falls in the nearest bin,\n\ so a shape function is constant beyond its data rather than extrapolated.\n\ \n\ @item @var{Intercept} is the model's constant term.\n\ \n\ @item @var{Link}, if given, selects what the additive prediction is mapped\n\ through: @qcode{0} returns it as it stands and @qcode{1} takes it as a\n\ log-odds, returning the @math{Nx2} matrix of class probabilities whose\n\ second column is the logistic function of it. The default is @qcode{0}.\n\ \n\ @item @var{PairEdges}, @var{PairValues} and @var{Pairs} carry the\n\ interaction terms, as @code{gamboostinter} returns the first two and as it\n\ was given the third. Each pair contributes the value of the cell its two\n\ predictors fall in on the pair's own cut points. All three must be given\n\ together or none of them.\n\ \n\ @item @var{PairMissing}, as @code{gamboostinter} returns it, gives what a row\n\ missing either predictor of a pair, or both, takes. Without it such a pair\n\ contributes nothing.\n\ @end itemize\n\ \n\ @seealso{gamboosttrain, gampredict, ClassificationGAM, RegressionGAM}\n\ @end deftypefn") { octave_idx_type nargin = args.length (); if (nargin != 4 && nargin != 5 && nargin != 8 && nargin != 9) { print_usage (); } if (! args(0).iscell ()) { error ("gamboostpredict: BinEdges must be a cell array."); } if (! args(1).iscell ()) { error ("gamboostpredict: ShapeValues must be a cell array."); } Cell edges = args(0).cell_value (); Cell values = args(1).cell_value (); if (edges.numel () != values.numel ()) { error ("gamboostpredict: BinEdges and ShapeValues must have the same " "number of elements."); } if (! args(2).isnumeric () || args(2).iscomplex () || args(2).isempty ()) { error ("gamboostpredict: X must be a numeric matrix."); } Matrix X = args(2).matrix_value (); if (X.columns () != edges.numel ()) { error ("gamboostpredict: X must have one column per additive term."); } if (! args(3).is_scalar_type () || ! args(3).isnumeric () || args(3).iscomplex ()) { error ("gamboostpredict: Intercept must be a numeric scalar."); } double intercept = args(3).scalar_value (); int link = 0; if (nargin > 4) { if (! args(4).is_scalar_type () || ! args(4).isnumeric () || args(4).iscomplex ()) { error ("gamboostpredict: Link must be a numeric scalar."); } link = args(4).int_value (); if (link != 0 && link != 1) { error ("gamboostpredict: Link must be 0 or 1."); } } GamBoostFit F; F.intercept = intercept; octave_idx_type d = edges.numel (); F.edges.resize ((std::size_t) d); F.value.resize ((std::size_t) d); for (octave_idx_type j = 0; j < d; j++) { if (! edges(j).isnumeric () || edges(j).iscomplex ()) { error ("gamboostpredict: every BinEdges element must be numeric."); } if (! values(j).isnumeric () || values(j).iscomplex ()) { error ("gamboostpredict: every ShapeValues element must be numeric."); } F.edges[(std::size_t) j] = edges(j).row_vector_value (); F.value[(std::size_t) j] = values(j).column_vector_value (); if (F.value[(std::size_t) j].numel () != F.edges[(std::size_t) j].numel () + 1) { error ("gamboostpredict: term %d has %d values for %d cut points.", (int) j + 1, (int) F.value[(std::size_t) j].numel (), (int) F.edges[(std::size_t) j].numel ()); } } octave_idx_type n = X.rows (); ColumnVector y (n); for (octave_idx_type i = 0; i < n; i++) { y(i) = gamb_predict_row (F, X, i); } if (nargin >= 8) { if (! args(5).iscell () || ! args(6).iscell ()) { error ("gamboostpredict: PairEdges and PairValues must be cell arrays."); } Cell pedges = args(5).cell_value (); Cell pvals = args(6).cell_value (); if (! args(7).isnumeric () || args(7).iscomplex () || args(7).columns () != 2) { error ("gamboostpredict: Pairs must be a numeric matrix with two " "columns."); } Matrix pairs = args(7).matrix_value (); if (pairs.rows () != pvals.numel () || pairs.rows () != pedges.numel ()) { error ("gamboostpredict: Pairs, PairEdges and PairValues must describe " "the same number of pairs."); } bool with_missing = (nargin == 9); Cell pmiss; if (with_missing) { if (! args(8).iscell () || args(8).numel () != pairs.rows ()) { error ("gamboostpredict: PairMissing must be a cell array with one " "element per pair."); } pmiss = args(8).cell_value (); } for (octave_idx_type q = 0; q < pairs.rows (); q++) { octave_idx_type j = (octave_idx_type) pairs(q, 0) - 1; octave_idx_type k = (octave_idx_type) pairs(q, 1) - 1; if (j < 0 || k < 0 || j >= d || k >= d) { error ("gamboostpredict: Pairs must hold predictor indices between " "1 and %d.", (int) d); } if (! pedges(q).iscell () || pedges(q).numel () != 2) { error ("gamboostpredict: each element of PairEdges must be a cell of " "two row vectors."); } Cell ep = pedges(q).cell_value (); RowVector ej = ep(0).row_vector_value (); RowVector ek = ep(1).row_vector_value (); Matrix V = pvals(q).matrix_value (); if (V.rows () != ej.numel () + 1 || V.columns () != ek.numel () + 1) { error ("gamboostpredict: pair %d has a %dx%d surface for %d and %d " "cut points.", (int) q + 1, (int) V.rows (), (int) V.columns (), (int) ej.numel (), (int) ek.numel ()); } RowVector mj; RowVector mk; double mb = 0.0; if (with_missing) { if (! pmiss(q).iscell () || pmiss(q).numel () != 3) { error ("gamboostpredict: pair %d has missing-value entries that do " "not match its surface.", (int) q + 1); } Cell pm = pmiss(q).cell_value (); mj = pm(0).row_vector_value (); mk = pm(1).row_vector_value (); mb = pm(2).scalar_value (); if (mj.numel () != V.columns () || mk.numel () != V.rows ()) { error ("gamboostpredict: pair %d has missing-value entries that do " "not match its surface.", (int) q + 1); } } for (octave_idx_type i = 0; i < n; i++) { octave_idx_type a = gamb_bin_of (ej, X(i, j)); octave_idx_type b = gamb_bin_of (ek, X(i, k)); if (a >= 0 && b >= 0) { y(i) += V(a, b); } else if (with_missing) { if (a < 0 && b < 0) { y(i) += mb; } else if (a < 0) { y(i) += mj(b); } else { y(i) += mk(a); } } } } } if (link == 0) { return ovl (y); } Matrix score (n, 2); for (octave_idx_type i = 0; i < n; i++) { double pos = 1.0 / (1.0 + std::exp (-y(i))); score(i, 0) = 1.0 - pos; score(i, 1) = pos; } return ovl (score); } /* %!test %! ## The additive prediction is the intercept plus each term's step function. %! E = {[1.5, 2.5]}; %! V = {[-1; 0; 2]}; %! assert_equal (gamboostpredict (E, V, [1; 2; 3], 0), [-1; 0; 2], 1e-14); %! assert_equal (gamboostpredict (E, V, [1; 2; 3], 0.5), [-0.5; 0.5; 2.5], ... %! 1e-14); %!test %! ## Terms add. %! E = {[1.5], [10]}; %! V = {[1; 2], [4; 8]}; %! assert_equal (gamboostpredict (E, V, [1, 5; 2, 20], 0), [5; 10], 1e-14); %!test %! ## A value beyond the fitted range falls in the nearest bin, so a shape %! ## function is constant outside its data rather than extrapolated. %! E = {[1.5, 2.5]}; %! V = {[-1; 0; 2]}; %! assert_equal (gamboostpredict (E, V, [-100; 100], 0), [-1; 2], 1e-14); %!test %! ## A missing value costs that term only. %! E = {[1.5], [10]}; %! V = {[1; 2], [4; 8]}; %! assert_equal (gamboostpredict (E, V, [NaN, 5], 0), 4, 1e-14); %!test %! ## A term with no cut points is a constant. %! E = {zeros(1,0)}; %! V = {3}; %! assert_equal (gamboostpredict (E, V, [1; 2; 3], 1), [4; 4; 4], 1e-14); %!test %! ## The logistic link returns both class probabilities, and they sum to one. %! E = {[1.5]}; %! V = {[-2; 2]}; %! s = gamboostpredict (E, V, [1; 2], 0, 1); %! assert_equal (sum (s, 2), [1; 1], 1e-14); %! assert_equal (s(:,2), 1 ./ (1 + exp ([2; -2])), 1e-14); %!test %! ## A model round-trips through its own trainer. %! x = [1; 2; 3; 4; 5; 6; 7; 8]; %! y = [0; 0; 0; 0; 1; 1; 1; 1]; %! M = gamboosttrain (x, y, 1, 30, 1, 1); %! f = gamboostpredict (M.BinEdges, M.ShapeValues, x, M.Intercept); %! p = 1 ./ (1 + exp (-f)); %! assert_equal (M.Residuals, y - p, 1e-12); %!test %! ## A pair term adds the value of the cell its two predictors fall in. %! E = {zeros(1,0), zeros(1,0)}; %! V = {0, 0}; %! PE = {{[1.5], [10]}}; %! PV = {[1, 2; 3, 4]}; %! X = [1, 5; 1, 20; 2, 5; 2, 20]; %! assert_equal (gamboostpredict (E, V, X, 0, 0, PE, PV, [1, 2]), ... %! [1; 2; 3; 4], 1e-14); %!test %! ## Interaction terms add to the additive part rather than replacing it. %! E = {[1.5], zeros(1,0)}; %! V = {[10; 20], 0}; %! PE = {{[1.5], [10]}}; %! PV = {[1, 2; 3, 4]}; %! X = [1, 5; 2, 20]; %! assert_equal (gamboostpredict (E, V, X, 100, 0, PE, PV, [1, 2]), ... %! [111; 124], 1e-14); %!test %! ## A pair with either predictor missing contributes nothing, as a tree does %! ## with a value it cannot place. %! E = {zeros(1,0), zeros(1,0)}; %! V = {0, 0}; %! PE = {{[1.5], [10]}}; %! PV = {[1, 2; 3, 4]}; %! assert_equal (gamboostpredict (E, V, [NaN, 5], 0, 0, PE, PV, [1, 2]), 0); %!test %! ## A pair with a predictor missing takes the value stored for that case: by %! ## cell of the other predictor, or one value when both are missing. %! E = {zeros(1,0), zeros(1,0)}; %! V = {0, 0}; %! PE = {{[1.5], [10]}}; %! PV = {[1, 2; 3, 4]}; %! PM = {{[5, 6], [7, 8], 9}}; %! X = [NaN, 5; NaN, 20; 1, NaN; 2, NaN; NaN, NaN; 2, 20]; %! assert_equal (gamboostpredict (E, V, X, 0, 0, PE, PV, [1, 2], PM), ... %! [5; 6; 7; 8; 9; 4], 1e-14); %!test %! ## The assembled model reproduces what the interaction phase itself %! ## computed: same residuals, so the stored surfaces and the recentring %! ## bookkeeping are consistent with the fit they came from. %! randn ('seed', 42); %! rand ('seed', 42); %! X = randn (200, 3); %! Y = double (rand (200, 1) < 1 ./ (1 + exp (-2 * X(:,1) .* X(:,2)))); %! M = gamboosttrain (X, Y, 1, 50, 1, 1); %! f0 = gamboostpredict (M.BinEdges, M.ShapeValues, X, M.Intercept); %! I = gamboostinter (X, Y, f0, 1, [1, 2], 40, 1, 4); %! f1 = gamboostpredict (M.BinEdges, M.ShapeValues, X, ... %! M.Intercept + I.Intercept, 0, I.PairEdges, ... %! I.PairValues, [1, 2]); %! assert_equal (Y - 1 ./ (1 + exp (-f1)), I.Residuals, 1e-12); %!error gamboostpredict ({1}, {1}) %!error ... %! gamboostpredict (1, {1}, [1;2], 0) %!error ... %! gamboostpredict ({1}, 1, [1;2], 0) %!error ... %! gamboostpredict ({1, 2}, {1}, [1;2], 0) %!error ... %! gamboostpredict ({[1.5]}, {[1;2]}, 'a', 0) %!error ... %! gamboostpredict ({[1.5]}, {[1;2]}, [1, 2], 0) %!error ... %! gamboostpredict ({[1.5]}, {[1;2]}, [1;2], [1, 2]) %!error ... %! gamboostpredict ({[1.5]}, {[1;2]}, [1;2], 0, 2) %!error ... %! gamboostpredict ({[1.5]}, {[1;2;3]}, [1;2], 0) %!error ... %! gamboostpredict ({[1.5], [1.5]}, {[1;2], [1;2]}, [1, 1], 0, 0, 1, ... %! {1}, [1, 2]) %!error ... %! gamboostpredict ({[1.5], [1.5]}, {[1;2], [1;2]}, [1, 1], 0, 0, {[1.5]}, ... %! {[1, 2; 3, 4]}, [1, 2]) %!error ... %! gamboostpredict ({[1.5], [1.5]}, {[1;2], [1;2]}, [1, 1], 0, 0, ... %! {{[1.5], [1, 2]}}, {[1, 2; 3, 4]}, [1, 2]) %!error ... %! gamboostpredict ({[1.5], [1.5]}, {[1;2], [1;2]}, [1, 1], 0, 0, ... %! {[1.5], [1.5]}, {[1, 2; 3, 4]}, [1, 2, 3]) %!error ... %! gamboostpredict ({[1.5], [1.5]}, {[1;2], [1;2]}, [1, 1], 0, 0, ... %! {{[1.5], [1.5]}}, {[1, 2; 3, 4]}, [1, 2], 1) %!error ... %! gamboostpredict ({[1.5], [1.5]}, {[1;2], [1;2]}, [1, 1], 0, 0, ... %! {{[1.5], [1.5]}}, {[1, 2; 3, 4]}, [1, 2], ... %! {{[1, 2, 3], [1, 2], 0}}) %!error ... %! gamboostpredict ({[1.5], [1.5]}, {[1;2], [1;2]}, [1, 1], 0, 0, ... %! {[1.5], [1.5]}, {[1, 2; 3, 4]}, [1, 2; 1, 2]) */ statistics-release-2.1.2/src/gamboosttrain.cc000066400000000000000000000522741527311104300213020ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include #include "gamboost.cpp" DEFUN_DLD(gamboosttrain, args, , "-*- texinfo -*-\n\ @deftypefn {statistics} {@var{Mdl} =} gamboosttrain (@var{X}, @var{Y}, @\n\ @var{Method}, @var{NumTrees}, @var{LearnRate}, @var{MaxNumSplits})\n\ @deftypefnx {statistics} {@var{Mdl} =} gamboosttrain (@var{X}, @var{Y}, @\n\ @var{Method}, @var{NumTrees}, @var{LearnRate}, @var{MaxNumSplits}, @\n\ @var{Verbose}, @var{NumPrint}, @var{F0})\n\ @deftypefnx {statistics} {@var{Mdl} =} gamboosttrain (@dots{}, @var{F0}, @\n\ @var{W})\n\ @deftypefnx {statistics} {@var{Mdl} =} gamboosttrain (@dots{}, @var{F0}, @\n\ @var{W}, @var{Categorical})\n\ @deftypefnx {statistics} {@var{Mdl} =} gamboosttrain (@dots{}, @var{Verbose}, @\n\ @var{NumPrint})\n\ \n\ Fit a generalized additive model of boosted trees.\n\ \n\ @code{@var{Mdl} = gamboosttrain (@var{X}, @var{Y}, @var{Method}, @\n\ @var{NumTrees}, @var{LearnRate}, @var{MaxNumSplits})} boosts one tree per\n\ column of @var{X} in each round and returns the additive model as a\n\ structure. It is the fitting engine shared by @code{ClassificationGAM} and\n\ @code{RegressionGAM}, and it is not meant to be called directly.\n\ \n\ @itemize\n\ @item @var{X} is an @math{NxP} numeric matrix of predictors. A missing\n\ value is not an error: the observation takes no part in the affected\n\ predictor's trees and that term contributes nothing to its prediction.\n\ \n\ @item @var{Y} is an @math{Nx1} numeric vector of responses. For\n\ @var{Method} 1 it must hold zeros and ones.\n\ \n\ @item @var{Method} selects what is boosted: @qcode{1} the logistic deviance,\n\ as a classifier is fitted, and @qcode{2} the squared error, as a regression\n\ is fitted.\n\ \n\ @item @var{NumTrees} is the number of rounds, each fitting one tree per\n\ predictor. It is a budget rather than a count: a fit that stops improving\n\ ends earlier and says so.\n\ \n\ @item @var{LearnRate} is the step a round starts at. A round that fails to\n\ earn its place is retried at half the step, so this is an initial value and\n\ not a fixed one.\n\ \n\ @item @var{MaxNumSplits} is the largest number of splits any one tree may\n\ make. @qcode{1} is a stump.\n\ \n\ @item @var{Verbose}, if greater than zero, prints a trace of the fit, and\n\ @var{NumPrint} how often: the first round and then every @var{NumPrint}\n\ rounds. The @qcode{RelTol} column is the relative improvement the round\n\ bought, which is what the stopping rule reads. MATLAB prints a column under\n\ the same heading holding a quantity of its own that cannot be derived from\n\ anything else it reports, so the two are not comparable.\n\ \n\ @item @var{F0}, if given and not empty, continues a fit: it is the prediction\n\ the fit reached over these rows, and what comes back is the increment.\n\ \n\ @item @var{W}, if given, is an @math{Nx1} vector of non-negative observation\n\ weights. Only their proportions matter. Every sum the fit takes over the\n\ observations is weighted; the fewest observations a leaf may hold still\n\ counts rows. @var{F0} and @var{W} may each be empty.\n\ \n\ @item @var{Categorical}, if given, is a logical vector with one element per\n\ column of @var{X}. A flagged column holds level codes, positive integers,\n\ with a missing or unseen level @qcode{NaN}. Each level is a bin of its own,\n\ and a tree sorts the levels by the step each would take alone before it\n\ chooses its cut, as R2024a does.\n\ @end itemize\n\ \n\ @var{Mdl} is a structure with the following fields.\n\ \n\ @itemize\n\ @item @qcode{Intercept}, the constant term the additive terms are added to.\n\ For a classifier it is fitted rather than fixed: it is seeded with the\n\ log-odds of the response mean and then collects the constant each shape\n\ function gives up when it is recentred. For a regression it is the response\n\ mean and stays there.\n\ @item @qcode{BinEdges}, a @math{1xP} cell of row vectors, the cut points each\n\ predictor was binned at.\n\ @item @qcode{ShapeValues}, a @math{1xP} cell of column vectors, one value per\n\ bin. A shape function is a step function, so this is the whole of it however\n\ many trees produced it.\n\ @item @qcode{NumTrees}, the number of rounds actually performed.\n\ @item @qcode{ReasonForTermination}, why fitting stopped.\n\ @item @qcode{Deviance}, the deviance at the last round.\n\ @item @qcode{Residuals}, the @math{Nx1} residual vector at the last round.\n\ @end itemize\n\ \n\ @seealso{gamboostpredict, gamtrain, ClassificationGAM, RegressionGAM}\n\ @end deftypefn") { octave_idx_type nargin = args.length (); if (nargin != 6 && nargin != 8 && nargin != 9 && nargin != 10 && nargin != 11) { print_usage (); } if (! args(0).isnumeric () || args(0).iscomplex () || args(0).isempty ()) { error ("gamboosttrain: X must be a numeric matrix."); } if (! args(1).isnumeric () || args(1).iscomplex () || args(1).isempty () || args(1).columns () != 1) { error ("gamboosttrain: Y must be a numeric column vector."); } if (args(0).rows () != args(1).rows ()) { error ("gamboosttrain: X and Y must have the same number of rows."); } Matrix X = args(0).matrix_value (); ColumnVector Y = args(1).column_vector_value (); octave_idx_type d = X.columns (); if (! args(2).is_scalar_type () || ! args(2).isnumeric () || args(2).iscomplex ()) { error ("gamboosttrain: Method must be a numeric scalar."); } int method = args(2).int_value (); if (method != 1 && method != 2) { error ("gamboosttrain: Method must be 1 or 2."); } if (! args(3).is_scalar_type () || ! args(3).isnumeric () || args(3).iscomplex ()) { error ("gamboosttrain: NumTrees must be a numeric scalar."); } octave_idx_type maxtrees = (octave_idx_type) args(3).int_value (); if (maxtrees < 1) { error ("gamboosttrain: NumTrees must be a positive integer."); } if (! args(4).is_scalar_type () || ! args(4).isnumeric () || args(4).iscomplex ()) { error ("gamboosttrain: LearnRate must be a numeric scalar."); } double lrate = args(4).scalar_value (); if (! (lrate > 0.0) || lrate > 1.0) { error ("gamboosttrain: LearnRate must be greater than 0 and at most 1."); } if (! args(5).is_scalar_type () || ! args(5).isnumeric () || args(5).iscomplex ()) { error ("gamboosttrain: MaxNumSplits must be a numeric scalar."); } octave_idx_type maxsplits = (octave_idx_type) args(5).int_value (); if (maxsplits < 1) { error ("gamboosttrain: MaxNumSplits must be a positive integer."); } if (method == 1) { for (octave_idx_type i = 0; i < Y.numel (); i++) { if (Y(i) != 0.0 && Y(i) != 1.0) { error ("gamboosttrain: Y must hold zeros and ones for Method 1."); } } } int verbose = 0; octave_idx_type numprint = 10; if (nargin >= 8) { if (! args(6).is_scalar_type () || ! args(6).isnumeric () || args(6).iscomplex ()) { error ("gamboosttrain: Verbose must be a numeric scalar."); } verbose = args(6).int_value (); if (verbose < 0) { error ("gamboosttrain: Verbose must be 0 or greater."); } if (! args(7).is_scalar_type () || ! args(7).isnumeric () || args(7).iscomplex ()) { error ("gamboosttrain: NumPrint must be a numeric scalar."); } numprint = (octave_idx_type) args(7).int_value (); if (numprint < 1) { error ("gamboosttrain: NumPrint must be a positive integer."); } } // A ninth argument continues a fit that has already run: it is the // prediction that fit reached over these same rows, and what comes back is // the increment to add to the shape values and the intercept already held. ColumnVector F0; bool resuming = false; if (nargin >= 9 && ! args(8).isempty ()) { if (! args(8).isnumeric () || args(8).iscomplex () || args(8).columns () != 1 || args(8).rows () != X.rows ()) { error ("gamboosttrain: F0 must be a numeric column vector with one element per row of X."); } F0 = args(8).column_vector_value (); resuming = true; } // Observation weights, one per row of X. The fit depends only on their // proportions, so their scale is free. ColumnVector W; bool weighted = false; if (nargin >= 10 && ! args(9).isempty ()) { if (! args(9).isnumeric () || args(9).iscomplex () || args(9).columns () != 1 || args(9).rows () != X.rows ()) { error ("gamboosttrain: W must be a numeric column vector with one element per row of X."); } W = args(9).column_vector_value (); double sw = 0.0; for (octave_idx_type i = 0; i < W.numel (); i++) { if (! (W(i) >= 0.0) || octave::math::isinf (W(i))) { error ("gamboosttrain: W must hold finite non-negative values."); } sw += W(i); } if (! (sw > 0.0)) { error ("gamboosttrain: W must not sum to zero."); } weighted = true; } // Which columns are categorical, holding level codes. std::vector cat; if (nargin == 11) { cat = gamb_categorical_arg (args(10), X, "gamboosttrain"); } GamBoostFit F = gamb_boost (X, Y, method, maxtrees, lrate, maxsplits, verbose, numprint, resuming ? &F0 : nullptr, weighted ? &W : nullptr, cat.empty () ? nullptr : &cat); Cell edges (1, d); Cell values (1, d); for (octave_idx_type j = 0; j < d; j++) { edges(j) = octave_value (F.edges[(std::size_t) j]); values(j) = octave_value (F.value[(std::size_t) j]); } octave_scalar_map Mdl; Mdl.assign ("Intercept", octave_value (F.intercept)); Mdl.assign ("BinEdges", octave_value (edges)); Mdl.assign ("ShapeValues", octave_value (values)); Mdl.assign ("NumTrees", octave_value ((double) F.ntrees)); Mdl.assign ("ReasonForTermination", octave_value (F.reason)); Mdl.assign ("Deviance", octave_value (F.deviance)); Mdl.assign ("Residuals", octave_value (F.residuals)); return ovl (Mdl); } /* %!test %! ## Every field is present and the shapes follow the predictor count. %! x = [1 5; 2 4; 3 3; 4 2; 5 1; 6 7; 7 8; 8 9; 9 10; 10 11]; %! y = [0; 0; 0; 0; 0; 1; 1; 1; 1; 1]; %! M = gamboosttrain (x, y, 1, 50, 1, 1); %! assert_equal (isstruct (M), true); %! assert_equal (numel (M.BinEdges), 2); %! assert_equal (numel (M.ShapeValues), 2); %! assert_equal (numel (M.ShapeValues{1}), numel (M.BinEdges{1}) + 1); %! assert_equal (numel (M.Residuals), 10); %!test %! ## A predictor is cut at every midpoint between its distinct values. %! x = [1; 2; 3; 4; 2; 3]; %! y = [0; 0; 1; 1; 0; 1]; %! M = gamboosttrain (x, y, 1, 5, 1, 1); %! assert_equal (M.BinEdges{1}, [1.5, 2.5, 3.5], 1e-12); %!test %! ## Fewer than ten observations cannot be split at all: a leaf holds at %! ## least five. R2024a does the same, on fitcgam and fitrgam alike. The %! ## rounds still run and still count, because with no shape function to fit %! ## they reweight the intercept alone, and it converges on the log-odds of %! ## the response mean rather than being seeded there. R2024a reports %! ## exactly log (4/5) and a flat score on this fixture. %! x = (1:9)'; %! y = [zeros(5, 1); ones(4, 1)]; %! M = gamboosttrain (x, y, 1, 50, 1, 1); %! assert_equal (max (M.ShapeValues{1}) - min (M.ShapeValues{1}), 0, 1e-12); %! assert_equal (M.Intercept, log (4/5), 1e-10); %! assert_equal (M.ReasonForTermination, 'Unable to improve the model fit.'); %! M10 = gamboosttrain ((1:10)', [zeros(5, 1); ones(5, 1)], 1, 50, 1, 1); %! assert_equal (max (M10.ShapeValues{1}) - min (M10.ShapeValues{1}) > 1, true); %!test %! ## A constant predictor admits no split: one bin and no cut points. %! x = [2; 2; 2; 2]; %! y = [0; 1; 0; 1]; %! M = gamboosttrain (x, y, 1, 5, 1, 1); %! assert_equal (isempty (M.BinEdges{1}), true); %! assert_equal (numel (M.ShapeValues{1}), 1); %!test %! ## A regression intercept is the response mean and boosting leaves it there. %! x = (1:16)'; %! y = [2; 4; 5; 4; 6; 8; 9; 10; 11; 13; 12; 15; 16; 18; 17; 20]; %! M = gamboosttrain (x, y, 2, 40, 1, 1); %! assert_equal (M.NumTrees > 0, true); %! assert_equal (M.Intercept, mean (y), 1e-12); %!test %! ## A classifier intercept is seeded at zero, an even chance, and boosting %! ## moves it, so a balanced response does not stay there. %! ## Sixteen observations, not eight: a leaf holds at least five, so a fit %! ## with fewer than ten cannot split at all and its intercept never moves. %! x = (1:16)'; %! y = [zeros(8, 1); ones(8, 1)]; %! M = gamboosttrain (x, y, 1, 60, 1, 1); %! assert_equal (M.NumTrees > 0, true); %! assert_equal (M.Intercept != 0, true); %!test %! ## The tree budget is a budget: a fit that converges reports so and stops %! ## short of it. %! x = (1:16)'; %! y = [zeros(8, 1); ones(8, 1)]; %! M = gamboosttrain (x, y, 1, 5000, 1, 1); %! assert_equal (M.ReasonForTermination, 'Unable to improve the model fit.'); %! assert_equal (M.NumTrees > 0, true); %! assert_equal (M.NumTrees < 5000, true); %!test %! ## A budget too small to converge in reports the other reason. %! x = (1:16)'; %! y = [zeros(8, 1); ones(8, 1)]; %! M = gamboosttrain (x, y, 1, 2, 1, 1); %! assert_equal (M.ReasonForTermination, ... %! 'Terminated after training the requested number of trees.'); %! assert_equal (M.NumTrees, 2); %!test %! ## A single-class response has an infinite log-odds and nothing to fit. %! x = [1; 2; 3; 4]; %! y = [1; 1; 1; 1]; %! M = gamboosttrain (x, y, 1, 10, 1, 1); %! assert_equal (M.Intercept, Inf); %! assert_equal (M.NumTrees, 0); %!test %! ## A missing predictor value costs the observation that term, not the fit. %! x = [1; 2; NaN; 4; 5; 6; 7; 8; 9; 10; 11; 12]; %! y = [0; 0; 0; 0; 0; 0; 1; 1; 1; 1; 1; 1]; %! M = gamboosttrain (x, y, 1, 20, 1, 1); %! assert_equal (M.NumTrees > 0, true); %! assert_equal (isfinite (M.Intercept), true); %! assert_equal (any (isnan (M.ShapeValues{1})), false); %!test %! ## More splits per tree reach a lower deviance on a shape one split cannot %! ## follow. Thirty observations, not ten: three splits make four leaves and %! ## a leaf holds at least five, so ten could never use the larger budget. %! x = (1:30)'; %! y = [zeros(8, 1); ones(14, 1); zeros(8, 1)]; %! M1 = gamboosttrain (x, y, 1, 30, 1, 1); %! M2 = gamboosttrain (x, y, 1, 30, 1, 3); %! assert_equal (M2.Deviance < M1.Deviance, true); %!test %! ## Above the cap the cuts are equally spaced through the OBSERVATIONS, not %! ## through the distinct values, so they crowd where the data is. Values 1 %! ## to 50 carry 1000 of these 1450 rows and values 51 to 500 carry one row %! ## each: R2024a puts 49 cuts in that dense fifth of the range, one between %! ## each pair of neighbouring values. %! x = [repmat((1:50)', 20, 1); (51:500)']; %! y = double (mod (1:1450, 2))'; %! M = gamboosttrain (x, y, 1, 1, 1, 1); %! assert_equal (sum (M.BinEdges{1} < 50), 49); %!test %! ## Ties collapse cuts: two quantile positions inside one repeated value %! ## give the same cut, and the repeats are dropped, so a tied predictor ends %! ## with fewer than the cap allows rather than with duplicate edges. R2024a %! ## keeps 129 here, the first at 1.5. %! x = [repmat((1:50)', 20, 1); (51:500)']; %! y = double (mod (1:1450, 2))'; %! M = gamboosttrain (x, y, 1, 1, 1, 1); %! assert_equal (numel (M.BinEdges{1}), 129); %! assert_equal (numel (unique (M.BinEdges{1})), 129); %! assert_equal (M.BinEdges{1}(1:3), [1.5, 2.5, 3.5]); %!test %! ## The cap binds at 255 cut points however many distinct values there are. %! x = (1:2000)'; %! y = double (mod (1:2000, 2))'; %! M = gamboosttrain (x, y, 1, 1, 1, 1); %! assert_equal (numel (M.BinEdges{1}), 255); %!test %! ## Below the cap nothing is thinned: one cut per gap between distinct %! ## values, which is what MATLAB, scikit-learn and the EBM all report. %! x = (1:200)'; %! y = double (mod (1:200, 2))'; %! M = gamboosttrain (x, y, 1, 1, 1, 1); %! assert_equal (numel (M.BinEdges{1}), 199); %! assert_equal (M.BinEdges{1}(1:3), [1.5, 2.5, 3.5], 1e-12); %!test %! ## Patience looks across a window rather than at one round: a fit that has %! ## stopped earning its keep ends and says so, well short of its budget. %! x = [1; 2; 3; 4; 5; 6; 7; 8]; %! y = [0; 0; 0; 0; 1; 1; 1; 1]; %! M = gamboosttrain (x, y, 1, 100000, 1, 1); %! assert_equal (M.ReasonForTermination, 'Unable to improve the model fit.'); %! assert_equal (M.NumTrees < 1000, true); %!test %! ## A verbose fit prints a trace and returns the same model as a quiet one. %! x = [1; 2; 3; 4; 5; 6; 7; 8]; %! y = [0; 0; 0; 0; 1; 1; 1; 1]; %! Q = gamboosttrain (x, y, 1, 20, 1, 1); %! V = evalc ('W = gamboosttrain (x, y, 1, 20, 1, 1, 1, 5);'); %! assert_equal (W.Intercept, Q.Intercept, 1e-12); %! assert_equal (W.NumTrees, Q.NumTrees); %! assert_equal (! isempty (strfind (V, 'NumTrees')), true); %! assert_equal (! isempty (strfind (V, 'LearnRate')), true); %!test %! ## NumPrint controls how often a round is reported: the first, then every %! ## NumPrint after it. %! x = (1:16)'; %! y = [zeros(8, 1); ones(8, 1)]; %! V5 = evalc ('gamboosttrain (x, y, 1, 20, 1, 1, 1, 5);'); %! V1 = evalc ('gamboosttrain (x, y, 1, 20, 1, 1, 1, 1);'); %! assert_equal (numel (strfind (V1, '| 1D|')) > ... %! numel (strfind (V5, '| 1D|')), true); %!test %! ## Verbose 0 prints nothing at all. %! x = [1; 2; 3; 4; 5; 6; 7; 8]; %! y = [0; 0; 0; 0; 1; 1; 1; 1]; %! V = evalc ('gamboosttrain (x, y, 1, 20, 1, 1, 0, 5);'); %! assert_equal (isempty (strfind (V, '1D')), true); %!test %! ## Above 255 cuts the grid follows R2024a: cut k at the midpoint of the %! ## sorted values at positions ceil (k*n/256) and ceil (k*n/256) + 1. %! k = (1:700)'; %! X = [sin(k), cos(2.3 * k)]; %! M = gamboosttrain (X, X(:,1) + X(:,2), 2, 1, 1, 1); %! E = M.BinEdges{1}; %! assert_equal (numel (E), 255); %! assert_equal (E([1, 2, 3, 100, 255]), ... %! [-0.99991205908099445, -0.99963859878373096, ... %! -0.99920743414670821, -0.32997659740324592, ... %! 0.99995116627924441], 1e-15); %!test %! ## Where the two values at a position are equal the cut moves up to the %! ## next larger value, and repeated cuts are dropped, as R2024a does. %! k = (1:1000)'; %! x = round (sin (1.7 * k) * 300) / 300; %! M = gamboosttrain ([x, sin(k)], sin (k), 2, 1, 1, 1); %! E = M.BinEdges{1}; %! assert_equal (numel (E), 231); %! assert_equal (E([1, 2, 3, 100, 231]), ... %! [-0.99833333333333329, -0.995, -0.9916666666666667, ... %! -0.19166666666666665, 0.99833333333333329], 1e-15); %!test %! ## MATLAB parity: a categorical predictor's levels are sorted by the step %! ## each would take alone before a tree chooses its cut. Level codes stand %! ## for the levels, and a missing or unseen level is NaN. %! k = (0:119)'; %! c1 = mod (k, 3) + 1; %! x2 = sin (k); %! c3 = mod (floor (k / 2), 2) + 1; %! y = 5 * (c1 == 2) + 0.5 * x2 - 3 * (c3 == 2) + 0.1 * cos (k); %! M = gamboosttrain ([c1, x2, c3], y, 2, 1, 1, 1, 0, 10, [], [], ... %! [true, false, true]); %! assert_equal (M.BinEdges{1}, [1.5, 2.5]); %! assert_equal (M.Intercept, 0.1666859425, 1e-10); %! Q = [1, 0, 1; 2, 0, 1; 3, 0, 1; 1, 0, 2; 1, 0.5, 1; NaN, 0, 1; ... %! NaN, 0, 1; NaN, 0, 2]; %! yq = gamboostpredict (M.BinEdges, M.ShapeValues, Q, M.Intercept); %! assert_equal (yq', [-0.3546416246, 4.645371152, -0.3546416246, ... %! -3.30728817, 0.4241346437, 1.312029301, ... %! 1.312029301, -1.640617244], 1e-9); %!error gamboosttrain (1, 2, 3) %!error ... %! gamboosttrain ('a', [1;0], 1, 10, 1, 1) %!error ... %! gamboosttrain ([1;2], [1, 0], 1, 10, 1, 1) %!error ... %! gamboosttrain ([1;2;3], [1;0], 1, 10, 1, 1) %!error ... %! gamboosttrain ([1;2], [1;0], 3, 10, 1, 1) %!error ... %! gamboosttrain ([1;2], [1;0], 1, 0, 1, 1) %!error ... %! gamboosttrain ([1;2], [1;0], 1, 10, 0, 1) %!error ... %! gamboosttrain ([1;2], [1;0], 1, 10, 2, 1) %!error ... %! gamboosttrain ([1;2], [1;0], 1, 10, 1, 0) %!error ... %! gamboosttrain ([1;2], [1;2], 1, 10, 1, 1) %!error ... %! gamboosttrain ([1;2], [1;0], 1, 10, 1, 1, -1, 5) %!error ... %! gamboosttrain ([1;2], [1;0], 1, 10, 1, 1, 1, 0) */ statistics-release-2.1.2/src/gampredict.cc000066400000000000000000000226541527311104300205470ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include "gam.cpp" using namespace std; DEFUN_DLD(gampredict, args, , "-*- texinfo -*-\n\ @deftypefn {statistics} {@var{yFit} =} gampredict (@var{Parameters}, @\n\ @var{X}, @var{Intercept})\n\ @deftypefnx {statistics} {@var{score} =} gampredict (@var{Parameters}, @\n\ @var{X}, @var{Intercept}, @var{Link})\n\ \n\ \n\ Evaluate a generalized additive model on new data.\n\ \n\ @code{@var{yFit} = gampredict (@var{Parameters}, @var{X}, @var{Intercept})}\n\ adds the intercept to the sum of the additive terms evaluated at each row of\n\ @var{X} and returns the @math{Nx1} result. It is the prediction engine\n\ shared by @code{ClassificationGAM} and @code{RegressionGAM}, and it is not\n\ meant to be called directly.\n\ \n\ @itemize\n\ @item @var{Parameters} is a @math{1xP} structure array of piecewise\n\ polynomials, as returned by @code{gamtrain} in the field of the same name.\n\ \n\ @item @var{X} is an @math{NxP} numeric matrix with one column per additive\n\ term, and a count that does not match is an error. A model carrying\n\ interaction terms must therefore be given the augmented matrix, not the\n\ predictors alone.\n\ A missing value predicts @qcode{NaN}, since no term of the model is defined\n\ at it. A value outside the range the term was fitted over is extrapolated\n\ from the nearest piece, as @code{ppval} extrapolates.\n\ \n\ @item @var{Intercept} is the model's constant term.\n\ \n\ @item @var{Link}, if given, selects what the additive prediction is mapped\n\ through: @qcode{0} returns it as it stands and @qcode{1} takes it as a\n\ log-odds, returning the @math{Nx2} matrix of class probabilities whose\n\ second column is the logistic function of it. The default is @qcode{0}.\n\ @end itemize\n\ \n\ @seealso{gamtrain, ClassificationGAM, RegressionGAM, fitcgam, fitrgam}\n\ @end deftypefn") { octave_idx_type nargin = args.length (); if (nargin < 3 || nargin > 4) { print_usage (); } if (! args(0).isstruct () || args(0).isempty ()) { error ("gampredict: Parameters must be a structure array."); } octave_map params = args(0).map_value (); if (! params.isfield ("breaks") || ! params.isfield ("coefs")) { error ("gampredict: Parameters must have 'breaks' and 'coefs' fields."); } if (! args(1).isnumeric () || args(1).iscomplex () || args(1).isempty ()) { error ("gampredict: X must be a numeric matrix."); } Matrix X = args(1).matrix_value (); octave_idx_type n = X.rows (); octave_idx_type d = X.columns (); // One column per additive term, exactly. The m-code this replaces looped // over the columns of X and took the term count from them, which tolerated // a caller passing a matrix that had not been augmented with its // interaction terms: too many terms went unnoticed and evaluated a // truncated model, while too few raised an out-of-bound index. That // contract was not self-consistent, and the direction it tolerated is the // one that returns a wrong number instead of an error. if (params.numel () != d) { error ("gampredict: X must have one column per additive term."); } if (! args(2).is_scalar_type () || ! args(2).isnumeric () || args(2).iscomplex ()) { error ("gampredict: Intercept must be a numeric scalar."); } double intercept = args(2).scalar_value (); int link = 0; if (nargin > 3) { if (! args(3).is_scalar_type () || ! args(3).isnumeric () || args(3).iscomplex ()) { error ("gampredict: Link must be a numeric scalar."); } link = args(3).int_value (); if (link != 0 && link != 1) { error ("gampredict: Link must be either 0 or 1."); } } Cell c_breaks = params.contents ("breaks"); Cell c_coefs = params.contents ("coefs"); ColumnVector y (n, intercept); for (octave_idx_type j = 0; j < d; j++) { if (! c_breaks(j).isnumeric () || ! c_coefs(j).isnumeric ()) { error ("gampredict: 'breaks' and 'coefs' must be numeric."); } RowVector breaks = c_breaks(j).row_vector_value (); Matrix coefs = c_coefs(j).matrix_value (); if (breaks.numel () != coefs.rows () + 1) { error ("gampredict: term %d has %d breaks for %d pieces.", (int) j + 1, (int) breaks.numel (), (int) coefs.rows ()); } for (octave_idx_type i = 0; i < n; i++) { y(i) += gam_ppval (breaks, coefs, X(i, j)); } } if (link == 0) { return ovl (y); } Matrix score (n, 2); for (octave_idx_type i = 0; i < n; i++) { double pos = 1.0 / (1.0 + exp (-y(i))); score(i, 0) = 1.0 - pos; score(i, 1) = pos; } return ovl (score); } /* %!test %! ## The additive prediction is the intercept plus each term's spline %! x = linspace (0, 1, 40)'; %! pp = splinefit (x, cos (3*x), 5, 'order', 3); %! P = struct ('form', 'pp', 'breaks', pp.breaks, 'coefs', pp.coefs, ... %! 'pieces', pp.pieces, 'order', pp.order, 'dim', pp.dim); %! assert_equal (gampredict (P, x, 0), ppval (pp, x), 1e-14); %! assert_equal (gampredict (P, x, 2.5), ppval (pp, x) + 2.5, 1e-14); %!test %! ## Two terms add %! x = linspace (0, 1, 30)'; %! p1 = splinefit (x, cos (3*x), 5, 'order', 3); %! p2 = splinefit (x, x.^2, 4, 'order', 2); %! P(1) = struct ('form', 'pp', 'breaks', p1.breaks, 'coefs', p1.coefs, ... %! 'pieces', p1.pieces, 'order', p1.order, 'dim', p1.dim); %! P(2) = struct ('form', 'pp', 'breaks', p2.breaks, 'coefs', p2.coefs, ... %! 'pieces', p2.pieces, 'order', p2.order, 'dim', p2.dim); %! y = gampredict (P, [x, x], 1); %! assert_equal (y, 1 + ppval (p1, x) + ppval (p2, x), 1e-13); %!test %! ## Fewer columns than terms is an error, not a truncated model. A caller %! ## that passes the predictors alone to a model carrying interaction terms %! ## used to be answered with the prediction of its leading terms. %! x = linspace (0, 1, 30)'; %! p1 = splinefit (x, cos (3*x), 5, 'order', 3); %! p2 = splinefit (x, x.^2, 4, 'order', 2); %! P2(1) = struct ('form', 'pp', 'breaks', p1.breaks, 'coefs', p1.coefs, ... %! 'pieces', p1.pieces, 'order', p1.order, 'dim', p1.dim); %! P2(2) = struct ('form', 'pp', 'breaks', p2.breaks, 'coefs', p2.coefs, ... %! 'pieces', p2.pieces, 'order', p2.order, 'dim', p2.dim); %! fail ('gampredict (P2, x, 0)', ... %! 'gampredict: X must have one column per additive term.'); %! assert_equal (gampredict (P2, [x, x], 0), ... %! ppval (p1, x) + ppval (p2, x), 1e-13); %!test %! ## The logistic link returns both class probabilities, and they sum to one %! x = linspace (0, 1, 20)'; %! pp = splinefit (x, 4*x - 2, 5, 'order', 3); %! P = struct ('form', 'pp', 'breaks', pp.breaks, 'coefs', pp.coefs, ... %! 'pieces', pp.pieces, 'order', pp.order, 'dim', pp.dim); %! s = gampredict (P, x, 0, 1); %! assert_equal (size (s), [20, 2]); %! assert_equal (sum (s, 2), ones (20, 1), 1e-14); %! assert_equal (s(:,2), 1 ./ (1 + exp (- gampredict (P, x, 0))), 1e-14); %!test %! ## A point outside the fitted range is extrapolated from the nearest piece, %! ## exactly as ppval extrapolates %! x = linspace (0, 1, 30)'; %! pp = splinefit (x, cos (3*x), 5, 'order', 3); %! P = struct ('form', 'pp', 'breaks', pp.breaks, 'coefs', pp.coefs, ... %! 'pieces', pp.pieces, 'order', pp.order, 'dim', pp.dim); %! xq = [-0.5; 1.5]; %! assert_equal (gampredict (P, xq, 0), ppval (pp, xq), 1e-13); %!test %! ## A missing predictor predicts NaN, and leaves the other rows alone %! x = linspace (0, 1, 10)'; %! pp = splinefit (x, cos (3*x), 4, 'order', 3); %! P = struct ('form', 'pp', 'breaks', pp.breaks, 'coefs', pp.coefs, ... %! 'pieces', pp.pieces, 'order', pp.order, 'dim', pp.dim); %! xq = [0.2; NaN; 0.8]; %! y = gampredict (P, xq, 0); %! assert (isnan (y(2))); %! assert_equal (y([1, 3]), ppval (pp, [0.2; 0.8]), 1e-14); %!shared P %! pp = splinefit (linspace (0, 1, 20)', linspace (0, 1, 20)', 4, 'order', 3); %! P = struct ('form', 'pp', 'breaks', pp.breaks, 'coefs', pp.coefs, ... %! 'pieces', pp.pieces, 'order', pp.order, 'dim', pp.dim); %!error gampredict () %!error gampredict (P, ones (5, 1)) %!error gampredict (P, ones (5, 1), 0, 1, 2) %!error ... %! gampredict (5, ones (5, 1), 0) %!error gampredict (P, 'a', 0) %!error ... %! gampredict (P, ones (5, 3), 0) %!error ... %! gampredict (P, ones (5, 1), [1, 2]) %!error ... %! gampredict (P, ones (5, 1), 0, 'a') %!error ... %! gampredict (P, ones (5, 1), 0, 2) */ statistics-release-2.1.2/src/gamtrain.cc000066400000000000000000000314341527311104300202260ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include "gam.cpp" using namespace std; DEFUN_DLD(gamtrain, args, , "-*- texinfo -*-\n\ @deftypefn {statistics} {@var{Mdl} =} gamtrain (@var{X}, @var{Y}, @\n\ @var{Knots}, @var{Order}, @var{Method}, @var{Inter}, @var{P1}, @var{P2})\n\ \n\ \n\ Fit a generalized additive model of smoothing splines.\n\ \n\ @code{@var{Mdl} = gamtrain (@var{X}, @var{Y}, @var{Knots}, @var{Order}, @\n\ @var{Method}, @var{Inter}, @var{P1}, @var{P2})} fits one univariate spline\n\ per column of @var{X} and returns the additive model as a structure. It is\n\ the fitting engine shared by @code{ClassificationGAM} and\n\ @code{RegressionGAM}, and it is not meant to be called directly.\n\ \n\ @itemize\n\ @item @var{X} is an @math{NxP} numeric matrix of predictors. A missing\n\ value is not an error: the observation is left out of the affected\n\ predictor's spline and its prediction from that term is @qcode{NaN}.\n\ \n\ @item @var{Y} is an @math{Nx1} numeric vector of responses. For\n\ @var{Method} 1 it must hold zeros and ones.\n\ \n\ @item @var{Knots} is a @math{1xP} vector giving the number of spline pieces\n\ for each predictor, and @var{Order} a @math{1xP} vector giving the degree of\n\ the polynomial on each piece. A spline of @math{K} pieces and degree\n\ @math{D} spans a space of @math{K + D} dimensions.\n\ \n\ @item @var{Method} selects the fitting scheme: @qcode{1} boosts the log-odds\n\ by gradient descent, as a classifier is fitted, and @qcode{2} backfits the\n\ partial residuals, as a regression is fitted.\n\ \n\ @item @var{Inter} is the intercept the fit starts from: a proportion for\n\ @var{Method} 1, which is stored as its log-odds, and the response mean for\n\ @var{Method} 2. A proportion of zero or one is not an error: its log-odds is\n\ infinite, the gradient is zero throughout and every additive term stays at\n\ zero, which is the fit a single-class response has.\n\ \n\ @item @var{P1} and @var{P2} are the scheme's two parameters. For\n\ @var{Method} 1 they are the learning rate and the number of boosting\n\ iterations; for @var{Method} 2 the convergence tolerance and the maximum\n\ number of backfitting cycles.\n\ @end itemize\n\ \n\ @var{Mdl} is a structure with the following fields.\n\ \n\ @itemize\n\ @item @qcode{Intercept}, the constant term the additive terms are added to.\n\ @item @qcode{Parameters}, a @math{1xP} structure array of piecewise\n\ polynomials in the form @code{ppval} consumes, one per predictor.\n\ @item @qcode{Iterations}, the number of iterations performed.\n\ @item @qcode{Residuals}, the @math{Nx1} residual vector at the last\n\ iteration.\n\ @item @qcode{RSS}, the scalar residual sum of squares for @var{Method} 1 and\n\ the @math{1xP} per-term criterion the backfitting stops on for @var{Method}\n\ 2.\n\ @end itemize\n\ \n\ @seealso{gampredict, ClassificationGAM, RegressionGAM, fitcgam, fitrgam}\n\ @end deftypefn") { if (args.length () != 8) { print_usage (); } // X and Y if (! args(0).isnumeric () || args(0).iscomplex () || args(0).isempty ()) { error ("gamtrain: X must be a numeric matrix."); } if (! args(1).isnumeric () || args(1).iscomplex () || args(1).isempty () || args(1).columns () != 1) { error ("gamtrain: Y must be a numeric column vector."); } if (args(0).rows () != args(1).rows ()) { error ("gamtrain: X and Y must have the same number of rows."); } Matrix X = args(0).matrix_value (); ColumnVector Y = args(1).column_vector_value (); octave_idx_type d = X.columns (); // Knots and Order, one per predictor if (! args(2).isnumeric () || args(2).iscomplex () || args(2).numel () != d) { error ("gamtrain: Knots must have one element per column of X."); } if (! args(3).isnumeric () || args(3).iscomplex () || args(3).numel () != d) { error ("gamtrain: Order must have one element per column of X."); } Matrix knots = args(2).matrix_value (); Matrix order = args(3).matrix_value (); for (octave_idx_type j = 0; j < d; j++) { if (knots(j) < 1 || floor (knots(j)) != knots(j)) { error ("gamtrain: Knots must be positive integers."); } // Order zero is the m-code's own lower bound and gives piecewise // constants: splinefit takes the degree, so a degree of zero is a spline // of order one. Rejecting it here would narrow what the learners accept. if (order(j) < 0 || floor (order(j)) != order(j)) { error ("gamtrain: Order must be non-negative integers."); } } // Method, intercept and the scheme's two parameters if (! args(4).is_scalar_type () || ! args(4).isnumeric () || args(4).iscomplex ()) { error ("gamtrain: Method must be a numeric scalar."); } int method = args(4).int_value (); if (method != 1 && method != 2) { error ("gamtrain: Method must be either 1 or 2."); } if (! args(5).is_scalar_type () || ! args(5).isnumeric () || args(5).iscomplex ()) { error ("gamtrain: Inter must be a numeric scalar."); } double inter = args(5).scalar_value (); if (! args(6).is_scalar_type () || ! args(6).isnumeric () || args(6).iscomplex () || args(6).scalar_value () <= 0) { error ("gamtrain: P1 must be a positive scalar."); } if (! args(7).is_scalar_type () || ! args(7).isnumeric () || args(7).iscomplex () || args(7).scalar_value () < 1) { error ("gamtrain: P2 must be a scalar not less than 1."); } double p1 = args(6).scalar_value (); octave_idx_type p2 = (octave_idx_type) args(7).scalar_value (); GamFit fit; if (method == 1) { // Inter is not restricted to the open unit interval. A single-class fit // is allowed and gives a proportion of exactly zero or one, whose log-odds // is infinite; the gradient is then zero everywhere and every additive // term stays at zero, which is the answer such a fit has. fit = gam_boost (X, Y, inter, knots, order, p1, p2); } else { fit = gam_backfit (X, Y, inter, knots, order, p1, p2); } octave_scalar_map Mdl; Mdl.assign ("Intercept", fit.intercept); Mdl.assign ("Parameters", fit.params); Mdl.assign ("Iterations", fit.iterations); Mdl.assign ("Residuals", fit.res); Mdl.assign ("RSS", fit.RSS); return ovl (Mdl); } /* %!test %! ## The boosted model returns the fields the learners store %! X = [linspace(0, 1, 40)', linspace(1, 2, 40)']; %! Y = double ([1:40]' > 20); %! Mdl = gamtrain (X, Y, [5, 5], [3, 3], 1, 0.5, 0.1, 100); %! assert_equal (fieldnames (Mdl), ... %! {'Intercept'; 'Parameters'; 'Iterations'; 'Residuals'; 'RSS'}); %! assert_equal (size (Mdl.Parameters), [1, 2]); %! assert_equal (Mdl.Iterations, 100); %! assert_equal (size (Mdl.Residuals), [40, 1]); %! assert_equal (size (Mdl.RSS), [1, 1]); %! assert_equal (Mdl.Intercept, log (0.5 / 0.5), 1e-14); %!test %! ## A spline of K pieces and degree D is a piecewise polynomial of K pieces %! ## and K + D coefficients per term %! X = linspace (0, 1, 30)'; %! Y = double ([1:30]' > 15); %! Mdl = gamtrain (X, Y, 6, 3, 1, 0.5, 0.1, 20); %! P = Mdl.Parameters(1); %! assert_equal (P.form, 'pp'); %! assert_equal (P.pieces, 6); %! assert_equal (P.order, 4); %! assert_equal (P.dim, 1); %! assert_equal (size (P.coefs), [6, 4]); %! assert_equal (size (P.breaks), [1, 7]); %!test %! ## The engine reproduces splinefit on a single fitted term. One boosting %! ## round at a learning rate of one is the spline of the gradient, which for %! ## an intercept of one half is the response less one half. %! x = linspace (0, 1, 50)'; %! Y = double (x > 0.4); %! Mdl = gamtrain (x, Y, 5, 3, 1, 0.5, 1, 1); %! pp = splinefit (x, Y - 0.5, 5, 'order', 3); %! assert_equal (Mdl.Parameters(1).coefs, pp.coefs, 1e-9); %! assert_equal (Mdl.Parameters(1).breaks, pp.breaks, 1e-14); %!test %! ## Backfitting a single term converges to the spline of the centred %! ## response, which is what one cycle already fits %! x = linspace (0, 2*pi, 60)'; %! Y = cos (x); %! Mdl = gamtrain (x, Y, 5, 3, 2, mean (Y), 1e-3, 1000); %! pp = splinefit (x, Y - mean (Y), 5, 'order', 3); %! assert_equal (Mdl.Parameters(1).coefs, pp.coefs, 1e-9); %! assert_equal (Mdl.Intercept, mean (Y), 1e-14); %! assert_equal (size (Mdl.RSS), [1, 1]); %!test %! ## Backfitting stops on the tolerance, and a looser one stops sooner %! X = [linspace(0, 1, 50)', linspace(0, 2, 50)']; %! Y = cos (3 * X(:,1)) + X(:,2); %! M1 = gamtrain (X, Y, [5, 5], [3, 3], 2, mean (Y), 1e-12, 1000); %! M2 = gamtrain (X, Y, [5, 5], [3, 3], 2, mean (Y), 1, 1000); %! assert (M2.Iterations <= M1.Iterations); %! assert_equal (size (M1.RSS), [1, 2]); %!test %! ## Fewer observations than the spline space has dimensions: the fit is the %! ## minimum norm one and does not raise %! x = [1; 2; 3; 4; 5]; %! Y = [2; 1; 4; 3; 6]; %! Mdl = gamtrain (x, Y, 5, 3, 2, mean (Y), 1e-3, 1000); %! assert_equal (size (Mdl.Parameters(1).coefs), [5, 4]); %! assert (all (isfinite (Mdl.Parameters(1).coefs(:)))); %!test %! ## A missing predictor drops the observation from that term's fit and %! ## predicts NaN for it, leaving the other observations finite %! x = linspace (0, 1, 40)'; %! x(7) = NaN; %! Y = double ([1:40]' > 20); %! Mdl = gamtrain (x, Y, 5, 3, 1, 0.5, 0.1, 10); %! assert (isnan (Mdl.Residuals(7))); %! assert (all (isfinite (Mdl.Residuals([1:6, 8:40])))); %!test %! ## Tied and unsorted predictor values are handled by the break placement %! x = [0.5; 0.5; 0.1; 0.9; 0.1; 0.7; 0.3; 0.5; 0.2; 0.6; 0.4; 0.8]; %! Y = [0; 0; 0; 1; 0; 1; 0; 1; 0; 1; 0; 1]; %! Mdl = gamtrain (x, Y, 5, 3, 1, 0.5, 0.1, 20); %! assert (all (isfinite (Mdl.Parameters(1).coefs(:)))); %! assert (issorted (Mdl.Parameters(1).breaks)); %!error gamtrain () %!error gamtrain (ones (5, 2), ones (5, 1)) %!error ... %! gamtrain ('a', ones (5, 1), 5, 3, 1, 0.5, 0.1, 10) %!error ... %! gamtrain (ones (5, 1), ones (5, 2), 5, 3, 1, 0.5, 0.1, 10) %!error ... %! gamtrain (ones (5, 1), ones (4, 1), 5, 3, 1, 0.5, 0.1, 10) %!error ... %! gamtrain (ones (5, 2), ones (5, 1), 5, [3, 3], 1, 0.5, 0.1, 10) %!error ... %! gamtrain (ones (5, 2), ones (5, 1), [5, 5], 3, 1, 0.5, 0.1, 10) %!error ... %! gamtrain (ones (5, 1), ones (5, 1), 2.5, 3, 1, 0.5, 0.1, 10) %!error ... %! gamtrain (ones (5, 1), ones (5, 1), 5, -1, 1, 0.5, 0.1, 10) %!error ... %! gamtrain (ones (5, 1), ones (5, 1), 5, 1.5, 1, 0.5, 0.1, 10) %!error ... %! gamtrain (ones (5, 1), ones (5, 1), 5, 3, 'a', 0.5, 0.1, 10) %!error ... %! gamtrain (ones (5, 1), ones (5, 1), 5, 3, 3, 0.5, 0.1, 10) %!error ... %! gamtrain (ones (5, 1), ones (5, 1), 5, 3, 1, [1, 2], 0.1, 10) %!error ... %! gamtrain (ones (5, 1), ones (5, 1), 5, 3, 1, 0.5, 0, 10) %!error ... %! gamtrain (ones (5, 1), ones (5, 1), 5, 3, 1, 0.5, 0.1, 0) %!test %! ## Order zero is piecewise constants, which the learners accept and the %! ## m-code fitted as splines of order one %! x = linspace (0, 1, 30)'; %! Y = double (x > 0.5); %! Mdl = gamtrain (x, Y, 5, 0, 1, 0.5, 0.1, 20); %! P = Mdl.Parameters(1); %! assert_equal (P.pieces, 5); %! assert_equal (P.order, 1); %! assert_equal (size (P.coefs), [5, 1]); %! pp = splinefit (x, Y - 0.5, 5, 'order', 0); %! M1 = gamtrain (x, Y, 5, 0, 1, 0.5, 1, 1); %! assert_equal (M1.Parameters(1).coefs, pp.coefs, 1e-12); %!test %! ## A single-class response gives an infinite intercept and terms that stay %! ## at zero, which is what the m-code it replaces produced %! x = linspace (0, 1, 20)'; %! Mdl = gamtrain (x, ones (20, 1), 5, 3, 1, 1, 0.1, 10); %! assert_equal (Mdl.Intercept, Inf); %! assert_equal (Mdl.Parameters(1).coefs, zeros (5, 4)); %! assert_equal (Mdl.Residuals, zeros (20, 1)); */ statistics-release-2.1.2/src/lbfgs.h000066400000000000000000000522051527311104300173620ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ // Limited-memory or full BFGS with a strong or weak Wolfe line search. // // The engine is deliberately free of any Octave type so that it can serve two // callers with opposite needs: a compiled learner whose objective is C++ and // which must never cross the interpreter boundary, and the __lbfgs__ oct-file, // whose objective is an Octave function handle. The objective is therefore a // template parameter, and anything modelling // // double operator () (const std::vector &x, std::vector &g) // // which returns the value at x and writes the gradient into g, will do. // // Stopping behaviour follows MATLAB's, as measured on R2024a with the // Statistics and Machine Learning Toolbox 24.1. Three findings are baked in // here because they are not what the documentation's wording suggests, and // a fourth that it does not state at all: // // 1. All three tolerances are ABSOLUTE comparisons against the scalar the // iteration records, not relative to that scalar's starting value. // MATLAB reports "Relative gradient tolerance reached." but stops at the // first iteration whose recorded gradient falls to or below the // tolerance; scaling the objective by 1e6 moved the recorded gradient up // rather than leaving it fixed, so no division by the objective value is // involved either. // 2. LossTolerance tests the objective VALUE, not the change in it. Given a // tolerance of 0.5 against a first-iteration loss of 0.3551, MATLAB // stopped at iteration 1. // 3. When several tolerances are met at once the reported criterion follows // a fixed order: relative change in the coefficients, gradient, step, // loss. // 4. BetaTolerance is a RELATIVE change and is measured differently from // the two above: the two-norm of the step over the two-norm of the // iterate it landed on, with no guard on the denominator. Measured on // R2024a's fitclinear, where a fit from a zero start reports exactly 1 // at the first iteration, which every guarded candidate contradicts. // A tolerance of zero there does not mean "test against zero" but "do // not run this test", and MATLAB then does not compute the quantity at // all, reporting NaN. Note that LossTolerance disables with -Inf and // BetaTolerance with 0: the two differ deliberately, each following // MATLAB for the quantity it governs, and unifying them would break // one or the other. // 5. The gradient and step are reduced by their infinity norm, which is // what MATLAB reports. Letting R2024a's fitrnet converge and computing // both norms at the weights it returned, its reported gradient matched // the infinity norm to twelve significant digits and the two-norm not at // all, the latter being 9.5 per cent larger on that fit. #ifndef OCTAVE_STATISTICS_LBFGS_H #define OCTAVE_STATISTICS_LBFGS_H #include #include #include using namespace std; namespace lbfgs { // Why the iteration stopped. Callers map these onto MATLAB's own strings. enum criterion { CRIT_GRADIENT = 0, CRIT_BETA, CRIT_STEP, CRIT_LOSS, CRIT_ITERATION_LIMIT, CRIT_LINE_SEARCH }; struct options { int iteration_limit; double gradient_tolerance; double loss_tolerance; double step_tolerance; double beta_tolerance; int history_size; double initial_step_size; // non-positive selects the automatic value int max_line_search; double wolfe_c1; double wolfe_c2; bool full_bfgs; // a dense inverse Hessian instead of pairs bool weak_wolfe; // bisect and double instead of zooming // Defaults are MATLAB's, read off fitcnet's ModelParameters on R2024a. // HistorySize is not exposed by fitcnet; 10 is what fmincon and the deep // learning solver both use, and 15 is what fitclinear uses. options () : iteration_limit (1000), gradient_tolerance (1e-6), loss_tolerance (1e-6), step_tolerance (1e-6), beta_tolerance (0.0), history_size (10), initial_step_size (0.0), max_line_search (25), wolfe_c1 (1e-4), wolfe_c2 (0.9), full_bfgs (false), weak_wolfe (false) { } }; struct record { double fval; double gradient; double step; double rel_beta; }; struct result { int crit; int iterations; int funcount; double fval; double gradient; double step; double rel_beta; vector history; }; // A stable name for the test that stopped the iteration, never edited for // readability. The engine deliberately does not hand out prose: MATLAB // words the same criterion differently per function, fitcnet reporting // "Relative gradient tolerance reached." where fitclinear reports // "Tolerance on gradient satisfied.", so each caller maps this token to // the wording, and where applicable the code, of the function it mirrors. inline const char * criterion_token (int crit) { switch (crit) { case CRIT_GRADIENT: return "gradient"; case CRIT_BETA: return "beta"; case CRIT_STEP: return "step"; case CRIT_LOSS: return "loss"; case CRIT_ITERATION_LIMIT: return "iteration"; default: return "linesearch"; } } inline double dot (const vector &a, const vector &b) { double s = 0.0; for (size_t i = 0; i < a.size (); i++) { s += a[i] * b[i]; } return s; } inline double inf_norm (const vector &a) { double m = 0.0; for (size_t i = 0; i < a.size (); i++) { double v = fabs (a[i]); if (v > m) { m = v; } } return m; } inline double two_norm (const vector &a) { double s = 0.0; for (size_t i = 0; i < a.size (); i++) { s += a[i] * a[i]; } return sqrt (s); } inline bool is_finite (double v) { return (v == v && v < HUGE_VAL && v > -HUGE_VAL); } // Safeguarded quadratic interpolation between the two bracket ends, falling // back to bisection whenever the interpolant is not usable. Keeping the // trial away from the ends by a tenth of the bracket is what stops the zoom // from stalling on a flat interpolant. inline double interpolate (double a_lo, double f_lo, double d_lo, double a_hi, double f_hi) { double lo = (a_lo < a_hi ? a_lo : a_hi); double hi = (a_lo < a_hi ? a_hi : a_lo); double edge = 0.1 * (hi - lo); double bisect = 0.5 * (a_lo + a_hi); double width = a_hi - a_lo; double denom = 2.0 * (f_hi - f_lo - d_lo * width); if (denom == 0.0 || ! is_finite (denom)) { return bisect; } double a = a_lo - d_lo * width * width / denom; if (! is_finite (a) || a < lo + edge || a > hi - edge) { return bisect; } return a; } // Strong Wolfe line search, Nocedal and Wright algorithms 3.5 and 3.6. On // success the accepted point, its gradient and its value are left in x, g // and f, so an accepted search costs the caller no further objective call. template bool line_search (Objective &fun, const vector &x0, const vector &d, double f0, double dphi0, double alpha0, const options &opt, vector &x, vector &g, double &f, double &alpha, int &nfev) { const size_t n = x0.size (); const double c1 = opt.wolfe_c1; const double c2 = opt.wolfe_c2; double dphi = 0.0; // phi (a) together with phi' (a), evaluated in place: the trial point is // left in x, its gradient in g and its value in f, so an accepted step // needs no repeat call. auto evaluate = [&] (double a) -> void { for (size_t i = 0; i < n; i++) { x[i] = x0[i] + a * d[i]; } f = fun (x, g); dphi = dot (g, d); nfev++; }; double a_lo = 0.0, f_lo = f0, d_lo = dphi0; double a_hi = 0.0, f_hi = 0.0; bool bracketed = false; double a_prev = 0.0, f_prev = f0, d_prev = dphi0; double a = alpha0; // Bracketing. The first trial that breaks sufficient decrease, or that // rises above its predecessor, or that turns the slope upwards, encloses // an acceptable step together with the trial before it. for (int i = 1; i <= opt.max_line_search; i++) { evaluate (a); if (! is_finite (f) || f > f0 + c1 * a * dphi0 || (i > 1 && f >= f_prev)) { a_lo = a_prev; f_lo = f_prev; d_lo = d_prev; a_hi = a; f_hi = f; bracketed = true; break; } if (fabs (dphi) <= -c2 * dphi0) { alpha = a; return true; } if (dphi >= 0.0) { a_lo = a; f_lo = f; d_lo = dphi; a_hi = a_prev; f_hi = f_prev; bracketed = true; break; } a_prev = a; f_prev = f; d_prev = dphi; a *= 2.0; } if (! bracketed) { // Every trial improved and none met the curvature condition, which // means the last one is still a descent step worth taking. if (a_prev > 0.0) { evaluate (a_prev); alpha = a_prev; return true; } return false; } // Zoom. The bracket shrinks until a trial satisfies both Wolfe // conditions, and a_lo always holds the best point seen so far. for (int j = 1; j <= opt.max_line_search; j++) { a = interpolate (a_lo, f_lo, d_lo, a_hi, f_hi); evaluate (a); if (! is_finite (f) || f > f0 + c1 * a * dphi0 || f >= f_lo) { a_hi = a; f_hi = f; } else { if (fabs (dphi) <= -c2 * dphi0) { alpha = a; return true; } if (dphi * (a_hi - a_lo) >= 0.0) { a_hi = a_lo; f_hi = f_lo; } a_lo = a; f_lo = f; d_lo = dphi; } if (fabs (a_hi - a_lo) <= 1e-16 * (1.0 + fabs (a_lo))) { break; } } // The budget ran out. Accepting the best bracket end is better than // failing outright: it still decreases the objective, and the curvature // safeguard on the (s, y) pair will discard the update if it has to. if (a_lo > 0.0) { evaluate (a_lo); alpha = a_lo; return true; } return false; } // Weak Wolfe line search by bisection and doubling. The step is halved // towards the last one that decreased the objective too little, and doubled // while the slope is still too steep, until both weak Wolfe conditions hold. // This is fitclinear's 'bfgs' and 'lbfgs' search, which reports itself as // 'weakwolfe': with c1 1e-4, c2 0.9 and a unit first step it reproduces // R2024a's histories to the iteration and its coefficients to 6e-12, on a // fixture whose first step is 2^-17 and whose second is 4. template bool weak_wolfe (Objective &fun, const vector &x0, const vector &d, double f0, double dphi0, double alpha0, const options &opt, vector &x, vector &g, double &f, double &alpha, int &nfev) { const size_t n = x0.size (); double a = alpha0; double lo = 0.0; double hi = HUGE_VAL; for (int i = 0; i < 100; i++) { for (size_t j = 0; j < n; j++) { x[j] = x0[j] + a * d[j]; } f = fun (x, g); nfev++; if (! is_finite (f) || f > f0 + opt.wolfe_c1 * a * dphi0) { hi = a; } else if (dot (g, d) < opt.wolfe_c2 * dphi0) { lo = a; } else { alpha = a; return true; } a = (hi < HUGE_VAL) ? 0.5 * (lo + hi) : 2.0 * lo; } return false; } // Minimise fun starting from x, which is overwritten with the final point. template result minimize (Objective &fun, vector &x, const options &opt) { const size_t n = x.size (); const int m = (opt.history_size > 0 ? opt.history_size : 1); vector g (n, 0.0), d (n, 0.0), q (n, 0.0); vector x_new (n, 0.0), g_new (n, 0.0); vector > S (m, vector (n, 0.0)); vector > Y (m, vector (n, 0.0)); vector rho (m, 0.0), coef (m, 0.0); int stored = 0, newest = -1; double gamma = 1.0; // Full BFGS keeps the inverse Hessian itself, which starts as the // identity and is scaled by s'y / y'y once, before the first update. vector > Hinv; if (opt.full_bfgs) { Hinv.assign (n, vector (n, 0.0)); for (size_t i = 0; i < n; i++) { Hinv[i][i] = 1.0; } } int updates = 0; result out; out.crit = CRIT_ITERATION_LIMIT; out.iterations = 0; out.funcount = 1; out.fval = fun (x, g); out.gradient = inf_norm (g); out.step = 0.0; out.rel_beta = NAN; for (int k = 1; k <= opt.iteration_limit; k++) { if (opt.full_bfgs) { for (size_t i = 0; i < n; i++) { double v = 0.0; for (size_t j = 0; j < n; j++) { v += Hinv[i][j] * g[j]; } q[i] = v; } stored = 0; } else { // Two-loop recursion, d = -H * g. q = g; for (int j = 0; j < stored; j++) { int idx = (newest - j + m) % m; coef[idx] = rho[idx] * dot (S[idx], q); for (size_t i = 0; i < n; i++) { q[i] -= coef[idx] * Y[idx][i]; } } for (size_t i = 0; i < n; i++) { q[i] *= gamma; } for (int j = stored - 1; j >= 0; j--) { int idx = (newest - j + m) % m; double beta = rho[idx] * dot (Y[idx], q); for (size_t i = 0; i < n; i++) { q[i] += (coef[idx] - beta) * S[idx][i]; } } } for (size_t i = 0; i < n; i++) { d[i] = -q[i]; } // Rounding can leave the recursion with a direction that is not one of // descent; dropping the history and stepping down the gradient is the // standard recovery. double dphi0 = dot (g, d); if (! (dphi0 < 0.0)) { for (size_t i = 0; i < n; i++) { d[i] = -g[i]; } dphi0 = -dot (g, g); stored = 0; newest = -1; gamma = 1.0; if (opt.full_bfgs) { for (size_t i = 0; i < n; i++) { for (size_t j = 0; j < n; j++) { Hinv[i][j] = (i == j ? 1.0 : 0.0); } } updates = 0; } } // A zero slope down the steepest descent direction means the gradient // itself is zero, so there is nothing left to search along. if (dphi0 == 0.0) { out.crit = CRIT_GRADIENT; break; } // A quasi-Newton step is unit-scaled once curvature information exists. // The first one has none, so it is scaled by the gradient instead. double alpha0 = 1.0; if (opt.weak_wolfe) { if (opt.initial_step_size > 0.0 && (opt.full_bfgs ? updates : stored) == 0) { alpha0 = opt.initial_step_size; } } else if ((opt.full_bfgs ? updates : stored) == 0) { if (opt.initial_step_size > 0.0) { alpha0 = opt.initial_step_size; } else { double gn = inf_norm (g); alpha0 = (gn > 1.0 ? 1.0 / gn : 1.0); } } // f_new is kept separate from out.fval: a failed search leaves its own // scratch value behind, and out.fval must go on describing the last // point actually accepted. double f_new = out.fval; double alpha = 0.0; bool ok = opt.weak_wolfe ? weak_wolfe (fun, x, d, out.fval, dphi0, alpha0, opt, x_new, g_new, f_new, alpha, out.funcount) : line_search (fun, x, d, out.fval, dphi0, alpha0, opt, x_new, g_new, f_new, alpha, out.funcount); // A failed search usually means the stored curvature has gone stale // rather than that the point is stationary, so the memory is dropped // and the step retried down the gradient before giving up. Only a // second failure, with nothing left to blame, ends the iteration. if (! ok && stored > 0 && ! opt.weak_wolfe) { stored = 0; newest = -1; gamma = 1.0; for (size_t i = 0; i < n; i++) { d[i] = -g[i]; } dphi0 = -dot (g, g); double gn = inf_norm (g); alpha0 = (gn > 1.0 ? 1.0 / gn : 1.0); f_new = out.fval; ok = line_search (fun, x, d, out.fval, dphi0, alpha0, opt, x_new, g_new, f_new, alpha, out.funcount); } if (! ok) { out.crit = CRIT_LINE_SEARCH; break; } // The curvature pair goes straight into the slot it would occupy, so // that the step norm can be read off it before the pair is judged. A // rejected pair simply leaves the slot to be overwritten next time. int idx = (newest + 1) % m; double sy = 0.0, yy = 0.0; for (size_t i = 0; i < n; i++) { S[idx][i] = x_new[i] - x[i]; Y[idx][i] = g_new[i] - g[i]; sy += S[idx][i] * Y[idx][i]; yy += Y[idx][i] * Y[idx][i]; } double stepnorm = inf_norm (S[idx]); // Keeping only pairs with positive curvature is what holds the implicit // inverse Hessian positive definite. if (opt.full_bfgs && yy > 0.0 && sy > 1e-10 * yy) { // H = (I - rho s y') H (I - rho y s') + rho s s', with H scaled by // s'y / y'y before the first update. double r = 1.0 / sy; if (updates == 0) { double scale = sy / yy; for (size_t i = 0; i < n; i++) { for (size_t j = 0; j < n; j++) { Hinv[i][j] *= scale; } } } vector Hy (n, 0.0); double yHy = 0.0; for (size_t i = 0; i < n; i++) { for (size_t j = 0; j < n; j++) { Hy[i] += Hinv[i][j] * Y[idx][j]; } yHy += Y[idx][i] * Hy[i]; } for (size_t i = 0; i < n; i++) { for (size_t j = 0; j < n; j++) { Hinv[i][j] += r * ((1.0 + r * yHy) * S[idx][i] * S[idx][j] - Hy[i] * S[idx][j] - S[idx][i] * Hy[j]); } } updates++; } else if (! opt.full_bfgs && yy > 0.0 && sy > 1e-10 * yy) { rho[idx] = 1.0 / sy; gamma = sy / yy; newest = idx; if (stored < m) { stored++; } } x.swap (x_new); g.swap (g_new); out.fval = f_new; out.gradient = inf_norm (g); out.step = stepnorm; out.iterations = k; // The relative change in the iterate, which MATLAB reports as // RelativeChangeInBeta: the two-norm of the step over the two-norm of // the point it landed on. The denominator is deliberately unguarded, // and a zero tolerance switches the test off rather than testing // against zero, leaving the quantity uncomputed as MATLAB leaves it. // A zero iterate can only be reached by a zero step, so the division // is 0/0 and yields NaN rather than a spurious number; the other way // round it yields Inf, which no tolerance accepts. if (opt.beta_tolerance > 0.0) { out.rel_beta = two_norm (S[idx]) / two_norm (x); } record r; r.fval = out.fval; r.gradient = out.gradient; r.step = out.step; r.rel_beta = out.rel_beta; out.history.push_back (r); // MATLAB's order: the relative change in the coefficients, then the // gradient, then step, then loss. R2024a's fitclinear reports the // coefficient tolerance at an iteration meeting both of the first two; // the rest were measured on fitcnet. if (opt.beta_tolerance > 0.0 && out.rel_beta <= opt.beta_tolerance) { out.crit = CRIT_BETA; break; } if (out.gradient <= opt.gradient_tolerance) { out.crit = CRIT_GRADIENT; break; } if (out.step <= opt.step_tolerance) { out.crit = CRIT_STEP; break; } if (out.fval <= opt.loss_tolerance) { out.crit = CRIT_LOSS; break; } } return out; } } #endif statistics-release-2.1.2/src/libsvmread.cc000066400000000000000000000132061527311104300205510ustar00rootroot00000000000000/* Copyright (C) 2025 Andreas Bertsatos Adapted from MATLAB libsvmwrite.c file from the LIBSVM 3.36 (2025) library by Chih-Chung Chang and Chih-Jen Lin. This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef max #define max(x,y) (((x)>(y))?(x):(y)) #endif #ifndef min #define min(x,y) (((x)<(y))?(x):(y)) #endif using namespace std; static char *line; static int max_line_len; static char* readline(FILE *input) { int len; if(fgets(line,max_line_len,input) == NULL) { return NULL; } while(strrchr(line,'\n') == NULL) { max_line_len *= 2; line = (char *) realloc(line, max_line_len); len = (int) strlen(line); if(fgets(line+len,max_line_len-len,input) == NULL) { break; } } return line; } // read the file in libsvm format void read(string filename, ColumnVector &label_vec, SparseMatrix &instance_mat) { int max_index, min_index, inst_max_index; size_t elements, k, i, l=0; FILE *fp = fopen(filename.c_str(),"r"); char *endptr; octave_idx_type *ir, *jc; double *labels, *samples; if(fp == NULL) { printf("can't open input file %s\n",filename.c_str()); return; } max_line_len = 1024; line = (char *) malloc(max_line_len*sizeof(char)); max_index = 0; min_index = 1; // our index starts from 1 elements = 0; while(readline(fp) != NULL) { char *idx, *val; // features int index = 0; inst_max_index = -1; strtok(line," \t"); while (1) { idx = strtok(NULL,":"); val = strtok(NULL," \t"); if(val == NULL) break; errno = 0; index = (int) strtol(idx,&endptr,10); if(endptr == idx || errno != 0 || *endptr != '\0' || index <= inst_max_index) { printf("libsvmread: wrong input format at line %d.\n", (int)l+1); return; } else inst_max_index = index; min_index = min(min_index, index); elements++; } max_index = max(max_index, inst_max_index); l++; } rewind(fp); // y label_vec = ColumnVector(l, 1); // x^T if (min_index <= 0) { octave_idx_type r = max_index-min_index+1; octave_idx_type c = l; octave_idx_type val = elements; instance_mat = SparseMatrix(r, c, val); } else { octave_idx_type r = max_index-min_index+1; octave_idx_type c = l; octave_idx_type val = elements; instance_mat = SparseMatrix(r, c, val); } labels = (double*)label_vec.data(); samples = (double*)instance_mat.data(); ir = (octave_idx_type*)instance_mat.ridx(); jc = (octave_idx_type*)instance_mat.cidx(); k=0; for(i=0;i start from 0 ir[k] = strtol(idx,&endptr,10) - min_index; errno = 0; samples[k] = strtod(val,&endptr); if (endptr == val || errno != 0 || (*endptr != '\0' && !isspace(*endptr))) { printf("libsvmread: wrong input format at line %d.\n", (int)i+1); return; } ++k; } } jc[l] = k; fclose(fp); free(line); // transpose instance sparse matrix in row format instance_mat.transpose(); } DEFUN_DLD (libsvmread, args, nargout, "-*- texinfo -*- \n\n\ @deftypefn {statistics} {[@var{labels}, @var{data}] =} libsvmread (@var{filename})\n\ \n\ \n\ This function reads the labels and the corresponding instance_matrix from a \ LIBSVM data file and stores them in @var{labels} and @var{data} respectively. \ These can then be used as inputs to @code{svmtrain} or @code{svmpredict} \ function. \ \n\ \n\ @end deftypefn") { if(args.length() != 1 || nargout != 2) { error ("libsvmread: wrong number of input or output arguments."); } if(!args(0).is_string()) { error ("libsvmread: filename must be a string."); } string filename = args(0).string_value(); octave_value_list retval(nargout); ColumnVector label_vec; SparseMatrix instance_mat; read(filename, label_vec, instance_mat); retval(0) = label_vec; retval(1) = instance_mat.transpose(); return retval; } /* %!error [L, D] = libsvmread (24); %!error ... %! D = libsvmread ("filename"); %!test %! [L, D] = libsvmread (file_in_loadpath ("heart_scale.dat")); %! assert_equal (size (L), [270, 1]); %! assert_equal (size (D), [270, 13]); %!test %! [L, D] = libsvmread (file_in_loadpath ("heart_scale.dat")); %! assert_equal (issparse (L), false); %! assert_equal (issparse (D), true); */ statistics-release-2.1.2/src/libsvmwrite.cc000066400000000000000000000106251527311104300207720ustar00rootroot00000000000000/* Copyright (C) 2025 Andreas Bertsatos Adapted from MATLAB libsvmwrite.c file from the LIBSVM 3.36 (2025) library by Chih-Chung Chang and Chih-Jen Lin. This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; void write(string filename, ColumnVector label_vec, SparseMatrix instance_mat) { // open file FILE *fp = fopen(filename.c_str(),"w"); if (fp == NULL) { error ("libsvmwrite: error opening file for write."); } else { // check for equal number of instances and labels int im_rows = (int)instance_mat.rows(); int lv_rows = (int)label_vec.rows(); if(im_rows != lv_rows) { // close file fclose (fp); remove (filename.c_str()); error ("libsvmwrite: length of label vector does not match instances."); } // transpose instance sparse matrix in column format SparseMatrix instance_mat_col = instance_mat.transpose(); octave_idx_type *ir, *jc, k, low, high; double *samples, *labels; // each column is one instance labels = (double*)label_vec.data(); samples = (double*)instance_mat_col.data(); ir = (octave_idx_type*)instance_mat_col.ridx(); jc = (octave_idx_type*)instance_mat_col.cidx(); for(int i = 0; i < lv_rows; i++) { fprintf(fp, "%.17g", labels[i]); low = jc[i], high = jc[i+1]; for(k=low;k 0) { error ("libsvmwrite: wrong number of output arguments."); return octave_value_list(); } // Transform the input Matrix to libsvm format if(args.length() == 3) { if(!args(1).is_double_type() || !args(2).is_double_type()) { error ("libsvmwrite: label vector and instance matrix must be double."); } if(!args(0).is_string()) { error ("libsvmwrite: filename must be a string."); } string filename = args(0).string_value(); if(args(2).issparse()) { ColumnVector label_vec = args(1).column_vector_value(); SparseMatrix instance_mat = args(2).sparse_matrix_value(); write(filename, label_vec, instance_mat); } else { error ("libsvmwrite: instance_matrix must be sparse."); } } else { error ("libsvmwrite: wrong number of input arguments."); } return octave_value_list(); } /* %!shared L, D %! [L, D] = libsvmread (file_in_loadpath ("heart_scale.dat")); %!error libsvmwrite ("", L, D); %!error ... %! libsvmwrite (tempname (), [L;L], D); %!error ... %! OUT = libsvmwrite (tempname (), L, D); %!error ... %! libsvmwrite (tempname (), single (L), D); %!error libsvmwrite (13412, L, D); %!error ... %! libsvmwrite (tempname (), L, full (D)); %!error ... %! libsvmwrite (tempname (), L, D, D); */ statistics-release-2.1.2/src/svm.cpp000066400000000000000000002155671527311104300174410ustar00rootroot00000000000000/* Copyright (C) 2025 Chih-Chung Chang and Chih-Jen Lin This file is part of the statistics package for GNU Octave. Permission granted by Chih-Jen Lin to the package maintainer to include this file and double license under GPLv3 by means of personal communication. This file is LIBSVM 3.37 with the per-instance weights of the "Weights for data instances" variant from the LIBSVM Tools page merged into it, so that each instance's weight scales its box constraint. Chih-Jen Lin confirmed on 2026-09-16 that the permission above covers those additions. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include #include #include #include #include "svm.h" int libsvm_version = LIBSVM_VERSION; typedef float Qfloat; typedef signed char schar; #ifndef min template static inline T min(T x,T y) { return (x static inline T max(T x,T y) { return (x>y)?x:y; } #endif template static inline void swap(T& x, T& y) { T t=x; x=y; y=t; } template static inline void clone(T*& dst, S* src, int n) { dst = new T[n]; memcpy((void *)dst,(void *)src,sizeof(T)*n); } static inline double powi(double base, int times) { double tmp = base, ret = 1.0; for(int t=times; t>0; t/=2) { if(t%2==1) ret*=tmp; tmp = tmp * tmp; } return ret; } #define INF HUGE_VAL #define TAU 1e-12 #define Malloc(type,n) (type *)malloc((n)*sizeof(type)) static void print_string_stdout(const char *s) { fputs(s,stdout); fflush(stdout); } static void (*svm_print_string) (const char *) = &print_string_stdout; // The number of solver runs that stopped at their iteration limit, read by // the caller after svm_train so that it can report the fit unconverged. static int svm_max_iter_count = 0; #if 1 static void info(const char *fmt,...) { char buf[BUFSIZ]; va_list ap; va_start(ap,fmt); vsnprintf(buf,BUFSIZ,fmt,ap); va_end(ap); (*svm_print_string)(buf); } #else static void info(const char *fmt,...) {} #endif // // Kernel Cache // // l is the number of total data items // size is the cache size limit in bytes // class Cache { public: Cache(int l,size_t size); ~Cache(); // request data [0,len) // return some position p where [p,len) need to be filled // (p >= len if nothing needs to be filled) int get_data(const int index, Qfloat **data, int len); void swap_index(int i, int j); private: int l; size_t size; struct head_t { head_t *prev, *next; // a circular list Qfloat *data; int len; // data[0,len) is cached in this entry }; head_t *head; head_t lru_head; void lru_delete(head_t *h); void lru_insert(head_t *h); }; Cache::Cache(int l_,size_t size_):l(l_),size(size_) { head = (head_t *)calloc(l,sizeof(head_t)); // initialized to 0 size /= sizeof(Qfloat); size_t header_size = l * sizeof(head_t) / sizeof(Qfloat); size = max(size, 2 * (size_t) l + header_size) - header_size; // cache must be large enough for two columns lru_head.next = lru_head.prev = &lru_head; } Cache::~Cache() { for(head_t *h = lru_head.next; h != &lru_head; h=h->next) free(h->data); free(head); } void Cache::lru_delete(head_t *h) { // delete from current location h->prev->next = h->next; h->next->prev = h->prev; } void Cache::lru_insert(head_t *h) { // insert to last position h->next = &lru_head; h->prev = lru_head.prev; h->prev->next = h; h->next->prev = h; } int Cache::get_data(const int index, Qfloat **data, int len) { head_t *h = &head[index]; if(h->len) lru_delete(h); int more = len - h->len; if(more > 0) { // free old space while(size < (size_t)more) { head_t *old = lru_head.next; lru_delete(old); free(old->data); size += old->len; old->data = 0; old->len = 0; } // allocate new space h->data = (Qfloat *)realloc(h->data,sizeof(Qfloat)*len); size -= more; // previous while loop guarantees size >= more and subtraction of size_t variable will not underflow swap(h->len,len); } lru_insert(h); *data = h->data; return len; } void Cache::swap_index(int i, int j) { if(i==j) return; if(head[i].len) lru_delete(&head[i]); if(head[j].len) lru_delete(&head[j]); swap(head[i].data,head[j].data); swap(head[i].len,head[j].len); if(head[i].len) lru_insert(&head[i]); if(head[j].len) lru_insert(&head[j]); if(i>j) swap(i,j); for(head_t *h = lru_head.next; h!=&lru_head; h=h->next) { if(h->len > i) { if(h->len > j) swap(h->data[i],h->data[j]); else { // give up lru_delete(h); free(h->data); size += h->len; h->data = 0; h->len = 0; } } } } // // Kernel evaluation // // the static method k_function is for doing single kernel evaluation // the constructor of Kernel prepares to calculate the l*l kernel matrix // the member function get_Q is for getting one column from the Q Matrix // class QMatrix { public: virtual Qfloat *get_Q(int column, int len) const = 0; virtual double *get_QD() const = 0; virtual void swap_index(int i, int j) const = 0; virtual ~QMatrix() {} }; class Kernel: public QMatrix { public: Kernel(int l, svm_node * const * x, const svm_parameter& param); virtual ~Kernel(); static double k_function(const svm_node *x, const svm_node *y, const svm_parameter& param); virtual Qfloat *get_Q(int column, int len) const = 0; virtual double *get_QD() const = 0; virtual void swap_index(int i, int j) const // no so const... { swap(x[i],x[j]); if(x_square) swap(x_square[i],x_square[j]); } protected: double (Kernel::*kernel_function)(int i, int j) const; private: const svm_node **x; double *x_square; // svm_parameter const int kernel_type; const int degree; const double gamma; const double coef0; static double dot(const svm_node *px, const svm_node *py); double kernel_linear(int i, int j) const { return dot(x[i],x[j]); } double kernel_poly(int i, int j) const { return powi(gamma*dot(x[i],x[j])+coef0,degree); } double kernel_rbf(int i, int j) const { return exp(-gamma*(x_square[i]+x_square[j]-2*dot(x[i],x[j]))); } double kernel_sigmoid(int i, int j) const { return tanh(gamma*dot(x[i],x[j])+coef0); } double kernel_precomputed(int i, int j) const { return x[i][(int)(x[j][0].value)].value; } }; Kernel::Kernel(int l, svm_node * const * x_, const svm_parameter& param) :kernel_type(param.kernel_type), degree(param.degree), gamma(param.gamma), coef0(param.coef0) { switch(kernel_type) { case LINEAR: kernel_function = &Kernel::kernel_linear; break; case POLY: kernel_function = &Kernel::kernel_poly; break; case RBF: kernel_function = &Kernel::kernel_rbf; break; case SIGMOID: kernel_function = &Kernel::kernel_sigmoid; break; case PRECOMPUTED: kernel_function = &Kernel::kernel_precomputed; break; } clone(x,x_,l); if(kernel_type == RBF) { x_square = new double[l]; for(int i=0;iindex != -1 && py->index != -1) { if(px->index == py->index) { sum += px->value * py->value; ++px; ++py; } else { if(px->index > py->index) ++py; else ++px; } } return sum; } double Kernel::k_function(const svm_node *x, const svm_node *y, const svm_parameter& param) { switch(param.kernel_type) { case LINEAR: return dot(x,y); case POLY: return powi(param.gamma*dot(x,y)+param.coef0,param.degree); case RBF: { double sum = 0; while(x->index != -1 && y->index !=-1) { if(x->index == y->index) { double d = x->value - y->value; sum += d*d; ++x; ++y; } else { if(x->index > y->index) { sum += y->value * y->value; ++y; } else { sum += x->value * x->value; ++x; } } } while(x->index != -1) { sum += x->value * x->value; ++x; } while(y->index != -1) { sum += y->value * y->value; ++y; } return exp(-param.gamma*sum); } case SIGMOID: return tanh(param.gamma*dot(x,y)+param.coef0); case PRECOMPUTED: //x: test (validation), y: SV return x[(int)(y->value)].value; default: return 0; // Unreachable } } // An SMO algorithm in Fan et al., JMLR 6(2005), p. 1889--1918 // Solves: // // min 0.5(\alpha^T Q \alpha) + p^T \alpha // // y^T \alpha = \delta // y_i = +1 or -1 // 0 <= alpha_i <= Cp for y_i = 1 // 0 <= alpha_i <= Cn for y_i = -1 // // Given: // // Q, p, y, Cp, Cn, and an initial feasible point \alpha // l is the size of vectors and matrices // eps is the stopping tolerance // // solution will be put in \alpha, objective value will be put in obj // class Solver { public: Solver() {}; virtual ~Solver() {}; struct SolutionInfo { double obj; double rho; double *upper_bound; double r; // for Solver_NU }; void Solve(int l, const QMatrix& Q, const double *p_, const schar *y_, double *alpha_, const double* C_, double eps, SolutionInfo* si, int shrinking); protected: int active_size; schar *y; double *G; // gradient of objective function enum { LOWER_BOUND, UPPER_BOUND, FREE }; char *alpha_status; // LOWER_BOUND, UPPER_BOUND, FREE double *alpha; const QMatrix *Q; const double *QD; double eps; double Cp,Cn; double *C; double *p; int *active_set; double *G_bar; // gradient, if we treat free variables as 0 int l; bool unshrink; // XXX double get_C(int i) { return C[i]; } void update_alpha_status(int i) { if(alpha[i] >= get_C(i)) alpha_status[i] = UPPER_BOUND; else if(alpha[i] <= 0) alpha_status[i] = LOWER_BOUND; else alpha_status[i] = FREE; } bool is_upper_bound(int i) { return alpha_status[i] == UPPER_BOUND; } bool is_lower_bound(int i) { return alpha_status[i] == LOWER_BOUND; } bool is_free(int i) { return alpha_status[i] == FREE; } void swap_index(int i, int j); void reconstruct_gradient(); virtual int select_working_set(int &i, int &j); virtual double calculate_rho(); virtual void do_shrinking(); private: bool be_shrunk(int i, double Gmax1, double Gmax2); }; void Solver::swap_index(int i, int j) { Q->swap_index(i,j); swap(y[i],y[j]); swap(G[i],G[j]); swap(alpha_status[i],alpha_status[j]); swap(alpha[i],alpha[j]); swap(p[i],p[j]); swap(active_set[i],active_set[j]); swap(G_bar[i],G_bar[j]); swap(C[i],C[j]); } void Solver::reconstruct_gradient() { // reconstruct inactive elements of G from G_bar and free variables if(active_size == l) return; int i,j; int nr_free = 0; for(j=active_size;j 2*active_size*(l-active_size)) { for(i=active_size;iget_Q(i,active_size); for(j=0;jget_Q(i,l); double alpha_i = alpha[i]; for(j=active_size;jl = l; this->Q = &Q; QD=Q.get_QD(); clone(p, p_,l); clone(y, y_,l); clone(alpha,alpha_,l); clone(C,C_,l); this->eps = eps; unshrink = false; // initialize alpha_status { alpha_status = new char[l]; for(int i=0;iINT_MAX/100 ? INT_MAX : 100*l); int counter = min(l,1000)+1; while(iter < max_iter) { // show progress and do shrinking if(--counter == 0) { counter = min(l,1000); if(shrinking) do_shrinking(); info("."); } int i,j; if(select_working_set(i,j)!=0) { // reconstruct the whole gradient reconstruct_gradient(); // reset active set size and check active_size = l; info("*"); if(select_working_set(i,j)!=0) break; else counter = 1; // do shrinking next iteration } ++iter; // update alpha[i] and alpha[j], handle bounds carefully const Qfloat *Q_i = Q.get_Q(i,active_size); const Qfloat *Q_j = Q.get_Q(j,active_size); double C_i = get_C(i); double C_j = get_C(j); double old_alpha_i = alpha[i]; double old_alpha_j = alpha[j]; if(y[i]!=y[j]) { double quad_coef = QD[i]+QD[j]+2*Q_i[j]; if (quad_coef <= 0) quad_coef = TAU; double delta = (-G[i]-G[j])/quad_coef; double diff = alpha[i] - alpha[j]; alpha[i] += delta; alpha[j] += delta; if(diff > 0) { if(alpha[j] < 0) { alpha[j] = 0; alpha[i] = diff; } } else { if(alpha[i] < 0) { alpha[i] = 0; alpha[j] = -diff; } } if(diff > C_i - C_j) { if(alpha[i] > C_i) { alpha[i] = C_i; alpha[j] = C_i - diff; } } else { if(alpha[j] > C_j) { alpha[j] = C_j; alpha[i] = C_j + diff; } } } else { double quad_coef = QD[i]+QD[j]-2*Q_i[j]; if (quad_coef <= 0) quad_coef = TAU; double delta = (G[i]-G[j])/quad_coef; double sum = alpha[i] + alpha[j]; alpha[i] -= delta; alpha[j] += delta; if(sum > C_i) { if(alpha[i] > C_i) { alpha[i] = C_i; alpha[j] = sum - C_i; } } else { if(alpha[j] < 0) { alpha[j] = 0; alpha[i] = sum; } } if(sum > C_j) { if(alpha[j] > C_j) { alpha[j] = C_j; alpha[i] = sum - C_j; } } else { if(alpha[i] < 0) { alpha[i] = 0; alpha[j] = sum; } } } // update G double delta_alpha_i = alpha[i] - old_alpha_i; double delta_alpha_j = alpha[j] - old_alpha_j; for(int k=0;k= max_iter) { if(active_size < l) { // reconstruct the whole gradient to calculate objective value reconstruct_gradient(); active_size = l; info("*"); } svm_max_iter_count++; info("\nWARNING: reaching max number of iterations\n"); } // calculate rho si->rho = calculate_rho(); // calculate objective value { double v = 0; int i; for(i=0;iobj = v/2; } // put back the solution { for(int i=0;iupper_bound[i] = C[i]; info("\noptimization finished, #iter = %d\n",iter); delete[] p; delete[] y; delete[] C; delete[] alpha; delete[] alpha_status; delete[] active_set; delete[] G; delete[] G_bar; } // return 1 if already optimal, return 0 otherwise int Solver::select_working_set(int &out_i, int &out_j) { // return i,j such that // i: maximizes -y_i * grad(f)_i, i in I_up(\alpha) // j: minimizes the decrease of obj value // (if quadratic coefficient <= 0, replace it with tau) // -y_j*grad(f)_j < -y_i*grad(f)_i, j in I_low(\alpha) double Gmax = -INF; double Gmax2 = -INF; int Gmax_idx = -1; int Gmin_idx = -1; double obj_diff_min = INF; for(int t=0;t= Gmax) { Gmax = -G[t]; Gmax_idx = t; } } else { if(!is_lower_bound(t)) if(G[t] >= Gmax) { Gmax = G[t]; Gmax_idx = t; } } int i = Gmax_idx; const Qfloat *Q_i = NULL; if(i != -1) // NULL Q_i not accessed: Gmax=-INF if i=-1 Q_i = Q->get_Q(i,active_size); for(int j=0;j= Gmax2) Gmax2 = G[j]; if (grad_diff > 0) { double obj_diff; double quad_coef = QD[i]+QD[j]-2.0*y[i]*Q_i[j]; if (quad_coef > 0) obj_diff = -(grad_diff*grad_diff)/quad_coef; else obj_diff = -(grad_diff*grad_diff)/TAU; if (obj_diff <= obj_diff_min) { Gmin_idx=j; obj_diff_min = obj_diff; } } } } else { if (!is_upper_bound(j)) { double grad_diff= Gmax-G[j]; if (-G[j] >= Gmax2) Gmax2 = -G[j]; if (grad_diff > 0) { double obj_diff; double quad_coef = QD[i]+QD[j]+2.0*y[i]*Q_i[j]; if (quad_coef > 0) obj_diff = -(grad_diff*grad_diff)/quad_coef; else obj_diff = -(grad_diff*grad_diff)/TAU; if (obj_diff <= obj_diff_min) { Gmin_idx=j; obj_diff_min = obj_diff; } } } } } if(Gmax+Gmax2 < eps || Gmin_idx == -1) return 1; out_i = Gmax_idx; out_j = Gmin_idx; return 0; } bool Solver::be_shrunk(int i, double Gmax1, double Gmax2) { if(is_upper_bound(i)) { if(y[i]==+1) return(-G[i] > Gmax1); else return(-G[i] > Gmax2); } else if(is_lower_bound(i)) { if(y[i]==+1) return(G[i] > Gmax2); else return(G[i] > Gmax1); } else return(false); } void Solver::do_shrinking() { int i; double Gmax1 = -INF; // max { -y_i * grad(f)_i | i in I_up(\alpha) } double Gmax2 = -INF; // max { y_i * grad(f)_i | i in I_low(\alpha) } // find maximal violating pair first for(i=0;i= Gmax1) Gmax1 = -G[i]; } if(!is_lower_bound(i)) { if(G[i] >= Gmax2) Gmax2 = G[i]; } } else { if(!is_upper_bound(i)) { if(-G[i] >= Gmax2) Gmax2 = -G[i]; } if(!is_lower_bound(i)) { if(G[i] >= Gmax1) Gmax1 = G[i]; } } } if(unshrink == false && Gmax1 + Gmax2 <= eps*10) { unshrink = true; reconstruct_gradient(); active_size = l; info("*"); } for(i=0;i i) { if (!be_shrunk(active_size, Gmax1, Gmax2)) { swap_index(i,active_size); break; } active_size--; } } } double Solver::calculate_rho() { double r; int nr_free = 0; double ub = INF, lb = -INF, sum_free = 0; for(int i=0;i0) r = sum_free/nr_free; else r = (ub+lb)/2; return r; } // // Solver for nu-svm classification and regression // // additional constraint: e^T \alpha = constant // class Solver_NU: public Solver { public: Solver_NU() {} void Solve(int l, const QMatrix& Q, const double *p, const schar *y, double *alpha, double* C_, double eps, SolutionInfo* si, int shrinking) { this->si = si; Solver::Solve(l,Q,p,y,alpha,C_,eps,si,shrinking); } private: SolutionInfo *si; int select_working_set(int &i, int &j); double calculate_rho(); bool be_shrunk(int i, double Gmax1, double Gmax2, double Gmax3, double Gmax4); void do_shrinking(); }; // return 1 if already optimal, return 0 otherwise int Solver_NU::select_working_set(int &out_i, int &out_j) { // return i,j such that y_i = y_j and // i: maximizes -y_i * grad(f)_i, i in I_up(\alpha) // j: minimizes the decrease of obj value // (if quadratic coefficient <= 0, replace it with tau) // -y_j*grad(f)_j < -y_i*grad(f)_i, j in I_low(\alpha) double Gmaxp = -INF; double Gmaxp2 = -INF; int Gmaxp_idx = -1; double Gmaxn = -INF; double Gmaxn2 = -INF; int Gmaxn_idx = -1; int Gmin_idx = -1; double obj_diff_min = INF; for(int t=0;t= Gmaxp) { Gmaxp = -G[t]; Gmaxp_idx = t; } } else { if(!is_lower_bound(t)) if(G[t] >= Gmaxn) { Gmaxn = G[t]; Gmaxn_idx = t; } } int ip = Gmaxp_idx; int in = Gmaxn_idx; const Qfloat *Q_ip = NULL; const Qfloat *Q_in = NULL; if(ip != -1) // NULL Q_ip not accessed: Gmaxp=-INF if ip=-1 Q_ip = Q->get_Q(ip,active_size); if(in != -1) Q_in = Q->get_Q(in,active_size); for(int j=0;j= Gmaxp2) Gmaxp2 = G[j]; if (grad_diff > 0) { double obj_diff; double quad_coef = QD[ip]+QD[j]-2*Q_ip[j]; if (quad_coef > 0) obj_diff = -(grad_diff*grad_diff)/quad_coef; else obj_diff = -(grad_diff*grad_diff)/TAU; if (obj_diff <= obj_diff_min) { Gmin_idx=j; obj_diff_min = obj_diff; } } } } else { if (!is_upper_bound(j)) { double grad_diff=Gmaxn-G[j]; if (-G[j] >= Gmaxn2) Gmaxn2 = -G[j]; if (grad_diff > 0) { double obj_diff; double quad_coef = QD[in]+QD[j]-2*Q_in[j]; if (quad_coef > 0) obj_diff = -(grad_diff*grad_diff)/quad_coef; else obj_diff = -(grad_diff*grad_diff)/TAU; if (obj_diff <= obj_diff_min) { Gmin_idx=j; obj_diff_min = obj_diff; } } } } } if(max(Gmaxp+Gmaxp2,Gmaxn+Gmaxn2) < eps || Gmin_idx == -1) return 1; if (y[Gmin_idx] == +1) out_i = Gmaxp_idx; else out_i = Gmaxn_idx; out_j = Gmin_idx; return 0; } bool Solver_NU::be_shrunk(int i, double Gmax1, double Gmax2, double Gmax3, double Gmax4) { if(is_upper_bound(i)) { if(y[i]==+1) return(-G[i] > Gmax1); else return(-G[i] > Gmax4); } else if(is_lower_bound(i)) { if(y[i]==+1) return(G[i] > Gmax2); else return(G[i] > Gmax3); } else return(false); } void Solver_NU::do_shrinking() { double Gmax1 = -INF; // max { -y_i * grad(f)_i | y_i = +1, i in I_up(\alpha) } double Gmax2 = -INF; // max { y_i * grad(f)_i | y_i = +1, i in I_low(\alpha) } double Gmax3 = -INF; // max { -y_i * grad(f)_i | y_i = -1, i in I_up(\alpha) } double Gmax4 = -INF; // max { y_i * grad(f)_i | y_i = -1, i in I_low(\alpha) } // find maximal violating pair first int i; for(i=0;i Gmax1) Gmax1 = -G[i]; } else if(-G[i] > Gmax4) Gmax4 = -G[i]; } if(!is_lower_bound(i)) { if(y[i]==+1) { if(G[i] > Gmax2) Gmax2 = G[i]; } else if(G[i] > Gmax3) Gmax3 = G[i]; } } if(unshrink == false && max(Gmax1+Gmax2,Gmax3+Gmax4) <= eps*10) { unshrink = true; reconstruct_gradient(); active_size = l; } for(i=0;i i) { if (!be_shrunk(active_size, Gmax1, Gmax2, Gmax3, Gmax4)) { swap_index(i,active_size); break; } active_size--; } } } double Solver_NU::calculate_rho() { int nr_free1 = 0,nr_free2 = 0; double ub1 = INF, ub2 = INF; double lb1 = -INF, lb2 = -INF; double sum_free1 = 0, sum_free2 = 0; for(int i=0;i 0) r1 = sum_free1/nr_free1; else r1 = (ub1+lb1)/2; if(nr_free2 > 0) r2 = sum_free2/nr_free2; else r2 = (ub2+lb2)/2; si->r = (r1+r2)/2; return (r1-r2)/2; } // // Q matrices for various formulations // class SVC_Q: public Kernel { public: SVC_Q(const svm_problem& prob, const svm_parameter& param, const schar *y_) :Kernel(prob.l, prob.x, param) { clone(y,y_,prob.l); cache = new Cache(prob.l,(size_t)(param.cache_size*(1<<20))); QD = new double[prob.l]; for(int i=0;i*kernel_function)(i,i); } Qfloat *get_Q(int i, int len) const { Qfloat *data; int start, j; if((start = cache->get_data(i,&data,len)) < len) { for(j=start;j*kernel_function)(i,j)); } return data; } double *get_QD() const { return QD; } void swap_index(int i, int j) const { cache->swap_index(i,j); Kernel::swap_index(i,j); swap(y[i],y[j]); swap(QD[i],QD[j]); } ~SVC_Q() { delete[] y; delete cache; delete[] QD; } private: schar *y; Cache *cache; double *QD; }; class ONE_CLASS_Q: public Kernel { public: ONE_CLASS_Q(const svm_problem& prob, const svm_parameter& param) :Kernel(prob.l, prob.x, param) { cache = new Cache(prob.l,(size_t)(param.cache_size*(1<<20))); QD = new double[prob.l]; for(int i=0;i*kernel_function)(i,i); } Qfloat *get_Q(int i, int len) const { Qfloat *data; int start, j; if((start = cache->get_data(i,&data,len)) < len) { for(j=start;j*kernel_function)(i,j); } return data; } double *get_QD() const { return QD; } void swap_index(int i, int j) const { cache->swap_index(i,j); Kernel::swap_index(i,j); swap(QD[i],QD[j]); } ~ONE_CLASS_Q() { delete cache; delete[] QD; } private: Cache *cache; double *QD; }; class SVR_Q: public Kernel { public: SVR_Q(const svm_problem& prob, const svm_parameter& param) :Kernel(prob.l, prob.x, param) { l = prob.l; cache = new Cache(l,(size_t)(param.cache_size*(1<<20))); QD = new double[2*l]; sign = new schar[2*l]; index = new int[2*l]; for(int k=0;k*kernel_function)(k,k); QD[k+l] = QD[k]; } buffer[0] = new Qfloat[2*l]; buffer[1] = new Qfloat[2*l]; next_buffer = 0; } void swap_index(int i, int j) const { swap(sign[i],sign[j]); swap(index[i],index[j]); swap(QD[i],QD[j]); } Qfloat *get_Q(int i, int len) const { Qfloat *data; int j, real_i = index[i]; if(cache->get_data(real_i,&data,l) < l) { for(j=0;j*kernel_function)(real_i,j); } // reorder and copy Qfloat *buf = buffer[next_buffer]; next_buffer = 1 - next_buffer; schar si = sign[i]; for(j=0;jl; double *minus_ones = new double[l]; schar *y = new schar[l]; double *C = new double[l]; int i; for(i=0;iy[i] > 0) { y[i] = +1; C[i] = prob->W[i]*Cp; } else { y[i] = -1; C[i] = prob->W[i]*Cn; } } Solver s; s.Solve(l, SVC_Q(*prob,*param,y), minus_ones, y, alpha, C, param->eps, si, param->shrinking); /* double sum_alpha=0; for(i=0;il)); */ for(i=0;il; double nu = param->nu; schar *y = new schar[l]; double *C = new double[l]; for(i=0;iy[i]>0) y[i] = +1; else y[i] = -1; C[i] = prob->W[i]; } double nu_l = 0; for(i=0;ieps, si, param->shrinking); double r = si->r; info("C = %f\n",1/r); for(i=0;iupper_bound[i] /= r; } si->rho /= r; si->obj /= (r*r); delete[] C; delete[] y; delete[] zeros; } static void solve_one_class( const svm_problem *prob, const svm_parameter *param, double *alpha, Solver::SolutionInfo* si) { int l = prob->l; double *zeros = new double[l]; schar *ones = new schar[l]; double *C = new double[l]; int i; double nu_l = 0; for(i=0;iW[i]; nu_l += C[i] * param->nu; } i = 0; while(nu_l > 0) { alpha[i] = min(C[i],nu_l); nu_l -= alpha[i]; ++i; } for(;ieps, si, param->shrinking); delete[] C; delete[] zeros; delete[] ones; } static void solve_epsilon_svr( const svm_problem *prob, const svm_parameter *param, double *alpha, Solver::SolutionInfo* si) { int l = prob->l; double *alpha2 = new double[2*l]; double *linear_term = new double[2*l]; double *C = new double[2*l]; schar *y = new schar[2*l]; int i; for(i=0;ip - prob->y[i]; y[i] = 1; C[i] = prob->W[i]*param->C; alpha2[i+l] = 0; linear_term[i+l] = param->p + prob->y[i]; y[i+l] = -1; C[i+l] = prob->W[i]*param->C; } Solver s; s.Solve(2*l, SVR_Q(*prob,*param), linear_term, y, alpha2, C, param->eps, si, param->shrinking); // Upstream divides by C * l; with a box constraint per instance the // divisor is their sum, as solve_nu_svr takes it, and the two agree // when every weight is 1 double sum_alpha = 0; double sum_C = 0; for(i=0;il; double *C = new double[2*l]; double *alpha2 = new double[2*l]; double *linear_term = new double[2*l]; schar *y = new schar[2*l]; int i; double sum = 0; for(i=0;iW[i]*param->C; sum += C[i] * param->nu; } sum /= 2; for(i=0;iy[i]; y[i] = 1; linear_term[i+l] = prob->y[i]; y[i+l] = -1; } Solver_NU s; s.Solve(2*l, SVR_Q(*prob,*param), linear_term, y, alpha2, C, param->eps, si, param->shrinking); info("epsilon = %f\n",-si->r); for(i=0;il); // Every value of the svm_type enum has a case below, but svm_type is an // int, so the compiler cannot rule out a sixth reaching the switch and // leaving si untouched for the info() call after it. svm_check_parameter // rejects any other value before training, so this is unreachable; the // initializer says so rather than leaving a warning in every build. Solver::SolutionInfo si = {}; switch(param->svm_type) { case C_SVC: si.upper_bound = Malloc(double,prob->l); solve_c_svc(prob,param,alpha,&si,Cp,Cn); break; case NU_SVC: si.upper_bound = Malloc(double,prob->l); solve_nu_svc(prob,param,alpha,&si); break; case ONE_CLASS: si.upper_bound = Malloc(double,prob->l); solve_one_class(prob,param,alpha,&si); break; case EPSILON_SVR: si.upper_bound = Malloc(double,2*prob->l); solve_epsilon_svr(prob,param,alpha,&si); break; case NU_SVR: si.upper_bound = Malloc(double,2*prob->l); solve_nu_svr(prob,param,alpha,&si); break; } info("obj = %f, rho = %f\n",si.obj,si.rho); // output SVs int nSV = 0; int nBSV = 0; for(int i=0;il;i++) { if(fabs(alpha[i]) > 0) { ++nSV; if(prob->y[i] > 0) { if(fabs(alpha[i]) >= si.upper_bound[i]) ++nBSV; } else { if(fabs(alpha[i]) >= si.upper_bound[i]) ++nBSV; } } } free(si.upper_bound); info("nSV = %d, nBSV = %d\n",nSV,nBSV); decision_function f; f.alpha = alpha; f.rho = si.rho; return f; } // Platt's binary SVM Probablistic Output: an improvement from Lin et al. static void sigmoid_train( int l, const double *dec_values, const double *labels, double& A, double& B) { double prior1=0, prior0 = 0; int i; for (i=0;i 0) prior1+=1; else prior0+=1; int max_iter=100; // Maximal number of iterations double min_step=1e-10; // Minimal step taken in line search double sigma=1e-12; // For numerically strict PD of Hessian double eps=1e-5; double hiTarget=(prior1+1.0)/(prior1+2.0); double loTarget=1/(prior0+2.0); double *t=Malloc(double,l); double fApB,p,q,h11,h22,h21,g1,g2,det,dA,dB,gd,stepsize; double newA,newB,newf,d1,d2; int iter; // Initial Point and Initial Fun Value A=0.0; B=log((prior0+1.0)/(prior1+1.0)); double fval = 0.0; for (i=0;i0) t[i]=hiTarget; else t[i]=loTarget; fApB = dec_values[i]*A+B; if (fApB>=0) fval += t[i]*fApB + log(1+exp(-fApB)); else fval += (t[i] - 1)*fApB +log(1+exp(fApB)); } for (iter=0;iter= 0) { p=exp(-fApB)/(1.0+exp(-fApB)); q=1.0/(1.0+exp(-fApB)); } else { p=1.0/(1.0+exp(fApB)); q=exp(fApB)/(1.0+exp(fApB)); } d2=p*q; h11+=dec_values[i]*dec_values[i]*d2; h22+=d2; h21+=dec_values[i]*d2; d1=t[i]-p; g1+=dec_values[i]*d1; g2+=d1; } // Stopping Criteria if (fabs(g1)= min_step) { newA = A + stepsize * dA; newB = B + stepsize * dB; // New function value newf = 0.0; for (i=0;i= 0) newf += t[i]*fApB + log(1+exp(-fApB)); else newf += (t[i] - 1)*fApB +log(1+exp(fApB)); } // Check sufficient decrease if (newf=max_iter) info("Reaching maximal iterations in two-class probability estimates\n"); free(t); } static double sigmoid_predict(double decision_value, double A, double B) { double fApB = decision_value*A+B; // 1-p used later; avoid catastrophic cancellation if (fApB >= 0) return exp(-fApB)/(1.0+exp(-fApB)); else return 1.0/(1+exp(fApB)) ; } // Method 2 from the multiclass_prob paper by Wu, Lin, and Weng to predict probabilities static void multiclass_probability(int k, double **r, double *p) { int t,j; int iter = 0, max_iter=max(100,k); double **Q=Malloc(double *,k); double *Qp=Malloc(double,k); double pQp, eps=0.005/k; for (t=0;tmax_error) max_error=error; } if (max_error=max_iter) info("Exceeds max_iter in multiclass_prob\n"); for(t=0;tl); double *dec_values = Malloc(double,prob->l); // random shuffle for(i=0;il;i++) perm[i]=i; for(i=0;il;i++) { int j = i+rand()%(prob->l-i); swap(perm[i],perm[j]); } for(i=0;il/nr_fold; int end = (i+1)*prob->l/nr_fold; int j,k; struct svm_problem subprob; subprob.l = prob->l-(end-begin); subprob.x = Malloc(struct svm_node*,subprob.l); subprob.y = Malloc(double,subprob.l); subprob.W = Malloc(double,subprob.l); k=0; for(j=0;jx[perm[j]]; subprob.y[k] = prob->y[perm[j]]; subprob.W[k] = prob->W[perm[j]]; ++k; } for(j=end;jl;j++) { subprob.x[k] = prob->x[perm[j]]; subprob.y[k] = prob->y[perm[j]]; subprob.W[k] = prob->W[perm[j]]; ++k; } int p_count=0,n_count=0; for(j=0;j0) p_count++; else n_count++; if(p_count==0 && n_count==0) for(j=begin;j 0 && n_count == 0) for(j=begin;j 0) for(j=begin;jx[perm[j]],&(dec_values[perm[j]])); // ensure +1 -1 order; reason not using CV subroutine dec_values[perm[j]] *= submodel->label[0]; } svm_free_and_destroy_model(&submodel); svm_destroy_param(&subparam); } free(subprob.x); free(subprob.y); free(subprob.W); } sigmoid_train(prob->l,dec_values,prob->y,probA,probB); free(dec_values); free(perm); } // Binning method from the oneclass_prob paper by Que and Lin to predict the probability as a normal instance (i.e., not an outlier) static double predict_one_class_probability(const svm_model *model, double dec_value) { double prob_estimate = 0.0; int nr_marks = 10; if(dec_value < model->prob_density_marks[0]) prob_estimate = 0.001; else if(dec_value > model->prob_density_marks[nr_marks-1]) prob_estimate = 0.999; else { for(int i=1;iprob_density_marks[i]) { prob_estimate = (double)i/nr_marks; break; } } return prob_estimate; } static int compare_double(const void *a, const void *b) { if(*(double *)a > *(double *)b) return 1; else if(*(double *)a < *(double *)b) return -1; return 0; } // Get parameters for one-class SVM probability estimates static int svm_one_class_probability(const svm_problem *prob, const svm_model *model, double *prob_density_marks) { double *dec_values = Malloc(double,prob->l); double *pred_results = Malloc(double,prob->l); int ret = 0; int nr_marks = 10; for(int i=0;il;i++) pred_results[i] = svm_predict_values(model,prob->x[i],&dec_values[i]); qsort(dec_values,prob->l,sizeof(double),compare_double); int neg_counter=0; for(int i=0;il;i++) if(dec_values[i]>=0) { neg_counter = i; break; } int pos_counter = prob->l-neg_counter; if(neg_counterl); double mae = 0; svm_parameter newparam = *param; newparam.probability = 0; svm_cross_validation(prob,&newparam,nr_fold,ymv); for(i=0;il;i++) { ymv[i]=prob->y[i]-ymv[i]; mae += fabs(ymv[i]); } mae /= prob->l; double std=sqrt(2*mae*mae); int count=0; mae=0; for(i=0;il;i++) if (fabs(ymv[i]) > 5*std) count=count+1; else mae+=fabs(ymv[i]); mae /= (prob->l-count); info("Prob. model for test data: target value = predicted value + z,\nz: Laplace distribution e^(-|z|/sigma)/(2sigma),sigma= %g\n",mae); free(ymv); return mae; } // label: label name, start: begin of each class, count: #data of classes, perm: indices to the original data // perm, length l, must be allocated before calling this subroutine static void svm_group_classes(const svm_problem *prob, int *nr_class_ret, int **label_ret, int **start_ret, int **count_ret, int *perm) { int l = prob->l; int max_nr_class = 16; int nr_class = 0; int *label = Malloc(int,max_nr_class); int *count = Malloc(int,max_nr_class); int *data_label = Malloc(int,l); int i; for(i=0;iy[i]; int j; for(j=0;j 0. // static void remove_zero_weight(svm_problem *newprob, const svm_problem *prob) { int i; int l = 0; for(i=0;il;i++) if(prob->W[i] > 0) l++; *newprob = *prob; newprob->l = l; newprob->x = Malloc(svm_node*,l); newprob->y = Malloc(double,l); newprob->W = Malloc(double,l); int j = 0; for(i=0;il;i++) if(prob->W[i] > 0) { newprob->x[j] = prob->x[i]; newprob->y[j] = prob->y[i]; newprob->W[j] = prob->W[i]; j++; } } // // Interface functions // svm_model *svm_train(const svm_problem *prob, const svm_parameter *param) { svm_problem newprob; remove_zero_weight(&newprob, prob); prob = &newprob; svm_model *model = Malloc(svm_model,1); model->param = *param; model->free_sv = 0; // XXX if(param->svm_type == ONE_CLASS || param->svm_type == EPSILON_SVR || param->svm_type == NU_SVR) { // regression or one-class-svm model->nr_class = 2; model->label = NULL; model->nSV = NULL; model->probA = NULL; model->probB = NULL; model->prob_density_marks = NULL; model->sv_coef = Malloc(double *,1); decision_function f = svm_train_one(prob,param,0,0); model->rho = Malloc(double,1); model->rho[0] = f.rho; int nSV = 0; int i; for(i=0;il;i++) if(fabs(f.alpha[i]) > 0) ++nSV; model->l = nSV; model->SV = Malloc(svm_node *,nSV); model->sv_coef[0] = Malloc(double,nSV); model->sv_indices = Malloc(int,nSV); int j = 0; for(i=0;il;i++) if(fabs(f.alpha[i]) > 0) { model->SV[j] = prob->x[i]; model->sv_coef[0][j] = f.alpha[i]; model->sv_indices[j] = i+1; ++j; } if(param->probability && (param->svm_type == EPSILON_SVR || param->svm_type == NU_SVR)) { model->probA = Malloc(double,1); model->probA[0] = svm_svr_probability(prob,param); } else if(param->probability && param->svm_type == ONE_CLASS) { int nr_marks = 10; double *prob_density_marks = Malloc(double,nr_marks); if(svm_one_class_probability(prob,model,prob_density_marks) == 0) model->prob_density_marks = prob_density_marks; else free(prob_density_marks); } free(f.alpha); } else { // classification int l = prob->l; int nr_class; int *label = NULL; int *start = NULL; int *count = NULL; int *perm = Malloc(int,l); // group training data of the same class svm_group_classes(prob,&nr_class,&label,&start,&count,perm); if(nr_class == 1) info("WARNING: training data in only one class. See README for details.\n"); svm_node **x = Malloc(svm_node *,l); double *W; W = Malloc(double,l); int i; for(i=0;ix[perm[i]]; W[i] = prob->W[perm[i]]; } // calculate weighted C double *weighted_C = Malloc(double, nr_class); for(i=0;iC; for(i=0;inr_weight;i++) { int j; for(j=0;jweight_label[i] == label[j]) break; if(j == nr_class) fprintf(stderr,"WARNING: class label %d specified in weight is not found\n", param->weight_label[i]); else weighted_C[j] *= param->weight[i]; } // train k*(k-1)/2 models bool *nonzero = Malloc(bool,l); for(i=0;iprobability) { probA=Malloc(double,nr_class*(nr_class-1)/2); probB=Malloc(double,nr_class*(nr_class-1)/2); } int p = 0; for(i=0;iprobability) svm_binary_svc_probability(&sub_prob,param,weighted_C[i],weighted_C[j],probA[p],probB[p]); f[p] = svm_train_one(&sub_prob,param,weighted_C[i],weighted_C[j]); for(k=0;k 0) nonzero[si+k] = true; for(k=0;k 0) nonzero[sj+k] = true; free(sub_prob.x); free(sub_prob.y); free(sub_prob.W); ++p; } // build output model->nr_class = nr_class; model->label = Malloc(int,nr_class); for(i=0;ilabel[i] = label[i]; model->rho = Malloc(double,nr_class*(nr_class-1)/2); for(i=0;irho[i] = f[i].rho; if(param->probability) { model->probA = Malloc(double,nr_class*(nr_class-1)/2); model->probB = Malloc(double,nr_class*(nr_class-1)/2); for(i=0;iprobA[i] = probA[i]; model->probB[i] = probB[i]; } } else { model->probA=NULL; model->probB=NULL; } model->prob_density_marks=NULL; // for one-class SVM probabilistic outputs only int total_sv = 0; int *nz_count = Malloc(int,nr_class); model->nSV = Malloc(int,nr_class); for(i=0;inSV[i] = nSV; nz_count[i] = nSV; } info("Total nSV = %d\n",total_sv); model->l = total_sv; model->SV = Malloc(svm_node *,total_sv); model->sv_indices = Malloc(int,total_sv); p = 0; for(i=0;iSV[p] = x[i]; model->sv_indices[p++] = perm[i] + 1; } int *nz_start = Malloc(int,nr_class); nz_start[0] = 0; for(i=1;isv_coef = Malloc(double *,nr_class-1); for(i=0;isv_coef[i] = Malloc(double,total_sv); p = 0; for(i=0;isv_coef[j-1][q++] = f[p].alpha[k]; q = nz_start[j]; for(k=0;ksv_coef[i][q++] = f[p].alpha[ci+k]; ++p; } free(label); free(probA); free(probB); free(count); free(perm); free(start); free(W); free(x); free(weighted_C); free(nonzero); for(i=0;il; int *perm = Malloc(int,l); int nr_class; if (nr_fold > l) { fprintf(stderr,"WARNING: # folds (%d) > # data (%d). Will use # folds = # data instead (i.e., leave-one-out cross validation)\n", nr_fold, l); nr_fold = l; } fold_start = Malloc(int,nr_fold+1); // stratified cv may not give leave-one-out rate // Each class to l folds -> some folds may have zero elements if((param->svm_type == C_SVC || param->svm_type == NU_SVC) && nr_fold < l) { int *start = NULL; int *label = NULL; int *count = NULL; svm_group_classes(prob,&nr_class,&label,&start,&count,perm); // random shuffle and then data grouped by fold using the array perm int *fold_count = Malloc(int,nr_fold); int c; int *index = Malloc(int,l); for(i=0;ix[perm[j]]; subprob.y[k] = prob->y[perm[j]]; subprob.W[k] = prob->W[perm[j]]; ++k; } for(j=end;jx[perm[j]]; subprob.y[k] = prob->y[perm[j]]; subprob.W[k] = prob->W[perm[j]]; ++k; } struct svm_model *submodel = svm_train(&subprob,param); if(param->probability && (param->svm_type == C_SVC || param->svm_type == NU_SVC)) { double *prob_estimates=Malloc(double,svm_get_nr_class(submodel)); for(j=begin;jx[perm[j]],prob_estimates); free(prob_estimates); } else for(j=begin;jx[perm[j]]); svm_free_and_destroy_model(&submodel); free(subprob.x); free(subprob.y); free(subprob.W); } free(fold_start); free(perm); } int svm_get_svm_type(const svm_model *model) { return model->param.svm_type; } int svm_get_nr_class(const svm_model *model) { return model->nr_class; } void svm_get_labels(const svm_model *model, int* label) { if (model->label != NULL) for(int i=0;inr_class;i++) label[i] = model->label[i]; } void svm_get_sv_indices(const svm_model *model, int* indices) { if (model->sv_indices != NULL) for(int i=0;il;i++) indices[i] = model->sv_indices[i]; } int svm_get_nr_sv(const svm_model *model) { return model->l; } double svm_get_svr_probability(const svm_model *model) { if ((model->param.svm_type == EPSILON_SVR || model->param.svm_type == NU_SVR) && model->probA!=NULL) return model->probA[0]; else { fprintf(stderr,"Model doesn't contain information for SVR probability inference\n"); return 0; } } double svm_predict_values(const svm_model *model, const svm_node *x, double* dec_values) { int i; if(model->param.svm_type == ONE_CLASS || model->param.svm_type == EPSILON_SVR || model->param.svm_type == NU_SVR) { double *sv_coef = model->sv_coef[0]; double sum = 0; for(i=0;il;i++) sum += sv_coef[i] * Kernel::k_function(x,model->SV[i],model->param); sum -= model->rho[0]; *dec_values = sum; if(model->param.svm_type == ONE_CLASS) return (sum>0)?1:-1; else return sum; } else { int nr_class = model->nr_class; int l = model->l; double *kvalue = Malloc(double,l); for(i=0;iSV[i],model->param); int *start = Malloc(int,nr_class); start[0] = 0; for(i=1;inSV[i-1]; int *vote = Malloc(int,nr_class); for(i=0;inSV[i]; int cj = model->nSV[j]; int k; double *coef1 = model->sv_coef[j-1]; double *coef2 = model->sv_coef[i]; for(k=0;krho[p]; dec_values[p] = sum; if(dec_values[p] > 0) ++vote[i]; else ++vote[j]; p++; } int vote_max_idx = 0; for(i=1;i vote[vote_max_idx]) vote_max_idx = i; free(kvalue); free(start); free(vote); return model->label[vote_max_idx]; } } double svm_predict(const svm_model *model, const svm_node *x) { int nr_class = model->nr_class; double *dec_values; if(model->param.svm_type == ONE_CLASS || model->param.svm_type == EPSILON_SVR || model->param.svm_type == NU_SVR) dec_values = Malloc(double, 1); else dec_values = Malloc(double, nr_class*(nr_class-1)/2); double pred_result = svm_predict_values(model, x, dec_values); free(dec_values); return pred_result; } double svm_predict_probability( const svm_model *model, const svm_node *x, double *prob_estimates) { if ((model->param.svm_type == C_SVC || model->param.svm_type == NU_SVC) && model->probA!=NULL && model->probB!=NULL) { int i; int nr_class = model->nr_class; double *dec_values = Malloc(double, nr_class*(nr_class-1)/2); svm_predict_values(model, x, dec_values); double min_prob=1e-7; double **pairwise_prob=Malloc(double *,nr_class); for(i=0;iprobA[k],model->probB[k]),min_prob),1-min_prob); pairwise_prob[j][i]=1-pairwise_prob[i][j]; k++; } if (nr_class == 2) { prob_estimates[0] = pairwise_prob[0][1]; prob_estimates[1] = pairwise_prob[1][0]; } else multiclass_probability(nr_class,pairwise_prob,prob_estimates); int prob_max_idx = 0; for(i=1;i prob_estimates[prob_max_idx]) prob_max_idx = i; for(i=0;ilabel[prob_max_idx]; } else if(model->param.svm_type == ONE_CLASS && model->prob_density_marks!=NULL) { double dec_value; double pred_result = svm_predict_values(model,x,&dec_value); prob_estimates[0] = predict_one_class_probability(model,dec_value); prob_estimates[1] = 1-prob_estimates[0]; return pred_result; } else return svm_predict(model, x); } static const char *svm_type_table[] = { "c_svc","nu_svc","one_class","epsilon_svr","nu_svr",NULL }; static const char *kernel_type_table[]= { "linear","polynomial","rbf","sigmoid","precomputed",NULL }; int svm_save_model(const char *model_file_name, const svm_model *model) { FILE *fp = fopen(model_file_name,"w"); if(fp==NULL) return -1; char *old_locale = setlocale(LC_ALL, NULL); if (old_locale) { old_locale = strdup(old_locale); } setlocale(LC_ALL, "C"); const svm_parameter& param = model->param; fprintf(fp,"svm_type %s\n", svm_type_table[param.svm_type]); fprintf(fp,"kernel_type %s\n", kernel_type_table[param.kernel_type]); if(param.kernel_type == POLY) fprintf(fp,"degree %d\n", param.degree); if(param.kernel_type == POLY || param.kernel_type == RBF || param.kernel_type == SIGMOID) fprintf(fp,"gamma %.17g\n", param.gamma); if(param.kernel_type == POLY || param.kernel_type == SIGMOID) fprintf(fp,"coef0 %.17g\n", param.coef0); int nr_class = model->nr_class; int l = model->l; fprintf(fp, "nr_class %d\n", nr_class); fprintf(fp, "total_sv %d\n",l); { fprintf(fp, "rho"); for(int i=0;irho[i]); fprintf(fp, "\n"); } if(model->label) { fprintf(fp, "label"); for(int i=0;ilabel[i]); fprintf(fp, "\n"); } if(model->probA) // regression has probA only { fprintf(fp, "probA"); for(int i=0;iprobA[i]); fprintf(fp, "\n"); } if(model->probB) { fprintf(fp, "probB"); for(int i=0;iprobB[i]); fprintf(fp, "\n"); } if(model->prob_density_marks) { fprintf(fp, "prob_density_marks"); int nr_marks=10; for(int i=0;iprob_density_marks[i]); fprintf(fp, "\n"); } if(model->nSV) { fprintf(fp, "nr_sv"); for(int i=0;inSV[i]); fprintf(fp, "\n"); } fprintf(fp, "SV\n"); const double * const *sv_coef = model->sv_coef; const svm_node * const *SV = model->SV; for(int i=0;ivalue)); else while(p->index != -1) { fprintf(fp,"%d:%.8g ",p->index,p->value); p++; } fprintf(fp, "\n"); } setlocale(LC_ALL, old_locale); free(old_locale); if (ferror(fp) != 0 || fclose(fp) != 0) return -1; else return 0; } static char *line = NULL; static int max_line_len; static char* readline(FILE *input) { int len; if(fgets(line,max_line_len,input) == NULL) return NULL; while(strrchr(line,'\n') == NULL) { max_line_len *= 2; line = (char *) realloc(line,max_line_len); len = (int) strlen(line); if(fgets(line+len,max_line_len-len,input) == NULL) break; } return line; } // // FSCANF helps to handle fscanf failures. // Its do-while block avoids the ambiguity when // if (...) // FSCANF(); // is used // #define FSCANF(_stream, _format, _var) do{ if (fscanf(_stream, _format, _var) != 1) return false; }while(0) bool read_model_header(FILE *fp, svm_model* model) { svm_parameter& param = model->param; // parameters for training only won't be assigned, but arrays are assigned as NULL for safety param.nr_weight = 0; param.weight_label = NULL; param.weight = NULL; char cmd[81]; while(1) { FSCANF(fp,"%80s",cmd); if(strcmp(cmd,"svm_type")==0) { FSCANF(fp,"%80s",cmd); int i; for(i=0;svm_type_table[i];i++) { if(strcmp(svm_type_table[i],cmd)==0) { param.svm_type=i; break; } } if(svm_type_table[i] == NULL) { fprintf(stderr,"unknown svm type.\n"); return false; } } else if(strcmp(cmd,"kernel_type")==0) { FSCANF(fp,"%80s",cmd); int i; for(i=0;kernel_type_table[i];i++) { if(strcmp(kernel_type_table[i],cmd)==0) { param.kernel_type=i; break; } } if(kernel_type_table[i] == NULL) { fprintf(stderr,"unknown kernel function.\n"); return false; } } else if(strcmp(cmd,"degree")==0) FSCANF(fp,"%d",¶m.degree); else if(strcmp(cmd,"gamma")==0) FSCANF(fp,"%lf",¶m.gamma); else if(strcmp(cmd,"coef0")==0) FSCANF(fp,"%lf",¶m.coef0); else if(strcmp(cmd,"nr_class")==0) FSCANF(fp,"%d",&model->nr_class); else if(strcmp(cmd,"total_sv")==0) FSCANF(fp,"%d",&model->l); else if(strcmp(cmd,"rho")==0) { int n = model->nr_class * (model->nr_class-1)/2; model->rho = Malloc(double,n); for(int i=0;irho[i]); } else if(strcmp(cmd,"label")==0) { int n = model->nr_class; model->label = Malloc(int,n); for(int i=0;ilabel[i]); } else if(strcmp(cmd,"probA")==0) { int n = model->nr_class * (model->nr_class-1)/2; model->probA = Malloc(double,n); for(int i=0;iprobA[i]); } else if(strcmp(cmd,"probB")==0) { int n = model->nr_class * (model->nr_class-1)/2; model->probB = Malloc(double,n); for(int i=0;iprobB[i]); } else if(strcmp(cmd,"prob_density_marks")==0) { int n = 10; // nr_marks model->prob_density_marks = Malloc(double,n); for(int i=0;iprob_density_marks[i]); } else if(strcmp(cmd,"nr_sv")==0) { int n = model->nr_class; model->nSV = Malloc(int,n); for(int i=0;inSV[i]); } else if(strcmp(cmd,"SV")==0) { while(1) { int c = getc(fp); if(c==EOF || c=='\n') break; } break; } else { fprintf(stderr,"unknown text in model file: [%s]\n",cmd); return false; } } return true; } svm_model *svm_load_model(const char *model_file_name) { FILE *fp = fopen(model_file_name,"rb"); if(fp==NULL) return NULL; char *old_locale = setlocale(LC_ALL, NULL); if (old_locale) { old_locale = strdup(old_locale); } setlocale(LC_ALL, "C"); // read parameters svm_model *model = Malloc(svm_model,1); model->rho = NULL; model->probA = NULL; model->probB = NULL; model->prob_density_marks = NULL; model->sv_indices = NULL; model->label = NULL; model->nSV = NULL; // read header if (!read_model_header(fp, model)) { fprintf(stderr, "ERROR: fscanf failed to read model\n"); setlocale(LC_ALL, old_locale); free(old_locale); free(model->rho); free(model->label); free(model->nSV); free(model); return NULL; } // read sv_coef and SV int elements = 0; long pos = ftell(fp); max_line_len = 1024; line = Malloc(char,max_line_len); char *p,*endptr,*idx,*val; while(readline(fp)!=NULL) { p = strtok(line,":"); while(1) { p = strtok(NULL,":"); if(p == NULL) break; ++elements; } } elements += model->l; fseek(fp,pos,SEEK_SET); int m = model->nr_class - 1; int l = model->l; model->sv_coef = Malloc(double *,m); int i; for(i=0;isv_coef[i] = Malloc(double,l); model->SV = Malloc(svm_node*,l); svm_node *x_space = NULL; if(l>0) x_space = Malloc(svm_node,elements); int j=0; for(i=0;iSV[i] = &x_space[j]; p = strtok(line, " \t"); model->sv_coef[0][i] = strtod(p,&endptr); for(int k=1;ksv_coef[k][i] = strtod(p,&endptr); } while(1) { idx = strtok(NULL, ":"); val = strtok(NULL, " \t"); if(val == NULL) break; x_space[j].index = (int) strtol(idx,&endptr,10); x_space[j].value = strtod(val,&endptr); ++j; } x_space[j++].index = -1; } free(line); setlocale(LC_ALL, old_locale); free(old_locale); if (ferror(fp) != 0 || fclose(fp) != 0) return NULL; model->free_sv = 1; // XXX return model; } void svm_free_model_content(svm_model* model_ptr) { if(model_ptr->free_sv && model_ptr->l > 0 && model_ptr->SV != NULL) free((void *)(model_ptr->SV[0])); if(model_ptr->sv_coef) { for(int i=0;inr_class-1;i++) free(model_ptr->sv_coef[i]); } free(model_ptr->SV); model_ptr->SV = NULL; free(model_ptr->sv_coef); model_ptr->sv_coef = NULL; free(model_ptr->rho); model_ptr->rho = NULL; free(model_ptr->label); model_ptr->label = NULL; free(model_ptr->probA); model_ptr->probA = NULL; free(model_ptr->probB); model_ptr->probB = NULL; free(model_ptr->prob_density_marks); model_ptr->prob_density_marks = NULL; free(model_ptr->sv_indices); model_ptr->sv_indices = NULL; free(model_ptr->nSV); model_ptr->nSV = NULL; } void svm_free_and_destroy_model(svm_model** model_ptr_ptr) { if(model_ptr_ptr != NULL && *model_ptr_ptr != NULL) { svm_free_model_content(*model_ptr_ptr); free(*model_ptr_ptr); *model_ptr_ptr = NULL; } } void svm_destroy_param(svm_parameter* param) { free(param->weight_label); free(param->weight); } const char *svm_check_parameter(const svm_problem *prob, const svm_parameter *param) { // svm_type int svm_type = param->svm_type; if(svm_type != C_SVC && svm_type != NU_SVC && svm_type != ONE_CLASS && svm_type != EPSILON_SVR && svm_type != NU_SVR) return "unknown svm type"; // kernel_type, degree int kernel_type = param->kernel_type; if(kernel_type != LINEAR && kernel_type != POLY && kernel_type != RBF && kernel_type != SIGMOID && kernel_type != PRECOMPUTED) return "unknown kernel type"; if((kernel_type == POLY || kernel_type == RBF || kernel_type == SIGMOID) && param->gamma < 0) return "gamma < 0"; if(kernel_type == POLY && param->degree < 0) return "degree of polynomial kernel < 0"; // cache_size,eps,C,nu,p,shrinking if(param->cache_size <= 0) return "cache_size <= 0"; if(param->eps <= 0) return "eps <= 0"; if(svm_type == C_SVC || svm_type == EPSILON_SVR || svm_type == NU_SVR) if(param->C <= 0) return "C <= 0"; if(svm_type == NU_SVC || svm_type == ONE_CLASS || svm_type == NU_SVR) if(param->nu <= 0 || param->nu > 1) return "nu <= 0 or nu > 1"; if(svm_type == EPSILON_SVR) if(param->p < 0) return "p < 0"; if(param->shrinking != 0 && param->shrinking != 1) return "shrinking != 0 and shrinking != 1"; if(param->probability != 0 && param->probability != 1) return "probability != 0 and probability != 1"; // check whether nu-svc is feasible if(svm_type == NU_SVC) { int l = prob->l; int max_nr_class = 16; int nr_class = 0; int *label = Malloc(int,max_nr_class); double *count = Malloc(double,max_nr_class); int i; for(i=0;iy[i]; int j; for(j=0;jW[i]; break; } if(j == nr_class) { if(nr_class == max_nr_class) { max_nr_class *= 2; label = (int *)realloc(label,max_nr_class*sizeof(int)); count = (double *)realloc(count,max_nr_class*sizeof(double)); } label[nr_class] = this_label; count[nr_class] = prob->W[i]; ++nr_class; } } for(i=0;inu*(n1+n2)/2 > min(n1,n2)) { free(label); free(count); return "specified nu is infeasible"; } } } free(label); free(count); } return NULL; } int svm_check_probability_model(const svm_model *model) { return ((model->param.svm_type == C_SVC || model->param.svm_type == NU_SVC) && model->probA!=NULL && model->probB!=NULL) || (model->param.svm_type == ONE_CLASS && model->prob_density_marks!=NULL) || ((model->param.svm_type == EPSILON_SVR || model->param.svm_type == NU_SVR) && model->probA!=NULL); } int svm_get_max_iter_count(void) { return svm_max_iter_count; } void svm_reset_max_iter_count(void) { svm_max_iter_count = 0; } void svm_set_print_string_function(void (*print_func)(const char *)) { if(print_func == NULL) svm_print_string = &print_string_stdout; else svm_print_string = print_func; } statistics-release-2.1.2/src/svm.h000066400000000000000000000111741527311104300170720ustar00rootroot00000000000000/* Copyright (C) 2025 Chih-Chung Chang and Chih-Jen Lin This file is part of the statistics package for GNU Octave. Permission granted by Chih-Jen Lin to the package maintainer to include this file and double license under GPLv3 by means of personal communication. This file is LIBSVM 3.37 with the per-instance weights of the "Weights for data instances" variant from the LIBSVM Tools page merged into it, so that each instance's weight scales its box constraint. Chih-Jen Lin confirmed on 2026-09-16 that the permission above covers those additions. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #ifndef _LIBSVM_H #define _LIBSVM_H #define LIBSVM_VERSION 337 #ifdef __cplusplus extern "C" { #endif extern int libsvm_version; struct svm_node { int index; double value; }; struct svm_problem { int l; double *y; struct svm_node **x; double *W; /* instance weight */ }; enum { C_SVC, NU_SVC, ONE_CLASS, EPSILON_SVR, NU_SVR }; /* svm_type */ enum { LINEAR, POLY, RBF, SIGMOID, PRECOMPUTED }; /* kernel_type */ struct svm_parameter { int svm_type; int kernel_type; int degree; /* for poly */ double gamma; /* for poly/rbf/sigmoid */ double coef0; /* for poly/sigmoid */ /* these are for training only */ double cache_size; /* in MB */ double eps; /* stopping criteria */ double C; /* for C_SVC, EPSILON_SVR and NU_SVR */ int nr_weight; /* for C_SVC */ int *weight_label; /* for C_SVC */ double* weight; /* for C_SVC */ double nu; /* for NU_SVC, ONE_CLASS, and NU_SVR */ double p; /* for EPSILON_SVR */ int shrinking; /* use the shrinking heuristics */ int probability; /* do probability estimates */ }; // // svm_model // struct svm_model { struct svm_parameter param; /* parameter */ int nr_class; /* number of classes, = 2 in regression/one class svm */ int l; /* total #SV */ struct svm_node **SV; /* SVs (SV[l]) */ double **sv_coef; /* coefficients for SVs in decision functions (sv_coef[k-1][l]) */ double *rho; /* constants in decision functions (rho[k*(k-1)/2]) */ double *probA; /* pairwise probability information */ double *probB; double *prob_density_marks; /* probability information for ONE_CLASS */ int *sv_indices; /* sv_indices[0,...,nSV-1] are values in [1,...,num_traning_data] to indicate SVs in the training set */ /* for classification only */ int *label; /* label of each class (label[k]) */ int *nSV; /* number of SVs for each class (nSV[k]) */ /* nSV[0] + nSV[1] + ... + nSV[k-1] = l */ /* XXX */ int free_sv; /* 1 if svm_model is created by svm_load_model*/ /* 0 if svm_model is created by svm_train */ }; struct svm_model *svm_train(const struct svm_problem *prob, const struct svm_parameter *param); void svm_cross_validation(const struct svm_problem *prob, const struct svm_parameter *param, int nr_fold, double *target); int svm_save_model(const char *model_file_name, const struct svm_model *model); struct svm_model *svm_load_model(const char *model_file_name); int svm_get_svm_type(const struct svm_model *model); int svm_get_nr_class(const struct svm_model *model); void svm_get_labels(const struct svm_model *model, int *label); void svm_get_sv_indices(const struct svm_model *model, int *sv_indices); int svm_get_nr_sv(const struct svm_model *model); double svm_get_svr_probability(const struct svm_model *model); double svm_predict_values(const struct svm_model *model, const struct svm_node *x, double* dec_values); double svm_predict(const struct svm_model *model, const struct svm_node *x); double svm_predict_probability(const struct svm_model *model, const struct svm_node *x, double* prob_estimates); void svm_free_model_content(struct svm_model *model_ptr); void svm_free_and_destroy_model(struct svm_model **model_ptr_ptr); void svm_destroy_param(struct svm_parameter *param); const char *svm_check_parameter(const struct svm_problem *prob, const struct svm_parameter *param); int svm_check_probability_model(const struct svm_model *model); void svm_set_print_string_function(void (*print_func)(const char *)); int svm_get_max_iter_count(void); void svm_reset_max_iter_count(void); #ifdef __cplusplus } #endif #endif /* _LIBSVM_H */ statistics-release-2.1.2/src/svm_model_octave.cpp000066400000000000000000000233151527311104300221460ustar00rootroot00000000000000/* Copyright (C) 2022 Andreas Bertsatos Copyright (C) 2025 Avanish Salunke Based on the Octave LIBSVM wrapper created by Alan Meeson (2014) based on an earlier version of the LIBSVM (3.18) library for MATLAB. Current implementation is based on LIBSVM 3.36 (2025) by Chih-Chung Chang and Chih-Jen Lin. This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include "svm.h" #define NUM_OF_RETURN_FIELD 12 #define Malloc(type,n) (type *)malloc((n)*sizeof(type)) [[maybe_unused]] static const char *field_names[] = { "Parameters", "nr_class", "totalSV", "rho", "Label", "sv_indices", "ProbA", "ProbB", "nSV", "sv_coef", "SVs", "ProbDensityMarks" }; const char *model_to_octave_structure(octave_value_list &plhs, int num_of_feature, struct svm_model *model) { int i, j, n; double *ptr; octave_scalar_map osm_model; // Parameters ColumnVector cm_parameters (5); cm_parameters(0) = model->param.svm_type; cm_parameters(1) = model->param.kernel_type; cm_parameters(2) = model->param.degree; cm_parameters(3) = model->param.gamma; cm_parameters(4) = model->param.coef0; osm_model.assign("Parameters", cm_parameters); // nr_class osm_model.assign("nr_class", octave_value(model->nr_class)); // total SV osm_model.assign("totalSV", octave_value(model->l)); // rho n = model->nr_class*(model->nr_class-1)/2; ColumnVector cm_rho(n); for(i = 0; i < n; i++) { cm_rho(i) = model->rho[i]; } osm_model.assign("rho", cm_rho); // Label if(model->label) { ColumnVector cm_label(model->nr_class); for(i = 0; i < model->nr_class; i++) { cm_label(i) = model->label[i]; } osm_model.assign("Label", cm_label); } else { osm_model.assign("Label", ColumnVector(0)); } // sv_indices if(model->sv_indices) { ColumnVector cm_sv_indices(model->l); for(i = 0; i < model->l; i++) { cm_sv_indices(i) = model->sv_indices[i]; } osm_model.assign("sv_indices", cm_sv_indices); } else { osm_model.assign("sv_indices", ColumnVector(0)); } // probA if(model->probA != NULL) { ColumnVector cm_proba(n); for(i = 0; i < n; i++) { cm_proba(i) = model->probA[i]; } osm_model.assign("ProbA", cm_proba); } else { osm_model.assign("ProbA", ColumnVector(0)); } // probB if(model->probB != NULL) { ColumnVector cm_probb(n); for(i = 0; i < n; i++) { cm_probb(i) = model->probB[i]; } osm_model.assign("ProbB", cm_probb); } else { osm_model.assign("ProbB", ColumnVector(0)); } // nSV if(model->nSV) { ColumnVector cm_nsv(model->nr_class); for(i = 0; i < model->nr_class; i++) { cm_nsv(i) = model->nSV[i]; } osm_model.assign("nSV", cm_nsv); } else { osm_model.assign("nSV", ColumnVector(0)); } // sv_coef Matrix m_sv_coef(model->l, model->nr_class-1); for (i = 0; i < model->nr_class-1; i++) { for(j = 0; j < model->l; j++) { m_sv_coef(j,i) = model->sv_coef[i][j]; } } osm_model.assign("sv_coef", m_sv_coef); // SVs { int ir_index, nonzero_element; octave_idx_type *ir, *jc; //mxArray *pprhs[1], *pplhs[1]; if(model->param.kernel_type == PRECOMPUTED) { nonzero_element = model->l; num_of_feature = 1; } else { nonzero_element = 0; for(i = 0; i < model->l; i++) { j = 0; while(model->SV[i][j].index != -1) { nonzero_element++; j++; } } } // SV in column, easier accessing SparseMatrix sm_rhs = SparseMatrix((octave_idx_type)num_of_feature, (octave_idx_type)model->l, (octave_idx_type)nonzero_element); ir = sm_rhs.ridx(); jc = sm_rhs.cidx(); ptr = (double*) sm_rhs.data(); jc[0] = ir_index = 0; for(i = 0; i < model->l; i++) { if(model->param.kernel_type == PRECOMPUTED) { // make a (1 x model->l) matrix ir[ir_index] = 0; ptr[ir_index] = model->SV[i][0].value; ir_index++; jc[i+1] = jc[i] + 1; } else { int x_index = 0; while (model->SV[i][x_index].index != -1) { ir[ir_index] = model->SV[i][x_index].index - 1; ptr[ir_index] = model->SV[i][x_index].value; ir_index++, x_index++; } jc[i+1] = jc[i] + x_index; } } // transpose back to SV in row sm_rhs = sm_rhs.transpose(); osm_model.assign("SVs", sm_rhs); } // changes from libsvm 3.36 if(model->prob_density_marks) { int nr_marks = 10; Matrix m_marks(nr_marks, 1); for(int i = 0; i < nr_marks; i++) { m_marks(i) = model->prob_density_marks[i]; } osm_model.setfield("ProbDensityMarks", m_marks); } else { osm_model.setfield("ProbDensityMarks", Matrix(0, 0)); } /* return */ plhs(0) = osm_model; return NULL; } struct svm_model *octave_matrix_to_model(octave_scalar_map &octave_model, [[maybe_unused]] const char **msg) { int i, j, n; double *ptr; struct svm_node *x_space; struct svm_model *model; model = Malloc(struct svm_model, 1); model->rho = NULL; model->probA = NULL; model->probB = NULL; model->label = NULL; model->prob_density_marks = NULL; model->sv_indices = NULL; model->nSV = NULL; model->free_sv = 1; // XXX //Parameters ColumnVector cm_parameters = octave_model.getfield("Parameters").column_vector_value(); model->param.svm_type = (int)cm_parameters(0); model->param.kernel_type = (int)cm_parameters(1); model->param.degree = (int)cm_parameters(2); model->param.gamma = cm_parameters(3); model->param.coef0 = cm_parameters(4); //nr_class model->nr_class = (int)octave_model.getfield("nr_class").int_value(); //total SV model->l = (int)octave_model.getfield("totalSV").int_value(); //rho n = model->nr_class * (model->nr_class-1)/2; model->rho = (double*) malloc(n*sizeof(double)); ColumnVector cm_rho = octave_model.getfield("rho").column_vector_value(); for(i = 0; i < n; i++) { model->rho[i] = cm_rho(i); } //label if (!octave_model.getfield("Label").isempty()) { model->label = (int*) malloc(model->nr_class*sizeof(int)); ColumnVector cm_label = octave_model.getfield("Label").column_vector_value(); for(i = 0; i < model->nr_class; i++) { model->label[i] = (int)cm_label(i); } } //sv_indices if (!octave_model.getfield("sv_indices").isempty()) { model->sv_indices = (int*) malloc(model->l*sizeof(int)); ColumnVector cv_svi = octave_model.getfield("sv_indices").column_vector_value(); for(i = 0; i < model->l; i++) { model->sv_indices[i] = (int)cv_svi(i); } } // probA if(!octave_model.getfield("ProbA").isempty()) { model->probA = (double*) malloc(n*sizeof(double)); ColumnVector cv_proba = octave_model.getfield("ProbA").column_vector_value(); for(i = 0; i < n; i++) { model->probA[i] = cv_proba(i); } } // probB if(!octave_model.getfield("ProbB").isempty()) { model->probB = (double*) malloc(n*sizeof(double)); ColumnVector cv_probb = octave_model.getfield("ProbB").column_vector_value(); for(i = 0; i < n; i++) { model->probB[i] = cv_probb(i); } } // nSV if(!octave_model.getfield("nSV").isempty()) { model->nSV = (int*) malloc(model->nr_class*sizeof(int)); ColumnVector cv_nsv = octave_model.getfield("nSV").column_vector_value(); for(i = 0; i < model->nr_class; i++) { model->nSV[i] = (int)cv_nsv(i); } } // sv_coef Matrix m_sv_coef = octave_model.getfield("sv_coef").matrix_value(); ptr = (double*) m_sv_coef.data(); model->sv_coef = (double**) malloc((model->nr_class-1)*sizeof(double)); for(i = 0; i < model->nr_class - 1; i++ ) { model->sv_coef[i] = (double*) malloc((model->l)*sizeof(double)); } for(i = 0; i < model->nr_class - 1; i++) { for(j = 0; j < model->l; j++) { model->sv_coef[i][j] = ptr[i*(model->l)+j];//m_sv_coef(i,j); } } // SV { int sr, elements; int num_samples; octave_idx_type *ir, *jc; // transpose SV SparseMatrix sm_sv = octave_model.getfield("SVs").sparse_matrix_value(); sm_sv = sm_sv.transpose(); sr = (int)sm_sv.cols(); ptr = (double*)sm_sv.data(); ir = sm_sv.ridx(); jc = sm_sv.cidx(); num_samples = (int)sm_sv.nzmax(); elements = num_samples + sr; model->SV = (struct svm_node **) malloc(sr * sizeof(struct svm_node *)); x_space = (struct svm_node *)malloc(elements * sizeof(struct svm_node)); // SV is in column for(i = 0; i < sr; i++) { int low = (int)jc[i], high = (int)jc[i+1]; int x_index = 0; model->SV[i] = &x_space[low+i]; for(j = low; j < high; j++) { model->SV[i][x_index].index = (int)ir[j] + 1; model->SV[i][x_index].value = ptr[j]; x_index++; } model->SV[i][x_index].index = -1; } } // changes from libsvm 3.36 if (octave_model.isfield("ProbDensityMarks")) { Matrix m_marks = octave_model.getfield("ProbDensityMarks").matrix_value(); if (m_marks.numel() > 0) { int nr_marks = 10; model->prob_density_marks = (double*) malloc(nr_marks * sizeof(double)); for(int i = 0; i < nr_marks; i++) { model->prob_density_marks[i] = m_marks(i); } } else { model->prob_density_marks = NULL; } } else { model->prob_density_marks = NULL; } return model; } statistics-release-2.1.2/src/svm_model_octave.h000066400000000000000000000024461527311104300216150ustar00rootroot00000000000000/* Copyright (C) 2022 Andreas Bertsatos Copyright (C) 2025 Avanish Salunke Based on the Octave LIBSVM wrapper created by Alan Meeson (2014) based on an earlier version of the LIBSVM (3.18) library for MATLAB. Current implementation is based on LIBSVM 3.37 (2025) by Chih-Chung Chang and Chih-Jen Lin. This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include "svm.h" const char *model_to_octave_structure(octave_value_list &plhs, int num_of_feature, struct svm_model *model); struct svm_model *octave_matrix_to_model(octave_scalar_map &octave_struct, const char **error_message); statistics-release-2.1.2/src/svmpredict.cc000066400000000000000000000415701527311104300206060ustar00rootroot00000000000000/* Copyright (C) 2022 Andreas Bertsatos Copyright (C) 2025 Avanish Salunke Based on the Octave LIBSVM wrapper adapted by Alan Meeson (2014) based on an earlier version of the LIBSVM (3.18) library for MATLAB. Current implementation is based on LIBSVM 3.36 (2025) by Chih-Chung Chang and Chih-Jen Lin. This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include #include #include #include "svm.h" #include "svm_model_octave.h" #define CMD_LEN 2048 int print_null(const char *,...) {return 0;} int (*info)(const char *fmt,...) = &printf; void read_sparse_instance(const SparseMatrix &args, int index, struct svm_node *x) { int i, j, low, high; octave_idx_type *ir, *jc; double *samples; ir = (octave_idx_type*)args.ridx(); jc = (octave_idx_type*)args.cidx(); samples = (double*)args.data(); // each column is one instance j = 0; low = (int)jc[index], high = (int)jc[index+1]; for(i=low;iparam.kernel_type == PRECOMPUTED) { // precomputed kernel requires dense matrix, so we make one t_data = args(1).matrix_value(); } else { //If it's a sparse matrix with a non PRECOMPUTED kernel, transpose it pplhs = args(1).sparse_matrix_value().transpose(); } } else { t_data = args(1).matrix_value(); } ptr_instance = (double*)t_data.data(); if(predict_probability) { if(svm_type==NU_SVR || svm_type==EPSILON_SVR) { info("Prob. model for test data: target value = predicted value + z,\nz: Laplace distribution e^(-|z|/sigma)/(2sigma),sigma=%g\n", svm_get_svr_probability(model)); } else { prob_estimates = (double*) malloc(nr_class*sizeof(double)); } } ColumnVector cv_predictions(testing_instance_number); Matrix m_pe; if(predict_probability) { // prob estimates are in plhs[2] if(svm_type == C_SVC || svm_type == NU_SVC || svm_type == ONE_CLASS) { m_pe = Matrix(testing_instance_number, nr_class); } else { m_pe = Matrix(0,0); } } else { // decision values are in plhs[2] if(svm_type == ONE_CLASS || svm_type == EPSILON_SVR || svm_type == NU_SVR || nr_class == 1) { m_pe = Matrix(testing_instance_number, 1); } else { m_pe = Matrix(testing_instance_number, nr_class*(nr_class-1)/2); } } // Write through the local arrays, not through a pointer taken from an // octave_value. tplhs(N).column_vector_value() and .matrix_value() return // by value, and Octave stores a 1x1 array as a scalar, so for a single // testing instance they have to materialise a fresh array instead of // sharing one. data() then pointed into a temporary that was freed before // the loop below wrote to it, corrupting the heap. The results are moved // into tplhs once the loop is done. ptr_predict_label = cv_predictions.fortran_vec(); ptr_prob_estimates = m_pe.fortran_vec(); ptr_dec_values = m_pe.fortran_vec(); x = (struct svm_node*)malloc((feature_number+1)*sizeof(struct svm_node)); for(instance_index=0;instance_indexparam.kernel_type != PRECOMPUTED) { read_sparse_instance(pplhs, instance_index, x); } else { for(i = 0; i < feature_number; i++) { x[i].index = i+1; x[i].value = ptr_instance[testing_instance_number*i+instance_index]; } x[feature_number].index = -1; } if(predict_probability) { if(svm_type == C_SVC || svm_type == NU_SVC || svm_type == ONE_CLASS) { predict_label = svm_predict_probability(model, x, prob_estimates); ptr_predict_label[instance_index] = predict_label; for(i = 0; i < nr_class; i++) { ptr_prob_estimates[instance_index + i * testing_instance_number] = prob_estimates[i]; } } else { predict_label = svm_predict(model,x); ptr_predict_label[instance_index] = predict_label; } } else { if(svm_type == ONE_CLASS || svm_type == EPSILON_SVR || svm_type == NU_SVR) { double res; predict_label = svm_predict_values(model, x, &res); ptr_dec_values[instance_index] = res; } else { double *dec_values = (double *) malloc(sizeof(double) * nr_class*(nr_class-1)/2); predict_label = svm_predict_values(model, x, dec_values); if(nr_class == 1) { ptr_dec_values[instance_index] = 1; } else { for(i = 0; i < (nr_class * (nr_class - 1)) / 2; i++) { ptr_dec_values[instance_index + i * testing_instance_number] = dec_values[i]; } } free(dec_values); } ptr_predict_label[instance_index] = predict_label; } if(predict_label == target_label) { ++correct; } error += (predict_label-target_label)*(predict_label-target_label); sump += predict_label; sumt += target_label; sumpp += predict_label*predict_label; sumtt += target_label*target_label; sumpt += predict_label*target_label; ++total; } tplhs(0) = cv_predictions; tplhs(2) = m_pe; if(svm_type==NU_SVR || svm_type==EPSILON_SVR) { info("Mean squared error = %g (regression)\n",error/total); info("Squared correlation coefficient = %g (regression)\n", ((total*sumpt-sump*sumt)*(total*sumpt-sump*sumt))/ ((total*sumpp-sump*sump)*(total*sumtt-sumt*sumt))); } else { info("Accuracy = %g%% (%d/%d) (classification)\n", (double)correct/total*100,correct,total); } // return accuracy, mean squared error, squared correlation coefficient ColumnVector cv_acc(3); ptr = cv_acc.fortran_vec(); ptr[0] = (double)correct/total*100; ptr[1] = error/total; ptr[2] = ((total*sumpt-sump*sumt)*(total*sumpt-sump*sumt))/ ((total*sumpp-sump*sump)*(total*sumtt-sumt*sumt)); tplhs(1) = cv_acc; free(x); if(prob_estimates != NULL) { free(prob_estimates); } switch(nlhs) { case 3: plhs(2) = tplhs(2); plhs(1) = tplhs(1); [[fallthrough]]; case 1: case 0: plhs(0) = tplhs(0); } } DEFUN_DLD (svmpredict, args, nargout, "-*- texinfo -*- \n\n\ @deftypefn {statistics} {@var{predicted_label} =} svmpredict (@var{labels}, @var{data}, @var{model})\n\ @deftypefnx {statistics} {@var{predicted_label} =} svmpredict (@var{labels}, @var{data}, @var{model}, \"libsvm_options\")\n\ @deftypefnx {statistics} {[@var{predicted_label}, @var{accuracy}, @var{decision_values}] =} svmpredict (@var{labels}, @var{data}, @var{model}, \"libsvm_options\")\n\ @deftypefnx {statistics} {[@var{predicted_label}, @var{accuracy}, @var{prob_estimates}] =} svmpredict (@var{labels}, @var{data}, @var{model}, \"libsvm_options\")\n\ \n\ \n\ This function predicts new labels from a testing instance matrix based on an \ SVM @var{model} created with @code{svmtrain}. \ \n\ \n\ @itemize \n\ @item @var{labels} : An m by 1 vector of prediction labels. If labels \ of test data are unknown, simply use any random values. (type must be double) \ \n\ \n\ @item @var{data} : An m by n matrix of m testing instances with n features. \ It can be dense or sparse. (type must be double) \ \n\ \n\ @item @var{model} : The output of @code{svmtrain} function. \ \n\ \n\ @item @code{libsvm_options} : A string of testing options in the same format \ as that of LIBSVM. \ \n\ \n\ @end itemize \ \n\ \n\ @code{libsvm_options} :\n\ \n\ @itemize \n\ @item @code{-b} : probability_estimates; whether to predict probability \ estimates.\n\ \n\ @end itemize \n\ @multitable @columnfractions 0.1 0.1 0.8 \n\ @item @tab 0 @tab return decision values. (default) \n\ \n\ @item @tab 1 @tab return probability estimates. \n\ \n\ @end multitable \n\ \n\ \n\ @itemize \n\ @item @code{-q} : quiet mode. (no outputs) \n\ \n\ @end itemize \n\ \n\ \n\ The @code{svmpredict} function has three outputs. The first one, \ @var{predicted_label}, is a vector of predicted labels. The second output, \ @var{accuracy}, is a vector including accuracy (for classification), mean \ squared error, and squared correlation coefficient (for regression). The \ third is a matrix containing decision values or probability estimates \ (if @code{-b 1}' is specified). If @math{k} is the number of classes in \ training data, for decision values, each row includes results of predicting \ @math{k(k-1)/2} binary-class SVMs. For classification, @math{k = 1} is a \ special case. Decision value +1 is returned for each testing instance, \ instead of an empty vector. For probabilities, each row contains @math{k} \ values indicating the probability that the testing instance is in each class. \ Note that the order of classes here is the same as @code{Label} field in the \ @var{model} structure. \ \n\ \n\ \n\ @emph{Note on LIBSVM 3.36 Update}: This implementation is based on LIBSVM 3.36 \ (2025) and now supports probability estimates for One-Class SVM (@code{-s 2}) \ when combined with the probability flag (@code{-b 1}). For One-Class SVM, \ the @var{prob_estimates} output is a single column vector containing the \ probability of the instance being an inlier. \n\ \n\ @end deftypefn") { int nlhs = nargout; int nrhs = args.length(); octave_value_list plhs(nlhs); int prob_estimate_flag = 0; struct svm_model *model; info = &print_null; if(nlhs == 2 || nlhs > 3) { error ("svmpredict: wrong number of output arguments."); } if(nrhs > 4 || nrhs < 3) { error ("svmpredict: wrong number of input arguments."); } if(!args(0).is_double_type() || !args(1).is_double_type()) { error ("svmpredict: label vector and instance matrix must be double."); } if(args(2).isstruct()) { // Set, because the reader below reports it when the model is // rejected and does not itself write one. const char *error_msg = "unrecognized model"; // parse options if(nrhs==4) { int i, argc = 1; char cmd[CMD_LEN], *argv[CMD_LEN/2]; // put options in argv[] strncpy(cmd, args(3).string_value().c_str(), CMD_LEN - 1); cmd[CMD_LEN - 1] = '\0'; if((argv[argc] = strtok(cmd, " ")) != NULL) { while((argv[++argc] = strtok(NULL, " ")) != NULL); } for(i=1;i=argc) && argv[i-1][1] != 'q') { fake_answer(nlhs, plhs); return plhs; } switch(argv[i-1][1]) { case 'b': prob_estimate_flag = atoi(argv[i]); break; case 'q': i--; info = &print_null; break; default: printf("svmpredict: unknown option: -%c\n", argv[i-1][1]); fake_answer(nlhs, plhs); return plhs; } } } octave_scalar_map osm_model = args(2).scalar_map_value(); model = octave_matrix_to_model(osm_model, &error_msg); if (model == NULL) { printf("svmpredict: can't read model: %s\n", error_msg); fake_answer(nlhs, plhs); return plhs; } if(prob_estimate_flag) { // Check if the SVM type supports probability, new support for ONE_CLASS if (model->param.svm_type != C_SVC && model->param.svm_type != NU_SVC && model->param.svm_type != ONE_CLASS) { svm_free_and_destroy_model(&model); error ("svmpredict: probability estimates are not supported for this SVM type (only C-SVC, NU-SVC, and ONE-CLASS).\n"); } // Check if the model itself was trained with probability info (-b 1) if(svm_check_probability_model(model)==0) { svm_free_and_destroy_model(&model); error ("svmpredict: model does not support probability estimates. Train with '-b 1'.\n"); } } else { if(svm_check_probability_model(model)!=0) info("Model supports probability estimates, but disabled in prediction.\n"); } predict(nlhs, plhs, args, model, prob_estimate_flag); // destroy model svm_free_and_destroy_model(&model); } else { error ("svmpredict: model should be a struct array."); } return plhs; } /* %!test %! # Test 1: Standard C-SVC Prediction (Original Regression Test) %! [L, D] = libsvmread (file_in_loadpath ("heart_scale.dat")); %! model = svmtrain (L, D, '-c 1 -g 0.07'); %! [predict_label, accuracy, dec_values] = svmpredict (L, D, model); %! assert_equal (size (predict_label), size (dec_values)); %! assert_equal (accuracy, [86.666, 0.533, 0.533]', [1e-3, 1e-3, 1e-3]'); %! assert_equal (dec_values(1), 1.225836001973273, 1e-14); %! assert_equal (dec_values(2), -0.3212992933043805, 1e-14); %! assert_equal (predict_label(1), 1); %! %!test %! # A single testing instance used to write through a freed pointer, since %! # Octave stores a 1x1 result as a scalar and matrix_value() then returns a %! # temporary. Every single-row query must match the batch answer. %! [L, D] = libsvmread (file_in_loadpath ("heart_scale.dat")); %! model = svmtrain (L, D, '-c 1 -g 0.07'); %! [bl, ~, bd] = svmpredict (L, D, model); %! for i = [1, 2, 7, 130, numel(L)] %! [l, ~, d] = svmpredict (L(i), D(i,:), model); %! assert_equal (size (l), [1, 1]); %! assert_equal (l, bl(i)); %! assert_equal (d, bd(i), 1e-12); %! endfor %! %!test %! # Test 2: One-Class Probability (NEW LIBSVM 3.36 FEATURE) %! [L, D] = libsvmread (file_in_loadpath ("heart_scale.dat")); %! # Train One-Class (-s 2) with Probability (-b 1) %! model_oc = svmtrain (L, D, '-s 2 -n 0.1 -g 0.07 -b 1'); %! assert_equal (isstruct (model_oc), true); %! # Predict with Probability (-b 1) %! [pred, acc, probs] = svmpredict (L, D, model_oc, '-b 1'); %! %! # Detail Check A: Output must be N x 2 (Column 1: Normal, Column 2: Outlier) %! assert_equal (size (probs), [length(L), 2]); %! %! # Detail Check B: Probabilities must sum to 1.0 for every instance %! assert_equal (sum (probs, 2), ones (length(L), 1), 1e-5); %! %! # Detail Check C: Values must be valid probabilities [0, 1] %! assert_equal (all (probs >= 0 & probs <= 1, 'all'), true); %! clear model_oc %! %!test %! # Test 3: One-Class Decision Values (Standard Check) %! # Verifies that the upgrade didn't break standard One-Class prediction (-b 0) %! [L, D] = libsvmread (file_in_loadpath ("heart_scale.dat")); %! model_oc = svmtrain (L, D, '-s 2 -n 0.1 -g 0.07'); %! [pred, acc, dec] = svmpredict (L, D, model_oc); %! # Standard One-Class output is N x 1 (Scalar decision values) %! assert_equal (size (dec), [length(L), 1]); %! clear model_oc %! %!shared L, D, model %! # Test 4: Error Handling (Original Checks) %! [L, D] = libsvmread (file_in_loadpath ("heart_scale.dat")); %! model = svmtrain (L, D, '-c 1 -g 0.07'); %! %!error ... %! [p, a] = svmpredict (L, D, model); %!error p = svmpredict (L, D); %!error ... %! p = svmpredict (single (L), D, model); %!error p = svmpredict (L, D, 123); */ statistics-release-2.1.2/src/svmtrain.cc000066400000000000000000000520001527311104300202570ustar00rootroot00000000000000/* Copyright (C) 2025 Andreas Bertsatos Copyright (C) 2025 Avanish Salunke Based on the Octave LIBSVM wrapper adapted by Alan Meeson (2014) based on an earlier version of the LIBSVM (3.18) library for MATLAB. Current implementation is based on LIBSVM 3.36 (2025) by Chih-Chung Chang and Chih-Jen Lin. This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include #include #include #include "svm.h" #include "svm_model_octave.h" #define CMD_LEN 2048 #define Malloc(type,n) (type *)malloc((n)*sizeof(type)) void print_null(const char *) {} //void print_string_octave(const char *s) {&printf;} int print_null_(const char *,...) {return 0;} int (*info_)(const char *fmt,...) = &printf; // svm arguments struct svm_parameter param; // set by parse_command_line struct svm_problem prob; // set by read_problem struct svm_model *model; struct svm_node *x_space; int cross_validation; int nr_fold; double do_cross_validation() { int i; int total_correct = 0; double total_error = 0; double sumv = 0, sumy = 0, sumvv = 0, sumyy = 0, sumvy = 0; double *target = Malloc(double,prob.l); double retval = 0.0; svm_cross_validation(&prob,¶m,nr_fold,target); if(param.svm_type == EPSILON_SVR || param.svm_type == NU_SVR) { for(i=0;i 2) { // put options in argv[] strncpy(cmd, args(2).string_value().c_str(), CMD_LEN - 1); cmd[CMD_LEN - 1] = '\0'; //mxGetString(args[2], cmd, mxGetN(args[2]) + 1); if((argv[argc] = strtok(cmd, " ")) != NULL) while((argv[++argc] = strtok(NULL, " ")) != NULL) ; } // parse options for(i=1;i=argc && argv[i-1][1] != 'q') // since option -q has no parameter return 1; switch(argv[i-1][1]) { case 's': param.svm_type = atoi(argv[i]); break; case 't': param.kernel_type = atoi(argv[i]); break; case 'd': param.degree = atoi(argv[i]); break; case 'g': param.gamma = atof(argv[i]); break; case 'r': param.coef0 = atof(argv[i]); break; case 'n': param.nu = atof(argv[i]); break; case 'm': param.cache_size = atof(argv[i]); break; case 'c': param.C = atof(argv[i]); break; case 'e': param.eps = atof(argv[i]); break; case 'p': param.p = atof(argv[i]); break; case 'h': param.shrinking = atoi(argv[i]); break; case 'b': param.probability = atoi(argv[i]); break; case 'q': print_func = &print_null; info_ = &print_null_; i--; break; case 'v': cross_validation = 1; nr_fold = atoi(argv[i]); if(nr_fold < 2) { printf("n-fold cross validation: n must >= 2\n"); return 1; } break; case 'w': ++param.nr_weight; param.weight_label = (int *)realloc(param.weight_label,sizeof(int)*param.nr_weight); param.weight = (double *)realloc(param.weight,sizeof(double)*param.nr_weight); param.weight_label[param.nr_weight-1] = atoi(&argv[i-1][2]); param.weight[param.nr_weight-1] = atof(argv[i]); break; default: printf("svmtrain: unknown option -%c\n", argv[i-1][1]); return 1; } } svm_set_print_string_function(print_func); return 0; } // read in a problem (in svmlight format) int read_problem_dense(ColumnVector &label_vec, Matrix &instance_mat) { int i, j, k; int elements, max_index, sc, label_vector_row_num; double *samples, *labels; prob.x = NULL; prob.y = NULL; x_space = NULL; labels = (double*)label_vec.data();//mxGetPr(label_vec); samples = (double*)instance_mat.data(); sc = (int)instance_mat.cols(); elements = 0; // the number of instance prob.l = (int)instance_mat.rows(); label_vector_row_num = (int)label_vec.rows(); if(label_vector_row_num!=prob.l) { //must throw an error to pass the BIST test error("svmtrain: label vector must have same number of elements as rows in instance matrix."); return -1; } if(param.kernel_type == PRECOMPUTED) elements = prob.l * (sc + 1); else { for(i = 0; i < prob.l; i++) { for(k = 0; k < sc; k++) if(samples[k * prob.l + i] != 0) elements++; // count the '-1' element elements++; } } prob.y = Malloc(double,prob.l); prob.x = Malloc(struct svm_node *,prob.l); x_space = Malloc(struct svm_node, elements); max_index = sc; j = 0; for(i = 0; i < prob.l; i++) { prob.x[i] = &x_space[j]; prob.y[i] = labels[i]; for(k = 0; k < sc; k++) { if(param.kernel_type == PRECOMPUTED || samples[k * prob.l + i] != 0) { x_space[j].index = k + 1; x_space[j].value = samples[k * prob.l + i]; j++; } } x_space[j++].index = -1; } if(param.gamma == 0 && max_index > 0) param.gamma = 1.0/max_index; if(param.kernel_type == PRECOMPUTED) for(i=0;i max_index) { printf("svmtrain: wrong input format: sample_serial_number out of range\n"); return -1; } } return 0; } int read_problem_sparse(ColumnVector &label_vec, SparseMatrix &instance_mat) { int i, j, k, low, high; octave_idx_type *ir, *jc; int elements, max_index, num_samples, label_vector_row_num; double *samples, *labels; // transposed instance sparse matrix SparseMatrix instance_mat_col = instance_mat.transpose(); prob.x = NULL; prob.y = NULL; x_space = NULL; // each column is one instance labels = (double*)label_vec.data(); samples = (double*)instance_mat_col.data(); ir = (octave_idx_type*)instance_mat_col.ridx(); jc = (octave_idx_type*)instance_mat_col.cidx(); num_samples = (int)instance_mat_col.nzmax(); // the number of instance prob.l = (int)instance_mat_col.cols(); label_vector_row_num = (int)label_vec.rows(); if(label_vector_row_num!=prob.l) { // must throw an error to pass the BIST test error("svmtrain: label vector must have same number of elements as rows in instance matrix."); return -1; } elements = num_samples + prob.l; max_index = (int)instance_mat_col.rows(); prob.y = Malloc(double,prob.l); prob.x = Malloc(struct svm_node *,prob.l); x_space = Malloc(struct svm_node, elements); j = 0; for(i=0;i 0) { param.gamma = 1.0/max_index; } return 0; } static void fake_answer(int nlhs, octave_value_list &plhs) { int i; for(i=0;i 2) { error ("svmtrain: wrong number of output arguments."); } // Transform the input Matrix to libsvm format if(nrhs > 1 && nrhs < 5) { int err; if(!args(0).is_double_type() || !args(1).is_double_type()) { error ("svmtrain: label vector and instance matrix must be double."); } if(parse_command_line(nrhs, args, NULL)) { svm_destroy_param(¶m); error ("svmtrain: wrong values in parameter string."); } if(args(1).issparse()) { if(param.kernel_type == PRECOMPUTED) { // precomputed kernel requires dense matrix, so we make one ColumnVector cv_lab = args(0).column_vector_value(); Matrix m_dat = args(1).matrix_value(); err = read_problem_dense(cv_lab, m_dat); } else { ColumnVector cv_lab = args(0).column_vector_value(); SparseMatrix m_dat = args(1).sparse_matrix_value(); err = read_problem_sparse(cv_lab, m_dat); } } else { ColumnVector cv_lab = args(0).column_vector_value(); Matrix m_dat = args(1).matrix_value(); err = read_problem_dense(cv_lab, m_dat); } // One weight per instance, 1 unless a fourth argument gives them. // Each scales the instance's box constraint C. prob.W = NULL; if(! err) { if(nrhs == 4) { if(! args(3).is_double_type() || args(3).iscomplex() || args(3).numel() != prob.l) { svm_destroy_param(¶m); free(prob.y); free(prob.x); free(x_space); error ("svmtrain: weights must be a real double vector with " "one element per instance."); } NDArray wv = args(3).array_value(); for(int wi = 0; wi < prob.l; wi++) { if(! (wv(wi) >= 0) || octave::math::isinf (wv(wi))) { svm_destroy_param(¶m); free(prob.y); free(prob.x); free(x_space); error ("svmtrain: weights must be finite and nonnegative."); } } prob.W = Malloc(double,prob.l); for(int wi = 0; wi < prob.l; wi++) prob.W[wi] = wv(wi); } else { prob.W = Malloc(double,prob.l); for(int wi = 0; wi < prob.l; wi++) prob.W[wi] = 1.0; } } // svmtrain's original code error_msg = svm_check_parameter(&prob, ¶m); if(err || error_msg) { if (error_msg != NULL) { printf("svmtrain: %s\n", error_msg); } svm_destroy_param(¶m); free(prob.y); free(prob.x); free(prob.W); free(x_space); fake_answer(nlhs, plhs); return plhs; } svm_reset_max_iter_count(); if(cross_validation) { double ptr = do_cross_validation(); plhs(0) = octave_value(ptr); } else { int nr_feat = (int)args(1).matrix_value().cols(); const char *error_msg; model = svm_train(&prob, ¶m); error_msg = model_to_octave_structure(plhs, nr_feat, model); if(error_msg) { printf("svmtrain: can't convert libsvm model to matrix structure: %s\n", error_msg); } svm_free_and_destroy_model(&model); } // False when any solver run stopped at its iteration limit. if(nlhs > 1) plhs(1) = octave_value(svm_get_max_iter_count() == 0); svm_destroy_param(¶m); free(prob.y); free(prob.x); free(prob.W); free(x_space); return plhs; } else { error ("svmtrain: wrong number of input arguments."); } } /* %!test %! # Test 1: Basic C-SVC Classification and Model Structure %! [L, D] = libsvmread (file_in_loadpath ("heart_scale.dat")); %! model = svmtrain(L, D, '-c 1 -g 0.07'); %! [predict_label, accuracy, dec_values] = svmpredict(L, D, model); %! %! assert_equal (isstruct (model), true); %! assert_equal (isfield (model, "Parameters"), true); %! assert_equal (model.totalSV, 130); %! assert_equal (model.nr_class, 2); %! assert_equal (size (model.Label), [2, 1]); %! %! # Check prediction output sizes %! assert_equal (size (predict_label), [length(L), 1]); %! assert_equal (size (dec_values), [length(L), 1]); %! %! %! # Test 2: One-Class SVM Model Structure Check %! # Ensures training with -s 2 is functional and the model structure is valid (accommodating 3.36 changes). %! model_oc = svmtrain(L, D, '-s 2 -n 0.5 -g 0.07'); %! assert_equal (isstruct (model_oc), true); %! assert_equal (model_oc.Parameters(1), 2); # Check svm_type is ONE_CLASS %! assert_equal (model_oc.nr_class, 2); %! assert_equal (model_oc.totalSV > 0, true); %! clear model_oc %! %! %! # Test 3: Regression SVR Test %! # Check training of Epsilon SVR (-s 3) %! model_svr = svmtrain (L, D, '-s 3 -p 0.1 -c 10'); %! assert_equal (isstruct (model_svr), true); %! assert_equal (model_svr.Parameters(1), 3); # Check svm_type is EPSILON_SVR %! assert_equal (model_svr.nr_class, 2); %! clear model_svr %! %! %! # Test 4: Input Argument Error Checking %!shared L, D %! [L, D] = libsvmread (file_in_loadpath ("heart_scale.dat")); %! %! # Check argument count errors %!error [L, D, C] = svmtrain (L, D); %!error model = svmtrain (L, D, "", ones (270, 1), 1); %! %! # Check argument type errors %!error ... %! model = svmtrain (single (L), D); %! %! # Check dimension mismatch error %!error ... %! model = svmtrain (L(1:end-1), D); %! %!test %! ## Instance weights scale the box constraint: unit weights change nothing, %! ## and weights of 2 are the same fit as doubling C. %! [L, D] = libsvmread (file_in_loadpath ("heart_scale.dat")); %! m1 = svmtrain (L, D, '-t 0 -c 1 -q'); %! m2 = svmtrain (L, D, '-t 0 -c 1 -q', ones (270, 1)); %! assert_equal (m2.sv_coef, m1.sv_coef); %! assert_equal (m2.rho, m1.rho); %! m3 = svmtrain (L, D, '-t 0 -c 2 -q'); %! m4 = svmtrain (L, D, '-t 0 -c 1 -q', 2 * ones (270, 1)); %! assert_equal (m4.sv_coef, m3.sv_coef, 1e-12); %! assert_equal (m4.rho, m3.rho, 1e-12); %!error ... %! svmtrain ([1; -1; 1; -1], [1; 2; 3; 4], "-q", [1, 1]) %!error ... %! svmtrain ([1; -1; 1; -1], [1; 2; 3; 4], "-q", [1; -1; 1; 1]) %!test %! [L, D] = libsvmread (file_in_loadpath ("heart_scale.dat")); %! [~, converged] = svmtrain (L, D, '-c 1 -g 0.07 -q'); %! assert_equal (converged, true); %!test %! x = (1:20)' * 1000; %! [~, converged] = svmtrain (x / 100, x, '-s 3 -t 1 -g 1 -r 0 -c 1 -q'); %! assert_equal (converged, false); %! # Test 5: One-Class Probability Training (New LIBSVM 3.36 Feature) %! # This ensures svmtrain DOES NOT reject -s 2 combined with -b 1 %! # and correctly populates the new ProbDensityMarks field. %!test %! [L, D] = libsvmread (file_in_loadpath ("heart_scale.dat")); %! model = svmtrain (L, D, '-s 2 -n 0.1 -g 0.07 -b 1'); %! %! assert_equal (isstruct (model), true); %! assert_equal (model.Parameters(1), 2); # Check svm_type is ONE_CLASS %! # CRITICAL CHECK: Verify the new field exists (Specific to upgrade) %! assert_equal (isfield (model, "ProbDensityMarks"), true); %! clear model */ statistics-release-2.1.2/src/tree.cpp000066400000000000000000001333251527311104300175620ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include #include #include // Growing a binary decision tree by recursive partitioning. // // This unit is #included by treetrain.cc and treepredict.cc, each of which // calls a different subset of it, so a helper unused in one is used in // another. Hence [[maybe_unused]] on those definitions: they are shared, // not dead, and removing one breaks a sibling oct-file. // // The cost of a tree is the split search, and the search is a sort. Sorting // each node's rows afresh for every predictor is what a direct reading of the // algorithm gives, and it repeats the same comparisons at every level: the // order of a node's rows along a predictor is the order its parent already // had, with the rows that went the other way removed. // // So the rows are sorted once per predictor at the root, and every split // partitions those orders in place, stably, into the two children. A node // then owns one contiguous range of each predictor's index array, already in // order, and a level costs a pass over the data rather than a sort of it. // // Within a node the search is a single sweep per predictor. Class weights // accumulate from the left, so both sides of every candidate cut are known // from one running total, and no cut is evaluated twice. // // Ties are broken exactly as a sweep in this order finds them: the first // predictor, and within it the lowest cut point, that strictly beats what is // held. The arithmetic below is written in the order the reference // implementation performs it, because a tie decided by a last-bit difference // would move a split. // Two splits can be equally good and be computed as differing in the last // bits, because each predictor accumulates the same weights in its own sort // order. Left to decide the winner, that noise makes the tree depend on the // compiler rather than on the data. Splits within this much of each other, // relatively, are therefore held to be equal and the earlier one is kept. // // Measured on eight thousand rows against MATLAB R2024a: every one of its // 1396 splits is within 1.2e-14 of our best, so the noise is of that order, // while the tolerance may be raised to 1e-6 before the grown tree changes at // all. 1e-12 sits a hundredfold above the noise and far below any gain // difference that carries meaning. static const double GAIN_TIE_TOL = 1e-12; // One node of the tree under construction. struct Node { octave_idx_type left, right, parent; octave_idx_type cutvar; // 0 when the node is a leaf double cutval; octave_idx_type start, stop; // its range in every sorted order octave_idx_type nsize; double nweight; std::vector catleft; // the levels sent left, sorted, when std::vector catright; // the cut is on a categorical predictor }; // The tree grows the same way whichever it is fitting. Only what a node // accumulates, how impure that makes it and what a leaf then answers with // differ, so those three are the only things that branch on the type. enum Criterion { GDI, DEVIANCE, MSE }; // A regression node carries the weight, the weighted sum of the response and // its weighted sum of squares, which give the mean and the sum of squared // error about it in closed form. struct Moments { double w, wy, wyy; void clear (void) { w = wy = wyy = 0.0; } void add (double wi, double yi) { w += wi; wy += wi * yi; wyy += wi * yi * yi; } double mean (void) const { return (w > 0.0) ? wy / w : 0.0; } // Sum of squared error about the node mean. Never let rounding take it // below zero, which a node holding one distinct value can do. double sse (void) const { if (w <= 0.0) return 0.0; double v = wyy - wy * wy / w; return (v > 0.0) ? v : 0.0; } }; static inline double impurity (const std::vector& cw, double total, octave_idx_type K, Criterion crit) { double imp = 0.0; if (crit == GDI) { for (octave_idx_type k = 0; k < K; k++) { double pr = cw[k] / total; imp += pr * pr; } return 1.0 - imp; } for (octave_idx_type k = 0; k < K; k++) { double pr = cw[k] / total; if (pr > 0.0) imp -= pr * std::log (pr); } return imp; } // How a node with more than two classes splits a categorical predictor: the // exact search, one of the three heuristics MATLAB names, or its automatic // choice between them. enum CatAlg { CAT_AUTO, CAT_EXACT, CAT_PULLLEFT, CAT_PCA, CAT_OVA }; // Everything the caller settles before the engine starts. A classifier and a // regression differ here in three fields and nowhere else. struct TreeOpts { octave_idx_type K, minparent, minleaf, maxsplits; bool mergeleaves; double qetoler; Criterion crit; octave_idx_type nvars; // predictors tried per node, 0 for all std::uint32_t seed; // seeds the draw when nvars is set std::vector iscat; // per predictor, empty when none is double maxcat; // levels an automatic exact search takes CatAlg catalg; }; // One value in 0 .. range-1, every value equally likely. The raw output of // std::mt19937 is fixed by the standard where the distributions built on it // are not, so drawing from it directly keeps a seeded tree the same under // every C++ library, and rejecting the low values a bare modulus would favour // keeps the draw unbiased. static octave_idx_type draw_below (std::mt19937& rng, octave_idx_type range) { const std::uint32_t r = static_cast (range); const std::uint32_t reject = (0u - r) % r; std::uint32_t x; do x = static_cast (rng ()); while (x < reject); return static_cast (x % r); } // The eigenvector of the largest eigenvalue of the symmetric K by K matrix A, // by cyclic Jacobi rotations, signed so that its largest entry in magnitude // is positive. static std::vector leading_eigenvector (std::vector A, octave_idx_type K) { std::vector V (K * K, 0.0); for (octave_idx_type i = 0; i < K; i++) V[i * K + i] = 1.0; for (int sweep = 0; sweep < 100; sweep++) { double off = 0.0; for (octave_idx_type p = 0; p < K; p++) for (octave_idx_type q = p + 1; q < K; q++) off += A[p * K + q] * A[p * K + q]; if (off < 1e-30) break; for (octave_idx_type p = 0; p < K; p++) for (octave_idx_type q = p + 1; q < K; q++) { const double apq = A[p * K + q]; if (std::fabs (apq) < 1e-300) continue; const double theta = (A[q * K + q] - A[p * K + p]) / (2.0 * apq); const double t = ((theta >= 0.0) ? 1.0 : -1.0) / (std::fabs (theta) + std::sqrt (theta * theta + 1.0)); const double c = 1.0 / std::sqrt (t * t + 1.0); const double sn = t * c; for (octave_idx_type k = 0; k < K; k++) { const double akp = A[k * K + p]; const double akq = A[k * K + q]; A[k * K + p] = c * akp - sn * akq; A[k * K + q] = sn * akp + c * akq; } for (octave_idx_type k = 0; k < K; k++) { const double apk = A[p * K + k]; const double aqk = A[q * K + k]; A[p * K + k] = c * apk - sn * aqk; A[q * K + k] = sn * apk + c * aqk; } for (octave_idx_type k = 0; k < K; k++) { const double vkp = V[k * K + p]; const double vkq = V[k * K + q]; V[k * K + p] = c * vkp - sn * vkq; V[k * K + q] = sn * vkp + c * vkq; } } } octave_idx_type top = 0; for (octave_idx_type i = 1; i < K; i++) if (A[i * K + i] > A[top * K + top]) top = i; std::vector v (K); octave_idx_type big = 0; for (octave_idx_type k = 0; k < K; k++) { v[k] = V[k * K + top]; if (std::fabs (v[k]) > std::fabs (v[big])) big = k; } if (v[big] < 0.0) for (octave_idx_type k = 0; k < K; k++) v[k] = -v[k]; return v; } // The best split of a node's rows on a categorical predictor, as its gain per // unit of the node's weight, with the levels sent each way. The rows // [start, have) of the predictor's order hold its known values, sorted, so // the rows of one level are contiguous. // // A regression orders the levels by their mean response and two classes by // the probability of the first class, and either takes the best of the L - 1 // cuts of that order, which is the exact search. More classes search every // partition when the node holds at most MaxNumCategories levels, the lowest // level always on the left and ties kept by the first partition in the order // the other levels' membership of the right counts up in binary. Above that // the automatic choice keeps the best of OVAbyClass, PCA and PullLeft for up // to four classes and of PCA and PullLeft for more, the earlier on a tie, as // measured on MATLAB R2024a; the heuristics follow MATLAB's descriptions of // them. A regression keeps the lower means on the left; every classifier // split from an order puts the lowest level on the left. static double categorical_split (const double *xj, const octave_idx_type *oj, octave_idx_type start, octave_idx_type have, const std::vector& y, const std::vector& resp, const std::vector& w, octave_idx_type K, Criterion crit, bool isreg, const std::vector& present, const Moments& presmom, double wp, double presentimp, double total, const TreeOpts& o, std::vector& catleft, std::vector& catright) { catleft.clear (); catright.clear (); // Each level's value, row count, weight and class weights or moments std::vector lev, lw, lcw; std::vector cnt; std::vector lmom; for (octave_idx_type i = start; i < have; i++) { const octave_idx_type r = oj[i]; if (lev.empty () || xj[r] != lev.back ()) { lev.push_back (xj[r]); cnt.push_back (0); lw.push_back (0.0); if (isreg) { Moments m; m.clear (); lmom.push_back (m); } else lcw.insert (lcw.end (), K, 0.0); } const std::size_t l = lev.size () - 1; cnt[l]++; lw[l] += w[r]; if (isreg) lmom[l].add (w[r], resp[r]); else lcw[l * K + y[r]] += w[r]; } const octave_idx_type L = static_cast (lev.size ()); if (L < 2) return 0.0; const octave_idx_type ntot = have - start; std::vector running (K), other (K); // The gain of sending the levels marked in inL left, and whether both // sides keep MinLeaf rows and some weight. auto gain_of = [&] (const std::vector& inL, bool& ok) -> double { octave_idx_type nl = 0; double wl = 0.0; Moments ml; ml.clear (); std::fill (running.begin (), running.end (), 0.0); for (octave_idx_type l = 0; l < L; l++) if (inL[l]) { nl += cnt[l]; wl += lw[l]; if (isreg) { ml.w += lmom[l].w; ml.wy += lmom[l].wy; ml.wyy += lmom[l].wyy; } else for (octave_idx_type k = 0; k < K; k++) running[k] += lcw[l * K + k]; } const double wr = wp - wl; ok = (nl >= o.minleaf && ntot - nl >= o.minleaf); if (! (wl > 0.0 && wr > 0.0)) { ok = false; return -std::numeric_limits::infinity (); } double impl, impr; if (isreg) { Moments rest; rest.w = presmom.w - ml.w; rest.wy = presmom.wy - ml.wy; rest.wyy = presmom.wyy - ml.wyy; impl = ml.sse () / wl; impr = rest.sse () / wr; } else { for (octave_idx_type k = 0; k < K; k++) other[k] = present[k] - running[k]; impl = impurity (running, wl, K, crit); impr = impurity (other, wr, K, crit); } return (wp * presentimp - wl * impl - wr * impr) / total; }; double best = 0.0; bool found = false; std::vector bestL; auto consider = [&] (const std::vector& inL) { bool ok; const double g = gain_of (inL, ok); if (ok && (! found || g > best + std::fabs (best) * GAIN_TIE_TOL)) { best = g; bestL = inL; found = true; } }; // The L - 1 cuts of the levels in ascending order of key, ties kept in // the order of the levels themselves auto scan_order = [&] (const std::vector& key) { std::vector ord (L); std::iota (ord.begin (), ord.end (), 0); std::stable_sort (ord.begin (), ord.end (), [&key] (octave_idx_type a, octave_idx_type b) { return key[a] < key[b]; }); std::vector inL (L, 0); for (octave_idx_type m = 0; m < L - 1; m++) { inL[ord[m]] = 1; consider (inL); } }; // The probability of class c at level l auto prob = [&] (octave_idx_type l, octave_idx_type c) -> double { return (lw[l] > 0.0) ? lcw[l * K + c] / lw[l] : 0.0; }; bool lowleft = ! isreg; if (isreg) { std::vector key (L); for (octave_idx_type l = 0; l < L; l++) key[l] = lmom[l].mean (); scan_order (key); } else if (K <= 2) { std::vector key (L); for (octave_idx_type l = 0; l < L; l++) key[l] = prob (l, 0); scan_order (key); } else if (o.catalg == CAT_EXACT || (o.catalg == CAT_AUTO && static_cast (L) <= o.maxcat)) { if (L > 31) error ("treetrain: an exact categorical search cannot take more " "than 31 levels."); std::vector inL (L, 1); const std::uint64_t stop = std::uint64_t (1) << (L - 1); for (std::uint64_t mask = 1; mask < stop; mask++) { for (octave_idx_type l = 1; l < L; l++) inL[l] = ! ((mask >> (l - 1)) & 1); consider (inL); } } else { lowleft = false; const bool automatic = (o.catalg == CAT_AUTO); if (o.catalg == CAT_OVA || (automatic && K <= 4)) for (octave_idx_type c = 0; c < K; c++) { std::vector key (L); for (octave_idx_type l = 0; l < L; l++) key[l] = -prob (l, c); scan_order (key); } if (o.catalg == CAT_PCA || automatic) { double ws = 0.0; std::vector mean (K, 0.0), C (K * K, 0.0); for (octave_idx_type l = 0; l < L; l++) { ws += lw[l]; for (octave_idx_type k = 0; k < K; k++) mean[k] += lw[l] * prob (l, k); } for (octave_idx_type k = 0; k < K; k++) mean[k] /= ws; for (octave_idx_type l = 0; l < L; l++) for (octave_idx_type a = 0; a < K; a++) for (octave_idx_type b = 0; b < K; b++) C[a * K + b] += lw[l] * (prob (l, a) - mean[a]) * (prob (l, b) - mean[b]) / ws; const std::vector v = leading_eigenvector (C, K); std::vector key (L, 0.0); for (octave_idx_type l = 0; l < L; l++) for (octave_idx_type k = 0; k < K; k++) key[l] += prob (l, k) * v[k]; scan_order (key); } if (o.catalg == CAT_PULLLEFT || automatic) { std::vector inL (L, 0); octave_idx_type nright = L; while (nright > 1) { std::vector cand; for (octave_idx_type c = 0; c < K; c++) { octave_idx_type top = -1; for (octave_idx_type l = 0; l < L; l++) if (! inL[l] && (top < 0 || prob (l, c) > prob (top, c))) top = l; if (std::find (cand.begin (), cand.end (), top) == cand.end ()) cand.push_back (top); } octave_idx_type pick = -1; double pg = 0.0; for (octave_idx_type x : cand) { inL[x] = 1; bool ok; const double g = gain_of (inL, ok); inL[x] = 0; if (pick < 0 || g > pg) { pick = x; pg = g; } } inL[pick] = 1; nright--; consider (inL); } } } if (! found) return 0.0; if (lowleft && ! bestL[0]) for (octave_idx_type l = 0; l < L; l++) bestL[l] = ! bestL[l]; for (octave_idx_type l = 0; l < L; l++) (bestL[l] ? catleft : catright).push_back (lev[l]); return best; } // Grow a tree and return it as one node table. Shared by treetrain, which // fits it, and used by treepredict through the descent below, so that the two // halves of the same rule cannot drift apart. [[maybe_unused]] static octave_scalar_map tree_build (const Matrix& X, const ColumnVector& yv, const ColumnVector& wv, const TreeOpts& o) { const octave_idx_type n = X.rows (); const octave_idx_type p = X.columns (); const octave_idx_type K = o.K; const octave_idx_type minparent = o.minparent; const octave_idx_type minleaf = o.minleaf; const octave_idx_type maxsplits = o.maxsplits; const bool mergeleaves = o.mergeleaves; const Criterion crit = o.crit; const double qetoler = o.qetoler; const bool isreg = (crit == MSE); // Sampling predictors per node grows the trees of a random forest. With // every predictor tried nothing is drawn, so such a tree is exactly the one // grown without sampling. const octave_idx_type nvars = (o.nvars > 0 && o.nvars < p) ? o.nvars : p; const bool sampling = (nvars < p); std::mt19937 rng (o.seed); std::vector cand (p); std::iota (cand.begin (), cand.end (), 0); if (! isreg && K < 1) error ("treetrain: NumClasses must be a positive integer."); // Class index of each row for a classifier, the response itself for a // regression, and the weight either way. std::vector y (isreg ? 0 : n); std::vector resp (isreg ? n : 0); std::vector w (n); for (octave_idx_type i = 0; i < n; i++) { if (isreg) resp[i] = yv(i); else { octave_idx_type k = static_cast (yv(i)) - 1; if (k < 0 || k >= K) error ("treetrain: Y holds a class index outside 1:K."); y[i] = k; } w[i] = wv(i); } // Row order along each predictor, sorted once and thereafter partitioned. // ord[j] holds the rows in ascending order of predictor j; a node owns the // slice [start, stop) of every one of them. // A row missing every predictor carries no information and is dropped, as // MATLAB drops it: it is absent from the root's size, not merely unsplit. std::vector keeprow; keeprow.reserve (n); for (octave_idx_type i = 0; i < n; i++) { bool allnan = true; for (octave_idx_type j = 0; j < p && allnan; j++) if (! std::isnan (X(i, j))) allnan = false; if (! allnan) keeprow.push_back (i); } const octave_idx_type ne = static_cast (keeprow.size ()); // Missing values sort to the end of every order, so a node's non-missing // rows are always a prefix of its range and stay one through a partition. // A bare less-than is not a strict weak ordering once NaN is in the data, // which std::sort is entitled to crash on, so the comparator is explicit. std::vector> ord (p); for (octave_idx_type j = 0; j < p; j++) { ord[j] = keeprow; const double *xj = X.data () + j * n; std::stable_sort (ord[j].begin (), ord[j].end (), [xj] (octave_idx_type a, octave_idx_type b) { bool na = std::isnan (xj[a]); bool nb = std::isnan (xj[b]); if (na != nb) return nb; if (na) return false; return xj[a] < xj[b]; }); } std::vector nodes; nodes.reserve (2 * ne + 2); Node root; root.left = root.right = root.cutvar = 0; root.parent = 0; root.cutval = std::numeric_limits::quiet_NaN (); root.start = 0; root.stop = ne; root.nsize = ne; root.nweight = 0.0; nodes.push_back (root); // Class weight of each node, laid out node major. std::vector cweight (K, 0.0); std::vector nodecw; nodecw.reserve ((2 * ne + 2) * K); // The unweighted count of each class, which is not the weight once the // weights are not all alike, and which the learner reports as ClassCount. std::vector nodecn; nodecn.reserve ((2 * ne + 2) * K); std::vector nodesse, nodemean; double rootsse = 0.0; std::vector running (K); // class weight left of a cut std::vector tally (K); // class weight of the whole node std::vector other (K); // and right of a cut std::vector present (K); // and of the rows a predictor has std::vector tallyn (K); // and their unweighted count std::vector side (n); std::vector buffer (n); std::vector held (n); octave_idx_type numsplits = 0; // The tree grows a layer at a time. Every node of a layer is searched // first, in index order, and only then are the splits made, so that when // the layer holds more splittable nodes than MaxNumSplits has left, the // ones left unsplit are those whose splits gain the least, as MATLAB // documents under tree depth control, and not simply the last ones. // Splits are made in index order, so a split's children still follow // every node of the layer, which is what makes the numbering breadth first. octave_idx_type lfirst = 0; while (lfirst < static_cast (nodes.size ())) { const octave_idx_type llast = static_cast (nodes.size ()); const octave_idx_type lsize = llast - lfirst; std::vector lvar (lsize, 0); std::vector lval (lsize, 0.0); std::vector lgain (lsize, 0.0); std::vector> lleft (lsize), lright (lsize); for (octave_idx_type idx = lfirst; idx < llast; idx++) { Node& nd = nodes[idx]; const octave_idx_type start = nd.start; const octave_idx_type stop = nd.stop; std::fill (tally.begin (), tally.end (), 0.0); std::fill (tallyn.begin (), tallyn.end (), 0.0); Moments nodemom; nodemom.clear (); double total = 0.0; for (octave_idx_type i = start; i < stop; i++) { octave_idx_type r = ord[0][i]; if (isreg) nodemom.add (w[r], resp[r]); else { tally[y[r]] += w[r]; tallyn[y[r]] += 1.0; } total += w[r]; } nd.nsize = stop - start; nd.nweight = total; if (isreg) { nodesse.push_back (nodemom.sse ()); nodemean.push_back (nodemom.mean ()); } else { nodecw.insert (nodecw.end (), tally.begin (), tally.end ()); nodecn.insert (nodecn.end (), tallyn.begin (), tallyn.end ()); } if (idx == 0 && isreg) rootsse = nodemom.sse (); // A node with nothing left to separate is a leaf. For a classifier // that is one class; for a regression it is one distinct response, // and QEToler holds it to a share of the whole tree's error rather // than to exactly zero. bool nothingtosplit; if (isreg) nothingtosplit = (nodemom.sse () <= qetoler * rootsse); else { octave_idx_type nonempty = 0; for (octave_idx_type k = 0; k < K; k++) if (tally[k] > 0.0) nonempty++; nothingtosplit = (nonempty < 2); } if (nd.nsize < minparent || numsplits >= maxsplits || nothingtosplit) continue; octave_idx_type bestvar = 0; double bestval = std::numeric_limits::quiet_NaN (); double bestgain = 0.0; std::vector bestleft, bestright, catl, catr; // A searched node tries a fresh subset of the predictors, drawn by a // partial shuffle and put back in index order, so a tie between two // predictors still goes to the lower index as it does without // sampling. A node whose subset holds no valid split is a leaf; no // second draw is made. if (sampling) { for (octave_idx_type k = 0; k < nvars; k++) std::swap (cand[k], cand[k + draw_below (rng, p - k)]); std::sort (cand.begin (), cand.begin () + nvars); } for (octave_idx_type c = 0; c < nvars; c++) { const octave_idx_type j = cand[c]; const double *xj = X.data () + j * n; const octave_idx_type *oj = &ord[j][0]; // The rows missing this predictor sit at the end of the range, so // the ones that can be split are the prefix before them. octave_idx_type have = stop; while (have > start && std::isnan (xj[oj[have - 1]])) have--; if (have - start < 2 * minleaf) continue; std::fill (present.begin (), present.end (), 0.0); Moments presmom, runmom; presmom.clear (); double wp = 0.0; for (octave_idx_type i = start; i < have; i++) { octave_idx_type r = oj[i]; if (isreg) presmom.add (w[r], resp[r]); else present[y[r]] += w[r]; wp += w[r]; } const double presentimp = isreg ? presmom.sse () / wp : impurity (present, wp, K, crit); // A categorical predictor is split into two sets of levels if (! o.iscat.empty () && o.iscat[j]) { const double g = categorical_split (xj, oj, start, have, y, resp, w, K, crit, isreg, present, presmom, wp, presentimp, total, o, catl, catr); if (! catl.empty () && g > bestgain + std::fabs (bestgain) * GAIN_TIE_TOL) { bestgain = g; bestvar = j + 1; bestval = std::numeric_limits::quiet_NaN (); bestleft = catl; bestright = catr; } continue; } std::fill (running.begin (), running.end (), 0.0); runmom.clear (); double wl = 0.0; // Position i splits after the i-th row of this node's order, so // it leaves i + 1 rows on the left. Only positions where the // value changes are cuts, and each side must keep MinLeaf rows. const octave_idx_type last = have - start - 1; for (octave_idx_type i = 0; i < last; i++) { octave_idx_type r = oj[start + i]; if (isreg) runmom.add (w[r], resp[r]); else running[y[r]] += w[r]; wl += w[r]; const octave_idx_type nleft = i + 1; if (nleft < minleaf || (have - start) - nleft < minleaf) continue; const double xa = xj[r]; const double xb = xj[oj[start + i + 1]]; if (! (xa < xb)) continue; const double wr = wp - wl; double impl, impr; if (isreg) { Moments rest; rest.w = presmom.w - runmom.w; rest.wy = presmom.wy - runmom.wy; rest.wyy = presmom.wyy - runmom.wyy; impl = runmom.sse () / wl; impr = rest.sse () / wr; } else { for (octave_idx_type k = 0; k < K; k++) other[k] = present[k] - running[k]; impl = impurity (running, wl, K, crit); impr = impurity (other, wr, K, crit); } // The rows that are missing this predictor cannot be placed // by it, so the split is judged on the rows it can place and // then scaled by their share of the node. Without that // scaling a predictor known for a handful of rows would beat // one known for all of them. const double gain = (wp * presentimp - wl * impl - wr * impr) / total; if (gain > bestgain + std::fabs (bestgain) * GAIN_TIE_TOL) { bestgain = gain; bestvar = j + 1; bestval = (xa + xb) / 2.0; bestleft.clear (); bestright.clear (); } } } if (bestvar == 0 || bestgain <= 0.0) continue; // The gain is per unit of the node's weight; weighted by it, it is // the reduction in the tree's risk, which is what ranks nodes of // different sizes against one another. lvar[idx - lfirst] = bestvar; lval[idx - lfirst] = bestval; lgain[idx - lfirst] = bestgain * total; lleft[idx - lfirst] = bestleft; lright[idx - lfirst] = bestright; } // Keep the most successful splits the budget allows. A stable sort // leaves the earlier node ahead on a tie. std::vector found; for (octave_idx_type i = 0; i < lsize; i++) if (lvar[i] != 0) found.push_back (i); const octave_idx_type budget = maxsplits - numsplits; if (static_cast (found.size ()) > budget) { std::stable_sort (found.begin (), found.end (), [&lgain] (octave_idx_type a, octave_idx_type b) { return lgain[a] > lgain[b]; }); for (std::size_t i = budget; i < found.size (); i++) lvar[found[i]] = 0; } for (octave_idx_type idx = lfirst; idx < llast; idx++) { if (lvar[idx - lfirst] == 0) continue; // Mark which side each row takes, then partition every predictor's // order in place, keeping the rows of each side in the order they // were. A row missing the chosen predictor cannot be placed on either // side, so it descends to neither child and is left in the tail of // the range, counted at this node and nowhere below it. Prediction // stops such a row at this node for the same reason. const octave_idx_type start = nodes[idx].start; const octave_idx_type stop = nodes[idx].stop; const octave_idx_type bestvar = lvar[idx - lfirst]; const double bestval = lval[idx - lfirst]; const std::vector& catleft = lleft[idx - lfirst]; const bool bycat = ! catleft.empty (); const double *xb = X.data () + (bestvar - 1) * n; octave_idx_type nleft = 0, nright = 0; for (octave_idx_type i = start; i < stop; i++) { octave_idx_type r = ord[0][i]; if (std::isnan (xb[r])) side[r] = 2; else if (bycat ? std::binary_search (catleft.begin (), catleft.end (), xb[r]) : (xb[r] < bestval)) { side[r] = 0; nleft++; } else { side[r] = 1; nright++; } } for (octave_idx_type j = 0; j < p; j++) { octave_idx_type a = 0, b = 0, c = 0; for (octave_idx_type i = start; i < stop; i++) { octave_idx_type r = ord[j][i]; if (side[r] == 0) ord[j][start + a++] = r; else if (side[r] == 1) buffer[b++] = r; else held[c++] = r; } for (octave_idx_type i = 0; i < b; i++) ord[j][start + a + i] = buffer[i]; for (octave_idx_type i = 0; i < c; i++) ord[j][start + a + b + i] = held[i]; } Node kid; kid.left = kid.right = kid.cutvar = 0; kid.cutval = std::numeric_limits::quiet_NaN (); kid.parent = idx + 1; kid.nsize = 0; kid.nweight = 0.0; nodes[idx].cutvar = bestvar; nodes[idx].cutval = bestval; nodes[idx].catleft = catleft; nodes[idx].catright = lright[idx - lfirst]; nodes[idx].left = static_cast (nodes.size ()) + 1; nodes[idx].right = static_cast (nodes.size ()) + 2; kid.start = start; kid.stop = start + nleft; nodes.push_back (kid); kid.start = start + nleft; kid.stop = start + nleft + nright; nodes.push_back (kid); numsplits++; } lfirst = llast; } octave_idx_type nn = static_cast (nodes.size ()); // Collapse a pair of leaves back into their parent when splitting it did // not lower the misclassification risk, deepest first so that a merge can // expose another. The risk is the weight the node's majority class does // not hold, which is the error criterion rather than the split criterion. // A node's class weight is the sum of its rows, and a parent's rows are its // two children's rows. Summed in those two different orders the totals // differ in the last bits, which is enough to decide the merge test below: // on eight thousand rows every pair that ought to merge missed by about // 1e-19, against an observation weight of 1.25e-4. Recomputing a branch's // totals from its children, deepest first, makes the two sides of that test // exact rather than nearly equal, and no tolerance is needed. // Only where a split placed every row is a parent the sum of its children, // and only there can its totals be recomputed from them. A node that held // rows back keeps its own, and the merge test below falls back to the form // that does not assume the two agree. std::vector whole (nn, false); for (octave_idx_type idx = nn - 1; idx >= 0; idx--) { octave_idx_type l = nodes[idx].left; if (l == 0) continue; octave_idx_type r = nodes[idx].right; if (nodes[idx].nsize != nodes[l-1].nsize + nodes[r-1].nsize) continue; whole[idx] = true; if (! isreg) for (octave_idx_type k = 0; k < K; k++) nodecw[idx * K + k] = nodecw[(l - 1) * K + k] + nodecw[(r - 1) * K + k]; nodes[idx].nweight = nodes[l-1].nweight + nodes[r-1].nweight; } std::vector keep; if (mergeleaves) { bool merged = true; while (merged) { merged = false; for (octave_idx_type idx = nn - 1; idx >= 0; idx--) { octave_idx_type l = nodes[idx].left; octave_idx_type r = nodes[idx].right; if (l == 0 || nodes[l-1].left != 0 || nodes[r-1].left != 0) continue; // Splitting is kept only when it lowers the misclassification // risk. Written as risks the two sides share the node weight, // and subtracting it loses the very digits the comparison turns // on; cancelled algebraically the test is over class weights // alone, and since the parent's are the sums of the children's // it is exact when it should be. bool collapse; if (isreg) { // Splitting can only lower the squared error, so a // regression merge is the degenerate case where it lowered // it by nothing at all. collapse = (nodesse[l-1] + nodesse[r-1] >= nodesse[idx]); } else { double pbest = 0.0, lbest = 0.0, rbest = 0.0; for (octave_idx_type k = 0; k < K; k++) { pbest = std::max (pbest, nodecw[idx * K + k]); lbest = std::max (lbest, nodecw[(l - 1) * K + k]); rbest = std::max (rbest, nodecw[(r - 1) * K + k]); } if (whole[idx]) { // The parent's best is the children's best summed // exactly when one class is the majority of both, so // that is what is tested, and no sum is formed. The // sum, held at extended precision in an x87 register, // missed the parent's rounded total on i386. collapse = false; for (octave_idx_type k = 0; k < K; k++) if (nodecw[(l - 1) * K + k] == lbest && nodecw[(r - 1) * K + k] == rbest) { collapse = true; break; } } else collapse = ((nodes[l-1].nweight - lbest) + (nodes[r-1].nweight - rbest) >= nodes[idx].nweight - pbest); } if (collapse) { nodes[idx].left = nodes[idx].right = 0; nodes[idx].cutvar = 0; nodes[idx].cutval = std::numeric_limits::quiet_NaN (); nodes[idx].catleft.clear (); nodes[idx].catright.clear (); merged = true; } } } } // Drop what a merge orphaned and renumber what is left, a parent keeping // its place ahead of its children. keep.assign (nn, false); keep[0] = true; for (octave_idx_type idx = 0; idx < nn; idx++) if (keep[idx] && nodes[idx].left != 0) { keep[nodes[idx].left - 1] = true; keep[nodes[idx].right - 1] = true; } std::vector newidx (nn, 0); octave_idx_type out = 0; for (octave_idx_type idx = 0; idx < nn; idx++) if (keep[idx]) newidx[idx] = ++out; Matrix children (out, 2, 0.0); ColumnVector parent (out, 0.0); ColumnVector cutvar (out, 0.0); ColumnVector cutval (out, 0.0); boolMatrix isbranch (out, 1, false); ColumnVector nodesize (out, 0.0); ColumnVector nodeweight (out, 0.0); Matrix classweight (out, isreg ? 0 : K, 0.0); Matrix classcount (out, isreg ? 0 : K, 0.0); ColumnVector nodemeanout (isreg ? out : 0, 0.0); ColumnVector nodeerror (isreg ? out : 0, 0.0); Cell cutcats (out, 2); for (octave_idx_type idx = 0; idx < nn; idx++) { if (! keep[idx]) continue; octave_idx_type i = newidx[idx] - 1; children(i, 0) = (nodes[idx].left == 0) ? 0 : newidx[nodes[idx].left - 1]; children(i, 1) = (nodes[idx].right == 0) ? 0 : newidx[nodes[idx].right - 1]; parent(i) = (nodes[idx].parent == 0) ? 0 : newidx[nodes[idx].parent - 1]; cutvar(i) = nodes[idx].cutvar; cutval(i) = (nodes[idx].cutvar == 0) ? std::numeric_limits::quiet_NaN () : nodes[idx].cutval; isbranch(i, 0) = (nodes[idx].cutvar != 0); cutcats(i, 0) = Matrix (); cutcats(i, 1) = Matrix (); if (nodes[idx].cutvar != 0 && ! nodes[idx].catleft.empty ()) { RowVector a (nodes[idx].catleft.size ()); RowVector b (nodes[idx].catright.size ()); for (std::size_t q = 0; q < nodes[idx].catleft.size (); q++) a(q) = nodes[idx].catleft[q]; for (std::size_t q = 0; q < nodes[idx].catright.size (); q++) b(q) = nodes[idx].catright[q]; cutcats(i, 0) = a; cutcats(i, 1) = b; } nodesize(i) = nodes[idx].nsize; nodeweight(i) = nodes[idx].nweight; if (isreg) { nodemeanout(i) = nodemean[idx]; nodeerror(i) = (nodes[idx].nweight > 0.0) ? nodesse[idx] / nodes[idx].nweight : 0.0; } else for (octave_idx_type k = 0; k < K; k++) { classweight(i, k) = nodecw[idx * K + k]; classcount(i, k) = nodecn[idx * K + k]; } } octave_scalar_map T; T.assign ("Children", children); T.assign ("Parent", parent); T.assign ("CutPredictorIndex", cutvar); T.assign ("CutPoint", cutval); T.assign ("CutCategories", cutcats); T.assign ("IsBranchNode", isbranch); T.assign ("NodeSize", nodesize); T.assign ("NodeWeight", nodeweight); T.assign ("ClassWeight", classweight); T.assign ("ClassCount", classcount); T.assign ("NodeMean", nodemeanout); T.assign ("NodeError", nodeerror); T.assign ("NumNodes", octave_value (out)); return T; } // Send each row of X down the tree and report where it came to rest and what // that node answers with. A row is stopped by a node whose split predictor it // is missing, exactly as growth held such a row back rather than sending it to // a child; the two are the same rule, which is why they live in one file. A // categorical cut sends a row by the set its level is in, and stops a row // whose level is in neither, a level the node never saw, as MATLAB does. [[maybe_unused]] static void tree_descend (const Matrix& X, const Matrix& children, const ColumnVector& cutvar, const ColumnVector& cutpoint, const Matrix& value, Matrix& V, ColumnVector& node, const Cell *cats = nullptr) { const octave_idx_type n = X.rows (); const octave_idx_type m = value.columns (); const octave_idx_type nn = children.rows (); V.resize (n, m); node.resize (n); std::vector> catl (nn), catr (nn); if (cats) for (octave_idx_type i = 0; i < nn; i++) { if ((*cats)(i, 0).isempty ()) continue; const NDArray a = (*cats)(i, 0).array_value (); const NDArray b = (*cats)(i, 1).array_value (); catl[i].assign (a.data (), a.data () + a.numel ()); catr[i].assign (b.data (), b.data () + b.numel ()); std::sort (catl[i].begin (), catl[i].end ()); std::sort (catr[i].begin (), catr[i].end ()); } for (octave_idx_type i = 0; i < n; i++) { octave_idx_type at = 1; while (children(at - 1, 0) != 0) { const octave_idx_type v = static_cast (cutvar(at - 1)); const double x = X(i, v - 1); if (std::isnan (x)) break; if (! catl[at - 1].empty ()) { const octave_idx_type from = at; if (std::binary_search (catl[from - 1].begin (), catl[from - 1].end (), x)) at = static_cast (children(from - 1, 0)); else if (std::binary_search (catr[from - 1].begin (), catr[from - 1].end (), x)) at = static_cast (children(from - 1, 1)); else break; continue; } at = static_cast ((x < cutpoint(at - 1)) ? children(at - 1, 0) : children(at - 1, 1)); } node(i) = at; for (octave_idx_type k = 0; k < m; k++) V(i, k) = value(at - 1, k); } } statistics-release-2.1.2/src/treepredict.cc000066400000000000000000000213401527311104300207310ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include "tree.cpp" DEFUN_DLD (treepredict, args, nargout, "-*- texinfo -*-\n\ @deftypefn {statistics} {@var{V} =} treepredict (@var{X}, @var{Children}, @\n\ @var{CutPredictorIndex}, @var{CutPoint}, @var{Value})\n\ @deftypefnx {statistics} {@var{V} =} treepredict (@dots{}, @var{CutCategories})\n\ @deftypefnx {statistics} {[@var{V}, @var{node}] =} treepredict (@dots{})\n\ \n\ \n\ Evaluate a binary decision tree on new data.\n\ \n\ @code{@var{V} = treepredict (@var{X}, @var{Children},\n\ @var{CutPredictorIndex}, @var{CutPoint}, @var{Value})} sends each row of the\n\ @math{NxP} matrix @var{X} down the tree and returns the value of the node it\n\ comes to rest at. It is the prediction engine shared by\n\ @code{ClassificationTree} and @code{RegressionTree}, and is not meant to be\n\ called directly.\n\ \n\ @var{Children}, @var{CutPredictorIndex} and @var{CutPoint} are the node table\n\ returned by @code{treetrain}, a zero in @var{Children} marking a leaf.\n\ @var{Value} carries one row per node: the class probabilities of a\n\ classifier, in which case @var{V} is @math{NxK}, or the mean of a\n\ regression, in which case it is @math{Nx1}.\n\ \n\ @code{[@var{V}, @var{node}] = treepredict (@dots{})} also returns the index\n\ of the node each row came to rest at.\n\ \n\ A row is stopped by the first node whose split predictor it is missing, and\n\ takes that node's value, which is how such a row was held back rather than\n\ sent to a child while the tree was grown.\n\ \n\ @var{CutCategories}, the @math{Mx2} cell array @code{treetrain} returns,\n\ gives the levels a categorical cut sends left and right. A row goes the way\n\ its level is listed, and a row whose level is in neither list, one the node\n\ never saw, is stopped there like a missing value.\n\ \n\ @seealso{treetrain}\n\ @end deftypefn") { if (args.length () < 5 || args.length () > 6) error ("treepredict: invalid number of input arguments."); Matrix X = args(0).matrix_value (); Matrix children = args(1).matrix_value (); ColumnVector cutvar = args(2).column_vector_value (); ColumnVector cutpoint = args(3).column_vector_value (); Matrix value = args(4).matrix_value (); const octave_idx_type nn = children.rows (); if (nn < 1) error ("treepredict: the tree must hold at least one node."); if (children.columns () != 2) error ("treepredict: CHILDREN must have two columns."); if (cutvar.numel () != nn || cutpoint.numel () != nn || value.rows () != nn) error ("treepredict: the node table must be of one length throughout."); const octave_idx_type p = X.columns (); for (octave_idx_type i = 0; i < nn; i++) { if (children(i, 0) == 0) continue; octave_idx_type v = static_cast (cutvar(i)); if (v < 1 || v > p) error ("treepredict: a node cuts on a predictor X does not hold."); if (children(i, 0) < 1 || children(i, 0) > nn || children(i, 1) < 1 || children(i, 1) > nn) error ("treepredict: a node names a child outside the tree."); } Cell cats; const bool hascats = (args.length () == 6); if (hascats) { if (! args(5).iscell ()) error ("treepredict: CUTCATEGORIES must be a cell array."); cats = args(5).cell_value (); if (cats.rows () != nn || cats.columns () != 2) error ("treepredict: CUTCATEGORIES must have one row per node and " "two columns."); } Matrix V; ColumnVector node; tree_descend (X, children, cutvar, cutpoint, value, V, node, hascats ? &cats : nullptr); if (nargout > 1) return ovl (V, node); return ovl (V); } /* %!test %! ## Every row of the training data lands where it was counted %! load fisheriris %! y = grp2idx (species); %! o = struct ('NumClasses', 3, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', true, 'Prune', true); %! T = treetrain (meas, y, ones (150, 1) / 150, o); %! prob = T.ClassWeight ./ sum (T.ClassWeight, 2); %! [score, node] = treepredict (meas, T.Children, T.CutPredictorIndex, ... %! T.CutPoint, prob); %! assert_equal (size (score), [150, 3]); %! assert_equal (node(1:12)', 2 * ones (1, 12)); %! assert_equal (sum (score(:,1)), 50, 1e-12); %! leaf = T.Children(:,1) == 0; %! assert_equal (all (leaf(node)), true); %!test %! ## A row is stopped by the first node whose predictor it is missing %! load fisheriris %! y = grp2idx (species); %! o = struct ('NumClasses', 3, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', true, 'Prune', true); %! T = treetrain (meas, y, ones (150, 1) / 150, o); %! prob = T.ClassWeight ./ sum (T.ClassWeight, 2); %! q = meas([1, 51, 101, 71], :); %! q(1,3) = NaN; %! q(2,:) = NaN; %! [score, node] = treepredict (q, T.Children, T.CutPredictorIndex, ... %! T.CutPoint, prob); %! assert_equal (node', [1, 1, 5, 5]); %! assert_equal (score(1,:), prob(1,:)); %! assert_equal (score(3,:), [0, 1/46, 45/46], 1e-12); %!test %! ## The value of a regression leaf is its mean %! g = mod ((1:20)', 3); %! x = [(1:20)', g]; %! y = [ones(10,1); 5 * ones(10,1)]; %! o = struct ('NumClasses', 1, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 19, 'SplitCriterion', 'mse', ... %! 'MergeLeaves', true, 'Prune', true, 'QEToler', 1e-6); %! T = treetrain (x, y, ones (20, 1) / 20, o); %! [yfit, node] = treepredict (x, T.Children, T.CutPredictorIndex, ... %! T.CutPoint, T.NodeMean); %! assert_equal (size (yfit), [20, 1]); %! assert_equal (yfit, y, 1e-12); %! assert_equal (node', [2 * ones(1, 10), 3 * ones(1, 10)]); %!test %! ## A tree of one node answers with that node for every row %! v = [0.25, 0.75]; %! [score, node] = treepredict (rand (7, 3), [0, 0], 0, NaN, v); %! assert_equal (node, ones (7, 1)); %! assert_equal (score, repmat (v, 7, 1)); %!test %! ## One output returns the value alone %! v = [0.25, 0.75]; %! got = treepredict (rand (4, 2), [0, 0], 0, NaN, v); %! assert_equal (got, repmat (v, 4, 1)); %!test %! ## A value below the cut point goes left %! kids = [2, 3; 0, 0; 0, 0]; %! score = treepredict ([1; 3], kids, [1; 0; 0], [2; NaN; NaN], [0; 10; 20]); %! assert_equal (score, [10; 20]); %!test %! ## A categorical cut sends a row by its level, and stops an unknown one %! kids = [2, 3; 0, 0; 0, 0]; %! cats = {[1, 3], 2; [], []; [], []}; %! [v, node] = treepredict ([1; 2; 3; 4; NaN], kids, [1; 0; 0], ... %! [NaN; NaN; NaN], [0; 10; 20], cats); %! assert_equal (v', [10, 20, 10, 0, 0]); %! assert_equal (node', [2, 3, 2, 1, 1]); ## Test input validation %!error ... %! treepredict (1, 2, 3, 4); %!error ... %! treepredict (1, 2, 3, 4, 5, 6, 7); %!error ... %! treepredict (rand (3, 2), [0, 0], 0, NaN, 1, 2); %!error ... %! treepredict (rand (3, 2), [0, 0], 0, NaN, 1, cell (2, 2)); %!error ... %! treepredict (rand (3, 2), zeros (0, 2), [], [], zeros (0, 1)); %!error ... %! treepredict (rand (3, 2), [0, 0, 0], 0, NaN, 1); %!error ... %! treepredict (rand (3, 2), [0, 0], [0; 0], NaN, 1); %!error ... %! treepredict (rand (3, 2), [0, 0], 0, NaN, [1; 2]); %!error ... %! kids = [2, 3; 0, 0; 0, 0]; ... %! treepredict (rand (3, 2), kids, [5; 0; 0], [1; NaN; NaN], [0; 1; 2]); %!error ... %! kids = [2, 9; 0, 0; 0, 0]; ... %! treepredict (rand (3, 2), kids, [1; 0; 0], [1; NaN; NaN], [0; 1; 2]); */ statistics-release-2.1.2/src/treetrain.cc000066400000000000000000000547161527311104300204310ustar00rootroot00000000000000/* Copyright (C) 2026 Andreas Bertsatos This file is part of the statistics package for GNU Octave. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . */ #include #include #include #include #include #include "tree.cpp" DEFUN_DLD (treetrain, args, , "-*- texinfo -*-\n\ @deftypefn {statistics} {@var{Mdl} =} treetrain (@var{X}, @var{Y}, @var{W}, @\n\ @var{opts})\n\ \n\ \n\ Grow a binary decision tree by recursive partitioning.\n\ \n\ @code{@var{Mdl} = treetrain (@var{X}, @var{Y}, @var{W}, @var{opts})} grows a\n\ tree on the @math{NxP} predictor matrix @var{X} and returns it as a structure\n\ with one row per node. It is the fitting engine shared by\n\ @code{ClassificationTree} and @code{RegressionTree}, and is not meant to be\n\ called directly.\n\ \n\ @var{Y} is an @math{Nx1} vector of class indices in @code{1:K} when fitting a\n\ classifier and the response itself when fitting a regression. @var{W} is an\n\ @math{Nx1} vector of observation weights, already adjusted for any prior.\n\ \n\ @var{opts} is a structure carrying @qcode{NumClasses}, @qcode{MinParent},\n\ @qcode{MinLeaf}, @qcode{MaxSplits}, @qcode{SplitCriterion},\n\ @qcode{MergeLeaves} and, for a regression, @qcode{QEToler}.\n\ @qcode{SplitCriterion} is @qcode{\"gdi\"} or @qcode{\"deviance\"} for a\n\ classifier and @qcode{\"mse\"} for a regression, and it is what selects\n\ between the two.\n\ \n\ @qcode{NumVariablesToSample}, when present, is the number of predictors\n\ each node chooses its split from, drawn afresh at every node, and\n\ @qcode{Seed}, an integer from 0 to @math{2^32-1}, seeds that draw, so the\n\ same seed grows the same tree. A node whose predictors hold no valid split\n\ is a leaf. Without @qcode{NumVariablesToSample} every predictor is tried and\n\ nothing is drawn.\n\ \n\ @qcode{CategoricalPredictors}, when present, lists the columns of @var{X}\n\ that hold levels rather than numbers, and such a column is split into two\n\ sets of levels. A regression orders the levels by mean response and two\n\ classes by the probability of the first class. More classes search every\n\ partition when the node holds at most @qcode{MaxNumCategories} levels, 10\n\ by default, and otherwise take MATLAB's heuristics, as\n\ @qcode{AlgorithmForCategorical} selects: @qcode{\"auto\"} (default),\n\ @qcode{\"exact\"}, @qcode{\"pullleft\"}, @qcode{\"pca\"} or\n\ @qcode{\"ovabyclass\"}.\n\ \n\ The returned structure holds @qcode{Children}, @qcode{Parent},\n\ @qcode{CutPredictorIndex}, @qcode{CutPoint}, @qcode{CutCategories},\n\ @qcode{IsBranchNode}, @qcode{NodeSize}, @qcode{NodeWeight} and\n\ @qcode{NumNodes}, plus\n\ @qcode{ClassWeight} and @qcode{ClassCount} for a classifier or\n\ @qcode{NodeMean} and @qcode{NodeError} for a regression.\n\ Nodes are numbered as they are created, so a parent always precedes its\n\ children.\n\ \n\ The cost complexity pruning sequence is not built here. A tree outlives\n\ the data it was grown on, so the sequence is @code{__treeprune__}, which\n\ takes a node table and the risk the caller measures by.\n\ \n\ @seealso{treepredict}\n\ @end deftypefn") { if (args.length () != 4) error ("treetrain: invalid number of input arguments."); Matrix X = args(0).matrix_value (); ColumnVector yv = args(1).column_vector_value (); ColumnVector wv = args(2).column_vector_value (); octave_scalar_map opts = args(3).scalar_map_value (); if (yv.numel () != X.rows () || wv.numel () != X.rows ()) error ("treetrain: X, Y and W must have the same number of rows."); TreeOpts o; o.K = opts.contents ("NumClasses").idx_type_value (); o.minparent = opts.contents ("MinParent").idx_type_value (); o.minleaf = opts.contents ("MinLeaf").idx_type_value (); o.maxsplits = opts.contents ("MaxSplits").idx_type_value (); o.mergeleaves = opts.contents ("MergeLeaves").bool_value (); o.qetoler = opts.isfield ("QEToler") ? opts.contents ("QEToler").double_value () : 0.0; o.nvars = 0; if (opts.isfield ("NumVariablesToSample")) { double v = opts.contents ("NumVariablesToSample").double_value (); if (! (v >= 1.0 && v == std::floor (v))) error ("treetrain: NumVariablesToSample must be a positive integer."); if (v > X.columns ()) error ("treetrain: NumVariablesToSample must not exceed the number " "of predictors."); o.nvars = static_cast (v); } o.seed = 0; if (opts.isfield ("Seed")) { double v = opts.contents ("Seed").double_value (); if (! (v >= 0.0 && v <= 4294967295.0 && v == std::floor (v))) error ("treetrain: Seed must be an integer from 0 to 2^32-1."); o.seed = static_cast (v); } o.iscat.clear (); if (opts.isfield ("CategoricalPredictors")) { const NDArray c = opts.contents ("CategoricalPredictors").array_value (); if (c.numel () > 0) o.iscat.assign (X.columns (), false); for (octave_idx_type i = 0; i < c.numel (); i++) { const double v = c(i); if (! (v >= 1.0 && v <= X.columns () && v == std::floor (v))) error ("treetrain: CategoricalPredictors must hold indices of " "columns of X."); o.iscat[static_cast (v) - 1] = true; } } o.maxcat = 10.0; if (opts.isfield ("MaxNumCategories")) { const double v = opts.contents ("MaxNumCategories").double_value (); if (! (v >= 0.0 && (std::isinf (v) || v == std::floor (v)))) error ("treetrain: MaxNumCategories must be a non-negative integer."); o.maxcat = v; } o.catalg = CAT_AUTO; if (opts.isfield ("AlgorithmForCategorical")) { const std::string a = opts.contents ("AlgorithmForCategorical").string_value (); if (a == "auto") o.catalg = CAT_AUTO; else if (a == "exact") o.catalg = CAT_EXACT; else if (a == "pullleft") o.catalg = CAT_PULLLEFT; else if (a == "pca") o.catalg = CAT_PCA; else if (a == "ovabyclass") o.catalg = CAT_OVA; else error ("treetrain: unsupported AlgorithmForCategorical."); } std::string critname = opts.contents ("SplitCriterion").string_value (); if (critname == "gdi") o.crit = GDI; else if (critname == "deviance") o.crit = DEVIANCE; else if (critname == "mse") o.crit = MSE; else error ("treetrain: unsupported SplitCriterion."); if (o.minleaf < 1) error ("treetrain: MinLeaf must be a positive integer."); if (o.minparent < 1) error ("treetrain: MinParent must be a positive integer."); if (o.crit != MSE && o.K < 1) error ("treetrain: NumClasses must be a positive integer."); for (octave_idx_type i = 0; i < wv.numel (); i++) if (! (wv(i) >= 0.0)) error ("treetrain: W must hold non-negative weights."); return ovl (tree_build (X, yv, wv, o)); } /* %!test %! ## The iris tree, grown and then merged, as MATLAB grows it %! load fisheriris %! y = grp2idx (species); %! o = struct ('NumClasses', 3, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', false); %! T = treetrain (meas, y, ones (150, 1) / 150, o); %! assert_equal (T.NumNodes, 11); %! assert_equal (T.NodeSize', [150, 50, 100, 54, 46, 48, 6, 3, 43, 47, 1]); %! assert_equal (T.CutPredictorIndex', [3, 0, 4, 3, 3, 4, 0, 0, 0, 0, 0]); %! assert_equal (T.CutPoint([1, 3, 4, 5, 6])', [2.45, 1.75, 4.95, 4.85, 1.65]); %!test %! ## MergeLeaves collapses the one pair that lowers no misclassification %! load fisheriris %! y = grp2idx (species); %! o = struct ('NumClasses', 3, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', true); %! T = treetrain (meas, y, ones (150, 1) / 150, o); %! assert_equal (T.NumNodes, 9); %! assert_equal (T.NodeSize', [150, 50, 100, 54, 46, 48, 6, 47, 1]); %! assert_equal (T.Parent', [0, 1, 1, 3, 3, 4, 4, 6, 6]); %!test %! ## A deeper tree, grown until no node can be split further %! load fisheriris %! y = grp2idx (species); %! o = struct ('NumClasses', 3, 'MinParent', 2, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', true); %! T = treetrain (meas, y, ones (150, 1) / 150, o); %! assert_equal (T.NumNodes, 17); %! assert_equal (T.NodeSize', [150, 50, 100, 54, 46, 48, 6, 3, 43, 47, 1, ... %! 3, 3, 1, 2, 2, 1]); %!test %! ## A row missing the split predictor descends to neither child %! load fisheriris %! y = grp2idx (species); %! x = meas; %! x(51:60, 4) = NaN; %! o = struct ('NumClasses', 3, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', false); %! T = treetrain (x, y, ones (150, 1) / 150, o); %! assert_equal (T.NodeSize', [150, 50, 100, 45, 55, 35, 1, 8, 46, 3, 43]); %! assert_equal (T.NodeSize(4) - T.NodeSize(6) - T.NodeSize(7), 9); %!test %! ## A row missing every predictor is dropped outright %! load fisheriris %! y = grp2idx (species); %! x = meas; %! x(1:5, :) = NaN; %! o = struct ('NumClasses', 3, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', false); %! T = treetrain (x, y, ones (150, 1) / 150, o); %! assert_equal (T.NodeSize(1), 145); %! assert_equal (T.NodeSize', [145, 45, 100, 54, 46, 48, 6, 3, 43, 47, 1]); %!test %! ## A predictor that is entirely missing is simply never chosen %! load fisheriris %! y = grp2idx (species); %! x = meas; %! x(:, 2) = NaN; %! o = struct ('NumClasses', 3, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', false); %! T = treetrain (x, y, ones (150, 1) / 150, o); %! assert_equal (T.CutPredictorIndex', [3, 0, 4, 3, 3, 4, 0, 0, 0, 0, 0]); %!test %! ## The split is judged on the rows a predictor has, scaled by their share: %! ## x1 is known for every row and wins, x2 is known for ten and does not %! y = [1;1;1;1;1;1;1;1;1;2;1;2;2;2;2;2;2;2;2;2]; %! x2 = NaN (20, 1); %! x2(1:5) = (1:5)'; %! x2(12:16) = (6:10)'; %! o = struct ('NumClasses', 2, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 19, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', false); %! T = treetrain ([(1:20)', x2], y, ones (20, 1) / 20, o); %! assert_equal (T.CutPredictorIndex(1), 1); %! assert_equal (T.CutPoint(1), 9.5); %! assert_equal (T.NodeSize', [20, 9, 11, 2, 9]); %!test %! ## MinParent holds a node together and MaxSplits stops the tree %! load fisheriris %! y = grp2idx (species); %! o = struct ('NumClasses', 3, 'MinParent', 60, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', false); %! assert_equal (treetrain (meas, y, ones (150, 1) / 150, o).NumNodes, 5); %! o.MinParent = 10; %! o.MaxSplits = 2; %! assert_equal (treetrain (meas, y, ones (150, 1) / 150, o).NumNodes, 5); %!test %! ## ClassCount counts rows and ClassWeight weighs them, which are the same %! ## thing only while the weights are %! load fisheriris %! y = grp2idx (species); %! o = struct ('NumClasses', 3, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', true); %! count = [50, 50, 0, 0, 0, 0, 0, 0, 0; ... %! 50, 0, 50, 49, 1, 47, 2, 47, 0; ... %! 50, 0, 50, 5, 45, 1, 4, 0, 1]; %! T = treetrain (meas, y, ones (150, 1) / 150, o); %! assert_equal (T.ClassCount', count); %! assert_equal (sum (T.ClassCount, 2), T.NodeSize); %! w = [3 * ones(50, 1); ones(100, 1)]; %! w = w / sum (w); %! W = treetrain (meas, y, w, o); %! assert_equal (W.NodeSize', [150, 50, 100, 54, 46, 48, 6, 47, 1]); %! assert_equal (W.ClassCount', count); %! prob = W.ClassWeight ./ sum (W.ClassWeight, 2); %! assert_equal (prob(1,:), [0.6, 0.2, 0.2], 1e-12); %!test %! ## A regression carries a leaf value in place of class counts %! g = zeros (20, 1); %! x = [(1:20)', g]; %! y = [ones(10,1); 5 * ones(10,1)]; %! o = struct ('NumClasses', 1, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 19, 'SplitCriterion', 'mse', ... %! 'MergeLeaves', true, 'QEToler', 1e-6); %! T = treetrain (x, y, ones (20, 1) / 20, o); %! assert_equal (isempty (T.ClassCount), true); %! assert_equal (isempty (T.ClassWeight), true); %!test %! ## A regression tree and its leaf values %! g = mod ((1:20)', 3); %! x = [(1:20)', g]; %! y = [ones(10,1); 5 * ones(10,1)]; %! o = struct ('NumClasses', 1, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 19, 'SplitCriterion', 'mse', ... %! 'MergeLeaves', true, 'QEToler', 1e-6); %! T = treetrain (x, y, ones (20, 1) / 20, o); %! assert_equal (T.NumNodes, 3); %! assert_equal (T.NodeSize', [20, 10, 10]); %! assert_equal (T.NodeMean', [3, 1, 5], 1e-12); %! assert_equal (T.NodeError', [4, 0, 0], 1e-12); %!test %! ## QEToler stops a node whose squared error is a small share of the root's %! g = mod ((1:40)', 7); %! x = [(1:40)', g]; %! y = (1:40)'; %! o = struct ('NumClasses', 1, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 39, 'SplitCriterion', 'mse', ... %! 'MergeLeaves', true, 'QEToler', 1e-6); %! loose = o; %! loose.QEToler = 0.05; %! assert_equal (treetrain (x, y, ones (40, 1) / 40, loose).NumNodes ... %! < treetrain (x, y, ones (40, 1) / 40, o).NumNodes, true); %!test %! ## A single class, a single row and identical rows are each one leaf %! o = struct ('NumClasses', 2, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 19, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', true); %! w = ones (20, 1) / 20; %! assert_equal (treetrain (rand (20, 2), ones (20, 1), w, o).NumNodes, 1); %! assert_equal (treetrain ([1, 2], 1, 1, o).NumNodes, 1); %! y = [ones(10,1); 2*ones(10,1)]; %! assert_equal (treetrain (ones (20, 2), y, w, o).NumNodes, 1); ## Test input validation %!test %! ## Sampling every predictor draws nothing and grows the unsampled tree %! load fisheriris %! o = struct ('NumClasses', 3, 'MinParent', 2, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', false); %! w = ones (150, 1) / 150; %! T = treetrain (meas, grp2idx (species), w, o); %! o.NumVariablesToSample = 4; %! o.Seed = 12345; %! assert_equal (isequaln (treetrain (meas, grp2idx (species), w, o), T), true); %!test %! ## The same seed grows the same tree %! load fisheriris %! o = struct ('NumClasses', 3, 'MinParent', 2, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', false, 'NumVariablesToSample', 1, 'Seed', 7); %! w = ones (150, 1) / 150; %! A = treetrain (meas, grp2idx (species), w, o); %! B = treetrain (meas, grp2idx (species), w, o); %! assert_equal (isequaln (A, B), true); %!test %! ## Different seeds grow different trees %! load fisheriris %! o = struct ('NumClasses', 3, 'MinParent', 2, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', false, 'NumVariablesToSample', 1, 'Seed', 7); %! w = ones (150, 1) / 150; %! assert_equal (treetrain (meas, grp2idx (species), w, o).NumNodes, 23); %! o.Seed = 8; %! assert_equal (treetrain (meas, grp2idx (species), w, o).NumNodes, 25); %!test %! ## A node whose sampled predictor holds no split is a leaf %! load fisheriris %! o = struct ('NumClasses', 3, 'MinParent', 2, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', false, 'NumVariablesToSample', 1, 'Seed', 0); %! X = [ones(150, 1), meas(:, 3)]; %! T = treetrain (X, grp2idx (species), ones (150, 1) / 150, o); %! assert_equal (T.NumNodes, 1); %!test %! ## A predictor that cannot split is never cut, however the draw falls %! load fisheriris %! o = struct ('NumClasses', 3, 'MinParent', 2, 'MinLeaf', 1, ... %! 'MaxSplits', 149, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', false, 'NumVariablesToSample', 1, 'Seed', 14); %! X = [ones(150, 1), meas(:, 3)]; %! T = treetrain (X, grp2idx (species), ones (150, 1) / 150, o); %! assert_equal (T.NumNodes, 15); %! cut = T.CutPredictorIndex(logical (T.IsBranchNode)); %! assert_equal (all (cut == 2), true); %!test %! ## A sampled regression tree is reproduced by its seed %! load fisheriris %! o = struct ('NumClasses', 0, 'MinParent', 10, 'MinLeaf', 5, ... %! 'MaxSplits', 149, 'SplitCriterion', 'mse', ... %! 'MergeLeaves', false, 'NumVariablesToSample', 1, 'Seed', 3); %! w = ones (150, 1) / 150; %! A = treetrain (meas(:, 2:4), meas(:, 1), w, o); %! B = treetrain (meas(:, 2:4), meas(:, 1), w, o); %! assert_equal (isequaln (A, B), true); %!shared o %! o = struct ('NumClasses', 2, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 19, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', true); %!error treetrain (1, 2, 3); %!error ... %! treetrain (1, 2, 3, o, 5); %!error ... %! treetrain (rand (10, 2), ones (9, 1), ones (10, 1), o); %!error ... %! treetrain (rand (10, 2), ones (10, 1), ones (9, 1), o); %!error ... %! bad = setfield (o, 'SplitCriterion', 'twoing'); ... %! treetrain (rand (10, 2), ones (10, 1), ones (10, 1), bad); %!error ... %! bad = setfield (o, 'MinLeaf', 0); ... %! treetrain (rand (10, 2), ones (10, 1), ones (10, 1), bad); %!error ... %! bad = setfield (o, 'MinParent', 0); ... %! treetrain (rand (10, 2), ones (10, 1), ones (10, 1), bad); %!error ... %! bad = setfield (o, 'NumClasses', 0); ... %! treetrain (rand (10, 2), ones (10, 1), ones (10, 1), bad); %!error ... %! treetrain (rand (10, 2), ones (10, 1), [-1; ones(9, 1)], o); %!error ... %! treetrain (rand (10, 2), 5 * ones (10, 1), ones (10, 1), o); %!error ... %! treetrain (rand (10, 2), zeros (10, 1), ones (10, 1), o); %!error ... %! bad = setfield (o, 'NumVariablesToSample', 0); ... %! treetrain (rand (10, 2), ones (10, 1), ones (10, 1), bad); %!error ... %! bad = setfield (o, 'NumVariablesToSample', 1.5); ... %! treetrain (rand (10, 2), ones (10, 1), ones (10, 1), bad); %!error ... %! bad = setfield (o, 'NumVariablesToSample', 3); ... %! treetrain (rand (10, 2), ones (10, 1), ones (10, 1), bad); %!error ... %! bad = setfield (o, 'Seed', -1); ... %! treetrain (rand (10, 2), ones (10, 1), ones (10, 1), bad); %!error ... %! bad = setfield (o, 'Seed', 2.5); ... %! treetrain (rand (10, 2), ones (10, 1), ones (10, 1), bad); %!error ... %! bad = setfield (o, 'Seed', 2^32); ... %! treetrain (rand (10, 2), ones (10, 1), ones (10, 1), bad); %!test %! ## MATLAB parity: two classes order the levels and split them in two %! k = (0:79)'; %! c = mod (k, 4) + 1; %! j = floor (k / 4); %! y = (c == 1) | (c == 2 & mod (j, 4) != 0) | (c == 3 & mod (j, 4) == 0); %! o = struct ('NumClasses', 2, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 79, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', true, 'CategoricalPredictors', 1); %! T = treetrain ([c, mod(k * 7, 10)], double (y) + 1, ones (80, 1) / 80, o); %! assert_equal (T.NumNodes, 3); %! assert_equal (T.CutCategories(1,:), {[1, 2], [3, 4]}); %! assert_equal (isnan (T.CutPoint(1)), true); %!test %! ## MATLAB parity: a regression orders the levels by their mean response %! k = (0:99)'; %! c = mod (k, 5) + 1; %! means = [3, 1, 4, 1.5, 5]; %! y = means(c)' + 0.1 * sin (k); %! o = struct ('NumClasses', 1, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 99, 'SplitCriterion', 'mse', ... %! 'MergeLeaves', true, 'QEToler', 1e-6, ... %! 'CategoricalPredictors', 1); %! T = treetrain ([c, mod(k * 3, 8)], y, ones (100, 1) / 100, o); %! assert_equal (T.NumNodes, 39); %! assert_equal (T.CutCategories(1,:), {[2, 4], [1, 3, 5]}); %!test %! ## Every named algorithm finds a split of three classes %! k = (0:119)'; %! c = mod (k, 6) + 1; %! y = mod (c + floor (k / 12), 3) + 1; %! for a = {'exact', 'pullleft', 'pca', 'ovabyclass'} %! o = struct ('NumClasses', 3, 'MinParent', 10, 'MinLeaf', 1, ... %! 'MaxSplits', 1, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', false, 'CategoricalPredictors', 1, ... %! 'AlgorithmForCategorical', a{1}); %! T = treetrain (c, y, ones (120, 1) / 120, o); %! assert_equal (sort ([T.CutCategories{1,:}]), 1:6); %! endfor %!error ... %! o = struct ('NumClasses', 2, 'MinParent', 1, 'MinLeaf', 1, ... %! 'MaxSplits', 1, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', false, 'CategoricalPredictors', 3); ... %! treetrain (ones (4, 2), [1; 1; 2; 2], ones (4, 1), o); %!error ... %! o = struct ('NumClasses', 2, 'MinParent', 1, 'MinLeaf', 1, ... %! 'MaxSplits', 1, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', false, 'MaxNumCategories', -1); ... %! treetrain (ones (4, 2), [1; 1; 2; 2], ones (4, 1), o); %!error ... %! o = struct ('NumClasses', 2, 'MinParent', 1, 'MinLeaf', 1, ... %! 'MaxSplits', 1, 'SplitCriterion', 'gdi', ... %! 'MergeLeaves', false, 'AlgorithmForCategorical', 'bogus'); ... %! treetrain (ones (4, 2), [1; 1; 2; 2], ones (4, 1), o); */